summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDrew Blessing <drew@gitlab.com>2018-07-11 15:58:06 -0500
committerDrew Blessing <drew@gitlab.com>2018-07-11 15:58:06 -0500
commit1ed2bf9a55c6018a8cb303a2fc9b10ad8a3ea72c (patch)
treee99de4b9ae74225946126a846992ca457b0232df
parent83da6325c2112a569a75f3be16fc2bdeb64246db (diff)
parentba38931d90b6149e7bf1176dc5075b92a51466ae (diff)
downloadgitlab-ce-1ed2bf9a55c6018a8cb303a2fc9b10ad8a3ea72c.tar.gz
Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce
-rw-r--r--.codeclimate.yml2
-rw-r--r--.eslintrc56
-rw-r--r--.eslintrc.yml71
-rw-r--r--.flayignore18
-rw-r--r--.gitattributes1
-rw-r--r--.gitignore4
-rw-r--r--.gitlab-ci.yml109
-rw-r--r--.gitlab/issue_templates/Feature Proposal.md9
-rw-r--r--.gitlab/issue_templates/Feature proposal.md15
-rw-r--r--.gitlab/issue_templates/Research proposal.md (renamed from .gitlab/issue_templates/Research Proposal.md)0
-rw-r--r--.gitlab/issue_templates/Security Developer Workflow.md70
-rw-r--r--.gitlab/issue_templates/Security developer workflow.md71
-rw-r--r--.gitlab/merge_request_templates/Database Changes.md50
-rw-r--r--.gitlab/merge_request_templates/Database changes.md50
-rw-r--r--.nvmrc2
-rw-r--r--.rubocop_todo.yml17
-rw-r--r--.ruby-version2
-rw-r--r--.scss-lint.yml3
-rw-r--r--CHANGELOG.md366
-rw-r--r--CONTRIBUTING.md117
-rw-r--r--Dangerfile6
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_PAGES_VERSION2
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile53
-rw-r--r--Gemfile.lock209
-rw-r--r--Gemfile.rails5.lock413
-rw-r--r--INSTALLATION_TYPE1
-rw-r--r--PROCESS.md59
-rw-r--r--README.md15
-rwxr-xr-xRakefile4
-rw-r--r--VERSION2
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_canceled.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_created.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_failed.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_manual.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_not_found.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_pending.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_running.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_skipped.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_success.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_warning.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_canceled.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_canceled.pngbin0 -> 864 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_created.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_created.pngbin0 -> 889 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_failed.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_failed.pngbin0 -> 1015 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_manual.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_manual.pngbin0 -> 1067 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_not_found.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_not_found.pngbin0 -> 945 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_pending.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_pending.pngbin0 -> 919 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_running.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_running.pngbin0 -> 1077 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_skipped.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_skipped.pngbin0 -> 923 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_success.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_success.pngbin0 -> 1044 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_warning.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_warning.pngbin0 -> 830 bytes
-rw-r--r--app/assets/images/favicon-blue.icobin5430 -> 0 bytes
-rw-r--r--app/assets/images/favicon-blue.pngbin0 -> 1522 bytes
-rw-r--r--app/assets/images/favicon-yellow.icobin5430 -> 0 bytes
-rw-r--r--app/assets/images/favicon-yellow.pngbin0 -> 1667 bytes
-rw-r--r--app/assets/images/favicon.icobin5430 -> 0 bytes
-rw-r--r--app/assets/images/favicon.pngbin0 -> 1611 bytes
-rw-r--r--app/assets/javascripts/activities.js2
-rw-r--r--app/assets/javascripts/ajax_loading_spinner.js2
-rw-r--r--app/assets/javascripts/api.js55
-rw-r--r--app/assets/javascripts/autosave.js6
-rw-r--r--app/assets/javascripts/awards_handler.js199
-rw-r--r--app/assets/javascripts/badges/components/badge.vue18
-rw-r--r--app/assets/javascripts/badges/components/badge_form.vue20
-rw-r--r--app/assets/javascripts/badges/components/badge_list.vue10
-rw-r--r--app/assets/javascripts/badges/components/badge_list_row.vue10
-rw-r--r--app/assets/javascripts/badges/components/badge_settings.vue2
-rw-r--r--app/assets/javascripts/behaviors/copy_to_clipboard.js6
-rw-r--r--app/assets/javascripts/behaviors/markdown/copy_as_gfm.js42
-rw-r--r--app/assets/javascripts/behaviors/quick_submit.js2
-rw-r--r--app/assets/javascripts/behaviors/requires_input.js4
-rw-r--r--app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js8
-rw-r--r--app/assets/javascripts/blob/balsamiq_viewer.js2
-rw-r--r--app/assets/javascripts/blob/pdf/index.js1
-rw-r--r--app/assets/javascripts/blob/sketch/index.js2
-rw-r--r--app/assets/javascripts/blob/stl_viewer.js2
-rw-r--r--app/assets/javascripts/blob/viewer/index.js4
-rw-r--r--app/assets/javascripts/blob_edit/blob_bundle.js3
-rw-r--r--app/assets/javascripts/boards/components/board.js89
-rw-r--r--app/assets/javascripts/boards/components/board_blank_state.vue4
-rw-r--r--app/assets/javascripts/boards/components/board_card.vue130
-rw-r--r--app/assets/javascripts/boards/components/board_delete.js9
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue55
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.vue24
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js67
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.js195
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.vue202
-rw-r--r--app/assets/javascripts/boards/components/modal/empty_state.js69
-rw-r--r--app/assets/javascripts/boards/components/modal/empty_state.vue73
-rw-r--r--app/assets/javascripts/boards/components/modal/footer.js82
-rw-r--r--app/assets/javascripts/boards/components/modal/footer.vue92
-rw-r--r--app/assets/javascripts/boards/components/modal/header.js79
-rw-r--r--app/assets/javascripts/boards/components/modal/header.vue82
-rw-r--r--app/assets/javascripts/boards/components/modal/index.js171
-rw-r--r--app/assets/javascripts/boards/components/modal/index.vue178
-rw-r--r--app/assets/javascripts/boards/components/modal/list.js158
-rw-r--r--app/assets/javascripts/boards/components/modal/list.vue161
-rw-r--r--app/assets/javascripts/boards/components/modal/lists_dropdown.js54
-rw-r--r--app/assets/javascripts/boards/components/modal/lists_dropdown.vue56
-rw-r--r--app/assets/javascripts/boards/components/modal/tabs.js46
-rw-r--r--app/assets/javascripts/boards/components/modal/tabs.vue49
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js3
-rw-r--r--app/assets/javascripts/boards/components/project_select.vue112
-rw-r--r--app/assets/javascripts/boards/components/sidebar/remove_issue.js73
-rw-r--r--app/assets/javascripts/boards/components/sidebar/remove_issue.vue91
-rw-r--r--app/assets/javascripts/boards/filtered_search_boards.js1
-rw-r--r--app/assets/javascripts/boards/filters/due_date_filters.js5
-rw-r--r--app/assets/javascripts/boards/index.js14
-rw-r--r--app/assets/javascripts/boards/mixins/sortable_default_options.js1
-rw-r--r--app/assets/javascripts/boards/models/assignee.js12
-rw-r--r--app/assets/javascripts/boards/models/issue.js3
-rw-r--r--app/assets/javascripts/boards/models/list.js152
-rw-r--r--app/assets/javascripts/boards/models/milestone.js2
-rw-r--r--app/assets/javascripts/boards/services/board_service.js10
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js28
-rw-r--r--app/assets/javascripts/boards/stores/modal_store.js2
-rw-r--r--app/assets/javascripts/build_artifacts.js2
-rw-r--r--app/assets/javascripts/ci_variable_list/ci_variable_list.js5
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js9
-rw-r--r--app/assets/javascripts/clusters/components/application_row.vue20
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue80
-rw-r--r--app/assets/javascripts/clusters/constants.js1
-rw-r--r--app/assets/javascripts/clusters/services/clusters_service.js5
-rw-r--r--app/assets/javascripts/clusters/stores/clusters_store.js16
-rw-r--r--app/assets/javascripts/commit/image_file.js8
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.vue4
-rw-r--r--app/assets/javascripts/commits.js2
-rw-r--r--app/assets/javascripts/commons/bootstrap.js10
-rw-r--r--app/assets/javascripts/compare_autocomplete.js6
-rw-r--r--app/assets/javascripts/confirm_danger_modal.js5
-rw-r--r--app/assets/javascripts/create_merge_request_dropdown.js71
-rw-r--r--app/assets/javascripts/cycle_analytics/components/banner.vue2
-rw-r--r--app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue6
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_component.vue2
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_review_component.vue4
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue4
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_test_component.vue2
-rw-r--r--app/assets/javascripts/deploy_keys/components/action_btn.vue2
-rw-r--r--app/assets/javascripts/deploy_keys/components/app.vue8
-rw-r--r--app/assets/javascripts/deploy_keys/components/key.vue30
-rw-r--r--app/assets/javascripts/deploy_keys/components/keys_panel.vue2
-rw-r--r--app/assets/javascripts/diff_notes/components/comment_resolve_btn.js5
-rw-r--r--app/assets/javascripts/diff_notes/components/diff_note_avatars.js153
-rw-r--r--app/assets/javascripts/diff_notes/components/jump_to_discussion.js18
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_btn.js141
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_count.js10
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js27
-rw-r--r--app/assets/javascripts/diff_notes/diff_notes_bundle.js31
-rw-r--r--app/assets/javascripts/diff_notes/mixins/discussion.js6
-rw-r--r--app/assets/javascripts/diff_notes/models/discussion.js2
-rw-r--r--app/assets/javascripts/diff_notes/models/note.js2
-rw-r--r--app/assets/javascripts/diff_notes/services/resolve.js40
-rw-r--r--app/assets/javascripts/diff_notes/stores/comments.js2
-rw-r--r--app/assets/javascripts/diffs/components/app.vue217
-rw-r--r--app/assets/javascripts/diffs/components/changed_files.vue184
-rw-r--r--app/assets/javascripts/diffs/components/changed_files_dropdown.vue126
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions.vue55
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions_dropdown.vue165
-rw-r--r--app/assets/javascripts/diffs/components/diff_content.vue62
-rw-r--r--app/assets/javascripts/diffs/components/diff_discussions.vue37
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue190
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue265
-rw-r--r--app/assets/javascripts/diffs/components/diff_gutter_avatars.vue105
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_gutter_content.vue203
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_note_form.vue115
-rw-r--r--app/assets/javascripts/diffs/components/diff_table_cell.vue137
-rw-r--r--app/assets/javascripts/diffs/components/edit_button.vue42
-rw-r--r--app/assets/javascripts/diffs/components/hidden_files_warning.vue51
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_comment_row.vue69
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_table_row.vue105
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_view.vue79
-rw-r--r--app/assets/javascripts/diffs/components/no_changes.vue49
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue123
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_table_row.vue146
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_view.vue113
-rw-r--r--app/assets/javascripts/diffs/constants.js27
-rw-r--r--app/assets/javascripts/diffs/index.js41
-rw-r--r--app/assets/javascripts/diffs/mixins/changed_files.js38
-rw-r--r--app/assets/javascripts/diffs/store/actions.js113
-rw-r--r--app/assets/javascripts/diffs/store/getters.js60
-rw-r--r--app/assets/javascripts/diffs/store/modules/diff_state.js18
-rw-r--r--app/assets/javascripts/diffs/store/modules/index.js12
-rw-r--r--app/assets/javascripts/diffs/store/mutation_types.js10
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js75
-rw-r--r--app/assets/javascripts/diffs/store/utils.js175
-rw-r--r--app/assets/javascripts/dispatcher.js8
-rw-r--r--app/assets/javascripts/due_date_select.js45
-rw-r--r--app/assets/javascripts/environments/components/container.vue6
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.vue99
-rw-r--r--app/assets/javascripts/environments/components/environment_external_url.vue57
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue895
-rw-r--r--app/assets/javascripts/environments/components/environment_monitoring.vue55
-rw-r--r--app/assets/javascripts/environments/components/environment_rollback.vue98
-rw-r--r--app/assets/javascripts/environments/components/environment_stop.vue110
-rw-r--r--app/assets/javascripts/environments/components/environment_terminal_button.vue57
-rw-r--r--app/assets/javascripts/environments/components/environments_app.vue8
-rw-r--r--app/assets/javascripts/environments/components/stop_environment_modal.vue92
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_view.vue12
-rw-r--r--app/assets/javascripts/environments/mixins/environments_mixin.js22
-rw-r--r--app/assets/javascripts/environments/services/environments_service.js2
-rw-r--r--app/assets/javascripts/environments/stores/environments_store.js4
-rw-r--r--app/assets/javascripts/feature_highlight/feature_highlight.js2
-rw-r--r--app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue2
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_utils.js6
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js2
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js4
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js4
-rw-r--r--app/assets/javascripts/filtered_search/recent_searches_root.js2
-rw-r--r--app/assets/javascripts/frequent_items/components/app.vue122
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_list.vue78
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue117
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_mixin.js23
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue55
-rw-r--r--app/assets/javascripts/frequent_items/constants.js38
-rw-r--r--app/assets/javascripts/frequent_items/event_hub.js (renamed from app/assets/javascripts/projects_dropdown/event_hub.js)0
-rw-r--r--app/assets/javascripts/frequent_items/index.js69
-rw-r--r--app/assets/javascripts/frequent_items/store/actions.js81
-rw-r--r--app/assets/javascripts/frequent_items/store/getters.js4
-rw-r--r--app/assets/javascripts/frequent_items/store/index.js16
-rw-r--r--app/assets/javascripts/frequent_items/store/mutation_types.js9
-rw-r--r--app/assets/javascripts/frequent_items/store/mutations.js71
-rw-r--r--app/assets/javascripts/frequent_items/store/state.js8
-rw-r--r--app/assets/javascripts/frequent_items/utils.js49
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js24
-rw-r--r--app/assets/javascripts/gl_dropdown.js23
-rw-r--r--app/assets/javascripts/gl_field_error.js10
-rw-r--r--app/assets/javascripts/gl_form.js22
-rw-r--r--app/assets/javascripts/group_label_subscription.js18
-rw-r--r--app/assets/javascripts/groups/components/app.vue7
-rw-r--r--app/assets/javascripts/groups/components/group_item.vue12
-rw-r--r--app/assets/javascripts/groups/components/item_actions.vue4
-rw-r--r--app/assets/javascripts/groups/components/item_stats.vue22
-rw-r--r--app/assets/javascripts/groups/components/item_stats_value.vue2
-rw-r--r--app/assets/javascripts/groups/index.js4
-rw-r--r--app/assets/javascripts/ide/components/activity_bar.vue44
-rw-r--r--app/assets/javascripts/ide/components/changed_file_icon.vue2
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/actions.vue26
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/form.vue26
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list.vue77
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue9
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_item.vue40
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/message_field.vue18
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue18
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue34
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue8
-rw-r--r--app/assets/javascripts/ide/components/editor_mode_dropdown.vue8
-rw-r--r--app/assets/javascripts/ide/components/error_message.vue69
-rw-r--r--app/assets/javascripts/ide/components/external_link.vue41
-rw-r--r--app/assets/javascripts/ide/components/file_finder/index.vue18
-rw-r--r--app/assets/javascripts/ide/components/file_finder/item.vue6
-rw-r--r--app/assets/javascripts/ide/components/ide.vue23
-rw-r--r--app/assets/javascripts/ide/components/ide_file_buttons.vue84
-rw-r--r--app/assets/javascripts/ide/components/ide_review.vue19
-rw-r--r--app/assets/javascripts/ide/components/ide_side_bar.vue95
-rw-r--r--app/assets/javascripts/ide/components/ide_status_bar.vue48
-rw-r--r--app/assets/javascripts/ide/components/ide_tree_list.vue4
-rw-r--r--app/assets/javascripts/ide/components/jobs/detail.vue137
-rw-r--r--app/assets/javascripts/ide/components/jobs/detail/description.vue47
-rw-r--r--app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue66
-rw-r--r--app/assets/javascripts/ide/components/jobs/item.vue44
-rw-r--r--app/assets/javascripts/ide/components/jobs/list.vue45
-rw-r--r--app/assets/javascripts/ide/components/jobs/stage.vue112
-rw-r--r--app/assets/javascripts/ide/components/merge_requests/dropdown.vue63
-rw-r--r--app/assets/javascripts/ide/components/merge_requests/info.vue43
-rw-r--r--app/assets/javascripts/ide/components/merge_requests/item.vue63
-rw-r--r--app/assets/javascripts/ide/components/merge_requests/list.vue132
-rw-r--r--app/assets/javascripts/ide/components/mr_file_icon.vue4
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/index.vue14
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/modal.vue26
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/upload.vue3
-rw-r--r--app/assets/javascripts/ide/components/panes/right.vue104
-rw-r--r--app/assets/javascripts/ide/components/pipelines/list.vue146
-rw-r--r--app/assets/javascripts/ide/components/repo_commit_section.vue31
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue71
-rw-r--r--app/assets/javascripts/ide/components/repo_file.vue53
-rw-r--r--app/assets/javascripts/ide/components/repo_file_status_icon.vue2
-rw-r--r--app/assets/javascripts/ide/components/repo_loading_file.vue6
-rw-r--r--app/assets/javascripts/ide/components/repo_tab.vue10
-rw-r--r--app/assets/javascripts/ide/components/repo_tabs.vue2
-rw-r--r--app/assets/javascripts/ide/components/resizable_panel.vue4
-rw-r--r--app/assets/javascripts/ide/constants.js18
-rw-r--r--app/assets/javascripts/ide/ide_router.js10
-rw-r--r--app/assets/javascripts/ide/index.js10
-rw-r--r--app/assets/javascripts/ide/lib/common/model.js17
-rw-r--r--app/assets/javascripts/ide/lib/common/model_manager.js5
-rw-r--r--app/assets/javascripts/ide/lib/diff/controller.js4
-rw-r--r--app/assets/javascripts/ide/lib/diff/diff_worker.js4
-rw-r--r--app/assets/javascripts/ide/lib/editor.js36
-rw-r--r--app/assets/javascripts/ide/lib/editor_options.js1
-rw-r--r--app/assets/javascripts/ide/lib/themes/gl_theme.js1
-rw-r--r--app/assets/javascripts/ide/monaco_loader.js16
-rw-r--r--app/assets/javascripts/ide/services/index.js48
-rw-r--r--app/assets/javascripts/ide/stores/actions.js9
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js35
-rw-r--r--app/assets/javascripts/ide/stores/actions/merge_request.js50
-rw-r--r--app/assets/javascripts/ide/stores/actions/project.js101
-rw-r--r--app/assets/javascripts/ide/stores/actions/tree.js64
-rw-r--r--app/assets/javascripts/ide/stores/getters.js5
-rw-r--r--app/assets/javascripts/ide/stores/index.js25
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/actions.js88
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/getters.js17
-rw-r--r--app/assets/javascripts/ide/stores/modules/merge_requests/actions.js59
-rw-r--r--app/assets/javascripts/ide/stores/modules/merge_requests/constants.js10
-rw-r--r--app/assets/javascripts/ide/stores/modules/merge_requests/getters.js4
-rw-r--r--app/assets/javascripts/ide/stores/modules/merge_requests/index.js12
-rw-r--r--app/assets/javascripts/ide/stores/modules/merge_requests/mutation_types.js5
-rw-r--r--app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js26
-rw-r--r--app/assets/javascripts/ide/stores/modules/merge_requests/state.js13
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/actions.js164
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/constants.js4
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/getters.js21
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js8
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/mutations.js86
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/state.js3
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/utils.js11
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js9
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js20
-rw-r--r--app/assets/javascripts/ide/stores/mutations/file.js15
-rw-r--r--app/assets/javascripts/ide/stores/mutations/tree.js5
-rw-r--r--app/assets/javascripts/ide/stores/state.js3
-rw-r--r--app/assets/javascripts/ide/stores/utils.js8
-rw-r--r--app/assets/javascripts/ide/stores/workers/files_decorator_worker.js2
-rw-r--r--app/assets/javascripts/image_diff/helpers/dom_helper.js3
-rw-r--r--app/assets/javascripts/image_diff/helpers/utils_helper.js3
-rw-r--r--app/assets/javascripts/importer_status.js14
-rw-r--r--app/assets/javascripts/init_changes_dropdown.js2
-rw-r--r--app/assets/javascripts/init_notes.js4
-rw-r--r--app/assets/javascripts/integrations/integration_settings_form.js21
-rw-r--r--app/assets/javascripts/issuable/auto_width_dropdown_select.js2
-rw-r--r--app/assets/javascripts/issuable_bulk_update_actions.js2
-rw-r--r--app/assets/javascripts/issuable_form.js4
-rw-r--r--app/assets/javascripts/issue.js2
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue8
-rw-r--r--app/assets/javascripts/issue_show/components/description.vue10
-rw-r--r--app/assets/javascripts/issue_show/components/edit_actions.vue20
-rw-r--r--app/assets/javascripts/issue_show/components/edited.vue4
-rw-r--r--app/assets/javascripts/issue_show/components/fields/description.vue14
-rw-r--r--app/assets/javascripts/issue_show/components/fields/description_template.vue10
-rw-r--r--app/assets/javascripts/issue_show/components/fields/title.vue2
-rw-r--r--app/assets/javascripts/issue_show/components/form.vue12
-rw-r--r--app/assets/javascripts/issue_show/components/locked_warning.vue2
-rw-r--r--app/assets/javascripts/issue_show/components/title.vue116
-rw-r--r--app/assets/javascripts/job.js104
-rw-r--r--app/assets/javascripts/jobs/components/header.vue9
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_detail_row.vue2
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_details_block.vue54
-rw-r--r--app/assets/javascripts/jobs/job_details_bundle.js2
-rw-r--r--app/assets/javascripts/jobs/job_details_mediator.js5
-rw-r--r--app/assets/javascripts/label_manager.js21
-rw-r--r--app/assets/javascripts/labels_select.js15
-rw-r--r--app/assets/javascripts/lazy_loader.js4
-rw-r--r--app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js2
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js223
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js111
-rw-r--r--app/assets/javascripts/lib/utils/dom_utils.js7
-rw-r--r--app/assets/javascripts/lib/utils/http_status.js1
-rw-r--r--app/assets/javascripts/lib/utils/logoutput_behaviours.js46
-rw-r--r--app/assets/javascripts/lib/utils/notify.js2
-rw-r--r--app/assets/javascripts/lib/utils/number_utils.js4
-rw-r--r--app/assets/javascripts/lib/utils/scroll_utils.js29
-rw-r--r--app/assets/javascripts/lib/utils/sticky.js23
-rw-r--r--app/assets/javascripts/lib/utils/text_markdown.js2
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js21
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js4
-rw-r--r--app/assets/javascripts/line_highlighter.js14
-rw-r--r--app/assets/javascripts/locale/index.js6
-rw-r--r--app/assets/javascripts/locale/sprintf.js2
-rw-r--r--app/assets/javascripts/main.js13
-rw-r--r--app/assets/javascripts/merge_conflicts/components/diff_file_editor.js17
-rw-r--r--app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js13
-rw-r--r--app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js11
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflict_service.js29
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflict_store.js4
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js41
-rw-r--r--app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js20
-rw-r--r--app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js32
-rw-r--r--app/assets/javascripts/merge_request.js3
-rw-r--r--app/assets/javascripts/merge_request_tabs.js179
-rw-r--r--app/assets/javascripts/milestone.js4
-rw-r--r--app/assets/javascripts/milestone_select.js12
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue60
-rw-r--r--app/assets/javascripts/monitoring/components/empty_state.vue4
-rw-r--r--app/assets/javascripts/monitoring/components/graph.vue27
-rw-r--r--app/assets/javascripts/monitoring/components/graph/axis.vue20
-rw-r--r--app/assets/javascripts/monitoring/components/graph/deployment.vue4
-rw-r--r--app/assets/javascripts/monitoring/components/graph/flag.vue10
-rw-r--r--app/assets/javascripts/monitoring/components/graph/legend.vue14
-rw-r--r--app/assets/javascripts/monitoring/components/graph/path.vue10
-rw-r--r--app/assets/javascripts/monitoring/components/graph/track_line.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/graph_group.vue6
-rw-r--r--app/assets/javascripts/monitoring/services/monitoring_service.js19
-rw-r--r--app/assets/javascripts/monitoring/stores/monitoring_store.js5
-rw-r--r--app/assets/javascripts/monitoring/utils/multiple_time_series.js6
-rw-r--r--app/assets/javascripts/mr_notes/index.js51
-rw-r--r--app/assets/javascripts/mr_notes/stores/actions.js7
-rw-r--r--app/assets/javascripts/mr_notes/stores/getters.js5
-rw-r--r--app/assets/javascripts/mr_notes/stores/index.js15
-rw-r--r--app/assets/javascripts/mr_notes/stores/modules/index.js12
-rw-r--r--app/assets/javascripts/mr_notes/stores/mutation_types.js3
-rw-r--r--app/assets/javascripts/mr_notes/stores/mutations.js7
-rw-r--r--app/assets/javascripts/namespace_select.js2
-rw-r--r--app/assets/javascripts/network/branch_graph.js57
-rw-r--r--app/assets/javascripts/new_branch_form.js4
-rw-r--r--app/assets/javascripts/new_commit_form.js2
-rw-r--r--app/assets/javascripts/notebook/cells/code.vue4
-rw-r--r--app/assets/javascripts/notebook/cells/code/index.vue4
-rw-r--r--app/assets/javascripts/notebook/cells/output/index.vue2
-rw-r--r--app/assets/javascripts/notes.js301
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue88
-rw-r--r--app/assets/javascripts/notes/components/diff_file_header.vue12
-rw-r--r--app/assets/javascripts/notes/components/diff_with_note.vue196
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue23
-rw-r--r--app/assets/javascripts/notes/components/discussion_locked_widget.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue48
-rw-r--r--app/assets/javascripts/notes/components/note_awards_list.vue18
-rw-r--r--app/assets/javascripts/notes/components/note_body.vue18
-rw-r--r--app/assets/javascripts/notes/components/note_edited_text.vue13
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue79
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue19
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue219
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue58
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue112
-rw-r--r--app/assets/javascripts/notes/constants.js3
-rw-r--r--app/assets/javascripts/notes/index.js84
-rw-r--r--app/assets/javascripts/notes/mixins/autosave.js6
-rw-r--r--app/assets/javascripts/notes/mixins/noteable.js9
-rw-r--r--app/assets/javascripts/notes/mixins/resolvable.js36
-rw-r--r--app/assets/javascripts/notes/services/notes_service.js8
-rw-r--r--app/assets/javascripts/notes/stores/actions.js120
-rw-r--r--app/assets/javascripts/notes/stores/collapse_utils.js108
-rw-r--r--app/assets/javascripts/notes/stores/getters.js77
-rw-r--r--app/assets/javascripts/notes/stores/index.js26
-rw-r--r--app/assets/javascripts/notes/stores/modules/index.js27
-rw-r--r--app/assets/javascripts/notes/stores/mutation_types.js10
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js106
-rw-r--r--app/assets/javascripts/notifications_form.js2
-rw-r--r--app/assets/javascripts/pages/admin/admin.js2
-rw-r--r--app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue2
-rw-r--r--app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue6
-rw-r--r--app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue12
-rw-r--r--app/assets/javascripts/pages/dashboard/todos/index/todos.js4
-rw-r--r--app/assets/javascripts/pages/groups/edit/index.js6
-rw-r--r--app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue2
-rw-r--r--app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue2
-rw-r--r--app/assets/javascripts/pages/profiles/keys/index.js16
-rw-r--r--app/assets/javascripts/pages/profiles/two_factor_auths/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/clusters/gcp/login/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/clusters/gcp/new/index.js5
-rw-r--r--app/assets/javascripts/pages/projects/clusters/new/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js15
-rw-r--r--app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js2
-rw-r--r--app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js9
-rw-r--r--app/assets/javascripts/pages/projects/index.js14
-rw-r--r--app/assets/javascripts/pages/projects/init_blob.js8
-rw-r--r--app/assets/javascripts/pages/projects/init_form.js2
-rw-r--r--app/assets/javascripts/pages/projects/issues/form.js2
-rw-r--r--app/assets/javascripts/pages/projects/jobs/terminal/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue2
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js2
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js17
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/show/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/network/network.js2
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue22
-rw-r--r--app/assets/javascripts/pages/projects/project.js3
-rw-r--r--app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js14
-rw-r--r--app/assets/javascripts/pages/projects/settings/repository/form.js4
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue8
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue2
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue34
-rw-r--r--app/assets/javascripts/pages/projects/tags/new/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue6
-rw-r--r--app/assets/javascripts/pages/projects/wikis/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/wikis/wikis.js11
-rw-r--r--app/assets/javascripts/pages/search/show/search.js2
-rw-r--r--app/assets/javascripts/pages/sessions/new/index.js3
-rw-r--r--app/assets/javascripts/pages/sessions/new/oauth_remember_me.js3
-rw-r--r--app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js5
-rw-r--r--app/assets/javascripts/pages/sessions/new/username_validator.js11
-rw-r--r--app/assets/javascripts/pages/snippets/form.js10
-rw-r--r--app/assets/javascripts/pages/users/activity_calendar.js5
-rw-r--r--app/assets/javascripts/pages/users/user_tabs.js33
-rw-r--r--app/assets/javascripts/pdf/index.vue4
-rw-r--r--app/assets/javascripts/pdf/page/index.vue2
-rw-r--r--app/assets/javascripts/performance_bar/components/detailed_metric.vue3
-rw-r--r--app/assets/javascripts/performance_bar/components/performance_bar_app.vue3
-rw-r--r--app/assets/javascripts/performance_bar/components/request_selector.vue5
-rw-r--r--app/assets/javascripts/pipelines/components/blank_state.vue28
-rw-r--r--app/assets/javascripts/pipelines/components/empty_state.vue38
-rw-r--r--app/assets/javascripts/pipelines/components/graph/action_component.vue13
-rw-r--r--app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue7
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_component.vue14
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_name_component.vue40
-rw-r--r--app/assets/javascripts/pipelines/components/graph/stage_column_component.vue6
-rw-r--r--app/assets/javascripts/pipelines/components/header_component.vue126
-rw-r--r--app/assets/javascripts/pipelines/components/nav_controls.vue66
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_url.vue90
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines.vue514
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_actions.vue76
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_artifacts.vue34
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_table.vue131
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_table_row.vue456
-rw-r--r--app/assets/javascripts/pipelines/components/stage.vue48
-rw-r--r--app/assets/javascripts/pipelines/components/time_ago.vue102
-rw-r--r--app/assets/javascripts/pipelines/mixins/pipelines.js11
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js5
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_mediator.js3
-rw-r--r--app/assets/javascripts/pipelines/services/pipelines_service.js2
-rw-r--r--app/assets/javascripts/preview_markdown.js18
-rw-r--r--app/assets/javascripts/profile/account/components/delete_account_modal.vue12
-rw-r--r--app/assets/javascripts/profile/account/components/update_username.vue16
-rw-r--r--app/assets/javascripts/profile/add_ssh_key_validation.js43
-rw-r--r--app/assets/javascripts/profile/gl_crop.js23
-rw-r--r--app/assets/javascripts/profile/profile.js15
-rw-r--r--app/assets/javascripts/project_find_file.js9
-rw-r--r--app/assets/javascripts/project_fork.js2
-rw-r--r--app/assets/javascripts/project_import.js2
-rw-r--r--app/assets/javascripts/project_label_subscription.js28
-rw-r--r--app/assets/javascripts/project_select.js7
-rw-r--r--app/assets/javascripts/project_visibility.js2
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_dropdown_mixin.js71
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue144
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue203
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue118
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/constants.js11
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/index.js88
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/store/actions.js95
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/store/getters.js3
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/store/index.js18
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/store/mutation_types.js8
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/store/mutations.js28
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/store/state.js13
-rw-r--r--app/assets/javascripts/projects/project_new.js8
-rw-r--r--app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue6
-rw-r--r--app/assets/javascripts/projects_dropdown/components/app.vue158
-rw-r--r--app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue57
-rw-r--r--app/assets/javascripts/projects_dropdown/components/projects_list_item.vue116
-rw-r--r--app/assets/javascripts/projects_dropdown/components/projects_list_search.vue63
-rw-r--r--app/assets/javascripts/projects_dropdown/components/search.vue65
-rw-r--r--app/assets/javascripts/projects_dropdown/constants.js10
-rw-r--r--app/assets/javascripts/projects_dropdown/index.js66
-rw-r--r--app/assets/javascripts/projects_dropdown/service/projects_service.js137
-rw-r--r--app/assets/javascripts/projects_dropdown/store/projects_store.js33
-rw-r--r--app/assets/javascripts/prometheus_metrics/prometheus_metrics.js4
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_create.js34
-rw-r--r--app/assets/javascripts/raven/raven_config.js2
-rw-r--r--app/assets/javascripts/registry/components/app.vue2
-rw-r--r--app/assets/javascripts/registry/components/collapsible_container.vue12
-rw-r--r--app/assets/javascripts/registry/components/table_registry.vue6
-rw-r--r--app/assets/javascripts/registry/index.js2
-rw-r--r--app/assets/javascripts/registry/stores/actions.js20
-rw-r--r--app/assets/javascripts/right_sidebar.js4
-rw-r--r--app/assets/javascripts/search_autocomplete.js10
-rw-r--r--app/assets/javascripts/settings_panels.js4
-rw-r--r--app/assets/javascripts/shared/milestones/form.js11
-rw-r--r--app/assets/javascripts/shortcuts_find_file.js4
-rw-r--r--app/assets/javascripts/shortcuts_issuable.js24
-rw-r--r--app/assets/javascripts/shortcuts_navigation.js1
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignee_title.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignees.vue31
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue15
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/edit_form.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/lock/edit_form.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue15
-rw-r--r--app/assets/javascripts/sidebar/components/participants/participants.vue9
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue5
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue5
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue18
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/help_state.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue6
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js1
-rw-r--r--app/assets/javascripts/sidebar/sidebar_mediator.js2
-rw-r--r--app/assets/javascripts/single_file_diff.js4
-rw-r--r--app/assets/javascripts/smart_interval.js9
-rw-r--r--app/assets/javascripts/snippet/snippet_embed.js2
-rw-r--r--app/assets/javascripts/syntax_highlight.js2
-rw-r--r--app/assets/javascripts/test_utils/simulate_drag.js4
-rw-r--r--app/assets/javascripts/tree.js2
-rw-r--r--app/assets/javascripts/u2f/authenticate.js8
-rw-r--r--app/assets/javascripts/users_select.js14
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment.vue95
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue9
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue190
-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.vue3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue14
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue12
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue36
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.js15
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue74
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue49
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue66
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js8
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_badge_link.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_icon.vue23
-rw-r--r--app/assets/javascripts/vue_shared/components/clipboard_button.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue9
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue45
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/deprecated_modal.vue24
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/constants.js12
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue74
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/download_diff_viewer.vue69
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue160
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue158
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/two_up_viewer.vue41
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue109
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue55
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_hidden_input.vue22
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue46
-rw-r--r--app/assets/javascripts/vue_shared/components/expand_button.vue17
-rw-r--r--app/assets/javascripts/vue_shared/components/file_icon.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/gl_modal.vue29
-rw-r--r--app/assets/javascripts/vue_shared/components/header_ci_component.vue16
-rw-r--r--app/assets/javascripts/vue_shared/components/identicon.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/issue_warning.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/lib/utils/dom_utils.js5
-rw-r--r--app/assets/javascripts/vue_shared/components/loading_button.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/loading_icon.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue44
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue20
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/memory_graph.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/navigation_tabs.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue13
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/system_note.vue141
-rw-r--r--app/assets/javascripts/vue_shared/components/panel_resizer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/project_avatar/image.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/recaptcha_modal.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue14
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input.vue22
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue15
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue14
-rw-r--r--app/assets/javascripts/vue_shared/components/table_pagination.vue18
-rw-r--r--app/assets/javascripts/vue_shared/components/tabs/tab.vue47
-rw-r--r--app/assets/javascripts/vue_shared/components/tabs/tabs.js76
-rw-r--r--app/assets/javascripts/vue_shared/components/toggle_button.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue6
-rw-r--r--app/assets/javascripts/vue_shared/directives/popover.js2
-rw-r--r--app/assets/javascripts/vue_shared/directives/tooltip.js8
-rw-r--r--app/assets/javascripts/vue_shared/models/assignee.js13
-rw-r--r--app/assets/javascripts/vue_shared/translate.js6
-rw-r--r--app/assets/javascripts/vue_shared/vue_resource_interceptor.js2
-rw-r--r--app/assets/javascripts/zen_mode.js2
-rw-r--r--app/assets/stylesheets/bootstrap.scss37
-rw-r--r--app/assets/stylesheets/bootstrap_migration.scss340
-rw-r--r--app/assets/stylesheets/errors.scss120
-rw-r--r--app/assets/stylesheets/framework.scss5
-rw-r--r--app/assets/stylesheets/framework/animations.scss9
-rw-r--r--app/assets/stylesheets/framework/avatar.scss2
-rw-r--r--app/assets/stylesheets/framework/awards.scss4
-rw-r--r--app/assets/stylesheets/framework/badges.scss2
-rw-r--r--app/assets/stylesheets/framework/banner.scss2
-rw-r--r--app/assets/stylesheets/framework/blank.scss8
-rw-r--r--app/assets/stylesheets/framework/blocks.scss15
-rw-r--r--app/assets/stylesheets/framework/broadcast_messages.scss6
-rw-r--r--app/assets/stylesheets/framework/buttons.scss17
-rw-r--r--app/assets/stylesheets/framework/calendar.scss2
-rw-r--r--app/assets/stylesheets/framework/ci_variable_list.scss12
-rw-r--r--app/assets/stylesheets/framework/common.scss21
-rw-r--r--app/assets/stylesheets/framework/contextual_sidebar.scss65
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss105
-rw-r--r--app/assets/stylesheets/framework/emojis.scss1
-rw-r--r--app/assets/stylesheets/framework/feature_highlight.scss2
-rw-r--r--app/assets/stylesheets/framework/files.scss84
-rw-r--r--app/assets/stylesheets/framework/filters.scss26
-rw-r--r--app/assets/stylesheets/framework/flash.scss30
-rw-r--r--app/assets/stylesheets/framework/forms.scss58
-rw-r--r--app/assets/stylesheets/framework/gfm.scss1
-rw-r--r--app/assets/stylesheets/framework/gitlab_theme.scss206
-rw-r--r--app/assets/stylesheets/framework/header.scss93
-rw-r--r--app/assets/stylesheets/framework/icons.scss24
-rw-r--r--app/assets/stylesheets/framework/issue_box.scss7
-rw-r--r--app/assets/stylesheets/framework/layout.scss14
-rw-r--r--app/assets/stylesheets/framework/lists.scss12
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss12
-rw-r--r--app/assets/stylesheets/framework/mixins.scss23
-rw-r--r--app/assets/stylesheets/framework/mobile.scss6
-rw-r--r--app/assets/stylesheets/framework/modal.scss17
-rw-r--r--app/assets/stylesheets/framework/page_header.scss6
-rw-r--r--app/assets/stylesheets/framework/pagination.scss90
-rw-r--r--app/assets/stylesheets/framework/panels.scss17
-rw-r--r--app/assets/stylesheets/framework/responsive_tables.scss22
-rw-r--r--app/assets/stylesheets/framework/secondary_navigation_elements.scss29
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss6
-rw-r--r--app/assets/stylesheets/framework/snippets.scss17
-rw-r--r--app/assets/stylesheets/framework/tables.scss92
-rw-r--r--app/assets/stylesheets/framework/tabs.scss1
-rw-r--r--app/assets/stylesheets/framework/terms.scss8
-rw-r--r--app/assets/stylesheets/framework/timeline.scss4
-rw-r--r--app/assets/stylesheets/framework/toggle.scss6
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap.scss201
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap_variables.scss199
-rw-r--r--app/assets/stylesheets/framework/typography.scss44
-rw-r--r--app/assets/stylesheets/framework/variables.scss89
-rw-r--r--app/assets/stylesheets/framework/wells.scss10
-rw-r--r--app/assets/stylesheets/highlight/dark.scss4
-rw-r--r--app/assets/stylesheets/highlight/monokai.scss4
-rw-r--r--app/assets/stylesheets/highlight/solarized_dark.scss4
-rw-r--r--app/assets/stylesheets/highlight/solarized_light.scss4
-rw-r--r--app/assets/stylesheets/highlight/white_base.scss4
-rw-r--r--app/assets/stylesheets/mailers/highlighted_diff_email.scss3
-rw-r--r--app/assets/stylesheets/pages/boards.scss49
-rw-r--r--app/assets/stylesheets/pages/builds.scss27
-rw-r--r--app/assets/stylesheets/pages/clusters.scss8
-rw-r--r--app/assets/stylesheets/pages/commits.scss36
-rw-r--r--app/assets/stylesheets/pages/convdev_index.scss28
-rw-r--r--app/assets/stylesheets/pages/cycle_analytics.scss16
-rw-r--r--app/assets/stylesheets/pages/detail_page.scss9
-rw-r--r--app/assets/stylesheets/pages/diff.scss208
-rw-r--r--app/assets/stylesheets/pages/editor.scss24
-rw-r--r--app/assets/stylesheets/pages/environments.scss40
-rw-r--r--app/assets/stylesheets/pages/events.scss4
-rw-r--r--app/assets/stylesheets/pages/groups.scss16
-rw-r--r--app/assets/stylesheets/pages/help.scss4
-rw-r--r--app/assets/stylesheets/pages/issuable.scss25
-rw-r--r--app/assets/stylesheets/pages/issues.scss10
-rw-r--r--app/assets/stylesheets/pages/labels.scss232
-rw-r--r--app/assets/stylesheets/pages/login.scss6
-rw-r--r--app/assets/stylesheets/pages/members.scss80
-rw-r--r--app/assets/stylesheets/pages/merge_conflicts.scss2
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss219
-rw-r--r--app/assets/stylesheets/pages/milestone.scss44
-rw-r--r--app/assets/stylesheets/pages/note_form.scss40
-rw-r--r--app/assets/stylesheets/pages/notes.scss90
-rw-r--r--app/assets/stylesheets/pages/notifications.scss2
-rw-r--r--app/assets/stylesheets/pages/pipeline_schedules.scss2
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss222
-rw-r--r--app/assets/stylesheets/pages/profile.scss14
-rw-r--r--app/assets/stylesheets/pages/profiles/preferences.scss79
-rw-r--r--app/assets/stylesheets/pages/projects.scss106
-rw-r--r--app/assets/stylesheets/pages/repo.scss353
-rw-r--r--app/assets/stylesheets/pages/runners.scss2
-rw-r--r--app/assets/stylesheets/pages/search.scss18
-rw-r--r--app/assets/stylesheets/pages/settings.scss67
-rw-r--r--app/assets/stylesheets/pages/settings_ci_cd.scss9
-rw-r--r--app/assets/stylesheets/pages/stat_graph.scss13
-rw-r--r--app/assets/stylesheets/pages/status.scss2
-rw-r--r--app/assets/stylesheets/pages/todos.scss12
-rw-r--r--app/assets/stylesheets/pages/tree.scss11
-rw-r--r--app/assets/stylesheets/pages/wiki.scss4
-rw-r--r--app/assets/stylesheets/performance_bar.scss12
-rw-r--r--app/assets/stylesheets/print.scss5
-rw-r--r--app/controllers/admin/appearances_controller.rb9
-rw-r--r--app/controllers/admin/application_settings_controller.rb2
-rw-r--r--app/controllers/admin/dashboard_controller.rb4
-rw-r--r--app/controllers/admin/deploy_keys_controller.rb4
-rw-r--r--app/controllers/admin/groups_controller.rb6
-rw-r--r--app/controllers/admin/hooks_controller.rb9
-rw-r--r--app/controllers/admin/identities_controller.rb2
-rw-r--r--app/controllers/admin/impersonations_controller.rb2
-rw-r--r--app/controllers/admin/jobs_controller.rb2
-rw-r--r--app/controllers/admin/runner_projects_controller.rb6
-rw-r--r--app/controllers/admin/runners_controller.rb2
-rw-r--r--app/controllers/admin/services_controller.rb2
-rw-r--r--app/controllers/admin/users_controller.rb6
-rw-r--r--app/controllers/application_controller.rb30
-rw-r--r--app/controllers/boards/lists_controller.rb14
-rw-r--r--app/controllers/concerns/group_tree.rb40
-rw-r--r--app/controllers/concerns/internal_redirect.rb4
-rw-r--r--app/controllers/concerns/issuable_actions.rb34
-rw-r--r--app/controllers/concerns/issuable_collections.rb7
-rw-r--r--app/controllers/concerns/issues_action.rb7
-rw-r--r--app/controllers/concerns/issues_calendar.rb24
-rw-r--r--app/controllers/concerns/lfs_request.rb2
-rw-r--r--app/controllers/concerns/notes_actions.rb6
-rw-r--r--app/controllers/concerns/preview_markdown.rb3
-rw-r--r--app/controllers/concerns/uploads_actions.rb30
-rw-r--r--app/controllers/dashboard/projects_controller.rb5
-rw-r--r--app/controllers/dashboard_controller.rb2
-rw-r--r--app/controllers/graphql_controller.rb45
-rw-r--r--app/controllers/groups/avatars_controller.rb2
-rw-r--r--app/controllers/groups/group_members_controller.rb6
-rw-r--r--app/controllers/groups/labels_controller.rb23
-rw-r--r--app/controllers/groups/milestones_controller.rb5
-rw-r--r--app/controllers/groups/runners_controller.rb2
-rw-r--r--app/controllers/groups/shared_projects_controller.rb33
-rw-r--r--app/controllers/groups/uploads_controller.rb4
-rw-r--r--app/controllers/health_controller.rb3
-rw-r--r--app/controllers/import/base_controller.rb4
-rw-r--r--app/controllers/import/bitbucket_controller.rb2
-rw-r--r--app/controllers/import/fogbugz_controller.rb2
-rw-r--r--app/controllers/import/github_controller.rb2
-rw-r--r--app/controllers/import/gitlab_controller.rb2
-rw-r--r--app/controllers/import/google_code_controller.rb2
-rw-r--r--app/controllers/jwt_controller.rb4
-rw-r--r--app/controllers/metrics_controller.rb2
-rw-r--r--app/controllers/notification_settings_controller.rb4
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb8
-rw-r--r--app/controllers/profiles/active_sessions_controller.rb2
-rw-r--r--app/controllers/profiles/avatars_controller.rb2
-rw-r--r--app/controllers/profiles/chat_names_controller.rb2
-rw-r--r--app/controllers/profiles/emails_controller.rb2
-rw-r--r--app/controllers/profiles/gpg_keys_controller.rb4
-rw-r--r--app/controllers/profiles/keys_controller.rb2
-rw-r--r--app/controllers/profiles/two_factor_auths_controller.rb2
-rw-r--r--app/controllers/profiles_controller.rb8
-rw-r--r--app/controllers/projects/application_controller.rb2
-rw-r--r--app/controllers/projects/artifacts_controller.rb9
-rw-r--r--app/controllers/projects/autocomplete_sources_controller.rb2
-rw-r--r--app/controllers/projects/avatars_controller.rb2
-rw-r--r--app/controllers/projects/blob_controller.rb70
-rw-r--r--app/controllers/projects/branches_controller.rb7
-rw-r--r--app/controllers/projects/clusters/applications_controller.rb28
-rw-r--r--app/controllers/projects/clusters/gcp_controller.rb102
-rw-r--r--app/controllers/projects/clusters/user_controller.rb40
-rw-r--r--app/controllers/projects/clusters_controller.rb128
-rw-r--r--app/controllers/projects/commits_controller.rb14
-rw-r--r--app/controllers/projects/deploy_keys_controller.rb2
-rw-r--r--app/controllers/projects/discussions_controller.rb9
-rw-r--r--app/controllers/projects/environments_controller.rb28
-rw-r--r--app/controllers/projects/git_http_client_controller.rb4
-rw-r--r--app/controllers/projects/group_links_controller.rb4
-rw-r--r--app/controllers/projects/hooks_controller.rb7
-rw-r--r--app/controllers/projects/issues_controller.rb9
-rw-r--r--app/controllers/projects/jobs_controller.rb30
-rw-r--r--app/controllers/projects/labels_controller.rb4
-rw-r--r--app/controllers/projects/lfs_api_controller.rb9
-rw-r--r--app/controllers/projects/lfs_storage_controller.rb4
-rw-r--r--app/controllers/projects/merge_requests/application_controller.rb3
-rw-r--r--app/controllers/projects/merge_requests/diffs_controller.rb25
-rw-r--r--app/controllers/projects/merge_requests_controller.rb50
-rw-r--r--app/controllers/projects/milestones_controller.rb13
-rw-r--r--app/controllers/projects/mirrors_controller.rb2
-rw-r--r--app/controllers/projects/pipeline_schedules_controller.rb2
-rw-r--r--app/controllers/projects/pipelines_controller.rb8
-rw-r--r--app/controllers/projects/releases_controller.rb2
-rw-r--r--app/controllers/projects/repositories_controller.rb2
-rw-r--r--app/controllers/projects/runner_projects_controller.rb5
-rw-r--r--app/controllers/projects/runners_controller.rb2
-rw-r--r--app/controllers/projects/services_controller.rb8
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb4
-rw-r--r--app/controllers/projects/snippets_controller.rb2
-rw-r--r--app/controllers/projects/tags_controller.rb2
-rw-r--r--app/controllers/projects/templates_controller.rb2
-rw-r--r--app/controllers/projects/triggers_controller.rb2
-rw-r--r--app/controllers/projects/uploads_controller.rb4
-rw-r--r--app/controllers/projects/wikis_controller.rb13
-rw-r--r--app/controllers/projects_controller.rb26
-rw-r--r--app/controllers/registrations_controller.rb27
-rw-r--r--app/controllers/sessions_controller.rb53
-rw-r--r--app/controllers/sherlock/transactions_controller.rb2
-rw-r--r--app/controllers/snippets_controller.rb2
-rw-r--r--app/controllers/users/terms_controller.rb5
-rw-r--r--app/finders/group_members_finder.rb26
-rw-r--r--app/finders/group_projects_finder.rb26
-rw-r--r--app/finders/issuable_finder.rb43
-rw-r--r--app/finders/issues_finder.rb10
-rw-r--r--app/finders/members_finder.rb4
-rw-r--r--app/finders/merge_requests_finder.rb6
-rw-r--r--app/finders/notes_finder.rb2
-rw-r--r--app/finders/pipelines_finder.rb9
-rw-r--r--app/finders/snippets_finder.rb15
-rw-r--r--app/finders/user_recent_events_finder.rb2
-rw-r--r--app/graphql/functions/base_function.rb4
-rw-r--r--app/graphql/functions/echo.rb13
-rw-r--r--app/graphql/gitlab_schema.rb11
-rw-r--r--app/graphql/mutations/.keep0
-rw-r--r--app/graphql/resolvers/base_resolver.rb4
-rw-r--r--app/graphql/resolvers/concerns/resolves_pipelines.rb23
-rw-r--r--app/graphql/resolvers/full_path_resolver.rb19
-rw-r--r--app/graphql/resolvers/merge_request_pipelines_resolver.rb16
-rw-r--r--app/graphql/resolvers/merge_request_resolver.rb20
-rw-r--r--app/graphql/resolvers/project_pipelines_resolver.rb11
-rw-r--r--app/graphql/resolvers/project_resolver.rb11
-rw-r--r--app/graphql/types/base_enum.rb4
-rw-r--r--app/graphql/types/base_field.rb5
-rw-r--r--app/graphql/types/base_input_object.rb4
-rw-r--r--app/graphql/types/base_interface.rb5
-rw-r--r--app/graphql/types/base_object.rb8
-rw-r--r--app/graphql/types/base_scalar.rb4
-rw-r--r--app/graphql/types/base_union.rb4
-rw-r--r--app/graphql/types/ci/pipeline_status_enum.rb9
-rw-r--r--app/graphql/types/ci/pipeline_type.rb31
-rw-r--r--app/graphql/types/merge_request_type.rb55
-rw-r--r--app/graphql/types/mutation_type.rb7
-rw-r--r--app/graphql/types/permission_types/base_permission_type.rb38
-rw-r--r--app/graphql/types/permission_types/ci/pipeline.rb11
-rw-r--r--app/graphql/types/permission_types/merge_request.rb17
-rw-r--r--app/graphql/types/permission_types/project.rb20
-rw-r--r--app/graphql/types/project_type.rb79
-rw-r--r--app/graphql/types/query_type.rb14
-rw-r--r--app/graphql/types/time_type.rb14
-rw-r--r--app/helpers/application_helper.rb2
-rw-r--r--app/helpers/application_settings_helper.rb9
-rw-r--r--app/helpers/avatars_helper.rb2
-rw-r--r--app/helpers/calendar_helper.rb8
-rw-r--r--app/helpers/ci_status_helper.rb10
-rw-r--r--app/helpers/clusters_helper.rb1
-rw-r--r--app/helpers/commits_helper.rb12
-rw-r--r--app/helpers/count_helper.rb8
-rw-r--r--app/helpers/favicon_helper.rb7
-rw-r--r--app/helpers/groups_helper.rb6
-rw-r--r--app/helpers/issuables_helper.rb14
-rw-r--r--app/helpers/labels_helper.rb10
-rw-r--r--app/helpers/markup_helper.rb4
-rw-r--r--app/helpers/merge_requests_helper.rb17
-rw-r--r--app/helpers/milestones_helper.rb6
-rw-r--r--app/helpers/nav_helper.rb1
-rw-r--r--app/helpers/notes_helper.rb17
-rw-r--r--app/helpers/page_layout_helper.rb5
-rw-r--r--app/helpers/pipeline_schedules_helper.rb2
-rw-r--r--app/helpers/projects_helper.rb85
-rw-r--r--app/helpers/rss_helper.rb2
-rw-r--r--app/helpers/search_helper.rb12
-rw-r--r--app/helpers/time_helper.rb8
-rw-r--r--app/helpers/users_helper.rb8
-rw-r--r--app/helpers/webpack_helper.rb2
-rw-r--r--app/helpers/workhorse_helper.rb2
-rw-r--r--app/mailers/emails/merge_requests.rb6
-rw-r--r--app/mailers/previews/devise_mailer_preview.rb (renamed from spec/mailers/previews/devise_mailer_preview.rb)0
-rw-r--r--app/mailers/previews/email_rejection_mailer_preview.rb (renamed from spec/mailers/previews/email_rejection_mailer_preview.rb)0
-rw-r--r--app/mailers/previews/notify_preview.rb170
-rw-r--r--app/mailers/previews/repository_check_mailer_preview.rb (renamed from spec/mailers/previews/repository_check_mailer_preview.rb)0
-rw-r--r--app/models/appearance.rb1
-rw-r--r--app/models/application_setting.rb64
-rw-r--r--app/models/application_setting/term.rb6
-rw-r--r--app/models/badge.rb2
-rw-r--r--app/models/board.rb4
-rw-r--r--app/models/ci/build.rb43
-rw-r--r--app/models/ci/build_runner_session.rb25
-rw-r--r--app/models/ci/build_trace_chunk.rb149
-rw-r--r--app/models/ci/build_trace_chunks/database.rb29
-rw-r--r--app/models/ci/build_trace_chunks/fog.rb59
-rw-r--r--app/models/ci/build_trace_chunks/redis.rb51
-rw-r--r--app/models/ci/group.rb8
-rw-r--r--app/models/ci/legacy_stage.rb6
-rw-r--r--app/models/ci/pipeline.rb57
-rw-r--r--app/models/ci/runner.rb78
-rw-r--r--app/models/ci/runner_namespace.rb6
-rw-r--r--app/models/ci/runner_project.rb4
-rw-r--r--app/models/ci/stage.rb32
-rw-r--r--app/models/clusters/applications/jupyter.rb92
-rw-r--r--app/models/clusters/applications/prometheus.rb3
-rw-r--r--app/models/clusters/applications/runner.rb5
-rw-r--r--app/models/clusters/cluster.rb8
-rw-r--r--app/models/clusters/platforms/kubernetes.rb4
-rw-r--r--app/models/clusters/providers/gcp.rb2
-rw-r--r--app/models/commit_status.rb10
-rw-r--r--app/models/concerns/atomic_internal_id.rb16
-rw-r--r--app/models/concerns/avatarable.rb47
-rw-r--r--app/models/concerns/batch_destroy_dependent_associations.rb28
-rw-r--r--app/models/concerns/cache_markdown_field.rb24
-rw-r--r--app/models/concerns/cacheable_attributes.rb38
-rw-r--r--app/models/concerns/diff_file.rb9
-rw-r--r--app/models/concerns/enum_with_nil.rb33
-rw-r--r--app/models/concerns/group_descendant.rb4
-rw-r--r--app/models/concerns/has_status.rb2
-rw-r--r--app/models/concerns/has_variable.rb2
-rw-r--r--app/models/concerns/iid_routes.rb9
-rw-r--r--app/models/concerns/issuable.rb6
-rw-r--r--app/models/concerns/project_features_compatibility.rb2
-rw-r--r--app/models/concerns/protected_ref.rb2
-rw-r--r--app/models/concerns/protected_ref_access.rb9
-rw-r--r--app/models/concerns/reactive_caching.rb1
-rw-r--r--app/models/concerns/redis_cacheable.rb2
-rw-r--r--app/models/concerns/resolvable_note.rb2
-rw-r--r--app/models/concerns/select_for_project_authorization.rb7
-rw-r--r--app/models/concerns/sortable.rb4
-rw-r--r--app/models/concerns/time_trackable.rb6
-rw-r--r--app/models/concerns/with_uploads.rb4
-rw-r--r--app/models/deployment.rb1
-rw-r--r--app/models/diff_note.rb67
-rw-r--r--app/models/discussion.rb4
-rw-r--r--app/models/environment.rb2
-rw-r--r--app/models/event.rb2
-rw-r--r--app/models/generic_commit_status.rb2
-rw-r--r--app/models/group.rb43
-rw-r--r--app/models/hooks/system_hook.rb5
-rw-r--r--app/models/hooks/web_hook.rb9
-rw-r--r--app/models/hooks/web_hook_log.rb5
-rw-r--r--app/models/import_export_upload.rb13
-rw-r--r--app/models/internal_id.rb26
-rw-r--r--app/models/issue.rb22
-rw-r--r--app/models/label.rb19
-rw-r--r--app/models/list.rb18
-rw-r--r--app/models/member.rb6
-rw-r--r--app/models/members/project_member.rb6
-rw-r--r--app/models/merge_request.rb87
-rw-r--r--app/models/merge_request_diff.rb27
-rw-r--r--app/models/merge_request_diff_file.rb7
-rw-r--r--app/models/milestone.rb4
-rw-r--r--app/models/namespace.rb6
-rw-r--r--app/models/network/commit.rb4
-rw-r--r--app/models/note.rb9
-rw-r--r--app/models/note_diff_file.rb7
-rw-r--r--app/models/notification_recipient.rb31
-rw-r--r--app/models/pages_domain.rb2
-rw-r--r--app/models/personal_snippet.rb1
-rw-r--r--app/models/project.rb109
-rw-r--r--app/models/project_auto_devops.rb31
-rw-r--r--app/models/project_group_link.rb3
-rw-r--r--app/models/project_import_data.rb2
-rw-r--r--app/models/project_services/bamboo_service.rb24
-rw-r--r--app/models/project_services/bugzilla_service.rb2
-rw-r--r--app/models/project_services/buildkite_service.rb2
-rw-r--r--app/models/project_services/chat_message/base_message.rb7
-rw-r--r--app/models/project_services/chat_message/pipeline_message.rb4
-rw-r--r--app/models/project_services/chat_notification_service.rb3
-rw-r--r--app/models/project_services/custom_issue_tracker_service.rb2
-rw-r--r--app/models/project_services/drone_ci_service.rb4
-rw-r--r--app/models/project_services/external_wiki_service.rb2
-rw-r--r--app/models/project_services/gemnasium_service.rb7
-rw-r--r--app/models/project_services/gitlab_issue_tracker_service.rb2
-rw-r--r--app/models/project_services/jira_service.rb6
-rw-r--r--app/models/project_services/kubernetes_service.rb4
-rw-r--r--app/models/project_services/microsoft_teams_service.rb2
-rw-r--r--app/models/project_services/mock_ci_service.rb2
-rw-r--r--app/models/project_services/prometheus_service.rb2
-rw-r--r--app/models/project_services/redmine_service.rb2
-rw-r--r--app/models/project_services/teamcity_service.rb2
-rw-r--r--app/models/project_team.rb23
-rw-r--r--app/models/project_wiki.rb6
-rw-r--r--app/models/protected_branch.rb2
-rw-r--r--app/models/remote_mirror.rb7
-rw-r--r--app/models/repository.rb73
-rw-r--r--app/models/service.rb7
-rw-r--r--app/models/term_agreement.rb2
-rw-r--r--app/models/timelog.rb9
-rw-r--r--app/models/user.rb47
-rw-r--r--app/models/wiki_page.rb1
-rw-r--r--app/policies/ci/build_policy.rb12
-rw-r--r--app/policies/ci/pipeline_policy.rb6
-rw-r--r--app/policies/clusters/cluster_policy.rb2
-rw-r--r--app/policies/deploy_token_policy.rb4
-rw-r--r--app/policies/environment_policy.rb10
-rw-r--r--app/policies/group_policy.rb17
-rw-r--r--app/policies/project_policy.rb11
-rw-r--r--app/presenters/commit_status_presenter.rb3
-rw-r--r--app/presenters/merge_request_presenter.rb23
-rw-r--r--app/presenters/project_presenter.rb5
-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.rb1
-rw-r--r--app/serializers/diff_file_entity.rb128
-rw-r--r--app/serializers/diffs_entity.rb65
-rw-r--r--app/serializers/diffs_serializer.rb3
-rw-r--r--app/serializers/discussion_entity.rb49
-rw-r--r--app/serializers/environment_entity.rb12
-rw-r--r--app/serializers/group_child_entity.rb2
-rw-r--r--app/serializers/job_entity.rb2
-rw-r--r--app/serializers/merge_request_basic_entity.rb4
-rw-r--r--app/serializers/merge_request_diff_entity.rb46
-rw-r--r--app/serializers/merge_request_user_entity.rb24
-rw-r--r--app/serializers/merge_request_widget_entity.rb17
-rw-r--r--app/serializers/note_entity.rb30
-rw-r--r--app/serializers/pipeline_details_entity.rb2
-rw-r--r--app/serializers/pipeline_serializer.rb17
-rw-r--r--app/serializers/runner_entity.rb2
-rw-r--r--app/serializers/status_entity.rb11
-rw-r--r--app/services/application_settings/update_service.rb14
-rw-r--r--app/services/applications/create_service.rb5
-rw-r--r--app/services/badges/update_service.rb2
-rw-r--r--app/services/base_count_service.rb6
-rw-r--r--app/services/base_service.rb2
-rw-r--r--app/services/boards/issues/list_service.rb29
-rw-r--r--app/services/boards/issues/move_service.rb6
-rw-r--r--app/services/boards/lists/create_service.rb20
-rw-r--r--app/services/check_gcp_project_billing_service.rb11
-rw-r--r--app/services/ci/register_job_service.rb31
-rw-r--r--app/services/clusters/applications/install_service.rb4
-rw-r--r--app/services/clusters/applications/schedule_installation_service.rb17
-rw-r--r--app/services/commits/change_service.rb2
-rw-r--r--app/services/commits/create_service.rb2
-rw-r--r--app/services/concerns/exclusive_lease_guard.rb2
-rw-r--r--app/services/concerns/issues/resolve_discussions.rb1
-rw-r--r--app/services/create_branch_service.rb2
-rw-r--r--app/services/delete_branch_service.rb2
-rw-r--r--app/services/import_export_clean_up_service.rb11
-rw-r--r--app/services/issuable_base_service.rb7
-rw-r--r--app/services/issues/base_service.rb5
-rw-r--r--app/services/issues/move_service.rb3
-rw-r--r--app/services/labels/find_or_create_service.rb8
-rw-r--r--app/services/lfs/unlock_file_service.rb2
-rw-r--r--app/services/members/update_service.rb2
-rw-r--r--app/services/merge_requests/base_service.rb4
-rw-r--r--app/services/merge_requests/create_from_issue_service.rb4
-rw-r--r--app/services/merge_requests/delete_non_latest_diffs_service.rb18
-rw-r--r--app/services/merge_requests/ff_merge_service.rb2
-rw-r--r--app/services/merge_requests/merge_request_diff_cache_service.rb17
-rw-r--r--app/services/merge_requests/merge_service.rb19
-rw-r--r--app/services/merge_requests/merge_when_pipeline_succeeds_service.rb6
-rw-r--r--app/services/merge_requests/post_merge_service.rb7
-rw-r--r--app/services/merge_requests/rebase_service.rb10
-rw-r--r--app/services/merge_requests/refresh_service.rb8
-rw-r--r--app/services/merge_requests/reload_diffs_service.rb43
-rw-r--r--app/services/merge_requests/squash_service.rb28
-rw-r--r--app/services/metrics_service.rb3
-rw-r--r--app/services/milestones/update_service.rb2
-rw-r--r--app/services/notes/update_service.rb2
-rw-r--r--app/services/notification_recipient_service.rb34
-rw-r--r--app/services/notification_service.rb34
-rw-r--r--app/services/pages_service.rb15
-rw-r--r--app/services/preview_markdown_service.rb7
-rw-r--r--app/services/projects/autocomplete_service.rb36
-rw-r--r--app/services/projects/count_service.rb6
-rw-r--r--app/services/projects/create_service.rb23
-rw-r--r--app/services/projects/destroy_service.rb34
-rw-r--r--app/services/projects/fork_service.rb2
-rw-r--r--app/services/projects/import_service.rb31
-rw-r--r--app/services/projects/lfs_pointers/lfs_download_link_list_service.rb93
-rw-r--r--app/services/projects/lfs_pointers/lfs_download_service.rb58
-rw-r--r--app/services/projects/lfs_pointers/lfs_import_service.rb92
-rw-r--r--app/services/projects/lfs_pointers/lfs_link_service.rb29
-rw-r--r--app/services/projects/lfs_pointers/lfs_list_service.rb19
-rw-r--r--app/services/projects/open_issues_count_service.rb68
-rw-r--r--app/services/projects/participants_service.rb8
-rw-r--r--app/services/projects/update_service.rb25
-rw-r--r--app/services/protected_branches/access_level_params.rb2
-rw-r--r--app/services/protected_branches/legacy_api_create_service.rb4
-rw-r--r--app/services/protected_branches/legacy_api_update_service.rb4
-rw-r--r--app/services/quick_actions/interpret_service.rb11
-rw-r--r--app/services/tags/create_service.rb2
-rw-r--r--app/services/tags/destroy_service.rb2
-rw-r--r--app/services/test_hooks/project_service.rb4
-rw-r--r--app/services/todo_service.rb30
-rw-r--r--app/services/update_release_service.rb2
-rw-r--r--app/services/validate_new_branch_service.rb2
-rw-r--r--app/services/web_hook_service.rb2
-rw-r--r--app/uploaders/attachment_uploader.rb2
-rw-r--r--app/uploaders/avatar_uploader.rb2
-rw-r--r--app/uploaders/favicon_uploader.rb15
-rw-r--r--app/uploaders/file_mover.rb2
-rw-r--r--app/uploaders/file_uploader.rb38
-rw-r--r--app/uploaders/gitlab_uploader.rb24
-rw-r--r--app/uploaders/import_export_uploader.rb15
-rw-r--r--app/uploaders/job_artifact_uploader.rb10
-rw-r--r--app/uploaders/legacy_artifact_uploader.rb2
-rw-r--r--app/uploaders/lfs_object_uploader.rb2
-rw-r--r--app/uploaders/namespace_file_uploader.rb2
-rw-r--r--app/uploaders/object_storage.rb77
-rw-r--r--app/uploaders/personal_file_uploader.rb2
-rw-r--r--app/uploaders/records_uploads.rb4
-rw-r--r--app/uploaders/uploader_helper.rb4
-rw-r--r--app/uploaders/workhorse.rb2
-rw-r--r--app/validators/abstract_path_validator.rb2
-rw-r--r--app/validators/addressable_url_validator.rb45
-rw-r--r--app/validators/certificate_fingerprint_validator.rb2
-rw-r--r--app/validators/certificate_key_validator.rb2
-rw-r--r--app/validators/certificate_validator.rb2
-rw-r--r--app/validators/cluster_name_validator.rb2
-rw-r--r--app/validators/color_validator.rb2
-rw-r--r--app/validators/cron_timezone_validator.rb2
-rw-r--r--app/validators/cron_validator.rb2
-rw-r--r--app/validators/duration_validator.rb2
-rw-r--r--app/validators/email_validator.rb2
-rw-r--r--app/validators/importable_url_validator.rb11
-rw-r--r--app/validators/key_restriction_validator.rb2
-rw-r--r--app/validators/line_code_validator.rb2
-rw-r--r--app/validators/namespace_name_validator.rb2
-rw-r--r--app/validators/namespace_path_validator.rb2
-rw-r--r--app/validators/project_path_validator.rb2
-rw-r--r--app/validators/public_url_validator.rb28
-rw-r--r--app/validators/top_level_group_validator.rb2
-rw-r--r--app/validators/url_placeholder_validator.rb32
-rw-r--r--app/validators/url_validator.rb64
-rw-r--r--app/validators/variable_duplicates_validator.rb6
-rw-r--r--app/views/abuse_report_mailer/notify.html.haml2
-rw-r--r--app/views/abuse_reports/new.html.haml12
-rw-r--r--app/views/admin/abuse_reports/_abuse_report.html.haml6
-rw-r--r--app/views/admin/abuse_reports/index.html.haml2
-rw-r--r--app/views/admin/appearances/_form.html.haml47
-rw-r--r--app/views/admin/application_settings/_abuse.html.haml11
-rw-r--r--app/views/admin/application_settings/_account_and_limit.html.haml50
-rw-r--r--app/views/admin/application_settings/_background_jobs.html.haml33
-rw-r--r--app/views/admin/application_settings/_ci_cd.html.haml66
-rw-r--r--app/views/admin/application_settings/_email.html.haml36
-rw-r--r--app/views/admin/application_settings/_gitaly.html.haml35
-rw-r--r--app/views/admin/application_settings/_help_page.html.haml25
-rw-r--r--app/views/admin/application_settings/_influx.html.haml104
-rw-r--r--app/views/admin/application_settings/_ip_limits.html.haml87
-rw-r--r--app/views/admin/application_settings/_koding.html.haml32
-rw-r--r--app/views/admin/application_settings/_logging.html.haml44
-rw-r--r--app/views/admin/application_settings/_outbound.html.haml11
-rw-r--r--app/views/admin/application_settings/_pages.html.haml28
-rw-r--r--app/views/admin/application_settings/_performance.html.haml25
-rw-r--r--app/views/admin/application_settings/_performance_bar.html.haml16
-rw-r--r--app/views/admin/application_settings/_plantuml.html.haml24
-rw-r--r--app/views/admin/application_settings/_prometheus.html.haml25
-rw-r--r--app/views/admin/application_settings/_realtime.html.haml23
-rw-r--r--app/views/admin/application_settings/_registry.html.haml7
-rw-r--r--app/views/admin/application_settings/_repository_check.html.haml86
-rw-r--r--app/views/admin/application_settings/_repository_mirrors_form.html.haml19
-rw-r--r--app/views/admin/application_settings/_repository_storage.html.haml79
-rw-r--r--app/views/admin/application_settings/_signin.html.haml82
-rw-r--r--app/views/admin/application_settings/_signup.html.haml80
-rw-r--r--app/views/admin/application_settings/_spam.html.haml82
-rw-r--r--app/views/admin/application_settings/_terminal.html.haml13
-rw-r--r--app/views/admin/application_settings/_terms.html.haml27
-rw-r--r--app/views/admin/application_settings/_third_party_offers.html.haml13
-rw-r--r--app/views/admin/application_settings/_usage.html.haml56
-rw-r--r--app/views/admin/application_settings/_visibility_and_access.html.haml87
-rw-r--r--app/views/admin/application_settings/show.html.haml24
-rw-r--r--app/views/admin/applications/_form.html.haml24
-rw-r--r--app/views/admin/applications/show.html.haml2
-rw-r--r--app/views/admin/background_jobs/show.html.haml8
-rw-r--r--app/views/admin/broadcast_messages/_form.html.haml26
-rw-r--r--app/views/admin/broadcast_messages/index.html.haml4
-rw-r--r--app/views/admin/conversational_development_index/_card.html.haml20
-rw-r--r--app/views/admin/conversational_development_index/_disabled.html.haml2
-rw-r--r--app/views/admin/conversational_development_index/_no_data.html.haml2
-rw-r--r--app/views/admin/conversational_development_index/show.html.haml4
-rw-r--r--app/views/admin/dashboard/index.html.haml86
-rw-r--r--app/views/admin/deploy_keys/edit.html.haml2
-rw-r--r--app/views/admin/deploy_keys/index.html.haml4
-rw-r--r--app/views/admin/deploy_keys/new.html.haml2
-rw-r--r--app/views/admin/gitaly_servers/index.html.haml4
-rw-r--r--app/views/admin/groups/_form.html.haml21
-rw-r--r--app/views/admin/groups/_group.html.haml7
-rw-r--r--app/views/admin/groups/show.html.haml64
-rw-r--r--app/views/admin/health_check/show.html.haml8
-rw-r--r--app/views/admin/hook_logs/_index.html.haml4
-rw-r--r--app/views/admin/hook_logs/show.html.haml2
-rw-r--r--app/views/admin/hooks/_form.html.haml18
-rw-r--r--app/views/admin/hooks/edit.html.haml4
-rw-r--r--app/views/admin/hooks/index.html.haml8
-rw-r--r--app/views/admin/identities/_form.html.haml12
-rw-r--r--app/views/admin/identities/_identity.html.haml10
-rw-r--r--app/views/admin/identities/edit.html.haml4
-rw-r--r--app/views/admin/identities/index.html.haml10
-rw-r--r--app/views/admin/identities/new.html.haml4
-rw-r--r--app/views/admin/labels/_form.html.haml27
-rw-r--r--app/views/admin/labels/_label.html.haml6
-rw-r--r--app/views/admin/labels/destroy.js.haml2
-rw-r--r--app/views/admin/labels/edit.html.haml8
-rw-r--r--app/views/admin/labels/index.html.haml12
-rw-r--r--app/views/admin/labels/new.html.haml4
-rw-r--r--app/views/admin/logs/show.html.haml8
-rw-r--r--app/views/admin/projects/_projects.html.haml4
-rw-r--r--app/views/admin/projects/index.html.haml4
-rw-r--r--app/views/admin/projects/show.html.haml64
-rw-r--r--app/views/admin/requests_profiles/index.html.haml4
-rw-r--r--app/views/admin/runners/_runner.html.haml18
-rw-r--r--app/views/admin/runners/index.html.haml18
-rw-r--r--app/views/admin/runners/show.html.haml14
-rw-r--r--app/views/admin/services/_form.html.haml5
-rw-r--r--app/views/admin/spam_logs/_spam_log.html.haml12
-rw-r--r--app/views/admin/system_info/show.html.haml8
-rw-r--r--app/views/admin/users/_access_levels.html.haml20
-rw-r--r--app/views/admin/users/_form.html.haml46
-rw-r--r--app/views/admin/users/_head.html.haml4
-rw-r--r--app/views/admin/users/_profile.html.haml6
-rw-r--r--app/views/admin/users/_projects.html.haml8
-rw-r--r--app/views/admin/users/_user.html.haml8
-rw-r--r--app/views/admin/users/index.html.haml18
-rw-r--r--app/views/admin/users/projects.html.haml20
-rw-r--r--app/views/admin/users/show.html.haml56
-rw-r--r--app/views/award_emoji/_awards_block.html.haml4
-rw-r--r--app/views/ci/lints/show.html.haml4
-rw-r--r--app/views/ci/runner/_how_to_setup_runner.html.haml2
-rw-r--r--app/views/ci/status/_dropdown_graph_badge.html.haml4
-rw-r--r--app/views/ci/variables/_variable_row.html.haml1
-rw-r--r--app/views/dashboard/_activity_head.html.haml2
-rw-r--r--app/views/dashboard/_groups_head.html.haml2
-rw-r--r--app/views/dashboard/_projects_head.html.haml2
-rw-r--r--app/views/dashboard/_snippets_head.html.haml4
-rw-r--r--app/views/dashboard/issues.html.haml3
-rw-r--r--app/views/dashboard/issues_calendar.ics.haml1
-rw-r--r--app/views/dashboard/projects/_nav.html.haml2
-rw-r--r--app/views/dashboard/projects/starred.html.haml2
-rw-r--r--app/views/dashboard/snippets/index.html.haml2
-rw-r--r--app/views/dashboard/todos/index.html.haml12
-rw-r--r--app/views/devise/sessions/_new_base.html.haml8
-rw-r--r--app/views/devise/sessions/_new_crowd.html.haml2
-rw-r--r--app/views/devise/sessions/_new_ldap.html.haml2
-rw-r--r--app/views/devise/sessions/two_factor.html.haml2
-rw-r--r--app/views/devise/shared/_omniauth_box.html.haml2
-rw-r--r--app/views/devise/shared/_signup_box.html.haml7
-rw-r--r--app/views/devise/shared/_tab_single.html.haml6
-rw-r--r--app/views/devise/shared/_tabs_ldap.html.haml18
-rw-r--r--app/views/devise/shared/_tabs_normal.html.haml10
-rw-r--r--app/views/discussions/_discussion.html.haml2
-rw-r--r--app/views/discussions/_notes.html.haml4
-rw-r--r--app/views/doorkeeper/applications/_form.html.haml4
-rw-r--r--app/views/doorkeeper/applications/index.html.haml2
-rw-r--r--app/views/doorkeeper/applications/show.html.haml2
-rw-r--r--app/views/doorkeeper/authorizations/new.html.haml2
-rw-r--r--app/views/doorkeeper/authorized_applications/index.html.haml2
-rw-r--r--app/views/email_rejection_mailer/rejection.html.haml1
-rw-r--r--app/views/email_rejection_mailer/rejection.text.haml1
-rw-r--r--app/views/errors/_footer.html.haml11
-rw-r--r--app/views/errors/access_denied.html.haml21
-rw-r--r--app/views/errors/not_found.html.haml19
-rw-r--r--app/views/events/_event_push.atom.haml2
-rw-r--r--app/views/events/event/_push.html.haml2
-rw-r--r--app/views/explore/groups/_nav.html.haml2
-rw-r--r--app/views/explore/groups/index.html.haml2
-rw-r--r--app/views/explore/projects/_filter.html.haml4
-rw-r--r--app/views/explore/projects/_nav.html.haml2
-rw-r--r--app/views/groups/_activities.html.haml2
-rw-r--r--app/views/groups/_create_chat_team.html.haml8
-rw-r--r--app/views/groups/_group_admin_settings.html.haml28
-rw-r--r--app/views/groups/edit.html.haml109
-rw-r--r--app/views/groups/group_members/_new_group_member.html.haml6
-rw-r--r--app/views/groups/group_members/index.html.haml11
-rw-r--r--app/views/groups/issues.html.haml7
-rw-r--r--app/views/groups/issues_calendar.ics.haml1
-rw-r--r--app/views/groups/labels/index.html.haml45
-rw-r--r--app/views/groups/merge_requests.html.haml2
-rw-r--r--app/views/groups/milestones/_form.html.haml10
-rw-r--r--app/views/groups/new.html.haml46
-rw-r--r--app/views/groups/projects.html.haml12
-rw-r--r--app/views/groups/runners/_runner.html.haml4
-rw-r--r--app/views/groups/settings/_advanced.html.haml49
-rw-r--r--app/views/groups/settings/_general.html.haml38
-rw-r--r--app/views/groups/settings/_permissions.html.haml28
-rw-r--r--app/views/groups/settings/ci_cd/show.html.haml6
-rw-r--r--app/views/groups/show.html.haml4
-rw-r--r--app/views/help/_shortcuts.html.haml175
-rw-r--r--app/views/help/index.html.haml8
-rw-r--r--app/views/help/ui.html.haml82
-rw-r--r--app/views/ide/index.html.haml4
-rw-r--r--app/views/import/_githubish_status.html.haml11
-rw-r--r--app/views/import/bitbucket/status.html.haml11
-rw-r--r--app/views/import/fogbugz/new.html.haml20
-rw-r--r--app/views/import/fogbugz/new_user_map.html.haml2
-rw-r--r--app/views/import/gitea/new.html.haml10
-rw-r--r--app/views/import/github/new.html.haml2
-rw-r--r--app/views/import/gitlab_projects/new.html.haml16
-rw-r--r--app/views/import/google_code/new.html.haml2
-rw-r--r--app/views/import/google_code/new_user_map.html.haml4
-rw-r--r--app/views/import/google_code/status.html.haml2
-rw-r--r--app/views/issues/_issues_calendar.ics.ruby15
-rw-r--r--app/views/kaminari/gitlab/_first_page.html.haml4
-rw-r--r--app/views/kaminari/gitlab/_gap.html.haml5
-rw-r--r--app/views/kaminari/gitlab/_last_page.html.haml4
-rw-r--r--app/views/kaminari/gitlab/_next_page.html.haml11
-rw-r--r--app/views/kaminari/gitlab/_page.html.haml4
-rw-r--r--app/views/kaminari/gitlab/_paginator.html.haml4
-rw-r--r--app/views/kaminari/gitlab/_prev_page.html.haml11
-rw-r--r--app/views/kaminari/gitlab/_without_count.html.haml12
-rw-r--r--app/views/layouts/_head.html.haml2
-rw-r--r--app/views/layouts/_search.html.haml2
-rw-r--r--app/views/layouts/devise.html.haml8
-rw-r--r--app/views/layouts/devise_empty.html.haml2
-rw-r--r--app/views/layouts/errors.html.haml66
-rw-r--r--app/views/layouts/header/_current_user_dropdown.html.haml1
-rw-r--r--app/views/layouts/header/_default.html.haml30
-rw-r--r--app/views/layouts/header/_empty.html.haml4
-rw-r--r--app/views/layouts/header/_new_dropdown.haml6
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml28
-rw-r--r--app/views/layouts/nav/groups_dropdown/_show.html.haml12
-rw-r--r--app/views/layouts/nav/projects_dropdown/_show.html.haml6
-rw-r--r--app/views/layouts/nav/sidebar/_admin.html.haml12
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml8
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml49
-rw-r--r--app/views/layouts/terms.html.haml8
-rw-r--r--app/views/notify/merge_request_unmergeable_email.html.haml2
-rw-r--r--app/views/notify/merge_request_unmergeable_email.text.haml8
-rw-r--r--app/views/notify/new_merge_request_email.html.haml2
-rw-r--r--app/views/notify/new_merge_request_email.text.erb2
-rw-r--r--app/views/notify/pipeline_failed_email.html.haml4
-rw-r--r--app/views/notify/pipeline_success_email.html.haml4
-rw-r--r--app/views/peek/_bar.html.haml2
-rw-r--r--app/views/profiles/_event_table.html.haml4
-rw-r--r--app/views/profiles/active_sessions/_active_session.html.haml8
-rw-r--r--app/views/profiles/active_sessions/index.html.haml5
-rw-r--r--app/views/profiles/chat_names/_chat_name.html.haml2
-rw-r--r--app/views/profiles/chat_names/new.html.haml2
-rw-r--r--app/views/profiles/emails/index.html.haml16
-rw-r--r--app/views/profiles/gpg_keys/_key.html.haml6
-rw-r--r--app/views/profiles/gpg_keys/_key_table.html.haml2
-rw-r--r--app/views/profiles/keys/_form.html.haml15
-rw-r--r--app/views/profiles/keys/_key.html.haml8
-rw-r--r--app/views/profiles/keys/_key_details.html.haml8
-rw-r--r--app/views/profiles/keys/_key_table.html.haml2
-rw-r--r--app/views/profiles/keys/index.html.haml9
-rw-r--r--app/views/profiles/notifications/_group_settings.html.haml2
-rw-r--r--app/views/profiles/notifications/_project_settings.html.haml2
-rw-r--r--app/views/profiles/notifications/show.html.haml2
-rw-r--r--app/views/profiles/passwords/edit.html.haml2
-rw-r--r--app/views/profiles/passwords/new.html.haml14
-rw-r--r--app/views/profiles/personal_access_tokens/index.html.haml24
-rw-r--r--app/views/profiles/preferences/show.html.haml14
-rw-r--r--app/views/profiles/show.html.haml7
-rw-r--r--app/views/profiles/two_factor_auths/_codes.html.haml2
-rw-r--r--app/views/profiles/two_factor_auths/show.html.haml2
-rw-r--r--app/views/projects/_bitbucket_import_modal.html.haml5
-rw-r--r--app/views/projects/_export.html.haml4
-rw-r--r--app/views/projects/_gitlab_import_modal.html.haml5
-rw-r--r--app/views/projects/_home_panel.html.haml6
-rw-r--r--app/views/projects/_import_project_pane.html.haml94
-rw-r--r--app/views/projects/_issuable_by_email.html.haml8
-rw-r--r--app/views/projects/_last_push.html.haml2
-rw-r--r--app/views/projects/_md_preview.html.haml2
-rw-r--r--app/views/projects/_merge_request_fast_forward_settings.html.haml13
-rw-r--r--app/views/projects/_merge_request_merge_method_settings.html.haml36
-rw-r--r--app/views/projects/_merge_request_merge_settings.html.haml24
-rw-r--r--app/views/projects/_merge_request_rebase_settings.html.haml13
-rw-r--r--app/views/projects/_merge_request_settings.html.haml15
-rw-r--r--app/views/projects/_new_project_fields.html.haml31
-rw-r--r--app/views/projects/_new_project_push_tip.html.haml6
-rw-r--r--app/views/projects/_project_templates.html.haml23
-rw-r--r--app/views/projects/_stat_anchor_list.html.haml8
-rw-r--r--app/views/projects/artifacts/browse.html.haml4
-rw-r--r--app/views/projects/artifacts/file.html.haml6
-rw-r--r--app/views/projects/blame/show.html.haml2
-rw-r--r--app/views/projects/blob/_blob.html.haml2
-rw-r--r--app/views/projects/blob/_breadcrumb.html.haml4
-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/blob/_header_content.html.haml2
-rw-r--r--app/views/projects/blob/_new_dir.html.haml9
-rw-r--r--app/views/projects/blob/_remove.html.haml9
-rw-r--r--app/views/projects/blob/_upload.html.haml5
-rw-r--r--app/views/projects/blob/edit.html.haml4
-rw-r--r--app/views/projects/blob/new.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_download.html.haml2
-rw-r--r--app/views/projects/branches/_branch.html.haml12
-rw-r--r--app/views/projects/branches/_delete_protected_modal.html.haml3
-rw-r--r--app/views/projects/branches/_panel.html.haml8
-rw-r--r--app/views/projects/branches/index.html.haml4
-rw-r--r--app/views/projects/branches/new.html.haml14
-rw-r--r--app/views/projects/buttons/_download.html.haml4
-rw-r--r--app/views/projects/buttons/_dropdown.html.haml4
-rw-r--r--app/views/projects/buttons/_xcode_link.html.haml2
-rw-r--r--app/views/projects/ci/builds/_build.html.haml10
-rw-r--r--app/views/projects/ci/lints/show.html.haml23
-rw-r--r--app/views/projects/clusters/_advanced_settings.html.haml4
-rw-r--r--app/views/projects/clusters/_dropdown.html.haml12
-rw-r--r--app/views/projects/clusters/_empty_state.html.haml4
-rw-r--r--app/views/projects/clusters/_gcp_signup_offer_banner.html.haml2
-rw-r--r--app/views/projects/clusters/_integration_form.html.haml35
-rw-r--r--app/views/projects/clusters/_sidebar.html.haml6
-rw-r--r--app/views/projects/clusters/gcp/_form.html.haml62
-rw-r--r--app/views/projects/clusters/gcp/_header.html.haml2
-rw-r--r--app/views/projects/clusters/gcp/_show.html.haml16
-rw-r--r--app/views/projects/clusters/gcp/login.html.haml21
-rw-r--r--app/views/projects/clusters/gcp/new.html.haml10
-rw-r--r--app/views/projects/clusters/new.html.haml35
-rw-r--r--app/views/projects/clusters/show.html.haml1
-rw-r--r--app/views/projects/clusters/user/_form.html.haml23
-rw-r--r--app/views/projects/clusters/user/_header.html.haml2
-rw-r--r--app/views/projects/clusters/user/_show.html.haml17
-rw-r--r--app/views/projects/clusters/user/new.html.haml11
-rw-r--r--app/views/projects/commit/_ajax_signature.html.haml2
-rw-r--r--app/views/projects/commit/_change.html.haml23
-rw-r--r--app/views/projects/commit/_ci_menu.html.haml6
-rw-r--r--app/views/projects/commit/_commit_box.html.haml10
-rw-r--r--app/views/projects/commit/_signature_badge.html.haml2
-rw-r--r--app/views/projects/commit/branches.html.haml3
-rw-r--r--app/views/projects/commits/_commit.html.haml11
-rw-r--r--app/views/projects/commits/_commit_list.html.haml4
-rw-r--r--app/views/projects/commits/_inline_commit.html.haml2
-rw-r--r--app/views/projects/commits/show.html.haml2
-rw-r--r--app/views/projects/compare/_form.html.haml51
-rw-r--r--app/views/projects/compare/show.html.haml2
-rw-r--r--app/views/projects/cycle_analytics/_overview.html.haml2
-rw-r--r--app/views/projects/cycle_analytics/show.html.haml14
-rw-r--r--app/views/projects/deploy_keys/_form.html.haml22
-rw-r--r--app/views/projects/deploy_keys/_index.html.haml2
-rw-r--r--app/views/projects/deploy_keys/edit.html.haml2
-rw-r--r--app/views/projects/deploy_tokens/_index.html.haml11
-rw-r--r--app/views/projects/deploy_tokens/_new_deploy_token.html.haml28
-rw-r--r--app/views/projects/deploy_tokens/_revoke_modal.html.haml8
-rw-r--r--app/views/projects/deploy_tokens/_table.html.haml2
-rw-r--r--app/views/projects/deployments/_actions.haml9
-rw-r--r--app/views/projects/deployments/_commit.html.haml2
-rw-r--r--app/views/projects/deployments/_deployment.html.haml8
-rw-r--r--app/views/projects/deployments/_rollback.haml7
-rw-r--r--app/views/projects/diffs/_collapsed.html.haml2
-rw-r--r--app/views/projects/diffs/_diffs.html.haml8
-rw-r--r--app/views/projects/diffs/_file.html.haml2
-rw-r--r--app/views/projects/diffs/_file_header.html.haml2
-rw-r--r--app/views/projects/diffs/_stats.html.haml4
-rw-r--r--app/views/projects/diffs/_warning.html.haml2
-rw-r--r--app/views/projects/edit.html.haml36
-rw-r--r--app/views/projects/empty.html.haml15
-rw-r--r--app/views/projects/environments/_external_url.html.haml4
-rw-r--r--app/views/projects/environments/_metrics_button.html.haml2
-rw-r--r--app/views/projects/environments/_stop.html.haml5
-rw-r--r--app/views/projects/environments/_terminal_button.html.haml2
-rw-r--r--app/views/projects/environments/empty.html.haml14
-rw-r--r--app/views/projects/environments/metrics.html.haml9
-rw-r--r--app/views/projects/environments/show.html.haml32
-rw-r--r--app/views/projects/environments/terminal.html.haml2
-rw-r--r--app/views/projects/find_file/show.html.haml4
-rw-r--r--app/views/projects/forks/index.html.haml2
-rw-r--r--app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml4
-rw-r--r--app/views/projects/graphs/charts.html.haml4
-rw-r--r--app/views/projects/graphs/show.html.haml2
-rw-r--r--app/views/projects/hook_logs/_index.html.haml4
-rw-r--r--app/views/projects/hook_logs/show.html.haml2
-rw-r--r--app/views/projects/hooks/_index.html.haml2
-rw-r--r--app/views/projects/hooks/edit.html.haml2
-rw-r--r--app/views/projects/imports/new.html.haml8
-rw-r--r--app/views/projects/issues/_discussion.html.haml3
-rw-r--r--app/views/projects/issues/_form.html.haml4
-rw-r--r--app/views/projects/issues/_issue.html.haml12
-rw-r--r--app/views/projects/issues/_nav_btns.html.haml4
-rw-r--r--app/views/projects/issues/_new_branch.html.haml10
-rw-r--r--app/views/projects/issues/calendar.ics.haml1
-rw-r--r--app/views/projects/issues/show.html.haml18
-rw-r--r--app/views/projects/jobs/_empty_state.html.haml4
-rw-r--r--app/views/projects/jobs/_header.html.haml2
-rw-r--r--app/views/projects/jobs/_sidebar.html.haml14
-rw-r--r--app/views/projects/jobs/_user.html.haml4
-rw-r--r--app/views/projects/jobs/show.html.haml10
-rw-r--r--app/views/projects/jobs/terminal.html.haml11
-rw-r--r--app/views/projects/labels/index.html.haml34
-rw-r--r--app/views/projects/mattermosts/_no_teams.html.haml2
-rw-r--r--app/views/projects/mattermosts/_team_selection.html.haml10
-rw-r--r--app/views/projects/mattermosts/new.html.haml2
-rw-r--r--app/views/projects/merge_requests/_form.html.haml4
-rw-r--r--app/views/projects/merge_requests/_how_to_merge.html.haml7
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml18
-rw-r--r--app/views/projects/merge_requests/_mr_box.html.haml2
-rw-r--r--app/views/projects/merge_requests/_mr_title.html.haml12
-rw-r--r--app/views/projects/merge_requests/conflicts/_submit_form.html.haml12
-rw-r--r--app/views/projects/merge_requests/creations/_new_compare.html.haml16
-rw-r--r--app/views/projects/merge_requests/creations/_new_submit.html.haml41
-rw-r--r--app/views/projects/merge_requests/diffs/_commit_widget.html.haml2
-rw-r--r--app/views/projects/merge_requests/diffs/_diffs.html.haml6
-rw-r--r--app/views/projects/merge_requests/diffs/_not_all_comments_displayed.html.haml2
-rw-r--r--app/views/projects/merge_requests/diffs/_version_controls.html.haml4
-rw-r--r--app/views/projects/merge_requests/invalid.html.haml8
-rw-r--r--app/views/projects/merge_requests/show.html.haml77
-rw-r--r--app/views/projects/milestones/_form.html.haml18
-rw-r--r--app/views/projects/milestones/index.html.haml2
-rw-r--r--app/views/projects/milestones/show.html.haml6
-rw-r--r--app/views/projects/mirrors/_push.html.haml6
-rw-r--r--app/views/projects/network/show.html.haml6
-rw-r--r--app/views/projects/new.html.haml28
-rw-r--r--app/views/projects/no_repo.html.haml2
-rw-r--r--app/views/projects/pages/_access.html.haml6
-rw-r--r--app/views/projects/pages/_destroy.haml8
-rw-r--r--app/views/projects/pages/_https_only.html.haml2
-rw-r--r--app/views/projects/pages/_list.html.haml10
-rw-r--r--app/views/projects/pages/_no_domains.html.haml4
-rw-r--r--app/views/projects/pages/_use.html.haml6
-rw-r--r--app/views/projects/pages/show.html.haml2
-rw-r--r--app/views/projects/pages_domains/_form.html.haml12
-rw-r--r--app/views/projects/pages_domains/edit.html.haml2
-rw-r--r--app/views/projects/pages_domains/new.html.haml4
-rw-r--r--app/views/projects/pages_domains/show.html.haml18
-rw-r--r--app/views/projects/pipeline_schedules/_form.html.haml14
-rw-r--r--app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml2
-rw-r--r--app/views/projects/pipeline_schedules/_tabs.html.haml8
-rw-r--r--app/views/projects/pipeline_schedules/index.html.haml2
-rw-r--r--app/views/projects/pipelines/_info.html.haml4
-rw-r--r--app/views/projects/pipelines/_with_tabs.html.haml50
-rw-r--r--app/views/projects/pipelines/new.html.haml10
-rw-r--r--app/views/projects/project_members/_groups.html.haml8
-rw-r--r--app/views/projects/project_members/_new_project_member.html.haml2
-rw-r--r--app/views/projects/project_members/_new_shared_group.html.haml2
-rw-r--r--app/views/projects/project_members/_team.html.haml13
-rw-r--r--app/views/projects/project_members/import.html.haml6
-rw-r--r--app/views/projects/project_members/index.html.haml12
-rw-r--r--app/views/projects/protected_branches/shared/_branches_list.html.haml4
-rw-r--r--app/views/projects/protected_branches/shared/_create_protected_branch.html.haml57
-rw-r--r--app/views/projects/protected_branches/shared/_index.html.haml6
-rw-r--r--app/views/projects/protected_branches/shared/_matching_branch.html.haml2
-rw-r--r--app/views/projects/protected_branches/shared/_protected_branch.html.haml2
-rw-r--r--app/views/projects/protected_tags/shared/_create_protected_tag.html.haml49
-rw-r--r--app/views/projects/protected_tags/shared/_index.html.haml4
-rw-r--r--app/views/projects/protected_tags/shared/_matching_tag.html.haml2
-rw-r--r--app/views/projects/protected_tags/shared/_protected_tag.html.haml2
-rw-r--r--app/views/projects/protected_tags/shared/_tags_list.html.haml4
-rw-r--r--app/views/projects/refs/logs_tree.js.haml2
-rw-r--r--app/views/projects/registry/repositories/_tag.html.haml2
-rw-r--r--app/views/projects/registry/repositories/index.html.haml8
-rw-r--r--app/views/projects/releases/edit.html.haml6
-rw-r--r--app/views/projects/repositories/_feed.html.haml2
-rw-r--r--app/views/projects/runners/_group_runners.html.haml4
-rw-r--r--app/views/projects/runners/_runner.html.haml8
-rw-r--r--app/views/projects/services/_form.html.haml2
-rw-r--r--app/views/projects/services/_index.html.haml6
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml86
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_help.html.haml27
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml2
-rw-r--r--app/views/projects/services/prometheus/_configuration_banner.html.haml2
-rw-r--r--app/views/projects/services/prometheus/_help.html.haml2
-rw-r--r--app/views/projects/services/prometheus/_show.html.haml20
-rw-r--r--app/views/projects/services/slack_slash_commands/_help.html.haml169
-rw-r--r--app/views/projects/settings/ci_cd/_autodevops_form.html.haml83
-rw-r--r--app/views/projects/settings/ci_cd/_badge.html.haml8
-rw-r--r--app/views/projects/settings/ci_cd/_form.html.haml91
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml38
-rw-r--r--app/views/projects/settings/integrations/_project_hook.html.haml10
-rw-r--r--app/views/projects/settings/integrations/show.html.haml4
-rw-r--r--app/views/projects/settings/members/show.html.haml2
-rw-r--r--app/views/projects/settings/repository/show.html.haml4
-rw-r--r--app/views/projects/snippets/_actions.html.haml30
-rw-r--r--app/views/projects/snippets/edit.html.haml6
-rw-r--r--app/views/projects/snippets/index.html.haml4
-rw-r--r--app/views/projects/snippets/new.html.haml8
-rw-r--r--app/views/projects/snippets/show.html.haml4
-rw-r--r--app/views/projects/tags/_tag.html.haml2
-rw-r--r--app/views/projects/tags/index.html.haml2
-rw-r--r--app/views/projects/tags/new.html.haml26
-rw-r--r--app/views/projects/tags/show.html.haml2
-rw-r--r--app/views/projects/tree/_blob_item.html.haml4
-rw-r--r--app/views/projects/tree/_submodule_item.html.haml2
-rw-r--r--app/views/projects/tree/_tree_content.html.haml6
-rw-r--r--app/views/projects/tree/_tree_header.html.haml12
-rw-r--r--app/views/projects/tree/_tree_item.html.haml2
-rw-r--r--app/views/projects/triggers/_content.html.haml2
-rw-r--r--app/views/projects/triggers/_form.html.haml2
-rw-r--r--app/views/projects/triggers/_index.html.haml10
-rw-r--r--app/views/projects/triggers/_trigger.html.haml4
-rw-r--r--app/views/projects/wikis/_form.html.haml20
-rw-r--r--app/views/projects/wikis/_new.html.haml3
-rw-r--r--app/views/projects/wikis/_pages_wiki_page.html.haml2
-rw-r--r--app/views/projects/wikis/_sidebar.html.haml2
-rw-r--r--app/views/projects/wikis/edit.html.haml2
-rw-r--r--app/views/projects/wikis/empty.html.haml6
-rw-r--r--app/views/projects/wikis/git_access.html.haml4
-rw-r--r--app/views/projects/wikis/show.html.haml2
-rw-r--r--app/views/search/_category.html.haml28
-rw-r--r--app/views/search/_filter.html.haml4
-rw-r--r--app/views/search/results/_blob.html.haml18
-rw-r--r--app/views/search/results/_blob_data.html.haml10
-rw-r--r--app/views/search/results/_commit.html.haml2
-rw-r--r--app/views/search/results/_issue.html.haml4
-rw-r--r--app/views/search/results/_merge_request.html.haml6
-rw-r--r--app/views/search/results/_snippet_title.html.haml6
-rw-r--r--app/views/search/results/_wiki_blob.html.haml15
-rw-r--r--app/views/shared/_allow_request_access.html.haml6
-rw-r--r--app/views/shared/_choose_group_avatar_button.html.haml2
-rw-r--r--app/views/shared/_clone_panel.html.haml12
-rw-r--r--app/views/shared/_commit_message_container.html.haml4
-rw-r--r--app/views/shared/_commit_well.html.haml2
-rw-r--r--app/views/shared/_confirm_modal.html.haml3
-rw-r--r--app/views/shared/_delete_label_modal.html.haml3
-rw-r--r--app/views/shared/_email_with_badge.html.haml2
-rw-r--r--app/views/shared/_event_filter.html.haml2
-rw-r--r--app/views/shared/_field.html.haml8
-rw-r--r--app/views/shared/_group_form.html.haml25
-rw-r--r--app/views/shared/_import_form.html.haml21
-rw-r--r--app/views/shared/_issuable_meta_data.html.haml8
-rw-r--r--app/views/shared/_issues.html.haml2
-rw-r--r--app/views/shared/_label.html.haml133
-rw-r--r--app/views/shared/_label_row.html.haml35
-rw-r--r--app/views/shared/_merge_requests.html.haml2
-rw-r--r--app/views/shared/_milestone_expired.html.haml9
-rw-r--r--app/views/shared/_milestones_filter.html.haml8
-rw-r--r--app/views/shared/_milestones_sort_dropdown.html.haml2
-rw-r--r--app/views/shared/_new_commit_form.html.haml4
-rw-r--r--app/views/shared/_new_merge_request_checkbox.html.haml6
-rw-r--r--app/views/shared/_new_project_item_select.html.haml4
-rw-r--r--app/views/shared/_personal_access_tokens_form.html.haml16
-rw-r--r--app/views/shared/_personal_access_tokens_table.html.haml2
-rw-r--r--app/views/shared/_project_limit.html.haml4
-rw-r--r--app/views/shared/_ref_switcher.html.haml2
-rw-r--r--app/views/shared/_service_settings.html.haml30
-rw-r--r--app/views/shared/_sort_dropdown.html.haml4
-rw-r--r--app/views/shared/_user_dropdown_contributing_link.html.haml5
-rw-r--r--app/views/shared/_visibility_level.html.haml6
-rw-r--r--app/views/shared/_visibility_radios.html.haml6
-rw-r--r--app/views/shared/boards/_show.html.haml11
-rw-r--r--app/views/shared/boards/components/_board.html.haml28
-rw-r--r--app/views/shared/boards/components/_sidebar.html.haml7
-rw-r--r--app/views/shared/boards/components/sidebar/_due_date.html.haml12
-rw-r--r--app/views/shared/boards/components/sidebar/_labels.html.haml10
-rw-r--r--app/views/shared/boards/components/sidebar/_milestone.html.haml12
-rw-r--r--app/views/shared/builds/_build_output.html.haml3
-rw-r--r--app/views/shared/builds/_tabs.html.haml10
-rw-r--r--app/views/shared/dashboard/_no_filter_selected.html.haml4
-rw-r--r--app/views/shared/deploy_keys/_form.html.haml8
-rw-r--r--app/views/shared/empty_states/_issues.html.haml6
-rw-r--r--app/views/shared/empty_states/_labels.html.haml4
-rw-r--r--app/views/shared/empty_states/_merge_requests.html.haml6
-rw-r--r--app/views/shared/empty_states/_wikis.html.haml30
-rw-r--r--app/views/shared/empty_states/_wikis_layout.html.haml7
-rw-r--r--app/views/shared/form_elements/_description.html.haml6
-rw-r--r--app/views/shared/groups/_dropdown.html.haml2
-rw-r--r--app/views/shared/groups/_group.html.haml2
-rw-r--r--app/views/shared/hook_logs/_content.html.haml8
-rw-r--r--app/views/shared/hook_logs/_status_label.html.haml2
-rw-r--r--app/views/shared/icons/_icon_calendar.svg1
-rw-r--r--app/views/shared/issuable/_board_create_list_dropdown.html.haml8
-rw-r--r--app/views/shared/issuable/_bulk_update_sidebar.html.haml4
-rw-r--r--app/views/shared/issuable/_close_reopen_button.html.haml6
-rw-r--r--app/views/shared/issuable/_close_reopen_report_toggle.html.haml6
-rw-r--r--app/views/shared/issuable/_feed_buttons.html.haml4
-rw-r--r--app/views/shared/issuable/_filter.html.haml2
-rw-r--r--app/views/shared/issuable/_form.html.haml23
-rw-r--r--app/views/shared/issuable/_label_page_create.html.haml7
-rw-r--r--app/views/shared/issuable/_label_page_default.html.haml7
-rw-r--r--app/views/shared/issuable/_milestone_dropdown.html.haml4
-rw-r--r--app/views/shared/issuable/_nav.html.haml2
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml21
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml37
-rw-r--r--app/views/shared/issuable/_sidebar_assignees.html.haml12
-rw-r--r--app/views/shared/issuable/_sidebar_todo.html.haml2
-rw-r--r--app/views/shared/issuable/form/_branch_chooser.html.haml8
-rw-r--r--app/views/shared/issuable/form/_contribution.html.haml18
-rw-r--r--app/views/shared/issuable/form/_default_templates.html.haml4
-rw-r--r--app/views/shared/issuable/form/_issue_assignee.html.haml2
-rw-r--r--app/views/shared/issuable/form/_merge_params.html.haml27
-rw-r--r--app/views/shared/issuable/form/_merge_request_assignee.html.haml4
-rw-r--r--app/views/shared/issuable/form/_metadata.html.haml29
-rw-r--r--app/views/shared/issuable/form/_metadata_issue_assignee.html.haml4
-rw-r--r--app/views/shared/issuable/form/_metadata_merge_request_assignee.html.haml2
-rw-r--r--app/views/shared/issuable/form/_title.html.haml9
-rw-r--r--app/views/shared/labels/_form.html.haml21
-rw-r--r--app/views/shared/members/_group.html.haml24
-rw-r--r--app/views/shared/members/_member.html.haml18
-rw-r--r--app/views/shared/members/_requests.html.haml6
-rw-r--r--app/views/shared/members/_sort_dropdown.html.haml2
-rw-r--r--app/views/shared/milestones/_form_dates.html.haml17
-rw-r--r--app/views/shared/milestones/_issuables.html.haml8
-rw-r--r--app/views/shared/milestones/_labels_tab.html.haml2
-rw-r--r--app/views/shared/milestones/_milestone.html.haml109
-rw-r--r--app/views/shared/milestones/_sidebar.html.haml36
-rw-r--r--app/views/shared/milestones/_tabs.html.haml32
-rw-r--r--app/views/shared/milestones/_top.html.haml6
-rw-r--r--app/views/shared/notes/_comment_button.html.haml2
-rw-r--r--app/views/shared/notes/_edit_form.html.haml2
-rw-r--r--app/views/shared/notes/_form.html.haml2
-rw-r--r--app/views/shared/notes/_hints.html.haml2
-rw-r--r--app/views/shared/notes/_note.html.haml8
-rw-r--r--app/views/shared/notes/_notes_with_form.html.haml9
-rw-r--r--app/views/shared/notifications/_button.html.haml4
-rw-r--r--app/views/shared/notifications/_custom_notifications.html.haml10
-rw-r--r--app/views/shared/plugins/_index.html.haml6
-rw-r--r--app/views/shared/projects/_dropdown.html.haml4
-rw-r--r--app/views/shared/projects/_edit_information.html.haml2
-rw-r--r--app/views/shared/projects/_project.html.haml4
-rw-r--r--app/views/shared/runners/_form.html.haml58
-rw-r--r--app/views/shared/runners/_runner_description.html.haml4
-rw-r--r--app/views/shared/runners/show.html.haml20
-rw-r--r--app/views/shared/snippets/_blob.html.haml2
-rw-r--r--app/views/shared/snippets/_embed.html.haml2
-rw-r--r--app/views/shared/snippets/_form.html.haml12
-rw-r--r--app/views/shared/snippets/_header.html.haml13
-rw-r--r--app/views/shared/snippets/_snippet.html.haml8
-rw-r--r--app/views/shared/tokens/_scopes_form.html.haml9
-rw-r--r--app/views/shared/web_hooks/_form.html.haml120
-rw-r--r--app/views/shared/web_hooks/_test_button.html.haml2
-rw-r--r--app/views/sherlock/file_samples/show.html.haml2
-rw-r--r--app/views/sherlock/queries/_backtrace.html.haml12
-rw-r--r--app/views/sherlock/queries/_general.html.haml26
-rw-r--r--app/views/sherlock/queries/show.html.haml4
-rw-r--r--app/views/sherlock/transactions/_file_samples.html.haml2
-rw-r--r--app/views/sherlock/transactions/_general.html.haml6
-rw-r--r--app/views/sherlock/transactions/_queries.html.haml2
-rw-r--r--app/views/sherlock/transactions/index.html.haml4
-rw-r--r--app/views/sherlock/transactions/show.html.haml8
-rw-r--r--app/views/snippets/_actions.html.haml4
-rw-r--r--app/views/snippets/_snippets_scope_menu.html.haml10
-rw-r--r--app/views/snippets/index.html.haml6
-rw-r--r--app/views/u2f/_authenticate.html.haml3
-rw-r--r--app/views/users/show.html.haml4
-rw-r--r--app/views/users/terms/index.html.haml25
-rw-r--r--app/workers/admin_email_worker.rb2
-rw-r--r--app/workers/all_queues.yml11
-rw-r--r--app/workers/archive_trace_worker.rb4
-rw-r--r--app/workers/authorized_projects_worker.rb2
-rw-r--r--app/workers/background_migration_worker.rb2
-rw-r--r--app/workers/build_coverage_worker.rb2
-rw-r--r--app/workers/build_finished_worker.rb2
-rw-r--r--app/workers/build_hooks_worker.rb2
-rw-r--r--app/workers/build_queue_worker.rb2
-rw-r--r--app/workers/build_success_worker.rb2
-rw-r--r--app/workers/build_trace_sections_worker.rb2
-rw-r--r--app/workers/check_gcp_project_billing_worker.rb92
-rw-r--r--app/workers/ci/archive_traces_cron_worker.rb29
-rw-r--r--app/workers/ci/build_trace_chunk_flush_worker.rb4
-rw-r--r--app/workers/cluster_install_app_worker.rb2
-rw-r--r--app/workers/cluster_provision_worker.rb2
-rw-r--r--app/workers/cluster_wait_for_app_installation_worker.rb2
-rw-r--r--app/workers/cluster_wait_for_ingress_ip_address_worker.rb2
-rw-r--r--app/workers/concerns/application_worker.rb2
-rw-r--r--app/workers/concerns/cluster_applications.rb2
-rw-r--r--app/workers/concerns/cluster_queue.rb2
-rw-r--r--app/workers/concerns/cronjob_queue.rb2
-rw-r--r--app/workers/concerns/each_shard_worker.rb31
-rw-r--r--app/workers/concerns/exception_backtrace.rb2
-rw-r--r--app/workers/concerns/gitlab/github_import/queue.rb2
-rw-r--r--app/workers/concerns/mail_scheduler_queue.rb2
-rw-r--r--app/workers/concerns/new_issuable.rb2
-rw-r--r--app/workers/concerns/object_storage_queue.rb2
-rw-r--r--app/workers/concerns/pipeline_background_queue.rb2
-rw-r--r--app/workers/concerns/pipeline_queue.rb2
-rw-r--r--app/workers/concerns/project_import_options.rb2
-rw-r--r--app/workers/concerns/project_start_import.rb2
-rw-r--r--app/workers/concerns/repository_check_queue.rb2
-rw-r--r--app/workers/concerns/waitable_worker.rb2
-rw-r--r--app/workers/create_gpg_signature_worker.rb2
-rw-r--r--app/workers/create_note_diff_file_worker.rb11
-rw-r--r--app/workers/create_pipeline_worker.rb2
-rw-r--r--app/workers/delete_diff_files_worker.rb17
-rw-r--r--app/workers/delete_merged_branches_worker.rb2
-rw-r--r--app/workers/delete_user_worker.rb2
-rw-r--r--app/workers/email_receiver_worker.rb10
-rw-r--r--app/workers/emails_on_push_worker.rb2
-rw-r--r--app/workers/expire_build_artifacts_worker.rb2
-rw-r--r--app/workers/expire_build_instance_artifacts_worker.rb2
-rw-r--r--app/workers/expire_job_cache_worker.rb2
-rw-r--r--app/workers/expire_pipeline_cache_worker.rb2
-rw-r--r--app/workers/git_garbage_collect_worker.rb60
-rw-r--r--app/workers/gitlab/github_import/advance_stage_worker.rb1
-rw-r--r--app/workers/gitlab/github_import/import_lfs_object_worker.rb25
-rw-r--r--app/workers/gitlab/github_import/stage/import_lfs_objects_worker.rb32
-rw-r--r--app/workers/gitlab/github_import/stage/import_notes_worker.rb2
-rw-r--r--app/workers/gitlab_shell_worker.rb2
-rw-r--r--app/workers/gitlab_usage_ping_worker.rb2
-rw-r--r--app/workers/group_destroy_worker.rb2
-rw-r--r--app/workers/import_export_project_cleanup_worker.rb2
-rw-r--r--app/workers/invalid_gpg_signature_update_worker.rb2
-rw-r--r--app/workers/irker_worker.rb17
-rw-r--r--app/workers/issue_due_scheduler_worker.rb2
-rw-r--r--app/workers/mail_scheduler/issue_due_worker.rb2
-rw-r--r--app/workers/mail_scheduler/notification_service_worker.rb2
-rw-r--r--app/workers/merge_worker.rb2
-rw-r--r--app/workers/namespaceless_project_destroy_worker.rb2
-rw-r--r--app/workers/new_issue_worker.rb2
-rw-r--r--app/workers/new_merge_request_worker.rb2
-rw-r--r--app/workers/new_note_worker.rb2
-rw-r--r--app/workers/object_storage/background_move_worker.rb2
-rw-r--r--app/workers/object_storage/migrate_uploads_worker.rb2
-rw-r--r--app/workers/object_storage_upload_worker.rb21
-rw-r--r--app/workers/pages_domain_verification_cron_worker.rb2
-rw-r--r--app/workers/pages_domain_verification_worker.rb2
-rw-r--r--app/workers/pages_worker.rb2
-rw-r--r--app/workers/pipeline_hooks_worker.rb2
-rw-r--r--app/workers/pipeline_metrics_worker.rb2
-rw-r--r--app/workers/pipeline_notification_worker.rb2
-rw-r--r--app/workers/pipeline_process_worker.rb2
-rw-r--r--app/workers/pipeline_schedule_worker.rb2
-rw-r--r--app/workers/pipeline_success_worker.rb2
-rw-r--r--app/workers/pipeline_update_worker.rb2
-rw-r--r--app/workers/plugin_worker.rb2
-rw-r--r--app/workers/post_receive.rb2
-rw-r--r--app/workers/process_commit_worker.rb7
-rw-r--r--app/workers/project_cache_worker.rb2
-rw-r--r--app/workers/project_destroy_worker.rb2
-rw-r--r--app/workers/project_export_worker.rb2
-rw-r--r--app/workers/project_migrate_hashed_storage_worker.rb2
-rw-r--r--app/workers/project_service_worker.rb2
-rw-r--r--app/workers/propagate_service_template_worker.rb2
-rw-r--r--app/workers/prune_old_events_worker.rb2
-rw-r--r--app/workers/prune_web_hook_logs_worker.rb26
-rw-r--r--app/workers/reactive_caching_worker.rb2
-rw-r--r--app/workers/rebase_worker.rb2
-rw-r--r--app/workers/remove_expired_group_links_worker.rb2
-rw-r--r--app/workers/remove_expired_members_worker.rb2
-rw-r--r--app/workers/remove_old_web_hook_logs_worker.rb2
-rw-r--r--app/workers/remove_unreferenced_lfs_objects_worker.rb2
-rw-r--r--app/workers/repository_archive_cache_worker.rb2
-rw-r--r--app/workers/repository_check/batch_worker.rb39
-rw-r--r--app/workers/repository_check/clear_worker.rb2
-rw-r--r--app/workers/repository_check/dispatch_worker.rb24
-rw-r--r--app/workers/repository_check/single_repository_worker.rb2
-rw-r--r--app/workers/repository_fork_worker.rb27
-rw-r--r--app/workers/repository_import_worker.rb2
-rw-r--r--app/workers/repository_remove_remote_worker.rb2
-rw-r--r--app/workers/repository_update_remote_mirror_worker.rb2
-rw-r--r--app/workers/requests_profiles_worker.rb2
-rw-r--r--app/workers/run_pipeline_schedule_worker.rb2
-rw-r--r--app/workers/schedule_update_user_activity_worker.rb2
-rw-r--r--app/workers/stage_update_worker.rb2
-rw-r--r--app/workers/storage_migrator_worker.rb27
-rw-r--r--app/workers/stuck_ci_jobs_worker.rb2
-rw-r--r--app/workers/stuck_import_jobs_worker.rb2
-rw-r--r--app/workers/stuck_merge_jobs_worker.rb2
-rw-r--r--app/workers/system_hook_push_worker.rb2
-rw-r--r--app/workers/trending_projects_worker.rb2
-rw-r--r--app/workers/update_head_pipeline_for_merge_request_worker.rb2
-rw-r--r--app/workers/update_merge_requests_worker.rb2
-rw-r--r--app/workers/update_user_activity_worker.rb2
-rw-r--r--app/workers/upload_checksum_worker.rb2
-rw-r--r--app/workers/wait_for_cluster_creation_worker.rb2
-rw-r--r--app/workers/web_hook_worker.rb2
-rwxr-xr-xbin/changelog61
-rw-r--r--changelogs/no-rm-rf-gitlab-basics.yml5
-rw-r--r--changelogs/unreleased/18141-osw-use-monospaced-font-on-diffs-commit-ref.yml5
-rw-r--r--changelogs/unreleased/18524-fix-double-brackets-in-wiki-markdown.yml5
-rw-r--r--changelogs/unreleased/19439-api-file-sha56-and-head.yml5
-rw-r--r--changelogs/unreleased/19468-add_readme_when_creating_project.yml5
-rw-r--r--changelogs/unreleased/19861-expand-api-render-an-arbitrary-markdown-document.yml5
-rw-r--r--changelogs/unreleased/20357.yml5
-rw-r--r--changelogs/unreleased/22647-width-contributors-graphs.yml5
-rw-r--r--changelogs/unreleased/22846-notifications-broken-during-email-address-change-before-email-confirmed.yml6
-rw-r--r--changelogs/unreleased/23465-print-markdown.yml5
-rw-r--r--changelogs/unreleased/31583-osw-gfm-complete-status-indication.yml5
-rw-r--r--changelogs/unreleased/35158-snippets-api-visibility.yml5
-rw-r--r--changelogs/unreleased/36234-nav-add-groups-dropdown.yml5
-rw-r--r--changelogs/unreleased/36907-fix-new-issue-link-from-failed-job.yml5
-rw-r--r--changelogs/unreleased/37561-add-id-settings.yml5
-rw-r--r--changelogs/unreleased/39543-milestone-page-list-redesign.yml5
-rw-r--r--changelogs/unreleased/39584-nesting-depth-5-pages-pipelines.yml5
-rw-r--r--changelogs/unreleased/39604-update-top-right-avatar-after-changing-avatar.yml5
-rw-r--r--changelogs/unreleased/39710-search-placeholder-cut-off.yml5
-rw-r--r--changelogs/unreleased/40005-u2f-unspported-browsers.yml5
-rw-r--r--changelogs/unreleased/40484-ordered-lists-copy-gfm.yml5
-rw-r--r--changelogs/unreleased/40725-move-mr-external-link-to-right.yml5
-rw-r--r--changelogs/unreleased/40855_remove_authentication_in_readonly_issue_api.yml5
-rw-r--r--changelogs/unreleased/41671-fixing-milestone-date-change-when-editing.yml5
-rw-r--r--changelogs/unreleased/42342-teams-pipeline-notifications.yml5
-rw-r--r--changelogs/unreleased/42415-omit-projects-from-get-group-endpoint.yml5
-rw-r--r--changelogs/unreleased/42531-open-invite-404.yml5
-rw-r--r--changelogs/unreleased/43270-import-with-milestones-failing.yml5
-rw-r--r--changelogs/unreleased/43367-fix-board-long-strings.yml5
-rw-r--r--changelogs/unreleased/43446-new-cluster-page-tabs.yml5
-rw-r--r--changelogs/unreleased/43472-remove-environment-scope-field-on-cluster-creation-form-for-core-starter-plans.yml5
-rw-r--r--changelogs/unreleased/43673-operations-tab-mvc.yml5
-rw-r--r--changelogs/unreleased/44319-remove-gray-buttons.yml5
-rw-r--r--changelogs/unreleased/44674-use-one-column-form-layout-on-admin-area-settings-page.yml5
-rw-r--r--changelogs/unreleased/44697-when-editing-a-comment-in-an-issue-the-preview-mode-is-toggled-in-the-main-textarea.yml6
-rw-r--r--changelogs/unreleased/44725-expire_correct_methods_after_change_head.yml5
-rw-r--r--changelogs/unreleased/44799-api-naming-issue-scope.yml5
-rw-r--r--changelogs/unreleased/45065-users-projects-json-sort.yml5
-rw-r--r--changelogs/unreleased/45400-automatically-created-mr-uses-wrong-target-branch-when-branching-from-tag.yml5
-rw-r--r--changelogs/unreleased/45442-updates-updated-at-to-issue-on-time-spent.yml5
-rw-r--r--changelogs/unreleased/45487-slack-tag-push-notifs.yml5
-rw-r--r--changelogs/unreleased/45557-machine-type-help-links.yml6
-rw-r--r--changelogs/unreleased/45575-invalid-characters-signup.yml5
-rw-r--r--changelogs/unreleased/45584-add-nip-io-domain-suggestion-in-auto-devops.yml5
-rw-r--r--changelogs/unreleased/45703-open-web-ide-file-tree.yml5
-rw-r--r--changelogs/unreleased/45715-remove-modal-retry.yml5
-rw-r--r--changelogs/unreleased/45738-add-environment-drop-down-to-metrics-dashboard.yml5
-rw-r--r--changelogs/unreleased/45827-expose_readme_url_in_project_api.yml5
-rw-r--r--changelogs/unreleased/45933-webide-fade-uneditable-area.yml5
-rw-r--r--changelogs/unreleased/45934-ide-firefox-scroll-md-preview.yml5
-rw-r--r--changelogs/unreleased/46010-add-index-to-runner-type.yml5
-rw-r--r--changelogs/unreleased/46082-runner-contacted_at-is-not-always-a-time-type.yml5
-rw-r--r--changelogs/unreleased/46193-fix-big-estimate.yml5
-rw-r--r--changelogs/unreleased/46202-webide-file-states.yml5
-rw-r--r--changelogs/unreleased/46246-gitlab-project-export-should-use-object-storage.yml5
-rw-r--r--changelogs/unreleased/46354-deprecate_gemnasium_service.yml5
-rw-r--r--changelogs/unreleased/46361-does-not-log-failed-sign-in-attempts-when-the-database-is-in-read-only-mode.yml5
-rw-r--r--changelogs/unreleased/46396-recognise-when-a-user-is-trying-to-validate-a-private-ssh-key-part-1.yml5
-rw-r--r--changelogs/unreleased/46396-recognise-when-a-user-is-trying-to-validate-a-private-ssh-key.yml5
-rw-r--r--changelogs/unreleased/46427-add-keyboard-shortcut-environments.yml5
-rw-r--r--changelogs/unreleased/46427-add-keyboard-shortcut-kubernetes.yml5
-rw-r--r--changelogs/unreleased/46427-change-keyboard-shortcut-of-activity-feed.yml5
-rw-r--r--changelogs/unreleased/46427-remove-outdated-todos-shortcut.yml5
-rw-r--r--changelogs/unreleased/46429-creating-a-deploy-token-doesn-t-bring-back-to-the-creation-page.yml5
-rw-r--r--changelogs/unreleased/46546-do-not-pre-select-previous-user-s-when-creating-protected-branches.yml5
-rw-r--r--changelogs/unreleased/46571-webhooks-nil-password.yml5
-rw-r--r--changelogs/unreleased/46783-removed-omniauth-provider-causing-invalid-application-setting.yml5
-rw-r--r--changelogs/unreleased/46831-remove-unused-bootstrap-component-css.yml5
-rw-r--r--changelogs/unreleased/46861-issuable-title-with-longer-username.yml5
-rw-r--r--changelogs/unreleased/46963-add_readme_button_for_non_empty_project.yml5
-rw-r--r--changelogs/unreleased/47040-inconsistent-job-list-in-job-details-view.yml5
-rw-r--r--changelogs/unreleased/47050-quick-actions-case-insensitive.yml5
-rw-r--r--changelogs/unreleased/47145-quick-actions-confidential.yml5
-rw-r--r--changelogs/unreleased/47221-explain-what-groups-are-in-the-new-group-page.yml5
-rw-r--r--changelogs/unreleased/47274-help-users-find-our-contributing-page.yml5
-rw-r--r--changelogs/unreleased/47462-issues-disabled-group-page.yml6
-rw-r--r--changelogs/unreleased/47631-operations-kubernetes-option-is-always-visible-when-repository-or-builds-are-disabled.yml5
-rw-r--r--changelogs/unreleased/47794-environment-scope-cluster-page.yml6
-rw-r--r--changelogs/unreleased/47865-changelog-for-style-updates.yml5
-rw-r--r--changelogs/unreleased/48050-add-full-commit-sha.yml5
-rw-r--r--changelogs/unreleased/48100-fix-branch-not-shown.yml6
-rw-r--r--changelogs/unreleased/48153-date-selection-dialog-broken-when-creating-a-new-milestone.yml5
-rw-r--r--changelogs/unreleased/48237-toggle-file-comments.yml5
-rw-r--r--changelogs/unreleased/48378-avatar-upload.yml5
-rw-r--r--changelogs/unreleased/48497-merge-request-refactor-displays-changes-dropdown-incorrectly.yml5
-rw-r--r--changelogs/unreleased/48515-sql-queries-are-not-shown-from-the-performance-bar-in-safari.yml5
-rw-r--r--changelogs/unreleased/48537-update-avatar-only-via-api.yml5
-rw-r--r--changelogs/unreleased/48578-disable-gcp-free-credit-banner-at-instance-level.yml5
-rw-r--r--changelogs/unreleased/48603-merge-request-refactor-title-and-copy-to-clipboard-button-are-behind-the-action-buttons.yml5
-rw-r--r--changelogs/unreleased/48634-header-navbar-line-separator-is-missing.yml5
-rw-r--r--changelogs/unreleased/48661-node-6-and-7-compatibility-broken-by-recent-monaco-editor-upgrade.yml5
-rw-r--r--changelogs/unreleased/48670-application-settings-may-not-be-invalidated-if-migrations-are-run.yml6
-rw-r--r--changelogs/unreleased/48677-also-check-auto_sign_in_with_provider.yml5
-rw-r--r--changelogs/unreleased/48825-performance.yml8
-rw-r--r--changelogs/unreleased/48934.yml5
-rw-r--r--changelogs/unreleased/48951-clean-up.yml5
-rw-r--r--changelogs/unreleased/48976-fix-sti-background-migration.yml6
-rw-r--r--changelogs/unreleased/48978-fix-helm-installation-on-cluster.yml5
-rw-r--r--changelogs/unreleased/49114-add-gitaly-servers-to-admin-overview-navigation-menu.yml5
-rw-r--r--changelogs/unreleased/ab-43706-composite-primary-keys.yml5
-rw-r--r--changelogs/unreleased/ab-46530-mediumtext-for-gpg-keys.yml5
-rw-r--r--changelogs/unreleased/add-dst-support-to-pipeline-schedule.yml5
-rw-r--r--changelogs/unreleased/add-missing-index-for-deployments.yml5
-rw-r--r--changelogs/unreleased/add-more-rebase-logging.yml5
-rw-r--r--changelogs/unreleased/add-title-placeholder-for-new-issues.yml5
-rw-r--r--changelogs/unreleased/author-doc-fix.yml5
-rw-r--r--changelogs/unreleased/backstage-gb-stages-position-migration-clean-up.yml5
-rw-r--r--changelogs/unreleased/bjk-48176_ruby_gc.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-add-gemfile-rails5-lock-check.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-bump-grape-path-helpers-gem-to-1-0-5.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-fix-protect-from-forgery-in-application-controller.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-rails5-activerecord-statementinvalid-mysql2-error-expression-1-of-select-list-is-not-in-group-by-clause.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-rails5-expected-search-search-seed_project-got-nil.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-rails5-expected-the-response-to-have-status-code-ok-but-it-was-404.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-rails5-fix-blob-requests-format.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-rails5-fix-data-store-spec.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-rails5-fix-optimistic-lock-values.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-rails5-fix-pipeline-schedules-controller-spec.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-rails5-fix-snippets-finder.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-rails5-found-new-routes-that-could-cause-conflicts-with-existing-namespaced-routes.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-rails5-invalid-single-table-inheritance-type-group-is-not-a-subclass-of-namespace.yml6
-rw-r--r--changelogs/unreleased/blackst0ne-rails5-set-request-format-in--commits-controller.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-remove-spinach.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-replace-spinach-project-deploy-keys-feature.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-replace-spinach-project-ff-merge-requests-feature.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-replace-spinach-project-forked-merge-requests-feature.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-replace-spinach-project-issues-references-feature.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-replace-spinach-project-merge-requests-references-feature.yml5
-rw-r--r--changelogs/unreleased/build-chunks-on-object-storage.yml6
-rw-r--r--changelogs/unreleased/bump-carrierwave-to-1-2-3.yml5
-rw-r--r--changelogs/unreleased/bvl-fix-maintainer-push-rejected.yml6
-rw-r--r--changelogs/unreleased/bvl-graphql-nested-merge-request.yml5
-rw-r--r--changelogs/unreleased/bvl-graphql-permissions.yml5
-rw-r--r--changelogs/unreleased/bvl-graphql-pipeline-lists.yml5
-rw-r--r--changelogs/unreleased/bvl-preload-parents-after-pagination.yml5
-rw-r--r--changelogs/unreleased/bw-enable-commonmark.yml5
-rw-r--r--changelogs/unreleased/cache-doc-fix.yml5
-rw-r--r--changelogs/unreleased/ccr-incoming-email-regex-anchor.yml3
-rw-r--r--changelogs/unreleased/ce-5024-filename-search.yml5
-rw-r--r--changelogs/unreleased/close-revoke-deploy-token-modal-on-escape-keypress.yml5
-rw-r--r--changelogs/unreleased/collapsed-contextual-nav-update.yml6
-rw-r--r--changelogs/unreleased/commits_api_with_stats.yml5
-rw-r--r--changelogs/unreleased/cr-add-locked-state-to-MR.yml5
-rw-r--r--changelogs/unreleased/cr-keep-issue-labels.yml5
-rw-r--r--changelogs/unreleased/create-live-trace-only-if-job-is-complete.yml5
-rw-r--r--changelogs/unreleased/da-port-cte-to-ce.yml5
-rw-r--r--changelogs/unreleased/db-configure-after-drop-tables.yml5
-rw-r--r--changelogs/unreleased/dm-blockquote-trailing-whitespace.yml5
-rw-r--r--changelogs/unreleased/dm-branch-api-can-push.yml5
-rw-r--r--changelogs/unreleased/dm-invalid-active-service-template.yml5
-rw-r--r--changelogs/unreleased/dm-label-reference-period.yml5
-rw-r--r--changelogs/unreleased/dm-user-without-projects-performance.yml5
-rw-r--r--changelogs/unreleased/docs-42067-document-runner-registration-api.yml5
-rw-r--r--changelogs/unreleased/dz-add-2fa-filter.yml5
-rw-r--r--changelogs/unreleased/existing-gcp-accounts.yml5
-rw-r--r--changelogs/unreleased/expose-ci-url.yml5
-rw-r--r--changelogs/unreleased/feature-expose-runner-ip-to-api.yml5
-rw-r--r--changelogs/unreleased/feature-gb-add-regexp-variables-expression.yml5
-rw-r--r--changelogs/unreleased/feature-oidc-subject-claim.yml5
-rw-r--r--changelogs/unreleased/fix-assignee-name-wrap.yml5
-rw-r--r--changelogs/unreleased/fix-boards-issue-highlight.yml5
-rw-r--r--changelogs/unreleased/fix-br-decode.yml5
-rw-r--r--changelogs/unreleased/fix-devops-remove-beta.yml5
-rw-r--r--changelogs/unreleased/fix-gb-exclude-persisted-variables-from-environment-name.yml5
-rw-r--r--changelogs/unreleased/fix-gb-not-allow-to-trigger-skipped-manual-actions.yml5
-rw-r--r--changelogs/unreleased/fix-gitaly-mr-creation-limits.yml5
-rw-r--r--changelogs/unreleased/fix-groups-api-ordering.yml4
-rw-r--r--changelogs/unreleased/fix-last-commit-author-link-is-blue.yml5
-rw-r--r--changelogs/unreleased/fix-paragraph-line-height-for-emoji.yml5
-rw-r--r--changelogs/unreleased/fix-reactive-cache-retry-rate.yml5
-rw-r--r--changelogs/unreleased/fix-registry-created-at-tooltip.yml5
-rw-r--r--changelogs/unreleased/fix-search-bar.yml5
-rw-r--r--changelogs/unreleased/fix-shorcut-modal.yml5
-rw-r--r--changelogs/unreleased/fix-trace-archive-cron-worker-race-condition.yml5
-rw-r--r--changelogs/unreleased/fix-unverified-hover-state.yml5
-rw-r--r--changelogs/unreleased/fix-web-ide-disable-markdown-autocomplete.yml5
-rw-r--r--changelogs/unreleased/fj-43565-wrong-role-displayed.yml5
-rw-r--r--changelogs/unreleased/fj-46278-apply-doorkeeper-scope-patch.yml5
-rw-r--r--changelogs/unreleased/fj-46278-enable-doorkeeper-reuse-access-token.yml6
-rw-r--r--changelogs/unreleased/fj-46411-fix-badge-api-endpoint-route-with-relative-url.yml5
-rw-r--r--changelogs/unreleased/fj-46459-fix-expose-url-when-base-url-set.yml5
-rw-r--r--changelogs/unreleased/fj-bumping-gollum-lib-and-gollum-rugged-adapter.yml5
-rw-r--r--changelogs/unreleased/fj-web-terminal-ci-build.yml5
-rw-r--r--changelogs/unreleased/fl-mr-refactor-performance-improvements.yml5
-rw-r--r--changelogs/unreleased/frozen-string-app-workers.yml5
-rw-r--r--changelogs/unreleased/frozen-string-enable-app-uploaders.yml5
-rw-r--r--changelogs/unreleased/frozen-string-enable-app-validators.yml5
-rw-r--r--changelogs/unreleased/frozen-string-enable-app-workers-2.yml5
-rw-r--r--changelogs/unreleased/gitaly-commit-count-opt-out.yml5
-rw-r--r--changelogs/unreleased/gitaly-opt-out-branch-tag.yml5
-rw-r--r--changelogs/unreleased/gitaly-timeouts.yml5
-rw-r--r--changelogs/unreleased/highlight-cluster-settings-message.yml5
-rw-r--r--changelogs/unreleased/ide-commit-actions-update.yml5
-rw-r--r--changelogs/unreleased/ide-hide-merge-request-if-disabled.yml5
-rw-r--r--changelogs/unreleased/ide-merge-request-info.yml5
-rw-r--r--changelogs/unreleased/improve-metadata-access-performance.yml5
-rw-r--r--changelogs/unreleased/issue-25256.yml5
-rw-r--r--changelogs/unreleased/jivl-add-dot-system-notes.yml5
-rw-r--r--changelogs/unreleased/jprovazn-delete-upload-worker.yml5
-rw-r--r--changelogs/unreleased/jprovazn-direct-upload.yml5
-rw-r--r--changelogs/unreleased/jprovazn-extra-line.yml5
-rw-r--r--changelogs/unreleased/jprovazn-fix-mr-caching.yml5
-rw-r--r--changelogs/unreleased/jprovazn-label-links-update.yml5
-rw-r--r--changelogs/unreleased/jprovazn-null-byte.yml5
-rw-r--r--changelogs/unreleased/jprovazn-pipeline-policy.yml6
-rw-r--r--changelogs/unreleased/jprovazn-remote-upload-destroy.yml5
-rw-r--r--changelogs/unreleased/jprovazn-upload-symlink.yml5
-rw-r--r--changelogs/unreleased/jr-48133-web-ide-commit-ellipsis.yml5
-rw-r--r--changelogs/unreleased/jr-web-ide-shortcuts.yml5
-rw-r--r--changelogs/unreleased/limit-metrics-content-type.yml5
-rw-r--r--changelogs/unreleased/memoize-database-version.yml5
-rw-r--r--changelogs/unreleased/more-group-api-sorting-options.yml5
-rw-r--r--changelogs/unreleased/move-boards-modal-empty-state-vue-component.yml5
-rw-r--r--changelogs/unreleased/move-disussion-actions-to-the-right.yml5
-rw-r--r--changelogs/unreleased/no-multi-assign-enable.yml5
-rw-r--r--changelogs/unreleased/no-multi-assign-follow-up.yml5
-rw-r--r--changelogs/unreleased/no-restricted-globals-enable.yml5
-rw-r--r--changelogs/unreleased/osw-delete-non-latest-mr-diff-files-migration.yml5
-rw-r--r--changelogs/unreleased/osw-delete-non-latest-mr-diff-files-upon-merge.yml5
-rw-r--r--changelogs/unreleased/osw-mark-as-merged-as-first-post-merge-action.yml5
-rw-r--r--changelogs/unreleased/perf-wiki-pattern-once.yml5
-rw-r--r--changelogs/unreleased/pipelines-index-performance.yml5
-rw-r--r--changelogs/unreleased/pr-importer-io-extra-error-handling.yml5
-rw-r--r--changelogs/unreleased/prefer-destructuring-fix.yml5
-rw-r--r--changelogs/unreleased/process-commits-as-normal-in-forks-with-missing-upstream.yml5
-rw-r--r--changelogs/unreleased/prune-web-hook-logs.yml5
-rw-r--r--changelogs/unreleased/rails5-fix-46276.yml5
-rw-r--r--changelogs/unreleased/rails5-fix-47366.yml5
-rw-r--r--changelogs/unreleased/rails5-fix-47370.yml5
-rw-r--r--changelogs/unreleased/rails5-fix-47804.yml5
-rw-r--r--changelogs/unreleased/rails5-fix-47805.yml6
-rw-r--r--changelogs/unreleased/rails5-fix-47835.yml6
-rw-r--r--changelogs/unreleased/rails5-fix-47836.yml6
-rw-r--r--changelogs/unreleased/rails5-fix-47960.yml5
-rw-r--r--changelogs/unreleased/rails5-fix-48009.yml5
-rw-r--r--changelogs/unreleased/rails5-fix-48012.yml6
-rw-r--r--changelogs/unreleased/rails5-fix-48104.yml6
-rw-r--r--changelogs/unreleased/rails5-fix-48140.yml6
-rw-r--r--changelogs/unreleased/rails5-fix-48141.yml6
-rw-r--r--changelogs/unreleased/rails5-fix-48142.yml5
-rw-r--r--changelogs/unreleased/rails5-fix-48430.yml5
-rw-r--r--changelogs/unreleased/rails5-fix-48432.yml5
-rw-r--r--changelogs/unreleased/rails5-fix-48977.yml5
-rw-r--r--changelogs/unreleased/rails5-fix-db-check.yml5
-rw-r--r--changelogs/unreleased/rails5-fix-mysql-arel-from.yml5
-rw-r--r--changelogs/unreleased/rails5-fix-pages-controller.yml5
-rw-r--r--changelogs/unreleased/rails5-mysql-fix-pr-importer-spec.yml5
-rw-r--r--changelogs/unreleased/rails5-mysql-rename-column.yml5
-rw-r--r--changelogs/unreleased/rails5-update-gemfile-lock.yml5
-rw-r--r--changelogs/unreleased/rd-33733-showing-created-date-instead-of-updated-date-in-project-lists.yml5
-rw-r--r--changelogs/unreleased/refactor-move-squash-before-merge-vue-component.yml5
-rw-r--r--changelogs/unreleased/registry-ux-improvements-remove-clipboard-prefix.yml5
-rw-r--r--changelogs/unreleased/remove-allocations-gem.yml5
-rw-r--r--changelogs/unreleased/remove-ci_job_request_with_tags_matcher.yml5
-rw-r--r--changelogs/unreleased/remove-is-shared-from-ci-runners.yml5
-rw-r--r--changelogs/unreleased/remove-link-label-vertical-alignment-property.yml5
-rw-r--r--changelogs/unreleased/remove-small-container-width.yml5
-rw-r--r--changelogs/unreleased/remove-trace-efficiently.yml5
-rw-r--r--changelogs/unreleased/rename-merge-request-widget-author-component.yml5
-rw-r--r--changelogs/unreleased/revert-merge-request-discussion-buttons-padding.yml5
-rw-r--r--changelogs/unreleased/runners-max-timeout-param.yml5
-rw-r--r--changelogs/unreleased/safari-scrollbar-bug.yml5
-rw-r--r--changelogs/unreleased/security-2682-fix-xss-for-markdown-toc.yml5
-rw-r--r--changelogs/unreleased/security-fj-bumping-sanitize-gem.yml5
-rw-r--r--changelogs/unreleased/security-html_escape_branch_name.yml5
-rw-r--r--changelogs/unreleased/security-html_escape_usernames.yml5
-rw-r--r--changelogs/unreleased/security-rd-do-not-show-internal-info-in-public-feed.yml5
-rw-r--r--changelogs/unreleased/sh-bump-rugged-0-27-2.yml5
-rw-r--r--changelogs/unreleased/sh-enforce-unique-and-not-null-project-ids-project-features.yml5
-rw-r--r--changelogs/unreleased/sh-fix-bamboo-change-set.yml5
-rw-r--r--changelogs/unreleased/sh-fix-cross-site-origin-uploads-js.yml5
-rw-r--r--changelogs/unreleased/sh-fix-grape-logging-status-code.yml5
-rw-r--r--changelogs/unreleased/sh-fix-issue-47797-ce.yml5
-rw-r--r--changelogs/unreleased/sh-handle-colons-in-url-passwords.yml5
-rw-r--r--changelogs/unreleased/sh-move-delete-groups-api-async.yml5
-rw-r--r--changelogs/unreleased/sh-optimize-locks-check-ce.yml5
-rw-r--r--changelogs/unreleased/straight-comparision-mode.yml5
-rw-r--r--changelogs/unreleased/support-active-setting-while-registering-a-runner.yml5
-rw-r--r--changelogs/unreleased/tc-repo-check-per-shard.yml5
-rw-r--r--changelogs/unreleased/text-expander-icon-update.yml5
-rw-r--r--changelogs/unreleased/transfer_project_api_endpoint.yml5
-rw-r--r--changelogs/unreleased/tz-diff-blob-image-viewer.yml5
-rw-r--r--changelogs/unreleased/unify-views-search-results.yml5
-rw-r--r--changelogs/unreleased/update-bcrypt-to-support-libxcrypt.yml5
-rw-r--r--changelogs/unreleased/update-environments-nav-controls.yml5
-rw-r--r--changelogs/unreleased/update-external-link-icon-in-header-user-dropdown.yml5
-rw-r--r--changelogs/unreleased/update-external-link-icon-in-merge-request-widget.yml5
-rw-r--r--changelogs/unreleased/update-integrations-external-link-icons.yml5
-rw-r--r--changelogs/unreleased/update-pipeline-icon-in-web-ide-sidebar.yml5
-rw-r--r--changelogs/unreleased/update-wiki-modal.yml5
-rw-r--r--changelogs/unreleased/upgrade-gitlab-markup.yml5
-rw-r--r--changelogs/unreleased/upgrade-hamlit-for-ruby25.yml5
-rw-r--r--changelogs/unreleased/use-backup-custom-hooks-gitaly.yml5
-rw-r--r--changelogs/unreleased/use-case-insensitive-ordering-for-dashboard-filters.yml5
-rw-r--r--changelogs/unreleased/use-tooltip-component-in-mr-widget-author-time-component.yml5
-rw-r--r--changelogs/unreleased/web-hooks-log-pagination.yml5
-rw-r--r--changelogs/unreleased/winh-make-it-right-now.yml5
-rw-r--r--changelogs/unreleased/winh-new-branch-url-encode.yml5
-rw-r--r--changelogs/unreleased/winh-stop-all-environments.yml5
-rw-r--r--changelogs/unreleased/zj-add-branch-mandatory.yml5
-rw-r--r--changelogs/unreleased/zj-calculate-checksum-mandator.yml5
-rw-r--r--changelogs/unreleased/zj-gitaly-read-write-check.yml5
-rw-r--r--changelogs/unreleased/zj-ref-contains-sha-mandatory.yml5
-rw-r--r--changelogs/unreleased/zj-wiki-find-file-opt-out.yml5
-rw-r--r--changelogs/unreleased/zj-workhorse-archive-mandatory.yml5
-rw-r--r--changelogs/unreleased/zj-workhorse-commit-patch-diff.yml5
-rw-r--r--config/application.rb37
-rw-r--r--config/aws.yml.example22
-rw-r--r--config/dependency_decisions.yml6
-rw-r--r--config/environment.rb2
-rw-r--r--config/environments/development.rb2
-rw-r--r--config/environments/test.rb4
-rw-r--r--config/gitlab.yml.example18
-rw-r--r--config/initializers/01_secret_token.rb95
-rw-r--r--config/initializers/1_settings.rb16
-rw-r--r--config/initializers/2_gitlab.rb2
-rw-r--r--config/initializers/6_validations.rb24
-rw-r--r--config/initializers/7_prometheus_metrics.rb2
-rw-r--r--config/initializers/active_record_data_types.rb2
-rw-r--r--config/initializers/active_record_locking.rb111
-rw-r--r--config/initializers/active_record_migration.rb10
-rw-r--r--config/initializers/active_record_table_definition.rb5
-rw-r--r--config/initializers/artifacts_direct_upload_support.rb7
-rw-r--r--config/initializers/carrierwave.rb31
-rw-r--r--config/initializers/console_message.rb4
-rw-r--r--config/initializers/devise.rb4
-rw-r--r--config/initializers/direct_upload_support.rb19
-rw-r--r--config/initializers/disable_email_interceptor.rb5
-rw-r--r--config/initializers/doorkeeper.rb52
-rw-r--r--config/initializers/doorkeeper_openid_connect.rb9
-rw-r--r--config/initializers/grape_route_helpers_fix.rb51
-rw-r--r--config/initializers/lograge.rb1
-rw-r--r--config/initializers/mime_types.rb2
-rw-r--r--config/initializers/mini_magick.rb3
-rw-r--r--config/initializers/omniauth.rb11
-rw-r--r--config/initializers/postgresql_opclasses_support.rb16
-rw-r--r--config/initializers/rack_attack_global.rb6
-rw-r--r--config/initializers/secret_token.rb92
-rw-r--r--config/initializers/sentry.rb2
-rw-r--r--config/karma.config.js1
-rw-r--r--config/locales/carrierwave.en.yml14
-rw-r--r--config/locales/doorkeeper.en.yml16
-rw-r--r--config/prometheus/additional_metrics.yml8
-rw-r--r--config/routes.rb6
-rw-r--r--config/routes/admin.rb1
-rw-r--r--config/routes/api.rb5
-rw-r--r--config/routes/dashboard.rb1
-rw-r--r--config/routes/group.rb5
-rw-r--r--config/routes/profile.rb2
-rw-r--r--config/routes/project.rb15
-rw-r--r--config/routes/uploads.rb2
-rw-r--r--config/routes/wiki.rb2
-rw-r--r--config/settings.rb18
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--config/unicorn.rb.example11
-rw-r--r--config/webpack.config.js48
-rw-r--r--danger/changelog/Dangerfile66
-rw-r--r--danger/changes_size/Dangerfile17
-rw-r--r--danger/database/Dangerfile83
-rw-r--r--danger/gemfile/Dangerfile24
-rw-r--r--danger/metadata/Dangerfile25
-rw-r--r--danger/specs/Dangerfile12
-rw-r--r--db/fixtures/development/04_project.rb4
-rw-r--r--db/fixtures/development/08_settings.rb7
-rw-r--r--db/fixtures/development/12_snippets.rb2
-rw-r--r--db/fixtures/development/19_environments.rb4
-rw-r--r--db/fixtures/development/20_nested_groups.rb28
-rw-r--r--db/migrate/20160226114608_add_trigram_indexes_for_searching.rb7
-rw-r--r--db/migrate/20160302152808_remove_wrong_import_url_from_projects.rb2
-rw-r--r--db/migrate/20160705054938_add_protected_branches_push_access.rb2
-rw-r--r--db/migrate/20160705054952_add_protected_branches_merge_access.rb2
-rw-r--r--db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb4
-rw-r--r--db/migrate/20161207231620_fixup_environment_name_uniqueness.rb3
-rw-r--r--db/migrate/20161207231626_add_environment_slug.rb3
-rw-r--r--db/migrate/20161226122833_remove_dot_git_from_usernames.rb4
-rw-r--r--db/migrate/20170622135728_add_unique_constraint_to_ci_variables.rb11
-rw-r--r--db/migrate/20170925184228_add_favicon_to_appearances.rb7
-rw-r--r--db/migrate/20171106155656_turn_issues_due_date_index_to_partial_index.rb6
-rw-r--r--db/migrate/20180201110056_add_foreign_keys_to_todos.rb2
-rw-r--r--db/migrate/20180408143354_rename_users_rss_token_to_feed_token.rb15
-rw-r--r--db/migrate/20180424160449_add_pipeline_iid_to_ci_pipelines.rb13
-rw-r--r--db/migrate/20180425205249_add_index_constraints_to_pipeline_iid.rb15
-rw-r--r--db/migrate/20180511131058_create_clusters_applications_jupyter.rb23
-rw-r--r--db/migrate/20180515005612_add_squash_to_merge_requests.rb19
-rw-r--r--db/migrate/20180515121227_create_notes_diff_files.rb21
-rw-r--r--db/migrate/20180523042841_rename_merge_requests_allow_maintainer_to_push.rb17
-rw-r--r--db/migrate/20180524132016_merge_requests_target_id_iid_state_partial_index.rb27
-rw-r--r--db/migrate/20180529093006_ensure_remote_mirror_columns.rb24
-rw-r--r--db/migrate/20180530135500_add_index_to_stages_position.rb15
-rw-r--r--db/migrate/20180531220618_change_default_value_for_dsa_key_restriction.rb16
-rw-r--r--db/migrate/20180601213245_add_deploy_strategy_to_project_auto_devops.rb19
-rw-r--r--db/migrate/20180608110058_rename_merge_requests_allow_collaboration.rb21
-rw-r--r--db/migrate/20180613081317_create_ci_builds_runner_session.rb21
-rw-r--r--db/migrate/20180625113853_create_import_export_uploads.rb16
-rw-r--r--db/migrate/20180626125654_add_index_on_deployable_for_deployments.rb18
-rw-r--r--db/migrate/20180628124813_alter_web_hook_logs_indexes.rb28
-rw-r--r--db/migrate/20180704204006_add_hide_third_party_offers_to_application_settings.rb18
-rw-r--r--db/migrate/merge_request_diff_file_limits_to_mysql.rb2
-rw-r--r--db/post_migrate/20161109150329_fix_project_records_with_invalid_visibility.rb3
-rw-r--r--db/post_migrate/20170324160416_migrate_user_activities_to_users_last_activity_on.rb3
-rw-r--r--db/post_migrate/20171124104327_migrate_kubernetes_service_to_new_clusters_architectures.rb2
-rw-r--r--db/post_migrate/20180408143355_cleanup_users_rss_token_rename.rb13
-rw-r--r--db/post_migrate/20180424151928_fill_file_store.rb72
-rw-r--r--db/post_migrate/20180507083701_set_minimal_project_build_timeout.rb19
-rw-r--r--db/post_migrate/20180521162137_migrate_remaining_mr_metrics_populating_background_migration.rb44
-rw-r--r--db/post_migrate/20180523125103_cleanup_merge_requests_allow_maintainer_to_push_rename.rb17
-rw-r--r--db/post_migrate/20180529152628_schedule_to_archive_legacy_traces.rb35
-rw-r--r--db/post_migrate/20180603190921_migrate_object_storage_upload_sidekiq_queue.rb16
-rw-r--r--db/post_migrate/20180604123514_cleanup_stages_position_migration.rb43
-rw-r--r--db/post_migrate/20180608201435_cleanup_merge_requests_allow_collaboration_rename.rb20
-rw-r--r--db/post_migrate/20180619121030_enqueue_delete_diff_files_workers.rb26
-rw-r--r--db/post_migrate/20180629191052_add_partial_index_to_projects_for_last_repository_check_at.rb18
-rw-r--r--db/post_migrate/20180702120647_enqueue_fix_cross_project_label_links.rb30
-rw-r--r--db/schema.rb68
-rw-r--r--doc/README.md8
-rw-r--r--doc/administration/auth/how_to_configure_ldap_gitlab_ce/index.md2
-rw-r--r--doc/administration/auth/ldap.md317
-rw-r--r--doc/administration/custom_hooks.md3
-rw-r--r--doc/administration/high_availability/database.md11
-rw-r--r--doc/administration/high_availability/gitlab.md35
-rw-r--r--doc/administration/high_availability/nfs.md4
-rw-r--r--doc/administration/index.md4
-rw-r--r--doc/administration/integration/koding.md18
-rw-r--r--doc/administration/integration/terminal.md16
-rw-r--r--doc/administration/job_artifacts.md15
-rw-r--r--doc/administration/job_traces.md155
-rw-r--r--doc/administration/monitoring/prometheus/gitlab_metrics.md16
-rw-r--r--doc/administration/monitoring/prometheus/index.md13
-rw-r--r--doc/administration/pages/index.md44
-rw-r--r--doc/administration/pages/source.md18
-rw-r--r--doc/administration/raketasks/check.md7
-rw-r--r--doc/administration/raketasks/project_import_export.md7
-rw-r--r--doc/administration/raketasks/storage.md50
-rw-r--r--doc/administration/repository_storage_types.md68
-rw-r--r--doc/administration/uploads.md4
-rw-r--r--doc/api/README.md17
-rw-r--r--doc/api/access_requests.md2
-rw-r--r--doc/api/applications.md2
-rw-r--r--doc/api/avatar.md33
-rw-r--r--doc/api/branches.md11
-rw-r--r--doc/api/commits.md1
-rw-r--r--doc/api/environments.md2
-rw-r--r--doc/api/graphql/index.md40
-rw-r--r--doc/api/groups.md31
-rw-r--r--doc/api/issues.md4
-rw-r--r--doc/api/jobs.md65
-rw-r--r--doc/api/members.md6
-rw-r--r--doc/api/merge_requests.md192
-rw-r--r--doc/api/namespaces.md2
-rw-r--r--doc/api/pages_domains.md8
-rw-r--r--doc/api/pipelines.md1
-rw-r--r--doc/api/projects.md90
-rw-r--r--doc/api/protected_branches.md18
-rw-r--r--doc/api/repositories.md1
-rw-r--r--doc/api/repository_files.md34
-rw-r--r--doc/api/runners.md12
-rw-r--r--doc/api/search.md9
-rw-r--r--doc/api/services.md2
-rw-r--r--doc/api/settings.md11
-rw-r--r--doc/api/snippets.md6
-rw-r--r--doc/api/v3_to_v4.md7
-rw-r--r--doc/articles/index.md2
-rw-r--r--doc/ci/README.md6
-rw-r--r--doc/ci/autodeploy/index.md130
-rw-r--r--doc/ci/docker/using_docker_build.md23
-rw-r--r--doc/ci/docker/using_docker_images.md4
-rw-r--r--doc/ci/environments.md38
-rw-r--r--doc/ci/examples/README.md8
-rw-r--r--doc/ci/examples/artifactory_and_gitlab/index.md10
-rw-r--r--doc/ci/examples/code_climate.md46
-rw-r--r--doc/ci/examples/code_quality.md49
-rw-r--r--doc/ci/examples/container_scanning.md23
-rw-r--r--doc/ci/examples/dast.md2
-rw-r--r--doc/ci/examples/deploy_spring_boot_to_cloud_foundry/img/cloud_foundry_secret_variables.pngbin0 -> 49735 bytes
-rw-r--r--doc/ci/examples/deploy_spring_boot_to_cloud_foundry/img/create_from_template.pngbin0 -> 82121 bytes
-rw-r--r--doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md142
-rw-r--r--doc/ci/examples/deployment/README.md2
-rw-r--r--doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md4
-rw-r--r--doc/ci/examples/laravel_with_gitlab_and_envoy/index.md6
-rw-r--r--doc/ci/pipelines.md2
-rw-r--r--doc/ci/runners/README.md8
-rw-r--r--doc/ci/ssh_keys/README.md8
-rw-r--r--doc/ci/triggers/README.md4
-rw-r--r--doc/ci/variables/README.md101
-rw-r--r--doc/ci/variables/where_variables_can_be_used.md113
-rw-r--r--doc/ci/yaml/README.md145
-rw-r--r--doc/customization/favicon.md16
-rw-r--r--doc/customization/favicon/appearance.pngbin0 -> 52245 bytes
-rw-r--r--doc/customization/favicon/custom_favicon.pngbin0 -> 60083 bytes
-rw-r--r--doc/development/README.md6
-rw-r--r--doc/development/api_graphql_styleguide.md214
-rw-r--r--doc/development/architecture.md2
-rw-r--r--doc/development/changelog.md2
-rw-r--r--doc/development/code_review.md2
-rw-r--r--doc/development/doc_styleguide.md498
-rw-r--r--doc/development/documentation/img/manual_build_docs.png (renamed from doc/development/img/manual_build_docs.png)bin14867 -> 14867 bytes
-rw-r--r--doc/development/documentation/index.md557
-rw-r--r--doc/development/documentation/styleguide.md513
-rw-r--r--doc/development/ee_features.md52
-rw-r--r--doc/development/emails.md4
-rw-r--r--doc/development/fe_guide/development_process.md6
-rw-r--r--doc/development/fe_guide/icons.md114
-rw-r--r--doc/development/fe_guide/index.md4
-rw-r--r--doc/development/fe_guide/style_guide_js.md2
-rw-r--r--doc/development/fe_guide/style_guide_scss.md10
-rw-r--r--doc/development/fe_guide/vue.md192
-rw-r--r--doc/development/fe_guide/vuex.md4
-rw-r--r--doc/development/gotchas.md48
-rw-r--r--doc/development/i18n/externalization.md37
-rw-r--r--doc/development/i18n/proofreader.md5
-rw-r--r--doc/development/licensing.md22
-rw-r--r--doc/development/new_fe_guide/dependencies.md19
-rw-r--r--doc/development/new_fe_guide/development/accessibility.md49
-rw-r--r--doc/development/new_fe_guide/development/testing.md136
-rw-r--r--doc/development/new_fe_guide/style/prettier.md14
-rw-r--r--doc/development/new_fe_guide/tips.md8
-rw-r--r--doc/development/query_recorder.md14
-rw-r--r--doc/development/rake_tasks.md17
-rw-r--r--doc/development/testing_guide/best_practices.md4
-rw-r--r--doc/development/testing_guide/frontend_testing.md4
-rw-r--r--doc/development/utilities.md41
-rw-r--r--doc/development/ux_guide/components.md2
-rw-r--r--doc/development/ux_guide/copy.md2
-rw-r--r--doc/development/what_requires_downtime.md80
-rw-r--r--doc/development/writing_documentation.md557
-rw-r--r--doc/downgrade_ee_to_ce/README.md2
-rw-r--r--doc/gitlab-basics/command-line-commands.md58
-rw-r--r--doc/gitlab-basics/create-project.md3
-rw-r--r--doc/gitlab-basics/img/create_new_project_info.pngbin75470 -> 77490 bytes
-rw-r--r--doc/gitlab-basics/start-using-git.md147
-rw-r--r--doc/install/installation.md34
-rw-r--r--doc/install/kubernetes/gitlab_chart.md137
-rw-r--r--doc/install/kubernetes/gitlab_omnibus.md6
-rw-r--r--doc/install/kubernetes/index.md51
-rw-r--r--doc/install/kubernetes/preparation/connect.md31
-rw-r--r--doc/install/kubernetes/preparation/eks.md44
-rw-r--r--doc/install/kubernetes/preparation/networking.md36
-rw-r--r--doc/install/kubernetes/preparation/rbac.md16
-rw-r--r--doc/install/kubernetes/preparation/tiller.md94
-rw-r--r--doc/install/kubernetes/preparation/tools_installation.md19
-rw-r--r--doc/install/openshift_and_gitlab/index.md8
-rw-r--r--doc/install/requirements.md12
-rw-r--r--doc/integration/bitbucket.md35
-rw-r--r--doc/integration/github.md2
-rw-r--r--doc/integration/gitlab.md12
-rw-r--r--doc/integration/google.md11
-rw-r--r--doc/integration/img/gitlab_app.pngbin15402 -> 56480 bytes
-rw-r--r--doc/integration/omniauth.md2
-rw-r--r--doc/integration/openid_connect_provider.md13
-rw-r--r--doc/integration/recaptcha.md19
-rw-r--r--doc/integration/saml.md151
-rw-r--r--doc/integration/shibboleth.md29
-rw-r--r--doc/public_access/public_access.md6
-rw-r--r--doc/raketasks/backup_restore.md41
-rw-r--r--doc/raketasks/user_management.md2
-rw-r--r--doc/security/information_exclusivity.md2
-rw-r--r--doc/security/webhooks.md2
-rw-r--r--doc/ssh/README.md10
-rw-r--r--doc/system_hooks/system_hooks.md8
-rw-r--r--doc/topics/autodevops/img/auto_monitoring.pngbin69473 -> 26675 bytes
-rw-r--r--doc/topics/autodevops/img/autodevops_domain_variables.pngbin0 -> 8456 bytes
-rw-r--r--doc/topics/autodevops/img/autodevops_multiple_clusters.pngbin0 -> 12619 bytes
-rw-r--r--doc/topics/autodevops/img/guide_choose_gke.pngbin0 -> 7895 bytes
-rw-r--r--doc/topics/autodevops/img/guide_cluster_apps.pngbin0 -> 28667 bytes
-rw-r--r--doc/topics/autodevops/img/guide_connect_cluster.pngbin38724 -> 15225 bytes
-rw-r--r--doc/topics/autodevops/img/guide_create_cluster.pngbin0 -> 18915 bytes
-rw-r--r--doc/topics/autodevops/img/guide_create_project.pngbin0 -> 17704 bytes
-rw-r--r--doc/topics/autodevops/img/guide_enable_autodevops.pngbin0 -> 27763 bytes
-rw-r--r--doc/topics/autodevops/img/guide_environments.pngbin0 -> 8570 bytes
-rw-r--r--doc/topics/autodevops/img/guide_environments_metrics.pngbin0 -> 10231 bytes
-rw-r--r--doc/topics/autodevops/img/guide_first_pipeline.pngbin0 -> 10350 bytes
-rw-r--r--doc/topics/autodevops/img/guide_gitlab_gke_details.pngbin0 -> 22677 bytes
-rw-r--r--doc/topics/autodevops/img/guide_gke_apis_after.pngbin0 -> 26811 bytes
-rw-r--r--doc/topics/autodevops/img/guide_gke_apis_before.pngbin0 -> 14882 bytes
-rw-r--r--doc/topics/autodevops/img/guide_google_auth.pngbin0 -> 12729 bytes
-rw-r--r--doc/topics/autodevops/img/guide_google_signin.pngbin0 -> 14343 bytes
-rw-r--r--doc/topics/autodevops/img/guide_ide_commit.pngbin0 -> 22035 bytes
-rw-r--r--doc/topics/autodevops/img/guide_integration.pngbin44263 -> 0 bytes
-rw-r--r--doc/topics/autodevops/img/guide_merge_request.pngbin0 -> 31157 bytes
-rw-r--r--doc/topics/autodevops/img/guide_merge_request_ide.pngbin0 -> 35052 bytes
-rw-r--r--doc/topics/autodevops/img/guide_merge_request_review_app.pngbin0 -> 25596 bytes
-rw-r--r--doc/topics/autodevops/img/guide_pipeline_stages.pngbin0 -> 12557 bytes
-rw-r--r--doc/topics/autodevops/img/guide_project_landing_page.pngbin0 -> 19227 bytes
-rw-r--r--doc/topics/autodevops/img/guide_project_template.pngbin0 -> 14699 bytes
-rw-r--r--doc/topics/autodevops/img/guide_secret.pngbin16233 -> 0 bytes
-rw-r--r--doc/topics/autodevops/img/rollout_staging_disabled.pngbin13837 -> 13834 bytes
-rw-r--r--doc/topics/autodevops/img/rollout_staging_enabled.pngbin17306 -> 17299 bytes
-rw-r--r--doc/topics/autodevops/img/staging_enabled.pngbin17929 -> 17922 bytes
-rw-r--r--doc/topics/autodevops/index.md201
-rw-r--r--doc/topics/autodevops/quick_start_guide.md347
-rw-r--r--doc/topics/git/index.md2
-rw-r--r--doc/university/README.md13
-rw-r--r--doc/university/glossary/README.md4
-rw-r--r--doc/university/high-availability/aws/README.md4
-rw-r--r--doc/update/10.6-to-10.7.md12
-rw-r--r--doc/update/10.8-to-11.0.md361
-rw-r--r--doc/update/11.0-to-11.1.md378
-rw-r--r--doc/user/admin_area/settings/img/enforce_terms.pngbin51979 -> 54881 bytes
-rw-r--r--doc/user/admin_area/settings/img/sign_up_terms.pngbin0 -> 18174 bytes
-rw-r--r--doc/user/admin_area/settings/sign_up_restrictions.md1
-rw-r--r--doc/user/admin_area/settings/terms.md13
-rw-r--r--doc/user/admin_area/settings/third_party_offers.md6
-rw-r--r--doc/user/discussions/index.md4
-rw-r--r--doc/user/gitlab_com/index.md1
-rw-r--r--doc/user/group/index.md2
-rw-r--r--doc/user/group/subgroups/index.md4
-rw-r--r--doc/user/index.md10
-rw-r--r--doc/user/markdown.md165
-rw-r--r--doc/user/permissions.md86
-rw-r--r--doc/user/profile/img/profil-preferences-navigation-theme.pngbin0 -> 16403 bytes
-rw-r--r--doc/user/profile/img/profile-preferences-syntax-themes.pngbin0 -> 44844 bytes
-rw-r--r--doc/user/profile/img/profile_settings_dropdown.pngbin4184 -> 6972 bytes
-rw-r--r--doc/user/profile/preferences.md52
-rw-r--r--doc/user/project/clusters/eks_and_gitlab/img/add_cluster.pngbin0 -> 77046 bytes
-rw-r--r--doc/user/project/clusters/eks_and_gitlab/img/create_dns.pngbin0 -> 29185 bytes
-rw-r--r--doc/user/project/clusters/eks_and_gitlab/img/create_project.pngbin0 -> 43429 bytes
-rw-r--r--doc/user/project/clusters/eks_and_gitlab/img/deploy_apps.pngbin0 -> 115299 bytes
-rw-r--r--doc/user/project/clusters/eks_and_gitlab/img/environment.pngbin0 -> 31644 bytes
-rw-r--r--doc/user/project/clusters/eks_and_gitlab/img/new_project.pngbin0 -> 10309 bytes
-rw-r--r--doc/user/project/clusters/eks_and_gitlab/img/pipeline.pngbin0 -> 26500 bytes
-rw-r--r--doc/user/project/clusters/eks_and_gitlab/index.md135
-rw-r--r--doc/user/project/clusters/index.md129
-rw-r--r--doc/user/project/container_registry.md19
-rw-r--r--doc/user/project/deploy_tokens/index.md2
-rw-r--r--doc/user/project/img/group_issue_board.pngbin0 -> 163417 bytes
-rw-r--r--doc/user/project/img/issue_board.pngbin82592 -> 100684 bytes
-rw-r--r--doc/user/project/img/issue_board_add_list.pngbin17312 -> 6404 bytes
-rw-r--r--doc/user/project/img/issue_board_assignee_lists.pngbin0 -> 134674 bytes
-rw-r--r--doc/user/project/img/issue_board_creation.pngbin0 -> 108674 bytes
-rw-r--r--doc/user/project/img/issue_board_edit_button.pngbin0 -> 108168 bytes
-rw-r--r--doc/user/project/img/issue_board_focus_mode.gifbin0 -> 1043366 bytes
-rw-r--r--doc/user/project/img/issue_board_move_issue_card_list.pngbin36747 -> 13670 bytes
-rw-r--r--doc/user/project/img/issue_board_system_notes.pngbin4899 -> 4893 bytes
-rw-r--r--doc/user/project/img/issue_board_view_scope.pngbin0 -> 63542 bytes
-rw-r--r--doc/user/project/img/issue_board_welcome_message.pngbin26533 -> 13519 bytes
-rw-r--r--doc/user/project/img/issue_boards_add_issues_modal.pngbin29176 -> 12421 bytes
-rw-r--r--doc/user/project/img/issue_boards_multiple.pngbin0 -> 6092 bytes
-rw-r--r--doc/user/project/img/issue_boards_remove_issue.pngbin135168 -> 39357 bytes
-rw-r--r--doc/user/project/import/bitbucket.md4
-rw-r--r--doc/user/project/import/github.md217
-rw-r--r--doc/user/project/import/img/import_projects_from_repo_url.pngbin150259 -> 142329 bytes
-rw-r--r--doc/user/project/index.md2
-rw-r--r--doc/user/project/integrations/bamboo.md7
-rw-r--r--doc/user/project/integrations/img/kubernetes_configuration.pngbin14407 -> 0 bytes
-rw-r--r--doc/user/project/integrations/index.md2
-rw-r--r--doc/user/project/integrations/kubernetes.md138
-rw-r--r--doc/user/project/integrations/microsoft_teams.md2
-rw-r--r--doc/user/project/integrations/project_services.md1
-rw-r--r--doc/user/project/integrations/prometheus.md2
-rw-r--r--doc/user/project/integrations/prometheus_library/nginx_ingress.md2
-rw-r--r--doc/user/project/integrations/webhooks.md16
-rw-r--r--doc/user/project/issue_board.md224
-rw-r--r--doc/user/project/issues/confidential_issues.md4
-rw-r--r--doc/user/project/issues/deleting_issues.md4
-rw-r--r--doc/user/project/issues/due_dates.md8
-rw-r--r--doc/user/project/issues/index.md6
-rw-r--r--doc/user/project/issues/issues_functionalities.md2
-rw-r--r--doc/user/project/members/index.md6
-rw-r--r--doc/user/project/members/share_project_with_groups.md2
-rw-r--r--doc/user/project/merge_requests/allow_collaboration.md20
-rw-r--r--doc/user/project/merge_requests/authorization_for_merge_requests.md6
-rw-r--r--doc/user/project/merge_requests/img/allow_collaboration.pngbin0 -> 39513 bytes
-rw-r--r--doc/user/project/merge_requests/img/allow_maintainer_push.pngbin49216 -> 0 bytes
-rw-r--r--doc/user/project/merge_requests/img/squash_edit_form.pngbin0 -> 4232 bytes
-rw-r--r--doc/user/project/merge_requests/img/squash_mr_commits.pngbin0 -> 85635 bytes
-rw-r--r--doc/user/project/merge_requests/img/squash_mr_widget.pngbin0 -> 6496 bytes
-rw-r--r--doc/user/project/merge_requests/img/squash_squashed_commit.pngbin0 -> 63371 bytes
-rw-r--r--doc/user/project/merge_requests/index.md14
-rw-r--r--doc/user/project/merge_requests/maintainer_access.md21
-rw-r--r--doc/user/project/merge_requests/squash_and_merge.md80
-rw-r--r--doc/user/project/milestones/index.md1
-rw-r--r--doc/user/project/pages/index.md2
-rw-r--r--doc/user/project/protected_branches.md16
-rw-r--r--doc/user/project/protected_tags.md4
-rw-r--r--doc/user/project/quick_actions.md1
-rw-r--r--doc/user/project/repository/index.md14
-rw-r--r--doc/user/project/settings/import_export.md5
-rw-r--r--doc/user/project/settings/index.md8
-rw-r--r--doc/user/project/web_ide/index.md21
-rw-r--r--doc/user/reserved_names.md1
-rw-r--r--doc/user/snippets.md74
-rw-r--r--doc/workflow/gitlab_flow.md2
-rw-r--r--doc/workflow/lfs/lfs_administration.md98
-rw-r--r--doc/workflow/lfs/manage_large_binaries_with_git_lfs.md2
-rw-r--r--doc/workflow/merge_requests.md2
-rw-r--r--doc/workflow/notifications.md13
-rw-r--r--doc/workflow/repository_mirroring.md252
-rw-r--r--doc/workflow/repository_mirroring/repository_mirroring_detect_host_keys.pngbin0 -> 61463 bytes
-rw-r--r--doc/workflow/repository_mirroring/repository_mirroring_diverged_branch.pngbin0 -> 22668 bytes
-rw-r--r--doc/workflow/repository_mirroring/repository_mirroring_hard_failed_main.pngbin0 -> 47943 bytes
-rw-r--r--doc/workflow/repository_mirroring/repository_mirroring_hard_failed_settings.pngbin0 -> 53279 bytes
-rw-r--r--doc/workflow/repository_mirroring/repository_mirroring_new_project.pngbin0 -> 20364 bytes
-rw-r--r--doc/workflow/repository_mirroring/repository_mirroring_pull_advanced_host_keys.pngbin0 -> 115796 bytes
-rw-r--r--doc/workflow/repository_mirroring/repository_mirroring_pull_settings.pngbin0 -> 100470 bytes
-rw-r--r--doc/workflow/repository_mirroring/repository_mirroring_pull_settings_for_ssh.pngbin0 -> 69467 bytes
-rw-r--r--doc/workflow/repository_mirroring/repository_mirroring_ssh_host_keys_verified.pngbin0 -> 23724 bytes
-rw-r--r--doc/workflow/repository_mirroring/repository_mirroring_ssh_public_key_authentication.pngbin0 -> 82456 bytes
-rw-r--r--doc/workflow/todos.md3
-rw-r--r--doc_styleguide.md3
-rw-r--r--lib/api/api.rb48
-rw-r--r--lib/api/avatar.rb21
-rw-r--r--lib/api/boards.rb1
-rw-r--r--lib/api/branches.rb8
-rw-r--r--lib/api/commits.rb22
-rw-r--r--lib/api/deploy_keys.rb12
-rw-r--r--lib/api/entities.rb79
-rw-r--r--lib/api/environments.rb3
-rw-r--r--lib/api/events.rb1
-rw-r--r--lib/api/files.rb56
-rw-r--r--lib/api/groups.rb13
-rw-r--r--lib/api/helpers.rb3
-rw-r--r--lib/api/helpers/headers_helpers.rb11
-rw-r--r--lib/api/helpers/internal_helpers.rb6
-rw-r--r--lib/api/helpers/related_resources_helpers.rb2
-rw-r--r--lib/api/helpers/runner.rb5
-rw-r--r--lib/api/internal.rb8
-rw-r--r--lib/api/issues.rb2
-rw-r--r--lib/api/jobs.rb1
-rw-r--r--lib/api/markdown.rb4
-rw-r--r--lib/api/merge_requests.rb74
-rw-r--r--lib/api/milestone_responses.rb2
-rw-r--r--lib/api/pipelines.rb9
-rw-r--r--lib/api/project_export.rb10
-rw-r--r--lib/api/project_hooks.rb2
-rw-r--r--lib/api/projects.rb27
-rw-r--r--lib/api/protected_branches.rb4
-rw-r--r--lib/api/repositories.rb3
-rw-r--r--lib/api/runner.rb46
-rw-r--r--lib/api/runners.rb17
-rw-r--r--lib/api/search.rb4
-rw-r--r--lib/api/services.rb4
-rw-r--r--lib/api/settings.rb23
-rw-r--r--lib/api/users.rb28
-rw-r--r--lib/api/v3/award_emoji.rb130
-rw-r--r--lib/api/v3/boards.rb72
-rw-r--r--lib/api/v3/branches.rb76
-rw-r--r--lib/api/v3/broadcast_messages.rb31
-rw-r--r--lib/api/v3/builds.rb250
-rw-r--r--lib/api/v3/commits.rb199
-rw-r--r--lib/api/v3/deploy_keys.rb143
-rw-r--r--lib/api/v3/deployments.rb43
-rw-r--r--lib/api/v3/entities.rb309
-rw-r--r--lib/api/v3/environments.rb87
-rw-r--r--lib/api/v3/files.rb138
-rw-r--r--lib/api/v3/groups.rb187
-rw-r--r--lib/api/v3/helpers.rb49
-rw-r--r--lib/api/v3/issues.rb240
-rw-r--r--lib/api/v3/labels.rb34
-rw-r--r--lib/api/v3/members.rb136
-rw-r--r--lib/api/v3/merge_request_diffs.rb44
-rw-r--r--lib/api/v3/merge_requests.rb297
-rw-r--r--lib/api/v3/milestones.rb65
-rw-r--r--lib/api/v3/notes.rb148
-rw-r--r--lib/api/v3/pipelines.rb38
-rw-r--r--lib/api/v3/project_hooks.rb111
-rw-r--r--lib/api/v3/project_snippets.rb143
-rw-r--r--lib/api/v3/projects.rb475
-rw-r--r--lib/api/v3/repositories.rb110
-rw-r--r--lib/api/v3/runners.rb66
-rw-r--r--lib/api/v3/services.rb670
-rw-r--r--lib/api/v3/settings.rb147
-rw-r--r--lib/api/v3/snippets.rb141
-rw-r--r--lib/api/v3/subscriptions.rb53
-rw-r--r--lib/api/v3/system_hooks.rb32
-rw-r--r--lib/api/v3/tags.rb40
-rw-r--r--lib/api/v3/templates.rb122
-rw-r--r--lib/api/v3/time_tracking_endpoints.rb116
-rw-r--r--lib/api/v3/todos.rb30
-rw-r--r--lib/api/v3/triggers.rb112
-rw-r--r--lib/api/v3/users.rb204
-rw-r--r--lib/api/v3/variables.rb29
-rw-r--r--lib/api/version.rb2
-rw-r--r--lib/backup.rb3
-rw-r--r--lib/backup/artifacts.rb6
-rw-r--r--lib/backup/builds.rb6
-rw-r--r--lib/backup/database.rb20
-rw-r--r--lib/backup/files.rb19
-rw-r--r--lib/backup/lfs.rb6
-rw-r--r--lib/backup/manager.rb61
-rw-r--r--lib/backup/pages.rb6
-rw-r--r--lib/backup/registry.rb6
-rw-r--r--lib/backup/repository.rb274
-rw-r--r--lib/backup/uploads.rb6
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb11
-rw-r--r--lib/banzai/filter/blockquote_fence_filter.rb12
-rw-r--r--lib/banzai/filter/emoji_filter.rb4
-rw-r--r--lib/banzai/filter/gollum_tags_filter.rb6
-rw-r--r--lib/banzai/filter/markdown_engines/common_mark.rb2
-rw-r--r--lib/banzai/filter/markdown_filter.rb2
-rw-r--r--lib/banzai/filter/merge_request_reference_filter.rb5
-rw-r--r--lib/banzai/filter/milestone_reference_filter.rb2
-rw-r--r--lib/banzai/filter/reference_filter.rb8
-rw-r--r--lib/banzai/filter/sanitization_filter.rb20
-rw-r--r--lib/banzai/filter/table_of_contents_filter.rb2
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb6
-rw-r--r--lib/banzai/reference_parser/issue_parser.rb3
-rw-r--r--lib/bitbucket/representation/issue.rb2
-rw-r--r--lib/constraints/feature_constrainer.rb13
-rw-r--r--lib/declarative_policy.rb12
-rw-r--r--lib/declarative_policy/base.rb10
-rw-r--r--lib/declarative_policy/delegate_dsl.rb6
-rw-r--r--lib/declarative_policy/policy_dsl.rb14
-rw-r--r--lib/declarative_policy/preferred_scope.rb10
-rw-r--r--lib/declarative_policy/rule.rb4
-rw-r--r--lib/declarative_policy/rule_dsl.rb10
-rw-r--r--lib/declarative_policy/runner.rb2
-rw-r--r--lib/extracts_path.rb5
-rw-r--r--lib/feature.rb11
-rw-r--r--lib/gitaly/server.rb18
-rw-r--r--lib/gitlab.rb18
-rw-r--r--lib/gitlab/access.rb38
-rw-r--r--lib/gitlab/auth.rb27
-rw-r--r--lib/gitlab/auth/o_auth/user.rb6
-rw-r--r--lib/gitlab/auth/request_authenticator.rb2
-rw-r--r--lib/gitlab/auth/saml/auth_hash.rb15
-rw-r--r--lib/gitlab/auth/saml/config.rb4
-rw-r--r--lib/gitlab/auth/saml/user.rb4
-rw-r--r--lib/gitlab/auth/user_access_denied_reason.rb8
-rw-r--r--lib/gitlab/auth/user_auth_finders.rb18
-rw-r--r--lib/gitlab/background_migration/add_merge_request_diff_commits_count.rb1
-rw-r--r--lib/gitlab/background_migration/archive_legacy_traces.rb23
-rw-r--r--lib/gitlab/background_migration/cleanup_concurrent_rename.rb14
-rw-r--r--lib/gitlab/background_migration/cleanup_concurrent_schema_change.rb52
-rw-r--r--lib/gitlab/background_migration/cleanup_concurrent_type_change.rb48
-rw-r--r--lib/gitlab/background_migration/create_fork_network_memberships_range.rb1
-rw-r--r--lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys.rb1
-rw-r--r--lib/gitlab/background_migration/delete_diff_files.rb81
-rw-r--r--lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits.rb1
-rw-r--r--lib/gitlab/background_migration/fill_file_store_job_artifact.rb19
-rw-r--r--lib/gitlab/background_migration/fill_file_store_lfs_object.rb19
-rw-r--r--lib/gitlab/background_migration/fill_store_upload.rb20
-rw-r--r--lib/gitlab/background_migration/fix_cross_project_label_links.rb140
-rw-r--r--lib/gitlab/background_migration/migrate_build_stage.rb1
-rw-r--r--lib/gitlab/background_migration/migrate_events_to_push_event_payloads.rb1
-rw-r--r--lib/gitlab/background_migration/migrate_system_uploads_to_new_folder.rb1
-rw-r--r--lib/gitlab/background_migration/move_personal_snippet_files.rb1
-rw-r--r--lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb1
-rw-r--r--lib/gitlab/background_migration/populate_fork_networks_range.rb2
-rw-r--r--lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data.rb3
-rw-r--r--lib/gitlab/background_migration/populate_untracked_uploads.rb2
-rw-r--r--lib/gitlab/background_migration/populate_untracked_uploads_dependencies.rb4
-rw-r--r--lib/gitlab/background_migration/prepare_untracked_uploads.rb5
-rw-r--r--lib/gitlab/background_migration/schedule_diff_files_deletion.rb44
-rw-r--r--lib/gitlab/cache/request_cache.rb37
-rw-r--r--lib/gitlab/checks/change_access.rb2
-rw-r--r--lib/gitlab/checks/commit_check.rb2
-rw-r--r--lib/gitlab/checks/force_push.rb16
-rw-r--r--lib/gitlab/ci/ansi2html.rb118
-rw-r--r--lib/gitlab/ci/build/artifacts/metadata.rb19
-rw-r--r--lib/gitlab/ci/config/entry/configurable.rb2
-rw-r--r--lib/gitlab/ci/pipeline/chain/populate.rb3
-rw-r--r--lib/gitlab/ci/pipeline/preloader.rb48
-rw-r--r--lib/gitlab/ci/status/stage/common.rb4
-rw-r--r--lib/gitlab/ci/trace.rb48
-rw-r--r--lib/gitlab/ci/trace/http_io.rb197
-rw-r--r--lib/gitlab/ci/trace/section_parser.rb12
-rw-r--r--lib/gitlab/ci/variables/collection/item.rb3
-rw-r--r--lib/gitlab/contributions_calendar.rb2
-rw-r--r--lib/gitlab/current_settings.rb17
-rw-r--r--lib/gitlab/cycle_analytics/summary/commit.rb16
-rw-r--r--lib/gitlab/data_builder/pipeline.rb2
-rw-r--r--lib/gitlab/database.rb22
-rw-r--r--lib/gitlab/database/count.rb72
-rw-r--r--lib/gitlab/database/median.rb17
-rw-r--r--lib/gitlab/database/migration_helpers.rb91
-rw-r--r--lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb1
-rw-r--r--lib/gitlab/diff/file.rb42
-rw-r--r--lib/gitlab/diff/file_collection/merge_request_diff.rb2
-rw-r--r--lib/gitlab/diff/image_point.rb6
-rw-r--r--lib/gitlab/diff/inline_diff.rb4
-rw-r--r--lib/gitlab/diff/line.rb43
-rw-r--r--lib/gitlab/diff/parser.rb8
-rw-r--r--lib/gitlab/ee_compat_check.rb28
-rw-r--r--lib/gitlab/encoding_helper.rb12
-rw-r--r--lib/gitlab/exclusive_lease_helpers.rb29
-rw-r--r--lib/gitlab/favicon.rb58
-rw-r--r--lib/gitlab/file_finder.rb43
-rw-r--r--lib/gitlab/fogbugz_import/importer.rb22
-rw-r--r--lib/gitlab/gfm/uploads_rewriter.rb22
-rw-r--r--lib/gitlab/git.rb2
-rw-r--r--lib/gitlab/git/blame.rb19
-rw-r--r--lib/gitlab/git/blob.rb219
-rw-r--r--lib/gitlab/git/commit.rb204
-rw-r--r--lib/gitlab/git/commit_stats.rb16
-rw-r--r--lib/gitlab/git/committer_with_hooks.rb2
-rw-r--r--lib/gitlab/git/conflict/resolver.rb69
-rw-r--r--lib/gitlab/git/diff_collection.rb12
-rw-r--r--lib/gitlab/git/gitlab_projects.rb62
-rw-r--r--lib/gitlab/git/hook.rb6
-rw-r--r--lib/gitlab/git/hooks_service.rb2
-rw-r--r--lib/gitlab/git/lfs_changes.rb38
-rw-r--r--lib/gitlab/git/pre_receive_error.rb21
-rw-r--r--lib/gitlab/git/remote_mirror.rb77
-rw-r--r--lib/gitlab/git/repository.rb1111
-rw-r--r--lib/gitlab/git/rev_list.rb23
-rw-r--r--lib/gitlab/git/storage/checker.rb2
-rw-r--r--lib/gitlab/git/storage/circuit_breaker.rb15
-rwxr-xr-xlib/gitlab/git/support/format-git-cat-file-input21
-rw-r--r--lib/gitlab/git/tag.rb13
-rw-r--r--lib/gitlab/git/tree.rb10
-rw-r--r--lib/gitlab/git/version.rb11
-rw-r--r--lib/gitlab/git/wiki.rb170
-rw-r--r--lib/gitlab/git_access.rb20
-rw-r--r--lib/gitlab/gitaly_client.rb29
-rw-r--r--lib/gitlab/gitaly_client/blob_service.rb10
-rw-r--r--lib/gitlab/gitaly_client/commit_service.rb29
-rw-r--r--lib/gitlab/gitaly_client/conflicts_service.rb12
-rw-r--r--lib/gitlab/gitaly_client/namespace_service.rb12
-rw-r--r--lib/gitlab/gitaly_client/operation_service.rb43
-rw-r--r--lib/gitlab/gitaly_client/ref_service.rb26
-rw-r--r--lib/gitlab/gitaly_client/remote_service.rb4
-rw-r--r--lib/gitlab/gitaly_client/repository_service.rb168
-rw-r--r--lib/gitlab/gitaly_client/storage_service.rb15
-rw-r--r--lib/gitlab/gitaly_client/storage_settings.rb30
-rw-r--r--lib/gitlab/gitaly_client/wiki_service.rb10
-rw-r--r--lib/gitlab/github_import/importer/lfs_object_importer.rb24
-rw-r--r--lib/gitlab/github_import/importer/lfs_objects_importer.rb37
-rw-r--r--lib/gitlab/github_import/importer/pull_request_importer.rb57
-rw-r--r--lib/gitlab/github_import/parallel_importer.rb9
-rw-r--r--lib/gitlab/github_import/representation/lfs_object.rb32
-rw-r--r--lib/gitlab/github_import/sequential_importer.rb5
-rw-r--r--lib/gitlab/gitlab_import/client.rb10
-rw-r--r--lib/gitlab/gitlab_import/importer.rb2
-rw-r--r--lib/gitlab/gitlab_import/project_creator.rb2
-rw-r--r--lib/gitlab/gon_helper.rb4
-rw-r--r--lib/gitlab/google_code_import/importer.rb22
-rw-r--r--lib/gitlab/gpg.rb6
-rw-r--r--lib/gitlab/gpg/commit.rb2
-rw-r--r--lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb17
-rw-r--r--lib/gitlab/grape_logging/loggers/queue_duration_logger.rb26
-rw-r--r--lib/gitlab/graphql.rb5
-rw-r--r--lib/gitlab/graphql/authorize.rb21
-rw-r--r--lib/gitlab/graphql/authorize/instrumentation.rb45
-rw-r--r--lib/gitlab/graphql/connections.rb12
-rw-r--r--lib/gitlab/graphql/connections/keyset_connection.rb73
-rw-r--r--lib/gitlab/graphql/errors.rb8
-rw-r--r--lib/gitlab/graphql/expose_permissions.rb15
-rw-r--r--lib/gitlab/graphql/present.rb20
-rw-r--r--lib/gitlab/graphql/present/instrumentation.rb36
-rw-r--r--lib/gitlab/graphql/variables.rb37
-rw-r--r--lib/gitlab/hashed_storage/migrator.rb57
-rw-r--r--lib/gitlab/hashed_storage/rake_helper.rb83
-rw-r--r--lib/gitlab/health_checks/db_check.rb2
-rw-r--r--lib/gitlab/health_checks/fs_shards_check.rb166
-rw-r--r--lib/gitlab/health_checks/gitaly_check.rb14
-rw-r--r--lib/gitlab/http_io.rb193
-rw-r--r--lib/gitlab/i18n.rb3
-rw-r--r--lib/gitlab/i18n/metadata_entry.rb11
-rw-r--r--lib/gitlab/i18n/po_linter.rb144
-rw-r--r--lib/gitlab/i18n/translation_entry.rb26
-rw-r--r--lib/gitlab/import_export.rb6
-rw-r--r--lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb17
-rw-r--r--lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb26
-rw-r--r--lib/gitlab/import_export/attribute_cleaner.rb11
-rw-r--r--lib/gitlab/import_export/attributes_finder.rb4
-rw-r--r--lib/gitlab/import_export/group_project_object_builder.rb90
-rw-r--r--lib/gitlab/import_export/import_export.yml3
-rw-r--r--lib/gitlab/import_export/members_mapper.rb2
-rw-r--r--lib/gitlab/import_export/project_tree_restorer.rb52
-rw-r--r--lib/gitlab/import_export/reader.rb2
-rw-r--r--lib/gitlab/import_export/relation_factory.rb98
-rw-r--r--lib/gitlab/import_export/repo_saver.rb4
-rw-r--r--lib/gitlab/import_export/saver.rb31
-rw-r--r--lib/gitlab/import_export/wiki_repo_saver.rb6
-rw-r--r--lib/gitlab/import_formatter.rb1
-rw-r--r--lib/gitlab/kubernetes.rb7
-rw-r--r--lib/gitlab/kubernetes/helm/base_command.rb2
-rw-r--r--lib/gitlab/kubernetes/helm/install_command.rb11
-rw-r--r--lib/gitlab/legacy_github_import/project_creator.rb5
-rw-r--r--lib/gitlab/mail_room.rb2
-rw-r--r--lib/gitlab/metrics/influx_db.rb2
-rw-r--r--lib/gitlab/metrics/method_call.rb2
-rw-r--r--lib/gitlab/metrics/samplers/influx_sampler.rb6
-rw-r--r--lib/gitlab/metrics/samplers/ruby_sampler.rb36
-rw-r--r--lib/gitlab/metrics/subscribers/active_record.rb2
-rw-r--r--lib/gitlab/metrics/transaction.rb2
-rw-r--r--lib/gitlab/metrics/web_transaction.rb9
-rw-r--r--lib/gitlab/middleware/multipart.rb16
-rw-r--r--lib/gitlab/middleware/read_only/controller.rb34
-rw-r--r--lib/gitlab/omniauth_initializer.rb5
-rw-r--r--lib/gitlab/path_regex.rb1
-rw-r--r--lib/gitlab/profiler.rb13
-rw-r--r--lib/gitlab/project_authorizations/with_nested_groups.rb2
-rw-r--r--lib/gitlab/project_authorizations/without_nested_groups.rb2
-rw-r--r--lib/gitlab/project_search_results.rb3
-rw-r--r--lib/gitlab/query_limiting/active_support_subscriber.rb2
-rw-r--r--lib/gitlab/quick_actions/extractor.rb8
-rw-r--r--lib/gitlab/quick_actions/substitution_definition.rb2
-rw-r--r--lib/gitlab/repository_cache_adapter.rb10
-rw-r--r--lib/gitlab/request_forgery_protection.rb2
-rw-r--r--lib/gitlab/search/parsed_query.rb23
-rw-r--r--lib/gitlab/search/query.rb55
-rw-r--r--lib/gitlab/setup_helper.rb5
-rw-r--r--lib/gitlab/shard_health_cache.rb41
-rw-r--r--lib/gitlab/shell.rb105
-rw-r--r--lib/gitlab/slash_commands/command.rb18
-rw-r--r--lib/gitlab/slash_commands/presenters/access.rb2
-rw-r--r--lib/gitlab/sql/cte.rb50
-rw-r--r--lib/gitlab/task_helpers.rb14
-rw-r--r--lib/gitlab/temporarily_allow.rb42
-rw-r--r--lib/gitlab/themes.rb15
-rw-r--r--lib/gitlab/url_blocker.rb19
-rw-r--r--lib/gitlab/url_builder.rb2
-rw-r--r--lib/gitlab/url_sanitizer.rb2
-rw-r--r--lib/gitlab/usage_data.rb17
-rw-r--r--lib/gitlab/user_access.rb2
-rw-r--r--lib/gitlab/utils/override.rb16
-rw-r--r--lib/gitlab/verify/batch_verifier.rb59
-rw-r--r--lib/gitlab/verify/job_artifacts.rb10
-rw-r--r--lib/gitlab/verify/lfs_objects.rb12
-rw-r--r--lib/gitlab/verify/rake_task.rb2
-rw-r--r--lib/gitlab/verify/uploads.rb12
-rw-r--r--lib/gitlab/webpack/dev_server_middleware.rb5
-rw-r--r--lib/gitlab/wiki_file_finder.rb23
-rw-r--r--lib/gitlab/workhorse.rb63
-rw-r--r--lib/google_api/cloud_platform/client.rb17
-rw-r--r--lib/mattermost/command.rb2
-rw-r--r--lib/mattermost/session.rb4
-rw-r--r--lib/mattermost/team.rb6
-rw-r--r--lib/microsoft_teams/notifier.rb2
-rw-r--r--lib/mysql_zero_date.rb18
-rw-r--r--lib/object_storage/direct_upload.rb166
-rw-r--r--lib/omni_auth/strategies/jwt.rb4
-rw-r--r--lib/peek/rblineprof/custom_controller_helpers.rb4
-rw-r--r--lib/rouge/formatters/html_gitlab.rb2
-rw-r--r--lib/rspec_flaky/listener.rb10
-rw-r--r--lib/rspec_flaky/report.rb4
-rw-r--r--lib/support/nginx/gitlab16
-rw-r--r--lib/support/nginx/gitlab-ssl16
-rw-r--r--lib/system_check/orphans/namespace_check.rb16
-rw-r--r--lib/system_check/orphans/repository_check.rb16
-rw-r--r--lib/system_check/simple_executor.rb30
-rw-r--r--lib/tasks/flay.rake2
-rw-r--r--lib/tasks/gettext.rake60
-rw-r--r--lib/tasks/gitlab/backup.rake132
-rw-r--r--lib/tasks/gitlab/bulk_add_permission.rake4
-rw-r--r--lib/tasks/gitlab/check.rake12
-rw-r--r--lib/tasks/gitlab/cleanup.rake5
-rw-r--r--lib/tasks/gitlab/db.rake4
-rw-r--r--lib/tasks/gitlab/import_export.rake21
-rw-r--r--lib/tasks/gitlab/info.rake8
-rw-r--r--lib/tasks/gitlab/storage.rake124
-rw-r--r--lib/tasks/gitlab/traces.rake4
-rw-r--r--lib/tasks/gitlab/uploads/migrate.rake2
-rw-r--r--lib/tasks/import.rake7
-rw-r--r--lib/tasks/lint.rake43
-rw-r--r--lib/tasks/migrate/setup_postgresql.rake15
-rw-r--r--lib/tasks/tokens.rake10
-rw-r--r--lib/uploaded_file.rb5
-rw-r--r--locale/bg/gitlab.po2066
-rw-r--r--locale/cs_CZ/gitlab.po5640
-rw-r--r--locale/de/gitlab.po2072
-rw-r--r--locale/eo/gitlab.po2066
-rw-r--r--locale/es/gitlab.po2150
-rw-r--r--locale/fil_PH/gitlab.po1996
-rw-r--r--locale/fr/gitlab.po3410
-rw-r--r--locale/gitlab.pot951
-rw-r--r--locale/gl_ES/gitlab.po5562
-rw-r--r--locale/id_ID/gitlab.po1978
-rw-r--r--locale/it/gitlab.po2094
-rw-r--r--locale/ja/gitlab.po2996
-rw-r--r--locale/ko/gitlab.po2050
-rw-r--r--locale/nl_NL/gitlab.po2002
-rw-r--r--locale/pl_PL/gitlab.po2034
-rw-r--r--locale/pt_BR/gitlab.po2924
-rw-r--r--locale/ru/gitlab.po2836
-rw-r--r--locale/tr_TR/gitlab.po2002
-rw-r--r--locale/uk/gitlab.po2634
-rw-r--r--locale/zh_CN/gitlab.po2580
-rw-r--r--locale/zh_HK/gitlab.po2050
-rw-r--r--locale/zh_TW/gitlab.po3920
-rw-r--r--package.json46
-rw-r--r--public/favicon.icobin5430 -> 0 bytes
-rw-r--r--qa/Dockerfile9
-rw-r--r--qa/qa.rb48
-rw-r--r--qa/qa/factory/repository/project_push.rb34
-rw-r--r--qa/qa/factory/repository/push.rb39
-rw-r--r--qa/qa/factory/repository/wiki_push.rb32
-rw-r--r--qa/qa/factory/resource/branch.rb22
-rw-r--r--qa/qa/factory/resource/group.rb2
-rw-r--r--qa/qa/factory/resource/kubernetes_cluster.rb55
-rw-r--r--qa/qa/factory/resource/merge_request.rb15
-rw-r--r--qa/qa/factory/resource/project.rb18
-rw-r--r--qa/qa/factory/resource/project_imported_from_github.rb37
-rw-r--r--qa/qa/factory/resource/project_milestone.rb36
-rw-r--r--qa/qa/factory/resource/sandbox.rb4
-rw-r--r--qa/qa/factory/resource/wiki.rb25
-rw-r--r--qa/qa/fixtures/auto_devops_rack/Gemfile3
-rw-r--r--qa/qa/fixtures/auto_devops_rack/Gemfile.lock15
-rw-r--r--qa/qa/fixtures/auto_devops_rack/Rakefile7
-rw-r--r--qa/qa/fixtures/auto_devops_rack/config.ru1
-rw-r--r--qa/qa/git/repository.rb19
-rw-r--r--qa/qa/page/admin/settings/main.rb4
-rw-r--r--qa/qa/page/base.rb2
-rw-r--r--qa/qa/page/component/select2.rb11
-rw-r--r--qa/qa/page/group/show.rb6
-rw-r--r--qa/qa/page/issuable/sidebar.rb17
-rw-r--r--qa/qa/page/main/login.rb65
-rw-r--r--qa/qa/page/menu/main.rb17
-rw-r--r--qa/qa/page/menu/side.rb40
-rw-r--r--qa/qa/page/merge_request/new.rb15
-rw-r--r--qa/qa/page/merge_request/show.rb56
-rw-r--r--qa/qa/page/project/import/github.rb66
-rw-r--r--qa/qa/page/project/job/show.rb7
-rw-r--r--qa/qa/page/project/milestone/index.rb17
-rw-r--r--qa/qa/page/project/milestone/new.rb27
-rw-r--r--qa/qa/page/project/new.rb20
-rw-r--r--qa/qa/page/project/operations/kubernetes/add.rb19
-rw-r--r--qa/qa/page/project/operations/kubernetes/add_existing.rb39
-rw-r--r--qa/qa/page/project/operations/kubernetes/index.rb19
-rw-r--r--qa/qa/page/project/operations/kubernetes/show.rb39
-rw-r--r--qa/qa/page/project/pipeline/show.rb4
-rw-r--r--qa/qa/page/project/settings/advanced.rb6
-rw-r--r--qa/qa/page/project/settings/ci_cd.rb25
-rw-r--r--qa/qa/page/project/settings/deploy_keys.rb4
-rw-r--r--qa/qa/page/project/settings/main.rb4
-rw-r--r--qa/qa/page/project/settings/merge_request.rb12
-rw-r--r--qa/qa/page/project/settings/protected_branches.rb18
-rw-r--r--qa/qa/page/project/settings/repository.rb10
-rw-r--r--qa/qa/page/project/show.rb43
-rw-r--r--qa/qa/page/project/wiki/edit.rb27
-rw-r--r--qa/qa/page/project/wiki/new.rb45
-rw-r--r--qa/qa/page/project/wiki/show.rb19
-rw-r--r--qa/qa/page/settings/common.rb20
-rw-r--r--qa/qa/page/shared/clone_panel.rb50
-rw-r--r--qa/qa/runtime/api.rb82
-rw-r--r--qa/qa/runtime/api/client.rb39
-rw-r--r--qa/qa/runtime/api/request.rb43
-rw-r--r--qa/qa/runtime/browser.rb24
-rw-r--r--qa/qa/runtime/env.rb33
-rw-r--r--qa/qa/runtime/namespace.rb4
-rw-r--r--qa/qa/runtime/release.rb2
-rw-r--r--qa/qa/scenario/test/integration/github.rb18
-rw-r--r--qa/qa/scenario/test/integration/kubernetes.rb11
-rw-r--r--qa/qa/service/kubernetes_cluster.rb77
-rw-r--r--qa/qa/service/runner.rb1
-rw-r--r--qa/qa/specs/features/api/basics_spec.rb61
-rw-r--r--qa/qa/specs/features/api/users_spec.rb21
-rw-r--r--qa/qa/specs/features/login/ldap_spec.rb10
-rw-r--r--qa/qa/specs/features/login/standard_spec.rb4
-rw-r--r--qa/qa/specs/features/mattermost/group_create_spec.rb4
-rw-r--r--qa/qa/specs/features/mattermost/login_spec.rb4
-rw-r--r--qa/qa/specs/features/merge_request/create_spec.rb28
-rw-r--r--qa/qa/specs/features/merge_request/rebase_spec.rb6
-rw-r--r--qa/qa/specs/features/merge_request/squash_spec.rb50
-rw-r--r--qa/qa/specs/features/project/activity_spec.rb6
-rw-r--r--qa/qa/specs/features/project/add_deploy_key_spec.rb4
-rw-r--r--qa/qa/specs/features/project/add_secret_variable_spec.rb4
-rw-r--r--qa/qa/specs/features/project/auto_devops_spec.rb57
-rw-r--r--qa/qa/specs/features/project/create_issue_spec.rb4
-rw-r--r--qa/qa/specs/features/project/create_spec.rb4
-rw-r--r--qa/qa/specs/features/project/deploy_key_clone_spec.rb10
-rw-r--r--qa/qa/specs/features/project/import_from_github_spec.rb106
-rw-r--r--qa/qa/specs/features/project/pipelines_spec.rb8
-rw-r--r--qa/qa/specs/features/project/wikis_spec.rb45
-rw-r--r--qa/qa/specs/features/repository/clone_spec.rb8
-rw-r--r--qa/qa/specs/features/repository/protected_branches_spec.rb87
-rw-r--r--qa/qa/specs/features/repository/push_spec.rb6
-rw-r--r--qa/spec/git/repository_spec.rb40
-rw-r--r--qa/spec/runtime/api/client_spec.rb (renamed from qa/spec/runtime/api_client_spec.rb)0
-rw-r--r--qa/spec/runtime/api/request_spec.rb44
-rw-r--r--qa/spec/runtime/api_request_spec.rb42
-rw-r--r--qa/spec/runtime/env_spec.rb23
-rw-r--r--qa/spec/support/stub_env.rb2
-rw-r--r--rubocop/cop/gitlab/finder_with_find_by.rb52
-rw-r--r--rubocop/cop/line_break_around_conditional_block.rb4
-rw-r--r--rubocop/cop/migration/update_large_table.rb15
-rw-r--r--rubocop/rubocop.rb1
-rw-r--r--scripts/frontend/postinstall.js22
-rw-r--r--scripts/frontend/prettier.js57
-rwxr-xr-xscripts/prune-old-flaky-specs6
-rwxr-xr-xscripts/rails5-gemfile-lock-check19
-rwxr-xr-xscripts/trigger-build185
-rwxr-xr-xscripts/trigger-build-cloud-native61
-rwxr-xr-xscripts/trigger-build-docs14
-rwxr-xr-xscripts/trigger-build-omnibus108
-rw-r--r--spec/bin/changelog_spec.rb25
-rw-r--r--spec/controllers/admin/application_settings_controller_spec.rb2
-rw-r--r--spec/controllers/application_controller_spec.rb120
-rw-r--r--spec/controllers/autocomplete_controller_spec.rb20
-rw-r--r--spec/controllers/boards/issues_controller_spec.rb6
-rw-r--r--spec/controllers/boards/lists_controller_spec.rb20
-rw-r--r--spec/controllers/concerns/controller_with_cross_project_access_check_spec.rb12
-rw-r--r--spec/controllers/concerns/group_tree_spec.rb11
-rw-r--r--spec/controllers/concerns/internal_redirect_spec.rb25
-rw-r--r--spec/controllers/dashboard/groups_controller_spec.rb4
-rw-r--r--spec/controllers/dashboard/milestones_controller_spec.rb2
-rw-r--r--spec/controllers/dashboard_controller_spec.rb2
-rw-r--r--spec/controllers/graphql_controller_spec.rb69
-rw-r--r--spec/controllers/groups/avatars_controller_spec.rb2
-rw-r--r--spec/controllers/groups/boards_controller_spec.rb2
-rw-r--r--spec/controllers/groups/milestones_controller_spec.rb2
-rw-r--r--spec/controllers/groups/runners_controller_spec.rb5
-rw-r--r--spec/controllers/groups/settings/ci_cd_controller_spec.rb2
-rw-r--r--spec/controllers/groups/shared_projects_controller_spec.rb68
-rw-r--r--spec/controllers/groups/uploads_controller_spec.rb8
-rw-r--r--spec/controllers/groups/variables_controller_spec.rb2
-rw-r--r--spec/controllers/groups_controller_spec.rb6
-rw-r--r--spec/controllers/health_controller_spec.rb4
-rw-r--r--spec/controllers/import/gitlab_projects_controller_spec.rb2
-rw-r--r--spec/controllers/import/google_code_controller_spec.rb2
-rw-r--r--spec/controllers/metrics_controller_spec.rb7
-rw-r--r--spec/controllers/oauth/authorizations_controller_spec.rb34
-rw-r--r--spec/controllers/omniauth_callbacks_controller_spec.rb189
-rw-r--r--spec/controllers/profiles/avatars_controller_spec.rb2
-rw-r--r--spec/controllers/profiles_controller_spec.rb13
-rw-r--r--spec/controllers/projects/avatars_controller_spec.rb4
-rw-r--r--spec/controllers/projects/badges_controller_spec.rb2
-rw-r--r--spec/controllers/projects/blame_controller_spec.rb2
-rw-r--r--spec/controllers/projects/blob_controller_spec.rb91
-rw-r--r--spec/controllers/projects/boards_controller_spec.rb68
-rw-r--r--spec/controllers/projects/branches_controller_spec.rb20
-rw-r--r--spec/controllers/projects/clusters/applications_controller_spec.rb4
-rw-r--r--spec/controllers/projects/clusters/gcp_controller_spec.rb202
-rw-r--r--spec/controllers/projects/clusters/user_controller_spec.rb89
-rw-r--r--spec/controllers/projects/clusters_controller_spec.rb255
-rw-r--r--spec/controllers/projects/commit_controller_spec.rb2
-rw-r--r--spec/controllers/projects/commits_controller_spec.rb2
-rw-r--r--spec/controllers/projects/compare_controller_spec.rb2
-rw-r--r--spec/controllers/projects/cycle_analytics_controller_spec.rb2
-rw-r--r--spec/controllers/projects/deploy_keys_controller_spec.rb2
-rw-r--r--spec/controllers/projects/deployments_controller_spec.rb2
-rw-r--r--spec/controllers/projects/discussions_controller_spec.rb23
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb28
-rw-r--r--spec/controllers/projects/find_file_controller_spec.rb2
-rw-r--r--spec/controllers/projects/forks_controller_spec.rb2
-rw-r--r--spec/controllers/projects/graphs_controller_spec.rb2
-rw-r--r--spec/controllers/projects/group_links_controller_spec.rb14
-rw-r--r--spec/controllers/projects/hooks_controller_spec.rb2
-rw-r--r--spec/controllers/projects/imports_controller_spec.rb26
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb8
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb126
-rw-r--r--spec/controllers/projects/labels_controller_spec.rb2
-rw-r--r--spec/controllers/projects/mattermosts_controller_spec.rb2
-rw-r--r--spec/controllers/projects/merge_requests/conflicts_controller_spec.rb2
-rw-r--r--spec/controllers/projects/merge_requests/creations_controller_spec.rb4
-rw-r--r--spec/controllers/projects/merge_requests/diffs_controller_spec.rb31
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb60
-rw-r--r--spec/controllers/projects/milestones_controller_spec.rb39
-rw-r--r--spec/controllers/projects/mirrors_controller_spec.rb2
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb14
-rw-r--r--spec/controllers/projects/pages_controller_spec.rb6
-rw-r--r--spec/controllers/projects/pages_domains_controller_spec.rb2
-rw-r--r--spec/controllers/projects/pipeline_schedules_controller_spec.rb44
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb131
-rw-r--r--spec/controllers/projects/pipelines_settings_controller_spec.rb2
-rw-r--r--spec/controllers/projects/project_members_controller_spec.rb22
-rw-r--r--spec/controllers/projects/prometheus/metrics_controller_spec.rb2
-rw-r--r--spec/controllers/projects/protected_branches_controller_spec.rb8
-rw-r--r--spec/controllers/projects/protected_tags_controller_spec.rb2
-rw-r--r--spec/controllers/projects/runners_controller_spec.rb5
-rw-r--r--spec/controllers/projects/services_controller_spec.rb4
-rw-r--r--spec/controllers/projects/settings/ci_cd_controller_spec.rb14
-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.rb6
-rw-r--r--spec/controllers/projects/templates_controller_spec.rb2
-rw-r--r--spec/controllers/projects/tree_controller_spec.rb2
-rw-r--r--spec/controllers/projects/uploads_controller_spec.rb8
-rw-r--r--spec/controllers/projects/variables_controller_spec.rb2
-rw-r--r--spec/controllers/projects_controller_spec.rb125
-rw-r--r--spec/controllers/registrations_controller_spec.rb21
-rw-r--r--spec/controllers/search_controller_spec.rb2
-rw-r--r--spec/controllers/sessions_controller_spec.rb62
-rw-r--r--spec/controllers/uploads_controller_spec.rb109
-rw-r--r--spec/controllers/users/terms_controller_spec.rb22
-rw-r--r--spec/dependencies/omniauth_saml_spec.rb22
-rw-r--r--spec/factories/application_settings.rb1
-rw-r--r--spec/factories/ci/build_trace_chunks.rb48
-rw-r--r--spec/factories/ci/builds.rb6
-rw-r--r--spec/factories/ci/runner_projects.rb2
-rw-r--r--spec/factories/ci/runners.rb31
-rw-r--r--spec/factories/clusters/applications/helm.rb3
-rw-r--r--spec/factories/group_members.rb2
-rw-r--r--spec/factories/import_export_uploads.rb5
-rw-r--r--spec/factories/issues.rb3
-rw-r--r--spec/factories/lfs_objects.rb2
-rw-r--r--spec/factories/merge_requests.rb8
-rw-r--r--spec/factories/notes.rb4
-rw-r--r--spec/factories/project_auto_devops.rb9
-rw-r--r--spec/factories/project_members.rb4
-rw-r--r--spec/factories/projects.rb30
-rw-r--r--spec/factories/protected_branches.rb8
-rw-r--r--spec/factories/protected_tags.rb6
-rw-r--r--spec/factories/term_agreements.rb8
-rw-r--r--spec/factories/users.rb4
-rw-r--r--spec/features/abuse_report_spec.rb4
-rw-r--r--spec/features/admin/admin_abuse_reports_spec.rb2
-rw-r--r--spec/features/admin/admin_appearance_spec.rb36
-rw-r--r--spec/features/admin/admin_broadcast_messages_spec.rb12
-rw-r--r--spec/features/admin/admin_browse_spam_logs_spec.rb2
-rw-r--r--spec/features/admin/admin_cohorts_spec.rb4
-rw-r--r--spec/features/admin/admin_disables_git_access_protocol_spec.rb16
-rw-r--r--spec/features/admin/admin_disables_two_factor_spec.rb6
-rw-r--r--spec/features/admin/admin_groups_spec.rb15
-rw-r--r--spec/features/admin/admin_health_check_spec.rb2
-rw-r--r--spec/features/admin/admin_hook_logs_spec.rb8
-rw-r--r--spec/features/admin/admin_projects_spec.rb6
-rw-r--r--spec/features/admin/admin_requests_profiles_spec.rb4
-rw-r--r--spec/features/admin/admin_runners_spec.rb25
-rw-r--r--spec/features/admin/admin_settings_spec.rb77
-rw-r--r--spec/features/admin/admin_users_spec.rb6
-rw-r--r--spec/features/admin/admin_uses_repository_checks_spec.rb10
-rw-r--r--spec/features/atom/dashboard_issues_spec.rb16
-rw-r--r--spec/features/atom/dashboard_spec.rb8
-rw-r--r--spec/features/atom/issues_spec.rb13
-rw-r--r--spec/features/atom/users_spec.rb8
-rw-r--r--spec/features/boards/add_issues_modal_spec.rb34
-rw-r--r--spec/features/boards/boards_spec.rb58
-rw-r--r--spec/features/boards/issue_ordering_spec.rb43
-rw-r--r--spec/features/boards/modal_filter_spec.rb24
-rw-r--r--spec/features/boards/new_issue_spec.rb4
-rw-r--r--spec/features/boards/sidebar_spec.rb28
-rw-r--r--spec/features/boards/sub_group_project_spec.rb4
-rw-r--r--spec/features/calendar_spec.rb2
-rw-r--r--spec/features/commits_spec.rb10
-rw-r--r--spec/features/container_registry_spec.rb8
-rw-r--r--spec/features/cycle_analytics_spec.rb8
-rw-r--r--spec/features/dashboard/activity_spec.rb22
-rw-r--r--spec/features/dashboard/archived_projects_spec.rb4
-rw-r--r--spec/features/dashboard/datetime_on_tooltips_spec.rb6
-rw-r--r--spec/features/dashboard/groups_list_spec.rb6
-rw-r--r--spec/features/dashboard/issues_filter_spec.rb10
-rw-r--r--spec/features/dashboard/issues_spec.rb6
-rw-r--r--spec/features/dashboard/merge_requests_spec.rb6
-rw-r--r--spec/features/dashboard/milestone_filter_spec.rb2
-rw-r--r--spec/features/dashboard/milestone_tabs_spec.rb2
-rw-r--r--spec/features/dashboard/milestones_spec.rb4
-rw-r--r--spec/features/dashboard/project_member_activity_index_spec.rb4
-rw-r--r--spec/features/dashboard/projects_spec.rb40
-rw-r--r--spec/features/dashboard/shortcuts_spec.rb6
-rw-r--r--spec/features/dashboard/todos/target_state_spec.rb12
-rw-r--r--spec/features/dashboard/todos/todos_filtering_spec.rb2
-rw-r--r--spec/features/dashboard/todos/todos_sorting_spec.rb2
-rw-r--r--spec/features/dashboard/todos/todos_spec.rb4
-rw-r--r--spec/features/dashboard/user_filters_projects_spec.rb2
-rw-r--r--spec/features/discussion_comments/commit_spec.rb2
-rw-r--r--spec/features/discussion_comments/issue_spec.rb2
-rw-r--r--spec/features/discussion_comments/merge_request_spec.rb2
-rw-r--r--spec/features/discussion_comments/snippets_spec.rb2
-rw-r--r--spec/features/error_pages_spec.rb42
-rw-r--r--spec/features/expand_collapse_diffs_spec.rb2
-rw-r--r--spec/features/explore/groups_list_spec.rb4
-rw-r--r--spec/features/explore/new_menu_spec.rb38
-rw-r--r--spec/features/global_search_spec.rb4
-rw-r--r--spec/features/group_variables_spec.rb6
-rw-r--r--spec/features/groups/activity_spec.rb12
-rw-r--r--spec/features/groups/empty_states_spec.rb34
-rw-r--r--spec/features/groups/group_settings_spec.rb53
-rw-r--r--spec/features/groups/issues_spec.rb40
-rw-r--r--spec/features/groups/labels/edit_spec.rb12
-rw-r--r--spec/features/groups/labels/index_spec.rb17
-rw-r--r--spec/features/groups/labels/subscription_spec.rb4
-rw-r--r--spec/features/groups/labels/user_sees_links_to_issuables.rb4
-rw-r--r--spec/features/groups/members/filter_members_spec.rb12
-rw-r--r--spec/features/groups/members/leave_group_spec.rb12
-rw-r--r--spec/features/groups/members/list_members_spec.rb8
-rw-r--r--spec/features/groups/members/manage_members_spec.rb18
-rw-r--r--spec/features/groups/members/master_manages_access_requests_spec.rb4
-rw-r--r--spec/features/groups/members/request_access_spec.rb20
-rw-r--r--spec/features/groups/members/search_members_spec.rb2
-rw-r--r--spec/features/groups/members/sort_members_spec.rb22
-rw-r--r--spec/features/groups/merge_requests_spec.rb19
-rw-r--r--spec/features/groups/milestone_spec.rb17
-rw-r--r--spec/features/groups/milestones_sorting_spec.rb6
-rw-r--r--spec/features/groups/settings/group_badges_spec.rb18
-rw-r--r--spec/features/groups/share_lock_spec.rb51
-rw-r--r--spec/features/groups/show_spec.rb6
-rw-r--r--spec/features/groups/user_browse_projects_group_page_spec.rb2
-rw-r--r--spec/features/groups/user_sees_users_dropdowns_in_issuables_list_spec.rb2
-rw-r--r--spec/features/groups_spec.rb14
-rw-r--r--spec/features/ics/dashboard_issues_spec.rb102
-rw-r--r--spec/features/ics/group_issues_spec.rb77
-rw-r--r--spec/features/ics/project_issues_spec.rb76
-rw-r--r--spec/features/ide_spec.rb2
-rw-r--r--spec/features/invites_spec.rb2
-rw-r--r--spec/features/issuables/close_reopen_report_toggle_spec.rb4
-rw-r--r--spec/features/issuables/markdown_references/jira_spec.rb2
-rw-r--r--spec/features/issuables/shortcuts_issuable_spec.rb25
-rw-r--r--spec/features/issues/award_emoji_spec.rb2
-rw-r--r--spec/features/issues/award_spec.rb2
-rw-r--r--spec/features/issues/bulk_assignment_labels_spec.rb4
-rw-r--r--spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb18
-rw-r--r--spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb24
-rw-r--r--spec/features/issues/filtered_search/dropdown_assignee_spec.rb59
-rw-r--r--spec/features/issues/filtered_search/dropdown_author_spec.rb8
-rw-r--r--spec/features/issues/filtered_search/dropdown_emoji_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/dropdown_hint_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/dropdown_label_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/dropdown_milestone_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/filter_issues_spec.rb44
-rw-r--r--spec/features/issues/filtered_search/search_bar_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/visual_tokens_spec.rb4
-rw-r--r--spec/features/issues/form_spec.rb9
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb4
-rw-r--r--spec/features/issues/issue_detail_spec.rb2
-rw-r--r--spec/features/issues/issue_sidebar_spec.rb4
-rw-r--r--spec/features/issues/markdown_toolbar_spec.rb2
-rw-r--r--spec/features/issues/move_spec.rb24
-rw-r--r--spec/features/issues/note_polling_spec.rb2
-rw-r--r--spec/features/issues/spam_issues_spec.rb2
-rw-r--r--spec/features/issues/todo_spec.rb4
-rw-r--r--spec/features/issues/update_issues_spec.rb4
-rw-r--r--spec/features/issues/user_creates_branch_and_merge_request_spec.rb4
-rw-r--r--spec/features/issues/user_uses_slash_commands_spec.rb48
-rw-r--r--spec/features/issues_spec.rb30
-rw-r--r--spec/features/labels_hierarchy_spec.rb26
-rw-r--r--spec/features/markdown/copy_as_gfm_spec.rb9
-rw-r--r--spec/features/markdown/gitlab_flavored_markdown_spec.rb2
-rw-r--r--spec/features/markdown/markdown_spec.rb235
-rw-r--r--spec/features/merge_request/maintainer_edits_fork_spec.rb4
-rw-r--r--spec/features/merge_request/user_allows_a_maintainer_to_push_spec.rb85
-rw-r--r--spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb85
-rw-r--r--spec/features/merge_request/user_cherry_picks_spec.rb2
-rw-r--r--spec/features/merge_request/user_creates_image_diff_notes_spec.rb10
-rw-r--r--spec/features/merge_request/user_customizes_merge_commit_message_spec.rb2
-rw-r--r--spec/features/merge_request/user_locks_discussion_spec.rb4
-rw-r--r--spec/features/merge_request/user_merges_immediately_spec.rb2
-rw-r--r--spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb2
-rw-r--r--spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb2
-rw-r--r--spec/features/merge_request/user_posts_diff_notes_spec.rb29
-rw-r--r--spec/features/merge_request/user_posts_notes_spec.rb28
-rw-r--r--spec/features/merge_request/user_resolves_conflicts_spec.rb23
-rw-r--r--spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb48
-rw-r--r--spec/features/merge_request/user_resolves_outdated_diff_discussions_spec.rb4
-rw-r--r--spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb10
-rw-r--r--spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb8
-rw-r--r--spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb24
-rw-r--r--spec/features/merge_request/user_sees_closing_issues_message_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_deleted_target_branch_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_diff_spec.rb15
-rw-r--r--spec/features/merge_request/user_sees_discussions_spec.rb10
-rw-r--r--spec/features/merge_request/user_sees_empty_state_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_merge_button_depending_on_unresolved_discussions_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_merge_widget_spec.rb6
-rw-r--r--spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb7
-rw-r--r--spec/features/merge_request/user_sees_notes_from_forked_project_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_pipelines_spec.rb4
-rw-r--r--spec/features/merge_request/user_sees_system_notes_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_versions_spec.rb8
-rw-r--r--spec/features/merge_request/user_sees_wip_help_message_spec.rb2
-rw-r--r--spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb38
-rw-r--r--spec/features/merge_request/user_toggles_whitespace_changes_spec.rb2
-rw-r--r--spec/features/merge_request/user_uses_slash_commands_spec.rb37
-rw-r--r--spec/features/merge_requests/user_mass_updates_spec.rb2
-rw-r--r--spec/features/merge_requests/user_squashes_merge_request_spec.rb124
-rw-r--r--spec/features/milestone_spec.rb32
-rw-r--r--spec/features/milestones/user_deletes_milestone_spec.rb1
-rw-r--r--spec/features/milestones/user_edits_milestone_spec.rb22
-rw-r--r--spec/features/oauth_login_spec.rb2
-rw-r--r--spec/features/participants_autocomplete_spec.rb8
-rw-r--r--spec/features/password_reset_spec.rb2
-rw-r--r--spec/features/profile_spec.rb12
-rw-r--r--spec/features/profiles/account_spec.rb26
-rw-r--r--spec/features/profiles/active_sessions_spec.rb6
-rw-r--r--spec/features/profiles/chat_names_spec.rb26
-rw-r--r--spec/features/profiles/emails_spec.rb14
-rw-r--r--spec/features/profiles/gpg_keys_spec.rb12
-rw-r--r--spec/features/profiles/keys_spec.rb28
-rw-r--r--spec/features/profiles/password_spec.rb2
-rw-r--r--spec/features/profiles/user_changes_notified_of_own_activity_spec.rb6
-rw-r--r--spec/features/profiles/user_visits_notifications_tab_spec.rb4
-rw-r--r--spec/features/profiles/user_visits_profile_spec.rb2
-rw-r--r--spec/features/project_variables_spec.rb2
-rw-r--r--spec/features/projects/activity/rss_spec.rb6
-rw-r--r--spec/features/projects/activity/user_sees_activity_spec.rb2
-rw-r--r--spec/features/projects/actve_tabs_spec.rb2
-rw-r--r--spec/features/projects/artifacts/file_spec.rb2
-rw-r--r--spec/features/projects/artifacts/raw_spec.rb2
-rw-r--r--spec/features/projects/awards/user_interacts_with_awards_in_issue_spec.rb2
-rw-r--r--spec/features/projects/badges/coverage_spec.rb20
-rw-r--r--spec/features/projects/badges/list_spec.rb12
-rw-r--r--spec/features/projects/badges/pipeline_badge_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.rb14
-rw-r--r--spec/features/projects/blobs/edit_spec.rb6
-rw-r--r--spec/features/projects/blobs/shortcuts_blob_spec.rb2
-rw-r--r--spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb6
-rw-r--r--spec/features/projects/branches/download_buttons_spec.rb18
-rw-r--r--spec/features/projects/branches/new_branch_ref_dropdown_spec.rb2
-rw-r--r--spec/features/projects/branches_spec.rb4
-rw-r--r--spec/features/projects/clusters/applications_spec.rb8
-rw-r--r--spec/features/projects/clusters/gcp_spec.rb216
-rw-r--r--spec/features/projects/clusters/interchangeability_spec.rb2
-rw-r--r--spec/features/projects/clusters/user_spec.rb6
-rw-r--r--spec/features/projects/clusters_spec.rb6
-rw-r--r--spec/features/projects/commit/builds_spec.rb12
-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.rb2
-rw-r--r--spec/features/projects/commit/user_comments_on_commit_spec.rb109
-rw-r--r--spec/features/projects/commits/rss_spec.rb10
-rw-r--r--spec/features/projects/commits/user_browses_commits_spec.rb2
-rw-r--r--spec/features/projects/compare_spec.rb2
-rw-r--r--spec/features/projects/deploy_keys_spec.rb5
-rw-r--r--spec/features/projects/diffs/diff_show_spec.rb7
-rw-r--r--spec/features/projects/environments/environment_metrics_spec.rb18
-rw-r--r--spec/features/projects/environments/environment_spec.rb93
-rw-r--r--spec/features/projects/environments/environments_spec.rb64
-rw-r--r--spec/features/projects/features_visibility_spec.rb4
-rw-r--r--spec/features/projects/files/project_owner_creates_license_file_spec.rb10
-rw-r--r--spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb6
-rw-r--r--spec/features/projects/files/template_selector_menu_spec.rb12
-rw-r--r--spec/features/projects/files/user_browses_files_spec.rb3
-rw-r--r--spec/features/projects/files/user_creates_files_spec.rb2
-rw-r--r--spec/features/projects/files/user_deletes_files_spec.rb2
-rw-r--r--spec/features/projects/files/user_edits_files_spec.rb2
-rw-r--r--spec/features/projects/files/user_find_file_spec.rb2
-rw-r--r--spec/features/projects/files/user_reads_pipeline_status_spec.rb4
-rw-r--r--spec/features/projects/files/user_replaces_files_spec.rb2
-rw-r--r--spec/features/projects/files/user_uploads_files_spec.rb2
-rw-r--r--spec/features/projects/fork_spec.rb4
-rw-r--r--spec/features/projects/graph_spec.rb22
-rw-r--r--spec/features/projects/hook_logs/user_reads_log_spec.rb12
-rw-r--r--spec/features/projects/import_export/export_file_spec.rb7
-rw-r--r--spec/features/projects/import_export/import_file_spec.rb16
-rw-r--r--spec/features/projects/import_export/namespace_export_file_spec.rb1
-rw-r--r--spec/features/projects/import_export/test_project_export.tar.gzbin343091 -> 3368 bytes
-rw-r--r--spec/features/projects/issuable_templates_spec.rb24
-rw-r--r--spec/features/projects/issues/rss_spec.rb10
-rw-r--r--spec/features/projects/issues/user_comments_on_issue_spec.rb15
-rw-r--r--spec/features/projects/issues/user_creates_issue_spec.rb3
-rw-r--r--spec/features/projects/issues/user_sorts_issues_spec.rb2
-rw-r--r--spec/features/projects/jobs/permissions_spec.rb5
-rw-r--r--spec/features/projects/jobs/user_browses_job_spec.rb4
-rw-r--r--spec/features/projects/jobs/user_browses_jobs_spec.rb2
-rw-r--r--spec/features/projects/jobs_spec.rb22
-rw-r--r--spec/features/projects/labels/issues_sorted_by_priority_spec.rb6
-rw-r--r--spec/features/projects/labels/subscription_spec.rb10
-rw-r--r--spec/features/projects/labels/update_prioritization_spec.rb20
-rw-r--r--spec/features/projects/labels/user_creates_labels_spec.rb4
-rw-r--r--spec/features/projects/labels/user_edits_labels_spec.rb2
-rw-r--r--spec/features/projects/labels/user_removes_labels_spec.rb22
-rw-r--r--spec/features/projects/labels/user_sees_links_to_issuables.rb14
-rw-r--r--spec/features/projects/members/anonymous_user_sees_members_spec.rb8
-rw-r--r--spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb6
-rw-r--r--spec/features/projects/members/group_member_cannot_request_access_to_his_group_project_spec.rb14
-rw-r--r--spec/features/projects/members/group_members_spec.rb12
-rw-r--r--spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb6
-rw-r--r--spec/features/projects/members/groups_with_access_list_spec.rb16
-rw-r--r--spec/features/projects/members/list_spec.rb16
-rw-r--r--spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb14
-rw-r--r--spec/features/projects/members/master_manages_access_requests_spec.rb4
-rw-r--r--spec/features/projects/members/member_cannot_request_access_to_his_project_spec.rb6
-rw-r--r--spec/features/projects/members/member_leaves_project_spec.rb6
-rw-r--r--spec/features/projects/members/owner_cannot_leave_project_spec.rb6
-rw-r--r--spec/features/projects/members/owner_cannot_request_access_to_his_project_spec.rb6
-rw-r--r--spec/features/projects/members/share_with_group_spec.rb50
-rw-r--r--spec/features/projects/members/sorting_spec.rb46
-rw-r--r--spec/features/projects/members/user_requests_access_spec.rb20
-rw-r--r--spec/features/projects/merge_request_button_spec.rb2
-rw-r--r--spec/features/projects/merge_requests/user_closes_merge_request_spec.rb2
-rw-r--r--spec/features/projects/merge_requests/user_comments_on_commit_spec.rb2
-rw-r--r--spec/features/projects/merge_requests/user_comments_on_diff_spec.rb16
-rw-r--r--spec/features/projects/merge_requests/user_comments_on_merge_request_spec.rb5
-rw-r--r--spec/features/projects/merge_requests/user_creates_merge_request_spec.rb2
-rw-r--r--spec/features/projects/merge_requests/user_edits_merge_request_spec.rb2
-rw-r--r--spec/features/projects/merge_requests/user_manages_subscription_spec.rb2
-rw-r--r--spec/features/projects/merge_requests/user_reopens_merge_request_spec.rb2
-rw-r--r--spec/features/projects/merge_requests/user_reverts_merge_request_spec.rb2
-rw-r--r--spec/features/projects/merge_requests/user_sorts_merge_requests_spec.rb6
-rw-r--r--spec/features/projects/merge_requests/user_views_diffs_spec.rb16
-rw-r--r--spec/features/projects/merge_requests/user_views_open_merge_request_spec.rb2
-rw-r--r--spec/features/projects/milestones/milestone_spec.rb10
-rw-r--r--spec/features/projects/milestones/milestones_sorting_spec.rb4
-rw-r--r--spec/features/projects/milestones/new_spec.rb6
-rw-r--r--spec/features/projects/milestones/user_interacts_with_labels_spec.rb2
-rw-r--r--spec/features/projects/new_project_spec.rb13
-rw-r--r--spec/features/projects/pages_spec.rb40
-rw-r--r--spec/features/projects/pipeline_schedules_spec.rb22
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb16
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb4
-rw-r--r--spec/features/projects/remote_mirror_spec.rb8
-rw-r--r--spec/features/projects/services/user_activates_asana_spec.rb2
-rw-r--r--spec/features/projects/services/user_activates_assembla_spec.rb2
-rw-r--r--spec/features/projects/services/user_activates_atlassian_bamboo_ci_spec.rb2
-rw-r--r--spec/features/projects/services/user_activates_emails_on_push_spec.rb2
-rw-r--r--spec/features/projects/services/user_activates_flowdock_spec.rb2
-rw-r--r--spec/features/projects/services/user_activates_hipchat_spec.rb2
-rw-r--r--spec/features/projects/services/user_activates_irker_spec.rb2
-rw-r--r--spec/features/projects/services/user_activates_issue_tracker_spec.rb2
-rw-r--r--spec/features/projects/services/user_activates_jetbrains_teamcity_ci_spec.rb2
-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.rb2
-rw-r--r--spec/features/projects/services/user_activates_pivotaltracker_spec.rb2
-rw-r--r--spec/features/projects/services/user_activates_prometheus_spec.rb2
-rw-r--r--spec/features/projects/services/user_activates_pushover_spec.rb2
-rw-r--r--spec/features/projects/services/user_activates_slack_notifications_spec.rb4
-rw-r--r--spec/features/projects/services/user_activates_slack_slash_command_spec.rb12
-rw-r--r--spec/features/projects/services/user_views_services_spec.rb2
-rw-r--r--spec/features/projects/settings/forked_project_settings_spec.rb4
-rw-r--r--spec/features/projects/settings/integration_settings_spec.rb4
-rw-r--r--spec/features/projects/settings/lfs_settings_spec.rb6
-rw-r--r--spec/features/projects/settings/pipelines_settings_spec.rb4
-rw-r--r--spec/features/projects/settings/project_badges_spec.rb20
-rw-r--r--spec/features/projects/settings/repository_settings_spec.rb10
-rw-r--r--spec/features/projects/settings/user_archives_project_spec.rb2
-rw-r--r--spec/features/projects/settings/user_changes_avatar_spec.rb2
-rw-r--r--spec/features/projects/settings/user_interacts_with_deploy_keys_spec.rb4
-rw-r--r--spec/features/projects/settings/user_manages_group_links_spec.rb6
-rw-r--r--spec/features/projects/settings/user_manages_project_members_spec.rb8
-rw-r--r--spec/features/projects/settings/user_sees_revoke_deploy_token_modal_spec.rb25
-rw-r--r--spec/features/projects/settings/user_transfers_a_project_spec.rb9
-rw-r--r--spec/features/projects/settings/visibility_settings_spec.rb8
-rw-r--r--spec/features/projects/show/developer_views_empty_project_instructions_spec.rb14
-rw-r--r--spec/features/projects/show/download_buttons_spec.rb20
-rw-r--r--spec/features/projects/show/no_password_spec.rb2
-rw-r--r--spec/features/projects/show/rss_spec.rb6
-rw-r--r--spec/features/projects/show/user_sees_deletion_failure_message_spec.rb2
-rw-r--r--spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb68
-rw-r--r--spec/features/projects/snippets/create_snippet_spec.rb2
-rw-r--r--spec/features/projects/snippets/show_spec.rb2
-rw-r--r--spec/features/projects/snippets/user_comments_on_snippet_spec.rb2
-rw-r--r--spec/features/projects/snippets/user_deletes_snippet_spec.rb2
-rw-r--r--spec/features/projects/snippets/user_updates_snippet_spec.rb2
-rw-r--r--spec/features/projects/snippets/user_views_snippets_spec.rb2
-rw-r--r--spec/features/projects/sub_group_issuables_spec.rb2
-rw-r--r--spec/features/projects/tags/download_buttons_spec.rb20
-rw-r--r--spec/features/projects/tree/create_directory_spec.rb4
-rw-r--r--spec/features/projects/tree/create_file_spec.rb4
-rw-r--r--spec/features/projects/tree/rss_spec.rb6
-rw-r--r--spec/features/projects/tree/tree_show_spec.rb4
-rw-r--r--spec/features/projects/tree/upload_file_spec.rb4
-rw-r--r--spec/features/projects/user_creates_project_spec.rb2
-rw-r--r--spec/features/projects/user_sees_sidebar_spec.rb12
-rw-r--r--spec/features/projects/user_uses_shortcuts_spec.rb10
-rw-r--r--spec/features/projects/user_views_empty_project_spec.rb4
-rw-r--r--spec/features/projects/view_on_env_spec.rb6
-rw-r--r--spec/features/projects/wiki/markdown_preview_spec.rb14
-rw-r--r--spec/features/projects/wiki/shortcuts_spec.rb4
-rw-r--r--spec/features/projects/wiki/user_creates_wiki_page_spec.rb7
-rw-r--r--spec/features/projects/wiki/user_deletes_wiki_page_spec.rb2
-rw-r--r--spec/features/projects/wiki/user_git_access_wiki_page_spec.rb2
-rw-r--r--spec/features/projects/wiki/user_updates_wiki_page_spec.rb7
-rw-r--r--spec/features/projects/wiki/user_views_wiki_empty_spec.rb75
-rw-r--r--spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb2
-rw-r--r--spec/features/projects/wiki/user_views_wiki_page_spec.rb4
-rw-r--r--spec/features/projects_spec.rb14
-rw-r--r--spec/features/protected_branches_spec.rb51
-rw-r--r--spec/features/protected_tags_spec.rb2
-rw-r--r--spec/features/raven_js_spec.rb2
-rw-r--r--spec/features/reportable_note/commit_spec.rb2
-rw-r--r--spec/features/reportable_note/issue_spec.rb2
-rw-r--r--spec/features/reportable_note/merge_request_spec.rb2
-rw-r--r--spec/features/reportable_note/snippets_spec.rb2
-rw-r--r--spec/features/runners_spec.rb141
-rw-r--r--spec/features/search/user_searches_for_code_spec.rb2
-rw-r--r--spec/features/search/user_searches_for_issues_spec.rb2
-rw-r--r--spec/features/search/user_searches_for_merge_requests_spec.rb2
-rw-r--r--spec/features/search/user_searches_for_milestones_spec.rb2
-rw-r--r--spec/features/search/user_searches_for_wiki_pages_spec.rb2
-rw-r--r--spec/features/security/group/internal_access_spec.rb10
-rw-r--r--spec/features/security/group/private_access_spec.rb10
-rw-r--r--spec/features/security/group/public_access_spec.rb10
-rw-r--r--spec/features/security/project/internal_access_spec.rb68
-rw-r--r--spec/features/security/project/private_access_spec.rb62
-rw-r--r--spec/features/security/project/public_access_spec.rb68
-rw-r--r--spec/features/security/project/snippet/internal_access_spec.rb12
-rw-r--r--spec/features/security/project/snippet/private_access_spec.rb8
-rw-r--r--spec/features/security/project/snippet/public_access_spec.rb16
-rw-r--r--spec/features/signed_commits_spec.rb69
-rw-r--r--spec/features/snippets/explore_spec.rb8
-rw-r--r--spec/features/snippets/internal_snippet_spec.rb6
-rw-r--r--spec/features/snippets/public_snippets_spec.rb6
-rw-r--r--spec/features/snippets/search_snippets_spec.rb6
-rw-r--r--spec/features/snippets/show_spec.rb22
-rw-r--r--spec/features/snippets/user_creates_snippet_spec.rb12
-rw-r--r--spec/features/snippets/user_deletes_snippet_spec.rb2
-rw-r--r--spec/features/snippets/user_edits_snippet_spec.rb2
-rw-r--r--spec/features/snippets/user_snippets_spec.rb12
-rw-r--r--spec/features/tags/master_creates_tag_spec.rb20
-rw-r--r--spec/features/tags/master_deletes_tag_spec.rb35
-rw-r--r--spec/features/tags/master_updates_tag_spec.rb12
-rw-r--r--spec/features/tags/master_views_tags_spec.rb16
-rw-r--r--spec/features/task_lists_spec.rb49
-rw-r--r--spec/features/triggers_spec.rb36
-rw-r--r--spec/features/u2f_spec.rb2
-rw-r--r--spec/features/uploads/user_uploads_avatar_to_group_spec.rb8
-rw-r--r--spec/features/uploads/user_uploads_avatar_to_profile_spec.rb29
-rw-r--r--spec/features/uploads/user_uploads_file_to_note_spec.rb4
-rw-r--r--spec/features/users/active_sessions_spec.rb10
-rw-r--r--spec/features/users/login_spec.rb41
-rw-r--r--spec/features/users/rss_spec.rb6
-rw-r--r--spec/features/users/show_spec.rb24
-rw-r--r--spec/features/users/signup_spec.rb30
-rw-r--r--spec/features/users/terms_spec.rb136
-rw-r--r--spec/features/users/user_browses_projects_on_user_page_spec.rb22
-rw-r--r--spec/finders/access_requests_finder_spec.rb4
-rw-r--r--spec/finders/admin/projects_finder_spec.rb2
-rw-r--r--spec/finders/concerns/finder_with_cross_project_access_spec.rb2
-rw-r--r--spec/finders/contributed_projects_finder_spec.rb4
-rw-r--r--spec/finders/environments_finder_spec.rb2
-rw-r--r--spec/finders/group_members_finder_spec.rb26
-rw-r--r--spec/finders/group_projects_finder_spec.rb18
-rw-r--r--spec/finders/issues_finder_spec.rb2
-rw-r--r--spec/finders/joined_groups_finder_spec.rb12
-rw-r--r--spec/finders/members_finder_spec.rb18
-rw-r--r--spec/finders/merge_requests_finder_spec.rb18
-rw-r--r--spec/finders/move_to_project_finder_spec.rb20
-rw-r--r--spec/finders/notes_finder_spec.rb4
-rw-r--r--spec/finders/personal_projects_finder_spec.rb2
-rw-r--r--spec/finders/pipelines_finder_spec.rb29
-rw-r--r--spec/finders/projects_finder_spec.rb4
-rw-r--r--spec/finders/runner_jobs_finder_spec.rb2
-rw-r--r--spec/finders/user_recent_events_finder_spec.rb45
-rw-r--r--spec/fixtures/api/schemas/cluster_status.json3
-rw-r--r--spec/fixtures/api/schemas/entities/issue.json2
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request_basic.json17
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request_widget.json6
-rw-r--r--spec/fixtures/api/schemas/list.json2
-rw-r--r--spec/fixtures/api/schemas/public_api/v3/issues.json78
-rw-r--r--spec/fixtures/api/schemas/public_api/v3/merge_requests.json90
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/branch.json3
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/commit/with_stats.json14
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/commits_with_stats.json4
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/merge_requests.json4
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/milestones.json3
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/snippets.json1
-rw-r--r--spec/fixtures/authentication/saml_response.xml42
-rw-r--r--spec/fixtures/exported-project.gzbin2560 -> 0 bytes
-rw-r--r--spec/fixtures/markdown.md.erb10
-rw-r--r--spec/fixtures/project_export.tar.gzbin0 -> 343091 bytes
-rw-r--r--spec/fixtures/trace/sample_trace32
-rw-r--r--spec/graphql/gitlab_schema_spec.rb39
-rw-r--r--spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb52
-rw-r--r--spec/graphql/resolvers/merge_request_pipelines_resolver_spec.rb30
-rw-r--r--spec/graphql/resolvers/merge_request_resolver_spec.rb45
-rw-r--r--spec/graphql/resolvers/project_pipelines_resolver_spec.rb22
-rw-r--r--spec/graphql/resolvers/project_resolver_spec.rb32
-rw-r--r--spec/graphql/types/ci/pipeline_type_spec.rb7
-rw-r--r--spec/graphql/types/merge_request_type_spec.rb16
-rw-r--r--spec/graphql/types/permission_types/base_permission_type_spec.rb47
-rw-r--r--spec/graphql/types/permission_types/merge_request_spec.rb13
-rw-r--r--spec/graphql/types/permission_types/merge_request_type_spec.rb5
-rw-r--r--spec/graphql/types/permission_types/project_spec.rb18
-rw-r--r--spec/graphql/types/project_type_spec.rb18
-rw-r--r--spec/graphql/types/query_type_spec.rb23
-rw-r--r--spec/graphql/types/time_type_spec.rb16
-rw-r--r--spec/helpers/application_helper_spec.rb2
-rw-r--r--spec/helpers/blob_helper_spec.rb4
-rw-r--r--spec/helpers/calendar_helper_spec.rb20
-rw-r--r--spec/helpers/emails_helper_spec.rb4
-rw-r--r--spec/helpers/groups_helper_spec.rb16
-rw-r--r--spec/helpers/issuables_helper_spec.rb9
-rw-r--r--spec/helpers/markup_helper_spec.rb25
-rw-r--r--spec/helpers/merge_requests_helper_spec.rb2
-rw-r--r--spec/helpers/notes_helper_spec.rb16
-rw-r--r--spec/helpers/page_layout_helper_spec.rb17
-rw-r--r--spec/helpers/preferences_helper_spec.rb4
-rw-r--r--spec/helpers/projects_helper_spec.rb72
-rw-r--r--spec/helpers/rss_helper_spec.rb8
-rw-r--r--spec/helpers/storage_helper_spec.rb24
-rw-r--r--spec/helpers/time_helper_spec.rb10
-rw-r--r--spec/initializers/6_validations_spec.rb43
-rw-r--r--spec/initializers/artifacts_direct_upload_support_spec.rb71
-rw-r--r--spec/initializers/direct_upload_support_spec.rb90
-rw-r--r--spec/initializers/fog_google_https_private_urls_spec.rb12
-rw-r--r--spec/initializers/grape_route_helpers_fix_spec.rb14
-rw-r--r--spec/initializers/secret_token_spec.rb2
-rw-r--r--spec/javascripts/.eslintrc33
-rw-r--r--spec/javascripts/.eslintrc.yml38
-rw-r--r--spec/javascripts/activities_spec.js2
-rw-r--r--spec/javascripts/api_spec.js48
-rw-r--r--spec/javascripts/awards_handler_spec.js106
-rw-r--r--spec/javascripts/badges/components/badge_list_spec.js2
-rw-r--r--spec/javascripts/badges/components/badge_settings_spec.js4
-rw-r--r--spec/javascripts/behaviors/copy_as_gfm_spec.js55
-rw-r--r--spec/javascripts/behaviors/quick_submit_spec.js81
-rw-r--r--spec/javascripts/behaviors/secret_values_spec.js2
-rw-r--r--spec/javascripts/blob/3d_viewer/mesh_object_spec.js4
-rw-r--r--spec/javascripts/blob/balsamiq/balsamiq_viewer_integration_spec.js2
-rw-r--r--spec/javascripts/blob/balsamiq/balsamiq_viewer_spec.js6
-rw-r--r--spec/javascripts/blob/notebook/index_spec.js9
-rw-r--r--spec/javascripts/blob/pdf/index_spec.js2
-rw-r--r--spec/javascripts/blob/sketch/index_spec.js2
-rw-r--r--spec/javascripts/blob/viewer/index_spec.js4
-rw-r--r--spec/javascripts/boards/board_blank_state_spec.js1
-rw-r--r--spec/javascripts/boards/board_card_spec.js3
-rw-r--r--spec/javascripts/boards/board_list_spec.js7
-rw-r--r--spec/javascripts/boards/board_new_issue_spec.js1
-rw-r--r--spec/javascripts/boards/boards_store_spec.js5
-rw-r--r--spec/javascripts/boards/issue_card_spec.js48
-rw-r--r--spec/javascripts/boards/issue_spec.js3
-rw-r--r--spec/javascripts/boards/list_spec.js3
-rw-r--r--spec/javascripts/boards/modal_store_spec.js2
-rw-r--r--spec/javascripts/bootstrap_jquery_spec.js2
-rw-r--r--spec/javascripts/bootstrap_linked_tabs_spec.js2
-rw-r--r--spec/javascripts/clusters/clusters_bundle_spec.js32
-rw-r--r--spec/javascripts/clusters/components/application_row_spec.js22
-rw-r--r--spec/javascripts/clusters/components/applications_spec.js94
-rw-r--r--spec/javascripts/clusters/services/mock_data.js35
-rw-r--r--spec/javascripts/clusters/stores/clusters_store_spec.js18
-rw-r--r--spec/javascripts/commit/commit_pipeline_status_component_spec.js5
-rw-r--r--spec/javascripts/commit/pipelines/pipelines_spec.js2
-rw-r--r--spec/javascripts/commits_spec.js2
-rw-r--r--spec/javascripts/create_merge_request_dropdown_spec.js67
-rw-r--r--spec/javascripts/datetime_utility_spec.js23
-rw-r--r--spec/javascripts/deploy_keys/components/key_spec.js2
-rw-r--r--spec/javascripts/diffs/components/app_spec.js1
-rw-r--r--spec/javascripts/diffs/components/changed_files_dropdown_spec.js1
-rw-r--r--spec/javascripts/diffs/components/changed_files_spec.js105
-rw-r--r--spec/javascripts/diffs/components/compare_versions_dropdown_spec.js1
-rw-r--r--spec/javascripts/diffs/components/compare_versions_spec.js1
-rw-r--r--spec/javascripts/diffs/components/diff_content_spec.js95
-rw-r--r--spec/javascripts/diffs/components/diff_discussions_spec.js24
-rw-r--r--spec/javascripts/diffs/components/diff_file_header_spec.js439
-rw-r--r--spec/javascripts/diffs/components/diff_file_spec.js88
-rw-r--r--spec/javascripts/diffs/components/diff_gutter_avatars_spec.js115
-rw-r--r--spec/javascripts/diffs/components/diff_line_gutter_content_spec.js108
-rw-r--r--spec/javascripts/diffs/components/diff_line_note_form_spec.js85
-rw-r--r--spec/javascripts/diffs/components/edit_button_spec.js1
-rw-r--r--spec/javascripts/diffs/components/hidden_files_warning_spec.js1
-rw-r--r--spec/javascripts/diffs/components/inline_diff_view_spec.js47
-rw-r--r--spec/javascripts/diffs/components/no_changes_spec.js1
-rw-r--r--spec/javascripts/diffs/components/parallel_diff_view_spec.js29
-rw-r--r--spec/javascripts/diffs/mock_data/diff_discussions.js496
-rw-r--r--spec/javascripts/diffs/mock_data/diff_file.js220
-rw-r--r--spec/javascripts/diffs/store/actions_spec.js238
-rw-r--r--spec/javascripts/diffs/store/getters_spec.js187
-rw-r--r--spec/javascripts/diffs/store/mutations_spec.js134
-rw-r--r--spec/javascripts/diffs/store/utils_spec.js210
-rw-r--r--spec/javascripts/environments/environment_rollback_spec.js4
-rw-r--r--spec/javascripts/environments/environment_stop_spec.js12
-rw-r--r--spec/javascripts/environments/environments_app_spec.js2
-rw-r--r--spec/javascripts/environments/folder/environments_folder_view_spec.js2
-rw-r--r--spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js8
-rw-r--r--spec/javascripts/filtered_search/filtered_search_token_keys_spec.js11
-rw-r--r--spec/javascripts/filtered_search/recent_searches_root_spec.js3
-rw-r--r--spec/javascripts/fixtures/commit.rb33
-rw-r--r--spec/javascripts/fixtures/event_filter.html.haml2
-rw-r--r--spec/javascripts/fixtures/groups.rb2
-rw-r--r--spec/javascripts/fixtures/images/green_box.pngbin0 -> 1306 bytes
-rw-r--r--spec/javascripts/fixtures/images/red_box.pngbin0 -> 1305 bytes
-rw-r--r--spec/javascripts/fixtures/issue_sidebar_label.html.haml2
-rw-r--r--spec/javascripts/fixtures/linked_tabs.html.haml10
-rw-r--r--spec/javascripts/fixtures/merge_requests.rb7
-rw-r--r--spec/javascripts/fixtures/projects.rb2
-rw-r--r--spec/javascripts/fixtures/snippet.rb1
-rw-r--r--spec/javascripts/frequent_items/components/app_spec.js251
-rw-r--r--spec/javascripts/frequent_items/components/frequent_items_list_item_spec.js75
-rw-r--r--spec/javascripts/frequent_items/components/frequent_items_list_spec.js84
-rw-r--r--spec/javascripts/frequent_items/components/frequent_items_search_input_spec.js77
-rw-r--r--spec/javascripts/frequent_items/mock_data.js168
-rw-r--r--spec/javascripts/frequent_items/store/actions_spec.js225
-rw-r--r--spec/javascripts/frequent_items/store/getters_spec.js24
-rw-r--r--spec/javascripts/frequent_items/store/mutations_spec.js117
-rw-r--r--spec/javascripts/frequent_items/utils_spec.js89
-rw-r--r--spec/javascripts/gl_dropdown_spec.js14
-rw-r--r--spec/javascripts/gl_field_errors_spec.js8
-rw-r--r--spec/javascripts/groups/components/app_spec.js6
-rw-r--r--spec/javascripts/groups/components/group_item_spec.js2
-rw-r--r--spec/javascripts/helpers/index.js3
-rw-r--r--spec/javascripts/helpers/init_vue_mr_page_helper.js46
-rw-r--r--spec/javascripts/helpers/user_mock_data_helper.js2
-rw-r--r--spec/javascripts/helpers/vue_mount_component_helper.js30
-rw-r--r--spec/javascripts/helpers/vue_resource_helper.js1
-rw-r--r--spec/javascripts/helpers/wait_for_promises.js1
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/form_spec.js13
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/list_item_spec.js17
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/list_spec.js5
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/message_field_spec.js1
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js15
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js2
-rw-r--r--spec/javascripts/ide/components/error_message_spec.js106
-rw-r--r--spec/javascripts/ide/components/external_link_spec.js35
-rw-r--r--spec/javascripts/ide/components/ide_file_buttons_spec.js61
-rw-r--r--spec/javascripts/ide/components/ide_spec.js14
-rw-r--r--spec/javascripts/ide/components/jobs/detail/description_spec.js28
-rw-r--r--spec/javascripts/ide/components/jobs/detail/scroll_button_spec.js59
-rw-r--r--spec/javascripts/ide/components/jobs/detail_spec.js185
-rw-r--r--spec/javascripts/ide/components/jobs/item_spec.js39
-rw-r--r--spec/javascripts/ide/components/jobs/list_spec.js67
-rw-r--r--spec/javascripts/ide/components/jobs/stage_spec.js95
-rw-r--r--spec/javascripts/ide/components/merge_requests/dropdown_spec.js47
-rw-r--r--spec/javascripts/ide/components/merge_requests/info_spec.js51
-rw-r--r--spec/javascripts/ide/components/merge_requests/item_spec.js61
-rw-r--r--spec/javascripts/ide/components/merge_requests/list_spec.js126
-rw-r--r--spec/javascripts/ide/components/panes/right_spec.js72
-rw-r--r--spec/javascripts/ide/components/pipelines/list_spec.js120
-rw-r--r--spec/javascripts/ide/components/repo_commit_section_spec.js50
-rw-r--r--spec/javascripts/ide/components/repo_editor_spec.js17
-rw-r--r--spec/javascripts/ide/components/repo_tab_spec.js26
-rw-r--r--spec/javascripts/ide/helpers.js30
-rw-r--r--spec/javascripts/ide/lib/common/model_manager_spec.js10
-rw-r--r--spec/javascripts/ide/lib/common/model_spec.js18
-rw-r--r--spec/javascripts/ide/lib/decorations/controller_spec.js18
-rw-r--r--spec/javascripts/ide/lib/diff/controller_spec.js27
-rw-r--r--spec/javascripts/ide/lib/editor_spec.js44
-rw-r--r--spec/javascripts/ide/mock_data.js117
-rw-r--r--spec/javascripts/ide/monaco_loader_spec.js15
-rw-r--r--spec/javascripts/ide/stores/actions/file_spec.js280
-rw-r--r--spec/javascripts/ide/stores/actions/merge_request_spec.js269
-rw-r--r--spec/javascripts/ide/stores/actions/project_spec.js197
-rw-r--r--spec/javascripts/ide/stores/actions/tree_spec.js234
-rw-r--r--spec/javascripts/ide/stores/actions_spec.js14
-rw-r--r--spec/javascripts/ide/stores/getters_spec.js30
-rw-r--r--spec/javascripts/ide/stores/modules/commit/actions_spec.js99
-rw-r--r--spec/javascripts/ide/stores/modules/commit/getters_spec.js92
-rw-r--r--spec/javascripts/ide/stores/modules/merge_requests/actions_spec.js240
-rw-r--r--spec/javascripts/ide/stores/modules/merge_requests/mutations_spec.js58
-rw-r--r--spec/javascripts/ide/stores/modules/pipelines/actions_spec.js426
-rw-r--r--spec/javascripts/ide/stores/modules/pipelines/getters_spec.js31
-rw-r--r--spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js211
-rw-r--r--spec/javascripts/ide/stores/mutations/file_spec.js47
-rw-r--r--spec/javascripts/ide/stores/mutations_spec.js8
-rw-r--r--spec/javascripts/ide/stores/utils_spec.js110
-rw-r--r--spec/javascripts/importer_status_spec.js20
-rw-r--r--spec/javascripts/integrations/integration_settings_form_spec.js23
-rw-r--r--spec/javascripts/issuable_time_tracker_spec.js2
-rw-r--r--spec/javascripts/issue_show/components/app_spec.js4
-rw-r--r--spec/javascripts/issue_spec.js2
-rw-r--r--spec/javascripts/job_spec.js28
-rw-r--r--spec/javascripts/jobs/header_spec.js7
-rw-r--r--spec/javascripts/jobs/mock_data.js4
-rw-r--r--spec/javascripts/labels_select_spec.js4
-rw-r--r--spec/javascripts/lib/utils/common_utils_spec.js121
-rw-r--r--spec/javascripts/lib/utils/mock_data.js5
-rw-r--r--spec/javascripts/lib/utils/text_utility_spec.js16
-rw-r--r--spec/javascripts/lib/utils/url_utility_spec.js29
-rw-r--r--spec/javascripts/line_highlighter_spec.js2
-rw-r--r--spec/javascripts/matchers.js26
-rw-r--r--spec/javascripts/merge_request_notes_spec.js108
-rw-r--r--spec/javascripts/merge_request_spec.js25
-rw-r--r--spec/javascripts/merge_request_tabs_spec.js566
-rw-r--r--spec/javascripts/mini_pipeline_graph_dropdown_spec.js2
-rw-r--r--spec/javascripts/monitoring/dashboard_spec.js45
-rw-r--r--spec/javascripts/monitoring/graph_spec.js10
-rw-r--r--spec/javascripts/monitoring/mock_data.js43
-rw-r--r--spec/javascripts/namespace_select_spec.js4
-rw-r--r--spec/javascripts/new_branch_spec.js2
-rw-r--r--spec/javascripts/notebook/cells/markdown_spec.js1
-rw-r--r--spec/javascripts/notes/components/comment_form_spec.js96
-rw-r--r--spec/javascripts/notes/components/diff_file_header_spec.js93
-rw-r--r--spec/javascripts/notes/components/diff_with_note_spec.js20
-rw-r--r--spec/javascripts/notes/components/discussion_counter_spec.js58
-rw-r--r--spec/javascripts/notes/components/note_actions_spec.js16
-rw-r--r--spec/javascripts/notes/components/note_app_spec.js72
-rw-r--r--spec/javascripts/notes/components/note_awards_list_spec.js8
-rw-r--r--spec/javascripts/notes/components/note_body_spec.js7
-rw-r--r--spec/javascripts/notes/components/note_form_spec.js28
-rw-r--r--spec/javascripts/notes/components/note_header_spec.js30
-rw-r--r--spec/javascripts/notes/components/note_signed_out_widget_spec.js20
-rw-r--r--spec/javascripts/notes/components/noteable_discussion_spec.js92
-rw-r--r--spec/javascripts/notes/components/noteable_note_spec.js16
-rw-r--r--spec/javascripts/notes/mock_data.js593
-rw-r--r--spec/javascripts/notes/stores/actions_spec.js66
-rw-r--r--spec/javascripts/notes/stores/collapse_utils_spec.js46
-rw-r--r--spec/javascripts/notes/stores/getters_spec.js58
-rw-r--r--spec/javascripts/notes/stores/mutation_spec.js129
-rw-r--r--spec/javascripts/notes_spec.js157
-rw-r--r--spec/javascripts/pdf/index_spec.js2
-rw-r--r--spec/javascripts/pdf/page_spec.js2
-rw-r--r--spec/javascripts/pipelines/empty_state_spec.js2
-rw-r--r--spec/javascripts/pipelines/graph/job_component_spec.js30
-rw-r--r--spec/javascripts/pipelines/graph/mock_data.js18
-rw-r--r--spec/javascripts/pipelines/mock_data.js2
-rw-r--r--spec/javascripts/pipelines/pipelines_spec.js2
-rw-r--r--spec/javascripts/pipelines/pipelines_table_row_spec.js40
-rw-r--r--spec/javascripts/pipelines/pipelines_table_spec.js2
-rw-r--r--spec/javascripts/profile/account/components/update_username_spec.js3
-rw-r--r--spec/javascripts/profile/add_ssh_key_validation_spec.js69
-rw-r--r--spec/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown_spec.js103
-rw-r--r--spec/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown_spec.js92
-rw-r--r--spec/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown_spec.js88
-rw-r--r--spec/javascripts/projects/gke_cluster_dropdowns/helpers.js49
-rw-r--r--spec/javascripts/projects/gke_cluster_dropdowns/mock_data.js75
-rw-r--r--spec/javascripts/projects/gke_cluster_dropdowns/stores/actions_spec.js131
-rw-r--r--spec/javascripts/projects/gke_cluster_dropdowns/stores/getters_spec.js65
-rw-r--r--spec/javascripts/projects/gke_cluster_dropdowns/stores/mutations_spec.js87
-rw-r--r--spec/javascripts/projects_dropdown/components/app_spec.js349
-rw-r--r--spec/javascripts/projects_dropdown/components/projects_list_frequent_spec.js72
-rw-r--r--spec/javascripts/projects_dropdown/components/projects_list_item_spec.js77
-rw-r--r--spec/javascripts/projects_dropdown/components/projects_list_search_spec.js84
-rw-r--r--spec/javascripts/projects_dropdown/components/search_spec.js101
-rw-r--r--spec/javascripts/projects_dropdown/mock_data.js96
-rw-r--r--spec/javascripts/projects_dropdown/service/projects_service_spec.js179
-rw-r--r--spec/javascripts/projects_dropdown/store/projects_store_spec.js41
-rw-r--r--spec/javascripts/right_sidebar_spec.js2
-rw-r--r--spec/javascripts/search_autocomplete_spec.js2
-rw-r--r--spec/javascripts/settings_panels_spec.js4
-rw-r--r--spec/javascripts/shortcuts_issuable_spec.js77
-rw-r--r--spec/javascripts/shortcuts_spec.js19
-rw-r--r--spec/javascripts/sidebar/confidential_issue_sidebar_spec.js5
-rw-r--r--spec/javascripts/sidebar/sidebar_move_issue_spec.js4
-rw-r--r--spec/javascripts/signin_tabs_memoizer_spec.js15
-rw-r--r--spec/javascripts/smart_interval_spec.js201
-rw-r--r--spec/javascripts/syntax_highlight_spec.js2
-rw-r--r--spec/javascripts/test_bundle.js9
-rw-r--r--spec/javascripts/test_constants.js3
-rw-r--r--spec/javascripts/u2f/authenticate_spec.js84
-rw-r--r--spec/javascripts/u2f/mock_u2f_device.js3
-rw-r--r--spec/javascripts/vue_mr_widget/components/deployment_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js8
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js18
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js23
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js7
-rw-r--r--spec/javascripts/vue_mr_widget/mr_widget_options_spec.js13
-rw-r--r--spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js10
-rw-r--r--spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js70
-rw-r--r--spec/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js185
-rw-r--r--spec/javascripts/vue_shared/components/dropdown/dropdown_button_spec.js69
-rw-r--r--spec/javascripts/vue_shared/components/dropdown/dropdown_hidden_input_spec.js37
-rw-r--r--spec/javascripts/vue_shared/components/dropdown/dropdown_search_input_spec.js52
-rw-r--r--spec/javascripts/vue_shared/components/dropdown/mock_data.js11
-rw-r--r--spec/javascripts/vue_shared/components/expand_button_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/file_icon_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/gl_modal_spec.js41
-rw-r--r--spec/javascripts/vue_shared/components/icon_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/lib/utils/dom_utils_spec.js13
-rw-r--r--spec/javascripts/vue_shared/components/markdown/header_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js20
-rw-r--r--spec/javascripts/vue_shared/components/notes/system_note_spec.js14
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input_spec.js37
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/table_pagination_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/tabs/tab_spec.js32
-rw-r--r--spec/javascripts/vue_shared/components/tabs/tabs_spec.js68
-rw-r--r--spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js4
-rw-r--r--spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js2
-rw-r--r--spec/javascripts/zen_mode_spec.js59
-rw-r--r--spec/lib/backup/files_spec.rb4
-rw-r--r--spec/lib/backup/manager_spec.rb9
-rw-r--r--spec/lib/backup/repository_spec.rb58
-rw-r--r--spec/lib/banzai/filter/blockquote_fence_filter_spec.rb4
-rw-r--r--spec/lib/banzai/filter/emoji_filter_spec.rb9
-rw-r--r--spec/lib/banzai/filter/image_lazy_load_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/label_reference_filter_spec.rb10
-rw-r--r--spec/lib/banzai/filter/markdown_filter_spec.rb58
-rw-r--r--spec/lib/banzai/filter/merge_request_reference_filter_spec.rb7
-rw-r--r--spec/lib/banzai/filter/milestone_reference_filter_spec.rb10
-rw-r--r--spec/lib/banzai/filter/redactor_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/sanitization_filter_spec.rb12
-rw-r--r--spec/lib/banzai/filter/table_of_contents_filter_spec.rb9
-rw-r--r--spec/lib/extracts_path_spec.rb26
-rw-r--r--spec/lib/feature_spec.rb24
-rw-r--r--spec/lib/gitaly/server_spec.rb34
-rw-r--r--spec/lib/gitlab/auth/o_auth/user_spec.rb8
-rw-r--r--spec/lib/gitlab/auth/request_authenticator_spec.rb10
-rw-r--r--spec/lib/gitlab/auth/saml/auth_hash_spec.rb51
-rw-r--r--spec/lib/gitlab/auth/saml/user_spec.rb41
-rw-r--r--spec/lib/gitlab/auth/user_access_denied_reason_spec.rb3
-rw-r--r--spec/lib/gitlab/auth/user_auth_finders_spec.rb48
-rw-r--r--spec/lib/gitlab/background_migration/archive_legacy_traces_spec.rb59
-rw-r--r--spec/lib/gitlab/background_migration/delete_diff_files_spec.rb73
-rw-r--r--spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb12
-rw-r--r--spec/lib/gitlab/background_migration/fix_cross_project_label_links_spec.rb109
-rw-r--r--spec/lib/gitlab/background_migration/move_personal_snippet_files_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/schedule_diff_files_deletion_spec.rb43
-rw-r--r--spec/lib/gitlab/bare_repository_import/importer_spec.rb7
-rw-r--r--spec/lib/gitlab/bare_repository_import/repository_spec.rb6
-rw-r--r--spec/lib/gitlab/bitbucket_import/importer_spec.rb30
-rw-r--r--spec/lib/gitlab/bitbucket_import/project_creator_spec.rb4
-rw-r--r--spec/lib/gitlab/checks/change_access_spec.rb8
-rw-r--r--spec/lib/gitlab/checks/force_push_spec.rb18
-rw-r--r--spec/lib/gitlab/checks/lfs_integrity_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb24
-rw-r--r--spec/lib/gitlab/ci/config_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb67
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/pipeline/preloader_spec.rb47
-rw-r--r--spec/lib/gitlab/ci/status/build/play_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/stage/common_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/trace/http_io_spec.rb315
-rw-r--r--spec/lib/gitlab/ci/trace_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/variables/collection/item_spec.rb64
-rw-r--r--spec/lib/gitlab/ci/variables/collection_spec.rb12
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb12
-rw-r--r--spec/lib/gitlab/closing_issue_extractor_spec.rb4
-rw-r--r--spec/lib/gitlab/conflict/file_spec.rb2
-rw-r--r--spec/lib/gitlab/current_settings_spec.rb143
-rw-r--r--spec/lib/gitlab/cycle_analytics/permissions_spec.rb4
-rw-r--r--spec/lib/gitlab/cycle_analytics/usage_data_spec.rb19
-rw-r--r--spec/lib/gitlab/data_builder/note_spec.rb8
-rw-r--r--spec/lib/gitlab/database/count_spec.rb81
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb55
-rw-r--r--spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb2
-rw-r--r--spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb2
-rw-r--r--spec/lib/gitlab/database_spec.rb38
-rw-r--r--spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb9
-rw-r--r--spec/lib/gitlab/diff/file_spec.rb86
-rw-r--r--spec/lib/gitlab/exclusive_lease_helpers_spec.rb76
-rw-r--r--spec/lib/gitlab/favicon_spec.rb67
-rw-r--r--spec/lib/gitlab/file_finder_spec.rb32
-rw-r--r--spec/lib/gitlab/gfm/uploads_rewriter_spec.rb64
-rw-r--r--spec/lib/gitlab/git/blame_spec.rb10
-rw-r--r--spec/lib/gitlab/git/blob_spec.rb203
-rw-r--r--spec/lib/gitlab/git/branch_spec.rb4
-rw-r--r--spec/lib/gitlab/git/commit_spec.rb159
-rw-r--r--spec/lib/gitlab/git/committer_with_hooks_spec.rb216
-rw-r--r--spec/lib/gitlab/git/diff_spec.rb6
-rw-r--r--spec/lib/gitlab/git/gitlab_projects_spec.rb112
-rw-r--r--spec/lib/gitlab/git/hook_spec.rb32
-rw-r--r--spec/lib/gitlab/git/hooks_service_spec.rb8
-rw-r--r--spec/lib/gitlab/git/index_spec.rb7
-rw-r--r--spec/lib/gitlab/git/lfs_changes_spec.rb41
-rw-r--r--spec/lib/gitlab/git/pre_receive_error_spec.rb9
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb833
-rw-r--r--spec/lib/gitlab/git/rev_list_spec.rb14
-rw-r--r--spec/lib/gitlab/git/wiki_spec.rb20
-rw-r--r--spec/lib/gitlab/git_access_spec.rb102
-rw-r--r--spec/lib/gitlab/git_access_wiki_spec.rb4
-rw-r--r--spec/lib/gitlab/gitaly_client/commit_service_spec.rb4
-rw-r--r--spec/lib/gitlab/gitaly_client/operation_service_spec.rb45
-rw-r--r--spec/lib/gitlab/github_import/importer/issue_importer_spec.rb4
-rw-r--r--spec/lib/gitlab/github_import/importer/lfs_object_importer_spec.rb23
-rw-r--r--spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb94
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb97
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb8
-rw-r--r--spec/lib/gitlab/github_import/importer/repository_importer_spec.rb3
-rw-r--r--spec/lib/gitlab/github_import/sequential_importer_spec.rb1
-rw-r--r--spec/lib/gitlab/gitlab_import/importer_spec.rb4
-rw-r--r--spec/lib/gitlab/gitlab_import/project_creator_spec.rb4
-rw-r--r--spec/lib/gitlab/google_code_import/importer_spec.rb2
-rw-r--r--spec/lib/gitlab/google_code_import/project_creator_spec.rb4
-rw-r--r--spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb4
-rw-r--r--spec/lib/gitlab/gpg_spec.rb13
-rw-r--r--spec/lib/gitlab/grape_logging/loggers/queue_duration_logger_spec.rb35
-rw-r--r--spec/lib/gitlab/graphql/connections/keyset_connection_spec.rb112
-rw-r--r--spec/lib/gitlab/hashed_storage/migrator_spec.rb75
-rw-r--r--spec/lib/gitlab/health_checks/fs_shards_check_spec.rb200
-rw-r--r--spec/lib/gitlab/health_checks/gitaly_check_spec.rb7
-rw-r--r--spec/lib/gitlab/http_io_spec.rb318
-rw-r--r--spec/lib/gitlab/i18n/metadata_entry_spec.rb6
-rw-r--r--spec/lib/gitlab/i18n/po_linter_spec.rb163
-rw-r--r--spec/lib/gitlab/i18n/translation_entry_spec.rb6
-rw-r--r--spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_object_storage_spec.rb105
-rw-r--r--spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb1
-rw-r--r--spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb31
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml8
-rw-r--r--spec/lib/gitlab/import_export/attribute_cleaner_spec.rb29
-rw-r--r--spec/lib/gitlab/import_export/avatar_saver_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/fork_spec.rb6
-rw-r--r--spec/lib/gitlab/import_export/group_project_object_builder_spec.rb52
-rw-r--r--spec/lib/gitlab/import_export/importer_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/members_mapper_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/merge_request_parser_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/project.json2
-rw-r--r--spec/lib/gitlab/import_export/project.light.json10
-rw-r--r--spec/lib/gitlab/import_export/project.milestone-iid.json80
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb87
-rw-r--r--spec/lib/gitlab/import_export/project_tree_saver_spec.rb6
-rw-r--r--spec/lib/gitlab/import_export/relation_factory_spec.rb31
-rw-r--r--spec/lib/gitlab/import_export/repo_restorer_spec.rb12
-rw-r--r--spec/lib/gitlab/import_export/repo_saver_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml16
-rw-r--r--spec/lib/gitlab/import_export/saver_spec.rb43
-rw-r--r--spec/lib/gitlab/import_export/uploads_saver_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb2
-rw-r--r--spec/lib/gitlab/import_sources_spec.rb19
-rw-r--r--spec/lib/gitlab/kubernetes/helm/api_spec.rb8
-rw-r--r--spec/lib/gitlab/kubernetes/helm/install_command_spec.rb62
-rw-r--r--spec/lib/gitlab/kubernetes_spec.rb15
-rw-r--r--spec/lib/gitlab/legacy_github_import/project_creator_spec.rb45
-rw-r--r--spec/lib/gitlab/metrics/samplers/influx_sampler_spec.rb4
-rw-r--r--spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb6
-rw-r--r--spec/lib/gitlab/metrics/web_transaction_spec.rb11
-rw-r--r--spec/lib/gitlab/middleware/go_spec.rb2
-rw-r--r--spec/lib/gitlab/middleware/multipart_spec.rb86
-rw-r--r--spec/lib/gitlab/middleware/read_only_spec.rb77
-rw-r--r--spec/lib/gitlab/path_regex_spec.rb12
-rw-r--r--spec/lib/gitlab/profiler_spec.rb45
-rw-r--r--spec/lib/gitlab/project_authorizations_spec.rb6
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb84
-rw-r--r--spec/lib/gitlab/quick_actions/extractor_spec.rb16
-rw-r--r--spec/lib/gitlab/repository_cache_adapter_spec.rb12
-rw-r--r--spec/lib/gitlab/sanitizers/svg_spec.rb4
-rw-r--r--spec/lib/gitlab/search/query_spec.rb39
-rw-r--r--spec/lib/gitlab/shard_health_cache_spec.rb52
-rw-r--r--spec/lib/gitlab/shell_spec.rb115
-rw-r--r--spec/lib/gitlab/slash_commands/issue_move_spec.rb2
-rw-r--r--spec/lib/gitlab/slash_commands/issue_new_spec.rb2
-rw-r--r--spec/lib/gitlab/slash_commands/issue_search_spec.rb2
-rw-r--r--spec/lib/gitlab/slash_commands/issue_show_spec.rb2
-rw-r--r--spec/lib/gitlab/sql/cte_spec.rb42
-rw-r--r--spec/lib/gitlab/sql/glob_spec.rb3
-rw-r--r--spec/lib/gitlab/themes_spec.rb6
-rw-r--r--spec/lib/gitlab/url_blocker_spec.rb56
-rw-r--r--spec/lib/gitlab/url_builder_spec.rb25
-rw-r--r--spec/lib/gitlab/url_sanitizer_spec.rb1
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb33
-rw-r--r--spec/lib/gitlab/user_access_spec.rb42
-rw-r--r--spec/lib/gitlab/utils/override_spec.rb170
-rw-r--r--spec/lib/gitlab/verify/job_artifacts_spec.rb29
-rw-r--r--spec/lib/gitlab/verify/lfs_objects_spec.rb25
-rw-r--r--spec/lib/gitlab/verify/uploads_spec.rb56
-rw-r--r--spec/lib/gitlab/wiki_file_finder_spec.rb20
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb152
-rw-r--r--spec/lib/gitlab_spec.rb60
-rw-r--r--spec/lib/google_api/cloud_platform/client_spec.rb24
-rw-r--r--spec/lib/mattermost/command_spec.rb10
-rw-r--r--spec/lib/mattermost/session_spec.rb26
-rw-r--r--spec/lib/mattermost/team_spec.rb48
-rw-r--r--spec/lib/microsoft_teams/notifier_spec.rb2
-rw-r--r--spec/lib/object_storage/direct_upload_spec.rb168
-rw-r--r--spec/lib/omni_auth/strategies/jwt_spec.rb6
-rw-r--r--spec/mailers/notify_spec.rb117
-rw-r--r--spec/mailers/previews/notify_preview.rb170
-rw-r--r--spec/migrations/active_record/schema_spec.rb6
-rw-r--r--spec/migrations/change_default_value_for_dsa_key_restriction_spec.rb33
-rw-r--r--spec/migrations/cleanup_stages_position_migration_spec.rb67
-rw-r--r--spec/migrations/enqueue_delete_diff_files_workers_spec.rb17
-rw-r--r--spec/migrations/fill_file_store_spec.rb43
-rw-r--r--spec/migrations/issues_moved_to_id_foreign_key_spec.rb2
-rw-r--r--spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb6
-rw-r--r--spec/migrations/migrate_object_storage_upload_sidekiq_queue_spec.rb33
-rw-r--r--spec/migrations/migrate_process_commit_worker_jobs_spec.rb6
-rw-r--r--spec/migrations/migrate_remaining_mr_metrics_populating_background_migration_spec.rb36
-rw-r--r--spec/migrations/remove_soft_removed_objects_spec.rb36
-rw-r--r--spec/migrations/schedule_to_archive_legacy_traces_spec.rb45
-rw-r--r--spec/migrations/turn_nested_groups_into_regular_groups_for_mysql_spec.rb8
-rw-r--r--spec/models/application_setting/term_spec.rb37
-rw-r--r--spec/models/application_setting_spec.rb181
-rw-r--r--spec/models/ci/build_metadata_spec.rb2
-rw-r--r--spec/models/ci/build_runner_session_spec.rb36
-rw-r--r--spec/models/ci/build_spec.rb278
-rw-r--r--spec/models/ci/build_trace_chunk_spec.rb452
-rw-r--r--spec/models/ci/build_trace_chunks/database_spec.rb105
-rw-r--r--spec/models/ci/build_trace_chunks/fog_spec.rb146
-rw-r--r--spec/models/ci/build_trace_chunks/redis_spec.rb132
-rw-r--r--spec/models/ci/group_spec.rb51
-rw-r--r--spec/models/ci/job_artifact_spec.rb8
-rw-r--r--spec/models/ci/pipeline_spec.rb135
-rw-r--r--spec/models/ci/runner_spec.rb232
-rw-r--r--spec/models/ci/stage_spec.rb142
-rw-r--r--spec/models/clusters/applications/ingress_spec.rb1
-rw-r--r--spec/models/clusters/applications/jupyter_spec.rb60
-rw-r--r--spec/models/clusters/applications/prometheus_spec.rb1
-rw-r--r--spec/models/clusters/applications/runner_spec.rb1
-rw-r--r--spec/models/clusters/cluster_spec.rb3
-rw-r--r--spec/models/commit_spec.rb31
-rw-r--r--spec/models/concerns/batch_destroy_dependent_associations_spec.rb60
-rw-r--r--spec/models/concerns/cache_markdown_field_spec.rb26
-rw-r--r--spec/models/concerns/cacheable_attributes_spec.rb133
-rw-r--r--spec/models/concerns/discussion_on_diff_spec.rb2
-rw-r--r--spec/models/concerns/has_variable_spec.rb4
-rw-r--r--spec/models/concerns/issuable_spec.rb13
-rw-r--r--spec/models/concerns/mentionable_spec.rb6
-rw-r--r--spec/models/concerns/protected_ref_access_spec.rb10
-rw-r--r--spec/models/concerns/reactive_caching_spec.rb12
-rw-r--r--spec/models/concerns/resolvable_discussion_spec.rb15
-rw-r--r--spec/models/concerns/resolvable_note_spec.rb8
-rw-r--r--spec/models/concerns/routable_spec.rb2
-rw-r--r--spec/models/concerns/sortable_spec.rb18
-rw-r--r--spec/models/concerns/token_authenticatable_spec.rb2
-rw-r--r--spec/models/deployment_spec.rb1
-rw-r--r--spec/models/diff_note_spec.rb62
-rw-r--r--spec/models/environment_spec.rb2
-rw-r--r--spec/models/event_spec.rb13
-rw-r--r--spec/models/group_spec.rb78
-rw-r--r--spec/models/hooks/system_hook_spec.rb8
-rw-r--r--spec/models/hooks/web_hook_log_spec.rb18
-rw-r--r--spec/models/import_export_upload_spec.rb25
-rw-r--r--spec/models/internal_id_spec.rb37
-rw-r--r--spec/models/issue_spec.rb3
-rw-r--r--spec/models/lfs_file_lock_spec.rb12
-rw-r--r--spec/models/member_spec.rb86
-rw-r--r--spec/models/members/group_member_spec.rb2
-rw-r--r--spec/models/members/project_member_spec.rb10
-rw-r--r--spec/models/merge_request_diff_spec.rb46
-rw-r--r--spec/models/merge_request_spec.rb319
-rw-r--r--spec/models/milestone_spec.rb2
-rw-r--r--spec/models/namespace_spec.rb27
-rw-r--r--spec/models/note_diff_file_spec.rb11
-rw-r--r--spec/models/note_spec.rb16
-rw-r--r--spec/models/notification_recipient_spec.rb44
-rw-r--r--spec/models/notification_setting_spec.rb2
-rw-r--r--spec/models/project_authorization_spec.rb6
-rw-r--r--spec/models/project_auto_devops_spec.rb121
-rw-r--r--spec/models/project_feature_spec.rb2
-rw-r--r--spec/models/project_services/bamboo_service_spec.rb22
-rw-r--r--spec/models/project_services/jira_service_spec.rb19
-rw-r--r--spec/models/project_services/kubernetes_service_spec.rb12
-rw-r--r--spec/models/project_services/mattermost_slash_commands_service_spec.rb10
-rw-r--r--spec/models/project_services/microsoft_teams_service_spec.rb7
-rw-r--r--spec/models/project_spec.rb182
-rw-r--r--spec/models/project_team_spec.rb112
-rw-r--r--spec/models/project_wiki_spec.rb6
-rw-r--r--spec/models/protected_branch/merge_access_level_spec.rb2
-rw-r--r--spec/models/protected_branch/push_access_level_spec.rb2
-rw-r--r--spec/models/remote_mirror_spec.rb21
-rw-r--r--spec/models/repository_spec.rb365
-rw-r--r--spec/models/route_spec.rb14
-rw-r--r--spec/models/service_spec.rb4
-rw-r--r--spec/models/term_agreement_spec.rb9
-rw-r--r--spec/models/timelog_spec.rb3
-rw-r--r--spec/models/user_spec.rb160
-rw-r--r--spec/policies/ci/build_policy_spec.rb12
-rw-r--r--spec/policies/ci/pipeline_policy_spec.rb2
-rw-r--r--spec/policies/ci/pipeline_schedule_policy_spec.rb8
-rw-r--r--spec/policies/ci/trigger_policy_spec.rb12
-rw-r--r--spec/policies/clusters/cluster_policy_spec.rb4
-rw-r--r--spec/policies/deploy_key_policy_spec.rb2
-rw-r--r--spec/policies/deploy_token_policy_spec.rb12
-rw-r--r--spec/policies/environment_policy_spec.rb106
-rw-r--r--spec/policies/global_policy_spec.rb4
-rw-r--r--spec/policies/group_policy_spec.rb48
-rw-r--r--spec/policies/project_policy_spec.rb70
-rw-r--r--spec/policies/protected_branch_policy_spec.rb4
-rw-r--r--spec/presenters/ci/build_presenter_spec.rb4
-rw-r--r--spec/presenters/merge_request_presenter_spec.rb6
-rw-r--r--spec/presenters/project_presenter_spec.rb8
-rw-r--r--spec/requests/api/access_requests_spec.rb40
-rw-r--r--spec/requests/api/avatar_spec.rb106
-rw-r--r--spec/requests/api/award_emoji_spec.rb2
-rw-r--r--spec/requests/api/badges_spec.rb54
-rw-r--r--spec/requests/api/boards_spec.rb1
-rw-r--r--spec/requests/api/branches_spec.rb45
-rw-r--r--spec/requests/api/commit_statuses_spec.rb2
-rw-r--r--spec/requests/api/commits_spec.rb53
-rw-r--r--spec/requests/api/deploy_keys_spec.rb40
-rw-r--r--spec/requests/api/deployments_spec.rb2
-rw-r--r--spec/requests/api/environments_spec.rb6
-rw-r--r--spec/requests/api/events_spec.rb4
-rw-r--r--spec/requests/api/files_spec.rb85
-rw-r--r--spec/requests/api/graphql/project/merge_request_spec.rb94
-rw-r--r--spec/requests/api/graphql/project_query_spec.rb56
-rw-r--r--spec/requests/api/group_variables_spec.rb10
-rw-r--r--spec/requests/api/groups_spec.rb84
-rw-r--r--spec/requests/api/helpers_spec.rb4
-rw-r--r--spec/requests/api/internal_spec.rb30
-rw-r--r--spec/requests/api/issues_spec.rb28
-rw-r--r--spec/requests/api/jobs_spec.rb30
-rw-r--r--spec/requests/api/labels_spec.rb2
-rw-r--r--spec/requests/api/members_spec.rb84
-rw-r--r--spec/requests/api/merge_request_diffs_spec.rb2
-rw-r--r--spec/requests/api/merge_requests_spec.rb305
-rw-r--r--spec/requests/api/namespaces_spec.rb12
-rw-r--r--spec/requests/api/notes_spec.rb6
-rw-r--r--spec/requests/api/pages_domains_spec.rb28
-rw-r--r--spec/requests/api/pipeline_schedules_spec.rb24
-rw-r--r--spec/requests/api/pipelines_spec.rb62
-rw-r--r--spec/requests/api/project_export_spec.rb59
-rw-r--r--spec/requests/api/project_hooks_spec.rb4
-rw-r--r--spec/requests/api/project_import_spec.rb8
-rw-r--r--spec/requests/api/project_snippets_spec.rb2
-rw-r--r--spec/requests/api/projects_spec.rb70
-rw-r--r--spec/requests/api/protected_branches_spec.rb34
-rw-r--r--spec/requests/api/repositories_spec.rb36
-rw-r--r--spec/requests/api/runner_spec.rb75
-rw-r--r--spec/requests/api/runners_spec.rb173
-rw-r--r--spec/requests/api/search_spec.rb24
-rw-r--r--spec/requests/api/settings_spec.rb30
-rw-r--r--spec/requests/api/snippets_spec.rb5
-rw-r--r--spec/requests/api/tags_spec.rb25
-rw-r--r--spec/requests/api/triggers_spec.rb2
-rw-r--r--spec/requests/api/users_spec.rb75
-rw-r--r--spec/requests/api/v3/award_emoji_spec.rb297
-rw-r--r--spec/requests/api/v3/boards_spec.rb114
-rw-r--r--spec/requests/api/v3/branches_spec.rb120
-rw-r--r--spec/requests/api/v3/broadcast_messages_spec.rb32
-rw-r--r--spec/requests/api/v3/builds_spec.rb550
-rw-r--r--spec/requests/api/v3/commits_spec.rb603
-rw-r--r--spec/requests/api/v3/deploy_keys_spec.rb179
-rw-r--r--spec/requests/api/v3/deployments_spec.rb69
-rw-r--r--spec/requests/api/v3/environments_spec.rb163
-rw-r--r--spec/requests/api/v3/files_spec.rb283
-rw-r--r--spec/requests/api/v3/groups_spec.rb566
-rw-r--r--spec/requests/api/v3/issues_spec.rb1298
-rw-r--r--spec/requests/api/v3/labels_spec.rb169
-rw-r--r--spec/requests/api/v3/members_spec.rb350
-rw-r--r--spec/requests/api/v3/merge_request_diffs_spec.rb48
-rw-r--r--spec/requests/api/v3/merge_requests_spec.rb749
-rw-r--r--spec/requests/api/v3/milestones_spec.rb238
-rw-r--r--spec/requests/api/v3/notes_spec.rb431
-rw-r--r--spec/requests/api/v3/pipelines_spec.rb201
-rw-r--r--spec/requests/api/v3/project_hooks_spec.rb219
-rw-r--r--spec/requests/api/v3/project_snippets_spec.rb226
-rw-r--r--spec/requests/api/v3/projects_spec.rb1495
-rw-r--r--spec/requests/api/v3/repositories_spec.rb366
-rw-r--r--spec/requests/api/v3/runners_spec.rb152
-rw-r--r--spec/requests/api/v3/services_spec.rb26
-rw-r--r--spec/requests/api/v3/settings_spec.rb63
-rw-r--r--spec/requests/api/v3/snippets_spec.rb186
-rw-r--r--spec/requests/api/v3/system_hooks_spec.rb56
-rw-r--r--spec/requests/api/v3/tags_spec.rb88
-rw-r--r--spec/requests/api/v3/templates_spec.rb201
-rw-r--r--spec/requests/api/v3/todos_spec.rb77
-rw-r--r--spec/requests/api/v3/triggers_spec.rb235
-rw-r--r--spec/requests/api/v3/users_spec.rb362
-rw-r--r--spec/requests/api/variables_spec.rb2
-rw-r--r--spec/requests/api/version_spec.rb2
-rw-r--r--spec/requests/api/wikis_spec.rb62
-rw-r--r--spec/requests/git_http_spec.rb27
-rw-r--r--spec/requests/lfs_http_spec.rb17
-rw-r--r--spec/requests/lfs_locks_api_spec.rb6
-rw-r--r--spec/requests/oauth_tokens_spec.rb55
-rw-r--r--spec/requests/openid_connect_spec.rb110
-rw-r--r--spec/requests/rack_attack_global_spec.rb2
-rw-r--r--spec/routing/api_routing_spec.rb31
-rw-r--r--spec/routing/project_routing_spec.rb20
-rw-r--r--spec/routing/routing_spec.rb10
-rw-r--r--spec/rubocop/cop/gitlab/finder_with_find_by_spec.rb56
-rw-r--r--spec/rubocop/cop/line_break_around_conditional_block_spec.rb12
-rw-r--r--spec/rubocop/cop/migration/update_large_table_spec.rb20
-rw-r--r--spec/serializers/blob_entity_spec.rb20
-rw-r--r--spec/serializers/build_serializer_spec.rb4
-rw-r--r--spec/serializers/deploy_key_entity_spec.rb4
-rw-r--r--spec/serializers/diff_file_entity_spec.rb62
-rw-r--r--spec/serializers/diffs_entity_spec.rb28
-rw-r--r--spec/serializers/discussion_entity_spec.rb53
-rw-r--r--spec/serializers/environment_entity_spec.rb3
-rw-r--r--spec/serializers/environment_serializer_spec.rb10
-rw-r--r--spec/serializers/group_child_entity_spec.rb2
-rw-r--r--spec/serializers/job_entity_spec.rb25
-rw-r--r--spec/serializers/merge_request_diff_entity_spec.rb24
-rw-r--r--spec/serializers/merge_request_user_entity_spec.rb19
-rw-r--r--spec/serializers/pipeline_serializer_spec.rb15
-rw-r--r--spec/serializers/runner_entity_spec.rb4
-rw-r--r--spec/serializers/status_entity_spec.rb12
-rw-r--r--spec/services/application_settings/update_service_spec.rb88
-rw-r--r--spec/services/auth/container_registry_authentication_service_spec.rb17
-rw-r--r--spec/services/check_gcp_project_billing_service_spec.rb32
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb8
-rw-r--r--spec/services/ci/register_job_service_spec.rb59
-rw-r--r--spec/services/ci/retry_build_service_spec.rb16
-rw-r--r--spec/services/ci/retry_pipeline_service_spec.rb8
-rw-r--r--spec/services/ci/stop_environments_service_spec.rb2
-rw-r--r--spec/services/ci/update_build_queue_service_spec.rb17
-rw-r--r--spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb17
-rw-r--r--spec/services/clusters/applications/schedule_installation_service_spec.rb33
-rw-r--r--spec/services/discussions/resolve_service_spec.rb2
-rw-r--r--spec/services/files/create_service_spec.rb2
-rw-r--r--spec/services/files/delete_service_spec.rb2
-rw-r--r--spec/services/files/multi_service_spec.rb2
-rw-r--r--spec/services/files/update_service_spec.rb14
-rw-r--r--spec/services/git_push_service_spec.rb12
-rw-r--r--spec/services/git_tag_push_service_spec.rb4
-rw-r--r--spec/services/groups/update_service_spec.rb8
-rw-r--r--spec/services/import_export_clean_up_service_spec.rb19
-rw-r--r--spec/services/issues/close_service_spec.rb2
-rw-r--r--spec/services/issues/create_service_spec.rb16
-rw-r--r--spec/services/issues/move_service_spec.rb26
-rw-r--r--spec/services/issues/reopen_service_spec.rb2
-rw-r--r--spec/services/issues/resolve_discussions_spec.rb4
-rw-r--r--spec/services/issues/update_service_spec.rb16
-rw-r--r--spec/services/keys/last_used_service_spec.rb4
-rw-r--r--spec/services/labels/find_or_create_service_spec.rb20
-rw-r--r--spec/services/lfs/unlock_file_service_spec.rb10
-rw-r--r--spec/services/members/approve_access_request_service_spec.rb6
-rw-r--r--spec/services/members/create_service_spec.rb2
-rw-r--r--spec/services/members/destroy_service_spec.rb10
-rw-r--r--spec/services/members/update_service_spec.rb6
-rw-r--r--spec/services/merge_requests/close_service_spec.rb2
-rw-r--r--spec/services/merge_requests/conflicts/list_service_spec.rb24
-rw-r--r--spec/services/merge_requests/conflicts/resolve_service_spec.rb11
-rw-r--r--spec/services/merge_requests/create_from_issue_service_spec.rb11
-rw-r--r--spec/services/merge_requests/create_service_spec.rb20
-rw-r--r--spec/services/merge_requests/delete_non_latest_diffs_service_spec.rb59
-rw-r--r--spec/services/merge_requests/ff_merge_service_spec.rb4
-rw-r--r--spec/services/merge_requests/merge_request_diff_cache_service_spec.rb39
-rw-r--r--spec/services/merge_requests/merge_service_spec.rb66
-rw-r--r--spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb26
-rw-r--r--spec/services/merge_requests/post_merge_service_spec.rb27
-rw-r--r--spec/services/merge_requests/rebase_service_spec.rb40
-rw-r--r--spec/services/merge_requests/reload_diffs_service_spec.rb64
-rw-r--r--spec/services/merge_requests/reopen_service_spec.rb2
-rw-r--r--spec/services/merge_requests/squash_service_spec.rb156
-rw-r--r--spec/services/merge_requests/update_service_spec.rb22
-rw-r--r--spec/services/milestones/close_service_spec.rb2
-rw-r--r--spec/services/milestones/create_service_spec.rb2
-rw-r--r--spec/services/milestones/destroy_service_spec.rb2
-rw-r--r--spec/services/milestones/promote_service_spec.rb2
-rw-r--r--spec/services/notes/create_service_spec.rb84
-rw-r--r--spec/services/notes/post_process_service_spec.rb2
-rw-r--r--spec/services/notes/quick_actions_service_spec.rb20
-rw-r--r--spec/services/notes/update_service_spec.rb2
-rw-r--r--spec/services/notification_recipient_service_spec.rb36
-rw-r--r--spec/services/notification_service_spec.rb314
-rw-r--r--spec/services/pages_service_spec.rb53
-rw-r--r--spec/services/preview_markdown_service_spec.rb12
-rw-r--r--spec/services/projects/after_import_service_spec.rb8
-rw-r--r--spec/services/projects/autocomplete_service_spec.rb69
-rw-r--r--spec/services/projects/batch_open_issues_count_service_spec.rb54
-rw-r--r--spec/services/projects/create_service_spec.rb21
-rw-r--r--spec/services/projects/destroy_service_spec.rb6
-rw-r--r--spec/services/projects/fork_service_spec.rb4
-rw-r--r--spec/services/projects/gitlab_projects_import_service_spec.rb2
-rw-r--r--spec/services/projects/hashed_storage/migrate_repository_service_spec.rb6
-rw-r--r--spec/services/projects/housekeeping_service_spec.rb72
-rw-r--r--spec/services/projects/import_service_spec.rb72
-rw-r--r--spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb102
-rw-r--r--spec/services/projects/lfs_pointers/lfs_download_service_spec.rb69
-rw-r--r--spec/services/projects/lfs_pointers/lfs_import_service_spec.rb146
-rw-r--r--spec/services/projects/lfs_pointers/lfs_link_service_spec.rb33
-rw-r--r--spec/services/projects/move_access_service_spec.rb12
-rw-r--r--spec/services/projects/move_project_authorizations_service_spec.rb8
-rw-r--r--spec/services/projects/move_project_group_links_service_spec.rb8
-rw-r--r--spec/services/projects/move_project_members_service_spec.rb8
-rw-r--r--spec/services/projects/open_issues_count_service_spec.rb86
-rw-r--r--spec/services/projects/overwrite_project_service_spec.rb8
-rw-r--r--spec/services/projects/participants_service_spec.rb2
-rw-r--r--spec/services/projects/transfer_service_spec.rb16
-rw-r--r--spec/services/projects/update_pages_service_spec.rb28
-rw-r--r--spec/services/projects/update_remote_mirror_service_spec.rb305
-rw-r--r--spec/services/projects/update_service_spec.rb8
-rw-r--r--spec/services/protected_branches/create_service_spec.rb8
-rw-r--r--spec/services/protected_tags/create_service_spec.rb4
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb18
-rw-r--r--spec/services/reset_project_cache_service_spec.rb2
-rw-r--r--spec/services/search/global_service_spec.rb2
-rw-r--r--spec/services/search_service_spec.rb2
-rw-r--r--spec/services/system_hooks_service_spec.rb2
-rw-r--r--spec/services/system_note_service_spec.rb9
-rw-r--r--spec/services/tags/create_service_spec.rb2
-rw-r--r--spec/services/test_hooks/project_service_spec.rb10
-rw-r--r--spec/services/todo_service_spec.rb25
-rw-r--r--spec/services/update_merge_request_metrics_service_spec.rb4
-rw-r--r--spec/services/upload_service_spec.rb10
-rw-r--r--spec/services/users/destroy_service_spec.rb8
-rw-r--r--spec/services/users/refresh_authorized_projects_service_spec.rb40
-rw-r--r--spec/services/web_hook_service_spec.rb30
-rw-r--r--spec/spec_helper.rb32
-rw-r--r--spec/support/api/milestones_shared_examples.rb6
-rw-r--r--spec/support/api/repositories_shared_context.rb2
-rw-r--r--spec/support/api/scopes/read_user_shared_examples.rb10
-rw-r--r--spec/support/api/time_tracking_shared_examples.rb8
-rw-r--r--spec/support/api/v3/time_tracking_shared_examples.rb128
-rw-r--r--spec/support/controllers/githubish_import_controller_shared_examples.rb9
-rw-r--r--spec/support/features/issuable_slash_commands_shared_examples.rb32
-rw-r--r--spec/support/features/reportable_note_shared_examples.rb6
-rw-r--r--spec/support/features/rss_shared_examples.rb24
-rwxr-xr-xspec/support/generate-seed-repo-rb2
-rw-r--r--spec/support/gitaly.rb5
-rw-r--r--spec/support/gitlab-git-test.git/packed-refs1
-rw-r--r--spec/support/gitlab_stubs/project_8.json68
-rw-r--r--spec/support/gitlab_stubs/projects.json283
-rw-r--r--spec/support/gitlab_stubs/session.json18
-rw-r--r--spec/support/helpers/api_helpers.rb11
-rw-r--r--spec/support/helpers/assets_helpers.rb15
-rw-r--r--spec/support/helpers/board_helpers.rb2
-rw-r--r--spec/support/helpers/cycle_analytics_helpers.rb30
-rw-r--r--spec/support/helpers/drag_to_helper.rb4
-rw-r--r--spec/support/helpers/exclusive_lease_helpers.rb36
-rw-r--r--spec/support/helpers/expect_next_instance_of.rb13
-rw-r--r--spec/support/helpers/features/notes_helpers.rb2
-rw-r--r--spec/support/helpers/features/sorting_helpers.rb2
-rw-r--r--spec/support/helpers/graphql_helpers.rb111
-rw-r--r--spec/support/helpers/jira_service_helper.rb2
-rw-r--r--spec/support/helpers/key_generator_helper.rb2
-rw-r--r--spec/support/helpers/login_helpers.rb53
-rw-r--r--spec/support/helpers/markdown_feature.rb2
-rw-r--r--spec/support/helpers/merge_request_diff_helpers.rb2
-rw-r--r--spec/support/helpers/migrations_helpers.rb4
-rw-r--r--spec/support/helpers/mobile_helpers.rb4
-rw-r--r--spec/support/helpers/query_recorder.rb7
-rw-r--r--spec/support/helpers/routes_helpers.rb7
-rw-r--r--spec/support/helpers/seed_repo.rb1
-rw-r--r--spec/support/helpers/sorting_helper.rb2
-rw-r--r--spec/support/helpers/stub_gitlab_calls.rb20
-rw-r--r--spec/support/helpers/stub_object_storage.rb24
-rw-r--r--spec/support/helpers/test_env.rb15
-rw-r--r--spec/support/helpers/wait_for_requests.rb4
-rw-r--r--spec/support/http_io/http_io_helpers.rb60
-rw-r--r--spec/support/import_export/configuration_helper.rb2
-rw-r--r--spec/support/import_export/export_file_helper.rb2
-rw-r--r--spec/support/matchers/access_matchers_for_controller.rb2
-rw-r--r--spec/support/matchers/disallow_request_matchers.rb15
-rw-r--r--spec/support/matchers/email_matchers.rb5
-rw-r--r--spec/support/matchers/exceed_query_limit.rb65
-rw-r--r--spec/support/matchers/graphql_matchers.rb71
-rw-r--r--spec/support/matchers/match_ids.rb7
-rw-r--r--spec/support/redis/redis_shared_examples.rb9
-rw-r--r--spec/support/rspec.rb2
-rw-r--r--spec/support/services/issuable_create_service_slash_commands_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/ci_trace_shared_examples.rb144
-rw-r--r--spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb83
-rw-r--r--spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/features/editable_merge_request_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb14
-rw-r--r--spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/protected_branches_access_control_ce.rb40
-rw-r--r--spec/support/shared_examples/file_finder.rb21
-rw-r--r--spec/support/shared_examples/helm_generated_script.rb2
-rw-r--r--spec/support/shared_examples/models/atomic_internal_id_spec.rb29
-rw-r--r--spec/support/shared_examples/models/members_notifications_shared_example.rb2
-rw-r--r--spec/support/shared_examples/notify_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/requests/api/merge_requests_list.rb281
-rw-r--r--spec/support/shared_examples/requests/graphql_shared_examples.rb11
-rw-r--r--spec/support/shared_examples/serializers/note_entity_examples.rb4
-rw-r--r--spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb99
-rw-r--r--spec/support/shared_examples/throttled_touch.rb4
-rw-r--r--spec/support/shared_examples/uploaders/object_storage_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/url_validator_examples.rb42
-rw-r--r--spec/support/shoulda/matchers/rails_shim.rb27
-rw-r--r--spec/support/trace/trace_helpers.rb27
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb29
-rw-r--r--spec/tasks/gitlab/db_rake_spec.rb10
-rw-r--r--spec/tasks/gitlab/git_rake_spec.rb17
-rw-r--r--spec/tasks/gitlab/gitaly_rake_spec.rb4
-rw-r--r--spec/tasks/gitlab/shell_rake_spec.rb8
-rw-r--r--spec/tasks/gitlab/storage_rake_spec.rb137
-rw-r--r--spec/tasks/tokens_spec.rb4
-rw-r--r--spec/uploaders/attachment_uploader_spec.rb2
-rw-r--r--spec/uploaders/file_mover_spec.rb2
-rw-r--r--spec/uploaders/file_uploader_spec.rb46
-rw-r--r--spec/uploaders/gitlab_uploader_spec.rb64
-rw-r--r--spec/uploaders/import_export_uploader_spec.rb20
-rw-r--r--spec/uploaders/job_artifact_uploader_spec.rb43
-rw-r--r--spec/uploaders/legacy_artifact_uploader_spec.rb3
-rw-r--r--spec/uploaders/namespace_file_uploader_spec.rb2
-rw-r--r--spec/uploaders/object_storage_spec.rb193
-rw-r--r--spec/uploaders/personal_file_uploader_spec.rb2
-rw-r--r--spec/uploaders/records_uploads_spec.rb2
-rw-r--r--spec/uploaders/uploader_helper_spec.rb2
-rw-r--r--spec/uploaders/workers/object_storage/background_move_worker_spec.rb6
-rw-r--r--spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb51
-rw-r--r--spec/validators/public_url_validator_spec.rb28
-rw-r--r--spec/validators/url_placeholder_validator_spec.rb39
-rw-r--r--spec/validators/url_validator_spec.rb95
-rw-r--r--spec/views/admin/dashboard/index.html.haml_spec.rb7
-rw-r--r--spec/views/devise/shared/_signin_box.html.haml_spec.rb1
-rw-r--r--spec/views/errors/access_denied.html.haml_spec.rb7
-rw-r--r--spec/views/help/index.html.haml_spec.rb2
-rw-r--r--spec/views/projects/imports/new.html.haml_spec.rb4
-rw-r--r--spec/views/projects/merge_requests/show.html.haml_spec.rb2
-rw-r--r--spec/views/projects/pipeline_schedules/_pipeline_schedule.html.haml_spec.rb6
-rw-r--r--spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb8
-rw-r--r--spec/views/shared/notes/_form.html.haml_spec.rb2
-rw-r--r--spec/workers/check_gcp_project_billing_worker_spec.rb116
-rw-r--r--spec/workers/ci/archive_traces_cron_worker_spec.rb71
-rw-r--r--spec/workers/concerns/waitable_worker_spec.rb8
-rw-r--r--spec/workers/create_note_diff_file_worker_spec.rb16
-rw-r--r--spec/workers/delete_diff_files_worker_spec.rb41
-rw-r--r--spec/workers/delete_user_worker_spec.rb10
-rw-r--r--spec/workers/every_sidekiq_worker_spec.rb2
-rw-r--r--spec/workers/git_garbage_collect_worker_spec.rb131
-rw-r--r--spec/workers/gitlab/github_import/stage/import_lfs_objects_worker_spec.rb28
-rw-r--r--spec/workers/gitlab/github_import/stage/import_notes_worker_spec.rb2
-rw-r--r--spec/workers/merge_worker_spec.rb2
-rw-r--r--spec/workers/object_storage_upload_worker_spec.rb108
-rw-r--r--spec/workers/pipeline_schedule_worker_spec.rb2
-rw-r--r--spec/workers/process_commit_worker_spec.rb40
-rw-r--r--spec/workers/project_cache_worker_spec.rb15
-rw-r--r--spec/workers/project_destroy_worker_spec.rb6
-rw-r--r--spec/workers/project_migrate_hashed_storage_worker_spec.rb60
-rw-r--r--spec/workers/propagate_service_template_worker_spec.rb38
-rw-r--r--spec/workers/prune_web_hook_logs_worker_spec.rb22
-rw-r--r--spec/workers/repository_check/batch_worker_spec.rb37
-rw-r--r--spec/workers/repository_check/dispatch_worker_spec.rb44
-rw-r--r--spec/workers/repository_check/single_repository_worker_spec.rb8
-rw-r--r--spec/workers/repository_fork_worker_spec.rb18
-rw-r--r--spec/workers/repository_import_worker_spec.rb19
-rw-r--r--spec/workers/repository_remove_remote_worker_spec.rb61
-rw-r--r--spec/workers/repository_update_remote_mirror_worker_spec.rb10
-rw-r--r--spec/workers/storage_migrator_worker_spec.rb25
-rw-r--r--spec/workers/stuck_ci_jobs_worker_spec.rb36
-rw-r--r--spec/workers/stuck_import_jobs_worker_spec.rb4
-rw-r--r--spec/workers/update_merge_requests_worker_spec.rb5
-rw-r--r--vendor/assets/javascripts/Sortable.js1374
-rw-r--r--vendor/assets/javascripts/date.format.js132
-rw-r--r--vendor/gitignore/Autotools.gitignore3
-rw-r--r--vendor/gitignore/CraftCMS.gitignore5
-rw-r--r--vendor/gitignore/Dart.gitignore10
-rw-r--r--vendor/gitignore/Delphi.gitignore2
-rw-r--r--vendor/gitignore/Eagle.gitignore1
-rw-r--r--vendor/gitignore/GWT.gitignore3
-rw-r--r--vendor/gitignore/Global/Backup.gitignore5
-rw-r--r--vendor/gitignore/Global/CodeKit.gitignore1
-rw-r--r--vendor/gitignore/Global/Eclipse.gitignore5
-rw-r--r--vendor/gitignore/Global/JetBrains.gitignore13
-rw-r--r--vendor/gitignore/Global/Matlab.gitignore15
-rw-r--r--vendor/gitignore/Global/Patch.gitignore2
-rw-r--r--vendor/gitignore/Global/SynopsysVCS.gitignore8
-rw-r--r--vendor/gitignore/Global/Vim.gitignore3
-rw-r--r--vendor/gitignore/LabVIEW.gitignore1
-rw-r--r--vendor/gitignore/Maven.gitignore4
-rw-r--r--vendor/gitignore/Node.gitignore12
-rw-r--r--vendor/gitignore/Objective-C.gitignore3
-rw-r--r--vendor/gitignore/Perl6.gitignore7
-rw-r--r--vendor/gitignore/Sass.gitignore2
-rw-r--r--vendor/gitignore/Swift.gitignore3
-rw-r--r--vendor/gitignore/TeX.gitignore10
-rw-r--r--vendor/gitignore/Terraform.gitignore12
-rw-r--r--vendor/gitignore/Typo3.gitignore5
-rw-r--r--vendor/gitignore/Umbraco.gitignore2
-rw-r--r--vendor/gitignore/UnrealEngine.gitignore3
-rw-r--r--vendor/gitignore/VisualStudio.gitignore7
-rw-r--r--vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml124
-rw-r--r--vendor/gitlab-ci-yml/autodeploy/Kubernetes-with-canary.gitlab-ci.yml87
-rw-r--r--vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml74
-rw-r--r--vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml74
-rw-r--r--vendor/jupyter/values.yaml19
-rw-r--r--vendor/licenses.csv457
-rw-r--r--vendor/project_templates/express.tar.gzbin4866 -> 4894 bytes
-rw-r--r--vendor/project_templates/rails.tar.gzbin25151 -> 25182 bytes
-rw-r--r--vendor/project_templates/spring.tar.gzbin49430 -> 49476 bytes
-rw-r--r--yarn.lock2396
4599 files changed, 116382 insertions, 76407 deletions
diff --git a/.codeclimate.yml b/.codeclimate.yml
index 8699a903f2a..9998ddba643 100644
--- a/.codeclimate.yml
+++ b/.codeclimate.yml
@@ -8,8 +8,6 @@ engines:
languages:
- ruby
- javascript
- exclude_paths:
- - "lib/api/v3/*"
ratings:
paths:
- Gemfile.lock
diff --git a/.eslintrc b/.eslintrc
deleted file mode 100644
index 3f187db0c07..00000000000
--- a/.eslintrc
+++ /dev/null
@@ -1,56 +0,0 @@
-{
- "env": {
- "browser": true,
- "es6": true
- },
- "extends": [
- "airbnb-base",
- "plugin:vue/recommended"
- ],
- "globals": {
- "__webpack_public_path__": true,
- "gl": false,
- "gon": false,
- "localStorage": false
- },
- "parserOptions": {
- "parser": "babel-eslint"
- },
- "plugins": [
- "filenames",
- "import",
- "html",
- "promise"
- ],
- "settings": {
- "html/html-extensions": [".html", ".html.raw"],
- "import/resolver": {
- "webpack": {
- "config": "./config/webpack.config.js"
- }
- }
- },
- "rules": {
- "filenames/match-regex": [2, "^[a-z0-9_]+$"],
- "import/no-commonjs": "error",
- "no-multiple-empty-lines": ["error", { "max": 1 }],
- "promise/catch-or-return": "error",
- "no-underscore-dangle": ["error", { "allow": ["__", "_links"] }],
- "no-mixed-operators": 0,
- "space-before-function-paren": 0,
- "curly": 0,
- "arrow-parens": 0,
- "vue/html-self-closing": [
- "error",
- {
- "html": {
- "void": "always",
- "normal": "never",
- "component": "always"
- },
- "svg": "always",
- "math": "always"
- }
- ]
- }
-}
diff --git a/.eslintrc.yml b/.eslintrc.yml
new file mode 100644
index 00000000000..77b1b72fe68
--- /dev/null
+++ b/.eslintrc.yml
@@ -0,0 +1,71 @@
+---
+env:
+ browser: true
+ es6: true
+extends:
+ - airbnb-base
+ - plugin:vue/recommended
+globals:
+ __webpack_public_path__: true
+ gl: false
+ gon: false
+ localStorage: false
+parserOptions:
+ parser: babel-eslint
+plugins:
+ - filenames
+ - import
+ - html
+ - promise
+settings:
+ html/html-extensions:
+ - ".html"
+ - ".html.raw"
+ import/resolver:
+ webpack:
+ config: "./config/webpack.config.js"
+rules:
+ filenames/match-regex:
+ - error
+ - "^[a-z0-9_]+$"
+ import/no-commonjs: error
+ no-multiple-empty-lines:
+ - error
+ - max: 1
+ promise/catch-or-return: error
+ no-underscore-dangle:
+ - error
+ - allow:
+ - __
+ - _links
+ no-mixed-operators: off
+ vue/html-self-closing:
+ - error
+ - html:
+ void: always
+ normal: never
+ component: always
+ svg: always
+ math: always
+ ## Conflicting rules with prettier:
+ space-before-function-paren: off
+ curly: off
+ arrow-parens: off
+ function-paren-newline: off
+ object-curly-newline: off
+ padded-blocks: off
+ # Disabled for now, to make the eslint 3 -> eslint 4 update smoother
+ ## Indent rule. We are using the old for now: https://eslint.org/docs/user-guide/migrating-to-4.0.0#indent-rewrite
+ indent: off
+ indent-legacy:
+ - error
+ - 2
+ - SwitchCase: 1
+ VariableDeclarator: 1
+ outerIIFEBody: 1
+ FunctionDeclaration:
+ parameters: 1
+ body: 1
+ FunctionExpression:
+ parameters: 1
+ body: 1
diff --git a/.flayignore b/.flayignore
index 7faa6c7bb90..4b6f7ba693a 100644
--- a/.flayignore
+++ b/.flayignore
@@ -1,11 +1,18 @@
*.erb
lib/gitlab/sanitizers/svg/whitelist.rb
lib/gitlab/diff/position_tracer.rb
+app/controllers/projects/approver_groups_controller.rb
+app/controllers/projects/approvers_controller.rb
+app/controllers/projects/protected_branches/merge_access_levels_controller.rb
+app/controllers/projects/protected_branches/push_access_levels_controller.rb
+app/controllers/projects/protected_tags/create_access_levels_controller.rb
app/policies/project_policy.rb
app/models/concerns/relative_positioning.rb
app/workers/stuck_merge_jobs_worker.rb
lib/gitlab/redis/*.rb
lib/gitlab/gitaly_client/operation_service.rb
+app/models/project_services/packagist_service.rb
+lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb
lib/gitlab/background_migration/*
app/models/project_services/kubernetes_service.rb
lib/gitlab/workhorse.rb
@@ -14,3 +21,14 @@ lib/gitlab/gitaly_client/ref_service.rb
lib/gitlab/gitaly_client/commit_service.rb
lib/gitlab/git/commit.rb
lib/gitlab/git/tag.rb
+
+ee/db/**/*
+ee/app/serializers/ee/merge_request_widget_entity.rb
+ee/lib/api/epics.rb
+ee/lib/api/geo_nodes.rb
+ee/lib/ee/api/group_boards.rb
+ee/lib/ee/api/boards.rb
+ee/lib/ee/gitlab/ldap/sync/admin_users.rb
+ee/app/workers/geo/file_download_dispatch_worker/job_artifact_job_finder.rb
+ee/app/workers/geo/file_download_dispatch_worker/lfs_object_job_finder.rb
+ee/spec/**/*
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000000..f1c41c9bb76
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+Dangerfile gitlab-language=ruby
diff --git a/.gitignore b/.gitignore
index c7d1648615d..9a42a663fb4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,7 +29,7 @@ eslint-report.html
/app/assets/javascripts/locale/**/app.js
/backups/*
/config/aws.yml
-/config/database.yml
+/config/database*.yml
/config/gitlab.yml
/config/gitlab_ci.yml
/config/initializers/rack_attack.rb
@@ -64,6 +64,7 @@ eslint-report.html
/tags
/tmp/*
/vendor/bundle/*
+/vendor/gitaly-ruby
/builds*
/shared/*
/.gitlab_workhorse_secret
@@ -75,3 +76,4 @@ eslint-report.html
/.rspec
/plugins/*
/.gitlab_pages_secret
+package-lock.json
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index ef263a3f106..137c26d7dae 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,4 +1,4 @@
-image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.7-golang-1.9-git-2.17-chrome-65.0-node-8.x-yarn-1.2-postgresql-9.6"
+image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.4.4-golang-1.9-git-2.17-chrome-67.0-node-8.x-yarn-1.2-postgresql-9.6-graphicsmagick-1.3.29"
.dedicated-runner: &dedicated-runner
retry: 1
@@ -6,7 +6,7 @@ image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.7-golang-1.9-git
- gitlab-org
.default-cache: &default-cache
- key: "ruby-2.3.7-with-yarn"
+ key: "ruby-2.4.4-debian-stretch-with-yarn"
paths:
- vendor/ruby
- .yarn-cache/
@@ -220,18 +220,6 @@ stages:
paths:
- log/development.log
-# Review docs base
-.review-docs: &review-docs
- <<: *dedicated-runner
- <<: *except-qa
- <<: *single-script-job
- variables:
- <<: *single-script-job-variables
- SCRIPT_NAME: trigger-build-docs
- when: manual
- only:
- - branches
-
# DB migration, rollback, and seed jobs
.db-migrate-reset: &db-migrate-reset
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
@@ -264,40 +252,67 @@ package-and-qa:
<<: *single-script-job
variables:
<<: *single-script-job-variables
- SCRIPT_NAME: trigger-build-omnibus
+ SCRIPT_NAME: trigger-build
retry: 0
script:
- - ./$SCRIPT_NAME
+ - ./$SCRIPT_NAME omnibus
when: manual
only:
- //@gitlab-org/gitlab-ce
- //@gitlab-org/gitlab-ee
-# Trigger a docs build in gitlab-docs
-# Useful to preview the docs changes live
-review-docs-deploy:
- <<: *review-docs
- stage: build
+# Review docs base
+.review-docs: &review-docs
+ <<: *dedicated-runner
+ <<: *single-script-job
+ variables:
+ <<: *single-script-job-variables
+ SCRIPT_NAME: trigger-build-docs
environment:
- name: review-docs/$CI_COMMIT_REF_NAME
+ name: review-docs/$CI_COMMIT_REF_SLUG
# 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://$DOCS_GITLAB_REPO_SUFFIX-$CI_COMMIT_REF_SLUG.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX
+ url: http://$CI_ENVIRONMENT_SLUG.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX
on_stop: review-docs-cleanup
+
+# Trigger a manual docs build in gitlab-docs only on non docs-only branches.
+# Useful to preview the docs changes live.
+review-docs-deploy-manual:
+ <<: *review-docs
+ stage: build
script:
- gem install gitlab --no-ri --no-rdoc
- ./$SCRIPT_NAME deploy
+ when: manual
+ only:
+ - branches@gitlab-org/gitlab-ce
+ - branches@gitlab-org/gitlab-ee
+ <<: *except-docs-and-qa
+
+# Always trigger a docs build in gitlab-docs only on docs-only branches.
+# Useful to preview the docs changes live.
+review-docs-deploy:
+ <<: *review-docs
+ stage: post-test
+ script:
+ - gem install gitlab --no-ri --no-rdoc
+ - ./$SCRIPT_NAME deploy
+ only:
+ - /(^docs[\/-].*|.*-docs$)/@gitlab-org/gitlab-ce
+ - /(^docs[\/-].*|.*-docs$)/@gitlab-org/gitlab-ee
+ <<: *except-qa
# Cleanup remote environment of gitlab-docs
review-docs-cleanup:
<<: *review-docs
stage: post-cleanup
environment:
- name: review-docs/$CI_COMMIT_REF_NAME
+ name: review-docs/$CI_COMMIT_REF_SLUG
action: stop
+ when: manual
script:
- gem install gitlab --no-ri --no-rdoc
- - ./SCRIPT_NAME cleanup
+ - ./$SCRIPT_NAME cleanup
##
# Trigger a docker image build in CNG (Cloud Native GitLab) repository
@@ -310,11 +325,9 @@ cloud-native-image:
variables:
GIT_DEPTH: "1"
cache: {}
- before_script:
- - gem install gitlab --no-rdoc --no-ri
- - chmod 755 ./scripts/trigger-build-cloud-native
script:
- - ./scripts/trigger-build-cloud-native
+ - gem install gitlab --no-ri --no-rdoc
+ - ./scripts/trigger-build cng
only:
- tags@gitlab-org/gitlab-ce
- tags@gitlab-org/gitlab-ee
@@ -335,6 +348,24 @@ retrieve-tests-metadata:
- wget -O $FLAKY_RSPEC_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$FLAKY_RSPEC_SUITE_REPORT_PATH || rm $FLAKY_RSPEC_SUITE_REPORT_PATH
- '[[ -f $FLAKY_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${FLAKY_RSPEC_SUITE_REPORT_PATH}'
+danger-review:
+ image: registry.gitlab.com/gitlab-org/gitaly/dangercontainer:latest
+ stage: prepare
+ before_script:
+ - source scripts/utils.sh
+ - retry gem install danger --no-ri --no-rdoc
+ cache: {}
+ only:
+ refs:
+ - branches@gitlab-org/gitlab-ce
+ - branches@gitlab-org/gitlab-ee
+ except:
+ variables:
+ - $CI_COMMIT_REF_NAME =~ /^ce-to-ee-.*/
+ script:
+ - git version
+ - danger --fail-on-errors=true
+
update-tests-metadata:
<<: *tests-metadata-state
<<: *only-canonical-masters
@@ -394,7 +425,11 @@ compile-assets:
- date
- yarn install --frozen-lockfile --cache-folder .yarn-cache
- date
+ - free -m
- bundle exec rake gitlab:assets:compile
+ variables:
+ # we override the max_old_space_size to prevent OOM errors
+ NODE_OPTIONS: --max_old_space_size=3584
artifacts:
expire_in: 7d
paths:
@@ -411,6 +446,7 @@ setup-test-env:
script:
- bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init'
- scripts/gitaly-test-build # Do not use 'bundle exec' here
+ - BUNDLE_GEMFILE=Gemfile.rails5 bundle install $BUNDLE_INSTALL_FLAGS
artifacts:
expire_in: 7d
paths:
@@ -550,7 +586,7 @@ static-analysis:
script:
- scripts/static-analysis
cache:
- key: "ruby-2.3.7-with-yarn-and-rubocop"
+ key: "ruby-2.4.4-debian-stretch-with-yarn-and-rubocop"
paths:
- vendor/ruby
- .yarn-cache/
@@ -586,12 +622,18 @@ downtime_check:
- /(^docs[\/-].*|.*-docs$)/
- /(^qa[\/-].*|.*-qa$)/
+rails5_gemfile_lock_check:
+ <<: *dedicated-no-docs-no-db-pull-cache-job
+ <<: *except-docs-and-qa
+ script:
+ - scripts/rails5-gemfile-lock-check
+
ee_compat_check:
<<: *rake-exec
except:
- master
- tags
- - /^[\d-]+-stable(-ee)?/
+ - /[\d-]+-stable(-ee)?/
- /^security-/
- branches@gitlab-org/gitlab-ee
- branches@gitlab/gitlab-ee
@@ -658,10 +700,13 @@ gitlab:assets:compile:
SKIP_STORAGE_VALIDATION: "true"
WEBPACK_REPORT: "true"
NO_COMPRESSION: "true"
+ # we override the max_old_space_size to prevent OOM errors
+ NODE_OPTIONS: --max_old_space_size=3584
script:
- date
- yarn install --frozen-lockfile --production --cache-folder .yarn-cache
- date
+ - free -m
- bundle exec rake gitlab:assets:compile
artifacts:
name: webpack-report
@@ -802,8 +847,6 @@ lint:javascript:report:
before_script: []
script:
- date
- - find app/ spec/ -name '*.js' -exec sed --in-place 's|/\* eslint-disable .*\*/||' {} \; # run report over all files
- - date
- yarn run eslint-report || true # ignore exit code
artifacts:
name: eslint-report
diff --git a/.gitlab/issue_templates/Feature Proposal.md b/.gitlab/issue_templates/Feature Proposal.md
deleted file mode 100644
index 5b55eb1374b..00000000000
--- a/.gitlab/issue_templates/Feature Proposal.md
+++ /dev/null
@@ -1,9 +0,0 @@
-### Description
-
-(Include problem, use cases, benefits, and/or goals)
-
-### Proposal
-
-### Links / references
-
-/label ~"feature proposal"
diff --git a/.gitlab/issue_templates/Feature proposal.md b/.gitlab/issue_templates/Feature proposal.md
new file mode 100644
index 00000000000..c4065d3c4ea
--- /dev/null
+++ b/.gitlab/issue_templates/Feature proposal.md
@@ -0,0 +1,15 @@
+### Problem to solve
+
+### Further details
+
+(Include use cases, benefits, and/or goals)
+
+### Proposal
+
+### What does success look like, and how can we measure that?
+
+(If no way to measure success, link to an issue that will implement a way to measure this)
+
+### Links / references
+
+/label ~"feature proposal"
diff --git a/.gitlab/issue_templates/Research Proposal.md b/.gitlab/issue_templates/Research proposal.md
index 5676656793d..5676656793d 100644
--- a/.gitlab/issue_templates/Research Proposal.md
+++ b/.gitlab/issue_templates/Research proposal.md
diff --git a/.gitlab/issue_templates/Security Developer Workflow.md b/.gitlab/issue_templates/Security Developer Workflow.md
deleted file mode 100644
index 0c878dbf64c..00000000000
--- a/.gitlab/issue_templates/Security Developer Workflow.md
+++ /dev/null
@@ -1,70 +0,0 @@
-<!--
-# Read me first!
-
-Create this issue under https://dev.gitlab.org/gitlab/gitlabhq
-
-Set the title to: `[Security] Description of the original issue`
--->
-
-### Prior to the security release
-
-- [ ] Read the [security process for developers] if you are not familiar with it.
-- [ ] Link to the original issue adding it to the [links section](#links)
-- [ ] Run `scripts/security-harness` in the CE, EE, and/or Omnibus to prevent pushing to any remote besides `dev.gitlab.org`
-- [ ] Create an MR targetting `org` `master`, prefixing your branch with `security-`
-- [ ] Label your MR with the ~security label, prefix the title with `WIP: [master]`
-- [ ] Add a link to the MR to the [links section](#links)
-- [ ] Add a link to an EE MR if required
-- [ ] Make sure the MR remains in-progress and gets approved after the review cycle, **but never merged**.
-- [ ] Assign the MR to a RM once is reviewed and ready to be merged. Check the [RM list] to see who to ping.
-
-#### Backports
-
-- [ ] Once the MR is ready to be merged, create MRs targetting the last 3 releases
- - [ ] At this point, it might be easy to squash the commits from the MR into one
- - You can use the script `bin/secpick` instead of the following steps, to help you cherry-picking. See the [seckpick documentation]
- - [ ] Create the branch `security-X-Y` from `X-Y-stable` if it doesn't exist (and make sure it's up to date with stable)
- - [ ] Create each MR targetting the security branch `security-X-Y`
- - [ ] Add the ~security label and prefix with the version `WIP: [X.Y]` the title of the MR
-- [ ] Make sure all MRs have a link in the [links section](#links) and are assigned to a Release Manager.
-
-[seckpick documentation]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md#secpick-script
-
-#### Documentation and final details
-
-- [ ] Check the topic on #security to see when the next release is going to happen and add a link to the [links section](#links)
-- [ ] Find out the versions affected (the Git history of the files affected may help you with this) and add them to the [details section](#details)
-- [ ] Fill in any upgrade notes that users may need to take into account in the [details section](#details)
-- [ ] Add Yes/No and further details if needed to the migration and settings columns in the [details section](#details)
-- [ ] Add the nickname of the external user who found the issue (and/or HackerOne profile) to the Thanks row in the [details section](#details)
-
-### Summary
-#### Links
-
-| Description | Link |
-| -------- | -------- |
-| Original issue | #TODO |
-| Security release issue | #TODO |
-| `master` MR | !TODO |
-| `master` MR (EE) | !TODO |
-| `Backport X.Y` MR | !TODO |
-| `Backport X.Y` MR | !TODO |
-| `Backport X.Y` MR | !TODO |
-| `Backport X.Y` MR (EE) | !TODO |
-| `Backport X.Y` MR (EE) | !TODO |
-| `Backport X.Y` MR (EE) | !TODO |
-
-#### Details
-
-| Description | Details | Further details|
-| -------- | -------- | -------- |
-| Versions affected | X.Y | |
-| Upgrade notes | | |
-| GitLab Settings updated | Yes/No| |
-| Migration required | Yes/No | |
-| Thanks | | |
-
-[security process for developers]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md
-[RM list]: https://about.gitlab.com/release-managers/
-
-/label ~security
diff --git a/.gitlab/issue_templates/Security developer workflow.md b/.gitlab/issue_templates/Security developer workflow.md
new file mode 100644
index 00000000000..c1f702e9385
--- /dev/null
+++ b/.gitlab/issue_templates/Security developer workflow.md
@@ -0,0 +1,71 @@
+<!--
+# Read me first!
+
+Create this issue under https://dev.gitlab.org/gitlab/gitlabhq
+
+Set the title to: `[Security] Description of the original issue`
+-->
+
+### Prior to the security release
+
+- [ ] Read the [security process for developers] if you are not familiar with it.
+- [ ] Link to the original issue adding it to the [links section](#links)
+- [ ] Run `scripts/security-harness` in the CE, EE, and/or Omnibus to prevent pushing to any remote besides `dev.gitlab.org`
+- [ ] Create an MR targetting `org` `master`, prefixing your branch with `security-`
+- [ ] Label your MR with the ~security label, prefix the title with `WIP: [master]`
+- [ ] Add a link to the MR to the [links section](#links)
+- [ ] Add a link to an EE MR if required
+- [ ] Make sure the MR remains in-progress and gets approved after the review cycle, **but never merged**.
+- [ ] Assign the MR to a RM once is reviewed and ready to be merged. Check the [RM list] to see who to ping.
+
+#### Backports
+
+- [ ] Once the MR is ready to be merged, create MRs targetting the last 3 releases
+ - [ ] At this point, it might be easy to squash the commits from the MR into one
+ - You can use the script `bin/secpick` instead of the following steps, to help you cherry-picking. See the [seckpick documentation]
+ - [ ] Create the branch `security-X-Y` from `X-Y-stable` if it doesn't exist (and make sure it's up to date with stable)
+ - [ ] Create each MR targetting the security branch `security-X-Y`
+ - [ ] Add the ~security label and prefix with the version `WIP: [X.Y]` the title of the MR
+- [ ] Make sure all MRs have a link in the [links section](#links) and are assigned to a Release Manager.
+
+[seckpick documentation]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md#secpick-script
+
+#### Documentation and final details
+
+- [ ] Check the topic on #security to see when the next release is going to happen and add a link to the [links section](#links)
+- [ ] Find out the versions affected (the Git history of the files affected may help you with this) and add them to the [details section](#details)
+- [ ] Fill in any upgrade notes that users may need to take into account in the [details section](#details)
+- [ ] Add Yes/No and further details if needed to the migration and settings columns in the [details section](#details)
+- [ ] Add the nickname of the external user who found the issue (and/or HackerOne profile) to the Thanks row in the [details section](#details)
+
+### Summary
+
+#### Links
+
+| Description | Link |
+| -------- | -------- |
+| Original issue | #TODO |
+| Security release issue | #TODO |
+| `master` MR | !TODO |
+| `master` MR (EE) | !TODO |
+| `Backport X.Y` MR | !TODO |
+| `Backport X.Y` MR | !TODO |
+| `Backport X.Y` MR | !TODO |
+| `Backport X.Y` MR (EE) | !TODO |
+| `Backport X.Y` MR (EE) | !TODO |
+| `Backport X.Y` MR (EE) | !TODO |
+
+#### Details
+
+| Description | Details | Further details|
+| -------- | -------- | -------- |
+| Versions affected | X.Y | |
+| Upgrade notes | | |
+| GitLab Settings updated | Yes/No| |
+| Migration required | Yes/No | |
+| Thanks | | |
+
+[security process for developers]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md
+[RM list]: https://about.gitlab.com/release-managers/
+
+/label ~security
diff --git a/.gitlab/merge_request_templates/Database Changes.md b/.gitlab/merge_request_templates/Database Changes.md
deleted file mode 100644
index 1c4f30d9320..00000000000
--- a/.gitlab/merge_request_templates/Database Changes.md
+++ /dev/null
@@ -1,50 +0,0 @@
-Add a description of your merge request here. Merge requests without an adequate
-description will not be reviewed until one is added.
-
-## Database Checklist
-
-When adding migrations:
-
-- [ ] Updated `db/schema.rb`
-- [ ] Added a `down` method so the migration can be reverted
-- [ ] Added the output of the migration(s) to the MR body
-- [ ] Added tests for the migration in `spec/migrations` if necessary (e.g. when migrating data)
-
-When adding or modifying queries to improve performance:
-
-- [ ] 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 before adding the foreign key
-- [ ] Removed any instances of `dependent: ...` that may no longer be necessary
-
-When adding tables:
-
-- [ ] 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:
-
-- [ ] Removed these in a post-deployment migration
-- [ ] Made sure the application no longer uses (or ignores) these structures
-
-## General Checklist
-
-- [ ] [Changelog entry](https://docs.gitlab.com/ee/development/changelog.html) added, if necessary
-- [ ] [Documentation created/updated](https://docs.gitlab.com/ee/development/doc_styleguide.html)
-- [ ] API support added
-- [ ] Tests added for this feature/bug
-- Conform by the [code review guidelines](https://docs.gitlab.com/ee/development/code_review.html)
- - [ ] Has been reviewed by a Backend maintainer
- - [ ] Has been reviewed by a Database specialist
-- [ ] Conform by the [merge request performance guides](https://docs.gitlab.com/ee/development/merge_request_performance_guidelines.html)
-- [ ] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/CONTRIBUTING.md#style-guides)
-- [ ] If you have multiple commits, please combine them into a few logically organized commits by [squashing them](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits)
-- [ ] Internationalization required/considered
-- [ ] If paid feature, have we considered GitLab.com plan and how it works for groups and is there a design for promoting it to users who aren't on the correct plan
-- [ ] End-to-end tests pass (`package-and-qa` manual pipeline job)
-
-/label ~database
diff --git a/.gitlab/merge_request_templates/Database changes.md b/.gitlab/merge_request_templates/Database changes.md
new file mode 100644
index 00000000000..d14d52e1b6b
--- /dev/null
+++ b/.gitlab/merge_request_templates/Database changes.md
@@ -0,0 +1,50 @@
+Add a description of your merge request here. Merge requests without an adequate
+description will not be reviewed until one is added.
+
+## Database checklist
+
+When adding migrations:
+
+- [ ] Updated `db/schema.rb`
+- [ ] Added a `down` method so the migration can be reverted
+- [ ] Added the output of the migration(s) to the MR body
+- [ ] Added tests for the migration in `spec/migrations` if necessary (e.g. when migrating data)
+
+When adding or modifying queries to improve performance:
+
+- [ ] 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 before adding the foreign key
+- [ ] Removed any instances of `dependent: ...` that may no longer be necessary
+
+When adding tables:
+
+- [ ] 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:
+
+- [ ] Removed these in a post-deployment migration
+- [ ] Made sure the application no longer uses (or ignores) these structures
+
+## General checklist
+
+- [ ] [Changelog entry](https://docs.gitlab.com/ee/development/changelog.html) added, if necessary
+- [ ] [Documentation created/updated](https://docs.gitlab.com/ee/development/doc_styleguide.html)
+- [ ] API support added
+- [ ] Tests added for this feature/bug
+- Conform by the [code review guidelines](https://docs.gitlab.com/ee/development/code_review.html)
+ - [ ] Has been reviewed by a Backend maintainer
+ - [ ] Has been reviewed by a Database specialist
+- [ ] Conform by the [merge request performance guides](https://docs.gitlab.com/ee/development/merge_request_performance_guidelines.html)
+- [ ] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/CONTRIBUTING.md#style-guides)
+- [ ] If you have multiple commits, please combine them into a few logically organized commits by [squashing them](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits)
+- [ ] Internationalization required/considered
+- [ ] If paid feature, have we considered GitLab.com plan and how it works for groups and is there a design for promoting it to users who aren't on the correct plan
+- [ ] End-to-end tests pass (`package-and-qa` manual pipeline job)
+
+/label ~database
diff --git a/.nvmrc b/.nvmrc
index f7ee06693c1..dba04c1e178 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-9.0.0
+8.11.3
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 16b0b5c95e2..8a1ca6747a8 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -10,10 +10,6 @@
Capybara/CurrentPathExpectation:
Enabled: false
-# Offense count: 956
-Capybara/FeatureMethods:
- Enabled: false
-
# Offense count: 23
FactoryBot/DynamicAttributeDefinedStatically:
Exclude:
@@ -173,7 +169,6 @@ Lint/UriEscapeUnescape:
- 'spec/requests/api/files_spec.rb'
- 'spec/requests/api/internal_spec.rb'
- 'spec/requests/api/issues_spec.rb'
- - 'spec/requests/api/v3/issues_spec.rb'
# Offense count: 1
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
@@ -204,12 +199,6 @@ Naming/HeredocDelimiterCase:
Naming/HeredocDelimiterNaming:
Enabled: false
-# Offense count: 27
-# Cop supports --auto-correct.
-# Configuration parameters: AutoCorrect.
-Performance/HashEachMethods:
- Enabled: false
-
# Offense count: 1
Performance/UnfreezeString:
Exclude:
@@ -333,8 +322,6 @@ RSpec/ScatteredSetup:
- 'spec/lib/gitlab/bitbucket_import/importer_spec.rb'
- 'spec/lib/gitlab/git/env_spec.rb'
- 'spec/requests/api/jobs_spec.rb'
- - 'spec/requests/api/v3/builds_spec.rb'
- - 'spec/requests/api/v3/projects_spec.rb'
- 'spec/services/projects/create_service_spec.rb'
# Offense count: 1
@@ -490,7 +477,7 @@ Style/EmptyLiteral:
- 'lib/gitlab/fogbugz_import/importer.rb'
- 'lib/gitlab/git/diff_collection.rb'
- 'lib/gitlab/gitaly_client.rb'
- - 'scripts/trigger-build-omnibus'
+ - 'scripts/trigger-build'
- 'spec/features/merge_requests/versions_spec.rb'
- 'spec/helpers/merge_requests_helper_spec.rb'
- 'spec/lib/gitlab/request_context_spec.rb'
@@ -618,7 +605,6 @@ Style/OrAssignment:
Exclude:
- 'app/models/concerns/token_authenticatable.rb'
- 'lib/api/commit_statuses.rb'
- - 'lib/api/v3/members.rb'
- 'lib/gitlab/project_transfer.rb'
# Offense count: 50
@@ -781,7 +767,6 @@ Style/TernaryParentheses:
- 'app/finders/projects_finder.rb'
- 'app/helpers/namespaces_helper.rb'
- 'features/support/capybara.rb'
- - 'lib/api/v3/projects.rb'
- 'lib/gitlab/ci/build/artifacts/metadata/entry.rb'
- 'spec/requests/api/pipeline_schedules_spec.rb'
- 'spec/support/capybara.rb'
diff --git a/.ruby-version b/.ruby-version
index 00355e29d11..79a614418f7 100644
--- a/.ruby-version
+++ b/.ruby-version
@@ -1 +1 @@
-2.3.7
+2.4.4
diff --git a/.scss-lint.yml b/.scss-lint.yml
index 180d377d6f8..3df66033fa8 100644
--- a/.scss-lint.yml
+++ b/.scss-lint.yml
@@ -46,8 +46,9 @@ linters:
# - properties
# - @include declarations with inner @content
# - nested rule sets.
+ # Disabled to minimize Bootstrap migration footprint
DeclarationOrder:
- enabled: true
+ enabled: false
# `scss-lint:disable` control comments should be preceded by a comment
# explaining why these linters are being disabled for this file.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 99cf96035d9..e90f599ced1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,337 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 11.0.3 (2018-07-05)
+
+### Fixed (14 changes, 1 of them is from the community)
+
+- Revert merge request widget button max height. !20175 (George Tsiolis)
+- Implement upload copy when moving an issue with upload on object storage. !20191
+- Fix broken '!' support to autocomplete MRs in GFM fields. !20204
+- Restore showing Elasticsearch and Geo status on dashboard. !20276
+- Fix merge request page rendering error when its target/source branch is missing. !20280
+- Fix sidebar collapse breapoints for job and wiki pages.
+- fix size of code blocks in headings.
+- Fix loading screen for search autocomplete dropdown.
+- Fix ambiguous due_date column for Issue scopes.
+- Always serve favicon from main GitLab domain so that CI badge can be drawn over it.
+- Fix tooltip flickering bug.
+- Fix refreshing cache keys for open issues count.
+- Replace deprecated bs.affix in merge request tabs with sticky polyfill.
+- Prevent pipeline job tooltip from scrolling off dropdown container.
+
+
+## 11.0.2 (2018-06-26)
+
+### Fixed (8 changes, 1 of them is from the community)
+
+- Serve favicon image always from the main GitLab domain to avoid issues with CORS. !19810 (Alexis Reigel)
+- Specify chart version when installing applications on Clusters. !20010
+- Fix invalid fuzzy translations being generated during installation. !20048
+- Fix incremental rollouts for Auto DevOps. !20061
+- Notify conflict for only open merge request. !20125
+- Only load Omniauth if enabled. !20132
+- Fix sorting by name on explore projects page. !20162
+- Fix alert button styling so that they don't show up white.
+
+### Performance (1 change)
+
+- Remove performance bottleneck preventing large wiki pages from displaying. !20174
+
+### Added (1 change)
+
+- Add support for verifying remote uploads, artifacts, and LFS objects in check rake tasks. !19501
+
+
+## 11.0.1 (2018-06-21)
+
+### Security (5 changes)
+
+- Fix XSS vulnerability for table of content generation.
+- Update sanitize gem to 4.6.5 to fix HTML injection vulnerability.
+- HTML escape branch name in project graphs page.
+- HTML escape the name of the user in ProjectsHelper#link_to_member.
+- Don't show events from internal projects for anonymous users in public feed.
+
+
+## 11.0.0 (2018-06-22)
+
+### Security (3 changes)
+
+- Fix API to remove deploy key from project instead of deleting it entirely.
+- Fixed bug that allowed importing arbitrary project attributes.
+- Prevent user passwords from being changed without providing the previous password.
+
+### Removed (2 changes)
+
+- Removed API v3 from the codebase. !18970
+- Removes outdated `g t` shortcut for TODO in favor of `Shift+T`. !19002
+
+### Fixed (69 changes, 23 of them are from the community)
+
+- Optimize the upload migration proces. !15947
+- Import bitbucket issues that are reported by an anonymous user. !18199 (bartl)
+- Fix an issue where the notification email address would be set to an unconfirmed email address. !18474
+- Stop logging email information when emails are disabled. !18521 (Marc Shaw)
+- Fix double-brackets being linkified in wiki markdown. !18524 (brewingcode)
+- Use case in-sensitive ordering by name for dashboard. !18553 (@vedharish)
+- Fix width of contributors graphs. !18639 (Paul Vorbach)
+- Fix modal width of shorcuts help page. !18766 (Lars Greiss)
+- Add missing tooltip to creation date on container registry overview. !18767 (Lars Greiss)
+- Add missing migration for minimal Project build_timeout. !18775
+- Update commit status from external CI services less aggressively. !18802
+- Fix Runner contacted at tooltip cache. !18810
+- Added support for LFS Download in the importing process. !18871
+- Fix issue board bug with long strings in titles. !18924
+- Does not log failed sign-in attempts when the database is in read-only mode. !18957
+- Fixes 500 error on /estimate BIG_VALUE. !18964 (Jacopo Beschi @jacopo-beschi)
+- Forbid to patch traces for finished jobs. !18969
+- Do not allow to trigger manual actions that were skipped. !18985
+- Renamed 'Overview' to 'Project' in collapsed contextual navigation at a project level. !18996 (Constance Okoghenun)
+- Fixed bug where generated api urls didn't add the base url if set. !19003
+- Fixed badge api endpoint route when relative url is set. !19004
+- Fixes: Runners search input placeholder is cut off. !19015 (Jacopo Beschi @jacopo-beschi)
+- Exclude CI_PIPELINE_ID from variables supported in dynamic environment name. !19032
+- Updates updated_at on label changes. !19065 (Jacopo Beschi @jacopo-beschi)
+- Disallow updating job status if the job is not running. !19101
+- Fix FreeBSD can not upload artifacts due to wrong tmp path. !19148
+- Check for nil AutoDevOps when saving project CI/CD settings. !19190
+- Missing timeout value in object storage pre-authorization. !19201
+- Use strings as properties key in kubernetes service spec. !19265 (Jasper Maes)
+- Fixed HTTP_PROXY environment not honored when reading remote traces. !19282 (NLR)
+- Updates ReactiveCaching clear_reactive_caching method to clear both data and alive caching. !19311
+- Fixes the styling on the modal headers. !19312 (samdbeckham)
+- Fixes a spelling error on the new label page. !19316 (samdbeckham)
+- Rails5 fix arel from. !19340 (Jasper Maes)
+- Support rails5 in postgres indexes function and fix some migrations. !19400 (Jasper Maes)
+- Fix repository archive generation when hashed storage is enabled. !19441
+- Rails 5 fix unknown keywords: changes, key_id, project, gl_repository, action, secret_token, protocol. !19466 (Jasper Maes)
+- Rails 5 fix glob spec. !19469 (Jasper Maes)
+- Showing project import_status in a humanized form no longer gives an error. !19470
+- Make avatars/icons hidden on mobile. !19585 (Takuya Noguchi)
+- Fix active tab highlight when creating new merge request. !19781 (Jan Beckmann)
+- Fixes Web IDE button on merge requests when GitLab is installed with relative URL.
+- Unverified hover state color changed to black.
+- Fix &nbsp; after sign-in with Google button.
+- Don't trim incoming emails that create new issues. (Cameron Crockett)
+- Wrapping problem on the issues page has been fixed.
+- Fix resolvable check if note's commit could not be found.
+- Fix filename matching when processing file or blob search results.
+- Allow maintainers to retry pipelines on forked projects (if allowed in merge request).
+- Fix deletion of Object Store uploads.
+- Fix overflowing Failed Jobs table in sm viewports on IE11.
+- Adjust insufficient diff hunks being persisted on NoteDiffFile.
+- Render calendar feed inline when accessed from GitLab.
+- Line height fixed. (Murat Dogan)
+- Use upload ID for creating lease key for file uploaders.
+- Use Github repo visibility during import while respecting restricted visibility levels.
+- Adjust permitted params filtering on merge scheduling.
+- Fix unscrollable Markdown preview of WebIDE on Firefox.
+- Enforce UTF-8 encoding on user input in LogrageWithTimestamp formatter and filter out file content from logs.
+- Fix project destruction failing due to idle in transaction timeouts.
+- Add a unique and not null constraint on the project_features.project_id column.
+- Expire Wiki content cache after importing a repository.
+- Fix admin counters not working when PostgreSQL has secondaries.
+- Fix backup creation and restore for specific Rake tasks.
+- Fix cross-origin errors when attempting to download JavaScript attachments.
+- Fix api_json.log not always reporting the right HTTP status code.
+- Fix attr_encryption key settings.
+- Remove gray button styles.
+- Fix print styles for markdown pages.
+
+### Deprecated (4 changes)
+
+- Deprecate Gemnasium project service. !18954
+- Rephrasing Merge Request's 'allow edits from maintainer' functionality. !19061
+- Rename issue scope created-by-me to created_by_me, and assigned-to-me to assigned_to_me. !44799
+- Migrate any remaining jobs from deprecated `object_storage_upload` queue.
+
+### Changed (42 changes, 11 of them are from the community)
+
+- Add support for smarter system notes. !17164
+- Automatically accepts project/group invite by email after user signup. !17634 (Jacopo Beschi @jacopo-beschi)
+- Dynamically fetch GCP cluster creation parameters. !17806
+- Label list page redesign. !18466
+- Move discussion actions to the right for small viewports. !18476 (George Tsiolis)
+- Add 2FA filter to the group members page. !18483
+- made listing and showing public issue apis available without authentication. !18638 (haseebeqx)
+- Refactoring UrlValidators to include url blocking. !18686
+- Removed "(Beta)" from "Auto DevOps" messages. !18759
+- Expose runner ip address to runners API. !18799 (Lars Greiss)
+- Moves MR widget external link icon to the right. !18828 (Jacopo Beschi @jacopo-beschi)
+- Add support for 'active' setting on Runner Registration API endpoint. !18848
+- Add dot to separate system notes content. !18864
+- Remove modalbox confirmation when retrying a pipeline. !18879
+- Remove docker pull prefix from registry clipboard feature. !18933 (Lars Greiss)
+- Move project sidebar sub-entries 'Environments' and 'Kubernetes' from 'CI/CD' to a new entry 'Operations'. !18941
+- Updated icons for branch and tag names in commit details. !18953 (Constance Okoghenun)
+- Expose readme url in Project API. !18960 (Imre Farkas)
+- Changes keyboard shortcut of Activity feed to `g v`. !19002
+- Updated Mattermost integration to use API v4 and only allow creation of Mattermost slash commands in the current user's teams. !19043 (Harrison Healey)
+- Add shortcuts to Web IDE docs and modal. !19044
+- Rename merge request widget author component. !19079 (George Tsiolis)
+- Rename the Master role to Maintainer. !19080
+- Use "right now" for short time periods. !19095
+- Update 404 and 403 pages with helpful actions. !19096
+- Add username to terms message in git and API calls. !19126
+- Change the IDE file buttons for an "Open in file view" button. !19129 (Sam Beckham)
+- Removes redundant script failure message from Job page. !19138
+- Add flash notice if user has already accepted terms and allow users to continue to root path. !19156
+- Redesign group settings page into expandable sections. !19184
+- Hashed Storage: migration rake task now can be executed to specific project. !19268
+- Make CI job update entrypoint to work as keep-alive endpoint. !19543
+- Avoid checking the user format in every url validation. !19575
+- Apply notification settings level of groups to all child objects.
+- Support restoring repositories into gitaly.
+- Bump omniauth-gitlab to 1.0.3.
+- Move API group deletion to Sidekiq.
+- Improve Failed Jobs tab in the Pipeline detail page.
+- Add additional theme color options.
+- Include milestones from parent groups when assigning a milestone to an issue or merge request.
+- Restore API v3 user endpoint.
+- Hide merge request option in IDE when disabled.
+
+### Performance (28 changes, 1 of them is from the community)
+
+- Add backgound migration for filling nullfied file_store columns. !18557
+- Add a cronworker to rescue stale live traces. !18680
+- Move SquashBeforeMerge vue component. !18813 (George Tsiolis)
+- Add index on runner_type for ci_runners. !18897
+- Fix CarrierWave reads local files into memoery when migrates to ObjectStorage. !19102
+- Remove double-checked internal id generation. !19181
+- Throttle updates to Project#last_repository_updated_at. !19183
+- Add background migrations for archiving legacy job traces. !19194
+- Use NPM provided version of SortableJS. !19274
+- Improve performance of group issues filtering on GitLab.com. !19429
+- Improve performance of LFS integrity check. !19494
+- Fix an N+1 when loading user avatars.
+- Only preload member records for the relevant projects/groups/user in projects API.
+- Fix some sources of excessive query counts when calculating notification recipients.
+- Optimise PagesWorker usage.
+- Optimise paused runners to reduce amount of used requests.
+- Update runner cached informations without performing validations.
+- Improve performance of project pipelines pages.
+- Persist truncated note diffs on a new table.
+- Remove unused running_or_pending_build_count.
+- Remove N+1 query for author in issues API.
+- Eliminate N+1 queries with authors and push_data_payload in Events API.
+- Eliminate cached N+1 queries for projects in Issue API.
+- Eliminate N+1 queries for CI job artifacts in /api/prjoects/:id/pipelines/:pipeline_id/jobs.
+- Fix N+1 with source_projects in merge requests API.
+- Replace grape-route-helpers with our own grape-path-helpers.
+- Move PR IO operations out of a transaction.
+- Improve performance of GroupsController#show.
+
+### Added (25 changes, 10 of them are from the community)
+
+- Closes MR check out branch modal with escape. (19050)
+- Allow changing the default favicon to a custom icon. !14497 (Alexis Reigel)
+- Export assigned issues in iCalendar feed. !17783 (Imre Farkas)
+- When MR becomes unmergeable, notify and create todo for author and merge user. !18042
+- Display help text below auto devops domain with nip.io domain name (#45561). !18496
+- Add per-project pipeline id. !18558
+- New design for wiki page deletion confirmation. !18712 (Constance Okoghenun)
+- Updates updated_at on issuable when setting time spent. !18757 (Jacopo Beschi @jacopo-beschi)
+- Expose artifacts_expire_at field for job entity in api. !18872 (Semyon Pupkov)
+- Add support for variables expression pattern matching syntax. !18902
+- Add API endpoint to render markdown text. !18926 (@blackst0ne)
+- Add `Squash and merge` to GitLab Core (CE). !18956 (@blackst0ne)
+- Adds keyboard shortcut `g k` for Kubernetes on Project pages. !19002
+- Adds keyboard shortcut `g e` for Environments on Project pages. !19002
+- Setup graphql with initial project & merge request query. !19008
+- Adds JupyterHub to cluster applications. !19019
+- Added ability to search by wiki titles. !19112
+- Add Avatar API. !19121 (Imre Farkas)
+- Add variables to POST api/v4/projects/:id/pipeline. !19124 (Jacopo Beschi @jacopo-beschi)
+- Add deploy strategies to the Auto DevOps settings. !19172
+- Automatize Deploy Token creation for Auto Devops. !19507
+- Add anchor for incoming email regex.
+- Support direct_upload with S3 Multipart uploads.
+- Add Open in Xcode link for xcode repositories.
+- Add pipeline status to the status bar of the Web IDE.
+
+### Other (40 changes, 17 of them are from the community)
+
+- Expand documentation for Runners API. !16484
+- Order UsersController#projects.json by updated_at. !18227 (Takuya Noguchi)
+- Replace the `project/issues/references.feature` spinach test with an rspec analog. !18769 (@blackst0ne)
+- Replace the `project/merge_requests/references.feature` spinach test with an rspec analog. !18794 (@blackst0ne)
+- Replace the `project/deploy_keys.feature` spinach test with an rspec analog. !18796 (@blackst0ne)
+- Replace the `project/ff_merge_requests.feature` spinach test with an rspec analog. !18800 (@blackst0ne)
+- Apply NestingDepth (level 5) (pages/pipelines.scss). !18830 (Takuya Noguchi)
+- Replace the `project/forked_merge_requests.feature` spinach test with an rspec analog. !18867 (@blackst0ne)
+- Remove Spinach. !18869 (@blackst0ne)
+- Add NOT NULL constraints to project_authorizations. !18980
+- Add helpful messages to empty wiki view. !19007
+- Increase text limit for GPG keys (mysql only). !19069
+- Take two for MR metrics population background migration. !19097
+- Remove Gemnasium badge from project README.md. !19136 (Takuya Noguchi)
+- Update awesome_print to 1.8.0. !19163 (Takuya Noguchi)
+- Update email_spec to 2.2.0. !19164 (Takuya Noguchi)
+- Update redis-namespace to 1.6.0. !19166 (Takuya Noguchi)
+- Update rdoc to 6.0.4. !19167 (Takuya Noguchi)
+- Updates the version of kubeclient from 3.0 to 3.1.0. !19199
+- Fix UI broken in line profiling modal due to Bootstrap 4. !19253 (Takuya Noguchi)
+- Add migration to disable the usage of DSA keys. !19299
+- Use the default strings of timeago.js for timeago. !19350 (Takuya Noguchi)
+- Update selenium-webdriver to 3.12.0. !19351 (Takuya Noguchi)
+- Include username in output when testing SSH to GitLab. !19358
+- Update screenshot in Gitlab.com integration documentation. !19433 (Tuğçe Nur Taş)
+- Users can accept terms during registration. !19583
+- Fix issue count on sidebar.
+- Add merge requests list endpoint for groups.
+- Upgrade GitLab from Bootstrap 3 to 4.
+- Make ActiveRecordSubscriber rails 5 compatible.
+- Show a more helpful error for import status.
+- Log response body to production_json.log when a controller responds with a 422 status.
+- Log Workhorse queue duration for Grape API calls.
+- Adjust SQL and transaction Prometheus buckets.
+- Adding branches through the WebUI is handled by Gitaly.
+- Remove shellout implementation for Repository checksums.
+- Refs containting sha checks are done by Gitaly.
+- Finding a wiki page is done by Gitaly by default.
+- Workhorse will use Gitaly to create archives.
+- Workhorse to send raw diff and patch for commits.
+
+
+## 10.8.5 (2018-06-21)
+
+### Security (5 changes)
+
+- Fix XSS vulnerability for table of content generation.
+- Update sanitize gem to 4.6.5 to fix HTML injection vulnerability.
+- HTML escape branch name in project graphs page.
+- HTML escape the name of the user in ProjectsHelper#link_to_member.
+- Don't show events from internal projects for anonymous users in public feed.
+
+
+## 10.8.4 (2018-06-06)
+
+- No changes.
+
+## 10.8.3 (2018-05-30)
+
+### Fixed (4 changes)
+
+- Replace Gitlab::REVISION with Gitlab.revision and handle installations without a .git directory. !19125
+- Fix encoding of branch names on compare and new merge request page. !19143
+- Fix remote mirror database inconsistencies when upgrading from EE to CE. !19196
+- Fix local storage not being cleared after creating a new issue.
+
+### Performance (1 change)
+
+- Memoize Gitlab::Database.version.
+
+
+## 10.8.2 (2018-05-28)
+
+### Security (3 changes)
+
+- Prevent user passwords from being changed without providing the previous password.
+- Fix API to remove deploy key from project instead of deleting it entirely.
+- Fixed bug that allowed importing arbitrary project attributes.
+
+
## 10.8.1 (2018-05-23)
### Fixed (9 changes)
@@ -193,6 +524,31 @@ entry.
- Gitaly handles repository forks by default.
+## 10.7.6 (2018-06-21)
+
+### Security (6 changes)
+
+- Fix XSS vulnerability for table of content generation.
+- Update sanitize gem to 4.6.5 to fix HTML injection vulnerability.
+- HTML escape branch name in project graphs page.
+- HTML escape the name of the user in ProjectsHelper#link_to_member.
+- Don't show events from internal projects for anonymous users in public feed.
+- XSS fix to use safe_params instead of params in url_for helpers.
+
+### Other (1 change)
+
+- Replacing gollum libraries for gitlab custom libs. !18343
+
+
+## 10.7.5 (2018-05-28)
+
+### Security (3 changes)
+
+- Prevent user passwords from being changed without providing the previous password.
+- Fix API to remove deploy key from project instead of deleting it entirely.
+- Fixed bug that allowed importing arbitrary project attributes.
+
+
## 10.7.4 (2018-05-21)
### Fixed (1 change)
@@ -457,6 +813,16 @@ entry.
- Upgrade Gitaly to upgrade its charlock_holmes.
+## 10.6.6 (2018-05-28)
+
+### Security (4 changes)
+
+- Do not allow non-members to create MRs via forked projects when MRs are private.
+- Prevent user passwords from being changed without providing the previous password.
+- Fix API to remove deploy key from project instead of deleting it entirely.
+- Fixed bug that allowed importing arbitrary project attributes.
+
+
## 10.6.5 (2018-04-24)
### Security (1 change)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 383d13656e2..fd4e769ecee 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -27,25 +27,26 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._
- [Helping others](#helping-others)
- [I want to contribute!](#i-want-to-contribute)
- [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, ~Quality, ~Platform, etc.)](#team-labels-cicd-discussion-quality-platform-etc)
- - [Milestone labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#milestone-labels-deliverable-stretch-next-patch-release)
- - [Priority labels (~P1, ~P2, ~P3 , ~P4)](#bug-priority-labels-p1-p2-p3-p4)
- - [Severity labels (~S1, ~S2, ~S3 , ~S4)](#bug-severity-labels-s1-s2-s3-s4)
- - [Label for community contributors (~"Accepting Merge Requests")](#label-for-community-contributors-accepting-merge-requests)
-- [Implement design & UI elements](#implement-design--ui-elements)
+ - [Type labels](#type-labels)
+ - [Subject labels](#subject-labels)
+ - [Team labels](#team-labels)
+ - [Release Scoping labels](#release-scoping-labels)
+ - [Bug Priority labels](#bug-priority-labels)
+ - [Bug Severity labels](#bug-severity-labels)
+ - [Severity impact guidance](#severity-impact-guidance)
+ - [Label for community contributors](#label-for-community-contributors)
+- [Implement design & UI elements](#implement-design-ui-elements)
- [Issue tracker](#issue-tracker)
- - [Issue triaging](#issue-triaging)
- - [Feature proposals](#feature-proposals)
- - [Issue tracker guidelines](#issue-tracker-guidelines)
- - [Issue weight](#issue-weight)
- - [Regression issues](#regression-issues)
- - [Technical and UX debt](#technical-and-ux-debt)
- - [Stewardship](#stewardship)
+ - [Issue triaging](#issue-triaging)
+ - [Feature proposals](#feature-proposals)
+ - [Issue tracker guidelines](#issue-tracker-guidelines)
+ - [Issue weight](#issue-weight)
+ - [Regression issues](#regression-issues)
+ - [Technical and UX debt](#technical-and-ux-debt)
+ - [Stewardship](#stewardship)
- [Merge requests](#merge-requests)
- - [Merge request guidelines](#merge-request-guidelines)
- - [Contribution acceptance criteria](#contribution-acceptance-criteria)
+ - [Merge request guidelines](#merge-request-guidelines)
+ - [Contribution acceptance criteria](#contribution-acceptance-criteria)
- [Definition of done](#definition-of-done)
- [Style guides](#style-guides)
- [Code of conduct](#code-of-conduct)
@@ -132,7 +133,7 @@ Most issues will have labels for at least one of the following:
- Type: ~"feature proposal", ~bug, ~customer, etc.
- Subject: ~wiki, ~"container registry", ~ldap, ~api, ~frontend, etc.
- Team: ~"CI/CD", ~Discussion, ~Quality, ~Platform, etc.
-- Milestone: ~Deliverable, ~Stretch, ~"Next Patch Release"
+- Release Scoping: ~Deliverable, ~Stretch, ~"Next Patch Release"
- Priority: ~P1, ~P2, ~P3, ~P4
- Severity: ~S1, ~S2, ~S3, ~S4
@@ -145,7 +146,7 @@ labels, you can _always_ add the team and type, and often also the subject.
[milestones-page]: https://gitlab.com/gitlab-org/gitlab-ce/milestones
[labels-page]: https://gitlab.com/gitlab-org/gitlab-ce/labels
-### Type labels (~"feature proposal", ~bug, ~customer, etc.)
+### Type labels
Type labels are very important. They define what kind of issue this is. Every
issue should have one or more.
@@ -161,28 +162,41 @@ already reserved for subject labels).
The descriptions on the [labels page][labels-page] explain what falls under each type label.
-### Subject labels (~wiki, ~"container registry", ~ldap, ~api, etc.)
+### Subject labels
Subject labels are labels that define what area or feature of GitLab this issue
hits. They are not always necessary, but very convenient.
+Examples of subject labels are ~wiki, ~ldap, ~api,
+~issues, ~"merge requests", ~labels, and ~"container registry".
+
If you are an expert in a particular area, it makes it easier to find issues to
work on. You can also subscribe to those labels to receive an email each time an
issue is labeled with a subject label corresponding to your expertise.
-Examples of subject labels are ~wiki, ~"container registry", ~ldap, ~api,
-~issues, ~"merge requests", ~labels, and ~"container registry".
-
Subject labels are always all-lowercase.
-### Team labels (~"CI/CD", ~Discussion, ~Quality, ~Platform, etc.)
+### Team labels
Team labels specify what team is responsible for this issue.
Assigning a team label makes sure issues get the attention of the appropriate
people.
-The current team labels are ~Build, ~"CI/CD", ~Discussion, ~Documentation, ~Quality,
-~Geo, ~Gitaly, ~Monitoring, ~Platform, ~Release, ~"Security Products" and ~"UX".
+The current team labels are:
+
+- ~Configuration
+- ~"CI/CD"
+- ~Discussion
+- ~Distribution
+- ~Documentation
+- ~Geo
+- ~Gitaly
+- ~Monitoring
+- ~Platform
+- ~Quality
+- ~Release
+- ~"Security Products"
+- ~UX
The descriptions on the [labels page][labels-page] explain what falls under the
responsibility of each team.
@@ -193,10 +207,10 @@ indicate if an issue needs backend work, frontend work, or both.
Team labels are always capitalized so that they show up as the first label for
any issue.
-### Milestone labels (~Deliverable, ~Stretch, ~"Next Patch Release")
+### Release Scoping labels
-Milestone labels help us clearly communicate expectations of the work for the
-release. There are three levels of Milestone labels:
+Release Scoping labels help us clearly communicate expectations of the work for the
+release. There are three levels of Release Scoping labels:
- ~Deliverable: Issues that are expected to be delivered in the current
milestone.
@@ -211,9 +225,9 @@ Each issue scheduled for the current milestone should be labeled ~Deliverable
or ~"Stretch". Any open issue for a previous milestone should be labeled
~"Next Patch Release", or otherwise rescheduled to a different milestone.
-### Bug Priority labels (~P1, ~P2, ~P3, ~P4)
+### Bug Priority labels
-Bug Priority labels help us define the time a ~bug fix should be completed. Priority determines how quickly the defect turnaround time must be.
+Bug Priority labels help us define the time a ~bug fix should be completed. Priority determines how quickly the defect turnaround time must be.
If there are multiple defects, the priority decides which defect has to be fixed immediately versus later.
This label documents the planned timeline & urgency which is used to measure against our actual SLA on delivering ~bug fixes.
@@ -224,7 +238,7 @@ This label documents the planned timeline & urgency which is used to measure aga
| ~P3 | Medium Priority | Within the next 3 releases (approx one quarter) | |
| ~P4 | Low Priority | Anything outside the next 3 releases (approx beyond one quarter) | The issue is prominent but does not impact user workflow and a workaround is documented |
-### Bug Severity labels (~S1, ~S2, ~S3, ~S4)
+### Bug Severity labels
Severity labels help us clearly communicate the impact of a ~bug on users.
@@ -240,11 +254,11 @@ Severity labels help us clearly communicate the impact of a ~bug on users.
| Label | Security Impact | Availability / Performance Impact |
|-------|---------------------------------------------------------------------|--------------------------------------------------------------|
| ~S1 | >50% users impacted (possible company extinction level event) | |
-| ~S2 | Many users or multiple paid customers impacted (but not apocalyptic)| The issue is (almost) guaranteed to occur in the near future |
+| ~S2 | Many users or multiple paid customers impacted (but not apocalyptic)| The issue is (almost) guaranteed to occur in the near future |
| ~S3 | A few users or a single paid customer impacted | The issue is likely to occur in the near future |
| ~S4 | No paid users/customer impacted, or expected impact within 30 days | The issue _may_ occur but it's not likely |
-### Label for community contributors (~"Accepting Merge Requests")
+### Label for community contributors
Issues that are beneficial to our users, 'nice to haves', that we currently do
not have the capacity for or want to give the priority to, are labeled as
@@ -300,20 +314,29 @@ For guidance on UX implementation at GitLab, please refer to our [Design System]
The UX team uses labels to manage their workflow.
-The ~"UX" label on an issue is a signal to the UX team that it will need UX attention.
-To better understand the priority by which UX tackles issues, see the [UX section](https://about.gitlab.com/handbook/ux/) of the handbook.
+The ~"UX" label on an issue is a signal to the UX team that it will need UX attention.
+To better understand the priority by which UX tackles issues, see the [UX section](https://about.gitlab.com/handbook/engineering/ux) of the handbook.
-Once an issue has been worked on and is ready for development, a UXer applies the ~"UX ready" label to that issue.
+Once an issue has been worked on and is ready for development, a UXer removes the ~"UX" label and applies the ~"UX ready" label to that issue.
-The UX team has a special type label called ~"design artifact". This label indicates that the final output
-for an issue is a UX solution/design. The solution will be developed by frontend and/or backend in a subsequent milestone.
-Any issue labeled ~"design artifact" should not also be labeled ~"frontend" or ~"backend" since no development is
+The UX team has a special type label called ~"design artifact". This label indicates that the final output
+for an issue is a UX solution/design. The solution will be developed by frontend and/or backend in a subsequent milestone.
+Any issue labeled ~"design artifact" should not also be labeled ~"frontend" or ~"backend" since no development is
needed until the solution has been decided.
~"design artifact" issues are like any other issue and should contain a milestone label, ~"Deliverable" or ~"Stretch", when scheduled in the current milestone.
-Once the ~"design artifact" issue has been completed, the UXer removes the ~"design artifact" label and applies the ~"UX ready" label. The Product Manager can use the
-existing issue or decide to create a whole new issue for the purpose of development.
+To prevent the misunderstanding that a feature will be be delivered in the
+assigned milestone, when only UX design is planned for that milestone, the
+Product Manager should create a separate issue for the ~"design artifact",
+assign the ~UX, ~"design artifact" and ~"Deliverable" labels, add a milestone
+and use a title that makes it clear that the scheduled issue is design only
+(e.g. `Design exploration for XYZ`).
+
+When the ~"design artifact" issue has been completed, the UXer removes the ~UX
+label, adds the ~"UX ready" label and closes the issue. This indicates the
+design artifact is complete. The UXer will also copy the designs to related
+issues for implementation in an upcoming milestone.
## Issue tracker
@@ -349,7 +372,7 @@ on those issues. Please select someone with relevant experience from the
[GitLab team][team]. If there is nobody mentioned with that expertise look in
the commit history for the affected files to find someone.
-[described in our handbook]: https://about.gitlab.com/handbook/engineering/issues/issue-triage-policies/
+[described in our handbook]: https://about.gitlab.com/handbook/engineering/issue-triage/
[issue bash events]: https://gitlab.com/gitlab-org/gitlab-ce/issues/17815
### Feature proposals
@@ -512,7 +535,7 @@ request is as follows:
1. Write [tests](https://docs.gitlab.com/ee/development/rake_tasks.html#run-tests) and code
1. [Generate a changelog entry with `bin/changelog`][changelog]
1. If you are writing documentation, make sure to follow the
- [documentation styleguide][doc-styleguide]
+ [documentation guidelines][doc-guidelines]
1. If you have multiple commits please combine them into a few logically
organized commits by [squashing them][git-squash]
1. Push the commit(s) to your fork
@@ -627,7 +650,7 @@ the feature you contribute through all of these steps.
1. Working and clean code that is commented where needed
1. [Unit, integration, and system tests][testing] that pass on the CI server
1. Performance/scalability implications have been considered, addressed, and tested
-1. [Documented][doc-styleguide] in the `/doc` directory
+1. [Documented][doc-guidelines] in the `/doc` directory
1. [Changelog entry added][changelog], if necessary
1. Reviewed and any concerns are addressed
1. Merged by a project maintainer
@@ -664,7 +687,7 @@ merge request:
contributors to enhance security
1. [Database Migrations](doc/development/migration_style_guide.md)
1. [Markdown](http://www.cirosantilli.com/markdown-styleguide)
-1. [Documentation styleguide][doc-styleguide]
+1. [Documentation styleguide](https://docs.gitlab.com/ee/development/documentation/styleguide.html)
1. Interface text should be written subjectively instead of objectively. It
should be the GitLab core team addressing a person. It should be written in
present time and never use past tense (has been/was). For example instead
@@ -727,7 +750,7 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
[rss-source]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#source-code-layout
[rss-naming]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#naming
[changelog]: doc/development/changelog.md "Generate a changelog entry"
-[doc-styleguide]: doc/development/doc_styleguide.md "Documentation styleguide"
+[doc-guidelines]: doc/development/documentation/index.md "Documentation guidelines"
[js-styleguide]: doc/development/fe_guide/style_guide_js.md "JavaScript styleguide"
[scss-styleguide]: doc/development/fe_guide/style_guide_scss.md "SCSS styleguide"
[newlines-styleguide]: doc/development/newlines_styleguide.md "Newlines styleguide"
diff --git a/Dangerfile b/Dangerfile
new file mode 100644
index 00000000000..84b72673c50
--- /dev/null
+++ b/Dangerfile
@@ -0,0 +1,6 @@
+danger.import_dangerfile(path: 'danger/metadata')
+danger.import_dangerfile(path: 'danger/changes_size')
+danger.import_dangerfile(path: 'danger/changelog')
+danger.import_dangerfile(path: 'danger/specs')
+danger.import_dangerfile(path: 'danger/gemfile')
+danger.import_dangerfile(path: 'danger/database')
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 7bb21aff834..e23e3fd2982 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0.102.0
+0.112.0
diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION
index f374f6662e9..3eefcb9dd5b 100644
--- a/GITLAB_PAGES_VERSION
+++ b/GITLAB_PAGES_VERSION
@@ -1 +1 @@
-0.9.1
+1.0.0
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index a8a18875682..69adf3456f8 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-7.1.2
+7.1.5
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index fae6e3d04b2..0062ac97180 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-4.2.1
+5.0.0
diff --git a/Gemfile b/Gemfile
index 6730ee9c164..d575568adaa 100644
--- a/Gemfile
+++ b/Gemfile
@@ -28,14 +28,14 @@ gem 'mysql2', '~> 0.4.10', group: :mysql
gem 'pg', '~> 0.18.2', group: :postgres
gem 'rugged', '~> 0.27'
-gem 'grape-route-helpers', '~> 2.1.0'
+gem 'grape-path-helpers', '~> 1.0'
gem 'faraday', '~> 0.12'
# Authentication libraries
gem 'devise', '~> 4.4'
gem 'doorkeeper', '~> 4.3'
-gem 'doorkeeper-openid_connect', '~> 1.3'
+gem 'doorkeeper-openid_connect', '~> 1.5'
gem 'omniauth', '~> 1.8'
gem 'omniauth-auth0', '~> 2.0.0'
gem 'omniauth-azure-oauth2', '~> 0.0.9'
@@ -47,10 +47,10 @@ gem 'omniauth-google-oauth2', '~> 0.5.3'
gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
gem 'omniauth-oauth2-generic', '~> 0.2.2'
gem 'omniauth-saml', '~> 1.10'
-gem 'omniauth-shibboleth', '~> 1.2.0'
+gem 'omniauth-shibboleth', '~> 1.3.0'
gem 'omniauth-twitter', '~> 1.4'
gem 'omniauth_crowd', '~> 2.2.0'
-gem 'omniauth-authentiq', '~> 0.3.1'
+gem 'omniauth-authentiq', '~> 0.3.3'
gem 'rack-oauth2', '~> 1.2.1'
gem 'jwt', '~> 1.5.6'
@@ -93,6 +93,10 @@ gem 'grape', '~> 1.0'
gem 'grape-entity', '~> 0.7.1'
gem 'rack-cors', '~> 1.0.0', require: 'rack/cors'
+# GraphQL API
+gem 'graphql', '~> 1.8.0'
+gem 'graphiql-rails', '~> 1.4.10'
+
# Disable strong_params so that Mash does not respond to :permitted?
gem 'hashie-forbidden_attributes'
@@ -100,10 +104,11 @@ gem 'hashie-forbidden_attributes'
gem 'kaminari', '~> 1.0'
# HAML
-gem 'hamlit', '~> 2.6.1'
+gem 'hamlit', '~> 2.8.8'
# Files attachments
gem 'carrierwave', '~> 1.2'
+gem 'mini_magick'
# Drag and Drop UI
gem 'dropzonejs-rails', '~> 0.7.1'
@@ -127,13 +132,13 @@ gem 'unf', '~> 0.1.4'
gem 'seed-fu', '~> 2.3.7'
# Markdown and HTML processing
-gem 'html-pipeline', '~> 2.7.1'
+gem 'html-pipeline', '~> 2.8'
gem 'deckar01-task_list', '2.0.0'
-gem 'gitlab-markup', '~> 1.6.2'
+gem 'gitlab-markup', '~> 1.6.4'
gem 'redcarpet', '~> 3.4'
gem 'commonmarker', '~> 0.17'
gem 'RedCloth', '~> 4.3.2'
-gem 'rdoc', '~> 4.2'
+gem 'rdoc', '~> 6.0'
gem 'org-ruby', '~> 0.9.12'
gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1'
@@ -144,6 +149,9 @@ gem 'truncato', '~> 0.7.9'
gem 'bootstrap_form', '~> 2.7.0'
gem 'nokogiri', '~> 1.8.2'
+# Calendar rendering
+gem 'icalendar'
+
# Diffs
gem 'diffy', '~> 3.1.0'
@@ -162,7 +170,7 @@ gem 'acts-as-taggable-on', '~> 5.0'
# Background jobs
gem 'sidekiq', '~> 5.1'
gem 'sidekiq-cron', '~> 0.6.0'
-gem 'redis-namespace', '~> 1.5.2'
+gem 'redis-namespace', '~> 1.6.0'
gem 'sidekiq-limit_fetch', '~> 3.4', require: false
# Cron Parser
@@ -219,10 +227,10 @@ gem 'asana', '~> 0.6.0'
gem 'ruby-fogbugz', '~> 0.2.1'
# Kubernetes integration
-gem 'kubeclient', '~> 3.0'
+gem 'kubeclient', '~> 3.1.0'
# Sanitize user input
-gem 'sanitize', '~> 2.0'
+gem 'sanitize', '~> 4.6.5'
gem 'babosa', '~> 1.0.2'
# Sanitizes SVG input
@@ -257,7 +265,6 @@ gem 'sass-rails', '~> 5.0.6'
gem 'uglifier', '~> 2.7.2'
gem 'addressable', '~> 2.5.2'
-gem 'bootstrap-sass', '~> 3.3.0'
gem 'font-awesome-rails', '~> 4.7'
gem 'gemojione', '~> 3.3'
gem 'gon', '~> 6.2'
@@ -292,7 +299,6 @@ gem 'peek-sidekiq', '~> 1.0.3'
# Metrics
group :metrics do
- gem 'allocations', '~> 1.0', require: false, platform: :mri
gem 'method_source', '~> 0.8', require: false
gem 'influxdb', '~> 0.2', require: false
@@ -321,12 +327,12 @@ group :development, :test do
gem 'pry-byebug', '~> 3.4.1', platform: :mri
gem 'pry-rails', '~> 0.3.4'
- gem 'awesome_print', '~> 1.2.0', require: false
+ gem 'awesome_print', require: false
gem 'fuubar', '~> 2.2.0'
gem 'database_cleaner', '~> 1.5.0'
gem 'factory_bot_rails', '~> 4.8.2'
- gem 'rspec-rails', '~> 3.6.0'
+ gem 'rspec-rails', '~> 3.7.0'
gem 'rspec-retry', '~> 0.4.5'
gem 'rspec_profiling', '~> 0.0.5'
gem 'rspec-set', '~> 0.1.3'
@@ -340,14 +346,14 @@ group :development, :test do
gem 'capybara', '~> 2.15'
gem 'capybara-screenshot', '~> 1.0.0'
- gem 'selenium-webdriver', '~> 3.5'
+ gem 'selenium-webdriver', '~> 3.12'
gem 'spring', '~> 2.0.0'
gem 'spring-commands-rspec', '~> 1.0.4'
- gem 'gitlab-styles', '~> 2.3', require: false
+ gem 'gitlab-styles', '~> 2.4', require: false
# Pin these dependencies, otherwise a new rule could break the CI pipelines
- gem 'rubocop', '~> 0.52.1'
+ gem 'rubocop', '~> 0.54.0'
gem 'rubocop-rspec', '~> 1.22.1'
gem 'scss_lint', '~> 0.56.0', require: false
@@ -372,7 +378,7 @@ end
group :test do
gem 'shoulda-matchers', '~> 3.1.2', require: false
- gem 'email_spec', '~> 1.6.0'
+ gem 'email_spec', '~> 2.2.0'
gem 'json-schema', '~> 2.8.0'
gem 'webmock', '~> 2.3.2'
gem 'rails-controller-testing' if rails5? # Rails5 only gem.
@@ -382,7 +388,7 @@ group :test do
gem 'test-prof', '~> 0.2.5'
end
-gem 'octokit', '~> 4.8'
+gem 'octokit', '~> 4.9'
gem 'mail_room', '~> 0.9.1'
@@ -402,18 +408,17 @@ gem 'vmstat', '~> 2.3.0'
gem 'sys-filesystem', '~> 1.1.6'
# SSH host key support
-gem 'net-ssh', '~> 4.2.0'
+gem 'net-ssh', '~> 5.0'
gem 'sshkey', '~> 1.9.0'
# Required for ED25519 SSH host key support
group :ed25519 do
- gem 'rbnacl-libsodium'
- gem 'rbnacl', '~> 4.0'
+ gem 'ed25519', '~> 1.2'
gem 'bcrypt_pbkdf', '~> 1.0'
end
# Gitaly GRPC client
-gem 'gitaly-proto', '~> 0.99.0', require: 'gitaly'
+gem 'gitaly-proto', '~> 0.105.0', require: 'gitaly'
gem 'grpc', '~> 1.11.0'
# Locked until https://github.com/google/protobuf/issues/4210 is closed
diff --git a/Gemfile.lock b/Gemfile.lock
index 9c2ef9dfa91..1b8a777ceb4 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -49,7 +49,6 @@ GEM
public_suffix (>= 2.0.2, < 4.0)
aes_key_wrap (1.0.1)
akismet (2.0.0)
- allocations (1.0.5)
arel (6.0.4)
asana (0.6.0)
faraday (~> 0.9)
@@ -69,10 +68,7 @@ GEM
attr_encrypted (3.1.0)
encryptor (~> 3.0.0)
attr_required (1.0.0)
- autoprefixer-rails (6.2.3)
- execjs
- json
- awesome_print (1.2.0)
+ awesome_print (1.8.0)
axiom-types (0.1.1)
descendants_tracker (~> 0.0.4)
ice_nine (~> 0.11.0)
@@ -80,7 +76,7 @@ GEM
babosa (1.0.2)
base32 (0.3.2)
batch-loader (1.2.1)
- bcrypt (3.1.11)
+ bcrypt (3.1.12)
bcrypt_pbkdf (1.0.0)
benchmark-ips (2.3.0)
better_errors (2.1.1)
@@ -91,9 +87,6 @@ GEM
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)
bootstrap_form (2.7.0)
brakeman (4.2.1)
browser (2.2.0)
@@ -115,13 +108,13 @@ GEM
capybara-screenshot (1.0.14)
capybara (>= 1.0, < 3)
launchy
- carrierwave (1.2.1)
+ carrierwave (1.2.3)
activemodel (>= 4.0.0)
activesupport (>= 4.0.0)
mime-types (>= 1.16)
cause (0.1)
charlock_holmes (0.7.6)
- childprocess (0.7.0)
+ childprocess (0.9.0)
ffi (~> 1.0, >= 1.0.11)
chronic (0.10.2)
chronic_duration (0.10.6)
@@ -174,19 +167,21 @@ GEM
diff-lcs (1.3)
diffy (3.1.0)
docile (1.1.5)
- domain_name (0.5.20170404)
+ domain_name (0.5.20180417)
unf (>= 0.0.5, < 1.0.0)
doorkeeper (4.3.2)
railties (>= 4.2)
- doorkeeper-openid_connect (1.3.0)
+ doorkeeper-openid_connect (1.5.0)
doorkeeper (~> 4.3)
json-jwt (~> 1.6)
dropzonejs-rails (0.7.2)
rails (> 3.1)
+ ed25519 (1.2.4)
email_reply_trimmer (0.1.6)
- email_spec (1.6.0)
+ email_spec (2.2.0)
+ htmlentities (~> 4.3.3)
launchy (~> 2.1)
- mail (~> 2.2)
+ mail (~> 2.7)
encryptor (3.0.0)
equalizer (0.0.11)
erubis (2.7.0)
@@ -287,7 +282,7 @@ GEM
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
- gitaly-proto (0.99.0)
+ gitaly-proto (0.105.0)
google-protobuf (~> 3.1)
grpc (~> 1.10)
github-linguist (5.3.3)
@@ -300,15 +295,15 @@ GEM
flowdock (~> 0.7)
gitlab-grit (>= 2.4.1)
multi_json
- gitlab-gollum-lib (4.2.7.2)
+ gitlab-gollum-lib (4.2.7.5)
gemojione (~> 3.2)
github-markup (~> 1.6)
gollum-grit_adapter (~> 1.0)
nokogiri (>= 1.6.1, < 2.0)
rouge (~> 3.1)
- sanitize (~> 2.1)
+ sanitize (~> 4.6.4)
stringex (~> 2.6)
- gitlab-gollum-rugged_adapter (0.4.4)
+ gitlab-gollum-rugged_adapter (0.4.4.1)
mime-types (>= 1.15)
rugged (~> 0.25)
gitlab-grit (2.8.2)
@@ -316,9 +311,9 @@ GEM
diff-lcs (~> 1.1)
mime-types (>= 1.16)
posix-spawn (~> 0.3)
- gitlab-markup (1.6.3)
- gitlab-styles (2.3.2)
- rubocop (~> 0.51)
+ gitlab-markup (1.6.4)
+ gitlab-styles (2.4.1)
+ rubocop (~> 0.54.0)
rubocop-gitlab-security (~> 0.1.0)
rubocop-rspec (~> 1.19)
gitlab_omniauth-ldap (2.0.4)
@@ -354,7 +349,7 @@ GEM
signet (~> 0.7)
gpgme (2.0.13)
mini_portile2 (~> 2.1)
- grape (1.0.2)
+ grape (1.0.3)
activesupport
builder
mustermann-grape (~> 1.0.0)
@@ -364,12 +359,16 @@ GEM
grape-entity (0.7.1)
activesupport (>= 4.0)
multi_json (>= 1.3.2)
- grape-route-helpers (2.1.0)
- activesupport
- grape (>= 0.16.0)
- rake
+ grape-path-helpers (1.0.5)
+ activesupport (>= 4, < 5.1)
+ grape (~> 1.0)
+ rake (~> 12)
grape_logging (1.7.0)
grape
+ graphiql-rails (1.4.10)
+ railties
+ sprockets-rails
+ graphql (1.8.1)
grpc (1.11.0)
google-protobuf (~> 3.1)
googleapis-common-protos-types (~> 1.0.0)
@@ -382,8 +381,8 @@ GEM
rake (>= 10, < 13)
rubocop (>= 0.49.0)
sysexits (~> 1.1)
- hamlit (2.6.1)
- temple (~> 0.7.6)
+ hamlit (2.8.8)
+ temple (>= 0.8.0)
thor
tilt
hashdiff (0.3.4)
@@ -395,7 +394,7 @@ GEM
hipchat (1.5.2)
httparty
mimemagic
- html-pipeline (2.7.1)
+ html-pipeline (2.8.3)
activesupport (>= 2)
nokogiri (>= 1.4)
html2text (0.2.0)
@@ -416,6 +415,7 @@ GEM
httpclient (2.8.3)
i18n (0.9.5)
concurrent-ruby (~> 1.0)
+ icalendar (2.4.1)
ice_nine (0.11.2)
influxdb (0.2.3)
cause
@@ -427,12 +427,10 @@ GEM
oauth (~> 0.5, >= 0.5.0)
jquery-atwho-rails (1.3.2)
json (1.8.6)
- json-jwt (1.9.2)
+ json-jwt (1.9.4)
activesupport
aes_key_wrap
bindata
- securecompare
- url_safe_base64
json-schema (2.8.0)
addressable (>= 2.4)
jwt (1.5.6)
@@ -451,10 +449,9 @@ GEM
kgio (2.10.0)
knapsack (1.16.0)
rake
- timecop (>= 0.1.0)
- kubeclient (3.0.0)
+ kubeclient (3.1.0)
http (~> 2.2.2)
- recursive-open-struct (~> 1.0.4)
+ recursive-open-struct (~> 1.0, >= 1.0.4)
rest-client (~> 2.0)
launchy (2.4.3)
addressable (~> 2.3)
@@ -498,6 +495,7 @@ GEM
mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521)
mimemagic (0.3.0)
+ mini_magick (4.8.0)
mini_mime (1.0.0)
mini_portile2 (2.3.0)
minitest (5.7.0)
@@ -510,10 +508,12 @@ GEM
mustermann (~> 1.0.0)
mysql2 (0.4.10)
net-ldap (0.16.0)
- net-ssh (4.2.0)
+ net-ssh (5.0.1)
netrc (0.11.0)
- nokogiri (1.8.2)
+ nokogiri (1.8.3)
mini_portile2 (~> 2.3.0)
+ nokogumbo (1.5.0)
+ nokogiri
numerizer (0.1.1)
oauth (0.5.4)
oauth2 (1.4.0)
@@ -522,15 +522,16 @@ GEM
multi_json (~> 1.3)
multi_xml (~> 0.5)
rack (>= 1.2, < 3)
- octokit (4.8.0)
+ octokit (4.9.0)
sawyer (~> 0.8.0, >= 0.5.3)
omniauth (1.8.1)
hashie (>= 3.4.6, < 3.6.0)
rack (>= 1.6.2, < 3)
omniauth-auth0 (2.0.0)
omniauth-oauth2 (~> 1.4)
- omniauth-authentiq (0.3.1)
- omniauth-oauth2 (~> 1.3, >= 1.3.1)
+ omniauth-authentiq (0.3.3)
+ jwt (>= 1.5)
+ omniauth-oauth2 (>= 1.5)
omniauth-azure-oauth2 (0.0.9)
jwt (~> 1.0)
omniauth (~> 1.0)
@@ -544,7 +545,7 @@ GEM
omniauth-github (1.3.0)
omniauth (~> 1.5)
omniauth-oauth2 (>= 1.4.0, < 2.0)
- omniauth-gitlab (1.0.2)
+ omniauth-gitlab (1.0.3)
omniauth (~> 1.0)
omniauth-oauth2 (~> 1.0)
omniauth-google-oauth2 (0.5.3)
@@ -567,7 +568,7 @@ GEM
omniauth-saml (1.10.0)
omniauth (~> 1.3, >= 1.3.2)
ruby-saml (~> 1.7)
- omniauth-shibboleth (1.2.1)
+ omniauth-shibboleth (1.3.0)
omniauth (>= 1.0.0)
omniauth-twitter (1.4.0)
omniauth-oauth (~> 1.1)
@@ -695,16 +696,11 @@ GEM
ffi (>= 0.5.0, < 2)
rblineprof (0.3.6)
debugger-ruby_core_source (~> 1.3)
- rbnacl (4.0.2)
- ffi
- rbnacl-libsodium (1.0.11)
- rbnacl (>= 3.0.1)
- rdoc (4.2.2)
- json (~> 1.4)
+ rdoc (6.0.4)
re2 (1.1.1)
recaptcha (3.0.0)
json
- recursive-open-struct (1.0.5)
+ recursive-open-struct (1.1.0)
redcarpet (3.4.0)
redis (3.3.5)
redis-actionpack (5.0.2)
@@ -714,8 +710,8 @@ GEM
redis-activesupport (5.0.4)
activesupport (>= 3, < 6)
redis-store (>= 1.3, < 2)
- redis-namespace (1.5.2)
- redis (~> 3.0, >= 3.0.4)
+ redis-namespace (1.6.0)
+ redis (>= 3.0.4)
redis-rack (2.0.4)
rack (>= 1.5, < 3)
redis-store (>= 1.2, < 2)
@@ -745,51 +741,51 @@ GEM
chunky_png
rqrcode-rails3 (0.1.7)
rqrcode (>= 0.4.2)
- rspec (3.6.0)
- rspec-core (~> 3.6.0)
- rspec-expectations (~> 3.6.0)
- rspec-mocks (~> 3.6.0)
- rspec-core (3.6.0)
- rspec-support (~> 3.6.0)
- rspec-expectations (3.6.0)
+ rspec (3.7.0)
+ rspec-core (~> 3.7.0)
+ rspec-expectations (~> 3.7.0)
+ rspec-mocks (~> 3.7.0)
+ rspec-core (3.7.1)
+ rspec-support (~> 3.7.0)
+ rspec-expectations (3.7.0)
diff-lcs (>= 1.2.0, < 2.0)
- rspec-support (~> 3.6.0)
- rspec-mocks (3.6.0)
+ rspec-support (~> 3.7.0)
+ rspec-mocks (3.7.0)
diff-lcs (>= 1.2.0, < 2.0)
- rspec-support (~> 3.6.0)
+ rspec-support (~> 3.7.0)
rspec-parameterized (0.4.0)
binding_of_caller
parser
proc_to_ast
rspec (>= 2.13, < 4)
unparser
- rspec-rails (3.6.0)
+ rspec-rails (3.7.2)
actionpack (>= 3.0)
activesupport (>= 3.0)
railties (>= 3.0)
- rspec-core (~> 3.6.0)
- rspec-expectations (~> 3.6.0)
- rspec-mocks (~> 3.6.0)
- rspec-support (~> 3.6.0)
+ rspec-core (~> 3.7.0)
+ rspec-expectations (~> 3.7.0)
+ rspec-mocks (~> 3.7.0)
+ rspec-support (~> 3.7.0)
rspec-retry (0.4.5)
rspec-core
rspec-set (0.1.3)
- rspec-support (3.6.0)
+ rspec-support (3.7.1)
rspec_profiling (0.0.5)
activerecord
pg
rails
sqlite3
- rubocop (0.52.1)
+ rubocop (0.54.0)
parallel (~> 1.10)
- parser (>= 2.4.0.2, < 3.0)
+ parser (>= 2.5)
powerpack (~> 0.1)
rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1)
rubocop-gitlab-security (0.1.1)
rubocop (>= 0.51)
- rubocop-rspec (1.22.1)
+ rubocop-rspec (1.22.2)
rubocop (>= 0.52.1)
ruby-enum (0.7.2)
i18n
@@ -806,10 +802,12 @@ GEM
rubyzip (1.2.1)
rufus-scheduler (3.4.0)
et-orbi (~> 1.0)
- rugged (0.27.0)
+ rugged (0.27.2)
safe_yaml (1.0.4)
- sanitize (2.1.0)
+ sanitize (4.6.5)
+ crass (~> 1.0.2)
nokogiri (>= 1.4.4)
+ nokogumbo (~> 1.4)
sass (3.5.5)
sass-listen (~> 4.0.0)
sass-listen (4.0.0)
@@ -827,15 +825,14 @@ GEM
scss_lint (0.56.0)
rake (>= 0.9, < 13)
sass (~> 3.5.3)
- securecompare (1.0.0)
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)
+ selenium-webdriver (3.12.0)
childprocess (~> 0.5)
- rubyzip (~> 1.0)
+ rubyzip (~> 1.2)
sentry-raven (2.7.2)
faraday (>= 0.7.6, < 1.0)
settingslogic (2.0.9)
@@ -871,7 +868,7 @@ GEM
activesupport (>= 4.2)
spring-commands-rspec (1.0.4)
spring (>= 0.9.1)
- sprockets (3.7.1)
+ sprockets (3.7.2)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.2.1)
@@ -892,7 +889,7 @@ GEM
sys-filesystem (1.1.6)
ffi
sysexits (1.2.0)
- temple (0.7.7)
+ temple (0.8.0)
test-prof (0.2.5)
test_after_commit (1.1.0)
activerecord (>= 3.2)
@@ -903,7 +900,7 @@ GEM
rack (>= 1, < 3)
thor (0.19.4)
thread_safe (0.3.6)
- tilt (2.0.6)
+ tilt (2.0.8)
timecop (0.8.1)
timfel-krb5-auth (0.8.3)
toml (0.1.2)
@@ -923,7 +920,7 @@ GEM
unf (0.1.4)
unf_ext
unf_ext (0.0.7.5)
- unicode-display_width (1.3.0)
+ unicode-display_width (1.3.2)
unicorn (5.1.0)
kgio (~> 2.6)
raindrops (~> 0.7)
@@ -939,7 +936,6 @@ GEM
equalizer (~> 0.0.9)
parser (>= 2.3.1.2, < 2.6)
procto (~> 0.0.2)
- url_safe_base64 (0.2.2)
validates_hostname (1.0.6)
activerecord (>= 3.0)
activesupport (>= 3.0)
@@ -977,13 +973,12 @@ DEPENDENCIES
acts-as-taggable-on (~> 5.0)
addressable (~> 2.5.2)
akismet (~> 2.0)
- allocations (~> 1.0)
asana (~> 0.6.0)
asciidoctor (~> 1.5.6)
asciidoctor-plantuml (= 0.0.8)
asset_sync (~> 2.4)
attr_encrypted (~> 3.1.0)
- awesome_print (~> 1.2.0)
+ awesome_print
babosa (~> 1.0.2)
base32 (~> 0.3.0)
batch-loader (~> 1.2.1)
@@ -991,7 +986,6 @@ DEPENDENCIES
benchmark-ips (~> 2.3.0)
better_errors (~> 2.1.0)
binding_of_caller (~> 0.7.2)
- bootstrap-sass (~> 3.3.0)
bootstrap_form (~> 2.7.0)
brakeman (~> 4.2)
browser (~> 2.2)
@@ -1015,10 +1009,11 @@ DEPENDENCIES
devise-two-factor (~> 3.0.0)
diffy (~> 3.1.0)
doorkeeper (~> 4.3)
- doorkeeper-openid_connect (~> 1.3)
+ doorkeeper-openid_connect (~> 1.5)
dropzonejs-rails (~> 0.7.1)
+ ed25519 (~> 1.2)
email_reply_trimmer (~> 0.1)
- email_spec (~> 1.6.0)
+ email_spec (~> 2.2.0)
factory_bot_rails (~> 4.8.2)
faraday (~> 0.12)
fast_blank
@@ -1042,13 +1037,13 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
- gitaly-proto (~> 0.99.0)
+ gitaly-proto (~> 0.105.0)
github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2)
gitlab-gollum-rugged_adapter (~> 0.4.4)
- gitlab-markup (~> 1.6.2)
- gitlab-styles (~> 2.3)
+ gitlab-markup (~> 1.6.4)
+ gitlab-styles (~> 2.4)
gitlab_omniauth-ldap (~> 2.0.4)
gon (~> 6.2)
google-api-client (~> 0.19.8)
@@ -1056,17 +1051,20 @@ DEPENDENCIES
gpgme
grape (~> 1.0)
grape-entity (~> 0.7.1)
- grape-route-helpers (~> 2.1.0)
+ grape-path-helpers (~> 1.0)
grape_logging (~> 1.7)
+ graphiql-rails (~> 1.4.10)
+ graphql (~> 1.8.0)
grpc (~> 1.11.0)
haml_lint (~> 0.26.0)
- hamlit (~> 2.6.1)
+ hamlit (~> 2.8.8)
hashie-forbidden_attributes
health_check (~> 2.6.0)
hipchat (~> 1.5.0)
- html-pipeline (~> 2.7.1)
+ html-pipeline (~> 2.8)
html2text
httparty (~> 0.13.3)
+ icalendar
influxdb (~> 0.2)
jira-ruby (~> 1.4)
jquery-atwho-rails (~> 1.3.2)
@@ -1074,7 +1072,7 @@ DEPENDENCIES
jwt (~> 1.5.6)
kaminari (~> 1.0)
knapsack (~> 1.16)
- kubeclient (~> 3.0)
+ kubeclient (~> 3.1.0)
letter_opener_web (~> 1.3.0)
license_finder (~> 3.1)
licensee (~> 8.9)
@@ -1082,17 +1080,18 @@ DEPENDENCIES
loofah (~> 2.2)
mail_room (~> 0.9.1)
method_source (~> 0.8)
+ mini_magick
minitest (~> 5.7.0)
mousetrap-rails (~> 1.4.6)
mysql2 (~> 0.4.10)
net-ldap
- net-ssh (~> 4.2.0)
+ net-ssh (~> 5.0)
nokogiri (~> 1.8.2)
oauth2 (~> 1.4)
- octokit (~> 4.8)
+ octokit (~> 4.9)
omniauth (~> 1.8)
omniauth-auth0 (~> 2.0.0)
- omniauth-authentiq (~> 0.3.1)
+ omniauth-authentiq (~> 0.3.3)
omniauth-azure-oauth2 (~> 0.0.9)
omniauth-cas3 (~> 1.1.4)
omniauth-facebook (~> 4.0.0)
@@ -1102,7 +1101,7 @@ DEPENDENCIES
omniauth-kerberos (~> 0.3.0)
omniauth-oauth2-generic (~> 0.2.2)
omniauth-saml (~> 1.10)
- omniauth-shibboleth (~> 1.2.0)
+ omniauth-shibboleth (~> 1.3.0)
omniauth-twitter (~> 1.4)
omniauth_crowd (~> 2.2.0)
org-ruby (~> 0.9.12)
@@ -1128,25 +1127,23 @@ DEPENDENCIES
rainbow (~> 2.2)
raindrops (~> 0.18)
rblineprof (~> 0.3.6)
- rbnacl (~> 4.0)
- rbnacl-libsodium
- rdoc (~> 4.2)
+ rdoc (~> 6.0)
re2 (~> 1.1.1)
recaptcha (~> 3.0)
redcarpet (~> 3.4)
redis (~> 3.2)
- redis-namespace (~> 1.5.2)
+ redis-namespace (~> 1.6.0)
redis-rails (~> 5.0.2)
request_store (~> 1.3)
responders (~> 2.0)
rouge (~> 3.1)
rqrcode-rails3 (~> 0.1.7)
rspec-parameterized
- rspec-rails (~> 3.6.0)
+ rspec-rails (~> 3.7.0)
rspec-retry (~> 0.4.5)
rspec-set (~> 0.1.3)
rspec_profiling (~> 0.0.5)
- rubocop (~> 0.52.1)
+ rubocop (~> 0.54.0)
rubocop-rspec (~> 1.22.1)
ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.17.0)
@@ -1154,12 +1151,12 @@ DEPENDENCIES
ruby_parser (~> 3.8)
rufus-scheduler (~> 3.4)
rugged (~> 0.27)
- sanitize (~> 2.0)
+ sanitize (~> 4.6.5)
sass-rails (~> 5.0.6)
scss_lint (~> 0.56.0)
seed-fu (~> 2.3.7)
select2-rails (~> 3.5.9)
- selenium-webdriver (~> 3.5)
+ selenium-webdriver (~> 3.12)
sentry-raven (~> 2.7)
settingslogic (~> 2.0.9)
sham_rack (~> 1.3.6)
@@ -1197,4 +1194,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
- 1.16.1
+ 1.16.2
diff --git a/Gemfile.rails5.lock b/Gemfile.rails5.lock
index af7305619eb..766f2479ea5 100644
--- a/Gemfile.rails5.lock
+++ b/Gemfile.rails5.lock
@@ -3,7 +3,7 @@ GEM
specs:
RedCloth (4.3.2)
abstract_type (0.0.7)
- ace-rails-ap (4.1.4)
+ ace-rails-ap (4.1.2)
actioncable (5.0.7)
actionpack (= 5.0.7)
nio4r (>= 1.2, < 3.0)
@@ -52,14 +52,13 @@ GEM
public_suffix (>= 2.0.2, < 4.0)
aes_key_wrap (1.0.1)
akismet (2.0.0)
- allocations (1.0.5)
arel (7.1.4)
- asana (0.6.3)
+ asana (0.6.0)
faraday (~> 0.9)
faraday_middleware (~> 0.9)
faraday_middleware-multi_json (~> 0.0)
oauth2 (~> 1.0)
- asciidoctor (1.5.6.1)
+ asciidoctor (1.5.6.2)
asciidoctor-plantuml (0.0.8)
asciidoctor (~> 1.5)
asset_sync (2.4.0)
@@ -71,10 +70,8 @@ GEM
atomic (1.1.99)
attr_encrypted (3.1.0)
encryptor (~> 3.0.0)
- attr_required (1.0.1)
- autoprefixer-rails (8.1.0.1)
- execjs
- awesome_print (1.2.0)
+ attr_required (1.0.0)
+ awesome_print (1.8.0)
axiom-types (0.1.1)
descendants_tracker (~> 0.0.4)
ice_nine (~> 0.11.0)
@@ -82,7 +79,7 @@ GEM
babosa (1.0.2)
base32 (0.3.2)
batch-loader (1.2.1)
- bcrypt (3.1.11)
+ bcrypt (3.1.12)
bcrypt_pbkdf (1.0.0)
benchmark-ips (2.3.0)
better_errors (2.1.1)
@@ -90,15 +87,12 @@ GEM
erubis (>= 2.6.6)
rack (>= 0.9.0)
bindata (2.4.3)
- binding_of_caller (0.7.3)
+ binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
blankslate (2.1.2.4)
- bootstrap-sass (3.3.7)
- autoprefixer-rails (>= 5.2.1)
- sass (>= 3.3.4)
bootstrap_form (2.7.0)
brakeman (4.2.1)
- browser (2.5.3)
+ browser (2.2.0)
builder (3.2.3)
bullet (5.5.1)
activesupport (>= 3.0.0)
@@ -107,32 +101,33 @@ GEM
bundler (~> 1.2)
thor (~> 0.18)
byebug (9.0.6)
- capybara (2.18.0)
+ capybara (2.15.1)
addressable
mini_mime (>= 0.1.3)
nokogiri (>= 1.3.3)
rack (>= 1.0.0)
rack-test (>= 0.5.4)
- xpath (>= 2.0, < 4.0)
- capybara-screenshot (1.0.18)
+ xpath (~> 2.0)
+ capybara-screenshot (1.0.14)
capybara (>= 1.0, < 3)
launchy
- carrierwave (1.2.2)
+ carrierwave (1.2.3)
activemodel (>= 4.0.0)
activesupport (>= 4.0.0)
mime-types (>= 1.16)
- charlock_holmes (0.7.5)
+ cause (0.1)
+ charlock_holmes (0.7.6)
childprocess (0.9.0)
ffi (~> 1.0, >= 1.0.11)
chronic (0.10.2)
chronic_duration (0.10.6)
numerizer (~> 0.1.1)
- chunky_png (1.3.10)
+ chunky_png (1.3.5)
citrus (3.0.2)
- coderay (1.1.2)
+ coderay (1.1.1)
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
- commonmarker (0.17.9)
+ commonmarker (0.17.8)
ruby-enum (~> 0.5)
concord (0.1.5)
adamantium (~> 0.2.0)
@@ -145,11 +140,11 @@ GEM
safe_yaml (~> 1.0.0)
crass (1.0.4)
creole (0.5.0)
- css_parser (1.6.0)
+ css_parser (1.5.0)
addressable
- daemons (1.2.6)
+ daemons (1.2.3)
database_cleaner (1.5.3)
- debug_inspector (0.0.3)
+ debug_inspector (0.0.2)
debugger-ruby_core_source (1.3.8)
deckar01-task_list (2.0.0)
html-pipeline
@@ -159,44 +154,46 @@ GEM
activerecord (>= 3.2.0, < 5.2)
descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1)
- device_detector (1.0.1)
- devise (4.4.1)
+ device_detector (1.0.0)
+ devise (4.4.3)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
- railties (>= 4.1.0, < 5.2)
+ railties (>= 4.1.0, < 6.0)
responders
warden (~> 1.2.3)
- devise-two-factor (3.0.2)
- activesupport (< 5.2)
+ devise-two-factor (3.0.0)
+ activesupport
attr_encrypted (>= 1.3, < 4, != 2)
devise (~> 4.0)
- railties (< 5.2)
+ railties
rotp (~> 2.0)
diff-lcs (1.3)
diffy (3.1.0)
docile (1.1.5)
- domain_name (0.5.20170404)
+ domain_name (0.5.20180417)
unf (>= 0.0.5, < 1.0.0)
- doorkeeper (4.3.1)
+ doorkeeper (4.3.2)
railties (>= 4.2)
- doorkeeper-openid_connect (1.3.0)
+ doorkeeper-openid_connect (1.5.0)
doorkeeper (~> 4.3)
json-jwt (~> 1.6)
- dropzonejs-rails (0.7.4)
+ dropzonejs-rails (0.7.2)
rails (> 3.1)
- email_reply_trimmer (0.1.10)
- email_spec (1.6.0)
+ ed25519 (1.2.4)
+ email_reply_trimmer (0.1.6)
+ email_spec (2.2.0)
+ htmlentities (~> 4.3.3)
launchy (~> 2.1)
- mail (~> 2.2)
+ mail (~> 2.7)
encryptor (3.0.0)
equalizer (0.0.11)
erubis (2.7.0)
escape_utils (1.1.1)
- et-orbi (1.0.9)
+ et-orbi (1.0.3)
tzinfo
- eventmachine (1.2.5)
- excon (0.60.0)
- execjs (2.7.0)
+ eventmachine (1.0.8)
+ excon (0.62.0)
+ execjs (2.6.0)
expression_parser (0.9.0)
factory_bot (4.8.2)
activesupport (>= 3.0.0)
@@ -212,8 +209,8 @@ GEM
multi_json
fast_blank (1.0.0)
fast_gettext (1.6.0)
- ffaker (2.8.1)
- ffi (1.9.23)
+ ffaker (2.4.0)
+ ffi (1.9.18)
flay (2.10.0)
erubis (~> 2.7.0)
path_expander (~> 1.0)
@@ -251,13 +248,13 @@ GEM
fog-json (1.0.2)
fog-core (~> 1.0)
multi_json (~> 1.10)
- fog-local (0.5.0)
- fog-core (>= 1.27, < 3.0)
- fog-openstack (0.1.24)
- fog-core (~> 1.40)
+ fog-local (0.3.1)
+ fog-core (~> 1.27)
+ fog-openstack (0.1.21)
+ fog-core (>= 1.40)
fog-json (>= 1.0)
ipaddress (>= 0.8)
- fog-rackspace (0.1.5)
+ fog-rackspace (0.1.1)
fog-core (>= 1.35)
fog-json (>= 1.0)
fog-xml (>= 0.1)
@@ -265,8 +262,8 @@ GEM
fog-xml (0.1.3)
fog-core
nokogiri (>= 1.5.11, < 2.0.0)
- font-awesome-rails (4.7.0.3)
- railties (>= 3.2, < 5.2)
+ font-awesome-rails (4.7.0.1)
+ railties (>= 3.2, < 5.1)
foreman (0.84.0)
thor (~> 0.19.1)
formatador (0.2.5)
@@ -277,7 +274,7 @@ GEM
rugged (~> 0.21)
gemojione (3.3.0)
json
- get_process_mem (0.2.1)
+ get_process_mem (0.2.0)
gettext (3.2.9)
locale (>= 2.0.5)
text (>= 1.3.0)
@@ -288,7 +285,7 @@ GEM
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
- gitaly-proto (0.99.0)
+ gitaly-proto (0.105.0)
google-protobuf (~> 3.1)
grpc (~> 1.10)
github-linguist (5.3.3)
@@ -301,15 +298,15 @@ GEM
flowdock (~> 0.7)
gitlab-grit (>= 2.4.1)
multi_json
- gitlab-gollum-lib (4.2.7.2)
+ gitlab-gollum-lib (4.2.7.5)
gemojione (~> 3.2)
github-markup (~> 1.6)
gollum-grit_adapter (~> 1.0)
nokogiri (>= 1.6.1, < 2.0)
rouge (~> 3.1)
- sanitize (~> 2.1)
+ sanitize (~> 4.6.4)
stringex (~> 2.6)
- gitlab-gollum-rugged_adapter (0.4.4)
+ gitlab-gollum-rugged_adapter (0.4.4.1)
mime-types (>= 1.15)
rugged (~> 0.25)
gitlab-grit (2.8.2)
@@ -317,9 +314,9 @@ GEM
diff-lcs (~> 1.1)
mime-types (>= 1.16)
posix-spawn (~> 0.3)
- gitlab-markup (1.6.3)
- gitlab-styles (2.3.2)
- rubocop (~> 0.51)
+ gitlab-markup (1.6.4)
+ gitlab-styles (2.4.1)
+ rubocop (~> 0.54.0)
rubocop-gitlab-security (~> 0.1.0)
rubocop-rspec (~> 1.19)
gitlab_omniauth-ldap (2.0.4)
@@ -353,9 +350,9 @@ GEM
multi_json (~> 1.11)
os (~> 0.9)
signet (~> 0.7)
- gpgme (2.0.16)
- mini_portile2 (~> 2.3)
- grape (1.0.2)
+ gpgme (2.0.13)
+ mini_portile2 (~> 2.1)
+ grape (1.0.3)
activesupport
builder
mustermann-grape (~> 1.0.0)
@@ -365,12 +362,16 @@ GEM
grape-entity (0.7.1)
activesupport (>= 4.0)
multi_json (>= 1.3.2)
- grape-route-helpers (2.1.0)
- activesupport
- grape (>= 0.16.0)
- rake
+ grape-path-helpers (1.0.5)
+ activesupport (>= 4, < 5.1)
+ grape (~> 1.0)
+ rake (~> 12)
grape_logging (1.7.0)
grape
+ graphiql-rails (1.4.10)
+ railties
+ sprockets-rails
+ graphql (1.8.1)
grpc (1.11.0)
google-protobuf (~> 3.1)
googleapis-common-protos-types (~> 1.0.0)
@@ -383,23 +384,23 @@ GEM
rake (>= 10, < 13)
rubocop (>= 0.49.0)
sysexits (~> 1.1)
- hamlit (2.6.2)
- temple (~> 0.7.6)
+ hamlit (2.8.8)
+ temple (>= 0.8.0)
thor
tilt
- hashdiff (0.3.7)
+ hashdiff (0.3.4)
hashie (3.5.7)
hashie-forbidden_attributes (0.1.1)
hashie (>= 3.0)
health_check (2.6.0)
rails (>= 4.0)
- hipchat (1.5.4)
+ hipchat (1.5.2)
httparty
mimemagic
- html-pipeline (2.7.1)
+ html-pipeline (2.8.3)
activesupport (>= 2)
nokogiri (>= 1.4)
- html2text (0.2.1)
+ html2text (0.2.0)
nokogiri (~> 1.6)
htmlentities (4.3.4)
http (2.2.2)
@@ -417,48 +418,49 @@ GEM
httpclient (2.8.3)
i18n (1.0.1)
concurrent-ruby (~> 1.0)
+ icalendar (2.4.1)
ice_nine (0.11.2)
- influxdb (0.5.3)
+ influxdb (0.2.3)
+ cause
+ json
ipaddress (0.8.3)
- jira-ruby (1.5.0)
+ jira-ruby (1.4.1)
activesupport
multipart-post
oauth (~> 0.5, >= 0.5.0)
jquery-atwho-rails (1.3.2)
json (1.8.6)
- json-jwt (1.9.2)
+ json-jwt (1.9.4)
activesupport
aes_key_wrap
bindata
- securecompare
- url_safe_base64
json-schema (2.8.0)
addressable (>= 2.4)
jwt (1.5.6)
- kaminari (1.1.1)
+ kaminari (1.0.1)
activesupport (>= 4.1.0)
- kaminari-actionview (= 1.1.1)
- kaminari-activerecord (= 1.1.1)
- kaminari-core (= 1.1.1)
- kaminari-actionview (1.1.1)
+ kaminari-actionview (= 1.0.1)
+ kaminari-activerecord (= 1.0.1)
+ kaminari-core (= 1.0.1)
+ kaminari-actionview (1.0.1)
actionview
- kaminari-core (= 1.1.1)
- kaminari-activerecord (1.1.1)
+ kaminari-core (= 1.0.1)
+ kaminari-activerecord (1.0.1)
activerecord
- kaminari-core (= 1.1.1)
- kaminari-core (1.1.1)
- kgio (2.11.2)
+ kaminari-core (= 1.0.1)
+ kaminari-core (1.0.1)
+ kgio (2.10.0)
knapsack (1.16.0)
rake
- kubeclient (3.0.0)
+ kubeclient (3.1.0)
http (~> 2.2.2)
- recursive-open-struct (~> 1.0.4)
+ recursive-open-struct (~> 1.0, >= 1.0.4)
rest-client (~> 2.0)
launchy (2.4.3)
addressable (~> 2.3)
- letter_opener (1.6.0)
+ letter_opener (1.4.1)
launchy (~> 2.2)
- letter_opener_web (1.3.3)
+ letter_opener_web (1.3.0)
actionmailer (>= 3.2)
letter_opener (~> 1.0)
railties (>= 3.2)
@@ -477,7 +479,7 @@ GEM
logging (2.2.2)
little-plugger (~> 1.1)
multi_json (~> 1.10)
- lograge (0.9.0)
+ lograge (0.10.0)
actionpack (>= 4)
activesupport (>= 4)
railties (>= 4)
@@ -491,11 +493,12 @@ GEM
memoist (0.16.0)
memoizable (0.4.2)
thread_safe (~> 0.3, >= 0.3.1)
- method_source (0.9.0)
+ method_source (0.8.2)
mime-types (3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521)
- mimemagic (0.3.2)
+ mimemagic (0.3.0)
+ mini_magick (4.8.0)
mini_mime (1.0.0)
mini_portile2 (2.3.0)
minitest (5.7.0)
@@ -507,12 +510,14 @@ GEM
mustermann-grape (1.0.0)
mustermann (~> 1.0.0)
mysql2 (0.4.10)
- net-ldap (0.16.1)
- net-ssh (4.2.0)
+ net-ldap (0.16.0)
+ net-ssh (5.0.1)
netrc (0.11.0)
nio4r (2.3.1)
- nokogiri (1.8.2)
+ nokogiri (1.8.3)
mini_portile2 (~> 2.3.0)
+ nokogumbo (1.5.0)
+ nokogiri
numerizer (0.1.1)
oauth (0.5.4)
oauth2 (1.4.0)
@@ -521,15 +526,16 @@ GEM
multi_json (~> 1.3)
multi_xml (~> 0.5)
rack (>= 1.2, < 3)
- octokit (4.8.0)
+ octokit (4.9.0)
sawyer (~> 0.8.0, >= 0.5.3)
omniauth (1.8.1)
hashie (>= 3.4.6, < 3.6.0)
rack (>= 1.6.2, < 3)
omniauth-auth0 (2.0.0)
omniauth-oauth2 (~> 1.4)
- omniauth-authentiq (0.3.1)
- omniauth-oauth2 (~> 1.3, >= 1.3.1)
+ omniauth-authentiq (0.3.3)
+ jwt (>= 1.5)
+ omniauth-oauth2 (>= 1.5)
omniauth-azure-oauth2 (0.0.9)
jwt (~> 1.0)
omniauth (~> 1.0)
@@ -561,12 +567,12 @@ GEM
omniauth-oauth2 (1.5.0)
oauth2 (~> 1.1)
omniauth (~> 1.2)
- omniauth-oauth2-generic (0.2.4)
+ omniauth-oauth2-generic (0.2.2)
omniauth-oauth2 (~> 1.0)
omniauth-saml (1.10.0)
omniauth (~> 1.3, >= 1.3.2)
ruby-saml (~> 1.7)
- omniauth-shibboleth (1.2.1)
+ omniauth-shibboleth (1.3.0)
omniauth (>= 1.0.0)
omniauth-twitter (1.4.0)
omniauth-oauth (~> 1.1)
@@ -580,7 +586,7 @@ GEM
orm_adapter (0.5.0)
os (0.9.6)
parallel (1.12.1)
- parser (2.5.0.5)
+ parser (2.5.1.0)
ast (~> 2.4.0)
parslet (1.5.0)
blankslate (~> 2.0)
@@ -616,9 +622,9 @@ GEM
json (>= 1.6.0)
posix-spawn (0.3.13)
powerpack (0.1.1)
- premailer (1.11.1)
+ premailer (1.10.4)
addressable
- css_parser (>= 1.6.0)
+ css_parser (>= 1.4.10)
htmlentities (>= 4.0.0)
premailer-rails (1.9.7)
actionmailer (>= 3, < 6)
@@ -628,15 +634,16 @@ GEM
parser
unparser
procto (0.0.3)
- prometheus-client-mmap (0.9.2)
- pry (0.11.3)
+ prometheus-client-mmap (0.9.3)
+ pry (0.10.4)
coderay (~> 1.1.0)
- method_source (~> 0.9.0)
- pry-byebug (3.4.3)
- byebug (>= 9.0, < 9.1)
+ method_source (~> 0.8.1)
+ slop (~> 3.4)
+ pry-byebug (3.4.2)
+ byebug (~> 9.0)
pry (~> 0.10)
- pry-rails (0.3.6)
- pry (>= 0.10.4)
+ pry-rails (0.3.5)
+ pry (>= 0.9.10)
public_suffix (3.0.2)
pyu-ruby-sasl (0.0.3.3)
rack (2.0.5)
@@ -653,7 +660,7 @@ GEM
rack (>= 1.1)
rack-protection (2.0.1)
rack
- rack-proxy (0.6.4)
+ rack-proxy (0.6.0)
rack
rack-test (0.6.3)
rack (>= 1.0)
@@ -691,22 +698,18 @@ GEM
thor (>= 0.18.1, < 2.0)
rainbow (2.2.2)
rake
- raindrops (0.19.0)
+ raindrops (0.18.0)
rake (12.3.1)
- rb-fsevent (0.10.3)
+ rb-fsevent (0.10.2)
rb-inotify (0.9.10)
ffi (>= 0.5.0, < 2)
- rblineprof (0.3.7)
+ rblineprof (0.3.6)
debugger-ruby_core_source (~> 1.3)
- rbnacl (4.0.2)
- ffi
- rbnacl-libsodium (1.0.16)
- rbnacl (>= 3.0.1)
- rdoc (4.3.0)
+ rdoc (6.0.4)
re2 (1.1.1)
- recaptcha (3.4.0)
+ recaptcha (3.0.0)
json
- recursive-open-struct (1.0.5)
+ recursive-open-struct (1.1.0)
redcarpet (3.4.0)
redis (3.3.5)
redis-actionpack (5.0.2)
@@ -716,8 +719,8 @@ GEM
redis-activesupport (5.0.4)
activesupport (>= 3, < 6)
redis-store (>= 1.3, < 2)
- redis-namespace (1.5.3)
- redis (~> 3.0, >= 3.0.4)
+ redis-namespace (1.6.0)
+ redis (>= 3.0.4)
redis-rack (2.0.4)
rack (>= 1.5, < 3)
redis-store (>= 1.2, < 2)
@@ -731,8 +734,7 @@ GEM
declarative (< 0.1.0)
declarative-option (< 0.2.0)
uber (< 0.2.0)
- request_store (1.4.0)
- rack (>= 1.4)
+ request_store (1.3.1)
responders (2.4.0)
actionpack (>= 4.2.0, < 5.3)
railties (>= 4.2.0, < 5.3)
@@ -741,51 +743,51 @@ GEM
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
retriable (3.1.1)
- rinku (2.0.4)
+ rinku (2.0.0)
rotp (2.1.2)
rouge (3.1.1)
- rqrcode (0.10.1)
- chunky_png (~> 1.0)
+ rqrcode (0.7.0)
+ chunky_png
rqrcode-rails3 (0.1.7)
rqrcode (>= 0.4.2)
- rspec (3.6.0)
- rspec-core (~> 3.6.0)
- rspec-expectations (~> 3.6.0)
- rspec-mocks (~> 3.6.0)
- rspec-core (3.6.0)
- rspec-support (~> 3.6.0)
- rspec-expectations (3.6.0)
+ rspec (3.7.0)
+ rspec-core (~> 3.7.0)
+ rspec-expectations (~> 3.7.0)
+ rspec-mocks (~> 3.7.0)
+ rspec-core (3.7.1)
+ rspec-support (~> 3.7.0)
+ rspec-expectations (3.7.0)
diff-lcs (>= 1.2.0, < 2.0)
- rspec-support (~> 3.6.0)
- rspec-mocks (3.6.0)
+ rspec-support (~> 3.7.0)
+ rspec-mocks (3.7.0)
diff-lcs (>= 1.2.0, < 2.0)
- rspec-support (~> 3.6.0)
+ rspec-support (~> 3.7.0)
rspec-parameterized (0.4.0)
binding_of_caller
parser
proc_to_ast
rspec (>= 2.13, < 4)
unparser
- rspec-rails (3.6.1)
+ rspec-rails (3.7.2)
actionpack (>= 3.0)
activesupport (>= 3.0)
railties (>= 3.0)
- rspec-core (~> 3.6.0)
- rspec-expectations (~> 3.6.0)
- rspec-mocks (~> 3.6.0)
- rspec-support (~> 3.6.0)
- rspec-retry (0.4.6)
+ rspec-core (~> 3.7.0)
+ rspec-expectations (~> 3.7.0)
+ rspec-mocks (~> 3.7.0)
+ rspec-support (~> 3.7.0)
+ rspec-retry (0.4.5)
rspec-core
rspec-set (0.1.3)
- rspec-support (3.6.0)
+ rspec-support (3.7.1)
rspec_profiling (0.0.5)
activerecord
pg
rails
sqlite3
- rubocop (0.52.1)
+ rubocop (0.54.0)
parallel (~> 1.10)
- parser (>= 2.4.0.2, < 3.0)
+ parser (>= 2.5)
powerpack (~> 0.1)
rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.7)
@@ -802,23 +804,25 @@ GEM
ruby-progressbar (1.9.0)
ruby-saml (1.7.2)
nokogiri (>= 1.5.10)
- ruby_parser (3.11.0)
- sexp_processor (~> 4.9)
+ ruby_parser (3.9.0)
+ sexp_processor (~> 4.1)
rubyntlm (0.6.2)
- rubypants (0.7.0)
+ rubypants (0.2.0)
rubyzip (1.2.1)
- rufus-scheduler (3.4.2)
+ rufus-scheduler (3.4.0)
et-orbi (~> 1.0)
- rugged (0.27.0)
+ rugged (0.27.2)
safe_yaml (1.0.4)
- sanitize (2.1.0)
+ sanitize (4.6.5)
+ crass (~> 1.0.2)
nokogiri (>= 1.4.4)
+ nokogumbo (~> 1.4)
sass (3.5.5)
sass-listen (~> 4.0.0)
sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
- sass-rails (5.0.7)
+ sass-rails (5.0.6)
railties (>= 4.0.0, < 6)
sass (~> 3.1)
sprockets (>= 2.8, < 4.0)
@@ -830,29 +834,28 @@ GEM
scss_lint (0.56.0)
rake (>= 0.9, < 13)
sass (~> 3.5.3)
- securecompare (1.0.0)
seed-fu (2.3.7)
activerecord (>= 3.1)
activesupport (>= 3.1)
- select2-rails (3.5.10)
+ select2-rails (3.5.9.3)
thor (~> 0.14)
- selenium-webdriver (3.11.0)
+ selenium-webdriver (3.12.0)
childprocess (~> 0.5)
rubyzip (~> 1.2)
sentry-raven (2.7.2)
faraday (>= 0.7.6, < 1.0)
settingslogic (2.0.9)
- sexp_processor (4.10.1)
+ sexp_processor (4.9.0)
sham_rack (1.3.6)
rack
shoulda-matchers (3.1.2)
activesupport (>= 4.0.0)
- sidekiq (5.1.1)
+ sidekiq (5.1.3)
concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0)
rack-protection (>= 1.5.0)
redis (>= 3.3.5, < 5)
- sidekiq-cron (0.6.3)
+ sidekiq-cron (0.6.0)
rufus-scheduler (>= 3.3.0)
sidekiq (>= 4.2.1)
sidekiq-limit_fetch (3.4.0)
@@ -862,18 +865,19 @@ GEM
faraday (~> 0.9)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
- simple_po_parser (1.1.3)
+ simple_po_parser (1.1.2)
simplecov (0.14.1)
docile (~> 1.1.0)
json (>= 1.8, < 3)
simplecov-html (~> 0.10.0)
- simplecov-html (0.10.2)
+ simplecov-html (0.10.0)
slack-notifier (1.5.1)
- spring (2.0.2)
+ slop (3.6.0)
+ spring (2.0.1)
activesupport (>= 4.2)
spring-commands-rspec (1.0.4)
spring (>= 0.9.1)
- sprockets (3.7.1)
+ sprockets (3.7.2)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.2.1)
@@ -882,7 +886,7 @@ GEM
sprockets (>= 3.0.0)
sqlite3 (1.3.13)
sshkey (1.9.0)
- stackprof (0.2.11)
+ stackprof (0.2.10)
state_machines (0.5.0)
state_machines-activemodel (0.5.1)
activemodel (>= 4.1, < 6.0)
@@ -891,19 +895,19 @@ GEM
activerecord (>= 4.1, < 6.0)
state_machines-activemodel (>= 0.5.0)
stringex (2.8.4)
- sys-filesystem (1.1.9)
+ sys-filesystem (1.1.6)
ffi
sysexits (1.2.0)
- temple (0.7.7)
+ temple (0.8.0)
test-prof (0.2.5)
text (1.3.1)
- thin (1.7.2)
+ thin (1.7.0)
daemons (~> 1.0, >= 1.0.9)
eventmachine (~> 1.0, >= 1.0.4)
rack (>= 1, < 3)
thor (0.19.4)
thread_safe (0.3.6)
- tilt (2.0.8)
+ tilt (2.0.6)
timecop (0.8.1)
timfel-krb5-auth (0.8.3)
toml (0.1.2)
@@ -923,7 +927,7 @@ GEM
unf (0.1.4)
unf_ext
unf_ext (0.0.7.5)
- unicode-display_width (1.3.0)
+ unicode-display_width (1.3.2)
unicorn (5.1.0)
kgio (~> 2.6)
raindrops (~> 0.7)
@@ -939,8 +943,7 @@ GEM
equalizer (~> 0.0.9)
parser (>= 2.3.1.2, < 2.6)
procto (~> 0.0.2)
- url_safe_base64 (0.2.2)
- validates_hostname (1.0.8)
+ validates_hostname (1.0.6)
activerecord (>= 3.0)
activesupport (>= 3.0)
version_sorter (2.1.0)
@@ -956,7 +959,7 @@ GEM
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff
- webpack-rails (0.9.11)
+ webpack-rails (0.9.10)
railties (>= 3.2.0)
websocket-driver (0.6.5)
websocket-extensions (>= 0.1.0)
@@ -967,8 +970,8 @@ GEM
rinku
with_env (1.1.0)
xml-simple (1.1.5)
- xpath (3.0.0)
- nokogiri (~> 1.8)
+ xpath (2.1.0)
+ nokogiri (~> 1.3)
PLATFORMS
ruby
@@ -980,13 +983,12 @@ DEPENDENCIES
acts-as-taggable-on (~> 5.0)
addressable (~> 2.5.2)
akismet (~> 2.0)
- allocations (~> 1.0)
asana (~> 0.6.0)
asciidoctor (~> 1.5.6)
asciidoctor-plantuml (= 0.0.8)
asset_sync (~> 2.4)
attr_encrypted (~> 3.1.0)
- awesome_print (~> 1.2.0)
+ awesome_print
babosa (~> 1.0.2)
base32 (~> 0.3.0)
batch-loader (~> 1.2.1)
@@ -994,7 +996,6 @@ DEPENDENCIES
benchmark-ips (~> 2.3.0)
better_errors (~> 2.1.0)
binding_of_caller (~> 0.7.2)
- bootstrap-sass (~> 3.3.0)
bootstrap_form (~> 2.7.0)
brakeman (~> 4.2)
browser (~> 2.2)
@@ -1018,10 +1019,11 @@ DEPENDENCIES
devise-two-factor (~> 3.0.0)
diffy (~> 3.1.0)
doorkeeper (~> 4.3)
- doorkeeper-openid_connect (~> 1.3)
+ doorkeeper-openid_connect (~> 1.5)
dropzonejs-rails (~> 0.7.1)
+ ed25519 (~> 1.2)
email_reply_trimmer (~> 0.1)
- email_spec (~> 1.6.0)
+ email_spec (~> 2.2.0)
factory_bot_rails (~> 4.8.2)
faraday (~> 0.12)
fast_blank
@@ -1045,13 +1047,13 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
- gitaly-proto (~> 0.99.0)
+ gitaly-proto (~> 0.105.0)
github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2)
gitlab-gollum-rugged_adapter (~> 0.4.4)
- gitlab-markup (~> 1.6.2)
- gitlab-styles (~> 2.3)
+ gitlab-markup (~> 1.6.4)
+ gitlab-styles (~> 2.4)
gitlab_omniauth-ldap (~> 2.0.4)
gon (~> 6.2)
google-api-client (~> 0.19.8)
@@ -1059,17 +1061,20 @@ DEPENDENCIES
gpgme
grape (~> 1.0)
grape-entity (~> 0.7.1)
- grape-route-helpers (~> 2.1.0)
+ grape-path-helpers (~> 1.0)
grape_logging (~> 1.7)
+ graphiql-rails (~> 1.4.10)
+ graphql (~> 1.8.0)
grpc (~> 1.11.0)
haml_lint (~> 0.26.0)
- hamlit (~> 2.6.1)
+ hamlit (~> 2.8.8)
hashie-forbidden_attributes
health_check (~> 2.6.0)
hipchat (~> 1.5.0)
- html-pipeline (~> 2.7.1)
+ html-pipeline (~> 2.8)
html2text
httparty (~> 0.13.3)
+ icalendar
influxdb (~> 0.2)
jira-ruby (~> 1.4)
jquery-atwho-rails (~> 1.3.2)
@@ -1077,7 +1082,7 @@ DEPENDENCIES
jwt (~> 1.5.6)
kaminari (~> 1.0)
knapsack (~> 1.16)
- kubeclient (~> 3.0)
+ kubeclient (~> 3.1.0)
letter_opener_web (~> 1.3.0)
license_finder (~> 3.1)
licensee (~> 8.9)
@@ -1085,17 +1090,18 @@ DEPENDENCIES
loofah (~> 2.2)
mail_room (~> 0.9.1)
method_source (~> 0.8)
+ mini_magick
minitest (~> 5.7.0)
mousetrap-rails (~> 1.4.6)
mysql2 (~> 0.4.10)
net-ldap
- net-ssh (~> 4.2.0)
+ net-ssh (~> 5.0)
nokogiri (~> 1.8.2)
oauth2 (~> 1.4)
- octokit (~> 4.8)
+ octokit (~> 4.9)
omniauth (~> 1.8)
omniauth-auth0 (~> 2.0.0)
- omniauth-authentiq (~> 0.3.1)
+ omniauth-authentiq (~> 0.3.3)
omniauth-azure-oauth2 (~> 0.0.9)
omniauth-cas3 (~> 1.1.4)
omniauth-facebook (~> 4.0.0)
@@ -1105,7 +1111,7 @@ DEPENDENCIES
omniauth-kerberos (~> 0.3.0)
omniauth-oauth2-generic (~> 0.2.2)
omniauth-saml (~> 1.10)
- omniauth-shibboleth (~> 1.2.0)
+ omniauth-shibboleth (~> 1.3.0)
omniauth-twitter (~> 1.4)
omniauth_crowd (~> 2.2.0)
org-ruby (~> 0.9.12)
@@ -1118,7 +1124,7 @@ DEPENDENCIES
peek-sidekiq (~> 1.0.3)
pg (~> 0.18.2)
premailer-rails (~> 1.9.7)
- prometheus-client-mmap (~> 0.9.2)
+ prometheus-client-mmap (~> 0.9.3)
pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4)
rack-attack (~> 4.4.1)
@@ -1132,42 +1138,41 @@ DEPENDENCIES
rainbow (~> 2.2)
raindrops (~> 0.18)
rblineprof (~> 0.3.6)
- rbnacl (~> 4.0)
- rbnacl-libsodium
- rdoc (~> 4.2)
+ rdoc (~> 6.0)
re2 (~> 1.1.1)
recaptcha (~> 3.0)
redcarpet (~> 3.4)
redis (~> 3.2)
- redis-namespace (~> 1.5.2)
+ redis-namespace (~> 1.6.0)
redis-rails (~> 5.0.2)
request_store (~> 1.3)
responders (~> 2.0)
rouge (~> 3.1)
rqrcode-rails3 (~> 0.1.7)
rspec-parameterized
- rspec-rails (~> 3.6.0)
+ rspec-rails (~> 3.7.0)
rspec-retry (~> 0.4.5)
rspec-set (~> 0.1.3)
rspec_profiling (~> 0.0.5)
- rubocop (~> 0.52.1)
+ rubocop (~> 0.54.0)
rubocop-rspec (~> 1.22.1)
ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.17.0)
+ ruby-progressbar
ruby_parser (~> 3.8)
rufus-scheduler (~> 3.4)
rugged (~> 0.27)
- sanitize (~> 2.0)
+ sanitize (~> 4.6.5)
sass-rails (~> 5.0.6)
scss_lint (~> 0.56.0)
seed-fu (~> 2.3.7)
select2-rails (~> 3.5.9)
- selenium-webdriver (~> 3.5)
+ selenium-webdriver (~> 3.12)
sentry-raven (~> 2.7)
settingslogic (~> 2.0.9)
sham_rack (~> 1.3.6)
shoulda-matchers (~> 3.1.2)
- sidekiq (~> 5.0)
+ sidekiq (~> 5.1)
sidekiq-cron (~> 0.6.0)
sidekiq-limit_fetch (~> 3.4)
simple_po_parser (~> 1.1.2)
@@ -1199,4 +1204,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
- 1.16.1
+ 1.16.2
diff --git a/INSTALLATION_TYPE b/INSTALLATION_TYPE
new file mode 100644
index 00000000000..5a18cd2fbf6
--- /dev/null
+++ b/INSTALLATION_TYPE
@@ -0,0 +1 @@
+source
diff --git a/PROCESS.md b/PROCESS.md
index f206506f7c5..a06ddb68b77 100644
--- a/PROCESS.md
+++ b/PROCESS.md
@@ -15,6 +15,8 @@
- [Between the 1st and the 7th](#between-the-1st-and-the-7th)
- [On the 7th](#on-the-7th)
- [After the 7th](#after-the-7th)
+- [Regressions](#regressions)
+ - [How to manage a regression](#how-to-manage-a-regression)
- [Release retrospective and kickoff](#release-retrospective-and-kickoff)
- [Retrospective](#retrospective)
- [Kickoff](#kickoff)
@@ -168,6 +170,8 @@ the stable branch are:
* Fixes for [regressions](#regressions)
* Fixes for security issues
+* Fixes or improvements to automated QA scenarios
+* Documentation updates for changes in the same release
* 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
@@ -184,11 +188,7 @@ 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:
+to. For example:
- `Pick into 10.1`
- `Pick into 10.0`
@@ -199,26 +199,9 @@ to be backported down to the `9.5` release, you will need to assign it the
If you think a merge request should go into an RC or patch even though it does not meet these requirements,
you can ask for an exception to be made.
-Go to [Release tasks issue tracker](https://gitlab.com/gitlab-org/release/tasks/issues/new) and create an issue
-using the `Exception-request` issue template.
-
-**Do not** set the relevant `Pick into X.Y` label (see above) before request an
-exception; this should be done after the exception is approved.
-
-You can find who is who on the [team page](https://about.gitlab.com/team/).
-
-Whether an exception is made is determined by weighing the benefit and urgency of the change
-(how important it is to the company that this is released _right now_ instead of in a month)
-against the potential negative impact
-(things breaking without enough time to comfortably find and fix them before the release on the 22nd).
-When in doubt, we err on the side of _not_ cherry-picking.
-
-For example, it is likely that an exception will be made for a trivial 1-5 line performance improvement
-(e.g. adding a database index or adding `includes` to a query), but not for a new feature, no matter how relatively small or thoroughly tested.
-
-All MRs which have had exceptions granted must be merged by the 15th.
+Check [this guide](https://gitlab.com/gitlab-org/release/docs/blob/master/general/exception-request/process.md) about how to open an exception request before opening one.
-### Regressions
+## Regressions
A regression for a particular monthly release is a bug that exists in that
release, but wasn't present in the release before. This includes bugs in
@@ -236,10 +219,30 @@ month. When we say 'the most recent monthly release', this can refer to either
the version currently running on GitLab.com, or the most recent version
available in the package repositories.
-A regression issue should be labeled with the appropriate [subject label](../CONTRIBUTING.md#subject-labels-wiki-container-registry-ldap-api-etc)
-and [team label](../CONTRIBUTING.md#team-labels-ci-discussion-edge-platform-etc),
-just like any other issue, to help GitLab team members focus on issues that are
-relevant to [their area of responsibility](https://about.gitlab.com/handbook/engineering/workflow/#choosing-something-to-work-on).
+### How to manage a regression
+
+Regressions are very important, and they should be considered high priority
+issues that should be solved as soon as possible, especially if they affect
+users. Despite that, ~regression label itself does not imply when the issue
+will be scheduled.
+
+When a regression is found:
+1. Create an issue describing the problem in the most detailed way possible
+1. If possible, provide links to real examples and how to reproduce the problem
+1. Label the issue properly, using the [team label](../CONTRIBUTING.md#team-labels),
+ the [subject label](../CONTRIBUTING.md#subject-labels)
+ and any other label that may apply in the specific case
+1. Add the ~bug and ~regression labels
+1. Notify the respective Engineering Manager to evaluate the Severity of the regression and add a [Severity label](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#bug-severity-labels). The counterpart Product Manager is included to weigh-in on prioritization as needed to set the [Priority label](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#bug-priority-labels).
+1. If the regression is either an ~S1, ~S2 or ~S3 severity, label the regression with the current milestone as it should be fixed in the current milestone.
+ 1. If the regression was introduced in an RC of the current release, label with ~Deliverable
+ 1. If the regression was introduced in the previous release, label with ~"Next Patch Release"
+1. If the regression is an ~S4 severity, the regression may be scheduled for later milestones at the discretion of Engineering Manager and Product Manager.
+
+When a new issue is found, the fix should start as soon as possible. You can
+ping the Engineering Manager or the Product Manager for the relative area to
+make them aware of the issue earlier. They will analyze the priority and change
+it if needed.
## Release retrospective and kickoff
diff --git a/README.md b/README.md
index 013cac75c46..b6e1cc9a432 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,5 @@
# GitLab
-[![Build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
-[![Overall test coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg)](https://gitlab.com/gitlab-org/gitlab-ce/pipelines)
-[![Dependency Status](https://gemnasium.com/gitlabhq/gitlabhq.svg)](https://gemnasium.com/gitlabhq/gitlabhq)
-[![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq)
-[![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42)
-[![Gitter](https://badges.gitter.im/gitlabhq/gitlabhq.svg)](https://gitter.im/gitlabhq/gitlabhq?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
-
## Test coverage
- [![Ruby coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby) Ruby
@@ -36,7 +29,7 @@ We're hiring developers, support people, and production engineers all the time,
There are two editions of GitLab:
- GitLab Community Edition (CE) is available freely under the MIT Expat license.
-- GitLab Enterprise Edition (EE) includes [extra features](https://about.gitlab.com/products/#compare-options) that are more useful for organizations with more than 100 users. To use EE and get official support please [become a subscriber](https://about.gitlab.com/products/).
+- GitLab Enterprise Edition (EE) includes [extra features](https://about.gitlab.com/pricing/#compare-options) that are more useful for organizations with more than 100 users. To use EE and get official support please [become a subscriber](https://about.gitlab.com/pricing/).
## Website
@@ -121,11 +114,15 @@ All documentation can be found on [docs.gitlab.com/ce/](https://docs.gitlab.com/
Please see [Getting help for GitLab](https://about.gitlab.com/getting-help/) on our website for the many options to get help.
+## Why?
+
+[Read here](https://about.gitlab.com/why/)
+
## Is it any good?
[Yes](https://news.ycombinator.com/item?id=3067434)
## Is it awesome?
-Thanks for [asking this question](https://twitter.com/supersloth/status/489462789384056832) Joshua.
[These people](https://twitter.com/gitlab/likes) seem to like it.
+
diff --git a/Rakefile b/Rakefile
index 85fff2d51eb..de0d6695c7b 100755
--- a/Rakefile
+++ b/Rakefile
@@ -2,9 +2,9 @@
# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
-require File.expand_path('../config/application', __FILE__)
+require File.expand_path('config/application', __dir__)
-relative_url_conf = File.expand_path('../config/initializers/relative_url', __FILE__)
+relative_url_conf = File.expand_path('config/initializers/relative_url', __dir__)
require relative_url_conf if File.exist?("#{relative_url_conf}.rb")
Gitlab::Application.load_tasks
diff --git a/VERSION b/VERSION
index 8ca9077d87b..0116f5d2c81 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-11.0.0-pre
+11.1.0-pre
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_canceled.ico b/app/assets/images/ci_favicons/dev/favicon_status_canceled.ico
deleted file mode 100644
index 4af3582b60d..00000000000
--- a/app/assets/images/ci_favicons/dev/favicon_status_canceled.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_created.ico b/app/assets/images/ci_favicons/dev/favicon_status_created.ico
deleted file mode 100644
index 13639da2e8a..00000000000
--- a/app/assets/images/ci_favicons/dev/favicon_status_created.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_failed.ico b/app/assets/images/ci_favicons/dev/favicon_status_failed.ico
deleted file mode 100644
index 5f0e711b104..00000000000
--- a/app/assets/images/ci_favicons/dev/favicon_status_failed.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_manual.ico b/app/assets/images/ci_favicons/dev/favicon_status_manual.ico
deleted file mode 100644
index 8b1168a1267..00000000000
--- a/app/assets/images/ci_favicons/dev/favicon_status_manual.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_not_found.ico b/app/assets/images/ci_favicons/dev/favicon_status_not_found.ico
deleted file mode 100644
index ed19b69e1c5..00000000000
--- a/app/assets/images/ci_favicons/dev/favicon_status_not_found.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_pending.ico b/app/assets/images/ci_favicons/dev/favicon_status_pending.ico
deleted file mode 100644
index 5dfefd4cc5a..00000000000
--- a/app/assets/images/ci_favicons/dev/favicon_status_pending.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_running.ico b/app/assets/images/ci_favicons/dev/favicon_status_running.ico
deleted file mode 100644
index a41539c0e3e..00000000000
--- a/app/assets/images/ci_favicons/dev/favicon_status_running.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_skipped.ico b/app/assets/images/ci_favicons/dev/favicon_status_skipped.ico
deleted file mode 100644
index 2c1ae552b93..00000000000
--- a/app/assets/images/ci_favicons/dev/favicon_status_skipped.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_success.ico b/app/assets/images/ci_favicons/dev/favicon_status_success.ico
deleted file mode 100644
index 70f0ca61eca..00000000000
--- a/app/assets/images/ci_favicons/dev/favicon_status_success.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_warning.ico b/app/assets/images/ci_favicons/dev/favicon_status_warning.ico
deleted file mode 100644
index db289e03eb1..00000000000
--- a/app/assets/images/ci_favicons/dev/favicon_status_warning.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_canceled.ico b/app/assets/images/ci_favicons/favicon_status_canceled.ico
deleted file mode 100644
index 23adcffff50..00000000000
--- a/app/assets/images/ci_favicons/favicon_status_canceled.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_canceled.png b/app/assets/images/ci_favicons/favicon_status_canceled.png
new file mode 100644
index 00000000000..8adaa9c600b
--- /dev/null
+++ b/app/assets/images/ci_favicons/favicon_status_canceled.png
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_created.ico b/app/assets/images/ci_favicons/favicon_status_created.ico
deleted file mode 100644
index f9d93b390d8..00000000000
--- a/app/assets/images/ci_favicons/favicon_status_created.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_created.png b/app/assets/images/ci_favicons/favicon_status_created.png
new file mode 100644
index 00000000000..ca788dd0034
--- /dev/null
+++ b/app/assets/images/ci_favicons/favicon_status_created.png
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_failed.ico b/app/assets/images/ci_favicons/favicon_status_failed.ico
deleted file mode 100644
index 28a22ebf724..00000000000
--- a/app/assets/images/ci_favicons/favicon_status_failed.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_failed.png b/app/assets/images/ci_favicons/favicon_status_failed.png
new file mode 100644
index 00000000000..93f1e2772fd
--- /dev/null
+++ b/app/assets/images/ci_favicons/favicon_status_failed.png
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_manual.ico b/app/assets/images/ci_favicons/favicon_status_manual.ico
deleted file mode 100644
index dbbf1abf30c..00000000000
--- a/app/assets/images/ci_favicons/favicon_status_manual.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_manual.png b/app/assets/images/ci_favicons/favicon_status_manual.png
new file mode 100644
index 00000000000..c926062c806
--- /dev/null
+++ b/app/assets/images/ci_favicons/favicon_status_manual.png
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_not_found.ico b/app/assets/images/ci_favicons/favicon_status_not_found.ico
deleted file mode 100644
index 49b9b232dd1..00000000000
--- a/app/assets/images/ci_favicons/favicon_status_not_found.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_not_found.png b/app/assets/images/ci_favicons/favicon_status_not_found.png
new file mode 100644
index 00000000000..df3049315a9
--- /dev/null
+++ b/app/assets/images/ci_favicons/favicon_status_not_found.png
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_pending.ico b/app/assets/images/ci_favicons/favicon_status_pending.ico
deleted file mode 100644
index 05962f3f148..00000000000
--- a/app/assets/images/ci_favicons/favicon_status_pending.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_pending.png b/app/assets/images/ci_favicons/favicon_status_pending.png
new file mode 100644
index 00000000000..f7d67d4a230
--- /dev/null
+++ b/app/assets/images/ci_favicons/favicon_status_pending.png
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_running.ico b/app/assets/images/ci_favicons/favicon_status_running.ico
deleted file mode 100644
index 7fa3d4d48d4..00000000000
--- a/app/assets/images/ci_favicons/favicon_status_running.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_running.png b/app/assets/images/ci_favicons/favicon_status_running.png
new file mode 100644
index 00000000000..ff4167c4b20
--- /dev/null
+++ b/app/assets/images/ci_favicons/favicon_status_running.png
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_skipped.ico b/app/assets/images/ci_favicons/favicon_status_skipped.ico
deleted file mode 100644
index b0c26b62068..00000000000
--- a/app/assets/images/ci_favicons/favicon_status_skipped.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_skipped.png b/app/assets/images/ci_favicons/favicon_status_skipped.png
new file mode 100644
index 00000000000..a9c36464b69
--- /dev/null
+++ b/app/assets/images/ci_favicons/favicon_status_skipped.png
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_success.ico b/app/assets/images/ci_favicons/favicon_status_success.ico
deleted file mode 100644
index b150960b5be..00000000000
--- a/app/assets/images/ci_favicons/favicon_status_success.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_success.png b/app/assets/images/ci_favicons/favicon_status_success.png
new file mode 100644
index 00000000000..bcc30c73f5f
--- /dev/null
+++ b/app/assets/images/ci_favicons/favicon_status_success.png
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_warning.ico b/app/assets/images/ci_favicons/favicon_status_warning.ico
deleted file mode 100644
index 7e71d71684d..00000000000
--- a/app/assets/images/ci_favicons/favicon_status_warning.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_warning.png b/app/assets/images/ci_favicons/favicon_status_warning.png
new file mode 100644
index 00000000000..6db3b0280f5
--- /dev/null
+++ b/app/assets/images/ci_favicons/favicon_status_warning.png
Binary files differ
diff --git a/app/assets/images/favicon-blue.ico b/app/assets/images/favicon-blue.ico
deleted file mode 100644
index 156fcf07588..00000000000
--- a/app/assets/images/favicon-blue.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/favicon-blue.png b/app/assets/images/favicon-blue.png
new file mode 100644
index 00000000000..2229fe79462
--- /dev/null
+++ b/app/assets/images/favicon-blue.png
Binary files differ
diff --git a/app/assets/images/favicon-yellow.ico b/app/assets/images/favicon-yellow.ico
deleted file mode 100644
index b650f277fb6..00000000000
--- a/app/assets/images/favicon-yellow.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/favicon-yellow.png b/app/assets/images/favicon-yellow.png
new file mode 100644
index 00000000000..2d5289818b4
--- /dev/null
+++ b/app/assets/images/favicon-yellow.png
Binary files differ
diff --git a/app/assets/images/favicon.ico b/app/assets/images/favicon.ico
deleted file mode 100644
index 3479cbbb46f..00000000000
--- a/app/assets/images/favicon.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/favicon.png b/app/assets/images/favicon.png
new file mode 100644
index 00000000000..845e0ec34a5
--- /dev/null
+++ b/app/assets/images/favicon.png
Binary files differ
diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js
index c117d080bda..de4566bb119 100644
--- a/app/assets/javascripts/activities.js
+++ b/app/assets/javascripts/activities.js
@@ -1,4 +1,4 @@
-/* eslint-disable no-param-reassign, class-methods-use-this */
+/* eslint-disable class-methods-use-this */
import $ from 'jquery';
import Cookies from 'js-cookie';
diff --git a/app/assets/javascripts/ajax_loading_spinner.js b/app/assets/javascripts/ajax_loading_spinner.js
index bd08308904c..54e86f329e4 100644
--- a/app/assets/javascripts/ajax_loading_spinner.js
+++ b/app/assets/javascripts/ajax_loading_spinner.js
@@ -26,7 +26,7 @@ export default class AjaxLoadingSpinner {
}
static toggleLoadingIcon(iconElement) {
- const classList = iconElement.classList;
+ const { classList } = iconElement;
classList.toggle(iconElement.dataset.icon);
classList.toggle('fa-spinner');
classList.toggle('fa-spin');
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index eb919241318..422becb7db8 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -11,6 +11,7 @@ const Api = {
projectPath: '/api/:version/projects/:id',
projectLabelsPath: '/:namespace_path/:project_path/labels',
mergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid',
+ mergeRequestsPath: '/api/:version/merge_requests',
mergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes',
mergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions',
groupLabelsPath: '/groups/:namespace_path/-/labels',
@@ -21,10 +22,9 @@ const Api = {
issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key',
usersPath: '/api/:version/users.json',
commitPath: '/api/:version/projects/:id/repository/commits',
+ commitPipelinesPath: '/:project_id/commit/:sha/pipelines',
branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
createBranchPath: '/api/:version/projects/:id/repository/branches',
- pipelinesPath: '/api/:version/projects/:id/pipelines',
- pipelineJobsPath: '/api/:version/projects/:id/pipelines/:pipeline_id/jobs',
group(groupId, callback) {
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
@@ -100,12 +100,18 @@ const Api = {
},
// Return Merge Request for project
- mergeRequest(projectPath, mergeRequestId) {
+ mergeRequest(projectPath, mergeRequestId, params = {}) {
const url = Api.buildUrl(Api.mergeRequestPath)
.replace(':id', encodeURIComponent(projectPath))
.replace(':mrid', mergeRequestId);
- return axios.get(url);
+ return axios.get(url, { params });
+ },
+
+ mergeRequests(params = {}) {
+ const url = Api.buildUrl(Api.mergeRequestsPath);
+
+ return axios.get(url, { params });
},
mergeRequestChanges(projectPath, mergeRequestId) {
@@ -144,14 +150,15 @@ const Api = {
},
// Return group projects list. Filtered by query
- groupProjects(groupId, query, callback) {
+ groupProjects(groupId, query, options, callback) {
const url = Api.buildUrl(Api.groupProjectsPath).replace(':id', groupId);
+ const defaults = {
+ search: query,
+ per_page: 20,
+ };
return axios
.get(url, {
- params: {
- search: query,
- per_page: 20,
- },
+ params: Object.assign({}, defaults, options),
})
.then(({ data }) => callback(data));
},
@@ -166,6 +173,19 @@ const Api = {
});
},
+ commitPipelines(projectId, sha) {
+ const encodedProjectId = projectId
+ .split('/')
+ .map(fragment => encodeURIComponent(fragment))
+ .join('/');
+
+ const url = Api.buildUrl(Api.commitPipelinesPath)
+ .replace(':project_id', encodedProjectId)
+ .replace(':sha', encodeURIComponent(sha));
+
+ return axios.get(url);
+ },
+
branchSingle(id, branch) {
const url = Api.buildUrl(Api.branchSinglePath)
.replace(':id', encodeURIComponent(id))
@@ -224,18 +244,13 @@ const Api = {
});
},
- pipelines(projectPath, params = {}) {
- const url = Api.buildUrl(this.pipelinesPath).replace(':id', encodeURIComponent(projectPath));
+ createBranch(id, { ref, branch }) {
+ const url = Api.buildUrl(this.createBranchPath).replace(':id', encodeURIComponent(id));
- return axios.get(url, { params });
- },
-
- pipelineJobs(projectPath, pipelineId, params = {}) {
- const url = Api.buildUrl(this.pipelineJobsPath)
- .replace(':id', encodeURIComponent(projectPath))
- .replace(':pipeline_id', pipelineId);
-
- return axios.get(url, { params });
+ return axios.post(url, {
+ ref,
+ branch,
+ });
},
buildUrl(url) {
diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js
index 0da872db7e5..fa00a3cf386 100644
--- a/app/assets/javascripts/autosave.js
+++ b/app/assets/javascripts/autosave.js
@@ -1,4 +1,4 @@
-/* eslint-disable no-param-reassign, prefer-template, no-var, no-void, consistent-return */
+/* eslint-disable no-param-reassign, prefer-template, no-void, consistent-return */
import AccessorUtilities from './lib/utils/accessor';
@@ -31,7 +31,9 @@ export default class Autosave {
// https://github.com/vuejs/vue/issues/2804#issuecomment-216968137
const event = new Event('change', { bubbles: true, cancelable: false });
const field = this.field.get(0);
- field.dispatchEvent(event);
+ if (field) {
+ field.dispatchEvent(event);
+ }
}
save() {
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index 976d32abe9b..70f20c5c7cf 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -11,7 +11,8 @@ import axios from './lib/utils/axios_utils';
const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd';
const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd';
-const requestAnimationFrame = window.requestAnimationFrame ||
+const requestAnimationFrame =
+ window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.setTimeout;
@@ -37,21 +38,28 @@ class AwardsHandler {
this.emoji = emoji;
this.eventListeners = [];
// If the user shows intent let's pre-build the menu
- this.registerEventListener('one', $(document), 'mouseenter focus', '.js-add-award', 'mouseenter focus', () => {
- const $menu = $('.emoji-menu');
- if ($menu.length === 0) {
- requestAnimationFrame(() => {
- this.createEmojiMenu();
- });
- }
- });
- this.registerEventListener('on', $(document), 'click', '.js-add-award', (e) => {
+ this.registerEventListener(
+ 'one',
+ $(document),
+ 'mouseenter focus',
+ '.js-add-award',
+ 'mouseenter focus',
+ () => {
+ const $menu = $('.emoji-menu');
+ if ($menu.length === 0) {
+ requestAnimationFrame(() => {
+ this.createEmojiMenu();
+ });
+ }
+ },
+ );
+ this.registerEventListener('on', $(document), 'click', '.js-add-award', e => {
e.stopPropagation();
e.preventDefault();
this.showEmojiMenu($(e.currentTarget));
});
- this.registerEventListener('on', $('html'), 'click', (e) => {
+ this.registerEventListener('on', $('html'), 'click', e => {
const $target = $(e.target);
if (!$target.closest('.emoji-menu').length) {
$('.js-awards-block.current').removeClass('current');
@@ -61,12 +69,14 @@ class AwardsHandler {
}
}
});
- this.registerEventListener('on', $(document), 'click', '.js-emoji-btn', (e) => {
+ this.registerEventListener('on', $(document), 'click', '.js-emoji-btn', e => {
e.preventDefault();
const $target = $(e.currentTarget);
const $glEmojiElement = $target.find('gl-emoji');
const $spriteIconElement = $target.find('.icon');
- const emojiName = ($glEmojiElement.length ? $glEmojiElement : $spriteIconElement).data('name');
+ const emojiName = ($glEmojiElement.length ? $glEmojiElement : $spriteIconElement).data(
+ 'name',
+ );
$target.closest('.js-awards-block').addClass('current');
this.addAward(this.getVotesBlock(), this.getAwardUrl(), emojiName);
@@ -83,7 +93,10 @@ class AwardsHandler {
showEmojiMenu($addBtn) {
if ($addBtn.hasClass('js-note-emoji')) {
- $addBtn.closest('.note').find('.js-awards-block').addClass('current');
+ $addBtn
+ .closest('.note')
+ .find('.js-awards-block')
+ .addClass('current');
} else {
$addBtn.closest('.js-awards-block').addClass('current');
}
@@ -177,32 +190,38 @@ class AwardsHandler {
const remainingCategories = Object.keys(categoryMap).slice(1);
const allCategoriesAddedPromise = remainingCategories.reduce(
(promiseChain, categoryNameKey) =>
- promiseChain.then(() =>
- new Promise((resolve) => {
- const emojisInCategory = categoryMap[categoryNameKey];
- const categoryMarkup = this.renderCategory(
- categoryLabelMap[categoryNameKey],
- emojisInCategory,
- );
- requestAnimationFrame(() => {
- emojiContentElement.insertAdjacentHTML('beforeend', categoryMarkup);
- resolve();
- });
- }),
- ),
+ promiseChain.then(
+ () =>
+ new Promise(resolve => {
+ const emojisInCategory = categoryMap[categoryNameKey];
+ const categoryMarkup = this.renderCategory(
+ categoryLabelMap[categoryNameKey],
+ emojisInCategory,
+ );
+ requestAnimationFrame(() => {
+ emojiContentElement.insertAdjacentHTML('beforeend', categoryMarkup);
+ resolve();
+ });
+ }),
+ ),
Promise.resolve(),
);
- allCategoriesAddedPromise.then(() => {
- // Used for tests
- // We check for the menu in case it was destroyed in the meantime
- if (menu) {
- menu.dispatchEvent(new CustomEvent('build-emoji-menu-finish'));
- }
- }).catch((err) => {
- emojiContentElement.insertAdjacentHTML('beforeend', '<p>We encountered an error while adding the remaining categories</p>');
- throw new Error(`Error occurred in addRemainingEmojiMenuCategories: ${err.message}`);
- });
+ allCategoriesAddedPromise
+ .then(() => {
+ // Used for tests
+ // We check for the menu in case it was destroyed in the meantime
+ if (menu) {
+ menu.dispatchEvent(new CustomEvent('build-emoji-menu-finish'));
+ }
+ })
+ .catch(err => {
+ emojiContentElement.insertAdjacentHTML(
+ 'beforeend',
+ '<p>We encountered an error while adding the remaining categories</p>',
+ );
+ throw new Error(`Error occurred in addRemainingEmojiMenuCategories: ${err.message}`);
+ });
}
renderCategory(name, emojiList, opts = {}) {
@@ -211,7 +230,9 @@ class AwardsHandler {
${name}
</h5>
<ul class="clearfix emoji-menu-list ${opts.menuListClass || ''}">
- ${emojiList.map(emojiName => `
+ ${emojiList
+ .map(
+ emojiName => `
<li class="emoji-menu-list-item">
<button class="emoji-menu-btn text-center js-emoji-btn" type="button">
${this.emoji.glEmojiTag(emojiName, {
@@ -219,7 +240,9 @@ class AwardsHandler {
})}
</button>
</li>
- `).join('\n')}
+ `,
+ )
+ .join('\n')}
</ul>
`;
}
@@ -232,7 +255,7 @@ class AwardsHandler {
top: `${$addBtn.offset().top + $addBtn.outerHeight()}px`,
};
if (position === 'right') {
- css.left = `${($addBtn.offset().left - $menu.outerWidth()) + 20}px`;
+ css.left = `${$addBtn.offset().left - $menu.outerWidth() + 20}px`;
$menu.addClass('is-aligned-right');
} else {
css.left = `${$addBtn.offset().left}px`;
@@ -345,7 +368,7 @@ class AwardsHandler {
counter.text(counterNumber - 1);
this.removeYouFromUserList($emojiButton);
} else if (emoji === 'thumbsup' || emoji === 'thumbsdown') {
- $emojiButton.tooltip('destroy');
+ $emojiButton.tooltip('dispose');
counter.text('0');
this.removeYouFromUserList($emojiButton);
if ($emojiButton.parents('.note').length) {
@@ -358,7 +381,7 @@ class AwardsHandler {
}
removeEmoji($emojiButton) {
- $emojiButton.tooltip('destroy');
+ $emojiButton.tooltip('dispose');
$emojiButton.remove();
const $votesBlock = this.getVotesBlock();
if ($votesBlock.find('.js-emoji-btn').length === 0) {
@@ -392,7 +415,7 @@ class AwardsHandler {
.removeAttr('data-title')
.removeAttr('data-original-title')
.attr('title', this.toSentence(authors))
- .tooltip('fixTitle');
+ .tooltip('_fixTitle');
}
addYouToUserList(votesBlock, emoji) {
@@ -405,7 +428,7 @@ class AwardsHandler {
users.unshift('You');
return awardBlock
.attr('title', this.toSentence(users))
- .tooltip('fixTitle');
+ .tooltip('_fixTitle');
}
createAwardButtonForVotesBlock(votesBlock, emojiName) {
@@ -416,7 +439,10 @@ class AwardsHandler {
</button>
`;
const $emojiButton = $(buttonHtml);
- $emojiButton.insertBefore(votesBlock.find('.js-award-holder')).find('.emoji-icon').data('name', emojiName);
+ $emojiButton
+ .insertBefore(votesBlock.find('.js-award-holder'))
+ .find('.emoji-icon')
+ .data('name', emojiName);
this.animateEmoji($emojiButton);
$('.award-control').tooltip();
votesBlock.removeClass('current');
@@ -426,7 +452,7 @@ class AwardsHandler {
const className = 'pulse animated once short';
$emoji.addClass(className);
- this.registerEventListener('on', $emoji, animationEndEventString, (e) => {
+ this.registerEventListener('on', $emoji, animationEndEventString, e => {
$(e.currentTarget).removeClass(className);
});
}
@@ -444,15 +470,16 @@ class AwardsHandler {
if (this.isUserAuthored($emojiButton)) {
this.userAuthored($emojiButton);
} else {
- axios.post(awardUrl, {
- name: emoji,
- })
- .then(({ data }) => {
- if (data.ok) {
- callback();
- }
- })
- .catch(() => flash(__('Something went wrong on our end.')));
+ axios
+ .post(awardUrl, {
+ name: emoji,
+ })
+ .then(({ data }) => {
+ if (data.ok) {
+ callback();
+ }
+ })
+ .catch(() => flash(__('Something went wrong on our end.')));
}
}
@@ -486,26 +513,33 @@ class AwardsHandler {
}
getFrequentlyUsedEmojis() {
- return this.frequentlyUsedEmojis || (() => {
- const frequentlyUsedEmojis = _.uniq((Cookies.get('frequently_used_emojis') || '').split(','));
- this.frequentlyUsedEmojis = frequentlyUsedEmojis.filter(
- inputName => this.emoji.isEmojiNameValid(inputName),
- );
-
- return this.frequentlyUsedEmojis;
- })();
+ return (
+ this.frequentlyUsedEmojis ||
+ (() => {
+ const frequentlyUsedEmojis = _.uniq(
+ (Cookies.get('frequently_used_emojis') || '').split(','),
+ );
+ this.frequentlyUsedEmojis = frequentlyUsedEmojis.filter(inputName =>
+ this.emoji.isEmojiNameValid(inputName),
+ );
+
+ return this.frequentlyUsedEmojis;
+ })()
+ );
}
setupSearch() {
const $search = $('.js-emoji-menu-search');
- this.registerEventListener('on', $search, 'input', (e) => {
- const term = $(e.target).val().trim();
+ this.registerEventListener('on', $search, 'input', e => {
+ const term = $(e.target)
+ .val()
+ .trim();
this.searchEmojis(term);
});
const $menu = $('.emoji-menu');
- this.registerEventListener('on', $menu, transitionEndEventString, (e) => {
+ this.registerEventListener('on', $menu, transitionEndEventString, e => {
if (e.target === e.currentTarget) {
// Clear the search
this.searchEmojis('');
@@ -523,19 +557,26 @@ class AwardsHandler {
// Generate a search result block
const h5 = $('<h5 class="emoji-search-title"/>').text('Search results');
const foundEmojis = this.findMatchingEmojiElements(term).show();
- const ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(foundEmojis);
+ const ul = $('<ul>')
+ .addClass('emoji-menu-list emoji-menu-search')
+ .append(foundEmojis);
$('.emoji-menu-content ul, .emoji-menu-content h5').hide();
- $('.emoji-menu-content').append(h5).append(ul);
+ $('.emoji-menu-content')
+ .append(h5)
+ .append(ul);
} else {
- $('.emoji-menu-content').children().show();
+ $('.emoji-menu-content')
+ .children()
+ .show();
}
}
findMatchingEmojiElements(query) {
const emojiMatches = this.emoji.filterEmojiNamesByAlias(query);
const $emojiElements = $('.emoji-menu-list:not(.frequent-emojis) [data-name]');
- const $matchingElements = $emojiElements
- .filter((i, elm) => emojiMatches.indexOf(elm.dataset.name) >= 0);
+ const $matchingElements = $emojiElements.filter(
+ (i, elm) => emojiMatches.indexOf(elm.dataset.name) >= 0,
+ );
return $matchingElements.closest('li').clone();
}
@@ -550,16 +591,13 @@ class AwardsHandler {
$emojiMenu.addClass(IS_RENDERED);
// enqueues animation as a microtask, so it begins ASAP once IS_RENDERED added
- return Promise.resolve()
- .then(() => $emojiMenu.addClass(IS_VISIBLE));
+ return Promise.resolve().then(() => $emojiMenu.addClass(IS_VISIBLE));
}
hideMenuElement($emojiMenu) {
- $emojiMenu.on(transitionEndEventString, (e) => {
+ $emojiMenu.on(transitionEndEventString, e => {
if (e.currentTarget === e.target) {
- $emojiMenu
- .removeClass(IS_RENDERED)
- .off(transitionEndEventString);
+ $emojiMenu.removeClass(IS_RENDERED).off(transitionEndEventString);
}
});
@@ -567,7 +605,7 @@ class AwardsHandler {
}
destroy() {
- this.eventListeners.forEach((entry) => {
+ this.eventListeners.forEach(entry => {
entry.element.off.call(entry.element, ...entry.args);
});
$('.emoji-menu').remove();
@@ -577,8 +615,9 @@ class AwardsHandler {
let awardsHandlerPromise = null;
export default function loadAwardsHandler(reload = false) {
if (!awardsHandlerPromise || reload) {
- awardsHandlerPromise = import(/* webpackChunkName: 'emoji' */ './emoji')
- .then(Emoji => new AwardsHandler(Emoji));
+ awardsHandlerPromise = import(/* webpackChunkName: 'emoji' */ './emoji').then(
+ Emoji => new AwardsHandler(Emoji),
+ );
}
return awardsHandlerPromise;
}
diff --git a/app/assets/javascripts/badges/components/badge.vue b/app/assets/javascripts/badges/components/badge.vue
index 6e6cb31e3ac..b4bfaee1d85 100644
--- a/app/assets/javascripts/badges/components/badge.vue
+++ b/app/assets/javascripts/badges/components/badge.vue
@@ -72,11 +72,11 @@ export default {
rel="noopener noreferrer"
>
<img
- class="project-badge"
:src="imageUrlWithRetries"
+ class="project-badge"
+ aria-hidden="true"
@load="onLoad"
@error="onError"
- aria-hidden="true"
/>
</a>
@@ -89,32 +89,32 @@ export default {
v-show="hasError"
class="btn-group"
>
- <div class="btn btn-default btn-xs disabled">
+ <div class="btn btn-default btn-sm disabled">
<icon
+ :size="16"
class="prepend-left-8 append-right-8"
name="doc_image"
- :size="16"
aria-hidden="true"
/>
</div>
<div
- class="btn btn-default btn-xs disabled"
+ class="btn btn-default btn-sm disabled"
>
<span class="prepend-left-8 append-right-8">{{ s__('Badges|No badge image') }}</span>
</div>
</div>
<button
- v-show="hasError"
- class="btn btn-transparent btn-xs text-primary"
- type="button"
v-tooltip
+ v-show="hasError"
:title="s__('Badges|Reload badge image')"
+ class="btn btn-transparent btn-sm text-primary"
+ type="button"
@click="reloadImage"
>
<icon
- name="retry"
:size="16"
+ name="retry"
/>
</button>
</div>
diff --git a/app/assets/javascripts/badges/components/badge_form.vue b/app/assets/javascripts/badges/components/badge_form.vue
index ae942b2c1a7..7a13f74c570 100644
--- a/app/assets/javascripts/badges/components/badge_form.vue
+++ b/app/assets/javascripts/badges/components/badge_form.vue
@@ -153,14 +153,14 @@ export default {
<label for="badge-link-url">{{ s__('Badges|Link') }}</label>
<input
id="badge-link-url"
- type="text"
- class="form-control"
v-model="linkUrl"
:placeholder="$options.badgeLinkUrlPlaceholder"
+ type="text"
+ class="form-control"
@input="debouncedPreview"
/>
<span
- class="help-block"
+ class="form-text text-muted"
v-html="helpText"
></span>
</div>
@@ -169,14 +169,14 @@ export default {
<label for="badge-image-url">{{ s__('Badges|Badge image URL') }}</label>
<input
id="badge-image-url"
- type="text"
- class="form-control"
v-model="imageUrl"
:placeholder="$options.badgeImageUrlPlaceholder"
+ type="text"
+ class="form-control"
@input="debouncedPreview"
/>
<span
- class="help-block"
+ class="form-text text-muted"
v-html="helpText"
></span>
</div>
@@ -184,8 +184,8 @@ export default {
<div class="form-group">
<label for="badge-preview">{{ s__('Badges|Badge image preview') }}</label>
<badge
- id="badge-preview"
v-show="renderedBadge && !isRendering"
+ id="badge-preview"
:image-url="renderedImageUrl"
:link-url="renderedLinkUrl"
/>
@@ -202,16 +202,16 @@ export default {
<div class="row-content-block">
<loading-button
- type="submit"
- container-class="btn btn-success"
:disabled="!canSubmit"
:loading="isSaving"
:label="submitButtonLabel"
+ type="submit"
+ container-class="btn btn-success"
/>
<button
+ v-if="isEditing"
class="btn btn-cancel"
type="button"
- v-if="isEditing"
@click="onCancel"
>{{ __('Cancel') }}</button>
</div>
diff --git a/app/assets/javascripts/badges/components/badge_list.vue b/app/assets/javascripts/badges/components/badge_list.vue
index ca7197e1e0f..268968b63b3 100644
--- a/app/assets/javascripts/badges/components/badge_list.vue
+++ b/app/assets/javascripts/badges/components/badge_list.vue
@@ -23,8 +23,8 @@ export default {
</script>
<template>
- <div class="panel panel-default">
- <div class="panel-heading">
+ <div class="card">
+ <div class="card-header">
{{ s__('Badges|Your badges') }}
<span
v-show="!isLoading"
@@ -33,19 +33,19 @@ export default {
</div>
<loading-icon
v-show="isLoading"
- class="panel-body"
+ class="card-body"
size="2"
/>
<div
v-if="hasNoBadges"
- class="panel-body"
+ class="card-body"
>
<span v-if="isGroupBadge">{{ s__('Badges|This group has no badges') }}</span>
<span v-else>{{ s__('Badges|This project has no badges') }}</span>
</div>
<div
v-else
- class="panel-body"
+ class="card-body"
>
<badge-list-row
v-for="badge in badges"
diff --git a/app/assets/javascripts/badges/components/badge_list_row.vue b/app/assets/javascripts/badges/components/badge_list_row.vue
index af062bdf8c6..98aa00af0d7 100644
--- a/app/assets/javascripts/badges/components/badge_list_row.vue
+++ b/app/assets/javascripts/badges/components/badge_list_row.vue
@@ -41,9 +41,9 @@ export default {
<template>
<div class="gl-responsive-table-row-layout gl-responsive-table-row">
<badge
- class="table-section section-30"
:image-url="badge.renderedImageUrl"
:link-url="badge.renderedLinkUrl"
+ class="table-section section-30"
/>
<span class="table-section section-50 str-truncated">{{ badge.linkUrl }}</span>
<div class="table-section section-10">
@@ -54,29 +54,29 @@ export default {
v-if="canEditBadge"
class="table-action-buttons">
<button
+ :disabled="badge.isDeleting"
class="btn btn-default append-right-8"
type="button"
- :disabled="badge.isDeleting"
@click="editBadge(badge)"
>
<icon
- name="pencil"
:size="16"
:aria-label="__('Edit')"
+ name="pencil"
/>
</button>
<button
+ :disabled="badge.isDeleting"
class="btn btn-danger"
type="button"
data-toggle="modal"
data-target="#delete-badge-modal"
- :disabled="badge.isDeleting"
@click="updateBadgeInModal(badge)"
>
<icon
- name="remove"
:size="16"
:aria-label="__('Delete')"
+ name="remove"
/>
</button>
<loading-icon
diff --git a/app/assets/javascripts/badges/components/badge_settings.vue b/app/assets/javascripts/badges/components/badge_settings.vue
index 83f78394238..cc47e56dd1e 100644
--- a/app/assets/javascripts/badges/components/badge_settings.vue
+++ b/app/assets/javascripts/badges/components/badge_settings.vue
@@ -44,8 +44,8 @@ export default {
<gl-modal
id="delete-badge-modal"
:header-title-text="s__('Badges|Delete badge?')"
- footer-primary-button-variant="danger"
:footer-primary-button-text="s__('Badges|Delete badge')"
+ footer-primary-button-variant="danger"
@submit="onSubmitModal">
<div class="well">
<badge
diff --git a/app/assets/javascripts/behaviors/copy_to_clipboard.js b/app/assets/javascripts/behaviors/copy_to_clipboard.js
index e2a73a1797c..00419e80cbb 100644
--- a/app/assets/javascripts/behaviors/copy_to_clipboard.js
+++ b/app/assets/javascripts/behaviors/copy_to_clipboard.js
@@ -8,10 +8,10 @@ function showTooltip(target, title) {
if (!$target.data('hideTooltip')) {
$target
.attr('title', title)
- .tooltip('fixTitle')
+ .tooltip('_fixTitle')
.tooltip('show')
.attr('title', originalTitle)
- .tooltip('fixTitle');
+ .tooltip('_fixTitle');
}
}
@@ -52,7 +52,7 @@ export default function initCopyToClipboard() {
* data types to the intended values.
*/
$(document).on('copy', 'body > textarea[readonly]', (e) => {
- const clipboardData = e.originalEvent.clipboardData;
+ const { clipboardData } = e.originalEvent;
if (!clipboardData) return;
const text = e.target.value;
diff --git a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js
index 75cf90de0b5..5d7a3bed301 100644
--- a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js
+++ b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js
@@ -1,4 +1,4 @@
-/* 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 */
+/* eslint-disable object-shorthand, no-unused-vars, no-use-before-define, max-len, no-restricted-syntax, guard-for-in, no-continue */
import $ from 'jquery';
import _ from 'underscore';
@@ -119,7 +119,7 @@ const gfmRules = {
return el.outerHTML;
},
'dl'(el, text) {
- let lines = text.trim().split('\n');
+ let lines = text.replace(/\n\n/g, '\n').trim().split('\n');
// Add two spaces to the front of subsequent list items lines,
// or leave the line entirely blank.
lines = lines.map((l) => {
@@ -129,9 +129,13 @@ const gfmRules = {
return ` ${line}`;
});
- return `<dl>\n${lines.join('\n')}\n</dl>`;
+ return `<dl>\n${lines.join('\n')}\n</dl>\n`;
},
- 'sub, dt, dd, kbd, q, samp, var, ruby, rt, rp, abbr, summary, details'(el, text) {
+ 'dt, dd, summary, details'(el, text) {
+ const tag = el.nodeName.toLowerCase();
+ return `<${tag}>${text}</${tag}>\n`;
+ },
+ 'sup, sub, kbd, q, samp, var, ruby, rt, rp, abbr'(el, text) {
const tag = el.nodeName.toLowerCase();
return `<${tag}>${text}</${tag}>`;
},
@@ -215,22 +219,22 @@ const gfmRules = {
return text.replace(/^- /mg, '1. ');
},
'h1'(el, text) {
- return `# ${text.trim()}`;
+ return `# ${text.trim()}\n`;
},
'h2'(el, text) {
- return `## ${text.trim()}`;
+ return `## ${text.trim()}\n`;
},
'h3'(el, text) {
- return `### ${text.trim()}`;
+ return `### ${text.trim()}\n`;
},
'h4'(el, text) {
- return `#### ${text.trim()}`;
+ return `#### ${text.trim()}\n`;
},
'h5'(el, text) {
- return `##### ${text.trim()}`;
+ return `##### ${text.trim()}\n`;
},
'h6'(el, text) {
- return `###### ${text.trim()}`;
+ return `###### ${text.trim()}\n`;
},
'strong'(el, text) {
return `**${text}**`;
@@ -241,11 +245,13 @@ const gfmRules = {
'del'(el, text) {
return `~~${text}~~`;
},
- 'sup'(el, text) {
- return `^${text}`;
- },
'hr'(el) {
- return '-----';
+ // extra leading \n is to ensure that there is a blank line between
+ // a list followed by an hr, otherwise this breaks old redcarpet rendering
+ return '\n-----\n';
+ },
+ 'p'(el, text) {
+ return `${text.trim()}\n`;
},
'table'(el) {
const theadEl = el.querySelector('thead');
@@ -263,7 +269,9 @@ const gfmRules = {
let before = '';
let after = '';
- switch (cell.style.textAlign) {
+ const alignment = cell.align || cell.style.textAlign;
+
+ switch (alignment) {
case 'center':
before = ':';
after = ':';
@@ -313,7 +321,7 @@ export class CopyAsGFM {
}
static copyAsGFM(e, transformer) {
- const clipboardData = e.originalEvent.clipboardData;
+ const { clipboardData } = e.originalEvent;
if (!clipboardData) return;
const documentFragment = getSelectedFragment();
@@ -330,7 +338,7 @@ export class CopyAsGFM {
}
static pasteGFM(e) {
- const clipboardData = e.originalEvent.clipboardData;
+ const { clipboardData } = e.originalEvent;
if (!clipboardData) return;
const text = clipboardData.getData('text/plain');
diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js
index 3ec932bdb73..b6e2781773c 100644
--- a/app/assets/javascripts/behaviors/quick_submit.js
+++ b/app/assets/javascripts/behaviors/quick_submit.js
@@ -69,7 +69,7 @@ $(document).on('keyup.quick_submit', '.js-quick-submit input[type=submit], .js-q
$this.tooltip({
container: 'body',
html: 'true',
- placement: 'auto top',
+ placement: 'top',
title,
trigger: 'manual',
});
diff --git a/app/assets/javascripts/behaviors/requires_input.js b/app/assets/javascripts/behaviors/requires_input.js
index ffff4ddb71a..a8b6dbf0948 100644
--- a/app/assets/javascripts/behaviors/requires_input.js
+++ b/app/assets/javascripts/behaviors/requires_input.js
@@ -42,9 +42,9 @@ $.fn.requiresInput = function requiresInput() {
function hideOrShowHelpBlock(form) {
const selected = $('.js-select-namespace option:selected');
if (selected.length && selected.data('optionsParent') === 'groups') {
- form.find('.help-block').hide();
+ form.find('.form-text.text-muted').hide();
} else if (selected.length) {
- form.find('.help-block').show();
+ form.find('.form-text.text-muted').show();
}
}
diff --git a/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js b/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js
index c17877a276d..7986287f7e7 100644
--- a/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js
+++ b/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js
@@ -2,9 +2,9 @@ import sqljs from 'sql.js';
import { template as _template } from 'underscore';
const PREVIEW_TEMPLATE = _template(`
- <div class="panel panel-default">
- <div class="panel-heading"><%- name %></div>
- <div class="panel-body">
+ <div class="card">
+ <div class="card-header"><%- name %></div>
+ <div class="card-body">
<img class="img-thumbnail" src="data:image/png;base64,<%- image %>"/>
</div>
</div>
@@ -84,7 +84,7 @@ class BalsamiqViewer {
renderTemplate(preview) {
const resource = this.getResource(preview.resourceID);
const name = BalsamiqViewer.parseTitle(resource);
- const image = preview.image;
+ const { image } = preview;
const template = PREVIEW_TEMPLATE({
name,
diff --git a/app/assets/javascripts/blob/balsamiq_viewer.js b/app/assets/javascripts/blob/balsamiq_viewer.js
index 06ef86ecb77..b88e69a07bf 100644
--- a/app/assets/javascripts/blob/balsamiq_viewer.js
+++ b/app/assets/javascripts/blob/balsamiq_viewer.js
@@ -12,7 +12,7 @@ export default function loadBalsamiqFile() {
if (!(viewer instanceof Element)) return;
- const endpoint = viewer.dataset.endpoint;
+ const { endpoint } = viewer.dataset;
const balsamiqViewer = new BalsamiqViewer(viewer);
balsamiqViewer.loadFile(endpoint).catch(onError);
diff --git a/app/assets/javascripts/blob/pdf/index.js b/app/assets/javascripts/blob/pdf/index.js
index 70136cc4087..7d5f487c4ba 100644
--- a/app/assets/javascripts/blob/pdf/index.js
+++ b/app/assets/javascripts/blob/pdf/index.js
@@ -1,4 +1,3 @@
-/* eslint-disable no-new */
import Vue from 'vue';
import pdfLab from '../../pdf/index.vue';
diff --git a/app/assets/javascripts/blob/sketch/index.js b/app/assets/javascripts/blob/sketch/index.js
index 0799991aa40..13318c58006 100644
--- a/app/assets/javascripts/blob/sketch/index.js
+++ b/app/assets/javascripts/blob/sketch/index.js
@@ -44,7 +44,7 @@ export default class SketchLoader {
previewLink.href = previewUrl;
previewLink.target = '_blank';
previewImage.src = previewUrl;
- previewImage.className = 'img-responsive';
+ previewImage.className = 'img-fluid';
previewLink.appendChild(previewImage);
this.container.appendChild(previewLink);
diff --git a/app/assets/javascripts/blob/stl_viewer.js b/app/assets/javascripts/blob/stl_viewer.js
index 63236b6477f..339906adc34 100644
--- a/app/assets/javascripts/blob/stl_viewer.js
+++ b/app/assets/javascripts/blob/stl_viewer.js
@@ -5,7 +5,7 @@ export default () => {
[].slice.call(document.querySelectorAll('.js-material-changer')).forEach((el) => {
el.addEventListener('click', (e) => {
- const target = e.target;
+ const { target } = e;
e.preventDefault();
diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js
index 137e1f5a099..5485248cfaf 100644
--- a/app/assets/javascripts/blob/viewer/index.js
+++ b/app/assets/javascripts/blob/viewer/index.js
@@ -70,7 +70,7 @@ export default class BlobViewer {
const initialViewer = this.$fileHolder[0].querySelector('.blob-viewer:not(.hidden)');
let initialViewerName = initialViewer.getAttribute('data-type');
- if (this.switcher && location.hash.indexOf('#L') === 0) {
+ if (this.switcher && window.location.hash.indexOf('#L') === 0) {
initialViewerName = 'simple';
}
@@ -116,7 +116,7 @@ export default class BlobViewer {
this.copySourceBtn.classList.add('disabled');
}
- $(this.copySourceBtn).tooltip('fixTitle');
+ $(this.copySourceBtn).tooltip('_fixTitle');
}
switchToViewer(name) {
diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js
index 4424232f642..a603d89b84a 100644
--- a/app/assets/javascripts/blob_edit/blob_bundle.js
+++ b/app/assets/javascripts/blob_edit/blob_bundle.js
@@ -1,5 +1,4 @@
-/* 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 */
+/* eslint-disable no-new */
import $ from 'jquery';
import NewCommitForm from '../new_commit_form';
diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js
index bea818010a4..a2355d7fd5c 100644
--- a/app/assets/javascripts/boards/components/board.js
+++ b/app/assets/javascripts/boards/components/board.js
@@ -1,7 +1,6 @@
-/* eslint-disable comma-dangle, space-before-function-paren, one-var */
+/* eslint-disable comma-dangle */
-import $ from 'jquery';
-import Sortable from 'vendor/Sortable';
+import Sortable from 'sortablejs';
import Vue from 'vue';
import AccessorUtilities from '../../lib/utils/accessor';
import boardList from './board_list.vue';
@@ -14,17 +13,28 @@ window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.Board = Vue.extend({
- template: '#js-board-template',
components: {
boardList,
'board-delete': gl.issueBoards.BoardDelete,
BoardBlankState,
},
props: {
- list: Object,
- disabled: Boolean,
- issueLinkBase: String,
- rootPath: String,
+ list: {
+ type: Object,
+ default: () => ({}),
+ },
+ disabled: {
+ type: Boolean,
+ required: true,
+ },
+ issueLinkBase: {
+ type: String,
+ required: true,
+ },
+ rootPath: {
+ type: String,
+ required: true,
+ },
boardId: {
type: String,
required: true,
@@ -46,56 +56,8 @@ gl.issueBoards.Board = Vue.extend({
});
},
deep: true,
- },
- detailIssue: {
- handler () {
- if (!Object.keys(this.detailIssue.issue).length) return;
-
- const issue = this.list.findIssue(this.detailIssue.issue.id);
-
- if (issue) {
- const offsetLeft = this.$el.offsetLeft;
- const boardsList = document.querySelectorAll('.boards-list')[0];
- const left = boardsList.scrollLeft - offsetLeft;
- let right = (offsetLeft + this.$el.offsetWidth);
-
- if (window.innerWidth > 768 && boardsList.classList.contains('is-compact')) {
- // -290 here because width of boardsList is animating so therefore
- // getting the width here is incorrect
- // 290 is the width of the sidebar
- right -= (boardsList.offsetWidth - 290);
- } else {
- right -= boardsList.offsetWidth;
- }
-
- if (right - boardsList.scrollLeft > 0) {
- $(boardsList).animate({
- scrollLeft: right
- }, this.sortableOptions.animation);
- } else if (left > 0) {
- $(boardsList).animate({
- scrollLeft: offsetLeft
- }, this.sortableOptions.animation);
- }
- }
- },
- deep: true
}
},
- methods: {
- showNewIssueForm() {
- this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm;
- },
- toggleExpanded(e) {
- if (this.list.isExpandable && !e.target.classList.contains('js-no-trigger-collapse')) {
- this.list.isExpanded = !this.list.isExpanded;
-
- if (AccessorUtilities.isLocalStorageAccessSafe()) {
- localStorage.setItem(`boards.${this.boardId}.${this.list.type}.expanded`, this.list.isExpanded);
- }
- }
- },
- },
mounted () {
this.sortableOptions = gl.issueBoards.getBoardSortableDefaultOptions({
disabled: this.disabled,
@@ -125,4 +87,19 @@ gl.issueBoards.Board = Vue.extend({
this.list.isExpanded = !isCollapsed;
}
},
+ methods: {
+ showNewIssueForm() {
+ this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm;
+ },
+ toggleExpanded(e) {
+ if (this.list.isExpandable && !e.target.classList.contains('js-no-trigger-collapse')) {
+ this.list.isExpanded = !this.list.isExpanded;
+
+ if (AccessorUtilities.isLocalStorageAccessSafe()) {
+ localStorage.setItem(`boards.${this.boardId}.${this.list.type}.expanded`, this.list.isExpanded);
+ }
+ }
+ },
+ },
+ template: '#js-board-template',
});
diff --git a/app/assets/javascripts/boards/components/board_blank_state.vue b/app/assets/javascripts/boards/components/board_blank_state.vue
index 2049eeb9c30..286529b4d13 100644
--- a/app/assets/javascripts/boards/components/board_blank_state.vue
+++ b/app/assets/javascripts/boards/components/board_blank_state.vue
@@ -72,8 +72,8 @@ export default {
:key="index"
>
<span
- class="label-color"
- :style="{ backgroundColor: label.color }">
+ :style="{ backgroundColor: label.color }"
+ class="label-color">
</span>
{{ label.title }}
</li>
diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue
index 84885ca9306..0398102ad02 100644
--- a/app/assets/javascripts/boards/components/board_card.vue
+++ b/app/assets/javascripts/boards/components/board_card.vue
@@ -1,83 +1,82 @@
<script>
-/* eslint-disable vue/require-default-prop */
-import './issue_card_inner';
-import eventHub from '../eventhub';
+ /* eslint-disable vue/require-default-prop */
+ import IssueCardInner from './issue_card_inner.vue';
+ import eventHub from '../eventhub';
-const Store = gl.issueBoards.BoardsStore;
+ const Store = gl.issueBoards.BoardsStore;
-export default {
- name: 'BoardsIssueCard',
- components: {
- 'issue-card-inner': gl.issueBoards.IssueCardInner,
- },
- props: {
- list: {
- type: Object,
- default: () => ({}),
+ export default {
+ name: 'BoardsIssueCard',
+ components: {
+ IssueCardInner,
},
- issue: {
- type: Object,
- default: () => ({}),
+ props: {
+ list: {
+ type: Object,
+ default: () => ({}),
+ },
+ issue: {
+ type: Object,
+ default: () => ({}),
+ },
+ issueLinkBase: {
+ type: String,
+ default: '',
+ },
+ disabled: {
+ type: Boolean,
+ default: false,
+ },
+ index: {
+ type: Number,
+ default: 0,
+ },
+ rootPath: {
+ type: String,
+ default: '',
+ },
+ groupId: {
+ type: Number,
+ },
},
- issueLinkBase: {
- type: String,
- default: '',
+ data() {
+ return {
+ showDetail: false,
+ detailIssue: Store.detail,
+ };
},
- disabled: {
- type: Boolean,
- default: false,
+ computed: {
+ issueDetailVisible() {
+ return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id;
+ },
},
- index: {
- type: Number,
- default: 0,
- },
- rootPath: {
- type: String,
- default: '',
- },
- groupId: {
- type: Number,
- },
- },
- 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) {
+ 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;
+ 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,
@@ -85,6 +84,7 @@ export default {
}"
:index="index"
:data-issue-id="issue.id"
+ class="board-card"
@mousedown="mouseDown"
@mousemove="mouseMove"
@mouseup="showIssue($event)">
diff --git a/app/assets/javascripts/boards/components/board_delete.js b/app/assets/javascripts/boards/components/board_delete.js
index 7be98825fda..c5945e8098d 100644
--- a/app/assets/javascripts/boards/components/board_delete.js
+++ b/app/assets/javascripts/boards/components/board_delete.js
@@ -1,4 +1,4 @@
-/* eslint-disable comma-dangle, space-before-function-paren, no-alert */
+/* eslint-disable comma-dangle, no-alert */
import $ from 'jquery';
import Vue from 'vue';
@@ -8,13 +8,16 @@ window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.BoardDelete = Vue.extend({
props: {
- list: Object
+ list: {
+ type: Object,
+ default: () => ({}),
+ },
},
methods: {
deleteBoard () {
$(this.$el).tooltip('hide');
- if (confirm('Are you sure you want to delete this list?')) {
+ if (window.confirm('Are you sure you want to delete this list?')) {
this.list.destroy();
}
}
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index 0d03c1c419c..5c7565234d8 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -1,5 +1,5 @@
<script>
-import Sortable from 'vendor/Sortable';
+import Sortable from 'sortablejs';
import boardNewIssue from './board_new_issue.vue';
import boardCard from './board_card.vue';
import eventHub from '../eventhub';
@@ -87,10 +87,46 @@ export default {
mounted() {
const options = gl.issueBoards.getBoardSortableDefaultOptions({
scroll: true,
- group: 'issues',
disabled: this.disabled,
filter: '.board-list-count, .is-disabled',
dataIdAttr: 'data-issue-id',
+ group: {
+ name: 'issues',
+ /**
+ * Dynamically determine between which containers
+ * items can be moved or copied as
+ * Assignee lists (EE feature) require this behavior
+ */
+ pull: (to, from, dragEl, e) => {
+ // As per Sortable's docs, `to` should provide
+ // reference to exact sortable container on which
+ // we're trying to drag element, but either it is
+ // a library's bug or our markup structure is too complex
+ // that `to` never points to correct container
+ // See https://github.com/RubaXa/Sortable/issues/1037
+ //
+ // So we use `e.target` which is always accurate about
+ // which element we're currently dragging our card upon
+ // So from there, we can get reference to actual container
+ // and thus the container type to enable Copy or Move
+ if (e.target) {
+ const containerEl = e.target.closest('.js-board-list') || e.target.querySelector('.js-board-list');
+ const toBoardType = containerEl.dataset.boardType;
+
+ if (toBoardType) {
+ const fromBoardType = this.list.type;
+
+ if ((fromBoardType === 'assignee' && toBoardType === 'label') ||
+ (fromBoardType === 'label' && toBoardType === 'assignee')) {
+ return 'clone';
+ }
+ }
+ }
+
+ return true;
+ },
+ revertClone: true,
+ },
onStart: (e) => {
const card = this.$refs.issue[e.oldIndex];
@@ -169,21 +205,22 @@ export default {
<template>
<div class="board-list-component">
<div
+ v-if="loading"
class="board-list-loading text-center"
- aria-label="Loading issues"
- v-if="loading">
+ aria-label="Loading issues">
<loading-icon />
</div>
<board-new-issue
+ v-if="list.type !== 'closed' && showIssueForm"
:group-id="groupId"
- :list="list"
- v-if="list.type !== 'closed' && showIssueForm"/>
+ :list="list"/>
<ul
- class="board-list"
v-show="!loading"
ref="list"
:data-board="list.id"
- :class="{ 'is-smaller': showIssueForm }">
+ :data-board-type="list.type"
+ :class="{ 'is-smaller': showIssueForm }"
+ class="board-list js-board-list">
<board-card
v-for="(issue, index) in issues"
ref="issue"
@@ -196,8 +233,8 @@ export default {
:disabled="disabled"
:key="issue.id" />
<li
- class="board-list-count text-center"
v-if="showCount"
+ class="board-list-count text-center"
data-issue-id="-1">
<loading-icon
v-show="list.loadingMore"
diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue
index 8d84c1735b8..ec23b1e7c11 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue.vue
@@ -49,11 +49,12 @@ export default {
this.error = false;
const labels = this.list.label ? [this.list.label] : [];
+ const assignees = this.list.assignee ? [this.list.assignee] : [];
const issue = new ListIssue({
title: this.title,
labels,
subscribed: true,
- assignees: [],
+ assignees,
project_id: this.selectedProject.id,
});
@@ -92,29 +93,29 @@ export default {
<template>
<div class="board-new-issue-form">
- <div class="card">
+ <div class="board-card">
<form @submit="submit($event)">
<div
- class="flash-container"
v-if="error"
+ class="flash-container"
>
<div class="flash-alert">
An error occurred. Please try again.
</div>
</div>
<label
- class="label-light"
:for="list.id + '-title'"
+ class="label-light"
>
Title
</label>
<input
+ ref="input"
+ v-model="title"
+ :id="list.id + '-title'"
class="form-control"
type="text"
- v-model="title"
- ref="input"
autocomplete="off"
- :id="list.id + '-title'"
/>
<project-select
v-if="groupId"
@@ -122,15 +123,15 @@ export default {
/>
<div class="clearfix prepend-top-10">
<button
- class="btn btn-success pull-left"
- type="submit"
- :disabled="disabled"
ref="submit-button"
+ :disabled="disabled"
+ class="btn btn-success float-left"
+ type="submit"
>
Submit issue
</button>
<button
- class="btn btn-default pull-right"
+ class="btn btn-default float-right"
type="button"
@click="cancel"
>
@@ -141,4 +142,3 @@ export default {
</div>
</div>
</template>
-
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index c4ee4f6c855..371be109229 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -1,4 +1,4 @@
-/* eslint-disable comma-dangle, space-before-function-paren, no-new */
+/* eslint-disable comma-dangle, no-new */
import $ from 'jquery';
import Vue from 'vue';
@@ -6,13 +6,13 @@ import Flash from '../../flash';
import { __ } from '../../locale';
import Sidebar from '../../right_sidebar';
import eventHub from '../../sidebar/event_hub';
-import assigneeTitle from '../../sidebar/components/assignees/assignee_title.vue';
-import assignees from '../../sidebar/components/assignees/assignees.vue';
+import AssigneeTitle from '../../sidebar/components/assignees/assignee_title.vue';
+import Assignees from '../../sidebar/components/assignees/assignees.vue';
import DueDateSelectors from '../../due_date_select';
-import './sidebar/remove_issue';
+import RemoveBtn from './sidebar/remove_issue.vue';
import IssuableContext from '../../issuable_context';
import LabelsSelect from '../../labels_select';
-import subscriptions from '../../sidebar/components/subscriptions/subscriptions.vue';
+import Subscriptions from '../../sidebar/components/subscriptions/subscriptions.vue';
import MilestoneSelect from '../../milestone_select';
const Store = gl.issueBoards.BoardsStore;
@@ -21,8 +21,17 @@ window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.BoardSidebar = Vue.extend({
+ components: {
+ AssigneeTitle,
+ Assignees,
+ RemoveBtn,
+ Subscriptions,
+ },
props: {
- currentUser: Object
+ currentUser: {
+ type: Object,
+ default: () => ({}),
+ },
},
data() {
return {
@@ -64,6 +73,26 @@ gl.issueBoards.BoardSidebar = Vue.extend({
deep: true
},
},
+ created () {
+ // Get events from glDropdown
+ eventHub.$on('sidebar.removeAssignee', this.removeAssignee);
+ eventHub.$on('sidebar.addAssignee', this.addAssignee);
+ eventHub.$on('sidebar.removeAllAssignees', this.removeAllAssignees);
+ eventHub.$on('sidebar.saveAssignees', this.saveAssignees);
+ },
+ beforeDestroy() {
+ eventHub.$off('sidebar.removeAssignee', this.removeAssignee);
+ eventHub.$off('sidebar.addAssignee', this.addAssignee);
+ eventHub.$off('sidebar.removeAllAssignees', this.removeAllAssignees);
+ eventHub.$off('sidebar.saveAssignees', this.saveAssignees);
+ },
+ mounted () {
+ new IssuableContext(this.currentUser);
+ new MilestoneSelect();
+ new DueDateSelectors();
+ new LabelsSelect();
+ new Sidebar();
+ },
methods: {
closeSidebar () {
this.detail.issue = {};
@@ -97,30 +126,4 @@ gl.issueBoards.BoardSidebar = Vue.extend({
});
},
},
- created () {
- // Get events from glDropdown
- eventHub.$on('sidebar.removeAssignee', this.removeAssignee);
- eventHub.$on('sidebar.addAssignee', this.addAssignee);
- eventHub.$on('sidebar.removeAllAssignees', this.removeAllAssignees);
- eventHub.$on('sidebar.saveAssignees', this.saveAssignees);
- },
- beforeDestroy() {
- eventHub.$off('sidebar.removeAssignee', this.removeAssignee);
- eventHub.$off('sidebar.addAssignee', this.addAssignee);
- eventHub.$off('sidebar.removeAllAssignees', this.removeAllAssignees);
- eventHub.$off('sidebar.saveAssignees', this.saveAssignees);
- },
- mounted () {
- new IssuableContext(this.currentUser);
- new MilestoneSelect();
- new DueDateSelectors();
- new LabelsSelect();
- new Sidebar();
- },
- components: {
- assigneeTitle,
- assignees,
- removeBtn: gl.issueBoards.RemoveIssueBtn,
- subscriptions,
- },
});
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js
deleted file mode 100644
index 84fe9b1288a..00000000000
--- a/app/assets/javascripts/boards/components/issue_card_inner.js
+++ /dev/null
@@ -1,195 +0,0 @@
-import $ from 'jquery';
-import Vue from 'vue';
-import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
-import eventHub from '../eventhub';
-
-const Store = gl.issueBoards.BoardsStore;
-
-window.gl = window.gl || {};
-window.gl.issueBoards = window.gl.issueBoards || {};
-
-gl.issueBoards.IssueCardInner = Vue.extend({
- props: {
- issue: {
- type: Object,
- required: true,
- },
- issueLinkBase: {
- type: String,
- required: true,
- },
- list: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- rootPath: {
- type: String,
- required: true,
- },
- updateFilters: {
- type: Boolean,
- required: false,
- default: false,
- },
- groupId: {
- type: Number,
- required: false,
- },
- },
- data() {
- return {
- limitBeforeCounter: 3,
- maxRender: 4,
- maxCounter: 99,
- };
- },
- components: {
- UserAvatarLink,
- },
- computed: {
- numberOverLimit() {
- return this.issue.assignees.length - this.limitBeforeCounter;
- },
- assigneeCounterTooltip() {
- return `${this.assigneeCounterLabel} more`;
- },
- assigneeCounterLabel() {
- if (this.numberOverLimit > this.maxCounter) {
- return `${this.maxCounter}+`;
- }
-
- return `+${this.numberOverLimit}`;
- },
- shouldRenderCounter() {
- if (this.issue.assignees.length <= this.maxRender) {
- return false;
- }
-
- return this.issue.assignees.length > this.numberOverLimit;
- },
- issueId() {
- if (this.issue.iid) {
- return `#${this.issue.iid}`;
- }
- return false;
- },
- showLabelFooter() {
- return this.issue.labels.find(l => this.showLabel(l)) !== undefined;
- },
- },
- methods: {
- isIndexLessThanlimit(index) {
- return index < this.limitBeforeCounter;
- },
- shouldRenderAssignee(index) {
- // Eg. maxRender is 4,
- // Render up to all 4 assignees if there are only 4 assigness
- // Otherwise render up to the limitBeforeCounter
- if (this.issue.assignees.length <= this.maxRender) {
- return index < this.maxRender;
- }
-
- return index < this.limitBeforeCounter;
- },
- assigneeUrl(assignee) {
- return `${this.rootPath}${assignee.username}`;
- },
- assigneeUrlTitle(assignee) {
- return `Assigned to ${assignee.name}`;
- },
- avatarUrlTitle(assignee) {
- return `Avatar for ${assignee.name}`;
- },
- showLabel(label) {
- if (!label.id) return false;
- return true;
- },
- filterByLabel(label, e) {
- if (!this.updateFilters) return;
-
- const filterPath = gl.issueBoards.BoardsStore.filter.path.split('&');
- const labelTitle = encodeURIComponent(label.title);
- const param = `label_name[]=${labelTitle}`;
- const labelIndex = filterPath.indexOf(param);
- $(e.currentTarget).tooltip('hide');
-
- if (labelIndex === -1) {
- filterPath.push(param);
- } else {
- filterPath.splice(labelIndex, 1);
- }
-
- gl.issueBoards.BoardsStore.filter.path = filterPath.join('&');
-
- Store.updateFiltersUrl();
-
- eventHub.$emit('updateTokens');
- },
- labelStyle(label) {
- return {
- backgroundColor: label.color,
- color: label.textColor,
- };
- },
- },
- template: `
- <div>
- <div class="card-header">
- <h4 class="card-title">
- <i
- class="fa fa-eye-slash confidential-icon"
- v-if="issue.confidential"
- aria-hidden="true"
- />
- <a
- class="js-no-trigger"
- :href="issue.path"
- :title="issue.title">{{ issue.title }}</a>
- <span
- class="card-number"
- v-if="issueId"
- >
- {{ issue.referencePath }}
- </span>
- </h4>
- <div class="card-assignee">
- <user-avatar-link
- v-for="(assignee, index) in issue.assignees"
- :key="assignee.id"
- v-if="shouldRenderAssignee(index)"
- class="js-no-trigger"
- :link-href="assigneeUrl(assignee)"
- :img-alt="avatarUrlTitle(assignee)"
- :img-src="assignee.avatar"
- :tooltip-text="assigneeUrlTitle(assignee)"
- tooltip-placement="bottom"
- />
- <span
- class="avatar-counter has-tooltip"
- :title="assigneeCounterTooltip"
- v-if="shouldRenderCounter"
- >
- {{ assigneeCounterLabel }}
- </span>
- </div>
- </div>
- <div
- class="card-footer"
- v-if="showLabelFooter"
- >
- <button
- class="label color-label has-tooltip"
- v-for="label in issue.labels"
- type="button"
- v-if="showLabel(label)"
- @click="filterByLabel(label, $event)"
- :style="labelStyle(label)"
- :title="label.description"
- data-container="body">
- {{ label.title }}
- </button>
- </div>
- </div>
- `,
-});
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue
new file mode 100644
index 00000000000..d50641dc3a9
--- /dev/null
+++ b/app/assets/javascripts/boards/components/issue_card_inner.vue
@@ -0,0 +1,202 @@
+<script>
+ import $ from 'jquery';
+ import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
+ import eventHub from '../eventhub';
+ import tooltip from '../../vue_shared/directives/tooltip';
+
+ const Store = gl.issueBoards.BoardsStore;
+
+ export default {
+ components: {
+ UserAvatarLink,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ issue: {
+ type: Object,
+ required: true,
+ },
+ issueLinkBase: {
+ type: String,
+ required: true,
+ },
+ list: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ rootPath: {
+ type: String,
+ required: true,
+ },
+ updateFilters: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ groupId: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ limitBeforeCounter: 3,
+ maxRender: 4,
+ maxCounter: 99,
+ };
+ },
+ computed: {
+ numberOverLimit() {
+ return this.issue.assignees.length - this.limitBeforeCounter;
+ },
+ assigneeCounterTooltip() {
+ return `${this.assigneeCounterLabel} more`;
+ },
+ assigneeCounterLabel() {
+ if (this.numberOverLimit > this.maxCounter) {
+ return `${this.maxCounter}+`;
+ }
+
+ return `+${this.numberOverLimit}`;
+ },
+ shouldRenderCounter() {
+ if (this.issue.assignees.length <= this.maxRender) {
+ return false;
+ }
+
+ return this.issue.assignees.length > this.numberOverLimit;
+ },
+ issueId() {
+ if (this.issue.iid) {
+ return `#${this.issue.iid}`;
+ }
+ return false;
+ },
+ showLabelFooter() {
+ return this.issue.labels.find(l => this.showLabel(l)) !== undefined;
+ },
+ },
+ methods: {
+ isIndexLessThanlimit(index) {
+ return index < this.limitBeforeCounter;
+ },
+ shouldRenderAssignee(index) {
+ // Eg. maxRender is 4,
+ // Render up to all 4 assignees if there are only 4 assigness
+ // Otherwise render up to the limitBeforeCounter
+ if (this.issue.assignees.length <= this.maxRender) {
+ return index < this.maxRender;
+ }
+
+ return index < this.limitBeforeCounter;
+ },
+ assigneeUrl(assignee) {
+ return `${this.rootPath}${assignee.username}`;
+ },
+ assigneeUrlTitle(assignee) {
+ return `Assigned to ${assignee.name}`;
+ },
+ avatarUrlTitle(assignee) {
+ return `Avatar for ${assignee.name}`;
+ },
+ showLabel(label) {
+ if (!label.id) return false;
+ return true;
+ },
+ filterByLabel(label, e) {
+ if (!this.updateFilters) return;
+
+ const filterPath = gl.issueBoards.BoardsStore.filter.path.split('&');
+ const labelTitle = encodeURIComponent(label.title);
+ const param = `label_name[]=${labelTitle}`;
+ const labelIndex = filterPath.indexOf(param);
+ $(e.currentTarget).tooltip('hide');
+
+ if (labelIndex === -1) {
+ filterPath.push(param);
+ } else {
+ filterPath.splice(labelIndex, 1);
+ }
+
+ gl.issueBoards.BoardsStore.filter.path = filterPath.join('&');
+
+ Store.updateFiltersUrl();
+
+ eventHub.$emit('updateTokens');
+ },
+ labelStyle(label) {
+ return {
+ backgroundColor: label.color,
+ color: label.textColor,
+ };
+ },
+ },
+ };
+</script>
+<template>
+ <div>
+ <div class="board-card-header">
+ <h4 class="board-card-title">
+ <i
+ v-if="issue.confidential"
+ class="fa fa-eye-slash confidential-icon"
+ aria-hidden="true"
+ ></i>
+ <a
+ :href="issue.path"
+ :title="issue.title"
+ class="js-no-trigger">{{ issue.title }}</a>
+ <span
+ v-if="issueId"
+ class="board-card-number"
+ >
+ {{ issue.referencePath }}
+ </span>
+ </h4>
+ <div class="board-card-assignee">
+ <user-avatar-link
+ v-for="(assignee, index) in issue.assignees"
+ v-if="shouldRenderAssignee(index)"
+ :key="assignee.id"
+ :link-href="assigneeUrl(assignee)"
+ :img-alt="avatarUrlTitle(assignee)"
+ :img-src="assignee.avatar"
+ :tooltip-text="assigneeUrlTitle(assignee)"
+ class="js-no-trigger"
+ tooltip-placement="bottom"
+ />
+ <span
+ v-tooltip
+ v-if="shouldRenderCounter"
+ :title="assigneeCounterTooltip"
+ class="avatar-counter"
+ >
+ {{ assigneeCounterLabel }}
+ </span>
+ </div>
+ </div>
+ <div
+ v-if="showLabelFooter"
+ class="board-card-footer"
+ >
+ <button
+ v-tooltip
+ v-for="label in issue.labels"
+ v-if="showLabel(label)"
+ :key="label.id"
+ :style="labelStyle(label)"
+ :title="label.description"
+ class="badge color-label"
+ type="button"
+ data-container="body"
+ @click="filterByLabel(label, $event)"
+ >
+ {{ label.title }}
+ </button>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js b/app/assets/javascripts/boards/components/modal/empty_state.js
deleted file mode 100644
index 9e37f95cdd6..00000000000
--- a/app/assets/javascripts/boards/components/modal/empty_state.js
+++ /dev/null
@@ -1,69 +0,0 @@
-import Vue from 'vue';
-import ModalStore from '../../stores/modal_store';
-import modalMixin from '../../mixins/modal_mixins';
-
-gl.issueBoards.ModalEmptyState = Vue.extend({
- mixins: [modalMixin],
- data() {
- return ModalStore.store;
- },
- props: {
- newIssuePath: {
- type: String,
- required: true,
- },
- emptyStateSvg: {
- type: String,
- required: true,
- },
- },
- computed: {
- contents() {
- const obj = {
- title: 'You haven\'t added any issues to your project yet',
- content: `
- An issue can be a bug, a todo or a feature request that needs to be
- discussed in a project. Besides, issues are searchable and filterable.
- `,
- };
-
- if (this.activeTab === 'selected') {
- obj.title = 'You haven\'t selected any issues yet';
- obj.content = `
- Go back to <strong>Open issues</strong> and select some issues
- to add to your board.
- `;
- }
-
- return obj;
- },
- },
- template: `
- <section class="empty-state">
- <div class="row">
- <div class="col-xs-12 col-sm-6 col-sm-push-6">
- <aside class="svg-content"><img :src="emptyStateSvg"/></aside>
- </div>
- <div class="col-xs-12 col-sm-6 col-sm-pull-6">
- <div class="text-content">
- <h4>{{ contents.title }}</h4>
- <p v-html="contents.content"></p>
- <a
- :href="newIssuePath"
- class="btn btn-success btn-inverted"
- v-if="activeTab === 'all'">
- New issue
- </a>
- <button
- type="button"
- class="btn btn-default"
- @click="changeTab('all')"
- v-if="activeTab === 'selected'">
- Open issues
- </button>
- </div>
- </div>
- </div>
- </section>
- `,
-});
diff --git a/app/assets/javascripts/boards/components/modal/empty_state.vue b/app/assets/javascripts/boards/components/modal/empty_state.vue
new file mode 100644
index 00000000000..dbd69f84526
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/empty_state.vue
@@ -0,0 +1,73 @@
+<script>
+import ModalStore from '../../stores/modal_store';
+import modalMixin from '../../mixins/modal_mixins';
+
+export default {
+ mixins: [modalMixin],
+ props: {
+ newIssuePath: {
+ type: String,
+ required: true,
+ },
+ emptyStateSvg: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return ModalStore.store;
+ },
+ computed: {
+ contents() {
+ const obj = {
+ title: 'You haven\'t added any issues to your project yet',
+ content: `
+ An issue can be a bug, a todo or a feature request that needs to be
+ discussed in a project. Besides, issues are searchable and filterable.
+ `,
+ };
+
+ if (this.activeTab === 'selected') {
+ obj.title = 'You haven\'t selected any issues yet';
+ obj.content = `
+ Go back to <strong>Open issues</strong> and select some issues
+ to add to your board.
+ `;
+ }
+
+ return obj;
+ },
+ },
+};
+</script>
+
+<template>
+ <section class="empty-state">
+ <div class="row">
+ <div class="col-12 col-md-6 order-md-last">
+ <aside class="svg-content"><img :src="emptyStateSvg"/></aside>
+ </div>
+ <div class="col-12 col-md-6 order-md-first">
+ <div class="text-content">
+ <h4>{{ contents.title }}</h4>
+ <p v-html="contents.content"></p>
+ <a
+ v-if="activeTab === 'all'"
+ :href="newIssuePath"
+ class="btn btn-success btn-inverted"
+ >
+ New issue
+ </a>
+ <button
+ v-if="activeTab === 'selected'"
+ class="btn btn-default"
+ type="button"
+ @click="changeTab('all')"
+ >
+ Open issues
+ </button>
+ </div>
+ </div>
+ </div>
+ </section>
+</template>
diff --git a/app/assets/javascripts/boards/components/modal/footer.js b/app/assets/javascripts/boards/components/modal/footer.js
deleted file mode 100644
index 9735e0ddacc..00000000000
--- a/app/assets/javascripts/boards/components/modal/footer.js
+++ /dev/null
@@ -1,82 +0,0 @@
-import Vue from 'vue';
-import Flash from '../../../flash';
-import { __ } from '../../../locale';
-import './lists_dropdown';
-import { pluralize } from '../../../lib/utils/text_utility';
-import ModalStore from '../../stores/modal_store';
-import modalMixin from '../../mixins/modal_mixins';
-
-gl.issueBoards.ModalFooter = Vue.extend({
- mixins: [modalMixin],
- data() {
- return {
- modal: ModalStore.store,
- state: gl.issueBoards.BoardsStore.state,
- };
- },
- computed: {
- submitDisabled() {
- return !ModalStore.selectedCount();
- },
- submitText() {
- const count = ModalStore.selectedCount();
-
- return `Add ${count > 0 ? count : ''} ${pluralize('issue', count)}`;
- },
- },
- methods: {
- addIssues() {
- const firstListIndex = 1;
- const list = this.modal.selectedList || this.state.lists[firstListIndex];
- const selectedIssues = ModalStore.getSelectedIssues();
- const issueIds = selectedIssues.map(issue => issue.id);
-
- // Post the data to the backend
- gl.boardService.bulkUpdate(issueIds, {
- add_label_ids: [list.label.id],
- }).catch(() => {
- Flash(__('Failed to update issues, please try again.'));
-
- selectedIssues.forEach((issue) => {
- list.removeIssue(issue);
- list.issuesSize -= 1;
- });
- });
-
- // Add the issues on the frontend
- selectedIssues.forEach((issue) => {
- list.addIssue(issue);
- list.issuesSize += 1;
- });
-
- this.toggleModal(false);
- },
- },
- components: {
- 'lists-dropdown': gl.issueBoards.ModalFooterListsDropdown,
- },
- template: `
- <footer
- class="form-actions add-issues-footer">
- <div class="pull-left">
- <button
- class="btn btn-success"
- type="button"
- :disabled="submitDisabled"
- @click="addIssues">
- {{ submitText }}
- </button>
- <span class="inline add-issues-footer-to-list">
- to list
- </span>
- <lists-dropdown></lists-dropdown>
- </div>
- <button
- class="btn btn-default pull-right"
- type="button"
- @click="toggleModal(false)">
- Cancel
- </button>
- </footer>
- `,
-});
diff --git a/app/assets/javascripts/boards/components/modal/footer.vue b/app/assets/javascripts/boards/components/modal/footer.vue
new file mode 100644
index 00000000000..d4affc8c3de
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/footer.vue
@@ -0,0 +1,92 @@
+<script>
+import Flash from '../../../flash';
+import { __ } from '../../../locale';
+import ListsDropdown from './lists_dropdown.vue';
+import { pluralize } from '../../../lib/utils/text_utility';
+import ModalStore from '../../stores/modal_store';
+import modalMixin from '../../mixins/modal_mixins';
+
+export default {
+ components: {
+ ListsDropdown,
+ },
+ mixins: [modalMixin],
+ data() {
+ return {
+ modal: ModalStore.store,
+ state: gl.issueBoards.BoardsStore.state,
+ };
+ },
+ computed: {
+ submitDisabled() {
+ return !ModalStore.selectedCount();
+ },
+ submitText() {
+ const count = ModalStore.selectedCount();
+
+ return `Add ${count > 0 ? count : ''} ${pluralize('issue', count)}`;
+ },
+ },
+ methods: {
+ buildUpdateRequest(list) {
+ return {
+ add_label_ids: [list.label.id],
+ };
+ },
+ addIssues() {
+ const firstListIndex = 1;
+ const list = this.modal.selectedList || this.state.lists[firstListIndex];
+ const selectedIssues = ModalStore.getSelectedIssues();
+ const issueIds = selectedIssues.map(issue => issue.id);
+ const req = this.buildUpdateRequest(list);
+
+ // Post the data to the backend
+ gl.boardService
+ .bulkUpdate(issueIds, req)
+ .catch(() => {
+ Flash(__('Failed to update issues, please try again.'));
+
+ selectedIssues.forEach((issue) => {
+ list.removeIssue(issue);
+ list.issuesSize -= 1;
+ });
+ });
+
+ // Add the issues on the frontend
+ selectedIssues.forEach((issue) => {
+ list.addIssue(issue);
+ list.issuesSize += 1;
+ });
+
+ this.toggleModal(false);
+ },
+ },
+};
+</script>
+<template>
+ <footer
+ class="form-actions add-issues-footer"
+ >
+ <div class="float-left">
+ <button
+ :disabled="submitDisabled"
+ class="btn btn-success"
+ type="button"
+ @click="addIssues"
+ >
+ {{ submitText }}
+ </button>
+ <span class="inline add-issues-footer-to-list">
+ to list
+ </span>
+ <lists-dropdown/>
+ </div>
+ <button
+ class="btn btn-default float-right"
+ type="button"
+ @click="toggleModal(false)"
+ >
+ Cancel
+ </button>
+ </footer>
+</template>
diff --git a/app/assets/javascripts/boards/components/modal/header.js b/app/assets/javascripts/boards/components/modal/header.js
deleted file mode 100644
index 67c29ebca72..00000000000
--- a/app/assets/javascripts/boards/components/modal/header.js
+++ /dev/null
@@ -1,79 +0,0 @@
-import Vue from 'vue';
-import modalFilters from './filters';
-import './tabs';
-import ModalStore from '../../stores/modal_store';
-import modalMixin from '../../mixins/modal_mixins';
-
-gl.issueBoards.ModalHeader = Vue.extend({
- mixins: [modalMixin],
- props: {
- projectId: {
- type: Number,
- required: true,
- },
- milestonePath: {
- type: String,
- required: true,
- },
- labelPath: {
- type: String,
- required: true,
- },
- },
- data() {
- return ModalStore.store;
- },
- computed: {
- selectAllText() {
- if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) {
- return 'Select all';
- }
-
- return 'Deselect all';
- },
- showSearch() {
- return this.activeTab === 'all' && !this.loading && this.issuesCount > 0;
- },
- },
- methods: {
- toggleAll() {
- this.$refs.selectAllBtn.blur();
-
- ModalStore.toggleAll();
- },
- },
- components: {
- 'modal-tabs': gl.issueBoards.ModalTabs,
- modalFilters,
- },
- template: `
- <div>
- <header class="add-issues-header form-actions">
- <h2>
- Add issues
- <button
- type="button"
- class="close"
- data-dismiss="modal"
- aria-label="Close"
- @click="toggleModal(false)">
- <span aria-hidden="true">×</span>
- </button>
- </h2>
- </header>
- <modal-tabs v-if="!loading && issuesCount > 0"></modal-tabs>
- <div
- class="add-issues-search append-bottom-10"
- v-if="showSearch">
- <modal-filters :store="filter" />
- <button
- type="button"
- class="btn btn-success btn-inverted prepend-left-10"
- ref="selectAllBtn"
- @click="toggleAll">
- {{ selectAllText }}
- </button>
- </div>
- </div>
- `,
-});
diff --git a/app/assets/javascripts/boards/components/modal/header.vue b/app/assets/javascripts/boards/components/modal/header.vue
new file mode 100644
index 00000000000..979fb4d7199
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/header.vue
@@ -0,0 +1,82 @@
+<script>
+ import ModalFilters from './filters';
+ import ModalTabs from './tabs.vue';
+ import ModalStore from '../../stores/modal_store';
+ import modalMixin from '../../mixins/modal_mixins';
+
+ export default {
+ components: {
+ ModalTabs,
+ ModalFilters,
+ },
+ mixins: [modalMixin],
+ props: {
+ projectId: {
+ type: Number,
+ required: true,
+ },
+ milestonePath: {
+ type: String,
+ required: true,
+ },
+ labelPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return ModalStore.store;
+ },
+ computed: {
+ selectAllText() {
+ if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) {
+ return 'Select all';
+ }
+
+ return 'Deselect all';
+ },
+ showSearch() {
+ return this.activeTab === 'all' && !this.loading && this.issuesCount > 0;
+ },
+ },
+ methods: {
+ toggleAll() {
+ this.$refs.selectAllBtn.blur();
+
+ ModalStore.toggleAll();
+ },
+ },
+ };
+</script>
+<template>
+ <div>
+ <header class="add-issues-header form-actions">
+ <h2>
+ Add issues
+ <button
+ type="button"
+ class="close"
+ data-dismiss="modal"
+ aria-label="Close"
+ @click="toggleModal(false)"
+ >
+ <span aria-hidden="true">×</span>
+ </button>
+ </h2>
+ </header>
+ <modal-tabs v-if="!loading && issuesCount > 0"/>
+ <div
+ v-if="showSearch"
+ class="add-issues-search append-bottom-10">
+ <modal-filters :store="filter" />
+ <button
+ ref="selectAllBtn"
+ type="button"
+ class="btn btn-success btn-inverted prepend-left-10"
+ @click="toggleAll"
+ >
+ {{ selectAllText }}
+ </button>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/boards/components/modal/index.js b/app/assets/javascripts/boards/components/modal/index.js
deleted file mode 100644
index 3083b3e4405..00000000000
--- a/app/assets/javascripts/boards/components/modal/index.js
+++ /dev/null
@@ -1,171 +0,0 @@
-/* global ListIssue */
-
-import Vue from 'vue';
-import queryData from '~/boards/utils/query_data';
-import loadingIcon from '~/vue_shared/components/loading_icon.vue';
-import './header';
-import './list';
-import './footer';
-import './empty_state';
-import ModalStore from '../../stores/modal_store';
-
-gl.issueBoards.IssuesModal = Vue.extend({
- props: {
- newIssuePath: {
- type: String,
- required: true,
- },
- emptyStateSvg: {
- type: String,
- required: true,
- },
- issueLinkBase: {
- type: String,
- required: true,
- },
- rootPath: {
- type: String,
- required: true,
- },
- projectId: {
- type: Number,
- required: true,
- },
- milestonePath: {
- type: String,
- required: true,
- },
- labelPath: {
- type: String,
- required: true,
- },
- },
- data() {
- return ModalStore.store;
- },
- watch: {
- page() {
- this.loadIssues();
- },
- showAddIssuesModal() {
- if (this.showAddIssuesModal && !this.issues.length) {
- this.loading = true;
- const loadingDone = () => {
- this.loading = false;
- };
-
- this.loadIssues()
- .then(loadingDone)
- .catch(loadingDone);
- } else if (!this.showAddIssuesModal) {
- this.issues = [];
- this.selectedIssues = [];
- this.issuesCount = false;
- }
- },
- filter: {
- handler() {
- if (this.$el.tagName) {
- this.page = 1;
- this.filterLoading = true;
- const loadingDone = () => {
- this.filterLoading = false;
- };
-
- this.loadIssues(true)
- .then(loadingDone)
- .catch(loadingDone);
- }
- },
- deep: true,
- },
- },
- methods: {
- loadIssues(clearIssues = false) {
- if (!this.showAddIssuesModal) return false;
-
- return gl.boardService.getBacklog(queryData(this.filter.path, {
- page: this.page,
- per: this.perPage,
- }))
- .then(res => res.data)
- .then((data) => {
- if (clearIssues) {
- this.issues = [];
- }
-
- data.issues.forEach((issueObj) => {
- const issue = new ListIssue(issueObj);
- const foundSelectedIssue = ModalStore.findSelectedIssue(issue);
- issue.selected = !!foundSelectedIssue;
-
- this.issues.push(issue);
- });
-
- this.loadingNewPage = false;
-
- if (!this.issuesCount) {
- this.issuesCount = data.size;
- }
- }).catch(() => {
- // TODO: handle request error
- });
- },
- },
- computed: {
- showList() {
- if (this.activeTab === 'selected') {
- return this.selectedIssues.length > 0;
- }
-
- return this.issuesCount > 0;
- },
- showEmptyState() {
- if (!this.loading && this.issuesCount === 0) {
- return true;
- }
-
- return this.activeTab === 'selected' && this.selectedIssues.length === 0;
- },
- },
- created() {
- this.page = 1;
- },
- components: {
- 'modal-header': gl.issueBoards.ModalHeader,
- 'modal-list': gl.issueBoards.ModalList,
- 'modal-footer': gl.issueBoards.ModalFooter,
- 'empty-state': gl.issueBoards.ModalEmptyState,
- loadingIcon,
- },
- template: `
- <div
- class="add-issues-modal"
- v-if="showAddIssuesModal">
- <div class="add-issues-container">
- <modal-header
- :project-id="projectId"
- :milestone-path="milestonePath"
- :label-path="labelPath">
- </modal-header>
- <modal-list
- :issue-link-base="issueLinkBase"
- :root-path="rootPath"
- :empty-state-svg="emptyStateSvg"
- v-if="!loading && showList && !filterLoading"></modal-list>
- <empty-state
- v-if="showEmptyState"
- :new-issue-path="newIssuePath"
- :empty-state-svg="emptyStateSvg"></empty-state>
- <section
- class="add-issues-list text-center"
- v-if="loading || filterLoading">
- <div class="add-issues-list-loading">
- <loading-icon />
- </div>
- </section>
- <modal-footer></modal-footer>
- </div>
- </div>
- `,
-});
diff --git a/app/assets/javascripts/boards/components/modal/index.vue b/app/assets/javascripts/boards/components/modal/index.vue
new file mode 100644
index 00000000000..33e72a6782e
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/index.vue
@@ -0,0 +1,178 @@
+<script>
+ /* global ListIssue */
+ import queryData from '~/boards/utils/query_data';
+ import loadingIcon from '~/vue_shared/components/loading_icon.vue';
+ import ModalHeader from './header.vue';
+ import ModalList from './list.vue';
+ import ModalFooter from './footer.vue';
+ import EmptyState from './empty_state.vue';
+ import ModalStore from '../../stores/modal_store';
+
+ export default {
+ components: {
+ EmptyState,
+ ModalHeader,
+ ModalList,
+ ModalFooter,
+ loadingIcon,
+ },
+ props: {
+ newIssuePath: {
+ type: String,
+ required: true,
+ },
+ emptyStateSvg: {
+ type: String,
+ required: true,
+ },
+ issueLinkBase: {
+ type: String,
+ required: true,
+ },
+ rootPath: {
+ type: String,
+ required: true,
+ },
+ projectId: {
+ type: Number,
+ required: true,
+ },
+ milestonePath: {
+ type: String,
+ required: true,
+ },
+ labelPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return ModalStore.store;
+ },
+ computed: {
+ showList() {
+ if (this.activeTab === 'selected') {
+ return this.selectedIssues.length > 0;
+ }
+
+ return this.issuesCount > 0;
+ },
+ showEmptyState() {
+ if (!this.loading && this.issuesCount === 0) {
+ return true;
+ }
+
+ return this.activeTab === 'selected' && this.selectedIssues.length === 0;
+ },
+ },
+ watch: {
+ page() {
+ this.loadIssues();
+ },
+ showAddIssuesModal() {
+ if (this.showAddIssuesModal && !this.issues.length) {
+ this.loading = true;
+ const loadingDone = () => {
+ this.loading = false;
+ };
+
+ this.loadIssues()
+ .then(loadingDone)
+ .catch(loadingDone);
+ } else if (!this.showAddIssuesModal) {
+ this.issues = [];
+ this.selectedIssues = [];
+ this.issuesCount = false;
+ }
+ },
+ filter: {
+ handler() {
+ if (this.$el.tagName) {
+ this.page = 1;
+ this.filterLoading = true;
+ const loadingDone = () => {
+ this.filterLoading = false;
+ };
+
+ this.loadIssues(true)
+ .then(loadingDone)
+ .catch(loadingDone);
+ }
+ },
+ deep: true,
+ },
+ },
+ created() {
+ this.page = 1;
+ },
+ methods: {
+ loadIssues(clearIssues = false) {
+ if (!this.showAddIssuesModal) return false;
+
+ return gl.boardService
+ .getBacklog(
+ queryData(this.filter.path, {
+ page: this.page,
+ per: this.perPage,
+ }),
+ )
+ .then(res => res.data)
+ .then(data => {
+ if (clearIssues) {
+ this.issues = [];
+ }
+
+ data.issues.forEach(issueObj => {
+ const issue = new ListIssue(issueObj);
+ const foundSelectedIssue = ModalStore.findSelectedIssue(issue);
+ issue.selected = !!foundSelectedIssue;
+
+ this.issues.push(issue);
+ });
+
+ this.loadingNewPage = false;
+
+ if (!this.issuesCount) {
+ this.issuesCount = data.size;
+ }
+ })
+ .catch(() => {
+ // TODO: handle request error
+ });
+ },
+ },
+ };
+</script>
+<template>
+ <div
+ v-if="showAddIssuesModal"
+ class="add-issues-modal">
+ <div class="add-issues-container">
+ <modal-header
+ :project-id="projectId"
+ :milestone-path="milestonePath"
+ :label-path="labelPath"
+ />
+ <modal-list
+ v-if="!loading && showList && !filterLoading"
+ :issue-link-base="issueLinkBase"
+ :root-path="rootPath"
+ :empty-state-svg="emptyStateSvg"
+ />
+ <empty-state
+ v-if="showEmptyState"
+ :new-issue-path="newIssuePath"
+ :empty-state-svg="emptyStateSvg"
+ />
+ <section
+ v-if="loading || filterLoading"
+ class="add-issues-list text-center"
+ >
+ <div class="add-issues-list-loading">
+ <loading-icon />
+ </div>
+ </section>
+ <modal-footer/>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/boards/components/modal/list.js b/app/assets/javascripts/boards/components/modal/list.js
deleted file mode 100644
index 6b04a6c7a6c..00000000000
--- a/app/assets/javascripts/boards/components/modal/list.js
+++ /dev/null
@@ -1,158 +0,0 @@
-/* global ListIssue */
-
-import Vue from 'vue';
-import bp from '../../../breakpoints';
-import ModalStore from '../../stores/modal_store';
-
-gl.issueBoards.ModalList = Vue.extend({
- props: {
- issueLinkBase: {
- type: String,
- required: true,
- },
- rootPath: {
- type: String,
- required: true,
- },
- emptyStateSvg: {
- type: String,
- required: true,
- },
- },
- data() {
- return ModalStore.store;
- },
- watch: {
- activeTab() {
- if (this.activeTab === 'all') {
- ModalStore.purgeUnselectedIssues();
- }
- },
- },
- computed: {
- loopIssues() {
- if (this.activeTab === 'all') {
- return this.issues;
- }
-
- return this.selectedIssues;
- },
- groupedIssues() {
- const groups = [];
- this.loopIssues.forEach((issue, i) => {
- const index = i % this.columns;
-
- if (!groups[index]) {
- groups.push([]);
- }
-
- groups[index].push(issue);
- });
-
- return groups;
- },
- },
- methods: {
- scrollHandler() {
- const currentPage = Math.floor(this.issues.length / this.perPage);
-
- if ((this.scrollTop() > this.scrollHeight() - 100) && !this.loadingNewPage
- && currentPage === this.page) {
- this.loadingNewPage = true;
- this.page += 1;
- }
- },
- toggleIssue(e, issue) {
- if (e.target.tagName !== 'A') {
- ModalStore.toggleIssue(issue);
- }
- },
- listHeight() {
- return this.$refs.list.getBoundingClientRect().height;
- },
- scrollHeight() {
- return this.$refs.list.scrollHeight;
- },
- scrollTop() {
- return this.$refs.list.scrollTop + this.listHeight();
- },
- showIssue(issue) {
- if (this.activeTab === 'all') return true;
-
- const index = ModalStore.selectedIssueIndex(issue);
-
- return index !== -1;
- },
- setColumnCount() {
- const breakpoint = bp.getBreakpointSize();
-
- if (breakpoint === 'lg' || breakpoint === 'md') {
- this.columns = 3;
- } else if (breakpoint === 'sm') {
- this.columns = 2;
- } else {
- this.columns = 1;
- }
- },
- },
- mounted() {
- this.scrollHandlerWrapper = this.scrollHandler.bind(this);
- this.setColumnCountWrapper = this.setColumnCount.bind(this);
- this.setColumnCount();
-
- this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper);
- window.addEventListener('resize', this.setColumnCountWrapper);
- },
- beforeDestroy() {
- this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper);
- window.removeEventListener('resize', this.setColumnCountWrapper);
- },
- components: {
- 'issue-card-inner': gl.issueBoards.IssueCardInner,
- },
- template: `
- <section
- class="add-issues-list add-issues-list-columns"
- ref="list">
- <div
- class="empty-state add-issues-empty-state-filter text-center"
- v-if="issuesCount > 0 && issues.length === 0">
- <div
- class="svg-content">
- <img :src="emptyStateSvg"/>
- </div>
- <div class="text-content">
- <h4>
- There are no issues to show.
- </h4>
- </div>
- </div>
- <div
- v-for="group in groupedIssues"
- class="add-issues-list-column">
- <div
- v-for="issue in group"
- v-if="showIssue(issue)"
- class="card-parent">
- <div
- class="card"
- :class="{ 'is-active': issue.selected }"
- @click="toggleIssue($event, issue)">
- <issue-card-inner
- :issue="issue"
- :issue-link-base="issueLinkBase"
- :root-path="rootPath">
- </issue-card-inner>
- <span
- :aria-label="'Issue #' + issue.id + ' selected'"
- aria-checked="true"
- v-if="issue.selected"
- class="issue-card-selected text-center">
- <i class="fa fa-check"></i>
- </span>
- </div>
- </div>
- </div>
- </section>
- `,
-});
diff --git a/app/assets/javascripts/boards/components/modal/list.vue b/app/assets/javascripts/boards/components/modal/list.vue
new file mode 100644
index 00000000000..a58b5afe970
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/list.vue
@@ -0,0 +1,161 @@
+<script>
+ import bp from '../../../breakpoints';
+ import ModalStore from '../../stores/modal_store';
+ import IssueCardInner from '../issue_card_inner.vue';
+
+ export default {
+ components: {
+ IssueCardInner,
+ },
+ props: {
+ issueLinkBase: {
+ type: String,
+ required: true,
+ },
+ rootPath: {
+ type: String,
+ required: true,
+ },
+ emptyStateSvg: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return ModalStore.store;
+ },
+ computed: {
+ loopIssues() {
+ if (this.activeTab === 'all') {
+ return this.issues;
+ }
+
+ return this.selectedIssues;
+ },
+ groupedIssues() {
+ const groups = [];
+ this.loopIssues.forEach((issue, i) => {
+ const index = i % this.columns;
+
+ if (!groups[index]) {
+ groups.push([]);
+ }
+
+ groups[index].push(issue);
+ });
+
+ return groups;
+ },
+ },
+ watch: {
+ activeTab() {
+ if (this.activeTab === 'all') {
+ ModalStore.purgeUnselectedIssues();
+ }
+ },
+ },
+ mounted() {
+ this.scrollHandlerWrapper = this.scrollHandler.bind(this);
+ this.setColumnCountWrapper = this.setColumnCount.bind(this);
+ this.setColumnCount();
+
+ this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper);
+ window.addEventListener('resize', this.setColumnCountWrapper);
+ },
+ beforeDestroy() {
+ this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper);
+ window.removeEventListener('resize', this.setColumnCountWrapper);
+ },
+ methods: {
+ scrollHandler() {
+ const currentPage = Math.floor(this.issues.length / this.perPage);
+
+ if (
+ this.scrollTop() > this.scrollHeight() - 100 &&
+ !this.loadingNewPage &&
+ currentPage === this.page
+ ) {
+ this.loadingNewPage = true;
+ this.page += 1;
+ }
+ },
+ toggleIssue(e, issue) {
+ if (e.target.tagName !== 'A') {
+ ModalStore.toggleIssue(issue);
+ }
+ },
+ listHeight() {
+ return this.$refs.list.getBoundingClientRect().height;
+ },
+ scrollHeight() {
+ return this.$refs.list.scrollHeight;
+ },
+ scrollTop() {
+ return this.$refs.list.scrollTop + this.listHeight();
+ },
+ showIssue(issue) {
+ if (this.activeTab === 'all') return true;
+
+ const index = ModalStore.selectedIssueIndex(issue);
+
+ return index !== -1;
+ },
+ setColumnCount() {
+ const breakpoint = bp.getBreakpointSize();
+
+ if (breakpoint === 'lg' || breakpoint === 'md') {
+ this.columns = 3;
+ } else if (breakpoint === 'sm') {
+ this.columns = 2;
+ } else {
+ this.columns = 1;
+ }
+ },
+ },
+ };
+</script>
+<template>
+ <section
+ ref="list"
+ class="add-issues-list add-issues-list-columns">
+ <div
+ v-if="issuesCount > 0 && issues.length === 0"
+ class="empty-state add-issues-empty-state-filter text-center">
+ <div class="svg-content">
+ <img :src="emptyStateSvg" />
+ </div>
+ <div class="text-content">
+ <h4>
+ There are no issues to show.
+ </h4>
+ </div>
+ </div>
+ <div
+ v-for="(group, index) in groupedIssues"
+ :key="index"
+ class="add-issues-list-column">
+ <div
+ v-for="issue in group"
+ v-if="showIssue(issue)"
+ :key="issue.id"
+ class="board-card-parent">
+ <div
+ :class="{ 'is-active': issue.selected }"
+ class="board-card"
+ @click="toggleIssue($event, issue)">
+ <issue-card-inner
+ :issue="issue"
+ :issue-link-base="issueLinkBase"
+ :root-path="rootPath"/>
+ <span
+ v-if="issue.selected"
+ :aria-label="'Issue #' + issue.id + ' selected'"
+ aria-checked="true"
+ class="issue-card-selected text-center">
+ <i class="fa fa-check"></i>
+ </span>
+ </div>
+ </div>
+ </div>
+ </section>
+</template>
diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.js b/app/assets/javascripts/boards/components/modal/lists_dropdown.js
deleted file mode 100644
index e644de2d4fc..00000000000
--- a/app/assets/javascripts/boards/components/modal/lists_dropdown.js
+++ /dev/null
@@ -1,54 +0,0 @@
-import Vue from 'vue';
-import ModalStore from '../../stores/modal_store';
-
-gl.issueBoards.ModalFooterListsDropdown = Vue.extend({
- data() {
- return {
- modal: ModalStore.store,
- state: gl.issueBoards.BoardsStore.state,
- };
- },
- computed: {
- selected() {
- return this.modal.selectedList || this.state.lists[1];
- },
- },
- destroyed() {
- this.modal.selectedList = null;
- },
- template: `
- <div class="dropdown inline">
- <button
- class="dropdown-menu-toggle"
- type="button"
- data-toggle="dropdown"
- aria-expanded="false">
- <span
- class="dropdown-label-box"
- :style="{ backgroundColor: selected.label.color }">
- </span>
- {{ selected.title }}
- <i class="fa fa-chevron-down"></i>
- </button>
- <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up">
- <ul>
- <li
- v-for="list in state.lists"
- v-if="list.type == 'label'">
- <a
- href="#"
- role="button"
- :class="{ 'is-active': list.id == selected.id }"
- @click.prevent="modal.selectedList = list">
- <span
- class="dropdown-label-box"
- :style="{ backgroundColor: list.label.color }">
- </span>
- {{ list.title }}
- </a>
- </li>
- </ul>
- </div>
- </div>
- `,
-});
diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.vue b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue
new file mode 100644
index 00000000000..6a5a39099bd
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue
@@ -0,0 +1,56 @@
+<script>
+import ModalStore from '../../stores/modal_store';
+
+export default {
+ data() {
+ return {
+ modal: ModalStore.store,
+ state: gl.issueBoards.BoardsStore.state,
+ };
+ },
+ computed: {
+ selected() {
+ return this.modal.selectedList || this.state.lists[1];
+ },
+ },
+ destroyed() {
+ this.modal.selectedList = null;
+ },
+};
+</script>
+<template>
+ <div class="dropdown inline">
+ <button
+ class="dropdown-menu-toggle"
+ type="button"
+ data-toggle="dropdown"
+ aria-expanded="false">
+ <span
+ :style="{ backgroundColor: selected.label.color }"
+ class="dropdown-label-box">
+ </span>
+ {{ selected.title }}
+ <i class="fa fa-chevron-down"></i>
+ </button>
+ <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up">
+ <ul>
+ <li
+ v-for="(list, i) in state.lists"
+ v-if="list.type == 'label'"
+ :key="i">
+ <a
+ :class="{ 'is-active': list.id == selected.id }"
+ href="#"
+ role="button"
+ @click.prevent="modal.selectedList = list">
+ <span
+ :style="{ backgroundColor: list.label.color }"
+ class="dropdown-label-box">
+ </span>
+ {{ list.title }}
+ </a>
+ </li>
+ </ul>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/boards/components/modal/tabs.js b/app/assets/javascripts/boards/components/modal/tabs.js
deleted file mode 100644
index b6465a88e5e..00000000000
--- a/app/assets/javascripts/boards/components/modal/tabs.js
+++ /dev/null
@@ -1,46 +0,0 @@
-import Vue from 'vue';
-import ModalStore from '../../stores/modal_store';
-import modalMixin from '../../mixins/modal_mixins';
-
-gl.issueBoards.ModalTabs = Vue.extend({
- mixins: [modalMixin],
- data() {
- return ModalStore.store;
- },
- computed: {
- selectedCount() {
- return ModalStore.selectedCount();
- },
- },
- destroyed() {
- this.activeTab = 'all';
- },
- template: `
- <div class="top-area prepend-top-10 append-bottom-10">
- <ul class="nav-links issues-state-filters">
- <li :class="{ 'active': activeTab == 'all' }">
- <a
- href="#"
- role="button"
- @click.prevent="changeTab('all')">
- Open issues
- <span class="badge">
- {{ issuesCount }}
- </span>
- </a>
- </li>
- <li :class="{ 'active': activeTab == 'selected' }">
- <a
- href="#"
- role="button"
- @click.prevent="changeTab('selected')">
- Selected issues
- <span class="badge">
- {{ selectedCount }}
- </span>
- </a>
- </li>
- </ul>
- </div>
- `,
-});
diff --git a/app/assets/javascripts/boards/components/modal/tabs.vue b/app/assets/javascripts/boards/components/modal/tabs.vue
new file mode 100644
index 00000000000..d926b080094
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/tabs.vue
@@ -0,0 +1,49 @@
+<script>
+ import ModalStore from '../../stores/modal_store';
+ import modalMixin from '../../mixins/modal_mixins';
+
+ export default {
+ mixins: [modalMixin],
+ data() {
+ return ModalStore.store;
+ },
+ computed: {
+ selectedCount() {
+ return ModalStore.selectedCount();
+ },
+ },
+ destroyed() {
+ this.activeTab = 'all';
+ },
+ };
+</script>
+<template>
+ <div class="top-area prepend-top-10 append-bottom-10">
+ <ul class="nav-links issues-state-filters">
+ <li :class="{ 'active': activeTab == 'all' }">
+ <a
+ href="#"
+ role="button"
+ @click.prevent="changeTab('all')"
+ >
+ Open issues
+ <span class="badge badge-pill">
+ {{ issuesCount }}
+ </span>
+ </a>
+ </li>
+ <li :class="{ 'active': activeTab == 'selected' }">
+ <a
+ href="#"
+ role="button"
+ @click.prevent="changeTab('selected')"
+ >
+ Selected issues
+ <span class="badge badge-pill">
+ {{ selectedCount }}
+ </span>
+ </a>
+ </li>
+ </ul>
+ </div>
+</template>
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js
index 71f49319c36..448ab9ed135 100644
--- a/app/assets/javascripts/boards/components/new_list_dropdown.js
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, no-new, space-before-function-paren, one-var, promise/catch-or-return, max-len */
+/* eslint-disable func-names, no-new, promise/catch-or-return */
import $ from 'jquery';
import axios from '~/lib/utils/axios_utils';
@@ -56,6 +56,7 @@ gl.issueBoards.newListDropdownInit = () => {
filterable: true,
selectable: true,
multiSelect: true,
+ containerSelector: '.js-tab-container-labels .dropdown-page-one .dropdown-content',
clicked (options) {
const { e } = options;
const label = options.selectedObj;
diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue
index 371774098b9..eb335f352d3 100644
--- a/app/assets/javascripts/boards/components/project_select.vue
+++ b/app/assets/javascripts/boards/components/project_select.vue
@@ -1,71 +1,69 @@
<script>
- /* global ListIssue */
+import $ from 'jquery';
+import _ from 'underscore';
+import eventHub from '../eventhub';
+import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+import Api from '../../api';
- import $ from 'jquery';
- import _ from 'underscore';
- import eventHub from '../eventhub';
- import loadingIcon from '../../vue_shared/components/loading_icon.vue';
- import Api from '../../api';
-
- export default {
- name: 'BoardProjectSelect',
- components: {
- loadingIcon,
- },
- props: {
- groupId: {
- type: Number,
- required: true,
- default: 0,
- },
+export default {
+ name: 'BoardProjectSelect',
+ components: {
+ loadingIcon,
+ },
+ props: {
+ groupId: {
+ type: Number,
+ required: true,
+ default: 0,
},
- data() {
- return {
- loading: true,
- selectedProject: {},
- };
+ },
+ data() {
+ return {
+ loading: true,
+ selectedProject: {},
+ };
+ },
+ computed: {
+ selectedProjectName() {
+ return this.selectedProject.name || 'Select a project';
},
- computed: {
- selectedProjectName() {
- return this.selectedProject.name || 'Select a project';
+ },
+ mounted() {
+ $(this.$refs.projectsDropdown).glDropdown({
+ filterable: true,
+ filterRemote: true,
+ search: {
+ fields: ['name_with_namespace'],
},
- },
- mounted() {
- $(this.$refs.projectsDropdown).glDropdown({
- filterable: true,
- filterRemote: true,
- search: {
- fields: ['name_with_namespace'],
- },
- clicked: ({ $el, e }) => {
- e.preventDefault();
- this.selectedProject = {
- id: $el.data('project-id'),
- name: $el.data('project-name'),
- };
- eventHub.$emit('setSelectedProject', this.selectedProject);
- },
- selectable: true,
- data: (term, callback) => {
- this.loading = true;
- return Api.groupProjects(this.groupId, term, (projects) => {
- this.loading = false;
- callback(projects);
- });
- },
- renderRow(project) {
- return `
+ clicked: ({ $el, e }) => {
+ e.preventDefault();
+ this.selectedProject = {
+ id: $el.data('project-id'),
+ name: $el.data('project-name'),
+ };
+ eventHub.$emit('setSelectedProject', this.selectedProject);
+ },
+ selectable: true,
+ data: (term, callback) => {
+ this.loading = true;
+ return Api.groupProjects(this.groupId, term, projects => {
+ this.loading = false;
+ callback(projects);
+ });
+ },
+ renderRow(project) {
+ return `
<li>
<a href='#' class='dropdown-menu-link' data-project-id="${project.id}" data-project-name="${project.name}">
${_.escape(project.name)}
</a>
</li>
`;
- },
- text: project => project.name,
- });
- },
- };
+ },
+ text: project => project.name,
+ });
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.js b/app/assets/javascripts/boards/components/sidebar/remove_issue.js
deleted file mode 100644
index 0a0820ec5fd..00000000000
--- a/app/assets/javascripts/boards/components/sidebar/remove_issue.js
+++ /dev/null
@@ -1,73 +0,0 @@
-import Vue from 'vue';
-import Flash from '../../../flash';
-import { __ } from '../../../locale';
-
-const Store = gl.issueBoards.BoardsStore;
-
-window.gl = window.gl || {};
-window.gl.issueBoards = window.gl.issueBoards || {};
-
-gl.issueBoards.RemoveIssueBtn = Vue.extend({
- props: {
- issue: {
- type: Object,
- required: true,
- },
- list: {
- type: Object,
- required: true,
- },
- },
- computed: {
- updateUrl() {
- return this.issue.path;
- },
- },
- methods: {
- removeIssue() {
- const issue = this.issue;
- const lists = issue.getLists();
- const listLabelIds = lists.map(list => list.label.id);
-
- let labelIds = issue.labels
- .map(label => label.id)
- .filter(id => !listLabelIds.includes(id));
- if (labelIds.length === 0) {
- labelIds = [''];
- }
-
- const data = {
- issue: {
- label_ids: labelIds,
- },
- };
-
- // Post the remove data
- Vue.http.patch(this.updateUrl, data).catch(() => {
- Flash(__('Failed to remove issue from board, please try again.'));
-
- lists.forEach((list) => {
- list.addIssue(issue);
- });
- });
-
- // Remove from the frontend store
- lists.forEach((list) => {
- list.removeIssue(issue);
- });
-
- Store.detail.issue = {};
- },
- },
- template: `
- <div
- class="block list">
- <button
- class="btn btn-default btn-block"
- type="button"
- @click="removeIssue">
- Remove from board
- </button>
- </div>
- `,
-});
diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.vue b/app/assets/javascripts/boards/components/sidebar/remove_issue.vue
new file mode 100644
index 00000000000..90d4c710daf
--- /dev/null
+++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.vue
@@ -0,0 +1,91 @@
+<script>
+ import Vue from 'vue';
+ import Flash from '../../../flash';
+ import { __ } from '../../../locale';
+
+ const Store = gl.issueBoards.BoardsStore;
+
+ export default Vue.extend({
+ props: {
+ issue: {
+ type: Object,
+ required: true,
+ },
+ list: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ updateUrl() {
+ return this.issue.path;
+ },
+ },
+ methods: {
+ removeIssue() {
+ const { issue } = this;
+ const lists = issue.getLists();
+ const req = this.buildPatchRequest(issue, lists);
+
+ const data = {
+ issue: this.seedPatchRequest(issue, req),
+ };
+
+ if (data.issue.label_ids.length === 0) {
+ data.issue.label_ids = [''];
+ }
+
+ // Post the remove data
+ Vue.http.patch(this.updateUrl, data).catch(() => {
+ Flash(__('Failed to remove issue from board, please try again.'));
+
+ lists.forEach(list => {
+ list.addIssue(issue);
+ });
+ });
+
+ // Remove from the frontend store
+ lists.forEach(list => {
+ list.removeIssue(issue);
+ });
+
+ Store.detail.issue = {};
+ },
+ /**
+ * Build the default patch request.
+ */
+ buildPatchRequest(issue, lists) {
+ const listLabelIds = lists.map(list => list.label.id);
+
+ const labelIds = issue.labels
+ .map(label => label.id)
+ .filter(id => !listLabelIds.includes(id));
+
+ return {
+ label_ids: labelIds,
+ };
+ },
+ /**
+ * Seed the given patch request.
+ *
+ * (This is overridden in EE)
+ */
+ seedPatchRequest(issue, req) {
+ return req;
+ },
+ },
+ });
+</script>
+<template>
+ <div
+ class="block list"
+ >
+ <button
+ class="btn btn-default btn-block"
+ type="button"
+ @click="removeIssue"
+ >
+ Remove from board
+ </button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js
index 70367c4f711..46d61ebbf24 100644
--- a/app/assets/javascripts/boards/filtered_search_boards.js
+++ b/app/assets/javascripts/boards/filtered_search_boards.js
@@ -1,4 +1,3 @@
-/* eslint-disable class-methods-use-this */
import FilteredSearchContainer from '../filtered_search/container';
import FilteredSearchManager from '../filtered_search/filtered_search_manager';
diff --git a/app/assets/javascripts/boards/filters/due_date_filters.js b/app/assets/javascripts/boards/filters/due_date_filters.js
index 70132dbfa6f..9eaa0cd227d 100644
--- a/app/assets/javascripts/boards/filters/due_date_filters.js
+++ b/app/assets/javascripts/boards/filters/due_date_filters.js
@@ -1,8 +1,7 @@
-/* global dateFormat */
-
import Vue from 'vue';
+import dateFormat from 'dateformat';
-Vue.filter('due-date', (value) => {
+Vue.filter('due-date', value => {
const date = new Date(value);
return dateFormat(date, 'mmm d, yyyy', true);
});
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index a6f8681cfac..200d1923635 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -1,4 +1,4 @@
-/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */
+/* eslint-disable quote-props, comma-dangle */
import $ from 'jquery';
import _ from 'underscore';
@@ -7,6 +7,7 @@ import Vue from 'vue';
import Flash from '~/flash';
import { __ } from '~/locale';
import '~/vue_shared/models/label';
+import '~/vue_shared/models/assignee';
import FilteredSearchBoards from './filtered_search_boards';
import eventHub from './eventhub';
@@ -15,7 +16,6 @@ import './models/issue';
import './models/list';
import './models/milestone';
import './models/project';
-import './models/assignee';
import './stores/boards_store';
import ModalStore from './stores/modal_store';
import BoardService from './services/board_service';
@@ -25,7 +25,7 @@ import './filters/due_date_filters';
import './components/board';
import './components/board_sidebar';
import './components/new_list_dropdown';
-import './components/modal/index';
+import BoardAddIssuesModal from './components/modal/index.vue';
import '~/vue_shared/vue_resource_interceptor'; // eslint-disable-line import/first
export default () => {
@@ -49,7 +49,7 @@ export default () => {
components: {
'board': gl.issueBoards.Board,
'board-sidebar': gl.issueBoards.BoardSidebar,
- 'board-add-issues-modal': gl.issueBoards.IssuesModal,
+ BoardAddIssuesModal,
},
data: {
state: Store.state,
@@ -121,7 +121,7 @@ export default () => {
this.filterManager.updateTokens();
},
updateDetailIssue(newIssue) {
- const sidebarInfoEndpoint = newIssue.sidebarInfoEndpoint;
+ const { sidebarInfoEndpoint } = newIssue;
if (sidebarInfoEndpoint && newIssue.subscribed === undefined) {
newIssue.setFetchingState('subscriptions', true);
BoardService.getIssueInfo(sidebarInfoEndpoint)
@@ -144,7 +144,7 @@ export default () => {
Store.detail.issue = {};
},
toggleSubscription(id) {
- const issue = Store.detail.issue;
+ const { issue } = Store.detail;
if (issue.id === id && issue.toggleSubscriptionEndpoint) {
issue.setFetchingState('subscriptions', true);
BoardService.toggleIssueSubscription(issue.toggleSubscriptionEndpoint)
@@ -214,7 +214,7 @@ export default () => {
if (this.disabled) {
$tooltip.tooltip();
} else {
- $tooltip.tooltip('destroy');
+ $tooltip.tooltip('dispose');
}
});
},
diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js b/app/assets/javascripts/boards/mixins/sortable_default_options.js
index ac316c31deb..a8df45fc473 100644
--- a/app/assets/javascripts/boards/mixins/sortable_default_options.js
+++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js
@@ -1,4 +1,3 @@
-/* eslint-disable no-unused-vars, no-mixed-operators, comma-dangle */
/* global DocumentTouch */
import $ from 'jquery';
diff --git a/app/assets/javascripts/boards/models/assignee.js b/app/assets/javascripts/boards/models/assignee.js
deleted file mode 100644
index 05dd449e4fd..00000000000
--- a/app/assets/javascripts/boards/models/assignee.js
+++ /dev/null
@@ -1,12 +0,0 @@
-/* eslint-disable no-unused-vars */
-
-class ListAssignee {
- constructor(user, defaultAvatar) {
- this.id = user.id;
- this.name = user.name;
- this.username = user.username;
- this.avatar = user.avatar_url || defaultAvatar;
- }
-}
-
-window.ListAssignee = ListAssignee;
diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js
index b381d48d625..c7cfb72067c 100644
--- a/app/assets/javascripts/boards/models/issue.js
+++ b/app/assets/javascripts/boards/models/issue.js
@@ -1,9 +1,10 @@
-/* eslint-disable no-unused-vars, space-before-function-paren, arrow-body-style, arrow-parens, comma-dangle, max-len */
+/* eslint-disable no-unused-vars, comma-dangle */
/* global ListLabel */
/* global ListMilestone */
/* global ListAssignee */
import Vue from 'vue';
+import '~/vue_shared/models/label';
import IssueProject from './project';
class ListIssue {
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index 7144f4190e7..4f05a0e4282 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -1,19 +1,41 @@
-/* eslint-disable space-before-function-paren, no-underscore-dangle, class-methods-use-this, consistent-return, no-shadow, no-param-reassign, max-len, no-unused-vars */
+/* eslint-disable no-underscore-dangle, class-methods-use-this, consistent-return, no-shadow, no-param-reassign, max-len */
/* global ListIssue */
-/* global ListLabel */
+
+import ListLabel from '~/vue_shared/models/label';
+import ListAssignee from '~/vue_shared/models/assignee';
import queryData from '../utils/query_data';
const PER_PAGE = 20;
+const TYPES = {
+ backlog: {
+ isPreset: true,
+ isExpandable: true,
+ isBlank: false,
+ },
+ closed: {
+ isPreset: true,
+ isExpandable: true,
+ isBlank: false,
+ },
+ blank: {
+ isPreset: true,
+ isExpandable: false,
+ isBlank: true,
+ },
+};
+
class List {
- constructor (obj, defaultAvatar) {
+ constructor(obj, defaultAvatar) {
this.id = obj.id;
this._uid = this.guid();
this.position = obj.position;
this.title = obj.title;
this.type = obj.list_type;
- this.preset = ['backlog', 'closed', 'blank'].indexOf(this.type) > -1;
- this.isExpandable = ['backlog', 'closed'].indexOf(this.type) > -1;
+
+ const typeInfo = this.getTypeInfo(this.type);
+ this.preset = !!typeInfo.isPreset;
+ this.isExpandable = !!typeInfo.isExpandable;
this.isExpanded = true;
this.page = 1;
this.loading = true;
@@ -24,9 +46,12 @@ class List {
if (obj.label) {
this.label = new ListLabel(obj.label);
+ } else if (obj.user) {
+ this.assignee = new ListAssignee(obj.user);
+ this.title = this.assignee.name;
}
- if (this.type !== 'blank' && this.id) {
+ if (!typeInfo.isBlank && this.id) {
this.getIssues().catch(() => {
// TODO: handle request error
});
@@ -34,14 +59,26 @@ class List {
}
guid() {
- const s4 = () => Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
+ const s4 = () =>
+ Math.floor((1 + Math.random()) * 0x10000)
+ .toString(16)
+ .substring(1);
return `${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`;
}
- save () {
- return gl.boardService.createList(this.label.id)
+ save() {
+ const entity = this.label || this.assignee;
+ let entityType = '';
+ if (this.label) {
+ entityType = 'label_id';
+ } else {
+ entityType = 'assignee_id';
+ }
+
+ return gl.boardService
+ .createList(entity.id, entityType)
.then(res => res.data)
- .then((data) => {
+ .then(data => {
this.id = data.id;
this.type = data.list_type;
this.position = data.position;
@@ -50,25 +87,23 @@ class List {
});
}
- destroy () {
+ destroy() {
const index = gl.issueBoards.BoardsStore.state.lists.indexOf(this);
gl.issueBoards.BoardsStore.state.lists.splice(index, 1);
gl.issueBoards.BoardsStore.updateNewListDropdown(this.id);
- gl.boardService.destroyList(this.id)
- .catch(() => {
- // TODO: handle request error
- });
+ gl.boardService.destroyList(this.id).catch(() => {
+ // TODO: handle request error
+ });
}
- update () {
- gl.boardService.updateList(this.id, this.position)
- .catch(() => {
- // TODO: handle request error
- });
+ update() {
+ gl.boardService.updateList(this.id, this.position).catch(() => {
+ // TODO: handle request error
+ });
}
- nextPage () {
+ nextPage() {
if (this.issuesSize > this.issues.length) {
if (this.issues.length / PER_PAGE >= 1) {
this.page += 1;
@@ -78,7 +113,7 @@ class List {
}
}
- getIssues (emptyIssues = true) {
+ getIssues(emptyIssues = true) {
const data = queryData(gl.issueBoards.BoardsStore.filter.path, { page: this.page });
if (this.label && data.label_name) {
@@ -89,9 +124,10 @@ class List {
this.loading = true;
}
- return gl.boardService.getIssuesForList(this.id, data)
+ return gl.boardService
+ .getIssuesForList(this.id, data)
.then(res => res.data)
- .then((data) => {
+ .then(data => {
this.loading = false;
this.issuesSize = data.size;
@@ -103,33 +139,23 @@ class List {
});
}
- newIssue (issue) {
+ newIssue(issue) {
this.addIssue(issue, null, 0);
this.issuesSize += 1;
- return gl.boardService.newIssue(this.id, issue)
+ return gl.boardService
+ .newIssue(this.id, issue)
.then(res => res.data)
- .then((data) => {
- issue.id = data.id;
- issue.iid = data.iid;
- issue.project = data.project;
- issue.path = data.real_path;
- issue.referencePath = data.reference_path;
-
- if (this.issuesSize > 1) {
- const moveBeforeId = this.issues[1].id;
- gl.boardService.moveIssue(issue.id, null, null, null, moveBeforeId);
- }
- });
+ .then(data => this.onNewIssueResponse(issue, data));
}
- createIssues (data) {
- data.forEach((issueObj) => {
+ createIssues(data) {
+ data.forEach(issueObj => {
this.addIssue(new ListIssue(issueObj, this.defaultAvatar));
});
}
- addIssue (issue, listFrom, newIndex) {
+ addIssue(issue, listFrom, newIndex) {
let moveBeforeId = null;
let moveAfterId = null;
@@ -152,6 +178,13 @@ class List {
issue.addLabel(this.label);
}
+ if (this.assignee) {
+ if (listFrom && listFrom.type === 'assignee') {
+ issue.removeAssignee(listFrom.assignee);
+ }
+ issue.addAssignee(this.assignee);
+ }
+
if (listFrom) {
this.issuesSize += 1;
@@ -160,29 +193,29 @@ class List {
}
}
- moveIssue (issue, oldIndex, newIndex, moveBeforeId, moveAfterId) {
+ moveIssue(issue, oldIndex, newIndex, moveBeforeId, moveAfterId) {
this.issues.splice(oldIndex, 1);
this.issues.splice(newIndex, 0, issue);
- gl.boardService.moveIssue(issue.id, null, null, moveBeforeId, moveAfterId)
- .catch(() => {
- // TODO: handle request error
- });
+ gl.boardService.moveIssue(issue.id, null, null, moveBeforeId, moveAfterId).catch(() => {
+ // TODO: handle request error
+ });
}
updateIssueLabel(issue, listFrom, moveBeforeId, moveAfterId) {
- gl.boardService.moveIssue(issue.id, listFrom.id, this.id, moveBeforeId, moveAfterId)
+ gl.boardService
+ .moveIssue(issue.id, listFrom.id, this.id, moveBeforeId, moveAfterId)
.catch(() => {
// TODO: handle request error
});
}
- findIssue (id) {
+ findIssue(id) {
return this.issues.find(issue => issue.id === id);
}
- removeIssue (removeIssue) {
- this.issues = this.issues.filter((issue) => {
+ removeIssue(removeIssue) {
+ this.issues = this.issues.filter(issue => {
const matchesRemove = removeIssue.id === issue.id;
if (matchesRemove) {
@@ -193,6 +226,25 @@ class List {
return !matchesRemove;
});
}
+
+ getTypeInfo (type) {
+ return TYPES[type] || {};
+ }
+
+ onNewIssueResponse (issue, data) {
+ issue.id = data.id;
+ issue.iid = data.iid;
+ issue.project = data.project;
+ issue.path = data.real_path;
+ issue.referencePath = data.reference_path;
+
+ if (this.issuesSize > 1) {
+ const moveBeforeId = this.issues[1].id;
+ gl.boardService.moveIssue(issue.id, null, null, null, moveBeforeId);
+ }
+ }
}
window.List = List;
+
+export default List;
diff --git a/app/assets/javascripts/boards/models/milestone.js b/app/assets/javascripts/boards/models/milestone.js
index c867b06d320..17d15278a74 100644
--- a/app/assets/javascripts/boards/models/milestone.js
+++ b/app/assets/javascripts/boards/models/milestone.js
@@ -1,5 +1,3 @@
-/* eslint-disable no-unused-vars */
-
class ListMilestone {
constructor(obj) {
this.id = obj.id;
diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js
index 7c90597f77c..029b0971f2c 100644
--- a/app/assets/javascripts/boards/services/board_service.js
+++ b/app/assets/javascripts/boards/services/board_service.js
@@ -30,11 +30,13 @@ export default class BoardService {
return axios.post(this.listsEndpointGenerate, {});
}
- createList(labelId) {
+ createList(entityId, entityType) {
+ const list = {
+ [entityType]: entityId,
+ };
+
return axios.post(this.listsEndpoint, {
- list: {
- label_id: labelId,
- },
+ list,
});
}
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index 20e78edf2a2..333338489bc 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -1,4 +1,4 @@
-/* eslint-disable comma-dangle, space-before-function-paren, one-var, no-shadow, dot-notation, max-len */
+/* eslint-disable comma-dangle, no-shadow */
/* global List */
import $ from 'jquery';
@@ -103,8 +103,15 @@ gl.issueBoards.BoardsStore = {
const listLabels = issueLists.map(listIssue => listIssue.label);
if (!issueTo) {
- // Add to new lists issues if it doesn't already exist
- listTo.addIssue(issue, listFrom, newIndex);
+ // Check if target list assignee is already present in this issue
+ if ((listTo.type === 'assignee' && listFrom.type === 'assignee') &&
+ issue.findAssignee(listTo.assignee)) {
+ const targetIssue = listTo.findIssue(issue.id);
+ targetIssue.removeAssignee(listFrom.assignee);
+ } else {
+ // Add to new lists issues if it doesn't already exist
+ listTo.addIssue(issue, listFrom, newIndex);
+ }
} else {
listTo.updateIssueLabel(issue, listFrom);
issueTo.removeLabel(listFrom.label);
@@ -115,7 +122,11 @@ gl.issueBoards.BoardsStore = {
list.removeIssue(issue);
});
issue.removeLabels(listLabels);
- } else {
+ } else if (listTo.type === 'backlog' && listFrom.type === 'assignee') {
+ issue.removeAssignee(listFrom.assignee);
+ listFrom.removeIssue(issue);
+ } else if ((listTo.type !== 'label' && listFrom.type === 'assignee') ||
+ (listTo.type !== 'assignee' && listFrom.type === 'label')) {
listFrom.removeIssue(issue);
}
},
@@ -126,13 +137,14 @@ gl.issueBoards.BoardsStore = {
list.moveIssue(issue, oldIndex, newIndex, beforeId, afterId);
},
findList (key, val, type = 'label') {
- return this.state.lists.filter((list) => {
- const byType = type ? list['type'] === type : true;
+ const filteredList = this.state.lists.filter((list) => {
+ const byType = type ? (list.type === type) || (list.type === 'assignee') : true;
return list[key] === val && byType;
- })[0];
+ });
+ return filteredList[0];
},
updateFiltersUrl () {
- history.pushState(null, null, `?${this.filter.path}`);
+ window.history.pushState(null, null, `?${this.filter.path}`);
}
};
diff --git a/app/assets/javascripts/boards/stores/modal_store.js b/app/assets/javascripts/boards/stores/modal_store.js
index a4220cd840d..0d9ac367a70 100644
--- a/app/assets/javascripts/boards/stores/modal_store.js
+++ b/app/assets/javascripts/boards/stores/modal_store.js
@@ -26,7 +26,7 @@ class ModalStore {
toggleIssue(issueObj) {
const issue = issueObj;
- const selected = issue.selected;
+ const { selected } = issue;
issue.selected = !selected;
diff --git a/app/assets/javascripts/build_artifacts.js b/app/assets/javascripts/build_artifacts.js
index 3fa16517388..e338376fcaa 100644
--- a/app/assets/javascripts/build_artifacts.js
+++ b/app/assets/javascripts/build_artifacts.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, prefer-arrow-callback, no-return-assign */
+/* eslint-disable func-names, prefer-arrow-callback */
import $ from 'jquery';
import { visitUrl } from './lib/utils/url_utility';
diff --git a/app/assets/javascripts/ci_variable_list/ci_variable_list.js b/app/assets/javascripts/ci_variable_list/ci_variable_list.js
index e177a3bfdc7..47efb3a8cee 100644
--- a/app/assets/javascripts/ci_variable_list/ci_variable_list.js
+++ b/app/assets/javascripts/ci_variable_list/ci_variable_list.js
@@ -141,6 +141,11 @@ export default class VariableList {
$rowClone.find(entry.selector).val(entry.default);
});
+ // Close any dropdowns
+ $rowClone.find('.dropdown-menu.show').each((index, $dropdown) => {
+ $dropdown.classList.remove('show');
+ });
+
this.initRow($rowClone);
$row.after($rowClone);
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index 01aec4f36af..8139aa69fc7 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -31,6 +31,7 @@ export default class Clusters {
installHelmPath,
installIngressPath,
installRunnerPath,
+ installJupyterPath,
installPrometheusPath,
managePrometheusPath,
clusterStatus,
@@ -51,6 +52,7 @@ export default class Clusters {
installIngressEndpoint: installIngressPath,
installRunnerEndpoint: installRunnerPath,
installPrometheusEndpoint: installPrometheusPath,
+ installJupyterEndpoint: installJupyterPath,
});
this.installApplication = this.installApplication.bind(this);
@@ -79,7 +81,7 @@ export default class Clusters {
}
initApplications() {
- const store = this.store;
+ const { store } = this;
const el = document.querySelector('#js-cluster-applications');
this.applications = new Vue({
@@ -209,11 +211,12 @@ export default class Clusters {
}
}
- installApplication(appId) {
+ installApplication(data) {
+ const appId = data.id;
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_LOADING);
this.store.updateAppProperty(appId, 'requestReason', null);
- this.service.installApplication(appId)
+ this.service.installApplication(appId, data.params)
.then(() => {
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_SUCCESS);
})
diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue
index fae580c091b..ec52fdfdf32 100644
--- a/app/assets/javascripts/clusters/components/application_row.vue
+++ b/app/assets/javascripts/clusters/components/application_row.vue
@@ -52,6 +52,11 @@
type: String,
required: false,
},
+ installApplicationRequestParams: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
},
computed: {
rowJsClass() {
@@ -109,7 +114,10 @@
},
methods: {
installClicked() {
- eventHub.$emit('installApplication', this.id);
+ eventHub.$emit('installApplication', {
+ id: this.id,
+ params: this.installApplicationRequestParams,
+ });
},
},
};
@@ -117,8 +125,8 @@
<template>
<div
- class="gl-responsive-table-row gl-responsive-table-row-col-span"
:class="rowJsClass"
+ class="gl-responsive-table-row gl-responsive-table-row-col-span"
>
<div
class="gl-responsive-table-row-layout"
@@ -147,8 +155,8 @@
<slot name="description"></slot>
</div>
<div
- class="table-section table-button-footer section-align-top"
:class="{ 'section-20': showManageButton, 'section-15': !showManageButton }"
+ class="table-section table-button-footer section-align-top"
role="gridcell"
>
<div
@@ -156,18 +164,18 @@
class="btn-group table-action-buttons"
>
<a
- class="btn"
:href="manageLink"
+ class="btn"
>
{{ manageButtonLabel }}
</a>
</div>
<div class="btn-group table-action-buttons">
<loading-button
- class="js-cluster-application-install-button"
:loading="installButtonLoading"
:disabled="installButtonDisabled"
:label="installButtonLabel"
+ class="js-cluster-application-install-button"
@click="installClicked"
/>
</div>
@@ -179,7 +187,7 @@
role="row"
>
<div
- class="alert alert-danger alert-block append-bottom-0"
+ class="alert alert-danger alert-block append-bottom-0 clusters-error-alert"
role="gridcell"
>
<div>
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index 9c12b89240c..8ee7279e544 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -121,6 +121,12 @@ export default {
false,
);
},
+ jupyterInstalled() {
+ return this.applications.jupyter.status === APPLICATION_INSTALLED;
+ },
+ jupyterHostname() {
+ return this.applications.jupyter.hostname;
+ },
},
};
</script>
@@ -146,11 +152,11 @@ export default {
<application-row
id="helm"
:title="applications.helm.title"
- title-link="https://docs.helm.sh/"
:status="applications.helm.status"
:status-reason="applications.helm.statusReason"
:request-status="applications.helm.requestStatus"
:request-reason="applications.helm.requestReason"
+ title-link="https://docs.helm.sh/"
>
<div slot="description">
{{ s__(`ClusterIntegration|Helm streamlines installing
@@ -162,11 +168,11 @@ export default {
<application-row
:id="ingressId"
:title="applications.ingress.title"
- title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/"
:status="applications.ingress.status"
:status-reason="applications.ingress.statusReason"
:request-status="applications.ingress.requestStatus"
:request-reason="applications.ingress.requestReason"
+ title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/"
>
<div slot="description">
<p>
@@ -185,17 +191,17 @@ export default {
class="input-group"
>
<input
- type="text"
id="ingress-ip-address"
- class="form-control js-ip-address"
:value="ingressExternalIp"
+ type="text"
+ class="form-control js-ip-address"
readonly
/>
- <span class="input-group-btn">
+ <span class="input-group-append">
<clipboard-button
:text="ingressExternalIp"
:title="s__('ClusterIntegration|Copy Ingress IP Address to clipboard')"
- class="js-clipboard-btn"
+ class="input-group-text js-clipboard-btn"
/>
</span>
</div>
@@ -249,12 +255,12 @@ export default {
<application-row
id="prometheus"
:title="applications.prometheus.title"
- title-link="https://prometheus.io/docs/introduction/overview/"
:manage-link="managePrometheusPath"
:status="applications.prometheus.status"
:status-reason="applications.prometheus.statusReason"
:request-status="applications.prometheus.requestStatus"
:request-reason="applications.prometheus.requestReason"
+ title-link="https://prometheus.io/docs/introduction/overview/"
>
<div
slot="description"
@@ -265,11 +271,11 @@ export default {
<application-row
id="runner"
:title="applications.runner.title"
- title-link="https://docs.gitlab.com/runner/"
:status="applications.runner.status"
:status-reason="applications.runner.statusReason"
:request-status="applications.runner.requestStatus"
:request-reason="applications.runner.requestReason"
+ title-link="https://docs.gitlab.com/runner/"
>
<div slot="description">
{{ s__(`ClusterIntegration|GitLab Runner connects to this
@@ -278,11 +284,67 @@ export default {
applications to production.`) }}
</div>
</application-row>
+ <application-row
+ id="jupyter"
+ :title="applications.jupyter.title"
+ :status="applications.jupyter.status"
+ :status-reason="applications.jupyter.statusReason"
+ :request-status="applications.jupyter.requestStatus"
+ :request-reason="applications.jupyter.requestReason"
+ :install-application-request-params="{ hostname: applications.jupyter.hostname }"
+ title-link="https://jupyterhub.readthedocs.io/en/stable/"
+ >
+ <div slot="description">
+ <p>
+ {{ s__(`ClusterIntegration|JupyterHub, a multi-user Hub, spawns,
+ manages, and proxies multiple instances of the single-user
+ Jupyter notebook server. JupyterHub can be used to serve
+ notebooks to a class of students, a corporate data science group,
+ or a scientific research group.`) }}
+ </p>
+
+ <template v-if="ingressExternalIp">
+ <div class="form-group">
+ <label for="jupyter-hostname">
+ {{ s__('ClusterIntegration|Jupyter Hostname') }}
+ </label>
+
+ <div class="input-group">
+ <input
+ v-model="applications.jupyter.hostname"
+ :readonly="jupyterInstalled"
+ type="text"
+ class="form-control js-hostname"
+ />
+ <span
+ class="input-group-btn"
+ >
+ <clipboard-button
+ :text="jupyterHostname"
+ :title="s__('ClusterIntegration|Copy Jupyter Hostname to clipboard')"
+ class="js-clipboard-btn"
+ />
+ </span>
+ </div>
+ </div>
+ <p v-if="ingressInstalled">
+ {{ s__(`ClusterIntegration|Replace this with your own hostname if you want.
+ If you do so, point hostname to Ingress IP Address from above.`) }}
+ <a
+ :href="ingressDnsHelpPath"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ {{ __('More information') }}
+ </a>
+ </p>
+ </template>
+ </div>
+ </application-row>
<!--
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>
diff --git a/app/assets/javascripts/clusters/constants.js b/app/assets/javascripts/clusters/constants.js
index b7179f52bb3..371f71fde44 100644
--- a/app/assets/javascripts/clusters/constants.js
+++ b/app/assets/javascripts/clusters/constants.js
@@ -11,3 +11,4 @@ export const REQUEST_LOADING = 'request-loading';
export const REQUEST_SUCCESS = 'request-success';
export const REQUEST_FAILURE = 'request-failure';
export const INGRESS = 'ingress';
+export const JUPYTER = 'jupyter';
diff --git a/app/assets/javascripts/clusters/services/clusters_service.js b/app/assets/javascripts/clusters/services/clusters_service.js
index 13468578f4f..a7d82292ba9 100644
--- a/app/assets/javascripts/clusters/services/clusters_service.js
+++ b/app/assets/javascripts/clusters/services/clusters_service.js
@@ -8,6 +8,7 @@ export default class ClusterService {
ingress: this.options.installIngressEndpoint,
runner: this.options.installRunnerEndpoint,
prometheus: this.options.installPrometheusEndpoint,
+ jupyter: this.options.installJupyterEndpoint,
};
}
@@ -15,8 +16,8 @@ export default class ClusterService {
return axios.get(this.options.endpoint);
}
- installApplication(appId) {
- return axios.post(this.appInstallEndpointMap[appId]);
+ installApplication(appId, params) {
+ return axios.post(this.appInstallEndpointMap[appId], params);
}
static updateCluster(endpoint, data) {
diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js
index 348bbec3b25..d90db7b103c 100644
--- a/app/assets/javascripts/clusters/stores/clusters_store.js
+++ b/app/assets/javascripts/clusters/stores/clusters_store.js
@@ -1,5 +1,5 @@
import { s__ } from '../../locale';
-import { INGRESS } from '../constants';
+import { INGRESS, JUPYTER } from '../constants';
export default class ClusterStore {
constructor() {
@@ -38,6 +38,14 @@ export default class ClusterStore {
requestStatus: null,
requestReason: null,
},
+ jupyter: {
+ title: s__('ClusterIntegration|JupyterHub'),
+ status: null,
+ statusReason: null,
+ requestStatus: null,
+ requestReason: null,
+ hostname: null,
+ },
},
};
}
@@ -83,6 +91,12 @@ export default class ClusterStore {
if (appId === INGRESS) {
this.state.applications.ingress.externalIp = serverAppEntry.external_ip;
+ } else if (appId === JUPYTER) {
+ this.state.applications.jupyter.hostname =
+ serverAppEntry.hostname ||
+ (this.state.applications.ingress.externalIp
+ ? `jupyter.${this.state.applications.ingress.externalIp}.nip.io`
+ : '');
}
});
}
diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js
index 7f3d04655a7..410580b4c25 100644
--- a/app/assets/javascripts/commit/image_file.js
+++ b/app/assets/javascripts/commit/image_file.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-use-before-define, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, quotes, one-var, one-var-declaration-per-line, no-unused-vars, no-return-assign, comma-dangle, quote-props, no-unused-expressions, no-sequences, object-shorthand, max-len */
+/* eslint-disable func-names, wrap-iife, no-var, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, quotes, one-var, one-var-declaration-per-line, no-unused-vars, no-return-assign, comma-dangle, quote-props, no-unused-expressions, no-sequences, max-len */
import $ from 'jquery';
@@ -95,7 +95,7 @@ export default class ImageFile {
});
return [maxWidth, maxHeight];
}
- // eslint-disable-next-line
+
views = {
'two-up': function() {
return $('.two-up.view .wrap', this.file).each((function(_this) {
@@ -122,7 +122,7 @@ export default class ImageFile {
return $('.swipe.view', this.file).each((function(_this) {
return function(index, view) {
var $swipeWrap, $swipeBar, $swipeFrame, wrapPadding, ref;
- ref = _this.prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1];
+ ref = _this.prepareFrames(view), [maxWidth, maxHeight] = ref;
$swipeFrame = $('.swipe-frame', view);
$swipeWrap = $('.swipe-wrap', view);
$swipeBar = $('.swipe-bar', view);
@@ -159,7 +159,7 @@ export default class ImageFile {
return $('.onion-skin.view', this.file).each((function(_this) {
return function(index, view) {
var $frame, $track, $dragger, $frameAdded, framePadding, ref, dragging = false;
- ref = _this.prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1];
+ ref = _this.prepareFrames(view), [maxWidth, maxHeight] = ref;
$frame = $('.onion-skin-frame', view);
$frameAdded = $('.frame.added', view);
$track = $('.drag-track', view);
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
index 24d63b99a29..95c4be64d35 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
@@ -77,9 +77,9 @@
<div class="content-list pipelines">
<loading-icon
+ v-if="isLoading"
:label="s__('Pipelines|Loading Pipelines')"
size="3"
- v-if="isLoading"
class="prepend-top-20"
/>
@@ -91,8 +91,8 @@
/>
<div
- class="table-holder"
v-else-if="shouldRenderTable"
+ class="table-holder"
>
<pipelines-table-component
:pipelines="state.pipelines"
diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js
index 7e2a3573f81..9a3ea7a55b6 100644
--- a/app/assets/javascripts/commits.js
+++ b/app/assets/javascripts/commits.js
@@ -45,7 +45,7 @@ export default class CommitsList {
this.content.fadeTo('fast', 1.0);
// Change url so if user reload a page - search results are saved
- history.replaceState({
+ window.history.replaceState({
page: commitsUrl,
}, document.title, commitsUrl);
})
diff --git a/app/assets/javascripts/commons/bootstrap.js b/app/assets/javascripts/commons/bootstrap.js
index db96da4ccba..50e2949ab55 100644
--- a/app/assets/javascripts/commons/bootstrap.js
+++ b/app/assets/javascripts/commons/bootstrap.js
@@ -1,15 +1,7 @@
import $ from 'jquery';
// bootstrap jQuery plugins
-import 'bootstrap-sass/assets/javascripts/bootstrap/affix';
-import 'bootstrap-sass/assets/javascripts/bootstrap/alert';
-import 'bootstrap-sass/assets/javascripts/bootstrap/button';
-import 'bootstrap-sass/assets/javascripts/bootstrap/dropdown';
-import 'bootstrap-sass/assets/javascripts/bootstrap/modal';
-import 'bootstrap-sass/assets/javascripts/bootstrap/tab';
-import 'bootstrap-sass/assets/javascripts/bootstrap/transition';
-import 'bootstrap-sass/assets/javascripts/bootstrap/tooltip';
-import 'bootstrap-sass/assets/javascripts/bootstrap/popover';
+import 'bootstrap';
// custom jQuery functions
$.fn.extend({
diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js
index 9c88466e576..a252036d657 100644
--- a/app/assets/javascripts/compare_autocomplete.js
+++ b/app/assets/javascripts/compare_autocomplete.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, object-shorthand, comma-dangle, prefer-arrow-callback, no-else-return, newline-per-chained-call, wrap-iife, max-len */
+/* eslint-disable func-names, one-var, no-var, one-var-declaration-per-line, object-shorthand, no-else-return, max-len */
import $ from 'jquery';
import { __ } from './locale';
@@ -54,7 +54,7 @@ export default function initCompareAutocomplete(limitTo = null, clickHandler = (
.attr('href', '#')
.addClass(ref === selected ? 'is-active' : '')
.text(ref)
- .attr('data-ref', escape(ref));
+ .attr('data-ref', ref);
return $('<li />').append(link);
}
},
@@ -78,7 +78,7 @@ export default function initCompareAutocomplete(limitTo = null, clickHandler = (
$dropdownContainer.on('click', '.dropdown-content a', e => {
$dropdown.prop('title', e.target.text.replace(/_+?/g, '-'));
if ($dropdown.hasClass('has-tooltip')) {
- $dropdown.tooltip('fixTitle');
+ $dropdown.tooltip('_fixTitle');
}
});
});
diff --git a/app/assets/javascripts/confirm_danger_modal.js b/app/assets/javascripts/confirm_danger_modal.js
index 1638e09132b..b0c85c2572e 100644
--- a/app/assets/javascripts/confirm_danger_modal.js
+++ b/app/assets/javascripts/confirm_danger_modal.js
@@ -2,13 +2,16 @@ import $ from 'jquery';
import { rstrip } from './lib/utils/common_utils';
function openConfirmDangerModal($form, text) {
+ const $input = $('.js-confirm-danger-input');
+ $input.val('');
+
$('.js-confirm-text').text(text || '');
- $('.js-confirm-danger-input').val('');
$('#modal-confirm-danger').modal('show');
const confirmTextMatch = $('.js-confirm-danger-match').text();
const $submit = $('.js-confirm-danger-submit');
$submit.disable();
+ $input.focus();
$('.js-confirm-danger-input').off('input').on('input', function handleInput() {
const confirmText = rstrip($(this).val());
diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js
index a88b6971f90..02aa507ba03 100644
--- a/app/assets/javascripts/create_merge_request_dropdown.js
+++ b/app/assets/javascripts/create_merge_request_dropdown.js
@@ -61,13 +61,19 @@ export default class CreateMergeRequestDropdown {
}
available() {
- this.availableButton.classList.remove('hide');
- this.unavailableButton.classList.add('hide');
+ this.availableButton.classList.remove('hidden');
+ this.unavailableButton.classList.add('hidden');
}
bindEvents() {
- this.createMergeRequestButton.addEventListener('click', this.onClickCreateMergeRequestButton.bind(this));
- this.createTargetButton.addEventListener('click', this.onClickCreateMergeRequestButton.bind(this));
+ 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));
@@ -77,7 +83,8 @@ export default class CreateMergeRequestDropdown {
checkAbilityToCreateBranch() {
this.setUnavailableButtonState();
- axios.get(this.canCreatePath)
+ axios
+ .get(this.canCreatePath)
.then(({ data }) => {
this.setUnavailableButtonState(false);
@@ -105,7 +112,8 @@ export default class CreateMergeRequestDropdown {
createBranch() {
this.isCreatingBranch = true;
- return axios.post(this.createBranchPath)
+ return axios
+ .post(this.createBranchPath)
.then(({ data }) => {
this.branchCreated = true;
window.location.href = data.url;
@@ -116,7 +124,8 @@ export default class CreateMergeRequestDropdown {
createMergeRequest() {
this.isCreatingMergeRequest = true;
- return axios.post(this.createMrPath)
+ return axios
+ .post(this.createMrPath)
.then(({ data }) => {
this.mergeRequestCreated = true;
window.location.href = data.url;
@@ -195,7 +204,8 @@ export default class CreateMergeRequestDropdown {
getRef(ref, target = 'all') {
if (!ref) return false;
- return axios.get(this.refsPath + ref)
+ return axios
+ .get(`${this.refsPath}${encodeURIComponent(ref)}`)
.then(({ data }) => {
const branches = data[Object.keys(data)[0]];
const tags = data[Object.keys(data)[1]];
@@ -204,7 +214,8 @@ export default class CreateMergeRequestDropdown {
if (target === 'branch') {
result = CreateMergeRequestDropdown.findByValue(branches, ref);
} else {
- result = CreateMergeRequestDropdown.findByValue(branches, ref, true) ||
+ result =
+ CreateMergeRequestDropdown.findByValue(branches, ref, true) ||
CreateMergeRequestDropdown.findByValue(tags, ref, true);
this.suggestedRef = result;
}
@@ -232,7 +243,7 @@ export default class CreateMergeRequestDropdown {
}
hide() {
- this.wrapperEl.classList.add('hide');
+ this.wrapperEl.classList.add('hidden');
}
init() {
@@ -255,11 +266,13 @@ export default class CreateMergeRequestDropdown {
}
isBusy() {
- return this.isCreatingMergeRequest ||
+ return (
+ this.isCreatingMergeRequest ||
this.mergeRequestCreated ||
this.isCreatingBranch ||
this.branchCreated ||
- this.isGettingRef;
+ this.isGettingRef
+ );
}
onChangeInput(event) {
@@ -268,10 +281,11 @@ export default class CreateMergeRequestDropdown {
if (event.target === this.branchInput) {
target = 'branch';
- value = this.branchInput.value;
+ ({ value } = this.branchInput);
} else if (event.target === this.refInput) {
target = 'ref';
- value = event.target.value.slice(0, event.target.selectionStart) +
+ value =
+ event.target.value.slice(0, event.target.selectionStart) +
event.target.value.slice(event.target.selectionEnd);
} else {
return false;
@@ -352,7 +366,7 @@ export default class CreateMergeRequestDropdown {
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'];
+ const messageClasses = ['text-muted', 'text-danger', 'text-success'];
inputClasses.forEach(cssClass => input.classList.remove(cssClass));
messageClasses.forEach(cssClass => message.classList.remove(cssClass));
@@ -379,7 +393,7 @@ export default class CreateMergeRequestDropdown {
this.removeMessage(target);
input.classList.add('gl-field-success-outline');
- message.classList.add('gl-field-success-message');
+ message.classList.add('text-success');
message.textContent = sprintf(__('%{text} is available'), { text });
message.style.display = 'inline-block';
}
@@ -389,25 +403,26 @@ export default class CreateMergeRequestDropdown {
const text = target === 'branch' ? __('branch name') : __('source');
this.removeMessage(target);
- message.classList.add('gl-field-hint');
+ message.classList.add('text-muted');
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');
+ 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.classList.add('text-danger');
message.textContent = text;
message.style.display = 'inline-block';
}
unavailable() {
- this.availableButton.classList.add('hide');
- this.unavailableButton.classList.remove('hide');
+ this.availableButton.classList.add('hidden');
+ this.unavailableButton.classList.remove('hidden');
}
updateBranchName(suggestedBranchName) {
@@ -459,11 +474,15 @@ export default class CreateMergeRequestDropdown {
// target - 'branch' or 'ref'
// ref - string - the new value to use as branch or ref
updateCreatePaths(target, ref) {
- const pathReplacement = `$1${ref}`;
+ const pathReplacement = `$1${encodeURIComponent(ref)}`;
- this.createBranchPath = this.createBranchPath.replace(this.regexps[target].createBranchPath,
- pathReplacement);
- this.createMrPath = this.createMrPath.replace(this.regexps[target].createMrPath,
- pathReplacement);
+ this.createBranchPath = this.createBranchPath.replace(
+ this.regexps[target].createBranchPath,
+ pathReplacement,
+ );
+ this.createMrPath = this.createMrPath.replace(
+ this.regexps[target].createMrPath,
+ pathReplacement,
+ );
}
}
diff --git a/app/assets/javascripts/cycle_analytics/components/banner.vue b/app/assets/javascripts/cycle_analytics/components/banner.vue
index 3204b8dd8e7..410d4873e55 100644
--- a/app/assets/javascripts/cycle_analytics/components/banner.vue
+++ b/app/assets/javascripts/cycle_analytics/components/banner.vue
@@ -23,9 +23,9 @@
<template>
<div class="landing content-block">
<button
+ :aria-label="__('Dismiss Cycle Analytics introduction box')"
class="js-ca-dismiss-button dismiss-button"
type="button"
- :aria-label="__('Dismiss Cycle Analytics introduction box')"
@click="dismissOverviewDialog"
>
<i
diff --git a/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue b/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue
index 32ae0cc1476..b626b187651 100644
--- a/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue
+++ b/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue
@@ -16,17 +16,17 @@
<template>
<span
v-if="count === 50"
- class="events-info pull-right"
+ class="events-info float-right"
>
<i
- class="fa fa-warning"
v-tooltip
- aria-hidden="true"
:title="n__(
'Limited to showing %d event at most',
'Limited to showing %d events at most',
50
)"
+ class="fa fa-warning"
+ aria-hidden="true"
data-placement="top"
>
</i>
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_component.vue
index 907638d798a..312fe75dde4 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_component.vue
+++ b/app/assets/javascripts/cycle_analytics/components/stage_component.vue
@@ -38,8 +38,8 @@
<user-avatar-image :img-src="issue.author.avatarUrl"/>
<h5 class="item-title issue-title">
<a
- class="issue-title"
:href="issue.url"
+ class="issue-title"
>
{{ issue.title }}
</a>
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue
index 34aa04083e6..d4735d030fc 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue
+++ b/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue
@@ -74,12 +74,12 @@
</template>
<template v-else>
<span
- class="merge-request-branch"
v-if="mergeRequest.branch"
+ class="merge-request-branch"
>
<icon
- name="fork"
:size="16"
+ name="fork"
/>
<a :href="mergeRequest.branch.url">
{{ mergeRequest.branch.name }}
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue
index 92f2a95a66a..22637485c01 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue
+++ b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue
@@ -38,8 +38,8 @@
<ul class="stage-event-list">
<li
v-for="(build, i) in items"
- class="stage-event-item item-build-component"
:key="i"
+ class="stage-event-item item-build-component"
>
<div class="item-details">
<!-- FIXME: Pass an alt attribute here for accessibility -->
@@ -52,8 +52,8 @@
#{{ build.id }}
</a>
<icon
- name="fork"
:size="16"
+ name="fork"
/>
<a
:href="build.branch.url"
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue
index b84bb6ed792..a0796f299e7 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue
+++ b/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue
@@ -64,8 +64,8 @@
#{{ build.id }}
</a>
<icon
- name="fork"
:size="16"
+ name="fork"
/>
<a
:href="build.branch.url"
diff --git a/app/assets/javascripts/deploy_keys/components/action_btn.vue b/app/assets/javascripts/deploy_keys/components/action_btn.vue
index 67dda0e29cb..7399fc97d45 100644
--- a/app/assets/javascripts/deploy_keys/components/action_btn.vue
+++ b/app/assets/javascripts/deploy_keys/components/action_btn.vue
@@ -40,9 +40,9 @@ export default {
<template>
<button
- class="btn"
:class="[{ disabled: isLoading }, btnCssClass]"
:disabled="isLoading"
+ class="btn"
@click="doAction">
<slot></slot>
<loading-icon
diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue
index c41fe55db63..d91e4809126 100644
--- a/app/assets/javascripts/deploy_keys/components/app.vue
+++ b/app/assets/javascripts/deploy_keys/components/app.vue
@@ -98,7 +98,7 @@ export default {
},
disableKey(deployKey, callback) {
// eslint-disable-next-line no-alert
- if (confirm(s__('DeployKeys|You are going to remove this deploy key. Are you sure?'))) {
+ if (window.confirm(s__('DeployKeys|You are going to remove this deploy key. Are you sure?'))) {
this.service
.disableKey(deployKey.id)
.then(this.fetchKeys)
@@ -116,8 +116,8 @@ export default {
<div class="append-bottom-default deploy-keys">
<loading-icon
v-if="isLoading && !hasKeys"
- size="2"
:label="s__('DeployKeys|Loading deploy keys')"
+ size="2"
/>
<template v-else-if="hasKeys">
<div class="top-area scrolling-tabs-container inner-page-scroll-tabs">
@@ -138,16 +138,16 @@ export default {
<navigation-tabs
:tabs="tabs"
- @onChangeTab="onChangeTab"
scope="deployKeys"
+ @onChangeTab="onChangeTab"
/>
</div>
<keys-panel
- class="qa-project-deploy-keys"
:project-id="projectId"
:keys="keys[currentTab]"
:store="store"
:endpoint="endpoint"
+ class="qa-project-deploy-keys"
/>
</template>
</div>
diff --git a/app/assets/javascripts/deploy_keys/components/key.vue b/app/assets/javascripts/deploy_keys/components/key.vue
index 6c2af7fa768..f66ca070445 100644
--- a/app/assets/javascripts/deploy_keys/components/key.vue
+++ b/app/assets/javascripts/deploy_keys/components/key.vue
@@ -135,9 +135,9 @@ export default {
<div class="table-mobile-content deploy-project-list">
<template v-if="projects.length > 0">
<a
- class="label deploy-project-label"
- :title="projectTooltipTitle(firstProject)"
v-tooltip
+ :title="projectTooltipTitle(firstProject)"
+ class="label deploy-project-label"
>
<span>
{{ firstProject.project.full_name }}
@@ -145,22 +145,22 @@ export default {
<icon :name="firstProject.can_push ? 'lock-open' : 'lock'"/>
</a>
<a
+ v-tooltip
v-if="isExpandable"
+ :title="restProjectsTooltip"
class="label deploy-project-label"
@click="toggleExpanded"
- :title="restProjectsTooltip"
- v-tooltip
>
<span>{{ restProjectsLabel }}</span>
</a>
<a
- v-else-if="isExpanded"
+ v-tooltip
v-for="deployKeysProject in restProjects"
+ v-else-if="isExpanded"
:key="deployKeysProject.project.full_path"
- class="label deploy-project-label"
:href="deployKeysProject.project.full_path"
:title="projectTooltipTitle(deployKeysProject)"
- v-tooltip
+ class="label deploy-project-label"
>
<span>
{{ deployKeysProject.project.full_name }}
@@ -181,8 +181,8 @@ export default {
</div>
<div class="table-mobile-content text-secondary key-created-at">
<span
- :title="tooltipTitle(deployKey.created_at)"
- v-tooltip>
+ v-tooltip
+ :title="tooltipTitle(deployKey.created_at)">
<icon name="calendar"/>
<span>{{ timeFormated(deployKey.created_at) }}</span>
</span>
@@ -198,34 +198,34 @@ export default {
{{ __('Enable') }}
</action-btn>
<a
+ v-tooltip
v-if="deployKey.can_edit"
- class="btn btn-default text-secondary"
:href="editDeployKeyPath"
:title="__('Edit')"
+ class="btn btn-default text-secondary"
data-container="body"
- v-tooltip
>
<icon name="pencil"/>
</a>
<action-btn
+ v-tooltip
v-if="isRemovable"
:deploy-key="deployKey"
+ :title="__('Remove')"
btn-css-class="btn-danger"
type="remove"
- :title="__('Remove')"
data-container="body"
- v-tooltip
>
<icon name="remove"/>
</action-btn>
<action-btn
+ v-tooltip
v-else-if="isEnabled"
:deploy-key="deployKey"
+ :title="__('Disable')"
btn-css-class="btn-warning"
type="disable"
- :title="__('Disable')"
data-container="body"
- v-tooltip
>
<icon name="cancel"/>
</action-btn>
diff --git a/app/assets/javascripts/deploy_keys/components/keys_panel.vue b/app/assets/javascripts/deploy_keys/components/keys_panel.vue
index 3b146c7389a..2f057ca29f6 100644
--- a/app/assets/javascripts/deploy_keys/components/keys_panel.vue
+++ b/app/assets/javascripts/deploy_keys/components/keys_panel.vue
@@ -59,8 +59,8 @@ export default {
/>
</template>
<div
- class="settings-message text-center"
v-else
+ class="settings-message text-center"
>
{{ s__('DeployKeys|No deploy keys found. Create one with the form above.') }}
</div>
diff --git a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js
index d1260ff5373..ed24d1775f4 100644
--- a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js
+++ b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js
@@ -6,7 +6,10 @@ import Vue from 'vue';
const CommentAndResolveBtn = Vue.extend({
props: {
- discussionId: String,
+ discussionId: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
diff --git a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
index 180a6bd67e7..5528d2a542b 100644
--- a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
+++ b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
@@ -7,7 +7,15 @@ import Notes from '../../notes';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
const DiffNoteAvatars = Vue.extend({
- props: ['discussionId'],
+ components: {
+ userAvatarImage,
+ },
+ props: {
+ discussionId: {
+ type: String,
+ required: true,
+ },
+ },
data() {
return {
isVisible: false,
@@ -17,77 +25,6 @@ const DiffNoteAvatars = Vue.extend({
collapseIcon,
};
},
- components: {
- userAvatarImage,
- },
- template: `
- <div class="diff-comment-avatar-holders"
- :class="discussionClassName"
- v-show="notesCount !== 0">
- <div v-if="!isVisible">
- <!-- FIXME: Pass an alt attribute here for accessibility -->
- <user-avatar-image
- v-for="note in notesSubset"
- :key="note.id"
- class="diff-comment-avatar js-diff-comment-avatar"
- @click.native="clickedAvatar($event)"
- :img-src="note.authorAvatar"
- :tooltip-text="getTooltipText(note)"
- :data-line-type="lineType"
- :size="19"
- data-html="true"
- />
- <span v-if="notesCount > shownAvatars"
- class="diff-comments-more-count has-tooltip js-diff-comment-avatar"
- data-container="body"
- data-placement="top"
- ref="extraComments"
- role="button"
- :data-line-type="lineType"
- :title="extraNotesTitle"
- @click="clickedAvatar($event)">{{ moreText }}</span>
- </div>
- <button class="diff-notes-collapse js-diff-comment-avatar"
- type="button"
- aria-label="Show comments"
- :data-line-type="lineType"
- @click="clickedAvatar($event)"
- v-if="isVisible"
- v-html="collapseIcon">
- </button>
- </div>
- `,
- mounted() {
- this.$nextTick(() => {
- this.addNoCommentClass();
- this.setDiscussionVisible();
-
- this.lineType = $(this.$el).closest('.diff-line-num').hasClass('old_line') ? 'old' : 'new';
- });
-
- $(document).on('toggle.comments', () => {
- this.$nextTick(() => {
- this.setDiscussionVisible();
- });
- });
- },
- beforeDestroy() {
- this.addNoCommentClass();
- $(document).off('toggle.comments');
- },
- watch: {
- storeState: {
- handler() {
- this.$nextTick(() => {
- $('.has-tooltip', this.$el).tooltip('fixTitle');
-
- // We need to add/remove a class to an element that is outside the Vue instance
- this.addNoCommentClass();
- });
- },
- deep: true,
- },
- },
computed: {
discussionClassName() {
return `js-diff-avatars-${this.discussionId}`;
@@ -128,6 +65,37 @@ const DiffNoteAvatars = Vue.extend({
return `${plusSign}${this.notesCount - this.shownAvatars}`;
},
},
+ watch: {
+ storeState: {
+ handler() {
+ this.$nextTick(() => {
+ $('.has-tooltip', this.$el).tooltip('_fixTitle');
+
+ // We need to add/remove a class to an element that is outside the Vue instance
+ this.addNoCommentClass();
+ });
+ },
+ deep: true,
+ },
+ },
+ mounted() {
+ this.$nextTick(() => {
+ this.addNoCommentClass();
+ this.setDiscussionVisible();
+
+ this.lineType = $(this.$el).closest('.diff-line-num').hasClass('old_line') ? 'old' : 'new';
+ });
+
+ $(document).on('toggle.comments', () => {
+ this.$nextTick(() => {
+ this.setDiscussionVisible();
+ });
+ });
+ },
+ beforeDestroy() {
+ this.addNoCommentClass();
+ $(document).off('toggle.comments');
+ },
methods: {
clickedAvatar(e) {
Notes.instance.onAddDiffNote(e);
@@ -138,12 +106,12 @@ const DiffNoteAvatars = Vue.extend({
this.$nextTick(() => {
this.setDiscussionVisible();
- $('.has-tooltip', this.$el).tooltip('fixTitle');
+ $('.has-tooltip', this.$el).tooltip('_fixTitle');
$('.has-tooltip', this.$el).tooltip('hide');
});
},
addNoCommentClass() {
- const notesCount = this.notesCount;
+ const { notesCount } = this;
$(this.$el).closest('.js-avatar-container')
.toggleClass('no-comment-btn', notesCount > 0)
@@ -164,6 +132,43 @@ const DiffNoteAvatars = Vue.extend({
return `${note.authorName}: ${note.noteTruncated}`;
},
},
+ template: `
+ <div class="diff-comment-avatar-holders"
+ :class="discussionClassName"
+ v-show="notesCount !== 0">
+ <div v-if="!isVisible">
+ <!-- FIXME: Pass an alt attribute here for accessibility -->
+ <user-avatar-image
+ v-for="note in notesSubset"
+ :key="note.id"
+ class="diff-comment-avatar js-diff-comment-avatar"
+ @click.native="clickedAvatar($event)"
+ :img-src="note.authorAvatar"
+ :tooltip-text="getTooltipText(note)"
+ :data-line-type="lineType"
+ :size="19"
+ data-html="true"
+ />
+ <span v-if="notesCount > shownAvatars"
+ class="diff-comments-more-count has-tooltip js-diff-comment-avatar"
+ data-container="body"
+ data-placement="top"
+ ref="extraComments"
+ role="button"
+ :data-line-type="lineType"
+ :title="extraNotesTitle"
+ @click="clickedAvatar($event)">{{ moreText }}</span>
+ </div>
+ <button class="diff-notes-collapse js-diff-comment-avatar"
+ type="button"
+ aria-label="Show comments"
+ :data-line-type="lineType"
+ @click="clickedAvatar($event)"
+ v-if="isVisible"
+ v-html="collapseIcon">
+ </button>
+ </div>
+ `,
});
Vue.component('diff-note-avatars', DiffNoteAvatars);
diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
index 8f9186dfb9a..2b893e35b6d 100644
--- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
+++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
@@ -1,16 +1,18 @@
-/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, guard-for-in, no-restricted-syntax, one-var, space-before-function-paren, no-lonely-if, no-continue, brace-style, max-len, quotes */
-/* global DiscussionMixins */
+/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, guard-for-in, no-restricted-syntax, no-lonely-if, no-continue, brace-style, max-len, quotes */
/* global CommentsStore */
import $ from 'jquery';
import Vue from 'vue';
-import '../mixins/discussion';
+import DiscussionMixins from '../mixins/discussion';
const JumpToDiscussion = Vue.extend({
mixins: [DiscussionMixins],
props: {
- discussionId: String
+ discussionId: {
+ type: String,
+ required: true,
+ },
},
data: function () {
return {
@@ -52,6 +54,9 @@ const JumpToDiscussion = Vue.extend({
return lastId;
}
},
+ created() {
+ this.discussion = this.discussions[this.discussionId];
+ },
methods: {
jumpToNextUnresolvedDiscussion: function () {
let discussionsSelector;
@@ -68,7 +73,7 @@ const JumpToDiscussion = Vue.extend({
}).toArray();
};
- const discussions = this.discussions;
+ const { discussions } = this;
if (activeTab === 'diffs') {
discussionsSelector = '.diffs .notes[data-discussion-id]';
@@ -202,9 +207,6 @@ const JumpToDiscussion = Vue.extend({
});
}
},
- created() {
- this.discussion = this.discussions[this.discussionId];
- },
});
Vue.component('jump-to-discussion', JumpToDiscussion);
diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js b/app/assets/javascripts/diff_notes/components/resolve_btn.js
index df4c72ba0ed..a69b34b0db8 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_btn.js
+++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js
@@ -1,4 +1,3 @@
-/* eslint-disable comma-dangle, object-shorthand, func-names, quote-props, no-else-return, camelcase, max-len */
/* global CommentsStore */
/* global ResolveService */
@@ -8,113 +7,135 @@ import Flash from '../../flash';
const ResolveBtn = Vue.extend({
props: {
- noteId: Number,
- discussionId: String,
- resolved: Boolean,
- canResolve: Boolean,
- resolvedBy: String,
- authorName: String,
- authorAvatar: String,
- noteTruncated: String,
+ noteId: {
+ type: Number,
+ required: true,
+ },
+ discussionId: {
+ type: String,
+ required: true,
+ },
+ resolved: {
+ type: Boolean,
+ required: true,
+ },
+ canResolve: {
+ type: Boolean,
+ required: true,
+ },
+ resolvedBy: {
+ type: String,
+ required: true,
+ },
+ authorName: {
+ type: String,
+ required: true,
+ },
+ authorAvatar: {
+ type: String,
+ required: true,
+ },
+ noteTruncated: {
+ type: String,
+ required: true,
+ },
},
- data: function () {
+ data() {
return {
discussions: CommentsStore.state,
- loading: false
+ loading: false,
};
},
- watch: {
- 'discussions': {
- handler: 'updateTooltip',
- deep: true
- }
- },
computed: {
- discussion: function () {
+ discussion() {
return this.discussions[this.discussionId];
},
- note: function () {
+ note() {
return this.discussion ? this.discussion.getNote(this.noteId) : {};
},
- buttonText: function () {
+ buttonText() {
if (this.isResolved) {
return `Resolved by ${this.resolvedByName}`;
} else if (this.canResolve) {
return 'Mark as resolved';
- } else {
- return 'Unable to resolve';
}
+
+ return 'Unable to resolve';
},
- isResolved: function () {
+ isResolved() {
if (this.note) {
return this.note.resolved;
- } else {
- return false;
}
+
+ return false;
},
- resolvedByName: function () {
+ resolvedByName() {
return this.note.resolved_by;
},
},
+ watch: {
+ discussions: {
+ handler: 'updateTooltip',
+ deep: true,
+ },
+ },
+ mounted() {
+ $(this.$refs.button).tooltip({
+ container: 'body',
+ });
+ },
+ beforeDestroy() {
+ CommentsStore.delete(this.discussionId, this.noteId);
+ },
+ created() {
+ CommentsStore.create({
+ discussionId: this.discussionId,
+ noteId: this.noteId,
+ canResolve: this.canResolve,
+ resolved: this.resolved,
+ resolvedBy: this.resolvedBy,
+ authorName: this.authorName,
+ authorAvatar: this.authorAvatar,
+ noteTruncated: this.noteTruncated,
+ });
+ },
methods: {
- updateTooltip: function () {
+ updateTooltip() {
this.$nextTick(() => {
$(this.$refs.button)
.tooltip('hide')
- .tooltip('fixTitle');
+ .tooltip('_fixTitle');
});
},
- resolve: function () {
+ resolve() {
if (!this.canResolve) return;
let promise;
this.loading = true;
if (this.isResolved) {
- promise = ResolveService
- .unresolve(this.noteId);
+ promise = ResolveService.unresolve(this.noteId);
} else {
- promise = ResolveService
- .resolve(this.noteId);
+ promise = ResolveService.resolve(this.noteId);
}
promise
.then(resp => resp.json())
- .then((data) => {
+ .then(data => {
this.loading = false;
- const resolved_by = data ? data.resolved_by : null;
+ const resolvedBy = data ? data.resolved_by : null;
- CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolved_by);
+ CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolvedBy);
this.discussion.updateHeadline(data);
gl.mrWidget.checkStatus();
- document.dispatchEvent(new CustomEvent('refreshVueNotes'));
-
this.updateTooltip();
})
- .catch(() => new Flash('An error occurred when trying to resolve a comment. Please try again.'));
- }
- },
- mounted: function () {
- $(this.$refs.button).tooltip({
- container: 'body'
- });
- },
- beforeDestroy: function () {
- CommentsStore.delete(this.discussionId, this.noteId);
+ .catch(
+ () => new Flash('An error occurred when trying to resolve a comment. Please try again.'),
+ );
+ },
},
- created: function () {
- CommentsStore.create({
- discussionId: this.discussionId,
- noteId: this.noteId,
- canResolve: this.canResolve,
- resolved: this.resolved,
- resolvedBy: this.resolvedBy,
- authorName: this.authorName,
- authorAvatar: this.authorAvatar,
- noteTruncated: this.noteTruncated,
- });
- }
});
Vue.component('resolve-btn', ResolveBtn);
diff --git a/app/assets/javascripts/diff_notes/components/resolve_count.js b/app/assets/javascripts/diff_notes/components/resolve_count.js
index fe7cf8f5fc1..e2683e09f40 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_count.js
+++ b/app/assets/javascripts/diff_notes/components/resolve_count.js
@@ -1,15 +1,17 @@
-/* eslint-disable comma-dangle, object-shorthand, func-names, no-param-reassign */
-/* global DiscussionMixins */
+/* eslint-disable comma-dangle, object-shorthand, func-names */
/* global CommentsStore */
import Vue from 'vue';
-import '../mixins/discussion';
+import DiscussionMixins from '../mixins/discussion';
window.ResolveCount = Vue.extend({
mixins: [DiscussionMixins],
props: {
- loggedOut: Boolean
+ loggedOut: {
+ type: Boolean,
+ required: true,
+ },
},
data: function () {
return {
diff --git a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js
index 6a036e96171..5ed13488788 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js
+++ b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js
@@ -1,4 +1,4 @@
-/* eslint-disable object-shorthand, func-names, space-before-function-paren, comma-dangle, no-else-return, quotes, max-len */
+/* eslint-disable object-shorthand, func-names, comma-dangle, no-else-return, quotes */
/* global CommentsStore */
/* global ResolveService */
@@ -6,9 +6,18 @@ import Vue from 'vue';
const ResolveDiscussionBtn = Vue.extend({
props: {
- discussionId: String,
- mergeRequestId: Number,
- canResolve: Boolean,
+ discussionId: {
+ type: String,
+ required: true,
+ },
+ mergeRequestId: {
+ type: Number,
+ required: true,
+ },
+ canResolve: {
+ type: Boolean,
+ required: true,
+ },
},
data: function() {
return {
@@ -45,16 +54,16 @@ const ResolveDiscussionBtn = Vue.extend({
}
}
},
+ created: function () {
+ CommentsStore.createDiscussion(this.discussionId, this.canResolve);
+
+ this.discussion = CommentsStore.state[this.discussionId];
+ },
methods: {
resolve: function () {
ResolveService.toggleResolveForDiscussion(this.mergeRequestId, this.discussionId);
}
},
- created: function () {
- CommentsStore.createDiscussion(this.discussionId, this.canResolve);
-
- this.discussion = CommentsStore.state[this.discussionId];
- }
});
Vue.component('resolve-discussion-btn', ResolveDiscussionBtn);
diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js b/app/assets/javascripts/diff_notes/diff_notes_bundle.js
index e17daec6a92..7dcf3594471 100644
--- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js
+++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js
@@ -1,5 +1,4 @@
-/* eslint-disable func-names, comma-dangle, new-cap, no-new, max-len */
-/* global ResolveCount */
+/* eslint-disable func-names, new-cap */
import $ from 'jquery';
import Vue from 'vue';
@@ -15,12 +14,13 @@ import './components/resolve_count';
import './components/resolve_discussion_btn';
import './components/diff_note_avatars';
import './components/new_issue_for_discussion';
-import { hasVueMRDiscussionsCookie } from '../lib/utils/common_utils';
export default () => {
- const projectPathHolder = document.querySelector('.merge-request') || document.querySelector('.commit-box');
- const projectPath = projectPathHolder.dataset.projectPath;
- const COMPONENT_SELECTOR = 'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn, new-issue-for-discussion-btn';
+ const projectPathHolder =
+ document.querySelector('.merge-request') || document.querySelector('.commit-box');
+ const { projectPath } = projectPathHolder.dataset;
+ const COMPONENT_SELECTOR =
+ 'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn, new-issue-for-discussion-btn';
window.gl = window.gl || {};
window.gl.diffNoteApps = {};
@@ -28,9 +28,9 @@ export default () => {
window.ResolveService = new gl.DiffNotesResolveServiceClass(projectPath);
gl.diffNotesCompileComponents = () => {
- $('diff-note-avatars').each(function () {
+ $('diff-note-avatars').each(function() {
const tmp = Vue.extend({
- template: $(this).get(0).outerHTML
+ template: $(this).get(0).outerHTML,
});
const tmpApp = new tmp().$mount();
@@ -41,12 +41,12 @@ export default () => {
});
});
- const $components = $(COMPONENT_SELECTOR).filter(function () {
+ const $components = $(COMPONENT_SELECTOR).filter(function() {
return $(this).closest('resolve-count').length !== 1;
});
if ($components) {
- $components.each(function () {
+ $components.each(function() {
const $this = $(this);
const noteId = $this.attr(':note-id');
const discussionId = $this.attr(':discussion-id');
@@ -54,7 +54,7 @@ export default () => {
if ($this.is('comment-and-resolve-btn') && !discussionId) return;
const tmp = Vue.extend({
- template: $this.get(0).outerHTML
+ template: $this.get(0).outerHTML,
});
const tmpApp = new tmp().$mount();
@@ -69,14 +69,5 @@ export default () => {
gl.diffNotesCompileComponents();
- if (!hasVueMRDiscussionsCookie()) {
- new Vue({
- el: '#resolve-count-app',
- components: {
- 'resolve-count': ResolveCount
- },
- });
- }
-
$(window).trigger('resize.nav');
};
diff --git a/app/assets/javascripts/diff_notes/mixins/discussion.js b/app/assets/javascripts/diff_notes/mixins/discussion.js
index 36c4abf02cf..ef35b589e58 100644
--- a/app/assets/javascripts/diff_notes/mixins/discussion.js
+++ b/app/assets/javascripts/diff_notes/mixins/discussion.js
@@ -1,6 +1,6 @@
-/* eslint-disable object-shorthand, func-names, guard-for-in, no-restricted-syntax, comma-dangle, no-param-reassign, max-len */
+/* eslint-disable object-shorthand, func-names, guard-for-in, no-restricted-syntax, comma-dangle, */
-window.DiscussionMixins = {
+const DiscussionMixins = {
computed: {
discussionCount: function () {
return Object.keys(this.discussions).length;
@@ -33,3 +33,5 @@ window.DiscussionMixins = {
}
}
};
+
+export default DiscussionMixins;
diff --git a/app/assets/javascripts/diff_notes/models/discussion.js b/app/assets/javascripts/diff_notes/models/discussion.js
index c97c559dd14..787e6d8855f 100644
--- a/app/assets/javascripts/diff_notes/models/discussion.js
+++ b/app/assets/javascripts/diff_notes/models/discussion.js
@@ -1,4 +1,4 @@
-/* eslint-disable space-before-function-paren, camelcase, guard-for-in, no-restricted-syntax, no-unused-vars, max-len */
+/* eslint-disable camelcase, guard-for-in, no-restricted-syntax */
/* global NoteModel */
import $ from 'jquery';
diff --git a/app/assets/javascripts/diff_notes/models/note.js b/app/assets/javascripts/diff_notes/models/note.js
index 04465aa507e..825a69deeec 100644
--- a/app/assets/javascripts/diff_notes/models/note.js
+++ b/app/assets/javascripts/diff_notes/models/note.js
@@ -1,5 +1,3 @@
-/* eslint-disable camelcase, no-unused-vars */
-
class NoteModel {
constructor(discussionId, noteObj) {
this.discussionId = discussionId;
diff --git a/app/assets/javascripts/diff_notes/services/resolve.js b/app/assets/javascripts/diff_notes/services/resolve.js
index d16f9297de1..0b3568e432d 100644
--- a/app/assets/javascripts/diff_notes/services/resolve.js
+++ b/app/assets/javascripts/diff_notes/services/resolve.js
@@ -8,8 +8,12 @@ window.gl = window.gl || {};
class ResolveServiceClass {
constructor(root) {
- this.noteResource = Vue.resource(`${root}/notes{/noteId}/resolve?html=true`);
- this.discussionResource = Vue.resource(`${root}/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve?html=true`);
+ this.noteResource = Vue.resource(
+ `${root}/notes{/noteId}/resolve?html=true`,
+ );
+ this.discussionResource = Vue.resource(
+ `${root}/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve?html=true`,
+ );
}
resolve(noteId) {
@@ -33,7 +37,7 @@ class ResolveServiceClass {
promise
.then(resp => resp.json())
- .then((data) => {
+ .then(data => {
discussion.loading = false;
const resolvedBy = data ? data.resolved_by : null;
@@ -45,9 +49,13 @@ class ResolveServiceClass {
if (gl.mrWidget) gl.mrWidget.checkStatus();
discussion.updateHeadline(data);
- document.dispatchEvent(new CustomEvent('refreshVueNotes'));
})
- .catch(() => new Flash('An error occurred when trying to resolve a discussion. Please try again.'));
+ .catch(
+ () =>
+ new Flash(
+ 'An error occurred when trying to resolve a discussion. Please try again.',
+ ),
+ );
}
resolveAll(mergeRequestId, discussionId) {
@@ -55,10 +63,13 @@ class ResolveServiceClass {
discussion.loading = true;
- return this.discussionResource.save({
- mergeRequestId,
- discussionId,
- }, {});
+ return this.discussionResource.save(
+ {
+ mergeRequestId,
+ discussionId,
+ },
+ {},
+ );
}
unResolveAll(mergeRequestId, discussionId) {
@@ -66,10 +77,13 @@ class ResolveServiceClass {
discussion.loading = true;
- return this.discussionResource.delete({
- mergeRequestId,
- discussionId,
- }, {});
+ return this.discussionResource.delete(
+ {
+ mergeRequestId,
+ discussionId,
+ },
+ {},
+ );
}
}
diff --git a/app/assets/javascripts/diff_notes/stores/comments.js b/app/assets/javascripts/diff_notes/stores/comments.js
index d802db7d3af..d7da7d974f3 100644
--- a/app/assets/javascripts/diff_notes/stores/comments.js
+++ b/app/assets/javascripts/diff_notes/stores/comments.js
@@ -1,4 +1,4 @@
-/* eslint-disable object-shorthand, func-names, camelcase, no-restricted-syntax, guard-for-in, comma-dangle, max-len, no-param-reassign */
+/* eslint-disable object-shorthand, func-names, camelcase, no-restricted-syntax, guard-for-in, comma-dangle, max-len */
/* global DiscussionModel */
import Vue from 'vue';
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
new file mode 100644
index 00000000000..0327fceb38d
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -0,0 +1,217 @@
+<script>
+import { mapState, mapGetters, mapActions } from 'vuex';
+import Icon from '~/vue_shared/components/icon.vue';
+import { __ } from '~/locale';
+import createFlash from '~/flash';
+import eventHub from '../../notes/event_hub';
+import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
+import CompareVersions from './compare_versions.vue';
+import ChangedFiles from './changed_files.vue';
+import DiffFile from './diff_file.vue';
+import NoChanges from './no_changes.vue';
+import HiddenFilesWarning from './hidden_files_warning.vue';
+
+export default {
+ name: 'DiffsApp',
+ components: {
+ Icon,
+ LoadingIcon,
+ CompareVersions,
+ ChangedFiles,
+ DiffFile,
+ NoChanges,
+ HiddenFilesWarning,
+ },
+ props: {
+ endpoint: {
+ type: String,
+ required: true,
+ },
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ shouldShow: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ currentUser: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ activeFile: '',
+ };
+ },
+ computed: {
+ ...mapState({
+ isLoading: state => state.diffs.isLoading,
+ diffFiles: state => state.diffs.diffFiles,
+ diffViewType: state => state.diffs.diffViewType,
+ mergeRequestDiffs: state => state.diffs.mergeRequestDiffs,
+ mergeRequestDiff: state => state.diffs.mergeRequestDiff,
+ latestVersionPath: state => state.diffs.latestVersionPath,
+ startVersion: state => state.diffs.startVersion,
+ commit: state => state.diffs.commit,
+ targetBranchName: state => state.diffs.targetBranchName,
+ renderOverflowWarning: state => state.diffs.renderOverflowWarning,
+ numTotalFiles: state => state.diffs.realSize,
+ numVisibleFiles: state => state.diffs.size,
+ plainDiffPath: state => state.diffs.plainDiffPath,
+ emailPatchPath: state => state.diffs.emailPatchPath,
+ }),
+ ...mapGetters('diffs', ['isParallelView']),
+ ...mapGetters(['isNotesFetched']),
+ targetBranch() {
+ return {
+ branchName: this.targetBranchName,
+ versionIndex: -1,
+ path: '',
+ };
+ },
+ notAllCommentsDisplayed() {
+ if (this.commit) {
+ return __('Only comments from the following commit are shown below');
+ } else if (this.startVersion) {
+ return __(
+ "Not all comments are displayed because you're comparing two versions of the diff.",
+ );
+ }
+ return __(
+ "Not all comments are displayed because you're viewing an old version of the diff.",
+ );
+ },
+ showLatestVersion() {
+ if (this.commit) {
+ return __('Show latest version of the diff');
+ }
+ return __('Show latest version');
+ },
+ },
+ watch: {
+ diffViewType() {
+ this.adjustView();
+ },
+ shouldShow() {
+ // When the shouldShow property changed to true, the route is rendered for the first time
+ // and if we have the isLoading as true this means we didn't fetch the data
+ if (this.isLoading) {
+ this.fetchData();
+ }
+
+ this.adjustView();
+ },
+ },
+ mounted() {
+ this.setBaseConfig({ endpoint: this.endpoint, projectPath: this.projectPath });
+
+ if (this.shouldShow) {
+ this.fetchData();
+ }
+ },
+ created() {
+ this.adjustView();
+ },
+ methods: {
+ ...mapActions('diffs', ['setBaseConfig', 'fetchDiffFiles']),
+ fetchData() {
+ this.fetchDiffFiles().catch(() => {
+ createFlash(__('Something went wrong on our end. Please try again!'));
+ });
+
+ if (!this.isNotesFetched) {
+ eventHub.$emit('fetchNotesData');
+ }
+ },
+ setActive(filePath) {
+ this.activeFile = filePath;
+ },
+ unsetActive(filePath) {
+ if (this.activeFile === filePath) {
+ this.activeFile = '';
+ }
+ },
+ adjustView() {
+ if (this.shouldShow && this.isParallelView) {
+ window.mrTabs.expandViewContainer();
+ } else {
+ window.mrTabs.resetViewContainer();
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div v-show="shouldShow">
+ <div
+ v-if="isLoading"
+ class="loading"
+ >
+ <loading-icon />
+ </div>
+ <div
+ v-else
+ id="diffs"
+ :class="{ active: shouldShow }"
+ class="diffs tab-pane"
+ >
+ <compare-versions
+ v-if="!commit && mergeRequestDiffs.length > 1"
+ :merge-request-diffs="mergeRequestDiffs"
+ :merge-request-diff="mergeRequestDiff"
+ :start-version="startVersion"
+ :target-branch="targetBranch"
+ />
+
+ <hidden-files-warning
+ v-if="renderOverflowWarning"
+ :visible="numVisibleFiles"
+ :total="numTotalFiles"
+ :plain-diff-path="plainDiffPath"
+ :email-patch-path="emailPatchPath"
+ />
+
+ <div
+ v-if="commit || startVersion || (mergeRequestDiff && !mergeRequestDiff.latest)"
+ class="mr-version-controls"
+ >
+ <div class="content-block comments-disabled-notif clearfix">
+ <i class="fa fa-info-circle"></i>
+ {{ notAllCommentsDisplayed }}
+ <div class="pull-right">
+ <a
+ :href="latestVersionPath"
+ class="btn btn-sm"
+ >
+ {{ showLatestVersion }}
+ </a>
+ </div>
+ </div>
+ </div>
+
+ <changed-files
+ :diff-files="diffFiles"
+ :active-file="activeFile"
+ />
+
+ <div
+ v-if="diffFiles.length > 0"
+ class="files"
+ >
+ <diff-file
+ v-for="file in diffFiles"
+ :key="file.newPath"
+ :file="file"
+ :current-user="currentUser"
+ @setActive="setActive(file.filePath)"
+ @unsetActive="unsetActive(file.filePath)"
+ />
+ </div>
+ <no-changes v-else />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/changed_files.vue b/app/assets/javascripts/diffs/components/changed_files.vue
new file mode 100644
index 00000000000..9d29357d800
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/changed_files.vue
@@ -0,0 +1,184 @@
+<script>
+import { mapGetters, mapActions } from 'vuex';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import { pluralize } from '~/lib/utils/text_utility';
+import { getParameterValues, mergeUrlParams } from '~/lib/utils/url_utility';
+import { contentTop } from '~/lib/utils/common_utils';
+import { __ } from '~/locale';
+import ChangedFilesDropdown from './changed_files_dropdown.vue';
+import changedFilesMixin from '../mixins/changed_files';
+
+export default {
+ components: {
+ Icon,
+ ChangedFilesDropdown,
+ ClipboardButton,
+ },
+ mixins: [changedFilesMixin],
+ props: {
+ activeFile: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ isStuck: false,
+ maxWidth: 'auto',
+ offsetTop: 0,
+ };
+ },
+ computed: {
+ ...mapGetters('diffs', ['isInlineView', 'isParallelView', 'areAllFilesCollapsed']),
+ sumAddedLines() {
+ return this.sumValues('addedLines');
+ },
+ sumRemovedLines() {
+ return this.sumValues('removedLines');
+ },
+ whitespaceVisible() {
+ return !getParameterValues('w')[0];
+ },
+ toggleWhitespaceText() {
+ if (this.whitespaceVisible) {
+ return __('Hide whitespace changes');
+ }
+ return __('Show whitespace changes');
+ },
+ toggleWhitespacePath() {
+ if (this.whitespaceVisible) {
+ return mergeUrlParams({ w: 1 }, window.location.href);
+ }
+
+ return mergeUrlParams({ w: 0 }, window.location.href);
+ },
+ top() {
+ return `${this.offsetTop}px`;
+ },
+ },
+ created() {
+ document.addEventListener('scroll', this.handleScroll);
+ this.offsetTop = contentTop();
+ },
+ beforeDestroy() {
+ document.removeEventListener('scroll', this.handleScroll);
+ },
+ methods: {
+ ...mapActions('diffs', ['setInlineDiffViewType', 'setParallelDiffViewType', 'expandAllFiles']),
+ pluralize,
+ handleScroll() {
+ if (!this.updating) {
+ requestAnimationFrame(this.updateIsStuck);
+ this.updating = true;
+ }
+ },
+ updateIsStuck() {
+ if (!this.$refs.wrapper) {
+ return;
+ }
+
+ const scrollPosition = window.scrollY;
+
+ this.isStuck = scrollPosition + this.offsetTop >= this.$refs.placeholder.offsetTop;
+ this.updating = false;
+ },
+ sumValues(key) {
+ return this.diffFiles.reduce((total, file) => total + file[key], 0);
+ },
+ },
+};
+</script>
+
+<template>
+ <span>
+ <div ref="placeholder"></div>
+ <div
+ ref="wrapper"
+ :style="{ top }"
+ :class="{'is-stuck': isStuck}"
+ class="content-block oneline-block diff-files-changed diff-files-changed-merge-request
+ files-changed js-diff-files-changed"
+ >
+ <div class="files-changed-inner">
+ <div
+ class="inline-parallel-buttons d-none d-md-block"
+ >
+ <a
+ v-if="areAllFilesCollapsed"
+ class="btn btn-default"
+ @click="expandAllFiles"
+ >
+ {{ __('Expand all') }}
+ </a>
+ <a
+ :href="toggleWhitespacePath"
+ class="btn btn-default"
+ >
+ {{ toggleWhitespaceText }}
+ </a>
+ <div class="btn-group">
+ <button
+ id="inline-diff-btn"
+ :class="{ active: isInlineView }"
+ type="button"
+ class="btn js-inline-diff-button"
+ data-view-type="inline"
+ @click="setInlineDiffViewType"
+ >
+ {{ __('Inline') }}
+ </button>
+ <button
+ id="parallel-diff-btn"
+ :class="{ active: isParallelView }"
+ type="button"
+ class="btn js-parallel-diff-button"
+ data-view-type="parallel"
+ @click="setParallelDiffViewType"
+ >
+ {{ __('Side-by-side') }}
+ </button>
+ </div>
+ </div>
+
+ <div class="commit-stat-summary dropdown">
+ <changed-files-dropdown
+ :diff-files="diffFiles"
+ />
+
+ <span
+ v-show="activeFile"
+ class="prepend-left-5"
+ >
+ <strong class="prepend-right-5">
+ {{ truncatedDiffPath(activeFile) }}
+ </strong>
+ <clipboard-button
+ :text="activeFile"
+ :title="s__('Copy file name to clipboard')"
+ tooltip-placement="bottom"
+ tooltip-container="body"
+ class="btn btn-default btn-transparent btn-clipboard"
+ />
+ </span>
+
+ <span
+ v-show="!isStuck"
+ id="diff-stats"
+ class="diff-stats-additions-deletions-expanded"
+ >
+ with
+ <strong class="cgreen">
+ {{ pluralize(`${sumAddedLines} addition`, sumAddedLines) }}
+ </strong>
+ and
+ <strong class="cred">
+ {{ pluralize(`${sumRemovedLines} deletion`, sumRemovedLines) }}
+ </strong>
+ </span>
+ </div>
+ </div>
+ </div>
+ </span>
+</template>
diff --git a/app/assets/javascripts/diffs/components/changed_files_dropdown.vue b/app/assets/javascripts/diffs/components/changed_files_dropdown.vue
new file mode 100644
index 00000000000..b38d217fbe3
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/changed_files_dropdown.vue
@@ -0,0 +1,126 @@
+<script>
+import Icon from '~/vue_shared/components/icon.vue';
+import changedFilesMixin from '../mixins/changed_files';
+
+export default {
+ components: {
+ Icon,
+ },
+ mixins: [changedFilesMixin],
+ data() {
+ return {
+ searchText: '',
+ };
+ },
+ computed: {
+ filteredDiffFiles() {
+ return this.diffFiles.filter(file =>
+ file.filePath.toLowerCase().includes(this.searchText.toLowerCase()),
+ );
+ },
+ },
+ methods: {
+ clearSearch() {
+ this.searchText = '';
+ },
+ },
+};
+</script>
+
+<template>
+ <span>
+ Showing
+ <button
+ class="diff-stats-summary-toggler"
+ data-toggle="dropdown"
+ type="button"
+ aria-expanded="false"
+ >
+ <span>
+ {{ n__('%d changed file', '%d changed files', diffFiles.length) }}
+ </span>
+ <icon
+ :size="8"
+ name="chevron-down"
+ />
+ </button>
+ <div class="dropdown-menu diff-file-changes">
+ <div class="dropdown-input">
+ <input
+ v-model="searchText"
+ type="search"
+ class="dropdown-input-field"
+ placeholder="Search files"
+ autocomplete="off"
+ />
+ <i
+ v-if="searchText.length === 0"
+ aria-hidden="true"
+ data-hidden="true"
+ class="fa fa-search dropdown-input-search">
+ </i>
+ <i
+ v-else
+ role="button"
+ class="fa fa-times dropdown-input-search"
+ @click="clearSearch"
+ ></i>
+ </div>
+ <div class="dropdown-content">
+ <ul>
+ <li
+ v-for="diffFile in filteredDiffFiles"
+ :key="diffFile.name"
+ >
+ <a
+ :href="`#${diffFile.fileHash}`"
+ :title="diffFile.newPath"
+ class="diff-changed-file"
+ >
+ <icon
+ :name="fileChangedIcon(diffFile)"
+ :size="16"
+ :class="fileChangedClass(diffFile)"
+ class="diff-file-changed-icon append-right-8"
+ />
+ <span class="diff-changed-file-content append-right-8">
+ <strong
+ v-if="diffFile.blob && diffFile.blob.name"
+ class="diff-changed-file-name"
+ >
+ {{ diffFile.blob.name }}
+ </strong>
+ <strong
+ v-else
+ class="diff-changed-blank-file-name"
+ >
+ {{ s__('Diffs|No file name available') }}
+ </strong>
+ <span class="diff-changed-file-path prepend-top-5">
+ {{ truncatedDiffPath(diffFile.blob.path) }}
+ </span>
+ </span>
+ <span class="diff-changed-stats">
+ <span class="cgreen">
+ +{{ diffFile.addedLines }}
+ </span>
+ <span class="cred">
+ -{{ diffFile.removedLines }}
+ </span>
+ </span>
+ </a>
+ </li>
+
+ <li
+ v-show="filteredDiffFiles.length === 0"
+ class="dropdown-menu-empty-item"
+ >
+ <a>
+ {{ __('No files found') }}
+ </a>
+ </li>
+ </ul>
+ </div>
+ </div>
+ </span>
+</template>
diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue
new file mode 100644
index 00000000000..1c9ad8e77f1
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/compare_versions.vue
@@ -0,0 +1,55 @@
+<script>
+import CompareVersionsDropdown from './compare_versions_dropdown.vue';
+
+export default {
+ components: {
+ CompareVersionsDropdown,
+ },
+ props: {
+ mergeRequestDiffs: {
+ type: Array,
+ required: true,
+ },
+ mergeRequestDiff: {
+ type: Object,
+ required: true,
+ },
+ startVersion: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ targetBranch: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ },
+ computed: {
+ comparableDiffs() {
+ return this.mergeRequestDiffs.slice(1);
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="mr-version-controls">
+ <div class="mr-version-menus-container content-block">
+ Changes between
+ <compare-versions-dropdown
+ :other-versions="mergeRequestDiffs"
+ :merge-request-version="mergeRequestDiff"
+ :show-commit-count="true"
+ class="mr-version-dropdown"
+ />
+ and
+ <compare-versions-dropdown
+ :other-versions="comparableDiffs"
+ :start-version="startVersion"
+ :target-branch="targetBranch"
+ class="mr-version-compare-dropdown"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue b/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue
new file mode 100644
index 00000000000..96cccb49378
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue
@@ -0,0 +1,165 @@
+<script>
+import Icon from '~/vue_shared/components/icon.vue';
+import { n__, __ } from '~/locale';
+import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+
+export default {
+ components: {
+ Icon,
+ TimeAgo,
+ },
+ props: {
+ otherVersions: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ mergeRequestVersion: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ startVersion: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ targetBranch: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ showCommitCount: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ baseVersion() {
+ return {
+ name: 'hii',
+ versionIndex: -1,
+ };
+ },
+ targetVersions() {
+ if (this.mergeRequestVersion) {
+ return this.otherVersions;
+ }
+ return [...this.otherVersions, this.targetBranch];
+ },
+ selectedVersionName() {
+ const selectedVersion = this.startVersion || this.targetBranch || this.mergeRequestVersion;
+ return this.versionName(selectedVersion);
+ },
+ },
+ methods: {
+ commitsText(version) {
+ return n__(
+ `${version.commitsCount} commit,`,
+ `${version.commitsCount} commits,`,
+ version.commitsCount,
+ );
+ },
+ href(version) {
+ if (this.showCommitCount) {
+ return version.versionPath;
+ }
+ return version.comparePath;
+ },
+ versionName(version) {
+ if (this.isLatest(version)) {
+ return __('latest version');
+ }
+ if (this.targetBranch && (this.isBase(version) || !version)) {
+ return this.targetBranch.branchName;
+ }
+ return `version ${version.versionIndex}`;
+ },
+ isActive(version) {
+ if (!version) {
+ return false;
+ }
+
+ if (this.targetBranch) {
+ return (
+ (this.isBase(version) && !this.startVersion) ||
+ (this.startVersion && this.startVersion.versionIndex === version.versionIndex)
+ );
+ }
+
+ return version.versionIndex === this.mergeRequestVersion.versionIndex;
+ },
+ isBase(version) {
+ if (!version || !this.targetBranch) {
+ return false;
+ }
+ return version.versionIndex === -1;
+ },
+ isLatest(version) {
+ return (
+ this.mergeRequestVersion && version.versionIndex === this.targetVersions[0].versionIndex
+ );
+ },
+ },
+};
+</script>
+
+<template>
+ <span class="dropdown inline">
+ <a
+ class="dropdown-toggle btn btn-default"
+ data-toggle="dropdown"
+ aria-expanded="false"
+ >
+ <span>
+ {{ selectedVersionName }}
+ </span>
+ <Icon
+ :size="12"
+ name="angle-down"
+ />
+ </a>
+ <div class="dropdown-menu dropdown-select dropdown-menu-selectable">
+ <div class="dropdown-content">
+ <ul>
+ <li
+ v-for="version in targetVersions"
+ :key="version.id"
+ >
+ <a
+ :class="{ 'is-active': isActive(version) }"
+ :href="href(version)"
+ >
+ <div>
+ <strong>
+ {{ versionName(version) }}
+ <template v-if="isBase(version)">
+ (base)
+ </template>
+ </strong>
+ </div>
+ <div>
+ <small class="commit-sha">
+ {{ version.truncatedCommitSha }}
+ </small>
+ </div>
+ <div>
+ <small>
+ <template v-if="showCommitCount">
+ {{ commitsText(version) }}
+ </template>
+ <time-ago
+ v-if="version.createdAt"
+ :time="version.createdAt"
+ class="js-timeago js-timeago-render"
+ />
+ </small>
+ </div>
+ </a>
+ </li>
+ </ul>
+ </div>
+ </div>
+ </span>
+</template>
diff --git a/app/assets/javascripts/diffs/components/diff_content.vue b/app/assets/javascripts/diffs/components/diff_content.vue
new file mode 100644
index 00000000000..02d5be1821b
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/diff_content.vue
@@ -0,0 +1,62 @@
+<script>
+import { mapGetters, mapState } from 'vuex';
+import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
+import { diffModes } from '~/ide/constants';
+import InlineDiffView from './inline_diff_view.vue';
+import ParallelDiffView from './parallel_diff_view.vue';
+
+export default {
+ components: {
+ InlineDiffView,
+ ParallelDiffView,
+ DiffViewer,
+ },
+ props: {
+ diffFile: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState({
+ projectPath: state => state.diffs.projectPath,
+ endpoint: state => state.diffs.endpoint,
+ }),
+ ...mapGetters('diffs', ['isInlineView', 'isParallelView']),
+ diffMode() {
+ const diffModeKey = Object.keys(diffModes).find(key => this.diffFile[`${key}File`]);
+ return diffModes[diffModeKey] || diffModes.replaced;
+ },
+ isTextFile() {
+ return this.diffFile.text;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="diff-content">
+ <div class="diff-viewer">
+ <template v-if="isTextFile">
+ <inline-diff-view
+ v-if="isInlineView"
+ :diff-file="diffFile"
+ :diff-lines="diffFile.highlightedDiffLines || []"
+ />
+ <parallel-diff-view
+ v-if="isParallelView"
+ :diff-file="diffFile"
+ :diff-lines="diffFile.parallelDiffLines || []"
+ />
+ </template>
+ <diff-viewer
+ v-else
+ :diff-mode="diffMode"
+ :new-path="diffFile.newPath"
+ :new-sha="diffFile.diffRefs.headSha"
+ :old-path="diffFile.oldPath"
+ :old-sha="diffFile.diffRefs.baseSha"
+ :project-path="projectPath"/>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/diff_discussions.vue b/app/assets/javascripts/diffs/components/diff_discussions.vue
new file mode 100644
index 00000000000..20483161033
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/diff_discussions.vue
@@ -0,0 +1,37 @@
+<script>
+import noteableDiscussion from '../../notes/components/noteable_discussion.vue';
+
+export default {
+ components: {
+ noteableDiscussion,
+ },
+ props: {
+ discussions: {
+ type: Array,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div
+ v-for="discussion in discussions"
+ :key="discussion.id"
+ class="discussion-notes diff-discussions"
+ >
+ <ul
+ :data-discussion-id="discussion.id"
+ class="notes"
+ >
+ <noteable-discussion
+ :discussion="discussion"
+ :render-header="false"
+ :render-diff-file="false"
+ :always-expanded="true"
+ />
+ </ul>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
new file mode 100644
index 00000000000..a61e368249a
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -0,0 +1,190 @@
+<script>
+import { mapActions } from 'vuex';
+import _ from 'underscore';
+import { __, sprintf } from '~/locale';
+import createFlash from '~/flash';
+import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
+import DiffFileHeader from './diff_file_header.vue';
+import DiffContent from './diff_content.vue';
+
+export default {
+ components: {
+ DiffFileHeader,
+ DiffContent,
+ LoadingIcon,
+ },
+ props: {
+ file: {
+ type: Object,
+ required: true,
+ },
+ currentUser: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isActive: false,
+ isLoadingCollapsedDiff: false,
+ forkMessageVisible: false,
+ };
+ },
+ computed: {
+ isCollapsed() {
+ return this.file.collapsed || false;
+ },
+ viewBlobLink() {
+ return sprintf(
+ __('You can %{linkStart}view the blob%{linkEnd} instead.'),
+ {
+ linkStart: `<a href="${_.escape(this.file.viewPath)}">`,
+ linkEnd: '</a>',
+ },
+ false,
+ );
+ },
+ showExpandMessage() {
+ return this.isCollapsed && !this.isLoadingCollapsedDiff && !this.file.tooLarge;
+ },
+ },
+ mounted() {
+ document.addEventListener('scroll', this.handleScroll);
+ },
+ beforeDestroy() {
+ document.removeEventListener('scroll', this.handleScroll);
+ },
+ methods: {
+ ...mapActions('diffs', ['loadCollapsedDiff']),
+ handleToggle() {
+ const { collapsed, highlightedDiffLines, parallelDiffLines } = this.file;
+
+ if (collapsed && !highlightedDiffLines && !parallelDiffLines.length) {
+ this.handleLoadCollapsedDiff();
+ } else {
+ this.file.collapsed = !this.file.collapsed;
+ }
+ },
+ handleScroll() {
+ if (!this.updating) {
+ requestAnimationFrame(this.scrollUpdate.bind(this));
+ this.updating = true;
+ }
+ },
+ scrollUpdate() {
+ const header = document.querySelector('.js-diff-files-changed');
+ if (!header) {
+ this.updating = false;
+ return;
+ }
+
+ const { top, bottom } = this.$el.getBoundingClientRect();
+ const { top: topOfFixedHeader, bottom: bottomOfFixedHeader } = header.getBoundingClientRect();
+
+ const headerOverlapsContent = top < topOfFixedHeader && bottom > bottomOfFixedHeader;
+ const fullyAboveHeader = bottom < bottomOfFixedHeader;
+ const fullyBelowHeader = top > topOfFixedHeader;
+
+ if (headerOverlapsContent && !this.isActive) {
+ this.$emit('setActive');
+ this.isActive = true;
+ } else if (this.isActive && (fullyAboveHeader || fullyBelowHeader)) {
+ this.$emit('unsetActive');
+ this.isActive = false;
+ }
+
+ this.updating = false;
+ },
+ handleLoadCollapsedDiff() {
+ this.isLoadingCollapsedDiff = true;
+
+ this.loadCollapsedDiff(this.file)
+ .then(() => {
+ this.isLoadingCollapsedDiff = false;
+ this.file.collapsed = false;
+ })
+ .catch(() => {
+ this.isLoadingCollapsedDiff = false;
+ createFlash(__('Something went wrong on our end. Please try again!'));
+ });
+ },
+ showForkMessage() {
+ this.forkMessageVisible = true;
+ },
+ hideForkMessage() {
+ this.forkMessageVisible = false;
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ :id="file.fileHash"
+ class="diff-file file-holder"
+ >
+ <diff-file-header
+ :current-user="currentUser"
+ :diff-file="file"
+ :collapsible="true"
+ :expanded="!isCollapsed"
+ :add-merge-request-buttons="true"
+ class="js-file-title file-title"
+ @toggleFile="handleToggle"
+ @showForkMessage="showForkMessage"
+ />
+
+ <div
+ v-if="forkMessageVisible"
+ class="js-file-fork-suggestion-section file-fork-suggestion">
+ <span class="file-fork-suggestion-note">
+ You're not allowed to <span class="js-file-fork-suggestion-section-action">edit</span>
+ files in this project directly. Please fork this project,
+ make your changes there, and submit a merge request.
+ </span>
+ <a
+ :href="file.forkPath"
+ class="js-fork-suggestion-button btn btn-grouped btn-inverted btn-success"
+ >
+ Fork
+ </a>
+ <button
+ class="js-cancel-fork-suggestion-button btn btn-grouped"
+ type="button"
+ @click="hideForkMessage"
+ >
+ Cancel
+ </button>
+ </div>
+
+ <diff-content
+ v-if="!isCollapsed"
+ :class="{ hidden: isCollapsed || file.tooLarge }"
+ :diff-file="file"
+ />
+ <loading-icon
+ v-if="isLoadingCollapsedDiff"
+ class="diff-content loading"
+ />
+ <div
+ v-if="showExpandMessage"
+ class="nothing-here-block diff-collapsed"
+ >
+ {{ __('This diff is collapsed.') }}
+ <a
+ class="click-to-expand js-click-to-expand"
+ href="#"
+ @click.prevent="handleToggle"
+ >
+ {{ __('Click to expand it.') }}
+ </a>
+ </div>
+ <div
+ v-if="file.tooLarge"
+ class="nothing-here-block diff-collapsed js-too-large-diff"
+ >
+ {{ __('This source diff could not be displayed because it is too large.') }}
+ <span v-html="viewBlobLink"></span>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
new file mode 100644
index 00000000000..c5abd0a9568
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -0,0 +1,265 @@
+<script>
+import _ from 'underscore';
+import { mapActions, mapGetters } from 'vuex';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import FileIcon from '~/vue_shared/components/file_icon.vue';
+import Tooltip from '~/vue_shared/directives/tooltip';
+import { truncateSha } from '~/lib/utils/text_utility';
+import { __, s__, sprintf } from '~/locale';
+import EditButton from './edit_button.vue';
+
+export default {
+ components: {
+ ClipboardButton,
+ EditButton,
+ Icon,
+ FileIcon,
+ },
+ directives: {
+ Tooltip,
+ },
+ props: {
+ diffFile: {
+ type: Object,
+ required: true,
+ },
+ collapsible: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ addMergeRequestButtons: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ expanded: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ currentUser: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ blobForkSuggestion: null,
+ };
+ },
+ computed: {
+ ...mapGetters('diffs', ['diffHasExpandedDiscussions']),
+ hasExpandedDiscussions() {
+ return this.diffHasExpandedDiscussions(this.diffFile);
+ },
+ icon() {
+ if (this.diffFile.submodule) {
+ return 'archive';
+ }
+
+ return this.diffFile.blob.icon;
+ },
+ titleLink() {
+ if (this.diffFile.submodule) {
+ return this.diffFile.submoduleTreeUrl || this.diffFile.submoduleLink;
+ }
+
+ return `#${this.diffFile.fileHash}`;
+ },
+ filePath() {
+ if (this.diffFile.submodule) {
+ return `${this.diffFile.filePath} @ ${truncateSha(this.diffFile.blob.id)}`;
+ }
+
+ if (this.diffFile.deletedFile) {
+ return sprintf(__('%{filePath} deleted'), { filePath: this.diffFile.filePath }, false);
+ }
+
+ return this.diffFile.filePath;
+ },
+ titleTag() {
+ return this.diffFile.fileHash ? 'a' : 'span';
+ },
+ isUsingLfs() {
+ return this.diffFile.storedExternally && this.diffFile.externalStorage === 'lfs';
+ },
+ collapseIcon() {
+ return this.expanded ? 'chevron-down' : 'chevron-right';
+ },
+ viewFileButtonText() {
+ const truncatedContentSha = _.escape(truncateSha(this.diffFile.contentSha));
+ return sprintf(
+ s__('MergeRequests|View file @ %{commitId}'),
+ {
+ commitId: `<span class="commit-sha">${truncatedContentSha}</span>`,
+ },
+ false,
+ );
+ },
+ viewReplacedFileButtonText() {
+ const truncatedBaseSha = _.escape(truncateSha(this.diffFile.diffRefs.baseSha));
+ return sprintf(
+ s__('MergeRequests|View replaced file @ %{commitId}'),
+ {
+ commitId: `<span class="commit-sha">${truncatedBaseSha}</span>`,
+ },
+ false,
+ );
+ },
+ },
+ methods: {
+ ...mapActions('diffs', ['toggleFileDiscussions']),
+ handleToggleFile(e, checkTarget) {
+ if (
+ !checkTarget ||
+ e.target === this.$refs.header ||
+ (e.target.classList && e.target.classList.contains('diff-toggle-caret'))
+ ) {
+ this.$emit('toggleFile');
+ }
+ },
+ showForkMessage() {
+ this.$emit('showForkMessage');
+ },
+ handleToggleDiscussions() {
+ this.toggleFileDiscussions(this.diffFile);
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ ref="header"
+ class="js-file-title file-title file-title-flex-parent"
+ @click="handleToggleFile($event, true)"
+ >
+ <div class="file-header-content">
+ <icon
+ v-if="collapsible"
+ :name="collapseIcon"
+ :size="16"
+ aria-hidden="true"
+ class="diff-toggle-caret append-right-5"
+ @click.stop="handleToggle"
+ />
+ <a
+ v-once
+ ref="titleWrapper"
+ :href="titleLink"
+ class="append-right-4"
+ >
+ <file-icon
+ :file-name="filePath"
+ :size="18"
+ aria-hidden="true"
+ css-classes="js-file-icon append-right-5"
+ />
+ <span v-if="diffFile.renamedFile">
+ <strong
+ v-tooltip
+ :title="diffFile.oldPath"
+ class="file-title-name"
+ data-container="body"
+ >
+ {{ diffFile.oldPath }}
+ </strong>
+ →
+ <strong
+ v-tooltip
+ :title="diffFile.newPath"
+ class="file-title-name"
+ data-container="body"
+ >
+ {{ diffFile.newPath }}
+ </strong>
+ </span>
+
+ <strong
+ v-tooltip
+ v-else
+ :title="filePath"
+ class="file-title-name"
+ data-container="body"
+ >
+ {{ filePath }}
+ </strong>
+ </a>
+
+ <clipboard-button
+ :title="__('Copy file path to clipboard')"
+ :text="diffFile.filePath"
+ css-class="btn-default btn-transparent btn-clipboard"
+ />
+
+ <small
+ v-if="diffFile.modeChanged"
+ ref="fileMode"
+ >
+ {{ diffFile.aMode }} → {{ diffFile.bMode }}
+ </small>
+
+ <span
+ v-if="isUsingLfs"
+ class="label label-lfs append-right-5"
+ >
+ {{ __('LFS') }}
+ </span>
+ </div>
+
+ <div
+ v-if="!diffFile.submodule && addMergeRequestButtons"
+ class="file-actions d-none d-sm-block"
+ >
+ <template
+ v-if="diffFile.blob && diffFile.blob.readableText"
+ >
+ <button
+ :class="{ active: hasExpandedDiscussions }"
+ :title="s__('MergeRequests|Toggle comments for this file')"
+ class="js-btn-vue-toggle-comments btn"
+ type="button"
+ @click="handleToggleDiscussions"
+ >
+ <icon name="comment" />
+ </button>
+
+ <edit-button
+ v-if="!diffFile.deletedFile"
+ :current-user="currentUser"
+ :edit-path="diffFile.editPath"
+ :can-modify-blob="diffFile.canModifyBlob"
+ @showForkMessage="showForkMessage"
+ />
+ </template>
+
+ <a
+ v-if="diffFile.replacedViewPath"
+ :href="diffFile.replacedViewPath"
+ class="btn view-file js-view-file"
+ v-html="viewReplacedFileButtonText"
+ >
+ </a>
+ <a
+ :href="diffFile.viewPath"
+ class="btn view-file js-view-file"
+ v-html="viewFileButtonText"
+ >
+ </a>
+
+ <a
+ v-tooltip
+ v-if="diffFile.externalUrl"
+ :href="diffFile.externalUrl"
+ :title="`View on ${diffFile.formattedExternalUrl}`"
+ target="_blank"
+ rel="noopener noreferrer"
+ class="btn btn-file-option"
+ >
+ <icon name="external-link" />
+ </a>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
new file mode 100644
index 00000000000..7e50a0aed84
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
@@ -0,0 +1,105 @@
+<script>
+import { mapActions } from 'vuex';
+import Icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import { pluralize, truncate } from '~/lib/utils/text_utility';
+import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
+import { COUNT_OF_AVATARS_IN_GUTTER, LENGTH_OF_AVATAR_TOOLTIP } from '../constants';
+
+export default {
+ directives: {
+ tooltip,
+ },
+ components: {
+ Icon,
+ UserAvatarImage,
+ },
+ props: {
+ discussions: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ discussionsExpanded() {
+ return this.discussions.every(discussion => discussion.expanded);
+ },
+ allDiscussions() {
+ return this.discussions.reduce((acc, note) => acc.concat(note.notes), []);
+ },
+ notesInGutter() {
+ return this.allDiscussions.slice(0, COUNT_OF_AVATARS_IN_GUTTER).map(n => ({
+ note: n.note,
+ author: n.author,
+ }));
+ },
+ moreCount() {
+ return this.allDiscussions.length - this.notesInGutter.length;
+ },
+ moreText() {
+ if (this.moreCount === 0) {
+ return '';
+ }
+
+ return pluralize(`${this.moreCount} more comment`, this.moreCount);
+ },
+ },
+ methods: {
+ ...mapActions(['toggleDiscussion']),
+ getTooltipText(noteData) {
+ let { note } = noteData;
+
+ if (note.length > LENGTH_OF_AVATAR_TOOLTIP) {
+ note = truncate(note, LENGTH_OF_AVATAR_TOOLTIP);
+ }
+
+ return `${noteData.author.name}: ${note}`;
+ },
+ toggleDiscussions() {
+ this.discussions.forEach(discussion => {
+ this.toggleDiscussion({
+ discussionId: discussion.id,
+ });
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="diff-comment-avatar-holders">
+ <button
+ v-if="discussionsExpanded"
+ type="button"
+ aria-label="Show comments"
+ class="diff-notes-collapse js-diff-comment-avatar js-diff-comment-button"
+ @click="toggleDiscussions"
+ >
+ <icon
+ :size="12"
+ name="collapse"
+ />
+ </button>
+ <template v-else>
+ <user-avatar-image
+ v-for="note in notesInGutter"
+ :key="note.id"
+ :img-src="note.author.avatar_url"
+ :tooltip-text="getTooltipText(note)"
+ :size="19"
+ class="diff-comment-avatar js-diff-comment-avatar"
+ @click.native="toggleDiscussions"
+ />
+ <span
+ v-tooltip
+ v-if="moreText"
+ :title="moreText"
+ class="diff-comments-more-count has-tooltip js-diff-comment-avatar js-diff-comment-plus"
+ data-container="body"
+ data-placement="top"
+ role="button"
+ @click="toggleDiscussions"
+ >+{{ moreCount }}</span>
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
new file mode 100644
index 00000000000..ad838a32518
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
@@ -0,0 +1,203 @@
+<script>
+import createFlash from '~/flash';
+import { s__ } from '~/locale';
+import { mapState, mapGetters, mapActions } from 'vuex';
+import Icon from '~/vue_shared/components/icon.vue';
+import DiffGutterAvatars from './diff_gutter_avatars.vue';
+import { LINE_POSITION_RIGHT, UNFOLD_COUNT } from '../constants';
+import * as utils from '../store/utils';
+
+export default {
+ components: {
+ DiffGutterAvatars,
+ Icon,
+ },
+ props: {
+ fileHash: {
+ type: String,
+ required: true,
+ },
+ contextLinesPath: {
+ type: String,
+ required: true,
+ },
+ lineType: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ lineNumber: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ lineCode: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ linePosition: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ metaData: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ showCommentButton: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isBottom: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isMatchLine: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isMetaLine: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isContextLine: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ ...mapState({
+ diffViewType: state => state.diffs.diffViewType,
+ diffFiles: state => state.diffs.diffFiles,
+ }),
+ ...mapGetters(['isLoggedIn', 'discussionsByLineCode']),
+ lineHref() {
+ return this.lineCode ? `#${this.lineCode}` : '#';
+ },
+ shouldShowCommentButton() {
+ return (
+ this.isLoggedIn &&
+ this.showCommentButton &&
+ !this.isMatchLine &&
+ !this.isContextLine &&
+ !this.hasDiscussions &&
+ !this.isMetaLine
+ );
+ },
+ discussions() {
+ return this.discussionsByLineCode[this.lineCode] || [];
+ },
+ hasDiscussions() {
+ return this.discussions.length > 0;
+ },
+ shouldShowAvatarsOnGutter() {
+ let render = this.hasDiscussions && this.showCommentButton;
+
+ if (!this.lineType && this.linePosition === LINE_POSITION_RIGHT) {
+ render = false;
+ }
+
+ return render;
+ },
+ },
+ methods: {
+ ...mapActions('diffs', ['loadMoreLines', 'showCommentForm']),
+ handleCommentButton() {
+ this.showCommentForm({ lineCode: this.lineCode });
+ },
+ handleLoadMoreLines() {
+ if (this.isRequesting) {
+ return;
+ }
+
+ this.isRequesting = true;
+ const endpoint = this.contextLinesPath;
+ const oldLineNumber = this.metaData.oldPos || 0;
+ const newLineNumber = this.metaData.newPos || 0;
+ const offset = newLineNumber - oldLineNumber;
+ const bottom = this.isBottom;
+ const { fileHash } = this;
+ const view = this.diffViewType;
+ let unfold = true;
+ let lineNumber = newLineNumber - 1;
+ let since = lineNumber - UNFOLD_COUNT;
+ let to = lineNumber;
+
+ if (bottom) {
+ lineNumber = newLineNumber + 1;
+ since = lineNumber;
+ to = lineNumber + UNFOLD_COUNT;
+ } else {
+ const diffFile = utils.findDiffFile(this.diffFiles, this.fileHash);
+ const indexForInline = utils.findIndexInInlineLines(diffFile.highlightedDiffLines, {
+ oldLineNumber,
+ newLineNumber,
+ });
+ const prevLine = diffFile.highlightedDiffLines[indexForInline - 2];
+ const prevLineNumber = (prevLine && prevLine.newLine) || 0;
+
+ if (since <= prevLineNumber + 1) {
+ since = prevLineNumber + 1;
+ unfold = false;
+ }
+ }
+
+ const params = { since, to, bottom, offset, unfold, view };
+ const lineNumbers = { oldLineNumber, newLineNumber };
+ this.loadMoreLines({ endpoint, params, lineNumbers, fileHash })
+ .then(() => {
+ this.isRequesting = false;
+ })
+ .catch(() => {
+ createFlash(s__('Diffs|Something went wrong while fetching diff lines.'));
+ this.isRequesting = false;
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <span
+ v-if="isMatchLine"
+ class="context-cell"
+ role="button"
+ @click="handleLoadMoreLines"
+ >...</span>
+ <template
+ v-else
+ >
+ <button
+ v-show="shouldShowCommentButton"
+ type="button"
+ class="add-diff-note js-add-diff-note-button"
+ title="Add a comment to this line"
+ @click="handleCommentButton"
+ >
+ <icon
+ :size="12"
+ name="comment"
+ />
+ </button>
+ <a
+ v-if="lineNumber"
+ v-once
+ :data-linenumber="lineNumber"
+ :href="lineHref"
+ >
+ </a>
+ <diff-gutter-avatars
+ v-if="shouldShowAvatarsOnGutter"
+ :discussions="discussions"
+ />
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/diff_line_note_form.vue b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
new file mode 100644
index 00000000000..db380e68bd1
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
@@ -0,0 +1,115 @@
+<script>
+import $ from 'jquery';
+import { mapState, mapGetters, mapActions } from 'vuex';
+import createFlash from '~/flash';
+import { s__ } from '~/locale';
+import noteForm from '../../notes/components/note_form.vue';
+import { getNoteFormData } from '../store/utils';
+import Autosave from '../../autosave';
+import { DIFF_NOTE_TYPE, NOTE_TYPE } from '../constants';
+
+export default {
+ components: {
+ noteForm,
+ },
+ props: {
+ diffFile: {
+ type: Object,
+ required: true,
+ },
+ diffLines: {
+ type: Array,
+ required: true,
+ },
+ line: {
+ type: Object,
+ required: true,
+ },
+ position: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ noteTargetLine: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState({
+ noteableData: state => state.notes.noteableData,
+ diffViewType: state => state.diffs.diffViewType,
+ }),
+ ...mapGetters(['isLoggedIn', 'noteableType', 'getNoteableData', 'getNotesDataByProp']),
+ },
+ mounted() {
+ if (this.isLoggedIn) {
+ const noteableData = this.getNoteableData;
+ const keys = [
+ NOTE_TYPE,
+ this.noteableType,
+ noteableData.id,
+ noteableData.diff_head_sha,
+ DIFF_NOTE_TYPE,
+ noteableData.source_project_id,
+ this.line.lineCode,
+ ];
+
+ this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), keys);
+ }
+ },
+ methods: {
+ ...mapActions('diffs', ['cancelCommentForm']),
+ ...mapActions(['saveNote', 'refetchDiscussionById']),
+ handleCancelCommentForm() {
+ this.autosave.reset();
+ this.cancelCommentForm({
+ lineCode: this.line.lineCode,
+ });
+ },
+ handleSaveNote(note) {
+ const postData = getNoteFormData({
+ note,
+ noteableData: this.noteableData,
+ noteableType: this.noteableType,
+ noteTargetLine: this.noteTargetLine,
+ diffViewType: this.diffViewType,
+ diffFile: this.diffFile,
+ linePosition: this.position,
+ });
+
+ this.saveNote(postData)
+ .then(result => {
+ const endpoint = this.getNotesDataByProp('discussionsPath');
+
+ this.refetchDiscussionById({ path: endpoint, discussionId: result.discussion_id })
+ .then(() => {
+ this.handleCancelCommentForm();
+ })
+ .catch(() => {
+ createFlash(s__('MergeRequests|Updating discussions failed'));
+ });
+ })
+ .catch(() => {
+ createFlash(s__('MergeRequests|Saving the comment failed'));
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="content discussion-form discussion-form-container discussion-notes"
+ >
+ <note-form
+ ref="noteForm"
+ :is-editing="true"
+ :line-code="line.lineCode"
+ save-button-title="Comment"
+ class="diff-comment-form"
+ @cancelForm="handleCancelCommentForm"
+ @handleFormUpdate="handleSaveNote"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/diff_table_cell.vue b/app/assets/javascripts/diffs/components/diff_table_cell.vue
new file mode 100644
index 00000000000..bd02b45a63c
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/diff_table_cell.vue
@@ -0,0 +1,137 @@
+<script>
+import { mapGetters } from 'vuex';
+import DiffLineGutterContent from './diff_line_gutter_content.vue';
+import {
+ MATCH_LINE_TYPE,
+ CONTEXT_LINE_TYPE,
+ EMPTY_CELL_TYPE,
+ OLD_LINE_TYPE,
+ OLD_NO_NEW_LINE_TYPE,
+ NEW_NO_NEW_LINE_TYPE,
+ LINE_HOVER_CLASS_NAME,
+ LINE_UNFOLD_CLASS_NAME,
+ INLINE_DIFF_VIEW_TYPE,
+ LINE_POSITION_LEFT,
+ LINE_POSITION_RIGHT,
+} from '../constants';
+
+export default {
+ components: {
+ DiffLineGutterContent,
+ },
+ props: {
+ line: {
+ type: Object,
+ required: true,
+ },
+ diffFile: {
+ type: Object,
+ required: true,
+ },
+ diffViewType: {
+ type: String,
+ required: false,
+ default: INLINE_DIFF_VIEW_TYPE,
+ },
+ showCommentButton: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ linePosition: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ lineType: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ isContentLine: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isBottom: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isHover: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ ...mapGetters(['isLoggedIn']),
+ normalizedLine() {
+ let normalizedLine;
+
+ if (this.diffViewType === INLINE_DIFF_VIEW_TYPE) {
+ normalizedLine = this.line;
+ } else if (this.linePosition === LINE_POSITION_LEFT) {
+ normalizedLine = this.line.left;
+ } else if (this.linePosition === LINE_POSITION_RIGHT) {
+ normalizedLine = this.line.right;
+ }
+
+ return normalizedLine;
+ },
+ isMatchLine() {
+ return this.normalizedLine.type === MATCH_LINE_TYPE;
+ },
+ isContextLine() {
+ return this.normalizedLine.type === CONTEXT_LINE_TYPE;
+ },
+ isMetaLine() {
+ const { type } = this.normalizedLine;
+
+ return (
+ type === OLD_NO_NEW_LINE_TYPE || type === NEW_NO_NEW_LINE_TYPE || type === EMPTY_CELL_TYPE
+ );
+ },
+ classNameMap() {
+ const { type } = this.normalizedLine;
+
+ return {
+ [type]: type,
+ [LINE_UNFOLD_CLASS_NAME]: this.isMatchLine,
+ [LINE_HOVER_CLASS_NAME]:
+ this.isLoggedIn &&
+ this.isHover &&
+ !this.isMatchLine &&
+ !this.isContextLine &&
+ !this.isMetaLine,
+ };
+ },
+ lineNumber() {
+ const { lineType, normalizedLine } = this;
+
+ return lineType === OLD_LINE_TYPE ? normalizedLine.oldLine : normalizedLine.newLine;
+ },
+ },
+};
+</script>
+
+<template>
+ <td
+ :class="classNameMap"
+ >
+ <diff-line-gutter-content
+ :file-hash="diffFile.fileHash"
+ :line-type="normalizedLine.type"
+ :line-code="normalizedLine.lineCode"
+ :line-position="linePosition"
+ :line-number="lineNumber"
+ :meta-data="normalizedLine.metaData"
+ :show-comment-button="showCommentButton"
+ :context-lines-path="diffFile.contextLinesPath"
+ :is-bottom="isBottom"
+ :is-match-line="isMatchLine"
+ :is-context-line="isContentLine"
+ :is-meta-line="isMetaLine"
+ />
+ </td>
+</template>
diff --git a/app/assets/javascripts/diffs/components/edit_button.vue b/app/assets/javascripts/diffs/components/edit_button.vue
new file mode 100644
index 00000000000..ebf90631d76
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/edit_button.vue
@@ -0,0 +1,42 @@
+<script>
+export default {
+ props: {
+ editPath: {
+ type: String,
+ required: true,
+ },
+ currentUser: {
+ type: Object,
+ required: true,
+ },
+ canModifyBlob: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ methods: {
+ handleEditClick(evt) {
+ if (!this.currentUser || this.canModifyBlob) {
+ // if we can Edit, do default Edit button behavior
+ return;
+ }
+
+ if (this.currentUser.canFork && this.currentUser.canCreateMergeRequest) {
+ evt.preventDefault();
+ this.$emit('showForkMessage');
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <a
+ :href="editPath"
+ class="btn btn-default js-edit-blob"
+ @click="handleEditClick"
+ >
+ Edit
+ </a>
+</template>
diff --git a/app/assets/javascripts/diffs/components/hidden_files_warning.vue b/app/assets/javascripts/diffs/components/hidden_files_warning.vue
new file mode 100644
index 00000000000..017dcfcc357
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/hidden_files_warning.vue
@@ -0,0 +1,51 @@
+<script>
+export default {
+ props: {
+ total: {
+ type: String,
+ required: true,
+ },
+ visible: {
+ type: Number,
+ required: true,
+ },
+ plainDiffPath: {
+ type: String,
+ required: true,
+ },
+ emailPatchPath: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="alert alert-warning">
+ <h4>
+ {{ __('Too many changes to show.') }}
+ <div class="pull-right">
+ <a
+ :href="plainDiffPath"
+ class="btn btn-sm"
+ >
+ {{ __('Plain diff') }}
+ </a>
+ <a
+ :href="emailPatchPath"
+ class="btn btn-sm"
+ >
+ {{ __('Email patch') }}
+ </a>
+ </div>
+ </h4>
+ <p>
+ To preserve performance only
+ <strong>
+ {{ visible }} of {{ total }}
+ </strong>
+ files are displayed.
+ </p>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue
new file mode 100644
index 00000000000..1e8f2eecd76
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue
@@ -0,0 +1,69 @@
+<script>
+import { mapState, mapGetters } from 'vuex';
+import diffDiscussions from './diff_discussions.vue';
+import diffLineNoteForm from './diff_line_note_form.vue';
+
+export default {
+ components: {
+ diffDiscussions,
+ diffLineNoteForm,
+ },
+ props: {
+ line: {
+ type: Object,
+ required: true,
+ },
+ diffFile: {
+ type: Object,
+ required: true,
+ },
+ diffLines: {
+ type: Array,
+ required: true,
+ },
+ lineIndex: {
+ type: Number,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState({
+ diffLineCommentForms: state => state.diffs.diffLineCommentForms,
+ }),
+ ...mapGetters(['discussionsByLineCode']),
+ discussions() {
+ return this.discussionsByLineCode[this.line.lineCode] || [];
+ },
+ className() {
+ return this.discussions.length ? '' : 'js-temp-notes-holder';
+ },
+ },
+};
+</script>
+
+<template>
+ <tr
+ :class="className"
+ class="notes_holder"
+ >
+ <td
+ class="notes_line"
+ colspan="2"
+ ></td>
+ <td class="notes_content">
+ <div class="content">
+ <diff-discussions
+ v-if="discussions.length"
+ :discussions="discussions"
+ />
+ <diff-line-note-form
+ v-if="diffLineCommentForms[line.lineCode]"
+ :diff-file="diffFile"
+ :diff-lines="diffLines"
+ :line="line"
+ :note-target-line="diffLines[lineIndex]"
+ />
+ </div>
+ </td>
+ </tr>
+</template>
diff --git a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
new file mode 100644
index 00000000000..8e4715c9862
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
@@ -0,0 +1,105 @@
+<script>
+import { mapGetters } from 'vuex';
+import DiffTableCell from './diff_table_cell.vue';
+import {
+ NEW_LINE_TYPE,
+ OLD_LINE_TYPE,
+ CONTEXT_LINE_TYPE,
+ CONTEXT_LINE_CLASS_NAME,
+ PARALLEL_DIFF_VIEW_TYPE,
+ LINE_POSITION_LEFT,
+ LINE_POSITION_RIGHT,
+} from '../constants';
+
+export default {
+ components: {
+ DiffTableCell,
+ },
+ props: {
+ diffFile: {
+ type: Object,
+ required: true,
+ },
+ line: {
+ type: Object,
+ required: true,
+ },
+ isBottom: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ isHover: false,
+ };
+ },
+ computed: {
+ ...mapGetters('diffs', ['isInlineView']),
+ isContextLine() {
+ return this.line.type === CONTEXT_LINE_TYPE;
+ },
+ classNameMap() {
+ return {
+ [this.line.type]: this.line.type,
+ [CONTEXT_LINE_CLASS_NAME]: this.isContextLine,
+ [PARALLEL_DIFF_VIEW_TYPE]: this.isParallelView,
+ };
+ },
+ inlineRowId() {
+ const { lineCode, oldLine, newLine } = this.line;
+
+ return lineCode || `${this.diffFile.fileHash}_${oldLine}_${newLine}`;
+ },
+ },
+ created() {
+ this.newLineType = NEW_LINE_TYPE;
+ this.oldLineType = OLD_LINE_TYPE;
+ this.linePositionLeft = LINE_POSITION_LEFT;
+ this.linePositionRight = LINE_POSITION_RIGHT;
+ },
+ methods: {
+ handleMouseMove(e) {
+ // To show the comment icon on the gutter we need to know if we hover the line.
+ // Current table structure doesn't allow us to do this with CSS in both of the diff view types
+ this.isHover = e.type === 'mouseover';
+ },
+ },
+};
+</script>
+
+<template>
+ <tr
+ :id="inlineRowId"
+ :class="classNameMap"
+ class="line_holder"
+ @mouseover="handleMouseMove"
+ @mouseout="handleMouseMove"
+ >
+ <diff-table-cell
+ :diff-file="diffFile"
+ :line="line"
+ :line-type="oldLineType"
+ :is-bottom="isBottom"
+ :is-hover="isHover"
+ :show-comment-button="true"
+ class="diff-line-num old_line"
+ />
+ <diff-table-cell
+ :diff-file="diffFile"
+ :line="line"
+ :line-type="newLineType"
+ :is-bottom="isBottom"
+ :is-hover="isHover"
+ class="diff-line-num new_line"
+ />
+ <td
+ v-once
+ :class="line.type"
+ class="line_content"
+ v-html="line.richText"
+ >
+ </td>
+ </tr>
+</template>
diff --git a/app/assets/javascripts/diffs/components/inline_diff_view.vue b/app/assets/javascripts/diffs/components/inline_diff_view.vue
new file mode 100644
index 00000000000..9c1359f7c89
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/inline_diff_view.vue
@@ -0,0 +1,79 @@
+<script>
+import { mapGetters, mapState } from 'vuex';
+import inlineDiffTableRow from './inline_diff_table_row.vue';
+import inlineDiffCommentRow from './inline_diff_comment_row.vue';
+import { trimFirstCharOfLineContent } from '../store/utils';
+
+export default {
+ components: {
+ inlineDiffCommentRow,
+ inlineDiffTableRow,
+ },
+ props: {
+ diffFile: {
+ type: Object,
+ required: true,
+ },
+ diffLines: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapGetters('diffs', ['commitId']),
+ ...mapGetters(['discussionsByLineCode']),
+ ...mapState({
+ diffLineCommentForms: state => state.diffs.diffLineCommentForms,
+ }),
+ normalizedDiffLines() {
+ return this.diffLines.map(line => (line.richText ? trimFirstCharOfLineContent(line) : line));
+ },
+ diffLinesLength() {
+ return this.normalizedDiffLines.length;
+ },
+ userColorScheme() {
+ return window.gon.user_color_scheme;
+ },
+ },
+ methods: {
+ shouldRenderCommentRow(line) {
+ if (this.diffLineCommentForms[line.lineCode]) return true;
+
+ const lineDiscussions = this.discussionsByLineCode[line.lineCode];
+ if (lineDiscussions === undefined) {
+ return false;
+ }
+
+ return lineDiscussions.every(discussion => discussion.expanded);
+ },
+ },
+};
+</script>
+
+<template>
+ <table
+ :class="userColorScheme"
+ :data-commit-id="commitId"
+ class="code diff-wrap-lines js-syntax-highlight text-file js-diff-inline-view">
+ <tbody>
+ <template
+ v-for="(line, index) in normalizedDiffLines"
+ >
+ <inline-diff-table-row
+ :diff-file="diffFile"
+ :line="line"
+ :is-bottom="index + 1 === diffLinesLength"
+ :key="line.lineCode"
+ />
+ <inline-diff-comment-row
+ v-if="shouldRenderCommentRow(line)"
+ :diff-file="diffFile"
+ :diff-lines="normalizedDiffLines"
+ :line="line"
+ :line-index="index"
+ :key="index"
+ />
+ </template>
+ </tbody>
+ </table>
+</template>
diff --git a/app/assets/javascripts/diffs/components/no_changes.vue b/app/assets/javascripts/diffs/components/no_changes.vue
new file mode 100644
index 00000000000..d817157fbcd
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/no_changes.vue
@@ -0,0 +1,49 @@
+<script>
+import { mapState } from 'vuex';
+import emptyImage from '~/../../views/shared/icons/_mr_widget_empty_state.svg';
+
+export default {
+ data() {
+ return {
+ emptyImage,
+ };
+ },
+ computed: {
+ ...mapState({
+ sourceBranch: state => state.notes.noteableData.source_branch,
+ targetBranch: state => state.notes.noteableData.target_branch,
+ newBlobPath: state => state.notes.noteableData.new_blob_path,
+ }),
+ },
+};
+</script>
+
+<template>
+ <div
+ class="row empty-state nothing-here-block"
+ >
+ <div class="col-xs-12">
+ <div class="svg-content">
+ <span
+ v-html="emptyImage"
+ ></span>
+ </div>
+ </div>
+ <div class="col-xs-12">
+ <div class="text-content text-center">
+ No changes between
+ <span class="ref-name">{{ sourceBranch }}</span>
+ and
+ <span class="ref-name">{{ targetBranch }}</span>
+ <div class="text-center">
+ <a
+ :href="newBlobPath"
+ class="btn btn-save"
+ >
+ {{ __('Create commit') }}
+ </a>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue
new file mode 100644
index 00000000000..1e20792b647
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue
@@ -0,0 +1,123 @@
+<script>
+import { mapState, mapGetters } from 'vuex';
+import diffDiscussions from './diff_discussions.vue';
+import diffLineNoteForm from './diff_line_note_form.vue';
+
+export default {
+ components: {
+ diffDiscussions,
+ diffLineNoteForm,
+ },
+ props: {
+ line: {
+ type: Object,
+ required: true,
+ },
+ diffFile: {
+ type: Object,
+ required: true,
+ },
+ diffLines: {
+ type: Array,
+ required: true,
+ },
+ lineIndex: {
+ type: Number,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState({
+ diffLineCommentForms: state => state.diffs.diffLineCommentForms,
+ }),
+ ...mapGetters(['discussionsByLineCode']),
+ leftLineCode() {
+ return this.line.left.lineCode;
+ },
+ rightLineCode() {
+ return this.line.right.lineCode;
+ },
+ hasDiscussion() {
+ const discussions = this.discussionsByLineCode;
+
+ return discussions[this.leftLineCode] || discussions[this.rightLineCode];
+ },
+ hasExpandedDiscussionOnLeft() {
+ const discussions = this.discussionsByLineCode[this.leftLineCode];
+
+ return discussions ? discussions.every(discussion => discussion.expanded) : false;
+ },
+ hasExpandedDiscussionOnRight() {
+ const discussions = this.discussionsByLineCode[this.rightLineCode];
+
+ return discussions ? discussions.every(discussion => discussion.expanded) : false;
+ },
+ hasAnyExpandedDiscussion() {
+ return this.hasExpandedDiscussionOnLeft || this.hasExpandedDiscussionOnRight;
+ },
+ shouldRenderDiscussionsOnLeft() {
+ return this.discussionsByLineCode[this.leftLineCode] && this.hasExpandedDiscussionOnLeft;
+ },
+ shouldRenderDiscussionsOnRight() {
+ return (
+ this.discussionsByLineCode[this.rightLineCode] &&
+ this.hasExpandedDiscussionOnRight &&
+ this.line.right.type
+ );
+ },
+ className() {
+ return this.hasDiscussion ? '' : 'js-temp-notes-holder';
+ },
+ },
+};
+</script>
+
+<template>
+ <tr
+ :class="className"
+ class="notes_holder"
+ >
+ <td class="notes_line old"></td>
+ <td class="notes_content parallel old">
+ <div
+ v-if="shouldRenderDiscussionsOnLeft"
+ class="content"
+ >
+ <diff-discussions
+ v-if="discussionsByLineCode[leftLineCode].length"
+ :discussions="discussionsByLineCode[leftLineCode]"
+ />
+ </div>
+ <diff-line-note-form
+ v-if="diffLineCommentForms[leftLineCode] &&
+ diffLineCommentForms[leftLineCode]"
+ :diff-file="diffFile"
+ :diff-lines="diffLines"
+ :line="line.left"
+ :note-target-line="diffLines[lineIndex].left"
+ position="left"
+ />
+ </td>
+ <td class="notes_line new"></td>
+ <td class="notes_content parallel new">
+ <div
+ v-if="shouldRenderDiscussionsOnRight"
+ class="content"
+ >
+ <diff-discussions
+ v-if="discussionsByLineCode[rightLineCode].length"
+ :discussions="discussionsByLineCode[rightLineCode]"
+ />
+ </div>
+ <diff-line-note-form
+ v-if="diffLineCommentForms[rightLineCode] &&
+ diffLineCommentForms[rightLineCode] && line.right.type"
+ :diff-file="diffFile"
+ :diff-lines="diffLines"
+ :line="line.right"
+ :note-target-line="diffLines[lineIndex].right"
+ position="right"
+ />
+ </td>
+ </tr>
+</template>
diff --git a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue
new file mode 100644
index 00000000000..b76fc63205b
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue
@@ -0,0 +1,146 @@
+<script>
+import $ from 'jquery';
+import { mapGetters } from 'vuex';
+import DiffTableCell from './diff_table_cell.vue';
+import {
+ NEW_LINE_TYPE,
+ OLD_LINE_TYPE,
+ CONTEXT_LINE_TYPE,
+ CONTEXT_LINE_CLASS_NAME,
+ OLD_NO_NEW_LINE_TYPE,
+ PARALLEL_DIFF_VIEW_TYPE,
+ NEW_NO_NEW_LINE_TYPE,
+ LINE_POSITION_LEFT,
+ LINE_POSITION_RIGHT,
+} from '../constants';
+
+export default {
+ components: {
+ DiffTableCell,
+ },
+ props: {
+ diffFile: {
+ type: Object,
+ required: true,
+ },
+ line: {
+ type: Object,
+ required: true,
+ },
+ isBottom: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ isLeftHover: false,
+ isRightHover: false,
+ };
+ },
+ computed: {
+ ...mapGetters('diffs', ['isParallelView']),
+ isContextLine() {
+ return this.line.left.type === CONTEXT_LINE_TYPE;
+ },
+ classNameMap() {
+ return {
+ [CONTEXT_LINE_CLASS_NAME]: this.isContextLine,
+ [PARALLEL_DIFF_VIEW_TYPE]: this.isParallelView,
+ };
+ },
+ parallelViewLeftLineType() {
+ if (this.line.right.type === NEW_NO_NEW_LINE_TYPE) {
+ return OLD_NO_NEW_LINE_TYPE;
+ }
+
+ return this.line.left.type;
+ },
+ },
+ created() {
+ this.newLineType = NEW_LINE_TYPE;
+ this.oldLineType = OLD_LINE_TYPE;
+ this.linePositionLeft = LINE_POSITION_LEFT;
+ this.linePositionRight = LINE_POSITION_RIGHT;
+ this.parallelDiffViewType = PARALLEL_DIFF_VIEW_TYPE;
+ },
+ methods: {
+ handleMouseMove(e) {
+ const isHover = e.type === 'mouseover';
+ const hoveringCell = e.target.closest('td');
+ const allCellsInHoveringRow = Array.from(e.currentTarget.children);
+ const hoverIndex = allCellsInHoveringRow.indexOf(hoveringCell);
+
+ if (hoverIndex >= 2) {
+ this.isRightHover = isHover;
+ } else {
+ this.isLeftHover = isHover;
+ }
+ },
+ // Prevent text selecting on both sides of parallel diff view
+ // Backport of the same code from legacy diff notes.
+ handleParallelLineMouseDown(e) {
+ const line = $(e.currentTarget);
+ const table = line.closest('table');
+
+ table.removeClass('left-side-selected right-side-selected');
+ const [lineClass] = ['left-side', 'right-side'].filter(name => line.hasClass(name));
+
+ if (lineClass) {
+ table.addClass(`${lineClass}-selected`);
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <tr
+ :class="classNameMap"
+ class="line_holder"
+ @mouseover="handleMouseMove"
+ @mouseout="handleMouseMove"
+ >
+ <diff-table-cell
+ :diff-file="diffFile"
+ :line="line"
+ :line-type="oldLineType"
+ :line-position="linePositionLeft"
+ :is-bottom="isBottom"
+ :is-hover="isLeftHover"
+ :show-comment-button="true"
+ :diff-view-type="parallelDiffViewType"
+ class="diff-line-num old_line"
+ />
+ <td
+ v-once
+ :id="line.left.lineCode"
+ :class="parallelViewLeftLineType"
+ class="line_content parallel left-side"
+ @mousedown.native="handleParallelLineMouseDown"
+ v-html="line.left.richText"
+ >
+ </td>
+ <diff-table-cell
+ :diff-file="diffFile"
+ :line="line"
+ :line-type="newLineType"
+ :line-position="linePositionRight"
+ :is-bottom="isBottom"
+ :is-hover="isRightHover"
+ :show-comment-button="true"
+ :diff-view-type="parallelDiffViewType"
+ class="diff-line-num new_line"
+ />
+ <td
+ v-once
+ :id="line.right.lineCode"
+ :class="line.right.type"
+ class="line_content parallel right-side"
+ @mousedown.native="handleParallelLineMouseDown"
+ v-html="line.right.richText"
+ >
+ </td>
+ </tr>
+</template>
diff --git a/app/assets/javascripts/diffs/components/parallel_diff_view.vue b/app/assets/javascripts/diffs/components/parallel_diff_view.vue
new file mode 100644
index 00000000000..216865474a6
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/parallel_diff_view.vue
@@ -0,0 +1,113 @@
+<script>
+import { mapState, mapGetters } from 'vuex';
+import parallelDiffTableRow from './parallel_diff_table_row.vue';
+import parallelDiffCommentRow from './parallel_diff_comment_row.vue';
+import { EMPTY_CELL_TYPE } from '../constants';
+import { trimFirstCharOfLineContent } from '../store/utils';
+
+export default {
+ components: {
+ parallelDiffTableRow,
+ parallelDiffCommentRow,
+ },
+ props: {
+ diffFile: {
+ type: Object,
+ required: true,
+ },
+ diffLines: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapGetters('diffs', ['commitId']),
+ ...mapGetters(['discussionsByLineCode']),
+ ...mapState({
+ diffLineCommentForms: state => state.diffs.diffLineCommentForms,
+ }),
+ parallelDiffLines() {
+ return this.diffLines.map(line => {
+ const parallelLine = Object.assign({}, line);
+
+ if (line.left) {
+ parallelLine.left = trimFirstCharOfLineContent(line.left);
+ } else {
+ parallelLine.left = { type: EMPTY_CELL_TYPE };
+ }
+
+ if (line.right) {
+ parallelLine.right = trimFirstCharOfLineContent(line.right);
+ } else {
+ parallelLine.right = { type: EMPTY_CELL_TYPE };
+ }
+
+ return parallelLine;
+ });
+ },
+ diffLinesLength() {
+ return this.parallelDiffLines.length;
+ },
+ userColorScheme() {
+ return window.gon.user_color_scheme;
+ },
+ },
+ methods: {
+ shouldRenderCommentRow(line) {
+ const leftLineCode = line.left.lineCode;
+ const rightLineCode = line.right.lineCode;
+ const discussions = this.discussionsByLineCode;
+ const leftDiscussions = discussions[leftLineCode];
+ const rightDiscussions = discussions[rightLineCode];
+ const hasDiscussion = leftDiscussions || rightDiscussions;
+
+ const hasExpandedDiscussionOnLeft = leftDiscussions
+ ? leftDiscussions.every(discussion => discussion.expanded)
+ : false;
+ const hasExpandedDiscussionOnRight = rightDiscussions
+ ? rightDiscussions.every(discussion => discussion.expanded)
+ : false;
+
+ if (hasDiscussion && (hasExpandedDiscussionOnLeft || hasExpandedDiscussionOnRight)) {
+ return true;
+ }
+
+ const hasCommentFormOnLeft = this.diffLineCommentForms[leftLineCode];
+ const hasCommentFormOnRight = this.diffLineCommentForms[rightLineCode];
+
+ return hasCommentFormOnLeft || hasCommentFormOnRight;
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ :class="userColorScheme"
+ :data-commit-id="commitId"
+ class="code diff-wrap-lines js-syntax-highlight text-file"
+ >
+ <table>
+ <tbody>
+ <template
+ v-for="(line, index) in parallelDiffLines"
+ >
+ <parallel-diff-table-row
+ :diff-file="diffFile"
+ :line="line"
+ :is-bottom="index + 1 === diffLinesLength"
+ :key="index"
+ />
+ <parallel-diff-comment-row
+ v-if="shouldRenderCommentRow(line)"
+ :key="line.left.lineCode || line.right.lineCode"
+ :line="line"
+ :diff-file="diffFile"
+ :diff-lines="parallelDiffLines"
+ :line-index="index"
+ />
+ </template>
+ </tbody>
+ </table>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js
new file mode 100644
index 00000000000..2fa8367f528
--- /dev/null
+++ b/app/assets/javascripts/diffs/constants.js
@@ -0,0 +1,27 @@
+export const INLINE_DIFF_VIEW_TYPE = 'inline';
+export const PARALLEL_DIFF_VIEW_TYPE = 'parallel';
+export const MATCH_LINE_TYPE = 'match';
+export const OLD_NO_NEW_LINE_TYPE = 'old-nonewline';
+export const NEW_NO_NEW_LINE_TYPE = 'new-nonewline';
+export const CONTEXT_LINE_TYPE = 'context';
+export const EMPTY_CELL_TYPE = 'empty-cell';
+export const COMMENT_FORM_TYPE = 'commentForm';
+export const DIFF_NOTE_TYPE = 'DiffNote';
+export const NOTE_TYPE = 'Note';
+export const NEW_LINE_TYPE = 'new';
+export const OLD_LINE_TYPE = 'old';
+export const TEXT_DIFF_POSITION_TYPE = 'text';
+
+export const LINE_POSITION_LEFT = 'left';
+export const LINE_POSITION_RIGHT = 'right';
+export const LINE_SIDE_LEFT = 'left-side';
+export const LINE_SIDE_RIGHT = 'right-side';
+
+export const DIFF_VIEW_COOKIE_NAME = 'diff_view';
+export const LINE_HOVER_CLASS_NAME = 'is-over';
+export const LINE_UNFOLD_CLASS_NAME = 'unfold js-unfold';
+export const CONTEXT_LINE_CLASS_NAME = 'diff-expanded';
+
+export const UNFOLD_COUNT = 20;
+export const COUNT_OF_AVATARS_IN_GUTTER = 3;
+export const LENGTH_OF_AVATAR_TOOLTIP = 17;
diff --git a/app/assets/javascripts/diffs/index.js b/app/assets/javascripts/diffs/index.js
new file mode 100644
index 00000000000..aae89109c27
--- /dev/null
+++ b/app/assets/javascripts/diffs/index.js
@@ -0,0 +1,41 @@
+import Vue from 'vue';
+import { mapState } from 'vuex';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import diffsApp from './components/app.vue';
+
+export default function initDiffsApp(store) {
+ return new Vue({
+ el: '#js-diffs-app',
+ name: 'MergeRequestDiffs',
+ components: {
+ diffsApp,
+ },
+ store,
+ data() {
+ const { dataset } = document.querySelector(this.$options.el);
+
+ return {
+ endpoint: dataset.endpoint,
+ projectPath: dataset.projectPath,
+ currentUser: convertObjectPropsToCamelCase(JSON.parse(dataset.currentUserData), {
+ deep: true,
+ }),
+ };
+ },
+ computed: {
+ ...mapState({
+ activeTab: state => state.page.activeTab,
+ }),
+ },
+ render(createElement) {
+ return createElement('diffs-app', {
+ props: {
+ endpoint: this.endpoint,
+ currentUser: this.currentUser,
+ projectPath: this.projectPath,
+ shouldShow: this.activeTab === 'diffs',
+ },
+ });
+ },
+ });
+}
diff --git a/app/assets/javascripts/diffs/mixins/changed_files.js b/app/assets/javascripts/diffs/mixins/changed_files.js
new file mode 100644
index 00000000000..da1339f0ffa
--- /dev/null
+++ b/app/assets/javascripts/diffs/mixins/changed_files.js
@@ -0,0 +1,38 @@
+export default {
+ props: {
+ diffFiles: {
+ type: Array,
+ required: true,
+ },
+ },
+ methods: {
+ fileChangedIcon(diffFile) {
+ if (diffFile.deletedFile) {
+ return 'file-deletion';
+ } else if (diffFile.newFile) {
+ return 'file-addition';
+ }
+ return 'file-modified';
+ },
+ fileChangedClass(diffFile) {
+ if (diffFile.deletedFile) {
+ return 'cred';
+ } else if (diffFile.newFile) {
+ return 'cgreen';
+ }
+
+ return '';
+ },
+ truncatedDiffPath(path) {
+ const maxLength = 60;
+
+ if (path.length > maxLength) {
+ const start = path.length - maxLength;
+ const end = start + maxLength;
+ return `...${path.slice(start, end)}`;
+ }
+
+ return path;
+ },
+ },
+};
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
new file mode 100644
index 00000000000..27001142257
--- /dev/null
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -0,0 +1,113 @@
+import Vue from 'vue';
+import axios from '~/lib/utils/axios_utils';
+import Cookies from 'js-cookie';
+import { handleLocationHash, historyPushState } from '~/lib/utils/common_utils';
+import { mergeUrlParams } from '~/lib/utils/url_utility';
+import * as types from './mutation_types';
+import {
+ PARALLEL_DIFF_VIEW_TYPE,
+ INLINE_DIFF_VIEW_TYPE,
+ DIFF_VIEW_COOKIE_NAME,
+} from '../constants';
+
+export const setBaseConfig = ({ commit }, options) => {
+ const { endpoint, projectPath } = options;
+ commit(types.SET_BASE_CONFIG, { endpoint, projectPath });
+};
+
+export const fetchDiffFiles = ({ state, commit }) => {
+ commit(types.SET_LOADING, true);
+
+ return axios
+ .get(state.endpoint)
+ .then(res => {
+ commit(types.SET_LOADING, false);
+ commit(types.SET_MERGE_REQUEST_DIFFS, res.data.merge_request_diffs || []);
+ commit(types.SET_DIFF_DATA, res.data);
+ return Vue.nextTick();
+ })
+ .then(handleLocationHash);
+};
+
+export const setInlineDiffViewType = ({ commit }) => {
+ commit(types.SET_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE);
+
+ Cookies.set(DIFF_VIEW_COOKIE_NAME, INLINE_DIFF_VIEW_TYPE);
+ const url = mergeUrlParams({ view: INLINE_DIFF_VIEW_TYPE }, window.location.href);
+ historyPushState(url);
+};
+
+export const setParallelDiffViewType = ({ commit }) => {
+ commit(types.SET_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE);
+
+ Cookies.set(DIFF_VIEW_COOKIE_NAME, PARALLEL_DIFF_VIEW_TYPE);
+ const url = mergeUrlParams({ view: PARALLEL_DIFF_VIEW_TYPE }, window.location.href);
+ historyPushState(url);
+};
+
+export const showCommentForm = ({ commit }, params) => {
+ commit(types.ADD_COMMENT_FORM_LINE, params);
+};
+
+export const cancelCommentForm = ({ commit }, params) => {
+ commit(types.REMOVE_COMMENT_FORM_LINE, params);
+};
+
+export const loadMoreLines = ({ commit }, options) => {
+ const { endpoint, params, lineNumbers, fileHash } = options;
+
+ params.from_merge_request = true;
+
+ return axios.get(endpoint, { params }).then(res => {
+ const contextLines = res.data || [];
+
+ commit(types.ADD_CONTEXT_LINES, {
+ lineNumbers,
+ contextLines,
+ params,
+ fileHash,
+ });
+ });
+};
+
+export const loadCollapsedDiff = ({ commit }, file) =>
+ axios.get(file.loadCollapsedDiffUrl).then(res => {
+ commit(types.ADD_COLLAPSED_DIFFS, {
+ file,
+ data: res.data,
+ });
+ });
+
+export const expandAllFiles = ({ commit }) => {
+ commit(types.EXPAND_ALL_FILES);
+};
+
+/**
+ * Toggles the file discussions after user clicked on the toggle discussions button.
+ *
+ * Gets the discussions for the provided diff.
+ *
+ * If all discussions are expanded, it will collapse all of them
+ * If all discussions are collapsed, it will expand all of them
+ * If some discussions are open and others closed, it will expand the closed ones.
+ *
+ * @param {Object} diff
+ */
+export const toggleFileDiscussions = ({ getters, dispatch }, diff) => {
+ const discussions = getters.getDiffFileDiscussions(diff);
+ const shouldCloseAll = getters.diffHasAllExpandedDiscussions(diff);
+ const shouldExpandAll = getters.diffHasAllCollpasedDiscussions(diff);
+
+ discussions.forEach(discussion => {
+ const data = { discussionId: discussion.id };
+
+ if (shouldCloseAll) {
+ dispatch('collapseDiscussion', data, { root: true });
+ } else if (shouldExpandAll || (!shouldCloseAll && !shouldExpandAll && !discussion.expanded)) {
+ dispatch('expandDiscussion', data, { root: true });
+ }
+ });
+};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js
new file mode 100644
index 00000000000..f89acb73ed8
--- /dev/null
+++ b/app/assets/javascripts/diffs/store/getters.js
@@ -0,0 +1,60 @@
+import _ from 'underscore';
+import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '../constants';
+
+export const isParallelView = state => state.diffViewType === PARALLEL_DIFF_VIEW_TYPE;
+
+export const isInlineView = state => state.diffViewType === INLINE_DIFF_VIEW_TYPE;
+
+export const areAllFilesCollapsed = state => state.diffFiles.every(file => file.collapsed);
+
+export const commitId = state => (state.commit && state.commit.id ? state.commit.id : null);
+
+/**
+ * Checks if the diff has all discussions expanded
+ * @param {Object} diff
+ * @returns {Boolean}
+ */
+export const diffHasAllExpandedDiscussions = (state, getters) => diff => {
+ const discussions = getters.getDiffFileDiscussions(diff);
+
+ return (discussions.length && discussions.every(discussion => discussion.expanded)) || false;
+};
+
+/**
+ * Checks if the diff has all discussions collpased
+ * @param {Object} diff
+ * @returns {Boolean}
+ */
+export const diffHasAllCollpasedDiscussions = (state, getters) => diff => {
+ const discussions = getters.getDiffFileDiscussions(diff);
+
+ return (discussions.length && discussions.every(discussion => !discussion.expanded)) || false;
+};
+
+/**
+ * Checks if the diff has any open discussions
+ * @param {Object} diff
+ * @returns {Boolean}
+ */
+export const diffHasExpandedDiscussions = (state, getters) => diff => {
+ const discussions = getters.getDiffFileDiscussions(diff);
+
+ return (
+ (discussions.length && discussions.find(discussion => discussion.expanded) !== undefined) ||
+ false
+ );
+};
+
+/**
+ * Returns an array with the discussions of the given diff
+ * @param {Object} diff
+ * @returns {Array}
+ */
+export const getDiffFileDiscussions = (state, getters, rootState, rootGetters) => diff =>
+ rootGetters.discussions.filter(
+ discussion =>
+ discussion.diff_discussion && _.isEqual(discussion.diff_file.file_hash, diff.fileHash),
+ ) || [];
+
+// prevent babel-plugin-rewire from generating an invalid default during karma∂ tests
+export default () => {};
diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js
new file mode 100644
index 00000000000..39d90a64aab
--- /dev/null
+++ b/app/assets/javascripts/diffs/store/modules/diff_state.js
@@ -0,0 +1,18 @@
+import Cookies from 'js-cookie';
+import { getParameterValues } from '~/lib/utils/url_utility';
+import { INLINE_DIFF_VIEW_TYPE, DIFF_VIEW_COOKIE_NAME } from '../../constants';
+
+const viewTypeFromQueryString = getParameterValues('view')[0];
+const viewTypeFromCookie = Cookies.get(DIFF_VIEW_COOKIE_NAME);
+const defaultViewType = INLINE_DIFF_VIEW_TYPE;
+
+export default () => ({
+ isLoading: true,
+ endpoint: '',
+ basePath: '',
+ commit: null,
+ diffFiles: [],
+ mergeRequestDiffs: [],
+ diffLineCommentForms: {},
+ diffViewType: viewTypeFromQueryString || viewTypeFromCookie || defaultViewType,
+});
diff --git a/app/assets/javascripts/diffs/store/modules/index.js b/app/assets/javascripts/diffs/store/modules/index.js
new file mode 100644
index 00000000000..20d1ebbe049
--- /dev/null
+++ b/app/assets/javascripts/diffs/store/modules/index.js
@@ -0,0 +1,12 @@
+import * as actions from '../actions';
+import * as getters from '../getters';
+import mutations from '../mutations';
+import createState from './diff_state';
+
+export default {
+ namespaced: true,
+ state: createState(),
+ getters,
+ actions,
+ mutations,
+};
diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js
new file mode 100644
index 00000000000..2c8e1a1466f
--- /dev/null
+++ b/app/assets/javascripts/diffs/store/mutation_types.js
@@ -0,0 +1,10 @@
+export const SET_BASE_CONFIG = 'SET_BASE_CONFIG';
+export const SET_LOADING = 'SET_LOADING';
+export const SET_DIFF_DATA = 'SET_DIFF_DATA';
+export const SET_DIFF_VIEW_TYPE = 'SET_DIFF_VIEW_TYPE';
+export const SET_MERGE_REQUEST_DIFFS = 'SET_MERGE_REQUEST_DIFFS';
+export const ADD_COMMENT_FORM_LINE = 'ADD_COMMENT_FORM_LINE';
+export const REMOVE_COMMENT_FORM_LINE = 'REMOVE_COMMENT_FORM_LINE';
+export const ADD_CONTEXT_LINES = 'ADD_CONTEXT_LINES';
+export const ADD_COLLAPSED_DIFFS = 'ADD_COLLAPSED_DIFFS';
+export const EXPAND_ALL_FILES = 'EXPAND_ALL_FILES';
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
new file mode 100644
index 00000000000..a98b2be89a3
--- /dev/null
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -0,0 +1,75 @@
+import Vue from 'vue';
+import _ from 'underscore';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { findDiffFile, addLineReferences, removeMatchLine, addContextLines } from './utils';
+import * as types from './mutation_types';
+
+export default {
+ [types.SET_BASE_CONFIG](state, options) {
+ const { endpoint, projectPath } = options;
+ Object.assign(state, { endpoint, projectPath });
+ },
+
+ [types.SET_LOADING](state, isLoading) {
+ Object.assign(state, { isLoading });
+ },
+
+ [types.SET_DIFF_DATA](state, data) {
+ Object.assign(state, {
+ ...convertObjectPropsToCamelCase(data, { deep: true }),
+ });
+ },
+
+ [types.SET_MERGE_REQUEST_DIFFS](state, mergeRequestDiffs) {
+ Object.assign(state, {
+ mergeRequestDiffs: convertObjectPropsToCamelCase(mergeRequestDiffs, { deep: true }),
+ });
+ },
+
+ [types.SET_DIFF_VIEW_TYPE](state, diffViewType) {
+ Object.assign(state, { diffViewType });
+ },
+
+ [types.ADD_COMMENT_FORM_LINE](state, { lineCode }) {
+ Vue.set(state.diffLineCommentForms, lineCode, true);
+ },
+
+ [types.REMOVE_COMMENT_FORM_LINE](state, { lineCode }) {
+ Vue.delete(state.diffLineCommentForms, lineCode);
+ },
+
+ [types.ADD_CONTEXT_LINES](state, options) {
+ const { lineNumbers, contextLines, fileHash } = options;
+ const { bottom } = options.params;
+ const diffFile = findDiffFile(state.diffFiles, fileHash);
+ const { highlightedDiffLines, parallelDiffLines } = diffFile;
+
+ removeMatchLine(diffFile, lineNumbers, bottom);
+ const lines = addLineReferences(contextLines, lineNumbers, bottom);
+ addContextLines({
+ inlineLines: highlightedDiffLines,
+ parallelLines: parallelDiffLines,
+ contextLines: lines,
+ bottom,
+ lineNumbers,
+ });
+ },
+
+ [types.ADD_COLLAPSED_DIFFS](state, { file, data }) {
+ const normalizedData = convertObjectPropsToCamelCase(data, { deep: true });
+ const [newFileData] = normalizedData.diffFiles.filter(f => f.fileHash === file.fileHash);
+
+ if (newFileData) {
+ const index = _.findIndex(state.diffFiles, f => f.fileHash === file.fileHash);
+ state.diffFiles.splice(index, 1, newFileData);
+ }
+ },
+
+ [types.EXPAND_ALL_FILES](state) {
+ // eslint-disable-next-line no-param-reassign
+ state.diffFiles = state.diffFiles.map(file => ({
+ ...file,
+ collapsed: false,
+ }));
+ },
+};
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
new file mode 100644
index 00000000000..d9589baa76e
--- /dev/null
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -0,0 +1,175 @@
+import _ from 'underscore';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import {
+ LINE_POSITION_LEFT,
+ LINE_POSITION_RIGHT,
+ TEXT_DIFF_POSITION_TYPE,
+ DIFF_NOTE_TYPE,
+ NEW_LINE_TYPE,
+ OLD_LINE_TYPE,
+ MATCH_LINE_TYPE,
+} from '../constants';
+
+export function findDiffFile(files, hash) {
+ return files.filter(file => file.fileHash === hash)[0];
+}
+
+export const getReversePosition = linePosition => {
+ if (linePosition === LINE_POSITION_RIGHT) {
+ return LINE_POSITION_LEFT;
+ }
+
+ return LINE_POSITION_RIGHT;
+};
+
+export function getNoteFormData(params) {
+ const {
+ note,
+ noteableType,
+ noteableData,
+ diffFile,
+ noteTargetLine,
+ diffViewType,
+ linePosition,
+ } = params;
+
+ const position = JSON.stringify({
+ base_sha: diffFile.diffRefs.baseSha,
+ start_sha: diffFile.diffRefs.startSha,
+ head_sha: diffFile.diffRefs.headSha,
+ old_path: diffFile.oldPath,
+ new_path: diffFile.newPath,
+ position_type: TEXT_DIFF_POSITION_TYPE,
+ old_line: noteTargetLine.oldLine,
+ new_line: noteTargetLine.newLine,
+ });
+
+ const postData = {
+ view: diffViewType,
+ line_type: linePosition === LINE_POSITION_RIGHT ? NEW_LINE_TYPE : OLD_LINE_TYPE,
+ merge_request_diff_head_sha: diffFile.diffRefs.headSha,
+ in_reply_to_discussion_id: '',
+ note_project_id: '',
+ target_type: noteableData.targetType,
+ target_id: noteableData.id,
+ note: {
+ note,
+ position,
+ noteable_type: noteableType,
+ noteable_id: noteableData.id,
+ commit_id: '',
+ type: DIFF_NOTE_TYPE,
+ line_code: noteTargetLine.lineCode,
+ },
+ };
+
+ return {
+ endpoint: noteableData.create_note_path,
+ data: postData,
+ };
+}
+
+export const findIndexInInlineLines = (lines, lineNumbers) => {
+ const { oldLineNumber, newLineNumber } = lineNumbers;
+
+ return _.findIndex(
+ lines,
+ line => line.oldLine === oldLineNumber && line.newLine === newLineNumber,
+ );
+};
+
+export const findIndexInParallelLines = (lines, lineNumbers) => {
+ const { oldLineNumber, newLineNumber } = lineNumbers;
+
+ return _.findIndex(
+ lines,
+ line =>
+ line.left &&
+ line.right &&
+ line.left.oldLine === oldLineNumber &&
+ line.right.newLine === newLineNumber,
+ );
+};
+
+export function removeMatchLine(diffFile, lineNumbers, bottom) {
+ const indexForInline = findIndexInInlineLines(diffFile.highlightedDiffLines, lineNumbers);
+ const indexForParallel = findIndexInParallelLines(diffFile.parallelDiffLines, lineNumbers);
+ const factor = bottom ? 1 : -1;
+
+ diffFile.highlightedDiffLines.splice(indexForInline + factor, 1);
+ diffFile.parallelDiffLines.splice(indexForParallel + factor, 1);
+}
+
+export function addLineReferences(lines, lineNumbers, bottom) {
+ const { oldLineNumber, newLineNumber } = lineNumbers;
+ const lineCount = lines.length;
+ let matchLineIndex = -1;
+
+ const linesWithNumbers = lines.map((l, index) => {
+ const line = convertObjectPropsToCamelCase(l);
+
+ if (line.type === MATCH_LINE_TYPE) {
+ matchLineIndex = index;
+ } else {
+ Object.assign(line, {
+ oldLine: bottom ? oldLineNumber + index + 1 : oldLineNumber + index - lineCount,
+ newLine: bottom ? newLineNumber + index + 1 : newLineNumber + index - lineCount,
+ });
+ }
+
+ return line;
+ });
+
+ if (matchLineIndex > -1) {
+ const line = linesWithNumbers[matchLineIndex];
+ const targetLine = bottom
+ ? linesWithNumbers[matchLineIndex - 1]
+ : linesWithNumbers[matchLineIndex + 1];
+
+ Object.assign(line, {
+ metaData: {
+ oldPos: targetLine.oldLine,
+ newPos: targetLine.newLine,
+ },
+ });
+ }
+
+ return linesWithNumbers;
+}
+
+export function addContextLines(options) {
+ const { inlineLines, parallelLines, contextLines, lineNumbers } = options;
+ const normalizedParallelLines = contextLines.map(line => ({
+ left: line,
+ right: line,
+ }));
+
+ if (options.bottom) {
+ inlineLines.push(...contextLines);
+ parallelLines.push(...normalizedParallelLines);
+ } else {
+ const inlineIndex = findIndexInInlineLines(inlineLines, lineNumbers);
+ const parallelIndex = findIndexInParallelLines(parallelLines, lineNumbers);
+ inlineLines.splice(inlineIndex, 0, ...contextLines);
+ parallelLines.splice(parallelIndex, 0, ...normalizedParallelLines);
+ }
+}
+
+/**
+ * Trims the first char of the `richText` property when it's either a space or a diff symbol.
+ * @param {Object} line
+ * @returns {Object}
+ */
+export function trimFirstCharOfLineContent(line = {}) {
+ const parsedLine = Object.assign({}, line);
+
+ if (line.richText) {
+ const firstChar = parsedLine.richText.charAt(0);
+
+ if (firstChar === ' ' || firstChar === '+' || firstChar === '-') {
+ parsedLine.richText = line.richText.substring(1);
+ }
+ }
+
+ return parsedLine;
+}
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 72f21f13860..a5af37e80b6 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -1,12 +1,12 @@
-/* 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 */
+/* eslint-disable consistent-return, no-new */
import $ from 'jquery';
-import Flash from './flash';
import GfmAutoComplete from './gfm_auto_complete';
import { convertPermissionToBoolean } from './lib/utils/common_utils';
import GlFieldErrors from './gl_field_errors';
import Shortcuts from './shortcuts';
import SearchAutocomplete from './search_autocomplete';
+import performanceBar from './performance_bar';
function initSearch() {
// Only when search form is present
@@ -72,9 +72,7 @@ function initGFMInput() {
function initPerformanceBar() {
if (document.querySelector('#js-peek')) {
- import('./performance_bar')
- .then(m => new m.default({ container: '#js-peek' })) // eslint-disable-line new-cap
- .catch(() => Flash('Error loading performance bar module'));
+ performanceBar({ container: '#js-peek' });
}
}
diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js
index 4164149dd06..8abd8bc581a 100644
--- a/app/assets/javascripts/due_date_select.js
+++ b/app/assets/javascripts/due_date_select.js
@@ -1,7 +1,6 @@
-/* global dateFormat */
-
import $ from 'jquery';
import Pikaday from 'pikaday';
+import dateFormat from 'dateformat';
import { __ } from '~/locale';
import axios from './lib/utils/axios_utils';
import { timeFor } from './lib/utils/datetime_utility';
@@ -55,7 +54,7 @@ class DueDateSelect {
format: 'yyyy-mm-dd',
parse: dateString => parsePikadayDate(dateString),
toString: date => pikadayToString(date),
- onSelect: (dateText) => {
+ onSelect: dateText => {
$dueDateInput.val(calendar.toString(dateText));
if (this.$dropdown.hasClass('js-issue-boards-due-date')) {
@@ -73,7 +72,7 @@ class DueDateSelect {
}
initRemoveDueDate() {
- this.$block.on('click', '.js-remove-due-date', (e) => {
+ this.$block.on('click', '.js-remove-due-date', e => {
const calendar = this.$datePicker.data('pikaday');
e.preventDefault();
@@ -124,7 +123,8 @@ class DueDateSelect {
this.$loading.fadeOut();
};
- gl.issueBoards.BoardsStore.detail.issue.update(this.$dropdown.attr('data-issue-update'))
+ gl.issueBoards.BoardsStore.detail.issue
+ .update(this.$dropdown.attr('data-issue-update'))
.then(fadeOutLoader)
.catch(fadeOutLoader);
}
@@ -147,17 +147,18 @@ class DueDateSelect {
$('.js-remove-due-date-holder').toggleClass('hidden', selectedDateValue.length);
- return axios.put(this.issueUpdateURL, this.datePayload)
- .then(() => {
- const tooltipText = hasDueDate ? `${__('Due date')}<br />${selectedDateValue} (${timeFor(selectedDateValue)})` : __('Due date');
- if (isDropdown) {
- this.$dropdown.trigger('loaded.gl.dropdown');
- this.$dropdown.dropdown('toggle');
- }
- this.$sidebarCollapsedValue.attr('data-original-title', tooltipText);
+ return axios.put(this.issueUpdateURL, this.datePayload).then(() => {
+ const tooltipText = hasDueDate
+ ? `${__('Due date')}<br />${selectedDateValue} (${timeFor(selectedDateValue)})`
+ : __('Due date');
+ if (isDropdown) {
+ this.$dropdown.trigger('loaded.gl.dropdown');
+ this.$dropdown.dropdown('toggle');
+ }
+ this.$sidebarCollapsedValue.attr('data-original-title', tooltipText);
- return this.$loading.fadeOut();
- });
+ return this.$loading.fadeOut();
+ });
}
}
@@ -170,6 +171,8 @@ export default class DueDateSelectors {
initMilestoneDatePicker() {
$('.datepicker').each(function initPikadayMilestone() {
const $datePicker = $(this);
+ const datePickerVal = $datePicker.val();
+
const calendar = new Pikaday({
field: $datePicker.get(0),
theme: 'gitlab-theme animate-picker',
@@ -182,20 +185,24 @@ export default class DueDateSelectors {
},
});
- calendar.setDate(parsePikadayDate($datePicker.val()));
+ calendar.setDate(parsePikadayDate(datePickerVal));
$datePicker.data('pikaday', calendar);
});
- $('.js-clear-due-date,.js-clear-start-date').on('click', (e) => {
+ $('.js-clear-due-date,.js-clear-start-date').on('click', e => {
e.preventDefault();
- const calendar = $(e.target).siblings('.datepicker').data('pikaday');
+ const calendar = $(e.target)
+ .siblings('.datepicker')
+ .data('pikaday');
calendar.setDate(null);
});
}
// eslint-disable-next-line class-methods-use-this
initIssuableSelect() {
- const $loading = $('.js-issuable-update .due_date').find('.block-loading').hide();
+ const $loading = $('.js-issuable-update .due_date')
+ .find('.block-loading')
+ .hide();
$('.js-due-date-select').each((i, dropdown) => {
const $dropdown = $(dropdown);
diff --git a/app/assets/javascripts/environments/components/container.vue b/app/assets/javascripts/environments/components/container.vue
index 6bd7c6b49cb..9aa224fa407 100644
--- a/app/assets/javascripts/environments/components/container.vue
+++ b/app/assets/javascripts/environments/components/container.vue
@@ -43,17 +43,17 @@
<div class="environments-container">
<loading-icon
+ v-if="isLoading"
class="prepend-top-default"
label="Loading environments"
- v-if="isLoading"
size="3"
/>
<slot name="emptyState"></slot>
<div
- class="table-holder"
- v-if="!isLoading && environments.length > 0">
+ v-if="!isLoading && environments.length > 0"
+ class="table-holder">
<environment-table
:environments="environments"
diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue
index ab9e22037d0..63d83e307ee 100644
--- a/app/assets/javascripts/environments/components/environment_actions.vue
+++ b/app/assets/javascripts/environments/components/environment_actions.vue
@@ -1,50 +1,50 @@
<script>
- import Icon from '~/vue_shared/components/icon.vue';
- import eventHub from '../event_hub';
- import loadingIcon from '../../vue_shared/components/loading_icon.vue';
- import tooltip from '../../vue_shared/directives/tooltip';
+import Icon from '~/vue_shared/components/icon.vue';
+import eventHub from '../event_hub';
+import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+import tooltip from '../../vue_shared/directives/tooltip';
- export default {
- directives: {
- tooltip,
+export default {
+ directives: {
+ tooltip,
+ },
+ components: {
+ loadingIcon,
+ Icon,
+ },
+ props: {
+ actions: {
+ type: Array,
+ required: false,
+ default: () => [],
},
- components: {
- loadingIcon,
- Icon,
+ },
+ data() {
+ return {
+ isLoading: false,
+ };
+ },
+ computed: {
+ title() {
+ return 'Deploy to...';
},
- props: {
- actions: {
- type: Array,
- required: false,
- default: () => [],
- },
- },
- data() {
- return {
- isLoading: false,
- };
- },
- computed: {
- title() {
- return 'Deploy to...';
- },
- },
- methods: {
- onClickAction(endpoint) {
- this.isLoading = true;
+ },
+ methods: {
+ onClickAction(endpoint) {
+ this.isLoading = true;
- eventHub.$emit('postAction', endpoint);
- },
+ eventHub.$emit('postAction', { endpoint });
+ },
- isActionDisabled(action) {
- if (action.playable === undefined) {
- return false;
- }
+ isActionDisabled(action) {
+ if (action.playable === undefined) {
+ return false;
+ }
- return !action.playable;
- },
+ return !action.playable;
},
- };
+ },
+};
</script>
<template>
<div
@@ -52,19 +52,16 @@
role="group">
<button
v-tooltip
+ :title="title"
+ :aria-label="title"
+ :disabled="isLoading"
type="button"
class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container"
data-container="body"
data-toggle="dropdown"
- :title="title"
- :aria-label="title"
- :disabled="isLoading"
>
<span>
- <icon
- name="play"
- :size="12"
- />
+ <icon name="play" />
<i
class="fa fa-caret-down"
aria-hidden="true"
@@ -74,21 +71,17 @@
</span>
</button>
- <ul class="dropdown-menu dropdown-menu-align-right">
+ <ul class="dropdown-menu dropdown-menu-right">
<li
v-for="(action, i) in actions"
:key="i">
<button
+ :class="{ disabled: isActionDisabled(action) }"
+ :disabled="isActionDisabled(action)"
type="button"
class="js-manual-action-link no-btn btn"
@click="onClickAction(action.play_path)"
- :class="{ disabled: isActionDisabled(action) }"
- :disabled="isActionDisabled(action)"
>
- <icon
- name="play"
- :size="12"
- />
<span>
{{ action.name }}
</span>
diff --git a/app/assets/javascripts/environments/components/environment_external_url.vue b/app/assets/javascripts/environments/components/environment_external_url.vue
index ea6f1168c68..7446196de13 100644
--- a/app/assets/javascripts/environments/components/environment_external_url.vue
+++ b/app/assets/javascripts/environments/components/environment_external_url.vue
@@ -1,45 +1,42 @@
<script>
- import Icon from '~/vue_shared/components/icon.vue';
- import tooltip from '../../vue_shared/directives/tooltip';
- import { s__ } from '../../locale';
+import Icon from '~/vue_shared/components/icon.vue';
+import tooltip from '../../vue_shared/directives/tooltip';
+import { s__ } from '../../locale';
- /**
- * Renders the external url link in environments table.
- */
- export default {
- components: {
- Icon,
+/**
+ * Renders the external url link in environments table.
+ */
+export default {
+ components: {
+ Icon,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ externalUrl: {
+ type: String,
+ required: true,
},
- directives: {
- tooltip,
+ },
+ computed: {
+ title() {
+ return s__('Environments|Open live environment');
},
- props: {
- externalUrl: {
- type: String,
- required: true,
- },
- },
- computed: {
- title() {
- return s__('Environments|Open');
- },
- },
- };
+ },
+};
</script>
<template>
<a
v-tooltip
+ :title="title"
+ :aria-label="title"
+ :href="externalUrl"
class="btn external-url"
data-container="body"
target="_blank"
rel="noopener noreferrer nofollow"
- :title="title"
- :aria-label="title"
- :href="externalUrl"
>
- <icon
- name="external-link"
- :size="12"
- />
+ <icon name="external-link" />
</a>
</template>
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index 79326ca3487..39f3790a286 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -1,437 +1,458 @@
<script>
- import Timeago from 'timeago.js';
- import _ from 'underscore';
- import tooltip from '~/vue_shared/directives/tooltip';
- import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
- 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';
- import RollbackComponent from './environment_rollback.vue';
- import TerminalButtonComponent from './environment_terminal_button.vue';
- import MonitoringButtonComponent from './environment_monitoring.vue';
- import CommitComponent from '../../vue_shared/components/commit.vue';
- import eventHub from '../event_hub';
-
- /**
- * Envrionment Item Component
- *
- * Renders a table row for each environment.
- */
- const timeagoInstance = new Timeago();
-
- export default {
- components: {
- UserAvatarLink,
- CommitComponent,
- ActionsComponent,
- ExternalUrlComponent,
- StopComponent,
- RollbackComponent,
- TerminalButtonComponent,
- MonitoringButtonComponent,
+import Timeago from 'timeago.js';
+import _ from 'underscore';
+import tooltip from '~/vue_shared/directives/tooltip';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
+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';
+import RollbackComponent from './environment_rollback.vue';
+import TerminalButtonComponent from './environment_terminal_button.vue';
+import MonitoringButtonComponent from './environment_monitoring.vue';
+import CommitComponent from '../../vue_shared/components/commit.vue';
+import eventHub from '../event_hub';
+
+/**
+ * Envrionment Item Component
+ *
+ * Renders a table row for each environment.
+ */
+const timeagoInstance = new Timeago();
+
+export default {
+ components: {
+ UserAvatarLink,
+ CommitComponent,
+ ActionsComponent,
+ ExternalUrlComponent,
+ StopComponent,
+ RollbackComponent,
+ TerminalButtonComponent,
+ MonitoringButtonComponent,
+ },
+
+ directives: {
+ tooltip,
+ },
+
+ props: {
+ model: {
+ type: Object,
+ required: true,
+ default: () => ({}),
},
- directives: {
- tooltip,
+ canCreateDeployment: {
+ type: Boolean,
+ required: false,
+ default: false,
},
- props: {
- model: {
- type: Object,
- required: true,
- default: () => ({}),
- },
-
- canCreateDeployment: {
- type: Boolean,
- required: false,
- default: false,
- },
-
- canReadEnvironment: {
- type: Boolean,
- required: false,
- default: false,
- },
+ canReadEnvironment: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+
+ computed: {
+ /**
+ * Verifies if `last_deployment` key exists in the current Envrionment.
+ * This key is required to render most of the html - this method works has
+ * an helper.
+ *
+ * @returns {Boolean}
+ */
+ hasLastDeploymentKey() {
+ if (this.model && this.model.last_deployment && !_.isEmpty(this.model.last_deployment)) {
+ return true;
+ }
+ return false;
+ },
+
+ /**
+ * Verifies is the given environment has manual actions.
+ * Used to verify if we should render them or nor.
+ *
+ * @returns {Boolean|Undefined}
+ */
+ hasManualActions() {
+ return (
+ this.model &&
+ this.model.last_deployment &&
+ this.model.last_deployment.manual_actions &&
+ this.model.last_deployment.manual_actions.length > 0
+ );
+ },
+
+ /**
+ * Returns whether the environment can be stopped.
+ *
+ * @returns {Boolean}
+ */
+ canStopEnvironment() {
+ return this.model && this.model.can_stop;
+ },
+
+ /**
+ * Verifies if the `deployable` key is present in `last_deployment` key.
+ * Used to verify whether we should or not render the rollback partial.
+ *
+ * @returns {Boolean|Undefined}
+ */
+ canRetry() {
+ return (
+ this.model &&
+ this.hasLastDeploymentKey &&
+ this.model.last_deployment &&
+ this.model.last_deployment.deployable
+ );
+ },
+
+ /**
+ * Verifies if the date to be shown is present.
+ *
+ * @returns {Boolean|Undefined}
+ */
+ canShowDate() {
+ return (
+ this.model &&
+ this.model.last_deployment &&
+ this.model.last_deployment.deployable &&
+ this.model.last_deployment.deployable !== undefined
+ );
+ },
+
+ /**
+ * Human readable date.
+ *
+ * @returns {String}
+ */
+ createdDate() {
+ if (
+ this.model &&
+ this.model.last_deployment &&
+ this.model.last_deployment.deployable &&
+ this.model.last_deployment.deployable.created_at
+ ) {
+ return timeagoInstance.format(this.model.last_deployment.deployable.created_at);
+ }
+ return '';
+ },
+
+ /**
+ * Returns the manual actions with the name parsed.
+ *
+ * @returns {Array.<Object>|Undefined}
+ */
+ manualActions() {
+ if (this.hasManualActions) {
+ return this.model.last_deployment.manual_actions.map(action => {
+ const parsedAction = {
+ name: humanize(action.name),
+ play_path: action.play_path,
+ playable: action.playable,
+ };
+ return parsedAction;
+ });
+ }
+ return [];
+ },
+
+ /**
+ * Builds the string used in the user image alt attribute.
+ *
+ * @returns {String}
+ */
+ userImageAltDescription() {
+ if (
+ this.model &&
+ this.model.last_deployment &&
+ this.model.last_deployment.user &&
+ this.model.last_deployment.user.username
+ ) {
+ return `${this.model.last_deployment.user.username}'s avatar'`;
+ }
+ return '';
+ },
+
+ /**
+ * If provided, returns the commit tag.
+ *
+ * @returns {String|Undefined}
+ */
+ commitTag() {
+ if (this.model && this.model.last_deployment && this.model.last_deployment.tag) {
+ return this.model.last_deployment.tag;
+ }
+ return undefined;
+ },
+
+ /**
+ * If provided, returns the commit ref.
+ *
+ * @returns {Object|Undefined}
+ */
+ commitRef() {
+ if (this.model && this.model.last_deployment && this.model.last_deployment.ref) {
+ return this.model.last_deployment.ref;
+ }
+ return undefined;
+ },
+
+ /**
+ * If provided, returns the commit url.
+ *
+ * @returns {String|Undefined}
+ */
+ commitUrl() {
+ if (
+ this.model &&
+ this.model.last_deployment &&
+ this.model.last_deployment.commit &&
+ this.model.last_deployment.commit.commit_path
+ ) {
+ return this.model.last_deployment.commit.commit_path;
+ }
+ return undefined;
+ },
+
+ /**
+ * If provided, returns the commit short sha.
+ *
+ * @returns {String|Undefined}
+ */
+ commitShortSha() {
+ if (
+ this.model &&
+ this.model.last_deployment &&
+ this.model.last_deployment.commit &&
+ this.model.last_deployment.commit.short_id
+ ) {
+ return this.model.last_deployment.commit.short_id;
+ }
+ return undefined;
+ },
+
+ /**
+ * If provided, returns the commit title.
+ *
+ * @returns {String|Undefined}
+ */
+ commitTitle() {
+ if (
+ this.model &&
+ this.model.last_deployment &&
+ this.model.last_deployment.commit &&
+ this.model.last_deployment.commit.title
+ ) {
+ return this.model.last_deployment.commit.title;
+ }
+ return undefined;
+ },
+
+ /**
+ * If provided, returns the commit tag.
+ *
+ * @returns {Object|Undefined}
+ */
+ commitAuthor() {
+ if (
+ this.model &&
+ this.model.last_deployment &&
+ this.model.last_deployment.commit &&
+ this.model.last_deployment.commit.author
+ ) {
+ return this.model.last_deployment.commit.author;
+ }
+
+ return undefined;
+ },
+
+ /**
+ * Verifies if the `retry_path` key is present and returns its value.
+ *
+ * @returns {String|Undefined}
+ */
+ retryUrl() {
+ if (
+ this.model &&
+ this.model.last_deployment &&
+ this.model.last_deployment.deployable &&
+ this.model.last_deployment.deployable.retry_path
+ ) {
+ return this.model.last_deployment.deployable.retry_path;
+ }
+ return undefined;
+ },
+
+ /**
+ * Verifies if the `last?` key is present and returns its value.
+ *
+ * @returns {Boolean|Undefined}
+ */
+ isLastDeployment() {
+ return this.model && this.model.last_deployment && this.model.last_deployment['last?'];
+ },
+
+ /**
+ * Builds the name of the builds needed to display both the name and the id.
+ *
+ * @returns {String}
+ */
+ buildName() {
+ if (this.model && this.model.last_deployment && this.model.last_deployment.deployable) {
+ const { deployable } = this.model.last_deployment;
+ return `${deployable.name} #${deployable.id}`;
+ }
+ return '';
+ },
+
+ /**
+ * Builds the needed string to show the internal id.
+ *
+ * @returns {String}
+ */
+ deploymentInternalId() {
+ if (this.model && this.model.last_deployment && this.model.last_deployment.iid) {
+ return `#${this.model.last_deployment.iid}`;
+ }
+ return '';
+ },
+
+ /**
+ * Verifies if the user object is present under last_deployment object.
+ *
+ * @returns {Boolean}
+ */
+ deploymentHasUser() {
+ return (
+ this.model &&
+ !_.isEmpty(this.model.last_deployment) &&
+ !_.isEmpty(this.model.last_deployment.user)
+ );
+ },
+
+ /**
+ * Returns the user object nested with the last_deployment object.
+ * Used to render the template.
+ *
+ * @returns {Object}
+ */
+ deploymentUser() {
+ if (
+ this.model &&
+ !_.isEmpty(this.model.last_deployment) &&
+ !_.isEmpty(this.model.last_deployment.user)
+ ) {
+ return this.model.last_deployment.user;
+ }
+ return {};
},
- computed: {
- /**
- * Verifies if `last_deployment` key exists in the current Envrionment.
- * This key is required to render most of the html - this method works has
- * an helper.
- *
- * @returns {Boolean}
- */
- hasLastDeploymentKey() {
- if (this.model &&
- this.model.last_deployment &&
- !_.isEmpty(this.model.last_deployment)) {
- return true;
- }
- return false;
- },
-
- /**
- * Verifies is the given environment has manual actions.
- * Used to verify if we should render them or nor.
- *
- * @returns {Boolean|Undefined}
- */
- hasManualActions() {
- return this.model &&
- this.model.last_deployment &&
- this.model.last_deployment.manual_actions &&
- this.model.last_deployment.manual_actions.length > 0;
- },
-
- /**
- * Returns the value of the `stop_action?` key provided in the response.
- *
- * @returns {Boolean}
- */
- hasStopAction() {
- return this.model && this.model['stop_action?'];
- },
-
- /**
- * Verifies if the `deployable` key is present in `last_deployment` key.
- * Used to verify whether we should or not render the rollback partial.
- *
- * @returns {Boolean|Undefined}
- */
- canRetry() {
- return this.model &&
- this.hasLastDeploymentKey &&
- this.model.last_deployment &&
- this.model.last_deployment.deployable;
- },
-
- /**
- * Verifies if the date to be shown is present.
- *
- * @returns {Boolean|Undefined}
- */
- canShowDate() {
- return this.model &&
- this.model.last_deployment &&
- this.model.last_deployment.deployable &&
- this.model.last_deployment.deployable !== undefined;
- },
-
- /**
- * Human readable date.
- *
- * @returns {String}
- */
- createdDate() {
- if (this.model &&
- this.model.last_deployment &&
- this.model.last_deployment.deployable &&
- this.model.last_deployment.deployable.created_at) {
- return timeagoInstance.format(this.model.last_deployment.deployable.created_at);
- }
- return '';
- },
-
- /**
- * Returns the manual actions with the name parsed.
- *
- * @returns {Array.<Object>|Undefined}
- */
- manualActions() {
- if (this.hasManualActions) {
- return this.model.last_deployment.manual_actions.map((action) => {
- const parsedAction = {
- name: humanize(action.name),
- play_path: action.play_path,
- playable: action.playable,
- };
- return parsedAction;
- });
- }
- return [];
- },
-
- /**
- * Builds the string used in the user image alt attribute.
- *
- * @returns {String}
- */
- userImageAltDescription() {
- if (this.model &&
- this.model.last_deployment &&
- this.model.last_deployment.user &&
- this.model.last_deployment.user.username) {
- return `${this.model.last_deployment.user.username}'s avatar'`;
- }
- return '';
- },
-
- /**
- * If provided, returns the commit tag.
- *
- * @returns {String|Undefined}
- */
- commitTag() {
- if (this.model &&
- this.model.last_deployment &&
- this.model.last_deployment.tag) {
- return this.model.last_deployment.tag;
- }
- return undefined;
- },
-
- /**
- * If provided, returns the commit ref.
- *
- * @returns {Object|Undefined}
- */
- commitRef() {
- if (this.model &&
- this.model.last_deployment &&
- this.model.last_deployment.ref) {
- return this.model.last_deployment.ref;
- }
- return undefined;
- },
-
- /**
- * If provided, returns the commit url.
- *
- * @returns {String|Undefined}
- */
- commitUrl() {
- if (this.model &&
- this.model.last_deployment &&
- this.model.last_deployment.commit &&
- this.model.last_deployment.commit.commit_path) {
- return this.model.last_deployment.commit.commit_path;
- }
- return undefined;
- },
-
- /**
- * If provided, returns the commit short sha.
- *
- * @returns {String|Undefined}
- */
- commitShortSha() {
- if (this.model &&
- this.model.last_deployment &&
- this.model.last_deployment.commit &&
- this.model.last_deployment.commit.short_id) {
- return this.model.last_deployment.commit.short_id;
- }
- return undefined;
- },
-
- /**
- * If provided, returns the commit title.
- *
- * @returns {String|Undefined}
- */
- commitTitle() {
- if (this.model &&
- this.model.last_deployment &&
- this.model.last_deployment.commit &&
- this.model.last_deployment.commit.title) {
- return this.model.last_deployment.commit.title;
- }
- return undefined;
- },
-
- /**
- * If provided, returns the commit tag.
- *
- * @returns {Object|Undefined}
- */
- commitAuthor() {
- if (this.model &&
- this.model.last_deployment &&
- this.model.last_deployment.commit &&
- this.model.last_deployment.commit.author) {
- return this.model.last_deployment.commit.author;
- }
-
- return undefined;
- },
-
- /**
- * Verifies if the `retry_path` key is present and returns its value.
- *
- * @returns {String|Undefined}
- */
- retryUrl() {
- if (this.model &&
- this.model.last_deployment &&
- this.model.last_deployment.deployable &&
- this.model.last_deployment.deployable.retry_path) {
- return this.model.last_deployment.deployable.retry_path;
- }
- return undefined;
- },
-
- /**
- * Verifies if the `last?` key is present and returns its value.
- *
- * @returns {Boolean|Undefined}
- */
- isLastDeployment() {
- return this.model && this.model.last_deployment &&
- this.model.last_deployment['last?'];
- },
-
- /**
- * Builds the name of the builds needed to display both the name and the id.
- *
- * @returns {String}
- */
- buildName() {
- if (this.model &&
- this.model.last_deployment &&
- this.model.last_deployment.deployable) {
- const deployable = this.model.last_deployment.deployable;
- return `${deployable.name} #${deployable.id}`;
- }
- return '';
- },
-
- /**
- * Builds the needed string to show the internal id.
- *
- * @returns {String}
- */
- deploymentInternalId() {
- if (this.model &&
- this.model.last_deployment &&
- this.model.last_deployment.iid) {
- return `#${this.model.last_deployment.iid}`;
- }
- return '';
- },
-
- /**
- * Verifies if the user object is present under last_deployment object.
- *
- * @returns {Boolean}
- */
- deploymentHasUser() {
- return this.model &&
- !_.isEmpty(this.model.last_deployment) &&
- !_.isEmpty(this.model.last_deployment.user);
- },
-
- /**
- * Returns the user object nested with the last_deployment object.
- * Used to render the template.
- *
- * @returns {Object}
- */
- deploymentUser() {
- if (this.model &&
- !_.isEmpty(this.model.last_deployment) &&
- !_.isEmpty(this.model.last_deployment.user)) {
- return this.model.last_deployment.user;
- }
- return {};
- },
-
- /**
- * Verifies if the build name column should be rendered by verifing
- * if all the information needed is present
- * and if the environment is not a folder.
- *
- * @returns {Boolean}
- */
- shouldRenderBuildName() {
- return !this.model.isFolder &&
- !_.isEmpty(this.model.last_deployment) &&
- !_.isEmpty(this.model.last_deployment.deployable);
- },
-
- /**
- * Verifies the presence of all the keys needed to render the buil_path.
- *
- * @return {String}
- */
- buildPath() {
- if (this.model &&
- this.model.last_deployment &&
- this.model.last_deployment.deployable &&
- this.model.last_deployment.deployable.build_path) {
- return this.model.last_deployment.deployable.build_path;
- }
-
- return '';
- },
-
- /**
- * Verifies the presence of all the keys needed to render the external_url.
- *
- * @return {String}
- */
- externalURL() {
- if (this.model && this.model.external_url) {
- return this.model.external_url;
- }
-
- return '';
- },
-
- /**
- * Verifies if deplyment internal ID should be rendered by verifing
- * if all the information needed is present
- * and if the environment is not a folder.
- *
- * @returns {Boolean}
- */
- shouldRenderDeploymentID() {
- return !this.model.isFolder &&
- !_.isEmpty(this.model.last_deployment) &&
- this.model.last_deployment.iid !== undefined;
- },
-
- environmentPath() {
- if (this.model && this.model.environment_path) {
- return this.model.environment_path;
- }
-
- return '';
- },
-
- monitoringUrl() {
- if (this.model && this.model.metrics_path) {
- return this.model.metrics_path;
- }
-
- return '';
- },
-
- displayEnvironmentActions() {
- return this.hasManualActions ||
- this.externalURL ||
- this.monitoringUrl ||
- this.hasStopAction ||
- this.canRetry;
- },
+ /**
+ * Verifies if the build name column should be rendered by verifing
+ * if all the information needed is present
+ * and if the environment is not a folder.
+ *
+ * @returns {Boolean}
+ */
+ shouldRenderBuildName() {
+ return (
+ !this.model.isFolder &&
+ !_.isEmpty(this.model.last_deployment) &&
+ !_.isEmpty(this.model.last_deployment.deployable)
+ );
},
- methods: {
- onClickFolder() {
- eventHub.$emit('toggleFolder', this.model);
- },
+ /**
+ * Verifies the presence of all the keys needed to render the buil_path.
+ *
+ * @return {String}
+ */
+ buildPath() {
+ if (
+ this.model &&
+ this.model.last_deployment &&
+ this.model.last_deployment.deployable &&
+ this.model.last_deployment.deployable.build_path
+ ) {
+ return this.model.last_deployment.deployable.build_path;
+ }
+
+ return '';
},
- };
+
+ /**
+ * Verifies the presence of all the keys needed to render the external_url.
+ *
+ * @return {String}
+ */
+ externalURL() {
+ if (this.model && this.model.external_url) {
+ return this.model.external_url;
+ }
+
+ return '';
+ },
+
+ /**
+ * Verifies if deplyment internal ID should be rendered by verifing
+ * if all the information needed is present
+ * and if the environment is not a folder.
+ *
+ * @returns {Boolean}
+ */
+ shouldRenderDeploymentID() {
+ return (
+ !this.model.isFolder &&
+ !_.isEmpty(this.model.last_deployment) &&
+ this.model.last_deployment.iid !== undefined
+ );
+ },
+
+ environmentPath() {
+ if (this.model && this.model.environment_path) {
+ return this.model.environment_path;
+ }
+
+ return '';
+ },
+
+ monitoringUrl() {
+ if (this.model && this.model.metrics_path) {
+ return this.model.metrics_path;
+ }
+
+ return '';
+ },
+
+ displayEnvironmentActions() {
+ return (
+ this.hasManualActions ||
+ this.externalURL ||
+ this.monitoringUrl ||
+ this.canStopEnvironment ||
+ this.canRetry
+ );
+ },
+ },
+
+ methods: {
+ onClickFolder() {
+ eventHub.$emit('toggleFolder', this.model);
+ },
+ },
+};
</script>
<template>
<div
- class="gl-responsive-table-row"
:class="{
'js-child-row environment-child-row': model.isChildren,
'folder-row': model.isFolder,
}"
+ class="gl-responsive-table-row"
role="row">
<div
class="table-section section-10"
@@ -446,19 +467,19 @@
</div>
<a
v-if="!model.isFolder"
- class="environment-name flex-truncate-parent table-mobile-content"
- :href="environmentPath">
+ :href="environmentPath"
+ class="environment-name flex-truncate-parent table-mobile-content">
<span
- class="flex-truncate-child"
v-tooltip
:title="model.name"
+ class="flex-truncate-child"
>{{ model.name }}</span>
</a>
<span
v-else
class="folder-name"
- @click="onClickFolder"
- role="button">
+ role="button"
+ @click="onClickFolder">
<span class="folder-icon">
<i
@@ -486,14 +507,14 @@
{{ model.folderName }}
</span>
- <span class="badge">
+ <span class="badge badge-pill">
{{ model.size }}
</span>
</span>
</div>
<div
- class="table-section section-10 deployment-column hidden-xs hidden-sm"
+ class="table-section section-10 deployment-column d-none d-sm-none d-md-block"
role="gridcell"
>
<span v-if="shouldRenderDeploymentID">
@@ -503,23 +524,23 @@
<span v-if="!model.isFolder && deploymentHasUser">
by
<user-avatar-link
- class="js-deploy-user-container"
:link-href="deploymentUser.web_url"
:img-src="deploymentUser.avatar_url"
:img-alt="userImageAltDescription"
:tooltip-text="deploymentUser.username"
+ class="js-deploy-user-container"
/>
</span>
</div>
<div
- class="table-section section-15 hidden-xs hidden-sm"
+ class="table-section section-15 d-none d-sm-none d-md-block"
role="gridcell"
>
<a
v-if="shouldRenderBuildName"
- class="build-link flex-truncate-parent"
:href="buildPath"
+ class="build-link flex-truncate-parent"
>
<span class="flex-truncate-child">{{ buildName }}</span>
</a>
@@ -580,11 +601,6 @@
class="btn-group table-action-buttons"
role="group">
- <actions-component
- v-if="hasManualActions && canCreateDeployment"
- :actions="manualActions"
- />
-
<external-url-component
v-if="externalURL && canReadEnvironment"
:external-url="externalURL"
@@ -595,21 +611,26 @@
:monitoring-url="monitoringUrl"
/>
+ <actions-component
+ v-if="hasManualActions && canCreateDeployment"
+ :actions="manualActions"
+ />
+
<terminal-button-component
v-if="model && model.terminal_path"
:terminal-path="model.terminal_path"
/>
- <stop-component
- v-if="hasStopAction && canCreateDeployment"
- :stop-url="model.stop_path"
- />
-
<rollback-component
v-if="canRetry && canCreateDeployment"
:is-last-deployment="isLastDeployment"
:retry-url="retryUrl"
/>
+
+ <stop-component
+ v-if="canStopEnvironment"
+ :environment="model"
+ />
</div>
</div>
</div>
diff --git a/app/assets/javascripts/environments/components/environment_monitoring.vue b/app/assets/javascripts/environments/components/environment_monitoring.vue
index deada134b27..ccc8419ca6d 100644
--- a/app/assets/javascripts/environments/components/environment_monitoring.vue
+++ b/app/assets/javascripts/environments/components/environment_monitoring.vue
@@ -1,43 +1,40 @@
<script>
- /**
- * Renders the Monitoring (Metrics) link in environments table.
- */
- import Icon from '~/vue_shared/components/icon.vue';
- import tooltip from '../../vue_shared/directives/tooltip';
+/**
+ * Renders the Monitoring (Metrics) link in environments table.
+ */
+import Icon from '~/vue_shared/components/icon.vue';
+import tooltip from '../../vue_shared/directives/tooltip';
- export default {
- components: {
- Icon,
+export default {
+ components: {
+ Icon,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ monitoringUrl: {
+ type: String,
+ required: true,
},
- directives: {
- tooltip,
+ },
+ computed: {
+ title() {
+ return 'Monitoring';
},
- props: {
- monitoringUrl: {
- type: String,
- required: true,
- },
- },
- computed: {
- title() {
- return 'Monitoring';
- },
- },
- };
+ },
+};
</script>
<template>
<a
v-tooltip
- class="btn monitoring-url hidden-xs hidden-sm"
- data-container="body"
- rel="noopener noreferrer nofollow"
:href="monitoringUrl"
:title="title"
:aria-label="title"
+ class="btn monitoring-url d-none d-sm-none d-md-block"
+ data-container="body"
+ rel="noopener noreferrer nofollow"
>
- <icon
- name="chart"
- :size="12"
- />
+ <icon name="chart" />
</a>
</template>
diff --git a/app/assets/javascripts/environments/components/environment_rollback.vue b/app/assets/javascripts/environments/components/environment_rollback.vue
index c822fb1574c..4deeef4beb9 100644
--- a/app/assets/javascripts/environments/components/environment_rollback.vue
+++ b/app/assets/javascripts/environments/components/environment_rollback.vue
@@ -1,56 +1,74 @@
<script>
- /**
- * Renders Rollback or Re deploy button in environments table depending
- * of the provided property `isLastDeployment`.
- *
- * Makes a post request when the button is clicked.
- */
- import eventHub from '../event_hub';
- import loadingIcon from '../../vue_shared/components/loading_icon.vue';
-
- export default {
- components: {
- loadingIcon,
+/**
+ * Renders Rollback or Re deploy button in environments table depending
+ * of the provided property `isLastDeployment`.
+ *
+ * Makes a post request when the button is clicked.
+ */
+import { s__ } from '~/locale';
+import Icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import eventHub from '../event_hub';
+import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
+
+export default {
+ components: {
+ Icon,
+ LoadingIcon,
+ },
+
+ directives: {
+ tooltip,
+ },
+
+ props: {
+ retryUrl: {
+ type: String,
+ default: '',
},
- props: {
- retryUrl: {
- type: String,
- default: '',
- },
-
- isLastDeployment: {
- type: Boolean,
- default: true,
- },
+
+ isLastDeployment: {
+ type: Boolean,
+ default: true,
},
- data() {
- return {
- isLoading: false,
- };
+ },
+ data() {
+ return {
+ isLoading: false,
+ };
+ },
+
+ computed: {
+ title() {
+ return this.isLastDeployment ? s__('Environments|Re-deploy to environment') : s__('Environments|Rollback environment');
},
- methods: {
- onClick() {
- this.isLoading = true;
+ },
+
+ methods: {
+ onClick() {
+ this.isLoading = true;
- eventHub.$emit('postAction', this.retryUrl);
- },
+ eventHub.$emit('postAction', { endpoint: this.retryUrl });
},
- };
+ },
+};
</script>
<template>
<button
+ v-tooltip
+ :disabled="isLoading"
+ :title="title"
type="button"
- class="btn hidden-xs hidden-sm"
+ class="btn d-none d-sm-none d-md-block"
@click="onClick"
- :disabled="isLoading"
>
- <span v-if="isLastDeployment">
- {{ s__("Environments|Re-deploy") }}
- </span>
- <span v-else>
- {{ s__("Environments|Rollback") }}
- </span>
+ <icon
+ v-if="isLastDeployment"
+ name="repeat" />
+ <icon
+ v-else
+ name="redo"/>
<loading-icon v-if="isLoading" />
</button>
diff --git a/app/assets/javascripts/environments/components/environment_stop.vue b/app/assets/javascripts/environments/components/environment_stop.vue
index dda7429a726..a814b3405f5 100644
--- a/app/assets/javascripts/environments/components/environment_stop.vue
+++ b/app/assets/javascripts/environments/components/environment_stop.vue
@@ -1,72 +1,78 @@
<script>
- /**
- * Renders the stop "button" that allows stop an environment.
- * Used in environments table.
- */
+/**
+ * Renders the stop "button" that allows stop an environment.
+ * Used in environments table.
+ */
- import $ from 'jquery';
- import eventHub from '../event_hub';
- import loadingIcon from '../../vue_shared/components/loading_icon.vue';
- import tooltip from '../../vue_shared/directives/tooltip';
+import $ from 'jquery';
+import Icon from '~/vue_shared/components/icon.vue';
+import { s__ } from '~/locale';
+import eventHub from '../event_hub';
+import LoadingButton from '../../vue_shared/components/loading_button.vue';
+import tooltip from '../../vue_shared/directives/tooltip';
- export default {
- components: {
- loadingIcon,
- },
+export default {
+ components: {
+ Icon,
+ LoadingButton,
+ },
- directives: {
- tooltip,
- },
+ directives: {
+ tooltip,
+ },
- props: {
- stopUrl: {
- type: String,
- default: '',
- },
+ props: {
+ environment: {
+ type: Object,
+ required: true,
},
+ },
- data() {
- return {
- isLoading: false,
- };
- },
+ data() {
+ return {
+ isLoading: false,
+ };
+ },
- computed: {
- title() {
- return 'Stop';
- },
+ computed: {
+ title() {
+ return s__('Environments|Stop environment');
},
+ },
- methods: {
- onClick() {
- // eslint-disable-next-line no-alert
- if (confirm('Are you sure you want to stop this environment?')) {
- this.isLoading = true;
+ mounted() {
+ eventHub.$on('stopEnvironment', this.onStopEnvironment);
+ },
- $(this.$el).tooltip('destroy');
+ beforeDestroy() {
+ eventHub.$off('stopEnvironment', this.onStopEnvironment);
+ },
- eventHub.$emit('postAction', this.stopUrl);
- }
- },
+ methods: {
+ onClick() {
+ $(this.$el).tooltip('dispose');
+ eventHub.$emit('requestStopEnvironment', this.environment);
+ },
+ onStopEnvironment(environment) {
+ if (this.environment.id === environment.id) {
+ this.isLoading = true;
+ }
},
- };
+ },
+};
</script>
<template>
- <button
+ <loading-button
v-tooltip
- type="button"
- class="btn stop-env-link hidden-xs hidden-sm"
- data-container="body"
- @click="onClick"
- :disabled="isLoading"
+ :loading="isLoading"
:title="title"
:aria-label="title"
+ container-class="btn btn-danger d-none d-sm-none d-md-block"
+ data-container="body"
+ data-toggle="modal"
+ data-target="#stop-environment-modal"
+ @click="onClick"
>
- <i
- class="fa fa-stop stop-env-icon"
- aria-hidden="true"
- >
- </i>
- <loading-icon v-if="isLoading" />
- </button>
+ <icon name="stop"/>
+ </loading-button>
</template>
diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.vue b/app/assets/javascripts/environments/components/environment_terminal_button.vue
index e8469d088ef..350417e5ad0 100644
--- a/app/assets/javascripts/environments/components/environment_terminal_button.vue
+++ b/app/assets/javascripts/environments/components/environment_terminal_button.vue
@@ -1,44 +1,41 @@
<script>
- /**
- * Renders a terminal button to open a web terminal.
- * Used in environments table.
- */
- import Icon from '~/vue_shared/components/icon.vue';
- import tooltip from '../../vue_shared/directives/tooltip';
+/**
+ * Renders a terminal button to open a web terminal.
+ * Used in environments table.
+ */
+import Icon from '~/vue_shared/components/icon.vue';
+import tooltip from '../../vue_shared/directives/tooltip';
- export default {
- components: {
- Icon,
+export default {
+ components: {
+ Icon,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ terminalPath: {
+ type: String,
+ required: false,
+ default: '',
},
- directives: {
- tooltip,
+ },
+ computed: {
+ title() {
+ return 'Terminal';
},
- props: {
- terminalPath: {
- type: String,
- required: false,
- default: '',
- },
- },
- computed: {
- title() {
- return 'Terminal';
- },
- },
- };
+ },
+};
</script>
<template>
<a
v-tooltip
- class="btn terminal-button hidden-xs hidden-sm"
- data-container="body"
:title="title"
:aria-label="title"
:href="terminalPath"
+ class="btn terminal-button d-none d-sm-none d-md-block"
+ data-container="body"
>
- <icon
- name="terminal"
- :size="12"
- />
+ <icon name="terminal" />
</a>
</template>
diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue
index 3da762446c9..8efdfb8abe0 100644
--- a/app/assets/javascripts/environments/components/environments_app.vue
+++ b/app/assets/javascripts/environments/components/environments_app.vue
@@ -5,10 +5,12 @@
import eventHub from '../event_hub';
import environmentsMixin from '../mixins/environments_mixin';
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
+ import StopEnvironmentModal from './stop_environment_modal.vue';
export default {
components: {
emptyState,
+ StopEnvironmentModal,
},
mixins: [
@@ -90,11 +92,13 @@
</script>
<template>
<div :class="cssContainerClass">
+ <stop-environment-modal :environment="environmentInStopModal" />
+
<div class="top-area">
<tabs
:tabs="tabs"
- @onChangeTab="onChangeTab"
scope="environments"
+ @onChangeTab="onChangeTab"
/>
<div
@@ -119,8 +123,8 @@
@onChangePage="onChangePage"
>
<empty-state
- slot="emptyState"
v-if="!isLoading && state.environments.length === 0"
+ slot="emptyState"
:new-path="newEnvironmentPath"
:help-path="helpPagePath"
:can-create-environment="canCreateEnvironment"
diff --git a/app/assets/javascripts/environments/components/stop_environment_modal.vue b/app/assets/javascripts/environments/components/stop_environment_modal.vue
new file mode 100644
index 00000000000..657cc8cd1aa
--- /dev/null
+++ b/app/assets/javascripts/environments/components/stop_environment_modal.vue
@@ -0,0 +1,92 @@
+<script>
+import GlModal from '~/vue_shared/components/gl_modal.vue';
+import { s__, sprintf } from '~/locale';
+import tooltip from '~/vue_shared/directives/tooltip';
+import LoadingButton from '~/vue_shared/components/loading_button.vue';
+import eventHub from '../event_hub';
+
+export default {
+ id: 'stop-environment-modal',
+ name: 'StopEnvironmentModal',
+
+ components: {
+ GlModal,
+ LoadingButton,
+ },
+
+ directives: {
+ tooltip,
+ },
+
+ props: {
+ environment: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ computed: {
+ noStopActionMessage() {
+ return sprintf(
+ s__(
+ `Environments|Note that this action will stop the environment,
+ but it will %{emphasisStart}not%{emphasisEnd} have an effect on any existing deployment
+ due to no “stop environment action†being defined
+ in the %{ciConfigLinkStart}.gitlab-ci.yml%{ciConfigLinkEnd} file.`,
+ ),
+ {
+ emphasisStart: '<strong>',
+ emphasisEnd: '</strong>',
+ ciConfigLinkStart:
+ '<a href="https://docs.gitlab.com/ee/ci/yaml/" target="_blank" rel="noopener noreferrer">',
+ ciConfigLinkEnd: '</a>',
+ },
+ false,
+ );
+ },
+ },
+
+ methods: {
+ onSubmit() {
+ eventHub.$emit('stopEnvironment', this.environment);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-modal
+ :id="$options.id"
+ :footer-primary-button-text="s__('Environments|Stop environment')"
+ footer-primary-button-variant="danger"
+ @submit="onSubmit"
+ >
+ <template slot="header">
+ <h4
+ class="modal-title d-flex mw-100"
+ >
+ Stopping
+ <span
+ v-tooltip
+ :title="environment.name"
+ class="text-truncate ml-1 mr-1 flex-fill"
+ >{{ environment.name }}</span>
+ ?
+ </h4>
+ </template>
+
+ <p>{{ s__('Environments|Are you sure you want to stop this environment?') }}</p>
+
+ <div
+ v-if="!environment.has_stop_action"
+ class="warning_message"
+ >
+ <p v-html="noStopActionMessage"></p>
+ <a
+ href="https://docs.gitlab.com/ee/ci/environments.html#stopping-an-environment"
+ target="_blank"
+ rel="noopener noreferrer"
+ >{{ s__('Environments|Learn more about stopping environments') }}</a>
+ </div>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.vue b/app/assets/javascripts/environments/folder/environments_folder_view.vue
index 5ef5e347387..e69bfa0b2cc 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_view.vue
+++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue
@@ -1,12 +1,18 @@
<script>
import environmentsMixin from '../mixins/environments_mixin';
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
+ import StopEnvironmentModal from '../components/stop_environment_modal.vue';
export default {
+ components: {
+ StopEnvironmentModal,
+ },
+
mixins: [
environmentsMixin,
CIPaginationMixin,
],
+
props: {
endpoint: {
type: String,
@@ -38,9 +44,11 @@
</script>
<template>
<div :class="cssContainerClass">
+ <stop-environment-modal :environment="environmentInStopModal" />
+
<div
- class="top-area"
v-if="!isLoading"
+ class="top-area"
>
<h4 class="js-folder-name environments-folder-name">
@@ -49,8 +57,8 @@
<tabs
:tabs="tabs"
- @onChangeTab="onChangeTab"
scope="environments"
+ @onChangeTab="onChangeTab"
/>
</div>
diff --git a/app/assets/javascripts/environments/mixins/environments_mixin.js b/app/assets/javascripts/environments/mixins/environments_mixin.js
index a7a79dbca70..d88624f7f8d 100644
--- a/app/assets/javascripts/environments/mixins/environments_mixin.js
+++ b/app/assets/javascripts/environments/mixins/environments_mixin.js
@@ -40,6 +40,7 @@ export default {
scope: getParameterByName('scope') || 'available',
page: getParameterByName('page') || '1',
requestData: {},
+ environmentInStopModal: {},
};
},
@@ -85,7 +86,7 @@ export default {
Flash(s__('Environments|An error occurred while fetching the environments.'));
},
- postAction(endpoint) {
+ postAction({ endpoint, errorMessage }) {
if (!this.isMakingRequest) {
this.isLoading = true;
@@ -93,7 +94,7 @@ export default {
.then(() => this.fetchEnvironments())
.catch(() => {
this.isLoading = false;
- Flash(s__('Environments|An error occurred while making the request.'));
+ Flash(errorMessage || s__('Environments|An error occurred while making the request.'));
});
}
},
@@ -106,6 +107,15 @@ export default {
.catch(this.errorCallback);
},
+ updateStopModal(environment) {
+ this.environmentInStopModal = environment;
+ },
+
+ stopEnvironment(environment) {
+ const endpoint = environment.stop_path;
+ const errorMessage = s__('Environments|An error occurred while stopping the environment, please try again');
+ this.postAction({ endpoint, errorMessage });
+ },
},
computed: {
@@ -162,9 +172,13 @@ export default {
});
eventHub.$on('postAction', this.postAction);
+ eventHub.$on('requestStopEnvironment', this.updateStopModal);
+ eventHub.$on('stopEnvironment', this.stopEnvironment);
},
- beforeDestroyed() {
- eventHub.$off('postAction');
+ beforeDestroy() {
+ eventHub.$off('postAction', this.postAction);
+ eventHub.$off('requestStopEnvironment', this.updateStopModal);
+ eventHub.$off('stopEnvironment', this.stopEnvironment);
},
};
diff --git a/app/assets/javascripts/environments/services/environments_service.js b/app/assets/javascripts/environments/services/environments_service.js
index 3b121551aca..4e07ccba91a 100644
--- a/app/assets/javascripts/environments/services/environments_service.js
+++ b/app/assets/javascripts/environments/services/environments_service.js
@@ -13,7 +13,7 @@ export default class EnvironmentsService {
// eslint-disable-next-line class-methods-use-this
postAction(endpoint) {
- return axios.post(endpoint, {}, { emulateJSON: true });
+ return axios.post(endpoint, {});
}
getFolderContent(folderUrl) {
diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js
index 5f2989ab854..5ce9225a4bb 100644
--- a/app/assets/javascripts/environments/stores/environments_store.js
+++ b/app/assets/javascripts/environments/stores/environments_store.js
@@ -146,7 +146,7 @@ export default class EnvironmentsStore {
* @return {Array}
*/
updateEnvironmentProp(environment, prop, newValue) {
- const environments = this.state.environments;
+ const { environments } = this.state;
const updatedEnvironments = environments.map((env) => {
const updateEnv = Object.assign({}, env);
@@ -161,7 +161,7 @@ export default class EnvironmentsStore {
}
getOpenFolders() {
- const environments = this.state.environments;
+ const { environments } = this.state;
return environments.filter(env => env.isFolder && env.isOpen);
}
diff --git a/app/assets/javascripts/feature_highlight/feature_highlight.js b/app/assets/javascripts/feature_highlight/feature_highlight.js
index 2d5bae9a9c4..2f27c9351bc 100644
--- a/app/assets/javascripts/feature_highlight/feature_highlight.js
+++ b/app/assets/javascripts/feature_highlight/feature_highlight.js
@@ -24,7 +24,7 @@ export function setupFeatureHighlightPopover(id, debounceTimeout = 300) {
template: `
<div class="popover feature-highlight-popover" role="tooltip">
<div class="arrow"></div>
- <div class="popover-content"></div>
+ <div class="popover-body"></div>
</div>
`,
})
diff --git a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue
index 26618af9515..a8eb8d94be3 100644
--- a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue
+++ b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue
@@ -72,9 +72,9 @@ export default {
@click="onItemActivated(item.text)">
<span>
<span
- class="filtered-search-history-dropdown-token"
v-for="(token, index) in item.tokens"
:key="`dropdown-token-${index}`"
+ class="filtered-search-history-dropdown-token"
>
<span class="name">{{ token.prefix }}</span>
<span class="value">{{ token.suffix }}</span>
diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js
index 9bc36c1f9b6..27fff488603 100644
--- a/app/assets/javascripts/filtered_search/dropdown_utils.js
+++ b/app/assets/javascripts/filtered_search/dropdown_utils.js
@@ -35,7 +35,7 @@ export default class DropdownUtils {
// Remove the symbol for filter
if (value[0] === filterSymbol) {
- symbol = value[0];
+ [symbol] = value;
value = value.slice(1);
}
@@ -162,7 +162,7 @@ export default class DropdownUtils {
// Determines the full search query (visual tokens + input)
static getSearchQuery(untilInput = false) {
- const container = FilteredSearchContainer.container;
+ const { container } = FilteredSearchContainer;
const tokens = [].slice.call(container.querySelectorAll('.tokens-container li'));
const values = [];
@@ -220,7 +220,7 @@ export default class DropdownUtils {
}
static getInputSelectionPosition(input) {
- const selectionStart = input.selectionStart;
+ const { selectionStart } = input;
let inputValue = input.value;
// Replace all spaces inside quote marks with underscores
// (will continue to match entire string until an end quote is found if any)
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
index d7e1de18d09..296571606d6 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
@@ -159,7 +159,7 @@ export default class FilteredSearchDropdownManager {
load(key, firstLoad = false) {
const mappingKey = this.mapping[key];
const glClass = mappingKey.gl;
- const element = mappingKey.element;
+ const { element } = mappingKey;
let forceShowList = false;
if (!mappingKey.reference) {
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index cf5ba1e1771..81286c54c4c 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -235,7 +235,7 @@ export default class FilteredSearchManager {
checkForEnter(e) {
if (e.keyCode === 38 || e.keyCode === 40) {
- const selectionStart = this.filteredSearchInput.selectionStart;
+ const { selectionStart } = this.filteredSearchInput;
e.preventDefault();
this.filteredSearchInput.setSelectionRange(selectionStart, selectionStart);
@@ -496,7 +496,7 @@ export default class FilteredSearchManager {
// Replace underscore with hyphen in the sanitizedkey.
// e.g. 'my_reaction' => 'my-reaction'
sanitizedKey = sanitizedKey.replace('_', '-');
- const symbol = match.symbol;
+ const { symbol } = match;
let quotationsToUse = '';
if (sanitizedValue.indexOf(' ') !== -1) {
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 600024c21c3..56fe1ab4e90 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
@@ -101,7 +101,7 @@ export default class FilteredSearchVisualTokens {
static updateLabelTokenColor(tokenValueContainer, tokenValue) {
const filteredSearchInput = FilteredSearchContainer.container.querySelector('.filtered-search');
- const baseEndpoint = filteredSearchInput.dataset.baseEndpoint;
+ const { baseEndpoint } = filteredSearchInput.dataset;
const labelsEndpoint = FilteredSearchVisualTokens.getEndpointWithQueryParams(
`${baseEndpoint}/labels.json`,
filteredSearchInput.dataset.endpointQueryParams,
@@ -215,7 +215,7 @@ export default class FilteredSearchVisualTokens {
static addFilterVisualToken(tokenName, tokenValue, canEdit) {
const { lastVisualToken, isLastVisualTokenValid }
= FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
- const addVisualTokenElement = FilteredSearchVisualTokens.addVisualTokenElement;
+ const { addVisualTokenElement } = FilteredSearchVisualTokens;
if (isLastVisualTokenValid) {
addVisualTokenElement(tokenName, tokenValue, false, canEdit);
diff --git a/app/assets/javascripts/filtered_search/recent_searches_root.js b/app/assets/javascripts/filtered_search/recent_searches_root.js
index f9338b82acf..c1efa9c86f4 100644
--- a/app/assets/javascripts/filtered_search/recent_searches_root.js
+++ b/app/assets/javascripts/filtered_search/recent_searches_root.js
@@ -29,7 +29,7 @@ class RecentSearchesRoot {
}
render() {
- const state = this.store.state;
+ const { state } = this.store;
this.vm = new Vue({
el: this.wrapperElement,
components: {
diff --git a/app/assets/javascripts/frequent_items/components/app.vue b/app/assets/javascripts/frequent_items/components/app.vue
new file mode 100644
index 00000000000..2f030de8967
--- /dev/null
+++ b/app/assets/javascripts/frequent_items/components/app.vue
@@ -0,0 +1,122 @@
+<script>
+import { mapState, mapActions, mapGetters } from 'vuex';
+import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
+import AccessorUtilities from '~/lib/utils/accessor';
+import eventHub from '../event_hub';
+import store from '../store/';
+import { FREQUENT_ITEMS, STORAGE_KEY } from '../constants';
+import { isMobile, updateExistingFrequentItem } from '../utils';
+import FrequentItemsSearchInput from './frequent_items_search_input.vue';
+import FrequentItemsList from './frequent_items_list.vue';
+import frequentItemsMixin from './frequent_items_mixin';
+
+export default {
+ store,
+ components: {
+ LoadingIcon,
+ FrequentItemsSearchInput,
+ FrequentItemsList,
+ },
+ mixins: [frequentItemsMixin],
+ props: {
+ currentUserName: {
+ type: String,
+ required: true,
+ },
+ currentItem: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState(['searchQuery', 'isLoadingItems', 'isFetchFailed', 'items']),
+ ...mapGetters(['hasSearchQuery']),
+ translations() {
+ return this.getTranslations(['loadingMessage', 'header']);
+ },
+ },
+ created() {
+ const { namespace, currentUserName, currentItem } = this;
+ const storageKey = `${currentUserName}/${STORAGE_KEY[namespace]}`;
+
+ this.setNamespace(namespace);
+ this.setStorageKey(storageKey);
+
+ if (currentItem.id) {
+ this.logItemAccess(storageKey, currentItem);
+ }
+
+ eventHub.$on(`${this.namespace}-dropdownOpen`, this.dropdownOpenHandler);
+ },
+ beforeDestroy() {
+ eventHub.$off(`${this.namespace}-dropdownOpen`, this.dropdownOpenHandler);
+ },
+ methods: {
+ ...mapActions(['setNamespace', 'setStorageKey', 'fetchFrequentItems']),
+ dropdownOpenHandler() {
+ if (this.searchQuery === '' || isMobile()) {
+ this.fetchFrequentItems();
+ }
+ },
+ logItemAccess(storageKey, item) {
+ if (!AccessorUtilities.isLocalStorageAccessSafe()) {
+ return false;
+ }
+
+ // Check if there's any frequent items list set
+ const storedRawItems = localStorage.getItem(storageKey);
+ const storedFrequentItems = storedRawItems
+ ? JSON.parse(storedRawItems)
+ : [{ ...item, frequency: 1 }]; // No frequent items list set, set one up.
+
+ // Check if item already exists in list
+ const itemMatchIndex = storedFrequentItems.findIndex(
+ frequentItem => frequentItem.id === item.id,
+ );
+
+ if (itemMatchIndex > -1) {
+ storedFrequentItems[itemMatchIndex] = updateExistingFrequentItem(
+ storedFrequentItems[itemMatchIndex],
+ item,
+ );
+ } else {
+ if (storedFrequentItems.length === FREQUENT_ITEMS.MAX_COUNT) {
+ storedFrequentItems.shift();
+ }
+
+ storedFrequentItems.push({ ...item, frequency: 1 });
+ }
+
+ return localStorage.setItem(storageKey, JSON.stringify(storedFrequentItems));
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <frequent-items-search-input
+ :namespace="namespace"
+ />
+ <loading-icon
+ v-if="isLoadingItems"
+ :label="translations.loadingMessage"
+ class="loading-animation prepend-top-20"
+ size="2"
+ />
+ <div
+ v-if="!isLoadingItems && !hasSearchQuery"
+ class="section-header"
+ >
+ {{ translations.header }}
+ </div>
+ <frequent-items-list
+ v-if="!isLoadingItems"
+ :items="items"
+ :namespace="namespace"
+ :has-search-query="hasSearchQuery"
+ :is-fetch-failed="isFetchFailed"
+ :matcher="searchQuery"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_list.vue b/app/assets/javascripts/frequent_items/components/frequent_items_list.vue
new file mode 100644
index 00000000000..8e511aa2a36
--- /dev/null
+++ b/app/assets/javascripts/frequent_items/components/frequent_items_list.vue
@@ -0,0 +1,78 @@
+<script>
+import FrequentItemsListItem from './frequent_items_list_item.vue';
+import frequentItemsMixin from './frequent_items_mixin';
+
+export default {
+ components: {
+ FrequentItemsListItem,
+ },
+ mixins: [frequentItemsMixin],
+ props: {
+ items: {
+ type: Array,
+ required: true,
+ },
+ hasSearchQuery: {
+ type: Boolean,
+ required: true,
+ },
+ isFetchFailed: {
+ type: Boolean,
+ required: true,
+ },
+ matcher: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ translations() {
+ return this.getTranslations([
+ 'itemListEmptyMessage',
+ 'itemListErrorMessage',
+ 'searchListEmptyMessage',
+ 'searchListErrorMessage',
+ ]);
+ },
+ isListEmpty() {
+ return this.items.length === 0;
+ },
+ listEmptyMessage() {
+ if (this.hasSearchQuery) {
+ return this.isFetchFailed
+ ? this.translations.searchListErrorMessage
+ : this.translations.searchListEmptyMessage;
+ }
+
+ return this.isFetchFailed
+ ? this.translations.itemListErrorMessage
+ : this.translations.itemListEmptyMessage;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="frequent-items-list-container">
+ <ul class="list-unstyled">
+ <li
+ v-if="isListEmpty"
+ :class="{ 'section-failure': isFetchFailed }"
+ class="section-empty"
+ >
+ {{ listEmptyMessage }}
+ </li>
+ <frequent-items-list-item
+ v-for="item in items"
+ v-else
+ :key="item.id"
+ :item-id="item.id"
+ :item-name="item.name"
+ :namespace="item.namespace"
+ :web-url="item.webUrl"
+ :avatar-url="item.avatarUrl"
+ :matcher="matcher"
+ />
+ </ul>
+ </div>
+</template>
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
new file mode 100644
index 00000000000..1f1665ff7fe
--- /dev/null
+++ b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
@@ -0,0 +1,117 @@
+<script>
+/* eslint-disable vue/require-default-prop, vue/require-prop-types */
+import Identicon from '../../vue_shared/components/identicon.vue';
+
+export default {
+ components: {
+ Identicon,
+ },
+ props: {
+ matcher: {
+ type: String,
+ required: false,
+ },
+ itemId: {
+ type: Number,
+ required: true,
+ },
+ itemName: {
+ type: String,
+ required: true,
+ },
+ namespace: {
+ type: String,
+ required: false,
+ },
+ webUrl: {
+ type: String,
+ required: true,
+ },
+ avatarUrl: {
+ required: true,
+ validator(value) {
+ return value === null || typeof value === 'string';
+ },
+ },
+ },
+ computed: {
+ hasAvatar() {
+ return this.avatarUrl !== null;
+ },
+ highlightedItemName() {
+ if (this.matcher) {
+ const matcherRegEx = new RegExp(this.matcher, 'gi');
+ const matches = this.itemName.match(matcherRegEx);
+
+ if (matches && matches.length > 0) {
+ return this.itemName.replace(matches[0], `<b>${matches[0]}</b>`);
+ }
+ }
+ return this.itemName;
+ },
+ /**
+ * Smartly truncates item namespace by doing two things;
+ * 1. Only include Group names in path by removing item name
+ * 2. Only include first and last group names in the path
+ * when namespace has more than 2 groups present
+ *
+ * First part (removal of item name from namespace) can be
+ * done from backend but doing so involves migration of
+ * existing item namespaces which is not wise thing to do.
+ */
+ truncatedNamespace() {
+ if (!this.namespace) {
+ return null;
+ }
+ 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>
+
+<template>
+ <li class="frequent-items-list-item-container">
+ <a
+ :href="webUrl"
+ class="clearfix"
+ >
+ <div class="frequent-items-item-avatar-container">
+ <img
+ v-if="hasAvatar"
+ :src="avatarUrl"
+ class="avatar s32"
+ />
+ <identicon
+ v-else
+ :entity-id="itemId"
+ :entity-name="itemName"
+ size-class="s32"
+ />
+ </div>
+ <div class="frequent-items-item-metadata-container">
+ <div
+ :title="itemName"
+ class="frequent-items-item-title"
+ v-html="highlightedItemName"
+ >
+ </div>
+ <div
+ v-if="truncatedNamespace"
+ :title="namespace"
+ class="frequent-items-item-namespace"
+ >
+ {{ truncatedNamespace }}
+ </div>
+ </div>
+ </a>
+ </li>
+</template>
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_mixin.js b/app/assets/javascripts/frequent_items/components/frequent_items_mixin.js
new file mode 100644
index 00000000000..704dc83ca8e
--- /dev/null
+++ b/app/assets/javascripts/frequent_items/components/frequent_items_mixin.js
@@ -0,0 +1,23 @@
+import { TRANSLATION_KEYS } from '../constants';
+
+export default {
+ props: {
+ namespace: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ getTranslations(keys) {
+ const translationStrings = keys.reduce(
+ (acc, key) => ({
+ ...acc,
+ [key]: TRANSLATION_KEYS[this.namespace][key],
+ }),
+ {},
+ );
+
+ return translationStrings;
+ },
+ },
+};
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue b/app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue
new file mode 100644
index 00000000000..a6a265eb3fd
--- /dev/null
+++ b/app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue
@@ -0,0 +1,55 @@
+<script>
+import _ from 'underscore';
+import { mapActions } from 'vuex';
+import eventHub from '../event_hub';
+import frequentItemsMixin from './frequent_items_mixin';
+
+export default {
+ mixins: [frequentItemsMixin],
+ data() {
+ return {
+ searchQuery: '',
+ };
+ },
+ computed: {
+ translations() {
+ return this.getTranslations(['searchInputPlaceholder']);
+ },
+ },
+ watch: {
+ searchQuery: _.debounce(function debounceSearchQuery() {
+ this.setSearchQuery(this.searchQuery);
+ }, 500),
+ },
+ mounted() {
+ eventHub.$on(`${this.namespace}-dropdownOpen`, this.setFocus);
+ },
+ beforeDestroy() {
+ eventHub.$off(`${this.namespace}-dropdownOpen`, this.setFocus);
+ },
+ methods: {
+ ...mapActions(['setSearchQuery']),
+ setFocus() {
+ this.$refs.search.focus();
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="search-input-container d-none d-sm-block">
+ <input
+ ref="search"
+ v-model="searchQuery"
+ :placeholder="translations.searchInputPlaceholder"
+ type="search"
+ class="form-control"
+ />
+ <i
+ v-if="!searchQuery"
+ class="search-icon fa fa-fw fa-search"
+ aria-hidden="true"
+ >
+ </i>
+ </div>
+</template>
diff --git a/app/assets/javascripts/frequent_items/constants.js b/app/assets/javascripts/frequent_items/constants.js
new file mode 100644
index 00000000000..9bc17f5ef4f
--- /dev/null
+++ b/app/assets/javascripts/frequent_items/constants.js
@@ -0,0 +1,38 @@
+import { s__ } from '~/locale';
+
+export const FREQUENT_ITEMS = {
+ MAX_COUNT: 20,
+ LIST_COUNT_DESKTOP: 5,
+ LIST_COUNT_MOBILE: 3,
+ ELIGIBLE_FREQUENCY: 3,
+};
+
+export const HOUR_IN_MS = 3600000;
+
+export const STORAGE_KEY = {
+ projects: 'frequent-projects',
+ groups: 'frequent-groups',
+};
+
+export const TRANSLATION_KEYS = {
+ projects: {
+ loadingMessage: s__('ProjectsDropdown|Loading projects'),
+ header: s__('ProjectsDropdown|Frequently visited'),
+ itemListErrorMessage: s__(
+ 'ProjectsDropdown|This feature requires browser localStorage support',
+ ),
+ itemListEmptyMessage: s__('ProjectsDropdown|Projects you visit often will appear here'),
+ searchListErrorMessage: s__('ProjectsDropdown|Something went wrong on our end.'),
+ searchListEmptyMessage: s__('ProjectsDropdown|Sorry, no projects matched your search'),
+ searchInputPlaceholder: s__('ProjectsDropdown|Search your projects'),
+ },
+ groups: {
+ loadingMessage: s__('GroupsDropdown|Loading groups'),
+ header: s__('GroupsDropdown|Frequently visited'),
+ itemListErrorMessage: s__('GroupsDropdown|This feature requires browser localStorage support'),
+ itemListEmptyMessage: s__('GroupsDropdown|Groups you visit often will appear here'),
+ searchListErrorMessage: s__('GroupsDropdown|Something went wrong on our end.'),
+ searchListEmptyMessage: s__('GroupsDropdown|Sorry, no groups matched your search'),
+ searchInputPlaceholder: s__('GroupsDropdown|Search your groups'),
+ },
+};
diff --git a/app/assets/javascripts/projects_dropdown/event_hub.js b/app/assets/javascripts/frequent_items/event_hub.js
index 0948c2e5352..0948c2e5352 100644
--- a/app/assets/javascripts/projects_dropdown/event_hub.js
+++ b/app/assets/javascripts/frequent_items/event_hub.js
diff --git a/app/assets/javascripts/frequent_items/index.js b/app/assets/javascripts/frequent_items/index.js
new file mode 100644
index 00000000000..5157ff211dc
--- /dev/null
+++ b/app/assets/javascripts/frequent_items/index.js
@@ -0,0 +1,69 @@
+import $ from 'jquery';
+import Vue from 'vue';
+import Translate from '~/vue_shared/translate';
+import eventHub from '~/frequent_items/event_hub';
+import frequentItems from './components/app.vue';
+
+Vue.use(Translate);
+
+const frequentItemDropdowns = [
+ {
+ namespace: 'projects',
+ key: 'project',
+ },
+ {
+ namespace: 'groups',
+ key: 'group',
+ },
+];
+
+document.addEventListener('DOMContentLoaded', () => {
+ frequentItemDropdowns.forEach(dropdown => {
+ const { namespace, key } = dropdown;
+ const el = document.getElementById(`js-${namespace}-dropdown`);
+ const navEl = document.getElementById(`nav-${namespace}-dropdown`);
+
+ // Don't do anything if element doesn't exist (No groups dropdown)
+ // This is for when the user accesses GitLab without logging in
+ if (!el || !navEl) {
+ return;
+ }
+
+ $(navEl).on('shown.bs.dropdown', () => {
+ eventHub.$emit(`${namespace}-dropdownOpen`);
+ });
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ components: {
+ frequentItems,
+ },
+ data() {
+ const { dataset } = this.$options.el;
+ const item = {
+ id: Number(dataset[`${key}Id`]),
+ name: dataset[`${key}Name`],
+ namespace: dataset[`${key}Namespace`],
+ webUrl: dataset[`${key}WebUrl`],
+ avatarUrl: dataset[`${key}AvatarUrl`] || null,
+ lastAccessedOn: Date.now(),
+ };
+
+ return {
+ currentUserName: dataset.userName,
+ currentItem: item,
+ };
+ },
+ render(createElement) {
+ return createElement('frequent-items', {
+ props: {
+ namespace,
+ currentUserName: this.currentUserName,
+ currentItem: this.currentItem,
+ },
+ });
+ },
+ });
+ });
+});
diff --git a/app/assets/javascripts/frequent_items/store/actions.js b/app/assets/javascripts/frequent_items/store/actions.js
new file mode 100644
index 00000000000..3dd89a82a42
--- /dev/null
+++ b/app/assets/javascripts/frequent_items/store/actions.js
@@ -0,0 +1,81 @@
+import Api from '~/api';
+import AccessorUtilities from '~/lib/utils/accessor';
+import * as types from './mutation_types';
+import { getTopFrequentItems } from '../utils';
+
+export const setNamespace = ({ commit }, namespace) => {
+ commit(types.SET_NAMESPACE, namespace);
+};
+
+export const setStorageKey = ({ commit }, key) => {
+ commit(types.SET_STORAGE_KEY, key);
+};
+
+export const requestFrequentItems = ({ commit }) => {
+ commit(types.REQUEST_FREQUENT_ITEMS);
+};
+export const receiveFrequentItemsSuccess = ({ commit }, data) => {
+ commit(types.RECEIVE_FREQUENT_ITEMS_SUCCESS, data);
+};
+export const receiveFrequentItemsError = ({ commit }) => {
+ commit(types.RECEIVE_FREQUENT_ITEMS_ERROR);
+};
+
+export const fetchFrequentItems = ({ state, dispatch }) => {
+ dispatch('requestFrequentItems');
+
+ if (AccessorUtilities.isLocalStorageAccessSafe()) {
+ const storedFrequentItems = JSON.parse(localStorage.getItem(state.storageKey));
+
+ dispatch(
+ 'receiveFrequentItemsSuccess',
+ !storedFrequentItems ? [] : getTopFrequentItems(storedFrequentItems),
+ );
+ } else {
+ dispatch('receiveFrequentItemsError');
+ }
+};
+
+export const requestSearchedItems = ({ commit }) => {
+ commit(types.REQUEST_SEARCHED_ITEMS);
+};
+export const receiveSearchedItemsSuccess = ({ commit }, data) => {
+ commit(types.RECEIVE_SEARCHED_ITEMS_SUCCESS, data);
+};
+export const receiveSearchedItemsError = ({ commit }) => {
+ commit(types.RECEIVE_SEARCHED_ITEMS_ERROR);
+};
+export const fetchSearchedItems = ({ state, dispatch }, searchQuery) => {
+ dispatch('requestSearchedItems');
+
+ const params = {
+ simple: true,
+ per_page: 20,
+ membership: !!gon.current_user_id,
+ };
+
+ if (state.namespace === 'projects') {
+ params.order_by = 'last_activity_at';
+ }
+
+ return Api[state.namespace](searchQuery, params)
+ .then(results => {
+ dispatch('receiveSearchedItemsSuccess', results);
+ })
+ .catch(() => {
+ dispatch('receiveSearchedItemsError');
+ });
+};
+
+export const setSearchQuery = ({ commit, dispatch }, query) => {
+ commit(types.SET_SEARCH_QUERY, query);
+
+ if (query) {
+ dispatch('fetchSearchedItems', query);
+ } else {
+ dispatch('fetchFrequentItems');
+ }
+};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/frequent_items/store/getters.js b/app/assets/javascripts/frequent_items/store/getters.js
new file mode 100644
index 00000000000..00165db6684
--- /dev/null
+++ b/app/assets/javascripts/frequent_items/store/getters.js
@@ -0,0 +1,4 @@
+export const hasSearchQuery = state => state.searchQuery !== '';
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/frequent_items/store/index.js b/app/assets/javascripts/frequent_items/store/index.js
new file mode 100644
index 00000000000..ece9e6419dd
--- /dev/null
+++ b/app/assets/javascripts/frequent_items/store/index.js
@@ -0,0 +1,16 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import * as actions from './actions';
+import * as getters from './getters';
+import mutations from './mutations';
+import state from './state';
+
+Vue.use(Vuex);
+
+export default () =>
+ new Vuex.Store({
+ actions,
+ getters,
+ mutations,
+ state: state(),
+ });
diff --git a/app/assets/javascripts/frequent_items/store/mutation_types.js b/app/assets/javascripts/frequent_items/store/mutation_types.js
new file mode 100644
index 00000000000..cbe2c9401ad
--- /dev/null
+++ b/app/assets/javascripts/frequent_items/store/mutation_types.js
@@ -0,0 +1,9 @@
+export const SET_NAMESPACE = 'SET_NAMESPACE';
+export const SET_STORAGE_KEY = 'SET_STORAGE_KEY';
+export const SET_SEARCH_QUERY = 'SET_SEARCH_QUERY';
+export const REQUEST_FREQUENT_ITEMS = 'REQUEST_FREQUENT_ITEMS';
+export const RECEIVE_FREQUENT_ITEMS_SUCCESS = 'RECEIVE_FREQUENT_ITEMS_SUCCESS';
+export const RECEIVE_FREQUENT_ITEMS_ERROR = 'RECEIVE_FREQUENT_ITEMS_ERROR';
+export const REQUEST_SEARCHED_ITEMS = 'REQUEST_SEARCHED_ITEMS';
+export const RECEIVE_SEARCHED_ITEMS_SUCCESS = 'RECEIVE_SEARCHED_ITEMS_SUCCESS';
+export const RECEIVE_SEARCHED_ITEMS_ERROR = 'RECEIVE_SEARCHED_ITEMS_ERROR';
diff --git a/app/assets/javascripts/frequent_items/store/mutations.js b/app/assets/javascripts/frequent_items/store/mutations.js
new file mode 100644
index 00000000000..41b660a243f
--- /dev/null
+++ b/app/assets/javascripts/frequent_items/store/mutations.js
@@ -0,0 +1,71 @@
+import * as types from './mutation_types';
+
+export default {
+ [types.SET_NAMESPACE](state, namespace) {
+ Object.assign(state, {
+ namespace,
+ });
+ },
+ [types.SET_STORAGE_KEY](state, storageKey) {
+ Object.assign(state, {
+ storageKey,
+ });
+ },
+ [types.SET_SEARCH_QUERY](state, searchQuery) {
+ const hasSearchQuery = searchQuery !== '';
+
+ Object.assign(state, {
+ searchQuery,
+ isLoadingItems: true,
+ hasSearchQuery,
+ });
+ },
+ [types.REQUEST_FREQUENT_ITEMS](state) {
+ Object.assign(state, {
+ isLoadingItems: true,
+ hasSearchQuery: false,
+ });
+ },
+ [types.RECEIVE_FREQUENT_ITEMS_SUCCESS](state, rawItems) {
+ Object.assign(state, {
+ items: rawItems,
+ isLoadingItems: false,
+ hasSearchQuery: false,
+ isFetchFailed: false,
+ });
+ },
+ [types.RECEIVE_FREQUENT_ITEMS_ERROR](state) {
+ Object.assign(state, {
+ isLoadingItems: false,
+ hasSearchQuery: false,
+ isFetchFailed: true,
+ });
+ },
+ [types.REQUEST_SEARCHED_ITEMS](state) {
+ Object.assign(state, {
+ isLoadingItems: true,
+ hasSearchQuery: true,
+ });
+ },
+ [types.RECEIVE_SEARCHED_ITEMS_SUCCESS](state, rawItems) {
+ Object.assign(state, {
+ items: rawItems.map(rawItem => ({
+ id: rawItem.id,
+ name: rawItem.name,
+ namespace: rawItem.name_with_namespace || rawItem.full_name,
+ webUrl: rawItem.web_url,
+ avatarUrl: rawItem.avatar_url,
+ })),
+ isLoadingItems: false,
+ hasSearchQuery: true,
+ isFetchFailed: false,
+ });
+ },
+ [types.RECEIVE_SEARCHED_ITEMS_ERROR](state) {
+ Object.assign(state, {
+ isLoadingItems: false,
+ hasSearchQuery: true,
+ isFetchFailed: true,
+ });
+ },
+};
diff --git a/app/assets/javascripts/frequent_items/store/state.js b/app/assets/javascripts/frequent_items/store/state.js
new file mode 100644
index 00000000000..75b04febee4
--- /dev/null
+++ b/app/assets/javascripts/frequent_items/store/state.js
@@ -0,0 +1,8 @@
+export default () => ({
+ namespace: '',
+ storageKey: '',
+ searchQuery: '',
+ isLoadingItems: false,
+ isFetchFailed: false,
+ items: [],
+});
diff --git a/app/assets/javascripts/frequent_items/utils.js b/app/assets/javascripts/frequent_items/utils.js
new file mode 100644
index 00000000000..aba692e4b99
--- /dev/null
+++ b/app/assets/javascripts/frequent_items/utils.js
@@ -0,0 +1,49 @@
+import _ from 'underscore';
+import bp from '~/breakpoints';
+import { FREQUENT_ITEMS, HOUR_IN_MS } from './constants';
+
+export const isMobile = () => {
+ const screenSize = bp.getBreakpointSize();
+
+ return screenSize === 'sm' || screenSize === 'xs';
+};
+
+export const getTopFrequentItems = items => {
+ if (!items) {
+ return [];
+ }
+ const frequentItemsCount = isMobile()
+ ? FREQUENT_ITEMS.LIST_COUNT_MOBILE
+ : FREQUENT_ITEMS.LIST_COUNT_DESKTOP;
+
+ const frequentItems = items.filter(item => item.frequency >= FREQUENT_ITEMS.ELIGIBLE_FREQUENCY);
+
+ if (!frequentItems || frequentItems.length === 0) {
+ return [];
+ }
+
+ frequentItems.sort((itemA, itemB) => {
+ // Sort all frequent items in decending order of frequency
+ // and then by lastAccessedOn with recent most first
+ if (itemA.frequency !== itemB.frequency) {
+ return itemB.frequency - itemA.frequency;
+ } else if (itemA.lastAccessedOn !== itemB.lastAccessedOn) {
+ return itemB.lastAccessedOn - itemA.lastAccessedOn;
+ }
+
+ return 0;
+ });
+
+ return _.first(frequentItems, frequentItemsCount);
+};
+
+export const updateExistingFrequentItem = (frequentItem, item) => {
+ const accessedOverHourAgo =
+ Math.abs(item.lastAccessedOn - frequentItem.lastAccessedOn) / HOUR_IN_MS > 1;
+
+ return {
+ ...item,
+ frequency: accessedOverHourAgo ? frequentItem.frequency + 1 : frequentItem.frequency,
+ lastAccessedOn: accessedOverHourAgo ? Date.now() : frequentItem.lastAccessedOn,
+ };
+};
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 9de57db48fd..73b2cd0b2c7 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -7,6 +7,16 @@ function sanitize(str) {
return str.replace(/<(?:.|\n)*?>/gm, '');
}
+export const defaultAutocompleteConfig = {
+ emojis: true,
+ members: true,
+ issues: true,
+ mergeRequests: true,
+ epics: true,
+ milestones: true,
+ labels: true,
+};
+
class GfmAutoComplete {
constructor(dataSources) {
this.dataSources = dataSources || {};
@@ -14,14 +24,7 @@ class GfmAutoComplete {
this.isLoadingData = {};
}
- setup(input, enableMap = {
- emojis: true,
- members: true,
- issues: true,
- milestones: true,
- mergeRequests: true,
- labels: true,
- }) {
+ setup(input, enableMap = defaultAutocompleteConfig) {
// Add GFM auto-completion to all input fields, that accept GFM input.
this.input = input || $('.js-gfm-input');
this.enableMap = enableMap;
@@ -77,7 +80,7 @@ class GfmAutoComplete {
let tpl = '/${name} ';
let referencePrefix = null;
if (value.params.length > 0) {
- referencePrefix = value.params[0][0];
+ [[referencePrefix]] = value.params;
if (/^[@%~]/.test(referencePrefix)) {
tpl += '<%- referencePrefix %>';
}
@@ -455,7 +458,7 @@ class GfmAutoComplete {
static isLoading(data) {
let dataToInspect = data;
if (data && data.length > 0) {
- dataToInspect = data[0];
+ [dataToInspect] = data;
}
const loadingState = GfmAutoComplete.defaultLoadingData[0];
@@ -490,6 +493,7 @@ GfmAutoComplete.atTypeMap = {
'@': 'members',
'#': 'issues',
'!': 'mergeRequests',
+ '&': 'epics',
'~': 'labels',
'%': 'milestones',
'/': 'commands',
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index fa48d7d1915..8d231e6c405 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -1,4 +1,4 @@
-/* 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 */
+/* eslint-disable func-names, no-underscore-dangle, no-var, one-var, one-var-declaration-per-line, 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 */
/* global fuzzaldrinPlus */
import $ from 'jquery';
@@ -374,7 +374,7 @@ GitLabDropdown = (function() {
$relatedTarget = $(e.relatedTarget);
$dropdownMenu = $relatedTarget.closest('.dropdown-menu');
if ($dropdownMenu.length === 0) {
- return _this.dropdown.removeClass('open');
+ return _this.dropdown.removeClass('show');
}
}
};
@@ -602,14 +602,18 @@ GitLabDropdown = (function() {
var selector;
selector = '.dropdown-content';
if (this.dropdown.find(".dropdown-toggle-page").length) {
- selector = ".dropdown-page-one .dropdown-content";
+ if (this.options.containerSelector) {
+ selector = this.options.containerSelector;
+ } else {
+ selector = '.dropdown-page-one .dropdown-content';
+ }
}
return $(selector, this.dropdown).empty();
};
GitLabDropdown.prototype.renderItem = function(data, group, index) {
- var field, fieldName, html, selected, text, url, value, rowHidden;
+ var field, html, selected, text, url, value, rowHidden;
if (!this.options.renderRow) {
value = this.options.id ? this.options.id(data) : data.id;
@@ -647,7 +651,7 @@ GitLabDropdown = (function() {
html = this.options.renderRow.call(this.options, data, this);
} else {
if (!selected) {
- fieldName = this.options.fieldName;
+ const { fieldName } = this.options;
if (value) {
field = this.dropdown.parent().find(`input[name='${fieldName}'][value='${value}']`);
@@ -701,7 +705,8 @@ GitLabDropdown = (function() {
GitLabDropdown.prototype.highlightTextMatches = function(text, term) {
const occurrences = fuzzaldrinPlus.match(text, term);
- const indexOf = [].indexOf;
+ const { indexOf } = [];
+
return text.split('').map(function(character, i) {
if (indexOf.call(occurrences, i) !== -1) {
return "<b>" + character + "</b>";
@@ -717,9 +722,9 @@ GitLabDropdown = (function() {
};
GitLabDropdown.prototype.rowClicked = function(el) {
- var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value, isMarking;
+ var field, groupName, isInput, selectedIndex, selectedObject, value, isMarking;
- fieldName = this.options.fieldName;
+ const { fieldName } = this.options;
isInput = $(this.el).is('input');
if (this.renderedData) {
groupName = el.data('group');
@@ -801,7 +806,7 @@ GitLabDropdown = (function() {
if (this.options.filterable) {
const initialScrollTop = $(window).scrollTop();
- if (this.dropdown.is('.open')) {
+ if (this.dropdown.is('.show') && !this.filterInput.is(':focus')) {
this.filterInput.focus();
}
diff --git a/app/assets/javascripts/gl_field_error.js b/app/assets/javascripts/gl_field_error.js
index 972b2252acb..87c6e37b9fb 100644
--- a/app/assets/javascripts/gl_field_error.js
+++ b/app/assets/javascripts/gl_field_error.js
@@ -62,7 +62,7 @@ export default class GlFieldError {
this.inputDomElement = this.inputElement.get(0);
this.form = formErrors;
this.errorMessage = this.inputElement.attr('title') || 'This field is required.';
- this.fieldErrorElement = $(`<p class='${errorMessageClass} hide'>${this.errorMessage}</p>`);
+ this.fieldErrorElement = $(`<p class='${errorMessageClass} hidden'>${this.errorMessage}</p>`);
this.state = {
valid: false,
@@ -146,8 +146,8 @@ export default class GlFieldError {
renderInvalid() {
this.inputElement.addClass(inputErrorClass);
- this.scopedSiblings.hide();
- return this.fieldErrorElement.show();
+ this.scopedSiblings.addClass('hidden');
+ return this.fieldErrorElement.removeClass('hidden');
}
renderClear() {
@@ -157,7 +157,7 @@ export default class GlFieldError {
this.accessCurrentValue(trimmedInput);
}
this.inputElement.removeClass(inputErrorClass);
- this.scopedSiblings.hide();
- this.fieldErrorElement.hide();
+ this.scopedSiblings.addClass('hidden');
+ this.fieldErrorElement.addClass('hidden');
}
}
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
index 9f5eba353d7..c74de7ac34d 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -1,14 +1,21 @@
import $ from 'jquery';
import autosize from 'autosize';
-import GfmAutoComplete from './gfm_auto_complete';
+import GfmAutoComplete, * as GFMConfig from './gfm_auto_complete';
import dropzoneInput from './dropzone_input';
import { addMarkdownListeners, removeMarkdownListeners } from './lib/utils/text_markdown';
export default class GLForm {
- constructor(form, enableGFM = false) {
+ constructor(form, enableGFM = {}) {
this.form = form;
this.textarea = this.form.find('textarea.js-gfm-input');
- this.enableGFM = enableGFM;
+ this.enableGFM = Object.assign({}, GFMConfig.defaultAutocompleteConfig, enableGFM);
+ // Disable autocomplete for keywords which do not have dataSources available
+ const dataSources = (gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources) || {};
+ Object.keys(this.enableGFM).forEach(item => {
+ if (item !== 'emojis') {
+ this.enableGFM[item] = !!dataSources[item];
+ }
+ });
// Before we start, we should clean up any previous data for this form
this.destroy();
// Setup the form
@@ -34,14 +41,7 @@ export default class GLForm {
// 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,
- });
+ this.autoComplete.setup(this.form.find('.js-gfm-input'), this.enableGFM);
dropzoneInput(this.form);
autosize(this.textarea);
}
diff --git a/app/assets/javascripts/group_label_subscription.js b/app/assets/javascripts/group_label_subscription.js
index 5648cb9a888..d33e3a37580 100644
--- a/app/assets/javascripts/group_label_subscription.js
+++ b/app/assets/javascripts/group_label_subscription.js
@@ -1,7 +1,12 @@
import $ from 'jquery';
+import { __ } from '~/locale';
import axios from './lib/utils/axios_utils';
import flash from './flash';
-import { __ } from './locale';
+
+const tooltipTitles = {
+ group: __('Unsubscribe at group level'),
+ project: __('Unsubscribe at project level'),
+};
export default class GroupLabelSubscription {
constructor(container) {
@@ -35,6 +40,7 @@ export default class GroupLabelSubscription {
this.$unsubscribeButtons.attr('data-url', url);
axios.post(url)
+ .then(() => GroupLabelSubscription.setNewTooltip($btn))
.then(() => this.toggleSubscriptionButtons())
.catch(() => flash(__('There was an error when subscribing to this label.')));
}
@@ -44,4 +50,14 @@ export default class GroupLabelSubscription {
this.$subscribeButtons.toggleClass('hidden');
this.$unsubscribeButtons.toggleClass('hidden');
}
+
+ static setNewTooltip($button) {
+ if (!$button.hasClass('js-subscribe-button')) return;
+
+ const type = $button.hasClass('js-group-level') ? 'group' : 'project';
+ const newTitle = tooltipTitles[type];
+
+ $('.js-unsubscribe-button', $button.closest('.label-actions-list'))
+ .tooltip('hide').attr('title', newTitle).tooltip('_fixTitle');
+ }
}
diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue
index 22eb7bd44c5..b0765747a36 100644
--- a/app/assets/javascripts/groups/components/app.vue
+++ b/app/assets/javascripts/groups/components/app.vue
@@ -148,7 +148,6 @@ export default {
if (!parentGroup.isOpen) {
if (parentGroup.children.length === 0) {
parentGroup.isChildrenLoading = true;
- // eslint-disable-next-line promise/catch-or-return
this.fetchGroups({
parentId: parentGroup.id,
})
@@ -216,10 +215,10 @@ export default {
<template>
<div>
<loading-icon
- class="loading-animation prepend-top-20"
- size="2"
v-if="isLoading"
:label="s__('GroupsTree|Loading groups')"
+ class="loading-animation prepend-top-20"
+ size="2"
/>
<groups-component
v-if="!isLoading"
@@ -230,10 +229,10 @@ export default {
/>
<deprecated-modal
v-show="showModal"
- kind="warning"
:primary-button-label="__('Leave')"
:title="__('Are you sure?')"
:text="groupLeaveConfirmationMessage"
+ kind="warning"
@cancel="hideLeaveGroupModal"
@submit="leaveGroup"
/>
diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue
index 764b130fdb8..efbf2e3a295 100644
--- a/app/assets/javascripts/groups/components/group_item.vue
+++ b/app/assets/javascripts/groups/components/group_item.vue
@@ -71,14 +71,14 @@ export default {
<template>
<li
- @click.stop="onClickRowGroup"
:id="groupDomId"
:class="rowClass"
class="group-row"
+ @click.stop="onClickRowGroup"
>
<div
- class="group-row-contents"
- :class="{ 'project-row-contents': !isGroup }">
+ :class="{ 'project-row-contents': !isGroup }"
+ class="group-row-contents">
<item-actions
v-if="isGroup"
:group="group"
@@ -99,8 +99,8 @@ export default {
/>
</div>
<div
- class="avatar-container prepend-top-8 prepend-left-5 s24 hidden-xs"
:class="{ 'content-loading': group.isChildrenLoading }"
+ class="avatar-container prepend-top-8 prepend-left-5 s24 d-none d-sm-block"
>
<a
:href="group.relativePath"
@@ -108,14 +108,14 @@ export default {
>
<img
v-if="hasAvatar"
- class="avatar s24"
:src="group.avatarUrl"
+ class="avatar s24"
/>
<identicon
v-else
- size-class="s24"
:entity-id="group.id"
:entity-name="group.name"
+ size-class="s24"
/>
</a>
</div>
diff --git a/app/assets/javascripts/groups/components/item_actions.vue b/app/assets/javascripts/groups/components/item_actions.vue
index 87065b3d6e3..24eec4901ec 100644
--- a/app/assets/javascripts/groups/components/item_actions.vue
+++ b/app/assets/javascripts/groups/components/item_actions.vue
@@ -54,13 +54,13 @@ export default {
<a
v-tooltip
v-if="group.canLeave"
- @click.prevent="onLeaveGroup"
:href="group.leavePath"
:title="leaveBtnTitle"
:aria-label="leaveBtnTitle"
data-container="body"
data-placement="bottom"
- class="leave-group btn no-expand">
+ class="leave-group btn no-expand"
+ @click.prevent="onLeaveGroup">
<icon name="leave"/>
</a>
</div>
diff --git a/app/assets/javascripts/groups/components/item_stats.vue b/app/assets/javascripts/groups/components/item_stats.vue
index 168b4e4af2c..87ab5480c15 100644
--- a/app/assets/javascripts/groups/components/item_stats.vue
+++ b/app/assets/javascripts/groups/components/item_stats.vue
@@ -45,44 +45,44 @@
<div class="stats">
<item-stats-value
v-if="isGroup"
- css-class="number-subgroups"
- icon-name="folder"
:title="__('Subgroups')"
:value="item.subgroupCount"
+ css-class="number-subgroups"
+ icon-name="folder"
/>
<item-stats-value
v-if="isGroup"
- css-class="number-projects"
- icon-name="bookmark"
:title="__('Projects')"
:value="item.projectCount"
+ css-class="number-projects"
+ icon-name="bookmark"
/>
<item-stats-value
v-if="isGroup"
- css-class="number-users"
- icon-name="users"
:title="__('Members')"
:value="item.memberCount"
+ css-class="number-users"
+ icon-name="users"
/>
<item-stats-value
v-if="isProject"
+ :value="item.starCount"
css-class="project-stars"
icon-name="star"
- :value="item.starCount"
/>
<item-stats-value
- css-class="item-visibility"
- tooltip-placement="left"
:icon-name="visibilityIcon"
:title="visibilityTooltip"
+ css-class="item-visibility"
+ tooltip-placement="left"
/>
<div
- class="last-updated"
v-if="isProject"
+ class="last-updated"
>
<time-ago-tooltip
- tooltip-placement="bottom"
:time="item.updatedAt"
+ tooltip-placement="bottom"
/>
</div>
</div>
diff --git a/app/assets/javascripts/groups/components/item_stats_value.vue b/app/assets/javascripts/groups/components/item_stats_value.vue
index 4d86ac8023c..ef9f2bca76c 100644
--- a/app/assets/javascripts/groups/components/item_stats_value.vue
+++ b/app/assets/javascripts/groups/components/item_stats_value.vue
@@ -52,10 +52,10 @@
<template>
<span
v-tooltip
- data-container="body"
:data-placement="tooltipPlacement"
:class="cssClass"
:title="title"
+ data-container="body"
>
<icon :name="iconName" />
<span
diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js
index 57eaac72906..83a9008a94b 100644
--- a/app/assets/javascripts/groups/index.js
+++ b/app/assets/javascripts/groups/index.js
@@ -29,7 +29,7 @@ export default () => {
groupsApp,
},
data() {
- const dataset = this.$options.el.dataset;
+ const { dataset } = this.$options.el;
const hideProjects = dataset.hideProjects === 'true';
const store = new GroupsStore(hideProjects);
const service = new GroupsService(dataset.endpoint);
@@ -42,7 +42,7 @@ export default () => {
};
},
beforeMount() {
- const dataset = this.$options.el.dataset;
+ const { dataset } = this.$options.el;
let groupFilterList = null;
const form = document.querySelector(dataset.formSel);
const filter = document.querySelector(dataset.filterSel);
diff --git a/app/assets/javascripts/ide/components/activity_bar.vue b/app/assets/javascripts/ide/components/activity_bar.vue
index 05dbc1410de..62697e0ecc3 100644
--- a/app/assets/javascripts/ide/components/activity_bar.vue
+++ b/app/assets/javascripts/ide/components/activity_bar.vue
@@ -1,4 +1,5 @@
<script>
+import $ from 'jquery';
import { mapActions, mapGetters, mapState } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
@@ -20,6 +21,13 @@ export default {
},
methods: {
...mapActions(['updateActivityBarView']),
+ changedActivityView(e, view) {
+ e.currentTarget.blur();
+
+ this.updateActivityBarView(view);
+
+ $(e.currentTarget).tooltip('hide');
+ },
},
activityBarViews,
};
@@ -31,12 +39,12 @@ export default {
<li v-once>
<a
v-tooltip
- data-container="body"
- data-placement="right"
:href="goBackUrl"
- class="ide-sidebar-link"
:title="s__('IDE|Go back')"
:aria-label="s__('IDE|Go back')"
+ data-container="body"
+ data-placement="right"
+ class="ide-sidebar-link"
>
<icon
:size="16"
@@ -47,16 +55,16 @@ export default {
<li>
<button
v-tooltip
- data-container="body"
- data-placement="right"
- type="button"
- class="ide-sidebar-link js-ide-edit-mode"
:class="{
active: currentActivityView === $options.activityBarViews.edit
}"
- @click.prevent="updateActivityBarView($options.activityBarViews.edit)"
:title="s__('IDE|Edit')"
:aria-label="s__('IDE|Edit')"
+ data-container="body"
+ data-placement="right"
+ type="button"
+ class="ide-sidebar-link js-ide-edit-mode"
+ @click.prevent="changedActivityView($event, $options.activityBarViews.edit)"
>
<icon
name="code"
@@ -66,16 +74,16 @@ export default {
<li>
<button
v-tooltip
- data-container="body"
- data-placement="right"
- type="button"
- class="ide-sidebar-link js-ide-review-mode"
:class="{
active: currentActivityView === $options.activityBarViews.review
}"
- @click.prevent="updateActivityBarView($options.activityBarViews.review)"
:title="s__('IDE|Review')"
:aria-label="s__('IDE|Review')"
+ data-container="body"
+ data-placement="right"
+ type="button"
+ class="ide-sidebar-link js-ide-review-mode"
+ @click.prevent="changedActivityView($event, $options.activityBarViews.review)"
>
<icon
name="file-modified"
@@ -85,16 +93,16 @@ export default {
<li v-show="hasChanges">
<button
v-tooltip
- data-container="body"
- data-placement="right"
- type="button"
- class="ide-sidebar-link js-ide-commit-mode"
:class="{
active: currentActivityView === $options.activityBarViews.commit
}"
- @click.prevent="updateActivityBarView($options.activityBarViews.commit)"
:title="s__('IDE|Commit')"
:aria-label="s__('IDE|Commit')"
+ data-container="body"
+ data-placement="right"
+ type="button"
+ class="ide-sidebar-link js-ide-commit-mode"
+ @click.prevent="changedActivityView($event, $options.activityBarViews.commit)"
>
<icon
name="commit"
diff --git a/app/assets/javascripts/ide/components/changed_file_icon.vue b/app/assets/javascripts/ide/components/changed_file_icon.vue
index 1cec84706fc..a4e06bbbe3c 100644
--- a/app/assets/javascripts/ide/components/changed_file_icon.vue
+++ b/app/assets/javascripts/ide/components/changed_file_icon.vue
@@ -43,7 +43,7 @@ export default {
return `${this.changedIcon}-solid`;
},
changedIconClass() {
- return `multi-${this.changedIcon} pull-left`;
+ return `multi-${this.changedIcon} float-left`;
},
tooltipTitle() {
if (!this.showTooltip) return undefined;
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
index b4f3778d946..eb7cb9745ec 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
@@ -10,7 +10,7 @@ export default {
},
computed: {
...mapState(['currentBranchId', 'changedFiles', 'stagedFiles']),
- ...mapGetters(['currentProject']),
+ ...mapGetters(['currentProject', 'currentBranch']),
commitToCurrentBranchText() {
return sprintf(
__('Commit to %{branchName} branch'),
@@ -22,17 +22,30 @@ export default {
return this.changedFiles.length > 0 && this.stagedFiles.length > 0;
},
},
+ watch: {
+ disableMergeRequestRadio() {
+ this.updateSelectedCommitAction();
+ },
+ },
mounted() {
- if (this.disableMergeRequestRadio) {
- this.updateCommitAction(consts.COMMIT_TO_CURRENT_BRANCH);
- }
+ this.updateSelectedCommitAction();
},
methods: {
...mapActions('commit', ['updateCommitAction']),
+ updateSelectedCommitAction() {
+ if (this.currentBranch && !this.currentBranch.can_push) {
+ this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH);
+ } else if (this.disableMergeRequestRadio) {
+ this.updateCommitAction(consts.COMMIT_TO_CURRENT_BRANCH);
+ }
+ },
},
commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH,
commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH,
commitToNewBranchMR: consts.COMMIT_TO_NEW_BRANCH_MR,
+ currentBranchPermissionsTooltip: __(
+ "This option is disabled as you don't have write permissions for the current branch",
+ ),
};
</script>
@@ -40,9 +53,11 @@ export default {
<div class="append-bottom-15 ide-commit-radios">
<radio-group
:value="$options.commitToCurrentBranch"
- :checked="true"
+ :disabled="currentBranch && !currentBranch.can_push"
+ :title="$options.currentBranchPermissionsTooltip"
>
<span
+ class="ide-radio-label"
v-html="commitToCurrentBranchText"
>
</span>
@@ -56,6 +71,7 @@ export default {
v-if="currentProject.merge_requests_enabled"
:value="$options.commitToNewBranchMR"
:label="__('Create a new branch and merge request')"
+ :title="__('This option is disabled while you still have unstaged changes')"
:show-input="true"
:disabled="disableMergeRequestRadio"
/>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
index 81961fe3c57..ee8eb206980 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/form.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
@@ -24,7 +24,7 @@ export default {
...mapState(['changedFiles', 'stagedFiles', 'currentActivityView', 'lastCommitMsg']),
...mapState('commit', ['commitMessage', 'submitCommitLoading']),
...mapGetters(['hasChanges']),
- ...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']),
+ ...mapGetters('commit', ['discardDraftButtonDisabled', 'preBuiltCommitMessage']),
overviewText() {
return sprintf(
__(
@@ -36,6 +36,9 @@ export default {
},
);
},
+ commitButtonText() {
+ return this.stagedFiles.length ? __('Commit') : __('Stage & Commit');
+ },
},
watch: {
currentActivityView() {
@@ -91,7 +94,6 @@ export default {
<template>
<div
- class="multi-file-commit-form"
:class="{
'is-compact': isCompact,
'is-full': !isCompact
@@ -99,6 +101,7 @@ export default {
:style="{
height: componentHeight ? `${componentHeight}px` : null,
}"
+ class="multi-file-commit-form"
>
<transition
name="commit-form-slide-up"
@@ -108,16 +111,16 @@ export default {
>
<div
v-if="isCompact"
- class="commit-form-compact"
ref="compactEl"
+ class="commit-form-compact"
>
<button
- type="button"
:disabled="!hasChanges"
+ type="button"
class="btn btn-primary btn-sm btn-block"
@click="toggleIsSmall"
>
- {{ __('Commit') }}
+ {{ __('Commit…') }}
</button>
<p
class="text-center"
@@ -126,9 +129,8 @@ export default {
</div>
<form
v-if="!isCompact"
- class="form-horizontal"
- @submit.prevent.stop="commitChanges"
ref="formEl"
+ @submit.prevent.stop="commitChanges"
>
<transition name="fade">
<success-message
@@ -137,21 +139,21 @@ export default {
</transition>
<commit-message-field
:text="commitMessage"
+ :placeholder="preBuiltCommitMessage"
@input="updateCommitMessage"
/>
<div class="clearfix prepend-top-15">
<actions />
<loading-button
:loading="submitCommitLoading"
- :disabled="commitButtonDisabled"
- container-class="btn btn-success btn-sm pull-left"
- :label="__('Commit')"
+ :label="commitButtonText"
+ container-class="btn btn-success btn-sm float-left"
@click="commitChanges"
/>
<button
v-if="!discardDraftButtonDisabled"
type="button"
- class="btn btn-default btn-sm pull-right"
+ class="btn btn-default btn-sm float-right"
@click="discardDraft"
>
{{ __('Discard draft') }}
@@ -159,7 +161,7 @@ export default {
<button
v-else
type="button"
- class="btn btn-default btn-sm pull-right"
+ class="btn btn-default btn-sm float-right"
@click="toggleIsSmall"
>
{{ __('Collapse') }}
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
index c3ac18bfb83..d0fb0e3d99e 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
@@ -34,6 +34,10 @@ export default {
type: String,
required: true,
},
+ actionBtnIcon: {
+ type: String,
+ required: true,
+ },
itemActionComponent: {
type: String,
required: true,
@@ -43,11 +47,15 @@ export default {
required: false,
default: false,
},
- },
- data() {
- return {
- showActionButton: false,
- };
+ activeFileKey: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ keyPrefix: {
+ type: String,
+ required: true,
+ },
},
computed: {
titleText() {
@@ -55,15 +63,15 @@ export default {
title: this.title,
});
},
+ filesLength() {
+ return this.fileList.length;
+ },
},
methods: {
...mapActions(['stageAllChanges', 'unstageAllChanges']),
actionBtnClicked() {
this[this.action]();
},
- setShowActionButton(show) {
- this.showActionButton = show;
- },
},
};
</script>
@@ -74,8 +82,6 @@ export default {
>
<header
class="multi-file-commit-panel-header"
- @mouseenter="setShowActionButton(true)"
- @mouseleave="setShowActionButton(false)"
>
<div
class="multi-file-commit-panel-header-title"
@@ -86,24 +92,40 @@ export default {
:size="18"
/>
{{ titleText }}
- <span
- v-show="!showActionButton"
- class="ide-commit-file-count"
- >
- {{ fileList.length }}
- </span>
- <button
- v-show="showActionButton"
- type="button"
- class="btn btn-blank btn-link ide-staged-action-btn"
- @click="actionBtnClicked"
- >
- {{ actionBtnText }}
- </button>
+ <div class="d-flex ml-auto">
+ <button
+ v-tooltip
+ v-show="filesLength"
+ :class="{
+ 'd-flex': filesLength
+ }"
+ :title="actionBtnText"
+ type="button"
+ class="btn btn-default ide-staged-action-btn p-0 order-1 align-items-center"
+ data-placement="bottom"
+ data-container="body"
+ data-boundary="viewport"
+ @click="actionBtnClicked"
+ >
+ <icon
+ :name="actionBtnIcon"
+ :size="12"
+ class="ml-auto mr-auto"
+ />
+ </button>
+ <span
+ :class="{
+ 'rounded-right': !filesLength
+ }"
+ class="ide-commit-file-count order-0 rounded-left text-center"
+ >
+ {{ filesLength }}
+ </span>
+ </div>
</div>
</header>
<ul
- v-if="fileList.length"
+ v-if="filesLength"
class="multi-file-commit-list list-unstyled append-bottom-0"
>
<li
@@ -113,14 +135,15 @@ export default {
<list-item
:file="file"
:action-component="itemActionComponent"
- :key-prefix="title"
+ :key-prefix="keyPrefix"
:staged-list="stagedList"
+ :active-file-key="activeFileKey"
/>
</li>
</ul>
<p
v-else
- class="multi-file-commit-list help-block"
+ class="multi-file-commit-list form-text text-muted"
>
{{ __('No changes') }}
</p>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
index 2254271c679..d376a004e84 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
@@ -38,14 +38,17 @@ export default {
return this.modifiedFilesLength ? 'multi-file-modified' : '';
},
additionsTooltip() {
- return sprintf(n__('1 %{type} addition', '%d %{type} additions', this.addedFilesLength), {
+ return sprintf(n__('1 %{type} addition', '%{count} %{type} additions', this.addedFilesLength), {
type: this.title.toLowerCase(),
+ count: this.addedFilesLength,
});
},
modifiedTooltip() {
return sprintf(
- n__('1 %{type} modification', '%d %{type} modifications', this.modifiedFilesLength),
- { type: this.title.toLowerCase() },
+ n__('1 %{type} modification', '%{count} %{type} modifications', this.modifiedFilesLength), {
+ type: this.title.toLowerCase(),
+ count: this.modifiedFilesLength,
+ },
);
},
titleTooltip() {
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
index 03f3e4de83c..ee21eeda3cd 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
@@ -1,5 +1,6 @@
<script>
import { mapActions } from 'vuex';
+import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import StageButton from './stage_button.vue';
import UnstageButton from './unstage_button.vue';
@@ -11,6 +12,9 @@ export default {
StageButton,
UnstageButton,
},
+ directives: {
+ tooltip,
+ },
props: {
file: {
type: Object,
@@ -30,6 +34,11 @@ export default {
required: false,
default: false,
},
+ activeFileKey: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
computed: {
iconName() {
@@ -39,6 +48,15 @@ export default {
iconClass() {
return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`;
},
+ fullKey() {
+ return `${this.keyPrefix}-${this.file.key}`;
+ },
+ isActive() {
+ return this.activeFileKey === this.fullKey;
+ },
+ tooltipTitle() {
+ return this.file.path === this.file.name ? '' : this.file.path;
+ },
},
methods: {
...mapActions([
@@ -51,7 +69,7 @@ export default {
openFileInEditor() {
return this.openPendingTab({
file: this.file,
- keyPrefix: this.keyPrefix.toLowerCase(),
+ keyPrefix: this.keyPrefix,
}).then(changeViewer => {
if (changeViewer) {
this.updateViewer(viewerTypes.diff);
@@ -70,24 +88,30 @@ export default {
</script>
<template>
- <div class="multi-file-commit-list-item">
- <button
- type="button"
- class="multi-file-commit-list-path"
+ <div class="multi-file-commit-list-item position-relative">
+ <div
+ v-tooltip
+ :title="tooltipTitle"
+ :class="{
+ 'is-active': isActive
+ }"
+ class="multi-file-commit-list-path w-100 border-0 ml-0 mr-0"
+ role="button"
@dblclick="fileAction"
@click="openFileInEditor"
>
- <span class="multi-file-commit-list-file-path">
+ <span class="multi-file-commit-list-file-path d-flex align-items-center">
<icon
:name="iconName"
:size="16"
:css-classes="iconClass"
- />{{ file.path }}
+ />{{ file.name }}
</span>
- </button>
+ </div>
<component
:is="actionComponent"
:path="file.path"
+ class="d-flex position-absolute"
/>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue
index dcd934f76b7..37ca108fafc 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue
@@ -16,6 +16,10 @@ export default {
type: String,
required: true,
},
+ placeholder: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
@@ -54,7 +58,7 @@ export default {
placement: 'top',
content: sprintf(
__(`
- The character highligher helps you keep the subject line to %{titleLength} characters
+ The character highlighter helps you keep the subject line to %{titleLength} characters
and wrap the body at %{bodyLength} so they are readable in git.
`),
{ titleLength: MAX_TITLE_LENGTH, bodyLength: MAX_BODY_LENGTH },
@@ -66,10 +70,10 @@ export default {
<template>
<fieldset class="common-note-form ide-commit-message-field">
<div
- class="md-area"
:class="{
'is-focused': isFocused
}"
+ class="md-area"
>
<div
v-once
@@ -80,7 +84,7 @@ export default {
{{ __('Commit Message') }}
<span
v-popover="$options.popoverOptions"
- class="help-block prepend-left-10"
+ class="form-text text-muted prepend-left-10"
>
<icon
name="question"
@@ -92,10 +96,10 @@ export default {
<div class="ide-commit-message-textarea-container">
<div class="ide-commit-message-highlights-container">
<div
- class="note-textarea highlights monospace"
:style="{
transform: `translate3d(0, ${-scrollTop}px, 0)`
}"
+ class="note-textarea highlights monospace"
>
<div
v-for="(line, index) in allLines"
@@ -113,15 +117,15 @@ export default {
</div>
</div>
<textarea
+ ref="textarea"
+ :placeholder="placeholder"
+ :value="text"
class="note-textarea ide-commit-message-textarea"
name="commit-message"
- :placeholder="__('Write a commit message...')"
- :value="text"
@scroll="handleScroll"
@input="onInput"
@focus="updateIsFocused(true)"
@blur="updateIsFocused(false)"
- ref="textarea"
>
</textarea>
</div>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
index 00f2312ae51..969e2aa61c4 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
@@ -1,6 +1,5 @@
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
-import { __ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
@@ -32,14 +31,17 @@ export default {
required: false,
default: false,
},
+ title: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
...mapState('commit', ['commitAction']),
...mapGetters('commit', ['newBranchName']),
tooltipTitle() {
- return this.disabled
- ? __('This option is disabled while you still have unstaged changes')
- : '';
+ return this.disabled ? this.title : '';
},
},
methods: {
@@ -58,12 +60,12 @@ export default {
}"
>
<input
- type="radio"
- name="commit-action"
:value="value"
- @change="updateCommitAction($event.target.value)"
:checked="commitAction === value"
:disabled="disabled"
+ type="radio"
+ name="commit-action"
+ @change="updateCommitAction($event.target.value)"
/>
<span class="prepend-left-10">
<span
@@ -80,9 +82,9 @@ export default {
class="ide-commit-new-branch"
>
<input
+ :placeholder="newBranchName"
type="text"
class="form-control monospace"
- :placeholder="newBranchName"
@input="updateBranchName($event.target.value)"
/>
</div>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue
index 52dce8412ab..7014b9f605e 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue
@@ -25,35 +25,51 @@ export default {
<template>
<div
v-once
- class="multi-file-discard-btn"
+ class="multi-file-discard-btn dropdown"
>
<button
v-tooltip
- type="button"
- class="btn btn-blank append-right-5"
:aria-label="__('Stage changes')"
:title="__('Stage changes')"
+ type="button"
+ class="btn btn-blank append-right-5 d-flex align-items-center"
data-container="body"
+ data-boundary="viewport"
+ data-placement="bottom"
@click.stop="stageChange(path)"
>
<icon
- name="mobile-issue-close"
:size="12"
+ name="mobile-issue-close"
/>
</button>
<button
v-tooltip
+ :title="__('More actions')"
type="button"
- class="btn btn-blank"
- :aria-label="__('Discard changes')"
- :title="__('Discard changes')"
+ class="btn btn-blank d-flex align-items-center"
data-container="body"
- @click.stop="discardFileChanges(path)"
+ data-boundary="viewport"
+ data-placement="bottom"
+ data-toggle="dropdown"
+ data-display="static"
>
<icon
- name="remove"
:size="12"
+ name="more"
/>
</button>
+ <div class="dropdown-menu dropdown-menu-right">
+ <ul>
+ <li>
+ <button
+ type="button"
+ @click.stop="discardFileChanges(path)"
+ >
+ {{ __('Discard changes') }}
+ </button>
+ </li>
+ </ul>
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue b/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue
index 123d60da47e..9cec73ec00e 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue
@@ -29,16 +29,18 @@ export default {
>
<button
v-tooltip
- type="button"
- class="btn btn-blank"
:aria-label="__('Unstage changes')"
:title="__('Unstage changes')"
+ type="button"
+ class="btn btn-blank d-flex align-items-center"
data-container="body"
+ data-boundary="viewport"
+ data-placement="bottom"
@click="unstageChange(path)"
>
<icon
- name="history"
:size="12"
+ name="history"
/>
</button>
</div>
diff --git a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
index b9af4d27145..95598c9aca6 100644
--- a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
+++ b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
@@ -44,11 +44,11 @@ export default {
<ul>
<li>
<a
- href="#"
- @click.prevent="changeMode($options.viewerTypes.mr)"
:class="{
'is-active': viewer === $options.viewerTypes.mr,
}"
+ href="#"
+ @click.prevent="changeMode($options.viewerTypes.mr)"
>
<strong class="dropdown-menu-inner-title">
{{ mergeReviewLine }}
@@ -60,11 +60,11 @@ export default {
</li>
<li>
<a
- href="#"
- @click.prevent="changeMode($options.viewerTypes.diff)"
:class="{
'is-active': viewer === $options.viewerTypes.diff,
}"
+ href="#"
+ @click.prevent="changeMode($options.viewerTypes.diff)"
>
<strong class="dropdown-menu-inner-title">{{ __('Reviewing') }}</strong>
<span class="dropdown-menu-inner-content">
diff --git a/app/assets/javascripts/ide/components/error_message.vue b/app/assets/javascripts/ide/components/error_message.vue
new file mode 100644
index 00000000000..acbc98b7a7b
--- /dev/null
+++ b/app/assets/javascripts/ide/components/error_message.vue
@@ -0,0 +1,69 @@
+<script>
+import { mapActions } from 'vuex';
+import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
+
+export default {
+ components: {
+ LoadingIcon,
+ },
+ props: {
+ message: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isLoading: false,
+ };
+ },
+ methods: {
+ ...mapActions(['setErrorMessage']),
+ clickAction() {
+ if (this.isLoading) return;
+
+ this.isLoading = true;
+
+ this.message
+ .action(this.message.actionPayload)
+ .then(() => {
+ this.isLoading = false;
+ })
+ .catch(() => {
+ this.isLoading = false;
+ });
+ },
+ clickFlash() {
+ if (!this.message.action) {
+ this.setErrorMessage(null);
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="flash-container flash-container-page"
+ @click="clickFlash"
+ >
+ <div class="flash-alert">
+ <span
+ v-html="message.text"
+ >
+ </span>
+ <button
+ v-if="message.action"
+ type="button"
+ class="flash-action text-white p-0 border-top-0 border-right-0 border-left-0 bg-transparent"
+ @click.stop.prevent="clickAction"
+ >
+ {{ message.actionText }}
+ <loading-icon
+ v-show="isLoading"
+ inline
+ />
+ </button>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/external_link.vue b/app/assets/javascripts/ide/components/external_link.vue
new file mode 100644
index 00000000000..e24fe5bbccb
--- /dev/null
+++ b/app/assets/javascripts/ide/components/external_link.vue
@@ -0,0 +1,41 @@
+<script>
+import Icon from '~/vue_shared/components/icon.vue';
+
+export default {
+ components: {
+ Icon,
+ },
+ props: {
+ file: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ showButtons() {
+ return this.file.permalink;
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ v-if="showButtons"
+ class="pull-right ide-btn-group"
+ >
+ <a
+ :href="file.permalink"
+ :title="s__('IDE|Open in file view')"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ <span class="vertical-align-middle">Open in file view</span>
+ <icon
+ :size="16"
+ name="external-link"
+ css-classes="vertical-align-middle space-right"
+ />
+ </a>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/file_finder/index.vue b/app/assets/javascripts/ide/components/file_finder/index.vue
index cabb3f59b17..0ba33053717 100644
--- a/app/assets/javascripts/ide/components/file_finder/index.vue
+++ b/app/assets/javascripts/ide/components/file_finder/index.vue
@@ -173,38 +173,38 @@ export default {
>
<div class="dropdown-input">
<input
+ ref="searchInput"
+ :placeholder="__('Search files')"
+ v-model="searchText"
type="search"
class="dropdown-input-field"
- :placeholder="__('Search files')"
autocomplete="off"
- v-model="searchText"
- ref="searchInput"
@keydown="onKeydown($event)"
@keyup="onKeyup($event)"
/>
<i
- aria-hidden="true"
- class="fa fa-search dropdown-input-search"
:class="{
hidden: showClearInputButton
}"
+ aria-hidden="true"
+ class="fa fa-search dropdown-input-search"
></i>
<i
- role="button"
:aria-label="__('Clear search input')"
- class="fa fa-times dropdown-input-clear"
:class="{
show: showClearInputButton
}"
+ role="button"
+ class="fa fa-times dropdown-input-clear"
@click="clearSearchInput"
></i>
</div>
<div>
<virtual-list
+ ref="virtualScrollList"
:size="listHeight"
:remain="listShowCount"
wtag="ul"
- ref="virtualScrollList"
>
<template v-if="filteredBlobsLength">
<li
@@ -212,11 +212,11 @@ export default {
:key="file.key"
>
<item
- class="disable-hover"
:file="file"
:search-text="searchText"
:focused="index === focusedIndex"
:index="index"
+ class="disable-hover"
@click="openFile"
@mouseover="onMouseOver"
@mousemove="onMouseMove"
diff --git a/app/assets/javascripts/ide/components/file_finder/item.vue b/app/assets/javascripts/ide/components/file_finder/item.vue
index d4427420207..f5252ce7706 100644
--- a/app/assets/javascripts/ide/components/file_finder/item.vue
+++ b/app/assets/javascripts/ide/components/file_finder/item.vue
@@ -30,7 +30,7 @@ export default {
},
computed: {
pathWithEllipsis() {
- const path = this.file.path;
+ const { path } = this.file;
return path.length < MAX_PATH_LENGTH
? path
@@ -59,11 +59,11 @@ export default {
<template>
<button
- type="button"
- class="diff-changed-file"
:class="{
'is-focused': focused,
}"
+ type="button"
+ class="diff-changed-file"
@click.prevent="clickRow"
@mouseover="mouseOverRow"
@mousemove="mouseMove"
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index 1ec69adce09..9f016e0338f 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -6,6 +6,8 @@ import RepoTabs from './repo_tabs.vue';
import IdeStatusBar from './ide_status_bar.vue';
import RepoEditor from './repo_editor.vue';
import FindFile from './file_finder/index.vue';
+import RightPane from './panes/right.vue';
+import ErrorMessage from './error_message.vue';
const originalStopCallback = Mousetrap.stopCallback;
@@ -16,6 +18,8 @@ export default {
IdeStatusBar,
RepoEditor,
FindFile,
+ RightPane,
+ ErrorMessage,
},
computed: {
...mapState([
@@ -25,6 +29,8 @@ export default {
'currentMergeRequestId',
'fileFindVisible',
'emptyStateSvgPath',
+ 'currentProjectId',
+ 'errorMessage',
]),
...mapGetters(['activeFile', 'hasChanges']),
},
@@ -69,6 +75,10 @@ export default {
<template>
<article class="ide">
+ <error-message
+ v-if="errorMessage"
+ :message="errorMessage"
+ />
<div
class="ide-view"
>
@@ -90,8 +100,8 @@ export default {
:merge-request-id="currentMergeRequestId"
/>
<repo-editor
- class="multi-file-edit-pane-content"
:file="activeFile"
+ class="multi-file-edit-pane-content"
/>
</template>
<template
@@ -102,12 +112,12 @@ export default {
class="ide-empty-state"
>
<div class="row js-empty-state">
- <div class="col-xs-12">
+ <div class="col-12">
<div class="svg-content svg-250">
<img :src="emptyStateSvgPath" />
</div>
</div>
- <div class="col-xs-12">
+ <div class="col-12">
<div class="text-content text-center">
<h4>
Welcome to the GitLab IDE
@@ -122,9 +132,10 @@ export default {
</div>
</template>
</div>
+ <right-pane
+ v-if="currentProjectId"
+ />
</div>
- <ide-status-bar
- :file="activeFile"
- />
+ <ide-status-bar :file="activeFile"/>
</article>
</template>
diff --git a/app/assets/javascripts/ide/components/ide_file_buttons.vue b/app/assets/javascripts/ide/components/ide_file_buttons.vue
deleted file mode 100644
index a6c6f46a144..00000000000
--- a/app/assets/javascripts/ide/components/ide_file_buttons.vue
+++ /dev/null
@@ -1,84 +0,0 @@
-<script>
-import { __ } from '~/locale';
-import tooltip from '~/vue_shared/directives/tooltip';
-import Icon from '~/vue_shared/components/icon.vue';
-
-export default {
- components: {
- Icon,
- },
- directives: {
- tooltip,
- },
- props: {
- file: {
- type: Object,
- required: true,
- },
- },
- computed: {
- showButtons() {
- return (
- this.file.rawPath || this.file.blamePath || this.file.commitsPath || this.file.permalink
- );
- },
- rawDownloadButtonLabel() {
- return this.file.binary ? __('Download') : __('Raw');
- },
- },
-};
-</script>
-
-<template>
- <div
- v-if="showButtons"
- class="pull-right ide-btn-group"
- >
- <a
- v-tooltip
- v-if="!file.binary"
- :href="file.blamePath"
- :title="__('Blame')"
- class="btn btn-xs btn-transparent blame"
- >
- <icon
- name="blame"
- :size="16"
- />
- </a>
- <a
- v-tooltip
- :href="file.commitsPath"
- :title="__('History')"
- class="btn btn-xs btn-transparent history"
- >
- <icon
- name="history"
- :size="16"
- />
- </a>
- <a
- v-tooltip
- :href="file.permalink"
- :title="__('Permalink')"
- class="btn btn-xs btn-transparent permalink"
- >
- <icon
- name="link"
- :size="16"
- />
- </a>
- <a
- v-tooltip
- :href="file.rawPath"
- target="_blank"
- class="btn btn-xs btn-transparent prepend-left-10 raw"
- rel="noopener noreferrer"
- :title="rawDownloadButtonLabel">
- <icon
- name="download"
- :size="16"
- />
- </a>
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/ide_review.vue b/app/assets/javascripts/ide/components/ide_review.vue
index 0c9ec3b00f0..f9978762c45 100644
--- a/app/assets/javascripts/ide/components/ide_review.vue
+++ b/app/assets/javascripts/ide/components/ide_review.vue
@@ -11,17 +11,20 @@ export default {
},
computed: {
...mapGetters(['currentMergeRequest']),
- ...mapState(['viewer']),
+ ...mapState(['viewer', 'currentMergeRequestId']),
showLatestChangesText() {
- return !this.currentMergeRequest || this.viewer === viewerTypes.diff;
+ return !this.currentMergeRequestId || this.viewer === viewerTypes.diff;
},
showMergeRequestText() {
- return this.currentMergeRequest && this.viewer === viewerTypes.mr;
+ return this.currentMergeRequestId && this.viewer === viewerTypes.mr;
+ },
+ mergeRequestId() {
+ return `!${this.currentMergeRequest.iid}`;
},
},
mounted() {
this.$nextTick(() => {
- this.updateViewer(this.currentMergeRequest ? viewerTypes.mr : viewerTypes.diff);
+ this.updateViewer(this.currentMergeRequestId ? viewerTypes.mr : viewerTypes.diff);
});
},
methods: {
@@ -33,8 +36,8 @@ export default {
<template>
<ide-tree-list
:viewer-type="viewer"
- header-class="ide-review-header"
:disable-action-dropdown="true"
+ header-class="ide-review-header"
>
<template
slot="header"
@@ -54,7 +57,11 @@ export default {
</template>
<template v-else-if="showMergeRequestText">
{{ __('Merge request') }}
- (<a :href="currentMergeRequest.web_url">!{{ currentMergeRequest.iid }}</a>)
+ (<a
+ v-if="currentMergeRequest"
+ :href="currentMergeRequest.web_url"
+ v-text="mergeRequestId"
+ ></a>)
</template>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue
index 3f980203911..21906674c4b 100644
--- a/app/assets/javascripts/ide/components/ide_side_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_side_bar.vue
@@ -1,4 +1,5 @@
<script>
+import $ from 'jquery';
import { mapState, mapGetters } from 'vuex';
import ProjectAvatarImage from '~/vue_shared/components/project_avatar/image.vue';
import Icon from '~/vue_shared/components/icon.vue';
@@ -13,6 +14,7 @@ import CommitSection from './repo_commit_section.vue';
import CommitForm from './commit_sidebar/form.vue';
import IdeReview from './ide_review.vue';
import SuccessMessage from './commit_sidebar/success_message.vue';
+import MergeRequestDropdown from './merge_requests/dropdown.vue';
import { activityBarViews } from '../constants';
export default {
@@ -32,10 +34,12 @@ export default {
CommitForm,
IdeReview,
SuccessMessage,
+ MergeRequestDropdown,
},
data() {
return {
showTooltip: false,
+ showMergeRequestsDropdown: false,
};
},
computed: {
@@ -46,6 +50,7 @@ export default {
'changedFiles',
'stagedFiles',
'lastCommitMsg',
+ 'currentMergeRequestId',
]),
...mapGetters(['currentProject', 'someUncommitedChanges']),
showSuccessMessage() {
@@ -61,9 +66,39 @@ export default {
watch: {
currentBranchId() {
this.$nextTick(() => {
+ if (!this.$refs.branchId) return;
+
this.showTooltip = this.$refs.branchId.scrollWidth > this.$refs.branchId.offsetWidth;
});
},
+ loading() {
+ this.$nextTick(() => {
+ this.addDropdownListeners();
+ });
+ },
+ },
+ mounted() {
+ this.addDropdownListeners();
+ },
+ beforeDestroy() {
+ $(this.$refs.mergeRequestDropdown)
+ .off('show.bs.dropdown')
+ .off('hide.bs.dropdown');
+ },
+ methods: {
+ addDropdownListeners() {
+ if (!this.$refs.mergeRequestDropdown) return;
+
+ $(this.$refs.mergeRequestDropdown)
+ .on('show.bs.dropdown', () => {
+ this.toggleMergeRequestDropdown();
+ }).on('hide.bs.dropdown', () => {
+ this.toggleMergeRequestDropdown();
+ });
+ },
+ toggleMergeRequestDropdown() {
+ this.showMergeRequestsDropdown = !this.showMergeRequestsDropdown;
+ },
},
};
</script>
@@ -80,53 +115,79 @@ export default {
<div class="multi-file-commit-panel-inner">
<template v-if="loading">
<div
- class="multi-file-loading-container"
v-for="n in 3"
:key="n"
+ class="multi-file-loading-container"
>
<skeleton-loading-container />
</div>
</template>
<template v-else>
- <div class="context-header ide-context-header">
- <a
- :href="currentProject.web_url"
+ <div
+ ref="mergeRequestDropdown"
+ class="context-header ide-context-header dropdown"
+ >
+ <button
+ type="button"
+ data-toggle="dropdown"
>
<div
v-if="currentProject.avatar_url"
class="avatar-container s40 project-avatar"
>
<project-avatar-image
- class="avatar-container project-avatar"
:link-href="currentProject.path"
:img-src="currentProject.avatar_url"
:img-alt="currentProject.name"
:img-size="40"
+ class="avatar-container project-avatar"
/>
</div>
<identicon
v-else
- size-class="s40"
:entity-id="currentProject.id"
:entity-name="currentProject.name"
+ size-class="s40"
/>
<div class="ide-sidebar-project-title">
<div class="sidebar-context-title">
{{ currentProject.name }}
</div>
- <div
- class="sidebar-context-title ide-sidebar-branch-title"
- ref="branchId"
- v-tooltip
- :title="branchTooltipTitle"
- >
- <icon
- name="branch"
- css-classes="append-right-5"
- />{{ currentBranchId }}
+ <div class="d-flex">
+ <div
+ v-tooltip
+ v-if="currentBranchId"
+ ref="branchId"
+ :title="branchTooltipTitle"
+ class="sidebar-context-title ide-sidebar-branch-title"
+ >
+ <icon
+ name="branch"
+ css-classes="append-right-5"
+ />{{ currentBranchId }}
+ </div>
+ <div
+ v-if="currentMergeRequestId"
+ :class="{
+ 'prepend-left-8': currentBranchId
+ }"
+ class="sidebar-context-title ide-sidebar-branch-title"
+ >
+ <icon
+ name="git-merge"
+ css-classes="append-right-5"
+ />!{{ currentMergeRequestId }}
+ </div>
</div>
</div>
- </a>
+ <icon
+ class="ml-auto"
+ name="chevron-down"
+ />
+ </button>
+ <merge-request-dropdown
+ :show="showMergeRequestsDropdown"
+ />
</div>
<div class="multi-file-commit-panel-inner-scroll">
<component
diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue
index 70c6d53c3ab..0582ad32e92 100644
--- a/app/assets/javascripts/ide/components/ide_status_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_status_bar.vue
@@ -1,14 +1,16 @@
<script>
-import { mapGetters } from 'vuex';
+import { mapActions, mapState, mapGetters } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import timeAgoMixin from '~/vue_shared/mixins/timeago';
+import CiIcon from '../../vue_shared/components/ci_icon.vue';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
export default {
components: {
icon,
userAvatarImage,
+ CiIcon,
},
directives: {
tooltip,
@@ -27,7 +29,14 @@ export default {
};
},
computed: {
+ ...mapState(['currentBranchId', 'currentProjectId']),
...mapGetters(['currentProject', 'lastCommit']),
+ ...mapState('pipelines', ['latestPipeline']),
+ },
+ watch: {
+ lastCommit() {
+ this.initPipelinePolling();
+ },
},
mounted() {
this.startTimer();
@@ -36,13 +45,21 @@ export default {
if (this.intervalId) {
clearInterval(this.intervalId);
}
+
+ this.stopPipelinePolling();
},
methods: {
+ ...mapActions('pipelines', ['fetchLatestPipeline', 'stopPipelinePolling']),
startTimer() {
this.intervalId = setInterval(() => {
this.commitAgeUpdate();
}, 1000);
},
+ initPipelinePolling() {
+ if (this.lastCommit) {
+ this.fetchLatestPipeline();
+ }
+ },
commitAgeUpdate() {
if (this.lastCommit) {
this.lastCommitFormatedAge = this.timeFormated(this.lastCommit.committed_date);
@@ -58,26 +75,43 @@ export default {
<template>
<footer class="ide-status-bar">
<div
- class="ide-status-branch"
v-if="lastCommit && lastCommitFormatedAge"
+ class="ide-status-branch"
>
+ <span
+ v-if="latestPipeline && latestPipeline.details"
+ class="ide-status-pipeline"
+ >
+ <ci-icon
+ v-tooltip
+ :status="latestPipeline.details.status"
+ :title="latestPipeline.details.status.text"
+ />
+ Pipeline
+ <a
+ :href="latestPipeline.details.status.details_path"
+ class="monospace">#{{ latestPipeline.id }}</a>
+ {{ latestPipeline.details.status.text }}
+ for
+ </span>
+
<icon
name="commit"
/>
<a
v-tooltip
- class="commit-sha"
:title="lastCommit.message"
:href="getCommitPath(lastCommit.short_id)"
+ class="commit-sha"
>{{ lastCommit.short_id }}</a>
by
{{ lastCommit.author_name }}
<time
v-tooltip
- data-placement="top"
- data-container="body"
:datetime="lastCommit.committed_date"
:title="tooltipTitle(lastCommit.committed_date)"
+ data-placement="top"
+ data-container="body"
>
{{ lastCommitFormatedAge }}
</time>
@@ -95,8 +129,8 @@ export default {
{{ file.eol }}
</div>
<div
- class="ide-status-file"
- v-if="file && !file.binary">
+ v-if="file && !file.binary"
+ class="ide-status-file">
{{ file.editorRow }}:{{ file.editorColumn }}
</div>
<div
diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue
index e64a09fcc90..0df99798d21 100644
--- a/app/assets/javascripts/ide/components/ide_tree_list.vue
+++ b/app/assets/javascripts/ide/components/ide_tree_list.vue
@@ -50,17 +50,17 @@ export default {
>
<template v-if="showLoading">
<div
- class="multi-file-loading-container"
v-for="n in 3"
:key="n"
+ class="multi-file-loading-container"
>
<skeleton-loading-container />
</div>
</template>
<template v-else>
<header
- class="ide-tree-header"
:class="headerClass"
+ class="ide-tree-header"
>
<slot name="header"></slot>
</header>
diff --git a/app/assets/javascripts/ide/components/jobs/detail.vue b/app/assets/javascripts/ide/components/jobs/detail.vue
new file mode 100644
index 00000000000..f39ce545656
--- /dev/null
+++ b/app/assets/javascripts/ide/components/jobs/detail.vue
@@ -0,0 +1,137 @@
+<script>
+import { mapActions, mapState } from 'vuex';
+import _ from 'underscore';
+import { __ } from '../../../locale';
+import tooltip from '../../../vue_shared/directives/tooltip';
+import Icon from '../../../vue_shared/components/icon.vue';
+import ScrollButton from './detail/scroll_button.vue';
+import JobDescription from './detail/description.vue';
+
+const scrollPositions = {
+ top: 0,
+ bottom: 1,
+};
+
+export default {
+ directives: {
+ tooltip,
+ },
+ components: {
+ Icon,
+ ScrollButton,
+ JobDescription,
+ },
+ data() {
+ return {
+ scrollPos: scrollPositions.top,
+ };
+ },
+ computed: {
+ ...mapState('pipelines', ['detailJob']),
+ isScrolledToBottom() {
+ return this.scrollPos === scrollPositions.bottom;
+ },
+ isScrolledToTop() {
+ return this.scrollPos === scrollPositions.top;
+ },
+ jobOutput() {
+ return this.detailJob.output || __('No messages were logged');
+ },
+ },
+ mounted() {
+ this.getTrace();
+ },
+ methods: {
+ ...mapActions('pipelines', ['fetchJobTrace', 'setDetailJob']),
+ scrollDown() {
+ if (this.$refs.buildTrace) {
+ this.$refs.buildTrace.scrollTo(0, this.$refs.buildTrace.scrollHeight);
+ }
+ },
+ scrollUp() {
+ if (this.$refs.buildTrace) {
+ this.$refs.buildTrace.scrollTo(0, 0);
+ }
+ },
+ scrollBuildLog: _.throttle(function buildLogScrollDebounce() {
+ const { scrollTop } = this.$refs.buildTrace;
+ const { offsetHeight, scrollHeight } = this.$refs.buildTrace;
+
+ if (scrollTop + offsetHeight === scrollHeight) {
+ this.scrollPos = scrollPositions.bottom;
+ } else if (scrollTop === 0) {
+ this.scrollPos = scrollPositions.top;
+ } else {
+ this.scrollPos = '';
+ }
+ }),
+ getTrace() {
+ return this.fetchJobTrace().then(() => this.scrollDown());
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="ide-pipeline build-page d-flex flex-column flex-fill">
+ <header class="ide-job-header d-flex align-items-center">
+ <button
+ class="btn btn-default btn-sm d-flex"
+ @click="setDetailJob(null)"
+ >
+ <icon
+ name="chevron-left"
+ />
+ {{ __('View jobs') }}
+ </button>
+ </header>
+ <div class="top-bar d-flex border-left-0">
+ <job-description
+ :job="detailJob"
+ />
+ <div class="controllers ml-auto">
+ <a
+ v-tooltip
+ :title="__('Show complete raw log')"
+ :href="detailJob.rawPath"
+ data-placement="top"
+ data-container="body"
+ class="controllers-buttons"
+ target="_blank"
+ >
+ <i
+ aria-hidden="true"
+ class="fa fa-file-text-o"
+ ></i>
+ </a>
+ <scroll-button
+ :disabled="isScrolledToTop"
+ direction="up"
+ @click="scrollUp"
+ />
+ <scroll-button
+ :disabled="isScrolledToBottom"
+ direction="down"
+ @click="scrollDown"
+ />
+ </div>
+ </div>
+ <pre
+ ref="buildTrace"
+ class="build-trace mb-0 h-100"
+ @scroll="scrollBuildLog"
+ >
+ <code
+ v-show="!detailJob.isLoading"
+ class="bash"
+ v-html="jobOutput"
+ >
+ </code>
+ <div
+ v-show="detailJob.isLoading"
+ class="build-loader-animation"
+ >
+ </div>
+ </pre>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/jobs/detail/description.vue b/app/assets/javascripts/ide/components/jobs/detail/description.vue
new file mode 100644
index 00000000000..7e24974f7e5
--- /dev/null
+++ b/app/assets/javascripts/ide/components/jobs/detail/description.vue
@@ -0,0 +1,47 @@
+<script>
+import Icon from '../../../../vue_shared/components/icon.vue';
+import CiIcon from '../../../../vue_shared/components/ci_icon.vue';
+
+export default {
+ components: {
+ Icon,
+ CiIcon,
+ },
+ props: {
+ job: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ jobId() {
+ return `#${this.job.id}`;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="d-flex align-items-center">
+ <ci-icon
+ :status="job.status"
+ :borderless="true"
+ :size="24"
+ class="d-flex"
+ />
+ <span class="prepend-left-8">
+ {{ job.name }}
+ <a
+ :href="job.path"
+ target="_blank"
+ class="ide-external-link"
+ >
+ {{ jobId }}
+ <icon
+ :size="12"
+ name="external-link"
+ />
+ </a>
+ </span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue b/app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue
new file mode 100644
index 00000000000..103a407987f
--- /dev/null
+++ b/app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue
@@ -0,0 +1,66 @@
+<script>
+import { __ } from '../../../../locale';
+import Icon from '../../../../vue_shared/components/icon.vue';
+import tooltip from '../../../../vue_shared/directives/tooltip';
+
+const directions = {
+ up: 'up',
+ down: 'down',
+};
+
+export default {
+ directives: {
+ tooltip,
+ },
+ components: {
+ Icon,
+ },
+ props: {
+ direction: {
+ type: String,
+ required: true,
+ validator(value) {
+ return Object.keys(directions).includes(value);
+ },
+ },
+ disabled: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ tooltipTitle() {
+ return this.direction === directions.up ? __('Scroll to top') : __('Scroll to bottom');
+ },
+ iconName() {
+ return `scroll_${this.direction}`;
+ },
+ },
+ methods: {
+ clickedScroll() {
+ this.$emit('click');
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ v-tooltip
+ :title="tooltipTitle"
+ class="controllers-buttons"
+ data-container="body"
+ data-placement="top"
+ >
+ <button
+ :disabled="disabled"
+ class="btn-scroll btn-transparent btn-blank"
+ type="button"
+ @click="clickedScroll"
+ >
+ <icon
+ :name="iconName"
+ />
+ </button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/jobs/item.vue b/app/assets/javascripts/ide/components/jobs/item.vue
new file mode 100644
index 00000000000..7f4695a0451
--- /dev/null
+++ b/app/assets/javascripts/ide/components/jobs/item.vue
@@ -0,0 +1,44 @@
+<script>
+import JobDescription from './detail/description.vue';
+
+export default {
+ components: {
+ JobDescription,
+ },
+ props: {
+ job: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ jobId() {
+ return `#${this.job.id}`;
+ },
+ },
+ methods: {
+ clickViewLog() {
+ this.$emit('clickViewLog', this.job);
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="ide-job-item">
+ <job-description
+ :job="job"
+ class="append-right-default"
+ />
+ <div class="ml-auto align-self-center">
+ <button
+ v-if="job.started"
+ type="button"
+ class="btn btn-default btn-sm"
+ @click="clickViewLog"
+ >
+ {{ __('View log') }}
+ </button>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/jobs/list.vue b/app/assets/javascripts/ide/components/jobs/list.vue
new file mode 100644
index 00000000000..3b16b860ecd
--- /dev/null
+++ b/app/assets/javascripts/ide/components/jobs/list.vue
@@ -0,0 +1,45 @@
+<script>
+import { mapActions } from 'vuex';
+import LoadingIcon from '../../../vue_shared/components/loading_icon.vue';
+import Stage from './stage.vue';
+
+export default {
+ components: {
+ LoadingIcon,
+ Stage,
+ },
+ props: {
+ stages: {
+ type: Array,
+ required: true,
+ },
+ loading: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ methods: {
+ ...mapActions('pipelines', ['fetchJobs', 'toggleStageCollapsed', 'setDetailJob']),
+ },
+};
+</script>
+
+<template>
+ <div>
+ <loading-icon
+ v-if="loading && !stages.length"
+ class="prepend-top-default"
+ size="2"
+ />
+ <template v-else>
+ <stage
+ v-for="stage in stages"
+ :key="stage.id"
+ :stage="stage"
+ @fetch="fetchJobs"
+ @toggleCollapsed="toggleStageCollapsed"
+ @clickViewLog="setDetailJob"
+ />
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/jobs/stage.vue b/app/assets/javascripts/ide/components/jobs/stage.vue
new file mode 100644
index 00000000000..15e881b7bc8
--- /dev/null
+++ b/app/assets/javascripts/ide/components/jobs/stage.vue
@@ -0,0 +1,112 @@
+<script>
+import tooltip from '../../../vue_shared/directives/tooltip';
+import Icon from '../../../vue_shared/components/icon.vue';
+import CiIcon from '../../../vue_shared/components/ci_icon.vue';
+import LoadingIcon from '../../../vue_shared/components/loading_icon.vue';
+import Item from './item.vue';
+
+export default {
+ directives: {
+ tooltip,
+ },
+ components: {
+ Icon,
+ CiIcon,
+ LoadingIcon,
+ Item,
+ },
+ props: {
+ stage: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ showTooltip: false,
+ };
+ },
+ computed: {
+ collapseIcon() {
+ return this.stage.isCollapsed ? 'angle-left' : 'angle-down';
+ },
+ showLoadingIcon() {
+ return this.stage.isLoading && !this.stage.jobs.length;
+ },
+ jobsCount() {
+ return this.stage.jobs.length;
+ },
+ },
+ mounted() {
+ const { stageTitle } = this.$refs;
+
+ this.showTooltip = stageTitle.scrollWidth > stageTitle.offsetWidth;
+
+ this.$emit('fetch', this.stage);
+ },
+ methods: {
+ toggleCollapsed() {
+ this.$emit('toggleCollapsed', this.stage.id);
+ },
+ clickViewLog(job) {
+ this.$emit('clickViewLog', job);
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="ide-stage card prepend-top-default"
+ >
+ <div
+ :class="{
+ 'border-bottom-0': stage.isCollapsed
+ }"
+ class="card-header"
+ @click="toggleCollapsed"
+ >
+ <ci-icon
+ :status="stage.status"
+ :size="24"
+ />
+ <strong
+ v-tooltip="showTooltip"
+ ref="stageTitle"
+ :title="showTooltip ? stage.name : null"
+ data-container="body"
+ class="prepend-left-8 ide-stage-title"
+ >
+ {{ stage.name }}
+ </strong>
+ <div
+ v-if="!stage.isLoading || stage.jobs.length"
+ class="append-right-8 prepend-left-4"
+ >
+ <span class="badge badge-pill">
+ {{ jobsCount }}
+ </span>
+ </div>
+ <icon
+ :name="collapseIcon"
+ css-classes="ide-stage-collapse-icon"
+ />
+ </div>
+ <div
+ v-show="!stage.isCollapsed"
+ class="card-body"
+ >
+ <loading-icon
+ v-if="showLoadingIcon"
+ />
+ <template v-else>
+ <item
+ v-for="job in stage.jobs"
+ :key="job.id"
+ :job="job"
+ @clickViewLog="clickViewLog"
+ />
+ </template>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/merge_requests/dropdown.vue b/app/assets/javascripts/ide/components/merge_requests/dropdown.vue
new file mode 100644
index 00000000000..4b9824bf04b
--- /dev/null
+++ b/app/assets/javascripts/ide/components/merge_requests/dropdown.vue
@@ -0,0 +1,63 @@
+<script>
+import { mapGetters } from 'vuex';
+import Tabs from '../../../vue_shared/components/tabs/tabs';
+import Tab from '../../../vue_shared/components/tabs/tab.vue';
+import List from './list.vue';
+
+export default {
+ components: {
+ Tabs,
+ Tab,
+ List,
+ },
+ props: {
+ show: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapGetters('mergeRequests', ['assignedData', 'createdData']),
+ createdMergeRequestLength() {
+ return this.createdData.mergeRequests.length;
+ },
+ assignedMergeRequestLength() {
+ return this.assignedData.mergeRequests.length;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="dropdown-menu ide-merge-requests-dropdown p-0">
+ <tabs
+ v-if="show"
+ stop-propagation
+ >
+ <tab active>
+ <template slot="title">
+ {{ __('Created by me') }}
+ <span class="badge badge-pill">
+ {{ createdMergeRequestLength }}
+ </span>
+ </template>
+ <list
+ :empty-text="__('You have not created any merge requests')"
+ type="created"
+ />
+ </tab>
+ <tab>
+ <template slot="title">
+ {{ __('Assigned to me') }}
+ <span class="badge badge-pill">
+ {{ assignedMergeRequestLength }}
+ </span>
+ </template>
+ <list
+ :empty-text="__('You do not have any assigned merge requests')"
+ type="assigned"
+ />
+ </tab>
+ </tabs>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/merge_requests/info.vue b/app/assets/javascripts/ide/components/merge_requests/info.vue
new file mode 100644
index 00000000000..199d2e74971
--- /dev/null
+++ b/app/assets/javascripts/ide/components/merge_requests/info.vue
@@ -0,0 +1,43 @@
+<script>
+import { mapGetters } from 'vuex';
+import Icon from '../../../vue_shared/components/icon.vue';
+import TitleComponent from '../../../issue_show/components/title.vue';
+import DescriptionComponent from '../../../issue_show/components/description.vue';
+
+export default {
+ components: {
+ Icon,
+ TitleComponent,
+ DescriptionComponent,
+ },
+ computed: {
+ ...mapGetters(['currentMergeRequest']),
+ },
+};
+</script>
+
+<template>
+ <div class="ide-merge-request-info h-100 d-flex flex-column">
+ <div class="detail-page-header">
+ <icon
+ name="git-merge"
+ class="align-self-center append-right-8"
+ />
+ <strong>
+ !{{ currentMergeRequest.iid }}
+ </strong>
+ </div>
+ <div class="issuable-details">
+ <title-component
+ :issuable-ref="currentMergeRequest.iid"
+ :title-html="currentMergeRequest.title_html"
+ :title-text="currentMergeRequest.title"
+ />
+ <description-component
+ :description-html="currentMergeRequest.description_html"
+ :description-text="currentMergeRequest.description"
+ :can-update="false"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/merge_requests/item.vue b/app/assets/javascripts/ide/components/merge_requests/item.vue
new file mode 100644
index 00000000000..4e18376bd48
--- /dev/null
+++ b/app/assets/javascripts/ide/components/merge_requests/item.vue
@@ -0,0 +1,63 @@
+<script>
+import Icon from '../../../vue_shared/components/icon.vue';
+
+export default {
+ components: {
+ Icon,
+ },
+ props: {
+ item: {
+ type: Object,
+ required: true,
+ },
+ currentId: {
+ type: String,
+ required: true,
+ },
+ currentProjectId: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ isActive() {
+ return (
+ this.item.iid === parseInt(this.currentId, 10) &&
+ this.currentProjectId === this.item.projectPathWithNamespace
+ );
+ },
+ pathWithID() {
+ return `${this.item.projectPathWithNamespace}!${this.item.iid}`;
+ },
+ },
+ methods: {
+ clickItem() {
+ this.$emit('click', this.item);
+ },
+ },
+};
+</script>
+
+<template>
+ <button
+ type="button"
+ class="btn-link d-flex align-items-center"
+ @click="clickItem"
+ >
+ <span class="d-flex append-right-default ide-merge-request-current-icon">
+ <icon
+ v-if="isActive"
+ :size="18"
+ name="mobile-issue-close"
+ />
+ </span>
+ <span>
+ <strong>
+ {{ item.title }}
+ </strong>
+ <span class="ide-merge-request-project-path d-block mt-1">
+ {{ pathWithID }}
+ </span>
+ </span>
+ </button>
+</template>
diff --git a/app/assets/javascripts/ide/components/merge_requests/list.vue b/app/assets/javascripts/ide/components/merge_requests/list.vue
new file mode 100644
index 00000000000..19d3e48ee10
--- /dev/null
+++ b/app/assets/javascripts/ide/components/merge_requests/list.vue
@@ -0,0 +1,132 @@
+<script>
+import { mapActions, mapGetters, mapState } from 'vuex';
+import _ from 'underscore';
+import LoadingIcon from '../../../vue_shared/components/loading_icon.vue';
+import Item from './item.vue';
+
+export default {
+ components: {
+ LoadingIcon,
+ Item,
+ },
+ props: {
+ type: {
+ type: String,
+ required: true,
+ },
+ emptyText: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ search: '',
+ };
+ },
+ computed: {
+ ...mapGetters('mergeRequests', ['getData']),
+ ...mapState(['currentMergeRequestId', 'currentProjectId']),
+ data() {
+ return this.getData(this.type);
+ },
+ isLoading() {
+ return this.data.isLoading;
+ },
+ mergeRequests() {
+ return this.data.mergeRequests;
+ },
+ hasMergeRequests() {
+ return this.mergeRequests.length !== 0;
+ },
+ hasNoSearchResults() {
+ return this.search !== '' && !this.hasMergeRequests;
+ },
+ },
+ watch: {
+ isLoading: {
+ handler: 'focusSearch',
+ },
+ },
+ mounted() {
+ this.loadMergeRequests();
+ },
+ methods: {
+ ...mapActions('mergeRequests', ['fetchMergeRequests', 'openMergeRequest']),
+ loadMergeRequests() {
+ this.fetchMergeRequests({ type: this.type, search: this.search });
+ },
+ viewMergeRequest(item) {
+ this.openMergeRequest({
+ projectPath: item.projectPathWithNamespace,
+ id: item.iid,
+ });
+ },
+ searchMergeRequests: _.debounce(function debounceSearch() {
+ this.loadMergeRequests();
+ }, 250),
+ focusSearch() {
+ if (!this.isLoading) {
+ this.$nextTick(() => {
+ this.$refs.searchInput.focus();
+ });
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div class="dropdown-input mt-3 pb-3 mb-0 border-bottom">
+ <input
+ ref="searchInput"
+ :placeholder="__('Search merge requests')"
+ v-model="search"
+ type="search"
+ class="dropdown-input-field"
+ @input="searchMergeRequests"
+ />
+ <i
+ aria-hidden="true"
+ class="fa fa-search dropdown-input-search"
+ ></i>
+ </div>
+ <div class="dropdown-content ide-merge-requests-dropdown-content d-flex">
+ <loading-icon
+ v-if="isLoading"
+ class="mt-3 mb-3 align-self-center ml-auto mr-auto"
+ size="2"
+ />
+ <ul
+ v-else
+ class="mb-3 w-100"
+ >
+ <template v-if="hasMergeRequests">
+ <li
+ v-for="item in mergeRequests"
+ :key="item.id"
+ >
+ <item
+ :item="item"
+ :current-id="currentMergeRequestId"
+ :current-project-id="currentProjectId"
+ @click="viewMergeRequest"
+ />
+ </li>
+ </template>
+ <li
+ v-else
+ class="ide-merge-requests-empty d-flex align-items-center justify-content-center"
+ >
+ <template v-if="hasNoSearchResults">
+ {{ __('No merge requests found') }}
+ </template>
+ <template v-else>
+ {{ emptyText }}
+ </template>
+ </li>
+ </ul>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/mr_file_icon.vue b/app/assets/javascripts/ide/components/mr_file_icon.vue
index 179a589d1ac..821be319cce 100644
--- a/app/assets/javascripts/ide/components/mr_file_icon.vue
+++ b/app/assets/javascripts/ide/components/mr_file_icon.vue
@@ -14,10 +14,10 @@ export default {
<template>
<icon
- name="git-merge"
v-tooltip
:title="__('Part of merge request changes')"
- css-classes="append-right-8"
:size="12"
+ name="git-merge"
+ css-classes="append-right-8"
/>
</template>
diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue
index a0ce1c9dac7..1e398d7e1aa 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/index.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue
@@ -55,10 +55,10 @@ export default {
<template>
<div class="ide-new-btn">
<div
- class="dropdown"
:class="{
- open: dropdownOpen,
+ show: dropdownOpen,
}"
+ class="dropdown"
>
<button
type="button"
@@ -67,19 +67,19 @@ export default {
@click.stop="openDropdown()"
>
<icon
- name="plus"
:size="12"
- css-classes="pull-left"
+ name="plus"
+ css-classes="float-left"
/>
<icon
- name="arrow-down"
:size="12"
- css-classes="pull-left"
+ name="arrow-down"
+ css-classes="float-left"
/>
</button>
<ul
- class="dropdown-menu dropdown-menu-right"
ref="dropdownMenu"
+ class="dropdown-menu dropdown-menu-right"
>
<li>
<a
diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
index a95a0225950..1e9668d5154 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
@@ -70,23 +70,21 @@ export default {
@submit="createEntryInStore"
>
<form
- class="form-horizontal"
slot="body"
+ class="form-group row"
@submit.prevent="createEntryInStore"
>
- <fieldset class="form-group append-bottom-0">
- <label class="label-light col-sm-3 ide-new-modal-label">
- {{ __('Name') }}
- </label>
- <div class="col-sm-9">
- <input
- type="text"
- class="form-control"
- v-model="entryName"
- ref="fieldName"
- />
- </div>
- </fieldset>
+ <label class="label-light col-form-label col-sm-3">
+ {{ __('Name') }}
+ </label>
+ <div class="col-sm-9">
+ <input
+ ref="fieldName"
+ v-model="entryName"
+ type="text"
+ class="form-control"
+ />
+ </div>
</form>
</deprecated-modal>
</template>
diff --git a/app/assets/javascripts/ide/components/new_dropdown/upload.vue b/app/assets/javascripts/ide/components/new_dropdown/upload.vue
index c165af5ce52..677b282bd61 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/upload.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/upload.vue
@@ -23,6 +23,7 @@
let { result } = target;
if (!isText) {
+ // eslint-disable-next-line prefer-destructuring
result = result.split('base64,')[1];
}
@@ -67,9 +68,9 @@
</a>
<input
id="file-upload"
+ ref="fileUpload"
type="file"
class="hidden"
- ref="fileUpload"
/>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/panes/right.vue b/app/assets/javascripts/ide/components/panes/right.vue
new file mode 100644
index 00000000000..e4a5fcc67c4
--- /dev/null
+++ b/app/assets/javascripts/ide/components/panes/right.vue
@@ -0,0 +1,104 @@
+<script>
+import { mapActions, mapState } from 'vuex';
+import tooltip from '../../../vue_shared/directives/tooltip';
+import Icon from '../../../vue_shared/components/icon.vue';
+import { rightSidebarViews } from '../../constants';
+import PipelinesList from '../pipelines/list.vue';
+import JobsDetail from '../jobs/detail.vue';
+import MergeRequestInfo from '../merge_requests/info.vue';
+import ResizablePanel from '../resizable_panel.vue';
+
+export default {
+ directives: {
+ tooltip,
+ },
+ components: {
+ Icon,
+ PipelinesList,
+ JobsDetail,
+ ResizablePanel,
+ MergeRequestInfo,
+ },
+ computed: {
+ ...mapState(['rightPane', 'currentMergeRequestId']),
+ pipelinesActive() {
+ return (
+ this.rightPane === rightSidebarViews.pipelines ||
+ this.rightPane === rightSidebarViews.jobsDetail
+ );
+ },
+ },
+ methods: {
+ ...mapActions(['setRightPane']),
+ clickTab(e, view) {
+ e.target.blur();
+
+ this.setRightPane(view);
+ },
+ },
+ rightSidebarViews,
+};
+</script>
+
+<template>
+ <div
+ class="multi-file-commit-panel ide-right-sidebar"
+ >
+ <resizable-panel
+ v-if="rightPane"
+ :collapsible="false"
+ :initial-width="350"
+ :min-size="350"
+ class="multi-file-commit-panel-inner"
+ side="right"
+ >
+ <component :is="rightPane" />
+ </resizable-panel>
+ <nav class="ide-activity-bar">
+ <ul class="list-unstyled">
+ <li
+ v-if="currentMergeRequestId"
+ >
+ <button
+ v-tooltip
+ :title="__('Merge Request')"
+ :aria-label="__('Merge Request')"
+ :class="{
+ active: rightPane === $options.rightSidebarViews.mergeRequestInfo
+ }"
+ data-container="body"
+ data-placement="left"
+ class="ide-sidebar-link is-right"
+ type="button"
+ @click="clickTab($event, $options.rightSidebarViews.mergeRequestInfo)"
+ >
+ <icon
+ :size="16"
+ name="text-description"
+ />
+ </button>
+ </li>
+ <li>
+ <button
+ v-tooltip
+ :title="__('Pipelines')"
+ :aria-label="__('Pipelines')"
+ :class="{
+ active: pipelinesActive
+ }"
+ data-container="body"
+ data-placement="left"
+ class="ide-sidebar-link is-right"
+ type="button"
+ @click="clickTab($event, $options.rightSidebarViews.pipelines)"
+ >
+ <icon
+ :size="16"
+ name="rocket"
+ />
+ </button>
+ </li>
+ </ul>
+ </nav>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/pipelines/list.vue b/app/assets/javascripts/ide/components/pipelines/list.vue
new file mode 100644
index 00000000000..5757dfdc925
--- /dev/null
+++ b/app/assets/javascripts/ide/components/pipelines/list.vue
@@ -0,0 +1,146 @@
+<script>
+import { mapActions, mapGetters, mapState } from 'vuex';
+import _ from 'underscore';
+import { sprintf, __ } from '../../../locale';
+import LoadingIcon from '../../../vue_shared/components/loading_icon.vue';
+import Icon from '../../../vue_shared/components/icon.vue';
+import CiIcon from '../../../vue_shared/components/ci_icon.vue';
+import Tabs from '../../../vue_shared/components/tabs/tabs';
+import Tab from '../../../vue_shared/components/tabs/tab.vue';
+import EmptyState from '../../../pipelines/components/empty_state.vue';
+import JobsList from '../jobs/list.vue';
+
+export default {
+ components: {
+ LoadingIcon,
+ Icon,
+ CiIcon,
+ Tabs,
+ Tab,
+ JobsList,
+ EmptyState,
+ },
+ computed: {
+ ...mapState(['pipelinesEmptyStateSvgPath', 'links']),
+ ...mapGetters(['currentProject']),
+ ...mapGetters('pipelines', ['jobsCount', 'failedJobsCount', 'failedStages', 'pipelineFailed']),
+ ...mapState('pipelines', ['isLoadingPipeline', 'latestPipeline', 'stages', 'isLoadingJobs']),
+ ciLintText() {
+ return sprintf(
+ __('You can also test your .gitlab-ci.yml in the %{linkStart}Lint%{linkEnd}'),
+ {
+ linkStart: `<a href="${_.escape(this.currentProject.web_url)}/-/ci/lint">`,
+ linkEnd: '</a>',
+ },
+ false,
+ );
+ },
+ showLoadingIcon() {
+ return this.isLoadingPipeline && this.latestPipeline === null;
+ },
+ },
+ created() {
+ this.fetchLatestPipeline();
+ },
+ methods: {
+ ...mapActions('pipelines', ['fetchLatestPipeline']),
+ },
+};
+</script>
+
+<template>
+ <div class="ide-pipeline">
+ <loading-icon
+ v-if="showLoadingIcon"
+ class="prepend-top-default"
+ size="2"
+ />
+ <template v-else-if="latestPipeline !== null">
+ <header
+ v-if="latestPipeline"
+ class="ide-tree-header ide-pipeline-header"
+ >
+ <ci-icon
+ :status="latestPipeline.details.status"
+ :size="24"
+ />
+ <span class="prepend-left-8">
+ <strong>
+ {{ __('Pipeline') }}
+ </strong>
+ <a
+ :href="latestPipeline.path"
+ target="_blank"
+ class="ide-external-link"
+ >
+ #{{ latestPipeline.id }}
+ <icon
+ :size="12"
+ name="external-link"
+ />
+ </a>
+ </span>
+ </header>
+ <empty-state
+ v-if="latestPipeline === false"
+ :help-page-path="links.ciHelpPagePath"
+ :empty-state-svg-path="pipelinesEmptyStateSvgPath"
+ :can-set-ci="true"
+ />
+ <div
+ v-else-if="latestPipeline.yamlError"
+ class="bs-callout bs-callout-danger"
+ >
+ <p class="append-bottom-0">
+ {{ __('Found errors in your .gitlab-ci.yml:') }}
+ </p>
+ <p class="append-bottom-0 break-word">
+ {{ latestPipeline.yamlError }}
+ </p>
+ <p
+ class="append-bottom-0"
+ v-html="ciLintText"
+ ></p>
+ </div>
+ <tabs
+ v-else
+ class="ide-pipeline-list"
+ >
+ <tab
+ :active="!pipelineFailed"
+ >
+ <template slot="title">
+ {{ __('Jobs') }}
+ <span
+ v-if="jobsCount"
+ class="badge badge-pill"
+ >
+ {{ jobsCount }}
+ </span>
+ </template>
+ <jobs-list
+ :loading="isLoadingJobs"
+ :stages="stages"
+ />
+ </tab>
+ <tab
+ :active="pipelineFailed"
+ >
+ <template slot="title">
+ {{ __('Failed Jobs') }}
+ <span
+ v-if="failedJobsCount"
+ class="badge badge-pill"
+ >
+ {{ failedJobsCount }}
+ </span>
+ </template>
+ <jobs-list
+ :loading="isLoadingJobs"
+ :stages="failedStages"
+ />
+ </tab>
+ </tabs>
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue
index c5092d8e04d..50ab242ba2a 100644
--- a/app/assets/javascripts/ide/components/repo_commit_section.vue
+++ b/app/assets/javascripts/ide/components/repo_commit_section.vue
@@ -6,7 +6,7 @@ import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import CommitFilesList from './commit_sidebar/list.vue';
import EmptyState from './commit_sidebar/empty_state.vue';
import * as consts from '../stores/modules/commit/constants';
-import { activityBarViews } from '../constants';
+import { activityBarViews, stageKeys } from '../constants';
export default {
components: {
@@ -27,11 +27,14 @@ export default {
'unusedSeal',
]),
...mapState('commit', ['commitMessage', 'submitCommitLoading']),
- ...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommitedChanges']),
- ...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']),
+ ...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommitedChanges', 'activeFile']),
+ ...mapGetters('commit', ['discardDraftButtonDisabled']),
showStageUnstageArea() {
return !!(this.someUncommitedChanges || this.lastCommitMsg || !this.unusedSeal);
},
+ activeFileKey() {
+ return this.activeFile ? this.activeFile.key : null;
+ },
},
watch: {
hasChanges() {
@@ -44,6 +47,7 @@ export default {
if (this.lastOpenedFile) {
this.openPendingTab({
file: this.lastOpenedFile,
+ keyPrefix: this.lastOpenedFile.changed ? stageKeys.unstaged : stageKeys.staged,
})
.then(changeViewer => {
if (changeViewer) {
@@ -62,6 +66,7 @@ export default {
return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commitChanges());
},
},
+ stageKeys,
};
</script>
@@ -72,8 +77,8 @@ export default {
<deprecated-modal
id="ide-create-branch-modal"
:primary-button-label="__('Create new branch')"
- kind="success"
:title="__('Branch has changed')"
+ kind="success"
@submit="forceCreateNewBranch"
>
<template slot="body">
@@ -85,22 +90,28 @@ export default {
v-if="showStageUnstageArea"
>
<commit-files-list
- class="is-first"
- icon-name="unstaged"
:title="__('Unstaged')"
+ :key-prefix="$options.stageKeys.unstaged"
:file-list="changedFiles"
+ :action-btn-text="__('Stage all changes')"
+ :active-file-key="activeFileKey"
action="stageAllChanges"
- :action-btn-text="__('Stage all')"
+ action-btn-icon="mobile-issue-close"
item-action-component="stage-button"
+ class="is-first"
+ icon-name="unstaged"
/>
<commit-files-list
- icon-name="staged"
:title="__('Staged')"
+ :key-prefix="$options.stageKeys.staged"
:file-list="stagedFiles"
+ :action-btn-text="__('Unstage all changes')"
+ :staged-list="true"
+ :active-file-key="activeFileKey"
action="unstageAllChanges"
- :action-btn-text="__('Unstage all')"
+ action-btn-icon="history"
item-action-component="unstage-button"
- :staged-list="true"
+ icon-name="staged"
/>
</template>
<empty-state
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index f2178c06c10..08ee12fd98f 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -1,17 +1,17 @@
<script>
-/* global monaco */
import { mapState, mapGetters, mapActions } from 'vuex';
import flash from '~/flash';
import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
+import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
import { activityBarViews, viewerTypes } from '../constants';
-import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor';
-import IdeFileButtons from './ide_file_buttons.vue';
+import ExternalLink from './external_link.vue';
export default {
components: {
ContentViewer,
- IdeFileButtons,
+ DiffViewer,
+ ExternalLink,
},
props: {
file: {
@@ -20,7 +20,13 @@ export default {
},
},
computed: {
- ...mapState(['rightPanelCollapsed', 'viewer', 'panelResizing', 'currentActivityView']),
+ ...mapState([
+ 'rightPanelCollapsed',
+ 'viewer',
+ 'panelResizing',
+ 'currentActivityView',
+ 'rightPane',
+ ]),
...mapGetters([
'currentMergeRequest',
'getStagedFile',
@@ -31,9 +37,18 @@ export default {
shouldHideEditor() {
return this.file && this.file.binary && !this.file.content;
},
+ showContentViewer() {
+ return (
+ (this.shouldHideEditor || this.file.viewMode === 'preview') &&
+ (this.viewer !== viewerTypes.mr || !this.file.mrChange)
+ );
+ },
+ showDiffViewer() {
+ return this.shouldHideEditor && this.file.mrChange && this.viewer === viewerTypes.mr;
+ },
editTabCSS() {
return {
- active: this.file.viewMode === 'edit',
+ active: this.file.viewMode === 'editor',
};
},
previewTabCSS() {
@@ -50,12 +65,12 @@ export default {
// Compare key to allow for files opened in review mode to be cached differently
if (oldVal.key !== this.file.key) {
- this.initMonaco();
+ this.initEditor();
if (this.currentActivityView !== activityBarViews.edit) {
this.setFileViewMode({
file: this.file,
- viewMode: 'edit',
+ viewMode: 'editor',
});
}
}
@@ -64,7 +79,7 @@ export default {
if (this.currentActivityView !== activityBarViews.edit) {
this.setFileViewMode({
file: this.file,
- viewMode: 'edit',
+ viewMode: 'editor',
});
}
},
@@ -79,20 +94,18 @@ export default {
this.editor.updateDimensions();
}
},
+ rightPane() {
+ this.editor.updateDimensions();
+ },
},
beforeDestroy() {
this.editor.dispose();
},
mounted() {
- if (this.editor && monaco) {
- this.initMonaco();
- } else {
- monacoLoader(['vs/editor/editor.main'], () => {
- this.editor = Editor.create(monaco);
-
- this.initMonaco();
- });
+ if (!this.editor) {
+ this.editor = Editor.create();
}
+ this.initEditor();
},
methods: {
...mapActions([
@@ -105,7 +118,7 @@ export default {
'updateViewer',
'removePendingTab',
]),
- initMonaco() {
+ initEditor() {
if (this.shouldHideEditor) return;
this.editor.clearEditor();
@@ -118,7 +131,7 @@ export default {
this.createEditorInstance();
})
.catch(err => {
- flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true);
+ flash('Error setting up editor. Please try again.', 'alert', document, null, false, true);
throw err;
});
},
@@ -197,14 +210,14 @@ export default {
>
<div class="ide-mode-tabs clearfix" >
<ul
- class="nav-links pull-left"
v-if="!shouldHideEditor && isEditModeActive"
+ class="nav-links float-left"
>
<li :class="editTabCSS">
<a
href="javascript:void(0);"
role="button"
- @click.prevent="setFileViewMode({ file, viewMode: 'edit' })">
+ @click.prevent="setFileViewMode({ file, viewMode: 'editor' })">
<template v-if="viewer === $options.viewerTypes.edit">
{{ __('Edit') }}
</template>
@@ -224,24 +237,32 @@ export default {
</a>
</li>
</ul>
- <ide-file-buttons
+ <external-link
:file="file"
/>
</div>
<div
- v-show="!shouldHideEditor && file.viewMode === 'edit'"
+ v-show="!shouldHideEditor && file.viewMode ==='editor'"
ref="editor"
- class="multi-file-editor-holder"
:class="{
'is-readonly': isCommitModeActive,
}"
+ class="multi-file-editor-holder"
>
</div>
<content-viewer
- v-if="shouldHideEditor || file.viewMode === 'preview'"
+ v-if="showContentViewer"
:content="file.content || file.raw"
:path="file.rawPath || file.path"
:file-size="file.size"
:project-path="file.projectId"/>
+ <diff-viewer
+ v-if="showDiffViewer"
+ :diff-mode="file.mrChange.diffMode"
+ :new-path="file.mrChange.new_path"
+ :new-sha="currentMergeRequest.sha"
+ :old-path="file.mrChange.old_path"
+ :old-sha="currentMergeRequest.baseCommitSha"
+ :project-path="file.projectId"/>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue
index 7bc865058c6..f490a3a2a39 100644
--- a/app/assets/javascripts/ide/components/repo_file.vue
+++ b/app/assets/javascripts/ide/components/repo_file.vue
@@ -95,24 +95,53 @@ export default {
return this.file.changed || this.file.tempFile || this.file.staged;
},
},
+ mounted() {
+ if (this.hasPathAtCurrentRoute()) {
+ this.scrollIntoView(true);
+ }
+ },
updated() {
if (this.file.type === 'blob' && this.file.active) {
- this.$el.scrollIntoView({
- behavior: 'smooth',
- block: 'nearest',
- });
+ this.scrollIntoView();
}
},
methods: {
...mapActions(['toggleTreeOpen']),
clickFile() {
// Manual Action if a tree is selected/opened
- if (this.isTree && this.$router.currentRoute.path === `/project${this.file.url}`) {
+ if (this.isTree && this.hasUrlAtCurrentRoute()) {
this.toggleTreeOpen(this.file.path);
}
router.push(`/project${this.file.url}`);
},
+ scrollIntoView(isInit = false) {
+ const block = isInit && this.isTree ? 'center' : 'nearest';
+
+ this.$el.scrollIntoView({
+ behavior: 'smooth',
+ block,
+ });
+ },
+ hasPathAtCurrentRoute() {
+ if (!this.$router || !this.$router.currentRoute) {
+ return false;
+ }
+
+ // - strip route up to "/-/" and ending "/"
+ const routePath = this.$router.currentRoute.path
+ .replace(/^.*?[/]-[/]/g, '')
+ .replace(/[/]$/g, '');
+
+ // - strip ending "/"
+ const filePath = this.file.path
+ .replace(/[/]$/g, '');
+
+ return filePath === routePath;
+ },
+ hasUrlAtCurrentRoute() {
+ return this.$router.currentRoute.path === `/project${this.file.url}`;
+ },
},
};
</script>
@@ -120,17 +149,17 @@ export default {
<template>
<div>
<div
- class="file"
:class="fileClass"
- @click="clickFile"
+ class="file"
role="button"
+ @click="clickFile"
>
<div
class="file-name"
>
<span
- class="ide-file-name str-truncated"
:style="levelIndentation"
+ class="ide-file-name str-truncated"
>
<file-icon
:file-name="file.name"
@@ -144,7 +173,7 @@ export default {
:file="file"
/>
</span>
- <span class="pull-right ide-file-icon-holder">
+ <span class="float-right ide-file-icon-holder">
<mr-file-icon
v-if="file.mrChange"
/>
@@ -156,10 +185,10 @@ export default {
<icon
v-tooltip
:title="folderChangesTooltip"
+ :size="12"
data-container="body"
data-placement="right"
name="file-modified"
- :size="12"
css-classes="prepend-left-5 multi-file-modified"
/>
</span>
@@ -169,7 +198,7 @@ export default {
:show-tooltip="true"
:show-staged-icon="true"
:force-modified-icon="true"
- class="pull-right"
+ class="float-right"
/>
</span>
<new-dropdown
@@ -177,7 +206,7 @@ export default {
:project-id="file.projectId"
:branch="file.branchId"
:path="file.path"
- class="pull-right prepend-left-8"
+ class="float-right prepend-left-8"
/>
</div>
</div>
diff --git a/app/assets/javascripts/ide/components/repo_file_status_icon.vue b/app/assets/javascripts/ide/components/repo_file_status_icon.vue
index 97589e116c5..76a3333be50 100644
--- a/app/assets/javascripts/ide/components/repo_file_status_icon.vue
+++ b/app/assets/javascripts/ide/components/repo_file_status_icon.vue
@@ -26,8 +26,8 @@ export default {
<template>
<span
- v-if="file.file_lock"
v-tooltip
+ v-if="file.file_lock"
:title="lockTooltip"
data-container="body"
>
diff --git a/app/assets/javascripts/ide/components/repo_loading_file.vue b/app/assets/javascripts/ide/components/repo_loading_file.vue
index 79af8c0b0c7..7a5ede82253 100644
--- a/app/assets/javascripts/ide/components/repo_loading_file.vue
+++ b/app/assets/javascripts/ide/components/repo_loading_file.vue
@@ -25,16 +25,16 @@
/>
</td>
<template v-if="!leftPanelCollapsed">
- <td class="hidden-sm hidden-xs">
+ <td class="d-none d-sm-none d-md-block">
<skeleton-loading-container
:small="true"
/>
</td>
- <td class="hidden-xs">
+ <td class="d-none d-sm-block">
<skeleton-loading-container
- class="animation-container-right"
:small="true"
+ class="animation-container-right"
/>
</td>
</template>
diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue
index fb26b973236..03772ae4a4c 100644
--- a/app/assets/javascripts/ide/components/repo_tab.vue
+++ b/app/assets/javascripts/ide/components/repo_tab.vue
@@ -44,6 +44,8 @@ export default {
methods: {
...mapActions(['closeFile', 'updateDelayViewerUpdated', 'openPendingTab']),
clickFile(tab) {
+ if (tab.active) return;
+
this.updateDelayViewerUpdated(true);
if (tab.pending) {
@@ -76,8 +78,8 @@ export default {
@mouseout="mouseOutTab"
>
<div
- class="multi-file-tab"
:title="tab.url"
+ class="multi-file-tab"
>
<file-icon
:file-name="tab.name"
@@ -89,16 +91,16 @@ export default {
/>
</div>
<button
+ :aria-label="closeLabel"
+ :disabled="tab.pending"
type="button"
class="multi-file-tab-close"
@click.stop.prevent="closeFile(tab)"
- :aria-label="closeLabel"
- :disabled="tab.pending"
>
<icon
v-if="!showChangedIcon"
- name="close"
:size="12"
+ name="close"
/>
<changed-file-icon
v-else
diff --git a/app/assets/javascripts/ide/components/repo_tabs.vue b/app/assets/javascripts/ide/components/repo_tabs.vue
index 99e51097e12..c12a63e26be 100644
--- a/app/assets/javascripts/ide/components/repo_tabs.vue
+++ b/app/assets/javascripts/ide/components/repo_tabs.vue
@@ -52,8 +52,8 @@ export default {
<template>
<div class="multi-file-tabs">
<ul
- class="list-unstyled append-bottom-0"
ref="tabsScroller"
+ class="list-unstyled append-bottom-0"
>
<repo-tab
v-for="tab in files"
diff --git a/app/assets/javascripts/ide/components/resizable_panel.vue b/app/assets/javascripts/ide/components/resizable_panel.vue
index 5ea2a2f6825..7277fcb7617 100644
--- a/app/assets/javascripts/ide/components/resizable_panel.vue
+++ b/app/assets/javascripts/ide/components/resizable_panel.vue
@@ -63,11 +63,11 @@ export default {
<template>
<div
- class="multi-file-commit-panel"
:class="{
'is-collapsed': collapsed && collapsible,
}"
:style="panelStyle"
+ class="multi-file-commit-panel"
@click="toggleFullbarCollapsed"
>
<slot></slot>
@@ -77,9 +77,9 @@ export default {
:start-size="initialWidth"
:min-size="minSize"
:max-size="$options.maxSize"
+ :side="side === 'right' ? 'left' : 'right'"
@resize-start="setResizingStatus(true)"
@resize-end="setResizingStatus(false)"
- :side="side === 'right' ? 'left' : 'right'"
/>
</div>
</template>
diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js
index 83fe22f40a4..45d36f6f42c 100644
--- a/app/assets/javascripts/ide/constants.js
+++ b/app/assets/javascripts/ide/constants.js
@@ -20,3 +20,21 @@ export const viewerTypes = {
edit: 'editor',
diff: 'diff',
};
+
+export const diffModes = {
+ replaced: 'replaced',
+ new: 'new',
+ deleted: 'deleted',
+ renamed: 'renamed',
+};
+
+export const rightSidebarViews = {
+ pipelines: 'pipelines-list',
+ jobsDetail: 'jobs-detail',
+ mergeRequestInfo: 'merge-request-info',
+};
+
+export const stageKeys = {
+ unstaged: 'unstaged',
+ staged: 'staged',
+};
diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js
index a21cec4e8d8..cc8dbb942d8 100644
--- a/app/assets/javascripts/ide/ide_router.js
+++ b/app/assets/javascripts/ide/ide_router.js
@@ -63,7 +63,7 @@ router.beforeEach((to, from, next) => {
.then(() => {
const fullProjectId = `${to.params.namespace}/${to.params.project}`;
- const baseSplit = to.params[0].split('/-/');
+ const baseSplit = (to.params[0] && to.params[0].split('/-/')) || [''];
const branchId = baseSplit[0].slice(-1) === '/' ? baseSplit[0].slice(0, -1) : baseSplit[0];
if (branchId) {
@@ -95,14 +95,6 @@ router.beforeEach((to, from, next) => {
}
})
.catch(e => {
- flash(
- 'Error while loading the branch files. Please try again.',
- 'alert',
- document,
- null,
- false,
- true,
- );
throw e;
});
} else if (to.params.mrid) {
diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js
index c5835cd3b06..2d74192e6b3 100644
--- a/app/assets/javascripts/ide/index.js
+++ b/app/assets/javascripts/ide/index.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import { mapActions } from 'vuex';
import Translate from '~/vue_shared/translate';
import ide from './components/ide.vue';
import store from './stores';
@@ -17,11 +18,18 @@ export function initIde(el) {
ide,
},
created() {
- this.$store.dispatch('setEmptyStateSvgs', {
+ this.setEmptyStateSvgs({
emptyStateSvgPath: el.dataset.emptyStateSvgPath,
noChangesStateSvgPath: el.dataset.noChangesStateSvgPath,
committedStateSvgPath: el.dataset.committedStateSvgPath,
+ pipelinesEmptyStateSvgPath: el.dataset.pipelinesEmptyStateSvgPath,
});
+ this.setLinks({
+ ciHelpPagePath: el.dataset.ciHelpPagePath,
+ });
+ },
+ methods: {
+ ...mapActions(['setEmptyStateSvgs', 'setLinks']),
},
render(createElement) {
return createElement('ide');
diff --git a/app/assets/javascripts/ide/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js
index b1e43a1e38c..78e6f632728 100644
--- a/app/assets/javascripts/ide/lib/common/model.js
+++ b/app/assets/javascripts/ide/lib/common/model.js
@@ -1,33 +1,32 @@
-/* global monaco */
+import { editor as monacoEditor, Uri } from 'monaco-editor';
import Disposable from './disposable';
import eventHub from '../../eventhub';
export default class Model {
- constructor(monaco, file, head = null) {
- this.monaco = monaco;
+ constructor(file, head = null) {
this.disposable = new Disposable();
this.file = file;
this.head = head;
this.content = file.content !== '' ? file.content : file.raw;
this.disposable.add(
- (this.originalModel = this.monaco.editor.createModel(
+ (this.originalModel = monacoEditor.createModel(
head ? head.content : this.file.raw,
undefined,
- new this.monaco.Uri(null, null, `original/${this.path}`),
+ new Uri(false, false, `original/${this.path}`),
)),
- (this.model = this.monaco.editor.createModel(
+ (this.model = monacoEditor.createModel(
this.content,
undefined,
- new this.monaco.Uri(null, null, this.path),
+ new Uri(false, false, this.path),
)),
);
if (this.file.mrChange) {
this.disposable.add(
- (this.baseModel = this.monaco.editor.createModel(
+ (this.baseModel = monacoEditor.createModel(
this.file.baseRaw,
undefined,
- new this.monaco.Uri(null, null, `target/${this.path}`),
+ new Uri(false, false, `target/${this.path}`),
)),
);
}
diff --git a/app/assets/javascripts/ide/lib/common/model_manager.js b/app/assets/javascripts/ide/lib/common/model_manager.js
index 7f643969480..bd9b8fc3fcc 100644
--- a/app/assets/javascripts/ide/lib/common/model_manager.js
+++ b/app/assets/javascripts/ide/lib/common/model_manager.js
@@ -3,8 +3,7 @@ import Disposable from './disposable';
import Model from './model';
export default class ModelManager {
- constructor(monaco) {
- this.monaco = monaco;
+ constructor() {
this.disposable = new Disposable();
this.models = new Map();
}
@@ -22,7 +21,7 @@ export default class ModelManager {
return this.getModel(file.key);
}
- const model = new Model(this.monaco, file, head);
+ const model = new Model(file, head);
this.models.set(model.path, model);
this.disposable.add(model);
diff --git a/app/assets/javascripts/ide/lib/diff/controller.js b/app/assets/javascripts/ide/lib/diff/controller.js
index f579424cf33..046e562ba2b 100644
--- a/app/assets/javascripts/ide/lib/diff/controller.js
+++ b/app/assets/javascripts/ide/lib/diff/controller.js
@@ -1,4 +1,4 @@
-/* global monaco */
+import { Range } from 'monaco-editor';
import { throttle } from 'underscore';
import DirtyDiffWorker from './diff_worker';
import Disposable from '../common/disposable';
@@ -16,7 +16,7 @@ export const getDiffChangeType = change => {
};
export const getDecorator = change => ({
- range: new monaco.Range(change.lineNumber, 1, change.endLineNumber, 1),
+ range: new Range(change.lineNumber, 1, change.endLineNumber, 1),
options: {
isWholeLine: true,
linesDecorationsClassName: `dirty-diff dirty-diff-${getDiffChangeType(change)}`,
diff --git a/app/assets/javascripts/ide/lib/diff/diff_worker.js b/app/assets/javascripts/ide/lib/diff/diff_worker.js
index e74c4046330..78b2eab6399 100644
--- a/app/assets/javascripts/ide/lib/diff/diff_worker.js
+++ b/app/assets/javascripts/ide/lib/diff/diff_worker.js
@@ -1,8 +1,10 @@
import { computeDiff } from './diff';
+// eslint-disable-next-line no-restricted-globals
self.addEventListener('message', (e) => {
- const data = e.data;
+ const { data } = e;
+ // eslint-disable-next-line no-restricted-globals
self.postMessage({
path: data.path,
changes: computeDiff(data.originalContent, data.newContent),
diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js
index 9c3bb9cc17d..02038fcb534 100644
--- a/app/assets/javascripts/ide/lib/editor.js
+++ b/app/assets/javascripts/ide/lib/editor.js
@@ -1,4 +1,5 @@
import _ from 'underscore';
+import { editor as monacoEditor, KeyCode, KeyMod } from 'monaco-editor';
import store from '../stores';
import DecorationsController from './decorations/controller';
import DirtyDiffController from './diff/controller';
@@ -8,6 +9,11 @@ import editorOptions, { defaultEditorOptions } from './editor_options';
import gitlabTheme from './themes/gl_theme';
import keymap from './keymap.json';
+function setupMonacoTheme() {
+ monacoEditor.defineTheme(gitlabTheme.themeName, gitlabTheme.monacoTheme);
+ monacoEditor.setTheme('gitlab');
+}
+
export const clearDomElement = el => {
if (!el || !el.firstChild) return;
@@ -17,24 +23,22 @@ export const clearDomElement = el => {
};
export default class Editor {
- static create(monaco) {
- if (this.editorInstance) return this.editorInstance;
-
- this.editorInstance = new Editor(monaco);
-
+ static create() {
+ if (!this.editorInstance) {
+ this.editorInstance = new Editor();
+ }
return this.editorInstance;
}
- constructor(monaco) {
- this.monaco = monaco;
+ constructor() {
this.currentModel = null;
this.instance = null;
this.dirtyDiffController = null;
this.disposable = new Disposable();
- this.modelManager = new ModelManager(this.monaco);
+ this.modelManager = new ModelManager();
this.decorationsController = new DecorationsController(this);
- this.setupMonacoTheme();
+ setupMonacoTheme();
this.debouncedUpdate = _.debounce(() => {
this.updateDimensions();
@@ -46,7 +50,7 @@ export default class Editor {
clearDomElement(domElement);
this.disposable.add(
- (this.instance = this.monaco.editor.create(domElement, {
+ (this.instance = monacoEditor.create(domElement, {
...defaultEditorOptions,
})),
(this.dirtyDiffController = new DirtyDiffController(
@@ -66,7 +70,7 @@ export default class Editor {
clearDomElement(domElement);
this.disposable.add(
- (this.instance = this.monaco.editor.createDiffEditor(domElement, {
+ (this.instance = monacoEditor.createDiffEditor(domElement, {
...defaultEditorOptions,
quickSuggestions: false,
occurrencesHighlight: false,
@@ -122,17 +126,11 @@ export default class Editor {
modified: model.getModel(),
});
- this.monaco.editor.createDiffNavigator(this.instance, {
+ monacoEditor.createDiffNavigator(this.instance, {
alwaysRevealFirst: true,
});
}
- setupMonacoTheme() {
- this.monaco.editor.defineTheme(gitlabTheme.themeName, gitlabTheme.monacoTheme);
-
- this.monaco.editor.setTheme('gitlab');
- }
-
clearEditor() {
if (this.instance) {
this.instance.setModel(null);
@@ -200,7 +198,7 @@ export default class Editor {
const getKeyCode = key => {
const monacoKeyMod = key.indexOf('KEY_') === 0;
- return monacoKeyMod ? this.monaco.KeyCode[key] : this.monaco.KeyMod[key];
+ return monacoKeyMod ? KeyCode[key] : KeyMod[key];
};
keymap.forEach(command => {
diff --git a/app/assets/javascripts/ide/lib/editor_options.js b/app/assets/javascripts/ide/lib/editor_options.js
index 9f895d49f2e..e35595ab1fd 100644
--- a/app/assets/javascripts/ide/lib/editor_options.js
+++ b/app/assets/javascripts/ide/lib/editor_options.js
@@ -12,5 +12,6 @@ export const defaultEditorOptions = {
export default [
{
readOnly: model => !!model.file.file_lock,
+ quickSuggestions: model => !(model.language === 'markdown'),
},
];
diff --git a/app/assets/javascripts/ide/lib/themes/gl_theme.js b/app/assets/javascripts/ide/lib/themes/gl_theme.js
index 2fc96250c7d..439ae50448a 100644
--- a/app/assets/javascripts/ide/lib/themes/gl_theme.js
+++ b/app/assets/javascripts/ide/lib/themes/gl_theme.js
@@ -9,6 +9,7 @@ export default {
'diffEditor.insertedTextBackground': '#ddfbe6',
'diffEditor.removedTextBackground': '#f9d7dc',
'editor.selectionBackground': '#aad6f8',
+ 'editorIndentGuide.activeBackground': '#cccccc',
},
},
};
diff --git a/app/assets/javascripts/ide/monaco_loader.js b/app/assets/javascripts/ide/monaco_loader.js
deleted file mode 100644
index 142a220097b..00000000000
--- a/app/assets/javascripts/ide/monaco_loader.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import monacoContext from 'monaco-editor/dev/vs/loader';
-
-monacoContext.require.config({
- paths: {
- vs: `${__webpack_public_path__}monaco-editor/vs`, // eslint-disable-line camelcase
- },
-});
-
-// ignore CDN config and use local assets path for service worker which cannot be cross-domain
-const relativeRootPath = (gon && gon.relative_url_root) || '';
-const monacoPath = `${relativeRootPath}/assets/webpack/monaco-editor/vs`;
-window.MonacoEnvironment = { getWorkerUrl: () => `${monacoPath}/base/worker/workerMain.js` };
-
-// eslint-disable-next-line no-underscore-dangle
-window.__monaco_context__ = monacoContext;
-export default monacoContext.require;
diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js
index a12e637616a..49a481f25d5 100644
--- a/app/assets/javascripts/ide/services/index.js
+++ b/app/assets/javascripts/ide/services/index.js
@@ -1,15 +1,11 @@
-import Vue from 'vue';
-import VueResource from 'vue-resource';
+import axios from '~/lib/utils/axios_utils';
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' } });
+ return axios.get(endpoint, {
+ params: { format: 'json', viewer: 'none' },
+ });
},
getRawFileData(file) {
if (file.tempFile) {
@@ -20,7 +16,11 @@ export default {
return Promise.resolve(file.raw);
}
- return Vue.http.get(file.rawPath, { params: { format: 'json' } }).then(res => res.text());
+ return axios
+ .get(file.rawPath, {
+ params: { format: 'json' },
+ })
+ .then(({ data }) => data);
},
getBaseRawFileData(file, sha) {
if (file.tempFile) {
@@ -31,17 +31,17 @@ export default {
return Promise.resolve(file.baseRaw);
}
- return Vue.http
+ return axios
.get(file.rawPath.replace(`/raw/${file.branchId}/${file.path}`, `/raw/${sha}/${file.path}`), {
params: { format: 'json' },
})
- .then(res => res.text());
+ .then(({ data }) => data);
},
getProjectData(namespace, project) {
return Api.project(`${namespace}/${project}`);
},
- getProjectMergeRequestData(projectId, mergeRequestId) {
- return Api.mergeRequest(projectId, mergeRequestId);
+ getProjectMergeRequestData(projectId, mergeRequestId, params = {}) {
+ return Api.mergeRequest(projectId, mergeRequestId, params);
},
getProjectMergeRequestChanges(projectId, mergeRequestId) {
return Api.mergeRequestChanges(projectId, mergeRequestId);
@@ -52,27 +52,15 @@ export default {
getBranchData(projectId, currentBranchId) {
return Api.branchSingle(projectId, currentBranchId);
},
- 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',
- },
- });
- },
getFiles(projectUrl, branchId) {
const url = `${projectUrl}/files/${branchId}`;
- return Vue.http.get(url, {
- params: {
- format: 'json',
- },
- });
+ return axios.get(url, { params: { format: 'json' } });
+ },
+ lastCommitPipelines({ getters }) {
+ const commitSha = getters.lastCommit.id;
+ return Api.commitPipelines(getters.currentProject.path_with_namespace, commitSha);
},
};
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index 1a98b42761e..5e91fa915ff 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -169,6 +169,15 @@ export const burstUnusedSeal = ({ state, commit }) => {
}
};
+export const setRightPane = ({ commit }, view) => {
+ commit(types.SET_RIGHT_PANE, view);
+};
+
+export const setLinks = ({ commit }, links) => commit(types.SET_LINKS, links);
+
+export const setErrorMessage = ({ commit }, errorMessage) =>
+ commit(types.SET_ERROR_MESSAGE, errorMessage);
+
export * from './actions/tree';
export * from './actions/file';
export * from './actions/project';
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index 13aea91d8ba..6c0887e11ee 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -1,5 +1,5 @@
-import { normalizeHeaders } from '~/lib/utils/common_utils';
-import flash from '~/flash';
+import { __ } from '../../../locale';
+import { normalizeHeaders } from '../../../lib/utils/common_utils';
import eventHub from '../../eventhub';
import service from '../../services';
import * as types from '../mutation_types';
@@ -8,7 +8,7 @@ import { setPageTitle } from '../utils';
import { viewerTypes } from '../../constants';
export const closeFile = ({ commit, state, dispatch }, file) => {
- const path = file.path;
+ const { path } = file;
const indexOfClosedFile = state.openFiles.findIndex(f => f.key === file.key);
const fileWasActive = file.active;
@@ -66,13 +66,10 @@ export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive
.getFileData(
`${gon.relative_url_root ? gon.relative_url_root : ''}${file.url.replace('/-/', '/')}`,
)
- .then(res => {
- const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
- setPageTitle(pageTitle);
+ .then(({ data, headers }) => {
+ const normalizedHeaders = normalizeHeaders(headers);
+ setPageTitle(decodeURI(normalizedHeaders['PAGE-TITLE']));
- return res.json();
- })
- .then(data => {
commit(types.SET_FILE_DATA, { data, file });
commit(types.TOGGLE_FILE_OPEN, path);
if (makeFileActive) dispatch('setFileActive', path);
@@ -80,11 +77,17 @@ export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive
})
.catch(() => {
commit(types.TOGGLE_LOADING, { entry: file });
- flash('Error loading file data. Please try again.', 'alert', document, null, false, true);
+ dispatch('setErrorMessage', {
+ text: __('An error occured whilst loading the file.'),
+ action: payload =>
+ dispatch('getFileData', payload).then(() => dispatch('setErrorMessage', null)),
+ actionText: __('Please try again'),
+ actionPayload: { path, makeFileActive },
+ });
});
};
-export const setFileMrChange = ({ state, commit }, { file, mrChange }) => {
+export const setFileMrChange = ({ commit }, { file, mrChange }) => {
commit(types.SET_FILE_MERGE_REQUEST_CHANGE, { file, mrChange });
};
@@ -113,7 +116,13 @@ export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) =
}
})
.catch(() => {
- flash('Error loading file content. Please try again.');
+ dispatch('setErrorMessage', {
+ text: __('An error occured whilst loading the file content.'),
+ action: payload =>
+ dispatch('getRawFileData', payload).then(() => dispatch('setErrorMessage', null)),
+ actionText: __('Please try again'),
+ actionPayload: { path, baseSha },
+ });
reject();
});
});
@@ -156,7 +165,7 @@ export const setEditorPosition = ({ getters, commit }, { editorRow, editorColumn
}
};
-export const setFileViewMode = ({ state, commit }, { file, viewMode }) => {
+export const setFileViewMode = ({ commit }, { file, viewMode }) => {
commit(types.SET_FILE_VIEWMODE, { file, viewMode });
};
diff --git a/app/assets/javascripts/ide/stores/actions/merge_request.js b/app/assets/javascripts/ide/stores/actions/merge_request.js
index da73034fd7d..6bdf9dc3028 100644
--- a/app/assets/javascripts/ide/stores/actions/merge_request.js
+++ b/app/assets/javascripts/ide/stores/actions/merge_request.js
@@ -1,29 +1,34 @@
-import flash from '~/flash';
+import { __ } from '../../../locale';
import service from '../../services';
import * as types from '../mutation_types';
export const getMergeRequestData = (
- { commit, state, dispatch },
+ { commit, dispatch, state },
{ projectId, mergeRequestId, force = false } = {},
) =>
new Promise((resolve, reject) => {
if (!state.projects[projectId].mergeRequests[mergeRequestId] || force) {
service
- .getProjectMergeRequestData(projectId, mergeRequestId)
- .then(res => res.data)
- .then(data => {
+ .getProjectMergeRequestData(projectId, mergeRequestId, { render_html: true })
+ .then(({ data }) => {
commit(types.SET_MERGE_REQUEST, {
projectPath: projectId,
mergeRequestId,
mergeRequest: data,
});
- if (!state.currentMergeRequestId) {
- commit(types.SET_CURRENT_MERGE_REQUEST, mergeRequestId);
- }
+ commit(types.SET_CURRENT_MERGE_REQUEST, mergeRequestId);
resolve(data);
})
.catch(() => {
- flash('Error loading merge request data. Please try again.');
+ dispatch('setErrorMessage', {
+ text: __('An error occured whilst loading the merge request.'),
+ action: payload =>
+ dispatch('getMergeRequestData', payload).then(() =>
+ dispatch('setErrorMessage', null),
+ ),
+ actionText: __('Please try again'),
+ actionPayload: { projectId, mergeRequestId, force },
+ });
reject(new Error(`Merge Request not loaded ${projectId}`));
});
} else {
@@ -32,15 +37,14 @@ export const getMergeRequestData = (
});
export const getMergeRequestChanges = (
- { commit, state, dispatch },
+ { commit, dispatch, state },
{ projectId, mergeRequestId, force = false } = {},
) =>
new Promise((resolve, reject) => {
if (!state.projects[projectId].mergeRequests[mergeRequestId].changes.length || force) {
service
.getProjectMergeRequestChanges(projectId, mergeRequestId)
- .then(res => res.data)
- .then(data => {
+ .then(({ data }) => {
commit(types.SET_MERGE_REQUEST_CHANGES, {
projectPath: projectId,
mergeRequestId,
@@ -49,7 +53,15 @@ export const getMergeRequestChanges = (
resolve(data);
})
.catch(() => {
- flash('Error loading merge request changes. Please try again.');
+ dispatch('setErrorMessage', {
+ text: __('An error occured whilst loading the merge request changes.'),
+ action: payload =>
+ dispatch('getMergeRequestChanges', payload).then(() =>
+ dispatch('setErrorMessage', null),
+ ),
+ actionText: __('Please try again'),
+ actionPayload: { projectId, mergeRequestId, force },
+ });
reject(new Error(`Merge Request Changes not loaded ${projectId}`));
});
} else {
@@ -58,7 +70,7 @@ export const getMergeRequestChanges = (
});
export const getMergeRequestVersions = (
- { commit, state, dispatch },
+ { commit, dispatch, state },
{ projectId, mergeRequestId, force = false } = {},
) =>
new Promise((resolve, reject) => {
@@ -75,7 +87,15 @@ export const getMergeRequestVersions = (
resolve(data);
})
.catch(() => {
- flash('Error loading merge request versions. Please try again.');
+ dispatch('setErrorMessage', {
+ text: __('An error occured whilst loading the merge request version data.'),
+ action: payload =>
+ dispatch('getMergeRequestVersions', payload).then(() =>
+ dispatch('setErrorMessage', null),
+ ),
+ actionText: __('Please try again'),
+ actionPayload: { projectId, mergeRequestId, force },
+ });
reject(new Error(`Merge Request Versions not loaded ${projectId}`));
});
} else {
diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js
index eff9bc03651..501e25d452b 100644
--- a/app/assets/javascripts/ide/stores/actions/project.js
+++ b/app/assets/javascripts/ide/stores/actions/project.js
@@ -1,11 +1,12 @@
+import _ from 'underscore';
import flash from '~/flash';
+import { __, sprintf } from '~/locale';
import service from '../../services';
+import api from '../../../api';
import * as types from '../mutation_types';
+import router from '../../ide_router';
-export const getProjectData = (
- { commit, state, dispatch },
- { namespace, projectId, force = false } = {},
-) =>
+export const getProjectData = ({ commit, state }, { namespace, projectId, force = false } = {}) =>
new Promise((resolve, reject) => {
if (!state.projects[`${namespace}/${projectId}`] || force) {
commit(types.TOGGLE_LOADING, { entry: state });
@@ -15,13 +16,12 @@ export const getProjectData = (
.then(data => {
commit(types.TOGGLE_LOADING, { entry: state });
commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data });
- if (!state.currentProjectId)
- commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`);
+ commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`);
resolve(data);
})
.catch(() => {
flash(
- 'Error loading project data. Please try again.',
+ __('Error loading project data. Please try again.'),
'alert',
document,
null,
@@ -36,7 +36,7 @@ export const getProjectData = (
});
export const getBranchData = (
- { commit, state, dispatch },
+ { commit, dispatch, state },
{ projectId, branchId, force = false } = {},
) =>
new Promise((resolve, reject) => {
@@ -57,15 +57,19 @@ export const getBranchData = (
commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
resolve(data);
})
- .catch(() => {
- flash(
- 'Error loading branch data. Please try again.',
- 'alert',
- document,
- null,
- false,
- true,
- );
+ .catch(e => {
+ if (e.response.status === 404) {
+ dispatch('showBranchNotFoundError', branchId);
+ } else {
+ flash(
+ __('Error loading branch data. Please try again.'),
+ 'alert',
+ document,
+ null,
+ false,
+ true,
+ );
+ }
reject(new Error(`Branch not loaded - ${projectId}/${branchId}`));
});
} else {
@@ -73,25 +77,50 @@ export const getBranchData = (
}
});
-export const refreshLastCommitData = (
- { commit, state, dispatch },
- { projectId, branchId } = {},
-) => service
- .getBranchData(projectId, branchId)
- .then(({ data }) => {
- commit(types.SET_BRANCH_COMMIT, {
- projectId,
- branchId,
- commit: data.commit,
+export const refreshLastCommitData = ({ commit }, { projectId, branchId } = {}) =>
+ service
+ .getBranchData(projectId, branchId)
+ .then(({ data }) => {
+ commit(types.SET_BRANCH_COMMIT, {
+ projectId,
+ branchId,
+ commit: data.commit,
+ });
+ })
+ .catch(() => {
+ flash(__('Error loading last commit.'), 'alert', document, null, false, true);
});
- })
- .catch(() => {
- flash(
- 'Error loading last commit.',
- 'alert',
- document,
- null,
+
+export const createNewBranchFromDefault = ({ state, dispatch, getters }, branch) =>
+ api
+ .createBranch(state.currentProjectId, {
+ ref: getters.currentProject.default_branch,
+ branch,
+ })
+ .then(() => {
+ dispatch('setErrorMessage', null);
+ router.push(`${router.currentRoute.path}?${Date.now()}`);
+ })
+ .catch(() => {
+ dispatch('setErrorMessage', {
+ text: __('An error occured creating the new branch.'),
+ action: payload => dispatch('createNewBranchFromDefault', payload),
+ actionText: __('Please try again'),
+ actionPayload: branch,
+ });
+ });
+
+export const showBranchNotFoundError = ({ dispatch }, branchId) => {
+ dispatch('setErrorMessage', {
+ text: sprintf(
+ __("Branch %{branchName} was not found in this project's repository."),
+ {
+ branchName: `<strong>${_.escape(branchId)}</strong>`,
+ },
false,
- true,
- );
+ ),
+ action: payload => dispatch('createNewBranchFromDefault', payload),
+ actionText: __('Create branch'),
+ actionPayload: branchId,
});
+};
diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js
index 6536be04f0a..ffaaaabff17 100644
--- a/app/assets/javascripts/ide/stores/actions/tree.js
+++ b/app/assets/javascripts/ide/stores/actions/tree.js
@@ -1,14 +1,23 @@
-import { normalizeHeaders } from '~/lib/utils/common_utils';
-import flash from '~/flash';
+import { __ } from '../../../locale';
import service from '../../services';
import * as types from '../mutation_types';
-import { findEntry } from '../utils';
import FilesDecoratorWorker from '../workers/files_decorator_worker';
-export const toggleTreeOpen = ({ commit, dispatch }, path) => {
+export const toggleTreeOpen = ({ commit }, path) => {
commit(types.TOGGLE_TREE_OPEN, path);
};
+export const showTreeEntry = ({ commit, dispatch, state }, path) => {
+ const entry = state.entries[path];
+ const parentPath = entry ? entry.parentPath : '';
+
+ if (parentPath) {
+ commit(types.SET_TREE_OPEN, parentPath);
+
+ dispatch('showTreeEntry', parentPath);
+ }
+};
+
export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
if (row.type === 'tree') {
dispatch('toggleTreeOpen', row.path);
@@ -21,44 +30,23 @@ export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
} else {
dispatch('getFileData', { path: row.path });
}
-};
-export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => {
- if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) 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.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.', 'alert', document, null, false, true));
+ dispatch('showTreeEntry', row.path);
};
export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } = {}) =>
new Promise((resolve, reject) => {
- if (!state.trees[`${projectId}/${branchId}`]) {
+ if (
+ !state.trees[`${projectId}/${branchId}`] ||
+ (state.trees[`${projectId}/${branchId}`].tree &&
+ state.trees[`${projectId}/${branchId}`].tree.length === 0)
+ ) {
const selectedProject = state.projects[projectId];
commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` });
service
.getFiles(selectedProject.web_url, branchId)
- .then(res => res.json())
- .then(data => {
+ .then(({ data }) => {
const worker = new FilesDecoratorWorker();
worker.addEventListener('message', e => {
const { entries, treeList } = e.data;
@@ -86,7 +74,17 @@ export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } =
});
})
.catch(e => {
- flash('Error loading tree data. Please try again.', 'alert', document, null, false, true);
+ if (e.response.status === 404) {
+ dispatch('showBranchNotFoundError', branchId);
+ } else {
+ dispatch('setErrorMessage', {
+ text: __('An error occured whilst loading all the files.'),
+ action: payload =>
+ dispatch('getFiles', payload).then(() => dispatch('setErrorMessage', null)),
+ actionText: __('Please try again'),
+ actionPayload: { projectId, branchId },
+ });
+ }
reject(e);
});
} else {
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index b239a605371..5ce268b0d05 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -82,10 +82,13 @@ export const getStagedFilesCountForPath = state => path =>
getChangesCountForFiles(state.stagedFiles, path);
export const lastCommit = (state, getters) => {
- const branch = getters.currentProject && getters.currentProject.branches[state.currentBranchId];
+ const branch = getters.currentProject && getters.currentBranch;
return branch ? branch.commit : null;
};
+export const currentBranch = (state, getters) =>
+ getters.currentProject && getters.currentProject.branches[state.currentBranchId];
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/ide/stores/index.js b/app/assets/javascripts/ide/stores/index.js
index 699710055e3..f8ce8a67ec0 100644
--- a/app/assets/javascripts/ide/stores/index.js
+++ b/app/assets/javascripts/ide/stores/index.js
@@ -6,16 +6,21 @@ import * as getters from './getters';
import mutations from './mutations';
import commitModule from './modules/commit';
import pipelines from './modules/pipelines';
+import mergeRequests from './modules/merge_requests';
Vue.use(Vuex);
-export default new Vuex.Store({
- state: state(),
- actions,
- mutations,
- getters,
- modules: {
- commit: commitModule,
- pipelines,
- },
-});
+export const createStore = () =>
+ new Vuex.Store({
+ state: state(),
+ actions,
+ mutations,
+ getters,
+ modules: {
+ commit: commitModule,
+ pipelines,
+ mergeRequests,
+ },
+ });
+
+export default createStore();
diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js
index cd25c3060f2..7828c31f20e 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js
@@ -1,7 +1,6 @@
import $ from 'jquery';
import { sprintf, __ } from '~/locale';
import flash from '~/flash';
-import { stripHtml } from '~/lib/utils/text_utility';
import * as rootTypes from '../../mutation_types';
import { createCommitPayload, createNewMergeRequestUrl } from '../../utils';
import router from '../../../ide_router';
@@ -31,9 +30,9 @@ export const setLastCommitMessage = ({ rootState, commit }, data) => {
const currentProject = rootState.projects[rootState.currentProjectId];
const commitStats = data.stats
? sprintf(__('with %{additions} additions, %{deletions} deletions.'), {
- additions: data.stats.additions, // eslint-disable-line indent
- deletions: data.stats.deletions, // eslint-disable-line indent
- }) // eslint-disable-line indent
+ additions: data.stats.additions, // eslint-disable-line indent-legacy
+ deletions: data.stats.deletions, // eslint-disable-line indent-legacy
+ }) // eslint-disable-line indent-legacy
: '';
const commitMsg = sprintf(
__('Your changes have been committed. Commit %{commitId} %{commitStats}'),
@@ -49,35 +48,7 @@ export const setLastCommitMessage = ({ rootState, commit }, data) => {
commit(rootTypes.SET_LAST_COMMIT_MSG, commitMsg, { root: true });
};
-export const checkCommitStatus = ({ rootState }) =>
- service
- .getBranchData(rootState.currentProjectId, rootState.currentBranchId)
- .then(({ data }) => {
- const { id } = data.commit;
- const selectedBranch =
- rootState.projects[rootState.currentProjectId].branches[rootState.currentBranchId];
-
- if (selectedBranch.workingReference !== id) {
- return true;
- }
-
- return false;
- })
- .catch(() =>
- flash(
- __('Error checking branch data. Please try again.'),
- 'alert',
- document,
- null,
- false,
- true,
- ),
- );
-
-export const updateFilesAfterCommit = (
- { commit, dispatch, state, rootState, rootGetters },
- { data },
-) => {
+export const updateFilesAfterCommit = ({ commit, dispatch, rootState }, { data }) => {
const selectedProject = rootState.projects[rootState.currentProjectId];
const lastCommit = {
commit_path: `${selectedProject.web_url}/commit/${data.id}`,
@@ -131,24 +102,24 @@ export const updateFilesAfterCommit = (
export const commitChanges = ({ commit, state, getters, dispatch, rootState, rootGetters }) => {
const newBranch = state.commitAction !== consts.COMMIT_TO_CURRENT_BRANCH;
- const payload = createCommitPayload(getters.branchName, newBranch, state, rootState);
- const getCommitStatus = newBranch ? Promise.resolve(false) : dispatch('checkCommitStatus');
+ const stageFilesPromise = rootState.stagedFiles.length
+ ? Promise.resolve()
+ : dispatch('stageAllChanges', null, { root: true });
commit(types.UPDATE_LOADING, true);
- return getCommitStatus
- .then(
- branchChanged =>
- new Promise(resolve => {
- if (branchChanged) {
- // show the modal with a Bootstrap call
- $('#ide-create-branch-modal').modal('show');
- } else {
- resolve();
- }
- }),
- )
- .then(() => service.commit(rootState.currentProjectId, payload))
+ return stageFilesPromise
+ .then(() => {
+ const payload = createCommitPayload({
+ branch: getters.branchName,
+ newBranch,
+ getters,
+ state,
+ rootState,
+ });
+
+ return service.commit(rootState.currentProjectId, payload);
+ })
.then(({ data }) => {
commit(types.UPDATE_LOADING, false);
@@ -223,12 +194,23 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
);
})
.catch(err => {
- let errMsg = __('Error committing changes. Please try again.');
- if (err.response.data && err.response.data.message) {
- errMsg += ` (${stripHtml(err.response.data.message)})`;
+ if (err.response.status === 400) {
+ $('#ide-create-branch-modal').modal('show');
+ } else {
+ dispatch(
+ 'setErrorMessage',
+ {
+ text: __('An error accured whilst committing your changes.'),
+ action: () =>
+ dispatch('commitChanges').then(() =>
+ dispatch('setErrorMessage', null, { root: true }),
+ ),
+ actionText: __('Please try again'),
+ },
+ { root: true },
+ );
+ window.dispatchEvent(new Event('resize'));
}
- flash(errMsg, 'alert', document, null, false, true);
- window.dispatchEvent(new Event('resize'));
commit(types.UPDATE_LOADING, false);
});
diff --git a/app/assets/javascripts/ide/stores/modules/commit/getters.js b/app/assets/javascripts/ide/stores/modules/commit/getters.js
index d01060201f2..3db4b2f903e 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/getters.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/getters.js
@@ -1,3 +1,4 @@
+import { sprintf, n__ } from '../../../../locale';
import * as consts from './constants';
const BRANCH_SUFFIX_COUNT = 5;
@@ -5,9 +6,6 @@ const BRANCH_SUFFIX_COUNT = 5;
export const discardDraftButtonDisabled = state =>
state.commitMessage === '' || state.submitCommitLoading;
-export const commitButtonDisabled = (state, getters, rootState) =>
- getters.discardDraftButtonDisabled || !rootState.stagedFiles.length;
-
export const newBranchName = (state, _, rootState) =>
`${gon.current_username}-${rootState.currentBranchId}-patch-${`${new Date().getTime()}`.substr(
-BRANCH_SUFFIX_COUNT,
@@ -28,5 +26,18 @@ export const branchName = (state, getters, rootState) => {
return rootState.currentBranchId;
};
+export const preBuiltCommitMessage = (state, _, rootState) => {
+ if (state.commitMessage) return state.commitMessage;
+
+ const files = (rootState.stagedFiles.length
+ ? rootState.stagedFiles
+ : rootState.changedFiles
+ ).reduce((acc, val) => acc.concat(val.path), []);
+
+ return sprintf(n__('Update %{files}', 'Update %{files} files', files.length), {
+ files: files.join(', '),
+ });
+};
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js b/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js
new file mode 100644
index 00000000000..cdd8076952f
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js
@@ -0,0 +1,59 @@
+import { __ } from '../../../../locale';
+import Api from '../../../../api';
+import router from '../../../ide_router';
+import { scopes } from './constants';
+import * as types from './mutation_types';
+import * as rootTypes from '../../mutation_types';
+
+export const requestMergeRequests = ({ commit }, type) =>
+ commit(types.REQUEST_MERGE_REQUESTS, type);
+export const receiveMergeRequestsError = ({ commit, dispatch }, { type, search }) => {
+ dispatch(
+ 'setErrorMessage',
+ {
+ text: __('Error loading merge requests.'),
+ action: payload =>
+ dispatch('fetchMergeRequests', payload).then(() =>
+ dispatch('setErrorMessage', null, { root: true }),
+ ),
+ actionText: __('Please try again'),
+ actionPayload: { type, search },
+ },
+ { root: true },
+ );
+ commit(types.RECEIVE_MERGE_REQUESTS_ERROR, type);
+};
+export const receiveMergeRequestsSuccess = ({ commit }, { type, data }) =>
+ commit(types.RECEIVE_MERGE_REQUESTS_SUCCESS, { type, data });
+
+export const fetchMergeRequests = ({ dispatch, state: { state } }, { type, search = '' }) => {
+ const scope = scopes[type];
+ dispatch('requestMergeRequests', type);
+ dispatch('resetMergeRequests', type);
+
+ Api.mergeRequests({ scope, state, search })
+ .then(({ data }) => dispatch('receiveMergeRequestsSuccess', { type, data }))
+ .catch(() => dispatch('receiveMergeRequestsError', { type, search }));
+};
+
+export const resetMergeRequests = ({ commit }, type) => commit(types.RESET_MERGE_REQUESTS, type);
+
+export const openMergeRequest = ({ commit, dispatch }, { projectPath, id }) => {
+ commit(rootTypes.CLEAR_PROJECTS, null, { root: true });
+ commit(rootTypes.SET_CURRENT_MERGE_REQUEST, `${id}`, { root: true });
+ commit(rootTypes.RESET_OPEN_FILES, null, { root: true });
+ dispatch('setCurrentBranchId', '', { root: true });
+ dispatch('pipelines/stopPipelinePolling', null, { root: true })
+ .then(() => {
+ dispatch('pipelines/resetLatestPipeline', null, { root: true });
+ dispatch('pipelines/clearEtagPoll', null, { root: true });
+ })
+ .catch(e => {
+ throw e;
+ });
+ dispatch('setRightPane', null, { root: true });
+
+ router.push(`/project/${projectPath}/merge_requests/${id}`);
+};
+
+export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/constants.js b/app/assets/javascripts/ide/stores/modules/merge_requests/constants.js
new file mode 100644
index 00000000000..a7085c7d04c
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/merge_requests/constants.js
@@ -0,0 +1,10 @@
+export const scopes = {
+ assigned: 'assigned-to-me',
+ created: 'created-by-me',
+};
+
+export const states = {
+ opened: 'opened',
+ closed: 'closed',
+ merged: 'merged',
+};
diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/getters.js b/app/assets/javascripts/ide/stores/modules/merge_requests/getters.js
new file mode 100644
index 00000000000..8e2b234be8d
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/merge_requests/getters.js
@@ -0,0 +1,4 @@
+export const getData = state => type => state[type];
+
+export const assignedData = state => state.assigned;
+export const createdData = state => state.created;
diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/index.js b/app/assets/javascripts/ide/stores/modules/merge_requests/index.js
new file mode 100644
index 00000000000..2e6dfb420f4
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/merge_requests/index.js
@@ -0,0 +1,12 @@
+import state from './state';
+import * as actions from './actions';
+import * as getters from './getters';
+import mutations from './mutations';
+
+export default {
+ namespaced: true,
+ state: state(),
+ actions,
+ mutations,
+ getters,
+};
diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/mutation_types.js b/app/assets/javascripts/ide/stores/modules/merge_requests/mutation_types.js
new file mode 100644
index 00000000000..0badddcbae7
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/merge_requests/mutation_types.js
@@ -0,0 +1,5 @@
+export const REQUEST_MERGE_REQUESTS = 'REQUEST_MERGE_REQUESTS';
+export const RECEIVE_MERGE_REQUESTS_ERROR = 'RECEIVE_MERGE_REQUESTS_ERROR';
+export const RECEIVE_MERGE_REQUESTS_SUCCESS = 'RECEIVE_MERGE_REQUESTS_SUCCESS';
+
+export const RESET_MERGE_REQUESTS = 'RESET_MERGE_REQUESTS';
diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js b/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js
new file mode 100644
index 00000000000..971da0806bd
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js
@@ -0,0 +1,26 @@
+/* eslint-disable no-param-reassign */
+import * as types from './mutation_types';
+
+export default {
+ [types.REQUEST_MERGE_REQUESTS](state, type) {
+ state[type].isLoading = true;
+ },
+ [types.RECEIVE_MERGE_REQUESTS_ERROR](state, type) {
+ state[type].isLoading = false;
+ },
+ [types.RECEIVE_MERGE_REQUESTS_SUCCESS](state, { type, data }) {
+ state[type].isLoading = false;
+ state[type].mergeRequests = data.map(mergeRequest => ({
+ id: mergeRequest.id,
+ iid: mergeRequest.iid,
+ title: mergeRequest.title,
+ projectId: mergeRequest.project_id,
+ projectPathWithNamespace: mergeRequest.web_url
+ .replace(`${gon.gitlab_url}/`, '')
+ .replace(`/merge_requests/${mergeRequest.iid}`, ''),
+ }));
+ },
+ [types.RESET_MERGE_REQUESTS](state, type) {
+ state[type].mergeRequests = [];
+ },
+};
diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/state.js b/app/assets/javascripts/ide/stores/modules/merge_requests/state.js
new file mode 100644
index 00000000000..57eb6b04283
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/merge_requests/state.js
@@ -0,0 +1,13 @@
+import { states } from './constants';
+
+export default () => ({
+ created: {
+ isLoading: false,
+ mergeRequests: [],
+ },
+ assigned: {
+ isLoading: false,
+ mergeRequests: [],
+ },
+ state: states.opened,
+});
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
index 07f7b201f2e..8cb01f25223 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
@@ -1,49 +1,153 @@
+import Visibility from 'visibilityjs';
+import axios from 'axios';
+import httpStatus from '../../../../lib/utils/http_status';
import { __ } from '../../../../locale';
-import Api from '../../../../api';
-import flash from '../../../../flash';
+import Poll from '../../../../lib/utils/poll';
+import service from '../../../services';
+import { rightSidebarViews } from '../../../constants';
import * as types from './mutation_types';
+let eTagPoll;
+
+export const clearEtagPoll = () => {
+ eTagPoll = null;
+};
+export const stopPipelinePolling = () => {
+ if (eTagPoll) eTagPoll.stop();
+};
+export const restartPipelinePolling = () => {
+ if (eTagPoll) eTagPoll.restart();
+};
+export const forcePipelineRequest = () => {
+ if (eTagPoll) eTagPoll.makeRequest();
+};
+
export const requestLatestPipeline = ({ commit }) => commit(types.REQUEST_LATEST_PIPELINE);
-export const receiveLatestPipelineError = ({ commit }) => {
- flash(__('There was an error loading latest pipeline'));
+export const receiveLatestPipelineError = ({ commit, dispatch }, err) => {
+ if (err.status !== httpStatus.NOT_FOUND) {
+ dispatch(
+ 'setErrorMessage',
+ {
+ text: __('An error occured whilst fetching the latest pipline.'),
+ action: () =>
+ dispatch('forcePipelineRequest').then(() =>
+ dispatch('setErrorMessage', null, { root: true }),
+ ),
+ actionText: __('Please try again'),
+ actionPayload: null,
+ },
+ { root: true },
+ );
+ }
commit(types.RECEIVE_LASTEST_PIPELINE_ERROR);
+ dispatch('stopPipelinePolling');
};
-export const receiveLatestPipelineSuccess = ({ commit }, pipeline) =>
- commit(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, pipeline);
+export const receiveLatestPipelineSuccess = ({ rootGetters, commit }, { pipelines }) => {
+ let lastCommitPipeline = false;
+
+ if (pipelines && pipelines.length) {
+ const lastCommitHash = rootGetters.lastCommit && rootGetters.lastCommit.id;
+ lastCommitPipeline = pipelines.find(pipeline => pipeline.commit.id === lastCommitHash);
+ }
+
+ commit(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, lastCommitPipeline);
+};
+
+export const fetchLatestPipeline = ({ dispatch, rootGetters }) => {
+ if (eTagPoll) return;
-export const fetchLatestPipeline = ({ dispatch, rootState }, sha) => {
dispatch('requestLatestPipeline');
- return Api.pipelines(rootState.currentProjectId, { sha, per_page: '1' })
- .then(({ data }) => {
- dispatch('receiveLatestPipelineSuccess', data.pop());
- })
- .catch(() => dispatch('receiveLatestPipelineError'));
+ eTagPoll = new Poll({
+ resource: service,
+ method: 'lastCommitPipelines',
+ data: { getters: rootGetters },
+ successCallback: ({ data }) => dispatch('receiveLatestPipelineSuccess', data),
+ errorCallback: err => dispatch('receiveLatestPipelineError', err),
+ });
+
+ if (!Visibility.hidden()) {
+ eTagPoll.makeRequest();
+ }
+
+ Visibility.change(() => {
+ if (!Visibility.hidden()) {
+ dispatch('restartPipelinePolling');
+ } else {
+ dispatch('stopPipelinePolling');
+ }
+ });
+};
+
+export const requestJobs = ({ commit }, id) => commit(types.REQUEST_JOBS, id);
+export const receiveJobsError = ({ commit, dispatch }, stage) => {
+ dispatch(
+ 'setErrorMessage',
+ {
+ text: __('An error occured whilst loading the pipelines jobs.'),
+ action: payload =>
+ dispatch('fetchJobs', payload).then(() =>
+ dispatch('setErrorMessage', null, { root: true }),
+ ),
+ actionText: __('Please try again'),
+ actionPayload: stage,
+ },
+ { root: true },
+ );
+ commit(types.RECEIVE_JOBS_ERROR, stage.id);
+};
+export const receiveJobsSuccess = ({ commit }, { id, data }) =>
+ commit(types.RECEIVE_JOBS_SUCCESS, { id, data });
+
+export const fetchJobs = ({ dispatch }, stage) => {
+ dispatch('requestJobs', stage.id);
+
+ axios
+ .get(stage.dropdownPath)
+ .then(({ data }) => dispatch('receiveJobsSuccess', { id: stage.id, data }))
+ .catch(() => dispatch('receiveJobsError', stage));
};
-export const requestJobs = ({ commit }) => commit(types.REQUEST_JOBS);
-export const receiveJobsError = ({ commit }) => {
- flash(__('There was an error loading jobs'));
- commit(types.RECEIVE_JOBS_ERROR);
+export const toggleStageCollapsed = ({ commit }, stageId) =>
+ commit(types.TOGGLE_STAGE_COLLAPSE, stageId);
+
+export const setDetailJob = ({ commit, dispatch }, job) => {
+ commit(types.SET_DETAIL_JOB, job);
+ dispatch('setRightPane', job ? rightSidebarViews.jobsDetail : rightSidebarViews.pipelines, {
+ root: true,
+ });
};
-export const receiveJobsSuccess = ({ commit }, data) => commit(types.RECEIVE_JOBS_SUCCESS, data);
-export const fetchJobs = ({ dispatch, state, rootState }, page = '1') => {
- dispatch('requestJobs');
+export const requestJobTrace = ({ commit }) => commit(types.REQUEST_JOB_TRACE);
+export const receiveJobTraceError = ({ commit, dispatch }) => {
+ dispatch(
+ 'setErrorMessage',
+ {
+ text: __('An error occured whilst fetching the job trace.'),
+ action: () =>
+ dispatch('fetchJobTrace').then(() => dispatch('setErrorMessage', null, { root: true })),
+ actionText: __('Please try again'),
+ actionPayload: null,
+ },
+ { root: true },
+ );
+ commit(types.RECEIVE_JOB_TRACE_ERROR);
+};
+export const receiveJobTraceSuccess = ({ commit }, data) =>
+ commit(types.RECEIVE_JOB_TRACE_SUCCESS, data);
- Api.pipelineJobs(rootState.currentProjectId, state.latestPipeline.id, {
- page,
- })
- .then(({ data, headers }) => {
- const nextPage = headers && headers['x-next-page'];
+export const fetchJobTrace = ({ dispatch, state }) => {
+ dispatch('requestJobTrace');
- dispatch('receiveJobsSuccess', data);
+ return axios
+ .get(`${state.detailJob.path}/trace`, { params: { format: 'json' } })
+ .then(({ data }) => dispatch('receiveJobTraceSuccess', data))
+ .catch(() => dispatch('receiveJobTraceError'));
+};
- if (nextPage) {
- dispatch('fetchJobs', nextPage);
- }
- })
- .catch(() => dispatch('receiveJobsError'));
+export const resetLatestPipeline = ({ commit }) => {
+ commit(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, null);
+ commit(types.SET_DETAIL_JOB, null);
};
export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/constants.js b/app/assets/javascripts/ide/stores/modules/pipelines/constants.js
new file mode 100644
index 00000000000..f5b96327e40
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/constants.js
@@ -0,0 +1,4 @@
+// eslint-disable-next-line import/prefer-default-export
+export const states = {
+ failed: 'failed',
+};
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/getters.js b/app/assets/javascripts/ide/stores/modules/pipelines/getters.js
index d6c91f5b64d..f545453806f 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/getters.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/getters.js
@@ -1,7 +1,22 @@
+import { states } from './constants';
+
export const hasLatestPipeline = state => !state.isLoadingPipeline && !!state.latestPipeline;
-export const failedJobs = state =>
+export const pipelineFailed = state =>
+ state.latestPipeline && state.latestPipeline.details.status.text === states.failed;
+
+export const failedStages = state =>
+ state.stages.filter(stage => stage.status.text.toLowerCase() === states.failed).map(stage => ({
+ ...stage,
+ jobs: stage.jobs.filter(job => job.status.text.toLowerCase() === states.failed),
+ }));
+
+export const failedJobsCount = state =>
state.stages.reduce(
- (acc, stage) => acc.concat(stage.jobs.filter(job => job.status === 'failed')),
- [],
+ (acc, stage) => acc + stage.jobs.filter(j => j.status.text === states.failed).length,
+ 0,
);
+
+export const jobsCount = state => state.stages.reduce((acc, stage) => acc + stage.jobs.length, 0);
+
+export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js b/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js
index 6b5701670a6..f4c36b9d96f 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js
@@ -5,3 +5,11 @@ export const RECEIVE_LASTEST_PIPELINE_SUCCESS = 'RECEIVE_LASTEST_PIPELINE_SUCCES
export const REQUEST_JOBS = 'REQUEST_JOBS';
export const RECEIVE_JOBS_ERROR = 'RECEIVE_JOBS_ERROR';
export const RECEIVE_JOBS_SUCCESS = 'RECEIVE_JOBS_SUCCESS';
+
+export const TOGGLE_STAGE_COLLAPSE = 'TOGGLE_STAGE_COLLAPSE';
+
+export const SET_DETAIL_JOB = 'SET_DETAIL_JOB';
+
+export const REQUEST_JOB_TRACE = 'REQUEST_JOB_TRACE';
+export const RECEIVE_JOB_TRACE_ERROR = 'RECEIVE_JOB_TRACE_ERROR';
+export const RECEIVE_JOB_TRACE_SUCCESS = 'RECEIVE_JOB_TRACE_SUCCESS';
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js
index 2b16e57b386..5a2213bbe89 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js
@@ -1,5 +1,6 @@
/* eslint-disable no-param-reassign */
import * as types from './mutation_types';
+import { normalizeJob } from './utils';
export default {
[types.REQUEST_LATEST_PIPELINE](state) {
@@ -14,40 +15,65 @@ export default {
if (pipeline) {
state.latestPipeline = {
id: pipeline.id,
- status: pipeline.status,
+ path: pipeline.path,
+ commit: pipeline.commit,
+ details: {
+ status: pipeline.details.status,
+ },
+ yamlError: pipeline.yaml_errors,
};
+ state.stages = pipeline.details.stages.map((stage, i) => {
+ const foundStage = state.stages.find(s => s.id === i);
+ return {
+ id: i,
+ dropdownPath: stage.dropdown_path,
+ name: stage.name,
+ status: stage.status,
+ isCollapsed: foundStage ? foundStage.isCollapsed : false,
+ isLoading: foundStage ? foundStage.isLoading : false,
+ jobs: foundStage ? foundStage.jobs : [],
+ };
+ });
+ } else {
+ state.latestPipeline = false;
}
},
- [types.REQUEST_JOBS](state) {
- state.isLoadingJobs = true;
+ [types.REQUEST_JOBS](state, id) {
+ state.stages = state.stages.map(stage => ({
+ ...stage,
+ isLoading: stage.id === id ? true : stage.isLoading,
+ }));
},
- [types.RECEIVE_JOBS_ERROR](state) {
- state.isLoadingJobs = false;
+ [types.RECEIVE_JOBS_ERROR](state, id) {
+ state.stages = state.stages.map(stage => ({
+ ...stage,
+ isLoading: stage.id === id ? false : stage.isLoading,
+ }));
},
- [types.RECEIVE_JOBS_SUCCESS](state, jobs) {
- state.isLoadingJobs = false;
-
- state.stages = jobs.reduce((acc, job) => {
- let stage = acc.find(s => s.title === job.stage);
-
- if (!stage) {
- stage = {
- title: job.stage,
- jobs: [],
- };
-
- acc.push(stage);
- }
-
- stage.jobs = stage.jobs.concat({
- id: job.id,
- name: job.name,
- status: job.status,
- stage: job.stage,
- duration: job.duration,
- });
-
- return acc;
- }, state.stages);
+ [types.RECEIVE_JOBS_SUCCESS](state, { id, data }) {
+ state.stages = state.stages.map(stage => ({
+ ...stage,
+ isLoading: stage.id === id ? false : stage.isLoading,
+ jobs: stage.id === id ? data.latest_statuses.map(normalizeJob) : stage.jobs,
+ }));
+ },
+ [types.TOGGLE_STAGE_COLLAPSE](state, id) {
+ state.stages = state.stages.map(stage => ({
+ ...stage,
+ isCollapsed: stage.id === id ? !stage.isCollapsed : stage.isCollapsed,
+ }));
+ },
+ [types.SET_DETAIL_JOB](state, job) {
+ state.detailJob = { ...job };
+ },
+ [types.REQUEST_JOB_TRACE](state) {
+ state.detailJob.isLoading = true;
+ },
+ [types.RECEIVE_JOB_TRACE_ERROR](state) {
+ state.detailJob.isLoading = false;
+ },
+ [types.RECEIVE_JOB_TRACE_SUCCESS](state, data) {
+ state.detailJob.isLoading = false;
+ state.detailJob.output = data.html;
},
};
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/state.js b/app/assets/javascripts/ide/stores/modules/pipelines/state.js
index 6f22542aaea..8651e267b53 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/state.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/state.js
@@ -1,6 +1,7 @@
export default () => ({
- isLoadingPipeline: false,
+ isLoadingPipeline: true,
isLoadingJobs: false,
latestPipeline: null,
stages: [],
+ detailJob: null,
});
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/utils.js b/app/assets/javascripts/ide/stores/modules/pipelines/utils.js
new file mode 100644
index 00000000000..a6caca2d2dc
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/utils.js
@@ -0,0 +1,11 @@
+// eslint-disable-next-line import/prefer-default-export
+export const normalizeJob = job => ({
+ id: job.id,
+ name: job.name,
+ status: job.status,
+ path: job.build_path,
+ rawPath: `${job.build_path}/raw`,
+ started: job.started,
+ output: '',
+ isLoading: false,
+});
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index a3fb3232f1d..555802e1811 100644
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -6,6 +6,7 @@ export const SET_LEFT_PANEL_COLLAPSED = 'SET_LEFT_PANEL_COLLAPSED';
export const SET_RIGHT_PANEL_COLLAPSED = 'SET_RIGHT_PANEL_COLLAPSED';
export const SET_RESIZING_STATUS = 'SET_RESIZING_STATUS';
export const SET_EMPTY_STATE_SVGS = 'SET_EMPTY_STATE_SVGS';
+export const SET_LINKS = 'SET_LINKS';
// Project Mutation Types
export const SET_PROJECT = 'SET_PROJECT';
@@ -27,6 +28,7 @@ export const TOGGLE_BRANCH_OPEN = 'TOGGLE_BRANCH_OPEN';
// Tree mutation types
export const SET_DIRECTORY_DATA = 'SET_DIRECTORY_DATA';
export const TOGGLE_TREE_OPEN = 'TOGGLE_TREE_OPEN';
+export const SET_TREE_OPEN = 'SET_TREE_OPEN';
export const SET_LAST_COMMIT_URL = 'SET_LAST_COMMIT_URL';
export const CREATE_TREE = 'CREATE_TREE';
export const REMOVE_ALL_CHANGES_FILES = 'REMOVE_ALL_CHANGES_FILES';
@@ -65,3 +67,10 @@ export const UPDATE_ACTIVITY_BAR_VIEW = 'UPDATE_ACTIVITY_BAR_VIEW';
export const UPDATE_TEMP_FLAG = 'UPDATE_TEMP_FLAG';
export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER';
export const BURST_UNUSED_SEAL = 'BURST_UNUSED_SEAL';
+
+export const SET_RIGHT_PANE = 'SET_RIGHT_PANE';
+
+export const CLEAR_PROJECTS = 'CLEAR_PROJECTS';
+export const RESET_OPEN_FILES = 'RESET_OPEN_FILES';
+
+export const SET_ERROR_MESSAGE = 'SET_ERROR_MESSAGE';
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index a257e2ef025..702be2140e2 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -114,12 +114,13 @@ export default {
},
[types.SET_EMPTY_STATE_SVGS](
state,
- { emptyStateSvgPath, noChangesStateSvgPath, committedStateSvgPath },
+ { emptyStateSvgPath, noChangesStateSvgPath, committedStateSvgPath, pipelinesEmptyStateSvgPath },
) {
Object.assign(state, {
emptyStateSvgPath,
noChangesStateSvgPath,
committedStateSvgPath,
+ pipelinesEmptyStateSvgPath,
});
},
[types.TOGGLE_FILE_FINDER](state, fileFindVisible) {
@@ -148,6 +149,23 @@ export default {
unusedSeal: false,
});
},
+ [types.SET_RIGHT_PANE](state, view) {
+ Object.assign(state, {
+ rightPane: state.rightPane === view ? null : view,
+ });
+ },
+ [types.SET_LINKS](state, links) {
+ Object.assign(state, { links });
+ },
+ [types.CLEAR_PROJECTS](state) {
+ Object.assign(state, { projects: {}, trees: {} });
+ },
+ [types.RESET_OPEN_FILES](state) {
+ Object.assign(state, { openFiles: [] });
+ },
+ [types.SET_ERROR_MESSAGE](state, errorMessage) {
+ Object.assign(state, { errorMessage });
+ },
...projectMutations,
...mergeRequestMutation,
...fileMutations,
diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js
index 13f123b6630..46547820425 100644
--- a/app/assets/javascripts/ide/stores/mutations/file.js
+++ b/app/assets/javascripts/ide/stores/mutations/file.js
@@ -1,5 +1,6 @@
/* eslint-disable no-param-reassign */
import * as types from '../mutation_types';
+import { diffModes } from '../../constants';
export default {
[types.SET_FILE_ACTIVE](state, { path, active }) {
@@ -46,6 +47,7 @@ export default {
baseRaw: null,
html: data.html,
size: data.size,
+ lastCommitSha: data.last_commit_sha,
});
},
[types.SET_FILE_RAW_DATA](state, { file, raw }) {
@@ -85,8 +87,19 @@ export default {
});
},
[types.SET_FILE_MERGE_REQUEST_CHANGE](state, { file, mrChange }) {
+ let diffMode = diffModes.replaced;
+ if (mrChange.new_file) {
+ diffMode = diffModes.new;
+ } else if (mrChange.deleted_file) {
+ diffMode = diffModes.deleted;
+ } else if (mrChange.renamed_file) {
+ diffMode = diffModes.renamed;
+ }
Object.assign(state.entries[file.path], {
- mrChange,
+ mrChange: {
+ ...mrChange,
+ diffMode,
+ },
});
},
[types.SET_FILE_VIEWMODE](state, { file, viewMode }) {
diff --git a/app/assets/javascripts/ide/stores/mutations/tree.js b/app/assets/javascripts/ide/stores/mutations/tree.js
index 1176c040fb9..2cf34af9274 100644
--- a/app/assets/javascripts/ide/stores/mutations/tree.js
+++ b/app/assets/javascripts/ide/stores/mutations/tree.js
@@ -6,6 +6,11 @@ export default {
opened: !state.entries[path].opened,
});
},
+ [types.SET_TREE_OPEN](state, path) {
+ Object.assign(state.entries[path], {
+ opened: true,
+ });
+ },
[types.CREATE_TREE](state, { treePath }) {
Object.assign(state, {
trees: Object.assign({}, state.trees, {
diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js
index e7411f16a4f..be229b2c723 100644
--- a/app/assets/javascripts/ide/stores/state.js
+++ b/app/assets/javascripts/ide/stores/state.js
@@ -23,4 +23,7 @@ export default () => ({
currentActivityView: activityBarViews.edit,
unusedSeal: true,
fileFindVisible: false,
+ rightPane: null,
+ links: {},
+ errorMessage: null,
});
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index e0b9766fbee..9e6b86dd844 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -17,6 +17,7 @@ export const dataStructure = () => ({
changed: false,
staged: false,
lastCommitPath: '',
+ lastCommitSha: '',
lastCommit: {
id: '',
url: '',
@@ -39,7 +40,7 @@ export const dataStructure = () => ({
editorColumn: 1,
fileLanguage: '',
eol: '',
- viewMode: 'edit',
+ viewMode: 'editor',
previewMode: null,
size: 0,
parentPath: null,
@@ -104,14 +105,15 @@ export const setPageTitle = title => {
document.title = title;
};
-export const createCommitPayload = (branch, newBranch, state, rootState) => ({
+export const createCommitPayload = ({ branch, getters, newBranch, state, rootState }) => ({
branch,
- commit_message: state.commitMessage,
+ commit_message: state.commitMessage || getters.preBuiltCommitMessage,
actions: rootState.stagedFiles.map(f => ({
action: f.tempFile ? 'create' : 'update',
file_path: f.path,
content: f.content,
encoding: f.base64 ? 'base64' : 'text',
+ last_commit_id: newBranch ? undefined : f.lastCommitSha,
})),
start_branch: newBranch ? rootState.currentBranchId : undefined,
});
diff --git a/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js b/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js
index 0a1c253c637..fa35c215880 100644
--- a/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js
+++ b/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js
@@ -1,6 +1,7 @@
import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils';
import { decorateData, sortTree } from '../utils';
+// eslint-disable-next-line no-restricted-globals
self.addEventListener('message', e => {
const { data, projectId, branchId, tempFile = false, content = '', base64 = false } = e.data;
@@ -89,6 +90,7 @@ self.addEventListener('message', e => {
return acc;
}, {});
+ // eslint-disable-next-line no-restricted-globals
self.postMessage({
entries,
treeList: sortTree(treeList),
diff --git a/app/assets/javascripts/image_diff/helpers/dom_helper.js b/app/assets/javascripts/image_diff/helpers/dom_helper.js
index 12d56714b34..a319bcccb8f 100644
--- a/app/assets/javascripts/image_diff/helpers/dom_helper.js
+++ b/app/assets/javascripts/image_diff/helpers/dom_helper.js
@@ -2,7 +2,8 @@ export function setPositionDataAttribute(el, options) {
// Update position data attribute so that the
// new comment form can use this data for ajax request
const { x, y, width, height } = options;
- const position = el.dataset.position;
+ const { position } = el.dataset;
+
const positionObject = Object.assign({}, JSON.parse(position), {
x,
y,
diff --git a/app/assets/javascripts/image_diff/helpers/utils_helper.js b/app/assets/javascripts/image_diff/helpers/utils_helper.js
index 28d9a969143..beec99e6934 100644
--- a/app/assets/javascripts/image_diff/helpers/utils_helper.js
+++ b/app/assets/javascripts/image_diff/helpers/utils_helper.js
@@ -40,8 +40,7 @@ export function getTargetSelection(event) {
const x = event.offsetX;
const y = event.offsetY;
- const width = imageEl.width;
- const height = imageEl.height;
+ const { width, height } = imageEl;
const actualWidth = imageEl.naturalWidth;
const actualHeight = imageEl.naturalHeight;
diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js
index b469e1e2adc..f9ff0722c01 100644
--- a/app/assets/javascripts/importer_status.js
+++ b/app/assets/javascripts/importer_status.js
@@ -58,7 +58,7 @@ class ImporterStatus {
job.find('.import-target').html(`<a href="${data.full_path}">${data.full_path}</a>`);
$('table.import-jobs tbody').prepend(job);
- job.addClass('active');
+ job.addClass('table-active');
const connectingVerb = this.ciCdOnly ? __('connecting') : __('importing');
job.find('.import-actions').html(sprintf(
_.escape(__('%{loadingIcon} Started')), {
@@ -67,7 +67,15 @@ class ImporterStatus {
false,
));
})
- .catch(() => flash(__('An error occurred while importing project')));
+ .catch((error) => {
+ let details = error;
+
+ if (error.response && error.response.data && error.response.data.errors) {
+ details = error.response.data.errors;
+ }
+
+ flash(__(`An error occurred while importing project: ${details}`));
+ });
}
autoUpdate() {
@@ -81,7 +89,7 @@ class ImporterStatus {
switch (job.import_status) {
case 'finished':
- jobItem.removeClass('active').addClass('success');
+ jobItem.removeClass('table-active').addClass('table-success');
statusField.html(`<span><i class="fa fa-check"></i> ${__('Done')}</span>`);
break;
case 'scheduled':
diff --git a/app/assets/javascripts/init_changes_dropdown.js b/app/assets/javascripts/init_changes_dropdown.js
index 09cca1dc7d9..5c5a6e01848 100644
--- a/app/assets/javascripts/init_changes_dropdown.js
+++ b/app/assets/javascripts/init_changes_dropdown.js
@@ -1,5 +1,5 @@
import $ from 'jquery';
-import stickyMonitor from './lib/utils/sticky';
+import { stickyMonitor } from './lib/utils/sticky';
export default (stickyTop) => {
stickyMonitor(document.querySelector('.js-diff-files-changed'), stickyTop);
diff --git a/app/assets/javascripts/init_notes.js b/app/assets/javascripts/init_notes.js
index 882aedfcc76..3c71258e53b 100644
--- a/app/assets/javascripts/init_notes.js
+++ b/app/assets/javascripts/init_notes.js
@@ -7,10 +7,10 @@ export default () => {
notesIds,
now,
diffView,
- autocomplete,
+ enableGFM,
} = JSON.parse(dataEl.innerHTML);
// Create a singleton so that we don't need to assign
// into the window object, we can just access the current isntance with Notes.instance
- Notes.initialize(notesUrl, notesIds, now, diffView, autocomplete);
+ Notes.initialize(notesUrl, notesIds, now, diffView, enableGFM);
};
diff --git a/app/assets/javascripts/integrations/integration_settings_form.js b/app/assets/javascripts/integrations/integration_settings_form.js
index 741894b5e6c..bd90d0eaa32 100644
--- a/app/assets/javascripts/integrations/integration_settings_form.js
+++ b/app/assets/javascripts/integrations/integration_settings_form.js
@@ -91,7 +91,6 @@ export default class IntegrationSettingsForm {
}
}
- /* eslint-disable promise/catch-or-return, no-new */
/**
* Test Integration config
*/
@@ -101,13 +100,19 @@ export default class IntegrationSettingsForm {
return axios.put(this.testEndPoint, formData)
.then(({ data }) => {
if (data.error) {
- flash(`${data.message} ${data.service_response}`, 'alert', document, {
- title: 'Save anyway',
- clickHandler: (e) => {
- e.preventDefault();
- this.$form.submit();
- },
- });
+ let flashActions;
+
+ if (data.test_failed) {
+ flashActions = {
+ title: 'Save anyway',
+ clickHandler: (e) => {
+ e.preventDefault();
+ this.$form.submit();
+ },
+ };
+ }
+
+ flash(`${data.message} ${data.service_response}`, 'alert', document, flashActions);
} else {
this.$form.submit();
}
diff --git a/app/assets/javascripts/issuable/auto_width_dropdown_select.js b/app/assets/javascripts/issuable/auto_width_dropdown_select.js
index b2c2de9e5de..07cf1eff279 100644
--- a/app/assets/javascripts/issuable/auto_width_dropdown_select.js
+++ b/app/assets/javascripts/issuable/auto_width_dropdown_select.js
@@ -10,7 +10,7 @@ class AutoWidthDropdownSelect {
}
init() {
- const dropdownClass = this.dropdownClass;
+ const { dropdownClass } = this;
this.$selectElement.select2({
dropdownCssClass: dropdownClass,
...AutoWidthDropdownSelect.selectOptions(this.dropdownClass),
diff --git a/app/assets/javascripts/issuable_bulk_update_actions.js b/app/assets/javascripts/issuable_bulk_update_actions.js
index e003fb1d127..35eaf21a836 100644
--- a/app/assets/javascripts/issuable_bulk_update_actions.js
+++ b/app/assets/javascripts/issuable_bulk_update_actions.js
@@ -1,4 +1,4 @@
-/* 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 */
+/* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, prefer-arrow-callback, max-len, no-unused-vars */
import $ from 'jquery';
import _ from 'underscore';
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index 90d4e19e90b..0140960b367 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -1,4 +1,4 @@
-/* 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 */
+/* eslint-disable no-new, no-unused-vars, consistent-return, no-else-return */
/* global GitLab */
import $ from 'jquery';
@@ -30,7 +30,7 @@ export default class IssuableForm {
}
this.initAutosave();
- this.form.on('submit:success', this.handleSubmit);
+ this.form.on('submit', this.handleSubmit);
this.form.on('click', '.btn-cancel', this.resetAutosave);
this.initWip();
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index 5113ac6775d..8c225cd7d91 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -1,4 +1,4 @@
-/* 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 */
+/* eslint-disable no-var, one-var, one-var-declaration-per-line, no-unused-vars, consistent-return, quotes, max-len */
import $ from 'jquery';
import axios from './lib/utils/axios_utils';
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index e87a8ed7fea..ad928484952 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -108,6 +108,11 @@
type: String,
required: true,
},
+ markdownVersion: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
projectPath: {
type: String,
required: true,
@@ -226,7 +231,7 @@
.then(res => res.data)
.then(data => this.checkForSpam(data))
.then((data) => {
- if (location.pathname !== data.web_url) {
+ if (window.location.pathname !== data.web_url) {
visitUrl(data.web_url);
}
@@ -282,6 +287,7 @@
:issuable-templates="issuableTemplates"
:markdown-docs-path="markdownDocsPath"
:markdown-preview-path="markdownPreviewPath"
+ :markdown-version="markdownVersion"
:project-path="projectPath"
:project-namespace="projectNamespace"
:show-delete-button="showDeleteButton"
diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue
index ae577e04a56..1174177f561 100644
--- a/app/assets/javascripts/issue_show/components/description.vue
+++ b/app/assets/javascripts/issue_show/components/description.vue
@@ -110,25 +110,25 @@
<template>
<div
v-if="descriptionHtml"
- class="description"
:class="{
'js-task-list-container': canUpdate
}"
+ class="description"
>
<div
- class="wiki"
+ ref="gfm-content"
:class="{
'issue-realtime-pre-pulse': preAnimation,
'issue-realtime-trigger-pulse': pulseAnimation
}"
- v-html="descriptionHtml"
- ref="gfm-content">
+ class="wiki"
+ v-html="descriptionHtml">
</div>
<textarea
- class="hidden js-task-list-field"
v-if="descriptionText"
v-model="descriptionText"
:data-update-url="updateUrl"
+ class="hidden js-task-list-field"
>
</textarea>
diff --git a/app/assets/javascripts/issue_show/components/edit_actions.vue b/app/assets/javascripts/issue_show/components/edit_actions.vue
index a539506bce2..597c6d69a81 100644
--- a/app/assets/javascripts/issue_show/components/edit_actions.vue
+++ b/app/assets/javascripts/issue_show/components/edit_actions.vue
@@ -38,7 +38,7 @@
},
deleteIssuable() {
// eslint-disable-next-line no-alert
- if (confirm('Issue will be removed! Are you sure?')) {
+ if (window.confirm('Issue will be removed! Are you sure?')) {
this.deleteLoading = true;
eventHub.$emit('delete.issuable');
@@ -51,36 +51,36 @@
<template>
<div class="prepend-top-default append-bottom-default clearfix">
<button
- class="btn btn-save pull-left"
:class="{ disabled: formState.updateLoading || !isSubmitEnabled }"
- type="submit"
:disabled="formState.updateLoading || !isSubmitEnabled"
+ class="btn btn-save float-left"
+ type="submit"
@click.prevent="updateIssuable">
Save changes
<i
+ v-if="formState.updateLoading"
class="fa fa-spinner fa-spin"
- aria-hidden="true"
- v-if="formState.updateLoading">
+ aria-hidden="true">
</i>
</button>
<button
- class="btn btn-default pull-right"
+ class="btn btn-default float-right"
type="button"
@click="closeForm">
Cancel
</button>
<button
v-if="shouldShowDeleteButton"
- class="btn btn-danger pull-right append-right-default"
:class="{ disabled: deleteLoading }"
- type="button"
:disabled="deleteLoading"
+ class="btn btn-danger float-right append-right-default"
+ type="button"
@click="deleteIssuable">
Delete
<i
+ v-if="deleteLoading"
class="fa fa-spinner fa-spin"
- aria-hidden="true"
- v-if="deleteLoading">
+ aria-hidden="true">
</i>
</button>
</div>
diff --git a/app/assets/javascripts/issue_show/components/edited.vue b/app/assets/javascripts/issue_show/components/edited.vue
index 01097b5b35e..5ff5b1630b1 100644
--- a/app/assets/javascripts/issue_show/components/edited.vue
+++ b/app/assets/javascripts/issue_show/components/edited.vue
@@ -37,16 +37,16 @@
Edited
<time-ago-tooltip
v-if="updatedAt"
- tooltip-placement="bottom"
:time="updatedAt"
+ tooltip-placement="bottom"
/>
<span
v-if="hasUpdatedBy"
>
by
<a
- class="author_link"
:href="updatedByPath"
+ class="author_link"
>
<span>{{ updatedByName }}</span>
</a>
diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue
index d9fa2764d65..97acc5ba385 100644
--- a/app/assets/javascripts/issue_show/components/fields/description.vue
+++ b/app/assets/javascripts/issue_show/components/fields/description.vue
@@ -20,6 +20,11 @@
type: String,
required: true,
},
+ markdownVersion: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
canAttachFile: {
type: Boolean,
required: false,
@@ -47,18 +52,19 @@
<markdown-field
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
+ :markdown-version="markdownVersion"
:can-attach-file="canAttachFile"
:enable-autocomplete="enableAutocomplete"
>
<textarea
id="issue-description"
+ ref="textarea"
+ slot="textarea"
+ v-model="formState.description"
class="note-textarea js-gfm-input js-autosize markdown-area"
data-supports-quick-actions="false"
aria-label="Description"
- v-model="formState.description"
- ref="textarea"
- slot="textarea"
- placeholder="Write a comment or drag your files here..."
+ placeholder="Write a comment or drag your files here…"
@keydown.meta.enter="updateIssuable"
@keydown.ctrl.enter="updateIssuable">
</textarea>
diff --git a/app/assets/javascripts/issue_show/components/fields/description_template.vue b/app/assets/javascripts/issue_show/components/fields/description_template.vue
index 7db0488e306..e90d9fad94e 100644
--- a/app/assets/javascripts/issue_show/components/fields/description_template.vue
+++ b/app/assets/javascripts/issue_show/components/fields/description_template.vue
@@ -48,15 +48,15 @@
class="dropdown js-issuable-selector-wrap"
data-issuable-type="issue">
<button
+ ref="toggle"
+ :data-namespace-path="projectNamespace"
+ :data-project-path="projectPath"
+ :data-data="issuableTemplatesJson"
class="dropdown-menu-toggle js-issuable-selector"
type="button"
- ref="toggle"
data-field-name="issuable_template"
data-selected="null"
- data-toggle="dropdown"
- :data-namespace-path="projectNamespace"
- :data-project-path="projectPath"
- :data-data="issuableTemplatesJson">
+ data-toggle="dropdown">
<span class="dropdown-toggle-text">
Choose a template
</span>
diff --git a/app/assets/javascripts/issue_show/components/fields/title.vue b/app/assets/javascripts/issue_show/components/fields/title.vue
index c3abb9fd9d5..7d1526a64b4 100644
--- a/app/assets/javascripts/issue_show/components/fields/title.vue
+++ b/app/assets/javascripts/issue_show/components/fields/title.vue
@@ -21,11 +21,11 @@
</label>
<input
id="issuable-title"
+ v-model="formState.title"
class="form-control"
type="text"
placeholder="Title"
aria-label="Title"
- v-model="formState.title"
@keydown.meta.enter="updateIssuable"
@keydown.ctrl.enter="updateIssuable" />
</fieldset>
diff --git a/app/assets/javascripts/issue_show/components/form.vue b/app/assets/javascripts/issue_show/components/form.vue
index 779705e19ac..e509bb52f7d 100644
--- a/app/assets/javascripts/issue_show/components/form.vue
+++ b/app/assets/javascripts/issue_show/components/form.vue
@@ -35,6 +35,11 @@
type: String,
required: true,
},
+ markdownVersion: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
projectPath: {
type: String,
required: true,
@@ -72,8 +77,8 @@
<locked-warning v-if="formState.lockedWarningVisible" />
<div class="row">
<div
- class="col-sm-4 col-lg-3"
- v-if="hasIssuableTemplates">
+ v-if="hasIssuableTemplates"
+ class="col-sm-4 col-lg-3">
<description-template
:form-state="formState"
:issuable-templates="issuableTemplates"
@@ -84,7 +89,7 @@
<div
:class="{
'col-sm-8 col-lg-9': hasIssuableTemplates,
- 'col-xs-12': !hasIssuableTemplates,
+ 'col-12': !hasIssuableTemplates,
}"
>
<title-field
@@ -97,6 +102,7 @@
:form-state="formState"
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
+ :markdown-version="markdownVersion"
:can-attach-file="canAttachFile"
:enable-autocomplete="enableAutocomplete"
/>
diff --git a/app/assets/javascripts/issue_show/components/locked_warning.vue b/app/assets/javascripts/issue_show/components/locked_warning.vue
index 1c2789f154a..ad0d40faf32 100644
--- a/app/assets/javascripts/issue_show/components/locked_warning.vue
+++ b/app/assets/javascripts/issue_show/components/locked_warning.vue
@@ -2,7 +2,7 @@
export default {
computed: {
currentPath() {
- return location.pathname;
+ return window.location.pathname;
},
},
};
diff --git a/app/assets/javascripts/issue_show/components/title.vue b/app/assets/javascripts/issue_show/components/title.vue
index aec890a2ff6..b5e8e0ea44b 100644
--- a/app/assets/javascripts/issue_show/components/title.vue
+++ b/app/assets/javascripts/issue_show/components/title.vue
@@ -1,77 +1,77 @@
<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';
+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 {
- directives: {
- tooltip,
+export default {
+ directives: {
+ tooltip,
+ },
+ mixins: [animateMixin],
+ props: {
+ issuableRef: {
+ type: [String, Number],
+ required: true,
},
- mixins: [animateMixin],
- props: {
- issuableRef: {
- type: String,
- required: true,
- },
- canUpdate: {
- required: false,
- type: Boolean,
- default: false,
- },
- titleHtml: {
- type: String,
- required: true,
- },
- titleText: {
- type: String,
- required: true,
- },
- showInlineEditButton: {
- type: Boolean,
- required: false,
- default: false,
- },
+ canUpdate: {
+ required: false,
+ type: Boolean,
+ default: false,
},
- data() {
- return {
- preAnimation: false,
- pulseAnimation: false,
- titleEl: document.querySelector('title'),
- };
+ titleHtml: {
+ type: String,
+ required: true,
},
- computed: {
- pencilIcon() {
- return spriteIcon('pencil', 'link-highlight');
- },
+ titleText: {
+ type: String,
+ required: true,
},
- watch: {
- titleHtml() {
- this.setPageTitle();
- this.animateChange();
- },
+ showInlineEditButton: {
+ type: Boolean,
+ required: false,
+ default: false,
},
- methods: {
- setPageTitle() {
- const currentPageTitleScope = this.titleEl.innerText.split('·');
- currentPageTitleScope[0] = `${this.titleText} (${this.issuableRef}) `;
- this.titleEl.textContent = currentPageTitleScope.join('·');
- },
- edit() {
- eventHub.$emit('open.form');
- },
+ },
+ data() {
+ return {
+ preAnimation: false,
+ pulseAnimation: false,
+ titleEl: document.querySelector('title'),
+ };
+ },
+ computed: {
+ pencilIcon() {
+ return spriteIcon('pencil', 'link-highlight');
},
- };
+ },
+ watch: {
+ titleHtml() {
+ this.setPageTitle();
+ this.animateChange();
+ },
+ },
+ 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>
<div class="title-container">
<h2
- class="title"
:class="{
'issue-realtime-pre-pulse': preAnimation,
'issue-realtime-trigger-pulse': pulseAnimation
}"
+ class="title"
v-html="titleHtml"
>
</h2>
@@ -80,11 +80,11 @@
v-if="showInlineEditButton && canUpdate"
type="button"
class="btn btn-default btn-edit btn-svg js-issuable-edit"
- v-html="pencilIcon"
title="Edit title and description"
data-placement="bottom"
data-container="body"
@click="edit"
+ v-html="pencilIcon"
>
</button>
</div>
diff --git a/app/assets/javascripts/job.js b/app/assets/javascripts/job.js
index ace45e9dd29..d4f2a3ef7d3 100644
--- a/app/assets/javascripts/job.js
+++ b/app/assets/javascripts/job.js
@@ -1,13 +1,17 @@
import $ from 'jquery';
import _ from 'underscore';
+import { polyfillSticky } from './lib/utils/sticky';
import axios from './lib/utils/axios_utils';
import { visitUrl } from './lib/utils/url_utility';
import bp from './breakpoints';
import { numberToHumanSize } from './lib/utils/number_utils';
import { setCiStatusFavicon } from './lib/utils/common_utils';
+import { isScrolledToBottom, scrollDown } from './lib/utils/scroll_utils';
+import LogOutputBehaviours from './lib/utils/logoutput_behaviours';
-export default class Job {
+export default class Job extends LogOutputBehaviours {
constructor(options) {
+ super();
this.timeout = null;
this.state = null;
this.fetchingStatusFavicon = false;
@@ -28,10 +32,6 @@ export default class Job {
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();
@@ -47,23 +47,14 @@ export default class Job {
.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()) {
+ if (!isScrolledToBottom()) {
this.toggleScrollAnimation(false);
- } else if (this.isScrolledToBottom() && !this.isLogComplete) {
+ } else if (isScrolledToBottom() && !this.isLogComplete) {
this.toggleScrollAnimation(true);
}
this.scrollThrottled();
@@ -79,76 +70,11 @@ export default class Job {
}
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 $(document).height() > $(window).height();
- }
-
- toggleScroll() {
- const $document = $(document);
- 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 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);
- }
- }
- // eslint-disable-next-line class-methods-use-this
- isScrolledToBottom() {
- const $document = $(document);
-
- const currentPosition = $document.scrollTop();
- const scrollHeight = $document.height();
-
- const windowHeight = $(window).height();
-
- return scrollHeight - currentPosition === windowHeight;
- }
-
- // eslint-disable-next-line class-methods-use-this
- scrollDown() {
- const $document = $(document);
- $document.scrollTop($document.height());
+ polyfillSticky(this.$topBar);
}
scrollToBottom() {
- this.scrollDown();
+ scrollDown();
this.hasBeenScrolled = true;
this.toggleScroll();
}
@@ -159,12 +85,6 @@ export default class Job {
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);
}
@@ -196,7 +116,7 @@ export default class Job {
this.state = log.state;
}
- this.isScrollInBottom = this.isScrolledToBottom();
+ this.isScrollInBottom = isScrolledToBottom();
if (log.append) {
this.$buildTraceOutput.append(log.html);
@@ -236,7 +156,7 @@ export default class Job {
})
.then(() => {
if (this.isScrollInBottom) {
- this.scrollDown();
+ scrollDown();
}
})
.then(() => this.toggleScroll());
@@ -244,7 +164,7 @@ export default class Job {
// eslint-disable-next-line class-methods-use-this
shouldHideSidebarForViewport() {
const bootstrapBreakpoint = bp.getBreakpointSize();
- return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm';
+ return bootstrapBreakpoint === 'xs';
}
toggleSidebar(shouldHide) {
diff --git a/app/assets/javascripts/jobs/components/header.vue b/app/assets/javascripts/jobs/components/header.vue
index 21b545d6cab..1e7f4b2c3f7 100644
--- a/app/assets/javascripts/jobs/components/header.vue
+++ b/app/assets/javascripts/jobs/components/header.vue
@@ -42,6 +42,9 @@ export default {
jobStarted() {
return !this.job.started === false;
},
+ headerTime() {
+ return this.jobStarted ? this.job.started : this.job.created_at;
+ },
},
watch: {
job() {
@@ -56,7 +59,7 @@ export default {
actions.push({
label: 'New issue',
path: this.job.new_issue_path,
- cssClass: 'js-new-issue btn btn-new btn-inverted visible-md-block visible-lg-block',
+ cssClass: 'js-new-issue btn btn-new btn-inverted d-none d-md-block d-lg-block d-xl-block',
type: 'link',
});
}
@@ -71,13 +74,13 @@ export default {
<ci-header
v-if="shouldRenderContent"
:status="status"
- item-name="Job"
:item-id="job.id"
- :time="job.created_at"
+ :time="headerTime"
:user="job.user"
:actions="actions"
:has-sidebar-button="true"
:should-render-triggered-label="jobStarted"
+ item-name="Job"
/>
<loading-icon
v-if="isLoading"
diff --git a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
index dfe87d89a39..83560a8ff0e 100644
--- a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
+++ b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
@@ -39,7 +39,7 @@
<span
v-if="hasHelpURL"
- class="help-button pull-right"
+ class="help-button float-right"
>
<a
:href="helpUrl"
diff --git a/app/assets/javascripts/jobs/components/sidebar_details_block.vue b/app/assets/javascripts/jobs/components/sidebar_details_block.vue
index db19dc9b238..d2adf628050 100644
--- a/app/assets/javascripts/jobs/components/sidebar_details_block.vue
+++ b/app/assets/javascripts/jobs/components/sidebar_details_block.vue
@@ -48,11 +48,10 @@ export default {
return `${this.job.runner.description} (#${this.job.runner.id})`;
},
retryButtonClass() {
- let className = 'js-retry-button pull-right btn btn-retry visible-md-block visible-lg-block';
+ let className =
+ 'js-retry-button float-right btn btn-retry d-none d-md-block d-lg-block d-xl-block';
className +=
- this.job.status && this.job.recoverable
- ? ' btn-primary'
- : ' btn-inverted-secondary';
+ this.job.status && this.job.recoverable ? ' btn-primary' : ' btn-inverted-secondary';
return className;
},
hasTimeout() {
@@ -102,10 +101,9 @@ export default {
{{ __('Retry') }}
</a>
<button
- type="button"
:aria-label="__('Toggle Sidebar')"
- class="btn btn-blank gutter-toggle pull-right
- visible-xs-block visible-sm-block js-sidebar-build-toggle"
+ type="button"
+ class="btn btn-blank gutter-toggle float-right d-block d-md-none js-sidebar-build-toggle"
>
<i
aria-hidden="true"
@@ -116,20 +114,20 @@ export default {
</div>
<template v-if="shouldRenderContent">
<div
- class="block retry-link"
v-if="job.retry_path || job.new_issue_path"
+ class="block retry-link"
>
<a
v-if="job.new_issue_path"
- class="js-new-issue btn btn-new btn-inverted"
:href="job.new_issue_path"
+ class="js-new-issue btn btn-new btn-inverted"
>
{{ __('New issue') }}
</a>
<a
v-if="canUserRetry"
- class="js-retry-job btn btn-inverted-secondary"
:href="job.retry_path"
+ class="js-retry-job btn btn-inverted-secondary"
data-method="post"
rel="nofollow"
>
@@ -138,8 +136,8 @@ export default {
</div>
<div :class="{block : renderBlock }">
<p
- class="build-detail-row js-job-mr"
v-if="job.merge_request"
+ class="build-detail-row js-job-mr"
>
<span class="build-light-text">
{{ __('Merge Request:') }}
@@ -150,51 +148,51 @@ export default {
</p>
<detail-row
- class="js-job-duration"
v-if="job.duration"
- title="Duration"
:value="duration"
+ class="js-job-duration"
+ title="Duration"
/>
<detail-row
- class="js-job-finished"
v-if="job.finished_at"
- title="Finished"
:value="timeFormated(job.finished_at)"
+ class="js-job-finished"
+ title="Finished"
/>
<detail-row
- class="js-job-erased"
v-if="job.erased_at"
- title="Erased"
:value="timeFormated(job.erased_at)"
+ class="js-job-erased"
+ title="Erased"
/>
<detail-row
- class="js-job-queued"
v-if="job.queued"
- title="Queued"
:value="queued"
+ class="js-job-queued"
+ title="Queued"
/>
<detail-row
- class="js-job-timeout"
v-if="hasTimeout"
- title="Timeout"
:help-url="runnerHelpUrl"
:value="timeout"
+ class="js-job-timeout"
+ title="Timeout"
/>
<detail-row
- class="js-job-runner"
v-if="job.runner"
- title="Runner"
:value="runnerId"
+ class="js-job-runner"
+ title="Runner"
/>
<detail-row
- class="js-job-coverage"
v-if="job.coverage"
- title="Coverage"
:value="coverage"
+ class="js-job-coverage"
+ title="Coverage"
/>
<p
- class="build-detail-row js-job-tags"
v-if="job.tags.length"
+ class="build-detail-row js-job-tags"
>
<span class="build-light-text">
{{ __('Tags:') }}
@@ -212,8 +210,8 @@ export default {
class="btn-group prepend-top-5"
role="group">
<a
- class="js-cancel-job btn btn-sm btn-default"
:href="job.cancel_path"
+ class="js-cancel-job btn btn-sm btn-default"
data-method="post"
rel="nofollow"
>
@@ -223,8 +221,8 @@ export default {
</div>
</template>
<loading-icon
- class="prepend-top-10"
v-if="isLoading"
+ class="prepend-top-10"
size="2"
/>
</div>
diff --git a/app/assets/javascripts/jobs/job_details_bundle.js b/app/assets/javascripts/jobs/job_details_bundle.js
index f2939ad4dbe..0db7b95636c 100644
--- a/app/assets/javascripts/jobs/job_details_bundle.js
+++ b/app/assets/javascripts/jobs/job_details_bundle.js
@@ -4,7 +4,7 @@ import jobHeader from './components/header.vue';
import detailsBlock from './components/sidebar_details_block.vue';
export default () => {
- const dataset = document.getElementById('js-job-details-vue').dataset;
+ const { dataset } = document.getElementById('js-job-details-vue');
const mediator = new JobMediator({ endpoint: dataset.endpoint });
mediator.fetchJob();
diff --git a/app/assets/javascripts/jobs/job_details_mediator.js b/app/assets/javascripts/jobs/job_details_mediator.js
index 5a216f8fae2..89019da9d1e 100644
--- a/app/assets/javascripts/jobs/job_details_mediator.js
+++ b/app/assets/javascripts/jobs/job_details_mediator.js
@@ -1,5 +1,3 @@
-/* global Build */
-
import Visibility from 'visibilityjs';
import Flash from '../flash';
import Poll from '../lib/utils/poll';
@@ -50,7 +48,8 @@ export default class JobMediator {
}
getJob() {
- return this.service.getJob()
+ return this.service
+ .getJob()
.then(response => this.successCallback(response))
.catch(() => this.errorCallback());
}
diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/label_manager.js
index e230dbbd4ac..c10b1a2b233 100644
--- a/app/assets/javascripts/label_manager.js
+++ b/app/assets/javascripts/label_manager.js
@@ -1,7 +1,7 @@
-/* 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 */
+/* eslint-disable class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, func-names, max-len */
import $ from 'jquery';
-import Sortable from 'vendor/Sortable';
+import Sortable from 'sortablejs';
import flash from './flash';
import axios from './lib/utils/axios_utils';
@@ -13,6 +13,7 @@ export default class LabelManager {
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.$badgeItemTemplate = $('#js-badge-item-template');
this.sortable = Sortable.create(this.prioritizedLabels.get(0), {
filter: '.empty-message',
forceFallback: true,
@@ -35,7 +36,7 @@ export default class LabelManager {
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');
+ $tooltip.tooltip('dispose');
_this.toggleLabelPriority($label, action);
_this.toggleEmptyState($label, $btn, action);
}
@@ -63,7 +64,11 @@ export default class LabelManager {
$target = this.otherLabels;
$from = this.prioritizedLabels;
}
- $label.detach().appendTo($target);
+
+ const $detachedLabel = $label.detach();
+ this.toggleLabelPriorityBadge($detachedLabel, action);
+ $detachedLabel.appendTo($target);
+
if ($from.find('li').length) {
$from.find('.empty-message').removeClass('hidden');
}
@@ -88,6 +93,14 @@ export default class LabelManager {
}
}
+ toggleLabelPriorityBadge($label, action) {
+ if (action === 'remove') {
+ $('.js-priority-badge', $label).remove();
+ } else {
+ $('.label-links', $label).append(this.$badgeItemTemplate.clone().html());
+ }
+ }
+
onPrioritySortUpdate() {
this.savePrioritySort()
.catch(() => flash(this.errorMessage));
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 9b62cfb8206..37a45d1d1a2 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -1,4 +1,4 @@
-/* eslint-disable no-useless-return, func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, no-unused-vars, one-var-declaration-per-line, prefer-template, no-new, consistent-return, object-shorthand, comma-dangle, no-shadow, no-param-reassign, brace-style, vars-on-top, quotes, no-lonely-if, no-else-return, dot-notation, no-empty, no-return-assign, camelcase, prefer-spread */
+/* eslint-disable no-useless-return, func-names, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, no-unused-vars, one-var-declaration-per-line, prefer-template, no-new, consistent-return, object-shorthand, comma-dangle, no-shadow, no-param-reassign, brace-style, vars-on-top, quotes, no-lonely-if, no-else-return, dot-notation, no-empty */
/* global Issuable */
/* global ListLabel */
@@ -56,7 +56,7 @@ export default class LabelsSelect {
.map(function () {
return this.value;
}).get();
- const handleClick = options.handleClick;
+ const { handleClick } = options;
$sidebarLabelTooltip.tooltip();
@@ -120,7 +120,7 @@ export default class LabelsSelect {
$sidebarLabelTooltip
.attr('title', labelTooltipTitle)
- .tooltip('fixTitle');
+ .tooltip('_fixTitle');
$('.has-tooltip', $value).tooltip({
container: 'body'
@@ -215,7 +215,7 @@ export default class LabelsSelect {
}
else {
if (label.color != null) {
- color = label.color[0];
+ [color] = label.color;
}
}
if (color) {
@@ -243,7 +243,8 @@ export default class LabelsSelect {
var $dropdownParent = $dropdown.parent();
var $dropdownInputField = $dropdownParent.find('.dropdown-input-field');
var isSelected = el !== null ? el.hasClass('is-active') : false;
- var title = selected.title;
+
+ var { title } = selected;
var selectedLabels = this.selected;
if ($dropdownInputField.length && $dropdownInputField.val().length) {
@@ -382,7 +383,7 @@ export default class LabelsSelect {
}));
}
else {
- var labels = gl.issueBoards.BoardsStore.detail.issue.labels;
+ var { labels } = gl.issueBoards.BoardsStore.detail.issue;
labels = labels.filter(function (selectedLabel) {
return selectedLabel.id !== label.id;
});
@@ -426,7 +427,7 @@ export default class LabelsSelect {
const tpl = _.template([
'<% _.each(labels, function(label){ %>',
'<a href="<%- issueUpdateURL.slice(0, issueUpdateURL.lastIndexOf("/")) %>?label_name[]=<%- encodeURIComponent(label.title) %>">',
- '<span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">',
+ '<span class="badge label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">',
'<%- label.title %>',
'</span>',
'</a>',
diff --git a/app/assets/javascripts/lazy_loader.js b/app/assets/javascripts/lazy_loader.js
index dbbf1637a47..9482d131344 100644
--- a/app/assets/javascripts/lazy_loader.js
+++ b/app/assets/javascripts/lazy_loader.js
@@ -44,8 +44,8 @@ export default class LazyLoader {
requestAnimationFrame(() => this.checkElementsInView());
}
checkElementsInView() {
- const scrollTop = pageYOffset;
- const visHeight = scrollTop + innerHeight + SCROLL_THRESHOLD;
+ const scrollTop = window.pageYOffset;
+ const visHeight = scrollTop + window.innerHeight + SCROLL_THRESHOLD;
// Loading Images which are in the current viewport or close to them
this.lazyImages = this.lazyImages.filter((selectedImage) => {
diff --git a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js
index 3873f4528ce..c28ed04f94f 100644
--- a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js
+++ b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js
@@ -93,7 +93,7 @@ export default class LinkedTabs {
const newState = `${copySource}${this.currentLocation.search}${this.currentLocation.hash}`;
- history.replaceState({
+ window.history.replaceState({
url: newState,
}, document.title, newState);
return newState;
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 9ff2042475b..6b7550efff8 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -1,10 +1,14 @@
import $ from 'jquery';
-import Cookies from 'js-cookie';
import axios from './axios_utils';
import { getLocationHash } from './url_utility';
import { convertToCamelCase } from './text_utility';
+import { isObject } from './type_utility';
-export const getPagePath = (index = 0) => $('body').attr('data-page').split(':')[index];
+export const getPagePath = (index = 0) => {
+ const page = $('body').attr('data-page') || '';
+
+ return page.split(':')[index];
+};
export const isInGroupsPage = () => getPagePath() === 'groups';
@@ -34,24 +38,25 @@ export const checkPageAndAction = (page, action) => {
export const isInIssuePage = () => checkPageAndAction('issues', 'show');
export const isInMRPage = () => checkPageAndAction('merge_requests', 'show');
export const isInEpicPage = () => checkPageAndAction('epics', 'show');
-export const isInNoteablePage = () => isInIssuePage() || isInMRPage();
-export const hasVueMRDiscussionsCookie = () => Cookies.get('vue_mr_discussions');
-
-export const ajaxGet = url => axios.get(url, {
- params: { format: 'js' },
- responseType: 'text',
-}).then(({ data }) => {
- $.globalEval(data);
-});
-export const rstrip = (val) => {
+export const ajaxGet = url =>
+ axios
+ .get(url, {
+ params: { format: 'js' },
+ responseType: 'text',
+ })
+ .then(({ data }) => {
+ $.globalEval(data);
+ });
+
+export const rstrip = val => {
if (val) {
return val.replace(/\s+$/, '');
}
return val;
};
-export const updateTooltipTitle = ($tooltipEl, newTitle) => $tooltipEl.attr('title', newTitle).tooltip('fixTitle');
+export const updateTooltipTitle = ($tooltipEl, newTitle) => $tooltipEl.attr('title', newTitle).tooltip('_fixTitle');
export const disableButtonIfEmptyField = (fieldSelector, buttonSelector, eventName = 'input') => {
const field = $(fieldSelector);
@@ -60,7 +65,7 @@ export const disableButtonIfEmptyField = (fieldSelector, buttonSelector, eventNa
closestSubmit.disable();
}
// eslint-disable-next-line func-names
- return field.on(eventName, function () {
+ return field.on(eventName, function() {
if (rstrip($(this).val()) === '') {
return closestSubmit.disable();
}
@@ -79,7 +84,7 @@ export const handleLocationHash = () => {
const target = document.getElementById(hash) || document.getElementById(`user-content-${hash}`);
const fixedTabs = document.querySelector('.js-tabs-affix');
- const fixedDiffStats = document.querySelector('.js-diff-files-changed.is-stuck');
+ const fixedDiffStats = document.querySelector('.js-diff-files-changed');
const fixedNav = document.querySelector('.navbar-gitlab');
let adjustment = 0;
@@ -102,7 +107,7 @@ export const handleLocationHash = () => {
// Check if element scrolled into viewport from above or below
// Courtesy http://stackoverflow.com/a/7557433/414749
-export const isInViewport = (el) => {
+export const isInViewport = el => {
const rect = el.getBoundingClientRect();
return (
@@ -113,13 +118,13 @@ export const isInViewport = (el) => {
);
};
-export const parseUrl = (url) => {
+export const parseUrl = url => {
const parser = document.createElement('a');
parser.href = url;
return parser;
};
-export const parseUrlPathname = (url) => {
+export const parseUrlPathname = url => {
const parsedUrl = parseUrl(url);
// parsedUrl.pathname will return an absolute path for Firefox and a relative path for IE11
// We have to make sure we always have an absolute path.
@@ -128,10 +133,14 @@ export const parseUrlPathname = (url) => {
// We can trust that each param has one & since values containing & will be encoded
// Remove the first character of search as it is always ?
-export const getUrlParamsArray = () => window.location.search.slice(1).split('&').map((param) => {
- const split = param.split('=');
- return [decodeURI(split[0]), split[1]].join('=');
-});
+export const getUrlParamsArray = () =>
+ window.location.search
+ .slice(1)
+ .split('&')
+ .map(param => {
+ const split = param.split('=');
+ return [decodeURI(split[0]), split[1]].join('=');
+ });
export const isMetaKey = e => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
@@ -141,18 +150,28 @@ export const isMetaKey = e => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
// 3) Middle-click or Mouse Wheel Click (e.which is 2)
export const isMetaClick = e => e.metaKey || e.ctrlKey || e.which === 2;
-export const scrollToElement = (element) => {
+export const contentTop = () => {
+ const perfBar = $('#js-peek').height() || 0;
+ const mrTabsHeight = $('.merge-request-tabs').height() || 0;
+ const headerHeight = $('.navbar-gitlab').height() || 0;
+ const diffFilesChanged = $('.js-diff-files-changed').height() || 0;
+
+ return perfBar + mrTabsHeight + headerHeight + diffFilesChanged;
+};
+
+export const scrollToElement = element => {
let $el = element;
if (!(element instanceof $)) {
$el = $(element);
}
- const top = $el.offset().top;
- const mrTabsHeight = $('.merge-request-tabs').height() || 0;
- const headerHeight = $('.navbar-gitlab').height() || 0;
+ const { top } = $el.offset();
- return $('body, html').animate({
- scrollTop: top - mrTabsHeight - headerHeight,
- }, 200);
+ return $('body, html').animate(
+ {
+ scrollTop: top - contentTop(),
+ },
+ 200,
+ );
};
/**
@@ -170,12 +189,25 @@ export const getParameterByName = (name, urlToParse) => {
return decodeURIComponent(results[2].replace(/\+/g, ' '));
};
+const handleSelectedRange = (range) => {
+ const container = range.commonAncestorContainer;
+ // add context to fragment if needed
+ if (container.tagName === 'OL') {
+ const parentContainer = document.createElement(container.tagName);
+ parentContainer.appendChild(range.cloneContents());
+ return parentContainer;
+ }
+ return range.cloneContents();
+};
+
export const getSelectedFragment = () => {
const selection = window.getSelection();
if (selection.rangeCount === 0) return null;
const documentFragment = document.createDocumentFragment();
+
for (let i = 0; i < selection.rangeCount; i += 1) {
- documentFragment.appendChild(selection.getRangeAt(i).cloneContents());
+ const range = selection.getRangeAt(i);
+ documentFragment.appendChild(handleSelectedRange(range));
}
if (documentFragment.textContent.length === 0) return null;
@@ -184,9 +216,7 @@ export const getSelectedFragment = () => {
export const insertText = (target, text) => {
// Firefox doesn't support `document.execCommand('insertText', false, text)` on textareas
- const selectionStart = target.selectionStart;
- const selectionEnd = target.selectionEnd;
- const value = target.value;
+ const { selectionStart, selectionEnd, value } = target;
const textBefore = value.substring(0, selectionStart);
const textAfter = value.substring(selectionEnd, value.length);
@@ -197,7 +227,10 @@ export const insertText = (target, text) => {
// eslint-disable-next-line no-param-reassign
target.value = newText;
// eslint-disable-next-line no-param-reassign
- target.selectionStart = target.selectionEnd = selectionStart + insertedText.length;
+ target.selectionStart = selectionStart + insertedText.length;
+
+ // eslint-disable-next-line no-param-reassign
+ target.selectionEnd = selectionStart + insertedText.length;
// Trigger autosave
target.dispatchEvent(new Event('input'));
@@ -209,7 +242,8 @@ export const insertText = (target, text) => {
};
export const nodeMatchesSelector = (node, selector) => {
- const matches = Element.prototype.matches ||
+ const matches =
+ Element.prototype.matches ||
Element.prototype.matchesSelector ||
Element.prototype.mozMatchesSelector ||
Element.prototype.msMatchesSelector ||
@@ -222,7 +256,8 @@ export const nodeMatchesSelector = (node, selector) => {
// IE11 doesn't support `node.matches(selector)`
- let parentNode = node.parentNode;
+ let { parentNode } = node;
+
if (!parentNode) {
parentNode = document.createElement('div');
// eslint-disable-next-line no-param-reassign
@@ -238,10 +273,10 @@ export const nodeMatchesSelector = (node, selector) => {
this will take in the headers from an API response and normalize them
this way we don't run into production issues when nginx gives us lowercased header keys
*/
-export const normalizeHeaders = (headers) => {
+export const normalizeHeaders = headers => {
const upperCaseHeaders = {};
- Object.keys(headers || {}).forEach((e) => {
+ Object.keys(headers || {}).forEach(e => {
upperCaseHeaders[e.toUpperCase()] = headers[e];
});
@@ -252,12 +287,14 @@ export const normalizeHeaders = (headers) => {
this will take in the getAllResponseHeaders result and normalize them
this way we don't run into production issues when nginx gives us lowercased header keys
*/
-export const normalizeCRLFHeaders = (headers) => {
+export const normalizeCRLFHeaders = headers => {
const headersObject = {};
const headersArray = headers.split('\n');
- headersArray.forEach((header) => {
+ headersArray.forEach(header => {
const keyValue = header.split(': ');
+
+ // eslint-disable-next-line prefer-destructuring
headersObject[keyValue[0]] = keyValue[1];
});
@@ -292,15 +329,13 @@ export const parseIntPagination = paginationInformation => ({
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;
- }, {});
+ return query.split('&').reduce((acc, element) => {
+ const val = element.split('=');
+ Object.assign(acc, {
+ [val[0]]: decodeURIComponent(val[1]),
+ });
+ return acc;
+ }, {});
};
/**
@@ -309,9 +344,13 @@ export const parseQueryStringIntoObject = (query = '') => {
*
* @param {Object} params
*/
-export const objectToQueryString = (params = {}) => Object.keys(params).map(param => `${param}=${params[param]}`).join('&');
+export const objectToQueryString = (params = {}) =>
+ Object.keys(params)
+ .map(param => `${param}=${params[param]}`)
+ .join('&');
-export const buildUrlWithCurrentLocation = param => (param ? `${window.location.pathname}${param}` : window.location.pathname);
+export const buildUrlWithCurrentLocation = param =>
+ (param ? `${window.location.pathname}${param}` : window.location.pathname);
/**
* Based on the current location and the string parameters provided
@@ -319,7 +358,7 @@ export const buildUrlWithCurrentLocation = param => (param ? `${window.location.
*
* @param {String} param
*/
-export const historyPushState = (newUrl) => {
+export const historyPushState = newUrl => {
window.history.pushState({}, document.title, newUrl);
};
@@ -368,7 +407,7 @@ export const backOff = (fn, timeout = 60000) => {
let timeElapsed = 0;
return new Promise((resolve, reject) => {
- const stop = arg => ((arg instanceof Error) ? reject(arg) : resolve(arg));
+ const stop = arg => (arg instanceof Error ? reject(arg) : resolve(arg));
const next = () => {
if (timeElapsed < timeout) {
@@ -384,6 +423,49 @@ export const backOff = (fn, timeout = 60000) => {
});
};
+export const createOverlayIcon = (iconPath, overlayPath) => {
+ const faviconImage = document.createElement('img');
+
+ return new Promise((resolve) => {
+ faviconImage.onload = () => {
+ const size = 32;
+
+ const canvas = document.createElement('canvas');
+ canvas.width = size;
+ canvas.height = size;
+
+ const context = canvas.getContext('2d');
+ context.clearRect(0, 0, size, size);
+ context.drawImage(
+ faviconImage, 0, 0, faviconImage.width, faviconImage.height, 0, 0, size, size,
+ );
+
+ const overlayImage = document.createElement('img');
+ overlayImage.onload = () => {
+ context.drawImage(
+ overlayImage, 0, 0, overlayImage.width, overlayImage.height, 0, 0, size, size,
+ );
+
+ const faviconWithOverlayUrl = canvas.toDataURL();
+
+ resolve(faviconWithOverlayUrl);
+ };
+ overlayImage.src = overlayPath;
+ };
+ faviconImage.src = iconPath;
+ });
+};
+
+export const setFaviconOverlay = (overlayPath) => {
+ const faviconEl = document.getElementById('favicon');
+
+ if (!faviconEl) { return null; }
+
+ const iconPath = faviconEl.getAttribute('data-original-href');
+
+ return createOverlayIcon(iconPath, overlayPath).then(faviconWithOverlayUrl => faviconEl.setAttribute('href', faviconWithOverlayUrl));
+};
+
export const setFavicon = (faviconPath) => {
const faviconEl = document.getElementById('favicon');
if (faviconEl && faviconPath) {
@@ -393,20 +475,21 @@ export const setFavicon = (faviconPath) => {
export const resetFavicon = () => {
const faviconEl = document.getElementById('favicon');
- const originalFavicon = faviconEl ? faviconEl.getAttribute('href') : null;
+
if (faviconEl) {
+ const originalFavicon = faviconEl.getAttribute('data-original-href');
faviconEl.setAttribute('href', originalFavicon);
}
};
export const setCiStatusFavicon = pageUrl =>
- axios.get(pageUrl)
+ axios
+ .get(pageUrl)
.then(({ data }) => {
if (data && data.favicon) {
- setFavicon(data.favicon);
- } else {
- resetFavicon();
+ return setFaviconOverlay(data.favicon);
}
+ return resetFavicon();
})
.catch(resetFavicon);
@@ -423,28 +506,38 @@ export const spriteIcon = (icon, className = '') => {
* Reasoning for this method is to ensure consistent property
* naming conventions across JS code.
*/
-export const convertObjectPropsToCamelCase = (obj = {}) => {
+export const convertObjectPropsToCamelCase = (obj = {}, options = {}) => {
if (obj === null) {
return {};
}
+ const initial = Array.isArray(obj) ? [] : {};
+
return Object.keys(obj).reduce((acc, prop) => {
const result = acc;
+ const val = obj[prop];
- result[convertToCamelCase(prop)] = obj[prop];
+ if (options.deep && (isObject(val) || Array.isArray(val))) {
+ result[convertToCamelCase(prop)] = convertObjectPropsToCamelCase(val, options);
+ } else {
+ result[convertToCamelCase(prop)] = obj[prop];
+ }
return acc;
- }, {});
+ }, initial);
};
-export const imagePath = imgUrl => `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/${imgUrl}`;
+export const imagePath = imgUrl =>
+ `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/${imgUrl}`;
export const addSelectOnFocusBehaviour = (selector = '.js-select-on-focus') => {
// Click a .js-select-on-focus field, select the contents
// Prevent a mouseup event from deselecting the input
$(selector).on('focusin', function selectOnFocusCallback() {
- $(this).select().one('mouseup', (e) => {
- e.preventDefault();
- });
+ $(this)
+ .select()
+ .one('mouseup', e => {
+ e.preventDefault();
+ });
});
};
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index b7624cf490a..1f66fa811ea 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -1,11 +1,10 @@
import $ from 'jquery';
import timeago from 'timeago.js';
-import dateFormat from 'vendor/date.format';
+import dateFormat from 'dateformat';
import { pluralize } from './text_utility';
import { languageCode, s__ } from '../../locale';
window.timeago = timeago;
-window.dateFormat = dateFormat;
/**
* Returns i18n month names array.
@@ -79,37 +78,37 @@ export function getTimeago() {
if (!timeagoInstance) {
const localeRemaining = function getLocaleRemaining(number, index) {
return [
- [s__('Timeago|less than a minute ago'), s__('Timeago|right now')],
- [s__('Timeago|less than a minute ago'), s__('Timeago|%s seconds remaining')],
- [s__('Timeago|about a minute ago'), s__('Timeago|1 minute remaining')],
+ [s__('Timeago|just now'), s__('Timeago|right now')],
+ [s__('Timeago|%s seconds ago'), s__('Timeago|%s seconds remaining')],
+ [s__('Timeago|1 minute ago'), s__('Timeago|1 minute remaining')],
[s__('Timeago|%s minutes ago'), s__('Timeago|%s minutes remaining')],
- [s__('Timeago|about an hour ago'), s__('Timeago|1 hour remaining')],
- [s__('Timeago|about %s hours ago'), s__('Timeago|%s hours remaining')],
- [s__('Timeago|a day ago'), s__('Timeago|1 day remaining')],
+ [s__('Timeago|1 hour ago'), s__('Timeago|1 hour remaining')],
+ [s__('Timeago|%s hours ago'), s__('Timeago|%s hours remaining')],
+ [s__('Timeago|1 day ago'), s__('Timeago|1 day remaining')],
[s__('Timeago|%s days ago'), s__('Timeago|%s days remaining')],
- [s__('Timeago|a week ago'), s__('Timeago|1 week remaining')],
+ [s__('Timeago|1 week ago'), s__('Timeago|1 week remaining')],
[s__('Timeago|%s weeks ago'), s__('Timeago|%s weeks remaining')],
- [s__('Timeago|a month ago'), s__('Timeago|1 month remaining')],
+ [s__('Timeago|1 month ago'), s__('Timeago|1 month remaining')],
[s__('Timeago|%s months ago'), s__('Timeago|%s months remaining')],
- [s__('Timeago|a year ago'), s__('Timeago|1 year remaining')],
+ [s__('Timeago|1 year ago'), s__('Timeago|1 year remaining')],
[s__('Timeago|%s years ago'), s__('Timeago|%s years remaining')],
][index];
};
const locale = function getLocale(number, index) {
return [
- [s__('Timeago|less than a minute ago'), s__('Timeago|right now')],
- [s__('Timeago|less than a minute ago'), s__('Timeago|in %s seconds')],
- [s__('Timeago|about a minute ago'), s__('Timeago|in 1 minute')],
+ [s__('Timeago|just now'), s__('Timeago|right now')],
+ [s__('Timeago|%s seconds ago'), s__('Timeago|in %s seconds')],
+ [s__('Timeago|1 minute ago'), s__('Timeago|in 1 minute')],
[s__('Timeago|%s minutes ago'), s__('Timeago|in %s minutes')],
- [s__('Timeago|about an hour ago'), s__('Timeago|in 1 hour')],
- [s__('Timeago|about %s hours ago'), s__('Timeago|in %s hours')],
- [s__('Timeago|a day ago'), s__('Timeago|in 1 day')],
+ [s__('Timeago|1 hour ago'), s__('Timeago|in 1 hour')],
+ [s__('Timeago|%s hours ago'), s__('Timeago|in %s hours')],
+ [s__('Timeago|1 day ago'), s__('Timeago|in 1 day')],
[s__('Timeago|%s days ago'), s__('Timeago|in %s days')],
- [s__('Timeago|a week ago'), s__('Timeago|in 1 week')],
+ [s__('Timeago|1 week ago'), s__('Timeago|in 1 week')],
[s__('Timeago|%s weeks ago'), s__('Timeago|in %s weeks')],
- [s__('Timeago|a month ago'), s__('Timeago|in 1 month')],
+ [s__('Timeago|1 month ago'), s__('Timeago|in 1 month')],
[s__('Timeago|%s months ago'), s__('Timeago|in %s months')],
- [s__('Timeago|a year ago'), s__('Timeago|in 1 year')],
+ [s__('Timeago|1 year ago'), s__('Timeago|in 1 year')],
[s__('Timeago|%s years ago'), s__('Timeago|in %s years')],
][index];
};
@@ -144,7 +143,7 @@ export const localTimeAgo = ($timeagoEls, setTimeago = true) => {
// Recreate with custom template
$(el).tooltip({
template:
- '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',
+ '<div class="tooltip local-timeago" role="tooltip"><div class="arrow"></div><div class="tooltip-inner"></div></div>',
});
}
@@ -271,6 +270,15 @@ export const totalDaysInMonth = date => {
};
/**
+ * Returns number of days in a quarter from provided
+ * months array.
+ *
+ * @param {Array} quarter
+ */
+export const totalDaysInQuarter = quarter =>
+ quarter.reduce((acc, month) => acc + totalDaysInMonth(month), 0);
+
+/**
* Returns list of Dates referring to Sundays of the month
* based on provided date
*
@@ -310,42 +318,21 @@ export const getSundays = date => {
};
/**
- * Returns list of Dates representing a timeframe of Months from month of provided date (inclusive)
- * up to provided length
- *
- * For eg;
- * If current month is January 2018 and `length` provided is `6`
- * Then this method will return list of Date objects as follows;
- *
- * [ October 2017, November 2017, December 2017, January 2018, February 2018, March 2018 ]
- *
- * If current month is March 2018 and `length` provided is `3`
- * Then this method will return list of Date objects as follows;
- *
- * [ February 2018, March 2018, April 2018 ]
+ * Returns list of Dates representing a timeframe of months from startDate and length
*
+ * @param {Date} startDate
* @param {Number} length
- * @param {Date} date
*/
-export const getTimeframeWindow = (length, date) => {
- if (!length) {
+export const getTimeframeWindowFrom = (startDate, length) => {
+ if (!(startDate instanceof Date) || !length) {
return [];
}
- const currentDate = date instanceof Date ? date : new Date();
- const currentMonthIndex = Math.floor(length / 2);
- const timeframe = [];
-
- // Move date object backward to the first month of timeframe
- currentDate.setDate(1);
- currentDate.setMonth(currentDate.getMonth() - currentMonthIndex);
-
- // Iterate and update date for the size of length
+ // Iterate and set date for the size of length
// and push date reference to timeframe list
- for (let i = 0; i < length; i += 1) {
- timeframe.push(new Date(currentDate.getTime()));
- currentDate.setMonth(currentDate.getMonth() + 1);
- }
+ const timeframe = new Array(length)
+ .fill()
+ .map((val, i) => new Date(startDate.getFullYear(), startDate.getMonth() + i, 1));
// Change date of last timeframe item to last date of the month
timeframe[length - 1].setDate(totalDaysInMonth(timeframe[length - 1]));
@@ -353,6 +340,30 @@ export const getTimeframeWindow = (length, date) => {
return timeframe;
};
+/**
+ * Returns count of day within current quarter from provided date
+ * and array of months for the quarter
+ *
+ * Eg;
+ * If date is 15 Feb 2018
+ * and quarter is [Jan, Feb, Mar]
+ *
+ * Then 15th Feb is 46th day of the quarter
+ * Where 31 (days in Jan) + 15 (date of Feb).
+ *
+ * @param {Date} date
+ * @param {Array} quarter
+ */
+export const dayInQuarter = (date, quarter) =>
+ quarter.reduce((acc, month) => {
+ if (date.getMonth() > month.getMonth()) {
+ return acc + totalDaysInMonth(month);
+ } else if (date.getMonth() === month.getMonth()) {
+ return acc + date.getDate();
+ }
+ return acc + 0;
+ }, 0);
+
window.gl = window.gl || {};
window.gl.utils = {
...(window.gl.utils || {}),
diff --git a/app/assets/javascripts/lib/utils/dom_utils.js b/app/assets/javascripts/lib/utils/dom_utils.js
index 914de9de940..6f42382246d 100644
--- a/app/assets/javascripts/lib/utils/dom_utils.js
+++ b/app/assets/javascripts/lib/utils/dom_utils.js
@@ -1,7 +1,4 @@
-import $ from 'jquery';
-import { isInIssuePage, isInMRPage, isInEpicPage, hasVueMRDiscussionsCookie } from './common_utils';
-
-const isVueMRDiscussions = () => isInMRPage() && hasVueMRDiscussionsCookie() && !$('#diffs').is(':visible');
+import { isInIssuePage, isInMRPage, isInEpicPage } from './common_utils';
export const addClassIfElementExists = (element, className) => {
if (element) {
@@ -9,4 +6,4 @@ export const addClassIfElementExists = (element, className) => {
}
};
-export const isInVueNoteablePage = () => isInIssuePage() || isInEpicPage() || isVueMRDiscussions();
+export const isInVueNoteablePage = () => isInIssuePage() || isInEpicPage() || isInMRPage();
diff --git a/app/assets/javascripts/lib/utils/http_status.js b/app/assets/javascripts/lib/utils/http_status.js
index bb151929431..229d53b18b0 100644
--- a/app/assets/javascripts/lib/utils/http_status.js
+++ b/app/assets/javascripts/lib/utils/http_status.js
@@ -8,4 +8,5 @@ export default {
OK: 200,
MULTIPLE_CHOICES: 300,
BAD_REQUEST: 400,
+ NOT_FOUND: 404,
};
diff --git a/app/assets/javascripts/lib/utils/logoutput_behaviours.js b/app/assets/javascripts/lib/utils/logoutput_behaviours.js
new file mode 100644
index 00000000000..1bf99d935ef
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/logoutput_behaviours.js
@@ -0,0 +1,46 @@
+import $ from 'jquery';
+import { canScroll, isScrolledToBottom, toggleDisableButton } from './scroll_utils';
+
+export default class LogOutputBehaviours {
+ constructor() {
+ // Scroll buttons
+ this.$scrollTopBtn = $('.js-scroll-up');
+ this.$scrollBottomBtn = $('.js-scroll-down');
+
+ this.$scrollTopBtn.off('click').on('click', this.scrollToTop.bind(this));
+ this.$scrollBottomBtn.off('click').on('click', this.scrollToBottom.bind(this));
+ }
+
+ toggleScroll() {
+ const $document = $(document);
+ const currentPosition = $document.scrollTop();
+ const scrollHeight = $document.height();
+
+ const windowHeight = $(window).height();
+ if (canScroll()) {
+ if (currentPosition > 0 && scrollHeight - currentPosition !== windowHeight) {
+ // User is in the middle of the log
+
+ toggleDisableButton(this.$scrollTopBtn, false);
+ toggleDisableButton(this.$scrollBottomBtn, false);
+ } else if (currentPosition === 0) {
+ // User is at Top of Log
+
+ toggleDisableButton(this.$scrollTopBtn, true);
+ toggleDisableButton(this.$scrollBottomBtn, false);
+ } else if (isScrolledToBottom()) {
+ // User is at the bottom of the build log.
+
+ toggleDisableButton(this.$scrollTopBtn, false);
+ toggleDisableButton(this.$scrollBottomBtn, true);
+ }
+ } else {
+ toggleDisableButton(this.$scrollTopBtn, true);
+ toggleDisableButton(this.$scrollBottomBtn, true);
+ }
+ }
+
+ toggleScrollAnimation(toggle) {
+ this.$scrollBottomBtn.toggleClass('animate', toggle);
+ }
+}
diff --git a/app/assets/javascripts/lib/utils/notify.js b/app/assets/javascripts/lib/utils/notify.js
index 973d6119158..305ad3e5e26 100644
--- a/app/assets/javascripts/lib/utils/notify.js
+++ b/app/assets/javascripts/lib/utils/notify.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, consistent-return, prefer-arrow-callback, no-return-assign, object-shorthand, comma-dangle, no-param-reassign, max-len */
+/* eslint-disable func-names, no-var, consistent-return, prefer-arrow-callback, no-return-assign, object-shorthand, comma-dangle, max-len */
function notificationGranted(message, opts, onclick) {
var notification;
diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js
index a02c79b787e..afbab59055b 100644
--- a/app/assets/javascripts/lib/utils/number_utils.js
+++ b/app/assets/javascripts/lib/utils/number_utils.js
@@ -12,8 +12,8 @@ export function formatRelevantDigits(number) {
let digitsLeft = '';
let relevantDigits = 0;
let formattedNumber = '';
- if (!isNaN(Number(number))) {
- digitsLeft = number.toString().split('.')[0];
+ if (!Number.isNaN(Number(number))) {
+ [digitsLeft] = number.toString().split('.');
switch (digitsLeft.length) {
case 1:
relevantDigits = 3;
diff --git a/app/assets/javascripts/lib/utils/scroll_utils.js b/app/assets/javascripts/lib/utils/scroll_utils.js
new file mode 100644
index 00000000000..9313b570863
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/scroll_utils.js
@@ -0,0 +1,29 @@
+import $ from 'jquery';
+
+export const canScroll = () => $(document).height() > $(window).height();
+
+/**
+ * Checks if the entire page is scrolled down all the way to the bottom
+ */
+export const isScrolledToBottom = () => {
+ const $document = $(document);
+
+ const currentPosition = $document.scrollTop();
+ const scrollHeight = $document.height();
+
+ const windowHeight = $(window).height();
+
+ return scrollHeight - currentPosition === windowHeight;
+};
+
+export const scrollDown = () => {
+ const $document = $(document);
+ $document.scrollTop($document.height());
+};
+
+export const toggleDisableButton = ($button, disable) => {
+ if (disable && $button.prop('disabled')) return;
+ $button.prop('disabled', disable);
+};
+
+export default {};
diff --git a/app/assets/javascripts/lib/utils/sticky.js b/app/assets/javascripts/lib/utils/sticky.js
index 098afcfa1b4..15a4dd62012 100644
--- a/app/assets/javascripts/lib/utils/sticky.js
+++ b/app/assets/javascripts/lib/utils/sticky.js
@@ -1,3 +1,5 @@
+import StickyFill from 'stickyfilljs';
+
export const createPlaceholder = () => {
const placeholder = document.createElement('div');
placeholder.classList.add('sticky-placeholder');
@@ -28,7 +30,16 @@ export const isSticky = (el, scrollY, stickyTop, insertPlaceholder) => {
}
};
-export default (el, stickyTop, insertPlaceholder = true) => {
+/**
+ * Create a listener that will toggle a 'is-stuck' class, based on the current scroll position.
+ *
+ * - If the current environment does not support `position: sticky`, do nothing.
+ *
+ * @param {HTMLElement} el The `position: sticky` element.
+ * @param {Number} stickyTop Used to determine when an element is stuck.
+ * @param {Boolean} insertPlaceholder Should a placeholder element be created when element is stuck?
+ */
+export const stickyMonitor = (el, stickyTop, insertPlaceholder = true) => {
if (!el) return;
if (typeof CSS === 'undefined' || !(CSS.supports('(position: -webkit-sticky) or (position: sticky)'))) return;
@@ -37,3 +48,13 @@ export default (el, stickyTop, insertPlaceholder = true) => {
passive: true,
});
};
+
+/**
+ * Polyfill the `position: sticky` behavior.
+ *
+ * - If the current environment supports `position: sticky`, do nothing.
+ * - Can receive an iterable element list (NodeList, jQuery collection, etc.) or single HTMLElement.
+ */
+export const polyfillSticky = (el) => {
+ StickyFill.add(el);
+};
diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js
index 5a16adea4dc..ce0bc4d40e9 100644
--- a/app/assets/javascripts/lib/utils/text_markdown.js
+++ b/app/assets/javascripts/lib/utils/text_markdown.js
@@ -1,4 +1,4 @@
-/* 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 */
+/* eslint-disable func-names, no-var, no-param-reassign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, max-len, consistent-return, no-unused-vars, max-len */
import $ from 'jquery';
import { insertText } from '~/lib/utils/common_utils';
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index 5e786ee6935..5f25c6ce1ae 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -58,6 +58,14 @@ export const slugify = str => str.trim().toLowerCase();
export const truncate = (string, maxLength) => `${string.substr(0, maxLength - 3)}...`;
/**
+ * Truncate SHA to 8 characters
+ *
+ * @param {String} sha
+ * @returns {String}
+ */
+export const truncateSha = sha => sha.substr(0, 8);
+
+/**
* Capitalizes first character
*
* @param {String} text
@@ -98,3 +106,16 @@ export const convertToSentenceCase = string => {
return splitWord.join(' ');
};
+
+/**
+ * Splits camelCase or PascalCase words
+ * e.g. HelloWorld => Hello World
+ *
+ * @param {*} string
+*/
+export const splitCamelCase = string => (
+ string
+ .replace(/([A-Z]+)([A-Z][a-z])/g, ' $1 $2')
+ .replace(/([a-z\d])([A-Z])/g, '$1 $2')
+ .trim()
+);
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index dd17544b656..72b72f4247d 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -85,9 +85,9 @@ export function redirectTo(url) {
}
export function webIDEUrl(route = undefined) {
- let returnUrl = `${gon.relative_url_root}/-/ide/`;
+ let returnUrl = `${gon.relative_url_root || ''}/-/ide/`;
if (route) {
- returnUrl += `project${route}`;
+ returnUrl += `project${route.replace(new RegExp(`^${gon.relative_url_root || ''}`), '')}`;
}
return returnUrl;
}
diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js
index f2323f57455..291655235d5 100644
--- a/app/assets/javascripts/line_highlighter.js
+++ b/app/assets/javascripts/line_highlighter.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-underscore-dangle, no-param-reassign, prefer-template, quotes, comma-dangle, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, no-else-return, max-len */
+/* eslint-disable func-names, no-var, no-underscore-dangle, no-param-reassign, prefer-template, quotes, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, no-else-return, max-len */
import $ from 'jquery';
@@ -35,7 +35,7 @@ const LineHighlighter = function(options = {}) {
options.highlightLineClass = options.highlightLineClass || 'hll';
options.fileHolderSelector = options.fileHolderSelector || '.file-holder';
options.scrollFileHolder = options.scrollFileHolder || false;
- options.hash = options.hash || location.hash;
+ options.hash = options.hash || window.location.hash;
this.options = options;
this._hash = options.hash;
@@ -142,12 +142,14 @@ LineHighlighter.prototype.highlightLine = function(lineNumber) {
//
// range - Array containing the starting and ending line numbers
LineHighlighter.prototype.highlightRange = function(range) {
- var i, lineNumber, ref, ref1, results;
if (range[1]) {
- results = [];
- for (lineNumber = i = ref = range[0], ref1 = range[1]; ref <= ref1 ? i <= ref1 : i >= ref1; lineNumber = ref <= ref1 ? (i += 1) : (i -= 1)) {
+ const results = [];
+ const ref = range[0] <= range[1] ? range : range.reverse();
+
+ for (let lineNumber = range[0]; lineNumber <= ref[1]; lineNumber += 1) {
results.push(this.highlightLine(lineNumber));
}
+
return results;
} else {
return this.highlightLine(range[0]);
@@ -170,7 +172,7 @@ LineHighlighter.prototype.setHash = function(firstLineNumber, lastLineNumber) {
//
// This method is stubbed in tests.
LineHighlighter.prototype.__setLocationHash__ = function(value) {
- return history.pushState({
+ return window.history.pushState({
url: value
// We're using pushState instead of assigning location.hash directly to
// prevent the page from scrolling on the hashchange event
diff --git a/app/assets/javascripts/locale/index.js b/app/assets/javascripts/locale/index.js
index 2f4328b56e1..2cc5fb10027 100644
--- a/app/assets/javascripts/locale/index.js
+++ b/app/assets/javascripts/locale/index.js
@@ -9,7 +9,7 @@ delete window.translations;
Translates `text`
@param text The text to be translated
@returns {String} The translated text
-**/
+*/
const gettext = locale.gettext.bind(locale);
/**
@@ -21,7 +21,7 @@ const gettext = locale.gettext.bind(locale);
@param pluralText Plural text to translate (eg. '%d days')
@param count Number to decide which translation to use (eg. 2)
@returns {String} Translated text with the number replaced (eg. '2 days')
-**/
+*/
const ngettext = (text, pluralText, count) => {
const translated = locale.ngettext(text, pluralText, count).replace(/%d/g, count).split('|');
@@ -38,7 +38,7 @@ const ngettext = (text, pluralText, count) => {
(eg. 'Context')
@param key Is the dynamic variable you want to be translated
@returns {String} Translated context based text
-**/
+*/
const pgettext = (keyOrContext, key) => {
const normalizedKey = key ? `${keyOrContext}|${key}` : keyOrContext;
const translated = gettext(normalizedKey).split('|');
diff --git a/app/assets/javascripts/locale/sprintf.js b/app/assets/javascripts/locale/sprintf.js
index 5f4a053f98e..599104dcfa0 100644
--- a/app/assets/javascripts/locale/sprintf.js
+++ b/app/assets/javascripts/locale/sprintf.js
@@ -10,7 +10,7 @@ import _ from 'underscore';
@see https://ruby-doc.org/core-2.3.3/Kernel.html#method-i-sprintf
@see https://gitlab.com/gitlab-org/gitlab-ce/issues/37992
-**/
+*/
export default (input, parameters, escapeParameters = true) => {
let output = input;
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 247aeb481c6..2718f73a830 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -26,7 +26,7 @@ import './feature_highlight/feature_highlight_options';
import LazyLoader from './lazy_loader';
import initLogoAnimation from './logo';
import './milestone_select';
-import './projects_dropdown';
+import './frequent_items';
import initBreadcrumbs from './breadcrumb';
import initDispatcher from './dispatcher';
@@ -46,9 +46,9 @@ document.addEventListener('beforeunload', () => {
// Unbind scroll events
$(document).off('scroll');
// Close any open tooltips
- $('.has-tooltip, [data-toggle="tooltip"]').tooltip('destroy');
+ $('.has-tooltip, [data-toggle="tooltip"]').tooltip('dispose');
// Close any open popover
- $('[data-toggle="popover"]').popover('destroy');
+ $('[data-toggle="popover"]').popover('dispose');
});
window.addEventListener('hashchange', handleLocationHash);
@@ -111,7 +111,7 @@ document.addEventListener('DOMContentLoaded', () => {
$('.remove-row').on('ajax:success', function removeRowAjaxSuccessCallback() {
$(this)
- .tooltip('destroy')
+ .tooltip('dispose')
.closest('li')
.fadeOut();
});
@@ -141,9 +141,10 @@ document.addEventListener('DOMContentLoaded', () => {
});
// Initialize tooltips
- $.fn.tooltip.Constructor.DEFAULTS.trigger = 'hover';
$body.tooltip({
selector: '.has-tooltip, [data-toggle="tooltip"]',
+ trigger: 'hover',
+ boundary: 'viewport',
placement(tip, el) {
return $(el).data('placement') || 'bottom';
},
@@ -196,7 +197,7 @@ document.addEventListener('DOMContentLoaded', () => {
$container.remove();
});
- $('.navbar-toggle').on('click', () => {
+ $('.navbar-toggler').on('click', () => {
$('.header-content').toggleClass('menu-expanded');
gl.lazyLoader.loadCheck();
});
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 2cb238529aa..81950515ab4 100644
--- a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js
+++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js
@@ -1,4 +1,4 @@
-/* eslint-disable comma-dangle, quote-props, no-useless-computed-key, object-shorthand, no-new, no-param-reassign, max-len */
+/* eslint-disable comma-dangle, quote-props, no-useless-computed-key, object-shorthand, no-param-reassign, max-len */
/* global ace */
import Vue from 'vue';
@@ -11,9 +11,18 @@ import { __ } from '~/locale';
global.mergeConflicts.diffFileEditor = Vue.extend({
props: {
- file: Object,
- onCancelDiscardConfirmation: Function,
- onAcceptDiscardConfirmation: Function
+ file: {
+ type: Object,
+ required: true,
+ },
+ onCancelDiscardConfirmation: {
+ type: Function,
+ required: true,
+ },
+ onAcceptDiscardConfirmation: {
+ type: Function,
+ required: true,
+ },
},
data() {
return {
diff --git a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js
index 56d6678e1bd..827cf5f478d 100644
--- a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js
+++ b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js
@@ -1,14 +1,19 @@
-/* eslint-disable no-param-reassign, comma-dangle */
+/* eslint-disable no-param-reassign */
import Vue from 'vue';
+import actionsMixin from '../mixins/line_conflict_actions';
+import utilsMixin from '../mixins/line_conflict_utils';
-((global) => {
+(global => {
global.mergeConflicts = global.mergeConflicts || {};
global.mergeConflicts.inlineConflictLines = Vue.extend({
+ mixins: [utilsMixin, actionsMixin],
props: {
- file: Object
+ file: {
+ type: Object,
+ required: true,
+ },
},
- mixins: [global.mergeConflicts.utils, global.mergeConflicts.actions],
});
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js
index 0fc4a13450a..69208ac2d36 100644
--- a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js
+++ b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js
@@ -1,15 +1,20 @@
-/* eslint-disable no-param-reassign, comma-dangle */
+/* eslint-disable no-param-reassign */
import Vue from 'vue';
+import actionsMixin from '../mixins/line_conflict_actions';
+import utilsMixin from '../mixins/line_conflict_utils';
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
global.mergeConflicts.parallelConflictLines = Vue.extend({
+ mixins: [utilsMixin, actionsMixin],
props: {
- file: Object
+ file: {
+ type: Object,
+ required: true,
+ },
},
- mixins: [global.mergeConflicts.utils, global.mergeConflicts.actions],
template: `
<table>
<tr class="line_holder parallel" v-for="section in file.parallelLines">
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js
index c68b47c9348..64d69159222 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js
+++ b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js
@@ -1,23 +1,16 @@
-/* eslint-disable no-param-reassign, comma-dangle */
import axios from '../lib/utils/axios_utils';
-((global) => {
- global.mergeConflicts = global.mergeConflicts || {};
-
- class mergeConflictsService {
- constructor(options) {
- this.conflictsPath = options.conflictsPath;
- this.resolveConflictsPath = options.resolveConflictsPath;
- }
-
- fetchConflictsData() {
- return axios.get(this.conflictsPath);
- }
+export default class MergeConflictsService {
+ constructor(options) {
+ this.conflictsPath = options.conflictsPath;
+ this.resolveConflictsPath = options.resolveConflictsPath;
+ }
- submitResolveConflicts(data) {
- return axios.post(this.resolveConflictsPath, data);
- }
+ fetchConflictsData() {
+ return axios.get(this.conflictsPath);
}
- global.mergeConflicts.mergeConflictsService = mergeConflictsService;
-})(window.gl || (window.gl = {}));
+ submitResolveConflicts(data) {
+ return axios.post(this.resolveConflictsPath, data);
+ }
+}
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js
index 70f185e3656..1501296ac4f 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js
+++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js
@@ -156,7 +156,7 @@ import Cookies from 'js-cookie';
return 0;
}
- const files = this.state.conflictsData.files;
+ const { files } = this.state.conflictsData;
let count = 0;
files.forEach((file) => {
@@ -313,7 +313,7 @@ import Cookies from 'js-cookie';
},
isReadyToCommit() {
- const files = this.state.conflictsData.files;
+ const { files } = this.state.conflictsData;
const hasCommitMessage = $.trim(this.state.conflictsData.commitMessage).length;
let unresolved = 0;
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
index 4abd5433bb5..7badd68089c 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
+++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
@@ -1,13 +1,9 @@
-/* eslint-disable new-cap, comma-dangle, no-new */
-
import $ from 'jquery';
import Vue from 'vue';
-import Flash from '../flash';
+import createFlash from '../flash';
import initIssuableSidebar from '../init_issuable_sidebar';
import './merge_conflict_store';
-import './merge_conflict_service';
-import './mixins/line_conflict_utils';
-import './mixins/line_conflict_actions';
+import MergeConflictsService from './merge_conflict_service';
import './components/diff_file_editor';
import './components/inline_conflict_lines';
import './components/parallel_conflict_lines';
@@ -16,10 +12,10 @@ import syntaxHighlight from '../syntax_highlight';
export default function initMergeConflicts() {
const INTERACTIVE_RESOLVE_MODE = 'interactive';
const conflictsEl = document.querySelector('#conflicts');
- const mergeConflictsStore = gl.mergeConflicts.mergeConflictsStore;
- const mergeConflictsService = new gl.mergeConflicts.mergeConflictsService({
+ const { mergeConflictsStore } = gl.mergeConflicts;
+ const mergeConflictsService = new MergeConflictsService({
conflictsPath: conflictsEl.dataset.conflictsPath,
- resolveConflictsPath: conflictsEl.dataset.resolveConflictsPath
+ resolveConflictsPath: conflictsEl.dataset.resolveConflictsPath,
});
initIssuableSidebar();
@@ -29,17 +25,26 @@ export default function initMergeConflicts() {
components: {
'diff-file-editor': gl.mergeConflicts.diffFileEditor,
'inline-conflict-lines': gl.mergeConflicts.inlineConflictLines,
- 'parallel-conflict-lines': gl.mergeConflicts.parallelConflictLines
+ 'parallel-conflict-lines': gl.mergeConflicts.parallelConflictLines,
},
data: mergeConflictsStore.state,
computed: {
- conflictsCountText() { return mergeConflictsStore.getConflictsCountText(); },
- readyToCommit() { return mergeConflictsStore.isReadyToCommit(); },
- commitButtonText() { return mergeConflictsStore.getCommitButtonText(); },
- showDiffViewTypeSwitcher() { return mergeConflictsStore.fileTextTypePresent(); }
+ conflictsCountText() {
+ return mergeConflictsStore.getConflictsCountText();
+ },
+ readyToCommit() {
+ return mergeConflictsStore.isReadyToCommit();
+ },
+ commitButtonText() {
+ return mergeConflictsStore.getCommitButtonText();
+ },
+ showDiffViewTypeSwitcher() {
+ return mergeConflictsStore.fileTextTypePresent();
+ },
},
created() {
- mergeConflictsService.fetchConflictsData()
+ mergeConflictsService
+ .fetchConflictsData()
.then(({ data }) => {
if (data.type === 'error') {
mergeConflictsStore.setFailedRequest(data.message);
@@ -87,9 +92,9 @@ export default function initMergeConflicts() {
})
.catch(() => {
mergeConflictsStore.setSubmitState(false);
- new Flash('Failed to save merge conflicts resolutions. Please try again!');
+ createFlash('Failed to save merge conflicts resolutions. Please try again!');
});
- }
- }
+ },
+ },
});
}
diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js
index 53e000d7e9e..364ae2b2688 100644
--- a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js
+++ b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js
@@ -1,13 +1,7 @@
-/* eslint-disable no-param-reassign, comma-dangle */
-
-((global) => {
- global.mergeConflicts = global.mergeConflicts || {};
-
- global.mergeConflicts.actions = {
- methods: {
- handleSelected(file, sectionId, selection) {
- gl.mergeConflicts.mergeConflictsStore.handleSelected(file, sectionId, selection);
- }
- }
- };
-})(window.gl || (window.gl = {}));
+export default {
+ methods: {
+ handleSelected(file, sectionId, selection) {
+ gl.mergeConflicts.mergeConflictsStore.handleSelected(file, sectionId, selection);
+ },
+ },
+};
diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js
index 0f475f62ee6..d25032fb142 100644
--- a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js
+++ b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js
@@ -1,19 +1,13 @@
-/* eslint-disable no-param-reassign, quote-props, comma-dangle */
-
-((global) => {
- global.mergeConflicts = global.mergeConflicts || {};
-
- global.mergeConflicts.utils = {
- methods: {
- lineCssClass(line) {
- return {
- 'head': line.isHead,
- 'origin': line.isOrigin,
- 'match': line.hasMatch,
- 'selected': line.isSelected,
- 'unselected': line.isUnselected
- };
- }
- }
- };
-})(window.gl || (window.gl = {}));
+export default {
+ methods: {
+ lineCssClass(line) {
+ return {
+ head: line.isHead,
+ origin: line.isOrigin,
+ match: line.hasMatch,
+ selected: line.isSelected,
+ unselected: line.isUnselected,
+ };
+ },
+ },
+};
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index d8222ebec63..7bf2c56dd5d 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, no-underscore-dangle, one-var, one-var-declaration-per-line, consistent-return, dot-notation, quote-props, comma-dangle, object-shorthand, max-len, prefer-arrow-callback */
+/* eslint-disable func-names, no-var, wrap-iife, quotes, no-underscore-dangle, one-var, one-var-declaration-per-line, consistent-return, comma-dangle, max-len, prefer-arrow-callback */
import $ from 'jquery';
import { __ } from '~/locale';
@@ -49,6 +49,7 @@ MergeRequest.prototype.initTabs = function() {
if (window.mrTabs) {
window.mrTabs.unbindEvents();
}
+
window.mrTabs = new MergeRequestTabs(this.opts);
};
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 3f84f4b9499..53d7504de35 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -1,6 +1,7 @@
/* eslint-disable no-new, class-methods-use-this */
import $ from 'jquery';
+import Vue from 'vue';
import Cookies from 'js-cookie';
import axios from './lib/utils/axios_utils';
import flash from './flash';
@@ -8,12 +9,14 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
import initChangesDropdown from './init_changes_dropdown';
import bp from './breakpoints';
import { parseUrlPathname, handleLocationHash, isMetaClick } from './lib/utils/common_utils';
+import { isInVueNoteablePage } from './lib/utils/dom_utils';
import { getLocationHash } from './lib/utils/url_utility';
import initDiscussionTab from './image_diff/init_discussion_tab';
import Diff from './diff';
import { localTimeAgo } from './lib/utils/datetime_utility';
import syntaxHighlight from './syntax_highlight';
import Notes from './notes';
+import { polyfillSticky } from './lib/utils/sticky';
/* eslint-disable max-len */
// MergeRequestTabs
@@ -62,32 +65,45 @@ import Notes from './notes';
/* eslint-enable max-len */
// Store the `location` object, allowing for easier stubbing in tests
-let location = window.location;
+let { location } = window;
export default class MergeRequestTabs {
constructor({ action, setUrl, stubLocation } = {}) {
- const mergeRequestTabs = document.querySelector('.js-tabs-affix');
+ this.mergeRequestTabs = document.querySelector('.merge-request-tabs-container');
+ this.mergeRequestTabsAll =
+ this.mergeRequestTabs && this.mergeRequestTabs.querySelectorAll
+ ? this.mergeRequestTabs.querySelectorAll('.merge-request-tabs li')
+ : null;
+ this.mergeRequestTabPanes = document.querySelector('#diff-notes-app');
+ this.mergeRequestTabPanesAll =
+ this.mergeRequestTabPanes && this.mergeRequestTabPanes.querySelectorAll
+ ? this.mergeRequestTabPanes.querySelectorAll('.tab-pane')
+ : null;
const navbar = document.querySelector('.navbar-gitlab');
const peek = document.getElementById('js-peek');
const paddingTop = 16;
+ this.commitsTab = document.querySelector('.tab-content .commits.tab-pane');
+
+ this.currentTab = null;
this.diffsLoaded = false;
this.pipelinesLoaded = false;
this.commitsLoaded = false;
this.fixedLayoutPref = null;
+ this.eventHub = new Vue();
this.setUrl = setUrl !== undefined ? setUrl : true;
this.setCurrentAction = this.setCurrentAction.bind(this);
this.tabShown = this.tabShown.bind(this);
- this.showTab = this.showTab.bind(this);
+ this.clickTab = this.clickTab.bind(this);
this.stickyTop = navbar ? navbar.offsetHeight - paddingTop : 0;
if (peek) {
this.stickyTop += peek.offsetHeight;
}
- if (mergeRequestTabs) {
- this.stickyTop += mergeRequestTabs.offsetHeight;
+ if (this.mergeRequestTabs) {
+ this.stickyTop += this.mergeRequestTabs.offsetHeight;
}
if (stubLocation) {
@@ -95,25 +111,22 @@ export default class MergeRequestTabs {
}
this.bindEvents();
- this.activateTab(action);
+ if (
+ this.mergeRequestTabs &&
+ this.mergeRequestTabs.querySelector(`a[data-action='${action}']`) &&
+ this.mergeRequestTabs.querySelector(`a[data-action='${action}']`).click
+ )
+ this.mergeRequestTabs.querySelector(`a[data-action='${action}']`).click();
this.initAffix();
}
bindEvents() {
- $(document)
- .on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
- .on('click', '.js-show-tab', this.showTab);
-
- $('.merge-request-tabs a[data-toggle="tab"]').on('click', this.clickTab);
+ $('.merge-request-tabs a[data-toggle="tabvue"]').on('click', this.clickTab);
}
// Used in tests
unbindEvents() {
- $(document)
- .off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
- .off('click', '.js-show-tab', this.showTab);
-
- $('.merge-request-tabs a[data-toggle="tab"]').off('click', this.clickTab);
+ $('.merge-request-tabs a[data-toggle="tabvue"]').off('click', this.clickTab);
}
destroyPipelinesView() {
@@ -125,52 +138,86 @@ export default class MergeRequestTabs {
}
}
- showTab(e) {
- e.preventDefault();
- this.activateTab($(e.target).data('action'));
- }
-
clickTab(e) {
- if (e.currentTarget && isMetaClick(e)) {
- const targetLink = e.currentTarget.getAttribute('href');
+ if (e.currentTarget) {
e.stopImmediatePropagation();
e.preventDefault();
- window.open(targetLink, '_blank');
+
+ const { action } = e.currentTarget.dataset;
+
+ if (action) {
+ const href = e.currentTarget.getAttribute('href');
+ this.tabShown(action, href);
+ } else if (isMetaClick(e)) {
+ const targetLink = e.currentTarget.getAttribute('href');
+ window.open(targetLink, '_blank');
+ }
}
}
- tabShown(e) {
- const $target = $(e.target);
- const action = $target.data('action');
-
- if (action === 'commits') {
- this.loadCommits($target.attr('href'));
- this.expandView();
- this.resetViewContainer();
- this.destroyPipelinesView();
- } else if (this.isDiffAction(action)) {
- this.loadDiff($target.attr('href'));
- if (bp.getBreakpointSize() !== 'lg') {
- this.shrinkView();
+ tabShown(action, href) {
+ if (action !== this.currentTab && this.mergeRequestTabs) {
+ this.currentTab = action;
+
+ if (this.mergeRequestTabPanesAll) {
+ this.mergeRequestTabPanesAll.forEach(el => {
+ const tabPane = el;
+ tabPane.style.display = 'none';
+ });
}
- if (this.diffViewType() === 'parallel') {
- this.expandViewContainer();
+
+ if (this.mergeRequestTabsAll) {
+ this.mergeRequestTabsAll.forEach(el => {
+ el.classList.remove('active');
+ });
}
- this.destroyPipelinesView();
- } else if (action === 'pipelines') {
- this.resetViewContainer();
- this.mountPipelinesView();
- } else {
- if (bp.getBreakpointSize() !== 'xs') {
+
+ const tabPane = this.mergeRequestTabPanes.querySelector(`#${action}`);
+ if (tabPane) tabPane.style.display = 'block';
+ const tab = this.mergeRequestTabs.querySelector(`.${action}-tab`);
+ if (tab) tab.classList.add('active');
+
+ if (action === 'commits') {
+ this.loadCommits(href);
+ this.expandView();
+ this.resetViewContainer();
+ this.destroyPipelinesView();
+ } else if (action === 'new') {
this.expandView();
+ this.resetViewContainer();
+ this.destroyPipelinesView();
+ } else if (this.isDiffAction(action)) {
+ if (!isInVueNoteablePage()) {
+ this.loadDiff(href);
+ }
+ if (bp.getBreakpointSize() !== 'lg') {
+ this.shrinkView();
+ }
+ if (this.diffViewType() === 'parallel') {
+ this.expandViewContainer();
+ }
+ this.destroyPipelinesView();
+ this.commitsTab.classList.remove('active');
+ } else if (action === 'pipelines') {
+ this.resetViewContainer();
+ this.mountPipelinesView();
+ } else {
+ this.mergeRequestTabPanes.querySelector('#notes').style.display = 'block';
+ this.mergeRequestTabs.querySelector('.notes-tab').classList.add('active');
+
+ if (bp.getBreakpointSize() !== 'xs') {
+ this.expandView();
+ }
+ this.resetViewContainer();
+ this.destroyPipelinesView();
+
+ initDiscussionTab();
+ }
+ if (this.setUrl) {
+ this.setCurrentAction(action);
}
- this.resetViewContainer();
- this.destroyPipelinesView();
- initDiscussionTab();
- }
- if (this.setUrl) {
- this.setCurrentAction(action);
+ this.eventHub.$emit('MergeRequestTabChange', this.getCurrentAction());
}
}
@@ -184,12 +231,6 @@ export default class MergeRequestTabs {
}
}
- // Activate a tab based on the current action
- activateTab(action) {
- // important note: the .tab('show') method triggers 'shown.bs.tab' event itself
- $(`.merge-request-tabs a[data-action='${action}']`).tab('show');
- }
-
// Replaces the current Merge Request-specific action in the URL with a new one
//
// If the action is "notes", the URL is reset to the standard
@@ -270,7 +311,7 @@ export default class MergeRequestTabs {
mountPipelinesView() {
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
- const CommitPipelinesTable = gl.CommitPipelinesTable;
+ const { CommitPipelinesTable } = gl;
this.commitPipelinesTable = new CommitPipelinesTable({
propsData: {
endpoint: pipelineTableViewEl.dataset.endpoint,
@@ -362,7 +403,7 @@ export default class MergeRequestTabs {
//
// status - Boolean, true to show, false to hide
toggleLoading(status) {
- $('.mr-loading-status .loading').toggle(status);
+ $('.mr-loading-status .loading').toggleClass('hide', !status);
}
diffViewType() {
@@ -417,7 +458,6 @@ export default class MergeRequestTabs {
initAffix() {
const $tabs = $('.js-tabs-affix');
- const $fixedNav = $('.navbar-gitlab');
// Screen space on small screens is usually very sparse
// So we dont affix the tabs on these
@@ -427,24 +467,9 @@ export default class MergeRequestTabs {
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 ($tabs.css('position') !== 'static') return;
- const $diffTabs = $('#diff-notes-app');
-
- $tabs
- .off('affix.bs.affix affix-top.bs.affix')
- .affix({
- offset: {
- top: () => $diffTabs.offset().top - $tabs.height() - $fixedNav.height(),
- },
- })
- .on('affix.bs.affix', () => $diffTabs.css({ marginTop: $tabs.height() }))
- .on('affix-top.bs.affix', () => $diffTabs.css({ marginTop: '' }));
-
- // Fix bug when reloading the page already scrolling
- if ($tabs.hasClass('affix')) {
- $tabs.trigger('affix.bs.affix');
- }
+ polyfillSticky($tabs);
}
}
diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js
index 325fa570f37..6da04020881 100644
--- a/app/assets/javascripts/milestone.js
+++ b/app/assets/javascripts/milestone.js
@@ -18,13 +18,13 @@ export default class Milestone {
return $('a[data-toggle="tab"]').on('show.bs.tab', (e) => {
const $target = $(e.target);
- location.hash = $target.attr('href');
+ window.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}"]`);
+ const $target = $(`.js-milestone-tabs a[href="${window.location.hash}"]`);
if ($target.length) {
$target.tab('show');
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index f8b3d3061f0..640a4c8260f 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -1,10 +1,11 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, one-var-declaration-per-line, no-unused-vars, object-shorthand, comma-dangle, no-else-return, no-self-compare, consistent-return, no-param-reassign, no-shadow */
+/* eslint-disable max-len, one-var, one-var-declaration-per-line, no-unused-vars, object-shorthand, no-else-return, no-self-compare, consistent-return, no-param-reassign, no-shadow */
/* global Issuable */
/* global ListMilestone */
import $ from 'jquery';
import _ from 'underscore';
import { __ } from '~/locale';
+import '~/gl_dropdown';
import axios from './lib/utils/axios_utils';
import { timeFor } from './lib/utils/datetime_utility';
import ModalStore from './boards/stores/modal_store';
@@ -16,10 +17,10 @@ export default class MilestoneSelect {
typeof currentProject === 'string' ? JSON.parse(currentProject) : currentProject;
}
- this.init(els, options);
+ MilestoneSelect.init(els, options);
}
- init(els, options) {
+ static init(els, options) {
let $els = $(els);
if (!els) {
@@ -56,7 +57,7 @@ export default class MilestoneSelect {
if (issueUpdateURL) {
milestoneLinkTemplate = _.template(
- '<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>',
+ '<a href="<%- web_url %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>',
);
milestoneLinkNoneTemplate = '<span class="no-value">None</span>';
}
@@ -224,7 +225,6 @@ export default class MilestoneSelect {
$selectBox.hide();
$value.css('display', '');
if (data.milestone != null) {
- data.milestone.full_path = this.currentProject.full_path;
data.milestone.remaining = timeFor(data.milestone.due_date);
data.milestone.name = data.milestone.title;
$value.html(milestoneLinkTemplate(data.milestone));
@@ -252,3 +252,5 @@ export default class MilestoneSelect {
});
}
}
+
+window.MilestoneSelect = MilestoneSelect;
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index f5572be5fbf..17a6d5bcd2a 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -1,5 +1,7 @@
<script>
import _ from 'underscore';
+import { s__ } from '~/locale';
+import Icon from '~/vue_shared/components/icon.vue';
import Flash from '../../flash';
import MonitoringService from '../services/monitoring_service';
import GraphGroup from './graph_group.vue';
@@ -13,6 +15,7 @@ export default {
Graph,
GraphGroup,
EmptyState,
+ Icon,
},
props: {
hasMetrics: {
@@ -80,6 +83,14 @@ export default {
type: String,
required: true,
},
+ environmentsEndpoint: {
+ type: String,
+ required: true,
+ },
+ currentEnvironmentName: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
@@ -96,6 +107,7 @@ export default {
this.service = new MonitoringService({
metricsEndpoint: this.metricsEndpoint,
deploymentEndpoint: this.deploymentEndpoint,
+ environmentsEndpoint: this.environmentsEndpoint,
});
eventHub.$on('toggleAspectRatio', this.toggleAspectRatio);
eventHub.$on('hoverChanged', this.hoverChanged);
@@ -122,7 +134,11 @@ export default {
this.service
.getDeploymentData()
.then(data => this.store.storeDeploymentData(data))
- .catch(() => new Flash('Error getting deployment information.')),
+ .catch(() => Flash(s__('Metrics|There was an error getting deployment information.'))),
+ this.service
+ .getEnvironmentsData()
+ .then((data) => this.store.storeEnvironmentsData(data))
+ .catch(() => Flash(s__('Metrics|There was an error getting environments information.'))),
])
.then(() => {
if (this.store.groups.length < 1) {
@@ -139,7 +155,7 @@ export default {
this.updateAspectRatio = true;
},
toggleAspectRatio() {
- this.updatedAspectRatios = this.updatedAspectRatios += 1;
+ this.updatedAspectRatios += 1;
if (this.store.getMetricsCount() === this.updatedAspectRatios) {
this.updateAspectRatio = !this.updateAspectRatio;
this.updatedAspectRatios = 0;
@@ -155,8 +171,41 @@ export default {
<template>
<div
v-if="!showEmptyState"
- class="prometheus-graphs"
+ class="prometheus-graphs prepend-top-10"
>
+ <div class="environments d-flex align-items-center">
+ {{ s__('Metrics|Environment') }}
+ <div class="dropdown prepend-left-10">
+ <button
+ class="dropdown-menu-toggle"
+ data-toggle="dropdown"
+ type="button"
+ >
+ <span>
+ {{ currentEnvironmentName }}
+ </span>
+ <icon
+ name="chevron-down"
+ />
+ </button>
+ <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up">
+ <ul>
+ <li
+ v-for="environment in store.environmentsData"
+ :key="environment.latest.id"
+ >
+ <a
+ :href="environment.latest.metrics_path"
+ :class="{ 'is-active': environment.latest.name == currentEnvironmentName }"
+ class="dropdown-item"
+ >
+ {{ environment.latest.name }}
+ </a>
+ </li>
+ </ul>
+ </div>
+ </div>
+ </div>
<graph-group
v-for="(groupData, index) in store.groups"
:key="index"
@@ -174,7 +223,10 @@ export default {
:tags-path="tagsPath"
:show-legend="showLegend"
:small-graph="forceSmallGraph"
- />
+ >
+ <!-- EE content -->
+ {{ null }}
+ </graph>
</graph-group>
</div>
<empty-state
diff --git a/app/assets/javascripts/monitoring/components/empty_state.vue b/app/assets/javascripts/monitoring/components/empty_state.vue
index c77f451c2d3..82b9a4b1adb 100644
--- a/app/assets/javascripts/monitoring/components/empty_state.vue
+++ b/app/assets/javascripts/monitoring/components/empty_state.vue
@@ -107,8 +107,8 @@ export default {
<div class="state-button">
<a
v-if="currentState.buttonPath"
- class="btn btn-success"
:href="currentState.buttonPath"
+ class="btn btn-success"
>
{{ currentState.buttonText }}
</a>
@@ -116,8 +116,8 @@ export default {
<div class="state-button">
<a
v-if="currentState.secondaryButtonPath"
- class="btn"
:href="currentState.secondaryButtonPath"
+ class="btn"
>
{{ currentState.secondaryButtonText }}
</a>
diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue
index de6755e0414..e5680a0499f 100644
--- a/app/assets/javascripts/monitoring/components/graph.vue
+++ b/app/assets/javascripts/monitoring/components/graph.vue
@@ -154,7 +154,7 @@ export default {
point.x = e.clientX;
point.y = e.clientY;
point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse());
- point.x = point.x += 7;
+ point.x += 7;
const firstTimeSeries = this.timeSeries[0];
const timeValueOverlay = firstTimeSeries.timeSeriesScaleX.invert(point.x);
const overlayIndex = bisectDate(firstTimeSeries.values, timeValueOverlay, 1);
@@ -232,20 +232,25 @@ export default {
@mouseover="showFlagContent = true"
@mouseleave="showFlagContent = false"
>
- <h5 class="text-center graph-title">
- {{ graphData.title }}
- </h5>
+ <div class="prometheus-graph-header">
+ <h5 class="prometheus-graph-title">
+ {{ graphData.title }}
+ </h5>
+ <div class="prometheus-graph-widgets">
+ <slot></slot>
+ </div>
+ </div>
<div
- class="prometheus-svg-container"
:style="paddingBottomRootSvg"
+ class="prometheus-svg-container"
>
<svg
- :viewBox="outerViewBox"
ref="baseSvg"
+ :viewBox="outerViewBox"
>
<g
- class="x-axis"
:transform="axisTransform"
+ class="x-axis"
/>
<g
class="y-axis"
@@ -260,9 +265,9 @@ export default {
:unit-of-display="unitOfDisplay"
/>
<svg
- class="graph-data"
- :viewBox="innerViewBox"
ref="graphData"
+ :viewBox="innerViewBox"
+ class="graph-data"
>
<graph-path
v-for="(path, index) in timeSeries"
@@ -282,11 +287,11 @@ export default {
:graph-height-offset="graphHeightOffset"
/>
<rect
- class="prometheus-graph-overlay"
+ ref="graphOverlay"
:width="(graphWidth - 70)"
:height="(graphHeight - 100)"
+ class="prometheus-graph-overlay"
transform="translate(-5, 20)"
- ref="graphOverlay"
@mousemove="handleMouseOverGraph($event)"
/>
</svg>
diff --git a/app/assets/javascripts/monitoring/components/graph/axis.vue b/app/assets/javascripts/monitoring/components/graph/axis.vue
index fc4b3689dfd..8a604a51eb2 100644
--- a/app/assets/javascripts/monitoring/components/graph/axis.vue
+++ b/app/assets/javascripts/monitoring/components/graph/axis.vue
@@ -92,48 +92,48 @@ export default {
<template>
<g class="axis-label-container">
<line
+ :y1="yPosition"
+ :x2="graphWidth + 20"
+ :y2="yPosition"
class="label-x-axis-line"
stroke="#000000"
stroke-width="1"
x1="10"
- :y1="yPosition"
- :x2="graphWidth + 20"
- :y2="yPosition"
/>
<line
+ :x2="10"
+ :y2="yPosition"
class="label-y-axis-line"
stroke="#000000"
stroke-width="1"
x1="10"
y1="0"
- :x2="10"
- :y2="yPosition"
/>
<rect
- class="rect-axis-text"
:transform="rectTransform"
:width="yLabelWidth"
:height="yLabelHeight"
+ class="rect-axis-text"
/>
<text
+ ref="ylabel"
+ :transform="textTransform"
class="label-axis-text y-label-text"
text-anchor="middle"
- :transform="textTransform"
- ref="ylabel"
>
{{ yAxisLabelSentenceCase }}
</text>
<rect
- class="rect-axis-text"
:x="xPosition + 60"
:y="graphHeight - 80"
+ class="rect-axis-text"
width="35"
height="50"
/>
<text
- class="label-axis-text x-label-text"
:x="xPosition + 60"
:y="yPosition"
+ class="label-axis-text x-label-text"
dy=".35em"
>
{{ timeString }}
diff --git a/app/assets/javascripts/monitoring/components/graph/deployment.vue b/app/assets/javascripts/monitoring/components/graph/deployment.vue
index 4012191ceb9..a7289ed53e8 100644
--- a/app/assets/javascripts/monitoring/components/graph/deployment.vue
+++ b/app/assets/javascripts/monitoring/components/graph/deployment.vue
@@ -33,18 +33,18 @@ export default {
:key="index"
:transform="transformDeploymentGroup(deployment)">
<rect
+ :height="calculatedHeight"
x="0"
y="0"
- :height="calculatedHeight"
width="3"
fill="url(#shadow-gradient)"
/>
<line
+ :y2="calculatedHeight"
class="deployment-line"
x1="0"
y1="0"
x2="0"
- :y2="calculatedHeight"
stroke="#000"
/>
</g>
diff --git a/app/assets/javascripts/monitoring/components/graph/flag.vue b/app/assets/javascripts/monitoring/components/graph/flag.vue
index 8a771107de8..92fe98508ad 100644
--- a/app/assets/javascripts/monitoring/components/graph/flag.vue
+++ b/app/assets/javascripts/monitoring/components/graph/flag.vue
@@ -97,7 +97,7 @@ export default {
? this.deploymentFlagData.seriesIndex
: indexFromCoordinates;
const value = series.values[index] && series.values[index].value;
- if (isNaN(value)) {
+ if (Number.isNaN(value)) {
return '-';
}
return `${formatRelevantDigits(value)}${this.unitOfDisplay}`;
@@ -117,13 +117,13 @@ export default {
<template>
<div
- class="prometheus-graph-cursor"
:style="cursorStyle"
+ class="prometheus-graph-cursor"
>
<div
v-if="showFlagContent"
- class="prometheus-graph-flag popover"
:class="flagOrientation"
+ class="prometheus-graph-flag popover"
>
<div class="arrow"></div>
<div class="popover-title">
@@ -139,8 +139,8 @@ export default {
>
<div>
<icon
- name="commit"
:size="12"
+ name="commit"
/>
<a :href="deploymentFlagData.commitUrl">
{{ deploymentFlagData.sha.slice(0, 8) }}
@@ -150,8 +150,8 @@ export default {
v-if="deploymentFlagData.tag"
>
<icon
- name="label"
:size="12"
+ name="label"
/>
<a :href="deploymentFlagData.tagUrl">
{{ deploymentFlagData.ref }}
diff --git a/app/assets/javascripts/monitoring/components/graph/legend.vue b/app/assets/javascripts/monitoring/components/graph/legend.vue
index da9280cf1f1..3276f3a1ceb 100644
--- a/app/assets/javascripts/monitoring/components/graph/legend.vue
+++ b/app/assets/javascripts/monitoring/components/graph/legend.vue
@@ -31,8 +31,8 @@ export default {
<table class="prometheus-table">
<tr
v-for="(series, index) in timeSeries"
- :key="index"
v-if="series.shouldRenderLegend"
+ :key="index"
:class="isStable(series)"
>
<td>
@@ -40,11 +40,11 @@ export default {
</td>
<track-line :track="series" />
<td
- class="legend-metric-title"
- v-if="timeSeries.length > 1">
+ v-if="timeSeries.length > 1"
+ class="legend-metric-title">
<track-info
- :track="series"
- v-if="series.metricTag" />
+ v-if="series.metricTag"
+ :track="series" />
<track-info
v-else
:track="series">
@@ -62,8 +62,8 @@ export default {
:key="`track-line-${trackIndex}`"/>
<td :key="`track-info-${trackIndex}`">
<track-info
- class="legend-metric-title"
- :track="track" />
+ :track="track"
+ class="legend-metric-title" />
</td>
</template>
</tr>
diff --git a/app/assets/javascripts/monitoring/components/graph/path.vue b/app/assets/javascripts/monitoring/components/graph/path.vue
index 52f8aa2ee3f..a9b7ce586ce 100644
--- a/app/assets/javascripts/monitoring/components/graph/path.vue
+++ b/app/assets/javascripts/monitoring/components/graph/path.vue
@@ -44,26 +44,26 @@ export default {
<template>
<g transform="translate(-5, 20)">
<circle
- class="circle-path"
+ v-if="showDot"
:cx="currentCoordinates.currentX"
:cy="currentCoordinates.currentY"
:fill="lineColor"
:stroke="lineColor"
+ class="circle-path"
r="3"
- v-if="showDot"
/>
<path
- class="metric-area"
:d="generatedAreaPath"
:fill="areaColor"
+ class="metric-area"
/>
<path
- class="metric-line"
:d="generatedLinePath"
:stroke="lineColor"
+ :stroke-dasharray="strokeDashArray"
+ class="metric-line"
fill="none"
stroke-width="1"
- :stroke-dasharray="strokeDashArray"
/>
</g>
</template>
diff --git a/app/assets/javascripts/monitoring/components/graph/track_line.vue b/app/assets/javascripts/monitoring/components/graph/track_line.vue
index 18be65fd1ef..ba3f93b39ff 100644
--- a/app/assets/javascripts/monitoring/components/graph/track_line.vue
+++ b/app/assets/javascripts/monitoring/components/graph/track_line.vue
@@ -24,11 +24,11 @@ export default {
<line
:stroke-dasharray="stylizedLine"
:stroke="track.lineColor"
- stroke-width="4"
:x1="0"
:x2="16"
:y1="4"
:y2="4"
+ stroke-width="4"
/>
</svg>
</td>
diff --git a/app/assets/javascripts/monitoring/components/graph_group.vue b/app/assets/javascripts/monitoring/components/graph_group.vue
index a6dbe42a8f0..241627f9790 100644
--- a/app/assets/javascripts/monitoring/components/graph_group.vue
+++ b/app/assets/javascripts/monitoring/components/graph_group.vue
@@ -17,12 +17,12 @@ export default {
<template>
<div
v-if="showPanels"
- class="panel panel-default prometheus-panel"
+ class="card prometheus-panel"
>
- <div class="panel-heading">
+ <div class="card-header">
<h4>{{ name }}</h4>
</div>
- <div class="panel-body prometheus-graph-group">
+ <div class="card-body prometheus-graph-group">
<slot></slot>
</div>
</div>
diff --git a/app/assets/javascripts/monitoring/services/monitoring_service.js b/app/assets/javascripts/monitoring/services/monitoring_service.js
index 6fcca36d2fa..260d424378e 100644
--- a/app/assets/javascripts/monitoring/services/monitoring_service.js
+++ b/app/assets/javascripts/monitoring/services/monitoring_service.js
@@ -1,6 +1,7 @@
import axios from '../../lib/utils/axios_utils';
import statusCodes from '../../lib/utils/http_status';
import { backOff } from '../../lib/utils/common_utils';
+import { s__ } from '../../locale';
const MAX_REQUESTS = 3;
@@ -23,9 +24,10 @@ function backOffRequest(makeRequestCallback) {
}
export default class MonitoringService {
- constructor({ metricsEndpoint, deploymentEndpoint }) {
+ constructor({ metricsEndpoint, deploymentEndpoint, environmentsEndpoint }) {
this.metricsEndpoint = metricsEndpoint;
this.deploymentEndpoint = deploymentEndpoint;
+ this.environmentsEndpoint = environmentsEndpoint;
}
getGraphsData() {
@@ -33,7 +35,7 @@ export default class MonitoringService {
.then(resp => resp.data)
.then((response) => {
if (!response || !response.data) {
- throw new Error('Unexpected metrics data response from prometheus endpoint');
+ throw new Error(s__('Metrics|Unexpected metrics data response from prometheus endpoint'));
}
return response.data;
});
@@ -47,9 +49,20 @@ export default class MonitoringService {
.then(resp => resp.data)
.then((response) => {
if (!response || !response.deployments) {
- throw new Error('Unexpected deployment data response from prometheus endpoint');
+ throw new Error(s__('Metrics|Unexpected deployment data response from prometheus endpoint'));
}
return response.deployments;
});
}
+
+ getEnvironmentsData() {
+ return axios.get(this.environmentsEndpoint)
+ .then(resp => resp.data)
+ .then((response) => {
+ if (!response || !response.environments) {
+ throw new Error(s__('Metrics|There was an error fetching the environments data, please try again'));
+ }
+ return response.environments;
+ });
+ }
}
diff --git a/app/assets/javascripts/monitoring/stores/monitoring_store.js b/app/assets/javascripts/monitoring/stores/monitoring_store.js
index 535c415cd6d..748b8cb6e6e 100644
--- a/app/assets/javascripts/monitoring/stores/monitoring_store.js
+++ b/app/assets/javascripts/monitoring/stores/monitoring_store.js
@@ -24,6 +24,7 @@ export default class MonitoringStore {
constructor() {
this.groups = [];
this.deploymentData = [];
+ this.environmentsData = [];
}
storeMetrics(groups = []) {
@@ -37,6 +38,10 @@ export default class MonitoringStore {
this.deploymentData = deploymentData;
}
+ storeEnvironmentsData(environmentsData = []) {
+ this.environmentsData = environmentsData;
+ }
+
getMetricsCount() {
return this.groups.reduce((count, group) => count + group.metrics.length, 0);
}
diff --git a/app/assets/javascripts/monitoring/utils/multiple_time_series.js b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
index 4d3f1f1a7cc..cee39fd0559 100644
--- a/app/assets/javascripts/monitoring/utils/multiple_time_series.js
+++ b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
@@ -41,10 +41,10 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
} else {
const unusedColors = _.difference(defaultColorOrder, usedColors);
if (unusedColors.length > 0) {
- pick = unusedColors[0];
+ [pick] = unusedColors;
} else {
usedColors = [];
- pick = defaultColorOrder[0];
+ [pick] = defaultColorOrder;
}
}
usedColors.push(pick);
@@ -73,7 +73,7 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
timeSeriesScaleX.ticks(d3.timeMinute, 60);
timeSeriesScaleY.domain(yDom);
- const defined = d => !isNaN(d.value) && d.value != null;
+ const defined = d => !Number.isNaN(d.value) && d.value != null;
const lineFunction = d3
.line()
diff --git a/app/assets/javascripts/mr_notes/index.js b/app/assets/javascripts/mr_notes/index.js
index e3c5bf06b3d..8aabb840847 100644
--- a/app/assets/javascripts/mr_notes/index.js
+++ b/app/assets/javascripts/mr_notes/index.js
@@ -1,20 +1,32 @@
+import $ from 'jquery';
import Vue from 'vue';
+import { mapActions, mapState, mapGetters } from 'vuex';
+import initDiffsApp from '../diffs';
import notesApp from '../notes/components/notes_app.vue';
import discussionCounter from '../notes/components/discussion_counter.vue';
-import store from '../notes/stores';
+import store from './stores';
+import MergeRequest from '../merge_request';
export default function initMrNotes() {
+ const mrShowNode = document.querySelector('.merge-request');
+ // eslint-disable-next-line no-new
+ new MergeRequest({
+ action: mrShowNode.dataset.mrAction,
+ });
+
// eslint-disable-next-line no-new
new Vue({
el: '#js-vue-mr-discussions',
+ name: 'MergeRequestDiscussions',
components: {
notesApp,
},
+ store,
data() {
- const notesDataset = document.getElementById('js-vue-mr-discussions')
- .dataset;
+ const notesDataset = document.getElementById('js-vue-mr-discussions').dataset;
const noteableData = JSON.parse(notesDataset.noteableData);
noteableData.noteableType = notesDataset.noteableType;
+ noteableData.targetType = notesDataset.targetType;
return {
noteableData,
@@ -22,12 +34,42 @@ export default function initMrNotes() {
notesData: JSON.parse(notesDataset.notesData),
};
},
+ computed: {
+ ...mapGetters(['discussionTabCounter']),
+ ...mapState({
+ activeTab: state => state.page.activeTab,
+ }),
+ },
+ watch: {
+ discussionTabCounter() {
+ this.updateDiscussionTabCounter();
+ },
+ },
+ created() {
+ this.setActiveTab(window.mrTabs.getCurrentAction());
+ },
+ mounted() {
+ this.notesCountBadge = $('.issuable-details').find('.notes-tab .badge');
+ $(document).on('visibilitychange', this.updateDiscussionTabCounter);
+ window.mrTabs.eventHub.$on('MergeRequestTabChange', this.setActiveTab);
+ },
+ beforeDestroy() {
+ $(document).off('visibilitychange', this.updateDiscussionTabCounter);
+ window.mrTabs.eventHub.$off('MergeRequestTabChange', this.setActiveTab);
+ },
+ methods: {
+ ...mapActions(['setActiveTab']),
+ updateDiscussionTabCounter() {
+ this.notesCountBadge.text(this.discussionTabCounter);
+ },
+ },
render(createElement) {
return createElement('notes-app', {
props: {
noteableData: this.noteableData,
notesData: this.notesData,
userData: this.currentUserData,
+ shouldShow: this.activeTab === 'show',
},
});
},
@@ -36,6 +78,7 @@ export default function initMrNotes() {
// eslint-disable-next-line no-new
new Vue({
el: '#js-vue-discussion-counter',
+ name: 'DiscussionCounter',
components: {
discussionCounter,
},
@@ -44,4 +87,6 @@ export default function initMrNotes() {
return createElement('discussion-counter');
},
});
+
+ initDiffsApp(store);
}
diff --git a/app/assets/javascripts/mr_notes/stores/actions.js b/app/assets/javascripts/mr_notes/stores/actions.js
new file mode 100644
index 00000000000..426c6a00d5e
--- /dev/null
+++ b/app/assets/javascripts/mr_notes/stores/actions.js
@@ -0,0 +1,7 @@
+import types from './mutation_types';
+
+export default {
+ setActiveTab({ commit }, tab) {
+ commit(types.SET_ACTIVE_TAB, tab);
+ },
+};
diff --git a/app/assets/javascripts/mr_notes/stores/getters.js b/app/assets/javascripts/mr_notes/stores/getters.js
new file mode 100644
index 00000000000..b10e9f9f9f1
--- /dev/null
+++ b/app/assets/javascripts/mr_notes/stores/getters.js
@@ -0,0 +1,5 @@
+export default {
+ isLoggedIn(state, getters) {
+ return !!getters.getUserData.id;
+ },
+};
diff --git a/app/assets/javascripts/mr_notes/stores/index.js b/app/assets/javascripts/mr_notes/stores/index.js
new file mode 100644
index 00000000000..dd2019001db
--- /dev/null
+++ b/app/assets/javascripts/mr_notes/stores/index.js
@@ -0,0 +1,15 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import notesModule from '~/notes/stores/modules';
+import diffsModule from '~/diffs/store/modules';
+import mrPageModule from './modules';
+
+Vue.use(Vuex);
+
+export default new Vuex.Store({
+ modules: {
+ page: mrPageModule,
+ notes: notesModule,
+ diffs: diffsModule,
+ },
+});
diff --git a/app/assets/javascripts/mr_notes/stores/modules/index.js b/app/assets/javascripts/mr_notes/stores/modules/index.js
new file mode 100644
index 00000000000..660081f76c8
--- /dev/null
+++ b/app/assets/javascripts/mr_notes/stores/modules/index.js
@@ -0,0 +1,12 @@
+import actions from '../actions';
+import getters from '../getters';
+import mutations from '../mutations';
+
+export default {
+ state: {
+ activeTab: null,
+ },
+ actions,
+ getters,
+ mutations,
+};
diff --git a/app/assets/javascripts/mr_notes/stores/mutation_types.js b/app/assets/javascripts/mr_notes/stores/mutation_types.js
new file mode 100644
index 00000000000..105104361cf
--- /dev/null
+++ b/app/assets/javascripts/mr_notes/stores/mutation_types.js
@@ -0,0 +1,3 @@
+export default {
+ SET_ACTIVE_TAB: 'SET_ACTIVE_TAB',
+};
diff --git a/app/assets/javascripts/mr_notes/stores/mutations.js b/app/assets/javascripts/mr_notes/stores/mutations.js
new file mode 100644
index 00000000000..8175aa9488f
--- /dev/null
+++ b/app/assets/javascripts/mr_notes/stores/mutations.js
@@ -0,0 +1,7 @@
+import types from './mutation_types';
+
+export default {
+ [types.SET_ACTIVE_TAB](state, tab) {
+ Object.assign(state, { activeTab: tab });
+ },
+};
diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js
index c7a8aac79df..17370edeb0c 100644
--- a/app/assets/javascripts/namespace_select.js
+++ b/app/assets/javascripts/namespace_select.js
@@ -1,4 +1,4 @@
-/* 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 */
+/* eslint-disable func-names, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, prefer-arrow-callback, max-len */
import $ from 'jquery';
import Api from './api';
diff --git a/app/assets/javascripts/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js
index bd007c707f2..94da1be4066 100644
--- a/app/assets/javascripts/network/branch_graph.js
+++ b/app/assets/javascripts/network/branch_graph.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, quotes, comma-dangle, one-var, one-var-declaration-per-line, no-mixed-operators, no-loop-func, no-floating-decimal, consistent-return, no-unused-vars, prefer-template, prefer-arrow-callback, camelcase, max-len */
+/* eslint-disable func-names, no-var, wrap-iife, quotes, comma-dangle, one-var, one-var-declaration-per-line, no-loop-func, no-floating-decimal, consistent-return, no-unused-vars, prefer-template, prefer-arrow-callback, camelcase, max-len */
import $ from 'jquery';
import { __ } from '../locale';
@@ -101,8 +101,8 @@ export default (function() {
};
BranchGraph.prototype.buildGraph = function() {
- var cuday, cumonth, day, j, len, mm, r, ref;
- r = this.r;
+ var cuday, cumonth, day, j, len, mm, ref;
+ const { r } = this;
cuday = 0;
cumonth = "";
r.rect(0, 0, 40, this.barHeight).attr({
@@ -112,7 +112,8 @@ export default (function() {
fill: "#444"
});
ref = this.days;
- for (mm = j = 0, len = ref.length; j < len; mm = (j += 1)) {
+
+ for (mm = 0, len = ref.length; mm < len; mm += 1) {
day = ref[mm];
if (cuday !== day[0] || cumonth !== day[1]) {
// Dates
@@ -120,7 +121,7 @@ export default (function() {
font: "12px Monaco, monospace",
fill: "#BBB"
});
- cuday = day[0];
+ [cuday] = day;
}
if (cumonth !== day[1]) {
// Months
@@ -128,6 +129,8 @@ export default (function() {
font: "12px Monaco, monospace",
fill: "#EEE"
});
+
+ // eslint-disable-next-line prefer-destructuring
cumonth = day[1];
}
}
@@ -168,8 +171,8 @@ export default (function() {
};
BranchGraph.prototype.bindEvents = function() {
- var element;
- element = this.element;
+ const { element } = this;
+
return $(element).scroll((function(_this) {
return function(event) {
return _this.renderPartialGraph();
@@ -206,11 +209,13 @@ export default (function() {
};
BranchGraph.prototype.appendLabel = function(x, y, commit) {
- var label, r, rect, shortrefs, text, textbox, triangle;
+ var label, rect, shortrefs, text, textbox, triangle;
+
if (!commit.refs) {
return;
}
- r = this.r;
+
+ const { r } = this;
shortrefs = commit.refs;
// Truncate if longer than 15 chars
if (shortrefs.length > 17) {
@@ -241,11 +246,8 @@ export default (function() {
};
BranchGraph.prototype.appendAnchor = function(x, y, commit) {
- var anchor, options, r, top;
- r = this.r;
- top = this.top;
- options = this.options;
- anchor = r.circle(x, y, 10).attr({
+ const { r, top, options } = this;
+ const anchor = r.circle(x, y, 10).attr({
fill: "#000",
opacity: 0,
cursor: "pointer"
@@ -261,14 +263,15 @@ export default (function() {
};
BranchGraph.prototype.drawDot = function(x, y, commit) {
- var avatar_box_x, avatar_box_y, r;
- r = this.r;
+ const { r } = this;
r.circle(x, y, 3).attr({
fill: this.colors[commit.space],
stroke: "none"
});
- avatar_box_x = this.offsetX + this.unitSpace * this.mspace + 10;
- avatar_box_y = y - 10;
+
+ const avatar_box_x = this.offsetX + this.unitSpace * this.mspace + 10;
+ const avatar_box_y = y - 10;
+
r.rect(avatar_box_x, avatar_box_y, 20, 20).attr({
stroke: this.colors[commit.space],
"stroke-width": 2
@@ -281,11 +284,12 @@ export default (function() {
};
BranchGraph.prototype.drawLines = function(x, y, commit) {
- var arrow, color, i, j, len, offset, parent, parentCommit, parentX1, parentX2, parentY, r, ref, results, route;
- r = this.r;
- ref = commit.parents;
- results = [];
- for (i = j = 0, len = ref.length; j < len; i = (j += 1)) {
+ var arrow, color, i, len, offset, parent, parentCommit, parentX1, parentX2, parentY, route;
+ const { r } = this;
+ const ref = commit.parents;
+ const results = [];
+
+ for (i = 0, len = ref.length; i < len; i += 1) {
parent = ref[i];
parentCommit = this.preparedCommits[parent[0]];
parentY = this.offsetY + this.unitTime * parentCommit.time;
@@ -329,11 +333,10 @@ export default (function() {
};
BranchGraph.prototype.markCommit = function(commit) {
- var r, x, y;
if (commit.id === this.options.commit_id) {
- r = this.r;
- x = this.offsetX + this.unitSpace * (this.mspace - commit.space);
- y = this.offsetY + this.unitTime * commit.time;
+ const { r } = this;
+ const x = this.offsetX + this.unitSpace * (this.mspace - commit.space);
+ const y = this.offsetY + this.unitTime * commit.time;
r.path(["M", x + 5, y, "L", x + 15, y + 4, "L", x + 15, y - 4, "Z"]).attr({
fill: "#000",
"fill-opacity": .5,
diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js
index 40c08ee0ace..205d9766656 100644
--- a/app/assets/javascripts/new_branch_form.js
+++ b/app/assets/javascripts/new_branch_form.js
@@ -1,4 +1,4 @@
-/* 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 */
+/* eslint-disable func-names, no-var, one-var, max-len, 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 */
import $ from 'jquery';
import RefSelectDropdown from './ref_select_dropdown';
@@ -52,7 +52,7 @@ export default class NewBranchForm {
validate() {
var errorMessage, errors, formatter, unique, validator;
- const indexOf = [].indexOf;
+ const { indexOf } = [];
this.branchNameError.empty();
unique = function(values, value) {
diff --git a/app/assets/javascripts/new_commit_form.js b/app/assets/javascripts/new_commit_form.js
index a2f0a44863f..17ec20f1cc1 100644
--- a/app/assets/javascripts/new_commit_form.js
+++ b/app/assets/javascripts/new_commit_form.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-return-assign, max-len */
+/* eslint-disable no-var, no-return-assign */
export default class NewCommitForm {
constructor(form) {
this.form = form;
diff --git a/app/assets/javascripts/notebook/cells/code.vue b/app/assets/javascripts/notebook/cells/code.vue
index b4067d229aa..18cef82cec0 100644
--- a/app/assets/javascripts/notebook/cells/code.vue
+++ b/app/assets/javascripts/notebook/cells/code.vue
@@ -39,10 +39,10 @@ export default {
<template>
<div class="cell">
<code-cell
- type="input"
:raw-code="rawInputCode"
:count="cell.execution_count"
- :code-css-class="codeCssClass" />
+ :code-css-class="codeCssClass"
+ type="input" />
<output-cell
v-if="hasOutput"
:count="cell.execution_count"
diff --git a/app/assets/javascripts/notebook/cells/code/index.vue b/app/assets/javascripts/notebook/cells/code/index.vue
index 0f3083f05b2..7d2a1a33b98 100644
--- a/app/assets/javascripts/notebook/cells/code/index.vue
+++ b/app/assets/javascripts/notebook/cells/code/index.vue
@@ -48,9 +48,9 @@
:type="promptType"
:count="count" />
<pre
- class="language-python"
- :class="codeCssClass"
ref="code"
+ :class="codeCssClass"
+ class="language-python"
v-text="code">
</pre>
</div>
diff --git a/app/assets/javascripts/notebook/cells/output/index.vue b/app/assets/javascripts/notebook/cells/output/index.vue
index 91b2269a83a..4183b976814 100644
--- a/app/assets/javascripts/notebook/cells/output/index.vue
+++ b/app/assets/javascripts/notebook/cells/output/index.vue
@@ -78,10 +78,10 @@
<template>
<component
:is="componentName"
- type="output"
:output-type="outputType"
:count="count"
:raw-code="rawCode"
:code-css-class="codeCssClass"
+ type="output"
/>
</template>
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 96f2b3eac98..8124ae6201f 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -1,10 +1,8 @@
-/* eslint-disable no-restricted-properties, func-names, space-before-function-paren,
-no-var, prefer-rest-params, wrap-iife, no-use-before-define, camelcase,
-no-unused-expressions, quotes, max-len, one-var, one-var-declaration-per-line,
-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 */
+/* eslint-disable no-restricted-properties, func-names, no-var, wrap-iife, camelcase,
+no-unused-expressions, max-len, one-var, one-var-declaration-per-line, default-case,
+prefer-template, consistent-return, no-alert, no-return-assign,
+no-param-reassign, prefer-arrow-callback, no-else-return, vars-on-top,
+no-unused-vars, no-shadow, no-useless-escape, class-methods-use-this */
/* global ResolveService */
/* global mrRefreshWidgetUrl */
@@ -22,6 +20,7 @@ import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_c
import axios from './lib/utils/axios_utils';
import { getLocationHash } from './lib/utils/url_utility';
import Flash from './flash';
+import { defaultAutocompleteConfig } from './gfm_auto_complete';
import CommentTypeToggle from './comment_type_toggle';
import GLForm from './gl_form';
import loadAwardsHandler from './awards_handler';
@@ -32,7 +31,7 @@ import {
getPagePath,
scrollToElement,
isMetaKey,
- hasVueMRDiscussionsCookie,
+ isInMRPage,
} from './lib/utils/common_utils';
import imageDiffHelper from './image_diff/helpers/index';
import { localTimeAgo } from './lib/utils/datetime_utility';
@@ -47,21 +46,9 @@ const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm;
export default class Notes {
- static initialize(
- notes_url,
- note_ids,
- last_fetched_at,
- view,
- enableGFM = true,
- ) {
+ static initialize(notes_url, note_ids, last_fetched_at, view, enableGFM) {
if (!this.instance) {
- this.instance = new Notes(
- notes_url,
- note_ids,
- last_fetched_at,
- view,
- enableGFM,
- );
+ this.instance = new Notes(notes_url, note_ids, last_fetched_at, view, enableGFM);
}
}
@@ -69,7 +56,7 @@ export default class Notes {
return this.instance;
}
- constructor(notes_url, note_ids, last_fetched_at, view, enableGFM = true) {
+ constructor(notes_url, note_ids, last_fetched_at, view, enableGFM = defaultAutocompleteConfig) {
this.updateTargetButtons = this.updateTargetButtons.bind(this);
this.updateComment = this.updateComment.bind(this);
this.visibilityChange = this.visibilityChange.bind(this);
@@ -104,13 +91,11 @@ export default class Notes {
this.basePollingInterval = 15000;
this.maxPollingSteps = 4;
- this.$wrapperEl = hasVueMRDiscussionsCookie()
- ? $(document).find('.diffs')
- : $(document);
+ this.$wrapperEl = isInMRPage() ? $(document).find('.diffs') : $(document);
this.cleanBinding();
this.addBinding();
this.setPollingInterval();
- this.setupMainTargetNoteForm();
+ this.setupMainTargetNoteForm(enableGFM);
this.taskList = new TaskList({
dataType: 'note',
fieldName: 'note',
@@ -146,55 +131,27 @@ export default class Notes {
// Reopen and close actions for Issue/MR combined with note form submit
this.$wrapperEl.on('click', '.js-comment-submit-button', this.postComment);
this.$wrapperEl.on('click', '.js-comment-save-button', this.updateComment);
- this.$wrapperEl.on(
- 'keyup input',
- '.js-note-text',
- this.updateTargetButtons,
- );
+ this.$wrapperEl.on('keyup input', '.js-note-text', this.updateTargetButtons);
// resolve a discussion
this.$wrapperEl.on('click', '.js-comment-resolve-button', this.postComment);
// remove a note (in general)
this.$wrapperEl.on('click', '.js-note-delete', this.removeNote);
// delete note attachment
- this.$wrapperEl.on(
- 'click',
- '.js-note-attachment-delete',
- this.removeAttachment,
- );
+ this.$wrapperEl.on('click', '.js-note-attachment-delete', this.removeAttachment);
// reset main target form when clicking discard
this.$wrapperEl.on('click', '.js-note-discard', this.resetMainTargetForm);
// update the file name when an attachment is selected
- this.$wrapperEl.on(
- 'change',
- '.js-note-attachment-input',
- this.updateFormAttachment,
- );
+ this.$wrapperEl.on('change', '.js-note-attachment-input', this.updateFormAttachment);
// reply to diff/discussion notes
- this.$wrapperEl.on(
- 'click',
- '.js-discussion-reply-button',
- this.onReplyToDiscussionNote,
- );
+ this.$wrapperEl.on('click', '.js-discussion-reply-button', this.onReplyToDiscussionNote);
// add diff note
this.$wrapperEl.on('click', '.js-add-diff-note-button', this.onAddDiffNote);
// add diff note for images
- this.$wrapperEl.on(
- 'click',
- '.js-add-image-diff-note-button',
- this.onAddImageDiffNote,
- );
+ this.$wrapperEl.on('click', '.js-add-image-diff-note-button', this.onAddImageDiffNote);
// hide diff note form
- this.$wrapperEl.on(
- 'click',
- '.js-close-discussion-note-form',
- this.cancelDiscussionForm,
- );
+ this.$wrapperEl.on('click', '.js-close-discussion-note-form', this.cancelDiscussionForm);
// toggle commit list
- this.$wrapperEl.on(
- 'click',
- '.system-note-commit-list-toggler',
- this.toggleCommitList,
- );
+ this.$wrapperEl.on('click', '.system-note-commit-list-toggler', this.toggleCommitList);
this.$wrapperEl.on('click', '.js-toggle-lazy-diff', this.loadLazyDiff);
this.$wrapperEl.on('click', '.js-toggle-lazy-diff-retry-button', this.onClickRetryLazyLoad.bind(this));
@@ -205,16 +162,8 @@ export default class Notes {
this.$wrapperEl.on('issuable:change', this.refresh);
// ajax:events that happen on Form when actions like Reopen, Close are performed on Issues and MRs.
this.$wrapperEl.on('ajax:success', '.js-main-target-form', this.addNote);
- this.$wrapperEl.on(
- 'ajax:success',
- '.js-discussion-note-form',
- this.addDiscussionNote,
- );
- this.$wrapperEl.on(
- 'ajax:success',
- '.js-main-target-form',
- this.resetMainTargetForm,
- );
+ this.$wrapperEl.on('ajax:success', '.js-discussion-note-form', this.addDiscussionNote);
+ this.$wrapperEl.on('ajax:success', '.js-main-target-form', this.resetMainTargetForm);
this.$wrapperEl.on(
'ajax:complete',
'.js-main-target-form',
@@ -224,8 +173,6 @@ export default class Notes {
this.$wrapperEl.on('keydown', '.js-note-text', this.keydownNoteText);
// When the URL fragment/hash has changed, `#note_xxx`
$(window).on('hashchange', this.onHashChange);
- this.boundGetContent = this.getContent.bind(this);
- document.addEventListener('refreshLegacyNotes', this.boundGetContent);
}
cleanBinding() {
@@ -249,21 +196,14 @@ export default class Notes {
this.$wrapperEl.off('ajax:success', '.js-main-target-form');
this.$wrapperEl.off('ajax:success', '.js-discussion-note-form');
this.$wrapperEl.off('ajax:complete', '.js-main-target-form');
- document.removeEventListener('refreshLegacyNotes', this.boundGetContent);
$(window).off('hashchange', this.onHashChange);
}
static initCommentTypeToggle(form) {
- const dropdownTrigger = form.querySelector(
- '.js-comment-type-dropdown .dropdown-toggle',
- );
- const dropdownList = form.querySelector(
- '.js-comment-type-dropdown .dropdown-menu',
- );
+ const dropdownTrigger = form.querySelector('.js-comment-type-dropdown .dropdown-toggle');
+ const dropdownList = form.querySelector('.js-comment-type-dropdown .dropdown-menu');
const noteTypeInput = form.querySelector('#note_type');
- const submitButton = form.querySelector(
- '.js-comment-type-dropdown .js-comment-submit-button',
- );
+ const submitButton = form.querySelector('.js-comment-type-dropdown .js-comment-submit-button');
const closeButton = form.querySelector('.js-note-target-close');
const reopenButton = form.querySelector('.js-note-target-reopen');
@@ -299,9 +239,7 @@ export default class Notes {
return;
}
myLastNote = $(
- `li.note[data-author-id='${
- gon.current_user_id
- }'][data-editable]:last`,
+ `li.note[data-author-id='${gon.current_user_id}'][data-editable]:last`,
$textarea.closest('.note, .notes_holder, #notes'),
);
if (myLastNote.length) {
@@ -315,7 +253,7 @@ export default class Notes {
if (discussionNoteForm.length) {
if ($textarea.val() !== '') {
if (
- !confirm('Are you sure you want to cancel creating this comment?')
+ !window.confirm('Are you sure you want to cancel creating this comment?')
) {
return;
}
@@ -329,7 +267,7 @@ export default class Notes {
newText = $textarea.val();
if (originalText !== newText) {
if (
- !confirm('Are you sure you want to cancel editing this comment?')
+ !window.confirm('Are you sure you want to cancel editing this comment?')
) {
return;
}
@@ -373,7 +311,7 @@ export default class Notes {
},
})
.then(({ data }) => {
- const notes = data.notes;
+ const { notes } = data;
this.last_fetched_at = data.last_fetched_at;
this.setPollingInterval(data.notes.length);
$.each(notes, (i, note) => this.renderNote(note));
@@ -398,8 +336,7 @@ export default class Notes {
if (shouldReset == null) {
shouldReset = true;
}
- nthInterval =
- this.basePollingInterval * Math.pow(2, this.maxPollingSteps - 1);
+ nthInterval = this.basePollingInterval * Math.pow(2, this.maxPollingSteps - 1);
if (shouldReset) {
this.pollingInterval = this.basePollingInterval;
} else if (this.pollingInterval < nthInterval) {
@@ -420,10 +357,7 @@ export default class Notes {
loadAwardsHandler()
.then(awardsHandler => {
- awardsHandler.addAwardToEmojiBar(
- votesBlock,
- noteEntity.commands_changes.emoji_award,
- );
+ awardsHandler.addAwardToEmojiBar(votesBlock, noteEntity.commands_changes.emoji_award);
awardsHandler.scrollToAwards();
})
.catch(() => {
@@ -473,17 +407,10 @@ export default class Notes {
if (!noteEntity.valid) {
if (noteEntity.errors && noteEntity.errors.commands_only) {
- if (
- noteEntity.commands_changes &&
- Object.keys(noteEntity.commands_changes).length > 0
- ) {
+ if (noteEntity.commands_changes && Object.keys(noteEntity.commands_changes).length > 0) {
$notesList.find('.system-note.being-posted').remove();
}
- this.addFlash(
- noteEntity.errors.commands_only,
- 'notice',
- this.parentTimeline.get(0),
- );
+ this.addFlash(noteEntity.errors.commands_only, 'notice', this.parentTimeline.get(0));
this.refresh();
}
return;
@@ -491,7 +418,7 @@ export default class Notes {
const $note = $notesList.find(`#note_${noteEntity.id}`);
if (Notes.isNewNote(noteEntity, this.note_ids)) {
- if (hasVueMRDiscussionsCookie()) {
+ if (isInMRPage()) {
return;
}
@@ -519,8 +446,7 @@ export default class Notes {
// There can be CRLF vs LF mismatches if we don't sanitize and compare the same way
const sanitizedNoteNote = normalizeNewlines(noteEntity.note);
const isTextareaUntouched =
- currentContent === initialContent ||
- currentContent === sanitizedNoteNote;
+ currentContent === initialContent || currentContent === sanitizedNoteNote;
if (isEditing && isTextareaUntouched) {
$textarea.val(noteEntity.note);
@@ -533,8 +459,6 @@ export default class Notes {
this.setupNewNote($updatedNote);
}
}
-
- Notes.refreshVueNotes();
}
isParallelView() {
@@ -552,13 +476,7 @@ export default class Notes {
}
this.note_ids.push(noteEntity.id);
- form =
- $form ||
- $(
- `.js-discussion-note-form[data-discussion-id="${
- noteEntity.discussion_id
- }"]`,
- );
+ form = $form || $(`.js-discussion-note-form[data-discussion-id="${noteEntity.discussion_id}"]`);
row =
form.length || !noteEntity.discussion_line_code
? form.closest('tr')
@@ -574,9 +492,7 @@ export default class Notes {
.first()
.find('.js-avatar-container.' + lineType + '_line');
// is this the first note of discussion?
- discussionContainer = $(
- `.notes[data-discussion-id="${noteEntity.discussion_id}"]`,
- );
+ discussionContainer = $(`.notes[data-discussion-id="${noteEntity.discussion_id}"]`);
if (!discussionContainer.length) {
discussionContainer = form.closest('.discussion').find('.notes');
}
@@ -584,18 +500,12 @@ export default class Notes {
if (noteEntity.diff_discussion_html) {
var $discussion = $(noteEntity.diff_discussion_html).renderGFM();
- if (
- !this.isParallelView() ||
- row.hasClass('js-temp-notes-holder') ||
- noteEntity.on_image
- ) {
+ if (!this.isParallelView() || row.hasClass('js-temp-notes-holder') || noteEntity.on_image) {
// insert the note and the reply button after the temp row
row.after($discussion);
} else {
// Merge new discussion HTML in
- var $notes = $discussion.find(
- `.notes[data-discussion-id="${noteEntity.discussion_id}"]`,
- );
+ var $notes = $discussion.find(`.notes[data-discussion-id="${noteEntity.discussion_id}"]`);
var contentContainerClass =
'.' +
$notes
@@ -608,29 +518,15 @@ export default class Notes {
.find(contentContainerClass + ' .content')
.append($notes.closest('.content').children());
}
- }
- // Init discussion on 'Discussion' page if it is merge request page
- const page = $('body').attr('data-page');
- if (
- (page && page.indexOf('projects:merge_request') !== -1) ||
- !noteEntity.diff_discussion_html
- ) {
- if (!hasVueMRDiscussionsCookie()) {
- Notes.animateAppendNote(
- noteEntity.discussion_html,
- $('.main-notes-list'),
- );
- }
+ } else {
+ Notes.animateAppendNote(noteEntity.discussion_html, $('.main-notes-list'));
}
} else {
// append new note to all matching discussions
Notes.animateAppendNote(noteEntity.html, discussionContainer);
}
- if (
- typeof gl.diffNotesCompileComponents !== 'undefined' &&
- noteEntity.discussion_resolvable
- ) {
+ if (typeof gl.diffNotesCompileComponents !== 'undefined' && noteEntity.discussion_resolvable) {
gl.diffNotesCompileComponents();
this.renderDiscussionAvatar(diffAvatarContainer, noteEntity);
@@ -703,14 +599,14 @@ export default class Notes {
*
* Sets some hidden fields in the form.
*/
- setupMainTargetNoteForm() {
+ setupMainTargetNoteForm(enableGFM) {
var form;
// find the form
form = $('.js-new-note-form');
// Set a global clone of the form for later cloning
this.formClone = form.clone();
// show the form
- this.setupNoteForm(form);
+ this.setupNoteForm(form, enableGFM);
// fix classes
form.removeClass('js-new-note-form');
form.addClass('js-main-target-form');
@@ -738,9 +634,9 @@ export default class Notes {
* setup GFM auto complete
* show the form
*/
- setupNoteForm(form) {
+ setupNoteForm(form, enableGFM = defaultAutocompleteConfig) {
var textarea, key;
- this.glForm = new GLForm(form, this.enableGFM);
+ this.glForm = new GLForm(form, enableGFM);
textarea = form.find('.js-note-text');
key = [
'Note',
@@ -784,6 +680,7 @@ export default class Notes {
}
updateNoteError($parentTimeline) {
+ // eslint-disable-next-line no-new
new Flash(
'Your comment could not be updated! Please check your network connection and try again.',
);
@@ -939,9 +836,7 @@ export default class Notes {
form.removeClass('current-note-edit-form');
form.find('.js-finish-edit-warning').hide();
// Replace markdown textarea text with original note text.
- return form
- .find('.js-note-text')
- .val(form.find('form.edit-note').data('originalNote'));
+ return form.find('.js-note-text').val(form.find('form.edit-note').data('originalNote'));
}
/**
@@ -989,21 +884,15 @@ export default class Notes {
// The notes tr can contain multiple lists of notes, like on the parallel diff
// notesTr does not exist for image diffs
- if (
- notesTr.find('.discussion-notes').length > 1 ||
- notesTr.length === 0
- ) {
+ if (notesTr.find('.discussion-notes').length > 1 || notesTr.length === 0) {
const $diffFile = $notes.closest('.diff-file');
if ($diffFile.length > 0) {
- const removeBadgeEvent = new CustomEvent(
- 'removeBadge.imageDiff',
- {
- detail: {
- // badgeNumber's start with 1 and index starts with 0
- badgeNumber: $notes.index() + 1,
- },
+ const removeBadgeEvent = new CustomEvent('removeBadge.imageDiff', {
+ detail: {
+ // badgeNumber's start with 1 and index starts with 0
+ badgeNumber: $notes.index() + 1,
},
- );
+ });
$diffFile[0].dispatchEvent(removeBadgeEvent);
}
@@ -1017,7 +906,6 @@ export default class Notes {
})(this),
);
- Notes.refreshVueNotes();
Notes.checkMergeRequestStatus();
return this.updateNotesCount(-1);
}
@@ -1033,7 +921,7 @@ export default class Notes {
$note.find('.note-attachment').remove();
$note.find('.note-body > .note-text').show();
$note.find('.note-header').show();
- return $note.find('.current-note-edit-form').remove();
+ return $note.find('.diffs .current-note-edit-form').remove();
}
/**
@@ -1107,9 +995,7 @@ export default class Notes {
form.find('.js-note-new-discussion').remove();
this.setupNoteForm(form);
- form
- .removeClass('js-main-target-form')
- .addClass('discussion-form js-discussion-note-form');
+ form.removeClass('js-main-target-form').addClass('discussion-form js-discussion-note-form');
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
var $commentBtn = form.find('comment-and-resolve-btn');
@@ -1119,9 +1005,7 @@ export default class Notes {
}
form.find('.js-note-text').focus();
- form
- .find('.js-comment-resolve-button')
- .attr('data-discussion-id', discussionID);
+ form.find('.js-comment-resolve-button').attr('data-discussion-id', discussionID);
}
/**
@@ -1154,9 +1038,7 @@ export default class Notes {
// Setup comment form
let newForm;
- const $noteContainer = $link
- .closest('.diff-viewer')
- .find('.note-container');
+ const $noteContainer = $link.closest('.diff-viewer').find('.note-container');
const $form = $noteContainer.find('> .discussion-form');
if ($form.length === 0) {
@@ -1225,14 +1107,12 @@ export default class Notes {
notesContent = targetRow.find(notesContentSelector);
addForm = true;
} else {
- const isCurrentlyShown = targetRow
- .find('.content:not(:empty)')
- .is(':visible');
+ const isCurrentlyShown = targetRow.find('.content:not(:empty)').is(':visible');
const isForced = forceShow === true || forceShow === false;
const showNow = forceShow === true || (!isCurrentlyShown && !isForced);
- targetRow.toggle(showNow);
- notesContent.toggle(showNow);
+ targetRow.toggleClass('hide', !showNow);
+ notesContent.toggleClass('hide', !showNow);
}
if (addForm) {
@@ -1371,13 +1251,15 @@ export default class Notes {
var postUrl = $originalContentEl.data('postUrl');
var targetId = $originalContentEl.data('targetId');
var targetType = $originalContentEl.data('targetType');
+ var markdownVersion = $originalContentEl.data('markdownVersion');
this.glForm = new GLForm($editForm.find('form'), this.enableGFM);
$editForm
.find('form')
.attr('action', `${postUrl}?html=true`)
- .attr('data-remote', 'true');
+ .attr('data-remote', 'true')
+ .attr('data-markdown-version', markdownVersion);
$editForm.find('.js-form-target-id').val(targetId);
$editForm.find('.js-form-target-type').val(targetType);
$editForm
@@ -1392,9 +1274,7 @@ export default class Notes {
if ($note.find('.js-conflict-edit-warning').length === 0) {
const $alert = $(`<div class="js-conflict-edit-warning alert alert-danger">
This comment has changed since you started editing, please review the
- <a href="#note_${
- noteEntity.id
- }" target="_blank" rel="noopener noreferrer">
+ <a href="#note_${noteEntity.id}" target="_blank" rel="noopener noreferrer">
updated comment
</a>
to ensure information is not lost
@@ -1404,15 +1284,13 @@ export default class Notes {
}
updateNotesCount(updateCount) {
- return this.notesCountBadge.text(
- parseInt(this.notesCountBadge.text(), 10) + updateCount,
- );
+ return this.notesCountBadge.text(parseInt(this.notesCountBadge.text(), 10) + updateCount);
}
static renderPlaceholderComponent($container) {
const el = $container.find('.js-code-placeholder').get(0);
+ // eslint-disable-next-line no-new
new Vue({
- // eslint-disable-line no-new
el,
components: {
SkeletonLoadingContainer,
@@ -1483,9 +1361,7 @@ export default class Notes {
toggleCommitList(e) {
const $element = $(e.currentTarget);
- const $closestSystemCommitList = $element.siblings(
- '.system-note-commit-list',
- );
+ const $closestSystemCommitList = $element.siblings('.system-note-commit-list');
$element
.find('.fa')
@@ -1518,9 +1394,7 @@ export default class Notes {
$systemNote.find('.note-text').addClass('system-note-commit-list');
$systemNote.find('.system-note-commit-list-toggler').show();
} else {
- $systemNote
- .find('.note-text')
- .addClass('system-note-commit-list hide-shade');
+ $systemNote.find('.note-text').addClass('system-note-commit-list hide-shade');
}
});
}
@@ -1591,10 +1465,6 @@ export default class Notes {
return $updatedNote;
}
- static refreshVueNotes() {
- document.dispatchEvent(new CustomEvent('refreshVueNotes'));
- }
-
/**
* Get data from Form attributes to use for saving/submitting comment.
*/
@@ -1675,7 +1545,7 @@ export default class Notes {
<div class="note-header">
<div class="note-header-info">
<a href="/${_.escape(currentUsername)}">
- <span class="hidden-xs">${_.escape(
+ <span class="d-none d-sm-inline-block">${_.escape(
currentUsername,
)}</span>
<span class="note-headline-light">${_.escape(
@@ -1694,7 +1564,7 @@ export default class Notes {
</li>`,
);
- $tempNote.find('.hidden-xs').text(_.escape(currentUserFullname));
+ $tempNote.find('.d-none.d-sm-inline-block').text(_.escape(currentUserFullname));
$tempNote
.find('.note-headline-light')
.text(`@${_.escape(currentUsername)}`);
@@ -1753,15 +1623,8 @@ export default class Notes {
.attr('id') === 'discussion';
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,
- formContentOriginal,
- } = this.getFormData($form);
+ const isDiscussionResolve = $submitBtn.hasClass('js-comment-resolve-button');
+ const { formData, formContent, formAction, formContentOriginal } = this.getFormData($form);
let noteUniqueId;
let systemNoteUniqueId;
let hasQuickActions = false;
@@ -1827,7 +1690,6 @@ export default class Notes {
$closeBtn.text($closeBtn.data('originalText'));
- /* eslint-disable promise/catch-or-return */
// Make request to submit comment on server
return axios
.post(`${formAction}?html=true`, formData)
@@ -1849,9 +1711,7 @@ export default class Notes {
// Reset cached commands list when command is applied
if (hasQuickActions) {
- $form
- .find('textarea.js-note-text')
- .trigger('clear-commands-cache.atwho');
+ $form.find('textarea.js-note-text').trigger('clear-commands-cache.atwho');
}
// Clear previous form errors
@@ -1896,12 +1756,8 @@ export default class Notes {
// append flash-container to the Notes list
if ($notesContainer.length) {
- $notesContainer.append(
- '<div class="flash-container" style="display: none;"></div>',
- );
+ $notesContainer.append('<div class="flash-container" style="display: none;"></div>');
}
-
- Notes.refreshVueNotes();
} else if (isMainForm) {
// Check if this was main thread comment
// Show final note element on UI and perform form and action buttons cleanup
@@ -1935,9 +1791,7 @@ export default class Notes {
// Show form again on UI on failure
if (isDiscussionForm && $notesContainer.length) {
- const replyButton = $notesContainer
- .parent()
- .find('.js-discussion-reply-button');
+ const replyButton = $notesContainer.parent().find('.js-discussion-reply-button');
this.replyToDiscussionNote(replyButton[0]);
$form = $notesContainer.parent().find('form');
}
@@ -1980,16 +1834,13 @@ export default class Notes {
// Show updated comment content temporarily
$noteBodyText.html(formContent);
- $editingNote
- .removeClass('is-editing fade-in-full')
- .addClass('being-posted fade-in-half');
+ $editingNote.removeClass('is-editing fade-in-full').addClass('being-posted fade-in-half');
$editingNote
.find('.note-headline-meta a')
.html(
'<i class="fa fa-spinner fa-spin" aria-label="Comment is being updated" aria-hidden="true"></i>',
);
- /* eslint-disable promise/catch-or-return */
// Make request to update comment on server
axios
.post(`${formAction}?html=true`, formData)
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 48642c4a086..6612bc44e0b 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -7,10 +7,7 @@ import { __, sprintf } from '~/locale';
import Flash from '../../flash';
import Autosave from '../../autosave';
import TaskList from '../../task_list';
-import {
- capitalizeFirstCharacter,
- convertToCamelCase,
-} from '../../lib/utils/text_utility';
+import { capitalizeFirstCharacter, convertToCamelCase, splitCamelCase } from '../../lib/utils/text_utility';
import * as constants from '../constants';
import eventHub from '../event_hub';
import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
@@ -37,6 +34,11 @@ export default {
type: String,
required: true,
},
+ markdownVersion: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
},
data() {
return {
@@ -56,21 +58,23 @@ export default {
]),
...mapState(['isToggleStateButtonLoading']),
noteableDisplayName() {
- return this.noteableType.replace(/_/g, ' ');
+ return splitCamelCase(this.noteableType).toLowerCase();
},
isLoggedIn() {
return this.getUserData.id;
},
commentButtonTitle() {
- return this.noteType === constants.COMMENT
- ? 'Comment'
- : 'Start discussion';
+ return this.noteType === constants.COMMENT ? 'Comment' : 'Start discussion';
+ },
+ startDiscussionDescription() {
+ let text = 'Discuss a specific suggestion or question';
+ if (this.getNoteableData.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE) {
+ text += ' that needs to be resolved';
+ }
+ return `${text}.`;
},
isOpen() {
- return (
- this.openState === constants.OPENED ||
- this.openState === constants.REOPENED
- );
+ return this.openState === constants.OPENED || this.openState === constants.REOPENED;
},
canCreateNote() {
return this.getNoteableData.current_user.can_create_note;
@@ -117,6 +121,9 @@ export default {
endpoint() {
return this.getNoteableData.create_note_path;
},
+ issuableTypeTitle() {
+ return this.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE ? 'merge request' : 'issue';
+ },
},
watch: {
note(newNote) {
@@ -129,9 +136,7 @@ export default {
mounted() {
// jQuery is needed here because it is a custom event being dispatched with jQuery.
$(document).on('issuable:change', (e, isClosed) => {
- this.toggleIssueLocalState(
- isClosed ? constants.CLOSED : constants.REOPENED,
- );
+ this.toggleIssueLocalState(isClosed ? constants.CLOSED : constants.REOPENED);
});
this.initAutoSave();
@@ -168,6 +173,7 @@ export default {
noteable_id: this.getNoteableData.id,
note: this.note,
},
+ merge_request_diff_head_sha: this.getNoteableData.diff_head_sha,
},
};
@@ -227,9 +233,7 @@ Please check your network connection and try again.`;
this.toggleStateButtonLoading(false);
Flash(
sprintf(
- __(
- 'Something went wrong while closing the %{issuable}. Please try again later',
- ),
+ __('Something went wrong while closing the %{issuable}. Please try again later'),
{ issuable: this.noteableDisplayName },
),
);
@@ -242,9 +246,7 @@ Please check your network connection and try again.`;
this.toggleStateButtonLoading(false);
Flash(
sprintf(
- __(
- 'Something went wrong while reopening the %{issuable}. Please try again later',
- ),
+ __('Something went wrong while reopening the %{issuable}. Please try again later'),
{ issuable: this.noteableDisplayName },
),
);
@@ -281,9 +283,7 @@ Please check your network connection and try again.`;
},
initAutoSave() {
if (this.isLoggedIn) {
- const noteableType = capitalizeFirstCharacter(
- convertToCamelCase(this.noteableType),
- );
+ const noteableType = capitalizeFirstCharacter(convertToCamelCase(this.noteableType));
this.autosave = new Autosave($(this.$refs.textarea), [
'Note',
@@ -312,8 +312,8 @@ Please check your network connection and try again.`;
<div>
<note-signed-out-widget v-if="!isLoggedIn" />
<discussion-locked-widget
- issuable-type="issue"
- v-else-if="isLocked(getNoteableData) && !canCreateNote"
+ v-else-if="!canCreateNote"
+ :issuable-type="issuableTypeTitle"
/>
<ul
v-else-if="canCreateNote"
@@ -321,7 +321,7 @@ Please check your network connection and try again.`;
<li class="timeline-entry">
<div class="timeline-entry-inner">
<div class="flash-container error-alert timeline-content"></div>
- <div class="timeline-icon hidden-xs hidden-sm">
+ <div class="timeline-icon d-none d-sm-none d-md-block">
<user-avatar-link
v-if="author"
:link-href="author.path"
@@ -345,23 +345,24 @@ Please check your network connection and try again.`;
/>
<markdown-field
+ ref="markdownField"
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
:quick-actions-docs-path="quickActionsDocsPath"
- :add-spacing-classes="false"
- ref="markdownField">
+ :markdown-version="markdownVersion"
+ :add-spacing-classes="false">
<textarea
id="note-body"
+ ref="textarea"
+ slot="textarea"
+ v-model="note"
+ :disabled="isSubmitting"
name="note[note]"
- class="note-textarea js-vue-comment-form
+ class="note-textarea js-vue-comment-form js-note-text
js-gfm-input js-autosize markdown-area js-vue-textarea"
data-supports-quick-actions="true"
aria-label="Description"
- v-model="note"
- ref="textarea"
- slot="textarea"
- :disabled="isSubmitting"
- placeholder="Write a comment or drag your files here..."
+ placeholder="Write a comment or drag your files here…"
@keydown.up="editCurrentUserLastNote()"
@keydown.meta.enter="handleSave()"
@keydown.ctrl.enter="handleSave()">
@@ -369,13 +370,13 @@ js-gfm-input js-autosize markdown-area js-vue-textarea"
</markdown-field>
<div class="note-form-actions">
<div
- class="pull-left btn-group
+ class="float-left btn-group
append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown">
<button
- @click.prevent="handleSave()"
:disabled="isSubmitButtonDisabled"
class="btn btn-create comment-btn js-comment-button js-comment-submit-button"
- type="submit">
+ type="submit"
+ @click.prevent="handleSave()">
{{ __(commentButtonTitle) }}
</button>
<button
@@ -383,6 +384,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
name="button"
type="button"
class="btn comment-btn note-type-toggle js-note-new-discussion dropdown-toggle"
+ data-display="static"
data-toggle="dropdown"
aria-label="Open comment type dropdown">
<i
@@ -422,7 +424,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
<div class="description">
<strong>Start discussion</strong>
<p>
- Discuss a specific suggestion or question.
+ {{ startDiscussionDescription }}
</p>
</div>
</button>
@@ -433,20 +435,20 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
<loading-button
v-if="canUpdateIssue"
:loading="isToggleStateButtonLoading"
- @click="handleSave(true)"
:container-class="[
actionButtonClassNames,
'btn btn-comment btn-comment-and-close js-action-button'
]"
:disabled="isToggleStateButtonLoading || isSubmitting"
:label="issueActionButtonTitle"
+ @click="handleSave(true)"
/>
<button
- type="button"
v-if="note.length"
- @click="discard"
- class="btn btn-cancel js-note-discard">
+ type="button"
+ class="btn btn-cancel js-note-discard"
+ @click="discard">
Discard draft
</button>
</div>
diff --git a/app/assets/javascripts/notes/components/diff_file_header.vue b/app/assets/javascripts/notes/components/diff_file_header.vue
index 94d9dc69964..fc7b52be241 100644
--- a/app/assets/javascripts/notes/components/diff_file_header.vue
+++ b/app/assets/javascripts/notes/components/diff_file_header.vue
@@ -29,12 +29,12 @@ export default {
<span>
<icon name="archive" />
<strong
- v-html="diffFile.submoduleLink"
class="file-title-name"
+ v-html="diffFile.submoduleLink"
></strong>
<clipboard-button
- title="Copy file path to clipboard"
:text="diffFile.submoduleLink"
+ title="Copy file path to clipboard"
css-class="btn-default btn-transparent btn-clipboard"
/>
</span>
@@ -48,16 +48,16 @@ export default {
<span v-html="diffFile.blobIcon"></span>
<span v-if="diffFile.renamedFile">
<strong
- class="file-title-name has-tooltip"
:title="diffFile.oldPath"
+ class="file-title-name has-tooltip"
data-container="body"
>
{{ diffFile.oldPath }}
</strong>
&rarr;
<strong
- class="file-title-name has-tooltip"
:title="diffFile.newPath"
+ class="file-title-name has-tooltip"
data-container="body"
>
{{ diffFile.newPath }}
@@ -66,8 +66,8 @@ export default {
<strong
v-else
- class="file-title-name has-tooltip"
:title="diffFile.oldPath"
+ class="file-title-name has-tooltip"
data-container="body"
>
{{ diffFile.filePath }}
@@ -78,8 +78,8 @@ export default {
</component>
<clipboard-button
- title="Copy file path to clipboard"
:text="diffFile.filePath"
+ title="Copy file path to clipboard"
css-class="btn-default btn-transparent btn-clipboard"
/>
diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue
index ee01ec85bbb..9c2908c477e 100644
--- a/app/assets/javascripts/notes/components/diff_with_note.vue
+++ b/app/assets/javascripts/notes/components/diff_with_note.vue
@@ -1,85 +1,155 @@
<script>
-import $ from 'jquery';
-import syntaxHighlight from '~/syntax_highlight';
-import imageDiffHelper from '~/image_diff/helpers/index';
-import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import DiffFileHeader from './diff_file_header.vue';
+ import { mapState, mapActions } from 'vuex';
+ import imageDiffHelper from '~/image_diff/helpers/index';
+ import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+ import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
+ import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
+ import { trimFirstCharOfLineContent } from '~/diffs/store/utils';
-export default {
- components: {
- DiffFileHeader,
- },
- props: {
- discussion: {
- type: Object,
- required: true,
+ export default {
+ components: {
+ DiffFileHeader,
+ SkeletonLoadingContainer,
},
- },
- computed: {
- isImageDiff() {
- return !this.diffFile.text;
+ props: {
+ discussion: {
+ type: Object,
+ required: true,
+ },
},
- diffFileClass() {
- const { text } = this.diffFile;
- return text ? 'text-file' : 'js-image-file';
+ data() {
+ return {
+ error: false,
+ };
},
- diffRows() {
- return $(this.discussion.truncatedDiffLines);
- },
- diffFile() {
- return convertObjectPropsToCamelCase(this.discussion.diffFile);
+ computed: {
+ ...mapState({
+ noteableData: state => state.notes.noteableData,
+ }),
+ hasTruncatedDiffLines() {
+ return this.discussion.truncatedDiffLines &&
+ this.discussion.truncatedDiffLines.length !== 0;
+ },
+ isDiscussionsExpanded() {
+ return true; // TODO: @fatihacet - Fix this.
+ },
+ isCollapsed() {
+ return this.diffFile.collapsed || false;
+ },
+ isImageDiff() {
+ return !this.diffFile.text;
+ },
+ diffFileClass() {
+ const { text } = this.diffFile;
+ return text ? 'text-file' : 'js-image-file';
+ },
+ diffFile() {
+ return convertObjectPropsToCamelCase(this.discussion.diffFile, { deep: true });
+ },
+ imageDiffHtml() {
+ return this.discussion.imageDiffHtml;
+ },
+ currentUser() {
+ return this.noteableData.current_user;
+ },
+ userColorScheme() {
+ return window.gon.user_color_scheme;
+ },
+ normalizedDiffLines() {
+ if (this.discussion.truncatedDiffLines) {
+ return this.discussion.truncatedDiffLines.map(line =>
+ trimFirstCharOfLineContent(convertObjectPropsToCamelCase(line)),
+ );
+ }
+
+ return [];
+ },
},
- imageDiffHtml() {
- return this.discussion.imageDiffHtml;
+ mounted() {
+ if (this.isImageDiff) {
+ const canCreateNote = false;
+ const renderCommentBadge = true;
+ imageDiffHelper.initImageDiff(this.$refs.fileHolder, canCreateNote, renderCommentBadge);
+ } else if (!this.hasTruncatedDiffLines) {
+ this.fetchDiff();
+ }
},
- },
- mounted() {
- if (this.isImageDiff) {
- const canCreateNote = false;
- const renderCommentBadge = true;
- imageDiffHelper.initImageDiff(
- this.$refs.fileHolder,
- canCreateNote,
- renderCommentBadge,
- );
- } else {
- const fileHolder = $(this.$refs.fileHolder);
- this.$nextTick(() => {
- syntaxHighlight(fileHolder);
- });
- }
- },
- methods: {
- rowTag(html) {
- return html.outerHTML ? 'tr' : 'template';
+ methods: {
+ ...mapActions(['fetchDiscussionDiffLines']),
+ rowTag(html) {
+ return html.outerHTML ? 'tr' : 'template';
+ },
+ fetchDiff() {
+ this.error = false;
+ this.fetchDiscussionDiffLines(this.discussion)
+ .then(this.highlight)
+ .catch(() => {
+ this.error = true;
+ });
+ },
},
- },
-};
+ };
</script>
<template>
<div
ref="fileHolder"
- class="diff-file file-holder"
:class="diffFileClass"
+ class="diff-file file-holder"
>
- <div class="js-file-title file-title file-title-flex-parent">
- <diff-file-header
- :diff-file="diffFile"
- />
- </div>
+ <diff-file-header
+ :diff-file="diffFile"
+ :current-user="currentUser"
+ :discussions-expanded="isDiscussionsExpanded"
+ :expanded="!isCollapsed"
+ />
<div
v-if="diffFile.text"
- class="diff-content code js-syntax-highlight"
+ :class="userColorScheme"
+ class="diff-content code"
>
<table>
- <component
- :is="rowTag(html)"
- :class="html.className"
- v-for="(html, index) in diffRows"
- v-html="html.outerHTML"
- :key="index"
- />
+ <tr
+ v-for="line in normalizedDiffLines"
+ :key="line.lineCode"
+ class="line_holder"
+ >
+ <td class="diff-line-num old_line">{{ line.oldLine }}</td>
+ <td class="diff-line-num new_line">{{ line.newLine }}</td>
+ <td
+ :class="line.type"
+ class="line_content"
+ v-html="line.richText"
+ >
+ </td>
+ </tr>
+ <tr
+ v-if="!hasTruncatedDiffLines"
+ class="line_holder line-holder-placeholder"
+ >
+ <td class="old_line diff-line-num"></td>
+ <td class="new_line diff-line-num"></td>
+ <td
+ v-if="error"
+ class="js-error-lazy-load-diff diff-loading-error-block"
+ >
+ Unable to load the diff
+ <button
+ class="btn-link btn-link-retry btn-no-padding js-toggle-lazy-diff-retry-button"
+ @click="fetchDiff"
+ >
+ Try again
+ </button>
+ </td>
+ <td
+ v-else
+ class="line_content js-success-lazy-load"
+ >
+ <span></span>
+ <skeleton-loading-container />
+ <span></span>
+ </td>
+ </tr>
<tr class="notes_holder">
<td
class="notes_line"
diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue
index cbe4774a360..6385b75e557 100644
--- a/app/assets/javascripts/notes/components/discussion_counter.vue
+++ b/app/assets/javascripts/notes/components/discussion_counter.vue
@@ -1,5 +1,5 @@
<script>
-import { mapGetters } from 'vuex';
+import { mapActions, mapGetters } from 'vuex';
import resolveSvg from 'icons/_icon_resolve_discussion.svg';
import resolvedSvg from 'icons/_icon_status_success_solid.svg';
import mrIssueSvg from 'icons/_icon_mr_issue.svg';
@@ -48,10 +48,14 @@ export default {
this.nextDiscussionSvg = nextDiscussionSvg;
},
methods: {
- jumpToFirstDiscussion() {
- const el = document.querySelector(
- `[data-discussion-id="${this.firstUnresolvedDiscussionId}"]`,
- );
+ ...mapActions(['expandDiscussion']),
+ jumpToFirstUnresolvedDiscussion() {
+ const discussionId = this.firstUnresolvedDiscussionId;
+ if (!discussionId) {
+ return;
+ }
+
+ const el = document.querySelector(`[data-discussion-id="${discussionId}"]`);
const activeTab = window.mrTabs.currentAction;
if (activeTab === 'commits' || activeTab === 'pipelines') {
@@ -59,6 +63,7 @@ export default {
}
if (el) {
+ this.expandDiscussion({ discussionId });
scrollToElement(el);
}
},
@@ -95,9 +100,9 @@ export default {
class="btn-group"
role="group">
<a
- :href="resolveAllDiscussionsIssuePath"
v-tooltip
- title="Resolve all discussions in new issue"
+ :href="resolveAllDiscussionsIssuePath"
+ :title="s__('Resolve all discussions in new issue')"
data-container="body"
class="new-issue-for-discussion btn btn-default discussion-create-issue-btn">
<span v-html="mrIssueSvg"></span>
@@ -108,11 +113,11 @@ export default {
class="btn-group"
role="group">
<button
- @click="jumpToFirstDiscussion"
v-tooltip
title="Jump to first unresolved discussion"
data-container="body"
- class="btn btn-default discussion-next-btn">
+ class="btn btn-default discussion-next-btn"
+ @click="jumpToFirstUnresolvedDiscussion">
<span v-html="nextDiscussionSvg"></span>
</button>
</div>
diff --git a/app/assets/javascripts/notes/components/discussion_locked_widget.vue b/app/assets/javascripts/notes/components/discussion_locked_widget.vue
index 13283b187d1..de0a5f8489b 100644
--- a/app/assets/javascripts/notes/components/discussion_locked_widget.vue
+++ b/app/assets/javascripts/notes/components/discussion_locked_widget.vue
@@ -14,8 +14,8 @@ export default {
<div class="disabled-comment text-center">
<span class="issuable-note-warning inline">
<icon
- name="lock"
:size="16"
+ name="lock"
class="icon"
/>
<span>
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index 626b0799581..cdbbb342331 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -27,6 +27,10 @@ export default {
type: Number,
required: true,
},
+ noteUrl: {
+ type: String,
+ required: true,
+ },
accessLevel: {
type: String,
required: false,
@@ -48,6 +52,11 @@ export default {
type: Boolean,
required: true,
},
+ canResolve: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
resolvable: {
type: Boolean,
required: false,
@@ -125,16 +134,16 @@ export default {
{{ accessLevel }}
</span>
<div
- v-if="resolvable"
+ v-if="canResolve"
class="note-actions-item">
<button
v-tooltip
- @click="onResolve"
:class="{ 'is-disabled': !resolvable, 'is-active': isResolved }"
:title="resolveButtonTitle"
:aria-label="resolveButtonTitle"
type="button"
- class="line-resolve-btn note-action-button">
+ class="line-resolve-btn note-action-button"
+ @click="onResolve">
<template v-if="!isResolving">
<div
v-if="isResolved"
@@ -164,16 +173,16 @@ export default {
>
<loading-icon :inline="true" />
<span
- v-html="emojiSmiling"
- class="link-highlight award-control-icon-neutral">
+ class="link-highlight award-control-icon-neutral"
+ v-html="emojiSmiling">
</span>
<span
- v-html="emojiSmiley"
- class="link-highlight award-control-icon-positive">
+ class="link-highlight award-control-icon-positive"
+ v-html="emojiSmiley">
</span>
<span
- v-html="emojiSmile"
- class="link-highlight award-control-icon-super-positive">
+ class="link-highlight award-control-icon-super-positive"
+ v-html="emojiSmile">
</span>
</a>
</div>
@@ -181,16 +190,16 @@ export default {
v-if="canEdit"
class="note-actions-item">
<button
- @click="onEdit"
v-tooltip
type="button"
title="Edit comment"
class="note-action-button js-note-edit btn btn-transparent"
data-container="body"
- data-placement="bottom">
+ data-placement="bottom"
+ @click="onEdit">
<span
- v-html="editSvg"
- class="link-highlight">
+ class="link-highlight"
+ v-html="editSvg">
</span>
</button>
</div>
@@ -216,11 +225,20 @@ export default {
Report as abuse
</a>
</li>
+ <li>
+ <button
+ :data-clipboard-text="noteUrl"
+ type="button"
+ css-class="btn-default btn-transparent"
+ >
+ Copy link
+ </button>
+ </li>
<li v-if="canEdit">
<button
- @click.prevent="onDelete"
class="btn btn-transparent js-note-delete js-note-delete"
- type="button">
+ type="button"
+ @click.prevent="onDelete">
<span class="text-danger">
Delete comment
</span>
diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue
index e8fd155a1ee..225d9f18612 100644
--- a/app/assets/javascripts/notes/components/note_awards_list.vue
+++ b/app/assets/javascripts/notes/components/note_awards_list.vue
@@ -199,10 +199,11 @@ export default {
:key="index"
:class="getAwardClassBindings(awardList, awardName)"
:title="awardTitle(awardList)"
- @click="handleAward(awardName)"
class="btn award-control"
+ data-boundary="viewport"
data-placement="bottom"
- type="button">
+ type="button"
+ @click="handleAward(awardName)">
<span v-html="getAwardHTML(awardName)"></span>
<span class="award-control-text js-counter">
{{ awardList.length }}
@@ -217,19 +218,20 @@ export default {
class="award-control btn js-add-award"
title="Add reaction"
aria-label="Add reaction"
+ data-boundary="viewport"
data-placement="bottom"
type="button">
<span
- v-html="emojiSmiling"
- class="award-control-icon award-control-icon-neutral">
+ class="award-control-icon award-control-icon-neutral"
+ v-html="emojiSmiling">
</span>
<span
- v-html="emojiSmiley"
- class="award-control-icon award-control-icon-positive">
+ class="award-control-icon award-control-icon-positive"
+ v-html="emojiSmiley">
</span>
<span
- v-html="emojiSmile"
- class="award-control-icon award-control-icon-super-positive">
+ class="award-control-icon award-control-icon-super-positive"
+ v-html="emojiSmile">
</span>
<i
aria-hidden="true"
diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue
index 0cb626c14f4..6f4a0709825 100644
--- a/app/assets/javascripts/notes/components/note_body.vue
+++ b/app/assets/javascripts/notes/components/note_body.vue
@@ -40,7 +40,7 @@ export default {
this.initTaskList();
if (this.isEditing) {
- this.initAutoSave(this.note.noteable_type);
+ this.initAutoSave(this.note);
}
},
updated() {
@@ -49,7 +49,7 @@ export default {
if (this.isEditing) {
if (!this.autosave) {
- this.initAutoSave(this.note.noteable_type);
+ this.initAutoSave(this.note);
} else {
this.setAutoSave();
}
@@ -72,7 +72,7 @@ export default {
this.$emit('handleFormUpdate', note, parentElement, callback);
},
formCancelHandler(shouldConfirm, isDirty) {
- this.$emit('cancelFormEdition', shouldConfirm, isDirty);
+ this.$emit('cancelForm', shouldConfirm, isDirty);
},
},
};
@@ -80,20 +80,21 @@ export default {
<template>
<div
- :class="{ 'js-task-list-container': canEdit }"
ref="note-body"
+ :class="{ 'js-task-list-container': canEdit }"
class="note-body">
<div
- v-html="note.note_html"
- class="note-text md"></div>
+ class="note-text md"
+ v-html="note.note_html"></div>
<note-form
v-if="isEditing"
ref="noteForm"
- @handleFormUpdate="handleFormUpdate"
- @cancelFormEdition="formCancelHandler"
:is-editing="isEditing"
:note-body="noteBody"
:note-id="note.id"
+ :markdown-version="note.cached_markdown_version"
+ @handleFormUpdate="handleFormUpdate"
+ @cancelForm="formCancelHandler"
/>
<textarea
v-if="canEdit"
@@ -105,6 +106,7 @@ export default {
:edited-at="note.last_edited_at"
:edited-by="note.last_edited_by"
action-text="Edited"
+ class="note_edited_ago"
/>
<note-awards-list
v-if="note.award_emoji.length"
diff --git a/app/assets/javascripts/notes/components/note_edited_text.vue b/app/assets/javascripts/notes/components/note_edited_text.vue
index 2dc39d1a186..391bb2ae179 100644
--- a/app/assets/javascripts/notes/components/note_edited_text.vue
+++ b/app/assets/javascripts/notes/components/note_edited_text.vue
@@ -11,14 +11,20 @@ export default {
type: String,
required: true,
},
+ actionDetailText: {
+ type: String,
+ required: false,
+ default: '',
+ },
editedAt: {
type: String,
- required: true,
+ required: false,
+ default: null,
},
editedBy: {
type: Object,
required: false,
- default: () => ({}),
+ default: null,
},
className: {
type: String,
@@ -33,13 +39,14 @@ export default {
<div :class="className">
{{ actionText }}
<template v-if="editedBy">
- {{ s__('ByAuthor|by') }}
+ by
<a
:href="editedBy.path"
class="js-vue-author author_link">
{{ editedBy.name }}
</a>
</template>
+ {{ actionDetailText }}
<time-ago-tooltip
:time="editedAt"
tooltip-placement="bottom"
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index c59a2e7a406..26482a02e00 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -24,12 +24,17 @@ export default {
required: false,
default: 0,
},
+ markdownVersion: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
saveButtonTitle: {
type: String,
required: false,
default: 'Save comment',
},
- note: {
+ discussion: {
type: Object,
required: false,
default: () => ({}),
@@ -38,6 +43,11 @@ export default {
type: Boolean,
required: true,
},
+ lineCode: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -66,9 +76,7 @@ export default {
return this.getNotesDataByProp('markdownDocsPath');
},
quickActionsDocsPath() {
- return !this.isEditing
- ? this.getNotesDataByProp('quickActionsDocsPath')
- : undefined;
+ return !this.isEditing ? this.getNotesDataByProp('quickActionsDocsPath') : undefined;
},
currentUserId() {
return this.getUserDataByProp('id');
@@ -95,24 +103,17 @@ export default {
const beforeSubmitDiscussionState = this.discussionResolved;
this.isSubmitting = true;
- this.$emit(
- 'handleFormUpdate',
- this.updatedNoteBody,
- this.$refs.editNoteForm,
- () => {
- this.isSubmitting = false;
+ this.$emit('handleFormUpdate', this.updatedNoteBody, this.$refs.editNoteForm, () => {
+ this.isSubmitting = false;
- if (shouldResolve) {
- this.resolveHandler(beforeSubmitDiscussionState);
- }
- },
- );
+ if (shouldResolve) {
+ this.resolveHandler(beforeSubmitDiscussionState);
+ }
+ });
},
editMyLastNote() {
if (this.updatedNoteBody === '') {
- const lastNoteInDiscussion = this.getDiscussionLastNote(
- this.updatedNoteBody,
- );
+ const lastNoteInDiscussion = this.getDiscussionLastNote(this.discussion);
if (lastNoteInDiscussion) {
eventHub.$emit('enterEditMode', {
@@ -123,11 +124,7 @@ export default {
},
cancelHandler(shouldConfirm = false) {
// Sends information about confirm message and if the textarea has changed
- this.$emit(
- 'cancelFormEdition',
- shouldConfirm,
- this.noteBody !== this.updatedNoteBody,
- );
+ this.$emit('cancelForm', shouldConfirm, this.noteBody !== this.updatedNoteBody);
},
},
};
@@ -136,7 +133,7 @@ export default {
<template>
<div
ref="editNoteForm"
- class="note-edit-form current-note-edit-form">
+ class="note-edit-form current-note-edit-form js-discussion-note-form">
<div
v-if="conflictWhileEditing"
class="js-conflict-edit-warning alert alert-danger">
@@ -150,7 +147,10 @@ export default {
to ensure information is not lost.
</div>
<div class="flash-container timeline-content"></div>
- <form class="edit-note common-note-form js-quick-submit gfm-form">
+ <form
+ :data-line-code="lineCode"
+ class="edit-note common-note-form js-quick-submit gfm-form"
+ >
<issue-warning
v-if="hasWarning(getNoteableData)"
@@ -161,19 +161,20 @@ export default {
<markdown-field
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
+ :markdown-version="markdownVersion"
:quick-actions-docs-path="quickActionsDocsPath"
:add-spacing-classes="false">
<textarea
id="note_note"
+ ref="textarea"
+ slot="textarea"
+ :data-supports-quick-actions="!isEditing"
+ v-model="updatedNoteBody"
name="note[note]"
- class="note-textarea js-gfm-input
+ class="note-textarea js-gfm-input js-note-text
js-autosize markdown-area js-vue-issue-note-form js-vue-textarea"
- :data-supports-quick-actions="!isEditing"
aria-label="Description"
- v-model="updatedNoteBody"
- ref="textarea"
- slot="textarea"
- placeholder="Write a comment or drag your files here..."
+ placeholder="Write a comment or drag your files here…"
@keydown.meta.enter="handleUpdate()"
@keydown.ctrl.enter="handleUpdate()"
@keydown.up="editMyLastNote()"
@@ -182,23 +183,23 @@ js-autosize markdown-area js-vue-issue-note-form js-vue-textarea"
</markdown-field>
<div class="note-form-actions clearfix">
<button
- type="button"
- @click="handleUpdate()"
:disabled="isDisabled"
- class="js-vue-issue-save btn btn-save">
+ type="button"
+ class="js-vue-issue-save btn btn-save js-comment-button "
+ @click="handleUpdate()">
{{ saveButtonTitle }}
</button>
<button
- v-if="note.resolvable"
- @click.prevent="handleUpdate(true)"
+ v-if="discussion.resolvable"
class="btn btn-nr btn-default append-right-10 js-comment-resolve-button"
+ @click.prevent="handleUpdate(true)"
>
{{ resolveButtonTitle }}
</button>
<button
- @click="cancelHandler()"
- class="btn btn-cancel note-edit-cancel"
- type="button">
+ class="btn btn-cancel note-edit-cancel js-close-discussion-note-form"
+ type="button"
+ @click="cancelHandler()">
Cancel
</button>
</div>
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index a4081957207..ee3580895df 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -20,11 +20,6 @@ export default {
required: false,
default: '',
},
- actionTextHtml: {
- type: String,
- required: false,
- default: '',
- },
noteId: {
type: Number,
required: true,
@@ -66,9 +61,9 @@ export default {
v-if="includeToggle"
class="discussion-actions">
<button
- @click="handleToggle"
class="note-action-button discussion-toggle-button js-vue-toggle-button"
- type="button">
+ type="button"
+ @click="handleToggle">
<i
:class="toggleChevronClass"
class="fa"
@@ -88,18 +83,16 @@ export default {
<template v-if="actionText">
{{ actionText }}
</template>
- <span
- v-if="actionTextHtml"
- v-html="actionTextHtml"
- class="system-note-message">
+ <span class="system-note-message">
+ <slot></slot>
</span>
<span class="system-note-separator">
&middot;
</span>
<a
:href="noteTimestampLink"
- @click="updateTargetNoteHash"
- class="note-timestamp system-note-separator">
+ class="note-timestamp system-note-separator"
+ @click="updateTargetNoteHash">
<time-ago-tooltip
:time="createdAt"
tooltip-placement="bottom"
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index e0f883a8e08..bee635398b3 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -1,7 +1,11 @@
<script>
+import _ from 'underscore';
import { mapActions, mapGetters } from 'vuex';
import resolveDiscussionsSvg from 'icons/_icon_mr_issue.svg';
import nextDiscussionsSvg from 'icons/_next_discussion.svg';
+import { convertObjectPropsToCamelCase, scrollToElement } from '~/lib/utils/common_utils';
+import { truncateSha } from '~/lib/utils/text_utility';
+import systemNote from '~/vue_shared/components/notes/system_note.vue';
import Flash from '../../flash';
import { SYSTEM_NOTE } from '../constants';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
@@ -17,9 +21,9 @@ import autosave from '../mixins/autosave';
import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable';
import tooltip from '../../vue_shared/directives/tooltip';
-import { scrollToElement } from '../../lib/utils/common_utils';
export default {
+ name: 'NoteableDiscussion',
components: {
noteableNote,
diffWithNote,
@@ -30,16 +34,32 @@ export default {
noteForm,
placeholderNote,
placeholderSystemNote,
+ systemNote,
},
directives: {
tooltip,
},
mixins: [autosave, noteable, resolvable],
props: {
- note: {
+ discussion: {
type: Object,
required: true,
},
+ renderHeader: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ renderDiffFile: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ alwaysExpanded: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -53,19 +73,27 @@ export default {
'getNoteableData',
'discussionCount',
'resolvedDiscussionCount',
+ 'allDiscussions',
'unresolvedDiscussions',
]),
- discussion() {
+ transformedDiscussion() {
return {
- ...this.note.notes[0],
- truncatedDiffLines: this.note.truncated_diff_lines,
- diffFile: this.note.diff_file,
- diffDiscussion: this.note.diff_discussion,
- imageDiffHtml: this.note.image_diff_html,
+ ...this.discussion.notes[0],
+ truncatedDiffLines: this.discussion.truncated_diff_lines || [],
+ truncatedDiffLinesPath: this.discussion.truncated_diff_lines_path,
+ diffFile: this.discussion.diff_file,
+ diffDiscussion: this.discussion.diff_discussion,
+ imageDiffHtml: this.discussion.image_diff_html,
+ active: this.discussion.active,
+ discussionPath: this.discussion.discussion_path,
+ resolved: this.discussion.resolved,
+ resolvedBy: this.discussion.resolved_by,
+ resolvedByPush: this.discussion.resolved_by_push,
+ resolvedAt: this.discussion.resolved_at,
};
},
author() {
- return this.discussion.author;
+ return this.transformedDiscussion.author;
},
canReply() {
return this.getNoteableData.current_user.can_create_note;
@@ -74,7 +102,7 @@ export default {
return this.getNoteableData.create_note_path;
},
lastUpdatedBy() {
- const { notes } = this.note;
+ const { notes } = this.discussion;
if (notes.length > 1) {
return notes[notes.length - 1].author;
@@ -83,7 +111,7 @@ export default {
return null;
},
lastUpdatedAt() {
- const { notes } = this.note;
+ const { notes } = this.discussion;
if (notes.length > 1) {
return notes[notes.length - 1].created_at;
@@ -91,27 +119,40 @@ export default {
return null;
},
- hasUnresolvedDiscussion() {
- return this.unresolvedDiscussions.length > 0;
+ resolvedText() {
+ return this.transformedDiscussion.resolvedByPush ? 'Automatically resolved' : 'Resolved';
+ },
+ hasMultipleUnresolvedDiscussions() {
+ return this.unresolvedDiscussions.length > 1;
+ },
+ shouldRenderDiffs() {
+ const { diffDiscussion, diffFile } = this.transformedDiscussion;
+
+ return diffDiscussion && diffFile && this.renderDiffFile;
},
wrapperComponent() {
- return this.discussion.diffDiscussion && this.discussion.diffFile
- ? diffWithNote
- : 'div';
+ return this.shouldRenderDiffs ? diffWithNote : 'div';
+ },
+ wrapperComponentProps() {
+ if (this.shouldRenderDiffs) {
+ return { discussion: convertObjectPropsToCamelCase(this.discussion) };
+ }
+
+ return {};
},
wrapperClass() {
- return this.isDiffDiscussion ? '' : 'panel panel-default';
+ return this.isDiffDiscussion ? '' : 'card discussion-wrapper';
},
},
mounted() {
if (this.isReplying) {
- this.initAutoSave(this.discussion.noteable_type);
+ this.initAutoSave(this.transformedDiscussion);
}
},
updated() {
if (this.isReplying) {
if (!this.autosave) {
- this.initAutoSave(this.discussion.noteable_type);
+ this.initAutoSave(this.transformedDiscussion);
} else {
this.setAutoSave();
}
@@ -127,7 +168,9 @@ export default {
'toggleDiscussion',
'removePlaceholderNotes',
'toggleResolveNote',
+ 'expandDiscussion',
]),
+ truncateSha,
componentName(note) {
if (note.isPlaceholderNote) {
if (note.placeholderType === SYSTEM_NOTE) {
@@ -136,23 +179,25 @@ export default {
return placeholderNote;
}
+ if (note.system) {
+ return systemNote;
+ }
+
return noteableNote;
},
componentData(note) {
- return note.isPlaceholderNote ? this.note.notes[0] : note;
+ return note.isPlaceholderNote ? this.discussion.notes[0] : note;
},
toggleDiscussionHandler() {
- this.toggleDiscussion({ discussionId: this.note.id });
+ this.toggleDiscussion({ discussionId: this.discussion.id });
},
showReplyForm() {
this.isReplying = true;
},
cancelReplyForm(shouldConfirm) {
if (shouldConfirm && this.$refs.noteForm.isDirty) {
- const msg = 'Are you sure you want to cancel creating this comment?';
-
// eslint-disable-next-line no-alert
- if (!confirm(msg)) {
+ if (!window.confirm('Are you sure you want to cancel creating this comment?')) {
return;
}
}
@@ -161,18 +206,23 @@ export default {
this.isReplying = false;
},
saveReply(noteText, form, callback) {
+ const postData = {
+ in_reply_to_discussion_id: this.discussion.reply_id,
+ target_type: this.getNoteableData.targetType,
+ note: { note: noteText },
+ };
+
+ if (this.discussion.for_commit) {
+ postData.note_project_id = this.discussion.project_id;
+ }
+
const replyData = {
endpoint: this.newNotePath,
flashContainer: this.$el,
- data: {
- in_reply_to_discussion_id: this.note.reply_id,
- target_type: this.noteableType,
- target_id: this.discussion.noteable_id,
- note: { note: noteText },
- },
+ data: postData,
};
- this.isReplying = false;
+ this.isReplying = false;
this.saveNote(replyData)
.then(() => {
this.resetAutoSave();
@@ -190,15 +240,19 @@ Please check your network connection and try again.`;
});
});
},
- jumpToDiscussion() {
+ jumpToNextDiscussion() {
+ const discussionIds = this.allDiscussions.map(d => d.id);
const unresolvedIds = this.unresolvedDiscussions.map(d => d.id);
- const index = unresolvedIds.indexOf(this.note.id);
+ const currentIndex = discussionIds.indexOf(this.discussion.id);
+ const remainingAfterCurrent = discussionIds.slice(currentIndex + 1);
+ const nextIndex = _.findIndex(remainingAfterCurrent, id => unresolvedIds.indexOf(id) > -1);
- if (index >= 0 && index !== unresolvedIds.length) {
- const nextId = unresolvedIds[index + 1];
+ if (nextIndex > -1) {
+ const nextId = remainingAfterCurrent[nextIndex];
const el = document.querySelector(`[data-discussion-id="${nextId}"]`);
if (el) {
+ this.expandDiscussion({ discussionId: nextId });
scrollToElement(el);
}
}
@@ -208,9 +262,7 @@ Please check your network connection and try again.`;
</script>
<template>
- <li
- :data-discussion-id="note.id"
- class="note note-discussion timeline-entry">
+ <li class="note note-discussion timeline-entry">
<div class="timeline-entry-inner">
<div class="timeline-icon">
<user-avatar-link
@@ -221,20 +273,52 @@ Please check your network connection and try again.`;
/>
</div>
<div class="timeline-content">
- <div class="discussion">
- <div class="discussion-header">
+ <div
+ :data-discussion-id="transformedDiscussion.discussion_id"
+ class="discussion js-discussion-container"
+ >
+ <div
+ v-if="renderHeader"
+ class="discussion-header"
+ >
<note-header
:author="author"
- :created-at="discussion.created_at"
- :note-id="discussion.id"
+ :created-at="transformedDiscussion.created_at"
+ :note-id="transformedDiscussion.id"
:include-toggle="true"
- :expanded="note.expanded"
+ :expanded="discussion.expanded"
@toggleHandler="toggleDiscussionHandler"
- action-text="started a discussion"
- class="discussion"
+ >
+ <template v-if="transformedDiscussion.diffDiscussion">
+ started a discussion on
+ <a :href="transformedDiscussion.discussionPath">
+ <template v-if="transformedDiscussion.active">
+ the diff
+ </template>
+ <template v-else>
+ an old version of the diff
+ </template>
+ </a>
+ </template>
+ <template v-else-if="discussion.for_commit">
+ started a discussion on commit
+ <a :href="discussion.discussion_path">
+ {{ truncateSha(discussion.commit_id) }}
+ </a>
+ </template>
+ <template v-else>
+ started a discussion
+ </template>
+ </note-header>
+ <note-edited-text
+ v-if="transformedDiscussion.resolved"
+ :edited-at="transformedDiscussion.resolvedAt"
+ :edited-by="transformedDiscussion.resolvedBy"
+ :action-text="resolvedText"
+ class-name="discussion-headline-light js-discussion-headline"
/>
<note-edited-text
- v-if="lastUpdatedAt"
+ v-else-if="lastUpdatedAt"
:edited-at="lastUpdatedAt"
:edited-by="lastUpdatedBy"
action-text="Last updated"
@@ -242,17 +326,17 @@ Please check your network connection and try again.`;
/>
</div>
<div
- v-if="note.expanded"
+ v-if="discussion.expanded || alwaysExpanded"
class="discussion-body">
<component
:is="wrapperComponent"
- :discussion="discussion"
+ v-bind="wrapperComponentProps"
:class="wrapperClass"
>
<div class="discussion-notes">
<ul class="notes">
<component
- v-for="note in note.notes"
+ v-for="note in discussion.notes"
:is="componentName(note)"
:note="componentData(note)"
:key="note.id"
@@ -260,28 +344,29 @@ Please check your network connection and try again.`;
</ul>
<div
:class="{ 'is-replying': isReplying }"
- class="discussion-reply-holder">
+ class="discussion-reply-holder"
+ >
<template v-if="!isReplying && canReply">
<div
- class="btn-group-justified discussion-with-resolve-btn"
+ class="btn-group d-flex discussion-with-resolve-btn"
role="group">
<div
- class="btn-group"
+ class="btn-group w-100"
role="group">
<button
- @click="showReplyForm"
type="button"
- class="js-vue-discussion-reply btn btn-text-field"
- title="Add a reply">Reply...</button>
+ class="js-vue-discussion-reply btn btn-text-field mr-2"
+ title="Add a reply"
+ @click="showReplyForm">Reply...</button>
</div>
<div
- v-if="note.resolvable"
+ v-if="discussion.resolvable"
class="btn-group"
role="group">
<button
- @click="resolveHandler()"
type="button"
- class="btn btn-default"
+ class="btn btn-default mr-2"
+ @click="resolveHandler()"
>
<i
v-if="isResolving"
@@ -292,7 +377,7 @@ Please check your network connection and try again.`;
</button>
</div>
<div
- v-if="note.resolvable"
+ v-if="discussion.resolvable"
class="btn-group discussion-actions"
role="group"
>
@@ -301,26 +386,26 @@ Please check your network connection and try again.`;
class="btn-group"
role="group">
<a
- :href="note.resolve_with_issue_path"
v-tooltip
+ :href="discussion.resolve_with_issue_path"
+ :title="s__('MergeRequests|Resolve this discussion in a new issue')"
class="new-issue-for-discussion btn
btn-default discussion-create-issue-btn"
- title="Resolve this discussion in a new issue"
data-container="body"
>
<span v-html="resolveDiscussionsSvg"></span>
</a>
</div>
<div
- v-if="hasUnresolvedDiscussion"
+ v-if="hasMultipleUnresolvedDiscussions"
class="btn-group"
role="group">
<button
- @click="jumpToDiscussion"
v-tooltip
class="btn btn-default discussion-next-btn"
title="Jump to next unresolved discussion"
data-container="body"
+ @click="jumpToNextDiscussion"
>
<span v-html="nextDiscussionsSvg"></span>
</button>
@@ -330,12 +415,12 @@ Please check your network connection and try again.`;
</template>
<note-form
v-if="isReplying"
- save-button-title="Comment"
- :note="note"
+ ref="noteForm"
+ :discussion="discussion"
:is-editing="false"
+ save-button-title="Comment"
@handleFormUpdate="saveReply"
- @cancelFormEdition="cancelReplyForm"
- ref="noteForm" />
+ @cancelForm="cancelReplyForm" />
<note-signed-out-widget v-if="!canReply" />
</div>
</div>
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index 566f5c68e66..4ebeb5599f2 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -12,6 +12,7 @@ import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable';
export default {
+ name: 'NoteableNote',
components: {
userAvatarLink,
noteHeader,
@@ -34,26 +35,31 @@ export default {
};
},
computed: {
- ...mapGetters(['targetNoteHash', 'getUserData']),
+ ...mapGetters(['targetNoteHash', 'getNoteableData', 'getUserData']),
author() {
return this.note.author;
},
classNameBindings() {
return {
+ [`note-row-${this.note.id}`]: true,
'is-editing': this.isEditing && !this.isRequesting,
'is-requesting being-posted': this.isRequesting,
'disabled-content': this.isDeleting,
- target: this.targetNoteHash === this.noteAnchorId,
+ target: this.isTarget,
};
},
+ canResolve() {
+ return this.note.resolvable && !!this.getUserData.id;
+ },
canReportAsAbuse() {
- return (
- this.note.report_abuse_path && this.author.id !== this.getUserData.id
- );
+ return this.note.report_abuse_path && this.author.id !== this.getUserData.id;
},
noteAnchorId() {
return `note_${this.note.id}`;
},
+ isTarget() {
+ return this.targetNoteHash === this.noteAnchorId;
+ },
},
created() {
@@ -65,19 +71,20 @@ export default {
});
},
+ mounted() {
+ if (this.isTarget) {
+ this.scrollToNoteIfNeeded($(this.$el));
+ }
+ },
+
methods: {
- ...mapActions([
- 'deleteNote',
- 'updateNote',
- 'toggleResolveNote',
- 'scrollToNoteIfNeeded',
- ]),
+ ...mapActions(['deleteNote', 'updateNote', 'toggleResolveNote', 'scrollToNoteIfNeeded']),
editHandler() {
this.isEditing = true;
},
deleteHandler() {
// eslint-disable-next-line no-alert
- if (confirm('Are you sure you want to delete this comment?')) {
+ if (window.confirm('Are you sure you want to delete this comment?')) {
this.isDeleting = true;
this.deleteNote(this.note)
@@ -85,9 +92,7 @@ export default {
this.isDeleting = false;
})
.catch(() => {
- Flash(
- 'Something went wrong while deleting your note. Please try again.',
- );
+ Flash('Something went wrong while deleting your note. Please try again.');
this.isDeleting = false;
});
}
@@ -96,7 +101,7 @@ export default {
const data = {
endpoint: this.note.path,
note: {
- target_type: this.noteableType,
+ target_type: this.getNoteableData.targetType,
target_id: this.note.noteable_id,
note: { note: noteText },
},
@@ -118,8 +123,7 @@ export default {
this.isRequesting = false;
this.isEditing = true;
this.$nextTick(() => {
- const msg =
- 'Something went wrong while editing your comment. Please try again.';
+ const msg = 'Something went wrong while editing your comment. Please try again.';
Flash(msg, 'alert', this.$el);
this.recoverNoteContent(noteText);
callback();
@@ -129,8 +133,7 @@ export default {
formCancelHandler(shouldConfirm, isDirty) {
if (shouldConfirm && isDirty) {
// eslint-disable-next-line no-alert
- if (!confirm('Are you sure you want to cancel editing this comment?'))
- return;
+ if (!window.confirm('Are you sure you want to cancel editing this comment?')) return;
}
this.$refs.noteBody.resetAutoSave();
if (this.oldContent) {
@@ -143,7 +146,7 @@ export default {
// 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.note = noteText;
+ this.$refs.noteBody.note.note = noteText;
},
},
};
@@ -151,10 +154,12 @@ export default {
<template>
<li
- class="note timeline-entry"
:id="noteAnchorId"
:class="classNameBindings"
- :data-award-url="note.toggle_award_path">
+ :data-award-url="note.toggle_award_path"
+ :data-note-id="note.id"
+ class="note timeline-entry"
+ >
<div class="timeline-entry-inner">
<div class="timeline-icon">
<user-avatar-link
@@ -170,16 +175,17 @@ export default {
:author="author"
:created-at="note.created_at"
:note-id="note.id"
- action-text="commented"
/>
<note-actions
:author-id="author.id"
:note-id="note.id"
+ :note-url="note.noteable_note_url"
:access-level="note.human_access"
:can-edit="note.current_user.can_edit"
:can-award-emoji="note.current_user.can_award_emoji"
:can-delete="note.current_user.can_edit"
:can-report-as-abuse="canReportAsAbuse"
+ :can-resolve="note.current_user.can_resolve"
:report-abuse-path="note.report_abuse_path"
:resolvable="note.resolvable"
:is-resolved="note.resolved"
@@ -191,12 +197,12 @@ export default {
/>
</div>
<note-body
+ ref="noteBody"
:note="note"
:can-edit="note.current_user.can_edit"
:is-editing="isEditing"
@handleFormUpdate="formUpdateHandler"
- @cancelFormEdition="formCancelHandler"
- ref="noteBody"
+ @cancelForm="formCancelHandler"
/>
</div>
</div>
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index ebfc827ac57..9b8713b40fb 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -1,10 +1,9 @@
<script>
-import $ from 'jquery';
import { mapGetters, mapActions } from 'vuex';
import { getLocationHash } from '../../lib/utils/url_utility';
import Flash from '../../flash';
-import store from '../stores/';
import * as constants from '../constants';
+import eventHub from '../event_hub';
import noteableNote from './noteable_note.vue';
import noteableDiscussion from './noteable_discussion.vue';
import systemNote from '../../vue_shared/components/notes/system_note.vue';
@@ -39,19 +38,28 @@ export default {
required: false,
default: () => ({}),
},
+ shouldShow: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ markdownVersion: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
},
- store,
data() {
return {
isLoading: true,
};
},
computed: {
- ...mapGetters(['notes', 'getNotesDataByProp', 'discussionCount']),
+ ...mapGetters(['isNotesFetched', 'discussions', 'getNotesDataByProp', 'discussionCount']),
noteableType() {
return this.noteableData.noteableType;
},
- allNotes() {
+ allDiscussions() {
if (this.isLoading) {
const totalNotes = parseInt(this.notesData.totalNotes, 10) || 0;
@@ -59,36 +67,40 @@ export default {
isSkeletonNote: true,
});
}
- return this.notes;
+
+ return this.discussions;
+ },
+ },
+ watch: {
+ shouldShow() {
+ if (!this.isNotesFetched) {
+ this.fetchNotes();
+ }
},
},
created() {
this.setNotesData(this.notesData);
this.setNoteableData(this.noteableData);
this.setUserData(this.userData);
+ this.setTargetNoteHash(getLocationHash());
+ eventHub.$once('fetchNotesData', this.fetchNotes);
},
mounted() {
- this.fetchNotes();
-
- const parentElement = this.$el.parentElement;
+ if (this.shouldShow) {
+ this.fetchNotes();
+ }
- if (
- parentElement &&
- parentElement.classList.contains('js-vue-notes-event')
- ) {
+ const { parentElement } = this.$el;
+ if (parentElement && parentElement.classList.contains('js-vue-notes-event')) {
parentElement.addEventListener('toggleAward', event => {
const { awardName, noteId } = event.detail;
this.actionToggleAward({ awardName, noteId });
});
}
- document.addEventListener('refreshVueNotes', this.fetchNotes);
- },
- beforeDestroy() {
- document.removeEventListener('refreshVueNotes', this.fetchNotes);
},
methods: {
...mapActions({
- actionFetchNotes: 'fetchNotes',
+ fetchDiscussions: 'fetchDiscussions',
poll: 'poll',
actionToggleAward: 'toggleAward',
scrollToNoteIfNeeded: 'scrollToNoteIfNeeded',
@@ -97,38 +109,42 @@ export default {
setUserData: 'setUserData',
setLastFetchedAt: 'setLastFetchedAt',
setTargetNoteHash: 'setTargetNoteHash',
+ toggleDiscussion: 'toggleDiscussion',
+ setNotesFetchedState: 'setNotesFetchedState',
}),
- getComponentName(note) {
- if (note.isSkeletonNote) {
+ getComponentName(discussion) {
+ if (discussion.isSkeletonNote) {
return skeletonLoadingContainer;
}
- if (note.isPlaceholderNote) {
- if (note.placeholderType === constants.SYSTEM_NOTE) {
+ if (discussion.isPlaceholderNote) {
+ if (discussion.placeholderType === constants.SYSTEM_NOTE) {
return placeholderSystemNote;
}
return placeholderNote;
- } else if (note.individual_note) {
- return note.notes[0].system ? systemNote : noteableNote;
+ } else if (discussion.individual_note) {
+ return discussion.notes[0].system ? systemNote : noteableNote;
}
return noteableDiscussion;
},
- getComponentData(note) {
- return note.individual_note ? note.notes[0] : note;
+ getComponentData(discussion) {
+ return discussion.individual_note ? { note: discussion.notes[0] } : { discussion };
},
fetchNotes() {
- return this.actionFetchNotes(this.getNotesDataByProp('discussionsPath'))
- .then(() => this.initPolling())
+ return this.fetchDiscussions(this.getNotesDataByProp('discussionsPath'))
+ .then(() => {
+ this.initPolling();
+ })
.then(() => {
this.isLoading = false;
+ this.setNotesFetchedState(true);
})
.then(() => this.$nextTick())
.then(() => this.checkLocationHash())
.catch(() => {
this.isLoading = false;
- Flash(
- 'Something went wrong while fetching comments. Please try again.',
- );
+ this.setNotesFetchedState(true);
+ Flash('Something went wrong while fetching comments. Please try again.');
});
},
initPolling() {
@@ -143,11 +159,19 @@ export default {
},
checkLocationHash() {
const hash = getLocationHash();
- const element = document.getElementById(hash);
+ const noteId = hash && hash.replace(/^note_/, '');
- if (hash && element) {
- this.setTargetNoteHash(hash);
- this.scrollToNoteIfNeeded($(element));
+ if (noteId) {
+ this.discussions.forEach(discussion => {
+ if (discussion.notes) {
+ discussion.notes.forEach(note => {
+ if (`${note.id}` === `${noteId}`) {
+ // FIXME: this modifies the store state without using a mutation/action
+ Object.assign(discussion, { expanded: true });
+ }
+ });
+ }
+ });
}
},
},
@@ -155,21 +179,25 @@ export default {
</script>
<template>
- <div id="notes">
+ <div
+ v-show="shouldShow"
+ id="notes"
+ >
<ul
id="notes-list"
- class="notes main-notes-list timeline">
-
+ class="notes main-notes-list timeline"
+ >
<component
- v-for="note in allNotes"
- :is="getComponentName(note)"
- :note="getComponentData(note)"
- :key="note.id"
+ v-for="discussion in allDiscussions"
+ :is="getComponentName(discussion)"
+ v-bind="getComponentData(discussion)"
+ :key="discussion.id"
/>
</ul>
<comment-form
:noteable-type="noteableType"
+ :markdown-version="markdownVersion"
/>
</div>
</template>
diff --git a/app/assets/javascripts/notes/constants.js b/app/assets/javascripts/notes/constants.js
index c4de4826eda..2c3e07c0506 100644
--- a/app/assets/javascripts/notes/constants.js
+++ b/app/assets/javascripts/notes/constants.js
@@ -11,9 +11,10 @@ export const EMOJI_THUMBSUP = 'thumbsup';
export const EMOJI_THUMBSDOWN = 'thumbsdown';
export const ISSUE_NOTEABLE_TYPE = 'issue';
export const EPIC_NOTEABLE_TYPE = 'epic';
-export const MERGE_REQUEST_NOTEABLE_TYPE = 'merge_request';
+export const MERGE_REQUEST_NOTEABLE_TYPE = 'MergeRequest';
export const UNRESOLVE_NOTE_METHOD_NAME = 'delete';
export const RESOLVE_NOTE_METHOD_NAME = 'post';
+export const DESCRIPTION_TYPE = 'changed the description';
export const NOTEABLE_TYPE_MAPPING = {
Issue: ISSUE_NOTEABLE_TYPE,
diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js
index e4121f151db..6dd4c9d66ac 100644
--- a/app/assets/javascripts/notes/index.js
+++ b/app/assets/javascripts/notes/index.js
@@ -1,46 +1,52 @@
import Vue from 'vue';
import notesApp from './components/notes_app.vue';
+import createStore from './stores';
-document.addEventListener(
- 'DOMContentLoaded',
- () =>
- new Vue({
- el: '#js-vue-notes',
- components: {
- notesApp,
- },
- data() {
- const notesDataset = document.getElementById('js-vue-notes').dataset;
- const parsedUserData = JSON.parse(notesDataset.currentUserData);
- const noteableData = JSON.parse(notesDataset.noteableData);
- let currentUserData = {};
+document.addEventListener('DOMContentLoaded', () => {
+ const store = createStore();
- noteableData.noteableType = notesDataset.noteableType;
+ return new Vue({
+ el: '#js-vue-notes',
+ components: {
+ notesApp,
+ },
+ store,
+ data() {
+ const notesDataset = document.getElementById('js-vue-notes').dataset;
+ const parsedUserData = JSON.parse(notesDataset.currentUserData);
+ const noteableData = JSON.parse(notesDataset.noteableData);
+ const { markdownVersion } = notesDataset;
+ let currentUserData = {};
- if (parsedUserData) {
- currentUserData = {
- id: parsedUserData.id,
- name: parsedUserData.name,
- username: parsedUserData.username,
- avatar_url: parsedUserData.avatar_path || parsedUserData.avatar_url,
- path: parsedUserData.path,
- };
- }
+ noteableData.noteableType = notesDataset.noteableType;
+ noteableData.targetType = notesDataset.targetType;
- return {
- noteableData,
- currentUserData,
- notesData: JSON.parse(notesDataset.notesData),
+ if (parsedUserData) {
+ currentUserData = {
+ id: parsedUserData.id,
+ name: parsedUserData.name,
+ username: parsedUserData.username,
+ avatar_url: parsedUserData.avatar_path || parsedUserData.avatar_url,
+ path: parsedUserData.path,
};
- },
- render(createElement) {
- return createElement('notes-app', {
- props: {
- noteableData: this.noteableData,
- notesData: this.notesData,
- userData: this.currentUserData,
- },
- });
- },
- }),
-);
+ }
+
+ return {
+ noteableData,
+ currentUserData,
+ markdownVersion,
+ notesData: JSON.parse(notesDataset.notesData),
+ };
+ },
+ render(createElement) {
+ return createElement('notes-app', {
+ props: {
+ noteableData: this.noteableData,
+ notesData: this.notesData,
+ userData: this.currentUserData,
+ markdownVersion: this.markdownVersion,
+ },
+ });
+ },
+ });
+});
diff --git a/app/assets/javascripts/notes/mixins/autosave.js b/app/assets/javascripts/notes/mixins/autosave.js
index 3dff715905f..36cc8d5d056 100644
--- a/app/assets/javascripts/notes/mixins/autosave.js
+++ b/app/assets/javascripts/notes/mixins/autosave.js
@@ -4,11 +4,11 @@ import { capitalizeFirstCharacter } from '../../lib/utils/text_utility';
export default {
methods: {
- initAutoSave(noteableType) {
+ initAutoSave(noteable) {
this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), [
'Note',
- capitalizeFirstCharacter(noteableType),
- this.note.id,
+ capitalizeFirstCharacter(noteable.noteable_type),
+ noteable.id,
]);
},
resetAutoSave() {
diff --git a/app/assets/javascripts/notes/mixins/noteable.js b/app/assets/javascripts/notes/mixins/noteable.js
index b68543d71c8..bf1cd6fe5a8 100644
--- a/app/assets/javascripts/notes/mixins/noteable.js
+++ b/app/assets/javascripts/notes/mixins/noteable.js
@@ -1,15 +1,10 @@
import * as constants from '../constants';
export default {
- props: {
- note: {
- type: Object,
- required: true,
- },
- },
computed: {
noteableType() {
- return constants.NOTEABLE_TYPE_MAPPING[this.note.noteable_type];
+ const note = this.discussion ? this.discussion.notes[0] : this.note;
+ return constants.NOTEABLE_TYPE_MAPPING[note.noteable_type];
},
},
};
diff --git a/app/assets/javascripts/notes/mixins/resolvable.js b/app/assets/javascripts/notes/mixins/resolvable.js
index f79049b85f6..cd8394e0619 100644
--- a/app/assets/javascripts/notes/mixins/resolvable.js
+++ b/app/assets/javascripts/notes/mixins/resolvable.js
@@ -2,42 +2,39 @@ import Flash from '~/flash';
import { __ } from '~/locale';
export default {
- props: {
- note: {
- type: Object,
- required: true,
- },
- },
computed: {
discussionResolved() {
- const { notes, resolved } = this.note;
+ if (this.discussion) {
+ const { notes, resolved } = this.discussion;
+
+ if (notes) {
+ // Decide resolved state using store. Only valid for discussions.
+ return notes.filter(note => !note.system).every(note => note.resolved);
+ }
- if (notes) {
- // Decide resolved state using store. Only valid for discussions.
- return notes.every(note => note.resolved && !note.system);
+ return resolved;
}
- return resolved;
+ return this.note.resolved;
},
resolveButtonTitle() {
if (this.updatedNoteBody) {
if (this.discussionResolved) {
- return __('Comment and unresolve discussion');
+ return __('Comment & unresolve discussion');
}
- return __('Comment and resolve discussion');
+ return __('Comment & resolve discussion');
}
- return this.discussionResolved
- ? __('Unresolve discussion')
- : __('Resolve discussion');
+
+ return this.discussionResolved ? __('Unresolve discussion') : __('Resolve discussion');
},
},
methods: {
resolveHandler(resolvedState = false) {
this.isResolving = true;
- const endpoint = this.note.resolve_path || `${this.note.path}/resolve`;
const isResolved = this.discussionResolved || resolvedState;
const discussion = this.resolveAsThread;
+ const endpoint = discussion ? this.discussion.resolve_path : `${this.note.path}/resolve`;
this.toggleResolveNote({ endpoint, isResolved, discussion })
.then(() => {
@@ -45,9 +42,8 @@ export default {
})
.catch(() => {
this.isResolving = false;
- const msg = __(
- 'Something went wrong while resolving this discussion. Please try again.',
- );
+
+ const msg = __('Something went wrong while resolving this discussion. Please try again.');
Flash(msg, 'alert', this.$el);
});
},
diff --git a/app/assets/javascripts/notes/services/notes_service.js b/app/assets/javascripts/notes/services/notes_service.js
index 7c623aac6ed..f5dce94caad 100644
--- a/app/assets/javascripts/notes/services/notes_service.js
+++ b/app/assets/javascripts/notes/services/notes_service.js
@@ -5,7 +5,7 @@ import * as constants from '../constants';
Vue.use(VueResource);
export default {
- fetchNotes(endpoint) {
+ fetchDiscussions(endpoint) {
return Vue.http.get(endpoint);
},
deleteNote(endpoint) {
@@ -22,15 +22,13 @@ export default {
},
toggleResolveNote(endpoint, isResolved) {
const { RESOLVE_NOTE_METHOD_NAME, UNRESOLVE_NOTE_METHOD_NAME } = constants;
- const method = isResolved
- ? UNRESOLVE_NOTE_METHOD_NAME
- : RESOLVE_NOTE_METHOD_NAME;
+ const method = isResolved ? UNRESOLVE_NOTE_METHOD_NAME : RESOLVE_NOTE_METHOD_NAME;
return Vue.http[method](endpoint);
},
poll(data = {}) {
const endpoint = data.notesData.notesPath;
- const lastFetchedAt = data.lastFetchedAt;
+ const { lastFetchedAt } = data;
const options = {
headers: {
'X-Last-Fetched-At': lastFetchedAt ? `${lastFetchedAt}` : undefined,
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 98ce070288e..3eefbe11c37 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import axios from '~/lib/utils/axios_utils';
import Visibility from 'visibilityjs';
import Flash from '../../flash';
import Poll from '../../lib/utils/poll';
@@ -12,27 +13,43 @@ import { isInViewport, scrollToElement } from '../../lib/utils/common_utils';
let eTagPoll;
-export const setNotesData = ({ commit }, data) =>
- commit(types.SET_NOTES_DATA, data);
-export const setNoteableData = ({ commit }, data) =>
- commit(types.SET_NOTEABLE_DATA, data);
-export const setUserData = ({ commit }, data) =>
- commit(types.SET_USER_DATA, data);
-export const setLastFetchedAt = ({ commit }, data) =>
- commit(types.SET_LAST_FETCHED_AT, data);
-export const setInitialNotes = ({ commit }, data) =>
- commit(types.SET_INITIAL_NOTES, data);
-export const setTargetNoteHash = ({ commit }, data) =>
- commit(types.SET_TARGET_NOTE_HASH, data);
-export const toggleDiscussion = ({ commit }, data) =>
- commit(types.TOGGLE_DISCUSSION, data);
-
-export const fetchNotes = ({ commit }, path) =>
+export const expandDiscussion = ({ commit }, data) => commit(types.EXPAND_DISCUSSION, data);
+
+export const collapseDiscussion = ({ commit }, data) => commit(types.COLLAPSE_DISCUSSION, data);
+
+export const setNotesData = ({ commit }, data) => commit(types.SET_NOTES_DATA, data);
+
+export const setNoteableData = ({ commit }, data) => commit(types.SET_NOTEABLE_DATA, data);
+
+export const setUserData = ({ commit }, data) => commit(types.SET_USER_DATA, data);
+
+export const setLastFetchedAt = ({ commit }, data) => commit(types.SET_LAST_FETCHED_AT, data);
+
+export const setInitialNotes = ({ commit }, discussions) =>
+ commit(types.SET_INITIAL_DISCUSSIONS, discussions);
+
+export const setTargetNoteHash = ({ commit }, data) => commit(types.SET_TARGET_NOTE_HASH, data);
+
+export const setNotesFetchedState = ({ commit }, state) =>
+ commit(types.SET_NOTES_FETCHED_STATE, state);
+
+export const toggleDiscussion = ({ commit }, data) => commit(types.TOGGLE_DISCUSSION, data);
+
+export const fetchDiscussions = ({ commit }, path) =>
service
- .fetchNotes(path)
+ .fetchDiscussions(path)
.then(res => res.json())
- .then(res => {
- commit(types.SET_INITIAL_NOTES, res);
+ .then(discussions => {
+ commit(types.SET_INITIAL_DISCUSSIONS, discussions);
+ });
+
+export const refetchDiscussionById = ({ commit }, { path, discussionId }) =>
+ service
+ .fetchDiscussions(path)
+ .then(res => res.json())
+ .then(discussions => {
+ const selectedDiscussion = discussions.find(discussion => discussion.id === discussionId);
+ if (selectedDiscussion) commit(types.UPDATE_DISCUSSION, selectedDiscussion);
});
export const deleteNote = ({ commit }, note) =>
@@ -69,20 +86,14 @@ export const createNewNote = ({ commit }, { endpoint, data }) =>
return res;
});
-export const removePlaceholderNotes = ({ commit }) =>
- commit(types.REMOVE_PLACEHOLDER_NOTES);
+export const removePlaceholderNotes = ({ commit }) => commit(types.REMOVE_PLACEHOLDER_NOTES);
-export const toggleResolveNote = (
- { commit },
- { endpoint, isResolved, discussion },
-) =>
+export const toggleResolveNote = ({ commit }, { endpoint, isResolved, discussion }) =>
service
.toggleResolveNote(endpoint, isResolved)
.then(res => res.json())
.then(res => {
- const mutationType = discussion
- ? types.UPDATE_DISCUSSION
- : types.UPDATE_NOTE;
+ const mutationType = discussion ? types.UPDATE_DISCUSSION : types.UPDATE_NOTE;
commit(mutationType, res);
});
@@ -114,7 +125,7 @@ export const reopenIssue = ({ commit, dispatch, state }) => {
export const toggleStateButtonLoading = ({ commit }, value) =>
commit(types.TOGGLE_STATE_BUTTON_LOADING, value);
-export const emitStateChangedEvent = ({ commit, getters }, data) => {
+export const emitStateChangedEvent = ({ getters }, data) => {
const event = new CustomEvent('issuable_vue_app:change', {
detail: {
data,
@@ -134,7 +145,8 @@ export const toggleIssueLocalState = ({ commit }, newState) => {
};
export const saveNote = ({ commit, dispatch }, noteData) => {
- const { note } = noteData.data.note;
+ // For MR discussuions we need to post as `note[note]` and issue we use `note.note`.
+ const note = noteData.data['note[note]'] || noteData.data.note.note;
let placeholderText = note;
const hasQuickActions = utils.hasQuickActions(placeholderText);
const replyId = noteData.data.in_reply_to_discussion_id;
@@ -179,10 +191,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
loadAwardsHandler()
.then(awardsHandler => {
- awardsHandler.addAwardToEmojiBar(
- votesBlock,
- commandsChanges.emoji_award,
- );
+ awardsHandler.addAwardToEmojiBar(votesBlock, commandsChanges.emoji_award);
awardsHandler.scrollToAwards();
})
.catch(() => {
@@ -194,10 +203,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
});
}
- if (
- commandsChanges.spend_time != null ||
- commandsChanges.time_estimate != null
- ) {
+ if (commandsChanges.spend_time != null || commandsChanges.time_estimate != null) {
sidebarTimeTrackingEventHub.$emit('timeTrackingUpdated', res);
}
}
@@ -211,24 +217,20 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
});
};
-const pollSuccessCallBack = (resp, commit, state, getters) => {
+const pollSuccessCallBack = (resp, commit, state, getters, dispatch) => {
if (resp.notes && resp.notes.length) {
const { notesById } = getters;
resp.notes.forEach(note => {
if (notesById[note.id]) {
commit(types.UPDATE_NOTE, note);
- } else if (
- note.type === constants.DISCUSSION_NOTE ||
- note.type === constants.DIFF_NOTE
- ) {
- const discussion = utils.findNoteObjectById(
- state.notes,
- note.discussion_id,
- );
+ } else if (note.type === constants.DISCUSSION_NOTE || note.type === constants.DIFF_NOTE) {
+ const discussion = utils.findNoteObjectById(state.discussions, note.discussion_id);
if (discussion) {
commit(types.ADD_NEW_REPLY_TO_DISCUSSION, note);
+ } else if (note.type === constants.DIFF_NOTE) {
+ dispatch('fetchDiscussions', state.notesData.discussionsPath);
} else {
commit(types.ADD_NEW_NOTE, note);
}
@@ -243,17 +245,14 @@ const pollSuccessCallBack = (resp, commit, state, getters) => {
return resp;
};
-export const poll = ({ commit, state, getters }) => {
+export const poll = ({ commit, state, getters, dispatch }) => {
eTagPoll = new Poll({
resource: service,
method: 'poll',
data: state,
successCallback: resp =>
- resp
- .json()
- .then(data => pollSuccessCallBack(data, commit, state, getters)),
- errorCallback: () =>
- Flash('Something went wrong while fetching latest comments.'),
+ resp.json().then(data => pollSuccessCallBack(data, commit, state, getters, dispatch)),
+ errorCallback: () => Flash('Something went wrong while fetching latest comments.'),
});
if (!Visibility.hidden()) {
@@ -292,14 +291,11 @@ export const fetchData = ({ commit, state, getters }) => {
.catch(() => Flash('Something went wrong while fetching latest comments.'));
};
-export const toggleAward = (
- { commit, state, getters, dispatch },
- { awardName, noteId },
-) => {
+export const toggleAward = ({ commit, getters }, { awardName, noteId }) => {
commit(types.TOGGLE_AWARD, { awardName, note: getters.notesById[noteId] });
};
-export const toggleAwardRequest = ({ commit, getters, dispatch }, data) => {
+export const toggleAwardRequest = ({ dispatch }, data) => {
const { endpoint, awardName } = data;
return service
@@ -316,5 +312,13 @@ export const scrollToNoteIfNeeded = (context, el) => {
}
};
+export const fetchDiscussionDiffLines = ({ commit }, discussion) =>
+ axios.get(discussion.truncatedDiffLinesPath).then(({ data }) => {
+ commit(types.SET_DISCUSSION_DIFF_LINES, {
+ discussionId: discussion.id,
+ diffLines: data.truncated_diff_lines,
+ });
+ });
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/notes/stores/collapse_utils.js b/app/assets/javascripts/notes/stores/collapse_utils.js
new file mode 100644
index 00000000000..fa4a1c56b20
--- /dev/null
+++ b/app/assets/javascripts/notes/stores/collapse_utils.js
@@ -0,0 +1,108 @@
+import { n__, s__, sprintf } from '~/locale';
+import { DESCRIPTION_TYPE } from '../constants';
+
+/**
+ * Changes the description from a note, returns 'changed the description n number of times'
+ */
+export const changeDescriptionNote = (note, descriptionChangedTimes, timeDifferenceMinutes) => {
+ const descriptionNote = Object.assign({}, note);
+
+ descriptionNote.note_html = sprintf(
+ s__(`MergeRequest|
+ %{paragraphStart}changed the description %{descriptionChangedTimes} times %{timeDifferenceMinutes}%{paragraphEnd}`),
+ {
+ paragraphStart: '<p dir="auto">',
+ paragraphEnd: '</p>',
+ descriptionChangedTimes,
+ timeDifferenceMinutes: n__('within %d minute ', 'within %d minutes ', timeDifferenceMinutes),
+ },
+ false,
+ );
+
+ descriptionNote.times_updated = descriptionChangedTimes;
+
+ return descriptionNote;
+};
+
+/**
+ * Checks the time difference between two notes from their 'created_at' dates
+ * returns an integer
+ */
+
+export const getTimeDifferenceMinutes = (noteBeggining, noteEnd) => {
+ const descriptionNoteBegin = new Date(noteBeggining.created_at);
+ const descriptionNoteEnd = new Date(noteEnd.created_at);
+ const timeDifferenceMinutes = (descriptionNoteEnd - descriptionNoteBegin) / 1000 / 60;
+
+ return Math.ceil(timeDifferenceMinutes);
+};
+
+/**
+ * Checks if a note is a system note and if the content is description
+ *
+ * @param {Object} note
+ * @returns {Boolean}
+ */
+export const isDescriptionSystemNote = note => note.system && note.note === DESCRIPTION_TYPE;
+
+/**
+ * Collapses the system notes of a description type, e.g. Changed the description, n minutes ago
+ * the notes will collapse as long as they happen no more than 10 minutes away from each away
+ * in between the notes can be anything, another type of system note
+ * (such as 'changed the weight') or a comment.
+ *
+ * @param {Array} notes
+ * @returns {Array}
+ */
+export const collapseSystemNotes = notes => {
+ let lastDescriptionSystemNote = null;
+ let lastDescriptionSystemNoteIndex = -1;
+ let descriptionChangedTimes = 1;
+
+ return notes.slice(0).reduce((acc, currentNote) => {
+ const note = currentNote.notes[0];
+
+ if (isDescriptionSystemNote(note)) {
+ // is it the first one?
+ if (!lastDescriptionSystemNote) {
+ lastDescriptionSystemNote = note;
+ lastDescriptionSystemNoteIndex = acc.length;
+ } else if (lastDescriptionSystemNote) {
+ const timeDifferenceMinutes = getTimeDifferenceMinutes(
+ lastDescriptionSystemNote,
+ note,
+ );
+
+ // are they less than 10 minutes appart?
+ if (timeDifferenceMinutes > 10) {
+ // reset counter
+ descriptionChangedTimes = 1;
+ // update the previous system note
+ lastDescriptionSystemNote = note;
+ lastDescriptionSystemNoteIndex = acc.length;
+ } else {
+ // increase counter
+ descriptionChangedTimes += 1;
+
+ // delete the previous one
+ acc.splice(lastDescriptionSystemNoteIndex, 1);
+
+ // replace the text of the current system note with the collapsed note.
+ currentNote.notes.splice(
+ 0,
+ 1,
+ changeDescriptionNote(note, descriptionChangedTimes, timeDifferenceMinutes),
+ );
+
+ // update the previous system note index
+ lastDescriptionSystemNoteIndex = acc.length;
+ }
+ }
+ }
+ acc.push(currentNote);
+ return acc;
+ }, []);
+};
+
+// for babel-rewire
+export default {};
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index 787be6f4c99..5c65e1c3bb5 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -1,58 +1,93 @@
import _ from 'underscore';
+import * as constants from '../constants';
+import { collapseSystemNotes } from './collapse_utils';
+
+export const discussions = state => collapseSystemNotes(state.discussions);
-export const notes = state => state.notes;
export const targetNoteHash = state => state.targetNoteHash;
export const getNotesData = state => state.notesData;
+
+export const isNotesFetched = state => state.isNotesFetched;
+
export const getNotesDataByProp = state => prop => state.notesData[prop];
export const getNoteableData = state => state.noteableData;
+
export const getNoteableDataByProp = state => prop => state.noteableData[prop];
+
export const openState = state => state.noteableData.state;
export const getUserData = state => state.userData || {};
-export const getUserDataByProp = state => prop =>
- state.userData && state.userData[prop];
+
+export const getUserDataByProp = state => prop => state.userData && state.userData[prop];
export const notesById = state =>
- state.notes.reduce((acc, note) => {
+ state.discussions.reduce((acc, note) => {
note.notes.every(n => Object.assign(acc, { [n.id]: n }));
return acc;
}, {});
+export const discussionsByLineCode = state =>
+ state.discussions.reduce((acc, note) => {
+ if (note.diff_discussion && note.line_code && note.resolvable) {
+ // For context about line notes: there might be multiple notes with the same line code
+ const items = acc[note.line_code] || [];
+ items.push(note);
+
+ Object.assign(acc, { [note.line_code]: items });
+ }
+ return acc;
+ }, {});
+
+export const noteableType = state => {
+ const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE, EPIC_NOTEABLE_TYPE } = constants;
+
+ if (state.noteableData.noteableType === EPIC_NOTEABLE_TYPE) {
+ return EPIC_NOTEABLE_TYPE;
+ }
+
+ return state.noteableData.merge_params ? MERGE_REQUEST_NOTEABLE_TYPE : ISSUE_NOTEABLE_TYPE;
+};
+
const reverseNotes = array => array.slice(0).reverse();
+
const isLastNote = (note, state) =>
- !note.system &&
- state.userData &&
- note.author &&
- note.author.id === state.userData.id;
+ !note.system && state.userData && note.author && note.author.id === state.userData.id;
export const getCurrentUserLastNote = state =>
- _.flatten(
- reverseNotes(state.notes).map(note => reverseNotes(note.notes)),
- ).find(el => isLastNote(el, state));
+ _.flatten(reverseNotes(state.discussions).map(note => reverseNotes(note.notes))).find(el =>
+ isLastNote(el, state),
+ );
export const getDiscussionLastNote = state => discussion =>
reverseNotes(discussion.notes).find(el => isLastNote(el, state));
export const discussionCount = state => {
- const discussions = state.notes.filter(n => !n.individual_note);
+ const filteredDiscussions = state.discussions.filter(n => !n.individual_note && n.resolvable);
- return discussions.length;
+ return filteredDiscussions.length;
};
export const unresolvedDiscussions = (state, getters) => {
const resolvedMap = getters.resolvedDiscussionsById;
- return state.notes.filter(n => !n.individual_note && !resolvedMap[n.id]);
+ return state.discussions.filter(n => !n.individual_note && !resolvedMap[n.id]);
+};
+
+export const allDiscussions = (state, getters) => {
+ const resolved = getters.resolvedDiscussionsById;
+ const unresolved = getters.unresolvedDiscussions;
+
+ return Object.values(resolved).concat(unresolved);
};
export const resolvedDiscussionsById = state => {
const map = {};
- state.notes.forEach(n => {
+ state.discussions.filter(d => d.resolvable).forEach(n => {
if (n.notes) {
- const resolved = n.notes.every(note => note.resolved && !note.system);
+ const resolved = n.notes.filter(note => note.resolvable).every(note => note.resolved);
if (resolved) {
map[n.id] = n;
@@ -69,5 +104,15 @@ export const resolvedDiscussionCount = (state, getters) => {
return Object.keys(resolvedMap).length;
};
+export const discussionTabCounter = state => {
+ let all = [];
+
+ state.discussions.forEach(discussion => {
+ all = all.concat(discussion.notes.filter(note => !note.system && !note.placeholder));
+ });
+
+ return all.length;
+};
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/notes/stores/index.js b/app/assets/javascripts/notes/stores/index.js
index 9ed19bf171e..0f48b8880f4 100644
--- a/app/assets/javascripts/notes/stores/index.js
+++ b/app/assets/javascripts/notes/stores/index.js
@@ -3,24 +3,14 @@ import Vuex from 'vuex';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
+import module from './modules';
Vue.use(Vuex);
-export default new Vuex.Store({
- state: {
- notes: [],
- targetNoteHash: null,
- lastFetchedAt: null,
-
- // View layer
- isToggleStateButtonLoading: false,
-
- // holds endpoints and permissions provided through haml
- notesData: {},
- userData: {},
- noteableData: {},
- },
- actions,
- getters,
- mutations,
-});
+export default () =>
+ new Vuex.Store({
+ state: module.state,
+ actions,
+ getters,
+ mutations,
+ });
diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js
new file mode 100644
index 00000000000..b4cb9267e0f
--- /dev/null
+++ b/app/assets/javascripts/notes/stores/modules/index.js
@@ -0,0 +1,27 @@
+import * as actions from '../actions';
+import * as getters from '../getters';
+import mutations from '../mutations';
+
+export default {
+ state: {
+ discussions: [],
+ targetNoteHash: null,
+ lastFetchedAt: null,
+
+ // View layer
+ isToggleStateButtonLoading: false,
+ isNotesFetched: false,
+
+ // holds endpoints and permissions provided through haml
+ notesData: {
+ markdownDocsPath: '',
+ },
+ userData: {},
+ noteableData: {
+ current_user: {},
+ },
+ },
+ actions,
+ getters,
+ mutations,
+};
diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js
index b455e23ecde..6f374f78691 100644
--- a/app/assets/javascripts/notes/stores/mutation_types.js
+++ b/app/assets/javascripts/notes/stores/mutation_types.js
@@ -5,14 +5,20 @@ export const REMOVE_PLACEHOLDER_NOTES = 'REMOVE_PLACEHOLDER_NOTES';
export const SET_NOTES_DATA = 'SET_NOTES_DATA';
export const SET_NOTEABLE_DATA = 'SET_NOTEABLE_DATA';
export const SET_USER_DATA = 'SET_USER_DATA';
-export const SET_INITIAL_NOTES = 'SET_INITIAL_NOTES';
+export const SET_INITIAL_DISCUSSIONS = 'SET_INITIAL_DISCUSSIONS';
export const SET_LAST_FETCHED_AT = 'SET_LAST_FETCHED_AT';
export const SET_TARGET_NOTE_HASH = 'SET_TARGET_NOTE_HASH';
export const SHOW_PLACEHOLDER_NOTE = 'SHOW_PLACEHOLDER_NOTE';
export const TOGGLE_AWARD = 'TOGGLE_AWARD';
-export const TOGGLE_DISCUSSION = 'TOGGLE_DISCUSSION';
export const UPDATE_NOTE = 'UPDATE_NOTE';
export const UPDATE_DISCUSSION = 'UPDATE_DISCUSSION';
+export const SET_DISCUSSION_DIFF_LINES = 'SET_DISCUSSION_DIFF_LINES';
+export const SET_NOTES_FETCHED_STATE = 'SET_NOTES_FETCHED_STATE';
+
+// DISCUSSION
+export const COLLAPSE_DISCUSSION = 'COLLAPSE_DISCUSSION';
+export const EXPAND_DISCUSSION = 'EXPAND_DISCUSSION';
+export const TOGGLE_DISCUSSION = 'TOGGLE_DISCUSSION';
// Issue
export const CLOSE_ISSUE = 'CLOSE_ISSUE';
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index c8edc06349f..ab6a95e2601 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -6,8 +6,8 @@ import { isInMRPage } from '../../lib/utils/common_utils';
export default {
[types.ADD_NEW_NOTE](state, note) {
const { discussion_id, type } = note;
- const [exists] = state.notes.filter(n => n.id === note.discussion_id);
- const isDiscussion = type === constants.DISCUSSION_NOTE;
+ const [exists] = state.discussions.filter(n => n.id === note.discussion_id);
+ const isDiscussion = type === constants.DISCUSSION_NOTE || type === constants.DIFF_NOTE;
if (!exists) {
const noteData = {
@@ -25,42 +25,49 @@ export default {
noteData.resolve_with_issue_path = note.resolve_with_issue_path;
}
- state.notes.push(noteData);
- document.dispatchEvent(new CustomEvent('refreshLegacyNotes'));
+ state.discussions.push(noteData);
}
},
[types.ADD_NEW_REPLY_TO_DISCUSSION](state, note) {
- const noteObj = utils.findNoteObjectById(state.notes, note.discussion_id);
+ const noteObj = utils.findNoteObjectById(state.discussions, note.discussion_id);
if (noteObj) {
noteObj.notes.push(note);
- document.dispatchEvent(new CustomEvent('refreshLegacyNotes'));
}
},
[types.DELETE_NOTE](state, note) {
- const noteObj = utils.findNoteObjectById(state.notes, note.discussion_id);
+ const noteObj = utils.findNoteObjectById(state.discussions, note.discussion_id);
if (noteObj.individual_note) {
- state.notes.splice(state.notes.indexOf(noteObj), 1);
+ state.discussions.splice(state.discussions.indexOf(noteObj), 1);
} else {
const comment = utils.findNoteObjectById(noteObj.notes, note.id);
noteObj.notes.splice(noteObj.notes.indexOf(comment), 1);
if (!noteObj.notes.length) {
- state.notes.splice(state.notes.indexOf(noteObj), 1);
+ state.discussions.splice(state.discussions.indexOf(noteObj), 1);
}
}
+ },
+
+ [types.EXPAND_DISCUSSION](state, { discussionId }) {
+ const discussion = utils.findNoteObjectById(state.discussions, discussionId);
+
+ discussion.expanded = true;
+ },
- document.dispatchEvent(new CustomEvent('refreshLegacyNotes'));
+ [types.COLLAPSE_DISCUSSION](state, { discussionId }) {
+ const discussion = utils.findNoteObjectById(state.discussions, discussionId);
+ discussion.expanded = false;
},
[types.REMOVE_PLACEHOLDER_NOTES](state) {
- const { notes } = state;
+ const { discussions } = state;
- for (let i = notes.length - 1; i >= 0; i -= 1) {
- const note = notes[i];
+ for (let i = discussions.length - 1; i >= 0; i -= 1) {
+ const note = discussions[i];
const children = note.notes;
if (children.length && !note.individual_note) {
@@ -72,7 +79,7 @@ export default {
}
} else if (note.isPlaceholderNote) {
// remove placeholders from state root
- notes.splice(i, 1);
+ discussions.splice(i, 1);
}
}
},
@@ -88,31 +95,30 @@ export default {
[types.SET_USER_DATA](state, data) {
Object.assign(state, { userData: data });
},
- [types.SET_INITIAL_NOTES](state, notesData) {
- const notes = [];
+ [types.SET_INITIAL_DISCUSSIONS](state, discussionsData) {
+ const discussions = [];
- notesData.forEach(note => {
+ discussionsData.forEach(discussion => {
// To support legacy notes, should be very rare case.
- if (note.individual_note && note.notes.length > 1) {
- note.notes.forEach(n => {
- notes.push({
- ...note,
+ if (discussion.individual_note && discussion.notes.length > 1) {
+ discussion.notes.forEach(n => {
+ discussions.push({
+ ...discussion,
notes: [n], // override notes array to only have one item to mimick individual_note
});
});
} else {
- const oldNote = utils.findNoteObjectById(state.notes, note.id);
+ const oldNote = utils.findNoteObjectById(state.discussions, discussion.id);
- notes.push({
- ...note,
- expanded: oldNote ? oldNote.expanded : note.expanded,
+ discussions.push({
+ ...discussion,
+ expanded: oldNote ? oldNote.expanded : discussion.expanded,
});
}
});
- Object.assign(state, { notes });
+ Object.assign(state, { discussions });
},
-
[types.SET_LAST_FETCHED_AT](state, fetchedAt) {
Object.assign(state, { lastFetchedAt: fetchedAt });
},
@@ -122,17 +128,17 @@ export default {
},
[types.SHOW_PLACEHOLDER_NOTE](state, data) {
- let notesArr = state.notes;
- if (data.replyId) {
- notesArr = utils.findNoteObjectById(notesArr, data.replyId).notes;
+ let notesArr = state.discussions;
+
+ const existingDiscussion = utils.findNoteObjectById(notesArr, data.replyId);
+ if (existingDiscussion) {
+ notesArr = existingDiscussion.notes;
}
notesArr.push({
individual_note: true,
isPlaceholderNote: true,
- placeholderType: data.isSystemNote
- ? constants.SYSTEM_NOTE
- : constants.NOTE,
+ placeholderType: data.isSystemNote ? constants.SYSTEM_NOTE : constants.NOTE,
notes: [
{
body: data.noteBody,
@@ -151,28 +157,23 @@ export default {
if (hasEmojiAwardedByCurrentUser.length) {
// If current user has awarded this emoji, remove it.
- note.award_emoji.splice(
- note.award_emoji.indexOf(hasEmojiAwardedByCurrentUser[0]),
- 1,
- );
+ note.award_emoji.splice(note.award_emoji.indexOf(hasEmojiAwardedByCurrentUser[0]), 1);
} else {
note.award_emoji.push({
name: awardName,
user: { id, name, username },
});
}
-
- document.dispatchEvent(new CustomEvent('refreshLegacyNotes'));
},
[types.TOGGLE_DISCUSSION](state, { discussionId }) {
- const discussion = utils.findNoteObjectById(state.notes, discussionId);
+ const discussion = utils.findNoteObjectById(state.discussions, discussionId);
discussion.expanded = !discussion.expanded;
},
[types.UPDATE_NOTE](state, note) {
- const noteObj = utils.findNoteObjectById(state.notes, note.discussion_id);
+ const noteObj = utils.findNoteObjectById(state.discussions, note.discussion_id);
if (noteObj.individual_note) {
noteObj.notes.splice(0, 1, note);
@@ -180,24 +181,20 @@ export default {
const comment = utils.findNoteObjectById(noteObj.notes, note.id);
noteObj.notes.splice(noteObj.notes.indexOf(comment), 1, note);
}
-
- // document.dispatchEvent(new CustomEvent('refreshLegacyNotes'));
},
[types.UPDATE_DISCUSSION](state, noteData) {
const note = noteData;
let index = 0;
- state.notes.forEach((n, i) => {
+ state.discussions.forEach((n, i) => {
if (n.id === note.id) {
index = i;
}
});
note.expanded = true; // override expand flag to prevent collapse
- state.notes.splice(index, 1, note);
-
- document.dispatchEvent(new CustomEvent('refreshLegacyNotes'));
+ state.discussions.splice(index, 1, note);
},
[types.CLOSE_ISSUE](state) {
@@ -211,4 +208,19 @@ export default {
[types.TOGGLE_STATE_BUTTON_LOADING](state, value) {
Object.assign(state, { isToggleStateButtonLoading: value });
},
+
+ [types.SET_NOTES_FETCHED_STATE](state, value) {
+ Object.assign(state, { isNotesFetched: value });
+ },
+
+ [types.SET_DISCUSSION_DIFF_LINES](state, { discussionId, diffLines }) {
+ const discussion = utils.findNoteObjectById(state.discussions, discussionId);
+ const index = state.discussions.indexOf(discussion);
+
+ const discussionWithDiffLines = Object.assign({}, discussion, {
+ truncated_diff_lines: diffLines,
+ });
+
+ state.discussions.splice(index, 1, discussionWithDiffLines);
+ },
};
diff --git a/app/assets/javascripts/notifications_form.js b/app/assets/javascripts/notifications_form.js
index 9e6cf67dff0..00e27ca0e70 100644
--- a/app/assets/javascripts/notifications_form.js
+++ b/app/assets/javascripts/notifications_form.js
@@ -15,7 +15,7 @@ export default class NotificationsForm {
toggleCheckbox(e) {
const $checkbox = $(e.currentTarget);
- const $parent = $checkbox.closest('.checkbox');
+ const $parent = $checkbox.closest('.form-check');
this.saveEvent($checkbox, $parent);
}
diff --git a/app/assets/javascripts/pages/admin/admin.js b/app/assets/javascripts/pages/admin/admin.js
index 91f154b7ecd..ff4d6ab15f9 100644
--- a/app/assets/javascripts/pages/admin/admin.js
+++ b/app/assets/javascripts/pages/admin/admin.js
@@ -25,7 +25,7 @@ export default function adminInit() {
$('body').on('click', '.js-toggle-colors-link', (e) => {
e.preventDefault();
- $('.js-toggle-colors-container').toggle();
+ $('.js-toggle-colors-container').toggleClass('hide');
});
$('.log-tabs a').on('click', function logTabsClick(e) {
diff --git a/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue b/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue
index ba1d8e4d8db..bc84666779e 100644
--- a/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue
+++ b/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue
@@ -40,8 +40,8 @@
<gl-modal
id="stop-jobs-modal"
:header-title-text="s__('AdminArea|Stop all jobs?')"
- footer-primary-button-variant="danger"
:footer-primary-button-text="s__('AdminArea|Stop jobs')"
+ footer-primary-button-variant="danger"
@submit="onSubmit"
>
{{ text }}
diff --git a/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue
index 343c65edb37..ff66d3a8ac4 100644
--- a/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue
+++ b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue
@@ -83,9 +83,9 @@
id="delete-project-modal"
:title="title"
:text="text"
- kind="danger"
:primary-button-label="primaryButtonLabel"
:submit-disabled="!canSubmit"
+ kind="danger"
@submit="onSubmit"
@cancel="onCancel"
>
@@ -107,15 +107,15 @@
value="delete"
/>
<input
+ :value="csrfToken"
type="hidden"
name="authenticity_token"
- :value="csrfToken"
/>
<input
+ v-model="enteredProjectName"
name="projectName"
class="form-control"
type="text"
- v-model="enteredProjectName"
aria-labelledby="input-label"
autocomplete="off"
/>
diff --git a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
index 9ce176744ba..d6aa4bb95d2 100644
--- a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
+++ b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
@@ -96,7 +96,7 @@
this.enteredUsername = '';
},
onSecondaryAction() {
- const form = this.$refs.form;
+ const { form } = this.$refs;
form.action = this.blockUserUrl;
this.$refs.method.value = 'put';
@@ -116,10 +116,10 @@
id="delete-user-modal"
:title="title"
:text="text"
- kind="danger"
:primary-button-label="primaryButtonLabel"
:secondary-button-label="secondaryButtonLabel"
:submit-disabled="!canSubmit"
+ kind="danger"
@submit="onSubmit"
@cancel="onCancel"
>
@@ -141,15 +141,15 @@
value="delete"
/>
<input
+ :value="csrfToken"
type="hidden"
name="authenticity_token"
- :value="csrfToken"
/>
<input
+ v-model="enteredUsername"
type="text"
name="username"
class="form-control"
- v-model="enteredUsername"
aria-labelledby="input-label"
autocomplete="off"
/>
@@ -160,11 +160,11 @@
slot-scope="props"
>
<button
+ :disabled="!canSubmit"
type="button"
class="btn js-secondary-button btn-warning"
- :disabled="!canSubmit"
- @click="onSecondaryAction"
data-dismiss="modal"
+ @click="onSecondaryAction"
>
{{ secondaryButtonLabel }}
</button>
diff --git a/app/assets/javascripts/pages/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
index c334eaa90f8..ff19b9a9c30 100644
--- a/app/assets/javascripts/pages/dashboard/todos/index/todos.js
+++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
@@ -1,4 +1,4 @@
-/* eslint-disable class-methods-use-this, no-unneeded-ternary, quote-props */
+/* eslint-disable class-methods-use-this, no-unneeded-ternary */
import $ from 'jquery';
import { visitUrl } from '~/lib/utils/url_utility';
@@ -61,7 +61,7 @@ export default class Todos {
e.stopPropagation();
e.preventDefault();
- const target = e.target;
+ const { target } = e;
target.setAttribute('disabled', true);
target.classList.add('disabled');
diff --git a/app/assets/javascripts/pages/groups/edit/index.js b/app/assets/javascripts/pages/groups/edit/index.js
index bb91ac84ffb..8737f537296 100644
--- a/app/assets/javascripts/pages/groups/edit/index.js
+++ b/app/assets/javascripts/pages/groups/edit/index.js
@@ -1,9 +1,15 @@
import groupAvatar from '~/group_avatar';
import TransferDropdown from '~/groups/transfer_dropdown';
import initConfirmDangerModal from '~/confirm_danger_modal';
+import initSettingsPanels from '~/settings_panels';
document.addEventListener('DOMContentLoaded', () => {
groupAvatar();
new TransferDropdown(); // eslint-disable-line no-new
initConfirmDangerModal();
});
+
+document.addEventListener('DOMContentLoaded', () => {
+ // Initialize expandable settings panels
+ initSettingsPanels();
+});
diff --git a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue
index 16f792d635a..4061c11ba8f 100644
--- a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue
+++ b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue
@@ -96,8 +96,8 @@ Once deleted, it cannot be undone or recovered.`),
id="delete-milestone-modal"
:title="title"
:text="text"
- kind="danger"
:primary-button-label="s__('Milestones|Delete milestone')"
+ kind="danger"
@submit="onSubmit">
<template
diff --git a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
index 2bda2aeb3a1..2c683a39f42 100644
--- a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
+++ b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
@@ -53,8 +53,8 @@
<template>
<gl-modal
id="promote-milestone-modal"
- footer-primary-button-variant="warning"
:footer-primary-button-text="s__('Milestones|Promote Milestone')"
+ footer-primary-button-variant="warning"
@submit="onSubmit"
>
<template
diff --git a/app/assets/javascripts/pages/profiles/keys/index.js b/app/assets/javascripts/pages/profiles/keys/index.js
new file mode 100644
index 00000000000..1cd3ee1dfdb
--- /dev/null
+++ b/app/assets/javascripts/pages/profiles/keys/index.js
@@ -0,0 +1,16 @@
+import AddSshKeyValidation from '~/profile/add_ssh_key_validation';
+
+document.addEventListener('DOMContentLoaded', () => {
+ const input = document.querySelector('.js-add-ssh-key-validation-input');
+ const warning = document.querySelector('.js-add-ssh-key-validation-warning');
+ const originalSubmit = input.form.querySelector('.js-add-ssh-key-validation-original-submit');
+ const confirmSubmit = warning.querySelector('.js-add-ssh-key-validation-confirm-submit');
+
+ const addSshKeyValidation = new AddSshKeyValidation(
+ input,
+ warning,
+ originalSubmit,
+ confirmSubmit,
+ );
+ addSshKeyValidation.register();
+});
diff --git a/app/assets/javascripts/pages/profiles/two_factor_auths/index.js b/app/assets/javascripts/pages/profiles/two_factor_auths/index.js
index fbdef329ab2..8e8f47c21d8 100644
--- a/app/assets/javascripts/pages/profiles/two_factor_auths/index.js
+++ b/app/assets/javascripts/pages/profiles/two_factor_auths/index.js
@@ -5,7 +5,7 @@ document.addEventListener('DOMContentLoaded', () => {
const twoFactorNode = document.querySelector('.js-two-factor-auth');
const skippable = twoFactorNode.dataset.twoFactorSkippable === 'true';
if (skippable) {
- const button = `<a class="btn btn-xs btn-warning pull-right" data-method="patch" href="${twoFactorNode.dataset.two_factor_skip_url}">Configure it later</a>`;
+ const button = `<a class="btn btn-sm btn-warning float-right" data-method="patch" href="${twoFactorNode.dataset.two_factor_skip_url}">Configure it later</a>`;
const flashAlert = document.querySelector('.flash-alert .container-fluid');
if (flashAlert) flashAlert.insertAdjacentHTML('beforeend', button);
}
diff --git a/app/assets/javascripts/pages/projects/clusters/gcp/login/index.js b/app/assets/javascripts/pages/projects/clusters/gcp/login/index.js
deleted file mode 100644
index 0c2d7d7c96a..00000000000
--- a/app/assets/javascripts/pages/projects/clusters/gcp/login/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import gcpSignupOffer from '~/clusters/components/gcp_signup_offer';
-
-gcpSignupOffer();
diff --git a/app/assets/javascripts/pages/projects/clusters/gcp/new/index.js b/app/assets/javascripts/pages/projects/clusters/gcp/new/index.js
new file mode 100644
index 00000000000..d4f34e32a48
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/clusters/gcp/new/index.js
@@ -0,0 +1,5 @@
+import initGkeDropdowns from '~/projects/gke_cluster_dropdowns';
+
+document.addEventListener('DOMContentLoaded', () => {
+ initGkeDropdowns();
+});
diff --git a/app/assets/javascripts/pages/projects/clusters/new/index.js b/app/assets/javascripts/pages/projects/clusters/new/index.js
deleted file mode 100644
index 0c2d7d7c96a..00000000000
--- a/app/assets/javascripts/pages/projects/clusters/new/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import gcpSignupOffer from '~/clusters/components/gcp_signup_offer';
-
-gcpSignupOffer();
diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js
index 653e2502d01..6c1788dc160 100644
--- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js
+++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, camelcase, one-var-declaration-per-line, quotes, no-param-reassign, quote-props, comma-dangle, prefer-template, max-len, no-return-assign, no-shadow */
+/* eslint-disable func-names, wrap-iife, no-var, one-var, camelcase, one-var-declaration-per-line, quotes, no-param-reassign, quote-props, comma-dangle, prefer-template, max-len, no-return-assign */
import $ from 'jquery';
import _ from 'underscore';
@@ -36,7 +36,9 @@ export default (function() {
var author_graph, author_header;
author_header = _this.create_author_header(d);
$(".contributors-list").append(author_header);
- _this.authors[d.author_name] = author_graph = new ContributorsAuthorGraph(d.dates);
+
+ author_graph = new ContributorsAuthorGraph(d.dates);
+ _this.authors[d.author_name] = author_graph;
return author_graph.draw();
};
})(this));
@@ -78,10 +80,11 @@ export default (function() {
};
ContributorsStatGraph.prototype.redraw_authors = function() {
- var author_commits, x_domain;
$("ol").html("");
- x_domain = ContributorsGraph.prototype.x_domain;
- author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field, x_domain);
+
+ const { x_domain } = ContributorsGraph.prototype;
+ const author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field, x_domain);
+
return _.each(author_commits, (function(_this) {
return function(d) {
_this.redraw_author_commit_info(d);
@@ -100,7 +103,7 @@ export default (function() {
};
ContributorsStatGraph.prototype.change_date_header = function() {
- const x_domain = ContributorsGraph.prototype.x_domain;
+ const { x_domain } = ContributorsGraph.prototype;
const formattedDateRange = sprintf(
s__('ContributorsPage|%{startDate} – %{endDate}'),
{
diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js
index 5316d3e9f3c..a02ec9e5f00 100644
--- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js
+++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, newline-per-chained-call, no-else-return, no-shadow */
+/* eslint-disable func-names, max-len, no-restricted-syntax, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, newline-per-chained-call, no-else-return, no-shadow */
import $ from 'jquery';
import _ from 'underscore';
diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js
index 77135ad1f0e..d12249bf612 100644
--- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js
+++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, object-shorthand, no-var, one-var, camelcase, one-var-declaration-per-line, comma-dangle, no-param-reassign, no-return-assign, quotes, prefer-arrow-callback, wrap-iife, consistent-return, no-unused-vars, max-len, no-cond-assign, no-else-return, max-len */
+/* eslint-disable func-names, object-shorthand, no-var, one-var, camelcase, one-var-declaration-per-line, comma-dangle, no-param-reassign, no-return-assign, quotes, prefer-arrow-callback, wrap-iife, consistent-return, no-unused-vars, max-len, no-cond-assign, no-else-return, max-len */
import _ from 'underscore';
export default {
@@ -111,10 +111,15 @@ export default {
parse_log_entry: function(log_entry, field, date_range) {
var parsed_entry;
parsed_entry = {};
+
parsed_entry.author_name = log_entry.author_name;
parsed_entry.author_email = log_entry.author_email;
parsed_entry.dates = {};
- parsed_entry.commits = parsed_entry.additions = parsed_entry.deletions = 0;
+
+ parsed_entry.commits = 0;
+ parsed_entry.additions = 0;
+ parsed_entry.deletions = 0;
+
_.each(_.omit(log_entry, 'author_name', 'author_email'), (function(_this) {
return function(value, key) {
if (_this.in_range(value.date, date_range)) {
diff --git a/app/assets/javascripts/pages/projects/index.js b/app/assets/javascripts/pages/projects/index.js
index de1e13de7e9..cc0e6553e83 100644
--- a/app/assets/javascripts/pages/projects/index.js
+++ b/app/assets/javascripts/pages/projects/index.js
@@ -1,7 +1,21 @@
+import gcpSignupOffer from '~/clusters/components/gcp_signup_offer';
+import initGkeDropdowns from '~/projects/gke_cluster_dropdowns';
import Project from './project';
import ShortcutsNavigation from '../../shortcuts_navigation';
document.addEventListener('DOMContentLoaded', () => {
+ const { page } = document.body.dataset;
+ const newClusterViews = [
+ 'projects:clusters:new',
+ 'projects:clusters:create_gcp',
+ 'projects:clusters:create_user',
+ ];
+
+ if (newClusterViews.indexOf(page) > -1) {
+ gcpSignupOffer();
+ initGkeDropdowns();
+ }
+
new Project(); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
});
diff --git a/app/assets/javascripts/pages/projects/init_blob.js b/app/assets/javascripts/pages/projects/init_blob.js
index 82143fa875a..56ab3fcdfcb 100644
--- a/app/assets/javascripts/pages/projects/init_blob.js
+++ b/app/assets/javascripts/pages/projects/init_blob.js
@@ -8,7 +8,8 @@ import initBlobBundle from '~/blob_edit/blob_bundle';
export default () => {
new LineHighlighter(); // eslint-disable-line no-new
- new BlobLinePermalinkUpdater( // eslint-disable-line no-new
+ // eslint-disable-next-line no-new
+ new BlobLinePermalinkUpdater(
document.querySelector('#blob-content-holder'),
'.diff-line-num[data-line-number]',
document.querySelectorAll('.js-data-file-blob-permalink-url, .js-blob-blame-link'),
@@ -19,12 +20,13 @@ export default () => {
new ShortcutsNavigation(); // eslint-disable-line no-new
- new ShortcutsBlob({ // eslint-disable-line no-new
+ // eslint-disable-next-line no-new
+ new ShortcutsBlob({
skipResetBindings: true,
fileBlobPermalinkUrl,
});
- new BlobForkSuggestion({ // eslint-disable-line no-new
+ new BlobForkSuggestion({
openButtons: document.querySelectorAll('.js-edit-blob-link-fork-toggler'),
forkButtons: document.querySelectorAll('.js-fork-suggestion-button'),
cancelButtons: document.querySelectorAll('.js-cancel-fork-suggestion-button'),
diff --git a/app/assets/javascripts/pages/projects/init_form.js b/app/assets/javascripts/pages/projects/init_form.js
index 0b6c5c1d30b..9f20a3e4e46 100644
--- a/app/assets/javascripts/pages/projects/init_form.js
+++ b/app/assets/javascripts/pages/projects/init_form.js
@@ -3,5 +3,5 @@ import GLForm from '~/gl_form';
export default function ($formEl) {
new ZenMode(); // eslint-disable-line no-new
- new GLForm($formEl, true); // eslint-disable-line no-new
+ new GLForm($formEl); // eslint-disable-line no-new
}
diff --git a/app/assets/javascripts/pages/projects/issues/form.js b/app/assets/javascripts/pages/projects/issues/form.js
index 14fddbc9a05..b2b8e5d2300 100644
--- a/app/assets/javascripts/pages/projects/issues/form.js
+++ b/app/assets/javascripts/pages/projects/issues/form.js
@@ -10,7 +10,7 @@ import IssuableTemplateSelectors from '~/templates/issuable_template_selectors';
export default () => {
new ShortcutsNavigation();
- new GLForm($('.issue-form'), true);
+ new GLForm($('.issue-form'));
new IssuableForm($('.issue-form'));
new LabelsSelect();
new MilestoneSelect();
diff --git a/app/assets/javascripts/pages/projects/jobs/terminal/index.js b/app/assets/javascripts/pages/projects/jobs/terminal/index.js
new file mode 100644
index 00000000000..7129e24cee1
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/jobs/terminal/index.js
@@ -0,0 +1,3 @@
+import initTerminal from '~/terminal/';
+
+document.addEventListener('DOMContentLoaded', initTerminal);
diff --git a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
index ad6df51bb7a..5d2247f6c6d 100644
--- a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
+++ b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
@@ -71,8 +71,8 @@
<template>
<gl-modal
id="promote-label-modal"
- footer-primary-button-variant="warning"
:footer-primary-button-text="s__('Labels|Promote Label')"
+ footer-primary-button-variant="warning"
@submit="onSubmit"
>
<div
diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js
index 406fc32f9a2..3a3c21f2202 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js
@@ -12,7 +12,7 @@ import IssuableTemplateSelectors from '~/templates/issuable_template_selectors';
export default () => {
new Diff();
new ShortcutsNavigation();
- new GLForm($('.merge-request-form'), true);
+ new GLForm($('.merge-request-form'));
new IssuableForm($('.merge-request-form'));
new LabelsSelect();
new MilestoneSelect();
diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
index 28d8761b502..26ead75cec4 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
@@ -1,30 +1,15 @@
-import MergeRequest from '~/merge_request';
import ZenMode from '~/zen_mode';
-import initNotes from '~/init_notes';
import initIssuableSidebar from '~/init_issuable_sidebar';
-import initDiffNotes from '~/diff_notes/diff_notes_bundle';
import ShortcutsIssuable from '~/shortcuts_issuable';
-import Diff from '~/diff';
import { handleLocationHash } from '~/lib/utils/common_utils';
import howToMerge from '~/how_to_merge';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
import initWidget from '../../../vue_merge_request_widget';
-export default function () {
- new Diff(); // eslint-disable-line no-new
+export default function() {
new ZenMode(); // eslint-disable-line no-new
-
initIssuableSidebar();
- initNotes();
- initDiffNotes();
initPipelines();
-
- const mrShowNode = document.querySelector('.merge-request');
-
- window.mergeRequest = new MergeRequest({
- action: mrShowNode.dataset.mrAction,
- });
-
new ShortcutsIssuable(true); // eslint-disable-line no-new
handleLocationHash();
howToMerge();
diff --git a/app/assets/javascripts/pages/projects/merge_requests/show/index.js b/app/assets/javascripts/pages/projects/merge_requests/show/index.js
index e5b2827b50c..f61f4db78d5 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/show/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/show/index.js
@@ -1,4 +1,3 @@
-import { hasVueMRDiscussionsCookie } from '~/lib/utils/common_utils';
import initMrNotes from '~/mr_notes';
import initSidebarBundle from '~/sidebar/sidebar_bundle';
import initShow from '../init_merge_request_show';
@@ -6,8 +5,5 @@ import initShow from '../init_merge_request_show';
document.addEventListener('DOMContentLoaded', () => {
initShow();
initSidebarBundle();
-
- if (hasVueMRDiscussionsCookie()) {
- initMrNotes();
- }
+ initMrNotes();
});
diff --git a/app/assets/javascripts/pages/projects/network/network.js b/app/assets/javascripts/pages/projects/network/network.js
index aa50dd4bb25..77368c47451 100644
--- a/app/assets/javascripts/pages/projects/network/network.js
+++ b/app/assets/javascripts/pages/projects/network/network.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, quote-props, prefer-template, comma-dangle, max-len */
+/* eslint-disable func-names, wrap-iife, no-var, quotes, quote-props, prefer-template, comma-dangle, max-len */
import $ from 'jquery';
import BranchGraph from '../../../network/branch_graph';
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
index 2d18fa2044b..d0613804067 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
@@ -65,11 +65,11 @@
<div class="cron-preset-radio-input">
<input
id="custom"
- class="label-light"
- type="radio"
:name="inputNameAttribute"
:value="cronInterval"
:checked="isEditable"
+ class="label-light"
+ type="radio"
@click="toggleCustomInput(true)"
/>
@@ -90,11 +90,11 @@
<div class="cron-preset-radio-input">
<input
id="every-day"
- class="label-light"
- type="radio"
v-model="cronInterval"
:name="inputNameAttribute"
:value="cronIntervalPresets.everyDay"
+ class="label-light"
+ type="radio"
@click="toggleCustomInput(false)"
/>
@@ -109,11 +109,11 @@
<div class="cron-preset-radio-input">
<input
id="every-week"
- class="label-light"
- type="radio"
v-model="cronInterval"
:name="inputNameAttribute"
:value="cronIntervalPresets.everyWeek"
+ class="label-light"
+ type="radio"
@click="toggleCustomInput(false)"
/>
@@ -128,11 +128,11 @@
<div class="cron-preset-radio-input">
<input
id="every-month"
- class="label-light"
- type="radio"
v-model="cronInterval"
:name="inputNameAttribute"
:value="cronIntervalPresets.everyMonth"
+ class="label-light"
+ type="radio"
@click="toggleCustomInput(false)"
/>
@@ -147,13 +147,13 @@
<div class="cron-interval-input-wrapper">
<input
id="schedule_cron"
- class="form-control inline cron-interval-input"
- type="text"
:placeholder="__('Define a custom pattern with cron syntax')"
- required="true"
v-model="cronInterval"
:name="inputNameAttribute"
:disabled="!isEditable"
+ class="form-control inline cron-interval-input"
+ type="text"
+ required="true"
/>
</div>
</div>
diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js
index c1e3425ec75..a853624e944 100644
--- a/app/assets/javascripts/pages/projects/project.js
+++ b/app/assets/javascripts/pages/projects/project.js
@@ -1,4 +1,5 @@
-/* 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 */
+/* eslint-disable func-names, no-var, no-return-assign, one-var,
+ one-var-declaration-per-line, object-shorthand, vars-on-top */
import $ from 'jquery';
import Cookies from 'js-cookie';
diff --git a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
index 6c2a785c0af..1faa59fb45b 100644
--- a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
@@ -22,4 +22,18 @@ document.addEventListener('DOMContentLoaded', () => {
errorBox: variableListEl.querySelector('.js-ci-variable-error-box'),
saveEndpoint: variableListEl.dataset.saveEndpoint,
});
+
+ // hide extra auto devops settings based on data-attributes
+ const autoDevOpsSettings = document.querySelector('.js-auto-devops-settings');
+ const autoDevOpsExtraSettings = document.querySelector('.js-extra-settings');
+
+ autoDevOpsSettings.addEventListener('click', event => {
+ const { target } = event;
+ if (target.classList.contains('js-toggle-extra-settings')) {
+ autoDevOpsExtraSettings.classList.toggle(
+ 'hidden',
+ !!(target.dataset && target.dataset.hideExtraSettings),
+ );
+ }
+ });
});
diff --git a/app/assets/javascripts/pages/projects/settings/repository/form.js b/app/assets/javascripts/pages/projects/settings/repository/form.js
index a5c17ab322c..a52861c9efa 100644
--- a/app/assets/javascripts/pages/projects/settings/repository/form.js
+++ b/app/assets/javascripts/pages/projects/settings/repository/form.js
@@ -13,7 +13,7 @@ export default () => {
new ProtectedTagEditList();
initDeployKeys();
initSettingsPanels();
- new ProtectedBranchCreate(); // eslint-disable-line no-new
- new ProtectedBranchEditList(); // eslint-disable-line no-new
+ new ProtectedBranchCreate();
+ new ProtectedBranchEditList();
new DueDateSelectors();
};
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue
index 9b13b2a524f..06101290f6c 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue
@@ -72,25 +72,25 @@
<template>
<div
- class="project-feature-controls"
:data-for="name"
+ class="project-feature-controls"
>
<input
v-if="name"
- type="hidden"
:name="name"
:value="value"
+ type="hidden"
/>
<project-feature-toggle
:value="featureEnabled"
- @change="toggleFeature"
:disabled-input="disabledInput"
+ @change="toggleFeature"
/>
<div class="select-wrapper">
<select
+ :disabled="displaySelectInput"
class="form-control project-repo-select select-control"
@change="selectOption"
- :disabled="displaySelectInput"
>
<option
v-for="[optionValue, optionName] in displayOptions"
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue
index 25a88f846eb..17b91479ea5 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue
@@ -42,7 +42,7 @@
</label>
<span
v-if="helpText"
- class="help-block"
+ class="form-text text-muted"
>
{{ helpText }}
</span>
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
index 755a34b7348..ae88b765abf 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
@@ -175,16 +175,16 @@
<div>
<div class="project-visibility-setting">
<project-setting-row
- label="Project visibility"
:help-path="visibilityHelpPath"
+ label="Project visibility"
>
<div class="project-feature-controls">
<div class="select-wrapper">
<select
- name="project[visibility_level]"
v-model="visibilityLevel"
- class="form-control select-control"
:disabled="!canChangeVisibilityLevel"
+ name="project[visibility_level]"
+ class="form-control select-control"
>
<option
:value="visibilityOptions.PRIVATE"
@@ -213,36 +213,36 @@
</i>
</div>
</div>
- <span class="help-block">{{ visibilityLevelDescription }}</span>
+ <span class="form-text text-muted">{{ visibilityLevelDescription }}</span>
<label
v-if="visibilityLevel !== visibilityOptions.PRIVATE"
class="request-access"
>
<input
+ :value="requestAccessEnabled"
type="hidden"
name="project[request_access_enabled]"
- :value="requestAccessEnabled"
/>
<input
- type="checkbox"
v-model="requestAccessEnabled"
+ type="checkbox"
/>
Allow users to request access
</label>
</project-setting-row>
</div>
<div
- class="project-feature-settings"
:class="{ 'highlight-changes': highlightChangesClass }"
+ class="project-feature-settings"
>
<project-setting-row
label="Issues"
help-text="Lightweight issue tracking system for this project"
>
<project-feature-setting
- name="project[project_feature_attributes][issues_access_level]"
:options="featureAccessLevelOptions"
v-model="issuesAccessLevel"
+ name="project[project_feature_attributes][issues_access_level]"
/>
</project-setting-row>
<project-setting-row
@@ -250,9 +250,9 @@
help-text="View and edit files in this project"
>
<project-feature-setting
- name="project[project_feature_attributes][repository_access_level]"
:options="featureAccessLevelOptions"
v-model="repositoryAccessLevel"
+ name="project[project_feature_attributes][repository_access_level]"
/>
</project-setting-row>
<div class="project-feature-setting-group">
@@ -261,10 +261,10 @@
help-text="Submit changes to be merged upstream"
>
<project-feature-setting
- name="project[project_feature_attributes][merge_requests_access_level]"
:options="repoFeatureAccessLevelOptions"
v-model="mergeRequestsAccessLevel"
:disabled-input="!repositoryEnabled"
+ name="project[project_feature_attributes][merge_requests_access_level]"
/>
</project-setting-row>
<project-setting-row
@@ -272,34 +272,34 @@
help-text="Build, test, and deploy your changes"
>
<project-feature-setting
- name="project[project_feature_attributes][builds_access_level]"
:options="repoFeatureAccessLevelOptions"
v-model="buildsAccessLevel"
:disabled-input="!repositoryEnabled"
+ name="project[project_feature_attributes][builds_access_level]"
/>
</project-setting-row>
<project-setting-row
v-if="registryAvailable"
- label="Container registry"
:help-path="registryHelpPath"
+ label="Container registry"
help-text="Every project can have its own space to store its Docker images"
>
<project-feature-toggle
- name="project[container_registry_enabled]"
v-model="containerRegistryEnabled"
:disabled-input="!repositoryEnabled"
+ name="project[container_registry_enabled]"
/>
</project-setting-row>
<project-setting-row
v-if="lfsAvailable"
- label="Git Large File Storage"
:help-path="lfsHelpPath"
+ label="Git Large File Storage"
help-text="Manages large files such as audio, video, and graphics files"
>
<project-feature-toggle
- name="project[lfs_enabled]"
v-model="lfsEnabled"
:disabled-input="!repositoryEnabled"
+ name="project[lfs_enabled]"
/>
</project-setting-row>
</div>
@@ -308,9 +308,9 @@
help-text="Pages for project documentation"
>
<project-feature-setting
- name="project[project_feature_attributes][wiki_access_level]"
:options="featureAccessLevelOptions"
v-model="wikiAccessLevel"
+ name="project[project_feature_attributes][wiki_access_level]"
/>
</project-setting-row>
<project-setting-row
@@ -318,9 +318,9 @@
help-text="Share code pastes with others out of Git repository"
>
<project-feature-setting
- name="project[project_feature_attributes][snippets_access_level]"
:options="featureAccessLevelOptions"
v-model="snippetsAccessLevel"
+ name="project[project_feature_attributes][snippets_access_level]"
/>
</project-setting-row>
</div>
diff --git a/app/assets/javascripts/pages/projects/tags/new/index.js b/app/assets/javascripts/pages/projects/tags/new/index.js
index 8d0edf7e06c..b3158f7e939 100644
--- a/app/assets/javascripts/pages/projects/tags/new/index.js
+++ b/app/assets/javascripts/pages/projects/tags/new/index.js
@@ -5,6 +5,6 @@ import GLForm from '../../../../gl_form';
document.addEventListener('DOMContentLoaded', () => {
new ZenMode(); // eslint-disable-line no-new
- new GLForm($('.tag-form'), true); // eslint-disable-line no-new
+ new GLForm($('.tag-form')); // eslint-disable-line no-new
new RefSelectDropdown($('.js-branch-select')); // eslint-disable-line no-new
});
diff --git a/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue b/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue
index df21e2f8771..0289209ff1e 100644
--- a/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue
+++ b/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue
@@ -50,8 +50,8 @@ export default {
<gl-modal
id="delete-wiki-modal"
:header-title-text="title"
- footer-primary-button-variant="danger"
:footer-primary-button-text="s__('WikiPageConfirmDelete|Delete page')"
+ footer-primary-button-variant="danger"
@submit="onSubmit"
>
{{ message }}
@@ -59,7 +59,7 @@ export default {
ref="form"
:action="deleteWikiUrl"
method="post"
- class="form-horizontal js-requires-input"
+ class="js-requires-input"
>
<input
ref="method"
@@ -68,9 +68,9 @@ export default {
value="delete"
/>
<input
+ :value="csrfToken"
type="hidden"
name="authenticity_token"
- :value="csrfToken"
/>
</form>
</gl-modal>
diff --git a/app/assets/javascripts/pages/projects/wikis/index.js b/app/assets/javascripts/pages/projects/wikis/index.js
index 0295653cb29..0a0fe3fc137 100644
--- a/app/assets/javascripts/pages/projects/wikis/index.js
+++ b/app/assets/javascripts/pages/projects/wikis/index.js
@@ -12,7 +12,7 @@ document.addEventListener('DOMContentLoaded', () => {
new Wikis(); // eslint-disable-line no-new
new ShortcutsWiki(); // eslint-disable-line no-new
new ZenMode(); // eslint-disable-line no-new
- new GLForm($('.wiki-form'), true); // eslint-disable-line no-new
+ new GLForm($('.wiki-form')); // eslint-disable-line no-new
const deleteWikiButton = document.getElementById('delete-wiki-button');
diff --git a/app/assets/javascripts/pages/projects/wikis/wikis.js b/app/assets/javascripts/pages/projects/wikis/wikis.js
index 34a12ef76a1..d3e8dbf4000 100644
--- a/app/assets/javascripts/pages/projects/wikis/wikis.js
+++ b/app/assets/javascripts/pages/projects/wikis/wikis.js
@@ -1,5 +1,7 @@
import bp from '../../../breakpoints';
import { slugify } from '../../../lib/utils/text_utility';
+import { parseQueryStringIntoObject } from '../../../lib/utils/common_utils';
+import { mergeUrlParams, redirectTo } from '../../../lib/utils/url_utility';
export default class Wikis {
constructor() {
@@ -28,7 +30,12 @@ export default class Wikis {
if (slug.length > 0) {
const wikisPath = slugInput.getAttribute('data-wikis-path');
- window.location.href = `${wikisPath}/${slug}`;
+
+ // If the wiki is empty, we need to merge the current URL params to keep the "create" view.
+ const params = parseQueryStringIntoObject(window.location.search.substr(1));
+ const url = mergeUrlParams(params, `${wikisPath}/${slug}`);
+ redirectTo(url);
+
e.preventDefault();
}
}
@@ -41,7 +48,7 @@ export default class Wikis {
static sidebarCanCollapse() {
const bootstrapBreakpoint = bp.getBreakpointSize();
- return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm';
+ return bootstrapBreakpoint === 'xs';
}
renderSidebar() {
diff --git a/app/assets/javascripts/pages/search/show/search.js b/app/assets/javascripts/pages/search/show/search.js
index 2e1fe78b3fa..e3e0ab91993 100644
--- a/app/assets/javascripts/pages/search/show/search.js
+++ b/app/assets/javascripts/pages/search/show/search.js
@@ -105,7 +105,7 @@ export default class Search {
getProjectsData(term) {
return new Promise((resolve) => {
if (this.groupId) {
- Api.groupProjects(this.groupId, term, resolve);
+ Api.groupProjects(this.groupId, term, {}, resolve);
} else {
Api.projects(term, {
order_by: 'id',
diff --git a/app/assets/javascripts/pages/sessions/new/index.js b/app/assets/javascripts/pages/sessions/new/index.js
index 80a7114f94d..07f32210d93 100644
--- a/app/assets/javascripts/pages/sessions/new/index.js
+++ b/app/assets/javascripts/pages/sessions/new/index.js
@@ -6,7 +6,8 @@ import OAuthRememberMe from './oauth_remember_me';
document.addEventListener('DOMContentLoaded', () => {
new UsernameValidator(); // eslint-disable-line no-new
new SigninTabsMemoizer(); // eslint-disable-line no-new
- new OAuthRememberMe({ // eslint-disable-line no-new
+
+ new OAuthRememberMe({
container: $('.omniauth-container'),
}).bindEvents();
});
diff --git a/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js b/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js
index 53030045292..761618109a4 100644
--- a/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js
+++ b/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js
@@ -5,7 +5,7 @@ import $ from 'jquery';
*
* Toggling this checkbox adds/removes a `remember_me` parameter to the
* login buttons' href, which is passed on to the omniauth callback.
- **/
+ */
export default class OAuthRememberMe {
constructor(opts = {}) {
@@ -17,7 +17,6 @@ export default class OAuthRememberMe {
$('#remember_me', this.container).on('click', this.toggleRememberMe);
}
- // eslint-disable-next-line class-methods-use-this
toggleRememberMe(event) {
const rememberMe = $(event.target).is(':checked');
diff --git a/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js b/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js
index d321892d2d2..1e7c29aefaa 100644
--- a/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js
+++ b/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js
@@ -37,6 +37,11 @@ export default class SigninTabsMemoizer {
const tab = document.querySelector(`${this.tabSelector} a[href="${anchorName}"]`);
if (tab) {
tab.click();
+ } else {
+ const firstTab = document.querySelector(`${this.tabSelector} a`);
+ if (firstTab) {
+ firstTab.click();
+ }
}
}
}
diff --git a/app/assets/javascripts/pages/sessions/new/username_validator.js b/app/assets/javascripts/pages/sessions/new/username_validator.js
index 825de01b5a2..97cf1aeaadc 100644
--- a/app/assets/javascripts/pages/sessions/new/username_validator.js
+++ b/app/assets/javascripts/pages/sessions/new/username_validator.js
@@ -1,4 +1,4 @@
-/* eslint-disable comma-dangle, consistent-return, class-methods-use-this, arrow-parens, no-param-reassign, max-len */
+/* eslint-disable comma-dangle, consistent-return, class-methods-use-this */
import $ from 'jquery';
import _ from 'underscore';
@@ -62,13 +62,13 @@ export default class UsernameValidator {
return this.setPendingState();
}
- if (!this.state.available) {
- return this.setUnavailableState();
- }
-
if (!this.state.valid) {
return this.setInvalidState();
}
+
+ if (!this.state.available) {
+ return this.setUnavailableState();
+ }
}
interceptInvalid(event) {
@@ -89,7 +89,6 @@ export default class UsernameValidator {
setAvailabilityState(usernameTaken) {
if (usernameTaken) {
- this.state.valid = false;
this.state.available = false;
} else {
this.state.available = true;
diff --git a/app/assets/javascripts/pages/snippets/form.js b/app/assets/javascripts/pages/snippets/form.js
index 72d05da1069..f369c7ef9a6 100644
--- a/app/assets/javascripts/pages/snippets/form.js
+++ b/app/assets/javascripts/pages/snippets/form.js
@@ -3,6 +3,14 @@ import GLForm from '~/gl_form';
import ZenMode from '~/zen_mode';
export default () => {
- new GLForm($('.snippet-form'), false); // eslint-disable-line no-new
+ // eslint-disable-next-line no-new
+ new GLForm($('.snippet-form'), {
+ members: false,
+ issues: false,
+ mergeRequests: false,
+ epics: false,
+ milestones: false,
+ labels: false,
+ });
new ZenMode(); // eslint-disable-line no-new
};
diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js
index 50d042fef29..9892a039941 100644
--- a/app/assets/javascripts/pages/users/activity_calendar.js
+++ b/app/assets/javascripts/pages/users/activity_calendar.js
@@ -2,6 +2,7 @@ import $ from 'jquery';
import _ from 'underscore';
import { scaleLinear, scaleThreshold } from 'd3-scale';
import { select } from 'd3-selection';
+import dateFormat from 'dateformat';
import { getDayName, getDayDifference } from '~/lib/utils/datetime_utility';
import axios from '~/lib/utils/axios_utils';
import flash from '~/flash';
@@ -26,7 +27,7 @@ function getSystemDate(systemUtcOffsetSeconds) {
function formatTooltipText({ date, count }) {
const dateObject = new Date(date);
const dateDayName = getDayName(dateObject);
- const dateText = dateObject.format('mmm d, yyyy');
+ const dateText = dateFormat(dateObject, 'mmm d, yyyy');
let contribText = 'No contributions';
if (count > 0) {
@@ -84,7 +85,7 @@ export default class ActivityCalendar {
date.setDate(date.getDate() + i);
const day = date.getDay();
- const count = timestamps[date.format('yyyy-mm-dd')] || 0;
+ const count = timestamps[dateFormat(date, 'yyyy-mm-dd')] || 0;
// Create a new group array if this is the first day of the week
// or if is first object
diff --git a/app/assets/javascripts/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js
index 124bc2ba710..a2ca03536f2 100644
--- a/app/assets/javascripts/pages/users/user_tabs.js
+++ b/app/assets/javascripts/pages/users/user_tabs.js
@@ -77,10 +77,9 @@ export default class UserTabs {
this.action = action || this.defaultAction;
this.$parentEl = $(parentEl) || $(document);
this.windowLocation = window.location;
- this.$parentEl.find('.nav-links a')
- .each((i, navLink) => {
- this.loaded[$(navLink).attr('data-action')] = false;
- });
+ this.$parentEl.find('.nav-links a').each((i, navLink) => {
+ this.loaded[$(navLink).attr('data-action')] = false;
+ });
this.actions = Object.keys(this.loaded);
this.bindEvents();
@@ -116,8 +115,7 @@ export default class UserTabs {
}
activateTab(action) {
- return this.$parentEl.find(`.nav-links .js-${action}-tab a`)
- .tab('show');
+ return this.$parentEl.find(`.nav-links .js-${action}-tab a`).tab('show');
}
setTab(action, endpoint) {
@@ -137,7 +135,8 @@ export default class UserTabs {
loadTab(action, endpoint) {
this.toggleLoading(true);
- return axios.get(endpoint)
+ return axios
+ .get(endpoint)
.then(({ data }) => {
const tabSelector = `div#${action}`;
this.$parentEl.find(tabSelector).html(data.html);
@@ -161,10 +160,11 @@ export default class UserTabs {
const utcOffset = $calendarWrap.data('utcOffset');
let utcFormatted = 'UTC';
if (utcOffset !== 0) {
- utcFormatted = `UTC${utcOffset > 0 ? '+' : ''}${(utcOffset / 3600)}`;
+ utcFormatted = `UTC${utcOffset > 0 ? '+' : ''}${utcOffset / 3600}`;
}
- axios.get(calendarPath)
+ axios
+ .get(calendarPath)
.then(({ data }) => {
$calendarWrap.html(CALENDAR_TEMPLATE);
$calendarWrap.find('.calendar-hint').append(`(Timezone: ${utcFormatted})`);
@@ -180,21 +180,24 @@ export default class UserTabs {
}
toggleLoading(status) {
- return this.$parentEl.find('.loading-status .loading')
- .toggle(status);
+ return this.$parentEl.find('.loading-status .loading').toggleClass('hide', !status);
}
setCurrentAction(source) {
let newState = source;
newState = newState.replace(/\/+$/, '');
newState += this.windowLocation.search + this.windowLocation.hash;
- history.replaceState({
- url: newState,
- }, document.title, newState);
+ window.history.replaceState(
+ {
+ url: newState,
+ },
+ document.title,
+ newState,
+ );
return newState;
}
getCurrentAction() {
- return this.$parentEl.find('.nav-links .active a').data('action');
+ return this.$parentEl.find('.nav-links a.active').data('action');
}
}
diff --git a/app/assets/javascripts/pdf/index.vue b/app/assets/javascripts/pdf/index.vue
index 00f32d9de78..2f480ecdc69 100644
--- a/app/assets/javascripts/pdf/index.vue
+++ b/app/assets/javascripts/pdf/index.vue
@@ -56,8 +56,8 @@
<template>
<div
- class="pdf-viewer"
- v-if="hasPDF">
+ v-if="hasPDF"
+ class="pdf-viewer">
<page
v-for="(page, index) in pages"
:key="index"
diff --git a/app/assets/javascripts/pdf/page/index.vue b/app/assets/javascripts/pdf/page/index.vue
index fcba819beba..9f06833d560 100644
--- a/app/assets/javascripts/pdf/page/index.vue
+++ b/app/assets/javascripts/pdf/page/index.vue
@@ -43,9 +43,9 @@
<template>
<canvas
- class="pdf-page"
ref="canvas"
:data-page="number"
+ class="pdf-page"
>
</canvas>
</template>
diff --git a/app/assets/javascripts/performance_bar/components/detailed_metric.vue b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
index db8a0055acd..dc7d6d29b8f 100644
--- a/app/assets/javascripts/performance_bar/components/detailed_metric.vue
+++ b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
@@ -39,9 +39,9 @@ export default {
</script>
<template>
<div
+ v-if="currentRequest.details"
:id="`peek-view-${metric}`"
class="view"
- v-if="currentRequest.details"
>
<button
:data-target="`#modal-peek-${metric}-details`"
@@ -56,6 +56,7 @@ export default {
<gl-modal
:id="`modal-peek-${metric}-details`"
:header-title-text="header"
+ modal-size="xl"
class="performance-bar-modal"
>
<table
diff --git a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
index 8ffaa52d9e8..b76965f280b 100644
--- a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
+++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
@@ -113,7 +113,7 @@ export default {
>
<div
v-if="currentRequest"
- class="container-fluid container-limited"
+ class="d-flex container-fluid container-limited"
>
<div
id="peek-view-host"
@@ -179,6 +179,7 @@ export default {
v-if="currentRequest"
:current-request="currentRequest"
:requests="requests"
+ class="ml-auto"
@change-current-request="changeCurrentRequest"
/>
</div>
diff --git a/app/assets/javascripts/performance_bar/components/request_selector.vue b/app/assets/javascripts/performance_bar/components/request_selector.vue
index 3ed07a4a47d..ad74f7b38f9 100644
--- a/app/assets/javascripts/performance_bar/components/request_selector.vue
+++ b/app/assets/javascripts/performance_bar/components/request_selector.vue
@@ -35,10 +35,7 @@ export default {
};
</script>
<template>
- <div
- id="peek-request-selector"
- class="pull-right"
- >
+ <div id="peek-request-selector">
<select v-model="currentRequestId">
<option
v-for="request in requests"
diff --git a/app/assets/javascripts/pipelines/components/blank_state.vue b/app/assets/javascripts/pipelines/components/blank_state.vue
index 8d3d6223d7b..34360105176 100644
--- a/app/assets/javascripts/pipelines/components/blank_state.vue
+++ b/app/assets/javascripts/pipelines/components/blank_state.vue
@@ -1,29 +1,29 @@
<script>
- export default {
- name: 'PipelinesSvgState',
- props: {
- svgPath: {
- type: String,
- required: true,
- },
+export default {
+ name: 'PipelinesSvgState',
+ props: {
+ svgPath: {
+ type: String,
+ required: true,
+ },
- message: {
- type: String,
- required: true,
- },
+ message: {
+ type: String,
+ required: true,
},
- };
+ },
+};
</script>
<template>
<div class="row empty-state">
- <div class="col-xs-12">
+ <div class="col-12">
<div class="svg-content">
<img :src="svgPath" />
</div>
</div>
- <div class="col-xs-12 text-center">
+ <div class="col-12 text-center">
<div class="text-content">
<h4>{{ message }}</h4>
</div>
diff --git a/app/assets/javascripts/pipelines/components/empty_state.vue b/app/assets/javascripts/pipelines/components/empty_state.vue
index 10ac8c08bed..c5a45afc634 100644
--- a/app/assets/javascripts/pipelines/components/empty_state.vue
+++ b/app/assets/javascripts/pipelines/components/empty_state.vue
@@ -1,31 +1,31 @@
<script>
- export default {
- name: 'PipelinesEmptyState',
- props: {
- helpPagePath: {
- type: String,
- required: true,
- },
- emptyStateSvgPath: {
- type: String,
- required: true,
- },
- canSetCi: {
- type: Boolean,
- required: true,
- },
+export default {
+ name: 'PipelinesEmptyState',
+ props: {
+ helpPagePath: {
+ type: String,
+ required: true,
},
- };
+ emptyStateSvgPath: {
+ type: String,
+ required: true,
+ },
+ canSetCi: {
+ type: Boolean,
+ required: true,
+ },
+ },
+};
</script>
<template>
<div class="row empty-state js-empty-state">
- <div class="col-xs-12">
+ <div class="col-12">
<div class="svg-content svg-250">
<img :src="emptyStateSvgPath" />
</div>
</div>
- <div class="col-xs-12">
+ <div class="col-12">
<div class="text-content">
<template v-if="canSetCi">
@@ -34,7 +34,7 @@
</h4>
<p>
- {{ s__(`Pipelines|Continous Integration can help
+ {{ s__(`Pipelines|Continuous Integration can help
catch bugs by running your tests automatically,
while Continuous Deployment can help you deliver
code to your product environment.`) }}
diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue
index 82b4ce083fb..b82e28a0735 100644
--- a/app/assets/javascripts/pipelines/components/graph/action_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue
@@ -41,7 +41,6 @@ export default {
type: String,
required: true,
},
-
},
data() {
return {
@@ -67,7 +66,8 @@ export default {
this.isDisabled = true;
- axios.post(`${this.link}.json`)
+ axios
+ .post(`${this.link}.json`)
.then(() => {
this.isDisabled = false;
this.$emit('pipelineActionRequestComplete');
@@ -83,15 +83,16 @@ export default {
</script>
<template>
<button
- type="button"
- @click="onClickAction"
v-tooltip
:title="tooltipText"
+ :class="cssClass"
+ :disabled="isDisabled"
+ type="button"
class="js-ci-action btn btn-blank
btn-transparent ci-action-icon-container ci-action-icon-wrapper"
- :class="cssClass"
data-container="body"
- :disabled="isDisabled"
+ data-boundary="viewport"
+ @click="onClickAction"
>
<icon :name="actionIcon"/>
</button>
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 7bfe11ab8cd..c32dc83da8e 100644
--- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
@@ -79,14 +79,16 @@ export default {
};
</script>
<template>
- <div class="ci-job-dropdown-container dropdown">
+ <div class="ci-job-dropdown-container dropdown dropright">
<button
v-tooltip
+ :title="tooltipText"
type="button"
data-toggle="dropdown"
data-container="body"
+ data-boundary="viewport"
+ data-display="static"
class="dropdown-menu-toggle build-content"
- :title="tooltipText"
>
<job-name-component
@@ -107,6 +109,7 @@ export default {
:key="i"
>
<job-component
+ :dropdown-length="job.size"
:job="item"
css-class-job-name="mini-pipeline-graph-dropdown-item"
@pipelineActionRequestComplete="pipelineActionRequestComplete"
diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue
index 27b938c4985..8af984ef91a 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue
@@ -46,6 +46,11 @@ export default {
required: false,
default: '',
},
+ dropdownLength: {
+ type: Number,
+ required: false,
+ default: Infinity,
+ },
},
computed: {
status() {
@@ -70,6 +75,10 @@ export default {
return textBuilder.join(' ');
},
+ tooltipBoundary() {
+ return this.dropdownLength < 5 ? 'viewport' : null;
+ },
+
/**
* Verifies if the provided job has an action path
*
@@ -94,6 +103,7 @@ export default {
:href="status.details_path"
:title="tooltipText"
:class="cssClassJobName"
+ :data-boundary="tooltipBoundary"
data-container="body"
data-html="true"
class="js-pipeline-graph-job-link"
@@ -106,11 +116,11 @@ export default {
</a>
<div
- v-else
v-tooltip
- class="js-job-component-tooltip non-details-job-component"
+ v-else
:title="tooltipText"
:class="cssClassJobName"
+ class="js-job-component-tooltip non-details-job-component"
data-html="true"
data-container="body"
>
diff --git a/app/assets/javascripts/pipelines/components/graph/job_name_component.vue b/app/assets/javascripts/pipelines/components/graph/job_name_component.vue
index 14f4964a406..6fdbcc1e049 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_name_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_name_component.vue
@@ -1,28 +1,28 @@
<script>
- import ciIcon from '../../../vue_shared/components/ci_icon.vue';
+import ciIcon from '../../../vue_shared/components/ci_icon.vue';
- /**
- * Component that renders both the CI icon status and the job name.
- * Used in
- * - Badge component
- * - Dropdown badge components
- */
- export default {
- components: {
- ciIcon,
+/**
+ * Component that renders both the CI icon status and the job name.
+ * Used in
+ * - Badge component
+ * - Dropdown badge components
+ */
+export default {
+ components: {
+ ciIcon,
+ },
+ props: {
+ name: {
+ type: String,
+ required: true,
},
- props: {
- name: {
- type: String,
- required: true,
- },
- status: {
- type: Object,
- required: true,
- },
+ status: {
+ type: Object,
+ required: true,
},
- };
+ },
+};
</script>
<template>
<span class="ci-job-name-component">
diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
index f32368947e8..2c728582b7c 100644
--- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
@@ -52,8 +52,8 @@ export default {
</script>
<template>
<li
- class="stage-column"
- :class="stageConnectorClass">
+ :class="stageConnectorClass"
+ class="stage-column">
<div class="stage-name">
{{ title }}
</div>
@@ -62,9 +62,9 @@ export default {
<li
v-for="(job, index) in jobs"
:key="job.id"
- class="build"
:class="buildConnnectorClass(index)"
:id="jobId(job)"
+ class="build"
>
<div class="curve"></div>
diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue
index e08c2092680..001eaeaa065 100644
--- a/app/assets/javascripts/pipelines/components/header_component.vue
+++ b/app/assets/javascripts/pipelines/components/header_component.vue
@@ -1,92 +1,92 @@
<script>
- import ciHeader from '../../vue_shared/components/header_ci_component.vue';
- import eventHub from '../event_hub';
- import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+import ciHeader from '../../vue_shared/components/header_ci_component.vue';
+import eventHub from '../event_hub';
+import loadingIcon from '../../vue_shared/components/loading_icon.vue';
- export default {
- name: 'PipelineHeaderSection',
- components: {
- ciHeader,
- loadingIcon,
+export default {
+ name: 'PipelineHeaderSection',
+ components: {
+ ciHeader,
+ loadingIcon,
+ },
+ props: {
+ pipeline: {
+ type: Object,
+ required: true,
},
- props: {
- pipeline: {
- type: Object,
- required: true,
- },
- isLoading: {
- type: Boolean,
- required: true,
- },
- },
- data() {
- return {
- actions: this.getActions(),
- };
+ isLoading: {
+ type: Boolean,
+ required: true,
},
+ },
+ data() {
+ return {
+ actions: this.getActions(),
+ };
+ },
- computed: {
- status() {
- return this.pipeline.details && this.pipeline.details.status;
- },
- shouldRenderContent() {
- return !this.isLoading && Object.keys(this.pipeline).length;
- },
+ computed: {
+ status() {
+ return this.pipeline.details && this.pipeline.details.status;
+ },
+ shouldRenderContent() {
+ return !this.isLoading && Object.keys(this.pipeline).length;
},
+ },
- watch: {
- pipeline() {
- this.actions = this.getActions();
- },
+ watch: {
+ pipeline() {
+ this.actions = this.getActions();
},
+ },
- methods: {
- postAction(action) {
- const index = this.actions.indexOf(action);
+ methods: {
+ postAction(action) {
+ const index = this.actions.indexOf(action);
- this.$set(this.actions[index], 'isLoading', true);
+ this.$set(this.actions[index], 'isLoading', true);
- eventHub.$emit('headerPostAction', action);
- },
+ eventHub.$emit('headerPostAction', action);
+ },
- getActions() {
- const actions = [];
+ getActions() {
+ const actions = [];
- if (this.pipeline.retry_path) {
- actions.push({
- label: 'Retry',
- path: this.pipeline.retry_path,
- cssClass: 'js-retry-button btn btn-inverted-secondary',
- type: 'button',
- isLoading: false,
- });
- }
+ if (this.pipeline.retry_path) {
+ actions.push({
+ label: 'Retry',
+ path: this.pipeline.retry_path,
+ cssClass: 'js-retry-button btn btn-inverted-secondary',
+ type: 'button',
+ isLoading: false,
+ });
+ }
- if (this.pipeline.cancel_path) {
- actions.push({
- label: 'Cancel running',
- path: this.pipeline.cancel_path,
- cssClass: 'js-btn-cancel-pipeline btn btn-danger',
- type: 'button',
- isLoading: false,
- });
- }
+ if (this.pipeline.cancel_path) {
+ actions.push({
+ label: 'Cancel running',
+ path: this.pipeline.cancel_path,
+ cssClass: 'js-btn-cancel-pipeline btn btn-danger',
+ type: 'button',
+ isLoading: false,
+ });
+ }
- return actions;
- },
+ return actions;
},
- };
+ },
+};
</script>
<template>
<div class="pipeline-header-container">
<ci-header
v-if="shouldRenderContent"
:status="status"
- item-name="Pipeline"
:item-id="pipeline.id"
:time="pipeline.created_at"
:user="pipeline.user"
:actions="actions"
+ item-name="Pipeline"
@actionClicked="postAction"
/>
<loading-icon
diff --git a/app/assets/javascripts/pipelines/components/nav_controls.vue b/app/assets/javascripts/pipelines/components/nav_controls.vue
index eba5678e3e5..9501afb7493 100644
--- a/app/assets/javascripts/pipelines/components/nav_controls.vue
+++ b/app/assets/javascripts/pipelines/components/nav_controls.vue
@@ -1,42 +1,42 @@
<script>
- import LoadingButton from '../../vue_shared/components/loading_button.vue';
+import LoadingButton from '../../vue_shared/components/loading_button.vue';
- export default {
- name: 'PipelineNavControls',
- components: {
- LoadingButton,
+export default {
+ name: 'PipelineNavControls',
+ components: {
+ LoadingButton,
+ },
+ props: {
+ newPipelinePath: {
+ type: String,
+ required: false,
+ default: null,
},
- props: {
- newPipelinePath: {
- type: String,
- required: false,
- default: null,
- },
- resetCachePath: {
- type: String,
- required: false,
- default: null,
- },
+ resetCachePath: {
+ type: String,
+ required: false,
+ default: null,
+ },
- ciLintPath: {
- type: String,
- required: false,
- default: null,
- },
+ ciLintPath: {
+ type: String,
+ required: false,
+ default: null,
+ },
- isResetCacheButtonLoading: {
- type: Boolean,
- required: false,
- default: false,
- },
+ isResetCacheButtonLoading: {
+ type: Boolean,
+ required: false,
+ default: false,
},
- methods: {
- onClickResetCache() {
- this.$emit('resetRunnersCache', this.resetCachePath);
- },
+ },
+ methods: {
+ onClickResetCache() {
+ this.$emit('resetRunnersCache', this.resetCachePath);
},
- };
+ },
+};
</script>
<template>
<div class="nav-controls">
@@ -50,10 +50,10 @@
<loading-button
v-if="resetCachePath"
- @click="onClickResetCache"
:loading="isResetCacheButtonLoading"
- class="btn btn-default js-clear-cache"
:label="s__('Pipelines|Clear Runner Caches')"
+ class="btn btn-default js-clear-cache"
+ @click="onClickResetCache"
/>
<a
diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue
index ceb4d9ca604..75db1e9ae7c 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_url.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue
@@ -1,52 +1,52 @@
<script>
- import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
- import tooltip from '../../vue_shared/directives/tooltip';
- import popover from '../../vue_shared/directives/popover';
+import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
+import tooltip from '../../vue_shared/directives/tooltip';
+import popover from '../../vue_shared/directives/popover';
- export default {
- components: {
- userAvatarLink,
+export default {
+ components: {
+ userAvatarLink,
+ },
+ directives: {
+ tooltip,
+ popover,
+ },
+ props: {
+ pipeline: {
+ type: Object,
+ required: true,
},
- directives: {
- tooltip,
- popover,
+ autoDevopsHelpPath: {
+ type: String,
+ required: true,
},
- props: {
- pipeline: {
- type: Object,
- required: true,
- },
- autoDevopsHelpPath: {
- type: String,
- required: true,
- },
+ },
+ computed: {
+ user() {
+ return this.pipeline.user;
},
- computed: {
- user() {
- return this.pipeline.user;
- },
- popoverOptions() {
- return {
- html: true,
- trigger: 'focus',
- placement: 'top',
- title: `<div class="autodevops-title">
+ popoverOptions() {
+ return {
+ html: true,
+ trigger: 'focus',
+ placement: 'top',
+ title: `<div class="autodevops-title">
This pipeline makes use of a predefined CI/CD configuration enabled by <b>Auto DevOps.</b>
</div>`,
- content: `<a
+ content: `<a
class="autodevops-link"
href="${this.autoDevopsHelpPath}"
target="_blank"
rel="noopener noreferrer nofollow">
Learn more about Auto DevOps
</a>`,
- };
- },
+ };
},
- };
+ },
+};
</script>
<template>
- <div class="table-section section-15 hidden-xs hidden-sm pipeline-tags">
+ <div class="table-section section-15 d-none d-sm-none d-md-block pipeline-tags">
<a
:href="pipeline.path"
class="js-pipeline-url-link">
@@ -55,10 +55,10 @@
<span>by</span>
<user-avatar-link
v-if="user"
- class="js-pipeline-url-user"
:link-href="pipeline.user.path"
:img-src="pipeline.user.avatar_url"
:tooltip-text="pipeline.user.name"
+ class="js-pipeline-url-user"
/>
<span
v-if="!user"
@@ -67,37 +67,37 @@
</span>
<div class="label-container">
<span
- v-if="pipeline.flags.latest"
v-tooltip
- class="js-pipeline-url-latest label label-success"
+ v-if="pipeline.flags.latest"
+ class="js-pipeline-url-latest badge badge-success"
title="Latest pipeline for this branch">
latest
</span>
<span
- v-if="pipeline.flags.yaml_errors"
v-tooltip
- class="js-pipeline-url-yaml label label-danger"
- :title="pipeline.yaml_errors">
+ v-if="pipeline.flags.yaml_errors"
+ :title="pipeline.yaml_errors"
+ class="js-pipeline-url-yaml badge badge-danger">
yaml invalid
</span>
<span
- v-if="pipeline.flags.failure_reason"
v-tooltip
- class="js-pipeline-url-failure label label-danger"
- :title="pipeline.failure_reason">
+ v-if="pipeline.flags.failure_reason"
+ :title="pipeline.failure_reason"
+ class="js-pipeline-url-failure badge badge-danger">
error
</span>
<a
+ v-popover="popoverOptions"
v-if="pipeline.flags.auto_devops"
tabindex="0"
- class="js-pipeline-url-autodevops label label-info autodevops-badge"
- v-popover="popoverOptions"
+ class="js-pipeline-url-autodevops badge badge-info autodevops-badge"
role="button">
Auto DevOps
</a>
<span
v-if="pipeline.flags.stuck"
- class="js-pipeline-url-stuck label label-warning">
+ class="js-pipeline-url-stuck badge badge-warning">
stuck
</span>
</div>
diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue
index 497a09cec65..c9d2dc3a3c5 100644
--- a/app/assets/javascripts/pipelines/components/pipelines.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines.vue
@@ -1,289 +1,289 @@
<script>
- import _ from 'underscore';
- import { __, sprintf, s__ } from '../../locale';
- import createFlash from '../../flash';
- import PipelinesService from '../services/pipelines_service';
- import pipelinesMixin from '../mixins/pipelines';
- import TablePagination from '../../vue_shared/components/table_pagination.vue';
- import NavigationTabs from '../../vue_shared/components/navigation_tabs.vue';
- import NavigationControls from './nav_controls.vue';
- import { getParameterByName } from '../../lib/utils/common_utils';
- import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
+import _ from 'underscore';
+import { __, sprintf, s__ } from '../../locale';
+import createFlash from '../../flash';
+import PipelinesService from '../services/pipelines_service';
+import pipelinesMixin from '../mixins/pipelines';
+import TablePagination from '../../vue_shared/components/table_pagination.vue';
+import NavigationTabs from '../../vue_shared/components/navigation_tabs.vue';
+import NavigationControls from './nav_controls.vue';
+import { getParameterByName } from '../../lib/utils/common_utils';
+import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
- export default {
- components: {
- TablePagination,
- NavigationTabs,
- NavigationControls,
+export default {
+ components: {
+ TablePagination,
+ NavigationTabs,
+ NavigationControls,
+ },
+ mixins: [pipelinesMixin, CIPaginationMixin],
+ props: {
+ store: {
+ type: Object,
+ required: true,
},
- mixins: [pipelinesMixin, CIPaginationMixin],
- props: {
- store: {
- 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',
- },
- endpoint: {
- type: String,
- required: true,
- },
- helpPagePath: {
- type: String,
- required: true,
- },
- emptyStateSvgPath: {
- type: String,
- required: true,
- },
- errorStateSvgPath: {
- type: String,
- required: true,
- },
- noPipelinesSvgPath: {
- type: String,
- required: true,
- },
- autoDevopsPath: {
- type: String,
- required: true,
- },
- hasGitlabCi: {
- type: Boolean,
- required: true,
- },
- canCreatePipeline: {
- type: Boolean,
- required: true,
- },
- ciLintPath: {
- type: String,
- required: false,
- default: null,
- },
- resetCachePath: {
- type: String,
- required: false,
- default: null,
- },
- newPipelinePath: {
- type: String,
- required: false,
- default: null,
- },
+ // 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',
},
- data() {
- return {
- // Start with loading state to avoid a glitch when the empty state will be rendered
- isLoading: true,
- state: this.store.state,
- scope: getParameterByName('scope') || 'all',
- page: getParameterByName('page') || '1',
- requestData: {},
- isResetCacheButtonLoading: false,
- };
+ endpoint: {
+ type: String,
+ required: true,
},
- stateMap: {
- // with tabs
- loading: 'loading',
- tableList: 'tableList',
- error: 'error',
- emptyTab: 'emptyTab',
-
- // without tabs
- emptyState: 'emptyState',
+ helpPagePath: {
+ type: String,
+ required: true,
+ },
+ emptyStateSvgPath: {
+ type: String,
+ required: true,
+ },
+ errorStateSvgPath: {
+ type: String,
+ required: true,
+ },
+ noPipelinesSvgPath: {
+ type: String,
+ required: true,
+ },
+ autoDevopsPath: {
+ type: String,
+ required: true,
+ },
+ hasGitlabCi: {
+ type: Boolean,
+ required: true,
},
- scopes: {
- all: 'all',
- pending: 'pending',
- running: 'running',
- finished: 'finished',
- branches: 'branches',
- tags: 'tags',
+ canCreatePipeline: {
+ type: Boolean,
+ required: true,
},
- computed: {
- /**
- * `hasGitlabCi` handles both internal and external CI.
- * The order on which the checks are made in this method is
- * important to guarantee we handle all the corner cases.
- */
- stateToRender() {
- const { stateMap } = this.$options;
+ ciLintPath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ resetCachePath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ newPipelinePath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ // Start with loading state to avoid a glitch when the empty state will be rendered
+ isLoading: true,
+ state: this.store.state,
+ scope: getParameterByName('scope') || 'all',
+ page: getParameterByName('page') || '1',
+ requestData: {},
+ isResetCacheButtonLoading: false,
+ };
+ },
+ stateMap: {
+ // with tabs
+ loading: 'loading',
+ tableList: 'tableList',
+ error: 'error',
+ emptyTab: 'emptyTab',
- if (this.isLoading) {
- return stateMap.loading;
- }
+ // without tabs
+ emptyState: 'emptyState',
+ },
+ scopes: {
+ all: 'all',
+ pending: 'pending',
+ running: 'running',
+ finished: 'finished',
+ branches: 'branches',
+ tags: 'tags',
+ },
+ computed: {
+ /**
+ * `hasGitlabCi` handles both internal and external CI.
+ * The order on which the checks are made in this method is
+ * important to guarantee we handle all the corner cases.
+ */
+ stateToRender() {
+ const { stateMap } = this.$options;
- if (this.hasError) {
- return stateMap.error;
- }
+ if (this.isLoading) {
+ return stateMap.loading;
+ }
- if (this.state.pipelines.length) {
- return stateMap.tableList;
- }
+ if (this.hasError) {
+ return stateMap.error;
+ }
- if ((this.scope !== 'all' && this.scope !== null) || this.hasGitlabCi) {
- return stateMap.emptyTab;
- }
+ if (this.state.pipelines.length) {
+ return stateMap.tableList;
+ }
- return stateMap.emptyState;
- },
- /**
- * Tabs are rendered in all states except empty state.
- * They are not rendered before the first request to avoid a flicker on first load.
- */
- shouldRenderTabs() {
- const { stateMap } = this.$options;
- return (
- this.hasMadeRequest &&
- [stateMap.loading, stateMap.tableList, stateMap.error, stateMap.emptyTab].includes(
- this.stateToRender,
- )
- );
- },
+ if ((this.scope !== 'all' && this.scope !== null) || this.hasGitlabCi) {
+ return stateMap.emptyTab;
+ }
+
+ return stateMap.emptyState;
+ },
+ /**
+ * Tabs are rendered in all states except empty state.
+ * They are not rendered before the first request to avoid a flicker on first load.
+ */
+ shouldRenderTabs() {
+ const { stateMap } = this.$options;
+ return (
+ this.hasMadeRequest &&
+ [stateMap.loading, stateMap.tableList, stateMap.error, stateMap.emptyTab].includes(
+ this.stateToRender,
+ )
+ );
+ },
- shouldRenderButtons() {
- return (
- (this.newPipelinePath || this.resetCachePath || this.ciLintPath) && this.shouldRenderTabs
- );
- },
+ shouldRenderButtons() {
+ return (
+ (this.newPipelinePath || this.resetCachePath || this.ciLintPath) && this.shouldRenderTabs
+ );
+ },
- shouldRenderPagination() {
- return (
- !this.isLoading &&
- this.state.pipelines.length &&
- this.state.pageInfo.total > this.state.pageInfo.perPage
- );
- },
+ shouldRenderPagination() {
+ return (
+ !this.isLoading &&
+ this.state.pipelines.length &&
+ this.state.pageInfo.total > this.state.pageInfo.perPage
+ );
+ },
- emptyTabMessage() {
- const { scopes } = this.$options;
- const possibleScopes = [scopes.pending, scopes.running, scopes.finished];
+ emptyTabMessage() {
+ const { scopes } = this.$options;
+ const possibleScopes = [scopes.pending, scopes.running, scopes.finished];
- if (possibleScopes.includes(this.scope)) {
- return sprintf(s__('Pipelines|There are currently no %{scope} pipelines.'), {
- scope: this.scope,
- });
- }
+ if (possibleScopes.includes(this.scope)) {
+ return sprintf(s__('Pipelines|There are currently no %{scope} pipelines.'), {
+ scope: this.scope,
+ });
+ }
- return s__('Pipelines|There are currently no pipelines.');
- },
+ return s__('Pipelines|There are currently no pipelines.');
+ },
- tabs() {
- const { count } = this.state;
- const { scopes } = this.$options;
+ tabs() {
+ const { count } = this.state;
+ const { scopes } = this.$options;
- return [
- {
- name: __('All'),
- scope: scopes.all,
- count: count.all,
- isActive: this.scope === 'all',
- },
- {
- name: __('Pending'),
- scope: scopes.pending,
- count: count.pending,
- isActive: this.scope === 'pending',
- },
- {
- name: __('Running'),
- scope: scopes.running,
- count: count.running,
- isActive: this.scope === 'running',
- },
- {
- name: __('Finished'),
- scope: scopes.finished,
- count: count.finished,
- isActive: this.scope === 'finished',
- },
- {
- name: __('Branches'),
- scope: scopes.branches,
- isActive: this.scope === 'branches',
- },
- {
- name: __('Tags'),
- scope: scopes.tags,
- isActive: this.scope === 'tags',
- },
- ];
- },
+ return [
+ {
+ name: __('All'),
+ scope: scopes.all,
+ count: count.all,
+ isActive: this.scope === 'all',
+ },
+ {
+ name: __('Pending'),
+ scope: scopes.pending,
+ count: count.pending,
+ isActive: this.scope === 'pending',
+ },
+ {
+ name: __('Running'),
+ scope: scopes.running,
+ count: count.running,
+ isActive: this.scope === 'running',
+ },
+ {
+ name: __('Finished'),
+ scope: scopes.finished,
+ count: count.finished,
+ isActive: this.scope === 'finished',
+ },
+ {
+ name: __('Branches'),
+ scope: scopes.branches,
+ isActive: this.scope === 'branches',
+ },
+ {
+ name: __('Tags'),
+ scope: scopes.tags,
+ isActive: this.scope === 'tags',
+ },
+ ];
},
- created() {
- this.service = new PipelinesService(this.endpoint);
- this.requestData = { page: this.page, scope: this.scope };
+ },
+ created() {
+ this.service = new PipelinesService(this.endpoint);
+ this.requestData = { page: this.page, scope: this.scope };
+ },
+ methods: {
+ successCallback(resp) {
+ // Because we are polling & the user is interacting verify if the response received
+ // matches the last request made
+ if (_.isEqual(resp.config.params, this.requestData)) {
+ this.store.storeCount(resp.data.count);
+ this.store.storePagination(resp.headers);
+ this.setCommonData(resp.data.pipelines);
+ }
},
- methods: {
- successCallback(resp) {
- // Because we are polling & the user is interacting verify if the response received
- // matches the last request made
- if (_.isEqual(resp.config.params, this.requestData)) {
- this.store.storeCount(resp.data.count);
- this.store.storePagination(resp.headers);
- this.setCommonData(resp.data.pipelines);
- }
- },
- /**
- * 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);
+ /**
+ * 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
- .getPipelines(this.requestData)
- .then(response => {
- this.isLoading = false;
- this.successCallback(response);
+ // fetch new data
+ return this.service
+ .getPipelines(this.requestData)
+ .then(response => {
+ this.isLoading = false;
+ this.successCallback(response);
- // restart polling
- this.poll.restart({ data: this.requestData });
- })
- .catch(() => {
- this.isLoading = false;
- this.errorCallback();
+ // restart polling
+ this.poll.restart({ data: this.requestData });
+ })
+ .catch(() => {
+ this.isLoading = false;
+ this.errorCallback();
- // restart polling
- this.poll.restart({ data: this.requestData });
- });
- },
+ // restart polling
+ this.poll.restart({ data: this.requestData });
+ });
+ },
- handleResetRunnersCache(endpoint) {
- this.isResetCacheButtonLoading = true;
+ handleResetRunnersCache(endpoint) {
+ this.isResetCacheButtonLoading = true;
- this.service
- .postAction(endpoint)
- .then(() => {
- this.isResetCacheButtonLoading = false;
- createFlash(s__('Pipelines|Project cache successfully reset.'), 'notice');
- })
- .catch(() => {
- this.isResetCacheButtonLoading = false;
- createFlash(s__('Pipelines|Something went wrong while cleaning runners cache.'));
- });
- },
+ this.service
+ .postAction(endpoint)
+ .then(() => {
+ this.isResetCacheButtonLoading = false;
+ createFlash(s__('Pipelines|Project cache successfully reset.'), 'notice');
+ })
+ .catch(() => {
+ this.isResetCacheButtonLoading = false;
+ createFlash(s__('Pipelines|Something went wrong while cleaning runners cache.'));
+ });
},
- };
+ },
+};
</script>
<template>
<div class="pipelines-container">
<div
- class="top-area scrolling-tabs-container inner-page-scroll-tabs"
v-if="shouldRenderTabs || shouldRenderButtons"
+ class="top-area scrolling-tabs-container inner-page-scroll-tabs"
>
<div class="fade-left">
<i
@@ -303,8 +303,8 @@
<navigation-tabs
v-if="shouldRenderTabs"
:tabs="tabs"
- @onChangeTab="onChangeTab"
scope="pipelines"
+ @onChangeTab="onChangeTab"
/>
<navigation-controls
@@ -312,8 +312,8 @@
:new-pipeline-path="newPipelinePath"
:reset-cache-path="resetCachePath"
:ci-lint-path="ciLintPath"
- @resetRunnersCache="handleResetRunnersCache"
:is-reset-cache-button-loading="isResetCacheButtonLoading"
+ @resetRunnersCache="handleResetRunnersCache"
/>
</div>
@@ -347,8 +347,8 @@
/>
<div
- class="table-holder"
v-else-if="stateToRender === $options.stateMap.tableList"
+ class="table-holder"
>
<pipelines-table-component
diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue
index 3297af7bde4..1c8d7303c52 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue
@@ -1,56 +1,56 @@
<script>
- import eventHub from '../event_hub';
- import loadingIcon from '../../vue_shared/components/loading_icon.vue';
- import icon from '../../vue_shared/components/icon.vue';
- import tooltip from '../../vue_shared/directives/tooltip';
+import eventHub from '../event_hub';
+import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+import icon from '../../vue_shared/components/icon.vue';
+import tooltip from '../../vue_shared/directives/tooltip';
- export default {
- directives: {
- tooltip,
+export default {
+ directives: {
+ tooltip,
+ },
+ components: {
+ loadingIcon,
+ icon,
+ },
+ props: {
+ actions: {
+ type: Array,
+ required: true,
},
- components: {
- loadingIcon,
- icon,
- },
- props: {
- actions: {
- type: Array,
- required: true,
- },
- },
- data() {
- return {
- isLoading: false,
- };
- },
- methods: {
- onClickAction(endpoint) {
- this.isLoading = true;
+ },
+ data() {
+ return {
+ isLoading: false,
+ };
+ },
+ methods: {
+ onClickAction(endpoint) {
+ this.isLoading = true;
- eventHub.$emit('postAction', endpoint);
- },
+ eventHub.$emit('postAction', endpoint);
+ },
- isActionDisabled(action) {
- if (action.playable === undefined) {
- return false;
- }
+ isActionDisabled(action) {
+ if (action.playable === undefined) {
+ return false;
+ }
- return !action.playable;
- },
+ return !action.playable;
},
- };
+ },
+};
</script>
<template>
<div class="btn-group">
<button
v-tooltip
+ :disabled="isLoading"
type="button"
class="dropdown-new btn btn-default js-pipeline-dropdown-manual-actions"
title="Manual job"
data-toggle="dropdown"
data-placement="top"
aria-label="Manual job"
- :disabled="isLoading"
>
<icon
name="play"
@@ -63,17 +63,17 @@
<loading-icon v-if="isLoading" />
</button>
- <ul class="dropdown-menu dropdown-menu-align-right">
+ <ul class="dropdown-menu dropdown-menu-right">
<li
v-for="(action, i) in actions"
:key="i"
>
<button
+ :class="{ disabled: isActionDisabled(action) }"
+ :disabled="isActionDisabled(action)"
type="button"
class="js-pipeline-action-link no-btn btn"
@click="onClickAction(action.path)"
- :class="{ disabled: isActionDisabled(action) }"
- :disabled="isActionDisabled(action)"
>
{{ action.name }}
</button>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue
index 1b9e0f917a4..d40de95e051 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue
@@ -1,21 +1,21 @@
<script>
- import tooltip from '../../vue_shared/directives/tooltip';
- import icon from '../../vue_shared/components/icon.vue';
+import tooltip from '../../vue_shared/directives/tooltip';
+import icon from '../../vue_shared/components/icon.vue';
- export default {
- directives: {
- tooltip,
+export default {
+ directives: {
+ tooltip,
+ },
+ components: {
+ icon,
+ },
+ props: {
+ artifacts: {
+ type: Array,
+ required: true,
},
- components: {
- icon,
- },
- props: {
- artifacts: {
- type: Array,
- required: true,
- },
- },
- };
+ },
+};
</script>
<template>
<div
@@ -37,14 +37,14 @@
>
</i>
</button>
- <ul class="dropdown-menu dropdown-menu-align-right">
+ <ul class="dropdown-menu dropdown-menu-right">
<li
v-for="(artifact, i) in artifacts"
:key="i">
<a
+ :href="artifact.path"
rel="nofollow"
download
- :href="artifact.path"
>
Download {{ artifact.name }} artifacts
</a>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_table.vue
index 41986b827cd..0d7324f3fb5 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_table.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_table.vue
@@ -1,72 +1,82 @@
<script>
- import Modal from '~/vue_shared/components/gl_modal.vue';
- import { s__, sprintf } from '~/locale';
- import PipelinesTableRowComponent from './pipelines_table_row.vue';
- import eventHub from '../event_hub';
+import Modal from '~/vue_shared/components/gl_modal.vue';
+import { s__, sprintf } from '~/locale';
+import PipelinesTableRowComponent from './pipelines_table_row.vue';
+import eventHub from '../event_hub';
- /**
- * Pipelines Table Component.
- *
- * Given an array of objects, renders a table.
- */
- export default {
- components: {
- PipelinesTableRowComponent,
- Modal,
+/**
+ * Pipelines Table Component.
+ *
+ * Given an array of objects, renders a table.
+ */
+export default {
+ components: {
+ PipelinesTableRowComponent,
+ Modal,
+ },
+ props: {
+ pipelines: {
+ type: Array,
+ required: true,
},
- props: {
- pipelines: {
- type: Array,
- required: true,
- },
- updateGraphDropdown: {
- type: Boolean,
- required: false,
- default: false,
- },
- autoDevopsHelpPath: {
- type: String,
- required: true,
- },
- viewType: {
- type: String,
- required: true,
- },
+ updateGraphDropdown: {
+ type: Boolean,
+ required: false,
+ default: false,
},
- data() {
- return {
- pipelineId: '',
- endpoint: '',
- };
+ autoDevopsHelpPath: {
+ type: String,
+ required: true,
},
- computed: {
- modalTitle() {
- return sprintf(s__('Pipeline|Stop pipeline #%{pipelineId}?'), {
+ viewType: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ pipelineId: '',
+ endpoint: '',
+ cancelingPipeline: null,
+ };
+ },
+ computed: {
+ modalTitle() {
+ return sprintf(
+ s__('Pipeline|Stop pipeline #%{pipelineId}?'),
+ {
pipelineId: `${this.pipelineId}`,
- }, false);
- },
- modalText() {
- return sprintf(s__('Pipeline|You’re about to stop pipeline %{pipelineId}.'), {
- pipelineId: `<strong>#${this.pipelineId}</strong>`,
- }, false);
- },
+ },
+ false,
+ );
},
- created() {
- eventHub.$on('openConfirmationModal', this.setModalData);
+ modalText() {
+ return sprintf(
+ s__('Pipeline|You’re about to stop pipeline %{pipelineId}.'),
+ {
+ pipelineId: `<strong>#${this.pipelineId}</strong>`,
+ },
+ false,
+ );
},
- beforeDestroy() {
- eventHub.$off('openConfirmationModal', this.setModalData);
+ },
+ created() {
+ eventHub.$on('openConfirmationModal', this.setModalData);
+ },
+ beforeDestroy() {
+ eventHub.$off('openConfirmationModal', this.setModalData);
+ },
+ methods: {
+ setModalData(data) {
+ this.pipelineId = data.pipelineId;
+ this.endpoint = data.endpoint;
},
- methods: {
- setModalData(data) {
- this.pipelineId = data.pipelineId;
- this.endpoint = data.endpoint;
- },
- onSubmit() {
- eventHub.$emit('postAction', this.endpoint);
- },
+ onSubmit() {
+ eventHub.$emit('postAction', this.endpoint);
+ this.cancelingPipeline = this.pipelineId;
},
- };
+ },
+};
</script>
<template>
<div class="ci-table">
@@ -106,13 +116,14 @@
:update-graph-dropdown="updateGraphDropdown"
:auto-devops-help-path="autoDevopsHelpPath"
:view-type="viewType"
+ :canceling-pipeline="cancelingPipeline"
/>
<modal
id="confirmation-modal"
:header-title-text="modalTitle"
- footer-primary-button-variant="danger"
:footer-primary-button-text="s__('Pipeline|Stop pipeline')"
+ footer-primary-button-variant="danger"
@submit="onSubmit"
>
<span v-html="modalText"></span>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
index 0f671ceea21..804822a3ea8 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
@@ -1,249 +1,253 @@
<script>
- import eventHub from '../event_hub';
- import PipelinesActionsComponent from './pipelines_actions.vue';
- import PipelinesArtifactsComponent from './pipelines_artifacts.vue';
- import CiBadge from '../../vue_shared/components/ci_badge_link.vue';
- import PipelineStage from './stage.vue';
- import PipelineUrl from './pipeline_url.vue';
- import PipelinesTimeago from './time_ago.vue';
- import CommitComponent from '../../vue_shared/components/commit.vue';
- import LoadingButton from '../../vue_shared/components/loading_button.vue';
- import Icon from '../../vue_shared/components/icon.vue';
- import { PIPELINES_TABLE } from '../constants';
+import eventHub from '../event_hub';
+import PipelinesActionsComponent from './pipelines_actions.vue';
+import PipelinesArtifactsComponent from './pipelines_artifacts.vue';
+import CiBadge from '../../vue_shared/components/ci_badge_link.vue';
+import PipelineStage from './stage.vue';
+import PipelineUrl from './pipeline_url.vue';
+import PipelinesTimeago from './time_ago.vue';
+import CommitComponent from '../../vue_shared/components/commit.vue';
+import LoadingButton from '../../vue_shared/components/loading_button.vue';
+import Icon from '../../vue_shared/components/icon.vue';
+import { PIPELINES_TABLE } from '../constants';
- /**
- * Pipeline table row.
- *
- * Given the received object renders a table row in the pipelines' table.
- */
- export default {
- components: {
- PipelinesActionsComponent,
- PipelinesArtifactsComponent,
- CommitComponent,
- PipelineStage,
- PipelineUrl,
- CiBadge,
- PipelinesTimeago,
- LoadingButton,
- Icon,
+/**
+ * Pipeline table row.
+ *
+ * Given the received object renders a table row in the pipelines' table.
+ */
+export default {
+ components: {
+ PipelinesActionsComponent,
+ PipelinesArtifactsComponent,
+ CommitComponent,
+ PipelineStage,
+ PipelineUrl,
+ CiBadge,
+ PipelinesTimeago,
+ LoadingButton,
+ Icon,
+ },
+ props: {
+ pipeline: {
+ type: Object,
+ required: true,
},
- props: {
- pipeline: {
- type: Object,
- required: true,
- },
- updateGraphDropdown: {
- type: Boolean,
- required: false,
- default: false,
- },
- autoDevopsHelpPath: {
- type: String,
- required: true,
- },
- viewType: {
- type: String,
- required: true,
- },
+ updateGraphDropdown: {
+ type: Boolean,
+ required: false,
+ default: false,
},
- pipelinesTable: PIPELINES_TABLE,
- data() {
- return {
- isRetrying: false,
- isCancelling: false,
- };
+ autoDevopsHelpPath: {
+ type: String,
+ required: true,
},
- computed: {
- /**
- * If provided, returns the commit tag.
- * Needed to render the commit component column.
- *
- * This field needs a lot of verification, because of different possible cases:
- *
- * 1. person who is an author of a commit might be a GitLab user
- * 2. if person who is an author of a commit is a GitLab user he/she can have a GitLab avatar
- * 3. If GitLab user does not have avatar he/she might have a Gravatar
- * 4. If committer is not a GitLab User he/she can have a Gravatar
- * 5. We do not have consistent API object in this case
- * 6. We should improve API and the code
- *
- * @returns {Object|Undefined}
- */
- commitAuthor() {
- let commitAuthorInformation;
+ viewType: {
+ type: String,
+ required: true,
+ },
+ cancelingPipeline: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ pipelinesTable: PIPELINES_TABLE,
+ data() {
+ return {
+ isRetrying: false,
+ };
+ },
+ computed: {
+ /**
+ * If provided, returns the commit tag.
+ * Needed to render the commit component column.
+ *
+ * This field needs a lot of verification, because of different possible cases:
+ *
+ * 1. person who is an author of a commit might be a GitLab user
+ * 2. if person who is an author of a commit is a GitLab user he/she can have a GitLab avatar
+ * 3. If GitLab user does not have avatar he/she might have a Gravatar
+ * 4. If committer is not a GitLab User he/she can have a Gravatar
+ * 5. We do not have consistent API object in this case
+ * 6. We should improve API and the code
+ *
+ * @returns {Object|Undefined}
+ */
+ commitAuthor() {
+ let commitAuthorInformation;
- if (!this.pipeline || !this.pipeline.commit) {
- return null;
- }
+ if (!this.pipeline || !this.pipeline.commit) {
+ return null;
+ }
- // 1. person who is an author of a commit might be a GitLab user
- if (this.pipeline.commit.author) {
- // 2. if person who is an author of a commit is a GitLab user
- // he/she can have a GitLab avatar
- if (this.pipeline.commit.author.avatar_url) {
- commitAuthorInformation = this.pipeline.commit.author;
+ // 1. person who is an author of a commit might be a GitLab user
+ if (this.pipeline.commit.author) {
+ // 2. if person who is an author of a commit is a GitLab user
+ // he/she can have a GitLab avatar
+ if (this.pipeline.commit.author.avatar_url) {
+ commitAuthorInformation = this.pipeline.commit.author;
- // 3. If GitLab user does not have avatar he/she might have a Gravatar
- } else if (this.pipeline.commit.author_gravatar_url) {
- commitAuthorInformation = Object.assign({}, this.pipeline.commit.author, {
- avatar_url: this.pipeline.commit.author_gravatar_url,
- });
- }
- // 4. If committer is not a GitLab User he/she can have a Gravatar
- } else {
- commitAuthorInformation = {
+ // 3. If GitLab user does not have avatar he/she might have a Gravatar
+ } else if (this.pipeline.commit.author_gravatar_url) {
+ commitAuthorInformation = Object.assign({}, this.pipeline.commit.author, {
avatar_url: this.pipeline.commit.author_gravatar_url,
- path: `mailto:${this.pipeline.commit.author_email}`,
- username: this.pipeline.commit.author_name,
- };
+ });
}
+ // 4. If committer is not a GitLab User he/she can have a Gravatar
+ } else {
+ commitAuthorInformation = {
+ avatar_url: this.pipeline.commit.author_gravatar_url,
+ path: `mailto:${this.pipeline.commit.author_email}`,
+ username: this.pipeline.commit.author_name,
+ };
+ }
- return commitAuthorInformation;
- },
+ return commitAuthorInformation;
+ },
- /**
- * If provided, returns the commit tag.
- * Needed to render the commit component column.
- *
- * @returns {String|Undefined}
- */
- commitTag() {
- if (this.pipeline.ref &&
- this.pipeline.ref.tag) {
- return this.pipeline.ref.tag;
- }
- return undefined;
- },
+ /**
+ * If provided, returns the commit tag.
+ * Needed to render the commit component column.
+ *
+ * @returns {String|Undefined}
+ */
+ commitTag() {
+ if (this.pipeline.ref && this.pipeline.ref.tag) {
+ return this.pipeline.ref.tag;
+ }
+ return undefined;
+ },
- /**
- * If provided, returns the commit ref.
- * Needed to render the commit component column.
- *
- * Matches `path` prop sent in the API to `ref_url` prop needed
- * in the commit component.
- *
- * @returns {Object|Undefined}
- */
- commitRef() {
- if (this.pipeline.ref) {
- return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => {
- if (prop === 'path') {
- // eslint-disable-next-line no-param-reassign
- accumulator.ref_url = this.pipeline.ref[prop];
- } else {
- // eslint-disable-next-line no-param-reassign
- accumulator[prop] = this.pipeline.ref[prop];
- }
- return accumulator;
- }, {});
- }
+ /**
+ * If provided, returns the commit ref.
+ * Needed to render the commit component column.
+ *
+ * Matches `path` prop sent in the API to `ref_url` prop needed
+ * in the commit component.
+ *
+ * @returns {Object|Undefined}
+ */
+ commitRef() {
+ if (this.pipeline.ref) {
+ return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => {
+ if (prop === 'path') {
+ // eslint-disable-next-line no-param-reassign
+ accumulator.ref_url = this.pipeline.ref[prop];
+ } else {
+ // eslint-disable-next-line no-param-reassign
+ accumulator[prop] = this.pipeline.ref[prop];
+ }
+ return accumulator;
+ }, {});
+ }
- return undefined;
- },
+ return undefined;
+ },
- /**
- * If provided, returns the commit url.
- * Needed to render the commit component column.
- *
- * @returns {String|Undefined}
- */
- commitUrl() {
- if (this.pipeline.commit &&
- this.pipeline.commit.commit_path) {
- return this.pipeline.commit.commit_path;
- }
- return undefined;
- },
+ /**
+ * If provided, returns the commit url.
+ * Needed to render the commit component column.
+ *
+ * @returns {String|Undefined}
+ */
+ commitUrl() {
+ if (this.pipeline.commit && this.pipeline.commit.commit_path) {
+ return this.pipeline.commit.commit_path;
+ }
+ return undefined;
+ },
- /**
- * If provided, returns the commit short sha.
- * Needed to render the commit component column.
- *
- * @returns {String|Undefined}
- */
- commitShortSha() {
- if (this.pipeline.commit &&
- this.pipeline.commit.short_id) {
- return this.pipeline.commit.short_id;
- }
- return undefined;
- },
+ /**
+ * If provided, returns the commit short sha.
+ * Needed to render the commit component column.
+ *
+ * @returns {String|Undefined}
+ */
+ commitShortSha() {
+ if (this.pipeline.commit && this.pipeline.commit.short_id) {
+ return this.pipeline.commit.short_id;
+ }
+ return undefined;
+ },
- /**
- * If provided, returns the commit title.
- * Needed to render the commit component column.
- *
- * @returns {String|Undefined}
- */
- commitTitle() {
- if (this.pipeline.commit &&
- this.pipeline.commit.title) {
- return this.pipeline.commit.title;
- }
- return undefined;
- },
+ /**
+ * If provided, returns the commit title.
+ * Needed to render the commit component column.
+ *
+ * @returns {String|Undefined}
+ */
+ commitTitle() {
+ if (this.pipeline.commit && this.pipeline.commit.title) {
+ return this.pipeline.commit.title;
+ }
+ return undefined;
+ },
- /**
- * Timeago components expects a number
- *
- * @return {type} description
- */
- pipelineDuration() {
- if (this.pipeline.details && this.pipeline.details.duration) {
- return this.pipeline.details.duration;
- }
+ /**
+ * Timeago components expects a number
+ *
+ * @return {type} description
+ */
+ pipelineDuration() {
+ if (this.pipeline.details && this.pipeline.details.duration) {
+ return this.pipeline.details.duration;
+ }
- return 0;
- },
+ return 0;
+ },
- /**
- * Timeago component expects a String.
- *
- * @return {String}
- */
- pipelineFinishedAt() {
- if (this.pipeline.details && this.pipeline.details.finished_at) {
- return this.pipeline.details.finished_at;
- }
+ /**
+ * Timeago component expects a String.
+ *
+ * @return {String}
+ */
+ pipelineFinishedAt() {
+ if (this.pipeline.details && this.pipeline.details.finished_at) {
+ return this.pipeline.details.finished_at;
+ }
- return '';
- },
+ return '';
+ },
- pipelineStatus() {
- if (this.pipeline.details && this.pipeline.details.status) {
- return this.pipeline.details.status;
- }
- return {};
- },
+ pipelineStatus() {
+ if (this.pipeline.details && this.pipeline.details.status) {
+ return this.pipeline.details.status;
+ }
+ return {};
+ },
- displayPipelineActions() {
- return this.pipeline.flags.retryable ||
- this.pipeline.flags.cancelable ||
- this.pipeline.details.manual_actions.length ||
- this.pipeline.details.artifacts.length;
- },
+ displayPipelineActions() {
+ return (
+ this.pipeline.flags.retryable ||
+ this.pipeline.flags.cancelable ||
+ this.pipeline.details.manual_actions.length ||
+ this.pipeline.details.artifacts.length
+ );
+ },
- isChildView() {
- return this.viewType === 'child';
- },
+ isChildView() {
+ return this.viewType === 'child';
},
- methods: {
- handleCancelClick() {
- this.isCancelling = true;
+ isCancelling() {
+ return this.cancelingPipeline === this.pipeline.id;
+ },
+ },
- eventHub.$emit('openConfirmationModal', {
- pipelineId: this.pipeline.id,
- endpoint: this.pipeline.cancel_path,
- });
- },
- handleRetryClick() {
- this.isRetrying = true;
- eventHub.$emit('retryPipeline', this.pipeline.retry_path);
- },
+ methods: {
+ handleCancelClick() {
+ eventHub.$emit('openConfirmationModal', {
+ pipelineId: this.pipeline.id,
+ endpoint: this.pipeline.cancel_path,
+ });
+ },
+ handleRetryClick() {
+ this.isRetrying = true;
+ eventHub.$emit('retryPipeline', this.pipeline.retry_path);
},
- };
+ },
+};
</script>
<template>
<div class="commit gl-responsive-table-row">
@@ -295,9 +299,9 @@
<div class="table-mobile-content">
<template v-if="pipeline.details.stages.length > 0">
<div
- class="stage-container dropdown js-mini-pipeline-graph"
v-for="(stage, index) in pipeline.details.stages"
- :key="index">
+ :key="index"
+ class="stage-container dropdown js-mini-pipeline-graph">
<pipeline-stage
:type="$options.pipelinesTable"
:stage="stage"
@@ -325,28 +329,28 @@
<pipelines-artifacts-component
v-if="pipeline.details.artifacts.length"
- class="hidden-xs hidden-sm"
:artifacts="pipeline.details.artifacts"
+ class="d-none d-sm-none d-md-block"
/>
<loading-button
v-if="pipeline.flags.retryable"
- @click="handleRetryClick"
- container-class="js-pipelines-retry-button btn btn-default btn-retry"
:loading="isRetrying"
:disabled="isRetrying"
+ container-class="js-pipelines-retry-button btn btn-default btn-retry"
+ @click="handleRetryClick"
>
<icon name="repeat" />
</loading-button>
<loading-button
v-if="pipeline.flags.cancelable"
- @click="handleCancelClick"
+ :loading="isCancelling"
+ :disabled="isCancelling"
data-toggle="modal"
data-target="#confirmation-modal"
container-class="js-pipelines-cancel-button btn btn-remove"
- :loading="isCancelling"
- :disabled="isCancelling"
+ @click="handleCancelClick"
>
<icon name="close" />
</loading-button>
diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue
index f9769815796..56fdb858088 100644
--- a/app/assets/javascripts/pipelines/components/stage.vue
+++ b/app/assets/javascripts/pipelines/components/stage.vue
@@ -158,22 +158,23 @@ export default {
<div class="dropdown">
<button
v-tooltip
+ id="stageDropdown"
+ ref="dropdown"
:class="triggerButtonClass"
- @click="onClickStage"
- class="mini-pipeline-graph-dropdown-toggle js-builds-dropdown-button"
:title="stage.title"
+ class="mini-pipeline-graph-dropdown-toggle js-builds-dropdown-button"
data-placement="top"
data-toggle="dropdown"
+ data-display="static"
type="button"
- id="stageDropdown"
aria-haspopup="true"
aria-expanded="false"
- ref="dropdown"
+ @click="onClickStage"
>
<span
- aria-hidden="true"
:aria-label="stage.title"
+ aria-hidden="true"
>
<icon :name="borderlessIcon" />
</span>
@@ -185,32 +186,27 @@ export default {
</i>
</button>
- <ul
+ <div
class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container"
aria-labelledby="stageDropdown"
>
-
- <li
+ <loading-icon v-if="isLoading"/>
+ <ul
+ v-else
class="js-builds-dropdown-list scrollable-menu"
>
-
- <loading-icon v-if="isLoading"/>
-
- <ul
- v-else
+ <li
+ v-for="job in dropdownContent"
+ :key="job.id"
>
- <li
- v-for="job in dropdownContent"
- :key="job.id"
- >
- <job-component
- :job="job"
- css-class-job-name="mini-pipeline-graph-dropdown-item"
- @pipelineActionRequestComplete="pipelineActionRequestComplete"
- />
- </li>
- </ul>
- </li>
- </ul>
+ <job-component
+ :dropdown-length="dropdownContent.length"
+ :job="job"
+ css-class-job-name="mini-pipeline-graph-dropdown-item"
+ @pipelineActionRequestComplete="pipelineActionRequestComplete"
+ />
+ </li>
+ </ul>
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/time_ago.vue b/app/assets/javascripts/pipelines/components/time_ago.vue
index cd54d26c9d3..cd43d78de40 100644
--- a/app/assets/javascripts/pipelines/components/time_ago.vue
+++ b/app/assets/javascripts/pipelines/components/time_ago.vue
@@ -1,60 +1,58 @@
<script>
- import iconTimerSvg from 'icons/_icon_timer.svg';
- import '../../lib/utils/datetime_utility';
- import tooltip from '../../vue_shared/directives/tooltip';
- import timeagoMixin from '../../vue_shared/mixins/timeago';
+import iconTimerSvg from 'icons/_icon_timer.svg';
+import '../../lib/utils/datetime_utility';
+import tooltip from '../../vue_shared/directives/tooltip';
+import timeagoMixin from '../../vue_shared/mixins/timeago';
- export default {
- directives: {
- tooltip,
+export default {
+ directives: {
+ tooltip,
+ },
+ mixins: [timeagoMixin],
+ props: {
+ finishedTime: {
+ type: String,
+ required: true,
},
- mixins: [
- timeagoMixin,
- ],
- props: {
- finishedTime: {
- type: String,
- required: true,
- },
- duration: {
- type: Number,
- required: true,
- },
+ duration: {
+ type: Number,
+ required: true,
},
- data() {
- return {
- iconTimerSvg,
- };
+ },
+ data() {
+ return {
+ iconTimerSvg,
+ };
+ },
+ computed: {
+ hasDuration() {
+ return this.duration > 0;
},
- computed: {
- hasDuration() {
- return this.duration > 0;
- },
- hasFinishedTime() {
- return this.finishedTime !== '';
- },
- durationFormated() {
- const date = new Date(this.duration * 1000);
+ hasFinishedTime() {
+ return this.finishedTime !== '';
+ },
+ durationFormated() {
+ const date = new Date(this.duration * 1000);
- let hh = date.getUTCHours();
- let mm = date.getUTCMinutes();
- let ss = date.getSeconds();
+ let hh = date.getUTCHours();
+ let mm = date.getUTCMinutes();
+ let ss = date.getSeconds();
- // left pad
- if (hh < 10) {
- hh = `0${hh}`;
- }
- if (mm < 10) {
- mm = `0${mm}`;
- }
- if (ss < 10) {
- ss = `0${ss}`;
- }
+ // left pad
+ if (hh < 10) {
+ hh = `0${hh}`;
+ }
+ if (mm < 10) {
+ mm = `0${mm}`;
+ }
+ if (ss < 10) {
+ ss = `0${ss}`;
+ }
- return `${hh}:${mm}:${ss}`;
- },
+ return `${hh}:${mm}:${ss}`;
},
- };
+ },
+};
</script>
<template>
<div class="table-section section-15 pipelines-time-ago">
@@ -66,8 +64,8 @@
</div>
<div class="table-mobile-content">
<p
- class="duration"
v-if="hasDuration"
+ class="duration"
>
<span v-html="iconTimerSvg">
</span>
@@ -75,8 +73,8 @@
</p>
<p
- class="finished-at hidden-xs hidden-sm"
v-if="hasFinishedTime"
+ class="finished-at d-none d-sm-none d-md-block"
>
<i
@@ -87,9 +85,9 @@
<time
v-tooltip
+ :title="tooltipTitle(finishedTime)"
data-placement="top"
- data-container="body"
- :title="tooltipTitle(finishedTime)">
+ data-container="body">
{{ timeFormated(finishedTime) }}
</time>
</p>
diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js
index 30b1eee186d..2cb558b0dec 100644
--- a/app/assets/javascripts/pipelines/mixins/pipelines.js
+++ b/app/assets/javascripts/pipelines/mixins/pipelines.js
@@ -75,8 +75,7 @@ export default {
// Stop polling
this.poll.stop();
// Update the table
- return this.getPipelines()
- .then(() => this.poll.restart());
+ return this.getPipelines().then(() => this.poll.restart());
},
fetchPipelines() {
if (!this.isMakingRequest) {
@@ -86,9 +85,10 @@ export default {
}
},
getPipelines() {
- return this.service.getPipelines(this.requestData)
+ return this.service
+ .getPipelines(this.requestData)
.then(response => this.successCallback(response))
- .catch((error) => this.errorCallback(error));
+ .catch(error => this.errorCallback(error));
},
setCommonData(pipelines) {
this.store.storePipelines(pipelines);
@@ -118,7 +118,8 @@ export default {
}
},
postAction(endpoint) {
- this.service.postAction(endpoint)
+ this.service
+ .postAction(endpoint)
.then(() => this.fetchPipelines())
.catch(() => Flash(__('An error occurred while making the request.')));
},
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index b49a16a87e6..dc9befe6349 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -10,7 +10,7 @@ import eventHub from './event_hub';
Vue.use(Translate);
export default () => {
- const dataset = document.querySelector('.js-pipeline-details-vue').dataset;
+ const { dataset } = document.querySelector('.js-pipeline-details-vue');
const mediator = new PipelinesMediator({ endpoint: dataset.endpoint });
@@ -31,7 +31,8 @@ export default () => {
requestRefreshPipelineGraph() {
// When an action is clicked
// (wether in the dropdown or in the main nodes, we refresh the big graph)
- this.mediator.refreshPipeline()
+ this.mediator
+ .refreshPipeline()
.catch(() => Flash(__('An error occurred while making the request.')));
},
},
diff --git a/app/assets/javascripts/pipelines/pipeline_details_mediator.js b/app/assets/javascripts/pipelines/pipeline_details_mediator.js
index 5633e54b28a..bd1e1895660 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_mediator.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_mediator.js
@@ -52,7 +52,8 @@ export default class pipelinesMediator {
refreshPipeline() {
this.poll.stop();
- return this.service.getPipeline()
+ return this.service
+ .getPipeline()
.then(response => this.successCallback(response))
.catch(() => this.errorCallback())
.finally(() => this.poll.restart());
diff --git a/app/assets/javascripts/pipelines/services/pipelines_service.js b/app/assets/javascripts/pipelines/services/pipelines_service.js
index 59c8b9c58e5..8317d3f4510 100644
--- a/app/assets/javascripts/pipelines/services/pipelines_service.js
+++ b/app/assets/javascripts/pipelines/services/pipelines_service.js
@@ -19,7 +19,7 @@ export default class PipelinesService {
getPipelines(data = {}) {
const { scope, page } = data;
- const CancelToken = axios.CancelToken;
+ const { CancelToken } = axios;
this.cancelationSource = CancelToken.source();
diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/preview_markdown.js
index 246a265ef2b..0964baf8954 100644
--- a/app/assets/javascripts/preview_markdown.js
+++ b/app/assets/javascripts/preview_markdown.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, no-var, object-shorthand, comma-dangle, prefer-arrow-callback */
+/* eslint-disable func-names, no-var, object-shorthand, prefer-arrow-callback */
import $ from 'jquery';
import axios from '~/lib/utils/axios_utils';
@@ -28,12 +28,16 @@ MarkdownPreview.prototype.ajaxCache = {};
MarkdownPreview.prototype.showPreview = function ($form) {
var mdText;
+ var markdownVersion;
+ var url;
var preview = $form.find('.js-md-preview');
- var url = preview.data('url');
if (preview.hasClass('md-preview-loading')) {
return;
}
+
mdText = $form.find('textarea.markdown-area').val();
+ markdownVersion = $form.attr('data-markdown-version');
+ url = this.versionedPreviewPath(preview.data('url'), markdownVersion);
if (mdText.trim().length === 0) {
preview.text(this.emptyMessage);
@@ -43,7 +47,7 @@ MarkdownPreview.prototype.showPreview = function ($form) {
this.fetchMarkdownPreview(mdText, url, (function (response) {
var body;
if (response.body.length > 0) {
- body = response.body;
+ ({ body } = response);
} else {
body = this.emptyMessage;
}
@@ -59,6 +63,14 @@ MarkdownPreview.prototype.showPreview = function ($form) {
}
};
+MarkdownPreview.prototype.versionedPreviewPath = function (markdownPreviewPath, markdownVersion) {
+ if (typeof markdownVersion === 'undefined') {
+ return markdownPreviewPath;
+ }
+
+ return `${markdownPreviewPath}${markdownPreviewPath.indexOf('?') === -1 ? '?' : '&'}markdown_version=${markdownVersion}`;
+};
+
MarkdownPreview.prototype.fetchMarkdownPreview = function (text, url, success) {
if (!url) {
return;
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 f50002afbf2..974629fa2af 100644
--- a/app/assets/javascripts/profile/account/components/delete_account_modal.vue
+++ b/app/assets/javascripts/profile/account/components/delete_account_modal.vue
@@ -80,10 +80,10 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
id="delete-account-modal"
:title="s__('Profiles|Delete your account?')"
:text="text"
- kind="danger"
:primary-button-label="s__('Profiles|Delete account')"
- @submit="onSubmit"
- :submit-disabled="!canSubmit()">
+ :submit-disabled="!canSubmit()"
+ kind="danger"
+ @submit="onSubmit">
<template
slot="body"
@@ -101,9 +101,9 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
value="delete"
/>
<input
+ :value="csrfToken"
type="hidden"
name="authenticity_token"
- :value="csrfToken"
/>
<p
@@ -114,18 +114,18 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
<input
v-if="confirmWithPassword"
+ v-model="enteredPassword"
name="password"
class="form-control"
type="password"
- v-model="enteredPassword"
aria-labelledby="input-label"
/>
<input
v-else
+ v-model="enteredUsername"
name="username"
class="form-control"
type="text"
- v-model="enteredUsername"
aria-labelledby="input-label"
/>
</form>
diff --git a/app/assets/javascripts/profile/account/components/update_username.vue b/app/assets/javascripts/profile/account/components/update_username.vue
index e5de3f69b01..ef484ddfd61 100644
--- a/app/assets/javascripts/profile/account/components/update_username.vue
+++ b/app/assets/javascripts/profile/account/components/update_username.vue
@@ -86,33 +86,37 @@ Please update your Git repository remotes as soon as possible.`),
<div class="form-group">
<label :for="$options.inputId">{{ s__('Profiles|Path') }}</label>
<div class="input-group">
- <div class="input-group-addon">{{ rootUrl }}</div>
+ <div class="input-group-prepend">
+ <div class="input-group-text">
+ {{ rootUrl }}
+ </div>
+ </div>
<input
:id="$options.inputId"
- class="form-control"
- required="required"
v-model="newUsername"
:disabled="isRequestPending"
+ class="form-control"
+ required="required"
/>
</div>
- <p class="help-block">
+ <p class="form-text text-muted">
{{ path }}
</p>
</div>
<button
:data-target="`#${$options.modalId}`"
+ :disabled="isRequestPending || newUsername === username"
class="btn btn-warning"
type="button"
data-toggle="modal"
- :disabled="isRequestPending || newUsername === username"
>
{{ $options.buttonText }}
</button>
<gl-modal
:id="$options.modalId"
:header-title-text="s__('Profiles|Change username') + '?'"
- footer-primary-button-variant="warning"
:footer-primary-button-text="$options.buttonText"
+ footer-primary-button-variant="warning"
@submit="onConfirm"
>
<span v-html="modalText"></span>
diff --git a/app/assets/javascripts/profile/add_ssh_key_validation.js b/app/assets/javascripts/profile/add_ssh_key_validation.js
new file mode 100644
index 00000000000..ab6a6c1896c
--- /dev/null
+++ b/app/assets/javascripts/profile/add_ssh_key_validation.js
@@ -0,0 +1,43 @@
+export default class AddSshKeyValidation {
+ constructor(inputElement, warningElement, originalSubmitElement, confirmSubmitElement) {
+ this.inputElement = inputElement;
+ this.form = inputElement.form;
+
+ this.warningElement = warningElement;
+
+ this.originalSubmitElement = originalSubmitElement;
+ this.confirmSubmitElement = confirmSubmitElement;
+
+ this.isValid = false;
+ }
+
+ register() {
+ this.form.addEventListener('submit', event => this.submit(event));
+
+ this.confirmSubmitElement.addEventListener('click', () => {
+ this.isValid = true;
+ this.form.submit();
+ });
+
+ this.inputElement.addEventListener('input', () => this.toggleWarning(false));
+ }
+
+ submit(event) {
+ this.isValid = AddSshKeyValidation.isPublicKey(this.inputElement.value);
+
+ if (this.isValid) return true;
+
+ event.preventDefault();
+ this.toggleWarning(true);
+ return false;
+ }
+
+ toggleWarning(isVisible) {
+ this.warningElement.classList.toggle('hide', !isVisible);
+ this.originalSubmitElement.classList.toggle('hide', isVisible);
+ }
+
+ static isPublicKey(value) {
+ return /^(ssh|ecdsa-sha2)-/.test(value);
+ }
+}
diff --git a/app/assets/javascripts/profile/gl_crop.js b/app/assets/javascripts/profile/gl_crop.js
index 8f93156cdd1..f641b23e519 100644
--- a/app/assets/javascripts/profile/gl_crop.js
+++ b/app/assets/javascripts/profile/gl_crop.js
@@ -1,4 +1,4 @@
-/* eslint-disable no-useless-escape, max-len, quotes, no-var, no-underscore-dangle, func-names, space-before-function-paren, no-unused-vars, no-return-assign, object-shorthand, one-var, one-var-declaration-per-line, comma-dangle, consistent-return, class-methods-use-this, new-parens */
+/* eslint-disable no-useless-escape, max-len, no-var, no-underscore-dangle, func-names, no-unused-vars, no-return-assign, object-shorthand, one-var, one-var-declaration-per-line, comma-dangle, consistent-return, class-methods-use-this, new-parens */
import $ from 'jquery';
import 'cropper';
@@ -47,7 +47,8 @@ import _ from 'underscore';
var _this;
_this = this;
this.fileInput.on('change', function(e) {
- return _this.onFileInputChange(e, this);
+ _this.onFileInputChange(e, this);
+ this.value = null;
});
this.pickImageEl.on('click', this.onPickImageClick);
this.modalCrop.on('shown.bs.modal', this.onModalShow);
@@ -85,11 +86,10 @@ import _ from 'underscore';
cropBoxResizable: false,
toggleDragModeOnDblclick: false,
built: function() {
- var $image, container, cropBoxHeight, cropBoxWidth;
- $image = $(this);
- container = $image.cropper('getContainerData');
- cropBoxWidth = _this.cropBoxWidth;
- cropBoxHeight = _this.cropBoxHeight;
+ const $image = $(this);
+ const container = $image.cropper('getContainerData');
+ const { cropBoxWidth, cropBoxHeight } = _this;
+
return $image.cropper('setCropBoxData', {
width: cropBoxWidth,
height: cropBoxHeight,
@@ -136,12 +136,13 @@ import _ from 'underscore';
}
dataURLtoBlob(dataURL) {
- var array, binary, i, k, len, v;
+ var array, binary, i, len, v;
binary = atob(dataURL.split(',')[1]);
array = [];
- for (k = i = 0, len = binary.length; i < len; k = (i += 1)) {
- v = binary[k];
- array.push(binary.charCodeAt(k));
+
+ for (i = 0, len = binary.length; i < len; i += 1) {
+ v = binary[i];
+ array.push(binary.charCodeAt(i));
}
return new Blob([new Uint8Array(array)], {
type: 'image/png'
diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js
index 0af34657d72..8cf7f2f23d0 100644
--- a/app/assets/javascripts/profile/profile.js
+++ b/app/assets/javascripts/profile/profile.js
@@ -1,8 +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 */
-
import $ from 'jquery';
import axios from '~/lib/utils/axios_utils';
-import { __ } from '~/locale';
import flash from '../flash';
export default class Profile {
@@ -64,7 +61,13 @@ export default class Profile {
url: this.form.attr('action'),
data: formData,
})
- .then(({ data }) => flash(data.message, 'notice'))
+ .then(({ data }) => {
+ if (avatarBlob != null) {
+ this.updateHeaderAvatar();
+ }
+
+ flash(data.message, 'notice');
+ })
.then(() => {
window.scrollTo(0, 0);
// Enable submit button after requests ends
@@ -73,6 +76,10 @@ export default class Profile {
.catch(error => flash(error.message));
}
+ updateHeaderAvatar() {
+ $('.header-user-avatar').attr('src', this.avatarGlCrop.dataURL);
+ }
+
setRepoRadio() {
const multiEditRadios = $('input[name="user[multi_file]"]');
if (this.newRepoActivated || this.newRepoActivated === 'true') {
diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js
index 4c4acd487f8..05485e352dc 100644
--- a/app/assets/javascripts/project_find_file.js
+++ b/app/assets/javascripts/project_find_file.js
@@ -1,4 +1,4 @@
-/* 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 */
+/* eslint-disable func-names, no-var, wrap-iife, quotes, consistent-return, one-var, one-var-declaration-per-line, no-cond-assign, max-len, prefer-template, no-unused-vars, no-return-assign */
import $ from 'jquery';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
@@ -88,10 +88,11 @@ export default class ProjectFindFile {
// render result
renderList(filePaths, searchText) {
- var blobItemUrl, filePath, html, i, j, len, matches, results;
+ var blobItemUrl, filePath, html, i, len, matches, results;
this.element.find(".tree-table > tbody").empty();
results = [];
- for (i = j = 0, len = filePaths.length; j < len; i = (j += 1)) {
+
+ for (i = 0, len = filePaths.length; i < len; i += 1) {
filePath = filePaths[i];
if (i === 20) {
break;
@@ -150,7 +151,7 @@ export default class ProjectFindFile {
}
goToTree() {
- return location.href = this.options.treeUrl;
+ return window.location.href = this.options.treeUrl;
}
goToBlob() {
diff --git a/app/assets/javascripts/project_fork.js b/app/assets/javascripts/project_fork.js
index 6fedd94a6a9..f5cd1c3cc3e 100644
--- a/app/assets/javascripts/project_fork.js
+++ b/app/assets/javascripts/project_fork.js
@@ -4,6 +4,6 @@ export default () => {
$('.js-fork-thumbnail').on('click', function forkThumbnailClicked() {
if ($(this).hasClass('disabled')) return false;
- return $('.js-fork-content').toggle();
+ return $('.js-fork-content').toggleClass('hidden');
});
};
diff --git a/app/assets/javascripts/project_import.js b/app/assets/javascripts/project_import.js
index d2d26d6f67e..5a0d2b642eb 100644
--- a/app/assets/javascripts/project_import.js
+++ b/app/assets/javascripts/project_import.js
@@ -2,7 +2,7 @@ import { visitUrl } from './lib/utils/url_utility';
export default function projectImport() {
setTimeout(() => {
- visitUrl(location.href);
+ visitUrl(window.location.href);
}, 5000);
}
diff --git a/app/assets/javascripts/project_label_subscription.js b/app/assets/javascripts/project_label_subscription.js
index f31beb4dc78..9049f87e037 100644
--- a/app/assets/javascripts/project_label_subscription.js
+++ b/app/assets/javascripts/project_label_subscription.js
@@ -3,6 +3,17 @@ import { __ } from './locale';
import axios from './lib/utils/axios_utils';
import flash from './flash';
+const tooltipTitles = {
+ group: {
+ subscribed: __('Unsubscribe at group level'),
+ unsubscribed: __('Subscribe at group level'),
+ },
+ project: {
+ subscribed: __('Unsubscribe at project level'),
+ unsubscribed: __('Subscribe at project level'),
+ },
+};
+
export default class ProjectLabelSubscription {
constructor(container) {
this.$container = $(container);
@@ -15,12 +26,10 @@ export default class ProjectLabelSubscription {
event.preventDefault();
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');
axios.post(url).then(() => {
let newStatus;
@@ -32,21 +41,28 @@ export default class ProjectLabelSubscription {
[newStatus, newAction] = ['unsubscribed', 'Subscribe'];
}
- $span.toggleClass('hidden');
$btn.removeClass('disabled');
this.$buttons.attr('data-status', newStatus);
this.$buttons.find('> span').text(newAction);
- this.$buttons.map((button) => {
+ this.$buttons.map((i, button) => {
const $button = $(button);
+ const originalTitle = $button.attr('data-original-title');
- if ($button.attr('data-original-title')) {
- $button.tooltip('hide').attr('data-original-title', newAction).tooltip('fixTitle');
+ if (originalTitle) {
+ ProjectLabelSubscription.setNewTitle($button, originalTitle, newStatus, newAction);
}
return button;
});
}).catch(() => flash(__('There was an error subscribing to this label.')));
}
+
+ static setNewTitle($button, originalTitle, newStatus) {
+ const type = /group/.test(originalTitle) ? 'group' : 'project';
+ const newTitle = tooltipTitles[type][newStatus];
+
+ $button.attr('title', newTitle).tooltip('_fixTitle');
+ }
}
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
index cb2e6855d1d..bce7556bd40 100644
--- a/app/assets/javascripts/project_select.js
+++ b/app/assets/javascripts/project_select.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-var, comma-dangle, object-shorthand, one-var, one-var-declaration-per-line, no-else-return, quotes, max-len */
+/* eslint-disable func-names, wrap-iife, no-var, comma-dangle, object-shorthand, one-var, one-var-declaration-per-line, no-else-return, quotes, max-len */
import $ from 'jquery';
import Api from './api';
@@ -47,7 +47,10 @@ export default function projectSelect() {
projectsCallback = finalCallback;
}
if (_this.groupId) {
- return Api.groupProjects(_this.groupId, query.term, projectsCallback);
+ return Api.groupProjects(_this.groupId, query.term, {
+ with_issues_enabled: _this.withIssuesEnabled,
+ with_merge_requests_enabled: _this.withMergeRequestsEnabled,
+ }, projectsCallback);
} else {
return Api.projects(query.term, {
order_by: _this.orderBy,
diff --git a/app/assets/javascripts/project_visibility.js b/app/assets/javascripts/project_visibility.js
index 7c95c71e239..a52ac768e57 100644
--- a/app/assets/javascripts/project_visibility.js
+++ b/app/assets/javascripts/project_visibility.js
@@ -7,7 +7,7 @@ function setVisibilityOptions(namespaceSelector) {
const selectedNamespace = namespaceSelector.options[namespaceSelector.selectedIndex];
const { name, visibility, visibilityLevel, showPath, editPath } = selectedNamespace.dataset;
- document.querySelectorAll('.visibility-level-setting .radio').forEach((option) => {
+ document.querySelectorAll('.visibility-level-setting .form-check').forEach((option) => {
const optionInput = option.querySelector('input[type=radio]');
const optionValue = optionInput ? optionInput.value : 0;
const optionTitle = option.querySelector('.option-title');
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_dropdown_mixin.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_dropdown_mixin.js
new file mode 100644
index 00000000000..c15d8ba49e1
--- /dev/null
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_dropdown_mixin.js
@@ -0,0 +1,71 @@
+import _ from 'underscore';
+import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
+import DropdownSearchInput from '~/vue_shared/components/dropdown/dropdown_search_input.vue';
+import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
+import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
+
+import store from '../store';
+
+export default {
+ store,
+ components: {
+ LoadingIcon,
+ DropdownButton,
+ DropdownSearchInput,
+ DropdownHiddenInput,
+ },
+ props: {
+ fieldId: {
+ type: String,
+ required: true,
+ },
+ fieldName: {
+ type: String,
+ required: true,
+ },
+ defaultValue: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ isLoading: false,
+ hasErrors: false,
+ searchQuery: '',
+ gapiError: '',
+ };
+ },
+ computed: {
+ results() {
+ if (!this.items) {
+ return [];
+ }
+
+ return this.items.filter(item => item.name.toLowerCase().indexOf(this.searchQuery) > -1);
+ },
+ },
+ methods: {
+ fetchSuccessHandler() {
+ if (this.defaultValue) {
+ const itemToSelect = _.find(this.items, item => item.name === this.defaultValue);
+
+ if (itemToSelect) {
+ this.setItem(itemToSelect.name);
+ }
+ }
+
+ this.isLoading = false;
+ this.hasErrors = false;
+ },
+ fetchFailureHandler(resp) {
+ this.isLoading = false;
+ this.hasErrors = true;
+
+ if (resp.result && resp.result.error) {
+ this.gapiError = resp.result.error.message;
+ }
+ },
+ },
+};
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue
new file mode 100644
index 00000000000..d4497924ad8
--- /dev/null
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue
@@ -0,0 +1,144 @@
+<script>
+import { sprintf, s__ } from '~/locale';
+import { mapState, mapGetters, mapActions } from 'vuex';
+
+import gkeDropdownMixin from './gke_dropdown_mixin';
+
+export default {
+ name: 'GkeMachineTypeDropdown',
+ mixins: [gkeDropdownMixin],
+ computed: {
+ ...mapState([
+ 'isValidatingProjectBilling',
+ 'projectHasBillingEnabled',
+ 'selectedZone',
+ 'selectedMachineType',
+ ]),
+ ...mapState({ items: 'machineTypes' }),
+ ...mapGetters(['hasZone', 'hasMachineType']),
+ allDropdownsSelected() {
+ return this.projectHasBillingEnabled && this.hasZone && this.hasMachineType;
+ },
+ isDisabled() {
+ return (
+ this.isLoading ||
+ this.isValidatingProjectBilling ||
+ !this.projectHasBillingEnabled ||
+ !this.hasZone
+ );
+ },
+ toggleText() {
+ if (this.isLoading) {
+ return s__('ClusterIntegration|Fetching machine types');
+ }
+
+ if (this.selectedMachineType) {
+ return this.selectedMachineType;
+ }
+
+ if (!this.projectHasBillingEnabled && !this.hasZone) {
+ return s__('ClusterIntegration|Select project and zone to choose machine type');
+ }
+
+ return !this.hasZone
+ ? s__('ClusterIntegration|Select zone to choose machine type')
+ : s__('ClusterIntegration|Select machine type');
+ },
+ errorMessage() {
+ return sprintf(
+ s__(
+ 'ClusterIntegration|An error occured while trying to fetch zone machine types: %{error}',
+ ),
+ { error: this.gapiError },
+ );
+ },
+ },
+ watch: {
+ selectedZone() {
+ this.hasErrors = false;
+
+ if (this.hasZone) {
+ this.isLoading = true;
+
+ this.fetchMachineTypes()
+ .then(this.fetchSuccessHandler)
+ .catch(this.fetchFailureHandler);
+ }
+ },
+ selectedMachineType() {
+ this.enableSubmit();
+ },
+ },
+ methods: {
+ ...mapActions(['fetchMachineTypes']),
+ ...mapActions({ setItem: 'setMachineType' }),
+ enableSubmit() {
+ if (this.allDropdownsSelected) {
+ const submitButtonEl = document.querySelector('.js-gke-cluster-creation-submit');
+
+ if (submitButtonEl) {
+ submitButtonEl.removeAttribute('disabled');
+ }
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div
+ class="js-gcp-machine-type-dropdown dropdown"
+ >
+ <dropdown-hidden-input
+ :name="fieldName"
+ :value="selectedMachineType"
+ />
+ <dropdown-button
+ :class="{ 'border-danger': hasErrors }"
+ :is-disabled="isDisabled"
+ :is-loading="isLoading"
+ :toggle-text="toggleText"
+ />
+ <div class="dropdown-menu dropdown-select">
+ <dropdown-search-input
+ v-model="searchQuery"
+ :placeholder-text="s__('ClusterIntegration|Search machine types')"
+ />
+ <div class="dropdown-content">
+ <ul>
+ <li v-show="!results.length">
+ <span class="menu-item">
+ {{ s__('ClusterIntegration|No machine types matched your search') }}
+ </span>
+ </li>
+ <li
+ v-for="result in results"
+ :key="result.id"
+ >
+ <button
+ type="button"
+ @click.prevent="setItem(result.name)"
+ >
+ {{ result.name }}
+ </button>
+ </li>
+ </ul>
+ </div>
+ <div class="dropdown-loading">
+ <loading-icon />
+ </div>
+ </div>
+ </div>
+ <span
+ v-if="hasErrors"
+ :class="{
+ 'text-danger': hasErrors,
+ 'text-muted': !hasErrors
+ }"
+ class="form-text"
+ >
+ {{ errorMessage }}
+ </span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue
new file mode 100644
index 00000000000..08d0a122579
--- /dev/null
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue
@@ -0,0 +1,203 @@
+<script>
+import _ from 'underscore';
+import { s__, sprintf } from '~/locale';
+import { mapState, mapGetters, mapActions } from 'vuex';
+
+import gkeDropdownMixin from './gke_dropdown_mixin';
+
+export default {
+ name: 'GkeProjectIdDropdown',
+ mixins: [gkeDropdownMixin],
+ props: {
+ docsUrl: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState(['selectedProject', 'isValidatingProjectBilling', 'projectHasBillingEnabled']),
+ ...mapState({ items: 'projects' }),
+ ...mapGetters(['hasProject']),
+ hasOneProject() {
+ return this.items && this.items.length === 1;
+ },
+ isDisabled() {
+ return (
+ this.isLoading || this.isValidatingProjectBilling || (this.items && this.items.length < 2)
+ );
+ },
+ toggleText() {
+ if (this.isValidatingProjectBilling) {
+ return s__('ClusterIntegration|Validating project billing status');
+ }
+
+ if (this.isLoading) {
+ return s__('ClusterIntegration|Fetching projects');
+ }
+
+ if (this.hasProject) {
+ return this.selectedProject.name;
+ }
+
+ if (!this.items) {
+ return s__('ClusterIntegration|No projects found');
+ }
+
+ return s__('ClusterIntegration|Select project');
+ },
+ helpText() {
+ let message;
+ if (this.hasErrors) {
+ return this.errorMessage;
+ }
+
+ if (!this.items) {
+ message =
+ 'ClusterIntegration|We were unable to fetch any projects. Ensure that you have a project on %{docsLinkStart}Google Cloud Platform%{docsLinkEnd}.';
+ }
+
+ message =
+ this.items && this.items.length
+ ? 'ClusterIntegration|To use a new project, first create one on %{docsLinkStart}Google Cloud Platform%{docsLinkEnd}.'
+ : 'ClusterIntegration|To create a cluster, first create a project on %{docsLinkStart}Google Cloud Platform%{docsLinkEnd}.';
+
+ return sprintf(
+ s__(message),
+ {
+ docsLinkEnd: '&nbsp;<i class="fa fa-external-link" aria-hidden="true"></i></a>',
+ docsLinkStart: `<a href="${_.escape(
+ this.docsUrl,
+ )}" target="_blank" rel="noopener noreferrer">`,
+ },
+ false,
+ );
+ },
+ errorMessage() {
+ if (!this.projectHasBillingEnabled) {
+ if (this.gapiError) {
+ return s__(
+ 'ClusterIntegration|We could not verify that one of your projects on GCP has billing enabled. Please try again.',
+ );
+ }
+
+ return sprintf(
+ s__(
+ 'This project does not have billing enabled. To create a cluster, <a href=%{linkToBilling} target="_blank" rel="noopener noreferrer">enable billing <i class="fa fa-external-link" aria-hidden="true"></i></a> and try again.',
+ ),
+ {
+ linkToBilling:
+ 'https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral',
+ },
+ false,
+ );
+ }
+
+ return sprintf(
+ s__('ClusterIntegration|An error occured while trying to fetch your projects: %{error}'),
+ { error: this.gapiError },
+ );
+ },
+ },
+ watch: {
+ selectedProject() {
+ this.setIsValidatingProjectBilling(true);
+
+ this.validateProjectBilling()
+ .then(this.validateProjectBillingSuccessHandler)
+ .catch(this.validateProjectBillingFailureHandler);
+ },
+ },
+ created() {
+ this.isLoading = true;
+
+ this.fetchProjects()
+ .then(this.fetchSuccessHandler)
+ .catch(this.fetchFailureHandler);
+ },
+ methods: {
+ ...mapActions(['fetchProjects', 'setIsValidatingProjectBilling', 'validateProjectBilling']),
+ ...mapActions({ setItem: 'setProject' }),
+ fetchSuccessHandler() {
+ if (this.defaultValue) {
+ const projectToSelect = _.find(this.items, item => item.projectId === this.defaultValue);
+
+ if (projectToSelect) {
+ this.setItem(projectToSelect);
+ }
+ } else if (this.items.length === 1) {
+ this.setItem(this.items[0]);
+ }
+
+ this.isLoading = false;
+ this.hasErrors = false;
+ },
+ validateProjectBillingSuccessHandler() {
+ this.hasErrors = !this.projectHasBillingEnabled;
+ },
+ validateProjectBillingFailureHandler(resp) {
+ this.hasErrors = true;
+
+ this.gapiError = resp.result ? resp.result.error.message : resp;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div
+ class="js-gcp-project-id-dropdown dropdown"
+ >
+ <dropdown-hidden-input
+ :name="fieldName"
+ :value="selectedProject.projectId"
+ />
+ <dropdown-button
+ :class="{
+ 'border-danger': hasErrors,
+ 'read-only': hasOneProject
+ }"
+ :is-disabled="isDisabled"
+ :is-loading="isLoading"
+ :toggle-text="toggleText"
+ />
+ <div class="dropdown-menu dropdown-select">
+ <dropdown-search-input
+ v-model="searchQuery"
+ :placeholder-text="s__('ClusterIntegration|Search projects')"
+ />
+ <div class="dropdown-content">
+ <ul>
+ <li v-show="!results.length">
+ <span class="menu-item">
+ {{ s__('ClusterIntegration|No projects matched your search') }}
+ </span>
+ </li>
+ <li
+ v-for="result in results"
+ :key="result.project_number"
+ >
+ <button
+ type="button"
+ @click.prevent="setItem(result)"
+ >
+ {{ result.name }}
+ </button>
+ </li>
+ </ul>
+ </div>
+ <div class="dropdown-loading">
+ <loading-icon />
+ </div>
+ </div>
+ </div>
+ <span
+ :class="{
+ 'text-danger': hasErrors,
+ 'text-muted': !hasErrors
+ }"
+ class="form-text"
+ v-html="helpText"
+ ></span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue
new file mode 100644
index 00000000000..b5476684c6a
--- /dev/null
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue
@@ -0,0 +1,118 @@
+<script>
+import { sprintf, s__ } from '~/locale';
+import { mapState, mapActions } from 'vuex';
+
+import gkeDropdownMixin from './gke_dropdown_mixin';
+
+export default {
+ name: 'GkeZoneDropdown',
+ mixins: [gkeDropdownMixin],
+ computed: {
+ ...mapState([
+ 'selectedProject',
+ 'selectedZone',
+ 'projects',
+ 'isValidatingProjectBilling',
+ 'projectHasBillingEnabled',
+ ]),
+ ...mapState({ items: 'zones' }),
+ isDisabled() {
+ return this.isLoading || this.isValidatingProjectBilling || !this.projectHasBillingEnabled;
+ },
+ toggleText() {
+ if (this.isLoading) {
+ return s__('ClusterIntegration|Fetching zones');
+ }
+
+ if (this.selectedZone) {
+ return this.selectedZone;
+ }
+
+ return !this.projectHasBillingEnabled
+ ? s__('ClusterIntegration|Select project to choose zone')
+ : s__('ClusterIntegration|Select zone');
+ },
+ errorMessage() {
+ return sprintf(
+ s__('ClusterIntegration|An error occured while trying to fetch project zones: %{error}'),
+ { error: this.gapiError },
+ );
+ },
+ },
+ watch: {
+ isValidatingProjectBilling(isValidating) {
+ this.hasErrors = false;
+
+ if (!isValidating && this.projectHasBillingEnabled) {
+ this.isLoading = true;
+
+ this.fetchZones()
+ .then(this.fetchSuccessHandler)
+ .catch(this.fetchFailureHandler);
+ }
+ },
+ },
+ methods: {
+ ...mapActions(['fetchZones']),
+ ...mapActions({ setItem: 'setZone' }),
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div
+ class="js-gcp-zone-dropdown dropdown"
+ >
+ <dropdown-hidden-input
+ :name="fieldName"
+ :value="selectedZone"
+ />
+ <dropdown-button
+ :class="{ 'border-danger': hasErrors }"
+ :is-disabled="isDisabled"
+ :is-loading="isLoading"
+ :toggle-text="toggleText"
+ />
+ <div class="dropdown-menu dropdown-select">
+ <dropdown-search-input
+ v-model="searchQuery"
+ :placeholder-text="s__('ClusterIntegration|Search zones')"
+ />
+ <div class="dropdown-content">
+ <ul>
+ <li v-show="!results.length">
+ <span class="menu-item">
+ {{ s__('ClusterIntegration|No zones matched your search') }}
+ </span>
+ </li>
+ <li
+ v-for="result in results"
+ :key="result.id"
+ >
+ <button
+ type="button"
+ @click.prevent="setItem(result.name)"
+ >
+ {{ result.name }}
+ </button>
+ </li>
+ </ul>
+ </div>
+ <div class="dropdown-loading">
+ <loading-icon />
+ </div>
+ </div>
+ </div>
+ <span
+ v-if="hasErrors"
+ :class="{
+ 'text-danger': hasErrors,
+ 'text-muted': !hasErrors
+ }"
+ class="form-text"
+ >
+ {{ errorMessage }}
+ </span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/constants.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/constants.js
new file mode 100644
index 00000000000..2a1c0819916
--- /dev/null
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/constants.js
@@ -0,0 +1,11 @@
+import { s__ } from '~/locale';
+
+export const GCP_API_ERROR = s__(
+ 'ClusterIntegration|An error occurred when trying to contact the Google Cloud API. Please try again later.',
+);
+export const GCP_API_CLOUD_BILLING_ENDPOINT =
+ 'https://www.googleapis.com/discovery/v1/apis/cloudbilling/v1/rest';
+export const GCP_API_CLOUD_RESOURCE_MANAGER_ENDPOINT =
+ 'https://www.googleapis.com/discovery/v1/apis/cloudresourcemanager/v1/rest';
+export const GCP_API_COMPUTE_ENDPOINT =
+ 'https://www.googleapis.com/discovery/v1/apis/compute/v1/rest';
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/index.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/index.js
new file mode 100644
index 00000000000..729b9404b64
--- /dev/null
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/index.js
@@ -0,0 +1,88 @@
+/* global gapi */
+import Vue from 'vue';
+import Flash from '~/flash';
+import GkeProjectIdDropdown from './components/gke_project_id_dropdown.vue';
+import GkeZoneDropdown from './components/gke_zone_dropdown.vue';
+import GkeMachineTypeDropdown from './components/gke_machine_type_dropdown.vue';
+import * as CONSTANTS from './constants';
+
+const mountComponent = (entryPoint, component, componentName, extraProps = {}) => {
+ const el = document.querySelector(entryPoint);
+ if (!el) return false;
+
+ const hiddenInput = el.querySelector('input');
+
+ return new Vue({
+ el,
+ components: {
+ [componentName]: component,
+ },
+ render: createElement =>
+ createElement(componentName, {
+ props: {
+ fieldName: hiddenInput.getAttribute('name'),
+ fieldId: hiddenInput.getAttribute('id'),
+ defaultValue: hiddenInput.value,
+ ...extraProps,
+ },
+ }),
+ });
+};
+
+const mountGkeProjectIdDropdown = () => {
+ const entryPoint = '.js-gcp-project-id-dropdown-entry-point';
+ const el = document.querySelector(entryPoint);
+
+ mountComponent(entryPoint, GkeProjectIdDropdown, 'gke-project-id-dropdown', {
+ docsUrl: el.dataset.docsurl,
+ });
+};
+
+const mountGkeZoneDropdown = () => {
+ mountComponent('.js-gcp-zone-dropdown-entry-point', GkeZoneDropdown, 'gke-zone-dropdown');
+};
+
+const mountGkeMachineTypeDropdown = () => {
+ mountComponent(
+ '.js-gcp-machine-type-dropdown-entry-point',
+ GkeMachineTypeDropdown,
+ 'gke-machine-type-dropdown',
+ );
+};
+
+const gkeDropdownErrorHandler = () => {
+ Flash(CONSTANTS.GCP_API_ERROR);
+};
+
+const initializeGapiClient = () => {
+ const el = document.querySelector('.js-gke-cluster-creation');
+ if (!el) return false;
+
+ return gapi.client
+ .init({
+ discoveryDocs: [
+ CONSTANTS.GCP_API_CLOUD_BILLING_ENDPOINT,
+ CONSTANTS.GCP_API_CLOUD_RESOURCE_MANAGER_ENDPOINT,
+ CONSTANTS.GCP_API_COMPUTE_ENDPOINT,
+ ],
+ })
+ .then(() => {
+ gapi.client.setToken({ access_token: el.dataset.token });
+
+ mountGkeProjectIdDropdown();
+ mountGkeZoneDropdown();
+ mountGkeMachineTypeDropdown();
+ })
+ .catch(gkeDropdownErrorHandler);
+};
+
+const initGkeDropdowns = () => {
+ if (!gapi) {
+ gkeDropdownErrorHandler();
+ return false;
+ }
+
+ return gapi.load('client', initializeGapiClient);
+};
+
+export default initGkeDropdowns;
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/store/actions.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/actions.js
new file mode 100644
index 00000000000..4834a856271
--- /dev/null
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/actions.js
@@ -0,0 +1,95 @@
+/* global gapi */
+import * as types from './mutation_types';
+
+const gapiResourceListRequest = ({ resource, params, commit, mutation, payloadKey }) =>
+ new Promise((resolve, reject) => {
+ const request = resource.list(params);
+
+ return request.then(
+ resp => {
+ const { result } = resp;
+
+ commit(mutation, result[payloadKey]);
+
+ resolve();
+ },
+ resp => {
+ reject(resp);
+ },
+ );
+ });
+
+export const setProject = ({ commit }, selectedProject) => {
+ commit(types.SET_PROJECT, selectedProject);
+};
+
+export const setZone = ({ commit }, selectedZone) => {
+ commit(types.SET_ZONE, selectedZone);
+};
+
+export const setMachineType = ({ commit }, selectedMachineType) => {
+ commit(types.SET_MACHINE_TYPE, selectedMachineType);
+};
+
+export const setIsValidatingProjectBilling = ({ commit }, isValidatingProjectBilling) => {
+ commit(types.SET_IS_VALIDATING_PROJECT_BILLING, isValidatingProjectBilling);
+};
+
+export const fetchProjects = ({ commit }) =>
+ gapiResourceListRequest({
+ resource: gapi.client.cloudresourcemanager.projects,
+ params: {},
+ commit,
+ mutation: types.SET_PROJECTS,
+ payloadKey: 'projects',
+ });
+
+export const validateProjectBilling = ({ dispatch, commit, state }) =>
+ new Promise((resolve, reject) => {
+ const request = gapi.client.cloudbilling.projects.getBillingInfo({
+ name: `projects/${state.selectedProject.projectId}`,
+ });
+
+ commit(types.SET_ZONE, '');
+ commit(types.SET_MACHINE_TYPE, '');
+
+ return request.then(
+ resp => {
+ const { billingEnabled } = resp.result;
+
+ commit(types.SET_PROJECT_BILLING_STATUS, !!billingEnabled);
+ dispatch('setIsValidatingProjectBilling', false);
+ resolve();
+ },
+ resp => {
+ dispatch('setIsValidatingProjectBilling', false);
+ reject(resp);
+ },
+ );
+ });
+
+export const fetchZones = ({ commit, state }) =>
+ gapiResourceListRequest({
+ resource: gapi.client.compute.zones,
+ params: {
+ project: state.selectedProject.projectId,
+ },
+ commit,
+ mutation: types.SET_ZONES,
+ payloadKey: 'items',
+ });
+
+export const fetchMachineTypes = ({ commit, state }) =>
+ gapiResourceListRequest({
+ resource: gapi.client.compute.machineTypes,
+ params: {
+ project: state.selectedProject.projectId,
+ zone: state.selectedZone,
+ },
+ commit,
+ mutation: types.SET_MACHINE_TYPES,
+ payloadKey: 'items',
+ });
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/store/getters.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/getters.js
new file mode 100644
index 00000000000..e39f02d0894
--- /dev/null
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/getters.js
@@ -0,0 +1,3 @@
+export const hasProject = state => !!state.selectedProject.projectId;
+export const hasZone = state => !!state.selectedZone;
+export const hasMachineType = state => !!state.selectedMachineType;
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/store/index.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/index.js
new file mode 100644
index 00000000000..5f72060633e
--- /dev/null
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/index.js
@@ -0,0 +1,18 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import * as actions from './actions';
+import * as getters from './getters';
+import mutations from './mutations';
+import createState from './state';
+
+Vue.use(Vuex);
+
+export const createStore = () =>
+ new Vuex.Store({
+ actions,
+ getters,
+ mutations,
+ state: createState(),
+ });
+
+export default createStore();
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/store/mutation_types.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/mutation_types.js
new file mode 100644
index 00000000000..45a91efc2d9
--- /dev/null
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/mutation_types.js
@@ -0,0 +1,8 @@
+export const SET_PROJECT = 'SET_PROJECT';
+export const SET_PROJECT_BILLING_STATUS = 'SET_PROJECT_BILLING_STATUS';
+export const SET_IS_VALIDATING_PROJECT_BILLING = 'SET_IS_VALIDATING_PROJECT_BILLING';
+export const SET_ZONE = 'SET_ZONE';
+export const SET_MACHINE_TYPE = 'SET_MACHINE_TYPE';
+export const SET_PROJECTS = 'SET_PROJECTS';
+export const SET_ZONES = 'SET_ZONES';
+export const SET_MACHINE_TYPES = 'SET_MACHINE_TYPES';
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/store/mutations.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/mutations.js
new file mode 100644
index 00000000000..88a2c1b630d
--- /dev/null
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/mutations.js
@@ -0,0 +1,28 @@
+import * as types from './mutation_types';
+
+export default {
+ [types.SET_PROJECT](state, selectedProject) {
+ Object.assign(state, { selectedProject });
+ },
+ [types.SET_IS_VALIDATING_PROJECT_BILLING](state, isValidatingProjectBilling) {
+ Object.assign(state, { isValidatingProjectBilling });
+ },
+ [types.SET_PROJECT_BILLING_STATUS](state, projectHasBillingEnabled) {
+ Object.assign(state, { projectHasBillingEnabled });
+ },
+ [types.SET_ZONE](state, selectedZone) {
+ Object.assign(state, { selectedZone });
+ },
+ [types.SET_MACHINE_TYPE](state, selectedMachineType) {
+ Object.assign(state, { selectedMachineType });
+ },
+ [types.SET_PROJECTS](state, projects) {
+ Object.assign(state, { projects });
+ },
+ [types.SET_ZONES](state, zones) {
+ Object.assign(state, { zones });
+ },
+ [types.SET_MACHINE_TYPES](state, machineTypes) {
+ Object.assign(state, { machineTypes });
+ },
+};
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/store/state.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/state.js
new file mode 100644
index 00000000000..9f3c473d4bc
--- /dev/null
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/state.js
@@ -0,0 +1,13 @@
+export default () => ({
+ selectedProject: {
+ projectId: '',
+ name: '',
+ },
+ selectedZone: '',
+ selectedMachineType: '',
+ isValidatingProjectBilling: null,
+ projectHasBillingEnabled: null,
+ projects: [],
+ zones: [],
+ machineTypes: [],
+});
diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js
index 93603dfc14d..002edb4663c 100644
--- a/app/assets/javascripts/projects/project_new.js
+++ b/app/assets/javascripts/projects/project_new.js
@@ -66,8 +66,8 @@ const bindEvents = () => {
.on('click', (e) => { e.preventDefault(); })
.popover({
title: $pushNewProjectTipTrigger.data('title'),
- placement: 'auto bottom',
- html: 'true',
+ placement: 'bottom',
+ html: true,
content: $('.push-new-project-tip-template').html(),
})
.on('shown.bs.popover', () => {
@@ -90,7 +90,7 @@ const bindEvents = () => {
function chooseTemplate() {
$('.template-option').hide();
$projectFieldsForm.addClass('selected');
- $selectedIcon.removeClass('active');
+ $selectedIcon.removeClass('d-block');
const value = $(this).val();
const templates = {
rails: {
@@ -109,7 +109,7 @@ const bindEvents = () => {
const selectedTemplate = templates[value];
$selectedTemplateText.text(selectedTemplate.text);
- $(selectedTemplate.icon).addClass('active');
+ $(selectedTemplate.icon).addClass('d-block');
$templateProjectNameInput.focus();
}
diff --git a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
index 63f20a0041d..a4c7c143e56 100644
--- a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
+++ b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
@@ -47,7 +47,7 @@
},
methods: {
successCallback(res) {
- const pipelines = res.data.pipelines;
+ const { pipelines } = res.data;
if (pipelines.length > 0) {
// The pipeline entity always keeps the latest pipeline info on the `details.status`
this.ciStatus = pipelines[0].details.status;
@@ -100,9 +100,9 @@
<template>
<div>
<loading-icon
+ v-if="isLoading"
label="Loading pipeline status"
size="3"
- v-if="isLoading"
/>
<a
v-else
@@ -112,8 +112,8 @@
v-tooltip
:title="statusTitle"
:aria-label="statusTitle"
- data-container="body"
:status="ciStatus"
+ data-container="body"
/>
</a>
</div>
diff --git a/app/assets/javascripts/projects_dropdown/components/app.vue b/app/assets/javascripts/projects_dropdown/components/app.vue
deleted file mode 100644
index 0bbd8a41753..00000000000
--- a/app/assets/javascripts/projects_dropdown/components/app.vue
+++ /dev/null
@@ -1,158 +0,0 @@
-<script>
-import bs from '../../breakpoints';
-import eventHub from '../event_hub';
-import loadingIcon from '../../vue_shared/components/loading_icon.vue';
-
-import projectsListFrequent from './projects_list_frequent.vue';
-import projectsListSearch from './projects_list_search.vue';
-
-import search from './search.vue';
-
-export default {
- components: {
- search,
- loadingIcon,
- projectsListFrequent,
- projectsListSearch,
- },
- props: {
- currentProject: {
- type: Object,
- required: true,
- },
- store: {
- type: Object,
- required: true,
- },
- service: {
- type: Object,
- required: true,
- },
- },
- data() {
- return {
- isLoadingProjects: false,
- isFrequentsListVisible: false,
- isSearchListVisible: false,
- isLocalStorageFailed: false,
- isSearchFailed: false,
- searchQuery: '',
- };
- },
- computed: {
- frequentProjects() {
- return this.store.getFrequentProjects();
- },
- searchProjects() {
- return this.store.getSearchedProjects();
- },
- },
- created() {
- if (this.currentProject.id) {
- this.logCurrentProjectAccess();
- }
-
- eventHub.$on('dropdownOpen', this.fetchFrequentProjects);
- eventHub.$on('searchProjects', this.fetchSearchedProjects);
- eventHub.$on('searchCleared', this.handleSearchClear);
- eventHub.$on('searchFailed', this.handleSearchFailure);
- },
- beforeDestroy() {
- eventHub.$off('dropdownOpen', this.fetchFrequentProjects);
- eventHub.$off('searchProjects', this.fetchSearchedProjects);
- eventHub.$off('searchCleared', this.handleSearchClear);
- eventHub.$off('searchFailed', this.handleSearchFailure);
- },
- methods: {
- toggleFrequentProjectsList(state) {
- this.isLoadingProjects = !state;
- this.isSearchListVisible = !state;
- this.isFrequentsListVisible = state;
- },
- toggleSearchProjectsList(state) {
- this.isLoadingProjects = !state;
- this.isFrequentsListVisible = !state;
- this.isSearchListVisible = state;
- },
- toggleLoader(state) {
- this.isFrequentsListVisible = !state;
- this.isSearchListVisible = !state;
- this.isLoadingProjects = state;
- },
- fetchFrequentProjects() {
- const screenSize = bs.getBreakpointSize();
- if (this.searchQuery && (screenSize !== 'sm' && screenSize !== 'xs')) {
- this.toggleSearchProjectsList(true);
- } else {
- this.toggleLoader(true);
- this.isLocalStorageFailed = false;
- const projects = this.service.getFrequentProjects();
- if (projects) {
- this.toggleFrequentProjectsList(true);
- this.store.setFrequentProjects(projects);
- } else {
- this.isLocalStorageFailed = true;
- this.toggleFrequentProjectsList(true);
- this.store.setFrequentProjects([]);
- }
- }
- },
- fetchSearchedProjects(searchQuery) {
- this.searchQuery = searchQuery;
- this.toggleLoader(true);
- this.service
- .getSearchedProjects(this.searchQuery)
- .then(res => res.json())
- .then(results => {
- this.toggleSearchProjectsList(true);
- this.store.setSearchedProjects(results);
- })
- .catch(() => {
- this.isSearchFailed = true;
- this.toggleSearchProjectsList(true);
- });
- },
- logCurrentProjectAccess() {
- this.service.logProjectAccess(this.currentProject);
- },
- handleSearchClear() {
- this.searchQuery = '';
- this.toggleFrequentProjectsList(true);
- this.store.clearSearchedProjects();
- },
- handleSearchFailure() {
- this.isSearchFailed = true;
- this.toggleSearchProjectsList(true);
- },
- },
-};
-</script>
-
-<template>
- <div>
- <search/>
- <loading-icon
- class="loading-animation prepend-top-20"
- size="2"
- v-if="isLoadingProjects"
- :label="s__('ProjectsDropdown|Loading projects')"
- />
- <div
- class="section-header"
- v-if="isFrequentsListVisible"
- >
- {{ s__('ProjectsDropdown|Frequently visited') }}
- </div>
- <projects-list-frequent
- v-if="isFrequentsListVisible"
- :local-storage-failed="isLocalStorageFailed"
- :projects="frequentProjects"
- />
- <projects-list-search
- v-if="isSearchListVisible"
- :search-failed="isSearchFailed"
- :matcher="searchQuery"
- :projects="searchProjects"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue b/app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue
deleted file mode 100644
index 246dbeaaded..00000000000
--- a/app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue
+++ /dev/null
@@ -1,57 +0,0 @@
-<script>
- import { s__ } from '../../locale';
- import projectsListItem from './projects_list_item.vue';
-
- export default {
- components: {
- projectsListItem,
- },
- props: {
- projects: {
- type: Array,
- required: true,
- },
- localStorageFailed: {
- type: Boolean,
- required: true,
- },
- },
- computed: {
- isListEmpty() {
- return this.projects.length === 0;
- },
- listEmptyMessage() {
- return this.localStorageFailed ?
- s__('ProjectsDropdown|This feature requires browser localStorage support') :
- s__('ProjectsDropdown|Projects you visit often will appear here');
- },
- },
- };
-</script>
-
-<template>
- <div
- class="projects-list-frequent-container"
- >
- <ul
- class="list-unstyled"
- >
- <li
- class="section-empty"
- v-if="isListEmpty"
- >
- {{ listEmptyMessage }}
- </li>
- <projects-list-item
- v-else
- v-for="(project, index) in projects"
- :key="index"
- :project-id="project.id"
- :project-name="project.name"
- :namespace="project.namespace"
- :web-url="project.webUrl"
- :avatar-url="project.avatarUrl"
- />
- </ul>
- </div>
-</template>
diff --git a/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue b/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue
deleted file mode 100644
index 759cdd1ded9..00000000000
--- a/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue
+++ /dev/null
@@ -1,116 +0,0 @@
-<script>
- /* eslint-disable vue/require-default-prop, vue/require-prop-types */
- import identicon from '../../vue_shared/components/identicon.vue';
-
- export default {
- components: {
- identicon,
- },
- props: {
- matcher: {
- type: String,
- required: false,
- },
- projectId: {
- type: Number,
- required: true,
- },
- projectName: {
- type: String,
- required: true,
- },
- namespace: {
- type: String,
- required: true,
- },
- webUrl: {
- type: String,
- required: true,
- },
- avatarUrl: {
- required: true,
- validator(value) {
- return value === null || typeof value === 'string';
- },
- },
- },
- computed: {
- hasAvatar() {
- return this.avatarUrl !== null;
- },
- highlightedProjectName() {
- if (this.matcher) {
- const matcherRegEx = new RegExp(this.matcher, 'gi');
- const matches = this.projectName.match(matcherRegEx);
-
- if (matches && matches.length > 0) {
- return this.projectName.replace(matches[0], `<b>${matches[0]}</b>`);
- }
- }
- return this.projectName;
- },
- /**
- * 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>
-
-<template>
- <li
- class="projects-list-item-container"
- >
- <a
- class="clearfix"
- :href="webUrl"
- >
- <div
- class="project-item-avatar-container"
- >
- <img
- v-if="hasAvatar"
- class="avatar s32"
- :src="avatarUrl"
- />
- <identicon
- v-else
- size-class="s32"
- :entity-id="projectId"
- :entity-name="projectName"
- />
- </div>
- <div
- class="project-item-metadata-container"
- >
- <div
- class="project-title"
- :title="projectName"
- v-html="highlightedProjectName"
- >
- </div>
- <div
- class="project-namespace"
- :title="namespace"
- >{{ truncatedNamespace }}</div>
- </div>
- </a>
- </li>
-</template>
diff --git a/app/assets/javascripts/projects_dropdown/components/projects_list_search.vue b/app/assets/javascripts/projects_dropdown/components/projects_list_search.vue
deleted file mode 100644
index 8d0c29177e6..00000000000
--- a/app/assets/javascripts/projects_dropdown/components/projects_list_search.vue
+++ /dev/null
@@ -1,63 +0,0 @@
-<script>
-import { s__ } from '../../locale';
-import projectsListItem from './projects_list_item.vue';
-
-export default {
- components: {
- projectsListItem,
- },
- props: {
- matcher: {
- type: String,
- required: true,
- },
- projects: {
- type: Array,
- required: true,
- },
- searchFailed: {
- type: Boolean,
- required: true,
- },
- },
- computed: {
- isListEmpty() {
- return this.projects.length === 0;
- },
- listEmptyMessage() {
- return this.searchFailed ?
- s__('ProjectsDropdown|Something went wrong on our end.') :
- s__('ProjectsDropdown|Sorry, no projects matched your search');
- },
- },
-};
-</script>
-
-<template>
- <div
- class="projects-list-search-container"
- >
- <ul
- class="list-unstyled"
- >
- <li
- v-if="isListEmpty"
- :class="{ 'section-failure': searchFailed }"
- class="section-empty"
- >
- {{ listEmptyMessage }}
- </li>
- <projects-list-item
- v-else
- v-for="(project, index) in projects"
- :key="index"
- :project-id="project.id"
- :project-name="project.name"
- :namespace="project.namespace"
- :web-url="project.webUrl"
- :avatar-url="project.avatarUrl"
- :matcher="matcher"
- />
- </ul>
- </div>
-</template>
diff --git a/app/assets/javascripts/projects_dropdown/components/search.vue b/app/assets/javascripts/projects_dropdown/components/search.vue
deleted file mode 100644
index 0c46ed184be..00000000000
--- a/app/assets/javascripts/projects_dropdown/components/search.vue
+++ /dev/null
@@ -1,65 +0,0 @@
-<script>
- import _ from 'underscore';
- import eventHub from '../event_hub';
-
- export default {
- data() {
- return {
- searchQuery: '',
- };
- },
- watch: {
- searchQuery() {
- this.handleInput();
- },
- },
- mounted() {
- eventHub.$on('dropdownOpen', this.setFocus);
- },
- beforeDestroy() {
- eventHub.$off('dropdownOpen', this.setFocus);
- },
- methods: {
- setFocus() {
- this.$refs.search.focus();
- },
- emitSearchEvents() {
- if (this.searchQuery) {
- eventHub.$emit('searchProjects', this.searchQuery);
- } else {
- eventHub.$emit('searchCleared');
- }
- },
- /**
- * Callback function within _.debounce is intentionally
- * kept as ES5 `function() {}` instead of ES6 `() => {}`
- * as it otherwise messes up function context
- * and component reference is no longer accessible via `this`
- */
- // eslint-disable-next-line func-names
- handleInput: _.debounce(function () {
- this.emitSearchEvents();
- }, 500),
- },
- };
-</script>
-
-<template>
- <div
- class="search-input-container hidden-xs"
- >
- <input
- type="search"
- class="form-control"
- ref="search"
- v-model="searchQuery"
- :placeholder="s__('ProjectsDropdown|Search your projects')"
- />
- <i
- v-if="!searchQuery"
- class="search-icon fa fa-fw fa-search"
- aria-hidden="true"
- >
- </i>
- </div>
-</template>
diff --git a/app/assets/javascripts/projects_dropdown/constants.js b/app/assets/javascripts/projects_dropdown/constants.js
deleted file mode 100644
index 8937097184c..00000000000
--- a/app/assets/javascripts/projects_dropdown/constants.js
+++ /dev/null
@@ -1,10 +0,0 @@
-export const FREQUENT_PROJECTS = {
- MAX_COUNT: 20,
- LIST_COUNT_DESKTOP: 5,
- LIST_COUNT_MOBILE: 3,
- ELIGIBLE_FREQUENCY: 3,
-};
-
-export const HOUR_IN_MS = 3600000;
-
-export const STORAGE_KEY = 'frequent-projects';
diff --git a/app/assets/javascripts/projects_dropdown/index.js b/app/assets/javascripts/projects_dropdown/index.js
deleted file mode 100644
index e1ca70c51a6..00000000000
--- a/app/assets/javascripts/projects_dropdown/index.js
+++ /dev/null
@@ -1,66 +0,0 @@
-import $ from 'jquery';
-import Vue from 'vue';
-
-import Translate from '../vue_shared/translate';
-import eventHub from './event_hub';
-import ProjectsService from './service/projects_service';
-import ProjectsStore from './store/projects_store';
-
-import projectsDropdownApp from './components/app.vue';
-
-Vue.use(Translate);
-
-document.addEventListener('DOMContentLoaded', () => {
- const el = document.getElementById('js-projects-dropdown');
- const navEl = document.getElementById('nav-projects-dropdown');
-
- // Don't do anything if element doesn't exist (No projects dropdown)
- // This is for when the user accesses GitLab without logging in
- if (!el || !navEl) {
- return;
- }
-
- $(navEl).on('shown.bs.dropdown', () => {
- eventHub.$emit('dropdownOpen');
- });
-
- // eslint-disable-next-line no-new
- new Vue({
- el,
- components: {
- projectsDropdownApp,
- },
- data() {
- const dataset = this.$options.el.dataset;
- const store = new ProjectsStore();
- const service = new ProjectsService(dataset.userName);
-
- const project = {
- id: Number(dataset.projectId),
- name: dataset.projectName,
- namespace: dataset.projectNamespace,
- webUrl: dataset.projectWebUrl,
- avatarUrl: dataset.projectAvatarUrl || null,
- lastAccessedOn: Date.now(),
- };
-
- return {
- store,
- service,
- state: store.state,
- currentUserName: dataset.userName,
- currentProject: project,
- };
- },
- render(createElement) {
- return createElement('projects-dropdown-app', {
- props: {
- currentUserName: this.currentUserName,
- currentProject: this.currentProject,
- store: this.store,
- service: this.service,
- },
- });
- },
- });
-});
diff --git a/app/assets/javascripts/projects_dropdown/service/projects_service.js b/app/assets/javascripts/projects_dropdown/service/projects_service.js
deleted file mode 100644
index ed1c3deead2..00000000000
--- a/app/assets/javascripts/projects_dropdown/service/projects_service.js
+++ /dev/null
@@ -1,137 +0,0 @@
-import _ from 'underscore';
-import Vue from 'vue';
-import VueResource from 'vue-resource';
-
-import bp from '../../breakpoints';
-import Api from '../../api';
-import AccessorUtilities from '../../lib/utils/accessor';
-
-import { FREQUENT_PROJECTS, HOUR_IN_MS, STORAGE_KEY } from '../constants';
-
-Vue.use(VueResource);
-
-export default class ProjectsService {
- constructor(currentUserName) {
- this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
- this.currentUserName = currentUserName;
- this.storageKey = `${this.currentUserName}/${STORAGE_KEY}`;
- this.projectsPath = Vue.resource(Api.buildUrl(Api.projectsPath));
- }
-
- getSearchedProjects(searchQuery) {
- return this.projectsPath.get({
- simple: true,
- per_page: 20,
- membership: !!gon.current_user_id,
- order_by: 'last_activity_at',
- search: searchQuery,
- });
- }
-
- getFrequentProjects() {
- if (this.isLocalStorageAvailable) {
- return this.getTopFrequentProjects();
- }
- return null;
- }
-
- logProjectAccess(project) {
- let matchFound = false;
- let storedFrequentProjects;
-
- if (this.isLocalStorageAvailable) {
- const storedRawProjects = localStorage.getItem(this.storageKey);
-
- // Check if there's any frequent projects list set
- if (!storedRawProjects) {
- // No frequent projects list set, set one up.
- storedFrequentProjects = [];
- storedFrequentProjects.push({ ...project, frequency: 1 });
- } else {
- // Check if project is already present in frequents list
- // When found, update metadata of it.
- storedFrequentProjects = JSON.parse(storedRawProjects).map(projectItem => {
- if (projectItem.id === project.id) {
- matchFound = true;
- const diff = Math.abs(project.lastAccessedOn - projectItem.lastAccessedOn) / HOUR_IN_MS;
- const updatedProject = {
- ...project,
- frequency: projectItem.frequency,
- lastAccessedOn: projectItem.lastAccessedOn,
- };
-
- // Check if duration since last access of this project
- // is over an hour
- if (diff > 1) {
- return {
- ...updatedProject,
- frequency: updatedProject.frequency + 1,
- lastAccessedOn: Date.now(),
- };
- }
-
- return {
- ...updatedProject,
- };
- }
-
- return projectItem;
- });
-
- // Check whether currently logged project is present in frequents list
- if (!matchFound) {
- // We always keep size of frequents collection to 20 projects
- // out of which only 5 projects with
- // highest value of `frequency` and most recent `lastAccessedOn`
- // are shown in projects dropdown
- if (storedFrequentProjects.length === FREQUENT_PROJECTS.MAX_COUNT) {
- storedFrequentProjects.shift(); // Remove an item from head of array
- }
-
- storedFrequentProjects.push({ ...project, frequency: 1 });
- }
- }
-
- localStorage.setItem(this.storageKey, JSON.stringify(storedFrequentProjects));
- }
- }
-
- getTopFrequentProjects() {
- const storedFrequentProjects = JSON.parse(localStorage.getItem(this.storageKey));
- let frequentProjectsCount = FREQUENT_PROJECTS.LIST_COUNT_DESKTOP;
-
- if (!storedFrequentProjects) {
- return [];
- }
-
- if (bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'xs') {
- frequentProjectsCount = FREQUENT_PROJECTS.LIST_COUNT_MOBILE;
- }
-
- const frequentProjects = storedFrequentProjects.filter(
- project => project.frequency >= FREQUENT_PROJECTS.ELIGIBLE_FREQUENCY,
- );
-
- if (!frequentProjects || frequentProjects.length === 0) {
- return [];
- }
-
- // Sort all frequent projects in decending order of frequency
- // and then by lastAccessedOn with recent most first
- frequentProjects.sort((projectA, projectB) => {
- if (projectA.frequency < projectB.frequency) {
- return 1;
- } else if (projectA.frequency > projectB.frequency) {
- return -1;
- } else if (projectA.lastAccessedOn < projectB.lastAccessedOn) {
- return 1;
- } else if (projectA.lastAccessedOn > projectB.lastAccessedOn) {
- return -1;
- }
-
- return 0;
- });
-
- return _.first(frequentProjects, frequentProjectsCount);
- }
-}
diff --git a/app/assets/javascripts/projects_dropdown/store/projects_store.js b/app/assets/javascripts/projects_dropdown/store/projects_store.js
deleted file mode 100644
index ffefbe693f4..00000000000
--- a/app/assets/javascripts/projects_dropdown/store/projects_store.js
+++ /dev/null
@@ -1,33 +0,0 @@
-export default class ProjectsStore {
- constructor() {
- this.state = {};
- this.state.frequentProjects = [];
- this.state.searchedProjects = [];
- }
-
- setFrequentProjects(rawProjects) {
- this.state.frequentProjects = rawProjects;
- }
-
- getFrequentProjects() {
- return this.state.frequentProjects;
- }
-
- setSearchedProjects(rawProjects) {
- this.state.searchedProjects = rawProjects.map(rawProject => ({
- id: rawProject.id,
- name: rawProject.name,
- namespace: rawProject.name_with_namespace,
- webUrl: rawProject.web_url,
- avatarUrl: rawProject.avatar_url,
- }));
- }
-
- getSearchedProjects() {
- return this.state.searchedProjects;
- }
-
- clearSearchedProjects() {
- this.state.searchedProjects = [];
- }
-}
diff --git a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js
index 0a60f4845b2..078ccbbbac2 100644
--- a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js
+++ b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js
@@ -31,7 +31,7 @@ export default class PrometheusMetrics {
/* eslint-disable class-methods-use-this */
handlePanelToggle(e) {
const $toggleBtn = $(e.currentTarget);
- const $currentPanelBody = $toggleBtn.closest('.panel').find('.panel-body');
+ const $currentPanelBody = $toggleBtn.closest('.card').find('.card-body');
$currentPanelBody.toggleClass('hidden');
if ($toggleBtn.hasClass('fa-caret-down')) {
$toggleBtn.removeClass('fa-caret-down').addClass('fa-caret-right');
@@ -107,7 +107,7 @@ export default class PrometheusMetrics {
if (data && data.success) {
stop(data);
} else {
- this.backOffRequestCounter = this.backOffRequestCounter += 1;
+ this.backOffRequestCounter += 1;
if (this.backOffRequestCounter < 3) {
next();
} else {
diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js b/app/assets/javascripts/protected_branches/protected_branch_create.js
index 7c61c070a35..b601b19e7be 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_create.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_create.js
@@ -1,11 +1,8 @@
import $ from 'jquery';
-import _ from 'underscore';
import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown';
import CreateItemDropdown from '../create_item_dropdown';
import AccessorUtilities from '../lib/utils/accessor';
-const PB_LOCAL_STORAGE_KEY = 'protected-branches-defaults';
-
export default class ProtectedBranchCreate {
constructor() {
this.$form = $('.js-new-protected-branch');
@@ -43,8 +40,6 @@ export default class ProtectedBranchCreate {
onSelect: this.onSelectCallback,
getData: ProtectedBranchCreate.getProtectedBranches,
});
-
- this.loadPreviousSelection($allowedToMergeDropdown.data('glDropdown'), $allowedToPushDropdown.data('glDropdown'));
}
// This will run after clicked callback
@@ -59,39 +54,10 @@ export default class ProtectedBranchCreate {
$allowedToPushInput.length
);
- this.savePreviousSelection($allowedToMergeInput.val(), $allowedToPushInput.val());
this.$form.find('input[type="submit"]').prop('disabled', completedForm);
}
static getProtectedBranches(term, callback) {
callback(gon.open_branches);
}
-
- loadPreviousSelection(mergeDropdown, pushDropdown) {
- let mergeIndex = 0;
- let pushIndex = 0;
- if (this.isLocalStorageAvailable) {
- const savedDefaults = JSON.parse(window.localStorage.getItem(PB_LOCAL_STORAGE_KEY));
- if (savedDefaults != null) {
- mergeIndex = _.findLastIndex(mergeDropdown.fullData.roles, {
- id: parseInt(savedDefaults.mergeSelection, 0),
- });
- pushIndex = _.findLastIndex(pushDropdown.fullData.roles, {
- id: parseInt(savedDefaults.pushSelection, 0),
- });
- }
- }
- mergeDropdown.selectRowAtIndex(mergeIndex);
- pushDropdown.selectRowAtIndex(pushIndex);
- }
-
- savePreviousSelection(mergeSelection, pushSelection) {
- if (this.isLocalStorageAvailable) {
- const branchDefaults = {
- mergeSelection,
- pushSelection,
- };
- window.localStorage.setItem(PB_LOCAL_STORAGE_KEY, JSON.stringify(branchDefaults));
- }
- }
}
diff --git a/app/assets/javascripts/raven/raven_config.js b/app/assets/javascripts/raven/raven_config.js
index ae54fa5f1a9..658caeecde1 100644
--- a/app/assets/javascripts/raven/raven_config.js
+++ b/app/assets/javascripts/raven/raven_config.js
@@ -37,7 +37,7 @@ const IGNORE_URLS = [
/extensions\//i,
/^chrome:\/\//i,
// Other plugins
- /127\.0\.0\.1:4001\/isrunning/i, // Cacaoweb
+ /127\.0\.0\.1:4001\/isrunning/i, // Cacaoweb
/webappstoolbarba\.texthelp\.com\//i,
/metrics\.itunes\.apple\.com\.edgesuite\.net\//i,
];
diff --git a/app/assets/javascripts/registry/components/app.vue b/app/assets/javascripts/registry/components/app.vue
index ea0f7199a70..31f88675912 100644
--- a/app/assets/javascripts/registry/components/app.vue
+++ b/app/assets/javascripts/registry/components/app.vue
@@ -48,8 +48,8 @@
/>
<collapsible-container
- v-else-if="!isLoading && repos.length"
v-for="(item, index) in repos"
+ v-else-if="!isLoading && repos.length"
:key="index"
:repo="item"
/>
diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue
index 2ce43ef0125..4116c4a0489 100644
--- a/app/assets/javascripts/registry/components/collapsible_container.vue
+++ b/app/assets/javascripts/registry/components/collapsible_container.vue
@@ -62,15 +62,15 @@
<div class="container-image-head">
<button
type="button"
- @click="toggleRepo"
class="js-toggle-repo btn-link"
+ @click="toggleRepo"
>
<i
- class="fa"
:class="{
'fa-chevron-right': !isOpen,
'fa-chevron-up': isOpen,
}"
+ class="fa"
aria-hidden="true"
>
</i>
@@ -84,14 +84,14 @@
css-class="btn-default btn-transparent btn-clipboard"
/>
- <div class="controls hidden-xs pull-right">
+ <div class="controls d-none d-sm-block float-right">
<button
+ v-tooltip
v-if="repo.canDelete"
- type="button"
- class="js-remove-repo btn btn-danger"
:title="s__('ContainerRegistry|Remove repository')"
:aria-label="s__('ContainerRegistry|Remove repository')"
- v-tooltip
+ type="button"
+ class="js-remove-repo btn btn-danger"
@click="handleDeleteRepository"
>
<i
diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue
index 673b1db6769..9f4973c3490 100644
--- a/app/assets/javascripts/registry/components/table_registry.vue
+++ b/app/assets/javascripts/registry/components/table_registry.vue
@@ -118,13 +118,13 @@
<td class="content">
<button
+ v-tooltip
v-if="item.canDelete"
- type="button"
- class="js-delete-registry btn btn-danger hidden-xs pull-right"
:title="s__('ContainerRegistry|Remove tag')"
:aria-label="s__('ContainerRegistry|Remove tag')"
+ type="button"
+ class="js-delete-registry btn btn-danger d-none d-sm-block float-right"
data-container="body"
- v-tooltip
@click="handleDeleteRegistry(item)"
>
<i
diff --git a/app/assets/javascripts/registry/index.js b/app/assets/javascripts/registry/index.js
index 6fb125192b2..e15cd94a915 100644
--- a/app/assets/javascripts/registry/index.js
+++ b/app/assets/javascripts/registry/index.js
@@ -10,7 +10,7 @@ export default () => new Vue({
registryApp,
},
data() {
- const dataset = document.querySelector(this.$options.el).dataset;
+ const { dataset } = document.querySelector(this.$options.el);
return {
endpoint: dataset.endpoint,
};
diff --git a/app/assets/javascripts/registry/stores/actions.js b/app/assets/javascripts/registry/stores/actions.js
index 593a43c7cc1..a78aa90b7b5 100644
--- a/app/assets/javascripts/registry/stores/actions.js
+++ b/app/assets/javascripts/registry/stores/actions.js
@@ -7,9 +7,10 @@ Vue.use(VueResource);
export const fetchRepos = ({ commit, state }) => {
commit(types.TOGGLE_MAIN_LOADING);
- return Vue.http.get(state.endpoint)
+ return Vue.http
+ .get(state.endpoint)
.then(res => res.json())
- .then((response) => {
+ .then(response => {
commit(types.TOGGLE_MAIN_LOADING);
commit(types.SET_REPOS_LIST, response);
});
@@ -18,19 +19,20 @@ export const fetchRepos = ({ commit, state }) => {
export const fetchList = ({ commit }, { repo, page }) => {
commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo);
- return Vue.http.get(repo.tagsPath, { params: { page } })
- .then((response) => {
- const headers = response.headers;
+ return Vue.http.get(repo.tagsPath, { params: { page } }).then(response => {
+ const { headers } = response;
- return response.json().then((resp) => {
- commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo);
- commit(types.SET_REGISTRY_LIST, { repo, resp, headers });
- });
+ return response.json().then(resp => {
+ commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo);
+ commit(types.SET_REGISTRY_LIST, { repo, resp, headers });
});
+ });
};
+// eslint-disable-next-line no-unused-vars
export const deleteRepo = ({ commit }, repo) => Vue.http.delete(repo.destroyPath);
+// eslint-disable-next-line no-unused-vars
export const deleteRegistry = ({ commit }, image) => Vue.http.delete(image.destroyPath);
export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data);
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index 6eb0b62fa1c..b27d635c6ac 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-vars, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, object-shorthand, comma-dangle, no-else-return, no-param-reassign, max-len */
+/* eslint-disable func-names, no-var, no-unused-vars, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, no-else-return, no-param-reassign, max-len */
import $ from 'jquery';
import _ from 'underscore';
@@ -108,7 +108,7 @@ Sidebar.prototype.todoUpdateDone = function(data) {
.attr('title', $el.data(`${attrPrefix}Text`));
if ($el.hasClass('has-tooltip')) {
- $el.tooltip('fixTitle');
+ $el.tooltip('_fixTitle');
}
if ($el.data(`${attrPrefix}Icon`)) {
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js
index 2da022fde63..5b2e0468784 100644
--- a/app/assets/javascripts/search_autocomplete.js
+++ b/app/assets/javascripts/search_autocomplete.js
@@ -1,4 +1,4 @@
-/* eslint-disable no-return-assign, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-unused-vars, no-cond-assign, consistent-return, object-shorthand, prefer-arrow-callback, func-names, space-before-function-paren, prefer-template, quotes, class-methods-use-this, no-sequences, wrap-iife, no-lonely-if, no-else-return, no-param-reassign, vars-on-top, max-len */
+/* eslint-disable no-return-assign, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-unused-vars, consistent-return, object-shorthand, prefer-template, quotes, class-methods-use-this, no-lonely-if, no-else-return, vars-on-top, max-len */
import $ from 'jquery';
import axios from './lib/utils/axios_utils';
@@ -289,7 +289,7 @@ export default class SearchAutocomplete {
}
// If the dropdown is closed, we'll open it
- if (!this.dropdown.hasClass('open')) {
+ if (!this.dropdown.hasClass('show')) {
this.loadingSuggestions = false;
this.dropdownToggle.dropdown('toggle');
return this.searchInput.removeClass('disabled');
@@ -424,9 +424,9 @@ export default class SearchAutocomplete {
}
disableAutocomplete() {
- if (!this.searchInput.hasClass('disabled') && this.dropdown.hasClass('open')) {
+ if (!this.searchInput.hasClass('disabled') && this.dropdown.hasClass('show')) {
this.searchInput.addClass('disabled');
- this.dropdown.removeClass('open').trigger('hidden.bs.dropdown');
+ this.dropdown.removeClass('show').trigger('hidden.bs.dropdown');
this.restoreMenu();
}
}
@@ -438,7 +438,7 @@ export default class SearchAutocomplete {
}
onClick(item, $el, e) {
- if (location.pathname.indexOf(item.url) !== -1) {
+ if (window.location.pathname.indexOf(item.url) !== -1) {
if (!e.metaKey) e.preventDefault();
if (!this.badgePresent) {
if (item.category === 'Projects') {
diff --git a/app/assets/javascripts/settings_panels.js b/app/assets/javascripts/settings_panels.js
index eecde4550f9..37b4a2a4c63 100644
--- a/app/assets/javascripts/settings_panels.js
+++ b/app/assets/javascripts/settings_panels.js
@@ -42,8 +42,8 @@ export default function initSettingsPanels() {
}
});
- if (location.hash) {
- const $target = $(location.hash);
+ if (window.location.hash) {
+ const $target = $(window.location.hash);
if ($target.length && $target.hasClass('settings')) {
expandSection($target);
}
diff --git a/app/assets/javascripts/shared/milestones/form.js b/app/assets/javascripts/shared/milestones/form.js
index 2f974d6ff9d..8681a1776c6 100644
--- a/app/assets/javascripts/shared/milestones/form.js
+++ b/app/assets/javascripts/shared/milestones/form.js
@@ -6,5 +6,14 @@ import GLForm from '../../gl_form';
export default (initGFM = true) => {
new ZenMode(); // eslint-disable-line no-new
new DueDateSelectors(); // eslint-disable-line no-new
- new GLForm($('.milestone-form'), initGFM); // eslint-disable-line no-new
+ // eslint-disable-next-line no-new
+ new GLForm($('.milestone-form'), {
+ emojis: true,
+ members: initGFM,
+ issues: initGFM,
+ mergeRequests: initGFM,
+ epics: initGFM,
+ milestones: initGFM,
+ labels: initGFM,
+ });
};
diff --git a/app/assets/javascripts/shortcuts_find_file.js b/app/assets/javascripts/shortcuts_find_file.js
index 1e246a56b85..8658081c6c2 100644
--- a/app/assets/javascripts/shortcuts_find_file.js
+++ b/app/assets/javascripts/shortcuts_find_file.js
@@ -13,8 +13,8 @@ export default class ShortcutsFindFile extends ShortcutsNavigation {
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();
+ // when press up/down key in textbox, cursor prevent to move to home/end
+ e.preventDefault();
return false;
}
diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js
index 193788f754f..e9451be31fd 100644
--- a/app/assets/javascripts/shortcuts_issuable.js
+++ b/app/assets/javascripts/shortcuts_issuable.js
@@ -9,12 +9,10 @@ export default class ShortcutsIssuable extends Shortcuts {
constructor(isMergeRequest) {
super();
- this.$replyField = isMergeRequest ? $('.js-main-target-form #note_note') : $('.js-main-target-form .js-vue-comment-form');
-
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('r', ShortcutsIssuable.replyWithSelectedText);
Mousetrap.bind('e', ShortcutsIssuable.editIssue);
if (isMergeRequest) {
@@ -24,11 +22,16 @@ export default class ShortcutsIssuable extends Shortcuts {
}
}
- replyWithSelectedText() {
+ static replyWithSelectedText() {
+ const $replyField = $('.js-main-target-form .js-vue-comment-form');
const documentFragment = window.gl.utils.getSelectedFragment();
+ if (!$replyField.length) {
+ return false;
+ }
+
if (!documentFragment) {
- this.$replyField.focus();
+ $replyField.focus();
return false;
}
@@ -39,21 +42,22 @@ export default class ShortcutsIssuable extends Shortcuts {
return false;
}
- const quote = _.map(selected.split('\n'), val => `${(`> ${val}`).trim()}\n`);
+ 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`)
+ const separator = ($replyField.val().trim() !== '' && '\n\n') || '';
+ $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);
+ $replyField.get(0).dispatchEvent(event);
// Focus the input field
- this.$replyField.focus();
+ $replyField.focus();
return false;
}
diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js
index 78f7353eb0d..6b595764bc5 100644
--- a/app/assets/javascripts/shortcuts_navigation.js
+++ b/app/assets/javascripts/shortcuts_navigation.js
@@ -20,6 +20,7 @@ export default class ShortcutsNavigation extends Shortcuts {
Mousetrap.bind('g s', () => findAndFollowLink('.shortcuts-snippets'));
Mousetrap.bind('g k', () => findAndFollowLink('.shortcuts-kubernetes'));
Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-environments'));
+ Mousetrap.bind('g l', () => findAndFollowLink('.shortcuts-metrics'));
Mousetrap.bind('i', () => findAndFollowLink('.shortcuts-new-issue'));
this.enabledHelp.push('.hidden-shortcut.project');
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
index 5eeb2a41bae..284a258d3c9 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
@@ -41,7 +41,7 @@ export default {
</i>
<a
v-if="editable"
- class="js-sidebar-dropdown-toggle edit-link pull-right"
+ class="js-sidebar-dropdown-toggle edit-link float-right"
href="#"
>
{{ __('Edit') }}
@@ -49,7 +49,7 @@ export default {
<a
v-if="showToggle"
aria-label="Toggle sidebar"
- class="gutter-toggle pull-right js-sidebar-toggle"
+ class="gutter-toggle float-right js-sidebar-toggle"
href="#"
role="button"
>
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
index 2d00e8ac7e0..d22a1e1ac66 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
@@ -125,12 +125,13 @@ export default {
<template>
<div>
<div
- class="sidebar-collapsed-icon sidebar-collapsed-user"
- :class="{ 'multiple-users': hasMoreThanOneAssignee }"
v-tooltip
+ :class="{ 'multiple-users': hasMoreThanOneAssignee }"
+ :title="collapsedTooltipTitle"
+ class="sidebar-collapsed-icon sidebar-collapsed-user"
data-container="body"
data-placement="left"
- :title="collapsedTooltipTitle"
+ data-boundary="viewport"
>
<i
v-if="hasNoUsers"
@@ -139,17 +140,17 @@ export default {
>
</i>
<button
- type="button"
- class="btn-link"
v-for="(user, index) in users"
v-if="shouldRenderCollapsedAssignee(index)"
:key="user.id"
+ type="button"
+ class="btn-link"
>
<img
- width="24"
- class="avatar avatar-inline s24"
:alt="assigneeAlt(user)"
:src="avatarUrl(user)"
+ width="24"
+ class="avatar avatar-inline s24"
/>
<span class="author">
{{ user.name }}
@@ -185,14 +186,14 @@ export default {
</template>
<template v-else-if="hasOneUser">
<a
- class="author_link bold"
:href="assigneeUrl(firstUser)"
+ class="author_link bold"
>
<img
- width="32"
- class="avatar avatar-inline s32"
:alt="assigneeAlt(firstUser)"
:src="avatarUrl(firstUser)"
+ width="32"
+ class="avatar avatar-inline s32"
/>
<span class="author">
{{ firstUser.name }}
@@ -205,23 +206,23 @@ export default {
<template v-else>
<div class="user-list">
<div
- class="user-item"
v-for="(user, index) in users"
v-if="renderAssignee(index)"
:key="user.id"
+ class="user-item"
>
<a
+ :href="assigneeUrl(user)"
+ :data-title="user.name"
class="user-link has-tooltip"
data-container="body"
data-placement="bottom"
- :href="assigneeUrl(user)"
- :data-title="user.name"
>
<img
- width="32"
- class="avatar avatar-inline s32"
:alt="assigneeAlt(user)"
:src="avatarUrl(user)"
+ width="32"
+ class="avatar avatar-inline s32"
/>
</a>
</div>
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
index b04a2eff798..123c92aff64 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
@@ -90,12 +90,12 @@ export default {
/>
<assignees
v-if="!store.isFetching.assignees"
- class="value"
:root-path="store.rootPath"
:users="store.assignees"
:editable="store.editable"
- @assign-self="assignSelf"
:issuable-type="issuableType"
+ class="value"
+ @assign-self="assignSelf"
/>
</div>
</template>
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 7f0de722f61..2b8d6207dea 100644
--- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
@@ -54,7 +54,7 @@ export default {
updateConfidentialAttribute(confidential) {
this.service
.update('issue', { confidential })
- .then(() => location.reload())
+ .then(() => window.location.reload())
.catch(() => {
Flash(
__(
@@ -70,12 +70,13 @@ export default {
<template>
<div class="block issuable-sidebar-item confidentiality">
<div
- class="sidebar-collapsed-icon"
- @click="toggleForm"
v-tooltip
+ :title="tooltipLabel"
+ class="sidebar-collapsed-icon"
data-container="body"
data-placement="left"
- :title="tooltipLabel"
+ data-boundary="viewport"
+ @click="toggleForm"
>
<icon
:name="confidentialityIcon"
@@ -86,7 +87,7 @@ export default {
{{ __('Confidentiality') }}
<a
v-if="isEditable"
- class="pull-right confidential-edit"
+ class="float-right confidential-edit"
href="#"
@click.prevent="toggleForm"
>
@@ -103,8 +104,8 @@ export default {
v-if="!isConfidential"
class="no-value sidebar-item-value">
<icon
- name="eye"
:size="16"
+ name="eye"
aria-hidden="true"
class="sidebar-item-icon inline"
/>
@@ -114,8 +115,8 @@ export default {
v-else
class="value sidebar-item-value hide-collapsed">
<icon
- name="eye-slash"
:size="16"
+ name="eye-slash"
aria-hidden="true"
class="sidebar-item-icon inline is-active"
/>
diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue
index 3783f71a848..4165aa19acf 100644
--- a/app/assets/javascripts/sidebar/components/confidential/edit_form.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue
@@ -32,7 +32,7 @@ export default {
</script>
<template>
- <div class="dropdown open">
+ <div class="dropdown show">
<div class="dropdown-menu sidebar-item-warning-message">
<div>
<p
diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form.vue b/app/assets/javascripts/sidebar/components/lock/edit_form.vue
index e1e4715826a..4906dad22e1 100644
--- a/app/assets/javascripts/sidebar/components/lock/edit_form.vue
+++ b/app/assets/javascripts/sidebar/components/lock/edit_form.vue
@@ -41,17 +41,17 @@ export default {
</script>
<template>
- <div class="dropdown open">
+ <div class="dropdown show">
<div class="dropdown-menu sidebar-item-warning-message">
<p
- class="text"
v-if="isLocked"
+ class="text"
v-html="unlockWarning">
</p>
<p
- class="text"
v-else
+ class="text"
v-html="lockWarning">
</p>
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 1a5e7b67eca..8bbc59f623a 100644
--- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
@@ -76,7 +76,7 @@ export default {
.update(this.issuableType, {
discussion_locked: locked,
})
- .then(() => location.reload())
+ .then(() => window.location.reload())
.catch(() =>
Flash(
this.__(
@@ -94,12 +94,13 @@ export default {
<template>
<div class="block issuable-sidebar-item lock">
<div
- class="sidebar-collapsed-icon"
- @click="toggleForm"
v-tooltip
+ :title="tooltipLabel"
+ class="sidebar-collapsed-icon"
data-container="body"
data-placement="left"
- :title="tooltipLabel"
+ data-boundary="viewport"
+ @click="toggleForm"
>
<icon
:name="lockIcon"
@@ -112,7 +113,7 @@ export default {
{{ sprintf(__('Lock %{issuableDisplayName}'), { issuableDisplayName: issuableDisplayName }) }}
<button
v-if="isEditable"
- class="pull-right lock-edit"
+ class="float-right lock-edit"
type="button"
@click.prevent="toggleForm"
>
@@ -133,8 +134,8 @@ export default {
class="value sidebar-item-value"
>
<icon
- name="lock"
:size="16"
+ name="lock"
aria-hidden="true"
class="sidebar-item-icon inline is-active"
/>
@@ -146,8 +147,8 @@ export default {
class="no-value sidebar-item-value hide-collapsed"
>
<icon
- name="lock-open"
:size="16"
+ name="lock-open"
aria-hidden="true"
class="sidebar-item-icon inline"
/>
diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue
index 8f9e6761d20..33dd6c981b6 100644
--- a/app/assets/javascripts/sidebar/components/participants/participants.vue
+++ b/app/assets/javascripts/sidebar/components/participants/participants.vue
@@ -80,11 +80,12 @@
<template>
<div>
<div
- class="sidebar-collapsed-icon"
v-tooltip
+ :title="participantLabel"
+ class="sidebar-collapsed-icon"
data-container="body"
data-placement="left"
- :title="participantLabel"
+ data-boundary="viewport"
@click="onClickCollapsedIcon"
>
<i
@@ -118,15 +119,15 @@
class="participants-author js-participants-author"
>
<a
- class="author_link"
:href="participant.web_url"
+ class="author_link"
>
<user-avatar-image
:lazy="true"
:img-src="participant.avatar_url"
- css-classes="avatar-inline"
:size="24"
:tooltip-text="participant.name"
+ css-classes="avatar-inline"
tooltip-placement="bottom"
/>
</a>
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
index f0df759ef7a..448c8fc3602 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
@@ -82,6 +82,7 @@
:title="notificationTooltip"
data-container="body"
data-placement="left"
+ data-boundary="viewport"
>
<icon
:name="notificationIcon"
@@ -91,14 +92,14 @@
/>
</span>
</div>
- <span class="issuable-header-text hide-collapsed pull-left">
+ <span class="issuable-header-text hide-collapsed float-left">
{{ __('Notifications') }}
</span>
<toggle-button
ref="toggleButton"
- class="pull-right hide-collapsed js-issuable-subscribe-button"
:is-loading="showLoadingState"
:value="subscribed"
+ class="float-right hide-collapsed js-issuable-subscribe-button"
@change="toggleSubscription"
/>
</div>
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
index 9d9ee9dea4d..1d030c4f67f 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
@@ -110,11 +110,12 @@
<template>
<div
- class="sidebar-collapsed-icon"
v-tooltip
+ :title="tooltipText"
+ class="sidebar-collapsed-icon"
data-container="body"
data-placement="left"
- :title="tooltipText"
+ data-boundary="viewport"
>
<icon name="timer" />
<div class="time-tracking-collapsed-summary">
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue
index 82c4562f9a9..d335c3f55af 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue
@@ -1,8 +1,12 @@
<script>
import { parseSeconds, stringifyTime } from '../../../lib/utils/pretty_time';
+import tooltip from '../../../vue_shared/directives/tooltip';
export default {
name: 'TimeTrackingComparisonPane',
+ directives: {
+ tooltip,
+ },
props: {
timeSpent: {
type: Number,
@@ -50,19 +54,17 @@ export default {
<template>
<div class="time-tracking-comparison-pane">
<div
+ v-tooltip
+ :title="timeRemainingTooltip"
+ :class="timeRemainingStatusClass"
class="compare-meter"
data-toggle="tooltip"
data-placement="top"
role="timeRemainingDisplay"
- :aria-valuenow="timeRemainingTooltip"
- :title="timeRemainingTooltip"
- :data-original-title="timeRemainingTooltip"
- :class="timeRemainingStatusClass"
>
<div
- class="meter-container"
- role="timeSpentPercent"
:aria-valuenow="timeRemainingPercent"
+ class="meter-container"
>
<div
:style="{ width: timeRemainingPercent }"
@@ -71,7 +73,7 @@ export default {
</div>
</div>
<div class="compare-display-container">
- <div class="compare-display pull-left">
+ <div class="compare-display float-left">
<span class="compare-label">
{{ s__('TimeTracking|Spent') }}
</span>
@@ -79,7 +81,7 @@ export default {
{{ timeSpentHumanReadable }}
</span>
</div>
- <div class="compare-display estimated pull-right">
+ <div class="compare-display estimated float-right">
<span class="compare-label">
{{ s__('TimeTrackingEstimated|Est') }}
</span>
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue
index 825063d9ba6..19ec0f05a26 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue
@@ -45,8 +45,8 @@ export default {
<p v-html="spendText">
</p>
<a
- class="btn btn-default learn-more-button"
:href="href"
+ class="btn btn-default learn-more-button"
>
{{ __('Learn more') }}
</a>
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
index 8f5d0bee107..ca3b9338c29 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
@@ -101,8 +101,8 @@ export default {
<template>
<div
- class="time_tracker time-tracking-component-wrap"
v-cloak
+ class="time_tracker time-tracking-component-wrap"
>
<time-tracking-collapsed-state
:show-comparison-state="showComparisonState"
@@ -116,8 +116,8 @@ export default {
<div class="title hide-collapsed">
{{ __('Time tracking') }}
<div
- class="help-button pull-right"
v-if="!showHelpState"
+ class="help-button float-right"
@click="toggleHelpState(true)"
>
<i
@@ -127,8 +127,8 @@ export default {
</i>
</div>
<div
- class="close-help-button pull-right"
v-if="showHelpState"
+ class="close-help-button float-right"
@click="toggleHelpState(false)"
>
<i
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 3086e7d0fc9..655bf9198b7 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -75,7 +75,6 @@ function mountLockComponent(mediator) {
function mountParticipantsComponent(mediator) {
const el = document.querySelector('.js-sidebar-participants-entry-point');
- // eslint-disable-next-line no-new
if (!el) return;
// eslint-disable-next-line no-new
diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js
index d86557e870a..d9ca5e46770 100644
--- a/app/assets/javascripts/sidebar/sidebar_mediator.js
+++ b/app/assets/javascripts/sidebar/sidebar_mediator.js
@@ -80,7 +80,7 @@ export default class SidebarMediator {
return this.service.moveIssue(this.store.moveToProjectId)
.then(response => response.json())
.then((data) => {
- if (location.pathname !== data.web_url) {
+ if (window.location.pathname !== data.web_url) {
visitUrl(data.web_url);
}
});
diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js
index 1afff0dba38..99c93952e2a 100644
--- a/app/assets/javascripts/single_file_diff.js
+++ b/app/assets/javascripts/single_file_diff.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, max-len */
+/* eslint-disable func-names, prefer-arrow-callback, consistent-return, */
import $ from 'jquery';
import { __ } from './locale';
@@ -11,7 +11,7 @@ import syntaxHighlight from './syntax_highlight';
const WRAPPER = '<div class="diff-content"></div>';
const LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>';
const ERROR_HTML = '<div class="nothing-here-block"><i class="fa fa-warning"></i> Could not load diff</div>';
-const COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <a class="click-to-expand">Click to expand it.</a></div>';
+const COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <button class="click-to-expand btn btn-link">Click to expand it.</button></div>';
export default class SingleFileDiff {
constructor(file) {
diff --git a/app/assets/javascripts/smart_interval.js b/app/assets/javascripts/smart_interval.js
index 77ab7c964e6..5e385400747 100644
--- a/app/assets/javascripts/smart_interval.js
+++ b/app/assets/javascripts/smart_interval.js
@@ -42,8 +42,7 @@ export default class SmartInterval {
/* public */
start() {
- const cfg = this.cfg;
- const state = this.state;
+ const { cfg, state } = this;
if (cfg.immediateExecution && !this.isLoading) {
cfg.immediateExecution = false;
@@ -100,7 +99,7 @@ export default class SmartInterval {
/* private */
initInterval() {
- const cfg = this.cfg;
+ const { cfg } = this;
if (!cfg.lazyStart) {
this.start();
@@ -151,7 +150,7 @@ export default class SmartInterval {
}
incrementInterval() {
- const cfg = this.cfg;
+ const { cfg } = this;
const currentInterval = this.getCurrentInterval();
if (cfg.hiddenInterval && !this.isPageVisible()) return;
let nextInterval = currentInterval * cfg.incrementByFactorOf;
@@ -166,7 +165,7 @@ export default class SmartInterval {
isPageVisible() { return this.state.pageVisibility === 'visible'; }
stopTimer() {
- const state = this.state;
+ const { state } = this;
state.intervalId = window.clearInterval(state.intervalId);
}
diff --git a/app/assets/javascripts/snippet/snippet_embed.js b/app/assets/javascripts/snippet/snippet_embed.js
index 81ec483f2d9..873a506a92f 100644
--- a/app/assets/javascripts/snippet/snippet_embed.js
+++ b/app/assets/javascripts/snippet/snippet_embed.js
@@ -1,5 +1,5 @@
export default () => {
- const { protocol, host, pathname } = location;
+ const { protocol, host, pathname } = window.location;
const shareBtn = document.querySelector('.js-share-btn');
const embedBtn = document.querySelector('.js-embed-btn');
const snippetUrlArea = document.querySelector('.js-snippet-url-area');
diff --git a/app/assets/javascripts/syntax_highlight.js b/app/assets/javascripts/syntax_highlight.js
index f52990ba232..37f3dd4b496 100644
--- a/app/assets/javascripts/syntax_highlight.js
+++ b/app/assets/javascripts/syntax_highlight.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-else-return, prefer-arrow-callback, max-len */
+/* eslint-disable consistent-return, no-else-return */
import $ from 'jquery';
diff --git a/app/assets/javascripts/test_utils/simulate_drag.js b/app/assets/javascripts/test_utils/simulate_drag.js
index e39213cb098..a5c18042ce7 100644
--- a/app/assets/javascripts/test_utils/simulate_drag.js
+++ b/app/assets/javascripts/test_utils/simulate_drag.js
@@ -38,14 +38,14 @@ function simulateEvent(el, type, options = {}) {
function isLast(target) {
const el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el;
- const children = el.children;
+ const { children } = el;
return children.length - 1 === target.index;
}
function getTarget(target) {
const el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el;
- const children = el.children;
+ const { children } = el;
return (
children[target.index] ||
diff --git a/app/assets/javascripts/tree.js b/app/assets/javascripts/tree.js
index afbb958d058..85123a63a45 100644
--- a/app/assets/javascripts/tree.js
+++ b/app/assets/javascripts/tree.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, quotes, consistent-return, no-var, one-var, one-var-declaration-per-line, no-else-return, prefer-arrow-callback, class-methods-use-this */
+/* eslint-disable func-names, max-len, quotes, consistent-return, no-var, one-var, one-var-declaration-per-line, no-else-return, prefer-arrow-callback, class-methods-use-this */
import $ from 'jquery';
import { visitUrl } from './lib/utils/url_utility';
diff --git a/app/assets/javascripts/u2f/authenticate.js b/app/assets/javascripts/u2f/authenticate.js
index 96af6d2fcca..78fd7ad441f 100644
--- a/app/assets/javascripts/u2f/authenticate.js
+++ b/app/assets/javascripts/u2f/authenticate.js
@@ -11,7 +11,6 @@ export default class U2FAuthenticate {
constructor(container, form, u2fParams, fallbackButton, fallbackUI) {
this.u2fUtils = null;
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);
@@ -41,7 +40,6 @@ export default class U2FAuthenticate {
this.signRequests = u2fParams.sign_requests.map(request => _(request).omit('challenge'));
this.templates = {
- notSupported: '#js-authenticate-u2f-not-supported',
setup: '#js-authenticate-u2f-setup',
inProgress: '#js-authenticate-u2f-in-progress',
error: '#js-authenticate-u2f-error',
@@ -55,7 +53,7 @@ export default class U2FAuthenticate {
this.u2fUtils = utils;
this.renderInProgress();
})
- .catch(() => this.renderNotSupported());
+ .catch(() => this.switchToFallbackUI());
}
authenticate() {
@@ -96,10 +94,6 @@ export default class U2FAuthenticate {
this.fallbackButton.classList.add('hidden');
}
- renderNotSupported() {
- return this.renderTemplate('notSupported');
- }
-
switchToFallbackUI() {
this.fallbackButton.classList.add('hidden');
this.container[0].classList.add('hidden');
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index 8486019897d..e3d7645040d 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, one-var, no-var, prefer-rest-params, wrap-iife, quotes, max-len, one-var-declaration-per-line, vars-on-top, prefer-arrow-callback, consistent-return, comma-dangle, object-shorthand, no-shadow, no-unused-vars, no-else-return, no-self-compare, prefer-template, no-unused-expressions, no-lonely-if, yoda, prefer-spread, no-void, camelcase, no-param-reassign */
+/* eslint-disable func-names, one-var, no-var, prefer-rest-params, quotes, max-len, one-var-declaration-per-line, vars-on-top, prefer-arrow-callback, consistent-return, comma-dangle, object-shorthand, no-shadow, no-unused-vars, no-else-return, no-self-compare, prefer-template, no-unused-expressions, yoda, prefer-spread, no-void, camelcase, no-param-reassign */
/* global Issuable */
/* global emitSidebarEvent */
@@ -202,7 +202,7 @@ function UsersSelect(currentUser, els, options = {}) {
tooltipTitle = __('Assignee');
}
$value.html(assigneeTemplate(user));
- $collapsedSidebar.attr('title', tooltipTitle).tooltip('fixTitle');
+ $collapsedSidebar.attr('title', tooltipTitle).tooltip('_fixTitle');
return $collapsedSidebar.html(collapsedAssigneeTemplate(user));
});
};
@@ -250,7 +250,6 @@ function UsersSelect(currentUser, els, options = {}) {
let anyUser;
let index;
- let j;
let len;
let name;
let obj;
@@ -259,7 +258,7 @@ function UsersSelect(currentUser, els, options = {}) {
showDivider = 0;
if (firstUser) {
// Move current user to the front of the list
- for (index = j = 0, len = users.length; j < len; index = (j += 1)) {
+ for (index = 0, len = users.length; index < len; index += 1) {
obj = users[index];
if (obj.username === firstUser) {
users.splice(index, 1);
@@ -501,7 +500,7 @@ function UsersSelect(currentUser, els, options = {}) {
if (this.multiSelect) {
selected = getSelected().find(u => user.id === u);
- const fieldName = this.fieldName;
+ const { fieldName } = this;
const field = $dropdown.closest('.selectbox').find("input[name='" + fieldName + "'][value='" + user.id + "']");
if (field.length) {
@@ -553,7 +552,7 @@ function UsersSelect(currentUser, els, options = {}) {
minimumInputLength: 0,
query: function(query) {
return _this.users(query.term, options, function(users) {
- var anyUser, data, emailUser, index, j, len, name, nullUser, obj, ref;
+ var anyUser, data, emailUser, index, len, name, nullUser, obj, ref;
data = {
results: users
};
@@ -561,7 +560,8 @@ function UsersSelect(currentUser, els, options = {}) {
if (firstUser) {
// Move current user to the front of the list
ref = data.results;
- for (index = j = 0, len = ref.length; j < len; index = (j += 1)) {
+
+ for (index = 0, len = ref.length; index < len; index += 1) {
obj = ref[index];
if (obj.username === firstUser) {
data.results.splice(index, 1);
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
index 1fea231c816..21f21232596 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
@@ -1,4 +1,5 @@
<script>
+import Icon from '~/vue_shared/components/icon.vue';
import timeagoMixin from '../../vue_shared/mixins/timeago';
import tooltip from '../../vue_shared/directives/tooltip';
import LoadingButton from '../../vue_shared/components/loading_button.vue';
@@ -14,6 +15,7 @@ export default {
LoadingButton,
MemoryUsage,
StatusIcon,
+ Icon,
},
directives: {
tooltip,
@@ -77,67 +79,62 @@ export default {
</script>
<template>
- <div class="mr-widget-heading deploy-heading">
+ <div class="mr-widget-heading deploy-heading append-bottom-default">
<div class="ci-widget media">
- <div class="ci-status-icon ci-status-icon-success">
- <span class="js-icon-link icon-link">
- <status-icon status="success" />
- </span>
- </div>
<div class="media-body">
<div class="deploy-body">
- <template v-if="hasDeploymentMeta">
- <span>
- Deployed to
- </span>
- <a
- :href="deployment.url"
- target="_blank"
- rel="noopener noreferrer nofollow"
- class="deploy-link js-deploy-meta"
+ <div class="deployment-info">
+ <template v-if="hasDeploymentMeta">
+ <span>
+ Deployed to
+ </span>
+ <a
+ :href="deployment.url"
+ target="_blank"
+ rel="noopener noreferrer nofollow"
+ class="deploy-link js-deploy-meta"
+ >
+ {{ deployment.name }}
+ </a>
+ </template>
+ <span
+ v-tooltip
+ v-if="hasDeploymentTime"
+ :title="deployment.deployed_at_formatted"
+ class="js-deploy-time"
>
- {{ deployment.name }}
- </a>
- </template>
- <template v-if="hasExternalUrls">
- <span>
- on
+ {{ deployTimeago }}
</span>
+ <memory-usage
+ v-if="hasMetrics"
+ :metrics-url="deployment.metrics_url"
+ :metrics-monitoring-url="deployment.metrics_monitoring_url"
+ />
+ </div>
+ <div>
<a
+ v-if="hasExternalUrls"
:href="deployment.external_url"
target="_blank"
rel="noopener noreferrer nofollow"
- class="deploy-link js-deploy-url"
+ class="deploy-link js-deploy-url btn btn-default btn-sm inline"
>
- {{ deployment.external_url_formatted }}
- <i
- class="fa fa-external-link"
- aria-hidden="true"
- >
- </i>
+ <span>
+ View app
+ <icon name="external-link" />
+ </span>
</a>
- </template>
- <span
- v-if="hasDeploymentTime"
- v-tooltip
- :title="deployment.deployed_at_formatted"
- class="js-deploy-time"
- >
- {{ deployTimeago }}
- </span>
- <loading-button
- v-if="deployment.stop_url"
- container-class="btn btn-default btn-xs prepend-left-default"
- label="Stop environment"
- :loading="isStopping"
- @click="stopEnvironment"
- />
+ <loading-button
+ v-if="deployment.stop_url"
+ :loading="isStopping"
+ container-class="btn btn-default btn-sm inline prepend-left-4"
+ title="Stop environment"
+ @click="stopEnvironment"
+ >
+ <icon name="stop" />
+ </loading-button>
+ </div>
</div>
- <memory-usage
- v-if="hasMetrics"
- :metrics-url="deployment.metrics_url"
- :metrics-monitoring-url="deployment.metrics_monitoring_url"
- />
</div>
</div>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue b/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue
index f012f9c6772..5e76f6b1cac 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue
@@ -105,7 +105,7 @@ export default {
MRWidgetService.fetchMetrics(this.metricsUrl)
.then((res) => {
if (res.status === statusCodes.NO_CONTENT) {
- this.backOffRequestCounter = this.backOffRequestCounter += 1;
+ this.backOffRequestCounter += 1;
/* eslint-disable no-unused-expressions */
this.backOffRequestCounter < 3 ? next() : stop(res);
} else {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue
index 8338fde61c7..22c2f74f900 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue
@@ -35,17 +35,17 @@
<template>
<a
:href="authorUrl"
- class="author-link inline"
:v-tooltip="showAuthorTooltip"
:title="author.name"
+ class="author-link inline"
>
<img
:src="avatarUrl"
class="avatar avatar-inline s16"
/>
<span
- class="author"
v-if="showAuthorName"
+ class="author"
>
{{ author.name }}
</span>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue
index 644e4b7d81a..ba16cb9e2c8 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue
@@ -1,11 +1,15 @@
<script>
+ import tooltip from '~/vue_shared/directives/tooltip';
import MrWidgetAuthor from './mr_widget_author.vue';
export default {
- name: 'MRWidgetAuthorTime',
+ name: 'MrWidgetAuthorTime',
components: {
MrWidgetAuthor,
},
+ directives: {
+ tooltip,
+ },
props: {
actionText: {
type: String,
@@ -31,9 +35,8 @@
{{ actionText }}
<mr-widget-author :author="author" />
<time
+ v-tooltip
:title="dateTitle"
- data-toggle="tooltip"
- data-placement="top"
data-container="body"
>
{{ dateReadable }}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
index 18ee4c62bf1..c18b74743e4 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
@@ -2,7 +2,7 @@
import tooltip from '~/vue_shared/directives/tooltip';
import { n__ } from '~/locale';
import { webIDEUrl } from '~/lib/utils/url_utility';
-import icon from '~/vue_shared/components/icon.vue';
+import Icon from '~/vue_shared/components/icon.vue';
import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
export default {
@@ -11,7 +11,7 @@ export default {
tooltip,
},
components: {
- icon,
+ Icon,
clipboardButton,
},
props: {
@@ -54,104 +54,114 @@ export default {
};
</script>
<template>
- <div class="mr-source-target">
- <div class="normal">
- <strong>
- {{ s__("mrWidget|Request to merge") }}
- <span
- class="label-branch js-source-branch"
- :class="{ 'label-truncated': isSourceBranchLong }"
- :title="isSourceBranchLong ? mr.sourceBranch : ''"
- data-placement="bottom"
- :v-tooltip="isSourceBranchLong"
- v-html="mr.sourceBranchLink"
- >
- </span>
+ <div class="mr-source-target append-bottom-default">
+ <div class="git-merge-icon-container append-right-default">
+ <icon name="git-merge" />
+ </div>
+ <div class="git-merge-container d-flex">
+ <div class="normal">
+ <strong>
+ {{ s__("mrWidget|Request to merge") }}
+ <span
+ :class="{ 'label-truncated': isSourceBranchLong }"
+ :title="isSourceBranchLong ? mr.sourceBranch : ''"
+ :v-tooltip="isSourceBranchLong"
+ class="label-branch js-source-branch"
+ data-placement="bottom"
+ v-html="mr.sourceBranchLink"
+ >
+ </span>
- <clipboard-button
- :text="branchNameClipboardData"
- :title="__('Copy branch name to clipboard')"
- css-class="btn-default btn-transparent btn-clipboard"
- />
+ <clipboard-button
+ :text="branchNameClipboardData"
+ :title="__('Copy branch name to clipboard')"
+ css-class="btn-default btn-transparent btn-clipboard"
+ />
- {{ s__("mrWidget|into") }}
+ {{ s__("mrWidget|into") }}
- <span
- class="label-branch"
- :v-tooltip="isTargetBranchLong"
- :class="{ 'label-truncatedtooltip': isTargetBranchLong }"
- :title="isTargetBranchLong ? mr.targetBranch : ''"
- data-placement="bottom"
- >
- <a
- :href="mr.targetBranchTreePath"
- class="js-target-branch"
+ <span
+ :v-tooltip="isTargetBranchLong"
+ :class="{ 'label-truncatedtooltip': isTargetBranchLong }"
+ :title="isTargetBranchLong ? mr.targetBranch : ''"
+ class="label-branch"
+ data-placement="bottom"
>
- {{ mr.targetBranch }}
- </a>
- </span>
- </strong>
- <span
- v-if="shouldShowCommitsBehindText"
- class="diverged-commits-count"
- >
- (<a :href="mr.targetBranchPath">{{ commitsText }}</a>)
- </span>
- </div>
+ <a
+ :href="mr.targetBranchTreePath"
+ class="js-target-branch"
+ >
+ {{ mr.targetBranch }}
+ </a>
+ </span>
+ </strong>
+ <div
+ v-if="shouldShowCommitsBehindText"
+ class="diverged-commits-count"
+ >
+ <span class="monospace">{{ mr.sourceBranch }}</span>
+ is {{ commitsText }}
+ <span class="monospace">{{ mr.targetBranch }}</span>
+ </div>
+ </div>
- <div v-if="mr.isOpen">
- <a
- v-if="!mr.sourceBranchRemoved"
- :href="webIdePath"
- class="btn btn-sm btn-default inline js-web-ide"
- >
- {{ s__("mrWidget|Web IDE") }}
- </a>
- <button
- data-target="#modal_merge_info"
- data-toggle="modal"
- :disabled="mr.sourceBranchRemoved"
- class="btn btn-sm btn-default inline js-check-out-branch"
- type="button"
+ <div
+ v-if="mr.isOpen"
+ class="branch-actions"
>
- {{ s__("mrWidget|Check out branch") }}
- </button>
- <span class="dropdown prepend-left-10">
+ <a
+ v-if="!mr.sourceBranchRemoved"
+ :href="webIdePath"
+ class="btn btn-default inline js-web-ide d-none d-md-inline-block"
+ >
+ {{ s__("mrWidget|Open in Web IDE") }}
+ </a>
<button
+ :disabled="mr.sourceBranchRemoved"
+ data-target="#modal_merge_info"
+ data-toggle="modal"
+ class="btn btn-default inline js-check-out-branch"
type="button"
- class="btn btn-sm inline dropdown-toggle"
- data-toggle="dropdown"
- aria-label="Download as"
- aria-haspopup="true"
- aria-expanded="false"
>
- <icon name="download" />
- <i
- class="fa fa-caret-down"
- aria-hidden="true">
- </i>
+ {{ s__("mrWidget|Check out branch") }}
</button>
- <ul class="dropdown-menu dropdown-menu-align-right">
- <li>
- <a
- class="js-download-email-patches"
- :href="mr.emailPatchesPath"
- download
- >
- {{ s__("mrWidget|Email patches") }}
- </a>
- </li>
- <li>
- <a
- class="js-download-plain-diff"
- :href="mr.plainDiffPath"
- download
- >
- {{ s__("mrWidget|Plain diff") }}
- </a>
- </li>
- </ul>
- </span>
+ <span class="dropdown prepend-left-10">
+ <button
+ type="button"
+ class="btn inline dropdown-toggle"
+ data-toggle="dropdown"
+ aria-label="Download as"
+ aria-haspopup="true"
+ aria-expanded="false"
+ >
+ <icon name="download" />
+ <i
+ class="fa fa-caret-down"
+ aria-hidden="true">
+ </i>
+ </button>
+ <ul class="dropdown-menu dropdown-menu-right">
+ <li>
+ <a
+ :href="mr.emailPatchesPath"
+ class="js-download-email-patches"
+ download
+ >
+ {{ s__("mrWidget|Email patches") }}
+ </a>
+ </li>
+ <li>
+ <a
+ :href="mr.plainDiffPath"
+ class="js-download-plain-diff"
+ download
+ >
+ {{ s__("mrWidget|Plain diff") }}
+ </a>
+ </li>
+ </ul>
+ </span>
+ </div>
</div>
</div>
</template>
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
index 48dff8c4916..4a3fd01fa39 100644
--- 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
@@ -26,6 +26,10 @@ export default {
type: String,
required: false,
},
+ sourceBranchLink: {
+ type: String,
+ required: false,
+ },
},
computed: {
hasPipeline() {
@@ -54,12 +58,18 @@ export default {
<template>
<div
v-if="hasPipeline || hasCIError"
- class="mr-widget-heading"
+ class="mr-widget-heading append-bottom-default"
>
<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
+ class="add-border ci-status-icon ci-status-icon-failed ci-error
+ js-ci-error append-right-default"
+ >
+ <icon
+ :size="32"
+ name="status_failed_borderless"
+ />
</div>
<div class="media-body">
Could not connect to the CI server. Please check your settings and try again
@@ -67,51 +77,67 @@ export default {
</template>
<template v-else-if="hasPipeline">
<a
- class="append-right-10"
:href="status.details_path"
+ class="align-self-start append-right-default"
>
- <ci-icon :status="status" />
+ <ci-icon
+ :status="status"
+ :size="32"
+ :borderless="true"
+ class="add-border"
+ />
</a>
+ <div class="ci-widget-container d-flex">
+ <div class="ci-widget-content">
+ <div class="media-body">
+ <div class="font-weight-bold">
+ Pipeline
+ <a
+ :href="pipeline.path"
+ class="pipeline-id font-weight-normal pipeline-number"
+ >#{{ pipeline.id }}</a>
- <div class="media-body">
- Pipeline
- <a
- :href="pipeline.path"
- class="pipeline-id"
- >
- #{{ pipeline.id }}
- </a>
-
- {{ pipeline.details.status.label }}
+ {{ pipeline.details.status.label }}
- <template v-if="hasCommitInfo">
- for
-
- <a
- :href="pipeline.commit.commit_path"
- class="commit-sha js-commit-link"
- >
- {{ pipeline.commit.short_id }}</a>.
- </template>
-
- <span class="mr-widget-pipeline-graph">
- <span
- class="stage-cell"
- v-if="hasStages"
- >
+ <template v-if="hasCommitInfo">
+ for
+ <a
+ :href="pipeline.commit.commit_path"
+ class="commit-sha js-commit-link font-weight-normal"
+ >
+ {{ pipeline.commit.short_id }}</a>
+ on
+ <span
+ class="label-branch"
+ v-html="sourceBranchLink"
+ >
+ </span>
+ </template>
+ </div>
<div
- v-for="(stage, i) in pipeline.details.stages"
- :key="i"
- class="stage-container dropdown js-mini-pipeline-graph"
+ v-if="pipeline.coverage"
+ class="coverage"
>
- <pipeline-stage :stage="stage" />
+ Coverage {{ pipeline.coverage }}%
</div>
+ </div>
+ </div>
+ <div>
+ <span class="mr-widget-pipeline-graph">
+ <span
+ v-if="hasStages"
+ class="stage-cell"
+ >
+ <div
+ v-for="(stage, i) in pipeline.details.stages"
+ :key="i"
+ class="stage-container dropdown js-mini-pipeline-graph mr-widget-pipeline-stages"
+ >
+ <pipeline-stage :stage="stage" />
+ </div>
+ </span>
</span>
- </span>
-
- <template v-if="pipeline.coverage">
- Coverage {{ pipeline.coverage }}%
- </template>
+ </div>
</div>
</template>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
index 1fdc3218671..55b87f3a8ec 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
@@ -32,7 +32,7 @@
};
</script>
<template>
- <div class="space-children flex-container-block append-right-10">
+ <div class="space-children d-flex append-right-10">
<div
v-if="isLoading"
class="mr-widget-icon"
@@ -43,6 +43,7 @@
<ci-icon
v-else
:status="statusObj"
+ :size="24"
/>
<button
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue b/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue
index 460437ceeff..56879c04d16 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue
@@ -25,9 +25,9 @@
</span>
<i
v-tooltip
- class="fa fa-question-circle"
:title="tooltipTitle"
:aria-label="tooltipTitle"
+ class="fa fa-question-circle"
>
</i>
</p>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue
index ebaf2b972eb..2133124347c 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue
@@ -39,10 +39,10 @@
{{ s__("mrWidget|This merge request failed to be merged automatically") }}
</span>
<button
- @click="refreshWidget"
:disabled="isRefreshing"
type="button"
- class="btn btn-xs btn-default"
+ class="btn btn-sm btn-default"
+ @click="refreshWidget"
>
<loading-icon
v-if="isRefreshing"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue
index caeaac75b45..ae6630dcd6f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue
@@ -11,8 +11,8 @@
<template>
<div class="mr-widget-body media">
<status-icon
- status="loading"
:show-disabled-button="true"
+ status="loading"
/>
<div class="media-body space-children">
<span class="bold">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue
index 68b691fc914..25a57e520ee 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue
@@ -1,11 +1,11 @@
<script>
- import mrWidgetAuthorTime from '../../components/mr_widget_author_time.vue';
+ import MrWidgetAuthorTime from '../../components/mr_widget_author_time.vue';
import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetClosed',
components: {
- mrWidgetAuthorTime,
+ MrWidgetAuthorTime,
statusIcon,
},
props: {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
index dad4b0fe49d..dff9ec657b9 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
@@ -20,8 +20,8 @@
<template>
<div class="mr-widget-body media">
<status-icon
- status="warning"
:show-disabled-button="true"
+ status="warning"
/>
<div class="media-body space-children">
@@ -43,13 +43,13 @@ To merge this request, first rebase locally.`) }}
<a
v-if="mr.canMerge && mr.conflictResolutionPath"
:href="mr.conflictResolutionPath"
- class="js-resolve-conflicts-button btn btn-default btn-xs"
+ class="js-resolve-conflicts-button btn btn-default btn-sm"
>
{{ s__("mrWidget|Resolve conflicts") }}
</a>
<button
v-if="mr.canMerge"
- class="js-merge-locally-button btn btn-default btn-xs"
+ class="js-merge-locally-button btn btn-default btn-sm"
data-toggle="modal"
data-target="#modal_merge_info"
>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue
index 7d366c495f0..c302960f16e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue
@@ -1,5 +1,6 @@
<script>
import { n__ } from '~/locale';
+import { stripHtml } from '~/lib/utils/text_utility';
import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub';
@@ -27,6 +28,9 @@ export default {
},
computed: {
+ mergeError() {
+ return this.mr.mergeError ? stripHtml(this.mr.mergeError, ' ').trim() : '';
+ },
timerText() {
return n__(
'Refreshing in a second to show the updated status...',
@@ -76,16 +80,16 @@ export default {
</template>
<template v-else>
<status-icon
- status="warning"
:show-disabled-button="true"
+ status="warning"
/>
<div class="media-body space-children">
<span class="bold">
<span
- class="has-error-message"
v-if="mr.mergeError"
+ class="has-error-message"
>
- {{ mr.mergeError }}.
+ {{ mergeError }}.
</span>
<span v-else>
{{ s__("mrWidget|Merge failed.") }}
@@ -97,9 +101,9 @@ export default {
</span>
</span>
<button
- @click="refresh"
- class="btn btn-default btn-xs js-refresh-button"
+ class="btn btn-default btn-sm js-refresh-button"
type="button"
+ @click="refresh"
>
{{ s__("mrWidget|Refresh now") }}
</button>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue
index 84be9327443..97f4196b94d 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue
@@ -82,7 +82,7 @@
<div class="mr-widget-body media">
<status-icon status="success" />
<div class="media-body">
- <h4 class="flex-container-block">
+ <h4 class="d-flex align-items-start">
<span class="append-right-10">
{{ s__("mrWidget|Set by") }}
<mr-widget-author :author="mr.setToMWPSBy" />
@@ -90,11 +90,11 @@
</span>
<a
v-if="mr.canCancelAutomaticMerge"
- @click.prevent="cancelAutomaticMerge"
:disabled="isCancellingAutoMerge"
role="button"
href="#"
- class="btn btn-xs btn-default js-cancel-auto-merge">
+ class="btn btn-sm btn-default js-cancel-auto-merge"
+ @click.prevent="cancelAutomaticMerge">
<i
v-if="isCancellingAutoMerge"
class="fa fa-spinner fa-spin"
@@ -119,7 +119,7 @@
</p>
<p
v-else
- class="flex-container-block"
+ class="d-flex align-items-start"
>
<span class="append-right-10">
{{ s__("mrWidget|The source branch will not be removed") }}
@@ -127,10 +127,10 @@
<a
v-if="canRemoveSourceBranch"
:disabled="isRemovingSourceBranch"
- @click.prevent="removeSourceBranch"
role="button"
- class="btn btn-xs btn-default js-remove-source-branch"
+ class="btn btn-sm btn-default js-remove-source-branch"
href="#"
+ @click.prevent="removeSourceBranch"
>
<i
v-if="isRemovingSourceBranch"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
index 3e36a3a10f9..1a444c04a1d 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
@@ -4,7 +4,7 @@
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import { s__, __ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
- import mrWidgetAuthorTime from '../../components/mr_widget_author_time.vue';
+ import MrWidgetAuthorTime from '../../components/mr_widget_author_time.vue';
import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub';
@@ -14,7 +14,7 @@
tooltip,
},
components: {
- mrWidgetAuthorTime,
+ MrWidgetAuthorTime,
loadingIcon,
statusIcon,
ClipboardButton,
@@ -116,44 +116,44 @@
:date-readable="mr.metrics.readableMergedAt"
/>
<a
- v-if="mr.canRevertInCurrentMR"
v-tooltip
- class="btn btn-close btn-xs"
+ v-if="mr.canRevertInCurrentMR"
+ :title="revertTitle"
+ class="btn btn-close btn-sm"
href="#modal-revert-commit"
data-toggle="modal"
data-container="body"
- :title="revertTitle"
>
{{ revertLabel }}
</a>
<a
- v-else-if="mr.revertInForkPath"
v-tooltip
- class="btn btn-close btn-xs"
- data-method="post"
+ v-else-if="mr.revertInForkPath"
:href="mr.revertInForkPath"
:title="revertTitle"
+ class="btn btn-close btn-sm"
+ data-method="post"
>
{{ revertLabel }}
</a>
<a
- v-if="mr.canCherryPickInCurrentMR"
v-tooltip
- class="btn btn-default btn-xs"
+ v-if="mr.canCherryPickInCurrentMR"
+ :title="cherryPickTitle"
+ class="btn btn-default btn-sm"
href="#modal-cherry-pick-commit"
data-toggle="modal"
data-container="body"
- :title="cherryPickTitle"
>
{{ cherryPickLabel }}
</a>
<a
- v-else-if="mr.cherryPickInForkPath"
v-tooltip
- class="btn btn-default btn-xs"
- data-method="post"
+ v-else-if="mr.cherryPickInForkPath"
:href="mr.cherryPickInForkPath"
:title="cherryPickTitle"
+ class="btn btn-default btn-sm"
+ data-method="post"
>
{{ cherryPickLabel }}
</a>
@@ -168,12 +168,12 @@
<a
:href="mr.mergeCommitPath"
class="commit-sha js-mr-merged-commit-sha"
+ v-text="mr.shortMergeCommitSha"
>
- {{ mr.shortMergeCommitSha }}
</a>
<clipboard-button
:title="__('Copy commit SHA to clipboard')"
- :text="mr.shortMergeCommitSha"
+ :text="mr.mergeCommitSha"
css-class="btn-default btn-transparent btn-clipboard js-mr-merged-copy-sha"
/>
</p>
@@ -186,10 +186,10 @@
>
<span>{{ s__("mrWidget|You can remove source branch now") }}</span>
<button
- @click="removeSourceBranch"
:disabled="isMakingRequest"
type="button"
- class="btn btn-xs btn-default js-remove-branch-button"
+ class="btn btn-sm btn-default js-remove-branch-button"
+ @click="removeSourceBranch"
>
{{ s__("mrWidget|Remove Source Branch") }}
</button>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue
index 718c0e4b3c6..b0e96f74626 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue
@@ -39,8 +39,8 @@
<template>
<div class="mr-widget-body media">
<status-icon
- status="warning"
:show-disabled-button="true"
+ status="warning"
/>
<div class="media-body space-children">
@@ -51,9 +51,9 @@
{{ missingBranchNameMessage }}
<i
v-tooltip
- class="fa fa-question-circle"
:title="message"
:aria-label="message"
+ class="fa fa-question-circle"
>
</i>
</span>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue
index e4af50b09f8..92eee2cf5dd 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue
@@ -12,8 +12,8 @@
<template>
<div class="mr-widget-body media">
<status-icon
- status="success"
:show-disabled-button="true"
+ status="success"
/>
<div class="media-body space-children">
<span class="bold">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue
index 6d7cc03f7ad..37ee5215cea 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue
@@ -11,8 +11,8 @@
<template>
<div class="mr-widget-body media">
<status-icon
- status="warning"
:show-disabled-button="true"
+ status="warning"
/>
<div class="media-body space-children">
<span class="bold">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
index 143fd328d88..2d8c3d6be87 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
@@ -110,9 +110,9 @@
js-toggle-container accept-action media space-children"
>
<button
+ :disabled="isMakingRequest"
type="button"
class="btn btn-sm btn-reopen btn-success qa-mr-rebase-button"
- :disabled="isMakingRequest"
@click="rebase"
>
<loading-icon v-if="isMakingRequest" />
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.js
new file mode 100644
index 00000000000..bf8628d18a6
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.js
@@ -0,0 +1,15 @@
+/*
+The squash-before-merge button is EE only, but it's located right in the middle
+of the readyToMerge state component template.
+
+If we didn't declare this component in CE, we'd need to maintain a separate copy
+of the readyToMergeState template in EE, which is pretty big and likely to change.
+
+Instead, in CE, we declare the component, but it's hidden and is configured to do nothing.
+In EE, the configuration extends this object to add a functioning squash-before-merge
+button.
+*/
+
+export default {
+ template: '',
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue
index 926a3172412..25c1044fe2b 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue
@@ -1,15 +1,63 @@
-/*
-The squash-before-merge button is EE only, but it's located right in the middle
-of the readyToMerge state component template.
-
-If we didn't declare this component in CE, we'd need to maintain a separate copy
-of the readyToMergeState template in EE, which is pretty big and likely to change.
-
-Instead, in CE, we declare the component, but it's hidden and is configured to do nothing.
-In EE, the configuration extends this object to add a functioning squash-before-merge
-button.
-*/
-
<script>
- export default {};
+import Icon from '~/vue_shared/components/icon.vue';
+import eventHub from '~/vue_merge_request_widget/event_hub';
+import tooltip from '~/vue_shared/directives/tooltip';
+
+export default {
+ components: {
+ Icon,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ mr: {
+ type: Object,
+ required: true,
+ },
+ isMergeButtonDisabled: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ squashBeforeMerge: this.mr.squash,
+ };
+ },
+ methods: {
+ updateSquashModel() {
+ eventHub.$emit('MRWidgetUpdateSquash', this.squashBeforeMerge);
+ },
+ },
+};
</script>
+
+<template>
+ <div class="accept-control inline">
+ <label class="merge-param-checkbox">
+ <input
+ :disabled="isMergeButtonDisabled"
+ v-model="squashBeforeMerge"
+ type="checkbox"
+ name="squash"
+ class="qa-squash-checkbox"
+ @change="updateSquashModel"
+ />
+ {{ __('Squash commits') }}
+ </label>
+ <a
+ v-tooltip
+ :href="mr.squashBeforeMergeHelpPath"
+ data-title="About this feature"
+ data-placement="bottom"
+ target="_blank"
+ rel="noopener noreferrer nofollow"
+ data-container="body"
+ >
+ <icon
+ name="question-o"
+ />
+ </a>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue
index 3d9161f6926..086dbabe77e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue
@@ -18,10 +18,10 @@ export default {
<template>
<div class="mr-widget-body mr-widget-empty-state">
<div class="row">
- <div class="artwork col-sm-5 col-sm-push-7 col-xs-12 text-center">
+ <div class="artwork col-md-5 order-md-last col-12 text-center">
<span v-html="emptyStateSVG"></span>
</div>
- <div class="text col-sm-7 col-sm-pull-5 col-xs-12">
+ <div class="text col-md-7 order-md-first col-12">
<span>
Merge requests are a place to propose changes you have made to a project
and discuss those changes with others.
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue
index 8d55477929f..2bb1a34412e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue
@@ -12,8 +12,8 @@ export default {
<template>
<div class="mr-widget-body media">
<status-icon
- status="warning"
:show-disabled-button="true"
+ status="warning"
/>
<div class="media-body space-children">
<span class="bold">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
index 0264625a526..fe777a07189 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
@@ -6,11 +6,13 @@ import MergeRequest from '../../../merge_request';
import Flash from '../../../flash';
import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub';
+import SquashBeforeMerge from './mr_widget_squash_before_merge.vue';
export default {
name: 'ReadyToMerge',
components: {
statusIcon,
+ 'squash-before-merge': SquashBeforeMerge,
},
props: {
mr: { type: Object, required: true },
@@ -101,6 +103,12 @@ export default {
return enableSquashBeforeMerge && commitsCount > 1;
},
},
+ created() {
+ eventHub.$on('MRWidgetUpdateSquash', this.handleUpdateSquash);
+ },
+ beforeDestroy() {
+ eventHub.$off('MRWidgetUpdateSquash', this.handleUpdateSquash);
+ },
methods: {
shouldShowMergeControls() {
return this.mr.isMergeAllowed || this.shouldShowMergeWhenPipelineSucceedsText;
@@ -128,13 +136,9 @@ export default {
commit_message: this.commitMessage,
merge_when_pipeline_succeeds: this.setToMergeWhenPipelineSucceeds,
should_remove_source_branch: this.removeSourceBranch === true,
+ squash: this.mr.squash,
};
- // Only truthy in EE extension of this component
- if (this.setAdditionalParams) {
- this.setAdditionalParams(options);
- }
-
this.isMakingRequest = true;
this.service.merge(options)
.then(res => res.data)
@@ -154,6 +158,9 @@ export default {
new Flash('Something went wrong. Please try again.'); // eslint-disable-line
});
},
+ handleUpdateSquash(val) {
+ this.mr.squash = val;
+ },
initiateMergePolling() {
simplePoll((continuePolling, stopPolling) => {
this.handleMergePolling(continuePolling, stopPolling);
@@ -228,11 +235,11 @@ export default {
<div class="mr-widget-body-controls media space-children">
<span class="btn-group append-bottom-5">
<button
- @click="handleMergeButtonClick()"
:disabled="isMergeButtonDisabled"
:class="mergeButtonClass"
type="button"
- class="qa-merge-button">
+ class="qa-merge-button"
+ @click="handleMergeButtonClick()">
<i
v-if="isMakingRequest"
class="fa fa-spinner fa-spin"
@@ -258,28 +265,28 @@ export default {
role="menu">
<li>
<a
- @click.prevent="handleMergeButtonClick(true)"
class="merge_when_pipeline_succeeds"
- href="#">
+ href="#"
+ @click.prevent="handleMergeButtonClick(true)">
<span class="media">
<span
- v-html="successSvg"
class="merge-opt-icon"
- aria-hidden="true"></span>
+ aria-hidden="true"
+ v-html="successSvg"></span>
<span class="media-body merge-opt-title">Merge when pipeline succeeds</span>
</span>
</a>
</li>
<li>
<a
- @click.prevent="handleMergeButtonClick(false, true)"
class="accept-merge-request"
- href="#">
+ href="#"
+ @click.prevent="handleMergeButtonClick(false, true)">
<span class="media">
<span
- v-html="warningSvg"
class="merge-opt-icon"
- aria-hidden="true"></span>
+ aria-hidden="true"
+ v-html="warningSvg"></span>
<span class="media-body merge-opt-title">Merge immediately</span>
</span>
</a>
@@ -292,8 +299,8 @@ export default {
<input
id="remove-source-branch-input"
v-model="removeSourceBranch"
- class="js-remove-source-branch-checkbox"
:disabled="isRemoveSourceBranchButtonDisabled"
+ class="js-remove-source-branch-checkbox"
type="checkbox"/> Remove source branch
</label>
@@ -310,10 +317,10 @@ export default {
</span>
<button
v-else
- @click="toggleCommitMessageEditor"
:disabled="isMergeButtonDisabled"
- class="js-modify-commit-message-button btn btn-default btn-xs"
- type="button">
+ class="js-modify-commit-message-button btn btn-default btn-sm"
+ type="button"
+ @click="toggleCommitMessageEditor">
Modify commit message
</button>
</template>
@@ -329,7 +336,7 @@ export default {
class="prepend-top-default commit-message-editor">
<div class="form-group clearfix">
<label
- class="control-label"
+ class="col-form-label"
for="commit-message">
Commit message
</label>
@@ -349,8 +356,8 @@ export default {
</p>
<div class="hint">
<a
- @click.prevent="updateCommitMessage"
href="#"
+ @click.prevent="updateCommitMessage"
>
{{ commitMessageLinkTitle }}
</a>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue
index 7cc07401911..16c903c923f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue
@@ -12,8 +12,8 @@ export default {
<template>
<div class="mr-widget-body media">
<status-icon
- status="warning"
:show-disabled-button="true"
+ status="warning"
/>
<div class="media-body space-children">
<span class="bold">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
index a1f7e696795..5eb2058a03b 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
@@ -18,8 +18,8 @@ export default {
<template>
<div class="mr-widget-body media">
<status-icon
- status="warning"
:show-disabled-button="true"
+ status="warning"
/>
<div class="media-body space-children">
<span class="bold">
@@ -28,7 +28,7 @@ export default {
<a
v-if="mr.createIssueToResolveDiscussionsPath"
:href="mr.createIssueToResolveDiscussionsPath"
- class="btn btn-default btn-xs js-create-issue"
+ class="btn btn-default btn-sm js-create-issue"
>
{{ s__("mrWidget|Create an issue to resolve them later") }}
</a>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
index fe2608e8212..89c9a41f316 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
@@ -43,8 +43,8 @@ export default {
<template>
<div class="mr-widget-body media">
<status-icon
- status="warning"
:show-disabled-button="Boolean(mr.removeWIPPath)"
+ status="warning"
/>
<div class="media-body space-children">
<span class="bold">
@@ -60,10 +60,10 @@ export default {
</span>
<button
v-if="mr.removeWIPPath"
- @click="removeWIP"
:disabled="isMakingRequest"
type="button"
- class="btn btn-default btn-xs js-remove-wip">
+ class="btn btn-default btn-sm js-remove-wip"
+ @click="removeWIP">
<i
v-if="isMakingRequest"
class="fa fa-spinner fa-spin"
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index f69fe03fcb3..b5de3dd6d73 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -36,7 +36,7 @@ import {
notify,
SourceBranchRemovalStatus,
} from './dependencies';
-import { setFavicon } from '../lib/utils/common_utils';
+import { setFaviconOverlay } from '../lib/utils/common_utils';
export default {
el: '#js-vue-mr-widget',
@@ -159,8 +159,9 @@ export default {
},
setFaviconHelper() {
if (this.mr.ciStatusFaviconPath) {
- setFavicon(this.mr.ciStatusFaviconPath);
+ return setFaviconOverlay(this.mr.ciStatusFaviconPath);
}
+ return Promise.resolve();
},
fetchDeployments() {
return this.service.fetchDeployments()
@@ -171,7 +172,7 @@ export default {
}
})
.catch(() => {
- createFlash('Something went wrong while fetching the environments for this merge request. Please try again.'); // eslint-disable-line
+ createFlash('Something went wrong while fetching the environments for this merge request. Please try again.');
});
},
fetchActionsContent() {
@@ -190,7 +191,7 @@ export default {
if (data.ci_status === this.mr.ciStatus) return;
if (!data.pipeline) return;
- const label = data.pipeline.details.status.label;
+ const { label } = data.pipeline.details.status;
const title = `Pipeline ${label}`;
const message = `Pipeline ${label} for "${data.title}"`;
@@ -210,7 +211,7 @@ export default {
// `params` should be an Array contains a Boolean, like `[true]`
// Passing parameter as Boolean didn't work.
eventHub.$on('SetBranchRemoveFlag', (params) => {
- this.mr.isRemovingSourceBranch = params[0];
+ [this.mr.isRemovingSourceBranch] = params;
});
eventHub.$on('FailedToMerge', (mergeError) => {
@@ -251,41 +252,44 @@ export default {
:pipeline="mr.pipeline"
:ci-status="mr.ciStatus"
:has-ci="mr.hasCI"
+ :source-branch-link="mr.sourceBranchLink"
/>
<deployment
v-for="deployment in mr.deployments"
:key="deployment.id"
:deployment="deployment"
/>
- <div class="mr-widget-section">
- <component
- :is="componentName"
- :mr="mr"
- :service="service"
- />
+ <div class="mr-section-container">
+ <div class="mr-widget-section">
+ <component
+ :is="componentName"
+ :mr="mr"
+ :service="service"
+ />
- <section
- v-if="mr.maintainerEditAllowed"
- class="mr-info-list mr-links"
- >
- {{ s__("mrWidget|Allows edits from maintainers") }}
- </section>
+ <section
+ v-if="mr.allowCollaboration"
+ class="mr-info-list mr-links"
+ >
+ {{ s__("mrWidget|Allows commits from members who can merge to the target branch") }}
+ </section>
- <mr-widget-related-links
- v-if="shouldRenderRelatedLinks"
- :state="mr.state"
- :related-links="mr.relatedLinks"
- />
+ <mr-widget-related-links
+ v-if="shouldRenderRelatedLinks"
+ :state="mr.state"
+ :related-links="mr.relatedLinks"
+ />
- <source-branch-removal-status
- v-if="shouldRenderSourceBranchRemovalStatus"
- />
- </div>
- <div
- class="mr-widget-footer"
- v-if="shouldRenderMergeHelp"
- >
- <mr-widget-merge-help />
+ <source-branch-removal-status
+ v-if="shouldRenderSourceBranchRemovalStatus"
+ />
+ </div>
+ <div
+ v-if="shouldRenderMergeHelp"
+ class="mr-widget-footer"
+ >
+ <mr-widget-merge-help />
+ </div>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index 83b7b054e6f..c881cd496d1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -15,12 +15,18 @@ export default class MergeRequestStore {
const currentUser = data.current_user;
const pipelineStatus = data.pipeline ? data.pipeline.details.status : null;
+ this.squash = data.squash;
+ this.squashBeforeMergeHelpPath = this.squashBeforeMergeHelpPath ||
+ data.squash_before_merge_help_path;
+ this.enableSquashBeforeMerge = this.enableSquashBeforeMerge || true;
+
this.title = data.title;
this.targetBranch = data.target_branch;
this.sourceBranch = data.source_branch;
this.mergeStatus = data.merge_status;
this.commitMessage = data.merge_commit_message;
this.shortMergeCommitSha = data.short_merge_commit_sha;
+ this.mergeCommitSha = data.merge_commit_sha;
this.commitMessageWithDescription = data.merge_commit_message_with_description;
this.commitsCount = data.commits_count;
this.divergedCommitsCount = data.diverged_commits_count;
@@ -78,7 +84,7 @@ export default class MergeRequestStore {
this.canBeMerged = data.can_be_merged || false;
this.isMergeAllowed = data.mergeable || false;
this.mergeOngoing = data.merge_ongoing;
- this.maintainerEditAllowed = data.allow_maintainer_to_push;
+ this.allowCollaboration = data.allow_collaboration;
// Cherry-pick and Revert actions related
this.canCherryPickInCurrentMR = currentUser.can_cherry_pick_on_current_merge_request || false;
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 0d64efcbf68..a2518e2a611 100644
--- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
@@ -50,9 +50,9 @@ export default {
</script>
<template>
<a
+ v-tooltip
:href="status.details_path"
:class="cssClass"
- v-tooltip
:title="!showText ? status.text : ''"
>
<ci-icon :status="status" />
diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue
index fcab8f571dd..03f924ba99d 100644
--- a/app/assets/javascripts/vue_shared/components/ci_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue
@@ -22,6 +22,8 @@ import Icon from '../../vue_shared/components/icon.vue';
* - Jobs show view header
* - Jobs show view sidebar
*/
+const validSizes = [8, 12, 16, 18, 24, 32, 48, 72];
+
export default {
components: {
Icon,
@@ -31,17 +33,36 @@ export default {
type: Object,
required: true,
},
+ size: {
+ type: Number,
+ required: false,
+ default: 16,
+ validator(value) {
+ return validSizes.includes(value);
+ },
+ },
+ borderless: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
cssClass() {
const status = this.status.group;
return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`;
},
+ icon() {
+ return this.borderless ? `${this.status.icon}_borderless` : this.status.icon;
+ },
},
};
</script>
<template>
<span :class="cssClass">
- <icon :name="status.icon" />
+ <icon
+ :name="icon"
+ :size="size"
+ />
</span>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/clipboard_button.vue b/app/assets/javascripts/vue_shared/components/clipboard_button.vue
index cb2cc3901ad..dc5760bce28 100644
--- a/app/assets/javascripts/vue_shared/components/clipboard_button.vue
+++ b/app/assets/javascripts/vue_shared/components/clipboard_button.vue
@@ -49,14 +49,14 @@ export default {
<template>
<button
- type="button"
- class="btn"
+ v-tooltip
:class="cssClass"
:title="title"
:data-clipboard-text="text"
- v-tooltip
:data-container="tooltipContainer"
:data-placement="tooltipPlacement"
+ type="button"
+ class="btn"
>
<i
aria-hidden="true"
diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue
index 8f250a6c989..13bca99dcb3 100644
--- a/app/assets/javascripts/vue_shared/components/commit.vue
+++ b/app/assets/javascripts/vue_shared/components/commit.vue
@@ -124,11 +124,11 @@ export default {
</div>
<a
- class="ref-name"
- :href="commitRef.ref_url"
v-tooltip
- data-container="body"
+ :href="commitRef.ref_url"
:title="commitRef.name"
+ class="ref-name"
+ data-container="body"
>
{{ commitRef.name }}
</a>
@@ -139,8 +139,8 @@ export default {
/>
<a
- class="commit-sha"
:href="commitUrl"
+ class="commit-sha"
>
{{ shortSha }}
</a>
@@ -152,15 +152,15 @@ export default {
>
<user-avatar-link
v-if="hasAuthor"
- class="avatar-image-container"
:link-href="author.path"
:img-src="author.avatar_url"
:img-alt="userImageAltDescription"
:tooltip-text="author.username"
+ class="avatar-image-container"
/>
<a
- class="commit-row-message"
:href="commitUrl"
+ class="commit-row-message"
>
{{ title }}
</a>
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue
index 395a71acccf..f1ef50d0e3d 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue
@@ -32,7 +32,10 @@ export default {
<div class="file-container">
<div class="file-content">
<p class="prepend-top-10 file-info">
- {{ fileName }} ({{ fileSizeReadable }})
+ {{ fileName }}
+ <template v-if="fileSize > 0">
+ ({{ fileSizeReadable }})
+ </template>
</p>
<a
:href="path"
@@ -41,9 +44,9 @@ export default {
download
target="_blank">
<icon
- name="download"
- css-classes="pull-left append-right-8"
:size="16"
+ name="download"
+ css-classes="float-left append-right-8"
/>
{{ __('Download') }}
</a>
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue
index a5999f909ca..133bdbb54f7 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue
@@ -1,4 +1,5 @@
<script>
+import _ from 'underscore';
import { numberToHumanSize } from '../../../../lib/utils/number_utils';
export default {
@@ -12,6 +13,10 @@ export default {
required: false,
default: 0,
},
+ renderInfo: {
+ type: Boolean,
+ default: true,
+ },
},
data() {
return {
@@ -26,14 +31,34 @@ export default {
return numberToHumanSize(this.fileSize);
},
},
+ beforeDestroy() {
+ window.removeEventListener('resize', this.resizeThrottled, false);
+ },
+ mounted() {
+ // The onImgLoad may have happened before the control was actually mounted
+ this.onImgLoad();
+ this.resizeThrottled = _.throttle(this.onImgLoad, 400);
+ window.addEventListener('resize', this.resizeThrottled, false);
+ },
methods: {
onImgLoad() {
- const contentImg = this.$refs.contentImg;
- this.isZoomable =
- contentImg.naturalWidth > contentImg.width || contentImg.naturalHeight > contentImg.height;
+ const { contentImg } = this.$refs;
+
+ if (contentImg) {
+ this.isZoomable =
+ contentImg.naturalWidth > contentImg.width ||
+ contentImg.naturalHeight > contentImg.height;
+
+ this.width = contentImg.naturalWidth;
+ this.height = contentImg.naturalHeight;
- this.width = contentImg.naturalWidth;
- this.height = contentImg.naturalHeight;
+ this.$emit('imgLoaded', {
+ width: this.width,
+ height: this.height,
+ renderedWidth: contentImg.clientWidth,
+ renderedHeight: contentImg.clientHeight,
+ });
+ }
},
onImgClick() {
if (this.isZoomable) this.isZoomed = !this.isZoomed;
@@ -47,20 +72,22 @@ export default {
<div class="file-content image_file">
<img
ref="contentImg"
- :class="{ 'isZoomable': isZoomable, 'isZoomed': isZoomed }"
+ :class="{ 'is-zoomable': isZoomable, 'is-zoomed': isZoomed }"
:src="path"
:alt="path"
@load="onImgLoad"
@click="onImgClick"/>
- <p class="file-info prepend-top-10">
+ <p
+ v-if="renderInfo"
+ class="file-info prepend-top-10">
<template v-if="fileSize>0">
{{ fileSizeReadable }}
</template>
<template v-if="fileSize>0 && width && height">
- -
+ |
</template>
<template v-if="width && height">
- {{ width }} x {{ height }}
+ W: {{ width }} | H: {{ height }}
</template>
</p>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
index 09e0094054d..a10deb93f0f 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
@@ -4,7 +4,7 @@ import { __ } from '~/locale';
import $ from 'jquery';
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
-const CancelToken = axios.CancelToken;
+const { CancelToken } = axios;
let axiosSource;
export default {
diff --git a/app/assets/javascripts/vue_shared/components/deprecated_modal.vue b/app/assets/javascripts/vue_shared/components/deprecated_modal.vue
index dcf1489b37c..9c1e5c68649 100644
--- a/app/assets/javascripts/vue_shared/components/deprecated_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/deprecated_modal.vue
@@ -86,8 +86,8 @@
<div class="modal-open">
<div
:id="id"
+ :class="id ? '' : 'd-block'"
class="modal"
- :class="id ? '' : 'show'"
role="dialog"
tabindex="-1"
>
@@ -99,15 +99,15 @@
<div class="modal-content">
<div class="modal-header">
<slot name="header">
- <h4 class="modal-title pull-left">
+ <h4 class="modal-title float-left">
{{ title }}
</h4>
<button
type="button"
- class="close pull-right"
- @click="emitCancel($event)"
+ class="close float-right"
data-dismiss="modal"
aria-label="Close"
+ @click="emitCancel($event)"
>
<span aria-hidden="true">&times;</span>
</button>
@@ -115,22 +115,22 @@
</div>
<div class="modal-body">
<slot
- name="body"
:text="text"
+ name="body"
>
<p>{{ text }}</p>
</slot>
</div>
<div
- class="modal-footer"
v-if="!hideFooter"
+ class="modal-footer"
>
<button
+ :class="btnCancelKindClass"
type="button"
class="btn"
- :class="btnCancelKindClass"
- @click="emitCancel($event)"
data-dismiss="modal"
+ @click="emitCancel($event)"
>
{{ closeButtonLabel }}
</button>
@@ -151,12 +151,12 @@
<button
v-if="primaryButtonLabel"
- type="button"
- class="btn js-primary-button"
:disabled="submitDisabled"
:class="btnKindClass"
- @click="emitSubmit($event)"
+ type="button"
+ class="btn js-primary-button"
data-dismiss="modal"
+ @click="emitSubmit($event)"
>
{{ primaryButtonLabel }}
</button>
@@ -166,7 +166,7 @@
</div>
<div
v-if="!id"
- class="modal-backdrop fade in"
+ class="modal-backdrop fade show"
>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/constants.js b/app/assets/javascripts/vue_shared/components/diff_viewer/constants.js
new file mode 100644
index 00000000000..6c1840361af
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/constants.js
@@ -0,0 +1,12 @@
+export const diffModes = {
+ replaced: 'replaced',
+ new: 'new',
+ deleted: 'deleted',
+ renamed: 'renamed',
+};
+
+export const imageViewMode = {
+ twoup: 'twoup',
+ swipe: 'swipe',
+ onion: 'onion',
+};
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue
new file mode 100644
index 00000000000..d3cbe3c7e74
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue
@@ -0,0 +1,74 @@
+<script>
+import { viewerInformationForPath } from '../content_viewer/lib/viewer_utils';
+import ImageDiffViewer from './viewers/image_diff_viewer.vue';
+import DownloadDiffViewer from './viewers/download_diff_viewer.vue';
+
+export default {
+ props: {
+ diffMode: {
+ type: String,
+ required: true,
+ },
+ newPath: {
+ type: String,
+ required: true,
+ },
+ newSha: {
+ type: String,
+ required: true,
+ },
+ oldPath: {
+ type: String,
+ required: true,
+ },
+ oldSha: {
+ type: String,
+ required: true,
+ },
+ projectPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ viewer() {
+ if (!this.newPath) return null;
+
+ const previewInfo = viewerInformationForPath(this.newPath);
+ if (!previewInfo) return DownloadDiffViewer;
+
+ switch (previewInfo.id) {
+ case 'image':
+ return ImageDiffViewer;
+ default:
+ return DownloadDiffViewer;
+ }
+ },
+ basePath() {
+ // We might get the project path from rails with the relative url already setup
+ return this.projectPath.indexOf('/') === 0 ? '' : `${gon.relative_url_root}/`;
+ },
+ fullOldPath() {
+ return `${this.basePath}${this.projectPath}/raw/${this.oldSha}/${this.oldPath}`;
+ },
+ fullNewPath() {
+ return `${this.basePath}${this.projectPath}/raw/${this.newSha}/${this.newPath}`;
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ v-if="viewer"
+ class="diff-file preview-container">
+ <component
+ :is="viewer"
+ :diff-mode="diffMode"
+ :new-path="fullNewPath"
+ :old-path="fullOldPath"
+ :project-path="projectPath"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/download_diff_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/download_diff_viewer.vue
new file mode 100644
index 00000000000..50389b6ae63
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/download_diff_viewer.vue
@@ -0,0 +1,69 @@
+<script>
+import DownloadViewer from '../../content_viewer/viewers/download_viewer.vue';
+import { diffModes } from '../constants';
+
+export default {
+ components: {
+ DownloadViewer,
+ },
+ props: {
+ diffMode: {
+ type: String,
+ required: true,
+ },
+ newPath: {
+ type: String,
+ required: true,
+ },
+ oldPath: {
+ type: String,
+ required: true,
+ },
+ projectPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ diffModes,
+};
+</script>
+
+<template>
+ <div class="diff-file-container">
+ <div class="diff-viewer">
+ <div
+ v-if="diffMode === $options.diffModes.replaced"
+ class="two-up view row">
+ <div class="col-sm-6 deleted">
+ <download-viewer
+ :path="oldPath"
+ :project-path="projectPath"
+ />
+ </div>
+ <div class="col-sm-6 added">
+ <download-viewer
+ :path="newPath"
+ :project-path="projectPath"
+ />
+ </div>
+ </div>
+ <div
+ v-else-if="diffMode === $options.diffModes.new"
+ class="added">
+ <download-viewer
+ :path="newPath"
+ :project-path="projectPath"
+ />
+ </div>
+ <div
+ v-else
+ class="deleted">
+ <download-viewer
+ :path="oldPath"
+ :project-path="projectPath"
+ />
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue
new file mode 100644
index 00000000000..38e881d17a2
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue
@@ -0,0 +1,160 @@
+<script>
+import { pixeliseValue } from '../../../lib/utils/dom_utils';
+import ImageViewer from '../../../content_viewer/viewers/image_viewer.vue';
+
+export default {
+ components: {
+ ImageViewer,
+ },
+ props: {
+ newPath: {
+ type: String,
+ required: true,
+ },
+ oldPath: {
+ type: String,
+ required: true,
+ },
+ projectPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ onionMaxWidth: undefined,
+ onionMaxHeight: undefined,
+ onionOldImgInfo: null,
+ onionNewImgInfo: null,
+ onionDraggerPos: 0,
+ onionOpacity: 1,
+ dragging: false,
+ };
+ },
+ computed: {
+ onionMaxPixelWidth() {
+ return pixeliseValue(this.onionMaxWidth);
+ },
+ onionMaxPixelHeight() {
+ return pixeliseValue(this.onionMaxHeight);
+ },
+ onionDraggerPixelPos() {
+ return pixeliseValue(this.onionDraggerPos);
+ },
+ },
+ beforeDestroy() {
+ document.body.removeEventListener('mouseup', this.stopDrag);
+ this.$refs.dragger.removeEventListener('mousedown', this.startDrag);
+ },
+ methods: {
+ dragMove(e) {
+ if (!this.dragging) return;
+ const left = e.pageX - this.$refs.dragTrack.getBoundingClientRect().left;
+ const dragTrackWidth =
+ this.$refs.dragTrack.clientWidth - this.$refs.dragger.clientWidth || 100;
+
+ let leftValue = left;
+ if (leftValue < 0) leftValue = 0;
+ if (leftValue > dragTrackWidth) leftValue = dragTrackWidth;
+
+ this.onionOpacity = left / dragTrackWidth;
+ this.onionDraggerPos = leftValue;
+ },
+ startDrag() {
+ this.dragging = true;
+ document.body.style.userSelect = 'none';
+ document.body.addEventListener('mousemove', this.dragMove);
+ },
+ stopDrag() {
+ this.dragging = false;
+ document.body.style.userSelect = '';
+ document.body.removeEventListener('mousemove', this.dragMove);
+ },
+ prepareOnionSkin() {
+ if (this.onionOldImgInfo && this.onionNewImgInfo) {
+ this.onionMaxWidth = Math.max(
+ this.onionOldImgInfo.renderedWidth,
+ this.onionNewImgInfo.renderedWidth,
+ );
+ this.onionMaxHeight = Math.max(
+ this.onionOldImgInfo.renderedHeight,
+ this.onionNewImgInfo.renderedHeight,
+ );
+
+ this.onionOpacity = 1;
+ this.onionDraggerPos =
+ this.$refs.dragTrack.clientWidth - this.$refs.dragger.clientWidth || 100;
+
+ document.body.addEventListener('mouseup', this.stopDrag);
+ }
+ },
+ onionNewImgLoaded(imgInfo) {
+ this.onionNewImgInfo = imgInfo;
+ this.prepareOnionSkin();
+ },
+ onionOldImgLoaded(imgInfo) {
+ this.onionOldImgInfo = imgInfo;
+ this.prepareOnionSkin();
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="onion-skin view">
+ <div
+ :style="{
+ 'width': onionMaxPixelWidth,
+ 'height': onionMaxPixelHeight,
+ 'user-select': dragging === true ? 'none' : '',
+ }"
+ class="onion-skin-frame">
+ <div
+ :style="{
+ 'width': onionMaxPixelWidth,
+ 'height': onionMaxPixelHeight,
+ }"
+ class="frame deleted">
+ <image-viewer
+ key="onionOldImg"
+ :render-info="false"
+ :path="oldPath"
+ :project-path="projectPath"
+ @imgLoaded="onionOldImgLoaded"
+ />
+ </div>
+ <div
+ ref="addedFrame"
+ :style="{
+ 'opacity': onionOpacity,
+ 'width': onionMaxPixelWidth,
+ 'height': onionMaxPixelHeight,
+ }"
+ class="added frame">
+ <image-viewer
+ key="onionNewImg"
+ :render-info="false"
+ :path="newPath"
+ :project-path="projectPath"
+ @imgLoaded="onionNewImgLoaded"
+ />
+ </div>
+ <div class="controls">
+ <div class="transparent"></div>
+ <div
+ ref="dragTrack"
+ class="drag-track"
+ @mousedown="startDrag"
+ @mouseup="stopDrag">
+ <div
+ ref="dragger"
+ :style="{ 'left': onionDraggerPixelPos }"
+ class="dragger">
+ </div>
+ </div>
+ <div class="opaque"></div>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue
new file mode 100644
index 00000000000..86366c799a2
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue
@@ -0,0 +1,158 @@
+<script>
+import _ from 'underscore';
+import { pixeliseValue } from '../../../lib/utils/dom_utils';
+import ImageViewer from '../../../content_viewer/viewers/image_viewer.vue';
+
+export default {
+ components: {
+ ImageViewer,
+ },
+ props: {
+ newPath: {
+ type: String,
+ required: true,
+ },
+ oldPath: {
+ type: String,
+ required: true,
+ },
+ projectPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ dragging: false,
+ swipeOldImgInfo: null,
+ swipeNewImgInfo: null,
+ swipeMaxWidth: undefined,
+ swipeMaxHeight: undefined,
+ swipeBarPos: 1,
+ swipeWrapWidth: undefined,
+ };
+ },
+ computed: {
+ swipeMaxPixelWidth() {
+ return pixeliseValue(this.swipeMaxWidth);
+ },
+ swipeMaxPixelHeight() {
+ return pixeliseValue(this.swipeMaxHeight);
+ },
+ swipeWrapPixelWidth() {
+ return pixeliseValue(this.swipeWrapWidth);
+ },
+ swipeBarPixelPos() {
+ return pixeliseValue(this.swipeBarPos);
+ },
+ },
+ beforeDestroy() {
+ window.removeEventListener('resize', this.resizeThrottled, false);
+ document.body.removeEventListener('mouseup', this.stopDrag);
+ document.body.removeEventListener('mousemove', this.dragMove);
+ },
+ mounted() {
+ window.addEventListener('resize', this.resize, false);
+ },
+ methods: {
+ dragMove(e) {
+ if (!this.dragging) return;
+
+ let leftValue = e.pageX - this.$refs.swipeFrame.getBoundingClientRect().left;
+ const spaceLeft = 20;
+ const { clientWidth } = this.$refs.swipeFrame;
+ if (leftValue <= 0) {
+ leftValue = 0;
+ } else if (leftValue > clientWidth - spaceLeft) {
+ leftValue = clientWidth - spaceLeft;
+ }
+
+ this.swipeWrapWidth = this.swipeMaxWidth - leftValue;
+ this.swipeBarPos = leftValue;
+ },
+ startDrag() {
+ this.dragging = true;
+ document.body.style.userSelect = 'none';
+ document.body.addEventListener('mousemove', this.dragMove);
+ },
+ stopDrag() {
+ this.dragging = false;
+ document.body.style.userSelect = '';
+ document.body.removeEventListener('mousemove', this.dragMove);
+ },
+ prepareSwipe() {
+ if (this.swipeOldImgInfo && this.swipeNewImgInfo) {
+ // Add 2 for border width
+ this.swipeMaxWidth =
+ Math.max(this.swipeOldImgInfo.renderedWidth, this.swipeNewImgInfo.renderedWidth) + 2;
+ this.swipeWrapWidth = this.swipeMaxWidth;
+ this.swipeMaxHeight =
+ Math.max(this.swipeOldImgInfo.renderedHeight, this.swipeNewImgInfo.renderedHeight) + 2;
+
+ document.body.addEventListener('mouseup', this.stopDrag);
+ }
+ },
+ swipeNewImgLoaded(imgInfo) {
+ this.swipeNewImgInfo = imgInfo;
+ this.prepareSwipe();
+ },
+ swipeOldImgLoaded(imgInfo) {
+ this.swipeOldImgInfo = imgInfo;
+ this.prepareSwipe();
+ },
+ resize: _.throttle(function throttledResize() {
+ this.swipeBarPos = 0;
+ }, 400),
+ },
+};
+</script>
+
+<template>
+ <div class="swipe view">
+ <div
+ ref="swipeFrame"
+ :style="{
+ 'width': swipeMaxPixelWidth,
+ 'height': swipeMaxPixelHeight,
+ }"
+ class="swipe-frame">
+ <div class="frame deleted">
+ <image-viewer
+ key="swipeOldImg"
+ ref="swipeOldImg"
+ :render-info="false"
+ :path="oldPath"
+ :project-path="projectPath"
+ @imgLoaded="swipeOldImgLoaded"
+ />
+ </div>
+ <div
+ ref="swipeWrap"
+ :style="{
+ 'width': swipeWrapPixelWidth,
+ 'height': swipeMaxPixelHeight,
+ }"
+ class="swipe-wrap">
+ <div class="frame added">
+ <image-viewer
+ key="swipeNewImg"
+ :render-info="false"
+ :path="newPath"
+ :project-path="projectPath"
+ @imgLoaded="swipeNewImgLoaded"
+ />
+ </div>
+ </div>
+ <span
+ ref="swipeBar"
+ :style="{ 'left': swipeBarPixelPos }"
+ class="swipe-bar"
+ @mousedown="startDrag"
+ @mouseup="stopDrag">
+ <span class="top-handle"></span>
+ <span class="bottom-handle"></span>
+ </span>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/two_up_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/two_up_viewer.vue
new file mode 100644
index 00000000000..9c19266ecdf
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/two_up_viewer.vue
@@ -0,0 +1,41 @@
+<script>
+import ImageViewer from '../../../content_viewer/viewers/image_viewer.vue';
+
+export default {
+ components: {
+ ImageViewer,
+ },
+ props: {
+ newPath: {
+ type: String,
+ required: true,
+ },
+ oldPath: {
+ type: String,
+ required: true,
+ },
+ projectPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="two-up view row">
+ <div class="col-sm-6 frame deleted">
+ <image-viewer
+ :path="oldPath"
+ :project-path="projectPath"
+ />
+ </div>
+ <div class="col-sm-6 frame added">
+ <image-viewer
+ :path="newPath"
+ :project-path="projectPath"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue
new file mode 100644
index 00000000000..1af85283277
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue
@@ -0,0 +1,109 @@
+<script>
+import ImageViewer from '../../content_viewer/viewers/image_viewer.vue';
+import TwoUpViewer from './image_diff/two_up_viewer.vue';
+import SwipeViewer from './image_diff/swipe_viewer.vue';
+import OnionSkinViewer from './image_diff/onion_skin_viewer.vue';
+import { diffModes, imageViewMode } from '../constants';
+
+export default {
+ components: {
+ ImageViewer,
+ TwoUpViewer,
+ SwipeViewer,
+ OnionSkinViewer,
+ },
+ props: {
+ diffMode: {
+ type: String,
+ required: true,
+ },
+ newPath: {
+ type: String,
+ required: true,
+ },
+ oldPath: {
+ type: String,
+ required: true,
+ },
+ projectPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ mode: imageViewMode.twoup,
+ };
+ },
+ methods: {
+ changeMode(newMode) {
+ this.mode = newMode;
+ },
+ },
+ diffModes,
+ imageViewMode,
+};
+</script>
+
+<template>
+ <div class="diff-file-container">
+ <div
+ v-if="diffMode === $options.diffModes.replaced"
+ class="diff-viewer">
+ <div class="image js-replaced-image">
+ <two-up-viewer
+ v-if="mode === $options.imageViewMode.twoup"
+ v-bind="$props"/>
+ <swipe-viewer
+ v-else-if="mode === $options.imageViewMode.swipe"
+ v-bind="$props"/>
+ <onion-skin-viewer
+ v-else-if="mode === $options.imageViewMode.onion"
+ v-bind="$props"/>
+ </div>
+ <div class="view-modes">
+ <ul class="view-modes-menu">
+ <li
+ :class="{
+ active: mode === $options.imageViewMode.twoup
+ }"
+ @click="changeMode($options.imageViewMode.twoup)">
+ {{ s__('ImageDiffViewer|2-up') }}
+ </li>
+ <li
+ :class="{
+ active: mode === $options.imageViewMode.swipe
+ }"
+ @click="changeMode($options.imageViewMode.swipe)">
+ {{ s__('ImageDiffViewer|Swipe') }}
+ </li>
+ <li
+ :class="{
+ active: mode === $options.imageViewMode.onion
+ }"
+ @click="changeMode($options.imageViewMode.onion)">
+ {{ s__('ImageDiffViewer|Onion skin') }}
+ </li>
+ </ul>
+ </div>
+ <div class="note-container"></div>
+ </div>
+ <div
+ v-else-if="diffMode === $options.diffModes.new"
+ class="diff-viewer added">
+ <image-viewer
+ :path="newPath"
+ :project-path="projectPath"
+ />
+ </div>
+ <div
+ v-else
+ class="diff-viewer deleted">
+ <image-viewer
+ :path="oldPath"
+ :project-path="projectPath"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
new file mode 100644
index 00000000000..3cba0c5e633
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
@@ -0,0 +1,55 @@
+<script>
+import { __ } from '~/locale';
+import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
+
+export default {
+ components: {
+ LoadingIcon,
+ },
+ props: {
+ isDisabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isLoading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ toggleText: {
+ type: String,
+ required: false,
+ default: __('Select'),
+ },
+ },
+};
+</script>
+
+<template>
+ <button
+ :disabled="isDisabled || isLoading"
+ class="dropdown-menu-toggle dropdown-menu-full-width"
+ type="button"
+ data-toggle="dropdown"
+ aria-expanded="false"
+ >
+ <loading-icon
+ v-show="isLoading"
+ :inline="true"
+ />
+ <span class="dropdown-toggle-text">
+ {{ toggleText }}
+ </span>
+ <span
+ v-show="!isLoading"
+ class="dropdown-toggle-icon"
+ >
+ <i
+ class="fa fa-chevron-down"
+ aria-hidden="true"
+ data-hidden="true"
+ ></i>
+ </span>
+ </button>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_hidden_input.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_hidden_input.vue
new file mode 100644
index 00000000000..b7a4613bdd2
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_hidden_input.vue
@@ -0,0 +1,22 @@
+<script>
+export default {
+ props: {
+ name: {
+ type: String,
+ required: true,
+ },
+ value: {
+ type: [Number, String],
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <input
+ :name="name"
+ :value="value"
+ type="hidden"
+ />
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue
new file mode 100644
index 00000000000..7f1912f6405
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue
@@ -0,0 +1,46 @@
+<script>
+import { __ } from '~/locale';
+
+export default {
+ props: {
+ placeholderText: {
+ type: String,
+ required: true,
+ default: __('Search'),
+ },
+ },
+ data() {
+ return { searchQuery: this.value };
+ },
+ watch: {
+ searchQuery(query) {
+ this.$emit('input', query);
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="dropdown-input">
+ <input
+ v-model="searchQuery"
+ :placeholder="placeholderText"
+ class="dropdown-input-field"
+ type="search"
+ autocomplete="off"
+ />
+ <i
+ class="fa fa-search dropdown-input-search"
+ aria-hidden="true"
+ data-hidden="true"
+ >
+ </i>
+ <i
+ class="fa fa-times dropdown-input-clear js-dropdown-input-clear"
+ aria-hidden="true"
+ data-hidden="true"
+ role="button"
+ >
+ </i>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/expand_button.vue b/app/assets/javascripts/vue_shared/components/expand_button.vue
index 9295be3e2b2..e6e92594b65 100644
--- a/app/assets/javascripts/vue_shared/components/expand_button.vue
+++ b/app/assets/javascripts/vue_shared/components/expand_button.vue
@@ -1,5 +1,7 @@
<script>
import { __ } from '~/locale';
+import Icon from '~/vue_shared/components/icon.vue';
+
/**
* Port of detail_behavior expand button.
*
@@ -12,6 +14,9 @@ import { __ } from '~/locale';
*/
export default {
name: 'ExpandButton',
+ components: {
+ Icon,
+ },
data() {
return {
isCollapsed: true,
@@ -22,6 +27,9 @@ export default {
return __('Click to expand text');
},
},
+ destroyed() {
+ this.isCollapsed = true;
+ },
methods: {
onClick() {
this.isCollapsed = !this.isCollapsed;
@@ -32,12 +40,15 @@ export default {
<template>
<span>
<button
- type="button"
v-show="isCollapsed"
- class="text-expander btn-blank"
:aria-label="ariaLabel"
+ type="button"
+ class="text-expander btn-blank"
@click="onClick">
- ...
+ <icon
+ :size="12"
+ name="ellipsis_h"
+ />
</button>
<span v-if="!isCollapsed">
<slot name="expanded"></slot>
diff --git a/app/assets/javascripts/vue_shared/components/file_icon.vue b/app/assets/javascripts/vue_shared/components/file_icon.vue
index be2755452e2..878c805ada5 100644
--- a/app/assets/javascripts/vue_shared/components/file_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/file_icon.vue
@@ -73,8 +73,8 @@ export default {
<template>
<span>
<svg
- :class="[iconSizeClass, cssClasses]"
v-if="!loading && !folder"
+ :class="[iconSizeClass, cssClasses]"
>
<use v-bind="{ 'xlink:href':spriteHref }" />
</svg>
diff --git a/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js b/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js
index 9ffbaae3ea5..9bca1993ccc 100644
--- a/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js
+++ b/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js
@@ -513,7 +513,7 @@ const fileNameIcons = {
'credits.md': 'credits',
'credits.md.rendered': 'credits',
'.flowconfig': 'flow',
- 'favicon.ico': 'favicon',
+ 'favicon.png': 'favicon',
'karma.conf.js': 'karma',
'karma.conf.ts': 'karma',
'karma.conf.coffee': 'karma',
diff --git a/app/assets/javascripts/vue_shared/components/gl_modal.vue b/app/assets/javascripts/vue_shared/components/gl_modal.vue
index f28e5e2715d..b298b989203 100644
--- a/app/assets/javascripts/vue_shared/components/gl_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/gl_modal.vue
@@ -1,15 +1,21 @@
<script>
const buttonVariants = ['danger', 'primary', 'success', 'warning'];
+const sizeVariants = ['sm', 'md', 'lg', 'xl'];
export default {
name: 'GlModal',
-
props: {
id: {
type: String,
required: false,
default: null,
},
+ modalSize: {
+ type: String,
+ required: false,
+ default: 'md',
+ validator: value => sizeVariants.includes(value),
+ },
headerTitleText: {
type: String,
required: false,
@@ -27,7 +33,11 @@ export default {
default: '',
},
},
-
+ computed: {
+ modalSizeClass() {
+ return this.modalSize === 'md' ? '' : `modal-${this.modalSize}`;
+ },
+ },
methods: {
emitCancel(event) {
this.$emit('cancel', event);
@@ -47,26 +57,27 @@ export default {
role="dialog"
>
<div
+ :class="modalSizeClass"
class="modal-dialog"
role="document"
>
<div class="modal-content">
<div class="modal-header">
<slot name="header">
+ <h4 class="modal-title">
+ <slot name="title">
+ {{ headerTitleText }}
+ </slot>
+ </h4>
<button
+ :aria-label="s__('Modal|Close')"
type="button"
class="close js-modal-close-action"
data-dismiss="modal"
- :aria-label="s__('Modal|Close')"
@click="emitCancel($event)"
>
<span aria-hidden="true">&times;</span>
</button>
- <h4 class="modal-title">
- <slot name="title">
- {{ headerTitleText }}
- </slot>
- </h4>
</slot>
</div>
@@ -85,9 +96,9 @@ export default {
{{ s__('Modal|Cancel') }}
</button>
<button
+ :class="`btn-${footerPrimaryButtonVariant}`"
type="button"
class="btn js-modal-primary-action"
- :class="`btn-${footerPrimaryButtonVariant}`"
data-dismiss="modal"
@click="emitSubmit($event)"
>
diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
index 088187ed348..62d35f6547d 100644
--- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue
+++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
@@ -117,8 +117,8 @@ export default {
</section>
<section
- class="header-action-buttons"
v-if="actions.length"
+ class="header-action-buttons"
>
<template
v-for="(action, i) in actions"
@@ -135,21 +135,21 @@ export default {
<a
v-else-if="action.type === 'ujs-link'"
:href="action.path"
- data-method="post"
- rel="nofollow"
:class="action.cssClass"
:key="i"
+ data-method="post"
+ rel="nofollow"
>
{{ action.label }}
</a>
<button
v-else-if="action.type === 'button'"
- @click="onClickAction(action)"
:disabled="action.isLoading"
:class="action.cssClass"
- type="button"
:key="i"
+ type="button"
+ @click="onClickAction(action)"
>
{{ action.label }}
<i
@@ -162,11 +162,11 @@ export default {
</template>
<button
v-if="hasSidebarButton"
+ id="toggleSidebar"
type="button"
- class="btn btn-default visible-xs-block
-visible-sm-block sidebar-toggle-btn js-sidebar-build-toggle js-sidebar-build-toggle-header"
+ class="btn btn-default d-block d-sm-none d-md-none
+sidebar-toggle-btn js-sidebar-build-toggle js-sidebar-build-toggle-header"
aria-label="Toggle Sidebar"
- id="toggleSidebar"
>
<i
class="fa fa-angle-double-left"
diff --git a/app/assets/javascripts/vue_shared/components/identicon.vue b/app/assets/javascripts/vue_shared/components/identicon.vue
index 23010f40f26..4ffc811e714 100644
--- a/app/assets/javascripts/vue_shared/components/identicon.vue
+++ b/app/assets/javascripts/vue_shared/components/identicon.vue
@@ -43,9 +43,9 @@ export default {
<template>
<div
- class="avatar identicon"
:class="sizeClass"
- :style="identiconStyles">
+ :style="identiconStyles"
+ class="avatar identicon">
{{ identiconTitle }}
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue
index 3d39b3ab173..ca8ce554588 100644
--- a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue
+++ b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue
@@ -33,11 +33,11 @@
<template>
<div class="issuable-note-warning">
<icon
+ v-if="!isLockedAndConfidential"
:name="warningIcon"
:size="16"
class="icon inline"
aria-hidden="true"
- v-if="!isLockedAndConfidential"
/>
<span v-if="isLockedAndConfidential">
diff --git a/app/assets/javascripts/vue_shared/components/lib/utils/dom_utils.js b/app/assets/javascripts/vue_shared/components/lib/utils/dom_utils.js
new file mode 100644
index 00000000000..02f28da8bb0
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/lib/utils/dom_utils.js
@@ -0,0 +1,5 @@
+export function pixeliseValue(val) {
+ return val ? `${val}px` : '';
+}
+
+export default {};
diff --git a/app/assets/javascripts/vue_shared/components/loading_button.vue b/app/assets/javascripts/vue_shared/components/loading_button.vue
index 88c13a1f340..2ff0c056b9c 100644
--- a/app/assets/javascripts/vue_shared/components/loading_button.vue
+++ b/app/assets/javascripts/vue_shared/components/loading_button.vue
@@ -54,19 +54,19 @@
<template>
<button
- @click="onClick"
- type="button"
:class="containerClass"
:disabled="loading || disabled"
+ type="button"
+ @click="onClick"
>
<transition name="fade">
<loading-icon
v-if="loading"
:inline="true"
- class="js-loading-button-icon"
:class="{
'append-right-5': label
}"
+ class="js-loading-button-icon"
/>
</transition>
<transition name="fade">
diff --git a/app/assets/javascripts/vue_shared/components/loading_icon.vue b/app/assets/javascripts/vue_shared/components/loading_icon.vue
index 12a75e016d7..db22c5f02cd 100644
--- a/app/assets/javascripts/vue_shared/components/loading_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/loading_icon.vue
@@ -35,10 +35,10 @@
:is="rootElementType"
class="loading-container text-center">
<i
- class="fa fa-spin fa-spinner"
:class="cssClass"
- aria-hidden="true"
:aria-label="label"
+ class="fa fa-spin fa-spinner"
+ aria-hidden="true"
>
</i>
</component>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 12c7d125062..d62537021ca 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -1,5 +1,6 @@
<script>
import $ from 'jquery';
+ import { s__ } from '~/locale';
import Flash from '../../../flash';
import GLForm from '../../../gl_form';
import markdownHeader from './header.vue';
@@ -22,6 +23,11 @@
type: String,
required: true,
},
+ markdownVersion: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
addSpacingClasses: {
type: Boolean,
required: false,
@@ -62,7 +68,15 @@
/*
GLForm class handles all the toolbar buttons
*/
- return new GLForm($(this.$refs['gl-form']), this.enableAutocomplete);
+ return new GLForm($(this.$refs['gl-form']), {
+ emojis: this.enableAutocomplete,
+ members: this.enableAutocomplete,
+ issues: this.enableAutocomplete,
+ mergeRequests: this.enableAutocomplete,
+ epics: this.enableAutocomplete,
+ milestones: this.enableAutocomplete,
+ labels: this.enableAutocomplete,
+ });
},
beforeDestroy() {
const glForm = $(this.$refs['gl-form']).data('glForm');
@@ -84,10 +98,11 @@
if (text) {
this.markdownPreviewLoading = true;
- this.$http.post(this.markdownPreviewPath, { text })
- .then(resp => resp.json())
- .then(data => this.renderMarkdown(data))
- .catch(() => new Flash('Error loading markdown preview'));
+ this.$http
+ .post(this.versionedPreviewPath(), { text })
+ .then(resp => resp.json())
+ .then(data => this.renderMarkdown(data))
+ .catch(() => new Flash(s__('Error loading markdown preview')));
} else {
this.renderMarkdown();
}
@@ -111,23 +126,30 @@
$(this.$refs['markdown-preview']).renderGFM();
});
},
+
+ versionedPreviewPath() {
+ const { markdownPreviewPath, markdownVersion } = this;
+ return `${markdownPreviewPath}${
+ markdownPreviewPath.indexOf('?') === -1 ? '?' : '&'
+ }markdown_version=${markdownVersion}`;
+ },
},
};
</script>
<template>
<div
- class="md-area js-vue-markdown-field"
+ ref="gl-form"
:class="{ 'prepend-top-default append-bottom-default': addSpacingClasses }"
- ref="gl-form">
+ class="md-area js-vue-markdown-field">
<markdown-header
:preview-markdown="previewMarkdown"
@preview-markdown="showPreviewTab"
@write-markdown="showWriteTab"
/>
<div
- class="md-write-holder"
v-show="!previewMarkdown"
+ class="md-write-holder"
>
<div class="zen-backdrop">
<slot name="textarea"></slot>
@@ -137,8 +159,8 @@
aria-label="Enter zen mode"
>
<icon
- name="screen-normal"
:size="32"
+ name="screen-normal"
/>
</a>
<markdown-toolbar
@@ -149,8 +171,8 @@
</div>
</div>
<div
- class="md md-preview-holder md-preview"
v-show="previewMarkdown"
+ class="md md-preview-holder md-preview js-vue-md-preview"
>
<div
ref="markdown-preview"
@@ -164,8 +186,8 @@
<template v-if="previewMarkdown && !markdownPreviewLoading">
<div
v-if="referencedCommands"
- v-html="referencedCommands"
class="referenced-commands"
+ v-html="referencedCommands"
>
</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 db453c30576..8c22f3f6536 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -29,8 +29,8 @@
methods: {
isValid(form) {
return !form ||
- form.find('.js-vue-markdown-field').length ||
- $(this.$el).closest('form') === form[0];
+ form.find('.js-vue-markdown-field').length &&
+ $(this.$el).closest('form')[0] === form[0];
},
previewMarkdownTab(event, form) {
@@ -54,8 +54,8 @@
<div class="md-header">
<ul class="nav-links clearfix">
<li
- class="md-header-tab"
:class="{ active: !previewMarkdown }"
+ class="md-header-tab"
>
<a
class="js-write-link"
@@ -67,11 +67,11 @@
</a>
</li>
<li
- class="md-header-tab"
:class="{ active: previewMarkdown }"
+ class="md-header-tab"
>
<a
- class="js-preview-link"
+ class="js-preview-link js-md-preview-button"
href="#md-preview-holder"
tabindex="-1"
@click.prevent="previewMarkdownTab($event)"
@@ -80,8 +80,8 @@
</a>
</li>
<li
- class="md-header-toolbar"
:class="{ active: !previewMarkdown }"
+ class="md-header-toolbar"
>
<toolbar-button
tag="**"
@@ -94,8 +94,8 @@
icon="italic"
/>
<toolbar-button
- tag="> "
:prepend="true"
+ tag="> "
button-title="Insert a quote"
icon="quote"
/>
@@ -106,20 +106,20 @@
icon="code"
/>
<toolbar-button
- tag="* "
:prepend="true"
+ tag="* "
button-title="Add a bullet list"
icon="list-bulleted"
/>
<toolbar-button
- tag="1. "
:prepend="true"
+ tag="1. "
button-title="Add a numbered list"
icon="list-numbered"
/>
<toolbar-button
- tag="* [ ] "
:prepend="true"
+ tag="* [ ] "
button-title="Add a task list"
icon="task-done"
/>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
index c0ee88bbf72..d63318f3da6 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
@@ -111,7 +111,7 @@
Attach a file
</button>
<button
- class="btn btn-default btn-xs hide button-cancel-uploading-files"
+ class="btn btn-default btn-sm hide button-cancel-uploading-files"
type="button"
>
Cancel
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 2d2d69ebeb2..9f1e009efdd 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
@@ -39,15 +39,15 @@
<template>
<button
v-tooltip
- type="button"
- class="toolbar-btn js-md"
- tabindex="-1"
- data-container="body"
:data-md-tag="tag"
:data-md-block="tagBlock"
:data-md-prepend="prepend"
:title="buttonTitle"
:aria-label="buttonTitle"
+ type="button"
+ class="toolbar-btn js-md"
+ tabindex="-1"
+ data-container="body"
>
<icon
:name="icon"
diff --git a/app/assets/javascripts/vue_shared/components/memory_graph.vue b/app/assets/javascripts/vue_shared/components/memory_graph.vue
index b07f6b07afe..522091ea889 100644
--- a/app/assets/javascripts/vue_shared/components/memory_graph.vue
+++ b/app/assets/javascripts/vue_shared/components/memory_graph.vue
@@ -113,19 +113,19 @@ export default {
<template>
<div class="memory-graph-container">
<svg
- class="has-tooltip"
:title="getFormattedMedian"
:width="width"
:height="height"
+ class="has-tooltip"
xmlns="http://www.w3.org/2000/svg">
<path
:d="pathD"
:viewBox="pathViewBox"
/>
<circle
- r="1.5"
:cx="dotX"
:cy="dotY"
+ r="1.5"
tranform="translate(0 -1)"
/>
</svg>
diff --git a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue
index 92d187e24bf..99d61b5639d 100644
--- a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue
+++ b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue
@@ -59,15 +59,15 @@ export default {
}"
>
<a
+ :class="`js-${scope}-tab-${tab.scope}`"
role="button"
@click="onTabClick(tab)"
- :class="`js-${scope}-tab-${tab.scope}`"
>
{{ tab.name }}
<span
v-if="shouldRenderBadge(tab.count)"
- class="badge"
+ class="badge badge-pill"
>
{{ tab.count }}
</span>
diff --git a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
index 50b1508691b..38115f268bb 100644
--- a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
@@ -54,7 +54,7 @@
<div class="note-header">
<div class="note-header-info">
<a :href="getUserData.path">
- <span class="hidden-xs">{{ getUserData.name }}</span>
+ <span class="d-none d-sm-inline-block">{{ getUserData.name }}</span>
<span class="note-headline-light">@{{ getUserData.username }}</span>
</a>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
index 80e3db52cb0..2eb6c20b2c0 100644
--- a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
@@ -14,11 +14,12 @@
</template>
<script>
- import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
+import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
- export default {
- components: {
- skeletonLoadingContainer,
- },
- };
+export default {
+ name: 'SkeletonNote',
+ components: {
+ skeletonLoadingContainer,
+ },
+};
</script>
diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
index aac10f84087..2122d0a508e 100644
--- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
@@ -1,51 +1,75 @@
<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 noteHeader from '~/notes/components/note_header.vue';
- import { spriteIcon } from '../../../lib/utils/common_utils';
+/**
+ * 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 $ from 'jquery';
+import { mapGetters } from 'vuex';
+import noteHeader from '~/notes/components/note_header.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import { spriteIcon } from '../../../lib/utils/common_utils';
- export default {
- name: 'SystemNote',
- components: {
- noteHeader,
+const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
+
+export default {
+ name: 'SystemNote',
+ components: {
+ Icon,
+ noteHeader,
+ },
+ props: {
+ note: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ expanded: false,
+ };
+ },
+ computed: {
+ ...mapGetters(['targetNoteHash']),
+ noteAnchorId() {
+ return `note_${this.note.id}`;
+ },
+ isTargetNote() {
+ return this.targetNoteHash === this.noteAnchorId;
},
- props: {
- note: {
- type: Object,
- required: true,
- },
+ iconHtml() {
+ return spriteIcon(this.note.system_note_icon_name);
},
- computed: {
- ...mapGetters([
- 'targetNoteHash',
- ]),
- noteAnchorId() {
- return `note_${this.note.id}`;
- },
- isTargetNote() {
- return this.targetNoteHash === this.noteAnchorId;
- },
- iconHtml() {
- return spriteIcon(this.note.system_note_icon_name);
- },
+ toggleIcon() {
+ return this.expanded ? 'chevron-up' : 'chevron-down';
},
- };
+ // following 2 methods taken from code in `collapseLongCommitList` of notes.js:
+ actionTextHtml() {
+ return $(this.note.note_html)
+ .unwrap()
+ .html();
+ },
+ hasMoreCommits() {
+ return (
+ $(this.note.note_html)
+ .filter('ul')
+ .children().length > MAX_VISIBLE_COMMIT_LIST_COUNT
+ );
+ },
+ },
+};
</script>
<template>
@@ -64,8 +88,35 @@
:author="note.author"
:created-at="note.created_at"
:note-id="note.id"
- :action-text-html="note.note_html"
- />
+ >
+ <span v-html="actionTextHtml"></span>
+ </note-header>
+ </div>
+ <div class="note-body">
+ <div
+ :class="{
+ 'system-note-commit-list': hasMoreCommits,
+ 'hide-shade': expanded
+ }"
+ class="note-text"
+ v-html="note.note_html"
+ ></div>
+ <div
+ v-if="hasMoreCommits"
+ class="flex-list"
+ >
+ <div
+ class="system-note-commit-list-toggler flex-row"
+ @click="expanded = !expanded"
+ >
+ <Icon
+ :name="toggleIcon"
+ :size="8"
+ class="append-right-5"
+ />
+ <span>Toggle commit list</span>
+ </div>
+ </div>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/panel_resizer.vue b/app/assets/javascripts/vue_shared/components/panel_resizer.vue
index abbe9a22717..8c2dcc2d902 100644
--- a/app/assets/javascripts/vue_shared/components/panel_resizer.vue
+++ b/app/assets/javascripts/vue_shared/components/panel_resizer.vue
@@ -82,9 +82,9 @@
<template>
<div
- class="dragHandle"
:class="className"
:style="cursorStyle"
+ class="dragHandle"
@mousedown="startDrag"
@dblclick="resetSize"
></div>
diff --git a/app/assets/javascripts/vue_shared/components/project_avatar/image.vue b/app/assets/javascripts/vue_shared/components/project_avatar/image.vue
index 279cc1de5bb..97ca4d93bd7 100644
--- a/app/assets/javascripts/vue_shared/components/project_avatar/image.vue
+++ b/app/assets/javascripts/vue_shared/components/project_avatar/image.vue
@@ -85,7 +85,6 @@
<template>
<img
v-tooltip
- class="avatar"
:class="{
lazy: lazy,
[avatarSizeClass]: true,
@@ -99,5 +98,6 @@
:data-container="tooltipContainer"
:data-placement="tooltipPlacement"
:title="tooltipText"
+ class="avatar"
/>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue
index 21ffdc1dc86..a2a9a5e6987 100644
--- a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue
@@ -66,10 +66,10 @@
<template>
<deprecated-modal
- kind="warning"
- class="recaptcha-modal js-recaptcha-modal"
:hide-footer="true"
:title="__('Please solve the reCAPTCHA')"
+ kind="warning"
+ class="recaptcha-modal js-recaptcha-modal"
@cancel="close"
>
<div slot="body">
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
index 3fcacd156c5..74998a4787d 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
@@ -97,8 +97,8 @@
<template>
<div
- class="block"
:class="blockClass"
+ class="block"
>
<div class="issuable-sidebar-header">
<toggle-sidebar
@@ -107,8 +107,8 @@
/>
</div>
<collapsed-calendar-icon
- class="sidebar-collapsed-icon"
:text="collapsedText"
+ class="sidebar-collapsed-icon"
/>
<div class="title">
{{ label }}
@@ -116,7 +116,7 @@
v-if="isLoading"
:inline="true"
/>
- <div class="pull-right">
+ <div class="float-right">
<button
v-if="editable && !editing"
type="button"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
index 70b46a9c2bb..a3fc358130f 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
@@ -2,13 +2,13 @@
import $ from 'jquery';
import { __ } from '~/locale';
import LabelsSelect from '~/labels_select';
+import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
import LoadingIcon from '../../loading_icon.vue';
import DropdownTitle from './dropdown_title.vue';
import DropdownValue from './dropdown_value.vue';
import DropdownValueCollapsed from './dropdown_value_collapsed.vue';
import DropdownButton from './dropdown_button.vue';
-import DropdownHiddenInput from './dropdown_hidden_input.vue';
import DropdownHeader from './dropdown_header.vue';
import DropdownSearchInput from './dropdown_search_input.vue';
import DropdownFooter from './dropdown_footer.vue';
@@ -140,11 +140,11 @@ export default {
v-for="label in context.labels"
:key="label.id"
:name="hiddenInputName"
- :label="label"
+ :value="label.id"
/>
<div
- class="dropdown"
ref="dropdown"
+ class="dropdown"
>
<dropdown-button
:ability-name="abilityName"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue
index 47497c1de98..48d2f16f554 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue
@@ -53,10 +53,7 @@ export default {
<template>
<button
- type="button"
ref="dropdownButton"
- class="dropdown-menu-toggle wide js-label-select js-multiselect js-context-config-modal"
- data-toggle="dropdown"
:class="{ 'js-extra-options': showExtraOptions }"
:data-ability-name="abilityName"
:data-field-name="fieldName"
@@ -64,6 +61,9 @@ export default {
:data-labels="labelsPath"
:data-namespace-path="namespace"
:data-show-any="showExtraOptions"
+ type="button"
+ class="dropdown-menu-toggle wide js-label-select js-multiselect js-context-config-modal"
+ data-toggle="dropdown"
>
<span class="dropdown-toggle-text">
{{ dropdownToggleText }}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue
index 34a07f33a23..fe895136ccc 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue
@@ -19,9 +19,9 @@ export default {
<div class="dropdown-page-two dropdown-new-label">
<div class="dropdown-title">
<button
+ :aria-label="__('Go back')"
type="button"
class="dropdown-title-button dropdown-menu-back"
- :aria-label="__('Go back')"
>
<i
aria-hidden="true"
@@ -32,9 +32,9 @@ export default {
</button>
{{ headerTitle }}
<button
+ :aria-label="__('Close')"
type="button"
class="dropdown-title-button dropdown-menu-close"
- :aria-label="__('Close')"
>
<i
aria-hidden="true"
@@ -48,19 +48,19 @@ export default {
<div class="dropdown-labels-error js-label-error"></div>
<input
id="new_label_name"
+ :placeholder="__('Name new label')"
type="text"
class="default-dropdown-input"
- :placeholder="__('Name new label')"
/>
<div class="suggest-colors suggest-colors-dropdown">
<a
v-for="(color, index) in suggestedColors"
- href="#"
:key="index"
:data-color="color"
:style="{
backgroundColor: color,
}"
+ href="#"
>
&nbsp;
</a>
@@ -69,21 +69,21 @@ export default {
<div class="dropdown-label-color-preview js-dropdown-label-color-preview"></div>
<input
id="new_label_color"
+ :placeholder="__('Assign custom color like #FF0000')"
type="text"
class="default-dropdown-input"
- :placeholder="__('Assign custom color like #FF0000')"
/>
</div>
<div class="clearfix">
<button
type="button"
- class="btn btn-primary pull-left js-new-label-btn disabled"
+ class="btn btn-primary float-left js-new-label-btn disabled"
>
{{ __('Create') }}
</button>
<button
type="button"
- class="btn btn-default pull-right js-cancel-label-btn"
+ class="btn btn-default float-right js-cancel-label-btn"
>
{{ __('Cancel') }}
</button>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer.vue
index 5f61e9fbe80..d64ad016f9b 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer.vue
@@ -34,9 +34,9 @@ export default {
</li>
<li>
<a
+ :href="labelsWebUrl"
data-is-link="true"
class="dropdown-external-link"
- :href="labelsWebUrl"
>
{{ manageLabelsTitle }}
</a>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue
index 7664acdf19c..e98b6392827 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue
@@ -6,9 +6,9 @@ export default {};
<div class="dropdown-title">
<span>{{ __('Assign labels') }}</span>
<button
+ :aria-label="__('Close')"
type="button"
class="dropdown-title-button dropdown-menu-close"
- :aria-label="__('Close')"
>
<i
aria-hidden="true"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input.vue
deleted file mode 100644
index 1832c3c1757..00000000000
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input.vue
+++ /dev/null
@@ -1,22 +0,0 @@
-<script>
-export default {
- props: {
- name: {
- type: String,
- required: true,
- },
- label: {
- type: Object,
- required: true,
- },
- },
-};
-</script>
-
-<template>
- <input
- type="hidden"
- :name="name"
- :value="label.id"
- />
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue
index ae633460c95..80d65a2a534 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue
@@ -5,10 +5,10 @@ export default {};
<template>
<div class="dropdown-input">
<input
+ :placeholder="__('Search')"
autocomplete="off"
class="dropdown-input-field"
type="search"
- :placeholder="__('Search')"
/>
<i
aria-hidden="true"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue
index 7da82e90e29..9ac32ff13c6 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue
@@ -21,7 +21,7 @@ export default {
</i>
<button
type="button"
- class="edit-link btn btn-blank pull-right js-sidebar-dropdown-toggle"
+ class="edit-link btn btn-blank float-right js-sidebar-dropdown-toggle"
>
{{ __('Edit') }}
</button>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue
index 69d588eb25d..10e990f8a80 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue
@@ -35,7 +35,12 @@ export default {
</script>
<template>
- <div class="hide-collapsed value issuable-show-labels js-value">
+ <div
+ :class="{
+ 'has-labels':!isEmpty,
+ }"
+ class="hide-collapsed value issuable-show-labels js-value"
+ >
<span
v-if="isEmpty"
class="text-secondary"
@@ -43,18 +48,18 @@ export default {
<slot>{{ __('None') }}</slot>
</span>
<a
- v-else
v-for="label in labels"
+ v-else
:key="label.id"
:href="labelFilterUrl(label)"
>
<span
v-tooltip
- class="label color-label"
- data-placement="bottom"
- data-container="body"
:style="labelStyle(label)"
:title="label.description"
+ class="badge color-label"
+ data-placement="bottom"
+ data-container="body"
>
{{ label.title }}
</span>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
index 68fa2ab8d01..af297f3c408 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
@@ -37,10 +37,10 @@ export default {
<template>
<div
v-tooltip
+ :title="labelsList"
class="sidebar-collapsed-icon"
data-placement="left"
data-container="body"
- :title="labelsList"
@click="handleClick"
>
<i
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue b/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue
index de6f8c32e74..ac2e99abe77 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue
@@ -28,21 +28,21 @@ export default {
<template>
<button
+ v-tooltip
+ :title="tooltipLabel"
type="button"
class="btn btn-blank gutter-toggle btn-sidebar-action"
- @click="toggle"
- v-tooltip
data-container="body"
data-placement="left"
- :title="tooltipLabel"
+ @click="toggle"
>
<i
- aria-label="toggle collapse"
- class="fa"
:class="{
'fa-angle-double-right': !collapsed,
'fa-angle-double-left': collapsed
}"
+ aria-label="toggle collapse"
+ class="fa"
>
</i>
</button>
diff --git a/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue b/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue
index 16304e4815d..4a5ffbe5d5a 100644
--- a/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue
+++ b/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue
@@ -22,10 +22,10 @@
<template>
<div
- class="animation-container"
:class="{
'animation-container-small': small,
}"
+ class="animation-container"
>
<div
v-for="(css, index) in lineClasses"
diff --git a/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue b/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue
index 86f06c8d266..b1c2df54ef6 100644
--- a/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue
+++ b/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue
@@ -84,8 +84,8 @@ export default {
<template>
<div
- class="stacked-progress-bar"
:class="cssClass"
+ class="stacked-progress-bar"
>
<span
v-if="!totalCount"
@@ -96,30 +96,30 @@ export default {
<span
v-tooltip
v-if="successPercent"
- class="status-green"
- data-placement="bottom"
:title="successTooltip"
:style="successBarStyle"
+ class="status-green"
+ data-placement="bottom"
>
{{ successPercent }}%
</span>
<span
v-tooltip
v-if="neutralPercent"
- class="status-neutral"
- data-placement="bottom"
:title="neutralTooltip"
:style="neutralBarStyle"
+ class="status-neutral"
+ data-placement="bottom"
>
{{ neutralPercent }}%
</span>
<span
v-tooltip
v-if="failurePercent"
- class="status-red"
- data-placement="bottom"
:title="failureTooltip"
:style="failureBarStyle"
+ class="status-red"
+ data-placement="bottom"
>
{{ failurePercent }}%
</span>
diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.vue b/app/assets/javascripts/vue_shared/components/table_pagination.vue
index 22fc5757447..8e9621c956f 100644
--- a/app/assets/javascripts/vue_shared/components/table_pagination.vue
+++ b/app/assets/javascripts/vue_shared/components/table_pagination.vue
@@ -55,7 +55,7 @@
},
getItems() {
const total = this.pageInfo.totalPages;
- const page = this.pageInfo.page;
+ const { page } = this.pageInfo;
const items = [];
if (page > 1) {
@@ -124,15 +124,18 @@
break;
}
},
+ hideOnSmallScreen(item) {
+ return !item.first && !item.last && !item.next && !item.prev && !item.active;
+ },
},
};
</script>
<template>
<div
v-if="showPagination"
- class="gl-pagination"
+ class="gl-pagination prepend-top-default"
>
- <ul class="pagination clearfix">
+ <ul class="pagination justify-content-center">
<li
v-for="(item, index) in getItems"
:key="index"
@@ -142,12 +145,17 @@
'js-next-button': item.next,
'js-last-button': item.last,
'js-first-button': item.first,
+ 'd-none d-md-block': hideOnSmallScreen(item),
separator: item.separator,
active: item.active,
- disabled: item.disabled
+ disabled: item.disabled || item.separator
}"
+ class="page-item"
>
- <a @click.prevent="changePage(item.title, item.disabled)">
+ <a
+ class="page-link"
+ @click.prevent="changePage(item.title, item.disabled)"
+ >
{{ item.title }}
</a>
</li>
diff --git a/app/assets/javascripts/vue_shared/components/tabs/tab.vue b/app/assets/javascripts/vue_shared/components/tabs/tab.vue
new file mode 100644
index 00000000000..1c6011dcfd0
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/tabs/tab.vue
@@ -0,0 +1,47 @@
+<script>
+export default {
+ props: {
+ title: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ active: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ // props can't be updated, so we map it to data where we can
+ localActive: this.active,
+ };
+ },
+ watch: {
+ active() {
+ this.localActive = this.active;
+ },
+ },
+ created() {
+ this.isTab = true;
+ },
+ updated() {
+ if (this.$parent) {
+ this.$parent.$forceUpdate();
+ }
+ },
+};
+</script>
+
+<template>
+ <div
+ :class="{
+ active: localActive
+ }"
+ class="tab-pane"
+ role="tabpanel"
+ >
+ <slot></slot>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/tabs/tabs.js b/app/assets/javascripts/vue_shared/components/tabs/tabs.js
new file mode 100644
index 00000000000..9b9e4bb47bd
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/tabs/tabs.js
@@ -0,0 +1,76 @@
+export default {
+ props: {
+ stopPropagation: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ currentIndex: 0,
+ tabs: [],
+ };
+ },
+ mounted() {
+ this.updateTabs();
+ },
+ methods: {
+ updateTabs() {
+ this.tabs = this.$children.filter(child => child.isTab);
+ this.currentIndex = this.tabs.findIndex(tab => tab.localActive);
+ },
+ setTab(e, index) {
+ if (this.stopPropagation) {
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ this.tabs[this.currentIndex].localActive = false;
+ this.tabs[index].localActive = true;
+
+ this.currentIndex = index;
+ },
+ },
+ render(h) {
+ const navItems = this.tabs.map((tab, i) =>
+ h(
+ 'li',
+ {
+ key: i,
+ },
+ [
+ h(
+ 'a',
+ {
+ class: tab.localActive ? 'active' : null,
+ attrs: {
+ href: '#',
+ },
+ on: {
+ click: e => this.setTab(e, i),
+ },
+ },
+ tab.$slots.title || tab.title,
+ ),
+ ],
+ ),
+ );
+ const nav = h(
+ 'ul',
+ {
+ class: 'nav-links tab-links',
+ },
+ [navItems],
+ );
+ const content = h(
+ 'div',
+ {
+ class: ['tab-content'],
+ },
+ [this.$slots.default],
+ );
+
+ return h('div', {}, [[nav], content]);
+ },
+};
diff --git a/app/assets/javascripts/vue_shared/components/toggle_button.vue b/app/assets/javascripts/vue_shared/components/toggle_button.vue
index 09031d3ffa1..a897300b62b 100644
--- a/app/assets/javascripts/vue_shared/components/toggle_button.vue
+++ b/app/assets/javascripts/vue_shared/components/toggle_button.vue
@@ -63,26 +63,26 @@
<label class="toggle-wrapper">
<input
v-if="name"
- type="hidden"
:name="name"
:value="value"
+ type="hidden"
/>
<button
- type="button"
- class="project-feature-toggle"
:aria-label="ariaLabel"
:class="{
'is-checked': value,
'is-disabled': disabledInput,
'is-loading': isLoading
}"
+ type="button"
+ class="project-feature-toggle"
@click="toggleFeature"
>
<loadingIcon class="loading-icon" />
<span class="toggle-icon">
<icon
- css-classes="toggle-icon-svg"
- :name="toggleIcon"/>
+ :name="toggleIcon"
+ css-classes="toggle-icon-svg"/>
</span>
</button>
</label>
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 cc9cc46bb4c..3a413c74410 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
@@ -85,7 +85,6 @@ export default {
<template>
<img
v-tooltip
- class="avatar"
:class="{
lazy: lazy,
[avatarSizeClass]: true,
@@ -99,5 +98,7 @@ export default {
:data-container="tooltipContainer"
:data-placement="tooltipPlacement"
:title="tooltipText"
+ class="avatar"
+ data-boundary="window"
/>
</template>
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 6955d164def..01c36fec41a 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
@@ -84,8 +84,8 @@ export default {
<template>
<a
- class="user-avatar-link"
- :href="linkHref">
+ :href="linkHref"
+ class="user-avatar-link">
<user-avatar-image
:img-src="imgSrc"
:img-alt="imgAlt"
@@ -94,8 +94,8 @@ export default {
:tooltip-text="avatarTooltipText"
:tooltip-placement="tooltipPlacement"
/><span
- v-if="shouldShowUsername"
v-tooltip
+ v-if="shouldShowUsername"
:title="tooltipText"
:tooltip-placement="tooltipPlacement"
>{{ username }}</span>
diff --git a/app/assets/javascripts/vue_shared/directives/popover.js b/app/assets/javascripts/vue_shared/directives/popover.js
index eb35294906b..c913bc34c68 100644
--- a/app/assets/javascripts/vue_shared/directives/popover.js
+++ b/app/assets/javascripts/vue_shared/directives/popover.js
@@ -17,6 +17,6 @@ export default {
},
unbind(el) {
- $(el).popover('destroy');
+ $(el).popover('dispose');
},
};
diff --git a/app/assets/javascripts/vue_shared/directives/tooltip.js b/app/assets/javascripts/vue_shared/directives/tooltip.js
index b7f7e9fec15..4f2412ce520 100644
--- a/app/assets/javascripts/vue_shared/directives/tooltip.js
+++ b/app/assets/javascripts/vue_shared/directives/tooltip.js
@@ -2,14 +2,16 @@ import $ from 'jquery';
export default {
bind(el) {
- $(el).tooltip();
+ $(el).tooltip({
+ trigger: 'hover',
+ });
},
componentUpdated(el) {
- $(el).tooltip('fixTitle');
+ $(el).tooltip('_fixTitle');
},
unbind(el) {
- $(el).tooltip('destroy');
+ $(el).tooltip('dispose');
},
};
diff --git a/app/assets/javascripts/vue_shared/models/assignee.js b/app/assets/javascripts/vue_shared/models/assignee.js
new file mode 100644
index 00000000000..4a29b0d0581
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/models/assignee.js
@@ -0,0 +1,13 @@
+export default class ListAssignee {
+ constructor(obj, defaultAvatar) {
+ this.id = obj.id;
+ this.name = obj.name;
+ this.username = obj.username;
+ this.avatar = obj.avatar_url || obj.avatar || defaultAvatar;
+ this.path = obj.path;
+ this.state = obj.state;
+ this.webUrl = obj.web_url || obj.webUrl;
+ }
+}
+
+window.ListAssignee = ListAssignee;
diff --git a/app/assets/javascripts/vue_shared/translate.js b/app/assets/javascripts/vue_shared/translate.js
index 2c7886ec308..48c63373b77 100644
--- a/app/assets/javascripts/vue_shared/translate.js
+++ b/app/assets/javascripts/vue_shared/translate.js
@@ -13,7 +13,7 @@ export default (Vue) => {
@param text The text to be translated
@returns {String} The translated text
- **/
+ */
__,
/**
Translate the text with a number
@@ -24,7 +24,7 @@ export default (Vue) => {
@param pluralText Plural text to translate (eg. '%d days')
@param count Number to decide which translation to use (eg. 2)
@returns {String} Translated text with the number replaced (eg. '2 days')
- **/
+ */
n__,
/**
Translate context based text
@@ -36,7 +36,7 @@ export default (Vue) => {
(eg. 'Context')
@param key Is the dynamic variable you want to be translated
@returns {String} Translated context based text
- **/
+ */
s__,
sprintf,
},
diff --git a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js
index b9693892f45..73b9131e5ba 100644
--- a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js
+++ b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js
@@ -28,7 +28,7 @@ Vue.http.interceptors.push((request, next) => {
response.headers.forEach((value, key) => {
headers[key] = value;
});
- // eslint-disable-next-line no-param-reassign
+
response.headers = headers;
});
});
diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js
index f68a4f28714..0138c9be803 100644
--- a/app/assets/javascripts/zen_mode.js
+++ b/app/assets/javascripts/zen_mode.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, consistent-return, camelcase, comma-dangle, max-len, class-methods-use-this */
+/* eslint-disable func-names, wrap-iife, prefer-arrow-callback, no-unused-vars, consistent-return, camelcase, comma-dangle, max-len, class-methods-use-this */
// Zen Mode (full screen) textarea
//
diff --git a/app/assets/stylesheets/bootstrap.scss b/app/assets/stylesheets/bootstrap.scss
new file mode 100644
index 00000000000..a040c2f8c20
--- /dev/null
+++ b/app/assets/stylesheets/bootstrap.scss
@@ -0,0 +1,37 @@
+/*
+ * Includes specific styles from the bootstrap4 foler in node_modules
+ */
+
+@import "../../../node_modules/bootstrap/scss/functions";
+@import "../../../node_modules/bootstrap/scss/variables";
+@import "../../../node_modules/bootstrap/scss/mixins";
+@import "../../../node_modules/bootstrap/scss/root";
+@import "../../../node_modules/bootstrap/scss/reboot";
+@import "../../../node_modules/bootstrap/scss/type";
+@import "../../../node_modules/bootstrap/scss/images";
+@import "../../../node_modules/bootstrap/scss/code";
+@import "../../../node_modules/bootstrap/scss/grid";
+@import "../../../node_modules/bootstrap/scss/tables";
+@import "../../../node_modules/bootstrap/scss/forms";
+@import "../../../node_modules/bootstrap/scss/buttons";
+@import "../../../node_modules/bootstrap/scss/transitions";
+@import "../../../node_modules/bootstrap/scss/dropdown";
+@import "../../../node_modules/bootstrap/scss/button-group";
+@import "../../../node_modules/bootstrap/scss/input-group";
+@import "../../../node_modules/bootstrap/scss/custom-forms";
+@import "../../../node_modules/bootstrap/scss/nav";
+@import "../../../node_modules/bootstrap/scss/navbar";
+@import "../../../node_modules/bootstrap/scss/card";
+@import "../../../node_modules/bootstrap/scss/breadcrumb";
+@import "../../../node_modules/bootstrap/scss/pagination";
+@import "../../../node_modules/bootstrap/scss/badge";
+@import "../../../node_modules/bootstrap/scss/alert";
+@import "../../../node_modules/bootstrap/scss/progress";
+@import "../../../node_modules/bootstrap/scss/media";
+@import "../../../node_modules/bootstrap/scss/list-group";
+@import "../../../node_modules/bootstrap/scss/close";
+@import "../../../node_modules/bootstrap/scss/modal";
+@import "../../../node_modules/bootstrap/scss/tooltip";
+@import "../../../node_modules/bootstrap/scss/popover";
+@import "../../../node_modules/bootstrap/scss/utilities";
+@import "../../../node_modules/bootstrap/scss/print";
diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss
new file mode 100644
index 00000000000..ded33e8b151
--- /dev/null
+++ b/app/assets/stylesheets/bootstrap_migration.scss
@@ -0,0 +1,340 @@
+/*
+ * Scss to help with bootstrap 3 to 4 migration
+ */
+
+$text-color: $gl-text-color;
+
+$brand-primary: $gl-primary;
+$brand-success: $gl-success;
+$brand-info: $gl-info;
+$brand-warning: $gl-warning;
+$brand-danger: $gl-danger;
+
+$border-radius-base: 3px !default;
+
+$modal-body-bg: $white-light;
+$input-border: $border-color;
+$input-border-focus: $focus-border-color;
+
+$padding-base-vertical: $gl-vert-padding;
+$padding-base-horizontal: $gl-padding;
+
+html {
+ // Override default font size used in bs4
+ font-size: 14px;
+}
+
+legend {
+ border-bottom: 1px solid $border-color;
+ margin-bottom: 20px;
+}
+
+button,
+html [type="button"],
+[type="reset"],
+[type="submit"],
+[role="button"] {
+ // Override bootstrap reboot
+ -webkit-appearance: inherit;
+ cursor: pointer;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ color: $gl-text-color;
+ font-weight: 600;
+}
+
+h1,
+.h1,
+h2,
+.h2,
+h3,
+.h3 {
+ margin-top: 20px;
+ margin-bottom: 10px;
+}
+
+h4,
+.h4,
+h5,
+.h5,
+h6,
+.h6 {
+ margin-top: 10px;
+ margin-bottom: 10px;
+}
+
+h5,
+.h5 {
+ font-size: $gl-font-size;
+}
+
+input[type="file"] {
+ // Bootstrap 4 file input height is taller by default
+ // which makes them look ugly
+ line-height: 1;
+}
+
+b,
+strong {
+ font-weight: bold;
+}
+
+a {
+ color: $gl-link-color;
+}
+
+hr {
+ overflow: hidden;
+}
+
+.form-group.row .col-form-label {
+ padding-top: 0;
+ // Bootstrap 4 aligns labels to the left
+ // for horizontal forms
+ @include media-breakpoint-up(md) {
+ text-align: right;
+ }
+}
+
+kbd {
+ display: inline-block;
+}
+
+code {
+ padding: 2px 4px;
+ color: $red-600;
+ background-color: $red-100;
+ border-radius: 3px;
+
+ .code > & {
+ background-color: inherit;
+ padding: unset;
+ }
+
+ .build-trace & {
+ background-color: inherit;
+ padding: inherit;
+ }
+}
+
+table {
+ // Remove any table border lines
+ border-spacing: 0;
+}
+
+.tooltip {
+ // Fix bootstrap4 bug whereby tooltips flicker when they are hovered over their borders
+ pointer-events: none;
+}
+
+.popover {
+ font-size: 14px;
+}
+
+@each $breakpoint in map-keys($grid-breakpoints) {
+ @include media-breakpoint-up($breakpoint) {
+ $infix: breakpoint-infix($breakpoint, $grid-breakpoints);
+
+ .d#{$infix}-table-header-group { display: table-header-group !important; }
+ }
+}
+
+.text-secondary {
+ // Override Bootstrap's light secondary color
+ // We have to use !important because bootstrap has that set as well
+ color: $gl-text-color-secondary !important;
+}
+
+.bg-success,
+.bg-primary,
+.bg-info,
+.bg-danger,
+.bg-warning {
+ .card-header {
+ color: $white-light;
+ }
+}
+
+// Polyfill deprecated selectors
+
+.hidden {
+ display: none !important;
+ visibility: hidden !important;
+}
+
+.hide {
+ display: none;
+}
+
+.dropdown-toggle::after,
+.dropright .dropdown-menu-toggle::after {
+ // Remove bootstrap's dropdown caret
+ display: none;
+}
+
+h3.popover-header {
+ // Default bootstrap popovers use <h3>
+ // which we default to having a top margin
+ margin-top: 0;
+}
+
+// Add to .label so that old system notes that are saved to the db
+// will still receive the correct styling
+.badge,
+.label {
+ padding: 4px 5px;
+ font-size: 12px;
+ font-style: normal;
+ font-weight: $gl-font-weight-normal;
+ display: inline-block;
+
+ &.badge-gray {
+ background-color: $label-gray-bg;
+ color: $gl-text-color;
+ text-shadow: none;
+ }
+
+ &.badge-inverse {
+ background-color: $label-inverse-bg;
+ }
+}
+
+.divider {
+ @extend .dropdown-divider;
+}
+
+.info-well {
+ background: $theme-gray-50;
+ color: $gl-text-color;
+ border: 1px solid $border-color;
+ border-radius: 4px;
+ margin-bottom: 16px;
+
+ .well-segment {
+ padding: 16px;
+
+ &:not(:last-of-type) {
+ border-bottom: 1px solid $well-inner-border;
+ }
+
+ p,
+ ol,
+ ul,
+ .form-group {
+ &:last-of-type {
+ margin-bottom: 0;
+ }
+ }
+ }
+
+ .badge.badge-gray {
+ background-color: $well-expand-item;
+ }
+}
+
+.card {
+ .card-title {
+ margin-bottom: 0;
+ }
+
+ &.card-without-border {
+ @extend .border-0;
+ }
+
+ &.card-without-margin {
+ margin: 0;
+ }
+
+ &.bg-light {
+ @extend .border-0;
+ }
+}
+
+.card-header {
+ h3.card-title,
+ h4.card-title {
+ margin-top: 0;
+ }
+}
+
+.nav-tabs {
+ // Override bootstrap's default border
+ border-bottom: 0;
+
+ .nav-link {
+ border-top: 0;
+ border-left: 0;
+ border-right: 0;
+ }
+
+ .nav-item {
+ margin-bottom: 0;
+ }
+}
+
+pre code {
+ white-space: pre-wrap;
+}
+
+.alert,
+.flash-notice {
+ border-radius: 0;
+}
+
+.alert-success {
+ background-color: $green-500;
+ border-color: $green-500;
+}
+
+.alert-info {
+ background-color: $blue-500;
+ border-color: $blue-500;
+}
+
+.alert-warning {
+ background-color: $orange-500;
+ border-color: $orange-500;
+}
+
+.alert-danger {
+ background-color: $red-500;
+ border-color: $red-500;
+}
+
+.alert-success,
+.alert-info,
+.alert-warning,
+.alert-danger,
+.flash-notice {
+ color: $white-light;
+
+ h4,
+ a:not(.btn),
+ .alert-link {
+ color: $white-light;
+ }
+}
+
+input[type=color].form-control {
+ height: $input-height;
+}
+
+.toggle-sidebar-button {
+ .collapse-text,
+ .icon-angle-double-left,
+ .icon-angle-double-right {
+ color: $gl-text-color-secondary;
+ }
+}
+
+.project-templates-buttons {
+ .btn {
+ vertical-align: unset;
+ }
+}
diff --git a/app/assets/stylesheets/errors.scss b/app/assets/stylesheets/errors.scss
new file mode 100644
index 00000000000..658e0ff638e
--- /dev/null
+++ b/app/assets/stylesheets/errors.scss
@@ -0,0 +1,120 @@
+/*
+ * This is a minimal stylesheet, meant to be used for error pages.
+ */
+@import 'framework/variables';
+@import '../../../node_modules/bootstrap/scss/functions';
+@import '../../../node_modules/bootstrap/scss/variables';
+@import '../../../node_modules/bootstrap/scss/mixins';
+@import '../../../node_modules/bootstrap/scss/reboot';
+@import '../../../node_modules/bootstrap/scss/buttons';
+@import '../../../node_modules/bootstrap/scss/forms';
+
+$body-color: #666;
+$header-color: #456;
+
+body {
+ color: $body-color;
+ text-align: center;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ margin: auto;
+ font-size: 14px;
+}
+
+h1 {
+ font-size: 56px;
+ line-height: 100px;
+ font-weight: 400;
+ color: $header-color;
+}
+
+h2 {
+ font-size: 24px;
+ color: $body-color;
+ line-height: 1.5em;
+}
+
+h3 {
+ color: $header-color;
+ font-size: 20px;
+ font-weight: 400;
+ line-height: 28px;
+}
+
+img {
+ max-width: 80vw;
+ display: block;
+ margin: 40px auto;
+}
+
+a {
+ text-decoration: none;
+ color: $blue-600;
+}
+
+.page-container {
+ margin: auto 20px;
+}
+
+.container {
+ margin: auto;
+ max-width: 600px;
+ border-bottom: 1px solid $border-color;
+ padding-bottom: 1em;
+}
+
+.action-container {
+ padding: 0.5em 0;
+}
+
+.form-inline-flex {
+ display: flex;
+ flex-wrap: wrap;
+
+ button {
+ display: block;
+ width: 100%;
+ }
+
+ .field {
+ display: block;
+ width: 100%;
+ margin-bottom: 1em;
+ }
+
+ @include media-breakpoint-up(sm) {
+ flex-wrap: nowrap;
+
+ button {
+ width: auto;
+ }
+
+ .field {
+ margin-bottom: 0;
+ margin-right: 0.5em;
+ }
+ }
+}
+
+.error-nav {
+ padding: 0;
+ text-align: center;
+
+ li {
+ display: block;
+ padding-bottom: 1em;
+ }
+
+ @include media-breakpoint-up(sm) {
+
+ li {
+ display: inline-block;
+ padding-bottom: 0;
+
+ &:not(:first-child)::before {
+ content: '\00B7';
+ display: inline-block;
+ padding: 0 1em;
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 9bd35183d8a..c46b0b5db09 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -1,7 +1,8 @@
@import 'framework/variables';
@import 'framework/mixins';
-@import 'framework/tw_bootstrap_variables';
-@import 'framework/tw_bootstrap';
+
+@import 'bootstrap';
+@import 'bootstrap_migration';
@import 'framework/layout';
@import 'framework/animations';
diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss
index 14cd32da9eb..549a8730301 100644
--- a/app/assets/stylesheets/framework/animations.scss
+++ b/app/assets/stylesheets/framework/animations.scss
@@ -251,3 +251,12 @@ $skeleton-line-widths: (
transform: translateX(468px);
}
}
+
+.slide-down-enter-active {
+ transition: transform 0.2s;
+}
+
+.slide-down-enter,
+.slide-down-leave-to {
+ transform: translateY(-30%);
+}
diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss
index 077d0424093..c1ec11e434a 100644
--- a/app/assets/stylesheets/framework/avatar.scss
+++ b/app/assets/stylesheets/framework/avatar.scss
@@ -31,7 +31,7 @@
.avatar {
@extend .avatar-circle;
- @include transition-property(none);
+ transition-property: none;
width: 40px;
height: 40px;
diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss
index a538b5a2946..8d11b92cf88 100644
--- a/app/assets/stylesheets/framework/awards.scss
+++ b/app/assets/stylesheets/framework/awards.scss
@@ -104,6 +104,10 @@
position: relative;
top: 3px;
}
+
+ > gl-emoji {
+ line-height: 1.5;
+ }
}
.award-menu-holder {
diff --git a/app/assets/stylesheets/framework/badges.scss b/app/assets/stylesheets/framework/badges.scss
index 6bbe32df772..57df9b969c3 100644
--- a/app/assets/stylesheets/framework/badges.scss
+++ b/app/assets/stylesheets/framework/badges.scss
@@ -1,4 +1,4 @@
-.badge {
+.badge.badge-pill {
font-weight: $gl-font-weight-normal;
background-color: $badge-bg;
color: $badge-color;
diff --git a/app/assets/stylesheets/framework/banner.scss b/app/assets/stylesheets/framework/banner.scss
index 02f3896d591..71bbab2065d 100644
--- a/app/assets/stylesheets/framework/banner.scss
+++ b/app/assets/stylesheets/framework/banner.scss
@@ -23,7 +23,7 @@
border-bottom: 1px solid $border-color;
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
justify-content: center;
flex-direction: column;
align-items: center;
diff --git a/app/assets/stylesheets/framework/blank.scss b/app/assets/stylesheets/framework/blank.scss
index 9982a5779af..91dbb2a6365 100644
--- a/app/assets/stylesheets/framework/blank.scss
+++ b/app/assets/stylesheets/framework/blank.scss
@@ -37,7 +37,7 @@
flex: 0 0 100%;
margin-bottom: 15px;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
flex: 0 0 49%;
&:nth-child(odd) {
@@ -67,7 +67,7 @@
border: 1px solid $border-color;
border-radius: $border-radius-default;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
display: flex;
align-items: center;
padding: 50px 30px;
@@ -89,12 +89,12 @@
}
.blank-state-body {
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
text-align: center;
margin-top: 20px;
}
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
padding-left: 20px;
}
}
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index c60f65e7a85..340fddd398b 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -13,9 +13,11 @@
&.diff-collapsed {
padding: 5px;
+ line-height: 34px;
.click-to-expand {
cursor: pointer;
+ vertical-align: initial;
}
}
}
@@ -151,7 +153,7 @@
display: inline-block;
margin-left: 5px;
font-size: 18px;
- color: $gray;
+ color: color("gray");
}
p {
@@ -195,7 +197,7 @@
}
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
text-align: center;
.avatar {
@@ -315,7 +317,7 @@
}
}
- @media (max-width: $screen-sm-min) {
+ @include media-breakpoint-down(sm) {
flex-direction: column;
.inner-content {
@@ -342,17 +344,12 @@
.btn {
margin: $btn-side-margin 5px;
- @media(max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
width: 100%;
}
}
}
-.flex-container-block {
- display: -webkit-flex;
- display: flex;
-}
-
.flex-right {
margin-left: auto;
}
diff --git a/app/assets/stylesheets/framework/broadcast_messages.scss b/app/assets/stylesheets/framework/broadcast_messages.scss
index 9b54fb94cdc..d3e7d751e63 100644
--- a/app/assets/stylesheets/framework/broadcast_messages.scss
+++ b/app/assets/stylesheets/framework/broadcast_messages.scss
@@ -19,3 +19,9 @@
@extend .broadcast-message;
margin-bottom: 20px;
}
+
+.toggle-colors {
+ input {
+ min-height: 34px;
+ }
+}
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index cd9d60b96d3..523fcb05a87 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -121,10 +121,6 @@
&.btn-sm {
margin-left: $btn-sm-side-margin;
}
-
- &.btn-xs {
- margin-left: $btn-xs-side-margin;
- }
}
@mixin btn-svg {
@@ -150,10 +146,6 @@
line-height: 18px;
}
- &.btn-xs {
- padding: 2px 5px;
- }
-
&.btn-success,
&.btn-new,
&.btn-create,
@@ -304,7 +296,8 @@
padding: 0 5px;
}
-.input-group-btn {
+.input-group-prepend,
+.input-group-append {
.btn {
@include btn-middle;
@@ -393,7 +386,7 @@
}
}
-@media (max-width: $screen-xs-max) {
+@include media-breakpoint-down(xs) {
.btn-wide-on-xs {
width: 100%;
}
@@ -496,6 +489,10 @@ fieldset[disabled] .btn,
}
}
+[readonly] {
+ cursor: default;
+}
+
.btn-no-padding {
padding: 0;
}
diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss
index c165ec0b94b..0b9dff64b0b 100644
--- a/app/assets/stylesheets/framework/calendar.scss
+++ b/app/assets/stylesheets/framework/calendar.scss
@@ -4,7 +4,7 @@
border-top: 0;
direction: rtl;
- @media (min-width: $screen-sm-min) and (max-width: $screen-md-max) {
+ @media (min-width: map-get($grid-breakpoints, sm)) and (max-width: map-get($grid-breakpoints, sm)) {
overflow-x: auto;
}
}
diff --git a/app/assets/stylesheets/framework/ci_variable_list.scss b/app/assets/stylesheets/framework/ci_variable_list.scss
index 5fe835dd8f9..7207e5119ce 100644
--- a/app/assets/stylesheets/framework/ci_variable_list.scss
+++ b/app/assets/stylesheets/framework/ci_variable_list.scss
@@ -10,14 +10,14 @@
display: flex;
align-items: flex-start;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
align-items: flex-end;
}
&:not(:last-child) {
margin-bottom: $gl-btn-padding;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
margin-bottom: 3 * $gl-btn-padding;
}
}
@@ -26,7 +26,7 @@
.ci-variable-body-item:last-child {
margin-right: $ci-variable-remove-button-width;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
margin-right: 0;
}
}
@@ -35,7 +35,7 @@
display: none;
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
.ci-variable-row-body {
margin-right: $ci-variable-remove-button-width;
}
@@ -48,7 +48,7 @@
align-items: flex-start;
width: 100%;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
display: block;
}
}
@@ -59,7 +59,7 @@
&:not(:last-child) {
margin-right: $gl-btn-padding;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
margin-right: 0;
margin-bottom: $gl-btn-padding;
}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 2faea55a5f5..218e37602dd 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -70,7 +70,7 @@ pre {
padding: 0;
}
- &.well-pre {
+ &.card.card-body-pre {
border: 1px solid $well-pre-bg;
background: $gray-light;
border-radius: 0;
@@ -218,7 +218,7 @@ li.note {
}
.git_error_tips {
- @extend .col-md-6;
+ @extend .col-lg-6;
text-align: left;
margin-top: 40px;
@@ -262,12 +262,7 @@ li.note {
}
.milestone {
- &.milestone-closed {
- background: $gray-light;
- }
-
.progress {
- margin-bottom: 0;
margin-top: 4px;
box-shadow: none;
background-color: $border-gray-light;
@@ -305,14 +300,6 @@ img.emoji {
margin-bottom: 10px;
}
-.btn-sign-in {
- text-shadow: none;
-
- @media (min-width: $screen-sm-min) {
- margin-top: 8px;
- }
-}
-
.side-filters {
fieldset {
margin-bottom: 15px;
@@ -327,7 +314,7 @@ img.emoji {
}
}
-.well {
+.card.card-body {
margin-bottom: $gl-padding;
hr {
@@ -336,7 +323,7 @@ img.emoji {
}
.search_box {
- @extend .well;
+ @extend .card.card-body;
text-align: center;
}
diff --git a/app/assets/stylesheets/framework/contextual_sidebar.scss b/app/assets/stylesheets/framework/contextual_sidebar.scss
index e2d97d0298f..ea4cb9a0b75 100644
--- a/app/assets/stylesheets/framework/contextual_sidebar.scss
+++ b/app/assets/stylesheets/framework/contextual_sidebar.scss
@@ -1,11 +1,11 @@
.page-with-contextual-sidebar {
transition: padding-left $sidebar-transition-duration;
- @media (min-width: $screen-md-min) {
+ @include media-breakpoint-up(md) {
padding-left: $contextual-sidebar-collapsed-width;
}
- @media (min-width: $screen-lg-min) {
+ @include media-breakpoint-up(lg) {
padding-left: $contextual-sidebar-width;
}
@@ -16,7 +16,7 @@
}
.page-with-icon-sidebar {
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
padding-left: $contextual-sidebar-collapsed-width;
}
}
@@ -26,19 +26,25 @@
margin-right: 2px;
width: $contextual-sidebar-width;
- a {
+ > a,
+ > button {
transition: padding $sidebar-transition-duration;
font-weight: $gl-font-weight-bold;
display: flex;
+ width: 100%;
align-items: center;
padding: 10px 16px 10px 10px;
color: $gl-text-color;
- }
+ background-color: transparent;
+ border: 0;
+ text-align: left;
- &:hover,
- a:hover {
- background-color: $link-hover-background;
- color: $gl-text-color;
+ &:hover,
+ &:focus {
+ background-color: $link-hover-background;
+ color: $gl-text-color;
+ outline: 0;
+ }
}
.avatar-container {
@@ -62,8 +68,7 @@
}
.nav-sidebar {
- transition: width $sidebar-transition-duration,
- left $sidebar-transition-duration;
+ transition: width $sidebar-transition-duration, left $sidebar-transition-duration;
position: fixed;
z-index: 400;
width: $contextual-sidebar-width;
@@ -71,12 +76,12 @@
bottom: 0;
left: 0;
background-color: $gray-light;
- box-shadow: inset -2px 0 0 $border-color;
+ box-shadow: inset -1px 0 0 $border-color;
transform: translate3d(0, 0, 0);
&:not(.sidebar-collapsed-desktop) {
- @media (min-width: $screen-sm-min) and (max-width: $screen-md-max) {
- box-shadow: inset -2px 0 0 $border-color,
+ @media (min-width: map-get($grid-breakpoints, sm)) and (max-width: map-get($grid-breakpoints, sm)) {
+ box-shadow: inset -1px 0 0 $border-color,
2px 1px 3px $dropdown-shadow-color;
}
}
@@ -88,7 +93,7 @@
overflow-x: hidden;
}
- .badge:not(.fly-out-badge),
+ .badge.badge-pill:not(.fly-out-badge),
.sidebar-context-title,
.nav-item-name {
display: none;
@@ -142,7 +147,7 @@
}
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
left: (-$contextual-sidebar-width);
}
@@ -166,7 +171,7 @@
width: 100%;
overflow: auto;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
overflow: hidden;
}
}
@@ -207,8 +212,8 @@
> li {
> a {
- @media (min-width: $screen-sm-min) {
- margin-right: 2px;
+ @include media-breakpoint-up(sm) {
+ margin-right: 1px;
}
&:hover {
@@ -218,11 +223,11 @@
&.is-showing-fly-out {
> a {
- margin-right: 2px;
+ margin-right: 1px;
}
.sidebar-sub-level-items {
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
position: fixed;
top: 0;
left: 0;
@@ -277,7 +282,7 @@
}
}
- .badge {
+ .badge.badge-pill {
background-color: $inactive-badge-background;
color: $gl-text-color-secondary;
}
@@ -291,7 +296,7 @@
padding-left: 11px;
}
- .badge {
+ .badge.badge-pill {
font-weight: $gl-font-weight-bold;
}
@@ -311,14 +316,14 @@
.toggle-sidebar-button,
.close-nav-button {
- width: $contextual-sidebar-width - 2px;
+ width: $contextual-sidebar-width - 1px;
transition: width $sidebar-transition-duration;
position: fixed;
bottom: 0;
padding: $gl-padding;
background-color: $gray-light;
border: 0;
- border-top: 2px solid $border-color;
+ border-top: 1px solid $border-color;
color: $gl-text-color-secondary;
display: flex;
align-items: center;
@@ -339,7 +344,7 @@
}
.toggle-sidebar-button {
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
display: none;
}
}
@@ -373,7 +378,7 @@
.toggle-sidebar-button {
padding: 16px;
- width: $contextual-sidebar-collapsed-width - 2px;
+ width: $contextual-sidebar-collapsed-width - 1px;
.collapse-text,
.icon-angle-double-left {
@@ -420,7 +425,7 @@
color: $gl-text-color-secondary;
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
display: flex;
align-items: center;
@@ -429,7 +434,7 @@
}
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
+ .breadcrumbs-links {
padding-left: $gl-padding;
border-left: 1px solid $gl-text-color-quaternary;
@@ -437,7 +442,7 @@
}
}
-@media (max-width: $screen-xs-max) {
+@include media-breakpoint-down(xs) {
.close-nav-button {
display: flex;
}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 664aade7375..c7b5e22c33d 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -24,7 +24,7 @@
display: none;
}
-.open {
+.show.dropdown {
.dropdown-menu,
.dropdown-menu-nav {
@include set-visible;
@@ -32,9 +32,15 @@
max-height: $dropdown-max-height;
overflow-y: auto;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
width: 100%;
}
+
+ &.frequent-items-dropdown-menu {
+ padding: 0;
+ overflow-y: initial;
+ max-height: initial;
+ }
}
.dropdown-toggle,
@@ -63,6 +69,10 @@
border-radius: $border-radius-base;
white-space: nowrap;
+ &:disabled.read-only {
+ color: $gl-text-color !important;
+ }
+
&.no-outline {
outline: 0;
}
@@ -184,7 +194,7 @@
text-decoration: none;
- .badge {
+ .badge.badge-pill {
background-color: darken($dropdown-link-hover-bg, 5%);
}
}
@@ -210,11 +220,10 @@
.dropdown-menu,
.dropdown-menu-nav {
- @include set-invisible;
+ display: none;
position: absolute;
width: auto;
top: 100%;
- left: 0;
z-index: 300;
min-width: 240px;
max-width: 500px;
@@ -328,7 +337,7 @@
color: $gl-text-color-secondary;
}
- .badge + span:not(.badge) {
+ .badge.badge-pill + span:not(.badge.badge-pill) {
// Expects up to 3 digits on the badge
margin-right: 40px;
}
@@ -392,7 +401,7 @@
transform: translateY(0);
}
-.comment-type-dropdown.open .dropdown-menu {
+.comment-type-dropdown.show .dropdown-menu {
display: block;
}
@@ -473,16 +482,11 @@
.dropdown-select {
width: $dropdown-width;
- @media (max-width: $screen-sm-min) {
+ @include media-breakpoint-down(sm) {
width: 100%;
}
}
-.dropdown-menu-align-right {
- left: auto;
- right: 0;
-}
-
.dropdown-menu-selectable {
li {
a,
@@ -783,9 +787,10 @@
}
}
-@media (max-width: $screen-xs-max) {
+@include media-breakpoint-down(xs) {
.navbar-gitlab {
li.header-projects,
+ li.header-groups,
li.header-more,
li.header-new,
li.header-user {
@@ -809,18 +814,18 @@
}
}
-header.header-content .dropdown-menu.projects-dropdown-menu {
+header.header-content .dropdown-menu.frequent-items-dropdown-menu {
padding: 0;
}
-.projects-dropdown-container {
+.frequent-items-dropdown-container {
display: flex;
flex-direction: row;
width: 500px;
height: 334px;
- .project-dropdown-sidebar,
- .project-dropdown-content {
+ .frequent-items-dropdown-sidebar,
+ .frequent-items-dropdown-content {
padding: 8px 0;
}
@@ -828,49 +833,51 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
color: $almost-black;
}
- .project-dropdown-sidebar {
+ .frequent-items-dropdown-sidebar {
width: 30%;
border-right: 1px solid $border-color;
}
- .project-dropdown-content {
+ .frequent-items-dropdown-content {
position: relative;
width: 70%;
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
flex-direction: column;
width: 100%;
height: auto;
flex: 1;
- .project-dropdown-sidebar,
- .project-dropdown-content {
+ .frequent-items-dropdown-sidebar,
+ .frequent-items-dropdown-content {
width: 100%;
}
- .project-dropdown-sidebar {
+ .frequent-items-dropdown-sidebar {
border-bottom: 1px solid $border-color;
border-right: 0;
}
}
- .projects-list-frequent-container,
- .projects-list-search-container {
+ .section-header,
+ .frequent-items-list-container li.section-empty {
+ padding: 0 $gl-padding;
+ color: $gl-text-color-secondary;
+ font-size: $gl-font-size;
+ }
+
+ .frequent-items-list-container {
padding: 8px 0;
overflow-y: auto;
li.section-empty.section-failure {
color: $callout-danger-color;
}
- }
- .section-header,
- .projects-list-frequent-container li.section-empty,
- .projects-list-search-container li.section-empty {
- padding: 0 15px;
- color: $gl-text-color-secondary;
- font-size: $gl-font-size;
+ .frequent-items-list-item-container a {
+ display: flex;
+ }
}
.search-input-container {
@@ -890,12 +897,12 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
margin-top: 8px;
}
- .projects-list-search-container {
+ .frequent-items-search-container {
height: 284px;
}
- @media (max-width: $screen-xs-max) {
- .projects-list-frequent-container {
+ @include media-breakpoint-down(xs) {
+ .frequent-items-list-container {
width: auto;
height: auto;
padding-bottom: 0;
@@ -903,40 +910,46 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
}
}
-.projects-list-item-container {
- .project-item-avatar-container .project-item-metadata-container {
+.frequent-items-list-item-container {
+ .frequent-items-item-avatar-container,
+ .frequent-items-item-metadata-container {
float: left;
}
- .project-title,
- .project-namespace {
+ .frequent-items-item-metadata-container {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ }
+
+ .frequent-items-item-title,
+ .frequent-items-item-namespace {
max-width: 250px;
- overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&:hover {
- .project-item-avatar-container .avatar {
+ .frequent-items-item-avatar-container .avatar {
border-color: $md-area-border;
}
}
- .project-title {
+ .frequent-items-item-title {
font-size: $gl-font-size;
font-weight: 400;
line-height: 16px;
}
- .project-namespace {
+ .frequent-items-item-namespace {
margin-top: 4px;
font-size: 12px;
line-height: 12px;
color: $gl-text-color-secondary;
}
- @media (max-width: $screen-xs-max) {
- .project-item-metadata-container {
+ @include media-breakpoint-down(xs) {
+ .frequent-items-item-metadata-container {
float: none;
}
}
diff --git a/app/assets/stylesheets/framework/emojis.scss b/app/assets/stylesheets/framework/emojis.scss
index 527e7d57c5c..3cde0490371 100644
--- a/app/assets/stylesheets/framework/emojis.scss
+++ b/app/assets/stylesheets/framework/emojis.scss
@@ -4,4 +4,5 @@ gl-emoji {
vertical-align: middle;
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 1.5em;
+ line-height: 0.9;
}
diff --git a/app/assets/stylesheets/framework/feature_highlight.scss b/app/assets/stylesheets/framework/feature_highlight.scss
index 4f26cd015e4..cad915bc86f 100644
--- a/app/assets/stylesheets/framework/feature_highlight.scss
+++ b/app/assets/stylesheets/framework/feature_highlight.scss
@@ -79,7 +79,7 @@
border-right-color: $dropdown-border-color;
}
- .popover-content {
+ .popover-body {
padding: 0;
}
}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index d835d49d8b2..00eac1688f2 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -75,7 +75,7 @@
padding: 8px $gl-padding;
border-bottom: 1px solid $border-color;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
text-align: left;
}
@@ -125,7 +125,7 @@
&.wiki {
padding: $gl-padding;
- @media (min-width: $screen-md-min) {
+ @include media-breakpoint-up(md) {
padding: $gl-padding * 2;
}
}
@@ -322,14 +322,17 @@ span.idiff {
}
.file-title-flex-parent {
- display: flex;
- align-items: center;
- justify-content: space-between;
- background-color: $gray-light;
- border-bottom: 1px solid $border-color;
- padding: 5px $gl-padding;
- margin: 0;
- border-radius: $border-radius-default $border-radius-default 0 0;
+ &,
+ .file-holder & {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ background-color: $gray-light;
+ border-bottom: 1px solid $border-color;
+ padding: 5px $gl-padding;
+ margin: 0;
+ border-radius: $border-radius-default $border-radius-default 0 0;
+ }
.file-header-content {
white-space: nowrap;
@@ -337,6 +340,17 @@ span.idiff {
text-overflow: ellipsis;
padding-right: 30px;
position: relative;
+ width: auto;
+
+ @media (max-width: map-get($grid-breakpoints, sm)-1) {
+ width: 100%;
+ }
+ }
+
+ .file-holder & {
+ .file-actions {
+ position: static;
+ }
}
.btn-clipboard {
@@ -364,7 +378,7 @@ span.idiff {
}
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
display: block;
.file-actions {
@@ -400,3 +414,51 @@ span.idiff {
color: $common-gray-light;
border: 1px solid $common-gray-light;
}
+
+.preview-container {
+ height: 100%;
+ overflow: auto;
+
+ .file-container {
+ background-color: $gray-darker;
+ display: flex;
+ height: 100%;
+ align-items: center;
+ justify-content: center;
+
+ text-align: center;
+
+ .file-content {
+ padding: $gl-padding;
+ max-width: 100%;
+ max-height: 100%;
+
+ img {
+ max-width: 90%;
+ max-height: 70vh;
+ }
+
+ .is-zoomable {
+ cursor: pointer;
+ cursor: zoom-in;
+
+ &.is-zoomed {
+ cursor: pointer;
+ cursor: zoom-out;
+ max-width: none;
+ max-height: none;
+ margin-right: $gl-padding;
+ }
+ }
+ }
+
+ .file-info {
+ font-size: $label-font-size;
+ color: $diff-image-info-color;
+ }
+ }
+
+ .md-previewer {
+ padding: $gl-padding;
+ }
+}
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 621a4adc0cb..5d79610b21e 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -9,19 +9,19 @@
float: right;
margin-right: 0;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
float: none;
}
}
}
.filters-section {
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
display: inline-block;
}
}
-@media (min-width: $screen-sm-min) {
+@include media-breakpoint-up(sm) {
.filter-item:not(:last-child) {
margin-right: 6px;
}
@@ -37,7 +37,7 @@
}
}
-@media (max-width: $screen-xs-max) {
+@include media-breakpoint-down(xs) {
.filter-item {
display: block;
margin: 0 0 10px;
@@ -53,7 +53,7 @@
display: -webkit-flex;
display: flex;
- @media (max-width: $screen-xs-min) {
+ @include media-breakpoint-down(xs) {
-webkit-flex-direction: column;
flex-direction: column;
}
@@ -193,7 +193,7 @@
border: 1px solid $border-color;
background-color: $white-light;
- @media (max-width: $screen-xs-min) {
+ @include media-breakpoint-down(xs) {
-webkit-flex: 1 1 auto;
flex: 1 1 auto;
margin-bottom: 10px;
@@ -224,7 +224,10 @@
.form-control {
position: relative;
min-width: 200px;
- padding: 5px 25px 6px 0;
+ padding-right: 25px;
+ padding-left: 0;
+ height: $input-height;
+ line-height: inherit;
border-color: transparent;
&:focus,
@@ -264,7 +267,7 @@
max-width: 280px;
overflow: auto;
- @media (max-width: $screen-xs-min) {
+ @include media-breakpoint-down(xs) {
width: auto;
left: 0;
right: 0;
@@ -299,6 +302,7 @@
height: 14px;
width: 14px;
vertical-align: middle;
+ margin-bottom: 4px;
}
.dropdown-toggle-text {
@@ -315,7 +319,7 @@
.filtered-search-history-dropdown {
width: 40%;
- @media (max-width: $screen-xs-min) {
+ @include media-breakpoint-down(xs) {
left: 0;
right: 0;
max-width: none;
@@ -353,7 +357,7 @@
}
}
-@media (max-width: $screen-xs-max) {
+@include media-breakpoint-down(xs) {
.issues-details-filters {
padding: 0 0 10px;
background-color: $white-light;
@@ -361,7 +365,7 @@
}
}
-@media (max-width: $screen-xs) {
+@include media-breakpoint-down(sm) {
.filter-dropdown-container {
.dropdown-toggle,
.dropdown,
diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss
index cb2f71b0033..e4bcb92876d 100644
--- a/app/assets/stylesheets/framework/flash.scss
+++ b/app/assets/stylesheets/framework/flash.scss
@@ -8,19 +8,33 @@
.flash-notice {
@extend .alert;
- @extend .alert-info;
+ background-color: $blue-500;
margin: 0;
+
+ &.flash-notice-persistent {
+ background-color: $blue-100;
+ color: $gl-text-color;
+
+ a {
+ color: $gl-link-color;
+
+ &:hover {
+ color: $gl-link-hover-color;
+ text-decoration: none;
+ }
+ }
+ }
}
.flash-warning {
@extend .alert;
- @extend .alert-warning;
+ background-color: $orange-500;
margin: 0;
}
.flash-alert {
@extend .alert;
- @extend .alert-danger;
+ background-color: $red-500;
margin: 0;
.flash-text,
@@ -28,7 +42,7 @@
display: inline-block;
}
- a.flash-action {
+ .flash-action {
margin-left: 5px;
text-decoration: none;
font-weight: $gl-font-weight-normal;
@@ -42,14 +56,16 @@
.flash-success {
@extend .alert;
- @extend .alert-success;
+ background-color: $green-500;
margin: 0;
}
.flash-notice,
.flash-alert,
- .flash-success {
+ .flash-success,
+ .flash-warning {
border-radius: $border-radius-default;
+ color: $white-light;
.container-fluid,
.container-fluid.container-limited {
@@ -72,7 +88,7 @@
}
}
-@media (max-width: $screen-sm-max) {
+@include media-breakpoint-down(sm) {
ul.notes {
.flash-container.timeline-content {
margin-left: 0;
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index 1fb4e3ec761..a22454c24e2 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -36,8 +36,8 @@ label {
}
}
-.control-label {
- @extend .col-sm-2;
+.form-control-label {
+ @extend .col-md-2;
}
.inline-input-group {
@@ -48,21 +48,21 @@ label {
width: 150px;
}
-@media (min-width: $screen-sm-min) {
+@include media-breakpoint-up(sm) {
.custom-form-control {
width: 150px;
}
}
/* Medium devices (desktops, 992px and up) */
-@media (min-width: $screen-md-min) {
+@include media-breakpoint-up(md) {
.custom-form-control {
width: 170px;
}
}
/* Large devices (large desktops, 1200px and up) */
-@media (min-width: $screen-lg-min) {
+@include media-breakpoint-up(lg) {
.custom-form-control {
width: 200px;
}
@@ -72,7 +72,7 @@ label {
margin-left: 0;
margin-right: 0;
- .control-label {
+ .form-control-label {
font-weight: $gl-font-weight-bold;
padding-top: 4px;
}
@@ -83,7 +83,8 @@ label {
font-family: $monospace_font;
}
- .input-group-btn .btn {
+ .input-group-prepend .btn,
+ .input-group-append .btn {
padding: 3px $gl-btn-padding;
background-color: $gray-light;
border: 1px solid $border-color;
@@ -102,10 +103,10 @@ label {
}
}
- @media(max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
padding: 0 $gl-padding;
- .control-label,
+ .form-control-label,
.text-block {
padding-left: 0;
}
@@ -124,7 +125,7 @@ label {
&.input-short {
width: $input-short-width;
- @media (min-width: $screen-md-min) {
+ @include media-breakpoint-up(md) {
width: $input-short-md-width;
}
}
@@ -163,13 +164,13 @@ label {
margin-top: 35px;
}
-.form-group .control-label,
-.form-group .control-label-full-width {
+.form-group .form-control-label,
+.form-group .form-control-label-full-width {
font-weight: $gl-font-weight-normal;
}
.form-control::-webkit-input-placeholder {
- color: $gl-text-color-secondary;
+ color: $placeholder-text-color;
}
.input-group {
@@ -178,17 +179,19 @@ label {
max-width: 180px;
}
- .input-group-addon {
+ .input-group-prepend,
+ .input-group-append {
background-color: $input-group-addon-bg;
}
- .input-group-addon:not(:first-child):not(:last-child) {
+ .input-group-prepend:not(:first-child):not(:last-child),
+ .input-group-append:not(:first-child):not(:last-child) {
border-left: 0;
border-right: 0;
}
}
-.help-block {
+.form-text.text-muted {
margin-bottom: 0;
margin-top: #{$grid-size / 2};
}
@@ -198,6 +201,10 @@ label {
}
.gl-show-field-errors {
+ .form-control {
+ height: 34px;
+ }
+
.gl-field-success-outline {
border: 1px solid $green-600;
@@ -229,10 +236,27 @@ label {
}
}
-@media(max-width: $screen-xs-max) {
+@include media-breakpoint-down(xs) {
.remember-me {
.remember-me-checkbox {
margin-top: 0;
}
}
}
+
+.input-icon-wrapper {
+ position: relative;
+
+ .input-icon-right {
+ position: absolute;
+ right: 0.8em;
+ top: 50%;
+ transform: translateY(-50%);
+ color: $theme-gray-600;
+ }
+}
+
+.input-lg {
+ max-width: 320px;
+ width: 100%;
+}
diff --git a/app/assets/stylesheets/framework/gfm.scss b/app/assets/stylesheets/framework/gfm.scss
index e378e84ca1b..1cf12b1a015 100644
--- a/app/assets/stylesheets/framework/gfm.scss
+++ b/app/assets/stylesheets/framework/gfm.scss
@@ -19,6 +19,7 @@
.gfm-color_chip {
display: inline-block;
+ line-height: 1;
margin: 0 0 2px 4px;
vertical-align: middle;
border-radius: 3px;
diff --git a/app/assets/stylesheets/framework/gitlab_theme.scss b/app/assets/stylesheets/framework/gitlab_theme.scss
index 0bbd6eb27c1..dff6bce370f 100644
--- a/app/assets/stylesheets/framework/gitlab_theme.scss
+++ b/app/assets/stylesheets/framework/gitlab_theme.scss
@@ -3,68 +3,74 @@
*/
@mixin gitlab-theme(
- $color-100,
- $color-200,
- $color-500,
- $color-700,
- $color-800,
- $color-900,
+ $location-badge-color,
+ $search-and-nav-links,
+ $active-tab-border,
+ $border-and-box-shadow,
+ $sidebar-text,
+ $nav-svg-color,
$color-alternate
) {
// Header
.navbar-gitlab {
- background-color: $color-900;
+ background-color: $nav-svg-color;
.navbar-collapse {
- color: $color-200;
+ color: $search-and-nav-links;
}
.container-fluid {
- .navbar-toggle {
- border-left: 1px solid lighten($color-700, 10%);
+ .navbar-toggler {
+ border-left: 1px solid lighten($border-and-box-shadow, 10%);
}
}
.navbar-sub-nav,
.navbar-nav {
> li {
- > a:hover,
- > a:focus {
- background-color: rgba($color-200, 0.2);
+ > a,
+ > button {
+ &:hover,
+ &:focus {
+ background-color: rgba($search-and-nav-links, 0.2);
+ }
}
- &.active > a,
- &.dropdown.open > a {
- color: $color-900;
- background-color: $color-alternate;
+ &.active,
+ &.dropdown.show {
+ > a,
+ > button {
+ color: $nav-svg-color;
+ background-color: $color-alternate;
+ }
}
&.line-separator {
- border-left: 1px solid rgba($color-200, 0.2);
+ border-left: 1px solid rgba($search-and-nav-links, 0.2);
}
}
}
.navbar-sub-nav {
- color: $color-200;
+ color: $search-and-nav-links;
}
.nav {
> li {
- color: $color-200;
+ color: $search-and-nav-links;
> a {
&.header-user-dropdown-toggle {
.header-user-avatar {
- border-color: $color-200;
+ border-color: $search-and-nav-links;
}
}
&:hover,
&:focus {
- @media (min-width: $screen-sm-min) {
- background-color: rgba($color-200, 0.2);
+ @include media-breakpoint-up(sm) {
+ background-color: rgba($search-and-nav-links, 0.2);
}
svg {
@@ -74,13 +80,13 @@
}
&.active > a,
- &.dropdown.open > a {
- color: $color-900;
+ &.dropdown.show > a {
+ color: $nav-svg-color;
background-color: $color-alternate;
&:hover {
svg {
- fill: $color-900;
+ fill: $nav-svg-color;
}
}
}
@@ -88,7 +94,7 @@
.impersonated-user,
.impersonated-user:hover {
svg {
- fill: $color-900;
+ fill: $nav-svg-color;
}
}
}
@@ -99,34 +105,34 @@
> a {
&:hover,
&:focus {
- background-color: rgba($color-200, 0.2);
+ background-color: rgba($search-and-nav-links, 0.2);
}
}
}
.search {
form {
- background-color: rgba($color-200, 0.2);
+ background-color: rgba($search-and-nav-links, 0.2);
&:hover {
- background-color: rgba($color-200, 0.3);
+ background-color: rgba($search-and-nav-links, 0.3);
}
}
.location-badge {
- color: $color-100;
- background-color: rgba($color-200, 0.1);
- border-right: 1px solid $color-800;
+ color: $location-badge-color;
+ background-color: rgba($search-and-nav-links, 0.1);
+ border-right: 1px solid $sidebar-text;
}
.search-input::placeholder {
- color: rgba($color-200, 0.8);
+ color: rgba($search-and-nav-links, 0.8);
}
.search-input-wrap {
.search-icon,
.clear-icon {
- fill: rgba($color-200, 0.8);
+ fill: rgba($search-and-nav-links, 0.8);
}
}
@@ -141,60 +147,58 @@
.search-input-wrap {
.search-icon {
- fill: rgba($color-200, 0.8);
+ fill: rgba($search-and-nav-links, 0.8);
}
}
}
}
- .btn-sign-in {
- background-color: $color-100;
- color: $color-900;
- }
-
// Sidebar
.nav-sidebar li.active {
- box-shadow: inset 4px 0 0 $color-700;
+ box-shadow: inset 4px 0 0 $border-and-box-shadow;
> a {
- color: $color-800;
+ color: $sidebar-text;
}
svg {
- fill: $color-800;
+ fill: $sidebar-text;
}
}
- .sidebar-top-level-items > li.active .badge {
- color: $color-800;
+ .sidebar-top-level-items > li.active .badge.badge-pill {
+ color: $sidebar-text;
}
- .nav-links li.active a {
- border-bottom-color: $color-500;
+ .nav-links li {
+ &.active a,
+ a.active {
+ border-bottom: 2px solid $active-tab-border;
- .badge {
- font-weight: $gl-font-weight-bold;
+ .badge.badge-pill {
+ font-weight: $gl-font-weight-bold;
+ }
}
}
.branch-header-title {
- color: $color-700;
- }
-
- .ide-file-list .file.file-active {
- color: $color-700;
+ color: $border-and-box-shadow;
}
.ide-sidebar-link {
&.active {
- color: $color-700;
- box-shadow: inset 3px 0 $color-700;
+ color: $border-and-box-shadow;
+ box-shadow: inset 3px 0 $border-and-box-shadow;
+
+ &.is-right {
+ box-shadow: inset -3px 0 $border-and-box-shadow;
+ }
}
}
}
body {
- &.ui_indigo {
+ &.ui-indigo {
@include gitlab-theme(
$indigo-100,
$indigo-200,
@@ -206,19 +210,19 @@ body {
);
}
- &.ui_dark {
+ &.ui-light-indigo {
@include gitlab-theme(
- $theme-gray-100,
- $theme-gray-200,
- $theme-gray-500,
- $theme-gray-700,
- $theme-gray-800,
- $theme-gray-900,
+ $indigo-100,
+ $indigo-200,
+ $indigo-500,
+ $indigo-500,
+ $indigo-700,
+ $indigo-700,
$white-light
);
}
- &.ui_blue {
+ &.ui-blue {
@include gitlab-theme(
$theme-blue-100,
$theme-blue-200,
@@ -230,7 +234,19 @@ body {
);
}
- &.ui_green {
+ &.ui-light-blue {
+ @include gitlab-theme(
+ $theme-light-blue-100,
+ $theme-light-blue-200,
+ $theme-light-blue-500,
+ $theme-light-blue-500,
+ $theme-light-blue-700,
+ $theme-light-blue-700,
+ $white-light
+ );
+ }
+
+ &.ui-green {
@include gitlab-theme(
$theme-green-100,
$theme-green-200,
@@ -242,7 +258,55 @@ body {
);
}
- &.ui_light {
+ &.ui-light-green {
+ @include gitlab-theme(
+ $theme-green-100,
+ $theme-green-200,
+ $theme-green-500,
+ $theme-green-500,
+ $theme-light-green-700,
+ $theme-light-green-700,
+ $white-light
+ );
+ }
+
+ &.ui-red {
+ @include gitlab-theme(
+ $theme-red-100,
+ $theme-red-200,
+ $theme-red-500,
+ $theme-red-700,
+ $theme-red-800,
+ $theme-red-900,
+ $white-light
+ );
+ }
+
+ &.ui-light-red {
+ @include gitlab-theme(
+ $theme-light-red-100,
+ $theme-light-red-200,
+ $theme-light-red-500,
+ $theme-light-red-500,
+ $theme-light-red-700,
+ $theme-light-red-700,
+ $white-light
+ );
+ }
+
+ &.ui-dark {
+ @include gitlab-theme(
+ $theme-gray-100,
+ $theme-gray-200,
+ $theme-gray-500,
+ $theme-gray-700,
+ $theme-gray-800,
+ $theme-gray-900,
+ $white-light
+ );
+ }
+
+ &.ui-light {
@include gitlab-theme(
$theme-gray-900,
$theme-gray-700,
@@ -277,8 +341,8 @@ body {
}
.container-fluid {
- .navbar-toggle,
- .navbar-toggle:hover {
+ .navbar-toggler,
+ .navbar-toggler:hover {
color: $theme-gray-700;
border-left: 1px solid $theme-gray-200;
}
@@ -328,7 +392,7 @@ body {
}
}
- .sidebar-top-level-items > li.active .badge {
+ .sidebar-top-level-items > li.active .badge.badge-pill {
color: $theme-gray-900;
}
}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 0136af76a13..2097bcebf69 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -37,6 +37,7 @@
}
.header-content {
+ width: 100%;
display: -webkit-flex;
display: flex;
justify-content: space-between;
@@ -91,7 +92,7 @@
border-radius: $border-radius-default;
.tanuki-logo {
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
margin-right: 8px;
}
}
@@ -110,7 +111,7 @@
}
&.menu-expanded {
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
.title-container {
display: none;
}
@@ -133,13 +134,15 @@
border-top: 0;
padding: 0;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
flex: 1 1 auto;
}
.nav {
- > li:not(.hidden-xs) a {
- @media (max-width: $screen-xs-max) {
+ flex-wrap: nowrap;
+
+ > li:not(.d-none) a {
+ @include media-breakpoint-down(xs) {
margin-left: 0;
min-width: 100%;
}
@@ -156,12 +159,13 @@
}
}
- .navbar-toggle {
+ .navbar-toggler {
+ position: relative;
right: -10px;
border-radius: 0;
min-width: 45px;
padding: 0;
- margin-right: -7px;
+ margin: $gl-padding-8 -7px $gl-padding-8 0;
font-size: 14px;
text-align: center;
color: currentColor;
@@ -181,14 +185,15 @@
}
.navbar-nav {
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
display: -webkit-flex;
display: flex;
padding-right: 10px;
+ flex-direction: row;
}
li {
- .badge {
+ .badge.badge-pill {
box-shadow: none;
font-weight: $gl-font-weight-bold;
}
@@ -197,7 +202,7 @@
.nav > li {
&.header-user {
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
padding-left: 10px;
}
}
@@ -208,7 +213,7 @@
padding: 6px 8px;
height: 32px;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
padding: 0;
}
@@ -264,14 +269,8 @@
.navbar-sub-nav,
.navbar-nav {
> li {
- > a:hover,
- > a:focus {
- text-decoration: none;
- outline: 0;
- color: $white-light;
- }
-
- > a {
+ > a,
+ > button {
display: -webkit-flex;
display: flex;
align-items: center;
@@ -283,12 +282,28 @@
border-radius: $border-radius-default;
height: 32px;
font-weight: $gl-font-weight-bold;
+
+ &:hover,
+ &:focus {
+ text-decoration: none;
+ outline: 0;
+ color: $white-light;
+ }
+ }
+
+ > button {
+ background: transparent;
+ border: 0;
}
&.line-separator {
margin: 8px;
}
}
+
+ .dropdown-menu {
+ position: absolute;
+ }
}
.navbar-sub-nav {
@@ -296,19 +311,13 @@
display: flex;
margin: 0 0 0 6px;
- .projects-dropdown-menu {
- padding: 0;
- overflow-y: initial;
- max-height: initial;
- }
-
.dropdown-chevron {
position: relative;
top: -1px;
font-size: 10px;
}
- .project-item-select-holder {
+ .frequent-items-item-select-holder {
display: inline;
}
@@ -324,8 +333,8 @@
fill: currentColor;
}
-.header-user .dropdown-menu-nav,
-.header-new .dropdown-menu-nav {
+.header-user .dropdown-menu,
+.header-new .dropdown-menu {
margin-top: $dropdown-vertical-offset;
}
@@ -378,7 +387,7 @@
margin-bottom: 0;
line-height: 16px;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
flex-wrap: wrap;
}
@@ -410,7 +419,7 @@
.breadcrumb-item-text {
text-decoration: inherit;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
@include str-truncated(128px);
}
}
@@ -442,17 +451,23 @@
}
.btn-sign-in {
- margin-top: 3px;
+ background-color: $indigo-100;
+ color: $indigo-900;
font-weight: $gl-font-weight-bold;
+ line-height: 18px;
&:hover {
background-color: $white-light;
}
+
+ @include media-breakpoint-down(xs) {
+ margin-top: $gl-padding-4;
+ }
}
.navbar-nav {
li {
- .badge {
+ .badge.badge-pill {
position: inherit;
font-weight: $gl-font-weight-normal;
margin-left: -6px;
@@ -478,7 +493,7 @@
}
}
-@media (max-width: $screen-xs-max) {
+@include media-breakpoint-down(xs) {
.navbar-gitlab .container-fluid {
font-size: 18px;
@@ -493,7 +508,7 @@
margin-left: -8px;
margin-right: -10px;
- .nav > li:not(.hidden-xs) {
+ .nav > li:not(.d-none) {
display: table-cell !important;
width: 25%;
@@ -514,9 +529,9 @@
}
.header-user {
- .dropdown-menu-nav {
+ .dropdown-menu {
width: auto;
- min-width: 160px;
+ min-width: unset;
margin-top: 4px;
color: $gl-text-color;
left: auto;
@@ -528,6 +543,10 @@
display: block;
}
}
+
+ svg {
+ vertical-align: text-top;
+ }
}
}
@@ -547,7 +566,7 @@
background: $white-light;
border-bottom: 1px solid $white-normal;
- .center-logo {
+ .mx-auto {
margin: 8px 0;
text-align: center;
diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss
index 30314f3d6cb..d1f7ff4438b 100644
--- a/app/assets/stylesheets/framework/icons.scss
+++ b/app/assets/stylesheets/framework/icons.scss
@@ -3,12 +3,20 @@
svg {
fill: $green-500;
}
+
+ &.add-border {
+ @include borderless-status-icon($green-500);
+ }
}
.ci-status-icon-failed {
svg {
fill: $gl-danger;
}
+
+ &.add-border {
+ @include borderless-status-icon($red-500);
+ }
}
.ci-status-icon-pending,
@@ -17,12 +25,20 @@
svg {
fill: $orange-500;
}
+
+ &.add-border {
+ @include borderless-status-icon($orange-500);
+ }
}
.ci-status-icon-running {
svg {
fill: $blue-400;
}
+
+ &.add-border {
+ @include borderless-status-icon($blue-400);
+ }
}
.ci-status-icon-canceled,
@@ -30,6 +46,10 @@
svg {
fill: $gl-text-color;
}
+
+ &.add-border {
+ @include borderless-status-icon($gl-text-color);
+ }
}
.ci-status-icon-created,
@@ -38,6 +58,10 @@
svg {
fill: $gray-darkest;
}
+
+ &.add-border {
+ @include borderless-status-icon($gray-darkest);
+ }
}
.ci-status-icon-manual {
diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss
index d8c57a0e2d9..86de88729ee 100644
--- a/app/assets/stylesheets/framework/issue_box.scss
+++ b/app/assets/stylesheets/framework/issue_box.scss
@@ -11,7 +11,7 @@
padding: 5px 11px;
margin-top: 4px;
/* Small devices (tablets, 768px and up) */
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
padding: 0 $gl-btn-padding;
margin-top: 5px;
}
@@ -45,4 +45,9 @@
&.status-box-upcoming {
background: $gl-text-color-secondary;
}
+
+ &.status-box-milestone {
+ color: $gl-text-color;
+ background: $gray-darker;
+ }
}
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index d107422e517..52b5f059f20 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -15,7 +15,7 @@ body {
background-color: $white-light !important;
}
- &.card-content {
+ &.board-card-content {
background-color: $gray-darker;
.content-wrapper {
@@ -54,10 +54,6 @@ body {
&.limit-container-width {
max-width: $limited-layout-width;
}
-
- &.limit-container-width-sm {
- max-width: $limited-layout-width-sm;
- }
}
.alert-wrapper {
@@ -74,7 +70,7 @@ body {
}
/* Center alert text and alert action links on smaller screens */
- @media (max-width: $screen-sm-max) {
+ @include media-breakpoint-down(sm) {
.alert {
text-align: center;
}
@@ -115,9 +111,3 @@ body {
.with-performance-bar .layout-page {
margin-top: $header-height + $performance-bar-height;
}
-
-.vertical-center {
- min-height: 100vh;
- display: flex;
- align-items: center;
-}
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index 45517416e93..d54490c87c6 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -2,7 +2,7 @@
* Well styled list
*
*/
-.well-list {
+.hover-list {
position: relative;
margin: 0;
padding: 0;
@@ -72,7 +72,7 @@
}
}
- .well-title {
+ .card.card-body-title {
font-size: $list-font-size;
line-height: 18px;
}
@@ -159,7 +159,7 @@ ul.content-list {
&:last-child {
margin-right: 0;
- @media(max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
margin: 0 auto;
}
}
@@ -173,7 +173,7 @@ ul.content-list {
.member-controls {
float: none;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
float: right;
}
}
@@ -228,7 +228,7 @@ ul.content-list {
}
}
- .label-default {
+ .badge-secondary {
color: $gl-text-color-secondary;
}
@@ -237,7 +237,7 @@ ul.content-list {
}
}
-.panel > .content-list > li {
+.card > .content-list > li {
padding: $gl-padding-top $gl-padding;
}
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index 7b5d1c2cf8b..7290a174668 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -61,10 +61,6 @@
padding-top: 0;
line-height: 19px;
- &.btn.btn-xs {
- padding: 2px 5px;
- }
-
&:focus {
margin-top: -10px;
padding-top: 10px;
@@ -74,7 +70,7 @@
}
.md-header-tab {
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
flex: 1;
width: 100%;
border-bottom: 1px solid $border-color;
@@ -90,7 +86,7 @@
&.active {
display: block;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
flex: none;
display: flex;
justify-content: center;
@@ -192,7 +188,7 @@
margin-left: $gl-padding;
margin-right: -5px;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
margin-left: 0;
margin-right: 0;
}
@@ -268,7 +264,7 @@
}
}
-@media (max-width: $screen-xs-max) {
+@include media-breakpoint-down(xs) {
.atwho-view-ul {
width: 350px;
}
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 0ea0b65b95f..76ebfc22ef7 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -19,14 +19,23 @@
width: auto;
display: inline-block;
overflow-x: auto;
- border-left: 0;
- border-right: 0;
- border-bottom: 0;
+ border: 0;
+ border-color: $md-area-border;
@supports(width: fit-content) {
display: block;
width: fit-content;
}
+
+ tr {
+ th {
+ border-bottom: solid 2px $md-area-border;
+ }
+
+ td {
+ border-color: $md-area-border;
+ }
+ }
}
/*
@@ -177,6 +186,7 @@
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
display: flex;
+ flex-wrap: nowrap;
&::-webkit-scrollbar {
display: none;
@@ -222,3 +232,10 @@
word-break: break-word;
max-width: 100%;
}
+
+@mixin borderless-status-icon($color) {
+ svg {
+ border: 1px solid $color;
+ border-radius: 50%;
+ }
+}
diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss
index 9e03bb98b8e..6244fb86fea 100644
--- a/app/assets/stylesheets/framework/mobile.scss
+++ b/app/assets/stylesheets/framework/mobile.scss
@@ -1,5 +1,5 @@
/** Common mobile (screen XS, SM) styles **/
-@media (max-width: $screen-xs-max) {
+@include media-breakpoint-down(xs) {
.container .content {
margin-top: 20px;
}
@@ -14,7 +14,7 @@
font-size: 12px;
margin-right: 3px;
- .badge {
+ .badge.badge-pill {
display: none;
}
}
@@ -86,7 +86,7 @@
}
}
-@media (max-width: $screen-sm-max) {
+@include media-breakpoint-down(sm) {
.issues-filters {
.milestone-filter {
display: none;
diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss
index eb789cc64b0..ffb40166c15 100644
--- a/app/assets/stylesheets/framework/modal.scss
+++ b/app/assets/stylesheets/framework/modal.scss
@@ -1,6 +1,9 @@
+.modal-xl {
+ max-width: 98%;
+}
+
.modal-header {
background-color: $modal-body-bg;
- padding: #{3 * $grid-size} #{2 * $grid-size};
.page-title,
.modal-title {
@@ -46,7 +49,7 @@
margin-left: $grid-size;
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
flex-direction: column;
.btn + .btn {
@@ -55,7 +58,7 @@
}
}
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
.btn:first-of-type {
margin-left: auto;
}
@@ -74,17 +77,11 @@ body.modal-open {
}
}
-@media (min-width: $screen-lg-min) {
- .modal-full {
- width: 98%;
- }
-}
-
.modal {
background-color: $black-transparent;
z-index: 2100;
- @media (min-width: $screen-md-min) {
+ @include media-breakpoint-up(md) {
.modal-dialog {
margin: 30px auto;
}
diff --git a/app/assets/stylesheets/framework/page_header.scss b/app/assets/stylesheets/framework/page_header.scss
index 0c879f40930..660e3dcac8d 100644
--- a/app/assets/stylesheets/framework/page_header.scss
+++ b/app/assets/stylesheets/framework/page_header.scss
@@ -3,7 +3,7 @@
padding: 10px 0;
margin-bottom: 0;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
display: flex;
align-items: center;
@@ -19,7 +19,7 @@
margin-right: 3px;
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
.btn {
width: 100%;
margin-top: 10px;
@@ -35,7 +35,7 @@
@extend .avatar-inline;
margin-left: 0;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
margin-left: 4px;
}
}
diff --git a/app/assets/stylesheets/framework/pagination.scss b/app/assets/stylesheets/framework/pagination.scss
index c3ec9db0f07..61d02511ff4 100644
--- a/app/assets/stylesheets/framework/pagination.scss
+++ b/app/assets/stylesheets/framework/pagination.scss
@@ -1,90 +1,14 @@
.gl-pagination {
- text-align: center;
- border-top: 1px solid $border-color;
- margin: 0;
- margin-top: 0;
-
- .pagination {
- padding: 0;
-
- a {
- cursor: pointer;
- }
-
- .separator,
- .separator:hover {
- a {
- cursor: default;
- background-color: $gray-light;
- padding: $gl-vert-padding;
- }
- }
+ a {
+ color: inherit;
+ text-decoration: none;
}
-
-
- .gap,
- .gap:hover {
- background-color: $gray-light;
- padding: $gl-vert-padding;
- cursor: default;
- }
-}
-
-.panel > .gl-pagination {
- margin: 0;
}
-/**
- * Extra-small screen pagination.
- */
-@media (max-width: 320px) {
- .gl-pagination {
- .first,
- .last {
- display: none;
- }
-
- .page {
- display: none;
-
- &.active {
- display: inline;
- }
- }
- }
-}
-
-/**
- * Small screen pagination
- */
-@media (max-width: $screen-xs-min) {
- .gl-pagination {
- .pagination li a {
- padding: 6px 10px;
- }
-
- .page {
- display: none;
-
- &.active {
- display: inline;
- }
- }
- }
-}
-
-/**
- * Medium screen pagination
- */
-@media (min-width: $screen-xs-min) and (max-width: $screen-md-max) {
- .gl-pagination {
- .page {
- display: none;
-
- &.active,
- &.sibling {
- display: inline;
- }
+.page-item {
+ &.active {
+ .page-link {
+ z-index: 3;
}
}
}
diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss
index e8d69e62194..a8e28104a94 100644
--- a/app/assets/stylesheets/framework/panels.scss
+++ b/app/assets/stylesheets/framework/panels.scss
@@ -1,14 +1,14 @@
-.panel {
+.card {
margin-bottom: $gl-padding;
}
-.panel-slim {
- @extend .panel;
+.card-slim {
+ @extend .card;
margin-bottom: $gl-vert-padding;
}
-.panel-heading {
+.card-header {
padding: $gl-vert-padding $gl-padding;
line-height: 36px;
@@ -21,7 +21,7 @@
line-height: 20px;
}
- .badge {
+ .badge.badge-pill {
margin-top: -2px;
margin-left: 5px;
}
@@ -41,12 +41,13 @@
}
}
-.panel-empty-heading {
+.card-empty-heading {
border-bottom: 0;
}
-.panel-body {
+.card-body {
padding: $gl-padding;
+ background-color: $white-light;
.form-actions {
margin: -$gl-padding;
@@ -54,7 +55,7 @@
}
}
-.panel-title {
+.card-title {
font-size: inherit;
line-height: inherit;
}
diff --git a/app/assets/stylesheets/framework/responsive_tables.scss b/app/assets/stylesheets/framework/responsive_tables.scss
index 34fccf6f0a4..764bebd82c6 100644
--- a/app/assets/stylesheets/framework/responsive_tables.scss
+++ b/app/assets/stylesheets/framework/responsive_tables.scss
@@ -6,7 +6,7 @@
.gl-responsive-table-row-layout {
width: 100%;
- @media (min-width: $screen-md-min) {
+ @include media-breakpoint-up(md) {
display: flex;
align-items: center;
@@ -21,7 +21,7 @@
margin-top: 10px;
border: 1px solid $border-color;
- @media (min-width: $screen-md-min) {
+ @include media-breakpoint-up(md) {
margin: 0;
padding: $gl-padding 0;
border: 0;
@@ -44,13 +44,13 @@
&.section-#{$width} {
flex: 0 0 #{$width + '%'};
- @media (min-width: $screen-md-min) {
+ @include media-breakpoint-up(md) {
max-width: #{$width + '%'};
}
}
}
- @media (max-width: $screen-sm-max) {
+ @include media-breakpoint-down(sm) {
display: flex;
align-self: stretch;
padding: 10px;
@@ -65,7 +65,7 @@
&.section-wrap {
white-space: normal;
- @media (max-width: $screen-sm-max) {
+ @include media-breakpoint-down(sm) {
flex-wrap: wrap;
}
}
@@ -76,11 +76,11 @@
}
.table-button-footer {
- @media (min-width: $screen-md-min) {
+ @include media-breakpoint-up(md) {
text-align: right;
}
- @media (max-width: $screen-sm-max) {
+ @include media-breakpoint-down(sm) {
display: block;
align-self: stretch;
min-height: 0;
@@ -122,7 +122,7 @@
.table-row-header {
font-size: 13px;
- @media (max-width: $screen-sm-max) {
+ @include media-breakpoint-down(sm) {
display: none;
}
}
@@ -132,13 +132,13 @@
color: $gl-text-color-secondary;
text-align: left;
- @media (min-width: $screen-md-min) {
+ @include media-breakpoint-up(md) {
display: none;
}
}
.table-mobile-content {
- @media (max-width: $screen-sm-max) {
+ @include media-breakpoint-down(sm) {
@include flex-max-width(60);
text-align: right;
}
@@ -154,7 +154,7 @@
overflow: hidden;
text-overflow: ellipsis;
- @media (min-width: $screen-md-min) {
+ @include media-breakpoint-up(md) {
flex: 0 0 90%;
}
diff --git a/app/assets/stylesheets/framework/secondary_navigation_elements.scss b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
index 937638d50e8..9dbb04e5443 100644
--- a/app/assets/stylesheets/framework/secondary_navigation_elements.scss
+++ b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
@@ -27,17 +27,18 @@
color: $black;
border-bottom: 2px solid $gray-darkest;
- .badge {
+ .badge.badge-pill {
color: $black;
}
}
}
- &.active a {
+ &.active a,
+ a.active {
color: $black;
font-weight: $gl-font-weight-bold;
- .badge {
+ .badge.badge-pill {
color: $black;
}
}
@@ -56,7 +57,7 @@
white-space: normal;
/* Small devices (phones, tablets, 768px and lower) */
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
width: 100%;
}
}
@@ -80,7 +81,7 @@
}
/* Small devices (phones, tablets, 768px and lower) */
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
width: 100%;
&.mobile-separator {
@@ -124,13 +125,13 @@
position: relative;
/* Medium devices (desktops, 992px and up) */
- @media (min-width: $screen-md-min) { width: 200px; }
+ @include media-breakpoint-up(md) { width: 200px; }
/* Large devices (large desktops, 1200px and up) */
- @media (min-width: $screen-lg-min) { width: 250px; }
+ @include media-breakpoint-up(lg) { width: 250px; }
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
padding-bottom: 0;
width: 100%;
@@ -172,7 +173,7 @@
.nav-controls {
width: auto;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
width: 100%;
}
}
@@ -192,11 +193,11 @@
width: 100%;
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
flex-flow: row wrap;
.nav-controls {
- $controls-margin: $btn-xs-side-margin - 2px;
+ $controls-margin: $btn-margin-5 - 2px;
flex: 0 0 100%;
&.controls-flex {
@@ -229,6 +230,8 @@
}
.scrolling-tabs-container {
+ position: relative;
+
.merge-request-tabs-container & {
overflow: hidden;
}
@@ -344,7 +347,7 @@
.empty-state .project-item-select-holder.btn-group {
float: none;
- display: inline-block;
+ justify-content: center;
.btn {
// overrides styles applied to plain `.empty-state .btn`
@@ -352,7 +355,7 @@
max-width: 300px;
width: auto;
- @media(max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
max-width: 250px;
}
}
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 64fff7463d2..8c716400913 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -31,7 +31,7 @@
.right-sidebar-collapsed {
padding-right: 0;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
&:not(.wiki-sidebar):not(.build-sidebar):not(.issuable-bulk-update-sidebar) .content-wrapper {
padding-right: $gutter_collapsed_width;
}
@@ -65,13 +65,13 @@
display: inline-flex;
}
- @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
+ @include media-breakpoint-only(sm) {
&:not(.wiki-sidebar):not(.build-sidebar):not(.issuable-bulk-update-sidebar) .content-wrapper {
padding-right: $gutter_collapsed_width;
}
}
- @media (min-width: $screen-md-min) {
+ @include media-breakpoint-up(md) {
.content-wrapper {
padding-right: $gutter_width;
}
diff --git a/app/assets/stylesheets/framework/snippets.scss b/app/assets/stylesheets/framework/snippets.scss
index 430633bb01b..7152ef9bcfd 100644
--- a/app/assets/stylesheets/framework/snippets.scss
+++ b/app/assets/stylesheets/framework/snippets.scss
@@ -40,7 +40,7 @@
}
.snippet-actions {
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
float: right;
}
}
@@ -49,26 +49,11 @@
margin-top: 15px;
}
-.snippet-embed-input {
- height: 35px;
-}
-
.embed-snippet {
padding-right: 0;
padding-top: $gl-padding;
- .form-control {
- cursor: auto;
- width: 101%;
- margin-left: -1px;
- }
-
.embed-toggle-list li button {
padding: 8px 40px;
}
-
- .embed-toggle,
- .snippet-clipboard-btn {
- height: 35px;
- }
}
diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss
index 5bde96caf42..339388392df 100644
--- a/app/assets/stylesheets/framework/tables.scss
+++ b/app/assets/stylesheets/framework/tables.scss
@@ -1,5 +1,6 @@
.table-holder {
margin: 0;
+ overflow: auto;
}
table {
@@ -38,6 +39,11 @@ table {
&.wide {
width: 55%;
}
+
+ &.table-th-transparent {
+ background: none;
+ color: $gl-text-color-secondary;
+ }
}
td {
@@ -45,10 +51,92 @@ table {
}
}
}
+
+ &.responsive-table {
+ @include media-breakpoint-down(sm) {
+ thead {
+ display: none;
+ }
+
+ &,
+ tbody,
+ td {
+ display: block;
+ }
+
+ td {
+ color: $gl-text-color-secondary;
+ }
+
+ tbody td.responsive-table-cell {
+ padding: $gl-padding 0;
+ width: 100%;
+ display: flex;
+ text-align: right;
+ align-items: center;
+ justify-content: space-between;
+
+ &[data-column]::before {
+ content: attr(data-column);
+ display: block;
+ text-align: left;
+ padding-right: $gl-padding;
+ color: $gl-text-color-secondary;
+ }
+
+ &:not([data-column]) {
+ flex-direction: row-reverse;
+ }
+ }
+
+ tr.responsive-table-border-start,
+ tr.responsive-table-border-end {
+ display: block;
+ border: solid $gl-text-color-quaternary;
+ padding-left: 0;
+ padding-right: 0;
+
+ > td {
+ border-color: $gl-text-color-quaternary;
+
+ &,
+ &:last-child {
+ padding-left: $gl-padding;
+ padding-right: $gl-padding;
+ }
+ }
+ }
+
+ tr.responsive-table-border-start {
+ border-width: 1px 1px 0;
+ border-radius: $border-radius-default $border-radius-default 0 0;
+ padding-top: 0;
+ padding-bottom: 0;
+
+ > td:first-child {
+ border-top: 0; // always have the <table> top border
+ }
+
+ > td:last-child {
+ border-bottom: 1px solid $gl-text-color-quaternary;
+ }
+ }
+
+ tr.responsive-table-border-end {
+ border-width: 0 1px 1px;
+ border-radius: 0 0 $border-radius-default $border-radius-default;
+ margin-bottom: 2 * $gl-padding;
+
+ > :last-child {
+ border-bottom: 0;
+ }
+ }
+ }
+ }
}
-.responsive-table {
- @media (max-width: $screen-sm-max) {
+.responsive-table:not(table) {
+ @include media-breakpoint-down(sm) {
th {
width: 100%;
}
diff --git a/app/assets/stylesheets/framework/tabs.scss b/app/assets/stylesheets/framework/tabs.scss
index c8ba14b7066..6b60149fbbb 100644
--- a/app/assets/stylesheets/framework/tabs.scss
+++ b/app/assets/stylesheets/framework/tabs.scss
@@ -1,6 +1,7 @@
.gitlab-tabs {
background: $gray-light;
border: 1px solid $border-color;
+ flex-wrap: nowrap;
li {
width: 50%;
diff --git a/app/assets/stylesheets/framework/terms.scss b/app/assets/stylesheets/framework/terms.scss
index 744fd0ff796..7cda674e5c8 100644
--- a/app/assets/stylesheets/framework/terms.scss
+++ b/app/assets/stylesheets/framework/terms.scss
@@ -11,15 +11,15 @@
padding-top: $gl-padding;
}
- .panel {
- .panel-heading {
+ .card {
+ .card-header {
display: -webkit-flex;
display: flex;
align-items: center;
justify-content: space-between;
line-height: $line-height-base;
- .title {
+ .card-title {
display: flex;
align-items: center;
@@ -34,6 +34,8 @@
.navbar-collapse {
padding-right: 0;
+ flex-grow: 0;
+ flex-basis: auto;
.navbar-nav {
margin: 0;
diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index 373f35e71d8..dfb145debe7 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -4,7 +4,7 @@
padding: 0;
&::before {
- @include notes-media('max', $screen-xs-min) {
+ @include notes-media('max', map-get($grid-breakpoints, sm)) {
background: none;
}
}
@@ -34,7 +34,7 @@
.timeline-entry-inner {
position: relative;
- @include notes-media('max', $screen-xs-min) {
+ @include notes-media('max', map-get($grid-breakpoints, sm)) {
.timeline-icon {
display: none;
}
diff --git a/app/assets/stylesheets/framework/toggle.scss b/app/assets/stylesheets/framework/toggle.scss
index 0cd83df218f..20394cc1e52 100644
--- a/app/assets/stylesheets/framework/toggle.scss
+++ b/app/assets/stylesheets/framework/toggle.scss
@@ -42,6 +42,10 @@
background: none;
}
+ &:focus {
+ outline: none;
+ }
+
.toggle-icon {
position: relative;
display: block;
@@ -121,7 +125,7 @@
cursor: not-allowed;
}
- @media (max-width: $screen-xs-min) {
+ @include media-breakpoint-down(xs) {
width: 50px;
&::before,
diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss
deleted file mode 100644
index 1c6e2bf3074..00000000000
--- a/app/assets/stylesheets/framework/tw_bootstrap.scss
+++ /dev/null
@@ -1,201 +0,0 @@
-/*
- * Twitter bootstrap with GitLab customizations/additions
- *
- */
-
-// Core variables and mixins
-@import "bootstrap/variables";
-@import "bootstrap/mixins";
-
-// Reset
-@import "bootstrap/normalize";
-@import "bootstrap/print";
-
-// Core CSS
-@import "bootstrap/scaffolding";
-@import "bootstrap/type";
-@import "bootstrap/code";
-@import "bootstrap/grid";
-@import "bootstrap/tables";
-@import "bootstrap/forms";
-@import "bootstrap/buttons";
-
-// Components
-@import "bootstrap/component-animations";
-// @import "bootstrap/dropdowns";
-@import "bootstrap/button-groups";
-@import "bootstrap/input-groups";
-@import "bootstrap/navs";
-@import "bootstrap/navbar";
-@import "bootstrap/breadcrumbs";
-@import "bootstrap/pagination";
-@import "bootstrap/pager";
-@import "bootstrap/labels";
-@import "bootstrap/badges";
-@import "bootstrap/alerts";
-@import "bootstrap/progress-bars";
-@import "bootstrap/list-group";
-@import "bootstrap/wells";
-@import "bootstrap/close";
-@import "bootstrap/panels";
-
-// Components w/ JavaScript
-@import "bootstrap/modals";
-@import "bootstrap/tooltip";
-@import "bootstrap/popovers";
-
-// Utility classes
-.clearfix {
- @include clearfix();
-}
-
-.center-block {
- @include center-block();
-}
-
-.pull-right {
- float: right !important;
-}
-
-.pull-left {
- float: left !important;
-}
-
-.hide {
- display: none;
-}
-
-.show {
- display: block !important;
-}
-
-.invisible {
- visibility: hidden;
-}
-
-.text-hide {
- @include text-hide();
-}
-
-.hidden {
- display: none !important;
- visibility: hidden !important;
-}
-
-.affix {
- position: fixed;
-}
-
-/*
- * Fix <summary> elements on firefox
- * See https://github.com/necolas/normalize.css/issues/640
- * and https://github.com/twbs/bootstrap/issues/21060
- *
- */
-summary {
- display: list-item;
-}
-
-@import "bootstrap/responsive-utilities";
-
-// Labels
-.label {
- padding: 4px 5px;
- font-size: 12px;
- font-style: normal;
- font-weight: $gl-font-weight-normal;
- display: inline-block;
-
- &.label-gray {
- background-color: $label-gray-bg;
- color: $gl-text-color;
- text-shadow: none;
- }
-
- &.label-inverse {
- background-color: $label-inverse-bg;
- }
-}
-
-
-/**
- * fix to keep tooltips position in top navigation bar
- *
- */
-.navbar .nav > li {
- position: relative;
- white-space: nowrap;
-}
-
-/**
- * Add some extra stuff to panels
- *
- */
-
-.panel {
- box-shadow: none;
-
- .panel-body {
- form,
- pre {
- margin: 0;
- }
-
- .form-actions {
- margin: -15px;
- margin-top: 18px;
- }
- }
-
- .panel-footer {
- .pagination {
- margin: 0;
- }
-
- .btn {
- min-width: 124px;
- }
-
- .btn-clipboard {
- min-width: 0;
- }
- }
-
- &.panel-small {
- .panel-heading {
- padding: 6px 15px;
- font-size: 13px;
- font-weight: $gl-font-weight-normal;
-
- a {
- color: $panel-heading-link-color;
- }
- }
- }
-
- &.panel-without-border {
- border: 0;
- }
-
- &.panel-without-margin {
- margin: 0;
- }
-}
-
-.panel-succes .panel-heading,
-.panel-info .panel-heading,
-.panel-danger .panel-heading,
-.panel-warning .panel-heading,
-.panel-primary .panel-heading,
-.alert {
- a:not(.btn) {
- @extend .alert-link;
- color: $white-light;
- text-decoration: underline;
- }
-}
-
-// 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
deleted file mode 100644
index d04e555769b..00000000000
--- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
+++ /dev/null
@@ -1,199 +0,0 @@
-// Override Bootstrap variables here (defaults from bootstrap-sass v3.3.3):
-// For all variables see https://github.com/twbs/bootstrap-sass/blob/master/templates/project/_bootstrap-variables.sass
-//
-// Variables
-// --------------------------------------------------
-
-
-//== Colors
-//
-//## Gray and brand colors for use across Bootstrap.
-
-// $gray-base: #000
-// $gray-darker: lighten($gray-base, 13.5%) // #222
-// $gray-dark: lighten($gray-base, 20%) // #333
-// $gray: lighten($gray-base, 33.5%) // #555
-// $gray-light: lighten($gray-base, 46.7%) // #777
-// $gray-lighter: lighten($gray-base, 93.5%) // #eee
-
-$brand-primary: $gl-primary;
-$brand-success: $gl-success;
-$brand-info: $gl-info;
-$brand-warning: $gl-warning;
-$brand-danger: $gl-danger;
-
-$border-radius-base: 3px !default;
-$border-radius-large: 3px !default;
-$border-radius-small: 3px !default;
-
-
-//== Scaffolding
-//
-$text-color: $gl-text-color;
-$link-color: $gl-link-color;
-$link-hover-color: $gl-link-hover-color;
-
-
-//== Typography
-//
-//## Font, line-height, and color for body text, headings, and more.
-
-$font-family-sans-serif: $regular_font;
-$font-family-monospace: $monospace_font;
-$font-size-base: $gl-font-size;
-
-
-//== Components
-//
-//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
-
-$padding-base-vertical: $gl-vert-padding;
-$padding-base-horizontal: $gl-padding;
-$component-active-color: $white-light;
-$component-active-bg: $brand-info;
-
-//== Forms
-//
-//##
-
-$input-color: $text-color;
-$input-border: $border-color;
-$input-border-focus: $focus-border-color;
-$legend-color: $text-color;
-
-
-//== Pagination
-//
-//##
-
-$pagination-color: $gl-text-color;
-$pagination-bg: $white-light;
-$pagination-border: $border-color;
-
-$pagination-hover-color: $gl-text-color;
-$pagination-hover-bg: $row-hover;
-$pagination-hover-border: $border-color;
-
-$pagination-active-color: $white-light;
-$pagination-active-bg: $gl-link-color;
-$pagination-active-border: $gl-link-color;
-
-$pagination-disabled-color: #cdcdcd;
-$pagination-disabled-bg: $gray-light;
-$pagination-disabled-border: $border-color;
-
-
-//== Form states and alerts
-//
-//## Define colors for form feedback states and, by default, alerts.
-
-$state-success-text: $white-light;
-$state-success-bg: $brand-success;
-$state-success-border: $brand-success;
-
-$state-info-text: $white-light;
-$state-info-bg: $brand-info;
-$state-info-border: $brand-info;
-
-$state-warning-text: $white-light;
-$state-warning-bg: $brand-warning;
-$state-warning-border: $brand-warning;
-
-$state-danger-text: $white-light;
-$state-danger-bg: $brand-danger;
-$state-danger-border: $brand-danger;
-
-
-//== Alerts
-//
-//## Define alert colors, border radius, and padding.
-
-$alert-border-radius: 0;
-
-
-//== Panels
-//
-//##
-
-$panel-border-radius: 2px;
-$panel-default-text: $text-color;
-$panel-default-border: $border-color;
-$panel-default-heading-bg: $gray-light;
-$panel-footer-bg: $gray-light;
-$panel-inner-border: $border-color;
-
-$badge-bg: $badge-bg;
-$badge-color: $badge-color;
-
-//== Wells
-//
-//##
-
-$well-bg: $gray-light;
-$well-border: #eee;
-
-//== Code
-//
-//##
-
-$code-color: $red-600;
-$code-bg: lighten($red-100, 2%);
-
-$kbd-color: $white-light;
-$kbd-bg: #333;
-
-//== Buttons
-//
-//##
-$btn-default-color: $gl-text-color;
-$btn-default-bg: $white-light;
-$btn-default-border: #e7e9ed;
-
-//== Nav
-//
-//##
-$nav-link-padding: 13px $gl-padding;
-
-//== Code
-//
-//##
-$pre-bg: $gray-light !default;
-$pre-color: $gl-text-color !default;
-$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: 860px;
-$modal-md: 540px;
-// $modal-sm: 300px
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 9e1371648ed..9874c928604 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -114,26 +114,27 @@
font-size: 0.95em;
}
- blockquote {
+ blockquote,
+ .blockquote {
color: $gl-grayish-blue;
font-size: inherit;
padding: 8px 24px;
margin: 16px 0;
border-left: 3px solid $white-dark;
- }
- blockquote:dir(rtl) {
- border-left: 0;
- border-right: 3px solid $white-dark;
- }
+ &:dir(rtl) {
+ border-left: 0;
+ border-right: 3px solid $white-dark;
+ }
- blockquote p {
- color: $gl-grayish-blue !important;
- font-size: inherit;
- line-height: 1.5;
+ p {
+ color: $gl-grayish-blue !important;
+ font-size: inherit;
+ line-height: 1.5;
- &:last-child {
- margin: 0;
+ &:last-child {
+ margin: 0;
+ }
}
}
@@ -321,6 +322,16 @@ h6 {
/** CODE **/
pre {
font-family: $monospace_font;
+ display: block;
+ padding: $gl-padding-8;
+ margin: 0 0 $gl-padding-8;
+ font-size: 13px;
+ word-break: break-all;
+ word-wrap: break-word;
+ color: $gl-text-color;
+ background-color: $gray-light;
+ border: 1px solid $border-color;
+ border-radius: $border-radius-small;
}
code {
@@ -330,10 +341,6 @@ code {
}
}
-a > code {
- color: $link-color;
-}
-
.monospace {
font-family: $monospace_font;
}
@@ -343,7 +350,8 @@ a > code {
}
.commit-sha,
-.ref-name {
+.ref-name,
+.pipeline-number {
@extend .monospace;
font-size: 95%;
}
@@ -391,7 +399,7 @@ h4 {
}
.text-right-lg {
- @media (min-width: $screen-lg-min) {
+ @include media-breakpoint-up(lg) {
text-align: right;
}
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index b8ea8ee14c5..6cfa09b56a7 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -99,7 +99,7 @@ $theme-gray-200: #dfdfdf;
$theme-gray-300: #cccccc;
$theme-gray-400: #bababa;
$theme-gray-500: #a7a7a7;
-$theme-gray-600: #949494;
+$theme-gray-600: #919191;
$theme-gray-700: #707070;
$theme-gray-800: #4f4f4f;
$theme-gray-900: #2e2e2e;
@@ -117,6 +117,15 @@ $theme-blue-800: #25496e;
$theme-blue-900: #1a3652;
$theme-blue-950: #0f2235;
+$theme-light-blue-50: #f2f7fc;
+$theme-light-blue-100: #ebf1f7;
+$theme-light-blue-200: #c9dcf2;
+$theme-light-blue-300: #83abd4;
+$theme-light-blue-400: #4d86bf;
+$theme-light-blue-500: #367cc2;
+$theme-light-blue-600: #3771ab;
+$theme-light-blue-700: #2261a1;
+
$theme-green-50: #f2faf6;
$theme-green-100: #e4f3ea;
$theme-green-200: #c0dfcd;
@@ -129,6 +138,29 @@ $theme-green-800: #145d33;
$theme-green-900: #0d4524;
$theme-green-950: #072d16;
+$theme-light-green-700: #156b39;
+
+$theme-red-50: #fcf4f2;
+$theme-red-100: #fae9e6;
+$theme-red-200: #ebcac5;
+$theme-red-300: #d99b91;
+$theme-red-400: #b0655a;
+$theme-red-500: #ad4a3b;
+$theme-red-600: #9e4133;
+$theme-red-700: #912f20;
+$theme-red-800: #78291d;
+$theme-red-900: #691a16;
+$theme-red-950: #36140f;
+
+$theme-light-red-50: #fff6f5;
+$theme-light-red-100: #fae2de;
+$theme-light-red-200: #f7d5d0;
+$theme-light-red-300: #d9796a;
+$theme-light-red-400: #cf604e;
+$theme-light-red-500: #c24b38;
+$theme-light-red-600: #b03927;
+$theme-light-red-700: #a62e21;
+
$black: #000;
$black-transparent: rgba(0, 0, 0, 0.3);
$almost-black: #242424;
@@ -159,7 +191,7 @@ $gl-font-weight-normal: 400;
$gl-font-weight-bold: 600;
$gl-text-color: #2e2e2e;
$gl-text-color-secondary: #707070;
-$gl-text-color-tertiary: #949494;
+$gl-text-color-tertiary: #919191;
$gl-text-color-quaternary: #d6d6d6;
$gl-text-color-inverted: rgba(255, 255, 255, 1);
$gl-text-color-secondary-inverted: rgba(255, 255, 255, 0.85);
@@ -201,7 +233,7 @@ $md-area-border: #ddd;
/*
* Code
*/
-$code_font_size: 12px;
+$code_font_size: 90%;
$code_line_height: 1.6;
/*
@@ -212,10 +244,11 @@ $tooltip-font-size: 12px;
/*
* Padding
*/
-$gl-padding-24: 24px;
-$gl-padding: 16px;
-$gl-padding-8: 8px;
$gl-padding-4: 4px;
+$gl-padding-8: 8px;
+$gl-padding: 16px;
+$gl-padding-24: 24px;
+$gl-padding-32: 32px;
$gl-col-padding: 15px;
$gl-input-padding: 10px;
$gl-vert-padding: 6px;
@@ -230,10 +263,9 @@ $row-hover: $blue-50;
$row-hover-border: $blue-200;
$progress-color: #c0392b;
$header-height: 40px;
-$ide-statusbar-height: 27px;
+$ide-statusbar-height: 25px;
$fixed-layout-width: 1280px;
$limited-layout-width: 990px;
-$limited-layout-width-sm: 790px;
$container-text-max-width: 540px;
$gl-avatar-size: 40px;
$error-exclamation-point: $red-500;
@@ -246,7 +278,7 @@ $active-item-blue: $blue-500;
$layout-link-gray: #7e7c7c;
$btn-side-margin: 10px;
$btn-sm-side-margin: 7px;
-$btn-xs-side-margin: 5px;
+$btn-margin-5: 5px;
$issue-status-expired: $orange-500;
$issuable-sidebar-color: $gl-text-color-secondary;
$sidebar-block-hover-color: #ebebeb;
@@ -306,6 +338,11 @@ $gl-warning: $orange-500;
$gl-danger: $red-500;
$gl-btn-active-background: rgba(0, 0, 0, 0.16);
$gl-btn-active-gradient: inset 0 2px 3px $gl-btn-active-background;
+// Bootstrap override states
+$success: $gl-success;
+$info: $gl-info;
+$warning: $gl-warning;
+$danger: $gl-danger;
/*
* Commit Diff Colors
@@ -363,6 +400,7 @@ $dropdown-chevron-size: 10px;
$dropdown-toggle-active-border-color: darken($border-color, 14%);
$dropdown-item-hover-bg: $gray-darker;
$dropdown-fade-mask-height: 32px;
+$dropdown-member-form-control-width: 163px;
/*
* Filtered Search
@@ -397,6 +435,22 @@ $badge-bg: rgba(0, 0, 0, 0.07);
$badge-color: $gl-text-color-secondary;
/*
+* Pagination
+*/
+$pagination-padding-y: 6px;
+$pagination-padding-x: 16px;
+$pagination-line-height: 20px;
+$pagination-border-color: $border-color;
+$pagination-active-bg: $blue-600;
+$pagination-active-border-color: $blue-600;
+$pagination-hover-bg: $blue-50;
+$pagination-hover-border-color: $border-color;
+$pagination-hover-color: $gl-text-color;
+$pagination-disabled-color: #cdcdcd;
+$pagination-disabled-bg: $gray-light;
+$pagination-disabled-border-color: $border-color;
+
+/*
* Status icons
*/
$status-icon-size: 22px;
@@ -689,6 +743,7 @@ Pipeline Graph
*/
$stage-hover-bg: $gray-darker;
$ci-action-icon-size: 22px;
+$ci-action-icon-size-lg: 24px;
$pipeline-dropdown-line-height: 20px;
$pipeline-dropdown-status-icon-size: 18px;
$ci-action-dropdown-button-size: 24px;
@@ -765,3 +820,19 @@ $modal-body-height: 134px;
Prometheus
*/
$prometheus-table-row-highlight-color: $theme-gray-100;
+
+$priority-label-empty-state-width: 114px;
+
+/*
+ * Override Bootstrap 4 variables
+ */
+
+$secondary: $gray-light;
+$input-disabled-bg: $gray-light;
+$input-border-color: $theme-gray-200;
+$input-color: $gl-text-color;
+$font-family-sans-serif: $regular_font;
+$font-family-monospace: $monospace_font;
+$input-line-height: 20px;
+$btn-line-height: 20px;
+$table-accent-bg: $gray-light;
diff --git a/app/assets/stylesheets/framework/wells.scss b/app/assets/stylesheets/framework/wells.scss
index 3fa7a260017..161943766d4 100644
--- a/app/assets/stylesheets/framework/wells.scss
+++ b/app/assets/stylesheets/framework/wells.scss
@@ -5,7 +5,7 @@
border-radius: $border-radius-default;
margin-bottom: $gl-padding;
- .well-segment {
+ .card.card-body-segment {
padding: $gl-padding;
&:not(:last-of-type) {
@@ -59,7 +59,7 @@
}
}
- .label.label-gray {
+ .label-gray {
background-color: $well-expand-item;
}
@@ -93,6 +93,10 @@
font-size: 12px;
}
}
+
+ svg {
+ vertical-align: text-top;
+ }
}
.light-well {
@@ -108,7 +112,7 @@
}
}
-.well-centered {
+.card.card-body-centered {
h1 {
font-weight: $gl-font-weight-normal;
text-align: center;
diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss
index f0ac9b46f91..604f806dc58 100644
--- a/app/assets/stylesheets/highlight/dark.scss
+++ b/app/assets/stylesheets/highlight/dark.scss
@@ -111,7 +111,9 @@ $dark-il: #de935f;
// Diff line
.line_holder {
- &.match .line_content {
+ &.match .line_content,
+ &.old-nonewline .line_content,
+ &.new-nonewline .line_content {
@include dark-diff-match-line;
}
diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss
index eba7919ada9..8e2720511da 100644
--- a/app/assets/stylesheets/highlight/monokai.scss
+++ b/app/assets/stylesheets/highlight/monokai.scss
@@ -111,7 +111,9 @@ $monokai-gi: #a6e22e;
// Diff line
.line_holder {
- &.match .line_content {
+ &.match .line_content,
+ &.old-nonewline .line_content,
+ &.new-nonewline .line_content {
@include dark-diff-match-line;
}
diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss
index ba53ef0352b..cd1f0f6650f 100644
--- a/app/assets/stylesheets/highlight/solarized_dark.scss
+++ b/app/assets/stylesheets/highlight/solarized_dark.scss
@@ -115,7 +115,9 @@ $solarized-dark-il: #2aa198;
// Diff line
.line_holder {
- &.match .line_content {
+ &.match .line_content,
+ &.old-nonewline .line_content,
+ &.new-nonewline .line_content {
@include dark-diff-match-line;
}
diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss
index e9fccf1b58a..09c3ea36414 100644
--- a/app/assets/stylesheets/highlight/solarized_light.scss
+++ b/app/assets/stylesheets/highlight/solarized_light.scss
@@ -122,7 +122,9 @@ $solarized-light-il: #2aa198;
// Diff line
.line_holder {
- &.match .line_content {
+ &.match .line_content,
+ &.old-nonewline .line_content,
+ &.new-nonewline .line_content {
@include matchLine;
}
diff --git a/app/assets/stylesheets/highlight/white_base.scss b/app/assets/stylesheets/highlight/white_base.scss
index 8cc5252648d..90a5250c247 100644
--- a/app/assets/stylesheets/highlight/white_base.scss
+++ b/app/assets/stylesheets/highlight/white_base.scss
@@ -102,7 +102,9 @@ pre.code,
// Diff line
.line_holder {
- &.match .line_content {
+ &.match .line_content,
+ .new-nonewline.line_content,
+ .old-nonewline.line_content {
@include matchLine;
}
diff --git a/app/assets/stylesheets/mailers/highlighted_diff_email.scss b/app/assets/stylesheets/mailers/highlighted_diff_email.scss
index 658ac26fca9..1835c4364d3 100644
--- a/app/assets/stylesheets/mailers/highlighted_diff_email.scss
+++ b/app/assets/stylesheets/mailers/highlighted_diff_email.scss
@@ -138,7 +138,8 @@ pre {
margin: 0;
}
-blockquote {
+blockquote,
+.blockquote {
color: $gl-grayish-blue;
padding: 0 0 0 15px;
margin: 0;
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 6bb40bae9ed..5de53892fac 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -58,7 +58,7 @@
.boards-app {
position: relative;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
transition: width $sidebar-transition-duration;
width: 100%;
@@ -81,11 +81,11 @@
white-space: nowrap;
min-height: 200px;
- @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
+ @include media-breakpoint-only(sm) {
height: calc(100vh - #{$issue-board-list-difference-sm});
}
- @media (min-width: $screen-md-min) {
+ @include media-breakpoint-up(md) {
height: calc(100vh - #{$issue-board-list-difference-md});
}
@@ -94,13 +94,13 @@
100vh - #{$issue-board-list-difference-xs} - #{$performance-bar-height}
);
- @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
+ @include media-breakpoint-only(sm) {
height: calc(
100vh - #{$issue-board-list-difference-sm} - #{$performance-bar-height}
);
}
- @media (min-width: $screen-md-min) {
+ @include media-breakpoint-up(md) {
height: calc(
100vh - #{$issue-board-list-difference-md} - #{$performance-bar-height}
);
@@ -117,7 +117,7 @@
white-space: normal;
vertical-align: top;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
width: 400px;
}
@@ -274,7 +274,7 @@
font-size: (26px / $issue-boards-font-size) * 1em;
}
-.card {
+.board-card {
position: relative;
padding: 11px 10px 11px $gl-padding;
background: $white-light;
@@ -282,23 +282,16 @@
box-shadow: 0 1px 2px $issue-boards-card-shadow;
list-style: none;
- // as a fallback, hide overflow content so that dragging and dropping still works
- overflow: hidden;
-
&:not(:last-child) {
margin-bottom: 5px;
}
&.is-active,
- &.is-active .card-assignee:hover a {
+ &.is-active .board-card-assignee:hover a {
background-color: $row-hover;
-
- &:first-child:not(:only-child) {
- box-shadow: -10px 0 10px 1px $row-hover;
- }
}
- .label {
+ .badge {
border: 0;
outline: 0;
}
@@ -310,7 +303,7 @@
}
}
-.card-title {
+.board-card-title {
@include overflow-break-word();
margin: 0 30px 0 0;
font-size: 1em;
@@ -322,11 +315,11 @@
}
}
-.card-header {
+.board-card-header {
display: flex;
min-height: 20px;
- .card-assignee {
+ .board-card-assignee {
display: flex;
justify-content: flex-end;
position: absolute;
@@ -397,16 +390,16 @@
}
}
-.card-footer {
+.board-card-footer {
margin: 0 0 5px;
- .label {
+ .badge {
margin-top: 5px;
margin-right: 6px;
}
}
-.card-number {
+.board-card-number {
font-size: 12px;
color: $gl-text-color-secondary;
}
@@ -564,11 +557,11 @@
.add-issues-list-column {
width: 100%;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
width: 50%;
}
- @media (min-width: $screen-md-min) {
+ @include media-breakpoint-up(md) {
width: (100% / 3);
}
}
@@ -583,11 +576,11 @@
margin-right: -$gl-vert-padding;
overflow-y: scroll;
- .card-parent {
+ .board-card-parent {
padding: 0 5px 5px;
}
- .card {
+ .board-card {
border: 1px solid $border-gray-dark;
box-shadow: 0 1px 2px rgba($issue-boards-card-shadow, 0.3);
cursor: pointer;
@@ -637,7 +630,7 @@
display: none;
margin-right: 10px;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
display: block;
}
}
@@ -645,7 +638,7 @@
.dropdown-menu-toggle {
width: 100px;
- @media (min-width: $screen-md-min) {
+ @include media-breakpoint-up(md) {
width: 140px;
}
}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 50f32660445..f030189af06 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -12,26 +12,22 @@
@keyframes blinking-dots {
0% {
background-color: rgba($white-light, 1);
- box-shadow: 12px 0 0 0 rgba($white-light, 0.2),
- 24px 0 0 0 rgba($white-light, 0.2);
+ box-shadow: 12px 0 0 0 rgba($white-light, 0.2), 24px 0 0 0 rgba($white-light, 0.2);
}
25% {
background-color: rgba($white-light, 0.4);
- box-shadow: 12px 0 0 0 rgba($white-light, 2),
- 24px 0 0 0 rgba($white-light, 0.2);
+ box-shadow: 12px 0 0 0 rgba($white-light, 2), 24px 0 0 0 rgba($white-light, 0.2);
}
75% {
background-color: rgba($white-light, 0.4);
- box-shadow: 12px 0 0 0 rgba($white-light, 0.2),
- 24px 0 0 0 rgba($white-light, 1);
+ box-shadow: 12px 0 0 0 rgba($white-light, 0.2), 24px 0 0 0 rgba($white-light, 1);
}
100% {
background-color: rgba($white-light, 1);
- box-shadow: 12px 0 0 0 rgba($white-light, 0.2),
- 24px 0 0 0 rgba($white-light, 0.2);
+ box-shadow: 12px 0 0 0 rgba($white-light, 0.2), 24px 0 0 0 rgba($white-light, 0.2);
}
}
@@ -71,10 +67,15 @@
.bash {
display: block;
}
+
+ &.build-trace-rounded {
+ border-radius: $border-radius-base;
+ }
}
.top-bar {
height: 35px;
+ min-height: 35px;
background: $gray-light;
border: 1px solid $border-color;
color: $gl-text-color;
@@ -215,7 +216,7 @@
}
.header-action-buttons {
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
.sidebar-toggle-btn {
margin-top: 0;
margin-left: 10px;
@@ -277,10 +278,6 @@
&.coverage {
padding: 0 16px 11px;
}
-
- .btn-group-justified {
- margin-top: 5px;
- }
}
.block-last {
@@ -305,7 +302,7 @@
background-color: $white-light;
}
- .label {
+ .badge.badge-pill {
margin-left: 2px;
}
@@ -320,7 +317,7 @@
}
}
- @media (max-width: $screen-sm-max) {
+ @include media-breakpoint-down(sm) {
display: block;
.btn {
diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss
index 3fd13078131..56beb7718a4 100644
--- a/app/assets/stylesheets/pages/clusters.scss
+++ b/app/assets/stylesheets/pages/clusters.scss
@@ -6,13 +6,17 @@
.cluster-applications-table {
// Wait for the Vue to kick-in and render the applications block
- min-height: 400px;
+ min-height: 628px;
}
.clusters-dropdown-menu {
max-width: 100%;
}
+.clusters-error-alert {
+ width: 100%;
+}
+
.clusters-container {
.nav-bar-right {
padding: $gl-padding-top $gl-padding;
@@ -63,7 +67,7 @@
text-decoration: none;
}
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
> div {
display: flex;
align-items: center;
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 0a27c6e0a25..f75be4e01cd 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -23,7 +23,7 @@
}
.commit-hash-full {
- @media (max-width: $screen-sm-max) {
+ @include media-breakpoint-down(sm) {
width: 80px;
white-space: nowrap;
overflow: hidden;
@@ -135,10 +135,10 @@
}
.text-expander {
- display: inline-block;
+ display: inline-flex;
background: $white-light;
color: $gl-text-color-secondary;
- padding: 0 4px;
+ padding: 1px $gl-padding-4;
cursor: pointer;
border: 1px solid $border-gray-dark;
border-radius: $border-radius-default;
@@ -180,10 +180,15 @@
.commit-content {
padding-right: 10px;
white-space: normal;
+
+ .commit-title {
+ display: flex;
+ align-items: center;
+ }
}
.commit-actions {
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
.fa-spinner {
font-size: 12px;
}
@@ -193,6 +198,10 @@
display: inline-flex;
}
+ .ci-status-icon svg {
+ vertical-align: text-bottom;
+ }
+
> .ci-status-link,
> .btn,
> .commit-sha-group {
@@ -249,16 +258,19 @@
.generic_commit_status {
a,
button {
- color: $gl-text-color;
vertical-align: baseline;
}
- a.autodevops-badge {
- color: $white-light;
- }
+ a {
+ color: $gl-text-color;
- a.autodevops-link {
- color: $gl-link-color;
+ &.autodevops-badge {
+ color: $white-light;
+ }
+
+ &.autodevops-link {
+ color: $gl-link-color;
+ }
}
.commit-row-description {
@@ -327,11 +339,11 @@
}
&.invalid {
- @include status-color($gray-dark, $gray, $gray-darkest);
+ @include status-color($gray-dark, color("gray"), $gray-darkest);
border-color: $gray-darkest;
&:not(span):hover {
- color: $gray;
+ color: color("gray");
}
}
}
diff --git a/app/assets/stylesheets/pages/convdev_index.scss b/app/assets/stylesheets/pages/convdev_index.scss
index fb1899284fd..bd338326154 100644
--- a/app/assets/stylesheets/pages/convdev_index.scss
+++ b/app/assets/stylesheets/pages/convdev_index.scss
@@ -53,19 +53,19 @@ $space-between-cards: 8px;
padding: $space-between-cards / 2;
position: relative;
- @media (min-width: $screen-xs-min) {
+ @include media-breakpoint-up(xs) {
width: percentage(1 / 4);
}
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
width: percentage(1 / 5);
}
- @media (min-width: $screen-md-min) {
+ @include media-breakpoint-up(md) {
width: percentage(1 / 6);
}
- @media (min-width: $screen-lg-min) {
+ @include media-breakpoint-up(lg) {
width: percentage(1 / 10);
}
}
@@ -82,7 +82,7 @@ $space-between-cards: 8px;
.convdev-card-low {
border-top-color: $color-low-score;
- .card-score-big {
+ .board-card-score-big {
background-color: $red-50;
}
}
@@ -90,7 +90,7 @@ $space-between-cards: 8px;
.convdev-card-average {
border-top-color: $color-average-score;
- .card-score-big {
+ .board-card-score-big {
background-color: $orange-50;
}
}
@@ -98,7 +98,7 @@ $space-between-cards: 8px;
.convdev-card-high {
border-top-color: $color-high-score;
- .card-score-big {
+ .board-card-score-big {
background-color: $green-50;
}
}
@@ -112,14 +112,14 @@ $space-between-cards: 8px;
margin: 0 0 2px;
}
- .text-light {
+ .light-text {
font-size: 13px;
line-height: 1.25;
color: $gl-text-color-secondary;
}
}
-.card-scores {
+.board-card-scores {
display: flex;
justify-content: space-around;
align-items: center;
@@ -127,22 +127,22 @@ $space-between-cards: 8px;
line-height: 1;
}
-.card-score {
+.board-card-score {
color: $gl-text-color-secondary;
- .card-score-name {
+ .board-card-score-name {
font-size: 13px;
margin-top: 4px;
}
}
-.card-score-value {
+.board-card-score-value {
font-size: 16px;
color: $gl-text-color;
font-weight: $gl-font-weight-normal;
}
-.card-score-big {
+.board-card-score-big {
border-top: 2px solid $border-color;
border-bottom: 1px solid $border-color;
font-size: 22px;
@@ -150,7 +150,7 @@ $space-between-cards: 8px;
font-weight: $gl-font-weight-normal;
}
-.card-buttons {
+.board-card-buttons {
display: flex;
> * {
diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss
index cfef6476d4d..a22c666a525 100644
--- a/app/assets/stylesheets/pages/cycle_analytics.scss
+++ b/app/assets/stylesheets/pages/cycle_analytics.scss
@@ -15,7 +15,7 @@
max-width: 480px;
padding: 0 $gl-padding;
- @media (max-width: $screen-sm-min) {
+ @include media-breakpoint-down(sm) {
margin: 0 auto;
}
}
@@ -75,13 +75,13 @@
}
}
- .panel {
+ .card {
.content-block {
padding: 24px 0;
border-bottom: 0;
position: relative;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
padding: 6px 0 24px;
}
}
@@ -89,7 +89,7 @@
.column {
text-align: center;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
padding: 15px 0;
}
@@ -106,7 +106,7 @@
}
&:last-child {
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
text-align: center;
}
}
@@ -213,7 +213,7 @@
.stage-panel {
min-width: 968px;
- .panel-heading {
+ .card-header {
padding: 0;
background-color: transparent;
}
@@ -266,7 +266,9 @@
&.issue-title,
&.commit-title,
&.merge-merquest-title {
- @include text-overflow();
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
max-width: 100%;
display: block;
diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss
index 2f2c04206e2..2e007c52592 100644
--- a/app/assets/stylesheets/pages/detail_page.scss
+++ b/app/assets/stylesheets/pages/detail_page.scss
@@ -14,7 +14,7 @@
white-space: nowrap;
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
display: block;
}
}
@@ -23,9 +23,10 @@
position: relative;
line-height: 35px;
display: flex;
- flex-grow: 1;
+ flex: 1 1;
+ min-width: 0;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
padding-left: 0;
padding-right: 0;
}
@@ -36,7 +37,7 @@
flex-shrink: 0;
flex: 0 0 auto;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
width: 100%;
margin-top: 10px;
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 70ce5de6a6c..7e89f8998fb 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -14,8 +14,8 @@
background-color: $gray-normal;
}
- .diff-toggle-caret {
- padding-right: 6px;
+ svg {
+ vertical-align: middle;
}
}
@@ -24,6 +24,10 @@
color: $gl-text-color;
border-radius: 0 0 3px 3px;
+ .code {
+ padding: 0;
+ }
+
.unfold {
cursor: pointer;
}
@@ -61,6 +65,7 @@
.diff-line-num {
width: 50px;
+ position: relative;
a {
transition: none;
@@ -77,6 +82,12 @@
span {
white-space: pre-wrap;
+
+ &.context-cell {
+ display: inline-block;
+ width: 100%;
+ height: 100%;
+ }
}
.line {
@@ -86,13 +97,13 @@
&.left-side-selected {
td.line_content.parallel.right-side {
- @include user-select(none);
+ user-select: none;
}
}
&.right-side-selected {
td.line_content.parallel.left-side {
- @include user-select(none);
+ user-select: none;
}
}
}
@@ -109,7 +120,7 @@
.old_line,
.new_line {
- @include user-select(none);
+ user-select: none;
margin: 0;
border: 0;
padding: 0 5px;
@@ -189,8 +200,22 @@
img {
border: 1px solid $white-light;
- background-image: linear-gradient(45deg, $border-color 25%, transparent 25%, transparent 75%, $border-color 75%, $border-color 100%),
- linear-gradient(45deg, $border-color 25%, transparent 25%, transparent 75%, $border-color 75%, $border-color 100%);
+ background-image: linear-gradient(
+ 45deg,
+ $border-color 25%,
+ transparent 25%,
+ transparent 75%,
+ $border-color 75%,
+ $border-color 100%
+ ),
+ linear-gradient(
+ 45deg,
+ $border-color 25%,
+ transparent 25%,
+ transparent 75%,
+ $border-color 75%,
+ $border-color 100%
+ );
background-size: 10px 10px;
background-position: 0 0, 5px 5px;
max-width: 100%;
@@ -395,6 +420,69 @@
.line_content {
white-space: pre-wrap;
}
+
+ .diff-file-container {
+ .frame.deleted {
+ border: 0;
+ background-color: inherit;
+
+ .image_file img {
+ border: 1px solid $deleted;
+ }
+ }
+
+ .frame.added {
+ border: 0;
+ background-color: inherit;
+
+ .image_file img {
+ border: 1px solid $added;
+ }
+ }
+
+ .swipe.view,
+ .onion-skin.view {
+ .swipe-wrap {
+ top: 0;
+ right: 0;
+ }
+
+ .frame.deleted {
+ top: 0;
+ right: 0;
+ }
+
+ .swipe-bar {
+ top: 0;
+
+ .top-handle {
+ top: -14px;
+ left: -7px;
+ }
+
+ .bottom-handle {
+ bottom: -14px;
+ left: -7px;
+ }
+ }
+
+ .file-container {
+ display: inline-block;
+
+ .file-content {
+ padding: 0;
+
+ img {
+ max-width: none;
+ }
+ }
+ }
+ }
+
+ .onion-skin.view .controls {
+ bottom: -25px;
+ }
+ }
}
.file-content .diff-file {
@@ -414,6 +502,10 @@
border-bottom: 0;
}
+.merge-request-details .file-content.image_file img {
+ max-height: 50vh;
+}
+
.diff-stats-summary-toggler {
padding: 0;
background-color: transparent;
@@ -536,7 +628,7 @@
margin-right: 0;
border-color: $white-light;
cursor: pointer;
- transition: all .1s ease-out;
+ transition: all 0.1s ease-out;
@for $i from 1 through 4 {
&:nth-child(#{$i}) {
@@ -563,7 +655,7 @@
height: 24px;
border-radius: 50%;
padding: 0;
- transition: transform .1s ease-out;
+ transition: transform 0.1s ease-out;
z-index: 100;
.collapse-icon {
@@ -592,43 +684,48 @@
}
.commit-stat-summary {
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
margin-left: -$gl-padding;
padding-left: $gl-padding;
background-color: $white-light;
}
}
- @media (min-width: $screen-sm-min) {
- position: -webkit-sticky;
- position: sticky;
+ @include media-breakpoint-up(sm) {
top: 24px;
background-color: $white-light;
- z-index: 190;
&.diff-files-changed-merge-request {
- top: 76px;
- }
-
- &:not(.is-stuck) .diff-stats-additions-deletions-collapsed {
- display: none;
+ position: sticky;
+ top: 90px;
+ z-index: 200;
+ margin: $gl-padding 0;
+ padding: 0;
}
&.is-stuck {
padding-top: 0;
padding-bottom: 0;
+ border-top: 1px solid $white-dark;
border-bottom: 1px solid $white-dark;
- transform: translateY(16px);
.diff-stats-additions-deletions-expanded,
.inline-parallel-buttons {
- display: none;
+ display: none !important;
+ }
+ }
+ }
+
+ @include media-breakpoint-up(lg) {
+ &.is-stuck {
+ .diff-stats-additions-deletions-collapsed {
+ display: block !important;
}
}
}
}
-@media (min-width: $screen-sm-min) {
+@include media-breakpoint-up(sm) {
.with-performance-bar {
.diff-files-changed.diff-files-changed-merge-request {
top: 76px + $performance-bar-height;
@@ -640,8 +737,12 @@
max-width: 560px;
width: 100%;
z-index: 150;
+ min-height: $dropdown-min-height;
+ max-height: $dropdown-max-height;
+ overflow-y: auto;
+ margin-bottom: 0;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
left: $gl-padding;
}
@@ -704,11 +805,35 @@
width: 100%;
height: 10px;
background-color: $white-light;
- background-image: linear-gradient(45deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%),
- linear-gradient(225deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%),
- linear-gradient(135deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%),
- linear-gradient(-45deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%);
- background-position: 5px 5px,0 5px,0 5px,5px 5px;
+ background-image: linear-gradient(
+ 45deg,
+ transparent,
+ transparent 73%,
+ $diff-jagged-border-gradient-color 75%,
+ $white-light 80%
+ ),
+ linear-gradient(
+ 225deg,
+ transparent,
+ transparent 73%,
+ $diff-jagged-border-gradient-color 75%,
+ $white-light 80%
+ ),
+ linear-gradient(
+ 135deg,
+ transparent,
+ transparent 73%,
+ $diff-jagged-border-gradient-color 75%,
+ $white-light 80%
+ ),
+ linear-gradient(
+ -45deg,
+ transparent,
+ transparent 73%,
+ $diff-jagged-border-gradient-color 75%,
+ $white-light 80%
+ );
+ background-position: 5px 5px, 0 5px, 0 5px, 5px 5px;
background-size: 10px 10px;
background-repeat: repeat;
}
@@ -746,11 +871,16 @@
.frame.click-to-comment {
position: relative;
cursor: image-url('illustrations/image_comment_light_cursor.svg')
- $image-comment-cursor-left-offset $image-comment-cursor-top-offset, auto;
+ $image-comment-cursor-left-offset $image-comment-cursor-top-offset,
+ auto;
// Retina cursor
- cursor: -webkit-image-set(image-url('illustrations/image_comment_light_cursor.svg') 1x, image-url('illustrations/image_comment_light_cursor@2x.svg') 2x)
- $image-comment-cursor-left-offset $image-comment-cursor-top-offset, auto;
+ cursor: -webkit-image-set(
+ image-url('illustrations/image_comment_light_cursor.svg') 1x,
+ image-url('illustrations/image_comment_light_cursor@2x.svg') 2x
+ )
+ $image-comment-cursor-left-offset $image-comment-cursor-top-offset,
+ auto;
.comment-indicator {
position: absolute;
@@ -772,9 +902,9 @@
}
}
-.frame .badge,
-.image-diff-avatar-link .badge,
-.notes > .badge {
+.frame .badge.badge-pill,
+.image-diff-avatar-link .badge.badge-pill,
+.notes > .badge.badge-pill {
position: absolute;
background-color: $blue-400;
color: $white-light;
@@ -788,7 +918,7 @@
}
}
-.frame .badge,
+.frame .badge.badge-pill,
.frame .image-comment-badge {
// Center align badges on the frame
transform: translate(-50%, -50%);
@@ -811,14 +941,14 @@
.image-diff-avatar-link {
position: relative;
- .badge,
+ .badge.badge-pill,
.image-comment-badge {
top: 25px;
right: 8px;
}
}
-.notes > .badge {
+.notes > .badge.badge-pill {
display: none;
left: -13px;
}
@@ -836,11 +966,11 @@
.diff-notes-collapse,
.note,
- .discussion-reply-holder, {
+ .discussion-reply-holder {
display: none;
}
- .notes > .badge {
+ .notes > .badge.badge-pill {
display: block;
}
}
diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss
index 8ecda50602d..437621299e0 100644
--- a/app/assets/stylesheets/pages/editor.scss
+++ b/app/assets/stylesheets/pages/editor.scss
@@ -37,7 +37,7 @@
padding-top: 7px;
padding-bottom: 7px;
- .pull-right {
+ .float-right {
height: 20px;
}
}
@@ -63,11 +63,11 @@
max-width: 450px;
float: left;
- @media(max-width: $screen-md-max) {
+ @media(max-width: map-get($grid-breakpoints, lg)-1) {
width: 280px;
}
- @media(max-width: $screen-sm-max) {
+ @media(max-width: map-get($grid-breakpoints, md)-1) {
width: 180px;
}
}
@@ -111,10 +111,10 @@
}
-@media(max-width: $screen-xs-max){
+@include media-breakpoint-down(xs) {
.file-editor {
.file-title {
- .pull-right {
+ .float-right {
height: auto;
}
}
@@ -153,7 +153,7 @@
vertical-align: top;
display: inline-block;
- @media(max-width: $screen-sm-max) {
+ @media(max-width: map-get($grid-breakpoints, md)-1) {
display: block;
margin: 19px 0 12px;
}
@@ -166,7 +166,7 @@
padding: 0 0 0 14px;
border-left: 1px solid $border-color;
- @media(max-width: $screen-sm-max) {
+ @media(max-width: map-get($grid-breakpoints, md)-1) {
display: block;
width: 100%;
margin: 5px 0;
@@ -181,7 +181,7 @@
margin-top: 6px;
line-height: 21px;
- @media(max-width: $screen-sm-max) {
+ @media(max-width: map-get($grid-breakpoints, md)-1) {
display: block;
margin: 5px 0;
}
@@ -193,7 +193,7 @@
vertical-align: top;
margin: 5px 0 0 8px;
- @media(max-width: $screen-sm-max) {
+ @media(max-width: map-get($grid-breakpoints, md)-1) {
display: block;
width: 100%;
margin: 0 0 16px;
@@ -209,7 +209,7 @@
font-family: $regular_font;
margin-top: -5px;
- @media(max-width: $screen-sm-max) {
+ @media(max-width: map-get($grid-breakpoints, md)-1) {
display: block;
width: 100%;
margin: 5px 0;
@@ -223,7 +223,7 @@
width: 250px;
vertical-align: top;
- @media(max-width: $screen-sm-max) {
+ @media(max-width: map-get($grid-breakpoints, md)-1) {
display: block;
width: 100%;
margin: 5px 0;
@@ -237,7 +237,7 @@
display: inline-block;
margin: 7px 0 0 10px;
- @media(max-width: $screen-sm-max) {
+ @media(max-width: map-get($grid-breakpoints, md)-1) {
display: block;
width: 100%;
margin: 20px 0;
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index 1f406cc1c2d..3144dcc4dc0 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -1,4 +1,4 @@
-@media (max-width: $screen-md-max) {
+@include media-breakpoint-down(md) {
.deployments-container {
width: 100%;
overflow: auto;
@@ -23,8 +23,7 @@
}
.btn-group {
-
- > a {
+ > .btn:not(.btn-danger) {
color: $gl-text-color-secondary;
}
@@ -138,7 +137,7 @@
border-left: 0;
border-right: 0;
- @media (min-width: $screen-sm-max) {
+ @media (min-width: map-get($grid-breakpoints, md)-1) {
border-top: 0;
}
}
@@ -223,6 +222,23 @@
}
}
+.prometheus-graphs {
+ .environments {
+ .dropdown-menu-toggle {
+ svg {
+ position: absolute;
+ right: 5%;
+ top: 25%;
+ }
+ }
+
+ .dropdown-menu-toggle,
+ .dropdown-menu {
+ width: 240px;
+ }
+ }
+}
+
.environments-actions {
.external-url,
.monitoring-url,
@@ -245,17 +261,29 @@
.prometheus-graph {
flex: 1 0 auto;
min-width: 450px;
+ max-width: 100%;
padding: $gl-padding / 2;
h5 {
font-size: 16px;
}
- @media (max-width: $screen-sm-max) {
+ @include media-breakpoint-down(sm) {
min-width: 100%;
}
}
+.prometheus-graph-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: $gl-padding-8;
+
+ h5 {
+ margin: 0;
+ }
+}
+
.prometheus-graph-cursor {
position: absolute;
background: $theme-gray-600;
@@ -432,7 +460,7 @@
}
}
- @media (max-width: $screen-sm-max) {
+ @include media-breakpoint-down(sm) {
.label-axis-text,
.text-metric-usage,
.legend-axis-text {
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index d9267f5cdf3..f79586b68b9 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -70,7 +70,7 @@
.md {
font-size: $gl-font-size;
- .label {
+ .badge.badge-pill {
color: $gl-text-color;
}
@@ -173,7 +173,7 @@
}
}
-@media (max-width: $screen-xs-max) {
+@include media-breakpoint-down(xs) {
.event-item {
padding-left: 0;
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index 409b7285f82..05bf5596fb3 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -2,7 +2,7 @@
@include str-truncated(90%);
}
-.dashboard .side .panel .panel-heading .input-group {
+.dashboard .side .card .card-header .input-group {
.form-control {
height: 42px;
@@ -40,7 +40,7 @@
flex: 1;
}
- .dropdown-menu-align-right {
+ .dropdown-menu-right {
margin-top: 0;
}
@@ -102,7 +102,7 @@
}
}
- @media (max-width: $screen-sm-max) {
+ @include media-breakpoint-down(sm) {
&,
.dropdown,
.dropdown .dropdown-toggle,
@@ -149,14 +149,14 @@
padding: 50px 100px;
overflow: hidden;
- @media (max-width: $screen-sm-max) {
+ @include media-breakpoint-down(sm) {
padding: 50px 0;
}
svg {
float: right;
- @media (max-width: $screen-sm-max) {
+ @include media-breakpoint-down(sm) {
float: none;
display: block;
width: 250px;
@@ -171,7 +171,7 @@
width: 460px;
margin-top: 120px;
- @media (max-width: $screen-sm-max) {
+ @include media-breakpoint-down(sm) {
float: none;
margin-top: 60px;
width: auto;
@@ -205,7 +205,7 @@
max-width: 480px;
padding: 0 $gl-padding;
- @media (max-width: $screen-sm-min) {
+ @include media-breakpoint-down(sm) {
margin: 0 auto;
}
}
@@ -425,7 +425,7 @@
margin-left: 5px;
> .btn {
- margin-right: $btn-xs-side-margin;
+ margin-right: $btn-margin-5;
}
}
}
diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss
index 9cc9e11bcd1..0350fe5752e 100644
--- a/app/assets/stylesheets/pages/help.scss
+++ b/app/assets/stylesheets/pages/help.scss
@@ -28,8 +28,8 @@
}
.key {
- @extend .label;
- @extend .label-inverse;
+ @extend .badge.badge-pill;
+ background-color: $label-inverse-bg;
font: 11px Consolas, "Liberation Mono", Menlo, Courier, monospace;
padding: 3px 5px;
}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index a8110f069d4..f9fd9f1ab8b 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -373,7 +373,7 @@
/* Extra small devices (phones, less than 768px) */
display: none;
/* Small devices (tablets, 768px and up) */
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
display: block;
}
@@ -485,6 +485,15 @@
.sidebar-collapsed-user {
padding-bottom: 0;
margin-bottom: 10px;
+
+ .author_link {
+ padding-left: 0;
+
+ .avatar {
+ position: static;
+ margin: 0;
+ }
+ }
}
.issuable-header-btn {
@@ -656,7 +665,7 @@
}
.issuable-form-padding-top {
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
padding-top: 7px;
}
}
@@ -670,7 +679,7 @@
padding-left: 9px;
padding-right: 9px;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
display: inline-block;
height: auto;
align-self: center;
@@ -678,7 +687,7 @@
}
.issuable-gutter-toggle {
- @media (max-width: $screen-sm-max) {
+ @include media-breakpoint-down(sm) {
margin-left: $btn-side-margin;
}
}
@@ -689,6 +698,8 @@
font-size: 14px;
line-height: 24px;
align-self: center;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
.js-issuable-selector-wrap {
@@ -696,7 +707,7 @@
width: 100%;
}
- @media (max-width: $screen-sm-max) {
+ @include media-breakpoint-down(sm) {
margin-bottom: $gl-padding;
}
}
@@ -737,7 +748,7 @@
}
}
- @media(max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
.issuable-meta {
.controls li {
margin-right: 0;
@@ -772,7 +783,7 @@
}
}
- @media(max-width: $screen-md-max) {
+ @media(max-width: map-get($grid-breakpoints, lg)-1) {
.task-status,
.issuable-due-date,
.project-ref-path {
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 0d17b9bae7e..19fb99bfa93 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -141,7 +141,7 @@ ul.related-merge-requests > li {
}
}
-@media (max-width: $screen-xs-max) {
+@include media-breakpoint-down(xs) {
.detail-page-header {
.issuable-meta {
line-height: 18px;
@@ -203,7 +203,7 @@ ul.related-merge-requests > li {
}
}
- .btn-group:not(.hide) {
+ .btn-group:not(.hidden) {
display: flex;
}
@@ -245,7 +245,7 @@ ul.related-merge-requests > li {
display: block;
}
-@media (min-width: $screen-sm-min) {
+@include media-breakpoint-up(sm) {
.emoji-block .row {
display: flex;
@@ -256,8 +256,8 @@ ul.related-merge-requests > li {
}
.create-mr-dropdown-wrap {
- .btn-group:not(.hide) {
- display: inline-block;
+ .btn-group:not(.hidden) {
+ display: inline-flex;
}
}
}
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index d81236c5883..391dfea0703 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -57,71 +57,11 @@
border-bottom-left-radius: $border-radius-base;
}
-.label-row {
- .label-name {
- display: inline-block;
- margin-bottom: 10px;
-
- @media (min-width: $screen-sm-min) {
- width: 200px;
- margin-left: $gl-padding * 2;
- margin-bottom: 0;
- }
-
- .label {
- overflow: hidden;
- text-overflow: ellipsis;
- max-width: 100%;
- }
- }
-
- .label-type {
- display: block;
- margin-bottom: 10px;
- margin-left: 50px;
-
- @media (min-width: $screen-sm-min) {
- display: inline-block;
- width: 100px;
- margin-left: 10px;
- margin-bottom: 0;
- vertical-align: top;
- }
- }
-
- .label-description {
- display: block;
- margin-bottom: 10px;
-
- .description-text {
- margin-bottom: $gl-padding;
- }
-
- a {
- color: $blue-600;
- }
-
- @media (min-width: $screen-sm-min) {
- display: inline-block;
- max-width: 50%;
- margin-left: 10px;
- margin-bottom: 0;
- vertical-align: top;
- }
- }
-
- .label {
- padding: 4px $grid-size;
- font-size: $label-font-size;
- position: relative;
- top: ($grid-size / 2);
- }
-}
-
.color-label {
padding: 0 $grid-size;
line-height: 16px;
border-radius: $label-border-radius;
+ color: $white-light;
}
.dropdown-labels-error {
@@ -132,26 +72,31 @@
}
.manage-labels-list {
- @media(min-width: $screen-md-min) {
- &.content-list li {
- padding: $gl-padding 0;
- }
- }
-
> li:not(.empty-message):not(.is-not-draggable) {
background-color: $white-light;
- cursor: move;
- cursor: -webkit-grab;
- cursor: -moz-grab;
-
- &:active {
- cursor: -webkit-grabbing;
- cursor: -moz-grabbing;
- }
+ margin-bottom: 5px;
+ display: flex;
+ justify-content: space-between;
+ padding: $gl-padding;
+ border-radius: $border-radius-default;
+ border: 1px solid $theme-gray-100;
&.sortable-ghost {
opacity: 0.3;
}
+
+ .prioritized-labels & {
+ box-shadow: 0 1px 2px $issue-boards-card-shadow;
+ cursor: move;
+ cursor: -webkit-grab;
+ cursor: -moz-grab;
+ border: 0;
+
+ &:active {
+ cursor: -webkit-grabbing;
+ cursor: -moz-grabbing;
+ }
+ }
}
.btn-action {
@@ -170,28 +115,11 @@
}
}
- .dropdown {
- @media (min-width: $screen-sm-min) {
- float: right;
- }
- }
-
- @media (max-width: $screen-xs-max) {
- .dropdown-menu {
- min-width: 100%;
- }
+ .color-label {
+ padding: $gl-padding-4 $grid-size;
}
}
-.draggable-handler {
- display: inline-block;
- vertical-align: top;
- margin: 5px 0;
- opacity: 0;
- transition: opacity .3s;
- color: $gray-darkest;
-}
-
.prioritized-labels {
margin-bottom: 30px;
@@ -214,22 +142,6 @@
}
}
-.toggle-priority {
- display: inline-block;
- vertical-align: top;
-
- button {
- border-color: transparent;
- padding: 5px 8px;
- vertical-align: top;
- font-size: 14px;
-
- &:hover {
- border-color: transparent;
- }
- }
-}
-
.filtered-labels {
font-size: 0;
padding: 12px 16px;
@@ -283,10 +195,8 @@
}
.label-subscribe-button {
- @media(min-width: $screen-md-min) {
- min-width: 105px;
- margin-left: $gl-padding;
- }
+ width: 105px;
+ font-weight: 200;
.label-subscribe-button-icon {
&[disabled] {
@@ -312,7 +222,7 @@
.label-link {
display: inline-flex;
- vertical-align: top;
+ vertical-align: text-bottom;
&:hover .color-label {
text-decoration: underline;
@@ -323,3 +233,95 @@
font-size: $label-font-size;
}
}
+
+.labels-container {
+ background-color: $gray-light;
+ border-radius: $border-radius-default;
+ padding: $gl-padding $gl-padding-8;
+}
+
+.label-actions-list {
+ list-style: none;
+ flex-shrink: 0;
+ padding: 0;
+}
+
+.label-badge {
+ color: $theme-gray-900;
+ font-weight: $gl-font-weight-normal;
+ padding: $gl-padding-4 $gl-padding-8;
+ border-radius: $border-radius-default;
+ font-size: $label-font-size;
+}
+
+.label-badge-blue {
+ background-color: $theme-blue-100;
+}
+
+.label-badge-gray {
+ background-color: $theme-gray-100;
+}
+
+.label-links {
+ list-style: none;
+ padding: 0;
+ white-space: nowrap;
+}
+
+.label-link-item {
+ padding: 0;
+}
+
+.label-list-item {
+ .content-list &::before,
+ .content-list &::after {
+ content: none;
+ }
+
+ .label-name {
+ width: 150px;
+ flex-shrink: 0;
+
+ .badge {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 100%;
+ }
+ }
+
+ .label-description {
+ flex-grow: 1;
+
+ a {
+ color: $blue-600;
+ }
+ }
+
+ .label {
+ padding: 4px $grid-size;
+ font-size: $label-font-size;
+ position: relative;
+ top: $gl-padding-4;
+ }
+
+ .label-action {
+ color: $theme-gray-800;
+ cursor: pointer;
+
+ svg {
+ fill: $theme-gray-800;
+ }
+
+ &:hover {
+ color: $blue-600;
+
+ svg {
+ fill: $blue-600;
+ }
+ }
+ }
+}
+
+.priority-labels-empty-state .svg-content img {
+ max-width: $priority-label-empty-state-width;
+}
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index 97303d02666..c1b1d2e028d 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -191,9 +191,9 @@
}
}
-@media (max-width: $screen-xs-max) {
+@include media-breakpoint-down(xs) {
.login-page {
- .col-sm-5.pull-right {
+ .col-md-5.float-right {
float: none !important;
margin-bottom: 45px;
}
@@ -243,7 +243,7 @@
.navless-container {
padding: 65px 15px; // height of footer + bottom padding of email confirmation link
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
padding: 0 15px 65px;
}
}
diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss
index d5ae141dd7e..5fdb2b4a90a 100644
--- a/app/assets/stylesheets/pages/members.scss
+++ b/app/assets/stylesheets/pages/members.scss
@@ -9,9 +9,13 @@
}
}
+.member-sort-dropdown {
+ margin-left: $gl-padding-8;
+}
+
.member {
.list-item-name {
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
float: left;
width: 50%;
}
@@ -22,46 +26,50 @@
}
.controls {
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
display: -webkit-flex;
display: flex;
}
- .dropdown-menu.dropdown-menu-align-right {
+ .dropdown-menu.dropdown-menu-right {
margin-top: -2px;
}
}
- .form-horizontal {
- margin-top: 20px;
+ .form-group {
+ margin-bottom: 0;
- @media (min-width: $screen-sm-min) {
- display: -webkit-flex;
- display: flex;
- margin-top: 3px;
+ @include media-breakpoint-down(sm) {
+ display: block;
+ margin-left: 5px;
}
}
.btn-remove {
width: 100%;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
width: auto;
}
}
&.existing-title {
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
float: left;
}
}
}
.member-form-control {
- @media (max-width: $screen-xs-max) {
- padding-bottom: 5px;
+ @include media-breakpoint-down(sm) {
+ width: $dropdown-member-form-control-width;
margin-left: 0;
+ padding-bottom: 5px;
+ }
+
+ @include media-breakpoint-down(xs) {
margin-right: 0;
+ width: auto;
}
}
@@ -73,7 +81,7 @@
.member-search-form {
position: relative;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
float: right;
}
@@ -86,7 +94,7 @@
width: 100%;
}
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
margin-top: 0;
width: 155px;
}
@@ -96,7 +104,7 @@
width: 100%;
padding-right: 35px;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
width: 250px;
}
}
@@ -113,10 +121,6 @@
background: transparent;
border: 0;
outline: 0;
-
- @media (min-width: $screen-sm-min) {
- right: 160px;
- }
}
.flex-project-members-panel {
@@ -125,7 +129,7 @@
align-items: center;
justify-content: center;
- @media (max-width: $screen-sm-min) {
+ @include media-breakpoint-down(sm) {
display: block;
.flex-project-title {
@@ -141,7 +145,7 @@
text-overflow: ellipsis;
}
- .badge {
+ .badge.badge-pill {
height: 17px;
line-height: 16px;
margin-right: 5px;
@@ -156,14 +160,14 @@
}
}
-.panel {
- .panel-heading {
- .badge {
+.card {
+ .card-header {
+ .badge.badge-pill {
margin-top: 0;
}
- @media (max-width: $screen-sm-min) {
- .badge {
+ @include media-breakpoint-down(sm) {
+ .badge.badge-pill {
margin-right: 0;
margin-left: 0;
}
@@ -203,11 +207,7 @@
align-self: flex-start;
}
- .form-horizontal ~ .btn {
- margin-right: 0;
- }
-
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
display: block;
.controls > .btn {
@@ -216,6 +216,12 @@
display: block;
}
+ .controls > .btn:last-child {
+ margin-left: 5px;
+ margin-right: 5px;
+ width: auto;
+ }
+
.form-control {
width: 100%;
}
@@ -228,14 +234,10 @@
.member-controls {
margin-top: 5px;
}
-
- .form-horizontal {
- margin-top: 10px;
- }
}
}
-.panel-mobile {
+.card-mobile {
.content-list.members-list li {
display: block;
@@ -255,10 +257,6 @@
margin-top: 0;
}
- .form-horizontal {
- display: block;
- }
-
.member-form-control {
margin: 5px 0;
}
diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss
index 3d5ed9ef3c5..e76525fdbf6 100644
--- a/app/assets/stylesheets/pages/merge_conflicts.scss
+++ b/app/assets/stylesheets/pages/merge_conflicts.scss
@@ -291,7 +291,7 @@ $colors: (
}
.resolve-info {
- @media (max-width: $screen-md-max) {
+ @media(max-width: map-get($grid-breakpoints, lg)-1) {
margin-bottom: $gl-padding;
}
}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 3581dd36a10..2af79c511ba 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -15,16 +15,38 @@
}
}
+.mr-widget-heading {
+ position: relative;
+ border: 1px solid $border-color;
+ border-radius: 4px;
+
+ &:not(.deploy-heading)::before {
+ content: '';
+ border-left: 1px solid $theme-gray-200;
+ position: absolute;
+ left: 32px;
+ top: -17px;
+ height: 16px;
+ }
+}
+
+.mr-section-container {
+ border: 1px solid $border-color;
+ border-radius: $border-radius-default;
+ border-top: 0;
+}
+
+.mr-widget-heading,
+.mr-widget-section,
+.mr-widget-footer {
+ padding: $gl-padding;
+}
+
.mr-state-widget {
color: $gl-text-color;
- border: 1px solid $border-color;
- border-radius: 2px;
- line-height: 28px;
- .mr-widget-heading,
.mr-widget-section,
.mr-widget-footer {
- padding: $gl-padding;
border-top: solid 1px $border-color;
}
@@ -51,12 +73,6 @@
opacity: 0.3;
}
- &.btn-xs {
- line-height: 1;
- padding: 5px 10px;
- margin-top: 1px;
- }
-
&.dropdown-toggle {
.fa {
color: inherit;
@@ -100,10 +116,8 @@
.modify-merge-commit-link {
padding: 0;
-
background-color: transparent;
border: 0;
-
color: $gl-text-color;
&:hover,
@@ -130,10 +144,17 @@
.ci-widget {
color: $gl-text-color;
display: flex;
+ align-items: center;
+ justify-content: space-between;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
flex-wrap: wrap;
}
+
+ .ci-widget-content {
+ display: flex;
+ align-items: center;
+ }
}
.mr-widget-icon {
@@ -142,8 +163,6 @@
}
.ci-status-icon svg {
- width: $status-icon-size;
- height: $status-icon-size;
margin: 3px 0;
position: relative;
overflow: visible;
@@ -151,8 +170,6 @@
}
.mr-widget-pipeline-graph {
- padding: 0 4px;
-
.dropdown-menu {
z-index: 300;
}
@@ -163,7 +180,7 @@
}
.normal {
- line-height: 28px;
+ flex: 1;
}
.capitalize {
@@ -174,7 +191,7 @@
@extend .ref-name;
color: $gl-text-color;
- font-weight: $gl-font-weight-bold;
+ font-weight: normal;
overflow: hidden;
word-break: break-all;
@@ -198,6 +215,8 @@
}
.mr-widget-body {
+ line-height: 28px;
+
@include clearfix;
&.media > *:first-child {
@@ -282,7 +301,7 @@
display: inline-block;
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
p {
font-size: 13px;
}
@@ -483,29 +502,73 @@
.mr-source-target {
display: flex;
flex-wrap: wrap;
- justify-content: space-between;
- align-items: center;
- background-color: $gray-light;
- border-radius: $border-radius-default $border-radius-default 0 0;
- padding: $gl-padding / 2 $gl-padding;
+ border-radius: $border-radius-default;
+ padding: $gl-padding;
+ border: 1px solid $border-color;
+ min-height: 69px;
+
+ @include media-breakpoint-up(md) {
+ align-items: center;
+ }
.dropdown-toggle .fa {
color: $gl-text-color;
}
+
+ .git-merge-icon-container {
+ border: 1px solid $theme-gray-400;
+ border-radius: 50%;
+ height: 32px;
+ width: 32px;
+ color: $theme-gray-700;
+ line-height: 28px;
+
+ .ic-git-merge {
+ vertical-align: middle;
+ width: 31px;
+ }
+ }
+
+ .git-merge-container {
+ justify-content: space-between;
+ flex: 1;
+ flex-direction: row;
+ align-items: center;
+
+ @include media-breakpoint-down(md) {
+ flex-direction: column;
+ align-items: flex-start;
+
+ .branch-actions {
+ margin-top: 16px;
+ }
+ }
+
+ @include media-breakpoint-up(lg) {
+ .branch-actions {
+ align-self: center;
+ }
+ }
+ }
+
+ .diverged-commits-count {
+ color: $gl-text-color-secondary;
+ font-size: 12px;
+ }
}
-.panel-new-merge-request {
- .panel-heading {
+.card-new-merge-request {
+ .card-header {
padding: 5px 10px;
font-weight: $gl-font-weight-bold;
line-height: 25px;
}
- .panel-body {
+ .card-body {
padding: 10px 5px;
}
- .panel-footer {
+ .card-footer {
padding: 0;
.btn {
@@ -519,7 +582,7 @@
}
.item-title {
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
width: 45%;
}
}
@@ -550,7 +613,7 @@
margin-bottom: 0;
}
- @media (min-width: $screen-xs-min) {
+ @include media-breakpoint-up(xs) {
float: left;
width: 50%;
margin-bottom: 0;
@@ -605,14 +668,12 @@
position: relative;
background: $gray-light;
color: $gl-text-color;
- z-index: 199;
.mr-version-menus-container {
- display: -webkit-flex;
display: flex;
- -webkit-align-items: center;
align-items: center;
padding: 16px;
+ z-index: 199;
}
.content-block {
@@ -648,7 +709,7 @@
background-color: $white-light;
border-bottom: 1px solid $border-color;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
position: sticky;
position: -webkit-sticky;
}
@@ -657,7 +718,7 @@
left: 0;
transition: right .15s;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
right: 0;
}
@@ -678,6 +739,7 @@
.merge-request-tabs {
display: flex;
+ flex-wrap: nowrap;
margin-bottom: 0;
padding: 0;
}
@@ -700,7 +762,7 @@
display: flex;
justify-content: space-between;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
flex-direction: column-reverse;
}
}
@@ -727,35 +789,64 @@
}
.deploy-heading {
+ margin-top: -19px;
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+ background-color: $gray-light;
+
+ @include media-breakpoint-up(md) {
+ padding: $gl-padding-8 $gl-padding;
+ }
+
.media-body {
min-width: 0;
+ font-size: 12px;
+ margin-left: 48px;
}
}
.deploy-body {
display: flex;
+ align-items: center;
flex-wrap: wrap;
- @media (min-width: $screen-xs) {
+ @include media-breakpoint-up(xs) {
flex-wrap: nowrap;
white-space: nowrap;
}
+ @include media-breakpoint-down(md) {
+ flex-direction: column;
+ align-items: flex-start;
+
+ .deployment-info {
+ margin-bottom: $gl-padding;
+ }
+ }
+
> *:not(:last-child) {
margin-right: .3em;
}
-}
-.deploy-link {
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- min-width: 100px;
- max-width: 150px;
+ svg {
+ vertical-align: text-top;
+ }
- @media (min-width: $screen-xs) {
- min-width: 0;
- max-width: 100%;
+ .deployment-info {
+ flex: 1;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ min-width: 100px;
+
+ @include media-breakpoint-up(xs) {
+ min-width: 0;
+ max-width: 100%;
+ }
+ }
+
+ .btn svg {
+ fill: $theme-gray-700;
}
}
@@ -775,3 +866,33 @@
}
}
}
+
+.ci-widget-container {
+ justify-content: space-between;
+ flex: 1;
+ flex-direction: row;
+
+ @include media-breakpoint-down(md) {
+ flex-direction: column;
+
+ .stage-cell .stage-container {
+ margin-top: 16px;
+ }
+
+ .dropdown .mini-pipeline-graph-dropdown-menu.dropdown-menu {
+ transform: initial;
+ }
+ }
+
+ .coverage {
+ font-size: 12px;
+ color: $theme-gray-700;
+ line-height: initial;
+ }
+
+ .mini-pipeline-graph-dropdown-toggle,
+ .stage-cell .mini-pipeline-graph-dropdown-toggle svg {
+ height: $ci-action-icon-size-lg;
+ width: $ci-action-icon-size-lg;
+ }
+}
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index bac3b70c734..46437ce5841 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -3,8 +3,20 @@
}
.milestones {
+ padding: $gl-padding-8;
+ margin-top: $gl-padding-8;
+ border-radius: $border-radius-default;
+ background-color: $theme-gray-100;
+
.milestone {
- padding: 10px 16px;
+ border: 0;
+ padding: $gl-padding-top $gl-padding;
+ border-radius: $border-radius-default;
+ background-color: $white-light;
+
+ &:not(:last-child) {
+ margin-bottom: $gl-padding-4;
+ }
h4 {
font-weight: $gl-font-weight-bold;
@@ -13,6 +25,24 @@
.progress {
width: 100%;
height: 6px;
+ margin-bottom: $gl-padding-4;
+ }
+
+ .milestone-progress {
+ a {
+ color: $gl-link-color;
+ }
+ }
+
+ .status-box {
+ font-size: $tooltip-font-size;
+ margin-top: 0;
+ margin-right: $gl-padding-4;
+
+ @include media-breakpoint-down(xs) {
+ line-height: unset;
+ padding: $gl-padding-4 $gl-input-padding;
+ }
}
}
}
@@ -31,7 +61,7 @@
}
}
- .panel-heading {
+ .card-header {
line-height: $line-height-base;
padding: 14px 16px;
display: -webkit-flex;
@@ -145,7 +175,7 @@
padding: 20px 0;
}
-@media (max-width: $screen-xs-max) {
+@include media-breakpoint-down(xs) {
.milestone-actions {
@include clearfix();
padding-top: $gl-vert-padding;
@@ -181,7 +211,7 @@
width: 100%;
}
- @media (min-width: $screen-xs-min) {
+ @include media-breakpoint-up(xs) {
.milestone-buttons .verbose {
display: inline;
}
@@ -229,7 +259,11 @@
}
}
-@media (max-width: $screen-xs-max) {
+.milestone-range {
+ color: $gl-text-color-tertiary;
+}
+
+@include media-breakpoint-down(xs) {
.milestone-banner-text,
.milestone-banner-link {
display: inline;
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 4a528bc2bb1..5e5696b1602 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -51,7 +51,7 @@
}
.note-image-attach {
- @extend .col-md-4;
+ @extend .col-lg-4;
margin-left: 45px;
float: none;
}
@@ -93,7 +93,7 @@
-webkit-flex-flow: row wrap;
width: 100%;
- .pull-right {
+ .float-right {
// Flexbox quirk to make sure right-aligned items stay right-aligned.
margin-left: auto;
}
@@ -129,7 +129,7 @@
.icon svg {
position: relative;
top: 2px;
- margin-right: $btn-xs-side-margin;
+ margin-right: $btn-margin-5;
width: $gl-font-size;
height: $gl-font-size;
fill: $orange-600;
@@ -185,12 +185,12 @@
}
.notes.notes-form > li.timeline-entry {
- @include notes-media('max', $screen-sm-max) {
+ @include notes-media('max', map-get($grid-breakpoints, md) - 1) {
padding: 0;
}
.timeline-content {
- @include notes-media('max', $screen-sm-max) {
+ @include notes-media('max', map-get($grid-breakpoints, md) - 1) {
margin: 0;
}
}
@@ -247,22 +247,6 @@
}
.discussion-with-resolve-btn {
- display: table;
- width: 100%;
- border-collapse: separate;
- table-layout: auto;
-
- .btn-group {
- display: table-cell;
- float: none;
- width: 1%;
-
- &:first-child {
- width: 100%;
- padding-right: 5px;
- }
- }
-
.discussion-actions {
display: table;
@@ -326,7 +310,7 @@
outline: 0;
}
- @media (min-width: $screen-md-min) {
+ @include media-breakpoint-up(md) {
float: left;
margin-right: $gl-padding;
@@ -350,13 +334,13 @@
line-height: 16px;
margin-top: 2px;
- @media (min-width: $screen-md-min) {
+ @include media-breakpoint-up(md) {
float: left;
}
}
.note-form-actions {
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
.btn {
float: none;
width: 100%;
@@ -375,7 +359,7 @@
left: 127px;
top: 2px;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
position: relative;
top: 0;
left: 0;
@@ -410,7 +394,7 @@
width: 298px;
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
display: flex;
width: 100%;
margin-bottom: 10px;
@@ -432,7 +416,7 @@
.uploading-container {
float: right;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
float: left;
margin-top: 5px;
}
@@ -444,7 +428,7 @@
}
.uploading-error-message {
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
&::after {
content: "\a";
white-space: pre;
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index feee964f9bb..32d14049067 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -3,9 +3,17 @@
*/
@-webkit-keyframes targe3-note {
- from { background: $note-targe3-outside; }
- 50% { background: $note-targe3-inside; }
- to { background: $note-targe3-outside; }
+ from {
+ background: $note-targe3-outside;
+ }
+
+ 50% {
+ background: $note-targe3-inside;
+ }
+
+ to {
+ background: $note-targe3-outside;
+ }
}
ul.notes {
@@ -22,7 +30,7 @@ ul.notes {
.discussion-body {
padding-top: 8px;
- .panel {
+ .card {
margin-bottom: 0;
}
}
@@ -33,16 +41,18 @@ ul.notes {
.diff-content {
overflow: visible;
+ padding: 0;
}
}
- > li { // .timeline-entry
+ > li {
+ // .timeline-entry
padding: 0;
display: block;
position: relative;
border-bottom: 0;
- @include notes-media('min', $screen-sm-min) {
+ @include notes-media('min', map-get($grid-breakpoints, sm)) {
padding-left: $note-icon-gutter-width;
}
@@ -66,7 +76,7 @@ ul.notes {
}
.timeline-icon {
- @include notes-media('min', $screen-sm-min) {
+ @include notes-media('min', map-get($grid-breakpoints, sm)) {
margin-left: -$note-icon-gutter-width;
}
}
@@ -74,7 +84,7 @@ ul.notes {
.timeline-content {
margin-left: $note-icon-gutter-width;
- @include notes-media('min', $screen-sm-min) {
+ @include notes-media('min', map-get($grid-breakpoints, sm)) {
margin-left: 0;
}
}
@@ -153,8 +163,7 @@ ul.notes {
}
.note-header {
-
- @include notes-media('max', $screen-xs-min) {
+ @include notes-media('max', map-get($grid-breakpoints, xs)) {
.inline {
display: block;
}
@@ -217,7 +226,7 @@ ul.notes {
.timeline-icon {
float: left;
- @include notes-media('min', $screen-sm-min) {
+ @include notes-media('min', map-get($grid-breakpoints, sm)) {
margin-left: 0;
width: auto;
}
@@ -231,7 +240,7 @@ ul.notes {
}
.timeline-content {
- @include notes-media('min', $screen-sm-min) {
+ @include notes-media('min', map-get($grid-breakpoints, sm)) {
margin-left: 30px;
}
}
@@ -245,7 +254,6 @@ ul.notes {
.system-note-commit-list-toggler {
color: $gl-link-color;
- display: none;
padding: 10px 0 0;
cursor: pointer;
position: relative;
@@ -423,7 +431,7 @@ ul.notes {
}
.note-header-author-name {
- @include notes-media('max', $screen-xs-max) {
+ @include notes-media('max', map-get($grid-breakpoints, sm) - 1) {
display: none;
}
}
@@ -431,7 +439,7 @@ ul.notes {
.note-headline-light {
display: inline;
- @include notes-media('max', $screen-xs-min) {
+ @include notes-media('max', map-get($grid-breakpoints, xs)) {
display: block;
}
}
@@ -486,7 +494,7 @@ ul.notes {
margin-left: 10px;
color: $gray-darkest;
- @include notes-media('max', $screen-xs-max) {
+ @include notes-media('max', map-get($grid-breakpoints, sm) - 1) {
float: none;
margin-left: 0;
}
@@ -624,15 +632,17 @@ ul.notes {
.line_holder .is-over:not(.no-comment-btn) {
.add-diff-note {
opacity: 1;
+ z-index: 101;
}
}
.add-diff-note {
@include btn-comment-icon;
opacity: 0;
- margin-top: -2px;
margin-left: -55px;
position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
z-index: 10;
}
@@ -661,14 +671,13 @@ ul.notes {
background-color: $white-light;
}
-
a {
color: $gl-link-color;
}
}
.line-resolve-all-container {
- @include notes-media('min', $screen-sm-min) {
+ @include notes-media('min', map-get($grid-breakpoints, sm)) {
margin-right: 0;
padding-left: $gl-padding;
}
@@ -712,7 +721,7 @@ ul.notes {
.line-resolve-all {
vertical-align: middle;
display: inline-block;
- padding: 6px 10px;
+ padding: 5px 10px 6px;
background-color: $gray-light;
border: 1px solid $border-color;
border-radius: $border-radius-default;
@@ -767,3 +776,44 @@ ul.notes {
height: auto;
}
}
+
+// Vue refactored diff discussion adjustments
+.files {
+ .diff-discussions {
+ .note-discussion.timeline-entry {
+ padding-left: 0;
+
+ &:last-child {
+ border-bottom: 0;
+ }
+
+ > .timeline-entry-inner {
+ padding: 0;
+
+ > .timeline-content {
+ margin-left: 0;
+ }
+
+ > .timeline-icon {
+ display: none;
+ }
+ }
+
+ .discussion-body {
+ padding-top: 0;
+
+ .discussion-wrapper {
+ border-color: transparent;
+ }
+ }
+ }
+ }
+
+ .diff-comment-form {
+ display: block;
+ }
+
+ .add-diff-note svg {
+ margin-top: 4px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/notifications.scss b/app/assets/stylesheets/pages/notifications.scss
index bdf07a99daf..e98cb444f0a 100644
--- a/app/assets/stylesheets/pages/notifications.scss
+++ b/app/assets/stylesheets/pages/notifications.scss
@@ -2,7 +2,7 @@
line-height: 34px;
.dropdown-menu {
- @extend .dropdown-menu-align-right;
+ @extend .dropdown-menu-right;
}
}
diff --git a/app/assets/stylesheets/pages/pipeline_schedules.scss b/app/assets/stylesheets/pages/pipeline_schedules.scss
index bc7fa8a26d9..86e70955389 100644
--- a/app/assets/stylesheets/pages/pipeline_schedules.scss
+++ b/app/assets/stylesheets/pages/pipeline_schedules.scss
@@ -69,7 +69,7 @@
.cron-preset-radio-input {
display: inline-block;
- @media (max-width: $screen-md-max) {
+ @include media-breakpoint-down(md) {
display: block;
margin: 0 0 5px 5px;
}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 1264d977b2f..b68c89c25d8 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -1,3 +1,30 @@
+@mixin flat-connector-before($length: 44px) {
+ &::before {
+ content: '';
+ position: absolute;
+ top: 48%;
+ left: -$length;
+ border-top: 2px solid $border-color;
+ width: $length;
+ height: 1px;
+ }
+}
+
+@mixin build-content($border-radius: 30px) {
+ display: inline-block;
+ padding: 8px 10px 9px;
+ width: 100%;
+ border: 1px solid $border-color;
+ border-radius: $border-radius;
+ background-color: $white-light;
+
+ &:hover {
+ background-color: $stage-hover-bg;
+ border: 1px solid $dropdown-toggle-active-border-color;
+ color: $gl-text-color;
+ }
+}
+
.pipelines {
.stage {
max-width: 90px;
@@ -9,6 +36,7 @@
}
.table-holder {
+ overflow: unset;
width: 100%;
}
@@ -16,13 +44,13 @@
margin: 0;
white-space: normal;
- @media (max-width: $screen-sm-max) {
+ @include media-breakpoint-down(sm) {
justify-content: flex-end;
}
}
.ci-table {
- .label {
+ .badge {
margin-bottom: 3px;
}
@@ -82,7 +110,7 @@
}
}
-@media (max-width: $screen-md-max) {
+@include media-breakpoint-down(md) {
.content-list {
&.builds-content-list {
width: 100%;
@@ -150,14 +178,14 @@
color: $gl-link-color;
}
- .label {
+ .badge {
margin-right: 4px;
}
.label-container {
font-size: 0;
- .label {
+ .badge {
margin-top: 5px;
}
}
@@ -226,7 +254,7 @@
.stage-cell {
&.table-section {
- @media (min-width: $screen-md-min) {
+ @include media-breakpoint-up(md) {
min-width: 160px; /* Hack alert: Without this the mini graph pipeline won't work properly*/
margin-right: -4px;
}
@@ -273,6 +301,21 @@
border-bottom: 2px solid $border-color;
}
}
+
+ //delete when all pipelines are updated to new size
+ &.mr-widget-pipeline-stages {
+ + .stage-container {
+ margin-left: 4px;
+ }
+
+ &:not(:last-child) {
+ &::after {
+ width: 4px;
+ right: -4px;
+ top: 11px;
+ }
+ }
+ }
}
}
@@ -294,18 +337,17 @@
}
.build-failures {
+ th {
+ border-top: 0;
+ }
+
.build-state {
padding: 20px 2px;
.build-name {
- float: right;
font-weight: $gl-font-weight-normal;
}
- .ci-status-icon-failed svg {
- vertical-align: middle;
- }
-
.stage {
color: $gl-text-color-secondary;
font-weight: $gl-font-weight-normal;
@@ -317,6 +359,81 @@
border: 0;
line-height: initial;
}
+
+ .build-trace-row td {
+ border-top: 0;
+ border-bottom-width: 1px;
+ border-bottom-style: solid;
+ padding-top: 0;
+ }
+
+ .build-trace {
+ width: 100%;
+ text-align: left;
+ margin-top: $gl-padding;
+ }
+
+ .build-name {
+ width: 196px;
+
+ a {
+ font-weight: $gl-font-weight-bold;
+ color: $gl-text-color;
+ text-decoration: none;
+
+ &:focus,
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+ }
+
+ .build-actions {
+ width: 70px;
+ text-align: right;
+ }
+
+ .build-stage {
+ width: 140px;
+ }
+
+ .ci-status-icon-failed {
+ padding: 10px 0 10px 12px;
+ width: 12px + 24px; // padding-left + svg width
+ }
+
+ .build-icon svg {
+ width: 24px;
+ height: 24px;
+ vertical-align: middle;
+ }
+
+ .build-state,
+ .build-trace-row {
+ > td:last-child {
+ padding-right: 0;
+ }
+ }
+
+ @include media-breakpoint-down(sm) {
+ td:empty {
+ display: none;
+ }
+
+ .ci-table {
+ margin-top: 2 * $gl-padding;
+ }
+
+ .build-trace-container {
+ padding-top: $gl-padding;
+ padding-bottom: $gl-padding;
+ }
+
+ .build-trace {
+ margin-bottom: 0;
+ margin-top: 0;
+ }
+ }
}
.pipeline-tab-content {
@@ -357,14 +474,8 @@
&:not(:first-child) {
margin-left: 44px;
- .left-connector::before {
- content: '';
- position: absolute;
- top: 48%;
- left: -44px;
- border-top: 2px solid $border-color;
- width: 44px;
- height: 1px;
+ .left-connector {
+ @include flat-connector-before;
}
}
}
@@ -479,12 +590,7 @@
}
.build-content {
- display: inline-block;
- padding: 8px 10px 9px;
- width: 100%;
- border: 1px solid $border-color;
- border-radius: 30px;
- background-color: $white-light;
+ @include build-content();
}
a.build-content:hover,
@@ -622,8 +728,7 @@
}
}
-// Dropdown button in mini pipeline graph
-button.mini-pipeline-graph-dropdown-toggle {
+@mixin mini-pipeline-item() {
border-radius: 100px;
background-color: $white-light;
border-width: 1px;
@@ -636,30 +741,6 @@ button.mini-pipeline-graph-dropdown-toggle {
position: relative;
vertical-align: middle;
- > .fa.fa-caret-down {
- position: absolute;
- left: 20px;
- top: 5px;
- display: inline-block;
- visibility: hidden;
- opacity: 0;
- color: inherit;
- font-size: 12px;
- transition: visibility 0.1s, opacity 0.1s linear;
- }
-
- &:active,
- &:focus,
- &:hover {
- outline: none;
- width: 35px;
-
- .fa.fa-caret-down {
- visibility: visible;
- opacity: 1;
- }
- }
-
// Dropdown button animation in mini pipeline graph
&.ci-status-icon-success {
@include mini-pipeline-graph-color($green-100, $green-500, $green-600);
@@ -691,6 +772,35 @@ button.mini-pipeline-graph-dropdown-toggle {
}
}
+// Dropdown button in mini pipeline graph
+button.mini-pipeline-graph-dropdown-toggle {
+ @include mini-pipeline-item();
+
+ > .fa.fa-caret-down {
+ position: absolute;
+ left: 20px;
+ top: 5px;
+ display: inline-block;
+ visibility: hidden;
+ opacity: 0;
+ color: inherit;
+ font-size: 12px;
+ transition: visibility 0.1s, opacity 0.1s linear;
+ }
+
+ &:active,
+ &:focus,
+ &:hover {
+ outline: none;
+ width: 35px;
+
+ .fa.fa-caret-down {
+ visibility: visible;
+ opacity: 1;
+ }
+ }
+}
+
/**
Action icons inside dropdowns:
- mini graph in pipelines table
@@ -744,7 +854,7 @@ button.mini-pipeline-graph-dropdown-toggle {
}
}
- // SVGs in the commit widget and mr widget
+ // SVGs in the commit widget and mr widget
a.ci-action-icon-container.ci-action-icon-wrapper svg {
top: 2px;
}
@@ -800,7 +910,7 @@ button.mini-pipeline-graph-dropdown-toggle {
display: block;
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
max-width: 60%;
}
}
@@ -887,7 +997,7 @@ button.mini-pipeline-graph-dropdown-toggle {
transform: translate(-50%, 0);
border-width: 0 5px 6px;
- @media (max-width: $screen-sm-max) {
+ @include media-breakpoint-down(sm) {
left: 100%;
margin-left: -12px;
}
@@ -906,10 +1016,10 @@ button.mini-pipeline-graph-dropdown-toggle {
/**
* Center dropdown menu in mini graph
*/
- &.dropdown-menu {
+ .dropdown &.dropdown-menu {
transform: translate(-80%, 0);
- @media (min-width: $screen-md-min) {
+ @media (min-width: map-get($grid-breakpoints, md)) {
transform: translate(-50%, 0);
right: auto;
left: 50%;
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index b199f9876d3..5d0d59e12f2 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -5,7 +5,7 @@
}
.avatar-image {
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
float: left;
margin-bottom: 0;
}
@@ -119,7 +119,7 @@
.key-list-item {
.key-list-item-info {
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
float: left;
}
}
@@ -188,7 +188,7 @@
.modal-dialog {
width: 380px;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
width: auto;
}
@@ -242,7 +242,7 @@
left: 0;
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
.cover-block {
padding-top: 20px;
}
@@ -352,7 +352,7 @@ table.u2f-registrations {
}
}
- @media(max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
text-align: center;
.bordered-box {
@@ -405,7 +405,7 @@ table.u2f-registrations {
margin-right: $gl-padding / 4;
}
- .label-verification-status {
+ .badge-verification-status {
border-width: 1px;
border-style: solid;
@@ -414,7 +414,7 @@ table.u2f-registrations {
}
&.unverified {
- @include status-color($gray-dark, $gray, $common-gray-dark);
+ @include status-color($gray-dark, color("gray"), $common-gray-dark);
}
}
}
diff --git a/app/assets/stylesheets/pages/profiles/preferences.scss b/app/assets/stylesheets/pages/profiles/preferences.scss
index 68d40b56133..a353f301d07 100644
--- a/app/assets/stylesheets/pages/profiles/preferences.scss
+++ b/app/assets/stylesheets/pages/profiles/preferences.scss
@@ -1,25 +1,3 @@
-@mixin application-theme-preview($color-1, $color-2, $color-3, $color-4) {
- .one {
- background-color: $color-1;
- border-top-left-radius: $border-radius-default;
- }
-
- .two {
- background-color: $color-2;
- border-top-right-radius: $border-radius-default;
- }
-
- .three {
- background-color: $color-3;
- border-bottom-left-radius: $border-radius-default;
- }
-
- .four {
- background-color: $color-4;
- border-bottom-right-radius: $border-radius-default;
- }
-}
-
.multi-file-editor-options {
label {
margin-right: 20px;
@@ -38,49 +16,67 @@
.application-theme {
label {
- margin-right: 20px;
+ margin: 0 $gl-padding-32 $gl-padding 0;
text-align: center;
}
.preview {
font-size: 0;
- margin-bottom: 10px;
+ height: 48px;
+ border-radius: 4px;
+ min-width: 112px;
+ margin-bottom: $gl-padding-8;
+
+ &.ui-indigo {
+ background-color: $indigo-900;
+ }
- &.indigo {
- @include application-theme-preview($indigo-900, $indigo-700, $indigo-800, $indigo-500);
+ &.ui-light-indigo {
+ background-color: $indigo-700;
}
- &.dark {
- @include application-theme-preview($theme-gray-900, $theme-gray-700, $theme-gray-800, $theme-gray-600);
+ &.ui-blue {
+ background-color: $theme-blue-900;
}
- &.light {
- @include application-theme-preview($theme-gray-600, $theme-gray-200, $theme-gray-400, $theme-gray-100);
+ &.ui-light-blue {
+ background-color: $theme-light-blue-700;
}
- &.blue {
- @include application-theme-preview($theme-blue-900, $theme-blue-700, $theme-blue-800, $theme-blue-500);
+ &.ui-green {
+ background-color: $theme-green-900;
}
- &.green {
- @include application-theme-preview($theme-green-900, $theme-green-700, $theme-green-800, $theme-green-500);
+ &.ui-light-green {
+ background-color: $theme-light-green-700;
+ }
+
+ &.ui-red {
+ background-color: $theme-red-900;
+ }
+
+ &.ui-light-red {
+ background-color: $theme-light-red-700;
+ }
+
+ &.ui-dark {
+ background-color: $theme-gray-900;
+ }
+
+ &.ui-light {
+ background-color: $theme-gray-200;
}
}
.preview-row {
display: block;
}
-
- .quadrant {
- display: inline-block;
- height: 50px;
- width: 80px;
- }
}
.syntax-theme {
label {
- margin-right: 20px;
+ margin-right: $gl-padding-32;
+ margin-bottom: $gl-padding;
text-align: center;
.preview {
@@ -89,7 +85,6 @@
img {
border-radius: 4px;
-
max-width: 100%;
}
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 53d021086bf..aa83e5bdebc 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -9,7 +9,7 @@
.new_project,
.edit-project,
.import-project {
- .help-block {
+ .form-text.text-muted {
margin-bottom: 10px;
}
@@ -34,7 +34,7 @@
}
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
.input-group > div {
&:last-child {
margin-bottom: 0;
@@ -46,7 +46,8 @@
}
}
- .input-group-addon {
+ .input-group-prepend,
+ .input-group-append {
overflow: hidden;
text-overflow: ellipsis;
line-height: unset;
@@ -82,7 +83,7 @@
border: 1px solid $border-color;
padding: 10px 32px;
- @media (max-width: $screen-xs-min) {
+ @include media-breakpoint-down(xs) {
padding: 10px 20px;
}
}
@@ -134,7 +135,7 @@
max-width: 400px;
}
- @media (max-width: $screen-xs-min) {
+ @include media-breakpoint-down(xs) {
padding-left: 20px;
}
}
@@ -144,7 +145,7 @@
padding-top: 24px;
padding-bottom: 24px;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
border-bottom: 1px solid $border-color;
}
@@ -230,11 +231,11 @@
}
.notification-dropdown .dropdown-menu {
- @extend .dropdown-menu-align-right;
+ @extend .dropdown-menu-right;
}
.download-button {
- @media (max-width: $screen-md-max) {
+ @include media-breakpoint-down(md) {
margin-left: 0;
}
}
@@ -353,12 +354,6 @@
min-width: 200px;
}
-.deploy-keys {
- .scrolling-tabs-container {
- position: relative;
- }
-}
-
.deploy-key {
// Ensure that the fingerprint does not overflow on small screens
.fingerprint {
@@ -444,11 +439,11 @@
height: 200px;
width: calc((100% / 2) - #{$gl-padding * 2});
- @media (min-width: $screen-md-min) {
+ @include media-breakpoint-up(md) {
width: calc((100% / 4) - #{$gl-padding * 2});
}
- @media (min-width: $screen-lg-min) {
+ @include media-breakpoint-up(lg) {
width: calc((100% / 5) - #{$gl-padding * 2});
}
@@ -502,6 +497,12 @@
&:not(:first-child) {
border-top: 1px solid $border-color;
}
+
+ .btn-template-icon {
+ position: absolute;
+ left: $gl-padding;
+ top: $gl-padding;
+ }
}
.template-title {
@@ -519,12 +520,6 @@
}
}
- svg {
- position: absolute;
- left: $gl-padding;
- top: $gl-padding;
- }
-
.project-fields-form {
display: none;
@@ -535,33 +530,23 @@
}
.template-input-group {
- position: relative;
-
- @media (min-width: $screen-sm-min) {
- display: flex;
- }
-
- .input-group-addon {
+ .input-group-prepend {
flex: 1;
- text-align: left;
- padding-left: ($gl-padding * 3);
- background-color: $white-light;
}
- .selected-template {
- line-height: 20px;
+ .input-group-text {
+ width: 100%;
+ background-color: $white-light;
}
.selected-icon {
+ padding-right: $gl-padding;
+
svg {
display: none;
top: 7px;
height: 20px;
width: 20px;
-
- &.active {
- display: block;
- }
}
}
}
@@ -601,12 +586,12 @@
margin: 0 auto 4px;
font-size: 24px;
- @media (min-width: $screen-xs-max) {
+ @media (min-width: map-get($grid-breakpoints, sm)-1) {
top: 0;
}
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
.btn-template-icon {
display: inline-block;
height: 14px;
@@ -625,31 +610,31 @@
.create-project-options {
display: flex;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
display: block;
}
.first-column {
- @media (min-width: $screen-xs-min) {
+ @include media-breakpoint-up(xs) {
max-width: 50%;
padding-right: 30px;
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
max-width: 100%;
width: 100%;
}
}
.second-column {
- @media (min-width: $screen-xs-min) {
+ @include media-breakpoint-up(xs) {
width: 50%;
flex: 1;
padding-left: 30px;
position: relative;
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
max-width: 100%;
width: 100%;
padding-left: 0;
@@ -657,7 +642,7 @@
}
// Mobile
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
padding-top: 30px;
}
@@ -677,7 +662,7 @@
line-height: 20px;
// Mobile
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
left: 50%;
top: 0;
transform: translateX(-50%);
@@ -697,7 +682,7 @@
top: 0;
// Mobile
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
top: 10px;
left: 10px;
right: 10px;
@@ -735,7 +720,7 @@
vertical-align: top;
margin-top: 0;
- @media (min-width: $screen-lg-min) {
+ @include media-breakpoint-up(lg) {
float: right;
}
}
@@ -866,7 +851,7 @@ pre.light-well {
}
}
-.panel .projects-list li {
+.card .projects-list li {
padding: 10px 15px;
margin: 0;
}
@@ -888,11 +873,11 @@ pre.light-well {
.form-control {
@extend .monospace;
- background: $white-light;
+ background-color: $white-light;
+ border-color: $border-color;
font-size: 14px;
margin-left: -1px;
cursor: auto;
- width: 101%;
}
}
@@ -921,7 +906,8 @@ pre.light-well {
}
.project-tip-command {
- > .input-group-btn:first-child {
+ > .input-group-prepend:first-child,
+ > .input-group-append:first-child {
width: auto;
}
}
@@ -972,7 +958,7 @@ pre.light-well {
.dropdown-menu-projects {
width: 300px;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
width: 500px;
}
@@ -986,7 +972,7 @@ pre.light-well {
.inline-input-group {
width: 100%;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
width: 300px;
}
}
@@ -997,8 +983,8 @@ pre.light-well {
text-align: center;
margin-top: -20px;
- @media (min-width: $screen-sm-min) {
- margin-top: 0;
+ @include media-breakpoint-up(sm) {
+ margin: 0 $gl-padding-8;
width: auto;
}
}
@@ -1036,7 +1022,7 @@ pre.light-well {
}
&.form-group {
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
margin-bottom: 0;
}
}
@@ -1064,12 +1050,12 @@ pre.light-well {
.project-feature {
padding-top: 10px;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
padding-left: 45px;
}
&.nested {
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
padding-left: 90px;
}
}
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index 175d2779bb7..6e2b285285a 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -22,8 +22,8 @@
height: calc(100vh - #{$header-height});
margin-top: 0;
border-top: 1px solid $white-dark;
- border-bottom: 1px solid $white-dark;
padding-bottom: $ide-statusbar-height;
+ color: $gl-text-color;
&.is-collapsed {
.ide-file-list {
@@ -46,12 +46,8 @@
.file {
cursor: pointer;
- &.file-open {
- background: $white-normal;
- }
-
&.file-active {
- font-weight: $gl-font-weight-bold;
+ background: $theme-gray-100;
}
.ide-file-name {
@@ -59,7 +55,9 @@
white-space: nowrap;
text-overflow: ellipsis;
max-width: inherit;
- line-height: 22px;
+ line-height: 16px;
+ display: inline-block;
+ height: 18px;
svg {
vertical-align: middle;
@@ -87,12 +85,14 @@
.ide-new-btn {
display: none;
+
+ .btn {
+ padding: 2px 5px;
+ }
}
&:hover,
&:focus {
- background: $white-normal;
-
.ide-new-btn {
display: block;
}
@@ -184,7 +184,7 @@
svg {
position: relative;
- top: -1px;
+ top: -2px;
}
.ide-file-changed-icon {
@@ -282,8 +282,8 @@
}
.margin {
- background-color: $gray-light;
- border-right: 1px solid $white-normal;
+ background-color: $white-light;
+ border-right: 1px solid $theme-gray-100;
.line-insert {
border-right: 1px solid $line-added-dark;
@@ -304,6 +304,15 @@
.multi-file-editor-holder {
height: 100%;
min-height: 0;
+
+ &.is-readonly,
+ .editor.original {
+ .monaco-editor,
+ .monaco-editor-background,
+ .monaco-editor .inputarea.ime-input {
+ background-color: $theme-gray-50;
+ }
+ }
}
.preview-container {
@@ -336,7 +345,6 @@
img {
max-width: 90%;
- max-height: 90%;
}
.isZoomable {
@@ -380,7 +388,7 @@
.ide-status-bar {
border-top: 1px solid $white-dark;
- padding: $gl-bar-padding $gl-padding;
+ padding: 2px $gl-padding-8 0;
background: $white-light;
display: flex;
justify-content: space-between;
@@ -391,12 +399,19 @@
left: 0;
width: 100%;
+ font-size: 12px;
+ line-height: 22px;
+
+ * {
+ font-size: inherit;
+ }
+
> div + div {
padding-left: $gl-padding;
}
svg {
- vertical-align: middle;
+ vertical-align: sub;
}
}
@@ -452,9 +467,9 @@
width: auto;
margin-right: 0;
- a:hover,
- a:focus {
- text-decoration: none;
+ > a,
+ > button {
+ height: 60px;
}
}
@@ -535,32 +550,12 @@
margin-right: -$grid-size;
min-height: 60px;
- .multi-file-commit-list-item {
- margin-left: 0;
- margin-right: 0;
- }
-
- &.help-block {
+ &.form-text.text-muted {
margin-left: 0;
right: 0;
}
}
-.multi-file-commit-list-item {
- .multi-file-discard-btn {
- display: none;
- margin-top: -2px;
- margin-left: auto;
- color: $gl-link-color;
- }
-
- &:hover {
- .multi-file-discard-btn {
- display: flex;
- }
- }
-}
-
.multi-file-addition,
.multi-file-addition-solid {
color: $green-500;
@@ -590,7 +585,7 @@
}
}
-.multi-file-commit-list-item,
+.multi-file-commit-list-path,
.ide-file-list .file {
display: flex;
align-items: center;
@@ -602,16 +597,20 @@
&:hover,
&:focus {
- background: $white-normal;
+ background: $theme-gray-100;
+ }
+
+ &:active {
+ background: $theme-gray-200;
}
}
.multi-file-commit-list-path {
- padding: 0;
- background: none;
- border: 0;
- text-align: left;
- width: 100%;
+ cursor: pointer;
+
+ &.is-active {
+ background-color: $white-normal;
+ }
&:hover,
&:focus {
@@ -626,17 +625,23 @@
}
.multi-file-commit-list-file-path {
- @include str-truncated(100%);
-
- &:hover {
- text-decoration: underline;
- }
+ @include str-truncated(calc(100% - 30px));
&:active {
text-decoration: none;
}
}
+.multi-file-discard-btn {
+ top: 4px;
+ right: 8px;
+ bottom: 4px;
+
+ svg {
+ top: 0;
+ }
+}
+
.multi-file-commit-form {
position: relative;
background-color: $white-light;
@@ -712,9 +717,17 @@
}
.ide-new-btn {
+ .btn {
+ padding-top: 3px;
+ padding-bottom: 3px;
+ }
+
+ .dropdown {
+ display: flex;
+ }
+
.dropdown-toggle svg {
- margin-top: -2px;
- margin-bottom: 2px;
+ top: 0;
}
.dropdown-menu {
@@ -823,18 +836,20 @@
}
.ide-staged-action-btn {
- margin-left: auto;
- line-height: 22px;
+ width: 22px;
+ margin-left: -1px;
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+
+ > svg {
+ top: 0;
+ }
}
.ide-commit-file-count {
min-width: 22px;
- margin-left: auto;
background-color: $gray-light;
- border-radius: $border-radius-default;
border: 1px solid $white-dark;
- line-height: 20px;
- text-align: center;
}
.ide-commit-radios {
@@ -848,7 +863,7 @@
}
}
- .help-block {
+ .form-text.text-muted {
margin-top: 0;
line-height: 0;
}
@@ -871,6 +886,7 @@
border-top: 1px solid transparent;
border-bottom: 1px solid transparent;
outline: 0;
+ cursor: pointer;
svg {
margin: 0 auto;
@@ -903,6 +919,16 @@
width: 1px;
background: $white-light;
}
+
+ &.is-right {
+ padding-right: $gl-padding;
+ padding-left: $gl-padding + 1px;
+
+ &::after {
+ right: auto;
+ left: -1px;
+ }
+ }
}
}
@@ -946,7 +972,7 @@
height: 30px;
}
- .help-block {
+ .form-text.text-muted {
margin-top: 2px;
color: $blue-500;
cursor: pointer;
@@ -1082,10 +1108,6 @@
font-size: 12px;
}
-.ide-new-modal-label {
- line-height: 34px;
-}
-
.multi-file-commit-panel-success-message {
position: absolute;
top: 61px;
@@ -1108,7 +1130,12 @@
.ide-context-header {
.avatar {
- flex: 0 0 40px;
+ flex: 0 0 38px;
+ }
+
+ .ide-merge-requests-dropdown.dropdown-menu {
+ width: 385px;
+ max-height: initial;
}
}
@@ -1118,4 +1145,198 @@
.sidebar-context-title {
white-space: nowrap;
}
+
+ .ide-sidebar-branch-title {
+ min-width: 50px;
+ }
+}
+
+.ide-external-link {
+ position: relative;
+
+ svg {
+ display: none;
+ position: absolute;
+ top: 2px;
+ right: -$gl-padding;
+ }
+
+ &:hover,
+ &:focus {
+ svg {
+ display: inline-block;
+ }
+ }
+}
+
+.ide-right-sidebar {
+ width: auto;
+ min-width: 60px;
+
+ .ide-activity-bar {
+ border-left: 1px solid $white-dark;
+ }
+
+ .multi-file-commit-panel-inner {
+ width: 350px;
+ padding: $grid-size $gl-padding;
+ background-color: $white-light;
+ border-left: 1px solid $white-dark;
+ }
+}
+
+.ide-pipeline {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ margin-top: -$grid-size;
+ margin-bottom: -$grid-size;
+
+ .empty-state {
+ margin-top: auto;
+ margin-bottom: auto;
+
+ p {
+ margin: $grid-size 0;
+ text-align: center;
+ line-height: 24px;
+ }
+
+ .btn,
+ h4 {
+ margin: 0;
+ }
+ }
+
+ .build-trace,
+ .top-bar {
+ margin-left: -$gl-padding;
+ }
+
+ &.build-page .top-bar {
+ top: 0;
+ font-size: 12px;
+ border-top-right-radius: $border-radius-default;
+ }
+}
+
+.ide-pipeline-list {
+ flex: 1;
+ overflow: auto;
+}
+
+.ide-pipeline-header {
+ min-height: 55px;
+ padding-left: $gl-padding;
+ padding-right: $gl-padding;
+
+ .ci-status-icon {
+ display: flex;
+ }
+}
+
+.ide-job-item {
+ display: flex;
+ padding: 16px;
+
+ &:not(:last-child) {
+ border-bottom: 1px solid $border-color;
+ }
+
+ .ci-status-icon {
+ display: flex;
+ justify-content: center;
+ min-width: 24px;
+ overflow: hidden;
+ }
+}
+
+.ide-stage {
+ .card-header {
+ display: flex;
+ cursor: pointer;
+
+ .ci-status-icon {
+ display: flex;
+ align-items: center;
+ }
+ }
+
+ .card-body {
+ padding: 0;
+ }
+}
+
+.ide-stage-collapse-icon {
+ margin: auto 0 auto auto;
+}
+
+.ide-stage-title {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.ide-job-header {
+ min-height: 60px;
+}
+
+.ide-merge-requests-dropdown {
+ .nav-links li {
+ width: 50%;
+ padding-left: 0;
+ padding-right: 0;
+
+ a {
+ text-align: center;
+
+ &:not(.active) {
+ background-color: $gray-light;
+ }
+ }
+ }
+
+ .dropdown-input {
+ padding-left: $gl-padding;
+ padding-right: $gl-padding;
+
+ .fa {
+ right: 26px;
+ }
+ }
+
+ .btn-link {
+ padding-top: $gl-padding;
+ padding-bottom: $gl-padding;
+ }
+}
+
+.ide-merge-request-current-icon {
+ min-width: 18px;
+}
+
+.ide-merge-requests-empty {
+ height: 230px;
+}
+
+.ide-merge-requests-dropdown-content {
+ min-height: 230px;
+ max-height: 470px;
+}
+
+.ide-merge-request-project-path {
+ font-size: 12px;
+ line-height: 16px;
+ color: $gl-text-color-secondary;
+}
+
+.ide-merge-request-info {
+ .detail-page-header {
+ line-height: initial;
+ min-height: 38px;
+ }
+
+ .issuable-details {
+ overflow: auto;
+ }
}
diff --git a/app/assets/stylesheets/pages/runners.scss b/app/assets/stylesheets/pages/runners.scss
index 5fb97b13470..2734faec558 100644
--- a/app/assets/stylesheets/pages/runners.scss
+++ b/app/assets/stylesheets/pages/runners.scss
@@ -51,7 +51,7 @@
}
}
-@media (max-width: $screen-md-max) {
+@include media-breakpoint-down(md) {
.runners-content {
width: 100%;
overflow: auto;
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index dbde0720993..2d66f336076 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -18,7 +18,8 @@
.file-finder-input:hover,
.issuable-search-form:hover,
.search-text-input:hover,
-.form-control:hover {
+.form-control:hover,
+:not[readonly] {
border-color: lighten($dropdown-input-focus-border, 20%);
box-shadow: 0 0 4px lighten($search-input-focus-shadow-color, 20%);
}
@@ -28,7 +29,7 @@ input[type="checkbox"]:hover {
}
.search {
- margin: 4px 8px 0;
+ margin: 0 8px;
form {
@extend .form-control;
@@ -47,6 +48,7 @@ input[type="checkbox"]:hover {
}
.location-badge {
+ white-space: nowrap;
height: 32px;
font-size: 12px;
margin: -4px 4px -4px -4px;
@@ -112,7 +114,7 @@ input[type="checkbox"]:hover {
}
.dropdown-content {
- max-height: 302px;
+ max-height: none;
}
}
@@ -166,7 +168,7 @@ input[type="checkbox"]:hover {
}
.search-holder {
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
display: -webkit-flex;
display: flex;
}
@@ -178,7 +180,7 @@ input[type="checkbox"]:hover {
position: relative;
margin-right: 0;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
margin-right: 5px;
}
}
@@ -202,7 +204,7 @@ input[type="checkbox"]:hover {
width: 100%;
margin-top: 5px;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
width: auto;
margin-top: 0;
margin-left: 5px;
@@ -210,7 +212,7 @@ input[type="checkbox"]:hover {
}
.dropdown {
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
margin-left: 5px;
margin-right: 5px;
}
@@ -220,7 +222,7 @@ input[type="checkbox"]:hover {
width: 100%;
margin-top: 5px;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
width: 180px;
margin-top: 0;
}
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index c410049bc0b..839ac5ba59b 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -52,7 +52,7 @@
.settings-content {
max-height: 1px;
- overflow-y: scroll;
+ overflow-y: hidden;
padding-right: 110px;
animation: collapseMaxHeight 300ms ease-out;
// Keep the section from expanding when we scroll over it
@@ -70,7 +70,7 @@
animation: none;
}
- @media(max-width: $screen-sm-max) {
+ @media(max-width: map-get($grid-breakpoints, md)-1) {
padding-right: 20px;
}
@@ -98,14 +98,10 @@
}
.bs-callout,
- .checkbox:first-child,
- .help-block {
+ .form-check:first-child,
+ .form-text.text-muted {
margin-top: 0;
}
-
- .label-light {
- margin-bottom: 0;
- }
}
.settings-list-icon {
@@ -131,12 +127,9 @@
color: $gl-danger;
}
-.service-settings .control-label {
- padding-top: 0;
-}
-
.integration-settings-form {
- .well {
+ .card.card-body,
+ .info-well {
padding: $gl-padding / 2;
box-shadow: none;
}
@@ -158,7 +151,7 @@
}
.visibility-level-setting {
- .radio {
+ .form-check {
margin-bottom: 10px;
i.fa {
@@ -174,7 +167,7 @@
.option-description,
.option-disabled-reason {
- margin-left: 29px;
+ margin-left: 30px;
color: $project-option-descr-color;
}
@@ -198,23 +191,39 @@
}
}
+.initialize-with-readme-setting {
+ .form-check {
+ margin-bottom: 10px;
+
+ .option-title {
+ font-weight: $gl-font-weight-normal;
+ display: inline-block;
+ color: $gl-text-color;
+ }
+
+ .option-description {
+ color: $project-option-descr-color;
+ }
+ }
+}
+
.prometheus-metrics-monitoring {
- .panel {
- .panel-toggle {
+ .card {
+ .card-toggle {
width: 14px;
}
- .badge {
+ .badge.badge-pill {
font-size: 12px;
line-height: 12px;
}
- .panel-heading .badge-count {
+ .card-header .label-count {
color: $white-light;
background: $common-gray-dark;
}
- .panel-body {
+ .card-body {
padding: 0;
}
@@ -249,7 +258,7 @@
li {
padding: $gl-padding;
- .badge {
+ .badge.badge-pill {
margin-left: 5px;
background: $badge-bg;
}
@@ -262,25 +271,12 @@
}
}
-.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;
- }
-}
-
.deprecated-service {
cursor: default;
}
@@ -296,7 +292,8 @@
}
.btn-clipboard {
- margin-left: 5px;
+ background-color: $white-light;
+ border: 1px solid $theme-gray-200;
}
.deploy-token-help-block {
diff --git a/app/assets/stylesheets/pages/settings_ci_cd.scss b/app/assets/stylesheets/pages/settings_ci_cd.scss
index a355e2dee24..777fdb3581e 100644
--- a/app/assets/stylesheets/pages/settings_ci_cd.scss
+++ b/app/assets/stylesheets/pages/settings_ci_cd.scss
@@ -16,3 +16,12 @@
.registry-placeholder {
min-height: 60px;
}
+
+.auto-devops-card {
+ margin-bottom: $gl-vert-padding;
+
+ > .card-body {
+ border-radius: $card-border-radius;
+ padding: $gl-padding $gl-padding-24;
+ }
+}
diff --git a/app/assets/stylesheets/pages/stat_graph.scss b/app/assets/stylesheets/pages/stat_graph.scss
index 8e2c42c1bd3..3f6f5f06075 100644
--- a/app/assets/stylesheets/pages/stat_graph.scss
+++ b/app/assets/stylesheets/pages/stat_graph.scss
@@ -14,7 +14,10 @@
}
#contributors-master {
- @include make-md-column(12);
+ @include media-breakpoint-up(md) {
+ @include make-col-ready();
+ @include make-col(12);
+ }
svg {
width: 100%;
@@ -33,10 +36,14 @@
}
.person {
- @include make-md-column(6);
+ @include media-breakpoint-up(md) {
+ @include make-col-ready();
+ @include make-col(6);
+ }
+
margin-top: 10px;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
width: 100%;
}
diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss
index ade5ddd147b..620297e589d 100644
--- a/app/assets/stylesheets/pages/status.scss
+++ b/app/assets/stylesheets/pages/status.scss
@@ -58,7 +58,7 @@
}
}
-.visible-xs-inline {
+.d-block.d-sm-none-inline {
.ci-status-link {
position: relative;
top: 2px;
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index 4b9824fab0c..e5d7dd13915 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -126,7 +126,7 @@
color: $gl-grayish-blue;
font-size: $gl-font-size;
- .label {
+ .badge.badge-pill {
color: $gl-text-color;
}
@@ -162,7 +162,7 @@
}
}
-@media (max-width: $screen-sm-max) {
+@include media-breakpoint-down(sm) {
.todos-filters {
.dropdown-menu-toggle {
width: 130px;
@@ -174,7 +174,7 @@
}
}
-@media (max-width: $screen-xs-max) {
+@include media-breakpoint-down(xs) {
.todo {
.avatar {
display: none;
@@ -214,7 +214,7 @@
margin-left: auto;
margin-right: auto;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
-webkit-flex-direction: row;
flex-direction: row;
padding-top: 80px;
@@ -233,7 +233,7 @@
margin-left: auto;
margin-right: auto;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
width: 300px;
margin-right: 0;
-webkit-order: 2;
@@ -244,7 +244,7 @@
.todos-all-done {
padding-top: 20px;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
padding-top: 50px;
}
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index e0ee7e9aa3d..efd26cb1f81 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -7,7 +7,7 @@
color: $gl-text-color-secondary;
}
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
display: flex;
.tree-ref-container {
@@ -41,15 +41,10 @@
position: relative;
}
}
-
- .add-to-tree-dropdown {
- position: absolute;
- left: 18px;
- }
}
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
.repo-breadcrumb {
margin-top: 10px;
position: relative;
@@ -121,7 +116,7 @@
margin-left: 5px;
}
- @media (min-width: $screen-md-min) and (max-width: $screen-md-max) {
+ @include media-breakpoint-only(md) {
@include str-truncated(450px);
}
diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss
index e70a57c2a67..800f5c68e39 100644
--- a/app/assets/stylesheets/pages/wiki.scss
+++ b/app/assets/stylesheets/pages/wiki.scss
@@ -65,7 +65,7 @@
display: block;
}
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
&.has-sidebar-toggle {
padding-right: 40px;
}
@@ -81,7 +81,7 @@
}
}
- @media (min-width: $screen-md-min) {
+ @include media-breakpoint-up(md) {
&.has-sidebar-toggle {
padding-right: 0;
}
diff --git a/app/assets/stylesheets/performance_bar.scss b/app/assets/stylesheets/performance_bar.scss
index 06ef58531d7..7a93c4dfa28 100644
--- a/app/assets/stylesheets/performance_bar.scss
+++ b/app/assets/stylesheets/performance_bar.scss
@@ -7,7 +7,6 @@
top: 0;
width: 100%;
z-index: 2000;
- overflow-x: hidden;
height: $performance-bar-height;
background: $black;
@@ -15,6 +14,7 @@
color: $perf-bar-text;
select {
+ color: $perf-bar-text;
width: 200px;
}
@@ -81,7 +81,7 @@
.view {
margin-right: 15px;
- float: left;
+ flex-shrink: 0;
&:last-child {
margin-right: 0;
@@ -106,12 +106,12 @@
}
.performance-bar-modal {
- .modal-footer {
- display: none;
+ .modal-body {
+ padding: 0;
}
- .modal-dialog {
- width: 860px;
+ .modal-footer {
+ display: none;
}
}
}
diff --git a/app/assets/stylesheets/print.scss b/app/assets/stylesheets/print.scss
index 90ccd4abd90..bb10928a037 100644
--- a/app/assets/stylesheets/print.scss
+++ b/app/assets/stylesheets/print.scss
@@ -22,9 +22,9 @@
header,
nav,
-nav.main-nav,
nav.navbar-collapse,
nav.navbar-collapse.collapse,
+.nav-sidebar,
.profiler-results,
.tree-ref-holder,
.tree-holder .breadcrumb,
@@ -38,7 +38,8 @@ ul.notes-form,
.edit-link,
.note-action-button,
.right-sidebar,
-.flash-container {
+.flash-container,
+#js-peek {
display: none !important;
}
diff --git a/app/controllers/admin/appearances_controller.rb b/app/controllers/admin/appearances_controller.rb
index ea302f17d16..9aaec905734 100644
--- a/app/controllers/admin/appearances_controller.rb
+++ b/app/controllers/admin/appearances_controller.rb
@@ -41,6 +41,13 @@ class Admin::AppearancesController < Admin::ApplicationController
redirect_to admin_appearances_path, notice: 'Header logo was succesfully removed.'
end
+ def favicon
+ @appearance.remove_favicon!
+ @appearance.save
+
+ redirect_to admin_appearances_path, notice: 'Favicon was succesfully removed.'
+ end
+
private
# Use callbacks to share common setup or constraints between actions.
@@ -61,6 +68,8 @@ class Admin::AppearancesController < Admin::ApplicationController
logo_cache
header_logo
header_logo_cache
+ favicon
+ favicon_cache
new_project_guidelines
updated_by
]
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index cdfe3d6ab1e..9723e400574 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -52,7 +52,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
private
def set_application_setting
- @application_setting = ApplicationSetting.current_without_cache
+ @application_setting = Gitlab::CurrentSettings.current_application_settings
end
def application_setting_params
diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb
index d6a6bc7d4a1..737942f3eb2 100644
--- a/app/controllers/admin/dashboard_controller.rb
+++ b/app/controllers/admin/dashboard_controller.rb
@@ -1,7 +1,11 @@
class Admin::DashboardController < Admin::ApplicationController
include CountHelper
+ COUNTED_ITEMS = [Project, User, Group, ForkedProjectLink, Issue, MergeRequest,
+ Note, Snippet, Key, Milestone].freeze
+
def index
+ @counts = Gitlab::Database::Count.approximate_counts(COUNTED_ITEMS)
@projects = Project.order_id_desc.without_deleted.with_route.limit(10)
@users = User.order_id_desc.limit(10)
@groups = Group.order_id_desc.with_route.limit(10)
diff --git a/app/controllers/admin/deploy_keys_controller.rb b/app/controllers/admin/deploy_keys_controller.rb
index b0c4c31cffc..5c2025c1988 100644
--- a/app/controllers/admin/deploy_keys_controller.rb
+++ b/app/controllers/admin/deploy_keys_controller.rb
@@ -22,7 +22,7 @@ class Admin::DeployKeysController < Admin::ApplicationController
end
def update
- if deploy_key.update_attributes(update_params)
+ if deploy_key.update(update_params)
flash[:notice] = 'Deploy key was successfully updated.'
redirect_to admin_deploy_keys_path
else
@@ -34,7 +34,7 @@ class Admin::DeployKeysController < Admin::ApplicationController
deploy_key.destroy
respond_to do |format|
- format.html { redirect_to admin_deploy_keys_path, status: 302 }
+ format.html { redirect_to admin_deploy_keys_path, status: :found }
format.json { head :ok }
end
end
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index 001f6520093..d7a5b745d3f 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -39,7 +39,7 @@ class Admin::GroupsController < Admin::ApplicationController
end
def update
- if @group.update_attributes(group_params)
+ if @group.update(group_params)
redirect_to [:admin, @group], notice: 'Group was successfully updated.'
else
render "edit"
@@ -72,10 +72,10 @@ class Admin::GroupsController < Admin::ApplicationController
end
def group_params
- params.require(:group).permit(group_params_ce)
+ params.require(:group).permit(allowed_group_params)
end
- def group_params_ce
+ def allowed_group_params
[
:avatar,
:description,
diff --git a/app/controllers/admin/hooks_controller.rb b/app/controllers/admin/hooks_controller.rb
index 2b47819303e..a98c355c7ba 100644
--- a/app/controllers/admin/hooks_controller.rb
+++ b/app/controllers/admin/hooks_controller.rb
@@ -9,7 +9,7 @@ class Admin::HooksController < Admin::ApplicationController
end
def create
- @hook = SystemHook.new(hook_params)
+ @hook = SystemHook.new(hook_params.to_h)
if @hook.save
redirect_to admin_hooks_path, notice: 'Hook was successfully created.'
@@ -23,7 +23,7 @@ class Admin::HooksController < Admin::ApplicationController
end
def update
- if hook.update_attributes(hook_params)
+ if hook.update(hook_params)
flash[:notice] = 'System hook was successfully updated.'
redirect_to admin_hooks_path
else
@@ -34,7 +34,7 @@ class Admin::HooksController < Admin::ApplicationController
def destroy
hook.destroy
- redirect_to admin_hooks_path, status: 302
+ redirect_to admin_hooks_path, status: :found
end
def test
@@ -52,8 +52,7 @@ class Admin::HooksController < Admin::ApplicationController
end
def hook_logs
- @hook_logs ||=
- Kaminari.paginate_array(hook.web_hook_logs.order(created_at: :desc)).page(params[:page])
+ @hook_logs ||= hook.web_hook_logs.recent.page(params[:page])
end
def hook_params
diff --git a/app/controllers/admin/identities_controller.rb b/app/controllers/admin/identities_controller.rb
index 43b4e3a2cc3..ceb45865804 100644
--- a/app/controllers/admin/identities_controller.rb
+++ b/app/controllers/admin/identities_controller.rb
@@ -25,7 +25,7 @@ class Admin::IdentitiesController < Admin::ApplicationController
end
def update
- if @identity.update_attributes(identity_params)
+ if @identity.update(identity_params)
RepairLdapBlockedUserService.new(@user).execute
redirect_to admin_user_identities_path(@user), notice: 'User identity was successfully updated.'
else
diff --git a/app/controllers/admin/impersonations_controller.rb b/app/controllers/admin/impersonations_controller.rb
index 39dbf85f6c0..d2f947d2c66 100644
--- a/app/controllers/admin/impersonations_controller.rb
+++ b/app/controllers/admin/impersonations_controller.rb
@@ -11,7 +11,7 @@ class Admin::ImpersonationsController < Admin::ApplicationController
session[:impersonator_id] = nil
- redirect_to admin_user_path(original_user), status: 302
+ redirect_to admin_user_path(original_user), status: :found
end
private
diff --git a/app/controllers/admin/jobs_controller.rb b/app/controllers/admin/jobs_controller.rb
index ae7a7f6279c..ac1ae0f16b3 100644
--- a/app/controllers/admin/jobs_controller.rb
+++ b/app/controllers/admin/jobs_controller.rb
@@ -20,6 +20,6 @@ class Admin::JobsController < Admin::ApplicationController
def cancel_all
Ci::Build.running_or_pending.each(&:cancel)
- redirect_to admin_jobs_path, status: 303
+ redirect_to admin_jobs_path, status: :see_other
end
end
diff --git a/app/controllers/admin/runner_projects_controller.rb b/app/controllers/admin/runner_projects_controller.rb
index 7ed2de71028..51d5799cd89 100644
--- a/app/controllers/admin/runner_projects_controller.rb
+++ b/app/controllers/admin/runner_projects_controller.rb
@@ -4,9 +4,7 @@ class Admin::RunnerProjectsController < Admin::ApplicationController
def create
@runner = Ci::Runner.find(params[:runner_project][:runner_id])
- runner_project = @runner.assign_to(@project, current_user)
-
- if runner_project.persisted?
+ if @runner.assign_to(@project, current_user)
redirect_to admin_runner_path(@runner)
else
redirect_to admin_runner_path(@runner), alert: 'Failed adding runner to project'
@@ -18,7 +16,7 @@ class Admin::RunnerProjectsController < Admin::ApplicationController
runner = rp.runner
rp.destroy
- redirect_to admin_runner_path(runner), status: 302
+ redirect_to admin_runner_path(runner), status: :found
end
private
diff --git a/app/controllers/admin/runners_controller.rb b/app/controllers/admin/runners_controller.rb
index 4b01904f2a1..6c76c55a9d4 100644
--- a/app/controllers/admin/runners_controller.rb
+++ b/app/controllers/admin/runners_controller.rb
@@ -28,7 +28,7 @@ class Admin::RunnersController < Admin::ApplicationController
def destroy
@runner.destroy
- redirect_to admin_runners_path, status: 302
+ redirect_to admin_runners_path, status: :found
end
def resume
diff --git a/app/controllers/admin/services_controller.rb b/app/controllers/admin/services_controller.rb
index a7025b62ad7..e70aa549140 100644
--- a/app/controllers/admin/services_controller.rb
+++ b/app/controllers/admin/services_controller.rb
@@ -16,7 +16,7 @@ class Admin::ServicesController < Admin::ApplicationController
end
def update
- if service.update_attributes(service_params[:service])
+ if service.update(service_params[:service])
PropagateServiceTemplateWorker.perform_async(service.id) if service.active?
redirect_to admin_application_settings_services_path,
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index bfeb5a2d097..a51a8c3ed4a 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -163,7 +163,7 @@ class Admin::UsersController < Admin::ApplicationController
format.json { head :ok }
else
format.html { redirect_back_or_admin_user(alert: 'There was an error removing the e-mail.') }
- format.json { render json: 'There was an error removing the e-mail.', status: 400 }
+ format.json { render json: 'There was an error removing the e-mail.', status: :bad_request }
end
end
end
@@ -187,10 +187,10 @@ class Admin::UsersController < Admin::ApplicationController
end
def user_params
- params.require(:user).permit(user_params_ce)
+ params.require(:user).permit(allowed_user_params)
end
- def user_params_ce
+ def allowed_user_params
[
:access_level,
:avatar,
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 2843d70c645..21cc6dfdd16 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -27,7 +27,7 @@ class ApplicationController < ActionController::Base
after_action :set_page_title_header, if: -> { request.format == :json }
- protect_from_forgery with: :exception
+ protect_from_forgery with: :exception, prepend: true
helper_method :can?
helper_method :import_sources_enabled?, :github_import_enabled?, :gitea_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled?
@@ -91,6 +91,10 @@ class ApplicationController < ActionController::Base
payload[:user_id] = logged_user.try(:id)
payload[:username] = logged_user.try(:username)
end
+
+ if response.status == 422 && response.body.present? && response.content_type == 'application/json'.freeze
+ payload[:response] = response.body
+ end
end
# Controllers such as GitHttpController may use alternative methods
@@ -130,12 +134,17 @@ class ApplicationController < ActionController::Base
end
def access_denied!(message = nil)
+ # If we display a custom access denied message to the user, we don't want to
+ # hide existence of the resource, rather tell them they cannot access it using
+ # the provided message
+ status = message.present? ? :forbidden : :not_found
+
respond_to do |format|
- format.any { head :not_found }
+ format.any { head status }
format.html do
render "errors/access_denied",
layout: "errors",
- status: 404,
+ status: status,
locals: { message: message }
end
end
@@ -146,14 +155,15 @@ class ApplicationController < ActionController::Base
end
def render_403
- head :forbidden
+ respond_to do |format|
+ format.any { head :forbidden }
+ format.html { render "errors/access_denied", layout: "errors", status: 403 }
+ end
end
def render_404
respond_to do |format|
- format.html do
- render file: Rails.root.join("public", "404"), layout: false, status: "404"
- end
+ format.html { render "errors/not_found", layout: "errors", status: 404 }
# Prevent the Rails CSRF protector from thinking a missing .js file is a JavaScript file
format.js { render json: '', status: :not_found, content_type: 'application/json' }
format.any { head :not_found }
@@ -274,8 +284,10 @@ class ApplicationController < ActionController::Base
return unless current_user
return if current_user.terms_accepted?
+ message = _("Please accept the Terms of Service before continuing.")
+
if sessionless_user?
- render_403
+ access_denied!(message)
else
# Redirect to the destination if the request is a get.
# Redirect to the source if it was a post, so the user can re-submit after
@@ -286,7 +298,7 @@ class ApplicationController < ActionController::Base
URI(request.referer).path if request.referer
end
- flash[:notice] = _("Please accept the Terms of Service before continuing.")
+ flash[:notice] = message
redirect_to terms_path(redirect: redirect_path), status: :found
end
end
diff --git a/app/controllers/boards/lists_controller.rb b/app/controllers/boards/lists_controller.rb
index 381fd4d7508..e8b5934f2a9 100644
--- a/app/controllers/boards/lists_controller.rb
+++ b/app/controllers/boards/lists_controller.rb
@@ -56,8 +56,12 @@ module Boards
private
+ def list_creation_attrs
+ %i[label_id]
+ end
+
def list_params
- params.require(:list).permit(:label_id)
+ params.require(:list).permit(list_creation_attrs)
end
def move_params
@@ -65,11 +69,15 @@ module Boards
end
def serialize_as_json(resource)
- resource.as_json(
+ resource.as_json(serialization_attrs)
+ end
+
+ def serialization_attrs
+ {
only: [:id, :list_type, :position],
methods: [:title],
label: true
- )
+ }
end
end
end
diff --git a/app/controllers/concerns/group_tree.rb b/app/controllers/concerns/group_tree.rb
index 56770a17406..6ec6897e707 100644
--- a/app/controllers/concerns/group_tree.rb
+++ b/app/controllers/concerns/group_tree.rb
@@ -1,21 +1,16 @@
module GroupTree
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def render_group_tree(groups)
- @groups = if params[:filter].present?
- # We find the ancestors by ID of the search results here.
- # Otherwise the ancestors would also have filters applied,
- # which would cause them not to be preloaded.
- group_ids = groups.search(params[:filter]).select(:id)
- Gitlab::GroupHierarchy.new(Group.where(id: group_ids))
- .base_and_ancestors
- else
- # Only show root groups if no parent-id is given
- groups.where(parent_id: params[:parent_id])
- end
+ groups = groups.sort_by_attribute(@sort = params[:sort])
- @groups = @groups.with_selects_for_list(archived: params[:archived])
- .sort_by_attribute(@sort = params[:sort])
- .page(params[:page])
+ groups = if params[:filter].present?
+ filtered_groups_with_ancestors(groups)
+ else
+ # If `params[:parent_id]` is `nil`, we will only show root-groups
+ groups.where(parent_id: params[:parent_id]).page(params[:page])
+ end
+
+ @groups = groups.with_selects_for_list(archived: params[:archived])
respond_to do |format|
format.html
@@ -28,4 +23,21 @@ module GroupTree
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
end
+
+ def filtered_groups_with_ancestors(groups)
+ filtered_groups = groups.search(params[:filter]).page(params[:page])
+
+ if Group.supports_nested_groups?
+ # We find the ancestors by ID of the search results here.
+ # Otherwise the ancestors would also have filters applied,
+ # which would cause them not to be preloaded.
+ #
+ # Pagination needs to be applied before loading the ancestors to
+ # make sure ancestors are not cut off by pagination.
+ Gitlab::GroupHierarchy.new(Group.where(id: filtered_groups.select(:id)))
+ .base_and_ancestors
+ else
+ filtered_groups
+ end
+ end
end
diff --git a/app/controllers/concerns/internal_redirect.rb b/app/controllers/concerns/internal_redirect.rb
index 7409b2e89a5..10b9852e329 100644
--- a/app/controllers/concerns/internal_redirect.rb
+++ b/app/controllers/concerns/internal_redirect.rb
@@ -23,6 +23,10 @@ module InternalRedirect
nil
end
+ def sanitize_redirect(url_or_path)
+ safe_redirect_path(url_or_path) || safe_redirect_path_for_url(url_or_path)
+ end
+
def host_allowed?(uri)
uri.host == request.host &&
uri.port == request.port
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index c925b4aada5..37e03d70b6f 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -7,6 +7,19 @@ module IssuableActions
before_action :authorize_admin_issuable!, only: :bulk_update
end
+ def permitted_keys
+ [
+ :issuable_ids,
+ :assignee_id,
+ :milestone_id,
+ :state_event,
+ :subscription_event,
+ label_ids: [],
+ add_label_ids: [],
+ remove_label_ids: []
+ ]
+ end
+
def show
respond_to do |format|
format.html
@@ -77,7 +90,7 @@ module IssuableActions
end
def discussions
- notes = issuable.notes
+ notes = issuable.discussion_notes
.inc_relations_for_view
.includes(:noteable)
.fresh
@@ -114,7 +127,7 @@ module IssuableActions
errors: [
"Someone edited this #{issuable.human_class_name} at the same time you did. Please refresh your browser and make sure your changes will not unintentionally remove theirs."
]
- }, status: 409
+ }, status: :conflict
end
end
end
@@ -140,24 +153,15 @@ module IssuableActions
end
def bulk_update_params
- permitted_keys = [
- :issuable_ids,
- :assignee_id,
- :milestone_id,
- :state_event,
- :subscription_event,
- label_ids: [],
- add_label_ids: [],
- remove_label_ids: []
- ]
+ permitted_keys_array = permitted_keys.dup
if resource_name == 'issue'
- permitted_keys << { assignee_ids: [] }
+ permitted_keys_array << { assignee_ids: [] }
else
- permitted_keys.unshift(:assignee_id)
+ permitted_keys_array.unshift(:assignee_id)
end
- params.require(:update).permit(permitted_keys)
+ params.require(:update).permit(permitted_keys_array)
end
def resource_name
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index ca1b80a36a0..2ef2ee76855 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -95,12 +95,7 @@ module IssuableCollections
elsif @group
@filter_params[:group_id] = @group.id
@filter_params[:include_subgroups] = true
- else
- # TODO: this filter ignore issues/mr created in public or
- # internal repos where you are not a member. Enable this filter
- # or improve current implementation to filter only issues you
- # created or assigned or mentioned
- # @filter_params[:authorized_only] = true
+ @filter_params[:use_cte_for_search] = true
end
@filter_params.permit(finder_type.valid_params)
diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb
index 3b11a373368..9d58656773d 100644
--- a/app/controllers/concerns/issues_action.rb
+++ b/app/controllers/concerns/issues_action.rb
@@ -1,6 +1,7 @@
module IssuesAction
extend ActiveSupport::Concern
include IssuableCollections
+ include IssuesCalendar
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def issues
@@ -17,10 +18,14 @@ module IssuesAction
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
+ def issues_calendar
+ render_issues_calendar(issuables_collection)
+ end
+
private
def finder_type
(super if defined?(super)) ||
- (IssuesFinder if action_name == 'issues')
+ (IssuesFinder if %w(issues issues_calendar).include?(action_name))
end
end
diff --git a/app/controllers/concerns/issues_calendar.rb b/app/controllers/concerns/issues_calendar.rb
new file mode 100644
index 00000000000..671a204621d
--- /dev/null
+++ b/app/controllers/concerns/issues_calendar.rb
@@ -0,0 +1,24 @@
+module IssuesCalendar
+ extend ActiveSupport::Concern
+
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ def render_issues_calendar(issuables)
+ @issues = issuables
+ .non_archived
+ .with_due_date
+ .limit(100)
+
+ respond_to do |format|
+ format.ics do
+ # NOTE: with text/calendar as Content-Type, the browser always downloads
+ # the content as a file (even ignoring the Content-Disposition
+ # header). We want to display the content inline when accessed
+ # from GitLab, similarly to the RSS feed.
+ if request.referer&.start_with?(::Settings.gitlab.base_url)
+ response.headers['Content-Type'] = 'text/plain'
+ end
+ end
+ end
+ end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
+end
diff --git a/app/controllers/concerns/lfs_request.rb b/app/controllers/concerns/lfs_request.rb
index 5e4e8a87153..79ee5b2f91e 100644
--- a/app/controllers/concerns/lfs_request.rb
+++ b/app/controllers/concerns/lfs_request.rb
@@ -27,7 +27,7 @@ module LfsRequest
message: 'Git LFS is not enabled on this GitLab server, contact your admin.',
documentation_url: help_url
},
- status: 501
+ status: :not_implemented
)
end
diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb
index 0c34e49206a..fe9a030cdf2 100644
--- a/app/controllers/concerns/notes_actions.rb
+++ b/app/controllers/concerns/notes_actions.rb
@@ -237,10 +237,6 @@ module NotesActions
def use_note_serializer?
return false if params['html']
- if noteable.is_a?(MergeRequest)
- cookies[:vue_mr_discussions] == 'true'
- else
- noteable.discussions_rendered_on_frontend?
- end
+ noteable.discussions_rendered_on_frontend?
end
end
diff --git a/app/controllers/concerns/preview_markdown.rb b/app/controllers/concerns/preview_markdown.rb
index 90bb7a87b45..99123fcb3b0 100644
--- a/app/controllers/concerns/preview_markdown.rb
+++ b/app/controllers/concerns/preview_markdown.rb
@@ -10,9 +10,12 @@ module PreviewMarkdown
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 }
+ when 'projects' then { issuable_state_filter_enabled: true }
else {}
end
+ markdown_params[:markdown_engine] = result[:markdown_engine]
+
render json: {
body: view_context.markdown(result[:text], markdown_params),
references: {
diff --git a/app/controllers/concerns/uploads_actions.rb b/app/controllers/concerns/uploads_actions.rb
index b9b9b6e4e88..434459a225a 100644
--- a/app/controllers/concerns/uploads_actions.rb
+++ b/app/controllers/concerns/uploads_actions.rb
@@ -1,8 +1,14 @@
module UploadsActions
+ extend ActiveSupport::Concern
+
include Gitlab::Utils::StrongMemoize
include SendFileUpload
- UPLOAD_MOUNTS = %w(avatar attachment file logo header_logo).freeze
+ UPLOAD_MOUNTS = %w(avatar attachment file logo header_logo favicon).freeze
+
+ included do
+ prepend_before_action :set_html_format, only: :show
+ end
def create
link_to_file = UploadService.new(model, params[:file], uploader_class).execute
@@ -31,11 +37,33 @@ module UploadsActions
disposition = uploader.image_or_video? ? 'inline' : 'attachment'
+ uploaders = [uploader, *uploader.versions.values]
+ uploader = uploaders.find { |version| version.filename == params[:filename] }
+
+ return render_404 unless uploader
+
send_upload(uploader, attachment: uploader.filename, disposition: disposition)
end
+ def authorize
+ set_workhorse_internal_api_content_type
+
+ authorized = uploader_class.workhorse_authorize(
+ has_length: false,
+ maximum_size: Gitlab::CurrentSettings.max_attachment_size.megabytes.to_i)
+
+ render json: authorized
+ end
+
private
+ # Explicitly set the format.
+ # Otherwise rails 5 will set it from a file extension.
+ # See https://github.com/rails/rails/commit/84e8accd6fb83031e4c27e44925d7596655285f7#diff-2b8f2fbb113b55ca8e16001c393da8f1
+ def set_html_format
+ request.format = :html
+ end
+
def uploader_class
raise NotImplementedError
end
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index 4d4ac025f8c..ccfcbbdc776 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -7,7 +7,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
skip_cross_project_access_check :index, :starred
def index
- @projects = load_projects(params.merge(non_public: true)).page(params[:page])
+ @projects = load_projects(params.merge(non_public: true))
respond_to do |format|
format.html
@@ -25,7 +25,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
def starred
@projects = load_projects(params.merge(starred: true))
- .includes(:forked_from_project, :tags).page(params[:page])
+ .includes(:forked_from_project, :tags)
@groups = []
@@ -51,6 +51,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
.new(params: finder_params, current_user: current_user)
.execute
.includes(:route, :creator, namespace: [:route, :owner])
+ .page(finder_params[:page])
prepare_projects_for_rendering(projects)
end
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index 68d328fa797..ff133001b84 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -54,7 +54,7 @@ class DashboardController < Dashboard::ApplicationController
return unless @no_filters_set
respond_to do |format|
- format.html
+ format.html { render }
format.atom { head :bad_request }
end
end
diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb
new file mode 100644
index 00000000000..0a1cf169aca
--- /dev/null
+++ b/app/controllers/graphql_controller.rb
@@ -0,0 +1,45 @@
+class GraphqlController < ApplicationController
+ # Unauthenticated users have access to the API for public data
+ skip_before_action :authenticate_user!
+
+ before_action :check_graphql_feature_flag!
+
+ def execute
+ variables = Gitlab::Graphql::Variables.new(params[:variables]).to_h
+ query = params[:query]
+ operation_name = params[:operationName]
+ context = {
+ current_user: current_user
+ }
+ result = GitlabSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
+ render json: result
+ end
+
+ rescue_from StandardError do |exception|
+ log_exception(exception)
+
+ render_error("Internal server error")
+ end
+
+ rescue_from Gitlab::Graphql::Variables::Invalid do |exception|
+ render_error(exception.message, status: :unprocessable_entity)
+ end
+
+ private
+
+ # Overridden from the ApplicationController to make the response look like
+ # a GraphQL response. That is nicely picked up in Graphiql.
+ def render_404
+ render_error("Not found!", status: :not_found)
+ end
+
+ def render_error(message, status: 500)
+ error = { errors: [message: message] }
+
+ render json: error, status: status
+ end
+
+ def check_graphql_feature_flag!
+ render_404 unless Feature.enabled?(:graphql)
+ end
+end
diff --git a/app/controllers/groups/avatars_controller.rb b/app/controllers/groups/avatars_controller.rb
index cc5ba5878f8..35a61b359c8 100644
--- a/app/controllers/groups/avatars_controller.rb
+++ b/app/controllers/groups/avatars_controller.rb
@@ -7,6 +7,6 @@ class Groups::AvatarsController < Groups::ApplicationController
@group.remove_avatar!
@group.save
- redirect_to edit_group_path(@group), status: 302
+ redirect_to edit_group_path(@group), status: :found
end
end
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index ef3eba80154..ef5d5e5c742 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -3,8 +3,12 @@ class Groups::GroupMembersController < Groups::ApplicationController
include MembersPresentation
include SortingHelper
+ def self.admin_not_required_endpoints
+ %i[index leave request_access]
+ end
+
# Authorize
- before_action :authorize_admin_group_member!, except: [:index, :leave, :request_access]
+ before_action :authorize_admin_group_member!, except: admin_not_required_endpoints
skip_cross_project_access_check :index, :create, :update, :destroy, :request_access,
:approve_access_request, :leave, :resend_invite,
diff --git a/app/controllers/groups/labels_controller.rb b/app/controllers/groups/labels_controller.rb
index 58be330f466..863f50e8e66 100644
--- a/app/controllers/groups/labels_controller.rb
+++ b/app/controllers/groups/labels_controller.rb
@@ -2,6 +2,7 @@ class Groups::LabelsController < Groups::ApplicationController
include ToggleSubscriptionAction
before_action :label, only: [:edit, :update, :destroy]
+ before_action :available_labels, only: [:index]
before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update, :destroy]
before_action :save_previous_label_path, only: [:edit]
@@ -12,17 +13,8 @@ class Groups::LabelsController < Groups::ApplicationController
format.html do
@labels = @group.labels.page(params[:page])
end
-
format.json do
- available_labels = LabelsFinder.new(
- current_user,
- group_id: @group.id,
- only_group_labels: params[:only_group_labels],
- include_ancestor_groups: params[:include_ancestor_groups],
- include_descendant_groups: params[:include_descendant_groups]
- ).execute
-
- render json: LabelSerializer.new.represent_appearance(available_labels)
+ render json: LabelSerializer.new.represent_appearance(@available_labels)
end
end
end
@@ -113,4 +105,15 @@ class Groups::LabelsController < Groups::ApplicationController
def save_previous_label_path
session[:previous_labels_path] = URI(request.referer || '').path
end
+
+ def available_labels
+ @available_labels ||=
+ LabelsFinder.new(
+ current_user,
+ group_id: @group.id,
+ only_group_labels: params[:only_group_labels],
+ include_ancestor_groups: params[:include_ancestor_groups],
+ include_descendant_groups: params[:include_descendant_groups]
+ ).execute
+ end
end
diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb
index 5903689dc62..9bd51de7e97 100644
--- a/app/controllers/groups/milestones_controller.rb
+++ b/app/controllers/groups/milestones_controller.rb
@@ -76,12 +76,15 @@ class Groups::MilestonesController < Groups::ApplicationController
def milestones
milestones = MilestonesFinder.new(search_params).execute
- legacy_milestones = GroupMilestone.build_collection(group, group_projects, params)
@sort = params[:sort] || 'due_date_asc'
MilestoneArray.sort(milestones + legacy_milestones, @sort)
end
+ def legacy_milestones
+ GroupMilestone.build_collection(group, group_projects, params)
+ end
+
def milestone
@milestone =
if params[:title]
diff --git a/app/controllers/groups/runners_controller.rb b/app/controllers/groups/runners_controller.rb
index 78992ec7f46..1036b4e6ed3 100644
--- a/app/controllers/groups/runners_controller.rb
+++ b/app/controllers/groups/runners_controller.rb
@@ -23,7 +23,7 @@ class Groups::RunnersController < Groups::ApplicationController
def destroy
@runner.destroy
- redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), status: 302
+ redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), status: :found
end
def resume
diff --git a/app/controllers/groups/shared_projects_controller.rb b/app/controllers/groups/shared_projects_controller.rb
new file mode 100644
index 00000000000..7dec1f5f402
--- /dev/null
+++ b/app/controllers/groups/shared_projects_controller.rb
@@ -0,0 +1,33 @@
+module Groups
+ class SharedProjectsController < Groups::ApplicationController
+ respond_to :json
+ before_action :group
+ skip_cross_project_access_check :index
+
+ def index
+ shared_projects = GroupProjectsFinder.new(
+ group: group,
+ current_user: current_user,
+ params: finder_params,
+ options: { only_shared: true }
+ ).execute
+ serializer = GroupChildSerializer.new(current_user: current_user)
+ .with_pagination(request, response)
+
+ render json: serializer.represent(shared_projects)
+ end
+
+ private
+
+ def finder_params
+ @finder_params ||= begin
+ # Make the `search` param consistent for the frontend,
+ # which will be using `filter`.
+ params[:search] ||= params[:filter] if params[:filter]
+ # Don't show archived projects
+ params[:non_archived] = true
+ params.permit(:sort, :search, :non_archived)
+ end
+ end
+ end
+end
diff --git a/app/controllers/groups/uploads_controller.rb b/app/controllers/groups/uploads_controller.rb
index f1578f75e88..74760194a1f 100644
--- a/app/controllers/groups/uploads_controller.rb
+++ b/app/controllers/groups/uploads_controller.rb
@@ -1,9 +1,11 @@
class Groups::UploadsController < Groups::ApplicationController
include UploadsActions
+ include WorkhorseRequest
skip_before_action :group, if: -> { action_name == 'show' && image_or_video? }
- before_action :authorize_upload_file!, only: [:create]
+ before_action :authorize_upload_file!, only: [:create, :authorize]
+ before_action :verify_workhorse_api!, only: [:authorize]
private
diff --git a/app/controllers/health_controller.rb b/app/controllers/health_controller.rb
index 16abf7bab7e..3fedd5bfb29 100644
--- a/app/controllers/health_controller.rb
+++ b/app/controllers/health_controller.rb
@@ -1,5 +1,5 @@
class HealthController < ActionController::Base
- protect_from_forgery with: :exception, except: :storage_check
+ protect_from_forgery with: :exception, except: :storage_check, prepend: true
include RequiresWhitelistedMonitoringClient
CHECKS = [
@@ -8,7 +8,6 @@ class HealthController < ActionController::Base
Gitlab::HealthChecks::Redis::CacheCheck,
Gitlab::HealthChecks::Redis::QueuesCheck,
Gitlab::HealthChecks::Redis::SharedStateCheck,
- Gitlab::HealthChecks::FsShardsCheck,
Gitlab::HealthChecks::GitalyCheck
].freeze
diff --git a/app/controllers/import/base_controller.rb b/app/controllers/import/base_controller.rb
index 663269a0f92..5766c6924cd 100644
--- a/app/controllers/import/base_controller.rb
+++ b/app/controllers/import/base_controller.rb
@@ -25,4 +25,8 @@ class Import::BaseController < ApplicationController
current_user.namespace
end
+
+ def project_save_error(project)
+ project.errors.full_messages.join(', ')
+ end
end
diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb
index 77af5fb9c4f..fa31933e778 100644
--- a/app/controllers/import/bitbucket_controller.rb
+++ b/app/controllers/import/bitbucket_controller.rb
@@ -55,7 +55,7 @@ class Import::BitbucketController < Import::BaseController
if project.persisted?
render json: ProjectSerializer.new.represent(project)
else
- render json: { errors: project.errors.full_messages }, status: :unprocessable_entity
+ render json: { errors: project_save_error(project) }, status: :unprocessable_entity
end
else
render json: { errors: 'This namespace has already been taken! Please choose another one.' }, status: :unprocessable_entity
diff --git a/app/controllers/import/fogbugz_controller.rb b/app/controllers/import/fogbugz_controller.rb
index 25ec13b8075..2d665e05ac3 100644
--- a/app/controllers/import/fogbugz_controller.rb
+++ b/app/controllers/import/fogbugz_controller.rb
@@ -66,7 +66,7 @@ class Import::FogbugzController < Import::BaseController
if project.persisted?
render json: ProjectSerializer.new.represent(project)
else
- render json: { errors: project.errors.full_messages }, status: :unprocessable_entity
+ render json: { errors: project_save_error(project) }, status: :unprocessable_entity
end
end
diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb
index f67ec4c248b..c9870332c0f 100644
--- a/app/controllers/import/github_controller.rb
+++ b/app/controllers/import/github_controller.rb
@@ -48,7 +48,7 @@ class Import::GithubController < Import::BaseController
if project.persisted?
render json: ProjectSerializer.new.represent(project)
else
- render json: { errors: project.errors.full_messages }, status: :unprocessable_entity
+ render json: { errors: project_save_error(project) }, status: :unprocessable_entity
end
else
render json: { errors: 'This namespace has already been taken! Please choose another one.' }, status: :unprocessable_entity
diff --git a/app/controllers/import/gitlab_controller.rb b/app/controllers/import/gitlab_controller.rb
index 39e2e9e094b..fccbdbca0f6 100644
--- a/app/controllers/import/gitlab_controller.rb
+++ b/app/controllers/import/gitlab_controller.rb
@@ -32,7 +32,7 @@ class Import::GitlabController < Import::BaseController
if project.persisted?
render json: ProjectSerializer.new.represent(project)
else
- render json: { errors: project.errors.full_messages }, status: :unprocessable_entity
+ render json: { errors: project_save_error(project) }, status: :unprocessable_entity
end
else
render json: { errors: 'This namespace has already been taken! Please choose another one.' }, status: :unprocessable_entity
diff --git a/app/controllers/import/google_code_controller.rb b/app/controllers/import/google_code_controller.rb
index 9b26a00f7c7..3bce27e810a 100644
--- a/app/controllers/import/google_code_controller.rb
+++ b/app/controllers/import/google_code_controller.rb
@@ -92,7 +92,7 @@ class Import::GoogleCodeController < Import::BaseController
if project.persisted?
render json: ProjectSerializer.new.represent(project)
else
- render json: { errors: project.errors.full_messages }, status: :unprocessable_entity
+ render json: { errors: project_save_error(project) }, status: :unprocessable_entity
end
end
diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb
index 67057b5b126..3cb9e46b548 100644
--- a/app/controllers/jwt_controller.rb
+++ b/app/controllers/jwt_controller.rb
@@ -41,7 +41,7 @@ class JwtController < ApplicationController
"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}" }
]
- }, status: 401
+ }, status: :unauthorized
end
def render_unauthorized
@@ -50,7 +50,7 @@ class JwtController < ApplicationController
{ code: 'UNAUTHORIZED',
message: 'HTTP Basic: Access denied' }
]
- }, status: 401
+ }, status: :unauthorized
end
def auth_params
diff --git a/app/controllers/metrics_controller.rb b/app/controllers/metrics_controller.rb
index 33b682d2859..0400ffcfee5 100644
--- a/app/controllers/metrics_controller.rb
+++ b/app/controllers/metrics_controller.rb
@@ -1,7 +1,7 @@
class MetricsController < ActionController::Base
include RequiresWhitelistedMonitoringClient
- protect_from_forgery with: :exception
+ protect_from_forgery with: :exception, prepend: true
def index
response = if Gitlab::Metrics.prometheus_metrics_enabled?
diff --git a/app/controllers/notification_settings_controller.rb b/app/controllers/notification_settings_controller.rb
index 8ec4bb1233f..ed20302487c 100644
--- a/app/controllers/notification_settings_controller.rb
+++ b/app/controllers/notification_settings_controller.rb
@@ -5,14 +5,14 @@ class NotificationSettingsController < ApplicationController
return render_404 unless can_read?(resource)
@notification_setting = current_user.notification_settings_for(resource)
- @saved = @notification_setting.update_attributes(notification_setting_params)
+ @saved = @notification_setting.update(notification_setting_params)
render_response
end
def update
@notification_setting = current_user.notification_settings.find(params[:id])
- @saved = @notification_setting.update_attributes(notification_setting_params)
+ @saved = @notification_setting.update(notification_setting_params)
render_response
end
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index ed89bed029b..1547d4b5972 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -2,7 +2,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
include AuthenticatesWithTwoFactor
include Devise::Controllers::Rememberable
- protect_from_forgery except: [:kerberos, :saml, :cas3]
+ protect_from_forgery except: [:kerberos, :saml, :cas3], prepend: true
def handle_omniauth
omniauth_flow(Gitlab::Auth::OAuth)
@@ -26,11 +26,11 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
# Extend the standard message generation to accept our custom exception
def failure_message
- exception = env["omniauth.error"]
+ exception = request.env["omniauth.error"]
error = exception.error_reason if exception.respond_to?(:error_reason)
error ||= exception.error if exception.respond_to?(:error)
error ||= exception.message if exception.respond_to?(:message)
- error ||= env["omniauth.error.type"].to_s
+ error ||= request.env["omniauth.error.type"].to_s
error.to_s.humanize if error
end
@@ -119,7 +119,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
set_remember_me(user)
- if user.two_factor_enabled?
+ if user.two_factor_enabled? && !auth_user.bypass_two_factor?
prompt_for_two_factor(user)
else
sign_in_and_redirect(user)
diff --git a/app/controllers/profiles/active_sessions_controller.rb b/app/controllers/profiles/active_sessions_controller.rb
index f0cdc228366..f1e77d68acd 100644
--- a/app/controllers/profiles/active_sessions_controller.rb
+++ b/app/controllers/profiles/active_sessions_controller.rb
@@ -7,7 +7,7 @@ class Profiles::ActiveSessionsController < Profiles::ApplicationController
ActiveSession.destroy(current_user, params[:id])
respond_to do |format|
- format.html { redirect_to profile_active_sessions_url, status: 302 }
+ format.html { redirect_to profile_active_sessions_url, status: :found }
format.js { head :ok }
end
end
diff --git a/app/controllers/profiles/avatars_controller.rb b/app/controllers/profiles/avatars_controller.rb
index 39b9f8a84d1..4f030ded80f 100644
--- a/app/controllers/profiles/avatars_controller.rb
+++ b/app/controllers/profiles/avatars_controller.rb
@@ -4,6 +4,6 @@ class Profiles::AvatarsController < Profiles::ApplicationController
Users::UpdateService.new(current_user, user: @user).execute { |user| user.remove_avatar! }
- redirect_to profile_path, status: 302
+ redirect_to profile_path, status: :found
end
end
diff --git a/app/controllers/profiles/chat_names_controller.rb b/app/controllers/profiles/chat_names_controller.rb
index 2353f0840d6..a186c5f36a8 100644
--- a/app/controllers/profiles/chat_names_controller.rb
+++ b/app/controllers/profiles/chat_names_controller.rb
@@ -39,7 +39,7 @@ class Profiles::ChatNamesController < Profiles::ApplicationController
flash[:alert] = "Could not delete chat nickname #{@chat_name.chat_name}."
end
- redirect_to profile_chat_names_path, status: 302
+ redirect_to profile_chat_names_path, status: :found
end
private
diff --git a/app/controllers/profiles/emails_controller.rb b/app/controllers/profiles/emails_controller.rb
index bbd7ba49d77..a39824ec9c8 100644
--- a/app/controllers/profiles/emails_controller.rb
+++ b/app/controllers/profiles/emails_controller.rb
@@ -19,7 +19,7 @@ class Profiles::EmailsController < Profiles::ApplicationController
Emails::DestroyService.new(current_user, user: current_user).execute(@email)
respond_to do |format|
- format.html { redirect_to profile_emails_url, status: 302 }
+ format.html { redirect_to profile_emails_url, status: :found }
format.js { head :ok }
end
end
diff --git a/app/controllers/profiles/gpg_keys_controller.rb b/app/controllers/profiles/gpg_keys_controller.rb
index 38e3eacd229..c32507756e8 100644
--- a/app/controllers/profiles/gpg_keys_controller.rb
+++ b/app/controllers/profiles/gpg_keys_controller.rb
@@ -21,7 +21,7 @@ class Profiles::GpgKeysController < Profiles::ApplicationController
@gpg_key.destroy
respond_to do |format|
- format.html { redirect_to profile_gpg_keys_url, status: 302 }
+ format.html { redirect_to profile_gpg_keys_url, status: :found }
format.js { head :ok }
end
end
@@ -30,7 +30,7 @@ class Profiles::GpgKeysController < Profiles::ApplicationController
@gpg_key.revoke
respond_to do |format|
- format.html { redirect_to profile_gpg_keys_url, status: 302 }
+ format.html { redirect_to profile_gpg_keys_url, status: :found }
format.js { head :ok }
end
end
diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb
index 12a6cd11f80..6035258667e 100644
--- a/app/controllers/profiles/keys_controller.rb
+++ b/app/controllers/profiles/keys_controller.rb
@@ -26,7 +26,7 @@ class Profiles::KeysController < Profiles::ApplicationController
Keys::DestroyService.new(current_user).execute(@key)
respond_to do |format|
- format.html { redirect_to profile_keys_url, status: 302 }
+ format.html { redirect_to profile_keys_url, status: :found }
format.js { head :ok }
end
end
diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb
index aa9789f8a0f..29ff18a1219 100644
--- a/app/controllers/profiles/two_factor_auths_controller.rb
+++ b/app/controllers/profiles/two_factor_auths_controller.rb
@@ -78,7 +78,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
def destroy
current_user.disable_two_factor!
- redirect_to profile_account_path, status: 302
+ redirect_to profile_account_path, status: :found
end
def skip
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index ac71f72e624..074db361949 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -34,12 +34,12 @@ class ProfilesController < Profiles::ApplicationController
redirect_to profile_personal_access_tokens_path
end
- def reset_rss_token
+ def reset_feed_token
Users::UpdateService.new(current_user, user: @user).execute! do |user|
- user.reset_rss_token!
+ user.reset_feed_token!
end
- flash[:notice] = "RSS token was successfully reset"
+ flash[:notice] = 'Feed token was successfully reset'
redirect_to profile_personal_access_tokens_path
end
@@ -93,8 +93,6 @@ class ProfilesController < Profiles::ApplicationController
:linkedin,
:location,
:name,
- :password,
- :password_confirmation,
:public_email,
:skype,
:twitter,
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index 5ab6d103c89..b4f814fd3a4 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -61,7 +61,7 @@ class Projects::ApplicationController < ApplicationController
def require_non_empty_project
# Be sure to return status code 303 to avoid a double DELETE:
# http://api.rubyonrails.org/classes/ActionController/Redirecting.html
- redirect_to project_path(@project), status: 303 if @project.empty_repo?
+ redirect_to project_path(@project), status: :see_other if @project.empty_repo?
end
def require_branch_head
diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb
index abc283d7aa9..6484a713f8e 100644
--- a/app/controllers/projects/artifacts_controller.rb
+++ b/app/controllers/projects/artifacts_controller.rb
@@ -7,6 +7,7 @@ class Projects::ArtifactsController < Projects::ApplicationController
before_action :authorize_read_build!
before_action :authorize_update_build!, only: [:keep]
before_action :extract_ref_name_and_path
+ before_action :set_request_format, only: [:file]
before_action :validate_artifacts!
before_action :entry, only: [:file]
@@ -101,4 +102,12 @@ class Projects::ArtifactsController < Projects::ApplicationController
render_404 unless @entry.exists?
end
+
+ def set_request_format
+ request.format = :html if set_request_format?
+ end
+
+ def set_request_format?
+ request.format != :json
+ end
end
diff --git a/app/controllers/projects/autocomplete_sources_controller.rb b/app/controllers/projects/autocomplete_sources_controller.rb
index 992c8ea6992..07627ffb69f 100644
--- a/app/controllers/projects/autocomplete_sources_controller.rb
+++ b/app/controllers/projects/autocomplete_sources_controller.rb
@@ -14,7 +14,7 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController
end
def labels
- render json: @autocomplete_service.labels(target)
+ render json: @autocomplete_service.labels_as_hash(target)
end
def milestones
diff --git a/app/controllers/projects/avatars_controller.rb b/app/controllers/projects/avatars_controller.rb
index 21a403f3765..a13d552dbd8 100644
--- a/app/controllers/projects/avatars_controller.rb
+++ b/app/controllers/projects/avatars_controller.rb
@@ -21,6 +21,6 @@ class Projects::AvatarsController < Projects::ApplicationController
@project.save
- redirect_to edit_project_path(@project), status: 302
+ redirect_to edit_project_path(@project), status: :found
end
end
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 0c1c286a0a4..ebc61264b39 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -3,10 +3,11 @@ class Projects::BlobController < Projects::ApplicationController
include ExtractsPath
include CreatesCommit
include RendersBlob
+ include NotesHelper
include ActionView::Helpers::SanitizeHelper
-
prepend_before_action :authenticate_user!, only: [:edit]
+ before_action :set_request_format, only: [:edit, :show, :update]
before_action :require_non_empty_project, except: [:new, :create]
before_action :authorize_download_code!
@@ -92,6 +93,7 @@ class Projects::BlobController < Projects::ApplicationController
@lines = Gitlab::Highlight.highlight(@blob.path, @blob.data, repository: @repository).lines
@form = UnfoldForm.new(params)
+
@lines = @lines[@form.since - 1..@form.to - 1].map(&:html_safe)
if @form.bottom?
@@ -102,11 +104,50 @@ class Projects::BlobController < Projects::ApplicationController
@match_line = "@@ -#{line}+#{line} @@"
end
- render layout: false
+ # We can keep only 'render_diff_lines' from this conditional when
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/44988 is done
+ if rendered_for_merge_request?
+ render_diff_lines
+ else
+ render layout: false
+ end
end
private
+ # Converts a String array to Gitlab::Diff::Line array
+ def render_diff_lines
+ @lines.map! do |line|
+ # These are marked as context lines but are loaded from blobs.
+ # We also have context lines loaded from diffs in other places.
+ diff_line = Gitlab::Diff::Line.new(line, 'context', nil, nil, nil)
+ diff_line.rich_text = line
+ diff_line
+ end
+
+ add_match_line
+
+ render json: @lines
+ end
+
+ def add_match_line
+ return unless @form.unfold?
+
+ if @form.bottom? && @form.to < @blob.lines.size
+ old_pos = @form.to - @form.offset
+ new_pos = @form.to
+ elsif @form.since != 1
+ old_pos = new_pos = @form.since
+ end
+
+ # Match line is not needed when it reaches the top limit or bottom limit of the file.
+ return unless new_pos
+
+ @match_line = Gitlab::Diff::Line.new(@match_line, 'match', nil, old_pos, new_pos)
+
+ @form.bottom? ? @lines.push(@match_line) : @lines.unshift(@match_line)
+ end
+
def blob
@blob ||= @repository.blob_at(@commit.id, @path)
@@ -188,6 +229,18 @@ class Projects::BlobController < Projects::ApplicationController
.last_for_path(@repository, @ref, @path).sha
end
+ # In Rails 4.2 if params[:format] is empty, Rails set it to :html
+ # But since Rails 5.0 the framework now looks for an extension.
+ # E.g. for `blob/master/CHANGELOG.md` in Rails 4 the format would be `:html`, but in Rails 5 on it'd be `:md`
+ # This before_action explicitly sets the `:html` format for all requests unless `:format` is set by a client e.g. by JS for XHR requests.
+ def set_request_format
+ request.format = :html if set_request_format?
+ end
+
+ def set_request_format?
+ params[:id].present? && params[:format].blank? && request.format != "json"
+ end
+
def show_html
environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit }
@environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
@@ -197,15 +250,14 @@ class Projects::BlobController < Projects::ApplicationController
end
def show_json
- json = blob_json(@blob)
- return render_404 unless json
-
+ set_last_commit_sha
path_segments = @path.split('/')
path_segments.pop
tree_path = path_segments.join('/')
- render json: json.merge(
+ json = {
id: @blob.id,
+ last_commit_sha: @last_commit_sha,
path: blob.path,
name: blob.name,
extension: blob.extension,
@@ -221,6 +273,10 @@ class Projects::BlobController < Projects::ApplicationController
commits_path: project_commits_path(project, @id),
tree_path: project_tree_path(project, File.join(@ref, tree_path)),
permalink: project_blob_path(project, File.join(@commit.id, @path))
- )
+ }
+
+ json.merge!(blob_json(@blob) || {}) unless params[:viewer] == 'none'
+
+ render json: json
end
end
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index b7b36f770f5..d1dc9fe9600 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -31,7 +31,10 @@ class Projects::BranchesController < Projects::ApplicationController
end
end
- render
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/48097
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ render
+ end
end
format.json do
branches = BranchesFinder.new(@repository, params).execute
@@ -95,7 +98,7 @@ class Projects::BranchesController < Projects::ApplicationController
flash_type = result[:status] == :error ? :alert : :notice
flash[flash_type] = result[:message]
- redirect_to project_branches_path(@project), status: 303
+ redirect_to project_branches_path(@project), status: :see_other
end
format.js { render nothing: true, status: result[:return_code] }
diff --git a/app/controllers/projects/clusters/applications_controller.rb b/app/controllers/projects/clusters/applications_controller.rb
index 90c7fa62216..a5c82caa897 100644
--- a/app/controllers/projects/clusters/applications_controller.rb
+++ b/app/controllers/projects/clusters/applications_controller.rb
@@ -5,9 +5,20 @@ class Projects::Clusters::ApplicationsController < Projects::ApplicationControll
before_action :authorize_create_cluster!, only: [:create]
def create
- Clusters::Applications::ScheduleInstallationService.new(project, current_user,
- application_class: @application_class,
- cluster: @cluster).execute
+ application = @application_class.find_or_initialize_by(cluster: @cluster)
+
+ if application.has_attribute?(:hostname)
+ application.hostname = params[:hostname]
+ end
+
+ if application.respond_to?(:oauth_application)
+ application.oauth_application = create_oauth_application(application)
+ end
+
+ application.save!
+
+ Clusters::Applications::ScheduleInstallationService.new(project, current_user).execute(application)
+
head :no_content
rescue StandardError
head :bad_request
@@ -22,4 +33,15 @@ class Projects::Clusters::ApplicationsController < Projects::ApplicationControll
def application_class
@application_class ||= Clusters::Cluster::APPLICATIONS[params[:application]] || render_404
end
+
+ def create_oauth_application(application)
+ oauth_application_params = {
+ name: params[:application],
+ redirect_uri: application.callback_url,
+ scopes: 'api read_user openid',
+ owner: current_user
+ }
+
+ Applications::CreateService.new(current_user, oauth_application_params).execute(request)
+ end
end
diff --git a/app/controllers/projects/clusters/gcp_controller.rb b/app/controllers/projects/clusters/gcp_controller.rb
deleted file mode 100644
index 6b0b22f8e73..00000000000
--- a/app/controllers/projects/clusters/gcp_controller.rb
+++ /dev/null
@@ -1,102 +0,0 @@
-class Projects::Clusters::GcpController < Projects::ApplicationController
- before_action :authorize_read_cluster!
- before_action :authorize_google_api, except: [:login]
- before_action :authorize_google_project_billing, only: [:new, :create]
- before_action :authorize_create_cluster!, only: [:new, :create]
- before_action :verify_billing, only: [:create]
-
- def login
- begin
- state = generate_session_key_redirect(gcp_new_namespace_project_clusters_path.to_s)
-
- @authorize_url = GoogleApi::CloudPlatform::Client.new(
- nil, callback_google_api_auth_url,
- state: state).authorize_url
- rescue GoogleApi::Auth::ConfigMissingError
- # no-op
- end
- end
-
- def new
- @cluster = ::Clusters::Cluster.new.tap do |cluster|
- cluster.build_provider_gcp
- end
- end
-
- def create
- @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
- end
- end
-
- private
-
- def verify_billing
- case google_project_billing_status
- when nil
- flash.now[:alert] = _('We could not verify that one of your projects on GCP has billing enabled. Please try again.')
- when false
- flash.now[:alert] = _('Please <a href=%{link_to_billing} target="_blank" rel="noopener noreferrer">enable billing for one of your projects to be able to create a Kubernetes cluster</a>, then try again.').html_safe % { link_to_billing: "https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral" }
- when true
- return
- end
-
- @cluster = ::Clusters::Cluster.new(create_params)
-
- render :new
- end
-
- def create_params
- params.require(:cluster).permit(
- :enabled,
- :name,
- :environment_scope,
- provider_gcp_attributes: [
- :gcp_project_id,
- :zone,
- :num_nodes,
- :machine_type
- ]).merge(
- provider_type: :gcp,
- platform_type: :kubernetes
- )
- end
-
- def authorize_google_api
- unless GoogleApi::CloudPlatform::Client.new(token_in_session, nil)
- .validate_token(expires_at_in_session)
- redirect_to action: 'login'
- end
- end
-
- def authorize_google_project_billing
- redis_token_key = CheckGcpProjectBillingWorker.store_session_token(token_in_session)
- CheckGcpProjectBillingWorker.perform_async(redis_token_key)
- end
-
- def google_project_billing_status
- CheckGcpProjectBillingWorker.get_billing_state(token_in_session)
- end
-
- def token_in_session
- @token_in_session ||=
- session[GoogleApi::CloudPlatform::Client.session_key_for_token]
- end
-
- def expires_at_in_session
- @expires_at_in_session ||=
- session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at]
- end
-
- def generate_session_key_redirect(uri)
- GoogleApi::CloudPlatform::Client.new_session_key_for_redirect_uri do |key|
- session[key] = uri
- end
- end
-end
diff --git a/app/controllers/projects/clusters/user_controller.rb b/app/controllers/projects/clusters/user_controller.rb
deleted file mode 100644
index d0db64b2fa9..00000000000
--- a/app/controllers/projects/clusters/user_controller.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-class Projects::Clusters::UserController < Projects::ApplicationController
- before_action :authorize_read_cluster!
- before_action :authorize_create_cluster!, only: [:new, :create]
-
- def new
- @cluster = ::Clusters::Cluster.new.tap do |cluster|
- cluster.build_platform_kubernetes
- end
- end
-
- def create
- @cluster = ::Clusters::CreateService
- .new(project, current_user, create_params)
- .execute
-
- if @cluster.persisted?
- redirect_to project_cluster_path(project, @cluster)
- else
- render :new
- end
- end
-
- private
-
- def create_params
- params.require(:cluster).permit(
- :enabled,
- :name,
- :environment_scope,
- platform_kubernetes_attributes: [
- :namespace,
- :api_url,
- :token,
- :ca_cert
- ]).merge(
- provider_type: :user,
- platform_type: :kubernetes
- )
- end
-end
diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb
index aeaba3a0acf..358fe59618b 100644
--- a/app/controllers/projects/clusters_controller.rb
+++ b/app/controllers/projects/clusters_controller.rb
@@ -1,10 +1,15 @@
class Projects::ClustersController < Projects::ApplicationController
- before_action :cluster, except: [:index, :new]
+ before_action :cluster, except: [:index, :new, :create_gcp, :create_user]
before_action :authorize_read_cluster!
+ before_action :generate_gcp_authorize_url, only: [:new]
+ before_action :validate_gcp_token, only: [:new]
+ before_action :gcp_cluster, only: [:new]
+ before_action :user_cluster, only: [:new]
before_action :authorize_create_cluster!, only: [:new]
before_action :authorize_update_cluster!, only: [:update]
before_action :authorize_admin_cluster!, only: [:destroy]
before_action :update_applications_status, only: [:status]
+ helper_method :token_in_session
STATUS_POLLING_INTERVAL = 10_000
@@ -57,13 +62,45 @@ class Projects::ClustersController < Projects::ApplicationController
def destroy
if cluster.destroy
flash[:notice] = _('Kubernetes cluster integration was successfully removed.')
- redirect_to project_clusters_path(project), status: 302
+ redirect_to project_clusters_path(project), status: :found
else
flash[:notice] = _('Kubernetes cluster integration was not removed.')
render :show
end
end
+ def create_gcp
+ @gcp_cluster = ::Clusters::CreateService
+ .new(project, current_user, create_gcp_cluster_params)
+ .execute(token_in_session)
+
+ if @gcp_cluster.persisted?
+ redirect_to project_cluster_path(project, @gcp_cluster)
+ else
+ generate_gcp_authorize_url
+ validate_gcp_token
+ user_cluster
+
+ render :new, locals: { active_tab: 'gcp' }
+ end
+ end
+
+ def create_user
+ @user_cluster = ::Clusters::CreateService
+ .new(project, current_user, create_user_cluster_params)
+ .execute(token_in_session)
+
+ if @user_cluster.persisted?
+ redirect_to project_cluster_path(project, @user_cluster)
+ else
+ generate_gcp_authorize_url
+ validate_gcp_token
+ gcp_cluster
+
+ render :new, locals: { active_tab: 'user' }
+ end
+ end
+
private
def cluster
@@ -71,19 +108,6 @@ class Projects::ClustersController < Projects::ApplicationController
.present(current_user: current_user)
end
- def create_params
- params.require(:cluster).permit(
- :enabled,
- :name,
- :provider_type,
- provider_gcp_attributes: [
- :gcp_project_id,
- :zone,
- :num_nodes,
- :machine_type
- ])
- end
-
def update_params
if cluster.managed?
params.require(:cluster).permit(
@@ -108,6 +132,80 @@ class Projects::ClustersController < Projects::ApplicationController
end
end
+ def create_gcp_cluster_params
+ params.require(:cluster).permit(
+ :enabled,
+ :name,
+ :environment_scope,
+ provider_gcp_attributes: [
+ :gcp_project_id,
+ :zone,
+ :num_nodes,
+ :machine_type
+ ]).merge(
+ provider_type: :gcp,
+ platform_type: :kubernetes
+ )
+ end
+
+ def create_user_cluster_params
+ params.require(:cluster).permit(
+ :enabled,
+ :name,
+ :environment_scope,
+ platform_kubernetes_attributes: [
+ :namespace,
+ :api_url,
+ :token,
+ :ca_cert
+ ]).merge(
+ provider_type: :user,
+ platform_type: :kubernetes
+ )
+ end
+
+ def generate_gcp_authorize_url
+ state = generate_session_key_redirect(new_project_cluster_path(@project).to_s)
+
+ @authorize_url = GoogleApi::CloudPlatform::Client.new(
+ nil, callback_google_api_auth_url,
+ state: state).authorize_url
+ rescue GoogleApi::Auth::ConfigMissingError
+ # no-op
+ end
+
+ def gcp_cluster
+ @gcp_cluster = ::Clusters::Cluster.new.tap do |cluster|
+ cluster.build_provider_gcp
+ end
+ end
+
+ def user_cluster
+ @user_cluster = ::Clusters::Cluster.new.tap do |cluster|
+ cluster.build_platform_kubernetes
+ end
+ end
+
+ def validate_gcp_token
+ @valid_gcp_token = GoogleApi::CloudPlatform::Client.new(token_in_session, nil)
+ .validate_token(expires_at_in_session)
+ end
+
+ def token_in_session
+ session[GoogleApi::CloudPlatform::Client.session_key_for_token]
+ end
+
+ def expires_at_in_session
+ @expires_at_in_session ||=
+ session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at]
+ end
+
+ def generate_session_key_redirect(uri)
+ GoogleApi::CloudPlatform::Client.new_session_key_for_redirect_uri do |key|
+ session[key] = uri
+ end
+ end
+
def authorize_update_cluster!
access_denied! unless can?(current_user, :update_cluster, cluster)
end
diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb
index 7b7cb52d7ed..9e495061f4e 100644
--- a/app/controllers/projects/commits_controller.rb
+++ b/app/controllers/projects/commits_controller.rb
@@ -9,6 +9,7 @@ class Projects::CommitsController < Projects::ApplicationController
before_action :assign_ref_vars
before_action :authorize_download_code!
before_action :set_commits
+ before_action :set_request_format, only: :show
def show
@merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened
@@ -61,6 +62,19 @@ class Projects::CommitsController < Projects::ApplicationController
@commits = prepare_commits_for_rendering(@commits)
end
+ # Rails 5 sets request.format from the extension.
+ # Explicitly set to :html.
+ def set_request_format
+ request.format = :html if set_request_format?
+ end
+
+ # Rails 5 sets request.format from extension.
+ # In this case if the ref ends with `.atom`, it's expected to be the html response,
+ # not the atom one. So explicitly set request.format as :html to act like rails4.
+ def set_request_format?
+ request.format.to_s == "text/html" || @commits.ref.ends_with?("atom")
+ end
+
def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42330')
end
diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb
index f43ef2e5f2f..06739d8fd4a 100644
--- a/app/controllers/projects/deploy_keys_controller.rb
+++ b/app/controllers/projects/deploy_keys_controller.rb
@@ -35,7 +35,7 @@ class Projects::DeployKeysController < Projects::ApplicationController
end
def update
- if deploy_key.update_attributes(update_params)
+ if deploy_key.update(update_params)
flash[:notice] = 'Deploy key was successfully updated.'
redirect_to_repository_settings(@project)
else
diff --git a/app/controllers/projects/discussions_controller.rb b/app/controllers/projects/discussions_controller.rb
index 8e86af43fee..78b9d53a780 100644
--- a/app/controllers/projects/discussions_controller.rb
+++ b/app/controllers/projects/discussions_controller.rb
@@ -21,7 +21,7 @@ class Projects::DiscussionsController < Projects::ApplicationController
def show
render json: {
- discussion_html: view_to_html_string('discussions/_diff_with_notes', discussion: discussion, expanded: true)
+ truncated_diff_lines: discussion.try(:truncated_diff_lines)
}
end
@@ -29,11 +29,6 @@ class Projects::DiscussionsController < Projects::ApplicationController
def render_discussion
if serialize_notes?
- # TODO - It is not needed to serialize notes when resolving
- # or unresolving discussions. We should remove this behavior
- # passing a parameter to DiscussionEntity to return an empty array
- # for notes.
- # Check issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/42853
prepare_notes_for_rendering(discussion.notes, merge_request)
render_json_with_discussions_serializer
else
@@ -44,7 +39,7 @@ class Projects::DiscussionsController < Projects::ApplicationController
def render_json_with_discussions_serializer
render json:
DiscussionSerializer.new(project: project, noteable: discussion.noteable, current_user: current_user, note_entity: ProjectNoteEntity)
- .represent(discussion, context: self)
+ .represent(discussion, context: self, render_truncated_diff_lines: true)
end
# Legacy method used to render discussions notes when not using Vue on views.
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index 52d528e816e..68353e6a210 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -2,11 +2,12 @@ class Projects::EnvironmentsController < Projects::ApplicationController
layout 'project'
before_action :authorize_read_environment!
before_action :authorize_create_environment!, only: [:new, :create]
- before_action :authorize_create_deployment!, only: [:stop]
+ before_action :authorize_stop_environment!, only: [:stop]
before_action :authorize_update_environment!, only: [:edit, :update]
before_action :authorize_admin_environment!, only: [:terminal, :terminal_websocket_authorize]
before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize, :metrics]
before_action :verify_api_request!, only: :terminal_websocket_authorize
+ before_action :expire_etag_cache, only: [:index]
def index
@environments = project.environments
@@ -115,7 +116,17 @@ class Projects::EnvironmentsController < Projects::ApplicationController
set_workhorse_internal_api_content_type
render json: Gitlab::Workhorse.terminal_websocket(terminal)
else
- render text: 'Not found', status: 404
+ render text: 'Not found', status: :not_found
+ end
+ end
+
+ def metrics_redirect
+ environment = project.default_environment
+
+ if environment
+ redirect_to environment_metrics_path(environment)
+ else
+ render :empty
end
end
@@ -148,6 +159,15 @@ class Projects::EnvironmentsController < Projects::ApplicationController
Gitlab::Workhorse.verify_api_request!(request.headers)
end
+ def expire_etag_cache
+ return if request.format.json?
+
+ # this forces to reload json content
+ Gitlab::EtagCaching::Store.new.tap do |store|
+ store.touch(project_environments_path(project, format: :json))
+ end
+ end
+
def environment_params
params.require(:environment).permit(:name, :external_url)
end
@@ -155,4 +175,8 @@ class Projects::EnvironmentsController < Projects::ApplicationController
def environment
@environment ||= project.environments.find(params[:id])
end
+
+ def authorize_stop_environment!
+ access_denied! unless can?(current_user, :stop_environment, environment)
+ end
end
diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb
index 07249fe3182..a52814e6e52 100644
--- a/app/controllers/projects/git_http_client_controller.rb
+++ b/app/controllers/projects/git_http_client_controller.rb
@@ -53,7 +53,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController
end
send_challenges
- render plain: "HTTP Basic: Access denied\n", status: 401
+ render plain: "HTTP Basic: Access denied\n", status: :unauthorized
rescue Gitlab::Auth::MissingPersonalAccessTokenError
render_missing_personal_access_token
end
@@ -83,7 +83,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController
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}",
- status: 401
+ status: :unauthorized
end
def repository
diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb
index f58ee3e9109..bc5f38f3c2b 100644
--- a/app/controllers/projects/group_links_controller.rb
+++ b/app/controllers/projects/group_links_controller.rb
@@ -24,7 +24,7 @@ class Projects::GroupLinksController < Projects::ApplicationController
def update
@group_link = @project.project_group_links.find(params[:id])
- @group_link.update_attributes(group_link_params)
+ @group_link.update(group_link_params)
end
def destroy
@@ -34,7 +34,7 @@ class Projects::GroupLinksController < Projects::ApplicationController
respond_to do |format|
format.html do
- redirect_to project_project_members_path(project), status: 302
+ redirect_to project_project_members_path(project), status: :found
end
format.js { head :ok }
end
diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb
index dd7aa1a67b9..2da2aad9b33 100644
--- a/app/controllers/projects/hooks_controller.rb
+++ b/app/controllers/projects/hooks_controller.rb
@@ -29,7 +29,7 @@ class Projects::HooksController < Projects::ApplicationController
end
def update
- if hook.update_attributes(hook_params)
+ if hook.update(hook_params)
flash[:notice] = 'Hook was successfully updated.'
redirect_to project_settings_integrations_path(@project)
else
@@ -48,7 +48,7 @@ class Projects::HooksController < Projects::ApplicationController
def destroy
hook.destroy
- redirect_to project_settings_integrations_path(@project), status: 302
+ redirect_to project_settings_integrations_path(@project), status: :found
end
private
@@ -58,8 +58,7 @@ class Projects::HooksController < Projects::ApplicationController
end
def hook_logs
- @hook_logs ||=
- Kaminari.paginate_array(hook.web_hook_logs.order(created_at: :desc)).page(params[:page])
+ @hook_logs ||= hook.web_hook_logs.recent.page(params[:page])
end
def hook_params
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index d69015c8665..7c897b2d86c 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -4,14 +4,15 @@ class Projects::IssuesController < Projects::ApplicationController
include IssuableActions
include ToggleAwardEmoji
include IssuableCollections
+ include IssuesCalendar
include SpammableActions
prepend_before_action :authenticate_user!, only: [:new]
before_action :whitelist_query_limiting, only: [:create, :create_merge_request, :move, :bulk_update]
before_action :check_issues_available!
- before_action :issue, except: [:index, :new, :create, :bulk_update]
- before_action :set_issuables_index, only: [:index]
+ before_action :issue, except: [:index, :calendar, :new, :create, :bulk_update]
+ before_action :set_issuables_index, only: [:index, :calendar]
# Allow write(create) issue
before_action :authorize_create_issue!, only: [:new, :create]
@@ -39,6 +40,10 @@ class Projects::IssuesController < Projects::ApplicationController
end
end
+ def calendar
+ render_issues_calendar(@issuables)
+ end
+
def new
params[:issue] ||= ActionController::Parameters.new(
assignee_ids: ""
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index dd12d30a085..e69faae754a 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -2,11 +2,12 @@ class Projects::JobsController < Projects::ApplicationController
include SendFileUpload
before_action :build, except: [:index, :cancel_all]
- before_action :authorize_read_build!,
- only: [:index, :show, :status, :raw, :trace]
+ before_action :authorize_read_build!
before_action :authorize_update_build!,
except: [:index, :show, :status, :raw, :trace, :cancel_all, :erase]
before_action :authorize_erase_build!, only: [:erase]
+ before_action :authorize_use_build_terminal!, only: [:terminal, :terminal_workhorse_authorize]
+ before_action :verify_api_request!, only: :terminal_websocket_authorize
layout 'project'
@@ -44,12 +45,10 @@ class Projects::JobsController < Projects::ApplicationController
end
def show
- @builds = @project.pipelines
- .find_by_sha(@build.sha)
- .builds
+ @pipeline = @build.pipeline
+ @builds = @pipeline.builds
.order('id DESC')
.present(current_user: current_user)
- @pipeline = @build.pipeline
respond_to do |format|
format.html
@@ -136,6 +135,15 @@ class Projects::JobsController < Projects::ApplicationController
end
end
+ def terminal
+ end
+
+ # GET .../terminal.ws : implemented in gitlab-workhorse
+ def terminal_websocket_authorize
+ set_workhorse_internal_api_content_type
+ render json: Gitlab::Workhorse.terminal_websocket(@build.terminal_specification)
+ end
+
private
def authorize_update_build!
@@ -146,6 +154,14 @@ class Projects::JobsController < Projects::ApplicationController
return access_denied! unless can?(current_user, :erase_build, build)
end
+ def authorize_use_build_terminal!
+ return access_denied! unless can?(current_user, :create_build_terminal, build)
+ end
+
+ def verify_api_request!
+ Gitlab::Workhorse.verify_api_request!(request.headers)
+ end
+
def raw_send_params
{ type: 'text/plain; charset=utf-8', disposition: 'inline' }
end
@@ -160,7 +176,7 @@ class Projects::JobsController < Projects::ApplicationController
def build
@build ||= project.builds.find(params[:id])
- .present(current_user: current_user)
+ .present(current_user: current_user)
end
def build_path(build)
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index 91016f6494e..21d3c918581 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -39,7 +39,7 @@ class Projects::LabelsController < Projects::ApplicationController
else
respond_to do |format|
format.html { render :new }
- format.json { render json: { message: @label.errors.messages }, status: 400 }
+ format.json { render json: { message: @label.errors.messages }, status: :bad_request }
end
end
end
@@ -115,7 +115,7 @@ class Projects::LabelsController < Projects::ApplicationController
flash[:notice] = "#{@label.title} promoted to <a href=\"#{group_labels_path(@project.group)}\">group label</a>.".html_safe
respond_to do |format|
format.html do
- redirect_to(project_labels_path(@project), status: 303)
+ redirect_to(project_labels_path(@project), status: :see_other)
end
format.json do
render json: { url: project_labels_path(@project) }
diff --git a/app/controllers/projects/lfs_api_controller.rb b/app/controllers/projects/lfs_api_controller.rb
index ee4ed674110..c64ccc3d473 100644
--- a/app/controllers/projects/lfs_api_controller.rb
+++ b/app/controllers/projects/lfs_api_controller.rb
@@ -25,7 +25,7 @@ class Projects::LfsApiController < Projects::GitHttpClientController
message: 'Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.',
documentation_url: "#{Gitlab.config.gitlab.url}/help"
},
- status: 501
+ status: :not_implemented
)
end
@@ -93,7 +93,7 @@ class Projects::LfsApiController < Projects::GitHttpClientController
end
def lfs_check_batch_operation!
- if upload_request? && Gitlab::Database.read_only?
+ if batch_operation_disallowed?
render(
json: {
message: lfs_read_only_message
@@ -105,6 +105,11 @@ class Projects::LfsApiController < Projects::GitHttpClientController
end
# Overridden in EE
+ def batch_operation_disallowed?
+ upload_request? && Gitlab::Database.read_only?
+ end
+
+ # Overridden in EE
def lfs_read_only_message
_('You cannot write to this read-only GitLab instance.')
end
diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb
index 43d8867a536..dd7e673ec75 100644
--- a/app/controllers/projects/lfs_storage_controller.rb
+++ b/app/controllers/projects/lfs_storage_controller.rb
@@ -18,7 +18,7 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
def upload_authorize
set_workhorse_internal_api_content_type
- authorized = LfsObjectUploader.workhorse_authorize
+ authorized = LfsObjectUploader.workhorse_authorize(has_length: true)
authorized.merge!(LfsOid: oid, LfsSize: size)
render json: authorized
@@ -28,7 +28,7 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
if store_file!(oid, size)
head 200
else
- render plain: 'Unprocessable entity', status: 422
+ render plain: 'Unprocessable entity', status: :unprocessable_entity
end
rescue ActiveRecord::RecordInvalid
render_lfs_forbidden
diff --git a/app/controllers/projects/merge_requests/application_controller.rb b/app/controllers/projects/merge_requests/application_controller.rb
index 67d4ea2ca8f..8e4aeec16dc 100644
--- a/app/controllers/projects/merge_requests/application_controller.rb
+++ b/app/controllers/projects/merge_requests/application_controller.rb
@@ -15,7 +15,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
def merge_request_params_attributes
[
- :allow_maintainer_to_push,
+ :allow_collaboration,
:assignee_id,
:description,
:force_remove_source_branch,
@@ -24,6 +24,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
:source_branch,
:source_project_id,
:state_event,
+ :squash,
:target_branch,
:target_project_id,
:task_num,
diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb
index fe8525a488c..48e02581d54 100644
--- a/app/controllers/projects/merge_requests/diffs_controller.rb
+++ b/app/controllers/projects/merge_requests/diffs_controller.rb
@@ -9,17 +9,21 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
before_action :define_diff_comment_vars
def show
- @environment = @merge_request.environments_for(current_user).last
-
- render json: { html: view_to_html_string("projects/merge_requests/diffs/_diffs") }
+ render_diffs
end
def diff_for_path
- render_diff_for_path(@diffs)
+ render_diffs
end
private
+ def render_diffs
+ @environment = @merge_request.environments_for(current_user).last
+
+ render json: DiffsSerializer.new(current_user: current_user).represent(@diffs, additional_attributes)
+ end
+
def define_diff_vars
@merge_request_diffs = @merge_request.merge_request_diffs.viewable.order_id_desc
@compare = commit || find_merge_request_diff_compare
@@ -63,6 +67,19 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
end
end
+ def additional_attributes
+ {
+ environment: @environment,
+ merge_request: @merge_request,
+ merge_request_diff: @merge_request_diff,
+ merge_request_diffs: @merge_request_diffs,
+ start_version: @start_version,
+ start_sha: @start_sha,
+ commit: @commit,
+ latest_diff: @merge_request_diff&.latest?
+ }
+ end
+
def define_diff_comment_vars
@new_diff_note_attrs = {
noteable_type: 'MergeRequest',
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 62b739918e6..dc6551fc761 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -28,21 +28,23 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
def show
- validates_merge_request
- close_merge_request_without_source_project
- check_if_can_be_merged
-
- # Return if the response has already been rendered
- return if response_body
+ close_merge_request_if_no_source_project
+ mark_merge_request_mergeable
respond_to do |format|
format.html do
+ # use next to appease Rubocop
+ next render('invalid') if target_branch_missing?
+
# Build a note object for comment form
@note = @project.notes.new(noteable: @merge_request)
@noteable = @merge_request
@commits_count = @merge_request.commits_count
+ # TODO cleanup- Fatih Simon Create an issue to remove these after the refactoring
+ # we no longer render notes here. I see it will require a small frontend refactoring,
+ # since we gather some data from this collection.
@discussions = @merge_request.discussions
@notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes), @noteable)
@@ -116,7 +118,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
format.json do
- render json: @merge_request.to_json(include: { milestone: {}, assignee: { only: [:name, :username], methods: [:avatar_url] }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short])
+ render json: serializer.represent(@merge_request, serializer: 'basic')
end
end
rescue ActiveRecord::StaleObjectError
@@ -190,7 +192,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
deployment = environment.first_deployment_for(@merge_request.diff_head_sha)
stop_url =
- if environment.stop_action? && can?(current_user, :create_deployment, environment)
+ if can?(current_user, :stop_environment, environment)
stop_project_environment_path(project, environment)
end
@@ -225,7 +227,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
def rebase
RebaseWorker.perform_async(@merge_request.id, current_user.id)
- render nothing: true, status: 200
+ render nothing: true, status: :ok
end
protected
@@ -234,26 +236,12 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
alias_method :issuable, :merge_request
alias_method :awardable, :merge_request
- def validates_merge_request
- # Show git not found page
- # if there is no saved commits between source & target branch
- if @merge_request.has_no_commits?
- # and if target branch doesn't exist
- return invalid_mr unless @merge_request.target_branch_exists?
- end
- end
-
- def invalid_mr
- # Render special view for MR with removed target branch
- render 'invalid'
- end
-
def merge_params
params.permit(merge_params_attributes)
end
def merge_params_attributes
- [:should_remove_source_branch, :commit_message]
+ [:should_remove_source_branch, :commit_message, :squash]
end
def merge_when_pipeline_succeeds_active?
@@ -261,7 +249,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
@merge_request.head_pipeline && @merge_request.head_pipeline.active?
end
- def close_merge_request_without_source_project
+ def close_merge_request_if_no_source_project
if !@merge_request.source_project && @merge_request.open?
@merge_request.close
end
@@ -269,7 +257,11 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
private
- def check_if_can_be_merged
+ def target_branch_missing?
+ @merge_request.has_no_commits? && !@merge_request.target_branch_exists?
+ end
+
+ def mark_merge_request_mergeable
@merge_request.check_if_can_be_merged
end
@@ -282,7 +274,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
return :sha_mismatch if params[:sha] != @merge_request.diff_head_sha
- @merge_request.update(merge_error: nil)
+ @merge_request.update(merge_error: nil, squash: merge_params.fetch(:squash, false))
if params[:merge_when_pipeline_succeeds].present?
return :failed unless @merge_request.actual_head_pipeline
@@ -296,14 +288,14 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
elsif @merge_request.actual_head_pipeline.success?
# This can be triggered when a user clicks the auto merge button while
# the tests finish at about the same time
- @merge_request.merge_async(current_user.id, params)
+ @merge_request.merge_async(current_user.id, merge_params)
:success
else
:failed
end
else
- @merge_request.merge_async(current_user.id, params)
+ @merge_request.merge_async(current_user.id, merge_params)
:success
end
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index c5a044541f1..5e86ec93f34 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -1,4 +1,5 @@
class Projects::MilestonesController < Projects::ApplicationController
+ include Gitlab::Utils::StrongMemoize
include MilestoneActions
before_action :check_issuables_available!
@@ -76,7 +77,7 @@ class Projects::MilestonesController < Projects::ApplicationController
def promote
promoted_milestone = Milestones::PromoteService.new(project, current_user).execute(milestone)
- flash[:notice] = "#{milestone.title} promoted to <a href=\"#{group_milestone_path(project.group, promoted_milestone.iid)}\">group milestone</a>.".html_safe
+ flash[:notice] = "#{milestone.title} promoted to <a href=\"#{group_milestone_path(project.group, promoted_milestone.iid)}\"><u>group milestone</u></a>.".html_safe
respond_to do |format|
format.html do
redirect_to project_milestones_path(project)
@@ -95,7 +96,7 @@ class Projects::MilestonesController < Projects::ApplicationController
Milestones::DestroyService.new(project, current_user).execute(milestone)
respond_to do |format|
- format.html { redirect_to namespace_project_milestones_path, status: 303 }
+ format.html { redirect_to namespace_project_milestones_path, status: :see_other }
format.js { head :ok }
end
end
@@ -103,7 +104,7 @@ class Projects::MilestonesController < Projects::ApplicationController
protected
def milestones
- @milestones ||= begin
+ strong_memoize(:milestones) do
MilestonesFinder.new(search_params).execute
end
end
@@ -121,10 +122,10 @@ class Projects::MilestonesController < Projects::ApplicationController
end
def search_params
- if @project.group && can?(current_user, :read_group, @project.group)
- group = @project.group
+ if request.format.json? && @project.group && can?(current_user, :read_group, @project.group)
+ groups = @project.group.self_and_ancestors_ids
end
- params.permit(:state).merge(project_ids: @project.id, group_ids: group&.id)
+ params.permit(:state).merge(project_ids: @project.id, group_ids: groups)
end
end
diff --git a/app/controllers/projects/mirrors_controller.rb b/app/controllers/projects/mirrors_controller.rb
index 5698ff4e706..3b24d231f3d 100644
--- a/app/controllers/projects/mirrors_controller.rb
+++ b/app/controllers/projects/mirrors_controller.rb
@@ -13,7 +13,7 @@ class Projects::MirrorsController < Projects::ApplicationController
end
def update
- if project.update_attributes(mirror_params)
+ if project.update(mirror_params)
flash[:notice] = 'Mirroring settings were successfully updated.'
else
flash[:alert] = project.errors.full_messages.join(', ').html_safe
diff --git a/app/controllers/projects/pipeline_schedules_controller.rb b/app/controllers/projects/pipeline_schedules_controller.rb
index fa258f3d9af..aeda7b3edf5 100644
--- a/app/controllers/projects/pipeline_schedules_controller.rb
+++ b/app/controllers/projects/pipeline_schedules_controller.rb
@@ -64,7 +64,7 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController
def destroy
if schedule.destroy
- redirect_to pipeline_schedules_path(@project), status: 302
+ redirect_to pipeline_schedules_path(@project), status: :found
else
redirect_to pipeline_schedules_path(@project),
status: :forbidden,
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 6b40fc2fe68..45cef123c34 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -13,7 +13,7 @@ class Projects::PipelinesController < Projects::ApplicationController
def index
@scope = params[:scope]
@pipelines = PipelinesFinder
- .new(project, scope: @scope)
+ .new(project, current_user, scope: @scope)
.execute
.page(params[:page])
.per(30)
@@ -23,8 +23,6 @@ class Projects::PipelinesController < Projects::ApplicationController
@finished_count = limited_pipelines_count(project, 'finished')
@pipelines_count = limited_pipelines_count(project)
- Gitlab::Ci::Pipeline::Preloader.preload(@pipelines)
-
respond_to do |format|
format.html
format.json do
@@ -34,7 +32,7 @@ class Projects::PipelinesController < Projects::ApplicationController
pipelines: PipelineSerializer
.new(project: @project, current_user: @current_user)
.with_pagination(request, response)
- .represent(@pipelines, disable_coverage: true),
+ .represent(@pipelines, disable_coverage: true, preload: true),
count: {
all: @pipelines_count,
running: @running_count,
@@ -180,7 +178,7 @@ class Projects::PipelinesController < Projects::ApplicationController
end
def limited_pipelines_count(project, scope = nil)
- finder = PipelinesFinder.new(project, scope: scope)
+ finder = PipelinesFinder.new(project, current_user, scope: scope)
view_context.limited_counter_with_delimiter(finder.execute)
end
diff --git a/app/controllers/projects/releases_controller.rb b/app/controllers/projects/releases_controller.rb
index 3e0a530fdb9..19e09b3af6f 100644
--- a/app/controllers/projects/releases_controller.rb
+++ b/app/controllers/projects/releases_controller.rb
@@ -14,7 +14,7 @@ class Projects::ReleasesController < Projects::ApplicationController
# it exists only to save a description to each Tag.
# If description is empty we should destroy the existing record.
if release_params[:description].present?
- release.update_attributes(release_params)
+ release.update(release_params)
else
release.destroy
end
diff --git a/app/controllers/projects/repositories_controller.rb b/app/controllers/projects/repositories_controller.rb
index d01f324e6fd..ecb2ece7532 100644
--- a/app/controllers/projects/repositories_controller.rb
+++ b/app/controllers/projects/repositories_controller.rb
@@ -24,7 +24,7 @@ class Projects::RepositoriesController < Projects::ApplicationController
send_git_archive @repository, ref: @ref, format: params[:format], append_sha: append_sha
rescue => ex
logger.error("#{self.class.name}: #{ex}")
- return git_not_found!
+ git_not_found!
end
def assign_archive_vars
diff --git a/app/controllers/projects/runner_projects_controller.rb b/app/controllers/projects/runner_projects_controller.rb
index 0ec2490655f..c098c82081e 100644
--- a/app/controllers/projects/runner_projects_controller.rb
+++ b/app/controllers/projects/runner_projects_controller.rb
@@ -9,9 +9,8 @@ class Projects::RunnerProjectsController < Projects::ApplicationController
return head(403) unless can?(current_user, :assign_runner, @runner)
path = project_runners_path(project)
- runner_project = @runner.assign_to(project, current_user)
- if runner_project.persisted?
+ if @runner.assign_to(project, current_user)
redirect_to path
else
redirect_to path, alert: 'Failed adding runner to project'
@@ -22,6 +21,6 @@ class Projects::RunnerProjectsController < Projects::ApplicationController
runner_project = project.runner_projects.find(params[:id])
runner_project.destroy
- redirect_to project_runners_path(project), status: 302
+ redirect_to project_runners_path(project), status: :found
end
end
diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb
index bef94cea989..cc7cce887bf 100644
--- a/app/controllers/projects/runners_controller.rb
+++ b/app/controllers/projects/runners_controller.rb
@@ -24,7 +24,7 @@ class Projects::RunnersController < Projects::ApplicationController
@runner.destroy
end
- redirect_to project_runners_path(@project), status: 302
+ redirect_to project_runners_path(@project), status: :found
end
def resume
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
index a5ea9ff7ed7..d55046047ae 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -34,20 +34,20 @@ class Projects::ServicesController < Projects::ApplicationController
private
def service_test_response
- if @service.update_attributes(service_params[:service])
+ if @service.update(service_params[:service])
data = @service.test_data(project, current_user)
outcome = @service.test(data)
if outcome[:success]
{}
else
- { error: true, message: 'Test failed.', service_response: outcome[:result].to_s }
+ { error: true, message: 'Test failed.', service_response: outcome[:result].to_s, test_failed: true }
end
else
- { error: true, message: 'Validations failed.', service_response: @service.errors.full_messages.join(',') }
+ { error: true, message: 'Validations failed.', service_response: @service.errors.full_messages.join(','), test_failed: false }
end
rescue Gitlab::HTTP::BlockedUrlError => e
- { error: true, message: 'Test failed.', service_response: e.message }
+ { error: true, message: 'Test failed.', service_response: e.message, test_failed: true }
end
def success_message
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index 1d850baf012..322ec096ffb 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -41,7 +41,7 @@ module Projects
:runners_token, :builds_enabled, :build_allow_git_fetch,
:build_timeout_human_readable, :build_coverage_regex, :public_builds,
:auto_cancel_pending_pipelines, :ci_config_path,
- auto_devops_attributes: [:id, :domain, :enabled]
+ auto_devops_attributes: [:id, :domain, :enabled, :deploy_strategy]
)
end
@@ -74,7 +74,7 @@ module Projects
.ordered
.page(params[:page]).per(20)
- @shared_runners = ::Ci::Runner.shared.active
+ @shared_runners = ::Ci::Runner.instance_type.active
@shared_runners_count = @shared_runners.count(:all)
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index 208a1d19862..f742d7edf83 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -82,7 +82,7 @@ class Projects::SnippetsController < Projects::ApplicationController
@snippet.destroy
- redirect_to project_snippets_path(@project), status: 302
+ redirect_to project_snippets_path(@project), status: :found
end
protected
diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb
index b62d7d9b7c5..b17753222a0 100644
--- a/app/controllers/projects/tags_controller.rb
+++ b/app/controllers/projects/tags_controller.rb
@@ -50,7 +50,7 @@ class Projects::TagsController < Projects::ApplicationController
respond_to do |format|
if result[:status] == :success
format.html do
- redirect_to project_tags_path(@project), status: 303
+ redirect_to project_tags_path(@project), status: :see_other
end
format.js
diff --git a/app/controllers/projects/templates_controller.rb b/app/controllers/projects/templates_controller.rb
index 694b468c8d3..52d6fb82093 100644
--- a/app/controllers/projects/templates_controller.rb
+++ b/app/controllers/projects/templates_controller.rb
@@ -14,6 +14,6 @@ class Projects::TemplatesController < Projects::ApplicationController
def get_template_class
template_types = { issue: Gitlab::Template::IssueTemplate, merge_request: Gitlab::Template::MergeRequestTemplate }.with_indifferent_access
@template_type = template_types[params[:template_type]]
- render json: [], status: 404 unless @template_type
+ render json: [], status: :not_found unless @template_type
end
end
diff --git a/app/controllers/projects/triggers_controller.rb b/app/controllers/projects/triggers_controller.rb
index e04145dd0b3..6f3de43f85a 100644
--- a/app/controllers/projects/triggers_controller.rb
+++ b/app/controllers/projects/triggers_controller.rb
@@ -50,7 +50,7 @@ class Projects::TriggersController < Projects::ApplicationController
flash[:alert] = "Could not remove the trigger."
end
- redirect_to project_settings_ci_cd_path(@project), status: 302
+ redirect_to project_settings_ci_cd_path(@project), status: :found
end
private
diff --git a/app/controllers/projects/uploads_controller.rb b/app/controllers/projects/uploads_controller.rb
index f5cf089ad98..7a85046164c 100644
--- a/app/controllers/projects/uploads_controller.rb
+++ b/app/controllers/projects/uploads_controller.rb
@@ -1,11 +1,13 @@
class Projects::UploadsController < Projects::ApplicationController
include UploadsActions
+ include WorkhorseRequest
# These will kick you out if you don't have access.
skip_before_action :project, :repository,
if: -> { action_name == 'show' && image_or_video? }
- before_action :authorize_upload_file!, only: [:create]
+ before_action :authorize_upload_file!, only: [:create, :authorize]
+ before_action :verify_workhorse_api!, only: [:authorize]
private
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index 1b0751f48c5..c01066c688a 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -14,6 +14,8 @@ class Projects::WikisController < Projects::ApplicationController
def show
@page = @project_wiki.find_page(params[:id], params[:version_id])
+ view_param = @project_wiki.empty? ? params[:view] : 'create'
+
if @page
render 'show'
elsif file = @project_wiki.find_file(params[:id], params[:version_id])
@@ -26,12 +28,12 @@ class Projects::WikisController < Projects::ApplicationController
disposition: 'inline',
filename: file.name
)
- else
- return render('empty') unless can?(current_user, :create_wiki, @project)
-
+ elsif can?(current_user, :create_wiki, @project) && view_param == 'create'
@page = build_page(title: params[:id])
render 'edit'
+ else
+ render 'empty'
end
end
@@ -93,6 +95,7 @@ class Projects::WikisController < Projects::ApplicationController
def destroy
@page = @project_wiki.find_page(params[:id])
+
WikiPages::DestroyService.new(@project, current_user).execute(@page)
redirect_to project_wiki_path(@project, :home),
@@ -117,7 +120,7 @@ class Projects::WikisController < Projects::ApplicationController
rescue ProjectWiki::CouldNotCreateWikiError
flash[:notice] = "Could not create Wiki Repository at this time. Please try again later."
redirect_to project_path(@project)
- return false
+ false
end
def wiki_params
@@ -126,7 +129,7 @@ class Projects::WikisController < Projects::ApplicationController
def build_page(args)
WikiPage.new(@project_wiki).tap do |page|
- page.update_attributes(args)
+ page.update_attributes(args) # rubocop:disable Rails/ActiveRecordAliases
end
end
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index a93b116c6fe..9d1c44db137 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -2,6 +2,7 @@ class ProjectsController < Projects::ApplicationController
include IssuableCollections
include ExtractsPath
include PreviewMarkdown
+ include SendFileUpload
before_action :whitelist_query_limiting, only: [:create]
before_action :authenticate_user!, except: [:index, :show, :activity, :refs]
@@ -63,7 +64,7 @@ class ProjectsController < Projects::ApplicationController
redirect_to(edit_project_path(@project))
end
else
- flash[:alert] = result[:message]
+ flash.now[:alert] = result[:message]
format.html { render 'edit' }
end
@@ -132,7 +133,7 @@ class ProjectsController < Projects::ApplicationController
::Projects::DestroyService.new(@project, current_user, {}).async_execute
flash[:notice] = _("Project '%{project_name}' is in the process of being deleted.") % { project_name: @project.full_name }
- redirect_to dashboard_projects_path, status: 302
+ redirect_to dashboard_projects_path, status: :found
rescue Projects::DestroyService::DestroyError => ex
redirect_to edit_project_path(@project), status: 302, alert: ex.message
end
@@ -188,9 +189,9 @@ class ProjectsController < Projects::ApplicationController
end
def download_export
- export_project_path = @project.export_project_path
-
- if export_project_path
+ if export_project_object_storage?
+ send_upload(@project.import_export_upload.export_file)
+ elsif export_project_path
send_file export_project_path, disposition: 'attachment'
else
redirect_to(
@@ -247,13 +248,13 @@ class ProjectsController < Projects::ApplicationController
if find_branches
branches = BranchesFinder.new(@repository, params).execute.take(100).map(&:name)
- options[s_('RefSwitcher|Branches')] = branches
+ options['Branches'] = branches
end
if find_tags && @repository.tag_count.nonzero?
tags = TagsFinder.new(@repository, params).execute.take(100).map(&:name)
- options[s_('RefSwitcher|Tags')] = tags
+ options['Tags'] = tags
end
# If reference is commit id - we should add it to branch/tag selectbox
@@ -265,8 +266,6 @@ class ProjectsController < Projects::ApplicationController
render json: options.to_json
end
- private
-
# Render project landing depending of which features are available
# So if page is not availble in the list it renders the next page
#
@@ -347,6 +346,7 @@ class ProjectsController < Projects::ApplicationController
:visibility_level,
:template_name,
:merge_method,
+ :initialize_with_readme,
project_feature_attributes: %i[
builds_access_level
@@ -423,4 +423,12 @@ class ProjectsController < Projects::ApplicationController
def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42440')
end
+
+ def export_project_path
+ @export_project_path ||= @project.export_project_path
+ end
+
+ def export_project_object_storage?
+ @project.export_project_object_exists?
+ end
end
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index f5a222b3a48..e6d6965036e 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -3,6 +3,9 @@ class RegistrationsController < Devise::RegistrationsController
include AcceptsPendingInvitations
before_action :whitelist_query_limiting, only: [:destroy]
+ before_action :ensure_terms_accepted,
+ if: -> { Gitlab::CurrentSettings.current_application_settings.enforce_terms? },
+ only: [:create]
def new
redirect_to(new_user_session_path)
@@ -18,7 +21,9 @@ class RegistrationsController < Devise::RegistrationsController
if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha
accept_pending_invitations
- super
+ super do |new_user|
+ persist_accepted_terms_if_required(new_user)
+ end
else
flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
flash.delete :recaptcha_error
@@ -40,6 +45,16 @@ class RegistrationsController < Devise::RegistrationsController
protected
+ def persist_accepted_terms_if_required(new_user)
+ return unless new_user.persisted?
+ return unless Gitlab::CurrentSettings.current_application_settings.enforce_terms?
+
+ if terms_accepted?
+ terms = ApplicationSetting::Term.latest
+ Users::RespondToTermsService.new(new_user, terms).execute(accepted: true)
+ end
+ end
+
def destroy_confirmation_valid?
if current_user.confirm_deletion_with_password?
current_user.valid_password?(params[:password])
@@ -91,4 +106,14 @@ class RegistrationsController < Devise::RegistrationsController
def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42380')
end
+
+ def ensure_terms_accepted
+ return if terms_accepted?
+
+ redirect_to new_user_session_path, alert: _('You must accept our Terms of Service and privacy policy in order to register an account')
+ end
+
+ def terms_accepted?
+ Gitlab::Utils.to_boolean(params[:terms_opt_in])
+ end
end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 1a339f76d26..9dd652206fe 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -3,21 +3,27 @@ class SessionsController < Devise::SessionsController
include AuthenticatesWithTwoFactor
include Devise::Controllers::Rememberable
include Recaptcha::ClientHelper
+ include Recaptcha::Verify
skip_before_action :check_two_factor_requirement, only: [:destroy]
prepend_before_action :check_initial_setup, only: [:new]
prepend_before_action :authenticate_with_two_factor,
if: :two_factor_enabled?, only: [:create]
+ prepend_before_action :check_captcha, only: [:create]
prepend_before_action :store_redirect_uri, only: [:new]
+ prepend_before_action :ldap_servers, only: [:new, :create]
before_action :auto_sign_in_with_provider, only: [:new]
before_action :load_recaptcha
after_action :log_failed_login, only: [:new], if: :failed_login?
+ helper_method :captcha_enabled?
+
+ CAPTCHA_HEADER = 'X-GitLab-Show-Login-Captcha'.freeze
+
def new
set_minimum_password_length
- @ldap_servers = Gitlab::Auth::LDAP::Config.available_servers
super
end
@@ -26,8 +32,8 @@ class SessionsController < Devise::SessionsController
super do |resource|
# User has successfully signed in, so clear any unused reset token
if resource.reset_password_token.present?
- resource.update_attributes(reset_password_token: nil,
- reset_password_sent_at: nil)
+ resource.update(reset_password_token: nil,
+ reset_password_sent_at: nil)
end
# hide the signed-in notification
@@ -46,6 +52,43 @@ class SessionsController < Devise::SessionsController
private
+ def captcha_enabled?
+ request.headers[CAPTCHA_HEADER] && Gitlab::Recaptcha.enabled?
+ end
+
+ # From https://github.com/plataformatec/devise/wiki/How-To:-Use-Recaptcha-with-Devise#devisepasswordscontroller
+ def check_captcha
+ return unless user_params[:password].present?
+ return unless captcha_enabled?
+ return unless Gitlab::Recaptcha.load_configurations!
+
+ if verify_recaptcha
+ increment_successful_login_captcha_counter
+ else
+ increment_failed_login_captcha_counter
+
+ self.resource = resource_class.new
+ flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
+ flash.delete :recaptcha_error
+
+ respond_with_navigational(resource) { render :new }
+ end
+ end
+
+ def increment_failed_login_captcha_counter
+ Gitlab::Metrics.counter(
+ :failed_login_captcha_total,
+ 'Number of failed CAPTCHA attempts for logins'.freeze
+ ).increment
+ end
+
+ def increment_successful_login_captcha_counter
+ Gitlab::Metrics.counter(
+ :successful_login_captcha_total,
+ 'Number of successful CAPTCHA attempts for logins'.freeze
+ ).increment
+ end
+
def log_failed_login
Gitlab::AppLogger.info("Failed Login: username=#{user_params[:login]} ip=#{request.remote_ip}")
end
@@ -152,6 +195,10 @@ class SessionsController < Devise::SessionsController
Gitlab::Recaptcha.load_configurations!
end
+ def ldap_servers
+ @ldap_servers ||= Gitlab::Auth::LDAP::Config.available_servers
+ end
+
def authentication_method
if user_params[:otp_attempt]
"two-factor"
diff --git a/app/controllers/sherlock/transactions_controller.rb b/app/controllers/sherlock/transactions_controller.rb
index cb6c3a7cd98..ae4953c3259 100644
--- a/app/controllers/sherlock/transactions_controller.rb
+++ b/app/controllers/sherlock/transactions_controller.rb
@@ -13,7 +13,7 @@ module Sherlock
def destroy_all
Gitlab::Sherlock.collection.clear
- redirect_to :back, status: 302
+ redirect_to :back, status: :found
end
end
end
diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index 3d51520ddf4..1d6d0943674 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -89,7 +89,7 @@ class SnippetsController < ApplicationController
@snippet.destroy
- redirect_to snippets_path, status: 302
+ redirect_to snippets_path, status: :found
end
protected
diff --git a/app/controllers/users/terms_controller.rb b/app/controllers/users/terms_controller.rb
index ab685b9106e..1b1560a2a00 100644
--- a/app/controllers/users/terms_controller.rb
+++ b/app/controllers/users/terms_controller.rb
@@ -2,6 +2,7 @@ module Users
class TermsController < ApplicationController
include InternalRedirect
+ skip_before_action :authenticate_user!
skip_before_action :enforce_terms!
skip_before_action :check_password_expiration
skip_before_action :check_two_factor_requirement
@@ -13,6 +14,10 @@ module Users
def index
@redirect = redirect_path
+
+ if current_user && @term.accepted_by_user?(current_user)
+ flash.now[:notice] = "You have already accepted the Terms of Service as #{current_user.to_reference}"
+ end
end
def accept
diff --git a/app/finders/group_members_finder.rb b/app/finders/group_members_finder.rb
index 067aff408df..2a656c0d31c 100644
--- a/app/finders/group_members_finder.rb
+++ b/app/finders/group_members_finder.rb
@@ -3,17 +3,29 @@ class GroupMembersFinder
@group = group
end
- def execute
+ def execute(include_descendants: false)
group_members = @group.members
+ wheres = []
- return group_members unless @group.parent
+ return group_members unless @group.parent || include_descendants
- parents_members = GroupMember.non_request
- .where(source_id: @group.ancestors.select(:id))
- .where.not(user_id: @group.users.select(:id))
+ wheres << "members.id IN (#{group_members.select(:id).to_sql})"
- wheres = ["members.id IN (#{group_members.select(:id).to_sql})"]
- wheres << "members.id IN (#{parents_members.select(:id).to_sql})"
+ if @group.parent
+ parents_members = GroupMember.non_request
+ .where(source_id: @group.ancestors.select(:id))
+ .where.not(user_id: @group.users.select(:id))
+
+ wheres << "members.id IN (#{parents_members.select(:id).to_sql})"
+ end
+
+ if include_descendants
+ descendant_members = GroupMember.non_request
+ .where(source_id: @group.descendants.select(:id))
+ .where.not(user_id: @group.users.select(:id))
+
+ wheres << "members.id IN (#{descendant_members.select(:id).to_sql})"
+ end
GroupMember.where(wheres.join(' OR '))
end
diff --git a/app/finders/group_projects_finder.rb b/app/finders/group_projects_finder.rb
index f73cf8adb4d..b6bdb2b7b0f 100644
--- a/app/finders/group_projects_finder.rb
+++ b/app/finders/group_projects_finder.rb
@@ -39,25 +39,15 @@ class GroupProjectsFinder < ProjectsFinder
end
def collection_with_user
- if group.users.include?(current_user)
- if only_shared?
- [shared_projects]
- elsif only_owned?
- [owned_projects]
- else
- [shared_projects, owned_projects]
- end
+ if only_shared?
+ [shared_projects.public_or_visible_to_user(current_user)]
+ elsif only_owned?
+ [owned_projects.public_or_visible_to_user(current_user)]
else
- if only_shared?
- [shared_projects.public_or_visible_to_user(current_user)]
- elsif only_owned?
- [owned_projects.public_or_visible_to_user(current_user)]
- else
- [
- owned_projects.public_or_visible_to_user(current_user),
- shared_projects.public_or_visible_to_user(current_user)
- ]
- end
+ [
+ owned_projects.public_or_visible_to_user(current_user),
+ shared_projects.public_or_visible_to_user(current_user)
+ ]
end
end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index c6ef79ce15e..6fdfd964fca 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -7,7 +7,7 @@
# current_user - which user use
# params:
# scope: 'created_by_me' or 'assigned_to_me' or 'all'
-# state: 'opened' or 'closed' or 'all'
+# state: 'opened' or 'closed' or 'locked' or 'all'
# group_id: integer
# project_id: integer
# milestone_title: string
@@ -23,6 +23,7 @@
# created_before: datetime
# updated_after: datetime
# updated_before: datetime
+# use_cte_for_search: boolean
#
class IssuableFinder
prepend FinderWithCrossProjectAccess
@@ -54,6 +55,7 @@ class IssuableFinder
sort
state
include_subgroups
+ use_cte_for_search
]
end
@@ -74,19 +76,21 @@ class IssuableFinder
items = init_collection
items = filter_items(items)
- # Filtering by project HAS TO be the last because we use the project IDs yielded by the issuable query thus far
- items = by_project(items)
+ # This has to be last as we may use a CTE as an optimization fence by
+ # passing the use_cte_for_search param
+ # https://www.postgresql.org/docs/current/static/queries-with.html
+ items = by_search(items)
sort(items)
end
def filter_items(items)
+ items = by_project(items)
items = by_scope(items)
items = by_created_at(items)
items = by_updated_at(items)
items = by_state(items)
items = by_group(items)
- items = by_search(items)
items = by_assignee(items)
items = by_author(items)
items = by_non_archived(items)
@@ -107,7 +111,6 @@ class IssuableFinder
#
def count_by_state
count_params = params.merge(state: nil, sort: nil)
- labels_count = label_names.any? ? label_names.count : 1
finder = self.class.new(current_user, count_params)
counts = Hash.new(0)
@@ -116,6 +119,11 @@ class IssuableFinder
# per issuable, so we have to count those in Ruby - which is bad, but still
# better than performing multiple queries.
#
+ # This does not apply when we are using a CTE for the search, as the labels
+ # GROUP BY is inside the subquery in that case, so we set labels_count to 1.
+ labels_count = label_names.any? ? label_names.count : 1
+ labels_count = 1 if use_cte_for_search?
+
finder.execute.reorder(nil).group(:state).count.each do |key, value|
counts[Array(key).last.to_sym] += value / labels_count
end
@@ -159,10 +167,7 @@ class IssuableFinder
finder_options = { include_subgroups: params[:include_subgroups], only_owned: true }
GroupProjectsFinder.new(group: group, current_user: current_user, options: finder_options).execute
else
- opts = { current_user: current_user }
- opts[:project_ids_relation] = item_project_ids(items) if items
-
- ProjectsFinder.new(opts).execute
+ ProjectsFinder.new(current_user: current_user).execute
end
@projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil)
@@ -306,6 +311,8 @@ class IssuableFinder
items.respond_to?(:merged) ? items.merged : items.closed
when 'opened'
items.opened
+ when 'locked'
+ items.where(state: 'locked')
else
items
end
@@ -329,8 +336,24 @@ class IssuableFinder
items
end
+ def use_cte_for_search?
+ return false unless search
+ return false unless Gitlab::Database.postgresql?
+
+ params[:use_cte_for_search]
+ end
+
def by_search(items)
- search ? items.full_search(search) : items
+ return items unless search
+
+ if use_cte_for_search?
+ cte = Gitlab::SQL::RecursiveCTE.new(klass.table_name)
+ cte << items
+
+ items = klass.with(cte.to_arel).from(klass.table_name)
+ end
+
+ items.full_search(search)
end
def by_iids(items)
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index 1787b4899cd..24a6b9349a0 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -75,6 +75,8 @@ class IssuesFinder < IssuableFinder
items = items.due_between(Date.today.beginning_of_week, Date.today.end_of_week)
elsif filter_by_due_this_month?
items = items.due_between(Date.today.beginning_of_month, Date.today.end_of_month)
+ elsif filter_by_due_next_month_and_previous_two_weeks?
+ items = items.due_between(Date.today - 2.weeks, (Date.today + 1.month).end_of_month)
end
end
@@ -97,6 +99,10 @@ class IssuesFinder < IssuableFinder
due_date? && params[:due_date] == Issue::DueThisMonth.name
end
+ def filter_by_due_next_month_and_previous_two_weeks?
+ due_date? && params[:due_date] == Issue::DueNextMonthAndPreviousTwoWeeks.name
+ end
+
def due_date?
params[:due_date].present?
end
@@ -130,8 +136,4 @@ class IssuesFinder < IssuableFinder
items
end
end
-
- def item_project_ids(items)
- items&.reorder(nil)&.select(:project_id)
- end
end
diff --git a/app/finders/members_finder.rb b/app/finders/members_finder.rb
index 4734d97b8c7..4c893ae2de6 100644
--- a/app/finders/members_finder.rb
+++ b/app/finders/members_finder.rb
@@ -7,12 +7,12 @@ class MembersFinder
@group = project.group
end
- def execute
+ def execute(include_descendants: false)
project_members = project.project_members
project_members = project_members.non_invite unless can?(current_user, :admin_project, project)
if group
- group_members = GroupMembersFinder.new(group).execute
+ group_members = GroupMembersFinder.new(group).execute(include_descendants: include_descendants)
group_members = group_members.non_invite
union = Gitlab::SQL::Union.new([project_members, group_members], remove_duplicates: false)
diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb
index e2240e5e0d8..40089c082c1 100644
--- a/app/finders/merge_requests_finder.rb
+++ b/app/finders/merge_requests_finder.rb
@@ -6,7 +6,7 @@
# current_user - which user use
# params:
# scope: 'created_by_me' or 'assigned_to_me' or 'all'
-# state: 'open', 'closed', 'merged', or 'all'
+# state: 'open', 'closed', 'merged', 'locked', or 'all'
# group_id: integer
# project_id: integer
# milestone_title: string
@@ -56,8 +56,4 @@ class MergeRequestsFinder < IssuableFinder
items.where(target_branch: target_branch)
end
-
- def item_project_ids(items)
- items&.reorder(nil)&.select(:target_project_id)
- end
end
diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb
index 35f4ff2f62f..9b7a35fb3b5 100644
--- a/app/finders/notes_finder.rb
+++ b/app/finders/notes_finder.rb
@@ -83,7 +83,7 @@ class NotesFinder
when "personal_snippet"
PersonalSnippet.all
else
- raise 'invalid target_type'
+ raise "invalid target_type '#{noteable_type}'"
end
end
diff --git a/app/finders/pipelines_finder.rb b/app/finders/pipelines_finder.rb
index 0a487839aff..a99a889a7e9 100644
--- a/app/finders/pipelines_finder.rb
+++ b/app/finders/pipelines_finder.rb
@@ -1,15 +1,20 @@
class PipelinesFinder
- attr_reader :project, :pipelines, :params
+ attr_reader :project, :pipelines, :params, :current_user
ALLOWED_INDEXED_COLUMNS = %w[id status ref user_id].freeze
- def initialize(project, params = {})
+ def initialize(project, current_user, params = {})
@project = project
+ @current_user = current_user
@pipelines = project.pipelines
@params = params
end
def execute
+ unless Ability.allowed?(current_user, :read_pipeline, project)
+ return Ci::Pipeline.none
+ end
+
items = pipelines
items = by_scope(items)
items = by_status(items)
diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb
index d498a2d6d11..9d3772d7541 100644
--- a/app/finders/snippets_finder.rb
+++ b/app/finders/snippets_finder.rb
@@ -54,7 +54,10 @@ class SnippetsFinder < UnionFinder
end
def authorized_snippets
- Snippet.where(feature_available_projects.or(not_project_related))
+ # This query was intentionally converted to a raw one to get it work in Rails 5.0.
+ # In Rails 5.0 and 5.1 there's a bug: https://github.com/rails/arel/issues/531
+ # Please convert it back when on rails 5.2 as it works again as expected since 5.2.
+ Snippet.where("#{feature_available_projects} OR #{not_project_related}")
.public_or_visible_to_user(current_user)
end
@@ -86,18 +89,20 @@ class SnippetsFinder < UnionFinder
def feature_available_projects
# Don't return any project related snippets if the user cannot read cross project
- return table[:id].eq(nil) unless Ability.allowed?(current_user, :read_cross_project)
+ return table[:id].eq(nil).to_sql unless Ability.allowed?(current_user, :read_cross_project)
projects = projects_for_user do |part|
part.with_feature_available_for_user(:snippets, current_user)
end.select(:id)
- arel_query = Arel::Nodes::SqlLiteral.new(projects.to_sql)
- table[:project_id].in(arel_query)
+ # This query was intentionally converted to a raw one to get it work in Rails 5.0.
+ # In Rails 5.0 and 5.1 there's a bug: https://github.com/rails/arel/issues/531
+ # Please convert it back when on rails 5.2 as it works again as expected since 5.2.
+ "snippets.project_id IN (#{projects.to_sql})"
end
def not_project_related
- table[:project_id].eq(nil)
+ table[:project_id].eq(nil).to_sql
end
def table
diff --git a/app/finders/user_recent_events_finder.rb b/app/finders/user_recent_events_finder.rb
index 65d6e019746..74776b2ed1f 100644
--- a/app/finders/user_recent_events_finder.rb
+++ b/app/finders/user_recent_events_finder.rb
@@ -56,7 +56,7 @@ class UserRecentEventsFinder
visible = target_user
.project_interactions
- .where(visibility_level: [Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC])
+ .where(visibility_level: Gitlab::VisibilityLevel.levels_for_user(current_user))
.select(:id)
Gitlab::SQL::Union.new([authorized, visible]).to_sql
diff --git a/app/graphql/functions/base_function.rb b/app/graphql/functions/base_function.rb
new file mode 100644
index 00000000000..42fb8f99acc
--- /dev/null
+++ b/app/graphql/functions/base_function.rb
@@ -0,0 +1,4 @@
+module Functions
+ class BaseFunction < GraphQL::Function
+ end
+end
diff --git a/app/graphql/functions/echo.rb b/app/graphql/functions/echo.rb
new file mode 100644
index 00000000000..e5bf109b8d7
--- /dev/null
+++ b/app/graphql/functions/echo.rb
@@ -0,0 +1,13 @@
+module Functions
+ class Echo < BaseFunction
+ argument :text, GraphQL::STRING_TYPE
+
+ description "Testing endpoint to validate the API with"
+
+ def call(obj, args, ctx)
+ username = ctx[:current_user]&.username
+
+ "#{username.inspect} says: #{args[:text]}"
+ end
+ end
+end
diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb
new file mode 100644
index 00000000000..d9f9129d08a
--- /dev/null
+++ b/app/graphql/gitlab_schema.rb
@@ -0,0 +1,11 @@
+class GitlabSchema < GraphQL::Schema
+ use BatchLoader::GraphQL
+ use Gitlab::Graphql::Authorize
+ use Gitlab::Graphql::Present
+ use Gitlab::Graphql::Connections
+
+ query(Types::QueryType)
+
+ default_max_page_size 100
+ # mutation(Types::MutationType)
+end
diff --git a/app/graphql/mutations/.keep b/app/graphql/mutations/.keep
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/app/graphql/mutations/.keep
diff --git a/app/graphql/resolvers/base_resolver.rb b/app/graphql/resolvers/base_resolver.rb
new file mode 100644
index 00000000000..89b7f9dad6f
--- /dev/null
+++ b/app/graphql/resolvers/base_resolver.rb
@@ -0,0 +1,4 @@
+module Resolvers
+ class BaseResolver < GraphQL::Schema::Resolver
+ end
+end
diff --git a/app/graphql/resolvers/concerns/resolves_pipelines.rb b/app/graphql/resolvers/concerns/resolves_pipelines.rb
new file mode 100644
index 00000000000..9ec45378d8e
--- /dev/null
+++ b/app/graphql/resolvers/concerns/resolves_pipelines.rb
@@ -0,0 +1,23 @@
+module ResolvesPipelines
+ extend ActiveSupport::Concern
+
+ included do
+ type [Types::Ci::PipelineType], null: false
+ argument :status,
+ Types::Ci::PipelineStatusEnum,
+ required: false,
+ description: "Filter pipelines by their status"
+ argument :ref,
+ GraphQL::STRING_TYPE,
+ required: false,
+ description: "Filter pipelines by the ref they are run for"
+ argument :sha,
+ GraphQL::STRING_TYPE,
+ required: false,
+ description: "Filter pipelines by the sha of the commit they are run for"
+ end
+
+ def resolve_pipelines(project, params = {})
+ PipelinesFinder.new(project, context[:current_user], params).execute
+ end
+end
diff --git a/app/graphql/resolvers/full_path_resolver.rb b/app/graphql/resolvers/full_path_resolver.rb
new file mode 100644
index 00000000000..4eb28aaed6c
--- /dev/null
+++ b/app/graphql/resolvers/full_path_resolver.rb
@@ -0,0 +1,19 @@
+module Resolvers
+ module FullPathResolver
+ extend ActiveSupport::Concern
+
+ prepended do
+ argument :full_path, GraphQL::ID_TYPE,
+ required: true,
+ description: 'The full path of the project or namespace, e.g., "gitlab-org/gitlab-ce"'
+ end
+
+ def model_by_full_path(model, full_path)
+ BatchLoader.for(full_path).batch(key: "#{model.model_name.param_key}:full_path") do |full_paths, loader|
+ # `with_route` avoids an N+1 calculating full_path
+ results = model.where_full_path_in(full_paths).with_route
+ results.each { |project| loader.call(project.full_path, project) }
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/merge_request_pipelines_resolver.rb b/app/graphql/resolvers/merge_request_pipelines_resolver.rb
new file mode 100644
index 00000000000..00b51ee1381
--- /dev/null
+++ b/app/graphql/resolvers/merge_request_pipelines_resolver.rb
@@ -0,0 +1,16 @@
+module Resolvers
+ class MergeRequestPipelinesResolver < BaseResolver
+ include ::ResolvesPipelines
+
+ alias_method :merge_request, :object
+
+ def resolve(**args)
+ resolve_pipelines(project, args)
+ .merge(merge_request.all_pipelines)
+ end
+
+ def project
+ merge_request.source_project
+ end
+ end
+end
diff --git a/app/graphql/resolvers/merge_request_resolver.rb b/app/graphql/resolvers/merge_request_resolver.rb
new file mode 100644
index 00000000000..9f2d348e95f
--- /dev/null
+++ b/app/graphql/resolvers/merge_request_resolver.rb
@@ -0,0 +1,20 @@
+module Resolvers
+ class MergeRequestResolver < BaseResolver
+ argument :iid, GraphQL::ID_TYPE,
+ required: true,
+ description: 'The IID of the merge request, e.g., "1"'
+
+ type Types::MergeRequestType, null: true
+
+ alias_method :project, :object
+
+ def resolve(iid:)
+ return unless project.present?
+
+ BatchLoader.for(iid.to_s).batch(key: project.id) do |iids, loader|
+ results = project.merge_requests.where(iid: iids)
+ results.each { |mr| loader.call(mr.iid.to_s, mr) }
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/project_pipelines_resolver.rb b/app/graphql/resolvers/project_pipelines_resolver.rb
new file mode 100644
index 00000000000..7f175a3b26c
--- /dev/null
+++ b/app/graphql/resolvers/project_pipelines_resolver.rb
@@ -0,0 +1,11 @@
+module Resolvers
+ class ProjectPipelinesResolver < BaseResolver
+ include ResolvesPipelines
+
+ alias_method :project, :object
+
+ def resolve(**args)
+ resolve_pipelines(project, args)
+ end
+ end
+end
diff --git a/app/graphql/resolvers/project_resolver.rb b/app/graphql/resolvers/project_resolver.rb
new file mode 100644
index 00000000000..ec115bad896
--- /dev/null
+++ b/app/graphql/resolvers/project_resolver.rb
@@ -0,0 +1,11 @@
+module Resolvers
+ class ProjectResolver < BaseResolver
+ prepend FullPathResolver
+
+ type Types::ProjectType, null: true
+
+ def resolve(full_path:)
+ model_by_full_path(Project, full_path)
+ end
+ end
+end
diff --git a/app/graphql/types/base_enum.rb b/app/graphql/types/base_enum.rb
new file mode 100644
index 00000000000..b45a845f74f
--- /dev/null
+++ b/app/graphql/types/base_enum.rb
@@ -0,0 +1,4 @@
+module Types
+ class BaseEnum < GraphQL::Schema::Enum
+ end
+end
diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb
new file mode 100644
index 00000000000..c5740a334d7
--- /dev/null
+++ b/app/graphql/types/base_field.rb
@@ -0,0 +1,5 @@
+module Types
+ class BaseField < GraphQL::Schema::Field
+ prepend Gitlab::Graphql::Authorize
+ end
+end
diff --git a/app/graphql/types/base_input_object.rb b/app/graphql/types/base_input_object.rb
new file mode 100644
index 00000000000..309e336e6c8
--- /dev/null
+++ b/app/graphql/types/base_input_object.rb
@@ -0,0 +1,4 @@
+module Types
+ class BaseInputObject < GraphQL::Schema::InputObject
+ end
+end
diff --git a/app/graphql/types/base_interface.rb b/app/graphql/types/base_interface.rb
new file mode 100644
index 00000000000..69e72dc5808
--- /dev/null
+++ b/app/graphql/types/base_interface.rb
@@ -0,0 +1,5 @@
+module Types
+ module BaseInterface
+ include GraphQL::Schema::Interface
+ end
+end
diff --git a/app/graphql/types/base_object.rb b/app/graphql/types/base_object.rb
new file mode 100644
index 00000000000..754adf4c04d
--- /dev/null
+++ b/app/graphql/types/base_object.rb
@@ -0,0 +1,8 @@
+module Types
+ class BaseObject < GraphQL::Schema::Object
+ prepend Gitlab::Graphql::Present
+ prepend Gitlab::Graphql::ExposePermissions
+
+ field_class Types::BaseField
+ end
+end
diff --git a/app/graphql/types/base_scalar.rb b/app/graphql/types/base_scalar.rb
new file mode 100644
index 00000000000..c0aa38be239
--- /dev/null
+++ b/app/graphql/types/base_scalar.rb
@@ -0,0 +1,4 @@
+module Types
+ class BaseScalar < GraphQL::Schema::Scalar
+ end
+end
diff --git a/app/graphql/types/base_union.rb b/app/graphql/types/base_union.rb
new file mode 100644
index 00000000000..36337fc6ee5
--- /dev/null
+++ b/app/graphql/types/base_union.rb
@@ -0,0 +1,4 @@
+module Types
+ class BaseUnion < GraphQL::Schema::Union
+ end
+end
diff --git a/app/graphql/types/ci/pipeline_status_enum.rb b/app/graphql/types/ci/pipeline_status_enum.rb
new file mode 100644
index 00000000000..2c12e5001d8
--- /dev/null
+++ b/app/graphql/types/ci/pipeline_status_enum.rb
@@ -0,0 +1,9 @@
+module Types
+ module Ci
+ class PipelineStatusEnum < BaseEnum
+ ::Ci::Pipeline.all_state_names.each do |state_symbol|
+ value state_symbol.to_s.upcase, value: state_symbol.to_s
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/ci/pipeline_type.rb b/app/graphql/types/ci/pipeline_type.rb
new file mode 100644
index 00000000000..bbb7d9354d0
--- /dev/null
+++ b/app/graphql/types/ci/pipeline_type.rb
@@ -0,0 +1,31 @@
+module Types
+ module Ci
+ class PipelineType < BaseObject
+ expose_permissions Types::PermissionTypes::Ci::Pipeline
+
+ graphql_name 'Pipeline'
+
+ field :id, GraphQL::ID_TYPE, null: false
+ field :iid, GraphQL::ID_TYPE, null: false
+
+ field :sha, GraphQL::STRING_TYPE, null: false
+ field :before_sha, GraphQL::STRING_TYPE, null: true
+ field :status, PipelineStatusEnum, null: false
+ field :duration,
+ GraphQL::INT_TYPE,
+ null: true,
+ description: "Duration of the pipeline in seconds"
+ field :coverage,
+ GraphQL::FLOAT_TYPE,
+ null: true,
+ description: "Coverage percentage"
+ field :created_at, Types::TimeType, null: false
+ field :updated_at, Types::TimeType, null: false
+ field :started_at, Types::TimeType, null: true
+ field :finished_at, Types::TimeType, null: true
+ field :committed_at, Types::TimeType, null: true
+
+ # TODO: Add triggering user as a type
+ end
+ end
+end
diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb
new file mode 100644
index 00000000000..88cd2adc6dc
--- /dev/null
+++ b/app/graphql/types/merge_request_type.rb
@@ -0,0 +1,55 @@
+module Types
+ class MergeRequestType < BaseObject
+ expose_permissions Types::PermissionTypes::MergeRequest
+
+ present_using MergeRequestPresenter
+
+ graphql_name 'MergeRequest'
+
+ field :id, GraphQL::ID_TYPE, null: false
+ field :iid, GraphQL::ID_TYPE, null: false
+ field :title, GraphQL::STRING_TYPE, null: false
+ field :description, GraphQL::STRING_TYPE, null: true
+ field :state, GraphQL::STRING_TYPE, null: true
+ field :created_at, Types::TimeType, null: false
+ field :updated_at, Types::TimeType, null: false
+ field :source_project, Types::ProjectType, null: true
+ field :target_project, Types::ProjectType, null: false
+ # Alias for target_project
+ field :project, Types::ProjectType, null: false
+ field :project_id, GraphQL::INT_TYPE, null: false, method: :target_project_id
+ field :source_project_id, GraphQL::INT_TYPE, null: true
+ field :target_project_id, GraphQL::INT_TYPE, null: false
+ field :source_branch, GraphQL::STRING_TYPE, null: false
+ field :target_branch, GraphQL::STRING_TYPE, null: false
+ field :work_in_progress, GraphQL::BOOLEAN_TYPE, method: :work_in_progress?, null: false
+ field :merge_when_pipeline_succeeds, GraphQL::BOOLEAN_TYPE, null: true
+ field :diff_head_sha, GraphQL::STRING_TYPE, null: true
+ field :merge_commit_sha, GraphQL::STRING_TYPE, null: true
+ field :user_notes_count, GraphQL::INT_TYPE, null: true
+ field :should_remove_source_branch, GraphQL::BOOLEAN_TYPE, method: :should_remove_source_branch?, null: true
+ field :force_remove_source_branch, GraphQL::BOOLEAN_TYPE, method: :force_remove_source_branch?, null: true
+ field :merge_status, GraphQL::STRING_TYPE, null: true
+ field :in_progress_merge_commit_sha, GraphQL::STRING_TYPE, null: true
+ field :merge_error, GraphQL::STRING_TYPE, null: true
+ field :allow_collaboration, GraphQL::BOOLEAN_TYPE, null: true
+ field :should_be_rebased, GraphQL::BOOLEAN_TYPE, method: :should_be_rebased?, null: false
+ field :rebase_commit_sha, GraphQL::STRING_TYPE, null: true
+ field :rebase_in_progress, GraphQL::BOOLEAN_TYPE, method: :rebase_in_progress?, null: false
+ field :diff_head_sha, GraphQL::STRING_TYPE, null: true
+ field :merge_commit_message, GraphQL::STRING_TYPE, null: true
+ field :merge_ongoing, GraphQL::BOOLEAN_TYPE, method: :merge_ongoing?, null: false
+ field :source_branch_exists, GraphQL::BOOLEAN_TYPE, method: :source_branch_exists?, null: false
+ field :mergeable_discussions_state, GraphQL::BOOLEAN_TYPE, null: true
+ field :web_url, GraphQL::STRING_TYPE, null: true
+ field :upvotes, GraphQL::INT_TYPE, null: false
+ field :downvotes, GraphQL::INT_TYPE, null: false
+ field :subscribed, GraphQL::BOOLEAN_TYPE, method: :subscribed?, null: false
+
+ field :head_pipeline, Types::Ci::PipelineType, null: true, method: :actual_head_pipeline do
+ authorize :read_pipeline
+ end
+ field :pipelines, Types::Ci::PipelineType.connection_type,
+ resolver: Resolvers::MergeRequestPipelinesResolver
+ end
+end
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
new file mode 100644
index 00000000000..06ed91c1658
--- /dev/null
+++ b/app/graphql/types/mutation_type.rb
@@ -0,0 +1,7 @@
+module Types
+ class MutationType < BaseObject
+ graphql_name "Mutation"
+
+ # TODO: Add Mutations as fields
+ end
+end
diff --git a/app/graphql/types/permission_types/base_permission_type.rb b/app/graphql/types/permission_types/base_permission_type.rb
new file mode 100644
index 00000000000..934ed572e56
--- /dev/null
+++ b/app/graphql/types/permission_types/base_permission_type.rb
@@ -0,0 +1,38 @@
+module Types
+ module PermissionTypes
+ class BasePermissionType < BaseObject
+ extend Gitlab::Allowable
+
+ RESOLVING_KEYWORDS = [:resolver, :method, :hash_key, :function].to_set.freeze
+
+ def self.abilities(*abilities)
+ abilities.each { |ability| ability_field(ability) }
+ end
+
+ def self.ability_field(ability, **kword_args)
+ unless resolving_keywords?(kword_args)
+ kword_args[:resolve] ||= -> (object, args, context) do
+ can?(context[:current_user], ability, object, args.to_h)
+ end
+ end
+
+ permission_field(ability, **kword_args)
+ end
+
+ def self.permission_field(name, **kword_args)
+ kword_args = kword_args.reverse_merge(
+ name: name,
+ type: GraphQL::BOOLEAN_TYPE,
+ description: "Whether or not a user can perform `#{name}` on this resource",
+ null: false)
+
+ field(**kword_args)
+ end
+
+ def self.resolving_keywords?(arguments)
+ RESOLVING_KEYWORDS.intersect?(arguments.keys.to_set)
+ end
+ private_class_method :resolving_keywords?
+ end
+ end
+end
diff --git a/app/graphql/types/permission_types/ci/pipeline.rb b/app/graphql/types/permission_types/ci/pipeline.rb
new file mode 100644
index 00000000000..942539c7cf7
--- /dev/null
+++ b/app/graphql/types/permission_types/ci/pipeline.rb
@@ -0,0 +1,11 @@
+module Types
+ module PermissionTypes
+ module Ci
+ class Pipeline < BasePermissionType
+ graphql_name 'PipelinePermissions'
+
+ abilities :update_pipeline, :admin_pipeline, :destroy_pipeline
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/permission_types/merge_request.rb b/app/graphql/types/permission_types/merge_request.rb
new file mode 100644
index 00000000000..5c21f6ee9c6
--- /dev/null
+++ b/app/graphql/types/permission_types/merge_request.rb
@@ -0,0 +1,17 @@
+module Types
+ module PermissionTypes
+ class MergeRequest < BasePermissionType
+ present_using MergeRequestPresenter
+ description 'Check permissions for the current user on a merge request'
+ graphql_name 'MergeRequestPermissions'
+
+ abilities :read_merge_request, :admin_merge_request,
+ :update_merge_request, :create_note
+
+ permission_field :push_to_source_branch, method: :can_push_to_source_branch?
+ permission_field :remove_source_branch, method: :can_remove_source_branch?
+ permission_field :cherry_pick_on_current_merge_request, method: :can_cherry_pick_on_current_merge_request?
+ permission_field :revert_on_current_merge_request, method: :can_revert_on_current_merge_request?
+ end
+ end
+end
diff --git a/app/graphql/types/permission_types/project.rb b/app/graphql/types/permission_types/project.rb
new file mode 100644
index 00000000000..755699a4415
--- /dev/null
+++ b/app/graphql/types/permission_types/project.rb
@@ -0,0 +1,20 @@
+module Types
+ module PermissionTypes
+ class Project < BasePermissionType
+ graphql_name 'ProjectPermissions'
+
+ abilities :change_namespace, :change_visibility_level, :rename_project,
+ :remove_project, :archive_project, :remove_fork_project,
+ :remove_pages, :read_project, :create_merge_request_in,
+ :read_wiki, :read_project_member, :create_issue, :upload_file,
+ :read_cycle_analytics, :download_code, :download_wiki_code,
+ :fork_project, :create_project_snippet, :read_commit_status,
+ :request_access, :create_pipeline, :create_pipeline_schedule,
+ :create_merge_request_from, :create_wiki, :push_code,
+ :create_deployment, :push_to_delete_protected_branch,
+ :admin_wiki, :admin_project, :update_pages,
+ :admin_remote_mirror, :create_label, :update_wiki, :destroy_wiki,
+ :create_pages, :destroy_pages
+ end
+ end
+end
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
new file mode 100644
index 00000000000..97707215b4e
--- /dev/null
+++ b/app/graphql/types/project_type.rb
@@ -0,0 +1,79 @@
+module Types
+ class ProjectType < BaseObject
+ expose_permissions Types::PermissionTypes::Project
+
+ graphql_name 'Project'
+
+ field :id, GraphQL::ID_TYPE, null: false
+
+ field :full_path, GraphQL::ID_TYPE, null: false
+ field :path, GraphQL::STRING_TYPE, null: false
+
+ field :name_with_namespace, GraphQL::STRING_TYPE, null: false
+ field :name, GraphQL::STRING_TYPE, null: false
+
+ field :description, GraphQL::STRING_TYPE, null: true
+
+ field :default_branch, GraphQL::STRING_TYPE, null: true
+ field :tag_list, GraphQL::STRING_TYPE, null: true
+
+ field :ssh_url_to_repo, GraphQL::STRING_TYPE, null: true
+ field :http_url_to_repo, GraphQL::STRING_TYPE, null: true
+ field :web_url, GraphQL::STRING_TYPE, null: true
+
+ field :star_count, GraphQL::INT_TYPE, null: false
+ field :forks_count, GraphQL::INT_TYPE, null: false
+
+ field :created_at, Types::TimeType, null: true
+ field :last_activity_at, Types::TimeType, null: true
+
+ field :archived, GraphQL::BOOLEAN_TYPE, null: true
+
+ field :visibility, GraphQL::STRING_TYPE, null: true
+
+ field :container_registry_enabled, GraphQL::BOOLEAN_TYPE, null: true
+ field :shared_runners_enabled, GraphQL::BOOLEAN_TYPE, null: true
+ field :lfs_enabled, GraphQL::BOOLEAN_TYPE, null: true
+ field :merge_requests_ff_only_enabled, GraphQL::BOOLEAN_TYPE, null: true
+
+ field :avatar_url, GraphQL::STRING_TYPE, null: true, resolve: -> (project, args, ctx) do
+ project.avatar_url(only_path: false)
+ end
+
+ %i[issues merge_requests wiki snippets].each do |feature|
+ field "#{feature}_enabled", GraphQL::BOOLEAN_TYPE, null: true, resolve: -> (project, args, ctx) do
+ project.feature_available?(feature, ctx[:current_user])
+ end
+ end
+
+ field :jobs_enabled, GraphQL::BOOLEAN_TYPE, null: true, resolve: -> (project, args, ctx) do
+ project.feature_available?(:builds, ctx[:current_user])
+ end
+
+ field :public_jobs, GraphQL::BOOLEAN_TYPE, method: :public_builds, null: true
+
+ field :open_issues_count, GraphQL::INT_TYPE, null: true, resolve: -> (project, args, ctx) do
+ project.open_issues_count if project.feature_available?(:issues, ctx[:current_user])
+ end
+
+ field :import_status, GraphQL::STRING_TYPE, null: true
+ field :ci_config_path, GraphQL::STRING_TYPE, null: true
+
+ field :only_allow_merge_if_pipeline_succeeds, GraphQL::BOOLEAN_TYPE, null: true
+ field :request_access_enabled, GraphQL::BOOLEAN_TYPE, null: true
+ field :only_allow_merge_if_all_discussions_are_resolved, GraphQL::BOOLEAN_TYPE, null: true
+ field :printing_merge_request_link_enabled, GraphQL::BOOLEAN_TYPE, null: true
+
+ field :merge_request,
+ Types::MergeRequestType,
+ null: true,
+ resolver: Resolvers::MergeRequestResolver do
+ authorize :read_merge_request
+ end
+
+ field :pipelines,
+ Types::Ci::PipelineType.connection_type,
+ null: false,
+ resolver: Resolvers::ProjectPipelinesResolver
+ end
+end
diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb
new file mode 100644
index 00000000000..010ec2d7942
--- /dev/null
+++ b/app/graphql/types/query_type.rb
@@ -0,0 +1,14 @@
+module Types
+ class QueryType < BaseObject
+ graphql_name 'Query'
+
+ field :project, Types::ProjectType,
+ null: true,
+ resolver: Resolvers::ProjectResolver,
+ description: "Find a project" do
+ authorize :read_project
+ end
+
+ field :echo, GraphQL::STRING_TYPE, null: false, function: Functions::Echo.new
+ end
+end
diff --git a/app/graphql/types/time_type.rb b/app/graphql/types/time_type.rb
new file mode 100644
index 00000000000..2333d82ad1e
--- /dev/null
+++ b/app/graphql/types/time_type.rb
@@ -0,0 +1,14 @@
+module Types
+ class TimeType < BaseScalar
+ graphql_name 'Time'
+ description 'Time represented in ISO 8601'
+
+ def self.coerce_input(value, ctx)
+ Time.parse(value)
+ end
+
+ def self.coerce_result(value, ctx)
+ value.iso8601
+ end
+ end
+end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index f5d94ad96a1..0190aa90763 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -270,7 +270,7 @@ module ApplicationHelper
{
members: members_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
issues: issues_project_autocomplete_sources_path(object),
- merge_requests: merge_requests_project_autocomplete_sources_path(object),
+ mergeRequests: merge_requests_project_autocomplete_sources_path(object),
labels: labels_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
milestones: milestones_project_autocomplete_sources_path(object),
commands: commands_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id])
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index b948e431882..358b896702b 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -36,7 +36,7 @@ module ApplicationSettingsHelper
# Return a group of checkboxes that use Bootstrap's button plugin for a
# toggle button effect.
- def restricted_level_checkboxes(help_block_id, checkbox_name)
+ def restricted_level_checkboxes(help_block_id, checkbox_name, options = {})
Gitlab::VisibilityLevel.values.map do |level|
checked = restricted_visibility_levels(true).include?(level)
css_class = checked ? 'active' : ''
@@ -46,6 +46,7 @@ module ApplicationSettingsHelper
check_box_tag(checkbox_name, level, checked,
autocomplete: 'off',
'aria-describedby' => help_block_id,
+ 'class' => options[:class],
id: tag_name) + visibility_level_icon(level) + visibility_level_label(level)
end
end
@@ -53,7 +54,7 @@ module ApplicationSettingsHelper
# Return a group of checkboxes that use Bootstrap's button plugin for a
# toggle button effect.
- def import_sources_checkboxes(help_block_id)
+ def import_sources_checkboxes(help_block_id, options = {})
Gitlab::ImportSources.options.map do |name, source|
checked = Gitlab::CurrentSettings.import_sources.include?(source)
css_class = checked ? 'active' : ''
@@ -63,6 +64,7 @@ module ApplicationSettingsHelper
check_box_tag(checkbox_name, source, checked,
autocomplete: 'off',
'aria-describedby' => help_block_id,
+ 'class' => options[:class],
id: name.tr(' ', '_')) + name
end
end
@@ -204,7 +206,7 @@ module ApplicationSettingsHelper
:pages_domain_verification_enabled,
:password_authentication_enabled_for_web,
:password_authentication_enabled_for_git,
- :performance_bar_allowed_group_id,
+ :performance_bar_allowed_group_path,
:performance_bar_enabled,
:plantuml_enabled,
:plantuml_url,
@@ -249,6 +251,7 @@ module ApplicationSettingsHelper
:user_oauth_applications,
:version_check_enabled,
:allow_local_requests_from_hooks_and_services,
+ :hide_third_party_offers,
:enforce_terms,
:terms,
:mirror_available
diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb
index d339c01d492..43d92bde064 100644
--- a/app/helpers/avatars_helper.rb
+++ b/app/helpers/avatars_helper.rb
@@ -78,7 +78,7 @@ module AvatarsHelper
user: commit_or_event.author,
user_name: commit_or_event.author_name,
user_email: commit_or_event.author_email,
- css_class: 'hidden-xs'
+ css_class: 'd-none d-sm-inline'
}))
end
diff --git a/app/helpers/calendar_helper.rb b/app/helpers/calendar_helper.rb
new file mode 100644
index 00000000000..c54b91b0ce5
--- /dev/null
+++ b/app/helpers/calendar_helper.rb
@@ -0,0 +1,8 @@
+module CalendarHelper
+ def calendar_url_options
+ { format: :ics,
+ feed_token: current_user.try(:feed_token),
+ due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name,
+ sort: 'closest_future_date' }
+ end
+end
diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb
index f0afcac5986..f49b5c7b51a 100644
--- a/app/helpers/ci_status_helper.rb
+++ b/app/helpers/ci_status_helper.rb
@@ -92,7 +92,7 @@ module CiStatusHelper
"pipeline-status/#{pipeline_status.sha}-#{pipeline_status.status}"
end
- def render_project_pipeline_status(pipeline_status, tooltip_placement: 'auto left')
+ def render_project_pipeline_status(pipeline_status, tooltip_placement: 'left')
project = pipeline_status.project
path = pipelines_project_commit_path(project, pipeline_status.sha, ref: pipeline_status.ref)
@@ -103,7 +103,7 @@ module CiStatusHelper
tooltip_placement: tooltip_placement)
end
- def render_commit_status(commit, ref: nil, tooltip_placement: 'auto left')
+ def render_commit_status(commit, ref: nil, tooltip_placement: 'left')
project = commit.project
path = pipelines_project_commit_path(project, commit, ref: ref)
@@ -114,7 +114,7 @@ module CiStatusHelper
tooltip_placement: tooltip_placement)
end
- def render_pipeline_status(pipeline, tooltip_placement: 'auto left')
+ def render_pipeline_status(pipeline, tooltip_placement: 'left')
project = pipeline.project
path = project_pipeline_path(project, pipeline)
render_status_with_link('pipeline', pipeline.status, path, tooltip_placement: tooltip_placement)
@@ -122,10 +122,10 @@ module CiStatusHelper
def no_runners_for_project?(project)
project.runners.blank? &&
- Ci::Runner.shared.blank?
+ Ci::Runner.instance_type.blank?
end
- def render_status_with_link(type, status, path = nil, tooltip_placement: 'auto left', cssclass: '', container: 'body')
+ def render_status_with_link(type, status, path = nil, tooltip_placement: 'left', cssclass: '', container: 'body')
klass = "ci-status-link ci-status-icon-#{status.dasherize} #{cssclass}"
title = "#{type.titleize}: #{ci_label_for_status(status)}"
data = { toggle: 'tooltip', placement: tooltip_placement, container: container }
diff --git a/app/helpers/clusters_helper.rb b/app/helpers/clusters_helper.rb
index c24d340d184..8fd0b6f14c6 100644
--- a/app/helpers/clusters_helper.rb
+++ b/app/helpers/clusters_helper.rb
@@ -4,6 +4,7 @@ module ClustersHelper
end
def render_gcp_signup_offer
+ return if Gitlab::CurrentSettings.current_application_settings.hide_third_party_offers?
return unless show_gcp_signup_offer?
content_tag :section, class: 'no-animate expanded' do
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index e594a1d0ba3..e5c3be47801 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -27,7 +27,7 @@ module CommitsHelper
return unless @project && @ref
# Add the root project link and the arrow icon
- crumbs = content_tag(:li) do
+ crumbs = content_tag(:li, class: 'breadcrumb-item') do
link_to(
@project.path,
project_commits_path(@project, @ref)
@@ -38,7 +38,7 @@ module CommitsHelper
parts = @path.split('/')
parts.each_with_index do |part, i|
- crumbs << content_tag(:li) do
+ crumbs << content_tag(:li, class: 'breadcrumb-item') do
# The text is just the individual part, but the link needs all the parts before it
link_to(
part,
@@ -62,8 +62,8 @@ module CommitsHelper
# 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
- sprite_icon('fork', size: 12, css_class: 'fork-svg') + "#{text}"
+ link_to(url, class: 'badge badge-gray ref-name branch-link') do
+ sprite_icon('branch', size: 12, css_class: 'fork-svg') + "#{text}"
end
end
@@ -76,8 +76,8 @@ module CommitsHelper
# 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', class: 'append-right-5') + "#{text}"
+ link_to(url, class: 'badge badge-gray ref-name') do
+ sprite_icon('tag', size: 12, css_class: 'append-right-5 vertical-align-middle') + "#{text}"
end
end
diff --git a/app/helpers/count_helper.rb b/app/helpers/count_helper.rb
index 24ee62e68ba..5cd98f40f78 100644
--- a/app/helpers/count_helper.rb
+++ b/app/helpers/count_helper.rb
@@ -1,5 +1,9 @@
module CountHelper
- def approximate_count_with_delimiters(model)
- number_with_delimiter(Gitlab::Database::Count.approximate_count(model))
+ def approximate_count_with_delimiters(count_data, model)
+ count = count_data[model]
+
+ raise "Missing model #{model} from count data" unless count
+
+ number_with_delimiter(count)
end
end
diff --git a/app/helpers/favicon_helper.rb b/app/helpers/favicon_helper.rb
new file mode 100644
index 00000000000..3a5342a8d9d
--- /dev/null
+++ b/app/helpers/favicon_helper.rb
@@ -0,0 +1,7 @@
+module FaviconHelper
+ def favicon_extension_whitelist
+ FaviconUploader::EXTENSION_WHITELIST
+ .map { |extension| "'.#{extension}'"}
+ .to_sentence
+ end
+end
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 95fea2f18d1..3c5c8bbd71b 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -128,8 +128,10 @@ module GroupsHelper
def get_group_sidebar_links
links = [:overview, :group_members]
- if can?(current_user, :read_cross_project)
- links += [:activity, :issues, :boards, :labels, :milestones, :merge_requests]
+ resources = [:activity, :issues, :boards, :labels, :milestones,
+ :merge_requests]
+ links += resources.select do |resource|
+ can?(current_user, "read_group_#{resource}".to_sym, @group)
end
if can?(current_user, :admin_group, @group)
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index f39a62bccc8..8766bb43cac 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -157,15 +157,15 @@ module IssuablesHelper
output = ""
output << "Opened #{time_ago_with_tooltip(issuable.created_at)} by ".html_safe
output << content_tag(:strong) do
- author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "hidden-xs", tooltip: true)
- author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "hidden-sm hidden-md hidden-lg")
+ author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "d-none d-sm-inline", tooltip: true)
+ author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "d-block d-sm-none")
end
output << "&ensp;".html_safe
output << content_tag(:span, (issuable_first_contribution_icon if issuable.first_contribution?), class: 'has-tooltip', title: _('1st contribution!'))
- output << content_tag(:span, (issuable.task_status if issuable.tasks?), id: "task_status", class: "hidden-xs hidden-sm")
- output << content_tag(:span, (issuable.task_status_short if issuable.tasks?), id: "task_status_short", class: "hidden-md hidden-lg")
+ output << content_tag(:span, (issuable.task_status if issuable.tasks?), id: "task_status", class: "d-none d-sm-none d-md-inline-block")
+ output << content_tag(:span, (issuable.task_status_short if issuable.tasks?), id: "task_status_short", class: "d-md-none")
output.html_safe
end
@@ -199,7 +199,7 @@ module IssuablesHelper
if display_count
count = issuables_count_for_state(issuable_type, state)
- html << " " << content_tag(:span, number_with_delimiter(count), class: 'badge')
+ html << " " << content_tag(:span, number_with_delimiter(count), class: 'badge badge-pill')
end
html.html_safe
@@ -236,6 +236,7 @@ module IssuablesHelper
issuableRef: issuable.to_reference,
markdownPreviewPath: preview_markdown_path(parent),
markdownDocsPath: help_page_path('user/markdown'),
+ markdownVersion: issuable.cached_markdown_version,
issuableTemplates: issuable_templates(issuable),
initialTitleHtml: markdown_field(issuable, :title),
initialTitleText: issuable.title,
@@ -359,7 +360,8 @@ module IssuablesHelper
url: project_todos_path(@project),
delete_path: (dashboard_todo_path(todo) if todo),
placement: (is_collapsed ? 'left' : nil),
- container: (is_collapsed ? 'body' : nil)
+ container: (is_collapsed ? 'body' : nil),
+ boundary: 'viewport'
}
end
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index c4a6a1e4bb3..c7df25cecef 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -81,7 +81,7 @@ module LabelsHelper
# Intentionally not using content_tag here so that this method can be called
# by LabelReferenceFilter
- span = %(<span class="label color-label #{"has-tooltip" if tooltip}" ) +
+ span = %(<span class="badge color-label #{"has-tooltip" if tooltip}" ) +
%(style="background-color: #{label.color}; color: #{text_color}" ) +
%(title="#{escape_once(label.description)}" data-container="body">) +
%(#{escape_once(label.name)}#{label_suffix}</span>)
@@ -211,6 +211,14 @@ module LabelsHelper
end
end
+ def label_status_tooltip(label, status)
+ type = label.is_a?(ProjectLabel) ? 'project' : 'group'
+ level = status.unsubscribed? ? type : status.sub('-level', '')
+ action = status.unsubscribed? ? 'Subscribe' : 'Unsubscribe'
+
+ "#{action} at #{level} level"
+ end
+
# Required for Banzai::Filter::LabelReferenceFilter
module_function :render_colored_label, :text_color_for_bg, :escape_once
end
diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb
index 39e7a7fd396..cbb971cf8b7 100644
--- a/app/helpers/markup_helper.rb
+++ b/app/helpers/markup_helper.rb
@@ -107,6 +107,7 @@ module MarkupHelper
def markup(file_name, text, context = {})
context[:project] ||= @project
+ context[:markdown_engine] ||= :redcarpet
html = context.delete(:rendered) || markup_unsafe(file_name, text, context)
prepare_for_rendering(html, context)
end
@@ -120,7 +121,8 @@ module MarkupHelper
project: @project,
project_wiki: @project_wiki,
page_slug: wiki_page.slug,
- issuable_state_filter_enabled: true
+ issuable_state_filter_enabled: true,
+ markdown_engine: :redcarpet
}
html =
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index c19c5b9cc82..097be8a0643 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -86,6 +86,8 @@ module MergeRequestsHelper
end
def version_index(merge_request_diff)
+ return nil if @merge_request_diffs.empty?
+
@merge_request_diffs.size - @merge_request_diffs.index(merge_request_diff)
end
@@ -97,15 +99,16 @@ module MergeRequestsHelper
{
merge_when_pipeline_succeeds: true,
should_remove_source_branch: true,
- sha: merge_request.diff_head_sha
- }.merge(merge_params_ee(merge_request))
+ sha: merge_request.diff_head_sha,
+ squash: merge_request.squash
+ }
end
def tab_link_for(merge_request, tab, options = {}, &block)
data_attrs = {
action: tab.to_s,
target: "##{tab}",
- toggle: options.fetch(:force_link, false) ? '' : 'tab'
+ toggle: options.fetch(:force_link, false) ? '' : 'tabvue'
}
url = case tab
@@ -125,8 +128,8 @@ module MergeRequestsHelper
link_to(url[merge_request.project, merge_request], data: data_attrs, &block)
end
- def allow_maintainer_push_unavailable_reason(merge_request)
- return if merge_request.can_allow_maintainer_to_push?(current_user)
+ def allow_collaboration_unavailable_reason(merge_request)
+ return if merge_request.can_allow_collaboration?(current_user)
minimum_visibility = [merge_request.target_project.visibility_level,
merge_request.source_project.visibility_level].min
@@ -149,8 +152,4 @@ module MergeRequestsHelper
current_user.fork_of(project)
end
end
-
- def merge_params_ee(merge_request)
- {}
- end
end
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index e8caab3e50c..15a15405f1d 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -89,7 +89,7 @@ module MilestonesHelper
def milestone_progress_bar(milestone)
options = {
- class: 'progress-bar progress-bar-success',
+ class: 'progress-bar bg-success',
style: "width: #{milestone.percent_complete(current_user)}%;"
}
@@ -112,8 +112,6 @@ module MilestonesHelper
def milestone_tooltip_title(milestone)
if milestone
"#{milestone.title}<br />#{milestone_tooltip_due_date(milestone)}"
- else
- _('Milestone')
end
end
@@ -173,6 +171,8 @@ module MilestonesHelper
def milestone_tooltip_due_date(milestone)
if milestone.due_date
"#{milestone.due_date.to_s(:medium)} (#{remaining_days_in_words(milestone)})"
+ else
+ _('Milestone')
end
end
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index 7754c34d6f0..a84a39235d8 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -11,6 +11,7 @@ module NavHelper
class_name = page_gutter_class
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 -= ['right-sidebar-expanded'] if defined?(@right_sidebar) && !@right_sidebar
class_name
end
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index 7f67574a428..5404ead44f3 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -143,7 +143,15 @@ module NotesHelper
notesIds: @notes.map(&:id),
now: Time.now.to_i,
diffView: diff_view,
- autocomplete: autocomplete
+ enableGFM: {
+ emojis: true,
+ members: autocomplete,
+ issues: autocomplete,
+ mergeRequests: autocomplete,
+ epics: autocomplete,
+ milestones: autocomplete,
+ labels: autocomplete
+ }
}
end
@@ -161,6 +169,7 @@ module NotesHelper
registerPath: new_session_path(:user, redirect_to_referer: 'yes', anchor: 'register-pane'),
newSessionPath: new_session_path(:user, redirect_to_referer: 'yes'),
markdownDocsPath: help_page_path('user/markdown'),
+ markdownVersion: issuable.cached_markdown_version,
quickActionsDocsPath: help_page_path('user/project/quick_actions'),
closePath: close_issuable_path(issuable),
reopenPath: reopen_issuable_path(issuable),
@@ -174,11 +183,11 @@ module NotesHelper
discussion.resolved_by_push? ? 'Automatically resolved' : 'Resolved'
end
- def has_vue_discussions_cookie?
- cookies[:vue_mr_discussions] == 'true'
+ def rendered_for_merge_request?
+ params[:from_merge_request].present?
end
def serialize_notes?
- has_vue_discussions_cookie? && !params['html']
+ rendered_for_merge_request? || params['html'].nil?
end
end
diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb
index a8397b03d63..68d892393ef 100644
--- a/app/helpers/page_layout_helper.rb
+++ b/app/helpers/page_layout_helper.rb
@@ -39,10 +39,7 @@ module PageLayoutHelper
end
def favicon
- return 'favicon-yellow.ico' if Gitlab::Utils.to_boolean(ENV['CANARY'])
- return 'favicon-blue.ico' if Rails.env.development?
-
- 'favicon.ico'
+ Gitlab::Favicon.main
end
def page_image
diff --git a/app/helpers/pipeline_schedules_helper.rb b/app/helpers/pipeline_schedules_helper.rb
index 6edaf78de1b..4b9f6bd2caf 100644
--- a/app/helpers/pipeline_schedules_helper.rb
+++ b/app/helpers/pipeline_schedules_helper.rb
@@ -3,7 +3,7 @@ module PipelineSchedulesHelper
ActiveSupport::TimeZone.all.map do |timezone|
{
name: timezone.name,
- offset: timezone.utc_offset,
+ offset: timezone.now.utc_offset,
identifier: timezone.tzinfo.identifier
}
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index fa54eafd3a3..b0f381db5ab 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -40,7 +40,8 @@ module ProjectsHelper
name_tag_options[:class] << 'has-tooltip'
end
- content_tag(:span, sanitize(username), name_tag_options)
+ # NOTE: ActionView::Helpers::TagHelper#content_tag HTML escapes username
+ content_tag(:span, username, name_tag_options)
end
def link_to_member(project, author, opts = {}, &block)
@@ -171,11 +172,13 @@ module ProjectsHelper
key = [
project.route.cache_key,
project.cache_key,
+ project.last_activity_date,
controller.controller_name,
controller.action_name,
Gitlab::CurrentSettings.cache_key,
"cross-project:#{can?(current_user, :read_cross_project)}",
- 'v2.5'
+ max_project_member_access_cache_key(project),
+ 'v2.6'
]
key << pipeline_status_cache_key(project.pipeline_status) if project.pipeline_status.has_status?
@@ -238,6 +241,14 @@ module ProjectsHelper
"git push --set-upstream #{repository_url}/$(git rev-parse --show-toplevel | xargs basename).git $(git rev-parse --abbrev-ref HEAD)"
end
+ def show_xcode_link?(project = @project)
+ browser.platform.mac? && project.repository.xcode_project?
+ end
+
+ def xcode_uri_to_repo(project = @project)
+ "xcode://clone?repo=#{CGI.escape(default_url_to_repo(project))}"
+ end
+
private
def get_project_nav_tabs(project, current_user)
@@ -257,6 +268,9 @@ module ProjectsHelper
if project.builds_enabled? && can?(current_user, :read_pipeline, project)
nav_tabs << :pipelines
+ end
+
+ if can?(current_user, :read_environment, project) || can?(current_user, :read_cluster, project)
nav_tabs << :operations
end
@@ -338,11 +352,15 @@ module ProjectsHelper
if allowed_protocols_present?
enabled_protocol
else
- if !current_user || current_user.require_ssh_key?
- gitlab_config.protocol
- else
- 'ssh'
- end
+ extra_default_clone_protocol
+ end
+ end
+
+ def extra_default_clone_protocol
+ if !current_user || current_user.require_ssh_key?
+ gitlab_config.protocol
+ else
+ 'ssh'
end
end
@@ -378,11 +396,11 @@ module ProjectsHelper
def project_status_css_class(status)
case status
when "started"
- "active"
+ "table-active"
when "failed"
- "danger"
+ "table-danger"
when "finished"
- "success"
+ "table-success"
end
end
@@ -395,13 +413,17 @@ module ProjectsHelper
@ref || @repository.try(:root_ref)
end
+ # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/1235
def sanitize_repo_path(project, message)
return '' unless message.present?
exports_path = File.join(Settings.shared['path'], 'tmp/project_exports')
filtered_message = message.strip.gsub(exports_path, "[REPO EXPORT PATH]")
- disk_path = Gitlab.config.repositories.storages[project.repository_storage].legacy_disk_path
+ disk_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ Gitlab.config.repositories.storages[project.repository_storage].legacy_disk_path
+ end
+
filtered_message.gsub(disk_path.chomp('/'), "[REPOS PATH]")
end
@@ -485,4 +507,45 @@ module ProjectsHelper
"list-label"
end
end
+
+ def sidebar_projects_paths
+ %w[
+ projects#show
+ projects#activity
+ cycle_analytics#show
+ ]
+ end
+
+ def sidebar_settings_paths
+ %w[
+ projects#edit
+ project_members#index
+ integrations#show
+ services#edit
+ repository#show
+ ci_cd#show
+ badges#index
+ pages#show
+ ]
+ end
+
+ def sidebar_repository_paths
+ %w[
+ tree
+ blob
+ blame
+ edit_tree
+ new_tree
+ find_file
+ commit
+ commits
+ compare
+ projects/repositories
+ tags
+ branches
+ releases
+ graphs
+ network
+ ]
+ end
end
diff --git a/app/helpers/rss_helper.rb b/app/helpers/rss_helper.rb
index 9ac4df88dc3..7d4fa83a67a 100644
--- a/app/helpers/rss_helper.rb
+++ b/app/helpers/rss_helper.rb
@@ -1,5 +1,5 @@
module RssHelper
def rss_url_options
- { format: :atom, rss_token: current_user.try(:rss_token) }
+ { format: :atom, feed_token: current_user.try(:feed_token) }
end
end
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index 761c1252fc8..f7dafca7834 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -25,14 +25,22 @@ module SearchHelper
return unless collection.count > 0
from = collection.offset_value + 1
- to = collection.offset_value + collection.length
+ to = collection.offset_value + collection.count
count = collection.total_count
"Showing #{from} - #{to} of #{count} #{scope.humanize(capitalize: false)} for \"#{term}\""
end
+ def find_project_for_result_blob(result)
+ @project
+ end
+
def parse_search_result(result)
- Gitlab::ProjectSearchResults.parse_search_result(result)
+ result
+ end
+
+ def search_blob_title(project, filename)
+ filename
end
private
diff --git a/app/helpers/time_helper.rb b/app/helpers/time_helper.rb
index 271e839692a..336385f6798 100644
--- a/app/helpers/time_helper.rb
+++ b/app/helpers/time_helper.rb
@@ -5,9 +5,13 @@ module TimeHelper
seconds = interval_in_seconds - minutes * 60
if minutes >= 1
- "#{pluralize(minutes, "minute")} #{pluralize(seconds, "second")}"
+ if seconds % 60 == 0
+ pluralize(minutes, "minute")
+ else
+ [pluralize(minutes, "minute"), pluralize(seconds, "second")].to_sentence
+ end
else
- "#{pluralize(seconds, "second")}"
+ pluralize(seconds, "second")
end
end
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index ce9373f5883..4d17b22a4a1 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -31,6 +31,14 @@ module UsersHelper
current_user_menu_items.include?(item)
end
+ def max_project_member_access(project)
+ current_user&.max_member_access_for_project(project.id) || Gitlab::Access::NO_ACCESS
+ end
+
+ def max_project_member_access_cache_key(project)
+ "access:#{max_project_member_access(project)}"
+ end
+
private
def get_profile_tabs
diff --git a/app/helpers/webpack_helper.rb b/app/helpers/webpack_helper.rb
index e12e4ba70e9..72f6b397046 100644
--- a/app/helpers/webpack_helper.rb
+++ b/app/helpers/webpack_helper.rb
@@ -1,5 +1,3 @@
-require 'gitlab/webpack/manifest'
-
module WebpackHelper
def webpack_bundle_tag(bundle)
javascript_include_tag(*webpack_entrypoint_paths(bundle))
diff --git a/app/helpers/workhorse_helper.rb b/app/helpers/workhorse_helper.rb
index 9f78b80c71d..a82271ce0ee 100644
--- a/app/helpers/workhorse_helper.rb
+++ b/app/helpers/workhorse_helper.rb
@@ -6,7 +6,7 @@ module WorkhorseHelper
headers.store(*Gitlab::Workhorse.send_git_blob(repository, blob))
headers['Content-Disposition'] = 'inline'
headers['Content-Type'] = safe_content_type(blob)
- head :ok # 'render nothing: true' messes up the Content-Type
+ render plain: ""
end
# Send a Git diff through Workhorse
diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb
index b3f2aeb08ca..70509e9066d 100644
--- a/app/mailers/emails/merge_requests.rb
+++ b/app/mailers/emails/merge_requests.rb
@@ -56,6 +56,12 @@ module Emails
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason))
end
+ def merge_request_unmergeable_email(recipient_id, merge_request_id, reason = nil)
+ setup_merge_request_mail(merge_request_id, recipient_id)
+
+ mail_answer_thread(@merge_request, merge_request_thread_options(@merge_request.author_id, recipient_id, reason))
+ end
+
def resolved_all_discussions_email(recipient_id, merge_request_id, resolved_by_user_id, reason = nil)
setup_merge_request_mail(merge_request_id, recipient_id)
diff --git a/spec/mailers/previews/devise_mailer_preview.rb b/app/mailers/previews/devise_mailer_preview.rb
index d6588efc486..d6588efc486 100644
--- a/spec/mailers/previews/devise_mailer_preview.rb
+++ b/app/mailers/previews/devise_mailer_preview.rb
diff --git a/spec/mailers/previews/email_rejection_mailer_preview.rb b/app/mailers/previews/email_rejection_mailer_preview.rb
index 639e8471232..639e8471232 100644
--- a/spec/mailers/previews/email_rejection_mailer_preview.rb
+++ b/app/mailers/previews/email_rejection_mailer_preview.rb
diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb
new file mode 100644
index 00000000000..3615cde8026
--- /dev/null
+++ b/app/mailers/previews/notify_preview.rb
@@ -0,0 +1,170 @@
+class NotifyPreview < ActionMailer::Preview
+ def note_merge_request_email_for_individual_note
+ note_email(:note_merge_request_email) do
+ note = <<-MD.strip_heredoc
+ This is an individual note on a merge request :smiley:
+
+ In this notification email, we expect to see:
+
+ - The note contents (that's what you're looking at)
+ - A link to view this note on Gitlab
+ - An explanation for why the user is receiving this notification
+ MD
+
+ create_note(noteable_type: 'merge_request', noteable_id: merge_request.id, note: note)
+ end
+ end
+
+ def note_merge_request_email_for_discussion
+ note_email(:note_merge_request_email) do
+ note = <<-MD.strip_heredoc
+ This is a new discussion on a merge request :smiley:
+
+ In this notification email, we expect to see:
+
+ - A line saying who started this discussion
+ - The note contents (that's what you're looking at)
+ - A link to view this discussion on Gitlab
+ - An explanation for why the user is receiving this notification
+ MD
+
+ create_note(noteable_type: 'merge_request', noteable_id: merge_request.id, type: 'DiscussionNote', note: note)
+ end
+ end
+
+ def note_merge_request_email_for_diff_discussion
+ note_email(:note_merge_request_email) do
+ note = <<-MD.strip_heredoc
+ This is a new discussion on a merge request :smiley:
+
+ In this notification email, we expect to see:
+
+ - A line saying who started this discussion and on what file
+ - The diff
+ - The note contents (that's what you're looking at)
+ - A link to view this discussion on Gitlab
+ - An explanation for why the user is receiving this notification
+ MD
+
+ position = Gitlab::Diff::Position.new(
+ old_path: "files/ruby/popen.rb",
+ new_path: "files/ruby/popen.rb",
+ old_line: nil,
+ new_line: 14,
+ diff_refs: merge_request.diff_refs
+ )
+
+ create_note(noteable_type: 'merge_request', noteable_id: merge_request.id, type: 'DiffNote', position: position, note: note)
+ end
+ end
+
+ def closed_issue_email
+ Notify.closed_issue_email(user.id, issue.id, user.id).message
+ end
+
+ def issue_status_changed_email
+ Notify.issue_status_changed_email(user.id, issue.id, 'closed', user.id).message
+ end
+
+ def closed_merge_request_email
+ Notify.closed_merge_request_email(user.id, issue.id, user.id).message
+ end
+
+ def merge_request_status_email
+ Notify.merge_request_status_email(user.id, merge_request.id, 'closed', user.id).message
+ end
+
+ def merged_merge_request_email
+ Notify.merged_merge_request_email(user.id, merge_request.id, user.id).message
+ end
+
+ def member_access_denied_email
+ Notify.member_access_denied_email('project', project.id, user.id).message
+ end
+
+ def member_access_granted_email
+ Notify.member_access_granted_email('project', user.id).message
+ end
+
+ def member_access_requested_email
+ Notify.member_access_requested_email('group', user.id, 'some@example.com').message
+ end
+
+ def member_invite_accepted_email
+ Notify.member_invite_accepted_email('project', user.id).message
+ end
+
+ def member_invite_declined_email
+ Notify.member_invite_declined_email(
+ 'project',
+ project.id,
+ 'invite@example.com',
+ user.id
+ ).message
+ end
+
+ def member_invited_email
+ Notify.member_invited_email('project', user.id, '1234').message
+ end
+
+ def pages_domain_enabled_email
+ cleanup do
+ pages_domain = PagesDomain.new(domain: 'my.example.com', project: project, verified_at: Time.now, enabled_until: 1.week.from_now)
+
+ Notify.pages_domain_enabled_email(pages_domain, user).message
+ end
+ end
+
+ def pipeline_success_email
+ Notify.pipeline_success_email(pipeline, pipeline.user.try(:email))
+ end
+
+ def pipeline_failed_email
+ Notify.pipeline_failed_email(pipeline, pipeline.user.try(:email))
+ end
+
+ private
+
+ def project
+ @project ||= Project.find_by_full_path('gitlab-org/gitlab-test')
+ end
+
+ def issue
+ @merge_request ||= project.issues.first
+ end
+
+ def merge_request
+ @merge_request ||= project.merge_requests.first
+ end
+
+ def pipeline
+ @pipeline = Ci::Pipeline.last
+ end
+
+ def user
+ @user ||= User.last
+ end
+
+ def create_note(params)
+ Notes::CreateService.new(project, user, params).execute
+ end
+
+ def note_email(method)
+ cleanup do
+ note = yield
+
+ Notify.public_send(method, user.id, note) # rubocop:disable GitlabSecurity/PublicSend
+ end
+ end
+
+ def cleanup
+ email = nil
+
+ ActiveRecord::Base.transaction do
+ email = yield
+ raise ActiveRecord::Rollback
+ end
+
+ email
+ end
+end
diff --git a/spec/mailers/previews/repository_check_mailer_preview.rb b/app/mailers/previews/repository_check_mailer_preview.rb
index 19d4eab1805..19d4eab1805 100644
--- a/spec/mailers/previews/repository_check_mailer_preview.rb
+++ b/app/mailers/previews/repository_check_mailer_preview.rb
diff --git a/app/models/appearance.rb b/app/models/appearance.rb
index 67cc84a9140..b770aadef0e 100644
--- a/app/models/appearance.rb
+++ b/app/models/appearance.rb
@@ -14,6 +14,7 @@ class Appearance < ActiveRecord::Base
mount_uploader :logo, AttachmentUploader
mount_uploader :header_logo, AttachmentUploader
+ mount_uploader :favicon, FaviconUploader
# Overrides CacheableAttributes.current_without_cache
def self.current_without_cache
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index e8ccb320fae..f770b219422 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -212,14 +212,6 @@ class ApplicationSetting < ActiveRecord::Base
end
end
- validates_each :disabled_oauth_sign_in_sources do |record, attr, value|
- value&.each do |source|
- unless Devise.omniauth_providers.include?(source.to_sym)
- record.errors.add(attr, "'#{source}' is not an OAuth sign-in source")
- end
- end
- end
-
validate :terms_exist, if: :enforce_terms?
before_validation :ensure_uuid!
@@ -230,6 +222,7 @@ class ApplicationSetting < ActiveRecord::Base
after_commit do
reset_memoized_terms
end
+ after_commit :expire_performance_bar_allowed_user_ids_cache, if: -> { previous_changes.key?('performance_bar_allowed_group_id') }
def self.defaults
{
@@ -301,6 +294,7 @@ class ApplicationSetting < ActiveRecord::Base
gitaly_timeout_medium: 30,
gitaly_timeout_default: 55,
allow_local_requests_from_hooks_and_services: false,
+ hide_third_party_offers: false,
mirror_available: true
}
end
@@ -329,6 +323,11 @@ class ApplicationSetting < ActiveRecord::Base
::Gitlab::Database.cached_column_exists?(:application_settings, :sidekiq_throttling_enabled)
end
+ def disabled_oauth_sign_in_sources=(sources)
+ sources = (sources || []).map(&:to_s) & Devise.omniauth_providers.map(&:to_s)
+ super(sources)
+ end
+
def domain_whitelist_raw
self.domain_whitelist&.join("\n")
end
@@ -359,17 +358,6 @@ class ApplicationSetting < ActiveRecord::Base
Array(read_attribute(:repository_storages))
end
- # DEPRECATED
- # repository_storage is still required in the API. Remove in 9.0
- # Still used in API v3
- def repository_storage
- repository_storages.first
- end
-
- def repository_storage=(value)
- self.repository_storages = [value]
- end
-
def default_project_visibility=(level)
super(Gitlab::VisibilityLevel.level_value(level))
end
@@ -386,31 +374,6 @@ class ApplicationSetting < ActiveRecord::Base
super(levels.map { |level| Gitlab::VisibilityLevel.level_value(level) })
end
- def performance_bar_allowed_group_id=(group_full_path)
- group_full_path = nil if group_full_path.blank?
-
- if group_full_path.nil?
- if group_full_path != performance_bar_allowed_group_id
- super(group_full_path)
- Gitlab::PerformanceBar.expire_allowed_user_ids_cache
- end
-
- return
- end
-
- group = Group.find_by_full_path(group_full_path)
-
- if group
- if group.id != performance_bar_allowed_group_id
- super(group.id)
- Gitlab::PerformanceBar.expire_allowed_user_ids_cache
- end
- else
- super(nil)
- Gitlab::PerformanceBar.expire_allowed_user_ids_cache
- end
- end
-
def performance_bar_allowed_group
Group.find_by_id(performance_bar_allowed_group_id)
end
@@ -420,15 +383,6 @@ class ApplicationSetting < ActiveRecord::Base
performance_bar_allowed_group_id.present?
end
- # - If `enable` is true, we early return since the actual attribute that holds
- # 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 Gitlab::Utils.to_boolean(enable)
-
- self.performance_bar_allowed_group_id = nil
- end
-
# Choose one of the available repository storage options. Currently all have
# equal weighting.
def pick_repository_storage
@@ -506,4 +460,8 @@ class ApplicationSetting < ActiveRecord::Base
errors.add(:terms, "You need to set terms to be enforced") unless terms.present?
end
+
+ def expire_performance_bar_allowed_user_ids_cache
+ Gitlab::PerformanceBar.expire_allowed_user_ids_cache
+ end
end
diff --git a/app/models/application_setting/term.rb b/app/models/application_setting/term.rb
index e8ce0ccbb71..3b1dfe7e4ef 100644
--- a/app/models/application_setting/term.rb
+++ b/app/models/application_setting/term.rb
@@ -1,6 +1,7 @@
class ApplicationSetting
class Term < ActiveRecord::Base
include CacheMarkdownField
+ has_many :term_agreements
validates :terms, presence: true
@@ -9,5 +10,10 @@ class ApplicationSetting
def self.latest
order(:id).last
end
+
+ def accepted_by_user?(user)
+ user.accepted_term_id == id ||
+ term_agreements.accepted.where(user: user).exists?
+ end
end
end
diff --git a/app/models/badge.rb b/app/models/badge.rb
index f7e10c2ebfc..265c5d872d4 100644
--- a/app/models/badge.rb
+++ b/app/models/badge.rb
@@ -18,7 +18,7 @@ class Badge < ActiveRecord::Base
scope :order_created_at_asc, -> { reorder(created_at: :asc) }
- validates :link_url, :image_url, url_placeholder: { protocols: %w(http https), placeholder_regex: PLACEHOLDERS_REGEX }
+ validates :link_url, :image_url, url: { protocols: %w(http https) }
validates :type, presence: true
def rendered_link_url(project = nil)
diff --git a/app/models/board.rb b/app/models/board.rb
index 3cede6fc99a..bb6bb753daf 100644
--- a/app/models/board.rb
+++ b/app/models/board.rb
@@ -26,4 +26,8 @@ class Board < ActiveRecord::Base
def closed_list
lists.merge(List.closed).take
end
+
+ def scoped?
+ false
+ end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 495430931aa..d8ddb4bc667 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -27,7 +27,13 @@ module Ci
has_one :job_artifacts_trace, -> { where(file_type: Ci::JobArtifact.file_types[:trace]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
has_one :metadata, class_name: 'Ci::BuildMetadata'
+ has_one :runner_session, class_name: 'Ci::BuildRunnerSession', validate: true, inverse_of: :build
+
+ accepts_nested_attributes_for :runner_session
+
delegate :timeout, to: :metadata, prefix: true, allow_nil: true
+ delegate :url, to: :runner_session, prefix: true, allow_nil: true
+ delegate :terminal_specification, to: :runner_session, allow_nil: true
delegate :gitlab_deploy_token, to: :project
##
@@ -55,12 +61,18 @@ module Ci
where('(artifacts_file IS NOT NULL AND artifacts_file <> ?) OR EXISTS (?)',
'', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').archive)
end
+
+ scope :without_archived_trace, ->() do
+ where('NOT EXISTS (?)', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').trace)
+ end
+
scope :with_artifacts_stored_locally, -> { with_artifacts_archive.where(artifacts_file_store: [nil, LegacyArtifactUploader::Store::LOCAL]) }
scope :with_artifacts_not_expired, ->() { with_artifacts_archive.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) }
scope :with_expired_artifacts, ->() { with_artifacts_archive.where('artifacts_expire_at < ?', Time.now) }
scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) }
scope :manual_actions, ->() { where(when: :manual, status: COMPLETED_STATUSES + [:manual]) }
scope :ref_protected, -> { where(protected: true) }
+ scope :with_live_trace, -> { where('EXISTS (?)', Ci::BuildTraceChunk.where('ci_builds.id = ci_build_trace_chunks.build_id').select(1)) }
scope :matches_tag_ids, -> (tag_ids) do
matcher = ::ActsAsTaggableOn::Tagging
@@ -144,6 +156,7 @@ module Ci
after_transition any => [:success] do |build|
build.run_after_commit do
BuildSuccessWorker.perform_async(id)
+ PagesWorker.perform_async(:deploy, id) if build.pages_generator?
end
end
@@ -167,6 +180,10 @@ module Ci
after_transition pending: :running do |build|
build.ensure_metadata.update_timeout_state
end
+
+ after_transition running: any do |build|
+ Ci::BuildRunnerSession.where(build: build).delete_all
+ end
end
def ensure_metadata
@@ -183,6 +200,11 @@ module Ci
pipeline.manual_actions.where.not(name: name)
end
+ def pages_generator?
+ Gitlab.config.pages.enabled &&
+ self.name == 'pages'
+ end
+
def playable?
action? && (manual? || retryable?)
end
@@ -349,7 +371,7 @@ module Ci
def update_coverage
coverage = trace.extract_coverage(coverage_regex)
- update_attributes(coverage: coverage) if coverage.present?
+ update(coverage: coverage) if coverage.present?
end
def parse_trace_sections!
@@ -364,6 +386,10 @@ module Ci
trace.exist?
end
+ def has_old_trace?
+ old_trace.present?
+ end
+
def trace=(data)
raise NotImplementedError
end
@@ -373,6 +399,8 @@ module Ci
end
def erase_old_trace!
+ return unless has_old_trace?
+
update_column(:trace, nil)
end
@@ -402,8 +430,6 @@ module Ci
build_data = Gitlab::DataBuilder::Build.build(self)
project.execute_hooks(build_data.dup, :job_hooks)
project.execute_services(build_data.dup, :job_hooks)
- PagesService.new(build_data).execute
- project.running_or_pending_build_count(force: true)
end
def browsable_artifacts?
@@ -411,9 +437,9 @@ module Ci
end
def artifacts_metadata_entry(path, **options)
- artifacts_metadata.use_file do |metadata_path|
+ artifacts_metadata.open do |metadata_stream|
metadata = Gitlab::Ci::Build::Artifacts::Metadata.new(
- metadata_path,
+ metadata_stream,
path,
**options)
@@ -574,6 +600,10 @@ module Ci
super(options).merge(when: read_attribute(:when))
end
+ def has_terminal?
+ running? && runner_session_url.present?
+ end
+
private
def update_artifacts_size
@@ -601,6 +631,7 @@ module Ci
variables
.concat(pipeline.persisted_variables)
.append(key: 'CI_JOB_ID', value: id.to_s)
+ .append(key: 'CI_JOB_URL', value: Gitlab::Routing.url_helpers.project_job_url(project, self))
.append(key: 'CI_JOB_TOKEN', value: token, public: false)
.append(key: 'CI_BUILD_ID', value: id.to_s)
.append(key: 'CI_BUILD_TOKEN', value: token, public: false)
@@ -618,7 +649,7 @@ module Ci
variables.append(key: 'GITLAB_FEATURES', value: project.licensed_features.join(','))
variables.append(key: 'CI_SERVER_NAME', value: 'GitLab')
variables.append(key: 'CI_SERVER_VERSION', value: Gitlab::VERSION)
- variables.append(key: 'CI_SERVER_REVISION', value: Gitlab::REVISION)
+ variables.append(key: 'CI_SERVER_REVISION', value: Gitlab.revision)
variables.append(key: 'CI_JOB_NAME', value: name)
variables.append(key: 'CI_JOB_STAGE', value: stage)
variables.append(key: 'CI_COMMIT_SHA', value: sha)
diff --git a/app/models/ci/build_runner_session.rb b/app/models/ci/build_runner_session.rb
new file mode 100644
index 00000000000..6f3be31d8e1
--- /dev/null
+++ b/app/models/ci/build_runner_session.rb
@@ -0,0 +1,25 @@
+module Ci
+ # The purpose of this class is to store Build related runner session.
+ # Data will be removed after transitioning from running to any state.
+ class BuildRunnerSession < ActiveRecord::Base
+ extend Gitlab::Ci::Model
+
+ self.table_name = 'ci_builds_runner_session'
+
+ belongs_to :build, class_name: 'Ci::Build', inverse_of: :runner_session
+
+ validates :build, presence: true
+ validates :url, url: { protocols: %w(https) }
+
+ def terminal_specification
+ return {} unless url.present?
+
+ {
+ subprotocols: ['terminal.gitlab.com'].freeze,
+ url: "#{url}/exec".sub("https://", "wss://"),
+ headers: { Authorization: authorization.presence }.compact,
+ ca_pem: certificate.presence
+ }
+ end
+ end
+end
diff --git a/app/models/ci/build_trace_chunk.rb b/app/models/ci/build_trace_chunk.rb
index 4856f10846c..b442de34061 100644
--- a/app/models/ci/build_trace_chunk.rb
+++ b/app/models/ci/build_trace_chunk.rb
@@ -1,54 +1,58 @@
module Ci
class BuildTraceChunk < ActiveRecord::Base
include FastDestroyAll
+ include ::Gitlab::ExclusiveLeaseHelpers
extend Gitlab::Ci::Model
belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id
default_value_for :data_store, :redis
- WriteError = Class.new(StandardError)
-
CHUNK_SIZE = 128.kilobytes
- CHUNK_REDIS_TTL = 1.week
WRITE_LOCK_RETRY = 10
WRITE_LOCK_SLEEP = 0.01.seconds
WRITE_LOCK_TTL = 1.minute
+ # Note: The ordering of this enum is related to the precedence of persist store.
+ # The bottom item takes the higest precedence, and the top item takes the lowest precedence.
enum data_store: {
redis: 1,
- db: 2
+ database: 2,
+ fog: 3
}
class << self
- def redis_data_key(build_id, chunk_index)
- "gitlab:ci:trace:#{build_id}:chunks:#{chunk_index}"
+ def all_stores
+ @all_stores ||= self.data_stores.keys
end
- def redis_data_keys
- redis.pluck(:build_id, :chunk_index).map do |data|
- redis_data_key(data.first, data.second)
- end
+ def persistable_store
+ # get first available store from the back of the list
+ all_stores.reverse.find { |store| get_store_class(store).available? }
end
- def redis_delete_data(keys)
- return if keys.empty?
-
- Gitlab::Redis::SharedState.with do |redis|
- redis.del(keys)
- end
+ def get_store_class(store)
+ @stores ||= {}
+ @stores[store] ||= "Ci::BuildTraceChunks::#{store.capitalize}".constantize.new
end
##
# FastDestroyAll concerns
def begin_fast_destroy
- redis_data_keys
+ all_stores.each_with_object({}) do |store, result|
+ relation = public_send(store) # rubocop:disable GitlabSecurity/PublicSend
+ keys = get_store_class(store).keys(relation)
+
+ result[store] = keys if keys.present?
+ end
end
##
# FastDestroyAll concerns
def finalize_fast_destroy(keys)
- redis_delete_data(keys)
+ keys.each do |store, value|
+ get_store_class(store).delete_keys(value)
+ end
end
end
@@ -66,10 +70,15 @@ module Ci
end
def append(new_data, offset)
+ raise ArgumentError, 'New data is missing' unless new_data
raise ArgumentError, 'Offset is out of range' if offset > size || offset < 0
raise ArgumentError, 'Chunk size overflow' if CHUNK_SIZE < (offset + new_data.bytesize)
- set_data(data.byteslice(0, offset) + new_data)
+ in_lock(*lock_params) do # Write opetation is atomic
+ unsafe_set_data!(data.byteslice(0, offset) + new_data)
+ end
+
+ schedule_to_persist if full?
end
def size
@@ -88,93 +97,63 @@ module Ci
(start_offset...end_offset)
end
- def use_database!
- in_lock do
- break if db?
- break unless size > 0
-
- self.update!(raw_data: data, data_store: :db)
- self.class.redis_delete_data([redis_data_key])
+ def persist_data!
+ in_lock(*lock_params) do # Write opetation is atomic
+ unsafe_persist_to!(self.class.persistable_store)
end
end
private
- def get_data
- if redis?
- redis_data
- elsif db?
- raw_data
- else
- raise 'Unsupported data store'
- end&.force_encoding(Encoding::BINARY) # Redis/Database return UTF-8 string as default
- end
-
- def set_data(value)
- raise ArgumentError, 'too much data' if value.bytesize > CHUNK_SIZE
-
- in_lock do
- if redis?
- redis_set_data(value)
- elsif db?
- self.raw_data = value
- else
- raise 'Unsupported data store'
- end
+ def unsafe_persist_to!(new_store)
+ return if data_store == new_store.to_s
+ raise ArgumentError, 'Can not persist empty data' unless size > 0
- @data = value
+ old_store_class = self.class.get_store_class(data_store)
- save! if changed?
+ get_data.tap do |the_data|
+ self.raw_data = nil
+ self.data_store = new_store
+ unsafe_set_data!(the_data)
end
- schedule_to_db if full?
- end
-
- def schedule_to_db
- return if db?
-
- Ci::BuildTraceChunkFlushWorker.perform_async(id)
+ old_store_class.delete_data(self)
end
- def full?
- size == CHUNK_SIZE
+ def get_data
+ self.class.get_store_class(data_store).data(self)&.force_encoding(Encoding::BINARY) # Redis/Database return UTF-8 string as default
+ rescue Excon::Error::NotFound
+ # If the data store is :fog and the file does not exist in the object storage, this method returns nil.
end
- def redis_data
- Gitlab::Redis::SharedState.with do |redis|
- redis.get(redis_data_key)
- end
- end
+ def unsafe_set_data!(value)
+ raise ArgumentError, 'New data size exceeds chunk size' if value.bytesize > CHUNK_SIZE
- def redis_set_data(data)
- Gitlab::Redis::SharedState.with do |redis|
- redis.set(redis_data_key, data, ex: CHUNK_REDIS_TTL)
- end
- end
+ self.class.get_store_class(data_store).set_data(self, value)
+ @data = value
- def redis_data_key
- self.class.redis_data_key(build_id, chunk_index)
+ save! if changed?
end
- def in_lock
- write_lock_key = "trace_write:#{build_id}:chunks:#{chunk_index}"
+ def schedule_to_persist
+ return if data_persisted?
- lease = Gitlab::ExclusiveLease.new(write_lock_key, timeout: WRITE_LOCK_TTL)
- retry_count = 0
+ Ci::BuildTraceChunkFlushWorker.perform_async(id)
+ end
- until uuid = lease.try_obtain
- # Keep trying until we obtain the lease. To prevent hammering Redis too
- # much we'll wait for a bit between retries.
- sleep(WRITE_LOCK_SLEEP)
- break if WRITE_LOCK_RETRY < (retry_count += 1)
- end
+ def data_persisted?
+ !redis?
+ end
- raise WriteError, 'Failed to obtain write lock' unless uuid
+ def full?
+ size == CHUNK_SIZE
+ end
- self.reload if self.persisted?
- return yield
- ensure
- Gitlab::ExclusiveLease.cancel(write_lock_key, uuid)
+ def lock_params
+ ["trace_write:#{build_id}:chunks:#{chunk_index}",
+ { ttl: WRITE_LOCK_TTL,
+ retries: WRITE_LOCK_RETRY,
+ sleep_sec: WRITE_LOCK_SLEEP }]
end
end
end
diff --git a/app/models/ci/build_trace_chunks/database.rb b/app/models/ci/build_trace_chunks/database.rb
new file mode 100644
index 00000000000..3666d77c790
--- /dev/null
+++ b/app/models/ci/build_trace_chunks/database.rb
@@ -0,0 +1,29 @@
+module Ci
+ module BuildTraceChunks
+ class Database
+ def available?
+ true
+ end
+
+ def keys(relation)
+ []
+ end
+
+ def delete_keys(keys)
+ # no-op
+ end
+
+ def data(model)
+ model.raw_data
+ end
+
+ def set_data(model, data)
+ model.raw_data = data
+ end
+
+ def delete_data(model)
+ model.update_columns(raw_data: nil) unless model.raw_data.nil?
+ end
+ end
+ end
+end
diff --git a/app/models/ci/build_trace_chunks/fog.rb b/app/models/ci/build_trace_chunks/fog.rb
new file mode 100644
index 00000000000..7506c40a39d
--- /dev/null
+++ b/app/models/ci/build_trace_chunks/fog.rb
@@ -0,0 +1,59 @@
+module Ci
+ module BuildTraceChunks
+ class Fog
+ def available?
+ object_store.enabled
+ end
+
+ def data(model)
+ connection.get_object(bucket_name, key(model))[:body]
+ end
+
+ def set_data(model, data)
+ connection.put_object(bucket_name, key(model), data)
+ end
+
+ def delete_data(model)
+ delete_keys([[model.build_id, model.chunk_index]])
+ end
+
+ def keys(relation)
+ return [] unless available?
+
+ relation.pluck(:build_id, :chunk_index)
+ end
+
+ def delete_keys(keys)
+ keys.each do |key|
+ connection.delete_object(bucket_name, key_raw(*key))
+ end
+ end
+
+ private
+
+ def key(model)
+ key_raw(model.build_id, model.chunk_index)
+ end
+
+ def key_raw(build_id, chunk_index)
+ "tmp/builds/#{build_id.to_i}/chunks/#{chunk_index.to_i}.log"
+ end
+
+ def bucket_name
+ return unless available?
+
+ object_store.remote_directory
+ end
+
+ def connection
+ return unless available?
+
+ @connection ||= ::Fog::Storage.new(object_store.connection.to_hash.deep_symbolize_keys)
+ end
+
+ def object_store
+ Gitlab.config.artifacts.object_store
+ end
+ end
+ end
+end
diff --git a/app/models/ci/build_trace_chunks/redis.rb b/app/models/ci/build_trace_chunks/redis.rb
new file mode 100644
index 00000000000..fdb6065e2a0
--- /dev/null
+++ b/app/models/ci/build_trace_chunks/redis.rb
@@ -0,0 +1,51 @@
+module Ci
+ module BuildTraceChunks
+ class Redis
+ CHUNK_REDIS_TTL = 1.week
+
+ def available?
+ true
+ end
+
+ def data(model)
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.get(key(model))
+ end
+ end
+
+ def set_data(model, data)
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set(key(model), data, ex: CHUNK_REDIS_TTL)
+ end
+ end
+
+ def delete_data(model)
+ delete_keys([[model.build_id, model.chunk_index]])
+ end
+
+ def keys(relation)
+ relation.pluck(:build_id, :chunk_index)
+ end
+
+ def delete_keys(keys)
+ return if keys.empty?
+
+ keys = keys.map { |key| key_raw(*key) }
+
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.del(keys)
+ end
+ end
+
+ private
+
+ def key(model)
+ key_raw(model.build_id, model.chunk_index)
+ end
+
+ def key_raw(build_id, chunk_index)
+ "gitlab:ci:trace:#{build_id.to_i}:chunks:#{chunk_index.to_i}"
+ end
+ end
+ end
+end
diff --git a/app/models/ci/group.rb b/app/models/ci/group.rb
index 87898b086c6..9c1046e8715 100644
--- a/app/models/ci/group.rb
+++ b/app/models/ci/group.rb
@@ -31,6 +31,14 @@ module Ci
end
end
+ def self.fabricate(stage)
+ stage.statuses.ordered.latest
+ .sort_by(&:sortable_name).group_by(&:group_name)
+ .map do |group_name, grouped_statuses|
+ self.new(stage, name: group_name, jobs: grouped_statuses)
+ end
+ end
+
private
def commit_statuses
diff --git a/app/models/ci/legacy_stage.rb b/app/models/ci/legacy_stage.rb
index 9b536af672b..ce691875e42 100644
--- a/app/models/ci/legacy_stage.rb
+++ b/app/models/ci/legacy_stage.rb
@@ -16,11 +16,7 @@ module Ci
end
def groups
- @groups ||= statuses.ordered.latest
- .sort_by(&:sortable_name).group_by(&:group_name)
- .map do |group_name, grouped_statuses|
- Ci::Group.new(self, name: group_name, jobs: grouped_statuses)
- end
+ @groups ||= Ci::Group.fabricate(self)
end
def to_param
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 53af87a271a..e5caa3ffa41 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -7,13 +7,19 @@ module Ci
include Presentable
include Gitlab::OptimisticLocking
include Gitlab::Utils::StrongMemoize
+ include AtomicInternalId
+ include EnumWithNil
belongs_to :project, inverse_of: :pipelines
belongs_to :user
belongs_to :auto_canceled_by, class_name: 'Ci::Pipeline'
belongs_to :pipeline_schedule, class_name: 'Ci::PipelineSchedule'
- has_many :stages
+ has_internal_id :iid, scope: :project, presence: false, init: ->(s) do
+ s&.project&.pipelines&.maximum(:iid) || s&.project&.pipelines&.count
+ end
+
+ has_many :stages, -> { order(position: :asc) }, inverse_of: :pipeline
has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id, inverse_of: :pipeline
has_many :builds, foreign_key: :commit_id, inverse_of: :pipeline
has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id # rubocop:disable Cop/ActiveRecordDependent
@@ -49,7 +55,7 @@ module Ci
after_create :keep_around_commits, unless: :importing?
- enum source: {
+ enum_with_nil source: {
unknown: nil,
push: 1,
web: 2,
@@ -59,7 +65,7 @@ module Ci
external: 6
}
- enum config_source: {
+ enum_with_nil config_source: {
unknown_source: nil,
repository_source: 1,
auto_devops_source: 2
@@ -249,6 +255,20 @@ module Ci
stage unless stage.statuses_count.zero?
end
+ ##
+ # TODO We do not completely switch to persisted stages because of
+ # race conditions with setting statuses gitlab-ce#23257.
+ #
+ def ordered_stages
+ return legacy_stages unless complete?
+
+ if Feature.enabled?('ci_pipeline_persisted_stages')
+ stages
+ else
+ legacy_stages
+ end
+ end
+
def legacy_stages
# TODO, this needs refactoring, see gitlab-ce#26481.
@@ -411,7 +431,7 @@ module Ci
def number_of_warnings
BatchLoader.for(id).batch(default_value: 0) do |pipeline_ids, loader|
- Build.where(commit_id: pipeline_ids)
+ ::Ci::Build.where(commit_id: pipeline_ids)
.latest
.failed_but_allowed
.group(:commit_id)
@@ -503,7 +523,8 @@ module Ci
def update_status
retry_optimistic_lock(self) do
- case latest_builds_status
+ case latest_builds_status.to_s
+ when 'created' then nil
when 'pending' then enqueue
when 'running' then run
when 'success' then succeed
@@ -511,6 +532,9 @@ module Ci
when 'canceled' then cancel
when 'skipped' then skip
when 'manual' then block
+ else
+ raise HasStatus::UnknownStatusError,
+ "Unknown status `#{latest_builds_status}`"
end
end
end
@@ -525,17 +549,21 @@ module Ci
def persisted_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
- variables.append(key: 'CI_PIPELINE_ID', value: id.to_s) if persisted?
+ break variables unless persisted?
+
+ variables.append(key: 'CI_PIPELINE_ID', value: id.to_s)
+ variables.append(key: 'CI_PIPELINE_URL', value: Gitlab::Routing.url_helpers.project_pipeline_url(project, self))
end
end
def predefined_variables
Gitlab::Ci::Variables::Collection.new
+ .append(key: 'CI_PIPELINE_IID', value: iid.to_s)
.append(key: 'CI_CONFIG_PATH', value: ci_yaml_file_path)
.append(key: 'CI_PIPELINE_SOURCE', value: source.to_s)
- .append(key: 'CI_COMMIT_MESSAGE', value: git_commit_message)
- .append(key: 'CI_COMMIT_TITLE', value: git_commit_full_title)
- .append(key: 'CI_COMMIT_DESCRIPTION', value: git_commit_description)
+ .append(key: 'CI_COMMIT_MESSAGE', value: git_commit_message.to_s)
+ .append(key: 'CI_COMMIT_TITLE', value: git_commit_full_title.to_s)
+ .append(key: 'CI_COMMIT_DESCRIPTION', value: git_commit_description.to_s)
end
def queued_duration
@@ -575,17 +603,6 @@ module Ci
@latest_builds_with_artifacts ||= builds.latest.with_artifacts_archive.to_a
end
- # Rails 5.0 autogenerated question mark enum methods return wrong result if enum value is nil.
- # They always return `false`.
- # These methods overwrite autogenerated ones to return correct results.
- def unknown?
- Gitlab.rails5? ? source.nil? : super
- end
-
- def unknown_source?
- Gitlab.rails5? ? config_source.nil? : super
- end
-
private
def ci_yaml_from_repo
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 530eacf4be0..bcd0c206bca 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -2,6 +2,7 @@ module Ci
class Runner < ActiveRecord::Base
extend Gitlab::Ci::Model
include Gitlab::SQL::Pattern
+ include IgnorableColumn
include RedisCacheable
include ChronicDurationAttribute
@@ -11,23 +12,28 @@ module Ci
AVAILABLE_SCOPES = %w[specific shared active paused online].freeze
FORM_EDITABLE = %i[description tag_list active run_untagged locked access_level maximum_timeout_human_readable].freeze
+ ignore_column :is_shared
+
has_many :builds
- has_many :runner_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :runner_projects, inverse_of: :runner, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :projects, through: :runner_projects
- has_many :runner_namespaces
+ has_many :runner_namespaces, inverse_of: :runner
has_many :groups, through: :runner_namespaces
has_one :last_build, ->() { order('id DESC') }, class_name: 'Ci::Build'
before_validation :set_default_values
- scope :specific, -> { where(is_shared: false) }
- scope :shared, -> { where(is_shared: true) }
scope :active, -> { where(active: true) }
scope :paused, -> { where(active: false) }
scope :online, -> { where('contacted_at > ?', contact_time_deadline) }
scope :ordered, -> { order(id: :desc) }
+ # BACKWARD COMPATIBILITY: There are needed to maintain compatibility with `AVAILABLE_SCOPES` used by `lib/api/runners.rb`
+ scope :deprecated_shared, -> { instance_type }
+ # this should get replaced with `project_type.or(group_type)` once using Rails5
+ scope :deprecated_specific, -> { where(runner_type: [runner_types[:project_type], runner_types[:group_type]]) }
+
scope :belonging_to_project, -> (project_id) {
joins(:runner_projects).where(ci_runner_projects: { project_id: project_id })
}
@@ -39,9 +45,9 @@ module Ci
joins(:groups).where(namespaces: { id: hierarchy_groups })
}
- scope :owned_or_shared, -> (project_id) do
+ scope :owned_or_instance_wide, -> (project_id) do
union = Gitlab::SQL::Union.new(
- [belonging_to_project(project_id), belonging_to_parent_group_of_project(project_id), shared],
+ [belonging_to_project(project_id), belonging_to_parent_group_of_project(project_id), instance_type],
remove_duplicates: false
)
from("(#{union.to_sql}) ci_runners")
@@ -56,10 +62,14 @@ module Ci
end
validate :tag_constraints
- validate :either_projects_or_group
validates :access_level, presence: true
validates :runner_type, presence: true
+ validate :no_projects, unless: :project_type?
+ validate :no_groups, unless: :group_type?
+ validate :any_project, if: :project_type?
+ validate :exactly_one_group, if: :group_type?
+
acts_as_taggable
after_destroy :cleanup_runner_queue
@@ -108,15 +118,21 @@ module Ci
end
def assign_to(project, current_user = nil)
- if shared?
- self.is_shared = false if shared?
+ if instance_type?
self.runner_type = :project_type
elsif group_type?
raise ArgumentError, 'Transitioning a group runner to a project runner is not supported'
end
- self.save
- project.runner_projects.create(runner_id: self.id)
+ begin
+ transaction do
+ self.projects << project
+ self.save!
+ end
+ rescue ActiveRecord::RecordInvalid => e
+ self.errors.add(:assign_to, e.message)
+ false
+ end
end
def display_name
@@ -125,10 +141,6 @@ module Ci
description
end
- def shared?
- is_shared
- end
-
def online?
contacted_at && contacted_at > self.class.contact_time_deadline
end
@@ -147,10 +159,6 @@ module Ci
runner_projects.count == 1
end
- def specific?
- !shared?
- end
-
def assigned_to_group?
runner_namespaces.any?
end
@@ -207,10 +215,8 @@ module Ci
cache_attributes(values)
- if persist_cached_data?
- self.assign_attributes(values)
- self.save if self.changed?
- end
+ # We save data without validation, it will always change due to `contacted_at`
+ self.update_columns(values) if persist_cached_data?
end
def pick_build!(build)
@@ -250,16 +256,30 @@ module Ci
end
def assignable_for?(project_id)
- self.class.owned_or_shared(project_id).where(id: self.id).any?
+ self.class.owned_or_instance_wide(project_id).where(id: self.id).any?
+ end
+
+ def no_projects
+ if projects.any?
+ errors.add(:runner, 'cannot have projects assigned')
+ end
end
- def either_projects_or_group
- if groups.many?
- errors.add(:runner, 'can only be assigned to one group')
+ def no_groups
+ if groups.any?
+ errors.add(:runner, 'cannot have groups assigned')
end
+ end
+
+ def any_project
+ unless projects.any?
+ errors.add(:runner, 'needs to be assigned to at least one project')
+ end
+ end
- if assigned_to_group? && assigned_to_project?
- errors.add(:runner, 'can only be assigned either to projects or to a group')
+ def exactly_one_group
+ unless groups.one?
+ errors.add(:runner, 'needs to be assigned to exactly one group')
end
end
diff --git a/app/models/ci/runner_namespace.rb b/app/models/ci/runner_namespace.rb
index 3269f86e8ca..29508fdd326 100644
--- a/app/models/ci/runner_namespace.rb
+++ b/app/models/ci/runner_namespace.rb
@@ -2,8 +2,10 @@ module Ci
class RunnerNamespace < ActiveRecord::Base
extend Gitlab::Ci::Model
- belongs_to :runner
- belongs_to :namespace, class_name: '::Namespace'
+ belongs_to :runner, inverse_of: :runner_namespaces, validate: true
+ belongs_to :namespace, inverse_of: :runner_namespaces, class_name: '::Namespace'
belongs_to :group, class_name: '::Group', foreign_key: :namespace_id
+
+ validates :runner_id, uniqueness: { scope: :namespace_id }
end
end
diff --git a/app/models/ci/runner_project.rb b/app/models/ci/runner_project.rb
index 505d178ba8e..52437047300 100644
--- a/app/models/ci/runner_project.rb
+++ b/app/models/ci/runner_project.rb
@@ -2,8 +2,8 @@ module Ci
class RunnerProject < ActiveRecord::Base
extend Gitlab::Ci::Model
- belongs_to :runner
- belongs_to :project
+ belongs_to :runner, inverse_of: :runner_projects
+ belongs_to :project, inverse_of: :runner_projects
validates :runner_id, uniqueness: { scope: :project_id }
end
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
index 5a1eeb966aa..ea07f37e6c1 100644
--- a/app/models/ci/stage.rb
+++ b/app/models/ci/stage.rb
@@ -68,16 +68,44 @@ module Ci
def update_status
retry_optimistic_lock(self) do
case statuses.latest.status
+ when 'created' then nil
when 'pending' then enqueue
when 'running' then run
when 'success' then succeed
when 'failed' then drop
when 'canceled' then cancel
when 'manual' then block
- when 'skipped' then skip
- else skip
+ when 'skipped', nil then skip
+ else
+ raise HasStatus::UnknownStatusError,
+ "Unknown status `#{statuses.latest.status}`"
end
end
end
+
+ def groups
+ @groups ||= Ci::Group.fabricate(self)
+ end
+
+ def has_warnings?
+ number_of_warnings.positive?
+ end
+
+ def number_of_warnings
+ BatchLoader.for(id).batch(default_value: 0) do |stage_ids, loader|
+ ::Ci::Build.where(stage_id: stage_ids)
+ .latest
+ .failed_but_allowed
+ .group(:stage_id)
+ .count
+ .each { |id, amount| loader.call(id, amount) }
+ end
+ end
+
+ def detailed_status(current_user)
+ Gitlab::Ci::Status::Stage::Factory
+ .new(self, current_user)
+ .fabricate!
+ end
end
end
diff --git a/app/models/clusters/applications/jupyter.rb b/app/models/clusters/applications/jupyter.rb
new file mode 100644
index 00000000000..975d434e1a4
--- /dev/null
+++ b/app/models/clusters/applications/jupyter.rb
@@ -0,0 +1,92 @@
+module Clusters
+ module Applications
+ class Jupyter < ActiveRecord::Base
+ VERSION = '0.0.1'.freeze
+
+ self.table_name = 'clusters_applications_jupyter'
+
+ include ::Clusters::Concerns::ApplicationCore
+ include ::Clusters::Concerns::ApplicationStatus
+ include ::Clusters::Concerns::ApplicationData
+
+ belongs_to :oauth_application, class_name: 'Doorkeeper::Application'
+
+ default_value_for :version, VERSION
+
+ def set_initial_status
+ return unless not_installable?
+
+ if cluster&.application_ingress_installed? && cluster.application_ingress.external_ip
+ self.status = 'installable'
+ end
+ end
+
+ def chart
+ "#{name}/jupyterhub"
+ end
+
+ def repository
+ 'https://jupyterhub.github.io/helm-chart/'
+ end
+
+ def values
+ content_values.to_yaml
+ end
+
+ def install_command
+ Gitlab::Kubernetes::Helm::InstallCommand.new(
+ name,
+ chart: chart,
+ values: values,
+ repository: repository
+ )
+ end
+
+ def callback_url
+ "http://#{hostname}/hub/oauth_callback"
+ end
+
+ private
+
+ def specification
+ {
+ "ingress" => {
+ "hosts" => [hostname]
+ },
+ "hub" => {
+ "extraEnv" => {
+ "GITLAB_HOST" => gitlab_url
+ },
+ "cookieSecret" => cookie_secret
+ },
+ "proxy" => {
+ "secretToken" => secret_token
+ },
+ "auth" => {
+ "gitlab" => {
+ "clientId" => oauth_application.uid,
+ "clientSecret" => oauth_application.secret,
+ "callbackUrl" => callback_url
+ }
+ }
+ }
+ end
+
+ def gitlab_url
+ Gitlab.config.gitlab.url
+ end
+
+ def content_values
+ YAML.load_file(chart_values_file).deep_merge!(specification)
+ end
+
+ def secret_token
+ @secret_token ||= SecureRandom.hex(32)
+ end
+
+ def cookie_secret
+ @cookie_secret ||= SecureRandom.hex(32)
+ end
+ end
+ end
+end
diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb
index c702c4ee807..48137c2ed68 100644
--- a/app/models/clusters/applications/prometheus.rb
+++ b/app/models/clusters/applications/prometheus.rb
@@ -3,7 +3,7 @@ module Clusters
class Prometheus < ActiveRecord::Base
include PrometheusAdapter
- VERSION = "2.0.0".freeze
+ VERSION = '6.7.3'.freeze
self.table_name = 'clusters_applications_prometheus'
@@ -37,6 +37,7 @@ module Clusters
Gitlab::Kubernetes::Helm::InstallCommand.new(
name,
chart: chart,
+ version: version,
values: values
)
end
diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb
index b881b4eaf36..e6f795f3e0b 100644
--- a/app/models/clusters/applications/runner.rb
+++ b/app/models/clusters/applications/runner.rb
@@ -43,7 +43,7 @@ module Clusters
def create_and_assign_runner
transaction do
- project.runners.create!(runner_create_params).tap do |runner|
+ Ci::Runner.create!(runner_create_params).tap do |runner|
update!(runner_id: runner.id)
end
end
@@ -53,7 +53,8 @@ module Clusters
{
name: 'kubernetes-cluster',
runner_type: :project_type,
- tag_list: %w(kubernetes cluster)
+ tag_list: %w(kubernetes cluster),
+ projects: [project]
}
end
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index 77947d515c1..b426b1bf8a1 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -8,7 +8,8 @@ module Clusters
Applications::Helm.application_name => Applications::Helm,
Applications::Ingress.application_name => Applications::Ingress,
Applications::Prometheus.application_name => Applications::Prometheus,
- Applications::Runner.application_name => Applications::Runner
+ Applications::Runner.application_name => Applications::Runner,
+ Applications::Jupyter.application_name => Applications::Jupyter
}.freeze
DEFAULT_ENVIRONMENT = '*'.freeze
@@ -26,6 +27,7 @@ module Clusters
has_one :application_ingress, class_name: 'Clusters::Applications::Ingress'
has_one :application_prometheus, class_name: 'Clusters::Applications::Prometheus'
has_one :application_runner, class_name: 'Clusters::Applications::Runner'
+ has_one :application_jupyter, class_name: 'Clusters::Applications::Jupyter'
accepts_nested_attributes_for :provider_gcp, update_only: true
accepts_nested_attributes_for :platform_kubernetes, update_only: true
@@ -39,6 +41,7 @@ module Clusters
delegate :active?, to: :platform_kubernetes, prefix: true, allow_nil: true
delegate :installed?, to: :application_helm, prefix: true, allow_nil: true
+ delegate :installed?, to: :application_ingress, prefix: true, allow_nil: true
enum platform_type: {
kubernetes: 1
@@ -74,7 +77,8 @@ module Clusters
application_helm || build_application_helm,
application_ingress || build_application_ingress,
application_prometheus || build_application_prometheus,
- application_runner || build_application_runner
+ application_runner || build_application_runner,
+ application_jupyter || build_application_jupyter
]
end
diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb
index ba6552f238f..36631d57ad1 100644
--- a/app/models/clusters/platforms/kubernetes.rb
+++ b/app/models/clusters/platforms/kubernetes.rb
@@ -11,12 +11,12 @@ module Clusters
attr_encrypted :password,
mode: :per_attribute_iv,
- key: Gitlab::Application.secrets.db_key_base,
+ key: Settings.attr_encrypted_db_key_base_truncated,
algorithm: 'aes-256-cbc'
attr_encrypted :token,
mode: :per_attribute_iv,
- key: Gitlab::Application.secrets.db_key_base,
+ key: Settings.attr_encrypted_db_key_base_truncated,
algorithm: 'aes-256-cbc'
before_validation :enforce_namespace_to_lower_case
diff --git a/app/models/clusters/providers/gcp.rb b/app/models/clusters/providers/gcp.rb
index 7fac32466ab..4db1bb35c12 100644
--- a/app/models/clusters/providers/gcp.rb
+++ b/app/models/clusters/providers/gcp.rb
@@ -11,7 +11,7 @@ module Clusters
attr_encrypted :access_token,
mode: :per_attribute_iv,
- key: Gitlab::Application.secrets.db_key_base,
+ key: Settings.attr_encrypted_db_key_base_truncated,
algorithm: 'aes-256-cbc'
validates :gcp_project_id,
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index a7d05722287..97516079b66 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -3,6 +3,7 @@ class CommitStatus < ActiveRecord::Base
include Importable
include AfterCommitQueue
include Presentable
+ include EnumWithNil
self.table_name = 'ci_builds'
@@ -39,7 +40,7 @@ class CommitStatus < ActiveRecord::Base
scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) }
scope :after_stage, -> (index) { where('stage_idx > ?', index) }
- enum failure_reason: {
+ enum_with_nil failure_reason: {
unknown_failure: nil,
script_failure: 1,
api_failure: 2,
@@ -190,11 +191,4 @@ class CommitStatus < ActiveRecord::Base
v =~ /\d+/ ? v.to_i : v
end
end
-
- # Rails 5.0 autogenerated question mark enum methods return wrong result if enum value is nil.
- # They always return `false`.
- # This method overwrites the autogenerated one to return correct result.
- def unknown_failure?
- Gitlab.rails5? ? failure_reason.nil? : super
- end
end
diff --git a/app/models/concerns/atomic_internal_id.rb b/app/models/concerns/atomic_internal_id.rb
index 22f516a172f..164c704260e 100644
--- a/app/models/concerns/atomic_internal_id.rb
+++ b/app/models/concerns/atomic_internal_id.rb
@@ -25,9 +25,13 @@ module AtomicInternalId
extend ActiveSupport::Concern
module ClassMethods
- def has_internal_id(column, scope:, init:) # rubocop:disable Naming/PredicateName
- before_validation(on: :create) do
+ def has_internal_id(column, scope:, init:, presence: true) # rubocop:disable Naming/PredicateName
+ before_validation :"ensure_#{scope}_#{column}!", on: :create
+ validates column, presence: presence
+
+ define_method("ensure_#{scope}_#{column}!") do
scope_value = association(scope).reader
+
if read_attribute(column).blank? && scope_value
scope_attrs = { scope_value.class.table_name.singularize.to_sym => scope_value }
usage = self.class.table_name.to_sym
@@ -35,13 +39,9 @@ module AtomicInternalId
new_iid = InternalId.generate_next(self, scope_attrs, usage, init)
write_attribute(column, new_iid)
end
- end
- validates column, presence: true, numericality: true
+ read_attribute(column)
+ end
end
end
-
- def to_param
- iid.to_s
- end
end
diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb
index 13246a774e3..095897b08e3 100644
--- a/app/models/concerns/avatarable.rb
+++ b/app/models/concerns/avatarable.rb
@@ -4,11 +4,14 @@ module Avatarable
included do
prepend ShadowMethods
include ObjectStorage::BackgroundMove
+ include Gitlab::Utils::StrongMemoize
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
mount_uploader :avatar, AvatarUploader
+
+ after_initialize :add_avatar_to_batch
end
module ShadowMethods
@@ -18,6 +21,17 @@ module Avatarable
avatar_path(only_path: args.fetch(:only_path, true)) || super
end
+
+ def retrieve_upload(identifier, paths)
+ upload = retrieve_upload_from_batch(identifier)
+
+ # This fallback is needed when deleting an upload, because we may have
+ # already been removed from the DB. We have to check an explicit `#nil?`
+ # because it's a BatchLoader instance.
+ upload = super if upload.nil?
+
+ upload
+ end
end
def avatar_type
@@ -52,4 +66,37 @@ module Avatarable
url_base + avatar.local_url
end
+
+ # Path that is persisted in the tracking Upload model. Used to fetch the
+ # upload from the model.
+ def upload_paths(identifier)
+ avatar_mounter.blank_uploader.store_dirs.map { |store, path| File.join(path, identifier) }
+ end
+
+ private
+
+ def retrieve_upload_from_batch(identifier)
+ BatchLoader.for(identifier: identifier, model: self).batch(key: self.class) do |upload_params, loader, args|
+ model_class = args[:key]
+ paths = upload_params.flat_map do |params|
+ params[:model].upload_paths(params[:identifier])
+ end
+
+ Upload.where(uploader: AvatarUploader, path: paths).find_each do |upload|
+ model = model_class.instantiate('id' => upload.model_id)
+
+ loader.call({ model: model, identifier: File.basename(upload.path) }, upload)
+ end
+ end
+ end
+
+ def add_avatar_to_batch
+ return unless avatar_mounter
+
+ avatar_mounter.read_identifiers.each { |identifier| retrieve_upload_from_batch(identifier) }
+ end
+
+ def avatar_mounter
+ strong_memoize(:avatar_mounter) { _mounter(:avatar) }
+ end
end
diff --git a/app/models/concerns/batch_destroy_dependent_associations.rb b/app/models/concerns/batch_destroy_dependent_associations.rb
new file mode 100644
index 00000000000..353ee2e73d0
--- /dev/null
+++ b/app/models/concerns/batch_destroy_dependent_associations.rb
@@ -0,0 +1,28 @@
+# Provides a way to work around Rails issue where dependent objects are all
+# loaded into memory before destroyed: https://github.com/rails/rails/issues/22510.
+#
+# This concern allows an ActiveRecord module to destroy all its dependent
+# associations in batches. The idea is borrowed from https://github.com/thisismydesign/batch_dependent_associations.
+#
+# The differences here with that gem:
+#
+# 1. We allow excluding certain associations.
+# 2. We don't need to support delete_all since we can use the EachBatch concern.
+module BatchDestroyDependentAssociations
+ extend ActiveSupport::Concern
+
+ DEPENDENT_ASSOCIATIONS_BATCH_SIZE = 1000
+
+ def dependent_associations_to_destroy
+ self.class.reflect_on_all_associations(:has_many).select { |assoc| assoc.options[:dependent] == :destroy }
+ end
+
+ def destroy_dependent_associations_in_batches(exclude: [])
+ dependent_associations_to_destroy.each do |association|
+ next if exclude.include?(association.name)
+
+ # rubocop:disable GitlabSecurity/PublicSend
+ public_send(association.name).find_each(batch_size: DEPENDENT_ASSOCIATIONS_BATCH_SIZE, &:destroy)
+ end
+ end
+end
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index db8cf322ef7..b05bf909058 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -40,6 +40,18 @@ module CacheMarkdownField
end
end
+ class MarkdownEngine
+ def self.from_version(version = nil)
+ return :common_mark if version.nil? || version == 0
+
+ if version < CacheMarkdownField::CACHE_COMMONMARK_VERSION_START
+ :redcarpet
+ else
+ :common_mark
+ end
+ end
+ end
+
def skip_project_check?
false
end
@@ -57,7 +69,7 @@ module CacheMarkdownField
# Banzai is less strict about authors, so don't always have an author key
context[:author] = self.author if self.respond_to?(:author)
- context[:markdown_engine] = markdown_engine
+ context[:markdown_engine] = MarkdownEngine.from_version(latest_cached_markdown_version)
context
end
@@ -114,7 +126,7 @@ module CacheMarkdownField
end
def latest_cached_markdown_version
- return CacheMarkdownField::CACHE_REDCARPET_VERSION unless cached_markdown_version
+ return CacheMarkdownField::CACHE_COMMONMARK_VERSION unless cached_markdown_version
if cached_markdown_version < CacheMarkdownField::CACHE_COMMONMARK_VERSION_START
CacheMarkdownField::CACHE_REDCARPET_VERSION
@@ -123,14 +135,6 @@ module CacheMarkdownField
end
end
- def markdown_engine
- if latest_cached_markdown_version < CacheMarkdownField::CACHE_COMMONMARK_VERSION_START
- :redcarpet
- else
- :common_mark
- end
- end
-
included do
cattr_reader :cached_markdown_fields do
FieldData.new
diff --git a/app/models/concerns/cacheable_attributes.rb b/app/models/concerns/cacheable_attributes.rb
index b32459fdabf..606549b947f 100644
--- a/app/models/concerns/cacheable_attributes.rb
+++ b/app/models/concerns/cacheable_attributes.rb
@@ -6,15 +6,16 @@ module CacheableAttributes
end
class_methods do
+ def cache_key
+ "#{name}:#{Gitlab::VERSION}:#{Rails.version}".freeze
+ end
+
# Can be overriden
def current_without_cache
last
end
- def cache_key
- "#{name}:#{Gitlab::VERSION}:#{Gitlab.migrations_hash}:json".freeze
- end
-
+ # Can be overriden
def defaults
{}
end
@@ -24,10 +25,18 @@ module CacheableAttributes
end
def cached
- json_attributes = Rails.cache.read(cache_key)
- return nil unless json_attributes.present?
+ if RequestStore.active?
+ RequestStore[:"#{name}_cached_attributes"] ||= retrieve_from_cache
+ else
+ retrieve_from_cache
+ end
+ end
+
+ def retrieve_from_cache
+ record = Rails.cache.read(cache_key)
+ ensure_cache_setup if record.present?
- build_from_defaults(JSON.parse(json_attributes))
+ record
end
def current
@@ -35,7 +44,12 @@ module CacheableAttributes
return cached_record if cached_record.present?
current_without_cache.tap { |current_record| current_record&.cache! }
- rescue
+ rescue => e
+ if Rails.env.production?
+ Rails.logger.warn("Cached record for #{name} couldn't be loaded, falling back to uncached record: #{e}")
+ else
+ raise e
+ end
# Fall back to an uncached value if there are any problems (e.g. Redis down)
current_without_cache
end
@@ -46,9 +60,15 @@ module CacheableAttributes
# Gracefully handle when Redis is not available. For example,
# omnibus may fail here during gitlab:assets:compile.
end
+
+ def ensure_cache_setup
+ # This is a workaround for a Rails bug that causes attribute methods not
+ # to be loaded when read from cache: https://github.com/rails/rails/issues/27348
+ define_attribute_methods
+ end
end
def cache!
- Rails.cache.write(self.class.cache_key, attributes.to_json)
+ Rails.cache.write(self.class.cache_key, self, expires_in: 1.minute)
end
end
diff --git a/app/models/concerns/diff_file.rb b/app/models/concerns/diff_file.rb
new file mode 100644
index 00000000000..72332072012
--- /dev/null
+++ b/app/models/concerns/diff_file.rb
@@ -0,0 +1,9 @@
+module DiffFile
+ extend ActiveSupport::Concern
+
+ def to_hash
+ keys = Gitlab::Git::Diff::SERIALIZE_KEYS - [:diff]
+
+ as_json(only: keys).merge(diff: diff).with_indifferent_access
+ end
+end
diff --git a/app/models/concerns/enum_with_nil.rb b/app/models/concerns/enum_with_nil.rb
new file mode 100644
index 00000000000..6b37903da20
--- /dev/null
+++ b/app/models/concerns/enum_with_nil.rb
@@ -0,0 +1,33 @@
+module EnumWithNil
+ extend ActiveSupport::Concern
+
+ included do
+ def self.enum_with_nil(definitions)
+ # use original `enum` to auto-define all methods
+ enum(definitions)
+
+ # override auto-defined methods only for the
+ # key which uses nil value
+ definitions.each do |name, values|
+ next unless key_with_nil = values.key(nil)
+
+ # E.g. for enum_with_nil failure_reason: { unknown_failure: nil }
+ # this overrides auto-generated method `unknown_failure?`
+ define_method("#{key_with_nil}?") do
+ Gitlab.rails5? ? self[name].nil? : super()
+ end
+
+ # E.g. for enum_with_nil failure_reason: { unknown_failure: nil }
+ # this overrides auto-generated method `failure_reason`
+ define_method(name) do
+ orig = super()
+
+ return orig unless Gitlab.rails5?
+ return orig unless orig.nil?
+
+ self.class.public_send(name.to_s.pluralize).key(nil) # rubocop:disable GitlabSecurity/PublicSend
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/group_descendant.rb b/app/models/concerns/group_descendant.rb
index 261ace57a17..5e9a95c3282 100644
--- a/app/models/concerns/group_descendant.rb
+++ b/app/models/concerns/group_descendant.rb
@@ -44,8 +44,8 @@ module GroupDescendant
This error is not user facing, but causes a +1 query.
MSG
extras = {
- parent: parent,
- child: child,
+ parent: parent.inspect,
+ child: child.inspect,
preloaded: preloaded.map(&:full_path)
}
issue_url = 'https://gitlab.com/gitlab-org/gitlab-ce/issues/40785'
diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb
index 7c3ed96bc28..72c236a0fc7 100644
--- a/app/models/concerns/has_status.rb
+++ b/app/models/concerns/has_status.rb
@@ -11,6 +11,8 @@ module HasStatus
STATUSES_ENUM = { created: 0, pending: 1, running: 2, success: 3,
failed: 4, canceled: 5, skipped: 6, manual: 7 }.freeze
+ UnknownStatusError = Class.new(StandardError)
+
class_methods do
def status_sql
scope_relevant = respond_to?(:exclude_ignored) ? exclude_ignored : all
diff --git a/app/models/concerns/has_variable.rb b/app/models/concerns/has_variable.rb
index 8a241e4374a..c8e20c0ab81 100644
--- a/app/models/concerns/has_variable.rb
+++ b/app/models/concerns/has_variable.rb
@@ -13,7 +13,7 @@ module HasVariable
attr_encrypted :value,
mode: :per_attribute_iv_and_salt,
insecure_mode: true,
- key: Gitlab::Application.secrets.db_key_base,
+ key: Settings.attr_encrypted_db_key_base,
algorithm: 'aes-256-cbc'
def key=(new_key)
diff --git a/app/models/concerns/iid_routes.rb b/app/models/concerns/iid_routes.rb
new file mode 100644
index 00000000000..246748cf52c
--- /dev/null
+++ b/app/models/concerns/iid_routes.rb
@@ -0,0 +1,9 @@
+module IidRoutes
+ ##
+ # This automagically enforces all related routes to use `iid` instead of `id`
+ # If you want to use `iid` for some routes and `id` for other routes, this module should not to be included,
+ # instead you should define `iid` or `id` explictly at each route generators. e.g. pipeline_path(project.id, pipeline.iid)
+ def to_param
+ iid.to_s
+ end
+end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index b45395343cc..b93c1145f82 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -97,8 +97,6 @@ module Issuable
strip_attributes :title
- after_save :ensure_metrics, unless: :imported?
-
# We want to use optimistic lock for cases when only title or description are involved
# http://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html
def locking_enabled?
@@ -109,6 +107,10 @@ module Issuable
false
end
+ def etag_caching_enabled?
+ false
+ end
+
def has_multiple_assignees?
assignees.count > 1
end
diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb
index b3fec99c816..1f7d78a2efe 100644
--- a/app/models/concerns/project_features_compatibility.rb
+++ b/app/models/concerns/project_features_compatibility.rb
@@ -1,4 +1,4 @@
-# Makes api V3 compatible with old project features permissions methods
+# Makes api V4 compatible with old project features permissions methods
#
# After migrating issues_enabled merge_requests_enabled builds_enabled snippets_enabled and wiki_enabled
# fields to a new table "project_features", support for the old fields is still needed in the API.
diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb
index 94eef4ff7cd..dbe8d31de37 100644
--- a/app/models/concerns/protected_ref.rb
+++ b/app/models/concerns/protected_ref.rb
@@ -23,7 +23,7 @@ module ProtectedRef
# If we don't `protected_branch` or `protected_tag` would be empty and
# `project` cannot be delegated to it, which in turn would cause validations
# to fail.
- has_many :"#{type}_access_levels", inverse_of: self.model_name.singular # rubocop:disable Cop/ActiveRecordDependent
+ has_many :"#{type}_access_levels", inverse_of: self.model_name.singular
validates :"#{type}_access_levels", length: { is: 1, message: "are restricted to a single instance per #{self.model_name.human}." }
diff --git a/app/models/concerns/protected_ref_access.rb b/app/models/concerns/protected_ref_access.rb
index bfda5b1678b..71b0c3468b9 100644
--- a/app/models/concerns/protected_ref_access.rb
+++ b/app/models/concerns/protected_ref_access.rb
@@ -2,19 +2,20 @@ module ProtectedRefAccess
extend ActiveSupport::Concern
ALLOWED_ACCESS_LEVELS = [
- Gitlab::Access::MASTER,
+ Gitlab::Access::MAINTAINER,
Gitlab::Access::DEVELOPER,
Gitlab::Access::NO_ACCESS
].freeze
HUMAN_ACCESS_LEVELS = {
- Gitlab::Access::MASTER => "Masters".freeze,
- Gitlab::Access::DEVELOPER => "Developers + Masters".freeze,
+ Gitlab::Access::MAINTAINER => "Maintainers".freeze,
+ Gitlab::Access::DEVELOPER => "Developers + Maintainers".freeze,
Gitlab::Access::NO_ACCESS => "No one".freeze
}.freeze
included do
- scope :master, -> { where(access_level: Gitlab::Access::MASTER) }
+ scope :master, -> { maintainer } # @deprecated
+ scope :maintainer, -> { where(access_level: Gitlab::Access::MAINTAINER) }
scope :developer, -> { where(access_level: Gitlab::Access::DEVELOPER) }
validates :access_level, presence: true, if: :role?, inclusion: {
diff --git a/app/models/concerns/reactive_caching.rb b/app/models/concerns/reactive_caching.rb
index eef9caf1c8e..be0a5b49012 100644
--- a/app/models/concerns/reactive_caching.rb
+++ b/app/models/concerns/reactive_caching.rb
@@ -74,6 +74,7 @@ module ReactiveCaching
def clear_reactive_cache!(*args)
Rails.cache.delete(full_reactive_cache_key(*args))
+ Rails.cache.delete(alive_reactive_cache_key(*args))
end
def exclusively_update_reactive_cache!(*args)
diff --git a/app/models/concerns/redis_cacheable.rb b/app/models/concerns/redis_cacheable.rb
index b5425295130..3bdc1330d23 100644
--- a/app/models/concerns/redis_cacheable.rb
+++ b/app/models/concerns/redis_cacheable.rb
@@ -48,7 +48,7 @@ module RedisCacheable
def cast_value_from_cache(attribute, value)
if Gitlab.rails5?
- self.class.type_for_attribute(attribute).cast(value)
+ self.class.type_for_attribute(attribute.to_s).cast(value)
else
self.class.column_for_attribute(attribute).type_cast_from_database(value)
end
diff --git a/app/models/concerns/resolvable_note.rb b/app/models/concerns/resolvable_note.rb
index 668c5a079e3..4a0f8b92b3a 100644
--- a/app/models/concerns/resolvable_note.rb
+++ b/app/models/concerns/resolvable_note.rb
@@ -32,7 +32,7 @@ module ResolvableNote
# Keep this method in sync with the `potentially_resolvable` scope
def potentially_resolvable?
- RESOLVABLE_TYPES.include?(self.class.name) && noteable.supports_resolvable_notes?
+ RESOLVABLE_TYPES.include?(self.class.name) && noteable&.supports_resolvable_notes?
end
# Keep this method in sync with the `resolvable` scope
diff --git a/app/models/concerns/select_for_project_authorization.rb b/app/models/concerns/select_for_project_authorization.rb
index 58194b0ea13..7af0fdbd618 100644
--- a/app/models/concerns/select_for_project_authorization.rb
+++ b/app/models/concerns/select_for_project_authorization.rb
@@ -6,8 +6,11 @@ module SelectForProjectAuthorization
select("projects.id AS project_id, members.access_level")
end
- def select_as_master_for_project_authorization
- select(["projects.id AS project_id", "#{Gitlab::Access::MASTER} AS access_level"])
+ def select_as_maintainer_for_project_authorization
+ select(["projects.id AS project_id", "#{Gitlab::Access::MAINTAINER} AS access_level"])
end
+
+ # @deprecated
+ alias_method :select_as_master_for_project_authorization, :select_as_maintainer_for_project_authorization
end
end
diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb
index db7254c27e0..cb76ae971d4 100644
--- a/app/models/concerns/sortable.rb
+++ b/app/models/concerns/sortable.rb
@@ -12,8 +12,8 @@ module Sortable
scope :order_created_asc, -> { reorder(created_at: :asc) }
scope :order_updated_desc, -> { reorder(updated_at: :desc) }
scope :order_updated_asc, -> { reorder(updated_at: :asc) }
- scope :order_name_asc, -> { reorder("lower(name) asc") }
- scope :order_name_desc, -> { reorder("lower(name) desc") }
+ scope :order_name_asc, -> { reorder(Arel::Nodes::Ascending.new(arel_table[:name].lower)) }
+ scope :order_name_desc, -> { reorder(Arel::Nodes::Descending.new(arel_table[:name].lower)) }
end
module ClassMethods
diff --git a/app/models/concerns/time_trackable.rb b/app/models/concerns/time_trackable.rb
index 1caf47072bc..0fc321c52bc 100644
--- a/app/models/concerns/time_trackable.rb
+++ b/app/models/concerns/time_trackable.rb
@@ -30,8 +30,6 @@ module TimeTrackable
return if @time_spent == 0
- touch if touchable?
-
if @time_spent == :reset
reset_spent_time
else
@@ -59,10 +57,6 @@ module TimeTrackable
private
- def touchable?
- valid? && persisted?
- end
-
def reset_spent_time
timelogs.new(time_spent: total_time_spent * -1, user: @time_spent_user) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
diff --git a/app/models/concerns/with_uploads.rb b/app/models/concerns/with_uploads.rb
index e7cfffb775b..4245d083a49 100644
--- a/app/models/concerns/with_uploads.rb
+++ b/app/models/concerns/with_uploads.rb
@@ -36,4 +36,8 @@ module WithUploads
upload.destroy
end
end
+
+ def retrieve_upload(_identifier, paths)
+ uploads.find_by(path: paths)
+ end
end
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 254764eefde..ac86e9e8de0 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -1,5 +1,6 @@
class Deployment < ActiveRecord::Base
include AtomicInternalId
+ include IidRoutes
belongs_to :project, required: true
belongs_to :environment, required: true
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index 616a626419b..d752d5bcdee 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -3,6 +3,7 @@
# A note of this type can be resolvable.
class DiffNote < Note
include NoteOnDiff
+ include Gitlab::Utils::StrongMemoize
NOTEABLE_TYPES = %w(MergeRequest Commit).freeze
@@ -12,7 +13,6 @@ class DiffNote < Note
validates :original_position, presence: true
validates :position, presence: true
- validates :diff_line, presence: true, if: :on_text?
validates :line_code, presence: true, line_code: true, if: :on_text?
validates :noteable_type, inclusion: { in: NOTEABLE_TYPES }
validate :positions_complete
@@ -23,6 +23,7 @@ class DiffNote < Note
before_validation :update_position, on: :create, if: :on_text?
before_validation :set_line_code, if: :on_text?
after_save :keep_around_commits
+ after_commit :create_diff_file, on: :create
def discussion_class(*)
DiffDiscussion
@@ -53,21 +54,25 @@ class DiffNote < Note
position.position_type == "image"
end
+ def create_diff_file
+ return unless should_create_diff_file?
+
+ diff_file = fetch_diff_file
+ diff_line = diff_file.line_for_position(self.original_position)
+
+ creation_params = diff_file.diff.to_hash
+ .except(:too_large)
+ .merge(diff: diff_file.diff_hunk(diff_line))
+
+ create_note_diff_file(creation_params)
+ end
+
def diff_file
- @diff_file ||=
- begin
- if created_at_diff?(noteable.diff_refs)
- # We're able to use the already persisted diffs (Postgres) if we're
- # presenting a "current version" of the MR discussion diff.
- # So no need to make an extra Gitaly diff request for it.
- # As an extra benefit, the returned `diff_file` already
- # has `highlighted_diff_lines` data set from Redis on
- # `Diff::FileCollection::MergeRequestDiff`.
- noteable.diffs(paths: original_position.paths, expanded: true).diff_files.first
- else
- original_position.diff_file(self.project.repository)
- end
- end
+ strong_memoize(:diff_file) do
+ enqueue_diff_file_creation_job if should_create_diff_file?
+
+ fetch_diff_file
+ end
end
def diff_line
@@ -98,6 +103,38 @@ class DiffNote < Note
private
+ def enqueue_diff_file_creation_job
+ # Avoid enqueuing multiple file creation jobs at once for a note (i.e.
+ # parallel calls to `DiffNote#diff_file`).
+ lease = Gitlab::ExclusiveLease.new("note_diff_file_creation:#{id}", timeout: 1.hour.to_i)
+ return unless lease.try_obtain
+
+ CreateNoteDiffFileWorker.perform_async(id)
+ end
+
+ def should_create_diff_file?
+ on_text? && note_diff_file.nil? && self == discussion.first_note
+ end
+
+ def fetch_diff_file
+ if note_diff_file
+ diff = Gitlab::Git::Diff.new(note_diff_file.to_hash)
+ Gitlab::Diff::File.new(diff,
+ repository: project.repository,
+ diff_refs: original_position.diff_refs)
+ elsif created_at_diff?(noteable.diff_refs)
+ # We're able to use the already persisted diffs (Postgres) if we're
+ # presenting a "current version" of the MR discussion diff.
+ # So no need to make an extra Gitaly diff request for it.
+ # As an extra benefit, the returned `diff_file` already
+ # has `highlighted_diff_lines` data set from Redis on
+ # `Diff::FileCollection::MergeRequestDiff`.
+ noteable.diffs(paths: original_position.paths, expanded: true).diff_files.first
+ else
+ original_position.diff_file(self.project.repository)
+ end
+ end
+
def supported?
for_commit? || self.noteable.has_complete_diff_refs?
end
diff --git a/app/models/discussion.rb b/app/models/discussion.rb
index 92482a1a875..35a0ef00856 100644
--- a/app/models/discussion.rb
+++ b/app/models/discussion.rb
@@ -17,6 +17,10 @@ class Discussion
to: :first_note
+ def project_id
+ project&.id
+ end
+
def self.build(notes, context_noteable = nil)
notes.first.discussion_class(context_noteable).new(notes, context_noteable)
end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index fddb269af4b..8d523dae324 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -32,7 +32,7 @@ class Environment < ActiveRecord::Base
validates :external_url,
length: { maximum: 255 },
allow_nil: true,
- addressable_url: true
+ url: true
delegate :stop_action, :manual_actions, to: :last_deployment, allow_nil: true
diff --git a/app/models/event.rb b/app/models/event.rb
index 741a84194e2..ac0b1c7b27c 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -40,6 +40,7 @@ class Event < ActiveRecord::Base
).freeze
RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour
+ REPOSITORY_UPDATED_AT_INTERVAL = 5.minutes
delegate :name, :email, :public_email, :username, to: :author, prefix: true, allow_nil: true
delegate :title, to: :issue, prefix: true, allow_nil: true
@@ -391,6 +392,7 @@ class Event < ActiveRecord::Base
def set_last_repository_updated_at
Project.unscoped.where(id: project_id)
+ .where("last_repository_updated_at < ? OR last_repository_updated_at IS NULL", REPOSITORY_UPDATED_AT_INTERVAL.ago)
.update_all(last_repository_updated_at: created_at)
end
diff --git a/app/models/generic_commit_status.rb b/app/models/generic_commit_status.rb
index 532b8f4ad69..5ac8bde44cd 100644
--- a/app/models/generic_commit_status.rb
+++ b/app/models/generic_commit_status.rb
@@ -1,7 +1,7 @@
class GenericCommitStatus < CommitStatus
before_validation :set_default_values
- validates :target_url, addressable_url: true,
+ validates :target_url, url: true,
length: { maximum: 255 },
allow_nil: true
diff --git a/app/models/group.rb b/app/models/group.rb
index 8fb77a7869d..ddebaff50b0 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -11,6 +11,7 @@ class Group < Namespace
include GroupDescendant
include TokenAuthenticatable
include WithUploads
+ include Gitlab::Utils::StrongMemoize
has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
alias_method :members, :group_members
@@ -26,7 +27,11 @@ class Group < Namespace
has_many :milestones
has_many :project_group_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :shared_projects, through: :project_group_links, source: :project
+
+ # Overridden on another method
+ # Left here just to be dependent: :destroy
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'
@@ -88,6 +93,15 @@ class Group < Namespace
end
end
+ # Overrides notification_settings has_many association
+ # This allows to apply notification settings from parent groups
+ # to child groups and projects.
+ def notification_settings
+ source_type = self.class.base_class.name
+
+ NotificationSetting.where(source_type: source_type, source_id: self_and_ancestors_ids)
+ end
+
def to_reference(_from = nil, full: nil)
"#{self.class.reference_prefix}#{full_path}"
end
@@ -141,13 +155,14 @@ class Group < Namespace
)
end
- def add_user(user, access_level, current_user: nil, expires_at: nil)
+ def add_user(user, access_level, current_user: nil, expires_at: nil, ldap: false)
GroupMember.add_user(
self,
user,
access_level,
current_user: current_user,
- expires_at: expires_at
+ expires_at: expires_at,
+ ldap: ldap
)
end
@@ -163,10 +178,13 @@ class Group < Namespace
add_user(user, :developer, current_user: current_user)
end
- def add_master(user, current_user = nil)
- add_user(user, :master, current_user: current_user)
+ def add_maintainer(user, current_user = nil)
+ add_user(user, :maintainer, current_user: current_user)
end
+ # @deprecated
+ alias_method :add_master, :add_maintainer
+
def add_owner(user, current_user = nil)
add_user(user, :owner, current_user: current_user)
end
@@ -183,18 +201,25 @@ class Group < Namespace
members_with_parents.owners.where(user_id: user).any?
end
- def has_master?(user)
+ def has_maintainer?(user)
return false unless user
- members_with_parents.masters.where(user_id: user).any?
+ members_with_parents.maintainers.where(user_id: user).any?
end
+ # @deprecated
+ alias_method :has_master?, :has_maintainer?
+
# Check if user is a last owner of the group.
# Parent owners are ignored for nested groups.
def last_owner?(user)
owners.include?(user) && owners.size == 1
end
+ def ldap_synced?
+ false
+ end
+
def post_create_hook
Gitlab::AppLogger.info("Group \"#{name}\" was created")
@@ -220,6 +245,12 @@ class Group < Namespace
members_with_parents.pluck(:user_id)
end
+ def self_and_ancestors_ids
+ strong_memoize(:self_and_ancestors_ids) do
+ self_and_ancestors.pluck(:id)
+ end
+ end
+
def members_with_parents
# Avoids an unnecessary SELECT when the group has no parents
source_ids =
diff --git a/app/models/hooks/system_hook.rb b/app/models/hooks/system_hook.rb
index 0528266e5b3..6bef00f26ea 100644
--- a/app/models/hooks/system_hook.rb
+++ b/app/models/hooks/system_hook.rb
@@ -11,4 +11,9 @@ class SystemHook < WebHook
default_value_for :push_events, false
default_value_for :repository_update_events, true
default_value_for :merge_requests_events, false
+
+ # Allow urls pointing localhost and the local network
+ def allow_local_requests?
+ true
+ end
end
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index 27729deeac9..e353abdda9c 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -3,7 +3,9 @@ class WebHook < ActiveRecord::Base
has_many :web_hook_logs, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- validates :url, presence: true, url: true
+ validates :url, presence: true, public_url: { allow_localhost: lambda(&:allow_local_requests?),
+ allow_local_network: lambda(&:allow_local_requests?) }
+
validates :token, format: { without: /\n/ }
def execute(data, hook_name)
@@ -13,4 +15,9 @@ class WebHook < ActiveRecord::Base
def async_execute(data, hook_name)
WebHookService.new(self, data, hook_name).async_execute
end
+
+ # Allow urls pointing localhost and the local network
+ def allow_local_requests?
+ false
+ end
end
diff --git a/app/models/hooks/web_hook_log.rb b/app/models/hooks/web_hook_log.rb
index e72c125fb69..59a1f2aed69 100644
--- a/app/models/hooks/web_hook_log.rb
+++ b/app/models/hooks/web_hook_log.rb
@@ -7,6 +7,11 @@ class WebHookLog < ActiveRecord::Base
validates :web_hook, presence: true
+ def self.recent
+ where('created_at >= ?', 2.days.ago.beginning_of_day)
+ .order(created_at: :desc)
+ end
+
def success?
response_status =~ /^2/
end
diff --git a/app/models/import_export_upload.rb b/app/models/import_export_upload.rb
new file mode 100644
index 00000000000..60d53d6c2c8
--- /dev/null
+++ b/app/models/import_export_upload.rb
@@ -0,0 +1,13 @@
+class ImportExportUpload < ActiveRecord::Base
+ include WithUploads
+ include ObjectStorage::BackgroundMove
+
+ belongs_to :project
+
+ mount_uploader :import_file, ImportExportUploader
+ mount_uploader :export_file, ImportExportUploader
+
+ def retrieve_upload(_identifier, paths)
+ Upload.find_by(model: self, path: paths)
+ end
+end
diff --git a/app/models/internal_id.rb b/app/models/internal_id.rb
index 189942c5ad8..f50f28deffe 100644
--- a/app/models/internal_id.rb
+++ b/app/models/internal_id.rb
@@ -14,7 +14,7 @@ class InternalId < ActiveRecord::Base
belongs_to :project
belongs_to :namespace
- enum usage: { issues: 0, merge_requests: 1, deployments: 2, milestones: 3, epics: 4 }
+ enum usage: { issues: 0, merge_requests: 1, deployments: 2, milestones: 3, epics: 4, ci_pipelines: 5 }
validates :usage, presence: true
@@ -24,12 +24,9 @@ class InternalId < ActiveRecord::Base
#
# The operation locks the record and gathers a `ROW SHARE` lock (in PostgreSQL).
# As such, the increment is atomic and safe to be called concurrently.
- #
- # If a `maximum_iid` is passed in, this overrides the incremented value if it's
- # greater than that. This can be used to correct the increment value if necessary.
- def increment_and_save!(maximum_iid)
+ def increment_and_save!
lock!
- self.last_value = [(last_value || 0) + 1, (maximum_iid || 0) + 1].max
+ self.last_value = (last_value || 0) + 1
save!
last_value
end
@@ -93,16 +90,7 @@ class InternalId < ActiveRecord::Base
# and increment its last value
#
# Note this will acquire a ROW SHARE lock on the InternalId record
-
- # Note we always calculate the maximum iid present here and
- # pass it in to correct the InternalId entry if it's last_value is off.
- #
- # This can happen in a transition phase where both `AtomicInternalId` and
- # `NonatomicInternalId` code runs (e.g. during a deploy).
- #
- # This is subject to be cleaned up with the 10.8 release:
- # https://gitlab.com/gitlab-org/gitlab-ce/issues/45389.
- (lookup || create_record).increment_and_save!(maximum_iid)
+ (lookup || create_record).increment_and_save!
end
end
@@ -128,15 +116,11 @@ class InternalId < ActiveRecord::Base
InternalId.create!(
**scope,
usage: usage_value,
- last_value: maximum_iid
+ last_value: init.call(subject) || 0
)
end
rescue ActiveRecord::RecordNotUnique
lookup
end
-
- def maximum_iid
- @maximum_iid ||= init.call(subject) || 0
- end
end
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 0332bfa9371..4715d942c8d 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -2,6 +2,7 @@ require 'carrierwave/orm/activerecord'
class Issue < ActiveRecord::Base
include AtomicInternalId
+ include IidRoutes
include Issuable
include Noteable
include Referable
@@ -14,12 +15,13 @@ class Issue < ActiveRecord::Base
ignore_column :assignee_id, :branch_name, :deleted_at
- DueDateStruct = Struct.new(:title, :name).freeze
- NoDueDate = DueDateStruct.new('No Due Date', '0').freeze
- AnyDueDate = DueDateStruct.new('Any Due Date', '').freeze
- Overdue = DueDateStruct.new('Overdue', 'overdue').freeze
- DueThisWeek = DueDateStruct.new('Due This Week', 'week').freeze
- DueThisMonth = DueDateStruct.new('Due This Month', 'month').freeze
+ DueDateStruct = Struct.new(:title, :name).freeze
+ NoDueDate = DueDateStruct.new('No Due Date', '0').freeze
+ AnyDueDate = DueDateStruct.new('Any Due Date', '').freeze
+ Overdue = DueDateStruct.new('Overdue', 'overdue').freeze
+ DueThisWeek = DueDateStruct.new('Due This Week', 'week').freeze
+ DueThisMonth = DueDateStruct.new('Due This Month', 'month').freeze
+ DueNextMonthAndPreviousTwoWeeks = DueDateStruct.new('Due Next Month And Previous Two Weeks', 'next_month_and_previous_two_weeks').freeze
belongs_to :project
belongs_to :moved_to, class_name: 'Issue'
@@ -46,6 +48,7 @@ class Issue < ActiveRecord::Base
scope :unassigned, -> { where('NOT EXISTS (SELECT TRUE FROM issue_assignees WHERE issue_id = issues.id)') }
scope :assigned_to, ->(u) { where('EXISTS (SELECT TRUE FROM issue_assignees WHERE user_id = ? AND issue_id = issues.id)', u.id)}
+ scope :with_due_date, -> { where.not(due_date: nil) }
scope :without_due_date, -> { where(due_date: nil) }
scope :due_before, ->(date) { where('issues.due_date < ?', date) }
scope :due_between, ->(from_date, to_date) { where('issues.due_date >= ?', from_date).where('issues.due_date <= ?', to_date) }
@@ -53,12 +56,14 @@ class Issue < ActiveRecord::Base
scope :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') }
scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') }
+ scope :order_closest_future_date, -> { reorder('CASE WHEN issues.due_date >= CURRENT_DATE THEN 0 ELSE 1 END ASC, ABS(CURRENT_DATE - issues.due_date) ASC') }
scope :preload_associations, -> { preload(:labels, project: :namespace) }
scope :public_only, -> { where(confidential: false) }
after_save :expire_etag_cache
+ after_save :ensure_metrics, unless: :imported?
attr_spammable :title, spam_title: true
attr_spammable :description, spam_description: true
@@ -119,6 +124,7 @@ class Issue < ActiveRecord::Base
def self.sort_by_attribute(method, excluded_labels: [])
case method.to_s
+ when 'closest_future_date' then order_closest_future_date
when 'due_date' then order_due_date_asc
when 'due_date_asc' then order_due_date_asc
when 'due_date_desc' then order_due_date_desc
@@ -302,6 +308,10 @@ class Issue < ActiveRecord::Base
end
end
+ def etag_caching_enabled?
+ true
+ end
+
def discussions_rendered_on_frontend?
true
end
diff --git a/app/models/label.rb b/app/models/label.rb
index de7f1d56c64..7bbcaa121ca 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -85,11 +85,16 @@ class Label < ActiveRecord::Base
(#{Project.reference_pattern})?
#{Regexp.escape(reference_prefix)}
(?:
- (?<label_id>\d+(?!\S\w)\b) | # Integer-based label ID, or
- (?<label_name>
- [A-Za-z0-9_\-\?\.&]+ | # String-based single-word label title, or
- ".+?" # String-based multi-word label surrounded in quotes
- )
+ (?<label_id>\d+(?!\S\w)\b)
+ | # Integer-based label ID, or
+ (?<label_name>
+ # String-based single-word label title, or
+ [A-Za-z0-9_\-\?\.&]+
+ (?<!\.|\?)
+ |
+ # String-based multi-word label surrounded in quotes
+ ".+?"
+ )
)
}x
end
@@ -137,6 +142,10 @@ class Label < ActiveRecord::Base
priority.try(:priority)
end
+ def priority?
+ priorities.present?
+ end
+
def template?
template
end
diff --git a/app/models/list.rb b/app/models/list.rb
index 5daf35ef845..4edcfa78835 100644
--- a/app/models/list.rb
+++ b/app/models/list.rb
@@ -2,17 +2,27 @@ class List < ActiveRecord::Base
belongs_to :board
belongs_to :label
- enum list_type: { backlog: 0, label: 1, closed: 2 }
+ enum list_type: { backlog: 0, label: 1, closed: 2, assignee: 3 }
validates :board, :list_type, presence: true
validates :label, :position, presence: true, if: :label?
validates :label_id, uniqueness: { scope: :board_id }, if: :label?
- validates :position, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, if: :label?
+ validates :position, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, if: :movable?
before_destroy :can_be_destroyed
- scope :destroyable, -> { where(list_type: list_types[:label]) }
- scope :movable, -> { where(list_type: list_types[:label]) }
+ scope :destroyable, -> { where(list_type: list_types.slice(*destroyable_types).values) }
+ scope :movable, -> { where(list_type: list_types.slice(*movable_types).values) }
+
+ class << self
+ def destroyable_types
+ [:label]
+ end
+
+ def movable_types
+ [:label]
+ end
+ end
def destroyable?
label?
diff --git a/app/models/member.rb b/app/models/member.rb
index 68572f2e33a..00a13a279a9 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -69,9 +69,11 @@ class Member < ActiveRecord::Base
scope :guests, -> { active.where(access_level: GUEST) }
scope :reporters, -> { active.where(access_level: REPORTER) }
scope :developers, -> { active.where(access_level: DEVELOPER) }
- scope :masters, -> { active.where(access_level: MASTER) }
+ scope :maintainers, -> { active.where(access_level: MAINTAINER) }
+ scope :masters, -> { maintainers } # @deprecated
scope :owners, -> { active.where(access_level: OWNER) }
- scope :owners_and_masters, -> { active.where(access_level: [OWNER, MASTER]) }
+ scope :owners_and_maintainers, -> { active.where(access_level: [OWNER, MAINTAINER]) }
+ scope :owners_and_masters, -> { owners_and_maintainers } # @deprecated
scope :order_name_asc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'ASC')) }
scope :order_name_desc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'DESC')) }
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 024106056b4..4f27d0aeaf8 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -17,19 +17,19 @@ class ProjectMember < Member
# Add users to projects with passed access option
#
# access can be an integer representing a access code
- # or symbol like :master representing role
+ # or symbol like :maintainer representing role
#
# Ex.
# add_users_to_projects(
# project_ids,
# user_ids,
- # ProjectMember::MASTER
+ # ProjectMember::MAINTAINER
# )
#
# add_users_to_projects(
# project_ids,
# user_ids,
- # :master
+ # :maintainer
# )
#
def add_users_to_projects(project_ids, users, access_level, current_user: nil, expires_at: nil)
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 628c61d5d69..b4090fd8baf 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -1,5 +1,6 @@
class MergeRequest < ActiveRecord::Base
include AtomicInternalId
+ include IidRoutes
include Issuable
include Noteable
include Referable
@@ -58,6 +59,7 @@ class MergeRequest < ActiveRecord::Base
after_create :ensure_merge_request_diff, unless: :importing?
after_update :clear_memoized_shas
after_update :reload_diff_if_branch_changed
+ after_save :ensure_metrics
# When this attribute is true some MR validation is ignored
# It allows us to close or modify broken merge requests
@@ -104,24 +106,37 @@ class MergeRequest < ActiveRecord::Base
state_machine :merge_status, initial: :unchecked do
event :mark_as_unchecked do
- transition [:can_be_merged, :cannot_be_merged] => :unchecked
+ transition [:can_be_merged, :unchecked] => :unchecked
+ transition [:cannot_be_merged, :cannot_be_merged_recheck] => :cannot_be_merged_recheck
end
event :mark_as_mergeable do
- transition [:unchecked, :cannot_be_merged] => :can_be_merged
+ transition [:unchecked, :cannot_be_merged_recheck] => :can_be_merged
end
event :mark_as_unmergeable do
- transition [:unchecked, :can_be_merged] => :cannot_be_merged
+ transition [:unchecked, :cannot_be_merged_recheck] => :cannot_be_merged
end
state :unchecked
+ state :cannot_be_merged_recheck
state :can_be_merged
state :cannot_be_merged
around_transition do |merge_request, transition, block|
Gitlab::Timeless.timeless(merge_request, &block)
end
+
+ after_transition unchecked: :cannot_be_merged do |merge_request, transition|
+ if merge_request.notify_conflict?
+ NotificationService.new.merge_request_unmergeable(merge_request)
+ TodoService.new.merge_request_became_unmergeable(merge_request)
+ end
+ end
+
+ def check_state?(merge_status)
+ [:unchecked, :cannot_be_merged_recheck].include?(merge_status.to_sym)
+ end
end
validates :source_project, presence: true, unless: [:allow_broken, :importing?, :closed_without_fork?]
@@ -327,6 +342,16 @@ class MergeRequest < ActiveRecord::Base
update_column(:merge_jid, jid)
end
+ def merge_participants
+ participants = [author]
+
+ if merge_when_pipeline_succeeds? && !participants.include?(merge_user)
+ participants << merge_user
+ end
+
+ participants
+ end
+
def first_commit
merge_request_diff ? merge_request_diff.first_commit : compare_commits.first
end
@@ -346,6 +371,10 @@ class MergeRequest < ActiveRecord::Base
end
end
+ def non_latest_diffs
+ merge_request_diffs.where.not(id: merge_request_diff.id)
+ end
+
def diff_size
# Calling `merge_request_diff.diffs.real_size` will also perform
# highlighting, which we don't need here.
@@ -587,22 +616,11 @@ class MergeRequest < ActiveRecord::Base
def reload_diff(current_user = nil)
return unless open?
- old_diff_refs = self.diff_refs
- new_diff = create_merge_request_diff
-
- MergeRequests::MergeRequestDiffCacheService.new.execute(self, new_diff)
-
- new_diff_refs = self.diff_refs
-
- update_diff_discussion_positions(
- old_diff_refs: old_diff_refs,
- new_diff_refs: new_diff_refs,
- current_user: current_user
- )
+ MergeRequests::ReloadDiffsService.new(self, current_user).execute
end
def check_if_can_be_merged
- return unless unchecked? && Gitlab::Database.read_write?
+ return unless self.class.state_machines[:merge_status].check_state?(merge_status) && Gitlab::Database.read_write?
can_be_merged =
!broken? && project.repository.can_be_merged?(diff_head_sha, target_branch)
@@ -683,6 +701,17 @@ class MergeRequest < ActiveRecord::Base
should_remove_source_branch? || force_remove_source_branch?
end
+ def notify_conflict?
+ (opened? || locked?) &&
+ has_commits? &&
+ !branch_missing? &&
+ !project.repository.can_be_merged?(diff_head_sha, target_branch)
+ rescue Gitlab::Git::CommandError
+ # Checking mergeability can trigger exception, e.g. non-utf8
+ # We ignore this type of errors.
+ false
+ end
+
def related_notes
# Fetch comments only from last 100 commits
commits_for_notes_limit = 100
@@ -1092,6 +1121,10 @@ class MergeRequest < ActiveRecord::Base
true
end
+ def discussions_rendered_on_frontend?
+ true
+ end
+
def update_project_counter_caches
Projects::OpenMergeRequestsCountService.new(target_project).refresh_cache
end
@@ -1102,21 +1135,31 @@ class MergeRequest < ActiveRecord::Base
project.merge_requests.merged.where(author_id: author_id).empty?
end
- def allow_maintainer_to_push
- maintainer_push_possible? && super
+ # TODO: remove once production database rename completes
+ alias_attribute :allow_collaboration, :allow_maintainer_to_push
+
+ def allow_collaboration
+ collaborative_push_possible? && allow_maintainer_to_push
end
- alias_method :allow_maintainer_to_push?, :allow_maintainer_to_push
+ alias_method :allow_collaboration?, :allow_collaboration
- def maintainer_push_possible?
+ def collaborative_push_possible?
source_project.present? && for_fork? &&
target_project.visibility_level > Gitlab::VisibilityLevel::PRIVATE &&
source_project.visibility_level > Gitlab::VisibilityLevel::PRIVATE &&
!ProtectedBranch.protected?(source_project, source_branch)
end
- def can_allow_maintainer_to_push?(user)
- maintainer_push_possible? &&
+ def can_allow_collaboration?(user)
+ collaborative_push_possible? &&
Ability.allowed?(user, :push_code, source_project)
end
+
+ def squash_in_progress?
+ # The source project can be deleted
+ return false unless source_project
+
+ source_project.repository.squash_in_progress?(id)
+ end
end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 06aa67c600f..3d72c447b4b 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -3,6 +3,7 @@ class MergeRequestDiff < ActiveRecord::Base
include Importable
include ManualInverseAssociation
include IgnorableColumn
+ include EachBatch
# Don't display more than 100 commits at once
COMMITS_SAFE_SIZE = 100
@@ -17,8 +18,14 @@ class MergeRequestDiff < ActiveRecord::Base
has_many :merge_request_diff_commits, -> { order(:merge_request_diff_id, :relative_order) }
state_machine :state, initial: :empty do
+ event :clean do
+ transition any => :without_files
+ end
+
state :collected
state :overflow
+ # Diff files have been deleted by the system
+ state :without_files
# Deprecated states: these are no longer used but these values may still occur
# in the database.
state :timeout
@@ -27,6 +34,7 @@ class MergeRequestDiff < ActiveRecord::Base
state :overflow_diff_lines_limit
end
+ scope :with_files, -> { without_states(:without_files, :empty) }
scope :viewable, -> { without_state(:empty) }
scope :by_commit_sha, ->(sha) do
joins(:merge_request_diff_commits).where(merge_request_diff_commits: { sha: sha }).reorder(nil)
@@ -42,6 +50,10 @@ 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 viewable?
+ collected? || without_files? || overflow?
+ end
+
# Collect information about commits and diff from repository
# and save it to the database as serialized data
def save_git_content
@@ -170,6 +182,21 @@ class MergeRequestDiff < ActiveRecord::Base
end
def diffs(diff_options = nil)
+ if without_files? && comparison = diff_refs.compare_in(project)
+ # It should fetch the repository when diffs are cleaned by the system.
+ # We don't keep these for storage overload purposes.
+ # See https://gitlab.com/gitlab-org/gitlab-ce/issues/37639
+ comparison.diffs(diff_options)
+ else
+ diffs_collection(diff_options)
+ end
+ end
+
+ # Should always return the DB persisted diffs collection
+ # (e.g. Gitlab::Diff::FileCollection::MergeRequestDiff.
+ # It's useful when trying to invalidate old caches through
+ # FileCollection::MergeRequestDiff#clear_cache!
+ def diffs_collection(diff_options = nil)
Gitlab::Diff::FileCollection::MergeRequestDiff.new(self, diff_options: diff_options)
end
diff --git a/app/models/merge_request_diff_file.rb b/app/models/merge_request_diff_file.rb
index 1199ff5af22..cd8ba6b904d 100644
--- a/app/models/merge_request_diff_file.rb
+++ b/app/models/merge_request_diff_file.rb
@@ -1,5 +1,6 @@
class MergeRequestDiffFile < ActiveRecord::Base
include Gitlab::EncodingHelper
+ include DiffFile
belongs_to :merge_request_diff
@@ -12,10 +13,4 @@ class MergeRequestDiffFile < ActiveRecord::Base
def diff
binary? ? super.unpack('m0').first : super
end
-
- def to_hash
- keys = Gitlab::Git::Diff::SERIALIZE_KEYS - [:diff]
-
- as_json(only: keys).merge(diff: diff).with_indifferent_access
- end
end
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index d14e3a4ded5..14cc12b38a5 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -9,6 +9,7 @@ class Milestone < ActiveRecord::Base
include CacheMarkdownField
include AtomicInternalId
+ include IidRoutes
include Sortable
include Referable
include StripAttribute
@@ -130,9 +131,10 @@ class Milestone < ActiveRecord::Base
rel.order(:project_id, :due_date).select('DISTINCT ON (project_id) id')
else
rel
- .group(:project_id)
+ .group(:project_id, :due_date, :id)
.having('due_date = MIN(due_date)')
.pluck(:id, :project_id, :due_date)
+ .uniq(&:second)
.map(&:first)
end
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 3dad4277713..7034c633268 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -21,7 +21,7 @@ class Namespace < ActiveRecord::Base
has_many :projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :project_statistics
- has_many :runner_namespaces, class_name: 'Ci::RunnerNamespace'
+ has_many :runner_namespaces, inverse_of: :namespace, class_name: 'Ci::RunnerNamespace'
has_many :runners, through: :runner_namespaces, source: :runner, class_name: 'Ci::Runner'
# This should _not_ be `inverse_of: :namespace`, because that would also set
@@ -228,6 +228,10 @@ class Namespace < ActiveRecord::Base
parent.present?
end
+ def root_ancestor
+ ancestors.reorder(nil).find_by(parent_id: nil)
+ end
+
def subgroup?
has_parent?
end
diff --git a/app/models/network/commit.rb b/app/models/network/commit.rb
index 22d48c9e661..d667948deae 100644
--- a/app/models/network/commit.rb
+++ b/app/models/network/commit.rb
@@ -11,8 +11,8 @@ module Network
@parent_spaces = []
end
- def method_missing(m, *args, &block)
- @commit.__send__(m, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
+ def method_missing(msg, *args, &block)
+ @commit.__send__(msg, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
end
def space
diff --git a/app/models/note.rb b/app/models/note.rb
index 109405d3f17..abc40d9016e 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -63,6 +63,7 @@ class Note < ActiveRecord::Base
has_many :todos
has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :system_note_metadata
+ has_one :note_diff_file, inverse_of: :diff_note, foreign_key: :diff_note_id
delegate :gfm_reference, :local_reference, to: :noteable
delegate :name, to: :project, prefix: true
@@ -100,7 +101,8 @@ class Note < ActiveRecord::Base
scope :inc_author_project, -> { includes(:project, :author) }
scope :inc_author, -> { includes(:author) }
scope :inc_relations_for_view, -> do
- includes(:project, :author, :updated_by, :resolved_by, :award_emoji, :system_note_metadata)
+ includes(:project, :author, :updated_by, :resolved_by, :award_emoji,
+ :system_note_metadata, :note_diff_file)
end
scope :diff_notes, -> { where(type: %w(LegacyDiffNote DiffNote)) }
@@ -382,6 +384,7 @@ class Note < ActiveRecord::Base
def expire_etag_cache
return unless noteable&.discussions_rendered_on_frontend?
+ return unless noteable&.etag_caching_enabled?
Gitlab::EtagCaching::Store.new.touch(etag_key)
end
@@ -433,6 +436,10 @@ class Note < ActiveRecord::Base
super.merge(noteable: noteable)
end
+ def retrieve_upload(_identifier, paths)
+ Upload.find_by(model: self, path: paths)
+ end
+
private
def keep_around_commit
diff --git a/app/models/note_diff_file.rb b/app/models/note_diff_file.rb
new file mode 100644
index 00000000000..e688018a6d9
--- /dev/null
+++ b/app/models/note_diff_file.rb
@@ -0,0 +1,7 @@
+class NoteDiffFile < ActiveRecord::Base
+ include DiffFile
+
+ belongs_to :diff_note, inverse_of: :note_diff_file
+
+ validates :diff_note, presence: true
+end
diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb
index 2c3580bbdc6..1a03dd9df56 100644
--- a/app/models/notification_recipient.rb
+++ b/app/models/notification_recipient.rb
@@ -1,4 +1,6 @@
class NotificationRecipient
+ include Gitlab::Utils::StrongMemoize
+
attr_reader :user, :type, :reason
def initialize(user, type, **opts)
unless NotificationSetting.levels.key?(type) || type == :subscription
@@ -64,7 +66,7 @@ class NotificationRecipient
return false unless @target
return false unless @target.respond_to?(:subscriptions)
- subscription = @target.subscriptions.find_by_user_id(@user.id)
+ subscription = @target.subscriptions.find { |subscription| subscription.user_id == @user.id }
subscription && !subscription.subscribed
end
@@ -142,10 +144,33 @@ class NotificationRecipient
return project_setting unless project_setting.nil? || project_setting.global?
- group_setting = @group && user.notification_settings_for(@group)
+ group_setting = closest_non_global_group_notification_settting
- return group_setting unless group_setting.nil? || group_setting.global?
+ return group_setting unless group_setting.nil?
user.global_notification_setting
end
+
+ # Returns the notificaton_setting of the lowest group in hierarchy with non global level
+ def closest_non_global_group_notification_settting
+ return unless @group
+ return if indexed_group_notification_settings.empty?
+
+ notification_setting = nil
+
+ @group.self_and_ancestors_ids.each do |id|
+ notification_setting = indexed_group_notification_settings[id]
+ break if notification_setting
+ end
+
+ notification_setting
+ end
+
+ def indexed_group_notification_settings
+ strong_memoize(:indexed_group_notification_settings) do
+ @group.notification_settings.where(user_id: user.id)
+ .where.not(level: NotificationSetting.levels[:global])
+ .index_by(&:source_id)
+ end
+ end
end
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
index 2e478a24778..bfea64c3759 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -19,7 +19,7 @@ class PagesDomain < ActiveRecord::Base
attr_encrypted :key,
mode: :per_attribute_iv_and_salt,
insecure_mode: true,
- key: Gitlab::Application.secrets.db_key_base,
+ key: Settings.attr_encrypted_db_key_base,
algorithm: 'aes-256-cbc'
after_initialize :set_verification_code
diff --git a/app/models/personal_snippet.rb b/app/models/personal_snippet.rb
index 82c1c4de3a0..355624fd552 100644
--- a/app/models/personal_snippet.rb
+++ b/app/models/personal_snippet.rb
@@ -1,2 +1,3 @@
class PersonalSnippet < Snippet
+ include WithUploads
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 0e727664d39..1894de6ceed 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -24,6 +24,8 @@ class Project < ActiveRecord::Base
include ChronicDurationAttribute
include FastDestroyAll::Helpers
include WithUploads
+ include BatchDestroyDependentAssociations
+ extend Gitlab::Cache::RequestCache
extend Gitlab::ConfigHelper
@@ -67,7 +69,7 @@ class Project < ActiveRecord::Base
add_authentication_token_field :runners_token
- before_validation :mark_remote_mirrors_for_removal, if: -> { ActiveRecord::Base.connection.table_exists?(:remote_mirrors) }
+ before_validation :mark_remote_mirrors_for_removal, if: -> { RemoteMirror.table_exists? }
before_save :ensure_runners_token
@@ -169,6 +171,7 @@ class Project < ActiveRecord::Base
has_one :fork_network, through: :fork_network_member
has_one :import_state, autosave: true, class_name: 'ProjectImportState', inverse_of: :project
+ has_one :import_export_upload, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
# Merge Requests for target project should be removed with it
has_many :merge_requests, foreign_key: 'target_project_id'
@@ -227,6 +230,7 @@ class Project < ActiveRecord::Base
has_many :commit_statuses
has_many :pipelines, class_name: 'Ci::Pipeline', inverse_of: :project
+ has_many :stages, class_name: 'Ci::Stage', inverse_of: :project
# Ci::Build objects store data on the file system such as artifact files and
# build traces. Currently there's no efficient way of removing this data in
@@ -235,7 +239,7 @@ class Project < ActiveRecord::Base
has_many :builds, class_name: 'Ci::Build', inverse_of: :project, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :build_trace_section_names, class_name: 'Ci::BuildTraceSectionName'
has_many :build_trace_chunks, class_name: 'Ci::BuildTraceChunk', through: :builds, source: :trace_chunks
- has_many :runner_projects, class_name: 'Ci::RunnerProject'
+ has_many :runner_projects, class_name: 'Ci::RunnerProject', inverse_of: :project
has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
has_many :variables, class_name: 'Ci::Variable'
has_many :triggers, class_name: 'Ci::Trigger'
@@ -265,7 +269,8 @@ class Project < ActiveRecord::Base
delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :members, to: :team, prefix: true
delegate :add_user, :add_users, to: :team
- delegate :add_guest, :add_reporter, :add_developer, :add_master, :add_role, to: :team
+ delegate :add_guest, :add_reporter, :add_developer, :add_maintainer, :add_role, to: :team
+ delegate :add_master, to: :team # @deprecated
delegate :group_runners_enabled, :group_runners_enabled=, :group_runners_enabled?, to: :ci_cd_settings
# Validations
@@ -288,8 +293,10 @@ class Project < ActiveRecord::Base
validates :namespace, presence: true
validates :name, uniqueness: { scope: :namespace_id }
- validates :import_url, addressable_url: true, if: :external_import?
- validates :import_url, importable_url: true, if: [:external_import?, :import_url_changed?]
+ validates :import_url, url: { protocols: %w(http https ssh git),
+ allow_localhost: false,
+ enforce_user: true,
+ ports: VALID_IMPORT_PORTS }, if: [:external_import?, :import_url_changed?]
validates :star_count, numericality: { greater_than_or_equal_to: 0 }
validate :check_limit, on: :create
validate :check_repository_path_availability, on: :update, if: ->(project) { project.renamed? }
@@ -672,6 +679,12 @@ class Project < ActiveRecord::Base
end
end
+ def human_import_status_name
+ ensure_import_state
+
+ import_state.human_status_name
+ end
+
def import_schedule
ensure_import_state(force: true)
@@ -1411,7 +1424,7 @@ class Project < ActiveRecord::Base
end
def shared_runners
- @shared_runners ||= shared_runners_available? ? Ci::Runner.shared : Ci::Runner.none
+ @shared_runners ||= shared_runners_available? ? Ci::Runner.instance_type : Ci::Runner.none
end
def group_runners
@@ -1423,16 +1436,22 @@ class Project < ActiveRecord::Base
Ci::Runner.from("(#{union.to_sql}) ci_runners")
end
+ def active_runners
+ strong_memoize(:active_runners) do
+ all_runners.active
+ end
+ end
+
def any_runners?(&block)
- all_runners.active.any?(&block)
+ active_runners.any?(&block)
end
def valid_runners_token?(token)
self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
end
- def open_issues_count
- Projects::OpenIssuesCountService.new(self).count
+ def open_issues_count(current_user = nil)
+ Projects::OpenIssuesCountService.new(self, current_user).count
end
def open_merge_requests_count
@@ -1600,6 +1619,7 @@ class Project < ActiveRecord::Base
def after_import
repository.after_import
+ wiki.repository.after_import
import_finish
remove_import_jid
update_project_counter_caches
@@ -1628,10 +1648,10 @@ class Project < ActiveRecord::Base
params = {
name: default_branch,
push_access_levels_attributes: [{
- access_level: Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_PUSH ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
+ access_level: Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_PUSH ? Gitlab::Access::DEVELOPER : Gitlab::Access::MAINTAINER
}],
merge_access_levels_attributes: [{
- access_level: Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
+ access_level: Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE ? Gitlab::Access::DEVELOPER : Gitlab::Access::MAINTAINER
}]
}
@@ -1647,12 +1667,6 @@ class Project < ActiveRecord::Base
import_state.update_column(:jid, nil)
end
- def running_or_pending_build_count(force: false)
- Rails.cache.fetch(['projects', id, 'running_or_pending_build_count'], force: force) do
- builds.running_or_pending.count(:all)
- end
- end
-
# Lazy loading of the `pipeline_status` attribute
def pipeline_status
@pipeline_status ||= Gitlab::Cache::Ci::ProjectPipelineStatus.load_for_project(self)
@@ -1700,7 +1714,7 @@ class Project < ActiveRecord::Base
:started
elsif after_export_in_progress?
:after_export_action
- elsif export_project_path
+ elsif export_project_path || export_project_object_exists?
:finished
else
:none
@@ -1715,16 +1729,21 @@ class Project < ActiveRecord::Base
import_export_shared.after_export_in_progress?
end
- def remove_exports
- return nil unless export_path.present?
-
- FileUtils.rm_rf(export_path)
+ def remove_exports(path = export_path)
+ if path.present?
+ FileUtils.rm_rf(path)
+ elsif export_project_object_exists?
+ import_export_upload.remove_export_file!
+ import_export_upload.save
+ end
end
def remove_exported_project_file
- return unless export_project_path.present?
+ remove_exports(export_project_path)
+ end
- FileUtils.rm_f(export_project_path)
+ def export_project_object_exists?
+ Gitlab::ImportExport.object_storage? && import_export_upload&.export_file&.file
end
def full_path_slug
@@ -1762,6 +1781,15 @@ class Project < ActiveRecord::Base
end
end
+ def default_environment
+ production_first = "(CASE WHEN name = 'production' THEN 0 ELSE 1 END), id ASC"
+
+ environments
+ .with_state(:available)
+ .reorder(production_first)
+ .first
+ end
+
def secret_variables_for(ref:, environment: nil)
# EE would use the environment
if protected_for?(ref)
@@ -1972,18 +2000,18 @@ class Project < ActiveRecord::Base
.limit(1)
.select(1)
source_of_merge_requests.opened
- .where(allow_maintainer_to_push: true)
+ .where(allow_collaboration: true)
.where('EXISTS (?)', developer_access_exists)
end
- def branch_allows_maintainer_push?(user, branch_name)
+ def branch_allows_collaboration?(user, branch_name)
return false unless user
cache_key = "user:#{user.id}:#{branch_name}:branch_allows_push"
- memoized_results = strong_memoize(:branch_allows_maintainer_push) do
+ memoized_results = strong_memoize(:branch_allows_collaboration) do
Hash.new do |result, cache_key|
- result[cache_key] = fetch_branch_allows_maintainer_push?(user, branch_name)
+ result[cache_key] = fetch_branch_allows_collaboration?(user, branch_name)
end
end
@@ -2002,6 +2030,15 @@ class Project < ActiveRecord::Base
@gitlab_deploy_token ||= deploy_tokens.gitlab_deploy_token
end
+ def any_lfs_file_locks?
+ lfs_file_locks.any?
+ end
+ request_cache(:any_lfs_file_locks?) { self.id }
+
+ def auto_cancel_pending_pipelines?
+ auto_cancel_pending_pipelines == 'enabled'
+ end
+
private
def storage
@@ -2125,18 +2162,22 @@ class Project < ActiveRecord::Base
raise ex
end
- def fetch_branch_allows_maintainer_push?(user, branch_name)
+ def fetch_branch_allows_collaboration?(user, branch_name)
check_access = -> do
next false if empty_repo?
- merge_request = source_of_merge_requests.opened
- .where(allow_maintainer_to_push: true)
- .find_by(source_branch: branch_name)
- merge_request&.can_be_merged_by?(user)
+ merge_requests = source_of_merge_requests.opened
+ .where(allow_collaboration: true)
+
+ if branch_name
+ merge_requests.find_by(source_branch: branch_name)&.can_be_merged_by?(user)
+ else
+ merge_requests.any? { |merge_request| merge_request.can_be_merged_by?(user) }
+ end
end
if RequestStore.active?
- RequestStore.fetch("project-#{id}:branch-#{branch_name}:user-#{user.id}:branch_allows_maintainer_push") do
+ RequestStore.fetch("project-#{id}:branch-#{branch_name}:user-#{user.id}:branch_allows_collaboration") do
check_access.call
end
else
diff --git a/app/models/project_auto_devops.rb b/app/models/project_auto_devops.rb
index ed6c1eddbc1..faa831b1949 100644
--- a/app/models/project_auto_devops.rb
+++ b/app/models/project_auto_devops.rb
@@ -1,11 +1,18 @@
class ProjectAutoDevops < ActiveRecord::Base
belongs_to :project
+ enum deploy_strategy: {
+ continuous: 0,
+ manual: 1
+ }
+
scope :enabled, -> { where(enabled: true) }
scope :disabled, -> { where(enabled: false) }
validates :domain, allow_blank: true, hostname: { allow_numeric_hostname: true }
+ after_save :create_gitlab_deploy_token, if: :needs_to_create_deploy_token?
+
def instance_domain
Gitlab::CurrentSettings.auto_devops_domain
end
@@ -20,6 +27,30 @@ class ProjectAutoDevops < ActiveRecord::Base
variables.append(key: 'AUTO_DEVOPS_DOMAIN',
value: domain.presence || instance_domain)
end
+
+ if manual?
+ variables.append(key: 'STAGING_ENABLED', value: '1')
+ variables.append(key: 'INCREMENTAL_ROLLOUT_ENABLED', value: '1')
+ end
end
end
+
+ private
+
+ def create_gitlab_deploy_token
+ project.deploy_tokens.create!(
+ name: DeployToken::GITLAB_DEPLOY_TOKEN_NAME,
+ read_registry: true
+ )
+ end
+
+ def needs_to_create_deploy_token?
+ auto_devops_enabled? &&
+ !project.public? &&
+ !project.deploy_tokens.find_by(name: DeployToken::GITLAB_DEPLOY_TOKEN_NAME).present?
+ end
+
+ def auto_devops_enabled?
+ Gitlab::CurrentSettings.auto_devops_enabled? || enabled?
+ end
end
diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb
index ac1e9ab2b0b..cf8fc41e870 100644
--- a/app/models/project_group_link.rb
+++ b/app/models/project_group_link.rb
@@ -4,7 +4,8 @@ class ProjectGroupLink < ActiveRecord::Base
GUEST = 10
REPORTER = 20
DEVELOPER = 30
- MASTER = 40
+ MAINTAINER = 40
+ MASTER = MAINTAINER # @deprecated
belongs_to :project
belongs_to :group
diff --git a/app/models/project_import_data.rb b/app/models/project_import_data.rb
index 6da6632f4f2..1d7089ccfc7 100644
--- a/app/models/project_import_data.rb
+++ b/app/models/project_import_data.rb
@@ -3,7 +3,7 @@ require 'carrierwave/orm/activerecord'
class ProjectImportData < ActiveRecord::Base
belongs_to :project, inverse_of: :import_data
attr_encrypted :credentials,
- key: Gitlab::Application.secrets.db_key_base,
+ key: Settings.attr_encrypted_db_key_base,
marshal: true,
encode: true,
mode: :per_attribute_iv_and_salt,
diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb
index 54e4b3278db..edc5c00d9c4 100644
--- a/app/models/project_services/bamboo_service.rb
+++ b/app/models/project_services/bamboo_service.rb
@@ -3,7 +3,7 @@ class BambooService < CiService
prop_accessor :bamboo_url, :build_key, :username, :password
- validates :bamboo_url, presence: true, url: true, if: :activated?
+ validates :bamboo_url, presence: true, public_url: true, if: :activated?
validates :build_key, presence: true, if: :activated?
validates :username,
presence: true,
@@ -67,11 +67,11 @@ class BambooService < CiService
def execute(data)
return unless supported_events.include?(data[:object_kind])
- get_path("updateAndBuild.action?buildKey=#{build_key}")
+ get_path("updateAndBuild.action", { buildKey: build_key })
end
def calculate_reactive_cache(sha, ref)
- response = get_path("rest/api/latest/result?label=#{sha}")
+ response = get_path("rest/api/latest/result/byChangeset/#{sha}")
{ build_page: read_build_page(response), commit_status: read_commit_status(response) }
end
@@ -113,18 +113,20 @@ class BambooService < CiService
URI.join("#{bamboo_url}/", path).to_s
end
- def get_path(path)
+ def get_path(path, query_params = {})
url = build_url(path)
if username.blank? && password.blank?
- Gitlab::HTTP.get(url, verify: false)
+ Gitlab::HTTP.get(url, verify: false, query: query_params)
else
- url << '&os_authType=basic'
- Gitlab::HTTP.get(url, verify: false,
- basic_auth: {
- username: username,
- password: password
- })
+ query_params[:os_authType] = 'basic'
+ Gitlab::HTTP.get(url,
+ verify: false,
+ query: query_params,
+ basic_auth: {
+ username: username,
+ password: password
+ })
end
end
end
diff --git a/app/models/project_services/bugzilla_service.rb b/app/models/project_services/bugzilla_service.rb
index 046e2809f45..e4e3a80976b 100644
--- a/app/models/project_services/bugzilla_service.rb
+++ b/app/models/project_services/bugzilla_service.rb
@@ -1,5 +1,5 @@
class BugzillaService < IssueTrackerService
- validates :project_url, :issues_url, :new_issue_url, presence: true, url: true, if: :activated?
+ validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb
index d2aaff8817a..35884c4560c 100644
--- a/app/models/project_services/buildkite_service.rb
+++ b/app/models/project_services/buildkite_service.rb
@@ -8,7 +8,7 @@ class BuildkiteService < CiService
prop_accessor :project_url, :token
boolean_accessor :enable_ssl_verification
- validates :project_url, presence: true, url: true, if: :activated?
+ validates :project_url, presence: true, public_url: true, if: :activated?
validates :token, presence: true, if: :activated?
after_save :compose_service_hook, if: :activated?
diff --git a/app/models/project_services/chat_message/base_message.rb b/app/models/project_services/chat_message/base_message.rb
index 22a65b5145e..f710fa85b5d 100644
--- a/app/models/project_services/chat_message/base_message.rb
+++ b/app/models/project_services/chat_message/base_message.rb
@@ -26,13 +26,18 @@ module ChatMessage
end
end
- def pretext
+ def summary
return message if markdown
format(message)
end
+ def pretext
+ summary
+ end
+
def fallback
+ format(message)
end
def attachments
diff --git a/app/models/project_services/chat_message/pipeline_message.rb b/app/models/project_services/chat_message/pipeline_message.rb
index 2135122278a..96fd23aede3 100644
--- a/app/models/project_services/chat_message/pipeline_message.rb
+++ b/app/models/project_services/chat_message/pipeline_message.rb
@@ -23,10 +23,6 @@ module ChatMessage
''
end
- def fallback
- format(message)
- end
-
def attachments
return message if markdown
diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb
index 7591ab4f478..a60b4c7fd0d 100644
--- a/app/models/project_services/chat_notification_service.rb
+++ b/app/models/project_services/chat_notification_service.rb
@@ -8,7 +8,7 @@ class ChatNotificationService < Service
prop_accessor :webhook, :username, :channel
boolean_accessor :notify_only_broken_pipelines, :notify_only_default_branch
- validates :webhook, presence: true, url: true, if: :activated?
+ validates :webhook, presence: true, public_url: true, if: :activated?
def initialize_properties
# Custom serialized properties initialization
@@ -155,6 +155,7 @@ class ChatNotificationService < Service
end
def notify_for_ref?(data)
+ return true if data[:object_kind] == 'tag_push'
return true if data.dig(:object_attributes, :tag)
return true unless notify_only_default_branch?
diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb
index b9e3e982b64..456c7f5cee2 100644
--- a/app/models/project_services/custom_issue_tracker_service.rb
+++ b/app/models/project_services/custom_issue_tracker_service.rb
@@ -1,5 +1,5 @@
class CustomIssueTrackerService < IssueTrackerService
- validates :project_url, :issues_url, :new_issue_url, presence: true, url: true, if: :activated?
+ validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb
index 71b10fc6bc1..ab4e46da89f 100644
--- a/app/models/project_services/drone_ci_service.rb
+++ b/app/models/project_services/drone_ci_service.rb
@@ -4,7 +4,7 @@ class DroneCiService < CiService
prop_accessor :drone_url, :token
boolean_accessor :enable_ssl_verification
- validates :drone_url, presence: true, url: true, if: :activated?
+ validates :drone_url, presence: true, public_url: true, if: :activated?
validates :token, presence: true, if: :activated?
after_save :compose_service_hook, if: :activated?
@@ -115,6 +115,6 @@ class DroneCiService < CiService
def merge_request_valid?(data)
data[:object_attributes][:state] == 'opened' &&
- data[:object_attributes][:merge_status] == 'unchecked'
+ MergeRequest.state_machines[:merge_status].check_state?(data[:object_attributes][:merge_status])
end
end
diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb
index 1553f169827..a4b1ef09e93 100644
--- a/app/models/project_services/external_wiki_service.rb
+++ b/app/models/project_services/external_wiki_service.rb
@@ -1,7 +1,7 @@
class ExternalWikiService < Service
prop_accessor :external_wiki_url
- validates :external_wiki_url, presence: true, url: true, if: :activated?
+ validates :external_wiki_url, presence: true, public_url: true, if: :activated?
def title
'External Wiki'
diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb
index 84248f9590b..8a6b0ed1a5f 100644
--- a/app/models/project_services/gemnasium_service.rb
+++ b/app/models/project_services/gemnasium_service.rb
@@ -43,13 +43,18 @@ class GemnasiumService < Service
def execute(data)
return unless supported_events.include?(data[:object_kind])
+ # Gitaly: this class will be removed https://gitlab.com/gitlab-org/gitlab-ee/issues/6010
+ repo_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ project.repository.path_to_repo
+ end
+
Gemnasium::GitlabService.execute(
ref: data[:ref],
before: data[:before],
after: data[:after],
token: token,
api_key: api_key,
- repo: project.repository.path_to_repo # Gitaly: fixed by https://gitlab.com/gitlab-org/security-products/gemnasium-migration/issues/9
+ repo: repo_path
)
end
end
diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb
index 88c428b4aae..16e32a4139e 100644
--- a/app/models/project_services/gitlab_issue_tracker_service.rb
+++ b/app/models/project_services/gitlab_issue_tracker_service.rb
@@ -1,7 +1,7 @@
class GitlabIssueTrackerService < IssueTrackerService
include Gitlab::Routing
- validates :project_url, :issues_url, :new_issue_url, presence: true, url: true, if: :activated?
+ validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index ed4bbfb6cfc..412d62388f0 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -3,8 +3,8 @@ class JiraService < IssueTrackerService
include ApplicationHelper
include ActionView::Helpers::AssetUrlHelper
- validates :url, url: true, presence: true, if: :activated?
- validates :api_url, url: true, allow_blank: true
+ validates :url, public_url: true, presence: true, if: :activated?
+ validates :api_url, public_url: true, allow_blank: true
validates :username, presence: true, if: :activated?
validates :password, presence: true, if: :activated?
@@ -265,7 +265,7 @@ class JiraService < IssueTrackerService
title: title,
status: status,
icon: {
- title: 'GitLab', url16x16: asset_url('favicon.ico', host: gitlab_config.url)
+ title: 'GitLab', url16x16: asset_url(Gitlab::Favicon.main, host: gitlab_config.url)
}
}
}
diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb
index 20fed432e55..722642f6da7 100644
--- a/app/models/project_services/kubernetes_service.rb
+++ b/app/models/project_services/kubernetes_service.rb
@@ -24,7 +24,7 @@ class KubernetesService < DeploymentService
prop_accessor :ca_pem
with_options presence: true, if: :activated? do
- validates :api_url, url: true
+ validates :api_url, public_url: true
validates :token
end
@@ -240,7 +240,7 @@ class KubernetesService < DeploymentService
end
def deprecation_validation
- return if active_changed?(from: true, to: false)
+ return if active_changed?(from: true, to: false) || (new_record? && !active?)
if deprecated?
errors[:base] << deprecation_message
diff --git a/app/models/project_services/microsoft_teams_service.rb b/app/models/project_services/microsoft_teams_service.rb
index 2facff53e26..99500caec0e 100644
--- a/app/models/project_services/microsoft_teams_service.rb
+++ b/app/models/project_services/microsoft_teams_service.rb
@@ -44,7 +44,7 @@ class MicrosoftTeamsService < ChatNotificationService
def notify(message, opts)
MicrosoftTeams::Notifier.new(webhook).ping(
title: message.project_name,
- pretext: message.pretext,
+ summary: message.summary,
activity: message.activity,
attachments: message.attachments
)
diff --git a/app/models/project_services/mock_ci_service.rb b/app/models/project_services/mock_ci_service.rb
index 2221459c90b..b89dc07a73e 100644
--- a/app/models/project_services/mock_ci_service.rb
+++ b/app/models/project_services/mock_ci_service.rb
@@ -3,7 +3,7 @@ class MockCiService < CiService
ALLOWED_STATES = %w[failed canceled running pending success success_with_warnings skipped not_found].freeze
prop_accessor :mock_service_url
- validates :mock_service_url, presence: true, url: true, if: :activated?
+ validates :mock_service_url, presence: true, public_url: true, if: :activated?
def title
'MockCI'
diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb
index dcaeb65dc32..df4254e0523 100644
--- a/app/models/project_services/prometheus_service.rb
+++ b/app/models/project_services/prometheus_service.rb
@@ -6,7 +6,7 @@ class PrometheusService < MonitoringService
boolean_accessor :manual_configuration
with_options presence: true, if: :manual_configuration? do
- validates :api_url, url: true
+ validates :api_url, public_url: true
end
before_save :synchronize_service_state
diff --git a/app/models/project_services/redmine_service.rb b/app/models/project_services/redmine_service.rb
index 6acf611eba5..3721093a6d1 100644
--- a/app/models/project_services/redmine_service.rb
+++ b/app/models/project_services/redmine_service.rb
@@ -1,5 +1,5 @@
class RedmineService < IssueTrackerService
- validates :project_url, :issues_url, :new_issue_url, presence: true, url: true, if: :activated?
+ validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb
index 145313b8e71..802678147cf 100644
--- a/app/models/project_services/teamcity_service.rb
+++ b/app/models/project_services/teamcity_service.rb
@@ -3,7 +3,7 @@ class TeamcityService < CiService
prop_accessor :teamcity_url, :build_type, :username, :password
- validates :teamcity_url, presence: true, url: true, if: :activated?
+ validates :teamcity_url, presence: true, public_url: true, if: :activated?
validates :build_type, presence: true, if: :activated?
validates :username,
presence: true,
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 33280eda0b9..c7d0f49d837 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -19,12 +19,15 @@ class ProjectTeam
add_user(user, :developer, current_user: current_user)
end
- def add_master(user, current_user: nil)
- add_user(user, :master, current_user: current_user)
+ def add_maintainer(user, current_user: nil)
+ add_user(user, :maintainer, current_user: current_user)
end
+ # @deprecated
+ alias_method :add_master, :add_maintainer
+
def add_role(user, role, current_user: nil)
- send(:"add_#{role}", user, current_user: current_user) # rubocop:disable GitlabSecurity/PublicSend
+ public_send(:"add_#{role}", user, current_user: current_user) # rubocop:disable GitlabSecurity/PublicSend
end
def find_member(user_id)
@@ -81,10 +84,13 @@ class ProjectTeam
@developers ||= fetch_members(Gitlab::Access::DEVELOPER)
end
- def masters
- @masters ||= fetch_members(Gitlab::Access::MASTER)
+ def maintainers
+ @maintainers ||= fetch_members(Gitlab::Access::MAINTAINER)
end
+ # @deprecated
+ alias_method :masters, :maintainers
+
def owners
@owners ||=
if group
@@ -136,10 +142,13 @@ class ProjectTeam
max_member_access(user.id) == Gitlab::Access::DEVELOPER
end
- def master?(user)
- max_member_access(user.id) == Gitlab::Access::MASTER
+ def maintainer?(user)
+ max_member_access(user.id) == Gitlab::Access::MAINTAINER
end
+ # @deprecated
+ alias_method :master?, :maintainer?
+
# Checks if `user` is authorized for this project, with at least the
# `min_access_level` (if given).
def member?(user, min_access_level = Gitlab::Access::GUEST)
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index f799a0b4227..fc868c3ebb7 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -107,7 +107,7 @@ class ProjectWiki
update_project_activity
rescue Gitlab::Git::Wiki::DuplicatePageError => e
@error_message = "Duplicate page: #{e.message}"
- return false
+ false
end
def update_page(page, content:, title: nil, format: :markdown, message: nil)
@@ -140,10 +140,6 @@ class ProjectWiki
[title, title_array.join("/")]
end
- def search_files(query)
- repository.search_files_by_content(query, default_branch)
- end
-
def repository
@repository ||= Repository.new(full_path, @project, disk_path: disk_path, is_wiki: true)
end
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index cb361a66591..dff99cfca35 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -5,7 +5,7 @@ class ProtectedBranch < ActiveRecord::Base
protected_ref_access_levels :merge, :push
def self.protected_ref_accessible_to?(ref, user, project:, action:, protected_refs: nil)
- # Masters, owners and admins are allowed to create the default branch
+ # Maintainers, owners and admins are allowed to create the default branch
if default_branch_protected? && project.empty_repo?
return true if user.admin? || project.team.max_member_access(user.id) > Gitlab::Access::DEVELOPER
end
diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb
index bbf8fd9c6a7..976b501e297 100644
--- a/app/models/remote_mirror.rb
+++ b/app/models/remote_mirror.rb
@@ -5,7 +5,7 @@ class RemoteMirror < ActiveRecord::Base
UNPROTECTED_BACKOFF_DELAY = 5.minutes
attr_encrypted :credentials,
- key: Gitlab::Application.secrets.db_key_base,
+ key: Settings.attr_encrypted_db_key_base,
marshal: true,
encode: true,
mode: :per_attribute_iv_and_salt,
@@ -16,8 +16,7 @@ class RemoteMirror < ActiveRecord::Base
belongs_to :project, inverse_of: :remote_mirrors
- validates :url, presence: true, url: { protocols: %w(ssh git http https), allow_blank: true }
- validates :url, addressable_url: true, if: :url_changed?
+ validates :url, presence: true, url: { protocols: %w(ssh git http https), allow_blank: true, enforce_user: true }
before_save :set_new_remote_name, if: :mirror_url_changed?
@@ -58,7 +57,7 @@ class RemoteMirror < ActiveRecord::Base
Gitlab::Metrics.add_event(:remote_mirrors_finished, path: remote_mirror.project.full_path)
timestamp = Time.now
- remote_mirror.update_attributes!(
+ remote_mirror.update!(
last_update_at: timestamp, last_successful_update_at: timestamp, last_error: nil
)
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 0e1bf11d7c0..5ed2a7b4068 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -21,7 +21,7 @@ class Repository
attr_accessor :full_path, :disk_path, :project, :is_wiki
delegate :ref_name_for_sha, to: :raw_repository
- delegate :bundle_to_disk, :create_from_bundle, to: :raw_repository
+ delegate :bundle_to_disk, to: :raw_repository
CreateTreeError = Class.new(StandardError)
@@ -83,7 +83,7 @@ class Repository
@raw_repository&.cleanup
end
- # Return absolute path to repository
+ # Don't use this! It's going away. Use Gitaly to read or write from repos.
def path_to_repo
@path_to_repo ||=
begin
@@ -99,11 +99,11 @@ class Repository
"#<#{self.class.name}:#{@disk_path}>"
end
- def commit(ref = 'HEAD')
+ def commit(ref = nil)
return nil unless exists?
return ref if ref.is_a?(::Commit)
- find_commit(ref)
+ find_commit(ref || root_ref)
end
# Finding a commit by the passed SHA
@@ -154,7 +154,10 @@ class Repository
# Returns a list of commits that are not present in any reference
def new_commits(newrev)
- refs = ::Gitlab::Git::RevList.new(raw, newrev: newrev).new_refs
+ # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/1233
+ refs = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ ::Gitlab::Git::RevList.new(raw, newrev: newrev).new_refs
+ end
refs.map { |sha| commit(sha.strip) }
end
@@ -247,7 +250,7 @@ class Repository
# This will still fail if the file is corrupted (e.g. 0 bytes)
raw_repository.write_ref(keep_around_ref_name(sha), sha, shell: false)
rescue Gitlab::Git::CommandError => ex
- Rails.logger.error "Unable to create keep-around reference for repository #{path}: #{ex}"
+ Rails.logger.error "Unable to create keep-around reference for repository #{disk_path}: #{ex}"
end
def kept_around?(sha)
@@ -270,6 +273,20 @@ class Repository
end
end
+ def archive_metadata(ref, storage_path, format = "tar.gz", append_sha:)
+ raw_repository.archive_metadata(
+ ref,
+ storage_path,
+ project.path,
+ format,
+ append_sha: append_sha
+ )
+ end
+
+ def cached_methods
+ CACHED_METHODS
+ end
+
def expire_tags_cache
expire_method_caches(%i(tag_names tag_count))
@tags = nil
@@ -410,7 +427,7 @@ class Repository
# Runs code after the HEAD of a repository is changed.
def after_change_head
- expire_method_caches(METHOD_CACHES_FOR_FILE_TYPES.keys)
+ expire_all_method_caches
end
# Runs code after a repository has been forked/imported.
@@ -445,12 +462,12 @@ class Repository
expire_branches_cache
end
- def method_missing(m, *args, &block)
- if m == :lookup && !block_given?
- lookup_cache[m] ||= {}
- lookup_cache[m][args.join(":")] ||= raw_repository.__send__(m, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
+ def method_missing(msg, *args, &block)
+ if msg == :lookup && !block_given?
+ lookup_cache[msg] ||= {}
+ lookup_cache[msg][args.join(":")] ||= raw_repository.__send__(msg, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
else
- raw_repository.__send__(m, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
+ raw_repository.__send__(msg, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
end
end
@@ -547,7 +564,7 @@ class Repository
end
def rendered_readme
- MarkupHelper.markup_unsafe(readme.name, readme.data, project: project) if readme
+ MarkupHelper.markup_unsafe(readme.name, readme.data, project: project, markdown_engine: :redcarpet) if readme
end
cache_method :rendered_readme
@@ -837,7 +854,7 @@ class Repository
@root_ref_sha ||= commit(root_ref).sha
end
- delegate :merged_branch_names, :can_be_merged?, to: :raw_repository
+ 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
@@ -946,6 +963,10 @@ class Repository
blob_data_at(sha, path)
end
+ def lfsconfig_for(sha)
+ blob_data_at(sha, '.lfsconfig')
+ 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
@@ -957,6 +978,22 @@ class Repository
remote_branch: merge_request.target_branch)
end
+ def blob_data_at(sha, path)
+ blob = blob_at(sha, path)
+ return unless blob
+
+ blob.load_all_data!
+ blob.data
+ end
+
+ def squash(user, merge_request)
+ raw.squash(user, merge_request.id, branch: merge_request.target_branch,
+ start_sha: merge_request.diff_start_sha,
+ end_sha: merge_request.diff_head_sha,
+ author: merge_request.author,
+ message: merge_request.title)
+ end
+
private
# TODO Generice finder, later split this on finders by Ref or Oid
@@ -971,14 +1008,6 @@ class Repository
::Commit.new(commit, @project) if commit
end
- def blob_data_at(sha, path)
- blob = blob_at(sha, path)
- return unless blob
-
- blob.load_all_data!
- blob.data
- end
-
def cache
@cache ||= Gitlab::RepositoryCache.new(self)
end
diff --git a/app/models/service.rb b/app/models/service.rb
index 831c2ea1141..ad835293b46 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -206,10 +206,11 @@ class Service < ActiveRecord::Base
args.each do |arg|
class_eval %{
def #{arg}?
+ # '!!' is used because nil or empty string is converted to nil
if Gitlab.rails5?
- !ActiveModel::Type::Boolean::FALSE_VALUES.include?(#{arg})
+ !!ActiveRecord::Type::Boolean.new.cast(#{arg})
else
- ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(#{arg})
+ !!ActiveRecord::Type::Boolean.new.type_cast_from_database(#{arg})
end
end
}
@@ -280,9 +281,9 @@ class Service < ActiveRecord::Base
def self.build_from_template(project_id, template)
service = template.dup
- service.active = false unless service.valid?
service.template = false
service.project_id = project_id
+ service.active = false if service.active? && !service.valid?
service
end
diff --git a/app/models/term_agreement.rb b/app/models/term_agreement.rb
index 8458a231bbd..c317bd0c90b 100644
--- a/app/models/term_agreement.rb
+++ b/app/models/term_agreement.rb
@@ -2,5 +2,7 @@ class TermAgreement < ActiveRecord::Base
belongs_to :term, class_name: 'ApplicationSetting::Term'
belongs_to :user
+ scope :accepted, -> { where(accepted: true) }
+
validates :user, :term, presence: true
end
diff --git a/app/models/timelog.rb b/app/models/timelog.rb
index e166cf69703..659146f43e4 100644
--- a/app/models/timelog.rb
+++ b/app/models/timelog.rb
@@ -2,8 +2,8 @@ class Timelog < ActiveRecord::Base
validates :time_spent, :user, presence: true
validate :issuable_id_is_present
- belongs_to :issue
- belongs_to :merge_request
+ belongs_to :issue, touch: true
+ belongs_to :merge_request, touch: true
belongs_to :user
def issuable
@@ -19,4 +19,9 @@ class Timelog < ActiveRecord::Base
errors.add(:base, 'Issue or Merge Request ID is required')
end
end
+
+ # Rails5 defaults to :touch_later, overwrite for normal touch
+ def belongs_to_touch_method
+ :touch
+ end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 0a838d34054..4987d01aac6 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -26,7 +26,7 @@ class User < ActiveRecord::Base
ignore_column :authentication_token
add_authentication_token_field :incoming_email_token
- add_authentication_token_field :rss_token
+ add_authentication_token_field :feed_token
default_value_for :admin, false
default_value_for(:external) { Gitlab::CurrentSettings.user_default_external }
@@ -99,7 +99,8 @@ class User < ActiveRecord::Base
has_many :group_members, -> { where(requested_at: nil) }, source: 'GroupMember'
has_many :groups, through: :group_members
has_many :owned_groups, -> { where(members: { access_level: Gitlab::Access::OWNER }) }, through: :group_members, source: :group
- has_many :masters_groups, -> { where(members: { access_level: Gitlab::Access::MASTER }) }, through: :group_members, source: :group
+ has_many :maintainers_groups, -> { where(members: { access_level: Gitlab::Access::MAINTAINER }) }, through: :group_members, source: :group
+ alias_attribute :masters_groups, :maintainers_groups
# Projects
has_many :groups_projects, through: :groups, source: :projects
@@ -244,7 +245,7 @@ class User < ActiveRecord::Base
scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
scope :external, -> { where(external: true) }
scope :active, -> { with_state(:active).non_internal }
- scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members WHERE user_id IS NOT NULL AND requested_at IS NULL)') }
+ scope :without_projects, -> { joins('LEFT JOIN project_authorizations ON users.id = project_authorizations.user_id').where(project_authorizations: { user_id: nil }) }
scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) }
scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'DESC')) }
scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'ASC')) }
@@ -496,7 +497,7 @@ class User < ActiveRecord::Base
def disable_two_factor!
transaction do
- update_attributes(
+ update(
otp_required_for_login: false,
encrypted_otp_secret: nil,
encrypted_otp_secret_iv: nil,
@@ -728,7 +729,7 @@ class User < ActiveRecord::Base
end
def several_namespaces?
- owned_groups.any? || masters_groups.any?
+ owned_groups.any? || maintainers_groups.any?
end
def namespace_id
@@ -974,15 +975,15 @@ class User < ActiveRecord::Base
end
def manageable_groups
- union_sql = Gitlab::SQL::Union.new([owned_groups.select(:id), masters_groups.select(:id)]).to_sql
+ union_sql = Gitlab::SQL::Union.new([owned_groups.select(:id), maintainers_groups.select(:id)]).to_sql
# Update this line to not use raw SQL when migrated to Rails 5.2.
# Either ActiveRecord or Arel constructions are fine.
# This was replaced with the raw SQL construction because of bugs in the arel gem.
# Bugs were fixed in arel 9.0.0 (Rails 5.2).
- owned_and_master_groups = Group.where("namespaces.id IN (#{union_sql})") # rubocop:disable GitlabSecurity/SqlInjection
+ owned_and_maintainer_groups = Group.where("namespaces.id IN (#{union_sql})") # rubocop:disable GitlabSecurity/SqlInjection
- Gitlab::GroupHierarchy.new(owned_and_master_groups).base_and_descendants
+ Gitlab::GroupHierarchy.new(owned_and_maintainer_groups).base_and_descendants
end
def namespaces
@@ -1023,22 +1024,25 @@ class User < ActiveRecord::Base
def ci_owned_runners
@ci_owned_runners ||= begin
project_runner_ids = Ci::RunnerProject
- .where(project: authorized_projects(Gitlab::Access::MASTER))
+ .where(project: authorized_projects(Gitlab::Access::MAINTAINER))
.select(:runner_id)
group_runner_ids = Ci::RunnerNamespace
- .where(namespace_id: owned_or_masters_groups.select(:id))
+ .where(namespace_id: owned_or_maintainers_groups.select(:id))
.select(:runner_id)
union = Gitlab::SQL::Union.new([project_runner_ids, group_runner_ids])
- Ci::Runner.specific.where("ci_runners.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
+ Ci::Runner.where("ci_runners.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
end
end
def notification_settings_for(source)
if notification_settings.loaded?
- notification_settings.find { |notification| notification.source == source }
+ notification_settings.find do |notification|
+ notification.source_type == source.class.base_class.name &&
+ notification.source_id == source.id
+ end
else
notification_settings.find_or_initialize_by(source: source)
end
@@ -1050,7 +1054,7 @@ class User < ActiveRecord::Base
return @global_notification_setting if defined?(@global_notification_setting)
@global_notification_setting = notification_settings.find_or_initialize_by(source: nil)
- @global_notification_setting.update_attributes(level: NotificationSetting.levels[DEFAULT_NOTIFICATION_LEVEL]) unless @global_notification_setting.persisted?
+ @global_notification_setting.update(level: NotificationSetting.levels[DEFAULT_NOTIFICATION_LEVEL]) unless @global_notification_setting.persisted?
@global_notification_setting
end
@@ -1167,11 +1171,11 @@ class User < ActiveRecord::Base
save
end
- # each existing user needs to have an `rss_token`.
+ # each existing user needs to have an `feed_token`.
# we do this on read since migrating all existing users is not a feasible
# solution.
- def rss_token
- ensure_rss_token!
+ def feed_token
+ ensure_feed_token!
end
def sync_attribute?(attribute)
@@ -1233,11 +1237,14 @@ class User < ActiveRecord::Base
!terms_accepted?
end
- def owned_or_masters_groups
- union = Gitlab::SQL::Union.new([owned_groups, masters_groups])
+ def owned_or_maintainers_groups
+ union = Gitlab::SQL::Union.new([owned_groups, maintainers_groups])
Group.from("(#{union.to_sql}) namespaces")
end
+ # @deprecated
+ alias_method :owned_or_masters_groups, :owned_or_maintainers_groups
+
protected
# override, from Devise::Validatable
@@ -1330,8 +1337,8 @@ class User < ActiveRecord::Base
end
end
- def self.unique_internal(scope, username, email_pattern, &b)
- scope.first || create_unique_internal(scope, username, email_pattern, &b)
+ def self.unique_internal(scope, username, email_pattern, &block)
+ scope.first || create_unique_internal(scope, username, email_pattern, &block)
end
def self.create_unique_internal(scope, username, email_pattern, &creation_block)
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index cde79b95062..4b49edb01a5 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Rails/ActiveRecordAliases
class WikiPage
PageChangedError = Class.new(StandardError)
PageRenameError = Class.new(StandardError)
diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb
index 8b65758f3e8..75c7e529902 100644
--- a/app/policies/ci/build_policy.rb
+++ b/app/policies/ci/build_policy.rb
@@ -14,8 +14,12 @@ module Ci
@subject.triggered_by?(@user)
end
- condition(:branch_allows_maintainer_push) do
- @subject.project.branch_allows_maintainer_push?(@user, @subject.ref)
+ condition(:branch_allows_collaboration) do
+ @subject.project.branch_allows_collaboration?(@user, @subject.ref)
+ end
+
+ condition(:terminal, scope: :subject) do
+ @subject.has_terminal?
end
rule { protected_ref }.policy do
@@ -25,9 +29,11 @@ module Ci
rule { can?(:admin_build) | (can?(:update_build) & owner_of_job) }.enable :erase_build
- rule { can?(:public_access) & branch_allows_maintainer_push }.policy do
+ rule { can?(:public_access) & branch_allows_collaboration }.policy do
enable :update_build
enable :update_commit_status
end
+
+ rule { can?(:update_build) & terminal }.enable :create_build_terminal
end
end
diff --git a/app/policies/ci/pipeline_policy.rb b/app/policies/ci/pipeline_policy.rb
index 540e4235299..b81329d0625 100644
--- a/app/policies/ci/pipeline_policy.rb
+++ b/app/policies/ci/pipeline_policy.rb
@@ -4,13 +4,13 @@ module Ci
condition(:protected_ref) { ref_protected?(@user, @subject.project, @subject.tag?, @subject.ref) }
- condition(:branch_allows_maintainer_push) do
- @subject.project.branch_allows_maintainer_push?(@user, @subject.ref)
+ condition(:branch_allows_collaboration) do
+ @subject.project.branch_allows_collaboration?(@user, @subject.ref)
end
rule { protected_ref }.prevent :update_pipeline
- rule { can?(:public_access) & branch_allows_maintainer_push }.policy do
+ rule { can?(:public_access) & branch_allows_collaboration }.policy do
enable :update_pipeline
end
diff --git a/app/policies/clusters/cluster_policy.rb b/app/policies/clusters/cluster_policy.rb
index 1f7c13072b9..b5b24491655 100644
--- a/app/policies/clusters/cluster_policy.rb
+++ b/app/policies/clusters/cluster_policy.rb
@@ -4,7 +4,7 @@ module Clusters
delegate { cluster.project }
- rule { can?(:master_access) }.policy do
+ rule { can?(:maintainer_access) }.policy do
enable :update_cluster
enable :admin_cluster
end
diff --git a/app/policies/deploy_token_policy.rb b/app/policies/deploy_token_policy.rb
index 7aa9106e8b1..d1b459cfc90 100644
--- a/app/policies/deploy_token_policy.rb
+++ b/app/policies/deploy_token_policy.rb
@@ -1,10 +1,10 @@
class DeployTokenPolicy < BasePolicy
with_options scope: :subject, score: 0
- condition(:master) { @subject.project.team.master?(@user) }
+ condition(:maintainer) { @subject.project.team.maintainer?(@user) }
rule { anonymous }.prevent_all
- rule { master }.policy do
+ rule { maintainer }.policy do
enable :create_deploy_token
enable :update_deploy_token
end
diff --git a/app/policies/environment_policy.rb b/app/policies/environment_policy.rb
index 375a5535359..978dc3a7c81 100644
--- a/app/policies/environment_policy.rb
+++ b/app/policies/environment_policy.rb
@@ -1,9 +1,13 @@
class EnvironmentPolicy < BasePolicy
delegate { @subject.project }
- condition(:stop_action_allowed) do
- @subject.stop_action? && can?(:update_build, @subject.stop_action)
+ condition(:stop_with_deployment_allowed) do
+ @subject.stop_action? && can?(:create_deployment) && can?(:update_build, @subject.stop_action)
end
- rule { can?(:create_deployment) & stop_action_allowed }.enable :stop_environment
+ condition(:stop_with_update_allowed) do
+ !@subject.stop_action? && can?(:update_environment, @subject)
+ end
+
+ rule { stop_with_deployment_allowed | stop_with_update_allowed }.enable :stop_environment
end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index 520710b757d..dc339b71ec7 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -11,7 +11,7 @@ class GroupPolicy < BasePolicy
condition(:guest) { access_level >= GroupMember::GUEST }
condition(:developer) { access_level >= GroupMember::DEVELOPER }
condition(:owner) { access_level >= GroupMember::OWNER }
- condition(:master) { access_level >= GroupMember::MASTER }
+ condition(:maintainer) { access_level >= GroupMember::MAINTAINER }
condition(:reporter) { access_level >= GroupMember::REPORTER }
condition(:nested_groups_supported, scope: :global) { Group.supports_nested_groups? }
@@ -59,7 +59,7 @@ class GroupPolicy < BasePolicy
enable :admin_issue
end
- rule { master }.policy do
+ rule { maintainer }.policy do
enable :create_projects
enable :admin_pipeline
enable :admin_build
@@ -72,6 +72,19 @@ class GroupPolicy < BasePolicy
enable :change_visibility_level
end
+ rule { can?(:read_nested_project_resources) }.policy do
+ enable :read_group_activity
+ enable :read_group_issues
+ enable :read_group_boards
+ enable :read_group_labels
+ enable :read_group_milestones
+ enable :read_group_merge_requests
+ end
+
+ rule { can?(:read_cross_project) & can?(:read_group) }.policy do
+ enable :read_nested_project_resources
+ end
+
rule { owner & nested_groups_supported }.enable :create_subgroup
rule { public_group | logged_in_viewable }.enable :view_globally
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 99a0d7118f2..bc49092633f 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -45,8 +45,8 @@ class ProjectPolicy < BasePolicy
desc "User has developer access"
condition(:developer) { team_access_level >= Gitlab::Access::DEVELOPER }
- desc "User has master access"
- condition(:master) { team_access_level >= Gitlab::Access::MASTER }
+ desc "User has maintainer access"
+ condition(:maintainer) { team_access_level >= Gitlab::Access::MAINTAINER }
desc "Project is public"
condition(:public_project, scope: :subject, score: 0) { project.public? }
@@ -123,14 +123,14 @@ class ProjectPolicy < BasePolicy
rule { guest }.enable :guest_access
rule { reporter }.enable :reporter_access
rule { developer }.enable :developer_access
- rule { master }.enable :master_access
+ rule { maintainer }.enable :maintainer_access
rule { owner | admin }.enable :owner_access
rule { can?(:owner_access) }.policy do
enable :guest_access
enable :reporter_access
enable :developer_access
- enable :master_access
+ enable :maintainer_access
enable :change_namespace
enable :change_visibility_level
@@ -228,7 +228,7 @@ class ProjectPolicy < BasePolicy
enable :create_deployment
end
- rule { can?(:master_access) }.policy do
+ rule { can?(:maintainer_access) }.policy do
enable :push_to_delete_protected_branch
enable :update_project_snippet
enable :update_environment
@@ -297,6 +297,7 @@ class ProjectPolicy < BasePolicy
prevent(*create_read_update_admin_destroy(:build))
prevent(*create_read_update_admin_destroy(:pipeline_schedule))
prevent(*create_read_update_admin_destroy(:environment))
+ prevent(*create_read_update_admin_destroy(:cluster))
prevent(*create_read_update_admin_destroy(:deployment))
end
diff --git a/app/presenters/commit_status_presenter.rb b/app/presenters/commit_status_presenter.rb
index c7f7aa836bd..9a7aaf4ef32 100644
--- a/app/presenters/commit_status_presenter.rb
+++ b/app/presenters/commit_status_presenter.rb
@@ -1,11 +1,10 @@
class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
CALLOUT_FAILURE_MESSAGES = {
unknown_failure: 'There is an unknown failure, please try again',
- script_failure: 'There has been a script failure. Check the job log for more information',
api_failure: 'There has been an API failure, please try again',
stuck_or_timeout_failure: 'There has been a timeout failure or the job got stuck. Check your timeout limits or try again',
runner_system_failure: 'There has been a runner system failure, please try again',
- missing_dependency_failure: 'There has been a missing dependency failure, check the job log for more information'
+ missing_dependency_failure: 'There has been a missing dependency failure'
}.freeze
presents :build
diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb
index 4b4132af2d0..f77b3541644 100644
--- a/app/presenters/merge_request_presenter.rb
+++ b/app/presenters/merge_request_presenter.rb
@@ -168,6 +168,29 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
.can_push_to_branch?(source_branch)
end
+ def can_remove_source_branch?
+ source_branch_exists? && merge_request.can_remove_source_branch?(current_user)
+ end
+
+ def mergeable_discussions_state
+ # This avoids calling MergeRequest#mergeable_discussions_state without
+ # considering the state of the MR first. If a MR isn't mergeable, we can
+ # safely short-circuit it.
+ if merge_request.mergeable_state?(skip_ci_check: true, skip_discussions_check: true)
+ merge_request.mergeable_discussions_state?
+ else
+ false
+ end
+ end
+
+ def web_url
+ Gitlab::UrlBuilder.build(merge_request)
+ end
+
+ def subscribed?
+ merge_request.subscribed?(current_user, merge_request.target_project)
+ end
+
private
def cached_can_be_reverted?
diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb
index ad655a7b3f4..d4d622d84ab 100644
--- a/app/presenters/project_presenter.rb
+++ b/app/presenters/project_presenter.rb
@@ -27,6 +27,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
def statistics_buttons(show_auto_devops_callout:)
[
+ readme_anchor_data,
changelog_anchor_data,
license_anchor_data,
contribution_guide_anchor_data,
@@ -212,11 +213,11 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end
def readme_anchor_data
- if current_user && can_current_user_push_to_default_branch? && repository.readme.blank?
+ if current_user && can_current_user_push_to_default_branch? && repository.readme.nil?
OpenStruct.new(enabled: false,
label: _('Add Readme'),
link: add_readme_path)
- elsif repository.readme.present?
+ elsif repository.readme
OpenStruct.new(enabled: true,
label: _('Readme'),
link: default_view != 'readme' ? readme_path : '#readme')
diff --git a/app/serializers/blob_entity.rb b/app/serializers/blob_entity.rb
index ad039a2623d..b501fd5e964 100644
--- a/app/serializers/blob_entity.rb
+++ b/app/serializers/blob_entity.rb
@@ -3,11 +3,13 @@ class BlobEntity < Grape::Entity
expose :id, :path, :name, :mode
+ expose :readable_text?, as: :readable_text
+
expose :icon do |blob|
IconsHelper.file_type_icon_class('file', blob.mode, blob.name)
end
- expose :url do |blob|
+ expose :url, if: -> (*) { request.respond_to?(:ref) } do |blob|
project_blob_path(request.project, File.join(request.ref, blob.path))
end
end
diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb
index ca4480fe2b1..2de9624aed4 100644
--- a/app/serializers/build_details_entity.rb
+++ b/app/serializers/build_details_entity.rb
@@ -35,7 +35,7 @@ class BuildDetailsEntity < JobEntity
def build_failed_issue_options
{ title: "Job Failed ##{build.id}",
- description: "Job [##{build.id}](#{project_job_path(project, build)}) failed for #{build.sha}:\n" }
+ description: "Job [##{build.id}](#{project_job_url(project, build)}) failed for #{build.sha}:\n" }
end
def current_user
diff --git a/app/serializers/cluster_application_entity.rb b/app/serializers/cluster_application_entity.rb
index b22a0b666ef..77fc3336521 100644
--- a/app/serializers/cluster_application_entity.rb
+++ b/app/serializers/cluster_application_entity.rb
@@ -3,4 +3,5 @@ class ClusterApplicationEntity < Grape::Entity
expose :status_name, as: :status
expose :status_reason
expose :external_ip, if: -> (e, _) { e.respond_to?(:external_ip) }
+ expose :hostname, if: -> (e, _) { e.respond_to?(:hostname) }
end
diff --git a/app/serializers/diff_file_entity.rb b/app/serializers/diff_file_entity.rb
index 6e68d275047..61135fba97b 100644
--- a/app/serializers/diff_file_entity.rb
+++ b/app/serializers/diff_file_entity.rb
@@ -1,25 +1,48 @@
class DiffFileEntity < Grape::Entity
+ include RequestAwareEntity
+ include BlobHelper
+ include CommitsHelper
include DiffHelper
include SubmoduleHelper
include BlobHelper
include IconsHelper
- include ActionView::Helpers::TagHelper
+ include TreeHelper
+ include ChecksCollaboration
+ include Gitlab::Utils::StrongMemoize
expose :submodule?, as: :submodule
expose :submodule_link do |diff_file|
- submodule_links(diff_file.blob, diff_file.content_sha, diff_file.repository).first
+ memoized_submodule_links(diff_file).first
+ end
+
+ expose :submodule_tree_url do |diff_file|
+ memoized_submodule_links(diff_file).last
end
- expose :blob_path do |diff_file|
- diff_file.blob.path
+ expose :blob, using: BlobEntity
+
+ expose :can_modify_blob do |diff_file|
+ merge_request = options[:merge_request]
+
+ next unless diff_file.blob
+
+ if merge_request&.source_project && current_user
+ can_modify_blob?(diff_file.blob, merge_request.source_project, merge_request.source_branch)
+ else
+ false
+ end
end
- expose :blob_icon do |diff_file|
- blob_icon(diff_file.b_mode, diff_file.file_path)
+ expose :file_hash do |diff_file|
+ Digest::SHA1.hexdigest(diff_file.file_path)
end
expose :file_path
+ expose :too_large?, as: :too_large
+ expose :collapsed?, as: :collapsed
+ expose :new_file?, as: :new_file
+
expose :deleted_file?, as: :deleted_file
expose :renamed_file?, as: :renamed_file
expose :old_path
@@ -28,6 +51,36 @@ class DiffFileEntity < Grape::Entity
expose :a_mode
expose :b_mode
expose :text?, as: :text
+ expose :added_lines
+ expose :removed_lines
+ expose :diff_refs
+ expose :content_sha
+ expose :stored_externally?, as: :stored_externally
+ expose :external_storage
+
+ expose :load_collapsed_diff_url, if: -> (diff_file, options) { diff_file.text? && options[:merge_request] } do |diff_file|
+ merge_request = options[:merge_request]
+ project = merge_request.target_project
+
+ next unless project
+
+ diff_for_path_namespace_project_merge_request_path(
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: merge_request.iid,
+ old_path: diff_file.old_path,
+ new_path: diff_file.new_path,
+ file_identifier: diff_file.file_identifier
+ )
+ end
+
+ expose :formatted_external_url, if: -> (_, options) { options[:environment] } do |diff_file|
+ options[:environment].formatted_external_url
+ end
+
+ expose :external_url, if: -> (_, options) { options[:environment] } do |diff_file|
+ options[:environment].external_url_for(diff_file.new_path, diff_file.content_sha)
+ end
expose :old_path_html do |diff_file|
old_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
@@ -38,4 +91,67 @@ class DiffFileEntity < Grape::Entity
_, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
new_path
end
+
+ expose :edit_path, if: -> (_, options) { options[:merge_request] } do |diff_file|
+ merge_request = options[:merge_request]
+
+ options = merge_request.persisted? ? { from_merge_request_iid: merge_request.iid } : {}
+
+ next unless merge_request.source_project
+
+ project_edit_blob_path(merge_request.source_project,
+ tree_join(merge_request.source_branch, diff_file.new_path),
+ options)
+ end
+
+ expose :view_path, if: -> (_, options) { options[:merge_request] } do |diff_file|
+ merge_request = options[:merge_request]
+
+ project = merge_request.target_project
+
+ next unless project
+ next unless diff_file.content_sha
+
+ project_blob_path(project, tree_join(diff_file.content_sha, diff_file.new_path))
+ end
+
+ expose :replaced_view_path, if: -> (_, options) { options[:merge_request] } do |diff_file|
+ image_diff = diff_file.rich_viewer && diff_file.rich_viewer.partial_name == 'image'
+ image_replaced = diff_file.old_content_sha && diff_file.old_content_sha != diff_file.content_sha
+
+ merge_request = options[:merge_request]
+ project = merge_request.target_project
+
+ next unless project
+
+ project_blob_path(project, tree_join(diff_file.old_content_sha, diff_file.old_path)) if image_diff && image_replaced
+ end
+
+ expose :context_lines_path, if: -> (diff_file, _) { diff_file.text? } do |diff_file|
+ next unless diff_file.content_sha
+
+ project_blob_diff_path(diff_file.repository.project, tree_join(diff_file.content_sha, diff_file.file_path))
+ end
+
+ # Used for inline diffs
+ expose :highlighted_diff_lines, if: -> (diff_file, _) { diff_file.text? } do |diff_file|
+ diff_file.diff_lines_for_serializer
+ end
+
+ # Used for parallel diffs
+ expose :parallel_diff_lines, if: -> (diff_file, _) { diff_file.text? }
+
+ def current_user
+ request.current_user
+ end
+
+ def memoized_submodule_links(diff_file)
+ strong_memoize(:submodule_links) do
+ if diff_file.submodule?
+ submodule_links(diff_file.blob, diff_file.content_sha, diff_file.repository)
+ else
+ []
+ end
+ end
+ end
end
diff --git a/app/serializers/diffs_entity.rb b/app/serializers/diffs_entity.rb
new file mode 100644
index 00000000000..bb804e5347a
--- /dev/null
+++ b/app/serializers/diffs_entity.rb
@@ -0,0 +1,65 @@
+class DiffsEntity < Grape::Entity
+ include DiffHelper
+ include RequestAwareEntity
+
+ expose :real_size
+ expose :size
+
+ expose :branch_name do |diffs|
+ merge_request&.source_branch
+ end
+
+ expose :target_branch_name do |diffs|
+ merge_request&.target_branch
+ end
+
+ expose :commit do |diffs|
+ options[:commit]
+ end
+
+ expose :merge_request_diff, using: MergeRequestDiffEntity do |diffs|
+ options[:merge_request_diff]
+ end
+
+ expose :start_version, using: MergeRequestDiffEntity do |diffs|
+ options[:start_version]
+ end
+
+ expose :latest_diff do |diffs|
+ options[:latest_diff]
+ end
+
+ expose :latest_version_path, if: -> (*) { merge_request } do |diffs|
+ diffs_project_merge_request_path(merge_request&.project, merge_request)
+ end
+
+ expose :added_lines do |diffs|
+ diffs.diff_files.sum(&:added_lines)
+ end
+
+ expose :removed_lines do |diffs|
+ diffs.diff_files.sum(&:removed_lines)
+ end
+
+ expose :render_overflow_warning do |diffs|
+ render_overflow_warning?(diffs.diff_files)
+ end
+
+ expose :email_patch_path, if: -> (*) { merge_request } do |diffs|
+ merge_request_path(merge_request, format: :patch)
+ end
+
+ expose :plain_diff_path, if: -> (*) { merge_request } do |diffs|
+ merge_request_path(merge_request, format: :diff)
+ end
+
+ expose :diff_files, using: DiffFileEntity
+
+ expose :merge_request_diffs, using: MergeRequestDiffEntity, if: -> (_, options) { options[:merge_request_diffs]&.any? } do |diffs|
+ options[:merge_request_diffs]
+ end
+
+ def merge_request
+ options[:merge_request]
+ end
+end
diff --git a/app/serializers/diffs_serializer.rb b/app/serializers/diffs_serializer.rb
new file mode 100644
index 00000000000..6771e10c5ac
--- /dev/null
+++ b/app/serializers/diffs_serializer.rb
@@ -0,0 +1,3 @@
+class DiffsSerializer < BaseSerializer
+ entity DiffsEntity
+end
diff --git a/app/serializers/discussion_entity.rb b/app/serializers/discussion_entity.rb
index 718fb35e62d..8a39a4950f5 100644
--- a/app/serializers/discussion_entity.rb
+++ b/app/serializers/discussion_entity.rb
@@ -1,16 +1,31 @@
class DiscussionEntity < Grape::Entity
include RequestAwareEntity
+ include NotesHelper
expose :id, :reply_id
+ expose :position, if: -> (d, _) { d.diff_discussion? && !d.legacy_diff_discussion? }
+ expose :line_code, if: -> (d, _) { d.diff_discussion? }
expose :expanded?, as: :expanded
+ expose :active?, as: :active, if: -> (d, _) { d.diff_discussion? }
+ expose :project_id
expose :notes do |discussion, opts|
request.note_entity.represent(discussion.notes, opts)
end
+ expose :discussion_path do |discussion|
+ discussion_path(discussion)
+ end
+
expose :individual_note?, as: :individual_note
- expose :resolvable?, as: :resolvable
+ expose :resolvable do |discussion|
+ discussion.resolvable?
+ end
+
expose :resolved?, as: :resolved
+ expose :resolved_by_push?, as: :resolved_by_push
+ expose :resolved_by
+ expose :resolved_at
expose :resolve_path, if: -> (d, _) { d.resolvable? } do |discussion|
resolve_project_merge_request_discussion_path(discussion.project, discussion.noteable, discussion.id)
end
@@ -18,24 +33,17 @@ class DiscussionEntity < Grape::Entity
new_project_issue_path(discussion.project, merge_request_to_resolve_discussions_of: discussion.noteable.iid, discussion_to_resolve: discussion.id)
end
- expose :diff_file, using: DiffFileEntity, if: -> (d, _) { defined? d.diff_file }
+ expose :diff_file, using: DiffFileEntity, if: -> (d, _) { d.diff_discussion? }
expose :diff_discussion?, as: :diff_discussion
- expose :truncated_diff_lines, if: -> (d, _) { (defined? d.diff_file) && d.diff_file.text? } do |discussion|
- options[:context].render_to_string(
- partial: "projects/diffs/line",
- collection: discussion.truncated_diff_lines,
- as: :line,
- locals: { diff_file: discussion.diff_file,
- discussion_expanded: true,
- plain: true },
- layout: false,
- formats: [:html]
- )
+ expose :truncated_diff_lines_path, if: -> (d, _) { !d.expanded? && !render_truncated_diff_lines? } do |discussion|
+ project_merge_request_discussion_path(discussion.project, discussion.noteable, discussion)
end
- expose :image_diff_html, if: -> (d, _) { defined? d.diff_file } do |discussion|
+ expose :truncated_diff_lines, if: -> (d, _) { d.diff_discussion? && d.on_text? && (d.expanded? || render_truncated_diff_lines?) }
+
+ expose :image_diff_html, if: -> (d, _) { d.diff_discussion? && d.on_image? } do |discussion|
diff_file = discussion.diff_file
partial = diff_file.new_file? || diff_file.deleted_file? ? 'single_image_diff' : 'replaced_image_diff'
options[:context].render_to_string(
@@ -47,4 +55,17 @@ class DiscussionEntity < Grape::Entity
formats: [:html]
)
end
+
+ expose :for_commit?, as: :for_commit
+ expose :commit_id
+
+ private
+
+ def render_truncated_diff_lines?
+ options[:render_truncated_diff_lines]
+ end
+
+ def current_user
+ request.current_user
+ end
end
diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb
index ba0ae6ba8a0..0fc3f92b151 100644
--- a/app/serializers/environment_entity.rb
+++ b/app/serializers/environment_entity.rb
@@ -7,7 +7,7 @@ class EnvironmentEntity < Grape::Entity
expose :external_url
expose :environment_type
expose :last_deployment, using: DeploymentEntity
- expose :stop_action?
+ expose :stop_action?, as: :has_stop_action
expose :metrics_path, if: -> (environment, _) { environment.has_metrics? } do |environment|
metrics_project_environment_path(environment.project, environment)
@@ -31,4 +31,14 @@ class EnvironmentEntity < Grape::Entity
end
expose :created_at, :updated_at
+
+ expose :can_stop do |environment|
+ environment.available? && can?(current_user, :stop_environment, environment)
+ end
+
+ private
+
+ def current_user
+ request.current_user
+ end
end
diff --git a/app/serializers/group_child_entity.rb b/app/serializers/group_child_entity.rb
index 15ec0f89bb2..ee150eefd9e 100644
--- a/app/serializers/group_child_entity.rb
+++ b/app/serializers/group_child_entity.rb
@@ -31,7 +31,7 @@ class GroupChildEntity < Grape::Entity
end
# Project only attributes
- expose :star_count,
+ expose :star_count, :archived,
if: lambda { |_instance, _options| project? }
# Group only attributes
diff --git a/app/serializers/job_entity.rb b/app/serializers/job_entity.rb
index 3076fed1674..960e7291ae6 100644
--- a/app/serializers/job_entity.rb
+++ b/app/serializers/job_entity.rb
@@ -26,7 +26,7 @@ class JobEntity < Grape::Entity
expose :created_at
expose :updated_at
expose :detailed_status, as: :status, with: StatusEntity
- expose :callout_message, if: -> (*) { failed? }
+ expose :callout_message, if: -> (*) { failed? && !build.script_failure? }
expose :recoverable, if: -> (*) { failed? }
private
diff --git a/app/serializers/merge_request_basic_entity.rb b/app/serializers/merge_request_basic_entity.rb
index e4aec977f01..1c06691026d 100644
--- a/app/serializers/merge_request_basic_entity.rb
+++ b/app/serializers/merge_request_basic_entity.rb
@@ -5,4 +5,8 @@ class MergeRequestBasicEntity < IssuableSidebarEntity
expose :state
expose :source_branch_exists?, as: :source_branch_exists
expose :rebase_in_progress?, as: :rebase_in_progress
+ expose :milestone, using: API::Entities::Milestone
+ expose :labels, using: LabelEntity
+ expose :assignee, using: API::Entities::UserBasic
+ expose :task_status, :task_status_short
end
diff --git a/app/serializers/merge_request_diff_entity.rb b/app/serializers/merge_request_diff_entity.rb
new file mode 100644
index 00000000000..32c761b45ac
--- /dev/null
+++ b/app/serializers/merge_request_diff_entity.rb
@@ -0,0 +1,46 @@
+class MergeRequestDiffEntity < Grape::Entity
+ include Gitlab::Routing
+ include GitHelper
+ include MergeRequestsHelper
+
+ expose :version_index do |merge_request_diff|
+ @merge_request_diffs = options[:merge_request_diffs]
+ diff = options[:merge_request_diff]
+
+ next unless diff.present?
+ next unless @merge_request_diffs.size > 1
+
+ version_index(merge_request_diff)
+ end
+
+ expose :created_at
+ expose :commits_count
+
+ expose :latest?, as: :latest
+
+ expose :short_commit_sha do |merge_request_diff|
+ short_sha(merge_request_diff.head_commit_sha)
+ end
+
+ expose :version_path do |merge_request_diff|
+ start_sha = options[:start_sha]
+ project = merge_request.target_project
+
+ next unless project
+
+ merge_request_version_path(project, merge_request, merge_request_diff, start_sha)
+ end
+
+ expose :compare_path do |merge_request_diff|
+ project = merge_request.target_project
+ diff = options[:merge_request_diff]
+
+ if project && diff
+ merge_request_version_path(project, merge_request, diff, merge_request_diff.head_commit_sha)
+ end
+ end
+
+ def merge_request
+ options[:merge_request]
+ end
+end
diff --git a/app/serializers/merge_request_user_entity.rb b/app/serializers/merge_request_user_entity.rb
new file mode 100644
index 00000000000..33fc7b724d5
--- /dev/null
+++ b/app/serializers/merge_request_user_entity.rb
@@ -0,0 +1,24 @@
+class MergeRequestUserEntity < UserEntity
+ include RequestAwareEntity
+ include BlobHelper
+ include TreeHelper
+
+ expose :can_fork do |user|
+ can?(user, :fork_project, request.project) if project
+ end
+
+ expose :can_create_merge_request do |user|
+ project && can?(user, :create_merge_request_in, project)
+ end
+
+ expose :fork_path, if: -> (*) { project } do |user|
+ params = edit_blob_fork_params("Edit")
+ project_forks_path(project, namespace_key: user.namespace.id, continue: params)
+ end
+
+ def project
+ return false unless request.respond_to?(:project) && request.project
+
+ request.project
+ end
+end
diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb
index d0165c148eb..5d72ebdd7fd 100644
--- a/app/serializers/merge_request_widget_entity.rb
+++ b/app/serializers/merge_request_widget_entity.rb
@@ -10,9 +10,10 @@ class MergeRequestWidgetEntity < IssuableEntity
expose :merge_when_pipeline_succeeds
expose :source_branch
expose :source_project_id
+ expose :squash
expose :target_branch
expose :target_project_id
- expose :allow_maintainer_to_push
+ expose :allow_collaboration
expose :should_be_rebased?, as: :should_be_rebased
expose :ff_only_enabled do |merge_request|
@@ -108,7 +109,7 @@ class MergeRequestWidgetEntity < IssuableEntity
expose :current_user do
expose :can_remove_source_branch do |merge_request|
- merge_request.source_branch_exists? && merge_request.can_remove_source_branch?(current_user)
+ presenter(merge_request).can_remove_source_branch?
end
expose :can_revert_on_current_merge_request do |merge_request|
@@ -119,12 +120,12 @@ class MergeRequestWidgetEntity < IssuableEntity
presenter(merge_request).can_cherry_pick_on_current_merge_request?
end
- expose :can_create_note do |issue|
- can?(request.current_user, :create_note, issue.project)
+ expose :can_create_note do |merge_request|
+ can?(request.current_user, :create_note, merge_request)
end
- expose :can_update do |issue|
- can?(request.current_user, :update_issue, issue)
+ expose :can_update do |merge_request|
+ can?(request.current_user, :update_merge_request, merge_request)
end
end
@@ -208,6 +209,10 @@ class MergeRequestWidgetEntity < IssuableEntity
commit_change_content_project_merge_request_path(merge_request.project, merge_request)
end
+ expose :preview_note_path do |merge_request|
+ preview_markdown_path(merge_request.project, quick_actions_target_type: 'MergeRequest', quick_actions_target_id: merge_request.id)
+ end
+
expose :merge_commit_path do |merge_request|
if merge_request.merge_commit_sha
project_commit_path(merge_request.project, merge_request.merge_commit_sha)
diff --git a/app/serializers/note_entity.rb b/app/serializers/note_entity.rb
index 06d603b277e..0e1f94a9f61 100644
--- a/app/serializers/note_entity.rb
+++ b/app/serializers/note_entity.rb
@@ -1,5 +1,6 @@
class NoteEntity < API::Entities::Note
include RequestAwareEntity
+ include NotesHelper
expose :type
@@ -15,16 +16,21 @@ class NoteEntity < API::Entities::Note
expose :current_user do
expose :can_edit do |note|
- Ability.allowed?(request.current_user, :admin_note, note)
+ can?(current_user, :admin_note, note)
end
expose :can_award_emoji do |note|
- Ability.allowed?(request.current_user, :award_emoji, note)
+ can?(current_user, :award_emoji, note)
+ end
+
+ expose :can_resolve do |note|
+ note.resolvable? && can?(current_user, :resolve_note, note)
end
end
expose :resolved?, as: :resolved
expose :resolvable?, as: :resolvable
+
expose :resolved_by, using: NoteUserEntity
expose :system_note_icon_name, if: -> (note, _) { note.system? } do |note|
@@ -42,5 +48,25 @@ class NoteEntity < API::Entities::Note
new_abuse_report_path(user_id: note.author.id, ref_url: Gitlab::UrlBuilder.build(note))
end
+ expose :noteable_note_url do |note|
+ noteable_note_url(note)
+ end
+
+ expose :resolve_path, if: -> (note, _) { note.part_of_discussion? && note.resolvable? } do |note|
+ resolve_project_merge_request_discussion_path(note.project, note.noteable, note.discussion_id)
+ end
+
+ expose :resolve_with_issue_path, if: -> (note, _) { note.part_of_discussion? && note.resolvable? } do |note|
+ new_project_issue_path(note.project, merge_request_to_resolve_discussions_of: note.noteable.iid, discussion_to_resolve: note.discussion_id)
+ end
+
expose :attachment, using: NoteAttachmentEntity, if: -> (note, _) { note.attachment? }
+
+ expose :cached_markdown_version
+
+ private
+
+ def current_user
+ request.current_user
+ end
end
diff --git a/app/serializers/pipeline_details_entity.rb b/app/serializers/pipeline_details_entity.rb
index 130968a44c1..8ba9cac53c4 100644
--- a/app/serializers/pipeline_details_entity.rb
+++ b/app/serializers/pipeline_details_entity.rb
@@ -1,6 +1,6 @@
class PipelineDetailsEntity < PipelineEntity
expose :details do
- expose :legacy_stages, as: :stages, using: StageEntity
+ expose :ordered_stages, as: :stages, using: StageEntity
expose :artifacts, using: BuildArtifactEntity
expose :manual_actions, using: BuildActionEntity
end
diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb
index 7181f8a6b04..17a022539bb 100644
--- a/app/serializers/pipeline_serializer.rb
+++ b/app/serializers/pipeline_serializer.rb
@@ -1,14 +1,11 @@
class PipelineSerializer < BaseSerializer
include WithPagination
-
- InvalidResourceError = Class.new(StandardError)
-
entity PipelineDetailsEntity
def represent(resource, opts = {})
if resource.is_a?(ActiveRecord::Relation)
-
resource = resource.preload([
+ :stages,
:retryable_builds,
:cancelable_statuses,
:trigger_requests,
@@ -20,10 +17,14 @@ class PipelineSerializer < BaseSerializer
end
if paginated?
- super(@paginator.paginate(resource), opts)
- else
- super(resource, opts)
+ resource = paginator.paginate(resource)
end
+
+ if opts.delete(:preload)
+ resource = Gitlab::Ci::Pipeline::Preloader.preload!(resource)
+ end
+
+ super(resource, opts)
end
def represent_status(resource)
@@ -36,7 +37,7 @@ class PipelineSerializer < BaseSerializer
def represent_stages(resource)
return {} unless resource.present?
- data = represent(resource, { only: [{ details: [:stages] }] })
+ data = represent(resource, { only: [{ details: [:stages] }], preload: true })
data.dig(:details, :stages) || []
end
end
diff --git a/app/serializers/runner_entity.rb b/app/serializers/runner_entity.rb
index e9999a36d8a..db26eadab2d 100644
--- a/app/serializers/runner_entity.rb
+++ b/app/serializers/runner_entity.rb
@@ -4,7 +4,7 @@ class RunnerEntity < Grape::Entity
expose :id, :description
expose :edit_path,
- if: -> (*) { can?(request.current_user, :admin_build, project) && runner.specific? } do |runner|
+ if: -> (*) { can?(request.current_user, :admin_build, project) && runner.project_type? } do |runner|
edit_project_runner_path(project, runner)
end
diff --git a/app/serializers/status_entity.rb b/app/serializers/status_entity.rb
index 8e8bda2f9df..47df7f9dcf9 100644
--- a/app/serializers/status_entity.rb
+++ b/app/serializers/status_entity.rb
@@ -7,16 +7,7 @@ class StatusEntity < Grape::Entity
expose :details_path
expose :favicon do |status|
- dir =
- if Gitlab::Utils.to_boolean(ENV['CANARY'])
- File.join('ci_favicons', 'canary')
- elsif Rails.env.development?
- File.join('ci_favicons', 'dev')
- else
- 'ci_favicons'
- end
-
- ActionController::Base.helpers.image_path(File.join(dir, "#{status.favicon}.ico"))
+ Gitlab::Favicon.status_overlay(status.favicon)
end
expose :action, if: -> (status, _) { status.has_action? } do
diff --git a/app/services/application_settings/update_service.rb b/app/services/application_settings/update_service.rb
index d6d3a661dab..7bcb8f49d0d 100644
--- a/app/services/application_settings/update_service.rb
+++ b/app/services/application_settings/update_service.rb
@@ -1,8 +1,14 @@
module ApplicationSettings
class UpdateService < ApplicationSettings::BaseService
+ attr_reader :params, :application_setting
+
def execute
update_terms(@params.delete(:terms))
+ if params.key?(:performance_bar_allowed_group_path)
+ params[:performance_bar_allowed_group_id] = performance_bar_allowed_group_id
+ end
+
@application_setting.update(@params)
end
@@ -18,5 +24,13 @@ module ApplicationSettings
ApplicationSetting::Term.create(terms: terms)
@application_setting.reset_memoized_terms
end
+
+ def performance_bar_allowed_group_id
+ performance_bar_enabled = !params.key?(:performance_bar_enabled) || params.delete(:performance_bar_enabled)
+ group_full_path = params.delete(:performance_bar_allowed_group_path)
+ return nil unless Gitlab::Utils.to_boolean(performance_bar_enabled)
+
+ Group.find_by_full_path(group_full_path)&.id if group_full_path.present?
+ end
end
end
diff --git a/app/services/applications/create_service.rb b/app/services/applications/create_service.rb
index 35d45f25a71..94a434b95dd 100644
--- a/app/services/applications/create_service.rb
+++ b/app/services/applications/create_service.rb
@@ -2,11 +2,10 @@ module Applications
class CreateService
def initialize(current_user, params)
@current_user = current_user
- @params = params
- @ip_address = @params.delete(:ip_address)
+ @params = params.except(:ip_address)
end
- def execute(request = nil)
+ def execute(request)
Doorkeeper::Application.create(@params)
end
end
diff --git a/app/services/badges/update_service.rb b/app/services/badges/update_service.rb
index 7ca84b5df31..495a4a2c99d 100644
--- a/app/services/badges/update_service.rb
+++ b/app/services/badges/update_service.rb
@@ -3,7 +3,7 @@ module Badges
# returns the updated badge
def execute(badge)
if params.present?
- badge.update_attributes(params)
+ badge.update(params)
end
badge
diff --git a/app/services/base_count_service.rb b/app/services/base_count_service.rb
index f2844854112..975e288301c 100644
--- a/app/services/base_count_service.rb
+++ b/app/services/base_count_service.rb
@@ -17,7 +17,7 @@ class BaseCountService
end
def refresh_cache(&block)
- Rails.cache.write(cache_key, block_given? ? yield : uncached_count, raw: raw?)
+ update_cache_for_key(cache_key, &block)
end
def uncached_count
@@ -41,4 +41,8 @@ class BaseCountService
def cache_options
{ raw: raw? }
end
+
+ def update_cache_for_key(key, &block)
+ Rails.cache.write(key, block_given? ? yield : uncached_count, raw: raw?)
+ end
end
diff --git a/app/services/base_service.rb b/app/services/base_service.rb
index 6883ba36c71..3519b7c5e7d 100644
--- a/app/services/base_service.rb
+++ b/app/services/base_service.rb
@@ -3,7 +3,7 @@ class BaseService
attr_accessor :project, :current_user, :params
- def initialize(project, user, params = {})
+ def initialize(project, user = nil, params = {})
@project, @current_user, @params = project, user, params.dup
end
diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb
index ac70a99c2c5..b1dbe73cdf7 100644
--- a/app/services/boards/issues/list_service.rb
+++ b/app/services/boards/issues/list_service.rb
@@ -3,13 +3,18 @@ module Boards
class ListService < Boards::BaseService
def execute
issues = IssuesFinder.new(current_user, filter_params).execute
- issues = without_board_labels(issues) unless movable_list? || closed_list?
- issues = with_list_label(issues) if movable_list?
+ issues = filter(issues)
issues.order_by_position_and_priority
end
private
+ def filter(issues)
+ issues = without_board_labels(issues) unless list&.movable? || list&.closed?
+ issues = with_list_label(issues) if list&.label?
+ issues
+ end
+
def board
@board ||= parent.boards.find(params[:board_id])
end
@@ -20,18 +25,6 @@ module Boards
@list = board.lists.find(params[:id]) if params.key?(:id)
end
- def movable_list?
- return @movable_list if defined?(@movable_list)
-
- @movable_list = list.present? && list.movable?
- end
-
- def closed_list?
- return @closed_list if defined?(@closed_list)
-
- @closed_list = list.present? && list.closed?
- end
-
def filter_params
set_parent
set_state
@@ -63,7 +56,7 @@ module Boards
def without_board_labels(issues)
return issues unless board_label_ids.any?
- issues.where.not(issues_label_links.limit(1).arel.exists)
+ issues.where.not('EXISTS (?)', issues_label_links.limit(1))
end
def issues_label_links
@@ -71,10 +64,8 @@ module Boards
end
def with_list_label(issues)
- issues.where(
- LabelLink.where("label_links.target_type = 'Issue' AND label_links.target_id = issues.id")
- .where("label_links.label_id = ?", list.label_id).limit(1).arel.exists
- )
+ issues.where('EXISTS (?)', LabelLink.where("label_links.target_type = 'Issue' AND label_links.target_id = issues.id")
+ .where("label_links.label_id = ?", list.label_id).limit(1))
end
end
end
diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb
index 3ceab209f3f..ee3112c7571 100644
--- a/app/services/boards/issues/move_service.rb
+++ b/app/services/boards/issues/move_service.rb
@@ -3,7 +3,7 @@ module Boards
class MoveService < Boards::BaseService
def execute(issue)
return false unless can?(current_user, :update_issue, issue)
- return false if issue_params.empty?
+ return false if issue_params(issue).empty?
update(issue)
end
@@ -28,10 +28,10 @@ module Boards
end
def update(issue)
- ::Issues::UpdateService.new(issue.project, current_user, issue_params).execute(issue)
+ ::Issues::UpdateService.new(issue.project, current_user, issue_params(issue)).execute(issue)
end
- def issue_params
+ def issue_params(issue)
attrs = {}
if move_between_lists?
diff --git a/app/services/boards/lists/create_service.rb b/app/services/boards/lists/create_service.rb
index 02f1c709374..6fd9885d4f3 100644
--- a/app/services/boards/lists/create_service.rb
+++ b/app/services/boards/lists/create_service.rb
@@ -1,16 +1,28 @@
module Boards
module Lists
class CreateService < Boards::BaseService
+ include Gitlab::Utils::StrongMemoize
+
def execute(board)
List.transaction do
- label = available_labels_for(board).find(params[:label_id])
+ target = target(board)
position = next_position(board)
- create_list(board, label, position)
+ create_list(board, type, target, position)
end
end
private
+ def type
+ :label
+ end
+
+ def target(board)
+ strong_memoize(:target) do
+ available_labels_for(board).find(params[:label_id])
+ end
+ end
+
def available_labels_for(board)
options = { include_ancestor_groups: true }
@@ -28,8 +40,8 @@ module Boards
max_position.nil? ? 0 : max_position.succ
end
- def create_list(board, label, position)
- board.lists.create(label: label, list_type: :label, position: position)
+ def create_list(board, type, target, position)
+ board.lists.create(type => target, list_type: type, position: position)
end
end
end
diff --git a/app/services/check_gcp_project_billing_service.rb b/app/services/check_gcp_project_billing_service.rb
deleted file mode 100644
index ea82b61b279..00000000000
--- a/app/services/check_gcp_project_billing_service.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-class CheckGcpProjectBillingService
- def execute(token)
- client = GoogleApi::CloudPlatform::Client.new(token, nil)
- client.projects_list.select do |project|
- begin
- client.projects_get_billing_info(project.project_id).billing_enabled
- rescue
- end
- end
- end
-end
diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb
index 4291631913a..6eb1c4f52de 100644
--- a/app/services/ci/register_job_service.rb
+++ b/app/services/ci/register_job_service.rb
@@ -13,9 +13,9 @@ module Ci
@runner = runner
end
- def execute
+ def execute(params = {})
builds =
- if runner.shared?
+ if runner.instance_type?
builds_for_shared_runner
elsif runner.group_type?
builds_for_group_runner
@@ -25,14 +25,12 @@ module Ci
valid = true
- if Feature.enabled?('ci_job_request_with_tags_matcher')
- # pick builds that does not have other tags than runner's one
- builds = builds.matches_tag_ids(runner.tags.ids)
+ # pick builds that does not have other tags than runner's one
+ builds = builds.matches_tag_ids(runner.tags.ids)
- # pick builds that have at least one tag
- unless runner.run_untagged?
- builds = builds.with_any_tags
- end
+ # pick builds that have at least one tag
+ unless runner.run_untagged?
+ builds = builds.with_any_tags
end
builds.find do |build|
@@ -43,6 +41,8 @@ module Ci
# with StateMachines::InvalidTransition or StaleObjectError when doing run! or save method.
begin
build.runner_id = runner.id
+ build.runner_session_attributes = params[:session] if params[:session].present?
+
build.run!
register_success(build)
@@ -89,7 +89,10 @@ module Ci
end
def builds_for_group_runner
- hierarchy_groups = Gitlab::GroupHierarchy.new(runner.groups).base_and_descendants
+ # Workaround for weird Rails bug, that makes `runner.groups.to_sql` to return `runner_id = NULL`
+ groups = ::Group.joins(:runner_namespaces).merge(runner.runner_namespaces)
+
+ hierarchy_groups = Gitlab::GroupHierarchy.new(groups).base_and_descendants
projects = Project.where(namespace_id: hierarchy_groups)
.with_group_runners_enabled
.with_builds_enabled
@@ -98,7 +101,7 @@ module Ci
end
def running_builds_for_shared_runners
- Ci::Build.running.where(runner: Ci::Runner.shared)
+ Ci::Build.running.where(runner: Ci::Runner.instance_type)
.group(:project_id).select(:project_id, 'count(*) AS running_builds')
end
@@ -114,7 +117,7 @@ module Ci
end
def register_success(job)
- labels = { shared_runner: runner.shared?,
+ labels = { shared_runner: runner.instance_type?,
jobs_running_for_project: jobs_running_for_project(job) }
job_queue_duration_seconds.observe(labels, Time.now - job.queued_at) unless job.queued_at.nil?
@@ -122,10 +125,10 @@ module Ci
end
def jobs_running_for_project(job)
- return '+Inf' unless runner.shared?
+ return '+Inf' unless runner.instance_type?
# excluding currently started job
- running_jobs_count = job.project.builds.running.where(runner: Ci::Runner.shared)
+ running_jobs_count = job.project.builds.running.where(runner: Ci::Runner.instance_type)
.limit(JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET + 1).count - 1
running_jobs_count < JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET ? running_jobs_count : "#{JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET}+"
end
diff --git a/app/services/clusters/applications/install_service.rb b/app/services/clusters/applications/install_service.rb
index 4c25a09814b..7ec3a9baa6e 100644
--- a/app/services/clusters/applications/install_service.rb
+++ b/app/services/clusters/applications/install_service.rb
@@ -12,8 +12,8 @@ module Clusters
ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id)
rescue Kubeclient::HttpError => ke
app.make_errored!("Kubernetes error: #{ke.message}")
- rescue StandardError
- app.make_errored!("Can't start installation process")
+ rescue StandardError => e
+ app.make_errored!("Can't start installation process. #{e.message}")
end
end
end
diff --git a/app/services/clusters/applications/schedule_installation_service.rb b/app/services/clusters/applications/schedule_installation_service.rb
index eb8caa68ef7..9c5461e85e1 100644
--- a/app/services/clusters/applications/schedule_installation_service.rb
+++ b/app/services/clusters/applications/schedule_installation_service.rb
@@ -1,21 +1,10 @@
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 execute(application)
+ application.make_scheduled!
- def cluster
- params[:cluster]
+ ClusterInstallAppWorker.perform_async(application.name, application.id)
end
end
end
diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb
index b9d0173a2d0..1ce6ab36cbf 100644
--- a/app/services/commits/change_service.rb
+++ b/app/services/commits/change_service.rb
@@ -13,8 +13,6 @@ module Commits
# rubocop:disable GitlabSecurity/PublicSend
message = @commit.public_send(:"#{action}_message", current_user)
-
- # rubocop:disable GitlabSecurity/PublicSend
repository.public_send(
action,
current_user,
diff --git a/app/services/commits/create_service.rb b/app/services/commits/create_service.rb
index f96f2931508..4d0578becbe 100644
--- a/app/services/commits/create_service.rb
+++ b/app/services/commits/create_service.rb
@@ -17,7 +17,7 @@ module Commits
new_commit = create_commit!
success(result: new_commit)
- rescue ValidationError, ChangeError, Gitlab::Git::Index::IndexError, Gitlab::Git::CommitError, Gitlab::Git::HooksService::PreReceiveError => ex
+ rescue ValidationError, ChangeError, Gitlab::Git::Index::IndexError, Gitlab::Git::CommitError, Gitlab::Git::PreReceiveError => ex
error(ex.message)
end
diff --git a/app/services/concerns/exclusive_lease_guard.rb b/app/services/concerns/exclusive_lease_guard.rb
index 30be6accc32..f45436370c1 100644
--- a/app/services/concerns/exclusive_lease_guard.rb
+++ b/app/services/concerns/exclusive_lease_guard.rb
@@ -47,6 +47,6 @@ module ExclusiveLeaseGuard
end
def log_error(message, extra_args = {})
- logger.error(message)
+ Rails.logger.error(message)
end
end
diff --git a/app/services/concerns/issues/resolve_discussions.rb b/app/services/concerns/issues/resolve_discussions.rb
index 26eb274f4d5..455f761ca9b 100644
--- a/app/services/concerns/issues/resolve_discussions.rb
+++ b/app/services/concerns/issues/resolve_discussions.rb
@@ -14,7 +14,6 @@ module Issues
def merge_request_to_resolve_discussions_of
strong_memoize(:merge_request_to_resolve_discussions_of) do
MergeRequestsFinder.new(current_user, project_id: project.id)
- .execute
.find_by(iid: merge_request_to_resolve_discussions_of_iid)
end
end
diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb
index 0ba6a0ac6b5..9b1a4d960e2 100644
--- a/app/services/create_branch_service.rb
+++ b/app/services/create_branch_service.rb
@@ -14,7 +14,7 @@ class CreateBranchService < BaseService
else
error('Invalid reference name')
end
- rescue Gitlab::Git::HooksService::PreReceiveError => ex
+ rescue Gitlab::Git::PreReceiveError => ex
error(ex.message)
end
diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb
index 1f059c31944..e1499dcee64 100644
--- a/app/services/delete_branch_service.rb
+++ b/app/services/delete_branch_service.rb
@@ -16,7 +16,7 @@ class DeleteBranchService < BaseService
else
error('Failed to remove branch')
end
- rescue Gitlab::Git::HooksService::PreReceiveError => ex
+ rescue Gitlab::Git::PreReceiveError => ex
error(ex.message)
end
diff --git a/app/services/import_export_clean_up_service.rb b/app/services/import_export_clean_up_service.rb
index 74088b970c9..3702c3742ef 100644
--- a/app/services/import_export_clean_up_service.rb
+++ b/app/services/import_export_clean_up_service.rb
@@ -10,7 +10,9 @@ class ImportExportCleanUpService
def execute
Gitlab::Metrics.measure(:import_export_clean_up) do
- next unless File.directory?(path)
+ clean_up_export_object_files
+
+ break unless File.directory?(path)
clean_up_export_files
end
@@ -21,4 +23,11 @@ class ImportExportCleanUpService
def clean_up_export_files
Gitlab::Popen.popen(%W(find #{path} -not -path #{path} -mmin +#{mmin} -delete))
end
+
+ def clean_up_export_object_files
+ ImportExportUpload.where('updated_at < ?', mmin.minutes.ago).each do |upload|
+ upload.remove_export_file!
+ upload.save!
+ end
+ end
end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 1f67e3ecf9d..5e06e0c61cf 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -130,7 +130,7 @@ class IssuableBaseService < BaseService
def create_issuable(issuable, attributes, label_ids:)
issuable.with_transaction_returning_status do
if issuable.save
- issuable.update_attributes(label_ids: label_ids)
+ issuable.update(label_ids: label_ids)
end
end
end
@@ -183,7 +183,10 @@ class IssuableBaseService < BaseService
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)
+ if labels_changing?(issuable.label_ids, label_ids)
+ params[:label_ids] = label_ids
+ issuable.touch
+ end
if issuable.changed? || params.present?
issuable.assign_attributes(params.merge(updated_by: current_user))
diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb
index 9f6cfc0f6d3..cbfef175af0 100644
--- a/app/services/issues/base_service.rb
+++ b/app/services/issues/base_service.rb
@@ -32,8 +32,9 @@ module Issues
def filter_assignee(issuable)
return if params[:assignee_ids].blank?
- # The number of assignees is limited by one for GitLab CE
- params[:assignee_ids] = params[:assignee_ids][0, 1]
+ unless issuable.allows_multiple_assignees?
+ params[:assignee_ids] = params[:assignee_ids].take(1)
+ end
assignee_ids = params[:assignee_ids].select { |assignee_id| assignee_can_read?(issuable, assignee_id) }
diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb
index 78e79344c99..6e5c29a5c40 100644
--- a/app/services/issues/move_service.rb
+++ b/app/services/issues/move_service.rb
@@ -58,7 +58,8 @@ module Issues
def cloneable_label_ids
params = {
project_id: @new_project.id,
- title: @old_issue.labels.pluck(:title)
+ title: @old_issue.labels.pluck(:title),
+ include_ancestor_groups: true
}
LabelsFinder.new(current_user, params).execute.pluck(:id)
diff --git a/app/services/labels/find_or_create_service.rb b/app/services/labels/find_or_create_service.rb
index 079f611b3f3..a72da3c637f 100644
--- a/app/services/labels/find_or_create_service.rb
+++ b/app/services/labels/find_or_create_service.rb
@@ -20,6 +20,7 @@ module Labels
@available_labels ||= LabelsFinder.new(
current_user,
"#{parent_type}_id".to_sym => parent.id,
+ include_ancestor_groups: include_ancestor_groups?,
only_group_labels: parent_is_group?
).execute(skip_authorization: skip_authorization)
end
@@ -30,7 +31,8 @@ module Labels
new_label = available_labels.find_by(title: title)
if new_label.nil? && (skip_authorization || Ability.allowed?(current_user, :admin_label, parent))
- new_label = Labels::CreateService.new(params).execute(parent_type.to_sym => parent)
+ create_params = params.except(:include_ancestor_groups)
+ new_label = Labels::CreateService.new(create_params).execute(parent_type.to_sym => parent)
end
new_label
@@ -47,5 +49,9 @@ module Labels
def parent_is_group?
parent_type == "group"
end
+
+ def include_ancestor_groups?
+ params[:include_ancestor_groups] == true
+ end
end
end
diff --git a/app/services/lfs/unlock_file_service.rb b/app/services/lfs/unlock_file_service.rb
index 7eb89339a92..7e3edf21d54 100644
--- a/app/services/lfs/unlock_file_service.rb
+++ b/app/services/lfs/unlock_file_service.rb
@@ -24,7 +24,7 @@ module Lfs
success(lock: lock, http_status: :ok)
elsif forced
- error(_('You must have master access to force delete a lock'), 403)
+ error(_('You must have maintainer access to force delete a lock'), 403)
else
error(_("%{lock_path} is locked by GitLab User %{lock_user_id}") % { lock_path: lock.path, lock_user_id: lock.user_id }, 403)
end
diff --git a/app/services/members/update_service.rb b/app/services/members/update_service.rb
index 48b3d59f7bd..cb19cf01dd7 100644
--- a/app/services/members/update_service.rb
+++ b/app/services/members/update_service.rb
@@ -6,7 +6,7 @@ module Members
old_access_level = member.human_access
- if member.update_attributes(params)
+ if member.update(params)
after_execute(action: permission, old_access_level: old_access_level, member: member)
end
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index 231ab76fde4..4c420b38258 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -38,8 +38,8 @@ module MergeRequests
def filter_params(merge_request)
super
- unless merge_request.can_allow_maintainer_to_push?(current_user)
- params.delete(:allow_maintainer_to_push)
+ unless merge_request.can_allow_collaboration?(current_user)
+ params.delete(:allow_collaboration)
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 cf687b71d16..3407b312700 100644
--- a/app/services/merge_requests/create_from_issue_service.rb
+++ b/app/services/merge_requests/create_from_issue_service.rb
@@ -41,7 +41,9 @@ module MergeRequests
end
def ref
- @ref || project.default_branch || 'master'
+ return @ref if project.repository.branch_exists?(@ref)
+
+ project.default_branch || 'master'
end
def merge_request
diff --git a/app/services/merge_requests/delete_non_latest_diffs_service.rb b/app/services/merge_requests/delete_non_latest_diffs_service.rb
new file mode 100644
index 00000000000..40079b21189
--- /dev/null
+++ b/app/services/merge_requests/delete_non_latest_diffs_service.rb
@@ -0,0 +1,18 @@
+module MergeRequests
+ class DeleteNonLatestDiffsService
+ BATCH_SIZE = 10
+
+ def initialize(merge_request)
+ @merge_request = merge_request
+ end
+
+ def execute
+ diffs = @merge_request.non_latest_diffs.with_files
+
+ diffs.each_batch(of: BATCH_SIZE) do |relation, index|
+ ids = relation.pluck(:id).map { |id| [id] }
+ DeleteDiffFilesWorker.bulk_perform_in(index * 5.minutes, ids)
+ end
+ end
+ end
+end
diff --git a/app/services/merge_requests/ff_merge_service.rb b/app/services/merge_requests/ff_merge_service.rb
index ba6853b835a..bffc09c34f0 100644
--- a/app/services/merge_requests/ff_merge_service.rb
+++ b/app/services/merge_requests/ff_merge_service.rb
@@ -13,7 +13,7 @@ module MergeRequests
source,
merge_request.target_branch,
merge_request: merge_request)
- rescue Gitlab::Git::HooksService::PreReceiveError => e
+ rescue Gitlab::Git::PreReceiveError => e
raise MergeError, e.message
rescue StandardError => e
raise MergeError, "Something went wrong during merge: #{e.message}"
diff --git a/app/services/merge_requests/merge_request_diff_cache_service.rb b/app/services/merge_requests/merge_request_diff_cache_service.rb
deleted file mode 100644
index 10aa9ae609c..00000000000
--- a/app/services/merge_requests/merge_request_diff_cache_service.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-module MergeRequests
- class MergeRequestDiffCacheService
- def execute(merge_request, new_diff)
- # Executing the iteration we cache all the highlighted diff information
- merge_request.diffs.diff_files.to_a
-
- # Remove cache for all diffs on this MR. Do not use the association on the
- # model, as that will interfere with other actions happening when
- # reloading the diff.
- MergeRequestDiff.where(merge_request: merge_request).each do |merge_request_diff|
- next if merge_request_diff == new_diff
-
- merge_request_diff.diffs.clear_cache!
- end
- end
- end
-end
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index 2209a60a840..3d587f97906 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -34,6 +34,19 @@ module MergeRequests
handle_merge_error(log_message: e.message, save_message_on_model: true)
end
+ def source
+ return merge_request.diff_head_sha unless merge_request.squash
+
+ squash_result = ::MergeRequests::SquashService.new(project, current_user, params).execute(merge_request)
+
+ case squash_result[:status]
+ when :success
+ squash_result[:squash_sha]
+ when :error
+ raise ::MergeRequests::MergeService::MergeError, squash_result[:message]
+ end
+ end
+
private
def error_check!
@@ -66,7 +79,7 @@ module MergeRequests
message = params[:commit_message] || merge_request.merge_commit_message
repository.merge(current_user, source, merge_request, message)
- rescue Gitlab::Git::HooksService::PreReceiveError => e
+ rescue Gitlab::Git::PreReceiveError => e
handle_merge_error(log_message: e.message)
raise MergeError, 'Something went wrong during merge pre-receive hook'
rescue => e
@@ -116,9 +129,5 @@ module MergeRequests
def merge_request_info
merge_request.to_reference(full: true)
end
-
- def source
- @source ||= @merge_request.diff_head_sha
- end
end
end
diff --git a/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb b/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb
index 850deb0ac7a..9a4e6eb2e88 100644
--- a/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb
+++ b/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb
@@ -24,11 +24,7 @@ module MergeRequests
pipeline_merge_requests(pipeline) do |merge_request|
next unless merge_request.merge_when_pipeline_succeeds?
-
- unless merge_request.mergeable?
- todo_service.merge_request_became_unmergeable(merge_request)
- next
- end
+ next unless merge_request.mergeable?
merge_request.merge_async(merge_request.merge_user_id, merge_request.merge_params)
end
diff --git a/app/services/merge_requests/post_merge_service.rb b/app/services/merge_requests/post_merge_service.rb
index c78e78afcd1..7606d68ff29 100644
--- a/app/services/merge_requests/post_merge_service.rb
+++ b/app/services/merge_requests/post_merge_service.rb
@@ -6,15 +6,16 @@ module MergeRequests
#
class PostMergeService < MergeRequests::BaseService
def execute(merge_request)
+ merge_request.mark_as_merged
close_issues(merge_request)
todo_service.merge_merge_request(merge_request, current_user)
- merge_request.mark_as_merged
create_event(merge_request)
create_note(merge_request)
notification_service.merge_mr(merge_request, current_user)
execute_hooks(merge_request, 'merge')
invalidate_cache_counts(merge_request, users: merge_request.assignees)
merge_request.update_project_counter_caches
+ delete_non_latest_diffs(merge_request)
end
private
@@ -31,6 +32,10 @@ module MergeRequests
end
end
+ def delete_non_latest_diffs(merge_request)
+ DeleteNonLatestDiffsService.new(merge_request).execute
+ end
+
def create_merge_event(merge_request, current_user)
EventCreateService.new.merge_mr(merge_request, current_user)
end
diff --git a/app/services/merge_requests/rebase_service.rb b/app/services/merge_requests/rebase_service.rb
index c0083cd6afd..c741e913860 100644
--- a/app/services/merge_requests/rebase_service.rb
+++ b/app/services/merge_requests/rebase_service.rb
@@ -18,9 +18,17 @@ module MergeRequests
return false
end
+ log_prefix = "#{self.class.name} info (#{merge_request.to_reference(full: true)}):"
+
+ Gitlab::GitLogger.info("#{log_prefix} rebase started")
+
rebase_sha = repository.rebase(current_user, merge_request)
- merge_request.update_attributes(rebase_commit_sha: rebase_sha)
+ Gitlab::GitLogger.info("#{log_prefix} rebased to #{rebase_sha}")
+
+ merge_request.update(rebase_commit_sha: rebase_sha)
+
+ Gitlab::GitLogger.info("#{log_prefix} rebase SHA saved: #{rebase_sha}")
true
rescue => e
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index 1fb1796b56c..0127d781686 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -3,6 +3,12 @@ module MergeRequests
def execute(oldrev, newrev, ref)
return true unless Gitlab::Git.branch_ref?(ref)
+ do_execute(oldrev, newrev, ref)
+ end
+
+ private
+
+ def do_execute(oldrev, newrev, ref)
@oldrev, @newrev = oldrev, newrev
@branch_name = Gitlab::Git.ref_name(ref)
@@ -28,8 +34,6 @@ module MergeRequests
true
end
- private
-
def close_upon_missing_source_branch_ref
# MergeRequest#reload_diff ignores not opened MRs. This means it won't
# create an `empty` diff for `closed` MRs without a source branch, keeping
diff --git a/app/services/merge_requests/reload_diffs_service.rb b/app/services/merge_requests/reload_diffs_service.rb
new file mode 100644
index 00000000000..2ec7b403903
--- /dev/null
+++ b/app/services/merge_requests/reload_diffs_service.rb
@@ -0,0 +1,43 @@
+module MergeRequests
+ class ReloadDiffsService
+ def initialize(merge_request, current_user)
+ @merge_request = merge_request
+ @current_user = current_user
+ end
+
+ def execute
+ old_diff_refs = merge_request.diff_refs
+ new_diff = merge_request.create_merge_request_diff
+
+ clear_cache(new_diff)
+ update_diff_discussion_positions(old_diff_refs)
+ end
+
+ private
+
+ attr_reader :merge_request, :current_user
+
+ def update_diff_discussion_positions(old_diff_refs)
+ new_diff_refs = merge_request.diff_refs
+
+ merge_request.update_diff_discussion_positions(old_diff_refs: old_diff_refs,
+ new_diff_refs: new_diff_refs,
+ current_user: current_user)
+ end
+
+ def clear_cache(new_diff)
+ # Executing the iteration we cache highlighted diffs for each diff file of
+ # MergeRequestDiff.
+ new_diff.diffs_collection.diff_files.to_a
+
+ # Remove cache for all diffs on this MR. Do not use the association on the
+ # model, as that will interfere with other actions happening when
+ # reloading the diff.
+ MergeRequestDiff.where(merge_request: merge_request).each do |merge_request_diff|
+ next if merge_request_diff == new_diff
+
+ merge_request_diff.diffs_collection.clear_cache!
+ end
+ end
+ end
+end
diff --git a/app/services/merge_requests/squash_service.rb b/app/services/merge_requests/squash_service.rb
new file mode 100644
index 00000000000..a40fb2786bd
--- /dev/null
+++ b/app/services/merge_requests/squash_service.rb
@@ -0,0 +1,28 @@
+module MergeRequests
+ class SquashService < MergeRequests::WorkingCopyBaseService
+ def execute(merge_request)
+ @merge_request = merge_request
+ @repository = target_project.repository
+
+ squash || error('Failed to squash. Should be done manually.')
+ end
+
+ def squash
+ if merge_request.commits_count < 2
+ return success(squash_sha: merge_request.diff_head_sha)
+ end
+
+ if merge_request.squash_in_progress?
+ return error('Squash task canceled: another squash is already in progress.')
+ end
+
+ squash_sha = repository.squash(current_user, merge_request)
+
+ success(squash_sha: squash_sha)
+ rescue => e
+ log_error("Failed to squash merge request #{merge_request.to_reference(full: true)}:")
+ log_error(e.message)
+ false
+ end
+ end
+end
diff --git a/app/services/metrics_service.rb b/app/services/metrics_service.rb
index 236e9fe8c44..51ff9eff5e4 100644
--- a/app/services/metrics_service.rb
+++ b/app/services/metrics_service.rb
@@ -6,7 +6,8 @@ class MetricsService
Gitlab::HealthChecks::Redis::RedisCheck,
Gitlab::HealthChecks::Redis::CacheCheck,
Gitlab::HealthChecks::Redis::QueuesCheck,
- Gitlab::HealthChecks::Redis::SharedStateCheck
+ Gitlab::HealthChecks::Redis::SharedStateCheck,
+ Gitlab::HealthChecks::GitalyCheck
].freeze
def prometheus_metrics_text
diff --git a/app/services/milestones/update_service.rb b/app/services/milestones/update_service.rb
index 31b441ed476..74edbf9b41d 100644
--- a/app/services/milestones/update_service.rb
+++ b/app/services/milestones/update_service.rb
@@ -11,7 +11,7 @@ module Milestones
end
if params.present?
- milestone.update_attributes(params.except(:state_event))
+ milestone.update(params.except(:state_event))
end
milestone
diff --git a/app/services/notes/update_service.rb b/app/services/notes/update_service.rb
index 75fd08ea0a9..e16ef398184 100644
--- a/app/services/notes/update_service.rb
+++ b/app/services/notes/update_service.rb
@@ -5,7 +5,7 @@ module Notes
old_mentioned_users = note.mentioned_users.to_a
- note.update_attributes(params.merge(updated_by: current_user))
+ note.update(params.merge(updated_by: current_user))
note.create_new_cross_references!(current_user)
if note.previous_changes.include?('note')
diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb
index 5658699664d..d9834fd0ccc 100644
--- a/app/services/notification_recipient_service.rb
+++ b/app/services/notification_recipient_service.rb
@@ -10,12 +10,16 @@ module NotificationRecipientService
NotificationRecipient.new(user, *args).notifiable?
end
- def self.build_recipients(*a)
- Builder::Default.new(*a).notification_recipients
+ def self.build_recipients(*args)
+ Builder::Default.new(*args).notification_recipients
end
- def self.build_new_note_recipients(*a)
- Builder::NewNote.new(*a).notification_recipients
+ def self.build_new_note_recipients(*args)
+ Builder::NewNote.new(*args).notification_recipients
+ end
+
+ def self.build_merge_request_unmergeable_recipients(*args)
+ Builder::MergeRequestUnmergeable.new(*args).notification_recipients
end
module Builder
@@ -40,7 +44,6 @@ module NotificationRecipientService
raise 'abstract'
end
- # rubocop:disable Rails/Delegate
def project
target.project
end
@@ -330,5 +333,26 @@ module NotificationRecipientService
note.author
end
end
+
+ class MergeRequestUnmergeable < Base
+ attr_reader :target
+ def initialize(merge_request)
+ @target = merge_request
+ end
+
+ def build!
+ target.merge_participants.each do |user|
+ add_recipients(user, :participating, nil)
+ end
+ end
+
+ def custom_action
+ :unmergeable_merge_request
+ end
+
+ def acting_user
+ nil
+ end
+ end
end
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 55a1735e54b..d7be9a925b5 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -129,11 +129,14 @@ class NotificationService
# When create a merge request we should send an email to:
#
+ # * mr author
# * mr assignee if their notification level is not Disabled
# * project team members with notification level higher then Participating
# * watchers of the mr's labels
# * users with custom level checked with "new merge request"
#
+ # In EE, approvers of the merge request are also included
+ #
def new_merge_request(merge_request, current_user)
new_resource_email(merge_request, :new_merge_request_email)
end
@@ -148,6 +151,15 @@ class NotificationService
end
end
+ # When a merge request is found to be unmergeable, we should send an email to:
+ #
+ # * mr author
+ # * mr merge user if set
+ #
+ def merge_request_unmergeable(merge_request)
+ merge_request_unmergeable_email(merge_request)
+ end
+
# When merge request text is updated, we should send an email to:
#
# * newly mentioned project team members with notification level higher than Participating
@@ -246,6 +258,10 @@ class NotificationService
# ignore gitlab service messages
return true if note.cross_reference? && note.system?
+ send_new_note_notifications(note)
+ end
+
+ def send_new_note_notifications(note)
notify_method = "note_#{note.to_ability_name}_email".to_sym
recipients = NotificationRecipientService.build_new_note_recipients(note)
@@ -258,9 +274,9 @@ class NotificationService
def new_access_request(member)
return true unless member.notifiable?(:subscription)
- recipients = member.source.members.active_without_invites_and_requests.owners_and_masters
- if fallback_to_group_owners_masters?(recipients, member)
- recipients = member.source.group.members.active_without_invites_and_requests.owners_and_masters
+ recipients = member.source.members.active_without_invites_and_requests.owners_and_maintainers
+ if fallback_to_group_owners_maintainers?(recipients, member)
+ recipients = member.source.group.members.active_without_invites_and_requests.owners_and_maintainers
end
recipients.each { |recipient| deliver_access_request_email(recipient, member) }
@@ -484,6 +500,14 @@ class NotificationService
end
end
+ def merge_request_unmergeable_email(merge_request)
+ recipients = NotificationRecipientService.build_merge_request_unmergeable_recipients(merge_request)
+
+ recipients.each do |recipient|
+ mailer.merge_request_unmergeable_email(recipient.user.id, merge_request.id).deliver_later
+ end
+ end
+
def mailer
Notify
end
@@ -495,7 +519,7 @@ class NotificationService
return [] unless project
- notifiable_users(project.team.masters, :watch, target: project)
+ notifiable_users(project.team.maintainers, :watch, target: project)
end
def notifiable?(*args)
@@ -510,7 +534,7 @@ class NotificationService
mailer.member_access_requested_email(member.real_source_type, member.id, recipient.user.notification_email).deliver_later
end
- def fallback_to_group_owners_masters?(recipients, member)
+ def fallback_to_group_owners_maintainers?(recipients, member)
return false if recipients.present?
member.source.respond_to?(:group) && member.source.group
diff --git a/app/services/pages_service.rb b/app/services/pages_service.rb
deleted file mode 100644
index 446eeb34d3b..00000000000
--- a/app/services/pages_service.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-class PagesService
- attr_reader :data
-
- def initialize(data)
- @data = data
- end
-
- def execute
- return unless Settings.pages.enabled
- return unless data[:build_name] == 'pages'
- return unless data[:build_status] == 'success'
-
- PagesWorker.perform_async(:deploy, data[:build_id])
- end
-end
diff --git a/app/services/preview_markdown_service.rb b/app/services/preview_markdown_service.rb
index 4ee2c1796bd..6da4d9523cf 100644
--- a/app/services/preview_markdown_service.rb
+++ b/app/services/preview_markdown_service.rb
@@ -6,7 +6,8 @@ class PreviewMarkdownService < BaseService
success(
text: text,
users: users,
- commands: commands.join(' ')
+ commands: commands.join(' '),
+ markdown_engine: markdown_engine
)
end
@@ -42,4 +43,8 @@ class PreviewMarkdownService < BaseService
def commands_target_id
params[:quick_actions_target_id]
end
+
+ def markdown_engine
+ CacheMarkdownField::MarkdownEngine.from_version(params[:markdown_version].to_i)
+ end
end
diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb
index 346971138b1..9d0eaaf3152 100644
--- a/app/services/projects/autocomplete_service.rb
+++ b/app/services/projects/autocomplete_service.rb
@@ -11,7 +11,7 @@ module Projects
order: { due_date: :asc, title: :asc }
}
- finder_params[:group_ids] = [@project.group.id] if @project.group
+ finder_params[:group_ids] = @project.group.self_and_ancestors_ids if @project.group
MilestonesFinder.new(finder_params).execute.select([:iid, :title])
end
@@ -20,24 +20,28 @@ module Projects
MergeRequestsFinder.new(current_user, project_id: project.id, state: 'opened').execute.select([:iid, :title])
end
- def labels(target = nil)
- labels = LabelsFinder.new(current_user, project_id: project.id, include_ancestor_groups: true)
- .execute.select([:color, :title])
-
- return labels unless target&.respond_to?(:labels)
-
- issuable_label_titles = target.labels.pluck(:title)
-
- if issuable_label_titles
- labels = labels.as_json(only: [:title, :color])
-
- issuable_label_titles.each do |issuable_label_title|
- found_label = labels.find { |label| label['title'] == issuable_label_title }
- found_label[:set] = true if found_label
+ def labels_as_hash(target = nil)
+ available_labels = LabelsFinder.new(
+ current_user,
+ project_id: project.id,
+ include_ancestor_groups: true
+ ).execute
+
+ label_hashes = available_labels.as_json(only: [:title, :color])
+
+ if target&.respond_to?(:labels)
+ already_set_labels = available_labels & target.labels
+ if already_set_labels.present?
+ titles = already_set_labels.map(&:title)
+ label_hashes.each do |hash|
+ if titles.include?(hash['title'])
+ hash[:set] = true
+ end
+ end
end
end
- labels
+ label_hashes
end
def commands(noteable, type)
diff --git a/app/services/projects/count_service.rb b/app/services/projects/count_service.rb
index 933829b557b..4c8e000928f 100644
--- a/app/services/projects/count_service.rb
+++ b/app/services/projects/count_service.rb
@@ -22,8 +22,10 @@ module Projects
)
end
- def cache_key
- ['projects', 'count_service', VERSION, @project.id, cache_key_name]
+ def cache_key(key = nil)
+ cache_key = key || cache_key_name
+
+ ['projects', 'count_service', VERSION, @project.id, cache_key]
end
def self.query(project_ids)
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index d16ecdb7b9b..85491089d8e 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -2,6 +2,8 @@ module Projects
class CreateService < BaseService
def initialize(user, params)
@current_user, @params = user, params.dup
+ @skip_wiki = @params.delete(:skip_wiki)
+ @initialize_with_readme = Gitlab::Utils.to_boolean(@params.delete(:initialize_with_readme))
end
def execute
@@ -11,7 +13,6 @@ module Projects
forked_from_project_id = params.delete(:forked_from_project_id)
import_data = params.delete(:import_data)
- @skip_wiki = params.delete(:skip_wiki)
@project = Project.new(params)
@@ -46,6 +47,9 @@ module Projects
yield(@project) if block_given?
+ # If the block added errors, don't try to save the project
+ return @project if @project.errors.any?
+
@project.creator = current_user
if forked_from_project_id
@@ -63,6 +67,7 @@ module Projects
message = "Unable to save #{e.record.type}: #{e.record.errors.full_messages.join(", ")} "
fail(error: message)
rescue => e
+ @project.errors.add(:base, e.message) if @project
fail(error: e.message)
end
@@ -98,6 +103,8 @@ module Projects
setup_authorizations
current_user.invalidate_personal_projects_count
+
+ create_readme if @initialize_with_readme
end
# Refresh the current user's authorizations inline (so they can access the
@@ -108,10 +115,21 @@ module Projects
@project.group.refresh_members_authorized_projects(blocking: false)
current_user.refresh_authorized_projects
else
- @project.add_master(@project.namespace.owner, current_user: current_user)
+ @project.add_maintainer(@project.namespace.owner, current_user: current_user)
end
end
+ def create_readme
+ commit_attrs = {
+ branch_name: 'master',
+ commit_message: 'Initial commit',
+ file_path: 'README.md',
+ file_content: "# #{@project.name}\n\n#{@project.description}"
+ }
+
+ Files::CreateService.new(@project, current_user, commit_attrs).execute
+ end
+
def skip_wiki?
!@project.feature_available?(:wiki, current_user) || @skip_wiki
end
@@ -141,7 +159,6 @@ module Projects
Rails.logger.error(log_message)
if @project
- @project.errors.add(:base, message)
@project.mark_import_as_failed(message) if @project.persisted? && @project.import?
end
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index adbc498d0bf..87173cc79ec 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -51,11 +51,11 @@ module Projects
flush_caches(@project)
- unless mv_repository(removal_path(repo_path), repo_path)
+ unless rollback_repository(removal_path(repo_path), repo_path)
raise_error('Failed to restore project repository. Please contact the administrator.')
end
- unless mv_repository(removal_path(wiki_path), wiki_path)
+ unless rollback_repository(removal_path(wiki_path), wiki_path)
raise_error('Failed to restore wiki repository. Please contact the administrator.')
end
end
@@ -84,6 +84,9 @@ module Projects
# Skip repository removal. We use this flag when remove user or group
return true if params[:skip_repo] == true
+ # There is a possibility project does not have repository or wiki
+ return true unless repo_exists?(path)
+
new_path = removal_path(path)
if mv_repository(path, new_path)
@@ -98,8 +101,18 @@ module Projects
end
end
- def mv_repository(from_path, to_path)
+ def rollback_repository(old_path, new_path)
# There is a possibility project does not have repository or wiki
+ return true unless repo_exists?(old_path)
+
+ mv_repository(old_path, new_path)
+ end
+
+ def repo_exists?(path)
+ gitlab_shell.exists?(project.repository_storage, path + '.git')
+ end
+
+ def mv_repository(from_path, to_path)
return true unless gitlab_shell.exists?(project.repository_storage, from_path + '.git')
gitlab_shell.mv_repository(project.repository_storage, from_path, to_path)
@@ -111,7 +124,7 @@ module Projects
# It's possible that the project was destroyed, but some after_commit
# hook failed and caused us to end up here. A destroyed model will be a frozen hash,
# which cannot be altered.
- project.update_attributes(delete_error: message, pending_delete: false) unless project.destroyed?
+ project.update(delete_error: message, pending_delete: false) unless project.destroyed?
log_error("Deletion failed on #{project.full_path} with the following message: #{message}")
end
@@ -122,13 +135,24 @@ module Projects
raise_error('Failed to remove some tags in project container registry. Please try again or contact administrator.')
end
+ log_destroy_event
trash_repositories!
- project.team.truncate
+ # Rails attempts to load all related records into memory before
+ # destroying: https://github.com/rails/rails/issues/22510
+ # This ensures we delete records in batches.
+ #
+ # Exclude container repositories because its before_destroy would be
+ # called multiple times, and it doesn't destroy any database records.
+ project.destroy_dependent_associations_in_batches(exclude: [:container_repositories])
project.destroy!
end
end
+ def log_destroy_event
+ log_info("Attempting to destroy #{project.full_path} (#{project.id})")
+ end
+
##
# This method makes sure that we correctly remove registry tags
# for legacy image repository (when repository path equals project path).
diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb
index 348eb0bf8d8..a8aafa9fb4f 100644
--- a/app/services/projects/fork_service.rb
+++ b/app/services/projects/fork_service.rb
@@ -37,7 +37,7 @@ module Projects
return new_project unless new_project.persisted?
builds_access_level = @project.project_feature.builds_access_level
- new_project.project_feature.update_attributes(builds_access_level: builds_access_level)
+ new_project.project_feature.update(builds_access_level: builds_access_level)
link_fork_network(new_project)
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index bdd9598f85a..1781a01cbd4 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -17,6 +17,8 @@ module Projects
def execute
add_repository_to_project
+ download_lfs_objects
+
import_data
success
@@ -29,7 +31,7 @@ module Projects
def add_repository_to_project
if project.external_import? && !unknown_url?
begin
- Gitlab::UrlBlocker.validate!(project.import_url, valid_ports: Project::VALID_IMPORT_PORTS)
+ Gitlab::UrlBlocker.validate!(project.import_url, ports: Project::VALID_IMPORT_PORTS)
rescue Gitlab::UrlBlocker::BlockedUrlError => e
raise Error, "Blocked import URL: #{e.message}"
end
@@ -37,7 +39,7 @@ module Projects
# 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?)
+ return if importer_imports_repository?
if unknown_url?
# In this case, we only want to import issues, not a repository.
@@ -73,6 +75,27 @@ module Projects
end
end
+ def download_lfs_objects
+ # In this case, we only want to import issues
+ return if unknown_url?
+
+ # If it has its own repository importer, it has to implements its own lfs import download
+ return if importer_imports_repository?
+
+ return unless project.lfs_enabled?
+
+ oids_to_download = Projects::LfsPointers::LfsImportService.new(project).execute
+ download_service = Projects::LfsPointers::LfsDownloadService.new(project)
+
+ oids_to_download.each do |oid, link|
+ download_service.execute(oid, link)
+ end
+ rescue => e
+ # Right now, to avoid aborting the importing process, we silently fail
+ # if any exception raises.
+ Rails.logger.error("The Lfs import process failed. #{e.message}")
+ end
+
def import_data
return unless has_importer?
@@ -98,5 +121,9 @@ module Projects
def unknown_url?
project.import_url == Project::UNKNOWN_IMPORT_URL
end
+
+ def importer_imports_repository?
+ has_importer? && importer_class.try(:imports_repository?)
+ end
end
end
diff --git a/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb b/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb
new file mode 100644
index 00000000000..d9fb74b090e
--- /dev/null
+++ b/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb
@@ -0,0 +1,93 @@
+# This service lists the download link from a remote source based on the
+# oids provided
+module Projects
+ module LfsPointers
+ class LfsDownloadLinkListService < BaseService
+ DOWNLOAD_ACTION = 'download'.freeze
+
+ DownloadLinksError = Class.new(StandardError)
+ DownloadLinkNotFound = Class.new(StandardError)
+
+ attr_reader :remote_uri
+
+ def initialize(project, remote_uri: nil)
+ super(project)
+
+ @remote_uri = remote_uri
+ end
+
+ # This method accepts two parameters:
+ # - oids: hash of oids to query. The structure is { lfs_file_oid => lfs_file_size }
+ #
+ # Returns a hash with the structure { lfs_file_oids => download_link }
+ def execute(oids)
+ return {} unless project&.lfs_enabled? && remote_uri && oids.present?
+
+ get_download_links(oids)
+ end
+
+ private
+
+ def get_download_links(oids)
+ response = Gitlab::HTTP.post(remote_uri,
+ body: request_body(oids),
+ headers: headers)
+
+ raise DownloadLinksError, response.message unless response.success?
+
+ parse_response_links(response['objects'])
+ end
+
+ def parse_response_links(objects_response)
+ objects_response.each_with_object({}) do |entry, link_list|
+ begin
+ oid = entry['oid']
+ link = entry.dig('actions', DOWNLOAD_ACTION, 'href')
+
+ raise DownloadLinkNotFound unless link
+
+ link_list[oid] = add_credentials(link)
+ rescue DownloadLinkNotFound, URI::InvalidURIError
+ Rails.logger.error("Link for Lfs Object with oid #{oid} not found or invalid.")
+ end
+ end
+ end
+
+ def request_body(oids)
+ {
+ operation: DOWNLOAD_ACTION,
+ objects: oids.map { |oid, size| { oid: oid, size: size } }
+ }.to_json
+ end
+
+ def headers
+ {
+ 'Accept' => LfsRequest::CONTENT_TYPE,
+ 'Content-Type' => LfsRequest::CONTENT_TYPE
+ }.freeze
+ end
+
+ def add_credentials(link)
+ uri = URI.parse(link)
+
+ if should_add_credentials?(uri)
+ uri.user = remote_uri.user
+ uri.password = remote_uri.password
+ end
+
+ uri.to_s
+ end
+
+ # The download link can be a local url or an object storage url
+ # If the download link has the some host as the import url then
+ # we add the same credentials because we may need them
+ def should_add_credentials?(link_uri)
+ url_credentials? && link_uri.host == remote_uri.host
+ end
+
+ def url_credentials?
+ remote_uri.user.present? || remote_uri.password.present?
+ end
+ end
+ end
+end
diff --git a/app/services/projects/lfs_pointers/lfs_download_service.rb b/app/services/projects/lfs_pointers/lfs_download_service.rb
new file mode 100644
index 00000000000..618c30b971f
--- /dev/null
+++ b/app/services/projects/lfs_pointers/lfs_download_service.rb
@@ -0,0 +1,58 @@
+# This service downloads and links lfs objects from a remote URL
+module Projects
+ module LfsPointers
+ class LfsDownloadService < BaseService
+ def execute(oid, url)
+ return unless project&.lfs_enabled? && oid.present? && url.present?
+
+ return if LfsObject.exists?(oid: oid)
+
+ sanitized_uri = Gitlab::UrlSanitizer.new(url)
+
+ with_tmp_file(oid) do |file|
+ size = download_and_save_file(file, sanitized_uri)
+ lfs_object = LfsObject.new(oid: oid, size: size, file: file)
+
+ project.all_lfs_objects << lfs_object
+ end
+ rescue StandardError => e
+ Rails.logger.error("LFS file with oid #{oid} could't be downloaded from #{sanitized_uri.sanitized_url}: #{e.message}")
+ end
+
+ private
+
+ def download_and_save_file(file, sanitized_uri)
+ IO.copy_stream(open(sanitized_uri.sanitized_url, headers(sanitized_uri)), file) # rubocop:disable Security/Open
+ end
+
+ def headers(sanitized_uri)
+ {}.tap do |headers|
+ credentials = sanitized_uri.credentials
+
+ if credentials[:user].present? || credentials[:password].present?
+ # Using authentication headers in the request
+ headers[:http_basic_authentication] = [credentials[:user], credentials[:password]]
+ end
+ end
+ end
+
+ def with_tmp_file(oid)
+ create_tmp_storage_dir
+
+ File.open(File.join(tmp_storage_dir, oid), 'w') { |file| yield file }
+ end
+
+ def create_tmp_storage_dir
+ FileUtils.makedirs(tmp_storage_dir) unless Dir.exist?(tmp_storage_dir)
+ end
+
+ def tmp_storage_dir
+ @tmp_storage_dir ||= File.join(storage_dir, 'tmp', 'download')
+ end
+
+ def storage_dir
+ @storage_dir ||= Gitlab.config.lfs.storage_path
+ end
+ end
+ end
+end
diff --git a/app/services/projects/lfs_pointers/lfs_import_service.rb b/app/services/projects/lfs_pointers/lfs_import_service.rb
new file mode 100644
index 00000000000..b6b0dec142f
--- /dev/null
+++ b/app/services/projects/lfs_pointers/lfs_import_service.rb
@@ -0,0 +1,92 @@
+# This service manages the whole worflow of discovering the Lfs files in a
+# repository, linking them to the project and downloading (and linking) the non
+# existent ones.
+module Projects
+ module LfsPointers
+ class LfsImportService < BaseService
+ include Gitlab::Utils::StrongMemoize
+
+ HEAD_REV = 'HEAD'.freeze
+ LFS_ENDPOINT_PATTERN = /^\t?url\s*=\s*(.+)$/.freeze
+ LFS_BATCH_API_ENDPOINT = '/info/lfs/objects/batch'.freeze
+
+ LfsImportError = Class.new(StandardError)
+
+ def execute
+ return {} unless project&.lfs_enabled?
+
+ if external_lfs_endpoint?
+ # If the endpoint host is different from the import_url it means
+ # that the repo is using a third party service for storing the LFS files.
+ # In this case, we have to disable lfs in the project
+ disable_lfs!
+
+ return {}
+ end
+
+ get_download_links
+ rescue LfsDownloadLinkListService::DownloadLinksError => e
+ raise LfsImportError, "The LFS objects download list couldn't be imported. Error: #{e.message}"
+ end
+
+ private
+
+ def external_lfs_endpoint?
+ lfsconfig_endpoint_uri && lfsconfig_endpoint_uri.host != import_uri.host
+ end
+
+ def disable_lfs!
+ project.update(lfs_enabled: false)
+ end
+
+ def get_download_links
+ existent_lfs = LfsListService.new(project).execute
+ linked_oids = LfsLinkService.new(project).execute(existent_lfs.keys)
+
+ # Retrieving those oids not linked and which we need to download
+ not_linked_lfs = existent_lfs.except(*linked_oids)
+
+ LfsDownloadLinkListService.new(project, remote_uri: current_endpoint_uri).execute(not_linked_lfs)
+ end
+
+ def lfsconfig_endpoint_uri
+ strong_memoize(:lfsconfig_endpoint_uri) do
+ # Retrieveing the blob data from the .lfsconfig file
+ data = project.repository.lfsconfig_for(HEAD_REV)
+ # Parsing the data to retrieve the url
+ parsed_data = data&.match(LFS_ENDPOINT_PATTERN)
+
+ if parsed_data
+ URI.parse(parsed_data[1]).tap do |endpoint|
+ endpoint.user ||= import_uri.user
+ endpoint.password ||= import_uri.password
+ end
+ end
+ end
+ rescue URI::InvalidURIError
+ raise LfsImportError, 'Invalid URL in .lfsconfig file'
+ end
+
+ def import_uri
+ @import_uri ||= URI.parse(project.import_url)
+ rescue URI::InvalidURIError
+ raise LfsImportError, 'Invalid project import URL'
+ end
+
+ def current_endpoint_uri
+ (lfsconfig_endpoint_uri || default_endpoint_uri)
+ end
+
+ # The import url must end with '.git' here we ensure it is
+ def default_endpoint_uri
+ @default_endpoint_uri ||= begin
+ import_uri.dup.tap do |uri|
+ path = uri.path.gsub(%r(/$), '')
+ path += '.git' unless path.ends_with?('.git')
+ uri.path = path + LFS_BATCH_API_ENDPOINT
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/projects/lfs_pointers/lfs_link_service.rb b/app/services/projects/lfs_pointers/lfs_link_service.rb
new file mode 100644
index 00000000000..d20bdf86c58
--- /dev/null
+++ b/app/services/projects/lfs_pointers/lfs_link_service.rb
@@ -0,0 +1,29 @@
+# Given a list of oids, this services links the existent Lfs Objects to the project
+module Projects
+ module LfsPointers
+ class LfsLinkService < BaseService
+ # Accept an array of oids to link
+ #
+ # Returns a hash with the same structure with oids linked
+ def execute(oids)
+ return {} unless project&.lfs_enabled?
+
+ # Search and link existing LFS Object
+ link_existing_lfs_objects(oids)
+ end
+
+ private
+
+ def link_existing_lfs_objects(oids)
+ existent_lfs_objects = LfsObject.where(oid: oids)
+
+ return [] unless existent_lfs_objects.any?
+
+ not_linked_lfs_objects = existent_lfs_objects.where.not(id: project.all_lfs_objects)
+ project.all_lfs_objects << not_linked_lfs_objects
+
+ existent_lfs_objects.pluck(:oid)
+ end
+ end
+ end
+end
diff --git a/app/services/projects/lfs_pointers/lfs_list_service.rb b/app/services/projects/lfs_pointers/lfs_list_service.rb
new file mode 100644
index 00000000000..b770982cbc0
--- /dev/null
+++ b/app/services/projects/lfs_pointers/lfs_list_service.rb
@@ -0,0 +1,19 @@
+# This service list all existent Lfs objects in a repository
+module Projects
+ module LfsPointers
+ class LfsListService < BaseService
+ REV = 'HEAD'.freeze
+
+ # Retrieve all lfs blob pointers and returns a hash
+ # with the structure { lfs_file_oid => lfs_file_size }
+ def execute
+ return {} unless project&.lfs_enabled?
+
+ Gitlab::Git::LfsChanges.new(project.repository, REV)
+ .all_pointers
+ .map! { |blob| [blob.lfs_oid, blob.lfs_size] }
+ .to_h
+ end
+ end
+ end
+end
diff --git a/app/services/projects/open_issues_count_service.rb b/app/services/projects/open_issues_count_service.rb
index a975a06a05c..78b1477186a 100644
--- a/app/services/projects/open_issues_count_service.rb
+++ b/app/services/projects/open_issues_count_service.rb
@@ -2,14 +2,72 @@ module Projects
# Service class for counting and caching the number of open issues of a
# project.
class OpenIssuesCountService < Projects::CountService
+ include Gitlab::Utils::StrongMemoize
+
+ # Cache keys used to store issues count
+ PUBLIC_COUNT_KEY = 'public_open_issues_count'.freeze
+ TOTAL_COUNT_KEY = 'total_open_issues_count'.freeze
+
+ def initialize(project, user = nil)
+ @user = user
+
+ super(project)
+ end
+
def cache_key_name
- 'open_issues_count'
+ public_only? ? PUBLIC_COUNT_KEY : TOTAL_COUNT_KEY
+ end
+
+ def public_only?
+ !user_is_at_least_reporter?
+ end
+
+ def relation_for_count
+ self.class.query(@project, public_only: public_only?)
+ end
+
+ def user_is_at_least_reporter?
+ strong_memoize(:user_is_at_least_reporter) do
+ @user && @project.team.member?(@user, Gitlab::Access::REPORTER)
+ end
+ end
+
+ def public_count_cache_key
+ cache_key(PUBLIC_COUNT_KEY)
+ end
+
+ def total_count_cache_key
+ cache_key(TOTAL_COUNT_KEY)
+ end
+
+ def refresh_cache(&block)
+ if block_given?
+ super(&block)
+ else
+ count_grouped_by_confidential = self.class.query(@project, public_only: false).group(:confidential).count
+ public_count = count_grouped_by_confidential[false] || 0
+ total_count = public_count + (count_grouped_by_confidential[true] || 0)
+
+ update_cache_for_key(public_count_cache_key) do
+ public_count
+ end
+
+ update_cache_for_key(total_count_cache_key) do
+ total_count
+ end
+ end
end
- def self.query(project_ids)
- # We don't include confidential issues in this number since this would
- # expose the number of confidential issues to non project members.
- Issue.opened.public_only.where(project: project_ids)
+ # We only show total issues count for reporters
+ # which are allowed to view confidential issues
+ # This will still show a discrepancy on issues number but should be less than before.
+ # Check https://gitlab.com/gitlab-org/gitlab-ce/issues/38418 description.
+ def self.query(projects, public_only: true)
+ if public_only
+ Issue.opened.public_only.where(project: projects)
+ else
+ Issue.opened.where(project: projects)
+ end
end
end
end
diff --git a/app/services/projects/participants_service.rb b/app/services/projects/participants_service.rb
index eb0472c6024..21741913385 100644
--- a/app/services/projects/participants_service.rb
+++ b/app/services/projects/participants_service.rb
@@ -5,14 +5,16 @@ module Projects
def execute(noteable)
@noteable = noteable
- project_members = sorted(project.team.members)
participants = noteable_owner + participants_in_noteable + all_members + groups + project_members
participants.uniq
end
+ def project_members
+ @project_members ||= sorted(project.team.members)
+ end
+
def all_members
- count = project.team.members.flatten.count
- [{ username: "all", name: "All Project and Group Members", count: count }]
+ [{ username: "all", name: "All Project and Group Members", count: project_members.count }]
end
end
end
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index 679f4a9cb62..f4fbaacc08b 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -17,7 +17,12 @@ module Projects
ensure_wiki_exists if enabling_wiki?
- if project.update_attributes(params.except(:default_branch))
+ yield if block_given?
+
+ # If the block added errors, don't try to save the project
+ return validation_failed! if project.errors.any?
+
+ if project.update(params.except(:default_branch))
if project.previous_changes.include?('path')
project.rename_repo
else
@@ -28,21 +33,25 @@ module Projects
success
else
- model_errors = project.errors.full_messages.to_sentence
- error_message = model_errors.presence || 'Project could not be updated!'
-
- error(error_message)
+ validation_failed!
end
end
def run_auto_devops_pipeline?
- return false if project.repository.gitlab_ci_yml || !project.auto_devops.previous_changes.include?('enabled')
+ return false if project.repository.gitlab_ci_yml || !project.auto_devops&.previous_changes&.include?('enabled')
project.auto_devops.enabled? || (project.auto_devops.enabled.nil? && Gitlab::CurrentSettings.auto_devops_enabled?)
end
private
+ def validation_failed!
+ model_errors = project.errors.full_messages.to_sentence
+ error_message = model_errors.presence || 'Project could not be updated!'
+
+ error(error_message)
+ end
+
def renaming_project_with_container_registry_tags?
new_path = params[:path]
@@ -53,8 +62,8 @@ module Projects
def changing_default_branch?
new_branch = params[:default_branch]
- project.repository.exists? &&
- new_branch && new_branch != project.default_branch
+ new_branch && project.repository.exists? &&
+ new_branch != project.default_branch
end
def enabling_wiki?
diff --git a/app/services/protected_branches/access_level_params.rb b/app/services/protected_branches/access_level_params.rb
index 253ae8b0124..4658b0e850d 100644
--- a/app/services/protected_branches/access_level_params.rb
+++ b/app/services/protected_branches/access_level_params.rb
@@ -14,7 +14,7 @@ module ProtectedBranches
private
def params_with_default(params)
- params[:"#{type}_access_level"] ||= Gitlab::Access::MASTER if use_default_access_level?(params)
+ params[:"#{type}_access_level"] ||= Gitlab::Access::MAINTAINER if use_default_access_level?(params)
params
end
diff --git a/app/services/protected_branches/legacy_api_create_service.rb b/app/services/protected_branches/legacy_api_create_service.rb
index e358fd0374e..bb7656489c5 100644
--- a/app/services/protected_branches/legacy_api_create_service.rb
+++ b/app/services/protected_branches/legacy_api_create_service.rb
@@ -9,14 +9,14 @@ module ProtectedBranches
if params.delete(:developers_can_push)
Gitlab::Access::DEVELOPER
else
- Gitlab::Access::MASTER
+ Gitlab::Access::MAINTAINER
end
merge_access_level =
if params.delete(:developers_can_merge)
Gitlab::Access::DEVELOPER
else
- Gitlab::Access::MASTER
+ Gitlab::Access::MAINTAINER
end
@params.merge!(push_access_levels_attributes: [{ access_level: push_access_level }],
diff --git a/app/services/protected_branches/legacy_api_update_service.rb b/app/services/protected_branches/legacy_api_update_service.rb
index 33176253ca2..1df38de0e4a 100644
--- a/app/services/protected_branches/legacy_api_update_service.rb
+++ b/app/services/protected_branches/legacy_api_update_service.rb
@@ -17,14 +17,14 @@ module ProtectedBranches
when true
params[:push_access_levels_attributes] = [{ access_level: Gitlab::Access::DEVELOPER }]
when false
- params[:push_access_levels_attributes] = [{ access_level: Gitlab::Access::MASTER }]
+ params[:push_access_levels_attributes] = [{ access_level: Gitlab::Access::MAINTAINER }]
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 }]
+ params[:merge_access_levels_attributes] = [{ access_level: Gitlab::Access::MAINTAINER }]
end
service = ProtectedBranches::UpdateService.new(@project, @current_user, @params)
diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb
index 0215994b1a7..9ac8fdb4cff 100644
--- a/app/services/quick_actions/interpret_service.rb
+++ b/app/services/quick_actions/interpret_service.rb
@@ -561,6 +561,17 @@ module QuickActions
end
end
+ desc 'Make issue confidential.'
+ explanation do
+ 'Makes this issue confidential'
+ end
+ condition do
+ issuable.is_a?(Issue) && current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
+ end
+ command :confidential do
+ @updates[:confidential] = true
+ end
+
def extract_users(params)
return [] if params.nil?
diff --git a/app/services/tags/create_service.rb b/app/services/tags/create_service.rb
index cc76d0df3a1..3cc88d77ba1 100644
--- a/app/services/tags/create_service.rb
+++ b/app/services/tags/create_service.rb
@@ -13,7 +13,7 @@ module Tags
new_tag = repository.add_tag(current_user, tag_name, target, message)
rescue Gitlab::Git::Repository::TagExistsError
return error("Tag #{tag_name} already exists")
- rescue Gitlab::Git::HooksService::PreReceiveError => ex
+ rescue Gitlab::Git::PreReceiveError => ex
return error(ex.message)
end
diff --git a/app/services/tags/destroy_service.rb b/app/services/tags/destroy_service.rb
index d3d46064729..b84b061e460 100644
--- a/app/services/tags/destroy_service.rb
+++ b/app/services/tags/destroy_service.rb
@@ -21,7 +21,7 @@ module Tags
else
error('Failed to remove tag')
end
- rescue Gitlab::Git::HooksService::PreReceiveError => ex
+ rescue Gitlab::Git::PreReceiveError => ex
error(ex.message)
end
diff --git a/app/services/test_hooks/project_service.rb b/app/services/test_hooks/project_service.rb
index 01d5d774cd5..65183e84cce 100644
--- a/app/services/test_hooks/project_service.rb
+++ b/app/services/test_hooks/project_service.rb
@@ -1,11 +1,13 @@
module TestHooks
class ProjectService < TestHooks::BaseService
- private
+ attr_writer :project
def project
@project ||= hook.project
end
+ private
+
def push_events_data
throw(:validation_error, 'Ensure the project has at least one commit.') if project.empty_repo?
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index ffd48e842c2..f91cd03bf5c 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -98,12 +98,12 @@ class TodoService
# When a build fails on the HEAD of a merge request we should:
#
- # * create a todo for author of MR to fix it
- # * create a todo for merge_user to keep an eye on it
+ # * create a todo for each merge participant
#
def merge_request_build_failed(merge_request)
- create_build_failed_todo(merge_request, merge_request.author)
- create_build_failed_todo(merge_request, merge_request.merge_user) if merge_request.merge_when_pipeline_succeeds?
+ merge_request.merge_participants.each do |user|
+ create_build_failed_todo(merge_request, user)
+ end
end
# When a new commit is pushed to a merge request we should:
@@ -116,20 +116,22 @@ class TodoService
# When a build is retried to a merge request we should:
#
- # * mark all pending todos related to the merge request for the author as done
- # * mark all pending todos related to the merge request for the merge_user as done
+ # * mark all pending todos related to the merge request as done for each merge participant
#
def merge_request_build_retried(merge_request)
- mark_pending_todos_as_done(merge_request, merge_request.author)
- mark_pending_todos_as_done(merge_request, merge_request.merge_user) if merge_request.merge_when_pipeline_succeeds?
+ merge_request.merge_participants.each do |user|
+ mark_pending_todos_as_done(merge_request, user)
+ end
end
- # When a merge request could not be automatically merged due to its unmergeable state we should:
+ # When a merge request could not be merged due to its unmergeable state we should:
#
- # * create a todo for a merge_user
+ # * create a todo for each merge participant
#
def merge_request_became_unmergeable(merge_request)
- create_unmergeable_todo(merge_request, merge_request.merge_user) if merge_request.merge_when_pipeline_succeeds?
+ merge_request.merge_participants.each do |user|
+ create_unmergeable_todo(merge_request, user)
+ end
end
# When create a note we should:
@@ -275,9 +277,9 @@ class TodoService
create_todos(todo_author, attributes)
end
- def create_unmergeable_todo(merge_request, merge_user)
- attributes = attributes_for_todo(merge_request.project, merge_request, merge_user, Todo::UNMERGEABLE)
- create_todos(merge_user, attributes)
+ def create_unmergeable_todo(merge_request, todo_author)
+ attributes = attributes_for_todo(merge_request.project, merge_request, todo_author, Todo::UNMERGEABLE)
+ create_todos(todo_author, attributes)
end
def attributes_for_target(target)
diff --git a/app/services/update_release_service.rb b/app/services/update_release_service.rb
index b7c36651968..dc696e9c440 100644
--- a/app/services/update_release_service.rb
+++ b/app/services/update_release_service.rb
@@ -7,7 +7,7 @@ class UpdateReleaseService < BaseService
release = project.releases.find_by(tag: tag_name)
if release
- release.update_attributes(description: release_description)
+ release.update(description: release_description)
success(release)
else
diff --git a/app/services/validate_new_branch_service.rb b/app/services/validate_new_branch_service.rb
index 7d1ed768ee8..643f2ce1481 100644
--- a/app/services/validate_new_branch_service.rb
+++ b/app/services/validate_new_branch_service.rb
@@ -13,7 +13,7 @@ class ValidateNewBranchService < BaseService
end
success
- rescue Gitlab::Git::HooksService::PreReceiveError => ex
+ rescue Gitlab::Git::PreReceiveError => ex
error(ex.message)
end
end
diff --git a/app/services/web_hook_service.rb b/app/services/web_hook_service.rb
index 7ec52b6ce2b..8a86e47f0ea 100644
--- a/app/services/web_hook_service.rb
+++ b/app/services/web_hook_service.rb
@@ -82,7 +82,7 @@ class WebHookService
post_url = hook.url.gsub("#{parsed_url.userinfo}@", '')
basic_auth = {
username: CGI.unescape(parsed_url.user),
- password: CGI.unescape(parsed_url.password)
+ password: CGI.unescape(parsed_url.password.presence || '')
}
make_request(post_url, basic_auth)
end
diff --git a/app/uploaders/attachment_uploader.rb b/app/uploaders/attachment_uploader.rb
index cd819dc9bff..0a166335b4e 100644
--- a/app/uploaders/attachment_uploader.rb
+++ b/app/uploaders/attachment_uploader.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AttachmentUploader < GitlabUploader
include RecordsUploads::Concern
include ObjectStorage::Concern
diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb
index 5848e6c6994..b29ef57b071 100644
--- a/app/uploaders/avatar_uploader.rb
+++ b/app/uploaders/avatar_uploader.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AvatarUploader < GitlabUploader
include UploaderHelper
include RecordsUploads::Concern
diff --git a/app/uploaders/favicon_uploader.rb b/app/uploaders/favicon_uploader.rb
new file mode 100644
index 00000000000..a0b275b56a9
--- /dev/null
+++ b/app/uploaders/favicon_uploader.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class FaviconUploader < AttachmentUploader
+ EXTENSION_WHITELIST = %w[png ico].freeze
+
+ def extension_whitelist
+ EXTENSION_WHITELIST
+ end
+
+ private
+
+ def filename_for_different_format(filename, format)
+ filename.chomp(File.extname(filename)) + ".#{format}"
+ end
+end
diff --git a/app/uploaders/file_mover.rb b/app/uploaders/file_mover.rb
index bd7736ad74e..a7f8615e9ba 100644
--- a/app/uploaders/file_mover.rb
+++ b/app/uploaders/file_mover.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class FileMover
attr_reader :secret, :file_name, :model, :update_field
diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb
index 133fdf6684d..21292ddcf44 100644
--- a/app/uploaders/file_uploader.rb
+++ b/app/uploaders/file_uploader.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# This class breaks the actual CarrierWave concept.
# Every uploader should use a base_dir that is model agnostic so we can build
# back URLs from base_dir-relative paths saved in the `Upload` model.
@@ -65,10 +67,10 @@ class FileUploader < GitlabUploader
SecureRandom.hex
end
- def upload_paths(filename)
+ def upload_paths(identifier)
[
- File.join(secret, filename),
- File.join(base_dir(Store::REMOTE), secret, filename)
+ File.join(secret, identifier),
+ File.join(base_dir(Store::REMOTE), secret, identifier)
]
end
@@ -81,6 +83,13 @@ class FileUploader < GitlabUploader
apply_context!(uploader_context)
end
+ def initialize_copy(from)
+ super
+
+ @secret = self.class.generate_secret
+ @upload = nil # calling record_upload would delete the old upload if set
+ end
+
# enforce the usage of Hashed storage when storing to
# remote store as the FileMover doesn't support OS
def base_dir(store = nil)
@@ -110,7 +119,7 @@ class FileUploader < GitlabUploader
end
def markdown_link
- markdown = "[#{markdown_name}](#{secure_url})"
+ markdown = +"[#{markdown_name}](#{secure_url})"
markdown.prepend("!") if image_or_video? || dangerous?
markdown
end
@@ -144,6 +153,27 @@ class FileUploader < GitlabUploader
@secret ||= self.class.generate_secret
end
+ # return a new uploader with a file copy on another project
+ def self.copy_to(uploader, to_project)
+ moved = uploader.dup.tap do |u|
+ u.model = to_project
+ end
+
+ moved.copy_file(uploader.file)
+ moved
+ end
+
+ def copy_file(file)
+ to_path = if file_storage?
+ File.join(self.class.root, store_path)
+ else
+ store_path
+ end
+
+ self.file = file.copy_to(to_path)
+ record_upload # after_store is not triggered
+ end
+
private
def apply_context!(uploader_context)
diff --git a/app/uploaders/gitlab_uploader.rb b/app/uploaders/gitlab_uploader.rb
index f8a237178d9..719bd6ef418 100644
--- a/app/uploaders/gitlab_uploader.rb
+++ b/app/uploaders/gitlab_uploader.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class GitlabUploader < CarrierWave::Uploader::Base
class_attribute :options
@@ -69,6 +71,28 @@ class GitlabUploader < CarrierWave::Uploader::Base
File.join('/', self.class.base_dir, dynamic_segment, filename)
end
+ def cached_size
+ size
+ end
+
+ def open
+ stream =
+ if file_storage?
+ File.open(path, "rb") if path
+ else
+ ::Gitlab::HttpIO.new(url, cached_size) if url
+ end
+
+ return unless stream
+ return stream unless block_given?
+
+ begin
+ yield(stream)
+ ensure
+ stream.close
+ end
+ end
+
private
# Designed to be overridden by child uploaders that have a dynamic path
diff --git a/app/uploaders/import_export_uploader.rb b/app/uploaders/import_export_uploader.rb
new file mode 100644
index 00000000000..213ac5c8011
--- /dev/null
+++ b/app/uploaders/import_export_uploader.rb
@@ -0,0 +1,15 @@
+class ImportExportUploader < AttachmentUploader
+ EXTENSION_WHITELIST = %w[tar.gz].freeze
+
+ def extension_whitelist
+ EXTENSION_WHITELIST
+ end
+
+ def move_to_store
+ true
+ end
+
+ def move_to_cache
+ false
+ end
+end
diff --git a/app/uploaders/job_artifact_uploader.rb b/app/uploaders/job_artifact_uploader.rb
index 2a5a830ce4f..f6af023e0f9 100644
--- a/app/uploaders/job_artifact_uploader.rb
+++ b/app/uploaders/job_artifact_uploader.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class JobArtifactUploader < GitlabUploader
extend Workhorse::UploadPath
include ObjectStorage::Concern
@@ -16,14 +18,6 @@ class JobArtifactUploader < GitlabUploader
dynamic_segment
end
- def open
- if file_storage?
- File.open(path, "rb") if path
- else
- ::Gitlab::Ci::Trace::HttpIO.new(url, cached_size) if url
- end
- end
-
private
def dynamic_segment
diff --git a/app/uploaders/legacy_artifact_uploader.rb b/app/uploaders/legacy_artifact_uploader.rb
index efb7893d153..b4d0d752016 100644
--- a/app/uploaders/legacy_artifact_uploader.rb
+++ b/app/uploaders/legacy_artifact_uploader.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class LegacyArtifactUploader < GitlabUploader
extend Workhorse::UploadPath
include ObjectStorage::Concern
diff --git a/app/uploaders/lfs_object_uploader.rb b/app/uploaders/lfs_object_uploader.rb
index eb521a22ebc..f3d32e6b39d 100644
--- a/app/uploaders/lfs_object_uploader.rb
+++ b/app/uploaders/lfs_object_uploader.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class LfsObjectUploader < GitlabUploader
extend Workhorse::UploadPath
include ObjectStorage::Concern
diff --git a/app/uploaders/namespace_file_uploader.rb b/app/uploaders/namespace_file_uploader.rb
index 1085ecb1700..52969762b7d 100644
--- a/app/uploaders/namespace_file_uploader.rb
+++ b/app/uploaders/namespace_file_uploader.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class NamespaceFileUploader < FileUploader
# Re-Override
def self.root
diff --git a/app/uploaders/object_storage.rb b/app/uploaders/object_storage.rb
index f2a8afccdeb..dad6e85fb56 100644
--- a/app/uploaders/object_storage.rb
+++ b/app/uploaders/object_storage.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'fog/aws'
require 'carrierwave/storage/fog'
@@ -10,8 +12,18 @@ module ObjectStorage
UnknownStoreError = Class.new(StandardError)
ObjectStorageUnavailable = Class.new(StandardError)
- DIRECT_UPLOAD_TIMEOUT = 4.hours
- TMP_UPLOAD_PATH = 'tmp/upload'.freeze
+ class ExclusiveLeaseTaken < StandardError
+ def initialize(lease_key)
+ @lease_key = lease_key
+ end
+
+ def message
+ *lease_key_group, _ = *@lease_key.split(":")
+ "Exclusive lease for #{lease_key_group.join(':')} is already taken."
+ end
+ end
+
+ TMP_UPLOAD_PATH = 'tmp/uploads'.freeze
module Store
LOCAL = 1
@@ -23,18 +35,18 @@ module ObjectStorage
module RecordsUploads
extend ActiveSupport::Concern
- def prepended(base)
+ prepended do |base|
raise "#{base} must include ObjectStorage::Concern to use extensions." unless base < Concern
- base.include(RecordsUploads::Concern)
+ base.include(::RecordsUploads::Concern)
end
def retrieve_from_store!(identifier)
- paths = store_dirs.map { |store, path| File.join(path, identifier) }
+ paths = upload_paths(identifier)
unless current_upload_satisfies?(paths, model)
# the upload we already have isn't right, find the correct one
- self.upload = uploads.find_by(model: model, path: paths)
+ self.upload = model&.retrieve_upload(identifier, paths)
end
super
@@ -47,7 +59,7 @@ module ObjectStorage
end
def upload=(upload)
- return unless upload
+ return if upload.nil?
self.object_store = upload.store
super
@@ -63,6 +75,15 @@ module ObjectStorage
upload.id)
end
+ def exclusive_lease_key
+ # For FileUploaders, model may have many uploaders. In that case
+ # we want to use exclusive key per upload, not per model to allow
+ # parallel migration
+ key_object = upload || model
+
+ "object_storage_migrate:#{key_object.class}:#{key_object.id}"
+ end
+
private
def current_upload_satisfies?(paths, model)
@@ -156,9 +177,9 @@ module ObjectStorage
model_class.uploader_options.dig(mount_point, :mount_on) || mount_point
end
- def workhorse_authorize
+ def workhorse_authorize(has_length:, maximum_size: nil)
{
- RemoteObject: workhorse_remote_upload_options,
+ RemoteObject: workhorse_remote_upload_options(has_length: has_length, maximum_size: maximum_size),
TempPath: workhorse_local_upload_path
}.compact
end
@@ -167,22 +188,16 @@ module ObjectStorage
File.join(self.root, TMP_UPLOAD_PATH)
end
- def workhorse_remote_upload_options
+ def workhorse_remote_upload_options(has_length:, maximum_size: nil)
return unless self.object_store_enabled?
return unless self.direct_upload_enabled?
id = [CarrierWave.generate_cache_id, SecureRandom.hex].join('-')
upload_path = File.join(TMP_UPLOAD_PATH, id)
- connection = ::Fog::Storage.new(self.object_store_credentials)
- expire_at = Time.now + DIRECT_UPLOAD_TIMEOUT
- options = { 'Content-Type' => 'application/octet-stream' }
+ direct_upload = ObjectStorage::DirectUpload.new(self.object_store_credentials, remote_store_path, upload_path,
+ has_length: has_length, maximum_size: maximum_size)
- {
- ID: id,
- GetURL: connection.get_object_url(remote_store_path, upload_path, expire_at),
- DeleteURL: connection.delete_object_url(remote_store_path, upload_path, expire_at),
- StoreURL: connection.put_object_url(remote_store_path, upload_path, expire_at, options)
- }
+ direct_upload.to_hash.merge(ID: id)
end
end
@@ -268,7 +283,7 @@ module ObjectStorage
end
def delete_migrated_file(migrated_file)
- migrated_file.delete if exists?
+ migrated_file.delete
end
def exists?
@@ -286,6 +301,13 @@ module ObjectStorage
}
end
+ # Returns all the possible paths for an upload.
+ # the `upload.path` is a lookup parameter, and it may change
+ # depending on the `store` param.
+ def upload_paths(identifier)
+ store_dirs.map { |store, path| File.join(path, identifier) }
+ end
+
def cache!(new_file = sanitized_file)
# We intercept ::UploadedFile which might be stored on remote storage
# We use that for "accelerated" uploads, where we store result on remote storage
@@ -305,6 +327,10 @@ module ObjectStorage
super
end
+ def exclusive_lease_key
+ "object_storage_migrate:#{model.class}:#{model.id}"
+ end
+
private
def schedule_background_upload?
@@ -371,17 +397,14 @@ module ObjectStorage
end
end
- def exclusive_lease_key
- "object_storage_migrate:#{model.class}:#{model.id}"
- end
-
def with_exclusive_lease
- uuid = Gitlab::ExclusiveLease.new(exclusive_lease_key, timeout: 1.hour.to_i).try_obtain
- raise 'exclusive lease already taken' unless uuid
+ lease_key = exclusive_lease_key
+ uuid = Gitlab::ExclusiveLease.new(lease_key, timeout: 1.hour.to_i).try_obtain
+ raise ExclusiveLeaseTaken.new(lease_key) unless uuid
yield uuid
ensure
- Gitlab::ExclusiveLease.cancel(exclusive_lease_key, uuid)
+ Gitlab::ExclusiveLease.cancel(lease_key, uuid)
end
#
diff --git a/app/uploaders/personal_file_uploader.rb b/app/uploaders/personal_file_uploader.rb
index e3898b07730..25474b494ff 100644
--- a/app/uploaders/personal_file_uploader.rb
+++ b/app/uploaders/personal_file_uploader.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class PersonalFileUploader < FileUploader
# Re-Override
def self.root
diff --git a/app/uploaders/records_uploads.rb b/app/uploaders/records_uploads.rb
index 89c74a78835..5795065ae11 100644
--- a/app/uploaders/records_uploads.rb
+++ b/app/uploaders/records_uploads.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module RecordsUploads
module Concern
extend ActiveSupport::Concern
@@ -22,7 +24,7 @@ module RecordsUploads
Upload.transaction do
uploads.where(path: upload_path).delete_all
- upload.destroy! if upload
+ upload.delete if upload
self.upload = build_upload.tap(&:save!)
end
diff --git a/app/uploaders/uploader_helper.rb b/app/uploaders/uploader_helper.rb
index fd446d31092..2a2b54a9270 100644
--- a/app/uploaders/uploader_helper.rb
+++ b/app/uploaders/uploader_helper.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
# Extra methods for uploader
module UploaderHelper
- IMAGE_EXT = %w[png jpg jpeg gif bmp tiff].freeze
+ IMAGE_EXT = %w[png jpg jpeg gif bmp tiff ico].freeze
# We recommend using the .mp4 format over .mov. Videos in .mov format can
# still be used but you really need to make sure they are served with the
# proper MIME type video/mp4 and not video/quicktime or your videos won't play
diff --git a/app/uploaders/workhorse.rb b/app/uploaders/workhorse.rb
index 782032cf516..84dc2791b9c 100644
--- a/app/uploaders/workhorse.rb
+++ b/app/uploaders/workhorse.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Workhorse
module UploadPath
def workhorse_upload_path
diff --git a/app/validators/abstract_path_validator.rb b/app/validators/abstract_path_validator.rb
index e43b66cbe3a..45ac695c5ec 100644
--- a/app/validators/abstract_path_validator.rb
+++ b/app/validators/abstract_path_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AbstractPathValidator < ActiveModel::EachValidator
extend Gitlab::EncodingHelper
diff --git a/app/validators/addressable_url_validator.rb b/app/validators/addressable_url_validator.rb
deleted file mode 100644
index 94542125d43..00000000000
--- a/app/validators/addressable_url_validator.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-# AddressableUrlValidator
-#
-# Custom validator for URLs. This is a stricter version of UrlValidator - it also checks
-# for using the right protocol, but it actually parses the URL checking for any syntax errors.
-# The regex is also different from `URI` as we use `Addressable::URI` here.
-#
-# By default, only URLs for http, https, ssh, and git protocols will be considered valid.
-# Provide a `:protocols` option to configure accepted protocols.
-#
-# Example:
-#
-# class User < ActiveRecord::Base
-# validates :personal_url, addressable_url: true
-#
-# validates :ftp_url, addressable_url: { protocols: %w(ftp) }
-#
-# validates :git_url, addressable_url: { protocols: %w(http https ssh git) }
-# end
-#
-class AddressableUrlValidator < ActiveModel::EachValidator
- DEFAULT_OPTIONS = { protocols: %w(http https ssh git) }.freeze
-
- def validate_each(record, attribute, value)
- unless valid_url?(value)
- record.errors.add(attribute, "must be a valid URL")
- end
- end
-
- private
-
- def valid_url?(value)
- return false unless value
-
- valid_protocol?(value) && valid_uri?(value)
- end
-
- def valid_uri?(value)
- Gitlab::UrlSanitizer.valid?(value)
- end
-
- def valid_protocol?(value)
- options = DEFAULT_OPTIONS.merge(self.options)
- value =~ /\A#{URI.regexp(options[:protocols])}\z/
- end
-end
diff --git a/app/validators/certificate_fingerprint_validator.rb b/app/validators/certificate_fingerprint_validator.rb
index 17df756183a..79d78653ec7 100644
--- a/app/validators/certificate_fingerprint_validator.rb
+++ b/app/validators/certificate_fingerprint_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CertificateFingerprintValidator < ActiveModel::EachValidator
FINGERPRINT_PATTERN = /\A([a-zA-Z0-9]{2}[\s\-:]?){16,}\z/.freeze
diff --git a/app/validators/certificate_key_validator.rb b/app/validators/certificate_key_validator.rb
index 8c7bb750339..5b2bbffc066 100644
--- a/app/validators/certificate_key_validator.rb
+++ b/app/validators/certificate_key_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# UrlValidator
#
# Custom validator for private keys.
diff --git a/app/validators/certificate_validator.rb b/app/validators/certificate_validator.rb
index b0c9a1b92a4..de8bb179dfb 100644
--- a/app/validators/certificate_validator.rb
+++ b/app/validators/certificate_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# UrlValidator
#
# Custom validator for private keys.
diff --git a/app/validators/cluster_name_validator.rb b/app/validators/cluster_name_validator.rb
index e7d32550176..85fd63f08e5 100644
--- a/app/validators/cluster_name_validator.rb
+++ b/app/validators/cluster_name_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# ClusterNameValidator
#
# Custom validator for ClusterName.
diff --git a/app/validators/color_validator.rb b/app/validators/color_validator.rb
index 571d0007aa2..1932d042e83 100644
--- a/app/validators/color_validator.rb
+++ b/app/validators/color_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# ColorValidator
#
# Custom validator for web color codes. It requires the leading hash symbol and
diff --git a/app/validators/cron_timezone_validator.rb b/app/validators/cron_timezone_validator.rb
index 542c7d006ad..c5f51d65060 100644
--- a/app/validators/cron_timezone_validator.rb
+++ b/app/validators/cron_timezone_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# CronTimezoneValidator
#
# Custom validator for CronTimezone.
diff --git a/app/validators/cron_validator.rb b/app/validators/cron_validator.rb
index 981fade47a6..bd48a7a6efb 100644
--- a/app/validators/cron_validator.rb
+++ b/app/validators/cron_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# CronValidator
#
# Custom validator for Cron.
diff --git a/app/validators/duration_validator.rb b/app/validators/duration_validator.rb
index 10ff44031c6..811828169ca 100644
--- a/app/validators/duration_validator.rb
+++ b/app/validators/duration_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# DurationValidator
#
# Validate the format conforms with ChronicDuration
diff --git a/app/validators/email_validator.rb b/app/validators/email_validator.rb
index aab07a7ece4..9459edb7515 100644
--- a/app/validators/email_validator.rb
+++ b/app/validators/email_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class EmailValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
record.errors.add(attribute, :invalid) unless value =~ Devise.email_regexp
diff --git a/app/validators/importable_url_validator.rb b/app/validators/importable_url_validator.rb
deleted file mode 100644
index 612d3c71913..00000000000
--- a/app/validators/importable_url_validator.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# ImportableUrlValidator
-#
-# This validator blocks projects from using dangerous import_urls to help
-# protect against Server-side Request Forgery (SSRF).
-class ImportableUrlValidator < ActiveModel::EachValidator
- def validate_each(record, attribute, value)
- Gitlab::UrlBlocker.validate!(value, valid_ports: Project::VALID_IMPORT_PORTS)
- rescue Gitlab::UrlBlocker::BlockedUrlError => e
- record.errors.add(attribute, "is blocked: #{e.message}")
- end
-end
diff --git a/app/validators/key_restriction_validator.rb b/app/validators/key_restriction_validator.rb
index 204be827941..891d13b1596 100644
--- a/app/validators/key_restriction_validator.rb
+++ b/app/validators/key_restriction_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class KeyRestrictionValidator < ActiveModel::EachValidator
FORBIDDEN = -1
diff --git a/app/validators/line_code_validator.rb b/app/validators/line_code_validator.rb
index ed29e5aeb67..a351180790e 100644
--- a/app/validators/line_code_validator.rb
+++ b/app/validators/line_code_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# LineCodeValidator
#
# Custom validator for GitLab line codes.
diff --git a/app/validators/namespace_name_validator.rb b/app/validators/namespace_name_validator.rb
index 2e51af2982d..fb1c241037c 100644
--- a/app/validators/namespace_name_validator.rb
+++ b/app/validators/namespace_name_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# NamespaceNameValidator
#
# Custom validator for GitLab namespace name strings.
diff --git a/app/validators/namespace_path_validator.rb b/app/validators/namespace_path_validator.rb
index 7b0ae4db5d4..c078b272b2f 100644
--- a/app/validators/namespace_path_validator.rb
+++ b/app/validators/namespace_path_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class NamespacePathValidator < AbstractPathValidator
extend Gitlab::EncodingHelper
diff --git a/app/validators/project_path_validator.rb b/app/validators/project_path_validator.rb
index 424fd77a6a3..aea0a68e7cf 100644
--- a/app/validators/project_path_validator.rb
+++ b/app/validators/project_path_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ProjectPathValidator < AbstractPathValidator
extend Gitlab::EncodingHelper
diff --git a/app/validators/public_url_validator.rb b/app/validators/public_url_validator.rb
new file mode 100644
index 00000000000..3ff880deedd
--- /dev/null
+++ b/app/validators/public_url_validator.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+# PublicUrlValidator
+#
+# Custom validator for URLs. This validator works like UrlValidator but
+# it blocks by default urls pointing to localhost or the local network.
+#
+# This validator accepts the same params UrlValidator does.
+#
+# Example:
+#
+# class User < ActiveRecord::Base
+# validates :personal_url, public_url: true
+#
+# validates :ftp_url, public_url: { protocols: %w(ftp) }
+#
+# validates :git_url, public_url: { allow_localhost: true, allow_local_network: true}
+# end
+#
+class PublicUrlValidator < UrlValidator
+ private
+
+ def default_options
+ # By default block all urls pointing to localhost or the local network
+ super.merge(allow_localhost: false,
+ allow_local_network: false)
+ end
+end
diff --git a/app/validators/top_level_group_validator.rb b/app/validators/top_level_group_validator.rb
index 7e2e735e0cf..b50c9dca154 100644
--- a/app/validators/top_level_group_validator.rb
+++ b/app/validators/top_level_group_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class TopLevelGroupValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if value&.subgroup?
diff --git a/app/validators/url_placeholder_validator.rb b/app/validators/url_placeholder_validator.rb
deleted file mode 100644
index dd681218b6b..00000000000
--- a/app/validators/url_placeholder_validator.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-# UrlValidator
-#
-# Custom validator for URLs.
-#
-# By default, only URLs for the HTTP(S) protocols will be considered valid.
-# Provide a `:protocols` option to configure accepted protocols.
-#
-# Also, this validator can help you validate urls with placeholders inside.
-# Usually, if you have a url like 'http://www.example.com/%{project_path}' the
-# URI parser will reject that URL format. Provide a `:placeholder_regex` option
-# to configure accepted placeholders.
-#
-# Example:
-#
-# class User < ActiveRecord::Base
-# validates :personal_url, url: true
-#
-# validates :ftp_url, url: { protocols: %w(ftp) }
-#
-# validates :git_url, url: { protocols: %w(http https ssh git) }
-#
-# validates :placeholder_url, url: { placeholder_regex: /(project_path|project_id|default_branch)/ }
-# end
-#
-class UrlPlaceholderValidator < UrlValidator
- def validate_each(record, attribute, value)
- placeholder_regex = self.options[:placeholder_regex]
- value = value.gsub(/%{#{placeholder_regex}}/, 'foo') if placeholder_regex && value
-
- super(record, attribute, value)
- end
-end
diff --git a/app/validators/url_validator.rb b/app/validators/url_validator.rb
index a77beb2683d..faaf1283078 100644
--- a/app/validators/url_validator.rb
+++ b/app/validators/url_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# UrlValidator
#
# Custom validator for URLs.
@@ -15,25 +17,71 @@
# validates :git_url, url: { protocols: %w(http https ssh git) }
# end
#
+# This validator can also block urls pointing to localhost or the local network to
+# protect against Server-side Request Forgery (SSRF), or check for the right port.
+#
+# The available options are:
+# - protocols: Allowed protocols. Default: http and https
+# - allow_localhost: Allow urls pointing to localhost. Default: true
+# - allow_local_network: Allow urls pointing to private network addresses. Default: true
+# - ports: Allowed ports. Default: all.
+# - enforce_user: Validate user format. Default: false
+#
+# Example:
+# class User < ActiveRecord::Base
+# validates :personal_url, url: { allow_localhost: false, allow_local_network: false}
+#
+# validates :web_url, url: { ports: [80, 443] }
+# end
class UrlValidator < ActiveModel::EachValidator
+ DEFAULT_PROTOCOLS = %w(http https).freeze
+
+ attr_reader :record
+
def validate_each(record, attribute, value)
- unless valid_url?(value)
- record.errors.add(attribute, "must be a valid URL")
+ @record = record
+
+ if value.present?
+ value.strip!
+ else
+ record.errors.add(attribute, 'must be a valid URL')
end
+
+ Gitlab::UrlBlocker.validate!(value, blocker_args)
+ rescue Gitlab::UrlBlocker::BlockedUrlError => e
+ record.errors.add(attribute, "is blocked: #{e.message}")
end
private
def default_options
- @default_options ||= { protocols: %w(http https) }
+ # By default the validator doesn't block any url based on the ip address
+ {
+ protocols: DEFAULT_PROTOCOLS,
+ ports: [],
+ allow_localhost: true,
+ allow_local_network: true,
+ enforce_user: false
+ }
end
- def valid_url?(value)
- return false if value.nil?
+ def current_options
+ options = self.options.map do |option, value|
+ [option, value.is_a?(Proc) ? value.call(record) : value]
+ end.to_h
- options = default_options.merge(self.options)
+ default_options.merge(options)
+ end
+
+ def blocker_args
+ current_options.slice(*default_options.keys).tap do |args|
+ if allow_setting_local_requests?
+ args[:allow_localhost] = args[:allow_local_network] = true
+ end
+ end
+ end
- value.strip!
- value =~ /\A#{URI.regexp(options[:protocols])}\z/
+ def allow_setting_local_requests?
+ ApplicationSetting.current&.allow_local_requests_from_hooks_and_services?
end
end
diff --git a/app/validators/variable_duplicates_validator.rb b/app/validators/variable_duplicates_validator.rb
index 72660be6c43..90193e85f2a 100644
--- a/app/validators/variable_duplicates_validator.rb
+++ b/app/validators/variable_duplicates_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# VariableDuplicatesValidator
#
# This validator is designed for especially the following condition
@@ -22,8 +24,8 @@ class VariableDuplicatesValidator < ActiveModel::EachValidator
def validate_duplicates(record, attribute, values)
duplicates = values.reject(&:marked_for_destruction?).group_by(&:key).select { |_, v| v.many? }.map(&:first)
if duplicates.any?
- error_message = "have duplicate values (#{duplicates.join(", ")})"
- error_message += " for #{values.first.send(options[:scope])} scope" if options[:scope] # rubocop:disable GitlabSecurity/PublicSend
+ error_message = +"have duplicate values (#{duplicates.join(", ")})"
+ error_message << " for #{values.first.send(options[:scope])} scope" if options[:scope] # rubocop:disable GitlabSecurity/PublicSend
record.errors.add(attribute, error_message)
end
end
diff --git a/app/views/abuse_report_mailer/notify.html.haml b/app/views/abuse_report_mailer/notify.html.haml
index d50b4071745..5abb0dc9182 100644
--- a/app/views/abuse_report_mailer/notify.html.haml
+++ b/app/views/abuse_report_mailer/notify.html.haml
@@ -4,7 +4,7 @@
#{link_to @abuse_report.reporter.name, user_url(@abuse_report.reporter)}
(@#{@abuse_report.reporter.username}).
-%blockquote
+.blockquote
= @abuse_report.message
%p
diff --git a/app/views/abuse_reports/new.html.haml b/app/views/abuse_reports/new.html.haml
index 06be1a53318..278ad210543 100644
--- a/app/views/abuse_reports/new.html.haml
+++ b/app/views/abuse_reports/new.html.haml
@@ -2,20 +2,20 @@
%h3.page-title Report abuse
%p Please use this form to report users who create spam issues, comments or behave inappropriately.
%hr
-= form_for @abuse_report, html: { class: 'form-horizontal js-quick-submit js-requires-input'} do |f|
+= form_for @abuse_report, html: { class: 'js-quick-submit js-requires-input'} do |f|
= form_errors(@abuse_report)
= f.hidden_field :user_id
- .form-group
- = f.label :user_id, class: 'control-label'
+ .form-group.row
+ = f.label :user_id, class: 'col-sm-2 col-form-label'
.col-sm-10
- name = "#{@abuse_report.user.name} (@#{@abuse_report.user.username})"
= text_field_tag :user_name, name, class: "form-control", readonly: true
- .form-group
- = f.label :message, class: 'control-label'
+ .form-group.row
+ = f.label :message, class: 'col-sm-2 col-form-label'
.col-sm-10
= f.text_area :message, class: "form-control", rows: 2, required: true, value: sanitize(@ref_url)
- .help-block
+ .form-text.text-muted
Explain the problem with this user. If appropriate, provide a link to the relevant issue or comment.
.form-actions
diff --git a/app/views/admin/abuse_reports/_abuse_report.html.haml b/app/views/admin/abuse_reports/_abuse_report.html.haml
index 18c6c559049..89a87f38968 100644
--- a/app/views/admin/abuse_reports/_abuse_report.html.haml
+++ b/app/views/admin/abuse_reports/_abuse_report.html.haml
@@ -1,7 +1,7 @@
- reporter = abuse_report.reporter
- user = abuse_report.user
%tr
- %th.visible-xs-block.visible-sm-block
+ %th.d-block.d-sm-none.d-md-none
%strong User
%td
- if user
@@ -11,7 +11,7 @@
- else
(removed)
%td
- %strong.subheading.visible-xs-block.visible-sm-block Reported by
+ %strong.subheading.d-block.d-sm-none.d-md-none Reported by
- if reporter
= link_to reporter.name, reporter
- else
@@ -19,7 +19,7 @@
.light.small
= time_ago_with_tooltip(abuse_report.created_at)
%td
- %strong.subheading.visible-xs-block.visible-sm-block Message
+ %strong.subheading.d-block.d-sm-none.d-md-none Message
.message
= markdown_field(abuse_report, :message)
%td
diff --git a/app/views/admin/abuse_reports/index.html.haml b/app/views/admin/abuse_reports/index.html.haml
index 6a5e170ddd8..cc29657a439 100644
--- a/app/views/admin/abuse_reports/index.html.haml
+++ b/app/views/admin/abuse_reports/index.html.haml
@@ -5,7 +5,7 @@
- if @abuse_reports.present?
.table-holder
%table.table.responsive-table
- %thead.hidden-sm.hidden-xs
+ %thead.d-none.d-sm-none.d-md-table-header-group
%tr
%th User
%th Reported by
diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml
index 15bda97c3b5..a0861870ba4 100644
--- a/app/views/admin/appearances/_form.html.haml
+++ b/app/views/admin/appearances/_form.html.haml
@@ -1,44 +1,63 @@
-= form_for @appearance, url: admin_appearances_path, html: { class: 'form-horizontal'} do |f|
+= form_for @appearance, url: admin_appearances_path do |f|
= form_errors(@appearance)
%fieldset.app_logo
%legend
Navigation bar:
- .form-group
- = f.label :header_logo, 'Header logo', class: 'control-label'
+ .form-group.row
+ = f.label :header_logo, 'Header logo', class: 'col-sm-2 col-form-label'
.col-sm-10
- if @appearance.header_logo?
= image_tag @appearance.header_logo_url, class: 'appearance-light-logo-preview'
- if @appearance.persisted?
%br
- = link_to 'Remove header logo', header_logos_admin_appearances_path, data: { confirm: "Header logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-logo"
+ = link_to 'Remove header logo', header_logos_admin_appearances_path, data: { confirm: "Header logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-inverted btn-remove btn-sm remove-logo"
%hr
= f.hidden_field :header_logo_cache
= f.file_field :header_logo, class: ""
.hint
Maximum file size is 1MB. Pages are optimized for a 28px tall header logo
+ %fieldset.app_logo
+ %legend
+ Favicon:
+ .form-group.row
+ = f.label :favicon, 'Favicon', class: 'col-sm-2 col-form-label'
+ .col-sm-10
+ - if @appearance.favicon?
+ = image_tag @appearance.favicon_url, class: 'appearance-light-logo-preview'
+ - if @appearance.persisted?
+ %br
+ = link_to 'Remove favicon', favicon_admin_appearances_path, data: { confirm: "Favicon will be removed. Are you sure?"}, method: :delete, class: "btn btn-inverted btn-remove btn-sm remove-logo"
+ %hr
+ = f.hidden_field :favicon_cache
+ = f.file_field :favicon, class: ''
+ .hint
+ Maximum file size is 1MB. Image size must be 32x32px. Allowed image formats are #{favicon_extension_whitelist}.
+ %br
+ Images with incorrect dimensions are not resized automatically, and may result in unexpected behavior.
+
%fieldset.sign-in
%legend
Sign in/Sign up pages:
- .form-group
- = f.label :title, class: 'control-label'
+ .form-group.row
+ = f.label :title, class: 'col-sm-2 col-form-label'
.col-sm-10
= f.text_field :title, class: "form-control"
- .form-group
- = f.label :description, class: 'control-label'
+ .form-group.row
+ = f.label :description, class: 'col-sm-2 col-form-label'
.col-sm-10
= f.text_area :description, class: "form-control", rows: 10
.hint
Description parsed with #{link_to "GitLab Flavored Markdown", help_page_path('user/markdown'), target: '_blank'}.
- .form-group
- = f.label :logo, class: 'control-label'
+ .form-group.row
+ = f.label :logo, class: 'col-sm-2 col-form-label'
.col-sm-10
- if @appearance.logo?
= image_tag @appearance.logo_url, class: 'appearance-logo-preview'
- if @appearance.persisted?
%br
- = link_to 'Remove logo', logo_admin_appearances_path, data: { confirm: "Logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-logo"
+ = link_to 'Remove logo', logo_admin_appearances_path, data: { confirm: "Logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-inverted btn-remove btn-sm remove-logo"
%hr
= f.hidden_field :logo_cache
= f.file_field :logo, class: ""
@@ -48,8 +67,8 @@
%fieldset
%legend
New project pages:
- .form-group
- = f.label :new_project_guidelines, class: 'control-label'
+ .form-group.row
+ = f.label :new_project_guidelines, class: 'col-sm-2 col-form-label'
.col-sm-10
= f.text_area :new_project_guidelines, class: "form-control", rows: 10
.hint
@@ -63,5 +82,5 @@
= link_to 'New project page', new_project_path, class: 'btn', target: '_blank', rel: 'noopener noreferrer'
- if @appearance.updated_at
- %span.pull-right
+ %span.float-right
Last edit #{time_ago_with_tooltip(@appearance.updated_at)}
diff --git a/app/views/admin/application_settings/_abuse.html.haml b/app/views/admin/application_settings/_abuse.html.haml
index bb3fa26a33e..91993838fc8 100644
--- a/app/views/admin/application_settings/_abuse.html.haml
+++ b/app/views/admin/application_settings/_abuse.html.haml
@@ -1,12 +1,11 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
- = f.label :admin_notification_email, 'Abuse reports notification email', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :admin_notification_email, class: 'form-control'
- .help-block
- Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area.
+ = f.label :admin_notification_email, 'Abuse reports notification email', class: 'label-light'
+ = f.text_field :admin_notification_email, class: 'form-control'
+ .form-text.text-muted
+ Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area.
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_account_and_limit.html.haml b/app/views/admin/application_settings/_account_and_limit.html.haml
index dd86c9ed2eb..f40939747f4 100644
--- a/app/views/admin/application_settings/_account_and_limit.html.haml
+++ b/app/views/admin/application_settings/_account_and_limit.html.haml
@@ -1,39 +1,33 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :gravatar_enabled do
- = f.check_box :gravatar_enabled
- Gravatar enabled
+ .form-check
+ = f.check_box :gravatar_enabled, class: 'form-check-input'
+ = f.label :gravatar_enabled, class: 'form-check-label' do
+ Gravatar enabled
.form-group
- = f.label :default_projects_limit, class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :default_projects_limit, class: 'form-control'
+ = f.label :default_projects_limit, class: 'label-light'
+ = f.number_field :default_projects_limit, class: 'form-control'
.form-group
- = f.label :max_attachment_size, 'Maximum attachment size (MB)', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :max_attachment_size, class: 'form-control'
+ = f.label :max_attachment_size, 'Maximum attachment size (MB)', class: 'label-light'
+ = f.number_field :max_attachment_size, class: 'form-control'
.form-group
- = f.label :session_expire_delay, 'Session duration (minutes)', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :session_expire_delay, class: 'form-control'
- %span.help-block#session_expire_delay_help_block GitLab restart is required to apply changes
+ = f.label :session_expire_delay, 'Session duration (minutes)', class: 'label-light'
+ = f.number_field :session_expire_delay, class: 'form-control'
+ %span.form-text.text-muted#session_expire_delay_help_block GitLab restart is required to apply changes
.form-group
- = f.label :user_oauth_applications, 'User OAuth applications', class: 'control-label col-sm-2'
- .col-sm-10
- .checkbox
- = f.label :user_oauth_applications do
- = f.check_box :user_oauth_applications
- Allow users to register any application to use GitLab as an OAuth provider
+ = f.label :user_oauth_applications, 'User OAuth applications', class: 'label-light'
+ .form-check
+ = f.check_box :user_oauth_applications, class: 'form-check-input'
+ = f.label :user_oauth_applications, class: 'form-check-label' do
+ Allow users to register any application to use GitLab as an OAuth provider
.form-group
- = f.label :user_default_external, 'New users set to external', class: 'control-label col-sm-2'
- .col-sm-10
- .checkbox
- = f.label :user_default_external do
- = f.check_box :user_default_external
- Newly registered users will by default be external
+ = f.label :user_default_external, 'New users set to external', class: 'label-light'
+ .form-check
+ = f.check_box :user_default_external, class: 'form-check-input'
+ = f.label :user_default_external, class: 'form-check-label' do
+ Newly registered users will by default be external
= f.submit 'Save changes', class: 'btn btn-success'
diff --git a/app/views/admin/application_settings/_background_jobs.html.haml b/app/views/admin/application_settings/_background_jobs.html.haml
index 8198a822a10..fd8e695ed49 100644
--- a/app/views/admin/application_settings/_background_jobs.html.haml
+++ b/app/views/admin/application_settings/_background_jobs.html.haml
@@ -1,4 +1,4 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
@@ -7,24 +7,21 @@
= link_to 'restart', help_page_path('administration/restart_gitlab')
to take effect.
.form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :sidekiq_throttling_enabled do
- = f.check_box :sidekiq_throttling_enabled
- Enable Sidekiq Job Throttling
- .help-block
- Limit the amount of resources slow running jobs are assigned.
+ .form-check
+ = f.check_box :sidekiq_throttling_enabled, class: 'form-check-input'
+ = f.label :sidekiq_throttling_enabled, class: 'form-check-label' do
+ Enable Sidekiq Job Throttling
+ .form-text.text-muted
+ Limit the amount of resources slow running jobs are assigned.
.form-group
- = f.label :sidekiq_throttling_queues, 'Sidekiq queues to throttle', class: 'control-label col-sm-2'
- .col-sm-10
- = f.select :sidekiq_throttling_queues, sidekiq_queue_options_for_select, { include_hidden: false }, multiple: true, class: 'select2 select-wide', data: { field: 'sidekiq_throttling_queues' }
- .help-block
- Choose which queues you wish to throttle.
+ = f.label :sidekiq_throttling_queues, 'Sidekiq queues to throttle', class: 'label-light'
+ = f.select :sidekiq_throttling_queues, sidekiq_queue_options_for_select, { include_hidden: false }, multiple: true, class: 'select2 select-wide', data: { field: 'sidekiq_throttling_queues' }
+ .form-text.text-muted
+ Choose which queues you wish to throttle.
.form-group
- = f.label :sidekiq_throttling_factor, 'Throttling Factor', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :sidekiq_throttling_factor, class: 'form-control', min: '0.01', max: '0.99', step: '0.01'
- .help-block
- The factor by which the queues should be throttled. A value between 0.0 and 1.0, exclusive.
+ = f.label :sidekiq_throttling_factor, 'Throttling Factor', class: 'label-light'
+ = f.number_field :sidekiq_throttling_factor, class: 'form-control', min: '0.01', max: '0.99', step: '0.01'
+ .form-text.text-muted
+ The factor by which the queues should be throttled. A value between 0.0 and 1.0, exclusive.
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_ci_cd.html.haml b/app/views/admin/application_settings/_ci_cd.html.haml
index 079f371e0ba..7c16cafe13f 100644
--- a/app/views/admin/application_settings/_ci_cd.html.haml
+++ b/app/views/admin/application_settings/_ci_cd.html.haml
@@ -1,47 +1,41 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :auto_devops_enabled do
- = f.check_box :auto_devops_enabled
- Enabled Auto DevOps for projects by default
- .help-block
- It will automatically build, test, and deploy applications based on a predefined CI/CD configuration
- = link_to icon('question-circle'), help_page_path('topics/autodevops/index.md')
+ .form-check
+ = f.check_box :auto_devops_enabled, class: 'form-check-input'
+ = f.label :auto_devops_enabled, class: 'form-check-label' do
+ Enabled Auto DevOps for projects by default
+ .form-text.text-muted
+ It will automatically build, test, and deploy applications based on a predefined CI/CD configuration
+ = link_to icon('question-circle'), help_page_path('topics/autodevops/index.md')
.form-group
- = f.label :auto_devops_domain, class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :auto_devops_domain, class: 'form-control', placeholder: 'domain.com'
- .help-block
- = s_("AdminSettings|Specify a domain to use by default for every project's Auto Review Apps and Auto Deploy stages.")
+ = f.label :auto_devops_domain, class: 'label-light'
+ = f.text_field :auto_devops_domain, class: 'form-control', placeholder: 'domain.com'
+ .form-text.text-muted
+ = s_("AdminSettings|Specify a domain to use by default for every project's Auto Review Apps and Auto Deploy stages.")
.form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :shared_runners_enabled do
- = f.check_box :shared_runners_enabled
- Enable shared runners for new projects
+ .form-check
+ = f.check_box :shared_runners_enabled, class: 'form-check-input'
+ = f.label :shared_runners_enabled, class: 'form-check-label' do
+ Enable shared runners for new projects
.form-group
- = f.label :shared_runners_text, class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_area :shared_runners_text, class: 'form-control', rows: 4
- .help-block Markdown enabled
+ = f.label :shared_runners_text, class: 'label-light'
+ = f.text_area :shared_runners_text, class: 'form-control', rows: 4
+ .form-text.text-muted Markdown enabled
.form-group
- = f.label :max_artifacts_size, 'Maximum artifacts size (MB)', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :max_artifacts_size, class: 'form-control'
- .help-block
- Set the maximum file size for each job's artifacts
- = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'maximum-artifacts-size')
+ = f.label :max_artifacts_size, 'Maximum artifacts size (MB)', class: 'label-light'
+ = f.number_field :max_artifacts_size, class: 'form-control'
+ .form-text.text-muted
+ Set the maximum file size for each job's artifacts
+ = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'maximum-artifacts-size')
.form-group
- = f.label :default_artifacts_expire_in, 'Default artifacts expiration', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :default_artifacts_expire_in, class: 'form-control'
- .help-block
- Set the default expiration time for each job's artifacts.
- 0 for unlimited.
- = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration')
+ = f.label :default_artifacts_expire_in, 'Default artifacts expiration', class: 'label-light'
+ = f.text_field :default_artifacts_expire_in, class: 'form-control'
+ .form-text.text-muted
+ Set the default expiration time for each job's artifacts.
+ 0 for unlimited.
+ = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration')
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_email.html.haml b/app/views/admin/application_settings/_email.html.haml
index 6c89f1c4e98..99e44ffa741 100644
--- a/app/views/admin/application_settings/_email.html.haml
+++ b/app/views/admin/application_settings/_email.html.haml
@@ -1,26 +1,24 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :email_author_in_body do
- = f.check_box :email_author_in_body
- Include author name in notification email body
- .help-block
- 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.
+ .form-check
+ = f.check_box :email_author_in_body, class: 'form-check-input'
+ = f.label :email_author_in_body, class: 'form-check-label' do
+ Include author name in notification email body
+ .form-text.text-muted
+ 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.
.form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :html_emails_enabled do
- = f.check_box :html_emails_enabled
- Enable HTML emails
- .help-block
- By default GitLab sends emails in HTML and plain text formats so mail
- clients can choose what format to use. Disable this option if you only
- want to send emails in plain text format.
+ .form-check
+ = f.check_box :html_emails_enabled, class: 'form-check-input'
+ = f.label :html_emails_enabled, class: 'form-check-label' do
+ Enable HTML emails
+ .form-text.text-muted
+ By default GitLab sends emails in HTML and plain text formats so mail
+ clients can choose what format to use. Disable this option if you only
+ want to send emails in plain text format.
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_gitaly.html.haml b/app/views/admin/application_settings/_gitaly.html.haml
index 4acc5b3a0c5..0b4001c0824 100644
--- a/app/views/admin/application_settings/_gitaly.html.haml
+++ b/app/views/admin/application_settings/_gitaly.html.haml
@@ -1,27 +1,24 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.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.
+ = f.label :gitaly_timeout_default, 'Default Timeout Period', class: 'label-light'
+ = f.number_field :gitaly_timeout_default, class: 'form-control'
+ .form-text.text-muted
+ 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.
+ = f.label :gitaly_timeout_fast, 'Fast Timeout Period', class: 'label-light'
+ = f.number_field :gitaly_timeout_fast, class: 'form-control'
+ .form-text.text-muted
+ 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.
+ = f.label :gitaly_timeout_medium, 'Medium Timeout Period', class: 'label-light'
+ = f.number_field :gitaly_timeout_medium, class: 'form-control'
+ .form-text.text-muted
+ Medium operation timeout (in seconds). This should be a value between the Fast and the Default timeout.
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_help_page.html.haml b/app/views/admin/application_settings/_help_page.html.haml
index 3bc101ddf04..1f402fcb786 100644
--- a/app/views/admin/application_settings/_help_page.html.haml
+++ b/app/views/admin/application_settings/_help_page.html.haml
@@ -1,22 +1,19 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
- = f.label :help_page_text, class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_area :help_page_text, class: 'form-control', rows: 4
- .help-block Markdown enabled
+ = f.label :help_page_text, class: 'label-light'
+ = f.text_area :help_page_text, class: 'form-control', rows: 4
+ .form-text.text-muted Markdown enabled
.form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :help_page_hide_commercial_content do
- = f.check_box :help_page_hide_commercial_content
- Hide marketing-related entries from help
+ .form-check
+ = f.check_box :help_page_hide_commercial_content, class: 'form-check-input'
+ = f.label :help_page_hide_commercial_content, class: 'form-check-label' do
+ Hide marketing-related entries from help
.form-group
- = f.label :help_page_support_url, 'Support page URL', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :help_page_support_url, class: 'form-control', placeholder: 'http://company.example.com/getting-help', :'aria-describedby' => 'support_help_block'
- %span.help-block#support_help_block Alternate support URL for help page
+ = f.label :help_page_support_url, 'Support page URL', class: 'label-light'
+ = f.text_field :help_page_support_url, class: 'form-control', placeholder: 'http://company.example.com/getting-help', :'aria-describedby' => 'support_help_block'
+ %span.form-text.text-muted#support_help_block Alternate support URL for help page
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_influx.html.haml b/app/views/admin/application_settings/_influx.html.haml
index a173fd38a9c..61e8e3199a9 100644
--- a/app/views/admin/application_settings/_influx.html.haml
+++ b/app/views/admin/application_settings/_influx.html.haml
@@ -1,4 +1,4 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
@@ -9,60 +9,52 @@
to take effect.
= link_to icon('question-circle'), help_page_path('administration/monitoring/performance/introduction')
.form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :metrics_enabled do
- = f.check_box :metrics_enabled
- Enable InfluxDB Metrics
- .form-group
- = f.label :metrics_host, 'InfluxDB host', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :metrics_host, class: 'form-control', placeholder: 'influxdb.example.com'
- .form-group
- = f.label :metrics_port, 'InfluxDB port', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :metrics_port, class: 'form-control', placeholder: '8089'
- .help-block
- The UDP port to use for connecting to InfluxDB. InfluxDB requires that
- your server configuration specifies a database to store data in when
- sending messages to this port, without it metrics data will not be
- saved.
- .form-group
- = f.label :metrics_pool_size, 'Connection pool size', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :metrics_pool_size, class: 'form-control'
- .help-block
- The amount of InfluxDB connections to open. Connections are opened
- lazily. Users using multi-threaded application servers should ensure
- enough connections are available (at minimum the amount of application
- server threads).
- .form-group
- = f.label :metrics_timeout, 'Connection timeout', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :metrics_timeout, class: 'form-control'
- .help-block
- The amount of seconds after which an InfluxDB connection will time
- out.
- .form-group
- = f.label :metrics_method_call_threshold, 'Method Call Threshold (ms)', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :metrics_method_call_threshold, class: 'form-control'
- .help-block
- A method call is only tracked when it takes longer to complete than
- the given amount of milliseconds.
- .form-group
- = f.label :metrics_sample_interval, 'Sampler Interval (sec)', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :metrics_sample_interval, class: 'form-control'
- .help-block
- The sampling interval in seconds. Sampled data includes memory usage,
- retained Ruby objects, file descriptors and so on.
- .form-group
- = f.label :metrics_packet_size, 'Metrics per packet', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :metrics_packet_size, class: 'form-control'
- .help-block
- The amount of points to store in a single UDP packet. More points
- results in fewer but larger UDP packets being sent.
+ .form-check
+ = f.check_box :metrics_enabled, class: 'form-check-input'
+ = f.label :metrics_enabled, class: 'form-check-label' do
+ Enable InfluxDB Metrics
+ .form-group
+ = f.label :metrics_host, 'InfluxDB host', class: 'label-light'
+ = f.text_field :metrics_host, class: 'form-control', placeholder: 'influxdb.example.com'
+ .form-group
+ = f.label :metrics_port, 'InfluxDB port', class: 'label-light'
+ = f.text_field :metrics_port, class: 'form-control', placeholder: '8089'
+ .form-text.text-muted
+ The UDP port to use for connecting to InfluxDB. InfluxDB requires that
+ your server configuration specifies a database to store data in when
+ sending messages to this port, without it metrics data will not be
+ saved.
+ .form-group
+ = f.label :metrics_pool_size, 'Connection pool size', class: 'label-light'
+ = f.number_field :metrics_pool_size, class: 'form-control'
+ .form-text.text-muted
+ The amount of InfluxDB connections to open. Connections are opened
+ lazily. Users using multi-threaded application servers should ensure
+ enough connections are available (at minimum the amount of application
+ server threads).
+ .form-group
+ = f.label :metrics_timeout, 'Connection timeout', class: 'label-light'
+ = f.number_field :metrics_timeout, class: 'form-control'
+ .form-text.text-muted
+ The amount of seconds after which an InfluxDB connection will time
+ out.
+ .form-group
+ = f.label :metrics_method_call_threshold, 'Method Call Threshold (ms)', class: 'label-light'
+ = f.number_field :metrics_method_call_threshold, class: 'form-control'
+ .form-text.text-muted
+ A method call is only tracked when it takes longer to complete than
+ the given amount of milliseconds.
+ .form-group
+ = f.label :metrics_sample_interval, 'Sampler Interval (sec)', class: 'label-light'
+ = f.number_field :metrics_sample_interval, class: 'form-control'
+ .form-text.text-muted
+ The sampling interval in seconds. Sampled data includes memory usage,
+ retained Ruby objects, file descriptors and so on.
+ .form-group
+ = f.label :metrics_packet_size, 'Metrics per packet', class: 'label-light'
+ = f.number_field :metrics_packet_size, class: 'form-control'
+ .form-text.text-muted
+ The amount of points to store in a single UDP packet. More points
+ results in fewer but larger UDP packets being sent.
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_ip_limits.html.haml b/app/views/admin/application_settings/_ip_limits.html.haml
index b83ffc375d9..73d570a5fee 100644
--- a/app/views/admin/application_settings/_ip_limits.html.haml
+++ b/app/views/admin/application_settings/_ip_limits.html.haml
@@ -1,54 +1,45 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.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-check
+ = f.check_box :throttle_unauthenticated_enabled, class: 'form-check-input'
+ = f.label :throttle_unauthenticated_enabled, class: 'form-check-label' do
+ Enable unauthenticated request rate limit
+ %span.form-text.text-muted
+ 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: 'label-light'
+ = 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: 'label-light'
+ = f.number_field :throttle_unauthenticated_period_in_seconds, class: 'form-control'
+ .form-group
+ .form-check
+ = f.check_box :throttle_authenticated_api_enabled, class: 'form-check-input'
+ = f.label :throttle_authenticated_api_enabled, class: 'form-check-label' do
+ Enable authenticated API request rate limit
+ %span.form-text.text-muted
+ 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: 'label-light'
+ = 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: 'label-light'
+ = f.number_field :throttle_authenticated_api_period_in_seconds, class: 'form-control'
+ .form-group
+ .form-check
+ = f.check_box :throttle_authenticated_web_enabled, class: 'form-check-input'
+ = f.label :throttle_authenticated_web_enabled, class: 'form-check-label' do
+ Enable authenticated web request rate limit
+ %span.form-text.text-muted
+ 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: 'label-light'
+ = 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: 'label-light'
+ = f.number_field :throttle_authenticated_web_period_in_seconds, class: 'form-control'
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_koding.html.haml b/app/views/admin/application_settings/_koding.html.haml
index 17358cf775b..ae60f68f5fe 100644
--- a/app/views/admin/application_settings/_koding.html.haml
+++ b/app/views/admin/application_settings/_koding.html.haml
@@ -1,24 +1,22 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :koding_enabled do
- = f.check_box :koding_enabled
- Enable Koding
- .help-block
- Koding integration has been deprecated since GitLab 10.0. If you disable your Koding integration, you will not be able to enable it again.
+ .form-check
+ = f.check_box :koding_enabled, class: 'form-check-input'
+ = f.label :koding_enabled, class: 'form-check-label' do
+ Enable Koding
+ .form-text.text-muted
+ Koding integration has been deprecated since GitLab 10.0. If you disable your Koding integration, you will not be able to enable it again.
.form-group
- = f.label :koding_url, 'Koding URL', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :koding_url, class: 'form-control', placeholder: 'http://gitlab.your-koding-instance.com:8090'
- .help-block
- Koding has integration enabled out of the box for the
- %strong gitlab
- team, and you need to provide that team's URL here. Learn more in the
- = succeed "." do
- = link_to "Koding administration documentation", help_page_path("administration/integration/koding")
+ = f.label :koding_url, 'Koding URL', class: 'label-light'
+ = f.text_field :koding_url, class: 'form-control', placeholder: 'http://gitlab.your-koding-instance.com:8090'
+ .form-text.text-muted
+ Koding has integration enabled out of the box for the
+ %strong gitlab
+ team, and you need to provide that team's URL here. Learn more in the
+ = succeed "." do
+ = link_to "Koding administration documentation", help_page_path("administration/integration/koding")
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_logging.html.haml b/app/views/admin/application_settings/_logging.html.haml
index 44a11ddc120..a6e549cd1f0 100644
--- a/app/views/admin/application_settings/_logging.html.haml
+++ b/app/views/admin/application_settings/_logging.html.haml
@@ -1,36 +1,32 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :sentry_enabled do
- = f.check_box :sentry_enabled
- Enable Sentry
- .help-block
- %p This setting requires a restart to take effect.
- Sentry is an error reporting and logging tool which is currently not shipped with GitLab, get it here:
- %a{ href: 'https://getsentry.com', target: '_blank', rel: 'noopener noreferrer' } https://getsentry.com
+ .form-check
+ = f.check_box :sentry_enabled, class: 'form-check-input'
+ = f.label :sentry_enabled, class: 'form-check-label' do
+ Enable Sentry
+ .form-text.text-muted
+ %p This setting requires a restart to take effect.
+ Sentry is an error reporting and logging tool which is currently not shipped with GitLab, get it here:
+ %a{ href: 'https://getsentry.com', target: '_blank', rel: 'noopener noreferrer' } https://getsentry.com
.form-group
- = f.label :sentry_dsn, 'Sentry DSN', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :sentry_dsn, class: 'form-control'
+ = f.label :sentry_dsn, 'Sentry DSN', class: 'label-light'
+ = f.text_field :sentry_dsn, class: 'form-control'
.form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :clientside_sentry_enabled do
- = f.check_box :clientside_sentry_enabled
- Enable Clientside Sentry
- .help-block
- Sentry can also be used for reporting and logging clientside exceptions.
- %a{ href: 'https://sentry.io/for/javascript/', target: '_blank', rel: 'noopener noreferrer' } https://sentry.io/for/javascript/
+ .form-check
+ = f.check_box :clientside_sentry_enabled, class: 'form-check-input'
+ = f.label :clientside_sentry_enabled, class: 'form-check-label' do
+ Enable Clientside Sentry
+ .form-text.text-muted
+ Sentry can also be used for reporting and logging clientside exceptions.
+ %a{ href: 'https://sentry.io/for/javascript/', target: '_blank', rel: 'noopener noreferrer' } https://sentry.io/for/javascript/
.form-group
- = f.label :clientside_sentry_dsn, 'Clientside Sentry DSN', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :clientside_sentry_dsn, class: 'form-control'
+ = f.label :clientside_sentry_dsn, 'Clientside Sentry DSN', class: 'label-light'
+ = f.text_field :clientside_sentry_dsn, class: 'form-control'
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_outbound.html.haml b/app/views/admin/application_settings/_outbound.html.haml
index d10f609006d..e046242bee0 100644
--- a/app/views/admin/application_settings/_outbound.html.haml
+++ b/app/views/admin/application_settings/_outbound.html.haml
@@ -1,12 +1,11 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :allow_local_requests_from_hooks_and_services do
- = f.check_box :allow_local_requests_from_hooks_and_services
- Allow requests to the local network from hooks and services
+ .form-check
+ = f.check_box :allow_local_requests_from_hooks_and_services, class: 'form-check-input'
+ = f.label :allow_local_requests_from_hooks_and_services, class: 'form-check-label' do
+ Allow requests to the local network from hooks and services
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_pages.html.haml b/app/views/admin/application_settings/_pages.html.haml
index b28ecf9a039..f168ec62ffd 100644
--- a/app/views/admin/application_settings/_pages.html.haml
+++ b/app/views/admin/application_settings/_pages.html.haml
@@ -1,22 +1,20 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
- = f.label :max_pages_size, 'Maximum size of pages (MB)', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :max_pages_size, class: 'form-control'
- .help-block 0 for unlimited
+ = f.label :max_pages_size, 'Maximum size of pages (MB)', class: 'label-light'
+ = f.number_field :max_pages_size, class: 'form-control'
+ .form-text.text-muted 0 for unlimited
.form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :pages_domain_verification_enabled do
- = f.check_box :pages_domain_verification_enabled
- Require users to prove ownership of custom domains
- .help-block
- Domain verification is an essential security measure for public GitLab
- sites. Users are required to demonstrate they control a domain before
- it is enabled
- = link_to icon('question-circle'), help_page_path('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record')
+ .form-check
+ = f.check_box :pages_domain_verification_enabled, class: 'form-check-input'
+ = f.label :pages_domain_verification_enabled, class: 'form-check-label' do
+ Require users to prove ownership of custom domains
+ .form-text.text-muted
+ Domain verification is an essential security measure for public GitLab
+ sites. Users are required to demonstrate they control a domain before
+ it is enabled
+ = link_to icon('question-circle'), help_page_path('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record')
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_performance.html.haml b/app/views/admin/application_settings/_performance.html.haml
index 01d5a31aa9f..ffa25af77ed 100644
--- a/app/views/admin/application_settings/_performance.html.haml
+++ b/app/views/admin/application_settings/_performance.html.haml
@@ -1,19 +1,18 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :authorized_keys_enabled do
- = f.check_box :authorized_keys_enabled
- Write to "authorized_keys" file
- .help-block
- By default, we write to the "authorized_keys" file to support Git
- over SSH without additional configuration. GitLab can be optimized
- to authenticate SSH keys via the database file. Only uncheck this
- if you have configured your OpenSSH server to use the
- AuthorizedKeysCommand. Click on the help icon for more details.
- = link_to icon('question-circle'), help_page_path('administration/operations/fast_ssh_key_lookup')
+ .form-check
+ = f.check_box :authorized_keys_enabled, class: 'form-check-input'
+ = f.label :authorized_keys_enabled, class: 'form-check-label' do
+ Write to "authorized_keys" file
+ .form-text.text-muted
+ By default, we write to the "authorized_keys" file to support Git
+ over SSH without additional configuration. GitLab can be optimized
+ to authenticate SSH keys via the database file. Only uncheck this
+ if you have configured your OpenSSH server to use the
+ AuthorizedKeysCommand. Click on the help icon for more details.
+ = link_to icon('question-circle'), help_page_path('administration/operations/fast_ssh_key_lookup')
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_performance_bar.html.haml b/app/views/admin/application_settings/_performance_bar.html.haml
index 5344f030c97..ddbfcc6b77b 100644
--- a/app/views/admin/application_settings/_performance_bar.html.haml
+++ b/app/views/admin/application_settings/_performance_bar.html.haml
@@ -1,16 +1,14 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :performance_bar_enabled do
- = f.check_box :performance_bar_enabled
- Enable the Performance Bar
+ .form-check
+ = f.check_box :performance_bar_enabled, class: 'form-check-input'
+ = f.label :performance_bar_enabled, class: 'form-check-label' do
+ Enable the Performance Bar
.form-group
- = f.label :performance_bar_allowed_group_id, 'Allowed group', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :performance_bar_allowed_group_id, class: 'form-control', placeholder: 'my-org/my-group', value: @application_setting.performance_bar_allowed_group&.full_path
+ = f.label :performance_bar_allowed_group_path, 'Allowed group', class: 'label-light'
+ = f.text_field :performance_bar_allowed_group_path, class: 'form-control', placeholder: 'my-org/my-group', value: @application_setting.performance_bar_allowed_group&.full_path
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_plantuml.html.haml b/app/views/admin/application_settings/_plantuml.html.haml
index 56764b3fb81..259f18b3b96 100644
--- a/app/views/admin/application_settings/_plantuml.html.haml
+++ b/app/views/admin/application_settings/_plantuml.html.haml
@@ -1,20 +1,18 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :plantuml_enabled do
- = f.check_box :plantuml_enabled
- Enable PlantUML
+ .form-check
+ = f.check_box :plantuml_enabled, class: 'form-check-input'
+ = f.label :plantuml_enabled, class: 'form-check-label' do
+ Enable PlantUML
.form-group
- = f.label :plantuml_url, 'PlantUML URL', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :plantuml_url, class: 'form-control', placeholder: 'http://gitlab.your-plantuml-instance.com:8080'
- .help-block
- Allow rendering of
- = link_to "PlantUML", "http://plantuml.com"
- diagrams in Asciidoc documents using an external PlantUML service.
+ = f.label :plantuml_url, 'PlantUML URL', class: 'label-light'
+ = f.text_field :plantuml_url, class: 'form-control', placeholder: 'http://gitlab.your-plantuml-instance.com:8080'
+ .form-text.text-muted
+ Allow rendering of
+ = link_to "PlantUML", "http://plantuml.com"
+ diagrams in Asciidoc documents using an external PlantUML service.
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_prometheus.html.haml b/app/views/admin/application_settings/_prometheus.html.haml
index 48745db2991..ad92b18b2c9 100644
--- a/app/views/admin/application_settings/_prometheus.html.haml
+++ b/app/views/admin/application_settings/_prometheus.html.haml
@@ -1,4 +1,4 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
@@ -12,17 +12,16 @@
to take effect.
= link_to icon('question-circle'), help_page_path('administration/monitoring/prometheus/index')
.form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :prometheus_metrics_enabled do
- = f.check_box :prometheus_metrics_enabled
- Enable Prometheus Metrics
- - unless Gitlab::Metrics.metrics_folder_present?
- .help-block
- %strong.cred WARNING:
- Environment variable
- %code prometheus_multiproc_dir
- does not exist or is not pointing to a valid directory.
- = link_to icon('question-circle'), help_page_path('administration/monitoring/prometheus/gitlab_metrics', anchor: 'metrics-shared-directory')
+ .form-check
+ = f.check_box :prometheus_metrics_enabled, class: 'form-check-input'
+ = f.label :prometheus_metrics_enabled, class: 'form-check-label' do
+ Enable Prometheus Metrics
+ - unless Gitlab::Metrics.metrics_folder_present?
+ .form-text.text-muted
+ %strong.cred WARNING:
+ Environment variable
+ %code prometheus_multiproc_dir
+ does not exist or is not pointing to a valid directory.
+ = link_to icon('question-circle'), help_page_path('administration/monitoring/prometheus/gitlab_metrics', anchor: 'metrics-shared-directory')
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_realtime.html.haml b/app/views/admin/application_settings/_realtime.html.haml
index 0a53a75119e..120cf4909b2 100644
--- a/app/views/admin/application_settings/_realtime.html.haml
+++ b/app/views/admin/application_settings/_realtime.html.haml
@@ -1,19 +1,18 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
- = f.label :polling_interval_multiplier, 'Polling interval multiplier', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :polling_interval_multiplier, class: 'form-control'
- .help-block
- Change this value to influence how frequently the GitLab UI polls for updates.
- If you set the value to 2 all polling intervals are multiplied
- by 2, which means that polling happens half as frequently.
- The multiplier can also have a decimal value.
- The default value (1) is a reasonable choice for the majority of GitLab
- installations. Set to 0 to completely disable polling.
- = link_to icon('question-circle'), help_page_path('administration/polling')
+ = f.label :polling_interval_multiplier, 'Polling interval multiplier', class: 'label-light'
+ = f.text_field :polling_interval_multiplier, class: 'form-control'
+ .form-text.text-muted
+ Change this value to influence how frequently the GitLab UI polls for updates.
+ If you set the value to 2 all polling intervals are multiplied
+ by 2, which means that polling happens half as frequently.
+ The multiplier can also have a decimal value.
+ The default value (1) is a reasonable choice for the majority of GitLab
+ installations. Set to 0 to completely disable polling.
+ = link_to icon('question-circle'), help_page_path('administration/polling')
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_registry.html.haml b/app/views/admin/application_settings/_registry.html.haml
index 3451ef62458..beac70482e5 100644
--- a/app/views/admin/application_settings/_registry.html.haml
+++ b/app/views/admin/application_settings/_registry.html.haml
@@ -1,10 +1,9 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
- = f.label :container_registry_token_expire_delay, 'Authorization token duration (minutes)', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :container_registry_token_expire_delay, class: 'form-control'
+ = f.label :container_registry_token_expire_delay, 'Authorization token duration (minutes)', class: 'label-light'
+ = f.number_field :container_registry_token_expire_delay, class: 'form-control'
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_repository_check.html.haml b/app/views/admin/application_settings/_repository_check.html.haml
index fe335f30a62..57facc380eb 100644
--- a/app/views/admin/application_settings/_repository_check.html.haml
+++ b/app/views/admin/application_settings/_repository_check.html.haml
@@ -1,62 +1,56 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.sub-section
%h4 Repository checks
.form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :repository_checks_enabled do
- = f.check_box :repository_checks_enabled
- Enable Repository Checks
- .help-block
- GitLab will periodically run
- %a{ href: 'https://git-scm.com/docs/git-fsck', target: 'blank' } 'git fsck'
- in all project and wiki repositories to look for silent disk corruption issues.
+ .form-check
+ = f.check_box :repository_checks_enabled, class: 'form-check-input'
+ = f.label :repository_checks_enabled, class: 'form-check-label' do
+ Enable Repository Checks
+ .form-text.text-muted
+ GitLab will periodically run
+ %a{ href: 'https://git-scm.com/docs/git-fsck', target: 'blank' } 'git fsck'
+ in all project and wiki repositories to look for silent disk corruption issues.
.form-group
- .col-sm-offset-2.col-sm-10
- = link_to 'Clear all repository checks', clear_repository_check_states_admin_application_settings_path, data: { confirm: 'This will clear repository check states for ALL projects in the database. This cannot be undone. Are you sure?' }, method: :put, class: "btn btn-sm btn-remove"
- .help-block
- If you got a lot of false alarms from repository checks you can choose to clear all repository check information from the database.
+ = link_to 'Clear all repository checks', clear_repository_check_states_admin_application_settings_path, data: { confirm: 'This will clear repository check states for ALL projects in the database. This cannot be undone. Are you sure?' }, method: :put, class: "btn btn-sm btn-remove"
+ .form-text.text-muted
+ If you got a lot of false alarms from repository checks you can choose to clear all repository check information from the database.
.sub-section
%h4 Housekeeping
.form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :housekeeping_enabled do
- = f.check_box :housekeeping_enabled
- Enable automatic repository housekeeping (git repack, git gc)
- .help-block
- If you keep automatic housekeeping disabled for a long time Git
- repository access on your GitLab server will become slower and your
- repositories will use more disk space. We recommend to always leave
- this enabled.
- .checkbox
- = f.label :housekeeping_bitmaps_enabled do
- = f.check_box :housekeeping_bitmaps_enabled
- Enable Git pack file bitmap creation
- .help-block
- Creating pack file bitmaps makes housekeeping take a little longer but
- bitmaps should accelerate 'git clone' performance.
+ .form-check
+ = f.check_box :housekeeping_enabled, class: 'form-check-input'
+ = f.label :housekeeping_enabled, class: 'form-check-label' do
+ Enable automatic repository housekeeping (git repack, git gc)
+ .form-text.text-muted
+ If you keep automatic housekeeping disabled for a long time Git
+ repository access on your GitLab server will become slower and your
+ repositories will use more disk space. We recommend to always leave
+ this enabled.
+ .form-check
+ = f.check_box :housekeeping_bitmaps_enabled, class: 'form-check-input'
+ = f.label :housekeeping_bitmaps_enabled, class: 'form-check-label' do
+ Enable Git pack file bitmap creation
+ .form-text.text-muted
+ Creating pack file bitmaps makes housekeeping take a little longer but
+ bitmaps should accelerate 'git clone' performance.
.form-group
- = f.label :housekeeping_incremental_repack_period, 'Incremental repack period', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :housekeeping_incremental_repack_period, class: 'form-control'
- .help-block
- Number of Git pushes after which an incremental 'git repack' is run.
+ = f.label :housekeeping_incremental_repack_period, 'Incremental repack period', class: 'label-light'
+ = f.number_field :housekeeping_incremental_repack_period, class: 'form-control'
+ .form-text.text-muted
+ Number of Git pushes after which an incremental 'git repack' is run.
.form-group
- = f.label :housekeeping_full_repack_period, 'Full repack period', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :housekeeping_full_repack_period, class: 'form-control'
- .help-block
- Number of Git pushes after which a full 'git repack' is run.
+ = f.label :housekeeping_full_repack_period, 'Full repack period', class: 'label-light'
+ = f.number_field :housekeeping_full_repack_period, class: 'form-control'
+ .form-text.text-muted
+ Number of Git pushes after which a full 'git repack' is run.
.form-group
- = f.label :housekeeping_gc_period, 'Git GC period', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :housekeeping_gc_period, class: 'form-control'
- .help-block
- Number of Git pushes after which 'git gc' is run.
+ = f.label :housekeeping_gc_period, 'Git GC period', class: 'label-light'
+ = f.number_field :housekeeping_gc_period, class: 'form-control'
+ .form-text.text-muted
+ Number of Git pushes after which 'git gc' is run.
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_repository_mirrors_form.html.haml b/app/views/admin/application_settings/_repository_mirrors_form.html.haml
index 09183ec6260..beeb5169361 100644
--- a/app/views/admin/application_settings/_repository_mirrors_form.html.haml
+++ b/app/views/admin/application_settings/_repository_mirrors_form.html.haml
@@ -1,16 +1,15 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path do |f|
= form_errors(@application_setting)
%fieldset
.form-group
- = f.label :mirror_available, 'Enable mirror configuration', class: 'control-label col-sm-2'
- .col-sm-10
- .checkbox
- = f.label :mirror_available do
- = f.check_box :mirror_available
- Allow mirrors to be setup for projects
- %span.help-block
- If disabled, only admins will be able to setup mirrors in projects.
- = link_to icon('question-circle'), help_page_path('workflow/repository_mirroring')
+ = f.label :mirror_available, 'Enable mirror configuration', class: 'label-light'
+ .form-check
+ = f.check_box :mirror_available, class: 'form-check-input'
+ = f.label :mirror_available, class: 'form-check-label' do
+ Allow mirrors to be setup for projects
+ %span.form-text.text-muted
+ If disabled, only admins will be able to setup mirrors in projects.
+ = link_to icon('question-circle'), help_page_path('workflow/repository_mirroring')
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_repository_storage.html.haml b/app/views/admin/application_settings/_repository_storage.html.haml
index 4eebb59110a..5a303666353 100644
--- a/app/views/admin/application_settings/_repository_storage.html.haml
+++ b/app/views/admin/application_settings/_repository_storage.html.haml
@@ -1,58 +1,51 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.sub-section
.form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :hashed_storage_enabled do
- = f.check_box :hashed_storage_enabled
- Create new projects using hashed storage paths
- .help-block
- Enable immutable, hash-based paths and repository names to store repositories on disk. This prevents
- repositories from having to be moved or renamed when the Project URL changes and may improve disk I/O performance.
- %em (EXPERIMENTAL)
+ .form-check
+ = f.check_box :hashed_storage_enabled, class: 'form-check-input'
+ = f.label :hashed_storage_enabled, class: 'form-check-label' do
+ Create new projects using hashed storage paths
+ .form-text.text-muted
+ Enable immutable, hash-based paths and repository names to store repositories on disk. This prevents
+ repositories from having to be moved or renamed when the Project URL changes and may improve disk I/O performance.
+ %em (EXPERIMENTAL)
.form-group
- = f.label :repository_storages, 'Storage paths for new projects', class: 'control-label col-sm-2'
- .col-sm-10
- = f.select :repository_storages, repository_storages_options_for_select(@application_setting.repository_storages),
- {include_hidden: false}, multiple: true, class: 'form-control'
- .help-block
- Manage repository storage paths. Learn more in the
- = succeed "." do
- = link_to "repository storages documentation", help_page_path("administration/repository_storage_paths")
+ = f.label :repository_storages, 'Storage paths for new projects', class: 'label-light'
+ = f.select :repository_storages, repository_storages_options_for_select(@application_setting.repository_storages),
+ {include_hidden: false}, multiple: true, class: 'form-control'
+ .form-text.text-muted
+ Manage repository storage paths. Learn more in the
+ = succeed "." do
+ = link_to "repository storages documentation", help_page_path("administration/repository_storage_paths")
.sub-section
%h4 Circuit breaker
.form-group
- = f.label :circuitbreaker_check_interval, _('Check interval'), class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :circuitbreaker_check_interval, class: 'form-control'
- .help-block
- = circuitbreaker_check_interval_help_text
+ = f.label :circuitbreaker_check_interval, _('Check interval'), class: 'label-light'
+ = f.number_field :circuitbreaker_check_interval, class: 'form-control'
+ .form-text.text-muted
+ = circuitbreaker_check_interval_help_text
.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
+ = f.label :circuitbreaker_access_retries, _('Number of access attempts'), class: 'label-light'
+ = f.number_field :circuitbreaker_access_retries, class: 'form-control'
+ .form-text.text-muted
+ = 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
+ = f.label :circuitbreaker_storage_timeout, _('Seconds to wait for a storage access attempt'), class: 'label-light'
+ = f.number_field :circuitbreaker_storage_timeout, class: 'form-control'
+ .form-text.text-muted
+ = circuitbreaker_storage_timeout_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
+ = f.label :circuitbreaker_failure_count_threshold, _('Maximum git storage failures'), class: 'label-light'
+ = f.number_field :circuitbreaker_failure_count_threshold, class: 'form-control'
+ .form-text.text-muted
+ = 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
+ = f.label :circuitbreaker_failure_reset_time, _('Seconds before reseting failure information'), class: 'label-light'
+ = f.number_field :circuitbreaker_failure_reset_time, class: 'form-control'
+ .form-text.text-muted
+ = circuitbreaker_failure_reset_time_help_text
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_signin.html.haml b/app/views/admin/application_settings/_signin.html.haml
index 48331c40bca..69d1a43c511 100644
--- a/app/views/admin/application_settings/_signin.html.haml
+++ b/app/views/admin/application_settings/_signin.html.haml
@@ -1,60 +1,52 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = 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-check
+ = f.check_box :password_authentication_enabled_for_web, class: 'form-check-input'
+ = f.label :password_authentication_enabled_for_web, class: 'form-check-label' do
+ Password authentication enabled for web interface
+ .form-text.text-muted
+ 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::Auth::LDAP::Config.enabled?
- or LDAP password
- must be used to authenticate.
+ .form-check
+ = f.check_box :password_authentication_enabled_for_git, class: 'form-check-input'
+ = f.label :password_authentication_enabled_for_git, class: 'form-check-label' do
+ Password authentication enabled for Git over HTTP(S)
+ .form-text.text-muted
+ When disabled, a Personal Access Token
+ - if Gitlab::Auth::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'
+ = f.label :enabled_oauth_sign_in_sources, 'Enabled OAuth sign-in sources', class: 'label-light'
= hidden_field_tag 'application_setting[enabled_oauth_sign_in_sources][]'
- .col-sm-10
- .btn-group{ data: { toggle: 'buttons' } }
- - oauth_providers_checkboxes.each do |source|
- = source
+ .btn-group{ data: { toggle: 'buttons' } }
+ - oauth_providers_checkboxes.each do |source|
+ = source
.form-group
- = f.label :two_factor_authentication, 'Two-factor authentication', class: 'control-label col-sm-2'
- .col-sm-10
- .checkbox
- = f.label :require_two_factor_authentication do
- = f.check_box :require_two_factor_authentication
- Require all users to setup Two-factor authentication
+ = f.label :two_factor_authentication, 'Two-factor authentication', class: 'label-light'
+ .form-check
+ = f.check_box :require_two_factor_authentication, class: 'form-check-input'
+ = f.label :require_two_factor_authentication, class: 'form-check-label' do
+ Require all users to setup Two-factor authentication
.form-group
- = f.label :two_factor_authentication, 'Two-factor grace period (hours)', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :two_factor_grace_period, min: 0, class: 'form-control', placeholder: '0'
- .help-block Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication
+ = f.label :two_factor_authentication, 'Two-factor grace period (hours)', class: 'label-light'
+ = f.number_field :two_factor_grace_period, min: 0, class: 'form-control', placeholder: '0'
+ .form-text.text-muted Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication
.form-group
- = f.label :home_page_url, 'Home page URL', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :home_page_url, class: 'form-control', placeholder: 'http://company.example.com', :'aria-describedby' => 'home_help_block'
- %span.help-block#home_help_block We will redirect non-logged in users to this page
+ = f.label :home_page_url, 'Home page URL', class: 'label-light'
+ = f.text_field :home_page_url, class: 'form-control', placeholder: 'http://company.example.com', :'aria-describedby' => 'home_help_block'
+ %span.form-text.text-muted#home_help_block We will redirect non-logged in users to this page
.form-group
- = f.label :after_sign_out_path, class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :after_sign_out_path, class: 'form-control', placeholder: 'http://company.example.com', :'aria-describedby' => 'after_sign_out_path_help_block'
- %span.help-block#after_sign_out_path_help_block We will redirect users to this page after they sign out
+ = f.label :after_sign_out_path, class: 'label-light'
+ = f.text_field :after_sign_out_path, class: 'form-control', placeholder: 'http://company.example.com', :'aria-describedby' => 'after_sign_out_path_help_block'
+ %span.form-text.text-muted#after_sign_out_path_help_block We will redirect users to this page after they sign out
.form-group
- = f.label :sign_in_text, class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_area :sign_in_text, class: 'form-control', rows: 4
- .help-block Markdown enabled
+ = f.label :sign_in_text, class: 'label-light'
+ = f.text_area :sign_in_text, class: 'form-control', rows: 4
+ .form-text.text-muted Markdown enabled
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_signup.html.haml b/app/views/admin/application_settings/_signup.html.haml
index 85f311dd894..b9ba9128cc9 100644
--- a/app/views/admin/application_settings/_signup.html.haml
+++ b/app/views/admin/application_settings/_signup.html.haml
@@ -1,58 +1,50 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :signup_enabled do
- = f.check_box :signup_enabled
- Sign-up enabled
+ .form-check
+ = f.check_box :signup_enabled, class: 'form-check-input'
+ = f.label :signup_enabled, class: 'form-check-label' do
+ Sign-up enabled
.form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :send_user_confirmation_email do
- = f.check_box :send_user_confirmation_email
- Send confirmation email on sign-up
+ .form-check
+ = f.check_box :send_user_confirmation_email, class: 'form-check-input'
+ = f.label :send_user_confirmation_email, class: 'form-check-label' do
+ Send confirmation email on sign-up
.form-group
- = f.label :domain_whitelist, 'Whitelisted domains for sign-ups', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_area :domain_whitelist_raw, placeholder: 'domain.com', class: 'form-control', rows: 8
- .help-block ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com
+ = f.label :domain_whitelist, 'Whitelisted domains for sign-ups', class: 'label-light'
+ = f.text_area :domain_whitelist_raw, placeholder: 'domain.com', class: 'form-control', rows: 8
+ .form-text.text-muted ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com
.form-group
- = f.label :domain_blacklist_enabled, 'Domain Blacklist', class: 'control-label col-sm-2'
- .col-sm-10
- .checkbox
- = f.label :domain_blacklist_enabled do
- = f.check_box :domain_blacklist_enabled
- Enable domain blacklist for sign ups
+ = f.label :domain_blacklist_enabled, 'Domain Blacklist', class: 'label-light'
+ .form-check
+ = f.check_box :domain_blacklist_enabled, class: 'form-check-input'
+ = f.label :domain_blacklist_enabled, class: 'form-check-label' do
+ Enable domain blacklist for sign ups
.form-group
- .col-sm-offset-2.col-sm-10
- .radio
- = label_tag :blacklist_type_file do
- = radio_button_tag :blacklist_type, :file
- .option-title
- Upload blacklist file
- .radio
- = label_tag :blacklist_type_raw do
- = radio_button_tag :blacklist_type, :raw, @application_setting.domain_blacklist.present? || @application_setting.domain_blacklist.blank?
- .option-title
- Enter blacklist manually
+ .form-check
+ = radio_button_tag :blacklist_type, :file, false, class: 'form-check-input'
+ = label_tag :blacklist_type_file, class: 'form-check-label' do
+ .option-title
+ Upload blacklist file
+ .form-check
+ = radio_button_tag :blacklist_type, :raw, @application_setting.domain_blacklist.present? || @application_setting.domain_blacklist.blank?, class: 'form-check-input'
+ = label_tag :blacklist_type_raw, class: 'form-check-label' do
+ .option-title
+ Enter blacklist manually
.form-group.blacklist-file
- = f.label :domain_blacklist_file, 'Blacklist file', class: 'control-label col-sm-2'
- .col-sm-10
- = f.file_field :domain_blacklist_file, class: 'form-control', accept: '.txt,.conf'
- .help-block Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines or commas for multiple entries.
+ = f.label :domain_blacklist_file, 'Blacklist file', class: 'label-light'
+ = f.file_field :domain_blacklist_file, class: 'form-control', accept: '.txt,.conf'
+ .form-text.text-muted Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines or commas for multiple entries.
.form-group.blacklist-raw
- = f.label :domain_blacklist, 'Blacklisted domains for sign-ups', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_area :domain_blacklist_raw, placeholder: 'domain.com', class: 'form-control', rows: 8
- .help-block 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
+ = f.label :domain_blacklist, 'Blacklisted domains for sign-ups', class: 'label-light'
+ = f.text_area :domain_blacklist_raw, placeholder: 'domain.com', class: 'form-control', rows: 8
+ .form-text.text-muted 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
.form-group
- = f.label :after_sign_up_text, class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_area :after_sign_up_text, class: 'form-control', rows: 4
- .help-block Markdown enabled
+ = f.label :after_sign_up_text, class: 'label-light'
+ = f.text_area :after_sign_up_text, class: 'form-control', rows: 4
+ .form-text.text-muted Markdown enabled
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_spam.html.haml b/app/views/admin/application_settings/_spam.html.haml
index 25e89097dfe..8f0dce962a9 100644
--- a/app/views/admin/application_settings/_spam.html.haml
+++ b/app/views/admin/application_settings/_spam.html.haml
@@ -1,65 +1,57 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :recaptcha_enabled do
- = f.check_box :recaptcha_enabled
- Enable reCAPTCHA
- %span.help-block#recaptcha_help_block Helps prevent bots from creating accounts
+ .form-check
+ = f.check_box :recaptcha_enabled, class: 'form-check-input'
+ = f.label :recaptcha_enabled, class: 'form-check-label' do
+ Enable reCAPTCHA
+ %span.form-text.text-muted#recaptcha_help_block Helps prevent bots from creating accounts
.form-group
- = f.label :recaptcha_site_key, 'reCAPTCHA Site Key', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :recaptcha_site_key, class: 'form-control'
- .help-block
- Generate site and private keys at
- %a{ href: 'http://www.google.com/recaptcha', target: 'blank' } http://www.google.com/recaptcha
+ = f.label :recaptcha_site_key, 'reCAPTCHA Site Key', class: 'label-light'
+ = f.text_field :recaptcha_site_key, class: 'form-control'
+ .form-text.text-muted
+ Generate site and private keys at
+ %a{ href: 'http://www.google.com/recaptcha', target: 'blank' } http://www.google.com/recaptcha
.form-group
- = f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :recaptcha_private_key, class: 'form-control'
+ = f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'label-light'
+ = f.text_field :recaptcha_private_key, class: 'form-control'
.form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :akismet_enabled do
- = f.check_box :akismet_enabled
- Enable Akismet
- %span.help-block#akismet_help_block Helps prevent bots from creating issues
+ .form-check
+ = f.check_box :akismet_enabled, class: 'form-check-input'
+ = f.label :akismet_enabled, class: 'form-check-label' do
+ Enable Akismet
+ %span.form-text.text-muted#akismet_help_block Helps prevent bots from creating issues
.form-group
- = f.label :akismet_api_key, 'Akismet API Key', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :akismet_api_key, class: 'form-control'
- .help-block
- Generate API key at
- %a{ href: 'http://www.akismet.com', target: 'blank' } http://www.akismet.com
+ = f.label :akismet_api_key, 'Akismet API Key', class: 'label-light'
+ = f.text_field :akismet_api_key, class: 'form-control'
+ .form-text.text-muted
+ Generate API key at
+ %a{ href: 'http://www.akismet.com', target: 'blank' } http://www.akismet.com
.form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :unique_ips_limit_enabled do
- = f.check_box :unique_ips_limit_enabled
- Limit sign in from multiple ips
- %span.help-block#unique_ip_help_block
- Helps prevent malicious users hide their activity
+ .form-check
+ = f.check_box :unique_ips_limit_enabled, class: 'form-check-input'
+ = f.label :unique_ips_limit_enabled, class: 'form-check-label' do
+ Limit sign in from multiple ips
+ %span.form-text.text-muted#unique_ip_help_block
+ Helps prevent malicious users hide their activity
.form-group
- = f.label :unique_ips_limit_per_user, 'IPs per user', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :unique_ips_limit_per_user, class: 'form-control'
- .help-block
- Maximum number of unique IPs per user
+ = f.label :unique_ips_limit_per_user, 'IPs per user', class: 'label-light'
+ = f.number_field :unique_ips_limit_per_user, class: 'form-control'
+ .form-text.text-muted
+ Maximum number of unique IPs per user
.form-group
- = f.label :unique_ips_limit_time_window, 'IP expiration time', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :unique_ips_limit_time_window, class: 'form-control'
- .help-block
- How many seconds an IP will be counted towards the limit
+ = f.label :unique_ips_limit_time_window, 'IP expiration time', class: 'label-light'
+ = f.number_field :unique_ips_limit_time_window, class: 'form-control'
+ .form-text.text-muted
+ How many seconds an IP will be counted towards the limit
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_terminal.html.haml b/app/views/admin/application_settings/_terminal.html.haml
index 36d8838803f..543628ff0ee 100644
--- a/app/views/admin/application_settings/_terminal.html.haml
+++ b/app/views/admin/application_settings/_terminal.html.haml
@@ -1,13 +1,12 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
- = f.label :terminal_max_session_time, 'Max session time', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :terminal_max_session_time, class: 'form-control'
- .help-block
- Maximum time for web terminal websocket connection (in seconds).
- 0 for unlimited.
+ = f.label :terminal_max_session_time, 'Max session time', class: 'label-light'
+ = f.number_field :terminal_max_session_time, class: 'form-control'
+ .form-text.text-muted
+ Maximum time for web terminal websocket connection (in seconds).
+ 0 for unlimited.
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_terms.html.haml b/app/views/admin/application_settings/_terms.html.haml
index 724246ab7e7..d3dc8659d1b 100644
--- a/app/views/admin/application_settings/_terms.html.haml
+++ b/app/views/admin/application_settings/_terms.html.haml
@@ -1,22 +1,19 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
- .col-sm-12
- .checkbox
- = f.label :enforce_terms do
- = f.check_box :enforce_terms
- = _("Require all users to accept Terms of Service when they access GitLab.")
- .help-block
- = _("When enabled, users cannot use GitLab until the terms have been accepted.")
+ .form-check
+ = f.check_box :enforce_terms, class: 'form-check-input'
+ = f.label :enforce_terms, class: 'form-check-label' do
+ = _("Require all users to accept Terms of Service and Privacy Policy when they access GitLab.")
+ .form-text.text-muted
+ = _("When enabled, users cannot use GitLab until the terms have been accepted.")
.form-group
- .col-sm-12
- = f.label :terms do
- = _("Terms of Service Agreement")
- .col-sm-12
- = f.text_area :terms, class: 'form-control', rows: 8
- .help-block
- = _("Markdown enabled")
+ = f.label :terms do
+ = _("Terms of Service Agreement and Privacy Policy")
+ = f.text_area :terms, class: 'form-control', rows: 8
+ .form-text.text-muted
+ = _("Markdown enabled")
= f.submit _("Save changes"), class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_third_party_offers.html.haml b/app/views/admin/application_settings/_third_party_offers.html.haml
new file mode 100644
index 00000000000..c5d775d4bf5
--- /dev/null
+++ b/app/views/admin/application_settings/_third_party_offers.html.haml
@@ -0,0 +1,13 @@
+- application_setting = local_assigns.fetch(:application_setting)
+
+= form_for application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
+ = form_errors(application_setting)
+
+ %fieldset
+ .form-group
+ .form-check
+ = f.check_box :hide_third_party_offers, class: 'form-check-input'
+ = f.label :hide_third_party_offers, class: 'form-check-label' do
+ Do not display offers from third parties within GitLab
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_usage.html.haml b/app/views/admin/application_settings/_usage.html.haml
index 7684e2cfdd1..49a3ee33a85 100644
--- a/app/views/admin/application_settings/_usage.html.haml
+++ b/app/views/admin/application_settings/_usage.html.haml
@@ -1,37 +1,35 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :version_check_enabled do
- = f.check_box :version_check_enabled
- Enable version check
- .help-block
- GitLab will inform you if a new version is available.
- = link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "version-check")
- about what information is shared with GitLab Inc.
+ .form-check
+ = f.check_box :version_check_enabled, class: 'form-check-input'
+ = f.label :version_check_enabled, class: 'form-check-label' do
+ Enable version check
+ .form-text.text-muted
+ GitLab will inform you if a new version is available.
+ = link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "version-check")
+ about what information is shared with GitLab Inc.
.form-group
- .col-sm-offset-2.col-sm-10
- - can_be_configured = @application_setting.usage_ping_can_be_configured?
- .checkbox
- = f.label :usage_ping_enabled do
- = f.check_box :usage_ping_enabled, disabled: !can_be_configured
- Enable usage ping
- .help-block
- - if can_be_configured
- To help improve GitLab and its user experience, GitLab will
- periodically collect usage information.
- = link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "usage-ping")
- about what information is shared with GitLab Inc. Visit
- = link_to 'Cohorts', admin_cohorts_path(anchor: 'usage-ping')
- to see the JSON payload sent.
- - else
- The usage ping is disabled, and cannot be configured through this
- form. For more information, see the documentation on
- = succeed '.' do
- = link_to 'deactivating the usage ping', help_page_path('user/admin_area/settings/usage_statistics', anchor: 'deactivate-the-usage-ping')
+ - can_be_configured = @application_setting.usage_ping_can_be_configured?
+ .form-check
+ = f.check_box :usage_ping_enabled, disabled: !can_be_configured, class: 'form-check-input'
+ = f.label :usage_ping_enabled, class: 'form-check-label' do
+ Enable usage ping
+ .form-text.text-muted
+ - if can_be_configured
+ To help improve GitLab and its user experience, GitLab will
+ periodically collect usage information.
+ = link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "usage-ping")
+ about what information is shared with GitLab Inc. Visit
+ = link_to 'Cohorts', admin_cohorts_path(anchor: 'usage-ping')
+ to see the JSON payload sent.
+ - else
+ The usage ping is disabled, and cannot be configured through this
+ form. For more information, see the documentation on
+ = succeed '.' do
+ = link_to 'deactivating the usage ping', help_page_path('user/admin_area/settings/usage_statistics', anchor: 'deactivate-the-usage-ping')
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_visibility_and_access.html.haml b/app/views/admin/application_settings/_visibility_and_access.html.haml
index a75dd90fe6b..4cc3e6a7d03 100644
--- a/app/views/admin/application_settings/_visibility_and_access.html.haml
+++ b/app/views/admin/application_settings/_visibility_and_access.html.haml
@@ -1,67 +1,58 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
- = f.label :default_branch_protection, class: 'control-label col-sm-2'
- .col-sm-10
- = f.select :default_branch_protection, options_for_select(Gitlab::Access.protection_options, @application_setting.default_branch_protection), {}, class: 'form-control'
+ = f.label :default_branch_protection, class: 'label-light'
+ = f.select :default_branch_protection, options_for_select(Gitlab::Access.protection_options, @application_setting.default_branch_protection), {}, class: 'form-control'
.form-group.visibility-level-setting
- = f.label :default_project_visibility, class: 'control-label col-sm-2'
- .col-sm-10
- = render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: Project.new)
+ = f.label :default_project_visibility, class: 'label-light'
+ = render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: Project.new)
.form-group.visibility-level-setting
- = f.label :default_snippet_visibility, class: 'control-label col-sm-2'
- .col-sm-10
- = render('shared/visibility_radios', model_method: :default_snippet_visibility, form: f, selected_level: @application_setting.default_snippet_visibility, form_model: ProjectSnippet.new)
+ = f.label :default_snippet_visibility, class: 'label-light'
+ = render('shared/visibility_radios', model_method: :default_snippet_visibility, form: f, selected_level: @application_setting.default_snippet_visibility, form_model: ProjectSnippet.new)
.form-group.visibility-level-setting
- = f.label :default_group_visibility, class: 'control-label col-sm-2'
- .col-sm-10
- = render('shared/visibility_radios', model_method: :default_group_visibility, form: f, selected_level: @application_setting.default_group_visibility, form_model: Group.new)
+ = f.label :default_group_visibility, class: 'label-light'
+ = render('shared/visibility_radios', model_method: :default_group_visibility, form: f, selected_level: @application_setting.default_group_visibility, form_model: Group.new)
.form-group
- = f.label :restricted_visibility_levels, class: 'control-label col-sm-2'
- .col-sm-10
- - checkbox_name = 'application_setting[restricted_visibility_levels][]'
- = hidden_field_tag(checkbox_name)
- - restricted_level_checkboxes('restricted-visibility-help', checkbox_name).each do |level|
- .checkbox
- = level
- %span.help-block#restricted-visibility-help
- Selected levels cannot be used by non-admin users for projects or snippets.
- If the public level is restricted, user profiles are only visible to logged in users.
+ = f.label :restricted_visibility_levels, class: 'label-light'
+ - checkbox_name = 'application_setting[restricted_visibility_levels][]'
+ = hidden_field_tag(checkbox_name)
+ - restricted_level_checkboxes('restricted-visibility-help', checkbox_name, class: 'form-check-input').each do |level|
+ .form-check
+ = level
+ %span.form-text.text-muted#restricted-visibility-help
+ Selected levels cannot be used by non-admin users for groups, projects or snippets.
+ If the public level is restricted, user profiles are only visible to logged in users.
.form-group
- = f.label :import_sources, class: 'control-label col-sm-2'
- .col-sm-10
- = hidden_field_tag 'application_setting[import_sources][]'
- - import_sources_checkboxes('import-sources-help').each do |source|
- .checkbox= source
- %span.help-block#import-sources-help
- Enabled sources for code import during project creation. OmniAuth must be configured for GitHub
- = link_to "(?)", help_page_path("integration/github")
- , Bitbucket
- = link_to "(?)", help_page_path("integration/bitbucket")
- and GitLab.com
- = link_to "(?)", help_page_path("integration/gitlab")
+ = f.label :import_sources, class: 'label-light'
+ = hidden_field_tag 'application_setting[import_sources][]'
+ - import_sources_checkboxes('import-sources-help', class: 'form-check-input').each do |source|
+ .form-check= source
+ %span.form-text.text-muted#import-sources-help
+ Enabled sources for code import during project creation. OmniAuth must be configured for GitHub
+ = link_to "(?)", help_page_path("integration/github")
+ , Bitbucket
+ = link_to "(?)", help_page_path("integration/bitbucket")
+ and GitLab.com
+ = link_to "(?)", help_page_path("integration/gitlab")
.form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :project_export_enabled do
- = f.check_box :project_export_enabled
- Project export enabled
+ .form-check
+ = f.check_box :project_export_enabled, class: 'form-check-input'
+ = f.label :project_export_enabled, class: 'form-check-label' do
+ Project export enabled
.form-group
- %label.control-label.col-sm-2 Enabled Git access protocols
- .col-sm-10
- = select(:application_setting, :enabled_git_access_protocol, [['Both SSH and HTTP(S)', nil], ['Only SSH', 'ssh'], ['Only HTTP(S)', 'http']], {}, class: 'form-control')
- %span.help-block#clone-protocol-help
- Allow only the selected protocols to be used for Git access.
+ %label.label-light Enabled Git access protocols
+ = select(:application_setting, :enabled_git_access_protocol, [['Both SSH and HTTP(S)', nil], ['Only SSH', 'ssh'], ['Only HTTP(S)', 'http']], {}, class: 'form-control')
+ %span.form-text.text-muted#clone-protocol-help
+ Allow only the selected protocols to be used for Git access.
- ApplicationSetting::SUPPORTED_KEY_TYPES.each do |type|
- field_name = :"#{type}_key_restriction"
.form-group
- = f.label field_name, "#{type.upcase} SSH keys", class: 'control-label col-sm-2'
- .col-sm-10
- = f.select field_name, key_restriction_options_for_select(type), {}, class: 'form-control'
+ = f.label field_name, "#{type.upcase} SSH keys", class: 'label-light'
+ = f.select field_name, key_restriction_options_for_select(type), {}, class: 'form-control'
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/show.html.haml b/app/views/admin/application_settings/show.html.haml
index 3f440c76ee0..5cb8001a364 100644
--- a/app/views/admin/application_settings/show.html.haml
+++ b/app/views/admin/application_settings/show.html.haml
@@ -17,7 +17,7 @@
%section.settings.as-account-limit.no-animate#js-account-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
- = _('Account and limit settings')
+ = _('Account and limit')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
%p
@@ -50,11 +50,11 @@
%section.settings.as-terms.no-animate#js-terms-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
- = _('Terms of Service')
+ = _('Terms of Service and Privacy Policy')
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
%p
- = _('Include a Terms of Service agreement that all users must accept.')
+ = _('Include a Terms of Service agreement and Privacy Policy that all users must accept.')
.settings-content
= render 'terms'
@@ -169,7 +169,7 @@
.settings-content
= render 'logging'
-%section.settings.as-repository-storage.no-animate#js-repository-storage-settings{ class: ('expanded' if expanded) }
+%section.qa-repository-storage-settings.settings.as-repository-storage.no-animate#js-repository-storage-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('Repository storage')
@@ -317,10 +317,24 @@
%section.settings.as-mirror.no-animate#js-mirror-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
- = _('Repository mirror settings')
+ = _('Repository mirror')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Configure push mirrors.')
.settings-content
= render partial: 'repository_mirrors_form'
+
+%section.settings.as-third-party-offers.no-animate#js-third-party-offers-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Third party offers')
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
+ %p
+ = _('Control the display of third party offers.')
+ .settings-content
+ = render 'third_party_offers', application_setting: @application_setting
+
+= render_if_exists 'admin/application_settings/pseudonymizer_settings', expanded: expanded
+
diff --git a/app/views/admin/applications/_form.html.haml b/app/views/admin/applications/_form.html.haml
index 93827d6a1ab..7f14cddebd8 100644
--- a/app/views/admin/applications/_form.html.haml
+++ b/app/views/admin/applications/_form.html.haml
@@ -1,34 +1,34 @@
-= form_for [:admin, @application], url: @url, html: {class: 'form-horizontal', role: 'form'} do |f|
+= form_for [:admin, @application], url: @url, html: {role: 'form'} do |f|
= form_errors(application)
- = content_tag :div, class: 'form-group' do
- = f.label :name, class: 'col-sm-2 control-label'
+ = content_tag :div, class: 'form-group row' do
+ = f.label :name, class: 'col-sm-2 col-form-label'
.col-sm-10
= f.text_field :name, class: 'form-control'
= doorkeeper_errors_for application, :name
- = content_tag :div, class: 'form-group' do
- = f.label :redirect_uri, class: 'col-sm-2 control-label'
+ = content_tag :div, class: 'form-group row' do
+ = f.label :redirect_uri, class: 'col-sm-2 col-form-label'
.col-sm-10
= f.text_area :redirect_uri, class: 'form-control'
= doorkeeper_errors_for application, :redirect_uri
- %span.help-block
+ %span.form-text.text-muted
Use one line per URI
- if Doorkeeper.configuration.native_redirect_uri
- %span.help-block
+ %span.form-text.text-muted
Use
%code= Doorkeeper.configuration.native_redirect_uri
for local tests
- = content_tag :div, class: 'form-group' do
- = f.label :trusted, class: 'col-sm-2 control-label'
+ = content_tag :div, class: 'form-group row' do
+ = f.label :trusted, class: 'col-sm-2 col-form-label'
.col-sm-10
= f.check_box :trusted
- %span.help-block
+ %span.form-text.text-muted
Trusted applications are automatically authorized on GitLab OAuth flow.
- .form-group
- = f.label :scopes, class: 'col-sm-2 control-label'
+ .form-group.row
+ = f.label :scopes, class: 'col-sm-2 col-form-label'
.col-sm-10
= render 'shared/tokens/scopes_form', prefix: 'doorkeeper_application', token: application, scopes: @scopes
diff --git a/app/views/admin/applications/show.html.haml b/app/views/admin/applications/show.html.haml
index 5125aa21b06..593a6d816e3 100644
--- a/app/views/admin/applications/show.html.haml
+++ b/app/views/admin/applications/show.html.haml
@@ -32,5 +32,5 @@
= render "shared/tokens/scopes_list", token: @application
.form-actions
- = link_to 'Edit', edit_admin_application_path(@application), class: 'btn btn-primary wide pull-left'
+ = link_to 'Edit', edit_admin_application_path(@application), class: 'btn btn-primary wide float-left'
= render 'delete_form', application: @application, submit_btn_css: 'btn btn-danger prepend-left-10'
diff --git a/app/views/admin/background_jobs/show.html.haml b/app/views/admin/background_jobs/show.html.haml
index f0cc4d7ee62..faa5854bb40 100644
--- a/app/views/admin/background_jobs/show.html.haml
+++ b/app/views/admin/background_jobs/show.html.haml
@@ -7,9 +7,9 @@
%hr
- .panel.panel-default
- .panel-heading Sidekiq running processes
- .panel-body
+ .card
+ .card-header Sidekiq running processes
+ .card-body
- if @sidekiq_processes.empty?
%h4.cred
%i.fa.fa-exclamation-triangle
@@ -41,5 +41,5 @@
- .panel.panel-default
+ .card
%iframe{ src: sidekiq_path, width: '100%', height: 970, style: "border: 0" }
diff --git a/app/views/admin/broadcast_messages/_form.html.haml b/app/views/admin/broadcast_messages/_form.html.haml
index 813ad451b44..7f34357f147 100644
--- a/app/views/admin/broadcast_messages/_form.html.haml
+++ b/app/views/admin/broadcast_messages/_form.html.haml
@@ -6,32 +6,32 @@
- else
Your message here
-= form_for [:admin, @broadcast_message], html: { class: 'broadcast-message-form form-horizontal js-quick-submit js-requires-input'} do |f|
+= form_for [:admin, @broadcast_message], html: { class: 'broadcast-message-form js-quick-submit js-requires-input'} do |f|
= form_errors(@broadcast_message)
- .form-group
- = f.label :message, class: 'control-label'
+ .form-group.row
+ = f.label :message, class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_area :message, class: "form-control js-autosize",
required: true,
data: { preview_path: preview_admin_broadcast_messages_path }
- .form-group.js-toggle-colors-container
- .col-sm-10.col-sm-offset-2
+ .form-group.row.js-toggle-colors-container
+ .col-sm-10.offset-sm-2
= link_to 'Customize colors', '#', class: 'js-toggle-colors-link'
- .form-group.js-toggle-colors-container.hide
- = f.label :color, "Background Color", class: 'control-label'
+ .form-group.row.js-toggle-colors-container.toggle-colors.hide
+ = f.label :color, "Background Color", class: 'col-form-label col-sm-2'
.col-sm-10
= f.color_field :color, class: "form-control"
- .form-group.js-toggle-colors-container.hide
- = f.label :font, "Font Color", class: 'control-label'
+ .form-group.row.js-toggle-colors-container.toggle-colors.hide
+ = f.label :font, "Font Color", class: 'col-form-label col-sm-2'
.col-sm-10
= f.color_field :font, class: "form-control"
- .form-group
- = f.label :starts_at, _("Starts at (UTC)"), class: 'control-label'
+ .form-group.row
+ = f.label :starts_at, _("Starts at (UTC)"), class: 'col-form-label col-sm-2'
.col-sm-10.datetime-controls
= f.datetime_select :starts_at, {}, class: 'form-control form-control-inline'
- .form-group
- = f.label :ends_at, _("Ends at (UTC)"), class: 'control-label'
+ .form-group.row
+ = f.label :ends_at, _("Ends at (UTC)"), class: 'col-form-label col-sm-2'
.col-sm-10.datetime-controls
= f.datetime_select :ends_at, {}, class: 'form-control form-control-inline'
.form-actions
diff --git a/app/views/admin/broadcast_messages/index.html.haml b/app/views/admin/broadcast_messages/index.html.haml
index b806882eee3..9ef58faf8cc 100644
--- a/app/views/admin/broadcast_messages/index.html.haml
+++ b/app/views/admin/broadcast_messages/index.html.haml
@@ -32,7 +32,7 @@
%td
= message.ends_at
%td
- = link_to icon('pencil-square-o'), edit_admin_broadcast_message_path(message), title: 'Edit', class: 'btn btn-xs'
- = link_to icon('times'), admin_broadcast_message_path(message), method: :delete, remote: true, title: 'Remove', class: 'js-remove-tr btn btn-xs btn-danger'
+ = link_to icon('pencil-square-o'), edit_admin_broadcast_message_path(message), title: 'Edit', class: 'btn btn-sm'
+ = link_to icon('times'), admin_broadcast_message_path(message), method: :delete, remote: true, title: 'Remove', class: 'js-remove-tr btn btn-sm btn-danger'
= paginate @broadcast_messages, theme: 'gitlab'
diff --git a/app/views/admin/conversational_development_index/_card.html.haml b/app/views/admin/conversational_development_index/_card.html.haml
index 6c8688e06ae..57eda06630b 100644
--- a/app/views/admin/conversational_development_index/_card.html.haml
+++ b/app/views/admin/conversational_development_index/_card.html.haml
@@ -3,20 +3,20 @@
.convdev-card-title
%h3
= card.title
- .text-light
+ .light-text
= card.description
- .card-scores
- .card-score
- .card-score-value
+ .board-card-scores
+ .board-card-score
+ .board-card-score-value
= format_score(card.instance_score)
- .card-score-name You
- .card-score
- .card-score-value
+ .board-card-score-name You
+ .board-card-score
+ .board-card-score-value
= format_score(card.leader_score)
- .card-score-name Lead
- .card-score-big
+ .board-card-score-name Lead
+ .board-card-score-big
= number_to_percentage(card.percentage_score, precision: 1)
- .card-buttons
+ .board-card-buttons
- if card.blog
%a{ href: card.blog }
= icon('info-circle', 'aria-hidden' => 'true')
diff --git a/app/views/admin/conversational_development_index/_disabled.html.haml b/app/views/admin/conversational_development_index/_disabled.html.haml
index 975d7df3da6..0a741b50960 100644
--- a/app/views/admin/conversational_development_index/_disabled.html.haml
+++ b/app/views/admin/conversational_development_index/_disabled.html.haml
@@ -1,5 +1,5 @@
.container.convdev-empty
- .col-sm-6.col-sm-push-3.text-center
+ .col-sm-12.justify-content-center.text-center
= custom_icon('convdev_no_index')
%h4 Usage ping is not enabled
%p
diff --git a/app/views/admin/conversational_development_index/_no_data.html.haml b/app/views/admin/conversational_development_index/_no_data.html.haml
index b23d2b5ec3a..d69c46194b4 100644
--- a/app/views/admin/conversational_development_index/_no_data.html.haml
+++ b/app/views/admin/conversational_development_index/_no_data.html.haml
@@ -1,5 +1,5 @@
.container.convdev-empty
- .col-sm-6.col-sm-push-3.text-center
+ .col-sm-12.justify-content-center.text-center
= custom_icon('convdev_no_data')
%h4 Data is still calculating...
%p
diff --git a/app/views/admin/conversational_development_index/show.html.haml b/app/views/admin/conversational_development_index/show.html.haml
index ed40e7b4d00..e3d1aa31dc2 100644
--- a/app/views/admin/conversational_development_index/show.html.haml
+++ b/app/views/admin/conversational_development_index/show.html.haml
@@ -21,11 +21,11 @@
score
= link_to icon('question-circle', 'aria-hidden' => 'true'), help_page_path('user/admin_area/monitoring/convdev')
- .convdev-cards.card-container
+ .convdev-cards.board-card-container
- @metric.cards.each do |card|
= render 'card', card: card
- .convdev-steps.visible-lg
+ .convdev-steps.d-none.d-lg-block.d-xl-block
- @metric.idea_to_production_steps.each_with_index do |step, index|
.convdev-step{ class: "convdev-#{score_level(step.percentage_score)}-score" }
= custom_icon("i2p_step_#{index + 1}")
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index cfa1d9d0f0c..18f2c1a509f 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -2,7 +2,7 @@
- breadcrumb_title "Dashboard"
%div{ class: container_class }
- = render_if_exists "admin/licenses/breakdown", license: @license
+ = render_if_exists 'admin/licenses/breakdown', license: @license
.admin-dashboard.prepend-top-default
.row
@@ -12,7 +12,7 @@
= link_to admin_projects_path do
%h3.text-center
Projects:
- = approximate_count_with_delimiters(Project)
+ = approximate_count_with_delimiters(@counts, Project)
%hr
= link_to('New project', new_project_path, class: "btn btn-new")
.col-sm-4
@@ -21,8 +21,8 @@
= link_to admin_users_path do
%h3.text-center
Users:
- = approximate_count_with_delimiters(User)
- = render_if_exists 'users_statistics'
+ = approximate_count_with_delimiters(@counts, User)
+ = render_if_exists 'admin/dashboard/users_statistics'
%hr
= link_to 'New user', new_admin_user_path, class: "btn btn-new"
.col-sm-4
@@ -31,7 +31,7 @@
= link_to admin_groups_path do
%h3.text-center
Groups:
- = approximate_count_with_delimiters(Group)
+ = approximate_count_with_delimiters(@counts, Group)
%hr
= link_to 'New group', new_admin_group_path, class: "btn btn-new"
.row
@@ -41,35 +41,35 @@
%h4 Statistics
%p
Forks
- %span.light.pull-right
- = approximate_count_with_delimiters(ForkedProjectLink)
+ %span.light.float-right
+ = approximate_count_with_delimiters(@counts, ForkedProjectLink)
%p
Issues
- %span.light.pull-right
- = approximate_count_with_delimiters(Issue)
+ %span.light.float-right
+ = approximate_count_with_delimiters(@counts, Issue)
%p
Merge Requests
- %span.light.pull-right
- = approximate_count_with_delimiters(MergeRequest)
+ %span.light.float-right
+ = approximate_count_with_delimiters(@counts, MergeRequest)
%p
Notes
- %span.light.pull-right
- = approximate_count_with_delimiters(Note)
+ %span.light.float-right
+ = approximate_count_with_delimiters(@counts, Note)
%p
Snippets
- %span.light.pull-right
- = approximate_count_with_delimiters(Snippet)
+ %span.light.float-right
+ = approximate_count_with_delimiters(@counts, Snippet)
%p
SSH Keys
- %span.light.pull-right
- = approximate_count_with_delimiters(Key)
+ %span.light.float-right
+ = approximate_count_with_delimiters(@counts, Key)
%p
Milestones
- %span.light.pull-right
- = approximate_count_with_delimiters(Milestone)
+ %span.light.float-right
+ = approximate_count_with_delimiters(@counts, Milestone)
%p
Active Users
- %span.light.pull-right
+ %span.light.float-right
= number_with_delimiter(User.active.count)
.col-md-4
.info-well
@@ -78,47 +78,47 @@
- sign_up = "Sign up"
%p{ "aria-label" => "#{sign_up}: status " + (allow_signup? ? "on" : "off") }
= sign_up
- %span.light.pull-right
+ %span.light.float-right
= boolean_to_icon allow_signup?
- ldap = "LDAP"
%p{ "aria-label" => "#{ldap}: status " + (Gitlab.config.ldap.enabled ? "on" : "off") }
= ldap
- %span.light.pull-right
+ %span.light.float-right
= boolean_to_icon Gitlab.config.ldap.enabled
- gravatar = "Gravatar"
%p{ "aria-label" => "#{gravatar}: status " + (gravatar_enabled? ? "on" : "off") }
= gravatar
- %span.light.pull-right
+ %span.light.float-right
= boolean_to_icon gravatar_enabled?
- omniauth = "OmniAuth"
%p{ "aria-label" => "#{omniauth}: status " + (Gitlab.config.omniauth.enabled ? "on" : "off") }
= omniauth
- %span.light.pull-right
+ %span.light.float-right
= boolean_to_icon Gitlab.config.omniauth.enabled
- reply_email = "Reply by email"
%p{ "aria-label" => "#{reply_email}: status " + (Gitlab::IncomingEmail.enabled? ? "on" : "off") }
= reply_email
- %span.light.pull-right
+ %span.light.float-right
= boolean_to_icon Gitlab::IncomingEmail.enabled?
- = render_if_exists 'elastic_and_geo'
+ = render_if_exists 'admin/dashboard/elastic_and_geo'
- container_reg = "Container Registry"
%p{ "aria-label" => "#{container_reg}: status " + (Gitlab.config.registry.enabled ? "on" : "off") }
= container_reg
- %span.light.pull-right
+ %span.light.float-right
= boolean_to_icon Gitlab.config.registry.enabled
- gitlab_pages = 'GitLab Pages'
- gitlab_pages_enabled = Gitlab.config.pages.enabled
%p{ "aria-label" => "#{gitlab_pages}: status " + (gitlab_pages_enabled ? "on" : "off") }
= gitlab_pages
- %span.light.pull-right
+ %span.light.float-right
= boolean_to_icon gitlab_pages_enabled
- gitlab_shared_runners = 'Shared Runners'
- gitlab_shared_runners_enabled = Gitlab.config.gitlab_ci.shared_runners_enabled
%p{ "aria-label" => "#{gitlab_shared_runners}: status " + (gitlab_shared_runners_enabled ? "on" : "off") }
= gitlab_shared_runners
- %span.light.pull-right
+ %span.light.float-right
= boolean_to_icon gitlab_shared_runners_enabled
.col-md-4
.info-well
@@ -126,44 +126,44 @@
%h4
Components
- if Gitlab::CurrentSettings.version_check_enabled
- .pull-right
+ .float-right
= version_status_badge
%p
GitLab
- %span.pull-right
+ %span.float-right
= Gitlab::VERSION
- = "(#{Gitlab::REVISION})"
+ = "(#{Gitlab.revision})"
%p
GitLab Shell
- %span.pull-right
+ %span.float-right
= Gitlab::Shell.new.version
%p
GitLab Workhorse
- %span.pull-right
+ %span.float-right
= gitlab_workhorse_version
%p
GitLab API
- %span.pull-right
+ %span.float-right
= API::API::version
- if Gitlab.config.pages.enabled
%p
GitLab Pages
- %span.pull-right
+ %span.float-right
= Gitlab::Pages::VERSION
- = render_if_exists 'geo'
+ = render_if_exists 'admin/dashboard/geo'
%p
Ruby
- %span.pull-right
+ %span.float-right
#{RUBY_VERSION}p#{RUBY_PATCHLEVEL}
%p
Rails
- %span.pull-right
+ %span.float-right
#{Rails::VERSION::STRING}
%p
= Gitlab::Database.adapter_name
- %span.pull-right
+ %span.float-right
= Gitlab::Database.version
%p
= link_to "Gitaly Servers", admin_gitaly_servers_path
@@ -175,7 +175,7 @@
- @projects.each do |project|
%p
= link_to project.full_name, [:admin, project.namespace.becomes(Namespace), project], class: 'str-truncated-60'
- %span.light.pull-right
+ %span.light.float-right
#{time_ago_with_tooltip(project.created_at)}
.col-md-4
.info-well
@@ -185,7 +185,7 @@
%p
= link_to [:admin, user], class: 'str-truncated-60' do
= user.name
- %span.light.pull-right
+ %span.light.float-right
#{time_ago_with_tooltip(user.created_at)}
.col-md-4
.info-well
@@ -195,5 +195,5 @@
%p
= link_to [:admin, group], class: 'str-truncated-60' do
= group.full_name
- %span.light.pull-right
+ %span.light.float-right
#{time_ago_with_tooltip(group.created_at)}
diff --git a/app/views/admin/deploy_keys/edit.html.haml b/app/views/admin/deploy_keys/edit.html.haml
index 3a59282e578..b50adef362f 100644
--- a/app/views/admin/deploy_keys/edit.html.haml
+++ b/app/views/admin/deploy_keys/edit.html.haml
@@ -3,7 +3,7 @@
%hr
%div
- = form_for [:admin, @deploy_key], html: { class: 'deploy-key-form form-horizontal' } do |f|
+ = form_for [:admin, @deploy_key], html: { class: 'deploy-key-form' } do |f|
= render partial: 'shared/deploy_keys/form', locals: { form: f, deploy_key: @deploy_key }
.form-actions
= f.submit 'Save changes', class: 'btn-save btn'
diff --git a/app/views/admin/deploy_keys/index.html.haml b/app/views/admin/deploy_keys/index.html.haml
index 1420163fd5a..52ab8bae119 100644
--- a/app/views/admin/deploy_keys/index.html.haml
+++ b/app/views/admin/deploy_keys/index.html.haml
@@ -2,7 +2,7 @@
%h3.page-title.deploy-keys-title
Public deploy keys (#{@deploy_keys.count})
- .pull-right
+ .float-right
= link_to 'New deploy key', new_admin_deploy_key_path, class: 'btn btn-new btn-sm btn-inverted'
- if @deploy_keys.any?
@@ -29,6 +29,6 @@
%span.cgray
added #{time_ago_with_tooltip(deploy_key.created_at)}
%td
- .pull-right
+ .float-right
= link_to 'Edit', edit_admin_deploy_key_path(deploy_key), class: 'btn btn-sm'
= link_to 'Remove', admin_deploy_key_path(deploy_key), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-sm btn-remove delete-key'
diff --git a/app/views/admin/deploy_keys/new.html.haml b/app/views/admin/deploy_keys/new.html.haml
index 13f5259698f..d4f8e340b69 100644
--- a/app/views/admin/deploy_keys/new.html.haml
+++ b/app/views/admin/deploy_keys/new.html.haml
@@ -3,7 +3,7 @@
%hr
%div
- = form_for [:admin, @deploy_key], html: { class: 'deploy-key-form form-horizontal' } do |f|
+ = form_for [:admin, @deploy_key], html: { class: 'deploy-key-form' } do |f|
= render partial: 'shared/deploy_keys/form', locals: { form: f, deploy_key: @deploy_key }
.form-actions
= f.submit 'Create', class: 'btn-create btn'
diff --git a/app/views/admin/gitaly_servers/index.html.haml b/app/views/admin/gitaly_servers/index.html.haml
index 231f94dc95d..9b24f411a75 100644
--- a/app/views/admin/gitaly_servers/index.html.haml
+++ b/app/views/admin/gitaly_servers/index.html.haml
@@ -6,10 +6,10 @@
- if @gitaly_servers.any?
.table-holder
%table.table.responsive-table
- %thead.hidden-sm.hidden-xs
+ %thead
%tr
%th= _("Storage")
- %th= n_("Gitaly|Address")
+ %th= s_("Gitaly|Address")
%th= _("Server version")
%th= _("Git version")
%th= _("Up to date")
diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml
index d9f05003904..c8008771236 100644
--- a/app/views/admin/groups/_form.html.haml
+++ b/app/views/admin/groups/_form.html.haml
@@ -1,23 +1,28 @@
-= form_for [:admin, @group], html: { class: "form-horizontal" } do |f|
+= form_for [:admin, @group] do |f|
= form_errors(@group)
= render 'shared/group_form', f: f
- .form-group.group-description-holder
- = f.label :avatar, "Group avatar", class: 'control-label'
+ = render_if_exists 'shared/repository_size_limit_setting', form: f, type: :group
+ = render_if_exists 'admin/namespace_plan', f: f
+
+ .form-group.row.group-description-holder
+ = f.label :avatar, "Group avatar", class: 'col-form-label col-sm-2'
.col-sm-10
= render 'shared/choose_group_avatar_button', f: f
= render 'shared/visibility_level', f: f, visibility_level: visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group
- .form-group
- .col-sm-offset-2.col-sm-10
+ .form-group.row
+ .offset-sm-2.col-sm-10
= render 'shared/allow_request_access', form: f
= render 'groups/group_admin_settings', f: f
+ = render_if_exists 'namespaces/shared_runners_minutes_settings', group: @group, form: f
+
- if @group.new_record?
- .form-group
- .col-sm-offset-2.col-sm-10
+ .form-group.row
+ .offset-sm-2.col-sm-10
.alert.alert-info
= render 'shared/group_tips'
.form-actions
@@ -28,3 +33,5 @@
.form-actions
= f.submit 'Save changes', class: "btn btn-save"
= link_to 'Cancel', admin_group_path(@group), class: "btn btn-cancel"
+
+= render_if_exists 'ldap_group_links/ldap_syncrhonizations', group: @group
diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml
index 47cc2d4d27e..3f96988c203 100644
--- a/app/views/admin/groups/_group.html.haml
+++ b/app/views/admin/groups/_group.html.haml
@@ -1,3 +1,4 @@
+- group = local_assigns.fetch(:group)
- css_class = 'no-description' if group.description.blank?
%li.group-row{ class: css_class }
@@ -5,9 +6,11 @@
= link_to 'Edit', admin_group_edit_path(group), id: "edit_#{dom_id(group)}", class: 'btn'
= link_to 'Delete', [:admin, group], data: { confirm: "Are you sure you want to remove #{group.name}?" }, method: :delete, class: 'btn btn-remove'
.stats
- %span.badge
+ %span.badge.badge-pill
= storage_counter(group.storage_size)
+ = render_if_exists 'admin/namespace_plan_badge', namespace: group
+
%span
= icon('bookmark')
= number_with_delimiter(group.projects.count)
@@ -20,7 +23,7 @@
= visibility_level_icon(group.visibility_level, fw: false)
.avatar-container.s40
- = group_icon(group, class: "avatar s40 hidden-xs")
+ = group_icon(group, class: "avatar s40 d-none d-sm-block")
.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 324f3c0a22f..a40f98ad24f 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -4,16 +4,16 @@
%h3.page-title
Group: #{@group.full_name}
- = link_to admin_group_edit_path(@group), class: "btn pull-right" do
+ = link_to admin_group_edit_path(@group), class: "btn float-right" do
%i.fa.fa-pencil-square-o
Edit
%hr
.row
.col-md-6
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
Group info:
- %ul.well-list
+ %ul.content-list
%li
.avatar-container.s60
= group_icon(@group, class: "avatar s60")
@@ -40,6 +40,8 @@
%strong
= @group.created_at.to_s(:medium)
+ = render_if_exists 'admin/namespace_plan_info', namespace: @group
+
%li
%span.light Storage:
%strong= storage_counter(@group.storage_size)
@@ -58,67 +60,71 @@
= group_lfs_status(@group)
= link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
- .panel.panel-default
- .panel-heading
- %h3.panel-title
+ = render_if_exists 'namespaces/shared_runner_status', namespace: @group
+
+ = render_if_exists 'ldap_group_links/ldap_group_links_show', group: @group
+
+ .card
+ .card-header
+ %h3.card-title
Projects
- %span.badge
+ %span.badge.badge-pill
#{@group.projects.count}
- %ul.well-list
+ %ul.content-list
- @projects.each do |project|
%li
%strong
= link_to project.full_name, [:admin, project.namespace.becomes(Namespace), project]
- %span.badge
+ %span.badge.badge-pill
= storage_counter(project.statistics.storage_size)
- %span.pull-right.light
+ %span.float-right.light
%span.monospace= project.full_path + '.git'
- .panel-footer
+ .card-footer
= paginate @projects, param_name: 'projects_page', theme: 'gitlab'
- if @group.shared_projects.any?
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
Projects shared with #{@group.name}
- %span.badge
+ %span.badge.badge-pill
#{@group.shared_projects.count}
- %ul.well-list
+ %ul.content-list
- @group.shared_projects.sort_by(&:name).each do |project|
%li
%strong
= link_to project.full_name, [:admin, project.namespace.becomes(Namespace), project]
- %span.badge
+ %span.badge.badge-pill
= storage_counter(project.statistics.storage_size)
- %span.pull-right.light
+ %span.float-right.light
%span.monospace= project.full_path + '.git'
.col-md-6
- if can?(current_user, :admin_group_member, @group)
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
Add user(s) to the group:
- .panel-body.form-holder
+ .card-body.form-holder
%p.light
Read more about project permissions
%strong= link_to "here", help_page_path("user/permissions"), class: "vlink"
= form_tag admin_group_members_update_path(@group), id: "new_project_member", class: "bulk_import", method: :put do
%div
- = users_select_tag(:user_ids, multiple: true, email_user: true, scope: :all)
+ = users_select_tag(:user_ids, multiple: true, email_user: true, skip_ldap: @group.ldap_synced?, scope: :all)
.prepend-top-10
= select_tag :access_level, options_for_select(GroupMember.access_level_roles), class: "project-access-select select2"
%hr
= button_tag 'Add users to group', class: "btn btn-create"
= render 'shared/members/requests', membership_source: @group, requesters: @requesters, force_mobile_view: true
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
%strong= @group.name
group members
- %span.badge= @group.members.size
- .pull-right
- = link_to icon('pencil-square-o', text: 'Manage access'), polymorphic_url([@group, :members]), class: "btn btn-xs"
- %ul.well-list.group-users-list.content-list.members-list
+ %span.badge.badge-pill= @group.members.size
+ .float-right
+ = link_to icon('pencil-square-o', text: 'Manage access'), polymorphic_url([@group, :members]), class: "btn btn-sm"
+ %ul.content-list.group-users-list.content-list.members-list
= render partial: 'shared/members/member', collection: @members, as: :member, locals: { show_controls: false }
- .panel-footer
+ .card-footer
= paginate @members, param_name: 'members_page', theme: 'gitlab'
diff --git a/app/views/admin/health_check/show.html.haml b/app/views/admin/health_check/show.html.haml
index e31fb58b205..d51ac854b04 100644
--- a/app/views/admin/health_check/show.html.haml
+++ b/app/views/admin/health_check/show.html.haml
@@ -5,7 +5,7 @@
%div{ class: container_class }
%h3.page-title= page_title
.bs-callout.clearfix
- .pull-left
+ .float-left
%p
#{ s_('HealthCheck|Access token is') }
%code#health-check-token= Gitlab::CurrentSettings.health_check_access_token
@@ -25,8 +25,8 @@
%code= metrics_url(token: Gitlab::CurrentSettings.health_check_access_token)
%hr
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
Current Status:
- if no_errors
= icon('circle', class: 'cgreen')
@@ -34,7 +34,7 @@
- else
= icon('warning', class: 'cred')
#{ s_('HealthCheck|Unhealthy') }
- .panel-body
+ .card-body
- if no_errors
#{ s_('HealthCheck|No Health Problems Detected') }
- else
diff --git a/app/views/admin/hook_logs/_index.html.haml b/app/views/admin/hook_logs/_index.html.haml
index 91a8c0c62fe..1d7c9930b6a 100644
--- a/app/views/admin/hook_logs/_index.html.haml
+++ b/app/views/admin/hook_logs/_index.html.haml
@@ -18,8 +18,8 @@
%tr
%td
= render partial: 'shared/hook_logs/status_label', locals: { hook_log: hook_log }
- %td.hidden-xs
- %span.label.label-gray.deploy-project-label
+ %td.d-none.d-sm-block
+ %span.badge.badge-gray.deploy-project-label
= hook_log.trigger.singularize.titleize
%td
= truncate(hook_log.url, length: 50)
diff --git a/app/views/admin/hook_logs/show.html.haml b/app/views/admin/hook_logs/show.html.haml
index 56127bacda2..2eb3ac85722 100644
--- a/app/views/admin/hook_logs/show.html.haml
+++ b/app/views/admin/hook_logs/show.html.haml
@@ -4,7 +4,7 @@
%hr
-= link_to 'Resend Request', retry_admin_hook_hook_log_path(@hook, @hook_log), class: "btn btn-default pull-right prepend-left-10"
+= link_to 'Resend Request', retry_admin_hook_hook_log_path(@hook, @hook_log), class: "btn btn-default float-right prepend-left-10"
= render partial: 'shared/hook_logs/content', locals: { hook_log: @hook_log }
diff --git a/app/views/admin/hooks/_form.html.haml b/app/views/admin/hooks/_form.html.haml
index a6324a97fd5..3abde755f0f 100644
--- a/app/views/admin/hooks/_form.html.haml
+++ b/app/views/admin/hooks/_form.html.haml
@@ -6,39 +6,39 @@
.form-group
= form.label :token, 'Secret Token', class: 'label-light'
= form.text_field :token, class: 'form-control'
- %p.help-block
+ %p.form-text.text-muted
Use this token to validate received payloads
.form-group
= form.label :url, 'Trigger', class: 'label-light'
%ul.list-unstyled
%li
- .help-block
+ .form-text.text-muted
System hook will be triggered on set of events like creating project
or adding ssh key. But you can also enable extra triggers like Push events.
.prepend-top-default
- = form.check_box :repository_update_events, class: 'pull-left'
+ = form.check_box :repository_update_events, class: 'float-left'
.prepend-left-20
= form.label :repository_update_events, class: 'list-label' do
%strong Repository update events
%p.light
This URL will be triggered when repository is updated
%li
- = form.check_box :push_events, class: 'pull-left'
+ = form.check_box :push_events, class: 'float-left'
.prepend-left-20
= form.label :push_events, class: 'list-label' do
%strong Push events
%p.light
This URL will be triggered for each branch updated to the repository
%li
- = form.check_box :tag_push_events, class: 'pull-left'
+ = form.check_box :tag_push_events, class: 'float-left'
.prepend-left-20
= form.label :tag_push_events, class: 'list-label' do
%strong Tag push events
%p.light
This URL will be triggered when a new tag is pushed to the repository
%li
- = form.check_box :merge_requests_events, class: 'pull-left'
+ = form.check_box :merge_requests_events, class: 'float-left'
.prepend-left-20
= form.label :merge_requests_events, class: 'list-label' do
%strong Merge request events
@@ -46,7 +46,7 @@
This URL will be triggered when a merge request is created/updated/merged
.form-group
= form.label :enable_ssl_verification, 'SSL verification', class: 'label-light checkbox'
- .checkbox
- = form.label :enable_ssl_verification do
- = form.check_box :enable_ssl_verification
+ .form-check
+ = form.check_box :enable_ssl_verification, class: 'form-check-input'
+ = form.label :enable_ssl_verification, class: 'form-check-label' do
%strong Enable SSL verification
diff --git a/app/views/admin/hooks/edit.html.haml b/app/views/admin/hooks/edit.html.haml
index 629b1a9940f..b9a650e1f1f 100644
--- a/app/views/admin/hooks/edit.html.haml
+++ b/app/views/admin/hooks/edit.html.haml
@@ -9,12 +9,12 @@
%hr
-= form_for @hook, as: :hook, url: admin_hook_path, html: { class: 'form-horizontal' } do |f|
+= form_for @hook, as: :hook, url: admin_hook_path do |f|
= render partial: 'form', locals: { form: f, hook: @hook }
.form-actions
= f.submit 'Save changes', class: 'btn btn-create'
= render 'shared/web_hooks/test_button', triggers: SystemHook.triggers, hook: @hook
- = link_to 'Remove', admin_hook_path(@hook), method: :delete, class: 'btn btn-remove pull-right', data: { confirm: 'Are you sure?' }
+ = link_to 'Remove', admin_hook_path(@hook), method: :delete, class: 'btn btn-remove float-right', data: { confirm: 'Are you sure?' }
%hr
diff --git a/app/views/admin/hooks/index.html.haml b/app/views/admin/hooks/index.html.haml
index d9e2ce5e74c..87f9b0e86a7 100644
--- a/app/views/admin/hooks/index.html.haml
+++ b/app/views/admin/hooks/index.html.haml
@@ -15,8 +15,8 @@
%hr
- if @hooks.any?
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
System hooks (#{@hooks.count})
%ul.content-list
- @hooks.each do |hook|
@@ -29,7 +29,7 @@
%div
- SystemHook.triggers.each_value do |event|
- if hook.public_send(event)
- %span.label.label-gray= event.to_s.titleize
- %span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? 'enabled' : 'disabled'}
+ %span.badge.badge-gray= event.to_s.titleize
+ %span.badge.badge-gray SSL Verification: #{hook.enable_ssl_verification ? 'enabled' : 'disabled'}
= render 'shared/plugins/index'
diff --git a/app/views/admin/identities/_form.html.haml b/app/views/admin/identities/_form.html.haml
index 5381b854f5c..946d868da01 100644
--- a/app/views/admin/identities/_form.html.haml
+++ b/app/views/admin/identities/_form.html.haml
@@ -1,16 +1,16 @@
-= form_for [:admin, @user, @identity], html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for [:admin, @user, @identity], html: { class: 'fieldset-form' } do |f|
= form_errors(@identity)
- .form-group
- = f.label :provider, class: 'control-label'
+ .form-group.row
+ = f.label :provider, class: 'col-form-label col-sm-2'
.col-sm-10
- values = Gitlab::Auth::OAuth::Provider.providers.map { |name| ["#{Gitlab::Auth::OAuth::Provider.label_for(name)} (#{name})", name] }
= f.select :provider, values, { allow_blank: false }, class: 'form-control'
- .form-group
- = f.label :extern_uid, "Identifier", class: 'control-label'
+ .form-group.row
+ = f.label :extern_uid, _("Identifier"), class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_field :extern_uid, class: 'form-control', required: true
.form-actions
- = f.submit 'Save changes', class: "btn btn-save"
+ = f.submit _('Save changes'), class: "btn btn-save"
diff --git a/app/views/admin/identities/_identity.html.haml b/app/views/admin/identities/_identity.html.haml
index ef5a3f1d969..5ed59809db5 100644
--- a/app/views/admin/identities/_identity.html.haml
+++ b/app/views/admin/identities/_identity.html.haml
@@ -4,9 +4,9 @@
%td
= identity.extern_uid
%td
- = link_to edit_admin_user_identity_path(@user, identity), class: 'btn btn-xs btn-grouped' do
- Edit
+ = link_to edit_admin_user_identity_path(@user, identity), class: 'btn btn-sm btn-grouped' do
+ = _("Edit")
= link_to [:admin, @user, identity], method: :delete,
- class: 'btn btn-xs btn-danger',
- data: { confirm: "Are you sure you want to remove this identity?" } do
- Delete
+ class: 'btn btn-sm btn-danger',
+ data: { confirm: _("Are you sure you want to remove this identity?") } do
+ = _('Delete')
diff --git a/app/views/admin/identities/edit.html.haml b/app/views/admin/identities/edit.html.haml
index 515d46b0f29..1ad6ce969cb 100644
--- a/app/views/admin/identities/edit.html.haml
+++ b/app/views/admin/identities/edit.html.haml
@@ -1,6 +1,6 @@
-- page_title "Edit", @identity.provider, "Identities", @user.name, "Users"
+- page_title _("Edit"), @identity.provider, _("Identities"), @user.name, _("Users")
%h3.page-title
- Edit identity for #{@user.name}
+ = _('Edit identity for %{user_name}') % { user_name: @user.name }
%hr
= render 'form'
diff --git a/app/views/admin/identities/index.html.haml b/app/views/admin/identities/index.html.haml
index ff67e59cdac..59373ee6752 100644
--- a/app/views/admin/identities/index.html.haml
+++ b/app/views/admin/identities/index.html.haml
@@ -1,15 +1,15 @@
-- page_title "Identities", @user.name, "Users"
+- page_title _("Identities"), @user.name, _("Users")
= render 'admin/users/head'
-= link_to 'New identity', new_admin_user_identity_path, class: 'pull-right btn btn-new'
+= link_to _('New identity'), new_admin_user_identity_path, class: 'float-right btn btn-new'
- if @identities.present?
.table-holder
%table.table
%thead
%tr
- %th Provider
- %th Identifier
+ %th= _('Provider')
+ %th= _('Identifier')
%th
= render @identities
- else
- %h4 This user has no identities
+ %h4= _('This user has no identities')
diff --git a/app/views/admin/identities/new.html.haml b/app/views/admin/identities/new.html.haml
index e30bf0ef0ee..ee743b0fd3c 100644
--- a/app/views/admin/identities/new.html.haml
+++ b/app/views/admin/identities/new.html.haml
@@ -1,4 +1,4 @@
-- page_title "New Identity"
-%h3.page-title New identity
+- page_title _("New Identity")
+%h3.page-title= _('New identity')
%hr
= render 'form'
diff --git a/app/views/admin/labels/_form.html.haml b/app/views/admin/labels/_form.html.haml
index d5e6bede36a..ee2d4c8430a 100644
--- a/app/views/admin/labels/_form.html.haml
+++ b/app/views/admin/labels/_form.html.haml
@@ -1,24 +1,25 @@
-= form_for [:admin, @label], html: { class: 'form-horizontal label-form js-requires-input' } do |f|
+= form_for [:admin, @label], html: { class: 'label-form js-requires-input' } do |f|
= form_errors(@label)
- .form-group
- = f.label :title, class: 'control-label'
+ .form-group.row
+ = f.label :title, class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_field :title, class: "form-control", required: true
- .form-group
- = f.label :description, class: 'control-label'
+ .form-group.row
+ = f.label :description, class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_field :description, class: "form-control js-quick-submit"
- .form-group
- = f.label :color, "Background color", class: 'control-label'
+ .form-group.row
+ = f.label :color, _("Background color"), class: 'col-form-label col-sm-2'
.col-sm-10
.input-group
- .input-group-addon.label-color-preview &nbsp;
+ .input-group-prepend
+ .input-group-text.label-color-preview &nbsp;
= f.text_field :color, class: "form-control"
- .help-block
- Choose any color.
+ .form-text.text-muted
+ = _('Choose any color.')
%br
- Or you can choose one of suggested colors below
+ = _("Or you can choose one of the suggested colors below")
.suggest-colors
- suggested_colors.each do |color|
@@ -26,5 +27,5 @@
&nbsp;
.form-actions
- = f.submit 'Save', class: 'btn btn-save js-save-button'
- = link_to "Cancel", admin_labels_path, class: 'btn btn-cancel'
+ = f.submit _('Save'), class: 'btn btn-save js-save-button'
+ = link_to _("Cancel"), admin_labels_path, class: 'btn btn-cancel'
diff --git a/app/views/admin/labels/_label.html.haml b/app/views/admin/labels/_label.html.haml
index 77b174fbb27..c3ea2352898 100644
--- a/app/views/admin/labels/_label.html.haml
+++ b/app/views/admin/labels/_label.html.haml
@@ -2,6 +2,6 @@
.label-row
= render_colored_label(label, tooltip: false)
= markdown_field(label, :description)
- .pull-right
- = link_to 'Edit', edit_admin_label_path(label), class: 'btn btn-sm'
- = link_to 'Delete', admin_label_path(label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Delete this label? Are you sure?"}
+ .float-right
+ = link_to _('Edit'), edit_admin_label_path(label), class: 'btn btn-sm'
+ = link_to _('Delete'), admin_label_path(label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Delete this label? Are you sure?"}
diff --git a/app/views/admin/labels/destroy.js.haml b/app/views/admin/labels/destroy.js.haml
index 9d51762890f..7a0dcbdd1c6 100644
--- a/app/views/admin/labels/destroy.js.haml
+++ b/app/views/admin/labels/destroy.js.haml
@@ -1,2 +1,2 @@
- if @labels.size == 0
- $('.labels').load(document.URL + ' .light-well').hide().fadeIn(1000)
+ $('.labels').load(document.URL + ' .card.bg-light').hide().fadeIn(1000)
diff --git a/app/views/admin/labels/edit.html.haml b/app/views/admin/labels/edit.html.haml
index 96f0d404ac4..652ed095d00 100644
--- a/app/views/admin/labels/edit.html.haml
+++ b/app/views/admin/labels/edit.html.haml
@@ -1,7 +1,7 @@
-- add_to_breadcrumbs "Labels", admin_labels_path
-- breadcrumb_title "Edit Label"
-- page_title "Edit", @label.name, "Labels"
+- add_to_breadcrumbs _("Labels"), admin_labels_path
+- breadcrumb_title _("Edit Label")
+- page_title _("Edit"), @label.name, _("Labels")
%h3.page-title
- Edit Label
+ = _('Edit Label')
%hr
= render 'form'
diff --git a/app/views/admin/labels/index.html.haml b/app/views/admin/labels/index.html.haml
index 05d6b9ed238..d3e5247447a 100644
--- a/app/views/admin/labels/index.html.haml
+++ b/app/views/admin/labels/index.html.haml
@@ -1,10 +1,10 @@
-- page_title "Labels"
+- page_title _("Labels")
%div
- = link_to new_admin_label_path, class: "pull-right btn btn-nr btn-new" do
- New label
+ = link_to new_admin_label_path, class: "float-right btn btn-nr btn-new" do
+ = _('New label')
%h3.page-title
- Labels
+ = _('Labels')
%hr
.labels
@@ -13,6 +13,6 @@
= render @labels
= paginate @labels, theme: 'gitlab'
- else
- .light-well
- .nothing-here-block There are no labels yet
+ .card.bg-light
+ .nothing-here-block= _('There are no labels yet')
diff --git a/app/views/admin/labels/new.html.haml b/app/views/admin/labels/new.html.haml
index 0135ad0723d..20103fb8a29 100644
--- a/app/views/admin/labels/new.html.haml
+++ b/app/views/admin/labels/new.html.haml
@@ -1,5 +1,5 @@
-- page_title "New Label"
+- page_title _("New Label")
%h3.page-title
- New Label
+ = _('New Label')
%hr
= render 'form'
diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml
index 78757b6384f..e4c0382a437 100644
--- a/app/views/admin/logs/show.html.haml
+++ b/app/views/admin/logs/show.html.haml
@@ -2,10 +2,10 @@
- page_title "Logs"
%div{ class: container_class }
- %ul.nav-links.log-tabs
+ %ul.nav-links.log-tabs.nav.nav-tabs
- @loggers.each do |klass|
- %li{ class: active_when(klass == @loggers.first) }>
- = link_to klass.file_name, "##{klass.file_name_noext}", data: { toggle: 'tab' }
+ %li.nav-item
+ = link_to klass.file_name, "##{klass.file_name_noext}", data: { toggle: 'tab' }, class: "#{active_when(klass == @loggers.first)} nav-link"
.row-content-block
To prevent performance issues admin logs output the last 2000 lines
.tab-content
@@ -15,7 +15,7 @@
.js-file-title.file-title
%i.fa.fa-file
= klass.file_name
- .pull-right
+ .float-right
= link_to '#', class: 'log-bottom' do
%i.fa.fa-arrow-down
Scroll down
diff --git a/app/views/admin/projects/_projects.html.haml b/app/views/admin/projects/_projects.html.haml
index b5d7b889504..00933d726d9 100644
--- a/app/views/admin/projects/_projects.html.haml
+++ b/app/views/admin/projects/_projects.html.haml
@@ -12,10 +12,10 @@
= s_('AdminProjects|Delete')
.stats
- %span.badge
+ %span.badge.badge-pill
= storage_counter(project.statistics.storage_size)
- if project.archived
- %span.label.label-warning archived
+ %span.badge.badge-warning archived
.title
= link_to [:admin, project.namespace.becomes(Namespace), project] do
.dash-project-avatar
diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml
index c37d8ac45b9..57de792f92d 100644
--- a/app/views/admin/projects/index.html.haml
+++ b/app/views/admin/projects/index.html.haml
@@ -15,7 +15,7 @@
- namespace = Namespace.find(params[:namespace_id])
- toggle_text = "#{namespace.kind}: #{namespace.full_path}"
= dropdown_toggle(toggle_text, { toggle: 'dropdown', is_filter: 'true' }, { toggle_class: 'js-namespace-select large' })
- .dropdown-menu.dropdown-select.dropdown-menu-align-right
+ .dropdown-menu.dropdown-select.dropdown-menu-right
= dropdown_title('Namespaces')
= dropdown_filter("Search for Namespace")
= dropdown_content
@@ -25,7 +25,7 @@
New Project
= button_tag "Search", class: "btn btn-primary btn-search hide"
- %ul.nav-links
+ %ul.nav-links.nav.nav-tabs
- opts = params[:visibility_level].present? ? {} : { page: admin_projects_path }
= nav_link(opts) do
= link_to admin_projects_path do
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index 75ca5106fd5..ccba1c461fc 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -3,15 +3,15 @@
- page_title @project.full_name, "Projects"
%h3.page-title
Project: #{@project.full_name}
- = link_to edit_project_path(@project), class: "btn btn-nr pull-right" do
+ = link_to edit_project_path(@project), class: "btn btn-nr float-right" do
%i.fa.fa-pencil-square-o
Edit
%hr
- if @project.last_repository_check_failed?
.row
.col-md-12
- .panel
- .panel-heading.alert.alert-danger
+ .card
+ .card-header.alert.alert-danger
Last repository check
= "(#{time_ago_with_tooltip(@project.last_repository_check_at)})"
failed. See
@@ -19,10 +19,10 @@
for error messages.
.row
.col-md-6
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
Project info:
- %ul.well-list
+ %ul.content-list
%li
%span.light Name:
%strong
@@ -110,14 +110,14 @@
= visibility_level_icon(@project.visibility_level)
= visibility_level_label(@project.visibility_level)
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
Transfer project
- .panel-body
- = form_for @project, url: transfer_admin_project_path(@project), method: :put, html: { class: 'form-horizontal' } do |f|
- .form-group
- = f.label :new_namespace_id, "Namespace", class: 'control-label'
- .col-sm-10
+ .card-body
+ = form_for @project, url: transfer_admin_project_path(@project), method: :put do |f|
+ .form-group.row
+ = f.label :new_namespace_id, "Namespace", class: 'col-form-label col-sm-3'
+ .col-sm-9
.dropdown
= dropdown_toggle('Search for Namespace', { toggle: 'dropdown', field_name: 'new_namespace_id' }, { toggle_class: 'js-namespace-select large' })
.dropdown-menu.dropdown-select
@@ -126,14 +126,14 @@
= dropdown_content
= dropdown_loading
- .form-group
- .col-sm-offset-2.col-sm-10
+ .form-group.row
+ .offset-sm-3.col-sm-9
= f.submit 'Transfer', class: 'btn btn-primary'
- .panel.panel-default.repository-check
- .panel-heading
+ .card.repository-check
+ .card-header
Repository check
- .panel-body
+ .card-body
= form_for @project, url: repository_check_admin_project_path(@project), method: :post do |f|
.form-group
- if @project.last_repository_check_at.nil?
@@ -158,29 +158,29 @@
.col-md-6
- if @group
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
%strong= @group.name
group members
- %span.badge= @group_members.size
- .pull-right
- = link_to admin_group_path(@group), class: 'btn btn-xs' do
+ %span.badge.badge-pill= @group_members.size
+ .float-right
+ = link_to admin_group_path(@group), class: 'btn btn-sm' do
= icon('pencil-square-o', text: 'Manage access')
- %ul.well-list.content-list.members-list
+ %ul.content-list.members-list
= render partial: 'shared/members/member', collection: @group_members, as: :member, locals: { show_controls: false }
- .panel-footer
+ .card-footer
= paginate @group_members, param_name: 'group_members_page', theme: 'gitlab'
= render 'shared/members/requests', membership_source: @project, requesters: @requesters, force_mobile_view: true
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
%strong= @project.name
project members
- %span.badge= @project.users.size
- .pull-right
- = link_to icon('pencil-square-o', text: 'Manage access'), polymorphic_url([@project, :members]), class: "btn btn-xs"
- %ul.well-list.project_members.content-list.members-list
+ %span.badge.badge-pill= @project.users.size
+ .float-right
+ = link_to icon('pencil-square-o', text: 'Manage access'), polymorphic_url([@project, :members]), class: "btn btn-sm"
+ %ul.content-list.project_members.members-list
= render partial: 'shared/members/member', collection: @project_members, as: :member, locals: { show_controls: false }
- .panel-footer
+ .card-footer
= paginate @project_members, param_name: 'project_members_page', theme: 'gitlab'
diff --git a/app/views/admin/requests_profiles/index.html.haml b/app/views/admin/requests_profiles/index.html.haml
index cb02a750490..adfc67d66d0 100644
--- a/app/views/admin/requests_profiles/index.html.haml
+++ b/app/views/admin/requests_profiles/index.html.haml
@@ -13,8 +13,8 @@
- if @profiles.present?
.prepend-top-default
- @profiles.each do |path, profiles|
- .panel.panel-default.panel-small
- .panel-heading
+ .card.card-small
+ .card-header
%code= path
%ul.content-list
- profiles.each do |profile|
diff --git a/app/views/admin/runners/_runner.html.haml b/app/views/admin/runners/_runner.html.haml
index 99fbbaec487..43937b01339 100644
--- a/app/views/admin/runners/_runner.html.haml
+++ b/app/views/admin/runners/_runner.html.haml
@@ -1,15 +1,15 @@
%tr{ id: dom_id(runner) }
%td
- - if runner.shared?
- %span.label.label-success shared
+ - if runner.instance_type?
+ %span.badge.badge-success shared
- elsif runner.group_type?
- %span.label.label-success group
+ %span.badge.badge-success group
- else
- %span.label.label-info specific
+ %span.badge.badge-info specific
- if runner.locked?
- %span.label.label-warning locked
+ %span.badge.badge-warning locked
- unless runner.active?
- %span.label.label-danger paused
+ %span.badge.badge-danger paused
%td
= link_to admin_runner_path(runner) do
@@ -21,7 +21,7 @@
%td
= runner.ip_address
%td
- - if runner.shared? || runner.group_type?
+ - if runner.instance_type? || runner.group_type?
n/a
- else
= runner.projects.count(:all)
@@ -29,7 +29,7 @@
#{runner.builds.count(:all)}
%td
- runner.tag_list.sort.each do |tag|
- %span.label.label-primary
+ %span.badge.badge-primary
= tag
%td
- if runner.contacted_at
@@ -37,7 +37,7 @@
- else
Never
%td.admin-runner-btn-group-cell
- .pull-right.btn-group
+ .float-right.btn-group
= link_to admin_runner_path(runner), class: 'btn btn-sm btn-default has-tooltip', title: 'Edit', ref: 'tooltip', aria: { label: 'Edit' }, data: { placement: 'top', container: 'body'} do
= icon('pencil')
&nbsp;
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index e49bf1cffc8..8dfd176f1b7 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -14,23 +14,23 @@
%span Each Runner can be in one of the following states:
%ul
%li
- %span.label.label-success shared
+ %span.badge.badge-success shared
\- Runner runs jobs from all unassigned projects
%li
- %span.label.label-success group
+ %span.badge.badge-success group
\- Runner runs jobs from all unassigned projects in its group
%li
- %span.label.label-info specific
+ %span.badge.badge-info specific
\- Runner runs jobs from assigned projects
%li
- %span.label.label-warning locked
+ %span.badge.badge-warning locked
\- Runner cannot be assigned to other projects
%li
- %span.label.label-danger paused
+ %span.badge.badge-danger paused
\- Runner will not receive any new jobs
.bs-callout.clearfix
- .pull-left
+ .float-left
%p
You can reset runners registration token by pressing a button below.
.prepend-top-10
@@ -42,13 +42,13 @@
locals: { registration_token: Gitlab::CurrentSettings.runners_registration_token }
.append-bottom-20.clearfix
- .pull-left
+ .float-left
= form_tag admin_runners_path, id: 'runners-search', class: 'form-inline', method: :get do
.form-group
= search_field_tag :search, params[:search], class: 'form-control input-short', placeholder: 'Runner description or token', spellcheck: false
= submit_tag 'Search', class: 'btn'
- .pull-right.light
+ .float-right.light
Runners with last contact more than a minute ago: #{@active_runners_cnt}
%br
@@ -67,7 +67,7 @@
%th Projects
%th Jobs
%th Tags
- %th= link_to 'Last contact', admin_runners_path(params.slice(:search).merge(sort: 'contacted_asc'))
+ %th= link_to 'Last contact', admin_runners_path(safe_params.slice(:search).merge(sort: 'contacted_asc'))
%th
- @runners.each do |runner|
diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml
index 73fadc042b1..62b7a4cbd07 100644
--- a/app/views/admin/runners/show.html.haml
+++ b/app/views/admin/runners/show.html.haml
@@ -1,8 +1,8 @@
= content_for :title do
%h3.project-title
Runner ##{@runner.id}
- .pull-right
- - if @runner.shared?
+ .float-right
+ - if @runner.instance_type?
%span.runner-state.runner-state-shared
Shared
- else
@@ -13,7 +13,7 @@
- breadcrumb_title "##{@runner.id}"
- @no_container = true
-- if @runner.shared?
+- if @runner.instance_type?
.bs-callout.bs-callout-success
%h4 This Runner will process jobs from ALL UNASSIGNED projects
%p
@@ -48,8 +48,8 @@
%strong
= project.full_name
%td
- .pull-right
- = link_to 'Disable', [:admin, project.namespace.becomes(Namespace), project, runner_project], method: :delete, class: 'btn btn-danger btn-xs'
+ .float-right
+ = link_to 'Disable', [:admin, project.namespace.becomes(Namespace), project, runner_project], method: :delete, class: 'btn btn-danger btn-sm'
%table.table.unassigned-projects
%thead
@@ -70,10 +70,10 @@
%td
= project.full_name
%td
- .pull-right
+ .float-right
= form_for [:admin, project.namespace.becomes(Namespace), project, project.runner_projects.new] do |f|
= f.hidden_field :runner_id, value: @runner.id
- = f.submit 'Enable', class: 'btn btn-xs'
+ = f.submit 'Enable', class: 'btn btn-sm'
= paginate @projects, theme: "gitlab"
.col-md-6
diff --git a/app/views/admin/services/_form.html.haml b/app/views/admin/services/_form.html.haml
index e5b8ebdf613..993006e8745 100644
--- a/app/views/admin/services/_form.html.haml
+++ b/app/views/admin/services/_form.html.haml
@@ -3,9 +3,8 @@
%p #{@service.description} template
-= form_for :service, url: admin_application_settings_service_path, method: :put, html: { class: 'form-horizontal fieldset-form' } do |form|
+= form_for :service, url: admin_application_settings_service_path, method: :put, html: { class: 'fieldset-form' } do |form|
= render 'shared/service_settings', form: form, subject: @service
.footer-block.row-content-block
- .form-actions
- = form.submit 'Save', class: 'btn btn-save'
+ = form.submit 'Save', class: 'btn btn-save'
diff --git a/app/views/admin/spam_logs/_spam_log.html.haml b/app/views/admin/spam_logs/_spam_log.html.haml
index ea6a0c4fb77..9d47dc1cce5 100644
--- a/app/views/admin/spam_logs/_spam_log.html.haml
+++ b/app/views/admin/spam_logs/_spam_log.html.haml
@@ -24,16 +24,16 @@
%td
- if user
= link_to 'Remove user', admin_spam_log_path(spam_log, remove_user: true),
- data: { confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-xs btn-remove"
+ data: { confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-sm btn-remove"
%td
- if spam_log.submitted_as_ham?
- .btn.btn-xs.disabled
+ .btn.btn-sm.disabled
Submitted as ham
- else
- = link_to 'Submit as ham', mark_as_ham_admin_spam_log_path(spam_log), method: :post, class: 'btn btn-xs btn-warning'
+ = link_to 'Submit as ham', mark_as_ham_admin_spam_log_path(spam_log), method: :post, class: 'btn btn-sm btn-warning'
- if user && !user.blocked?
- = link_to 'Block user', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-xs"
+ = link_to 'Block user', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-sm"
- else
- .btn.btn-xs.disabled
+ .btn.btn-sm.disabled
Already blocked
- = link_to 'Remove log', [:admin, spam_log], remote: true, method: :delete, class: "btn btn-xs btn-close js-remove-tr"
+ = link_to 'Remove log', [:admin, spam_log], remote: true, method: :delete, class: "btn btn-sm btn-close js-remove-tr"
diff --git a/app/views/admin/system_info/show.html.haml b/app/views/admin/system_info/show.html.haml
index 23f9927cfee..b19934e028d 100644
--- a/app/views/admin/system_info/show.html.haml
+++ b/app/views/admin/system_info/show.html.haml
@@ -5,7 +5,7 @@
.prepend-top-default
.row
.col-sm-4
- .light-well
+ .card.bg-light.light-well
%h4 CPU
.data
- if @cpus
@@ -14,7 +14,7 @@
= icon('warning', class: 'text-warning')
Unable to collect CPU info
.col-sm-4
- .light-well
+ .card.bg-light.light-well
%h4 Memory Usage
.data
- if @memory
@@ -23,7 +23,7 @@
= icon('warning', class: 'text-warning')
Unable to collect memory info
.col-sm-4
- .light-well
+ .card.bg-light.light-well
%h4 Disk Usage
.data
- @disks.each do |disk|
@@ -31,7 +31,7 @@
%p= disk[:disk_name]
%p= disk[:mount_path]
.col-sm-4
- .light-well
+ .card.bg-light.light-well
%h4 Uptime
.data
%h1= distance_of_time_in_words_to_now(Rails.application.config.booted_at)
diff --git a/app/views/admin/users/_access_levels.html.haml b/app/views/admin/users/_access_levels.html.haml
index 794aaec89bd..04acc5f8423 100644
--- a/app/views/admin/users/_access_levels.html.haml
+++ b/app/views/admin/users/_access_levels.html.haml
@@ -1,26 +1,26 @@
%fieldset
%legend Access
- .form-group
- = f.label :projects_limit, class: 'control-label'
+ .form-group.row
+ = f.label :projects_limit, class: 'col-form-label col-sm-2'
.col-sm-10= f.number_field :projects_limit, min: 0, max: Gitlab::Database::MAX_INT_VALUE, class: 'form-control'
- .form-group
- = f.label :can_create_group, class: 'control-label'
+ .form-group.row
+ = f.label :can_create_group, class: 'col-form-label col-sm-2'
.col-sm-10= f.check_box :can_create_group
- .form-group
- = f.label :access_level, class: 'control-label'
+ .form-group.row
+ = f.label :access_level, class: 'col-form-label col-sm-2'
.col-sm-10
- editing_current_user = (current_user == @user)
= f.radio_button :access_level, :regular, disabled: editing_current_user
- = label_tag :regular do
+ = label_tag :regular, class: 'font-weight-bold' do
Regular
%p.light
Regular users have access to their groups and projects
= f.radio_button :access_level, :admin, disabled: editing_current_user
- = label_tag :admin do
+ = label_tag :admin, class: 'font-weight-bold' do
Admin
%p.light
Administrators have access to all groups, projects and users and can manage all features in this installation
@@ -28,8 +28,8 @@
%p.light
You cannot remove your own admin rights.
- .form-group
- = f.label :external, class: 'control-label'
+ .form-group.row
+ = f.label :external, class: 'col-form-label col-sm-2'
.col-sm-10
= f.check_box :external do
External
diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml
index e911af3f6f9..58be07fc83e 100644
--- a/app/views/admin/users/_form.html.haml
+++ b/app/views/admin/users/_form.html.haml
@@ -1,21 +1,21 @@
.user_new
- = form_for [:admin, @user], html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_for [:admin, @user], html: { class: 'fieldset-form' } do |f|
= form_errors(@user)
%fieldset
%legend Account
- .form-group
- = f.label :name, class: 'control-label'
+ .form-group.row
+ = f.label :name, class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_field :name, required: true, autocomplete: 'off', class: 'form-control'
%span.help-inline * required
- .form-group
- = f.label :username, class: 'control-label'
+ .form-group.row
+ = f.label :username, class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_field :username, required: true, autocomplete: 'off', autocorrect: 'off', autocapitalize: 'off', spellcheck: false, class: 'form-control'
%span.help-inline * required
- .form-group
- = f.label :email, class: 'control-label'
+ .form-group.row
+ = f.label :email, class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_field :email, required: true, autocomplete: 'off', class: 'form-control'
%span.help-inline * required
@@ -23,8 +23,8 @@
- if @user.new_record?
%fieldset
%legend Password
- .form-group
- = f.label :password, class: 'control-label'
+ .form-group.row
+ = f.label :password, class: 'col-form-label col-sm-2'
.col-sm-10
%strong
Reset link will be generated and sent to the user.
@@ -33,33 +33,33 @@
- else
%fieldset
%legend Password
- .form-group
- = f.label :password, class: 'control-label'
+ .form-group.row
+ = f.label :password, class: 'col-form-label col-sm-2'
.col-sm-10= f.password_field :password, disabled: f.object.force_random_password, class: 'form-control'
- .form-group
- = f.label :password_confirmation, class: 'control-label'
+ .form-group.row
+ = f.label :password_confirmation, class: 'col-form-label col-sm-2'
.col-sm-10= f.password_field :password_confirmation, disabled: f.object.force_random_password, class: 'form-control'
= render partial: 'access_levels', locals: { f: f }
%fieldset
%legend Profile
- .form-group
- = f.label :avatar, class: 'control-label'
+ .form-group.row
+ = f.label :avatar, class: 'col-form-label col-sm-2'
.col-sm-10
= f.file_field :avatar
- .form-group
- = f.label :skype, class: 'control-label'
+ .form-group.row
+ = f.label :skype, class: 'col-form-label col-sm-2'
.col-sm-10= f.text_field :skype, class: 'form-control'
- .form-group
- = f.label :linkedin, class: 'control-label'
+ .form-group.row
+ = f.label :linkedin, class: 'col-form-label col-sm-2'
.col-sm-10= f.text_field :linkedin, class: 'form-control'
- .form-group
- = f.label :twitter, class: 'control-label'
+ .form-group.row
+ = f.label :twitter, class: 'col-form-label col-sm-2'
.col-sm-10= f.text_field :twitter, class: 'form-control'
- .form-group
- = f.label :website_url, 'Website', class: 'control-label'
+ .form-group.row
+ = f.label :website_url, 'Website', class: 'col-form-label col-sm-2'
.col-sm-10= f.text_field :website_url, class: 'form-control'
.form-actions
diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml
index be41c33b853..bfbc16d37a0 100644
--- a/app/views/admin/users/_head.html.haml
+++ b/app/views/admin/users/_head.html.haml
@@ -7,14 +7,14 @@
- if @user.admin
%span.cred (Admin)
- .pull-right
+ .float-right
- if @user != current_user && @user.can?(:log_in)
= link_to 'Impersonate', impersonate_admin_user_path(@user), method: :post, class: "btn btn-nr btn-grouped btn-info"
= link_to edit_admin_user_path(@user), class: "btn btn-nr btn-grouped" do
%i.fa.fa-pencil-square-o
Edit
%hr
-%ul.nav-links
+%ul.nav-links.nav.nav-tabs
= nav_link(path: 'users#show') do
= link_to "Account", admin_user_path(@user)
= nav_link(path: 'users#projects') do
diff --git a/app/views/admin/users/_profile.html.haml b/app/views/admin/users/_profile.html.haml
index 6bc217f84cc..4fcb9aad343 100644
--- a/app/views/admin/users/_profile.html.haml
+++ b/app/views/admin/users/_profile.html.haml
@@ -1,7 +1,7 @@
-.panel.panel-default
- .panel-heading
+.card
+ .card-header
Profile
- %ul.well-list
+ %ul.content-list
%li
%span.light Member since
%strong= user.created_at.to_s(:medium)
diff --git a/app/views/admin/users/_projects.html.haml b/app/views/admin/users/_projects.html.haml
index a126a858ea8..81cfb71af16 100644
--- a/app/views/admin/users/_projects.html.haml
+++ b/app/views/admin/users/_projects.html.haml
@@ -1,13 +1,13 @@
- if local_assigns.has_key?(:contributed_projects) && contributed_projects.present?
- .panel.panel-default.contributed-projects
- .panel-heading Projects contributed to
+ .card.contributed-projects
+ .card-header Projects contributed to
= render 'shared/projects/list',
projects: contributed_projects.sort_by(&:star_count).reverse,
projects_limit: 5, stars: true, avatar: false
- if local_assigns.has_key?(:projects) && projects.present?
- .panel.panel-default
- .panel-heading Personal projects
+ .card
+ .card-header Personal projects
= render 'shared/projects/list',
projects: projects.sort_by(&:star_count).reverse,
projects_limit: 10, stars: true, avatar: false
diff --git a/app/views/admin/users/_user.html.haml b/app/views/admin/users/_user.html.haml
index badf3dd74b3..b2163ee85fa 100644
--- a/app/views/admin/users/_user.html.haml
+++ b/app/views/admin/users/_user.html.haml
@@ -5,11 +5,11 @@
.user-name.row-title.str-truncated-100
= link_to user.name, [:admin, user]
- if user.blocked?
- %span.label.label-danger blocked
+ %span.badge.badge-danger blocked
- if user.admin?
- %span.label.label-success Admin
+ %span.badge.badge-success Admin
- if user.external?
- %span.label.label-default External
+ %span.badge.badge-secondary External
- if user == current_user
%span It's you!
.row-second-line.str-truncated-100
@@ -21,7 +21,7 @@
%a.dropdown-new.btn.btn-default#project-settings-button{ href: '#', data: { toggle: 'dropdown' } }
= icon('cog')
= icon('caret-down')
- %ul.dropdown-menu.dropdown-menu-align-right
+ %ul.dropdown-menu.dropdown-menu-right
%li.dropdown-header
Settings
%li
diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml
index 10b8bf5d565..faeb82656ba 100644
--- a/app/views/admin/users/index.html.haml
+++ b/app/views/admin/users/index.html.haml
@@ -13,7 +13,7 @@
.dropdown
- toggle_text = if @sort.present? then sort_options_hash[@sort] else sort_title_name end
= dropdown_toggle(toggle_text, { toggle: 'dropdown' })
- %ul.dropdown-menu.dropdown-menu-align-right
+ %ul.dropdown-menu.dropdown-menu-right
%li.dropdown-header
Sort by
%li
@@ -38,35 +38,35 @@
= icon('angle-left')
.fade-right
= icon('angle-right')
- %ul.nav-links.scrolling-tabs
+ %ul.nav-links.nav.nav-tabs.scrolling-tabs
= nav_link(html_options: { class: active_when(params[:filter].nil?) }) do
= link_to admin_users_path do
Active
- %small.badge= limited_counter_with_delimiter(User.active)
+ %small.badge.badge-pill= limited_counter_with_delimiter(User.active)
= nav_link(html_options: { class: active_when(params[:filter] == 'admins') }) do
= link_to admin_users_path(filter: "admins") do
Admins
- %small.badge= limited_counter_with_delimiter(User.admins)
+ %small.badge.badge-pill= limited_counter_with_delimiter(User.admins)
= nav_link(html_options: { class: "#{active_when(params[:filter] == 'two_factor_enabled')} filter-two-factor-enabled" }) do
= link_to admin_users_path(filter: 'two_factor_enabled') do
2FA Enabled
- %small.badge= limited_counter_with_delimiter(User.with_two_factor)
+ %small.badge.badge-pill= limited_counter_with_delimiter(User.with_two_factor)
= nav_link(html_options: { class: "#{active_when(params[:filter] == 'two_factor_disabled')} filter-two-factor-disabled" }) do
= link_to admin_users_path(filter: 'two_factor_disabled') do
2FA Disabled
- %small.badge= limited_counter_with_delimiter(User.without_two_factor)
+ %small.badge.badge-pill= limited_counter_with_delimiter(User.without_two_factor)
= nav_link(html_options: { class: active_when(params[:filter] == 'external') }) do
= link_to admin_users_path(filter: 'external') do
External
- %small.badge= limited_counter_with_delimiter(User.external)
+ %small.badge.badge-pill= limited_counter_with_delimiter(User.external)
= nav_link(html_options: { class: active_when(params[:filter] == 'blocked') }) do
= link_to admin_users_path(filter: "blocked") do
Blocked
- %small.badge= limited_counter_with_delimiter(User.blocked)
+ %small.badge.badge-pill= limited_counter_with_delimiter(User.blocked)
= nav_link(html_options: { class: active_when(params[:filter] == 'wop') }) do
= link_to admin_users_path(filter: "wop") do
Without projects
- %small.badge= limited_counter_with_delimiter(User.without_projects)
+ %small.badge.badge-pill= limited_counter_with_delimiter(User.without_projects)
%ul.flex-list.content-list
- if @users.empty?
diff --git a/app/views/admin/users/projects.html.haml b/app/views/admin/users/projects.html.haml
index 96835ee9af5..cf50d45f755 100644
--- a/app/views/admin/users/projects.html.haml
+++ b/app/views/admin/users/projects.html.haml
@@ -2,19 +2,19 @@
= render 'admin/users/head'
- if @user.groups.any?
- .panel.panel-default
- .panel-heading Group projects
- %ul.well-list
+ .card
+ .card-header Group projects
+ %ul.hover-list
- @user.group_members.includes(:source).each do |group_member|
- group = group_member.group
%li.group_member
%strong= link_to group.name, admin_group_path(group)
&ndash; access to
#{pluralize(group.projects.count, 'project')}
- .pull-right
+ .float-right
%span.light.vertical-align-middle= group_member.human_access
- unless group_member.owner?
- = link_to group_group_member_path(group, group_member), data: { confirm: remove_member_message(group_member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove prepend-left-10", title: 'Remove user from group' do
+ = link_to group_group_member_path(group, group_member), data: { confirm: remove_member_message(group_member) }, method: :delete, remote: true, class: "btn-sm btn btn-remove prepend-left-10", title: 'Remove user from group' do
%i.fa.fa-times.fa-inverse
.row
@@ -26,9 +26,9 @@
.col-md-6
- .panel.panel-default
- .panel-heading Joined projects (#{@joined_projects.count})
- %ul.well-list
+ .card
+ .card-header Joined projects (#{@joined_projects.count})
+ %ul.hover-list
- @joined_projects.sort_by(&:full_name).each do |project|
- member = project.team.find_member(@user.id)
%li.project_member
@@ -37,12 +37,12 @@
= project.full_name
- if member
- .pull-right
+ .float-right
- if member.owner?
%span.light Owner
- else
%span.light.vertical-align-middle= member.human_access
- if member.respond_to? :project
- = link_to project_project_member_path(project, member), data: { confirm: remove_member_message(member) }, remote: true, method: :delete, class: "btn-xs btn btn-remove prepend-left-10", title: 'Remove user from project' do
+ = link_to project_project_member_path(project, member), data: { confirm: remove_member_message(member) }, remote: true, method: :delete, class: "btn-sm btn btn-remove prepend-left-10", title: 'Remove user from project' do
%i.fa.fa-times
diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml
index 814ccdae8f3..b0562226f5f 100644
--- a/app/views/admin/users/show.html.haml
+++ b/app/views/admin/users/show.html.haml
@@ -5,10 +5,10 @@
.row
.col-md-6
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
= @user.name
- %ul.well-list
+ %ul.content-list
%li
= image_tag avatar_icon_for_user(@user, 60), class: "avatar s60"
%li
@@ -18,10 +18,10 @@
= @user.username
= render 'admin/users/profile', user: @user
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
Account:
- %ul.well-list
+ %ul.content-list
%li
%span.light Name:
%strong= @user.name
@@ -37,7 +37,7 @@
%li
%span.light Secondary email:
%strong= email.email
- = link_to remove_email_admin_user_path(@user, email), data: { confirm: "Are you sure you want to remove #{email.email}?" }, method: :delete, class: "btn-xs btn btn-remove pull-right", title: 'Remove secondary email', id: "remove_email_#{email.id}" do
+ = link_to remove_email_admin_user_path(@user, email), data: { confirm: "Are you sure you want to remove #{email.email}?" }, method: :delete, class: "btn-sm btn btn-remove float-right", title: 'Remove secondary email', id: "remove_email_#{email.id}" do
%i.fa.fa-times
%li.two-factor-status
@@ -45,7 +45,7 @@
%strong{ class: @user.two_factor_enabled? ? 'cgreen' : 'cred' }
- if @user.two_factor_enabled?
Enabled
- = link_to 'Disable', disable_two_factor_admin_user_path(@user), data: {confirm: 'Are you sure?'}, method: :patch, class: 'btn btn-xs btn-remove pull-right', title: 'Disable Two-factor Authentication'
+ = link_to 'Disable', disable_two_factor_admin_user_path(@user), data: {confirm: 'Are you sure?'}, method: :patch, class: 'btn btn-sm btn-remove float-right', title: 'Disable Two-factor Authentication'
- else
Disabled
@@ -128,20 +128,20 @@
.col-md-6
- unless @user == current_user
- unless @user.confirmed?
- .panel.panel-info
- .panel-heading
+ .card.bg-info
+ .card-header
Confirm user
- .panel-body
+ .card-body
- if @user.unconfirmed_email.present?
- email = " (#{@user.unconfirmed_email})"
%p This user has an unconfirmed email address#{email}. You may force a confirmation.
%br
= link_to 'Confirm user', confirm_admin_user_path(@user), method: :put, class: "btn btn-info", data: { confirm: 'Are you sure?' }
- if @user.blocked?
- .panel.panel-info
- .panel-heading
+ .card.bg-info
+ .card-header
This user is blocked
- .panel-body
+ .card-body
%p A blocked user cannot:
%ul
%li Log in
@@ -149,10 +149,10 @@
%br
= link_to 'Unblock user', unblock_admin_user_path(@user), method: :put, class: "btn btn-info", data: { confirm: 'Are you sure?' }
- else
- .panel.panel-warning
- .panel-heading
+ .card.bg-warning
+ .card-header
Block this user
- .panel-body
+ .card-body
%p Blocking user has the following effects:
%ul
%li User will not be able to login
@@ -162,23 +162,23 @@
%br
= link_to 'Block user', block_admin_user_path(@user), data: { confirm: 'USER WILL BE BLOCKED! Are you sure?' }, method: :put, class: "btn btn-warning"
- if @user.access_locked?
- .panel.panel-info
- .panel-heading
+ .card.bg-info
+ .card-header
This account has been locked
- .panel-body
+ .card-body
%p This user has been temporarily locked due to excessive number of failed logins. You may manually unlock the account.
%br
= link_to 'Unlock user', unlock_admin_user_path(@user), method: :put, class: "btn btn-info", data: { confirm: 'Are you sure?' }
- .panel.panel-danger
- .panel-heading
+ .card.bg-danger
+ .card-header
= s_('AdminUsers|Delete user')
- .panel-body
+ .card-body
- if @user.can_be_removed? && can?(current_user, :destroy_user, @user)
%p Deleting a user has the following effects:
= render 'users/deletion_guidance', user: @user
%br
- %button.delete-user-button.btn.text-danger{ data: { toggle: 'modal',
+ %button.delete-user-button.btn.btn-danger{ data: { toggle: 'modal',
target: '#delete-user-modal',
delete_user_url: admin_user_path(@user),
block_user_url: block_admin_user_path(@user),
@@ -196,10 +196,10 @@
%p
You don't have access to delete this user.
- .panel.panel-danger
- .panel-heading
+ .card.bg-danger
+ .card-header
= s_('AdminUsers|Delete user and contributions')
- .panel-body
+ .card-body
- if can?(current_user, :destroy_user, @user)
%p
This option deletes the user and any contributions that
@@ -210,7 +210,7 @@
the user, and projects in them, will also be removed. Commits
to other projects are unaffected.
%br
- %button.delete-user-button.btn.text-danger{ data: { toggle: 'modal',
+ %button.delete-user-button.btn.btn-danger{ data: { toggle: 'modal',
target: '#delete-user-modal',
delete_user_url: admin_user_path(@user, hard_delete: true),
block_user_url: block_admin_user_path(@user),
diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml
index 4b3c52af16a..8ca9fb4512e 100644
--- a/app/views/award_emoji/_awards_block.html.haml
+++ b/app/views/award_emoji/_awards_block.html.haml
@@ -12,9 +12,9 @@
- if can?(current_user, :award_emoji, awardable)
.award-menu-holder.js-award-holder
%button.btn.award-control.has-tooltip.js-add-award{ type: 'button',
- 'aria-label': 'Add reaction',
+ 'aria-label': _('Add reaction'),
class: ("js-user-authored" if user_authored),
- data: { title: 'Add reaction', placement: "bottom" } }
+ data: { title: _('Add reaction'), placement: "bottom" } }
%span{ class: "award-control-icon award-control-icon-neutral" }= custom_icon('emoji_slightly_smiling_face')
%span{ class: "award-control-icon award-control-icon-positive" }= custom_icon('emoji_smiley')
%span{ class: "award-control-icon award-control-icon-super-positive" }= custom_icon('emoji_smile')
diff --git a/app/views/ci/lints/show.html.haml b/app/views/ci/lints/show.html.haml
index 22f149d1caa..d4455749803 100644
--- a/app/views/ci/lints/show.html.haml
+++ b/app/views/ci/lints/show.html.haml
@@ -1,8 +1,8 @@
.row.empty-state
- .col-xs-12
+ .col-12
.svg-content
= image_tag 'illustrations/feature_moved.svg'
- .col-xs-12
+ .col-12
.text-content.text-center
%h4= _("GitLab CI Linter has been moved")
%p
diff --git a/app/views/ci/runner/_how_to_setup_runner.html.haml b/app/views/ci/runner/_how_to_setup_runner.html.haml
index 37fb8fbab26..3ae9ce6c11f 100644
--- a/app/views/ci/runner/_how_to_setup_runner.html.haml
+++ b/app/views/ci/runner/_how_to_setup_runner.html.haml
@@ -5,7 +5,7 @@
%ol
%li
= _("Install a Runner compatible with GitLab CI")
- = (_("(checkout the %{link} for information on how to install it).") % { link: link }).html_safe
+ = (_("(check out the %{link} for information on how to install it).") % { link: link }).html_safe
%li
= _("Specify the following URL during the Runner setup:")
%code#coordinator_address= root_url(only_path: false)
diff --git a/app/views/ci/status/_dropdown_graph_badge.html.haml b/app/views/ci/status/_dropdown_graph_badge.html.haml
index d828f6f971d..8b0463db000 100644
--- a/app/views/ci/status/_dropdown_graph_badge.html.haml
+++ b/app/views/ci/status/_dropdown_graph_badge.html.haml
@@ -6,12 +6,12 @@
- tooltip = "#{subject.name} - #{status.status_tooltip}"
- if status.has_details?
- = link_to status.details_path, class: 'mini-pipeline-graph-dropdown-item', data: { toggle: 'tooltip', title: tooltip, html: true, container: 'body' } do
+ = link_to status.details_path, class: 'mini-pipeline-graph-dropdown-item', data: { toggle: 'tooltip', title: tooltip, html: 'true', container: 'body' } do
%span{ class: klass }= sprite_icon(status.icon)
%span.ci-build-text= subject.name
- else
- .menu-item.mini-pipeline-graph-dropdown-item{ data: { toggle: 'tooltip', html: true, title: tooltip, container: 'body' } }
+ .menu-item.mini-pipeline-graph-dropdown-item{ data: { toggle: 'tooltip', html: 'true', title: tooltip, container: 'body' } }
%span{ class: klass }= sprite_icon(status.icon)
%span.ci-build-text= subject.name
diff --git a/app/views/ci/variables/_variable_row.html.haml b/app/views/ci/variables/_variable_row.html.haml
index 571eb28f195..6ee55836dd2 100644
--- a/app/views/ci/variables/_variable_row.html.haml
+++ b/app/views/ci/variables/_variable_row.html.haml
@@ -43,5 +43,6 @@
%span.toggle-icon
= sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked')
= sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked')
+ = render_if_exists 'ci/variables/environment_scope', form_field: form_field, variable: variable
%button.js-row-remove-button.ci-variable-row-remove-button{ type: 'button', 'aria-label': s_('CiVariables|Remove variable row') }
= icon('minus-circle')
diff --git a/app/views/dashboard/_activity_head.html.haml b/app/views/dashboard/_activity_head.html.haml
index 7a3f3667ac1..7503548fa3d 100644
--- a/app/views/dashboard/_activity_head.html.haml
+++ b/app/views/dashboard/_activity_head.html.haml
@@ -1,5 +1,5 @@
.top-area
- %ul.nav-links
+ %ul.nav-links.nav.nav-tabs
%li{ class: active_when(params[:filter].nil?) }>
= link_to activity_dashboard_path, class: 'shortcuts-activity', data: {placement: 'right'} do
Your projects
diff --git a/app/views/dashboard/_groups_head.html.haml b/app/views/dashboard/_groups_head.html.haml
index 617c20b9635..d8f1e50544c 100644
--- a/app/views/dashboard/_groups_head.html.haml
+++ b/app/views/dashboard/_groups_head.html.haml
@@ -1,5 +1,5 @@
.top-area
- %ul.nav-links.mobile-separator
+ %ul.nav-links.mobile-separator.nav.nav-tabs
= nav_link(page: dashboard_groups_path) do
= link_to dashboard_groups_path, title: _("Your groups") do
Your groups
diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml
index 449a2ce625e..9b1d9b659f9 100644
--- a/app/views/dashboard/_projects_head.html.haml
+++ b/app/views/dashboard/_projects_head.html.haml
@@ -4,7 +4,7 @@
.top-area.scrolling-tabs-container.inner-page-scroll-tabs
.fade-left= icon('angle-left')
.fade-right= icon('angle-right')
- %ul.nav-links.scrolling-tabs.mobile-separator
+ %ul.nav-links.scrolling-tabs.mobile-separator.nav.nav-tabs
= nav_link(page: [dashboard_projects_path, root_path]) do
= link_to dashboard_projects_path, class: 'shortcuts-activity', data: {placement: 'right'} do
Your projects
diff --git a/app/views/dashboard/_snippets_head.html.haml b/app/views/dashboard/_snippets_head.html.haml
index a9488df07bd..e7e323a8683 100644
--- a/app/views/dashboard/_snippets_head.html.haml
+++ b/app/views/dashboard/_snippets_head.html.haml
@@ -1,5 +1,5 @@
.top-area
- %ul.nav-links
+ %ul.nav-links.nav.nav-tabs
= nav_link(page: dashboard_snippets_path, html_options: {class: 'home'}) do
= link_to dashboard_snippets_path, title: 'Your snippets', data: {placement: 'right'} do
Your snippets
@@ -8,5 +8,5 @@
Explore snippets
- if current_user
- .nav-controls.hidden-xs
+ .nav-controls.d-none.d-sm-block
= link_to "New snippet", new_snippet_path, class: "btn btn-new", title: "New snippet"
diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml
index 4bf04dadf01..86a21e24ac9 100644
--- a/app/views/dashboard/issues.html.haml
+++ b/app/views/dashboard/issues.html.haml
@@ -7,8 +7,7 @@
.top-area
= render 'shared/issuable/nav', type: :issues, display_count: !@no_filters_set
.nav-controls
- = link_to safe_params.merge(rss_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: 'Subscribe' do
- = icon('rss')
+ = render 'shared/issuable/feed_buttons'
= render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", with_feature_enabled: 'issues', type: :issues
= render 'shared/issuable/filter', type: :issues
diff --git a/app/views/dashboard/issues_calendar.ics.haml b/app/views/dashboard/issues_calendar.ics.haml
new file mode 100644
index 00000000000..59573e5fecf
--- /dev/null
+++ b/app/views/dashboard/issues_calendar.ics.haml
@@ -0,0 +1 @@
+= render 'issues/issues_calendar', issues: @issues
diff --git a/app/views/dashboard/projects/_nav.html.haml b/app/views/dashboard/projects/_nav.html.haml
index 97f854cc5f0..da3cf5807b0 100644
--- a/app/views/dashboard/projects/_nav.html.haml
+++ b/app/views/dashboard/projects/_nav.html.haml
@@ -1,5 +1,5 @@
.nav-block
- %ul.nav-links.mobile-separator
+ %ul.nav-links.mobile-separator.nav.nav-tabs
= nav_link(html_options: { class: ("active" unless params[:personal].present?) }) do
= link_to s_('DashboardProjects|All'), dashboard_projects_path
= nav_link(html_options: { class: ("active" if params[:personal].present?) }) do
diff --git a/app/views/dashboard/projects/starred.html.haml b/app/views/dashboard/projects/starred.html.haml
index b1efe59aadc..8933d9e31ff 100644
--- a/app/views/dashboard/projects/starred.html.haml
+++ b/app/views/dashboard/projects/starred.html.haml
@@ -11,5 +11,5 @@
- if params[:filter_projects] || any_projects?(@projects)
= render 'projects'
- else
- %h3 You don't have starred projects yet
+ %h3.page-title You don't have starred projects yet
%p.slead Visit project page and press on star icon and it will appear on this page.
diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml
index e86b1ab3116..4391624196b 100644
--- a/app/views/dashboard/snippets/index.html.haml
+++ b/app/views/dashboard/snippets/index.html.haml
@@ -5,7 +5,7 @@
= render 'dashboard/snippets_head'
= render partial: 'snippets/snippets_scope_menu', locals: { include_private: true }
-.visible-xs
+.d-block.d-sm-none
&nbsp;
= link_to new_snippet_path, class: "btn btn-new btn-block", title: "New snippet" do
New snippet
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index 664966989db..d5a9cc646a6 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -4,18 +4,18 @@
- if current_user.todos.any?
.top-area
- %ul.nav-links.mobile-separator
+ %ul.nav-links.mobile-separator.nav.nav-tabs
%li.todos-pending{ class: active_when(params[:state].blank? || params[:state] == 'pending') }>
= link_to todos_filter_path(state: 'pending') do
%span
Todos
- %span.badge
+ %span.badge.badge-pill
= number_with_delimiter(todos_pending_count)
%li.todos-done{ class: active_when(params[:state] == 'done') }>
= link_to todos_filter_path(state: 'done') do
%span
Done
- %span.badge
+ %span.badge.badge-pill
= number_with_delimiter(todos_done_count)
.nav-controls
@@ -35,7 +35,7 @@
- if params[:project_id].present?
= hidden_field_tag(:project_id, params[:project_id])
= dropdown_tag(project_dropdown_label(params[:project_id], 'Project'), options: { toggle_class: 'js-project-search js-filter-submit', title: 'Filter by project', filter: true, filterInput: 'input#project-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit',
- placeholder: 'Search projects', data: { data: todo_projects_options, default_label: 'Project' } })
+ placeholder: 'Search projects', data: { data: todo_projects_options, default_label: 'Project', display: 'static' } })
.filter-item.inline
- if params[:author_id].present?
= hidden_field_tag(:author_id, params[:author_id])
@@ -60,7 +60,7 @@
- else
= sort_title_recently_created
= icon('chevron-down')
- %ul.dropdown-menu.dropdown-menu-sort
+ %ul.dropdown-menu.dropdown-menu-sort.dropdown-menu-right
%li
= link_to todos_filter_path(sort: sort_value_label_priority) do
= sort_title_label_priority
@@ -73,7 +73,7 @@
- if @todos.any?
.js-todos-list-container
.js-todos-options{ data: { per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages } }
- .panel.panel-default.panel-without-border.panel-without-margin
+ .card.card-without-border.card-without-margin
%ul.content-list.todos-list
= render @todos
= paginate @todos, theme: "gitlab"
diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml
index 41462f503cb..0ee563ac066 100644
--- a/app/views/devise/sessions/_new_base.html.haml
+++ b/app/views/devise/sessions/_new_base.html.haml
@@ -6,11 +6,15 @@
= f.label :password
= f.password_field :password, class: "form-control bottom", required: true, title: "This field is required."
- if devise_mapping.rememberable?
- .remember-me.checkbox
+ .remember-me
%label{ for: "user_remember_me" }
= f.check_box :remember_me, class: 'remember-me-checkbox'
%span Remember me
- .pull-right.forgot-password
+ .float-right.forgot-password
= link_to "Forgot your password?", new_password_path(:user)
+ %div
+ - if captcha_enabled?
+ = recaptcha_tags
+
.submit-container.move-submit-down
= f.submit "Sign in", class: "btn btn-save"
diff --git a/app/views/devise/sessions/_new_crowd.html.haml b/app/views/devise/sessions/_new_crowd.html.haml
index 2556cb6f59b..36ff42090be 100644
--- a/app/views/devise/sessions/_new_crowd.html.haml
+++ b/app/views/devise/sessions/_new_crowd.html.haml
@@ -6,7 +6,7 @@
= label_tag :password
= password_field_tag :password, nil, { class: "form-control bottom", title: "This field is required.", required: true }
- if devise_mapping.rememberable?
- .remember-me.checkbox
+ .remember-me
%label{ for: "remember_me" }
= check_box_tag :remember_me, '1', false, id: 'remember_me'
%span Remember me
diff --git a/app/views/devise/sessions/_new_ldap.html.haml b/app/views/devise/sessions/_new_ldap.html.haml
index 3159d21598a..6bf7349f602 100644
--- a/app/views/devise/sessions/_new_ldap.html.haml
+++ b/app/views/devise/sessions/_new_ldap.html.haml
@@ -6,7 +6,7 @@
= label_tag :password
= password_field_tag :password, nil, { class: "form-control bottom", title: "This field is required.", required: true }
- if devise_mapping.rememberable?
- .remember-me.checkbox
+ .remember-me
%label{ for: "remember_me" }
= check_box_tag :remember_me, '1', false, id: 'remember_me'
%span Remember me
diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml
index 6e54b9b5645..ba168c4eab8 100644
--- a/app/views/devise/sessions/two_factor.html.haml
+++ b/app/views/devise/sessions/two_factor.html.haml
@@ -9,7 +9,7 @@
%div
= f.label 'Two-Factor Authentication code', name: :otp_attempt
= f.text_field :otp_attempt, class: 'form-control', required: true, autofocus: true, autocomplete: 'off', title: 'This field is required.'
- %p.help-block.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes.
+ %p.form-text.text-muted.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes.
.prepend-top-20
= f.submit "Verify code", class: "btn btn-save"
diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml
index 546cec4d565..3723814debe 100644
--- a/app/views/devise/shared/_omniauth_box.html.haml
+++ b/app/views/devise/shared/_omniauth_box.html.haml
@@ -7,7 +7,7 @@
%span.light
- has_icon = provider_has_icon?(provider)
= link_to provider_image_tag(provider), omniauth_authorize_path(:user, provider), method: :post, class: 'oauth-login' + (has_icon ? ' oauth-image-link' : ' btn'), id: "oauth-login-#{provider}"
- %fieldset.prepend-top-10.checkbox.remember-me
+ %fieldset.prepend-top-10.remember-me
%label
= check_box_tag :remember_me, nil, false, class: 'remember-me-checkbox'
%span
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index 2554b2688bb..ee7369f54a9 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -22,6 +22,13 @@
= f.label :password
= f.password_field :password, class: "form-control bottom", required: true, pattern: ".{#{@minimum_password_length},}", title: "Minimum length is #{@minimum_password_length} characters."
%p.gl-field-hint Minimum length is #{@minimum_password_length} characters
+ - if Gitlab::CurrentSettings.current_application_settings.enforce_terms?
+ .form-group
+ = check_box_tag :terms_opt_in, '1', false, required: true
+ = label_tag :terms_opt_in do
+ - terms_link = link_to s_("I accept the|Terms of Service and Privacy Policy"), terms_path, target: "_blank"
+ - accept_terms_label = _("I accept the %{terms_link}") % { terms_link: terms_link }
+ = accept_terms_label.html_safe
%div
- if Gitlab::Recaptcha.enabled?
= recaptcha_tags
diff --git a/app/views/devise/shared/_tab_single.html.haml b/app/views/devise/shared/_tab_single.html.haml
index 7bd414d64c3..5683b4207b4 100644
--- a/app/views/devise/shared/_tab_single.html.haml
+++ b/app/views/devise/shared/_tab_single.html.haml
@@ -1,3 +1,3 @@
-%ul.nav-links.new-session-tabs.single-tab
- %li.active
- %a= tab_title
+%ul.nav-links.new-session-tabs.single-tab.nav-tabs.nav
+ %li.nav-item
+ %a.nav-link.active= tab_title
diff --git a/app/views/devise/shared/_tabs_ldap.html.haml b/app/views/devise/shared/_tabs_ldap.html.haml
index f50e0724e09..58c585a29ff 100644
--- a/app/views/devise/shared/_tabs_ldap.html.haml
+++ b/app/views/devise/shared/_tabs_ldap.html.haml
@@ -1,13 +1,13 @@
-%ul.nav-links.new-session-tabs{ class: ('custom-provider-tabs' if form_based_providers.any?) }
+%ul.nav-links.new-session-tabs.nav-tabs.nav{ class: ('custom-provider-tabs' if form_based_providers.any?) }
- if crowd_enabled?
- %li.active
- = link_to "Crowd", "#crowd", 'data-toggle' => 'tab'
+ %li.nav-item
+ = link_to "Crowd", "#crowd", class: 'nav-link active', 'data-toggle' => 'tab'
- @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'
+ %li.nav-item
+ = link_to server['label'], "##{server['provider_name']}", class: "nav-link #{active_when(i.zero? && !crowd_enabled?)}", 'data-toggle' => 'tab'
- if password_authentication_enabled_for_web?
- %li
- = link_to 'Standard', '#login-pane', 'data-toggle' => 'tab'
+ %li.nav-item
+ = link_to 'Standard', '#login-pane', class: 'nav-link', 'data-toggle' => 'tab'
- if allow_signup?
- %li
- = link_to 'Register', '#register-pane', 'data-toggle' => 'tab'
+ %li.nav-item
+ = link_to 'Register', '#register-pane', class: 'nav-link', 'data-toggle' => 'tab'
diff --git a/app/views/devise/shared/_tabs_normal.html.haml b/app/views/devise/shared/_tabs_normal.html.haml
index fa3c3df7f60..284d4fa1b89 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{ role: 'tablist' }
- %li.active{ role: 'presentation' }
- %a{ href: '#login-pane', data: { toggle: 'tab' }, role: 'tab' } Sign in
+%ul.nav-links.new-session-tabs.nav-tabs.nav{ role: 'tablist' }
+ %li.nav-item{ role: 'presentation' }
+ %a.nav-link.active{ href: '#login-pane', data: { toggle: 'tab' }, role: 'tab' } Sign in
- if allow_signup?
- %li{ role: 'presentation' }
- %a{ href: '#register-pane', data: { toggle: 'tab' }, role: 'tab' } Register
+ %li.nav-item{ role: 'presentation' }
+ %a.nav-link{ href: '#register-pane', data: { toggle: 'tab' }, role: 'tab' } Register
diff --git a/app/views/discussions/_discussion.html.haml b/app/views/discussions/_discussion.html.haml
index ebe8c327079..1765251c93d 100644
--- a/app/views/discussions/_discussion.html.haml
+++ b/app/views/discussions/_discussion.html.haml
@@ -51,5 +51,5 @@
- if discussion.diff_discussion? && discussion.diff_file
= render "discussions/diff_with_notes", discussion: discussion
- else
- .panel.panel-default
+ .card
= 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 1cc227428e9..30b00ca86b3 100644
--- a/app/views/discussions/_notes.html.haml
+++ b/app/views/discussions/_notes.html.haml
@@ -11,7 +11,7 @@
- if discussion.try(:on_image?) && show_toggle
%button.diff-notes-collapse.js-diff-notes-toggle{ type: 'button' }
= sprite_icon('collapse', css_class: 'collapse-icon')
- %button.btn-transparent.badge.js-diff-notes-toggle{ type: 'button' }
+ %button.btn-transparent.badge.badge-pill.js-diff-notes-toggle{ type: 'button' }
= badge_counter
= render partial: "shared/notes/note", collection: discussion.notes, as: :note, locals: { badge_counter: badge_counter, show_image_comment_badge: show_image_comment_badge }
@@ -22,7 +22,7 @@
- if discussion.potentially_resolvable?
- line_type = local_assigns.fetch(:line_type, nil)
- .btn-group-justified.discussion-with-resolve-btn{ role: "group" }
+ .btn-group.discussion-with-resolve-btn{ role: "group" }
.btn-group{ role: "group" }
= link_to_reply_discussion(discussion, line_type)
diff --git a/app/views/doorkeeper/applications/_form.html.haml b/app/views/doorkeeper/applications/_form.html.haml
index cf0e0de1ca4..be0935b8313 100644
--- a/app/views/doorkeeper/applications/_form.html.haml
+++ b/app/views/doorkeeper/applications/_form.html.haml
@@ -9,10 +9,10 @@
= f.label :redirect_uri, class: 'label-light'
= f.text_area :redirect_uri, class: 'form-control', required: true
- %span.help-block
+ %span.form-text.text-muted
Use one line per URI
- if Doorkeeper.configuration.native_redirect_uri
- %span.help-block
+ %span.form-text.text-muted
Use
%code= Doorkeeper.configuration.native_redirect_uri
for local tests
diff --git a/app/views/doorkeeper/applications/index.html.haml b/app/views/doorkeeper/applications/index.html.haml
index d1237d7bf6f..cdf3ff81bd9 100644
--- a/app/views/doorkeeper/applications/index.html.haml
+++ b/app/views/doorkeeper/applications/index.html.haml
@@ -73,7 +73,7 @@
%tr
%td
Anonymous
- .help-block
+ .form-text.text-muted
%em Authorization was granted by entering your username and password in the application.
%td= token.created_at
%td= token.scopes
diff --git a/app/views/doorkeeper/applications/show.html.haml b/app/views/doorkeeper/applications/show.html.haml
index 6364f0be4a3..89ad626f73f 100644
--- a/app/views/doorkeeper/applications/show.html.haml
+++ b/app/views/doorkeeper/applications/show.html.haml
@@ -30,5 +30,5 @@
= render "shared/tokens/scopes_list", token: @application
.form-actions
- = link_to 'Edit', edit_oauth_application_path(@application), class: 'btn btn-primary wide pull-left'
+ = link_to 'Edit', edit_oauth_application_path(@application), class: 'btn btn-primary wide float-left'
= render 'delete_form', application: @application, submit_btn_css: 'btn btn-danger prepend-left-10'
diff --git a/app/views/doorkeeper/authorizations/new.html.haml b/app/views/doorkeeper/authorizations/new.html.haml
index 6d9c6b5572a..28cdc7607e0 100644
--- a/app/views/doorkeeper/authorizations/new.html.haml
+++ b/app/views/doorkeeper/authorizations/new.html.haml
@@ -35,7 +35,7 @@
- @pre_auth.scopes.each do |scope|
%li
%strong= t scope, scope: [:doorkeeper, :scopes]
- .scope-description= t scope, scope: [:doorkeeper, :scope_desc]
+ .text-secondary= 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/doorkeeper/authorized_applications/index.html.haml b/app/views/doorkeeper/authorized_applications/index.html.haml
index c8a585560a2..30c9d02b72e 100644
--- a/app/views/doorkeeper/authorized_applications/index.html.haml
+++ b/app/views/doorkeeper/authorized_applications/index.html.haml
@@ -1,4 +1,4 @@
-%header.page-header
+%header
%h1 Your authorized applications
%main{ :role => "main" }
.table-holder
diff --git a/app/views/email_rejection_mailer/rejection.html.haml b/app/views/email_rejection_mailer/rejection.html.haml
index 7f7d841fe21..c4ae7befe4e 100644
--- a/app/views/email_rejection_mailer/rejection.html.haml
+++ b/app/views/email_rejection_mailer/rejection.html.haml
@@ -2,3 +2,4 @@
Unfortunately, your email message to GitLab could not be processed.
= markdown @reason
+= render_if_exists 'shared/additional_email_text'
diff --git a/app/views/email_rejection_mailer/rejection.text.haml b/app/views/email_rejection_mailer/rejection.text.haml
index af518b5b583..0e13b2a6473 100644
--- a/app/views/email_rejection_mailer/rejection.text.haml
+++ b/app/views/email_rejection_mailer/rejection.text.haml
@@ -1,3 +1,4 @@
Unfortunately, your email message to GitLab could not be processed.
\
= @reason
+= render_if_exists 'shared/additional_email_text'
diff --git a/app/views/errors/_footer.html.haml b/app/views/errors/_footer.html.haml
new file mode 100644
index 00000000000..e67a3a142f6
--- /dev/null
+++ b/app/views/errors/_footer.html.haml
@@ -0,0 +1,11 @@
+%nav
+ %ul.error-nav
+ %li
+ = link_to s_('Nav|Home'), root_path
+ %li
+ - if current_user
+ = link_to s_('Nav|Sign out and sign in with a different account'), destroy_user_session_path
+ - else
+ = link_to s_('Nav|Sign In / Register'), new_session_path(:user, redirect_to_referer: 'yes')
+ %li
+ = link_to s_('Nav|Help'), help_path
diff --git a/app/views/errors/access_denied.html.haml b/app/views/errors/access_denied.html.haml
index bf540439c79..8ae29b9d337 100644
--- a/app/views/errors/access_denied.html.haml
+++ b/app/views/errors/access_denied.html.haml
@@ -1,15 +1,16 @@
-- message = local_assigns.fetch(:message)
-
+- message = local_assigns.fetch(:message, nil)
- content_for(:title, 'Access Denied')
-%img{ :alt => "GitLab Logo", :src => image_path('logo.svg') }
- %h1
- 403
+
+= image_tag('illustrations/error-403.svg', alt: '403', lazy: false)
.container
- %h3 Access Denied
- %hr
+ %h3
+ = s_("403|You don't have the permission to access this page.")
- if message
%p
= message
- - else
- %p You are not allowed to access this page.
- %p Read more about project permissions #{link_to "here", help_page_path("user/permissions"), class: "vlink"}
+ %p
+ = s_('403|Please contact your GitLab administrator to get the permission.')
+ .action-container.js-go-back{ style: 'display: none' }
+ %a{ href: 'javascript:history.back()', class: 'btn btn-success' }
+ = s_('Go Back')
+= render "errors/footer"
diff --git a/app/views/errors/not_found.html.haml b/app/views/errors/not_found.html.haml
index a0b9a632e22..ae055f398ac 100644
--- a/app/views/errors/not_found.html.haml
+++ b/app/views/errors/not_found.html.haml
@@ -1,8 +1,15 @@
- content_for(:title, 'Not Found')
-%img{ :alt => "GitLab Logo", :src => image_path('logo.svg') }
- %h1
- 404
+= image_tag('illustrations/error-404.svg', alt: '404', lazy: false)
.container
- %h3 The resource you were looking for doesn't exist.
- %hr
- %p You may have mistyped the address or the page may have moved.
+ %h3
+ = s_('404|Page Not Found')
+ %p
+ = s_("404|Make sure the address is correct and the page hasn't moved.")
+ %p
+ = s_('404|Please contact your GitLab administrator if you think this is a mistake.')
+ .action-container
+ = form_tag search_path, method: :get, class: 'form-inline-flex' do |f|
+ .field
+ = search_field_tag :search, '', placeholder: _('Search for projects, issues, etc.'), class: 'form-control'
+ = button_tag 'Search', class: 'btn btn-success', name: nil, type: 'submit'
+= render 'errors/footer'
diff --git a/app/views/events/_event_push.atom.haml b/app/views/events/_event_push.atom.haml
index e3c5fd55f08..bc1d32607e4 100644
--- a/app/views/events/_event_push.atom.haml
+++ b/app/views/events/_event_push.atom.haml
@@ -6,7 +6,7 @@
at
= event.created_at.to_s(:short)
- unless event.rm_ref?
- %blockquote= markdown(escape_once(event.commit_title), pipeline: :atom, project: event.project, author: event.author)
+ .blockquote= markdown(escape_once(event.commit_title), pipeline: :atom, project: event.project, author: event.author)
- if event.commits_count > 1
%p
%i
diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml
index f85f5c5be88..85f2d00bde3 100644
--- a/app/views/events/event/_push.html.haml
+++ b/app/views/events/event/_push.html.haml
@@ -14,7 +14,7 @@
- if event.push_with_commits?
.event-body
- %ul.well-list.event_commits
+ %ul.content-list.event_commits
= render "events/commit", project: project, event: event
- create_mr = event.new_ref? && create_mr_button?(project.default_branch, event.ref_name, project) && event.authored_by?(current_user)
diff --git a/app/views/explore/groups/_nav.html.haml b/app/views/explore/groups/_nav.html.haml
index c8d95b52156..ab4787c6d05 100644
--- a/app/views/explore/groups/_nav.html.haml
+++ b/app/views/explore/groups/_nav.html.haml
@@ -1,5 +1,5 @@
.top-area
- %ul.nav-links
+ %ul.nav-links.nav.nav-tabs
= nav_link(page: explore_groups_path) do
= link_to explore_groups_path do
Explore Groups
diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml
index efa8b2706da..0643b9cfbc5 100644
--- a/app/views/explore/groups/index.html.haml
+++ b/app/views/explore/groups/index.html.haml
@@ -9,7 +9,7 @@
= render 'nav'
- if cookies[:explore_groups_landing_dismissed] != 'true'
- .explore-groups.landing.content-block.js-explore-groups-landing.hidden
+ .explore-groups.landing.content-block.js-explore-groups-landing.hide
%button.dismiss-button{ type: 'button', 'aria-label' => 'Dismiss' }= icon('times')
.svg-container
= custom_icon('icon_explore_groups_splash')
diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml
index f630f1effdc..6abb56ba6d2 100644
--- a/app/views/explore/projects/_filter.html.haml
+++ b/app/views/explore/projects/_filter.html.haml
@@ -1,6 +1,6 @@
- if current_user
.dropdown
- %button.dropdown-toggle{ href: '#', "data-toggle" => "dropdown" }
+ %button.dropdown-toggle{ href: '#', "data-toggle" => "dropdown", 'data-display' => 'static' }
= icon('globe')
%span.light Visibility:
- if params[:visibility_level].present?
@@ -8,7 +8,7 @@
- else
Any
= icon('chevron-down')
- %ul.dropdown-menu.dropdown-menu-align-right
+ %ul.dropdown-menu.dropdown-menu-right
%li
= link_to filter_projects_path(visibility_level: nil) do
Any
diff --git a/app/views/explore/projects/_nav.html.haml b/app/views/explore/projects/_nav.html.haml
index e0a2a1e9c96..558cd26f1e0 100644
--- a/app/views/explore/projects/_nav.html.haml
+++ b/app/views/explore/projects/_nav.html.haml
@@ -1,5 +1,5 @@
.top-area
- %ul.nav-links
+ %ul.nav-links.nav.nav-tabs
= nav_link(page: [trending_explore_projects_path, explore_root_path]) do
= link_to trending_explore_projects_path do
Trending
diff --git a/app/views/groups/_activities.html.haml b/app/views/groups/_activities.html.haml
index fd6e7111f38..577c63503a8 100644
--- a/app/views/groups/_activities.html.haml
+++ b/app/views/groups/_activities.html.haml
@@ -1,4 +1,4 @@
-.nav-block
+.nav-block.activities
.controls
= link_to group_path(@group, rss_url_options), class: 'btn rss-btn has-tooltip' , title: 'Subscribe' do
%i.fa.fa-rss
diff --git a/app/views/groups/_create_chat_team.html.haml b/app/views/groups/_create_chat_team.html.haml
index 20de1b4c973..f950968030f 100644
--- a/app/views/groups/_create_chat_team.html.haml
+++ b/app/views/groups/_create_chat_team.html.haml
@@ -1,12 +1,12 @@
.form-group
- = f.label :create_chat_team, class: 'control-label' do
+ = f.label :create_chat_team, class: 'col-form-label' do
%span.mattermost-icon
= custom_icon('icon_mattermost')
Mattermost
.col-sm-10
- .checkbox.js-toggle-container
- = f.label :create_chat_team do
- .js-toggle-button= f.check_box(:create_chat_team, { checked: true }, true, false)
+ .form-check.js-toggle-container
+ .js-toggle-button.form-check-input= f.check_box(:create_chat_team, { checked: true }, true, false)
+ = f.label :create_chat_team, class: 'form-check-label' do
Create a Mattermost team for this group
%br
%small.light.js-toggle-content
diff --git a/app/views/groups/_group_admin_settings.html.haml b/app/views/groups/_group_admin_settings.html.haml
index 65e95f3aeef..f7cc62c6929 100644
--- a/app/views/groups/_group_admin_settings.html.haml
+++ b/app/views/groups/_group_admin_settings.html.haml
@@ -1,26 +1,26 @@
-.form-group
- = f.label :lfs_enabled, 'Large File Storage', class: 'control-label'
+.form-group.row
+ = f.label :lfs_enabled, 'Large File Storage', class: 'col-form-label col-sm-2'
.col-sm-10
- .checkbox
- = f.label :lfs_enabled do
- = f.check_box :lfs_enabled, checked: @group.lfs_enabled?
+ .form-check
+ = f.check_box :lfs_enabled, checked: @group.lfs_enabled?, class: 'form-check-input'
+ = f.label :lfs_enabled, class: 'form-check-label' do
%strong
Allow projects within this group to use Git LFS
= link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
%br/
%span.descr This setting can be overridden in each project.
-.form-group
- = f.label :require_two_factor_authentication, 'Two-factor authentication', class: 'control-label col-sm-2'
+.form-group.row
+ = f.label :require_two_factor_authentication, 'Two-factor authentication', class: 'col-form-label col-sm-2'
.col-sm-10
- .checkbox
- = f.label :require_two_factor_authentication do
- = f.check_box :require_two_factor_authentication
+ .form-check
+ = f.check_box :require_two_factor_authentication, class: 'form-check-input'
+ = f.label :require_two_factor_authentication, class: 'form-check-label' do
%strong
Require all users in this group to setup Two-factor authentication
= link_to icon('question-circle'), help_page_path('security/two_factor_authentication', anchor: 'enforcing-2fa-for-all-users-in-a-group')
-.form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
+.form-group.row
+ .offset-sm-2.col-sm-10
+ .form-check
= f.text_field :two_factor_grace_period, class: 'form-control'
- .help-block Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication
+ .form-text.text-muted Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index 3375e01b3a1..cae2df4699e 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -1,78 +1,39 @@
- breadcrumb_title "General Settings"
- @content_class = "limit-container-width" unless fluid_layout
-
-.panel.panel-default.prepend-top-default
- .panel-heading
- Group settings
- .panel-body
- = form_for @group, html: { multipart: true, class: "form-horizontal gl-show-field-errors" }, authenticity_token: true do |f|
- = form_errors(@group)
- = render 'shared/group_form', f: f
-
- .form-group
- .col-sm-offset-2.col-sm-10
- .avatar-container.s160
- = group_icon(@group, alt: '', class: 'avatar group-avatar s160')
- %p.light
- - if @group.avatar?
- You can change the group avatar here
- - else
- You can upload a group avatar here
- = render 'shared/choose_group_avatar_button', f: f
- - if @group.avatar?
- %hr
- = link_to _('Remove avatar'), group_avatar_path(@group.to_param), data: { confirm: _("Avatar will be removed. Are you sure?")}, method: :delete, class: "btn btn-danger btn-inverted"
-
- = render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group
-
- .form-group
- .col-sm-offset-2.col-sm-10
- = render 'shared/allow_request_access', form: f
-
- .form-group
- %label.control-label
- = s_("GroupSettings|Share with group lock")
- .col-sm-10
- .checkbox
- = f.label :share_with_group_lock do
- = f.check_box :share_with_group_lock, disabled: !can_change_share_with_group_lock?(@group)
- %strong
- - group_link = link_to @group.name, group_path(@group)
- = s_("GroupSettings|Prevent sharing a project within %{group} with other groups").html_safe % { group: group_link }
- %br
- %span.descr= share_with_group_lock_help_text(@group)
-
- = render 'group_admin_settings', f: f
-
- .form-actions
- = f.submit 'Save group', class: "btn btn-save"
-
-.panel.panel-danger
- .panel-heading Remove group
- .panel-body
- = form_tag(@group, method: :delete) do
- %p
- Removing group will cause all child projects and resources to be removed.
- %br
- %strong Removed group can not be restored!
-
- .form-actions
- = button_to 'Remove group', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_group_message(@group) }
-
-- if supports_nested_groups?
- .panel.panel-warning
- .panel-heading Transfer group
- .panel-body
- = form_for @group, url: transfer_group_path(@group), method: :put do |f|
- .form-group
- = dropdown_tag('Select parent group', options: { toggle_class: 'js-groups-dropdown', title: 'Parent Group', filter: true, dropdown_class: 'dropdown-open-top dropdown-group-transfer', placeholder: "Search groups", data: { data: parent_group_options(@group) } })
- = hidden_field_tag 'new_parent_group_id'
-
- %ul
- %li Be careful. Changing a group's parent can have unintended #{link_to 'side effects', 'https://docs.gitlab.com/ce/user/project/index.html#redirects-when-changing-repository-paths', target: 'blank'}.
- %li You can only transfer the group to a group you manage.
- %li You will need to update your local repositories to point to the new location.
- %li If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility.
- = f.submit 'Transfer group', class: "btn btn-warning"
+- expanded = Rails.env.test?
+
+
+%section.settings.gs-general.no-animate#js-general-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('General')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
+ %p
+ = _('Update your group name, description, avatar, and other general settings.')
+ .settings-content
+ = render 'groups/settings/general'
+
+%section.settings.gs-permissions.no-animate#js-permissions-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Permissions')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
+ %p
+ = _('Enable or disable certain group features and choose access levels.')
+ .settings-content
+ = render 'groups/settings/permissions'
+
+%section.settings.gs-advanced.no-animate#js-advanced-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Advanced')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
+ %p
+ = _('Perform advanced options such as changing path, transferring, or removing the group.')
+ .settings-content
+ = render 'groups/settings/advanced'
= render 'shared/confirm_modal', phrase: @group.path
diff --git a/app/views/groups/group_members/_new_group_member.html.haml b/app/views/groups/group_members/_new_group_member.html.haml
index 5b1a4630c56..aa03f8365f9 100644
--- a/app/views/groups/group_members/_new_group_member.html.haml
+++ b/app/views/groups/group_members/_new_group_member.html.haml
@@ -2,12 +2,12 @@
.row
.col-md-4.col-lg-6
= users_select_tag(:user_ids, multiple: true, class: 'input-clamp', scope: :all, email_user: true)
- .help-block.append-bottom-10
+ .form-text.text-muted.append-bottom-10
Search for members by name, username, or email, or invite new ones using their email address.
.col-md-3.col-lg-2
= select_tag :access_level, options_for_select(GroupMember.access_level_roles, @group_member.access_level), class: "form-control project-access-select"
- .help-block.append-bottom-10
+ .form-text.text-muted.append-bottom-10
= link_to "Read more", help_page_path("user/permissions"), class: "vlink"
about role permissions
@@ -15,7 +15,7 @@
.clearable-input
= text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date'
%i.clear-icon.js-clear-input
- .help-block.append-bottom-10
+ .form-text.text-muted.append-bottom-10
On this date, the member(s) will automatically lose access to this group and all of its projects.
.col-md-2
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index c8addc49117..13d584f5f1d 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -17,17 +17,18 @@
.clearfix
%h5.member.existing-title
Existing members
- .panel.panel-default
- .panel-heading.flex-project-members-panel
+ .card
+ .card-header.flex-project-members-panel
%span.flex-project-title
Members with access to
%strong= @group.name
%span.badge= @members.total_count
= form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form flex-project-members-form' do
.form-group
- = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
- %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
- = icon("search")
+ .position-relative.append-right-8
+ = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
+ %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
+ = icon("search")
- if can_manage_members
= render 'shared/members/filter_2fa_dropdown'
= render 'shared/members/sort_dropdown'
diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml
index 662db18cf86..5e1ae1dbe38 100644
--- a/app/views/groups/issues.html.haml
+++ b/app/views/groups/issues.html.haml
@@ -8,11 +8,8 @@
.top-area
= render 'shared/issuable/nav', type: :issues
.nav-controls
- = link_to safe_params.merge(rss_url_options), class: 'btn' do
- = icon('rss')
- %span.icon-label
- Subscribe
- = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", type: :issues
+ = render 'shared/issuable/feed_buttons'
+ = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", type: :issues, with_feature_enabled: 'issues'
= render 'shared/issuable/search_bar', type: :issues
diff --git a/app/views/groups/issues_calendar.ics.haml b/app/views/groups/issues_calendar.ics.haml
new file mode 100644
index 00000000000..59573e5fecf
--- /dev/null
+++ b/app/views/groups/issues_calendar.ics.haml
@@ -0,0 +1 @@
+= render 'issues/issues_calendar', issues: @issues
diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml
index ac7e12fcd0b..db7eaff6658 100644
--- a/app/views/groups/labels/index.html.haml
+++ b/app/views/groups/labels/index.html.haml
@@ -1,21 +1,32 @@
-- page_title 'Labels'
-
+- @no_container = true
+- page_title "Labels"
+- can_admin_label = can?(current_user, :admin_label, @group)
+- hide_class = ''
+- hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1')
- issuables = ['issues', 'merge requests']
-.top-area.adjust
- .nav-text
- = _("Labels can be applied to %{features}. Group labels are available for any project within the group.") % { features: issuables.to_sentence }
+- if can_admin_label
+ - content_for(:header_content) do
+ .nav-controls
+ = link_to _('New label'), new_group_label_path(@group), class: "btn btn-new"
+
+- if @labels.exists?
+ #promote-label-modal
+ %div{ class: container_class }
+ .top-area.adjust
+ .nav-text
+ = _('Labels can be applied to %{features}. Group labels are available for any project within the group.') % { features: issuables.to_sentence }
- .nav-controls
- - if can?(current_user, :admin_label, @group)
- = link_to "New label", new_group_label_path(@group), class: "btn btn-new"
+ .labels-container.prepend-top-5
+ .other-labels
+ - if can_admin_label
+ %h5{ class: ('hide' if hide) } Labels
+ %ul.content-list.manage-labels-list.js-other-labels
+ = render partial: 'shared/label', subject: @group, collection: @labels, as: :label, locals: { use_label_priority: false }
+ = paginate @labels, theme: 'gitlab'
+- else
+ = render 'shared/empty_states/labels'
-.labels
- .other-labels
- - if @labels.present?
- %ul.content-list.manage-labels-list.js-other-labels
- = render partial: 'shared/label', subject: @group, collection: @labels, as: :label
- = paginate @labels, theme: 'gitlab'
- - else
- .nothing-here-block
- = _("No labels created yet.")
+%template#js-badge-item-template
+ %li.label-link-item.js-priority-badge.inline.prepend-left-10
+ .label-badge.label-badge-blue= _('Prioritized label')
diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml
index 4ccd16f3e11..e2a317dbf67 100644
--- a/app/views/groups/merge_requests.html.haml
+++ b/app/views/groups/merge_requests.html.haml
@@ -7,7 +7,7 @@
= render 'shared/issuable/nav', type: :merge_requests
- if current_user
.nav-controls
- = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", type: :merge_requests
+ = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", type: :merge_requests, with_feature_enabled: 'merge_requests'
= render 'shared/issuable/search_bar', type: :merge_requests
diff --git a/app/views/groups/milestones/_form.html.haml b/app/views/groups/milestones/_form.html.haml
index a1be0d3220a..6d35457a0ec 100644
--- a/app/views/groups/milestones/_form.html.haml
+++ b/app/views/groups/milestones/_form.html.haml
@@ -1,14 +1,14 @@
-= form_for [@group, @milestone], html: { class: 'form-horizontal milestone-form common-note-form js-quick-submit js-requires-input' } do |f|
+= form_for [@group, @milestone], html: { class: 'milestone-form common-note-form js-quick-submit js-requires-input' } do |f|
.row
= form_errors(@milestone)
.col-md-6
- .form-group
- = f.label :title, "Title", class: "control-label"
+ .form-group.row
+ = f.label :title, "Title", class: "col-form-label col-sm-2"
.col-sm-10
= f.text_field :title, maxlength: 255, class: "form-control", required: true, autofocus: true
- .form-group.milestone-description
- = f.label :description, "Description", class: "control-label"
+ .form-group.row.milestone-description
+ = f.label :description, "Description", class: "col-form-label col-sm-2"
.col-sm-10
= 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
diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml
index e9daac95ca1..53f54db1ddf 100644
--- a/app/views/groups/new.html.haml
+++ b/app/views/groups/new.html.haml
@@ -4,27 +4,37 @@
- page_title 'New Group'
- header_title "Groups", dashboard_groups_path
-%h3.page-title
- New Group
-%hr
+.row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ = _('New group')
+ %p
+ - group_docs_path = help_page_path('user/group/index')
+ - group_docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: group_docs_path }
+ = s_('%{group_docs_link_start}Groups%{group_docs_link_end} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects.').html_safe % { group_docs_link_start: group_docs_link_start, group_docs_link_end: '</a>'.html_safe }
+ %p
+ - subgroup_docs_path = help_page_path('user/group/subgroups/index')
+ - subgroup_docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: subgroup_docs_path }
+ = s_('Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}.').html_safe % { subgroup_docs_link_start: subgroup_docs_link_start, subgroup_docs_link_end: '</a>'.html_safe }
-= form_for @group, html: { class: 'group-form form-horizontal gl-show-field-errors' } do |f|
- = form_errors(@group)
- = render 'shared/group_form', f: f, autofocus: true
+ .col-lg-9
+ = form_for @group, html: { class: 'group-form gl-show-field-errors' } do |f|
+ = form_errors(@group)
+ = render 'shared/group_form', f: f, autofocus: true
- .form-group.group-description-holder
- = f.label :avatar, "Group avatar", class: 'control-label'
- .col-sm-10
- = render 'shared/choose_group_avatar_button', f: f
+ .form-group.row.group-description-holder
+ = f.label :avatar, "Group avatar", class: 'col-form-label col-sm-2'
+ .col-sm-10
+ = render 'shared/choose_group_avatar_button', f: f
- = render 'shared/visibility_level', f: f, visibility_level: default_group_visibility, can_change_visibility_level: true, form_model: @group
+ = render 'shared/visibility_level', f: f, visibility_level: default_group_visibility, can_change_visibility_level: true, form_model: @group
- = render 'create_chat_team', f: f if Gitlab.config.mattermost.enabled
+ = render 'create_chat_team', f: f if Gitlab.config.mattermost.enabled
- .form-group
- .col-sm-offset-2.col-sm-10
- = render 'shared/group_tips'
+ .form-group.row
+ .offset-sm-2.col-sm-10
+ = render 'shared/group_tips'
- .form-actions
- = f.submit 'Create group', class: "btn btn-create"
- = link_to 'Cancel', dashboard_groups_path, class: 'btn btn-cancel'
+ .form-actions
+ = f.submit 'Create group', class: "btn btn-create"
+ = link_to 'Cancel', dashboard_groups_path, class: 'btn btn-cancel'
diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml
index ef181b425bc..ba186875a86 100644
--- a/app/views/groups/projects.html.haml
+++ b/app/views/groups/projects.html.haml
@@ -1,24 +1,24 @@
- breadcrumb_title "Projects"
-.panel.panel-default.prepend-top-default
- .panel-heading
+.card.prepend-top-default
+ .card-header
%strong= @group.name
projects:
- if can? current_user, :admin_group, @group
.controls
= link_to new_project_path(namespace_id: @group.id), class: "btn btn-sm btn-success" do
New project
- %ul.well-list
+ %ul.content-list
- @projects.each do |project|
%li
.list-item-name
%span{ class: visibility_level_color(project.visibility_level) }
= visibility_level_icon(project.visibility_level)
%strong= link_to project.full_name, project
- .pull-right
+ .float-right
- if project.archived
- %span.label.label-warning archived
- %span.badge
+ %span.badge.badge-warning archived
+ %span.badge.badge-pill
= storage_counter(project.statistics.storage_size)
= link_to 'Members', project_project_members_path(project), id: "edit_#{dom_id(project)}", class: "btn btn-sm"
= link_to 'Edit', edit_project_path(project), id: "edit_#{dom_id(project)}", class: "btn btn-sm"
diff --git a/app/views/groups/runners/_runner.html.haml b/app/views/groups/runners/_runner.html.haml
index 76650a961d6..3f89b04a5fc 100644
--- a/app/views/groups/runners/_runner.html.haml
+++ b/app/views/groups/runners/_runner.html.haml
@@ -8,13 +8,13 @@
= link_to edit_group_runner_path(@group, runner) do
= icon('edit')
- .pull-right
+ .float-right
- if runner.active?
= link_to _('Pause'), pause_group_runner_path(@group, runner), method: :post, class: 'btn btn-sm btn-danger', data: { confirm: _("Are you sure?") }
- else
= link_to _('Resume'), resume_group_runner_path(@group, runner), method: :post, class: 'btn btn-success btn-sm'
= link_to _('Remove Runner'), group_runner_path(@group, runner), data: { confirm: _("Are you sure?") }, method: :delete, class: 'btn btn-danger btn-sm'
- .pull-right
+ .float-right
%small.light
\##{runner.id}
- if runner.description.present?
diff --git a/app/views/groups/settings/_advanced.html.haml b/app/views/groups/settings/_advanced.html.haml
new file mode 100644
index 00000000000..b7c673db705
--- /dev/null
+++ b/app/views/groups/settings/_advanced.html.haml
@@ -0,0 +1,49 @@
+.sub-section
+ %h4.warning-title Change group path
+ = form_for @group, html: { multipart: true, class: 'gl-show-field-errors' }, authenticity_token: true do |f|
+ = form_errors(@group)
+ .form-group
+ %p
+ Changing group path can have unintended side effects.
+ = succeed '.' do
+ = link_to 'Learn more', help_page_path('user/group/index', anchor: 'changing-a-groups-path'), target: '_blank'
+
+ .input-group.gl-field-error-anchor
+ .group-root-path.input-group-prepend.has-tooltip{ title: group_path(@group), :'data-placement' => 'bottom' }
+ .input-group-text
+ %span>= root_url
+ - if parent
+ %strong= parent.full_path + '/'
+ = f.hidden_field :parent_id
+ = f.text_field :path, placeholder: 'open-source', class: 'form-control',
+ autofocus: local_assigns[:autofocus] || false, required: true,
+ pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS,
+ title: 'Please choose a group path with no special characters.',
+ "data-bind-in" => "#{'create_chat_team' if Gitlab.config.mattermost.enabled}"
+
+ = f.submit 'Change group path', class: 'btn btn-warning'
+
+.sub-section
+ %h4.danger-title Remove group
+ = form_tag(@group, method: :delete) do
+ %p
+ Removing group will cause all child projects and resources to be removed.
+ %br
+ %strong Removed group can not be restored!
+
+ = button_to 'Remove group', '#', class: 'btn btn-remove js-confirm-danger', data: { 'confirm-danger-message' => remove_group_message(@group) }
+
+- if supports_nested_groups?
+ .sub-section
+ %h4.warning-title Transfer group
+ = form_for @group, url: transfer_group_path(@group), method: :put do |f|
+ .form-group
+ = dropdown_tag('Select parent group', options: { toggle_class: 'js-groups-dropdown', title: 'Parent Group', filter: true, dropdown_class: 'dropdown-open-top dropdown-group-transfer', placeholder: 'Search groups', data: { data: parent_group_options(@group) } })
+ = hidden_field_tag 'new_parent_group_id'
+
+ %ul
+ %li Be careful. Changing a group's parent can have unintended #{link_to 'side effects', 'https://docs.gitlab.com/ce/user/project/index.html#redirects-when-changing-repository-paths', target: 'blank'}.
+ %li You can only transfer the group to a group you manage.
+ %li You will need to update your local repositories to point to the new location.
+ %li If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility.
+ = f.submit 'Transfer group', class: 'btn btn-warning'
diff --git a/app/views/groups/settings/_general.html.haml b/app/views/groups/settings/_general.html.haml
new file mode 100644
index 00000000000..64786d24266
--- /dev/null
+++ b/app/views/groups/settings/_general.html.haml
@@ -0,0 +1,38 @@
+= form_for @group, html: { multipart: true, class: 'gl-show-field-errors' }, authenticity_token: true do |f|
+ = form_errors(@group)
+
+ %fieldset
+ .row
+ .form-group.col-md-9
+ = f.label :name, class: 'label-light' do
+ Group name
+ = f.text_field :name, class: 'form-control'
+
+ .form-group.col-md-3
+ = f.label :id, class: 'label-light' do
+ Group ID
+ = f.text_field :id, class: 'form-control', readonly: true
+
+ .form-group
+ = f.label :description, class: 'label-light' do
+ Group description
+ %span.light (optional)
+ = f.text_area :description, class: 'form-control', rows: 3, maxlength: 250
+
+ = render_if_exists 'shared/repository_size_limit_setting', form: f, type: :group
+
+ .form-group.row
+ .col-sm-12
+ .avatar-container.s160
+ = group_icon(@group, alt: '', class: 'avatar group-avatar s160')
+ %p.light
+ - if @group.avatar?
+ You can change the group avatar here
+ - else
+ You can upload a group avatar here
+ = render 'shared/choose_group_avatar_button', f: f
+ - if @group.avatar?
+ %hr
+ = link_to _('Remove avatar'), group_avatar_path(@group.to_param), data: { confirm: _('Avatar will be removed. Are you sure?')}, method: :delete, class: 'btn btn-danger btn-inverted'
+
+ = f.submit 'Save group', class: 'btn btn-success'
diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml
new file mode 100644
index 00000000000..f1f67af1d1e
--- /dev/null
+++ b/app/views/groups/settings/_permissions.html.haml
@@ -0,0 +1,28 @@
+= form_for @group, html: { multipart: true, class: 'gl-show-field-errors' }, authenticity_token: true do |f|
+ = form_errors(@group)
+
+ %fieldset
+ = render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group
+
+ .form-group.row
+ .offset-sm-2.col-sm-10
+ = render 'shared/allow_request_access', form: f
+
+ .form-group.row
+ %label.col-form-label.col-sm-2
+ = s_('GroupSettings|Share with group lock')
+ .col-sm-10
+ .form-check
+ = f.check_box :share_with_group_lock, disabled: !can_change_share_with_group_lock?(@group), class: 'form-check-input'
+ = f.label :share_with_group_lock, class: 'form-check-label' do
+ %strong
+ - group_link = link_to @group.name, group_path(@group)
+ = s_('GroupSettings|Prevent sharing a project within %{group} with other groups').html_safe % { group: group_link }
+ %br
+ %span.descr= share_with_group_lock_help_text(@group)
+
+ = render 'groups/group_admin_settings', f: f
+
+ = render_if_exists 'groups/member_lock_setting', f: f, group: @group
+
+ = f.submit 'Save group', class: 'btn btn-success'
diff --git a/app/views/groups/settings/ci_cd/show.html.haml b/app/views/groups/settings/ci_cd/show.html.haml
index 082e1b7befa..647948c7dff 100644
--- a/app/views/groups/settings/ci_cd/show.html.haml
+++ b/app/views/groups/settings/ci_cd/show.html.haml
@@ -6,8 +6,8 @@
%section.settings#secret-variables.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
- = _('Secret variables')
- = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank', rel: 'noopener noreferrer'
+ = _('Variables')
+ = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'variables'), target: '_blank', rel: 'noopener noreferrer'
%button.btn.btn-default.js-settings-toggle{ type: "button" }
= expanded ? _('Collapse') : _('Expand')
%p.append-bottom-0
@@ -18,7 +18,7 @@
%section.settings#runners-settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
- = _('Runners settings')
+ = _('Runners')
%button.btn.btn-default.js-settings-toggle{ type: "button" }
= expanded ? _('Collapse') : _('Expand')
%p
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index 8e1dea4afc1..5a88619f769 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -18,9 +18,9 @@
- 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" } }
+ %button.btn.btn-success.dropdown-toggle.js-dropdown-toggle{ type: "button", data: { "dropdown-trigger" => "#new-project-or-subgroup-dropdown", 'display' => 'static' } }
= icon("caret-down", class: "dropdown-btn-icon")
- %ul#new-project-or-subgroup-dropdown.dropdown-menu.dropdown-menu-align-right{ data: { dropdown: true } }
+ %ul#new-project-or-subgroup-dropdown.dropdown-menu.dropdown-menu-right{ data: { dropdown: true } }
%li.droplab-item-selected{ role: "button", data: { value: "new-project", text: new_project_label } }
.menu-item
.icon-container
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index 9a3a03a7671..37b56f92030 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -2,11 +2,12 @@
.modal-dialog.modal-lg
.modal-content
.modal-header
- %a.close{ href: "#", "data-dismiss" => "modal" } ×
- %h4
+ %h4.modal-title
Keyboard Shortcuts
%small
= link_to '(Show all)', '#', class: 'js-more-help-button'
+ %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
+ %span{ "aria-hidden": true } &times;
.modal-body
.row
.col-lg-4
@@ -17,71 +18,71 @@
%th Global Shortcuts
%tr
%td.shortcut
- .key s
+ %kbd s
%td Focus Search
%tr
%td.shortcut
- .key f
+ %kbd f
%td Focus Filter
- if performance_bar_enabled?
%tr
%td.shortcut
- .key p b
+ %kbd p b
%td Show/hide the Performance Bar
%tr
%td.shortcut
- .key ?
+ %kbd ?
%td Show/hide this dialog
%tr
%td.shortcut
- if browser.platform.mac?
- .key &#8984; shift p
+ %kbd &#8984; shift p
- else
- .key ctrl shift p
+ %kbd ctrl shift p
%td Toggle Markdown preview
%tr
%td.shortcut
- .key
+ %kbd
%i.fa.fa-arrow-up
%td Edit last comment (when focused on an empty textarea)
%tr
%td.shortcut
- .key shift t
+ %kbd shift t
%td
Go to todos
%tr
%td.shortcut
- .key shift a
+ %kbd shift a
%td
Go to the activity feed
%tr
%td.shortcut
- .key shift p
+ %kbd shift p
%td
Go to projects
%tr
%td.shortcut
- .key shift i
+ %kbd shift i
%td
Go to issues
%tr
%td.shortcut
- .key shift m
+ %kbd shift m
%td
Go to merge requests
%tr
%td.shortcut
- .key shift g
+ %kbd shift g
%td
Go to groups
%tr
%td.shortcut
- .key shift l
+ %kbd shift l
%td
Go to milestones
%tr
%td.shortcut
- .key shift s
+ %kbd shift s
%td
Go to snippets
%tbody
@@ -90,21 +91,21 @@
%th Finding Project File
%tr
%td.shortcut
- .key
+ %kbd
%i.fa.fa-arrow-up
%td Move selection up
%tr
%td.shortcut
- .key
+ %kbd
%i.fa.fa-arrow-down
%td Move selection down
%tr
%td.shortcut
- .key enter
+ %kbd enter
%td Open Selection
%tr
%td.shortcut
- .key esc
+ %kbd esc
%td Go back
.col-lg-4
%table.shortcut-mappings
@@ -114,95 +115,101 @@
%th Project
%tr
%td.shortcut
- .key g
- .key p
+ %kbd g
+ %kbd p
%td
Go to the project's overview page
%tr
%td.shortcut
- .key g
- .key v
+ %kbd g
+ %kbd v
%td
Go to the project's activity feed
%tr
%td.shortcut
- .key g
- .key f
+ %kbd g
+ %kbd f
%td
Go to files
%tr
%td.shortcut
- .key g
- .key c
+ %kbd g
+ %kbd c
%td
Go to commits
%tr
%td.shortcut
- .key g
- .key j
+ %kbd g
+ %kbd j
%td
Go to jobs
%tr
%td.shortcut
- .key g
- .key n
+ %kbd g
+ %kbd n
%td
Go to network graph
%tr
%td.shortcut
- .key g
- .key d
+ %kbd g
+ %kbd d
%td
Go to repository charts
%tr
%td.shortcut
- .key g
- .key i
+ %kbd g
+ %kbd i
%td
Go to issues
%tr
%td.shortcut
- .key g
- .key b
+ %kbd g
+ %kbd b
%td
Go to issue boards
%tr
%td.shortcut
- .key g
- .key m
+ %kbd g
+ %kbd m
%td
Go to merge requests
%tr
%td.shortcut
- .key g
- .key e
+ %kbd g
+ %kbd e
%td
Go to environments
%tr
%td.shortcut
- .key g
- .key k
+ %kbd g
+ %kbd l
+ %td
+ Go to metrics
+ %tr
+ %td.shortcut
+ %kbd g
+ %kbd k
%td
Go to kubernetes
%tr
%td.shortcut
- .key g
- .key s
+ %kbd g
+ %kbd s
%td
Go to snippets
%tr
%td.shortcut
- .key g
- .key w
+ %kbd g
+ %kbd w
%td
Go to wiki
%tr
%td.shortcut
- .key t
+ %kbd t
%td Go to finding file
%tr
%td.shortcut
- .key i
+ %kbd i
%td New issue
%tbody
@@ -211,17 +218,17 @@
%th Project Files browsing
%tr
%td.shortcut
- .key
+ %kbd
%i.fa.fa-arrow-up
%td Move selection up
%tr
%td.shortcut
- .key
+ %kbd
%i.fa.fa-arrow-down
%td Move selection down
%tr
%td.shortcut
- .key enter
+ %kbd enter
%td Open Selection
%tbody
%tr
@@ -229,7 +236,7 @@
%th Project File
%tr
%td.shortcut
- .key y
+ %kbd y
%td Go to file permalink
%tbody
%tr
@@ -238,115 +245,115 @@
%tr
%td.shortcut
- if browser.platform.mac?
- .key &#8984; p
+ %kbd &#8984; p
- else
- .key ctrl p
+ %kbd ctrl p
%td Go to file
.col-lg-4
%table.shortcut-mappings
- %tbody.hidden-shortcut.network{ style: 'display:none' }
+ %tbody.hidden-shortcut{ style: 'display:none' }
%tr
%th
%th Network Graph
%tr
%td.shortcut
- .key
+ %kbd
%i.fa.fa-arrow-left
\/
- .key h
+ %kbd h
%td Scroll left
%tr
%td.shortcut
- .key
+ %kbd
%i.fa.fa-arrow-right
\/
- .key l
+ %kbd l
%td Scroll right
%tr
%td.shortcut
- .key
+ %kbd
%i.fa.fa-arrow-up
\/
- .key k
+ %kbd k
%td Scroll up
%tr
%td.shortcut
- .key
+ %kbd
%i.fa.fa-arrow-down
\/
- .key j
+ %kbd j
%td Scroll down
%tr
%td.shortcut
- .key
+ %kbd
shift
%i.fa.fa-arrow-up
\/
- .key
+ %kbd
shift k
%td Scroll to top
%tr
%td.shortcut
- .key
+ %kbd
shift
%i.fa.fa-arrow-down
\/
- .key
+ %kbd
shift j
%td Scroll to bottom
- %tbody.hidden-shortcut.issues{ style: 'display:none' }
+ %tbody.hidden-shortcut{ style: 'display:none' }
%tr
%th
%th Issues
%tr
%td.shortcut
- .key a
+ %kbd a
%td Change assignee
%tr
%td.shortcut
- .key m
+ %kbd m
%td Change milestone
%tr
%td.shortcut
- .key r
+ %kbd r
%td Reply (quoting selected text)
%tr
%td.shortcut
- .key e
+ %kbd e
%td Edit issue
%tr
%td.shortcut
- .key l
+ %kbd l
%td Change Label
- %tbody.hidden-shortcut.merge_requests{ style: 'display:none' }
+ %tbody.hidden-shortcut{ style: 'display:none' }
%tr
%th
%th Merge Requests
%tr
%td.shortcut
- .key a
+ %kbd a
%td Change assignee
%tr
%td.shortcut
- .key m
+ %kbd m
%td Change milestone
%tr
%td.shortcut
- .key r
+ %kbd r
%td Reply (quoting selected text)
%tr
%td.shortcut
- .key e
+ %kbd e
%td Edit merge request
%tr
%td.shortcut
- .key l
+ %kbd l
%td Change Label
- %tbody.hidden-shortcut.wiki{ style: 'display:none' }
+ %tbody.hidden-shortcut{ style: 'display:none' }
%tr
%th
%th Wiki pages
%tr
%td.shortcut
- .key e
+ %kbd e
%td Edit wiki page
diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml
index bf2725dc328..7a66bac09cb 100644
--- a/app/views/help/index.html.haml
+++ b/app/views/help/index.html.haml
@@ -8,7 +8,7 @@
Community Edition
- if user_signed_in?
%span= Gitlab::VERSION
- %small= link_to Gitlab::REVISION, Gitlab::COM_URL + namespace_project_commits_path('gitlab-org', 'gitlab-ce', Gitlab::REVISION)
+ %small= link_to Gitlab.revision, Gitlab::COM_URL + namespace_project_commits_path('gitlab-org', 'gitlab-ce', Gitlab.revision)
= version_status_badge
%hr
@@ -33,10 +33,10 @@
.documentation-index.wiki
= markdown(@help_index)
.col-md-4
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
Quick help
- %ul.well-list
+ %ul.content-list
%li= link_to 'See our website for getting help', support_url
%li
%button.btn-blank.btn-link.js-trigger-search-bar{ type: 'button' }
diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml
index 7908a04c2eb..b32b602ceb3 100644
--- a/app/views/help/ui.html.haml
+++ b/app/views/help/ui.html.haml
@@ -116,9 +116,9 @@
.lead
List with hover effect
- %code .well-list
+ %code .hover-list
.example
- %ul.well-list
+ %ul.hover-list
%li
One item
%li
@@ -129,9 +129,9 @@
.lead
List inside panel
.example
- .panel.panel-default
- .panel-heading Your list
- %ul.well-list
+ .card
+ .card-header Your list
+ %ul.content-list
%li
One item
%li
@@ -174,7 +174,7 @@
.example
.top-area
- %ul.nav-links
+ %ul.nav-links.nav.nav-tabs
%li.active
%a Open
%li
@@ -205,7 +205,7 @@
%h2#buttons Buttons
.example
- %button.btn.btn-default{ :type => "button" } Default
+ %button.btn.btn-default{ :type => "button" } Secondary
%button.btn.btn-primary{ :type => "button" } Primary
%button.btn.btn-success{ :type => "button" } Success
%button.btn.btn-info{ :type => "button" } Info
@@ -217,7 +217,7 @@
.example
.clearfix
- .dropdown.inline.pull-left
+ .dropdown.inline.float-left
%button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } }
Dropdown
= icon('chevron-down')
@@ -225,11 +225,11 @@
%li
%a{ href: "#" }
Dropdown option
- .dropdown.inline.pull-right
+ .dropdown.inline.float-right
%button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } }
Dropdown
= icon('chevron-down')
- %ul.dropdown-menu.dropdown-menu-align-right
+ %ul.dropdown-menu.dropdown-menu-right
%li
%a{ href: "#" }
Dropdown option
@@ -415,26 +415,26 @@
.row
.col-md-6
- .panel.panel-success
- .panel-heading Success
- .panel-body
+ .card.bg-success
+ .card-header Success
+ .card-body
= lorem
- .panel.panel-primary
- .panel-heading Primary
- .panel-body
+ .card.bg-primary
+ .card-header Primary
+ .card-body
= lorem
- .panel.panel-info
- .panel-heading Info
- .panel-body
+ .card.bg-info
+ .card-header Info
+ .card-body
= lorem
.col-md-6
- .panel.panel-warning
- .panel-heading Warning
- .panel-body
+ .card.bg-warning
+ .card-header Warning
+ .card-body
= lorem
- .panel.panel-danger
- .panel-heading Danger
- .panel-body
+ .card.bg-danger
+ .card-header Danger
+ .card-body
= lorem
%h2#alerts Alerts
@@ -443,8 +443,6 @@
.col-md-6
.alert.alert-success
= lorem
- .alert.alert-primary
- = lorem
.alert.alert-info
= lorem
.col-md-6
@@ -460,23 +458,23 @@
%code form.horizontal-form
.example
- %form.form-horizontal
- .form-group
- %label.col-sm-2.control-label{ :for => "inputEmail3" } Email
+ %form
+ .form-group.row
+ %label.col-sm-2.col-form-label{ :for => "inputEmail3" } Email
.col-sm-10
%input#inputEmail3.form-control{ :placeholder => "Email", :type => "email" }/
- .form-group
- %label.col-sm-2.control-label{ :for => "inputPassword3" } Password
+ .form-group.row
+ %label.col-sm-2.col-form-label{ :for => "inputPassword3" } Password
.col-sm-10
%input#inputPassword3.form-control{ :placeholder => "Password", :type => "password" }/
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- %label
- %input{ :type => "checkbox" }/
+ .form-group.row
+ .offset-sm-2.col-sm-10
+ .form-check
+ %input.form-check-input{ :type => "checkbox" }/
+ %label.form-check-label
Remember me
- .form-group
- .col-sm-offset-2.col-sm-10
+ .form-group.row
+ .offset-sm-2.col-sm-10
%button.btn.btn-default{ :type => "submit" } Sign in
.lead
@@ -491,9 +489,9 @@
.form-group
%label{ :for => "exampleInputPassword1" } Password
%input#exampleInputPassword1.form-control{ :placeholder => "Password", :type => "password" }/
- .checkbox
- %label
- %input{ :type => "checkbox" }/
+ .form-check
+ %input.form-check-input{ :type => "checkbox" }/
+ %label.form-check-label
Remember me
%button.btn.btn-default{ :type => "submit" } Sign in
diff --git a/app/views/ide/index.html.haml b/app/views/ide/index.html.haml
index da9331b45dd..9f8b0acd763 100644
--- a/app/views/ide/index.html.haml
+++ b/app/views/ide/index.html.haml
@@ -3,7 +3,9 @@
#ide.ide-loading{ data: {"empty-state-svg-path" => image_path('illustrations/multi_file_editor_empty.svg'),
"no-changes-state-svg-path" => image_path('illustrations/multi-editor_no_changes_empty.svg'),
- "committed-state-svg-path" => image_path('illustrations/multi-editor_all_changes_committed_empty.svg') } }
+ "committed-state-svg-path" => image_path('illustrations/multi-editor_all_changes_committed_empty.svg'),
+ "pipelines-empty-state-svg-path": image_path('illustrations/pipelines_empty.svg'),
+ "ci-help-page-path" => help_page_path('ci/quick_start/README'), } }
.text-center
= icon('spinner spin 2x')
%h2.clgray= _('Loading the GitLab IDE...')
diff --git a/app/views/import/_githubish_status.html.haml b/app/views/import/_githubish_status.html.haml
index 638c8b5a672..5e7be5cd37b 100644
--- a/app/views/import/_githubish_status.html.haml
+++ b/app/views/import/_githubish_status.html.haml
@@ -40,20 +40,21 @@
= project.human_import_status_name
- @repos.each do |repo|
- %tr{ id: "repo_#{repo.id}" }
+ %tr{ id: "repo_#{repo.id}", data: { qa: { repo_path: repo.full_name } } }
%td
= provider_project_link(provider, repo.full_name)
%td.import-target
%fieldset.row
.input-group
- .project-path.input-group-btn
+ .project-path.input-group-prepend
- if current_user.can_select_namespace?
- selected = params[:namespace_id] || :current_user
- opts = current_user.can_create_group? ? { extra_group: Group.new(name: repo.owner.login, path: repo.owner.login) } : {}
- = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'select2 js-select-namespace', tabindex: 1 }
+ = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'input-group-text select2 js-select-namespace qa-project-namespace-select', tabindex: 1 }
- else
- = text_field_tag :path, current_user.namespace_path, class: "input-large form-control", tabindex: 1, disabled: true
- %span.input-group-addon /
+ = text_field_tag :path, current_user.namespace_path, class: "input-group-text input-large form-control", tabindex: 1, disabled: true
+ %span.input-group-prepend
+ .input-group-text /
= text_field_tag :path, repo.name, class: "input-mini form-control", tabindex: 2, autofocus: true, required: true
%td.import-actions.job-status
= button_tag class: "btn btn-import js-add-to-import" do
diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml
index 9589e0956f4..4e8f715db4f 100644
--- a/app/views/import/bitbucket/status.html.haml
+++ b/app/views/import/bitbucket/status.html.haml
@@ -54,14 +54,15 @@
%td.import-target
%fieldset.row
.input-group
- .project-path.input-group-btn
+ .project-path.input-group-prepend
- if current_user.can_select_namespace?
- selected = params[:namespace_id] || :current_user
- opts = current_user.can_create_group? ? { extra_group: Group.new(name: repo.owner, path: repo.owner) } : {}
- = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'select2 js-select-namespace', tabindex: 1 }
+ = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'input-group-text select2 js-select-namespace', tabindex: 1 }
- else
- = text_field_tag :path, current_user.namespace_path, class: "input-large form-control", tabindex: 1, disabled: true
- %span.input-group-addon /
+ = text_field_tag :path, current_user.namespace_path, class: "input-group-text input-large form-control", tabindex: 1, disabled: true
+ %span.input-group-prepend
+ .input-group-text /
= text_field_tag :path, repo.name, class: "input-mini form-control", tabindex: 2, autofocus: true, required: true
%td.import-actions.job-status
= button_tag class: 'btn btn-import js-add-to-import' do
@@ -73,7 +74,7 @@
= link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: '_blank', rel: 'noopener noreferrer'
%td.import-target
%td.import-actions-job-status
- = label_tag 'Incompatible Project', nil, class: 'label label-danger'
+ = label_tag 'Incompatible Project', nil, class: 'label badge-danger'
- if @incompatible_repos.any?
%p
diff --git a/app/views/import/fogbugz/new.html.haml b/app/views/import/fogbugz/new.html.haml
index 5515fad6f48..74d686b6703 100644
--- a/app/views/import/fogbugz/new.html.haml
+++ b/app/views/import/fogbugz/new.html.haml
@@ -5,22 +5,22 @@
Import projects from FogBugz
%hr
-= form_tag callback_import_fogbugz_path, class: 'form-horizontal' do
+= form_tag callback_import_fogbugz_path do
%p
To get started you enter your FogBugz URL and login information below.
In the next steps, you'll be able to map users and select the projects
you want to import.
- .form-group
- = label_tag :uri, 'FogBugz URL', class: 'control-label'
- .col-sm-4
+ .form-group.row
+ = label_tag :uri, 'FogBugz URL', class: 'col-form-label col-md-2'
+ .col-md-4
= text_field_tag :uri, nil, placeholder: 'https://mycompany.fogbugz.com', class: 'form-control'
- .form-group
- = label_tag :email, 'FogBugz Email', class: 'control-label'
- .col-sm-4
+ .form-group.row
+ = label_tag :email, 'FogBugz Email', class: 'col-form-label col-md-2'
+ .col-md-4
= text_field_tag :email, nil, class: 'form-control'
- .form-group
- = label_tag :password, 'FogBugz Password', class: 'control-label'
- .col-sm-4
+ .form-group.row
+ = label_tag :password, 'FogBugz Password', class: 'col-form-label col-md-2'
+ .col-md-4
= password_field_tag :password, nil, class: 'form-control'
.form-actions
= submit_tag 'Continue to the next step', class: 'btn btn-create'
diff --git a/app/views/import/fogbugz/new_user_map.html.haml b/app/views/import/fogbugz/new_user_map.html.haml
index 84e0009487f..d27c5d3c36d 100644
--- a/app/views/import/fogbugz/new_user_map.html.haml
+++ b/app/views/import/fogbugz/new_user_map.html.haml
@@ -5,7 +5,7 @@
Import projects from FogBugz
%hr
-= form_tag create_user_map_import_fogbugz_path, class: 'form-horizontal' do
+= form_tag create_user_map_import_fogbugz_path do
%p
Customize how FogBugz email addresses and usernames are imported into GitLab.
In the next step, you'll be able to select the projects you want to import.
diff --git a/app/views/import/gitea/new.html.haml b/app/views/import/gitea/new.html.haml
index 02a116f996b..581576a8a3d 100644
--- a/app/views/import/gitea/new.html.haml
+++ b/app/views/import/gitea/new.html.haml
@@ -10,13 +10,13 @@
= succeed '.' do
= link_to 'Personal Access Token', 'https://github.com/gogits/go-gogs-client/wiki#access-token'
-= form_tag personal_access_token_import_gitea_path, class: 'form-horizontal' do
- .form-group
- = label_tag :gitea_host_url, 'Gitea Host URL', class: 'control-label'
+= form_tag personal_access_token_import_gitea_path do
+ .form-group.row
+ = label_tag :gitea_host_url, 'Gitea Host URL', class: 'col-form-label col-sm-2'
.col-sm-4
= text_field_tag :gitea_host_url, nil, placeholder: 'https://try.gitea.io', class: 'form-control'
- .form-group
- = label_tag :personal_access_token, 'Personal Access Token', class: 'control-label'
+ .form-group.row
+ = label_tag :personal_access_token, 'Personal Access Token', class: 'col-form-label col-sm-2'
.col-sm-4
= text_field_tag :personal_access_token, nil, class: 'form-control'
.form-actions
diff --git a/app/views/import/github/new.html.haml b/app/views/import/github/new.html.haml
index c63cf2b31cb..b9ebb1a39d9 100644
--- a/app/views/import/github/new.html.haml
+++ b/app/views/import/github/new.html.haml
@@ -19,7 +19,7 @@
= form_tag personal_access_token_import_github_path, method: :post, class: 'form-inline' do
.form-group
- = text_field_tag :personal_access_token, '', class: 'form-control', placeholder: _('Personal Access Token'), size: 40
+ = text_field_tag :personal_access_token, '', class: 'form-control append-right-8', placeholder: _('Personal Access Token'), size: 40
= submit_tag _('List your GitHub repositories'), class: 'btn btn-success'
- unless github_import_configured?
diff --git a/app/views/import/gitlab_projects/new.html.haml b/app/views/import/gitlab_projects/new.html.haml
index dec85368d10..cc672a5ea7c 100644
--- a/app/views/import/gitlab_projects/new.html.haml
+++ b/app/views/import/gitlab_projects/new.html.haml
@@ -8,20 +8,22 @@
= form_tag import_gitlab_project_path, class: 'new_project', multipart: true do
.row
- .form-group.col-xs-12.col-sm-6
+ .form-group.col-12.col-sm-6
= label_tag :namespace_id, 'Project path', class: 'label-light'
.form-group
.input-group
- if current_user.can_select_namespace?
- .input-group-addon.has-tooltip{ title: root_url }
- = root_url
+ .input-group-prepend.has-tooltip{ title: root_url }
+ .input-group-text
+ = root_url
= select_tag :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.has-tooltip{ title: user_url(current_user.username) + '/' }
- #{user_url(current_user.username)}/
+ .input-group-prepend.static-namespace.has-tooltip{ title: user_url(current_user.username) + '/' }
+ .input-group-text.border-0
+ #{user_url(current_user.username)}/
= hidden_field_tag :namespace_id, value: current_user.namespace_id
- .form-group.col-xs-12.col-sm-6.project-path
+ .form-group.col-12.col-sm-6.project-path
= label_tag :path, 'Project name', class: 'label-light'
= text_field_tag :path, @path, placeholder: "my-awesome-project", class: "js-path-name form-control", tabindex: 2, autofocus: true, required: true
@@ -35,6 +37,6 @@
.form-group
= file_field_tag :file, class: ''
.row
- .form-actions
+ .form-actions.col-sm-12
= submit_tag 'Import project', class: 'btn btn-create'
= link_to 'Cancel', new_project_path, class: 'btn btn-cancel'
diff --git a/app/views/import/google_code/new.html.haml b/app/views/import/google_code/new.html.haml
index c5800a1cca0..2f1fb8d9c56 100644
--- a/app/views/import/google_code/new.html.haml
+++ b/app/views/import/google_code/new.html.haml
@@ -5,7 +5,7 @@
Import projects from Google Code
%hr
-= form_tag callback_import_google_code_path, class: 'form-horizontal', multipart: true do
+= form_tag callback_import_google_code_path, multipart: true do
%p
Follow the steps below to export your Google Code project data.
In the next step, you'll be able to select the projects you want to import.
diff --git a/app/views/import/google_code/new_user_map.html.haml b/app/views/import/google_code/new_user_map.html.haml
index 0738b3db1eb..91c774f575c 100644
--- a/app/views/import/google_code/new_user_map.html.haml
+++ b/app/views/import/google_code/new_user_map.html.haml
@@ -5,7 +5,7 @@
Import projects from Google Code
%hr
-= form_tag create_user_map_import_google_code_path, class: 'form-horizontal' do
+= form_tag create_user_map_import_google_code_path do
%p
Customize how Google Code email addresses and usernames are imported into GitLab.
In the next step, you'll be able to select the projects you want to import.
@@ -36,7 +36,7 @@
will add "By <a href="#">johnsmith@example.com</a>" to all issues and comments originally created by johnsmith@example.com.
By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address.
- .form-group
+ .form-group.row
.col-sm-12
= text_area_tag :user_map, JSON.pretty_generate(@user_map), class: 'form-control', rows: 15
diff --git a/app/views/import/google_code/status.html.haml b/app/views/import/google_code/status.html.haml
index bc61aeece72..acf7a108cb0 100644
--- a/app/views/import/google_code/status.html.haml
+++ b/app/views/import/google_code/status.html.haml
@@ -66,7 +66,7 @@
= link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank", rel: 'noopener noreferrer'
%td.import-target
%td.import-actions-job-status
- = label_tag "Incompatible Project", nil, class: "label label-danger"
+ = label_tag "Incompatible Project", nil, class: "label badge-danger"
- if @incompatible_repos.any?
%p
diff --git a/app/views/issues/_issues_calendar.ics.ruby b/app/views/issues/_issues_calendar.ics.ruby
new file mode 100644
index 00000000000..3563635d33d
--- /dev/null
+++ b/app/views/issues/_issues_calendar.ics.ruby
@@ -0,0 +1,15 @@
+cal = Icalendar::Calendar.new
+cal.prodid = '-//GitLab//NONSGML GitLab//EN'
+cal.x_wr_calname = 'GitLab Issues'
+
+@issues.includes(project: :namespace).each do |issue|
+ cal.event do |event|
+ event.dtstart = Icalendar::Values::Date.new(issue.due_date)
+ event.summary = "#{issue.title} (in #{issue.project.full_path})"
+ event.description = "Find out more at #{issue_url(issue)}"
+ event.url = issue_url(issue)
+ event.transp = 'TRANSPARENT'
+ end
+end
+
+cal.to_ical
diff --git a/app/views/kaminari/gitlab/_first_page.html.haml b/app/views/kaminari/gitlab/_first_page.html.haml
index e7a70e3bb28..3b7d4a1c578 100644
--- a/app/views/kaminari/gitlab/_first_page.html.haml
+++ b/app/views/kaminari/gitlab/_first_page.html.haml
@@ -5,5 +5,5 @@
-# total_pages: total number of pages
-# per_page: number of items to fetch per page
-# remote: data-remote
-%li.first
- = link_to_unless current_page.first?, raw(t 'views.pagination.first'), url, remote: remote
+%li.page-item.js-first-button
+ = link_to_unless current_page.first?, raw(t 'views.pagination.first'), url, remote: remote, class: 'page-link'
diff --git a/app/views/kaminari/gitlab/_gap.html.haml b/app/views/kaminari/gitlab/_gap.html.haml
index 889514c4755..849f92fdc95 100644
--- a/app/views/kaminari/gitlab/_gap.html.haml
+++ b/app/views/kaminari/gitlab/_gap.html.haml
@@ -4,6 +4,5 @@
-# total_pages: total number of pages
-# per_page: number of items to fetch per page
-# remote: data-remote
-%li
- %span.gap
- = raw(t 'views.pagination.truncate')
+%li.page-item.disabled.d-none.d-md-block
+ = link_to raw(t 'views.pagination.truncate'), '#', class: 'page-link'
diff --git a/app/views/kaminari/gitlab/_last_page.html.haml b/app/views/kaminari/gitlab/_last_page.html.haml
index 53f780d1d1b..7836e17f877 100644
--- a/app/views/kaminari/gitlab/_last_page.html.haml
+++ b/app/views/kaminari/gitlab/_last_page.html.haml
@@ -5,5 +5,5 @@
-# total_pages: total number of pages
-# per_page: number of items to fetch per page
-# remote: data-remote
-%li.last
- = link_to_unless current_page.last?, raw(t 'views.pagination.last'), url, {remote: remote}
+%li.page-item.js-last-button
+ = link_to_unless current_page.last?, raw(t 'views.pagination.last'), url, {remote: remote, class: 'page-link'}
diff --git a/app/views/kaminari/gitlab/_next_page.html.haml b/app/views/kaminari/gitlab/_next_page.html.haml
index c93dc7a50e8..a7fa1a21a6c 100644
--- a/app/views/kaminari/gitlab/_next_page.html.haml
+++ b/app/views/kaminari/gitlab/_next_page.html.haml
@@ -5,9 +5,8 @@
-# total_pages: total number of pages
-# per_page: number of items to fetch per page
-# remote: data-remote
-- if current_page.last?
- %li.next.disabled
- %span= raw(t 'views.pagination.next')
-- else
- %li.next
- = link_to raw(t 'views.pagination.next'), url, rel: 'next', remote: remote
+
+- page_url = current_page.last? ? '#' : url
+
+%li.page-item.js-next-button{ class: ('disabled' if current_page.last?) }
+ = link_to raw(t 'views.pagination.next'), page_url, rel: 'next', remote: remote, class: 'page-link'
diff --git a/app/views/kaminari/gitlab/_page.html.haml b/app/views/kaminari/gitlab/_page.html.haml
index 5c5be03a7cd..d0dc1784540 100644
--- a/app/views/kaminari/gitlab/_page.html.haml
+++ b/app/views/kaminari/gitlab/_page.html.haml
@@ -6,5 +6,5 @@
-# total_pages: total number of pages
-# per_page: number of items to fetch per page
-# remote: data-remote
-%li.page{ class: [active_when(page.current?), ('sibling' if page.next? || page.prev?)] }
- = link_to page, url, { remote: remote, rel: page.next? ? 'next' : page.prev? ? 'prev' : nil }
+%li.page-item.js-pagination-page{ class: [active_when(page.current?), ('sibling' if page.next? || page.prev?), ('d-none d-md-block' if !page.current?) ] }
+ = link_to page, url, { remote: remote, rel: page.next? ? 'next' : page.prev? ? 'prev' : nil, class: 'page-link' }
diff --git a/app/views/kaminari/gitlab/_paginator.html.haml b/app/views/kaminari/gitlab/_paginator.html.haml
index 8fe6bd653ae..ac9e274dbc7 100644
--- a/app/views/kaminari/gitlab/_paginator.html.haml
+++ b/app/views/kaminari/gitlab/_paginator.html.haml
@@ -6,8 +6,8 @@
-# remote: data-remote
-# paginator: the paginator that renders the pagination tags inside
= paginator.render do
- .gl-pagination
- %ul.pagination.clearfix
+ .gl-pagination.prepend-top-default
+ %ul.pagination.justify-content-center
- unless current_page.first?
= first_page_tag unless total_pages < 5 # As kaminari will always show the first 5 pages
= prev_page_tag
diff --git a/app/views/kaminari/gitlab/_prev_page.html.haml b/app/views/kaminari/gitlab/_prev_page.html.haml
index b7c6caf7ff4..12b0e106a62 100644
--- a/app/views/kaminari/gitlab/_prev_page.html.haml
+++ b/app/views/kaminari/gitlab/_prev_page.html.haml
@@ -5,9 +5,8 @@
-# total_pages: total number of pages
-# per_page: number of items to fetch per page
-# remote: data-remote
-- if current_page.first?
- %li.prev.disabled
- %span= raw(t 'views.pagination.previous')
-- else
- %li.prev
- = link_to raw(t 'views.pagination.previous'), url, rel: 'prev', remote: remote
+
+- page_url = current_page.first? ? '#' : url
+
+%li.page-item.js-previous-button{ class: ('disabled' if current_page.first?) }
+ = link_to raw(t 'views.pagination.previous'), page_url, rel: 'prev', remote: remote, class: 'page-link'
diff --git a/app/views/kaminari/gitlab/_without_count.html.haml b/app/views/kaminari/gitlab/_without_count.html.haml
index 250029c4475..f780400ebcb 100644
--- a/app/views/kaminari/gitlab/_without_count.html.haml
+++ b/app/views/kaminari/gitlab/_without_count.html.haml
@@ -1,8 +1,8 @@
-.gl-pagination
- %ul.pagination.clearfix
+.gl-pagination.prepend-top-default
+ %ul.pagination.justify-content-center
- if previous_path
- %li.prev
- = link_to(t('views.pagination.previous'), previous_path, rel: 'prev')
+ %li.page-item.prev
+ = link_to(t('views.pagination.previous'), previous_path, rel: 'prev', class: 'page-link')
- if next_path
- %li.next
- = link_to(t('views.pagination.next'), next_path, rel: 'next')
+ %li.page-item.next
+ = link_to(t('views.pagination.next'), next_path, rel: 'next', class: 'page-link')
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 02bdfe9aa3c..9253a0652da 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -25,7 +25,7 @@
%title= page_title(site_name)
%meta{ name: "description", content: page_description }
- = favicon_link_tag favicon, id: 'favicon'
+ = favicon_link_tag favicon, id: 'favicon', data: { original_href: favicon }, type: 'image/png'
= stylesheet_link_tag "application", media: "all"
= stylesheet_link_tag "print", media: "print"
diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml
index 52587760ba4..91f796adb2e 100644
--- a/app/views/layouts/_search.html.haml
+++ b/app/views/layouts/_search.html.haml
@@ -7,7 +7,7 @@
- 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), 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|
+ = form_tag search_path, method: :get, class: 'form-inline' do |f|
.search-input-container
- if label.present?
.location-badge= label
diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml
index 6513b719199..81f35615555 100644
--- a/app/views/layouts/devise.html.haml
+++ b/app/views/layouts/devise.html.haml
@@ -1,7 +1,7 @@
!!! 5
%html.devise-layout-html{ class: system_message_class }
= render "layouts/head"
- %body.ui_indigo.login-page.application.navless{ data: { page: body_data_page } }
+ %body.ui-indigo.login-page.application.navless{ data: { page: body_data_page } }
.page-wrap
= render "layouts/header/empty"
.login-page-broadcast
@@ -10,9 +10,7 @@
.content
= render "layouts/flash"
.row
- .col-sm-5.pull-right.new-session-forms-container
- = yield
- .col-sm-7.brand-holder.pull-left
+ .col-sm-7.brand-holder
%h1
= brand_title
= brand_image
@@ -28,6 +26,8 @@
- if Gitlab::CurrentSettings.sign_in_text.present?
= markdown_field(Gitlab::CurrentSettings.current_application_settings, :sign_in_text)
+ .col-sm-5.new-session-forms-container
+ = yield
%hr.footer-fixed
.container.footer-container
diff --git a/app/views/layouts/devise_empty.html.haml b/app/views/layouts/devise_empty.html.haml
index adf90cb7667..52805e0da73 100644
--- a/app/views/layouts/devise_empty.html.haml
+++ b/app/views/layouts/devise_empty.html.haml
@@ -1,7 +1,7 @@
!!! 5
%html{ lang: "en", class: system_message_class }
= render "layouts/head"
- %body.ui_indigo.login-page.application.navless
+ %body.ui-indigo.login-page.application.navless
= render "layouts/header/empty"
= render "layouts/broadcast"
.container.navless-container
diff --git a/app/views/layouts/errors.html.haml b/app/views/layouts/errors.html.haml
index 9382ee8715e..06069a72951 100644
--- a/app/views/layouts/errors.html.haml
+++ b/app/views/layouts/errors.html.haml
@@ -3,57 +3,17 @@
%head
%meta{ :content => "width=device-width, initial-scale=1, maximum-scale=1", :name => "viewport" }
%title= yield(:title)
- :css
- body {
- color: #666;
- text-align: center;
- font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
- margin: auto;
- font-size: 14px;
- }
+ %style
+ = Rails.application.assets_manifest.find_sources('errors.css').first.to_s.html_safe
+ %body
+ .page-container
+ = yield
+ -# haml-lint:disable InlineJavaScript
+ :javascript
+ (function(){
+ var goBackElement = document.querySelector('.js-go-back');
- h1 {
- font-size: 56px;
- line-height: 100px;
- font-weight: 400;
- color: #456;
- }
-
- h2 {
- font-size: 24px;
- color: #666;
- line-height: 1.5em;
- }
-
- h3 {
- color: #456;
- font-size: 20px;
- font-weight: 400;
- line-height: 28px;
- }
-
- hr {
- max-width: 800px;
- margin: 18px auto;
- border: 0;
- border-top: 1px solid #EEE;
- border-bottom: 1px solid white;
- }
-
- img {
- max-width: 40vw;
- display: block;
- margin: 40px auto;
- }
-
- .container {
- margin: auto 20px;
- }
-
- ul {
- margin: auto;
- text-align: left;
- display:inline-block;
- }
-%body
- = yield
+ if (goBackElement && history.length > 1) {
+ goBackElement.style.display = 'block';
+ }
+ }());
diff --git a/app/views/layouts/header/_current_user_dropdown.html.haml b/app/views/layouts/header/_current_user_dropdown.html.haml
index 24b6c490a5a..9ed05d6e3d0 100644
--- a/app/views/layouts/header/_current_user_dropdown.html.haml
+++ b/app/views/layouts/header/_current_user_dropdown.html.haml
@@ -17,6 +17,7 @@
= link_to _("Help"), help_path
- if current_user_menu?(:help) || current_user_menu?(:settings) || current_user_menu?(:profile)
%li.divider
+ = render 'shared/user_dropdown_contributing_link'
- if current_user_menu?(:sign_out)
%li
= link_to _("Sign out"), destroy_user_session_path, class: "sign-out-link"
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index dc121812406..3aa8eb18bf3 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -1,4 +1,4 @@
-%header.navbar.navbar-gitlab.qa-navbar
+%header.navbar.navbar-gitlab.qa-navbar.navbar-expand-sm
%a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content
.container-fluid
.header-content
@@ -8,7 +8,7 @@
= brand_header_logo
- logo_text = brand_header_logo_type
- if logo_text.present?
- %span.logo-text.hidden-xs
+ %span.logo-text.d-none.d-sm-block
= logo_text
- if current_user
@@ -21,9 +21,9 @@
- if current_user
= render 'layouts/header/new_dropdown'
- if header_link?(:search)
- %li.hidden-sm.hidden-xs
+ %li.nav-item.d-none.d-sm-none.d-md-block.m-auto
= render 'layouts/search' unless current_controller?(:search)
- %li.visible-sm-inline-block.visible-xs-inline-block
+ %li.nav-item.d-inline-block.d-sm-none.d-md-none
= link_to search_path, title: 'Search', aria: { label: "Search" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= sprite_icon('search', size: 16)
@@ -32,38 +32,40 @@
= link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= sprite_icon('issues', size: 16)
- issues_count = assigned_issuables_count(:issues)
- %span.badge.issues-count{ class: ('hidden' if issues_count.zero?) }
+ %span.badge.badge-pill.issues-count{ class: ('hidden' if issues_count.zero?) }
= number_with_delimiter(issues_count)
- if header_link?(:merge_requests)
= nav_link(path: 'dashboard#merge_requests', html_options: { class: "user-counter" }) do
= link_to assigned_mrs_dashboard_path, title: 'Merge requests', class: 'dashboard-shortcuts-merge_requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= sprite_icon('git-merge', size: 16)
- merge_requests_count = assigned_issuables_count(:merge_requests)
- %span.badge.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) }
+ %span.badge.badge-pill.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) }
= number_with_delimiter(merge_requests_count)
- if header_link?(:todos)
= nav_link(controller: 'dashboard/todos', html_options: { class: "user-counter" }) do
= link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= sprite_icon('todo-done', size: 16)
- %span.badge.todos-count{ class: ('hidden' if todos_pending_count.zero?) }
+ %span.badge.badge-pill.todos-count{ class: ('hidden' if todos_pending_count.zero?) }
= todos_count_format(todos_pending_count)
- if header_link?(:user_dropdown)
- %li.header-user.dropdown
+ %li.nav-item.header-user.dropdown
= link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do
= image_tag avatar_icon_for_user(current_user, 23), width: 23, height: 23, class: "header-user-avatar qa-user-avatar"
= sprite_icon('angle-down', css_class: 'caret-down')
- .dropdown-menu-nav.dropdown-menu-align-right
+ .dropdown-menu.dropdown-menu-right
= render 'layouts/header/current_user_dropdown'
- if header_link?(:admin_impersonation)
- %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
+ %li.nav-item.impersonation
+ = link_to admin_impersonation_path, class: 'nav-link impersonation-btn', method: :delete, title: "Stop impersonation", aria: { label: 'Stop impersonation' }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
= icon('user-secret')
- if header_link?(:sign_in)
- %li
+ %li.nav-item
%div
- = link_to "Sign in / Register", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in'
+ - sign_in_text = allow_signup? ? 'Sign in / Register' : 'Sign in'
+ = link_to sign_in_text, new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in'
- %button.navbar-toggle.hidden-sm.hidden-md.hidden-lg{ type: 'button' }
+
+ %button.navbar-toggler.d-block.d-sm-none{ type: 'button' }
%span.sr-only Toggle navigation
= sprite_icon('more', size: 12, css_class: 'more-icon js-navbar-toggle-right')
= sprite_icon('close', size: 12, css_class: 'close-icon js-navbar-toggle-left')
diff --git a/app/views/layouts/header/_empty.html.haml b/app/views/layouts/header/_empty.html.haml
index 2ed4edb1136..2dfc787b7a8 100644
--- a/app/views/layouts/header/_empty.html.haml
+++ b/app/views/layouts/header/_empty.html.haml
@@ -1,4 +1,4 @@
-%header.navbar.navbar-fixed-top.navbar-empty
+%header.navbar.fixed-top.navbar-empty
.container
- .center-logo
+ .mx-auto
= brand_header_logo
diff --git a/app/views/layouts/header/_new_dropdown.haml b/app/views/layouts/header/_new_dropdown.haml
index 6f53f5ac1ae..792291bde75 100644
--- a/app/views/layouts/header/_new_dropdown.haml
+++ b/app/views/layouts/header/_new_dropdown.haml
@@ -1,8 +1,8 @@
%li.header-new.dropdown
- = link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip qa-new-menu-toggle", title: "New...", ref: 'tooltip', aria: { label: "New..." }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body' } do
+ = link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip qa-new-menu-toggle", title: "New...", ref: 'tooltip', aria: { label: "New..." }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body', display: 'static' } do
= sprite_icon('plus-square', size: 16)
= sprite_icon('angle-down', css_class: 'caret-down')
- .dropdown-menu-nav.dropdown-menu-align-right
+ .dropdown-menu.dropdown-menu-right
%ul
- if @group&.persisted?
- create_group_project = can?(current_user, :create_projects, @group)
@@ -37,7 +37,7 @@
%li.dropdown-bold-header GitLab
- if current_user.can_create_project?
%li
- = link_to 'New project', new_project_path
+ = link_to 'New project', new_project_path, class: 'qa-global-new-project-link'
- if current_user.can_create_group?
%li
= link_to 'New group', new_group_path
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index f773bd0832d..4029287fc0e 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -1,44 +1,42 @@
%ul.list-unstyled.navbar-sub-nav
- if dashboard_nav_link?(:projects)
= nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown header-projects qa-projects-dropdown" }) do
- %a{ href: "#", data: { toggle: "dropdown" } }
+ %button{ type: 'button', data: { toggle: "dropdown" } }
Projects
= sprite_icon('angle-down', css_class: 'caret-down')
- .dropdown-menu.projects-dropdown-menu
+ .dropdown-menu.frequent-items-dropdown-menu
= render "layouts/nav/projects_dropdown/show"
- if dashboard_nav_link?(:groups)
- = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { class: "hidden-xs" }) do
- = link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups qa-groups-link', title: 'Groups' do
+ = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { id: 'nav-groups-dropdown', class: "home dropdown header-groups qa-groups-dropdown" }) do
+ %button{ type: 'button', data: { toggle: "dropdown" } }
Groups
+ = sprite_icon('angle-down', css_class: 'caret-down')
+ .dropdown-menu.frequent-items-dropdown-menu
+ = render "layouts/nav/groups_dropdown/show"
- if dashboard_nav_link?(:activity)
- = nav_link(path: 'dashboard#activity', html_options: { class: "visible-lg" }) do
+ = nav_link(path: 'dashboard#activity', html_options: { class: "d-none d-lg-block d-xl-block" }) do
= link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do
Activity
- if dashboard_nav_link?(:milestones)
- = nav_link(controller: 'dashboard/milestones', html_options: { class: "visible-lg" }) do
+ = nav_link(controller: 'dashboard/milestones', html_options: { class: "d-none d-lg-block d-xl-block" }) do
= link_to dashboard_milestones_path, class: 'dashboard-shortcuts-milestones', title: 'Milestones' do
Milestones
- if dashboard_nav_link?(:snippets)
- = nav_link(controller: 'dashboard/snippets', html_options: { class: "visible-lg" }) do
+ = nav_link(controller: 'dashboard/snippets', html_options: { class: "d-none d-lg-block d-xl-block" }) do
= link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do
Snippets
- if any_dashboard_nav_link?([:groups, :milestones, :activity, :snippets])
- %li.header-more.dropdown.hidden-lg
+ %li.header-more.dropdown.d-lg-none.d-xl-none
%a{ href: "#", data: { toggle: "dropdown" } }
More
= sprite_icon('angle-down', css_class: 'caret-down')
.dropdown-menu
%ul
- - if dashboard_nav_link?(:groups)
- = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { class: "visible-xs" }) do
- = link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups', title: 'Groups' do
- Groups
-
- if dashboard_nav_link?(:activity)
= nav_link(path: 'dashboard#activity') do
= link_to activity_dashboard_path, title: 'Activity' do
@@ -61,13 +59,13 @@
Projects
- if current_controller?('ide')
- %li.line-separator.hidden-xs
+ %li.line-separator.d-none.d-sm-block
= nav_link(controller: 'ide') do
= link_to '#', class: 'dashboard-shortcuts-web-ide', title: 'Web IDE' do
Web IDE
- if current_user.admin? || Gitlab::Sherlock.enabled?
- %li.line-separator.hidden-xs
+ %li.line-separator.d-none.d-sm-block
- if current_user.admin?
= nav_link(controller: 'admin/dashboard') do
= link_to admin_root_path, class: 'admin-icon qa-admin-area-link', title: 'Admin area', aria: { label: "Admin area" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
diff --git a/app/views/layouts/nav/groups_dropdown/_show.html.haml b/app/views/layouts/nav/groups_dropdown/_show.html.haml
new file mode 100644
index 00000000000..3ce1fa6bcca
--- /dev/null
+++ b/app/views/layouts/nav/groups_dropdown/_show.html.haml
@@ -0,0 +1,12 @@
+- group_meta = { id: @group.id, name: @group.name, namespace: @group.full_name, web_url: group_path(@group), avatar_url: @group.avatar_url } if @group&.persisted?
+.frequent-items-dropdown-container
+ .frequent-items-dropdown-sidebar.qa-groups-dropdown-sidebar
+ %ul
+ = nav_link(path: 'dashboard/groups#index') do
+ = link_to dashboard_groups_path, class: 'qa-your-groups-link' do
+ = _('Your groups')
+ = nav_link(path: 'groups#explore') do
+ = link_to explore_groups_path do
+ = _('Explore groups')
+ .frequent-items-dropdown-content
+ #js-groups-dropdown{ data: { user_name: current_user.username, group: group_meta } }
diff --git a/app/views/layouts/nav/projects_dropdown/_show.html.haml b/app/views/layouts/nav/projects_dropdown/_show.html.haml
index 5809d6f7fea..f2170f71532 100644
--- a/app/views/layouts/nav/projects_dropdown/_show.html.haml
+++ b/app/views/layouts/nav/projects_dropdown/_show.html.haml
@@ -1,6 +1,6 @@
- project_meta = { id: @project.id, name: @project.name, namespace: @project.full_name, web_url: project_path(@project), avatar_url: @project.avatar_url } if @project&.persisted?
-.projects-dropdown-container
- .project-dropdown-sidebar.qa-projects-dropdown-sidebar
+.frequent-items-dropdown-container
+ .frequent-items-dropdown-sidebar.qa-projects-dropdown-sidebar
%ul
= nav_link(path: 'dashboard/projects#index') do
= link_to dashboard_projects_path, class: 'qa-your-projects-link' do
@@ -11,5 +11,5 @@
= nav_link(path: 'projects#trending') do
= link_to explore_root_path do
= _('Explore projects')
- .project-dropdown-content
+ .frequent-items-dropdown-content
#js-projects-dropdown{ data: { user_name: current_user.username, project: project_meta } }
diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml
index dd086f70641..4c73da4c75b 100644
--- a/app/views/layouts/nav/sidebar/_admin.html.haml
+++ b/app/views/layouts/nav/sidebar/_admin.html.haml
@@ -6,14 +6,14 @@
= sprite_icon('admin', size: 24)
.sidebar-context-title Admin Area
%ul.sidebar-top-level-items
- = nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts conversational_development_index), html_options: {class: 'home'}) do
+ = nav_link(controller: %w(dashboard admin projects users groups jobs runners gitaly_servers cohorts conversational_development_index), html_options: {class: 'home'}) do
= link_to admin_root_path, class: 'shortcuts-tree' do
.nav-icon-container
= sprite_icon('overview')
%span.nav-item-name
Overview
%ul.sidebar-sub-level-items
- = nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts conversational_development_index), html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: %w(dashboard admin projects users groups jobs runners gitaly_servers cohorts conversational_development_index), html_options: { class: "fly-out-top-item" } ) do
= link_to admin_root_path do
%strong.fly-out-top-item-name
#{ _('Overview') }
@@ -42,6 +42,10 @@
= link_to admin_runners_path, title: 'Runners' do
%span
Runners
+ = nav_link(controller: :gitaly_servers) do
+ = link_to admin_gitaly_servers_path, title: 'Gitaly Servers' do
+ %span
+ Gitaly Servers
= nav_link path: 'cohorts#index' do
= link_to admin_cohorts_path, title: 'Cohorts' do
%span
@@ -127,13 +131,13 @@
= sprite_icon('slight-frown')
%span.nav-item-name
Abuse Reports
- %span.badge.count= number_with_delimiter(AbuseReport.count(:all))
+ %span.badge.badge-pill.count= number_with_delimiter(AbuseReport.count(:all))
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :abuse_reports, html_options: { class: "fly-out-top-item" } ) do
= link_to admin_abuse_reports_path do
%strong.fly-out-top-item-name
#{ _('Abuse Reports') }
- %span.badge.count.merge_counter.js-merge-counter.fly-out-badge= number_with_delimiter(AbuseReport.count(:all))
+ %span.badge.badge-pill.count.merge_counter.js-merge-counter.fly-out-badge= number_with_delimiter(AbuseReport.count(:all))
- if akismet_enabled?
= nav_link(controller: :spam_logs) do
diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
index 517d9aa3d99..39a033337ff 100644
--- a/app/views/layouts/nav/sidebar/_group.html.haml
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
@@ -43,14 +43,14 @@
= sprite_icon('issues')
%span.nav-item-name
Issues
- %span.badge.count= number_with_delimiter(issues_count)
+ %span.badge.badge-pill.count= number_with_delimiter(issues_count)
%ul.sidebar-sub-level-items
= nav_link(path: ['groups#issues', 'labels#index', 'milestones#index'], html_options: { class: "fly-out-top-item" } ) do
= link_to issues_group_path(@group) do
%strong.fly-out-top-item-name
#{ _('Issues') }
- %span.badge.count.issue_counter.fly-out-badge= number_with_delimiter(issues_count)
+ %span.badge.badge-pill.count.issue_counter.fly-out-badge= number_with_delimiter(issues_count)
%li.divider.fly-out-top-item
= nav_link(path: 'groups#issues', html_options: { class: 'home' }) do
@@ -83,13 +83,13 @@
= sprite_icon('git-merge')
%span.nav-item-name
Merge Requests
- %span.badge.count= number_with_delimiter(merge_requests_count)
+ %span.badge.badge-pill.count= number_with_delimiter(merge_requests_count)
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(path: 'groups#merge_requests', html_options: { class: "fly-out-top-item" } ) do
= link_to merge_requests_group_path(@group) do
%strong.fly-out-top-item-name
#{ _('Merge Requests') }
- %span.badge.count.merge_counter.js-merge-counter.fly-out-badge= number_with_delimiter(merge_requests_count)
+ %span.badge.badge-pill.count.merge_counter.js-merge-counter.fly-out-badge= number_with_delimiter(merge_requests_count)
- if group_sidebar_link?(:group_members)
= nav_link(path: 'group_members#index') do
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index f3af15d771d..33de74dbaa2 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -8,7 +8,7 @@
.sidebar-context-title
= @project.name
%ul.sidebar-top-level-items
- = nav_link(path: ['projects#show', 'projects#activity', 'cycle_analytics#show'], html_options: { class: 'home' }) do
+ = nav_link(path: sidebar_projects_paths, html_options: { class: 'home' }) do
= link_to project_path(@project), class: 'shortcuts-project' do
.nav-icon-container
= sprite_icon('project')
@@ -29,13 +29,15 @@
= link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity' do
%span= _('Activity')
+ = render_if_exists 'projects/sidebar/security_dashboard'
+
- if can?(current_user, :read_cycle_analytics, @project)
= nav_link(path: 'cycle_analytics#show') do
= link_to project_cycle_analytics_path(@project), title: _('Cycle Analytics'), class: 'shortcuts-project-cycle-analytics' do
%span= _('Cycle Analytics')
- if project_nav_tab? :files
- = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare projects/repositories tags branches releases graphs network)) do
+ = nav_link(controller: sidebar_repository_paths) do
= link_to project_tree_path(@project), class: 'shortcuts-tree' do
.nav-icon-container
= sprite_icon('doc_text')
@@ -43,7 +45,7 @@
= _('Repository')
%ul.sidebar-sub-level-items
- = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare projects/repositories tags branches releases graphs network), html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: sidebar_repository_paths, html_options: { class: "fly-out-top-item" } ) do
= link_to project_tree_path(@project) do
%strong.fly-out-top-item-name
= _('Repository')
@@ -80,6 +82,8 @@
= link_to charts_project_graph_path(@project, current_ref) do
= _('Charts')
+ = render_if_exists 'projects/sidebar/repository_locked_files'
+
- if project_nav_tab? :issues
= nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do
= link_to project_issues_path(@project), class: 'shortcuts-issues' do
@@ -88,17 +92,17 @@
%span.nav-item-name
= _('Issues')
- if @project.issues_enabled?
- %span.badge.count.issue_counter
- = number_with_delimiter(@project.open_issues_count)
+ %span.badge.badge-pill.count.issue_counter
+ = number_with_delimiter(@project.open_issues_count(current_user))
%ul.sidebar-sub-level-items
- = nav_link(controller: :issues, html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: :issues, action: :index, html_options: { class: "fly-out-top-item" } ) do
= link_to project_issues_path(@project) do
%strong.fly-out-top-item-name
= _('Issues')
- if @project.issues_enabled?
- %span.badge.count.issue_counter.fly-out-badge
- = number_with_delimiter(@project.open_issues_count)
+ %span.badge.badge-pill.count.issue_counter.fly-out-badge
+ = number_with_delimiter(@project.open_issues_count(current_user))
%li.divider.fly-out-top-item
= nav_link(controller: :issues, action: :index) do
= link_to project_issues_path(@project), title: 'Issues' do
@@ -115,8 +119,10 @@
%span
= _('Labels')
+ = render_if_exists 'projects/sidebar/issues_service_desk'
+
= nav_link(controller: :milestones) do
- = link_to project_milestones_path(@project), title: 'Milestones' do
+ = link_to project_milestones_path(@project), title: 'Milestones', class: 'qa-milestones-link' do
%span
= _('Milestones')
- if project_nav_tab? :external_issue_tracker
@@ -140,14 +146,14 @@
= sprite_icon('git-merge')
%span.nav-item-name
= _('Merge Requests')
- %span.badge.count.merge_counter.js-merge-counter
+ %span.badge.badge-pill.count.merge_counter.js-merge-counter
= number_with_delimiter(@project.open_merge_requests_count)
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :merge_requests, html_options: { class: "fly-out-top-item" } ) do
= link_to project_merge_requests_path(@project) do
%strong.fly-out-top-item-name
= _('Merge Requests')
- %span.badge.count.merge_counter.js-merge-counter.fly-out-badge
+ %span.badge.badge-pill.count.merge_counter.js-merge-counter.fly-out-badge
= number_with_delimiter(@project.open_merge_requests_count)
- if project_nav_tab? :pipelines
@@ -190,7 +196,7 @@
- if project_nav_tab? :operations
= nav_link(controller: [:environments, :clusters, :user, :gcp]) do
- = link_to project_environments_path(@project), class: 'shortcuts-operations' do
+ = link_to metrics_project_environments_path(@project), class: 'shortcuts-operations' do
.nav-icon-container
= sprite_icon('cloud-gear')
%span.nav-item-name
@@ -198,14 +204,19 @@
%ul.sidebar-sub-level-items
= nav_link(controller: [:environments, :clusters, :user, :gcp], html_options: { class: "fly-out-top-item" } ) do
- = link_to project_environments_path(@project) do
+ = link_to metrics_project_environments_path(@project) do
%strong.fly-out-top-item-name
= _('Operations')
%li.divider.fly-out-top-item
- if project_nav_tab? :environments
- = nav_link(controller: :environments) do
- = link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do
+ = nav_link(controller: :environments, action: [:metrics, :metrics_redirect]) do
+ = link_to metrics_project_environments_path(@project), title: _('Metrics'), class: 'shortcuts-metrics' do
+ %span
+ = _('Metrics')
+
+ = nav_link(controller: :environments, action: [:index, :folder, :show, :new, :edit, :create, :update, :stop, :terminal]) do
+ = link_to project_environments_path(@project), title: _('Environments'), class: 'shortcuts-environments' do
%span
= _('Environments')
@@ -234,7 +245,7 @@
= link_to 'Auto DevOps', help_page_path('topics/autodevops/index.md')
%span= _('uses Kubernetes clusters to deploy your code!')
%hr
- %button.btn.btn-create.btn-xs.dismiss-feature-highlight{ type: 'button' }
+ %button.btn.btn-create.btn-sm.dismiss-feature-highlight{ type: 'button' }
%span= _("Got it!")
= sprite_icon('thumb-up')
@@ -278,7 +289,7 @@
= _('Snippets')
- if project_nav_tab? :settings
- = nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show badges#index pages#show]) do
+ = nav_link(path: sidebar_settings_paths) do
= link_to edit_project_path(@project), class: 'shortcuts-tree' do
.nav-icon-container
= sprite_icon('settings')
@@ -288,7 +299,7 @@
%ul.sidebar-sub-level-items
- can_edit = can?(current_user, :admin_project, @project)
- if can_edit
- = nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show badges#index pages#show], html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(path: sidebar_settings_paths, html_options: { class: "fly-out-top-item" } ) do
= link_to edit_project_path(@project) do
%strong.fly-out-top-item-name
= _('Settings')
@@ -326,6 +337,8 @@
%span
= _('Pages')
+ = render_if_exists 'projects/sidebar/settings_audit_events'
+
- else
= nav_link(controller: :project_members) do
= link_to project_settings_members_path(@project), title: 'Members', class: 'shortcuts-tree' do
diff --git a/app/views/layouts/terms.html.haml b/app/views/layouts/terms.html.haml
index 87f4151f241..977eb350365 100644
--- a/app/views/layouts/terms.html.haml
+++ b/app/views/layouts/terms.html.haml
@@ -14,9 +14,9 @@
%div{ class: "#{container_class} limit-container-width" }
.content{ id: "content-body" }
- .panel.panel-default
- .panel-heading
- .title
+ .card
+ .card-header
+ .card-title
= brand_header_logo
- logo_text = brand_header_logo_type
- if logo_text.present?
@@ -29,6 +29,6 @@
= link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do
= image_tag avatar_icon_for_user(current_user, 23), width: 23, height: 23, class: "header-user-avatar qa-user-avatar"
= sprite_icon('angle-down', css_class: 'caret-down')
- .dropdown-menu-nav.dropdown-menu-align-right
+ .dropdown-menu.dropdown-menu-right
= render 'layouts/header/current_user_dropdown'
= yield
diff --git a/app/views/notify/merge_request_unmergeable_email.html.haml b/app/views/notify/merge_request_unmergeable_email.html.haml
new file mode 100644
index 00000000000..7ec0c1ef390
--- /dev/null
+++ b/app/views/notify/merge_request_unmergeable_email.html.haml
@@ -0,0 +1,2 @@
+%p
+ Merge Request #{link_to @merge_request.to_reference, project_merge_request_url(@merge_request.target_project, @merge_request)} can no longer be merged due to conflict.
diff --git a/app/views/notify/merge_request_unmergeable_email.text.haml b/app/views/notify/merge_request_unmergeable_email.text.haml
new file mode 100644
index 00000000000..dcdd6db69d6
--- /dev/null
+++ b/app/views/notify/merge_request_unmergeable_email.text.haml
@@ -0,0 +1,8 @@
+Merge Request #{@merge_request.to_reference} can no longer be merged due to conflict.
+
+Merge Request url: #{project_merge_request_url(@merge_request.target_project, @merge_request)}
+
+= merge_path_description(@merge_request, 'to')
+
+Author: #{@merge_request.author_name}
+Assignee: #{@merge_request.assignee_name}
diff --git a/app/views/notify/new_merge_request_email.html.haml b/app/views/notify/new_merge_request_email.html.haml
index 0a9adc6f243..dd6a84e503d 100644
--- a/app/views/notify/new_merge_request_email.html.haml
+++ b/app/views/notify/new_merge_request_email.html.haml
@@ -9,6 +9,8 @@
%p
Assignee: #{@merge_request.assignee_name}
+= render_if_exists 'notify/merge_request_approvers', merge_request: @merge_request
+
- if @merge_request.description
%div
= markdown(@merge_request.description, pipeline: :email, author: @merge_request.author)
diff --git a/app/views/notify/new_merge_request_email.text.erb b/app/views/notify/new_merge_request_email.text.erb
index 7d98400e6fe..d5b8f8d764f 100644
--- a/app/views/notify/new_merge_request_email.text.erb
+++ b/app/views/notify/new_merge_request_email.text.erb
@@ -5,6 +5,6 @@ New Merge Request <%= @merge_request.to_reference %>
<%= merge_path_description(@merge_request, 'to') %>
Author: <%= @merge_request.author_name %>
Assignee: <%= @merge_request.assignee_name %>
+<%= render_if_exists 'notify/merge_request_approvers', merge_request: @merge_request %>
<%= @merge_request.description %>
-
diff --git a/app/views/notify/pipeline_failed_email.html.haml b/app/views/notify/pipeline_failed_email.html.haml
index 38dab104eb5..baafaa6e3a0 100644
--- a/app/views/notify/pipeline_failed_email.html.haml
+++ b/app/views/notify/pipeline_failed_email.html.haml
@@ -12,7 +12,7 @@
&nbsp;
%tr.section
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
- %table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" }
+ %table.table-info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" }
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project
@@ -114,7 +114,7 @@
= failed.size
failed
#{'build'.pluralize(failed.size)}.
-%tr.warning
+%tr.table-warning
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;border:1px solid #ededed;border-bottom:0;border-radius:3px 3px 0 0;overflow:hidden;background-color:#fdf4f6;color:#d22852;font-size:14px;line-height:1.4;text-align:center;padding:8px 15px;" }
Logs may contain sensitive data. Please consider before forwarding this email.
%tr.section
diff --git a/app/views/notify/pipeline_success_email.html.haml b/app/views/notify/pipeline_success_email.html.haml
index 7b06e8afa0b..4fe3c4c8269 100644
--- a/app/views/notify/pipeline_success_email.html.haml
+++ b/app/views/notify/pipeline_success_email.html.haml
@@ -1,4 +1,4 @@
-%tr.success
+%tr.table-success
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;color:#ffffff;background-color:#31af64;" }
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" }
%tbody
@@ -12,7 +12,7 @@
&nbsp;
%tr.section
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
- %table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" }
+ %table.table-info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" }
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project
diff --git a/app/views/peek/_bar.html.haml b/app/views/peek/_bar.html.haml
index cb0cccb8f8a..89d3b931f88 100644
--- a/app/views/peek/_bar.html.haml
+++ b/app/views/peek/_bar.html.haml
@@ -2,6 +2,6 @@
#js-peek{ data: { env: Peek.env,
request_id: Peek.request_id,
- peek_url: peek_routes.results_url,
+ peek_url: "#{peek_routes_path}/results",
profile_url: url_for(safe_params.merge(lineprofiler: 'true')) },
class: Peek.env }
diff --git a/app/views/profiles/_event_table.html.haml b/app/views/profiles/_event_table.html.haml
index d0ad90ac6cc..9f525547dd9 100644
--- a/app/views/profiles/_event_table.html.haml
+++ b/app/views/profiles/_event_table.html.haml
@@ -1,7 +1,7 @@
%h5.prepend-top-0
History of authentications
-%ul.well-list
+%ul.content-list
- events.each do |event|
%li
%span.description
@@ -9,6 +9,6 @@
Signed in with
= event.details[:with]
authentication
- %span.pull-right= time_ago_with_tooltip(event.created_at)
+ %span.float-right= time_ago_with_tooltip(event.created_at)
= paginate events, theme: "gitlab"
diff --git a/app/views/profiles/active_sessions/_active_session.html.haml b/app/views/profiles/active_sessions/_active_session.html.haml
index d40b771f48b..23ef31a0c85 100644
--- a/app/views/profiles/active_sessions/_active_session.html.haml
+++ b/app/views/profiles/active_sessions/_active_session.html.haml
@@ -1,10 +1,10 @@
- is_current_session = active_session.current?(session)
-%li
- .pull-left.append-right-10{ data: { toggle: 'tooltip' }, title: active_session.human_device_type }
+%li.list-group-item
+ .float-left.append-right-10{ data: { toggle: 'tooltip' }, title: active_session.human_device_type }
= active_session_device_type_icon(active_session)
- .description.pull-left
+ .description.float-left
%div
%strong= active_session.ip_address
- if is_current_session
@@ -25,7 +25,7 @@
= l(active_session.created_at, format: :short)
- unless is_current_session
- .pull-right
+ .float-right
= link_to profile_active_session_path(active_session.session_id), data: { confirm: 'Are you sure? The device will be signed out of GitLab.' }, method: :delete, class: "btn btn-danger prepend-left-10" do
%span.sr-only Revoke
Revoke
diff --git a/app/views/profiles/active_sessions/index.html.haml b/app/views/profiles/active_sessions/index.html.haml
index d0250bb4eab..8688a52843d 100644
--- a/app/views/profiles/active_sessions/index.html.haml
+++ b/app/views/profiles/active_sessions/index.html.haml
@@ -10,5 +10,6 @@
.col-lg-8
.append-bottom-default
- %ul.well-list
- = render partial: 'profiles/active_sessions/active_session', collection: @sessions
+ .card.border-0
+ %ul.list-group.list-group-flush
+ = render partial: 'profiles/active_sessions/active_session', collection: @sessions
diff --git a/app/views/profiles/chat_names/_chat_name.html.haml b/app/views/profiles/chat_names/_chat_name.html.haml
index c7094800fb2..9e82e47c1e1 100644
--- a/app/views/profiles/chat_names/_chat_name.html.haml
+++ b/app/views/profiles/chat_names/_chat_name.html.haml
@@ -24,4 +24,4 @@
Never
%td
- = link_to 'Remove', profile_chat_name_path(chat_name), method: :delete, class: 'btn btn-danger pull-right', data: { confirm: 'Are you sure you want to revoke this nickname?' }
+ = link_to 'Remove', profile_chat_name_path(chat_name), method: :delete, class: 'btn btn-danger float-right', data: { confirm: 'Are you sure you want to revoke this nickname?' }
diff --git a/app/views/profiles/chat_names/new.html.haml b/app/views/profiles/chat_names/new.html.haml
index 5bf22aa94f1..d86941b7a29 100644
--- a/app/views/profiles/chat_names/new.html.haml
+++ b/app/views/profiles/chat_names/new.html.haml
@@ -9,7 +9,7 @@
.actions
= form_tag profile_chat_names_path, method: :post do
= hidden_field_tag :token, @chat_name_token.token
- = submit_tag "Authorize", class: "btn btn-success wide pull-left"
+ = submit_tag "Authorize", class: "btn btn-success wide float-left"
= form_tag deny_profile_chat_names_path, method: :delete do
= hidden_field_tag :token, @chat_name_token.token
= submit_tag "Deny", class: "btn btn-danger prepend-left-10"
diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml
index e3c2bd1150e..a5db9dbe7f8 100644
--- a/app/views/profiles/emails/index.html.haml
+++ b/app/views/profiles/emails/index.html.haml
@@ -29,23 +29,23 @@
Your Public Email will be displayed on your public profile.
%li
All email addresses will be used to identify your commits.
- %ul.well-list
+ %ul.content-list
%li
= render partial: 'shared/email_with_badge', locals: { email: @primary_email, verified: current_user.confirmed? }
- %span.pull-right
- %span.label.label-success Primary email
+ %span.float-right
+ %span.badge.badge-success Primary email
- if @primary_email === current_user.public_email
- %span.label.label-info Public email
+ %span.badge.badge-info Public email
- if @primary_email === current_user.notification_email
- %span.label.label-info Notification email
+ %span.badge.badge-info Notification email
- @emails.each do |email|
%li
= render partial: 'shared/email_with_badge', locals: { email: email.email, verified: email.confirmed? }
- %span.pull-right
+ %span.float-right
- if email.email === current_user.public_email
- %span.label.label-info Public email
+ %span.badge.badge-info Public email
- if email.email === current_user.notification_email
- %span.label.label-info Notification email
+ %span.badge.badge-info Notification email
- unless email.confirmed?
- confirm_title = "#{email.confirmation_sent_at ? 'Resend' : 'Send'} confirmation email"
= link_to confirm_title, resend_confirmation_instructions_profile_email_path(email), method: :put, class: 'btn btn-sm btn-warning prepend-left-10'
diff --git a/app/views/profiles/gpg_keys/_key.html.haml b/app/views/profiles/gpg_keys/_key.html.haml
index 5ed517c1ef6..d1fd7bc8e71 100644
--- a/app/views/profiles/gpg_keys/_key.html.haml
+++ b/app/views/profiles/gpg_keys/_key.html.haml
@@ -1,6 +1,6 @@
%li.key-list-item
- .pull-left.append-right-10
- = icon 'key', class: "settings-list-icon hidden-xs"
+ .float-left.append-right-10
+ = icon 'key', class: "settings-list-icon d-none d-sm-block"
.key-list-item-info
- key.emails_with_verified_status.map do |email, verified|
= render partial: 'shared/email_with_badge', locals: { email: email, verified: verified }
@@ -14,7 +14,7 @@
- key.subkeys.each do |subkey|
%li
%code= subkey.fingerprint
- .pull-right
+ .float-right
%span.key-created-at
created #{time_ago_with_tooltip(key.created_at)}
= link_to profile_gpg_key_path(key), data: { confirm: 'Are you sure? Removing this GPG key does not affect already signed commits.' }, method: :delete, class: "btn btn-danger prepend-left-10" do
diff --git a/app/views/profiles/gpg_keys/_key_table.html.haml b/app/views/profiles/gpg_keys/_key_table.html.haml
index cabb92c5a24..b9b60c218fd 100644
--- a/app/views/profiles/gpg_keys/_key_table.html.haml
+++ b/app/views/profiles/gpg_keys/_key_table.html.haml
@@ -1,7 +1,7 @@
- is_admin = local_assigns.fetch(:admin, false)
- if @gpg_keys.any?
- %ul.well-list
+ %ul.content-list
= render partial: 'profiles/gpg_keys/key', collection: @gpg_keys, locals: { is_admin: is_admin }
- else
%p.settings-message.text-center
diff --git a/app/views/profiles/keys/_form.html.haml b/app/views/profiles/keys/_form.html.haml
index 6ea358d9f63..43a2d53b84d 100644
--- a/app/views/profiles/keys/_form.html.haml
+++ b/app/views/profiles/keys/_form.html.haml
@@ -4,10 +4,19 @@
.form-group
= f.label :key, class: 'label-light'
- = f.text_area :key, class: "form-control", rows: 8, required: true, placeholder: "Don't paste the private part of the SSH key. Paste the public part, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'."
+ %p= _("Paste your public SSH key, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'. Don't use your private SSH key.")
+ = f.text_area :key, class: "form-control js-add-ssh-key-validation-input", rows: 8, required: true, placeholder: s_('Profiles|Typically starts with "ssh-rsa …"')
.form-group
= f.label :title, class: 'label-light'
- = f.text_field :title, class: "form-control", required: true
+ = f.text_field :title, class: "form-control input-lg", required: true, placeholder: s_('Profiles|e.g. My MacBook key')
+ %p.form-text.text-muted= _('Name your individual key via a title')
+
+ .js-add-ssh-key-validation-warning.hide
+ .bs-callout.bs-callout-warning{ role: 'alert', aria_live: 'assertive' }
+ %strong= _('Oops, are you sure?')
+ %p= s_("Profiles|This doesn't look like a public SSH key, are you sure you want to add it?")
+
+ %button.btn.btn-create.js-add-ssh-key-validation-confirm-submit= _("Yes, add it")
.prepend-top-default
- = f.submit 'Add key', class: "btn btn-create"
+ = f.submit s_('Profiles|Add key'), class: "btn btn-create js-add-ssh-key-validation-original-submit"
diff --git a/app/views/profiles/keys/_key.html.haml b/app/views/profiles/keys/_key.html.haml
index 103446243e5..ce20994b0f4 100644
--- a/app/views/profiles/keys/_key.html.haml
+++ b/app/views/profiles/keys/_key.html.haml
@@ -1,9 +1,9 @@
%li.key-list-item
- .pull-left.append-right-10
+ .float-left.append-right-10
- if key.valid?
- = icon 'key', class: 'settings-list-icon hidden-xs'
+ = icon 'key', class: 'settings-list-icon d-none d-sm-block'
- else
- = icon 'exclamation-triangle', class: 'settings-list-icon hidden-xs has-tooltip',
+ = icon 'exclamation-triangle', class: 'settings-list-icon d-none d-sm-block has-tooltip',
title: key.errors.full_messages.join(', ')
@@ -15,7 +15,7 @@
.last-used-at
last used:
= key.last_used_at ? time_ago_with_tooltip(key.last_used_at) : 'n/a'
- .pull-right
+ .float-right
%span.key-created-at
created #{time_ago_with_tooltip(key.created_at)}
= link_to path_to_key(key, is_admin), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-transparent prepend-left-10" do
diff --git a/app/views/profiles/keys/_key_details.html.haml b/app/views/profiles/keys/_key_details.html.haml
index 77521417f47..2ac514d3f6f 100644
--- a/app/views/profiles/keys/_key_details.html.haml
+++ b/app/views/profiles/keys/_key_details.html.haml
@@ -1,10 +1,10 @@
- is_admin = defined?(admin) ? true : false
.row.prepend-top-default
.col-md-4
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
SSH Key
- %ul.well-list
+ %ul.content-list
%li
%span.light Title:
%strong= @key.title
@@ -23,5 +23,5 @@
%pre.well-pre
= @key.key
.col-md-12
- .pull-right
+ .float-right
= link_to 'Remove', path_to_key(@key, is_admin), data: {confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove delete-key"
diff --git a/app/views/profiles/keys/_key_table.html.haml b/app/views/profiles/keys/_key_table.html.haml
index e78763bdcb2..e088140fdd2 100644
--- a/app/views/profiles/keys/_key_table.html.haml
+++ b/app/views/profiles/keys/_key_table.html.haml
@@ -1,7 +1,7 @@
- is_admin = local_assigns.fetch(:admin, false)
- if @keys.any?
- %ul.well-list
+ %ul.content-list
= render partial: 'profiles/keys/key', collection: @keys, locals: { is_admin: is_admin }
- else
%p.settings-message.text-center
diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml
index 1e206def7ee..55ca8d0ebd4 100644
--- a/app/views/profiles/keys/index.html.haml
+++ b/app/views/profiles/keys/index.html.haml
@@ -11,10 +11,11 @@
%h5.prepend-top-0
Add an SSH key
%p.profile-settings-content
- Before you can add an SSH key you need to
- = link_to "generate one", help_page_path("ssh/README", anchor: 'generating-a-new-ssh-key-pair')
- or use an
- = link_to "existing key.", help_page_path("ssh/README", anchor: 'locating-an-existing-ssh-key-pair')
+ - generate_link_url = help_page_path("ssh/README", anchor: 'generating-a-new-ssh-key-pair')
+ - existing_link_url = help_page_path("ssh/README", anchor: 'locating-an-existing-ssh-key-pair')
+ - generate_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: generate_link_url }
+ - existing_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: existing_link_url }
+ = _('To add an SSH key you need to %{generate_link_start}generate one%{link_end} or use an %{existing_link_start}existing key%{link_end}.').html_safe % { generate_link_start: generate_link_start, existing_link_start: existing_link_start, link_end: '</a>'.html_safe }
= render 'form'
%hr
%h5
diff --git a/app/views/profiles/notifications/_group_settings.html.haml b/app/views/profiles/notifications/_group_settings.html.haml
index 537bba21f4a..a12246bcdcc 100644
--- a/app/views/profiles/notifications/_group_settings.html.haml
+++ b/app/views/profiles/notifications/_group_settings.html.haml
@@ -8,5 +8,5 @@
%span.str-truncated
= link_to group.name, group_path(group)
- .pull-right
+ .float-right
= render 'shared/notifications/button', notification_setting: setting
diff --git a/app/views/profiles/notifications/_project_settings.html.haml b/app/views/profiles/notifications/_project_settings.html.haml
index 5b2a69b8891..823fec3fab4 100644
--- a/app/views/profiles/notifications/_project_settings.html.haml
+++ b/app/views/profiles/notifications/_project_settings.html.haml
@@ -8,5 +8,5 @@
%span.str-truncated
= link_to_project(project)
- .pull-right
+ .float-right
= render 'shared/notifications/button', notification_setting: setting
diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml
index 8f099aa6dd7..71ea625e5d5 100644
--- a/app/views/profiles/notifications/show.html.haml
+++ b/app/views/profiles/notifications/show.html.haml
@@ -29,7 +29,7 @@
= label_tag :global_notification_level, "Global notification level", class: "label-light"
%br
.clearfix
- .form-group.pull-left.global-notification-setting
+ .form-group.float-left.global-notification-setting
= render 'shared/notifications/button', notification_setting: @global_notification_setting
.clearfix
diff --git a/app/views/profiles/passwords/edit.html.haml b/app/views/profiles/passwords/edit.html.haml
index c606b5a1e6c..8a51a30191a 100644
--- a/app/views/profiles/passwords/edit.html.haml
+++ b/app/views/profiles/passwords/edit.html.haml
@@ -20,7 +20,7 @@
.form-group
= f.label :current_password, class: 'label-light'
= f.password_field :current_password, required: true, class: 'form-control'
- %p.help-block
+ %p.form-text.text-muted
You must provide your current password in order to change it.
.form-group
= f.label :password, 'New password', class: 'label-light'
diff --git a/app/views/profiles/passwords/new.html.haml b/app/views/profiles/passwords/new.html.haml
index 2eb9fac57c3..2176d7f8a31 100644
--- a/app/views/profiles/passwords/new.html.haml
+++ b/app/views/profiles/passwords/new.html.haml
@@ -2,7 +2,7 @@
- header_title "New Password"
%h3.page-title Setup new password
%hr
-= form_for @user, url: profile_password_path, method: :post, html: { class: 'form-horizontal '} do |f|
+= form_for @user, url: profile_password_path, method: :post do |f|
%p.slead
Please set a new password before proceeding.
%br
@@ -11,14 +11,14 @@
= form_errors(@user)
- unless @user.password_automatically_set?
- .form-group
- = f.label :current_password, class: 'control-label'
+ .form-group.row
+ = f.label :current_password, class: 'col-form-label col-sm-2'
.col-sm-10= f.password_field :current_password, required: true, class: 'form-control'
- .form-group
- = f.label :password, class: 'control-label'
+ .form-group.row
+ = f.label :password, class: 'col-form-label col-sm-2'
.col-sm-10= f.password_field :password, required: true, class: 'form-control'
- .form-group
- = f.label :password_confirmation, class: 'control-label'
+ .form-group.row
+ = f.label :password_confirmation, class: 'col-form-label col-sm-2'
.col-sm-10
= f.password_field :password_confirmation, required: true, class: 'form-control'
.form-actions
diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml
index 9b87a7aaca8..d111113c646 100644
--- a/app/views/profiles/personal_access_tokens/index.html.haml
+++ b/app/views/profiles/personal_access_tokens/index.html.haml
@@ -20,9 +20,9 @@
.form-group
.input-group
= text_field_tag 'created-personal-access-token', @new_personal_access_token, readonly: true, class: "form-control js-select-on-focus", 'aria-describedby' => "created-personal-access-token-help-block"
- %span.input-group-btn
- = clipboard_button(text: @new_personal_access_token, title: "Copy personal access token to clipboard", placement: "left", class: "btn-default btn-clipboard")
- %span#created-personal-access-token-help-block.help-block.text-danger Make sure you save it - you won't be able to access it again.
+ %span.input-group-append
+ = clipboard_button(text: @new_personal_access_token, title: "Copy personal access token to clipboard", placement: "left", class: "input-group-text btn-default btn-clipboard")
+ %span#created-personal-access-token-help-block.form-text.text-muted.text-danger Make sure you save it - you won't be able to access it again.
%hr
@@ -34,18 +34,18 @@
.row.prepend-top-default
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
- RSS token
+ Feed 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.
+ Your feed token is used to authenticate you when your RSS reader loads a personalized RSS feed or when when your calendar application loads a personalized calendar, and is included in those 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.
+ .col-lg-8.feed-token-reset
+ = label_tag :feed_token, 'Feed token', class: "label-light"
+ = text_field_tag :feed_token, current_user.feed_token, class: 'form-control', readonly: true, onclick: 'this.select()'
+ %p.form-text.text-muted
+ Keep this token secret. Anyone who gets ahold of it can read activity and issue RSS feeds or your calendar feed 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.' }
+ = link_to 'reset it', [:reset, :feed_token, :profile], method: :put, data: { confirm: 'Are you sure? Any RSS or calendar URLs currently in use will stop working.' }
if that ever happens.
- if incoming_email_token_enabled?
@@ -61,7 +61,7 @@
.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
+ %p.form-text.text-muted
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.' }
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index 6aefd97bb96..8f1078bd41d 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -4,18 +4,12 @@
= form_for @user, url: profile_preferences_path, remote: true, method: :put, html: { class: 'row prepend-top-default js-preferences-form' } do |f|
.col-lg-4.application-theme
%h4.prepend-top-0
- GitLab navigation theme
+ = s_('Preferences|Navigation theme')
%p Customize the appearance of the application header and navigation sidebar.
.col-lg-8.application-theme
- Gitlab::Themes.each do |theme|
= label_tag do
- .preview{ class: theme.name.downcase }
- .preview-row
- .quadrant.one
- .quadrant.two
- .preview-row
- .quadrant.three
- .quadrant.four
+ .preview{ class: theme.css_class }
= f.radio_button :theme_id, theme.id, checked: Gitlab::Themes.for_user(@user).id == theme.id
= theme.name
@@ -51,7 +45,7 @@
= f.label :layout, class: 'label-light' do
Layout width
= f.select :layout, layout_choices, {}, class: 'form-control'
- .help-block
+ .form-text.text-muted
Choose between fixed (max. 1200px) and fluid (100%) application layout.
.form-group
= f.label :dashboard, class: 'label-light' do
@@ -61,7 +55,7 @@
= f.label :project_view, class: 'label-light' do
Project overview content
= f.select :project_view, project_view_choices, {}, class: 'form-control'
- .help-block
+ .form-text.text-muted
Choose what content you want to see on a project’s overview page
.form-group
= f.submit 'Save changes', class: 'btn btn-save'
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index e497eab32e0..507cd5dcc12 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -26,7 +26,7 @@
%button.btn.js-choose-user-avatar-button{ type: 'button' }= _("Choose file...")
%span.avatar-file-name.prepend-left-default.js-avatar-filename= _("No file chosen")
= f.file_field_without_bootstrap :avatar, class: 'js-user-avatar-input hidden', accept: 'image/*'
- .help-block= _("The maximum file size allowed is 200KB.")
+ .form-text.text-muted= _("The maximum file size allowed is 200KB.")
- if @user.avatar?
%hr
= link_to _('Remove avatar'), profile_avatar_path, data: { confirm: _('Avatar will be removed. Are you sure?') }, method: :delete, class: 'btn btn-danger btn-inverted'
@@ -77,11 +77,10 @@
.modal-dialog
.modal-content
.modal-header
- %button.close{ type: 'button', 'data-dismiss': 'modal' }
- %span
- &times;
%h4.modal-title
Position and size your new avatar
+ %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
+ %span{ "aria-hidden": true } &times;
.modal-body
.profile-crop-image-container
%img.modal-profile-crop-image{ alt: 'Avatar cropper' }
diff --git a/app/views/profiles/two_factor_auths/_codes.html.haml b/app/views/profiles/two_factor_auths/_codes.html.haml
index 43b58be7f98..93722d7b034 100644
--- a/app/views/profiles/two_factor_auths/_codes.html.haml
+++ b/app/views/profiles/two_factor_auths/_codes.html.haml
@@ -4,7 +4,7 @@
%b will
lose access to your account.
-.codes.well
+.codes.card
%ul
- @codes.each do |code|
%li
diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
index d1eae05c46c..e35ebdea435 100644
--- a/app/views/profiles/two_factor_auths/show.html.haml
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -86,7 +86,7 @@
%tr
%td= registration.name.presence || "<no name set>"
%td= registration.created_at.to_date.to_s(:medium)
- %td= link_to "Delete", profile_u2f_registration_path(registration), method: :delete, class: "btn btn-danger pull-right", data: { confirm: "Are you sure you want to delete this device? This action cannot be undone." }
+ %td= link_to "Delete", profile_u2f_registration_path(registration), method: :delete, class: "btn btn-danger float-right", data: { confirm: "Are you sure you want to delete this device? This action cannot be undone." }
- else
.settings-message.text-center
diff --git a/app/views/projects/_bitbucket_import_modal.html.haml b/app/views/projects/_bitbucket_import_modal.html.haml
index c24a496486c..c54a4ceb890 100644
--- a/app/views/projects/_bitbucket_import_modal.html.haml
+++ b/app/views/projects/_bitbucket_import_modal.html.haml
@@ -2,8 +2,9 @@
.modal-dialog
.modal-content
.modal-header
- %a.close{ href: "#", "data-dismiss" => "modal" } ×
- %h3 Import projects from Bitbucket
+ %h3.modal-title Import projects from Bitbucket
+ %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
+ %span{ "aria-hidden": true } &times;
.modal-body
To enable importing projects from Bitbucket,
- if current_user.admin?
diff --git a/app/views/projects/_export.html.haml b/app/views/projects/_export.html.haml
index 1e7d9444986..aa980da7e95 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.no-animate{ class: ('expanded' if expanded) }
+%section.settings.no-animate#js-export-project{ class: ('expanded' if expanded) }
.settings-header
%h4
Export project
@@ -31,7 +31,7 @@
%li Any encrypted tokens
%p
Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page.
- - if project.export_project_path
+ - if project.export_status == :finished
= link_to 'Download export', download_export_project_path(project),
rel: 'nofollow', download: '', method: :get, class: "btn btn-default"
= link_to 'Generate new export', generate_new_export_project_path(project),
diff --git a/app/views/projects/_gitlab_import_modal.html.haml b/app/views/projects/_gitlab_import_modal.html.haml
index 00aef66e1f8..5519415cdc3 100644
--- a/app/views/projects/_gitlab_import_modal.html.haml
+++ b/app/views/projects/_gitlab_import_modal.html.haml
@@ -2,8 +2,9 @@
.modal-dialog
.modal-content
.modal-header
- %a.close{ href: "#", "data-dismiss" => "modal" } ×
- %h3 Import projects from GitLab.com
+ %h3.modal-title Import projects from GitLab.com
+ %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
+ %span{ "aria-hidden": true } &times;
.modal-body
To enable importing projects from GitLab.com,
- if current_user.admin?
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 043057e79ee..89940512bc6 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -37,11 +37,15 @@
= render 'projects/buttons/star'
= render 'projects/buttons/fork'
- %span.hidden-xs
+ %span.d-none.d-sm-inline
- if can?(current_user, :download_code, @project)
.project-clone-holder
= render "shared/clone_panel"
+ - if show_xcode_link?(@project)
+ .project-action-button.project-xcode.inline
+ = render "projects/buttons/xcode_link"
+
- if current_user
- if can?(current_user, :download_code, @project)
= render 'projects/buttons/download', project: @project, ref: @ref
diff --git a/app/views/projects/_import_project_pane.html.haml b/app/views/projects/_import_project_pane.html.haml
index 4bee6cb97eb..8f535b9d789 100644
--- a/app/views/projects/_import_project_pane.html.haml
+++ b/app/views/projects/_import_project_pane.html.haml
@@ -1,51 +1,49 @@
- active_tab = local_assigns.fetch(:active_tab, 'blank')
- f = local_assigns.fetch(:f)
-.project-import.row
- .col-lg-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 js-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_path, class: 'btn import_gitea' do
- = custom_icon('go_logo')
- Gitea
- %div
- - if git_import_enabled?
- %button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active' } }
- = icon('git', text: 'Repo by URL')
- .col-lg-12
- .js-toggle-content.toggle-import-form{ class: ('hide' if active_tab != 'import') }
- %hr
- = render "shared/import_form", f: f
- = render 'new_project_fields', f: f, project_name_id: "import-url-name"
+.project-import
+ .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 js-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_path, class: 'btn import_gitea' do
+ = custom_icon('go_logo')
+ Gitea
+ %div
+ - if git_import_enabled?
+ %button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active' } }
+ = icon('git', text: 'Repo by URL')
+ .js-toggle-content.toggle-import-form{ class: ('hide' if active_tab != 'import') }
+ %hr
+ = render "shared/import_form", f: f
+ = render 'new_project_fields', f: f, project_name_id: "import-url-name"
diff --git a/app/views/projects/_issuable_by_email.html.haml b/app/views/projects/_issuable_by_email.html.haml
index c137e38ed50..22adf5b4008 100644
--- a/app/views/projects/_issuable_by_email.html.haml
+++ b/app/views/projects/_issuable_by_email.html.haml
@@ -8,17 +8,17 @@
.modal-dialog{ role: "document" }
.modal-content
.modal-header
- %button.close{ type: "button", data: { dismiss: "modal" }, aria: { label: "close" } }
- %span{ aria: { hidden: "true" } }= icon("times")
%h4.modal-title
Create new #{name} by email
+ %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
+ %span{ "aria-hidden": true } &times;
.modal-body
%p
You can create a new #{name} inside this project by sending an email to the following email address:
.email-modal-input-group.input-group
= text_field_tag :issuable_email, email, class: "monospace js-select-on-focus form-control", readonly: true
- .input-group-btn
- = clipboard_button(target: '#issuable_email', class: 'btn btn-clipboard btn-transparent hidden-xs')
+ .input-group-append
+ = clipboard_button(target: '#issuable_email', class: 'btn btn-clipboard input-group-text btn-transparent d-none d-sm-block')
= mail_to email, class: 'btn btn-clipboard btn-transparent',
subject: _("Enter the #{name} title"),
body: _("Enter the #{name} description"),
diff --git a/app/views/projects/_last_push.html.haml b/app/views/projects/_last_push.html.haml
index f6d396c8127..3b66fdbdf1a 100644
--- a/app/views/projects/_last_push.html.haml
+++ b/app/views/projects/_last_push.html.haml
@@ -1,6 +1,6 @@
- event = last_push_event
- if event && show_last_push_widget?(event)
- .row-content-block.top-block.hidden-xs.white
+ .row-content-block.top-block.d-none.d-sm-block.white
.event-last-push
.event-last-push-text
%span= s_("LastPushEvent|You pushed to")
diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml
index 8212ab9a31e..8fb6aa55436 100644
--- a/app/views/projects/_md_preview.html.haml
+++ b/app/views/projects/_md_preview.html.haml
@@ -9,7 +9,7 @@
.md-area
.md-header
- %ul.nav-links.clearfix
+ %ul.nav.nav-tabs.nav-links.clearfix
%li.md-header-tab.active
%a.js-md-write-button{ href: "#md-write-holder", tabindex: -1 }
Write
diff --git a/app/views/projects/_merge_request_fast_forward_settings.html.haml b/app/views/projects/_merge_request_fast_forward_settings.html.haml
deleted file mode 100644
index f455522d17c..00000000000
--- a/app/views/projects/_merge_request_fast_forward_settings.html.haml
+++ /dev/null
@@ -1,13 +0,0 @@
-- form = local_assigns.fetch(:form)
-- project = local_assigns.fetch(:project)
-
-.radio
- = label_tag :project_merge_method_ff do
- = form.radio_button :merge_method, :ff, class: "js-merge-method-radio qa-radio-button-merge-ff"
- %strong Fast-forward merge
- %br
- %span.descr
- No merge commits are created and all merges are fast-forwarded, which means that merging is only allowed if the branch could be fast-forwarded.
- %br
- %span.descr
- When fast-forward merge is not possible, the user is given the option to rebase.
diff --git a/app/views/projects/_merge_request_merge_method_settings.html.haml b/app/views/projects/_merge_request_merge_method_settings.html.haml
new file mode 100644
index 00000000000..3bb220ac6d0
--- /dev/null
+++ b/app/views/projects/_merge_request_merge_method_settings.html.haml
@@ -0,0 +1,36 @@
+- form = local_assigns.fetch(:form)
+- project = local_assigns.fetch(:project)
+
+.form-group
+ = label_tag :merge_method_merge, class: 'label-light' do
+ Merge method
+ .form-check
+ = form.radio_button :merge_method, :merge, class: "js-merge-method-radio form-check-input"
+ = label_tag :project_merge_method_merge, class: 'form-check-label' do
+ %strong Merge commit
+ %br
+ %span.descr
+ A merge commit is created for every merge, and merging is allowed as long as there are no conflicts.
+
+ .form-check
+ = form.radio_button :merge_method, :rebase_merge, class: "js-merge-method-radio form-check-input"
+ = label_tag :project_merge_method_rebase_merge, class: 'form-check-label' do
+ %strong Merge commit with semi-linear history
+ %br
+ %span.descr
+ A merge commit is created for every merge, but merging is only allowed if fast-forward merge is possible.
+ This way you could make sure that if this merge request would build, after merging to target branch it would also build.
+ %br
+ %span.descr
+ When fast-forward merge is not possible, the user is given the option to rebase.
+
+ .form-check
+ = form.radio_button :merge_method, :ff, class: "js-merge-method-radio qa-radio-button-merge-ff form-check-input"
+ = label_tag :project_merge_method_ff, class: 'form-check-label' do
+ %strong Fast-forward merge
+ %br
+ %span.descr
+ No merge commits are created and all merges are fast-forwarded, which means that merging is only allowed if the branch could be fast-forwarded.
+ %br
+ %span.descr
+ When fast-forward merge is not possible, the user is given the option to rebase.
diff --git a/app/views/projects/_merge_request_merge_settings.html.haml b/app/views/projects/_merge_request_merge_settings.html.haml
index f6e5712ce81..f178c94e008 100644
--- a/app/views/projects/_merge_request_merge_settings.html.haml
+++ b/app/views/projects/_merge_request_merge_settings.html.haml
@@ -1,23 +1,23 @@
- form = local_assigns.fetch(:form)
.form-group
- .checkbox.builds-feature{ class: ("hidden" if @project && @project.project_feature.send(:builds_access_level) == 0) }
- = form.label :only_allow_merge_if_pipeline_succeeds do
- = form.check_box :only_allow_merge_if_pipeline_succeeds
+ .form-check.builds-feature{ class: ("hidden" if @project && @project.project_feature.send(:builds_access_level) == 0) }
+ = form.check_box :only_allow_merge_if_pipeline_succeeds, class: 'form-check-input'
+ = form.label :only_allow_merge_if_pipeline_succeeds, class: 'form-check-label' do
%strong Only allow merge requests to be merged if the pipeline succeeds
%br
%span.descr
Pipelines need to be configured to enable this feature.
= link_to icon('question-circle'), help_page_path('user/project/merge_requests/merge_when_pipeline_succeeds', anchor: 'only-allow-merge-requests-to-be-merged-if-the-pipeline-succeeds'), target: '_blank'
- .checkbox
- = form.label :only_allow_merge_if_all_discussions_are_resolved do
- = form.check_box :only_allow_merge_if_all_discussions_are_resolved
+ .form-check
+ = form.check_box :only_allow_merge_if_all_discussions_are_resolved, class: 'form-check-input'
+ = form.label :only_allow_merge_if_all_discussions_are_resolved, class: 'form-check-label' do
%strong Only allow merge requests to be merged if all discussions are resolved
- .checkbox
- = form.label :resolve_outdated_diff_discussions do
- = form.check_box :resolve_outdated_diff_discussions
+ .form-check
+ = form.check_box :resolve_outdated_diff_discussions, class: 'form-check-input'
+ = form.label :resolve_outdated_diff_discussions, class: 'form-check-label' do
%strong Automatically resolve merge request diff discussions when they become outdated
- .checkbox
- = form.label :printing_merge_request_link_enabled do
- = form.check_box :printing_merge_request_link_enabled
+ .form-check
+ = form.check_box :printing_merge_request_link_enabled, class: 'form-check-input'
+ = form.label :printing_merge_request_link_enabled, class: 'form-check-label' do
%strong Show link to create/view merge request when pushing from the command line
diff --git a/app/views/projects/_merge_request_rebase_settings.html.haml b/app/views/projects/_merge_request_rebase_settings.html.haml
deleted file mode 100644
index 54e0b73d24c..00000000000
--- a/app/views/projects/_merge_request_rebase_settings.html.haml
+++ /dev/null
@@ -1,13 +0,0 @@
-- form = local_assigns.fetch(:form)
-
-.radio
- = label_tag :project_merge_method_rebase_merge do
- = form.radio_button :merge_method, :rebase_merge, class: "js-merge-method-radio"
- %strong Merge commit with semi-linear history
- %br
- %span.descr
- A merge commit is created for every merge, but merging is only allowed if fast-forward merge is possible.
- This way you could make sure that if this merge request would build, after merging to target branch it would also build.
- %br
- %span.descr
- When fast-forward merge is not possible, the user is given the option to rebase.
diff --git a/app/views/projects/_merge_request_settings.html.haml b/app/views/projects/_merge_request_settings.html.haml
index fd0c419cdac..c80e831dd33 100644
--- a/app/views/projects/_merge_request_settings.html.haml
+++ b/app/views/projects/_merge_request_settings.html.haml
@@ -1,18 +1,5 @@
- form = local_assigns.fetch(:form)
-.form-group
- = label_tag :merge_method_merge, class: 'label-light' do
- Merge method
- .radio
- = label_tag :project_merge_method_merge do
- = form.radio_button :merge_method, :merge, class: "js-merge-method-radio"
- %strong Merge commit
- %br
- %span.descr
- A merge commit is created for every merge, and merging is allowed as long as there are no conflicts.
-
- = render 'merge_request_rebase_settings', form: form
-
- = render 'merge_request_fast_forward_settings', project: @project, form: form
+= render 'projects/merge_request_merge_method_settings', project: @project, form: form
= render 'projects/merge_request_merge_settings', form: form
diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml
index 241bc3dbca0..f4994f5459b 100644
--- a/app/views/projects/_new_project_fields.html.haml
+++ b/app/views/projects/_new_project_fields.html.haml
@@ -9,13 +9,15 @@
Project path
.input-group
- if current_user.can_select_namespace?
- .input-group-addon.has-tooltip{ title: root_url }
- = root_url
+ .input-group-prepend.has-tooltip{ title: root_url }
+ .input-group-text
+ = 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 qa-project-namespace-select', tabindex: 1}
- else
- .input-group-addon.static-namespace.has-tooltip{ title: user_url(current_user.username) + '/' }
- #{user_url(current_user.username)}/
+ .input-group-prepend.static-namespace.has-tooltip{ title: user_url(current_user.username) + '/' }
+ .input-group-text.border-0
+ #{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
@@ -23,7 +25,7 @@
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
+ .form-text.text-muted
Want to house several dependent projects under the same namespace?
= link_to "Create a group", new_group_path
@@ -33,11 +35,20 @@
%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' }, target: '_blank', rel: 'noopener noreferrer'
- = render 'shared/visibility_level', f: f, visibility_level: visibility_level.to_i, can_change_visibility_level: true, form_model: @project, with_label: false
+= 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' }, target: '_blank', rel: 'noopener noreferrer'
+= render 'shared/visibility_level', f: f, visibility_level: visibility_level.to_i, can_change_visibility_level: true, form_model: @project, with_label: false
+
+.form-group.row.initialize-with-readme-setting
+ %div{ :class => "col-sm-12" }
+ .form-check
+ = check_box_tag 'project[initialize_with_readme]', '1', false, class: 'form-check-input'
+ = label_tag 'project[initialize_with_readme]', class: 'form-check-label' do
+ .option-title
+ %strong Initialize repository with a README
+ .option-description
+ Allows you to immediately clone this project’s repository. Skip this if you plan to push up an existing repository.
= 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/_new_project_push_tip.html.haml b/app/views/projects/_new_project_push_tip.html.haml
index 9bc69211d12..22e9522c0e7 100644
--- a/app/views/projects/_new_project_push_tip.html.haml
+++ b/app/views/projects/_new_project_push_tip.html.haml
@@ -3,9 +3,9 @@
= label_tag(:push_to_create_tip, _("Private projects can be created in your personal namespace with:"), class: "weight-normal")
%p.input-group.project-tip-command
- %span.input-group-btn
+ %span
= text_field_tag :push_to_create_tip, push_to_create_project_command, class: "js-select-on-focus form-control monospace", readonly: true, aria: { label: _("Push project from command line") }
- %span.input-group-btn
- = clipboard_button(text: push_to_create_project_command, title: _("Copy command to clipboard"), placement: "right")
+ %span.input-group-append
+ = clipboard_button(text: push_to_create_project_command, title: _("Copy command to clipboard"), class: 'input-group-text', placement: "right")
%p
= link_to("What does this command do?", help_page_path("gitlab-basics/create-project", anchor: "push-to-create-a-new-project"), target: "_blank")
diff --git a/app/views/projects/_project_templates.html.haml b/app/views/projects/_project_templates.html.haml
index d50175727be..d08807b5135 100644
--- a/app/views/projects/_project_templates.html.haml
+++ b/app/views/projects/_project_templates.html.haml
@@ -10,15 +10,18 @@
%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
+ .row
+ .form-group.col-sm-12
+ %label.label-light
+ Template
+ .input-group.template-input-group
+ .input-group-prepend
+ .input-group-text
+ .selected-icon
+ - Gitlab::ProjectTemplate.all.each do |template|
+ = custom_icon(template.logo)
+ .selected-template
+ .input-group-append
+ %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/_stat_anchor_list.html.haml b/app/views/projects/_stat_anchor_list.html.haml
index a115b65938b..15ec58289e3 100644
--- a/app/views/projects/_stat_anchor_list.html.haml
+++ b/app/views/projects/_stat_anchor_list.html.haml
@@ -1,8 +1,8 @@
- anchors = local_assigns.fetch(:anchors, [])
- return unless anchors.any?
-%ul.nav
+%ul.nav.justify-content-center
- anchors.each do |anchor|
- %li
- = link_to_if anchor.link, anchor.label, anchor.link, class: anchor.enabled ? 'stat-link' : "btn btn-#{anchor.class_modifier || 'missing'}" do
- %span.stat-text= anchor.label
+ %li.nav-item
+ = link_to_if anchor.link, anchor.label, anchor.link, class: anchor.enabled ? 'nav-link stat-link' : "nav-link btn btn-#{anchor.class_modifier || 'missing'}" do
+ .stat-text= anchor.label
diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml
index 7ff7466e561..87b165e581a 100644
--- a/app/views/projects/artifacts/browse.html.haml
+++ b/app/views/projects/artifacts/browse.html.haml
@@ -9,10 +9,10 @@
.tree-holder
.nav-block
%ul.breadcrumb.repo-breadcrumb
- %li
+ %li.breadcrumb-item
= link_to 'Artifacts', browse_project_job_artifacts_path(@project, @build)
- path_breadcrumbs do |title, path|
- %li
+ %li.breadcrumb-item
= link_to truncate(title, length: 40), browse_project_job_artifacts_path(@project, @build, path)
.tree-controls
diff --git a/app/views/projects/artifacts/file.html.haml b/app/views/projects/artifacts/file.html.haml
index 2942d618a42..aac7a1870df 100644
--- a/app/views/projects/artifacts/file.html.haml
+++ b/app/views/projects/artifacts/file.html.haml
@@ -5,11 +5,11 @@
.tree-holder
.nav-block
%ul.breadcrumb.repo-breadcrumb
- %li
+ %li.breadcrumb-item
= link_to 'Artifacts', browse_project_job_artifacts_path(@project, @build)
- path_breadcrumbs do |title, path|
- title = truncate(title, length: 40)
- %li
+ %li.breadcrumb-item
- if path == @path
= link_to file_project_job_artifacts_path(@project, @build, path) do
%strong= title
@@ -22,7 +22,7 @@
.js-file-title.file-title-flex-parent
= render 'projects/blob/header_content', blob: blob
- .file-actions.hidden-xs
+ .file-actions.d-none.d-sm-block
= render 'projects/blob/viewer_switcher', blob: blob
.btn-group{ role: "group" }<
diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml
index e45861ac08d..e90916e340d 100644
--- a/app/views/projects/blame/show.html.haml
+++ b/app/views/projects/blame/show.html.haml
@@ -22,7 +22,7 @@
.commit-row-title
%span.item-title.str-truncated-100
= link_to_markdown commit.title, project_commit_path(@project, commit.id), class: "cdark", title: commit.title
- .pull-right
+ .float-right
= link_to commit.short_id, project_commit_path(@project, commit), class: "commit-sha"
&nbsp;
.light
diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml
index 849716a679b..a4b1b496b69 100644
--- a/app/views/projects/blob/_blob.html.haml
+++ b/app/views/projects/blob/_blob.html.haml
@@ -1,6 +1,6 @@
= render "projects/blob/breadcrumb", blob: blob
-.info-well.hidden-xs
+.info-well.d-none.d-sm-block
.well-segment
%ul.blob-commit-info
= render 'projects/commits/commit', commit: @last_commit, project: @project, ref: @ref
diff --git a/app/views/projects/blob/_breadcrumb.html.haml b/app/views/projects/blob/_breadcrumb.html.haml
index 1c148de9678..a4fb5f6ba88 100644
--- a/app/views/projects/blob/_breadcrumb.html.haml
+++ b/app/views/projects/blob/_breadcrumb.html.haml
@@ -5,12 +5,12 @@
= render 'shared/ref_switcher', destination: 'blob', path: @path
%ul.breadcrumb.repo-breadcrumb
- %li
+ %li.breadcrumb-item
= link_to project_tree_path(@project, @ref) do
= @project.path
- path_breadcrumbs do |title, path|
- title = truncate(title, length: 40)
- %li
+ %li.breadcrumb-item
- if path == @path
= link_to project_blob_path(@project, tree_join(@ref, path)) do
%strong= title
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index c9fa90acd11..8560b72fe85 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -16,7 +16,7 @@
= text_field_tag 'file_name', params[:file_name], placeholder: "File name",
required: true, class: 'form-control new-file-name js-file-path-name-input'
- .pull-right.file-buttons
+ .float-right.file-buttons
= button_tag class: 'soft-wrap-toggle btn', type: 'button', tabindex: '-1' do
%span.no-wrap
= custom_icon('icon_no_wrap')
diff --git a/app/views/projects/blob/_header.html.haml b/app/views/projects/blob/_header.html.haml
index 1b150ec3e5c..0a0b3ce1d6f 100644
--- a/app/views/projects/blob/_header.html.haml
+++ b/app/views/projects/blob/_header.html.haml
@@ -11,6 +11,7 @@
= view_on_environment_button(@commit.sha, @path, @environment) if @environment
.btn-group{ role: "group" }<
+ = render_if_exists 'projects/blob/header_file_locks_link'
= edit_blob_button
= ide_edit_button
- if current_user
@@ -18,3 +19,4 @@
= delete_blob_link
= render 'projects/fork_suggestion'
+= render_if_exists 'projects/blob/header_file_locks', project: @project, path: @path
diff --git a/app/views/projects/blob/_header_content.html.haml b/app/views/projects/blob/_header_content.html.haml
index 5d457a50c49..4bef45932d0 100644
--- a/app/views/projects/blob/_header_content.html.haml
+++ b/app/views/projects/blob/_header_content.html.haml
@@ -10,4 +10,4 @@
= number_to_human_size(blob.raw_size)
- if blob.stored_externally? && blob.external_storage == :lfs
- %span.label.label-lfs.append-right-5 LFS
+ %span.badge.label-lfs.append-right-5 LFS
diff --git a/app/views/projects/blob/_new_dir.html.haml b/app/views/projects/blob/_new_dir.html.haml
index 48ff66900be..6f3a691518b 100644
--- a/app/views/projects/blob/_new_dir.html.haml
+++ b/app/views/projects/blob/_new_dir.html.haml
@@ -2,12 +2,13 @@
.modal-dialog.modal-lg
.modal-content
.modal-header
- %a.close{ href: "#", "data-dismiss" => "modal" } ×
%h3.page-title= _('Create New Directory')
+ %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
+ %span{ "aria-hidden": true } &times;
.modal-body
- = form_tag project_create_dir_path(@project, @id), method: :post, remote: false, class: 'form-horizontal js-create-dir-form js-quick-submit js-requires-input' do
- .form-group
- = label_tag :dir_name, _('Directory name'), class: 'control-label'
+ = form_tag project_create_dir_path(@project, @id), method: :post, remote: false, class: 'js-create-dir-form js-quick-submit js-requires-input' do
+ .form-group.row
+ = label_tag :dir_name, _('Directory name'), class: 'col-form-label col-sm-2'
.col-sm-10
= text_field_tag :dir_name, params[:dir_name], required: true, class: 'form-control'
diff --git a/app/views/projects/blob/_remove.html.haml b/app/views/projects/blob/_remove.html.haml
index 750bdef3308..f80bae5c88c 100644
--- a/app/views/projects/blob/_remove.html.haml
+++ b/app/views/projects/blob/_remove.html.haml
@@ -2,14 +2,15 @@
.modal-dialog
.modal-content
.modal-header
- %a.close{ href: "#", "data-dismiss" => "modal" } ×
%h3.page-title Delete #{@blob.name}
+ %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
+ %span{ "aria-hidden": true } &times;
.modal-body
- = form_tag project_blob_path(@project, @id), method: :delete, class: 'form-horizontal js-delete-blob-form js-quick-submit js-requires-input' do
+ = form_tag project_blob_path(@project, @id), method: :delete, class: 'js-delete-blob-form js-quick-submit js-requires-input' do
= render 'shared/new_commit_form', placeholder: "Delete #{@blob.name}"
- .form-group
- .col-sm-offset-2.col-sm-10
+ .form-group.row
+ .offset-sm-2.col-sm-10
= button_tag 'Delete file', class: 'btn btn-remove btn-remove-file'
= link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal"
diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml
index 182d02376bf..0a5c73c9037 100644
--- a/app/views/projects/blob/_upload.html.haml
+++ b/app/views/projects/blob/_upload.html.haml
@@ -2,10 +2,11 @@
.modal-dialog.modal-lg
.modal-content
.modal-header
- %a.close{ href: "#", "data-dismiss" => "modal" } &times;
%h3.page-title= title
+ %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
+ %span{ "aria-hidden": true } &times;
.modal-body
- = form_tag form_path, method: method, class: 'js-quick-submit js-upload-blob-form form-horizontal', data: { method: method } do
+ = form_tag form_path, method: method, class: 'js-quick-submit js-upload-blob-form', data: { method: method } do
.dropzone
.dropzone-previews.blob-upload-dropzone-previews
%p.dz-message.light
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index 9d90251ab66..27cf040da7c 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -15,7 +15,7 @@
Edit file
= render 'template_selectors'
.file-editor
- %ul.nav-links.no-bottom.js-edit-mode
+ %ul.nav-links.no-bottom.js-edit-mode.nav.nav-tabs
%li.active
= link_to '#editor' do
Write
@@ -24,7 +24,7 @@
= link_to '#preview', 'data-preview-url' => project_preview_blob_path(@project, @id) do
= editing_preview_title(@blob.name)
- = form_tag(project_update_blob_path(@project, @id), method: :put, class: 'form-horizontal js-quick-submit js-requires-input js-edit-blob-form', data: blob_editor_paths) do
+ = form_tag(project_update_blob_path(@project, @id), method: :put, class: 'js-quick-submit js-requires-input js-edit-blob-form', data: blob_editor_paths) do
= render 'projects/blob/editor', ref: @ref, path: @path, blob_data: @blob.data
= render 'shared/new_commit_form', placeholder: "Update #{@blob.name}"
= hidden_field_tag 'last_commit_sha', @last_commit_sha
diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml
index fa091d8f6ef..39442564a2b 100644
--- a/app/views/projects/blob/new.html.haml
+++ b/app/views/projects/blob/new.html.haml
@@ -7,7 +7,7 @@
New file
= render 'template_selectors'
.file-editor
- = form_tag(project_create_blob_path(@project, @id), method: :post, class: 'form-horizontal js-edit-blob-form js-new-blob-form js-quick-submit js-requires-input', data: blob_editor_paths) do
+ = form_tag(project_create_blob_path(@project, @id), method: :post, class: 'js-edit-blob-form js-new-blob-form js-quick-submit js-requires-input', data: blob_editor_paths) do
= render 'projects/blob/editor', ref: @ref
= render 'shared/new_commit_form', placeholder: "Add new file"
diff --git a/app/views/projects/blob/viewers/_download.html.haml b/app/views/projects/blob/viewers/_download.html.haml
index f9b1da05a00..fda4b9c92cd 100644
--- a/app/views/projects/blob/viewers/_download.html.haml
+++ b/app/views/projects/blob/viewers/_download.html.haml
@@ -1,5 +1,5 @@
.file-content.blob_file.blob-no-preview
- .center.render-error.vertical-center
+ .center.render-error
= link_to blob_raw_path do
%h1.light
= sprite_icon('download')
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 0e012b5a216..88f9b7dfc9f 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -12,13 +12,13 @@
= link_to project_tree_path(@project, branch.name), class: 'item-title str-truncated-100 ref-name prepend-left-8' do
= branch.name
- if branch.name == @repository.root_ref
- %span.label.label-primary.prepend-left-5 default
+ %span.badge.badge-primary.prepend-left-5 default
- elsif merged
- %span.label.label-info.has-tooltip.prepend-left-5{ title: s_('Branches|Merged into %{default_branch}') % { default_branch: @repository.root_ref } }
+ %span.badge.badge-info.has-tooltip.prepend-left-5{ title: s_('Branches|Merged into %{default_branch}') % { default_branch: @repository.root_ref } }
= s_('Branches|merged')
- if protected_branch?(@project, branch)
- %span.label.label-success.prepend-left-5
+ %span.badge.badge-success.prepend-left-5
= s_('Branches|protected')
.block-truncated
@@ -28,7 +28,7 @@
= s_('Branches|Cant find HEAD commit for this branch')
- if branch.name != @repository.root_ref
- .divergence-graph.hidden-xs{ title: s_('%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead') % { number_commits_behind: diverging_count_label(number_commits_behind),
+ .divergence-graph.d-none.d-md-block{ title: s_('%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead') % { number_commits_behind: diverging_count_label(number_commits_behind),
default_branch: @repository.root_ref,
number_commits_ahead: diverging_count_label(number_commits_ahead) } }
.graph-side
@@ -39,7 +39,7 @@
.bar.bar-ahead{ style: "width: #{number_commits_ahead * bar_graph_width_factor}%" }
%span.count.count-ahead= diverging_count_label(number_commits_ahead)
- .controls.hidden-xs<
+ .controls.d-none.d-md-block<
- if merge_project && create_mr_button?(@repository.root_ref, branch.name)
= link_to create_mr_path(@repository.root_ref, branch.name), class: 'btn btn-default' do
= _('Merge request')
@@ -72,7 +72,7 @@
- else
%button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip disabled",
disabled: true,
- title: s_('Branches|Only a project master or owner can delete a protected branch') }
+ title: s_('Branches|Only a project maintainer or owner can delete a protected branch') }
= icon("trash-o")
- else
= link_to project_branch_path(@project, branch.name),
diff --git a/app/views/projects/branches/_delete_protected_modal.html.haml b/app/views/projects/branches/_delete_protected_modal.html.haml
index e0008e322a0..8aa79d2d464 100644
--- a/app/views/projects/branches/_delete_protected_modal.html.haml
+++ b/app/views/projects/branches/_delete_protected_modal.html.haml
@@ -2,11 +2,12 @@
.modal-dialog
.modal-content
.modal-header
- %button.close{ data: { dismiss: 'modal' } } ×
%h3.page-title
- title_branch_name = capture do
%span.js-branch-name.ref-name>[branch name]
= s_("Branches|Delete protected branch '%{branch_name}'?").html_safe % { branch_name: title_branch_name }
+ %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
+ %span{ "aria-hidden": true } &times;
.modal-body
%p
diff --git a/app/views/projects/branches/_panel.html.haml b/app/views/projects/branches/_panel.html.haml
index 12e5a8e8d69..398f76d379a 100644
--- a/app/views/projects/branches/_panel.html.haml
+++ b/app/views/projects/branches/_panel.html.haml
@@ -7,13 +7,13 @@
- return unless branches.any?
-.panel.panel-default.prepend-top-10
- .panel-heading
- %h4.panel-title
+.card.prepend-top-10
+ .card-header
+ %h4.card-title
= panel_title
%ul.content-list.all-branches
- branches.first(overview_max_branches).each do |branch|
= render "projects/branches/branch", branch: branch, merged: project.repository.merged_to_root_ref?(branch)
- if branches.size > overview_max_branches
- .panel-footer.text-center
+ .card-footer.text-center
= link_to show_more_text, project_branches_filtered_path(project, state: state), id: "state-#{state}", data: { state: state }
diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index 5dcc72d8263..d6568c9f64a 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -3,7 +3,7 @@
%div{ class: container_class }
.top-area.adjust
- %ul.nav-links.issues-state-filters
+ %ul.nav-links.issues-state-filters.nav.nav-tabs
%li{ class: active_when(@mode == 'overview') }>
= link_to s_('Branches|Overview'), project_branches_path(@project), title: s_('Branches|Show overview of the branches')
@@ -26,7 +26,7 @@
%span.light
= branches_sort_options_hash[@sort]
= icon('chevron-down')
- %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
+ %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable
%li.dropdown-header
= s_('Branches|Sort by')
- branches_sort_options_hash.each do |value, title|
diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml
index c7fc5a98ca8..65b414c8af2 100644
--- a/app/views/projects/branches/new.html.haml
+++ b/app/views/projects/branches/new.html.haml
@@ -9,14 +9,14 @@
New Branch
%hr
-= form_tag namespace_project_branches_path, method: :post, id: "new-branch-form", class: "form-horizontal js-create-branch-form js-requires-input" do
- .form-group
- = label_tag :branch_name, nil, class: 'control-label'
+= form_tag namespace_project_branches_path, method: :post, id: "new-branch-form", class: "js-create-branch-form js-requires-input" do
+ .form-group.row
+ = label_tag :branch_name, nil, class: 'col-form-label col-sm-2'
.col-sm-10
= text_field_tag :branch_name, params[:branch_name], required: true, autofocus: true, class: 'form-control js-branch-name'
- .help-block.text-danger.js-branch-name-error
- .form-group
- = label_tag :ref, 'Create from', class: 'control-label'
+ .form-text.text-muted.text-danger.js-branch-name-error
+ .form-group.row
+ = label_tag :ref, 'Create from', class: 'col-form-label col-sm-2'
.col-sm-10.create-from
.dropdown
= hidden_field_tag :ref, default_ref
@@ -24,7 +24,7 @@
.text-left.dropdown-toggle-text= default_ref
= icon('chevron-down')
= render 'shared/ref_dropdown', dropdown_class: 'wide'
- .help-block Existing branch name, tag, or commit SHA
+ .form-text.text-muted Existing branch name, tag, or commit SHA
.form-actions
= button_tag 'Create branch', class: 'btn btn-create', tabindex: 3
= link_to 'Cancel', project_branches_path(@project), class: 'btn btn-cancel'
diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
index f49f6e630d2..f7551434d47 100644
--- a/app/views/projects/buttons/_download.html.haml
+++ b/app/views/projects/buttons/_download.html.haml
@@ -3,11 +3,11 @@
- if !project.empty_repo? && can?(current_user, :download_code, project)
- archive_prefix = "#{project.path}-#{ref.tr('/', '-')}"
.project-action-button.dropdown.inline>
- %button.btn.has-tooltip{ title: s_('DownloadSource|Download'), 'data-toggle' => 'dropdown', 'aria-label' => s_('DownloadSource|Download') }
+ %button.btn.has-tooltip{ title: s_('DownloadSource|Download'), 'data-toggle' => 'dropdown', 'aria-label' => s_('DownloadSource|Download'), 'data-display' => 'static' }
= sprite_icon('download')
= icon("caret-down")
%span.sr-only= _('Select Archive Format')
- %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' }
+ %ul.dropdown-menu.dropdown-menu-right{ role: 'menu' }
%li.dropdown-header
#{ _('Source code') }
%li
diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml
index 2e86a7d36d7..8b9c52f0802 100644
--- a/app/views/projects/buttons/_dropdown.html.haml
+++ b/app/views/projects/buttons/_dropdown.html.haml
@@ -8,10 +8,10 @@
- if show_menu
.project-action-button.dropdown.inline
- %a.btn.dropdown-toggle.has-tooltip{ href: '#', title: _('Create new...'), 'data-toggle' => 'dropdown', 'data-container' => 'body', 'aria-label' => _('Create new...') }
+ %a.btn.dropdown-toggle.has-tooltip{ href: '#', title: _('Create new...'), 'data-toggle' => 'dropdown', 'data-container' => 'body', 'aria-label' => _('Create new...'), 'data-display' => 'static' }
= icon('plus')
= icon("caret-down")
- %ul.dropdown-menu.dropdown-menu-align-right.project-home-dropdown
+ %ul.dropdown-menu.dropdown-menu-right.project-home-dropdown
- if can_create_issue || merge_project || can_create_project_snippet
%li.dropdown-header= _('This project')
diff --git a/app/views/projects/buttons/_xcode_link.html.haml b/app/views/projects/buttons/_xcode_link.html.haml
new file mode 100644
index 00000000000..a8b32fb0ef5
--- /dev/null
+++ b/app/views/projects/buttons/_xcode_link.html.haml
@@ -0,0 +1,2 @@
+%a.btn.btn-default{ href: xcode_uri_to_repo(@project) }
+ = _("Open in Xcode")
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 9126476e79e..44c1453e239 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -41,14 +41,14 @@
.label-container
- if job.tags.any?
- job.tags.each do |tag|
- %span.label.label-primary
+ %span.badge.badge-primary
= tag
- if job.try(:trigger_request)
- %span.label.label-info triggered
+ %span.badge.badge-info triggered
- if job.try(:allow_failure)
- %span.label.label-danger allowed to fail
+ %span.badge.badge-danger allowed to fail
- if job.action?
- %span.label.label-info manual
+ %span.badge.badge-info manual
- if pipeline_link
%td
@@ -93,7 +93,7 @@
#{job.coverage}%
%td
- .pull-right
+ .float-right
- if can?(current_user, :read_build, job) && job.artifacts?
= link_to download_project_job_artifacts_path(job.project, job), rel: 'nofollow', download: '', title: 'Download artifacts', class: 'btn btn-build' do
= sprite_icon('download')
diff --git a/app/views/projects/ci/lints/show.html.haml b/app/views/projects/ci/lints/show.html.haml
index 6ca8152183d..cbda6bf2107 100644
--- a/app/views/projects/ci/lints/show.html.haml
+++ b/app/views/projects/ci/lints/show.html.haml
@@ -3,22 +3,21 @@
- content_for :library_javascripts do
= page_specific_javascript_tag('lib/ace.js')
-%h2 Check your .gitlab-ci.yml
+%h2.pt-3.pb-3 Check your .gitlab-ci.yml
.project-ci-linter
- .row
- = form_tag project_ci_lint_path(@project), method: :post do
- .form-group
- .col-sm-12
- .file-holder
- .js-file-title.file-title.clearfix
- Content of .gitlab-ci.yml
- #ci-editor.ci-editor= @content
- = text_area_tag(:content, @content, class: 'hidden form-control span1', rows: 7, require: true)
+ = form_tag project_ci_lint_path(@project), method: :post do
+ .row
.col-sm-12
- .pull-left.prepend-top-10
+ .file-holder
+ .js-file-title.file-title.clearfix
+ Content of .gitlab-ci.yml
+ #ci-editor.ci-editor= @content
+ = text_area_tag(:content, @content, class: 'hidden form-control span1', rows: 7, require: true)
+ .col-sm-12
+ .float-left.prepend-top-10
= submit_tag('Validate', class: 'btn btn-success submit-yml')
- .pull-right.prepend-top-10
+ .float-right.prepend-top-10
= button_tag('Clear', type: 'button', class: 'btn btn-default clear-yml')
.row.prepend-top-20
diff --git a/app/views/projects/clusters/_advanced_settings.html.haml b/app/views/projects/clusters/_advanced_settings.html.haml
index 14979bee714..243e8cd9ba0 100644
--- a/app/views/projects/clusters/_advanced_settings.html.haml
+++ b/app/views/projects/clusters/_advanced_settings.html.haml
@@ -7,8 +7,8 @@
- link_gke = link_to(s_('ClusterIntegration|Google Kubernetes Engine'), @cluster.gke_cluster_url, target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|Manage your Kubernetes cluster by visiting %{link_gke}').html_safe % { link_gke: link_gke }
- .well.form-group
- %label.text-danger
+ .sub-section.form-group
+ %h4.text-danger
= s_('ClusterIntegration|Remove Kubernetes cluster integration')
%p
= s_("ClusterIntegration|Remove this Kubernetes cluster's configuration from this project. This will not delete your actual Kubernetes cluster.")
diff --git a/app/views/projects/clusters/_dropdown.html.haml b/app/views/projects/clusters/_dropdown.html.haml
deleted file mode 100644
index d55a9c60b64..00000000000
--- a/app/views/projects/clusters/_dropdown.html.haml
+++ /dev/null
@@ -1,12 +0,0 @@
-%h4.prepend-top-0= s_('ClusterIntegration|Choose how to set up Kubernetes cluster integration')
-
-.dropdown.clusters-dropdown
- %button.dropdown-menu-toggle.dropdown-menu-full-width{ type: 'button', data: { toggle: 'dropdown' }, 'aria-haspopup': true, 'aria-expanded': false }
- %span.dropdown-toggle-text
- = dropdown_text
- = icon('chevron-down')
- %ul.dropdown-menu.clusters-dropdown-menu.dropdown-menu-full-width
- %li
- = link_to(s_('ClusterIntegration|Create Kubernetes cluster on Google Kubernetes Engine'), gcp_new_namespace_project_clusters_path(@project.namespace, @project))
- %li
- = link_to(s_('ClusterIntegration|Add an existing Kubernetes cluster'), user_new_namespace_project_clusters_path(@project.namespace, @project))
diff --git a/app/views/projects/clusters/_empty_state.html.haml b/app/views/projects/clusters/_empty_state.html.haml
index 5f49d03b1bb..b8a3556a206 100644
--- a/app/views/projects/clusters/_empty_state.html.haml
+++ b/app/views/projects/clusters/_empty_state.html.haml
@@ -1,7 +1,7 @@
.row.empty-state
- .col-xs-12
+ .col-12
.svg-content= image_tag 'illustrations/clusters_empty.svg'
- .col-xs-12
+ .col-12
.text-content
%h4.text-center= s_('ClusterIntegration|Integrate Kubernetes cluster automation')
- link_to_help_page = link_to(_('Learn more about Kubernetes'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
diff --git a/app/views/projects/clusters/_gcp_signup_offer_banner.html.haml b/app/views/projects/clusters/_gcp_signup_offer_banner.html.haml
index d0402197821..9298d93663d 100644
--- a/app/views/projects/clusters/_gcp_signup_offer_banner.html.haml
+++ b/app/views/projects/clusters/_gcp_signup_offer_banner.html.haml
@@ -6,7 +6,7 @@
= image_tag 'illustrations/logos/google-cloud-platform_logo.svg'
.col-sm-10
%h4= s_('ClusterIntegration|Redeem up to $500 in free credit for Google Cloud Platform')
- %p= s_('ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for new GCP accounts to get started with GitLab\'s Google Kubernetes Engine Integration.').html_safe % { sign_up_link: link }
+ %p= s_('ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab\'s Google Kubernetes Engine Integration.').html_safe % { sign_up_link: link }
%a.btn.btn-info{ href: 'https://goo.gl/AaJzRW', target: '_blank', rel: 'noopener noreferrer' }
Apply for credit
diff --git a/app/views/projects/clusters/_integration_form.html.haml b/app/views/projects/clusters/_integration_form.html.haml
index db97203a2aa..b46b45fea49 100644
--- a/app/views/projects/clusters/_integration_form.html.haml
+++ b/app/views/projects/clusters/_integration_form.html.haml
@@ -1,6 +1,6 @@
= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field|
= form_errors(@cluster)
- .form-group.append-bottom-20
+ .form-group
%h5= s_('ClusterIntegration|Integration status')
%p
- if @cluster.enabled?
@@ -10,7 +10,7 @@
= s_('ClusterIntegration|Kubernetes cluster integration is enabled for this project.')
- else
= s_('ClusterIntegration|Kubernetes cluster integration is disabled for this project.')
- %label.append-bottom-10.js-cluster-enable-toggle-area
+ %label.append-bottom-0.js-cluster-enable-toggle-area
%button{ type: 'button',
class: "js-project-feature-toggle project-feature-toggle #{'is-checked' if @cluster.enabled?} #{'is-disabled' unless can?(current_user, :update_cluster, @cluster)}",
"aria-label": s_("ClusterIntegration|Toggle Kubernetes cluster"),
@@ -20,19 +20,26 @@
= sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked')
= sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked')
- .form-group
- %h5= s_('ClusterIntegration|Security')
- %p
- = s_("ClusterIntegration|The default cluster configuration grants access to a wide set of functionalities needed to successfully build and deploy a containerised application.")
- = link_to s_("ClusterIntegration|Learn more about security configuration"), help_page_path('user/project/clusters/index.md', anchor: 'security-implications')
-
- .form-group
- %h5= s_('ClusterIntegration|Environment scope')
- %p
- = s_("ClusterIntegration|Choose which of your project's environments will use this Kubernetes cluster.")
- = link_to s_("ClusterIntegration|Learn more about environments"), help_page_path('ci/environments')
- = field.text_field :environment_scope, class: 'form-control js-select-on-focus', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope')
+ - if has_multiple_clusters?(@project)
+ .form-group
+ %h5= s_('ClusterIntegration|Environment scope')
+ %p
+ = s_("ClusterIntegration|Choose which of your project's environments will use this Kubernetes cluster.")
+ = link_to s_("ClusterIntegration|Learn more about environments"), help_page_path('ci/environments')
+ = field.text_field :environment_scope, class: 'form-control js-select-on-focus', placeholder: s_('ClusterIntegration|Environment scope')
- if can?(current_user, :update_cluster, @cluster)
.form-group
= field.submit _('Save changes'), class: 'btn btn-success'
+
+ - unless has_multiple_clusters?(@project)
+ %h5= s_('ClusterIntegration|Environment scope')
+ %p
+ %code *
+ is the default environment scope for this cluster. This means that all jobs, regardless of their environment, will use this cluster.
+ = link_to 'More information', ('https://docs.gitlab.com/ee/user/project/clusters/#setting-the-environment-scope')
+
+ %h5= s_('ClusterIntegration|Security')
+ %p
+ = s_("ClusterIntegration|The default cluster configuration grants access to a wide set of functionalities needed to successfully build and deploy a containerised application.")
+ = link_to s_("ClusterIntegration|Learn more about security configuration"), help_page_path('user/project/clusters/index.md', anchor: 'security-implications')
diff --git a/app/views/projects/clusters/_sidebar.html.haml b/app/views/projects/clusters/_sidebar.html.haml
index 73cd7c50922..3d10348212f 100644
--- a/app/views/projects/clusters/_sidebar.html.haml
+++ b/app/views/projects/clusters/_sidebar.html.haml
@@ -1,7 +1,9 @@
+- clusters_help_url = help_page_path('user/project/clusters/index.md')
+- help_link_start = "<a href=\"%{url}\" target=\"_blank\" rel=\"noopener noreferrer\">".html_safe
+- help_link_end = '</a>'.html_safe
%h4.prepend-top-0
= s_('ClusterIntegration|Kubernetes cluster integration')
%p
= s_('ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way.')
%p
- - link = link_to(_('Kubernetes'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
- = s_('ClusterIntegration|Learn more about %{link_to_documentation}').html_safe % { link_to_documentation: link }
+ = s_('ClusterIntegration|Learn more about %{help_link_start}Kubernetes%{help_link_end}.').html_safe % { help_link_start: help_link_start % { url: clusters_help_url }, help_link_end: help_link_end }
diff --git a/app/views/projects/clusters/gcp/_form.html.haml b/app/views/projects/clusters/gcp/_form.html.haml
index 5739a57dcfe..0a2e320556d 100644
--- a/app/views/projects/clusters/gcp/_form.html.haml
+++ b/app/views/projects/clusters/gcp/_form.html.haml
@@ -1,35 +1,65 @@
+= javascript_include_tag 'https://apis.google.com/js/api.js'
+- external_link_icon = icon('external-link')
+- zones_link_url = 'https://cloud.google.com/compute/docs/regions-zones/regions-zones'
+- machine_type_link_url = 'https://cloud.google.com/compute/docs/machine-types'
+- pricing_link_url = 'https://cloud.google.com/compute/pricing#machinetype'
+- help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe
+- help_link_end = ' %{external_link_icon}</a>'.html_safe % { external_link_icon: external_link_icon }
+
%p
- link_to_help_page = link_to(s_('ClusterIntegration|help page'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration.').html_safe % { link_to_help_page: link_to_help_page}
-= form_for @cluster, html: { class: 'prepend-top-20' }, url: gcp_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field|
- = form_errors(@cluster)
+%p= link_to('Select a different Google account', @authorize_url)
+
+= form_for @gcp_cluster, html: { class: 'js-gke-cluster-creation prepend-top-20', data: { token: token_in_session } }, url: create_gcp_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field|
+ = form_errors(@gcp_cluster)
.form-group
- = field.label :name, s_('ClusterIntegration|Kubernetes cluster name')
+ = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-light'
= field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name')
.form-group
- = field.label :environment_scope, s_('ClusterIntegration|Environment scope')
+ = field.label :environment_scope, s_('ClusterIntegration|Environment scope'), class: 'label-light'
= field.text_field :environment_scope, class: 'form-control', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope')
- = field.fields_for :provider_gcp, @cluster.provider_gcp do |provider_gcp_field|
+ = field.fields_for :provider_gcp, @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', placeholder: s_('ClusterIntegration|Project ID')
+ = provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project'), class: 'label-light'
+ .js-gcp-project-id-dropdown-entry-point{ data: { docsUrl: 'https://console.cloud.google.com/home/dashboard' } }
+ = provider_gcp_field.hidden_field :gcp_project_id
+ .dropdown
+ %button.dropdown-menu-toggle.dropdown-menu-full-width{ type: 'button', disabled: true }
+ %span.dropdown-toggle-text
+ = _('Select project')
+ = icon('chevron-down')
+ %span.form-text.text-muted &nbsp;
.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'
+ = provider_gcp_field.label :zone, s_('ClusterIntegration|Zone'), class: 'label-light'
+ .js-gcp-zone-dropdown-entry-point
+ = provider_gcp_field.hidden_field :zone
+ .dropdown
+ %button.dropdown-menu-toggle.dropdown-menu-full-width{ type: 'button', disabled: true }
+ %span.dropdown-toggle-text
+ = _('Select project to choose zone')
+ = icon('chevron-down')
+ %p.form-text.text-muted
+ = s_('ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}.').html_safe % { help_link_start: help_link_start % { url: zones_link_url }, help_link_end: help_link_end }
.form-group
- = provider_gcp_field.label :num_nodes, s_('ClusterIntegration|Number of nodes')
+ = provider_gcp_field.label :num_nodes, s_('ClusterIntegration|Number of nodes'), class: 'label-light'
= provider_gcp_field.text_field :num_nodes, class: 'form-control', placeholder: '3'
.form-group
- = 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-4'
+ = provider_gcp_field.label :machine_type, s_('ClusterIntegration|Machine type'), class: 'label-light'
+ .js-gcp-machine-type-dropdown-entry-point
+ = provider_gcp_field.hidden_field :machine_type
+ .dropdown
+ %button.dropdown-menu-toggle.dropdown-menu-full-width{ type: 'button', disabled: true }
+ %span.dropdown-toggle-text
+ = _('Select project and zone to choose machine type')
+ = icon('chevron-down')
+ %p.form-text.text-muted
+ = s_('ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}.').html_safe % { help_link_start_machine_type: help_link_start % { url: machine_type_link_url }, help_link_start_pricing: help_link_start % { url: pricing_link_url }, help_link_end: help_link_end }
.form-group
- = field.submit s_('ClusterIntegration|Create Kubernetes cluster'), class: 'btn btn-success'
+ = field.submit s_('ClusterIntegration|Create Kubernetes cluster'), class: 'js-gke-cluster-creation-submit btn btn-success', disabled: true
diff --git a/app/views/projects/clusters/gcp/_header.html.haml b/app/views/projects/clusters/gcp/_header.html.haml
index fa989943492..a2ad3cd64df 100644
--- a/app/views/projects/clusters/gcp/_header.html.haml
+++ b/app/views/projects/clusters/gcp/_header.html.haml
@@ -1,4 +1,4 @@
-%h4.prepend-top-20
+%h4
= s_('ClusterIntegration|Enter the details for your Kubernetes cluster')
%p
= s_('ClusterIntegration|Please make sure that your Google account meets the following requirements:')
diff --git a/app/views/projects/clusters/gcp/_show.html.haml b/app/views/projects/clusters/gcp/_show.html.haml
index 78cd687ef93..877e0cc876c 100644
--- a/app/views/projects/clusters/gcp/_show.html.haml
+++ b/app/views/projects/clusters/gcp/_show.html.haml
@@ -3,8 +3,8 @@
= s_('ClusterIntegration|Kubernetes cluster name')
.input-group
%input.form-control.cluster-name.js-select-on-focus{ value: @cluster.name, readonly: true }
- %span.input-group-btn
- = clipboard_button(text: @cluster.name, title: s_('ClusterIntegration|Copy Kubernetes cluster name'), class: 'btn-default')
+ %span.input-group-append
+ = clipboard_button(text: @cluster.name, title: s_('ClusterIntegration|Copy Kubernetes cluster name'), class: 'input-group-text btn-default')
= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field|
= form_errors(@cluster)
@@ -14,22 +14,22 @@
= platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL')
.input-group
= platform_kubernetes_field.text_field :api_url, class: 'form-control js-select-on-focus', placeholder: s_('ClusterIntegration|API URL'), readonly: true
- %span.input-group-btn
- = clipboard_button(text: @cluster.platform_kubernetes.api_url, title: s_('ClusterIntegration|Copy API URL'), class: 'btn-default')
+ %span.input-group-append
+ = clipboard_button(text: @cluster.platform_kubernetes.api_url, title: s_('ClusterIntegration|Copy API URL'), class: 'input-group-text btn-default')
.form-group
= platform_kubernetes_field.label :ca_cert, s_('ClusterIntegration|CA Certificate')
.input-group
= platform_kubernetes_field.text_area :ca_cert, class: 'form-control js-select-on-focus', placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)'), readonly: true
- %span.input-group-addon.clipboard-addon
- = clipboard_button(text: @cluster.platform_kubernetes.ca_cert, title: s_('ClusterIntegration|Copy CA Certificate'), class: 'btn-blank')
+ %span.input-group-append.clipboard-addon
+ = clipboard_button(text: @cluster.platform_kubernetes.ca_cert, title: s_('ClusterIntegration|Copy CA Certificate'), class: 'input-group-text btn-blank')
.form-group
= platform_kubernetes_field.label :token, s_('ClusterIntegration|Token')
.input-group
= platform_kubernetes_field.text_field :token, class: 'form-control js-cluster-token js-select-on-focus', type: 'password', placeholder: s_('ClusterIntegration|Token'), readonly: true
- %span.input-group-btn
- %button.btn.btn-default.js-show-cluster-token{ type: 'button' }
+ %span.input-group-append
+ %button.btn.btn-default.input-group-text.js-show-cluster-token{ type: 'button' }
= s_('ClusterIntegration|Show')
= clipboard_button(text: @cluster.platform_kubernetes.token, title: s_('ClusterIntegration|Copy Token'), class: 'btn-default')
diff --git a/app/views/projects/clusters/gcp/login.html.haml b/app/views/projects/clusters/gcp/login.html.haml
deleted file mode 100644
index ff046c59a7a..00000000000
--- a/app/views/projects/clusters/gcp/login.html.haml
+++ /dev/null
@@ -1,21 +0,0 @@
-- breadcrumb_title 'Kubernetes'
-- page_title _("Login")
-
-= render_gcp_signup_offer
-
-.row.prepend-top-default
- .col-sm-4
- = render 'projects/clusters/sidebar'
- .col-sm-8
- = render 'projects/clusters/dropdown', dropdown_text: s_('ClusterIntegration|Create Kubernetes cluster on Google Kubernetes Engine')
- = render 'header'
-.row
- .col-sm-8.col-sm-offset-4.signin-with-google
- - if @authorize_url
- = link_to @authorize_url do
- = image_tag('auth_buttons/signin_with_google.png', width: '191px')
- = _('or')
- = link_to('create a new Google account', 'https://accounts.google.com/SignUpWithoutGmail?service=cloudconsole&continue=https%3A%2F%2Fconsole.cloud.google.com%2Ffreetrial%3Futm_campaign%3D2018_cpanel%26utm_source%3Dgitlab%26utm_medium%3Dreferral', target: '_blank', rel: 'noopener noreferrer')
- - 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/gcp/new.html.haml b/app/views/projects/clusters/gcp/new.html.haml
deleted file mode 100644
index ea78d66d883..00000000000
--- a/app/views/projects/clusters/gcp/new.html.haml
+++ /dev/null
@@ -1,10 +0,0 @@
-- breadcrumb_title 'Kubernetes'
-- page_title _("New Kubernetes Cluster")
-
-.row.prepend-top-default
- .col-sm-4
- = render 'projects/clusters/sidebar'
- .col-sm-8
- = render 'projects/clusters/dropdown', dropdown_text: s_('ClusterIntegration|Create Kubernetes cluster on Google Kubernetes Engine')
- = render 'header'
- = render 'form'
diff --git a/app/views/projects/clusters/new.html.haml b/app/views/projects/clusters/new.html.haml
index 828e2a84753..a38003f5750 100644
--- a/app/views/projects/clusters/new.html.haml
+++ b/app/views/projects/clusters/new.html.haml
@@ -1,15 +1,36 @@
- breadcrumb_title 'Kubernetes'
- page_title _("Kubernetes Cluster")
+- active_tab = local_assigns.fetch(:active_tab, 'gcp')
+= javascript_include_tag 'https://apis.google.com/js/api.js'
= render_gcp_signup_offer
.row.prepend-top-default
- .col-sm-4
+ .col-md-3
= render 'sidebar'
- .col-sm-8
- %h4.prepend-top-0= s_('ClusterIntegration|Choose how to set up Kubernetes cluster integration')
+ .col-md-9.js-toggle-container
+ %ul.nav-links.nav-tabs.gitlab-tabs.nav{ role: 'tablist' }
+ %li.nav-item{ role: 'presentation' }
+ %a.nav-link{ href: '#create-gcp-cluster-pane', id: 'create-gcp-cluster-tab', class: active_when(active_tab == 'gcp'), data: { toggle: 'tab' }, role: 'tab' }
+ %span Create new Cluster on GKE
+ %li.nav-item{ role: 'presentation' }
+ %a.nav-link{ href: '#add-user-cluster-pane', id: 'add-user-cluster-tab', class: active_when(active_tab == 'user'), data: { toggle: 'tab' }, role: 'tab' }
+ %span Add existing cluster
- %p= s_('ClusterIntegration|Create a new Kubernetes cluster on Google Kubernetes Engine right from GitLab')
- = link_to s_('ClusterIntegration|Create on Google Kubernetes Engine'), 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 Kubernetes cluster'), user_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20'
+ .tab-content.gitlab-tab-content
+ .tab-pane{ id: 'create-gcp-cluster-pane', class: active_when(active_tab == 'gcp'), role: 'tabpanel' }
+ = render 'projects/clusters/gcp/header'
+ - if @valid_gcp_token
+ = render 'projects/clusters/gcp/form'
+ - elsif @authorize_url
+ .signin-with-google
+ = link_to(image_tag('auth_buttons/signin_with_google.png', width: '191px'), @authorize_url)
+ = _('or')
+ = link_to('create a new Google account', 'https://accounts.google.com/SignUpWithoutGmail?service=cloudconsole&continue=https%3A%2F%2Fconsole.cloud.google.com%2Ffreetrial%3Futm_campaign%3D2018_cpanel%26utm_source%3Dgitlab%26utm_medium%3Dreferral', target: '_blank', rel: 'noopener noreferrer')
+ - 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 }
+
+ .tab-pane{ id: 'add-user-cluster-pane', class: active_when(active_tab == 'user'), role: 'tabpanel' }
+ = render 'projects/clusters/user/header'
+ = render 'projects/clusters/user/form'
diff --git a/app/views/projects/clusters/show.html.haml b/app/views/projects/clusters/show.html.haml
index 4c510293204..08d2deff6f8 100644
--- a/app/views/projects/clusters/show.html.haml
+++ b/app/views/projects/clusters/show.html.haml
@@ -11,6 +11,7 @@
install_ingress_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :ingress),
install_prometheus_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :prometheus),
install_runner_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :runner),
+ install_jupyter_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :jupyter),
toggle_status: @cluster.enabled? ? 'true': 'false',
cluster_status: @cluster.status_name,
cluster_status_reason: @cluster.status_reason,
diff --git a/app/views/projects/clusters/user/_form.html.haml b/app/views/projects/clusters/user/_form.html.haml
index 2e92524ce8f..3006bb5073e 100644
--- a/app/views/projects/clusters/user/_form.html.haml
+++ b/app/views/projects/clusters/user/_form.html.haml
@@ -1,27 +1,28 @@
-= form_for @cluster, url: user_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field|
- = form_errors(@cluster)
+= form_for @user_cluster, url: create_user_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field|
+ = form_errors(@user_cluster)
.form-group
- = field.label :name, s_('ClusterIntegration|Kubernetes cluster name')
+ = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-light'
= field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name')
- .form-group
- = field.label :environment_scope, s_('ClusterIntegration|Environment scope')
- = field.text_field :environment_scope, class: 'form-control', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope')
+ - if has_multiple_clusters?(@project)
+ .form-group
+ = field.label :environment_scope, s_('ClusterIntegration|Environment scope'), class: 'label-light'
+ = field.text_field :environment_scope, class: 'form-control', placeholder: s_('ClusterIntegration|Environment scope')
- = field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field|
+ = field.fields_for :platform_kubernetes, @user_cluster.platform_kubernetes do |platform_kubernetes_field|
.form-group
- = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL')
+ = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL'), class: 'label-light'
= platform_kubernetes_field.text_field :api_url, class: 'form-control', placeholder: s_('ClusterIntegration|API URL')
.form-group
- = platform_kubernetes_field.label :ca_cert, s_('ClusterIntegration|CA Certificate')
+ = platform_kubernetes_field.label :ca_cert, s_('ClusterIntegration|CA Certificate'), class: 'label-light'
= platform_kubernetes_field.text_area :ca_cert, class: 'form-control', placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)')
.form-group
- = platform_kubernetes_field.label :token, s_('ClusterIntegration|Token')
+ = platform_kubernetes_field.label :token, s_('ClusterIntegration|Token'), class: 'label-light'
= platform_kubernetes_field.text_field :token, class: 'form-control', placeholder: s_('ClusterIntegration|Service token'), autocomplete: 'off'
.form-group
- = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)')
+ = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-light'
= platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
.form-group
diff --git a/app/views/projects/clusters/user/_header.html.haml b/app/views/projects/clusters/user/_header.html.haml
index 37f6a788518..749177fa6c1 100644
--- a/app/views/projects/clusters/user/_header.html.haml
+++ b/app/views/projects/clusters/user/_header.html.haml
@@ -1,4 +1,4 @@
-%h4.prepend-top-20
+%h4
= s_('ClusterIntegration|Enter the details for your Kubernetes cluster')
%p
- link_to_help_page = link_to(s_('ClusterIntegration|documentation'), help_page_path('user/project/clusters/index', anchor: 'adding-an-existing-kubernetes-cluster'), target: '_blank', rel: 'noopener noreferrer')
diff --git a/app/views/projects/clusters/user/_show.html.haml b/app/views/projects/clusters/user/_show.html.haml
index ebbf7e775c7..4d117f435dc 100644
--- a/app/views/projects/clusters/user/_show.html.haml
+++ b/app/views/projects/clusters/user/_show.html.haml
@@ -1,28 +1,29 @@
= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field|
= form_errors(@cluster)
.form-group
- = field.label :name, s_('ClusterIntegration|Kubernetes cluster name')
+ = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-light'
= field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name')
= field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field|
.form-group
- = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL')
+ = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL'), class: 'label-light'
= platform_kubernetes_field.text_field :api_url, class: 'form-control', placeholder: s_('ClusterIntegration|API URL')
.form-group
- = platform_kubernetes_field.label :ca_cert, s_('ClusterIntegration|CA Certificate')
+ = platform_kubernetes_field.label :ca_cert, s_('ClusterIntegration|CA Certificate'), class: 'label-light'
= platform_kubernetes_field.text_area :ca_cert, class: 'form-control', placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)')
.form-group
- = platform_kubernetes_field.label :token, s_('ClusterIntegration|Token')
+ = platform_kubernetes_field.label :token, s_('ClusterIntegration|Token'), class: 'label-light'
.input-group
= platform_kubernetes_field.text_field :token, class: 'form-control js-cluster-token', type: 'password', placeholder: s_('ClusterIntegration|Token'), autocomplete: 'off'
- %span.input-group-addon.clipboard-addon
- %button.js-show-cluster-token.btn-blank{ type: 'button' }
- = s_('ClusterIntegration|Show')
+ %span.input-group-append.clipboard-addon
+ .input-group-text
+ %button.js-show-cluster-token.btn-blank{ type: 'button' }
+ = s_('ClusterIntegration|Show')
.form-group
- = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)')
+ = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-light'
= platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
.form-group
diff --git a/app/views/projects/clusters/user/new.html.haml b/app/views/projects/clusters/user/new.html.haml
deleted file mode 100644
index 7fb75cd9cc7..00000000000
--- a/app/views/projects/clusters/user/new.html.haml
+++ /dev/null
@@ -1,11 +0,0 @@
-- breadcrumb_title 'Kubernetes'
-- page_title _("New Kubernetes cluster")
-
-.row.prepend-top-default
- .col-sm-4
- = render 'projects/clusters/sidebar'
- .col-sm-8
- = render 'projects/clusters/dropdown', dropdown_text: s_('ClusterIntegration|Add an existing Kubernetes cluster')
- = render 'header'
- .prepend-top-20
- = render 'form'
diff --git a/app/views/projects/commit/_ajax_signature.html.haml b/app/views/projects/commit/_ajax_signature.html.haml
index 36b28c731a1..eb677cff5f0 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?
- %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 } }
+ %a{ href: 'javascript:void(0)', tabindex: 0, class: commit_signature_badge_classes('js-loading-gpg-badge'), data: { toggle: 'tooltip', placement: 'top', title: 'GPG signature (loading...)', 'commit-sha' => commit.sha } }
diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml
index 21e4664d4e4..3d97e93c9e9 100644
--- a/app/views/projects/commit/_change.html.haml
+++ b/app/views/projects/commit/_change.html.haml
@@ -15,22 +15,23 @@
.modal-dialog
.modal-content
.modal-header
- %a.close{ href: "#", "data-dismiss" => "modal" } ×
%h3.page-title= title
+ %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
+ %span{ "aria-hidden": true } &times;
.modal-body
- if description
- %p.append-bottom-20= description
- = form_tag [type.underscore, @project.namespace.becomes(Namespace), @project, commit], method: :post, remote: false, class: "form-horizontal js-#{type}-form js-requires-input" do
+ %p= description
+ = form_tag [type.underscore, @project.namespace.becomes(Namespace), @project, commit], method: :post, remote: false, class: "js-#{type}-form js-requires-input" do
.form-group.branch
- = label_tag 'start_branch', branch_label, class: 'control-label'
- .col-sm-10
- = hidden_field_tag :start_branch, @project.default_branch, id: 'start_branch'
- = dropdown_tag(@project.default_branch, options: { title: s_("BranchSwitcherTitle|Switch branch"), filter: true, placeholder: s_("BranchSwitcherPlaceholder|Search branches"), toggle_class: 'js-project-refs-dropdown dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "start_branch", selected: @project.default_branch, start_branch: @project.default_branch, refs_url: project_branches_path(@project), submit_form_on_click: false } })
+ = label_tag 'start_branch', branch_label, class: 'label-light'
- - if can?(current_user, :push_code, @project)
- = render 'shared/new_merge_request_checkbox'
- - else
- = hidden_field_tag 'create_merge_request', 1, id: nil
+ = hidden_field_tag :start_branch, @project.default_branch, id: 'start_branch'
+ = dropdown_tag(@project.default_branch, options: { title: s_("BranchSwitcherTitle|Switch branch"), filter: true, placeholder: s_("BranchSwitcherPlaceholder|Search branches"), toggle_class: 'js-project-refs-dropdown dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "start_branch", selected: @project.default_branch, start_branch: @project.default_branch, refs_url: project_branches_path(@project), submit_form_on_click: false } })
+
+ - if can?(current_user, :push_code, @project)
+ = render 'shared/new_merge_request_checkbox'
+ - else
+ = hidden_field_tag 'create_merge_request', 1, id: nil
.form-actions
= submit_tag label, class: 'btn btn-create'
= link_to _("Cancel"), '#', class: "btn btn-cancel", "data-dismiss" => "modal"
diff --git a/app/views/projects/commit/_ci_menu.html.haml b/app/views/projects/commit/_ci_menu.html.haml
index 7338468967f..f6666921a25 100644
--- a/app/views/projects/commit/_ci_menu.html.haml
+++ b/app/views/projects/commit/_ci_menu.html.haml
@@ -1,10 +1,10 @@
-%ul.nav-links.no-top.no-bottom.commit-ci-menu
+%ul.nav-links.no-top.no-bottom.commit-ci-menu.nav.nav-tabs
= nav_link(path: 'commit#show') do
= link_to project_commit_path(@project, @commit.id) do
Changes
- %span.badge= @diffs.size
+ %span.badge.badge-pill= @diffs.size
- if can?(current_user, :read_pipeline, @project)
= nav_link(path: 'commit#pipelines') do
= link_to pipelines_project_commit_path(@project, @commit.id) do
Pipelines
- %span.badge.js-pipelines-mr-count= @commit.pipelines.size
+ %span.badge.badge-pill.js-pipelines-mr-count= @commit.pipelines.size
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 1bffb3e8bf0..886dd73c33b 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -7,7 +7,7 @@
#{ s_('CommitBoxTitle|Commit') }
%span.commit-sha= @commit.short_id
= clipboard_button(text: @commit.id, title: _("Copy commit SHA to clipboard"))
- %span.hidden-xs authored
+ %span.d-none.d-sm-inline authored
#{time_ago_with_tooltip(@commit.authored_date)}
%span= s_('ByAuthor|by')
= author_avatar(@commit, size: 24)
@@ -21,17 +21,17 @@
.header-action-buttons
- if defined?(@notes_count) && @notes_count > 0
- %span.btn.disabled.btn-grouped.hidden-xs.append-right-10
+ %span.btn.disabled.btn-grouped.d-none.d-sm-block.append-right-10
= icon('comment')
= @notes_count
- = link_to project_tree_path(@project, @commit), class: "btn btn-default append-right-10 hidden-xs hidden-sm" do
+ = link_to project_tree_path(@project, @commit), class: "btn btn-default append-right-10 d-none d-sm-none d-md-inline" do
#{ _('Browse files') }
.dropdown.inline
%a.btn.btn-default.dropdown-toggle{ data: { toggle: "dropdown" } }
%span= _('Options')
= icon('caret-down')
- %ul.dropdown-menu.dropdown-menu-align-right
- %li.visible-xs-block.visible-sm-block
+ %ul.dropdown-menu.dropdown-menu-right
+ %li.d-block.d-sm-none.d-md-none
= link_to project_tree_path(@project, @commit) do
#{ _('Browse Files') }
- if can_collaborate && !@commit.has_been_reverted?(current_user)
diff --git a/app/views/projects/commit/_signature_badge.html.haml b/app/views/projects/commit/_signature_badge.html.haml
index aac020b42c5..c4d986ef742 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')
-%a{ href: 'javascript:void(0)', tabindex: 0, 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: '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 8611129b356..0b8e5105bc0 100644
--- a/app/views/projects/commit/branches.html.haml
+++ b/app/views/projects/commit/branches.html.haml
@@ -6,7 +6,8 @@
- if @branches.any? || @tags.any? || @tags_limit_exceeded
%span
- = link_to "…", "#", class: "js-details-expand label label-gray"
+ = link_to "#", class: "js-details-expand badge badge-gray ref-name" do
+ = sprite_icon('ellipsis_h', size: 12, css_class: 'vertical-align-middle')
%span.js-details-content.hide
= commit_branches_links(@project, @branches)
- if @tags_limit_exceeded
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index c390c9c4469..90e55fd0fb0 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -18,7 +18,7 @@
= cache(cache_key, expires_in: 1.day) do
%li.commit.flex-row.js-toggle-container{ id: "commit-#{commit.short_id}" }
- .avatar-cell.hidden-xs
+ .avatar-cell.d-none.d-sm-block
= author_avatar(commit, size: 36)
.commit-detail.flex-list
@@ -27,14 +27,15 @@
= link_to commit.title, project_commit_path(project, commit.id, merge_request_iid: merge_request.iid), class: "commit-row-message item-title"
- else
= link_to_markdown_field(commit, :title, link, class: "commit-row-message item-title")
- %span.commit-row-message.visible-xs-inline
+ %span.commit-row-message.d-block.d-sm-none
&middot;
= commit.short_id
- if commit.status(ref)
- .visible-xs-inline
+ .d-block.d-sm-none
= render_commit_status(commit, ref: ref)
- if commit.description?
- %button.text-expander.hidden-xs.js-toggle-button{ type: "button" } ...
+ %button.text-expander.js-toggle-button
+ = sprite_icon('ellipsis_h', size: 12)
.commiter
- commit_author_link = commit_author_link(commit, avatar: false, size: 24)
@@ -46,7 +47,7 @@
%pre.commit-row-description.js-toggle-content.prepend-top-8.append-bottom-8
= preserve(markdown_field(commit, :description))
- .commit-actions.flex-row.hidden-xs
+ .commit-actions.flex-row.d-none.d-sm-flex
- if request.xhr?
= render partial: 'projects/commit/signature', object: commit.signature
- else
diff --git a/app/views/projects/commits/_commit_list.html.haml b/app/views/projects/commits/_commit_list.html.haml
index 6f5835cb9be..8f8eb2c3d5a 100644
--- a/app/views/projects/commits/_commit_list.html.haml
+++ b/app/views/projects/commits/_commit_list.html.haml
@@ -1,8 +1,8 @@
- commits, hidden = limited_commits(@commits)
- commits = Commit.decorate(commits, @project)
-.panel.panel-default
- .panel-heading
+.card
+ .card-header
Commits (#{@commits.count})
- if hidden > 0
%ul.content-list
diff --git a/app/views/projects/commits/_inline_commit.html.haml b/app/views/projects/commits/_inline_commit.html.haml
index 26385d2f534..caaff082cc3 100644
--- a/app/views/projects/commits/_inline_commit.html.haml
+++ b/app/views/projects/commits/_inline_commit.html.haml
@@ -4,5 +4,5 @@
&nbsp;
%span.str-truncated
= link_to_markdown_field(commit, :title, project_commit_path(project, commit.id), class: "commit-row-message")
- .pull-right
+ .float-right
#{time_ago_with_tooltip(commit.committed_date)}
diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml
index 483cca11df9..9d254463fb6 100644
--- a/app/views/projects/commits/show.html.haml
+++ b/app/views/projects/commits/show.html.haml
@@ -15,7 +15,7 @@
%ul.breadcrumb.repo-breadcrumb
= commits_breadcrumbs
- .tree-controls.hidden-xs.hidden-sm
+ .tree-controls.d-none.d-sm-none.d-md-block
- if @merge_request.present?
.control
= link_to _("View open merge request"), project_merge_request_path(@project, @merge_request), class: 'btn'
diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml
index 40cdf96e76d..07112c98804 100644
--- a/app/views/projects/compare/_form.html.haml
+++ b/app/views/projects/compare/_form.html.haml
@@ -1,28 +1,29 @@
= form_tag project_compare_index_path(@project), method: :post, class: 'form-inline js-requires-input js-signature-container', data: { 'signatures-path' => signatures_namespace_project_compare_index_path } do
- .clearfix
- - if params[:to] && params[:from]
- .compare-switch-container
- = link_to icon('exchange'), { from: params[:to], to: params[:from] }, class: 'commits-compare-switch has-tooltip btn btn-white', title: 'Swap revisions'
- .form-group.dropdown.compare-form-group.to.js-compare-to-dropdown
- .input-group.inline-input-group
- %span.input-group-addon
+ - if params[:to] && params[:from]
+ .compare-switch-container
+ = link_to icon('exchange'), { from: params[:to], to: params[:from] }, class: 'commits-compare-switch has-tooltip btn btn-white', title: 'Swap revisions'
+ .form-group.dropdown.compare-form-group.to.js-compare-to-dropdown
+ .input-group.inline-input-group
+ %span.input-group-prepend
+ .input-group-text
= s_("CompareBranches|Source")
- = hidden_field_tag :to, params[:to]
- = button_tag type: 'button', title: params[:to], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip git-revision-dropdown-toggle", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to], field_name: :to } do
- .dropdown-toggle-text.str-truncated= params[:to] || _("Select branch/tag")
- = render 'shared/ref_dropdown'
- .compare-ellipsis.inline ...
- .form-group.dropdown.compare-form-group.from.js-compare-from-dropdown
- .input-group.inline-input-group
- %span.input-group-addon
+ = hidden_field_tag :to, params[:to]
+ = button_tag type: 'button', title: params[:to], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip git-revision-dropdown-toggle", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to], field_name: :to } do
+ .dropdown-toggle-text.str-truncated= params[:to] || _("Select branch/tag")
+ = render 'shared/ref_dropdown'
+ .compare-ellipsis.inline ...
+ .form-group.dropdown.compare-form-group.from.js-compare-from-dropdown
+ .input-group.inline-input-group
+ %span.input-group-prepend
+ .input-group-text
= s_("CompareBranches|Target")
- = hidden_field_tag :from, params[:from]
- = button_tag type: 'button', title: params[:from], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip git-revision-dropdown-toggle", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do
- .dropdown-toggle-text.str-truncated= params[:from] || _("Select branch/tag")
- = render 'shared/ref_dropdown'
- &nbsp;
- = button_tag s_("CompareBranches|Compare"), class: "btn btn-create commits-compare-btn"
- - if @merge_request.present?
- = link_to _("View open merge request"), project_merge_request_path(@project, @merge_request), class: 'prepend-left-10 btn'
- - elsif create_mr_button?
- = link_to _("Create merge request"), create_mr_path, class: 'prepend-left-10 btn'
+ = hidden_field_tag :from, params[:from]
+ = button_tag type: 'button', title: params[:from], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip git-revision-dropdown-toggle", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do
+ .dropdown-toggle-text.str-truncated= params[:from] || _("Select branch/tag")
+ = render 'shared/ref_dropdown'
+ &nbsp;
+ = button_tag s_("CompareBranches|Compare"), class: "btn btn-create commits-compare-btn"
+ - if @merge_request.present?
+ = link_to _("View open merge request"), project_merge_request_path(@project, @merge_request), class: 'prepend-left-10 btn'
+ - elsif create_mr_button?
+ = link_to _("Create merge request"), create_mr_path, class: 'prepend-left-10 btn'
diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml
index 8da55664878..b6bebbabed0 100644
--- a/app/views/projects/compare/show.html.haml
+++ b/app/views/projects/compare/show.html.haml
@@ -10,7 +10,7 @@
= render "projects/commits/commit_list"
= render "projects/diffs/diffs", diffs: @diffs, environment: @environment
- else
- .light-well
+ .card.bg-light
.center
%h4
= s_("CompareBranches|There isn't anything to compare.")
diff --git a/app/views/projects/cycle_analytics/_overview.html.haml b/app/views/projects/cycle_analytics/_overview.html.haml
index 9007f2c24ba..5b0d73b8c68 100644
--- a/app/views/projects/cycle_analytics/_overview.html.haml
+++ b/app/views/projects/cycle_analytics/_overview.html.haml
@@ -1,7 +1,7 @@
.cycle-analytics-overview
.container
.row
- .col-md-10.col-md-offset-1
+ .col-md-10.offset-md-1
.row.overview-details
.col-md-6.overview-text
%h4 Introducing Cycle Analytics
diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml
index 5041f322612..bdf021fd87f 100644
--- a/app/views/projects/cycle_analytics/show.html.haml
+++ b/app/views/projects/cycle_analytics/show.html.haml
@@ -8,21 +8,21 @@
"v-on:dismiss-overview-dialog" => "dismissOverviewDialog()" }
= icon("spinner spin", "v-show" => "isLoading")
.wrapper{ "v-show" => "!isLoading && !hasError" }
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
{{ __('Pipeline Health') }}
.content-block
.container-fluid
.row
- .col-sm-3.col-xs-12.column{ "v-for" => "item in state.summary" }
+ .col-sm-3.col-12.column{ "v-for" => "item in state.summary" }
%h3.header {{ item.value }}
%p.text {{ item.title }}
- .col-sm-3.col-xs-12.column
+ .col-sm-3.col-12.column
.dropdown.inline.js-ca-dropdown
%button.dropdown-menu-toggle{ "data-toggle" => "dropdown", :type => "button" }
%span.dropdown-label {{ n__('Last %d day', 'Last %d days', 30) }}
%i.fa.fa-chevron-down
- %ul.dropdown-menu.dropdown-menu-align-right
+ %ul.dropdown-menu.dropdown-menu-right
%li
%a{ "href" => "#", "data-value" => "7" }
{{ n__('Last %d day', 'Last %d days', 7) }}
@@ -33,8 +33,8 @@
%a{ "href" => "#", "data-value" => "90" }
{{ n__('Last %d day', 'Last %d days', 90) }}
.stage-panel-container
- .panel.panel-default.stage-panel
- .panel-heading
+ .card.stage-panel
+ .card-header
%nav.col-headers
%ul
%li.stage-header
diff --git a/app/views/projects/deploy_keys/_form.html.haml b/app/views/projects/deploy_keys/_form.html.haml
index c363180d0db..5ad8091a02b 100644
--- a/app/views/projects/deploy_keys/_form.html.haml
+++ b/app/views/projects/deploy_keys/_form.html.haml
@@ -1,24 +1,24 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @deploy_keys.new_key], url: namespace_project_deploy_keys_path, html: { class: "js-requires-input" } do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @deploy_keys.new_key], url: namespace_project_deploy_keys_path, html: { class: "js-requires-input container" } do |f|
= form_errors(@deploy_keys.new_key)
- .form-group
+ .form-group.row
= f.label :title, class: "label-light"
= f.text_field :title, class: 'form-control', required: true
- .form-group
+ .form-group.row
= f.label :key, class: "label-light"
= f.text_area :key, class: "form-control", rows: 5, required: true
- .form-group
+ .form-group.row
%p.light.append-bottom-0
Paste a machine public key here. Read more about how to generate it
= link_to "here", help_page_path("ssh/README")
= f.fields_for :deploy_keys_projects do |deploy_keys_project_form|
- .form-group
- .checkbox
- = deploy_keys_project_form.label :can_push do
- = deploy_keys_project_form.check_box :can_push
- %strong Write access allowed
- .form-group
+ .form-group.row
+ = deploy_keys_project_form.label :can_push do
+ = deploy_keys_project_form.check_box :can_push
+ %strong Write access allowed
+ .form-group.row
%p.light.append-bottom-0
Allow this key to push to repository as well? (Default only allows pull access.)
- = f.submit "Add key", class: "btn-create btn"
+ .form-group.row
+ = f.submit "Add key", class: "btn-create btn"
diff --git a/app/views/projects/deploy_keys/_index.html.haml b/app/views/projects/deploy_keys/_index.html.haml
index 6af57d3ab26..fb1ea471dec 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.no-animate{ class: ('expanded' if expanded) }
+%section.qa-deploy-keys-settings.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
Deploy Keys
diff --git a/app/views/projects/deploy_keys/edit.html.haml b/app/views/projects/deploy_keys/edit.html.haml
index cd910b82b57..e009b6fef0e 100644
--- a/app/views/projects/deploy_keys/edit.html.haml
+++ b/app/views/projects/deploy_keys/edit.html.haml
@@ -3,7 +3,7 @@
%hr
%div
- = form_for [@project.namespace.becomes(Namespace), @project, @deploy_key], html: { class: 'form-horizontal js-requires-input' } do |f|
+ = form_for [@project.namespace.becomes(Namespace), @project, @deploy_key], html: { class: 'js-requires-input' } do |f|
= render partial: 'shared/deploy_keys/form', locals: { form: f, deploy_key: @deploy_key }
.form-actions
= f.submit 'Save changes', class: 'btn-save btn'
diff --git a/app/views/projects/deploy_tokens/_index.html.haml b/app/views/projects/deploy_tokens/_index.html.haml
index 50e5950ced4..33faab0c510 100644
--- a/app/views/projects/deploy_tokens/_index.html.haml
+++ b/app/views/projects/deploy_tokens/_index.html.haml
@@ -1,6 +1,6 @@
- expanded = expand_deploy_tokens_section?(@new_deploy_token)
-%section.settings.no-animate{ class: ('expanded' if expanded) }
+%section.settings.no-animate#js-deploy-tokens{ class: ('expanded' if expanded) }
.settings-header
%h4= s_('DeployTokens|Deploy Tokens')
%button.btn.js-settings-toggle.qa-expand-deploy-keys{ type: 'button' }
@@ -10,9 +10,8 @@
.settings-content
- if @new_deploy_token.persisted?
= render 'projects/deploy_tokens/new_deploy_token', deploy_token: @new_deploy_token
- - else
- %h5.prepend-top-0
- = s_('DeployTokens|Add a deploy token')
- = render 'projects/deploy_tokens/form', project: @project, token: @new_deploy_token, presenter: @deploy_tokens
- %hr
+ %h5.prepend-top-0
+ = s_('DeployTokens|Add a deploy token')
+ = render 'projects/deploy_tokens/form', project: @project, token: @new_deploy_token, presenter: @deploy_tokens
+ %hr
= render 'projects/deploy_tokens/table', project: @project, active_tokens: @deploy_tokens
diff --git a/app/views/projects/deploy_tokens/_new_deploy_token.html.haml b/app/views/projects/deploy_tokens/_new_deploy_token.html.haml
index 1e715681e59..5dd9ffba074 100644
--- a/app/views/projects/deploy_tokens/_new_deploy_token.html.haml
+++ b/app/views/projects/deploy_tokens/_new_deploy_token.html.haml
@@ -1,14 +1,18 @@
-.created-deploy-token-container
- %h5.prepend-top-0
- = s_('DeployTokens|Your New Deploy Token')
+.created-deploy-token-container.info-well
+ .well-segment
+ %h5.prepend-top-0
+ = s_('DeployTokens|Your New Deploy Token')
- .form-group
- = text_field_tag 'deploy-token-user', deploy_token.username, readonly: true, class: 'deploy-token-field form-control js-select-on-focus'
- = clipboard_button(text: deploy_token.username, title: s_('DeployTokens|Copy username to clipboard'), placement: 'left')
- %span.deploy-token-help-block.prepend-top-5.text-success= s_("DeployTokens|Use this username as a login.")
+ .form-group
+ .input-group
+ = text_field_tag 'deploy-token-user', deploy_token.username, readonly: true, class: 'deploy-token-field form-control js-select-on-focus'
+ .input-group-append
+ = clipboard_button(text: deploy_token.username, title: s_('DeployTokens|Copy username to clipboard'), placement: 'left')
+ %span.deploy-token-help-block.prepend-top-5.text-success= s_("DeployTokens|Use this username as a login.")
- .form-group
- = text_field_tag 'deploy-token', deploy_token.token, readonly: true, class: 'deploy-token-field form-control js-select-on-focus'
- = clipboard_button(text: deploy_token.token, title: s_('DeployTokens|Copy deploy token to clipboard'), placement: 'left')
- %span.deploy-token-help-block.prepend-top-5.text-danger= s_("DeployTokens|Use this token as a password. Make sure you save it - you won't be able to access it again.")
-%hr
+ .form-group
+ .input-group
+ = text_field_tag 'deploy-token', deploy_token.token, readonly: true, class: 'deploy-token-field form-control js-select-on-focus'
+ .input-group-append
+ = clipboard_button(text: deploy_token.token, title: s_('DeployTokens|Copy deploy token to clipboard'), placement: 'left')
+ %span.deploy-token-help-block.prepend-top-5.text-danger= s_("DeployTokens|Use this token as a password. Make sure you save it - you won't be able to access it again.")
diff --git a/app/views/projects/deploy_tokens/_revoke_modal.html.haml b/app/views/projects/deploy_tokens/_revoke_modal.html.haml
index 085964fe22e..35eacae2c2e 100644
--- a/app/views/projects/deploy_tokens/_revoke_modal.html.haml
+++ b/app/views/projects/deploy_tokens/_revoke_modal.html.haml
@@ -1,12 +1,12 @@
-.modal{ id: "revoke-modal-#{token.id}" }
+.modal{ id: "revoke-modal-#{token.id}", tabindex: -1 }
.modal-dialog
.modal-content
.modal-header
- %h4.modal-title.pull-left
+ %h4.modal-title
= s_('DeployTokens|Revoke')
%b #{token.name}?
- %button.close{ 'aria-label' => _('Close'), 'data-dismiss' => 'modal', type: 'button' }
- %span{ 'aria-hidden' => 'true' } &times;
+ %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
+ %span{ "aria-hidden": true } &times;
.modal-body
%p
= s_('DeployTokens|You are about to revoke')
diff --git a/app/views/projects/deploy_tokens/_table.html.haml b/app/views/projects/deploy_tokens/_table.html.haml
index 5013a9b250d..91466a6736b 100644
--- a/app/views/projects/deploy_tokens/_table.html.haml
+++ b/app/views/projects/deploy_tokens/_table.html.haml
@@ -24,7 +24,7 @@
- else
%span.token-never-expires-label Never
%td= token.scopes.present? ? token.scopes.join(", ") : "<no scopes selected>"
- %td= link_to s_('DeployTokens|Revoke'), "#", class: "btn btn-danger pull-right", data: { toggle: "modal", target: "#revoke-modal-#{token.id}"}
+ %td= link_to s_('DeployTokens|Revoke'), "#", class: "btn btn-danger float-right", data: { toggle: "modal", target: "#revoke-modal-#{token.id}"}
= render 'projects/deploy_tokens/revoke_modal', token: token, project: project
- else
.settings-message.text-center
diff --git a/app/views/projects/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml
index e2baaa625ae..f4c91377ecb 100644
--- a/app/views/projects/deployments/_actions.haml
+++ b/app/views/projects/deployments/_actions.haml
@@ -3,13 +3,12 @@
- if actions.present?
.btn-group
.dropdown
- %button.dropdown.dropdown-new.btn.btn-default{ type: 'button', 'data-toggle' => 'dropdown' }
- = custom_icon('icon_play')
+ %button.dropdown.dropdown-new.btn.btn-default.has-tooltip{ type: 'button', 'data-toggle' => 'dropdown', title: s_('Environments|Deploy to...') }
+ = sprite_icon('play')
= icon('caret-down')
- %ul.dropdown-menu.dropdown-menu-align-right
+ %ul.dropdown-menu.dropdown-menu-right
- actions.each do |action|
- next unless can?(current_user, :update_build, action)
%li
- = link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do
- = custom_icon('icon_play')
+ = link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow', class: 'btn' do
%span= action.name.humanize
diff --git a/app/views/projects/deployments/_commit.html.haml b/app/views/projects/deployments/_commit.html.haml
index c7ac687e4a6..282566eeadc 100644
--- a/app/views/projects/deployments/_commit.html.haml
+++ b/app/views/projects/deployments/_commit.html.haml
@@ -14,4 +14,4 @@
= author_avatar(deployment.commit, size: 20)
= link_to_markdown commit_title, project_commit_path(@project, deployment.sha), class: "commit-row-message"
- else
- Cant find HEAD commit for this branch
+ = _("Can't find HEAD commit for this branch")
diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml
index 520696b01c6..85bc8ec07e3 100644
--- a/app/views/projects/deployments/_deployment.html.haml
+++ b/app/views/projects/deployments/_deployment.html.haml
@@ -1,14 +1,14 @@
.gl-responsive-table-row.deployment{ role: 'row' }
.table-section.section-10{ role: 'gridcell' }
- .table-mobile-header{ role: 'rowheader' } ID
+ .table-mobile-header{ role: 'rowheader' }= _("ID")
%strong.table-mobile-content ##{deployment.iid}
.table-section.section-30{ role: 'gridcell' }
- .table-mobile-header{ role: 'rowheader' } Commit
+ .table-mobile-header{ role: 'rowheader' }= _("Commit")
= render 'projects/deployments/commit', deployment: deployment
.table-section.section-25.build-column{ role: 'gridcell' }
- .table-mobile-header{ role: 'rowheader' } Job
+ .table-mobile-header{ role: 'rowheader' }= _("Job")
- if deployment.deployable
.table-mobile-content
.flex-truncate-parent
@@ -21,7 +21,7 @@
= user_avatar(user: deployment.user, size: 20)
.table-section.section-15{ role: 'gridcell' }
- .table-mobile-header{ role: 'rowheader' } Created
+ .table-mobile-header{ role: 'rowheader' }= _("Created")
%span.table-mobile-content= time_ago_with_tooltip(deployment.created_at)
.table-section.section-20.table-button-footer{ role: 'gridcell' }
diff --git a/app/views/projects/deployments/_rollback.haml b/app/views/projects/deployments/_rollback.haml
index 5941e01c6f1..281e042c915 100644
--- a/app/views/projects/deployments/_rollback.haml
+++ b/app/views/projects/deployments/_rollback.haml
@@ -1,6 +1,7 @@
- if can?(current_user, :create_deployment, deployment) && deployment.deployable
- = link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build' do
+ - tooltip = deployment.last? ? s_('Environments|Re-deploy to environment') : s_('Environments|Rollback environment')
+ = link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build has-tooltip', title: tooltip do
- if deployment.last?
- Re-deploy
+ = sprite_icon('repeat')
- else
- Rollback
+ = sprite_icon('redo')
diff --git a/app/views/projects/diffs/_collapsed.html.haml b/app/views/projects/diffs/_collapsed.html.haml
index 5762f4d86d7..9bd1255fe00 100644
--- a/app/views/projects/diffs/_collapsed.html.haml
+++ b/app/views/projects/diffs/_collapsed.html.haml
@@ -2,4 +2,4 @@
- url = url_for(safe_params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path, file_identifier: diff_file.file_identifier))
.nothing-here-block.diff-collapsed{ data: { diff_for_path: url } }
This diff is collapsed.
- %a.click-to-expand Click to expand it.
+ %button.click-to-expand.btn.btn-link Click to expand it.
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index 9f420ee86f7..077c6c68f7e 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -6,16 +6,16 @@
.content-block.oneline-block.files-changed.diff-files-changed.js-diff-files-changed{ class: ("diff-files-changed-merge-request" if merge_request) }
.files-changed-inner
- .inline-parallel-buttons.hidden-xs.hidden-sm
+ .inline-parallel-buttons.d-none.d-sm-none.d-md-block
- if !diffs_expanded? && diff_files.any? { |diff_file| diff_file.collapsed? }
= link_to 'Expand all', url_for(safe_params.merge(expanded: 1, format: nil)), class: 'btn btn-default'
- if show_whitespace_toggle
- if current_controller?(:commit)
- = commit_diff_whitespace_link(diffs.project, @commit, class: 'hidden-xs')
+ = commit_diff_whitespace_link(diffs.project, @commit, class: 'd-none d-sm-inline-block')
- elsif current_controller?('projects/merge_requests/diffs')
- = diff_merge_request_whitespace_link(diffs.project, @merge_request, class: 'hidden-xs')
+ = diff_merge_request_whitespace_link(diffs.project, @merge_request, class: 'd-none d-sm-inline-block')
- elsif current_controller?(:compare)
- = diff_compare_whitespace_link(diffs.project, params[:from], params[:to], class: 'hidden-xs')
+ = diff_compare_whitespace_link(diffs.project, params[:from], params[:to], class: 'd-none d-sm-inline-block')
.btn-group
= inline_diff_btn
= parallel_diff_btn
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index 47bfcb21cf4..b4df654c839 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -10,7 +10,7 @@
- unless diff_file.submodule?
- blob = diff_file.blob
- .file-actions.hidden-xs
+ .file-actions.d-none.d-sm-block
- if blob&.readable_text?
= link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip', title: "Toggle comments for this file", disabled: @diff_notes_disabled do
= icon('comment')
diff --git a/app/views/projects/diffs/_file_header.html.haml b/app/views/projects/diffs/_file_header.html.haml
index dbeddf6689a..4cb04d744dc 100644
--- a/app/views/projects/diffs/_file_header.html.haml
+++ b/app/views/projects/diffs/_file_header.html.haml
@@ -37,4 +37,4 @@
#{diff_file.a_mode} → #{diff_file.b_mode}
- if diff_file.stored_externally? && diff_file.external_storage == :lfs
- %span.label.label-lfs.append-right-5 LFS
+ %span.badge.label-lfs.append-right-5 LFS
diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml
index 6fd6018dea3..aa1112c3313 100644
--- a/app/views/projects/diffs/_stats.html.haml
+++ b/app/views/projects/diffs/_stats.html.haml
@@ -2,7 +2,7 @@
- sum_removed_lines = diff_files.sum(&:removed_lines)
.commit-stat-summary.dropdown
Showing
- %button.diff-stats-summary-toggler.js-diff-stats-dropdown{ type: "button", data: { toggle: "dropdown" } }<
+ %button.diff-stats-summary-toggler.js-diff-stats-dropdown{ type: "button", data: { toggle: "dropdown", display: "static" } }<
= pluralize(diff_files.size, "changed file")
= icon("caret-down", class: "prepend-left-5")
%span.diff-stats-additions-deletions-expanded#diff-stats
@@ -10,7 +10,7 @@
%strong.cgreen= pluralize(sum_added_lines, 'addition')
and
%strong.cred= pluralize(sum_removed_lines, 'deletion')
- .diff-stats-additions-deletions-collapsed.pull-right.hidden-xs.hidden-sm{ "aria-hidden": "true", "aria-describedby": "diff-stats" }
+ .diff-stats-additions-deletions-collapsed.float-right.d-none.d-sm-none{ "aria-hidden": "true", "aria-describedby": "diff-stats" }
%strong.cgreen<
+#{sum_added_lines}
%strong.cred<
diff --git a/app/views/projects/diffs/_warning.html.haml b/app/views/projects/diffs/_warning.html.haml
index da34a83d8e0..abe494f2974 100644
--- a/app/views/projects/diffs/_warning.html.haml
+++ b/app/views/projects/diffs/_warning.html.haml
@@ -1,7 +1,7 @@
.alert.alert-warning
%h4
Too many changes to show.
- .pull-right
+ .float-right
- if current_controller?(:commit)
= link_to "Plain diff", project_commit_path(@project, @commit, format: :diff), class: "btn btn-sm"
= link_to "Email patch", project_commit_path(@project, @commit, format: :patch), class: "btn btn-sm"
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 0994498c6be..c2d900cbcf7 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -4,10 +4,10 @@
- expanded = Rails.env.test?
.project-edit-container
- %section.settings.general-settings.no-animate{ class: ('expanded' if expanded) }
+ %section.settings.general-settings.no-animate#js-general-project-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
- General project settings
+ General project
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
@@ -33,14 +33,19 @@
%span.light (optional)
= f.text_area :description, class: "form-control", rows: 3, maxlength: 250
+ = render_if_exists 'projects/classification_policy_settings', f: f
+
- unless @project.empty_repo?
.form-group
= f.label :default_branch, "Default Branch", class: 'label-light'
= f.select(:default_branch, @project.repository.branch_names, {}, {class: 'select2 select-wide'})
+
+ = render_if_exists 'shared/repository_size_limit_setting', form: f, type: :project
+
.form-group
= f.label :tag_list, "Tags", class: 'label-light'
= f.text_field :tag_list, value: @project.tag_list.sort.join(', '), maxlength: 2000, class: "form-control"
- %p.help-block Separate tags with commas.
+ %p.form-text.text-muted Separate tags with commas.
%fieldset.features
%h5.prepend-top-0= _("Project avatar")
.form-group
@@ -54,13 +59,13 @@
%button.btn.js-choose-project-avatar-button{ type: 'button' }= _("Choose file...")
%span.file_name.prepend-left-default.js-avatar-filename= _("No file chosen")
= f.file_field :avatar, class: "js-project-avatar-input hidden"
- .help-block= _("The maximum file size allowed is 200KB.")
+ .form-text.text-muted= _("The maximum file size allowed is 200KB.")
- if @project.avatar?
%hr
= link_to _('Remove avatar'), project_avatar_path(@project), data: { confirm: _("Avatar will be removed. Are you sure?") }, method: :delete, class: "btn btn-danger btn-inverted"
= f.submit 'Save changes', class: "btn btn-success js-btn-save-general-project-settings"
- %section.settings.sharing-permissions.no-animate{ class: ('expanded' if expanded) }
+ %section.settings.sharing-permissions.no-animate#js-shared-permissions{ class: ('expanded' if expanded) }
.settings-header
%h4
Permissions
@@ -75,25 +80,31 @@
.js-project-permissions-form
= f.submit 'Save changes', class: "btn btn-save"
- %section.settings.merge-requests-feature.no-animate{ class: [('expanded' if expanded), ('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)] }
+ = render_if_exists 'projects/issues_settings'
+
+ %section.qa-merge-request-settings.settings.merge-requests-feature.no-animate#js-merge-request-settings{ class: [('expanded' if expanded), ('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)] }
.settings-header
%h4
- Merge request settings
+ Merge request
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Customize your merge request restrictions.
.settings-content
+ = render_if_exists 'shared/promotions/promote_mr_features'
+
= 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
+ = render 'projects/merge_request_settings', form: f
= f.submit 'Save changes', class: "btn btn-save qa-save-merge-request-changes"
+ = render_if_exists 'projects/service_desk_settings'
+
= render 'export', project: @project
- %section.settings.advanced-settings.no-animate{ class: ('expanded' if expanded) }
+ %section.qa-advanced-settings.settings.advanced-settings.no-animate#js-project-advanced-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
- Advanced settings
+ Advanced
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
@@ -142,8 +153,9 @@
%span Path
.form-group
.input-group
- .input-group-addon
- #{URI.join(root_url, @project.namespace.full_path)}/
+ .input-group-prepend
+ .input-group-text
+ #{URI.join(root_url, @project.namespace.full_path)}/
= f.text_field :path, class: 'form-control'
%ul
%li Be careful. Renaming a project's repository can have unintended side effects.
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 63fc5c6d05a..d47dc3d8143 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -1,3 +1,4 @@
+- @content_class = "limit-container-width" unless fluid_layout
- @no_container = true
- breadcrumb_title _("Details")
@@ -6,7 +7,7 @@
= render "home_panel"
.project-empty-note-panel
- %div{ class: [container_class, ("limit-container-width-sm" unless fluid_layout)] }
+ %div{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
.prepend-top-20
%h4
= _('The repository for this project is empty')
@@ -36,7 +37,7 @@
= render 'stat_anchor_list', anchors: @project.empty_repo_statistics_buttons
- if can?(current_user, :push_code, @project)
- %div{ class: [container_class, ("limit-container-width-sm" unless fluid_layout)] }
+ %div{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
.prepend-top-20
.empty_wrapper
%h3#repo-command-line-instructions.page-title-empty
@@ -44,14 +45,14 @@
.git-empty
%fieldset
%h5 Git global setup
- %pre.light-well
+ %pre.bg-light
:preserve
git config --global user.name "#{h git_user_name}"
git config --global user.email "#{h git_user_email}"
%fieldset
%h5 Create a new repository
- %pre.light-well
+ %pre.bg-light
:preserve
git clone #{ content_tag(:span, default_url_to_repo, class: 'clone')}
cd #{h @project.path}
@@ -64,7 +65,7 @@
%fieldset
%h5 Existing folder
- %pre.light-well
+ %pre.bg-light
:preserve
cd existing_folder
git init
@@ -77,7 +78,7 @@
%fieldset
%h5 Existing Git repository
- %pre.light-well
+ %pre.bg-light
:preserve
cd existing_repo
git remote rename origin old-origin
@@ -89,4 +90,4 @@
- if can? current_user, :remove_project, @project
.prepend-top-20
- = link_to 'Remove project', [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-inverted btn-remove pull-right"
+ = link_to 'Remove project', [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-inverted btn-remove float-right"
diff --git a/app/views/projects/environments/_external_url.html.haml b/app/views/projects/environments/_external_url.html.haml
index a82ef5ee5bb..4694bc39d54 100644
--- a/app/views/projects/environments/_external_url.html.haml
+++ b/app/views/projects/environments/_external_url.html.haml
@@ -1,4 +1,4 @@
- if environment.external_url && can?(current_user, :read_environment, environment)
- = link_to environment.external_url, target: '_blank', rel: 'noopener noreferrer', class: 'btn external-url' do
- = icon('external-link')
+ = link_to environment.external_url, target: '_blank', rel: 'noopener noreferrer', class: 'btn external-url has-tooltip', title: s_('Environments|Open live environment') do
+ = sprite_icon('external-link')
View deployment
diff --git a/app/views/projects/environments/_metrics_button.html.haml b/app/views/projects/environments/_metrics_button.html.haml
index b4102fcf103..a4b27575095 100644
--- a/app/views/projects/environments/_metrics_button.html.haml
+++ b/app/views/projects/environments/_metrics_button.html.haml
@@ -3,5 +3,5 @@
- return unless can?(current_user, :read_environment, environment)
= link_to environment_metrics_path(environment), title: 'See metrics', class: 'btn metrics-button' do
- = icon('area-chart')
+ = sprite_icon('chart')
Monitoring
diff --git a/app/views/projects/environments/_stop.html.haml b/app/views/projects/environments/_stop.html.haml
deleted file mode 100644
index c35f9af2873..00000000000
--- a/app/views/projects/environments/_stop.html.haml
+++ /dev/null
@@ -1,5 +0,0 @@
-- if can?(current_user, :create_deployment, environment) && environment.stop_action?
- .inline
- = link_to stop_project_environment_path(@project, environment), method: :post,
- class: 'btn stop-env-link', rel: 'nofollow', data: { confirm: 'Are you sure you want to stop this environment?' } do
- = icon('stop', class: 'stop-env-icon')
diff --git a/app/views/projects/environments/_terminal_button.html.haml b/app/views/projects/environments/_terminal_button.html.haml
index a6201bdbc42..38bc087664b 100644
--- a/app/views/projects/environments/_terminal_button.html.haml
+++ b/app/views/projects/environments/_terminal_button.html.haml
@@ -1,3 +1,3 @@
- if environment.has_terminals? && can?(current_user, :admin_environment, @project)
= link_to terminal_project_environment_path(@project, environment), class: 'btn terminal-button' do
- = icon('terminal')
+ = sprite_icon('terminal')
diff --git a/app/views/projects/environments/empty.html.haml b/app/views/projects/environments/empty.html.haml
new file mode 100644
index 00000000000..1413930ebdb
--- /dev/null
+++ b/app/views/projects/environments/empty.html.haml
@@ -0,0 +1,14 @@
+- page_title _("Metrics")
+
+.row
+ .col-sm-12
+ .svg-content
+ = image_tag 'illustrations/operations_metrics_empty.svg'
+.row.empty-environments
+ .col-sm-12.text-center
+ %h4
+ = s_('Metrics|No deployed environments')
+ .state-description
+ = s_('Metrics|Check out the CI/CD documentation on deploying to an environment')
+ .prepend-top-10
+ = link_to s_("Metrics|Learn about environments"), help_page_path('ci/environments'), class: 'btn btn-success'
diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml
index d6f0b230b58..290970a1045 100644
--- a/app/views/projects/environments/metrics.html.haml
+++ b/app/views/projects/environments/metrics.html.haml
@@ -2,15 +2,9 @@
- page_title "Metrics for environment", @environment.name
.prometheus-container{ class: container_class }
- .top-area
- .row
- .col-sm-6
- %h3
- Environment:
- = link_to @environment.name, environment_path(@environment)
-
#prometheus-graphs{ data: { "settings-path": edit_project_service_path(@project, 'prometheus'),
"clusters-path": project_clusters_path(@project),
+ "current-environment-name": @environment.name,
"documentation-path": help_page_path('administration/monitoring/prometheus/index.md'),
"empty-getting-started-svg-path": image_path('illustrations/monitoring/getting_started.svg'),
"empty-loading-svg-path": image_path('illustrations/monitoring/loading.svg'),
@@ -18,6 +12,7 @@
"empty-unable-to-connect-svg-path": image_path('illustrations/monitoring/unable_to_connect.svg'),
"metrics-endpoint": additional_metrics_project_environment_path(@project, @environment, format: :json),
"deployment-endpoint": project_environment_deployments_path(@project, @environment, format: :json),
+ "environments-endpoint": project_environments_path(@project, format: :json),
"project-path": project_path(@project),
"tags-path": project_tags_path(@project),
"has-metrics": "#{@environment.has_metrics?}" } }
diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml
index add394a6356..a33bc9d4ce6 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -4,6 +4,33 @@
- page_title "Environments"
%div{ class: container_class }
+ - if can?(current_user, :stop_environment, @environment)
+ #stop-environment-modal.modal.fade{ tabindex: -1 }
+ .modal-dialog
+ .modal-content
+ .modal-header
+ %h4.modal-title.d-flex.mw-100
+ Stopping
+ %span.has-tooltip.text-truncate.ml-1.mr-1.flex-fill{ title: @environment.name, data: { container: '#stop-environment-modal' } }
+ = @environment.name
+ ?
+ .modal-body
+ %p= s_('Environments|Are you sure you want to stop this environment?')
+ - unless @environment.stop_action?
+ .warning_message
+ %p= s_('Environments|Note that this action will stop the environment, but it will %{emphasis_start}not%{emphasis_end} have an effect on any existing deployment due to no “stop environment action†being defined in the %{ci_config_link_start}.gitlab-ci.yml%{ci_config_link_end} file.').html_safe % { emphasis_start: '<strong>'.html_safe,
+ emphasis_end: '</strong>'.html_safe,
+ ci_config_link_start: '<a href="https://docs.gitlab.com/ee/ci/yaml/" target="_blank" rel="noopener noreferrer">'.html_safe,
+ ci_config_link_end: '</a>'.html_safe }
+ %a{ href: 'https://docs.gitlab.com/ee/ci/environments.html#stopping-an-environment',
+ target: '_blank',
+ rel: 'noopener noreferrer' }
+ = s_('Environments|Learn more about stopping environments')
+ .modal-footer
+ = button_tag _('Cancel'), type: 'button', class: 'btn btn-cancel', data: { dismiss: 'modal' }
+ = button_to stop_project_environment_path(@project, @environment), class: 'btn btn-danger has-tooltip', method: :post do
+ = s_('Environments|Stop environment')
+
.row.top-area.adjust
.col-md-7
%h3.page-title= @environment.name
@@ -15,7 +42,10 @@
- if can?(current_user, :update_environment, @environment)
= link_to 'Edit', edit_project_environment_path(@project, @environment), class: 'btn'
- if can?(current_user, :stop_environment, @environment)
- = link_to 'Stop', stop_project_environment_path(@project, @environment), data: { confirm: 'Are you sure you want to stop this environment?' }, class: 'btn btn-danger', method: :post
+ = button_tag class: 'btn btn-danger', type: 'button', data: { toggle: 'modal',
+ target: '#stop-environment-modal' } do
+ = sprite_icon('stop')
+ = s_('Environments|Stop')
.environments-container
- if @deployments.blank?
diff --git a/app/views/projects/environments/terminal.html.haml b/app/views/projects/environments/terminal.html.haml
index 6ec4ff56552..5b680189bc8 100644
--- a/app/views/projects/environments/terminal.html.haml
+++ b/app/views/projects/environments/terminal.html.haml
@@ -16,7 +16,7 @@
.nav-controls
- if @environment.external_url.present?
= link_to @environment.external_url, class: 'btn btn-default', target: '_blank', rel: 'noopener noreferrer nofollow' do
- = icon('external-link')
+ = sprite_icon('external-link')
= render 'projects/deployments/actions', deployment: @environment.last_deployment
.terminal-container{ class: container_class }
diff --git a/app/views/projects/find_file/show.html.haml b/app/views/projects/find_file/show.html.haml
index a3467eb6f05..a966bfb2dd9 100644
--- a/app/views/projects/find_file/show.html.haml
+++ b/app/views/projects/find_file/show.html.haml
@@ -5,10 +5,10 @@
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'find_file', path: @path
%ul.breadcrumb.repo-breadcrumb
- %li
+ %li.breadcrumb-item
= link_to project_tree_path(@project, @ref) do
= @project.path
- %li.file-finder
+ %li.file-finder.breadcrumb-item
%input#file_find.form-control.file-finder-input{ type: "text", placeholder: _('Find by path'), autocomplete: 'off' }
.tree-content-holder
diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml
index 21a4702a2a9..57afc7ac9c3 100644
--- a/app/views/projects/forks/index.html.haml
+++ b/app/views/projects/forks/index.html.haml
@@ -16,7 +16,7 @@
- else
= sort_title_recently_created
= icon('chevron-down')
- %ul.dropdown-menu.dropdown-menu-align-right
+ %ul.dropdown-menu.dropdown-menu-right
%li
- excluded_filters = [:state, :scope, :label_name, :milestone_id, :assignee_id, :author_id]
= link_to page_filter_path(sort: sort_value_recently_created, without: excluded_filters) do
diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
index 620fd1906ba..639efd34a74 100644
--- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
+++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
@@ -35,10 +35,10 @@
.label-container
- if generic_commit_status.tags.any?
- generic_commit_status.tags.each do |tag|
- %span.label.label-primary
+ %span.badge.badge-primary
= tag
- if retried
- %span.label.label-warning retried
+ %span.badge.badge-warning retried
- if pipeline_link
%td
diff --git a/app/views/projects/graphs/charts.html.haml b/app/views/projects/graphs/charts.html.haml
index 14c47a5d91c..3f1974d05f4 100644
--- a/app/views/projects/graphs/charts.html.haml
+++ b/app/views/projects/graphs/charts.html.haml
@@ -14,7 +14,7 @@
= icon('circle')
&nbsp;
= language[:label]
- .pull-right
+ .float-right
= language[:value]
\%
.col-md-8
@@ -30,7 +30,7 @@
#{@commits_graph.start_date.strftime('%b %d')}
- end_time = capture do
#{@commits_graph.end_date.strftime('%b %d')}
- = (_("Commit statistics for %{ref} %{start_time} - %{end_time}") % { ref: "<strong>#{@ref}</strong>", start_time: start_time, end_time: end_time }).html_safe
+ = (_("Commit statistics for %{ref} %{start_time} - %{end_time}") % { ref: "<strong>#{h @ref}</strong>", start_time: start_time, end_time: end_time }).html_safe
.col-md-6
.tree-ref-container
diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml
index c81ee6874e3..f1b14d4c4d1 100644
--- a/app/views/projects/graphs/show.html.haml
+++ b/app/views/projects/graphs/show.html.haml
@@ -24,4 +24,4 @@
.graphs.row
#contributors-master
#contributors.clearfix
- %ol.contributors-list.clearfix
+ %ol.contributors-list.row
diff --git a/app/views/projects/hook_logs/_index.html.haml b/app/views/projects/hook_logs/_index.html.haml
index 8096d9530c3..3e54c3ca9f8 100644
--- a/app/views/projects/hook_logs/_index.html.haml
+++ b/app/views/projects/hook_logs/_index.html.haml
@@ -18,8 +18,8 @@
%tr
%td
= render partial: 'shared/hook_logs/status_label', locals: { hook_log: hook_log }
- %td.hidden-xs
- %span.label.label-gray.deploy-project-label
+ %td.d-none.d-sm-block
+ %span.badge.badge-gray.deploy-project-label
= hook_log.trigger.singularize.titleize
%td
= truncate(hook_log.url, length: 50)
diff --git a/app/views/projects/hook_logs/show.html.haml b/app/views/projects/hook_logs/show.html.haml
index 1cf4105bd27..e51efa85df0 100644
--- a/app/views/projects/hook_logs/show.html.haml
+++ b/app/views/projects/hook_logs/show.html.haml
@@ -4,6 +4,6 @@
Request details
.col-lg-9
- = link_to 'Resend Request', retry_project_hook_hook_log_path(@project, @hook, @hook_log), class: "btn btn-default pull-right prepend-left-10"
+ = link_to 'Resend Request', retry_project_hook_hook_log_path(@project, @hook, @hook_log), class: "btn btn-default float-right prepend-left-10"
= render partial: 'shared/hook_logs/content', locals: { hook_log: @hook_log }
diff --git a/app/views/projects/hooks/_index.html.haml b/app/views/projects/hooks/_index.html.haml
index 776681ea09a..5990582fd55 100644
--- a/app/views/projects/hooks/_index.html.haml
+++ b/app/views/projects/hooks/_index.html.haml
@@ -15,7 +15,7 @@
%h5.prepend-top-default
Webhooks (#{@hooks.count})
- if @hooks.any?
- %ul.well-list
+ %ul.content-list
- @hooks.each do |hook|
= render 'project_hook', hook: hook
- else
diff --git a/app/views/projects/hooks/edit.html.haml b/app/views/projects/hooks/edit.html.haml
index dcc1f0e3fbe..c31aef60453 100644
--- a/app/views/projects/hooks/edit.html.haml
+++ b/app/views/projects/hooks/edit.html.haml
@@ -13,7 +13,7 @@
= f.submit 'Save changes', class: 'btn btn-create'
= render 'shared/web_hooks/test_button', triggers: ProjectHook.triggers, hook: @hook
- = link_to 'Remove', project_hook_path(@project, @hook), method: :delete, class: 'btn btn-remove pull-right', data: { confirm: 'Are you sure?' }
+ = link_to 'Remove', project_hook_path(@project, @hook), method: :delete, class: 'btn btn-remove float-right', data: { confirm: 'Are you sure?' }
%hr
diff --git a/app/views/projects/imports/new.html.haml b/app/views/projects/imports/new.html.haml
index 778ff91362d..16c4f21279d 100644
--- a/app/views/projects/imports/new.html.haml
+++ b/app/views/projects/imports/new.html.haml
@@ -5,14 +5,14 @@
%hr
- if @project.import_failed?
- .panel.panel-danger
- .panel-heading The repository could not be imported.
- .panel-body
+ .card.bg-danger
+ .card-header The repository could not be imported.
+ .card-body
%pre
:preserve
#{h(sanitize_repo_path(@project, @project.import_error))}
-= form_for @project, url: project_import_path(@project), method: :post, html: { class: 'form-horizontal' } do |f|
+= form_for @project, url: project_import_path(@project), method: :post do |f|
= render "shared/import_form", f: f
.form-actions
diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml
index 816f2fa816d..665968a64e1 100644
--- a/app/views/projects/issues/_discussion.html.haml
+++ b/app/views/projects/issues/_discussion.html.haml
@@ -8,5 +8,6 @@
%section.js-vue-notes-event
#js-vue-notes{ data: { notes_data: notes_data(@issue),
noteable_data: serialize_issuable(@issue),
- noteable_type: 'issue',
+ noteable_type: 'Issue',
+ target_type: 'issue',
current_user_data: UserSerializer.new.represent(current_user, only_path: true).to_json } }
diff --git a/app/views/projects/issues/_form.html.haml b/app/views/projects/issues/_form.html.haml
index 8b011af78eb..1e4e9450ffa 100644
--- a/app/views/projects/issues/_form.html.haml
+++ b/app/views/projects/issues/_form.html.haml
@@ -1,2 +1,4 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'form-horizontal issue-form common-note-form js-quick-submit js-requires-input' } do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @issue],
+ html: { class: 'issue-form common-note-form js-quick-submit js-requires-input' },
+ data: { markdown_version: @issue.cached_markdown_version } do |f|
= render 'shared/issuable/form', f: f, issuable: @issue
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index e27f5658e87..8a14146cb87 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -12,25 +12,25 @@
= confidential_icon(issue)
= link_to issue.title, issue_path(issue)
- if issue.tasks?
- %span.task-status.hidden-xs
+ %span.task-status.d-none.d-sm-inline-block
&nbsp;
= issue.task_status
.issuable-info
%span.issuable-reference
#{issuable_reference(issue)}
- %span.issuable-authored.hidden-xs
+ %span.issuable-authored.d-none.d-sm-inline-block
&middot;
opened #{time_ago_with_tooltip(issue.created_at, placement: 'bottom')}
by #{link_to_member(@project, issue.author, avatar: false)}
- if issue.milestone
- %span.issuable-milestone.hidden-xs
+ %span.issuable-milestone.d-none.d-sm-inline-block
&nbsp;
- = link_to project_issues_path(issue.project, milestone_title: issue.milestone.title), data: { html: 1, toggle: 'tooltip', title: milestone_tooltip_due_date(issue.milestone) } do
+ = link_to project_issues_path(issue.project, milestone_title: issue.milestone.title), data: { html: 'true', toggle: 'tooltip', title: milestone_tooltip_due_date(issue.milestone) } do
= icon('clock-o')
= issue.milestone.title
- if issue.due_date
- %span.issuable-due-date.hidden-xs.has-tooltip{ class: "#{'cred' if issue.overdue?}", title: _('Due date') }
+ %span.issuable-due-date.d-none.d-sm-inline-block.has-tooltip{ class: "#{'cred' if issue.overdue?}", title: _('Due date') }
&nbsp;
= icon('calendar')
= issue.due_date.to_s(:medium)
@@ -50,5 +50,5 @@
= render 'shared/issuable_meta_data', issuable: issue
- .pull-right.issuable-updated-at.hidden-xs
+ .float-right.issuable-updated-at.d-none.d-sm-inline-block
%span updated #{time_ago_with_tooltip(issue.updated_at, placement: 'bottom', html_class: 'issue_update_ago')}
diff --git a/app/views/projects/issues/_nav_btns.html.haml b/app/views/projects/issues/_nav_btns.html.haml
index 297b928f020..0dd2d2e6c5d 100644
--- a/app/views/projects/issues/_nav_btns.html.haml
+++ b/app/views/projects/issues/_nav_btns.html.haml
@@ -1,5 +1,5 @@
-= link_to safe_params.merge(rss_url_options), class: 'btn btn-default append-right-10 has-tooltip', title: 'Subscribe' do
- = icon('rss')
+= render 'shared/issuable/feed_buttons'
+
- if @can_bulk_update
= button_tag "Edit issues", class: "btn btn-default append-right-10 js-bulk-update-toggle"
- if show_new_issue_link?(@project)
diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml
index 4b8bf578b28..a678cb6f058 100644
--- a/app/views/projects/issues/_new_branch.html.haml
+++ b/app/views/projects/issues/_new_branch.html.haml
@@ -14,15 +14,15 @@
= icon('spinner', class: 'fa-spin')
%span.text
Checking branch availability…
- .btn-group.available.hide
+ .btn-group.available.hidden
%button.btn.js-create-merge-request.btn-success.btn-inverted{ type: 'button', data: { action: data_action } }
= value
- %button.btn.create-merge-request-dropdown-toggle.dropdown-toggle.btn-success.btn-inverted.js-dropdown-toggle{ type: 'button', data: { dropdown: { trigger: '#create-merge-request-dropdown' } } }
+ %button.btn.create-merge-request-dropdown-toggle.dropdown-toggle.btn-success.btn-inverted.js-dropdown-toggle{ type: 'button', data: { dropdown: { trigger: '#create-merge-request-dropdown' }, display: 'static' } }
= icon('caret-down')
.droplab-dropdown
- %ul#create-merge-request-dropdown.create-merge-request-dropdown-menu.dropdown-menu.dropdown-menu-align-right.gl-show-field-errors{ data: { dropdown: true } }
+ %ul#create-merge-request-dropdown.create-merge-request-dropdown-menu.dropdown-menu.dropdown-menu-right.gl-show-field-errors{ data: { dropdown: true } }
- if can_create_merge_request
%li.droplab-item-selected{ role: 'button', data: { value: 'create-mr', text: _('Create merge request') } }
.menu-item
@@ -40,13 +40,13 @@
%label{ for: 'new-branch-name' }
= _('Branch name')
%input#new-branch-name.js-branch-name.form-control{ type: 'text', placeholder: "#{@issue.to_branch_name}", value: "#{@issue.to_branch_name}" }
- %span.js-branch-message.help-block
+ %span.js-branch-message.form-text
.form-group
%label{ for: 'source-name' }
= _('Source (branch or tag)')
%input#source-name.js-ref.ref.form-control{ type: 'text', placeholder: "#{@project.default_branch}", value: "#{@project.default_branch}", data: { value: "#{@project.default_branch}" } }
- %span.js-ref-message.help-block
+ %span.js-ref-message.form-text.text-muted
.form-group
%button.btn.btn-success.js-create-target{ type: 'button', data: { action: 'create-mr' } }
diff --git a/app/views/projects/issues/calendar.ics.haml b/app/views/projects/issues/calendar.ics.haml
new file mode 100644
index 00000000000..59573e5fecf
--- /dev/null
+++ b/app/views/projects/issues/calendar.ics.haml
@@ -0,0 +1 @@
+= render 'issues/issues_calendar', issues: @issues
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index f1fc1c2316d..2d036bd4e3e 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -12,12 +12,12 @@
.detail-page-header
.detail-page-header-body
.issuable-status-box.status-box.status-box-issue-closed{ class: issue_button_visibility(@issue, false) }
- = sprite_icon('mobile-issue-close', size: 16, css_class: 'hidden-sm hidden-md hidden-lg')
- %span.hidden-xs
+ = sprite_icon('mobile-issue-close', size: 16, css_class: 'd-block d-sm-none')
+ %span.d-none.d-sm-block
Closed
.issuable-status-box.status-box.status-box-open{ class: issue_button_visibility(@issue, true) }
- = sprite_icon('issue-open-m', size: 16, css_class: 'hidden-sm hidden-md hidden-lg')
- %span.hidden-xs Open
+ = sprite_icon('issue-open-m', size: 16, css_class: 'd-block d-sm-none')
+ %span.d-none.d-sm-block Open
.issuable-meta
- if @issue.confidential
@@ -26,15 +26,15 @@
.issuable-warning-icon.inline= sprite_icon('lock', size: 16, css_class: 'icon')
= issuable_meta(@issue, @project, "Issue")
- %a.btn.btn-default.pull-right.visible-xs-block.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" }
+ %a.btn.btn-default.float-right.d-block.d-sm-none.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" }
= icon('angle-double-left')
.detail-page-header-actions.js-issuable-actions
.clearfix.issue-btn-group.dropdown
- %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
+ %button.btn.btn-default.float-left.d-md-none.d-lg-none.d-xl-none{ type: "button", data: { toggle: "dropdown" } }
Options
= icon('caret-down')
- .dropdown-menu.dropdown-menu-align-right.hidden-lg
+ .dropdown-menu.dropdown-menu-right.d-lg-none.d-xl-none
%ul
- unless current_user == @issue.author
%li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue))
@@ -51,9 +51,9 @@
= render 'shared/issuable/close_reopen_button', issuable: @issue, can_update: can_update_issue
- if can_report_spam
- = link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam'
+ = link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'd-none d-sm-none d-md-block btn btn-grouped btn-spam', title: 'Submit as spam'
- if can_create_issue
- = link_to new_project_issue_path(@project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do
+ = link_to new_project_issue_path(@project), class: 'd-none d-sm-none d-md-block btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do
New issue
.issue-details.issuable-details
diff --git a/app/views/projects/jobs/_empty_state.html.haml b/app/views/projects/jobs/_empty_state.html.haml
index 311934d9c33..ea552c73c92 100644
--- a/app/views/projects/jobs/_empty_state.html.haml
+++ b/app/views/projects/jobs/_empty_state.html.haml
@@ -5,10 +5,10 @@
- action = local_assigns.fetch(:action, nil)
.row.empty-state
- .col-xs-12
+ .col-12
.svg-content{ class: illustration_size }
= image_tag illustration
- .col-xs-12
+ .col-12
.text-content
%h4.text-center= title
- if content
diff --git a/app/views/projects/jobs/_header.html.haml b/app/views/projects/jobs/_header.html.haml
index 83a2af1dc74..b83e8dddccb 100644
--- a/app/views/projects/jobs/_header.html.haml
+++ b/app/views/projects/jobs/_header.html.haml
@@ -27,5 +27,5 @@
= link_to "New issue", new_project_issue_path(@project, issue: build_failed_issue_options), class: 'btn btn-new btn-inverted'
- if can?(current_user, :update_build, @build) && @build.retryable?
= link_to "Retry job", retry_project_job_path(@project, @build), class: 'btn btn-inverted-secondary', method: :post
- %button.btn.btn-default.pull-right.visible-xs-block.visible-sm-block.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" }
+ %button.btn.btn-default.float-right.d-block.d-sm-none.d-md-none.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" }
= icon('angle-double-left')
diff --git a/app/views/projects/jobs/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml
index 826404c2008..b88fe47726d 100644
--- a/app/views/projects/jobs/_sidebar.html.haml
+++ b/app/views/projects/jobs/_sidebar.html.haml
@@ -1,6 +1,10 @@
%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" } }
.sidebar-container
.blocks-container
+ - if can?(current_user, :create_build_terminal, @build)
+ .block
+ = link_to terminal_project_job_path(@project, @build), class: 'terminal-button pull-right btn visible-md-block visible-lg-block', title: 'Terminal' do
+ Terminal
#js-details-block-vue{ data: { can_user_retry: can?(current_user, :update_build, @build) && @build.retryable? } }
@@ -14,11 +18,11 @@
#{time_ago_with_tooltip(@build.artifacts_expire_at)}
- elsif @build.has_expiring_artifacts?
%p.build-detail-row
- The artifacts will be removed in
- %span= time_ago_with_tooltip @build.artifacts_expire_at
+ The artifacts will be removed
+ #{time_ago_with_tooltip(@build.artifacts_expire_at)}
- if @build.artifacts?
- .btn-group.btn-group-justified{ role: :group }
+ .btn-group.d-flex{ role: :group }
- if @build.has_expiring_artifacts? && can?(current_user, :update_build, @build)
= link_to keep_project_job_artifacts_path(@project, @build), class: 'btn btn-sm btn-default', method: :post do
Keep
@@ -42,7 +46,7 @@
- if @build.trigger_variables.any?
%p
- %button.btn.group.btn-group-justified.js-reveal-variables Reveal Variables
+ %button.btn.group.js-reveal-variables Reveal Variables
%dl.js-build-variables.trigger-build-variables.hide
- @build.trigger_variables.each do |trigger_variable|
@@ -83,7 +87,7 @@
- builds.select{|build| build.status == build_status}.each do |build|
.build-job{ class: sidebar_build_class(build, @build), data: { stage: build.stage } }
- tooltip = build.tooltip_message
- = link_to(project_job_path(@project, build), data: { toggle: 'tooltip', html: true, title: tooltip, container: 'body' }) do
+ = link_to(project_job_path(@project, build), data: { toggle: 'tooltip', html: 'true', title: tooltip, container: 'body' }) do
= sprite_icon('arrow-right', size:16, css_class: 'icon-arrow-right')
%span{ class: "ci-status-icon-#{build.status}" }
= ci_icon_for_status(build.status)
diff --git a/app/views/projects/jobs/_user.html.haml b/app/views/projects/jobs/_user.html.haml
index 461d503f95d..90ce581a903 100644
--- a/app/views/projects/jobs/_user.html.haml
+++ b/app/views/projects/jobs/_user.html.haml
@@ -1,7 +1,7 @@
by
%a{ href: user_path(@build.user) }
- %span.hidden-xs
+ %span.d-none.d-sm-inline
= image_tag avatar_icon_for_user(@build.user, 24), class: "avatar s24"
%strong{ data: { toggle: 'tooltip', placement: 'top', title: @build.user.to_reference } }
= @build.user.name
- %strong.visible-xs-inline= @build.user.to_reference
+ %strong.d-inline.d-sm-none= @build.user.to_reference
diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml
index cbbcc8f1db5..1f33bb3a129 100644
--- a/app/views/projects/jobs/show.html.haml
+++ b/app/views/projects/jobs/show.html.haml
@@ -15,7 +15,7 @@
- elsif @build.tags.any?
This job is stuck, because you don't have any active runners online with any of these tags assigned to them:
- @build.tags.each do |tag|
- %span.label.label-primary
+ %span.badge.badge-primary
= tag
- else
This job is stuck, because you don't have any active runners that can run this job.
@@ -58,13 +58,13 @@
- if @build.running? || @build.has_trace?
.build-trace-container.prepend-top-default
.top-bar.js-top-bar
- .js-truncated-info.truncated-info.hidden-xs.pull-left.hidden<
+ .js-truncated-info.truncated-info.d-none.d-sm-block.float-left.hidden<
Showing last
%span.js-truncated-info-size.truncated-info-size><
of log -
%a.js-raw-link.raw-link{ href: raw_project_job_path(@project, @build) }>< Complete Raw
- .controllers.pull-right
+ .controllers.float-right
- if @build.has_trace?
= link_to raw_project_job_path(@project, @build),
title: 'Show complete raw',
@@ -86,9 +86,7 @@
%button.js-scroll-down.btn-scroll.btn-transparent.btn-blank{ type: 'button', disabled: true }
= custom_icon('scroll_down')
- %pre.build-trace#build-trace
- %code.bash.js-build-output
- .build-loader-animation.js-build-refresh
+ = render 'shared/builds/build_output'
- else
= render "empty_states"
diff --git a/app/views/projects/jobs/terminal.html.haml b/app/views/projects/jobs/terminal.html.haml
new file mode 100644
index 00000000000..efea666a4d9
--- /dev/null
+++ b/app/views/projects/jobs/terminal.html.haml
@@ -0,0 +1,11 @@
+- @no_container = true
+- add_to_breadcrumbs 'Jobs', project_jobs_path(@project)
+- add_to_breadcrumbs "##{@build.id}", project_job_path(@project, @build)
+- breadcrumb_title 'Terminal'
+- page_title 'Terminal', "#{@build.name} (##{@build.id})", 'Jobs'
+
+- content_for :page_specific_javascripts do
+ = stylesheet_link_tag "xterm/xterm"
+
+.terminal-container{ class: container_class }
+ #terminal{ data: { project_path: terminal_project_job_path(@project, @build, format: :ws) } }
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index 9c78bade254..fb5b0fc15c9 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -1,40 +1,44 @@
- @no_container = true
- page_title "Labels"
-- hide_class = ''
- can_admin_label = can?(current_user, :admin_label, @project)
+- hide_class = ''
+
+- if can_admin_label
+ - content_for(:header_content) do
+ .nav-controls
+ = link_to _('New label'), new_project_label_path(@project), class: "btn btn-new"
- if @labels.exists? || @prioritized_labels.exists?
#promote-label-modal
%div{ class: container_class }
.top-area.adjust
.nav-text
- Labels can be applied to issues and merge requests.
+ = _('Labels can be applied to issues and merge requests.')
- if can_admin_label
- Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging.
+ = _('Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging.')
- - if can_admin_label
- .nav-controls
- = link_to new_project_label_path(@project), class: "btn btn-new" do
- New label
-
- .labels
+ .labels-container.prepend-top-5
- if can_admin_label
-# Only show it in the first page
- hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1')
.prioritized-labels{ class: ('hide' if hide) }
- %h5 Prioritized Labels
- %ul.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_project_labels_path(@project) }
- #js-priority-labels-empty-state{ class: "#{'hidden' unless @prioritized_labels.empty?}" }
+ %h5.prepend-top-10= _('Prioritized Labels')
+ .content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_project_labels_path(@project) }
+ #js-priority-labels-empty-state.priority-labels-empty-state{ class: "#{'hidden' unless @prioritized_labels.empty?}" }
= render 'shared/empty_states/priority_labels'
- if @prioritized_labels.present?
- = render partial: 'shared/label', subject: @project, collection: @prioritized_labels, as: :label
+ = render partial: 'shared/label', subject: @project, collection: @prioritized_labels, as: :label, locals: { force_priority: true }
- if @labels.present?
.other-labels
- if can_admin_label
- %h5{ class: ('hide' if hide) } Other Labels
- %ul.content-list.manage-labels-list.js-other-labels
+ %h5{ class: ('hide' if hide) }= _('Other Labels')
+ .content-list.manage-labels-list.js-other-labels
= render partial: 'shared/label', subject: @project, collection: @labels, as: :label
= paginate @labels, theme: 'gitlab'
- else
= render 'shared/empty_states/labels'
+
+%template#js-badge-item-template
+ %li.label-link-item.js-priority-badge.inline.prepend-left-10
+ .label-badge.label-badge-blue= _('Prioritized label')
diff --git a/app/views/projects/mattermosts/_no_teams.html.haml b/app/views/projects/mattermosts/_no_teams.html.haml
index 243dcfdc187..0377cd6586e 100644
--- a/app/views/projects/mattermosts/_no_teams.html.haml
+++ b/app/views/projects/mattermosts/_no_teams.html.haml
@@ -13,4 +13,4 @@
and try again.
%hr
.clearfix
- = link_to 'Go back', edit_project_service_path(@project, @service), class: 'btn btn-lg pull-right'
+ = link_to 'Go back', edit_project_service_path(@project, @service), class: 'btn btn-lg float-right'
diff --git a/app/views/projects/mattermosts/_team_selection.html.haml b/app/views/projects/mattermosts/_team_selection.html.haml
index 20acd476f73..37c09f12f63 100644
--- a/app/views/projects/mattermosts/_team_selection.html.haml
+++ b/app/views/projects/mattermosts/_team_selection.html.haml
@@ -11,11 +11,11 @@
- options = options_for_select(mattermost_teams_options(@teams), selected_id)
= f.select(:team_id, options, { include_blank: 'Select team...'}, { class: 'form-control', disabled: @teams.one?, selected: selected_id, required: true })
= f.hidden_field(:team_id, value: selected_id, required: true) if @teams.one?
- .help-block
+ .form-text.text-muted
- if @teams.one?
- This is the only available team.
+ This is the only available team that you are a member of.
- else
- The list shows all available teams.
+ The list shows all available teams that you are a member of.
To create a team,
= link_to "#{Gitlab.config.mattermost.host}/create_team" do
use Mattermost's interface
@@ -25,7 +25,7 @@
%h4 Command trigger word
%p Choose the word that will trigger commands
= f.text_field(:trigger, value: @project.path, class: 'form-control', required: true)
- .help-block
+ .form-text.text-muted
%p
Trigger word must be unique, and can't begin with a slash or contain any spaces.
Use the word that works best for your team.
@@ -41,6 +41,6 @@
= icon('external-link')
%hr
.clearfix
- .pull-right
+ .float-right
= link_to 'Cancel', edit_project_service_path(@project, @service), class: 'btn btn-lg'
= f.submit 'Install', class: 'btn btn-save btn-lg'
diff --git a/app/views/projects/mattermosts/new.html.haml b/app/views/projects/mattermosts/new.html.haml
index 15829a3f143..9e293d07cb7 100644
--- a/app/views/projects/mattermosts/new.html.haml
+++ b/app/views/projects/mattermosts/new.html.haml
@@ -1,7 +1,7 @@
- @body_class = 'card-content'
.service-installation
- .inline.pull-right
+ .inline.float-right
= custom_icon('mattermost_logo', size: 48)
%h3 Install Mattermost Command
- if @teams.empty?
diff --git a/app/views/projects/merge_requests/_form.html.haml b/app/views/projects/merge_requests/_form.html.haml
index 9607a7b5d06..5a59f956cb5 100644
--- a/app/views/projects/merge_requests/_form.html.haml
+++ b/app/views/projects/merge_requests/_form.html.haml
@@ -1,2 +1,4 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal common-note-form js-requires-input js-quick-submit' } do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @merge_request],
+ html: { class: 'merge-request-form common-note-form js-requires-input js-quick-submit' },
+ data: { markdown_version: @merge_request.cached_markdown_version } do |f|
= render 'shared/issuable/form', f: f, issuable: @merge_request
diff --git a/app/views/projects/merge_requests/_how_to_merge.html.haml b/app/views/projects/merge_requests/_how_to_merge.html.haml
index 54a661040ea..62dd21ef6e0 100644
--- a/app/views/projects/merge_requests/_how_to_merge.html.haml
+++ b/app/views/projects/merge_requests/_how_to_merge.html.haml
@@ -1,9 +1,10 @@
-#modal_merge_info.modal
+#modal_merge_info.modal{ tabindex: '-1' }
.modal-dialog
.modal-content
.modal-header
- %a.close{ href: "#", "data-dismiss" => "modal" } ×
- %h3 Check out, review, and merge locally
+ %h3.modal-title Check out, review, and merge locally
+ %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
+ %span{ "aria-hidden": true } &times;
.modal-body
%p
%strong Step 1.
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 027a9ff1416..cd3d896fff2 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -9,21 +9,21 @@
%span.merge-request-title-text
= link_to merge_request.title, merge_request_path(merge_request)
- if merge_request.tasks?
- %span.task-status.hidden-xs
+ %span.task-status.d-none.d-sm-inline-block
&nbsp;
= merge_request.task_status
.issuable-info
%span.issuable-reference
#{issuable_reference(merge_request)}
- %span.issuable-authored.hidden-xs
+ %span.issuable-authored.d-none.d-sm-inline-block
&middot;
opened #{time_ago_with_tooltip(merge_request.created_at, placement: 'bottom')}
by #{link_to_member(@project, merge_request.author, avatar: false)}
- if merge_request.milestone
- %span.issuable-milestone.hidden-xs
+ %span.issuable-milestone.d-none.d-sm-inline-block
&nbsp;
- = link_to project_merge_requests_path(merge_request.project, milestone_title: merge_request.milestone.title), data: { html: 1, toggle: 'tooltip', title: milestone_tooltip_due_date(merge_request.milestone) } do
+ = link_to project_merge_requests_path(merge_request.project, milestone_title: merge_request.milestone.title), data: { html: 'true', toggle: 'tooltip', title: milestone_tooltip_due_date(merge_request.milestone) } do
= icon('clock-o')
= merge_request.milestone.title
- if merge_request.target_project.default_branch != merge_request.target_branch
@@ -40,17 +40,17 @@
.issuable-meta
%ul.controls
- if merge_request.merged?
- %li.issuable-status.hidden-xs
+ %li.issuable-status.d-none.d-sm-inline-block
MERGED
- elsif merge_request.closed?
- %li.issuable-status.hidden-xs
+ %li.issuable-status.d-none.d-sm-inline-block
= icon('ban')
CLOSED
- if merge_request.head_pipeline
- %li.issuable-pipeline-status.hidden-xs
+ %li.issuable-pipeline-status.d-none.d-sm-inline-block
= render_pipeline_status(merge_request.head_pipeline)
- if merge_request.open? && merge_request.broken?
- %li.issuable-pipeline-broken.hidden-xs
+ %li.issuable-pipeline-broken.d-none.d-sm-inline-block
= link_to merge_request_path(merge_request), class: "has-tooltip", title: _('Cannot be merged automatically') do
= icon('exclamation-triangle')
- if merge_request.assignee
@@ -59,5 +59,5 @@
= render 'shared/issuable_meta_data', issuable: merge_request
- .pull-right.issuable-updated-at.hidden-xs
+ .float-right.issuable-updated-at.d-none.d-sm-inline-block
%span updated #{time_ago_with_tooltip(merge_request.updated_at, placement: 'bottom', html_class: 'merge_request_updated_ago')}
diff --git a/app/views/projects/merge_requests/_mr_box.html.haml b/app/views/projects/merge_requests/_mr_box.html.haml
index 8a390cf8700..1a9ab288683 100644
--- a/app/views/projects/merge_requests/_mr_box.html.haml
+++ b/app/views/projects/merge_requests/_mr_box.html.haml
@@ -1,4 +1,4 @@
-.detail-page-description.content-block
+.detail-page-description
%h2.title
= markdown_field(@merge_request, :title)
diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml
index 22c8b6b513d..a58179091ae 100644
--- a/app/views/projects/merge_requests/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/_mr_title.html.haml
@@ -7,8 +7,8 @@
.detail-page-header
.detail-page-header-body
.issuable-status-box.status-box{ class: status_box_class(@merge_request) }
- = sprite_icon(@merge_request.state_icon_name, size: 16, css_class: 'hidden-sm hidden-md hidden-lg')
- %span.hidden-xs
+ = sprite_icon(@merge_request.state_icon_name, size: 16, css_class: 'd-block d-sm-none')
+ %span.d-none.d-sm-block
= @merge_request.state_human_name
.issuable-meta
@@ -16,15 +16,15 @@
.issuable-warning-icon.inline= sprite_icon('lock', size: 16, css_class: 'icon')
= issuable_meta(@merge_request, @project, "Merge request")
- %a.btn.btn-default.pull-right.visible-xs-block.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" }
+ %a.btn.btn-default.float-right.d-block.d-sm-none.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" }
= icon('angle-double-left')
.detail-page-header-actions.js-issuable-actions
.clearfix.issue-btn-group.dropdown
- %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
+ %button.btn.btn-default.float-left.d-md-none.d-lg-none.d-xl-none{ type: "button", data: { toggle: "dropdown" } }
Options
= icon('caret-down')
- .dropdown-menu.dropdown-menu-align-right.hidden-lg
+ .dropdown-menu.dropdown-menu-right.d-lg-none.d-xl-none
%ul
- if can_update_merge_request
%li= link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
@@ -37,6 +37,6 @@
= 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 js-issuable-edit"
+ = link_to 'Edit', edit_project_merge_request_path(@project, @merge_request), class: "d-none d-sm-none d-md-block btn btn-grouped js-issuable-edit"
= render 'shared/issuable/close_reopen_button', issuable: @merge_request, can_update: can_update_merge_request
diff --git a/app/views/projects/merge_requests/conflicts/_submit_form.html.haml b/app/views/projects/merge_requests/conflicts/_submit_form.html.haml
index b86a87a1fc6..4d84ee2488b 100644
--- a/app/views/projects/merge_requests/conflicts/_submit_form.html.haml
+++ b/app/views/projects/merge_requests/conflicts/_submit_form.html.haml
@@ -2,8 +2,8 @@
- translation =_('You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}') % { use_ours: '<code>Use Ours</code>', use_theirs: '<code>Use Theirs</code>', branch_name: branch_name }
%hr
-.form-horizontal.resolve-conflicts-form
- .form-group
+.resolve-conflicts-form
+ .form-group.row
.col-md-4
%h4= _('Resolve conflicts on source branch')
.resolve-info
@@ -14,11 +14,11 @@
.commit-message-container
.max-width-marker
%textarea.form-control.js-commit-message#commit-message{ "v-model" => "conflictsData.commitMessage", "rows" => "5" }
- .form-group
- .col-md-offset-4.col-md-8
+ .form-group.row
+ .offset-md-4.col-md-8
.row
- .col-xs-6
+ .col-6
%button.btn.btn-success.js-submit-button{ type: "button", "@click" => "commit()", ":disabled" => "!readyToCommit" }
%span {{commitButtonText}}
- .col-xs-6.text-right
+ .col-6.text-right
= link_to "Cancel", project_merge_request_path(@merge_request.project, @merge_request), class: "btn btn-cancel"
diff --git a/app/views/projects/merge_requests/creations/_new_compare.html.haml b/app/views/projects/merge_requests/creations/_new_compare.html.haml
index 773b12b4536..ca0f7d6098f 100644
--- a/app/views/projects/merge_requests/creations/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/creations/_new_compare.html.haml
@@ -1,14 +1,14 @@
%h3.page-title
New Merge Request
-= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], url: project_new_merge_request_path(@project), method: :get, html: { class: "merge-request-form form-inline js-requires-input" } do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], url: project_new_merge_request_path(@project), method: :get, html: { class: "merge-request-form js-requires-input" } do |f|
.hide.alert.alert-danger.mr-compare-errors
.js-merge-request-new-compare.row{ 'data-source-branch-url': project_new_merge_request_branch_from_path(@source_project), 'data-target-branch-url': project_new_merge_request_branch_to_path(@source_project) }
.col-lg-6
- .panel.panel-default.panel-new-merge-request
- .panel-heading
+ .card.card-new-merge-request
+ .card-header
Source branch
- .panel-body.clearfix
+ .card-body.clearfix
.merge-request-select.dropdown
= f.hidden_field :source_project_id
= dropdown_toggle @merge_request.source_project_path, { toggle: "dropdown", 'field-name': "#{f.object_name}[source_project_id]", disabled: @merge_request.persisted? }, { toggle_class: "js-compare-dropdown js-source-project" }
@@ -32,10 +32,10 @@
%ul.list-unstyled.mr_source_commit
.col-lg-6
- .panel.panel-default.panel-new-merge-request
- .panel-heading
+ .card.card-new-merge-request
+ .card-header
Target branch
- .panel-body.clearfix
+ .card-body.clearfix
- projects = target_projects(@project)
.merge-request-select.dropdown
= f.hidden_field :target_project_id
@@ -55,7 +55,7 @@
= dropdown_filter(_("Search branches"))
= dropdown_content
= dropdown_loading
- .panel-footer
+ .card-footer
.text-center= icon('spinner spin', class: "js-target-loading")
%ul.list-unstyled.mr_target_commit
diff --git a/app/views/projects/merge_requests/creations/_new_submit.html.haml b/app/views/projects/merge_requests/creations/_new_submit.html.haml
index 68780cedeb1..f7a5d85500f 100644
--- a/app/views/projects/merge_requests/creations/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/creations/_new_submit.html.haml
@@ -7,10 +7,10 @@
%span into
%strong.ref-name= target_title
- %span.pull-right
+ %span.float-right
= link_to 'Change branches', mr_change_branches_path(@merge_request)
%hr
-= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal common-note-form js-requires-input js-quick-submit' } do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form common-note-form js-requires-input js-quick-submit' } do |f|
= render 'shared/issuable/form', f: f, issuable: @merge_request, commits: @commits
= f.hidden_field :source_project_id
= f.hidden_field :source_branch
@@ -24,23 +24,28 @@
There are no commits yet.
= custom_icon ('illustration_no_commits')
- else
- %ul.merge-request-tabs.nav-links.no-top.no-bottom
- %li.commits-tab.active
- = link_to url_for(safe_params), data: {target: 'div#commits', action: 'new', toggle: 'tab'} do
- Commits
- %span.badge= @commits.size
- - if @pipelines.any?
- %li.builds-tab
- = link_to url_for(safe_params.merge(action: 'pipelines')), data: {target: 'div#pipelines', action: 'pipelines', toggle: 'tab'} do
- Pipelines
- %span.badge= @pipelines.size
- %li.diffs-tab
- = link_to url_for(safe_params.merge(action: 'diffs')), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do
- Changes
- %span.badge= @merge_request.diff_size
+ .merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') }
+ .merge-request-tabs-container
+ .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
+ .fade-left= icon('angle-left')
+ .fade-right= icon('angle-right')
+ %ul.merge-request-tabs.nav.nav-tabs.nav-links.no-top.no-bottom.js-tabs-affix
+ %li.commits-tab.new-tab
+ = link_to url_for(safe_params), data: {target: 'div#commits', action: 'new', toggle: 'tabvue'} do
+ Commits
+ %span.badge.badge-pill= @commits.size
+ - if @pipelines.any?
+ %li.builds-tab
+ = link_to url_for(safe_params.merge(action: 'pipelines')), data: {target: 'div#pipelines', action: 'pipelines', toggle: 'tabvue'} do
+ Pipelines
+ %span.badge.badge-pill= @pipelines.size
+ %li.diffs-tab
+ = link_to url_for(safe_params.merge(action: 'diffs')), data: {target: 'div#diffs', action: 'diffs', toggle: 'tabvue'} do
+ Changes
+ %span.badge.badge-pill= @merge_request.diff_size
- .tab-content
- #commits.commits.tab-pane.active
+ #diff-notes-app.tab-content
+ #new.commits.tab-pane.active
= render "projects/merge_requests/commits"
#diffs.diffs.tab-pane
-# This tab is always loaded via AJAX
diff --git a/app/views/projects/merge_requests/diffs/_commit_widget.html.haml b/app/views/projects/merge_requests/diffs/_commit_widget.html.haml
index 2e5594f8cbe..dab95b97346 100644
--- a/app/views/projects/merge_requests/diffs/_commit_widget.html.haml
+++ b/app/views/projects/merge_requests/diffs/_commit_widget.html.haml
@@ -1,5 +1,5 @@
- if @commit
- .info-well.hidden-xs.prepend-top-default
+ .info-well.d-none.d-sm-block.prepend-top-default
.well-segment
%ul.blob-commit-info
= render 'projects/commits/commit', commit: @commit, merge_request: @merge_request, view_details: true
diff --git a/app/views/projects/merge_requests/diffs/_diffs.html.haml b/app/views/projects/merge_requests/diffs/_diffs.html.haml
index 986ba5ae02d..bf3df0abf86 100644
--- a/app/views/projects/merge_requests/diffs/_diffs.html.haml
+++ b/app/views/projects/merge_requests/diffs/_diffs.html.haml
@@ -5,9 +5,9 @@
- if @merge_request_diff&.empty?
.row.empty-state.nothing-here-block
- .col-xs-12
+ .col-12
.svg-content= image_tag 'illustrations/merge_request_changes_empty.svg'
- .col-xs-12
+ .col-12
.text-content.text-center
%p
No changes between
@@ -16,6 +16,6 @@
%span.ref-name= @merge_request.target_branch
.text-center= link_to 'Create commit', project_new_blob_path(@project, @merge_request.source_branch), class: 'btn btn-save'
- else
- - diff_viewable = @merge_request_diff ? @merge_request_diff.collected? || @merge_request_diff.overflow? : true
+ - diff_viewable = @merge_request_diff ? @merge_request_diff.viewable? : true
- if diff_viewable
= render "projects/diffs/diffs", diffs: @diffs, environment: @environment, merge_request: true
diff --git a/app/views/projects/merge_requests/diffs/_not_all_comments_displayed.html.haml b/app/views/projects/merge_requests/diffs/_not_all_comments_displayed.html.haml
index 529fbb8547a..8d7138747fb 100644
--- a/app/views/projects/merge_requests/diffs/_not_all_comments_displayed.html.haml
+++ b/app/views/projects/merge_requests/diffs/_not_all_comments_displayed.html.haml
@@ -11,7 +11,7 @@
comparing two versions of the diff
- else
viewing an old version of the diff
- .pull-right
+ .float-right
= link_to diffs_project_merge_request_path(@merge_request.project, @merge_request), class: 'btn btn-sm' do
Show latest version
= "of the diff" if @commit
diff --git a/app/views/projects/merge_requests/diffs/_version_controls.html.haml b/app/views/projects/merge_requests/diffs/_version_controls.html.haml
index 1c26f0405d2..52bf584d550 100644
--- a/app/views/projects/merge_requests/diffs/_version_controls.html.haml
+++ b/app/views/projects/merge_requests/diffs/_version_controls.html.haml
@@ -3,7 +3,7 @@
.mr-version-menus-container.content-block
Changes between
%span.dropdown.inline.mr-version-dropdown
- %a.dropdown-toggle.btn.btn-default{ data: {toggle: :dropdown} }
+ %a.dropdown-toggle.btn.btn-default{ data: { toggle: :dropdown, display: 'static' } }
%span
- if @merge_request_diff.latest?
latest version
@@ -36,7 +36,7 @@
- if @merge_request_diff.base_commit_sha
and
%span.dropdown.inline.mr-version-compare-dropdown
- %a.btn.btn-default.dropdown-toggle{ data: {toggle: :dropdown} }
+ %a.btn.btn-default.dropdown-toggle{ data: { toggle: :dropdown, display: 'static' } }
- if @start_version
version #{version_index(@start_version)}
- else
diff --git a/app/views/projects/merge_requests/invalid.html.haml b/app/views/projects/merge_requests/invalid.html.haml
index 6df19d6438b..749228a9664 100644
--- a/app/views/projects/merge_requests/invalid.html.haml
+++ b/app/views/projects/merge_requests/invalid.html.haml
@@ -10,13 +10,13 @@
- if @merge_request.for_fork? && !@merge_request.source_project
fork project was removed
- elsif !@merge_request.source_branch_exists?
- %span.label.label-inverse= @merge_request.source_branch
+ %span.badge.badge-inverse= @merge_request.source_branch
does not exist in
- %span.label.label-info= @merge_request.source_project_path
+ %span.badge.badge-info= @merge_request.source_project_path
- elsif !@merge_request.target_branch_exists?
- %span.label.label-inverse= @merge_request.target_branch
+ %span.badge.badge-inverse= @merge_request.target_branch
does not exist in
- %span.label.label-info= @merge_request.target_project_path
+ %span.badge.badge-info= @merge_request.target_project_path
- else
of internal error
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index 15a0e4d7ef5..b23baa22d8b 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -20,6 +20,8 @@
window.gl = window.gl || {};
window.gl.mrWidgetData = #{serialize_issuable(@merge_request, serializer: 'widget')}
+ window.gl.mrWidgetData.squash_before_merge_help_path = '#{help_page_path("user/project/merge_requests/squash_and_merge")}';
+
#js-vue-mr-widget.mr-widget
.content-block.content-block-small.emoji-list-container.js-noteable-awards
@@ -30,45 +32,27 @@
.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
.fade-left= icon('angle-left')
.fade-right= icon('angle-right')
- .nav-links.scrolling-tabs
- %ul.merge-request-tabs
- %li.notes-tab
- = tab_link_for @merge_request, :show, force_link: @commit.present? do
- Discussion
- %span.badge= @merge_request.related_notes.user.count
- - if @merge_request.source_project
- %li.commits-tab
- = tab_link_for @merge_request, :commits do
- Commits
- %span.badge= @commits_count
- - if @pipelines.any?
- %li.pipelines-tab
- = tab_link_for @merge_request, :pipelines do
- Pipelines
- %span.badge.js-pipelines-mr-count= @pipelines.size
- %li.diffs-tab
- = tab_link_for @merge_request, :diffs do
- Changes
- %span.badge= @merge_request.diff_size
+ %ul.merge-request-tabs.nav-tabs.nav.nav-links.scrolling-tabs
+ %li.notes-tab
+ = tab_link_for @merge_request, :show, force_link: @commit.present? do
+ Discussion
+ %span.badge.badge-pill= @merge_request.related_notes.user.count
+ - if @merge_request.source_project
+ %li.commits-tab
+ = tab_link_for @merge_request, :commits do
+ Commits
+ %span.badge.badge-pill= @commits_count
+ - if @pipelines.any?
+ %li.pipelines-tab
+ = tab_link_for @merge_request, :pipelines do
+ Pipelines
+ %span.badge.badge-pill.js-pipelines-mr-count= @pipelines.size
+ %li.diffs-tab
+ = tab_link_for @merge_request, :diffs do
+ Changes
+ %span.badge.badge-pill= @merge_request.diff_size
- - if has_vue_discussions_cookie?
- #js-vue-discussion-counter
- - else
- #resolve-count-app.line-resolve-all-container.prepend-top-10{ "v-cloak" => true }
- %resolve-count{ "inline-template" => true, ":logged-out" => "#{current_user.nil?}" }
- %div
- .line-resolve-all{ "v-show" => "discussionCount > 0",
- ":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" }
- %span.line-resolve-btn.is-disabled{ type: "button",
- ":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" }
- %template{ 'v-if' => 'resolvedDiscussionCount === discussionCount' }
- = render 'shared/icons/icon_status_success_solid.svg'
- %template{ 'v-else' => '' }
- = render 'shared/icons/icon_resolve_discussion.svg'
- %span.line-resolve-text
- {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ resolvedCountText }} resolved
- = render "discussions/new_issue_for_all_discussions", merge_request: @merge_request
- = render "discussions/jump_to_next"
+ #js-vue-discussion-counter
.tab-content#diff-notes-app
#notes.notes.tab-pane.voting_notes
@@ -76,20 +60,21 @@
%section.col-md-12
%script.js-notes-data{ type: "application/json" }= initial_notes_data(true).to_json.html_safe
.issuable-discussion.js-vue-notes-event
- = render "projects/merge_requests/discussion"
- - if has_vue_discussions_cookie?
- #js-vue-mr-discussions{ data: { notes_data: notes_data(@merge_request),
- noteable_data: serialize_issuable(@merge_request),
- noteable_type: 'merge_request',
- current_user_data: UserSerializer.new.represent(current_user).to_json} }
+ #js-vue-mr-discussions{ data: { notes_data: notes_data(@merge_request),
+ noteable_data: serialize_issuable(@merge_request),
+ noteable_type: 'MergeRequest',
+ target_type: 'merge_request',
+ current_user_data: UserSerializer.new(project: @project).represent(current_user, {}, MergeRequestUserEntity).to_json} }
#commits.commits.tab-pane
-# This tab is always loaded via AJAX
#pipelines.pipelines.tab-pane
- if @pipelines.any?
= render 'projects/commit/pipelines_list', disable_initialization: true, endpoint: pipelines_project_merge_request_path(@project, @merge_request)
- #diffs.diffs.tab-pane{ data: { "is-locked" => @merge_request.discussion_locked? } }
- -# This tab is always loaded via AJAX
+ #js-diffs-app.diffs.tab-pane{ data: { "is-locked" => @merge_request.discussion_locked?,
+ endpoint: diffs_project_merge_request_path(@project, @merge_request, 'json', request.query_parameters),
+ current_user_data: UserSerializer.new(project: @project).represent(current_user, {}, MergeRequestUserEntity).to_json,
+ project_path: project_path(@merge_request.project)} }
.mr-loading-status
= spinner
diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml
index 2e74b1b83cb..28f0a167128 100644
--- a/app/views/projects/milestones/_form.html.haml
+++ b/app/views/projects/milestones/_form.html.haml
@@ -1,16 +1,18 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @milestone], html: {class: 'form-horizontal milestone-form common-note-form js-quick-submit js-requires-input'} do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @milestone],
+ html: {class: 'milestone-form common-note-form js-quick-submit js-requires-input'},
+ data: { markdown_version: @milestone.cached_markdown_version } do |f|
= form_errors(@milestone)
.row
.col-md-6
- .form-group
- = f.label :title, "Title", class: "control-label"
+ .form-group.row
+ = f.label :title, "Title", class: "col-form-label col-sm-2"
.col-sm-10
- = f.text_field :title, maxlength: 255, class: "form-control", required: true, autofocus: true
- .form-group.milestone-description
- = f.label :description, "Description", class: "control-label"
+ = f.text_field :title, maxlength: 255, class: "qa-milestone-title form-control", required: true, autofocus: true
+ .form-group.row.milestone-description
+ = f.label :description, "Description", class: "col-form-label col-sm-2"
.col-sm-10
= render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project) } do
- = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...'
+ = render 'projects/zen', f: f, attr: :description, classes: 'qa-milestone-description note-textarea', placeholder: 'Write milestone description...'
= render 'shared/notes/hints'
.clearfix
.error-alert
@@ -18,7 +20,7 @@
.form-actions
- if @milestone.new_record?
- = f.submit 'Create milestone', class: "btn-create btn"
+ = f.submit 'Create milestone', class: "btn-create btn qa-milestone-create-button"
= link_to "Cancel", project_milestones_path(@project), class: "btn btn-cancel"
- else
= f.submit 'Save changes', class: "btn-save btn"
diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml
index 5b0197ed58c..26d2ea8447b 100644
--- a/app/views/projects/milestones/index.html.haml
+++ b/app/views/projects/milestones/index.html.haml
@@ -8,7 +8,7 @@
.nav-controls
= render 'shared/milestones_sort_dropdown'
- if can?(current_user, :admin_milestone, @project)
- = link_to new_project_milestone_path(@project), class: "btn btn-new", title: 'New milestone' do
+ = link_to new_project_milestone_path(@project), class: "btn btn-new qa-new-project-milestone", title: 'New milestone' do
New milestone
.milestones
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index 5ec219fdf00..2a9e20c2caa 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -56,11 +56,11 @@
#delete-milestone-modal
- %a.btn.btn-default.btn-grouped.pull-right.visible-xs-block.js-sidebar-toggle{ href: "#" }
+ %a.btn.btn-default.btn-grouped.float-right.d-block.d-sm-none.js-sidebar-toggle{ href: "#" }
= icon('angle-double-left')
.detail-page-description.milestone-detail
- %h2.title
+ %h2.title.qa-milestone-title
= markdown_field(@milestone, :title)
%div
@@ -69,6 +69,8 @@
.wiki
= markdown_field(@milestone, :description)
+ = render_if_exists 'shared/milestones/burndown', milestone: @milestone, project: @project
+
- if can?(current_user, :read_issue, @project) && @milestone.total_items_count(current_user).zero?
.alert.alert-success.prepend-top-default
%span Assign some issues to this milestone.
diff --git a/app/views/projects/mirrors/_push.html.haml b/app/views/projects/mirrors/_push.html.haml
index 4a6aefce351..2b2871a81e5 100644
--- a/app/views/projects/mirrors/_push.html.haml
+++ b/app/views/projects/mirrors/_push.html.haml
@@ -1,5 +1,5 @@
- expanded = Rails.env.test?
-%section.settings.no-animate{ class: ('expanded' if expanded) }
+%section.settings.no-animate#js-push-remote-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
Push to a remote repository
@@ -30,7 +30,7 @@
#{h(@remote_mirror.last_error.strip)}
= f.fields_for :remote_mirrors, @remote_mirror do |rm_form|
.form-group
- = rm_form.check_box :enabled, class: "pull-left"
+ = rm_form.check_box :enabled, class: "float-left"
.prepend-left-20
= rm_form.label :enabled, "Remote mirror repository", class: "label-light append-bottom-0"
%p.light.append-bottom-0
@@ -42,7 +42,7 @@
= render "projects/mirrors/instructions"
.form-group
- = rm_form.check_box :only_protected_branches, class: 'pull-left'
+ = rm_form.check_box :only_protected_branches, class: 'float-left'
.prepend-left-20
= rm_form.label :only_protected_branches, class: 'label-light'
= link_to icon('question-circle'), help_page_path('user/project/protected_branches')
diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml
index 4b7be9a223f..2d3f9116703 100644
--- a/app/views/projects/network/show.html.haml
+++ b/app/views/projects/network/show.html.haml
@@ -9,9 +9,9 @@
= button_tag class: 'btn btn-success' do
= icon('search')
.inline.prepend-left-20
- .checkbox.light
- = label_tag :filter_ref do
- = check_box_tag :filter_ref, 1, @options[:filter_ref]
+ .form-check.light
+ = check_box_tag :filter_ref, 1, @options[:filter_ref], class: 'form-check-input'
+ = label_tag :filter_ref, class: 'form-check-label' do
%span= _("Begin with the selected commit")
- if @commit
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 5beaa3c6d23..5bb1bfb7059 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -28,19 +28,19 @@
%template.push-new-project-tip-template= render partial: "new_project_push_tip"
.col-lg-9.js-toggle-container
- %ul.nav-links.gitlab-tabs{ role: 'tablist' }
- %li{ class: active_when(active_tab == 'blank'), 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{ class: active_when(active_tab == 'template'), 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{ class: active_when(active_tab == 'import'), 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
+ %ul.nav.nav-tabs.nav-links.gitlab-tabs{ role: 'tablist' }
+ %li.nav-item{ role: 'presentation' }
+ %a.nav-link.active{ href: '#blank-project-pane', id: 'blank-project-tab', data: { toggle: 'tab' }, role: 'tab' }
+ %span.d-none.d-sm-block Blank project
+ %span.d-block.d-sm-none Blank
+ %li.nav-item{ role: 'presentation' }
+ %a.nav-link{ href: '#create-from-template-pane', id: 'create-from-template-tab', data: { toggle: 'tab' }, role: 'tab' }
+ %span.d-none.d-sm-block Create from template
+ %span.d-block.d-sm-none Template
+ %li.nav-item{ role: 'presentation' }
+ %a.nav-link{ href: '#import-project-pane', id: 'import-project-tab', data: { toggle: 'tab' }, role: 'tab' }
+ %span.d-none.d-sm-block Import project
+ %span.d-block.d-sm-none Import
.tab-content.gitlab-tab-content
.tab-pane{ id: 'blank-project-pane', class: active_when(active_tab == 'blank'), role: 'tabpanel' }
@@ -63,7 +63,7 @@
%h4 No import options available
%p Contact an administrator to enable options for importing your project.
-.save-project-loader.hide
+.save-project-loader.d-none
.center
%h2
%i.fa.fa-spinner.fa-spin
diff --git a/app/views/projects/no_repo.html.haml b/app/views/projects/no_repo.html.haml
index 14d880028c7..08772a0188b 100644
--- a/app/views/projects/no_repo.html.haml
+++ b/app/views/projects/no_repo.html.haml
@@ -21,4 +21,4 @@
- if can? current_user, :remove_project, @project
.prepend-top-20
- = link_to _('Remove project'), project_path(@project), data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-inverted btn-remove pull-right"
+ = link_to _('Remove project'), project_path(@project), data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-inverted btn-remove float-right"
diff --git a/app/views/projects/pages/_access.html.haml b/app/views/projects/pages/_access.html.haml
index 82e20eeebb3..73ea30e1d3d 100644
--- a/app/views/projects/pages/_access.html.haml
+++ b/app/views/projects/pages/_access.html.haml
@@ -1,8 +1,8 @@
- if @project.pages_deployed?
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
Access pages
- .panel-body
+ .card-body
%p
%strong
Congratulations! Your pages are served under:
diff --git a/app/views/projects/pages/_destroy.haml b/app/views/projects/pages/_destroy.haml
index 7d6c30b7f8d..9b77c4e3494 100644
--- a/app/views/projects/pages/_destroy.haml
+++ b/app/views/projects/pages/_destroy.haml
@@ -1,11 +1,11 @@
- if @project.pages_deployed?
- if can?(current_user, :remove_pages, @project)
- .panel.panel-default.panel.panel-danger
- .panel-heading Remove pages
+ .card.bg-danger
+ .card-header Remove pages
.errors-holder
- .panel-body
+ .card-body
%p
- Removing the pages will prevent from exposing them to outside world.
+ Removing pages will prevent them from being exposed to the outside world.
.form-actions
= link_to 'Remove pages', project_pages_path(@project), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove"
- else
diff --git a/app/views/projects/pages/_https_only.html.haml b/app/views/projects/pages/_https_only.html.haml
index 6a3ffce949f..57345edb90b 100644
--- a/app/views/projects/pages/_https_only.html.haml
+++ b/app/views/projects/pages/_https_only.html.haml
@@ -1,5 +1,5 @@
= form_for @project, url: namespace_project_pages_path(@project.namespace.becomes(Namespace), @project), html: { class: 'inline', title: pages_https_only_title } do |f|
- = f.check_box :pages_https_only, class: 'pull-left', disabled: pages_https_only_disabled?
+ = f.check_box :pages_https_only, class: 'float-left', disabled: pages_https_only_disabled?
.prepend-left-20
= f.label :pages_https_only, class: pages_https_only_label_class do
diff --git a/app/views/projects/pages/_list.html.haml b/app/views/projects/pages/_list.html.haml
index 27bbe52a714..e7178f9160c 100644
--- a/app/views/projects/pages/_list.html.haml
+++ b/app/views/projects/pages/_list.html.haml
@@ -1,10 +1,10 @@
- verification_enabled = Gitlab::CurrentSettings.pages_domain_verification_enabled?
- if can?(current_user, :update_pages, @project) && @domains.any?
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
Domains (#{@domains.count})
- %ul.well-list.pages-domain-list{ class: ("has-verification-status" if verification_enabled) }
+ %ul.content-list.pages-domain-list{ class: ("has-verification-status" if verification_enabled) }
- @domains.each do |domain|
%li.pages-domain-list-item.unstyled
- if verification_enabled
@@ -17,9 +17,9 @@
= icon('external-link')
- if domain.subject
%p
- %span.label.label-gray Certificate: #{domain.subject}
+ %span.badge.badge-gray Certificate: #{domain.subject}
- if domain.expired?
- %span.label.label-danger Expired
+ %span.badge.badge-danger Expired
%div
= link_to 'Details', project_pages_domain_path(@project, domain), class: "btn btn-sm btn-grouped"
= link_to 'Remove', project_pages_domain_path(@project, domain), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped"
diff --git a/app/views/projects/pages/_no_domains.html.haml b/app/views/projects/pages/_no_domains.html.haml
index 7cea5f3e70b..8c93cf7a8ad 100644
--- a/app/views/projects/pages/_no_domains.html.haml
+++ b/app/views/projects/pages/_no_domains.html.haml
@@ -1,6 +1,6 @@
- if can?(current_user, :update_pages, @project)
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
Domains
.nothing-here-block
Support for domains and certificates is disabled.
diff --git a/app/views/projects/pages/_use.html.haml b/app/views/projects/pages/_use.html.haml
index e442e6e9a09..cd9177c0f9e 100644
--- a/app/views/projects/pages/_use.html.haml
+++ b/app/views/projects/pages/_use.html.haml
@@ -1,8 +1,8 @@
- unless @project.pages_deployed?
- .panel.panel-info
- .panel-heading
+ .card.bg-info
+ .card-header
Configure pages
- .panel-body
+ .card-body
%p
Learn how to upload your static site and have it served by
GitLab by following the
diff --git a/app/views/projects/pages/show.html.haml b/app/views/projects/pages/show.html.haml
index 6adaea799b2..7e1a3b9bea6 100644
--- a/app/views/projects/pages/show.html.haml
+++ b/app/views/projects/pages/show.html.haml
@@ -4,7 +4,7 @@
Pages
- if can?(current_user, :update_pages, @project) && (Gitlab.config.pages.external_http || Gitlab.config.pages.external_https)
- = link_to new_project_pages_domain_path(@project), class: 'btn btn-new pull-right', title: 'New Domain' do
+ = link_to new_project_pages_domain_path(@project), class: 'btn btn-new float-right', title: 'New Domain' do
New Domain
%p.light
diff --git a/app/views/projects/pages_domains/_form.html.haml b/app/views/projects/pages_domains/_form.html.haml
index d81b07832bb..0d848f7899c 100644
--- a/app/views/projects/pages_domains/_form.html.haml
+++ b/app/views/projects/pages_domains/_form.html.haml
@@ -4,22 +4,22 @@
- @domain.errors.full_messages.each do |msg|
%p= msg
-.form-group
- = f.label :domain, class: 'control-label' do
+.form-group.row
+ = f.label :domain, class: 'col-form-label col-sm-2' do
Domain
.col-sm-10
= f.text_field :domain, required: true, autocomplete: 'off', class: 'form-control', disabled: @domain.persisted?
- if Gitlab.config.pages.external_https
- .form-group
- = f.label :certificate, class: 'control-label' do
+ .form-group.row
+ = f.label :certificate, class: 'col-form-label col-sm-2' do
Certificate (PEM)
.col-sm-10
= f.text_area :certificate, rows: 5, class: 'form-control'
%span.help-inline Upload a certificate for your domain with all intermediates
- .form-group
- = f.label :key, class: 'control-label' do
+ .form-group.row
+ = f.label :key, class: 'col-form-label col-sm-2' do
Key (PEM)
.col-sm-10
= f.text_area :key, rows: 5, class: 'form-control'
diff --git a/app/views/projects/pages_domains/edit.html.haml b/app/views/projects/pages_domains/edit.html.haml
index 6c404990492..ee70de22f13 100644
--- a/app/views/projects/pages_domains/edit.html.haml
+++ b/app/views/projects/pages_domains/edit.html.haml
@@ -5,7 +5,7 @@
= @domain.domain
%hr.clearfix
%div
- = form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'fieldset-form' } do |f|
= render 'form', { f: f }
.form-actions
= f.submit 'Save Changes', class: "btn btn-save"
diff --git a/app/views/projects/pages_domains/new.html.haml b/app/views/projects/pages_domains/new.html.haml
index 269df803a2b..376ce3f68aa 100644
--- a/app/views/projects/pages_domains/new.html.haml
+++ b/app/views/projects/pages_domains/new.html.haml
@@ -4,9 +4,9 @@
New Pages Domain
%hr.clearfix
%div
- = form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'fieldset-form' } do |f|
= render 'form', { f: f }
.form-actions
= f.submit 'Create New Domain', class: "btn btn-save"
- .pull-right
+ .float-right
= link_to _('Cancel'), project_pages_path(@project), class: 'btn btn-cancel'
diff --git a/app/views/projects/pages_domains/show.html.haml b/app/views/projects/pages_domains/show.html.haml
index 44d66f3b2d0..a8484187493 100644
--- a/app/views/projects/pages_domains/show.html.haml
+++ b/app/views/projects/pages_domains/show.html.haml
@@ -12,7 +12,7 @@
This domain is not verified. You will need to verify ownership before access is enabled.
%h3.page-title.with-button
- = link_to 'Edit', edit_project_pages_domain_path(@project, @domain), class: 'btn btn-success pull-right'
+ = link_to 'Edit', edit_project_pages_domain_path(@project, @domain), class: 'btn btn-success float-right'
Pages Domain
.table-holder
@@ -30,9 +30,9 @@
%td
.input-group
= text_field_tag :domain_dns, dns_record , class: "monospace js-select-on-focus form-control", readonly: true
- .input-group-btn
- = clipboard_button(target: '#domain_dns', class: 'btn-default hidden-xs')
- %p.help-block
+ .input-group-append
+ = clipboard_button(target: '#domain_dns', class: 'btn-default input-group-text d-none d-sm-block')
+ %p.form-text.text-muted
To access this domain create a new DNS record
- if verification_enabled
@@ -43,16 +43,16 @@
%td
= form_tag verify_project_pages_domain_path(@project, @domain) do
.status-badge
- - text, status = @domain.unverified? ? [_('Unverified'), 'label-danger'] : [_('Verified'), 'label-success']
- .label{ class: status }
+ - text, status = @domain.unverified? ? [_('Unverified'), 'badge-danger'] : [_('Verified'), 'badge-success']
+ .badge{ class: status }
= text
%button.btn.has-tooltip{ type: "submit", data: { container: 'body' }, title: _("Retry verification") }
= sprite_icon('redo')
.input-group
= text_field_tag :domain_verification, verification_record, class: "monospace js-select-on-focus form-control", readonly: true
- .input-group-btn
- = clipboard_button(target: '#domain_verification', class: 'btn-default hidden-xs')
- %p.help-block
+ .input-group-append
+ = clipboard_button(target: '#domain_verification', class: 'btn-default d-none d-sm-block')
+ %p.form-text.text-muted
- help_link = help_page_path('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record')
To #{link_to 'verify ownership', help_link} of your domain,
add the above key to a TXT record within to your DNS configuration.
diff --git a/app/views/projects/pipeline_schedules/_form.html.haml b/app/views/projects/pipeline_schedules/_form.html.haml
index 160e325996a..1cdf981fcb4 100644
--- a/app/views/projects/pipeline_schedules/_form.html.haml
+++ b/app/views/projects/pipeline_schedules/_form.html.haml
@@ -1,24 +1,24 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @schedule], as: :schedule, html: { id: "new-pipeline-schedule-form", class: "form-horizontal js-pipeline-schedule-form" } do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @schedule], as: :schedule, html: { id: "new-pipeline-schedule-form", class: "js-pipeline-schedule-form" } do |f|
= form_errors(@schedule)
- .form-group
+ .form-group.row
.col-md-9
= f.label :description, _('Description'), class: 'label-light'
= f.text_field :description, class: 'form-control', required: true, autofocus: true, placeholder: s_('PipelineSchedules|Provide a short description for this pipeline')
- .form-group
+ .form-group.row
.col-md-9
= f.label :cron, _('Interval Pattern'), class: 'label-light'
#interval-pattern-input{ data: { initial_interval: @schedule.cron } }
- .form-group
+ .form-group.row
.col-md-9
= f.label :cron_timezone, _('Cron Timezone'), class: 'label-light'
= dropdown_tag(_("Select a timezone"), options: { toggle_class: 'btn js-timezone-dropdown', title: _("Select a timezone"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: timezone_data } } )
= f.text_field :cron_timezone, value: @schedule.cron_timezone, id: 'schedule_cron_timezone', class: 'hidden', name: 'schedule[cron_timezone]', required: true
- .form-group
+ .form-group.row
.col-md-9
= f.label :ref, _('Target Branch'), class: 'label-light'
= dropdown_tag(_("Select target branch"), options: { toggle_class: 'btn js-target-branch-dropdown', dropdown_class: 'git-revision-dropdown', title: _("Select target branch"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: @project.repository.branch_names, default_branch: @project.default_branch } } )
= f.text_field :ref, value: @schedule.ref, id: 'schedule_ref', class: 'hidden', name: 'schedule[ref]', required: true
- .form-group.js-ci-variable-list-section
+ .form-group.row.js-ci-variable-list-section
.col-md-9
%label.label-light
#{ s_('PipelineSchedules|Variables') }
@@ -32,7 +32,7 @@
= n_('Hide value', 'Hide values', @schedule.variables.size)
- else
= n_('Reveal value', 'Reveal values', @schedule.variables.size)
- .form-group
+ .form-group.row
.col-md-9
= f.label :active, s_('PipelineSchedules|Activated'), class: 'label-light'
%div
diff --git a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
index 55d0e8bb7f9..8d88f0be083 100644
--- a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
+++ b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
@@ -25,7 +25,7 @@
= link_to user_path(pipeline_schedule.owner) do
= pipeline_schedule.owner&.name
%td
- .pull-right.btn-group
+ .float-right.btn-group
- if can?(current_user, :play_pipeline_schedule, pipeline_schedule)
= link_to play_pipeline_schedule_path(pipeline_schedule), method: :post, title: s_('Play'), class: 'btn' do
= icon('play')
diff --git a/app/views/projects/pipeline_schedules/_tabs.html.haml b/app/views/projects/pipeline_schedules/_tabs.html.haml
index 8996c1b3e38..61f6ad34052 100644
--- a/app/views/projects/pipeline_schedules/_tabs.html.haml
+++ b/app/views/projects/pipeline_schedules/_tabs.html.haml
@@ -1,18 +1,18 @@
-%ul.nav-links.mobile-separator
+%ul.nav-links.mobile-separator.nav.nav-tabs
%li{ class: active_when(scope.nil?) }>
= link_to schedule_path_proc.call(nil) do
= s_("PipelineSchedules|All")
- %span.badge.js-totalbuilds-count
+ %span.badge.badge-pill.js-totalbuilds-count
= number_with_delimiter(all_schedules.count(:id))
%li{ class: active_when(scope == 'active') }>
= link_to schedule_path_proc.call('active') do
= s_("PipelineSchedules|Active")
- %span.badge
+ %span.badge.badge-pill
= number_with_delimiter(all_schedules.active.count(:id))
%li{ class: active_when(scope == 'inactive') }>
= link_to schedule_path_proc.call('inactive') do
= s_("PipelineSchedules|Inactive")
- %span.badge
+ %span.badge.badge-pill
= number_with_delimiter(all_schedules.inactive.count(:id))
diff --git a/app/views/projects/pipeline_schedules/index.html.haml b/app/views/projects/pipeline_schedules/index.html.haml
index bcb6dddba1a..3677666070e 100644
--- a/app/views/projects/pipeline_schedules/index.html.haml
+++ b/app/views/projects/pipeline_schedules/index.html.haml
@@ -18,5 +18,5 @@
%ul.content-list
= render partial: "table"
- else
- .light-well
+ .card.bg-light
.nothing-here-block= _("No schedules")
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index 9db30042bf4..04131a90a57 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -27,9 +27,9 @@
.icon-container.commit-icon
= custom_icon("icon_commit")
= link_to @commit.short_id, project_commit_path(@project, @pipeline.sha), class: "commit-sha js-details-short"
- = link_to("#", class: "js-details-expand hidden-xs hidden-sm") do
+ = link_to("#", class: "js-details-expand d-none d-sm-none d-md-inline") do
%span.text-expander
- \...
+ = sprite_icon('ellipsis_h', size: 12)
%span.js-details-content.hide
= link_to @pipeline.sha, project_commit_path(@project, @pipeline.sha), class: "commit-sha commit-hash-full"
= clipboard_button(text: @pipeline.sha, title: "Copy commit SHA to clipboard")
diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml
index 4dbf95be357..951f80b378d 100644
--- a/app/views/projects/pipelines/_with_tabs.html.haml
+++ b/app/views/projects/pipelines/_with_tabs.html.haml
@@ -1,17 +1,17 @@
.tabs-holder
- %ul.pipelines-tabs.nav-links.no-top.no-bottom.mobile-separator
+ %ul.pipelines-tabs.nav-links.no-top.no-bottom.mobile-separator.nav.nav-tabs
%li.js-pipeline-tab-link
- = link_to project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-pipeline', action: 'pipelines', toggle: 'tab' }, class: 'pipeline-tab' do
+ = link_to project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-pipeline', action: 'pipelines', toggle: 'tab' }, class: 'pipeline-tab' do
= _("Pipeline")
%li.js-builds-tab-link
= link_to builds_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-builds', action: 'builds', toggle: 'tab' }, class: 'builds-tab' do
= _("Jobs")
- %span.badge.js-builds-counter= pipeline.total_size
+ %span.badge.badge-pill.js-builds-counter= pipeline.total_size
- if @pipeline.failed_builds.present?
%li.js-failures-tab-link
= link_to failures_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-failures', action: 'failures', toggle: 'tab' }, class: 'failures-tab' do
= _("Failed Jobs")
- %span.badge.js-failures-counter= @pipeline.failed_builds.count
+ %span.badge.badge-pill.js-failures-counter= @pipeline.failed_builds.count
.tab-content
#js-tab-pipeline.tab-pane
@@ -43,12 +43,36 @@
= render partial: "projects/stage/stage", collection: pipeline.legacy_stages, as: :stage
- if @pipeline.failed_builds.present?
- #js-tab-failures.build-failures.tab-pane
- - @pipeline.failed_builds.each_with_index do |build, index|
- .build-state
- %span.ci-status-icon-failed= custom_icon('icon_status_failed')
- %span.stage
- = build.stage.titleize
- %span.build-name
- = link_to build.name, pipeline_job_url(pipeline, build)
- %pre.build-log= build_summary(build, skip: index >= 10)
+ #js-tab-failures.build-failures.tab-pane.build-page
+ %table.table.responsive-table.ci-table.responsive-table-sm-rounded
+ %thead
+ %th.table-th-transparent
+ %th.table-th-transparent= _("Name")
+ %th.table-th-transparent= _("Stage")
+ %th.table-th-transparent= _("Failure")
+
+ %tbody
+ - @pipeline.failed_builds.each_with_index do |build, index|
+ - job = build.present(current_user: current_user)
+ %tr.build-state.responsive-table-border-start
+ %td.responsive-table-cell.ci-status-icon-failed{ data: { column: "Status"} }
+ .d-none.d-md-block.build-icon
+ = custom_icon("icon_status_#{build.status}")
+ .d-md-none.build-badge
+ = render "ci/status/badge", link: false, status: job.detailed_status(current_user)
+ %td.responsive-table-cell.build-name{ data: { column: _("Name")} }
+ = link_to build.name, pipeline_job_url(pipeline, build)
+ %td.responsive-table-cell.build-stage{ data: { column: _("Stage")} }
+ = build.stage.titleize
+ %td.responsive-table-cell.build-failure{ data: { column: _("Failure")} }
+ = build.present.callout_failure_message
+ %td.responsive-table-cell.build-actions
+ = link_to retry_project_job_path(build.project, build, return_to: request.original_url), method: :post, title: _('Retry'), class: 'btn btn-build' do
+ = icon('repeat')
+ %tr.build-trace-row.responsive-table-border-end
+ %td
+ %td.responsive-table-cell.build-trace-container{ colspan: 4 }
+ %pre.build-trace.build-trace-rounded
+ %code.bash.js-build-output
+ = build_summary(build)
+
diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml
index 81984ee94b0..956f8fef6b8 100644
--- a/app/views/projects/pipelines/new.html.haml
+++ b/app/views/projects/pipelines/new.html.haml
@@ -6,9 +6,9 @@
= s_("Pipeline|Run Pipeline")
%hr
-= form_for @pipeline, as: :pipeline, url: project_pipelines_path(@project), html: { id: "new-pipeline-form", class: "form-horizontal js-new-pipeline-form js-requires-input" } do |f|
+= form_for @pipeline, as: :pipeline, url: project_pipelines_path(@project), html: { id: "new-pipeline-form", class: "js-new-pipeline-form js-requires-input" } do |f|
= form_errors(@pipeline)
- .form-group
+ .form-group.row
.col-sm-12
= f.label :ref, s_('Pipeline|Create for')
= hidden_field_tag 'pipeline[ref]', params[:ref] || @project.default_branch
@@ -16,7 +16,7 @@
options: { toggle_class: 'js-branch-select wide git-revision-dropdown-toggle',
filter: true, dropdown_class: "dropdown-menu-selectable git-revision-dropdown", placeholder: s_("Pipeline|Search branches"),
data: { selected: params[:ref] || @project.default_branch, field_name: 'pipeline[ref]' } })
- .help-block
+ .form-text.text-muted
= s_("Pipeline|Existing branch name or tag")
.col-sm-12.prepend-top-10.js-ci-variable-list-section
@@ -24,12 +24,12 @@
= s_('Pipeline|Variables')
%ul.ci-variable-list
= render 'ci/variables/variable_row', form_field: 'pipeline', only_key_value: true
- .help-block
+ .form-text.text-muted
= (s_("Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default.") % {settings_link: settings_link}).html_safe
.form-actions
= f.submit s_('Pipeline|Create pipeline'), class: 'btn btn-success js-variables-save-button', tabindex: 3
- = link_to 'Cancel', project_pipelines_path(@project), class: 'btn btn-default pull-right'
+ = link_to 'Cancel', project_pipelines_path(@project), class: 'btn btn-default float-right'
-# haml-lint:disable InlineJavaScript
%script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe
diff --git a/app/views/projects/project_members/_groups.html.haml b/app/views/projects/project_members/_groups.html.haml
index fdeb5f21fbe..3f05e06b0c6 100644
--- a/app/views/projects/project_members/_groups.html.haml
+++ b/app/views/projects/project_members/_groups.html.haml
@@ -1,7 +1,7 @@
-.panel.panel-default.project-members-groups
- .panel-heading
+.card.project-members-groups
+ .card-header
Groups with access to
%strong= @project.name
- %span.badge= group_links.size
- %ul.content-list
+ %span.badge.badge-pill= group_links.size
+ %ul.content-list.members-list
= render partial: 'shared/members/group', collection: group_links, as: :group_link
diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml
index bf5b11ea30c..5064db8d5cd 100644
--- a/app/views/projects/project_members/_new_project_member.html.haml
+++ b/app/views/projects/project_members/_new_project_member.html.haml
@@ -9,7 +9,7 @@
.select-wrapper
= select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "form-control project-access-select select-control"
= icon('chevron-down')
- .help-block.append-bottom-10
+ .form-text.text-muted.append-bottom-10
= link_to "Read more", help_page_path("user/permissions"), class: "vlink"
about role permissions
.form-group
diff --git a/app/views/projects/project_members/_new_shared_group.html.haml b/app/views/projects/project_members/_new_shared_group.html.haml
index c10ef648a8f..684219735e2 100644
--- a/app/views/projects/project_members/_new_shared_group.html.haml
+++ b/app/views/projects/project_members/_new_shared_group.html.haml
@@ -9,7 +9,7 @@
.select-wrapper
= select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control select-control"
= icon('chevron-down')
- .help-block.append-bottom-10
+ .form-text.text-muted.append-bottom-10
= link_to "Read more", help_page_path("user/permissions"), class: "vlink"
about role permissions
.form-group
diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml
index 16bcf671c25..0c5a187f208 100644
--- a/app/views/projects/project_members/_team.html.haml
+++ b/app/views/projects/project_members/_team.html.haml
@@ -1,17 +1,18 @@
- project = local_assigns.fetch(:project)
- members = local_assigns.fetch(:members)
-.panel.panel-default
- .panel-heading.flex-project-members-panel
+.card
+ .card-header.flex-project-members-panel
%span.flex-project-title
Members of
%strong= project.name
- %span.badge= members.total_count
+ %span.badge.badge-pill= members.total_count
= form_tag project_project_members_path(project), method: :get, class: 'form-inline member-search-form flex-project-members-form' do
.form-group
- = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
- %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
- = icon("search")
+ .position-relative
+ = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
+ %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
+ = icon("search")
= render 'shared/members/sort_dropdown'
%ul.content-list.members-list
= render partial: 'shared/members/member', collection: members, as: :member
diff --git a/app/views/projects/project_members/import.html.haml b/app/views/projects/project_members/import.html.haml
index 755128af565..6a52e72bfd8 100644
--- a/app/views/projects/project_members/import.html.haml
+++ b/app/views/projects/project_members/import.html.haml
@@ -5,9 +5,9 @@
%p.light
Only project members will be imported. Group members will be skipped.
%hr
-= form_tag apply_import_project_project_members_path(@project), method: 'post', class: 'form-horizontal' do
- .form-group
- = label_tag :source_project_id, "Project", class: 'control-label'
+= form_tag apply_import_project_project_members_path(@project), method: 'post' do
+ .form-group.row
+ = label_tag :source_project_id, "Project", class: 'col-form-label col-sm-2'
.col-sm-10= select_tag(:source_project_id, options_from_collection_for_select(@projects, :id, :name_with_namespace), prompt: "Select project", class: "select2 lg", required: true)
.form-actions
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index d81103c3a92..9716322f8a1 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -12,17 +12,17 @@
- else
%p
Members can be added by project
- %i Masters
+ %i Maintainers
or
%i Owners
.light
- if can?(current_user, :admin_project_member, @project)
- %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
+ %ul.nav-links.nav.nav-tabs.gitlab-tabs{ role: 'tablist' }
+ %li.nav-tab{ role: 'presentation' }
+ %a.nav-link.active{ 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
+ %li.nav-tab{ role: 'presentation' }
+ %a.nav-link{ href: '#share-with-group-pane', id: 'share-with-group-tab', data: { toggle: 'tab' }, role: 'tab' } Share with group
.tab-content.gitlab-tab-content
.tab-pane.active{ id: 'add-member-pane', role: 'tabpanel' }
diff --git a/app/views/projects/protected_branches/shared/_branches_list.html.haml b/app/views/projects/protected_branches/shared/_branches_list.html.haml
index d1ed438eb21..9a06eca89bb 100644
--- a/app/views/projects/protected_branches/shared/_branches_list.html.haml
+++ b/app/views/projects/protected_branches/shared/_branches_list.html.haml
@@ -1,7 +1,7 @@
.protected-branches-list.js-protected-branches-list.qa-protected-branches-list
- if @protected_branches.empty?
- .panel-heading
- %h3.panel-title
+ .card-header.bg-white
+ %h3.card-title.mb-0
Protected branch (#{@protected_branches_count})
%p.settings-message.text-center
There are currently no protected branches, protect a branch with the form above.
diff --git a/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml b/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml
index 9f0c4f3b3a8..c2d6c034e35 100644
--- a/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml
@@ -1,33 +1,32 @@
= form_for [@project.namespace.becomes(Namespace), @project, @protected_branch], html: { class: 'new-protected-branch js-new-protected-branch' } do |f|
- .panel.panel-default
- .panel-heading
- %h3.panel-title
+ .card
+ .card-header
+ %h3.card-title
Protect a branch
- .panel-body
- .form-horizontal
- = form_errors(@protected_branch)
- .form-group
- = f.label :name, class: 'col-md-2 text-right' do
- Branch:
- .col-md-10
- = render partial: "projects/protected_branches/shared/dropdown", locals: { f: f }
- .help-block
- = link_to 'Wildcards', help_page_path('user/project/protected_branches', anchor: 'wildcard-protected-branches')
- such as
- %code *-stable
- or
- %code production/*
- are supported
- .form-group
- %label.col-md-2.text-right{ for: 'merge_access_levels_attributes' }
- Allowed to merge:
- .col-md-10
- = yield :merge_access_levels
- .form-group
- %label.col-md-2.text-right{ for: 'push_access_levels_attributes' }
- Allowed to push:
- .col-md-10
- = yield :push_access_levels
+ .card-body
+ = form_errors(@protected_branch)
+ .form-group.row
+ = f.label :name, class: 'col-md-2 text-right' do
+ Branch:
+ .col-md-10
+ = render partial: "projects/protected_branches/shared/dropdown", locals: { f: f }
+ .form-text.text-muted
+ = link_to 'Wildcards', help_page_path('user/project/protected_branches', anchor: 'wildcard-protected-branches')
+ such as
+ %code *-stable
+ or
+ %code production/*
+ are supported
+ .form-group.row
+ %label.col-md-2.text-right{ for: 'merge_access_levels_attributes' }
+ Allowed to merge:
+ .col-md-10
+ = yield :merge_access_levels
+ .form-group.row
+ %label.col-md-2.text-right{ for: 'push_access_levels_attributes' }
+ Allowed to push:
+ .col-md-10
+ = yield :push_access_levels
- .panel-footer
+ .card-footer
= f.submit 'Protect', class: 'btn-create btn', disabled: true
diff --git a/app/views/projects/protected_branches/shared/_index.html.haml b/app/views/projects/protected_branches/shared/_index.html.haml
index fd5c1aa342a..4f1c6c92484 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.no-animate{ class: ('expanded' if expanded) }
+%section.qa-protected-branches-settings.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
Protected Branches
@@ -12,8 +12,8 @@
%p
By default, protected branches are designed to:
%ul
- %li prevent their creation, if not already created, from everybody except Masters
- %li prevent pushes from everybody except Masters
+ %li prevent their creation, if not already created, from everybody except Maintainers
+ %li prevent pushes from everybody except Maintainers
%li prevent <strong>anyone</strong> from force pushing to the branch
%li prevent <strong>anyone</strong> from deleting the branch
%p Read more about #{link_to "protected branches", help_page_path("user/project/protected_branches")} and #{link_to "project permissions", help_page_path("user/permissions")}.
diff --git a/app/views/projects/protected_branches/shared/_matching_branch.html.haml b/app/views/projects/protected_branches/shared/_matching_branch.html.haml
index 98793d632e6..2c76bf87945 100644
--- a/app/views/projects/protected_branches/shared/_matching_branch.html.haml
+++ b/app/views/projects/protected_branches/shared/_matching_branch.html.haml
@@ -3,7 +3,7 @@
= link_to matching_branch.name, project_ref_path(@project, matching_branch.name), class: 'ref-name'
- if @project.root_ref?(matching_branch.name)
- %span.label.label-info.prepend-left-5 default
+ %span.badge.badge-info.prepend-left-5 default
%td
- commit = @project.commit(matching_branch.name)
= link_to(commit.short_id, project_commit_path(@project, commit.id), class: 'commit-sha')
diff --git a/app/views/projects/protected_branches/shared/_protected_branch.html.haml b/app/views/projects/protected_branches/shared/_protected_branch.html.haml
index 2d3b2af00c2..82ef08272d3 100644
--- a/app/views/projects/protected_branches/shared/_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/shared/_protected_branch.html.haml
@@ -5,7 +5,7 @@
%span.ref-name.qa-protected-branch-name= protected_branch.name
- if @project.root_ref?(protected_branch.name)
- %span.label.label-info.prepend-left-5 default
+ %span.badge.badge-info.prepend-left-5 default
%td
- if protected_branch.wildcard?
- matching_branches = protected_branch.matching(repository.branches)
diff --git a/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml b/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml
index 5a53c704fcb..0ae5ca3ff36 100644
--- a/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml
+++ b/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml
@@ -1,29 +1,28 @@
= form_for [@project.namespace.becomes(Namespace), @project, @protected_tag], html: { class: 'new-protected-tag js-new-protected-tag' } do |f|
- .panel.panel-default
- .panel-heading
- %h3.panel-title
+ .card
+ .card-header
+ %h3.card-title
Protect a tag
- .panel-body
- .form-horizontal
- = form_errors(@protected_tag)
- .form-group
- = f.label :name, class: 'col-md-2 text-right' do
- Tag:
- .col-md-10.protected-tags-dropdown
- = render partial: "projects/protected_tags/shared/dropdown", locals: { f: f }
- .help-block
- = link_to 'Wildcards', help_page_path('user/project/protected_tags', anchor: 'wildcard-protected-tags')
- such as
- %code v*
- or
- %code *-release
- are supported
- .form-group
- %label.col-md-2.text-right{ for: 'create_access_levels_attributes' }
- Allowed to create:
- .col-md-10
- .create_access_levels-container
- = yield :create_access_levels
+ .card-body
+ = form_errors(@protected_tag)
+ .form-group.row
+ = f.label :name, class: 'col-md-2 text-right' do
+ Tag:
+ .col-md-10.protected-tags-dropdown
+ = render partial: "projects/protected_tags/shared/dropdown", locals: { f: f }
+ .form-text.text-muted
+ = link_to 'Wildcards', help_page_path('user/project/protected_tags', anchor: 'wildcard-protected-tags')
+ such as
+ %code v*
+ or
+ %code *-release
+ are supported
+ .form-group.row
+ %label.col-md-2.text-right{ for: 'create_access_levels_attributes' }
+ Allowed to create:
+ .col-md-10
+ .create_access_levels-container
+ = yield :create_access_levels
- .panel-footer
+ .card-footer
= f.submit 'Protect', class: 'btn-create btn', disabled: true
diff --git a/app/views/projects/protected_tags/shared/_index.html.haml b/app/views/projects/protected_tags/shared/_index.html.haml
index c33723d8072..9a50a51e4be 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.no-animate{ class: ('expanded' if expanded) }
+%section.settings.no-animate#js-protected-tags-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
Protected Tags
@@ -12,7 +12,7 @@
%p
By default, protected tags are designed to:
%ul
- %li Prevent tag creation by everybody except Masters
+ %li Prevent tag creation by everybody except Maintainers
%li Prevent <strong>anyone</strong> from updating the tag
%li Prevent <strong>anyone</strong> from deleting the tag
diff --git a/app/views/projects/protected_tags/shared/_matching_tag.html.haml b/app/views/projects/protected_tags/shared/_matching_tag.html.haml
index 05f102d1ca3..133c76cd2ad 100644
--- a/app/views/projects/protected_tags/shared/_matching_tag.html.haml
+++ b/app/views/projects/protected_tags/shared/_matching_tag.html.haml
@@ -3,7 +3,7 @@
= link_to matching_tag.name, project_ref_path(@project, matching_tag.name), class: 'ref-name'
- if @project.root_ref?(matching_tag.name)
- %span.label.label-info.prepend-left-5 default
+ %span.badge.badge-info.prepend-left-5 default
%td
- commit = @project.commit(matching_tag.name)
= link_to(commit.short_id, project_commit_path(@project, commit.id), class: 'commit-sha')
diff --git a/app/views/projects/protected_tags/shared/_protected_tag.html.haml b/app/views/projects/protected_tags/shared/_protected_tag.html.haml
index c778f7b9781..1702e38df7e 100644
--- a/app/views/projects/protected_tags/shared/_protected_tag.html.haml
+++ b/app/views/projects/protected_tags/shared/_protected_tag.html.haml
@@ -3,7 +3,7 @@
%span.ref-name= protected_tag.name
- if @project.root_ref?(protected_tag.name)
- %span.label.label-info.prepend-left-5 default
+ %span.badge.badge-info.prepend-left-5 default
%td
- if protected_tag.wildcard?
- matching_tags = protected_tag.matching(repository.tags)
diff --git a/app/views/projects/protected_tags/shared/_tags_list.html.haml b/app/views/projects/protected_tags/shared/_tags_list.html.haml
index 3ed82e51dbe..c3081d75fb4 100644
--- a/app/views/projects/protected_tags/shared/_tags_list.html.haml
+++ b/app/views/projects/protected_tags/shared/_tags_list.html.haml
@@ -1,7 +1,7 @@
.protected-tags-list.js-protected-tags-list
- if @protected_tags.empty?
- .panel-heading
- %h3.panel-title
+ .card-header
+ %h3.card-title
Protected tag (#{@protected_tags_count})
%p.settings-message.text-center
There are currently no protected tags, protect a tag with the form above.
diff --git a/app/views/projects/refs/logs_tree.js.haml b/app/views/projects/refs/logs_tree.js.haml
index d07bb661615..506bf54b3f8 100644
--- a/app/views/projects/refs/logs_tree.js.haml
+++ b/app/views/projects/refs/logs_tree.js.haml
@@ -8,6 +8,8 @@
row.find("td.tree-time-ago").html('#{escape_javascript time_ago_with_tooltip(commit.committed_date)}');
row.find("td.tree-commit").html('#{escape_javascript render("projects/tree/tree_commit_column", commit: commit)}');
+ = render_if_exists 'projects/refs/logs_tree_lock_label', lock_label: content_data[:lock_label]
+
- if @more_log_url
:plain
if($('#tree-slider').length) {
diff --git a/app/views/projects/registry/repositories/_tag.html.haml b/app/views/projects/registry/repositories/_tag.html.haml
index 0223372bff8..a4cde53e8c6 100644
--- a/app/views/projects/registry/repositories/_tag.html.haml
+++ b/app/views/projects/registry/repositories/_tag.html.haml
@@ -24,7 +24,7 @@
\-
- if can?(current_user, :update_container_image, @project)
%td.content
- .controls.hidden-xs.pull-right
+ .controls.d-none.d-sm-block.float-right
= link_to project_registry_repository_tag_path(@project, tag.repository, tag.name),
method: :delete,
class: 'btn btn-remove has-tooltip',
diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml
index 2a683d5be0f..0426f2215ad 100644
--- a/app/views/projects/registry/repositories/index.html.haml
+++ b/app/views/projects/registry/repositories/index.html.haml
@@ -16,11 +16,11 @@
.row.prepend-top-10
.col-lg-12
- .panel.panel-default
- .panel-heading
- %h4.panel-title
+ .card
+ .card-header
+ %h4.card-title
= s_('ContainerRegistry|How to use the Container Registry')
- .panel-body
+ .card-body
%p
- link_token = link_to(_('personal access token'), help_page_path('user/profile/account/two_factor_authentication', anchor: 'personal-access-tokens'), target: '_blank')
- link_2fa = link_to(_('2FA enabled'), help_page_path('user/profile/account/two_factor_authentication'), target: '_blank')
diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml
index 4d962f9433f..8093cc2c2d7 100644
--- a/app/views/projects/releases/edit.html.haml
+++ b/app/views/projects/releases/edit.html.haml
@@ -11,9 +11,11 @@
%strong= @tag.name
- = form_for(@release, method: :put, url: project_tag_release_path(@project, @tag.name), html: { class: 'form-horizontal common-note-form release-form js-quick-submit' }) do |f|
+ = form_for(@release, method: :put, url: project_tag_release_path(@project, @tag.name),
+ html: { class: 'common-note-form release-form js-quick-submit' },
+ data: { markdown_version: @release.cached_markdown_version }) do |f|
= render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do
- = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here..."
+ = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here…"
= render 'shared/notes/hints'
.error-alert
.prepend-top-default
diff --git a/app/views/projects/repositories/_feed.html.haml b/app/views/projects/repositories/_feed.html.haml
index 87895a15239..ae0d9ab9908 100644
--- a/app/views/projects/repositories/_feed.html.haml
+++ b/app/views/projects/repositories/_feed.html.haml
@@ -14,5 +14,5 @@
= image_tag avatar_icon_for_email(commit.author_email), class: "", width: 16, alt: ''
= markdown(truncate(commit.title, length: 40), pipeline: :single_line, author: commit.author)
%td
- %span.pull-right.cgray
+ %span.float-right.cgray
= time_ago_with_tooltip(commit.committed_date)
diff --git a/app/views/projects/runners/_group_runners.html.haml b/app/views/projects/runners/_group_runners.html.haml
index dfed0553f84..86de71c732b 100644
--- a/app/views/projects/runners/_group_runners.html.haml
+++ b/app/views/projects/runners/_group_runners.html.haml
@@ -26,9 +26,9 @@
- if can?(current_user, :admin_pipeline, @project.group)
- group_link = link_to _('Group CI/CD settings'), group_settings_ci_cd_path(@project.group)
- = _('Group masters can register group runners in the %{link}').html_safe % { link: group_link }
+ = _('Group maintainers can register group runners in the %{link}').html_safe % { link: group_link }
- else
- = _('Ask your group master to setup a group Runner.')
+ = _('Ask your group maintainer to setup a group Runner.')
- else
%h4.underlined-title
diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml
index 69218f344f7..6ee83fae25e 100644
--- a/app/views/projects/runners/_runner.html.haml
+++ b/app/views/projects/runners/_runner.html.haml
@@ -15,7 +15,7 @@
%span.commit-sha
= runner.short_sha
- .pull-right
+ .float-right
- if @project_runners.include?(runner)
- if runner.active?
= link_to _('Pause'), pause_project_runner_path(@project, runner), method: :post, class: 'btn btn-sm btn-danger', data: { confirm: _("Are you sure?") }
@@ -26,11 +26,11 @@
- else
- runner_project = @project.runner_projects.find_by(runner_id: runner)
= link_to _('Disable for this project'), project_runner_project_path(@project, runner_project), data: { confirm: _("Are you sure?") }, method: :delete, class: 'btn btn-danger btn-sm'
- - elsif !(runner.is_shared? || runner.group_type?) # We can simplify this to `runner.project_type?` when migrating #runner_type is complete
+ - elsif runner.project_type?
= form_for [@project.namespace.becomes(Namespace), @project, @project.runner_projects.new] do |f|
= f.hidden_field :runner_id, value: runner.id
= f.submit _('Enable for this project'), class: 'btn btn-sm'
- .pull-right
+ .float-right
%small.light
\##{runner.id}
- if runner.description.present?
@@ -39,5 +39,5 @@
- if runner.tag_list.present?
%p
- runner.tag_list.sort.each do |tag|
- %span.label.label-primary
+ %span.badge.badge-primary
= tag
diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml
index 684b082efbb..aa30ebdc3b8 100644
--- a/app/views/projects/services/_form.html.haml
+++ b/app/views/projects/services/_form.html.haml
@@ -9,7 +9,7 @@
- if @service.respond_to?(:detailed_description)
%p= @service.detailed_description
.col-lg-9
- = form_for(@service, as: :service, url: project_service_path(@project, @service.to_param), method: :put, html: { class: 'gl-show-field-errors form-horizontal integration-settings-form js-integration-settings-form', data: { 'can-test' => @service.can_test?, 'test-url' => test_project_service_path(@project, @service) } }) do |form|
+ = form_for(@service, as: :service, url: project_service_path(@project, @service.to_param), method: :put, html: { class: 'gl-show-field-errors integration-settings-form js-integration-settings-form', data: { 'can-test' => @service.can_test?, 'test-url' => test_project_service_path(@project, @service) } }) do |form|
= render 'shared/service_settings', form: form, subject: @service
- if @service.editable?
.footer-block.row-content-block
diff --git a/app/views/projects/services/_index.html.haml b/app/views/projects/services/_index.html.haml
index dac7d4d1bbb..16e48814578 100644
--- a/app/views/projects/services/_index.html.haml
+++ b/app/views/projects/services/_index.html.haml
@@ -8,13 +8,13 @@
%colgroup
%col
%col
- %col.hidden-xs
+ %col
%col{ width: "120" }
%thead
%tr
%th
%th Service
- %th.hidden-xs Description
+ %th.d-none.d-sm-block Description
%th Last edit
- @services.sort_by(&:title).each do |service|
%tr
@@ -23,7 +23,7 @@
%td
= link_to edit_project_service_path(@project, service.to_param) do
%strong= service.title
- %td.hidden-xs
+ %td.d-none.d-sm-block
= service.description
%td.light
- if service.updated_at.present?
diff --git a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
index 2ab0227126a..9314804c5dd 100644
--- a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
+++ b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
@@ -6,34 +6,34 @@
1.
= link_to 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands', target: '_blank', rel: 'noopener noreferrer nofollow' do
Enable custom slash commands
- = icon('external-link')
+ = sprite_icon('external-link', size: 16)
on your Mattermost installation
%li
2.
= link_to 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command', target: '_blank', rel: 'noopener noreferrer nofollow' do
Add a slash command
- = icon('external-link')
+ = sprite_icon('external-link', size: 16)
in your Mattermost team with these options:
%hr
.help-form
.form-group
- = label_tag :display_name, 'Display name', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :display_name, "GitLab / #{@project.full_name}", class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(target: '#display_name')
+ = label_tag :display_name, 'Display name', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.input-group
+ = text_field_tag :display_name, "GitLab / #{@project.full_name}", class: 'form-control form-control-sm', readonly: 'readonly'
+ .input-group-append
+ = clipboard_button(target: '#display_name', class: 'input-group-text')
.form-group
- = label_tag :description, 'Description', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(target: '#description')
+ = label_tag :description, 'Description', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.input-group
+ = text_field_tag :description, run_actions_text, class: 'form-control form-control-sm', readonly: 'readonly'
+ .input-group-append
+ = clipboard_button(target: '#description', class: 'input-group-text')
.form-group
- = label_tag nil, 'Command trigger word', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.text-block
+ = label_tag nil, 'Command trigger word', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.text-block
%p Fill in the word that works best for your team.
%p
Suggestions:
@@ -42,47 +42,47 @@
%code= @project.full_path
.form-group
- = label_tag :request_url, 'Request URL', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :request_url, service_trigger_url(subject), class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(target: '#request_url')
+ = label_tag :request_url, 'Request URL', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.input-group
+ = text_field_tag :request_url, service_trigger_url(subject), class: 'form-control form-control-sm', readonly: 'readonly'
+ .input-group-append
+ = clipboard_button(target: '#request_url', class: 'input-group-text')
.form-group
- = label_tag nil, 'Request method', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.text-block POST
+ = label_tag nil, 'Request method', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.text-block POST
.form-group
- = label_tag :response_username, 'Response username', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :response_username, 'GitLab', class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(target: '#response_username')
+ = label_tag :response_username, 'Response username', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.input-group
+ = text_field_tag :response_username, 'GitLab', class: 'form-control form-control-sm', readonly: 'readonly'
+ .input-group-append
+ = clipboard_button(target: '#response_username', class: 'input-group-text')
.form-group
- = label_tag :response_icon, 'Response icon', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :response_icon, asset_url('gitlab_logo.png'), class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(target: '#response_icon')
+ = label_tag :response_icon, 'Response icon', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.input-group
+ = text_field_tag :response_icon, asset_url('gitlab_logo.png'), class: 'form-control form-control-sm', readonly: 'readonly'
+ .input-group-append
+ = clipboard_button(target: '#response_icon', class: 'input-group-text')
.form-group
- = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.text-block Yes
+ = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.text-block Yes
.form-group
- = label_tag :autocomplete_hint, 'Autocomplete hint', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :autocomplete_hint, '[help]', class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(target: '#autocomplete_hint')
+ = label_tag :autocomplete_hint, 'Autocomplete hint', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.input-group
+ = text_field_tag :autocomplete_hint, '[help]', class: 'form-control form-control-sm', readonly: 'readonly'
+ .input-group-append
+ = clipboard_button(target: '#autocomplete_hint', class: 'input-group-text')
.form-group
- = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(target: '#autocomplete_description')
+ = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.input-group
+ = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control form-control-sm', readonly: 'readonly'
+ .input-group-append
+ = clipboard_button(target: '#autocomplete_description', class: 'input-group-text')
%hr
diff --git a/app/views/projects/services/mattermost_slash_commands/_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_help.html.haml
index 2a1b9d4c465..f51dd581d29 100644
--- a/app/views/projects/services/mattermost_slash_commands/_help.html.haml
+++ b/app/views/projects/services/mattermost_slash_commands/_help.html.haml
@@ -1,18 +1,19 @@
- enabled = Gitlab.config.mattermost.enabled
-.well
- %p
- This service allows users to perform common operations on this
- project by entering slash commands in Mattermost.
- = link_to help_page_path('user/project/integrations/mattermost_slash_commands.md'), target: '_blank' do
- View documentation
- = icon('external-link')
- %p.inline
- See list of available commands in Mattermost after setting up this service,
- by entering
- %kbd.inline /&lt;trigger&gt; help
- - unless enabled || @service.template?
- = render 'projects/services/mattermost_slash_commands/detailed_help', subject: @service
+.info-well
+ .well-segment
+ %p
+ This service allows users to perform common operations on this
+ project by entering slash commands in Mattermost.
+ = link_to help_page_path('user/project/integrations/mattermost_slash_commands.md'), target: '_blank' do
+ View documentation
+ = sprite_icon('external-link', size: 16)
+ %p.inline
+ See list of available commands in Mattermost after setting up this service,
+ by entering
+ %kbd.inline /&lt;trigger&gt; help
+ - unless enabled || @service.template?
+ = render 'projects/services/mattermost_slash_commands/detailed_help', subject: @service
- if enabled && !@service.template?
= render 'projects/services/mattermost_slash_commands/installation_info', subject: @service
diff --git a/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml b/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml
index 44c0b7a90dc..2da8e5428ec 100644
--- a/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml
+++ b/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml
@@ -1,7 +1,7 @@
.services-installation-info
- unless @service.activated?
.row
- .col-sm-9.col-sm-offset-3
+ .col-sm-9.offset-sm-3
= link_to new_project_mattermost_path(@project), class: 'btn btn-lg' do
= custom_icon('mattermost_logo', size: 15)
Add to Mattermost
diff --git a/app/views/projects/services/prometheus/_configuration_banner.html.haml b/app/views/projects/services/prometheus/_configuration_banner.html.haml
index 2cc2a6b2b5b..898b55e4b39 100644
--- a/app/views/projects/services/prometheus/_configuration_banner.html.haml
+++ b/app/views/projects/services/prometheus/_configuration_banner.html.haml
@@ -2,7 +2,7 @@
= s_('PrometheusService|Auto configuration')
- if service.manual_configuration?
- .well
+ .info-well
= s_('PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below')
- else
.container-fluid
diff --git a/app/views/projects/services/prometheus/_help.html.haml b/app/views/projects/services/prometheus/_help.html.haml
index 88acb824ba7..35d655e4b32 100644
--- a/app/views/projects/services/prometheus/_help.html.haml
+++ b/app/views/projects/services/prometheus/_help.html.haml
@@ -5,5 +5,5 @@
= s_('PrometheusService|Manual configuration')
- unless @service.editable?
- .well
+ .info-well
= s_('PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters')
diff --git a/app/views/projects/services/prometheus/_show.html.haml b/app/views/projects/services/prometheus/_show.html.haml
index 43e6a173108..bda597cc02b 100644
--- a/app/views/projects/services/prometheus/_show.html.haml
+++ b/app/views/projects/services/prometheus/_show.html.haml
@@ -7,12 +7,12 @@
= 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: active_common_project_prometheus_metrics_path(@project, :json), metrics_help_path: help_page_path('user/project/integrations/prometheus_library/metrics') } }
- .panel-heading
- %h3.panel-title
+ .card.js-panel-monitored-metrics{ data: { active_metrics: active_common_project_prometheus_metrics_path(@project, :json), metrics_help_path: help_page_path('user/project/integrations/prometheus_library/metrics') } }
+ .card-header
+ %h3.card-title
= s_('PrometheusService|Common metrics')
- %span.badge.js-monitored-count 0
- .panel-body
+ %span.badge.badge-pill.js-monitored-count 0
+ .card-body
.loading-metrics.js-loading-metrics
%p.prepend-top-10.prepend-left-10
= icon('spinner spin', class: 'metrics-load-spinner')
@@ -22,13 +22,13 @@
= s_('PrometheusService|Waiting for your first deployment to an environment to find common metrics')
%ul.list-unstyled.metrics-list.hidden.js-metrics-list
- .panel.panel-default.hidden.js-panel-missing-env-vars
- .panel-heading
- %h3.panel-title
+ .card.hidden.js-panel-missing-env-vars
+ .card-header
+ %h3.card-title
= icon('caret-right lg fw', class: 'panel-toggle js-panel-toggle', 'aria-label' => 'Toggle panel')
= s_('PrometheusService|Missing environment variable')
- %span.badge.js-env-var-count 0
- .panel-body.hidden
+ %span.badge.badge-pill.js-env-var-count 0
+ .card-body.hidden
.flash-container
.flash-notice
.flash-text
diff --git a/app/views/projects/services/slack_slash_commands/_help.html.haml b/app/views/projects/services/slack_slash_commands/_help.html.haml
index d592a5e4663..f25d2ecdfb1 100644
--- a/app/views/projects/services/slack_slash_commands/_help.html.haml
+++ b/app/views/projects/services/slack_slash_commands/_help.html.haml
@@ -1,99 +1,100 @@
- pretty_name = defined?(@project) ? @project.full_name : 'namespace / path'
- run_actions_text = "Perform common operations on GitLab project: #{pretty_name}"
-.well
- %p
- This service allows users to perform common operations on this
- project by entering slash commands in Slack.
- = link_to help_page_path('user/project/integrations/slack_slash_commands.md'), target: '_blank' do
- View documentation
- = icon('external-link')
- %p.inline
- See list of available commands in Slack after setting up this service,
- by entering
- %kbd.inline /&lt;command&gt; help
- - unless @service.template?
- %p To setup this service:
- %ul.list-unstyled.indent-list
- %li
- 1.
- = link_to 'https://my.slack.com/services/new/slash-commands', target: '_blank', rel: 'noreferrer noopener nofollow' do
- Add a slash command
- = icon('external-link')
- in your Slack team with these options:
+.info-well
+ .well-segment
+ %p
+ This service allows users to perform common operations on this
+ project by entering slash commands in Slack.
+ = link_to help_page_path('user/project/integrations/slack_slash_commands.md'), target: '_blank' do
+ View documentation
+ = sprite_icon('external-link', size: 16)
+ %p.inline
+ See list of available commands in Slack after setting up this service,
+ by entering
+ %kbd.inline /&lt;command&gt; help
+ - unless @service.template?
+ %p To setup this service:
+ %ul.list-unstyled.indent-list
+ %li
+ 1.
+ = link_to 'https://my.slack.com/services/new/slash-commands', target: '_blank', rel: 'noreferrer noopener nofollow' do
+ Add a slash command
+ = sprite_icon('external-link', size: 16)
+ in your Slack team with these options:
- %hr
+ %hr
- .help-form
- .form-group
- = label_tag nil, 'Command', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.text-block
- %p Fill in the word that works best for your team.
- %p
- Suggestions:
- %code= 'gitlab'
- %code= @project.path # Path contains no spaces, but dashes
- %code= @project.full_path
+ .help-form
+ .form-group
+ = label_tag nil, 'Command', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.text-block
+ %p Fill in the word that works best for your team.
+ %p
+ Suggestions:
+ %code= 'gitlab'
+ %code= @project.path # Path contains no spaces, but dashes
+ %code= @project.full_path
- .form-group
- = label_tag :url, 'URL', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :url, service_trigger_url(subject), class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(target: '#url')
+ .form-group
+ = label_tag :url, 'URL', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.input-group
+ = text_field_tag :url, service_trigger_url(subject), class: 'form-control form-control-sm', readonly: 'readonly'
+ .input-group-append
+ = clipboard_button(target: '#url', class: 'input-group-text')
- .form-group
- = label_tag nil, 'Method', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.text-block POST
+ .form-group
+ = label_tag nil, 'Method', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.text-block POST
- .form-group
- = label_tag :customize_name, 'Customize name', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :customize_name, 'GitLab', class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(target: '#customize_name')
+ .form-group
+ = label_tag :customize_name, 'Customize name', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.input-group
+ = text_field_tag :customize_name, 'GitLab', class: 'form-control form-control-sm', readonly: 'readonly'
+ .input-group-append
+ = clipboard_button(target: '#customize_name', class: 'input-group-text')
- .form-group
- = label_tag nil, 'Customize icon', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.text-block
- = image_tag(asset_url('slash-command-logo.png'), width: 36, height: 36)
- = link_to('Download image', asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank', rel: 'noopener noreferrer')
+ .form-group
+ = label_tag nil, 'Customize icon', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.text-block
+ = image_tag(asset_url('slash-command-logo.png'), width: 36, height: 36)
+ = link_to('Download image', asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank', rel: 'noopener noreferrer')
- .form-group
- = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.text-block Show this command in the autocomplete list
+ .form-group
+ = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.text-block Show this command in the autocomplete list
- .form-group
- = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(target: '#autocomplete_description')
+ .form-group
+ = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.input-group
+ = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control form-control-sm', readonly: 'readonly'
+ .input-group-append
+ = clipboard_button(target: '#autocomplete_description', class: 'input-group-text')
- .form-group
- = label_tag :autocomplete_usage_hint, 'Autocomplete usage hint', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :autocomplete_usage_hint, '[help]', class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(target: '#autocomplete_usage_hint')
+ .form-group
+ = label_tag :autocomplete_usage_hint, 'Autocomplete usage hint', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.input-group
+ = text_field_tag :autocomplete_usage_hint, '[help]', class: 'form-control form-control-sm', readonly: 'readonly'
+ .input-group-append
+ = clipboard_button(target: '#autocomplete_usage_hint', class: 'input-group-text')
- .form-group
- = label_tag :descriptive_label, 'Descriptive label', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :descriptive_label, 'Perform common operations on GitLab project', class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(target: '#descriptive_label')
+ .form-group
+ = label_tag :descriptive_label, 'Descriptive label', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.input-group
+ = text_field_tag :descriptive_label, 'Perform common operations on GitLab project', class: 'form-control form-control-sm', readonly: 'readonly'
+ .input-group-append
+ = clipboard_button(target: '#descriptive_label', class: 'input-group-text')
- %hr
+ %hr
- %ul.list-unstyled.indent-list
- %li
- 2. Paste the
- %strong Token
- into the field below
- %li
- 3. Select the
- %strong Active
- checkbox, press
- %strong Save changes
- and start using GitLab inside Slack!
+ %ul.list-unstyled.indent-list
+ %li
+ 2. Paste the
+ %strong Token
+ into the field below
+ %li
+ 3. Select the
+ %strong Active
+ checkbox, press
+ %strong Save changes
+ and start using GitLab inside Slack!
diff --git a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
index 8cb6c446e18..31c2616d283 100644
--- a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
@@ -1,43 +1,66 @@
-.row.prepend-top-default
+.row
.col-lg-12
= form_for @project, url: project_settings_ci_cd_path(@project) do |f|
= form_errors(@project)
- %fieldset.builds-feature
+ %fieldset.builds-feature.js-auto-devops-settings
.form-group
- message = auto_devops_warning_message(@project)
- ci_file_formatted = '<code>.gitlab-ci.yml</code>'.html_safe
- if message
- %p.settings-message.text-center
+ %p.auto-devops-warning-message.settings-message.text-center
= message.html_safe
= f.fields_for :auto_devops_attributes, @auto_devops do |form|
- .radio
- = form.label :enabled_true do
- = form.radio_button :enabled, 'true'
- %strong= s_('CICD|Enable Auto DevOps')
- %br
- = s_('CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project.').html_safe % { ci_file: ci_file_formatted }
+ .card.auto-devops-card
+ .card-body
+ .form-check
+ = form.radio_button :enabled, 'true', class: 'form-check-input js-toggle-extra-settings'
+ = form.label :enabled_true, class: 'form-check-label' do
+ %strong= s_('CICD|Enable Auto DevOps')
+ .form-text.text-muted
+ = s_('CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project.').html_safe % { ci_file: ci_file_formatted }
- .radio
- = form.label :enabled_false do
- = form.radio_button :enabled, 'false'
- %strong= s_('CICD|Disable Auto DevOps')
- %br
- = s_('CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery.').html_safe % { ci_file: ci_file_formatted }
+ .card.auto-devops-card
+ .card-body
+ .form-check
+ = form.radio_button :enabled, '', class: 'form-check-input js-toggle-extra-settings'
+ = form.label :enabled_, class: 'form-check-label' do
+ %strong= s_('CICD|Instance default (%{state})') % { state: "#{Gitlab::CurrentSettings.auto_devops_enabled? ? _('enabled') : _('disabled')}" }
+ .form-text.text-muted
+ = s_('CICD|Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific %{ci_file}.').html_safe % { ci_file: ci_file_formatted }
- .radio
- = form.label :enabled_ do
- = form.radio_button :enabled, ''
- %strong= s_('CICD|Instance default (%{state})') % { state: "#{Gitlab::CurrentSettings.auto_devops_enabled? ? _('enabled') : _('disabled')}" }
- %br
- = s_('CICD|Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific %{ci_file}.').html_safe % { ci_file: ci_file_formatted }
+ .card.auto-devops-card.js-extra-settings{ class: form.object&.enabled == false ? 'hidden' : nil }
+ .card-body.bg-light
+ = form.label :domain do
+ %strong= _('Domain')
+ = form.text_field :domain, class: 'form-control', placeholder: 'domain.com'
+ .form-text.text-muted
+ = s_('CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages.')
+ - if cluster_ingress_ip = cluster_ingress_ip(@project)
+ = s_('%{nip_domain} can be used as an alternative to a custom domain.').html_safe % { nip_domain: "<code>#{cluster_ingress_ip}.nip.io</code>".html_safe }
+ = link_to icon('question-circle'), help_page_path('topics/autodevops/index.md', anchor: 'auto-devops-base-domain'), target: '_blank'
- = form.label :domain, class:"prepend-top-10" do
- = _('Domain')
- = form.text_field :domain, class: 'form-control', placeholder: 'domain.com'
- .help-block
- = s_('CICD|A domain is required to use Auto Review Apps and Auto Deploy Stages.')
- - if cluster_ingress_ip = cluster_ingress_ip(@project)
- = s_('%{nip_domain} can be used as an alternative to a custom domain.').html_safe % { nip_domain: "<code>#{cluster_ingress_ip}.nip.io</code>".html_safe }
- = link_to icon('question-circle'), help_page_path('topics/autodevops/index.md', anchor: 'auto-devops-base-domain'), target: '_blank'
+ %label.prepend-top-10
+ %strong= s_('CICD|Deployment strategy')
+ %p.settings-message.text-center
+ = s_('CICD|Deployment strategy needs a domain name to work correctly.')
+ .form-check
+ = form.radio_button :deploy_strategy, 'continuous', class: 'form-check-input'
+ = form.label :deploy_strategy_continuous, class: 'form-check-label' do
+ %strong= s_('CICD|Continuous deployment to production')
+ = link_to icon('question-circle'), help_page_path('topics/autodevops/index.md', anchor: 'auto-deploy'), target: '_blank'
+ .form-check
+ = form.radio_button :deploy_strategy, 'manual', class: 'form-check-input'
+ = form.label :deploy_strategy_manual, class: 'form-check-label' do
+ %strong= s_('CICD|Automatic deployment to staging, manual deployment to production')
+ = link_to icon('question-circle'), help_page_path('ci/environments.md', anchor: 'manually-deploying-to-environments'), target: '_blank'
- = f.submit 'Save changes', class: "btn btn-success prepend-top-15"
+ .card.auto-devops-card
+ .card-body
+ .form-check
+ = form.radio_button :enabled, 'false', class: 'form-check-input js-toggle-extra-settings', data: { hide_extra_settings: true }
+ = form.label :enabled_false, class: 'form-check-label' do
+ %strong= s_('CICD|Disable Auto DevOps')
+ .form-text.text-muted
+ = s_('CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery.').html_safe % { ci_file: ci_file_formatted }
+
+ = f.submit _('Save changes'), class: "btn btn-success prepend-top-15"
diff --git a/app/views/projects/settings/ci_cd/_badge.html.haml b/app/views/projects/settings/ci_cd/_badge.html.haml
index e8028059487..d14360913a4 100644
--- a/app/views/projects/settings/ci_cd/_badge.html.haml
+++ b/app/views/projects/settings/ci_cd/_badge.html.haml
@@ -2,15 +2,15 @@
.col-lg-12
%h4
= badge.title.capitalize
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
%b
= badge.title.capitalize
&middot;
= badge.to_html
- .pull-right
+ .float-right
= render 'shared/ref_switcher', destination: 'badges', align_right: true
- .panel-body
+ .card-body
.row
.col-md-2.text-center
Markdown
diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml
index 80c226ad273..fb113aa7639 100644
--- a/app/views/projects/settings/ci_cd/_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_form.html.haml
@@ -4,97 +4,97 @@
= form_errors(@project)
%fieldset.builds-feature
.form-group.append-bottom-default.js-secret-runner-token
- = f.label :runners_token, "Runner token", class: 'label-light'
+ = f.label :runners_token, _("Runner token"), class: 'label-light'
.form-control.js-secret-value-placeholder
= '*' * 20
= f.text_field :runners_token, class: "form-control hide js-secret-value", placeholder: 'xEeFCaDAB89'
- %p.help-block The secure token used by the Runner to checkout the project
+ %p.form-text.text-muted= _("The secure token used by the Runner to checkout the project")
%button.btn.btn-info.prepend-top-10.js-secret-value-reveal-button{ type: 'button', data: { secret_reveal_status: 'false' } }
= _('Reveal value')
%hr
.form-group
%h5.prepend-top-0
- Git strategy for pipelines
+ = _("Git strategy for pipelines")
%p
- Choose between <code>clone</code> or <code>fetch</code> to get the recent application code
+ = _("Choose between <code>clone</code> or <code>fetch</code> to get the recent application code")
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'git-strategy'), target: '_blank'
- .radio
- = f.label :build_allow_git_fetch_false do
- = f.radio_button :build_allow_git_fetch, 'false'
+ .form-check
+ = f.radio_button :build_allow_git_fetch, 'false', { class: 'form-check-input' }
+ = f.label :build_allow_git_fetch_false, class: 'form-check-label' do
%strong git clone
%br
%span.descr
- Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job
- .radio
- = f.label :build_allow_git_fetch_true do
- = f.radio_button :build_allow_git_fetch, 'true'
+ = _("Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job")
+ .form-check
+ = f.radio_button :build_allow_git_fetch, 'true', { class: 'form-check-input' }
+ = f.label :build_allow_git_fetch_true, class: 'form-check-label' do
%strong git fetch
%br
%span.descr
- Faster as it re-uses the project workspace (falling back to clone if it doesn't exist)
+ = _("Faster as it re-uses the project workspace (falling back to clone if it doesn't exist)")
%hr
.form-group
- = f.label :build_timeout_human_readable, 'Timeout', class: 'label-light'
+ = f.label :build_timeout_human_readable, _('Timeout'), class: 'label-light'
= f.text_field :build_timeout_human_readable, class: 'form-control'
- %p.help-block
- Per job. If a job passes this threshold, it will be marked as failed
+ %p.form-text.text-muted
+ = _("Per job. If a job passes this threshold, it will be marked as failed")
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'timeout'), target: '_blank'
%hr
.form-group
- = f.label :ci_config_path, 'Custom CI config path', class: 'label-light'
+ = f.label :ci_config_path, _('Custom CI config path'), class: 'label-light'
= f.text_field :ci_config_path, class: 'form-control', placeholder: '.gitlab-ci.yml'
- %p.help-block
- The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>
+ %p.form-text.text-muted
+ = _("The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>")
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'custom-ci-config-path'), target: '_blank'
%hr
.form-group
- .checkbox
- = f.label :public_builds do
- = f.check_box :public_builds
- %strong Public pipelines
- .help-block
- Allow public access to pipelines and job details, including output logs and artifacts
+ .form-check
+ = f.check_box :public_builds, { class: 'form-check-input' }
+ = f.label :public_builds, class: 'form-check-label' do
+ %strong= _("Public pipelines")
+ .form-text.text-muted
+ = _("Allow public access to pipelines and job details, including output logs and artifacts")
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'visibility-of-pipelines'), target: '_blank'
.bs-callout.bs-callout-info
- %p If enabled:
+ %p #{_("If enabled")}:
%ul
%li
- For public projects, anyone can view pipelines and access job details (output logs and artifacts)
+ = _("For public projects, anyone can view pipelines and access job details (output logs and artifacts)")
%li
- For internal projects, any logged in user can view pipelines and access job details (output logs and artifacts)
+ = _("For internal projects, any logged in user can view pipelines and access job details (output logs and artifacts)")
%li
- For private projects, any member (guest or higher) can view pipelines and access job details (output logs and artifacts)
+ = _("For private projects, any member (guest or higher) can view pipelines and access job details (output logs and artifacts)")
%p
- If disabled, the access level will depend on the user's
- permissions in the project.
+ = _("If disabled, the access level will depend on the user's permissions in the project.")
%hr
.form-group
- .checkbox
- = f.label :auto_cancel_pending_pipelines do
- = f.check_box :auto_cancel_pending_pipelines, {}, 'enabled', 'disabled'
- %strong Auto-cancel redundant, pending pipelines
- .help-block
- New pipelines will cancel older, pending pipelines on the same branch
+ .form-check
+ = f.check_box :auto_cancel_pending_pipelines, { class: 'form-check-input' }, 'enabled', 'disabled'
+ = f.label :auto_cancel_pending_pipelines, class: 'form-check-label' do
+ %strong= _("Auto-cancel redundant, pending pipelines")
+ .form-text.text-muted
+ = _("New pipelines will cancel older, pending pipelines on the same branch")
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'auto-cancel-pending-pipelines'), target: '_blank'
%hr
.form-group
- = f.label :build_coverage_regex, "Test coverage parsing", class: 'label-light'
+ = f.label :build_coverage_regex, _("Test coverage parsing"), class: 'label-light'
.input-group
- %span.input-group-addon /
+ %span.input-group-prepend
+ .input-group-text /
= f.text_field :build_coverage_regex, class: 'form-control', placeholder: 'Regular expression'
- %span.input-group-addon /
- %p.help-block
- A regular expression that will be used to find the test coverage
- output in the job trace. Leave blank to disable
+ %span.input-group-append
+ .input-group-text /
+ %p.form-text.text-muted
+ = _("A regular expression that will be used to find the test coverage output in the job trace. Leave blank to disable")
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'test-coverage-parsing'), target: '_blank'
.bs-callout.bs-callout-info
- %p Below are examples of regex for existing tools:
+ %p= _("Below are examples of regex for existing tools:")
%ul
%li
Simplecov (Ruby) -
@@ -117,8 +117,11 @@
%li
JaCoCo (Java/Kotlin)
%code Total.*?([0-9]{1,3})%
+ %li
+ go test -cover (Go)
+ %code coverage: \d+.\d+% of statements
- = f.submit 'Save changes', class: "btn btn-save"
+ = f.submit _('Save changes'), class: "btn btn-save"
%hr
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index 7d8dd58e7e0..be22bbd7a9b 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -1,6 +1,6 @@
- @content_class = "limit-container-width" unless fluid_layout
-- page_title "CI / CD Settings"
-- page_title "CI / CD"
+- page_title _("CI / CD Settings")
+- page_title _("CI / CD")
- expanded = Rails.env.test?
- general_expanded = @project.errors.empty? ? expanded : true
@@ -8,15 +8,15 @@
%section.settings#js-general-pipeline-settings.no-animate{ class: ('expanded' if general_expanded) }
.settings-header
%h4
- General pipelines settings
+ = _("General pipelines")
%button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ = expanded ? _('Collapse') : _('Expand')
%p
- Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report.
+ = _("Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report.")
.settings-content
= render 'form'
-%section.settings#autodevops-settings.no-animate{ class: ('expanded' if expanded) }
+%section.qa-autodevops-settings.settings#autodevops-settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
= s_('CICD|Auto DevOps')
@@ -28,38 +28,36 @@
.settings-content
= render 'autodevops_form'
-%section.settings.no-animate{ class: ('expanded' if expanded) }
+%section.qa-runners-settings.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
- Runners settings
+ = _("Runners")
%button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ = expanded ? _('Collapse') : _('Expand')
%p
- Register and see your runners for this project.
+ = _("Register and see your runners for this project.")
.settings-content
= render 'projects/runners/index'
-%section.settings.no-animate{ class: ('expanded' if expanded) }
+%section.qa-variables-settings.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
- = _('Secret variables')
- = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank', rel: 'noopener noreferrer'
+ = _('Variables')
+ = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'variables'), target: '_blank', rel: 'noopener noreferrer'
%button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ = expanded ? _('Collapse') : _('Expand')
%p.append-bottom-0
= render "ci/variables/content"
.settings-content
= render 'ci/variables/index', save_endpoint: project_variables_path(@project)
-%section.settings.no-animate{ class: ('expanded' if expanded) }
+%section.settings.no-animate#js-pipeline-triggers{ class: ('expanded' if expanded) }
.settings-header
%h4
- Pipeline triggers
+ = _("Pipeline triggers")
%button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ = expanded ? _('Collapse') : _('Expand')
%p
- Triggers can force a specific branch or tag to get rebuilt with an API call. These tokens will
- impersonate their associated user including their access to projects and their project
- permissions.
+ = _("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
= render 'projects/triggers/index'
diff --git a/app/views/projects/settings/integrations/_project_hook.html.haml b/app/views/projects/settings/integrations/_project_hook.html.haml
index cd003107d66..ef445f2e139 100644
--- a/app/views/projects/settings/integrations/_project_hook.html.haml
+++ b/app/views/projects/settings/integrations/_project_hook.html.haml
@@ -5,12 +5,12 @@
%div
- ProjectHook.triggers.each_value do |event|
- if hook.public_send(event)
- %span.label.label-gray.deploy-project-label= event.to_s.titleize
+ %span.badge.badge-gray.deploy-project-label= event.to_s.titleize
.col-md-4.col-lg-5.text-right-lg.prepend-top-5
%span.append-right-10.inline
- SSL Verification: #{hook.enable_ssl_verification ? 'enabled' : 'disabled'}
- = link_to 'Edit', edit_project_hook_path(@project, hook), class: 'btn btn-sm'
+ #{_("SSL Verification")}: #{hook.enable_ssl_verification ? _('enabled') : _('disabled')}
+ = link_to _('Edit'), edit_project_hook_path(@project, hook), class: 'btn btn-sm'
= render 'shared/web_hooks/test_button', triggers: ProjectHook.triggers, hook: hook, button_class: 'btn-sm'
- = link_to project_hook_path(@project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-transparent' do
- %span.sr-only Remove
+ = link_to project_hook_path(@project, hook), data: { confirm: _('Are you sure?') }, method: :delete, class: 'btn btn-transparent' do
+ %span.sr-only= _("Remove")
= icon('trash')
diff --git a/app/views/projects/settings/integrations/show.html.haml b/app/views/projects/settings/integrations/show.html.haml
index 2f1a548e119..76770290f36 100644
--- a/app/views/projects/settings/integrations/show.html.haml
+++ b/app/views/projects/settings/integrations/show.html.haml
@@ -1,5 +1,5 @@
- @content_class = "limit-container-width" unless fluid_layout
-- breadcrumb_title "Integrations Settings"
-- page_title 'Integrations'
+- breadcrumb_title _("Integrations Settings")
+- page_title _('Integrations')
= render 'projects/hooks/index'
= render 'projects/services/index'
diff --git a/app/views/projects/settings/members/show.html.haml b/app/views/projects/settings/members/show.html.haml
index ea2cd36b212..5fca734222b 100644
--- a/app/views/projects/settings/members/show.html.haml
+++ b/app/views/projects/settings/members/show.html.haml
@@ -1,5 +1,5 @@
- @content_class = "limit-container-width" unless fluid_layout
-- page_title "Members"
+- page_title _("Members")
= render "projects/project_members/index"
diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml
index 5dda2ec28b4..98c609d7bd4 100644
--- a/app/views/projects/settings/repository/show.html.haml
+++ b/app/views/projects/settings/repository/show.html.haml
@@ -1,5 +1,5 @@
-- breadcrumb_title "Repository Settings"
-- page_title "Repository"
+- breadcrumb_title _("Repository Settings")
+- page_title _("Repository")
- @content_class = "limit-container-width" unless fluid_layout
= render "projects/mirrors/show"
diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml
index f09871c7fcc..4a3aa3dc626 100644
--- a/app/views/projects/snippets/_actions.html.haml
+++ b/app/views/projects/snippets/_actions.html.haml
@@ -1,36 +1,36 @@
- return unless current_user
-.hidden-xs
+.d-none.d-sm-block
- if can?(current_user, :update_project_snippet, @snippet)
= link_to edit_project_snippet_path(@project, @snippet), class: "btn btn-grouped" do
- Edit
+ = _('Edit')
- if can?(current_user, :update_project_snippet, @snippet)
- = link_to project_snippet_path(@project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-inverted btn-remove", title: 'Delete Snippet' do
- Delete
+ = link_to project_snippet_path(@project, @snippet), method: :delete, data: { confirm: _("Are you sure?") }, class: "btn btn-grouped btn-inverted btn-remove", title: _('Delete Snippet') do
+ = _('Delete')
- if can?(current_user, :create_project_snippet, @project)
- = link_to new_project_snippet_path(@project), class: 'btn btn-grouped btn-inverted btn-create', title: "New snippet" do
- New snippet
+ = link_to new_project_snippet_path(@project), class: 'btn btn-grouped btn-inverted btn-create', title: _("New snippet") do
+ = _('New snippet')
- if @snippet.submittable_as_spam_by?(current_user)
- = link_to 'Submit as spam', mark_as_spam_project_snippet_path(@project, @snippet), method: :post, class: 'btn btn-grouped btn-spam', title: 'Submit as spam'
+ = link_to _('Submit as spam'), mark_as_spam_project_snippet_path(@project, @snippet), method: :post, class: 'btn btn-grouped btn-spam', title: _('Submit as spam')
- if can?(current_user, :create_project_snippet, @project) || can?(current_user, :update_project_snippet, @snippet)
- .visible-xs-block.dropdown
+ .d-block.d-sm-none.dropdown
%button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
- Options
+ = _('Options')
= icon('caret-down')
.dropdown-menu.dropdown-menu-full-width
%ul
- if can?(current_user, :create_project_snippet, @project)
%li
- = link_to new_project_snippet_path(@project), title: "New snippet" do
- New snippet
+ = link_to new_project_snippet_path(@project), title: _("New snippet") do
+ = _('New snippet')
- if can?(current_user, :update_project_snippet, @snippet)
%li
- = link_to project_snippet_path(@project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, title: 'Delete Snippet' do
- Delete
+ = link_to project_snippet_path(@project, @snippet), method: :delete, data: { confirm: _("Are you sure?") }, title: _('Delete Snippet') do
+ = _('Delete')
- if can?(current_user, :update_project_snippet, @snippet)
%li
= link_to edit_project_snippet_path(@project, @snippet) do
- Edit
+ = _('Edit')
- if @snippet.submittable_as_spam_by?(current_user)
%li
- = link_to 'Submit as spam', mark_as_spam_project_snippet_path(@project, @snippet), method: :post
+ = link_to _('Submit as spam'), mark_as_spam_project_snippet_path(@project, @snippet), method: :post
diff --git a/app/views/projects/snippets/edit.html.haml b/app/views/projects/snippets/edit.html.haml
index 32844f5204a..6dbd67df886 100644
--- a/app/views/projects/snippets/edit.html.haml
+++ b/app/views/projects/snippets/edit.html.haml
@@ -1,8 +1,8 @@
-- add_to_breadcrumbs "Snippets", project_snippets_path(@project)
+- add_to_breadcrumbs _("Snippets"), project_snippets_path(@project)
- breadcrumb_title @snippet.to_reference
-- page_title "Edit", "#{@snippet.title} (#{@snippet.to_reference})", "Snippets"
+- page_title _("Edit"), "#{@snippet.title} (#{@snippet.to_reference})", _("Snippets")
%h3.page-title
- Edit Snippet
+ = _("Edit Snippet")
%hr
= render "shared/snippets/form", url: project_snippet_path(@project, @snippet)
diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml
index 65efc083fdd..1c4c73dc776 100644
--- a/app/views/projects/snippets/index.html.haml
+++ b/app/views/projects/snippets/index.html.haml
@@ -1,4 +1,4 @@
-- page_title "Snippets"
+- page_title _("Snippets")
- if current_user
.top-area
@@ -7,6 +7,6 @@
.nav-controls
- if can?(current_user, :create_project_snippet, @project)
- = link_to "New snippet", new_project_snippet_path(@project), class: "btn btn-new", title: "New snippet"
+ = link_to _("New snippet"), new_project_snippet_path(@project), class: "btn btn-new", title: _("New snippet")
= render 'snippets/snippets'
diff --git a/app/views/projects/snippets/new.html.haml b/app/views/projects/snippets/new.html.haml
index 1359a815429..26b333d4ecf 100644
--- a/app/views/projects/snippets/new.html.haml
+++ b/app/views/projects/snippets/new.html.haml
@@ -1,8 +1,8 @@
-- add_to_breadcrumbs "Snippets", project_snippets_path(@project)
-- breadcrumb_title "New"
-- page_title "New Snippets"
+- add_to_breadcrumbs _("Snippets"), project_snippets_path(@project)
+- breadcrumb_title _("New")
+- page_title _("New Snippets")
%h3.page-title
- New Snippet
+ = _('New Snippet')
%hr
= render "shared/snippets/form", url: project_snippets_path(@project, @snippet)
diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml
index 7062c5b765e..f495b4eaf30 100644
--- a/app/views/projects/snippets/show.html.haml
+++ b/app/views/projects/snippets/show.html.haml
@@ -1,7 +1,7 @@
- @content_class = "limit-container-width limited-inner-width-container" unless fluid_layout
-- add_to_breadcrumbs "Snippets", project_snippets_path(@project)
+- add_to_breadcrumbs _("Snippets"), project_snippets_path(@project)
- breadcrumb_title @snippet.to_reference
-- page_title "#{@snippet.title} (#{@snippet.to_reference})", "Snippets"
+- page_title "#{@snippet.title} (#{@snippet.to_reference})", _("Snippets")
= render 'shared/snippets/header'
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index 98b4d6339da..f55202c2c5f 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -6,7 +6,7 @@
= link_to tag.name, project_tag_path(@project, tag.name), class: 'item-title ref-name prepend-left-4'
- if protected_tag?(@project, tag)
- %span.label.label-success.prepend-left-4
+ %span.badge.badge-success.prepend-left-4
= s_('TagsPage|protected')
- if tag.message.present?
diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml
index 10415d011d6..96ecac815c0 100644
--- a/app/views/projects/tags/index.html.haml
+++ b/app/views/projects/tags/index.html.haml
@@ -16,7 +16,7 @@
%span.light
= tags_sort_options_hash[@sort]
= icon('chevron-down')
- %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
+ %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable
%li.dropdown-header
= s_('TagsPage|Sort by')
- tags_sort_options_hash.each do |value, title|
diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml
index 1827a3d323c..da822ac5675 100644
--- a/app/views/projects/tags/new.html.haml
+++ b/app/views/projects/tags/new.html.haml
@@ -10,35 +10,35 @@
= 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
- .form-group
- = label_tag :tag_name, nil, class: 'control-label'
+= form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "common-note-form tag-form js-quick-submit js-requires-input" do
+ .form-group.row
+ = label_tag :tag_name, nil, class: 'col-form-label col-sm-2'
.col-sm-10
= text_field_tag :tag_name, params[:tag_name], required: true, autofocus: true, class: 'form-control'
- .form-group
- = label_tag :ref, 'Create from', class: 'control-label'
+ .form-group.row
+ = label_tag :ref, 'Create from', class: 'col-form-label col-sm-2'
.col-sm-10.create-from
.dropdown
= hidden_field_tag :ref, default_ref
= button_tag type: 'button', title: default_ref, class: 'dropdown-menu-toggle wide 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
+ .form-text.text-muted
= s_('TagsPage|Existing branch name, tag, or commit SHA')
- .form-group
- = label_tag :message, nil, class: 'control-label'
+ .form-group.row
+ = label_tag :message, nil, class: 'col-form-label col-sm-2'
.col-sm-10
= text_area_tag :message, @message, required: false, class: 'form-control', rows: 5
- .help-block
+ .form-text.text-muted
= s_('TagsPage|Optionally, add a message to the tag.')
%hr
- .form-group
- = label_tag :release_description, s_('TagsPage|Release notes'), class: 'control-label'
+ .form-group.row
+ = label_tag :release_description, s_('TagsPage|Release notes'), class: 'col-form-label col-sm-2'
.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: s_('TagsPage|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
+ .form-text.text-muted
= 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 s_('TagsPage|Create tag'), class: 'btn btn-create'
diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml
index 7a3469cdd26..15a960f81b8 100644
--- a/app/views/projects/tags/show.html.haml
+++ b/app/views/projects/tags/show.html.haml
@@ -11,7 +11,7 @@
= icon('tag')
= @tag.name
- if protected_tag?(@project, @tag)
- %span.label.label-success
+ %span.badge.badge-success
= s_('TagsPage|protected')
- if @commit
= render 'projects/branches/commit', commit: @commit, project: @project
diff --git a/app/views/projects/tree/_blob_item.html.haml b/app/views/projects/tree/_blob_item.html.haml
index 8c1c532cb3e..f79f3af36d4 100644
--- a/app/views/projects/tree/_blob_item.html.haml
+++ b/app/views/projects/tree/_blob_item.html.haml
@@ -6,7 +6,7 @@
= link_to project_blob_path(@project, tree_join(@id || @commit.id, blob_item.name)), class: 'str-truncated', title: file_name do
%span= file_name
- if is_lfs_blob
- %span.label.label-lfs.prepend-left-5 LFS
- %td.hidden-xs.tree-commit
+ %span.badge.label-lfs.prepend-left-5 LFS
+ %td.d-none.d-sm-table-cell.tree-commit
%td.tree-time-ago.cgray.text-right
= render 'projects/tree/spinner'
diff --git a/app/views/projects/tree/_submodule_item.html.haml b/app/views/projects/tree/_submodule_item.html.haml
index 04d52361db0..e563c8c4036 100644
--- a/app/views/projects/tree/_submodule_item.html.haml
+++ b/app/views/projects/tree/_submodule_item.html.haml
@@ -3,4 +3,4 @@
%i.fa.fa-archive.fa-fw
= submodule_link(submodule_item, @ref)
%td
- %td.hidden-xs
+ %td.d-none.d-sm-table-cell
diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml
index 6ea78851b8d..587aeafa82f 100644
--- a/app/views/projects/tree/_tree_content.html.haml
+++ b/app/views/projects/tree/_tree_content.html.haml
@@ -4,15 +4,15 @@
%thead
%tr
%th= s_('ProjectFileTree|Name')
- %th.hidden-xs
- .pull-left= _('Last commit')
+ %th.d-none.d-sm-table-cell
+ .float-left= _('Last commit')
%th.text-right= _('Last update')
- if @path.present?
%tr.tree-item
%td.tree-item-file-name
= link_to "..", project_tree_path(@project, up_dir_path), class: 'prepend-left-10'
%td
- %td.hidden-xs
+ %td.d-none.d-sm-table-cell
= render_tree(tree)
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index fc8ebfa1fb1..9d196075bf1 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -6,23 +6,23 @@
= render 'shared/ref_switcher', destination: 'tree', path: @path, show_create: true
- if on_top_of_branch?
- - addtotree_toggle_attributes = { href: '#', 'data-toggle': 'dropdown', 'data-target': '.add-to-tree-dropdown' }
+ - addtotree_toggle_attributes = { href: '#', 'data-toggle': 'dropdown', 'data-target': '.add-to-tree-dropdown', 'data-boundary': 'window' }
- else
- addtotree_toggle_attributes = { title: _("You can only add files when you are on a branch"), data: { container: 'body' }, class: 'disabled has-tooltip' }
%ul.breadcrumb.repo-breadcrumb
- %li
+ %li.breadcrumb-item
= link_to project_tree_path(@project, @ref) do
= @project.path
- path_breadcrumbs do |title, path|
- %li
+ %li.breadcrumb-item
= link_to truncate(title, length: 40), project_tree_path(@project, tree_join(@ref, path))
- if can_collaborate || can_create_mr_from_fork
- %li
+ %li.breadcrumb-item
%a.btn.add-to-tree{ addtotree_toggle_attributes }
- = sprite_icon('plus', size: 16, css_class: 'pull-left')
- = sprite_icon('arrow-down', size: 16, css_class: 'pull-left')
+ = sprite_icon('plus', size: 16, css_class: 'float-left')
+ = sprite_icon('arrow-down', size: 16, css_class: 'float-left')
- if on_top_of_branch?
.add-to-tree-dropdown
%ul.dropdown-menu
diff --git a/app/views/projects/tree/_tree_item.html.haml b/app/views/projects/tree/_tree_item.html.haml
index af3816fc9f4..ce0cd95b468 100644
--- a/app/views/projects/tree/_tree_item.html.haml
+++ b/app/views/projects/tree/_tree_item.html.haml
@@ -4,6 +4,6 @@
- path = flatten_tree(@path, tree_item)
= link_to project_tree_path(@project, tree_join(@id || @commit.id, path)), class: 'str-truncated', title: path do
%span= path
- %td.hidden-xs.tree-commit
+ %td.d-none.d-sm-table-cell.tree-commit
%td.tree-time-ago.text-right
= render 'projects/tree/spinner'
diff --git a/app/views/projects/triggers/_content.html.haml b/app/views/projects/triggers/_content.html.haml
index 6c2d603d95d..96a41aa066c 100644
--- a/app/views/projects/triggers/_content.html.haml
+++ b/app/views/projects/triggers/_content.html.haml
@@ -1,6 +1,6 @@
%p.append-bottom-default
Triggers with the
- %span.label.label-primary legacy
+ %span.badge.badge-primary legacy
label do not have an associated user and only have access to the current project.
%br
= succeed '.' do
diff --git a/app/views/projects/triggers/_form.html.haml b/app/views/projects/triggers/_form.html.haml
index 5f708b3a2ed..3539aea3580 100644
--- a/app/views/projects/triggers/_form.html.haml
+++ b/app/views/projects/triggers/_form.html.haml
@@ -4,7 +4,7 @@
- if @trigger.token
.form-group
%label.label-light Token
- %p.form-control-static= @trigger.token
+ %p.form-control-plaintext= @trigger.token
.form-group
= f.label :key, "Description", class: "label-light"
= f.text_field :description, class: "form-control", required: true, title: 'Trigger description is required.', placeholder: "Trigger description"
diff --git a/app/views/projects/triggers/_index.html.haml b/app/views/projects/triggers/_index.html.haml
index 0f655e4ed83..a15bb4c4f3f 100644
--- a/app/views/projects/triggers/_index.html.haml
+++ b/app/views/projects/triggers/_index.html.haml
@@ -1,11 +1,11 @@
.row.prepend-top-default.append-bottom-default.triggers-container
.col-lg-12
= render "projects/triggers/content"
- .panel.panel-default
- .panel-heading
- %h4.panel-title
+ .card
+ .card-header
+ %h4.card-title
Manage your project's triggers
- .panel-body
+ .card-body
= render "projects/triggers/form", btn_text: "Add trigger"
%hr
- if @triggers.any?
@@ -26,7 +26,7 @@
%p.settings-message.text-center.append-bottom-default
No triggers have been created yet. Add one using the form above.
- .panel-footer
+ .card-footer
%p
In the following examples, you can see the exact API call you need to
diff --git a/app/views/projects/triggers/_trigger.html.haml b/app/views/projects/triggers/_trigger.html.haml
index 9201680f119..7e4618e1a88 100644
--- a/app/views/projects/triggers/_trigger.html.haml
+++ b/app/views/projects/triggers/_trigger.html.haml
@@ -8,9 +8,9 @@
.label-container
- if trigger.legacy?
- %span.label.label-primary.has-tooltip{ title: "Trigger makes use of deprecated functionality" } legacy
+ %span.badge.badge-primary.has-tooltip{ title: "Trigger makes use of deprecated functionality" } legacy
- if !trigger.can_access_project?
- %span.label.label-danger.has-tooltip{ title: "Trigger user has insufficient permissions to project" } invalid
+ %span.badge.badge-danger.has-tooltip{ title: "Trigger user has insufficient permissions to project" } invalid
%td
- if trigger.description? && trigger.description.length > 15
diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml
index d285251d06f..de692466fe5 100644
--- a/app/views/projects/wikis/_form.html.haml
+++ b/app/views/projects/wikis/_form.html.haml
@@ -1,13 +1,15 @@
- commit_message = @page.persisted? ? s_("WikiPageEdit|Update %{page_title}") : s_("WikiPageCreate|Create %{page_title}")
- commit_message = commit_message % { page_title: @page.title }
-= form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form common-note-form prepend-top-default js-quick-submit' } do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post,
+ html: { class: 'wiki-form common-note-form prepend-top-default js-quick-submit' },
+ data: { markdown_version: CacheMarkdownField::CACHE_REDCARPET_VERSION } do |f|
= form_errors(@page)
- if @page.persisted?
= f.hidden_field :last_commit_sha, value: @page.last_commit_sha
- .form-group
+ .form-group.row
.col-sm-12= f.label :title, class: 'control-label-full-width'
.col-sm-12
= f.text_field :title, class: 'form-control', value: @page.title
@@ -16,22 +18,22 @@
= icon('lightbulb-o')
= s_("WikiEditPageTip|Tip: You can move this page by adding the path to the beginning of the title.")
= link_to icon('question-circle'), help_page_path('user/project/wiki/index', anchor: 'moving-a-wiki-page'), target: '_blank'
- .form-group
+ .form-group.row
.col-sm-12= f.label :format, class: 'control-label-full-width'
.col-sm-12
= f.select :format, options_for_select(ProjectWiki::MARKUPS, {selected: @page.format}), {}, class: 'form-control'
- .form-group
+ .form-group.row
.col-sm-12= f.label :content, class: 'control-label-full-width'
.col-sm-12
= render layout: 'projects/md_preview', locals: { url: project_wiki_preview_markdown_path(@project, @page.slug) } do
- = render 'projects/zen', f: f, attr: :content, classes: 'note-textarea', placeholder: s_("WikiPage|Write your content or drag files here...")
+ = render 'projects/zen', f: f, attr: :content, classes: 'note-textarea', placeholder: s_("WikiPage|Write your content or drag files here…")
= render 'shared/notes/hints'
.clearfix
.error-alert
- .help-block
+ .form-text.text-muted
= succeed '.' do
= (s_("WikiMarkdownTip|To link to a (new) page, simply type %{link_example}") % { link_example: '<code>[Link Title](page-slug)</code>' }).html_safe
@@ -39,16 +41,16 @@
- markdown_link = link_to s_("WikiMarkdownDocs|documentation"), help_page_path('user/markdown', anchor: 'wiki-specific-markdown')
= (s_("WikiMarkdownDocs|More examples are in the %{docs_link}") % { docs_link: markdown_link }).html_safe
- .form-group
+ .form-group.row
.col-sm-12= f.label :commit_message, class: 'control-label-full-width'
.col-sm-12= f.text_field :message, class: 'form-control', rows: 18, value: commit_message
.form-actions
- if @page && @page.persisted?
= f.submit _("Save changes"), class: 'btn-save btn'
- .pull-right
+ .float-right
= link_to _("Cancel"), project_wiki_path(@project, @page), class: 'btn btn-cancel btn-grouped'
- else
= f.submit s_("Wiki|Create page"), class: 'btn-create btn'
- .pull-right
+ .float-right
= link_to _("Cancel"), project_wiki_path(@project, :home), class: 'btn btn-cancel'
diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml
index 06a3cac12d5..38382aae67c 100644
--- a/app/views/projects/wikis/_new.html.haml
+++ b/app/views/projects/wikis/_new.html.haml
@@ -2,8 +2,9 @@
.modal-dialog
.modal-content
.modal-header
- %a.close{ href: "#", "data-dismiss" => "modal" } ×
%h3.page-title= s_("WikiNewPageTitle|New Wiki Page")
+ %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
+ %span{ "aria-hidden": true } &times;
.modal-body
%form.new-wiki-page
.form-group
diff --git a/app/views/projects/wikis/_pages_wiki_page.html.haml b/app/views/projects/wikis/_pages_wiki_page.html.haml
index efa16d38f84..cbb441d7509 100644
--- a/app/views/projects/wikis/_pages_wiki_page.html.haml
+++ b/app/views/projects/wikis/_pages_wiki_page.html.haml
@@ -1,5 +1,5 @@
%li
= link_to wiki_page.title, project_wiki_path(@project, wiki_page)
%small (#{wiki_page.format})
- .pull-right
+ .float-right
%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/_sidebar.html.haml b/app/views/projects/wikis/_sidebar.html.haml
index 2c7551c6f8c..a23396dc0d8 100644
--- a/app/views/projects/wikis/_sidebar.html.haml
+++ b/app/views/projects/wikis/_sidebar.html.haml
@@ -1,7 +1,7 @@
%aside.right-sidebar.right-sidebar-expanded.wiki-sidebar.js-wiki-sidebar.js-right-sidebar{ data: { "offset-top" => "50", "spy" => "affix" } }
.sidebar-container
.block.wiki-sidebar-header.append-bottom-default
- %a.gutter-toggle.pull-right.visible-xs-block.visible-sm-block.js-sidebar-wiki-toggle{ href: "#" }
+ %a.gutter-toggle.float-right.d-block.d-sm-block.d-md-none.js-sidebar-wiki-toggle{ href: "#" }
= icon('angle-double-right')
- git_access_url = project_wikis_git_access_path(@project)
diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml
index 35c7dc2984a..d80d2957466 100644
--- a/app/views/projects/wikis/edit.html.haml
+++ b/app/views/projects/wikis/edit.html.haml
@@ -1,4 +1,4 @@
-- @content_class = "limit-container-width limit-container-width-sm" unless fluid_layout
+- @content_class = "limit-container-width" unless fluid_layout
- page_title _("Edit"), @page.title.capitalize, _("Wiki")
= wiki_page_errors(@error)
diff --git a/app/views/projects/wikis/empty.html.haml b/app/views/projects/wikis/empty.html.haml
index d6e568bac94..62fa6e1907b 100644
--- a/app/views/projects/wikis/empty.html.haml
+++ b/app/views/projects/wikis/empty.html.haml
@@ -1,6 +1,4 @@
- page_title _("Wiki")
+- @right_sidebar = false
-%h3.page-title= s_("Wiki|Empty page")
-%hr
-.error_message
- = s_("WikiEmptyPageError|You are not allowed to create wiki pages")
+= render 'shared/empty_states/wikis'
diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml
index 10dbbc0e42c..8c2cbd495a0 100644
--- a/app/views/projects/wikis/git_access.html.haml
+++ b/app/views/projects/wikis/git_access.html.haml
@@ -1,8 +1,8 @@
-- @content_class = "limit-container-width limit-container-width-sm" unless fluid_layout
+- @content_class = "limit-container-width" unless fluid_layout
- page_title s_("WikiClone|Git Access"), _("Wiki")
.wiki-page-header.has-sidebar-toggle
- %button.btn.btn-default.visible-xs.visible-sm.pull-right.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" }
+ %button.btn.btn-default.d-block.d-sm-block.d-md-none.float-right.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" }
= icon('angle-double-left')
.git-access-header
diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml
index ff72c8bb75d..a08973c7f32 100644
--- a/app/views/projects/wikis/show.html.haml
+++ b/app/views/projects/wikis/show.html.haml
@@ -1,4 +1,4 @@
-- @content_class = "limit-container-width limit-container-width-sm" unless fluid_layout
+- @content_class = "limit-container-width" unless fluid_layout
- breadcrumb_title @page.title.capitalize
- wiki_breadcrumb_dropdown_links(@page.slug)
- page_title @page.title.capitalize, _("Wiki")
diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml
index 7d43fd61081..ff9a7b53a86 100644
--- a/app/views/search/_category.html.haml
+++ b/app/views/search/_category.html.haml
@@ -1,80 +1,80 @@
.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
.fade-left= icon('angle-left')
.fade-right= icon('angle-right')
- %ul.nav-links.search-filter.scrolling-tabs
+ %ul.nav-links.search-filter.scrolling-tabs.nav.nav-tabs
- if @project
- if project_search_tabs?(:blobs)
%li{ class: active_when(@scope == 'blobs') }
= link_to search_filter_path(scope: 'blobs') do
Code
- %span.badge
+ %span.badge.badge-pill
= @search_results.blobs_count
- if project_search_tabs?(:issues)
%li{ class: active_when(@scope == 'issues') }
= link_to search_filter_path(scope: 'issues') do
Issues
- %span.badge
+ %span.badge.badge-pill
= limited_count(@search_results.limited_issues_count)
- if project_search_tabs?(:merge_requests)
%li{ class: active_when(@scope == 'merge_requests') }
= link_to search_filter_path(scope: 'merge_requests') do
Merge requests
- %span.badge
+ %span.badge.badge-pill
= limited_count(@search_results.limited_merge_requests_count)
- if project_search_tabs?(:milestones)
%li{ class: active_when(@scope == 'milestones') }
= link_to search_filter_path(scope: 'milestones') do
Milestones
- %span.badge
+ %span.badge.badge-pill
= limited_count(@search_results.limited_milestones_count)
- if project_search_tabs?(:notes)
%li{ class: active_when(@scope == 'notes') }
= link_to search_filter_path(scope: 'notes') do
Comments
- %span.badge
+ %span.badge.badge-pill
= limited_count(@search_results.limited_notes_count)
- if project_search_tabs?(:wiki)
%li{ class: active_when(@scope == 'wiki_blobs') }
= link_to search_filter_path(scope: 'wiki_blobs') do
Wiki
- %span.badge
+ %span.badge.badge-pill
= @search_results.wiki_blobs_count
- if project_search_tabs?(:commits)
%li{ class: active_when(@scope == 'commits') }
= link_to search_filter_path(scope: 'commits') do
Commits
- %span.badge
+ %span.badge.badge-pill
= @search_results.commits_count
- elsif @show_snippets
%li{ class: active_when(@scope == 'snippet_blobs') }
= link_to search_filter_path(scope: 'snippet_blobs', snippets: true, group_id: nil, project_id: nil) do
Snippet Contents
- %span.badge
+ %span.badge.badge-pill
= @search_results.snippet_blobs_count
%li{ class: active_when(@scope == 'snippet_titles') }
= link_to search_filter_path(scope: 'snippet_titles', snippets: true, group_id: nil, project_id: nil) do
Titles and Filenames
- %span.badge
+ %span.badge.badge-pill
= @search_results.snippet_titles_count
- else
%li{ class: active_when(@scope == 'projects') }
= link_to search_filter_path(scope: 'projects') do
Projects
- %span.badge
+ %span.badge.badge-pill
= limited_count(@search_results.limited_projects_count)
%li{ class: active_when(@scope == 'issues') }
= link_to search_filter_path(scope: 'issues') do
Issues
- %span.badge
+ %span.badge.badge-pill
= limited_count(@search_results.limited_issues_count)
%li{ class: active_when(@scope == 'merge_requests') }
= link_to search_filter_path(scope: 'merge_requests') do
Merge requests
- %span.badge
+ %span.badge.badge-pill
= limited_count(@search_results.limited_merge_requests_count)
%li{ class: active_when(@scope == 'milestones') }
= link_to search_filter_path(scope: 'milestones') do
Milestones
- %span.badge
+ %span.badge.badge-pill
= limited_count(@search_results.limited_milestones_count)
diff --git a/app/views/search/_filter.html.haml b/app/views/search/_filter.html.haml
index e4902d368e7..6837f64f132 100644
--- a/app/views/search/_filter.html.haml
+++ b/app/views/search/_filter.html.haml
@@ -11,7 +11,7 @@
- else
Any
= icon("chevron-down")
- .dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-align-right
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-right
= dropdown_title("Filter results by group")
= dropdown_filter("Search groups")
= dropdown_content
@@ -26,7 +26,7 @@
- else
Any
= icon("chevron-down")
- .dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-align-right
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-right
= dropdown_title("Filter results by project")
= dropdown_filter("Search projects")
= dropdown_content
diff --git a/app/views/search/results/_blob.html.haml b/app/views/search/results/_blob.html.haml
index de473c23d66..fdcd126e7a3 100644
--- a/app/views/search/results/_blob.html.haml
+++ b/app/views/search/results/_blob.html.haml
@@ -1,13 +1,5 @@
-- file_name, blob = blob
-.blob-result
- .file-holder
- .js-file-title.file-title
- - ref = @search_results.repository_ref
- - blob_link = project_blob_path(@project, tree_join(ref, file_name))
- = link_to blob_link do
- %i.fa.fa-file
- %strong
- = file_name
- - if blob
- .file-content.code.term
- = render 'shared/file_highlight', blob: blob, first_line_number: blob.startline, blob_link: blob_link
+- project = find_project_for_result_blob(blob)
+- file_name, blob = parse_search_result(blob)
+- blob_link = project_blob_path(project, tree_join(blob.ref, file_name))
+
+= render partial: 'search/results/blob_data', locals: { blob: blob, project: project, file_name: file_name, blob_link: blob_link }
diff --git a/app/views/search/results/_blob_data.html.haml b/app/views/search/results/_blob_data.html.haml
new file mode 100644
index 00000000000..143e9f91ca3
--- /dev/null
+++ b/app/views/search/results/_blob_data.html.haml
@@ -0,0 +1,10 @@
+.blob-result
+ .file-holder
+ .js-file-title.file-title
+ = link_to blob_link do
+ %i.fa.fa-file
+ %strong
+ = search_blob_title(project, file_name)
+ - if blob.data
+ .file-content.code.term
+ = render 'shared/file_highlight', blob: blob, first_line_number: blob.startline
diff --git a/app/views/search/results/_commit.html.haml b/app/views/search/results/_commit.html.haml
index f34eaf89027..ed5a3badf11 100644
--- a/app/views/search/results/_commit.html.haml
+++ b/app/views/search/results/_commit.html.haml
@@ -1 +1 @@
-= render 'projects/commits/commit', project: @project, commit: commit, ref: nil
+= render 'projects/commits/commit', project: commit.project, commit: commit, ref: nil, show_project_name: @project.nil?
diff --git a/app/views/search/results/_issue.html.haml b/app/views/search/results/_issue.html.haml
index b7a27ef6be2..c413c3d4abb 100644
--- a/app/views/search/results/_issue.html.haml
+++ b/app/views/search/results/_issue.html.haml
@@ -4,8 +4,8 @@
= link_to [issue.project.namespace.becomes(Namespace), issue.project, issue] do
%span.term.str-truncated= issue.title
- if issue.closed?
- %span.label.label-danger.prepend-left-5 Closed
- .pull-right ##{issue.iid}
+ %span.badge.badge-danger.prepend-left-5 Closed
+ .float-right ##{issue.iid}
- if issue.description.present?
.description.term
= search_md_sanitize(issue, :description)
diff --git a/app/views/search/results/_merge_request.html.haml b/app/views/search/results/_merge_request.html.haml
index 8b0fd74f680..519176af108 100644
--- a/app/views/search/results/_merge_request.html.haml
+++ b/app/views/search/results/_merge_request.html.haml
@@ -3,10 +3,10 @@
= link_to [merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request] do
%span.term.str-truncated= merge_request.title
- if merge_request.merged?
- %span.label.label-primary.prepend-left-5 Merged
+ %span.badge.badge-primary.prepend-left-5 Merged
- elsif merge_request.closed?
- %span.label.label-danger.prepend-left-5 Closed
- .pull-right= merge_request.to_reference
+ %span.badge.badge-danger.prepend-left-5 Closed
+ .float-right= merge_request.to_reference
- if merge_request.description.present?
.description.term
= search_md_sanitize(merge_request, :description)
diff --git a/app/views/search/results/_snippet_title.html.haml b/app/views/search/results/_snippet_title.html.haml
index d46c4d11e51..9c8afb2165b 100644
--- a/app/views/search/results/_snippet_title.html.haml
+++ b/app/views/search/results/_snippet_title.html.haml
@@ -3,13 +3,13 @@
= link_to reliable_snippet_path(snippet_title) do
= truncate(snippet_title.title, length: 60)
- if snippet_title.private?
- %span.label.label-gray
+ %span.badge.badge-gray
%i.fa.fa-lock
private
- %span.cgray.monospace.tiny.pull-right.term
+ %span.cgray.monospace.tiny.float-right.term
= snippet_title.file_name
- %small.pull-right.cgray
+ %small.float-right.cgray
- if snippet_title.project_id?
= link_to snippet_title.project.full_name, project_path(snippet_title.project)
diff --git a/app/views/search/results/_wiki_blob.html.haml b/app/views/search/results/_wiki_blob.html.haml
index 16a0e432d62..4346217c230 100644
--- a/app/views/search/results/_wiki_blob.html.haml
+++ b/app/views/search/results/_wiki_blob.html.haml
@@ -1,10 +1,5 @@
-- wiki_blob = parse_search_result(wiki_blob)
-.blob-result
- .file-holder
- .js-file-title.file-title
- = link_to project_wiki_path(@project, wiki_blob.basename) do
- %i.fa.fa-file
- %strong
- = wiki_blob.basename
- .file-content.code.term
- = render 'shared/file_highlight', blob: wiki_blob, first_line_number: wiki_blob.startline
+- project = find_project_for_result_blob(wiki_blob)
+- file_name, wiki_blob = parse_search_result(wiki_blob)
+- wiki_blob_link = project_wiki_path(project, wiki_blob.basename)
+
+= render partial: 'search/results/blob_data', locals: { blob: wiki_blob, project: project, file_name: file_name, blob_link: wiki_blob_link }
diff --git a/app/views/shared/_allow_request_access.html.haml b/app/views/shared/_allow_request_access.html.haml
index 53a99a736c0..92268e74b1e 100644
--- a/app/views/shared/_allow_request_access.html.haml
+++ b/app/views/shared/_allow_request_access.html.haml
@@ -1,6 +1,6 @@
-.checkbox
- = form.label :request_access_enabled do
- = form.check_box :request_access_enabled
+.form-check
+ = form.check_box :request_access_enabled, class: 'form-check-input'
+ = form.label :request_access_enabled, class: 'form-check-label' do
%strong Allow users to request access
%br
%span.descr Allow users to request access if visibility is public or internal.
diff --git a/app/views/shared/_choose_group_avatar_button.html.haml b/app/views/shared/_choose_group_avatar_button.html.haml
index 75c65520350..0552fe62090 100644
--- a/app/views/shared/_choose_group_avatar_button.html.haml
+++ b/app/views/shared/_choose_group_avatar_button.html.haml
@@ -1,4 +1,4 @@
%button.btn.js-choose-group-avatar-button{ type: 'button' }= _("Choose File ...")
%span.file_name.js-avatar-filename= _("No file chosen")
= f.file_field :avatar, class: "js-group-avatar-input hidden"
-.help-block= _("The maximum file size allowed is 200KB.")
+.form-text.text-muted= _("The maximum file size allowed is 200KB.")
diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml
index 687cd4d1532..3655c2a1d42 100644
--- a/app/views/shared/_clone_panel.html.haml
+++ b/app/views/shared/_clone_panel.html.haml
@@ -1,22 +1,22 @@
- project = project || @project
.git-clone-holder.input-group
- .input-group-btn
+ .input-group-prepend
- if allowed_protocols_present?
- .clone-dropdown-btn.btn
+ .input-group-text.clone-dropdown-btn.btn
%span
= enabled_project_button(project, enabled_protocol)
- else
- %a#clone-dropdown.btn.clone-dropdown-btn.qa-clone-dropdown{ href: '#', data: { toggle: 'dropdown' } }
+ %a#clone-dropdown.input-group-text.btn.clone-dropdown-btn.qa-clone-dropdown{ href: '#', data: { toggle: 'dropdown' } }
%span
= default_clone_protocol.upcase
= icon('caret-down')
- %ul.dropdown-menu.dropdown-menu-selectable.dropdown-menu-right.clone-options-dropdown
+ %ul.dropdown-menu.dropdown-menu-selectable.clone-options-dropdown
%li
= ssh_clone_button(project)
%li
= http_clone_button(project)
= text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true, aria: { label: 'Project clone URL' }
- .input-group-btn
- = clipboard_button(target: '#project_clone', title: _("Copy URL to clipboard"), class: "btn-default btn-clipboard")
+ .input-group-append
+ = clipboard_button(target: '#project_clone', title: _("Copy URL to clipboard"), class: "input-group-text btn-default btn-clipboard")
diff --git a/app/views/shared/_commit_message_container.html.haml b/app/views/shared/_commit_message_container.html.haml
index 2329de9e11f..68c14c307ac 100644
--- a/app/views/shared/_commit_message_container.html.haml
+++ b/app/views/shared/_commit_message_container.html.haml
@@ -1,7 +1,7 @@
-.form-group.commit_message-group
+.form-group.row.commit_message-group
- nonce = SecureRandom.hex
- descriptions = local_assigns.slice(:message_with_description, :message_without_description)
- = label_tag "commit_message-#{nonce}", class: 'control-label' do
+ = label_tag "commit_message-#{nonce}", class: 'col-form-label col-sm-2' do
#{ _('Commit message') }
.col-sm-10
.commit-message-container
diff --git a/app/views/shared/_commit_well.html.haml b/app/views/shared/_commit_well.html.haml
index 50e3d80a84d..6f1fe9bfdc5 100644
--- a/app/views/shared/_commit_well.html.haml
+++ b/app/views/shared/_commit_well.html.haml
@@ -1,4 +1,4 @@
-.info-well.hidden-xs.project-last-commit.append-bottom-default
+.info-well.d-none.d-sm-block.project-last-commit.append-bottom-default
.well-segment
%ul.blob-commit-info
= render 'projects/commits/commit', commit: commit, ref: ref, project: project
diff --git a/app/views/shared/_confirm_modal.html.haml b/app/views/shared/_confirm_modal.html.haml
index f94f8ffc604..1dcf4369253 100644
--- a/app/views/shared/_confirm_modal.html.haml
+++ b/app/views/shared/_confirm_modal.html.haml
@@ -2,9 +2,10 @@
.modal-dialog
.modal-content
.modal-header
- %a.close{ href: "#", "data-dismiss" => "modal" } ×
%h3.page-title
Confirmation required
+ %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
+ %span{ "aria-hidden": true } &times;
.modal-body
%p.text-danger.js-confirm-text
diff --git a/app/views/shared/_delete_label_modal.html.haml b/app/views/shared/_delete_label_modal.html.haml
index 01effefc34d..b96380923ac 100644
--- a/app/views/shared/_delete_label_modal.html.haml
+++ b/app/views/shared/_delete_label_modal.html.haml
@@ -2,8 +2,9 @@
.modal-dialog
.modal-content
.modal-header
- %button.close{ data: {dismiss: 'modal' } } &times;
%h3.page-title Delete #{render_colored_label(label, tooltip: false)} ?
+ %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
+ %span{ "aria-hidden": true } &times;
.modal-body
%p
diff --git a/app/views/shared/_email_with_badge.html.haml b/app/views/shared/_email_with_badge.html.haml
index b7bbc109238..ad863b1967d 100644
--- a/app/views/shared/_email_with_badge.html.haml
+++ b/app/views/shared/_email_with_badge.html.haml
@@ -1,4 +1,4 @@
-- css_classes = %w(label label-verification-status)
+- css_classes = %w(badge badge-verification-status)
- css_classes << (verified ? 'verified': 'unverified')
- text = verified ? 'Verified' : 'Unverified'
diff --git a/app/views/shared/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml
index e7fa7477e0c..ecb5b1c6ebc 100644
--- a/app/views/shared/_event_filter.html.haml
+++ b/app/views/shared/_event_filter.html.haml
@@ -1,7 +1,7 @@
.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
.fade-left= icon('angle-left')
.fade-right= icon('angle-right')
- %ul.nav-links.event-filter.scrolling-tabs
+ %ul.nav-links.event-filter.scrolling-tabs.nav.nav-tabs
= event_filter_link EventFilter.all, _('All'), s_('EventFilterBy|Filter by all')
- if event_filter_visible(:repository)
= event_filter_link EventFilter.push, _('Push events'), s_('EventFilterBy|Filter by push events')
diff --git a/app/views/shared/_field.html.haml b/app/views/shared/_field.html.haml
index aea0a8fd8e0..b89045e726a 100644
--- a/app/views/shared/_field.html.haml
+++ b/app/views/shared/_field.html.haml
@@ -9,11 +9,11 @@
- help = field[:help]
- disabled = disable_fields_service?(@service)
-.form-group
+.form-group.row
- if type == "password" && value.present?
- = form.label name, "Enter new #{title.downcase}", class: "control-label"
+ = form.label name, "Enter new #{title.downcase}", class: "col-form-label col-sm-2"
- else
- = form.label name, title, class: "control-label"
+ = form.label name, title, class: "col-form-label col-sm-2"
.col-sm-10
- if type == 'text'
= form.text_field name, class: "form-control", placeholder: placeholder, required: required, disabled: disabled
@@ -26,4 +26,4 @@
- elsif type == 'password'
= form.password_field name, autocomplete: "new-password", class: "form-control", required: value.blank? && required, disabled: disabled
- if help
- %span.help-block= help
+ %span.form-text.text-muted= help
diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml
index 403d22c79f8..dbed4b94d61 100644
--- a/app/views/shared/_group_form.html.haml
+++ b/app/views/shared/_group_form.html.haml
@@ -2,15 +2,16 @@
- group_path = root_url
- group_path << parent.full_path + '/' if parent
-.form-group
- = f.label :path, class: 'control-label' do
+.form-group.row
+ = f.label :path, class: 'col-form-label col-sm-2' do
Group path
.col-sm-10
.input-group.gl-field-error-anchor
- .group-root-path.input-group-addon.has-tooltip{ title: group_path, :'data-placement' => 'bottom' }
- %span>= root_url
- - if parent
- %strong= parent.full_path + '/'
+ .group-root-path.input-group-prepend.has-tooltip{ title: group_path, :'data-placement' => 'bottom' }
+ .input-group-text
+ %span>= root_url
+ - if parent
+ %strong= parent.full_path + '/'
= f.hidden_field :parent_id
= f.text_field :path, placeholder: 'open-source', class: 'form-control',
autofocus: local_assigns[:autofocus] || false, required: true,
@@ -24,8 +25,8 @@
= succeed '.' do
= link_to 'Learn more', help_page_path('user/group/index', anchor: 'changing-a-groups-path'), target: '_blank'
-.form-group.group-name-holder
- = f.label :name, class: 'control-label' do
+.form-group.row.group-name-holder
+ = f.label :name, class: 'col-form-label col-sm-2' do
Group name
.col-sm-10
= f.text_field :name, class: 'form-control',
@@ -33,14 +34,14 @@
title: 'You can choose a descriptive name different from the path.'
- if @group.persisted?
- .form-group.group-name-holder
- = f.label :id, class: 'control-label' do
+ .form-group.row.group-name-holder
+ = f.label :id, class: 'col-form-label col-sm-2' do
= _("Group ID")
.col-sm-10
= f.text_field :id, class: 'form-control', readonly: true
-.form-group.group-description-holder
- = f.label :description, class: 'control-label'
+.form-group.row.group-description-holder
+ = f.label :description, class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_area :description, maxlength: 250,
class: 'form-control js-gfm-input', rows: 4
diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml
index 3806ead6c87..356e12cf9f8 100644
--- a/app/views/shared/_import_form.html.haml
+++ b/app/views/shared/_import_form.html.haml
@@ -7,13 +7,14 @@
= f.text_field :import_url, autocomplete: 'off', class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git', required: true
- .well.prepend-top-20
- %ul
- %li
- = _('The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>.').html_safe
- %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>.').html_safe
- %li
- = import_will_timeout_message(ci_cd_only)
- %li
- = import_svn_message(ci_cd_only)
+ .info-well.prepend-top-20
+ .well-segment
+ %ul
+ %li
+ = _('The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>.').html_safe
+ %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>.').html_safe
+ %li
+ = import_will_timeout_message(ci_cd_only)
+ %li
+ = import_svn_message(ci_cd_only)
diff --git a/app/views/shared/_issuable_meta_data.html.haml b/app/views/shared/_issuable_meta_data.html.haml
index 430d9a9dd76..6cc8c485666 100644
--- a/app/views/shared/_issuable_meta_data.html.haml
+++ b/app/views/shared/_issuable_meta_data.html.haml
@@ -5,21 +5,21 @@
- issuable_mr = @issuable_meta_data[issuable.id].merge_requests_count
- if issuable_mr > 0
- %li.issuable-mr.hidden-xs.has-tooltip{ title: _('Related merge requests') }
+ %li.issuable-mr.d-none.d-sm-block.has-tooltip{ title: _('Related merge requests') }
= image_tag('icon-merge-request-unmerged.svg', class: 'icon-merge-request-unmerged')
= issuable_mr
- if upvotes > 0
- %li.issuable-upvotes.hidden-xs.has-tooltip{ title: _('Upvotes') }
+ %li.issuable-upvotes.d-none.d-sm-block.has-tooltip{ title: _('Upvotes') }
= icon('thumbs-up')
= upvotes
- if downvotes > 0
- %li.issuable-downvotes.hidden-xs.has-tooltip{ title: _('Downvotes') }
+ %li.issuable-downvotes.d-none.d-sm-block.has-tooltip{ title: _('Downvotes') }
= icon('thumbs-down')
= downvotes
-%li.issuable-comments.hidden-xs
+%li.issuable-comments.d-none.d-sm-block
= link_to issuable_url, class: ['has-tooltip', ('no-comments' if note_count.zero?)], title: _('Comments') do
= icon('comments')
= note_count
diff --git a/app/views/shared/_issues.html.haml b/app/views/shared/_issues.html.haml
index 49555b6ff4e..987a5d4f13f 100644
--- a/app/views/shared/_issues.html.haml
+++ b/app/views/shared/_issues.html.haml
@@ -1,5 +1,5 @@
- if @issues.to_a.any?
- .panel.panel-default.panel-small.panel-without-border
+ .card.card-small.card-without-border
%ul.content-list.issues-list.issuable-list
= render partial: 'projects/issues/issue', collection: @issues
= paginate @issues, theme: "gitlab"
diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml
index 836df57a3a2..e93925b5ef9 100644
--- a/app/views/shared/_label.html.haml
+++ b/app/views/shared/_label.html.haml
@@ -1,93 +1,72 @@
- label_css_id = dom_id(label)
- status = label_subscription_status(label, @project).inquiry if current_user
- subject = local_assigns[:subject]
+- use_label_priority = local_assigns.fetch(:use_label_priority, false)
+- force_priority = local_assigns.fetch(:force_priority, use_label_priority ? label.priority.present? : false)
- toggle_subscription_path = toggle_subscription_label_path(label, @project) if current_user
- show_label_merge_requests_link = show_label_issuables_link?(label, :merge_requests, project: @project)
- show_label_issues_link = show_label_issuables_link?(label, :issues, project: @project)
+- tooltip_title = label_status_tooltip(label, status) if status
%li.label-list-item{ id: label_css_id, data: { id: label.id } }
- = render "shared/label_row", label: label
-
- .visible-xs.visible-sm-inline-block.dropdown
- %button.btn.btn-default.label-options-toggle{ type: 'button', data: { toggle: "dropdown" } }
- Options
- = icon('caret-down')
- .dropdown-menu.dropdown-menu-align-right
- %ul
- - if show_label_merge_requests_link
- %li
- = link_to_label(label, subject: subject, type: :merge_request) do
- View merge requests
- - if show_label_issues_link
- %li
- = link_to_label(label, subject: subject) do
- View open issues
- - if current_user
- %li.label-subscription
- - if can_subscribe_to_label_in_different_levels?(label)
- %a.js-unsubscribe-button.label-subscribe-button{ role: 'button', href: '#', class: ('hidden' if status.unsubscribed?), data: { url: toggle_subscription_path } }
- %span Unsubscribe
- %a.js-subscribe-button.label-subscribe-button{ role: 'button', href: '#', class: ('hidden' unless status.unsubscribed?), data: { url: toggle_subscription_project_label_path(@project, label) } }
- %span Subscribe at project level
- %a.js-subscribe-button.label-subscribe-button{ role: 'button', href: '#', class: ('hidden' unless status.unsubscribed?), data: { url: toggle_subscription_group_label_path(label.group, label) } }
- %span Subscribe at group level
- - else
- %a.js-subscribe-button.label-subscribe-button{ role: 'button', href: '#', data: { status: status, url: toggle_subscription_path } }
- %span= label_subscription_toggle_button_text(label, @project)
-
- - if can?(current_user, :admin_label, label)
- %li
- = link_to 'Edit', edit_label_path(label)
- %li
- = link_to 'Delete',
- destroy_label_path(label),
- title: 'Delete',
- method: :delete,
- data: {confirm: 'Remove this label? Are you sure?'},
- class: 'text-danger'
-
- .pull-right.hidden-xs.hidden-sm
+ = render "shared/label_row", label: label, subject: subject, force_priority: force_priority
+ %ul.label-actions-list
+ - if @project
+ %li.inline
+ .label-badge.label-badge-gray= label.model_name.human.capitalize
+ - if can?(current_user, :admin_label, @project)
+ %li.inline.js-toggle-priority{ data: { url: remove_priority_project_label_path(@project, label),
+ dom_id: dom_id(label), type: label.type } }
+ %button.label-action.add-priority.btn.btn-transparent.has-tooltip{ title: _('Prioritize'), type: 'button', data: { placement: 'top' }, aria_label: _('Prioritize label') }
+ = sprite_icon('star-o')
+ %button.label-action.remove-priority.btn.btn-transparent.has-tooltip{ title: _('Remove priority'), type: 'button', data: { placement: 'top' }, aria_label: _('Deprioritize label') }
+ = sprite_icon('star')
- if can?(current_user, :admin_label, label)
- - if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, label.project.group)
- %button.js-promote-project-label-button.btn.btn-transparent.btn-action.has-tooltip{ title: _('Promote to Group Label'),
- disabled: true,
- type: 'button',
- data: { url: promote_project_label_path(label.project, label),
- label_title: label.title,
- label_color: label.color,
- label_text_color: label.text_color,
- group_name: label.project.group.name,
- target: '#promote-label-modal',
- container: 'body',
- toggle: 'modal' } }
- = sprite_icon('level-up')
- = link_to edit_label_path(label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do
- %span.sr-only Edit
- = sprite_icon('pencil')
- %span{ data: { toggle: 'modal', target: "#modal-delete-label-#{label.id}" } }
- = link_to "#", title: "Delete", class: 'btn btn-transparent btn-action remove-row', data: { toggle: "tooltip" } do
- %span.sr-only Delete
- = sprite_icon('remove')
+ %li.inline
+ = link_to edit_label_path(label), class: 'btn btn-transparent label-action edit', aria_label: 'Edit label' do
+ = sprite_icon('pencil')
+ %li.inline
+ .dropdown
+ %button{ type: 'button', class: 'btn btn-transparent js-label-options-dropdown label-action', data: { toggle: 'dropdown' }, aria_label: _('Label actions dropdown') }
+ = sprite_icon('ellipsis_v')
+ .dropdown-menu.dropdown-open-left
+ %ul
+ - if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, label.project.group)
+ %li
+ %button.js-promote-project-label-button.btn.btn-transparent.btn-action{ disabled: true, type: 'button',
+ data: { url: promote_project_label_path(label.project, label),
+ label_title: label.title,
+ label_color: label.color,
+ label_text_color: label.text_color,
+ group_name: label.project.group.name,
+ target: '#promote-label-modal',
+ container: 'body',
+ toggle: 'modal' } }
+ = _('Promote to group label')
+ - if can?(current_user, :admin_label, label)
+ %li
+ %span{ data: { toggle: 'modal', target: "#modal-delete-label-#{label.id}" } }
+ %button.text-danger.remove-row{ type: 'button' }= _('Delete')
- if current_user
- .label-subscription.inline
+ %li.inline.label-subscription
- if can_subscribe_to_label_in_different_levels?(label)
- %button.js-unsubscribe-button.label-subscribe-button.btn.btn-default{ type: 'button', class: ('hidden' if status.unsubscribed?), data: { url: toggle_subscription_path } }
- %span Unsubscribe
- = icon('spinner spin', class: 'label-subscribe-button-loading')
-
+ %button.js-unsubscribe-button.label-subscribe-button.btn.btn-default{ class: ('hidden' if status.unsubscribed?), data: { url: toggle_subscription_path, toggle: 'tooltip' }, title: tooltip_title }
+ %span= _('Unsubscribe')
.dropdown.dropdown-group-label{ class: ('hidden' unless status.unsubscribed?) }
- %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
- %span Subscribe
- = icon('chevron-down')
- %ul.dropdown-menu
- %li
- %a.js-subscribe-button{ class: ('hidden' unless status.unsubscribed?), data: { url: toggle_subscription_project_label_path(@project, label) } }
- Project level
- %a.js-subscribe-button{ class: ('hidden' unless status.unsubscribed?), data: { url: toggle_subscription_group_label_path(label.group, label) } }
- Group level
+ %button.label-subscribe-button.btn.btn-default{ data: { toggle: 'dropdown' } }
+ %span
+ = _('Subscribe')
+ = sprite_icon('chevron-down')
+ .dropdown-menu.dropdown-open-left
+ %ul
+ %li
+ %button.js-subscribe-button.label-subscribe-button.btn.btn-default{ class: ('hidden' unless status.unsubscribed?), data: { status: status, url: toggle_subscription_project_label_path(@project, label) } }
+ %span= _('Subscribe at project level')
+ %li
+ %button.js-subscribe-button.js-group-level.label-subscribe-button.btn.btn-default{ class: ('hidden' unless status.unsubscribed?), data: { status: status, url: toggle_subscription_group_label_path(label.group, label) } }
+ %span= _('Subscribe at group level')
- else
- %button.js-subscribe-button.label-subscribe-button.btn.btn-default{ type: 'button', data: { status: status, url: toggle_subscription_path } }
+ %button.js-subscribe-button.label-subscribe-button.btn.btn-default{ data: { status: status, url: toggle_subscription_path, toggle: 'tooltip' }, title: tooltip_title }
%span= label_subscription_toggle_button_text(label, @project)
- = icon('spinner spin', class: 'label-subscribe-button-loading')
= render 'shared/delete_label_modal', label: label
diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml
index bd4f191502e..0ae3ab8f090 100644
--- a/app/views/shared/_label_row.html.haml
+++ b/app/views/shared/_label_row.html.haml
@@ -1,30 +1,23 @@
- subject = local_assigns[:subject]
+- force_priority = local_assigns.fetch(:force_priority, false)
- show_label_issues_link = show_label_issuables_link?(label, :issues, project: @project)
- show_label_merge_requests_link = show_label_issuables_link?(label, :merge_requests, project: @project)
-%span.label-row
- - if can?(current_user, :admin_label, @project)
- .draggable-handler
- = icon('bars')
- .js-toggle-priority.toggle-priority{ data: { url: remove_priority_project_label_path(@project, label),
- dom_id: dom_id(label), type: label.type } }
- %button.add-priority.btn.has-tooltip{ title: 'Prioritize', type: 'button', :'data-placement' => 'top' }
- = icon('star-o')
- %button.remove-priority.btn.has-tooltip{ title: 'Remove priority', type: 'button', :'data-placement' => 'top' }
- = icon('star')
- %span.label-name
- = link_to_label(label, subject: @project, tooltip: false)
- - if defined?(@project) && @project.group.present?
- %span.label-type
- = label.model_name.human.titleize
-
- %span.label-description
+.label-name
+ = link_to_label(label, subject: @project, tooltip: false)
+.label-description
+ .append-right-default.prepend-left-default
- if label.description.present?
- .description-text
+ .description-text.append-bottom-10
= markdown_field(label, :description)
- .hidden-xs.hidden-sm
+ %ul.label-links
- if show_label_issues_link
- = link_to_label(label, subject: subject) { 'Issues' }
+ %li.label-link-item.inline
+ = link_to_label(label, subject: subject) { 'Issues' }
- if show_label_merge_requests_link
&middot;
- = link_to_label(label, subject: subject, type: :merge_request) { 'Merge requests' }
+ %li.label-link-item.inline
+ = link_to_label(label, subject: subject, type: :merge_request) { _('Merge requests') }
+ - if force_priority
+ %li.label-link-item.js-priority-badge.inline.prepend-left-10
+ .label-badge.label-badge-blue= _('Prioritized label')
diff --git a/app/views/shared/_merge_requests.html.haml b/app/views/shared/_merge_requests.html.haml
index 0517896cfbd..700ec4b606f 100644
--- a/app/views/shared/_merge_requests.html.haml
+++ b/app/views/shared/_merge_requests.html.haml
@@ -1,5 +1,5 @@
- if @merge_requests.to_a.any?
- .panel.panel-default.panel-small.panel-without-border
+ .card.card-small.card-without-border
%ul.content-list.mr-list.issuable-list
= render partial: 'projects/merge_requests/merge_request', collection: @merge_requests
diff --git a/app/views/shared/_milestone_expired.html.haml b/app/views/shared/_milestone_expired.html.haml
index 5e9007aaaac..099e3ac8462 100644
--- a/app/views/shared/_milestone_expired.html.haml
+++ b/app/views/shared/_milestone_expired.html.haml
@@ -1,7 +1,6 @@
- if milestone.expired? and not milestone.closed?
- %span.cred (Expired)
+ .status-box.status-box-expired.append-bottom-5 Expired
- if milestone.upcoming?
- %span.clgray (Upcoming)
-- if milestone.due_date || milestone.start_date
- %span
- = milestone_date_range(milestone)
+ .status-box.status-box-mr-merged.append-bottom-5 Upcoming
+- if milestone.closed?
+ .status-box.status-box-closed.append-bottom-5 Closed
diff --git a/app/views/shared/_milestones_filter.html.haml b/app/views/shared/_milestones_filter.html.haml
index 034b76b978f..6c1ac20d544 100644
--- a/app/views/shared/_milestones_filter.html.haml
+++ b/app/views/shared/_milestones_filter.html.haml
@@ -1,13 +1,13 @@
-%ul.nav-links.mobile-separator
+%ul.nav-links.mobile-separator.nav.nav-tabs
%li{ class: milestone_class_for_state(params[:state], 'opened', true) }>
= link_to milestones_filter_path(state: 'opened') do
Open
- %span.badge= counts[:opened]
+ %span.badge.badge-pill= counts[:opened]
%li{ class: milestone_class_for_state(params[:state], 'closed') }>
= link_to milestones_filter_path(state: 'closed', sort: 'due_date_desc') do
Closed
- %span.badge= counts[:closed]
+ %span.badge.badge-pill= counts[:closed]
%li{ class: milestone_class_for_state(params[:state], 'all') }>
= link_to milestones_filter_path(state: 'all', sort: 'due_date_desc') do
All
- %span.badge= counts[:all]
+ %span.badge.badge-pill= counts[:all]
diff --git a/app/views/shared/_milestones_sort_dropdown.html.haml b/app/views/shared/_milestones_sort_dropdown.html.haml
index 9b2f2fdcc93..a6ba3b59365 100644
--- a/app/views/shared/_milestones_sort_dropdown.html.haml
+++ b/app/views/shared/_milestones_sort_dropdown.html.haml
@@ -6,7 +6,7 @@
- else
= sort_title_due_date_soon
= icon('chevron-down')
- %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort
+ %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-sort
%li
= link_to page_filter_path(sort: sort_value_due_date_soon, label: true) do
= sort_title_due_date_soon
diff --git a/app/views/shared/_new_commit_form.html.haml b/app/views/shared/_new_commit_form.html.haml
index 9221fd1e025..81c33eeea4f 100644
--- a/app/views/shared/_new_commit_form.html.haml
+++ b/app/views/shared/_new_commit_form.html.haml
@@ -7,8 +7,8 @@
= hidden_field_tag 'branch_name', @ref
- else
- if can?(current_user, :push_code, @project)
- .form-group.branch
- = label_tag 'branch_name', _('Target Branch'), class: 'control-label'
+ .form-group.row.branch
+ = label_tag 'branch_name', _('Target Branch'), class: 'col-form-label col-sm-2'
.col-sm-10
= text_field_tag 'branch_name', branch_name, required: true, class: "form-control js-branch-name ref-name"
diff --git a/app/views/shared/_new_merge_request_checkbox.html.haml b/app/views/shared/_new_merge_request_checkbox.html.haml
index 133c31f09c4..24c0dfe247f 100644
--- a/app/views/shared/_new_merge_request_checkbox.html.haml
+++ b/app/views/shared/_new_merge_request_checkbox.html.haml
@@ -1,7 +1,7 @@
-.checkbox
+.form-check.prepend-top-8
- nonce = SecureRandom.hex
- = label_tag "create_merge_request-#{nonce}" do
- = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: "create_merge_request-#{nonce}"
+ = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request form-check-input', id: "create_merge_request-#{nonce}"
+ = label_tag "create_merge_request-#{nonce}", class: 'form-check-label' do
- translation_variables = { new_merge_request: "<strong>#{_('new merge request')}</strong>" }
- translation = _('Start a %{new_merge_request} with these changes') % translation_variables
#{ translation.html_safe }
diff --git a/app/views/shared/_new_project_item_select.html.haml b/app/views/shared/_new_project_item_select.html.haml
index ac2ebb701a5..d38d161047b 100644
--- a/app/views/shared/_new_project_item_select.html.haml
+++ b/app/views/shared/_new_project_item_select.html.haml
@@ -1,7 +1,7 @@
- if any_projects?(@projects)
.project-item-select-holder.btn-group
- %a.btn.btn-new.new-project-item-link{ href: '', data: { label: local_assigns[:label], type: local_assigns[:type] } }
+ %a.btn.btn-new.new-project-item-link.qa-new-project-item-link{ href: '', data: { label: local_assigns[:label], type: local_assigns[:type] } }
= icon('spinner spin')
= project_select_tag :project_path, class: "project-item-select", data: { include_groups: local_assigns[:include_groups], order_by: 'last_activity_at', relative_path: local_assigns[:path] }, with_feature_enabled: local_assigns[:with_feature_enabled]
- %button.btn.btn-new.new-project-item-select-button
+ %button.btn.btn-new.new-project-item-select-button.qa-new-project-item-select-button
= icon('caret-down')
diff --git a/app/views/shared/_personal_access_tokens_form.html.haml b/app/views/shared/_personal_access_tokens_form.html.haml
index b8b1f4ca42f..28407b543b9 100644
--- a/app/views/shared/_personal_access_tokens_form.html.haml
+++ b/app/views/shared/_personal_access_tokens_form.html.haml
@@ -9,13 +9,17 @@
= form_errors(token)
- .form-group
- = f.label :name, class: 'label-light'
- = f.text_field :name, class: "form-control", required: true
+ .row
+ .form-group.col-md-6
+ = f.label :name, class: 'label-light'
+ = f.text_field :name, class: "form-control", required: true
- .form-group
- = f.label :expires_at, class: 'label-light'
- = f.text_field :expires_at, class: "datepicker form-control"
+ .row
+ .form-group.col-md-6
+ = f.label :expires_at, class: 'label-light'
+ .input-icon-wrapper
+ = f.text_field :expires_at, class: "datepicker form-control", placeholder: 'YYYY-MM-DD'
+ = icon('calendar', { class: 'input-icon-right' })
.form-group
= f.label :scopes, class: 'label-light'
diff --git a/app/views/shared/_personal_access_tokens_table.html.haml b/app/views/shared/_personal_access_tokens_table.html.haml
index c5e4d6e2871..cadac1cc99d 100644
--- a/app/views/shared/_personal_access_tokens_table.html.haml
+++ b/app/views/shared/_personal_access_tokens_table.html.haml
@@ -35,7 +35,7 @@
= text_field_tag 'impersonation-token-token', token.token, readonly: true, class: "form-control"
= clipboard_button(text: token.token)
- path = impersonation ? revoke_admin_user_impersonation_token_path(token.user, token) : revoke_profile_personal_access_token_path(token)
- %td= link_to "Revoke", path, method: :put, class: "btn btn-danger pull-right", data: { confirm: "Are you sure you want to revoke this #{type} Token? This action cannot be undone." }
+ %td= link_to "Revoke", path, method: :put, class: "btn btn-danger float-right", data: { confirm: "Are you sure you want to revoke this #{type} Token? This action cannot be undone." }
- else
.settings-message.text-center
This user has no active #{type} Tokens.
diff --git a/app/views/shared/_project_limit.html.haml b/app/views/shared/_project_limit.html.haml
index f4eb8e491b9..2c52eccccb6 100644
--- a/app/views/shared/_project_limit.html.haml
+++ b/app/views/shared/_project_limit.html.haml
@@ -1,8 +1,8 @@
- if cookies[:hide_project_limit_message].blank? && !current_user.hide_project_limit && !current_user.can_create_project? && current_user.projects_limit > 0
- .project-limit-message.alert.alert-warning.hidden-xs
+ .project-limit-message.alert.alert-warning.d-none.d-sm-block
You won't be able to create new projects because you have reached your project limit.
- .pull-right
+ .float-right
= link_to "Don't show again", profile_path(user: {hide_project_limit: true}), method: :put, class: 'alert-link'
|
= link_to 'Remind later', '#', class: 'hide-project-limit-message alert-link'
diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml
index f1c39b9e923..4e7061eef1c 100644
--- a/app/views/shared/_ref_switcher.html.haml
+++ b/app/views/shared/_ref_switcher.html.haml
@@ -9,7 +9,7 @@
= 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, sort: 'updated_desc'), field_name: 'ref', submit_form_on_click: true, visit: true }, { toggle_class: "js-project-refs-dropdown qa-branches-select" }
- .dropdown-menu.dropdown-menu-selectable.git-revision-dropdown.dropdown-menu-paging.qa-branches-dropdown{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) }
+ .dropdown-menu.dropdown-menu-selectable.git-revision-dropdown.dropdown-menu-paging.qa-branches-dropdown{ class: ("dropdown-menu-right" if local_assigns[:align_right]) }
.dropdown-page-one
= dropdown_title _("Switch branch/tag")
= dropdown_filter _("Search branches and tags")
diff --git a/app/views/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml
index a41aaed66a3..6fa61c15493 100644
--- a/app/views/shared/_service_settings.html.haml
+++ b/app/views/shared/_service_settings.html.haml
@@ -3,37 +3,37 @@
- if lookup_context.template_exists?('help', "projects/services/#{@service.to_param}", true)
= render "projects/services/#{@service.to_param}/help", subject: subject
- elsif @service.help.present?
- .well
- = markdown @service.help
+ .info-well
+ .well-segment
+ = markdown @service.help
.service-settings
- if @service.show_active_box?
- .form-group
- = form.label :active, "Active", class: "control-label"
+ .form-group.row
+ = form.label :active, "Active", class: "col-form-label col-sm-2"
.col-sm-10
= form.check_box :active, disabled: disable_fields_service?(@service)
- if @service.configurable_events.present?
- .form-group
- = form.label :url, "Trigger", class: 'control-label'
+ .form-group.row
+ .col-sm-2.text-right Trigger
.col-sm-10
- @service.configurable_events.each do |event|
- %div
- = form.check_box service_event_field_name(event), class: 'pull-left'
- .prepend-left-20
- = form.label service_event_field_name(event), class: 'list-label' do
+ .form-group
+ .form-check
+ = form.check_box service_event_field_name(event), class: 'form-check-input'
+ = form.label service_event_field_name(event), class: 'form-check-label' do
%strong
= event.humanize
- - field = @service.event_field(event)
+ - field = @service.event_field(event)
- - if field
- %p
+ - if field
= form.text_field field[:name], class: "form-control", placeholder: field[:placeholder]
- %p.light
- = @service.class.event_description(event)
+ %p.text-muted
+ = @service.class.event_description(event)
- @service.global_fields.each do |field|
- type = field[:type]
diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml
index 7ff5e679f17..be6d4f1c32b 100644
--- a/app/views/shared/_sort_dropdown.html.haml
+++ b/app/views/shared/_sort_dropdown.html.haml
@@ -2,10 +2,10 @@
- viewing_issues = controller.controller_name == 'issues' || controller.action_name == 'issues'
.dropdown.inline.prepend-left-10
- %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown' } }
+ %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' } }
= sorted_by
= icon('chevron-down')
- %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable.dropdown-menu-sort
+ %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort
%li
= sortable_item(sort_title_priority, page_filter_path(sort: sort_value_priority, label: true), sorted_by)
= sortable_item(sort_title_created_date, page_filter_path(sort: sort_value_created_date, label: true), sorted_by)
diff --git a/app/views/shared/_user_dropdown_contributing_link.html.haml b/app/views/shared/_user_dropdown_contributing_link.html.haml
new file mode 100644
index 00000000000..333d6fa3489
--- /dev/null
+++ b/app/views/shared/_user_dropdown_contributing_link.html.haml
@@ -0,0 +1,5 @@
+%li
+ = link_to "https://about.gitlab.com/contributing", target: '_blank', class: 'text-nowrap' do
+ = _("Contribute to GitLab")
+ = sprite_icon('external-link', size: 16)
+%li.divider
diff --git a/app/views/shared/_visibility_level.html.haml b/app/views/shared/_visibility_level.html.haml
index 192d2502aaf..01ce1225b8d 100644
--- a/app/views/shared/_visibility_level.html.haml
+++ b/app/views/shared/_visibility_level.html.haml
@@ -1,11 +1,11 @@
- with_label = local_assigns.fetch(:with_label, true)
-.form-group.visibility-level-setting
+.form-group.row.visibility-level-setting
- if with_label
- = f.label :visibility_level, class: 'control-label' do
+ = f.label :visibility_level, class: 'col-form-label col-sm-2' do
Visibility Level
= link_to icon('question-circle'), help_page_path("public_access/public_access")
- %div{ :class => ("col-sm-10" if with_label) }
+ %div{ :class => (with_label ? "col-sm-10" : "col-sm-12") }
- if can_change_visibility_level
= render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: visibility_level, form_model: form_model)
- else
diff --git a/app/views/shared/_visibility_radios.html.haml b/app/views/shared/_visibility_radios.html.haml
index 0ec7677a566..dd6b9cce58e 100644
--- a/app/views/shared/_visibility_radios.html.haml
+++ b/app/views/shared/_visibility_radios.html.haml
@@ -2,9 +2,9 @@
- disallowed = disallowed_visibility_level?(form_model, level)
- restricted = restricted_visibility_levels.include?(level)
- disabled = disallowed || restricted
- .radio{ class: [('disabled' if disabled), ('restricted' if restricted)] }
- = form.label "#{model_method}_#{level}" do
- = form.radio_button model_method, level, checked: (selected_level == level), disabled: disabled
+ .form-check{ class: [('disabled' if disabled), ('restricted' if restricted)] }
+ = form.radio_button model_method, level, checked: (selected_level == level), disabled: disabled, class: 'form-check-input'
+ = form.label "#{model_method}_#{level}", class: 'form-check-label' do
= visibility_level_icon(level)
.option-title
= visibility_level_label(level)
diff --git a/app/views/shared/boards/_show.html.haml b/app/views/shared/boards/_show.html.haml
index dac60094686..28e6fe1b16d 100644
--- a/app/views/shared/boards/_show.html.haml
+++ b/app/views/shared/boards/_show.html.haml
@@ -2,19 +2,20 @@
- group = local_assigns.fetch(:group, false)
- @no_breadcrumb_container = true
- @no_container = true
-- @content_class = "issue-boards-content"
-- breadcrumb_title "Issue Board"
-- page_title "Boards"
+- @content_class = "issue-boards-content js-focus-mode-board"
+- breadcrumb_title _("Issue Boards")
+- page_title _("Boards")
- content_for :page_specific_javascripts do
-# haml-lint:disable InlineJavaScript
%script#js-board-template{ type: "text/x-template" }= render "shared/boards/components/board"
%script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal
+ %script#js-board-promotion{ type: "text/x-template" }= render_if_exists "shared/promotions/promote_issue_board"
#board-app.boards-app{ "v-cloak" => true, data: board_data, ":class" => "{ 'is-compact': detailIssueVisible }" }
- .hidden-xs.hidden-sm
- = render 'shared/issuable/search_bar', type: :boards
+ .d-none.d-sm-none.d-md-block
+ = render 'shared/issuable/search_bar', type: :boards, board: board
.boards-list
.boards-app-loading.text-center{ "v-if" => "loading" }
diff --git a/app/views/shared/boards/components/_board.html.haml b/app/views/shared/boards/components/_board.html.haml
index aea40df41b0..03e008f5fa0 100644
--- a/app/views/shared/boards/components/_board.html.haml
+++ b/app/views/shared/boards/components/_board.html.haml
@@ -1,4 +1,4 @@
-.board{ ":class" => '{ "is-draggable": !list.preset, "is-expandable": list.isExpandable, "is-collapsed": !list.isExpanded }',
+.board{ ":class" => '{ "is-draggable": !list.preset, "is-expandable": list.isExpandable, "is-collapsed": !list.isExpanded, "board-type-assignee": list.type === "assignee" }',
":data-id" => "list.id" }
.board-inner
%header.board-header{ ":class" => '{ "has-border": list.label && list.label.color }', ":style" => "{ borderTopColor: (list.label && list.label.color ? list.label.color : null) }", "@click" => "toggleExpanded($event)" }
@@ -7,14 +7,22 @@
":class": "{ \"fa-caret-down\": list.isExpanded, \"fa-caret-right\": !list.isExpanded }",
"aria-hidden": "true" }
+ %a.user-avatar-link.js-no-trigger{ "v-if": "list.type === \"assignee\"", ":href": "list.assignee.path" }
+ -# haml-lint:disable AltText
+ %img.avatar.s20.has-tooltip{ height: "20", width: "20", ":src": "list.assignee.avatar", ":alt": "list.assignee.name" }
+
%span.board-title-text.has-tooltip{ "v-if": "list.type !== \"label\"",
- ":title" => '(list.label ? list.label.description : "")', data: { container: "body" } }
+ ":title" => '((list.label && list.label.description) || list.title || "")', data: { container: "body" } }
{{ list.title }}
+ %span.board-title-sub-text.prepend-left-5.has-tooltip{ "v-if": "list.type === \"assignee\"",
+ ":title" => '(list.assignee && list.assignee.username || "")' }
+ @{{ list.assignee.username }}
+
%span.has-tooltip{ "v-if": "list.type === \"label\"",
":title" => '(list.label ? list.label.description : "")',
data: { container: "body", placement: "bottom" },
- class: "label color-label title board-title-text",
+ class: "badge color-label title board-title-text",
":style" => "{ backgroundColor: (list.label && list.label.color ? list.label.color : null), color: (list.label && list.label.textColor ? list.label.textColor : \"#2e2e2e\") }" }
{{ list.title }}
@@ -22,21 +30,20 @@
%board-delete{ "inline-template" => true,
":list" => "list",
"v-if" => "!list.preset && list.id" }
- %button.board-delete.has-tooltip.pull-right{ type: "button", title: "Delete list", "aria-label" => "Delete list", data: { placement: "bottom" }, "@click.stop" => "deleteBoard" }
+ %button.board-delete.has-tooltip.float-right{ type: "button", title: _("Delete list"), "aria-label" => _("Delete list"), data: { placement: "bottom" }, "@click.stop" => "deleteBoard" }
= icon("trash")
- .issue-count-badge.clearfix{ "v-if" => 'list.type !== "blank"' }
- %span.issue-count-badge-count.pull-left{ ":class" => '{ "has-btn": list.type !== "closed" && !disabled }' }
+ .issue-count-badge.clearfix{ "v-if" => 'list.type !== "blank" && list.type !== "promotion"' }
+ %span.issue-count-badge-count.float-left{ ":class" => '{ "has-btn": list.type !== "closed" && !disabled }' }
{{ list.issuesSize }}
- if can?(current_user, :admin_list, current_board_parent)
%button.issue-count-badge-add-button.btn.btn-sm.btn-default.has-tooltip.js-no-trigger-collapse{ type: "button",
"@click" => "showNewIssueForm",
"v-if" => 'list.type !== "closed"',
- "aria-label" => "New issue",
- "title" => "New issue",
+ "aria-label" => _("New issue"),
+ "title" => _("New issue"),
data: { placement: "top", container: "body" } }
= icon("plus", class: "js-no-trigger-collapse")
-
- %board-list{ "v-if" => 'list.type !== "blank"',
+ %board-list{ "v-if" => 'list.type !== "blank" && list.type !== "promotion"',
":list" => "list",
":issues" => "list.issues",
":loading" => "list.loading",
@@ -47,3 +54,4 @@
"ref" => "board-list" }
- if can?(current_user, :admin_list, current_board_parent)
%board-blank-state{ "v-if" => 'list.id == "blank"' }
+ = render_if_exists 'shared/boards/board_promotion_state'
diff --git a/app/views/shared/boards/components/_sidebar.html.haml b/app/views/shared/boards/components/_sidebar.html.haml
index b385cc3f962..1ff956649ed 100644
--- a/app/views/shared/boards/components/_sidebar.html.haml
+++ b/app/views/shared/boards/components/_sidebar.html.haml
@@ -3,23 +3,26 @@
%aside.right-sidebar.right-sidebar-expanded.issue-boards-sidebar{ "v-show" => "showSidebar" }
.issuable-sidebar
.block.issuable-sidebar-header
- %span.issuable-header-text.hide-collapsed.pull-left
+ %span.issuable-header-text.hide-collapsed.float-left
%strong
{{ issue.title }}
%br/
%span
+ = render_if_exists "shared/boards/components/sidebar/issue_project_path"
= precede "#" do
{{ issue.iid }}
- %a.gutter-toggle.pull-right{ role: "button",
+ %a.gutter-toggle.float-right{ role: "button",
href: "#",
"@click.prevent" => "closeSidebar",
"aria-label" => "Toggle sidebar" }
= custom_icon("icon_close", size: 15)
.js-issuable-update
= render "shared/boards/components/sidebar/assignee"
+ = render_if_exists "shared/boards/components/sidebar/epic"
= render "shared/boards/components/sidebar/milestone"
= render "shared/boards/components/sidebar/due_date"
= render "shared/boards/components/sidebar/labels"
+ = render_if_exists "shared/boards/components/sidebar/weight"
= render "shared/boards/components/sidebar/notifications"
%remove-btn{ ":issue" => "issue",
":issue-update" => "issue.sidebarInfoEndpoint",
diff --git a/app/views/shared/boards/components/sidebar/_due_date.html.haml b/app/views/shared/boards/components/sidebar/_due_date.html.haml
index d13b998e6f4..5630375f428 100644
--- a/app/views/shared/boards/components/sidebar/_due_date.html.haml
+++ b/app/views/shared/boards/components/sidebar/_due_date.html.haml
@@ -1,20 +1,20 @@
.block.due_date
.title
- Due date
+ = _("Due date")
- if can_admin_issue?
= icon("spinner spin", class: "block-loading")
- = link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right"
+ = link_to _("Edit"), "#", class: "js-sidebar-dropdown-toggle edit-link float-right"
.value
.value-content
%span.no-value{ "v-if" => "!issue.dueDate" }
- No due date
+ = _("No due date")
%span.bold{ "v-if" => "issue.dueDate" }
{{ issue.dueDate | due-date }}
- if can_admin_issue?
%span.no-value.js-remove-due-date-holder{ "v-if" => "issue.dueDate" }
\-
%a.js-remove-due-date{ href: "#", role: "button" }
- remove due date
+ = _('remove due date')
- if can_admin_issue?
.selectbox
%input{ type: "hidden",
@@ -23,9 +23,9 @@
.dropdown
%button.dropdown-menu-toggle.js-due-date-select.js-issue-boards-due-date{ type: 'button',
data: { toggle: 'dropdown', field_name: "issue[due_date]", ability_name: "issue" } }
- %span.dropdown-toggle-text Due date
+ %span.dropdown-toggle-text= _("Due date")
= icon('chevron-down')
.dropdown-menu.dropdown-menu-due-date
- = dropdown_title('Due date')
+ = dropdown_title(_('Due date'))
= dropdown_content do
.js-due-date-calendar
diff --git a/app/views/shared/boards/components/sidebar/_labels.html.haml b/app/views/shared/boards/components/sidebar/_labels.html.haml
index 1c73534c642..607e7f471c9 100644
--- a/app/views/shared/boards/components/sidebar/_labels.html.haml
+++ b/app/views/shared/boards/components/sidebar/_labels.html.haml
@@ -1,15 +1,15 @@
.block.labels
.title
- Labels
+ = _("Labels")
- if can_admin_issue?
= icon("spinner spin", class: "block-loading")
- = link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right"
+ = link_to _("Edit"), "#", class: "js-sidebar-dropdown-toggle edit-link float-right"
.value.issuable-show-labels.dont-hide
%span.no-value{ "v-if" => "issue.labels && issue.labels.length === 0" }
- None
+ = _("None")
%a{ href: "#",
"v-for" => "label in issue.labels" }
- %span.label.color-label.has-tooltip{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" }
+ .badge.color-label.has-tooltip{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" }
{{ label.title }}
- if can_admin_issue?
.selectbox
@@ -28,7 +28,7 @@
namespace_path: @namespace_path,
project_path: @project.try(:path) } }
%span.dropdown-toggle-text
- Label
+ = _("Label")
= icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
= render partial: "shared/issuable/label_page_default"
diff --git a/app/views/shared/boards/components/sidebar/_milestone.html.haml b/app/views/shared/boards/components/sidebar/_milestone.html.haml
index f51c4a97f2b..b15d60002fc 100644
--- a/app/views/shared/boards/components/sidebar/_milestone.html.haml
+++ b/app/views/shared/boards/components/sidebar/_milestone.html.haml
@@ -1,12 +1,12 @@
.block.milestone
.title
- Milestone
+ = _("Milestone")
- if can_admin_issue?
= icon("spinner spin", class: "block-loading")
- = link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right"
+ = link_to _("Edit"), "#", class: "js-sidebar-dropdown-toggle edit-link float-right"
.value
%span.no-value{ "v-if" => "!issue.milestone" }
- None
+ = _("None")
%span.bold.has-tooltip{ "v-if" => "issue.milestone" }
{{ issue.milestone.title }}
- if can_admin_issue?
@@ -19,10 +19,10 @@
%button.dropdown-menu-toggle.js-milestone-select.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", show_no: "true", field_name: "issue[milestone_id]", milestones: milestones_filter_path(format: :json), ability_name: "issue", use_id: "true", default_no: "true" },
":data-selected" => "milestoneTitle",
":data-issuable-id" => "issue.iid" }
- Milestone
+ = _("Milestone")
= icon("chevron-down")
.dropdown-menu.dropdown-select.dropdown-menu-selectable
- = dropdown_title("Assign milestone")
- = dropdown_filter("Search milestones")
+ = dropdown_title(_("Assign milestone"))
+ = dropdown_filter(_("Search milestones"))
= dropdown_content
= dropdown_loading
diff --git a/app/views/shared/builds/_build_output.html.haml b/app/views/shared/builds/_build_output.html.haml
new file mode 100644
index 00000000000..0e18128a8f1
--- /dev/null
+++ b/app/views/shared/builds/_build_output.html.haml
@@ -0,0 +1,3 @@
+%pre.build-trace#build-trace
+ %code.bash.js-build-output
+ .build-loader-animation.js-build-refresh
diff --git a/app/views/shared/builds/_tabs.html.haml b/app/views/shared/builds/_tabs.html.haml
index 0b003125912..5c74e71b644 100644
--- a/app/views/shared/builds/_tabs.html.haml
+++ b/app/views/shared/builds/_tabs.html.haml
@@ -1,24 +1,24 @@
-%ul.nav-links.mobile-separator
+%ul.nav-links.mobile-separator.nav.nav-tabs
%li{ class: active_when(scope.nil?) }>
= link_to build_path_proc.call(nil) do
All
- %span.badge.js-totalbuilds-count
+ %span.badge.badge-pill.js-totalbuilds-count
= limited_counter_with_delimiter(all_builds)
%li{ class: active_when(scope == 'pending') }>
= link_to build_path_proc.call('pending') do
Pending
- %span.badge
+ %span.badge.badge-pill
= limited_counter_with_delimiter(all_builds.pending)
%li{ class: active_when(scope == 'running') }>
= link_to build_path_proc.call('running') do
Running
- %span.badge
+ %span.badge.badge-pill
= limited_counter_with_delimiter(all_builds.running)
%li{ class: active_when(scope == 'finished') }>
= link_to build_path_proc.call('finished') do
Finished
- %span.badge
+ %span.badge.badge-pill
= limited_counter_with_delimiter(all_builds.finished)
diff --git a/app/views/shared/dashboard/_no_filter_selected.html.haml b/app/views/shared/dashboard/_no_filter_selected.html.haml
index b2e6967f6aa..32246dac4c7 100644
--- a/app/views/shared/dashboard/_no_filter_selected.html.haml
+++ b/app/views/shared/dashboard/_no_filter_selected.html.haml
@@ -1,8 +1,8 @@
.row.empty-state.text-center
- .col-xs-12
+ .col-12
.svg-130.prepend-top-default
= image_tag 'illustrations/issue-dashboard_results-without-filter.svg'
- .col-xs-12
+ .col-12
.text-content
%h4
= _("Please select at least one filter to see results")
diff --git a/app/views/shared/deploy_keys/_form.html.haml b/app/views/shared/deploy_keys/_form.html.haml
index 87c2965bb21..913c065e188 100644
--- a/app/views/shared/deploy_keys/_form.html.haml
+++ b/app/views/shared/deploy_keys/_form.html.haml
@@ -5,26 +5,26 @@
= form_errors(deploy_key)
.form-group
- = form.label :title, class: 'control-label'
+ = form.label :title, class: 'col-form-label col-sm-2'
.col-sm-10= form.text_field :title, class: 'form-control'
.form-group
- if deploy_key.new_record?
- = form.label :key, class: 'control-label'
+ = form.label :key, class: 'col-form-label col-sm-2'
.col-sm-10
%p.light
Paste a machine public key here. Read more about how to generate it
= link_to 'here', help_page_path('ssh/README')
= form.text_area :key, class: 'form-control thin_area', rows: 5
- else
- = form.label :fingerprint, class: 'control-label'
+ = form.label :fingerprint, class: 'col-form-label col-sm-2'
.col-sm-10
= form.text_field :fingerprint, class: 'form-control', readonly: 'readonly'
- if deploy_keys_project.present?
= form.fields_for :deploy_keys_projects, deploy_keys_project do |deploy_keys_project_form|
.form-group
- .control-label
+ .col-form-label.col-sm-2
.col-sm-10
= deploy_keys_project_form.label :can_push do
= deploy_keys_project_form.check_box :can_push
diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml
index 62437f5fc9d..2e26fe63d3e 100644
--- a/app/views/shared/empty_states/_issues.html.haml
+++ b/app/views/shared/empty_states/_issues.html.haml
@@ -3,10 +3,10 @@
- has_button = button_path || project_select_button
.row.empty-state
- .col-xs-12
+ .col-12
.svg-content
= image_tag 'illustrations/issues.svg'
- .col-xs-12
+ .col-12
.text-content
- if current_user
%h4
@@ -16,7 +16,7 @@
- if has_button
.text-center
- if project_select_button
- = render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue', type: :issues
+ = render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue', type: :issues, with_feature_enabled: 'issues'
- else
= link_to 'New issue', button_path, class: 'btn btn-success', title: 'New issue', id: 'new_issue_link'
- else
diff --git a/app/views/shared/empty_states/_labels.html.haml b/app/views/shared/empty_states/_labels.html.haml
index 04db9de3606..e8749ee3956 100644
--- a/app/views/shared/empty_states/_labels.html.haml
+++ b/app/views/shared/empty_states/_labels.html.haml
@@ -1,8 +1,8 @@
.row.empty-state.labels
- .col-xs-12
+ .col-12
.svg-content
= image_tag 'illustrations/labels.svg'
- .col-xs-12
+ .col-12
.text-content
%h4= _("Labels can be applied to issues and merge requests to categorize them.")
%p= _("You can also star a label to make it a priority label.")
diff --git a/app/views/shared/empty_states/_merge_requests.html.haml b/app/views/shared/empty_states/_merge_requests.html.haml
index 2edf3557df4..186139f3526 100644
--- a/app/views/shared/empty_states/_merge_requests.html.haml
+++ b/app/views/shared/empty_states/_merge_requests.html.haml
@@ -3,10 +3,10 @@
- has_button = button_path || project_select_button
.row.empty-state.merge-requests
- .col-xs-12
+ .col-12
.svg-content
= image_tag 'illustrations/merge_requests.svg'
- .col-xs-12
+ .col-12
.text-content
- if has_button
%h4
@@ -15,7 +15,7 @@
= _("Interested parties can even contribute by pushing commits if they want to.")
.text-center
- if project_select_button
- = render 'shared/new_project_item_select', path: 'merge_requests/new', label: _('New merge request'), type: :merge_requests
+ = render 'shared/new_project_item_select', path: 'merge_requests/new', label: _('New merge request'), type: :merge_requests, with_feature_enabled: 'merge_requests'
- else
= link_to _('New merge request'), button_path, class: 'btn btn-new', title: _('New merge request'), id: 'new_merge_request_link'
- else
diff --git a/app/views/shared/empty_states/_wikis.html.haml b/app/views/shared/empty_states/_wikis.html.haml
new file mode 100644
index 00000000000..f1a41074c28
--- /dev/null
+++ b/app/views/shared/empty_states/_wikis.html.haml
@@ -0,0 +1,30 @@
+- layout_path = 'shared/empty_states/wikis_layout'
+
+- if can?(current_user, :create_wiki, @project)
+ - create_path = project_wiki_path(@project, params[:id], { view: 'create' })
+ - create_link = link_to s_('WikiEmpty|Create your first page'), create_path, class: 'btn btn-new', title: s_('WikiEmpty|Create your first page')
+
+ = render layout: layout_path, locals: { image_path: 'illustrations/wiki_login_empty.svg' } do
+ %h4
+ = s_('WikiEmpty|The wiki lets you write documentation for your project')
+ %p.text-left
+ = s_("WikiEmpty|A wiki is where you can store all the details about your project. This can include why you've created it, its principles, how to use it, and so on.")
+ = create_link
+
+- elsif can?(current_user, :read_issue, @project)
+ - issues_link = link_to s_('WikiEmptyIssueMessage|issue tracker'), project_issues_path(@project)
+ - new_issue_link = link_to s_('WikiEmpty|Suggest wiki improvement'), new_project_issue_path(@project), class: 'btn btn-new', title: s_('WikiEmptyIssueMessage|Suggest wiki improvement')
+
+ = render layout: layout_path, locals: { image_path: 'illustrations/wiki_logout_empty.svg' } do
+ %h4
+ = s_('WikiEmpty|This project has no wiki pages')
+ %p.text-left
+ = s_('WikiEmptyIssueMessage|You must be a project member in order to add wiki pages. If you have suggestions for how to improve the wiki for this project, consider opening an issue in the %{issues_link}.').html_safe % { issues_link: issues_link }
+ = new_issue_link
+
+- else
+ = render layout: layout_path, locals: { image_path: 'illustrations/wiki_logout_empty.svg' } do
+ %h4
+ = s_('WikiEmpty|This project has no wiki pages')
+ %p
+ = s_('WikiEmpty|You must be a project member in order to add wiki pages.')
diff --git a/app/views/shared/empty_states/_wikis_layout.html.haml b/app/views/shared/empty_states/_wikis_layout.html.haml
new file mode 100644
index 00000000000..a5f100e3469
--- /dev/null
+++ b/app/views/shared/empty_states/_wikis_layout.html.haml
@@ -0,0 +1,7 @@
+.row.empty-state
+ .col-12
+ .svg-content
+ = image_tag image_path
+ .col-12
+ .text-content.text-center
+ = yield
diff --git a/app/views/shared/form_elements/_description.html.haml b/app/views/shared/form_elements/_description.html.haml
index 38e9899ca4b..25df2fe5cd6 100644
--- a/app/views/shared/form_elements/_description.html.haml
+++ b/app/views/shared/form_elements/_description.html.haml
@@ -9,14 +9,14 @@
- else
- preview_url = preview_markdown_path(project)
-.form-group.detail-page-description
- = form.label :description, 'Description', class: 'control-label'
+.form-group.row.detail-page-description
+ = form.label :description, 'Description', class: 'col-form-label col-sm-2'
.col-sm-10
= render layout: 'projects/md_preview', locals: { url: preview_url, referenced_users: true } do
= render 'projects/zen', f: form, attr: :description,
classes: 'note-textarea qa-issuable-form-description',
- placeholder: "Write a comment or drag your files here...",
+ placeholder: "Write a comment or drag your files here…",
supports_quick_actions: supports_quick_actions
= render 'shared/notes/hints', supports_quick_actions: supports_quick_actions
.clearfix
diff --git a/app/views/shared/groups/_dropdown.html.haml b/app/views/shared/groups/_dropdown.html.haml
index 8607be9cd06..2237b93a10b 100644
--- a/app/views/shared/groups/_dropdown.html.haml
+++ b/app/views/shared/groups/_dropdown.html.haml
@@ -13,7 +13,7 @@
%span.dropdown-label
= options_hash[default_sort_by]
= icon('chevron-down')
- %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
+ %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable
%li.dropdown-header
= _("Sort by")
- options_hash.each do |value, title|
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index 90395600d4e..a1b901aaffa 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -16,7 +16,7 @@
.avatar-container.s40
= link_to group do
- = group_icon(group, class: "avatar s40 hidden-xs")
+ = group_icon(group, class: "avatar s40 d-none d-sm-block")
.title
= link_to group.full_name, group, class: 'group-name'
diff --git a/app/views/shared/hook_logs/_content.html.haml b/app/views/shared/hook_logs/_content.html.haml
index c80b179d525..f3b56df0c96 100644
--- a/app/views/shared/hook_logs/_content.html.haml
+++ b/app/views/shared/hook_logs/_content.html.haml
@@ -6,8 +6,8 @@
%p
%strong Trigger:
- %td.hidden-xs
- %span.label.label-gray.deploy-project-label
+ %td.d-none.d-sm-block
+ %span.badge.badge-gray.deploy-project-label
= hook_log.trigger.singularize.titleize
%p
%strong Elapsed time:
@@ -30,7 +30,7 @@
%h5 Request body:
%pre
- :plain
+ :escaped
#{JSON.pretty_generate(hook_log.request_data)}
%h5 Response headers:
%pre
@@ -40,5 +40,5 @@
%h5 Response body:
%pre
- :plain
+ :escaped
#{hook_log.response_body}
diff --git a/app/views/shared/hook_logs/_status_label.html.haml b/app/views/shared/hook_logs/_status_label.html.haml
index b4ea8e6f952..993880b7d6e 100644
--- a/app/views/shared/hook_logs/_status_label.html.haml
+++ b/app/views/shared/hook_logs/_status_label.html.haml
@@ -1,3 +1,3 @@
-- label_status = hook_log.success? ? 'label-success' : 'label-danger'
+- label_status = hook_log.success? ? 'badge-success' : 'badge-danger'
%span{ class: "label #{label_status}" }
= hook_log.response_status
diff --git a/app/views/shared/icons/_icon_calendar.svg b/app/views/shared/icons/_icon_calendar.svg
new file mode 100644
index 00000000000..4d0a703f9a0
--- /dev/null
+++ b/app/views/shared/icons/_icon_calendar.svg
@@ -0,0 +1 @@
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M15 5v7a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V5a2 2 0 0 1 2-2h1V2a1 1 0 1 1 2 0v1h4V2a1 1 0 1 1 2 0v1h1a2 2 0 0 1 2 2zM3 6v6a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V6H3zm2 2h2a1 1 0 1 1 0 2H5a1 1 0 1 1 0-2z" fill="#000" fill-rule="evenodd"/></svg> \ No newline at end of file
diff --git a/app/views/shared/issuable/_board_create_list_dropdown.html.haml b/app/views/shared/issuable/_board_create_list_dropdown.html.haml
new file mode 100644
index 00000000000..23b2e1b91e5
--- /dev/null
+++ b/app/views/shared/issuable/_board_create_list_dropdown.html.haml
@@ -0,0 +1,8 @@
+.dropdown.prepend-left-10#js-add-list
+ %button.btn.btn-create.btn-inverted.js-new-board-list{ type: "button", data: board_list_data }
+ Add list
+ .dropdown-menu.dropdown-menu-paging.dropdown-menu-right.dropdown-menu-issues-board-new.dropdown-menu-selectable.js-tab-container-labels
+ = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" }
+ - if can?(current_user, :admin_label, board.parent)
+ = render partial: "shared/issuable/label_page_create"
+ = dropdown_loading
diff --git a/app/views/shared/issuable/_bulk_update_sidebar.html.haml b/app/views/shared/issuable/_bulk_update_sidebar.html.haml
index 0d507cc7a6e..ca02424215c 100644
--- a/app/views/shared/issuable/_bulk_update_sidebar.html.haml
+++ b/app/views/shared/issuable/_bulk_update_sidebar.html.haml
@@ -4,9 +4,9 @@
.issuable-sidebar.hidden
= form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: "bulk-update" do
.block.issuable-sidebar-header
- .filter-item.inline.update-issues-btn.pull-left
+ .filter-item.inline.update-issues-btn.float-left
= button_tag "Update all", class: "btn update-selected-issues btn-info", disabled: true
- = button_tag "Cancel", class: "btn btn-default js-bulk-update-menu-hide pull-right"
+ = button_tag "Cancel", class: "btn btn-default js-bulk-update-menu-hide float-right"
.block
.title
Status
diff --git a/app/views/shared/issuable/_close_reopen_button.html.haml b/app/views/shared/issuable/_close_reopen_button.html.haml
index 9ef015047c9..933d4b2ea65 100644
--- a/app/views/shared/issuable/_close_reopen_button.html.haml
+++ b/app/views/shared/issuable/_close_reopen_button.html.haml
@@ -4,11 +4,11 @@
- if can_update && is_current_user
= link_to "Close #{display_issuable_type}", close_issuable_path(issuable), method: button_method,
- class: "hidden-xs hidden-sm btn btn-grouped btn-close js-btn-issue-action #{issuable_button_visibility(issuable, true)}", title: "Close #{display_issuable_type}"
+ class: "d-none d-sm-none d-md-block btn btn-grouped btn-close js-btn-issue-action #{issuable_button_visibility(issuable, true)}", title: "Close #{display_issuable_type}"
= link_to "Reopen #{display_issuable_type}", reopen_issuable_path(issuable), method: button_method,
- class: "hidden-xs hidden-sm btn btn-grouped btn-reopen js-btn-issue-action #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}"
+ class: "d-none d-sm-none d-md-block btn btn-grouped btn-reopen js-btn-issue-action #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}"
- elsif can_update && !is_current_user
= render 'shared/issuable/close_reopen_report_toggle', issuable: issuable
- else
= link_to 'Report abuse', new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)),
- class: 'hidden-xs hidden-sm btn btn-grouped btn-close-color', title: 'Report abuse'
+ class: 'd-none d-sm-none d-md-block btn btn-grouped btn-close-color', title: 'Report abuse'
diff --git a/app/views/shared/issuable/_close_reopen_report_toggle.html.haml b/app/views/shared/issuable/_close_reopen_report_toggle.html.haml
index 39a5171c1d6..0d59c9304b4 100644
--- a/app/views/shared/issuable/_close_reopen_report_toggle.html.haml
+++ b/app/views/shared/issuable/_close_reopen_report_toggle.html.haml
@@ -1,12 +1,12 @@
- display_issuable_type = issuable_display_type(issuable)
- button_action = issuable.closed? ? 'reopen' : 'close'
- display_button_action = button_action.capitalize
-- button_responsive_class = 'hidden-xs hidden-sm'
+- button_responsive_class = 'd-none d-sm-none d-md-block'
- button_class = "#{button_responsive_class} btn btn-grouped js-issuable-close-button js-btn-issue-action issuable-close-button"
- toggle_class = "#{button_responsive_class} btn btn-nr dropdown-toggle js-issuable-close-toggle"
- button_method = issuable_close_reopen_button_method(issuable)
-.pull-left.btn-group.prepend-left-10.issuable-close-dropdown.droplab-dropdown.js-issuable-close-dropdown
+.float-left.btn-group.prepend-left-10.issuable-close-dropdown.droplab-dropdown.js-issuable-close-dropdown
= link_to "#{display_button_action} #{display_issuable_type}", close_reopen_issuable_path(issuable),
method: button_method, class: "#{button_class} btn-#{button_action}", title: "#{display_button_action} #{display_issuable_type}"
@@ -14,7 +14,7 @@
data: { 'dropdown-trigger' => '#issuable-close-menu' }, 'aria-label' => 'Toggle dropdown' do
= icon('caret-down', class: 'toggle-icon icon')
- %ul#issuable-close-menu.js-issuable-close-menu.dropdown-menu{ class: button_responsive_class, data: { dropdown: true } }
+ %ul#issuable-close-menu.js-issuable-close-menu.dropdown-menu{ data: { dropdown: true } }
%li.close-item{ class: "#{issuable_button_visibility(issuable, true) || 'droplab-item-selected'}",
data: { text: "Close #{display_issuable_type}", url: close_issuable_path(issuable),
button_class: "#{button_class} btn-close", toggle_class: "#{toggle_class} btn-close-color", method: button_method } }
diff --git a/app/views/shared/issuable/_feed_buttons.html.haml b/app/views/shared/issuable/_feed_buttons.html.haml
new file mode 100644
index 00000000000..d4834090413
--- /dev/null
+++ b/app/views/shared/issuable/_feed_buttons.html.haml
@@ -0,0 +1,4 @@
+= link_to safe_params.merge(rss_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: 'Subscribe to RSS feed' do
+ = icon('rss')
+= link_to safe_params.merge(calendar_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: 'Subscribe to calendar' do
+ = custom_icon('icon_calendar')
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index 1bd5b4164b1..1cd8ce0826c 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -25,7 +25,7 @@
= render "shared/issuable/label_dropdown", selected: selected_labels, use_id: false, selected_toggle: params[:label_name], data_options: { field_name: "label_name[]" }
- unless @no_filters_set
- .pull-right
+ .float-right
= render 'shared/sort_dropdown'
- has_labels = @labels && @labels.any?
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index 4c8f03f1498..b49e47a7266 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -11,8 +11,8 @@
= link_to "the #{issuable.class.model_name.human.downcase}", polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), target: "_blank", rel: 'noopener noreferrer'
and make sure your changes will not unintentionally remove theirs
-.form-group
- = form.label :title, class: 'control-label'
+.form-group.row
+ = form.label :title, class: 'col-form-label col-sm-2'
= render 'shared/issuable/form/template_selector', issuable: issuable
= render 'shared/issuable/form/title', issuable: issuable, form: form, has_wip_commits: commits && commits.detect(&:work_in_progress?)
@@ -20,15 +20,17 @@
= render 'shared/form_elements/description', model: issuable, form: form, project: project
- if issuable.respond_to?(:confidential)
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = form.label :confidential do
- = form.check_box :confidential
+ .form-group.row
+ .offset-sm-2.col-sm-10
+ .form-check
+ = form.check_box :confidential, class: 'form-check-input'
+ = form.label :confidential, class: 'form-check-label' do
This issue is confidential and should only be visible to team members with at least Reporter access.
= render 'shared/issuable/form/metadata', issuable: issuable, form: form
+= render_if_exists 'shared/issuable/approvals', issuable: issuable, form: form
+
= render 'shared/issuable/form/branch_chooser', issuable: issuable, form: form
= render 'shared/issuable/form/merge_params', issuable: issuable
@@ -36,8 +38,8 @@
= render 'shared/issuable/form/contribution', issuable: issuable, form: form
- if @merge_request_to_resolve_discussions_of
- .form-group
- .col-sm-10.col-sm-offset-2
+ .form-group.row
+ .col-sm-10.offset-sm-2
= icon('info-circle')
- if @merge_request_to_resolve_discussions_of.discussions_can_be_resolved_by?(current_user)
= hidden_field_tag 'merge_request_to_resolve_discussions_of', @merge_request_to_resolve_discussions_of.iid
@@ -57,7 +59,7 @@
- is_footer = !(issuable.is_a?(MergeRequest) && issuable.new_record?)
.row-content-block{ class: (is_footer ? "footer-block" : "middle-block") }
- .pull-right
+ .float-right
- if issuable.new_record?
= link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class]), class: 'btn btn-cancel'
- else
@@ -77,5 +79,6 @@
%strong= link_to('contribution guidelines', guide_url)
for this project.
+= render_if_exists 'shared/issuable/remove_approver'
= form.hidden_field :lock_version
diff --git a/app/views/shared/issuable/_label_page_create.html.haml b/app/views/shared/issuable/_label_page_create.html.haml
index 91aa329eb93..55edaa7eda4 100644
--- a/app/views/shared/issuable/_label_page_create.html.haml
+++ b/app/views/shared/issuable/_label_page_create.html.haml
@@ -1,6 +1,7 @@
+- show_close = local_assigns.fetch(:show_close, true)
- subject = @project || @group
.dropdown-page-two.dropdown-new-label
- = dropdown_title(create_label_title(subject), options: { back: true })
+ = dropdown_title(create_label_title(subject), options: { back: true, close: show_close })
= dropdown_content do
.dropdown-labels-error.js-label-error
%input#new_label_name.default-dropdown-input{ type: "text", placeholder: _('Name new label') }
@@ -12,7 +13,7 @@
.dropdown-label-color-preview.js-dropdown-label-color-preview
%input#new_label_color.default-dropdown-input{ type: "text", placeholder: _('Assign custom color like #FF0000') }
.clearfix
- %button.btn.btn-primary.pull-left.js-new-label-btn{ type: "button" }
+ %button.btn.btn-primary.float-left.js-new-label-btn{ type: "button" }
= _('Create')
- %button.btn.btn-default.pull-right.js-cancel-label-btn{ type: "button" }
+ %button.btn.btn-default.float-right.js-cancel-label-btn{ type: "button" }
= _('Cancel')
diff --git a/app/views/shared/issuable/_label_page_default.html.haml b/app/views/shared/issuable/_label_page_default.html.haml
index 2bd922bca2b..aa4a5f0e0d3 100644
--- a/app/views/shared/issuable/_label_page_default.html.haml
+++ b/app/views/shared/issuable/_label_page_default.html.haml
@@ -1,15 +1,18 @@
- title = local_assigns.fetch(:title, _('Assign labels'))
+- content_title = local_assigns.fetch(:content_title, _('Create lists from labels. Issues with that label appear in that list.'))
+- show_title = local_assigns.fetch(:show_title, true)
- show_create = local_assigns.fetch(:show_create, true)
- show_footer = local_assigns.fetch(:show_footer, true)
- filter_placeholder = local_assigns.fetch(:filter_placeholder, 'Search')
- show_boards_content = local_assigns.fetch(:show_boards_content, false)
- subject = @project || @group
.dropdown-page-one
- = dropdown_title(title)
+ - if show_title
+ = dropdown_title(title)
- if show_boards_content
.issue-board-dropdown-content
%p
- = _('Create lists from labels. Issues with that label appear in that list.')
+ = content_title
= dropdown_filter(filter_placeholder)
= dropdown_content
- if current_board_parent && show_footer
diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml
index 955b8866c2c..c2da363b8c6 100644
--- a/app/views/shared/issuable/_milestone_dropdown.html.haml
+++ b/app/views/shared/issuable/_milestone_dropdown.html.haml
@@ -1,11 +1,13 @@
- project = @target_project || @project
- extra_class = extra_class || ''
- show_menu_above = show_menu_above || false
+- selected = local_assigns.fetch(:selected, nil)
+
- selected_text = selected.try(:title) || params[:milestone_title]
- dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by milestone")
- if selected.present? || params[:milestone_title].present?
= hidden_field_tag(name, name == :milestone_title ? selected_text : selected.id)
-= dropdown_tag(milestone_dropdown_label(selected_text), options: { title: dropdown_title, toggle_class: "js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone",
+= dropdown_tag(milestone_dropdown_label(selected_text), options: { title: dropdown_title, toggle_class: "qa-issuable-milestone-dropdown js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "qa-issuable-dropdown-menu-milestone dropdown-menu-selectable dropdown-menu-milestone",
placeholder: "Search milestones", footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, show_started: show_started, field_name: name, selected: selected_text, project_id: project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do
- if project
%ul.dropdown-footer-list
diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml
index a5f40ea934b..157637dbd11 100644
--- a/app/views/shared/issuable/_nav.html.haml
+++ b/app/views/shared/issuable/_nav.html.haml
@@ -2,7 +2,7 @@
- page_context_word = type.to_s.humanize(capitalize: false)
- display_count = local_assigns.fetch(:display_count, :true)
-%ul.nav-links.issues-state-filters.mobile-separator
+%ul.nav-links.issues-state-filters.mobile-separator.nav.nav-tabs
%li{ class: active_when(params[:state] == 'opened') }>
= link_to page_filter_path(state: 'opened', label: true), id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened.", data: { state: 'opened' } do
#{issuables_state_counter_text(type, :opened, display_count)}
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index fc6f71ef60f..9ce7f6fe269 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -1,9 +1,14 @@
- type = local_assigns.fetch(:type)
+- board = local_assigns.fetch(:board, nil)
- block_css_class = type != :boards_modal ? 'row-content-block second-block' : ''
- full_path = @project.present? ? @project.full_path : @group.full_path
+- user_can_admin_list = board && can?(current_user, :admin_list, board.parent)
.issues-filters
.issues-details-filters.filtered-search-block{ class: block_css_class, "v-pre" => type == :boards_modal }
+ - if type == :boards
+ #js-multiple-boards-switcher.inline.boards-switcher{ "v-cloak" => true }
+ = render_if_exists "shared/boards/switcher", board: board
= form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search]), method: :get, class: 'filter-form js-filter-form' do
- if params[:search].present?
= hidden_field_tag :search, params[:search]
@@ -99,20 +104,18 @@
%gl-emoji
%span.js-data-value.prepend-left-10
{{name}}
+
+ = render_if_exists 'shared/issuable/filter_weight', type: type
+
%button.clear-search.hidden{ type: 'button' }
= icon('times')
.filter-dropdown-container
- if type == :boards
- - if can?(current_user, :admin_list, board.parent)
- .dropdown.prepend-left-10#js-add-list
- %button.btn.btn-create.btn-inverted.js-new-board-list{ type: "button", data: board_list_data }
- Add list
- .dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable
- = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" }
- - if can?(current_user, :admin_label, board.parent)
- = render partial: "shared/issuable/label_page_create"
- = dropdown_loading
+ .js-board-config{ data: { can_admin_list: user_can_admin_list, has_scope: board.scoped? } }
+ - if user_can_admin_list
+ = render 'shared/issuable/board_create_list_dropdown', board: board
- if @project
#js-add-issues-btn.prepend-left-10{ data: { can_admin_list: can?(current_user, :admin_list, @project) } }
+ #js-toggle-focus-btn
- elsif type != :boards_modal
= render 'shared/sort_dropdown'
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 093033775a9..0ca35ea1298 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -5,9 +5,9 @@
- can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
.block.issuable-sidebar-header
- if current_user
- %span.issuable-header-text.hide-collapsed.pull-left
+ %span.issuable-header-text.hide-collapsed.float-left
= _('Todo')
- %a.gutter-toggle.pull-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => "Toggle sidebar", title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left' } }
+ %a.gutter-toggle.float-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => "Toggle sidebar", title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left', boundary: 'viewport' } }
= sidebar_gutter_toggle_icon
- if current_user
= render "shared/issuable/sidebar_todo", todo: todo, issuable: issuable
@@ -18,8 +18,11 @@
= render "shared/issuable/sidebar_todo", todo: todo, issuable: issuable, is_collapsed: true
.block.assignee
= render "shared/issuable/sidebar_assignees", issuable: issuable, can_edit_issuable: can_edit_issuable, signed_in: current_user.present?
+
+ = render_if_exists 'shared/issuable/sidebar_item_epic', issuable: issuable
+
.block.milestone
- .sidebar-collapsed-icon.has-tooltip{ title: milestone_tooltip_title(issuable.milestone), data: { container: 'body', html: 1, placement: 'left' } }
+ .sidebar-collapsed-icon.has-tooltip{ title: milestone_tooltip_title(issuable.milestone), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } }
= icon('clock-o', 'aria-hidden': 'true')
%span.milestone-title
- if issuable.milestone
@@ -30,17 +33,17 @@
= _('Milestone')
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
- = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
+ = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right'
.value.hide-collapsed
- if issuable.milestone
- = link_to issuable.milestone.title, milestone_path(issuable.milestone), class: "bold has-tooltip", title: milestone_tooltip_due_date(issuable.milestone), data: { container: "body", html: 1 }
+ = link_to issuable.milestone.title, milestone_path(issuable.milestone), class: "bold has-tooltip", title: milestone_tooltip_due_date(issuable.milestone), data: { container: "body", html: 'true', boundary: 'viewport' }
- else
%span.no-value
= _('None')
.selectbox.hide-collapsed
= f.hidden_field 'milestone_id', value: issuable.milestone_id, id: nil
- = dropdown_tag('Milestone', options: { title: _('Assign milestone'), toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: _('Search milestones'), data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: project_milestones_path(@project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), use_id: true, default_no: true, selected: (issuable.milestone.name if issuable.milestone), null_default: true }})
+ = dropdown_tag('Milestone', options: { title: _('Assign milestone'), toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: _('Search milestones'), data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: project_milestones_path(@project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), use_id: true, default_no: true, selected: (issuable.milestone.name if issuable.milestone), null_default: true, display: 'static' }})
- if issuable.has_attribute?(:time_estimate)
#issuable-time-tracker.block
// Fallback while content is loading
@@ -49,7 +52,7 @@
= icon('spinner spin', 'aria-hidden': 'true')
- if issuable.has_attribute?(:due_date)
.block.due_date
- .sidebar-collapsed-icon.has-tooltip{ data: { placement: 'left', container: 'body', html: 1 }, title: sidebar_due_date_tooltip_label(issuable) }
+ .sidebar-collapsed-icon.has-tooltip{ data: { placement: 'left', container: 'body', html: 'true', boundary: 'viewport' }, title: sidebar_due_date_tooltip_label(issuable) }
= icon('calendar', 'aria-hidden': 'true')
%span.js-due-date-sidebar-value
= issuable.due_date.try(:to_s, :medium) || 'None'
@@ -57,7 +60,7 @@
= _('Due date')
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
- = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
+ = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right'
.value.hide-collapsed
%span.value-content
- if issuable.due_date
@@ -74,7 +77,7 @@
.selectbox.hide-collapsed
= f.hidden_field :due_date, value: issuable.due_date.try(:strftime, 'yy-mm-dd')
.dropdown
- %button.dropdown-menu-toggle.js-due-date-select{ type: 'button', data: { toggle: 'dropdown', field_name: "#{issuable.to_ability_name}[due_date]", ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable) } }
+ %button.dropdown-menu-toggle.js-due-date-select{ type: 'button', data: { toggle: 'dropdown', field_name: "#{issuable.to_ability_name}[due_date]", ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), display: 'static' } }
%span.dropdown-toggle-text
= _('Due date')
= icon('chevron-down', 'aria-hidden': 'true')
@@ -86,7 +89,7 @@
- if @labels
- selected_labels = issuable.labels
.block.labels
- .sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(issuable.labels_array), data: { placement: "left", container: "body" } }
+ .sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(issuable.labels_array), data: { placement: "left", container: "body", boundary: 'viewport' } }
= icon('tags', 'aria-hidden': 'true')
%span
= selected_labels.size
@@ -94,7 +97,7 @@
= _('Labels')
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
- = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
+ = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right'
.value.issuable-show-labels.dont-hide.hide-collapsed{ class: ("has-labels" if selected_labels.any?) }
- if selected_labels.any?
- selected_labels.each do |label|
@@ -106,7 +109,7 @@
- selected_labels.each do |label|
= hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil
.dropdown
- %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (labels_filter_path(false) if @project) } }
+ %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (labels_filter_path(false) if @project), display: 'static' } }
%span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) }
= multi_label_name(selected_labels, "Labels")
= icon('chevron-down', 'aria-hidden': 'true')
@@ -115,6 +118,8 @@
- if can? current_user, :admin_label, @project and @project
= render partial: "shared/issuable/label_page_create"
+ = render_if_exists 'shared/issuable/sidebar_weight', issuable: issuable
+
- if issuable.has_attribute?(:confidential)
-# haml-lint:disable InlineJavaScript
%script#js-confidential-issue-data{ type: "application/json" }= { is_confidential: @issue.confidential, is_editable: can_edit_issuable }.to_json.html_safe
@@ -133,20 +138,20 @@
- project_ref = cross_project_reference(@project, issuable)
.block.project-reference
.sidebar-collapsed-icon.dont-change-state
- = clipboard_button(text: project_ref, title: _('Copy reference to clipboard'), placement: "left")
+ = clipboard_button(text: project_ref, title: _('Copy reference to clipboard'), placement: "left", boundary: 'viewport')
.cross-project-reference.hide-collapsed
%span
= _('Reference:')
%cite{ title: project_ref }
= project_ref
- = clipboard_button(text: project_ref, title: _('Copy reference to clipboard'), placement: "left")
+ = clipboard_button(text: project_ref, title: _('Copy reference to clipboard'), placement: "left", boundary: 'viewport')
- if current_user && issuable.can_move?(current_user)
.block.js-sidebar-move-issue-block
- .sidebar-collapsed-icon{ data: { toggle: 'tooltip', placement: 'left', container: 'body' }, title: _('Move issue') }
+ .sidebar-collapsed-icon{ data: { toggle: 'tooltip', placement: 'left', container: 'body', boundary: 'viewport' }, title: _('Move issue') }
= custom_icon('icon_arrow_right')
.dropdown.sidebar-move-issue-dropdown.hide-collapsed
%button.btn.btn-default.btn-block.js-sidebar-dropdown-toggle.js-move-issue{ type: 'button',
- data: { toggle: 'dropdown' } }
+ data: { toggle: 'dropdown', display: 'static' } }
= _('Move issue')
.dropdown-menu.dropdown-menu-selectable
= dropdown_title(_('Move issue'))
diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml
index 21006a76b28..8a13c7a3b83 100644
--- a/app/views/shared/issuable/_sidebar_assignees.html.haml
+++ b/app/views/shared/issuable/_sidebar_assignees.html.haml
@@ -4,7 +4,7 @@
= _('Assignee')
= icon('spinner spin')
- else
- .sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: sidebar_assignee_tooltip_label(issuable) }
+ .sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body", boundary: 'viewport' }, title: sidebar_assignee_tooltip_label(issuable) }
- if issuable.assignee
= link_to_member(@project, issuable.assignee, size: 24)
- else
@@ -13,15 +13,15 @@
= _('Assignee')
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
- = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
+ = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right'
- if !signed_in
- %a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => _('Toggle sidebar') }
+ %a.gutter-toggle.float-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => _('Toggle sidebar') }
= sidebar_gutter_toggle_icon
.value.hide-collapsed
- if issuable.assignee
= link_to_member(@project, issuable.assignee, size: 32, extra_class: 'bold') do
- if !issuable.can_be_merged_by?(issuable.assignee)
- %span.pull-right.cannot-be-merged{ data: { toggle: 'tooltip', placement: 'left' }, title: _('Not allowed to merge') }
+ %span.float-right.cannot-be-merged{ data: { toggle: 'tooltip', placement: 'left' }, title: _('Not allowed to merge') }
= icon('exclamation-triangle', 'aria-hidden': 'true')
%span.username
= issuable.assignee.to_reference
@@ -37,7 +37,7 @@
- issuable.assignees.each do |assignee|
= hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", assignee.id, id: nil, data: { avatar_url: assignee.avatar_url, name: assignee.name, username: assignee.username }
- - options = { toggle_class: 'js-user-search js-author-search', title: _('Assign to'), filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: _('Search users'), data: { first_user: current_user&.username, current_user: true, project_id: @project&.id, author_id: issuable.author_id, field_name: "#{issuable.to_ability_name}[assignee_ids][]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } }
+ - options = { toggle_class: 'js-user-search js-author-search', title: _('Assign to'), filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: _('Search users'), data: { first_user: current_user&.username, current_user: true, project_id: @project&.id, author_id: issuable.author_id, field_name: "#{issuable.to_ability_name}[assignee_ids][]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true, display: 'static' } }
- title = _('Select assignee')
- if issuable.is_a?(Issue)
@@ -50,7 +50,7 @@
- data[:multi_select] = true
- data['dropdown-title'] = title
- data['dropdown-header'] = dropdown_options[:data][:'dropdown-header']
- - data['max-select'] = dropdown_options[:data][:'max-select']
+ - data['max-select'] = dropdown_options[:data][:'max-select'] if dropdown_options[:data][:'max-select']
- options[:data].merge!(data)
= dropdown_tag(title, options: options)
diff --git a/app/views/shared/issuable/_sidebar_todo.html.haml b/app/views/shared/issuable/_sidebar_todo.html.haml
index 74327fb1ba8..583b33a8a1b 100644
--- a/app/views/shared/issuable/_sidebar_todo.html.haml
+++ b/app/views/shared/issuable/_sidebar_todo.html.haml
@@ -3,7 +3,7 @@
- todo_content = is_collapsed ? icon('plus-square') : _('Add todo')
%button.issuable-todo-btn.js-issuable-todo{ type: 'button',
- class: (is_collapsed ? 'btn-blank sidebar-collapsed-icon dont-change-state has-tooltip' : 'btn btn-default issuable-header-btn pull-right'),
+ class: (is_collapsed ? 'btn-blank sidebar-collapsed-icon dont-change-state has-tooltip' : 'btn btn-default issuable-header-btn float-right'),
title: (todo.nil? ? _('Add todo') : _('Mark todo as done')),
'aria-label' => (todo.nil? ? _('Add todo') : _('Mark todo as done')),
data: issuable_todo_button_data(issuable, todo, is_collapsed) }
diff --git a/app/views/shared/issuable/form/_branch_chooser.html.haml b/app/views/shared/issuable/form/_branch_chooser.html.haml
index 9a589387255..fbc96baa0f7 100644
--- a/app/views/shared/issuable/form/_branch_chooser.html.haml
+++ b/app/views/shared/issuable/form/_branch_chooser.html.haml
@@ -6,13 +6,13 @@
%hr
- if issuable.new_record?
- .form-group
- = form.label :source_branch, class: 'control-label'
+ .form-group.row
+ = form.label :source_branch, class: 'col-form-label col-sm-2'
.col-sm-10
.issuable-form-select-holder
= form.select(:source_branch, [issuable.source_branch], {}, { class: 'source_branch select2 ref-name', disabled: true })
-.form-group
- = form.label :target_branch, class: 'control-label'
+.form-group.row
+ = form.label :target_branch, class: 'col-form-label col-sm-2'
.col-sm-10.target-branch-select-dropdown-container
.issuable-form-select-holder
= form.hidden_field(:target_branch,
diff --git a/app/views/shared/issuable/form/_contribution.html.haml b/app/views/shared/issuable/form/_contribution.html.haml
index de508278d7c..bc9a1edc39c 100644
--- a/app/views/shared/issuable/form/_contribution.html.haml
+++ b/app/views/shared/issuable/form/_contribution.html.haml
@@ -7,14 +7,14 @@
%hr
-.form-group
- .control-label
+.form-group.row
+ %label.col-form-label.col-sm-2
= _('Contribution')
.col-sm-10
- .checkbox
- = form.label :allow_maintainer_to_push do
- = form.check_box :allow_maintainer_to_push, disabled: !issuable.can_allow_maintainer_to_push?(current_user)
- = _('Allow edits from maintainers.')
- = link_to 'About this feature', help_page_path('user/project/merge_requests/maintainer_access')
- .help-block
- = allow_maintainer_push_unavailable_reason(issuable)
+ .form-check.prepend-top-5
+ = form.check_box :allow_collaboration, disabled: !issuable.can_allow_collaboration?(current_user), class: 'form-check-input'
+ = form.label :allow_collaboration, class: 'form-check-label' do
+ = _('Allow commits from members who can merge to the target branch.')
+ = link_to 'About this feature', help_page_path('user/project/merge_requests/allow_collaboration')
+ .form-text.text-muted
+ = allow_collaboration_unavailable_reason(issuable)
diff --git a/app/views/shared/issuable/form/_default_templates.html.haml b/app/views/shared/issuable/form/_default_templates.html.haml
new file mode 100644
index 00000000000..49a5ce926b3
--- /dev/null
+++ b/app/views/shared/issuable/form/_default_templates.html.haml
@@ -0,0 +1,4 @@
+%p.form-text.text-muted
+ Add
+ = link_to 'description templates', help_page_path('user/project/description_templates'), tabindex: -1
+ to help your contributors communicate effectively!
diff --git a/app/views/shared/issuable/form/_issue_assignee.html.haml b/app/views/shared/issuable/form/_issue_assignee.html.haml
index 9b2b6e572e7..0d13fccaf3e 100644
--- a/app/views/shared/issuable/form/_issue_assignee.html.haml
+++ b/app/views/shared/issuable/form/_issue_assignee.html.haml
@@ -11,7 +11,7 @@
Assignee
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
- = link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
+ = link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link float-right'
.value.hide-collapsed
- if assignees.any?
- assignees.each do |assignee|
diff --git a/app/views/shared/issuable/form/_merge_params.html.haml b/app/views/shared/issuable/form/_merge_params.html.haml
index 8f6509a8ce8..1881875b7c0 100644
--- a/app/views/shared/issuable/form/_merge_params.html.haml
+++ b/app/views/shared/issuable/form/_merge_params.html.haml
@@ -3,15 +3,20 @@
- return unless issuable.is_a?(MergeRequest)
- return if issuable.closed_without_fork?
--# This check is duplicated below to avoid CE -> EE merge conflicts.
--# This comment and the following line should only exist in CE.
-- return unless issuable.can_remove_source_branch?(current_user)
-
-.form-group
- .col-sm-10.col-sm-offset-2
- - if issuable.can_remove_source_branch?(current_user)
- .checkbox
- = label_tag 'merge_request[force_remove_source_branch]' do
- = hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil
- = check_box_tag 'merge_request[force_remove_source_branch]', '1', issuable.force_remove_source_branch?
+- if issuable.can_remove_source_branch?(current_user)
+ .form-group.row
+ .col-sm-10.offset-sm-2
+ .form-check
+ = hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil
+ = check_box_tag 'merge_request[force_remove_source_branch]', '1', issuable.force_remove_source_branch?, class: 'form-check-input'
+ = label_tag 'merge_request[force_remove_source_branch]', class: 'form-check-label' do
Remove source branch when merge request is accepted.
+
+.form-group.row
+ .col-sm-10.offset-sm-2
+ .form-check
+ = hidden_field_tag 'merge_request[squash]', '0', id: nil
+ = check_box_tag 'merge_request[squash]', '1', issuable.squash, class: 'form-check-input'
+ = label_tag 'merge_request[squash]', class: 'form-check-label' do
+ Squash commits when merge request is accepted.
+ = link_to 'About this feature', help_page_path('user/project/merge_requests/squash_and_merge')
diff --git a/app/views/shared/issuable/form/_merge_request_assignee.html.haml b/app/views/shared/issuable/form/_merge_request_assignee.html.haml
index d7740eddcca..05c03dedd91 100644
--- a/app/views/shared/issuable/form/_merge_request_assignee.html.haml
+++ b/app/views/shared/issuable/form/_merge_request_assignee.html.haml
@@ -9,12 +9,12 @@
Assignee
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
- = link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
+ = link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link float-right'
.value.hide-collapsed
- if merge_request.assignee
= link_to_member(@project, merge_request.assignee, size: 32, extra_class: 'bold') do
- unless merge_request.can_be_merged_by?(merge_request.assignee)
- %span.pull-right.cannot-be-merged{ data: { toggle: 'tooltip', placement: 'left' }, title: 'Not allowed to merge' }
+ %span.float-right.cannot-be-merged{ data: { toggle: 'tooltip', placement: 'left' }, title: 'Not allowed to merge' }
= icon('exclamation-triangle', 'aria-hidden': 'true')
%span.username
= merge_request.assignee.to_reference
diff --git a/app/views/shared/issuable/form/_metadata.html.haml b/app/views/shared/issuable/form/_metadata.html.haml
index 1608bd59cf1..3b017c62a80 100644
--- a/app/views/shared/issuable/form/_metadata.html.haml
+++ b/app/views/shared/issuable/form/_metadata.html.haml
@@ -8,28 +8,31 @@
%hr
.row
- %div{ class: (has_due_date ? "col-lg-6" : "col-sm-12") }
- .form-group.issue-assignee
+ %div{ class: (has_due_date ? "col-lg-6" : "col-12") }
+ .form-group.row.issue-assignee
- if issuable.is_a?(Issue)
= render "shared/issuable/form/metadata_issue_assignee", issuable: issuable, form: form, has_due_date: has_due_date
- else
= render "shared/issuable/form/metadata_merge_request_assignee", issuable: issuable, form: form, has_due_date: has_due_date
- .form-group.issue-milestone
- = form.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}"
- .col-sm-10{ class: ("col-lg-8" if has_due_date) }
+ .form-group.row.issue-milestone
+ = form.label :milestone_id, "Milestone", class: "col-form-label #{has_due_date ? "col-md-2 col-lg-4" : "col-sm-2"}"
+ .col-sm-10{ class: ("col-md-8" if has_due_date) }
.issuable-form-select-holder
- = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, show_started: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone"
- .form-group
+ = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, show_started: false, extra_class: "qa-issuable-milestone-dropdown js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone"
+ .form-group.row
- has_labels = @labels && @labels.any?
- = form.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}"
+ = form.label :label_ids, "Labels", class: "col-form-label #{has_due_date ? "col-md-2 col-lg-4" : "col-sm-2"}"
= form.hidden_field :label_ids, multiple: true, value: ''
- .col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" }
+ .col-sm-10{ class: "#{"col-md-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" }
.issuable-form-select-holder
- = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false}, dropdown_title: "Select label"
+ = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false }, dropdown_title: "Select label"
+
+ = render_if_exists "shared/issuable/form/weight", issuable: issuable, form: form
+
- if has_due_date
.col-lg-6
- .form-group
- = form.label :due_date, "Due date", class: "control-label"
- .col-sm-10
+ .form-group.row
+ = form.label :due_date, "Due date", class: "col-form-label col-md-2 col-lg-4"
+ .col-8
.issuable-form-select-holder
= form.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date"
diff --git a/app/views/shared/issuable/form/_metadata_issue_assignee.html.haml b/app/views/shared/issuable/form/_metadata_issue_assignee.html.haml
index 567cde764e2..6d4f9ccd66f 100644
--- a/app/views/shared/issuable/form/_metadata_issue_assignee.html.haml
+++ b/app/views/shared/issuable/form/_metadata_issue_assignee.html.haml
@@ -1,5 +1,5 @@
-= form.label :assignee_ids, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}"
-.col-sm-10{ class: ("col-lg-8" if has_due_date) }
+= form.label :assignee_ids, "Assignee", class: "col-form-label #{"col-md-2 col-lg-4" if has_due_date}"
+.col-sm-10{ class: ("col-md-8" if has_due_date) }
.issuable-form-select-holder.selectbox
- issuable.assignees.each do |assignee|
= hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", assignee.id, id: nil, data: { meta: assignee.name, avatar_url: assignee.avatar_url, name: assignee.name, username: assignee.username }
diff --git a/app/views/shared/issuable/form/_metadata_merge_request_assignee.html.haml b/app/views/shared/issuable/form/_metadata_merge_request_assignee.html.haml
index d0ea4e149df..3521f71f409 100644
--- a/app/views/shared/issuable/form/_metadata_merge_request_assignee.html.haml
+++ b/app/views/shared/issuable/form/_metadata_merge_request_assignee.html.haml
@@ -1,4 +1,4 @@
-= form.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}"
+= form.label :assignee_id, "Assignee", class: "col-form-label #{has_due_date ? "col-lg-4" : "col-sm-2"}"
.col-sm-10{ class: ("col-lg-8" if has_due_date) }
.issuable-form-select-holder
= form.hidden_field :assignee_id
diff --git a/app/views/shared/issuable/form/_title.html.haml b/app/views/shared/issuable/form/_title.html.haml
index e81639f35ea..e49bdec386a 100644
--- a/app/views/shared/issuable/form/_title.html.haml
+++ b/app/views/shared/issuable/form/_title.html.haml
@@ -6,10 +6,10 @@
%div{ class: div_class }
= form.text_field :title, required: true, maxlength: 255, autofocus: true,
- autocomplete: 'off', class: 'form-control pad qa-issuable-form-title'
+ autocomplete: 'off', class: 'form-control pad qa-issuable-form-title', placeholder: _('Title')
- if issuable.respond_to?(:work_in_progress?)
- %p.help-block
+ %p.form-text.text-muted
.js-wip-explanation
%a.js-toggle-wip{ href: '', tabindex: -1 }
Remove the
@@ -30,7 +30,4 @@
merge request from being merged before it's ready.
- if no_issuable_templates && can?(current_user, :push_code, issuable.project)
- %p.help-block
- Add
- = link_to 'description templates', help_page_path('user/project/description_templates'), tabindex: -1
- to help your contributors communicate effectively!
+ = render 'shared/issuable/form/default_templates'
diff --git a/app/views/shared/labels/_form.html.haml b/app/views/shared/labels/_form.html.haml
index e8b04f56839..2bf5efae1e6 100644
--- a/app/views/shared/labels/_form.html.haml
+++ b/app/views/shared/labels/_form.html.haml
@@ -1,24 +1,25 @@
-= form_for @label, as: :label, url: url, html: { class: 'form-horizontal label-form js-quick-submit js-requires-input' } do |f|
+= form_for @label, as: :label, url: url, html: { class: 'label-form js-quick-submit js-requires-input' } do |f|
= form_errors(@label)
- .form-group
- = f.label :title, class: 'control-label'
+ .form-group.row
+ = f.label :title, class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_field :title, class: "form-control", required: true, autofocus: true
- .form-group
- = f.label :description, class: 'control-label'
+ .form-group.row
+ = f.label :description, class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_field :description, class: "form-control js-quick-submit"
- .form-group
- = f.label :color, "Background color", class: 'control-label'
+ .form-group.row
+ = f.label :color, "Background color", class: 'col-form-label col-sm-2'
.col-sm-10
.input-group
- .input-group-addon.label-color-preview &nbsp;
+ .input-group-prepend
+ .input-group-text.label-color-preview &nbsp;
= f.text_field :color, class: "form-control"
- .help-block
+ .form-text.text-muted
Choose any color.
%br
- Or you can choose one of suggested colors below
+ Or you can choose one of the suggested colors below
.suggest-colors
- suggested_colors.each do |color|
diff --git a/app/views/shared/members/_group.html.haml b/app/views/shared/members/_group.html.haml
index fc634856061..d0b492b43f3 100644
--- a/app/views/shared/members/_group.html.haml
+++ b/app/views/shared/members/_group.html.haml
@@ -5,16 +5,16 @@
%li.member.group_member{ id: dom_id }
%span.list-item-name
= group_icon(group, class: "avatar s40", alt: '')
- %strong
- = link_to group.full_name, group_path(group)
- .cgray
- Given access #{time_ago_with_tooltip(group_link.created_at)}
- - if group_link.expires?
- ·
- %span{ class: ('text-warning' if group_link.expires_soon?) }
- Expires in #{distance_of_time_in_words_to_now(group_link.expires_at)}
+ .user-info
+ = link_to group.full_name, group_path(group), class: 'member'
+ .cgray
+ Given access #{time_ago_with_tooltip(group_link.created_at)}
+ - if group_link.expires?
+ ·
+ %span{ class: ('text-warning' if group_link.expires_soon?) }
+ Expires in #{distance_of_time_in_words_to_now(group_link.expires_at)}
.controls.member-controls
- = form_tag project_group_link_path(@project, group_link), method: :put, remote: true, class: 'form-horizontal js-edit-member-form' do
+ = form_tag project_group_link_path(@project, group_link), method: :put, remote: true, class: 'js-edit-member-form form-group row append-right-5' do
= hidden_field_tag "group_link[group_access]", group_link.group_access
.member-form-control.dropdown.append-right-5
%button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button",
@@ -23,7 +23,7 @@
%span.dropdown-toggle-text
= group_link.human_access
= icon("chevron-down")
- .dropdown-menu.dropdown-select.dropdown-menu-align-right.dropdown-menu-selectable
+ .dropdown-menu.dropdown-select.dropdown-menu-right.dropdown-menu-selectable
= dropdown_title("Change permissions")
.dropdown-content
%ul
@@ -40,6 +40,6 @@
method: :delete,
data: { confirm: "Are you sure you want to remove #{group.name}?" },
class: 'btn btn-remove prepend-left-10' do
- %span.visible-xs-block
+ %span.d-block.d-sm-none
Delete
- = icon('trash', class: 'hidden-xs')
+ = icon('trash', class: 'd-none d-sm-block')
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index 1961ad6d616..46debe1f2b9 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -14,10 +14,10 @@
%span.cgray= user.to_reference
- if user == current_user
- %span.label.label-success.prepend-left-5 It's you
+ %span.badge.badge-success.prepend-left-5 It's you
- if user.blocked?
- %label.label.label-danger
+ %label.badge.badge-danger
%strong Blocked
- if user.two_factor_enabled?
@@ -57,11 +57,11 @@
- if member.can_resend_invite?
= link_to icon('paper-plane'), polymorphic_path([:resend_invite, member]),
method: :post,
- class: 'btn btn-default prepend-left-10 hidden-xs',
+ class: 'btn btn-default prepend-left-10 d-none d-sm-block',
title: 'Resend invite'
- if user != current_user && member.can_update?
- = form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f|
+ = form_for member, remote: true, html: { class: 'js-edit-member-form form-group row append-right-5' } do |f|
= f.hidden_field :access_level
.member-form-control.dropdown.append-right-5
%button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button",
@@ -69,7 +69,7 @@
%span.dropdown-toggle-text
= member.human_access
= icon("chevron-down")
- .dropdown-menu.dropdown-select.dropdown-menu-align-right.dropdown-menu-selectable
+ .dropdown-menu.dropdown-select.dropdown-menu-right.dropdown-menu-selectable
= dropdown_title("Change permissions")
.dropdown-content
%ul
@@ -93,10 +93,10 @@
method: :post,
class: 'btn btn-success prepend-left-10',
title: 'Grant access' do
- %span{ class: ('visible-xs-block' unless force_mobile_view) }
+ %span{ class: ('d-block d-sm-none' unless force_mobile_view) }
Grant access
- unless force_mobile_view
- = icon('check inverse', class: 'hidden-xs')
+ = icon('check inverse', class: 'd-none d-sm-block')
- if member.can_remove?
- if current_user == user
@@ -110,9 +110,9 @@
data: { confirm: remove_member_message(member) },
class: 'btn btn-remove prepend-left-10',
title: remove_member_title(member) do
- %span{ class: ('visible-xs-block' unless force_mobile_view) }
+ %span{ class: ('d-block d-sm-none' unless force_mobile_view) }
Delete
- unless force_mobile_view
- = icon('trash', class: 'hidden-xs')
+ = icon('trash', class: 'd-none d-sm-block')
- else
%span.member-access-text= member.human_access
diff --git a/app/views/shared/members/_requests.html.haml b/app/views/shared/members/_requests.html.haml
index 1fbd6bcc4cb..54679ab86cc 100644
--- a/app/views/shared/members/_requests.html.haml
+++ b/app/views/shared/members/_requests.html.haml
@@ -4,10 +4,10 @@
- return if requesters.empty?
-.panel.panel-default.prepend-top-default{ class: ('panel-mobile' if force_mobile_view ) }
- .panel-heading
+.card.prepend-top-default{ class: ('card-mobile' if force_mobile_view ) }
+ .card-header
Users requesting access to
%strong= membership_source.name
- %span.badge= requesters.size
+ %span.badge.badge-pill= requesters.size
%ul.content-list.members-list
= render partial: 'shared/members/member', collection: requesters, as: :member, locals: { force_mobile_view: force_mobile_view }
diff --git a/app/views/shared/members/_sort_dropdown.html.haml b/app/views/shared/members/_sort_dropdown.html.haml
index bad0891f9f2..56b8c8f033e 100644
--- a/app/views/shared/members/_sort_dropdown.html.haml
+++ b/app/views/shared/members/_sort_dropdown.html.haml
@@ -1,6 +1,6 @@
.dropdown.inline.member-sort-dropdown
= dropdown_toggle(member_sort_options_hash[@sort], { toggle: 'dropdown' })
- %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
+ %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable
%li.dropdown-header
Sort by
- member_sort_options_hash.each do |value, title|
diff --git a/app/views/shared/milestones/_form_dates.html.haml b/app/views/shared/milestones/_form_dates.html.haml
index a74cdbe274b..922805958a5 100644
--- a/app/views/shared/milestones/_form_dates.html.haml
+++ b/app/views/shared/milestones/_form_dates.html.haml
@@ -1,12 +1,11 @@
.col-md-6
- .form-group
- = f.label :start_date, "Start Date", class: "control-label"
+ .form-group.row
+ = f.label :start_date, "Start Date", class: "col-form-label col-sm-2"
.col-sm-10
- = f.text_field :start_date, class: "datepicker form-control", placeholder: "Select start date"
- %a.inline.pull-right.prepend-top-5.js-clear-start-date{ href: "#" } Clear start date
-.col-md-6
- .form-group
- = f.label :due_date, "Due Date", class: "control-label"
+ = f.text_field :start_date, class: "datepicker form-control", placeholder: "Select start date", autocomplete: 'off'
+ %a.inline.float-right.prepend-top-5.js-clear-start-date{ href: "#" } Clear start date
+ .form-group.row
+ = f.label :due_date, "Due Date", class: "col-form-label col-sm-2"
.col-sm-10
- = f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date"
- %a.inline.pull-right.prepend-top-5.js-clear-due-date{ href: "#" } Clear due date
+ = f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date", autocomplete: 'off'
+ %a.inline.float-right.prepend-top-5.js-clear-due-date{ href: "#" } Clear due date
diff --git a/app/views/shared/milestones/_issuables.html.haml b/app/views/shared/milestones/_issuables.html.haml
index 7175e275f95..ee6354b1c28 100644
--- a/app/views/shared/milestones/_issuables.html.haml
+++ b/app/views/shared/milestones/_issuables.html.haml
@@ -1,9 +1,9 @@
- show_counter = local_assigns.fetch(:show_counter, false)
- primary = local_assigns.fetch(:primary, false)
-- panel_class = primary ? 'panel-primary' : 'panel-default'
+- panel_class = primary ? 'bg-primary text-white' : ''
-.panel{ class: panel_class }
- .panel-heading
+.card{ class: panel_class }
+ .card-header
.title
= title
- if show_counter
@@ -11,7 +11,7 @@
= number_with_delimiter(issuables.length)
- class_prefix = dom_class(issuables).pluralize
- %ul{ class: "well-list milestone-#{class_prefix}-list", id: "#{class_prefix}-list-#{id}" }
+ %ul{ class: "content-list milestone-#{class_prefix}-list", id: "#{class_prefix}-list-#{id}" }
= render partial: 'shared/milestones/issuable',
collection: issuables,
as: :issuable,
diff --git a/app/views/shared/milestones/_labels_tab.html.haml b/app/views/shared/milestones/_labels_tab.html.haml
index a26b3b8009e..6797520650d 100644
--- a/app/views/shared/milestones/_labels_tab.html.haml
+++ b/app/views/shared/milestones/_labels_tab.html.haml
@@ -10,7 +10,7 @@
%span.prepend-description-left
= markdown_field(label, :description)
- .pull-right.hidden-xs.hidden-sm.hidden-md
+ .float-right.d-none.d-lg-block.d-xl-block
= link_to milestones_label_path(options.merge(state: 'opened')), class: 'btn btn-transparent btn-action' do
- pluralize milestone_issues_by_label_count(@milestone, label, state: :opened), 'open issue'
= link_to milestones_label_path(options.merge(state: 'closed')), class: 'btn btn-transparent btn-action' do
diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml
index ac494814f55..c559945a9c9 100644
--- a/app/views/shared/milestones/_milestone.html.haml
+++ b/app/views/shared/milestones/_milestone.html.haml
@@ -1,76 +1,59 @@
- dashboard = local_assigns[:dashboard]
- custom_dom_id = dom_id(milestone.try(:milestones) ? milestone.milestones.first : milestone)
+- milestone_type = milestone.group_milestone? ? 'Group Milestone' : 'Project Milestone'
%li{ class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: custom_dom_id }
.row
.col-sm-6
- %strong= link_to truncate(milestone.title, length: 100), milestone_path
- - if milestone.group_milestone?
- %span - Group Milestone
- - else
- %span - Project Milestone
+ .append-bottom-5
+ %strong= link_to truncate(milestone.title, length: 100), milestone_path
+ - if @group
+ = " - #{milestone_type}"
- .col-sm-6
- .pull-right.light #{milestone.percent_complete(current_user)}% complete
- .row
- .col-sm-6
+ - if @project || milestone.is_a?(GlobalMilestone) || milestone.group_milestone?
+ - if milestone.due_date || milestone.start_date
+ .milestone-range.append-bottom-5
+ = milestone_date_range(milestone)
+ %div
+ = render('shared/milestone_expired', milestone: milestone)
+ - if milestone.legacy_group_milestone?
+ .projects
+ - milestone.milestones.each do |milestone|
+ = link_to milestone_path(milestone) do
+ %span.label-badge.label-badge-blue.d-inline-block.append-bottom-5
+ = dashboard ? milestone.project.full_name : milestone.project.name
+
+ .col-sm-4.milestone-progress
+ = milestone_progress_bar(milestone)
= link_to pluralize(milestone.total_issues_count(current_user), 'Issue'), issues_path
&middot;
= link_to pluralize(milestone.merge_requests.size, 'Merge Request'), merge_requests_path
- .col-sm-6= milestone_progress_bar(milestone)
- - if milestone.is_a?(GlobalMilestone) || milestone.group_milestone?
- .row
- .col-sm-6
- - if milestone.legacy_group_milestone?
- .expiration= render('shared/milestone_expired', milestone: milestone)
- .projects
- - milestone.milestones.each do |milestone|
- = link_to milestone_path(milestone) do
- %span.label.label-gray
- = dashboard ? milestone.project.full_name : milestone.project.name
- - if @group
- .col-sm-6.milestone-actions
+ .float-lg-right.light #{milestone.percent_complete(current_user)}% complete
+ .col-sm-2
+ .milestone-actions.d-flex.justify-content-sm-start.justify-content-md-end
+ - if @project
+ - if can?(current_user, :admin_milestone, milestone.project) and milestone.active?
+ - if @project.group
+ %button.js-promote-project-milestone-button.btn.btn-blank.btn-sm.btn-grouped.has-tooltip{ title: _('Promote to Group Milestone'),
+ disabled: true,
+ type: 'button',
+ data: { url: promote_project_milestone_path(milestone.project, milestone),
+ milestone_title: milestone.title,
+ group_name: @project.group.name,
+ target: '#promote-milestone-modal',
+ container: 'body',
+ toggle: 'modal' } }
+ = sprite_icon('level-up', size: 14)
+
+ = link_to 'Close Milestone', project_milestone_path(@project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-sm btn-close btn-grouped"
+ - unless milestone.active?
+ = link_to 'Reopen Milestone', project_milestone_path(@project, milestone, {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen"
+ - if @group
- if can?(current_user, :admin_milestones, @group)
- - if milestone.group_milestone?
- = link_to edit_group_milestone_path(@group, milestone), class: "btn btn-xs btn-grouped" do
- Edit
- \
- if milestone.closed?
- = link_to 'Reopen Milestone', group_milestone_route(milestone, {state_event: :activate }), method: :put, class: "btn btn-xs btn-grouped btn-reopen"
+ = link_to 'Reopen Milestone', group_milestone_route(milestone, {state_event: :activate }), method: :put, class: "btn btn-sm btn-grouped btn-reopen"
- else
- = link_to 'Close Milestone', group_milestone_route(milestone, {state_event: :close }), method: :put, class: "btn btn-xs btn-grouped btn-close"
-
- - if @project
- .row
- .col-sm-6
- = render('shared/milestone_expired', milestone: milestone)
- .col-sm-6.milestone-actions
- - if can?(current_user, :admin_milestone, milestone.project) and milestone.active?
- = link_to edit_project_milestone_path(milestone.project, milestone), class: "btn btn-xs btn-grouped" do
- Edit
- \
-
- - if @project.group
- %button.js-promote-project-milestone-button.btn.btn-xs.btn-grouped.has-tooltip{ title: _('Promote to Group Milestone'),
- disabled: true,
- type: 'button',
- data: { url: promote_project_milestone_path(milestone.project, milestone),
- milestone_title: milestone.title,
- group_name: @project.group.name,
- target: '#promote-milestone-modal',
- container: 'body',
- toggle: 'modal' } }
- = _('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"
-
- %button.js-delete-milestone-button.btn.btn-xs.btn-grouped.btn-danger{ data: { toggle: 'modal',
- target: '#delete-milestone-modal',
- milestone_id: milestone.id,
- milestone_title: markdown_field(milestone, :title),
- milestone_url: project_milestone_path(milestone.project, milestone),
- milestone_issue_count: milestone.issues.count,
- milestone_merge_request_count: milestone.merge_requests.count },
- disabled: true }
- = _('Delete')
- = icon('spin spinner', class: 'js-loading-icon hidden' )
+ = link_to 'Close Milestone', group_milestone_route(milestone, {state_event: :close }), method: :put, class: "btn btn-sm btn-grouped btn-close"
+ - if dashboard
+ .status-box.status-box-milestone
+ = milestone_type
diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml
index 8e9a1b56bb8..becd1c4884e 100644
--- a/app/views/shared/milestones/_sidebar.html.haml
+++ b/app/views/shared/milestones/_sidebar.html.haml
@@ -4,7 +4,7 @@
%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => affix_offset, "spy" => "affix", "always-show-toggle" => true }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
.issuable-sidebar.milestone-sidebar
.block.milestone-progress.issuable-sidebar-header
- %a.gutter-toggle.pull-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => "Toggle sidebar", title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left' } }
+ %a.gutter-toggle.float-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => "Toggle sidebar", title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left', boundary: 'viewport' } }
= sidebar_gutter_toggle_icon
.title.hide-collapsed
%strong.bold== #{milestone.percent_complete(current_user)}%
@@ -14,7 +14,7 @@
= milestone_progress_bar(milestone)
.block.milestone-progress.hide-expanded
- .sidebar-collapsed-icon.has-tooltip{ title: milestone_progress_tooltip_text(milestone), data: { container: 'body', html: 1, placement: 'left' } }
+ .sidebar-collapsed-icon.has-tooltip{ title: milestone_progress_tooltip_text(milestone), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } }
%span== #{milestone.percent_complete(current_user)}%
= milestone_progress_bar(milestone)
@@ -22,7 +22,7 @@
.title
Start date
- if @project && can?(current_user, :admin_milestone, @project)
- = link_to 'Edit', edit_project_milestone_path(@project, @milestone), class: 'js-sidebar-dropdown-toggle edit-link pull-right'
+ = link_to 'Edit', edit_project_milestone_path(@project, @milestone), class: 'js-sidebar-dropdown-toggle edit-link float-right'
.value
%span.value-content
- if milestone.start_date
@@ -36,29 +36,29 @@
%span.collapsed-milestone-date
- if milestone.start_date && milestone.due_date
- if milestone.start_date.year == milestone.due_date.year
- .milestone-date.has-tooltip{ title: milestone_time_for(milestone.start_date, :start), data: { container: 'body', html: 1, placement: 'left' } }
+ .milestone-date.has-tooltip{ title: milestone_time_for(milestone.start_date, :start), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } }
= milestone.start_date.strftime('%b %-d')
- else
- .milestone-date.has-tooltip{ title: milestone_time_for(milestone.start_date, :start), data: { container: 'body', html: 1, placement: 'left' } }
+ .milestone-date.has-tooltip{ title: milestone_time_for(milestone.start_date, :start), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } }
= milestone.start_date.strftime('%b %-d %Y')
.date-separator -
- .due_date.has-tooltip{ title: milestone_time_for(milestone.due_date, :end), data: { container: 'body', html: 1, placement: 'left' } }
+ .due_date.has-tooltip{ title: milestone_time_for(milestone.due_date, :end), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } }
= milestone.due_date.strftime('%b %-d %Y')
- elsif milestone.start_date
From
- .milestone-date.has-tooltip{ title: milestone_time_for(milestone.start_date, :start), data: { container: 'body', html: 1, placement: 'left' } }
+ .milestone-date.has-tooltip{ title: milestone_time_for(milestone.start_date, :start), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } }
= milestone.start_date.strftime('%b %-d %Y')
- elsif milestone.due_date
Until
- .milestone-date.has-tooltip{ title: milestone_time_for(milestone.due_date, :end), data: { container: 'body', html: 1, placement: 'left' } }
+ .milestone-date.has-tooltip{ title: milestone_time_for(milestone.due_date, :end), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } }
= milestone.due_date.strftime('%b %-d %Y')
- else
- .has-tooltip{ title: milestone_time_for(milestone.start_date, :start), data: { container: 'body', html: 1, placement: 'left' } }
+ .has-tooltip{ title: milestone_time_for(milestone.start_date, :start), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } }
None
.title.hide-collapsed
Due date
- if @project && can?(current_user, :admin_milestone, @project)
- = link_to 'Edit', edit_project_milestone_path(@project, @milestone), class: 'js-sidebar-dropdown-toggle edit-link pull-right'
+ = link_to 'Edit', edit_project_milestone_path(@project, @milestone), class: 'js-sidebar-dropdown-toggle edit-link float-right'
.value.hide-collapsed
%span.value-content
- if milestone.due_date
@@ -72,15 +72,15 @@
- if !project || can?(current_user, :read_issue, project)
.block.issues
- .sidebar-collapsed-icon.has-tooltip{ title: milestone_issues_tooltip_text(milestone), data: { container: 'body', html: 1, placement: 'left' } }
+ .sidebar-collapsed-icon.has-tooltip{ title: milestone_issues_tooltip_text(milestone), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } }
%strong
= custom_icon('issues')
%span= milestone.issues_visible_to_user(current_user).count
.title.hide-collapsed
Issues
- %span.badge= milestone.issues_visible_to_user(current_user).count
+ %span.badge.badge-pill= milestone.issues_visible_to_user(current_user).count
- if show_new_issue_link?(project)
- = link_to new_project_issue_path(project, issue: { milestone_id: milestone.id }), class: "pull-right", title: "New Issue" do
+ = link_to new_project_issue_path(project, issue: { milestone_id: milestone.id }), class: "float-right", title: "New Issue" do
New issue
.value.hide-collapsed.bold
%span.milestone-stat
@@ -99,14 +99,16 @@
= _('Time tracking')
= icon('spinner spin')
+ = render_if_exists 'shared/milestones/weight', milestone: milestone
+
.block.merge-requests
- .sidebar-collapsed-icon.has-tooltip{ title: milestone_merge_requests_tooltip_text(milestone), data: { container: 'body', html: 1, placement: 'left' } }
+ .sidebar-collapsed-icon.has-tooltip{ title: milestone_merge_requests_tooltip_text(milestone), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } }
%strong
= custom_icon('mr_bold')
%span= milestone.merge_requests.count
.title.hide-collapsed
Merge requests
- %span.badge= milestone.merge_requests.count
+ %span.badge.badge-pill= milestone.merge_requests.count
.value.hide-collapsed.bold
- if !project || can?(current_user, :read_merge_request, project)
%span.milestone-stat
@@ -136,10 +138,10 @@
- if milestone_ref.present?
.block.reference
.sidebar-collapsed-icon.dont-change-state
- = clipboard_button(text: milestone_ref, title: "Copy reference to clipboard", placement: "left")
+ = clipboard_button(text: milestone_ref, title: "Copy reference to clipboard", placement: "left", boundary: 'viewport')
.cross-project-reference.hide-collapsed
%span
Reference:
%cite{ title: milestone_ref }
= milestone_ref
- = clipboard_button(text: milestone_ref, title: "Copy reference to clipboard", placement: "left")
+ = clipboard_button(text: milestone_ref, title: "Copy reference to clipboard", placement: "left", boundary: 'viewport')
diff --git a/app/views/shared/milestones/_tabs.html.haml b/app/views/shared/milestones/_tabs.html.haml
index b95a4ea674d..55460acab8f 100644
--- a/app/views/shared/milestones/_tabs.html.haml
+++ b/app/views/shared/milestones/_tabs.html.haml
@@ -3,29 +3,29 @@
.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
.fade-left= icon('angle-left')
.fade-right= icon('angle-right')
- %ul.nav-links.scrolling-tabs.js-milestone-tabs
+ %ul.nav-links.scrolling-tabs.js-milestone-tabs.nav.nav-tabs
- if issues_accessible
- %li.active
- = link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do
+ %li.nav-item
+ = link_to '#tab-issues', class: 'nav-link active', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do
Issues
- %span.badge= milestone.issues_visible_to_user(current_user).size
- %li
- = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-endpoint': milestone_merge_request_tab_path(milestone) do
+ %span.badge.badge-pill= milestone.issues_visible_to_user(current_user).size
+ %li.nav-item
+ = link_to '#tab-merge-requests', class: 'nav-link', 'data-toggle' => 'tab', 'data-endpoint': milestone_merge_request_tab_path(milestone) do
Merge Requests
- %span.badge= milestone.merge_requests.size
+ %span.badge.badge-pill= milestone.merge_requests.size
- else
- %li.active
- = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-endpoint': milestone_merge_request_tab_path(milestone) do
+ %li.nav-item
+ = link_to '#tab-merge-requests', class: 'nav-link active', 'data-toggle' => 'tab', 'data-endpoint': milestone_merge_request_tab_path(milestone) do
Merge Requests
- %span.badge= milestone.merge_requests.size
- %li
- = link_to '#tab-participants', 'data-toggle' => 'tab', 'data-endpoint': milestone_participants_tab_path(milestone) do
+ %span.badge.badge-pill= milestone.merge_requests.size
+ %li.nav-item
+ = link_to '#tab-participants', class: 'nav-link', 'data-toggle' => 'tab', 'data-endpoint': milestone_participants_tab_path(milestone) do
Participants
- %span.badge= milestone.participants.count
- %li
- = link_to '#tab-labels', 'data-toggle' => 'tab', 'data-endpoint': milestone_labels_tab_path(milestone) do
+ %span.badge.badge-pill= milestone.participants.count
+ %li.nav-item
+ = link_to '#tab-labels', class: 'nav-link', 'data-toggle' => 'tab', 'data-endpoint': milestone_labels_tab_path(milestone) do
Labels
- %span.badge= milestone.labels.count
+ %span.badge.badge-pill= milestone.labels.count
- issues = milestone.sorted_issues(current_user)
- show_project_name = local_assigns.fetch(:show_project_name, false)
diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml
index 797ff034bb2..320e3788a0f 100644
--- a/app/views/shared/milestones/_top.html.haml
+++ b/app/views/shared/milestones/_top.html.haml
@@ -5,7 +5,7 @@
- is_dynamic_milestone = milestone.legacy_group_milestone? || milestone.dashboard_milestone?
.detail-page-header
- %a.btn.btn-default.btn-grouped.pull-right.visible-xs-block.js-sidebar-toggle{ href: "#" }
+ %a.btn.btn-default.btn-grouped.float-right.d-block.d-sm-none.js-sidebar-toggle{ href: "#" }
= icon('angle-double-left')
.status-box{ class: "status-box-#{milestone.closed? ? 'closed' : 'open'}" }
@@ -22,7 +22,7 @@
&nbsp;&middot;
= milestone_date_range(milestone)
- if group
- .pull-right
+ .float-right
- if can?(current_user, :admin_milestones, group)
- if milestone.group_milestone?
= link_to edit_group_milestone_path(group, milestone), class: "btn btn btn-grouped" do
@@ -48,6 +48,8 @@
- close_msg = group ? 'You may close the milestone now.' : 'Navigate to the project to close the milestone.'
%span All issues for this milestone are closed. #{close_msg}
+= render_if_exists 'shared/milestones/burndown', milestone: @milestone, project: @project
+
- if is_dynamic_milestone
.table-holder
%table.table
diff --git a/app/views/shared/notes/_comment_button.html.haml b/app/views/shared/notes/_comment_button.html.haml
index 4b9af78bc1a..ed336df4e9d 100644
--- a/app/views/shared/notes/_comment_button.html.haml
+++ b/app/views/shared/notes/_comment_button.html.haml
@@ -1,6 +1,6 @@
- noteable_name = @note.noteable.human_class_name
-.pull-left.btn-group.append-right-10.droplab-dropdown.comment-type-dropdown.js-comment-type-dropdown
+.float-left.btn-group.append-right-10.droplab-dropdown.comment-type-dropdown.js-comment-type-dropdown
%input.btn.btn-nr.btn-create.comment-btn.js-comment-button.js-comment-submit-button{ type: 'submit', value: 'Comment' }
- if @note.can_be_discussion_note?
diff --git a/app/views/shared/notes/_edit_form.html.haml b/app/views/shared/notes/_edit_form.html.haml
index 8923e5602a4..71a5b94e958 100644
--- a/app/views/shared/notes/_edit_form.html.haml
+++ b/app/views/shared/notes/_edit_form.html.haml
@@ -3,7 +3,7 @@
= hidden_field_tag :target_id, '', class: 'js-form-target-id'
= hidden_field_tag :target_type, '', class: 'js-form-target-type'
= render layout: 'projects/md_preview', locals: { url: preview_markdown_path(project), referenced_users: true } do
- = render 'projects/zen', attr: 'note[note]', classes: 'note-textarea js-note-text js-task-list-field', placeholder: "Write a comment or drag your files here..."
+ = render 'projects/zen', attr: 'note[note]', classes: 'note-textarea js-note-text js-task-list-field', placeholder: "Write a comment or drag your files here…"
= render 'shared/notes/hints'
.note-form-actions.clearfix
diff --git a/app/views/shared/notes/_form.html.haml b/app/views/shared/notes/_form.html.haml
index 71c0d740bc8..c360f1ffe2a 100644
--- a/app/views/shared/notes/_form.html.haml
+++ b/app/views/shared/notes/_form.html.haml
@@ -29,7 +29,7 @@
= render 'projects/zen', f: f,
attr: :note,
classes: 'note-textarea js-note-text',
- placeholder: "Write a comment or drag your files here...",
+ placeholder: "Write a comment or drag your files here…",
supports_quick_actions: supports_quick_actions,
supports_autocomplete: supports_autocomplete
= render 'shared/notes/hints', supports_quick_actions: supports_quick_actions
diff --git a/app/views/shared/notes/_hints.html.haml b/app/views/shared/notes/_hints.html.haml
index bc1ac3d8ac2..00eae553279 100644
--- a/app/views/shared/notes/_hints.html.haml
+++ b/app/views/shared/notes/_hints.html.haml
@@ -32,4 +32,4 @@
= icon('file-image-o', class: 'toolbar-button-icon')
Attach a file
- %button.btn.btn-default.btn-xs.hide.button-cancel-uploading-files{ type: 'button' } Cancel
+ %button.btn.btn-default.btn-sm.hide.button-cancel-uploading-files{ type: 'button' } Cancel
diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml
index d4e67b5e7e3..f5464058bc0 100644
--- a/app/views/shared/notes/_note.html.haml
+++ b/app/views/shared/notes/_note.html.haml
@@ -25,7 +25,7 @@
- elsif note_counter == 0
- counter = badge_counter if local_assigns[:badge_counter]
- badge_class = "hidden" if @fresh_discussion || counter.nil?
- %span.badge{ class: badge_class }
+ %span.badge.badge-pill{ class: badge_class }
= counter
.timeline-content
.note-header
@@ -36,8 +36,6 @@
= note.author.to_reference
%span.note-headline-light
%span.note-headline-meta
- - unless note.system
- commented
- if note.system
%span.system-note-message
= markdown_field(note, :note)
@@ -54,14 +52,14 @@
.note-text.md
= markdown_field(note, :note)
= edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago')
- .original-note-content.hidden{ data: { post_url: note_url(note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore } }
+ .original-note-content.hidden{ data: { post_url: note_url(note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore, markdown_version: note.cached_markdown_version } }
#{note.note}
- if note_editable
= render 'shared/notes/edit', note: note
.note-awards
= render 'award_emoji/awards_block', awardable: note, inline: false
- if note.system
- .system-note-commit-list-toggler
+ .system-note-commit-list-toggler.hide
Toggle commit list
%i.fa.fa-angle-down
- if note.attachment.url
diff --git a/app/views/shared/notes/_notes_with_form.html.haml b/app/views/shared/notes/_notes_with_form.html.haml
index 1db7c4e67cf..e0832fd9136 100644
--- a/app/views/shared/notes/_notes_with_form.html.haml
+++ b/app/views/shared/notes/_notes_with_form.html.haml
@@ -1,19 +1,18 @@
- issuable = @issue || @merge_request
- discussion_locked = issuable&.discussion_locked?
-- unless has_vue_discussions_cookie?
- %ul#notes-list.notes.main-notes-list.timeline
- = render "shared/notes/notes"
+%ul#notes-list.notes.main-notes-list.timeline
+ = render "shared/notes/notes"
= render 'shared/notes/edit_form', project: @project
- if can_create_note?
- %ul.notes.notes-form.timeline{ :class => ('hidden' if has_vue_discussions_cookie?) }
+ %ul.notes.notes-form.timeline
%li.timeline-entry
.timeline-entry-inner
.flash-container.timeline-content
- .timeline-icon.hidden-xs.hidden-sm
+ .timeline-icon.d-none.d-sm-none.d-md-block
%a.author_link{ href: user_path(current_user) }
= image_tag avatar_icon_for_user(current_user), alt: current_user.to_reference, class: 'avatar s40'
.timeline-content.timeline-content-form
diff --git a/app/views/shared/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml
index e99d8d0973f..09ddf732ada 100644
--- a/app/views/shared/notifications/_button.html.haml
+++ b/app/views/shared/notifications/_button.html.haml
@@ -6,14 +6,14 @@
.js-notification-toggle-btns
%div{ class: ("btn-group" if notification_setting.custom?) }
- if notification_setting.custom?
- %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting) } }
+ %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } }
= icon("bell", class: "js-notification-loading")
= notification_title(notification_setting.level)
%button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } }
= icon('caret-down')
.sr-only Toggle dropdown
- else
- %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } }
+ %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), display: 'static' } }
= icon("bell", class: "js-notification-loading")
= notification_title(notification_setting.level)
= icon("caret-down")
diff --git a/app/views/shared/notifications/_custom_notifications.html.haml b/app/views/shared/notifications/_custom_notifications.html.haml
index 9186c2ba9c9..1f6e8f98bbb 100644
--- a/app/views/shared/notifications/_custom_notifications.html.haml
+++ b/app/views/shared/notifications/_custom_notifications.html.haml
@@ -2,10 +2,10 @@
.modal-dialog
.modal-content
.modal-header
- %button.close{ type: "button", "aria-label": "close", data: { dismiss: "modal" } }
- %span{ "aria-hidden": "true" } ×
%h4#custom-notifications-title.modal-title
#{ _('Custom notification events') }
+ %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
+ %span{ "aria-hidden": true } &times;
.modal-body
.container-fluid
@@ -22,9 +22,9 @@
- NotificationSetting::EMAIL_EVENTS.each_with_index do |event, index|
- field_id = "#{notifications_menu_identifier("modal", notification_setting)}_notification_setting[#{event}]"
.form-group
- .checkbox{ class: ("prepend-top-0" if index == 0) }
- %label{ for: field_id }
- = check_box("notification_setting", event, id: field_id, class: "js-custom-notification-event", checked: notification_setting.public_send(event))
+ .form-check{ class: ("prepend-top-0" if index == 0) }
+ = check_box("notification_setting", event, id: field_id, class: "js-custom-notification-event form-check-input", checked: notification_setting.public_send(event))
+ %label.form-check-label{ for: field_id }
%strong
= notification_event_name(event)
= icon("spinner spin", class: "custom-notification-event-loading")
diff --git a/app/views/shared/plugins/_index.html.haml b/app/views/shared/plugins/_index.html.haml
index fc643c3ecc2..7bcc54e7459 100644
--- a/app/views/shared/plugins/_index.html.haml
+++ b/app/views/shared/plugins/_index.html.haml
@@ -10,8 +10,8 @@
.col-lg-8.append-bottom-default
- if plugins.any?
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
Plugins (#{plugins.count})
%ul.content-list
- plugins.each do |file|
@@ -19,5 +19,5 @@
.monospace
= File.basename(file)
- else
- %p.light-well.text-center
+ %p.card.bg-light.text-center
No plugins found.
diff --git a/app/views/shared/projects/_dropdown.html.haml b/app/views/shared/projects/_dropdown.html.haml
index 3d917346f6b..98b258d9275 100644
--- a/app/views/shared/projects/_dropdown.html.haml
+++ b/app/views/shared/projects/_dropdown.html.haml
@@ -1,8 +1,8 @@
- @sort ||= sort_value_latest_activity
.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
+ = dropdown_toggle(toggle_text, { toggle: 'dropdown', display: 'static' }, { id: 'sort-projects-dropdown' })
+ %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable
%li.dropdown-header
Sort by
- projects_sort_options_hash.each do |value, title|
diff --git a/app/views/shared/projects/_edit_information.html.haml b/app/views/shared/projects/_edit_information.html.haml
index ec9dc8f62c2..9230e045a81 100644
--- a/app/views/shared/projects/_edit_information.html.haml
+++ b/app/views/shared/projects/_edit_information.html.haml
@@ -1,6 +1,6 @@
- unless can?(current_user, :push_code, @project)
.inline.prepend-left-10
- - if @project.branch_allows_maintainer_push?(current_user, selected_branch)
+ - if @project.branch_allows_collaboration?(current_user, selected_branch)
= commit_in_single_accessible_branch
- else
= commit_in_fork_help
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 0687f6d961d..6be1fb485a4 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -4,7 +4,7 @@
- ci = false unless local_assigns[:ci] == true
- skip_namespace = false unless local_assigns[:skip_namespace] == true
- user = local_assigns[:user]
-- access = user&.max_member_access_for_project(project.id) unless user.nil?
+- access = max_project_member_access(project)
- css_class = '' unless local_assigns[:css_class]
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && can_show_last_commit_in_list?(project)
- css_class += " no-description" if project.description.blank? && !show_last_commit_as_description
@@ -46,7 +46,7 @@
.controls
.prepend-top-0
- if project.archived
- %span.prepend-left-10.label.label-warning archived
+ %span.prepend-left-10.badge.badge-warning archived
- if can?(current_user, :read_cross_project) && project.pipeline_status.has_status?
%span.prepend-left-10
= render_project_pipeline_status(project.pipeline_status)
diff --git a/app/views/shared/runners/_form.html.haml b/app/views/shared/runners/_form.html.haml
index 302a543cf12..0337680d79b 100644
--- a/app/views/shared/runners/_form.html.haml
+++ b/app/views/shared/runners/_form.html.haml
@@ -1,56 +1,56 @@
-= form_for runner, url: runner_form_url, html: { class: 'form-horizontal' } do |f|
+= form_for runner, url: runner_form_url do |f|
= form_errors(runner)
- .form-group
- = label :active, "Active", class: 'control-label'
+ .form-group.row
+ = label :active, "Active", class: 'col-form-label col-sm-2'
.col-sm-10
- .checkbox
- = f.check_box :active
+ .form-check
+ = f.check_box :active, { class: 'form-check-input' }
%span.light Paused Runners don't accept new jobs
- .form-group
- = label :protected, "Protected", class: 'control-label'
+ .form-group.row
+ = label :protected, "Protected", class: 'col-form-label col-sm-2'
.col-sm-10
- .checkbox
- = f.check_box :access_level, {}, 'ref_protected', 'not_protected'
+ .form-check
+ = f.check_box :access_level, { class: 'form-check-input' }, 'ref_protected', 'not_protected'
%span.light This runner will only run on pipelines triggered on protected branches
- .form-group
- = label :run_untagged, 'Run untagged jobs', class: 'control-label'
+ .form-group.row
+ = label :run_untagged, 'Run untagged jobs', class: 'col-form-label col-sm-2'
.col-sm-10
- .checkbox
- = f.check_box :run_untagged
+ .form-check
+ = f.check_box :run_untagged, { class: 'form-check-input' }
%span.light Indicates whether this runner can pick jobs without tags
- unless runner.group_type?
- .form-group
- = label :locked, _('Lock to current projects'), class: 'control-label'
+ .form-group.row
+ = label :locked, _('Lock to current projects'), class: 'col-form-label col-sm-2'
.col-sm-10
- .checkbox
- = f.check_box :locked
+ .form-check
+ = f.check_box :locked, { class: 'form-check-input' }
%span.light= _('When a runner is locked, it cannot be assigned to other projects')
- .form-group
- = label_tag :token, class: 'control-label' do
+ .form-group.row
+ = label_tag :token, class: 'col-form-label col-sm-2' do
Token
.col-sm-10
= f.text_field :token, class: 'form-control', readonly: true
- .form-group
- = label_tag :ip_address, class: 'control-label' do
+ .form-group.row
+ = label_tag :ip_address, class: 'col-form-label col-sm-2' do
IP Address
.col-sm-10
= f.text_field :ip_address, class: 'form-control', readonly: true
- .form-group
- = label_tag :description, class: 'control-label' do
+ .form-group.row
+ = label_tag :description, class: 'col-form-label col-sm-2' do
Description
.col-sm-10
= f.text_field :description, class: 'form-control'
- .form-group
- = label_tag :maximum_timeout_human_readable, class: 'control-label' do
+ .form-group.row
+ = label_tag :maximum_timeout_human_readable, class: 'col-form-label col-sm-2' do
Maximum job timeout
.col-sm-10
= f.text_field :maximum_timeout_human_readable, class: 'form-control'
- .help-block This timeout will take precedence when lower than Project-defined timeout
- .form-group
- = label_tag :tag_list, class: 'control-label' do
+ .form-text.text-muted This timeout will take precedence when lower than Project-defined timeout
+ .form-group.row
+ = label_tag :tag_list, class: 'col-form-label col-sm-2' do
Tags
.col-sm-10
= f.text_field :tag_list, value: runner.tag_list.sort.join(', '), class: 'form-control'
- .help-block You can setup jobs to only use Runners with specific tags. Separate tags with commas.
+ .form-text.text-muted You can setup jobs to only use Runners with specific tags. Separate tags with commas.
.form-actions
= f.submit 'Save changes', class: 'btn btn-save'
diff --git a/app/views/shared/runners/_runner_description.html.haml b/app/views/shared/runners/_runner_description.html.haml
index 1d59c2f7078..da5c032add5 100644
--- a/app/views/shared/runners/_runner_description.html.haml
+++ b/app/views/shared/runners/_runner_description.html.haml
@@ -9,8 +9,8 @@
%div
%ul
%li
- %span.label.label-success active
+ %span.badge.badge-success active
= _('- Runner is active and can process any new jobs')
%li
- %span.label.label-danger paused
+ %span.badge.badge-danger paused
= _('- Runner is paused and will not receive any new jobs')
diff --git a/app/views/shared/runners/show.html.haml b/app/views/shared/runners/show.html.haml
index 93c6ba86640..362569bfbaf 100644
--- a/app/views/shared/runners/show.html.haml
+++ b/app/views/shared/runners/show.html.haml
@@ -2,8 +2,8 @@
%h3.page-title
Runner ##{@runner.id}
- .pull-right
- - if @runner.shared?
+ .float-right
+ - if @runner.instance_type?
%span.runner-state.runner-state-shared
Shared
- elsif @runner.group_type?
@@ -21,22 +21,22 @@
%th Value
%tr
%td Active
- %td= @runner.active? ? _('Yes') : _('No')
+ %td= @runner.active? ? 'Yes' : 'No'
%tr
%td Protected
- %td= @runner.ref_protected? ? _('Yes') : _('No')
+ %td= @runner.active? ? _('Yes') : _('No')
%tr
- %td= _('Can run untagged jobs')
- %td= @runner.run_untagged? ? _('Yes') : _('No')
+ %td Can run untagged jobs
+ %td= @runner.run_untagged? ? 'Yes' : 'No'
- unless @runner.group_type?
%tr
- %td= _('Locked to this project')
- %td= @runner.locked? ? _('Yes') : _('No')
+ %td Locked to this project
+ %td= @runner.locked? ? 'Yes' : 'No'
%tr
%td Tags
%td
- @runner.tag_list.sort.each do |tag|
- %span.label.label-primary
+ %span.badge.badge-primary
= tag
%tr
%td Name
@@ -60,7 +60,7 @@
%td Description
%td= @runner.description
%tr
- %td= _('Maximum job timeout')
+ %td Maximum job timeout
%td= @runner.maximum_timeout_human_readable
%tr
%td Last contact
diff --git a/app/views/shared/snippets/_blob.html.haml b/app/views/shared/snippets/_blob.html.haml
index 11f0fa7c49f..2132fcbccc5 100644
--- a/app/views/shared/snippets/_blob.html.haml
+++ b/app/views/shared/snippets/_blob.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.d-none.d-sm-block
= render 'projects/blob/viewer_switcher', blob: blob
.btn-group{ role: "group" }<
diff --git a/app/views/shared/snippets/_embed.html.haml b/app/views/shared/snippets/_embed.html.haml
index 2d93e51a2d9..36f56fbad1a 100644
--- a/app/views/shared/snippets/_embed.html.haml
+++ b/app/views/shared/snippets/_embed.html.haml
@@ -15,7 +15,7 @@
%span.logo-text
GitLab
- .file-actions.hidden-xs
+ .file-actions.d-none.d-sm-block
.btn-group{ role: "group" }<
= embedded_snippet_raw_button
diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml
index c75c882a693..5e5c050d5c3 100644
--- a/app/views/shared/snippets/_form.html.haml
+++ b/app/views/shared/snippets/_form.html.haml
@@ -2,11 +2,13 @@
= page_specific_javascript_tag('lib/ace.js')
.snippet-form-holder
- = form_for @snippet, url: url, html: { class: "form-horizontal snippet-form js-requires-input js-quick-submit common-note-form" } do |f|
+ = form_for @snippet, url: url,
+ html: { class: "snippet-form js-requires-input js-quick-submit common-note-form" },
+ data: { markdown_version: @snippet.cached_markdown_version } do |f|
= form_errors(@snippet)
- .form-group
- = f.label :title, class: 'control-label'
+ .form-group.row
+ = f.label :title, class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_field :title, class: 'form-control', required: true, autofocus: true
@@ -15,8 +17,8 @@
= render 'shared/visibility_level', f: f, visibility_level: @snippet.visibility_level, can_change_visibility_level: true, form_model: @snippet
.file-editor
- .form-group
- = f.label :file_name, "File", class: 'control-label'
+ .form-group.row
+ = f.label :file_name, "File", class: 'col-form-label col-sm-2'
.col-sm-10
.file-holder.snippet
.js-file-title.file-title
diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml
index 9f55c10d19b..828ec870dc0 100644
--- a/app/views/shared/snippets/_header.html.haml
+++ b/app/views/shared/snippets/_header.html.haml
@@ -7,7 +7,7 @@
%span.creator
Authored
= time_ago_with_tooltip(@snippet.created_at, placement: 'bottom', html_class: 'snippet_updated_ago')
- by #{link_to_member(@project, @snippet.author, size: 24, author_class: "author item-title", avatar_class: "hidden-xs")}
+ by #{link_to_member(@project, @snippet.author, size: 24, author_class: "author item-title", avatar_class: "d-none d-sm-inline")}
.detail-page-header-actions
- if @snippet.project_id?
@@ -32,10 +32,10 @@
- if public_snippet?
.embed-snippet
.input-group
- .input-group-btn
- %button.btn.embed-toggle{ 'data-toggle': 'dropdown', type: 'button' }
+ .input-group-prepend
+ %button.btn.btn-svg.embed-toggle.input-group-text{ 'data-toggle': 'dropdown', type: 'button' }
%span.js-embed-action= _("Embed")
- = sprite_icon('angle-down', size: 12)
+ = sprite_icon('angle-down', size: 12, css_class: 'caret-down')
%ul.dropdown-menu.dropdown-menu-selectable.embed-toggle-list
%li
%button.js-embed-btn.btn.btn-transparent.is-active{ type: 'button' }
@@ -44,7 +44,6 @@
%button.js-share-btn.btn.btn-transparent{ type: 'button' }
%strong.embed-toggle-list-item= _("Share")
%input.js-snippet-url-area.snippet-embed-input.form-control{ type: "text", autocomplete: 'off', value: snippet_embed }
- .input-group-btn
- %button.js-clipboard-btn.snippet-clipboard-btn.btn.btn-default.has-tooltip{ title: "Copy to clipboard", 'data-clipboard-target': '.js-snippet-url-area' }
- = sprite_icon('duplicate', size: 16)
+ .input-group-append
+ = clipboard_button(title: s_('Copy to clipboard'), class: 'js-clipboard-btn snippet-clipboard-btn btn btn-default', target: '.js-snippet-url-area')
.clearfix
diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml
index 3acec88c2e3..5069e2e4ca6 100644
--- a/app/views/shared/snippets/_snippet.html.haml
+++ b/app/views/shared/snippets/_snippet.html.haml
@@ -1,13 +1,13 @@
- link_project = local_assigns.fetch(:link_project, false)
%li.snippet-row
- = image_tag avatar_icon_for_user(snippet.author), class: "avatar s40 hidden-xs", alt: ''
+ = image_tag avatar_icon_for_user(snippet.author), class: "avatar s40 d-none d-sm-block", alt: ''
.title
= link_to reliable_snippet_path(snippet) do
= snippet.title
- if snippet.file_name
- %span.snippet-filename.monospace.hidden-xs
+ %span.snippet-filename.monospace.d-none.d-sm-inline-block
= snippet.file_name
%ul.controls
@@ -28,10 +28,10 @@
= link_to user_snippets_path(snippet.author) do
= snippet.author_name
- if link_project && snippet.project_id?
- %span.hidden-xs
+ %span.d-none.d-sm-inline-block
in
= link_to project_path(snippet.project) do
= snippet.project.full_name
- .pull-right.snippet-updated-at
+ .float-right.snippet-updated-at
%span updated #{time_ago_with_tooltip(snippet.updated_at, placement: 'bottom')}
diff --git a/app/views/shared/tokens/_scopes_form.html.haml b/app/views/shared/tokens/_scopes_form.html.haml
index ae437dd16d6..dcb3fca23f2 100644
--- a/app/views/shared/tokens/_scopes_form.html.haml
+++ b/app/views/shared/tokens/_scopes_form.html.haml
@@ -3,8 +3,7 @@
- token = local_assigns.fetch(:token)
- scopes.each do |scope|
- %fieldset
- = 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]
+ %fieldset.form-group.form-check
+ = check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}", class: 'form-check-input'
+ = label_tag ("#{prefix}_scopes_#{scope}"), scope, class: 'label-light form-check-label'
+ .text-secondary= t scope, scope: [:doorkeeper, :scope_desc]
diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml
index d36ca032558..660769fa50d 100644
--- a/app/views/shared/web_hooks/_form.html.haml
+++ b/app/views/shared/web_hooks/_form.html.haml
@@ -6,84 +6,74 @@
.form-group
= form.label :token, 'Secret Token', class: 'label-light'
= form.text_field :token, class: 'form-control', placeholder: ''
- %p.help-block
+ %p.form-text.text-muted
Use this token to validate received payloads. It will be sent with the request in the X-Gitlab-Token HTTP header.
.form-group
= form.label :url, 'Trigger', class: 'label-light'
- %ul.list-unstyled
+ %ul.list-unstyled.prepend-left-20
%li
- = form.check_box :push_events, class: 'pull-left'
- .prepend-left-20
- = form.label :push_events, class: 'list-label' do
- %strong Push events
- %p.light
- This URL will be triggered by a push to the repository
+ = form.check_box :push_events, class: 'form-check-input'
+ = form.label :push_events, class: 'list-label form-check-label ml-1' do
+ %strong Push events
+ %p.light.ml-1
+ This URL will be triggered by a push to the repository
%li
- = form.check_box :tag_push_events, class: 'pull-left'
- .prepend-left-20
- = form.label :tag_push_events, class: 'list-label' do
- %strong Tag push events
- %p.light
- This URL will be triggered when a new tag is pushed to the repository
+ = form.check_box :tag_push_events, class: 'form-check-input'
+ = form.label :tag_push_events, class: 'list-label form-check-label ml-1' do
+ %strong Tag push events
+ %p.light.ml-1
+ This URL will be triggered when a new tag is pushed to the repository
%li
- = form.check_box :note_events, class: 'pull-left'
- .prepend-left-20
- = form.label :note_events, class: 'list-label' do
- %strong Comments
- %p.light
- This URL will be triggered when someone adds a comment
+ = form.check_box :note_events, class: 'form-check-input'
+ = form.label :note_events, class: 'list-label form-check-label ml-1' do
+ %strong Comments
+ %p.light.ml-1
+ This URL will be triggered when someone adds a comment
%li
- = form.check_box :confidential_note_events, class: 'pull-left'
- .prepend-left-20
- = form.label :confidential_note_events, class: 'list-label' do
- %strong Confidential Comments
- %p.light
- This URL will be triggered when someone adds a comment on a confidential issue
+ = form.check_box :confidential_note_events, class: 'form-check-input'
+ = form.label :confidential_note_events, class: 'list-label form-check-label ml-1' do
+ %strong Confidential Comments
+ %p.light.ml-1
+ This URL will be triggered when someone adds a comment on a confidential issue
%li
- = form.check_box :issues_events, class: 'pull-left'
- .prepend-left-20
- = form.label :issues_events, class: 'list-label' do
- %strong Issues events
- %p.light
- This URL will be triggered when an issue is created/updated/merged
+ = form.check_box :issues_events, class: 'form-check-input'
+ = form.label :issues_events, class: 'list-label form-check-label ml-1' do
+ %strong Issues events
+ %p.light.ml-1
+ This URL will be triggered when an issue is created/updated/merged
%li
- = form.check_box :confidential_issues_events, class: 'pull-left'
- .prepend-left-20
- = form.label :confidential_issues_events, class: 'list-label' do
- %strong Confidential Issues events
- %p.light
- This URL will be triggered when a confidential issue is created/updated/merged
+ = form.check_box :confidential_issues_events, class: 'form-check-input'
+ = form.label :confidential_issues_events, class: 'list-label form-check-label ml-1' do
+ %strong Confidential Issues events
+ %p.light.ml-1
+ This URL will be triggered when a confidential issue is created/updated/merged
%li
- = form.check_box :merge_requests_events, class: 'pull-left'
- .prepend-left-20
- = form.label :merge_requests_events, class: 'list-label' do
- %strong Merge request events
- %p.light
- This URL will be triggered when a merge request is created/updated/merged
+ = form.check_box :merge_requests_events, class: 'form-check-input'
+ = form.label :merge_requests_events, class: 'list-label form-check-label ml-1' do
+ %strong Merge request events
+ %p.light.ml-1
+ This URL will be triggered when a merge request is created/updated/merged
%li
- = form.check_box :job_events, class: 'pull-left'
- .prepend-left-20
- = form.label :job_events, class: 'list-label' do
- %strong Job events
- %p.light
- This URL will be triggered when the job status changes
+ = form.check_box :job_events, class: 'form-check-input'
+ = form.label :job_events, class: 'list-label form-check-label ml-1' do
+ %strong Job events
+ %p.light.ml-1
+ This URL will be triggered when the job status changes
%li
- = form.check_box :pipeline_events, class: 'pull-left'
- .prepend-left-20
- = form.label :pipeline_events, class: 'list-label' do
- %strong Pipeline events
- %p.light
- This URL will be triggered when the pipeline status changes
+ = form.check_box :pipeline_events, class: 'form-check-input'
+ = form.label :pipeline_events, class: 'list-label form-check-label ml-1' do
+ %strong Pipeline events
+ %p.light.ml-1
+ This URL will be triggered when the pipeline status changes
%li
- = form.check_box :wiki_page_events, class: 'pull-left'
- .prepend-left-20
- = form.label :wiki_page_events, class: 'list-label' do
- %strong Wiki Page events
- %p.light
- This URL will be triggered when a wiki page is created/updated
+ = form.check_box :wiki_page_events, class: 'form-check-input'
+ = form.label :wiki_page_events, class: 'list-label form-check-label ml-1' do
+ %strong Wiki Page events
+ %p.light.ml-1
+ This URL will be triggered when a wiki page is created/updated
.form-group
= form.label :enable_ssl_verification, 'SSL verification', class: 'label-light checkbox'
- .checkbox
- = form.label :enable_ssl_verification do
- = form.check_box :enable_ssl_verification
+ .form-check
+ = form.check_box :enable_ssl_verification, class: 'form-check-input'
+ = form.label :enable_ssl_verification, class: 'form-check-label ml-1' do
%strong Enable SSL verification
diff --git a/app/views/shared/web_hooks/_test_button.html.haml b/app/views/shared/web_hooks/_test_button.html.haml
index cf1d5e061c6..5ece8b1d4c7 100644
--- a/app/views/shared/web_hooks/_test_button.html.haml
+++ b/app/views/shared/web_hooks/_test_button.html.haml
@@ -6,7 +6,7 @@
%button.btn{ 'data-toggle' => 'dropdown', class: button_class }
Test
= icon('caret-down')
- %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' }
+ %ul.dropdown-menu.dropdown-menu-right{ role: 'menu' }
- triggers.each_value do |event|
%li
= link_to_test_hook(hook, event)
diff --git a/app/views/sherlock/file_samples/show.html.haml b/app/views/sherlock/file_samples/show.html.haml
index 1a6e2542dc1..7255d352775 100644
--- a/app/views/sherlock/file_samples/show.html.haml
+++ b/app/views/sherlock/file_samples/show.html.haml
@@ -4,7 +4,7 @@
- header_title t('sherlock.title'), sherlock_transactions_path
.row-content-block
- .pull-right
+ .float-right
= link_to(sherlock_transaction_path(@transaction), class: 'btn') do
%i.fa.fa-arrow-left
= t('sherlock.transaction')
diff --git a/app/views/sherlock/queries/_backtrace.html.haml b/app/views/sherlock/queries/_backtrace.html.haml
index 30e956e5f40..38b4d2c6102 100644
--- a/app/views/sherlock/queries/_backtrace.html.haml
+++ b/app/views/sherlock/queries/_backtrace.html.haml
@@ -1,9 +1,9 @@
.prepend-top-default
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
%strong
= t('sherlock.application_backtrace')
- %ul.well-list
+ %ul.content-list
- @query.application_backtrace.each do |location|
%li
%strong
@@ -15,11 +15,11 @@
= t('sherlock.line')
= location.line
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
%strong
= t('sherlock.full_backtrace')
- %ul.well-list
+ %ul.content-list
- @query.backtrace.each do |location|
%li
- if location.application?
diff --git a/app/views/sherlock/queries/_general.html.haml b/app/views/sherlock/queries/_general.html.haml
index 5a447f791dc..37747faed62 100644
--- a/app/views/sherlock/queries/_general.html.haml
+++ b/app/views/sherlock/queries/_general.html.haml
@@ -1,9 +1,9 @@
.prepend-top-default
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
%strong
= t('sherlock.general')
- %ul.well-list
+ %ul.content-list
%li
%span.light
#{t('sherlock.time')}:
@@ -23,31 +23,31 @@
= t('sherlock.line')
= frame.line
- .panel.panel-default
- .panel-heading
- .pull-right
- %button.js-clipboard-trigger.btn.btn-xs{ title: t('sherlock.copy_to_clipboard'), type: :button }
+ .card
+ .card-header
+ .float-right
+ %button.js-clipboard-trigger.btn.btn-sm{ title: t('sherlock.copy_to_clipboard'), type: :button }
%i.fa.fa-clipboard
%pre.hidden
= @query.formatted_query
%strong
= t('sherlock.query')
- %ul.well-list
+ %ul.content-list
%li
.code.js-syntax-highlight.sherlock-code
:preserve
#{highlight("#{@query.id}.sql", @query.formatted_query)}
- .panel.panel-default
- .panel-heading
- .pull-right
- %button.js-clipboard-trigger.btn.btn-xs{ title: t('sherlock.copy_to_clipboard'), type: :button }
+ .card
+ .card-header
+ .float-right
+ %button.js-clipboard-trigger.btn.btn-sm{ title: t('sherlock.copy_to_clipboard'), type: :button }
%i.fa.fa-clipboard
%pre.hidden
= @query.explain
%strong
= t('sherlock.query_plan')
- %ul.well-list
+ %ul.content-list
%li
.code.js-syntax-highlight.sherlock-code
%pre
diff --git a/app/views/sherlock/queries/show.html.haml b/app/views/sherlock/queries/show.html.haml
index c45da6ee9a4..413130a2907 100644
--- a/app/views/sherlock/queries/show.html.haml
+++ b/app/views/sherlock/queries/show.html.haml
@@ -1,7 +1,7 @@
- page_title t('sherlock.title'), t('sherlock.transaction'), t('sherlock.query')
- header_title t('sherlock.title'), sherlock_transactions_path
-%ul.nav-links
+%ul.nav-links.nav.nav-tabs
%li.active
%a{ href: "#tab-general", data: { toggle: "tab" } }
= t('sherlock.general')
@@ -10,7 +10,7 @@
= t('sherlock.backtrace')
.row-content-block
- .pull-right
+ .float-right
= link_to(sherlock_transaction_path(@transaction), class: 'btn') do
%i.fa.fa-arrow-left
= t('sherlock.transaction')
diff --git a/app/views/sherlock/transactions/_file_samples.html.haml b/app/views/sherlock/transactions/_file_samples.html.haml
index 4349c9b7ace..5b3448605f2 100644
--- a/app/views/sherlock/transactions/_file_samples.html.haml
+++ b/app/views/sherlock/transactions/_file_samples.html.haml
@@ -21,4 +21,4 @@
%td
= link_to(t('sherlock.view'),
sherlock_transaction_file_sample_path(@transaction, sample),
- class: 'btn btn-xs')
+ class: 'btn btn-sm')
diff --git a/app/views/sherlock/transactions/_general.html.haml b/app/views/sherlock/transactions/_general.html.haml
index a37fb5d449a..9c028b5c741 100644
--- a/app/views/sherlock/transactions/_general.html.haml
+++ b/app/views/sherlock/transactions/_general.html.haml
@@ -1,9 +1,9 @@
.prepend-top-default
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
%strong
= t('sherlock.general')
- %ul.well-list
+ %ul.content-list
%li
%span.light
#{t('sherlock.id')}:
diff --git a/app/views/sherlock/transactions/_queries.html.haml b/app/views/sherlock/transactions/_queries.html.haml
index b8d93e9ff45..c1ec4b91bb6 100644
--- a/app/views/sherlock/transactions/_queries.html.haml
+++ b/app/views/sherlock/transactions/_queries.html.haml
@@ -21,4 +21,4 @@
%td
= link_to(t('sherlock.view'),
sherlock_transaction_query_path(@transaction, query),
- class: 'btn btn-xs')
+ class: 'btn btn-sm')
diff --git a/app/views/sherlock/transactions/index.html.haml b/app/views/sherlock/transactions/index.html.haml
index 6ed7e9e21a6..4d9df01ae31 100644
--- a/app/views/sherlock/transactions/index.html.haml
+++ b/app/views/sherlock/transactions/index.html.haml
@@ -2,7 +2,7 @@
- header_title t('sherlock.title'), sherlock_transactions_path
.row-content-block
- .pull-right
+ .float-right
= link_to(destroy_all_sherlock_transactions_path,
class: 'btn btn-danger',
method: :delete) do
@@ -37,5 +37,5 @@
%td
= time_ago_with_tooltip trans.finished_at
%td
- = link_to(sherlock_transaction_path(trans), class: 'btn btn-xs') do
+ = link_to(sherlock_transaction_path(trans), class: 'btn btn-sm') do
= t('sherlock.view')
diff --git a/app/views/sherlock/transactions/show.html.haml b/app/views/sherlock/transactions/show.html.haml
index eab91e8fbe4..565b337d446 100644
--- a/app/views/sherlock/transactions/show.html.haml
+++ b/app/views/sherlock/transactions/show.html.haml
@@ -1,23 +1,23 @@
- page_title t('sherlock.title'), t('sherlock.transaction')
- header_title t('sherlock.title'), sherlock_transactions_path
-%ul.nav-links
+%ul.nav-links.nav.nav-tabs
%li.active
%a{ href: "#tab-general", data: { toggle: "tab" } }
= t('sherlock.general')
%li
%a{ href: "#tab-queries", data: { toggle: "tab" } }
= t('sherlock.queries')
- %span.badge
+ %span.badge.badge-pill
#{@transaction.queries.length}
%li
%a{ href: "#tab-file-samples", data: { toggle: "tab" } }
= t('sherlock.file_samples')
- %span.badge
+ %span.badge.badge-pill
#{@transaction.file_samples.length}
.row-content-block
- .pull-right
+ .float-right
= link_to(sherlock_transactions_path, class: 'btn') do
%i.fa.fa-arrow-left
= t('sherlock.all_transactions')
diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml
index a7f118d3f7d..ae69d0d07c7 100644
--- a/app/views/snippets/_actions.html.haml
+++ b/app/views/snippets/_actions.html.haml
@@ -1,6 +1,6 @@
- return unless current_user
-.hidden-xs
+.d-none.d-sm-block
- if can?(current_user, :update_personal_snippet, @snippet)
= link_to edit_snippet_path(@snippet), class: "btn btn-grouped" do
Edit
@@ -11,7 +11,7 @@
New snippet
- if @snippet.submittable_as_spam_by?(current_user)
= link_to 'Submit as spam', mark_as_spam_snippet_path(@snippet), method: :post, class: 'btn btn-grouped btn-spam', title: 'Submit as spam'
-.visible-xs-block.dropdown
+.d-block.d-sm-none.dropdown
%button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
Options
= icon('caret-down')
diff --git a/app/views/snippets/_snippets_scope_menu.html.haml b/app/views/snippets/_snippets_scope_menu.html.haml
index 65aa4fbc757..dc4b0fd9ba0 100644
--- a/app/views/snippets/_snippets_scope_menu.html.haml
+++ b/app/views/snippets/_snippets_scope_menu.html.haml
@@ -1,11 +1,11 @@
- subject = local_assigns.fetch(:subject, current_user)
- include_private = local_assigns.fetch(:include_private, false)
-.nav-links.snippet-scope-menu.mobile-separator
+.nav-links.snippet-scope-menu.mobile-separator.nav.nav-tabs
%li{ class: active_when(params[:scope].nil?) }
= link_to subject_snippets_path(subject) do
All
- %span.badge
+ %span.badge.badge-pill
- if include_private
= subject.snippets.count
- else
@@ -15,17 +15,17 @@
%li{ class: active_when(params[:scope] == "are_private") }
= link_to subject_snippets_path(subject, scope: 'are_private') do
Private
- %span.badge
+ %span.badge.badge-pill
= subject.snippets.are_private.count
%li{ class: active_when(params[:scope] == "are_internal") }
= link_to subject_snippets_path(subject, scope: 'are_internal') do
Internal
- %span.badge
+ %span.badge.badge-pill
= subject.snippets.are_internal.count
%li{ class: active_when(params[:scope] == "are_public") }
= link_to subject_snippets_path(subject, scope: 'are_public') do
Public
- %span.badge
+ %span.badge.badge-pill
= subject.snippets.are_public.count
diff --git a/app/views/snippets/index.html.haml b/app/views/snippets/index.html.haml
index 7e4918a6085..9b4a7dbe68d 100644
--- a/app/views/snippets/index.html.haml
+++ b/app/views/snippets/index.html.haml
@@ -1,12 +1,12 @@
- page_title "By #{@user.name}", "Snippets"
%ol.breadcrumb
- %li
+ %li.breadcrumb-item
= link_to snippets_path do
Snippets
- %li
+ %li.breadcrumb-item
= @user.name
- .pull-right.hidden-xs
+ .float-right.d-none.d-sm-block
= link_to user_path(@user) do
#{@user.name} profile page
diff --git a/app/views/u2f/_authenticate.html.haml b/app/views/u2f/_authenticate.html.haml
index 7eb221620ad..1c788b9a737 100644
--- a/app/views/u2f/_authenticate.html.haml
+++ b/app/views/u2f/_authenticate.html.haml
@@ -2,9 +2,6 @@
%a.btn.btn-block.btn-info#js-login-2fa-device{ href: '#' } Sign in via 2FA code
-# haml-lint:disable InlineJavaScript
-%script#js-authenticate-u2f-not-supported{ type: "text/template" }
- %p Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer).
-
%script#js-authenticate-u2f-in-progress{ type: "text/template" }
%p Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now.
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index fb909237b9a..b2ec7166832 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -81,7 +81,7 @@
.scrolling-tabs-container
.fade-left= icon('angle-left')
.fade-right= icon('angle-right')
- %ul.nav-links.user-profile-nav.scrolling-tabs
+ %ul.nav-links.user-profile-nav.scrolling-tabs.nav.nav-tabs
- if profile_tab?(:activity)
%li.js-activity-tab
= link_to user_path, data: { target: 'div#activity', action: 'activity', toggle: 'tab' } do
@@ -107,7 +107,7 @@
.tab-content
- if profile_tab?(:activity)
#activity.tab-pane
- .row-content-block.calender-block.white.second-block.hidden-xs
+ .row-content-block.calender-block.white.second-block.d-none.d-sm-block
.user-calendar{ data: { calendar_path: user_calendar_path(@user, :json), calendar_activities_path: user_calendar_activities_path, utc_offset: Time.zone.utc_offset } }
%h4.center.light
%i.fa.fa-spinner.fa-spin
diff --git a/app/views/users/terms/index.html.haml b/app/views/users/terms/index.html.haml
index c5406696bdd..a869cf9cdee 100644
--- a/app/views/users/terms/index.html.haml
+++ b/app/views/users/terms/index.html.haml
@@ -1,13 +1,18 @@
- redirect_params = { redirect: @redirect } if @redirect
-.panel-content.rendered-terms
+.card-body.rendered-terms
= markdown_field(@term, :terms)
-.row-content-block.footer-block.clearfix
- - if can?(current_user, :accept_terms, @term)
- .pull-right
- = button_to accept_term_path(@term, redirect_params), class: 'btn btn-success prepend-left-8' do
- = _('Accept terms')
- - if can?(current_user, :decline_terms, @term)
- .pull-right
- = button_to decline_term_path(@term, redirect_params), class: 'btn btn-default prepend-left-8' do
- = _('Decline and sign out')
+- if current_user
+ .card-footer.footer-block.clearfix
+ - if can?(current_user, :accept_terms, @term)
+ .float-right
+ = button_to accept_term_path(@term, redirect_params), class: 'btn btn-success prepend-left-8' do
+ = _('Accept terms')
+ - else
+ .pull-right
+ = link_to root_path, class: 'btn btn-success prepend-left-8' do
+ = _('Continue')
+ - if can?(current_user, :decline_terms, @term)
+ .float-right
+ = button_to decline_term_path(@term, redirect_params), class: 'btn btn-default prepend-left-8' do
+ = _('Decline and sign out')
diff --git a/app/workers/admin_email_worker.rb b/app/workers/admin_email_worker.rb
index 044e470141e..06324575ffc 100644
--- a/app/workers/admin_email_worker.rb
+++ b/app/workers/admin_email_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AdminEmailWorker
include ApplicationWorker
include CronjobQueue
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index b6433eb3eff..d4be1ccfcfa 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -11,39 +11,41 @@
- cronjob:remove_old_web_hook_logs
- cronjob:remove_unreferenced_lfs_objects
- cronjob:repository_archive_cache
-- cronjob:repository_check_batch
+- cronjob:repository_check_dispatch
- cronjob:requests_profiles
- cronjob:schedule_update_user_activity
- cronjob:stuck_ci_jobs
- cronjob:stuck_import_jobs
- cronjob:stuck_merge_jobs
+- cronjob:ci_archive_traces_cron
- cronjob:trending_projects
- cronjob:issue_due_scheduler
+- cronjob:prune_web_hook_logs
- gcp_cluster:cluster_install_app
- gcp_cluster:cluster_provision
- gcp_cluster:cluster_wait_for_app_installation
- gcp_cluster:wait_for_cluster_creation
-- gcp_cluster:check_gcp_project_billing
- gcp_cluster:cluster_wait_for_ingress_ip_address
- github_import_advance_stage
- github_importer:github_import_import_diff_note
- github_importer:github_import_import_issue
- github_importer:github_import_import_note
+- github_importer:github_import_import_lfs_object
- github_importer:github_import_import_pull_request
- github_importer:github_import_refresh_import_jid
- github_importer:github_import_stage_finish_import
- github_importer:github_import_stage_import_base_data
- github_importer:github_import_stage_import_issues_and_diff_notes
- github_importer:github_import_stage_import_notes
+- github_importer:github_import_stage_import_lfs_objects
- github_importer:github_import_stage_import_pull_requests
- github_importer:github_import_stage_import_repository
- mail_scheduler:mail_scheduler_issue_due
- mail_scheduler:mail_scheduler_notification_service
-- object_storage_upload
- object_storage:object_storage_background_move
- object_storage:object_storage_migrate_uploads
@@ -69,6 +71,7 @@
- pipeline_processing:update_head_pipeline_for_merge_request
- repository_check:repository_check_clear
+- repository_check:repository_check_batch
- repository_check:repository_check_single_repository
- default
@@ -115,3 +118,5 @@
- upload_checksum
- web_hook
- repository_update_remote_mirror
+- create_note_diff_file
+- delete_diff_files
diff --git a/app/workers/archive_trace_worker.rb b/app/workers/archive_trace_worker.rb
index dea7425ad88..c6f89a17729 100644
--- a/app/workers/archive_trace_worker.rb
+++ b/app/workers/archive_trace_worker.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
class ArchiveTraceWorker
include ApplicationWorker
include PipelineBackgroundQueue
def perform(job_id)
- Ci::Build.find_by(id: job_id).try do |job|
+ Ci::Build.without_archived_trace.find_by(id: job_id).try do |job|
job.trace.archive!
end
end
diff --git a/app/workers/authorized_projects_worker.rb b/app/workers/authorized_projects_worker.rb
index 8fe3619f6ee..dd62bb0f33d 100644
--- a/app/workers/authorized_projects_worker.rb
+++ b/app/workers/authorized_projects_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AuthorizedProjectsWorker
include ApplicationWorker
prepend WaitableWorker
diff --git a/app/workers/background_migration_worker.rb b/app/workers/background_migration_worker.rb
index 376703f6319..eaec7d48f35 100644
--- a/app/workers/background_migration_worker.rb
+++ b/app/workers/background_migration_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class BackgroundMigrationWorker
include ApplicationWorker
diff --git a/app/workers/build_coverage_worker.rb b/app/workers/build_coverage_worker.rb
index 62b212c79be..53d77dc4524 100644
--- a/app/workers/build_coverage_worker.rb
+++ b/app/workers/build_coverage_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class BuildCoverageWorker
include ApplicationWorker
include PipelineQueue
diff --git a/app/workers/build_finished_worker.rb b/app/workers/build_finished_worker.rb
index 46f1ac09915..9dc2c7f3601 100644
--- a/app/workers/build_finished_worker.rb
+++ b/app/workers/build_finished_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class BuildFinishedWorker
include ApplicationWorker
include PipelineQueue
diff --git a/app/workers/build_hooks_worker.rb b/app/workers/build_hooks_worker.rb
index cbfca8c342c..f1f71dc589c 100644
--- a/app/workers/build_hooks_worker.rb
+++ b/app/workers/build_hooks_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class BuildHooksWorker
include ApplicationWorker
include PipelineQueue
diff --git a/app/workers/build_queue_worker.rb b/app/workers/build_queue_worker.rb
index e4f4e6c1d9e..1b3f1fd3c2a 100644
--- a/app/workers/build_queue_worker.rb
+++ b/app/workers/build_queue_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class BuildQueueWorker
include ApplicationWorker
include PipelineQueue
diff --git a/app/workers/build_success_worker.rb b/app/workers/build_success_worker.rb
index 4b9097bc5e4..e1c1cc24a94 100644
--- a/app/workers/build_success_worker.rb
+++ b/app/workers/build_success_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class BuildSuccessWorker
include ApplicationWorker
include PipelineQueue
diff --git a/app/workers/build_trace_sections_worker.rb b/app/workers/build_trace_sections_worker.rb
index c0f5c144e10..f4114b3353c 100644
--- a/app/workers/build_trace_sections_worker.rb
+++ b/app/workers/build_trace_sections_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class BuildTraceSectionsWorker
include ApplicationWorker
include PipelineQueue
diff --git a/app/workers/check_gcp_project_billing_worker.rb b/app/workers/check_gcp_project_billing_worker.rb
deleted file mode 100644
index 363f81590ab..00000000000
--- a/app/workers/check_gcp_project_billing_worker.rb
+++ /dev/null
@@ -1,92 +0,0 @@
-require 'securerandom'
-
-class CheckGcpProjectBillingWorker
- include ApplicationWorker
- include ClusterQueue
-
- LEASE_TIMEOUT = 3.seconds.to_i
- SESSION_KEY_TIMEOUT = 5.minutes
- BILLING_TIMEOUT = 1.hour
- BILLING_CHANGED_LABELS = { state_transition: nil }.freeze
-
- def self.get_session_token(token_key)
- Gitlab::Redis::SharedState.with do |redis|
- redis.get(get_redis_session_key(token_key))
- end
- end
-
- def self.store_session_token(token)
- generate_token_key.tap do |token_key|
- Gitlab::Redis::SharedState.with do |redis|
- redis.set(get_redis_session_key(token_key), token, ex: SESSION_KEY_TIMEOUT)
- end
- end
- end
-
- def self.get_billing_state(token)
- Gitlab::Redis::SharedState.with do |redis|
- value = redis.get(redis_shared_state_key_for(token))
- ActiveRecord::Type::Boolean.new.type_cast_from_user(value)
- end
- end
-
- def perform(token_key)
- return unless token_key
-
- token = self.class.get_session_token(token_key)
- return unless token
- return unless try_obtain_lease_for(token)
-
- billing_enabled_state = !CheckGcpProjectBillingService.new.execute(token).empty?
- update_billing_change_counter(self.class.get_billing_state(token), billing_enabled_state)
- self.class.set_billing_state(token, billing_enabled_state)
- end
-
- private
-
- def self.generate_token_key
- SecureRandom.uuid
- end
-
- def self.get_redis_session_key(token_key)
- "gitlab:gcp:session:#{token_key}"
- end
-
- def self.redis_shared_state_key_for(token)
- "gitlab:gcp:#{Digest::SHA1.hexdigest(token)}:billing_enabled"
- end
-
- def self.set_billing_state(token, value)
- Gitlab::Redis::SharedState.with do |redis|
- redis.set(redis_shared_state_key_for(token), value, ex: BILLING_TIMEOUT)
- end
- end
-
- def try_obtain_lease_for(token)
- Gitlab::ExclusiveLease
- .new("check_gcp_project_billing_worker:#{token.hash}", timeout: LEASE_TIMEOUT)
- .try_obtain
- end
-
- def billing_changed_counter
- @billing_changed_counter ||= Gitlab::Metrics.counter(
- :gcp_billing_change_count,
- "Counts the number of times a GCP project changed billing_enabled state from false to true",
- BILLING_CHANGED_LABELS
- )
- end
-
- def state_transition(previous_state, current_state)
- if previous_state.nil? && !current_state
- 'no_billing'
- elsif previous_state.nil? && current_state
- 'with_billing'
- elsif !previous_state && current_state
- 'billing_configured'
- end
- end
-
- def update_billing_change_counter(previous_state, current_state)
- billing_changed_counter.increment(state_transition: state_transition(previous_state, current_state))
- end
-end
diff --git a/app/workers/ci/archive_traces_cron_worker.rb b/app/workers/ci/archive_traces_cron_worker.rb
new file mode 100644
index 00000000000..7d4e9660a4e
--- /dev/null
+++ b/app/workers/ci/archive_traces_cron_worker.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Ci
+ class ArchiveTracesCronWorker
+ include ApplicationWorker
+ include CronjobQueue
+
+ def perform
+ # Archive stale live traces which still resides in redis or database
+ # This could happen when ArchiveTraceWorker sidekiq jobs were lost by receiving SIGKILL
+ # More details in https://gitlab.com/gitlab-org/gitlab-ce/issues/36791
+ Ci::Build.finished.with_live_trace.find_each(batch_size: 100) do |build|
+ begin
+ build.trace.archive!
+ rescue ::Gitlab::Ci::Trace::AlreadyArchivedError
+ rescue => e
+ failed_archive_counter.increment
+ Rails.logger.error "Failed to archive stale live trace. id: #{build.id} message: #{e.message}"
+ end
+ end
+ end
+
+ private
+
+ def failed_archive_counter
+ @failed_archive_counter ||= Gitlab::Metrics.counter(:job_trace_archive_failed_total, "Counter of failed attempts of traces archiving")
+ end
+ end
+end
diff --git a/app/workers/ci/build_trace_chunk_flush_worker.rb b/app/workers/ci/build_trace_chunk_flush_worker.rb
index 218d6688bd9..9dbf2e5e1ac 100644
--- a/app/workers/ci/build_trace_chunk_flush_worker.rb
+++ b/app/workers/ci/build_trace_chunk_flush_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Ci
class BuildTraceChunkFlushWorker
include ApplicationWorker
@@ -5,7 +7,7 @@ module Ci
def perform(build_trace_chunk_id)
::Ci::BuildTraceChunk.find_by(id: build_trace_chunk_id).try do |build_trace_chunk|
- build_trace_chunk.use_database!
+ build_trace_chunk.persist_data!
end
end
end
diff --git a/app/workers/cluster_install_app_worker.rb b/app/workers/cluster_install_app_worker.rb
index f771cb4939f..32e2ea7996c 100644
--- a/app/workers/cluster_install_app_worker.rb
+++ b/app/workers/cluster_install_app_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ClusterInstallAppWorker
include ApplicationWorker
include ClusterQueue
diff --git a/app/workers/cluster_provision_worker.rb b/app/workers/cluster_provision_worker.rb
index 1ab4de3b647..59de7903c1c 100644
--- a/app/workers/cluster_provision_worker.rb
+++ b/app/workers/cluster_provision_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ClusterProvisionWorker
include ApplicationWorker
include ClusterQueue
diff --git a/app/workers/cluster_wait_for_app_installation_worker.rb b/app/workers/cluster_wait_for_app_installation_worker.rb
index d564d5e48bf..e8d7e52f70f 100644
--- a/app/workers/cluster_wait_for_app_installation_worker.rb
+++ b/app/workers/cluster_wait_for_app_installation_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ClusterWaitForAppInstallationWorker
include ApplicationWorker
include ClusterQueue
diff --git a/app/workers/cluster_wait_for_ingress_ip_address_worker.rb b/app/workers/cluster_wait_for_ingress_ip_address_worker.rb
index 8ba5951750c..6865384df44 100644
--- a/app/workers/cluster_wait_for_ingress_ip_address_worker.rb
+++ b/app/workers/cluster_wait_for_ingress_ip_address_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ClusterWaitForIngressIpAddressWorker
include ApplicationWorker
include ClusterQueue
diff --git a/app/workers/concerns/application_worker.rb b/app/workers/concerns/application_worker.rb
index 37586e161c9..bb06e31641d 100644
--- a/app/workers/concerns/application_worker.rb
+++ b/app/workers/concerns/application_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
Sidekiq::Worker.extend ActiveSupport::Concern
module ApplicationWorker
diff --git a/app/workers/concerns/cluster_applications.rb b/app/workers/concerns/cluster_applications.rb
index 24ecaa0b52f..9758a1ceb0e 100644
--- a/app/workers/concerns/cluster_applications.rb
+++ b/app/workers/concerns/cluster_applications.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ClusterApplications
extend ActiveSupport::Concern
diff --git a/app/workers/concerns/cluster_queue.rb b/app/workers/concerns/cluster_queue.rb
index 24b9f145220..e44b40c36c9 100644
--- a/app/workers/concerns/cluster_queue.rb
+++ b/app/workers/concerns/cluster_queue.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
##
# Concern for setting Sidekiq settings for the various Gcp clusters workers.
#
diff --git a/app/workers/concerns/cronjob_queue.rb b/app/workers/concerns/cronjob_queue.rb
index b6581779f6a..0683b229381 100644
--- a/app/workers/concerns/cronjob_queue.rb
+++ b/app/workers/concerns/cronjob_queue.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Concern that sets various Sidekiq settings for workers executed using a
# cronjob.
module CronjobQueue
diff --git a/app/workers/concerns/each_shard_worker.rb b/app/workers/concerns/each_shard_worker.rb
new file mode 100644
index 00000000000..d0a728fb495
--- /dev/null
+++ b/app/workers/concerns/each_shard_worker.rb
@@ -0,0 +1,31 @@
+module EachShardWorker
+ extend ActiveSupport::Concern
+ include ::Gitlab::Utils::StrongMemoize
+
+ def each_eligible_shard
+ Gitlab::ShardHealthCache.update(eligible_shard_names)
+
+ eligible_shard_names.each do |shard_name|
+ yield shard_name
+ end
+ end
+
+ # override when you want to filter out some shards
+ def eligible_shard_names
+ healthy_shard_names
+ end
+
+ def healthy_shard_names
+ strong_memoize(:healthy_shard_names) do
+ healthy_ready_shards.map { |result| result.labels[:shard] }
+ end
+ end
+
+ def healthy_ready_shards
+ ready_shards.select(&:success)
+ end
+
+ def ready_shards
+ Gitlab::HealthChecks::GitalyCheck.readiness
+ end
+end
diff --git a/app/workers/concerns/exception_backtrace.rb b/app/workers/concerns/exception_backtrace.rb
index ea0f1f8d19b..37c9eaba0d7 100644
--- a/app/workers/concerns/exception_backtrace.rb
+++ b/app/workers/concerns/exception_backtrace.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Concern for enabling a few lines of exception backtraces in Sidekiq
module ExceptionBacktrace
extend ActiveSupport::Concern
diff --git a/app/workers/concerns/gitlab/github_import/queue.rb b/app/workers/concerns/gitlab/github_import/queue.rb
index 22c2ce458e8..59b621f16ab 100644
--- a/app/workers/concerns/gitlab/github_import/queue.rb
+++ b/app/workers/concerns/gitlab/github_import/queue.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module GithubImport
module Queue
diff --git a/app/workers/concerns/mail_scheduler_queue.rb b/app/workers/concerns/mail_scheduler_queue.rb
index f3e9680d756..c051151e973 100644
--- a/app/workers/concerns/mail_scheduler_queue.rb
+++ b/app/workers/concerns/mail_scheduler_queue.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module MailSchedulerQueue
extend ActiveSupport::Concern
diff --git a/app/workers/concerns/new_issuable.rb b/app/workers/concerns/new_issuable.rb
index 526ed0bad07..7735dec5e6b 100644
--- a/app/workers/concerns/new_issuable.rb
+++ b/app/workers/concerns/new_issuable.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module NewIssuable
attr_reader :issuable, :user
diff --git a/app/workers/concerns/object_storage_queue.rb b/app/workers/concerns/object_storage_queue.rb
index a80f473a6d4..8650eed213a 100644
--- a/app/workers/concerns/object_storage_queue.rb
+++ b/app/workers/concerns/object_storage_queue.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Concern for setting Sidekiq settings for the various GitLab ObjectStorage workers.
module ObjectStorageQueue
extend ActiveSupport::Concern
diff --git a/app/workers/concerns/pipeline_background_queue.rb b/app/workers/concerns/pipeline_background_queue.rb
index 8bf43de6b26..bbb8ad0c982 100644
--- a/app/workers/concerns/pipeline_background_queue.rb
+++ b/app/workers/concerns/pipeline_background_queue.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
##
# Concern for setting Sidekiq settings for the low priority CI pipeline workers.
#
diff --git a/app/workers/concerns/pipeline_queue.rb b/app/workers/concerns/pipeline_queue.rb
index e77093a6902..3aaed4669e5 100644
--- a/app/workers/concerns/pipeline_queue.rb
+++ b/app/workers/concerns/pipeline_queue.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
##
# Concern for setting Sidekiq settings for the various CI pipeline workers.
#
diff --git a/app/workers/concerns/project_import_options.rb b/app/workers/concerns/project_import_options.rb
index ef23990ad97..22bdf441d6b 100644
--- a/app/workers/concerns/project_import_options.rb
+++ b/app/workers/concerns/project_import_options.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ProjectImportOptions
extend ActiveSupport::Concern
diff --git a/app/workers/concerns/project_start_import.rb b/app/workers/concerns/project_start_import.rb
index 4e55a1ee3d6..46a133db2a1 100644
--- a/app/workers/concerns/project_start_import.rb
+++ b/app/workers/concerns/project_start_import.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Used in EE by mirroring
module ProjectStartImport
def start(project)
diff --git a/app/workers/concerns/repository_check_queue.rb b/app/workers/concerns/repository_check_queue.rb
index 43fb66c31b0..216d67e5dbc 100644
--- a/app/workers/concerns/repository_check_queue.rb
+++ b/app/workers/concerns/repository_check_queue.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Concern for setting Sidekiq settings for the various repository check workers.
module RepositoryCheckQueue
extend ActiveSupport::Concern
diff --git a/app/workers/concerns/waitable_worker.rb b/app/workers/concerns/waitable_worker.rb
index 48ebe862248..d85bc7d1660 100644
--- a/app/workers/concerns/waitable_worker.rb
+++ b/app/workers/concerns/waitable_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module WaitableWorker
extend ActiveSupport::Concern
diff --git a/app/workers/create_gpg_signature_worker.rb b/app/workers/create_gpg_signature_worker.rb
index f371731f68c..a2da1bda11f 100644
--- a/app/workers/create_gpg_signature_worker.rb
+++ b/app/workers/create_gpg_signature_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CreateGpgSignatureWorker
include ApplicationWorker
diff --git a/app/workers/create_note_diff_file_worker.rb b/app/workers/create_note_diff_file_worker.rb
new file mode 100644
index 00000000000..0850250f7e3
--- /dev/null
+++ b/app/workers/create_note_diff_file_worker.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class CreateNoteDiffFileWorker
+ include ApplicationWorker
+
+ def perform(diff_note_id)
+ diff_note = DiffNote.find(diff_note_id)
+
+ diff_note.create_diff_file
+ end
+end
diff --git a/app/workers/create_pipeline_worker.rb b/app/workers/create_pipeline_worker.rb
index c3ac35e54f5..037b4a57d4b 100644
--- a/app/workers/create_pipeline_worker.rb
+++ b/app/workers/create_pipeline_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CreatePipelineWorker
include ApplicationWorker
include PipelineQueue
diff --git a/app/workers/delete_diff_files_worker.rb b/app/workers/delete_diff_files_worker.rb
new file mode 100644
index 00000000000..bb8fbb9c373
--- /dev/null
+++ b/app/workers/delete_diff_files_worker.rb
@@ -0,0 +1,17 @@
+class DeleteDiffFilesWorker
+ include ApplicationWorker
+
+ def perform(merge_request_diff_id)
+ merge_request_diff = MergeRequestDiff.find(merge_request_diff_id)
+
+ return if merge_request_diff.without_files?
+
+ MergeRequestDiff.transaction do
+ merge_request_diff.clean!
+
+ MergeRequestDiffFile
+ .where(merge_request_diff_id: merge_request_diff.id)
+ .delete_all
+ end
+ end
+end
diff --git a/app/workers/delete_merged_branches_worker.rb b/app/workers/delete_merged_branches_worker.rb
index 07cd1f02fb5..017d7fd1cb0 100644
--- a/app/workers/delete_merged_branches_worker.rb
+++ b/app/workers/delete_merged_branches_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class DeleteMergedBranchesWorker
include ApplicationWorker
diff --git a/app/workers/delete_user_worker.rb b/app/workers/delete_user_worker.rb
index 6c431b02979..4d0295f8d2e 100644
--- a/app/workers/delete_user_worker.rb
+++ b/app/workers/delete_user_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class DeleteUserWorker
include ApplicationWorker
diff --git a/app/workers/email_receiver_worker.rb b/app/workers/email_receiver_worker.rb
index dd8a6cbbef1..12706613ac2 100644
--- a/app/workers/email_receiver_worker.rb
+++ b/app/workers/email_receiver_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class EmailReceiverWorker
include ApplicationWorker
@@ -13,14 +15,14 @@ class EmailReceiverWorker
private
- def handle_failure(raw, e)
- Rails.logger.warn("Email can not be processed: #{e}\n\n#{raw}")
+ def handle_failure(raw, error)
+ Rails.logger.warn("Email can not be processed: #{error}\n\n#{raw}")
return unless raw.present?
can_retry = false
reason =
- case e
+ case error
when Gitlab::Email::UnknownIncomingEmail
"We couldn't figure out what the email is for. Please create your issue or comment through the web interface."
when Gitlab::Email::SentNotificationNotFoundError
@@ -40,7 +42,7 @@ class EmailReceiverWorker
"The thread you are replying to no longer exists, perhaps it was deleted? If you believe this is in error, contact a staff member."
when Gitlab::Email::InvalidRecordError
can_retry = true
- e.message
+ error.message
end
if reason
diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb
index 2a4d65b5cb3..8d0cfc73ccd 100644
--- a/app/workers/emails_on_push_worker.rb
+++ b/app/workers/emails_on_push_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class EmailsOnPushWorker
include ApplicationWorker
diff --git a/app/workers/expire_build_artifacts_worker.rb b/app/workers/expire_build_artifacts_worker.rb
index 87e5dca01fd..5d3a9a39b93 100644
--- a/app/workers/expire_build_artifacts_worker.rb
+++ b/app/workers/expire_build_artifacts_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ExpireBuildArtifactsWorker
include ApplicationWorker
include CronjobQueue
diff --git a/app/workers/expire_build_instance_artifacts_worker.rb b/app/workers/expire_build_instance_artifacts_worker.rb
index 234b4357cf7..3b57ecb36e3 100644
--- a/app/workers/expire_build_instance_artifacts_worker.rb
+++ b/app/workers/expire_build_instance_artifacts_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ExpireBuildInstanceArtifactsWorker
include ApplicationWorker
diff --git a/app/workers/expire_job_cache_worker.rb b/app/workers/expire_job_cache_worker.rb
index 7217364a9f2..14a57b90114 100644
--- a/app/workers/expire_job_cache_worker.rb
+++ b/app/workers/expire_job_cache_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ExpireJobCacheWorker
include ApplicationWorker
include PipelineQueue
diff --git a/app/workers/expire_pipeline_cache_worker.rb b/app/workers/expire_pipeline_cache_worker.rb
index db73d37868a..992fc63c451 100644
--- a/app/workers/expire_pipeline_cache_worker.rb
+++ b/app/workers/expire_pipeline_cache_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ExpirePipelineCacheWorker
include ApplicationWorker
include PipelineQueue
diff --git a/app/workers/git_garbage_collect_worker.rb b/app/workers/git_garbage_collect_worker.rb
index be4203bc7ad..2d381c6fd6c 100644
--- a/app/workers/git_garbage_collect_worker.rb
+++ b/app/workers/git_garbage_collect_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class GitGarbageCollectWorker
include ApplicationWorker
@@ -6,12 +8,6 @@ class GitGarbageCollectWorker
# Timeout set to 24h
LEASE_TIMEOUT = 86400
- GITALY_MIGRATED_TASKS = {
- gc: :garbage_collect,
- full_repack: :repack_full,
- incremental_repack: :repack_incremental
- }.freeze
-
def perform(project_id, task = :gc, lease_key = nil, lease_uuid = nil)
project = Project.find(project_id)
active_uuid = get_lease_uuid(lease_key)
@@ -27,21 +23,7 @@ class GitGarbageCollectWorker
end
task = task.to_sym
- cmd = command(task)
-
- gitaly_migrate(GITALY_MIGRATED_TASKS[task]) do |is_enabled|
- if is_enabled
- gitaly_call(task, project.repository.raw_repository)
- else
- repo_path = project.repository.path_to_repo
- description = "'#{cmd.join(' ')}' in #{repo_path}"
- Gitlab::GitLogger.info(description)
-
- output, status = Gitlab::Popen.popen(cmd, repo_path)
-
- Gitlab::GitLogger.error("#{description} failed:\n#{output}") unless status.zero?
- end
- end
+ gitaly_call(task, project.repository.raw_repository)
# Refresh the branch cache in case garbage collection caused a ref lookup to fail
flush_ref_caches(project) if task == :gc
@@ -82,21 +64,12 @@ class GitGarbageCollectWorker
when :incremental_repack
client.repack_incremental
end
- end
-
- def command(task)
- case task
- when :gc
- git(write_bitmaps: bitmaps_enabled?) + %w[gc]
- when :full_repack
- git(write_bitmaps: bitmaps_enabled?) + %w[repack -A -d --pack-kept-objects]
- when :incremental_repack
- # Normal git repack fails when bitmaps are enabled. It is impossible to
- # create a bitmap here anyway.
- git(write_bitmaps: false) + %w[repack -d]
- else
- raise "Invalid gc task: #{task.inspect}"
- end
+ rescue GRPC::NotFound => e
+ Gitlab::GitLogger.error("#{__method__} failed:\nRepository not found")
+ raise Gitlab::Git::Repository::NoRepository.new(e)
+ rescue GRPC::BadStatus => e
+ Gitlab::GitLogger.error("#{__method__} failed:\n#{e}")
+ raise Gitlab::Git::CommandError.new(e)
end
def flush_ref_caches(project)
@@ -108,19 +81,4 @@ class GitGarbageCollectWorker
def bitmaps_enabled?
Gitlab::CurrentSettings.housekeeping_bitmaps_enabled
end
-
- def git(write_bitmaps:)
- config_value = write_bitmaps ? 'true' : 'false'
- %W[git -c repack.writeBitmaps=#{config_value}]
- end
-
- def gitaly_migrate(method, &block)
- Gitlab::GitalyClient.migrate(method, &block)
- rescue GRPC::NotFound => e
- Gitlab::GitLogger.error("#{method} failed:\nRepository not found")
- raise Gitlab::Git::Repository::NoRepository.new(e)
- rescue GRPC::BadStatus => e
- Gitlab::GitLogger.error("#{method} failed:\n#{e}")
- raise Gitlab::Git::CommandError.new(e)
- end
end
diff --git a/app/workers/gitlab/github_import/advance_stage_worker.rb b/app/workers/gitlab/github_import/advance_stage_worker.rb
index 8d708e15a66..be0b6c180b0 100644
--- a/app/workers/gitlab/github_import/advance_stage_worker.rb
+++ b/app/workers/gitlab/github_import/advance_stage_worker.rb
@@ -21,6 +21,7 @@ module Gitlab
STAGES = {
issues_and_diff_notes: Stage::ImportIssuesAndDiffNotesWorker,
notes: Stage::ImportNotesWorker,
+ lfs_objects: Stage::ImportLfsObjectsWorker,
finish: Stage::FinishImportWorker
}.freeze
diff --git a/app/workers/gitlab/github_import/import_lfs_object_worker.rb b/app/workers/gitlab/github_import/import_lfs_object_worker.rb
new file mode 100644
index 00000000000..520c5cb091a
--- /dev/null
+++ b/app/workers/gitlab/github_import/import_lfs_object_worker.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ class ImportLfsObjectWorker
+ include ObjectImporter
+
+ def representation_class
+ Representation::LfsObject
+ end
+
+ def importer_class
+ Importer::LfsObjectImporter
+ end
+
+ def counter_name
+ :github_importer_imported_lfs_objects
+ end
+
+ def counter_description
+ 'The number of imported GitHub Lfs Objects'
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/github_import/stage/import_lfs_objects_worker.rb b/app/workers/gitlab/github_import/stage/import_lfs_objects_worker.rb
new file mode 100644
index 00000000000..29257603a9d
--- /dev/null
+++ b/app/workers/gitlab/github_import/stage/import_lfs_objects_worker.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Stage
+ class ImportLfsObjectsWorker
+ include ApplicationWorker
+ include GithubImport::Queue
+ include StageMethods
+
+ def perform(project_id)
+ return unless (project = find_project(project_id))
+
+ import(project)
+ end
+
+ # project - An instance of Project.
+ def import(project)
+ waiter = Importer::LfsObjectsImporter
+ .new(project, nil)
+ .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_notes_worker.rb b/app/workers/gitlab/github_import/stage/import_notes_worker.rb
index 5f4678a595f..ccf0013180d 100644
--- a/app/workers/gitlab/github_import/stage/import_notes_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_notes_worker.rb
@@ -18,7 +18,7 @@ module Gitlab
AdvanceStageWorker.perform_async(
project.id,
{ waiter.key => waiter.jobs_remaining },
- :finish
+ :lfs_objects
)
end
end
diff --git a/app/workers/gitlab_shell_worker.rb b/app/workers/gitlab_shell_worker.rb
index a0028e41332..0e4d40acc5c 100644
--- a/app/workers/gitlab_shell_worker.rb
+++ b/app/workers/gitlab_shell_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class GitlabShellWorker
include ApplicationWorker
include Gitlab::ShellAdapter
diff --git a/app/workers/gitlab_usage_ping_worker.rb b/app/workers/gitlab_usage_ping_worker.rb
index 6dd281b1147..b75e724ca98 100644
--- a/app/workers/gitlab_usage_ping_worker.rb
+++ b/app/workers/gitlab_usage_ping_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class GitlabUsagePingWorker
LEASE_TIMEOUT = 86400
diff --git a/app/workers/group_destroy_worker.rb b/app/workers/group_destroy_worker.rb
index 509bd09dc2e..b4a3ddcae51 100644
--- a/app/workers/group_destroy_worker.rb
+++ b/app/workers/group_destroy_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class GroupDestroyWorker
include ApplicationWorker
include ExceptionBacktrace
diff --git a/app/workers/import_export_project_cleanup_worker.rb b/app/workers/import_export_project_cleanup_worker.rb
index 9788c8df3a3..da3debdeede 100644
--- a/app/workers/import_export_project_cleanup_worker.rb
+++ b/app/workers/import_export_project_cleanup_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ImportExportProjectCleanupWorker
include ApplicationWorker
include CronjobQueue
diff --git a/app/workers/invalid_gpg_signature_update_worker.rb b/app/workers/invalid_gpg_signature_update_worker.rb
index 6774ab307c6..4724ab7ad98 100644
--- a/app/workers/invalid_gpg_signature_update_worker.rb
+++ b/app/workers/invalid_gpg_signature_update_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class InvalidGpgSignatureUpdateWorker
include ApplicationWorker
diff --git a/app/workers/irker_worker.rb b/app/workers/irker_worker.rb
index 9ae5456be4c..29631c6b7ac 100644
--- a/app/workers/irker_worker.rb
+++ b/app/workers/irker_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'json'
require 'socket'
@@ -69,8 +71,8 @@ class IrkerWorker
newbranch = "#{Gitlab.config.gitlab.url}/#{repo_path}/branches"
newbranch = "\x0302\x1f#{newbranch}\x0f" if @colors
- privmsg = "[#{repo_name}] #{committer} has created a new branch "
- privmsg += "#{branch}: #{newbranch}"
+ privmsg = "[#{repo_name}] #{committer} has created a new branch " \
+ "#{branch}: #{newbranch}"
sendtoirker privmsg
end
@@ -112,9 +114,7 @@ class IrkerWorker
url = compare_url data, project.full_path
commits = colorize_commits data['total_commits_count']
- new_commits = 'new commit'
- new_commits += 's' if data['total_commits_count'] > 1
-
+ new_commits = 'new commit'.pluralize(data['total_commits_count'])
sendtoirker "[#{repo}] #{committer} pushed #{commits} #{new_commits} " \
"to #{branch}: #{url}"
end
@@ -122,8 +122,8 @@ class IrkerWorker
def compare_url(data, repo_path)
sha1 = Commit.truncate_sha(data['before'])
sha2 = Commit.truncate_sha(data['after'])
- compare_url = "#{Gitlab.config.gitlab.url}/#{repo_path}/compare"
- compare_url += "/#{sha1}...#{sha2}"
+ compare_url = "#{Gitlab.config.gitlab.url}/#{repo_path}/compare" \
+ "/#{sha1}...#{sha2}"
colorize_url compare_url
end
@@ -144,8 +144,7 @@ class IrkerWorker
def files_count(commit)
diff_size = commit.raw_deltas.size
- files = "#{diff_size} file"
- files += 's' if diff_size > 1
+ files = "#{diff_size} file".pluralize(diff_size)
files
end
diff --git a/app/workers/issue_due_scheduler_worker.rb b/app/workers/issue_due_scheduler_worker.rb
index 16ab5d069e0..c04a2d75e0b 100644
--- a/app/workers/issue_due_scheduler_worker.rb
+++ b/app/workers/issue_due_scheduler_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class IssueDueSchedulerWorker
include ApplicationWorker
include CronjobQueue
diff --git a/app/workers/mail_scheduler/issue_due_worker.rb b/app/workers/mail_scheduler/issue_due_worker.rb
index 54285884a52..8794ad7a82c 100644
--- a/app/workers/mail_scheduler/issue_due_worker.rb
+++ b/app/workers/mail_scheduler/issue_due_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module MailScheduler
class IssueDueWorker
include ApplicationWorker
diff --git a/app/workers/mail_scheduler/notification_service_worker.rb b/app/workers/mail_scheduler/notification_service_worker.rb
index 7cfe0aa0df1..4726e416182 100644
--- a/app/workers/mail_scheduler/notification_service_worker.rb
+++ b/app/workers/mail_scheduler/notification_service_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'active_job/arguments'
module MailScheduler
diff --git a/app/workers/merge_worker.rb b/app/workers/merge_worker.rb
index ba832fe30c6..ee864b733cd 100644
--- a/app/workers/merge_worker.rb
+++ b/app/workers/merge_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class MergeWorker
include ApplicationWorker
diff --git a/app/workers/namespaceless_project_destroy_worker.rb b/app/workers/namespaceless_project_destroy_worker.rb
index adb25c2a170..d9df42c9e17 100644
--- a/app/workers/namespaceless_project_destroy_worker.rb
+++ b/app/workers/namespaceless_project_destroy_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Worker to destroy projects that do not have a namespace
#
# It destroys everything it can without having the info about the namespace it
diff --git a/app/workers/new_issue_worker.rb b/app/workers/new_issue_worker.rb
index 3bc030f9c62..85b53973f56 100644
--- a/app/workers/new_issue_worker.rb
+++ b/app/workers/new_issue_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class NewIssueWorker
include ApplicationWorker
include NewIssuable
diff --git a/app/workers/new_merge_request_worker.rb b/app/workers/new_merge_request_worker.rb
index bda2a0ab59d..5d8b8904502 100644
--- a/app/workers/new_merge_request_worker.rb
+++ b/app/workers/new_merge_request_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class NewMergeRequestWorker
include ApplicationWorker
include NewIssuable
diff --git a/app/workers/new_note_worker.rb b/app/workers/new_note_worker.rb
index 67c54fbf10e..74f34dcf9aa 100644
--- a/app/workers/new_note_worker.rb
+++ b/app/workers/new_note_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class NewNoteWorker
include ApplicationWorker
diff --git a/app/workers/object_storage/background_move_worker.rb b/app/workers/object_storage/background_move_worker.rb
index 9c4d72e0ecf..8dff65e46e3 100644
--- a/app/workers/object_storage/background_move_worker.rb
+++ b/app/workers/object_storage/background_move_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ObjectStorage
class BackgroundMoveWorker
include ApplicationWorker
diff --git a/app/workers/object_storage/migrate_uploads_worker.rb b/app/workers/object_storage/migrate_uploads_worker.rb
index a3ecfa8e711..01d03ec7888 100644
--- a/app/workers/object_storage/migrate_uploads_worker.rb
+++ b/app/workers/object_storage/migrate_uploads_worker.rb
@@ -1,6 +1,4 @@
# frozen_string_literal: true
-# rubocop:disable Metrics/LineLength
-# rubocop:disable Style/Documentation
module ObjectStorage
class MigrateUploadsWorker
diff --git a/app/workers/object_storage_upload_worker.rb b/app/workers/object_storage_upload_worker.rb
deleted file mode 100644
index 5c80f34069c..00000000000
--- a/app/workers/object_storage_upload_worker.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# @Deprecated - remove once the `object_storage_upload` queue is empty
-# The queue has been renamed `object_storage:object_storage_background_upload`
-#
-class ObjectStorageUploadWorker
- include ApplicationWorker
-
- sidekiq_options retry: 5
-
- def perform(uploader_class_name, subject_class_name, file_field, subject_id)
- uploader_class = uploader_class_name.constantize
- subject_class = subject_class_name.constantize
-
- return unless uploader_class < ObjectStorage::Concern
- return unless uploader_class.object_store_enabled?
- return unless uploader_class.background_upload_enabled?
-
- subject = subject_class.find(subject_id)
- uploader = subject.public_send(file_field) # rubocop:disable GitlabSecurity/PublicSend
- uploader.migrate!(ObjectStorage::Store::REMOTE)
- end
-end
diff --git a/app/workers/pages_domain_verification_cron_worker.rb b/app/workers/pages_domain_verification_cron_worker.rb
index a3ff4bd2101..92d62a15aee 100644
--- a/app/workers/pages_domain_verification_cron_worker.rb
+++ b/app/workers/pages_domain_verification_cron_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class PagesDomainVerificationCronWorker
include ApplicationWorker
include CronjobQueue
diff --git a/app/workers/pages_domain_verification_worker.rb b/app/workers/pages_domain_verification_worker.rb
index 2e93489113c..4610b688189 100644
--- a/app/workers/pages_domain_verification_worker.rb
+++ b/app/workers/pages_domain_verification_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class PagesDomainVerificationWorker
include ApplicationWorker
diff --git a/app/workers/pages_worker.rb b/app/workers/pages_worker.rb
index 66a0ff83bef..13a6576a301 100644
--- a/app/workers/pages_worker.rb
+++ b/app/workers/pages_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class PagesWorker
include ApplicationWorker
diff --git a/app/workers/pipeline_hooks_worker.rb b/app/workers/pipeline_hooks_worker.rb
index c94918ff4ee..58023e0af1b 100644
--- a/app/workers/pipeline_hooks_worker.rb
+++ b/app/workers/pipeline_hooks_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class PipelineHooksWorker
include ApplicationWorker
include PipelineQueue
diff --git a/app/workers/pipeline_metrics_worker.rb b/app/workers/pipeline_metrics_worker.rb
index d46d1f122fc..a97019b100a 100644
--- a/app/workers/pipeline_metrics_worker.rb
+++ b/app/workers/pipeline_metrics_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class PipelineMetricsWorker
include ApplicationWorker
include PipelineQueue
diff --git a/app/workers/pipeline_notification_worker.rb b/app/workers/pipeline_notification_worker.rb
index a9a1168a6e3..3a8846b3747 100644
--- a/app/workers/pipeline_notification_worker.rb
+++ b/app/workers/pipeline_notification_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class PipelineNotificationWorker
include ApplicationWorker
include PipelineQueue
diff --git a/app/workers/pipeline_process_worker.rb b/app/workers/pipeline_process_worker.rb
index 24424b3f472..83744c5338a 100644
--- a/app/workers/pipeline_process_worker.rb
+++ b/app/workers/pipeline_process_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class PipelineProcessWorker
include ApplicationWorker
include PipelineQueue
diff --git a/app/workers/pipeline_schedule_worker.rb b/app/workers/pipeline_schedule_worker.rb
index c49758878a4..a1815757735 100644
--- a/app/workers/pipeline_schedule_worker.rb
+++ b/app/workers/pipeline_schedule_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class PipelineScheduleWorker
include ApplicationWorker
include CronjobQueue
diff --git a/app/workers/pipeline_success_worker.rb b/app/workers/pipeline_success_worker.rb
index 2ab0739a17f..68e9af6a619 100644
--- a/app/workers/pipeline_success_worker.rb
+++ b/app/workers/pipeline_success_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class PipelineSuccessWorker
include ApplicationWorker
include PipelineQueue
diff --git a/app/workers/pipeline_update_worker.rb b/app/workers/pipeline_update_worker.rb
index fc9da2d45b1..c33468c1f14 100644
--- a/app/workers/pipeline_update_worker.rb
+++ b/app/workers/pipeline_update_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class PipelineUpdateWorker
include ApplicationWorker
include PipelineQueue
diff --git a/app/workers/plugin_worker.rb b/app/workers/plugin_worker.rb
index bfcc683d99a..c293e28be4a 100644
--- a/app/workers/plugin_worker.rb
+++ b/app/workers/plugin_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class PluginWorker
include ApplicationWorker
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index f88b3fdbfb1..09a594cdb4e 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class PostReceive
include ApplicationWorker
diff --git a/app/workers/process_commit_worker.rb b/app/workers/process_commit_worker.rb
index 201e7f332b4..c9f6df9b56d 100644
--- a/app/workers/process_commit_worker.rb
+++ b/app/workers/process_commit_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Worker for processing individiual commit messages pushed to a repository.
#
# Jobs for this worker are scheduled for every commit that is being pushed. As a
@@ -77,9 +79,10 @@ class ProcessCommitWorker
# Avoid reprocessing commits that already exist in the upstream
# when project is forked. This will also prevent duplicated system notes.
def commit_exists_in_upstream?(project, commit_hash)
- return false unless project.forked?
+ upstream_project = project.fork_source
+
+ return false unless upstream_project
- upstream_project = project.forked_from_project
commit_id = commit_hash.with_indifferent_access[:id]
upstream_project.commit(commit_id).present?
end
diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb
index a993b4b2680..b0e1d8837d9 100644
--- a/app/workers/project_cache_worker.rb
+++ b/app/workers/project_cache_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Worker for updating any project specific caches.
class ProjectCacheWorker
include ApplicationWorker
diff --git a/app/workers/project_destroy_worker.rb b/app/workers/project_destroy_worker.rb
index 1ba854ca4cb..4447e867240 100644
--- a/app/workers/project_destroy_worker.rb
+++ b/app/workers/project_destroy_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ProjectDestroyWorker
include ApplicationWorker
include ExceptionBacktrace
diff --git a/app/workers/project_export_worker.rb b/app/workers/project_export_worker.rb
index c3d84bb0b93..ed9da39c7c3 100644
--- a/app/workers/project_export_worker.rb
+++ b/app/workers/project_export_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ProjectExportWorker
include ApplicationWorker
include ExceptionBacktrace
diff --git a/app/workers/project_migrate_hashed_storage_worker.rb b/app/workers/project_migrate_hashed_storage_worker.rb
index d01eb744e5d..9e4d66250a4 100644
--- a/app/workers/project_migrate_hashed_storage_worker.rb
+++ b/app/workers/project_migrate_hashed_storage_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ProjectMigrateHashedStorageWorker
include ApplicationWorker
diff --git a/app/workers/project_service_worker.rb b/app/workers/project_service_worker.rb
index 75c4b8b3663..a0bc9288cf0 100644
--- a/app/workers/project_service_worker.rb
+++ b/app/workers/project_service_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ProjectServiceWorker
include ApplicationWorker
diff --git a/app/workers/propagate_service_template_worker.rb b/app/workers/propagate_service_template_worker.rb
index 635a97c99af..c9da1cae255 100644
--- a/app/workers/propagate_service_template_worker.rb
+++ b/app/workers/propagate_service_template_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Worker for updating any project specific caches.
class PropagateServiceTemplateWorker
include ApplicationWorker
diff --git a/app/workers/prune_old_events_worker.rb b/app/workers/prune_old_events_worker.rb
index 5ff62ab1369..c1d05ebbcfd 100644
--- a/app/workers/prune_old_events_worker.rb
+++ b/app/workers/prune_old_events_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class PruneOldEventsWorker
include ApplicationWorker
include CronjobQueue
diff --git a/app/workers/prune_web_hook_logs_worker.rb b/app/workers/prune_web_hook_logs_worker.rb
new file mode 100644
index 00000000000..45c7d32f7eb
--- /dev/null
+++ b/app/workers/prune_web_hook_logs_worker.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+# Worker that deletes a fixed number of outdated rows from the "web_hook_logs"
+# table.
+class PruneWebHookLogsWorker
+ include ApplicationWorker
+ include CronjobQueue
+
+ # The maximum number of rows to remove in a single job.
+ DELETE_LIMIT = 50_000
+
+ def perform
+ # MySQL doesn't allow "DELETE FROM ... WHERE id IN ( ... )" if the inner
+ # query refers to the same table. To work around this we wrap the IN body in
+ # another sub query.
+ WebHookLog
+ .where(
+ 'id IN (SELECT id FROM (?) ids_to_remove)',
+ WebHookLog
+ .select(:id)
+ .where('created_at < ?', 90.days.ago.beginning_of_day)
+ .limit(DELETE_LIMIT)
+ )
+ .delete_all
+ end
+end
diff --git a/app/workers/reactive_caching_worker.rb b/app/workers/reactive_caching_worker.rb
index ef3ddb9024b..9b331f15dc5 100644
--- a/app/workers/reactive_caching_worker.rb
+++ b/app/workers/reactive_caching_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ReactiveCachingWorker
include ApplicationWorker
diff --git a/app/workers/rebase_worker.rb b/app/workers/rebase_worker.rb
index 090987778a2..a6baebc1443 100644
--- a/app/workers/rebase_worker.rb
+++ b/app/workers/rebase_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class RebaseWorker
include ApplicationWorker
diff --git a/app/workers/remove_expired_group_links_worker.rb b/app/workers/remove_expired_group_links_worker.rb
index 7e64c3070a8..6b8b972a440 100644
--- a/app/workers/remove_expired_group_links_worker.rb
+++ b/app/workers/remove_expired_group_links_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class RemoveExpiredGroupLinksWorker
include ApplicationWorker
include CronjobQueue
diff --git a/app/workers/remove_expired_members_worker.rb b/app/workers/remove_expired_members_worker.rb
index 68960f72bf6..41913900571 100644
--- a/app/workers/remove_expired_members_worker.rb
+++ b/app/workers/remove_expired_members_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class RemoveExpiredMembersWorker
include ApplicationWorker
include CronjobQueue
diff --git a/app/workers/remove_old_web_hook_logs_worker.rb b/app/workers/remove_old_web_hook_logs_worker.rb
index 87fed42d7ce..17140ac4450 100644
--- a/app/workers/remove_old_web_hook_logs_worker.rb
+++ b/app/workers/remove_old_web_hook_logs_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class RemoveOldWebHookLogsWorker
include ApplicationWorker
include CronjobQueue
diff --git a/app/workers/remove_unreferenced_lfs_objects_worker.rb b/app/workers/remove_unreferenced_lfs_objects_worker.rb
index 8daf079fc31..95e7a9f537f 100644
--- a/app/workers/remove_unreferenced_lfs_objects_worker.rb
+++ b/app/workers/remove_unreferenced_lfs_objects_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class RemoveUnreferencedLfsObjectsWorker
include ApplicationWorker
include CronjobQueue
diff --git a/app/workers/repository_archive_cache_worker.rb b/app/workers/repository_archive_cache_worker.rb
index 86a258cf94f..c1dff8ced90 100644
--- a/app/workers/repository_archive_cache_worker.rb
+++ b/app/workers/repository_archive_cache_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class RepositoryArchiveCacheWorker
include ApplicationWorker
include CronjobQueue
diff --git a/app/workers/repository_check/batch_worker.rb b/app/workers/repository_check/batch_worker.rb
index 72f0a9b0619..07559ea479b 100644
--- a/app/workers/repository_check/batch_worker.rb
+++ b/app/workers/repository_check/batch_worker.rb
@@ -1,14 +1,37 @@
+# frozen_string_literal: true
+
module RepositoryCheck
class BatchWorker
include ApplicationWorker
- include CronjobQueue
+ include RepositoryCheckQueue
+ include ExclusiveLeaseGuard
RUN_TIME = 3600
BATCH_SIZE = 10_000
+ LEASE_TIMEOUT = 1.hour
+
+ attr_reader :shard_name
+
+ def perform(shard_name)
+ @shard_name = shard_name
- def perform
return unless Gitlab::CurrentSettings.repository_checks_enabled
+ return unless Gitlab::ShardHealthCache.healthy_shard?(shard_name)
+
+ try_obtain_lease do
+ perform_repository_checks
+ end
+ end
+ def lease_timeout
+ LEASE_TIMEOUT
+ end
+
+ def lease_key
+ "repository_check_batch_worker:#{shard_name}"
+ end
+
+ def perform_repository_checks
start = Time.now
# This loop will break after a little more than one hour ('a little
@@ -19,7 +42,7 @@ module RepositoryCheck
project_ids.each do |project_id|
break if Time.now - start >= RUN_TIME
- next unless try_obtain_lease(project_id)
+ next unless try_obtain_lease_for_project(project_id)
SingleRepositoryWorker.new.perform(project_id)
end
@@ -37,19 +60,23 @@ module RepositoryCheck
end
def never_checked_project_ids(batch_size)
- Project.where(last_repository_check_at: nil)
+ projects_on_shard.where(last_repository_check_at: nil)
.where('created_at < ?', 24.hours.ago)
.limit(batch_size).pluck(:id)
end
def old_checked_project_ids(batch_size)
- Project.where.not(last_repository_check_at: nil)
+ projects_on_shard.where.not(last_repository_check_at: nil)
.where('last_repository_check_at < ?', 1.month.ago)
.reorder(last_repository_check_at: :asc)
.limit(batch_size).pluck(:id)
end
- def try_obtain_lease(id)
+ def projects_on_shard
+ Project.where(repository_storage: shard_name)
+ end
+
+ def try_obtain_lease_for_project(id)
# Use a 24-hour timeout because on servers/projects where 'git fsck' is
# super slow we definitely do not want to run it twice in parallel.
Gitlab::ExclusiveLease.new(
diff --git a/app/workers/repository_check/clear_worker.rb b/app/workers/repository_check/clear_worker.rb
index 97b89dc3db5..81e1a4b63bb 100644
--- a/app/workers/repository_check/clear_worker.rb
+++ b/app/workers/repository_check/clear_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module RepositoryCheck
class ClearWorker
include ApplicationWorker
diff --git a/app/workers/repository_check/dispatch_worker.rb b/app/workers/repository_check/dispatch_worker.rb
new file mode 100644
index 00000000000..96634f09a15
--- /dev/null
+++ b/app/workers/repository_check/dispatch_worker.rb
@@ -0,0 +1,24 @@
+module RepositoryCheck
+ class DispatchWorker
+ include ApplicationWorker
+ include CronjobQueue
+ include ::EachShardWorker
+ include ExclusiveLeaseGuard
+
+ LEASE_TIMEOUT = 1.hour
+
+ def perform
+ return unless Gitlab::CurrentSettings.repository_checks_enabled
+
+ try_obtain_lease do
+ each_eligible_shard do |shard_name|
+ RepositoryCheck::BatchWorker.perform_async(shard_name)
+ end
+ end
+ end
+
+ def lease_timeout
+ LEASE_TIMEOUT
+ end
+ end
+end
diff --git a/app/workers/repository_check/single_repository_worker.rb b/app/workers/repository_check/single_repository_worker.rb
index 3cffb8b14e4..f44e5693b25 100644
--- a/app/workers/repository_check/single_repository_worker.rb
+++ b/app/workers/repository_check/single_repository_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module RepositoryCheck
class SingleRepositoryWorker
include ApplicationWorker
diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb
index 08b1c3a7d7a..5ef9b744db3 100644
--- a/app/workers/repository_fork_worker.rb
+++ b/app/workers/repository_fork_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class RepositoryForkWorker
include ApplicationWorker
include Gitlab::ShellAdapter
@@ -8,27 +10,12 @@ class RepositoryForkWorker
target_project_id = args.shift
target_project = Project.find(target_project_id)
- # By v10.8, we should've drained the queue of all jobs using the old arguments.
- # We can remove the else clause if we're no longer logging the message in that clause.
- # See https://gitlab.com/gitlab-org/gitaly/issues/1110
- if args.empty?
- source_project = target_project.forked_from_project
- unless source_project
- return target_project.mark_import_as_failed('Source project cannot be found.')
- end
-
- fork_repository(target_project, source_project.repository_storage, source_project.disk_path)
- else
- Rails.logger.info("Project #{target_project.id} is being forked using old-style arguments.")
-
- source_repository_storage_path, source_disk_path = *args
-
- source_repository_storage_name = Gitlab.config.repositories.storages.find do |_, info|
- info.legacy_disk_path == source_repository_storage_path
- end&.first || raise("no shard found for path '#{source_repository_storage_path}'")
-
- fork_repository(target_project, source_repository_storage_name, source_disk_path)
+ source_project = target_project.forked_from_project
+ unless source_project
+ return target_project.mark_import_as_failed('Source project cannot be found.')
end
+
+ fork_repository(target_project, source_project.repository_storage, source_project.disk_path)
end
private
diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb
index d79b5ee5346..25fec542ac7 100644
--- a/app/workers/repository_import_worker.rb
+++ b/app/workers/repository_import_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class RepositoryImportWorker
include ApplicationWorker
include ExceptionBacktrace
diff --git a/app/workers/repository_remove_remote_worker.rb b/app/workers/repository_remove_remote_worker.rb
index 1c19b604b77..a85e9fa9394 100644
--- a/app/workers/repository_remove_remote_worker.rb
+++ b/app/workers/repository_remove_remote_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class RepositoryRemoveRemoteWorker
include ApplicationWorker
include ExclusiveLeaseGuard
diff --git a/app/workers/repository_update_remote_mirror_worker.rb b/app/workers/repository_update_remote_mirror_worker.rb
index bb963979e88..9d4e67deb9c 100644
--- a/app/workers/repository_update_remote_mirror_worker.rb
+++ b/app/workers/repository_update_remote_mirror_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class RepositoryUpdateRemoteMirrorWorker
UpdateAlreadyInProgressError = Class.new(StandardError)
UpdateError = Class.new(StandardError)
diff --git a/app/workers/requests_profiles_worker.rb b/app/workers/requests_profiles_worker.rb
index 55c236e9e9d..ae022d43e29 100644
--- a/app/workers/requests_profiles_worker.rb
+++ b/app/workers/requests_profiles_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class RequestsProfilesWorker
include ApplicationWorker
include CronjobQueue
diff --git a/app/workers/run_pipeline_schedule_worker.rb b/app/workers/run_pipeline_schedule_worker.rb
index 8f5138fc873..1f6cb18c812 100644
--- a/app/workers/run_pipeline_schedule_worker.rb
+++ b/app/workers/run_pipeline_schedule_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class RunPipelineScheduleWorker
include ApplicationWorker
include PipelineQueue
diff --git a/app/workers/schedule_update_user_activity_worker.rb b/app/workers/schedule_update_user_activity_worker.rb
index d9376577597..ff42fb8f0e5 100644
--- a/app/workers/schedule_update_user_activity_worker.rb
+++ b/app/workers/schedule_update_user_activity_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ScheduleUpdateUserActivityWorker
include ApplicationWorker
include CronjobQueue
diff --git a/app/workers/stage_update_worker.rb b/app/workers/stage_update_worker.rb
index e4b683fca33..ec8c8e3689f 100644
--- a/app/workers/stage_update_worker.rb
+++ b/app/workers/stage_update_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class StageUpdateWorker
include ApplicationWorker
include PipelineQueue
diff --git a/app/workers/storage_migrator_worker.rb b/app/workers/storage_migrator_worker.rb
index f92421a667d..fa76fbac55c 100644
--- a/app/workers/storage_migrator_worker.rb
+++ b/app/workers/storage_migrator_worker.rb
@@ -1,29 +1,10 @@
+# frozen_string_literal: true
+
class StorageMigratorWorker
include ApplicationWorker
- BATCH_SIZE = 100
-
def perform(start, finish)
- projects = build_relation(start, finish)
-
- projects.with_route.find_each(batch_size: BATCH_SIZE) do |project|
- Rails.logger.info "Starting storage migration of #{project.full_path} (ID=#{project.id})..."
-
- begin
- project.migrate_to_hashed_storage!
- rescue => err
- Rails.logger.error("#{err.message} migrating storage of #{project.full_path} (ID=#{project.id}), trace - #{err.backtrace}")
- end
- end
- end
-
- def build_relation(start, finish)
- relation = Project
- table = Project.arel_table
-
- relation = relation.where(table[:id].gteq(start)) if start
- relation = relation.where(table[:id].lteq(finish)) if finish
-
- relation
+ migrator = Gitlab::HashedStorage::Migrator.new
+ migrator.bulk_migrate(start, finish)
end
end
diff --git a/app/workers/stuck_ci_jobs_worker.rb b/app/workers/stuck_ci_jobs_worker.rb
index 7ebf69bdc39..c78b7fac589 100644
--- a/app/workers/stuck_ci_jobs_worker.rb
+++ b/app/workers/stuck_ci_jobs_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class StuckCiJobsWorker
include ApplicationWorker
include CronjobQueue
diff --git a/app/workers/stuck_import_jobs_worker.rb b/app/workers/stuck_import_jobs_worker.rb
index 6fdd7592e74..79ce06dd66e 100644
--- a/app/workers/stuck_import_jobs_worker.rb
+++ b/app/workers/stuck_import_jobs_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class StuckImportJobsWorker
include ApplicationWorker
include CronjobQueue
diff --git a/app/workers/stuck_merge_jobs_worker.rb b/app/workers/stuck_merge_jobs_worker.rb
index 16394293c79..b0a62f76e94 100644
--- a/app/workers/stuck_merge_jobs_worker.rb
+++ b/app/workers/stuck_merge_jobs_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class StuckMergeJobsWorker
include ApplicationWorker
include CronjobQueue
diff --git a/app/workers/system_hook_push_worker.rb b/app/workers/system_hook_push_worker.rb
index ceeaaf8d189..15e369ebcfb 100644
--- a/app/workers/system_hook_push_worker.rb
+++ b/app/workers/system_hook_push_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class SystemHookPushWorker
include ApplicationWorker
diff --git a/app/workers/trending_projects_worker.rb b/app/workers/trending_projects_worker.rb
index 7eb65452a7d..3297a1fe3d0 100644
--- a/app/workers/trending_projects_worker.rb
+++ b/app/workers/trending_projects_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class TrendingProjectsWorker
include ApplicationWorker
include CronjobQueue
diff --git a/app/workers/update_head_pipeline_for_merge_request_worker.rb b/app/workers/update_head_pipeline_for_merge_request_worker.rb
index 76f84ff920f..0487a393566 100644
--- a/app/workers/update_head_pipeline_for_merge_request_worker.rb
+++ b/app/workers/update_head_pipeline_for_merge_request_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class UpdateHeadPipelineForMergeRequestWorker
include ApplicationWorker
include PipelineQueue
diff --git a/app/workers/update_merge_requests_worker.rb b/app/workers/update_merge_requests_worker.rb
index 74bb9993275..742841219b3 100644
--- a/app/workers/update_merge_requests_worker.rb
+++ b/app/workers/update_merge_requests_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class UpdateMergeRequestsWorker
include ApplicationWorker
diff --git a/app/workers/update_user_activity_worker.rb b/app/workers/update_user_activity_worker.rb
index 27ec5cd33fb..15f01a70337 100644
--- a/app/workers/update_user_activity_worker.rb
+++ b/app/workers/update_user_activity_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class UpdateUserActivityWorker
include ApplicationWorker
diff --git a/app/workers/upload_checksum_worker.rb b/app/workers/upload_checksum_worker.rb
index 65d40336f18..2a0536106d7 100644
--- a/app/workers/upload_checksum_worker.rb
+++ b/app/workers/upload_checksum_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class UploadChecksumWorker
include ApplicationWorker
diff --git a/app/workers/wait_for_cluster_creation_worker.rb b/app/workers/wait_for_cluster_creation_worker.rb
index 19cdb279aaa..8aa1d9290fd 100644
--- a/app/workers/wait_for_cluster_creation_worker.rb
+++ b/app/workers/wait_for_cluster_creation_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class WaitForClusterCreationWorker
include ApplicationWorker
include ClusterQueue
diff --git a/app/workers/web_hook_worker.rb b/app/workers/web_hook_worker.rb
index dfc3f33ad9d..09219a24a16 100644
--- a/app/workers/web_hook_worker.rb
+++ b/app/workers/web_hook_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class WebHookWorker
include ApplicationWorker
diff --git a/bin/changelog b/bin/changelog
index 9b60f53ce40..758c036161e 100755
--- a/bin/changelog
+++ b/bin/changelog
@@ -19,7 +19,26 @@ Options = Struct.new(
)
INVALID_TYPE = -1
+module ChangelogHelpers
+ Abort = Class.new(StandardError)
+ Done = Class.new(StandardError)
+
+ MAX_FILENAME_LENGTH = 140 # ecryptfs has a limit of 140 characters
+
+ def capture_stdout(cmd)
+ output = IO.popen(cmd, &:read)
+ fail_with "command failed: #{cmd.join(' ')}" unless $?.success?
+ output
+ end
+
+ def fail_with(message)
+ raise Abort, "\e[31merror\e[0m #{message}"
+ end
+end
+
class ChangelogOptionParser
+ extend ChangelogHelpers
+
Type = Struct.new(:name, :description)
TYPES = [
Type.new('added', 'New feature'),
@@ -68,7 +87,7 @@ class ChangelogOptionParser
opts.on('-h', '--help', 'Print help message') do
$stdout.puts opts
- exit
+ raise Done.new
end
end
@@ -108,23 +127,26 @@ class ChangelogOptionParser
def assert_valid_type!(type)
unless type
- $stderr.puts "Invalid category index, please select an index between 1 and #{TYPES.length}"
- exit 1
+ raise Abort, "Invalid category index, please select an index between 1 and #{TYPES.length}"
end
end
def git_user_name
- %x{git config user.name}.strip
+ capture_stdout(%w[git config user.name]).strip
end
end
end
class ChangelogEntry
+ include ChangelogHelpers
+
attr_reader :options
def initialize(options)
@options = options
+ end
+ def execute
assert_feature_branch!
assert_title!
assert_new_file!
@@ -159,13 +181,9 @@ class ChangelogEntry
end
def amend_commit
- %x{git add #{file_path}}
- exec("git commit --amend")
- end
+ fail_with "git add failed" unless system(*%W[git add #{file_path}])
- def fail_with(message)
- $stderr.puts "\e[31merror\e[0m #{message}"
- exit 1
+ Kernel.exec(*%w[git commit --amend])
end
def assert_feature_branch!
@@ -203,14 +221,16 @@ class ChangelogEntry
end
def last_commit_subject
- %x{git log --format="%s" -1}.strip
+ capture_stdout(%w[git log --format=%s -1]).strip
end
def file_path
- File.join(
+ base_path = File.join(
unreleased_path,
- branch_name.gsub(/[^\w-]/, '-') << '.yml'
- )
+ branch_name.gsub(/[^\w-]/, '-'))
+
+ # Add padding for .yml extension
+ base_path[0..MAX_FILENAME_LENGTH - 5] + '.yml'
end
def unreleased_path
@@ -225,7 +245,7 @@ class ChangelogEntry
end
def branch_name
- @branch_name ||= %x{git symbolic-ref --short HEAD}.strip
+ @branch_name ||= capture_stdout(%w[git symbolic-ref --short HEAD]).strip
end
def remove_trailing_whitespace(yaml_content)
@@ -234,8 +254,15 @@ class ChangelogEntry
end
if $0 == __FILE__
- options = ChangelogOptionParser.parse(ARGV)
- ChangelogEntry.new(options)
+ begin
+ options = ChangelogOptionParser.parse(ARGV)
+ ChangelogEntry.new(options).execute
+ rescue ChangelogHelpers::Abort => ex
+ $stderr.puts ex.message
+ exit 1
+ rescue ChangelogHelpers::Done
+ exit
+ end
end
# vim: ft=ruby
diff --git a/changelogs/no-rm-rf-gitlab-basics.yml b/changelogs/no-rm-rf-gitlab-basics.yml
deleted file mode 100644
index d5aa1091b45..00000000000
--- a/changelogs/no-rm-rf-gitlab-basics.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
- title: Do not use '-f' with 'rm' in gitlab-basics docs
- merge_request: 18027
- author: Elias Werberich
- type: changed
diff --git a/changelogs/unreleased/18141-osw-use-monospaced-font-on-diffs-commit-ref.yml b/changelogs/unreleased/18141-osw-use-monospaced-font-on-diffs-commit-ref.yml
new file mode 100644
index 00000000000..43ff880a8cb
--- /dev/null
+++ b/changelogs/unreleased/18141-osw-use-monospaced-font-on-diffs-commit-ref.yml
@@ -0,0 +1,5 @@
+---
+title: Use monospaced font for MR diff commit link ref on GFM
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/18524-fix-double-brackets-in-wiki-markdown.yml b/changelogs/unreleased/18524-fix-double-brackets-in-wiki-markdown.yml
deleted file mode 100644
index 9287243a7e3..00000000000
--- a/changelogs/unreleased/18524-fix-double-brackets-in-wiki-markdown.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix double-brackets being linkified in wiki markdown
-merge_request: 18524
-author: brewingcode
-type: fixed
diff --git a/changelogs/unreleased/19439-api-file-sha56-and-head.yml b/changelogs/unreleased/19439-api-file-sha56-and-head.yml
new file mode 100644
index 00000000000..4bc1e560631
--- /dev/null
+++ b/changelogs/unreleased/19439-api-file-sha56-and-head.yml
@@ -0,0 +1,5 @@
+---
+title: Add SHA256 and HEAD on File API
+merge_request: 19439
+author: ahmet2mir
+type: added
diff --git a/changelogs/unreleased/19468-add_readme_when_creating_project.yml b/changelogs/unreleased/19468-add_readme_when_creating_project.yml
new file mode 100644
index 00000000000..f85fc773ef0
--- /dev/null
+++ b/changelogs/unreleased/19468-add_readme_when_creating_project.yml
@@ -0,0 +1,5 @@
+---
+title: Add option to add README when creating a project
+merge_request: 20335
+author:
+type: added
diff --git a/changelogs/unreleased/19861-expand-api-render-an-arbitrary-markdown-document.yml b/changelogs/unreleased/19861-expand-api-render-an-arbitrary-markdown-document.yml
deleted file mode 100644
index a97e8a2b5cc..00000000000
--- a/changelogs/unreleased/19861-expand-api-render-an-arbitrary-markdown-document.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add API endpoint to render markdown text
-merge_request: 18926
-author: "@blackst0ne"
-type: added
diff --git a/changelogs/unreleased/20357.yml b/changelogs/unreleased/20357.yml
new file mode 100644
index 00000000000..b4ce686eece
--- /dev/null
+++ b/changelogs/unreleased/20357.yml
@@ -0,0 +1,5 @@
+---
+title: Fix double "in" in time to artifact deletion message
+merge_request: 20357
+author: "@bbodenmiller"
+type: fixed
diff --git a/changelogs/unreleased/22647-width-contributors-graphs.yml b/changelogs/unreleased/22647-width-contributors-graphs.yml
deleted file mode 100644
index 87be3a25d8a..00000000000
--- a/changelogs/unreleased/22647-width-contributors-graphs.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix width of contributors graphs
-merge_request: 18639
-author: Paul Vorbach
-type: fixed
diff --git a/changelogs/unreleased/22846-notifications-broken-during-email-address-change-before-email-confirmed.yml b/changelogs/unreleased/22846-notifications-broken-during-email-address-change-before-email-confirmed.yml
deleted file mode 100644
index 2b4727c5f03..00000000000
--- a/changelogs/unreleased/22846-notifications-broken-during-email-address-change-before-email-confirmed.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Fix an issue where the notification email address would be set to an unconfirmed
- email address
-merge_request: 18474
-author:
-type: fixed
diff --git a/changelogs/unreleased/23465-print-markdown.yml b/changelogs/unreleased/23465-print-markdown.yml
deleted file mode 100644
index ba950667acc..00000000000
--- a/changelogs/unreleased/23465-print-markdown.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix print styles for markdown pages
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/31583-osw-gfm-complete-status-indication.yml b/changelogs/unreleased/31583-osw-gfm-complete-status-indication.yml
new file mode 100644
index 00000000000..6f2cf275592
--- /dev/null
+++ b/changelogs/unreleased/31583-osw-gfm-complete-status-indication.yml
@@ -0,0 +1,5 @@
+---
+title: Present state indication on GFM preview
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/35158-snippets-api-visibility.yml b/changelogs/unreleased/35158-snippets-api-visibility.yml
new file mode 100644
index 00000000000..f06015dda46
--- /dev/null
+++ b/changelogs/unreleased/35158-snippets-api-visibility.yml
@@ -0,0 +1,5 @@
+---
+title: Expose visibility via Snippets API
+merge_request: 19620
+author: Jan Beckmann
+type: added
diff --git a/changelogs/unreleased/36234-nav-add-groups-dropdown.yml b/changelogs/unreleased/36234-nav-add-groups-dropdown.yml
new file mode 100644
index 00000000000..86a24102665
--- /dev/null
+++ b/changelogs/unreleased/36234-nav-add-groups-dropdown.yml
@@ -0,0 +1,5 @@
+---
+title: Add dropdown to Groups link in top bar
+merge_request: 18280
+author:
+type: added
diff --git a/changelogs/unreleased/36907-fix-new-issue-link-from-failed-job.yml b/changelogs/unreleased/36907-fix-new-issue-link-from-failed-job.yml
new file mode 100644
index 00000000000..80a50734f72
--- /dev/null
+++ b/changelogs/unreleased/36907-fix-new-issue-link-from-failed-job.yml
@@ -0,0 +1,5 @@
+---
+title: Fix link to job when creating a new issue from a failed job
+merge_request: 20328
+author:
+type: fixed
diff --git a/changelogs/unreleased/37561-add-id-settings.yml b/changelogs/unreleased/37561-add-id-settings.yml
new file mode 100644
index 00000000000..122ac23cb53
--- /dev/null
+++ b/changelogs/unreleased/37561-add-id-settings.yml
@@ -0,0 +1,5 @@
+---
+title: Allows settings sections to expand by default when linking to them
+merge_request: 20211
+author:
+type: other
diff --git a/changelogs/unreleased/39543-milestone-page-list-redesign.yml b/changelogs/unreleased/39543-milestone-page-list-redesign.yml
new file mode 100644
index 00000000000..dcd73c5eddf
--- /dev/null
+++ b/changelogs/unreleased/39543-milestone-page-list-redesign.yml
@@ -0,0 +1,5 @@
+---
+title: Milestone page list redesign
+merge_request: 19832
+author: Constance Okoghenun
+type: changed
diff --git a/changelogs/unreleased/39584-nesting-depth-5-pages-pipelines.yml b/changelogs/unreleased/39584-nesting-depth-5-pages-pipelines.yml
deleted file mode 100644
index 9f07fcdfa0b..00000000000
--- a/changelogs/unreleased/39584-nesting-depth-5-pages-pipelines.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Apply NestingDepth (level 5) (pages/pipelines.scss)
-merge_request: 18830
-author: Takuya Noguchi
-type: other
diff --git a/changelogs/unreleased/39604-update-top-right-avatar-after-changing-avatar.yml b/changelogs/unreleased/39604-update-top-right-avatar-after-changing-avatar.yml
new file mode 100644
index 00000000000..17192673996
--- /dev/null
+++ b/changelogs/unreleased/39604-update-top-right-avatar-after-changing-avatar.yml
@@ -0,0 +1,5 @@
+---
+title: Change avatar image in the header when user updates their avatar.
+merge_request: 20119
+author: Jamie Schembri
+type: added
diff --git a/changelogs/unreleased/39710-search-placeholder-cut-off.yml b/changelogs/unreleased/39710-search-placeholder-cut-off.yml
deleted file mode 100644
index 59290768c6a..00000000000
--- a/changelogs/unreleased/39710-search-placeholder-cut-off.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'Fixes: Runners search input placeholder is cut off'
-merge_request: 19015
-author: Jacopo Beschi @jacopo-beschi
-type: fixed
diff --git a/changelogs/unreleased/40005-u2f-unspported-browsers.yml b/changelogs/unreleased/40005-u2f-unspported-browsers.yml
new file mode 100644
index 00000000000..eb5ff99246e
--- /dev/null
+++ b/changelogs/unreleased/40005-u2f-unspported-browsers.yml
@@ -0,0 +1,5 @@
+---
+title: Improve U2F workflow when using unsupported browsers
+merge_request: 19938
+author: Jan Beckmann
+type: changed
diff --git a/changelogs/unreleased/40484-ordered-lists-copy-gfm.yml b/changelogs/unreleased/40484-ordered-lists-copy-gfm.yml
new file mode 100644
index 00000000000..f4b34909ae9
--- /dev/null
+++ b/changelogs/unreleased/40484-ordered-lists-copy-gfm.yml
@@ -0,0 +1,5 @@
+---
+title: Keep lists ordered when copying only list items
+merge_request: 18522
+author: Jan Beckmann
+type: fixed
diff --git a/changelogs/unreleased/40725-move-mr-external-link-to-right.yml b/changelogs/unreleased/40725-move-mr-external-link-to-right.yml
deleted file mode 100644
index e3ebeb5eb61..00000000000
--- a/changelogs/unreleased/40725-move-mr-external-link-to-right.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Moves MR widget external link icon to the right
-merge_request: 18828
-author: Jacopo Beschi @jacopo-beschi
-type: changed
diff --git a/changelogs/unreleased/40855_remove_authentication_in_readonly_issue_api.yml b/changelogs/unreleased/40855_remove_authentication_in_readonly_issue_api.yml
deleted file mode 100644
index 58686639594..00000000000
--- a/changelogs/unreleased/40855_remove_authentication_in_readonly_issue_api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: made listing and showing public issue apis available without authentication
-merge_request: 18638
-author: haseebeqx
-type: changed
diff --git a/changelogs/unreleased/41671-fixing-milestone-date-change-when-editing.yml b/changelogs/unreleased/41671-fixing-milestone-date-change-when-editing.yml
new file mode 100644
index 00000000000..c6a0dc4f129
--- /dev/null
+++ b/changelogs/unreleased/41671-fixing-milestone-date-change-when-editing.yml
@@ -0,0 +1,5 @@
+---
+title: "Fixing milestone date change when editing"
+merge_request: 20279
+author: Orlando Del Aguila
+type: fixed \ No newline at end of file
diff --git a/changelogs/unreleased/42342-teams-pipeline-notifications.yml b/changelogs/unreleased/42342-teams-pipeline-notifications.yml
new file mode 100644
index 00000000000..4ef3a35465b
--- /dev/null
+++ b/changelogs/unreleased/42342-teams-pipeline-notifications.yml
@@ -0,0 +1,5 @@
+---
+title: Fixes Microsoft Teams notifications for pipeline events
+merge_request: 19632
+author: Jeff Brown
+type: fixed
diff --git a/changelogs/unreleased/42415-omit-projects-from-get-group-endpoint.yml b/changelogs/unreleased/42415-omit-projects-from-get-group-endpoint.yml
new file mode 100644
index 00000000000..cabe5216045
--- /dev/null
+++ b/changelogs/unreleased/42415-omit-projects-from-get-group-endpoint.yml
@@ -0,0 +1,5 @@
+---
+title: Adds with_projects optional parameter to GET /groups/:id API endpoint
+merge_request: 20494
+author:
+type: changed
diff --git a/changelogs/unreleased/42531-open-invite-404.yml b/changelogs/unreleased/42531-open-invite-404.yml
deleted file mode 100644
index 73729f4a929..00000000000
--- a/changelogs/unreleased/42531-open-invite-404.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Automatically accepts project/group invite by email after user signup
-merge_request: 17634
-author: Jacopo Beschi @jacopo-beschi
-type: changed
diff --git a/changelogs/unreleased/43270-import-with-milestones-failing.yml b/changelogs/unreleased/43270-import-with-milestones-failing.yml
new file mode 100644
index 00000000000..13bf8072376
--- /dev/null
+++ b/changelogs/unreleased/43270-import-with-milestones-failing.yml
@@ -0,0 +1,5 @@
+---
+title: Fix label and milestone duplicated records and IID errors
+merge_request: 19961
+author:
+type: fixed
diff --git a/changelogs/unreleased/43367-fix-board-long-strings.yml b/changelogs/unreleased/43367-fix-board-long-strings.yml
deleted file mode 100644
index 6228741601e..00000000000
--- a/changelogs/unreleased/43367-fix-board-long-strings.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix issue board bug with long strings in titles
-merge_request: 18924
-author:
-type: fixed
diff --git a/changelogs/unreleased/43446-new-cluster-page-tabs.yml b/changelogs/unreleased/43446-new-cluster-page-tabs.yml
new file mode 100644
index 00000000000..e8c73257b16
--- /dev/null
+++ b/changelogs/unreleased/43446-new-cluster-page-tabs.yml
@@ -0,0 +1,5 @@
+---
+title: Create new or add existing Kubernetes cluster from a single page
+merge_request: 18963
+author:
+type: changed
diff --git a/changelogs/unreleased/43472-remove-environment-scope-field-on-cluster-creation-form-for-core-starter-plans.yml b/changelogs/unreleased/43472-remove-environment-scope-field-on-cluster-creation-form-for-core-starter-plans.yml
new file mode 100644
index 00000000000..7d2804f0310
--- /dev/null
+++ b/changelogs/unreleased/43472-remove-environment-scope-field-on-cluster-creation-form-for-core-starter-plans.yml
@@ -0,0 +1,5 @@
+---
+title: Removes the environment scope field for users that cannot edit it
+merge_request: 19643
+author:
+type: changed
diff --git a/changelogs/unreleased/43673-operations-tab-mvc.yml b/changelogs/unreleased/43673-operations-tab-mvc.yml
deleted file mode 100644
index cd580e7a8d6..00000000000
--- a/changelogs/unreleased/43673-operations-tab-mvc.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move project sidebar sub-entries 'Environments' and 'Kubernetes' from 'CI/CD' to a new entry 'Operations'
-merge_request: 18941
-author:
-type: changed
diff --git a/changelogs/unreleased/44319-remove-gray-buttons.yml b/changelogs/unreleased/44319-remove-gray-buttons.yml
deleted file mode 100644
index 9803dde8493..00000000000
--- a/changelogs/unreleased/44319-remove-gray-buttons.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove gray button styles
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/44674-use-one-column-form-layout-on-admin-area-settings-page.yml b/changelogs/unreleased/44674-use-one-column-form-layout-on-admin-area-settings-page.yml
new file mode 100644
index 00000000000..69733889d5a
--- /dev/null
+++ b/changelogs/unreleased/44674-use-one-column-form-layout-on-admin-area-settings-page.yml
@@ -0,0 +1,5 @@
+---
+title: Use one column form layout on Admin Area Settings page
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/44697-when-editing-a-comment-in-an-issue-the-preview-mode-is-toggled-in-the-main-textarea.yml b/changelogs/unreleased/44697-when-editing-a-comment-in-an-issue-the-preview-mode-is-toggled-in-the-main-textarea.yml
new file mode 100644
index 00000000000..750e28f1a8d
--- /dev/null
+++ b/changelogs/unreleased/44697-when-editing-a-comment-in-an-issue-the-preview-mode-is-toggled-in-the-main-textarea.yml
@@ -0,0 +1,6 @@
+---
+title: Fixed bug when editing a comment in an issue,the preview mode is toggled in
+ the main textarea
+merge_request: 20112
+author: Constance Okoghenun
+type: fixed
diff --git a/changelogs/unreleased/44725-expire_correct_methods_after_change_head.yml b/changelogs/unreleased/44725-expire_correct_methods_after_change_head.yml
new file mode 100644
index 00000000000..21a65f142c3
--- /dev/null
+++ b/changelogs/unreleased/44725-expire_correct_methods_after_change_head.yml
@@ -0,0 +1,5 @@
+---
+title: Expire correct method caches after HEAD changed
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/44799-api-naming-issue-scope.yml b/changelogs/unreleased/44799-api-naming-issue-scope.yml
deleted file mode 100644
index 75c6ea4cd0d..00000000000
--- a/changelogs/unreleased/44799-api-naming-issue-scope.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Rename issue scope created-by-me to created_by_me, and assigned-to-me to assigned_to_me
-merge_request: 44799
-author:
-type: deprecated
diff --git a/changelogs/unreleased/45065-users-projects-json-sort.yml b/changelogs/unreleased/45065-users-projects-json-sort.yml
deleted file mode 100644
index 89a1d7eb36f..00000000000
--- a/changelogs/unreleased/45065-users-projects-json-sort.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Order UsersController#projects.json by updated_at
-merge_request: 18227
-author: Takuya Noguchi
-type: other
diff --git a/changelogs/unreleased/45400-automatically-created-mr-uses-wrong-target-branch-when-branching-from-tag.yml b/changelogs/unreleased/45400-automatically-created-mr-uses-wrong-target-branch-when-branching-from-tag.yml
new file mode 100644
index 00000000000..5aba62435ed
--- /dev/null
+++ b/changelogs/unreleased/45400-automatically-created-mr-uses-wrong-target-branch-when-branching-from-tag.yml
@@ -0,0 +1,5 @@
+---
+title: Set MR target branch to default branch if target branch is not valid
+merge_request: 19067
+author:
+type: fixed
diff --git a/changelogs/unreleased/45442-updates-updated-at-to-issue-on-time-spent.yml b/changelogs/unreleased/45442-updates-updated-at-to-issue-on-time-spent.yml
deleted file mode 100644
index 0694206d4fb..00000000000
--- a/changelogs/unreleased/45442-updates-updated-at-to-issue-on-time-spent.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Updates updated_at on issuable when setting time spent
-merge_request: 18757
-author: Jacopo Beschi @jacopo-beschi
-type: added
diff --git a/changelogs/unreleased/45487-slack-tag-push-notifs.yml b/changelogs/unreleased/45487-slack-tag-push-notifs.yml
new file mode 100644
index 00000000000..647000bd97c
--- /dev/null
+++ b/changelogs/unreleased/45487-slack-tag-push-notifs.yml
@@ -0,0 +1,5 @@
+---
+title: Fix chat service tag notifications not sending when only default branch enabled
+merge_request: 19864
+author:
+type: fixed
diff --git a/changelogs/unreleased/45557-machine-type-help-links.yml b/changelogs/unreleased/45557-machine-type-help-links.yml
new file mode 100644
index 00000000000..870a650e10b
--- /dev/null
+++ b/changelogs/unreleased/45557-machine-type-help-links.yml
@@ -0,0 +1,6 @@
+---
+title: Add machine type and pricing documentation links, add class to labels to make
+ bold
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/45575-invalid-characters-signup.yml b/changelogs/unreleased/45575-invalid-characters-signup.yml
new file mode 100644
index 00000000000..679bd13e59b
--- /dev/null
+++ b/changelogs/unreleased/45575-invalid-characters-signup.yml
@@ -0,0 +1,5 @@
+---
+title: 'Fix username validation order on signup, resolves #45575'
+merge_request: 19610
+author: Jan Beckmann
+type: fixed
diff --git a/changelogs/unreleased/45584-add-nip-io-domain-suggestion-in-auto-devops.yml b/changelogs/unreleased/45584-add-nip-io-domain-suggestion-in-auto-devops.yml
deleted file mode 100644
index 31b4c29e03d..00000000000
--- a/changelogs/unreleased/45584-add-nip-io-domain-suggestion-in-auto-devops.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Display help text below auto devops domain with nip.io domain name (#45561)
-merge_request: 18496
-author:
-type: added
diff --git a/changelogs/unreleased/45703-open-web-ide-file-tree.yml b/changelogs/unreleased/45703-open-web-ide-file-tree.yml
new file mode 100644
index 00000000000..abee9cad2d5
--- /dev/null
+++ b/changelogs/unreleased/45703-open-web-ide-file-tree.yml
@@ -0,0 +1,5 @@
+---
+title: Update WebIDE to show file in tree on load
+merge_request: 19887
+author:
+type: changed
diff --git a/changelogs/unreleased/45715-remove-modal-retry.yml b/changelogs/unreleased/45715-remove-modal-retry.yml
deleted file mode 100644
index 04f2ff5142e..00000000000
--- a/changelogs/unreleased/45715-remove-modal-retry.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove modalbox confirmation when retrying a pipeline
-merge_request: 18879
-author:
-type: changed
diff --git a/changelogs/unreleased/45738-add-environment-drop-down-to-metrics-dashboard.yml b/changelogs/unreleased/45738-add-environment-drop-down-to-metrics-dashboard.yml
new file mode 100644
index 00000000000..5aaeaaf0448
--- /dev/null
+++ b/changelogs/unreleased/45738-add-environment-drop-down-to-metrics-dashboard.yml
@@ -0,0 +1,5 @@
+---
+title: Add environment dropdown for the metrics page
+merge_request: 19833
+author:
+type: changed
diff --git a/changelogs/unreleased/45827-expose_readme_url_in_project_api.yml b/changelogs/unreleased/45827-expose_readme_url_in_project_api.yml
deleted file mode 100644
index 7c495cf4dc0..00000000000
--- a/changelogs/unreleased/45827-expose_readme_url_in_project_api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Expose readme url in Project API
-merge_request: 18960
-author: Imre Farkas
-type: changed
diff --git a/changelogs/unreleased/45933-webide-fade-uneditable-area.yml b/changelogs/unreleased/45933-webide-fade-uneditable-area.yml
new file mode 100644
index 00000000000..dfb186122e7
--- /dev/null
+++ b/changelogs/unreleased/45933-webide-fade-uneditable-area.yml
@@ -0,0 +1,5 @@
+---
+title: Fade uneditable area in Web IDE
+merge_request: 20008
+author:
+type: changed
diff --git a/changelogs/unreleased/45934-ide-firefox-scroll-md-preview.yml b/changelogs/unreleased/45934-ide-firefox-scroll-md-preview.yml
deleted file mode 100644
index b9e70bc5679..00000000000
--- a/changelogs/unreleased/45934-ide-firefox-scroll-md-preview.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix unscrollable Markdown preview of WebIDE on Firefox
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/46010-add-index-to-runner-type.yml b/changelogs/unreleased/46010-add-index-to-runner-type.yml
deleted file mode 100644
index fb8340e37b2..00000000000
--- a/changelogs/unreleased/46010-add-index-to-runner-type.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add index on runner_type for ci_runners
-merge_request: 18897
-author:
-type: performance
diff --git a/changelogs/unreleased/46082-runner-contacted_at-is-not-always-a-time-type.yml b/changelogs/unreleased/46082-runner-contacted_at-is-not-always-a-time-type.yml
deleted file mode 100644
index 07f67251b24..00000000000
--- a/changelogs/unreleased/46082-runner-contacted_at-is-not-always-a-time-type.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix Runner contacted at tooltip cache.
-merge_request: 18810
-author:
-type: fixed
diff --git a/changelogs/unreleased/46193-fix-big-estimate.yml b/changelogs/unreleased/46193-fix-big-estimate.yml
deleted file mode 100644
index d0da0c10033..00000000000
--- a/changelogs/unreleased/46193-fix-big-estimate.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixes 500 error on /estimate BIG_VALUE
-merge_request: 18964
-author: Jacopo Beschi @jacopo-beschi
-type: fixed
diff --git a/changelogs/unreleased/46202-webide-file-states.yml b/changelogs/unreleased/46202-webide-file-states.yml
new file mode 100644
index 00000000000..8d697b643be
--- /dev/null
+++ b/changelogs/unreleased/46202-webide-file-states.yml
@@ -0,0 +1,5 @@
+---
+title: Update Web IDE file tree styles
+merge_request: 19969
+author:
+type: changed
diff --git a/changelogs/unreleased/46246-gitlab-project-export-should-use-object-storage.yml b/changelogs/unreleased/46246-gitlab-project-export-should-use-object-storage.yml
new file mode 100644
index 00000000000..908c7a238fd
--- /dev/null
+++ b/changelogs/unreleased/46246-gitlab-project-export-should-use-object-storage.yml
@@ -0,0 +1,5 @@
+---
+title: Add Object Storage to project export
+merge_request: 20105
+author:
+type: added
diff --git a/changelogs/unreleased/46354-deprecate_gemnasium_service.yml b/changelogs/unreleased/46354-deprecate_gemnasium_service.yml
deleted file mode 100644
index c5ead45d883..00000000000
--- a/changelogs/unreleased/46354-deprecate_gemnasium_service.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Deprecate Gemnasium project service
-merge_request: 18954
-author:
-type: deprecated
diff --git a/changelogs/unreleased/46361-does-not-log-failed-sign-in-attempts-when-the-database-is-in-read-only-mode.yml b/changelogs/unreleased/46361-does-not-log-failed-sign-in-attempts-when-the-database-is-in-read-only-mode.yml
deleted file mode 100644
index e4255f11ecf..00000000000
--- a/changelogs/unreleased/46361-does-not-log-failed-sign-in-attempts-when-the-database-is-in-read-only-mode.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Does not log failed sign-in attempts when the database is in read-only mode
-merge_request: 18957
-author:
-type: fixed
diff --git a/changelogs/unreleased/46396-recognise-when-a-user-is-trying-to-validate-a-private-ssh-key-part-1.yml b/changelogs/unreleased/46396-recognise-when-a-user-is-trying-to-validate-a-private-ssh-key-part-1.yml
new file mode 100644
index 00000000000..d8c7d612c3d
--- /dev/null
+++ b/changelogs/unreleased/46396-recognise-when-a-user-is-trying-to-validate-a-private-ssh-key-part-1.yml
@@ -0,0 +1,5 @@
+---
+title: Update new SSH key page to improve copy
+merge_request: 19994
+author:
+type: other
diff --git a/changelogs/unreleased/46396-recognise-when-a-user-is-trying-to-validate-a-private-ssh-key.yml b/changelogs/unreleased/46396-recognise-when-a-user-is-trying-to-validate-a-private-ssh-key.yml
new file mode 100644
index 00000000000..64bbecf3405
--- /dev/null
+++ b/changelogs/unreleased/46396-recognise-when-a-user-is-trying-to-validate-a-private-ssh-key.yml
@@ -0,0 +1,5 @@
+---
+title: Update new SSH key page to improve key input validation
+merge_request: 19997
+author:
+type: other
diff --git a/changelogs/unreleased/46427-add-keyboard-shortcut-environments.yml b/changelogs/unreleased/46427-add-keyboard-shortcut-environments.yml
deleted file mode 100644
index 609968f3230..00000000000
--- a/changelogs/unreleased/46427-add-keyboard-shortcut-environments.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adds keyboard shortcut `g e` for Environments on Project pages
-merge_request: 19002
-author:
-type: added
diff --git a/changelogs/unreleased/46427-add-keyboard-shortcut-kubernetes.yml b/changelogs/unreleased/46427-add-keyboard-shortcut-kubernetes.yml
deleted file mode 100644
index 48e51b2615e..00000000000
--- a/changelogs/unreleased/46427-add-keyboard-shortcut-kubernetes.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adds keyboard shortcut `g k` for Kubernetes on Project pages
-merge_request: 19002
-author:
-type: added
diff --git a/changelogs/unreleased/46427-change-keyboard-shortcut-of-activity-feed.yml b/changelogs/unreleased/46427-change-keyboard-shortcut-of-activity-feed.yml
deleted file mode 100644
index 9a7cf0d6944..00000000000
--- a/changelogs/unreleased/46427-change-keyboard-shortcut-of-activity-feed.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Changes keyboard shortcut of Activity feed to `g v`
-merge_request: 19002
-author:
-type: changed
diff --git a/changelogs/unreleased/46427-remove-outdated-todos-shortcut.yml b/changelogs/unreleased/46427-remove-outdated-todos-shortcut.yml
deleted file mode 100644
index f416e35030e..00000000000
--- a/changelogs/unreleased/46427-remove-outdated-todos-shortcut.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Removes outdated `g t` shortcut for TODO in favor of `Shift+T`
-merge_request: 19002
-author:
-type: removed
diff --git a/changelogs/unreleased/46429-creating-a-deploy-token-doesn-t-bring-back-to-the-creation-page.yml b/changelogs/unreleased/46429-creating-a-deploy-token-doesn-t-bring-back-to-the-creation-page.yml
new file mode 100644
index 00000000000..b564fb0174f
--- /dev/null
+++ b/changelogs/unreleased/46429-creating-a-deploy-token-doesn-t-bring-back-to-the-creation-page.yml
@@ -0,0 +1,5 @@
+---
+title: Allows you to create another deploy token dimmediately after creating one
+merge_request: 19639
+author:
+type: changed
diff --git a/changelogs/unreleased/46546-do-not-pre-select-previous-user-s-when-creating-protected-branches.yml b/changelogs/unreleased/46546-do-not-pre-select-previous-user-s-when-creating-protected-branches.yml
new file mode 100644
index 00000000000..7d42d971022
--- /dev/null
+++ b/changelogs/unreleased/46546-do-not-pre-select-previous-user-s-when-creating-protected-branches.yml
@@ -0,0 +1,5 @@
+---
+title: CE port gitlab-ee!6112
+merge_request: 19714
+author:
+type: other
diff --git a/changelogs/unreleased/46571-webhooks-nil-password.yml b/changelogs/unreleased/46571-webhooks-nil-password.yml
new file mode 100644
index 00000000000..34c5f09478f
--- /dev/null
+++ b/changelogs/unreleased/46571-webhooks-nil-password.yml
@@ -0,0 +1,5 @@
+---
+title: Fix webhook error when password is not present
+merge_request: 19945
+author: Jan Beckmann
+type: fixed
diff --git a/changelogs/unreleased/46783-removed-omniauth-provider-causing-invalid-application-setting.yml b/changelogs/unreleased/46783-removed-omniauth-provider-causing-invalid-application-setting.yml
new file mode 100644
index 00000000000..d5ecf5163d4
--- /dev/null
+++ b/changelogs/unreleased/46783-removed-omniauth-provider-causing-invalid-application-setting.yml
@@ -0,0 +1,5 @@
+---
+title: Ignore unknown OAuth sources in ApplicationSetting
+merge_request: 20129
+author:
+type: fixed
diff --git a/changelogs/unreleased/46831-remove-unused-bootstrap-component-css.yml b/changelogs/unreleased/46831-remove-unused-bootstrap-component-css.yml
new file mode 100644
index 00000000000..e0e2b481b69
--- /dev/null
+++ b/changelogs/unreleased/46831-remove-unused-bootstrap-component-css.yml
@@ -0,0 +1,5 @@
+---
+title: Removes unused bootstrap 4 scss files
+merge_request: 19423
+author:
+type: deprecated
diff --git a/changelogs/unreleased/46861-issuable-title-with-longer-username.yml b/changelogs/unreleased/46861-issuable-title-with-longer-username.yml
new file mode 100644
index 00000000000..9df6879deb6
--- /dev/null
+++ b/changelogs/unreleased/46861-issuable-title-with-longer-username.yml
@@ -0,0 +1,5 @@
+---
+title: Fix CSS for buttons not to be hidden on issues/MR title
+merge_request: 19176
+author: Takuya Noguchi
+type: fixed
diff --git a/changelogs/unreleased/46963-add_readme_button_for_non_empty_project.yml b/changelogs/unreleased/46963-add_readme_button_for_non_empty_project.yml
new file mode 100644
index 00000000000..fdf41a26c4d
--- /dev/null
+++ b/changelogs/unreleased/46963-add_readme_button_for_non_empty_project.yml
@@ -0,0 +1,5 @@
+---
+title: Add readme button to non-empty project page
+merge_request: 20104
+author:
+type: fixed
diff --git a/changelogs/unreleased/47040-inconsistent-job-list-in-job-details-view.yml b/changelogs/unreleased/47040-inconsistent-job-list-in-job-details-view.yml
new file mode 100644
index 00000000000..5629a40a1f1
--- /dev/null
+++ b/changelogs/unreleased/47040-inconsistent-job-list-in-job-details-view.yml
@@ -0,0 +1,5 @@
+---
+title: Show jobs from same pipeline in sidebar in job details view.
+merge_request: 20243
+author:
+type: fixed
diff --git a/changelogs/unreleased/47050-quick-actions-case-insensitive.yml b/changelogs/unreleased/47050-quick-actions-case-insensitive.yml
new file mode 100644
index 00000000000..176aba627b9
--- /dev/null
+++ b/changelogs/unreleased/47050-quick-actions-case-insensitive.yml
@@ -0,0 +1,5 @@
+---
+title: Make quick commands case insensitive
+merge_request: 19614
+author: Jan Beckmann
+type: fixed
diff --git a/changelogs/unreleased/47145-quick-actions-confidential.yml b/changelogs/unreleased/47145-quick-actions-confidential.yml
new file mode 100644
index 00000000000..7ae4e2268af
--- /dev/null
+++ b/changelogs/unreleased/47145-quick-actions-confidential.yml
@@ -0,0 +1,5 @@
+---
+title: Add /confidential quick action
+merge_request:
+author: Jan Beckmann
+type: added
diff --git a/changelogs/unreleased/47221-explain-what-groups-are-in-the-new-group-page.yml b/changelogs/unreleased/47221-explain-what-groups-are-in-the-new-group-page.yml
new file mode 100644
index 00000000000..94c58a3863a
--- /dev/null
+++ b/changelogs/unreleased/47221-explain-what-groups-are-in-the-new-group-page.yml
@@ -0,0 +1,5 @@
+---
+title: Update new group page to better explain what groups are
+merge_request: 19991
+author:
+type: other
diff --git a/changelogs/unreleased/47274-help-users-find-our-contributing-page.yml b/changelogs/unreleased/47274-help-users-find-our-contributing-page.yml
new file mode 100644
index 00000000000..ed13c917a2e
--- /dev/null
+++ b/changelogs/unreleased/47274-help-users-find-our-contributing-page.yml
@@ -0,0 +1,5 @@
+---
+title: Add a link to the contributing page in the user dropdown
+merge_request: 19708
+author:
+type: added
diff --git a/changelogs/unreleased/47462-issues-disabled-group-page.yml b/changelogs/unreleased/47462-issues-disabled-group-page.yml
new file mode 100644
index 00000000000..c8cad608cb3
--- /dev/null
+++ b/changelogs/unreleased/47462-issues-disabled-group-page.yml
@@ -0,0 +1,6 @@
+---
+title: Only show new issue / new merge request on group page when issues / merge requests
+ are enabled
+merge_request: 19869
+author: Jan Beckmann
+type: fixed
diff --git a/changelogs/unreleased/47631-operations-kubernetes-option-is-always-visible-when-repository-or-builds-are-disabled.yml b/changelogs/unreleased/47631-operations-kubernetes-option-is-always-visible-when-repository-or-builds-are-disabled.yml
new file mode 100644
index 00000000000..5c23b3ef320
--- /dev/null
+++ b/changelogs/unreleased/47631-operations-kubernetes-option-is-always-visible-when-repository-or-builds-are-disabled.yml
@@ -0,0 +1,5 @@
+---
+title: Omits operartions and kubernetes item from project sidebar when repository or builds are disabled
+merge_request: 19835
+author:
+type: fixed
diff --git a/changelogs/unreleased/47794-environment-scope-cluster-page.yml b/changelogs/unreleased/47794-environment-scope-cluster-page.yml
new file mode 100644
index 00000000000..75eb7ec209c
--- /dev/null
+++ b/changelogs/unreleased/47794-environment-scope-cluster-page.yml
@@ -0,0 +1,6 @@
+---
+title: Change environment scope text depending on number of project clusters. Update
+ form to only include form-groups
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/47865-changelog-for-style-updates.yml b/changelogs/unreleased/47865-changelog-for-style-updates.yml
new file mode 100644
index 00000000000..2e4fbbda000
--- /dev/null
+++ b/changelogs/unreleased/47865-changelog-for-style-updates.yml
@@ -0,0 +1,5 @@
+---
+title: Minor style changes to personal access token form and scope checkboxes
+merge_request: 20052
+author:
+type: other
diff --git a/changelogs/unreleased/48050-add-full-commit-sha.yml b/changelogs/unreleased/48050-add-full-commit-sha.yml
new file mode 100644
index 00000000000..30376fe35e0
--- /dev/null
+++ b/changelogs/unreleased/48050-add-full-commit-sha.yml
@@ -0,0 +1,5 @@
+---
+title: Uses long sha version of the merged commit in MR widget copy to clipboard button
+merge_request: 19955
+author:
+type: other
diff --git a/changelogs/unreleased/48100-fix-branch-not-shown.yml b/changelogs/unreleased/48100-fix-branch-not-shown.yml
new file mode 100644
index 00000000000..917c5c23f67
--- /dev/null
+++ b/changelogs/unreleased/48100-fix-branch-not-shown.yml
@@ -0,0 +1,6 @@
+---
+title: Fix branches are not shown in Merge Request dropdown when preferred language
+ is not English
+merge_request: 20016
+author: Hiroyuki Sato
+type: fixed
diff --git a/changelogs/unreleased/48153-date-selection-dialog-broken-when-creating-a-new-milestone.yml b/changelogs/unreleased/48153-date-selection-dialog-broken-when-creating-a-new-milestone.yml
new file mode 100644
index 00000000000..13ab5b0467d
--- /dev/null
+++ b/changelogs/unreleased/48153-date-selection-dialog-broken-when-creating-a-new-milestone.yml
@@ -0,0 +1,5 @@
+---
+title: Prevent browser autocomplete for milestone date fields
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/48237-toggle-file-comments.yml b/changelogs/unreleased/48237-toggle-file-comments.yml
new file mode 100644
index 00000000000..2e893aad0b2
--- /dev/null
+++ b/changelogs/unreleased/48237-toggle-file-comments.yml
@@ -0,0 +1,5 @@
+---
+title: Fixes toggle discussion button not expanding collapsed discussions
+merge_request: 20452
+author:
+type: fixed
diff --git a/changelogs/unreleased/48378-avatar-upload.yml b/changelogs/unreleased/48378-avatar-upload.yml
new file mode 100644
index 00000000000..1e359ee72d5
--- /dev/null
+++ b/changelogs/unreleased/48378-avatar-upload.yml
@@ -0,0 +1,5 @@
+---
+title: Fixes issue with uploading same image to Profile Avatar twice
+merge_request: 20161
+author: Chirag Bhatia
+type: fixed
diff --git a/changelogs/unreleased/48497-merge-request-refactor-displays-changes-dropdown-incorrectly.yml b/changelogs/unreleased/48497-merge-request-refactor-displays-changes-dropdown-incorrectly.yml
new file mode 100644
index 00000000000..41af2f8cc4f
--- /dev/null
+++ b/changelogs/unreleased/48497-merge-request-refactor-displays-changes-dropdown-incorrectly.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed Merge request changes dropdown displays incorrectly
+merge_request: 20237
+author: Constance Okoghenun
+type: fixed
diff --git a/changelogs/unreleased/48515-sql-queries-are-not-shown-from-the-performance-bar-in-safari.yml b/changelogs/unreleased/48515-sql-queries-are-not-shown-from-the-performance-bar-in-safari.yml
new file mode 100644
index 00000000000..65c59dbf31f
--- /dev/null
+++ b/changelogs/unreleased/48515-sql-queries-are-not-shown-from-the-performance-bar-in-safari.yml
@@ -0,0 +1,5 @@
+---
+title: Fix performance bar modal visibility in Safari
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/48537-update-avatar-only-via-api.yml b/changelogs/unreleased/48537-update-avatar-only-via-api.yml
new file mode 100644
index 00000000000..9b3ab946cc1
--- /dev/null
+++ b/changelogs/unreleased/48537-update-avatar-only-via-api.yml
@@ -0,0 +1,5 @@
+---
+title: Allow updating a project's avatar without other params
+merge_request:
+author: Jamie Schembri
+type: fixed
diff --git a/changelogs/unreleased/48578-disable-gcp-free-credit-banner-at-instance-level.yml b/changelogs/unreleased/48578-disable-gcp-free-credit-banner-at-instance-level.yml
new file mode 100644
index 00000000000..575767df912
--- /dev/null
+++ b/changelogs/unreleased/48578-disable-gcp-free-credit-banner-at-instance-level.yml
@@ -0,0 +1,5 @@
+---
+title: Add option to hide third party offers in admin application settings
+merge_request: 20379
+author:
+type: added
diff --git a/changelogs/unreleased/48603-merge-request-refactor-title-and-copy-to-clipboard-button-are-behind-the-action-buttons.yml b/changelogs/unreleased/48603-merge-request-refactor-title-and-copy-to-clipboard-button-are-behind-the-action-buttons.yml
new file mode 100644
index 00000000000..792c7814f7e
--- /dev/null
+++ b/changelogs/unreleased/48603-merge-request-refactor-title-and-copy-to-clipboard-button-are-behind-the-action-buttons.yml
@@ -0,0 +1,5 @@
+---
+title: Fix overlapping file title and file actions in MR changes tag
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/48634-header-navbar-line-separator-is-missing.yml b/changelogs/unreleased/48634-header-navbar-line-separator-is-missing.yml
new file mode 100644
index 00000000000..92d9295982e
--- /dev/null
+++ b/changelogs/unreleased/48634-header-navbar-line-separator-is-missing.yml
@@ -0,0 +1,5 @@
+---
+title: Line separator to the left of the 'Admin area' wrench icon had vanished
+merge_request: 20282
+author: bitsapien
+type: fixed
diff --git a/changelogs/unreleased/48661-node-6-and-7-compatibility-broken-by-recent-monaco-editor-upgrade.yml b/changelogs/unreleased/48661-node-6-and-7-compatibility-broken-by-recent-monaco-editor-upgrade.yml
new file mode 100644
index 00000000000..36a4b5f754d
--- /dev/null
+++ b/changelogs/unreleased/48661-node-6-and-7-compatibility-broken-by-recent-monaco-editor-upgrade.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve compatibility issues with node 6
+merge_request: 20461
+author:
+type: fixed
diff --git a/changelogs/unreleased/48670-application-settings-may-not-be-invalidated-if-migrations-are-run.yml b/changelogs/unreleased/48670-application-settings-may-not-be-invalidated-if-migrations-are-run.yml
new file mode 100644
index 00000000000..f4267582f89
--- /dev/null
+++ b/changelogs/unreleased/48670-application-settings-may-not-be-invalidated-if-migrations-are-run.yml
@@ -0,0 +1,6 @@
+---
+title: Stop relying on migrations in the CacheableAttributes cache key and cache attributes
+ for 1 minute instead
+merge_request: 20389
+author:
+type: fixed
diff --git a/changelogs/unreleased/48677-also-check-auto_sign_in_with_provider.yml b/changelogs/unreleased/48677-also-check-auto_sign_in_with_provider.yml
new file mode 100644
index 00000000000..3021fe6b9c8
--- /dev/null
+++ b/changelogs/unreleased/48677-also-check-auto_sign_in_with_provider.yml
@@ -0,0 +1,5 @@
+---
+title: Load Devise with Omniauth when auto_sign_in_with_provider is configured
+merge_request: 20302
+author:
+type: fixed
diff --git a/changelogs/unreleased/48825-performance.yml b/changelogs/unreleased/48825-performance.yml
new file mode 100644
index 00000000000..428852f6f8b
--- /dev/null
+++ b/changelogs/unreleased/48825-performance.yml
@@ -0,0 +1,8 @@
+---
+title: Improves performance of mr code, by fixing the state being mutated outside
+ of the store in the util function trimFirstCharOfLineContent and in map operations.
+ Avoids map operation in an empty array. Adds specs to the trimFirstCharOfLineContent
+ function
+merge_request: 20380
+author: filipa
+type: performance
diff --git a/changelogs/unreleased/48934.yml b/changelogs/unreleased/48934.yml
new file mode 100644
index 00000000000..8e2e53ed198
--- /dev/null
+++ b/changelogs/unreleased/48934.yml
@@ -0,0 +1,5 @@
+---
+title: Improve danger confirmation modals by focusing input field
+merge_request:
+author: Jamie Schembri
+type: added
diff --git a/changelogs/unreleased/48951-clean-up.yml b/changelogs/unreleased/48951-clean-up.yml
new file mode 100644
index 00000000000..0102cd43f96
--- /dev/null
+++ b/changelogs/unreleased/48951-clean-up.yml
@@ -0,0 +1,5 @@
+---
+title: Removes unused vuex code in mr refactor and removes unneeded dependencies
+merge_request: 20499
+author:
+type: other
diff --git a/changelogs/unreleased/48976-fix-sti-background-migration.yml b/changelogs/unreleased/48976-fix-sti-background-migration.yml
new file mode 100644
index 00000000000..e95536b213c
--- /dev/null
+++ b/changelogs/unreleased/48976-fix-sti-background-migration.yml
@@ -0,0 +1,6 @@
+---
+title: "[Rails5] Fix 'Invalid single-table inheritance type: Group is not a subclass
+ of Gitlab::BackgroundMigration::FixCrossProjectLabelLinks::Namespace'"
+merge_request: 20462
+author: "@blackst0ne"
+type: fixed
diff --git a/changelogs/unreleased/48978-fix-helm-installation-on-cluster.yml b/changelogs/unreleased/48978-fix-helm-installation-on-cluster.yml
new file mode 100644
index 00000000000..f786d9e2235
--- /dev/null
+++ b/changelogs/unreleased/48978-fix-helm-installation-on-cluster.yml
@@ -0,0 +1,5 @@
+---
+title: Fixes base command used in Helm installations
+merge_request: 20471
+author:
+type: fixed
diff --git a/changelogs/unreleased/49114-add-gitaly-servers-to-admin-overview-navigation-menu.yml b/changelogs/unreleased/49114-add-gitaly-servers-to-admin-overview-navigation-menu.yml
new file mode 100644
index 00000000000..ab320450a1a
--- /dev/null
+++ b/changelogs/unreleased/49114-add-gitaly-servers-to-admin-overview-navigation-menu.yml
@@ -0,0 +1,5 @@
+---
+title: Gitaly Servers link into Admin > Overview navigation menu
+merge_request: 20550
+author:
+type: added
diff --git a/changelogs/unreleased/ab-43706-composite-primary-keys.yml b/changelogs/unreleased/ab-43706-composite-primary-keys.yml
deleted file mode 100644
index b17050a64c8..00000000000
--- a/changelogs/unreleased/ab-43706-composite-primary-keys.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add NOT NULL constraints to project_authorizations.
-merge_request: 18980
-author:
-type: other
diff --git a/changelogs/unreleased/ab-46530-mediumtext-for-gpg-keys.yml b/changelogs/unreleased/ab-46530-mediumtext-for-gpg-keys.yml
deleted file mode 100644
index 88ef62ebc0e..00000000000
--- a/changelogs/unreleased/ab-46530-mediumtext-for-gpg-keys.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Increase text limit for GPG keys (mysql only).
-merge_request: 19069
-author:
-type: other
diff --git a/changelogs/unreleased/add-dst-support-to-pipeline-schedule.yml b/changelogs/unreleased/add-dst-support-to-pipeline-schedule.yml
new file mode 100644
index 00000000000..08376014ad7
--- /dev/null
+++ b/changelogs/unreleased/add-dst-support-to-pipeline-schedule.yml
@@ -0,0 +1,5 @@
+---
+title: Add support for daylight savings time to pipleline schedules
+merge_request: 20145
+author:
+type: fixed
diff --git a/changelogs/unreleased/add-missing-index-for-deployments.yml b/changelogs/unreleased/add-missing-index-for-deployments.yml
new file mode 100644
index 00000000000..7863c0ee039
--- /dev/null
+++ b/changelogs/unreleased/add-missing-index-for-deployments.yml
@@ -0,0 +1,5 @@
+---
+title: Add index on deployable_type/id for deployments
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/add-more-rebase-logging.yml b/changelogs/unreleased/add-more-rebase-logging.yml
new file mode 100644
index 00000000000..a7d1c3aa664
--- /dev/null
+++ b/changelogs/unreleased/add-more-rebase-logging.yml
@@ -0,0 +1,5 @@
+---
+title: Add more detailed logging to githost.log when rebasing
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/add-title-placeholder-for-new-issues.yml b/changelogs/unreleased/add-title-placeholder-for-new-issues.yml
new file mode 100644
index 00000000000..ce9e3b4ac18
--- /dev/null
+++ b/changelogs/unreleased/add-title-placeholder-for-new-issues.yml
@@ -0,0 +1,5 @@
+---
+title: Add title placeholder for new issues
+merge_request: 20271
+author: George Tsiolis
+type: changed
diff --git a/changelogs/unreleased/author-doc-fix.yml b/changelogs/unreleased/author-doc-fix.yml
new file mode 100644
index 00000000000..83521543239
--- /dev/null
+++ b/changelogs/unreleased/author-doc-fix.yml
@@ -0,0 +1,5 @@
+---
+title: Fix fields for author & assignee in MR API docs.
+merge_request: 19798
+author: gfyoung
+type: fixed
diff --git a/changelogs/unreleased/backstage-gb-stages-position-migration-clean-up.yml b/changelogs/unreleased/backstage-gb-stages-position-migration-clean-up.yml
new file mode 100644
index 00000000000..d2ada88870b
--- /dev/null
+++ b/changelogs/unreleased/backstage-gb-stages-position-migration-clean-up.yml
@@ -0,0 +1,5 @@
+---
+title: Fully migrate pipeline stages position
+merge_request: 19369
+author:
+type: performance
diff --git a/changelogs/unreleased/bjk-48176_ruby_gc.yml b/changelogs/unreleased/bjk-48176_ruby_gc.yml
new file mode 100644
index 00000000000..45c6338df81
--- /dev/null
+++ b/changelogs/unreleased/bjk-48176_ruby_gc.yml
@@ -0,0 +1,5 @@
+---
+title: Cleanup Prometheus ruby metrics
+merge_request: 20039
+author: Ben Kochie
+type: fixed
diff --git a/changelogs/unreleased/blackst0ne-add-gemfile-rails5-lock-check.yml b/changelogs/unreleased/blackst0ne-add-gemfile-rails5-lock-check.yml
new file mode 100644
index 00000000000..69d49f3e3e0
--- /dev/null
+++ b/changelogs/unreleased/blackst0ne-add-gemfile-rails5-lock-check.yml
@@ -0,0 +1,5 @@
+---
+title: Add CI job to check Gemfile.rails5.lock
+merge_request: 19605
+author: "@blackst0ne"
+type: other
diff --git a/changelogs/unreleased/blackst0ne-bump-grape-path-helpers-gem-to-1-0-5.yml b/changelogs/unreleased/blackst0ne-bump-grape-path-helpers-gem-to-1-0-5.yml
new file mode 100644
index 00000000000..9d975ff81bf
--- /dev/null
+++ b/changelogs/unreleased/blackst0ne-bump-grape-path-helpers-gem-to-1-0-5.yml
@@ -0,0 +1,5 @@
+---
+title: Bump grape-path-helpers to 1.0.5
+merge_request: 19604
+author: "@blackst0ne"
+type: other
diff --git a/changelogs/unreleased/blackst0ne-fix-protect-from-forgery-in-application-controller.yml b/changelogs/unreleased/blackst0ne-fix-protect-from-forgery-in-application-controller.yml
new file mode 100644
index 00000000000..da75ea8b09e
--- /dev/null
+++ b/changelogs/unreleased/blackst0ne-fix-protect-from-forgery-in-application-controller.yml
@@ -0,0 +1,5 @@
+---
+title: "[Rails5] Force the callback run first"
+merge_request: 20055
+author: "@blackst0ne"
+type: fixed
diff --git a/changelogs/unreleased/blackst0ne-rails5-activerecord-statementinvalid-mysql2-error-expression-1-of-select-list-is-not-in-group-by-clause.yml b/changelogs/unreleased/blackst0ne-rails5-activerecord-statementinvalid-mysql2-error-expression-1-of-select-list-is-not-in-group-by-clause.yml
new file mode 100644
index 00000000000..d9cccc49830
--- /dev/null
+++ b/changelogs/unreleased/blackst0ne-rails5-activerecord-statementinvalid-mysql2-error-expression-1-of-select-list-is-not-in-group-by-clause.yml
@@ -0,0 +1,5 @@
+---
+title: "[Rails5] Fix milestone GROUP BY query"
+merge_request: 20256
+author: "@blackst0ne"
+type: fixed
diff --git a/changelogs/unreleased/blackst0ne-rails5-expected-search-search-seed_project-got-nil.yml b/changelogs/unreleased/blackst0ne-rails5-expected-search-search-seed_project-got-nil.yml
new file mode 100644
index 00000000000..e7bb2703b03
--- /dev/null
+++ b/changelogs/unreleased/blackst0ne-rails5-expected-search-search-seed_project-got-nil.yml
@@ -0,0 +1,5 @@
+---
+title: "[Rails5] Fix sessions_controller_spec"
+merge_request: 19936
+author: "@blackst0ne"
+type: fixed
diff --git a/changelogs/unreleased/blackst0ne-rails5-expected-the-response-to-have-status-code-ok-but-it-was-404.yml b/changelogs/unreleased/blackst0ne-rails5-expected-the-response-to-have-status-code-ok-but-it-was-404.yml
new file mode 100644
index 00000000000..fad15de2dd5
--- /dev/null
+++ b/changelogs/unreleased/blackst0ne-rails5-expected-the-response-to-have-status-code-ok-but-it-was-404.yml
@@ -0,0 +1,5 @@
+---
+title: "[Rails5] Set request.format for artifacts_controller"
+merge_request: 19937
+author: "@blackst0ne"
+type: fixed
diff --git a/changelogs/unreleased/blackst0ne-rails5-fix-blob-requests-format.yml b/changelogs/unreleased/blackst0ne-rails5-fix-blob-requests-format.yml
new file mode 100644
index 00000000000..a83aa03606a
--- /dev/null
+++ b/changelogs/unreleased/blackst0ne-rails5-fix-blob-requests-format.yml
@@ -0,0 +1,5 @@
+---
+title: "[Rails5] Explicitly set request.format for blob_controller"
+merge_request: 19876
+author: "@blackst0ne"
+type: fixed
diff --git a/changelogs/unreleased/blackst0ne-rails5-fix-data-store-spec.yml b/changelogs/unreleased/blackst0ne-rails5-fix-data-store-spec.yml
new file mode 100644
index 00000000000..403c3764321
--- /dev/null
+++ b/changelogs/unreleased/blackst0ne-rails5-fix-data-store-spec.yml
@@ -0,0 +1,5 @@
+---
+title: '[Rails5] Fix "-1 is not a valid data_store"'
+merge_request: 19917
+author: "@blackst0ne"
+type: fixed
diff --git a/changelogs/unreleased/blackst0ne-rails5-fix-optimistic-lock-values.yml b/changelogs/unreleased/blackst0ne-rails5-fix-optimistic-lock-values.yml
new file mode 100644
index 00000000000..1915dff73ab
--- /dev/null
+++ b/changelogs/unreleased/blackst0ne-rails5-fix-optimistic-lock-values.yml
@@ -0,0 +1,5 @@
+---
+title: "[Rails5] Fix optimistic lock value"
+merge_request: 19878
+author: "@blackst0ne"
+type: fixed
diff --git a/changelogs/unreleased/blackst0ne-rails5-fix-pipeline-schedules-controller-spec.yml b/changelogs/unreleased/blackst0ne-rails5-fix-pipeline-schedules-controller-spec.yml
new file mode 100644
index 00000000000..7a2b19ad681
--- /dev/null
+++ b/changelogs/unreleased/blackst0ne-rails5-fix-pipeline-schedules-controller-spec.yml
@@ -0,0 +1,5 @@
+---
+title: "[Rails5] Fix pipeline_schedules_controller_spec"
+merge_request: 19919
+author: "@blackst0ne"
+type: fixed
diff --git a/changelogs/unreleased/blackst0ne-rails5-fix-snippets-finder.yml b/changelogs/unreleased/blackst0ne-rails5-fix-snippets-finder.yml
new file mode 100644
index 00000000000..597b85de26f
--- /dev/null
+++ b/changelogs/unreleased/blackst0ne-rails5-fix-snippets-finder.yml
@@ -0,0 +1,5 @@
+---
+title: "[Rails5] Fix snippets_finder arel queries"
+merge_request: 19796
+author: "@blackst0ne"
+type: fixed
diff --git a/changelogs/unreleased/blackst0ne-rails5-found-new-routes-that-could-cause-conflicts-with-existing-namespaced-routes.yml b/changelogs/unreleased/blackst0ne-rails5-found-new-routes-that-could-cause-conflicts-with-existing-namespaced-routes.yml
new file mode 100644
index 00000000000..c8d916af824
--- /dev/null
+++ b/changelogs/unreleased/blackst0ne-rails5-found-new-routes-that-could-cause-conflicts-with-existing-namespaced-routes.yml
@@ -0,0 +1,5 @@
+---
+title: "[Rails5] Fix ActionCable '/cable' mountpoint conflict"
+merge_request: 20015
+author: "@blackst0ne"
+type: fixed
diff --git a/changelogs/unreleased/blackst0ne-rails5-invalid-single-table-inheritance-type-group-is-not-a-subclass-of-namespace.yml b/changelogs/unreleased/blackst0ne-rails5-invalid-single-table-inheritance-type-group-is-not-a-subclass-of-namespace.yml
new file mode 100644
index 00000000000..92e6ce35941
--- /dev/null
+++ b/changelogs/unreleased/blackst0ne-rails5-invalid-single-table-inheritance-type-group-is-not-a-subclass-of-namespace.yml
@@ -0,0 +1,6 @@
+---
+title: "[Rails5] Invalid single-table inheritance type: Group is not a subclass of
+ Namespace"
+merge_request: 19918
+author: "@blackst0ne"
+type: fixed
diff --git a/changelogs/unreleased/blackst0ne-rails5-set-request-format-in--commits-controller.yml b/changelogs/unreleased/blackst0ne-rails5-set-request-format-in--commits-controller.yml
new file mode 100644
index 00000000000..3f8f8fd5d66
--- /dev/null
+++ b/changelogs/unreleased/blackst0ne-rails5-set-request-format-in--commits-controller.yml
@@ -0,0 +1,5 @@
+---
+title: "[Rails5] Set request.format in commits_controller"
+merge_request: 20023
+author: "@blackst0ne"
+type: fixed
diff --git a/changelogs/unreleased/blackst0ne-remove-spinach.yml b/changelogs/unreleased/blackst0ne-remove-spinach.yml
deleted file mode 100644
index 104da257bad..00000000000
--- a/changelogs/unreleased/blackst0ne-remove-spinach.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove Spinach
-merge_request: 18869
-author: '@blackst0ne'
-type: other
diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-deploy-keys-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-deploy-keys-feature.yml
deleted file mode 100644
index 7014de4ece7..00000000000
--- a/changelogs/unreleased/blackst0ne-replace-spinach-project-deploy-keys-feature.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'Replace the `project/deploy_keys.feature` spinach test with an rspec analog'
-merge_request: 18796
-author: '@blackst0ne'
-type: other
diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-ff-merge-requests-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-ff-merge-requests-feature.yml
deleted file mode 100644
index 7802391ec64..00000000000
--- a/changelogs/unreleased/blackst0ne-replace-spinach-project-ff-merge-requests-feature.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'Replace the `project/ff_merge_requests.feature` spinach test with an rspec analog'
-merge_request: 18800
-author: '@blackst0ne'
-type: other
diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-forked-merge-requests-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-forked-merge-requests-feature.yml
deleted file mode 100644
index 2ac43490c26..00000000000
--- a/changelogs/unreleased/blackst0ne-replace-spinach-project-forked-merge-requests-feature.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'Replace the `project/forked_merge_requests.feature` spinach test with an rspec analog'
-merge_request: 18867
-author: '@blackst0ne'
-type: other
diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-issues-references-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-issues-references-feature.yml
deleted file mode 100644
index 968a937ca5a..00000000000
--- a/changelogs/unreleased/blackst0ne-replace-spinach-project-issues-references-feature.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'Replace the `project/issues/references.feature` spinach test with an rspec analog'
-merge_request: 18769
-author: '@blackst0ne'
-type: other
diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-merge-requests-references-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-merge-requests-references-feature.yml
deleted file mode 100644
index c0ba984bfdc..00000000000
--- a/changelogs/unreleased/blackst0ne-replace-spinach-project-merge-requests-references-feature.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'Replace the `project/merge_requests/references.feature` spinach test with an rspec analog'
-merge_request: 18794
-author: '@blackst0ne'
-type: other
diff --git a/changelogs/unreleased/build-chunks-on-object-storage.yml b/changelogs/unreleased/build-chunks-on-object-storage.yml
new file mode 100644
index 00000000000..9f36dfee378
--- /dev/null
+++ b/changelogs/unreleased/build-chunks-on-object-storage.yml
@@ -0,0 +1,6 @@
+---
+title: Use object storage as the first class persistable store for new live trace
+ architecture
+merge_request: 19515
+author:
+type: changed
diff --git a/changelogs/unreleased/bump-carrierwave-to-1-2-3.yml b/changelogs/unreleased/bump-carrierwave-to-1-2-3.yml
new file mode 100644
index 00000000000..373ac48553e
--- /dev/null
+++ b/changelogs/unreleased/bump-carrierwave-to-1-2-3.yml
@@ -0,0 +1,5 @@
+---
+title: Bump carrierwave gem verion to 1.2.3
+merge_request: 20287
+author:
+type: performance
diff --git a/changelogs/unreleased/bvl-fix-maintainer-push-rejected.yml b/changelogs/unreleased/bvl-fix-maintainer-push-rejected.yml
new file mode 100644
index 00000000000..54154ad2449
--- /dev/null
+++ b/changelogs/unreleased/bvl-fix-maintainer-push-rejected.yml
@@ -0,0 +1,6 @@
+---
+title: Fix bug where maintainer would not be allowed to push to forks with merge requests
+ that have `Allow maintainer edits` enabled.
+merge_request: 18968
+author:
+type: fixed
diff --git a/changelogs/unreleased/bvl-graphql-nested-merge-request.yml b/changelogs/unreleased/bvl-graphql-nested-merge-request.yml
new file mode 100644
index 00000000000..f0f0488d31a
--- /dev/null
+++ b/changelogs/unreleased/bvl-graphql-nested-merge-request.yml
@@ -0,0 +1,5 @@
+---
+title: Allow querying a single merge request within a project
+merge_request: 19853
+author:
+type: changed
diff --git a/changelogs/unreleased/bvl-graphql-permissions.yml b/changelogs/unreleased/bvl-graphql-permissions.yml
new file mode 100644
index 00000000000..42d5e24bb15
--- /dev/null
+++ b/changelogs/unreleased/bvl-graphql-permissions.yml
@@ -0,0 +1,5 @@
+---
+title: 'Expose permissions of the current user on resources in GraphQL'
+merge_request: 20152
+author:
+type: added
diff --git a/changelogs/unreleased/bvl-graphql-pipeline-lists.yml b/changelogs/unreleased/bvl-graphql-pipeline-lists.yml
new file mode 100644
index 00000000000..be258dc12ad
--- /dev/null
+++ b/changelogs/unreleased/bvl-graphql-pipeline-lists.yml
@@ -0,0 +1,5 @@
+---
+title: Add pipeline lists to GraphQL
+merge_request: 20249
+author:
+type: added
diff --git a/changelogs/unreleased/bvl-preload-parents-after-pagination.yml b/changelogs/unreleased/bvl-preload-parents-after-pagination.yml
new file mode 100644
index 00000000000..ff3d4716d34
--- /dev/null
+++ b/changelogs/unreleased/bvl-preload-parents-after-pagination.yml
@@ -0,0 +1,5 @@
+---
+title: Reduce the number of queries when searching for groups
+merge_request: 20398
+author:
+type: performance
diff --git a/changelogs/unreleased/bw-enable-commonmark.yml b/changelogs/unreleased/bw-enable-commonmark.yml
new file mode 100644
index 00000000000..89252e5063d
--- /dev/null
+++ b/changelogs/unreleased/bw-enable-commonmark.yml
@@ -0,0 +1,5 @@
+---
+title: Use CommonMark syntax and rendering for new Markdown content
+merge_request: 19331
+author:
+type: added
diff --git a/changelogs/unreleased/cache-doc-fix.yml b/changelogs/unreleased/cache-doc-fix.yml
new file mode 100644
index 00000000000..db4726a92e9
--- /dev/null
+++ b/changelogs/unreleased/cache-doc-fix.yml
@@ -0,0 +1,5 @@
+---
+title: 'Remove incorrect CI doc re: PowerShell'
+merge_request: 19622
+author: gfyoung
+type: fixed
diff --git a/changelogs/unreleased/ccr-incoming-email-regex-anchor.yml b/changelogs/unreleased/ccr-incoming-email-regex-anchor.yml
deleted file mode 100644
index a0d787e570e..00000000000
--- a/changelogs/unreleased/ccr-incoming-email-regex-anchor.yml
+++ /dev/null
@@ -1,3 +0,0 @@
-title: Add anchor for incoming email regex
-merge_request: !18917
-type: added
diff --git a/changelogs/unreleased/ce-5024-filename-search.yml b/changelogs/unreleased/ce-5024-filename-search.yml
new file mode 100644
index 00000000000..a8bf9b1f802
--- /dev/null
+++ b/changelogs/unreleased/ce-5024-filename-search.yml
@@ -0,0 +1,5 @@
+---
+title: Add filename filtering to code search
+merge_request: 19509
+author:
+type: added
diff --git a/changelogs/unreleased/close-revoke-deploy-token-modal-on-escape-keypress.yml b/changelogs/unreleased/close-revoke-deploy-token-modal-on-escape-keypress.yml
new file mode 100644
index 00000000000..98316cae406
--- /dev/null
+++ b/changelogs/unreleased/close-revoke-deploy-token-modal-on-escape-keypress.yml
@@ -0,0 +1,5 @@
+---
+title: Close revoke deploy token modal on escape keypress
+merge_request: 20347
+author: George Tsiolis
+type: changed
diff --git a/changelogs/unreleased/collapsed-contextual-nav-update.yml b/changelogs/unreleased/collapsed-contextual-nav-update.yml
deleted file mode 100644
index 31a32a9e1e9..00000000000
--- a/changelogs/unreleased/collapsed-contextual-nav-update.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Renamed 'Overview' to 'Project' in collapsed contextual navigation at a project
- level
-merge_request: 18996
-author: Constance Okoghenun
-type: fixed
diff --git a/changelogs/unreleased/commits_api_with_stats.yml b/changelogs/unreleased/commits_api_with_stats.yml
new file mode 100644
index 00000000000..4357f1a6305
--- /dev/null
+++ b/changelogs/unreleased/commits_api_with_stats.yml
@@ -0,0 +1,5 @@
+---
+title: Added with_statsoption for GET /projects/:id/repository/commits
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/cr-add-locked-state-to-MR.yml b/changelogs/unreleased/cr-add-locked-state-to-MR.yml
new file mode 100644
index 00000000000..f290ddc0b87
--- /dev/null
+++ b/changelogs/unreleased/cr-add-locked-state-to-MR.yml
@@ -0,0 +1,5 @@
+---
+title: Adds the `locked` state to the merge request API so that it can be used as a search filter.
+merge_request: 20186
+author:
+type: fixed
diff --git a/changelogs/unreleased/cr-keep-issue-labels.yml b/changelogs/unreleased/cr-keep-issue-labels.yml
new file mode 100644
index 00000000000..051e7faffea
--- /dev/null
+++ b/changelogs/unreleased/cr-keep-issue-labels.yml
@@ -0,0 +1,5 @@
+---
+title: Keeps the label on an issue when the issue is moved.
+merge_request: 20036
+author:
+type: fixed
diff --git a/changelogs/unreleased/create-live-trace-only-if-job-is-complete.yml b/changelogs/unreleased/create-live-trace-only-if-job-is-complete.yml
deleted file mode 100644
index f32c70cf884..00000000000
--- a/changelogs/unreleased/create-live-trace-only-if-job-is-complete.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Forbid to patch traces for finished jobs
-merge_request: 18969
-author:
-type: fixed
diff --git a/changelogs/unreleased/da-port-cte-to-ce.yml b/changelogs/unreleased/da-port-cte-to-ce.yml
new file mode 100644
index 00000000000..6fa759fcf7d
--- /dev/null
+++ b/changelogs/unreleased/da-port-cte-to-ce.yml
@@ -0,0 +1,5 @@
+---
+title: Add Gitlab::SQL:CTE for easily building CTE statements
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/db-configure-after-drop-tables.yml b/changelogs/unreleased/db-configure-after-drop-tables.yml
new file mode 100644
index 00000000000..00844b334fa
--- /dev/null
+++ b/changelogs/unreleased/db-configure-after-drop-tables.yml
@@ -0,0 +1,5 @@
+---
+title: Fixes an issue where migrations instead of schema loading were run
+merge_request: 20227
+author:
+type: changed
diff --git a/changelogs/unreleased/dm-blockquote-trailing-whitespace.yml b/changelogs/unreleased/dm-blockquote-trailing-whitespace.yml
new file mode 100644
index 00000000000..98ecdde4f4c
--- /dev/null
+++ b/changelogs/unreleased/dm-blockquote-trailing-whitespace.yml
@@ -0,0 +1,5 @@
+---
+title: Allow trailing whitespace on blockquote fence lines
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/dm-branch-api-can-push.yml b/changelogs/unreleased/dm-branch-api-can-push.yml
new file mode 100644
index 00000000000..3be8962089b
--- /dev/null
+++ b/changelogs/unreleased/dm-branch-api-can-push.yml
@@ -0,0 +1,5 @@
+---
+title: Expose whether current user can push into a branch on branches API
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/dm-invalid-active-service-template.yml b/changelogs/unreleased/dm-invalid-active-service-template.yml
new file mode 100644
index 00000000000..8b77fac55b9
--- /dev/null
+++ b/changelogs/unreleased/dm-invalid-active-service-template.yml
@@ -0,0 +1,5 @@
+---
+title: Deactivate new KubernetesService created from active template to prevent project creation from failing
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/dm-label-reference-period.yml b/changelogs/unreleased/dm-label-reference-period.yml
new file mode 100644
index 00000000000..9fdd590641d
--- /dev/null
+++ b/changelogs/unreleased/dm-label-reference-period.yml
@@ -0,0 +1,5 @@
+---
+title: Properly detect label reference if followed by period or question mark
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/dm-user-without-projects-performance.yml b/changelogs/unreleased/dm-user-without-projects-performance.yml
new file mode 100644
index 00000000000..e7fc0ae6d54
--- /dev/null
+++ b/changelogs/unreleased/dm-user-without-projects-performance.yml
@@ -0,0 +1,5 @@
+---
+title: Improve performance of listing users without projects
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/docs-42067-document-runner-registration-api.yml b/changelogs/unreleased/docs-42067-document-runner-registration-api.yml
deleted file mode 100644
index 6b507174044..00000000000
--- a/changelogs/unreleased/docs-42067-document-runner-registration-api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Expand documentation for Runners API
-merge_request: 16484
-author:
-type: other
diff --git a/changelogs/unreleased/dz-add-2fa-filter.yml b/changelogs/unreleased/dz-add-2fa-filter.yml
deleted file mode 100644
index 82d501d6604..00000000000
--- a/changelogs/unreleased/dz-add-2fa-filter.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add 2FA filter to the group members page
-merge_request: 18483
-author:
-type: changed
diff --git a/changelogs/unreleased/existing-gcp-accounts.yml b/changelogs/unreleased/existing-gcp-accounts.yml
new file mode 100644
index 00000000000..ce396c70b4a
--- /dev/null
+++ b/changelogs/unreleased/existing-gcp-accounts.yml
@@ -0,0 +1,5 @@
+---
+title: Add back copy for existing gcp accounts within offer banner
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/expose-ci-url.yml b/changelogs/unreleased/expose-ci-url.yml
new file mode 100644
index 00000000000..b6ad7d18e0d
--- /dev/null
+++ b/changelogs/unreleased/expose-ci-url.yml
@@ -0,0 +1,5 @@
+---
+title: Add CI_PIPELINE_URL and CI_JOB_URL
+merge_request: 19618
+author:
+type: added
diff --git a/changelogs/unreleased/feature-expose-runner-ip-to-api.yml b/changelogs/unreleased/feature-expose-runner-ip-to-api.yml
deleted file mode 100644
index e755cf5f2d4..00000000000
--- a/changelogs/unreleased/feature-expose-runner-ip-to-api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Expose runner ip address to runners API
-merge_request: 18799
-author: Lars Greiss
-type: changed
diff --git a/changelogs/unreleased/feature-gb-add-regexp-variables-expression.yml b/changelogs/unreleased/feature-gb-add-regexp-variables-expression.yml
deleted file mode 100644
index d77c5b42497..00000000000
--- a/changelogs/unreleased/feature-gb-add-regexp-variables-expression.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add support for variables expression pattern matching syntax
-merge_request: 18902
-author:
-type: added
diff --git a/changelogs/unreleased/feature-oidc-subject-claim.yml b/changelogs/unreleased/feature-oidc-subject-claim.yml
new file mode 100644
index 00000000000..e995ca26234
--- /dev/null
+++ b/changelogs/unreleased/feature-oidc-subject-claim.yml
@@ -0,0 +1,5 @@
+---
+title: Don't hash user ID in OIDC subject claim
+merge_request: 19784
+author: Markus Koller
+type: changed
diff --git a/changelogs/unreleased/fix-assignee-name-wrap.yml b/changelogs/unreleased/fix-assignee-name-wrap.yml
deleted file mode 100644
index 2407288785f..00000000000
--- a/changelogs/unreleased/fix-assignee-name-wrap.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Wrapping problem on the issues page has been fixed
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-boards-issue-highlight.yml b/changelogs/unreleased/fix-boards-issue-highlight.yml
new file mode 100644
index 00000000000..0cc3faa81ca
--- /dev/null
+++ b/changelogs/unreleased/fix-boards-issue-highlight.yml
@@ -0,0 +1,5 @@
+---
+title: Fix boards issue highlight
+merge_request: 20063
+author: George Tsiolis
+type: changed
diff --git a/changelogs/unreleased/fix-br-decode.yml b/changelogs/unreleased/fix-br-decode.yml
new file mode 100644
index 00000000000..66ecc3deb35
--- /dev/null
+++ b/changelogs/unreleased/fix-br-decode.yml
@@ -0,0 +1,5 @@
+---
+title: mergeError message has been binded using v-html directive
+merge_request: 19058
+author: Murat Dogan
+type: fixed
diff --git a/changelogs/unreleased/fix-devops-remove-beta.yml b/changelogs/unreleased/fix-devops-remove-beta.yml
deleted file mode 100644
index 326003eb956..00000000000
--- a/changelogs/unreleased/fix-devops-remove-beta.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Removed "(Beta)" from "Auto DevOps" messages
-merge_request: 18759
-author:
-type: changed
diff --git a/changelogs/unreleased/fix-gb-exclude-persisted-variables-from-environment-name.yml b/changelogs/unreleased/fix-gb-exclude-persisted-variables-from-environment-name.yml
deleted file mode 100644
index 92426832f30..00000000000
--- a/changelogs/unreleased/fix-gb-exclude-persisted-variables-from-environment-name.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Exclude CI_PIPELINE_ID from variables supported in dynamic environment name
-merge_request: 19032
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-gb-not-allow-to-trigger-skipped-manual-actions.yml b/changelogs/unreleased/fix-gb-not-allow-to-trigger-skipped-manual-actions.yml
deleted file mode 100644
index c2a788d6ad0..00000000000
--- a/changelogs/unreleased/fix-gb-not-allow-to-trigger-skipped-manual-actions.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Do not allow to trigger manual actions that were skipped
-merge_request: 18985
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-gitaly-mr-creation-limits.yml b/changelogs/unreleased/fix-gitaly-mr-creation-limits.yml
new file mode 100644
index 00000000000..e903f2e5277
--- /dev/null
+++ b/changelogs/unreleased/fix-gitaly-mr-creation-limits.yml
@@ -0,0 +1,5 @@
+---
+title: Fix merge request diffs when created with gitaly_diff_between enabled
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-groups-api-ordering.yml b/changelogs/unreleased/fix-groups-api-ordering.yml
new file mode 100644
index 00000000000..3a6a7f84356
--- /dev/null
+++ b/changelogs/unreleased/fix-groups-api-ordering.yml
@@ -0,0 +1,4 @@
+title: Fixed pagination of groups API
+merge_request: 19665
+author: Marko, Peter
+type: added
diff --git a/changelogs/unreleased/fix-last-commit-author-link-is-blue.yml b/changelogs/unreleased/fix-last-commit-author-link-is-blue.yml
new file mode 100644
index 00000000000..aaceeaecfb1
--- /dev/null
+++ b/changelogs/unreleased/fix-last-commit-author-link-is-blue.yml
@@ -0,0 +1,5 @@
+---
+title: Updated last commit link color
+merge_request: 20234
+author: Constance Okoghenun
+type: fixed
diff --git a/changelogs/unreleased/fix-paragraph-line-height-for-emoji.yml b/changelogs/unreleased/fix-paragraph-line-height-for-emoji.yml
new file mode 100644
index 00000000000..5aaf0fac60e
--- /dev/null
+++ b/changelogs/unreleased/fix-paragraph-line-height-for-emoji.yml
@@ -0,0 +1,5 @@
+---
+title: Fix paragraph line height for emoji
+merge_request: 20137
+author: George Tsiolis
+type: fixed
diff --git a/changelogs/unreleased/fix-reactive-cache-retry-rate.yml b/changelogs/unreleased/fix-reactive-cache-retry-rate.yml
deleted file mode 100644
index 044e7fe39c0..00000000000
--- a/changelogs/unreleased/fix-reactive-cache-retry-rate.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update commit status from external CI services less aggressively
-merge_request: 18802
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-registry-created-at-tooltip.yml b/changelogs/unreleased/fix-registry-created-at-tooltip.yml
deleted file mode 100644
index 911b3b10fd4..00000000000
--- a/changelogs/unreleased/fix-registry-created-at-tooltip.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'Add missing tooltip to creation date on container registry overview'
-merge_request: 18767
-author: Lars Greiss
-type: fixed
diff --git a/changelogs/unreleased/fix-search-bar.yml b/changelogs/unreleased/fix-search-bar.yml
new file mode 100644
index 00000000000..d4c0c1efddf
--- /dev/null
+++ b/changelogs/unreleased/fix-search-bar.yml
@@ -0,0 +1,5 @@
+---
+title: Fix search bar text input alignment
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-shorcut-modal.yml b/changelogs/unreleased/fix-shorcut-modal.yml
deleted file mode 100644
index 796a1523a61..00000000000
--- a/changelogs/unreleased/fix-shorcut-modal.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix modal width of shorcuts help page
-merge_request: 18766
-author: Lars Greiss
-type: fixed
diff --git a/changelogs/unreleased/fix-trace-archive-cron-worker-race-condition.yml b/changelogs/unreleased/fix-trace-archive-cron-worker-race-condition.yml
new file mode 100644
index 00000000000..ca8f4252008
--- /dev/null
+++ b/changelogs/unreleased/fix-trace-archive-cron-worker-race-condition.yml
@@ -0,0 +1,5 @@
+---
+title: Check if archived trace exist before archive it
+merge_request: 20297
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-unverified-hover-state.yml b/changelogs/unreleased/fix-unverified-hover-state.yml
deleted file mode 100644
index 003138f9821..00000000000
--- a/changelogs/unreleased/fix-unverified-hover-state.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Unverified hover state color changed to black
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-web-ide-disable-markdown-autocomplete.yml b/changelogs/unreleased/fix-web-ide-disable-markdown-autocomplete.yml
new file mode 100644
index 00000000000..6a4d9b6c8c4
--- /dev/null
+++ b/changelogs/unreleased/fix-web-ide-disable-markdown-autocomplete.yml
@@ -0,0 +1,5 @@
+---
+title: Disabled Web IDE autocomplete suggestions for Markdown files.
+merge_request:
+author: Isaac Smith
+type: fixed
diff --git a/changelogs/unreleased/fj-43565-wrong-role-displayed.yml b/changelogs/unreleased/fj-43565-wrong-role-displayed.yml
new file mode 100644
index 00000000000..67ff25bc50c
--- /dev/null
+++ b/changelogs/unreleased/fj-43565-wrong-role-displayed.yml
@@ -0,0 +1,5 @@
+---
+title: Fix wrong role badge displayed in projects dashboard
+merge_request: 20374
+author:
+type: fixed
diff --git a/changelogs/unreleased/fj-46278-apply-doorkeeper-scope-patch.yml b/changelogs/unreleased/fj-46278-apply-doorkeeper-scope-patch.yml
new file mode 100644
index 00000000000..1f4de2cb490
--- /dev/null
+++ b/changelogs/unreleased/fj-46278-apply-doorkeeper-scope-patch.yml
@@ -0,0 +1,5 @@
+---
+title: Fix OAuth Application Authorization screen to appear with each access
+merge_request: 20216
+author:
+type: fixed
diff --git a/changelogs/unreleased/fj-46278-enable-doorkeeper-reuse-access-token.yml b/changelogs/unreleased/fj-46278-enable-doorkeeper-reuse-access-token.yml
new file mode 100644
index 00000000000..0994f4de248
--- /dev/null
+++ b/changelogs/unreleased/fj-46278-enable-doorkeeper-reuse-access-token.yml
@@ -0,0 +1,6 @@
+---
+title: Enable Doorkeeper option to avoid generating new tokens when users login via
+ oauth
+merge_request: 20200
+author:
+type: fixed
diff --git a/changelogs/unreleased/fj-46411-fix-badge-api-endpoint-route-with-relative-url.yml b/changelogs/unreleased/fj-46411-fix-badge-api-endpoint-route-with-relative-url.yml
deleted file mode 100644
index bd4e5a43352..00000000000
--- a/changelogs/unreleased/fj-46411-fix-badge-api-endpoint-route-with-relative-url.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed badge api endpoint route when relative url is set
-merge_request: 19004
-author:
-type: fixed
diff --git a/changelogs/unreleased/fj-46459-fix-expose-url-when-base-url-set.yml b/changelogs/unreleased/fj-46459-fix-expose-url-when-base-url-set.yml
deleted file mode 100644
index 16b0ee06898..00000000000
--- a/changelogs/unreleased/fj-46459-fix-expose-url-when-base-url-set.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed bug where generated api urls didn't add the base url if set
-merge_request: 19003
-author:
-type: fixed
diff --git a/changelogs/unreleased/fj-bumping-gollum-lib-and-gollum-rugged-adapter.yml b/changelogs/unreleased/fj-bumping-gollum-lib-and-gollum-rugged-adapter.yml
new file mode 100644
index 00000000000..3b4d429707f
--- /dev/null
+++ b/changelogs/unreleased/fj-bumping-gollum-lib-and-gollum-rugged-adapter.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed bug that allowed to remove other wiki pages if the title had wildcard characters
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/fj-web-terminal-ci-build.yml b/changelogs/unreleased/fj-web-terminal-ci-build.yml
new file mode 100644
index 00000000000..c3608d4203b
--- /dev/null
+++ b/changelogs/unreleased/fj-web-terminal-ci-build.yml
@@ -0,0 +1,5 @@
+---
+title: Add Web Terminal for Ci Builds
+merge_request:
+author: Vicky Chijwani
+type: added
diff --git a/changelogs/unreleased/fl-mr-refactor-performance-improvements.yml b/changelogs/unreleased/fl-mr-refactor-performance-improvements.yml
new file mode 100644
index 00000000000..649d1b5da67
--- /dev/null
+++ b/changelogs/unreleased/fl-mr-refactor-performance-improvements.yml
@@ -0,0 +1,5 @@
+---
+title: Structure getters for diff Store properly and adds specs
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/frozen-string-app-workers.yml b/changelogs/unreleased/frozen-string-app-workers.yml
new file mode 100644
index 00000000000..48b50cc6ca4
--- /dev/null
+++ b/changelogs/unreleased/frozen-string-app-workers.yml
@@ -0,0 +1,5 @@
+---
+title: Enable frozen string in app/workers/*.rb
+merge_request: 19944
+author: gfyoung
+type: other
diff --git a/changelogs/unreleased/frozen-string-enable-app-uploaders.yml b/changelogs/unreleased/frozen-string-enable-app-uploaders.yml
new file mode 100644
index 00000000000..d43ca8bed8c
--- /dev/null
+++ b/changelogs/unreleased/frozen-string-enable-app-uploaders.yml
@@ -0,0 +1,5 @@
+---
+title: Enable frozen string in apps/validators/*.rb
+merge_request: 20382
+author: gfyoung
+type: other
diff --git a/changelogs/unreleased/frozen-string-enable-app-validators.yml b/changelogs/unreleased/frozen-string-enable-app-validators.yml
new file mode 100644
index 00000000000..db480b06d9b
--- /dev/null
+++ b/changelogs/unreleased/frozen-string-enable-app-validators.yml
@@ -0,0 +1,5 @@
+---
+title: Enable frozen string in apps/validators/*.rb
+merge_request: 20220
+author: gfyoung
+type: other
diff --git a/changelogs/unreleased/frozen-string-enable-app-workers-2.yml b/changelogs/unreleased/frozen-string-enable-app-workers-2.yml
new file mode 100644
index 00000000000..81de6899d76
--- /dev/null
+++ b/changelogs/unreleased/frozen-string-enable-app-workers-2.yml
@@ -0,0 +1,5 @@
+---
+title: Finish enabling frozen string for app/workers/*.rb
+merge_request: 20197
+author: gfyoung
+type: other
diff --git a/changelogs/unreleased/gitaly-commit-count-opt-out.yml b/changelogs/unreleased/gitaly-commit-count-opt-out.yml
new file mode 100644
index 00000000000..fd8298b1d7b
--- /dev/null
+++ b/changelogs/unreleased/gitaly-commit-count-opt-out.yml
@@ -0,0 +1,5 @@
+---
+title: Move some Gitaly RPC's to opt-out
+merge_request: 19591
+author:
+type: other
diff --git a/changelogs/unreleased/gitaly-opt-out-branch-tag.yml b/changelogs/unreleased/gitaly-opt-out-branch-tag.yml
new file mode 100644
index 00000000000..750fc863eed
--- /dev/null
+++ b/changelogs/unreleased/gitaly-opt-out-branch-tag.yml
@@ -0,0 +1,5 @@
+---
+title: Move Gitaly branch/tag/ref RPC's to opt-out
+merge_request: 19644
+author:
+type: other
diff --git a/changelogs/unreleased/gitaly-timeouts.yml b/changelogs/unreleased/gitaly-timeouts.yml
new file mode 100644
index 00000000000..ac8008faa2d
--- /dev/null
+++ b/changelogs/unreleased/gitaly-timeouts.yml
@@ -0,0 +1,5 @@
+---
+title: Updated Gitaly fail-fast timeout values
+merge_request: !20259
+author:
+type: performance
diff --git a/changelogs/unreleased/highlight-cluster-settings-message.yml b/changelogs/unreleased/highlight-cluster-settings-message.yml
new file mode 100644
index 00000000000..4e029941c51
--- /dev/null
+++ b/changelogs/unreleased/highlight-cluster-settings-message.yml
@@ -0,0 +1,5 @@
+---
+title: Highlight cluster settings message
+merge_request: 19996
+author: George Tsiolis
+type: changed
diff --git a/changelogs/unreleased/ide-commit-actions-update.yml b/changelogs/unreleased/ide-commit-actions-update.yml
new file mode 100644
index 00000000000..35bee94e156
--- /dev/null
+++ b/changelogs/unreleased/ide-commit-actions-update.yml
@@ -0,0 +1,5 @@
+---
+title: Improve Web IDE commit flow
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/ide-hide-merge-request-if-disabled.yml b/changelogs/unreleased/ide-hide-merge-request-if-disabled.yml
deleted file mode 100644
index 9efef2c6839..00000000000
--- a/changelogs/unreleased/ide-hide-merge-request-if-disabled.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Hide merge request option in IDE when disabled
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/ide-merge-request-info.yml b/changelogs/unreleased/ide-merge-request-info.yml
new file mode 100644
index 00000000000..104f48ae309
--- /dev/null
+++ b/changelogs/unreleased/ide-merge-request-info.yml
@@ -0,0 +1,5 @@
+---
+title: Display merge request title & description in Web IDE
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/improve-metadata-access-performance.yml b/changelogs/unreleased/improve-metadata-access-performance.yml
new file mode 100644
index 00000000000..b16fa99dd3b
--- /dev/null
+++ b/changelogs/unreleased/improve-metadata-access-performance.yml
@@ -0,0 +1,5 @@
+---
+title: Access metadata directly from Object Storage
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/issue-25256.yml b/changelogs/unreleased/issue-25256.yml
deleted file mode 100644
index e981b5f9a8f..00000000000
--- a/changelogs/unreleased/issue-25256.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Don't trim incoming emails that create new issues
-merge_request:
-author: Cameron Crockett
-type: fixed
diff --git a/changelogs/unreleased/jivl-add-dot-system-notes.yml b/changelogs/unreleased/jivl-add-dot-system-notes.yml
deleted file mode 100644
index 2246bab1464..00000000000
--- a/changelogs/unreleased/jivl-add-dot-system-notes.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add dot to separate system notes content
-merge_request: 18864
-author:
-type: changed
diff --git a/changelogs/unreleased/jprovazn-delete-upload-worker.yml b/changelogs/unreleased/jprovazn-delete-upload-worker.yml
new file mode 100644
index 00000000000..52916482d32
--- /dev/null
+++ b/changelogs/unreleased/jprovazn-delete-upload-worker.yml
@@ -0,0 +1,5 @@
+---
+title: Remove deprecated object_storage_upload queue.
+merge_request:
+author:
+type: removed
diff --git a/changelogs/unreleased/jprovazn-direct-upload.yml b/changelogs/unreleased/jprovazn-direct-upload.yml
new file mode 100644
index 00000000000..57f6d1e07c3
--- /dev/null
+++ b/changelogs/unreleased/jprovazn-direct-upload.yml
@@ -0,0 +1,5 @@
+---
+title: Support direct_upload for generic uploads
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/jprovazn-extra-line.yml b/changelogs/unreleased/jprovazn-extra-line.yml
new file mode 100644
index 00000000000..2628620f8ec
--- /dev/null
+++ b/changelogs/unreleased/jprovazn-extra-line.yml
@@ -0,0 +1,5 @@
+---
+title: Don't show context button for diffs of deleted files.
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/jprovazn-fix-mr-caching.yml b/changelogs/unreleased/jprovazn-fix-mr-caching.yml
new file mode 100644
index 00000000000..7ad7ed54143
--- /dev/null
+++ b/changelogs/unreleased/jprovazn-fix-mr-caching.yml
@@ -0,0 +1,5 @@
+---
+title: Invalidate merge request diffs cache if diff data change.
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/jprovazn-label-links-update.yml b/changelogs/unreleased/jprovazn-label-links-update.yml
new file mode 100644
index 00000000000..75fb46ede6b
--- /dev/null
+++ b/changelogs/unreleased/jprovazn-label-links-update.yml
@@ -0,0 +1,5 @@
+---
+title: Fix cross-project label references.
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/jprovazn-null-byte.yml b/changelogs/unreleased/jprovazn-null-byte.yml
deleted file mode 100644
index 4c4760ac412..00000000000
--- a/changelogs/unreleased/jprovazn-null-byte.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix filename matching when processing file or blob search results
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/jprovazn-pipeline-policy.yml b/changelogs/unreleased/jprovazn-pipeline-policy.yml
deleted file mode 100644
index 2997c6c8667..00000000000
--- a/changelogs/unreleased/jprovazn-pipeline-policy.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Allow maintainers to retry pipelines on forked projects (if allowed in merge
- request)
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/jprovazn-remote-upload-destroy.yml b/changelogs/unreleased/jprovazn-remote-upload-destroy.yml
deleted file mode 100644
index 22e55920fa3..00000000000
--- a/changelogs/unreleased/jprovazn-remote-upload-destroy.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix deletion of Object Store uploads
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/jprovazn-upload-symlink.yml b/changelogs/unreleased/jprovazn-upload-symlink.yml
new file mode 100644
index 00000000000..265791d332f
--- /dev/null
+++ b/changelogs/unreleased/jprovazn-upload-symlink.yml
@@ -0,0 +1,5 @@
+---
+title: Add /uploads subdirectory to allowed upload paths.
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/jr-48133-web-ide-commit-ellipsis.yml b/changelogs/unreleased/jr-48133-web-ide-commit-ellipsis.yml
new file mode 100644
index 00000000000..ac58eaccaaf
--- /dev/null
+++ b/changelogs/unreleased/jr-48133-web-ide-commit-ellipsis.yml
@@ -0,0 +1,5 @@
+---
+title: Add ellispsis to web ide commit button
+merge_request: 20030
+author:
+type: other
diff --git a/changelogs/unreleased/jr-web-ide-shortcuts.yml b/changelogs/unreleased/jr-web-ide-shortcuts.yml
deleted file mode 100644
index a895eab432a..00000000000
--- a/changelogs/unreleased/jr-web-ide-shortcuts.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add shortcuts to Web IDE docs and modal
-merge_request: 19044
-author:
-type: changed
diff --git a/changelogs/unreleased/limit-metrics-content-type.yml b/changelogs/unreleased/limit-metrics-content-type.yml
new file mode 100644
index 00000000000..42cb4347771
--- /dev/null
+++ b/changelogs/unreleased/limit-metrics-content-type.yml
@@ -0,0 +1,5 @@
+---
+title: Limit the action suffixes in transaction metrics
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/memoize-database-version.yml b/changelogs/unreleased/memoize-database-version.yml
deleted file mode 100644
index 575348a53a1..00000000000
--- a/changelogs/unreleased/memoize-database-version.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Memoize Gitlab::Database.version
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/more-group-api-sorting-options.yml b/changelogs/unreleased/more-group-api-sorting-options.yml
new file mode 100644
index 00000000000..b29f76a65a9
--- /dev/null
+++ b/changelogs/unreleased/more-group-api-sorting-options.yml
@@ -0,0 +1,5 @@
+---
+title: Added id sorting option to GET groups and subgroups API
+merge_request: 19665
+author: Marko, Peter
+type: added
diff --git a/changelogs/unreleased/move-boards-modal-empty-state-vue-component.yml b/changelogs/unreleased/move-boards-modal-empty-state-vue-component.yml
new file mode 100644
index 00000000000..54a61d7c914
--- /dev/null
+++ b/changelogs/unreleased/move-boards-modal-empty-state-vue-component.yml
@@ -0,0 +1,5 @@
+---
+title: Move boards modal EmptyState vue component
+merge_request: 20068
+author: George Tsiolis
+type: performance
diff --git a/changelogs/unreleased/move-disussion-actions-to-the-right.yml b/changelogs/unreleased/move-disussion-actions-to-the-right.yml
deleted file mode 100644
index b79c6f36585..00000000000
--- a/changelogs/unreleased/move-disussion-actions-to-the-right.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move discussion actions to the right for small viewports
-merge_request: 18476
-author: George Tsiolis
-type: changed
diff --git a/changelogs/unreleased/no-multi-assign-enable.yml b/changelogs/unreleased/no-multi-assign-enable.yml
new file mode 100644
index 00000000000..bb9c69b18e7
--- /dev/null
+++ b/changelogs/unreleased/no-multi-assign-enable.yml
@@ -0,0 +1,5 @@
+---
+title: Enable no-multi-assignment in JS files
+merge_request: 19808
+author: gfyoung
+type: other
diff --git a/changelogs/unreleased/no-multi-assign-follow-up.yml b/changelogs/unreleased/no-multi-assign-follow-up.yml
new file mode 100644
index 00000000000..817760ff649
--- /dev/null
+++ b/changelogs/unreleased/no-multi-assign-follow-up.yml
@@ -0,0 +1,5 @@
+---
+title: Improve no-multi-assignment fixes after enabling rule
+merge_request: 19915
+author: gfyoung
+type: other
diff --git a/changelogs/unreleased/no-restricted-globals-enable.yml b/changelogs/unreleased/no-restricted-globals-enable.yml
new file mode 100644
index 00000000000..1fa2eac0d03
--- /dev/null
+++ b/changelogs/unreleased/no-restricted-globals-enable.yml
@@ -0,0 +1,5 @@
+---
+title: Enable no-restricted globals in JS files
+merge_request: 19877
+author: gfyoung
+type: other
diff --git a/changelogs/unreleased/osw-delete-non-latest-mr-diff-files-migration.yml b/changelogs/unreleased/osw-delete-non-latest-mr-diff-files-migration.yml
new file mode 100644
index 00000000000..e4cbae1a109
--- /dev/null
+++ b/changelogs/unreleased/osw-delete-non-latest-mr-diff-files-migration.yml
@@ -0,0 +1,5 @@
+---
+title: Schedule workers to delete non-latest diffs in post-migration
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/osw-delete-non-latest-mr-diff-files-upon-merge.yml b/changelogs/unreleased/osw-delete-non-latest-mr-diff-files-upon-merge.yml
new file mode 100644
index 00000000000..3e752125f3a
--- /dev/null
+++ b/changelogs/unreleased/osw-delete-non-latest-mr-diff-files-upon-merge.yml
@@ -0,0 +1,5 @@
+---
+title: Delete non-latest merge request diff files upon merge
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/osw-mark-as-merged-as-first-post-merge-action.yml b/changelogs/unreleased/osw-mark-as-merged-as-first-post-merge-action.yml
new file mode 100644
index 00000000000..2049afc3d44
--- /dev/null
+++ b/changelogs/unreleased/osw-mark-as-merged-as-first-post-merge-action.yml
@@ -0,0 +1,5 @@
+---
+title: Mark MR as merged regardless of errors when closing issues
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/perf-wiki-pattern-once.yml b/changelogs/unreleased/perf-wiki-pattern-once.yml
new file mode 100644
index 00000000000..fb4085a06ae
--- /dev/null
+++ b/changelogs/unreleased/perf-wiki-pattern-once.yml
@@ -0,0 +1,5 @@
+---
+title: Improve render performance of large wiki pages
+merge_request: 20465
+author: Peter Leitzen
+type: performance
diff --git a/changelogs/unreleased/pipelines-index-performance.yml b/changelogs/unreleased/pipelines-index-performance.yml
deleted file mode 100644
index 928c2ddab72..00000000000
--- a/changelogs/unreleased/pipelines-index-performance.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improve performance of project pipelines pages
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/pr-importer-io-extra-error-handling.yml b/changelogs/unreleased/pr-importer-io-extra-error-handling.yml
new file mode 100644
index 00000000000..2f7121b2840
--- /dev/null
+++ b/changelogs/unreleased/pr-importer-io-extra-error-handling.yml
@@ -0,0 +1,5 @@
+---
+title: Ensure MR diffs always exist in the PR importer
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/prefer-destructuring-fix.yml b/changelogs/unreleased/prefer-destructuring-fix.yml
new file mode 100644
index 00000000000..452e04f553e
--- /dev/null
+++ b/changelogs/unreleased/prefer-destructuring-fix.yml
@@ -0,0 +1,5 @@
+---
+title: Enable prefer-structuring in JS files
+merge_request: 19943
+author: gfyoung
+type: other
diff --git a/changelogs/unreleased/process-commits-as-normal-in-forks-with-missing-upstream.yml b/changelogs/unreleased/process-commits-as-normal-in-forks-with-missing-upstream.yml
new file mode 100644
index 00000000000..6994a238074
--- /dev/null
+++ b/changelogs/unreleased/process-commits-as-normal-in-forks-with-missing-upstream.yml
@@ -0,0 +1,5 @@
+---
+title: Process commits as normal in forks when the upstream project is deleted
+merge_request: 20534
+author:
+type: fixed
diff --git a/changelogs/unreleased/prune-web-hook-logs.yml b/changelogs/unreleased/prune-web-hook-logs.yml
new file mode 100644
index 00000000000..e8c805b2a92
--- /dev/null
+++ b/changelogs/unreleased/prune-web-hook-logs.yml
@@ -0,0 +1,5 @@
+---
+title: Prune web hook logs older than 90 days
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/rails5-fix-46276.yml b/changelogs/unreleased/rails5-fix-46276.yml
new file mode 100644
index 00000000000..cdca91a755d
--- /dev/null
+++ b/changelogs/unreleased/rails5-fix-46276.yml
@@ -0,0 +1,5 @@
+---
+title: Rails5 fix format in uploads actions
+merge_request: 19907
+author: Jasper Maes
+type: fixed
diff --git a/changelogs/unreleased/rails5-fix-47366.yml b/changelogs/unreleased/rails5-fix-47366.yml
new file mode 100644
index 00000000000..7ea03d2b95e
--- /dev/null
+++ b/changelogs/unreleased/rails5-fix-47366.yml
@@ -0,0 +1,5 @@
+---
+title: Rails5 fix expected `issuable.reload.updated_at` to have changed
+merge_request: 19733
+author: Jasper Maes
+type: fixed
diff --git a/changelogs/unreleased/rails5-fix-47370.yml b/changelogs/unreleased/rails5-fix-47370.yml
new file mode 100644
index 00000000000..90c19593b7d
--- /dev/null
+++ b/changelogs/unreleased/rails5-fix-47370.yml
@@ -0,0 +1,5 @@
+---
+title: Use same gem versions for rails5 as for rails4 where possible
+merge_request: 19498
+author: Jasper Maes
+type: fixed
diff --git a/changelogs/unreleased/rails5-fix-47804.yml b/changelogs/unreleased/rails5-fix-47804.yml
new file mode 100644
index 00000000000..3332ed3bbaa
--- /dev/null
+++ b/changelogs/unreleased/rails5-fix-47804.yml
@@ -0,0 +1,5 @@
+---
+title: Rails5 fix stack level too deep
+merge_request: 19762
+author: Jasper Maes
+type: fixed
diff --git a/changelogs/unreleased/rails5-fix-47805.yml b/changelogs/unreleased/rails5-fix-47805.yml
new file mode 100644
index 00000000000..8bd8ad5488c
--- /dev/null
+++ b/changelogs/unreleased/rails5-fix-47805.yml
@@ -0,0 +1,6 @@
+---
+title: 'Rails5 ActionController::ParameterMissing: param is missing or the value is
+ empty: application_setting'
+merge_request: 19763
+author: Jasper Maes
+type: fixed
diff --git a/changelogs/unreleased/rails5-fix-47835.yml b/changelogs/unreleased/rails5-fix-47835.yml
new file mode 100644
index 00000000000..fe9cbf1a03a
--- /dev/null
+++ b/changelogs/unreleased/rails5-fix-47835.yml
@@ -0,0 +1,6 @@
+---
+title: Rails5 fix no implicit conversion of Hash into String. ActionController::Parameters
+ no longer returns an hash in Rails 5
+merge_request: 19792
+author: Jasper Maes
+type: fixed
diff --git a/changelogs/unreleased/rails5-fix-47836.yml b/changelogs/unreleased/rails5-fix-47836.yml
new file mode 100644
index 00000000000..2aef2db607a
--- /dev/null
+++ b/changelogs/unreleased/rails5-fix-47836.yml
@@ -0,0 +1,6 @@
+---
+title: Rails5 fix passing Group objects array into for_projects_and_groups milestone
+ scope
+merge_request: 19863
+author: Jasper Maes
+type: fixed
diff --git a/changelogs/unreleased/rails5-fix-47960.yml b/changelogs/unreleased/rails5-fix-47960.yml
new file mode 100644
index 00000000000..2229511ccd6
--- /dev/null
+++ b/changelogs/unreleased/rails5-fix-47960.yml
@@ -0,0 +1,5 @@
+---
+title: Rails5 fix update_attribute usage not causing a save
+merge_request: 19881
+author: Jasper Maes
+type: fixed
diff --git a/changelogs/unreleased/rails5-fix-48009.yml b/changelogs/unreleased/rails5-fix-48009.yml
new file mode 100644
index 00000000000..7ade9ef2b7d
--- /dev/null
+++ b/changelogs/unreleased/rails5-fix-48009.yml
@@ -0,0 +1,5 @@
+---
+title: Rails5 update Gemfile.rails5.lock
+merge_request: 19921
+author: Jasper Maes
+type: fixed
diff --git a/changelogs/unreleased/rails5-fix-48012.yml b/changelogs/unreleased/rails5-fix-48012.yml
new file mode 100644
index 00000000000..953ccbd8af7
--- /dev/null
+++ b/changelogs/unreleased/rails5-fix-48012.yml
@@ -0,0 +1,6 @@
+---
+title: Rails5 fix passing Group objects array into for_projects_and_groups milestone
+ scope
+merge_request: 19920
+author: Jasper Maes
+type: fixed
diff --git a/changelogs/unreleased/rails5-fix-48104.yml b/changelogs/unreleased/rails5-fix-48104.yml
new file mode 100644
index 00000000000..6cf519ad791
--- /dev/null
+++ b/changelogs/unreleased/rails5-fix-48104.yml
@@ -0,0 +1,6 @@
+---
+title: 'Rails5 fix expected: 1 time with arguments: (97, anything, {"squash"=>false})
+ received: 0 times'
+merge_request: 20004
+author: Jasper Maes
+type: fixed
diff --git a/changelogs/unreleased/rails5-fix-48140.yml b/changelogs/unreleased/rails5-fix-48140.yml
new file mode 100644
index 00000000000..a6992803e5a
--- /dev/null
+++ b/changelogs/unreleased/rails5-fix-48140.yml
@@ -0,0 +1,6 @@
+---
+title: 'Rails 5 fix Capybara::ElementNotFound: Unable to find visible css #modal-revert-commit
+ and expected: "/bar" got: "/foo"'
+merge_request: 20044
+author: Jasper Maes
+type: fixed
diff --git a/changelogs/unreleased/rails5-fix-48141.yml b/changelogs/unreleased/rails5-fix-48141.yml
new file mode 100644
index 00000000000..5e2aa23b8fb
--- /dev/null
+++ b/changelogs/unreleased/rails5-fix-48141.yml
@@ -0,0 +1,6 @@
+---
+title: 'Rails5 fix expected: 0 times with any arguments received: 1 time with arguments:
+ DashboardController'
+merge_request: 20018
+author: Jasper Maes
+type: fixed
diff --git a/changelogs/unreleased/rails5-fix-48142.yml b/changelogs/unreleased/rails5-fix-48142.yml
new file mode 100644
index 00000000000..bfd95cfbe8b
--- /dev/null
+++ b/changelogs/unreleased/rails5-fix-48142.yml
@@ -0,0 +1,5 @@
+---
+title: Rails5 fix Admin::HooksController
+merge_request: 20017
+author: Jasper Maes
+type: fixed
diff --git a/changelogs/unreleased/rails5-fix-48430.yml b/changelogs/unreleased/rails5-fix-48430.yml
new file mode 100644
index 00000000000..16495615395
--- /dev/null
+++ b/changelogs/unreleased/rails5-fix-48430.yml
@@ -0,0 +1,5 @@
+---
+title: Rails5 fix MySQL milliseconds problem in specs
+merge_request: 20221
+author: Jasper Maes
+type: fixed
diff --git a/changelogs/unreleased/rails5-fix-48432.yml b/changelogs/unreleased/rails5-fix-48432.yml
new file mode 100644
index 00000000000..732294447a9
--- /dev/null
+++ b/changelogs/unreleased/rails5-fix-48432.yml
@@ -0,0 +1,5 @@
+---
+title: Rails5 fix Mysql comparison failure caused by milliseconds problem
+merge_request: 20222
+author: Jasper Maes
+type: fixed
diff --git a/changelogs/unreleased/rails5-fix-48977.yml b/changelogs/unreleased/rails5-fix-48977.yml
new file mode 100644
index 00000000000..bfd86f20e24
--- /dev/null
+++ b/changelogs/unreleased/rails5-fix-48977.yml
@@ -0,0 +1,5 @@
+---
+title: Rails5 fix mysql milliseconds problem in specs
+merge_request: 20464
+author: Jasper Maes
+type: fixed
diff --git a/changelogs/unreleased/rails5-fix-db-check.yml b/changelogs/unreleased/rails5-fix-db-check.yml
new file mode 100644
index 00000000000..ccac4619ea7
--- /dev/null
+++ b/changelogs/unreleased/rails5-fix-db-check.yml
@@ -0,0 +1,5 @@
+---
+title: Rails5 fix connection execute return integer instead of string
+merge_request: 19901
+author: Jasper Maes
+type: fixed
diff --git a/changelogs/unreleased/rails5-fix-mysql-arel-from.yml b/changelogs/unreleased/rails5-fix-mysql-arel-from.yml
new file mode 100644
index 00000000000..9883ff306f1
--- /dev/null
+++ b/changelogs/unreleased/rails5-fix-mysql-arel-from.yml
@@ -0,0 +1,5 @@
+---
+title: Rails5 fix arel from in mysql_median_datetime_sql
+merge_request: 20167
+author: Jasper Maes
+type: fixed
diff --git a/changelogs/unreleased/rails5-fix-pages-controller.yml b/changelogs/unreleased/rails5-fix-pages-controller.yml
new file mode 100644
index 00000000000..eeb3747c4eb
--- /dev/null
+++ b/changelogs/unreleased/rails5-fix-pages-controller.yml
@@ -0,0 +1,5 @@
+---
+title: Rails5 fix Projects::PagesController spec
+merge_request: 20007
+author: Jasper Maes
+type: fixed
diff --git a/changelogs/unreleased/rails5-mysql-fix-pr-importer-spec.yml b/changelogs/unreleased/rails5-mysql-fix-pr-importer-spec.yml
new file mode 100644
index 00000000000..afd9865ee45
--- /dev/null
+++ b/changelogs/unreleased/rails5-mysql-fix-pr-importer-spec.yml
@@ -0,0 +1,5 @@
+---
+title: Rails5 mysql fix milliseconds problem in pull request importer spec
+merge_request: 20475
+author: Jasper Maes
+type: fixed
diff --git a/changelogs/unreleased/rails5-mysql-rename-column.yml b/changelogs/unreleased/rails5-mysql-rename-column.yml
new file mode 100644
index 00000000000..cbae9250744
--- /dev/null
+++ b/changelogs/unreleased/rails5-mysql-rename-column.yml
@@ -0,0 +1,5 @@
+---
+title: Rails5 MySQL fix rename_column as part of cleanup_concurrent_column_type_change
+merge_request: 20514
+author: Jasper Maes
+type: fixed
diff --git a/changelogs/unreleased/rails5-update-gemfile-lock.yml b/changelogs/unreleased/rails5-update-gemfile-lock.yml
new file mode 100644
index 00000000000..58931587fff
--- /dev/null
+++ b/changelogs/unreleased/rails5-update-gemfile-lock.yml
@@ -0,0 +1,5 @@
+---
+title: Update Gemfile.rails5.lock with latest Gemfile.lock changes
+merge_request: 20466
+author: Jasper Maes
+type: fixed
diff --git a/changelogs/unreleased/rd-33733-showing-created-date-instead-of-updated-date-in-project-lists.yml b/changelogs/unreleased/rd-33733-showing-created-date-instead-of-updated-date-in-project-lists.yml
new file mode 100644
index 00000000000..3934381b0cf
--- /dev/null
+++ b/changelogs/unreleased/rd-33733-showing-created-date-instead-of-updated-date-in-project-lists.yml
@@ -0,0 +1,5 @@
+---
+title: Invalidate cache with project details when repository is updated
+merge_request: 19774
+author:
+type: fixed
diff --git a/changelogs/unreleased/refactor-move-squash-before-merge-vue-component.yml b/changelogs/unreleased/refactor-move-squash-before-merge-vue-component.yml
deleted file mode 100644
index b8b2762a21d..00000000000
--- a/changelogs/unreleased/refactor-move-squash-before-merge-vue-component.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move SquashBeforeMerge vue component
-merge_request: 18813
-author: George Tsiolis
-type: performance
diff --git a/changelogs/unreleased/registry-ux-improvements-remove-clipboard-prefix.yml b/changelogs/unreleased/registry-ux-improvements-remove-clipboard-prefix.yml
deleted file mode 100644
index ddf7f51aa5e..00000000000
--- a/changelogs/unreleased/registry-ux-improvements-remove-clipboard-prefix.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove docker pull prefix from registry clipboard feature
-merge_request: 18933
-author: Lars Greiss
-type: changed
diff --git a/changelogs/unreleased/remove-allocations-gem.yml b/changelogs/unreleased/remove-allocations-gem.yml
new file mode 100644
index 00000000000..e809fd26701
--- /dev/null
+++ b/changelogs/unreleased/remove-allocations-gem.yml
@@ -0,0 +1,5 @@
+---
+title: Remove remaining traces of the Allocations Gem
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/remove-ci_job_request_with_tags_matcher.yml b/changelogs/unreleased/remove-ci_job_request_with_tags_matcher.yml
new file mode 100644
index 00000000000..b86512445d5
--- /dev/null
+++ b/changelogs/unreleased/remove-ci_job_request_with_tags_matcher.yml
@@ -0,0 +1,5 @@
+---
+title: Remove the ci_job_request_with_tags_matcher
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/remove-is-shared-from-ci-runners.yml b/changelogs/unreleased/remove-is-shared-from-ci-runners.yml
new file mode 100644
index 00000000000..a6917431a53
--- /dev/null
+++ b/changelogs/unreleased/remove-is-shared-from-ci-runners.yml
@@ -0,0 +1,5 @@
+---
+title: Remove the use of `is_shared` of `Ci::Runner`
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/remove-link-label-vertical-alignment-property.yml b/changelogs/unreleased/remove-link-label-vertical-alignment-property.yml
new file mode 100644
index 00000000000..40ec3998b05
--- /dev/null
+++ b/changelogs/unreleased/remove-link-label-vertical-alignment-property.yml
@@ -0,0 +1,5 @@
+---
+title: Change label link vertical alignment property
+merge_request: 18777
+author: George Tsiolis
+type: changed
diff --git a/changelogs/unreleased/remove-small-container-width.yml b/changelogs/unreleased/remove-small-container-width.yml
new file mode 100644
index 00000000000..1af8aafa87e
--- /dev/null
+++ b/changelogs/unreleased/remove-small-container-width.yml
@@ -0,0 +1,5 @@
+---
+title: Remove small container width
+merge_request: 19893
+author: George Tsiolis
+type: changed
diff --git a/changelogs/unreleased/remove-trace-efficiently.yml b/changelogs/unreleased/remove-trace-efficiently.yml
new file mode 100644
index 00000000000..a6ba6d28dce
--- /dev/null
+++ b/changelogs/unreleased/remove-trace-efficiently.yml
@@ -0,0 +1,5 @@
+---
+title: Remove redundant query when removing trace
+merge_request: 20324
+author:
+type: performance
diff --git a/changelogs/unreleased/rename-merge-request-widget-author-component.yml b/changelogs/unreleased/rename-merge-request-widget-author-component.yml
deleted file mode 100644
index 15e6eafd826..00000000000
--- a/changelogs/unreleased/rename-merge-request-widget-author-component.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Rename merge request widget author component
-merge_request: 19079
-author: George Tsiolis
-type: changed
diff --git a/changelogs/unreleased/revert-merge-request-discussion-buttons-padding.yml b/changelogs/unreleased/revert-merge-request-discussion-buttons-padding.yml
new file mode 100644
index 00000000000..9f11dd3dc3f
--- /dev/null
+++ b/changelogs/unreleased/revert-merge-request-discussion-buttons-padding.yml
@@ -0,0 +1,5 @@
+---
+title: Revert merge request discussion buttons padding
+merge_request: 20060
+author: George Tsiolis
+type: changed
diff --git a/changelogs/unreleased/runners-max-timeout-param.yml b/changelogs/unreleased/runners-max-timeout-param.yml
new file mode 100644
index 00000000000..875f805d849
--- /dev/null
+++ b/changelogs/unreleased/runners-max-timeout-param.yml
@@ -0,0 +1,5 @@
+---
+title: Add missing maximum_timeout parameter
+merge_request: 20355
+author: gfyoung
+type: fixed
diff --git a/changelogs/unreleased/safari-scrollbar-bug.yml b/changelogs/unreleased/safari-scrollbar-bug.yml
new file mode 100644
index 00000000000..792a66d1ada
--- /dev/null
+++ b/changelogs/unreleased/safari-scrollbar-bug.yml
@@ -0,0 +1,5 @@
+---
+title: Remove scrollbar in Safari in repo settings page
+merge_request: 19809
+author: gfyoung
+type: fixed
diff --git a/changelogs/unreleased/security-2682-fix-xss-for-markdown-toc.yml b/changelogs/unreleased/security-2682-fix-xss-for-markdown-toc.yml
new file mode 100644
index 00000000000..f595678c3c2
--- /dev/null
+++ b/changelogs/unreleased/security-2682-fix-xss-for-markdown-toc.yml
@@ -0,0 +1,5 @@
+---
+title: Fix XSS vulnerability for table of content generation
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-fj-bumping-sanitize-gem.yml b/changelogs/unreleased/security-fj-bumping-sanitize-gem.yml
new file mode 100644
index 00000000000..bec1033425d
--- /dev/null
+++ b/changelogs/unreleased/security-fj-bumping-sanitize-gem.yml
@@ -0,0 +1,5 @@
+---
+title: Update sanitize gem to 4.6.5 to fix HTML injection vulnerability
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-html_escape_branch_name.yml b/changelogs/unreleased/security-html_escape_branch_name.yml
new file mode 100644
index 00000000000..02d1065348f
--- /dev/null
+++ b/changelogs/unreleased/security-html_escape_branch_name.yml
@@ -0,0 +1,5 @@
+---
+title: HTML escape branch name in project graphs page
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-html_escape_usernames.yml b/changelogs/unreleased/security-html_escape_usernames.yml
new file mode 100644
index 00000000000..7e69e4ae266
--- /dev/null
+++ b/changelogs/unreleased/security-html_escape_usernames.yml
@@ -0,0 +1,5 @@
+---
+title: HTML escape the name of the user in ProjectsHelper#link_to_member
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-rd-do-not-show-internal-info-in-public-feed.yml b/changelogs/unreleased/security-rd-do-not-show-internal-info-in-public-feed.yml
new file mode 100644
index 00000000000..ff78c162dff
--- /dev/null
+++ b/changelogs/unreleased/security-rd-do-not-show-internal-info-in-public-feed.yml
@@ -0,0 +1,5 @@
+---
+title: Don't show events from internal projects for anonymous users in public feed
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/sh-bump-rugged-0-27-2.yml b/changelogs/unreleased/sh-bump-rugged-0-27-2.yml
new file mode 100644
index 00000000000..6c519648b51
--- /dev/null
+++ b/changelogs/unreleased/sh-bump-rugged-0-27-2.yml
@@ -0,0 +1,5 @@
+---
+title: Bump rugged to 0.27.2
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-enforce-unique-and-not-null-project-ids-project-features.yml b/changelogs/unreleased/sh-enforce-unique-and-not-null-project-ids-project-features.yml
deleted file mode 100644
index aae42b66c84..00000000000
--- a/changelogs/unreleased/sh-enforce-unique-and-not-null-project-ids-project-features.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add a unique and not null constraint on the project_features.project_id column
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-fix-bamboo-change-set.yml b/changelogs/unreleased/sh-fix-bamboo-change-set.yml
new file mode 100644
index 00000000000..85e79e17dee
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-bamboo-change-set.yml
@@ -0,0 +1,5 @@
+---
+title: Fix Bamboo CI status not showing for branch plans
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-fix-cross-site-origin-uploads-js.yml b/changelogs/unreleased/sh-fix-cross-site-origin-uploads-js.yml
deleted file mode 100644
index 3c51aaae896..00000000000
--- a/changelogs/unreleased/sh-fix-cross-site-origin-uploads-js.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix cross-origin errors when attempting to download JavaScript attachments
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-fix-grape-logging-status-code.yml b/changelogs/unreleased/sh-fix-grape-logging-status-code.yml
deleted file mode 100644
index aabf9a84bfb..00000000000
--- a/changelogs/unreleased/sh-fix-grape-logging-status-code.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix api_json.log not always reporting the right HTTP status code
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-fix-issue-47797-ce.yml b/changelogs/unreleased/sh-fix-issue-47797-ce.yml
new file mode 100644
index 00000000000..456d96acacb
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-issue-47797-ce.yml
@@ -0,0 +1,5 @@
+---
+title: Fix handling of annotated tags when Gitaly is not in use
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-handle-colons-in-url-passwords.yml b/changelogs/unreleased/sh-handle-colons-in-url-passwords.yml
new file mode 100644
index 00000000000..7717d0aab37
--- /dev/null
+++ b/changelogs/unreleased/sh-handle-colons-in-url-passwords.yml
@@ -0,0 +1,5 @@
+---
+title: Properly handle colons in URL passwords
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-move-delete-groups-api-async.yml b/changelogs/unreleased/sh-move-delete-groups-api-async.yml
deleted file mode 100644
index 1b200cac5c5..00000000000
--- a/changelogs/unreleased/sh-move-delete-groups-api-async.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move API group deletion to Sidekiq
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/sh-optimize-locks-check-ce.yml b/changelogs/unreleased/sh-optimize-locks-check-ce.yml
new file mode 100644
index 00000000000..933ec9b79bf
--- /dev/null
+++ b/changelogs/unreleased/sh-optimize-locks-check-ce.yml
@@ -0,0 +1,5 @@
+---
+title: Eliminate N+1 queries in LFS file locks checks during a push
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/straight-comparision-mode.yml b/changelogs/unreleased/straight-comparision-mode.yml
new file mode 100644
index 00000000000..2f6a0c0b54d
--- /dev/null
+++ b/changelogs/unreleased/straight-comparision-mode.yml
@@ -0,0 +1,5 @@
+---
+title: Allow straight diff in Compare API
+merge_request: 20120
+author: Maciej Nowak
+type: added
diff --git a/changelogs/unreleased/support-active-setting-while-registering-a-runner.yml b/changelogs/unreleased/support-active-setting-while-registering-a-runner.yml
deleted file mode 100644
index 6c378fd450a..00000000000
--- a/changelogs/unreleased/support-active-setting-while-registering-a-runner.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add support for 'active' setting on Runner Registration API endpoint
-merge_request: 18848
-author:
-type: changed
diff --git a/changelogs/unreleased/tc-repo-check-per-shard.yml b/changelogs/unreleased/tc-repo-check-per-shard.yml
new file mode 100644
index 00000000000..227b6b0b93b
--- /dev/null
+++ b/changelogs/unreleased/tc-repo-check-per-shard.yml
@@ -0,0 +1,5 @@
+---
+title: Run repository checks in parallel for each shard
+merge_request: 20179
+author:
+type: added
diff --git a/changelogs/unreleased/text-expander-icon-update.yml b/changelogs/unreleased/text-expander-icon-update.yml
new file mode 100644
index 00000000000..be9dc98728f
--- /dev/null
+++ b/changelogs/unreleased/text-expander-icon-update.yml
@@ -0,0 +1,5 @@
+---
+title: Updated the icon for expand buttons to ellipsis
+merge_request: 18793
+author: Constance Okoghenun
+type: changed \ No newline at end of file
diff --git a/changelogs/unreleased/transfer_project_api_endpoint.yml b/changelogs/unreleased/transfer_project_api_endpoint.yml
new file mode 100644
index 00000000000..60c704c62a0
--- /dev/null
+++ b/changelogs/unreleased/transfer_project_api_endpoint.yml
@@ -0,0 +1,5 @@
+---
+title: Add transfer project API endpoint
+merge_request: 20122
+author: Aram Visser
+type: added
diff --git a/changelogs/unreleased/tz-diff-blob-image-viewer.yml b/changelogs/unreleased/tz-diff-blob-image-viewer.yml
new file mode 100644
index 00000000000..81d87bc71f5
--- /dev/null
+++ b/changelogs/unreleased/tz-diff-blob-image-viewer.yml
@@ -0,0 +1,5 @@
+---
+title: Web IDE supports now Image + Download Diff Viewing
+merge_request: 18768
+author:
+type: added
diff --git a/changelogs/unreleased/unify-views-search-results.yml b/changelogs/unreleased/unify-views-search-results.yml
new file mode 100644
index 00000000000..81ad3616648
--- /dev/null
+++ b/changelogs/unreleased/unify-views-search-results.yml
@@ -0,0 +1,5 @@
+---
+title: Hide project name if searching against a project
+merge_request: 19595
+author:
+type: changed
diff --git a/changelogs/unreleased/update-bcrypt-to-support-libxcrypt.yml b/changelogs/unreleased/update-bcrypt-to-support-libxcrypt.yml
new file mode 100644
index 00000000000..c18a0f75d22
--- /dev/null
+++ b/changelogs/unreleased/update-bcrypt-to-support-libxcrypt.yml
@@ -0,0 +1,5 @@
+---
+title: update bcrypt to also support libxcrypt
+merge_request: 20260
+author: muhammadn
+type: other
diff --git a/changelogs/unreleased/update-environments-nav-controls.yml b/changelogs/unreleased/update-environments-nav-controls.yml
new file mode 100644
index 00000000000..639dadd0cdf
--- /dev/null
+++ b/changelogs/unreleased/update-environments-nav-controls.yml
@@ -0,0 +1,5 @@
+---
+title: Update environments nav controls icons
+merge_request: 20199
+author: George Tsiolis
+type: changed
diff --git a/changelogs/unreleased/update-external-link-icon-in-header-user-dropdown.yml b/changelogs/unreleased/update-external-link-icon-in-header-user-dropdown.yml
new file mode 100644
index 00000000000..ee769f06379
--- /dev/null
+++ b/changelogs/unreleased/update-external-link-icon-in-header-user-dropdown.yml
@@ -0,0 +1,5 @@
+---
+title: Update external link icon in header user dropdown
+merge_request: 20150
+author: George Tsiolis
+type: changed
diff --git a/changelogs/unreleased/update-external-link-icon-in-merge-request-widget.yml b/changelogs/unreleased/update-external-link-icon-in-merge-request-widget.yml
new file mode 100644
index 00000000000..c650c32f884
--- /dev/null
+++ b/changelogs/unreleased/update-external-link-icon-in-merge-request-widget.yml
@@ -0,0 +1,5 @@
+---
+title: Update external link icon in merge request widget
+merge_request: 20154
+author: George Tsiolis
+type: changed
diff --git a/changelogs/unreleased/update-integrations-external-link-icons.yml b/changelogs/unreleased/update-integrations-external-link-icons.yml
new file mode 100644
index 00000000000..9972744bd00
--- /dev/null
+++ b/changelogs/unreleased/update-integrations-external-link-icons.yml
@@ -0,0 +1,5 @@
+---
+title: Update integrations external link icons
+merge_request: 20205
+author: George Tsiolis
+type: changed
diff --git a/changelogs/unreleased/update-pipeline-icon-in-web-ide-sidebar.yml b/changelogs/unreleased/update-pipeline-icon-in-web-ide-sidebar.yml
new file mode 100644
index 00000000000..3f1f3c643e2
--- /dev/null
+++ b/changelogs/unreleased/update-pipeline-icon-in-web-ide-sidebar.yml
@@ -0,0 +1,5 @@
+---
+title: Update pipeline icon in web ide sidebar
+merge_request: 20058
+author: George Tsiolis
+type: changed
diff --git a/changelogs/unreleased/update-wiki-modal.yml b/changelogs/unreleased/update-wiki-modal.yml
deleted file mode 100644
index 00f2fc4f181..00000000000
--- a/changelogs/unreleased/update-wiki-modal.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: New design for wiki page deletion confirmation
-merge_request: 18712
-author: Constance Okoghenun
-type: added
diff --git a/changelogs/unreleased/upgrade-gitlab-markup.yml b/changelogs/unreleased/upgrade-gitlab-markup.yml
new file mode 100644
index 00000000000..9b0eaa7068d
--- /dev/null
+++ b/changelogs/unreleased/upgrade-gitlab-markup.yml
@@ -0,0 +1,5 @@
+---
+title: Fix extra blank line at start of rendered reStructuredText code block
+merge_request: 19596
+author:
+type: fixed
diff --git a/changelogs/unreleased/upgrade-hamlit-for-ruby25.yml b/changelogs/unreleased/upgrade-hamlit-for-ruby25.yml
new file mode 100644
index 00000000000..39e10121507
--- /dev/null
+++ b/changelogs/unreleased/upgrade-hamlit-for-ruby25.yml
@@ -0,0 +1,5 @@
+---
+title: 'Update hamlit to fix ruby 2.5 incompatibilities, fixes #42045'
+merge_request:
+author: Matthew Dawson
+type: fixed
diff --git a/changelogs/unreleased/use-backup-custom-hooks-gitaly.yml b/changelogs/unreleased/use-backup-custom-hooks-gitaly.yml
new file mode 100644
index 00000000000..4b9766332c3
--- /dev/null
+++ b/changelogs/unreleased/use-backup-custom-hooks-gitaly.yml
@@ -0,0 +1,5 @@
+---
+title: migrate backup rake task to gitaly
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/use-case-insensitive-ordering-for-dashboard-filters.yml b/changelogs/unreleased/use-case-insensitive-ordering-for-dashboard-filters.yml
deleted file mode 100644
index 098e4b1d5fa..00000000000
--- a/changelogs/unreleased/use-case-insensitive-ordering-for-dashboard-filters.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: "Use case in-sensitive ordering by name for dashboard"
-merge_request: 18553
-author: "@vedharish"
-type: fixed
diff --git a/changelogs/unreleased/use-tooltip-component-in-mr-widget-author-time-component.yml b/changelogs/unreleased/use-tooltip-component-in-mr-widget-author-time-component.yml
new file mode 100644
index 00000000000..4ab9b6dadc0
--- /dev/null
+++ b/changelogs/unreleased/use-tooltip-component-in-mr-widget-author-time-component.yml
@@ -0,0 +1,5 @@
+---
+title: Use Tooltip component in MrWidgetAuthorTime vue comonent
+merge_request: 19635
+author: George Tsiolis
+type: performance
diff --git a/changelogs/unreleased/web-hooks-log-pagination.yml b/changelogs/unreleased/web-hooks-log-pagination.yml
new file mode 100644
index 00000000000..fd9e4f9ca13
--- /dev/null
+++ b/changelogs/unreleased/web-hooks-log-pagination.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed pagination of web hook logs
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/winh-make-it-right-now.yml b/changelogs/unreleased/winh-make-it-right-now.yml
deleted file mode 100644
index 7b386c0b332..00000000000
--- a/changelogs/unreleased/winh-make-it-right-now.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Use "right now" for short time periods
-merge_request: 19095
-author:
-type: changed
diff --git a/changelogs/unreleased/winh-new-branch-url-encode.yml b/changelogs/unreleased/winh-new-branch-url-encode.yml
new file mode 100644
index 00000000000..f3554d0d4a1
--- /dev/null
+++ b/changelogs/unreleased/winh-new-branch-url-encode.yml
@@ -0,0 +1,5 @@
+---
+title: Fix branch name encoding for dropdown on issue page
+merge_request: 19634
+author:
+type: fixed
diff --git a/changelogs/unreleased/winh-stop-all-environments.yml b/changelogs/unreleased/winh-stop-all-environments.yml
new file mode 100644
index 00000000000..6e5f2f506d9
--- /dev/null
+++ b/changelogs/unreleased/winh-stop-all-environments.yml
@@ -0,0 +1,5 @@
+---
+title: Support manually stopping any environment from the UI
+merge_request: 20077
+author:
+type: changed
diff --git a/changelogs/unreleased/zj-add-branch-mandatory.yml b/changelogs/unreleased/zj-add-branch-mandatory.yml
deleted file mode 100644
index 82712ce842d..00000000000
--- a/changelogs/unreleased/zj-add-branch-mandatory.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adding branches through the WebUI is handled by Gitaly
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/zj-calculate-checksum-mandator.yml b/changelogs/unreleased/zj-calculate-checksum-mandator.yml
deleted file mode 100644
index 83315a3c5dd..00000000000
--- a/changelogs/unreleased/zj-calculate-checksum-mandator.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove shellout implementation for Repository checksums
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/zj-gitaly-read-write-check.yml b/changelogs/unreleased/zj-gitaly-read-write-check.yml
new file mode 100644
index 00000000000..43951d20e8f
--- /dev/null
+++ b/changelogs/unreleased/zj-gitaly-read-write-check.yml
@@ -0,0 +1,5 @@
+---
+title: Gitaly metrics check for read/writeability
+merge_request: 20022
+author:
+type: other
diff --git a/changelogs/unreleased/zj-ref-contains-sha-mandatory.yml b/changelogs/unreleased/zj-ref-contains-sha-mandatory.yml
deleted file mode 100644
index 61bdce43c0e..00000000000
--- a/changelogs/unreleased/zj-ref-contains-sha-mandatory.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Refs containting sha checks are done by Gitaly
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/zj-wiki-find-file-opt-out.yml b/changelogs/unreleased/zj-wiki-find-file-opt-out.yml
deleted file mode 100644
index 5af53c56017..00000000000
--- a/changelogs/unreleased/zj-wiki-find-file-opt-out.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Finding a wiki page is done by Gitaly by default
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/zj-workhorse-archive-mandatory.yml b/changelogs/unreleased/zj-workhorse-archive-mandatory.yml
deleted file mode 100644
index 3a4a351a2b9..00000000000
--- a/changelogs/unreleased/zj-workhorse-archive-mandatory.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Workhorse will use Gitaly to create archives
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/zj-workhorse-commit-patch-diff.yml b/changelogs/unreleased/zj-workhorse-commit-patch-diff.yml
deleted file mode 100644
index bce68692d98..00000000000
--- a/changelogs/unreleased/zj-workhorse-commit-patch-diff.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Workhorse to send raw diff and patch for commits
-merge_request:
-author:
-type: other
diff --git a/config/application.rb b/config/application.rb
index 09f706e3d70..0304f466734 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -1,10 +1,16 @@
-require File.expand_path('../boot', __FILE__)
+require File.expand_path('boot', __dir__)
require 'rails/all'
Bundler.require(:default, Rails.env)
module Gitlab
+ # This method is used for smooth upgrading from the current Rails 4.x to Rails 5.0.
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/14286
+ def self.rails5?
+ ENV["RAILS5"].in?(%w[1 true])
+ end
+
class Application < Rails::Application
require_dependency Rails.root.join('lib/gitlab/redis/wrapper')
require_dependency Rails.root.join('lib/gitlab/redis/cache')
@@ -12,6 +18,12 @@ module Gitlab
require_dependency Rails.root.join('lib/gitlab/redis/shared_state')
require_dependency Rails.root.join('lib/gitlab/request_context')
require_dependency Rails.root.join('lib/gitlab/current_settings')
+ require_dependency Rails.root.join('lib/gitlab/middleware/read_only')
+
+ # This needs to be loaded before DB connection is made
+ # to make sure that all connections have NO_ZERO_DATE
+ # setting disabled
+ require_dependency Rails.root.join('lib/mysql_zero_date')
# Settings in config/environments/* take precedence over those specified here.
# Application configuration should go into files in config/initializers
@@ -33,7 +45,8 @@ module Gitlab
#{config.root}/app/workers/concerns
#{config.root}/app/services/concerns
#{config.root}/app/serializers/concerns
- #{config.root}/app/finders/concerns])
+ #{config.root}/app/finders/concerns
+ #{config.root}/app/graphql/resolvers/concerns])
config.generators.templates.push("#{config.root}/generator_templates")
@@ -56,6 +69,13 @@ module Gitlab
# Configure the default encoding used in templates for Ruby 1.9.
config.encoding = "utf-8"
+ # ActionCable mount point.
+ # The default Rails' mount point is `/cable` which may conflict with existing
+ # namespaces/users.
+ # https://github.com/rails/rails/blob/5-0-stable/actioncable/lib/action_cable.rb#L38
+ # Please change this value when configuring ActionCable for real usage.
+ config.action_cable.mount_path = "/-/cable" if rails5?
+
# Configure sensitive parameters which will be filtered from the log file.
#
# Parameters filtered:
@@ -70,6 +90,7 @@ module Gitlab
# - Webhook URLs (:hook)
# - Sentry DSN (:sentry_dsn)
# - Deploy keys (:key)
+ # - File content from Web Editor (:content)
config.filter_parameters += [/token$/, /password/, /secret/]
config.filter_parameters += %i(
certificate
@@ -81,6 +102,7 @@ module Gitlab
sentry_dsn
trace
variables
+ content
)
# Enable escaping HTML in JSON.
@@ -116,6 +138,7 @@ module Gitlab
config.assets.precompile << "snippets.css"
config.assets.precompile << "locale/**/app.js"
config.assets.precompile << "emoji_sprites.css"
+ config.assets.precompile << "errors.css"
# Import gitlab-svgs directly from vendored directory
config.assets.paths << "#{config.root}/node_modules/@gitlab-org/gitlab-svgs/dist"
@@ -172,7 +195,7 @@ module Gitlab
ENV['GIT_TERMINAL_PROMPT'] = '0'
# Gitlab Read-only middleware support
- config.middleware.insert_after ActionDispatch::Flash, '::Gitlab::Middleware::ReadOnly'
+ config.middleware.insert_after ActionDispatch::Flash, ::Gitlab::Middleware::ReadOnly
config.generators do |g|
g.factory_bot false
@@ -188,7 +211,7 @@ module Gitlab
next unless name.include?('namespace_project')
define_method(name.sub('namespace_project', 'project')) do |project, *args|
- send(name, project&.namespace, project, *args) # rubocop:disable GitlabSecurity/PublicSend
+ send(name, project&.namespace, project, *args)
end
end
end
@@ -200,10 +223,4 @@ module Gitlab
Gitlab::Routing.add_helpers(MilestonesRoutingHelper)
end
end
-
- # This method is used for smooth upgrading from the current Rails 4.x to Rails 5.0.
- # https://gitlab.com/gitlab-org/gitlab-ce/issues/14286
- def self.rails5?
- ENV["RAILS5"].in?(%w[1 true])
- end
end
diff --git a/config/aws.yml.example b/config/aws.yml.example
deleted file mode 100644
index bb10c3cec7b..00000000000
--- a/config/aws.yml.example
+++ /dev/null
@@ -1,22 +0,0 @@
-# See https://github.com/jnicklas/carrierwave#using-amazon-s3
-# for more options
-# If you change this file in a Merge Request, please also create
-# a Merge Request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests
-#
-production:
- access_key_id: AKIA1111111111111UA
- secret_access_key: secret
- bucket: mygitlab.production.us
- region: us-east-1
-
-development:
- access_key_id: AKIA1111111111111UA
- secret_access_key: secret
- bucket: mygitlab.development.us
- region: us-east-1
-
-test:
- access_key_id: AKIA1111111111111UA
- secret_access_key: secret
- bucket: mygitlab.test.us
- region: us-east-1
diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml
index 6616b85129e..21c20cd5e93 100644
--- a/config/dependency_decisions.yml
+++ b/config/dependency_decisions.yml
@@ -534,3 +534,9 @@
:why: https://github.com/squaremo/bitsyntax-js/blob/master/LICENSE-MIT
:versions: []
:when: 2018-02-20 22:20:25.958123000 Z
+- - :approve
+ - "@webassemblyjs/ieee754"
+ - :who: Mike Greiling
+ :why: https://github.com/xtuc/webassemblyjs/blob/master/LICENSE
+ :versions: []
+ :when: 2018-06-08 05:30:56.764116000 Z
diff --git a/config/environment.rb b/config/environment.rb
index 487a4564b47..5d35937f7c6 100644
--- a/config/environment.rb
+++ b/config/environment.rb
@@ -4,7 +4,7 @@
if %w[1 true].include?(ENV["RAILS5"])
require_relative 'application'
else
- require File.expand_path('../application', __FILE__)
+ require File.expand_path('application', __dir__)
end
# Initialize the rails application
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 45a8c1add3e..23790b84e3c 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -39,7 +39,7 @@ Rails.application.configure do
config.action_mailer.delivery_method = :letter_opener_web
# Don't make a mess when bootstrapping a development environment
config.action_mailer.perform_deliveries = (ENV['BOOTSTRAP'] != '1')
- config.action_mailer.preview_path = 'spec/mailers/previews'
+ config.action_mailer.preview_path = 'app/mailers/previews'
config.eager_load = false
diff --git a/config/environments/test.rb b/config/environments/test.rb
index 1849c984351..af1011a1ab1 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -1,7 +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')
+ 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 7eb44b8059e..ab109f5d04f 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -33,7 +33,7 @@ production: &base
port: 80 # Set to 443 if using HTTPS, see installation.md#using-https for additional HTTPS configuration details
https: false # Set to true if using HTTPS, see installation.md#using-https for additional HTTPS configuration details
- # Uncommment this line below if your ssh host is different from HTTP/HTTPS one
+ # Uncomment this line below if your ssh host is different from HTTP/HTTPS one
# (you'd obviously need to replace ssh.host_example.com with your own host).
# Otherwise, ssh host will be set to the `host:` value above
# ssh_host: ssh.host_example.com
@@ -78,10 +78,15 @@ production: &base
# username_changing_enabled: false # default: true - User can change her username/namespace
## Default theme ID
## 1 - Indigo
- ## 2 - Dark
- ## 3 - Light
- ## 4 - Blue
+ ## 2 - Light Indigo
+ ## 3 - Blue
+ ## 4 - Light Blue
## 5 - Green
+ ## 6 - Light Green
+ ## 7 - Red
+ ## 8 - Light Red
+ ## 9 - Dark
+ ## 10 - Light
# default_theme: 1 # default: 1
## Automatic issue closing
@@ -155,6 +160,9 @@ production: &base
# aws_access_key_id: AWS_ACCESS_KEY_ID
# aws_secret_access_key: AWS_SECRET_ACCESS_KEY
# region: us-east-1
+ # aws_signature_version: 4 # For creation of signed URLs. Set to 2 if provider does not support v4.
+ # endpoint: 'https://s3.amazonaws.com' # default: nil - Useful for S3 compliant services such as DigitalOcean Spaces
+
## Git LFS
lfs:
@@ -175,6 +183,7 @@ production: &base
# Use the following options to configure an AWS compatible host
# host: 'localhost' # default: s3.amazonaws.com
# endpoint: 'http://127.0.0.1:9000' # default: nil
+ # aws_signature_version: 4 # For creation of signed URLs. Set to 2 if provider does not support v4.
# path_style: true # Use 'host/bucket_name/object' instead of 'bucket_name.host/object'
## Uploads (attachments, avatars, etc...)
@@ -192,6 +201,7 @@ production: &base
provider: AWS
aws_access_key_id: AWS_ACCESS_KEY_ID
aws_secret_access_key: AWS_SECRET_ACCESS_KEY
+ aws_signature_version: 4 # For creation of signed URLs. Set to 2 if provider does not support v4.
region: us-east-1
# host: 'localhost' # default: s3.amazonaws.com
# endpoint: 'http://127.0.0.1:9000' # default: nil
diff --git a/config/initializers/01_secret_token.rb b/config/initializers/01_secret_token.rb
new file mode 100644
index 00000000000..02bded43083
--- /dev/null
+++ b/config/initializers/01_secret_token.rb
@@ -0,0 +1,95 @@
+# This file needs to be loaded BEFORE any initializers that attempt to
+# prepend modules that require access to secrets (e.g. EE's 0_as_concern.rb).
+#
+# Be sure to restart your server when you modify this file.
+
+require 'securerandom'
+
+# Transition material in .secret to the secret_key_base key in config/secrets.yml.
+# Historically, ENV['SECRET_KEY_BASE'] takes precedence over .secret, so we maintain that
+# behavior.
+#
+# It also used to be the case that the key material in ENV['SECRET_KEY_BASE'] or .secret
+# was used to encrypt OTP (two-factor authentication) data so if present, we copy that key
+# material into config/secrets.yml under otp_key_base.
+#
+# Finally, if we have successfully migrated all secrets to config/secrets.yml, delete the
+# .secret file to avoid confusion.
+#
+def create_tokens
+ secret_file = Rails.root.join('.secret')
+ file_secret_key = File.read(secret_file).chomp if File.exist?(secret_file)
+ env_secret_key = ENV['SECRET_KEY_BASE']
+
+ # Ensure environment variable always overrides secrets.yml.
+ Rails.application.secrets.secret_key_base = env_secret_key if env_secret_key.present?
+
+ defaults = {
+ secret_key_base: file_secret_key || generate_new_secure_token,
+ otp_key_base: env_secret_key || file_secret_key || generate_new_secure_token,
+ db_key_base: generate_new_secure_token,
+ openid_connect_signing_key: generate_new_rsa_private_key
+ }
+
+ missing_secrets = set_missing_keys(defaults)
+ write_secrets_yml(missing_secrets) unless missing_secrets.empty?
+
+ begin
+ File.delete(secret_file) if file_secret_key
+ rescue => e
+ warn "Error deleting useless .secret file: #{e}"
+ end
+end
+
+def generate_new_secure_token
+ SecureRandom.hex(64)
+end
+
+def generate_new_rsa_private_key
+ OpenSSL::PKey::RSA.new(2048).to_pem
+end
+
+def warn_missing_secret(secret)
+ warn "Missing Rails.application.secrets.#{secret} for #{Rails.env} environment. The secret will be generated and stored in config/secrets.yml."
+end
+
+def set_missing_keys(defaults)
+ defaults.stringify_keys.each_with_object({}) do |(key, default), missing|
+ if Rails.application.secrets[key].blank?
+ warn_missing_secret(key)
+
+ missing[key] = Rails.application.secrets[key] = default
+ end
+ end
+end
+
+def write_secrets_yml(missing_secrets)
+ secrets_yml = Rails.root.join('config/secrets.yml')
+ rails_env = Rails.env.to_s
+ secrets = YAML.load_file(secrets_yml) if File.exist?(secrets_yml)
+ secrets ||= {}
+ secrets[rails_env] ||= {}
+
+ secrets[rails_env].merge!(missing_secrets) do |key, old, new|
+ # Previously, it was possible this was set to the literal contents of an Erb
+ # expression that evaluated to an empty value. We don't want to support that
+ # specifically, just ensure we don't break things further.
+ #
+ if old.present?
+ warn <<EOM
+Rails.application.secrets.#{key} was blank, but the literal value in config/secrets.yml was:
+ #{old}
+
+This probably isn't the expected value for this secret. To keep using a literal Erb string in config/secrets.yml, replace `<%` with `<%%`.
+EOM
+
+ exit 1 # rubocop:disable Rails/Exit
+ end
+
+ new
+ end
+
+ File.write(secrets_yml, YAML.dump(secrets), mode: 'w', perm: 0600)
+end
+
+create_tokens
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index dd36700964a..693a2934a1b 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -279,7 +279,7 @@ Settings.cron_jobs['expire_build_artifacts_worker']['cron'] ||= '50 * * * *'
Settings.cron_jobs['expire_build_artifacts_worker']['job_class'] = 'ExpireBuildArtifactsWorker'
Settings.cron_jobs['repository_check_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['repository_check_worker']['cron'] ||= '20 * * * *'
-Settings.cron_jobs['repository_check_worker']['job_class'] = 'RepositoryCheck::BatchWorker'
+Settings.cron_jobs['repository_check_worker']['job_class'] = 'RepositoryCheck::DispatchWorker'
Settings.cron_jobs['admin_email_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['admin_email_worker']['cron'] ||= '0 0 * * 0'
Settings.cron_jobs['admin_email_worker']['job_class'] = 'AdminEmailWorker'
@@ -289,6 +289,9 @@ Settings.cron_jobs['repository_archive_cache_worker']['job_class'] = 'Repository
Settings.cron_jobs['import_export_project_cleanup_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['import_export_project_cleanup_worker']['cron'] ||= '0 * * * *'
Settings.cron_jobs['import_export_project_cleanup_worker']['job_class'] = 'ImportExportProjectCleanupWorker'
+Settings.cron_jobs['ci_archive_traces_cron_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['ci_archive_traces_cron_worker']['cron'] ||= '17 * * * *'
+Settings.cron_jobs['ci_archive_traces_cron_worker']['job_class'] = 'Ci::ArchiveTracesCronWorker'
Settings.cron_jobs['requests_profiles_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['requests_profiles_worker']['cron'] ||= '0 0 * * *'
Settings.cron_jobs['requests_profiles_worker']['job_class'] = 'RequestsProfilesWorker'
@@ -335,6 +338,10 @@ Settings.cron_jobs['issue_due_scheduler_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['issue_due_scheduler_worker']['cron'] ||= '50 00 * * *'
Settings.cron_jobs['issue_due_scheduler_worker']['job_class'] = 'IssueDueSchedulerWorker'
+Settings.cron_jobs['prune_web_hook_logs_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['prune_web_hook_logs_worker']['cron'] ||= '0 */1 * * *'
+Settings.cron_jobs['prune_web_hook_logs_worker']['job_class'] = 'PruneWebHookLogsWorker'
+
#
# Sidekiq
#
@@ -391,8 +398,11 @@ repositories_storages = Settings.repositories.storages.values
repository_downloads_path = Settings.gitlab['repository_downloads_path'].to_s.gsub(%r{/$}, '')
repository_downloads_full_path = File.expand_path(repository_downloads_path, Settings.gitlab['user_home'])
-if repository_downloads_path.blank? || repositories_storages.any? { |rs| [repository_downloads_path, repository_downloads_full_path].include?(rs.legacy_disk_path.gsub(%r{/$}, '')) }
- Settings.gitlab['repository_downloads_path'] = File.join(Settings.shared['path'], 'cache/archive')
+# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/1255
+Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ if repository_downloads_path.blank? || repositories_storages.any? { |rs| [repository_downloads_path, repository_downloads_full_path].include?(rs.legacy_disk_path.gsub(%r{/$}, '')) }
+ Settings.gitlab['repository_downloads_path'] = File.join(Settings.shared['path'], 'cache/archive')
+ end
end
#
diff --git a/config/initializers/2_gitlab.rb b/config/initializers/2_gitlab.rb
index 1d2ab606a63..8b7f245b7b0 100644
--- a/config/initializers/2_gitlab.rb
+++ b/config/initializers/2_gitlab.rb
@@ -1 +1 @@
-require_relative '../../lib/gitlab'
+require_dependency 'gitlab'
diff --git a/config/initializers/6_validations.rb b/config/initializers/6_validations.rb
index 89aabe530fe..bf9e5a50382 100644
--- a/config/initializers/6_validations.rb
+++ b/config/initializers/6_validations.rb
@@ -2,20 +2,6 @@ def storage_name_valid?(name)
!!(name =~ /\A[a-zA-Z0-9\-_]+\z/)
end
-def find_parent_path(name, path)
- parent = Pathname.new(path).realpath.parent
- Gitlab.config.repositories.storages.detect do |n, rs|
- name != n && Pathname.new(rs.legacy_disk_path).realpath == parent
- end
-rescue Errno::EIO, Errno::ENOENT => e
- warning = "WARNING: couldn't verify #{path} (#{name}). "\
- "If this is an external storage, it might be offline."
- message = "#{warning}\n#{e.message}"
- Rails.logger.error("#{message}\n\t" + e.backtrace.join("\n\t"))
-
- nil
-end
-
def storage_validation_error(message)
raise "#{message}. Please fix this in your gitlab.yml before starting GitLab."
end
@@ -37,14 +23,4 @@ def validate_storages_config
end
end
-def validate_storages_paths
- Gitlab.config.repositories.storages.each do |name, repository_storage|
- parent_name, _parent_path = find_parent_path(name, repository_storage.legacy_disk_path)
- if parent_name
- storage_validation_error("#{name} is a nested path of #{parent_name}. Nested paths are not supported for repository storages")
- end
- end
-end
-
validate_storages_config
-validate_storages_paths unless Rails.env.test? || ENV['SKIP_STORAGE_VALIDATION'] == 'true'
diff --git a/config/initializers/7_prometheus_metrics.rb b/config/initializers/7_prometheus_metrics.rb
index eb7959e4da6..146c4b1e024 100644
--- a/config/initializers/7_prometheus_metrics.rb
+++ b/config/initializers/7_prometheus_metrics.rb
@@ -25,7 +25,7 @@ Sidekiq.configure_server do |config|
end
end
-if Gitlab::Metrics.prometheus_metrics_enabled?
+if !Rails.env.test? && Gitlab::Metrics.prometheus_metrics_enabled?
unless Sidekiq.server?
Gitlab::Metrics::Samplers::UnicornSampler.initialize_instance(Settings.monitoring.unicorn_sampler_interval).start
end
diff --git a/config/initializers/active_record_data_types.rb b/config/initializers/active_record_data_types.rb
index fda13d0c4cb..717e30b5b7e 100644
--- a/config/initializers/active_record_data_types.rb
+++ b/config/initializers/active_record_data_types.rb
@@ -65,7 +65,7 @@ elsif Gitlab::Database.mysql?
prepend RegisterDateTimeWithTimeZone
# Add the class `DateTimeWithTimeZone` so we can map `timestamp` to it.
- class MysqlDateTimeWithTimeZone < MysqlDateTime
+ class MysqlDateTimeWithTimeZone < (Gitlab.rails5? ? ActiveRecord::Type::DateTime : MysqlDateTime)
def type
:datetime_with_timezone
end
diff --git a/config/initializers/active_record_locking.rb b/config/initializers/active_record_locking.rb
index 3e7111fd063..21ff323927b 100644
--- a/config/initializers/active_record_locking.rb
+++ b/config/initializers/active_record_locking.rb
@@ -1,73 +1,80 @@
# rubocop:disable Lint/RescueException
-# Remove this entire initializer when we are at rails 5.0.
-# This file fixes the bug (see below) which has been fixed in the upstream.
-unless Gitlab.rails5?
- # This patch fixes https://github.com/rails/rails/issues/26024
- # TODO: Remove it when it's no longer necessary
-
- module ActiveRecord
- module Locking
- module Optimistic
- # We overwrite this method because we don't want to have default value
- # for newly created records
- def _create_record(attribute_names = self.attribute_names, *) # :nodoc:
- super
- end
+# Remove this monkey-patch when all lock_version values are converted from NULLs to zeros.
+# See https://gitlab.com/gitlab-org/gitlab-ce/issues/25228
+module ActiveRecord
+ module Locking
+ module Optimistic
+ # We overwrite this method because we don't want to have default value
+ # for newly created records
+ def _create_record(attribute_names = self.attribute_names, *) # :nodoc:
+ super
+ end
- def _update_record(attribute_names = self.attribute_names) #:nodoc:
- return super unless locking_enabled?
- return 0 if attribute_names.empty?
+ def _update_record(attribute_names = self.attribute_names) #:nodoc:
+ return super unless locking_enabled?
+ return 0 if attribute_names.empty?
- lock_col = self.class.locking_column
+ lock_col = self.class.locking_column
- previous_lock_value = send(lock_col).to_i # rubocop:disable GitlabSecurity/PublicSend
+ previous_lock_value = send(lock_col).to_i
- # This line is added as a patch
- previous_lock_value = nil if previous_lock_value == '0' || previous_lock_value == 0
+ # This line is added as a patch
+ previous_lock_value = nil if previous_lock_value == '0' || previous_lock_value == 0
- increment_lock
+ increment_lock
- attribute_names += [lock_col]
- attribute_names.uniq!
+ attribute_names += [lock_col]
+ attribute_names.uniq!
- begin
- relation = self.class.unscoped
+ begin
+ relation = self.class.unscoped
- affected_rows = relation.where(
- self.class.primary_key => id,
- lock_col => previous_lock_value
- ).update_all(
- attributes_for_update(attribute_names).map do |name|
- [name, _read_attribute(name)]
- end.to_h
- )
+ affected_rows = relation.where(
+ self.class.primary_key => id,
+ lock_col => previous_lock_value
+ ).update_all(
+ attributes_for_update(attribute_names).map do |name|
+ [name, _read_attribute(name)]
+ end.to_h
+ )
- unless affected_rows == 1
- raise ActiveRecord::StaleObjectError.new(self, "update")
- end
+ unless affected_rows == 1
+ raise ActiveRecord::StaleObjectError.new(self, "update")
+ end
- affected_rows
+ affected_rows
- # If something went wrong, revert the version.
- rescue Exception
- send(lock_col + '=', previous_lock_value) # rubocop:disable GitlabSecurity/PublicSend
- raise
- end
+ # If something went wrong, revert the version.
+ rescue Exception
+ send(lock_col + '=', previous_lock_value)
+ raise
end
+ end
- # This is patched because we need it to query `lock_version IS NULL`
- # rather than `lock_version = 0` whenever lock_version is NULL.
- def relation_for_destroy
- return super unless locking_enabled?
+ # This is patched because we need it to query `lock_version IS NULL`
+ # rather than `lock_version = 0` whenever lock_version is NULL.
+ def relation_for_destroy
+ return super unless locking_enabled?
- column_name = self.class.locking_column
- super.where(self.class.arel_table[column_name].eq(self[column_name]))
- end
+ column_name = self.class.locking_column
+ super.where(self.class.arel_table[column_name].eq(self[column_name]))
end
+ end
+
+ # This is patched because we want `lock_version` default to `NULL`
+ # rather than `0`
+ if Gitlab.rails5?
+ class LockingType
+ def deserialize(value)
+ super
+ end
- # This is patched because we want `lock_version` default to `NULL`
- # rather than `0`
+ def serialize(value)
+ super
+ end
+ end
+ else
class LockingType < SimpleDelegator
def type_cast_from_database(value)
super
diff --git a/config/initializers/active_record_migration.rb b/config/initializers/active_record_migration.rb
new file mode 100644
index 00000000000..04c06be7834
--- /dev/null
+++ b/config/initializers/active_record_migration.rb
@@ -0,0 +1,10 @@
+require 'active_record/migration'
+
+module ActiveRecord
+ class Migration
+ # data_source_exists? is not available in 4.2.10, table_exists deprecated in 5.0
+ def table_exists?(table_name)
+ ActiveRecord::Base.connection.data_source_exists?(table_name)
+ end
+ end
+end
diff --git a/config/initializers/active_record_table_definition.rb b/config/initializers/active_record_table_definition.rb
index 8e3a1c7a62f..a71069f27a3 100644
--- a/config/initializers/active_record_table_definition.rb
+++ b/config/initializers/active_record_table_definition.rb
@@ -29,6 +29,11 @@ module ActiveRecord
def datetime_with_timezone(column_name, **options)
column(column_name, :datetime_with_timezone, options)
end
+
+ # Disable timestamp alias to datetime
+ def aliased_types(name, fallback)
+ fallback
+ end
end
end
end
diff --git a/config/initializers/artifacts_direct_upload_support.rb b/config/initializers/artifacts_direct_upload_support.rb
deleted file mode 100644
index d2bc35ea613..00000000000
--- a/config/initializers/artifacts_direct_upload_support.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-artifacts_object_store = Gitlab.config.artifacts.object_store
-
-if artifacts_object_store.enabled &&
- artifacts_object_store.direct_upload &&
- artifacts_object_store.connection&.provider.to_s != 'Google'
- raise "Only 'Google' is supported as a object storage provider when 'direct_upload' of artifacts is used"
-end
diff --git a/config/initializers/carrierwave.rb b/config/initializers/carrierwave.rb
deleted file mode 100644
index 5cde6cbb0ff..00000000000
--- a/config/initializers/carrierwave.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-CarrierWave::SanitizedFile.sanitize_regexp = /[^[:word:]\.\-\+]/
-
-aws_file = Rails.root.join('config', 'aws.yml')
-
-if File.exist?(aws_file)
- AWS_CONFIG = YAML.load(File.read(aws_file))[Rails.env]
-
- CarrierWave.configure do |config|
- config.fog_provider = 'fog/aws'
-
- config.fog_credentials = {
- provider: 'AWS', # required
- aws_access_key_id: AWS_CONFIG['access_key_id'], # required
- aws_secret_access_key: AWS_CONFIG['secret_access_key'], # required
- region: AWS_CONFIG['region'], # optional, defaults to 'us-east-1'
- }
-
- # required
- config.fog_directory = AWS_CONFIG['bucket']
-
- # optional, defaults to true
- config.fog_public = false
-
- # optional, defaults to {}
- config.fog_attributes = { 'Cache-Control' => 'max-age=315576000' }
-
- # optional time (in seconds) that authenticated urls will be valid.
- # when fog_public is false and provider is AWS or Google, defaults to 600
- config.fog_authenticated_url_expiration = 1 << 29
- end
-end
diff --git a/config/initializers/console_message.rb b/config/initializers/console_message.rb
index 536ab337d85..f7c26732e6d 100644
--- a/config/initializers/console_message.rb
+++ b/config/initializers/console_message.rb
@@ -3,8 +3,8 @@ if defined?(Rails::Console)
# note that this will not print out when using `spring`
justify = 15
puts "-------------------------------------------------------------------------------------"
- puts " Gitlab:".ljust(justify) + "#{Gitlab::VERSION} (#{Gitlab::REVISION})"
- puts " Gitlab Shell:".ljust(justify) + Gitlab::Shell.new.version
+ puts " GitLab:".ljust(justify) + "#{Gitlab::VERSION} (#{Gitlab.revision})"
+ puts " GitLab Shell:".ljust(justify) + "#{Gitlab::VersionInfo.parse(Gitlab::Shell.new.version)}"
puts " #{Gitlab::Database.adapter_name}:".ljust(justify) + Gitlab::Database.version
puts "-------------------------------------------------------------------------------------"
end
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb
index 362b9cc9a88..e5772c33307 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -219,5 +219,7 @@ Devise.setup do |config|
end
end
- Gitlab::OmniauthInitializer.new(config).execute(Gitlab.config.omniauth.providers)
+ if Gitlab::OmniauthInitializer.enabled?
+ Gitlab::OmniauthInitializer.new(config).execute(Gitlab.config.omniauth.providers)
+ end
end
diff --git a/config/initializers/direct_upload_support.rb b/config/initializers/direct_upload_support.rb
new file mode 100644
index 00000000000..32fc8c8bc69
--- /dev/null
+++ b/config/initializers/direct_upload_support.rb
@@ -0,0 +1,19 @@
+class DirectUploadsValidator
+ SUPPORTED_DIRECT_UPLOAD_PROVIDERS = %w(Google AWS).freeze
+
+ ValidationError = Class.new(StandardError)
+
+ def verify!(object_store)
+ return unless object_store.enabled
+ return unless object_store.direct_upload
+ return if SUPPORTED_DIRECT_UPLOAD_PROVIDERS.include?(object_store.connection&.provider.to_s)
+
+ raise ValidationError, "Only #{SUPPORTED_DIRECT_UPLOAD_PROVIDERS.join(',')} are supported as a object storage provider when 'direct_upload' is used"
+ end
+end
+
+DirectUploadsValidator.new.tap do |validator|
+ [Gitlab.config.artifacts, Gitlab.config.uploads, Gitlab.config.lfs].each do |uploader|
+ validator.verify!(uploader.object_store)
+ end
+end
diff --git a/config/initializers/disable_email_interceptor.rb b/config/initializers/disable_email_interceptor.rb
index c76a6b8b19f..e8770c8d460 100644
--- a/config/initializers/disable_email_interceptor.rb
+++ b/config/initializers/disable_email_interceptor.rb
@@ -1,2 +1,5 @@
# Interceptor in lib/disable_email_interceptor.rb
-ActionMailer::Base.register_interceptor(DisableEmailInterceptor) unless Gitlab.config.gitlab.email_enabled
+unless Gitlab.config.gitlab.email_enabled
+ ActionMailer::Base.register_interceptor(DisableEmailInterceptor)
+ ActionMailer::Base.logger = nil
+end
diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb
index e3a342590d4..f321b4ea763 100644
--- a/config/initializers/doorkeeper.rb
+++ b/config/initializers/doorkeeper.rb
@@ -37,7 +37,7 @@ Doorkeeper.configure do
# Reuse access token for the same resource owner within an application (disabled by default)
# Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/383
- # reuse_access_token
+ reuse_access_token
# Issue access tokens with refresh token (disabled by default)
use_refresh_token
@@ -106,3 +106,53 @@ Doorkeeper.configure do
base_controller '::Gitlab::BaseDoorkeeperController'
end
+
+# Monkey patch to avoid creating new applications if the scope of the
+# app created does not match the complete list of scopes of the configured app.
+# It also prevents the OAuth authorize application window to appear every time.
+
+# Remove after we upgrade the doorkeeper gem from version 4.3.2
+if Doorkeeper.gem_version > Gem::Version.new('4.3.2')
+ raise "Doorkeeper was upgraded, please remove the monkey patch in #{__FILE__}"
+end
+
+module Doorkeeper
+ module AccessTokenMixin
+ module ClassMethods
+ def matching_token_for(application, resource_owner_or_id, scopes)
+ resource_owner_id =
+ if resource_owner_or_id.respond_to?(:to_key)
+ resource_owner_or_id.id
+ else
+ resource_owner_or_id
+ end
+
+ tokens = authorized_tokens_for(application.try(:id), resource_owner_id)
+ tokens.detect do |token|
+ scopes_match?(token.scopes, scopes, application.try(:scopes))
+ end
+ end
+
+ def scopes_match?(token_scopes, param_scopes, app_scopes)
+ return true if token_scopes.empty? && param_scopes.empty?
+
+ (token_scopes.sort == param_scopes.sort) &&
+ Doorkeeper::OAuth::Helpers::ScopeChecker.valid?(
+ param_scopes.to_s,
+ Doorkeeper.configuration.scopes,
+ app_scopes)
+ end
+
+ def authorized_tokens_for(application_id, resource_owner_id)
+ ordered_by(:created_at, :desc)
+ .where(application_id: application_id,
+ resource_owner_id: resource_owner_id,
+ revoked_at: nil)
+ end
+
+ def last_authorized_token_for(application_id, resource_owner_id)
+ authorized_tokens_for(application_id, resource_owner_id).first
+ end
+ end
+ end
+end
diff --git a/config/initializers/doorkeeper_openid_connect.rb b/config/initializers/doorkeeper_openid_connect.rb
index 98e1f6e830f..ae5d834a02c 100644
--- a/config/initializers/doorkeeper_openid_connect.rb
+++ b/config/initializers/doorkeeper_openid_connect.rb
@@ -18,12 +18,17 @@ Doorkeeper::OpenidConnect.configure do
end
subject do |user|
- # hash the user's ID with the Rails secret_key_base to avoid revealing it
- Digest::SHA256.hexdigest "#{user.id}-#{Rails.application.secrets.secret_key_base}"
+ user.id
end
claims do
with_options scope: :openid do |o|
+ o.claim(:sub_legacy, response: [:id_token, :user_info]) do |user|
+ # provide the previously hashed 'sub' claim to allow third-party apps
+ # to migrate to the new unhashed value
+ Digest::SHA256.hexdigest "#{user.id}-#{Rails.application.secrets.secret_key_base}"
+ end
+
o.claim(:name) { |user| user.name }
o.claim(:nickname) { |user| user.username }
o.claim(:email) { |user| user.public_email }
diff --git a/config/initializers/grape_route_helpers_fix.rb b/config/initializers/grape_route_helpers_fix.rb
deleted file mode 100644
index 612cca3dfbd..00000000000
--- a/config/initializers/grape_route_helpers_fix.rb
+++ /dev/null
@@ -1,51 +0,0 @@
-if defined?(GrapeRouteHelpers)
- module GrapeRouteHelpers
- module AllRoutes
- # Bringing in PR https://github.com/reprah/grape-route-helpers/pull/21 due to abandonment.
- #
- # Without the following fix, when two helper methods are the same, but have different arguments
- # (for example: api_v1_cats_owners_path(id: 1) vs api_v1_cats_owners_path(id: 1, owner_id: 2))
- # if the helper method with the least number of arguments is defined first (because the route was defined first)
- # then it will shadow the longer route.
- #
- # The fix is to sort descending by amount of arguments
- def decorated_routes
- @decorated_routes ||= all_routes
- .map { |r| DecoratedRoute.new(r) }
- .sort_by { |r| -r.dynamic_path_segments.count }
- end
- end
-
- class DecoratedRoute
- # GrapeRouteHelpers gem tries to parse the versions
- # from a string, not supporting Grape `version` array definition.
- #
- # Without the following fix, we get this on route helpers generation:
- #
- # => undefined method `scan' for ["v3", "v4"]
- #
- # 2.0.0 implementation of this method:
- #
- # ```
- # def route_versions
- # version_pattern = /[^\[",\]\s]+/
- # if route_version
- # route_version.scan(version_pattern)
- # else
- # [nil]
- # end
- # end
- # ```
- def route_versions
- return [nil] if route_version.nil? || route_version.empty?
-
- if route_version.is_a?(String)
- version_pattern = /[^\[",\]\s]+/
- route_version.scan(version_pattern)
- else
- route_version
- end
- end
- end
- end
-end
diff --git a/config/initializers/lograge.rb b/config/initializers/lograge.rb
index 114c1cb512f..1cf8a24e98c 100644
--- a/config/initializers/lograge.rb
+++ b/config/initializers/lograge.rb
@@ -27,6 +27,7 @@ unless Sidekiq.server?
gitaly_calls = Gitlab::GitalyClient.get_request_count
payload[:gitaly_calls] = gitaly_calls if gitaly_calls > 0
+ payload[:response] = event.payload[:response] if event.payload[:response]
payload
end
diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb
index e9326653cbe..acbdf8de5a6 100644
--- a/config/initializers/mime_types.rb
+++ b/config/initializers/mime_types.rb
@@ -15,3 +15,5 @@ Mime::Type.register "video/ogg", :ogv
Mime::Type.unregister :json
Mime::Type.register 'application/json', :json, [LfsRequest::CONTENT_TYPE, 'application/json']
+
+Mime::Type.register 'image/x-icon', :ico
diff --git a/config/initializers/mini_magick.rb b/config/initializers/mini_magick.rb
new file mode 100644
index 00000000000..db0e7bbaaa3
--- /dev/null
+++ b/config/initializers/mini_magick.rb
@@ -0,0 +1,3 @@
+MiniMagick.configure do |config|
+ config.cli = :graphicsmagick
+end
diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb
index e33ebb25c4c..c558eb28ced 100644
--- a/config/initializers/omniauth.rb
+++ b/config/initializers/omniauth.rb
@@ -17,14 +17,7 @@ OmniAuth.config.before_request_phase do |env|
Gitlab::RequestForgeryProtection.call(env)
end
-if Gitlab.config.omniauth.enabled
+if Gitlab::OmniauthInitializer.enabled?
provider_names = Gitlab.config.omniauth.providers.map(&:name)
- require 'omniauth-kerberos' if provider_names.include?('kerberos')
-end
-
-module OmniAuth
- module Strategies
- autoload :Bitbucket, Rails.root.join('lib', 'omni_auth', 'strategies', 'bitbucket')
- autoload :Jwt, Rails.root.join('lib', 'omni_auth', 'strategies', 'jwt')
- end
+ Gitlab::Auth.omniauth_setup_providers(provider_names)
end
diff --git a/config/initializers/postgresql_opclasses_support.rb b/config/initializers/postgresql_opclasses_support.rb
index c2f3023b330..7b8afc78817 100644
--- a/config/initializers/postgresql_opclasses_support.rb
+++ b/config/initializers/postgresql_opclasses_support.rb
@@ -41,7 +41,12 @@ module ActiveRecord
# Abstract representation of an index definition on a table. Instances of
# this type are typically created and returned by methods in database
# adapters. e.g. ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#indexes
- class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using, :opclasses) #:nodoc:
+ attrs = [:table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using, :opclasses]
+
+ # In Rails 5 the second last attribute is newly `:comment`
+ attrs.insert(-2, :comment) if Gitlab.rails5?
+
+ class IndexDefinition < Struct.new(*attrs) #:nodoc:
end
end
end
@@ -107,8 +112,15 @@ module ActiveRecord
result.map do |row|
index_name = row[0]
- unique = row[1] == 't'
+ unique = if Gitlab.rails5?
+ row[1]
+ else
+ row[1] == 't'
+ end
indkey = row[2].split(" ")
+ if Gitlab.rails5?
+ indkey = indkey.map(&:to_i)
+ end
inddef = row[3]
oid = row[4]
diff --git a/config/initializers/rack_attack_global.rb b/config/initializers/rack_attack_global.rb
index a90516eee7d..45963831c41 100644
--- a/config/initializers/rack_attack_global.rb
+++ b/config/initializers/rack_attack_global.rb
@@ -26,7 +26,7 @@ class Rack::Attack
throttle('throttle_unauthenticated', Gitlab::Throttle.unauthenticated_options) do |req|
Gitlab::Throttle.settings.throttle_unauthenticated_enabled &&
req.unauthenticated? &&
- !req.api_internal_request? &&
+ !req.should_be_skipped? &&
req.ip
end
@@ -59,6 +59,10 @@ class Rack::Attack
path =~ %r{^/api/v\d+/internal/}
end
+ def should_be_skipped?
+ api_internal_request?
+ end
+
def web_request?
!api_request?
end
diff --git a/config/initializers/secret_token.rb b/config/initializers/secret_token.rb
deleted file mode 100644
index 750a5b34f3b..00000000000
--- a/config/initializers/secret_token.rb
+++ /dev/null
@@ -1,92 +0,0 @@
-# Be sure to restart your server when you modify this file.
-
-require 'securerandom'
-
-# Transition material in .secret to the secret_key_base key in config/secrets.yml.
-# Historically, ENV['SECRET_KEY_BASE'] takes precedence over .secret, so we maintain that
-# behavior.
-#
-# It also used to be the case that the key material in ENV['SECRET_KEY_BASE'] or .secret
-# was used to encrypt OTP (two-factor authentication) data so if present, we copy that key
-# material into config/secrets.yml under otp_key_base.
-#
-# Finally, if we have successfully migrated all secrets to config/secrets.yml, delete the
-# .secret file to avoid confusion.
-#
-def create_tokens
- secret_file = Rails.root.join('.secret')
- file_secret_key = File.read(secret_file).chomp if File.exist?(secret_file)
- env_secret_key = ENV['SECRET_KEY_BASE']
-
- # Ensure environment variable always overrides secrets.yml.
- Rails.application.secrets.secret_key_base = env_secret_key if env_secret_key.present?
-
- defaults = {
- secret_key_base: file_secret_key || generate_new_secure_token,
- otp_key_base: env_secret_key || file_secret_key || generate_new_secure_token,
- db_key_base: generate_new_secure_token,
- openid_connect_signing_key: generate_new_rsa_private_key
- }
-
- missing_secrets = set_missing_keys(defaults)
- write_secrets_yml(missing_secrets) unless missing_secrets.empty?
-
- begin
- File.delete(secret_file) if file_secret_key
- rescue => e
- warn "Error deleting useless .secret file: #{e}"
- end
-end
-
-def generate_new_secure_token
- SecureRandom.hex(64)
-end
-
-def generate_new_rsa_private_key
- OpenSSL::PKey::RSA.new(2048).to_pem
-end
-
-def warn_missing_secret(secret)
- warn "Missing Rails.application.secrets.#{secret} for #{Rails.env} environment. The secret will be generated and stored in config/secrets.yml."
-end
-
-def set_missing_keys(defaults)
- defaults.stringify_keys.each_with_object({}) do |(key, default), missing|
- if Rails.application.secrets[key].blank?
- warn_missing_secret(key)
-
- missing[key] = Rails.application.secrets[key] = default
- end
- end
-end
-
-def write_secrets_yml(missing_secrets)
- secrets_yml = Rails.root.join('config/secrets.yml')
- rails_env = Rails.env.to_s
- secrets = YAML.load_file(secrets_yml) if File.exist?(secrets_yml)
- secrets ||= {}
- secrets[rails_env] ||= {}
-
- secrets[rails_env].merge!(missing_secrets) do |key, old, new|
- # Previously, it was possible this was set to the literal contents of an Erb
- # expression that evaluated to an empty value. We don't want to support that
- # specifically, just ensure we don't break things further.
- #
- if old.present?
- warn <<EOM
-Rails.application.secrets.#{key} was blank, but the literal value in config/secrets.yml was:
- #{old}
-
-This probably isn't the expected value for this secret. To keep using a literal Erb string in config/secrets.yml, replace `<%` with `<%%`.
-EOM
-
- exit 1 # rubocop:disable Rails/Exit
- end
-
- new
- end
-
- File.write(secrets_yml, YAML.dump(secrets), mode: 'w', perm: 0600)
-end
-
-create_tokens
diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb
index b2da3b3dc19..17d09293205 100644
--- a/config/initializers/sentry.rb
+++ b/config/initializers/sentry.rb
@@ -13,7 +13,7 @@ def configure_sentry
if sentry_enabled
Raven.configure do |config|
config.dsn = Gitlab::CurrentSettings.current_application_settings.sentry_dsn
- config.release = Gitlab::REVISION
+ config.release = Gitlab.revision
# Sanitize fields based on those sanitized from Rails.
config.sanitize_fields = Rails.application.config.filter_parameters.map(&:to_s)
diff --git a/config/karma.config.js b/config/karma.config.js
index 28a688797d9..84810332dc2 100644
--- a/config/karma.config.js
+++ b/config/karma.config.js
@@ -15,6 +15,7 @@ function fatalError(message) {
// disable problematic options
webpackConfig.entry = undefined;
webpackConfig.mode = 'development';
+webpackConfig.optimization.nodeEnv = false;
webpackConfig.optimization.runtimeChunk = false;
webpackConfig.optimization.splitChunks = false;
diff --git a/config/locales/carrierwave.en.yml b/config/locales/carrierwave.en.yml
new file mode 100644
index 00000000000..12619226460
--- /dev/null
+++ b/config/locales/carrierwave.en.yml
@@ -0,0 +1,14 @@
+en:
+ errors:
+ messages:
+ carrierwave_processing_error: failed to be processed
+ carrierwave_integrity_error: is not of an allowed file type
+ carrierwave_download_error: could not be downloaded
+ extension_whitelist_error: "You are not allowed to upload %{extension} files, allowed types: %{allowed_types}"
+ extension_blacklist_error: "You are not allowed to upload %{extension} files, prohibited types: %{prohibited_types}"
+ content_type_whitelist_error: "You are not allowed to upload %{content_type} files"
+ content_type_blacklist_error: "You are not allowed to upload %{content_type} files"
+ rmagick_processing_error: "Failed to manipulate with rmagick, maybe it is not an image?"
+ mini_magick_processing_error: "Failed to manipulate with MiniMagick, maybe it is not an image? Original Error: %{e}"
+ min_size_error: "File size should be greater than %{min_size}"
+ max_size_error: "File size should be less than %{max_size}"
diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml
index 889111282ef..9f451046462 100644
--- a/config/locales/doorkeeper.en.yml
+++ b/config/locales/doorkeeper.en.yml
@@ -60,17 +60,23 @@ en:
scopes:
api: Access the authenticated user's API
read_user: Read the authenticated user's personal information
+ read_repository: Allows read-access to the repository
+ read_registry: Grants permission to read container registry images
openid: Authenticate using OpenID Connect
- sudo: Perform API actions as any user in the system (if the authenticated user is an admin)
+ sudo: Perform API actions as any user in the system
scope_desc:
api:
- Full access to GitLab as the user, including read/write on all their groups and projects
+ Grants complete read/write access to the API, including all groups and projects.
read_user:
- Read-only access to the user's profile information, like username, public email and full name
+ Grants read-only access to the authenticated user's profile through the /user API endpoint, which includes username, public email, and full name. Also grants access to read-only API endpoints under /users.
+ read_repository:
+ Grants read-only access to repositories on private projects using Git-over-HTTP (not using the API).
+ read_registry:
+ Grants read-only access to container registry images on private projects.
openid:
- The ability to authenticate using GitLab, and read-only access to the user's profile information and group memberships
+ Grants permission to authenticate with GitLab using OpenID Connect. Also gives read-only access to the user's profile and group memberships.
sudo:
- Access to the Sudo feature, to perform API actions as any user in the system (only available for admins)
+ Grants permission to perform API actions as any user in the system, when authenticated as an admin user.
flash:
applications:
create:
diff --git a/config/prometheus/additional_metrics.yml b/config/prometheus/additional_metrics.yml
index 13732384953..c994bad7865 100644
--- a/config/prometheus/additional_metrics.yml
+++ b/config/prometheus/additional_metrics.yml
@@ -29,14 +29,14 @@
label: Pod average
unit: ms
- title: "HTTP Error Rate"
- y_label: "HTTP 500 Errors / Sec"
+ y_label: "HTTP Errors"
required_metrics:
- nginx_upstream_responses_total
weight: 1
queries:
- - query_range: 'sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m]))'
- label: HTTP Errors
- unit: "errors / sec"
+ - query_range: 'sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) / sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) * 100'
+ label: 5xx Errors
+ unit: "%"
- group: Response metrics (HA Proxy)
priority: 10
metrics:
diff --git a/config/routes.rb b/config/routes.rb
index 52726f94753..e0a9139b1b4 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -11,6 +11,12 @@ Rails.application.routes.draw do
post :toggle_award_emoji, on: :member
end
+ favicon_redirect = redirect do |_params, _request|
+ ActionController::Base.helpers.asset_url(Gitlab::Favicon.main)
+ end
+ get 'favicon.png', to: favicon_redirect
+ get 'favicon.ico', to: favicon_redirect
+
draw :sherlock
draw :development
draw :ci
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index 3cca1210e39..ff27ceb50dc 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -102,6 +102,7 @@ namespace :admin do
get :preview_sign_in
delete :logo
delete :header_logos
+ delete :favicon
end
end
diff --git a/config/routes/api.rb b/config/routes/api.rb
index ce7a7c88900..b1aebf4d606 100644
--- a/config/routes/api.rb
+++ b/config/routes/api.rb
@@ -1,2 +1,7 @@
+constraints(::Constraints::FeatureConstrainer.new(:graphql)) do
+ post '/api/graphql', to: 'graphql#execute'
+ mount GraphiQL::Rails::Engine, at: '/-/graphql-explorer', graphql_path: '/api/graphql'
+end
+
API::API.logger Rails.logger
mount API::API => '/'
diff --git a/config/routes/dashboard.rb b/config/routes/dashboard.rb
index d2437285cdf..f1e8c2b9d82 100644
--- a/config/routes/dashboard.rb
+++ b/config/routes/dashboard.rb
@@ -1,4 +1,5 @@
resource :dashboard, controller: 'dashboard', only: [] do
+ get :issues, action: :issues_calendar, constraints: lambda { |req| req.format == :ics }
get :issues
get :merge_requests
get :activity
diff --git a/config/routes/group.rb b/config/routes/group.rb
index 7c4c3d370e0..25fbb38ba87 100644
--- a/config/routes/group.rb
+++ b/config/routes/group.rb
@@ -5,9 +5,10 @@ end
constraints(::Constraints::GroupUrlConstrainer.new) do
scope(path: 'groups/*id',
controller: :groups,
- constraints: { id: Gitlab::PathRegex.full_namespace_route_regex, format: /(html|json|atom)/ }) do
+ constraints: { id: Gitlab::PathRegex.full_namespace_route_regex, format: /(html|json|atom|ics)/ }) do
scope(path: '-') do
get :edit, as: :edit_group
+ get :issues, as: :issues_group_calendar, action: :issues_calendar, constraints: lambda { |req| req.format == :ics }
get :issues, as: :issues_group
get :merge_requests, as: :merge_requests_group
get :projects, as: :projects_group
@@ -30,6 +31,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
resource :variables, only: [:show, :update]
resources :children, only: [:index]
+ resources :shared_projects, only: [:index]
resources :labels, except: [:show] do
post :toggle_subscription, on: :member
@@ -53,6 +55,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
resources :uploads, only: [:create] do
collection do
get ":secret/:filename", action: :show, as: :show, constraints: { filename: %r{[^/]+} }
+ post :authorize
end
end
diff --git a/config/routes/profile.rb b/config/routes/profile.rb
index a9ba5ac2c0b..c1cac3905f1 100644
--- a/config/routes/profile.rb
+++ b/config/routes/profile.rb
@@ -7,7 +7,7 @@ resource :profile, only: [:show, :update] do
get :applications, to: 'oauth/applications#index'
put :reset_incoming_email_token
- put :reset_rss_token
+ put :reset_feed_token
put :update_username
end
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 5a1be1a8b73..5057e937941 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -206,14 +206,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resources :clusters, except: [:edit, :create] do
collection do
- scope :providers do
- get '/user/new', to: 'clusters/user#new'
- post '/user', to: 'clusters/user#create'
-
- get '/gcp/new', to: 'clusters/gcp#new'
- get '/gcp/login', to: 'clusters/gcp#login'
- post '/gcp', to: 'clusters/gcp#create'
- end
+ post :create_gcp
+ post :create_user
end
member do
@@ -235,6 +229,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
collection do
+ get :metrics, action: :metrics_redirect
get :folder, path: 'folders/*id', constraints: { format: /(html|json)/ }
end
@@ -284,6 +279,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
post :erase
get :trace, defaults: { format: 'json' }
get :raw
+ get :terminal
+ get '/terminal.ws/authorize', to: 'jobs#terminal_websocket_authorize', constraints: { format: nil }
end
resource :artifacts, only: [] do
@@ -353,6 +350,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
+ get :issues, to: 'issues#calendar', constraints: lambda { |req| req.format == :ics }
resources :issues, concerns: :awardable, constraints: { id: /\d+/ } do
member do
post :toggle_subscription
@@ -405,6 +403,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resources :uploads, only: [:create] do
collection do
get ":secret/:filename", action: :show, as: :show, constraints: { filename: %r{[^/]+} }
+ post :authorize
end
end
diff --git a/config/routes/uploads.rb b/config/routes/uploads.rb
index 6370645bcb9..6becadd57ae 100644
--- a/config/routes/uploads.rb
+++ b/config/routes/uploads.rb
@@ -17,7 +17,7 @@ scope path: :uploads do
# Appearance
get "-/system/:model/:mounted_as/:id/:filename",
to: "uploads#show",
- constraints: { model: /appearance/, mounted_as: /logo|header_logo/, filename: /.+/ }
+ constraints: { model: /appearance/, mounted_as: /logo|header_logo|favicon/, filename: /.+/ }
# Project markdown uploads
get ":namespace_id/:project_id/:secret/:filename",
diff --git a/config/routes/wiki.rb b/config/routes/wiki.rb
index c2da84ff6f2..cd3828b743c 100644
--- a/config/routes/wiki.rb
+++ b/config/routes/wiki.rb
@@ -6,7 +6,7 @@ scope(controller: :wikis) do
post '/', to: 'wikis#create'
end
- scope(path: 'wikis/*id', as: :wiki, format: false) do
+ scope(path: 'wikis/*id', as: :wiki, format: false, defaults: { format: :html }) do
get :edit
get :history
post :preview_markdown
diff --git a/config/settings.rb b/config/settings.rb
index 69d637761ea..3f3481bb65d 100644
--- a/config/settings.rb
+++ b/config/settings.rb
@@ -85,6 +85,24 @@ class Settings < Settingslogic
File.expand_path(path, Rails.root)
end
+ # Ruby 2.4+ requires passing in the exact required length for OpenSSL keys
+ # (https://github.com/ruby/ruby/commit/ce635262f53b760284d56bb1027baebaaec175d1).
+ # Previous versions quietly truncated the input.
+ #
+ # Use this when using :per_attribute_iv mode for attr_encrypted.
+ # We have to truncate the string to 32 bytes for a 256-bit cipher.
+ def attr_encrypted_db_key_base_truncated
+ Gitlab::Application.secrets.db_key_base[0..31]
+ end
+
+ # This should be used for :per_attribute_salt_and_iv mode. There is no
+ # need to truncate the key because the encryptor will use the salt to
+ # generate a hash of the password:
+ # https://github.com/attr-encrypted/encryptor/blob/c3a62c4a9e74686dd95e0548f9dc2a361fdc95d1/lib/encryptor.rb#L77
+ def attr_encrypted_db_key_base
+ Gitlab::Application.secrets.db_key_base
+ end
+
private
def base_url(config)
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index e1e8f36b663..3400142db36 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -75,4 +75,6 @@
- [pipeline_background, 1]
- [repository_update_remote_mirror, 1]
- [repository_remove_remote, 1]
+ - [create_note_diff_file, 1]
+ - [delete_diff_files, 1]
diff --git a/config/unicorn.rb.example b/config/unicorn.rb.example
index cc10da2bd88..220a0191160 100644
--- a/config/unicorn.rb.example
+++ b/config/unicorn.rb.example
@@ -81,6 +81,17 @@ GC.respond_to?(:copy_on_write_friendly=) and
# fast LAN.
check_client_connection false
+before_exec do |server|
+ # The following is necessary to ensure stale Prometheus metrics don't
+ # accumulate over time. It needs to be done in this hook as opposed to
+ # inside an init script to ensure metrics files aren't deleted after new
+ # unicorn workers start after a SIGUSR2 is received.
+ if ENV['prometheus_multiproc_dir']
+ old_metrics = Dir[File.join(ENV['prometheus_multiproc_dir'], '*.db')]
+ FileUtils.rm_rf(old_metrics)
+ end
+end
+
before_fork do |server, worker|
# the following is highly recommended for Rails + "preload_app true"
# as there's no need for the master process to hold a connection
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 27050e7069d..583f05f2fb7 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -1,11 +1,10 @@
-const fs = require('fs');
const path = require('path');
const glob = require('glob');
const webpack = require('webpack');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const StatsWriterPlugin = require('webpack-stats-plugin').StatsWriterPlugin;
-const CopyWebpackPlugin = require('copy-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
+const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const ROOT_PATH = path.resolve(__dirname, '..');
@@ -17,10 +16,13 @@ const DEV_SERVER_PORT = parseInt(process.env.DEV_SERVER_PORT, 10) || 3808;
const DEV_SERVER_LIVERELOAD = IS_DEV_SERVER && process.env.DEV_SERVER_LIVERELOAD !== 'false';
const WEBPACK_REPORT = process.env.WEBPACK_REPORT;
const NO_COMPRESSION = process.env.NO_COMPRESSION;
+const NO_SOURCEMAPS = process.env.NO_SOURCEMAPS;
const VUE_VERSION = require('vue/package.json').version;
const VUE_LOADER_VERSION = require('vue-loader/package.json').version;
+const devtool = IS_PRODUCTION ? 'source-map' : 'cheap-module-eval-source-map';
+
let autoEntriesCount = 0;
let watchAutoEntries = [];
const defaultEntries = ['./main'];
@@ -168,19 +170,10 @@ module.exports = {
name: '[name].[hash:8].[ext]',
},
},
- {
- test: /monaco-editor\/\w+\/vs\/loader\.js$/,
- use: [
- { loader: 'exports-loader', options: 'l.global' },
- { loader: 'imports-loader', options: 'l=>{},this=>l,AMDLoader=>this,module=>undefined' },
- ],
- },
],
- noParse: [/monaco-editor\/\w+\/vs\//],
},
optimization: {
- nodeEnv: false,
runtimeChunk: 'single',
splitChunks: {
maxInitialRequests: 4,
@@ -226,6 +219,9 @@ module.exports = {
// enable vue-loader to use existing loader rules for other module types
new VueLoaderPlugin(),
+ // automatically configure monaco editor web workers
+ new MonacoWebpackPlugin(),
+
// prevent pikaday from including moment.js
new webpack.IgnorePlugin(/moment/, /pikaday/),
@@ -235,29 +231,6 @@ module.exports = {
jQuery: 'jquery',
}),
- // copy pre-compiled vendor libraries verbatim
- new CopyWebpackPlugin([
- {
- 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) && !/typescript/i.test(path)) {
- return (
- '(function(){\n' +
- 'var define = this.define, require = this.require;\n' +
- 'window.define = define; window.require = require;\n' +
- content +
- '\n}.call(window.__monaco_context__ || (window.__monaco_context__ = {})));'
- );
- }
- return content;
- },
- },
- ]),
-
// compression can require a lot of compute time and is disabled in CI
IS_PRODUCTION && !NO_COMPRESSION && new CompressionPlugin(),
@@ -306,13 +279,16 @@ module.exports = {
host: DEV_SERVER_HOST,
port: DEV_SERVER_PORT,
disableHostCheck: true,
- headers: { 'Access-Control-Allow-Origin': '*' },
+ headers: {
+ 'Access-Control-Allow-Origin': '*',
+ 'Access-Control-Allow-Headers': '*',
+ },
stats: 'errors-only',
hot: DEV_SERVER_LIVERELOAD,
inline: DEV_SERVER_LIVERELOAD,
},
- devtool: IS_PRODUCTION ? 'source-map' : 'cheap-module-eval-source-map',
+ devtool: NO_SOURCEMAPS ? false : devtool,
// sqljs requires fs
node: { fs: 'empty' },
diff --git a/danger/changelog/Dangerfile b/danger/changelog/Dangerfile
new file mode 100644
index 00000000000..76ebe57cf8d
--- /dev/null
+++ b/danger/changelog/Dangerfile
@@ -0,0 +1,66 @@
+# rubocop:disable Style/SignalException
+
+require 'yaml'
+
+NO_CHANGELOG_LABELS = %w[backstage QA test].freeze
+SEE_DOC = "See [the documentation](https://docs.gitlab.com/ce/development/changelog.html).".freeze
+MISSING_CHANGELOG_MESSAGE = <<~MSG.freeze
+**[CHANGELOG missing](https://docs.gitlab.com/ce/development/changelog.html).**
+
+You can create one with:
+
+```
+bin/changelog -m %<mr_iid>s
+```
+
+If your merge request doesn't warrant a CHANGELOG entry,
+consider adding any of the %<labels>s labels.
+#{SEE_DOC}
+MSG
+
+def ee?
+ ENV['CI_PROJECT_NAME'] == 'gitlab-ee' || File.exist?('../../CHANGELOG-EE.md')
+end
+
+def ee_changelog?(changelog_path)
+ changelog_path =~ /unreleased-ee/
+end
+
+def ce_port_changelog?(changelog_path)
+ ee? && !ee_changelog?(changelog_path)
+end
+
+def check_changelog(path)
+ yaml = YAML.safe_load(File.read(path))
+
+ fail "`title` should be set, in #{gitlab.html_link(path)}! #{SEE_DOC}" if yaml["title"].nil?
+ fail "`type` should be set, in #{gitlab.html_link(path)}! #{SEE_DOC}" if yaml["type"].nil?
+
+ if yaml["merge_request"].nil?
+ message "Consider setting `merge_request` to #{gitlab.mr_json["iid"]} in #{gitlab.html_link(path)}. #{SEE_DOC}"
+ elsif yaml["merge_request"] != gitlab.mr_json["iid"] && !ce_port_changelog?(changelog_path)
+ fail "Merge request ID was not set to #{gitlab.mr_json["iid"]}! #{SEE_DOC}"
+ end
+rescue StandardError
+ # YAML could not be parsed, fail the build.
+ fail "#{gitlab.html_link(path)} isn't valid YAML! #{SEE_DOC}"
+end
+
+def presented_no_changelog_labels
+ NO_CHANGELOG_LABELS.map { |label| "~#{label}" }.join(', ')
+end
+
+changelog_needed = (gitlab.mr_labels & NO_CHANGELOG_LABELS).empty?
+changelog_found = git.added_files.find { |path| path =~ %r{\A(ee/)?(changelogs/unreleased)(-ee)?/} }
+
+if git.modified_files.include?("CHANGELOG.md")
+ fail "CHANGELOG.md was edited. Please remove the additions and create an entry with `bin/changelog -m #{gitlab.mr_json["iid"]}` instead."
+end
+
+if changelog_needed
+ if changelog_found
+ check_changelog(path)
+ else
+ warn format(MISSING_CHANGELOG_MESSAGE, mr_iid: gitlab.mr_json["iid"], labels: presented_no_changelog_labels)
+ end
+end
diff --git a/danger/changes_size/Dangerfile b/danger/changes_size/Dangerfile
new file mode 100644
index 00000000000..8251d0d5cbf
--- /dev/null
+++ b/danger/changes_size/Dangerfile
@@ -0,0 +1,17 @@
+# FIXME: git.info_for_file raises the following error
+# /usr/local/bundle/gems/git-1.4.0/lib/git/lib.rb:956:in `command': (Danger::DSLError)
+# [!] Invalid `Dangerfile` file:
+# [!] Invalid `Dangerfile` file: git '--git-dir=/builds/gitlab-org/gitlab-ce/.git' '--work-tree=/builds/gitlab-org/gitlab-ce' cat-file '-t' '' 2>&1:fatal: Not a valid object name
+# This seems to be the same as https://github.com/danger/danger/issues/535.
+
+# locale_files_updated = git.modified_files.select { |path| path.start_with?('locale') }
+# locale_files_updated.each do |locale_file_updated|
+# git_stats = git.info_for_file(locale_file_updated)
+# message "Git stats for #{locale_file_updated}: #{git_stats[:insertions]} insertions, #{git_stats[:deletions]} insertions"
+# end
+
+if git.lines_of_code > 2_000
+ warn "This merge request is definitely too big (more than #{git.lines_of_code} lines changed), please split it into multiple merge requests."
+elsif git.lines_of_code > 500
+ warn "This merge request is quite big (more than #{git.lines_of_code} lines changed), please consider splitting it into multiple merge requests."
+end
diff --git a/danger/database/Dangerfile b/danger/database/Dangerfile
new file mode 100644
index 00000000000..6f48994945a
--- /dev/null
+++ b/danger/database/Dangerfile
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+# All the files/directories that should be reviewed by the DB team.
+DB_FILES = [
+ 'db/',
+ 'app/models/project_authorization.rb',
+ 'app/services/users/refresh_authorized_projects_service.rb',
+ 'lib/gitlab/background_migration.rb',
+ 'lib/gitlab/background_migration/',
+ 'lib/gitlab/database.rb',
+ 'lib/gitlab/database/',
+ 'lib/gitlab/github_import.rb',
+ 'lib/gitlab/github_import/',
+ 'lib/gitlab/sql/',
+ 'rubocop/cop/migration',
+ 'ee/db/',
+ 'ee/lib/gitlab/database/'
+].freeze
+
+SCHEMA_NOT_UPDATED_MESSAGE = <<~MSG
+**New %<migrations>s added but %<schema>s wasn't updated.**
+
+Usually, when adding new %<migrations>s, %<schema>s should be
+updated too (unless the migration isn't changing the DB schema
+and isn't the most recent one).
+MSG
+
+def database_paths_requiring_review(files)
+ to_review = []
+
+ files.each do |file|
+ review = DB_FILES.any? do |pattern|
+ file.start_with?(pattern)
+ end
+
+ to_review << file if review
+ end
+
+ to_review
+end
+
+all_files = git.added_files + git.modified_files
+
+non_geo_db_schema_updated = !git.modified_files.grep(%r{\Adb/schema\.rb/}).empty?
+geo_db_schema_updated = !git.modified_files.grep(%r{\Aee/db/geo/schema\.rb/}).empty?
+
+non_geo_migration_created = !git.added_files.grep(%r{\A(db/(post_)?migrate)/}).empty?
+geo_migration_created = !git.added_files.grep(%r{\Aee/db/geo/(post_)?migrate/}).empty?
+
+if non_geo_migration_created && !non_geo_db_schema_updated
+ warn format(SCHEMA_NOT_UPDATED_MESSAGE, migrations: 'migrations', schema: gitlab.html_link("db/schema.rb"))
+end
+
+if geo_migration_created && !geo_db_schema_updated
+ warn format(SCHEMA_NOT_UPDATED_MESSAGE, migrations: 'Geo migrations', schema: gitlab.html_link("ee/db/geo/schema.rb"))
+end
+
+db_paths_to_review = database_paths_requiring_review(all_files)
+
+unless db_paths_to_review.empty?
+ message 'This merge request adds or changes files that require a ' \
+ 'review from the Database team.'
+
+ markdown(<<~MARKDOWN.strip)
+## Database Review
+
+The following files require a review from the Database team:
+
+* #{db_paths_to_review.map { |path| "`#{path}`" }.join("\n* ")}
+
+To make sure these changes are reviewed, take the following steps:
+
+1. Edit your merge request, and add `gl-database` to the list of Group
+ approvers.
+1. Mention `@gl-database` in a separate comment, and explain what needs to be
+ reviewed by the team. Please don't mention the team until your changes are
+ ready for review.
+ MARKDOWN
+
+ unless gitlab.mr_labels.include?('database')
+ warn 'This merge request is missing the ~database label.'
+ end
+end
diff --git a/danger/gemfile/Dangerfile b/danger/gemfile/Dangerfile
new file mode 100644
index 00000000000..8ef4a464fe4
--- /dev/null
+++ b/danger/gemfile/Dangerfile
@@ -0,0 +1,24 @@
+GEMFILE_LOCK_NOT_UPDATED_MESSAGE = <<~MSG.freeze
+**%<gemfile>s was updated but %<gemfile_lock>s wasn't updated.**
+
+Usually, when %<gemfile>s is updated, you should run
+```
+bundle install && \
+ BUNDLE_GEMFILE=Gemfile.rails5 bundle install
+```
+
+or
+
+```
+bundle update <the-added-or-updated-gem>
+```
+
+and commit the %<gemfile_lock>s changes.
+MSG
+
+gemfile_modified = git.modified_files.include?("Gemfile")
+gemfile_lock_modified = git.modified_files.include?("Gemfile.lock")
+
+if gemfile_modified && !gemfile_lock_modified
+ warn format(GEMFILE_LOCK_NOT_UPDATED_MESSAGE, gemfile: gitlab.html_link("Gemfile"), gemfile_lock: gitlab.html_link("Gemfile.lock"))
+end
diff --git a/danger/metadata/Dangerfile b/danger/metadata/Dangerfile
new file mode 100644
index 00000000000..3cfaa04e01b
--- /dev/null
+++ b/danger/metadata/Dangerfile
@@ -0,0 +1,25 @@
+# rubocop:disable Style/SignalException
+
+if gitlab.mr_body.size < 5
+ fail "Please provide a proper merge request description."
+end
+
+if gitlab.mr_labels.empty?
+ fail "Please add labels to this merge request."
+end
+
+unless gitlab.mr_json["assignee"]
+ warn "This merge request does not have any assignee yet. Setting an assignee clarifies who needs to take action on the merge request at any given time."
+end
+
+has_milestone = !gitlab.mr_json["milestone"].nil?
+
+unless has_milestone
+ warn "This merge request does not refer to an existing milestone.", sticky: false
+end
+
+has_pick_into_stable_label = gitlab.mr_labels.find { |label| label.start_with?('Pick into') }
+
+if gitlab.branch_for_base != "master" && !has_pick_into_stable_label
+ warn "Most of the time, all merge requests should target `master`. Otherwise, please set the relevant `Pick into X.Y` label."
+end
diff --git a/danger/specs/Dangerfile b/danger/specs/Dangerfile
new file mode 100644
index 00000000000..934ea0beadb
--- /dev/null
+++ b/danger/specs/Dangerfile
@@ -0,0 +1,12 @@
+NO_NEW_SPEC_MESSAGE = <<~MSG.freeze
+You've made some app changes, but didn't add any tests.
+That's OK as long as you're refactoring existing code,
+but please consider adding the ~backstage label in that case.
+MSG
+
+has_app_changes = !git.modified_files.grep(%r{\A(ee/)?(app|lib|db/(geo/)?(post_)?migrate)/}).empty?
+has_spec_changes = !git.modified_files.grep(/spec/).empty?
+
+if has_app_changes && !has_spec_changes
+ warn NO_NEW_SPEC_MESSAGE, sticky: false
+end
diff --git a/db/fixtures/development/04_project.rb b/db/fixtures/development/04_project.rb
index 213c8bca639..51e69879c79 100644
--- a/db/fixtures/development/04_project.rb
+++ b/db/fixtures/development/04_project.rb
@@ -67,6 +67,10 @@ Sidekiq::Testing.inline! do
skip_disk_validation: true
}
+ if i % 2 == 0
+ params[:storage_version] = Project::LATEST_STORAGE_VERSION
+ end
+
project = Projects::CreateService.new(User.first, params).execute
# Seed-Fu runs this entire fixture in a transaction, so the `after_commit`
# hook won't run until after the fixture is loaded. That is too late
diff --git a/db/fixtures/development/08_settings.rb b/db/fixtures/development/08_settings.rb
new file mode 100644
index 00000000000..141465c06cf
--- /dev/null
+++ b/db/fixtures/development/08_settings.rb
@@ -0,0 +1,7 @@
+# We want to enable hashed storage for every new project in development
+# Details https://gitlab.com/gitlab-org/gitlab-ce/issues/46241
+Gitlab::Seeder.quiet do
+ ApplicationSetting.create_from_defaults unless ApplicationSetting.current_without_cache
+ ApplicationSetting.current_without_cache.update!(hashed_storage_enabled: true)
+ print '.'
+end
diff --git a/db/fixtures/development/12_snippets.rb b/db/fixtures/development/12_snippets.rb
index 4f3bdba043d..a9f4069a0f8 100644
--- a/db/fixtures/development/12_snippets.rb
+++ b/db/fixtures/development/12_snippets.rb
@@ -17,7 +17,7 @@ class Member < ActiveRecord::Base
scope :guests, -> { where(access_level: GUEST) }
scope :reporters, -> { where(access_level: REPORTER) }
scope :developers, -> { where(access_level: DEVELOPER) }
- scope :masters, -> { where(access_level: MASTER) }
+ scope :maintainers, -> { where(access_level: MAINTAINER) }
scope :owners, -> { where(access_level: OWNER) }
delegate :name, :username, :email, to: :user, prefix: true
diff --git a/db/fixtures/development/19_environments.rb b/db/fixtures/development/19_environments.rb
index 00a14f458d1..65089f6ba4e 100644
--- a/db/fixtures/development/19_environments.rb
+++ b/db/fixtures/development/19_environments.rb
@@ -30,14 +30,14 @@ class Gitlab::Seeder::Environments
def create_merge_request_review_deployments!
@project
.merge_requests
- .select { |mr| mr.source_branch.match(/\p{Alnum}+/) }
+ .select { |mr| mr.source_branch.match(/[^a-zA-Z0-9]+/) }
.sample(4)
.each do |merge_request|
next unless merge_request.diff_head_sha
create_deployment!(
merge_request.source_project,
- "review/#{merge_request.source_branch.gsub(/[^a-zA-Z0-9]/, '')}",
+ "review/#{merge_request.source_branch.gsub(/[^a-zA-Z0-9]+/, '')}",
merge_request.source_branch,
merge_request.diff_head_sha
)
diff --git a/db/fixtures/development/20_nested_groups.rb b/db/fixtures/development/20_nested_groups.rb
index 2bc78e120a5..3d95e243f8a 100644
--- a/db/fixtures/development/20_nested_groups.rb
+++ b/db/fixtures/development/20_nested_groups.rb
@@ -1,30 +1,5 @@
require './spec/support/sidekiq'
-def create_group_with_parents(user, full_path)
- parent_path = nil
- group = nil
-
- until full_path.blank?
- path, _, full_path = full_path.partition('/')
-
- if parent_path
- parent = Group.find_by_full_path(parent_path)
-
- parent_path += '/'
- parent_path += path
-
- group = Groups::CreateService.new(user, path: path, parent_id: parent.id).execute
- else
- parent_path = path
-
- group = Group.find_by_full_path(parent_path) ||
- Groups::CreateService.new(user, path: path).execute
- end
- end
-
- group
-end
-
Sidekiq::Testing.inline! do
Gitlab::Seeder.quiet do
flag = 'SEED_NESTED_GROUPS'
@@ -48,7 +23,8 @@ Sidekiq::Testing.inline! do
full_path = url.sub('https://android.googlesource.com/', '')
full_path = full_path.sub(/\.git\z/, '')
full_path, _, project_path = full_path.rpartition('/')
- group = Group.find_by_full_path(full_path) || create_group_with_parents(user, full_path)
+ group = Group.find_by_full_path(full_path) ||
+ Groups::NestedCreateService.new(user, group_path: full_path).execute
params = {
import_url: url,
diff --git a/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb b/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb
index 375e389e07a..7aa79bf5e02 100644
--- a/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb
+++ b/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb
@@ -37,7 +37,12 @@ class AddTrigramIndexesForSearching < ActiveRecord::Migration
res = execute("SELECT true AS enabled FROM pg_available_extensions WHERE name = 'pg_trgm' AND installed_version IS NOT NULL;")
row = res.first
- row && row['enabled'] == 't' ? true : false
+ check = if Gitlab.rails5?
+ true
+ else
+ 't'
+ end
+ row && row['enabled'] == check ? true : false
end
def create_trigrams_extension
diff --git a/db/migrate/20160302152808_remove_wrong_import_url_from_projects.rb b/db/migrate/20160302152808_remove_wrong_import_url_from_projects.rb
index 611767ac7fe..95105118764 100644
--- a/db/migrate/20160302152808_remove_wrong_import_url_from_projects.rb
+++ b/db/migrate/20160302152808_remove_wrong_import_url_from_projects.rb
@@ -8,7 +8,7 @@ class RemoveWrongImportUrlFromProjects < ActiveRecord::Migration
extend AttrEncrypted
attr_accessor :credentials
attr_encrypted :credentials,
- key: Gitlab::Application.secrets.db_key_base,
+ key: Settings.attr_encrypted_db_key_base,
marshal: true,
encode: true,
:mode => :per_attribute_iv_and_salt,
diff --git a/db/migrate/20160705054938_add_protected_branches_push_access.rb b/db/migrate/20160705054938_add_protected_branches_push_access.rb
index 97aaaf9d2c8..de3aefcb1fb 100644
--- a/db/migrate/20160705054938_add_protected_branches_push_access.rb
+++ b/db/migrate/20160705054938_add_protected_branches_push_access.rb
@@ -9,7 +9,7 @@ class AddProtectedBranchesPushAccess < ActiveRecord::Migration
create_table :protected_branch_push_access_levels do |t|
t.references :protected_branch, index: { name: "index_protected_branch_push_access" }, foreign_key: true, null: false
- # Gitlab::Access::MASTER == 40
+ # Gitlab::Access::MAINTAINER == 40
t.integer :access_level, default: 40, null: false
t.timestamps null: false
diff --git a/db/migrate/20160705054952_add_protected_branches_merge_access.rb b/db/migrate/20160705054952_add_protected_branches_merge_access.rb
index 51a52a5ac17..9b18a2061b3 100644
--- a/db/migrate/20160705054952_add_protected_branches_merge_access.rb
+++ b/db/migrate/20160705054952_add_protected_branches_merge_access.rb
@@ -9,7 +9,7 @@ class AddProtectedBranchesMergeAccess < ActiveRecord::Migration
create_table :protected_branch_merge_access_levels do |t|
t.references :protected_branch, index: { name: "index_protected_branch_merge_access" }, foreign_key: true, null: false
- # Gitlab::Access::MASTER == 40
+ # Gitlab::Access::MAINTAINER == 40
t.integer :access_level, default: 40, null: false
t.timestamps null: false
diff --git a/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb b/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb
index a96ea7d9db4..dc16d5c5169 100644
--- a/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb
+++ b/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb
@@ -12,7 +12,9 @@ class MigrateProcessCommitWorkerJobs < ActiveRecord::Migration
end
def repository_storage_path
- Gitlab.config.repositories.storages[repository_storage].legacy_disk_path
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ Gitlab.config.repositories.storages[repository_storage].legacy_disk_path
+ end
end
def repository_path
diff --git a/db/migrate/20161207231620_fixup_environment_name_uniqueness.rb b/db/migrate/20161207231620_fixup_environment_name_uniqueness.rb
index f73e4f6c99b..1db8c68626a 100644
--- a/db/migrate/20161207231620_fixup_environment_name_uniqueness.rb
+++ b/db/migrate/20161207231620_fixup_environment_name_uniqueness.rb
@@ -1,4 +1,5 @@
class FixupEnvironmentNameUniqueness < ActiveRecord::Migration
+ include Gitlab::Database::ArelMethods
include Gitlab::Database::MigrationHelpers
DOWNTIME = true
@@ -41,7 +42,7 @@ class FixupEnvironmentNameUniqueness < ActiveRecord::Migration
conflicts.each do |id, name|
update_sql =
- Arel::UpdateManager.new(ActiveRecord::Base)
+ arel_update_manager
.table(environments)
.set(environments[:name] => name + "-" + id.to_s)
.where(environments[:id].eq(id))
diff --git a/db/migrate/20161207231626_add_environment_slug.rb b/db/migrate/20161207231626_add_environment_slug.rb
index 83cdd484c4c..162f82a01cb 100644
--- a/db/migrate/20161207231626_add_environment_slug.rb
+++ b/db/migrate/20161207231626_add_environment_slug.rb
@@ -2,6 +2,7 @@
# for more information on how to write migrations for GitLab.
class AddEnvironmentSlug < ActiveRecord::Migration
+ include Gitlab::Database::ArelMethods
include Gitlab::Database::MigrationHelpers
DOWNTIME = true
@@ -19,7 +20,7 @@ class AddEnvironmentSlug < ActiveRecord::Migration
finder = environments.project(:id, :name)
connection.exec_query(finder.to_sql).rows.each do |id, name|
- updater = Arel::UpdateManager.new(ActiveRecord::Base)
+ updater = arel_update_manager
.table(environments)
.set(environments[:slug] => generate_slug(name))
.where(environments[:id].eq(id))
diff --git a/db/migrate/20161226122833_remove_dot_git_from_usernames.rb b/db/migrate/20161226122833_remove_dot_git_from_usernames.rb
index 8986cd8cb4b..133435523e1 100644
--- a/db/migrate/20161226122833_remove_dot_git_from_usernames.rb
+++ b/db/migrate/20161226122833_remove_dot_git_from_usernames.rb
@@ -64,7 +64,9 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration
# we rename suffix instead of removing it
path = path.sub(/\.git\z/, '_git')
- check_routes(path.dup, 0, path)
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ check_routes(path.dup, 0, path)
+ end
end
def check_routes(base, counter, path)
diff --git a/db/migrate/20170622135728_add_unique_constraint_to_ci_variables.rb b/db/migrate/20170622135728_add_unique_constraint_to_ci_variables.rb
index 8b2cc40ee59..787022b7bfe 100644
--- a/db/migrate/20170622135728_add_unique_constraint_to_ci_variables.rb
+++ b/db/migrate/20170622135728_add_unique_constraint_to_ci_variables.rb
@@ -2,12 +2,13 @@ class AddUniqueConstraintToCiVariables < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
+ INDEX_NAME = 'index_ci_variables_on_project_id_and_key_and_environment_scope'
disable_ddl_transaction!
def up
unless this_index_exists?
- add_concurrent_index(:ci_variables, columns, name: index_name, unique: true)
+ add_concurrent_index(:ci_variables, columns, name: INDEX_NAME, unique: true)
end
end
@@ -18,21 +19,17 @@ class AddUniqueConstraintToCiVariables < ActiveRecord::Migration
add_concurrent_index(:ci_variables, :project_id)
end
- remove_concurrent_index(:ci_variables, columns, name: index_name)
+ remove_concurrent_index(:ci_variables, columns, name: INDEX_NAME)
end
end
private
def this_index_exists?
- index_exists?(:ci_variables, columns, name: index_name)
+ index_exists?(:ci_variables, columns, name: INDEX_NAME)
end
def columns
@columns ||= [:project_id, :key, :environment_scope]
end
-
- def index_name
- 'index_ci_variables_on_project_id_and_key_and_environment_scope'
- end
end
diff --git a/db/migrate/20170925184228_add_favicon_to_appearances.rb b/db/migrate/20170925184228_add_favicon_to_appearances.rb
new file mode 100644
index 00000000000..65083733afb
--- /dev/null
+++ b/db/migrate/20170925184228_add_favicon_to_appearances.rb
@@ -0,0 +1,7 @@
+class AddFaviconToAppearances < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def change
+ add_column :appearances, :favicon, :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
index e4bed778695..08784de4043 100644
--- 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
@@ -20,9 +20,7 @@ class TurnIssuesDueDateIndexToPartialIndex < ActiveRecord::Migration
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)
+ remove_concurrent_index_by_name(:issues, OLD_INDEX_NAME)
end
def down
@@ -32,6 +30,6 @@ class TurnIssuesDueDateIndexToPartialIndex < ActiveRecord::Migration
name: OLD_INDEX_NAME
)
- remove_concurrent_index(:issues, nil, name: NEW_INDEX_NAME)
+ remove_concurrent_index_by_name(:issues, NEW_INDEX_NAME)
end
end
diff --git a/db/migrate/20180201110056_add_foreign_keys_to_todos.rb b/db/migrate/20180201110056_add_foreign_keys_to_todos.rb
index b7c40f8c01a..020b0550321 100644
--- a/db/migrate/20180201110056_add_foreign_keys_to_todos.rb
+++ b/db/migrate/20180201110056_add_foreign_keys_to_todos.rb
@@ -31,7 +31,7 @@ class AddForeignKeysToTodos < ActiveRecord::Migration
end
def down
- remove_foreign_key :todos, :users
+ remove_foreign_key :todos, column: :user_id
remove_foreign_key :todos, column: :author_id
remove_foreign_key :todos, :notes
end
diff --git a/db/migrate/20180408143354_rename_users_rss_token_to_feed_token.rb b/db/migrate/20180408143354_rename_users_rss_token_to_feed_token.rb
new file mode 100644
index 00000000000..007cbebaf1b
--- /dev/null
+++ b/db/migrate/20180408143354_rename_users_rss_token_to_feed_token.rb
@@ -0,0 +1,15 @@
+class RenameUsersRssTokenToFeedToken < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ rename_column_concurrently :users, :rss_token, :feed_token
+ end
+
+ def down
+ cleanup_concurrent_column_rename :users, :feed_token, :rss_token
+ end
+end
diff --git a/db/migrate/20180424160449_add_pipeline_iid_to_ci_pipelines.rb b/db/migrate/20180424160449_add_pipeline_iid_to_ci_pipelines.rb
new file mode 100644
index 00000000000..e8f0c91d612
--- /dev/null
+++ b/db/migrate/20180424160449_add_pipeline_iid_to_ci_pipelines.rb
@@ -0,0 +1,13 @@
+class AddPipelineIidToCiPipelines < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ add_column :ci_pipelines, :iid, :integer
+ end
+
+ def down
+ remove_column :ci_pipelines, :iid, :integer
+ end
+end
diff --git a/db/migrate/20180425205249_add_index_constraints_to_pipeline_iid.rb b/db/migrate/20180425205249_add_index_constraints_to_pipeline_iid.rb
new file mode 100644
index 00000000000..3fa59b44d5d
--- /dev/null
+++ b/db/migrate/20180425205249_add_index_constraints_to_pipeline_iid.rb
@@ -0,0 +1,15 @@
+class AddIndexConstraintsToPipelineIid < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :ci_pipelines, [:project_id, :iid], unique: true, where: 'iid IS NOT NULL'
+ end
+
+ def down
+ remove_concurrent_index :ci_pipelines, [:project_id, :iid]
+ end
+end
diff --git a/db/migrate/20180511131058_create_clusters_applications_jupyter.rb b/db/migrate/20180511131058_create_clusters_applications_jupyter.rb
new file mode 100644
index 00000000000..f3923884e37
--- /dev/null
+++ b/db/migrate/20180511131058_create_clusters_applications_jupyter.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 CreateClustersApplicationsJupyter < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :clusters_applications_jupyter do |t|
+ t.references :cluster, null: false, unique: true, foreign_key: { on_delete: :cascade }
+ t.references :oauth_application, foreign_key: { on_delete: :nullify }
+
+ t.integer :status, null: false
+ t.string :version, null: false
+ t.string :hostname
+
+ t.timestamps_with_timezone null: false
+
+ t.text :status_reason
+ end
+ end
+end
diff --git a/db/migrate/20180515005612_add_squash_to_merge_requests.rb b/db/migrate/20180515005612_add_squash_to_merge_requests.rb
new file mode 100644
index 00000000000..f526b45bd4b
--- /dev/null
+++ b/db/migrate/20180515005612_add_squash_to_merge_requests.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 AddSquashToMergeRequests < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+
+ def up
+ unless column_exists?(:merge_requests, :squash)
+ add_column_with_default :merge_requests, :squash, :boolean, default: false, allow_null: false
+ end
+ end
+
+ def down
+ remove_column :merge_requests, :squash if column_exists?(:merge_requests, :squash)
+ end
+end
diff --git a/db/migrate/20180515121227_create_notes_diff_files.rb b/db/migrate/20180515121227_create_notes_diff_files.rb
new file mode 100644
index 00000000000..7108bc1a64b
--- /dev/null
+++ b/db/migrate/20180515121227_create_notes_diff_files.rb
@@ -0,0 +1,21 @@
+class CreateNotesDiffFiles < ActiveRecord::Migration
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def change
+ create_table :note_diff_files do |t|
+ t.references :diff_note, references: :notes, null: false, index: { unique: true }
+ t.text :diff, null: false
+ t.boolean :new_file, null: false
+ t.boolean :renamed_file, null: false
+ t.boolean :deleted_file, null: false
+ t.string :a_mode, null: false
+ t.string :b_mode, null: false
+ t.text :new_path, null: false
+ t.text :old_path, null: false
+ end
+
+ add_foreign_key :note_diff_files, :notes, column: :diff_note_id, on_delete: :cascade
+ end
+end
diff --git a/db/migrate/20180523042841_rename_merge_requests_allow_maintainer_to_push.rb b/db/migrate/20180523042841_rename_merge_requests_allow_maintainer_to_push.rb
new file mode 100644
index 00000000000..41bc7b71694
--- /dev/null
+++ b/db/migrate/20180523042841_rename_merge_requests_allow_maintainer_to_push.rb
@@ -0,0 +1,17 @@
+class RenameMergeRequestsAllowMaintainerToPush < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ # NOOP
+ end
+
+ def down
+ if column_exists?(:merge_requests, :allow_collaboration)
+ cleanup_concurrent_column_rename :merge_requests, :allow_collaboration, :allow_maintainer_to_push
+ end
+ end
+end
diff --git a/db/migrate/20180524132016_merge_requests_target_id_iid_state_partial_index.rb b/db/migrate/20180524132016_merge_requests_target_id_iid_state_partial_index.rb
new file mode 100644
index 00000000000..cee576b91c8
--- /dev/null
+++ b/db/migrate/20180524132016_merge_requests_target_id_iid_state_partial_index.rb
@@ -0,0 +1,27 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class MergeRequestsTargetIdIidStatePartialIndex < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ INDEX_NAME = 'index_merge_requests_on_target_project_id_and_iid_opened'
+
+ disable_ddl_transaction!
+
+ def up
+ # On GitLab.com this index will take up roughly 5 MB of space.
+ add_concurrent_index(
+ :merge_requests,
+ [:target_project_id, :iid],
+ where: "state = 'opened'",
+ name: INDEX_NAME
+ )
+ end
+
+ def down
+ remove_concurrent_index_by_name(:merge_requests, INDEX_NAME)
+ end
+end
diff --git a/db/migrate/20180529093006_ensure_remote_mirror_columns.rb b/db/migrate/20180529093006_ensure_remote_mirror_columns.rb
new file mode 100644
index 00000000000..290416cb61c
--- /dev/null
+++ b/db/migrate/20180529093006_ensure_remote_mirror_columns.rb
@@ -0,0 +1,24 @@
+class EnsureRemoteMirrorColumns < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column :remote_mirrors, :last_update_started_at, :datetime unless column_exists?(:remote_mirrors, :last_update_started_at)
+ add_column :remote_mirrors, :remote_name, :string unless column_exists?(:remote_mirrors, :remote_name)
+
+ unless column_exists?(:remote_mirrors, :only_protected_branches)
+ add_column_with_default(:remote_mirrors,
+ :only_protected_branches,
+ :boolean,
+ default: false,
+ allow_null: false)
+ end
+ end
+
+ def down
+ # db/migrate/20180503131624_create_remote_mirrors.rb will remove the table
+ end
+end
diff --git a/db/migrate/20180530135500_add_index_to_stages_position.rb b/db/migrate/20180530135500_add_index_to_stages_position.rb
new file mode 100644
index 00000000000..61150f33a25
--- /dev/null
+++ b/db/migrate/20180530135500_add_index_to_stages_position.rb
@@ -0,0 +1,15 @@
+class AddIndexToStagesPosition < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :ci_stages, [:pipeline_id, :position]
+ end
+
+ def down
+ remove_concurrent_index :ci_stages, [:pipeline_id, :position]
+ end
+end
diff --git a/db/migrate/20180531220618_change_default_value_for_dsa_key_restriction.rb b/db/migrate/20180531220618_change_default_value_for_dsa_key_restriction.rb
new file mode 100644
index 00000000000..d0dcacc5b66
--- /dev/null
+++ b/db/migrate/20180531220618_change_default_value_for_dsa_key_restriction.rb
@@ -0,0 +1,16 @@
+class ChangeDefaultValueForDsaKeyRestriction < ActiveRecord::Migration
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def up
+ change_column :application_settings, :dsa_key_restriction, :integer, null: false,
+ default: -1
+
+ execute("UPDATE application_settings SET dsa_key_restriction = -1")
+ end
+
+ def down
+ change_column :application_settings, :dsa_key_restriction, :integer, null: false,
+ default: 0
+ end
+end
diff --git a/db/migrate/20180601213245_add_deploy_strategy_to_project_auto_devops.rb b/db/migrate/20180601213245_add_deploy_strategy_to_project_auto_devops.rb
new file mode 100644
index 00000000000..6f50d428965
--- /dev/null
+++ b/db/migrate/20180601213245_add_deploy_strategy_to_project_auto_devops.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 AddDeployStrategyToProjectAutoDevops < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default :project_auto_devops, :deploy_strategy, :integer, default: 0, allow_null: false
+ end
+
+ def down
+ remove_column :project_auto_devops, :deploy_strategy
+ end
+end
diff --git a/db/migrate/20180608110058_rename_merge_requests_allow_collaboration.rb b/db/migrate/20180608110058_rename_merge_requests_allow_collaboration.rb
new file mode 100644
index 00000000000..dcbbef9bd4a
--- /dev/null
+++ b/db/migrate/20180608110058_rename_merge_requests_allow_collaboration.rb
@@ -0,0 +1,21 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RenameMergeRequestsAllowCollaboration < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ if column_exists?(:merge_requests, :allow_collaboration)
+ rename_column_concurrently :merge_requests, :allow_collaboration, :allow_maintainer_to_push
+ end
+ end
+
+ def down
+ # NOOP
+ end
+end
diff --git a/db/migrate/20180613081317_create_ci_builds_runner_session.rb b/db/migrate/20180613081317_create_ci_builds_runner_session.rb
new file mode 100644
index 00000000000..e550c07b9ab
--- /dev/null
+++ b/db/migrate/20180613081317_create_ci_builds_runner_session.rb
@@ -0,0 +1,21 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class CreateCiBuildsRunnerSession < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def change
+ create_table :ci_builds_runner_session, id: :bigserial do |t|
+ t.integer :build_id, null: false
+ t.string :url, null: false
+ t.string :certificate
+ t.string :authorization
+
+ t.foreign_key :ci_builds, column: :build_id, on_delete: :cascade
+ t.index :build_id, unique: true
+ end
+ end
+end
diff --git a/db/migrate/20180625113853_create_import_export_uploads.rb b/db/migrate/20180625113853_create_import_export_uploads.rb
new file mode 100644
index 00000000000..be42304b0ae
--- /dev/null
+++ b/db/migrate/20180625113853_create_import_export_uploads.rb
@@ -0,0 +1,16 @@
+class CreateImportExportUploads < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def change
+ create_table :import_export_uploads do |t|
+ t.datetime_with_timezone :updated_at, null: false
+
+ t.references :project, index: true, foreign_key: { on_delete: :cascade }, unique: true
+
+ t.text :import_file
+ t.text :export_file
+ end
+
+ add_index :import_export_uploads, :updated_at
+ end
+end
diff --git a/db/migrate/20180626125654_add_index_on_deployable_for_deployments.rb b/db/migrate/20180626125654_add_index_on_deployable_for_deployments.rb
new file mode 100644
index 00000000000..a0e3a228f6c
--- /dev/null
+++ b/db/migrate/20180626125654_add_index_on_deployable_for_deployments.rb
@@ -0,0 +1,18 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddIndexOnDeployableForDeployments < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :deployments, [:deployable_type, :deployable_id]
+ end
+
+ def down
+ remove_concurrent_index :deployments, [:deployable_type, :deployable_id]
+ end
+end
diff --git a/db/migrate/20180628124813_alter_web_hook_logs_indexes.rb b/db/migrate/20180628124813_alter_web_hook_logs_indexes.rb
new file mode 100644
index 00000000000..1878e76811d
--- /dev/null
+++ b/db/migrate/20180628124813_alter_web_hook_logs_indexes.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AlterWebHookLogsIndexes < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ # "created_at" comes first so the Sidekiq worker pruning old webhook logs can
+ # use a composite index index.
+ #
+ # We leave the old standalone index on "web_hook_id" in place so future code
+ # that doesn't care about "created_at" can still use that index.
+ COLUMNS_TO_INDEX = %i[created_at web_hook_id]
+
+ def up
+ add_concurrent_index(:web_hook_logs, COLUMNS_TO_INDEX)
+ end
+
+ def down
+ remove_concurrent_index(:web_hook_logs, COLUMNS_TO_INDEX)
+ end
+end
diff --git a/db/migrate/20180704204006_add_hide_third_party_offers_to_application_settings.rb b/db/migrate/20180704204006_add_hide_third_party_offers_to_application_settings.rb
new file mode 100644
index 00000000000..6631c5d1b6c
--- /dev/null
+++ b/db/migrate/20180704204006_add_hide_third_party_offers_to_application_settings.rb
@@ -0,0 +1,18 @@
+class AddHideThirdPartyOffersToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default(:application_settings, :hide_third_party_offers,
+ :boolean,
+ default: false,
+ allow_null: false)
+ end
+
+ def down
+ remove_column(:application_settings, :hide_third_party_offers)
+ end
+end
diff --git a/db/migrate/merge_request_diff_file_limits_to_mysql.rb b/db/migrate/merge_request_diff_file_limits_to_mysql.rb
index 3958380e4b9..ca3bc7d6be9 100644
--- a/db/migrate/merge_request_diff_file_limits_to_mysql.rb
+++ b/db/migrate/merge_request_diff_file_limits_to_mysql.rb
@@ -4,7 +4,7 @@ class MergeRequestDiffFileLimitsToMysql < ActiveRecord::Migration
def up
return unless Gitlab::Database.mysql?
- change_column :merge_request_diff_files, :diff, :text, limit: 2147483647
+ change_column :merge_request_diff_files, :diff, :text, limit: 2147483647, default: nil
end
def down
diff --git a/db/post_migrate/20161109150329_fix_project_records_with_invalid_visibility.rb b/db/post_migrate/20161109150329_fix_project_records_with_invalid_visibility.rb
index 69007b8e8ed..f058e85c1ec 100644
--- a/db/post_migrate/20161109150329_fix_project_records_with_invalid_visibility.rb
+++ b/db/post_migrate/20161109150329_fix_project_records_with_invalid_visibility.rb
@@ -1,4 +1,5 @@
class FixProjectRecordsWithInvalidVisibility < ActiveRecord::Migration
+ include Gitlab::Database::ArelMethods
include Gitlab::Database::MigrationHelpers
BATCH_SIZE = 500
@@ -33,7 +34,7 @@ class FixProjectRecordsWithInvalidVisibility < ActiveRecord::Migration
end
updates.each do |visibility_level, project_ids|
- updater = Arel::UpdateManager.new(ActiveRecord::Base)
+ updater = arel_update_manager
.table(projects)
.set(projects[:visibility_level] => visibility_level)
.where(projects[:id].in(project_ids))
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 78413a608f1..392fa00b1ba 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,5 +1,6 @@
# rubocop:disable Migration/UpdateLargeTable
class MigrateUserActivitiesToUsersLastActivityOn < ActiveRecord::Migration
+ include Gitlab::Database::ArelMethods
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
@@ -39,7 +40,7 @@ class MigrateUserActivitiesToUsersLastActivityOn < ActiveRecord::Migration
activities = activities(day.at_beginning_of_day, day.at_end_of_day, page: page)
update_sql =
- Arel::UpdateManager.new(ActiveRecord::Base)
+ arel_update_manager
.table(users_table)
.set(users_table[:last_activity_on] => day.to_date)
.where(users_table[:username].in(activities.map(&:first)))
diff --git a/db/post_migrate/20171124104327_migrate_kubernetes_service_to_new_clusters_architectures.rb b/db/post_migrate/20171124104327_migrate_kubernetes_service_to_new_clusters_architectures.rb
index 11b581e4b57..a957f107405 100644
--- a/db/post_migrate/20171124104327_migrate_kubernetes_service_to_new_clusters_architectures.rb
+++ b/db/post_migrate/20171124104327_migrate_kubernetes_service_to_new_clusters_architectures.rb
@@ -48,7 +48,7 @@ class MigrateKubernetesServiceToNewClustersArchitectures < ActiveRecord::Migrati
attr_encrypted :token,
mode: :per_attribute_iv,
- key: Gitlab::Application.secrets.db_key_base,
+ key: Settings.attr_encrypted_db_key_base_truncated,
algorithm: 'aes-256-cbc'
end
diff --git a/db/post_migrate/20180408143355_cleanup_users_rss_token_rename.rb b/db/post_migrate/20180408143355_cleanup_users_rss_token_rename.rb
new file mode 100644
index 00000000000..bff83379087
--- /dev/null
+++ b/db/post_migrate/20180408143355_cleanup_users_rss_token_rename.rb
@@ -0,0 +1,13 @@
+class CleanupUsersRssTokenRename < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ def up
+ cleanup_concurrent_column_rename :users, :rss_token, :feed_token
+ end
+
+ def down
+ rename_column_concurrently :users, :feed_token, :rss_token
+ end
+end
diff --git a/db/post_migrate/20180424151928_fill_file_store.rb b/db/post_migrate/20180424151928_fill_file_store.rb
new file mode 100644
index 00000000000..b41feb233be
--- /dev/null
+++ b/db/post_migrate/20180424151928_fill_file_store.rb
@@ -0,0 +1,72 @@
+class FillFileStore < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class JobArtifact < ActiveRecord::Base
+ include EachBatch
+ self.table_name = 'ci_job_artifacts'
+ BATCH_SIZE = 10_000
+
+ def self.params_for_background_migration
+ yield self.where(file_store: nil), 'FillFileStoreJobArtifact', 5.minutes, BATCH_SIZE
+ end
+ end
+
+ class LfsObject < ActiveRecord::Base
+ include EachBatch
+ self.table_name = 'lfs_objects'
+ BATCH_SIZE = 10_000
+
+ def self.params_for_background_migration
+ yield self.where(file_store: nil), 'FillFileStoreLfsObject', 5.minutes, BATCH_SIZE
+ end
+ end
+
+ class Upload < ActiveRecord::Base
+ include EachBatch
+ self.table_name = 'uploads'
+ self.inheritance_column = :_type_disabled # Disable STI
+ BATCH_SIZE = 10_000
+
+ def self.params_for_background_migration
+ yield self.where(store: nil), 'FillStoreUpload', 5.minutes, BATCH_SIZE
+ end
+ end
+
+ def up
+ # NOTE: Schedule background migrations that fill 'NULL' value by '1'(ObjectStorage::Store::LOCAL) on `file_store`, `store` columns
+ #
+ # Here are the target columns
+ # - ci_job_artifacts.file_store
+ # - lfs_objects.file_store
+ # - uploads.store
+
+ FillFileStore::JobArtifact.params_for_background_migration do |relation, class_name, delay_interval, batch_size|
+ queue_background_migration_jobs_by_range_at_intervals(relation,
+ class_name,
+ delay_interval,
+ batch_size: batch_size)
+ end
+
+ FillFileStore::LfsObject.params_for_background_migration do |relation, class_name, delay_interval, batch_size|
+ queue_background_migration_jobs_by_range_at_intervals(relation,
+ class_name,
+ delay_interval,
+ batch_size: batch_size)
+ end
+
+ FillFileStore::Upload.params_for_background_migration do |relation, class_name, delay_interval, batch_size|
+ queue_background_migration_jobs_by_range_at_intervals(relation,
+ class_name,
+ delay_interval,
+ batch_size: batch_size)
+ end
+ end
+
+ def down
+ # noop
+ end
+end
diff --git a/db/post_migrate/20180507083701_set_minimal_project_build_timeout.rb b/db/post_migrate/20180507083701_set_minimal_project_build_timeout.rb
new file mode 100644
index 00000000000..d9d9e93f5a3
--- /dev/null
+++ b/db/post_migrate/20180507083701_set_minimal_project_build_timeout.rb
@@ -0,0 +1,19 @@
+class SetMinimalProjectBuildTimeout < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ MINIMUM_TIMEOUT = 600
+
+ # Allow this migration to resume if it fails partway through
+ disable_ddl_transaction!
+
+ def up
+ update_column_in_batches(:projects, :build_timeout, MINIMUM_TIMEOUT) do |table, query|
+ query.where(table[:build_timeout].lt(MINIMUM_TIMEOUT))
+ end
+ end
+
+ def down
+ # no-op
+ end
+end
diff --git a/db/post_migrate/20180521162137_migrate_remaining_mr_metrics_populating_background_migration.rb b/db/post_migrate/20180521162137_migrate_remaining_mr_metrics_populating_background_migration.rb
new file mode 100644
index 00000000000..0282688fa40
--- /dev/null
+++ b/db/post_migrate/20180521162137_migrate_remaining_mr_metrics_populating_background_migration.rb
@@ -0,0 +1,44 @@
+class MigrateRemainingMrMetricsPopulatingBackgroundMigration < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ BATCH_SIZE = 5_000
+ MIGRATION = 'PopulateMergeRequestMetricsWithEventsData'
+ DELAY_INTERVAL = 10.minutes
+
+ disable_ddl_transaction!
+
+ class MergeRequest < ActiveRecord::Base
+ self.table_name = 'merge_requests'
+
+ include ::EachBatch
+ end
+
+ def up
+ # Perform any ongoing background migration that might still be running. This
+ # avoids scheduling way too many of the same jobs on self-hosted instances
+ # if they're updating GitLab across multiple versions. The "Take one"
+ # migration was executed on 10.4 on
+ # SchedulePopulateMergeRequestMetricsWithEventsData.
+ Gitlab::BackgroundMigration.steal(MIGRATION)
+
+ metrics_not_exists_clause = <<~SQL
+ NOT EXISTS (SELECT 1 FROM merge_request_metrics
+ WHERE merge_request_metrics.merge_request_id = merge_requests.id)
+ SQL
+
+ relation = MergeRequest.where(metrics_not_exists_clause)
+
+ # We currently have ~400_000 MR records without metrics on GitLab.com.
+ # This means it'll schedule ~80 jobs (5000 MRs each) with a 10 minutes gap,
+ # so this should take ~14 hours for all background migrations to complete.
+ #
+ queue_background_migration_jobs_by_range_at_intervals(relation,
+ MIGRATION,
+ DELAY_INTERVAL,
+ batch_size: BATCH_SIZE)
+ end
+
+ def down
+ end
+end
diff --git a/db/post_migrate/20180523125103_cleanup_merge_requests_allow_maintainer_to_push_rename.rb b/db/post_migrate/20180523125103_cleanup_merge_requests_allow_maintainer_to_push_rename.rb
new file mode 100644
index 00000000000..7301bcf2c6c
--- /dev/null
+++ b/db/post_migrate/20180523125103_cleanup_merge_requests_allow_maintainer_to_push_rename.rb
@@ -0,0 +1,17 @@
+class CleanupMergeRequestsAllowMaintainerToPushRename < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ # NOOP
+ end
+
+ def down
+ if column_exists?(:merge_requests, :allow_collaboration)
+ rename_column_concurrently :merge_requests, :allow_collaboration, :allow_maintainer_to_push
+ end
+ end
+end
diff --git a/db/post_migrate/20180529152628_schedule_to_archive_legacy_traces.rb b/db/post_migrate/20180529152628_schedule_to_archive_legacy_traces.rb
new file mode 100644
index 00000000000..965cd3a8714
--- /dev/null
+++ b/db/post_migrate/20180529152628_schedule_to_archive_legacy_traces.rb
@@ -0,0 +1,35 @@
+class ScheduleToArchiveLegacyTraces < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ BATCH_SIZE = 5000
+ BACKGROUND_MIGRATION_CLASS = 'ArchiveLegacyTraces'
+
+ disable_ddl_transaction!
+
+ class Build < ActiveRecord::Base
+ include EachBatch
+ self.table_name = 'ci_builds'
+ self.inheritance_column = :_type_disabled # Disable STI
+
+ scope :type_build, -> { where(type: 'Ci::Build') }
+
+ scope :finished, -> { where(status: [:success, :failed, :canceled]) }
+
+ scope :without_archived_trace, -> do
+ where('NOT EXISTS (SELECT 1 FROM ci_job_artifacts WHERE ci_builds.id = ci_job_artifacts.job_id AND ci_job_artifacts.file_type = 3)')
+ end
+ end
+
+ def up
+ queue_background_migration_jobs_by_range_at_intervals(
+ ::ScheduleToArchiveLegacyTraces::Build.type_build.finished.without_archived_trace,
+ BACKGROUND_MIGRATION_CLASS,
+ 5.minutes,
+ batch_size: BATCH_SIZE)
+ end
+
+ def down
+ # noop
+ end
+end
diff --git a/db/post_migrate/20180603190921_migrate_object_storage_upload_sidekiq_queue.rb b/db/post_migrate/20180603190921_migrate_object_storage_upload_sidekiq_queue.rb
new file mode 100644
index 00000000000..57bee6269b9
--- /dev/null
+++ b/db/post_migrate/20180603190921_migrate_object_storage_upload_sidekiq_queue.rb
@@ -0,0 +1,16 @@
+class MigrateObjectStorageUploadSidekiqQueue < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ sidekiq_queue_migrate 'object_storage_upload', to: 'object_storage:object_storage_background_move'
+ end
+
+ def down
+ # do not migrate any jobs back because we would migrate also
+ # jobs which were not part of the 'object_storage_upload'
+ end
+end
diff --git a/db/post_migrate/20180604123514_cleanup_stages_position_migration.rb b/db/post_migrate/20180604123514_cleanup_stages_position_migration.rb
new file mode 100644
index 00000000000..73c23dffca0
--- /dev/null
+++ b/db/post_migrate/20180604123514_cleanup_stages_position_migration.rb
@@ -0,0 +1,43 @@
+class CleanupStagesPositionMigration < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ TMP_INDEX_NAME = 'tmp_id_stage_position_partial_null_index'.freeze
+
+ disable_ddl_transaction!
+
+ class Stages < ActiveRecord::Base
+ include EachBatch
+ self.table_name = 'ci_stages'
+ end
+
+ def up
+ disable_statement_timeout
+
+ Gitlab::BackgroundMigration.steal('MigrateStageIndex')
+
+ unless index_exists_by_name?(:ci_stages, TMP_INDEX_NAME)
+ add_concurrent_index(:ci_stages, :id, where: 'position IS NULL', name: TMP_INDEX_NAME)
+ end
+
+ migratable = <<~SQL
+ position IS NULL AND EXISTS (
+ SELECT 1 FROM ci_builds WHERE stage_id = ci_stages.id AND stage_idx IS NOT NULL
+ )
+ SQL
+
+ Stages.where(migratable).each_batch(of: 1000) do |batch|
+ batch.pluck(:id).each do |stage|
+ Gitlab::BackgroundMigration::MigrateStageIndex.new.perform(stage, stage)
+ end
+ end
+
+ remove_concurrent_index_by_name(:ci_stages, TMP_INDEX_NAME)
+ end
+
+ def down
+ if index_exists_by_name?(:ci_stages, TMP_INDEX_NAME)
+ remove_concurrent_index_by_name(:ci_stages, TMP_INDEX_NAME)
+ end
+ end
+end
diff --git a/db/post_migrate/20180608201435_cleanup_merge_requests_allow_collaboration_rename.rb b/db/post_migrate/20180608201435_cleanup_merge_requests_allow_collaboration_rename.rb
new file mode 100644
index 00000000000..3f3edb8ea3d
--- /dev/null
+++ b/db/post_migrate/20180608201435_cleanup_merge_requests_allow_collaboration_rename.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 CleanupMergeRequestsAllowCollaborationRename < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ if column_exists?(:merge_requests, :allow_collaboration)
+ cleanup_concurrent_column_rename :merge_requests, :allow_collaboration, :allow_maintainer_to_push
+ end
+ end
+
+ def down
+ # NOOP
+ end
+end
diff --git a/db/post_migrate/20180619121030_enqueue_delete_diff_files_workers.rb b/db/post_migrate/20180619121030_enqueue_delete_diff_files_workers.rb
new file mode 100644
index 00000000000..c4d2f5f61a0
--- /dev/null
+++ b/db/post_migrate/20180619121030_enqueue_delete_diff_files_workers.rb
@@ -0,0 +1,26 @@
+class EnqueueDeleteDiffFilesWorkers < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ SCHEDULER = 'ScheduleDiffFilesDeletion'.freeze
+ TMP_INDEX = 'tmp_partial_diff_id_with_files_index'.freeze
+
+ disable_ddl_transaction!
+
+ def up
+ unless index_exists_by_name?(:merge_request_diffs, TMP_INDEX)
+ add_concurrent_index(:merge_request_diffs, :id, where: "(state NOT IN ('without_files', 'empty'))", name: TMP_INDEX)
+ end
+
+ BackgroundMigrationWorker.perform_async(SCHEDULER)
+
+ # We don't remove the index since it's going to be used on DeleteDiffFiles
+ # worker. We should remove it in an upcoming release.
+ end
+
+ def down
+ if index_exists_by_name?(:merge_request_diffs, TMP_INDEX)
+ remove_concurrent_index_by_name(:merge_request_diffs, TMP_INDEX)
+ end
+ end
+end
diff --git a/db/post_migrate/20180629191052_add_partial_index_to_projects_for_last_repository_check_at.rb b/db/post_migrate/20180629191052_add_partial_index_to_projects_for_last_repository_check_at.rb
new file mode 100644
index 00000000000..a701d3678db
--- /dev/null
+++ b/db/post_migrate/20180629191052_add_partial_index_to_projects_for_last_repository_check_at.rb
@@ -0,0 +1,18 @@
+class AddPartialIndexToProjectsForLastRepositoryCheckAt < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ INDEX_NAME = "index_projects_on_last_repository_check_at"
+
+ def up
+ add_concurrent_index(:projects, :last_repository_check_at, where: "last_repository_check_at IS NOT NULL", name: INDEX_NAME)
+ end
+
+ def down
+ remove_concurrent_index(:projects, :last_repository_check_at, where: "last_repository_check_at IS NOT NULL", name: INDEX_NAME)
+ end
+end
diff --git a/db/post_migrate/20180702120647_enqueue_fix_cross_project_label_links.rb b/db/post_migrate/20180702120647_enqueue_fix_cross_project_label_links.rb
new file mode 100644
index 00000000000..59aa41adede
--- /dev/null
+++ b/db/post_migrate/20180702120647_enqueue_fix_cross_project_label_links.rb
@@ -0,0 +1,30 @@
+class EnqueueFixCrossProjectLabelLinks < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ BATCH_SIZE = 100
+ MIGRATION = 'FixCrossProjectLabelLinks'
+ DELAY_INTERVAL = 5.minutes
+
+ disable_ddl_transaction!
+
+ class Label < ActiveRecord::Base
+ self.table_name = 'labels'
+ end
+
+ class Namespace < ActiveRecord::Base
+ self.table_name = 'namespaces'
+
+ include ::EachBatch
+
+ default_scope { where(type: 'Group', id: Label.where(type: 'GroupLabel').select('distinct group_id')) }
+ end
+
+ def up
+ queue_background_migration_jobs_by_range_at_intervals(Namespace, MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE)
+ end
+
+ def down
+ # noop
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 37d336b9928..d2aa31fae30 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: 20180521171529) do
+ActiveRecord::Schema.define(version: 20180704204006) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -38,6 +38,7 @@ ActiveRecord::Schema.define(version: 20180521171529) do
t.integer "cached_markdown_version"
t.text "new_project_guidelines"
t.text "new_project_guidelines_html"
+ t.string "favicon"
end
create_table "application_setting_terms", force: :cascade do |t|
@@ -110,7 +111,7 @@ ActiveRecord::Schema.define(version: 20180521171529) do
t.text "shared_runners_text_html"
t.text "after_sign_up_text_html"
t.integer "rsa_key_restriction", default: 0, null: false
- t.integer "dsa_key_restriction", default: 0, null: false
+ t.integer "dsa_key_restriction", default: -1, null: false
t.integer "ecdsa_key_restriction", default: 0, null: false
t.integer "ed25519_key_restriction", default: 0, null: false
t.boolean "housekeeping_enabled", default: true, null: false
@@ -166,6 +167,7 @@ ActiveRecord::Schema.define(version: 20180521171529) do
t.boolean "allow_local_requests_from_hooks_and_services", default: false, null: false
t.boolean "enforce_terms", default: false
t.boolean "mirror_available", default: true, null: false
+ t.boolean "hide_third_party_offers", default: false, null: false
end
create_table "audit_events", force: :cascade do |t|
@@ -357,6 +359,15 @@ ActiveRecord::Schema.define(version: 20180521171529) do
add_index "ci_builds_metadata", ["build_id"], name: "index_ci_builds_metadata_on_build_id", unique: true, using: :btree
add_index "ci_builds_metadata", ["project_id"], name: "index_ci_builds_metadata_on_project_id", using: :btree
+ create_table "ci_builds_runner_session", id: :bigserial, force: :cascade do |t|
+ t.integer "build_id", null: false
+ t.string "url", null: false
+ t.string "certificate"
+ t.string "authorization"
+ end
+
+ add_index "ci_builds_runner_session", ["build_id"], name: "index_ci_builds_runner_session_on_build_id", unique: true, using: :btree
+
create_table "ci_group_variables", force: :cascade do |t|
t.string "key", null: false
t.text "value"
@@ -451,10 +462,12 @@ ActiveRecord::Schema.define(version: 20180521171529) do
t.integer "config_source"
t.boolean "protected"
t.integer "failure_reason"
+ t.integer "iid"
end
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", "iid"], name: "index_ci_pipelines_on_project_id_and_iid", unique: true, where: "(iid IS NOT NULL)", 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
@@ -518,6 +531,7 @@ ActiveRecord::Schema.define(version: 20180521171529) do
end
add_index "ci_stages", ["pipeline_id", "name"], name: "index_ci_stages_on_pipeline_id_and_name", unique: true, using: :btree
+ add_index "ci_stages", ["pipeline_id", "position"], name: "index_ci_stages_on_pipeline_id_and_position", using: :btree
add_index "ci_stages", ["pipeline_id"], name: "index_ci_stages_on_pipeline_id", using: :btree
add_index "ci_stages", ["project_id"], name: "index_ci_stages_on_project_id", using: :btree
@@ -635,6 +649,17 @@ ActiveRecord::Schema.define(version: 20180521171529) do
t.string "external_ip"
end
+ create_table "clusters_applications_jupyter", force: :cascade do |t|
+ t.integer "cluster_id", null: false
+ t.integer "oauth_application_id"
+ t.integer "status", null: false
+ t.string "version", null: false
+ t.string "hostname"
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.text "status_reason"
+ end
+
create_table "clusters_applications_prometheus", force: :cascade do |t|
t.integer "cluster_id", null: false
t.integer "status", null: false
@@ -742,6 +767,7 @@ ActiveRecord::Schema.define(version: 20180521171529) do
end
add_index "deployments", ["created_at"], name: "index_deployments_on_created_at", using: :btree
+ add_index "deployments", ["deployable_type", "deployable_id"], name: "index_deployments_on_deployable_type_and_deployable_id", using: :btree
add_index "deployments", ["environment_id", "id"], name: "index_deployments_on_environment_id_and_id", using: :btree
add_index "deployments", ["environment_id", "iid", "project_id"], name: "index_deployments_on_environment_id_and_iid_and_project_id", using: :btree
add_index "deployments", ["project_id", "iid"], name: "index_deployments_on_project_id_and_iid", unique: true, using: :btree
@@ -924,6 +950,16 @@ ActiveRecord::Schema.define(version: 20180521171529) do
add_index "identities", ["user_id"], name: "index_identities_on_user_id", using: :btree
+ create_table "import_export_uploads", force: :cascade do |t|
+ t.datetime_with_timezone "updated_at", null: false
+ t.integer "project_id"
+ t.text "import_file"
+ t.text "export_file"
+ end
+
+ add_index "import_export_uploads", ["project_id"], name: "index_import_export_uploads_on_project_id", using: :btree
+ add_index "import_export_uploads", ["updated_at"], name: "index_import_export_uploads_on_updated_at", using: :btree
+
create_table "internal_ids", id: :bigserial, force: :cascade do |t|
t.integer "project_id"
t.integer "usage", null: false
@@ -1216,6 +1252,7 @@ ActiveRecord::Schema.define(version: 20180521171529) do
t.boolean "discussion_locked"
t.integer "latest_merge_request_diff_id"
t.string "rebase_commit_sha"
+ t.boolean "squash", default: false, null: false
t.boolean "allow_maintainer_to_push"
end
@@ -1232,6 +1269,7 @@ ActiveRecord::Schema.define(version: 20180521171529) do
add_index "merge_requests", ["source_project_id", "source_branch"], name: "index_merge_requests_on_source_project_id_and_source_branch", using: :btree
add_index "merge_requests", ["target_branch"], name: "index_merge_requests_on_target_branch", using: :btree
add_index "merge_requests", ["target_project_id", "iid"], name: "index_merge_requests_on_target_project_id_and_iid", unique: true, using: :btree
+ add_index "merge_requests", ["target_project_id", "iid"], name: "index_merge_requests_on_target_project_id_and_iid_opened", where: "((state)::text = 'opened'::text)", using: :btree
add_index "merge_requests", ["target_project_id", "merge_commit_sha", "id"], name: "index_merge_requests_on_tp_id_and_merge_commit_sha_and_id", using: :btree
add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree
add_index "merge_requests", ["title"], name: "index_merge_requests_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
@@ -1302,6 +1340,20 @@ ActiveRecord::Schema.define(version: 20180521171529) do
add_index "namespaces", ["runners_token"], name: "index_namespaces_on_runners_token", unique: true, using: :btree
add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree
+ create_table "note_diff_files", force: :cascade do |t|
+ t.integer "diff_note_id", null: false
+ t.text "diff", null: false
+ t.boolean "new_file", null: false
+ t.boolean "renamed_file", null: false
+ t.boolean "deleted_file", null: false
+ t.string "a_mode", null: false
+ t.string "b_mode", null: false
+ t.text "new_path", null: false
+ t.text "old_path", null: false
+ end
+
+ add_index "note_diff_files", ["diff_note_id"], name: "index_note_diff_files_on_diff_note_id", unique: true, using: :btree
+
create_table "notes", force: :cascade do |t|
t.text "note"
t.string "noteable_type"
@@ -1463,6 +1515,7 @@ ActiveRecord::Schema.define(version: 20180521171529) do
t.datetime_with_timezone "updated_at", null: false
t.boolean "enabled"
t.string "domain"
+ t.integer "deploy_strategy", default: 0, null: false
end
add_index "project_auto_devops", ["project_id"], name: "index_project_auto_devops_on_project_id", unique: true, using: :btree
@@ -1613,6 +1666,7 @@ ActiveRecord::Schema.define(version: 20180521171529) do
add_index "projects", ["description"], name: "index_projects_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
add_index "projects", ["id"], name: "index_projects_on_id_partial_for_visibility", unique: true, where: "(visibility_level = ANY (ARRAY[10, 20]))", using: :btree
add_index "projects", ["last_activity_at"], name: "index_projects_on_last_activity_at", using: :btree
+ add_index "projects", ["last_repository_check_at"], name: "index_projects_on_last_repository_check_at", where: "(last_repository_check_at IS NOT NULL)", using: :btree
add_index "projects", ["last_repository_check_failed"], name: "index_projects_on_last_repository_check_failed", using: :btree
add_index "projects", ["last_repository_updated_at"], name: "index_projects_on_last_repository_updated_at", using: :btree
add_index "projects", ["name"], name: "index_projects_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"}
@@ -2066,9 +2120,9 @@ ActiveRecord::Schema.define(version: 20180521171529) do
t.date "last_activity_on"
t.boolean "notified_of_own_activity"
t.string "preferred_language"
- t.string "rss_token"
t.integer "theme_id", limit: 2
t.integer "accepted_term_id"
+ t.string "feed_token"
end
add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
@@ -2076,12 +2130,12 @@ ActiveRecord::Schema.define(version: 20180521171529) do
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
add_index "users", ["email"], name: "index_users_on_email_trigram", using: :gin, opclasses: {"email"=>"gin_trgm_ops"}
+ add_index "users", ["feed_token"], name: "index_users_on_feed_token", using: :btree
add_index "users", ["ghost"], name: "index_users_on_ghost", using: :btree
add_index "users", ["incoming_email_token"], name: "index_users_on_incoming_email_token", using: :btree
add_index "users", ["name"], name: "index_users_on_name", using: :btree
add_index "users", ["name"], name: "index_users_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"}
add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree
- add_index "users", ["rss_token"], name: "index_users_on_rss_token", using: :btree
add_index "users", ["state"], name: "index_users_on_state", using: :btree
add_index "users", ["username"], name: "index_users_on_username", using: :btree
add_index "users", ["username"], name: "index_users_on_username_trigram", using: :gin, opclasses: {"username"=>"gin_trgm_ops"}
@@ -2111,6 +2165,7 @@ ActiveRecord::Schema.define(version: 20180521171529) do
t.datetime "updated_at", null: false
end
+ add_index "web_hook_logs", ["created_at", "web_hook_id"], name: "index_web_hook_logs_on_created_at_and_web_hook_id", using: :btree
add_index "web_hook_logs", ["web_hook_id"], name: "index_web_hook_logs_on_web_hook_id", using: :btree
create_table "web_hooks", force: :cascade do |t|
@@ -2154,6 +2209,7 @@ ActiveRecord::Schema.define(version: 20180521171529) do
add_foreign_key "ci_builds", "projects", name: "fk_befce0568a", on_delete: :cascade
add_foreign_key "ci_builds_metadata", "ci_builds", column: "build_id", on_delete: :cascade
add_foreign_key "ci_builds_metadata", "projects", on_delete: :cascade
+ add_foreign_key "ci_builds_runner_session", "ci_builds", column: "build_id", on_delete: :cascade
add_foreign_key "ci_group_variables", "namespaces", column: "group_id", name: "fk_33ae4d58d8", on_delete: :cascade
add_foreign_key "ci_job_artifacts", "ci_builds", column: "job_id", on_delete: :cascade
add_foreign_key "ci_job_artifacts", "projects", on_delete: :cascade
@@ -2180,6 +2236,8 @@ ActiveRecord::Schema.define(version: 20180521171529) do
add_foreign_key "clusters", "users", on_delete: :nullify
add_foreign_key "clusters_applications_helm", "clusters", on_delete: :cascade
add_foreign_key "clusters_applications_ingress", "clusters", name: "fk_753a7b41c1", on_delete: :cascade
+ add_foreign_key "clusters_applications_jupyter", "clusters", on_delete: :cascade
+ add_foreign_key "clusters_applications_jupyter", "oauth_applications", on_delete: :nullify
add_foreign_key "clusters_applications_prometheus", "clusters", name: "fk_557e773639", on_delete: :cascade
add_foreign_key "clusters_applications_runners", "ci_runners", column: "runner_id", name: "fk_02de2ded36", on_delete: :nullify
add_foreign_key "clusters_applications_runners", "clusters", on_delete: :cascade
@@ -2203,6 +2261,7 @@ ActiveRecord::Schema.define(version: 20180521171529) do
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 "import_export_uploads", "projects", on_delete: :cascade
add_foreign_key "internal_ids", "namespaces", name: "fk_162941d509", on_delete: :cascade
add_foreign_key "internal_ids", "projects", on_delete: :cascade
add_foreign_key "issue_assignees", "issues", name: "fk_b7d881734a", on_delete: :cascade
@@ -2243,6 +2302,7 @@ ActiveRecord::Schema.define(version: 20180521171529) do
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
add_foreign_key "milestones", "projects", name: "fk_9bd0a0c791", on_delete: :cascade
+ add_foreign_key "note_diff_files", "notes", column: "diff_note_id", on_delete: :cascade
add_foreign_key "notes", "projects", name: "fk_99e097b079", on_delete: :cascade
add_foreign_key "oauth_openid_requests", "oauth_access_grants", column: "access_grant_id", name: "fk_oauth_openid_requests_oauth_access_grants_access_grant_id"
add_foreign_key "pages_domains", "projects", name: "fk_ea2f6dfc6f", on_delete: :cascade
diff --git a/doc/README.md b/doc/README.md
index ff8dd3fab8a..32924942497 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -162,7 +162,7 @@ configuration. Then customize everything from buildpacks to CI/CD.
- [Auto DevOps](topics/autodevops/index.md)
- [Deployment of Helm, Ingress, and Prometheus on Kubernetes](user/project/clusters/index.md#installing-applications)
-- [Protected secret variables](ci/variables/README.md#protected-secret-variables)
+- [Protected variables](ci/variables/README.md#protected-variables)
- [Easy creation of Kubernetes clusters on GKE](user/project/clusters/index.md#adding-and-creating-a-new-gke-cluster-via-gitlab)
### Monitor
@@ -191,7 +191,7 @@ instant how code changes impact your production environment.
- [User account](user/profile/index.md): Manage your account
- [Authentication](topics/authentication/index.md): Account security with two-factor authentication, setup your ssh keys and deploy keys for secure access to your projects.
- [Profile settings](user/profile/index.md#profile-settings): Manage your profile settings, two factor authentication and more.
-- [User permissions](user/permissions.md): Learn what each role in a project (external/guest/reporter/developer/master/owner) can do.
+- [User permissions](user/permissions.md): Learn what each role in a project (external/guest/reporter/developer/maintainer/owner) can do.
### Git and GitLab
@@ -215,7 +215,7 @@ Learn how to contribute to GitLab:
- [Development](development/README.md): All styleguides and explanations how to contribute.
- [Legal](legal/README.md): Contributor license agreements.
-- [Writing documentation](development/writing_documentation.md): Contributing to GitLab Docs.
+- [Writing documentation](development/documentation/index.md): Contributing to GitLab Docs.
## GitLab subscriptions
@@ -228,7 +228,7 @@ straight away.
### GitLab self-hosted
-With GitLab self-hosted, you deploy your own GitLab instance on-premises or on a private cloud of your choice. GitLab self-hosted is available for [free and with paid subscriptions](https://about.gitlab.com/products/): Core, Starter, Premium, and Ultimate.
+With GitLab self-hosted, you deploy your own GitLab instance on-premises or on a private cloud of your choice. GitLab self-hosted is available for [free and with paid subscriptions](https://about.gitlab.com/pricing/): Core, Starter, Premium, and Ultimate.
Every feature available in Core is also available in Starter, Premium, and Ultimate.
Starter features are also available in Premium and Ultimate, and Premium features are also
diff --git a/doc/administration/auth/how_to_configure_ldap_gitlab_ce/index.md b/doc/administration/auth/how_to_configure_ldap_gitlab_ce/index.md
index aa5e9513290..621d4f77d5e 100644
--- a/doc/administration/auth/how_to_configure_ldap_gitlab_ce/index.md
+++ b/doc/administration/auth/how_to_configure_ldap_gitlab_ce/index.md
@@ -107,7 +107,7 @@ Global Admins GitLab.org/GitLab INT/Global Groups/Global Admins
## GitLab LDAP configuration
-The initial configuration of LDAP in GitLab requires changes to the `gitlab.rb` configuration file. Below is an example of a complete configuration using an Active Directory.
+The initial configuration of LDAP in GitLab requires changes to the `gitlab.rb` configuration file (`/etc/gitlab/gitlab.rb`). Below is an example of a complete configuration using an Active Directory.
The two Active Directory specific values are `active_directory: true` and `uid: 'sAMAccountName'`. `sAMAccountName` is an attribute returned by Active Directory used for GitLab usernames. See the example output from `ldapsearch` for a full list of attributes a "person" object (user) has in **AD** - [`ldapsearch` example](#using-ldapsearch-unix)
diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md
index 63fbb24bac1..3c98d683924 100644
--- a/doc/administration/auth/ldap.md
+++ b/doc/administration/auth/ldap.md
@@ -1,10 +1,20 @@
+[//]: # (Do *NOT* modify this file in EE documentation. All changes in this)
+[//]: # (file should happen in CE, too. If the change is EE-specific, put)
+[//]: # (it in `ldap-ee.md`.)
+
# LDAP
GitLab integrates with LDAP to support user authentication.
This integration works with most LDAP-compliant directory
servers, including Microsoft Active Directory, Apple Open Directory, Open LDAP,
-and 389 Server. GitLab EE includes enhanced integration, including group
-membership syncing.
+and 389 Server. GitLab Enterprise Editions include enhanced integration,
+including group membership syncing as well as multiple LDAP servers support.
+
+## GitLab EE
+
+The information on this page is relevant for both GitLab CE and EE. For more
+details about EE-specific LDAP features, see the
+[LDAP Enterprise Edition documentation](https://docs.gitlab.com/ee/administration/auth/ldap-ee.html).
## Security
@@ -27,8 +37,10 @@ are already logged in or are using Git over SSH will still be able to access
GitLab for up to one hour. Manually block the user in the GitLab Admin area to
immediately block all access.
->**Note**: GitLab EE supports a configurable sync time, with a default
-of one hour.
+NOTE: **Note**:
+GitLab Enterprise Edition Starter supports a
+[configurable sync time](https://docs.gitlab.com/ee/administration/auth/ldap-ee.html#adjusting-ldap-user-and-group-sync-schedules),
+with a default of one hour.
## Git password authentication
@@ -38,19 +50,21 @@ in the application settings.
## Configuration
+NOTE: **Note**:
+In GitLab Enterprise Edition Starter, you can configure multiple LDAP servers
+to connect to one GitLab server.
+
For a complete guide on configuring LDAP with GitLab Community Edition, please check
the admin guide [How to configure LDAP with GitLab CE](how_to_configure_ldap_gitlab_ce/index.md).
To enable LDAP integration you need to add your LDAP server settings in
-`/etc/gitlab/gitlab.rb` or `/home/git/gitlab/config/gitlab.yml`.
+`/etc/gitlab/gitlab.rb` or `/home/git/gitlab/config/gitlab.yml` for Omnibus
+GitLab and installations from source respectively.
There is a Rake task to check LDAP configuration. After configuring LDAP
using the documentation below, see [LDAP check Rake task](../raketasks/check.md#ldap-check)
for information on the LDAP check Rake task.
->**Note**: In GitLab EE, you can configure multiple LDAP servers to connect to
-one GitLab server.
-
Prior to version 7.4, GitLab used a different syntax for configuring
LDAP integration. The old LDAP integration syntax still works but may be
removed in a future version. If your `gitlab.rb` or `gitlab.yml` file contains
@@ -61,159 +75,202 @@ The configuration inside `gitlab_rails['ldap_servers']` below is sensitive to
incorrect indentation. Be sure to retain the indentation given in the example.
Copy/paste can sometimes cause problems.
+NOTE: **Note:**
+The `encryption` value `ssl` corresponds to 'Simple TLS' in the LDAP
+library. `tls` corresponds to StartTLS, not to be confused with regular TLS.
+Normally, if you specify `ssl` it will be on port 636, while `tls` (StartTLS)
+would be on port 389. `plain` also operates on port 389.
+
**Omnibus configuration**
```ruby
gitlab_rails['ldap_enabled'] = true
gitlab_rails['ldap_servers'] = YAML.load <<-EOS # remember to close this block with 'EOS' below
-main: # 'main' is the GitLab 'provider ID' of this LDAP server
- ## label
- #
- # A human-friendly name for your LDAP server. It is OK to change the label later,
- # for instance if you find out it is too large to fit on the web page.
- #
- # Example: 'Paris' or 'Acme, Ltd.'
+##
+## 'main' is the GitLab 'provider ID' of this LDAP server
+##
+main:
+ ##
+ ## A human-friendly name for your LDAP server. It is OK to change the label later,
+ ## for instance if you find out it is too large to fit on the web page.
+ ##
+ ## Example: 'Paris' or 'Acme, Ltd.'
+ ##
label: 'LDAP'
- # Example: 'ldap.mydomain.com'
+ ##
+ ## Example: 'ldap.mydomain.com'
+ ##
host: '_your_ldap_server'
- # This port is an example, it is sometimes different but it is always an integer and not a string
+
+ ##
+ ## This port is an example, it is sometimes different but it is always an
+ ## integer and not a string.
+ ##
port: 389 # usually 636 for SSL
uid: 'sAMAccountName' # This should be the attribute, not the value that maps to uid.
- # Examples: 'america\\momo' or 'CN=Gitlab Git,CN=Users,DC=mydomain,DC=com'
+ ##
+ ## Examples: 'america\\momo' or 'CN=Gitlab Git,CN=Users,DC=mydomain,DC=com'
+ ##
bind_dn: '_the_full_dn_of_the_user_you_will_bind_with'
password: '_the_password_of_the_bind_user'
- # Encryption method. The "method" key is deprecated in favor of
- # "encryption".
- #
- # Examples: "start_tls" or "simple_tls" or "plain"
- #
- # Deprecated values: "tls" was replaced with "start_tls" and "ssl" was
- # replaced with "simple_tls".
- #
+ ##
+ ## Encryption method. The "method" key is deprecated in favor of
+ ## "encryption".
+ ##
+ ## Examples: "start_tls" or "simple_tls" or "plain"
+ ##
+ ## Deprecated values: "tls" was replaced with "start_tls" and "ssl" was
+ ## replaced with "simple_tls".
+ ##
+ ##
encryption: 'plain'
- # Enables SSL certificate verification if encryption method is
- # "start_tls" or "simple_tls". Defaults to true since GitLab 10.0 for
- # security. This may break installations upon upgrade to 10.0, that did
- # not know their LDAP SSL certificates were not setup properly. For
- # example, when using self-signed certificates, the ca_file path may
- # need to be specified.
+ ##
+ ## Enables SSL certificate verification if encryption method is
+ ## "start_tls" or "simple_tls". Defaults to true since GitLab 10.0 for
+ ## security. This may break installations upon upgrade to 10.0, that did
+ ## not know their LDAP SSL certificates were not setup properly.
+ ##
verify_certificates: true
- # Specifies the path to a file containing a PEM-format CA certificate,
- # e.g. if you need to use an internal CA.
- #
- # Example: '/etc/ca.pem'
- #
- ca_file: ''
-
- # Specifies the SSL version for OpenSSL to use, if the OpenSSL default
- # is not appropriate.
- #
- # Example: 'TLSv1_1'
- #
+ ##
+ ## Specifies the SSL version for OpenSSL to use, if the OpenSSL default
+ ## is not appropriate.
+ ##
+ ## Example: 'TLSv1_1'
+ ##
+ ##
ssl_version: ''
- # Set a timeout, in seconds, for LDAP queries. This helps avoid blocking
- # a request if the LDAP server becomes unresponsive.
- # A value of 0 means there is no timeout.
+ ##
+ ## Set a timeout, in seconds, for LDAP queries. This helps avoid blocking
+ ## a request if the LDAP server becomes unresponsive.
+ ## A value of 0 means there is no timeout.
+ ##
timeout: 10
- # This setting specifies if LDAP server is Active Directory LDAP server.
- # For non AD servers it skips the AD specific queries.
- # If your LDAP server is not AD, set this to false.
+ ##
+ ## This setting specifies if LDAP server is Active Directory LDAP server.
+ ## For non AD servers it skips the AD specific queries.
+ ## If your LDAP server is not AD, set this to false.
+ ##
active_directory: true
- # If allow_username_or_email_login is enabled, GitLab will ignore everything
- # after the first '@' in the LDAP username submitted by the user on login.
- #
- # Example:
- # - the user enters 'jane.doe@example.com' and 'p@ssw0rd' as LDAP credentials;
- # - GitLab queries the LDAP server with 'jane.doe' and 'p@ssw0rd'.
- #
- # If you are using "uid: 'userPrincipalName'" on ActiveDirectory you need to
- # disable this setting, because the userPrincipalName contains an '@'.
+ ##
+ ## If allow_username_or_email_login is enabled, GitLab will ignore everything
+ ## after the first '@' in the LDAP username submitted by the user on login.
+ ##
+ ## Example:
+ ## - the user enters 'jane.doe@example.com' and 'p@ssw0rd' as LDAP credentials;
+ ## - GitLab queries the LDAP server with 'jane.doe' and 'p@ssw0rd'.
+ ##
+ ## If you are using "uid: 'userPrincipalName'" on ActiveDirectory you need to
+ ## disable this setting, because the userPrincipalName contains an '@'.
+ ##
allow_username_or_email_login: false
- # To maintain tight control over the number of active users on your GitLab installation,
- # enable this setting to keep new users blocked until they have been cleared by the admin
- # (default: false).
+ ##
+ ## To maintain tight control over the number of active users on your GitLab installation,
+ ## enable this setting to keep new users blocked until they have been cleared by the admin
+ ## (default: false).
+ ##
block_auto_created_users: false
- # Base where we can search for users
- #
- # Ex. 'ou=People,dc=gitlab,dc=example' or 'DC=mydomain,DC=com'
- #
+ ##
+ ## Base where we can search for users
+ ##
+ ## Ex. 'ou=People,dc=gitlab,dc=example' or 'DC=mydomain,DC=com'
+ ##
+ ##
base: ''
- # Filter LDAP users
- #
- # Format: RFC 4515 https://tools.ietf.org/search/rfc4515
- # Ex. (employeeType=developer)
- #
- # Note: GitLab does not support omniauth-ldap's custom filter syntax.
- #
- # Example for getting only specific users:
- # '(&(objectclass=user)(|(samaccountname=momo)(samaccountname=toto)))'
- #
+ ##
+ ## Filter LDAP users
+ ##
+ ## Format: RFC 4515 https://tools.ietf.org/search/rfc4515
+ ## Ex. (employeeType=developer)
+ ##
+ ## Note: GitLab does not support omniauth-ldap's custom filter syntax.
+ ##
+ ## Example for getting only specific users:
+ ## '(&(objectclass=user)(|(samaccountname=momo)(samaccountname=toto)))'
+ ##
user_filter: ''
- # LDAP attributes that GitLab will use to create an account for the LDAP user.
- # The specified attribute can either be the attribute name as a string (e.g. 'mail'),
- # or an array of attribute names to try in order (e.g. ['mail', 'email']).
- # Note that the user's LDAP login will always be the attribute specified as `uid` above.
+ ##
+ ## LDAP attributes that GitLab will use to create an account for the LDAP user.
+ ## The specified attribute can either be the attribute name as a string (e.g. 'mail'),
+ ## or an array of attribute names to try in order (e.g. ['mail', 'email']).
+ ## Note that the user's LDAP login will always be the attribute specified as `uid` above.
+ ##
attributes:
- # The username will be used in paths for the user's own projects
- # (like `gitlab.example.com/username/project`) and when mentioning
- # them in issues, merge request and comments (like `@username`).
- # If the attribute specified for `username` contains an email address,
- # the GitLab username will be the part of the email address before the '@'.
+ ##
+ ## The username will be used in paths for the user's own projects
+ ## (like `gitlab.example.com/username/project`) and when mentioning
+ ## them in issues, merge request and comments (like `@username`).
+ ## If the attribute specified for `username` contains an email address,
+ ## the GitLab username will be the part of the email address before the '@'.
+ ##
username: ['uid', 'userid', 'sAMAccountName']
email: ['mail', 'email', 'userPrincipalName']
- # If no full name could be found at the attribute specified for `name`,
- # the full name is determined using the attributes specified for
- # `first_name` and `last_name`.
+ ##
+ ## If no full name could be found at the attribute specified for `name`,
+ ## the full name is determined using the attributes specified for
+ ## `first_name` and `last_name`.
+ ##
name: 'cn'
first_name: 'givenName'
last_name: 'sn'
- # If lowercase_usernames is enabled, GitLab will lower case the username.
+ ##
+ ## If lowercase_usernames is enabled, GitLab will lower case the username.
+ ##
lowercase_usernames: false
-
+ ##
## EE only
+ ##
- # Base where we can search for groups
- #
- # Ex. ou=groups,dc=gitlab,dc=example
- #
+ ## Base where we can search for groups
+ ##
+ ## Ex. ou=groups,dc=gitlab,dc=example
+ ##
group_base: ''
- # The CN of a group containing GitLab administrators
- #
- # Ex. administrators
- #
- # Note: Not `cn=administrators` or the full DN
- #
+ ## The CN of a group containing GitLab administrators
+ ##
+ ## Ex. administrators
+ ##
+ ## Note: Not `cn=administrators` or the full DN
+ ##
admin_group: ''
- # The LDAP attribute containing a user's public SSH key
- #
- # Ex. ssh_public_key
- #
+ ## An array of CNs of groups containing users that should be considered external
+ ##
+ ## Ex. ['interns', 'contractors']
+ ##
+ ## Note: Not `cn=interns` or the full DN
+ ##
+ external_groups: []
+
+ ##
+ ## The LDAP attribute containing a user's public SSH key
+ ##
+ ## Example: sshPublicKey
+ ##
sync_ssh_keys: false
-# GitLab EE only: add more LDAP servers
-# Choose an ID made of a-z and 0-9 . This ID will be stored in the database
-# so that GitLab can remember which LDAP server a user belongs to.
-# uswest2:
-# label:
-# host:
-# ....
+## GitLab EE only: add more LDAP servers
+## Choose an ID made of a-z and 0-9 . This ID will be stored in the database
+## so that GitLab can remember which LDAP server a user belongs to.
+#uswest2:
+# label:
+# host:
+# ....
EOS
```
@@ -222,21 +279,23 @@ EOS
Use the same format as `gitlab_rails['ldap_servers']` for the contents under
`servers:` in the example below:
-```
+```yaml
production:
# snip...
ldap:
enabled: false
servers:
- main: # 'main' is the GitLab 'provider ID' of this LDAP server
- ## label
- #
- # A human-friendly name for your LDAP server. It is OK to change the label later,
- # for instance if you find out it is too large to fit on the web page.
- #
- # Example: 'Paris' or 'Acme, Ltd.'
+ ##
+ ## 'main' is the GitLab 'provider ID' of this LDAP server
+ ##
+ main:
+ ##
+ ## A human-friendly name for your LDAP server. It is OK to change the label later,
+ ## for instance if you find out it is too large to fit on the web page.
+ ##
+ ## Example: 'Paris' or 'Acme, Ltd.'
label: 'LDAP'
- # snip...
+ ## snip...
```
## Using an LDAP filter to limit access to your GitLab server
@@ -283,6 +342,24 @@ nested members in the user filter should not be confused with
Please note that GitLab does not support the custom filter syntax used by
omniauth-ldap.
+### Escaping special characters
+
+If the `user_filter` DN contains special characters. For example, a comma:
+
+```
+OU=GitLab, Inc,DC=gitlab,DC=com
+```
+
+This character needs to be escaped as documented in [RFC 4515](https://tools.ietf.org/search/rfc4515).
+
+Due to the way the string is parsed, the special character needs to be converted
+to hex and `\\5C\\` (`5C` = `\` in hex) added before it.
+As an example the above DN would look like
+
+```
+OU=GitLab\\5C\\2C Inc,DC=gitlab,DC=com
+```
+
## Enabling LDAP sign-in for existing GitLab users
When a user signs in to GitLab with LDAP for the first time, and their LDAP
diff --git a/doc/administration/custom_hooks.md b/doc/administration/custom_hooks.md
index 960970aea30..1c508c77ffa 100644
--- a/doc/administration/custom_hooks.md
+++ b/doc/administration/custom_hooks.md
@@ -3,7 +3,7 @@
>
**Note:** Custom Git hooks must be configured on the filesystem of the GitLab
server. Only GitLab server administrators will be able to complete these tasks.
-Please explore [webhooks] as an option if you do not
+Please explore [webhooks] and [CI] as an option if you do not
have filesystem access. For a user configurable Git hook interface, see
[Push Rules](https://docs.gitlab.com/ee/push_rules/push_rules.html),
available in GitLab Enterprise Edition.
@@ -80,6 +80,7 @@ STDERR takes precedence over STDOUT.
![Custom message from custom Git hook](img/custom_hooks_error_msg.png)
+[CI]: ../ci/README.md
[hooks]: https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks#Server-Side-Hooks
[webhooks]: ../user/project/integrations/webhooks.md
[5073]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5073
diff --git a/doc/administration/high_availability/database.md b/doc/administration/high_availability/database.md
index ca6d8d2de67..b5124b1d540 100644
--- a/doc/administration/high_availability/database.md
+++ b/doc/administration/high_availability/database.md
@@ -33,16 +33,7 @@ If you use a cloud-managed service, or provide your own PostgreSQL:
external_url 'https://gitlab.example.com'
# Disable all components except PostgreSQL
- postgresql['enable'] = true
- bootstrap['enable'] = false
- nginx['enable'] = false
- unicorn['enable'] = false
- sidekiq['enable'] = false
- redis['enable'] = false
- prometheus['enable'] = false
- gitaly['enable'] = false
- gitlab_workhorse['enable'] = false
- mailroom['enable'] = false
+ roles ['postgres_role']
# PostgreSQL configuration
gitlab_rails['db_password'] = 'DB password'
diff --git a/doc/administration/high_availability/gitlab.md b/doc/administration/high_availability/gitlab.md
index e201848791c..0d9c10687f2 100644
--- a/doc/administration/high_availability/gitlab.md
+++ b/doc/administration/high_availability/gitlab.md
@@ -47,7 +47,8 @@ for each GitLab application server in your environment.
URL. Depending your the NFS configuration, you may need to change some GitLab
data locations. See [NFS documentation](nfs.md) for `/etc/gitlab/gitlab.rb`
configuration values for various scenarios. The example below assumes you've
- added NFS mounts in the default data locations.
+ added NFS mounts in the default data locations. Additionally the UID and GIDs
+ given are just examples and you should configure with your preferred values.
```ruby
external_url 'https://gitlab.example.com'
@@ -68,6 +69,14 @@ for each GitLab application server in your environment.
gitlab_rails['redis_port'] = '6379'
gitlab_rails['redis_host'] = '10.1.0.6' # IP/hostname of Redis server
gitlab_rails['redis_password'] = 'Redis Password'
+
+ # Ensure UIDs and GIDs match between servers for permissions via NFS
+ user['uid'] = 9000
+ user['gid'] = 9000
+ web_server['uid'] = 9001
+ web_server['gid'] = 9001
+ registry['uid'] = 9002
+ registry['gid'] = 9002
```
> **Note:** To maintain uniformity of links across HA clusters, the `external_url`
@@ -75,25 +84,24 @@ for each GitLab application server in your environment.
servers should point to the external url that users will use to access GitLab.
In a typical HA setup, this will be the url of the load balancer which will
route traffic to all GitLab application servers in the HA cluster.
-
-1. Run `sudo gitlab-ctl reconfigure` to compile the configuration.
+
+ > **Note:** When you specify `https` in the `external_url`, as in the example
+ above, GitLab assumes you have SSL certificates in `/etc/gitlab/ssl/`. If
+ certificates are not present, Nginx will fail to start. See
+ [Nginx documentation](http://docs.gitlab.com/omnibus/settings/nginx.html#enable-https)
+ for more information.
## First GitLab application server
-As a final step, run the setup rake task on the first GitLab application server.
-It is not necessary to run this on additional application servers.
+As a final step, run the setup rake task **only on** the first GitLab application server.
+Do not run this on additional application servers.
1. Initialize the database by running `sudo gitlab-rake gitlab:setup`.
+1. Run `sudo gitlab-ctl reconfigure` to compile the configuration.
> **WARNING:** Only run this setup task on **NEW** GitLab instances because it
will wipe any existing data.
-> **Note:** When you specify `https` in the `external_url`, as in the example
- above, GitLab assumes you have SSL certificates in `/etc/gitlab/ssl/`. If
- certificates are not present, Nginx will fail to start. See
- [Nginx documentation](http://docs.gitlab.com/omnibus/settings/nginx.html#enable-https)
- for more information.
-
## Extra configuration for additional GitLab application servers
Additional GitLab servers (servers configured **after** the first GitLab server)
@@ -101,8 +109,7 @@ need some extra configuration.
1. Configure shared secrets. These values can be obtained from the primary
GitLab server in `/etc/gitlab/gitlab-secrets.json`. Add these to
- `/etc/gitlab/gitlab.rb` **prior to** running the first `reconfigure` in
- the steps above.
+ `/etc/gitlab/gitlab.rb` **prior to** running the first `reconfigure`.
```ruby
gitlab_shell['secret_token'] = 'fbfb19c355066a9afb030992231c4a363357f77345edd0f2e772359e5be59b02538e1fa6cae8f93f7d23355341cea2b93600dab6d6c3edcdced558fc6d739860'
@@ -115,6 +122,8 @@ need some extra configuration.
from running on upgrade. Only the primary GitLab application server should
handle migrations.
+1. Run `sudo gitlab-ctl reconfigure` to compile the configuration.
+
## Troubleshooting
- `mount: wrong fs type, bad option, bad superblock on`
diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md
index 957f17e3ea3..87e96b71dd4 100644
--- a/doc/administration/high_availability/nfs.md
+++ b/doc/administration/high_availability/nfs.md
@@ -25,7 +25,9 @@ options:
errors when the Omnibus package tries to alter permissions. Note that GitLab
and other bundled components do **not** run as `root` but as non-privileged
users. The recommendation for `no_root_squash` is to allow the Omnibus package
- to set ownership and permissions on files, as needed.
+ to set ownership and permissions on files, as needed. In some cases where the
+ `no_root_squash` option is not available, the `root` flag can achieve the same
+ result.
- `sync` - Force synchronous behavior. Default is asynchronous and under certain
circumstances it could lead to data loss if a failure occurs before data has
synced.
diff --git a/doc/administration/index.md b/doc/administration/index.md
index df935095e61..88190b2df5f 100644
--- a/doc/administration/index.md
+++ b/doc/administration/index.md
@@ -11,7 +11,7 @@ Regular users don't have access to GitLab administration tools and settings.
GitLab has two product distributions: the open source
[GitLab Community Edition (CE)](https://gitlab.com/gitlab-org/gitlab-ce),
and the open core [GitLab Enterprise Edition (EE)](https://gitlab.com/gitlab-org/gitlab-ee),
-available through [different subscriptions](https://about.gitlab.com/products/).
+available through [different subscriptions](https://about.gitlab.com/pricing/).
You can [install GitLab CE or GitLab EE](https://about.gitlab.com/installation/ce-or-ee/),
but the features you'll have access to depend on the subscription you choose
@@ -45,10 +45,12 @@ Learn how to install, configure, update, and maintain your GitLab instance.
- [Environment variables](environment_variables.md): Supported environment variables that can be used to override their defaults values in order to configure GitLab.
- [Plugins](plugins.md): With custom plugins, GitLab administrators can introduce custom integrations without modifying GitLab's source code.
- [Enforcing Terms of Service](../user/admin_area/settings/terms.md)
+- [Third party offers](../user/admin_area/settings/third_party_offers.md)
#### Customizing GitLab's appearance
- [Header logo](../customization/branded_page_and_email_header.md): Change the logo on all pages and email headers.
+- [Favicon](../customization/favicon.md): Change the default favicon to your own logo.
- [Branded login page](../customization/branded_login_page.md): Customize the login page with your own logo, title, and description.
- [Welcome message](../customization/welcome_message.md): Add a custom welcome message to the sign-in page.
- ["New Project" page](../customization/new_project_page.md): Customize the text to be displayed on the page that opens whenever your users create a new project.
diff --git a/doc/administration/integration/koding.md b/doc/administration/integration/koding.md
index 67f9f01efb8..6c1ec3028cc 100644
--- a/doc/administration/integration/koding.md
+++ b/doc/administration/integration/koding.md
@@ -100,14 +100,14 @@ As it's pointed out before, you will need public access to this machine that
you've installed Koding and GitLab on. Better to use a domain but a static IP
is also fine.
-For IP based installation you can use [xip.io](https://xip.io) service which is
+For IP based installation you can use [nip.io](https://nip.io) service which is
free and provides DNS resolution to IP based requests like following;
- - 127.0.0.1.xip.io -> resolves to 127.0.0.1
- - foo.bar.baz.127.0.0.1.xip.io -> resolves to 127.0.0.1
+ - 127.0.0.1.nip.io -> resolves to 127.0.0.1
+ - foo.bar.baz.127.0.0.1.nip.io -> resolves to 127.0.0.1
- and so on...
-As Koding needs subdomains for team names; `foo.127.0.0.1.xip.io` requests for
+As Koding needs subdomains for team names; `foo.127.0.0.1.nip.io` requests for
a running koding instance on `127.0.0.1` server will be handled as `foo` team
requests.
@@ -127,8 +127,8 @@ your Koding installation. Team called `gitlab` has integration on Koding out
of the box, so if you didn't change anything your team on Koding should be
`gitlab`.
-So, if your Koding is running on `http://1.2.3.4.xip.io:8090` your URL needs
-to be `http://gitlab.1.2.3.4.xip.io:8090`. You need to provide the same host
+So, if your Koding is running on `http://1.2.3.4.nip.io:8090` your URL needs
+to be `http://gitlab.1.2.3.4.nip.io:8090`. You need to provide the same host
with your Koding installation here.
@@ -192,7 +192,7 @@ cd koding
docker-compose run \
--service-ports backend \
/opt/koding/scripts/bootstrap-container build \
- --host=**YOUR_IP**.xip.io \
+ --host=**YOUR_IP**.nip.io \
--gitlabHost=**GITLAB_IP** \
--gitlabPort=**GITLAB_PORT** \
--gitlabToken=**SECRET_TOKEN** \
@@ -224,7 +224,7 @@ cd koding
docker-compose run \
--service-ports backend \
/opt/koding/scripts/bootstrap-container build \
- --host=**YOUR_IP**.xip.io \
+ --host=**YOUR_IP**.nip.io \
```
#### Enable Single Sign On
@@ -233,7 +233,7 @@ Once you restarted your Koding and logged in with your username and password
you need to activate oauth authentication for your user. To do that
- Navigate to Dashboard on Koding from;
- `http://gitlab.**YOUR_IP**.xip.io:8090/Home/my-account`
+ `http://gitlab.**YOUR_IP**.nip.io:8090/Home/my-account`
- Scroll down to Integrations section
- Click on toggle to turn On integration in GitLab integration section
diff --git a/doc/administration/integration/terminal.md b/doc/administration/integration/terminal.md
index 91e844c7b42..e11ed58eb91 100644
--- a/doc/administration/integration/terminal.md
+++ b/doc/administration/integration/terminal.md
@@ -1,12 +1,13 @@
# Web terminals
-> [Introduced][ce-7690] in GitLab 8.15. Only project masters and owners can
- access web terminals.
+>
+[Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7690)
+in GitLab 8.15. Only project maintainers and owners can access web terminals.
-With the introduction of the [Kubernetes project service][kubservice], GitLab
-gained the ability to store and use credentials for a Kubernetes cluster. One
-of the things it uses these credentials for is providing access to
-[web terminals](../../ci/environments.html#web-terminals) for environments.
+With the introduction of the [Kubernetes integration](../../user/project/clusters/index.md),
+GitLab gained the ability to store and use credentials for a Kubernetes cluster.
+One of the things it uses these credentials for is providing access to
+[web terminals](../../ci/environments.md#web-terminals) for environments.
## How it works
@@ -80,6 +81,3 @@ Terminal sessions use long-lived connections; by default, these may last
forever. You can configure a maximum session time in the Admin area of your
GitLab instance if you find this undesirable from a scalability or security
point of view.
-
-[ce-7690]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7690
-[kubservice]: ../../user/project/integrations/kubernetes.md
diff --git a/doc/administration/job_artifacts.md b/doc/administration/job_artifacts.md
index 77fe4d561a1..8c55c8c4298 100644
--- a/doc/administration/job_artifacts.md
+++ b/doc/administration/job_artifacts.md
@@ -88,12 +88,12 @@ _The artifacts are stored by default in
### Using object storage
>**Notes:**
-- [Introduced][ee-1762] in [GitLab Premium][eep] 9.4.
-- Since version 9.5, artifacts are [browsable], when object storage is enabled.
- 9.4 lacks this feature.
-> Available in [GitLab Premium](https://about.gitlab.com/products/) and
-[GitLab.com Silver](https://about.gitlab.com/gitlab-com/).
-> Since version 10.6, available in [GitLab CE](https://about.gitlab.com/products/)
+- [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1762) in
+ [GitLab Premium](https://about.gitlab.com/pricing/) 9.4.
+- Since version 9.5, artifacts are [browsable](../user/project/pipelines/job_artifacts.md#browsing-artifacts),
+ when object storage is enabled. 9.4 lacks this feature.
+- Since version 10.6, available in [GitLab Core](https://about.gitlab.com/pricing/)
+- Since version 11.0, we support `direct_upload` to S3.
If you don't want to use the local disk where GitLab is installed to store the
artifacts, you can use an object storage like AWS S3 instead.
@@ -108,7 +108,7 @@ For source installations the following settings are nested under `artifacts:` an
|---------|-------------|---------|
| `enabled` | Enable/disable object storage | `false` |
| `remote_directory` | The bucket name where Artifacts will be stored| |
-| `direct_upload` | Set to true to enable direct upload of Artifacts without the need of local shared storage. Option may be removed once we decide to support only single storage for all files. Currently only `Google` provider is supported | `false` |
+| `direct_upload` | Set to true to enable direct upload of Artifacts without the need of local shared storage. Option may be removed once we decide to support only single storage for all files. | `false` |
| `background_upload` | Set to false to disable automatic upload. Option may be removed once upload is direct to S3 | `true` |
| `proxy_download` | Set to true to enable proxying all files served. Option allows to reduce egress traffic as this allows clients to download directly from remote storage instead of proxying all data | `false` |
| `connection` | Various connection options described below | |
@@ -122,6 +122,7 @@ The connection settings match those provided by [Fog](https://github.com/fog), a
| `provider` | Always `AWS` for compatible hosts | AWS |
| `aws_access_key_id` | AWS credentials, or compatible | |
| `aws_secret_access_key` | AWS credentials, or compatible | |
+| `aws_signature_version` | AWS signature version to use. 2 or 4 are valid options. Digital Ocean Spaces and other providers may need 2. | 4 |
| `region` | AWS region | us-east-1 |
| `host` | S3 compatible host for when not using AWS, e.g. `localhost` or `storage.example.com` | s3.amazonaws.com |
| `endpoint` | Can be used when configuring an S3 compatible service such as [Minio](https://www.minio.io), by entering a URL such as `http://127.0.0.1:9000` | (optional) |
diff --git a/doc/administration/job_traces.md b/doc/administration/job_traces.md
index f0b2054a7f3..24d1a3fd151 100644
--- a/doc/administration/job_traces.md
+++ b/doc/administration/job_traces.md
@@ -1,15 +1,29 @@
# Job traces (logs)
-By default, all job traces (logs) are saved to `/var/opt/gitlab/gitlab-ci/builds`
-and `/home/git/gitlab/builds` for Omnibus packages and installations from source
-respectively. The job logs are organized by year and month (for example, `2017_03`),
-and then by project ID.
+Job traces are sent by GitLab Runner while it's processing a job. You can see
+traces in job pages, pipelines, email notifications, etc.
There isn't a way to automatically expire old job logs, but it's safe to remove
them if they're taking up too much space. If you remove the logs manually, the
job output in the UI will be empty.
-## Changing the job traces location
+## Data flow
+
+In general, there are two states in job traces: "live trace" and "archived trace".
+In the following table you can see the phases a trace goes through.
+
+| Phase | State | Condition | Data flow | Stored path |
+| ----- | ----- | --------- | --------- | ----------- |
+| 1: patching | Live trace | When a job is running | GitLab Runner => Unicorn => file storage |`#{ROOT_PATH}/builds/#{YYYY_mm}/#{project_id}/#{job_id}.log`|
+| 2: overwriting | Live trace | When a job is finished | GitLab Runner => Unicorn => file storage |`#{ROOT_PATH}/builds/#{YYYY_mm}/#{project_id}/#{job_id}.log`|
+| 3: archiving | Archived trace | After a job is finished | Sidekiq moves live trace to artifacts folder |`#{ROOT_PATH}/shared/artifacts/#{disk_hash}/#{YYYY_mm_dd}/#{job_id}/#{job_artifact_id}/trace.log`|
+| 4: uploading | Archived trace | After a trace is archived | Sidekiq moves archived trace to [object storage](#uploading-traces-to-object-storage) (if configured) |`#{bucket_name}/#{disk_hash}/#{YYYY_mm_dd}/#{job_id}/#{job_artifact_id}/trace.log`|
+
+The `ROOT_PATH` varies per your environment. For Omnibus GitLab it
+would be `/var/opt/gitlab/gitlab-ci`, whereas for installations from source
+it would be `/home/git/gitlab`.
+
+## Changing the job traces local location
To change the location where the job logs will be stored, follow the steps below.
@@ -41,97 +55,110 @@ To change the location where the job logs will be stored, follow the steps below
[reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure "How to reconfigure Omnibus GitLab"
[restart gitlab]: restart_gitlab.md#installations-from-source "How to restart GitLab"
+## Uploading traces to object storage
+
+An archived trace is considered as a [job artifact](job_artifacts.md).
+Therefore, when you [set up an object storage](job_artifacts.md#object-storage-settings),
+job traces are automatically migrated to it along with the other job artifacts.
+
+See [Data flow](#data-flow) to learn about the process.
+
## New live trace architecture
> [Introduced][ce-18169] in GitLab 10.4.
+> [Announced as General availability][ce-46097] in GitLab 11.0.
-> **Notes**:
-- This feature is still Beta, which could impact GitLab.com/on-premises instances, and in the worst case scenario, traces will be lost.
-- This feature is still being discussed in [an issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/46097) for the performance improvements.
-- This feature is off by default. Please check below how to enable/disable this featrue.
+NOTE: **Note:**
+This feature is off by default. Check below how to [enable/disable](#enabling-live-trace) it.
-**What is "live trace"?**
+By combining the process with object storage settings, we can completely bypass
+the local file storage. This is a useful option if GitLab is installed as
+cloud-native, for example on Kubernetes.
-Job trace that is sent by runner while jobs are running. You can see live trace in job pages UI.
-The live traces are archived once job finishes.
+The data flow is the same as described in the [data flow section](#data-flow)
+with one change: _the stored path of the first two phases is different_. This new live
+trace architecture stores chunks of traces in Redis and a persistent store (object storage or database) instead of
+file storage. Redis is used as first-class storage, and it stores up-to 128KB
+of data. Once the full chunk is sent, it is flushed a persistent store, either object storage(temporary directory) or database.
+After a while, the data in Redis and a persitent store will be archived to [object storage](#uploading-traces-to-object-storage).
-**What is new architecture?**
+The data are stored in the following Redis namespace: `Gitlab::Redis::SharedState`.
-So far, when GitLab Runner sends a job trace to GitLab-Rails, traces have been saved to file storage as text files.
-This was a problem for [Cloud Native-compatible GitLab application](https://gitlab.com/gitlab-com/migration/issues/23) where GitLab had to rely on File Storage.
+Here is the detailed data flow:
-This new live trace architecture stores chunks of traces in Redis and database instead of file storage.
-Redis is used as first-class storage, and it stores up-to 128kB. Once the full chunk is sent it will be flushed to database. Afterwhile, the data in Redis and database will be archived to ObjectStorage.
+1. GitLab Runner picks a job from GitLab
+1. GitLab Runner sends a piece of trace to GitLab
+1. GitLab appends the data to Redis
+1. Once the data in Redis reach 128KB, the data is flushed to a persistent store (object storage or the database).
+1. The above steps are repeated until the job is finished.
+1. Once the job is finished, GitLab schedules a Sidekiq worker to archive the trace.
+1. The Sidekiq worker archives the trace to object storage and cleans up the trace
+ in Redis and a persistent store (object storage or the database).
-Here is the detailed data flow.
+### Enabling live trace
-1. GitLab Runner picks a job from GitLab-Rails
-1. GitLab Runner sends a piece of trace to GitLab-Rails
-1. GitLab-Rails appends the data to Redis
-1. If the data in Redis is fulfilled 128kB, the data is flushed to Database.
-1. 2.~4. is continued until the job is finished
-1. Once the job is finished, GitLab-Rails schedules a sidekiq worker to archive the trace
-1. The sidekiq worker archives the trace to Object Storage, and cleanup the trace in Redis and Database
+The following commands are to be issues in a Rails console:
-**How to check if it's on or off?**
+```sh
+# Omnibus GitLab
+gitlab-rails console
+
+# Installation from source
+cd /home/git/gitlab
+sudo -u git -H bin/rails console RAILS_ENV=production
+```
+
+**To check if live trace is enabled:**
```ruby
Feature.enabled?('ci_enable_live_trace')
```
-**How to enable?**
+**To enable live trace:**
```ruby
Feature.enable('ci_enable_live_trace')
```
->**Note:**
-The transition period will be handled gracefully. Upcoming traces will be generated with the new architecture, and on-going live traces will stay with the legacy architecture (i.e. on-going live traces won't be re-generated forcibly with the new architecture).
+NOTE: **Note:**
+The transition period will be handled gracefully. Upcoming traces will be
+generated with the new architecture, and on-going live traces will stay with the
+legacy architecture, which means that on-going live traces won't be forcibly
+re-generated with the new architecture.
-**How to disable?**
+**To disable live trace:**
```ruby
Feature.disable('ci_enable_live_trace')
```
->**Note:**
-The transition period will be handled gracefully. Upcoming traces will be generated with the legacy architecture, and on-going live traces will stay with the new architecture (i.e. on-going live traces won't be re-generated forcibly with the legacy architecture).
-
-**Redis namespace:**
-
-`Gitlab::Redis::SharedState`
-
-**Potential impact:**
+NOTE: **Note:**
+The transition period will be handled gracefully. Upcoming traces will be generated
+with the legacy architecture, and on-going live traces will stay with the new
+architecture, which means that on-going live traces won't be forcibly re-generated
+with the legacy architecture.
-- This feature could incur data loss:
- - Case 1: When all data in Redis are accidentally flushed.
- - On-going live traces could be recovered by re-sending traces (This is supported by all versions of GitLab Runner)
- - Finished jobs which has not archived live traces will lose the last part (~128kB) of trace data.
- - Case 2: When sidekiq workers failed to archive (e.g. There was a bug that prevents archiving process, Sidekiq inconsistancy, etc):
- - Currently all trace data in Redis will be deleted after one week. If the sidekiq workers can't finish by the expiry date, the part of trace data will be lost.
-- This feature could consume all memory on Redis instance. If the number of jobs is 1000, 128MB (128kB * 1000) is consumed.
-- This feature could pressure Database replication lag. `INSERT` are generated to indicate that we have trace chunk. `UPDATE` with 128kB of data is issued once we receive multiple chunks.
-- and so on
+### Potential implications
-**How to test?**
+In some cases, having data stored on Redis could incur data loss:
-We're currently evaluating this feature on dev.gitalb.org or staging.gitlab.com to verify this features. Here is the list of tests/measurements.
+1. **Case 1: When all data in Redis are accidentally flushed**
+ - On going live traces could be recovered by re-sending traces (this is
+ supported by all versions of the GitLab Runner).
+ - Finished jobs which have not archived live traces will lose the last part
+ (~128KB) of trace data.
-- Features:
- - Live traces should be visible on job pages
- - Archived traces should be visible on job pages
- - Live traces should be archived to Object storage
- - Live traces should be cleaned up after archived
- - etc
-- Performance:
- - Schedule 1000~10000 jobs and let GitLab-runners process concurrently. Measure memoery presssure, IO load, etc.
- - etc
-- Failover:
- - Simulate Redis outage
- - etc
+1. **Case 2: When Sidekiq workers fail to archive (e.g., there was a bug that
+ prevents archiving process, Sidekiq inconsistency, etc.)**
+ - Currently all trace data in Redis will be deleted after one week. If the
+ Sidekiq workers can't finish by the expiry date, the part of trace data will be lost.
-**How to verify the correctnesss?**
+Another issue that might arise is that it could consume all memory on the Redis
+instance. If the number of jobs is 1000, 128MB (128KB * 1000) is consumed.
-- TBD
+Also, it could pressure the database replication lag. `INSERT`s are generated to
+indicate that we have trace chunk. `UPDATE`s with 128KB of data is issued once we
+receive multiple chunks.
-[ce-44935]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18169
+[ce-18169]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18169
+[ce-46097]: https://gitlab.com/gitlab-org/gitlab-ce/issues/46097
diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md
index 411a0fae93f..c0e98099e42 100644
--- a/doc/administration/monitoring/prometheus/gitlab_metrics.md
+++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md
@@ -48,6 +48,22 @@ The following metrics are available:
| filesystem_circuitbreaker_latency_seconds | Gauge | 9.5 | Time spent validating if a storage is accessible |
| filesystem_circuitbreaker | Gauge | 9.5 | Whether or not the circuit for a certain shard is broken or not |
| circuitbreaker_storage_check_duration_seconds | Histogram | 10.3 | Time a single storage probe took |
+| failed_login_captcha_total | Gauge | 11.0 | Counter of failed CAPTCHA attempts during login |
+| successful_login_captcha_total | Gauge | 11.0 | Counter of successful CAPTCHA attempts during login |
+
+### Ruby metrics
+
+Some basic Ruby runtime metrics are available:
+
+| Metric | Type | Since | Description |
+|:-------------------------------------- |:--------- |:----- |:----------- |
+| ruby_gc_duration_seconds_total | Counter | 11.1 | Time spent by Ruby in GC |
+| ruby_gc_stat_... | Gauge | 11.1 | Various metrics from [GC.stat] |
+| ruby_file_descriptors | Gauge | 11.1 | File descriptors per process |
+| ruby_memory_bytes | Gauge | 11.1 | Memory usage by process |
+| ruby_sampler_duration_seconds_total | Counter | 11.1 | Time spent collecting stats |
+
+[GC.stat]: https://ruby-doc.org/core-2.3.0/GC.html#method-c-stat
## Metrics shared directory
diff --git a/doc/administration/monitoring/prometheus/index.md b/doc/administration/monitoring/prometheus/index.md
index f47add48345..1c79e86dcb4 100644
--- a/doc/administration/monitoring/prometheus/index.md
+++ b/doc/administration/monitoring/prometheus/index.md
@@ -29,7 +29,8 @@ For installations from source you'll have to install and configure it yourself.
Prometheus and it's exporters are on by default, starting with GitLab 9.0.
Prometheus will run as the `gitlab-prometheus` user and listen on
-`http://localhost:9090`. Each exporter will be automatically be set up as a
+`http://localhost:9090`. By default Prometheus is only accessible from the GitLab server itself.
+Each exporter will be automatically set up as a
monitoring target for Prometheus, unless individually disabled.
To disable Prometheus and all of its exporters, as well as any added in the future:
@@ -44,14 +45,16 @@ To disable Prometheus and all of its exporters, as well as any added in the futu
1. Save the file and [reconfigure GitLab][reconfigure] for the changes to
take effect
-## Changing the port Prometheus listens on
+## Changing the port and address Prometheus listens on
>**Note:**
The following change was added in [GitLab Omnibus 8.17][1261]. Although possible,
-it's not recommended to change the default address and port Prometheus listens
+it's not recommended to change the port Prometheus listens
on as this might affect or conflict with other services running on the GitLab
server. Proceed at your own risk.
+In order to access Prometheus from outside the GitLab server you will need to
+set a FQDN or IP in `prometheus['listen_address']`.
To change the address/port that Prometheus listens on:
1. Edit `/etc/gitlab/gitlab.rb`
@@ -80,9 +83,9 @@ You can visit `http://localhost:9090` for the dashboard that Prometheus offers b
>**Note:**
If SSL has been enabled on your GitLab instance, you may not be able to access
-Prometheus on the same browser as GitLab due to [HSTS][hsts]. We plan to
+Prometheus on the same browser as GitLab if using the same FQDN due to [HSTS][hsts]. We plan to
[provide access via GitLab][multi-user-prometheus], but in the interim there are
-some workarounds: using a separate browser for Prometheus, resetting HSTS, or
+some workarounds: using a separate FQDN, using server IP, using a separate browser for Prometheus, resetting HSTS, or
having [Nginx proxy it][nginx-custom-config].
The performance data collected by Prometheus can be viewed directly in the
diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md
index c0221533f13..b3602bc35ab 100644
--- a/doc/administration/pages/index.md
+++ b/doc/administration/pages/index.md
@@ -49,8 +49,8 @@ supporting custom domains a secondary IP is not needed.
Before proceeding with the Pages configuration, you will need to:
-1. Have a separate domain under which the GitLab Pages will be served. In this
- document we assume that to be `example.io`.
+1. Have an exclusive root domain for serving GitLab Pages. Note that you cannot
+ use a subdomain of your GitLab's instance domain.
1. Configure a **wildcard DNS record**.
1. (Optional) Have a **wildcard certificate** for that domain if you decide to
serve Pages under HTTPS.
@@ -83,12 +83,12 @@ you need to add a [wildcard DNS A record][wiki-wildcard-dns] pointing to the
host that GitLab runs. For example, an entry would look like this:
```
-*.example.io. 1800 IN A 1.1.1.1
+*.example.io. 1800 IN A 192.0.2.1
*.example.io. 1800 IN AAAA 2001::1
```
where `example.io` is the domain under which GitLab Pages will be served
-and `1.1.1.1` is the IPv4 address of your GitLab instance and `2001::1` is the
+and `192.0.2.1` is the IPv4 address of your GitLab instance and `2001::1` is the
IPv6 address. If you don't have IPv6, you can omit the AAAA record.
> **Note:**
@@ -193,13 +193,13 @@ world. Custom domains are supported, but no TLS.
```shell
pages_external_url "http://example.io"
- nginx['listen_addresses'] = ['1.1.1.1']
+ nginx['listen_addresses'] = ['192.0.2.1']
pages_nginx['enable'] = false
- gitlab_pages['external_http'] = ['1.1.1.2:80', '[2001::2]:80']
+ gitlab_pages['external_http'] = ['192.0.2.2:80', '[2001::2]:80']
```
- where `1.1.1.1` is the primary IP address that GitLab is listening to and
- `1.1.1.2` and `2001::2` are the secondary IPs the GitLab Pages daemon
+ where `192.0.2.1` is the primary IP address that GitLab is listening to and
+ `192.0.2.2` and `2001::2` are the secondary IPs the GitLab Pages daemon
listens on. If you don't have IPv6, you can omit the IPv6 address.
1. [Reconfigure GitLab][reconfigure]
@@ -228,16 +228,16 @@ world. Custom domains and TLS are supported.
```shell
pages_external_url "https://example.io"
- nginx['listen_addresses'] = ['1.1.1.1']
+ nginx['listen_addresses'] = ['192.0.2.1']
pages_nginx['enable'] = false
gitlab_pages['cert'] = "/etc/gitlab/ssl/example.io.crt"
gitlab_pages['cert_key'] = "/etc/gitlab/ssl/example.io.key"
- gitlab_pages['external_http'] = ['1.1.1.2:80', '[2001::2]:80']
- gitlab_pages['external_https'] = ['1.1.1.2:443', '[2001::2]:443']
+ gitlab_pages['external_http'] = ['192.0.2.2:80', '[2001::2]:80']
+ gitlab_pages['external_https'] = ['192.0.2.2:443', '[2001::2]:443']
```
- where `1.1.1.1` is the primary IP address that GitLab is listening to and
- `1.1.1.2` and `2001::2` are the secondary IPs where the GitLab Pages daemon
+ where `192.0.2.1` is the primary IP address that GitLab is listening to and
+ `192.0.2.2` and `2001::2` are the secondary IPs where the GitLab Pages daemon
listens on. If you don't have IPv6, you can omit the IPv6 address.
1. [Reconfigure GitLab][reconfigure]
@@ -259,6 +259,24 @@ verification requirement. Navigate to `Admin area âž” Settings` and uncheck
**Require users to prove ownership of custom domains** in the Pages section.
This setting is enabled by default.
+## Activate verbose logging for daemon
+
+Verbose logging was [introduced](https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests/2533) in
+Omnibus GitLab 11.1.
+
+Follow the steps below to configure verbose logging of GitLab Pages daemon.
+
+1. By default the daemon only logs with `INFO` level.
+
+ If you wish to make it log events with level `DEBUG` you must configure this in
+ `/etc/gitlab/gitlab.rb`:
+
+ ```shell
+ gitlab_pages['log_verbose'] = true
+ ```
+
+1. [Reconfigure GitLab][reconfigure]
+
## Change storage path
Follow the steps below to change the default path where GitLab Pages' contents
diff --git a/doc/administration/pages/source.md b/doc/administration/pages/source.md
index a45c3306457..4e40a7cb18d 100644
--- a/doc/administration/pages/source.md
+++ b/doc/administration/pages/source.md
@@ -67,11 +67,11 @@ you need to add a [wildcard DNS A record][wiki-wildcard-dns] pointing to the
host that GitLab runs. For example, an entry would look like this:
```
-*.example.io. 1800 IN A 1.1.1.1
+*.example.io. 1800 IN A 192.0.2.1
```
where `example.io` is the domain under which GitLab Pages will be served
-and `1.1.1.1` is the IP address of your GitLab instance.
+and `192.0.2.1` is the IP address of your GitLab instance.
> **Note:**
You should not use the GitLab domain to serve user pages. For more information
@@ -253,7 +253,7 @@ world. Custom domains are supported, but no TLS.
port: 80
https: false
- external_http: 1.1.1.2:80
+ external_http: 192.0.2.2:80
```
1. Edit `/etc/default/gitlab` and set `gitlab_pages_enabled` to `true` in
@@ -263,7 +263,7 @@ world. Custom domains are supported, but no TLS.
```
gitlab_pages_enabled=true
- gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 1.1.1.2:80"
+ gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 192.0.2.2:80"
```
1. Copy the `gitlab-pages-ssl` Nginx configuration file:
@@ -274,7 +274,7 @@ world. Custom domains are supported, but no TLS.
```
1. Edit all GitLab related configs in `/etc/nginx/site-available/` and replace
- `0.0.0.0` with `1.1.1.1`, where `1.1.1.1` the primary IP where GitLab
+ `0.0.0.0` with `192.0.2.1`, where `192.0.2.1` the primary IP where GitLab
listens to.
1. Restart NGINX
1. [Restart GitLab][restart]
@@ -320,8 +320,8 @@ world. Custom domains and TLS are supported.
port: 443
https: true
- external_http: 1.1.1.2:80
- external_https: 1.1.1.2:443
+ external_http: 192.0.2.2:80
+ external_https: 192.0.2.2:443
```
1. Edit `/etc/default/gitlab` and set `gitlab_pages_enabled` to `true` in
@@ -333,7 +333,7 @@ world. Custom domains and TLS are supported.
```
gitlab_pages_enabled=true
- gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 1.1.1.2:80 -listen-https 1.1.1.2:443 -root-cert /path/to/example.io.crt -root-key /path/to/example.io.key
+ gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 192.0.2.2:80 -listen-https 192.0.2.2:443 -root-cert /path/to/example.io.crt -root-key /path/to/example.io.key
```
1. Copy the `gitlab-pages-ssl` Nginx configuration file:
@@ -344,7 +344,7 @@ world. Custom domains and TLS are supported.
```
1. Edit all GitLab related configs in `/etc/nginx/site-available/` and replace
- `0.0.0.0` with `1.1.1.1`, where `1.1.1.1` the primary IP where GitLab
+ `0.0.0.0` with `192.0.2.1`, where `192.0.2.1` the primary IP where GitLab
listens to.
1. Restart NGINX
1. [Restart GitLab][restart]
diff --git a/doc/administration/raketasks/check.md b/doc/administration/raketasks/check.md
index 7d34d35e7d1..2649bf61d74 100644
--- a/doc/administration/raketasks/check.md
+++ b/doc/administration/raketasks/check.md
@@ -78,9 +78,10 @@ Example output:
## Uploaded Files Integrity
-Various types of file can be uploaded to a GitLab installation by users.
-Checksums are generated and stored in the database upon upload, and integrity
-checks using those checksums can be run. These checks also detect missing files.
+Various types of files can be uploaded to a GitLab installation by users.
+These integrity checks can detect missing files. Additionally, for locally
+stored files, checksums are generated and stored in the database upon upload,
+and these checks will verify them against current files.
Currently, integrity checks are supported for the following types of file:
diff --git a/doc/administration/raketasks/project_import_export.md b/doc/administration/raketasks/project_import_export.md
index ecc4ac6b29b..7bd765a35e0 100644
--- a/doc/administration/raketasks/project_import_export.md
+++ b/doc/administration/raketasks/project_import_export.md
@@ -30,5 +30,12 @@ sudo gitlab-rake gitlab:import_export:data
bundle exec rake gitlab:import_export:data RAILS_ENV=production
```
+In order to enable Object Storage on the Export, you can use the [feature flag][feature-flags]:
+
+```
+import_export_object_storage
+```
+
[ce-3050]: https://gitlab.com/gitlab-org/gitlab-ce/issues/3050
+[feature-flags]: https://docs.gitlab.com/ee/api/features.html
[tmp]: ../../development/shared_files.md
diff --git a/doc/administration/raketasks/storage.md b/doc/administration/raketasks/storage.md
index 6ec5baeb6e3..7ad38abe4f5 100644
--- a/doc/administration/raketasks/storage.md
+++ b/doc/administration/raketasks/storage.md
@@ -17,14 +17,21 @@ This task will schedule all your existing projects and attachments associated wi
**Omnibus Installation**
```bash
-gitlab-rake gitlab:storage:migrate_to_hashed
+sudo gitlab-rake gitlab:storage:migrate_to_hashed
```
**Source Installation**
```bash
-rake gitlab:storage:migrate_to_hashed
+sudo -u git -H bundle exec rake gitlab:storage:migrate_to_hashed RAILS_ENV=production
+```
+
+They both also accept a range as environment variable:
+```bash
+# to migrate any non migrated project from ID 20 to 50.
+export ID_FROM=20
+export ID_TO=50
```
You can monitor the progress in the _Admin > Monitoring > Background jobs_ screen.
@@ -45,14 +52,13 @@ To have a simple summary of projects using **Legacy** storage:
**Omnibus Installation**
```bash
-gitlab-rake gitlab:storage:legacy_projects
+sudo gitlab-rake gitlab:storage:legacy_projects
```
**Source Installation**
```bash
-rake gitlab:storage:legacy_projects
-
+sudo -u git -H bundle exec rake gitlab:storage:legacy_projects RAILS_ENV=production
```
------
@@ -62,13 +68,13 @@ To list projects using **Legacy** storage:
**Omnibus Installation**
```bash
-gitlab-rake gitlab:storage:list_legacy_projects
+sudo gitlab-rake gitlab:storage:list_legacy_projects
```
**Source Installation**
```bash
-rake gitlab:storage:list_legacy_projects
+sudo -u git -H bundle exec rake gitlab:storage:list_legacy_projects RAILS_ENV=production
```
@@ -79,14 +85,13 @@ To have a simple summary of projects using **Hashed** storage:
**Omnibus Installation**
```bash
-gitlab-rake gitlab:storage:hashed_projects
+sudo gitlab-rake gitlab:storage:hashed_projects
```
**Source Installation**
```bash
-rake gitlab:storage:hashed_projects
-
+sudo -u git -H bundle exec rake gitlab:storage:hashed_projects RAILS_ENV=production
```
------
@@ -96,14 +101,13 @@ To list projects using **Hashed** storage:
**Omnibus Installation**
```bash
-gitlab-rake gitlab:storage:list_hashed_projects
+sudo gitlab-rake gitlab:storage:list_hashed_projects
```
**Source Installation**
```bash
-rake gitlab:storage:list_hashed_projects
-
+sudo -u git -H bundle exec rake gitlab:storage:list_hashed_projects RAILS_ENV=production
```
## List attachments on Legacy storage
@@ -113,14 +117,13 @@ To have a simple summary of project attachments using **Legacy** storage:
**Omnibus Installation**
```bash
-gitlab-rake gitlab:storage:legacy_attachments
+sudo gitlab-rake gitlab:storage:legacy_attachments
```
**Source Installation**
```bash
-rake gitlab:storage:legacy_attachments
-
+sudo -u git -H bundle exec rake gitlab:storage:legacy_attachments RAILS_ENV=production
```
------
@@ -130,14 +133,13 @@ To list project attachments using **Legacy** storage:
**Omnibus Installation**
```bash
-gitlab-rake gitlab:storage:list_legacy_attachments
+sudo gitlab-rake gitlab:storage:list_legacy_attachments
```
**Source Installation**
```bash
-rake gitlab:storage:list_legacy_attachments
-
+sudo -u git -H bundle exec rake gitlab:storage:list_legacy_attachments RAILS_ENV=production
```
## List attachments on Hashed storage
@@ -147,14 +149,13 @@ To have a simple summary of project attachments using **Hashed** storage:
**Omnibus Installation**
```bash
-gitlab-rake gitlab:storage:hashed_attachments
+sudo gitlab-rake gitlab:storage:hashed_attachments
```
**Source Installation**
```bash
-rake gitlab:storage:hashed_attachments
-
+sudo -u git -H bundle exec rake gitlab:storage:hashed_attachments RAILS_ENV=production
```
------
@@ -164,14 +165,13 @@ To list project attachments using **Hashed** storage:
**Omnibus Installation**
```bash
-gitlab-rake gitlab:storage:list_hashed_attachments
+sudo gitlab-rake gitlab:storage:list_hashed_attachments
```
**Source Installation**
```bash
-rake gitlab:storage:list_hashed_attachments
-
+sudo -u git -H bundle exec rake gitlab:storage:list_hashed_attachments RAILS_ENV=production
```
[storage-types]: ../repository_storage_types.md
diff --git a/doc/administration/repository_storage_types.md b/doc/administration/repository_storage_types.md
index 39bd19ac851..087fe729b28 100644
--- a/doc/administration/repository_storage_types.md
+++ b/doc/administration/repository_storage_types.md
@@ -82,6 +82,46 @@ To migrate your existing projects to the new storage type, check the specific
[rake tasks]: raketasks/storage.md#migrate-existing-projects-to-hashed-storage
[storage-paths]: repository_storage_types.md
+#### Rollback
+
+There is no automated rollback implemented. Below are the steps required to rollback
+from each storage migration.
+
+The rollback has to be performed in the reverse order. To get into "Legacy" state,
+you need to rollback Attachments first, then Project.
+
+Also note that if Geo is enabled, after the migration was triggered, an event is generated
+to replicate the operation on any Secondary node. That means the on disk changes will also
+need to be performed on these nodes as well. Database changes will propagate without issues.
+
+You must make sure the migration event was already processed or otherwise it may migrate
+the files back to Hashed state again.
+
+##### Attachments
+
+To rollback single Attachment migration, rename `aa/bb/abcdef1234567890...` folder back to `namespace/project`.
+
+Both folder names can be generated by the `FileUploader.absolute_base_dir(project)`, you
+just need to switch the version from the `project` back to the previous one.
+
+```ruby
+project.storage_version
+# => 2
+
+FileUploader.absolute_base_dir(project)
+# => "/opt/gitlab/embedded/service/gitlab-rails/public/uploads/@hashed/d4/73/d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35"
+
+project.storage_version = 1
+
+FileUploader.absolute_base_dir(project)
+# => "/opt/gitlab/embedded/service/gitlab-rails/public/uploads/gitlab/gitlab-shell-renamed"
+```
+
+##### Project
+
+To rollback single Project migration, move `@hashed/aa/bb/aabbcdef1234567890abcdef.git` and `@hashed/aa/bb/aabbcdef1234567890abcdef.wiki.git`
+back to `namespace/project.git` and `namespace/project.wiki.git` respectively and switch the version from the `project` back to `null`.
+
### Hashed Storage coverage
We are incrementally moving every storable object in GitLab to the Hashed
@@ -100,6 +140,30 @@ which is true for CI Cache and LFS Objects.
| Pages | Yes | No | - | - |
| Docker Registry | Yes | No | - | - |
| CI Build Logs | No | No | - | - |
-| CI Artifacts | No | No | Yes (Premium) | - |
+| CI Artifacts | No | No | Yes | 9.4 / 10.6 |
| CI Cache | No | No | Yes | - |
-| LFS Objects | Yes | No | Yes (Premium) | - |
+| LFS Objects | Yes | Similar | Yes | 10.0 / 10.7 |
+
+#### Implementation Details
+
+##### Avatars
+
+Each file is stored in a folder with its `id` from the database. The filename is always `avatar.png` for user avatars.
+When avatar is replaced, `Upload` model is destroyed and a new one takes place with different `id`.
+
+##### CI Artifacts
+
+CI Artifacts are S3 compatible since **9.4** (GitLab Premium), and available in GitLab Core since **10.6**.
+
+##### LFS Objects
+
+LFS Objects implements a similar storage pattern using 2 chars, 2 level folders, following git own implementation:
+
+```ruby
+"shared/lfs-objects/#{oid[0..1}/#{oid[2..3]}/#{oid[4..-1]}"
+
+# Based on object `oid`: `8909029eb962194cfb326259411b22ae3f4a814b5be4f80651735aeef9f3229c`, path will be:
+"shared/lfs-objects/89/09/029eb962194cfb326259411b22ae3f4a814b5be4f80651735aeef9f3229c"
+```
+
+They are also S3 compatible since **10.0** (GitLab Premium), and available in GitLab Core since **10.7**.
diff --git a/doc/administration/uploads.md b/doc/administration/uploads.md
index 7f0bd8f04e3..85eca403253 100644
--- a/doc/administration/uploads.md
+++ b/doc/administration/uploads.md
@@ -52,6 +52,7 @@ _The uploads are stored by default in
>**Notes:**
- [Introduced][ee-3867] in [GitLab Enterprise Edition Premium][eep] 10.5.
+- Since version 11.1, we support direct_upload to S3.
If you don't want to use the local disk where GitLab is installed to store the
uploads, you can use an object storage provider like AWS S3 instead.
@@ -65,7 +66,7 @@ For source installations the following settings are nested under `uploads:` and
|---------|-------------|---------|
| `enabled` | Enable/disable object storage | `false` |
| `remote_directory` | The bucket name where Uploads will be stored| |
-| `direct_upload` | Set to true to enable direct upload of Uploads without the need of local shared storage. Option may be removed once we decide to support only single storage for all files. This is beta option as it uses inefficient way of uploading data (via Unicorn). The accelerated uploads gonna be implemented in future releases | `false` |
+| `direct_upload` | Set to true to enable direct upload of Uploads without the need of local shared storage. Option may be removed once we decide to support only single storage for all files. | `false` |
| `background_upload` | Set to false to disable automatic upload. Option may be removed once upload is direct to S3 | `true` |
| `proxy_download` | Set to true to enable proxying all files served. Option allows to reduce egress traffic as this allows clients to download directly from remote storage instead of proxying all data | `false` |
| `connection` | Various connection options described below | |
@@ -79,6 +80,7 @@ The connection settings match those provided by [Fog](https://github.com/fog), a
| `provider` | Always `AWS` for compatible hosts | AWS |
| `aws_access_key_id` | AWS credentials, or compatible | |
| `aws_secret_access_key` | AWS credentials, or compatible | |
+| `aws_signature_version` | AWS signature version to use. 2 or 4 are valid options. Digital Ocean Spaces and other providers may need 2. | 4 |
| `region` | AWS region | us-east-1 |
| `host` | S3 compatible host for when not using AWS, e.g. `localhost` or `storage.example.com` | s3.amazonaws.com |
| `endpoint` | Can be used when configuring an S3 compatible service such as [Minio](https://www.minio.io), by entering a URL such as `http://127.0.0.1:9000` | (optional) |
diff --git a/doc/api/README.md b/doc/api/README.md
index 194907accc7..6267618d3bc 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -90,24 +90,23 @@ specification.
## Compatibility Guidelines
The HTTP API is versioned using a single number, the current one being 4. This
-number symbolises the same as the major version number as described by
+number symbolises the same as the major version number as described by
[SemVer](https://semver.org/). This mean that backward incompatible changes
will require this version number to change. However, the minor version is
-not explicit. This allows for a stable API endpoint, but also means new
+not explicit. This allows for a stable API endpoint, but also means new
features can be added to the API in the same version number.
New features and bug fixes are released in tandem with a new GitLab, and apart
from incidental patch and security releases, are released on the 22nd each
-month. Backward incompatible changes (e.g. endpoints removal, parameters
-removal etc.), as well as removal of entire API versions are done in tandem
-with a major point release of GitLab itself. All deprecations and changes
-between two versions should be listed in the documentation. For the changes
+month. Backward incompatible changes (e.g. endpoints removal, parameters
+removal etc.), as well as removal of entire API versions are done in tandem
+with a major point release of GitLab itself. All deprecations and changes
+between two versions should be listed in the documentation. For the changes
between v3 and v4; please read the [v3 to v4 documentation](v3_to_v4.md)
-#### Current status
+### Current status
-Currently two API versions are available, v3 and v4. v3 is deprecated and
-will soon be removed. Deletion is scheduled for
+Currently only API version v4 is available. Version v3 was removed in
[GitLab 11.0](https://gitlab.com/gitlab-org/gitlab-ce/issues/36819).
## Basic usage
diff --git a/doc/api/access_requests.md b/doc/api/access_requests.md
index 603fa4a8194..4b2014ca843 100644
--- a/doc/api/access_requests.md
+++ b/doc/api/access_requests.md
@@ -10,7 +10,7 @@
10 => Guest access
20 => Reporter access
30 => Developer access
-40 => Master access
+40 => Maintainer access
50 => Owner access # Only valid for groups
```
diff --git a/doc/api/applications.md b/doc/api/applications.md
index 933867ed0bb..6d244594b71 100644
--- a/doc/api/applications.md
+++ b/doc/api/applications.md
@@ -23,7 +23,7 @@ POST /applications
| `scopes` | string | yes | The scopes of the application |
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "name=MyApplication&redirect_uri=http://redirect.uri&scopes=" https://gitlab.example.com/api/v3/applications
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "name=MyApplication&redirect_uri=http://redirect.uri&scopes=" https://gitlab.example.com/api/v4/applications
```
Example response:
diff --git a/doc/api/avatar.md b/doc/api/avatar.md
new file mode 100644
index 00000000000..7faed893066
--- /dev/null
+++ b/doc/api/avatar.md
@@ -0,0 +1,33 @@
+# Avatar API
+
+> [Introduced][ce-19121] in GitLab 11.0
+
+## Get a single avatar URL
+
+Get a single avatar URL for a given email addres. If user with matching public
+email address is not found, results from external avatar services are returned.
+This endpoint can be accessed without authentication. In case public visibility
+is restricted, response will be `403 Forbidden` when unauthenticated.
+
+```
+GET /avatar?email=admin@example.com
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ------- | -------- | --------------------- |
+| `email` | string | yes | Public email address of the user |
+| `size` | integer | no | Single pixel dimension (since images are squares). Only used for avatar lookups at `Gravatar` or at the configured `Libravatar` server |
+
+```bash
+curl https://gitlab.example.com/api/v4/avatar?email=admin@example.com
+```
+
+Example response:
+
+```json
+{
+ "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon"
+}
+```
+
+[ce-19121]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/19121
diff --git a/doc/api/branches.md b/doc/api/branches.md
index 01bb30c3859..bfb21608d28 100644
--- a/doc/api/branches.md
+++ b/doc/api/branches.md
@@ -29,6 +29,7 @@ Example response:
"protected": true,
"developers_can_push": false,
"developers_can_merge": false,
+ "can_push": true,
"commit": {
"author_email": "john@example.com",
"author_name": "John Smith",
@@ -76,6 +77,7 @@ Example response:
"protected": true,
"developers_can_push": false,
"developers_can_merge": false,
+ "can_push": true,
"commit": {
"author_email": "john@example.com",
"author_name": "John Smith",
@@ -140,7 +142,8 @@ Example response:
"merged": false,
"protected": true,
"developers_can_push": true,
- "developers_can_merge": true
+ "developers_can_merge": true,
+ "can_push": true
}
```
@@ -188,7 +191,8 @@ Example response:
"merged": false,
"protected": false,
"developers_can_push": false,
- "developers_can_merge": false
+ "developers_can_merge": false,
+ "can_push": true
}
```
@@ -231,7 +235,8 @@ Example response:
"merged": false,
"protected": false,
"developers_can_push": false,
- "developers_can_merge": false
+ "developers_can_merge": false,
+ "can_push": true
}
```
diff --git a/doc/api/commits.md b/doc/api/commits.md
index d1584cf64de..d07b9d5614a 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -16,6 +16,7 @@ GET /projects/:id/repository/commits
| `until` | string | no | Only commits before or on this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ |
| `path` | string | no | The file path |
| `all` | boolean | no | Retrieve every commit from the repository |
+| `with_stats` | boolean | no | Stats about each commit will be added to the response |
```bash
diff --git a/doc/api/environments.md b/doc/api/environments.md
index 6e20781f51a..29da4590a59 100644
--- a/doc/api/environments.md
+++ b/doc/api/environments.md
@@ -123,7 +123,7 @@ POST /projects/:id/environments/:environment_id/stop
| `environment_id` | integer | yes | The ID of the environment |
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environments/1/stop"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/environments/1/stop"
```
Example response:
diff --git a/doc/api/graphql/index.md b/doc/api/graphql/index.md
new file mode 100644
index 00000000000..71922318227
--- /dev/null
+++ b/doc/api/graphql/index.md
@@ -0,0 +1,40 @@
+# GraphQL API (Alpha)
+
+> [Introduced][ce-19008] in GitLab 11.0.
+
+[GraphQL](https://graphql.org/) is a query language for APIs that
+allows clients to request exactly the data they need, making it
+possible to get all required data in a limited number of requests.
+
+The GraphQL data (fields) can be described in the form of types,
+allowing clients to use [clientside GraphQL
+libraries](https://graphql.org/code/#graphql-clients) to consume the
+API and avoid manual parsing.
+
+Since there's no fixed endpoints and datamodel, new abilities can be
+added to the API without creating breaking changes. This allows us to
+have a versionless API as described in [the GraphQL
+documentation](https://graphql.org/learn/best-practices/#versioning).
+
+## Enabling the GraphQL feature
+
+The GraphQL API itself is currently in Alpha, and therefore hidden behind a
+feature flag. You can enable the feature using the [features api][features-api] on a self-hosted instance.
+
+For example:
+
+```shell
+curl --data "value=100" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/features/graphql
+```
+
+## Available queries
+
+A first iteration of a GraphQL API includes a query for: `project`. Within a project it is also possible to fetch a `mergeRequest` by IID.
+
+## GraphiQL
+
+The API can be explored by using the GraphiQL IDE, it is available on your
+instance on `gitlab.example.com/-/graphql-explorer`.
+
+[ce-19008]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/19008
+[features-api]: ../features.md
diff --git a/doc/api/groups.md b/doc/api/groups.md
index 96842ef330f..11de75039ee 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -12,7 +12,7 @@ Parameters:
| `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, `true` for admin) |
| `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` |
+| `order_by` | string | no | Order groups by `name`, `path` or `id`. Default is `name` |
| `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` |
| `statistics` | boolean | no | Include group statistics (admins only) |
| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) |
@@ -96,7 +96,7 @@ Parameters:
| `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, `true` for admin) |
| `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` |
+| `order_by` | string | no | Order groups by `name`, `path` or `id`. Default is `name` |
| `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` |
| `statistics` | boolean | no | Include group statistics (admins only) |
| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) |
@@ -147,6 +147,8 @@ Parameters:
| `simple` | boolean | no | Return only the ID, URL, name, and path of each project |
| `owned` | boolean | no | Limit by projects owned by the current user |
| `starred` | boolean | no | Limit by projects starred by the current user |
+| `with_issues_enabled` | boolean | no | Limit by enabled issues feature |
+| `with_merge_requests_enabled` | boolean | no | Limit by enabled merge requests feature |
| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) |
Example response:
@@ -208,6 +210,7 @@ Parameters:
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) |
+| `with_projects` | boolean | no | Include details from projects that belong to the specified group (defaults to `true`). |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/4
@@ -359,6 +362,30 @@ Example response:
}
```
+When adding the parameter `with_projects=false`, projects will not be returned.
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/4?with_projects=false
+```
+
+Example response:
+
+```json
+{
+ "id": 4,
+ "name": "Twitter",
+ "path": "twitter",
+ "description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.",
+ "visibility": "public",
+ "avatar_url": null,
+ "web_url": "https://gitlab.example.com/groups/twitter",
+ "request_access_enabled": false,
+ "full_name": "Twitter",
+ "full_path": "twitter",
+ "parent_id": null
+}
+```
+
## New group
Creates a new project group. Available only for users who can create groups.
diff --git a/doc/api/issues.md b/doc/api/issues.md
index d0063e0b8a2..5613cb6d915 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -467,7 +467,7 @@ POST /projects/:id/issues
| `description` | string | no | The description of an issue |
| `confidential` | boolean | no | Set an issue to be confidential. Default is `false`. |
| `assignee_ids` | Array[integer] | no | The ID of the users to assign issue |
-| `milestone_id` | integer | no | The ID of a milestone to assign issue |
+| `milestone_id` | integer | no | The global ID of a milestone to assign issue |
| `labels` | string | no | Comma-separated label names for an issue |
| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project owner rights) |
| `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` |
@@ -548,7 +548,7 @@ PUT /projects/:id/issues/:issue_iid
| `description` | string | no | The description of an issue |
| `confidential` | boolean | no | Updates an issue to be confidential |
| `assignee_ids` | Array[integer] | no | The ID of the user(s) to assign the issue to. Set to `0` or provide an empty value to unassign all assignees. |
-| `milestone_id` | integer | no | The ID of a milestone to assign the issue to. Set to `0` or provide an empty value to unassign a milestone.|
+| `milestone_id` | integer | no | The global ID of a milestone to assign the issue to. Set to `0` or provide an empty value to unassign a milestone.|
| `labels` | string | no | Comma-separated label names for an issue. Set to an empty string to unassign all labels. |
| `state_event` | string | no | The state event of an issue. Set `close` to close the issue and `reopen` to reopen it |
| `updated_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project owner rights) |
diff --git a/doc/api/jobs.md b/doc/api/jobs.md
index e4e48edd9a7..cfa5e9a3e95 100644
--- a/doc/api/jobs.md
+++ b/doc/api/jobs.md
@@ -32,14 +32,12 @@ Example of response
"title": "Test the CI integration."
},
"coverage": null,
- "created_at": "2015-12-24T15:51:21.802Z",
- "artifacts_file": {
- "filename": "artifacts.zip",
- "size": 1000
- },
- "finished_at": "2015-12-24T17:54:27.895Z",
- "id": 7,
- "name": "teaspoon",
+ "created_at": "2015-12-24T15:51:21.727Z",
+ "artifacts_file": null,
+ "finished_at": "2015-12-24T17:54:24.921Z",
+ "artifacts_expire_at": "2016-01-23T17:54:24.921Z",
+ "id": 6,
+ "name": "rspec:other",
"pipeline": {
"id": 6,
"ref": "master",
@@ -49,7 +47,7 @@ Example of response
"ref": "master",
"runner": null,
"stage": "test",
- "started_at": "2015-12-24T17:54:27.722Z",
+ "started_at": "2015-12-24T17:54:24.729Z",
"status": "failed",
"tag": false,
"user": {
@@ -78,11 +76,15 @@ Example of response
"title": "Test the CI integration."
},
"coverage": null,
- "created_at": "2015-12-24T15:51:21.727Z",
- "artifacts_file": null,
- "finished_at": "2015-12-24T17:54:24.921Z",
- "id": 6,
- "name": "rspec:other",
+ "created_at": "2015-12-24T15:51:21.802Z",
+ "artifacts_file": {
+ "filename": "artifacts.zip",
+ "size": 1000
+ },
+ "finished_at": "2015-12-24T17:54:27.895Z",
+ "artifacts_expire_at": "2016-01-23T17:54:27.895Z"
+ "id": 7,
+ "name": "teaspoon",
"pipeline": {
"id": 6,
"ref": "master",
@@ -92,7 +94,7 @@ Example of response
"ref": "master",
"runner": null,
"stage": "test",
- "started_at": "2015-12-24T17:54:24.729Z",
+ "started_at": "2015-12-24T17:54:27.722Z",
"status": "failed",
"tag": false,
"user": {
@@ -146,14 +148,12 @@ Example of response
"title": "Test the CI integration."
},
"coverage": null,
- "created_at": "2015-12-24T15:51:21.802Z",
- "artifacts_file": {
- "filename": "artifacts.zip",
- "size": 1000
- },
- "finished_at": "2015-12-24T17:54:27.895Z",
- "id": 7,
- "name": "teaspoon",
+ "created_at": "2015-12-24T15:51:21.727Z",
+ "artifacts_file": null,
+ "finished_at": "2015-12-24T17:54:24.921Z",
+ "artifacts_expire_at": "2016-01-23T17:54:24.921Z"
+ "id": 6,
+ "name": "rspec:other",
"pipeline": {
"id": 6,
"ref": "master",
@@ -163,7 +163,7 @@ Example of response
"ref": "master",
"runner": null,
"stage": "test",
- "started_at": "2015-12-24T17:54:27.722Z",
+ "started_at": "2015-12-24T17:54:24.729Z",
"status": "failed",
"tag": false,
"user": {
@@ -192,11 +192,15 @@ Example of response
"title": "Test the CI integration."
},
"coverage": null,
- "created_at": "2015-12-24T15:51:21.727Z",
- "artifacts_file": null,
- "finished_at": "2015-12-24T17:54:24.921Z",
- "id": 6,
- "name": "rspec:other",
+ "created_at": "2015-12-24T15:51:21.802Z",
+ "artifacts_file": {
+ "filename": "artifacts.zip",
+ "size": 1000
+ },
+ "finished_at": "2015-12-24T17:54:27.895Z",
+ "artifacts_expire_at": "2016-01-23T17:54:27.895Z"
+ "id": 7,
+ "name": "teaspoon",
"pipeline": {
"id": 6,
"ref": "master",
@@ -206,7 +210,7 @@ Example of response
"ref": "master",
"runner": null,
"stage": "test",
- "started_at": "2015-12-24T17:54:24.729Z",
+ "started_at": "2015-12-24T17:54:27.722Z",
"status": "failed",
"tag": false,
"user": {
@@ -261,6 +265,7 @@ Example of response
"created_at": "2015-12-24T15:51:21.880Z",
"artifacts_file": null,
"finished_at": "2015-12-24T17:54:31.198Z",
+ "artifacts_expire_at": "2016-01-23T17:54:31.198Z",
"id": 8,
"name": "rubocop",
"pipeline": {
diff --git a/doc/api/members.md b/doc/api/members.md
index 3234f833eae..8ebe464c359 100644
--- a/doc/api/members.md
+++ b/doc/api/members.md
@@ -8,7 +8,7 @@ The access levels are defined in the `Gitlab::Access` module. Currently, these l
10 => Guest access
20 => Reporter access
30 => Developer access
-40 => Master access
+40 => Maintainer access
50 => Owner access # Only valid for groups
```
@@ -173,3 +173,7 @@ DELETE /projects/:id/members/:user_id
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/:id/members/:user_id
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/members/:user_id
```
+
+## Give a group access to a project
+
+Look at [share project with group](projects.md#share-project-with-group)
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index cbd51c9870c..34c2dd7b34d 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -11,7 +11,7 @@ default it returns only merge requests created by the current user. To
get all merge requests, use parameter `scope=all`.
The `state` parameter can be used to get only merge requests with a
-given state (`opened`, `closed`, or `merged`) or all of them (`all`).
+given state (`opened`, `closed`, `locked`, or `merged`) or all of them (`all`). It should be noted that when searching by `locked` it will mostly return no results as it is a short-lived, transitional state.
The pagination parameters `page` and `per_page` can be used to
restrict the list of merge requests.
@@ -35,7 +35,7 @@ Parameters:
| Attribute | Type | Required | Description |
| ------------------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------------------- |
-| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, or `merged` |
+| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, `locked`, or `merged` |
| `order_by` | string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` |
| `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` |
| `milestone` | string | no | Return merge requests for a specific milestone |
@@ -70,18 +70,18 @@ Parameters:
"author": {
"id": 1,
"username": "admin",
- "email": "admin@example.com",
"name": "Administrator",
"state": "active",
- "created_at": "2012-04-29T08:46:00Z"
+ "avatar_url": null,
+ "web_url" : "https://gitlab.example.com/admin"
},
"assignee": {
"id": 1,
"username": "admin",
- "email": "admin@example.com",
"name": "Administrator",
"state": "active",
- "created_at": "2012-04-29T08:46:00Z"
+ "avatar_url": null,
+ "web_url" : "https://gitlab.example.com/admin"
},
"source_project_id": 2,
"target_project_id": 3,
@@ -107,6 +107,7 @@ Parameters:
"changes_count": "1",
"should_remove_source_branch": true,
"force_remove_source_branch": false,
+ "squash": false,
"web_url": "http://example.com/example/example/merge_requests/1",
"time_stats": {
"time_estimate": 0,
@@ -121,7 +122,7 @@ Parameters:
## List project merge requests
Get all merge requests for this project.
-The `state` parameter can be used to get only merge requests with a given state (`opened`, `closed`, or `merged`) or all of them (`all`).
+The `state` parameter can be used to get only merge requests with a given state (`opened`, `closed`, `locked`, or `merged`) or all of them (`all`).
The pagination parameters `page` and `per_page` can be used to restrict the list of merge requests.
```
@@ -154,7 +155,7 @@ Parameters:
| ------------------- | -------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `id` | integer | yes | The ID of a project |
| `iids[]` | Array[integer] | no | Return the request having the given `iid` |
-| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, or `merged` |
+| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, `locked`, or `merged` |
| `order_by` | string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` |
| `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` |
| `milestone` | string | no | Return merge requests for a specific milestone |
@@ -189,18 +190,125 @@ Parameters:
"author": {
"id": 1,
"username": "admin",
- "email": "admin@example.com",
"name": "Administrator",
"state": "active",
- "created_at": "2012-04-29T08:46:00Z"
+ "avatar_url": null,
+ "web_url" : "https://gitlab.example.com/admin"
},
"assignee": {
"id": 1,
"username": "admin",
- "email": "admin@example.com",
"name": "Administrator",
"state": "active",
- "created_at": "2012-04-29T08:46:00Z"
+ "avatar_url": null,
+ "web_url" : "https://gitlab.example.com/admin"
+ },
+ "source_project_id": 2,
+ "target_project_id": 3,
+ "labels": [ ],
+ "description": "fixed login page css paddings",
+ "work_in_progress": false,
+ "milestone": {
+ "id": 5,
+ "iid": 1,
+ "project_id": 3,
+ "title": "v2.0",
+ "description": "Assumenda aut placeat expedita exercitationem labore sunt enim earum.",
+ "state": "closed",
+ "created_at": "2015-02-02T19:49:26.013Z",
+ "updated_at": "2015-02-02T19:49:26.013Z",
+ "due_date": null
+ },
+ "merge_when_pipeline_succeeds": true,
+ "merge_status": "can_be_merged",
+ "sha": "8888888888888888888888888888888888888888",
+ "merge_commit_sha": null,
+ "user_notes_count": 1,
+ "changes_count": "1",
+ "should_remove_source_branch": true,
+ "force_remove_source_branch": false,
+ "squash": false,
+ "web_url": "http://example.com/example/example/merge_requests/1",
+ "discussion_locked": false,
+ "time_stats": {
+ "time_estimate": 0,
+ "total_time_spent": 0,
+ "human_time_estimate": null,
+ "human_total_time_spent": null
+ }
+ }
+]
+```
+
+## List group merge requests
+
+Get all merge requests for this group and its subgroups.
+The `state` parameter can be used to get only merge requests with a given state (`opened`, `closed`, `locked`, or `merged`) or all of them (`all`).
+The pagination parameters `page` and `per_page` can be used to restrict the list of merge requests.
+
+```
+GET /groups/:id/merge_requests
+GET /groups/:id/merge_requests?state=opened
+GET /groups/:id/merge_requests?state=all
+GET /groups/:id/merge_requests?milestone=release
+GET /groups/:id/merge_requests?labels=bug,reproduced
+GET /groups/:id/merge_requests?my_reaction_emoji=star
+```
+
+`group_id` represents the ID of the group which contains the project where the MR resides.
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| ------------------- | -------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------ |
+| `id` | integer | yes | The ID of a group |
+| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, `locked`, or `merged` |
+| `order_by` | string | no | Return merge requests ordered by `created_at` or `updated_at` fields. Default is `created_at` |
+| `sort` | string | no | Return merge requests sorted in `asc` or `desc` order. Default is `desc` |
+| `milestone` | string | no | Return merge requests for a specific milestone |
+| `view` | string | no | If `simple`, returns the `iid`, URL, title, description, and basic state of merge request |
+| `labels` | string | no | Return merge requests matching a comma separated list of labels |
+| `created_after` | datetime | no | Return merge requests created on or after the given time |
+| `created_before` | datetime | no | Return merge requests created on or before the given time |
+| `updated_after` | datetime | no | Return merge requests updated on or after the given time |
+| `updated_before` | datetime | no | Return merge requests updated on or before the given time |
+| `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> |
+| `author_id` | integer | no | Returns merge requests created by the given user `id` _([Introduced][ce-13060] in GitLab 9.5)_ |
+| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id` _([Introduced][ce-13060] in GitLab 9.5)_ |
+| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ |
+| `source_branch` | string | no | Return merge requests with the given source branch |
+| `target_branch` | string | no | Return merge requests with the given target branch |
+| `search` | string | no | Search merge requests against their `title` and `description` |
+
+```json
+[
+ {
+ "id": 1,
+ "iid": 1,
+ "target_branch": "master",
+ "source_branch": "test1",
+ "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": {
+ "id": 1,
+ "username": "admin",
+ "name": "Administrator",
+ "state": "active",
+ "avatar_url": null,
+ "web_url" : "https://gitlab.example.com/admin"
+ },
+ "assignee": {
+ "id": 1,
+ "username": "admin",
+ "name": "Administrator",
+ "state": "active",
+ "avatar_url": null,
+ "web_url" : "https://gitlab.example.com/admin"
},
"source_project_id": 2,
"target_project_id": 3,
@@ -250,6 +358,7 @@ 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 internal ID of the merge request
+- `render_html` (optional) - If `true` response includes rendered HTML for title and description
```json
{
@@ -305,6 +414,7 @@ Parameters:
"changes_count": "1",
"should_remove_source_branch": true,
"force_remove_source_branch": false,
+ "squash": false,
"web_url": "http://example.com/example/example/merge_requests/1",
"discussion_locked": false,
"time_stats": {
@@ -439,14 +549,16 @@ Parameters:
"username": "jarrett",
"id": 5,
"state": "active",
- "avatar_url": "http://www.gravatar.com/avatar/b95567800f828948baf5f4160ebb2473?s=40&d=identicon"
+ "avatar_url": "http://www.gravatar.com/avatar/b95567800f828948baf5f4160ebb2473?s=40&d=identicon",
+ "web_url" : "https://gitlab.example.com/jarrett"
},
"assignee": {
"name": "Administrator",
"username": "root",
"id": 1,
"state": "active",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40&d=identicon"
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40&d=identicon",
+ "web_url" : "https://gitlab.example.com/root"
},
"source_project_id": 4,
"target_project_id": 4,
@@ -473,6 +585,7 @@ Parameters:
"changes_count": "1",
"should_remove_source_branch": true,
"force_remove_source_branch": false,
+ "squash": false,
"web_url": "http://example.com/example/example/merge_requests/1",
"discussion_locked": false,
"time_stats": {
@@ -539,9 +652,11 @@ POST /projects/:id/merge_requests
| `description` | string | no | Description of MR |
| `target_project_id` | integer | no | The target project (numeric id) |
| `labels` | string | no | Labels for MR as a comma-separated list |
-| `milestone_id` | integer | no | The ID of a milestone |
+| `milestone_id` | integer | no | The global ID of a milestone |
| `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging |
-| `allow_maintainer_to_push` | boolean | no | Whether or not a maintainer of the target project can push to the source branch |
+| `allow_collaboration` | boolean | no | Allow commits from members who can merge to the target branch |
+| `allow_maintainer_to_push` | boolean | no | Deprecated, see allow_collaboration |
+| `squash` | boolean | no | Squash commits into a single commit when merging |
```json
{
@@ -557,18 +672,18 @@ POST /projects/:id/merge_requests
"author": {
"id": 1,
"username": "admin",
- "email": "admin@example.com",
"name": "Administrator",
"state": "active",
- "created_at": "2012-04-29T08:46:00Z"
+ "avatar_url": null,
+ "web_url" : "https://gitlab.example.com/admin"
},
"assignee": {
"id": 1,
"username": "admin",
- "email": "admin@example.com",
"name": "Administrator",
"state": "active",
- "created_at": "2012-04-29T08:46:00Z"
+ "avatar_url": null,
+ "web_url" : "https://gitlab.example.com/admin"
},
"source_project_id": 3,
"target_project_id": 4,
@@ -595,8 +710,10 @@ POST /projects/:id/merge_requests
"changes_count": "1",
"should_remove_source_branch": true,
"force_remove_source_branch": false,
+ "squash": false,
"web_url": "http://example.com/example/example/merge_requests/1",
"discussion_locked": false,
+ "allow_collaboration": false,
"allow_maintainer_to_push": false,
"time_stats": {
"time_estimate": 0,
@@ -622,13 +739,15 @@ PUT /projects/:id/merge_requests/:merge_request_iid
| `target_branch` | string | no | The target branch |
| `title` | string | no | Title of MR |
| `assignee_id` | integer | no | The ID of the user to assign the merge request to. Set to `0` or provide an empty value to unassign all assignees. |
-| `milestone_id` | integer | no | The ID of a milestone to assign the merge request to. Set to `0` or provide an empty value to unassign a milestone.|
+| `milestone_id` | integer | no | The global ID of a milestone to assign the merge request to. Set to `0` or provide an empty value to unassign a milestone.|
| `labels` | string | no | Comma-separated label names for a merge request. Set to an empty string to unassign all labels. |
| `description` | string | no | Description of MR |
| `state_event` | string | no | New state (close/reopen) |
| `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging |
+| `squash` | boolean | no | Squash commits into a single commit when merging |
| `discussion_locked` | boolean | no | Flag indicating if the merge request's discussion is locked. If the discussion is locked only project members can add, edit or resolve comments. |
-| `allow_maintainer_to_push` | boolean | no | Whether or not a maintainer of the target project can push to the source branch |
+| `allow_collaboration` | boolean | no | Allow commits from members who can merge to the target branch |
+| `allow_maintainer_to_push` | boolean | no | Deprecated, see allow_collaboration |
Must include at least one non-required attribute from above.
@@ -645,18 +764,18 @@ Must include at least one non-required attribute from above.
"author": {
"id": 1,
"username": "admin",
- "email": "admin@example.com",
"name": "Administrator",
"state": "active",
- "created_at": "2012-04-29T08:46:00Z"
+ "avatar_url": null,
+ "web_url" : "https://gitlab.example.com/admin"
},
"assignee": {
"id": 1,
"username": "admin",
- "email": "admin@example.com",
"name": "Administrator",
"state": "active",
- "created_at": "2012-04-29T08:46:00Z"
+ "avatar_url": null,
+ "web_url" : "https://gitlab.example.com/admin"
},
"source_project_id": 3,
"target_project_id": 4,
@@ -683,8 +802,10 @@ Must include at least one non-required attribute from above.
"changes_count": "1",
"should_remove_source_branch": true,
"force_remove_source_branch": false,
+ "squash": false,
"web_url": "http://example.com/example/example/merge_requests/1",
"discussion_locked": false,
+ "allow_collaboration": false,
"allow_maintainer_to_push": false,
"time_stats": {
"time_estimate": 0,
@@ -752,18 +873,18 @@ Parameters:
"author": {
"id": 1,
"username": "admin",
- "email": "admin@example.com",
"name": "Administrator",
"state": "active",
- "created_at": "2012-04-29T08:46:00Z"
+ "avatar_url": null,
+ "web_url" : "https://gitlab.example.com/admin"
},
"assignee": {
"id": 1,
"username": "admin",
- "email": "admin@example.com",
"name": "Administrator",
"state": "active",
- "created_at": "2012-04-29T08:46:00Z"
+ "avatar_url": null,
+ "web_url" : "https://gitlab.example.com/admin"
},
"source_project_id": 4,
"target_project_id": 4,
@@ -790,6 +911,7 @@ Parameters:
"changes_count": "1",
"should_remove_source_branch": true,
"force_remove_source_branch": false,
+ "squash": false,
"web_url": "http://example.com/example/example/merge_requests/1",
"discussion_locked": false,
"time_stats": {
@@ -830,18 +952,18 @@ Parameters:
"author": {
"id": 1,
"username": "admin",
- "email": "admin@example.com",
"name": "Administrator",
"state": "active",
- "created_at": "2012-04-29T08:46:00Z"
+ "avatar_url": null,
+ "web_url" : "https://gitlab.example.com/admin"
},
"assignee": {
"id": 1,
"username": "admin",
- "email": "admin@example.com",
"name": "Administrator",
"state": "active",
- "created_at": "2012-04-29T08:46:00Z"
+ "avatar_url": null,
+ "web_url" : "https://gitlab.example.com/admin"
},
"source_project_id": 4,
"target_project_id": 4,
@@ -868,6 +990,7 @@ Parameters:
"changes_count": "1",
"should_remove_source_branch": true,
"force_remove_source_branch": false,
+ "squash": false,
"web_url": "http://example.com/example/example/merge_requests/1",
"discussion_locked": false,
"time_stats": {
@@ -1200,6 +1323,7 @@ Example response:
"changes_count": "1",
"should_remove_source_branch": true,
"force_remove_source_branch": false,
+ "squash": false,
"web_url": "http://example.com/example/example/merge_requests/1"
},
"target_url": "https://gitlab.example.com/gitlab-org/gitlab-ci/merge_requests/7",
diff --git a/doc/api/namespaces.md b/doc/api/namespaces.md
index 1f0dc700640..656bf07bb55 100644
--- a/doc/api/namespaces.md
+++ b/doc/api/namespaces.md
@@ -54,7 +54,7 @@ Example response:
]
```
-**Note**: `members_count_with_descendants` are presented only for group masters/owners.
+**Note**: `members_count_with_descendants` are presented only for group maintainers/owners.
## Search for namespace
diff --git a/doc/api/pages_domains.md b/doc/api/pages_domains.md
index 20275b902c6..da2ffcfe40a 100644
--- a/doc/api/pages_domains.md
+++ b/doc/api/pages_domains.md
@@ -122,11 +122,11 @@ POST /projects/:id/pages/domains
| `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
+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
+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
@@ -158,11 +158,11 @@ PUT /projects/:id/pages/domains/:domain
| `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
+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
+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
diff --git a/doc/api/pipelines.md b/doc/api/pipelines.md
index 899f5da6647..ebae68fe389 100644
--- a/doc/api/pipelines.md
+++ b/doc/api/pipelines.md
@@ -102,6 +102,7 @@ POST /projects/:id/pipeline
|------------|---------|----------|---------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `ref` | string | yes | Reference to commit |
+| `variables` | array | no | An array containing the variables available in the pipeline, matching the structure [{ 'key' => 'UPLOAD_TO_S3', 'value' => 'true' }] |
```
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/pipeline?ref=master"
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 79cf5e1cc10..a35c2a56992 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -34,7 +34,7 @@ There are currently three options for `merge_method` to choose from:
## List all projects
Get a list of all visible projects across GitLab for the authenticated user.
-When accessed without authentication, only public projects are returned.
+When accessed without authentication, only public projects with "simple" fields are returned.
```
GET /projects
@@ -47,7 +47,7 @@ GET /projects
| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
| `search` | string | no | Return list of projects matching the search criteria |
-| `simple` | boolean | no | Return only the ID, URL, name, and path of each project |
+| `simple` | boolean | no | Return only limited fields for each project. This is a no-op without authentication as then _only_ simple fields are returned. |
| `owned` | boolean | no | Limit by projects owned by the current user |
| `membership` | boolean | no | Limit by projects that the current user is a member of |
| `starred` | boolean | no | Limit by projects starred by the current user |
@@ -56,6 +56,41 @@ GET /projects
| `with_issues_enabled` | boolean | no | Limit by enabled issues feature |
| `with_merge_requests_enabled` | boolean | no | Limit by enabled merge requests feature |
+When `simple=true` or the user is unauthenticated this returns something like:
+
+```json
+[
+ {
+ "id": 4,
+ "description": null,
+ "default_branch": "master",
+ "ssh_url_to_repo": "git@example.com:diaspora/diaspora-client.git",
+ "http_url_to_repo": "http://example.com/diaspora/diaspora-client.git",
+ "web_url": "http://example.com/diaspora/diaspora-client",
+ "readme_url": "http://example.com/diaspora/diaspora-client/blob/master/README.md",
+ "tag_list": [
+ "example",
+ "disapora client"
+ ],
+ "name": "Diaspora Client",
+ "name_with_namespace": "Diaspora / Diaspora Client",
+ "path": "diaspora-client",
+ "path_with_namespace": "diaspora/diaspora-client",
+ "created_at": "2013-09-30T13:46:02Z",
+ "last_activity_at": "2013-09-30T13:46:02Z",
+ "forks_count": 0,
+ "avatar_url": "http://example.com/uploads/project/avatar/4/uploads/avatar.png",
+ "star_count": 0,
+ },
+ {
+ "id": 6,
+ "description": null,
+ "default_branch": "master",
+...
+```
+
+When the user is authenticated and `simple` is not set this returns something like:
+
```json
[
{
@@ -235,7 +270,7 @@ GET /users/:user_id/projects
| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
| `search` | string | no | Return list of projects matching the search criteria |
-| `simple` | boolean | no | Return only the ID, URL, name, and path of each project |
+| `simple` | boolean | no | Return only limited fields for each project. This is a no-op without authentication as then _only_ simple fields are returned. |
| `owned` | boolean | no | Limit by projects owned by the current user |
| `membership` | boolean | no | Limit by projects that the current user is a member of |
| `starred` | boolean | no | Limit by projects starred by the current user |
@@ -511,6 +546,39 @@ GET /projects/:id
}
```
+If the project is a fork, and you provide a valid token to authenticate, the
+`forked_from_project` field will appear in the response.
+
+```json
+{
+ "id":3,
+
+ ...
+
+ "forked_from_project":{
+ "id":13083,
+ "description":"GitLab Community Edition",
+ "name":"GitLab Community Edition",
+ "name_with_namespace":"GitLab.org / GitLab Community Edition",
+ "path":"gitlab-ce",
+ "path_with_namespace":"gitlab-org/gitlab-ce",
+ "created_at":"2013-09-26T06:02:36.000Z",
+ "default_branch":"master",
+ "tag_list":[],
+ "ssh_url_to_repo":"git@gitlab.com:gitlab-org/gitlab-ce.git",
+ "http_url_to_repo":"https://gitlab.com/gitlab-org/gitlab-ce.git",
+ "web_url":"https://gitlab.com/gitlab-org/gitlab-ce",
+ "avatar_url":"https://assets.gitlab-static.net/uploads/-/system/project/avatar/13083/logo-extra-whitespace.png",
+ "star_count":3812,
+ "forks_count":3561,
+ "last_activity_at":"2018-01-02T11:40:26.570Z"
+ }
+
+ ...
+
+}
+```
+
## Get project users
Get the users list of a project.
@@ -690,7 +758,7 @@ GET /projects/:id/forks
| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
| `search` | string | no | Return list of projects matching the search criteria |
-| `simple` | boolean | no | Return only the ID, URL, name, and path of each project |
+| `simple` | boolean | no | Return only limited fields for each project. This is a no-op without authentication as then _only_ simple fields are returned. |
| `owned` | boolean | no | Limit by projects owned by the current user |
| `membership` | boolean | no | Limit by projects that the current user is a member of |
| `starred` | boolean | no | Limit by projects starred by the current user |
@@ -1169,7 +1237,7 @@ The `file=` parameter must point to a file on your filesystem and be preceded
by `@`. For example:
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "file=@dk.png" https://gitlab.example.com/api/v3/projects/5/uploads
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "file=@dk.png" https://gitlab.example.com/api/v4/projects/5/uploads
```
Returned object:
@@ -1198,7 +1266,7 @@ POST /projects/:id/share
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `group_id` | integer | yes | The ID of the group to share with |
-| `group_access` | integer | yes | The permissions level to grant the group |
+| `group_access` | integer | yes | The [permissions level](members.md) to grant the group |
| `expires_at` | string | no | Share expiration date in ISO 8601 format: 2016-09-26 |
## Delete a shared project link within a group
@@ -1390,6 +1458,16 @@ POST /projects/:id/housekeeping
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+### Transfer a project to a new namespace
+
+```
+PUT /projects/:id/transfer
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `namespace` | integer/string | yes | The ID or path of the namespace to transfer to project to |
+
## Branches
Read more in the [Branches](branches.md) documentation.
diff --git a/doc/api/protected_branches.md b/doc/api/protected_branches.md
index 950ead52560..f6813f27dc0 100644
--- a/doc/api/protected_branches.md
+++ b/doc/api/protected_branches.md
@@ -8,7 +8,7 @@ The access levels are defined in the `ProtectedRefAccess::ALLOWED_ACCESS_LEVELS`
```
0 => No access
30 => Developer access
-40 => Master access
+40 => Maintainer access
```
## List protected branches
@@ -36,13 +36,13 @@ Example response:
"push_access_levels": [
{
"access_level": 40,
- "access_level_description": "Masters"
+ "access_level_description": "Maintainers"
}
],
"merge_access_levels": [
{
"access_level": 40,
- "access_level_description": "Masters"
+ "access_level_description": "Maintainers"
}
]
},
@@ -75,13 +75,13 @@ Example response:
"push_access_levels": [
{
"access_level": 40,
- "access_level_description": "Masters"
+ "access_level_description": "Maintainers"
}
],
"merge_access_levels": [
{
"access_level": 40,
- "access_level_description": "Masters"
+ "access_level_description": "Maintainers"
}
]
}
@@ -104,8 +104,8 @@ curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitl
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `name` | string | yes | The name of the branch or wildcard |
-| `push_access_level` | string | no | Access levels allowed to push (defaults: `40`, master access level) |
-| `merge_access_level` | string | no | Access levels allowed to merge (defaults: `40`, master access level) |
+| `push_access_level` | string | no | Access levels allowed to push (defaults: `40`, maintainer access level) |
+| `merge_access_level` | string | no | Access levels allowed to merge (defaults: `40`, maintainer access level) |
Example response:
@@ -115,13 +115,13 @@ Example response:
"push_access_levels": [
{
"access_level": 30,
- "access_level_description": "Developers + Masters"
+ "access_level_description": "Developers + Maintainers"
}
],
"merge_access_levels": [
{
"access_level": 30,
- "access_level_description": "Developers + Masters"
+ "access_level_description": "Developers + Maintainers"
}
]
}
diff --git a/doc/api/repositories.md b/doc/api/repositories.md
index 5aff255c20a..cb816bbd712 100644
--- a/doc/api/repositories.md
+++ b/doc/api/repositories.md
@@ -130,6 +130,7 @@ Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `from` (required) - the commit SHA or branch name
- `to` (required) - the commit SHA or branch name
+- `straight` (optional) - comparison method, `true` for direct comparison between `from` and `to` (`from`..`to`), `false` to compare using merge base (`from`...`to`)'. Default is `false`.
```
GET /projects/:id/repository/compare?from=master&to=feature
diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md
index c29dc22e12d..49fb9bc141d 100644
--- a/doc/api/repository_files.md
+++ b/doc/api/repository_files.md
@@ -27,6 +27,7 @@ Example response:
"size": 1476,
"encoding": "base64",
"content": "IyA9PSBTY2hlbWEgSW5mb3...",
+ "content_sha256": "4c294617b60715c1d218e61164a3abd4808a4284cbc30e6728a01ad9aada4481",
"ref": "master",
"blob_id": "79f7bbd25901e8334750839545a9bd021f0e4c83",
"commit_id": "d5a3ff139356ce33e37e73add446f16869741b50",
@@ -39,6 +40,36 @@ Parameters:
- `file_path` (required) - Url encoded full path to new file. Ex. lib%2Fclass%2Erb
- `ref` (required) - The name of branch, tag or commit
+NOTE: **Note:**
+`blob_id` is the blob sha, see [repositories - Get a blob from repository](repositories.md#get-a-blob-from-repository)
+
+In addition to the `GET` method, you can also use `HEAD` to get just file metadata.
+
+```
+HEAD /projects/:id/repository/files/:file_path
+```
+
+```bash
+curl --head --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/files/app%2Fmodels%2Fkey%2Erb?ref=master'
+```
+
+Example response:
+
+```text
+HTTP/1.1 200 OK
+...
+X-Gitlab-Blob-Id: 79f7bbd25901e8334750839545a9bd021f0e4c83
+X-Gitlab-Commit-Id: d5a3ff139356ce33e37e73add446f16869741b50
+X-Gitlab-Content-Sha256: 4c294617b60715c1d218e61164a3abd4808a4284cbc30e6728a01ad9aada4481
+X-Gitlab-Encoding: base64
+X-Gitlab-File-Name: key.rb
+X-Gitlab-File-Path: app/models/key.rb
+X-Gitlab-Last-Commit-Id: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
+X-Gitlab-Ref: master
+X-Gitlab-Size: 1476
+...
+```
+
## Get raw file from repository
```
@@ -54,6 +85,9 @@ Parameters:
- `file_path` (required) - Url encoded full path to new file. Ex. lib%2Fclass%2Erb
- `ref` (required) - The name of branch, tag or commit
+NOTE: **Note:**
+Like [Get file from repository](repository_files.md#get-file-from-repository) you can use `HEAD` to get just file metadata.
+
## Create new file in repository
```
diff --git a/doc/api/runners.md b/doc/api/runners.md
index 3ca07ce9795..ac814bbf19a 100644
--- a/doc/api/runners.md
+++ b/doc/api/runners.md
@@ -30,6 +30,7 @@ Example response:
"description": "test-1-20150125",
"id": 6,
"is_shared": false,
+ "ip_address": "127.0.0.1",
"name": null,
"online": true,
"status": "online"
@@ -38,6 +39,7 @@ Example response:
"active": true,
"description": "test-2-20150125",
"id": 8,
+ "ip_address": "127.0.0.1",
"is_shared": false,
"name": null,
"online": false,
@@ -72,6 +74,7 @@ Example response:
"active": true,
"description": "shared-runner-1",
"id": 1,
+ "ip_address": "127.0.0.1",
"is_shared": true,
"name": null,
"online": true,
@@ -81,6 +84,7 @@ Example response:
"active": true,
"description": "shared-runner-2",
"id": 3,
+ "ip_address": "127.0.0.1",
"is_shared": true,
"name": null,
"online": false
@@ -90,6 +94,7 @@ Example response:
"active": true,
"description": "test-1-20150125",
"id": 6,
+ "ip_address": "127.0.0.1",
"is_shared": false,
"name": null,
"online": true
@@ -99,6 +104,7 @@ Example response:
"active": true,
"description": "test-2-20150125",
"id": 8,
+ "ip_address": "127.0.0.1",
"is_shared": false,
"name": null,
"online": false,
@@ -131,6 +137,7 @@ Example response:
"architecture": null,
"description": "test-1-20150125",
"id": 6,
+ "ip_address": "127.0.0.1",
"is_shared": false,
"contacted_at": "2016-01-25T16:39:48.066Z",
"name": null,
@@ -189,6 +196,7 @@ Example response:
"architecture": null,
"description": "test-1-20150125-test",
"id": 6,
+ "ip_address": "127.0.0.1",
"is_shared": false,
"contacted_at": "2016-01-25T16:39:48.066Z",
"name": null,
@@ -257,6 +265,7 @@ Example response:
[
{
"id": 2,
+ "ip_address": "127.0.0.1",
"status": "running",
"stage": "test",
"name": "test",
@@ -345,6 +354,7 @@ Example response:
"active": true,
"description": "test-2-20150125",
"id": 8,
+ "ip_address": "127.0.0.1",
"is_shared": false,
"name": null,
"online": false,
@@ -354,6 +364,7 @@ Example response:
"active": true,
"description": "development_runner",
"id": 5,
+ "ip_address": "127.0.0.1",
"is_shared": true,
"name": null,
"online": true
@@ -386,6 +397,7 @@ Example response:
"active": true,
"description": "test-2016-02-01",
"id": 9,
+ "ip_address": "127.0.0.1",
"is_shared": false,
"name": null,
"online": true,
diff --git a/doc/api/search.md b/doc/api/search.md
index 107ddaffa6a..9716f682ace 100644
--- a/doc/api/search.md
+++ b/doc/api/search.md
@@ -776,6 +776,15 @@ Example response:
### Scope: blobs
+Filters are available for this scope:
+- filename
+- path
+- extension
+
+to use a filter simply include it in your query like so: `a query filename:some_name*`.
+
+You may use wildcards (`*`) to use glob matching.
+
```bash
curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/6/search?scope=blobs&search=installation
```
diff --git a/doc/api/services.md b/doc/api/services.md
index f23303ef836..aeb48ccc36c 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -1,6 +1,6 @@
# Services API
->**Note:** This API requires an access token with Master or Owner permissions
+>**Note:** This API requires an access token with Maintainer or Owner permissions
## Asana
diff --git a/doc/api/settings.md b/doc/api/settings.md
index e06b1bfb6df..e6b207d8746 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -55,6 +55,7 @@ Example response:
"ed25519_key_restriction": 0,
"enforce_terms": true,
"terms": "Hello world!",
+ "performance_bar_allowed_group_id": 42
}
```
@@ -80,7 +81,7 @@ PUT /application/settings
| `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_branch_protection` | integer | no | Determine if developers can push to master. Can take `0` _(not protected, both developers and maintainers can push new commits, force push, or delete the branch)_, `1` _(partially protected, developers and maintainers can push new commits, but cannot force push or delete the branch)_ or `2` _(fully protected, developers cannot push new commits, but maintainers 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` |
@@ -120,8 +121,9 @@ PUT /application/settings
| `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 |
+| `performance_bar_allowed_group_path` | string | no | Path of the group that is allowed to toggle the performance bar |
+| `performance_bar_allowed_group_id` | string | no | Deprecated: Use `performance_bar_allowed_group_path` instead. Path of the group that is allowed to toggle the performance bar |
+| `performance_bar_enabled` | boolean | no | Deprecated: Pass `performance_bar_allowed_group_path: nil` instead. 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. |
@@ -133,7 +135,7 @@ PUT /application/settings
| `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. |
+| `restricted_visibility_levels` | array of strings | no | Selected levels cannot be used by non-admin users for groups, 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 |
@@ -201,5 +203,6 @@ Example response:
"ed25519_key_restriction": 0,
"enforce_terms": true,
"terms": "Hello world!",
+ "performance_bar_allowed_group_id": 42
}
```
diff --git a/doc/api/snippets.md b/doc/api/snippets.md
index 42b760c107d..7892866cd8e 100644
--- a/doc/api/snippets.md
+++ b/doc/api/snippets.md
@@ -49,6 +49,7 @@ Example response:
"title": "test",
"file_name": "add.rb",
"description": "Ruby test snippet",
+ "visibility": "private",
"author": {
"id": 1,
"username": "john_smith",
@@ -99,6 +100,7 @@ Example response:
"title": "This is a snippet",
"file_name": "test.txt",
"description": "Hello World snippet",
+ "visibility": "internal",
"author": {
"id": 1,
"username": "john_smith",
@@ -150,6 +152,7 @@ Example response:
"title": "test",
"file_name": "add.rb",
"description": "description of snippet",
+ "visibility": "internal",
"author": {
"id": 1,
"username": "john_smith",
@@ -238,7 +241,8 @@ Example response:
"raw_url": "http://localhost:3000/snippets/48/raw",
"title": "Minus similique nesciunt vel fugiat qui ullam sunt.",
"updated_at": "2016-11-25T16:53:34.479Z",
- "web_url": "http://localhost:3000/snippets/48"
+ "web_url": "http://localhost:3000/snippets/48",
+ "visibility": "public"
}
]
```
diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md
index 9835fab7c98..98eae66469f 100644
--- a/doc/api/v3_to_v4.md
+++ b/doc/api/v3_to_v4.md
@@ -2,10 +2,9 @@
Since GitLab 9.0, API V4 is the preferred version to be used.
-API V3 will be unsupported from GitLab 9.5, to be released on August
-22, 2017. It will be removed in GitLab 9.5 or later. In the meantime, we advise
-you to make any necessary changes to applications that use V3. The V3 API
-documentation is still
+API V3 was unsupported from GitLab 9.5, released on August
+22, 2017. API v3 was removed in [GitLab 11.0](https://gitlab.com/gitlab-org/gitlab-ce/issues/36819).
+The V3 API documentation is still
[available](https://gitlab.com/gitlab-org/gitlab-ce/blob/8-16-stable/doc/api/README.md).
Below are the changes made between V3 and V4.
diff --git a/doc/articles/index.md b/doc/articles/index.md
index 9f85533ea94..87ee17bb6de 100644
--- a/doc/articles/index.md
+++ b/doc/articles/index.md
@@ -4,7 +4,7 @@ comments: false
# Technical articles list (deprecated)
-[Technical articles](../development/writing_documentation.md#technical-articles) are
+[Technical articles](../development/documentation/index.md#technical-articles) are
topic-related documentation, written with an user-friendly approach and language, aiming
to provide the community with guidance on specific processes to achieve certain objectives.
diff --git a/doc/ci/README.md b/doc/ci/README.md
index 8d1d72c2a2b..7666219acb0 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -19,7 +19,7 @@ Here's some info we've gathered to get you started.
The first steps towards your GitLab CI/CD journey.
- [Getting started with GitLab CI/CD](quick_start/README.md): understand how GitLab CI/CD works.
-- GitLab CI/CD configuration file: [`.gitlab-ci.yml`](yaml/README.md) - Learn all about the ins and outs of `.gitlab-ci.yml`.
+- [GitLab CI/CD configuration file: `.gitlab-ci.yml`](yaml/README.md) - Learn all about the ins and outs of `.gitlab-ci.yml`.
- [Pipelines and jobs](pipelines.md): configure your GitLab CI/CD pipelines to build, test, and deploy your application.
- Runners: The [GitLab Runner](https://docs.gitlab.com/runner/) is responsible by running the jobs in your CI/CD pipeline. On GitLab.com, Shared Runners are enabled by default, so
you don't need to set up anything to start to use them with GitLab CI/CD.
@@ -46,7 +46,9 @@ you don't need to set up anything to start to use them with GitLab CI/CD.
## Exploring GitLab CI/CD
- [CI/CD Variables](variables/README.md) - Learn how to use variables defined in
- your `.gitlab-ci.yml` or secured ones defined in your project's settings
+ your `.gitlab-ci.yml` or the ones defined in your project's settings
+ - [Where variables can be used](variables/where_variables_can_be_used.md) - A
+ deeper look on where and how the CI/CD variables can be used
- **The permissions model** - Learn about the access levels a user can have for
performing certain CI actions
- [User permissions](../user/permissions.md#gitlab-ci)
diff --git a/doc/ci/autodeploy/index.md b/doc/ci/autodeploy/index.md
index 7102af5c529..985ec4b972c 100644
--- a/doc/ci/autodeploy/index.md
+++ b/doc/ci/autodeploy/index.md
@@ -1,129 +1 @@
-# Auto Deploy
-
-> [Introduced][mr-8135] in GitLab 8.15.
-> Auto deploy is an experimental feature and is **not recommended for Production use** at this time.
-
-> As of GitLab 9.1, access to the container registry is only available while the
-Pipeline is running. Restarting a pod, scaling a service, or other actions which
-require on-going access **will fail**. On-going secure access is planned for a
-subsequent release.
-
-> As of GitLab 10.0, Auto Deploy templates are **deprecated** and the
-functionality has been included in [Auto
-DevOps](../../topics/autodevops/index.md).
-
-Auto deploy is an easy way to configure GitLab CI for the deployment of your
-application. GitLab Community maintains a list of `.gitlab-ci.yml`
-templates for various infrastructure providers and deployment scripts
-powering them. These scripts are responsible for packaging your application,
-setting up the infrastructure and spinning up necessary services (for
-example a database).
-
-## How it works
-
-The Autodeploy templates are based on the [kubernetes-deploy][kube-deploy]
-project which is used to simplify the deployment process to Kubernetes by
-providing intelligent `build`, `deploy`, and `destroy` commands which you can
-use in your `.gitlab-ci.yml` as is. It uses [Herokuish](https://github.com/gliderlabs/herokuish),
-which uses [Heroku buildpacks](https://devcenter.heroku.com/articles/buildpacks)
-to do some of the work, plus some of GitLab's own tools to package it all up. For
-your convenience, a [Docker image][kube-image] is also provided.
-
-You can use the [Kubernetes project service](../../user/project/integrations/kubernetes.md)
-to store credentials to your infrastructure provider and they will be available
-during the deployment.
-
-## Quick start
-
-We made a [simple guide](quick_start_guide.md) to using Auto Deploy with GitLab.com.
-
-For a demonstration of GitLab Auto Deploy, read the blog post [Auto Deploy from GitLab to an OpenShift Container Cluster](https://about.gitlab.com/2017/05/16/devops-containers-gitlab-openshift/)
-
-## Supported templates
-
-The list of supported auto deploy templates is available in the
-[gitlab-ci-yml project][auto-deploy-templates].
-
-## Configuration
-
->**Note:**
-In order to understand why the following steps are required, read the
-[how it works](#how-it-works) section.
-
-To configure Autodeploy, you will need to:
-
-1. Enable a deployment [project service][project-services] to store your
- credentials. For example, if you want to deploy to OpenShift you have to
- enable [Kubernetes service][kubernetes-service].
-1. Configure GitLab Runner to use the
- [Docker or Kubernetes executor](https://docs.gitlab.com/runner/executors/) with
- [privileged mode enabled][docker-in-docker].
-1. Navigate to the "Project" tab and click "Set up auto deploy" button.
- ![Auto deploy button](img/auto_deploy_button.png)
-1. Select a template.
- ![Dropdown with auto deploy templates](img/auto_deploy_dropdown.png)
-1. Commit your changes and create a merge request.
-1. Test your deployment configuration using a [Review App][review-app] that was
- created automatically for you.
-
-## Private project support
-
-> Experimental support [introduced][mr-2] in GitLab 9.1.
-
-When a project has been marked as private, GitLab's [Container Registry][container-registry] requires authentication when downloading containers. Auto deploy will automatically provide the required authentication information to Kubernetes, allowing temporary access to the registry. Authentication credentials will be valid while the pipeline is running, allowing for a successful initial deployment.
-
-After the pipeline completes, Kubernetes will no longer be able to access the container registry. Restarting a pod, scaling a service, or other actions which require on-going access to the registry will fail. On-going secure access is planned for a subsequent release.
-
-## PostgreSQL database support
-
-> Experimental support [introduced][mr-8] in GitLab 9.1.
-
-In order to support applications that require a database, [PostgreSQL][postgresql] is provisioned by default. Credentials to access the database are preconfigured, but can be customized by setting the associated [variables](#postgresql-variables). These credentials can be used for defining a `DATABASE_URL` of the format: `postgres://user:password@postgres-host:postgres-port/postgres-database`. It is important to note that the database itself is temporary, and contents will be not be saved.
-
-PostgreSQL provisioning can be disabled by setting the variable `DISABLE_POSTGRES` to `"yes"`.
-
-The following PostgreSQL variables are supported:
-
-1. `DISABLE_POSTGRES: "yes"`: disable automatic deployment of PostgreSQL
-1. `POSTGRES_USER: "my-user"`: use custom username for PostgreSQL
-1. `POSTGRES_PASSWORD: "password"`: use custom password for PostgreSQL
-1. `POSTGRES_DB: "my database"`: use custom database name for PostgreSQL
-
-## Auto Monitoring
-
-> Introduced in [GitLab 9.5](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13438).
-
-Apps auto-deployed using one the [Kubernetes templates](#supported-templates) can also be automatically monitored for:
-
-* Response Metrics: latency, throughput, error rate
-* System Metrics: CPU utilization, memory utilization
-
-Metrics are gathered from [nginx-ingress](../../user/project/integrations/prometheus_library/nginx_ingress.md) and [Kubernetes](../../user/project/integrations/prometheus_library/kubernetes.md).
-
-To view the metrics, open the [Monitoring dashboard for a deployed environment](../environments.md#monitoring-environments).
-
-![Auto Metrics](img/auto_monitoring.png)
-
-### Configuring Auto Monitoring
-
-If GitLab has been deployed using the [omnibus-gitlab](../../install/kubernetes/gitlab_omnibus.md) Helm chart, no configuration is required.
-
-If you have installed GitLab using a different method:
-
-1. [Deploy Prometheus](../../user/project/integrations/prometheus.md#configuring-your-own-prometheus-server-within-kubernetes) into your Kubernetes cluster
-1. If you would like response metrics, ensure you are running at least version 0.9.0 of NGINX Ingress and [enable Prometheus metrics](https://github.com/kubernetes/ingress/blob/master/examples/customization/custom-vts-metrics/nginx/nginx-vts-metrics-conf.yaml).
-1. Finally, [annotate](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/) the NGINX Ingress deployment to be scraped by Prometheus using `prometheus.io/scrape: "true"` and `prometheus.io/port: "10254"`.
-
-[mr-8135]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8135
-[mr-2]: https://gitlab.com/gitlab-examples/kubernetes-deploy/merge_requests/2
-[mr-8]: https://gitlab.com/gitlab-examples/kubernetes-deploy/merge_requests/8
-[project-settings]: https://docs.gitlab.com/ce/public_access/public_access.html
-[project-services]: ../../user/project/integrations/project_services.md
-[auto-deploy-templates]: https://gitlab.com/gitlab-org/gitlab-ci-yml/tree/master/autodeploy
-[kubernetes-service]: ../../user/project/integrations/kubernetes.md
-[docker-in-docker]: ../docker/using_docker_build.md#use-docker-in-docker-executor
-[review-app]: ../review_apps/index.md
-[kube-image]: https://gitlab.com/gitlab-examples/kubernetes-deploy/container_registry "Kubernetes deploy Container Registry"
-[kube-deploy]: https://gitlab.com/gitlab-examples/kubernetes-deploy "Kubernetes deploy example project"
-[container-registry]: https://docs.gitlab.com/ce/user/project/container_registry.html
-[postgresql]: https://www.postgresql.org/
+This document was moved to [another location](../../topics/autodevops/index.md#auto-deploy).
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index 07b144f6ddd..fbac37e688e 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -134,9 +134,20 @@ In order to do that, follow the steps:
```yaml
image: docker:stable
- # When using dind, it's wise to use the overlayfs driver for
- # improved performance.
variables:
+ # When using dind service we need to instruct docker, to talk with the
+ # daemon started inside of the service. The daemon is available with
+ # a network connection instead of the default /var/run/docker.sock socket.
+ #
+ # The 'docker' hostname is the alias of the service container as described at
+ # https://docs.gitlab.com/ee/ci/docker/using_docker_images.html#accessing-the-services
+ #
+ # Note that if you're using Kubernetes executor, the variable should be set to
+ # tcp://localhost:2375 because of how Kubernetes executor connects services
+ # to the job container
+ DOCKER_HOST: tcp://docker:2375/
+ # When using dind, it's wise to use the overlayfs driver for
+ # improved performance.
DOCKER_DRIVER: overlay2
services:
@@ -293,6 +304,7 @@ services:
variables:
CONTAINER_IMAGE: registry.gitlab.com/$CI_PROJECT_PATH
+ DOCKER_HOST: tcp://docker:2375
DOCKER_DRIVER: overlay2
before_script:
@@ -391,6 +403,9 @@ could look like:
image: docker:stable
services:
- docker:dind
+ variables:
+ DOCKER_HOST: tcp://docker:2375
+ DOCKER_DRIVER: overlay2
stage: build
script:
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.example.com
@@ -410,6 +425,8 @@ services:
- docker:dind
variables:
+ DOCKER_HOST: tcp://docker:2375
+ DOCKER_DRIVER: overlay2
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME
before_script:
@@ -445,6 +462,8 @@ stages:
- deploy
variables:
+ DOCKER_HOST: tcp://docker:2375
+ DOCKER_DRIVER: overlay2
CONTAINER_TEST_IMAGE: registry.example.com/my-group/my-project/my-image:$CI_COMMIT_REF_NAME
CONTAINER_RELEASE_IMAGE: registry.example.com/my-group/my-project/my-image:latest
diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md
index 7c0f837ea9c..71f1d69cdf4 100644
--- a/doc/ci/docker/using_docker_images.md
+++ b/doc/ci/docker/using_docker_images.md
@@ -496,7 +496,7 @@ To configure access for `registry.example.com`, follow these steps:
bXlfdXNlcm5hbWU6bXlfcGFzc3dvcmQ=
```
-1. Create a [secret variable] `DOCKER_AUTH_CONFIG` with the content of the
+1. Create a [variable] `DOCKER_AUTH_CONFIG` with the content of the
Docker configuration file as the value:
```json
@@ -632,7 +632,7 @@ creation.
[postgres-hub]: https://hub.docker.com/r/_/postgres/
[mysql-hub]: https://hub.docker.com/r/_/mysql/
[runner-priv-reg]: http://docs.gitlab.com/runner/configuration/advanced-configuration.html#using-a-private-container-registry
-[secret variable]: ../variables/README.md#secret-variables
+[variable]: ../variables/README.md#variables
[entrypoint]: https://docs.docker.com/engine/reference/builder/#entrypoint
[cmd]: https://docs.docker.com/engine/reference/builder/#cmd
[register]: https://docs.gitlab.com/runner/register/
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
index 3a491f0073c..8ea2e0a81dc 100644
--- a/doc/ci/environments.md
+++ b/doc/ci/environments.md
@@ -24,7 +24,7 @@ Environments are like tags for your CI jobs, describing where code gets deployed
Deployments are created when [jobs] deploy versions of code to environments,
so every environment can have one or more deployments. GitLab keeps track of
your deployments, so you always know what is currently being deployed on your
-servers. If you have a deployment service such as [Kubernetes][kubernetes-service]
+servers. If you have a deployment service such as [Kubernetes][kube]
enabled for your project, you can use it to assist with your deployments, and
can even access a [web terminal](#web-terminals) for your environment from within GitLab!
@@ -114,7 +114,7 @@ Let's now see how that information is exposed within GitLab.
## Viewing the current status of an environment
-The environment list under your project's **Pipelines âž” Environments**, is
+The environment list under your project's **Operations > Environments**, is
where you can find information of the last deployment status of an environment.
Here's how the Environments page looks so far.
@@ -167,7 +167,7 @@ that works.
You can't control everything, so sometimes things go wrong. When that unfortunate
time comes GitLab has you covered. Simply by clicking the **Rollback** button
that can be found in the deployments page
-(**Pipelines âž” Environments âž” `environment name`**) you can relaunch the
+(**Operations > Environments > `environment name`**) you can relaunch the
job with the commit associated with it.
>**Note:**
@@ -246,23 +246,14 @@ As the name suggests, it is possible to create environments on the fly by just
declaring their names dynamically in `.gitlab-ci.yml`. Dynamic environments is
the basis of [Review apps](review_apps/index.md).
->**Note:**
-The `name` and `url` parameters can use most of the defined CI variables,
-including predefined, secure variables and `.gitlab-ci.yml`
-[`variables`](yaml/README.md#variables). You however cannot use variables
-defined under `script` or on the Runner's side. There are other variables that
-are unsupported in environment name context:
-- `CI_PIPELINE_ID`
-- `CI_JOB_ID`
-- `CI_JOB_TOKEN`
-- `CI_BUILD_ID`
-- `CI_BUILD_TOKEN`
-- `CI_REGISTRY_USER`
-- `CI_REGISTRY_PASSWORD`
-- `CI_REPOSITORY_URL`
-- `CI_ENVIRONMENT_URL`
-- `CI_DEPLOY_USER`
-- `CI_DEPLOY_PASSWORD`
+NOTE: **Note:**
+The `name` and `url` parameters can use most of the CI/CD variables,
+including [predefined](variables/README.md#predefined-variables-environment-variables),
+[project/group ones](variables/README.md#variables) and
+[`.gitlab-ci.yml` variables](yaml/README.md#variables). You however cannot use variables
+defined under `script` or on the Runner's side. There are also other variables that
+are unsupported in the context of `environment:name`. You can read more about
+[where variables can be used](variables/where_variables_can_be_used.md).
GitLab Runner exposes various [environment variables][variables] when a job runs,
and as such, you can use them as environment names. Let's add another job in
@@ -602,10 +593,10 @@ version of the app, all without leaving GitLab.
>**Note:**
Web terminals were added in GitLab 8.15 and are only available to project
-masters and owners.
+maintainers and owners.
If you deploy to your environments with the help of a deployment service (e.g.,
-the [Kubernetes service][kubernetes-service]), GitLab can open
+the [Kubernetes integration][kube]), GitLab can open
a terminal session to your environment! This is a very powerful feature that
allows you to debug issues without leaving the comfort of your web browser. To
enable it, just follow the instructions given in the service integration
@@ -671,7 +662,6 @@ Below are some links you may find interesting:
[Pipelines]: pipelines.md
[jobs]: yaml/README.md#jobs
[yaml]: yaml/README.md
-[kubernetes-service]: ../user/project/integrations/kubernetes.md
[environments]: #environments
[deployments]: #deployments
[permissions]: ../user/permissions.md
@@ -683,5 +673,5 @@ Below are some links you may find interesting:
[gitlab-flow]: ../workflow/gitlab_flow.md
[gitlab runner]: https://docs.gitlab.com/runner/
[git-strategy]: yaml/README.md#git-strategy
-[kube]: ../user/project/integrations/kubernetes.md
+[kube]: ../user/project/clusters/index.md
[prom]: ../user/project/integrations/prometheus.md
diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md
index de60cd27cd1..811f4d1f07a 100644
--- a/doc/ci/examples/README.md
+++ b/doc/ci/examples/README.md
@@ -19,7 +19,9 @@ There's also a collection of repositories with [example projects](https://gitlab
- [How to test and deploy Laravel/PHP applications with GitLab CI/CD and Envoy](laravel_with_gitlab_and_envoy/index.md)
- **Ruby**: [Test and deploy a Ruby application to Heroku](test-and-deploy-ruby-application-to-heroku.md)
- **Python**: [Test and deploy a Python application to Heroku](test-and-deploy-python-application-to-heroku.md)
-- **Java**: [Continuous Delivery of a Spring Boot application with GitLab CI and Kubernetes](https://about.gitlab.com/2016/12/14/continuous-delivery-of-a-spring-boot-application-with-gitlab-ci-and-kubernetes/)
+- **Java**:
+ - [Deploy a Spring Boot application to Cloud Foundry with GitLab CI/CD](deploy_spring_boot_to_cloud_foundry/index.md)
+ - [Continuous Delivery of a Spring Boot application with GitLab CI and Kubernetes](https://about.gitlab.com/2016/12/14/continuous-delivery-of-a-spring-boot-application-with-gitlab-ci-and-kubernetes/)
- **Scala**: [Test a Scala application](test-scala-application.md)
- **Clojure**: [Test a Clojure application](test-clojure-application.md)
- **Elixir**:
@@ -41,9 +43,9 @@ There's also a collection of repositories with [example projects](https://gitlab
- [Using `dpl` as deployment tool](deployment/README.md)
- [The `.gitlab-ci.yml` file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml)
-## Code quality analysis
+## Code Quality analysis
-[Analyze code quality with the Code Climate CLI](code_climate.md).
+**(Starter)** [Analyze your project's Code Quality](code_quality.md).
## Static Application Security Testing (SAST)
diff --git a/doc/ci/examples/artifactory_and_gitlab/index.md b/doc/ci/examples/artifactory_and_gitlab/index.md
index d931c9a77f4..9657f52159e 100644
--- a/doc/ci/examples/artifactory_and_gitlab/index.md
+++ b/doc/ci/examples/artifactory_and_gitlab/index.md
@@ -58,7 +58,7 @@ The application is ready to use, but you need some additional steps to deploy it
1. Log in to Artifactory with your user's credentials.
1. From the main screen, click on the `libs-release-local` item in the **Set Me Up** panel.
1. Copy to clipboard the configuration snippet under the **Deploy** paragraph.
-1. Change the `url` value in order to have it configurable via secret variables.
+1. Change the `url` value in order to have it configurable via variables.
1. Copy the snippet in the `pom.xml` file for your project, just after the
`dependencies` section. The snippet should look like this:
@@ -98,7 +98,7 @@ parameter in `.gitlab-ci.yml` to use the custom location instead of the default
</settings>
```
- Username and password will be replaced by the correct values using secret variables.
+ Username and password will be replaced by the correct values using variables.
### Configure GitLab CI/CD for `simple-maven-dep`
@@ -107,8 +107,8 @@ Now it's time we set up [GitLab CI/CD](https://about.gitlab.com/features/gitlab-
GitLab CI/CD uses a file in the root of the repo, named `.gitlab-ci.yml`, to read the definitions for jobs
that will be executed by the configured GitLab Runners. You can read more about this file in the [GitLab Documentation](https://docs.gitlab.com/ee/ci/yaml/).
-First of all, remember to set up secret variables for your deployment. Navigate to your project's **Settings > CI/CD** page
-and add the following secret variables (replace them with your current values, of course):
+First of all, remember to set up variables for your deployment. Navigate to your project's **Settings > CI/CD > Variables** page
+and add the following ones (replace them with your current values, of course):
- **MAVEN_REPO_URL**: `http://artifactory.example.com:8081/artifactory` (your Artifactory URL)
- **MAVEN_REPO_USER**: `gitlab` (your Artifactory username)
@@ -156,7 +156,7 @@ by running all Maven phases in a sequential order, therefore, executing `mvn tes
Both `build` and `test` jobs leverage the `mvn` command to compile the application and to test it as defined in the test suite that is part of the application.
-Deploy to Artifactory is done as defined by the secret variables we have just set up.
+Deploy to Artifactory is done as defined by the variables we have just set up.
The deployment occurs only if we're pushing or merging to `master` branch, so that the development versions are tested but not published.
Done! Now you have all the changes in the GitLab repo, and a pipeline has already been started for this commit. In the **Pipelines** tab you can see what's happening.
diff --git a/doc/ci/examples/code_climate.md b/doc/ci/examples/code_climate.md
index d1aa783cc9c..b34637efc8d 100644
--- a/doc/ci/examples/code_climate.md
+++ b/doc/ci/examples/code_climate.md
@@ -1,42 +1,6 @@
-# Analyze project code quality with Code Climate CLI
+---
+redirect_from: 'https://docs.gitlab.com/ee/ci/examples/code_climate.html'
+redirect_to: code_quality.md
+---
-This example shows how to run [Code Climate CLI][cli] on your code by using
-GitLab CI and Docker.
-
-First, you need GitLab Runner with [docker-in-docker executor][dind].
-
-Once you set up the Runner, add a new job to `.gitlab-ci.yml`, called `codequality`:
-
-```yaml
-codequality:
- image: docker:stable
- variables:
- DOCKER_DRIVER: overlay2
- allow_failure: true
- services:
- - docker:stable-dind
- script:
- - export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
- - docker run
- --env SOURCE_CODE="$PWD"
- --volume "$PWD":/code
- --volume /var/run/docker.sock:/var/run/docker.sock
- "registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code
- artifacts:
- paths: [codeclimate.json]
-```
-
-The above example will create a `codequality` job in your CI/CD pipeline which
-will scan your source code for code quality issues. The report will be saved
-as an artifact that you can later download and analyze.
-
-TIP: **Tip:**
-Starting with [GitLab Starter][ee] 9.3, this information will
-be automatically extracted and shown right in the merge request widget. To do
-so, the CI/CD job must be named `codequality` and the artifact path must be
-`codeclimate.json`.
-[Learn more on code quality diffs in merge requests](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html).
-
-[cli]: https://github.com/codeclimate/codeclimate
-[dind]: ../docker/using_docker_build.md#use-docker-in-docker-executor
-[ee]: https://about.gitlab.com/products/
+This document was moved to [another location](code_quality.md).
diff --git a/doc/ci/examples/code_quality.md b/doc/ci/examples/code_quality.md
new file mode 100644
index 00000000000..2a7040ecdeb
--- /dev/null
+++ b/doc/ci/examples/code_quality.md
@@ -0,0 +1,49 @@
+# Analyze your project's Code Quality
+
+This example shows how to run Code Quality on your code by using GitLab CI/CD
+and Docker.
+
+First, you need GitLab Runner with [docker-in-docker executor][dind].
+
+Once you set up the Runner, add a new job to `.gitlab-ci.yml`, called `code_quality`:
+
+```yaml
+code_quality:
+ image: docker:stable
+ variables:
+ DOCKER_DRIVER: overlay2
+ allow_failure: true
+ services:
+ - docker:stable-dind
+ script:
+ - export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
+ - docker run
+ --env SOURCE_CODE="$PWD"
+ --volume "$PWD":/code
+ --volume /var/run/docker.sock:/var/run/docker.sock
+ "registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code
+ artifacts:
+ paths: [gl-code-quality-report.json]
+```
+
+The above example will create a `code_quality` job in your CI/CD pipeline which
+will scan your source code for code quality issues. The report will be saved
+as an artifact that you can later download and analyze.
+
+TIP: **Tip:**
+Starting with [GitLab Starter][ee] 9.3, this information will
+be automatically extracted and shown right in the merge request widget. To do
+so, the CI/CD job must be named `code_quality` and the artifact path must be
+`gl-code-quality-report.json`.
+[Learn more on Code Quality in merge requests](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality.html).
+
+CAUTION: **Caution:**
+Code Quality was previously using `codeclimate` and `codequality` for job name and
+`codeclimate.json` for the artifact name. While these old names
+are still maintained they have been deprecated with GitLab 11.0 and may be removed
+in next major release, GitLab 12.0. You are advised to update your current `.gitlab-ci.yml`
+configuration to reflect that change.
+
+[cli]: https://github.com/codeclimate/codeclimate
+[dind]: ../docker/using_docker_build.md#use-docker-in-docker-executor
+[ee]: https://about.gitlab.com/pricing/
diff --git a/doc/ci/examples/container_scanning.md b/doc/ci/examples/container_scanning.md
index a9501f6c577..0f79f7d1b17 100644
--- a/doc/ci/examples/container_scanning.md
+++ b/doc/ci/examples/container_scanning.md
@@ -7,10 +7,10 @@ for Vulnerability Static Analysis for containers.
All you need is a GitLab Runner with the Docker executor (the shared Runners on
GitLab.com will work fine). You can then add a new job to `.gitlab-ci.yml`,
-called `sast:container`:
+called `container_scanning`:
```yaml
-sast:container:
+container_scanning:
image: docker:stable
variables:
DOCKER_DRIVER: overlay2
@@ -34,12 +34,12 @@ sast:container:
- retries=0
- echo "Waiting for clair daemon to start"
- while( ! wget -T 10 -q -O /dev/null http://docker:6060/v1/namespaces ) ; do sleep 1 ; echo -n "." ; if [ $retries -eq 10 ] ; then echo " Timeout, aborting." ; exit 1 ; fi ; retries=$(($retries+1)) ; done
- - ./clair-scanner -c http://docker:6060 --ip $(hostname -i) -r gl-sast-container-report.json -l clair.log -w clair-whitelist.yml ${CI_APPLICATION_REPOSITORY}:${CI_APPLICATION_TAG} || true
+ - ./clair-scanner -c http://docker:6060 --ip $(hostname -i) -r gl-container-scanning-report.json -l clair.log -w clair-whitelist.yml ${CI_APPLICATION_REPOSITORY}:${CI_APPLICATION_TAG} || true
artifacts:
- paths: [gl-sast-container-report.json]
+ paths: [gl-container-scanning-report.json]
```
-The above example will create a `sast:container` job in your CI/CD pipeline, pull
+The above example will create a `container_scanning` job in your CI/CD pipeline, pull
the image from the [Container Registry](../../user/project/container_registry.md)
(whose name is defined from the two `CI_APPLICATION_` variables) and scan it
for possible vulnerabilities. The report will be saved as an artifact that you
@@ -52,8 +52,15 @@ in our case its named `clair-whitelist.yml`.
TIP: **Tip:**
Starting with [GitLab Ultimate][ee] 10.4, this information will
be automatically extracted and shown right in the merge request widget. To do
-so, the CI/CD job must be named `sast:container` and the artifact path must be
-`gl-sast-container-report.json`.
+so, the CI/CD job must be named `container_scanning` and the artifact path must be
+`gl-container-scanning-report.json`.
[Learn more on container scanning results shown in merge requests](https://docs.gitlab.com/ee/user/project/merge_requests/container_scanning.html).
-[ee]: https://about.gitlab.com/products/
+CAUTION: **Caution:**
+Before GitLab 11.0, Container Scanning was previously using `sast:container` for job name and
+`gl-sast-container-report.json` for the artifact name. While these old names
+are still maintained, they have been deprecated with GitLab 11.0 and may be removed
+in next major release, GitLab 12.0. You are advised to update your current `.gitlab-ci.yml`
+configuration to reflect that change.
+
+[ee]: https://about.gitlab.com/pricing/
diff --git a/doc/ci/examples/dast.md b/doc/ci/examples/dast.md
index a8720f0b7ea..ff20f0b3b5e 100644
--- a/doc/ci/examples/dast.md
+++ b/doc/ci/examples/dast.md
@@ -60,4 +60,4 @@ so, the CI job must be named `dast` and the artifact path must be
`gl-dast-report.json`.
[Learn more about DAST results shown in merge requests](https://docs.gitlab.com/ee/user/project/merge_requests/dast.html).
-[ee]: https://about.gitlab.com/products/
+[ee]: https://about.gitlab.com/pricing/
diff --git a/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/img/cloud_foundry_secret_variables.png b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/img/cloud_foundry_secret_variables.png
new file mode 100644
index 00000000000..5b5d91ec07a
--- /dev/null
+++ b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/img/cloud_foundry_secret_variables.png
Binary files differ
diff --git a/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/img/create_from_template.png b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/img/create_from_template.png
new file mode 100644
index 00000000000..f3761632556
--- /dev/null
+++ b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/img/create_from_template.png
Binary files differ
diff --git a/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md
new file mode 100644
index 00000000000..b88761be56b
--- /dev/null
+++ b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md
@@ -0,0 +1,142 @@
+---
+author: Dylan Griffith
+author_gitlab: DylanGriffith
+level: intermediary
+article_type: tutorial
+date: 2018-06-07
+description: "Continuous Deployment of a Spring Boot application to Cloud Foundry with GitLab CI/CD"
+---
+
+# Deploy a Spring Boot application to Cloud Foundry with GitLab CI/CD
+
+## Introduction
+
+In this article, we'll demonstrate how to deploy a [Spring
+Boot](https://projects.spring.io/spring-boot/) application to [Cloud
+Foundry (CF)](https://www.cloudfoundry.org/) with GitLab CI/CD using the [Continuous
+Deployment](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#continuous-deployment)
+method.
+
+All the code for this project can be found in this [GitLab
+repo](https://gitlab.com/gitlab-examples/spring-gitlab-cf-deploy-demo).
+
+In case you're interested in deploying Spring Boot applications to Kubernetes
+using GitLab CI/CD, read through the blog post [Continuous Delivery of a Spring Boot application with GitLab CI and Kubernetes](https://about.gitlab.com/2016/12/14/continuous-delivery-of-a-spring-boot-application-with-gitlab-ci-and-kubernetes/).
+
+## Requirements
+
+_We assume you are familiar with Java, GitLab, Cloud Foundry, and GitLab CI/CD._
+
+To follow along with this tutorial you will need the following:
+
+- An account on [Pivotal Web Services (PWS)](https://run.pivotal.io/) or any
+ other Cloud Foundry instance
+- An account on GitLab
+
+NOTE: **Note:**
+You will need to replace the `api.run.pivotal.io` URL in the all below
+commands with the [API
+URL](https://docs.cloudfoundry.org/running/cf-api-endpoint.html) of your CF
+instance if you're not deploying to PWS.
+
+## Create your project
+
+To create your Spring Boot application you can use the Spring template in
+GitLab when creating a new project:
+
+![New Project From Template](img/create_from_template.png)
+
+## Configure the deployment to Cloud Foundry
+
+To deploy to Cloud Foundry we need to add a `manifest.yml` file. This
+is the configuration for the CF CLI we will use to deploy the application. We
+will create this in the root directory of our project with the following
+content:
+
+```yaml
+---
+applications:
+- name: gitlab-hello-world
+ random-route: true
+ memory: 1G
+ path: target/demo-0.0.1-SNAPSHOT.jar
+```
+
+## Configure GitLab CI/CD to deploy your application
+
+Now we need to add the the GitLab CI/CD configuration file
+([`.gitlab-ci.yml`](../../yaml/README.md)) to our
+project's root. This is how GitLab figures out what commands need to be run whenever
+code is pushed to our repository. We will add the following `.gitlab-ci.yml`
+file to the root directory of the repository, GitLab will detect it
+automatically and run the steps defined once we push our code:
+
+```yaml
+image: java:8
+
+stages:
+ - build
+ - deploy
+
+build:
+ stage: build
+ script: ./mvnw package
+ artifacts:
+ paths:
+ - target/demo-0.0.1-SNAPSHOT.jar
+
+production:
+ stage: deploy
+ script:
+ - curl --location "https://cli.run.pivotal.io/stable?release=linux64-binary&source=github" | tar zx
+ - ./cf login -u $CF_USERNAME -p $CF_PASSWORD -a api.run.pivotal.io
+ - ./cf push
+ only:
+ - master
+```
+
+We've used the `java:8` [docker
+image](../../docker/using_docker_images.md) to build
+our application as it provides the up-to-date Java 8 JDK on [Docker
+Hub](https://hub.docker.com/). We've also added the [`only`
+clause](../../yaml/README.md#only-and-except-simplified)
+to ensure our deployments only happen when we push to the master branch.
+
+Now, since the steps defined in `.gitlab-ci.yml` require credentials to login
+to CF, you'll need to add your CF credentials as [environment
+variables](../../variables/README.md#predefined-variables-environment-variables)
+on GitLab CI/CD. To set the environment variables, navigate to your project's
+**Settings > CI/CD** and expand **Secret Variables**. Name the variables
+`CF_USERNAME` and `CF_PASSWORD` and set them to the correct values.
+
+![Secret Variable Settings in GitLab](img/cloud_foundry_secret_variables.png)
+
+Once set up, GitLab CI/CD will deploy your app to CF at every push to your
+repository's deafult branch. To see the build logs or watch your builds running
+live, navigate to **CI/CD > Pipelines**.
+
+CAUTION: **Caution:**
+It is considered best practice for security to create a separate deploy
+user for your application and add its credentials to GitLab instead of using
+a developer's credentials.
+
+To start a manual deployment in GitLab go to **CI/CD > Pipelines** then click
+on **Run Pipeline**. Once the app is finished deploying it will display the URL
+of your application in the logs for the `production` job like:
+
+```shell
+requested state: started
+instances: 1/1
+usage: 1G x 1 instances
+urls: gitlab-hello-world-undissembling-hotchpot.cfapps.io
+last uploaded: Mon Nov 6 10:02:25 UTC 2017
+stack: cflinuxfs2
+buildpack: client-certificate-mapper=1.2.0_RELEASE container-security-provider=1.8.0_RELEASE java-buildpack=v4.5-offline-https://github.com/cloudfoundry/java-buildpack.git#ffeefb9 java-main java-opts jvmkill-agent=1.10.0_RELEASE open-jdk-like-jre=1.8.0_1...
+
+ state since cpu memory disk details
+#0 running 2017-11-06 09:03:22 PM 120.4% 291.9M of 1G 137.6M of 1G
+```
+
+You can then visit your deployed application (for this example,
+https://gitlab-hello-world-undissembling-hotchpot.cfapps.io/) and you should
+see the "Spring is here!" message.
diff --git a/doc/ci/examples/deployment/README.md b/doc/ci/examples/deployment/README.md
index 2dcdc2d41ec..bd60d641493 100644
--- a/doc/ci/examples/deployment/README.md
+++ b/doc/ci/examples/deployment/README.md
@@ -111,7 +111,7 @@ We also use two secure variables:
## Storing API keys
Secure Variables can added by going to your project's
-**Settings âž” CI / CD âž” Secret variables**. The variables that are defined
+**Settings âž” CI / CD âž” Variables**. The variables that are defined
in the project settings are sent along with the build script to the Runner.
The secure variables are stored out of the repository. Never store secrets in
your project's `.gitlab-ci.yml`. It is also important that the secret's value
diff --git a/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md b/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md
index 3d21c0cc306..c226b5bfb71 100644
--- a/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md
+++ b/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md
@@ -406,7 +406,7 @@ and further delves into the principles of GitLab CI/CD than discussed in this ar
We need to be able to deploy to AWS with our AWS account credentials, but we certainly
don't want to put secrets into source code. Luckily GitLab provides a solution for this
-with [Secret Variables](../../../ci/variables/README.md). This can get complicated
+with [Variables](../../../ci/variables/README.md). This can get complicated
due to [IAM](https://aws.amazon.com/iam/) management. As a best practice, you shouldn't
use root security credentials. Proper IAM credential management is beyond the scope of this
article, but AWS will remind you that using root credentials is unadvised and against their
@@ -428,7 +428,7 @@ fully understand [IAM Best Practices in AWS](http://docs.aws.amazon.com/IAM/late
To deploy our build artifacts, we need to install the [AWS CLI](https://aws.amazon.com/cli/) on
the Shared Runner. The Shared Runner also needs to be able to authenticate with your AWS
account to deploy the artifacts. By convention, AWS CLI will look for `AWS_ACCESS_KEY_ID`
-and `AWS_SECRET_ACCESS_KEY`. GitLab's CI gives us a way to pass the secret variables we
+and `AWS_SECRET_ACCESS_KEY`. GitLab's CI gives us a way to pass the variables we
set up in the prior section using the `variables` portion of the `deploy` job. At the end,
we add directives to ensure deployment `only` happens on pushes to `master`. This way, every
single branch still runs through CI, and only merging (or committing directly) to master will
diff --git a/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md b/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md
index 1f9b9d53fc1..39c65399332 100644
--- a/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md
+++ b/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md
@@ -116,11 +116,11 @@ cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
cat ~/.ssh/id_rsa
```
-Now, let's add it to your GitLab project as a [secret variable](../../variables/README.md#secret-variables).
-Secret variables are user-defined variables and are stored out of `.gitlab-ci.yml`, for security purposes.
+Now, let's add it to your GitLab project as a [variable](../../variables/README.md#variables).
+Variables are user-defined variables and are stored out of `.gitlab-ci.yml`, for security purposes.
They can be added per project by navigating to the project's **Settings** > **CI/CD**.
-![secret variables page](img/secret_variables_page.png)
+![variables page](img/secret_variables_page.png)
To the field **KEY**, add the name `SSH_PRIVATE_KEY`, and to the **VALUE** field, paste the private key you've copied earlier.
We'll use this variable in the `.gitlab-ci.yml` later, to easily connect to our remote server as the deployer user without entering its password.
diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md
index b16cbc61d14..4e964af97f5 100644
--- a/doc/ci/pipelines.md
+++ b/doc/ci/pipelines.md
@@ -258,7 +258,7 @@ on that specific branch:
- trigger **manual actions** on existing pipelines
- **retry/cancel** existing jobs (using Web UI or Pipelines API)
-**Secret variables** marked as **protected** are accessible only to jobs that
+**Variables** marked as **protected** are accessible only to jobs that
run on protected branches, avoiding untrusted users to get unintended access to
sensitive information like deployment credentials and tokens.
diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md
index 703a7f030ed..8f1ff190804 100644
--- a/doc/ci/runners/README.md
+++ b/doc/ci/runners/README.md
@@ -84,7 +84,7 @@ visit the project you want to make the Runner work for in GitLab:
## Registering a group Runner
-Creating a group Runner requires Master permissions for the group. To create a
+Creating a group Runner requires Maintainer permissions for the group. To create a
group Runner visit the group you want to make the Runner work for in GitLab:
1. Go to **Settings > CI/CD** to obtain the token
@@ -120,9 +120,9 @@ To lock/unlock a Runner:
## Assigning a Runner to another project
-If you are Master on a project where a specific Runner is assigned to, and the
+If you are Maintainer on a project where a specific Runner is assigned to, and the
Runner is not [locked only to that project](#locking-a-specific-runner-from-being-enabled-for-other-projects),
-you can enable the Runner also on any other project where you have Master permissions.
+you can enable the Runner also on any other project where you have Maintainer permissions.
To enable/disable a Runner in your project:
@@ -132,7 +132,7 @@ To enable/disable a Runner in your project:
> **Note**:
Consider that if you don't lock your specific Runner to a specific project, any
-user with Master role in you project can assign your Runner to another arbitrary
+user with Maintainer role in you project can assign your Runner to another arbitrary
project without requiring your authorization, so use it with caution.
An admin can enable/disable a specific Runner for projects:
diff --git a/doc/ci/ssh_keys/README.md b/doc/ci/ssh_keys/README.md
index 693c8e9ef18..4cb05509e7b 100644
--- a/doc/ci/ssh_keys/README.md
+++ b/doc/ci/ssh_keys/README.md
@@ -25,7 +25,7 @@ with any type of [executor](https://docs.gitlab.com/runner/executors/)
## How it works
1. Create a new SSH key pair locally with [ssh-keygen](http://linux.die.net/man/1/ssh-keygen)
-1. Add the private key as a [secret variable](../variables/README.md) to
+1. Add the private key as a [variable](../variables/README.md) to
your project
1. Run the [ssh-agent](http://linux.die.net/man/1/ssh-agent) during job to load
the private key.
@@ -49,7 +49,7 @@ to access it. This is where an SSH key pair comes in handy.
**Do not** add a passphrase to the SSH key, or the `before_script` will\
prompt for it.
-1. Create a new [secret variable](../variables/README.md#secret-variables).
+1. Create a new [variable](../variables/README.md#variables).
As **Key** enter the name `SSH_PRIVATE_KEY` and in the **Value** field paste
the content of your _private_ key that you created earlier.
@@ -157,7 +157,7 @@ ssh-keyscan example.com
ssh-keyscan 1.2.3.4
```
-Create a new [secret variable](../variables/README.md#secret-variables) with
+Create a new [variable](../variables/README.md#variables) with
`SSH_KNOWN_HOSTS` as "Key", and as a "Value" add the output of `ssh-keyscan`.
NOTE: **Note:**
@@ -165,7 +165,7 @@ If you need to connect to multiple servers, all the server host keys
need to be collected in the **Value** of the variable, one key per line.
TIP: **Tip:**
-By using a secret variable instead of `ssh-keyscan` directly inside
+By using a variable instead of `ssh-keyscan` directly inside
`.gitlab-ci.yml`, it has the benefit that you don't have to change `.gitlab-ci.yml`
if the host domain name changes for some reason. Also, the values are predefined
by you, meaning that if the host keys suddenly change, the CI/CD job will fail,
diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md
index 47a576fdf5f..c213b096a14 100644
--- a/doc/ci/triggers/README.md
+++ b/doc/ci/triggers/README.md
@@ -53,7 +53,7 @@ The action is irreversible.
it will not trigger a job.
- If your project is public, passing the token in plain text is probably not the
wisest idea, so you might want to use a
- [secret variable](../variables/README.md#secret-variables) for that purpose.
+ [variable](../variables/README.md#variables) for that purpose.
To trigger a job you need to send a `POST` request to GitLab's API endpoint:
@@ -219,7 +219,7 @@ removed with one of the future versions of GitLab. You are advised to
[ee-2017]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2017
[ci-229]: https://gitlab.com/gitlab-org/gitlab-ci/merge_requests/229
-[ee]: https://about.gitlab.com/products/
+[ee]: https://about.gitlab.com/pricing/
[variables]: ../variables/README.md
[predef]: ../variables/README.md#predefined-variables-environment-variables
[registry]: ../../user/project/container_registry.md
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index aedf7958c8a..9f6476edc34 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -10,17 +10,23 @@ The variables can be overwritten and they take precedence over each other in
this order:
1. [Trigger variables][triggers] or [scheduled pipeline variables](../../user/project/pipelines/schedules.md#making-use-of-scheduled-pipeline-variables) (take precedence over all)
-1. Project-level [secret variables](#secret-variables) or [protected secret variables](#protected-secret-variables)
-1. Group-level [secret variables](#secret-variables) or [protected secret variables](#protected-secret-variables)
+1. Project-level [variables](#variables) or [protected variables](#protected-variables)
+1. Group-level [variables](#variables) or [protected variables](#protected-variables)
1. YAML-defined [job-level variables](../yaml/README.md#variables)
1. YAML-defined [global variables](../yaml/README.md#variables)
1. [Deployment variables](#deployment-variables)
1. [Predefined variables](#predefined-variables-environment-variables) (are the
lowest in the chain)
-For example, if you define `API_TOKEN=secure` as a secret variable and
+For example, if you define `API_TOKEN=secure` as a project variable and
`API_TOKEN=yaml` in your `.gitlab-ci.yml`, the `API_TOKEN` will take the value
-`secure` as the secret variables are higher in the chain.
+`secure` as the project variables are higher in the chain.
+
+## Unsupported variables
+
+There are cases where some variables cannot be used in the context of a
+`.gitlab-ci.yml` definition (for example under `script`). Read more
+about which variables are [not supported](where_variables_can_be_used.md).
## Predefined variables (Environment variables)
@@ -36,6 +42,7 @@ future GitLab releases.**
| Variable | GitLab | Runner | Description |
|-------------------------------- |--------|--------|-------------|
+| **ARTIFACT_DOWNLOAD_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to download artifacts running a job |
| **CI** | all | 0.4 | Mark that job is executed in CI environment |
| **CI_COMMIT_REF_NAME** | 9.0 | all | The branch or tag name for which project is built |
| **CI_COMMIT_REF_SLUG** | 9.0 | all | `$CI_COMMIT_REF_NAME` lowercased, shortened to 63 bytes, and with everything except `0-9` and `a-z` replaced with `-`. No leading / trailing `-`. Use in URLs, host names and domain names. |
@@ -46,6 +53,8 @@ future GitLab releases.**
| **CI_COMMIT_DESCRIPTION** | 10.8 | all | The description of the commit: the message without first line, if the title is shorter than 100 characters; full message in other case. |
| **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_DEPLOY_USER** | 10.8 | all | Authentication username of the [GitLab Deploy Token][gitlab-deploy-token], only present if the Project has one related.|
+| **CI_DEPLOY_PASSWORD** | 10.8 | all | Authentication password of the [GitLab Deploy Token][gitlab-deploy-token], only present if the Project has one related.|
| **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. |
@@ -55,6 +64,7 @@ future GitLab releases.**
| **CI_JOB_NAME** | 9.0 | 0.5 | The name of the job as defined in `.gitlab-ci.yml` |
| **CI_JOB_STAGE** | 9.0 | 0.5 | The name of the stage as defined in `.gitlab-ci.yml` |
| **CI_JOB_TOKEN** | 9.0 | 1.2 | Token used for authenticating with the GitLab Container Registry |
+| **CI_JOB_URL** | 11.1 | 0.5 | Job details URL |
| **CI_REPOSITORY_URL** | 9.0 | all | The URL to clone the Git repository |
| **CI_RUNNER_DESCRIPTION** | 8.10 | 0.5 | The description of the runner as saved in GitLab |
| **CI_RUNNER_ID** | 8.10 | 0.5 | The unique id of runner being used |
@@ -63,6 +73,7 @@ future GitLab releases.**
| **CI_RUNNER_REVISION** | all | 10.6 | GitLab Runner revision that is executing the current job |
| **CI_RUNNER_EXECUTABLE_ARCH** | all | 10.6 | The OS/architecture of the GitLab Runner executable (note that this is not necessarily the same as the environment of the executor) |
| **CI_PIPELINE_ID** | 8.10 | 0.5 | The unique id of the current pipeline that GitLab CI uses internally |
+| **CI_PIPELINE_IID** | 11.0 | all | The unique id of the current pipeline scoped to project |
| **CI_PIPELINE_TRIGGERED** | all | all | The flag to indicate that job was [triggered] |
| **CI_PIPELINE_SOURCE** | 10.0 | all | Indicates how the pipeline was triggered. Possible options are: `push`, `web`, `trigger`, `schedule`, `api`, and `pipeline`. For pipelines created before GitLab 9.5, this will show as `unknown` |
| **CI_PROJECT_DIR** | all | all | The full path where the repository is cloned and where the job is run |
@@ -71,6 +82,7 @@ future GitLab releases.**
| **CI_PROJECT_NAMESPACE** | 8.10 | 0.5 | The project namespace (username or groupname) that is currently being built |
| **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_PIPELINE_URL** | 11.1 | 0.5 | Pipeline details URL |
| **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 |
@@ -82,16 +94,13 @@ future GitLab releases.**
| **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 |
-| **GITLAB_USER_ID** | 8.12 | all | The id of the user who started the job |
| **GITLAB_USER_EMAIL** | 8.12 | all | The email of the user who started the job |
+| **GITLAB_USER_ID** | 8.12 | all | The id of the user who started the job |
| **GITLAB_USER_LOGIN** | 10.0 | all | The login username of the user who started the job |
| **GITLAB_USER_NAME** | 10.0 | all | The real name of the user who started the job |
| **RESTORE_CACHE_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to restore the cache running a job |
-| **CI_DEPLOY_USER** | 10.8 | all | Authentication username of the [GitLab Deploy Token][gitlab-deploy-token], only present if the Project has one related.|
-| **CI_DEPLOY_PASSWORD** | 10.8 | all | Authentication password of the [GitLab Deploy Token][gitlab-deploy-token], only present if the Project has one related.|
## 9.0 Renaming
@@ -158,49 +167,49 @@ script:
- 'eval $LS_CMD' # will execute 'ls -al $TMP_DIR'
```
-## Secret variables
+## Variables
NOTE: **Note:**
-Group-level secret variables were added in GitLab 9.4.
+Group-level variables were added in GitLab 9.4.
CAUTION: **Important:**
-Be aware that secret variables are not masked, and their values can be shown
+Be aware that variables are not masked, and their values can be shown
in the job logs if explicitly asked to do so. If your project is public or
internal, you can set the pipelines private from your [project's Pipelines
settings](../../user/project/pipelines/settings.md#visibility-of-pipelines).
-Follow the discussion in issue [#13784][ce-13784] for masking the secret variables.
+Follow the discussion in issue [#13784][ce-13784] for masking the variables.
-GitLab CI allows you to define per-project or per-group secret variables
-that are set in the pipeline environment. The secret variables are stored out of
+GitLab CI allows you to define per-project or per-group variables
+that are set in the pipeline environment. The variables are stored out of
the repository (not in `.gitlab-ci.yml`) and are securely passed to GitLab Runner
making them available during a pipeline run. It's the recommended method to
use for storing things like passwords, SSH keys and credentials.
-Project-level secret variables can be added by going to your project's
-**Settings > CI/CD**, then finding the section called **Secret variables**.
+Project-level variables can be added by going to your project's
+**Settings > CI/CD**, then finding the section called **Variables**.
-Likewise, group-level secret variables can be added by going to your group's
-**Settings > CI/CD**, then finding the section called **Secret variables**.
+Likewise, group-level variables can be added by going to your group's
+**Settings > CI/CD**, then finding the section called **Variables**.
Any variables of [subgroups] will be inherited recursively.
-![Secret variables](img/secret_variables.png)
+![Variables](img/secret_variables.png)
Once you set them, they will be available for all subsequent pipelines. You can also
-[protect your variables](#protected-secret-variables).
+[protect your variables](#protected-variables).
-### Protected secret variables
+### Protected variables
>**Notes:**
This feature requires GitLab 9.3 or higher.
-Secret variables could be protected. Whenever a secret variable is
+Variables could be protected. Whenever a variable is
protected, it would only be securely passed to pipelines running on the
[protected branches] or [protected tags]. The other pipelines would not get any
protected variables.
Protected variables can be added by going to your project's
**Settings > CI/CD**, then finding the section called
-**Secret variables**, and check "Protected".
+**Variables**, and check "Protected".
Once you set them, they will be available for all subsequent pipelines.
@@ -215,8 +224,8 @@ are set in the build environment. These variables are only defined for
[deployment jobs](../environments.md). Please consult the documentation of
the project services that you are using to learn which variables they define.
-An example project service that defines deployment variables is
-[Kubernetes Service](../../user/project/integrations/kubernetes.md#deployment-variables).
+An example project service that defines deployment variables is the
+[Kubernetes integration](../../user/project/clusters/index.md#deployment-variables).
## Debug tracing
@@ -224,7 +233,7 @@ An example project service that defines deployment variables is
CAUTION: **Warning:**
Enabling debug tracing can have severe security implications. The
-output **will** contain the content of all your secret variables and any other
+output **will** contain the content of all your variables and any other
secrets! The output **will** be uploaded to the GitLab server and made visible
in job traces!
@@ -346,6 +355,8 @@ Running on runner-8a2f473d-project-1796893-concurrent-0 via runner-8a2f473d-mach
++ CI_PROJECT_URL=https://example.com/gitlab-examples/ci-debug-trace
++ export CI_PIPELINE_ID=52666
++ CI_PIPELINE_ID=52666
+++ export CI_PIPELINE_IID=123
+++ CI_PIPELINE_IID=123
++ export CI_RUNNER_ID=1337
++ CI_RUNNER_ID=1337
++ export CI_RUNNER_DESCRIPTION=shared-runners-manager-1.example.com
@@ -410,7 +421,7 @@ job_name:
```
You can also list all environment variables with the `export` command,
-but be aware that this will also expose the values of all the secret variables
+but be aware that this will also expose the values of all the variables
you set, in the job log:
```yaml
@@ -433,6 +444,7 @@ export CI_JOB_MANUAL="true"
export CI_JOB_TRIGGERED="true"
export CI_JOB_TOKEN="abcde-1234ABCD5678ef"
export CI_PIPELINE_ID="1000"
+export CI_PIPELINE_IID="10"
export CI_PROJECT_ID="34"
export CI_PROJECT_DIR="/builds/gitlab-org/gitlab-ce"
export CI_PROJECT_NAME="gitlab-ce"
@@ -462,7 +474,7 @@ It is possible to use variables expressions with only / except policies in
`.gitlab-ci.yml`. By using this approach you can limit what jobs are going to
be created within a pipeline after pushing a code to GitLab.
-This is particularly useful in combination with secret variables and triggered
+This is particularly useful in combination with variables and triggered
pipeline variables.
```yaml
@@ -540,36 +552,8 @@ Below you can find supported syntax reference:
Pattern matching is case-sensitive by default. Use `i` flag modifier, like
`/pattern/i` to make a pattern case-insensitive.
-### Unsupported predefined variables
-
-Because GitLab evaluates variables before creating jobs, we do not support a
-few variables that depend on persistence layer, like `$CI_JOB_ID`.
-
-Environments (like `production` or `staging`) are also being created based on
-what jobs pipeline consists of, thus some environment-specific variables are
-not supported as well.
-
-We do not support variables containing tokens because of security reasons.
-
-You can find a full list of unsupported variables below:
-
-- `CI_PIPELINE_ID`
-- `CI_JOB_ID`
-- `CI_JOB_TOKEN`
-- `CI_BUILD_ID`
-- `CI_BUILD_TOKEN`
-- `CI_REGISTRY_USER`
-- `CI_REGISTRY_PASSWORD`
-- `CI_REPOSITORY_URL`
-- `CI_ENVIRONMENT_URL`
-- `CI_DEPLOY_USER`
-- `CI_DEPLOY_PASSWORD`
-
-These variables are also not supported in a context of a
-[dynamic environment name][dynamic-environments].
-
-[ce-13784]: https://gitlab.com/gitlab-org/gitlab-ce/issues/13784 "Simple protection of CI secret variables"
-[eep]: https://about.gitlab.com/products/ "Available only in GitLab Premium"
+[ce-13784]: https://gitlab.com/gitlab-org/gitlab-ce/issues/13784 "Simple protection of CI variables"
+[eep]: https://about.gitlab.com/pricing/ "Available only in GitLab Premium"
[envs]: ../environments.md
[protected branches]: ../../user/project/protected_branches.md
[protected tags]: ../../user/project/protected_tags.md
@@ -579,5 +563,4 @@ These variables are also not supported in a context of a
[triggers]: ../triggers/README.md#pass-job-variables-to-a-trigger
[subgroups]: ../../user/group/subgroups/index.md
[builds-policies]: ../yaml/README.md#only-and-except-complex
-[dynamic-environments]: ../environments.md#dynamic-environments
[gitlab-deploy-token]: ../../user/project/deploy_tokens/index.md#gitlab-deploy-token
diff --git a/doc/ci/variables/where_variables_can_be_used.md b/doc/ci/variables/where_variables_can_be_used.md
new file mode 100644
index 00000000000..b2b4a26bdda
--- /dev/null
+++ b/doc/ci/variables/where_variables_can_be_used.md
@@ -0,0 +1,113 @@
+# Where variables can be used
+
+As it's described in the [CI/CD variables](README.md) docs, you can
+define many different variables. Some of them can be used for all GitLab CI/CD
+features, but some of them are more or less limited.
+
+This document describes where and how the different types of variables can be used.
+
+## Variables usage
+
+There are basically two places where you can use any defined variables:
+
+1. On GitLab's side there's `.gitlab-ci.yml`
+1. On the Runner's side there's `config.toml`
+
+### `.gitlab-ci.yml` file
+
+| Definition | Can be expanded? | Expansion place | Description |
+|--------------------------------------|-------------------|-----------------|--------------|
+| `environment:url` | yes | GitLab | The variable expansion is made by GitLab's [internal variable expansion mechanism](#gitlab-internal-variable-expansion-mechanism).<ul><li>**Supported:** all variables defined for a job (project/group variables, variables from `.gitlab-ci.yml`, variables from triggers, variables from pipeline schedules)</li><li>**Not suported:** variables defined in Runner's `config.toml` and variables created in job's `script`</li></ul> |
+| `environment:name` | yes | GitLab | Similar to `environment:url`, but the variables expansion **doesn't support**: <ul><li>variables that are based on the environment's name (`CI_ENVIRONMENT_NAME`, `CI_ENVIRONMENT_SLUG`)</li><li>any other variables related to environment (currently only `CI_ENVIRONMENT_URL`)</li><li>[persisted variables](#persisted-variables)</li></ul> |
+| `variables` | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) |
+| `image` | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) |
+| `services:[]` | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) |
+| `services:[]:name` | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) |
+| `cache:key` | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) |
+| `artifacts:name` | yes | Runner | The variable expansion is made by GitLab Runner's shell environment |
+| `script`, `before_script`, `after_script` | yes | Script execution shell | The variable expansion is made by the [execution shell environment](#execution-shell-environment) |
+| `only:variables:[]`, `except:variables:[]` | no | n/a | The variable must be in the form of `$variable`.<br/>**Not supported:**<ul><li>variables that are based on the environment's name (`CI_ENVIRONMENT_NAME`, `CI_ENVIRONMENT_SLUG`)</li><li>any other variables related to environment (currently only `CI_ENVIRONMENT_URL`)</li><li>[persisted variables](#persisted-variables)</li></ul> |
+
+### `config.toml` file
+
+NOTE: **Note:**
+You can read more about `config.toml` in the [Runner's docs](https://docs.gitlab.com/runner/configuration/advanced-configuration.html).
+
+| Definition | Can be expanded? | Description |
+|--------------------------------------|------------------|-------------|
+| `runners.environment` | yes | The variable expansion is made by the Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) |
+| `runners.kubernetes.pod_labels` | yes | The Variable expansion is made by the Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) |
+| `runners.kubernetes.pod_annotations` | yes | The Variable expansion is made by the Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) |
+
+## Expansion mechanisms
+
+There are three expansion mechanisms:
+
+- GitLab
+- GitLab Runner
+- Execution shell environment
+
+### GitLab internal variable expansion mechanism
+
+The expanded part needs to be in a form of `$variable`, or `${variable}` or `%variable%`.
+Each form is handled in the same way, no matter which OS/shell will finally handle the job,
+since the expansion is done in GitLab before any Runner will get the job.
+
+### GitLab Runner internal variable expansion mechanism
+
+- **Supported:** project/group variables, `.gitlab-ci.yml` variables, `config.toml` variables, and
+ variables from triggers and pipeline schedules
+- **Not supported:** variables defined inside of scripts (e.g., `export MY_VARIABLE="test"`)
+
+The Runner uses Go's `os.Expand()` method for variable expansion. It means that it will handle
+only variables defined as `$variable` and `${variable}`. What's also important, is that
+the expansion is done only once, so nested variables may or may not work, depending on the
+ordering of variables definitions.
+
+### Execution shell environment
+
+This is an expansion that takes place during the `script` execution.
+How it works depends on the used shell (bash/sh/cmd/PowerShell). For example, if the job's
+`script` contains a line `echo $MY_VARIABLE-${MY_VARIABLE_2}`, it should be properly handled
+by bash/sh (leaving empty strings or some values depending whether the variables were
+defined or not), but will not work with Windows' cmd/PowerShell, since these shells
+are using a different variables syntax.
+
+**Supported:**
+
+- The `script` may use all available variables that are default for the shell (e.g., `$PATH` which
+ should be present in all bash/sh shells) and all variables defined by GitLab CI/CD (project/group variables,
+ `.gitlab-ci.yml` variables, `config.toml` variables, and variables from triggers and pipeline schedules).
+- The `script` may also use all variables defined in the lines before. So, for example, if you define
+ a variable `export MY_VARIABLE="test"`:
+
+ - in `before_script`, it will work in the following lines of `before_script` and
+ all lines of the related `script`
+ - in `script`, it will work in the following lines of `script`
+ - in `after_script`, it will work in following lines of `after_script`
+
+## Persisted variables
+
+NOTE: **Note:**
+Some of the persisted variables contain tokens and cannot be used by some definitions
+due to security reasons.
+
+The following variables are known as "persisted":
+
+- `CI_PIPELINE_ID`
+- `CI_JOB_ID`
+- `CI_JOB_TOKEN`
+- `CI_BUILD_ID`
+- `CI_BUILD_TOKEN`
+- `CI_REGISTRY_USER`
+- `CI_REGISTRY_PASSWORD`
+- `CI_REPOSITORY_URL`
+- `CI_DEPLOY_USER`
+- `CI_DEPLOY_PASSWORD`
+
+They are:
+
+- **supported** for all definitions as [described in the table](#gitlab-ci-yml-file) where the "Expansion place" is "Runner"
+- **not supported:**
+ - by the definitions [described in the table](#gitlab-ci-yml-file) where the "Expansion place" is "GitLab"
+ - in the `only` and `except` [variables expressions](README.md#variables-expressions)
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 3e77a6f58b7..096b64eb881 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -88,18 +88,18 @@ The example below simply moves all files from the root of the project to the
`public/` directory. The `.public` workaround is so `cp` doesn't also copy
`public/` to itself in an infinite loop:
-```
+```yaml
pages:
stage: deploy
script:
- - mkdir .public
- - cp -r * .public
- - mv .public public
+ - mkdir .public
+ - cp -r * .public
+ - mv .public public
artifacts:
paths:
- - public
+ - public
only:
- - master
+ - master
```
Read more on [GitLab Pages user documentation](../../user/project/pages/index.md).
@@ -131,15 +131,15 @@ if you set it per-job:
```yaml
before_script:
-- global before script
+ - global before script
job:
before_script:
- - execute this instead of global before script
+ - execute this instead of global before script
script:
- - my command
+ - my command
after_script:
- - execute this after my script
+ - execute this after my script
```
## `stages`
@@ -327,7 +327,7 @@ Refs strategy equals to simplified only/except configuration, whereas
kubernetes strategy accepts only `active` keyword.
`variables` keyword is used to define variables expressions. In other words
-you can use predefined variables / secret variables / project / group or
+you can use predefined variables / project / group or
environment-scoped variables to define an expression GitLab is going to
evaluate in order to decide whether a job should be created or not.
@@ -409,18 +409,18 @@ fails, it will not stop the next stage from running, since it's marked with
job1:
stage: test
script:
- - execute_script_that_will_fail
+ - execute_script_that_will_fail
allow_failure: true
job2:
stage: test
script:
- - execute_script_that_will_succeed
+ - execute_script_that_will_succeed
job3:
stage: deploy
script:
- - deploy_to_staging
+ - deploy_to_staging
```
## `when`
@@ -442,38 +442,38 @@ For example:
```yaml
stages:
-- build
-- cleanup_build
-- test
-- deploy
-- cleanup
+ - build
+ - cleanup_build
+ - test
+ - deploy
+ - cleanup
build_job:
stage: build
script:
- - make build
+ - make build
cleanup_build_job:
stage: cleanup_build
script:
- - cleanup build when failed
+ - cleanup build when failed
when: on_failure
test_job:
stage: test
script:
- - make test
+ - make test
deploy_job:
stage: deploy
script:
- - make deploy
+ - make deploy
when: manual
cleanup_job:
stage: cleanup
script:
- - cleanup after jobs
+ - cleanup after jobs
when: always
```
@@ -734,8 +734,8 @@ rspec:
script: test
cache:
paths:
- - binaries/*.apk
- - .config
+ - binaries/*.apk
+ - .config
```
Locally defined cache overrides globally defined options. The following `rspec`
@@ -744,14 +744,14 @@ job will cache only `binaries/`:
```yaml
cache:
paths:
- - my/files
+ - my/files
rspec:
script: test
cache:
key: rspec
paths:
- - binaries/
+ - binaries/
```
Note that since cache is shared between jobs, if you're using different
@@ -786,7 +786,7 @@ For example, to enable per-branch caching:
cache:
key: "$CI_COMMIT_REF_SLUG"
paths:
- - binaries/
+ - binaries/
```
If you use **Windows Batch** to run your shell scripts you need to replace
@@ -796,17 +796,7 @@ If you use **Windows Batch** to run your shell scripts you need to replace
cache:
key: "%CI_COMMIT_REF_SLUG%"
paths:
- - binaries/
-```
-
-If you use **Windows PowerShell** to run your shell scripts you need to replace
-`$` with `$env:`:
-
-```yaml
-cache:
- key: "$env:CI_COMMIT_REF_SLUG"
- paths:
- - binaries/
+ - binaries/
```
### `cache:untracked`
@@ -829,7 +819,7 @@ rspec:
cache:
untracked: true
paths:
- - binaries/
+ - binaries/
```
### `cache:policy`
@@ -907,8 +897,8 @@ Send all files in `binaries` and `.config`:
```yaml
artifacts:
paths:
- - binaries/
- - .config
+ - binaries/
+ - .config
```
To disable artifact passing, define the job with empty [dependencies](#dependencies):
@@ -937,7 +927,7 @@ release-job:
- mvn package -U
artifacts:
paths:
- - target/*.war
+ - target/*.war
only:
- tags
```
@@ -952,6 +942,11 @@ useful when you'd like to download the archive from GitLab. The `artifacts:name`
variable can make use of any of the [predefined variables](../variables/README.md).
The default name is `artifacts`, which becomes `artifacts.zip` when downloaded.
+NOTE: **Note:**
+If your branch-name contains forward slashes
+(e.g. `feature/my-feature`) it is advised to use `$CI_COMMIT_REF_SLUG`
+instead of `$CI_COMMIT_REF_NAME` for proper naming of the artifact.
+
To create an archive with a name of the current job:
```yaml
@@ -959,7 +954,7 @@ job:
artifacts:
name: "$CI_JOB_NAME"
paths:
- - binaries/
+ - binaries/
```
To create an archive with a name of the current branch or tag including only
@@ -970,7 +965,7 @@ job:
artifacts:
name: "$CI_COMMIT_REF_NAME"
paths:
- - binaries/
+ - binaries/
```
To create an archive with a name of the current job and the current branch or
@@ -981,7 +976,7 @@ job:
artifacts:
name: "$CI_JOB_NAME-$CI_COMMIT_REF_NAME"
paths:
- - binaries/
+ - binaries/
```
To create an archive with a name of the current [stage](#stages) and branch name:
@@ -991,7 +986,7 @@ job:
artifacts:
name: "$CI_JOB_STAGE-$CI_COMMIT_REF_NAME"
paths:
- - binaries/
+ - binaries/
```
---
@@ -1004,7 +999,7 @@ job:
artifacts:
name: "%CI_JOB_STAGE%-%CI_COMMIT_REF_NAME%"
paths:
- - binaries/
+ - binaries/
```
If you use **Windows PowerShell** to run your shell scripts you need to replace
@@ -1015,7 +1010,7 @@ job:
artifacts:
name: "$env:CI_JOB_STAGE-$env:CI_COMMIT_REF_NAME"
paths:
- - binaries/
+ - binaries/
```
### `artifacts:untracked`
@@ -1040,7 +1035,7 @@ Send all Git untracked files and files in `binaries`:
artifacts:
untracked: true
paths:
- - binaries/
+ - binaries/
```
### `artifacts:when`
@@ -1130,26 +1125,26 @@ build:osx:
script: make build:osx
artifacts:
paths:
- - binaries/
+ - binaries/
build:linux:
stage: build
script: make build:linux
artifacts:
paths:
- - binaries/
+ - binaries/
test:osx:
stage: test
script: make test:osx
dependencies:
- - build:osx
+ - build:osx
test:linux:
stage: test
script: make test:linux
dependencies:
- - build:linux
+ - build:linux
deploy:
stage: deploy
@@ -1249,7 +1244,7 @@ Runner itself](../variables/README.md#predefined-variables-environment-variables
One example would be `CI_COMMIT_REF_NAME` which has the value of
the branch or tag name for which project is built. Apart from the variables
you can set in `.gitlab-ci.yml`, there are also the so called
-[secret variables](../variables/README.md#secret-variables)
+[Variables](../variables/README.md#variables)
which can be set in GitLab's UI.
[Learn more about variables and their priority.][variables]
@@ -1416,6 +1411,43 @@ variables:
You can set it globally or per-job in the [`variables`](#variables) section.
+### Custom build directories
+
+> [Introduced][gitlab-runner-876] in Gitlab Runner 11.1
+
+NOTE: **Note:**
+This can only be used when `custom_build_dir` is set to true in the [Runner's
+configuration](https://docs.gitlab.com/runner/configuration/advanced-configuration.html).
+
+By default, GitLab Runner clones the repository in the `/builds` directory,
+but sometimes your project might require to have the code in a specific
+directory, like the GO projects for example. In that case, you can specify
+the `CI_PROJECT_DIR` variable to tell the Runner in which directory to clone
+the repository:
+
+```yml
+image: golang:1.10-alpine3.7
+
+variables:
+ CI_PROJECT_DIR: /go/src/gitlab.com/namespace/project-name
+
+stages:
+ - test
+
+dir:
+ stage: test
+ script:
+ - pwd # /go/src/gitlab.com/namespace/project-name
+```
+
+The following executors may use this feature only when
+[concurrent](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-global-section)
+is set to `1`:
+
+- `shell`
+- `ssh`
+- `docker`, `docker+machine` when the job's working directory is mounted as a host volume.
+
## Special YAML features
It's possible to use special YAML features like anchors (`&`), aliases (`*`)
@@ -1609,5 +1641,6 @@ CI with various languages.
[ce-7983]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7983
[ce-7447]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7447
[ce-12909]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12909
+[gitlab-runner-876]: https://gitlab.com/gitlab-org/gitlab-runner/merge_requests/876
[schedules]: ../../user/project/pipelines/schedules.md
[variables-expressions]: ../variables/README.md#variables-expressions
diff --git a/doc/customization/favicon.md b/doc/customization/favicon.md
new file mode 100644
index 00000000000..45a18159b5e
--- /dev/null
+++ b/doc/customization/favicon.md
@@ -0,0 +1,16 @@
+# Changing the favicon
+
+> [Introduced][ce-14497] in GitLab 11.0.
+
+[ce-14497]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14497
+
+Navigate to the **Admin** area and go to the **Appearance** page.
+
+Upload the custom favicon (**Favicon**) in the section **Favicon**.
+
+![appearance](favicon/appearance.png)
+
+After saving the page, the new favicon will be shown in the browser. The main
+favicon as well as the CI status icons will show the custom icon:
+
+![custom_favicon](favicon/custom_favicon.png)
diff --git a/doc/customization/favicon/appearance.png b/doc/customization/favicon/appearance.png
new file mode 100644
index 00000000000..6c41a05fc1f
--- /dev/null
+++ b/doc/customization/favicon/appearance.png
Binary files differ
diff --git a/doc/customization/favicon/custom_favicon.png b/doc/customization/favicon/custom_favicon.png
new file mode 100644
index 00000000000..fa1b8827a36
--- /dev/null
+++ b/doc/customization/favicon/custom_favicon.png
Binary files differ
diff --git a/doc/development/README.md b/doc/development/README.md
index 898c60e96c0..5d6fed5bc72 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -32,6 +32,8 @@ description: 'Learn how to contribute to GitLab.'
- [GitLab utilities](utilities.md)
- [API styleguide](api_styleguide.md) Use this styleguide if you are
contributing to the API.
+- [GraphQL API styleguide](api_graphql_styleguide.md) Use this
+ styleguide if you are contribution to the [GraphQL API](../api/graphql/index.md)
- [Sidekiq guidelines](sidekiq_style_guide.md) for working with Sidekiq workers
- [Working with Gitaly](gitaly.md)
- [Manage feature flags](feature_flags.md)
@@ -86,8 +88,8 @@ description: 'Learn how to contribute to GitLab.'
## Documentation guides
-- [Writing documentation](writing_documentation.md)
-- [Documentation styleguide](doc_styleguide.md)
+- [Writing documentation](documentation/index.md)
+- [Documentation styleguide](documentation/styleguide.md)
- [Markdown](../user/markdown.md)
## Internationalization (i18n) guides
diff --git a/doc/development/api_graphql_styleguide.md b/doc/development/api_graphql_styleguide.md
new file mode 100644
index 00000000000..b4a2349844b
--- /dev/null
+++ b/doc/development/api_graphql_styleguide.md
@@ -0,0 +1,214 @@
+# GraphQL API
+
+## Authentication
+
+Authentication happens through the `GraphqlController`, right now this
+uses the same authentication as the Rails application. So the session
+can be shared.
+
+It is also possible to add a `private_token` to the querystring, or
+add a `HTTP_PRIVATE_TOKEN` header.
+
+### Authorization
+
+Fields can be authorized using the same abilities used in the Rails
+app. This can be done using the `authorize` helper:
+
+```ruby
+module Types
+ class QueryType < BaseObject
+ graphql_name 'Query'
+
+ field :project, Types::ProjectType, null: true, resolver: Resolvers::ProjectResolver do
+ authorize :read_project
+ end
+ end
+```
+
+The object found by the resolve call is used for authorization.
+
+This works for authorizing a single record, for authorizing
+collections, we should only load what the currently authenticated user
+is allowed to view. Preferably we use our existing finders for that.
+
+## Types
+
+When exposing a model through the GraphQL API, we do so by creating a
+new type in `app/graphql/types`.
+
+When exposing properties in a type, make sure to keep the logic inside
+the definition as minimal as possible. Instead, consider moving any
+logic into a presenter:
+
+```ruby
+class Types::MergeRequestType < BaseObject
+ present_using MergeRequestPresenter
+
+ name 'MergeRequest'
+end
+```
+
+An existing presenter could be used, but it is also possible to create
+a new presenter specifically for GraphQL.
+
+The presenter is initialized using the object resolved by a field, and
+the context.
+
+### Connection Types
+
+GraphQL uses [cursor based
+pagination](https://graphql.org/learn/pagination/#pagination-and-edges)
+to expose collections of items. This provides the clients with a lot
+of flexibility while also allowing the backend to use different
+pagination models.
+
+To expose a collection of resources we can use a connection type. This wraps the array with default pagination fields. For example a query for project-pipelines could look like this:
+
+```
+query($project_path: ID!) {
+ project(fullPath: $project_path) {
+ pipelines(first: 2) {
+ pageInfo {
+ hasNextPage
+ hasPreviousPage
+ }
+ edges {
+ cursor
+ node {
+ id
+ status
+ }
+ }
+ }
+ }
+}
+```
+
+This would return the first 2 pipelines of a project and related
+pagination info., ordered by descending ID. The returned data would
+look like this:
+
+```json
+{
+ "data": {
+ "project": {
+ "pipelines": {
+ "pageInfo": {
+ "hasNextPage": true,
+ "hasPreviousPage": false
+ },
+ "edges": [
+ {
+ "cursor": "Nzc=",
+ "node": {
+ "id": "77",
+ "status": "FAILED"
+ }
+ },
+ {
+ "cursor": "Njc=",
+ "node": {
+ "id": "67",
+ "status": "FAILED"
+ }
+ }
+ ]
+ }
+ }
+ }
+}
+```
+
+To get the next page, the cursor of the last known element could be
+passed:
+
+```
+query($project_path: ID!) {
+ project(fullPath: $project_path) {
+ pipelines(first: 2, after: "Njc=") {
+ pageInfo {
+ hasNextPage
+ hasPreviousPage
+ }
+ edges {
+ cursor
+ node {
+ id
+ status
+ }
+ }
+ }
+ }
+}
+```
+
+### Exposing permissions for a type
+
+To expose permissions the current user has on a resource, you can call
+the `expose_permissions` passing in a separate type representing the
+permissions for the resource.
+
+For example:
+
+```ruby
+module Types
+ class MergeRequestType < BaseObject
+ expose_permissions Types::MergeRequestPermissionsType
+ end
+end
+```
+
+The permission type inherits from `BasePermissionType` which includes
+some helper methods, that allow exposing permissions as non-nullable
+booleans:
+
+```ruby
+class MergeRequestPermissionsType < BasePermissionType
+ present_using MergeRequestPresenter
+
+ graphql_name 'MergeRequestPermissions'
+
+ abilities :admin_merge_request, :update_merge_request, :create_note
+
+ ability_field :resolve_note,
+ description: 'Whether or not the user can resolve disussions on the merge request'
+ permission_field :push_to_source_branch, method: :can_push_to_source_branch?
+end
+```
+
+- **`permission_field`**: Will act the same as `graphql-ruby`'s
+ `field` method but setting a default description and type and making
+ them non-nullable. These options can still be overridden by adding
+ them as arguments.
+- **`ability_field`**: Expose an ability defined in our policies. This
+ takes behaves the same way as `permission_field` and the same
+ arguments can be overridden.
+- **`abilities`**: Allows exposing several abilities defined in our
+ policies at once. The fields for these will all have be non-nullable
+ booleans with a default description.
+
+## Resolvers
+
+To find objects to display in a field, we can add resolvers to
+`app/graphql/resolvers`.
+
+Arguments can be defined within the resolver, those arguments will be
+made available to the fields using the resolver.
+
+We already have a `FullPathLoader` that can be included in other
+resolvers to quickly find Projects and Namespaces which will have a
+lot of dependant objects.
+
+To limit the amount of queries performed, we can use `BatchLoader`.
+
+## Testing
+
+_full stack_ tests for a graphql query or mutation live in
+`spec/requests/api/graphql`.
+
+When adding a query, the `a working graphql query` shared example can
+be used to test if the query renders valid results.
+
+Using the `GraphqlHelpers#all_graphql_fields_for`-helper, a query
+including all available fields can be constructed. This makes it easy
+to add a test rendering all possible fields for a query.
diff --git a/doc/development/architecture.md b/doc/development/architecture.md
index 31117b5e723..6ca3e9e5a0a 100644
--- a/doc/development/architecture.md
+++ b/doc/development/architecture.md
@@ -2,7 +2,7 @@
## Software delivery
-There are two software distributions of GitLab: the open source [Community Edition](https://gitlab.com/gitlab-org/gitlab-ce/) (CE), and the open core [Enterprise Edition](https://gitlab.com/gitlab-org/gitlab-ee/) (EE). GitLab is available under [different subscriptions](https://about.gitlab.com/products/).
+There are two software distributions of GitLab: the open source [Community Edition](https://gitlab.com/gitlab-org/gitlab-ce/) (CE), and the open core [Enterprise Edition](https://gitlab.com/gitlab-org/gitlab-ee/) (EE). GitLab is available under [different subscriptions](https://about.gitlab.com/pricing/).
New versions of GitLab are released in stable branches and the master branch is for bleeding edge development.
diff --git a/doc/development/changelog.md b/doc/development/changelog.md
index a9fa5ae834f..9e0c81b3d60 100644
--- a/doc/development/changelog.md
+++ b/doc/development/changelog.md
@@ -45,6 +45,8 @@ the `author` field. GitLab team members **should not**.
a changelog entry regardless of these guidelines if the contributor wants one.
Example: "Fixed a typo on the search results page. (Jane Smith)"
- Performance improvements **should** have a changelog entry.
+- Any change that introduces a database migration **must** have a
+ changelog entry.
## Writing good changelog entries
diff --git a/doc/development/code_review.md b/doc/development/code_review.md
index d03b7fa23ca..23c80799235 100644
--- a/doc/development/code_review.md
+++ b/doc/development/code_review.md
@@ -22,7 +22,7 @@ There are a few rules to get your merge request accepted:
1. If your merge request includes UX, frontend and backend changes [^1], it must
be **approved by a [UX team member, a frontend and a backend maintainer][team]**.
1. If your merge request includes a new dependency or a filesystem change, it must
- be **approved by a [Build team member][team]**. See [how to work with the Build team][build handbook] for more details.
+ be *approved by a [Distribution team member][team]*. See how to work with the [Distribution team for more details.](https://about.gitlab.com/handbook/engineering/dev-backend/distribution/)
1. To lower the amount of merge requests maintainers need to review, you can
ask or assign any [reviewers][projects] for a first review.
1. If you need some guidance (e.g. it's your first merge request), feel free
diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md
index 5d595c33915..7d84f8ca86a 100644
--- a/doc/development/doc_styleguide.md
+++ b/doc/development/doc_styleguide.md
@@ -1,499 +1,3 @@
---
-description: 'Writing styles, markup, formatting, and reusing regular expressions throughout the GitLab Documentation.'
+redirect_to: 'documentation/styleguide.md'
---
-
-# Documentation style guidelines
-
-The documentation style guide defines the markup structure used in
-GitLab documentation. Check the
-[documentation guidelines](writing_documentation.md) for general development instructions.
-
-Check the GitLab handbook for the [writing styles guidelines](https://about.gitlab.com/handbook/communication/#writing-style-guidelines).
-
-## Text
-
-- Split up long lines (wrap text), this makes it much easier to review and edit. Only
- double line breaks are shown as a full line break in [GitLab markdown][gfm].
- 80-100 characters is a good line length
-- Make sure that the documentation is added in the correct
- [directory](writing_documentation.md#documentation-directory-structure) and that
- there's a link to it somewhere useful
-- Do not duplicate information
-- Be brief and clear
-- Unless there's a logical reason not to, add documents in alphabetical order
-- Write in US English
-- Use [single spaces][] instead of double spaces
-- Jump a line between different markups (e.g., after every paragraph, header, list, etc)
-- Capitalize "G" and "L" in GitLab
-- Use sentence case for titles, headings, labels, menu items, and buttons.
-- Use title case when referring to [features](https://about.gitlab.com/features/) or [products](https://about.gitlab.com/pricing/), and methods. Note that some features are also objects (e.g. "Merge Requests" and "merge requests"). E.g.: GitLab Runner, Geo, Issue Boards, Git, Prometheus, Continuous Integration.
-
-## Formatting
-
-- Use double asterisks (`**`) to mark a word or text in bold (`**bold**`)
-- Use undescore (`_`) for text in italics (`_italic_`)
-- Jump a line between different markups, for example:
-
- ```md
- ## Header
-
- Paragraph.
-
- - List item
- - List item
- ```
-
-### Punctuation
-
-For punctuation rules, please refer to the [GitLab UX guide](https://design.gitlab.com/content/punctuation/).
-
-### Ordered and unordered lists
-
-- Use dashes (`-`) for unordered lists instead of asterisks (`*`)
-- Use the number one (`1`) for ordered lists
-- For punctuation in bullet lists, please refer to the [GitLab UX guide](https://design.gitlab.com/content/punctuation/)
-
-## Headings
-
-- Add **only one H1** in each document, by adding `#` at the beginning of
- it (when using markdown). The `h1` will be the document `<title>`.
-- For subheadings, use `##`, `###` and so on
-- Avoid putting numbers in headings. Numbers shift, hence documentation anchor
- links shift too, which eventually leads to dead links. If you think it is
- compelling to add numbers in headings, make sure to at least discuss it with
- someone in the Merge Request
-- [Avoid using symbols and special chars](https://gitlab.com/gitlab-com/gitlab-docs/issues/84)
- in headers. Whenever possible, they should be plain and short text.
-- Avoid adding things that show ephemeral statuses. For example, if a feature is
- considered beta or experimental, put this info in a note, not in the heading.
-- When introducing a new document, be careful for the headings to be
- grammatically and syntactically correct. Mention one or all
- of the following GitLab members for a review: `@axil` or `@marcia`.
- This is to ensure that no document with wrong heading is going
- live without an audit, thus preventing dead links and redirection issues when
- corrected
-- Leave exactly one newline after a heading
-
-## Links
-
-- Use the regular inline link markdown markup `[Text](https://example.com)`.
- It's easier to read, review, and maintain.
-- If there's a link that repeats several times through the same document,
- you can use `[Text][identifier]` and at the bottom of the section or the
- document add: `[identifier]: https://example.com`, in which case, we do
- encourage you to also add an alternative text: `[identifier]: https://example.com "Alternative text"` that appears when hovering your mouse on a link.
-- To link to internal documentation, use relative links, not full URLs. Use `../` to
- navigate tp high-level directories, and always add the file name `file.md` at the
- end of the link with the `.md` extension, not `.html`.
- Example: instead of `[text](../../merge_requests/)`, use
- `[text](../../merge_requests/index.md)` or, `[text](../../ci/README.md)`, or,
- for anchor links, `[text](../../ci/README.md#examples)`.
- Using the markdown extension is necessary for the [`/help`](writing_documentation.md#gitlab-help)
- section of GitLab.
-- To link from CE to EE-only documentation, use the EE-only doc full URL.
-- Use [meaningful anchor texts](https://www.futurehosting.com/blog/links-should-have-meaningful-anchor-text-heres-why/).
- E.g., instead of writing something like `Read more about GitLab Issue Boards [here](LINK)`,
- write `Read more about [GitLab Issue Boards](LINK)`.
-
-## Images
-
-- Place images in a separate directory named `img/` in the same directory where
- the `.md` document that you're working on is located. Always prepend their
- names with the name of the document that they will be included in. For
- example, if there is a document called `twitter.md`, then a valid image name
- could be `twitter_login_screen.png`. [**Exception**: images for
- [articles](writing_documentation.md#technical-articles) should be
- put in a directory called `img` underneath `/articles/article_title/img/`, therefore,
- there's no need to prepend the document name to their filenames.]
-- Images should have a specific, non-generic name that will differentiate them.
-- Keep all file names in lower case.
-- Consider using PNG images instead of JPEG.
-- Compress all images with <https://tinypng.com/> or similar tool.
-- Compress gifs with <https://ezgif.com/optimize> or similar tool.
-- Images should be used (only when necessary) to _illustrate_ the description
-of a process, not to _replace_ it.
-
-Inside the document:
-
-- The Markdown way of using an image inside a document is:
- `![Proper description what the image is about](img/document_image_title.png)`
-- Always use a proper description for what the image is about. That way, when a
- browser fails to show the image, this text will be used as an alternative
- description
-- If there are consecutive images with little text between them, always add
- three dashes (`---`) between the image and the text to create a horizontal
- line for better clarity
-- If a heading is placed right after an image, always add three dashes (`---`)
- between the image and the heading
-
-## Alert boxes
-
-Whenever you want to call the attention to a particular sentence,
-use the following markup for highlighting.
-
-_Note that the alert boxes only work for one paragraph only. Multiple paragraphs,
-lists, headers, etc will not render correctly._
-
-### Note
-
-```md
-NOTE: **Note:**
-This is something to note.
-```
-
-How it renders in docs.gitlab.com:
-
-NOTE: **Note:**
-This is something to note.
-
-### Tip
-
-```md
-TIP: **Tip:**
-This is a tip.
-```
-
-How it renders in docs.gitlab.com:
-
-TIP: **Tip:**
-This is a tip.
-
-### Caution
-
-```md
-CAUTION: **Caution:**
-This is something to be cautious about.
-```
-
-How it renders in docs.gitlab.com:
-
-CAUTION: **Caution:**
-This is something to be cautious about.
-
-### Danger
-
-```md
-DANGER: **Danger:**
-This is a breaking change, a bug, or something very important to note.
-```
-
-How it renders in docs.gitlab.com:
-
-DANGER: **Danger:**
-This is a breaking change, a bug, or something very important to note.
-
-## Blockquotes
-
-For highlighting a text within a blue blockquote, use this format:
-
-```md
-> This is a blockquote.
-```
-
-which renders in docs.gitlab.com to:
-
-> This is a blockquote.
-
-If the text spans across multiple lines it's OK to split the line.
-
-## Specific sections and terms
-
-To mention and/or reference specific terms in GitLab, please follow the styles
-below.
-
-### GitLab versions and tiers
-
-- Every piece of documentation that comes with a new feature should declare the
- GitLab version that feature got introduced. Right below the heading add a
- note:
-
- ```md
- > Introduced in GitLab 8.3.
- ```
-
-- Whenever possible, every feature should have a link to the MR, issue, or epic that introduced it.
- The above note would be then transformed to:
-
- ```md
- > [Introduced][ce-1242] in GitLab 8.3.
- ```
-
- , where the [link identifier](#links) is named after the repository (CE) and
- the MR number.
-
-- If the feature is only available in GitLab Enterprise Edition, don't forget to mention
- the [paid tier](https://about.gitlab.com/handbook/marketing/product-marketing/#tiers)
- the feature is available in:
-
- ```md
- > [Introduced][ee-1234] in [GitLab Starter](https://about.gitlab.com/pricing/) 8.3.
- ```
-
-### Product badges
-
-When a feature is available in EE-only tiers, add the corresponding tier according to the
-feature availability:
-
-- For GitLab Starter and GitLab.com Bronze: `**[STARTER]**`
-- For GitLab Premium and GitLab.com Silver: `**[PREMIUM]**`
-- For GitLab Ultimate and GitLab.com Gold: `**[ULTIMATE]**`
-- For GitLab Core and GitLab.com Free: `**[CORE]**`
-
-To exclude GitLab.com tiers (when the feature is not available in GitLab.com), add the
-keyword "only":
-
-- For GitLab Starter: `**[STARTER ONLY]**`
-- For GitLab Premium: `**[PREMIUM ONLY]**`
-- For GitLab Ultimate: `**[ULTIMATE ONLY]**`
-- For GitLab Core: `**[CORE ONLY]**`
-
-The tier should be ideally added to headers, so that the full badge will be displayed.
-But it can be also mentioned from paragraphs, list items, and table cells. For these cases,
-the tier mention will be represented by an orange question mark.
-E.g., `**[STARTER]**` renders **[STARTER]**, `**[STARTER ONLY]**` renders **[STARTER ONLY]**.
-
-The absence of tiers' mentions mean that the feature is available in GitLab Core,
-GitLab.com Free, and higher tiers.
-
-#### How it works
-
-Introduced by [!244](https://gitlab.com/gitlab-com/gitlab-docs/merge_requests/244),
-the special markup `**[STARTER]**` will generate a `span` element to trigger the
-badges and tooltips (`<span class="badge-trigger starter">`). When the keyword
-"only" is added, the corresponding GitLab.com badge will not be displayed.
-
-### GitLab Restart
-
-There are many cases that a restart/reconfigure of GitLab is required. To
-avoid duplication, link to the special document that can be found in
-[`doc/administration/restart_gitlab.md`][doc-restart]. Usually the text will
-read like:
-
- ```
- Save the file and [reconfigure GitLab](../administration/restart_gitlab.md)
- for the changes to take effect.
- ```
-
-If the document you are editing resides in a place other than the GitLab CE/EE
-`doc/` directory, instead of the relative link, use the full path:
-`http://docs.gitlab.com/ce/administration/restart_gitlab.html`.
-Replace `reconfigure` with `restart` where appropriate.
-
-### Installation guide
-
-**Ruby:**
-In [step 2 of the installation guide](../install/installation.md#2-ruby),
-we install Ruby from source. Whenever there is a new version that needs to
-be updated, remember to change it throughout the codeblock and also replace
-the sha256sum (it can be found in the [downloads page][ruby-dl] of the Ruby
-website).
-
-[ruby-dl]: https://www.ruby-lang.org/en/downloads/ "Ruby download website"
-
-### Configuration documentation for source and Omnibus installations
-
-GitLab currently officially supports two installation methods: installations
-from source and Omnibus packages installations.
-
-Whenever there is a setting that is configurable for both installation methods,
-prefer to document it in the CE docs to avoid duplication.
-
-Configuration settings include:
-
-- settings that touch configuration files in `config/`
-- NGINX settings and settings in `lib/support/` in general
-
-When there is a list of steps to perform, usually that entails editing the
-configuration file and reconfiguring/restarting GitLab. In such case, follow
-the style below as a guide:
-
-```md
-**For Omnibus installations**
-
-1. Edit `/etc/gitlab/gitlab.rb`:
-
- ```ruby
- external_url "https://gitlab.example.com"
- ```
-
-1. Save the file and [reconfigure] GitLab for the changes to take effect.
-
----
-
-**For installations from source**
-
-1. Edit `config/gitlab.yml`:
-
- ```yaml
- gitlab:
- host: "gitlab.example.com"
- ```
-
-1. Save the file and [restart] GitLab for the changes to take effect.
-
-
-[reconfigure]: path/to/administration/restart_gitlab.md#omnibus-gitlab-reconfigure
-[restart]: path/to/administration/restart_gitlab.md#installations-from-source
-```
-
-In this case:
-
-- before each step list the installation method is declared in bold
-- three dashes (`---`) are used to create a horizontal line and separate the
- two methods
-- the code blocks are indented one or more spaces under the list item to render
- correctly
-- different highlighting languages are used for each config in the code block
-- the [references](#references) guide is used for reconfigure/restart
-
-### Fake tokens
-
-There may be times where a token is needed to demonstrate an API call using
-cURL or a secret variable used in CI. It is strongly advised not to use real
-tokens in documentation even if the probability of a token being exploited is
-low.
-
-You can use the following fake tokens as examples.
-
-| **Token type** | **Token value** |
-| --------------------- | --------------------------------- |
-| Private user token | `9koXpg98eAheJpvBs5tK` |
-| Personal access token | `n671WNGecHugsdEDPsyo` |
-| Application ID | `2fcb195768c39e9a94cec2c2e32c59c0aad7a3365c10892e8116b5d83d4096b6` |
-| Application secret | `04f294d1eaca42b8692017b426d53bbc8fe75f827734f0260710b83a556082df` |
-| Secret CI variable | `Li8j-mLUVA3eZYjPfd_H` |
-| Specific Runner token | `yrnZW46BrtBFqM7xDzE7dddd` |
-| Shared Runner token | `6Vk7ZsosqQyfreAxXTZr` |
-| Trigger token | `be20d8dcc028677c931e04f3871a9b` |
-| Webhook secret token | `6XhDroRcYPM5by_h-HLY` |
-| Health check token | `Tu7BgjR9qeZTEyRzGG2P` |
-| Request profile token | `7VgpS4Ax5utVD2esNstz` |
-
-### API
-
-Here is a list of must-have items. Use them in the exact order that appears
-on this document. Further explanation is given below.
-
-- Every method must have the REST API request. For example:
-
- ```
- GET /projects/:id/repository/branches
- ```
-
-- Every method must have a detailed
- [description of the parameters](#method-description).
-- Every method must have a cURL example.
-- Every method must have a response body (in JSON format).
-
-#### Method description
-
-Use the following table headers to describe the methods. Attributes should
-always be in code blocks using backticks (``` ` ```).
-
-```
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-```
-
-Rendered example:
-
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `user` | string | yes | The GitLab username |
-
-#### cURL commands
-
-- Use `https://gitlab.example.com/api/v4/` as an endpoint.
-- 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 personal access token and don't pass data of
- username and password.
-
-| Methods | Description |
-| ------- | ----------- |
-| `-H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK"` | Use this method as is, whenever authentication needed |
-| `-X POST` | Use this method when creating new objects |
-| `-X PUT` | Use this method when updating existing objects |
-| `-X DELETE` | Use this method when removing existing objects |
-
-#### cURL Examples
-
-Below is a set of [cURL][] examples that you can use in the API documentation.
-
-##### Simple cURL command
-
-Get the details of a group:
-
-```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/gitlab-org
-```
-
-##### cURL example with parameters passed in the URL
-
-Create a new project under the authenticated user's namespace:
-
-```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects?name=foo"
-```
-
-##### Post data using cURL's --data
-
-Instead of using `-X POST` and appending the parameters to the URI, you can use
-cURL's `--data` option. The example below will create a new project `foo` under
-the authenticated user's namespace.
-
-```bash
-curl --data "name=foo" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects"
-```
-
-##### Post data using JSON content
-
-> **Note:** In this example we create a new group. Watch carefully the single
-and double quotes.
-
-```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "Content-Type: application/json" --data '{"path": "my-group", "name": "My group"}' https://gitlab.example.com/api/v4/groups
-```
-
-##### Post data using form-data
-
-Instead of using JSON or urlencode you can use multipart/form-data which
-properly handles data encoding:
-
-```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "title=ssh-key" --form "key=ssh-rsa AAAAB3NzaC1yc2EA..." https://gitlab.example.com/api/v4/users/25/keys
-```
-
-The above example is run by and administrator and will add an SSH public key
-titled ssh-key to user's account which has an id of 25.
-
-##### Escape special characters
-
-Spaces or slashes (`/`) may sometimes result to errors, thus it is recommended
-to escape them when possible. In the example below we create a new issue which
-contains spaces in its title. Observe how spaces are escaped using the `%20`
-ASCII code.
-
-```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/42/issues?title=Hello%20Dude"
-```
-
-Use `%2F` for slashes (`/`).
-
-##### Pass arrays to API calls
-
-The GitLab API sometimes accepts arrays of strings or integers. For example, to
-restrict the sign-up e-mail domains of a GitLab instance to `*.example.com` and
-`example.net`, you would do something like this:
-
-```bash
-curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "domain_whitelist[]=*.example.com" --data "domain_whitelist[]=example.net" https://gitlab.example.com/api/v4/application/settings
-```
-
-[cURL]: http://curl.haxx.se/ "cURL website"
-[single spaces]: http://www.slate.com/articles/technology/technology/2011/01/space_invaders.html
-[gfm]: http://docs.gitlab.com/ce/user/markdown.html#newlines "GitLab flavored markdown documentation"
-[ce-1242]: https://gitlab.com/gitlab-org/gitlab-ce/issues/1242
-[doc-restart]: ../administration/restart_gitlab.md "GitLab restart documentation"
diff --git a/doc/development/img/manual_build_docs.png b/doc/development/documentation/img/manual_build_docs.png
index 615facabb5f..615facabb5f 100644
--- a/doc/development/img/manual_build_docs.png
+++ b/doc/development/documentation/img/manual_build_docs.png
Binary files differ
diff --git a/doc/development/documentation/index.md b/doc/development/documentation/index.md
new file mode 100644
index 00000000000..f5cdd310f6f
--- /dev/null
+++ b/doc/development/documentation/index.md
@@ -0,0 +1,557 @@
+---
+description: Learn how to contribute to GitLab Documentation.
+---
+
+# GitLab Documentation guidelines
+
+ - **General Documentation**: written by the [developers responsible by creating features](#contributing-to-docs). Should be submitted in the same merge request containing code. Feature proposals (by GitLab contributors) should also be accompanied by its respective documentation. They can be later improved by PMs and Technical Writers.
+ - **[Technical Articles](#technical-articles)**: written by any [GitLab Team](https://about.gitlab.com/team/) member, GitLab contributors, or [Community Writers](https://about.gitlab.com/handbook/product/technical-writing/community-writers/).
+ - **Indexes per topic**: initially prepared by the Technical Writing Team, and kept up-to-date by developers and PMs in the same merge request containing code. They gather all resources for that topic in a single page (user and admin documentation, articles, and third-party docs).
+
+## Contributing to docs
+
+Whenever a feature is changed, updated, introduced, or deprecated, the merge
+request introducing these changes must be accompanied by the documentation
+(either updating existing ones or creating new ones). This is also valid when
+changes are introduced to the UI.
+
+The one responsible for writing the first piece of documentation is the developer who
+wrote the code. It's the job of the Product Manager to ensure all features are
+shipped with its docs, whether is a small or big change. At the pace GitLab evolves,
+this is the only way to keep the docs up-to-date. If you have any questions about it,
+ask a Technical Writer. Otherwise, when your content is ready, assign one of
+them to review it for you.
+
+We use the [monthly release blog post](https://about.gitlab.com/handbook/marketing/blog/release-posts/#monthly-releases) as a changelog checklist to ensure everything
+is documented.
+
+Whenever you submit a merge request for the documentation, use the documentation MR description template.
+
+Please check the [documentation workflow](https://about.gitlab.com/handbook/product/technical-writing/workflow/) before getting started.
+
+## Documentation structure
+
+- Overview and use cases: what it is, why it is necessary, why one would use it
+- Requirements: what do we need to get started
+- Tutorial: how to set it up, how to use it
+
+Always link a new document from its topic-related index, otherwise, it will
+not be included it in the documentation site search.
+
+_Note: to be extended._
+
+### Feature overview and use cases
+
+Every major feature (regardless if present in GitLab Community or Enterprise editions)
+should present, at the beginning of the document, two main sections: **overview** and
+**use cases**. Every GitLab EE-only feature should also contain these sections.
+
+**Overview**: as the name suggests, the goal here is to provide an overview of the feature.
+Describe what is it, what it does, why it is important/cool/nice-to-have,
+what problem it solves, and what you can do with this feature that you couldn't
+do before.
+
+**Use cases**: provide at least two, ideally three, use cases for every major feature.
+You should answer this question: what can you do with this feature/change? Use cases
+are examples of how this feature or change can be used in real life.
+
+Examples:
+- CE and EE: [Issues](../user/project/issues/index.md#use-cases)
+- CE and EE: [Merge Requests](../user/project/merge_requests/index.md#overview)
+- EE-only: [Geo](https://docs.gitlab.com/ee/gitlab-geo/README.html#overview)
+- EE-only: [Jenkins integration](https://docs.gitlab.com/ee/integration/jenkins.md#overview)
+
+Note that if you don't have anything to add between the doc title (`<h1>`) and
+the header `## Overview`, you can omit the header, but keep the content of the
+overview there.
+
+> **Overview** and **use cases** are required to **every** Enterprise Edition feature,
+and for every **major** feature present in Community Edition.
+
+## Markdown and styles
+
+Currently GitLab docs use Redcarpet as [markdown](../user/markdown.md) engine, but there's an [open discussion](https://gitlab.com/gitlab-com/gitlab-docs/issues/50) for implementing Kramdown in the near future.
+
+All the docs follow the [documentation style guidelines](styleguide.md).
+
+## Documentation directory structure
+
+The documentation is structured based on the GitLab UI structure itself,
+separated by [`user`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/user),
+[`administrator`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/administration), and [`contributor`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/development).
+
+In order to have a [solid site structure](https://searchengineland.com/seo-benefits-developing-solid-site-structure-277456) for our documentation,
+all docs should be linked. Every new document should be cross-linked to its related documentation, and linked from its topic-related index, when existent.
+
+The directories `/workflow/`, `/gitlab-basics/`, `/university/`, and `/articles/` have
+been deprecated and the majority their docs have been moved to their correct location
+in small iterations. Please don't create new docs in these folders.
+
+### Location and naming documents
+
+The documentation hierarchy can be vastly improved by providing a better layout
+and organization of directories.
+
+Having a structured document layout, we will be able to have meaningful URLs
+like `docs.gitlab.com/user/project/merge_requests/index.html`. With this pattern,
+you can immediately tell that you are navigating a user related documentation
+and is about the project and its merge requests.
+
+Do not create summaries of similar types of content (e.g. an index of all articles, videos, etc.),
+rather organize content by its subject (e.g. everything related to CI goes together)
+and cross-link between any related content.
+
+The table below shows what kind of documentation goes where.
+
+| Directory | What belongs here |
+| --------- | -------------- |
+| `doc/user/` | User related documentation. Anything that can be done within the GitLab UI goes here including `/admin`. |
+| `doc/administration/` | Documentation that requires the user to have access to the server where GitLab is installed. The admin settings that can be accessed via GitLab's interface go under `doc/user/admin_area/`. |
+| `doc/api/` | API related documentation. |
+| `doc/development/` | Documentation related to the development of GitLab. Any styleguides should go here. |
+| `doc/legal/` | Legal documents about contributing to GitLab. |
+| `doc/install/`| Probably the most visited directory, since `installation.md` is there. Ideally this should go under `doc/administration/`, but it's best to leave it as-is in order to avoid confusion (still debated though). |
+| `doc/update/` | Same with `doc/install/`. Should be under `administration/`, but this is a well known location, better leave as-is, at least for now. |
+| `doc/topics/` | Indexes per Topic (`doc/topics/topic-name/index.md`): all resources for that topic (user and admin documentation, articles, and third-party docs) |
+
+---
+
+**General rules:**
+
+1. The correct naming and location of a new document, is a combination
+ of the relative URL of the document in question and the GitLab Map design
+ that is used for UX purposes ([source][graffle], [image][gitlab-map]).
+1. When creating a new document and it has more than one word in its name,
+ make sure to use underscores instead of spaces or dashes (`-`). For example,
+ a proper naming would be `import_projects_from_github.md`. The same rule
+ applies to images.
+1. Start a new directory with an `index.md` file.
+1. There are four main directories, `user`, `administration`, `api` and `development`.
+1. The `doc/user/` directory has five main subdirectories: `project/`, `group/`,
+ `profile/`, `dashboard/` and `admin_area/`.
+ 1. `doc/user/project/` should contain all project related documentation.
+ 1. `doc/user/group/` should contain all group related documentation.
+ 1. `doc/user/profile/` should contain all profile related documentation.
+ Every page you would navigate under `/profile` should have its own document,
+ i.e. `account.md`, `applications.md`, `emails.md`, etc.
+ 1. `doc/user/dashboard/` should contain all dashboard related documentation.
+ 1. `doc/user/admin_area/` should contain all admin related documentation
+ describing what can be achieved by accessing GitLab's admin interface
+ (_not to be confused with `doc/administration` where server access is
+ required_).
+ 1. Every category under `/admin/application_settings` should have its
+ own document located at `doc/user/admin_area/settings/`. For example,
+ the **Visibility and Access Controls** category should have a document
+ located at `doc/user/admin_area/settings/visibility_and_access_controls.md`.
+1. The `doc/topics/` directory holds topic-related technical content. Create
+ `doc/topics/topic-name/subtopic-name/index.md` when subtopics become necessary.
+ General user- and admin- related documentation, should be placed accordingly.
+
+If you are unsure where a document should live, you can ping `@axil` or `@marcia` in your
+merge request.
+
+### Changing document location
+
+Changing a document's location is not to be taken lightly. Remember that the
+documentation is available to all installations under `help/` and not only to
+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 replace all of its contents with a new line:
+
+```
+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/`.
+
+---
+
+For example, if you were to move `doc/workflow/lfs/lfs_administration.md` to
+`doc/administration/lfs.md`, then the steps would be:
+
+1. Copy `doc/workflow/lfs/lfs_administration.md` to `doc/administration/lfs.md`
+1. Replace the contents of `doc/workflow/lfs/lfs_administration.md` with:
+
+ ```
+ This document was moved to [another location](../../administration/lfs.md).
+ ```
+
+1. Find and replace any occurrences of the old location with the new one.
+ A quick way to find them is to use `git grep`. First go to the root directory
+ where you cloned the `gitlab-ce` repository and then do:
+
+ ```
+ git grep -n "workflow/lfs/lfs_administration"
+ git grep -n "lfs/lfs_administration"
+ ```
+
+NOTE: **Note:**
+If the document being moved has any Disqus comments on it, there are extra steps
+to follow documented just [below](#redirections-for-pages-with-disqus-comments).
+
+Things to note:
+
+- Since we also use inline documentation, except for the documentation itself,
+ the document might also be referenced in the views of GitLab (`app/`) which will
+ render when visiting `/help`, and sometimes in the testing suite (`spec/`).
+- The above `git grep` command will search recursively in the directory you run
+ it in for `workflow/lfs/lfs_administration` and `lfs/lfs_administration`
+ and will print the file and the line where this file is mentioned.
+ You may ask why the two greps. Since we use relative paths to link to
+ documentation, sometimes it might be useful to search a path deeper.
+- The `*.md` extension is not used when a document is linked to GitLab's
+ built-in help page, that's why we omit it in `git grep`.
+- Use the checklist on the documentation MR description template.
+
+#### Alternative redirection method
+
+Alternatively to the method described above, you can simply replace the content
+of the old file with a frontmatter containing a redirect link:
+
+```yaml
+---
+redirect_to: '../path/to/file/README.md'
+---
+```
+
+It supports both full and relative URLs, e.g. `https://docs.gitlab.com/ee/path/to/file.html`, `../path/to/file.html`, `path/to/file.md`. Note that any `*.md` paths will be compiled to `*.html`.
+
+### Redirections for pages with Disqus comments
+
+If the documentation page being relocated already has any Disqus comments,
+we need to preserve the Disqus thread.
+
+Disqus uses an identifier per page, and for docs.gitlab.com, the page identifier
+is configured to be the page URL. Therefore, when we change the document location,
+we need to preserve the old URL as the same Disqus identifier.
+
+To do that, add to the frontmatter the variable `redirect_from`,
+using the old URL as value. For example, let's say I moved the document
+available under `https://docs.gitlab.com/my-old-location/README.html` to a new location,
+`https://docs.gitlab.com/my-new-location/index.html`.
+
+Into the **new document** frontmatter add the following:
+
+```yaml
+---
+redirect_from: 'https://docs.gitlab.com/my-old-location/README.html'
+---
+```
+
+Note: it is necessary to include the file name in the `redirect_from` URL,
+even if it's `index.html` or `README.html`.
+
+## Testing
+
+We treat documentation as code, thus have implemented some testing.
+Currently, the following tests are in place:
+
+1. `docs lint`: Check that all internal (relative) links work correctly and
+ that all cURL examples in API docs use the full switches. It's recommended
+ to [check locally](#previewing-locally) before pushing to GitLab by executing the command
+ `bundle exec nanoc check internal_links` on your local
+ [`gitlab-docs`](https://gitlab.com/gitlab-com/gitlab-docs) directory.
+1. [`ee_compat_check`](../automatic_ce_ee_merge.md#avoiding-ce-gt-ee-merge-conflicts-beforehand) (runs on CE only):
+ When you submit a merge request to GitLab Community Edition (CE),
+ there is this additional job that runs against Enterprise Edition (EE)
+ and checks if your changes can apply cleanly to the EE codebase.
+ If that job fails, read the instructions in the job log for what to do next.
+ As CE is merged into EE once a day, it's important to avoid merge conflicts.
+ Submitting an EE-equivalent merge request cherry-picking all commits from CE to EE is
+ essential to avoid them.
+
+## Branch naming
+
+If your contribution contains **only** documentation changes, you can speed up
+the CI process by following some branch naming conventions. You have three
+choices:
+
+| Branch name | Valid example |
+| ----------- | ------------- |
+| Starting with `docs/` | `docs/update-api-issues` |
+| Starting with `docs-` | `docs-update-api-issues` |
+| Ending in `-docs` | `123-update-api-issues-docs` |
+
+If your branch name matches any of the above, it will run only the docs
+tests. If it doesn't, the whole test suite will run (including docs).
+
+## Merge requests for GitLab documentation
+
+Before getting started, make sure you read the introductory section
+"[contributing to docs](#contributing-to-docs)" above and the
+[tech writing workflow](https://about.gitlab.com/handbook/product/technical-writing/workflow/)
+for GitLab Team members.
+
+- Use the current [merge request description template](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab/merge_request_templates/Documentation.md)
+- Use the correct [branch name](#branch-naming)
+- Label the MR `Documentation`
+- Assign the correct milestone (see note below)
+
+
+NOTE: **Note:**
+If the release version you want to add the documentation to has already been
+frozen or released, use the label `Pick into X.Y` to get it merged into
+the correct release. Avoid picking into a past release as much as you can, as
+it increases the work of the release managers.
+
+### Cherry-picking from CE to EE
+
+As we have the `master` branch of CE merged into EE once a day, it's common to
+run into merge conflicts. To avoid them, we [test for merge conflicts against EE](#testing)
+with the `ee-compat-check` job, and use the following method of creating equivalent
+branches for CE and EE.
+
+Follow this [method for cherry-picking from CE to EE](../automatic_ce_ee_merge.md#cherry-picking-from-ce-to-ee), with a few adjustments:
+
+- Create the [CE branch](#branch-naming) starting with `docs-`,
+ e.g.: `git checkout -b docs-example`
+- Create the EE-equivalent branch ending with `-ee`, e.g.,
+ `git checkout -b docs-example-ee`
+- Once all the jobs are passing in CE and EE, and you've addressed the
+feedback from your own team, assign the CE MR to a technical writer for review
+- When both MRs are ready, the EE merge request will be merged first, and the
+CE-equivalent will be merged next.
+- Note that the review will occur only in the CE MR, as the EE MR
+contains the same commits as the CE MR.
+- If you have a few more changes that apply to the EE-version only, you can submit
+a couple more commits to the EE branch, but ask the reviewer to review the EE merge request
+additionally to the CE MR. If there are many EE-only changes though, start a new MR
+to EE only.
+
+## Previewing the changes live
+
+NOTE: **Note:**
+To preview your changes to documentation locally, follow this
+[development guide](https://gitlab.com/gitlab-com/gitlab-docs/blob/master/README.md#development).
+
+The live preview is currently enabled for the following projects:
+
+- https://gitlab.com/gitlab-org/gitlab-ce
+- https://gitlab.com/gitlab-org/gitlab-ee
+- https://gitlab.com/gitlab-org/gitlab-runner
+
+If your branch contains only documentation changes, you can use
+[special branch names](#branch-naming) to avoid long running pipelines.
+
+For [docs-only changes](#branch-naming), the review app is run automatically.
+For all other branches, you can use the manual `review-docs-deploy-manual` job
+in your merge request. You will need at least Maintainer permissions to be able
+to run it. In the mini pipeline graph, you should see an `>>` icon. Clicking on it will
+reveal the `review-docs-deploy-manual` job. Hit the play button for the job to start.
+
+![Manual trigger a docs build](img/manual_build_docs.png)
+
+NOTE: **Note:**
+You will need to push a branch to those repositories, it doesn't work for forks.
+
+The `review-docs-deploy*` job will:
+
+1. Create a new branch in the [gitlab-docs](https://gitlab.com/gitlab-com/gitlab-docs)
+ project named after the scheme: `$DOCS_GITLAB_REPO_SUFFIX-$CI_ENVIRONMENT_SLUG`,
+ where `DOCS_GITLAB_REPO_SUFFIX` is the suffix for each product, e.g, `ce` for
+ CE, etc.
+1. Trigger a cross project pipeline and build the docs site with your changes
+
+After a few minutes, the Review App will be deployed and you will be able to
+preview the changes. The docs URL can be found in two places:
+
+- In the merge request widget
+- In the output of the `review-docs-deploy*` job, which also includes the
+ triggered pipeline so that you can investigate whether something went wrong
+
+In case the Review App URL returns 404, follow these steps to debug:
+
+1. **Did you follow the URL from the merge request widget?** If yes, then check if
+ the link is the same as the one in the job output.
+1. **Did you follow the URL from the job output?** If yes, then it means that
+ either the site is not yet deployed or something went wrong with the remote
+ pipeline. Give it a few minutes and it should appear online, otherwise you
+ can check the status of the remote pipeline from the link in the job output.
+ If the pipeline failed or got stuck, drop a line in the `#docs` chat channel.
+
+TIP: **Tip:**
+Someone that has no merge rights to the CE/EE projects (think of forks from
+contributors) will not be able to run the manual job. In that case, you can
+ask someone from the GitLab team who has the permissions to do that for you.
+
+NOTE: **Note:**
+Make sure that you always delete the branch of the merge request you were
+working on. If you don't, the remote docs branch won't be removed either,
+and the server where the Review Apps are hosted will eventually be out of
+disk space.
+
+### Technical aspects
+
+If you want to know the hot details, here's what's really happening:
+
+1. You manually run the `review-docs-deploy` job in a CE/EE merge request.
+1. The job runs the [`scripts/trigger-build-docs`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/scripts/trigger-build-docs)
+ script with the `deploy` flag, which in turn:
+ 1. Takes your branch name and applies the following:
+ - The slug of the branch name is used to avoid special characters since
+ ultimately this will be used by NGINX.
+ - The `preview-` prefix is added to avoid conflicts if there's a remote branch
+ with the same name that you created in the merge request.
+ - The final branch name is truncated to 42 characters to avoid filesystem
+ limitations with long branch names (> 63 chars).
+ 1. The remote branch is then created if it doesn't exist (meaning you can
+ re-run the manual job as many times as you want and this step will be skipped).
+ 1. A new cross-project pipeline is triggered in the docs project.
+ 1. The preview URL is shown both at the job output and in the merge request
+ widget. You also get the link to the remote pipeline.
+1. In the docs project, the pipeline is created and it
+ [skips the test jobs](https://gitlab.com/gitlab-com/gitlab-docs/blob/8d5d5c750c602a835614b02f9db42ead1c4b2f5e/.gitlab-ci.yml#L50-55)
+ to lower the build time.
+1. Once the docs site is built, the HTML files are uploaded as artifacts.
+1. A specific Runner tied only to the docs project, runs the Review App job
+ that downloads the artifacts and uses `rsync` to transfer the files over
+ to a location where NGINX serves them.
+
+The following GitLab features are used among others:
+
+- [Manual actions](../../ci/yaml/README.md#manual-actions)
+- [Multi project pipelines](https://docs.gitlab.com/ee/ci/multi_project_pipeline_graphs.html)
+- [Review Apps](../../ci/review_apps/index.md)
+- [Artifacts](../../ci/yaml/README.md#artifacts)
+- [Specific Runner](../../ci/runners/README.md#locking-a-specific-runner-from-being-enabled-for-other-projects)
+
+## GitLab `/help`
+
+Every GitLab instance includes the documentation, which is available from `/help`
+(`http://my-instance.com/help`), e.g., <https://gitlab.com/help>.
+
+When you're building a new feature, you may need to link the documentation
+from GitLab, the application. This is normally done in files inside the
+`app/views/` directory with the help of the `help_page_path` helper method.
+
+In its simplest form, the HAML code to generate a link to the `/help` page is:
+
+```haml
+= link_to 'Help page', help_page_path('user/permissions')
+```
+
+The `help_page_path` contains the path to the document you want to link to with
+the following conventions:
+
+- it is relative to the `doc/` directory in the GitLab repository
+- the `.md` extension must be omitted
+- it must not end with a slash (`/`)
+
+Below are some special cases where should be used depending on the context.
+You can combine one or more of the following:
+
+1. **Linking to an anchor link.** Use `anchor` as part of the `help_page_path`
+ method:
+
+ ```haml
+ = link_to 'Help page', help_page_path('user/permissions', anchor: 'anchor-link')
+ ```
+
+1. **Opening links in a new tab.** This should be the default behavior:
+
+ ```haml
+ = link_to 'Help page', help_page_path('user/permissions'), target: '_blank'
+ ```
+
+1. **Linking to a circle icon.** Usually used in settings where a long
+ description cannot be used, like near checkboxes. You can basically use
+ any font awesome icon, but prefer the `question-circle`:
+
+ ```haml
+ = link_to icon('question-circle'), help_page_path('user/permissions')
+ ```
+
+1. **Using a button link.** Useful in places where text would be out of context
+ with the rest of the page layout:
+
+ ```haml
+ = link_to 'Help page', help_page_path('user/permissions'), class: 'btn btn-info'
+ ```
+
+1. **Using links inline of some text.**
+
+ ```haml
+ Description to #{link_to 'Help page', help_page_path('user/permissions')}.
+ ```
+
+1. **Adding a period at the end of the sentence.** Useful when you don't want
+ the period to be part of the link:
+
+ ```haml
+ = succeed '.' do
+ Learn more in the
+ = link_to 'Help page', help_page_path('user/permissions')
+ ```
+
+## General Documentation vs Technical Articles
+
+### General documentation
+
+General documentation is categorized by _User_, _Admin_, and _Contributor_, and describe what that feature is, what it does, and its available settings.
+
+### Technical Articles
+
+Technical articles replace technical content that once lived in the [GitLab Blog](https://about.gitlab.com/blog/), where they got out-of-date and weren't easily found.
+
+They are topic-related documentation, written with an user-friendly approach and language, aiming to provide the community with guidance on specific processes to achieve certain objectives.
+
+A technical article guides users and/or admins to achieve certain objectives (within guides and tutorials), or provide an overview of that particular topic or feature (within technical overviews). It can also describe the use, implementation, or integration of third-party tools with GitLab.
+
+They should be placed in a new directory named `/article-title/index.md` under a topic-related folder, and their images should be placed in `/article-title/img/`. For example, a new article on GitLab Pages should be placed in `doc/user/project/pages/article-title/` and a new article on GitLab CI/CD should be placed in `doc/ci/examples/article-title/`.
+
+#### Types of Technical Articles
+
+- **User guides**: technical content to guide regular users from point A to point B
+- **Admin guides**: technical content to guide administrators of GitLab instances from point A to point B
+- **Technical Overviews**: technical content describing features, solutions, and third-party integrations
+- **Tutorials**: technical content provided step-by-step on how to do things, or how to reach very specific objectives
+
+#### Understanding guides, tutorials, and technical overviews
+
+Suppose there's a process to go from point A to point B in 5 steps: `(A) 1 > 2 > 3 > 4 > 5 (B)`.
+
+A **guide** can be understood as a description of certain processes to achieve a particular objective. A guide brings you from A to B describing the characteristics of that process, but not necessarily going over each step. It can mention, for example, steps 2 and 3, but does not necessarily explain how to accomplish them.
+
+- Live example: "[Static sites and GitLab Pages domains (Part 1)](../user/project/pages/getting_started_part_one.md) to [Creating and Tweaking GitLab CI/CD for GitLab Pages (Part 4)](../../user/project/pages/getting_started_part_four.md)"
+
+A **tutorial** requires a clear **step-by-step** guidance to achieve a singular objective. It brings you from A to B, describing precisely all the necessary steps involved in that process, showing each of the 5 steps to go from A to B.
+It does not only describes steps 2 and 3, but also shows you how to accomplish them.
+
+- Live example (on the blog): [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/)
+
+A **technical overview** is a description of what a certain feature is, and what it does, but does not walk
+through the process of how to use it systematically.
+
+- Live example (on the blog): [GitLab Workflow, an overview](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/)
+
+#### Special format
+
+Every **Technical Article** contains a frontmatter at the beginning of the doc
+with the following information:
+
+- **Type of article** (user guide, admin guide, technical overview, tutorial)
+- **Knowledge level** expected from the reader to be able to follow through (beginner, intermediate, advanced)
+- **Author's name** and **GitLab.com handle**
+- **Publication date** (ISO format YYYY-MM-DD)
+
+For example:
+
+
+```yaml
+---
+author: John Doe
+author_gitlab: johnDoe
+level: beginner
+article_type: user guide
+date: 2017-02-01
+---
+```
+
+#### Technical Articles - Writing Method
+
+Use the [writing method](https://about.gitlab.com/handbook/product/technical-writing/#writing-method) defined by the Technical Writing team.
+
+[gitlab-map]: https://gitlab.com/gitlab-org/gitlab-design/raw/master/production/resources/gitlab-map.png
+[graffle]: https://gitlab.com/gitlab-org/gitlab-design/blob/d8d39f4a87b90fb9ae89ca12dc565347b4900d5e/production/resources/gitlab-map.graffle
diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md
new file mode 100644
index 00000000000..a315cfdc116
--- /dev/null
+++ b/doc/development/documentation/styleguide.md
@@ -0,0 +1,513 @@
+---
+description: 'Writing styles, markup, formatting, and reusing regular expressions throughout the GitLab Documentation.'
+---
+
+# Documentation style guidelines
+
+The documentation style guide defines the markup structure used in
+GitLab documentation. Check the
+[documentation guidelines](index.md) for general development instructions.
+
+Check the GitLab handbook for the [writing styles guidelines](https://about.gitlab.com/handbook/communication/#writing-style-guidelines).
+
+## Text
+
+- Split up long lines (wrap text), this makes it much easier to review and edit. Only
+ double line breaks are shown as a full line break in [GitLab markdown][gfm].
+ 80-100 characters is a good line length
+- Make sure that the documentation is added in the correct
+ [directory](index.md#documentation-directory-structure) and that
+ there's a link to it somewhere useful
+- Do not duplicate information
+- Be brief and clear
+- Unless there's a logical reason not to, add documents in alphabetical order
+- Write in US English
+- Use [single spaces][] instead of double spaces
+- Jump a line between different markups (e.g., after every paragraph, header, list, etc)
+- Capitalize "G" and "L" in GitLab
+- Use sentence case for titles, headings, labels, menu items, and buttons.
+- Use title case when referring to [features](https://about.gitlab.com/features/) or
+[products](https://about.gitlab.com/pricing/) (e.g., GitLab Runner, Geo,
+Issue Boards, GitLab Core, Git, Prometheus, Kubernetes, etc), and methods or methodologies
+(e.g., Continuous Integration, Continuous Deployment, Scrum, Agile, etc). Note that
+some features are also objects (e.g. "Merge Requests" and "merge requests").
+
+## Formatting
+
+- Use double asterisks (`**`) to mark a word or text in bold (`**bold**`)
+- Use undescore (`_`) for text in italics (`_italic_`)
+- Jump a line between different markups, for example:
+
+ ```md
+ ## Header
+
+ Paragraph.
+
+ - List item
+ - List item
+ ```
+
+### Punctuation
+
+For punctuation rules, please refer to the [GitLab UX guide](https://design.gitlab.com/content/punctuation/).
+
+### Ordered and unordered lists
+
+- Use dashes (`-`) for unordered lists instead of asterisks (`*`)
+- Use the number one (`1`) for ordered lists
+- For punctuation in bullet lists, please refer to the [GitLab UX guide](https://design.gitlab.com/content/punctuation/)
+
+## Headings
+
+- Add **only one H1** in each document, by adding `#` at the beginning of
+ it (when using markdown). The `h1` will be the document `<title>`.
+- For subheadings, use `##`, `###` and so on
+- Avoid putting numbers in headings. Numbers shift, hence documentation anchor
+ links shift too, which eventually leads to dead links. If you think it is
+ compelling to add numbers in headings, make sure to at least discuss it with
+ someone in the Merge Request
+- [Avoid using symbols and special chars](https://gitlab.com/gitlab-com/gitlab-docs/issues/84)
+ in headers. Whenever possible, they should be plain and short text.
+- Avoid adding things that show ephemeral statuses. For example, if a feature is
+ considered beta or experimental, put this info in a note, not in the heading.
+- When introducing a new document, be careful for the headings to be
+ grammatically and syntactically correct. Mention one or all
+ of the following GitLab members for a review: `@axil` or `@marcia`.
+ This is to ensure that no document with wrong heading is going
+ live without an audit, thus preventing dead links and redirection issues when
+ corrected
+- Leave exactly one new line after a heading
+
+## Links
+
+- Use the regular inline link markdown markup `[Text](https://example.com)`.
+ It's easier to read, review, and maintain.
+- If there's a link that repeats several times through the same document,
+ you can use `[Text][identifier]` and at the bottom of the section or the
+ document add: `[identifier]: https://example.com`, in which case, we do
+ encourage you to also add an alternative text: `[identifier]: https://example.com "Alternative text"` that appears when hovering your mouse on a link.
+- To link to internal documentation, use relative links, not full URLs. Use `../` to
+ navigate tp high-level directories, and always add the file name `file.md` at the
+ end of the link with the `.md` extension, not `.html`.
+ Example: instead of `[text](../../merge_requests/)`, use
+ `[text](../../merge_requests/index.md)` or, `[text](../../ci/README.md)`, or,
+ for anchor links, `[text](../../ci/README.md#examples)`.
+ Using the markdown extension is necessary for the [`/help`](index.md#gitlab-help)
+ section of GitLab.
+- To link from CE to EE-only documentation, use the EE-only doc full URL.
+- Use [meaningful anchor texts](https://www.futurehosting.com/blog/links-should-have-meaningful-anchor-text-heres-why/).
+ E.g., instead of writing something like `Read more about GitLab Issue Boards [here](LINK)`,
+ write `Read more about [GitLab Issue Boards](LINK)`.
+
+## Navigation
+
+To indicate the steps of navigation through the UI:
+
+- Use the exact word as shown in the UI, including any capital letters as-is
+- Use bold text for navigation items and the char `>` as separator
+(e.g., `Navigate to your project's **Settings > CI/CD**` )
+- If there are any expandable menus, make sure to mention that the user
+needs to expand the tab to find the settings you're referring to
+
+## Images
+
+- Place images in a separate directory named `img/` in the same directory where
+ the `.md` document that you're working on is located. Always prepend their
+ names with the name of the document that they will be included in. For
+ example, if there is a document called `twitter.md`, then a valid image name
+ could be `twitter_login_screen.png`. [**Exception**: images for
+ [articles](index.md#technical-articles) should be
+ put in a directory called `img` underneath `/articles/article_title/img/`, therefore,
+ there's no need to prepend the document name to their filenames.]
+- Images should have a specific, non-generic name that will differentiate them.
+- Keep all file names in lower case.
+- Consider using PNG images instead of JPEG.
+- Compress all images with <https://tinypng.com/> or similar tool.
+- Compress gifs with <https://ezgif.com/optimize> or similar tool.
+- Images should be used (only when necessary) to _illustrate_ the description
+of a process, not to _replace_ it.
+
+Inside the document:
+
+- The Markdown way of using an image inside a document is:
+ `![Proper description what the image is about](img/document_image_title.png)`
+- Always use a proper description for what the image is about. That way, when a
+ browser fails to show the image, this text will be used as an alternative
+ description
+- If there are consecutive images with little text between them, always add
+ three dashes (`---`) between the image and the text to create a horizontal
+ line for better clarity
+- If a heading is placed right after an image, always add three dashes (`---`)
+ between the image and the heading
+
+## Alert boxes
+
+Whenever you want to call the attention to a particular sentence,
+use the following markup for highlighting.
+
+_Note that the alert boxes only work for one paragraph only. Multiple paragraphs,
+lists, headers, etc will not render correctly._
+
+### Note
+
+```md
+NOTE: **Note:**
+This is something to note.
+```
+
+How it renders in docs.gitlab.com:
+
+NOTE: **Note:**
+This is something to note.
+
+### Tip
+
+```md
+TIP: **Tip:**
+This is a tip.
+```
+
+How it renders in docs.gitlab.com:
+
+TIP: **Tip:**
+This is a tip.
+
+### Caution
+
+```md
+CAUTION: **Caution:**
+This is something to be cautious about.
+```
+
+How it renders in docs.gitlab.com:
+
+CAUTION: **Caution:**
+This is something to be cautious about.
+
+### Danger
+
+```md
+DANGER: **Danger:**
+This is a breaking change, a bug, or something very important to note.
+```
+
+How it renders in docs.gitlab.com:
+
+DANGER: **Danger:**
+This is a breaking change, a bug, or something very important to note.
+
+## Blockquotes
+
+For highlighting a text within a blue blockquote, use this format:
+
+```md
+> This is a blockquote.
+```
+
+which renders in docs.gitlab.com to:
+
+> This is a blockquote.
+
+If the text spans across multiple lines it's OK to split the line.
+
+## Specific sections and terms
+
+To mention and/or reference specific terms in GitLab, please follow the styles
+below.
+
+### GitLab versions and tiers
+
+- Every piece of documentation that comes with a new feature should declare the
+ GitLab version that feature got introduced. Right below the heading add a
+ note:
+
+ ```md
+ > Introduced in GitLab 8.3.
+ ```
+
+- Whenever possible, every feature should have a link to the MR, issue, or epic that introduced it.
+ The above note would be then transformed to:
+
+ ```md
+ > [Introduced][ce-1242] in GitLab 8.3.
+ ```
+
+ , where the [link identifier](#links) is named after the repository (CE) and
+ the MR number.
+
+- If the feature is only available in GitLab Enterprise Edition, don't forget to mention
+ the [paid tier](https://about.gitlab.com/handbook/marketing/product-marketing/#tiers)
+ the feature is available in:
+
+ ```md
+ > [Introduced][ee-1234] in [GitLab Starter](https://about.gitlab.com/pricing/) 8.3.
+ ```
+
+### Product badges
+
+When a feature is available in EE-only tiers, add the corresponding tier according to the
+feature availability:
+
+- For GitLab Starter and GitLab.com Bronze: `**[STARTER]**`
+- For GitLab Premium and GitLab.com Silver: `**[PREMIUM]**`
+- For GitLab Ultimate and GitLab.com Gold: `**[ULTIMATE]**`
+- For GitLab Core and GitLab.com Free: `**[CORE]**`
+
+To exclude GitLab.com tiers (when the feature is not available in GitLab.com), add the
+keyword "only":
+
+- For GitLab Starter: `**[STARTER ONLY]**`
+- For GitLab Premium: `**[PREMIUM ONLY]**`
+- For GitLab Ultimate: `**[ULTIMATE ONLY]**`
+- For GitLab Core: `**[CORE ONLY]**`
+
+The tier should be ideally added to headers, so that the full badge will be displayed.
+But it can be also mentioned from paragraphs, list items, and table cells. For these cases,
+the tier mention will be represented by an orange question mark.
+E.g., `**[STARTER]**` renders **[STARTER]**, `**[STARTER ONLY]**` renders **[STARTER ONLY]**.
+
+The absence of tiers' mentions mean that the feature is available in GitLab Core,
+GitLab.com Free, and higher tiers.
+
+#### How it works
+
+Introduced by [!244](https://gitlab.com/gitlab-com/gitlab-docs/merge_requests/244),
+the special markup `**[STARTER]**` will generate a `span` element to trigger the
+badges and tooltips (`<span class="badge-trigger starter">`). When the keyword
+"only" is added, the corresponding GitLab.com badge will not be displayed.
+
+### GitLab Restart
+
+There are many cases that a restart/reconfigure of GitLab is required. To
+avoid duplication, link to the special document that can be found in
+[`doc/administration/restart_gitlab.md`][doc-restart]. Usually the text will
+read like:
+
+ ```
+ Save the file and [reconfigure GitLab](../../administration/restart_gitlab.md)
+ for the changes to take effect.
+ ```
+
+If the document you are editing resides in a place other than the GitLab CE/EE
+`doc/` directory, instead of the relative link, use the full path:
+`http://docs.gitlab.com/ce/administration/restart_gitlab.html`.
+Replace `reconfigure` with `restart` where appropriate.
+
+### Installation guide
+
+**Ruby:**
+In [step 2 of the installation guide](../../install/installation.md#2-ruby),
+we install Ruby from source. Whenever there is a new version that needs to
+be updated, remember to change it throughout the codeblock and also replace
+the sha256sum (it can be found in the [downloads page][ruby-dl] of the Ruby
+website).
+
+[ruby-dl]: https://www.ruby-lang.org/en/downloads/ "Ruby download website"
+
+### Configuration documentation for source and Omnibus installations
+
+GitLab currently officially supports two installation methods: installations
+from source and Omnibus packages installations.
+
+Whenever there is a setting that is configurable for both installation methods,
+prefer to document it in the CE docs to avoid duplication.
+
+Configuration settings include:
+
+- settings that touch configuration files in `config/`
+- NGINX settings and settings in `lib/support/` in general
+
+When there is a list of steps to perform, usually that entails editing the
+configuration file and reconfiguring/restarting GitLab. In such case, follow
+the style below as a guide:
+
+```md
+**For Omnibus installations**
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ external_url "https://gitlab.example.com"
+ ```
+
+1. Save the file and [reconfigure] GitLab for the changes to take effect.
+
+---
+
+**For installations from source**
+
+1. Edit `config/gitlab.yml`:
+
+ ```yaml
+ gitlab:
+ host: "gitlab.example.com"
+ ```
+
+1. Save the file and [restart] GitLab for the changes to take effect.
+
+
+[reconfigure]: path/to/administration/restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart]: path/to/administration/restart_gitlab.md#installations-from-source
+```
+
+In this case:
+
+- before each step list the installation method is declared in bold
+- three dashes (`---`) are used to create a horizontal line and separate the
+ two methods
+- the code blocks are indented one or more spaces under the list item to render
+ correctly
+- different highlighting languages are used for each config in the code block
+- the [references](#references) guide is used for reconfigure/restart
+
+### Fake tokens
+
+There may be times where a token is needed to demonstrate an API call using
+cURL or a variable used in CI. It is strongly advised not to use real
+tokens in documentation even if the probability of a token being exploited is
+low.
+
+You can use the following fake tokens as examples.
+
+| **Token type** | **Token value** |
+| --------------------- | --------------------------------- |
+| Private user token | `9koXpg98eAheJpvBs5tK` |
+| Personal access token | `n671WNGecHugsdEDPsyo` |
+| Application ID | `2fcb195768c39e9a94cec2c2e32c59c0aad7a3365c10892e8116b5d83d4096b6` |
+| Application secret | `04f294d1eaca42b8692017b426d53bbc8fe75f827734f0260710b83a556082df` |
+| Secret CI variable | `Li8j-mLUVA3eZYjPfd_H` |
+| Specific Runner token | `yrnZW46BrtBFqM7xDzE7dddd` |
+| Shared Runner token | `6Vk7ZsosqQyfreAxXTZr` |
+| Trigger token | `be20d8dcc028677c931e04f3871a9b` |
+| Webhook secret token | `6XhDroRcYPM5by_h-HLY` |
+| Health check token | `Tu7BgjR9qeZTEyRzGG2P` |
+| Request profile token | `7VgpS4Ax5utVD2esNstz` |
+
+### API
+
+Here is a list of must-have items. Use them in the exact order that appears
+on this document. Further explanation is given below.
+
+- Every method must have the REST API request. For example:
+
+ ```
+ GET /projects/:id/repository/branches
+ ```
+
+- Every method must have a detailed
+ [description of the parameters](#method-description).
+- Every method must have a cURL example.
+- Every method must have a response body (in JSON format).
+
+#### Method description
+
+Use the following table headers to describe the methods. Attributes should
+always be in code blocks using backticks (``` ` ```).
+
+```
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+```
+
+Rendered example:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `user` | string | yes | The GitLab username |
+
+#### cURL commands
+
+- Use `https://gitlab.example.com/api/v4/` as an endpoint.
+- 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 personal access token and don't pass data of
+ username and password.
+
+| Methods | Description |
+| ------- | ----------- |
+| `-H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK"` | Use this method as is, whenever authentication needed |
+| `-X POST` | Use this method when creating new objects |
+| `-X PUT` | Use this method when updating existing objects |
+| `-X DELETE` | Use this method when removing existing objects |
+
+#### cURL Examples
+
+Below is a set of [cURL][] examples that you can use in the API documentation.
+
+##### Simple cURL command
+
+Get the details of a group:
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/gitlab-org
+```
+
+##### cURL example with parameters passed in the URL
+
+Create a new project under the authenticated user's namespace:
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects?name=foo"
+```
+
+##### Post data using cURL's --data
+
+Instead of using `-X POST` and appending the parameters to the URI, you can use
+cURL's `--data` option. The example below will create a new project `foo` under
+the authenticated user's namespace.
+
+```bash
+curl --data "name=foo" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects"
+```
+
+##### Post data using JSON content
+
+> **Note:** In this example we create a new group. Watch carefully the single
+and double quotes.
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "Content-Type: application/json" --data '{"path": "my-group", "name": "My group"}' https://gitlab.example.com/api/v4/groups
+```
+
+##### Post data using form-data
+
+Instead of using JSON or urlencode you can use multipart/form-data which
+properly handles data encoding:
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "title=ssh-key" --form "key=ssh-rsa AAAAB3NzaC1yc2EA..." https://gitlab.example.com/api/v4/users/25/keys
+```
+
+The above example is run by and administrator and will add an SSH public key
+titled ssh-key to user's account which has an id of 25.
+
+##### Escape special characters
+
+Spaces or slashes (`/`) may sometimes result to errors, thus it is recommended
+to escape them when possible. In the example below we create a new issue which
+contains spaces in its title. Observe how spaces are escaped using the `%20`
+ASCII code.
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/42/issues?title=Hello%20Dude"
+```
+
+Use `%2F` for slashes (`/`).
+
+##### Pass arrays to API calls
+
+The GitLab API sometimes accepts arrays of strings or integers. For example, to
+restrict the sign-up e-mail domains of a GitLab instance to `*.example.com` and
+`example.net`, you would do something like this:
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "domain_whitelist[]=*.example.com" --data "domain_whitelist[]=example.net" https://gitlab.example.com/api/v4/application/settings
+```
+
+[cURL]: http://curl.haxx.se/ "cURL website"
+[single spaces]: http://www.slate.com/articles/technology/technology/2011/01/space_invaders.html
+[gfm]: http://docs.gitlab.com/ce/user/markdown.html#newlines "GitLab flavored markdown documentation"
+[ce-1242]: https://gitlab.com/gitlab-org/gitlab-ce/issues/1242
+[doc-restart]: ../../administration/restart_gitlab.md "GitLab restart documentation"
diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md
index 057a4094aed..32de741c9fe 100644
--- a/doc/development/ee_features.md
+++ b/doc/development/ee_features.md
@@ -5,7 +5,7 @@
- **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].
+ [EE features list](https://about.gitlab.com/features/).
## Act as CE when unlicensed
@@ -368,27 +368,17 @@ resolve when you add the indentation to the equation.
EE-specific views should be placed in `ee/app/views/`, using extra
sub-directories if appropriate.
+#### Using `render_if_exists`
+
Instead of using regular `render`, we should use `render_if_exists`, which
will not render anything if it cannot find the specific partial. We use this
so that we could put `render_if_exists` in CE, keeping code the same between
CE and EE.
-Also, it should search for the EE partial first, and then CE partial, and
-then if nothing found, render nothing.
-
-This has two uses:
-
-- CE renders nothing, and EE renders its EE partial.
-- CE renders its CE partial, and EE renders its EE partial, while the view
- file stays the same.
-
The advantages of this:
- Minimal code difference between CE and EE.
- Very clear hints about where we're extending EE views while reading CE codes.
-- Whenever we want to show something different in CE, we could just add CE
- partials. Same applies the other way around. If we just use
- `render_if_exists`, it would be very easy to change the content in EE.
The disadvantage of this:
@@ -396,6 +386,42 @@ The disadvantage of this:
port `render_if_exists` to CE.
- If we have typos in the partial name, it would be silently ignored.
+#### Using `render_ce`
+
+For `render` and `render_if_exists`, they search for the EE partial first,
+and then CE partial. They would only render a particular partial, not all
+partials with the same name. We could take the advantage of this, so that
+the same partial path (e.g. `shared/issuable/form/default_templates`) could
+be referring to the CE partial in CE (i.e.
+`app/views/shared/issuable/form/_default_templates.html.haml`), while EE
+partial in EE (i.e.
+`ee/app/views/shared/issuable/form/_default_templates.html.haml`). This way,
+we could show different things between CE and EE.
+
+However sometimes we would also want to reuse the CE partial in EE partial
+because we might just want to add something to the existing CE partial. We
+could workaround this by adding another partial with a different name, but it
+would be tedious to do so.
+
+In this case, we could as well just use `render_ce` which would ignore any EE
+partials. One example would be
+`ee/app/views/shared/issuable/form/_default_templates.html.haml`:
+
+``` haml
+- if @project.feature_available?(:issuable_default_templates)
+ = render_ce 'shared/issuable/form/default_templates'
+- elsif show_promotions?
+ = render 'shared/promotions/promote_issue_templates'
+```
+
+In the above example, we can't use
+`render 'shared/issuable/form/default_templates'` because it would find the
+same EE partial, causing infinite recursion. Instead, we could use `render_ce`
+so it ignores any partials in `ee/` and then it would render the CE partial
+(i.e. `app/views/shared/issuable/form/_default_templates.html.haml`)
+for the same path (i.e. `shared/issuable/form/default_templates`). This way
+we could easily wrap around the CE partial.
+
### Code in `lib/`
Place EE-specific logic in the top-level `EE` module namespace. Namespace the
diff --git a/doc/development/emails.md b/doc/development/emails.md
index 73cac82caf0..35ada35babe 100644
--- a/doc/development/emails.md
+++ b/doc/development/emails.md
@@ -10,12 +10,12 @@ To view rendered emails "sent" in your development instance, visit
Rails provides a way to preview our mailer templates in HTML and plaintext using
dummy data.
-The previews live in [`spec/mailers/previews`][previews] and can be viewed at
+The previews live in [`app/mailers/previews`][previews] and can be viewed at
[`/rails/mailers`](http://localhost:3000/rails/mailers).
See the [Rails guides] for more info.
-[previews]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/spec/mailers/previews
+[previews]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/mailers/previews
[Rails guides]: http://guides.rubyonrails.org/action_mailer_basics.html#previewing-emails
## Incoming email
diff --git a/doc/development/fe_guide/development_process.md b/doc/development/fe_guide/development_process.md
index d240dbe8b02..905668eef58 100644
--- a/doc/development/fe_guide/development_process.md
+++ b/doc/development/fe_guide/development_process.md
@@ -1,6 +1,6 @@
# Frontend Development Process
-You can find more about the organization of the frontend team in the [handbook](https://about.gitlab.com/handbook/frontend/).
+You can find more about the organization of the frontend team in the [handbook](https://about.gitlab.com/handbook/engineering/frontend/).
## Development Checklist
@@ -34,7 +34,7 @@ Please use your best judgement when to use it and please contribute new points t
- [ ] **Cookie Mode** Think about hiding the feature behind a cookie flag if the implementation is on top of existing features
- [ ] **New route** Are you refactoring something big then you might consider adding a new route where you implement the new feature and when finished delete the current route and rename the new one. (for example 'merge_request' and 'new_merge_request')
- [ ] **Setup** Is there any specific setup needed for your implementation (for example a kubernetes cluster)? Then let everyone know if it is not already mentioned where they can find documentation (if it doesn't exist - create it)
-- [ ] **Security** Are there any new security relevant implementations? Then please contact the security team for an app security review. If you are not sure ask our [domain expert](https://about.gitlab.com/handbook/frontend/#frontend-domain-experts)
+- [ ] **Security** Are there any new security relevant implementations? Then please contact the security team for an app security review. If you are not sure ask our [domain expert](https://about.gitlab.com/handbook/engineering/frontend/#frontend-domain-experts)
#### During development
@@ -51,7 +51,7 @@ Please use your best judgement when to use it and please contribute new points t
- [ ] **Performance** Have you checked performance? For example do the same thing with 500 comments instead of 1. Document the tests and possible findings in the MR so a reviewer can directly see it.
- [ ] Have you tested with a variety of our [supported browsers](../../install/requirements.md#supported-web-browsers)? You can use [browserstack](https://www.browserstack.com/) to be able to access a wide variety of browsers and operating systems.
- [ ] Did you check the mobile view?
-- [ ] Check the built webpack bundle (For the report run `WEBPACK_REPORT=true gdk run`, then open `webpack-report/index.html`) if we have unnecessary bloat due to wrong references, including libraries multiple times, etc.. If you need help contact the webpack [domain expert](https://about.gitlab.com/handbook/frontend/#frontend-domain-experts)
+- [ ] Check the built webpack bundle (For the report run `WEBPACK_REPORT=true gdk run`, then open `webpack-report/index.html`) if we have unnecessary bloat due to wrong references, including libraries multiple times, etc.. If you need help contact the webpack [domain expert](https://about.gitlab.com/handbook/engineering/frontend/#frontend-domain-experts)
- [ ] **Tests** Not only greenfield tests - Test also all bad cases that come to your mind.
- [ ] If you have multiple MR's then also smoke test against the final merge.
- [ ] Are there any big changes on how and especially how frequently we use the API then let production know about it
diff --git a/doc/development/fe_guide/icons.md b/doc/development/fe_guide/icons.md
index b469a9c6aef..3d8da6accc1 100644
--- a/doc/development/fe_guide/icons.md
+++ b/doc/development/fe_guide/icons.md
@@ -1,26 +1,44 @@
-# Icons
+# Icons and SVG Illustrations
-We are using SVG Icons in GitLab with a SVG Sprite, due to this the icons are only loaded once and then referenced through an ID. The sprite SVG is located under `/assets/icons.svg`. Our goal is to replace one by one all inline SVG Icons (as those currently bloat the HTML) and also all Font Awesome usages.
+We manage our own Icon and Illustration library in the [gitlab-svgs][gitlab-svgs] repository.
+This repository is published on [npm][npm] and managed as a dependency via yarn.
+You can browse all available Icons and Illustrations [here][svg-preview].
+To upgrade to a new version run `yarn upgrade @gitlab-org/gitlab-svgs`.
-### Usage in HAML/Rails
+## Icons
-To use a sprite Icon in HAML or Rails we use a specific helper function :
+We are using SVG Icons in GitLab with a SVG Sprite.
+This means the icons are only loaded once, and are referenced through an ID.
+The sprite SVG is located under `/assets/icons.svg`.
+
+Our goal is to replace one by one all inline SVG Icons (as those currently bloat the HTML) and also all Font Awesome icons.
-`sprite_icon(icon_name, size: nil, css_class: '')`
+### Usage in HAML/Rails
-**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/)`).
+To use a sprite Icon in HAML or Rails we use a specific helper function :
-**size (optional)** Use one of the following sizes : 16,24,32,48,72 (this will be translated into a `s16` class)
+```ruby
+sprite_icon(icon_name, size: nil, css_class: '')
+```
-**css_class (optional)** If you want to add additional css classes
+- **icon_name** Use the icon_name that you can find in the SVG Sprite
+ ([Overview is available here][svg-preview]).
+- **size (optional)** Use one of the following sizes : 16, 24, 32, 48, 72 (this will be translated into a `s16` class)
+- **css_class (optional)** If you want to add additional css classes
**Example**
-`= sprite_icon('issues', size: 72, css_class: 'icon-danger')`
+```haml
+= sprite_icon('issues', size: 72, css_class: 'icon-danger')
+```
**Output from example above**
-`<svg class="s72 icon-danger"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/assets/icons.svg#issues"></use></svg>`
+```html
+<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
@@ -28,33 +46,71 @@ We have a special Vue component for our sprite icons in `\vue_shared\components\
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.
+```javascript
+<script>
+import Icon from "~/vue_shared/components/icon.vue"
+
+export default {
+ components: {
+ Icon,
+ },
+};
+<script>
+<template>
+ <icon
+ name="issues"
+ :size="72"
+ css-classes="icon-danger"
+ />
+</template>
+```
+
+- **name** Name of the Icon in the SVG Sprite ([Overview is available here][svg-preview]).
+- **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
+## SVG Illustrations
-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.
+Please use from now on for any SVG based illustrations simple `img` tags to show an illustration by simply using either `image_tag` or `image_path` helpers.
+Please use the class `svg-content` around it to ensure nice rendering.
-To upgrade to a new SVG Sprite version run `yarn upgrade @gitlab-org/gitlab-svgs`.
+### Usage in HAML/Rails
-# SVG Illustrations
+**Example**
-Please use from now on for any SVG based illustrations simple `img` tags to show an illustration by simply using either `image_tag` or `image_path` helpers. Please use the class `svg-content` around it to ensure nice rendering. The illustrations are also organised in the [gitlab-svgs](https://gitlab.com/gitlab-org/gitlab-svgs) repository (as they are then automatically optimised).
+```haml
+.svg-content
+ = image_tag 'illustrations/merge_requests.svg'
+```
-**Example**
+### Usage in Vue
-`= image_tag 'illustrations/merge_requests.svg'`
+To use an SVG illustrations in a template provide the path as a property and display it through a standard img tag.
+
+Component:
+
+```js
+<script>
+export default {
+ props: {
+ svgIllustrationPath: {
+ type: String,
+ required: true,
+ },
+ },
+};
+<script>
+<template>
+ <img :src="svgIllustrationPath" />
+</template>
+```
+
+[npm]: https://www.npmjs.com/package/@gitlab-org/gitlab-svgs
+[gitlab-svgs]: https://gitlab.com/gitlab-org/gitlab-svgs
+[svg-preview]: https://gitlab-org.gitlab.io/gitlab-svgs
diff --git a/doc/development/fe_guide/index.md b/doc/development/fe_guide/index.md
index 6d3796e7560..11b9a2e6a64 100644
--- a/doc/development/fe_guide/index.md
+++ b/doc/development/fe_guide/index.md
@@ -54,8 +54,8 @@ Vuex specific design patterns and practices.
## [Axios](axios.md)
Axios specific practices and gotchas.
-## [Icons](icons.md)
-How we use SVG for our Icons.
+## [Icons and Illustrations](icons.md)
+How we use SVG for our Icons and Illustrations.
## [Components](components.md)
diff --git a/doc/development/fe_guide/style_guide_js.md b/doc/development/fe_guide/style_guide_js.md
index 04dfe418dbe..284b4b53334 100644
--- a/doc/development/fe_guide/style_guide_js.md
+++ b/doc/development/fe_guide/style_guide_js.md
@@ -632,7 +632,7 @@ Useful links:
// good
<span title="tooltip text">Foo</span>
- $('span').tooltip('fixTitle');
+ $('span').tooltip('_fixTitle');
```
### The Javascript/Vue Accord
diff --git a/doc/development/fe_guide/style_guide_scss.md b/doc/development/fe_guide/style_guide_scss.md
index 655d94793dd..48eb6d0a7d6 100644
--- a/doc/development/fe_guide/style_guide_scss.md
+++ b/doc/development/fe_guide/style_guide_scss.md
@@ -216,12 +216,12 @@ If you want a line or set of lines to be ignored by the linter, you can use
`// scss-lint:disable RuleName` ([more info][disabling-linters]):
```scss
-// This lint rule is disabled because the class name comes from a gem.
-// scss-lint:disable SelectorFormat
-.ui_indigo {
- background-color: #333;
+// This lint rule is disabled because it is supported only in Chrome/Safari
+// scss-lint:disable PropertySpelling
+body {
+ text-decoration-skip: ink;
}
-// scss-lint:enable SelectorFormat
+// scss-lint:enable PropertySpelling
```
Make sure a comment is added on the line above the `disable` rule, otherwise the
diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md
index f971d8b7388..219b98ac696 100644
--- a/doc/development/fe_guide/vue.md
+++ b/doc/development/fe_guide/vue.md
@@ -8,7 +8,7 @@ All new features built with Vue.js must follow a [Flux architecture][flux].
The main goal we are trying to achieve is to have only one data flow and only one data entry.
In order to achieve this goal, you can either use [vuex](#vuex) or use the [store pattern][state-management], explained below:
-Each Vue bundle needs a Store - where we keep all the data -,a Service - that we use to communicate with the server - and a main Vue component.
+Each Vue bundle needs a Store - where we keep all the data -, a Service - that we use to communicate with the server - and a main Vue component.
Think of the Main Vue Component as the entry point of your application. This is the only smart
component that should exist in each Vue feature.
@@ -17,7 +17,7 @@ This component is responsible for:
1. Calling the Store to store the data received
1. Mounting all the other components
- ![Vue Architecture](img/vue_arch.png)
+![Vue Architecture](img/vue_arch.png)
You can also read about this architecture in vue docs about [state management][state-management]
and about [one way data flow][one-way-data-flow].
@@ -51,14 +51,14 @@ of the new feature should be.
The Store and the Service should be imported and initialized in this file and
provided as a prop to the main component.
-Don't forget to follow [these steps.][page_specific_javascript]
+Don't forget to follow [these steps][page_specific_javascript].
### Bootstrapping Gotchas
-#### Providing data from Haml to JavaScript
+#### Providing data from HAML to JavaScript
While mounting a Vue application may be a need to provide data from Rails to JavaScript.
To do that, provide the data through `data` attributes in the HTML element and query them while mounting the application.
-_Note:_ You should only do this while initing the application, because the mounted element will be replaced with Vue-generated DOM.
+_Note:_ You should only do this while initializing the application, because the mounted element will be replaced with Vue-generated DOM.
The advantage of providing data from the DOM to the Vue instance through `props` in the `render` function
instead of querying the DOM inside the main vue component is that makes tests easier by avoiding the need to
@@ -68,6 +68,7 @@ create a fixture or an HTML element in the unit test. See the following example:
// haml
.js-vue-app{ data: { endpoint: 'foo' }}
+// index.js
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '.js-vue-app',
data() {
@@ -87,13 +88,11 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
```
#### Accessing the `gl` object
-When we need to query the `gl` object for data that won't change during the application's lyfecyle, we should do it in the same place where we query the DOM.
+When we need to query the `gl` object for data that won't change during the application's life cyle, we should do it in the same place where we query the DOM.
By following this practice, we can avoid the need to mock the `gl` object, which will make tests easier.
It should be done while initializing our Vue instance, and the data should be provided as `props` to the main component:
-##### example:
```javascript
-
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '.js-vue-app',
render(createElement) {
@@ -121,25 +120,6 @@ in one table would not be a good use of this pattern.
You can read more about components in Vue.js site, [Component System][component-system]
-#### Components Gotchas
-1. Using SVGs icons in components: To use an SVG icon in a template use the `icon.vue`
-1. Using SVGs illustrations in components: To use an SVG illustrations in a template provide the path as a prop and display it through a standard img tag.
- ```javascript
- <script>
- export default {
- props: {
- svgIllustrationPath: {
- type: String,
- required: true,
- },
- },
- };
- <script>
- <template>
- <img :src="svgIllustrationPath" />
- </template>
- ```
-
### A folder for the Store
#### Vuex
@@ -163,13 +143,13 @@ Refer to [axios](axios.md) for more details.
Axios instance should only be imported in the service file.
- ```javascript
- import axios from 'javascripts/lib/utils/axios_utils';
- ```
+```javascript
+import axios from '~/lib/utils/axios_utils';
+```
### End Result
-The following example shows an application:
+The following example shows an application:
```javascript
// store.js
@@ -177,8 +157,8 @@ export default class Store {
/**
* This is where we will iniatialize the state of our data.
- * Usually in a small SPA you don't need any options when starting the store. In the case you do
- * need guarantee it's an Object and it's documented.
+ * Usually in a small SPA you don't need any options when starting the store.
+ * In that case you do need guarantee it's an Object and it's documented.
*
* @param {Object} options
*/
@@ -186,7 +166,7 @@ export default class Store {
this.options = options;
// Create a state object to handle all our data in the same place
- this.todos = []:
+ this.todos = [];
}
setTodos(todos = []) {
@@ -207,7 +187,7 @@ export default class Store {
}
// service.js
-import axios from 'javascripts/lib/utils/axios_utils'
+import axios from '~/lib/utils/axios_utils'
export default class Service {
constructor(options) {
@@ -233,8 +213,8 @@ export default {
type: Object,
required: true,
},
- }
-}
+ },
+};
</script>
<template>
<div>
@@ -275,7 +255,7 @@ export default {
},
created() {
- this.service = new Service('todos');
+ this.service = new Service('/todos');
this.getTodos();
},
@@ -284,9 +264,9 @@ export default {
getTodos() {
this.isLoading = true;
- this.service.getTodos()
- .then(response => response.json())
- .then((response) => {
+ this.service
+ .getTodos()
+ .then(response => {
this.store.setTodos(response);
this.isLoading = false;
})
@@ -296,18 +276,21 @@ export default {
});
},
- addTodo(todo) {
- this.service.addTodo(todo)
- then(response => response.json())
- .then((response) => {
- this.store.addTodo(response);
- })
- .catch(() => {
- // Show an error
- });
- }
- }
-}
+ addTodo(event) {
+ this.service
+ .addTodo({
+ title: 'New entry',
+ text: `You clicked on ${event.target.tagName}`,
+ })
+ .then(response => {
+ this.store.addTodo(response);
+ })
+ .catch(() => {
+ // Show an error
+ });
+ },
+ },
+};
</script>
<template>
<div class="container">
@@ -333,7 +316,7 @@ export default {
<div>
</template>
-// bundle.js
+// index.js
import todoComponent from 'todos_main_component.vue';
new Vue({
@@ -365,81 +348,84 @@ Each Vue component has a unique output. This output is always present in the ren
Although we can test each method of a Vue component individually, our goal must be to test the output
of the render/template function, which represents the state at all times.
-Make use of Vue Resource Interceptors to mock data returned by the service.
+Make use of the [axios mock adapter](axios.md#mock-axios-response-on-tests) to mock data returned.
Here's how we would test the Todo App above:
```javascript
-import component from 'todos_main_component';
+import Vue from 'vue';
+import axios from '~/lib/utils/axios_utils';
+import MockAdapter from 'axios-mock-adapter';
describe('Todos App', () => {
- it('should render the loading state while the request is being made', () => {
+ let vm;
+ let mock;
+
+ beforeEach(() => {
+ // Create a mock adapter for stubbing axios API requests
+ mock = new MockAdapter(axios);
+
const Component = Vue.extend(component);
- const vm = new Component().$mount();
+ // Mount the Component
+ vm = new Component().$mount();
+ });
+
+ afterEach(() => {
+ // Reset the mock adapter
+ mock.restore();
+ // Destroy the mounted component
+ vm.$destroy();
+ });
+ it('should render the loading state while the request is being made', () => {
expect(vm.$el.querySelector('i.fa-spin')).toBeDefined();
});
- describe('with data', () => {
- // Mock the service to return data
- const interceptor = (request, next) => {
- next(request.respondWith(JSON.stringify([{
+ it('should render todos returned by the endpoint', done => {
+ // Mock the get request on the API endpoint to return data
+ mock.onGet('/todos').replyOnce(200, [
+ {
title: 'This is a todo',
- body: 'This is the text'
- }]), {
- status: 200,
- }));
- };
-
- let vm;
-
- beforeEach(() => {
- Vue.http.interceptors.push(interceptor);
-
- const Component = Vue.extend(component);
+ text: 'This is the text',
+ },
+ ]);
- vm = new Component().$mount();
+ Vue.nextTick(() => {
+ const items = vm.$el.querySelectorAll('.js-todo-list div')
+ expect(items.length).toBe(1);
+ expect(items[0].textContent).toContain('This is the text');
+ done();
});
+ });
- afterEach(() => {
- Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
- });
+ it('should add a todos on button click', (done) => {
+ // Mock the put request and check that the sent data object is correct
+ mock.onPut('/todos').replyOnce((req) => {
+ expect(req.data).toContain('text');
+ expect(req.data).toContain('title');
- it('should render todos', (done) => {
- setTimeout(() => {
- expect(vm.$el.querySelectorAll('.js-todo-list div').length).toBe(1);
- done();
- }, 0);
+ return [201, {}];
});
- });
- describe('add todo', () => {
- let vm;
- beforeEach(() => {
- const Component = Vue.extend(component);
- vm = new Component().$mount();
- });
- it('should add a todos', (done) => {
- setTimeout(() => {
- vm.$el.querySelector('.js-add-todo').click();
+ vm.$el.querySelector('.js-add-todo').click();
- // Add a new interceptor to mock the add Todo request
- Vue.nextTick(() => {
- expect(vm.$el.querySelectorAll('.js-todo-list div').length).toBe(2);
- });
- }, 0);
+ // Add a new interceptor to mock the add Todo request
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelectorAll('.js-todo-list div').length).toBe(2);
+ done();
});
});
});
```
-#### `mountComponent` helper
+
+### `mountComponent` helper
There is a helper in `spec/javascripts/helpers/vue_mount_component_helper.js` that allows you to mount a component with the given props:
```javascript
import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper.js'
+import mountComponent from 'spec/helpers/vue_mount_component_helper'
import component from 'component.vue'
const Component = Vue.extend(component);
@@ -447,13 +433,10 @@ const data = {prop: 'foo'};
const vm = mountComponent(Component, data);
```
-#### Test the component's output
+### Test the component's output
The main return value of a Vue component is the rendered output. In order to test the component we
need to test the rendered output. [Vue][vue-test] guide's to unit test show us exactly that:
-### Stubbing API responses
-Refer to [mock axios](axios.md#mock-axios-response-on-tests)
-
[vue-docs]: http://vuejs.org/guide/index.html
[issue-boards]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/boards
@@ -466,4 +449,3 @@ Refer to [mock axios](axios.md#mock-axios-response-on-tests)
[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
[axios]: https://github.com/axios/axios
-[axios-interceptors]: https://github.com/axios/axios#interceptors
diff --git a/doc/development/fe_guide/vuex.md b/doc/development/fe_guide/vuex.md
index 858b03c60bf..4089cd37d73 100644
--- a/doc/development/fe_guide/vuex.md
+++ b/doc/development/fe_guide/vuex.md
@@ -78,7 +78,7 @@ In this file, we will write the actions that will call the respective mutations:
```javascript
import * as types from './mutation_types';
- import axios from '~/lib/utils/axios-utils';
+ import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
export const requestUsers = ({ commit }) => commit(types.REQUEST_USERS);
@@ -214,7 +214,7 @@ import { mapGetters } from 'vuex';
};
```
-### `mutations_types.js`
+### `mutation_types.js`
From [vuex mutations docs][vuex-mutations]:
> It is a commonly seen pattern to use constants for mutation types in various Flux implementations. This allows the code to take advantage of tooling like linters, and putting all constants in a single file allows your collaborators to get an at-a-glance view of what mutations are possible in the entire application.
diff --git a/doc/development/gotchas.md b/doc/development/gotchas.md
index 5786287d00c..d25d856c3a3 100644
--- a/doc/development/gotchas.md
+++ b/doc/development/gotchas.md
@@ -92,6 +92,54 @@ describe API::Labels do
end
```
+## Avoid using `allow_any_instance_of` in RSpec
+
+### Why
+
+* Because it is not isolated therefore it might be broken at times.
+* Because it doesn't work whenever the method we want to stub was defined
+ in a prepended module, which is very likely the case in EE. We could see
+ error like this:
+
+ 1.1) Failure/Error: allow_any_instance_of(ApplicationSetting).to receive_messages(messages)
+ Using `any_instance` to stub a method (elasticsearch_indexing) that has been defined on a prepended module (EE::ApplicationSetting) is not supported.
+
+### Alternative: `expect_next_instance_of`
+
+Instead of writing:
+
+```ruby
+# Don't do this:
+allow_any_instance_of(Project).to receive(:add_import_job)
+```
+
+We could write:
+
+```ruby
+# Do this:
+expect_next_instance_of(Project) do |project|
+ expect(project).to receive(:add_import_job)
+end
+```
+
+If we also want to expect the instance was initialized with some particular
+arguments, we could also pass it to `expect_next_instance_of` like:
+
+```ruby
+# Do this:
+expect_next_instance_of(MergeRequests::RefreshService, project, user) do |refresh_service|
+ expect(refresh_service).to receive(:execute).with(oldrev, newrev, ref)
+end
+```
+
+This would expect the following:
+
+```ruby
+# Above expects:
+refresh_service = MergeRequests::RefreshService.new(project, user)
+refresh_service.execute(oldrev, newrev, ref)
+```
+
## Do not `rescue Exception`
See ["Why is it bad style to `rescue Exception => e` in Ruby?"][Exception].
diff --git a/doc/development/i18n/externalization.md b/doc/development/i18n/externalization.md
index 0edcb23c7c5..6323275426f 100644
--- a/doc/development/i18n/externalization.md
+++ b/doc/development/i18n/externalization.md
@@ -174,6 +174,8 @@ For example use `%{created_at}` in Ruby but `%{createdAt}` in JavaScript.
# => When size == 2: 'There are 2 mice.'
```
+ Avoid using `%d` or count variables in sigular strings. This allows more natural translation in some languages.
+
- In JavaScript:
```js
@@ -233,7 +235,7 @@ This makes use of [`Intl.DateTimeFormat`].
Please never split a sentence as that would assume the sentence grammar and
structure is the same in all languages.
-For instance, the following
+For instance, the following:
```js
{{ s__("mrWidget|Set by") }}
@@ -247,6 +249,27 @@ should be externalized as follows:
{{ sprintf(s__("mrWidget|Set by %{author} to be merged automatically when the pipeline succeeds"), { author: author.name }) }}
```
+#### Avoid splitting sentences when adding links
+
+This also applies when using links in between translated sentences, otherwise these texts are not translatable in certain languages.
+
+Instead of:
+
+```haml
+- zones_link = link_to(s_('ClusterIntegration|zones'), 'https://cloud.google.com/compute/docs/regions-zones/regions-zones', target: '_blank', rel: 'noopener noreferrer')
+= s_('ClusterIntegration|Learn more about %{zones_link}').html_safe % { zones_link: zones_link }
+```
+
+Set the link starting and ending HTML fragments as variables like so:
+
+```haml
+- zones_link_url = 'https://cloud.google.com/compute/docs/regions-zones/regions-zones'
+- zones_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: zones_link_url }
+= s_('ClusterIntegration|Learn more about %{zones_link_start}zones%{zones_link_end}').html_safe % { zones_link_start: zones_link_start, zones_link_end: '</a>'.html_safe }
+```
+
+The reasoning behind this is that in some languages words change depending on context. For example in Japanese 㯠is added to the subject of a sentence and を to the object. This is impossible to translate correctly if we extract individual words from the sentence.
+
When in doubt, try to follow the best practices described in this [Mozilla
Developer documentation][mdn].
@@ -258,7 +281,7 @@ Now that the new content is marked for translation, we need to update the PO
files with the following command:
```sh
-bin/rake gettext:find
+bin/rake gettext:regenerate
```
This command will update the `locale/gitlab.pot` file with the newly externalized
@@ -269,16 +292,6 @@ file in. Once the changes are on master, they will be picked up by
If there are merge conflicts in the `gitlab.pot` file, you can delete the file
and regenerate it using the same command. Confirm that you are not deleting any strings accidentally by looking over the diff.
-The command also updates the translation files for each language: `locale/*/gitlab.po`
-These changes can be discarded, the language files will be updated by Crowdin
-automatically.
-
-Discard all of them at once like this:
-
-```sh
-git checkout locale/*/gitlab.po
-```
-
### Validating PO files
To make sure we keep our translation files up to date, there's a linter that is
diff --git a/doc/development/i18n/proofreader.md b/doc/development/i18n/proofreader.md
index 5185d843ccb..ca8ebcdc984 100644
--- a/doc/development/i18n/proofreader.md
+++ b/doc/development/i18n/proofreader.md
@@ -11,12 +11,13 @@ are very appreciative of the work done by translators and proofreaders!
- Chinese Traditional
- Huang Tao - [GitLab](https://gitlab.com/htve), [Crowdin](https://crowdin.com/profile/htve)
- Weizhe Ding - [GitLab](https://gitlab.com/d.weizhe), [Crowdin](https://crowdin.com/profile/d.weizhe)
+ - Yi-Jyun Pan - [GitLab](https://gitlab.com/pan93412), [Crowdin](https://crowdin.com/profile/pan93412)
- Chinese Traditional, Hong Kong
- Huang Tao - [GitLab](https://gitlab.com/htve), [Crowdin](https://crowdin.com/profile/htve)
- Dutch
- Esperanto
- French
- - Rémy Coutable - [GitLab](https://gitlab.com/rymai), [Crowdin](https://crowdin.com/profile/rymai)
+ - Davy Defaud - [GitLab](https://gitlab.com/DevDef), [Crowdin](https://crowdin.com/profile/DevDef)
- German
- Indonesian
- Ahmad Naufal Mukhtar - [GitLab](https://gitlab.com/anaufalm), [Crowdin](https://crowdin.com/profile/anaufalm)
@@ -24,6 +25,7 @@ are very appreciative of the work done by translators and proofreaders!
- Paolo Falomo - [GitLab](https://gitlab.com/paolofalomo), [Crowdin](https://crowdin.com/profile/paolo.falomo)
- Japanese
- Yamana Tokiuji - [GitLab](https://gitlab.com/tokiuji), [Crowdin](https://crowdin.com/profile/yamana)
+ - Hiroyuki Sato - [GitLab](https://gitlab.com/hiroponz), [Crowdin](https://crowdin.com/profile/hiroponz)
- Korean
- Chang-Ho Cha - [GitLab](https://gitlab.com/changho-cha), [Crowdin](https://crowdin.com/profile/zzazang)
- Huang Tao - [GitLab](https://gitlab.com/htve), [Crowdin](https://crowdin.com/profile/htve)
@@ -31,6 +33,7 @@ are very appreciative of the work done by translators and proofreaders!
- Filip Mech - [GitLab](https://gitlab.com/mehenz), [Crowdin](https://crowdin.com/profile/mehenz)
- Portuguese, Brazilian
- Paulo George Gomes Bezerra - [GitLab](https://gitlab.com/paulobezerra), [Crowdin](https://crowdin.com/profile/paulogomes.rep)
+ - André Gama - [GitLab](https://gitlab.com/andregamma), [Crowdin](https://crowdin.com/profile/ToeOficial)
- Russian
- Nikita Grylov - [GitLab](https://gitlab.com/nixel2007), [Crowdin](https://crowdin.com/profile/nixel2007)
- Alexy Lustin - [GitLab](https://gitlab.com/allustin), [Crowdin](https://crowdin.com/profile/lustin)
diff --git a/doc/development/licensing.md b/doc/development/licensing.md
index c06bc0d4731..ddaf636a742 100644
--- a/doc/development/licensing.md
+++ b/doc/development/licensing.md
@@ -60,7 +60,7 @@ Libraries with the following licenses are acceptable for use:
## Unacceptable Licenses
-Libraries with the following licenses are unacceptable for use:
+Libraries with the following licenses require legal approval for use:
- [GNU GPL][GPL] (version 1, [version 2][GPLv2], [version 3][GPLv3], or any future versions): GPL-licensed libraries cannot be linked to from non-GPL projects.
- [GNU AGPLv3][AGPLv3]: AGPL-licensed libraries cannot be linked to from non-GPL projects.
@@ -68,6 +68,26 @@ Libraries with the following licenses are unacceptable for use:
- [Facebook BSD + PATENTS][Facebook]: is a 3-clause BSD license with a patent grant that has been deemed [Category X][x-list] by the Apache foundation.
- [WTFPL][WTFPL]: is a public domain dedication [rejected by the OSI (3.2)][WTFPL-OSI]. Also has a strong language which is not in accordance with our diversity policy.
+## GPL Cooperation Commitment
+
+Before filing or continuing to prosecute any legal proceeding or claim (other than a Defensive Action) arising from termination of a Covered License, GitLab commits to extend to the person or entity (“youâ€) accused of violating the Covered License the following provisions regarding cure and reinstatement, taken from GPL version 3. As used here, the term ‘this License’ refers to the specific Covered License being enforced.
+
+However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation.
+
+Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice.
+
+GitLab intends this Commitment to be irrevocable, and binding and enforceable against GitLab and assignees of or successors to GitLab’s copyrights.
+
+GitLab may modify this Commitment by publishing a new edition on this page or a successor location.
+
+Definitions
+
+‘Covered License’ means the GNU General Public License, version 2 (GPLv2), the GNU Lesser General Public License, version 2.1 (LGPLv2.1), or the GNU Library General Public License, version 2 (LGPLv2), all as published by the Free Software Foundation.
+
+‘Defensive Action’ means a legal proceeding or claim that GitLab brings against you in response to a prior proceeding or claim initiated by you or your affiliate.
+
+GitLab means GitLab Inc. and its affiliates and subsidiaries.
+
## Requesting Approval for Licenses
Libraries that are not listed in the [Acceptable Licenses][Acceptable-Licenses] or [Unacceptable Licenses][Unacceptable-Licenses] list can be submitted to the legal team for review. Please email `legal@gitlab.com` with the details. After a decision has been made, the original requestor is responsible for updating this document.
diff --git a/doc/development/new_fe_guide/dependencies.md b/doc/development/new_fe_guide/dependencies.md
index 3417d77a06d..12a4f089d41 100644
--- a/doc/development/new_fe_guide/dependencies.md
+++ b/doc/development/new_fe_guide/dependencies.md
@@ -1,3 +1,20 @@
# Dependencies
-> TODO: Add Dependencies \ No newline at end of file
+## Adding Dependencies.
+
+GitLab uses `yarn` to manage dependencies. These dependencies are defined in
+two groups within `package.json`, `dependencies` and `devDependencies`. For
+our purposes, we consider anything that is required to compile our production
+assets a "production" dependency. That is, anything required to run the
+`webpack` script with `NODE_ENV=production`. Tools like `eslint`, `karma`, and
+various plugins and tools used in development are considered `devDependencies`.
+This distinction is used by omnibus to determine which dependencies it requires
+when building GitLab.
+
+Exceptions are made for some tools that we require in the
+`gitlab:assets:compile` CI job such as `webpack-bundle-analyzer` to analyze our
+production assets post-compile.
+
+---
+
+> TODO: Add Dependencies
diff --git a/doc/development/new_fe_guide/development/accessibility.md b/doc/development/new_fe_guide/development/accessibility.md
index ed35f08432f..2a3a126ca5c 100644
--- a/doc/development/new_fe_guide/development/accessibility.md
+++ b/doc/development/new_fe_guide/development/accessibility.md
@@ -1,3 +1,48 @@
-# Accessibility
+# Accessiblity
+Using semantic HTML plays a key role when it comes to accessibility.
-> TODO: Add content
+## Accessible Rich Internet Applications - ARIA
+WAI-ARIA, the Accessible Rich Internet Applications specification, defines a way to make Web content and Web applications more accessible to people with disabilities.
+
+> Note: It is [recommended][using-aria] to use semantic elements as the primary method to achieve accessibility rather than adding aria attributes. Adding aria attributes should be seen as a secondary method for creating accessible elements.
+
+### Role
+The `role` attribute describes the role the element plays in the context of the document.
+
+Check the list of WAI-ARIA roles [here][roles]
+
+## Icons
+When using icons or images that aren't absolutely needed to understand the context, we should use `aria-hidden="true"`.
+
+On the other hand, if an icon is crucial to understand the context we should do one of the following:
+1. Use `aria-label` in the element with a meaningful description
+1. Use `aria-labelledby` to point to an element that contains the explanation for that icon
+
+## Form inputs
+In forms we should use the `for` attribute in the label statement:
+```
+<div>
+ <label for="name">Fill in your name:</label>
+ <input type="text" id="name" name="name">
+</div>
+```
+
+## Testing
+
+1. On MacOS you can use [VoiceOver][voice-over] by pressing `cmd+F5`.
+1. On Windows you can use [Narrator][narrator] by pressing Windows logo key + Ctrl + Enter.
+
+## Online resources
+
+- [Chrome Accessibility Developer Tools][dev-tools] for testing accessibility
+- [Audit Rules Page][audit-rules] for best practices
+- [Lighthouse Accessibility Score][lighthouse] for accessibility audits
+
+[using-aria]: https://www.w3.org/TR/using-aria/#notes2
+[dev-tools]: https://github.com/GoogleChrome/accessibility-developer-tools
+[audit-rules]: https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules
+[aria-w3c]: https://www.w3.org/TR/wai-aria-1.1/
+[roles]: https://www.w3.org/TR/wai-aria-1.1/#landmark_roles
+[voice-over]: https://www.apple.com/accessibility/mac/vision/
+[narrator]: https://www.microsoft.com/en-us/accessibility/windows
+[lighthouse]: https://developers.google.com/web/tools/lighthouse/scoring#a11y
diff --git a/doc/development/new_fe_guide/development/testing.md b/doc/development/new_fe_guide/development/testing.md
index c359bd83ed1..e1e13474b75 100644
--- a/doc/development/new_fe_guide/development/testing.md
+++ b/doc/development/new_fe_guide/development/testing.md
@@ -1,3 +1,135 @@
-# Testing
+# Overview of Frontend Testing
-> TODO: Add content
+## Types of tests in our codebase
+
+* **RSpec**
+ * **[Ruby unit tests](#ruby-unit-tests-spec-rb)** for models, controllers, helpers, etc. (`/spec/**/*.rb`)
+ * **[Full feature tests](#full-feature-tests-spec-features-rb)** (`/spec/features/**/*.rb`)
+* **[Karma](#karma-tests-spec-javascripts-js)** (`/spec/javascripts/**/*.js`)
+* ~~Spinach~~ — These have been removed from our codebase in May 2018. (`/features/`)
+
+## RSpec: Ruby unit tests `/spec/**/*.rb`
+
+These tests are meant to unit test the ruby models, controllers and helpers.
+
+### When do we write/update these tests?
+
+Whenever we create or modify any Ruby models, controllers or helpers we add/update corresponding tests.
+
+---
+
+## RSpec: Full feature tests `/spec/features/**/*.rb`
+
+Full feature tests will load a full app environment and allow us to test things like rendering DOM, interacting with links and buttons, testing the outcome of those interactions through multiple pages if necessary. These are also called end-to-end tests but should not be confused with QA end-to-end tests (`package-and-qa` manual pipeline job).
+
+### When do we write/update these tests?
+
+When we add a new feature, we write at least two tests covering the success and the failure scenarios.
+
+### Relevant notes
+
+A `:js` flag is added to the test to make sure the full environment is loaded.
+
+```
+scenario 'successfully', :js do
+ sign_in(create(:admin))
+end
+```
+
+The steps of each test are written using capybara methods ([documentation](http://www.rubydoc.info/gems/capybara/2.15.1)).
+
+Bear in mind <abbr title="XMLHttpRequest">XHR</abbr> calls might require you to use `wait_for_requests` in between steps, like so:
+
+```rspec
+find('.form-control').native.send_keys(:enter)
+
+wait_for_requests
+
+expect(page).not_to have_selector('.card')
+```
+
+---
+
+## Karma tests `/spec/javascripts/**/*.js`
+
+These are the more frontend-focused, at the moment. They're **faster** than `rspec` and make for very quick testing of frontend components.
+
+### When do we write/update these tests?
+
+When we add/update a method/action/mutation to Vue or Vuex, we write karma tests to ensure the logic we wrote doesn't break. We should, however, refrain from writing tests that double-test Vue's internal features.
+
+### Relevant notes
+
+Karma tests are run against a virtual DOM.
+
+To populate the DOM, we can use fixtures to fake the generation of HTML instead of having Rails do that.
+
+Be sure to check the [best practices for karma tests](../../testing_guide/frontend_testing.html#best-practices).
+
+### Vue and Vuex
+
+Test as much as possible without double-testing Vue's internal features, as mentioned above.
+
+Make sure to test computedProperties, mutations, actions. Run the action and test that the proper mutations are committed.
+
+Also check these [notes on testing Vue components](../../fe_guide/vue.html#testing-vue-components).
+
+#### Vuex Helper: `testAction`
+
+We have a helper available to make testing actions easier, as per [official documentation](https://vuex.vuejs.org/en/testing.html):
+
+```
+testAction(
+ actions.actionName, // action
+ { }, // params to be passed to action
+ state, // state
+ [
+ { type: types.MUTATION},
+ { type: types.MUTATION_1, payload: {}},
+ ], // mutations committed
+ [
+ { type: 'actionName', payload: {}},
+ { type: 'actionName1', payload: {}},
+ ] // actions dispatched
+ done,
+);
+```
+
+Check an example in [spec/javascripts/ide/stores/actions_spec.jsspec/javascripts/ide/stores/actions_spec.js](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/javascripts/ide/stores/actions_spec.js).
+
+#### Vue Helper: `mountComponent`
+
+To make mounting a Vue component easier and more readable, we have a few helpers available in `spec/helpers/vue_mount_component_helper`.
+
+* `createComponentWithStore`
+* `mountComponentWithStore`
+
+Examples of usage:
+
+```
+beforeEach(() => {
+ vm = createComponentWithStore(Component, store);
+
+ vm.$store.state.currentBranchId = 'master';
+
+ vm.$mount();
+},
+```
+
+```
+beforeEach(() => {
+ vm = mountComponentWithStore(Component, {
+ el: '#dummy-element',
+ store,
+ props: { badge },
+ });
+},
+```
+
+Don't forget to clean up:
+
+```
+afterEach(() => {
+ vm.$destroy();
+});
+```
diff --git a/doc/development/new_fe_guide/style/prettier.md b/doc/development/new_fe_guide/style/prettier.md
index eb18189282b..6395af6f815 100644
--- a/doc/development/new_fe_guide/style/prettier.md
+++ b/doc/development/new_fe_guide/style/prettier.md
@@ -43,3 +43,17 @@ yarn prettier-all-save
Formats all files in the repository with Prettier. (This should only be used to test global rule updates otherwise you would end up with huge MR's).
The source of these Yarn scripts can be found in `/scripts/frontend/prettier.js`.
+
+### Scripts during Conversion period
+
+```
+node ./scripts/frontend/prettier.js check ./vendor/
+```
+
+This will go over all files in a specific folder check it.
+
+```
+node ./scripts/frontend/prettier.js save ./vendor/
+```
+
+This will go over all files in a specific folder and save it.
diff --git a/doc/development/new_fe_guide/tips.md b/doc/development/new_fe_guide/tips.md
index f0cdf52d618..881ad1662ae 100644
--- a/doc/development/new_fe_guide/tips.md
+++ b/doc/development/new_fe_guide/tips.md
@@ -1,3 +1,9 @@
# Tips
-> TODO: Add tips
+## Clearing production compiled assets
+
+To clear production compiled assets created with `yarn webpack-prod` you can run:
+
+```
+yarn clean
+```
diff --git a/doc/development/query_recorder.md b/doc/development/query_recorder.md
index 26d3355e94d..2167ed57428 100644
--- a/doc/development/query_recorder.md
+++ b/doc/development/query_recorder.md
@@ -22,6 +22,20 @@ 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.
+## Cached queries
+
+By default, QueryRecorder will ignore cached queries in the count. However, it may be better to count
+all queries to avoid introducing an N+1 query that may be masked by the statement cache. To do this,
+pass the `skip_cached` variable to `QueryRecorder` and use the `exceed_all_query_limit` matcher:
+
+```
+it "avoids N+1 database queries" do
+ control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) { visit_some_page }.count
+ create_list(:issue, 5)
+ expect { visit_some_page }.not_to exceed_all_query_limit(control_count)
+end
+```
+
## Finding the source of the query
It may be useful to identify the source of the queries by looking at the call backtrace.
diff --git a/doc/development/rake_tasks.md b/doc/development/rake_tasks.md
index 31addcaf675..fc51b74da1d 100644
--- a/doc/development/rake_tasks.md
+++ b/doc/development/rake_tasks.md
@@ -176,3 +176,20 @@ git push -u origin update-project-templates
```
Now create a merge request and merge that to master.
+
+## Generate route lists
+
+To see the full list of API routes, you can run:
+
+```shell
+bundle exec rake grape:path_helpers
+```
+
+For the Rails controllers, run:
+
+```shell
+bundle exec rake routes
+```
+
+Since these take some time to create, it's often helpful to save the output to
+a file for quick reference.
diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md
index a76a5096b69..1a926a660f1 100644
--- a/doc/development/testing_guide/best_practices.md
+++ b/doc/development/testing_guide/best_practices.md
@@ -120,6 +120,10 @@ Add `screenshot_and_save_page` in a `:js` spec to screenshot what Capybara
Add `screenshot_and_open_image` in a `:js` spec to screenshot what Capybara
"sees", and automatically open the image.
+The HTML dumps created by this are missing CSS.
+This results in them looking very different from the actual application.
+There is a [small hack](https://gitlab.com/gitlab-org/gitlab-ce/snippets/1718469) to add CSS which makes debugging easier.
+
### Fast unit tests
Some classes are well-isolated from Rails and you should be able to test them
diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md
index 3b2b9c8c947..f8993653aec 100644
--- a/doc/development/testing_guide/frontend_testing.md
+++ b/doc/development/testing_guide/frontend_testing.md
@@ -172,6 +172,10 @@ object which can be treated like any other jasmine spy object.
Further documentation on the babel rewire pluign API can be found on
[its repository Readme doc](https://github.com/speedskater/babel-plugin-rewire#babel-plugin-rewire).
+#### Waiting in tests
+
+If you cannot avoid using [`setTimeout`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout) in tests, please use the [Jasmine mock clock](https://jasmine.github.io/api/2.9/Clock.html).
+
### Vue.js unit tests
See this [section][vue-test].
diff --git a/doc/development/utilities.md b/doc/development/utilities.md
index 8f9aff1a35f..0d074a3ef05 100644
--- a/doc/development/utilities.md
+++ b/doc/development/utilities.md
@@ -135,3 +135,44 @@ We developed a number of utilities to ease development.
Find.new.clear_memoization(:result)
```
+
+## [`RequestCache`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/cache/request_cache.rb)
+
+This module provides a simple way to cache values in RequestStore,
+and the cache key would be based on the class name, method name,
+optionally customized instance level values, optionally customized
+method level values, and optional method arguments.
+
+A simple example that only uses the instance level customised values:
+
+``` ruby
+class UserAccess
+ extend Gitlab::Cache::RequestCache
+
+ request_cache_key do
+ [user&.id, project&.id]
+ end
+
+ request_cache def can_push_to_branch?(ref)
+ # ...
+ end
+end
+```
+
+This way, the result of `can_push_to_branch?` would be cached in
+`RequestStore.store` based on the cache key. If `RequestStore` is not
+currently active, then it would be stored in a hash saved in an
+instance variable, so the cache logic would be the same.
+
+We can also set different strategies for different methods:
+
+``` ruby
+class Commit
+ extend Gitlab::Cache::RequestCache
+
+ def author
+ User.find_by_any_email(author_email.downcase)
+ end
+ request_cache(:author) { author_email.downcase }
+end
+```
diff --git a/doc/development/ux_guide/components.md b/doc/development/ux_guide/components.md
index b57520a00e0..4a3b3125f59 100644
--- a/doc/development/ux_guide/components.md
+++ b/doc/development/ux_guide/components.md
@@ -193,7 +193,7 @@ List with avatar, title and description using .content-list
![List with avatar](img/components-listwithavatar.png)
-List with hover effect .well-list
+List with hover effect .content-list
![List with hover effect](img/components-listwithhover.png)
diff --git a/doc/development/ux_guide/copy.md b/doc/development/ux_guide/copy.md
index 070efdc15b5..d5afa544372 100644
--- a/doc/development/ux_guide/copy.md
+++ b/doc/development/ux_guide/copy.md
@@ -192,7 +192,7 @@ Portions of this page are modifications based on work created and shared by the
[material design]: https://material.io/guidelines/
[features]: https://about.gitlab.com/features/ "GitLab features page"
-[products]: https://about.gitlab.com/products/ "GitLab products page"
+[products]: https://about.gitlab.com/pricing/ "GitLab products page"
[serial comma]: https://en.wikipedia.org/wiki/Serial_comma "“Serial comma†in Wikipedia"
[android project]: http://source.android.com/
[creative commons]: http://creativecommons.org/licenses/by/2.5/
diff --git a/doc/development/what_requires_downtime.md b/doc/development/what_requires_downtime.md
index b8be8daa157..b668c9de6a0 100644
--- a/doc/development/what_requires_downtime.md
+++ b/doc/development/what_requires_downtime.md
@@ -195,22 +195,22 @@ end
And that's it, we're done!
-## Changing Column Types For Large Tables
+## Changing The Schema For Large Tables
-While `change_column_type_concurrently` can be used for changing the type of a
-column without downtime it doesn't work very well for large tables. Because all
-of the work happens in sequence the migration can take a very long time to
-complete, preventing a deployment from proceeding.
-`change_column_type_concurrently` can also produce a lot of pressure on the
-database due to it rapidly updating many rows in sequence.
+While `change_column_type_concurrently` and `rename_column_concurrently` can be
+used for changing the schema of a table without downtime, it doesn't work very
+well for large tables. Because all of the work happens in sequence the migration
+can take a very long time to complete, preventing a deployment from proceeding.
+They can also produce a lot of pressure on the database due to it rapidly
+updating many rows in sequence.
To reduce database pressure you should instead use
-`change_column_type_using_background_migration` when migrating a column in a
-large table (e.g. `issues`). This method works similar to
-`change_column_type_concurrently` but uses background migration to spread the
-work / load over a longer time period, without slowing down deployments.
+`change_column_type_using_background_migration` or `rename_column_using_background_migration`
+when migrating a column in a large table (e.g. `issues`). These methods work
+similarly to the concurrent counterparts but uses background migration to spread
+the work / load over a longer time period, without slowing down deployments.
-Usage of this method is fairly simple:
+For example, to change the column type using a background migration:
```ruby
class ExampleMigration < ActiveRecord::Migration
@@ -252,6 +252,62 @@ Keep in mind that the relation passed to
`change_column_type_using_background_migration` _must_ include `EachBatch`,
otherwise it will raise a `TypeError`.
+This migration then needs to be followed in a separate release (_not_ a patch
+release) by a cleanup migration, which should steal from the queue and handle
+any remaining rows. For example:
+
+```ruby
+class MigrateRemainingIssuesClosedAt < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class Issue < ActiveRecord::Base
+ self.table_name = 'issues'
+ include EachBatch
+ end
+
+ def up
+ Gitlab::BackgroundMigration.steal('CopyColumn')
+ Gitlab::BackgroundMigration.steal('CleanupConcurrentTypeChange')
+
+ migrate_remaining_rows if migrate_column_type?
+ end
+
+ def down
+ # Previous migrations already revert the changes made here.
+ end
+
+ def migrate_remaining_rows
+ Issue.where('closed_at_for_type_change IS NULL AND closed_at IS NOT NULL').each_batch do |batch|
+ batch.update_all('closed_at_for_type_change = closed_at')
+ end
+
+ cleanup_concurrent_column_type_change(:issues, :closed_at)
+ end
+
+ def migrate_column_type?
+ # Some environments may have already executed the previous version of this
+ # migration, thus we don't need to migrate those environments again.
+ column_for('issues', 'closed_at').type == :datetime # rubocop:disable Migration/Datetime
+ end
+end
+```
+
+The same applies to `rename_column_using_background_migration`:
+
+1. Create a migration using the helper, which will schedule background
+ migrations to spread the writes over a longer period of time.
+2. In the next monthly release, create a clean-up migration to steal from the
+ Sidekiq queues, migrate any missing rows, and cleanup the rename. This
+ migration should skip the steps after stealing from the Sidekiq queues if the
+ column has already been renamed.
+
+For more information, see [the documentation on cleaning up background
+migrations](background_migrations.md#cleaning-up).
+
## Adding Indexes
Adding indexes is an expensive process that blocks INSERT and UPDATE queries for
diff --git a/doc/development/writing_documentation.md b/doc/development/writing_documentation.md
index 1c41fc7611f..038a4b1e6ea 100644
--- a/doc/development/writing_documentation.md
+++ b/doc/development/writing_documentation.md
@@ -1,558 +1,3 @@
---
-description: Learn how to contribute to GitLab Documentation.
+redirect_to: 'documentation/index.md'
---
-
-# GitLab Documentation guidelines
-
- - **General Documentation**: written by the [developers responsible by creating features](#contributing-to-docs). Should be submitted in the same merge request containing code. Feature proposals (by GitLab contributors) should also be accompanied by its respective documentation. They can be later improved by PMs and Technical Writers.
- - **[Technical Articles](#technical-articles)**: written by any [GitLab Team](https://about.gitlab.com/team/) member, GitLab contributors, or [Community Writers](https://about.gitlab.com/handbook/product/technical-writing/community-writers/).
- - **Indexes per topic**: initially prepared by the Technical Writing Team, and kept up-to-date by developers and PMs in the same merge request containing code. They gather all resources for that topic in a single page (user and admin documentation, articles, and third-party docs).
-
-## Contributing to docs
-
-Whenever a feature is changed, updated, introduced, or deprecated, the merge
-request introducing these changes must be accompanied by the documentation
-(either updating existing ones or creating new ones). This is also valid when
-changes are introduced to the UI.
-
-The one responsible for writing the first piece of documentation is the developer who
-wrote the code. It's the job of the Product Manager to ensure all features are
-shipped with its docs, whether is a small or big change. At the pace GitLab evolves,
-this is the only way to keep the docs up-to-date. If you have any questions about it,
-ask a Technical Writer. Otherwise, when your content is ready, assign one of
-them to review it for you.
-
-We use the [monthly release blog post](https://about.gitlab.com/handbook/marketing/blog/release-posts/#monthly-releases) as a changelog checklist to ensure everything
-is documented.
-
-Whenever you submit a merge request for the documentation, use the documentation MR description template.
-
-Please check the [documentation workflow](https://about.gitlab.com/handbook/product/technical-writing/workflow/) before getting started.
-
-## Documentation structure
-
-- Overview and use cases: what it is, why it is necessary, why one would use it
-- Requirements: what do we need to get started
-- Tutorial: how to set it up, how to use it
-
-Always link a new document from its topic-related index, otherwise, it will
-not be included it in the documentation site search.
-
-_Note: to be extended._
-
-### Feature overview and use cases
-
-Every major feature (regardless if present in GitLab Community or Enterprise editions)
-should present, at the beginning of the document, two main sections: **overview** and
-**use cases**. Every GitLab EE-only feature should also contain these sections.
-
-**Overview**: as the name suggests, the goal here is to provide an overview of the feature.
-Describe what is it, what it does, why it is important/cool/nice-to-have,
-what problem it solves, and what you can do with this feature that you couldn't
-do before.
-
-**Use cases**: provide at least two, ideally three, use cases for every major feature.
-You should answer this question: what can you do with this feature/change? Use cases
-are examples of how this feature or change can be used in real life.
-
-Examples:
-- CE and EE: [Issues](../user/project/issues/index.md#use-cases)
-- CE and EE: [Merge Requests](../user/project/merge_requests/index.md#overview)
-- EE-only: [Geo](https://docs.gitlab.com/ee/gitlab-geo/README.html#overview)
-- EE-only: [Jenkins integration](https://docs.gitlab.com/ee/integration/jenkins.md#overview)
-
-Note that if you don't have anything to add between the doc title (`<h1>`) and
-the header `## Overview`, you can omit the header, but keep the content of the
-overview there.
-
-> **Overview** and **use cases** are required to **every** Enterprise Edition feature,
-and for every **major** feature present in Community Edition.
-
-## Markdown and styles
-
-Currently GitLab docs use Redcarpet as [markdown](../user/markdown.md) engine, but there's an [open discussion](https://gitlab.com/gitlab-com/gitlab-docs/issues/50) for implementing Kramdown in the near future.
-
-All the docs follow the [documentation style guidelines](doc_styleguide.md).
-
-## Documentation directory structure
-
-The documentation is structured based on the GitLab UI structure itself,
-separated by [`user`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/user),
-[`administrator`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/administration), and [`contributor`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/development).
-
-In order to have a [solid site structure](https://searchengineland.com/seo-benefits-developing-solid-site-structure-277456) for our documentation,
-all docs should be linked. Every new document should be cross-linked to its related documentation, and linked from its topic-related index, when existent.
-
-The directories `/workflow/`, `/gitlab-basics/`, `/university/`, and `/articles/` have
-been deprecated and the majority their docs have been moved to their correct location
-in small iterations. Please don't create new docs in these folders.
-
-### Location and naming documents
-
-The documentation hierarchy can be vastly improved by providing a better layout
-and organization of directories.
-
-Having a structured document layout, we will be able to have meaningful URLs
-like `docs.gitlab.com/user/project/merge_requests/index.html`. With this pattern,
-you can immediately tell that you are navigating a user related documentation
-and is about the project and its merge requests.
-
-Do not create summaries of similar types of content (e.g. an index of all articles, videos, etc.),
-rather organize content by its subject (e.g. everything related to CI goes together)
-and cross-link between any related content.
-
-The table below shows what kind of documentation goes where.
-
-| Directory | What belongs here |
-| --------- | -------------- |
-| `doc/user/` | User related documentation. Anything that can be done within the GitLab UI goes here including `/admin`. |
-| `doc/administration/` | Documentation that requires the user to have access to the server where GitLab is installed. The admin settings that can be accessed via GitLab's interface go under `doc/user/admin_area/`. |
-| `doc/api/` | API related documentation. |
-| `doc/development/` | Documentation related to the development of GitLab. Any styleguides should go here. |
-| `doc/legal/` | Legal documents about contributing to GitLab. |
-| `doc/install/`| Probably the most visited directory, since `installation.md` is there. Ideally this should go under `doc/administration/`, but it's best to leave it as-is in order to avoid confusion (still debated though). |
-| `doc/update/` | Same with `doc/install/`. Should be under `administration/`, but this is a well known location, better leave as-is, at least for now. |
-| `doc/topics/` | Indexes per Topic (`doc/topics/topic-name/index.md`): all resources for that topic (user and admin documentation, articles, and third-party docs) |
-
----
-
-**General rules:**
-
-1. The correct naming and location of a new document, is a combination
- of the relative URL of the document in question and the GitLab Map design
- that is used for UX purposes ([source][graffle], [image][gitlab-map]).
-1. When creating a new document and it has more than one word in its name,
- make sure to use underscores instead of spaces or dashes (`-`). For example,
- a proper naming would be `import_projects_from_github.md`. The same rule
- applies to images.
-1. Start a new directory with an `index.md` file.
-1. There are four main directories, `user`, `administration`, `api` and `development`.
-1. The `doc/user/` directory has five main subdirectories: `project/`, `group/`,
- `profile/`, `dashboard/` and `admin_area/`.
- 1. `doc/user/project/` should contain all project related documentation.
- 1. `doc/user/group/` should contain all group related documentation.
- 1. `doc/user/profile/` should contain all profile related documentation.
- Every page you would navigate under `/profile` should have its own document,
- i.e. `account.md`, `applications.md`, `emails.md`, etc.
- 1. `doc/user/dashboard/` should contain all dashboard related documentation.
- 1. `doc/user/admin_area/` should contain all admin related documentation
- describing what can be achieved by accessing GitLab's admin interface
- (_not to be confused with `doc/administration` where server access is
- required_).
- 1. Every category under `/admin/application_settings` should have its
- own document located at `doc/user/admin_area/settings/`. For example,
- the **Visibility and Access Controls** category should have a document
- located at `doc/user/admin_area/settings/visibility_and_access_controls.md`.
-1. The `doc/topics/` directory holds topic-related technical content. Create
- `doc/topics/topic-name/subtopic-name/index.md` when subtopics become necessary.
- General user- and admin- related documentation, should be placed accordingly.
-
-If you are unsure where a document should live, you can ping `@axil` or `@marcia` in your
-merge request.
-
-### Changing document location
-
-Changing a document's location is not to be taken lightly. Remember that the
-documentation is available to all installations under `help/` and not only to
-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 replace all of its contents with a new line:
-
-```
-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/`.
-
----
-
-For example, if you were to move `doc/workflow/lfs/lfs_administration.md` to
-`doc/administration/lfs.md`, then the steps would be:
-
-1. Copy `doc/workflow/lfs/lfs_administration.md` to `doc/administration/lfs.md`
-1. Replace the contents of `doc/workflow/lfs/lfs_administration.md` with:
-
- ```
- This document was moved to [another location](../../administration/lfs.md).
- ```
-
-1. Find and replace any occurrences of the old location with the new one.
- A quick way to find them is to use `git grep`. First go to the root directory
- where you cloned the `gitlab-ce` repository and then do:
-
- ```
- git grep -n "workflow/lfs/lfs_administration"
- git grep -n "lfs/lfs_administration"
- ```
-
-NOTE: **Note:**
-If the document being moved has any Disqus comments on it, there are extra steps
-to follow documented just [below](#redirections-for-pages-with-disqus-comments).
-
-Things to note:
-
-- Since we also use inline documentation, except for the documentation itself,
- the document might also be referenced in the views of GitLab (`app/`) which will
- render when visiting `/help`, and sometimes in the testing suite (`spec/`).
-- The above `git grep` command will search recursively in the directory you run
- it in for `workflow/lfs/lfs_administration` and `lfs/lfs_administration`
- and will print the file and the line where this file is mentioned.
- You may ask why the two greps. Since we use relative paths to link to
- documentation, sometimes it might be useful to search a path deeper.
-- The `*.md` extension is not used when a document is linked to GitLab's
- built-in help page, that's why we omit it in `git grep`.
-- Use the checklist on the documentation MR description template.
-
-#### Alternative redirection method
-
-Alternatively to the method described above, you can simply replace the content
-of the old file with a frontmatter containing a redirect link:
-
-```yaml
----
-redirect_to: '../path/to/file/README.md'
----
-```
-
-It supports both full and relative URLs, e.g. `https://docs.gitlab.com/ee/path/to/file.html`, `../path/to/file.html`, `path/to/file.md`. Note that any `*.md` paths will be compiled to `*.html`.
-
-### Redirections for pages with Disqus comments
-
-If the documentation page being relocated already has any Disqus comments,
-we need to preserve the Disqus thread.
-
-Disqus uses an identifier per page, and for docs.gitlab.com, the page identifier
-is configured to be the page URL. Therefore, when we change the document location,
-we need to preserve the old URL as the same Disqus identifier.
-
-To do that, add to the frontmatter the variable `redirect_from`,
-using the old URL as value. For example, let's say I moved the document
-available under `https://docs.gitlab.com/my-old-location/README.html` to a new location,
-`https://docs.gitlab.com/my-new-location/index.html`.
-
-Into the **new document** frontmatter add the following:
-
-```yaml
----
-redirect_from: 'https://docs.gitlab.com/my-old-location/README.html'
----
-```
-
-Note: it is necessary to include the file name in the `redirect_from` URL,
-even if it's `index.html` or `README.html`.
-
-## Testing
-
-We treat documentation as code, thus have implemented some testing.
-Currently, the following tests are in place:
-
-1. `docs lint`: Check that all internal (relative) links work correctly and
- that all cURL examples in API docs use the full switches. It's recommended
- to [check locally](#previewing-locally) before pushing to GitLab by executing the command
- `bundle exec nanoc check internal_links` on your local
- [`gitlab-docs`](https://gitlab.com/gitlab-com/gitlab-docs) directory.
-1. [`ee_compat_check`](https://docs.gitlab.com/ee/development/automatic_ce_ee_merge.html#avoiding-ce-gt-ee-merge-conflicts-beforehand) (runs on CE only):
- When you submit a merge request to GitLab Community Edition (CE),
- there is this additional job that runs against Enterprise Edition (EE)
- and checks if your changes can apply cleanly to the EE codebase.
- If that job fails, read the instructions in the job log for what to do next.
- As CE is merged into EE once a day, it's important to avoid merge conflicts.
- Submitting an EE-equivalent merge request cherry-picking all commits from CE to EE is
- essential to avoid them.
-
-## Branch naming
-
-If your contribution contains **only** documentation changes, you can speed up
-the CI process by following some branch naming conventions. You have three
-choices:
-
-| Branch name | Valid example |
-| ----------- | ------------- |
-| Starting with `docs/` | `docs/update-api-issues` |
-| Starting with `docs-` | `docs-update-api-issues` |
-| Ending in `-docs` | `123-update-api-issues-docs` |
-
-If your branch name matches any of the above, it will run only the docs
-tests. If it doesn't, the whole test suite will run (including docs).
-
-## Merge requests for GitLab documentation
-
-Before getting started, make sure you read the introductory section
-"[contributing to docs](#contributing-to-docs)" above and the
-[tech writing workflow](https://about.gitlab.com/handbook/product/technical-writing/workflow/)
-for GitLab Team members.
-
-- Use the current [merge request description template](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab/merge_request_templates/Documentation.md)
-- Use the correct [branch name](#branch-naming)
-- Label the MR `Documentation`
-- Assign the correct milestone (see note below)
-
-
-NOTE: **Note:**
-If the release version you want to add the documentation to has already been
-frozen or released, use the label `Pick into X.Y` to get it merged into
-the correct release. Avoid picking into a past release as much as you can, as
-it increases the work of the release managers.
-
-### Cherry-picking from CE to EE
-
-As we have the `master` branch of CE merged into EE once a day, it's common to
-run into merge conflicts. To avoid them, we [test for merge conflicts against EE](#testing)
-with the `ee-compat-check` job, and use the following method of creating equivalent
-branches for CE and EE.
-
-Follow this [method for cherry-picking from CE to EE](automatic_ce_ee_merge.md#cherry-picking-from-ce-to-ee), with a few adjustments:
-
-- Create the [CE branch](#branch-naming) starting with `docs-`,
- e.g.: `git checkout -b docs-example`
-- Create the EE-equivalent branch ending with `-ee`, e.g.,
- `git checkout -b docs-example-ee`
-- Once all the jobs are passing in CE and EE, and you've addressed the
-feedback from your own team, assign the CE MR to a technical writer for review
-- When both MRs are ready, the EE merge request will be merged first, and the
-CE-equivalent will be merged next.
-- Note that the review will occur only in the CE MR, as the EE MR
-contains the same commits as the CE MR.
-- If you have a few more changes that apply to the EE-version only, you can submit
-a couple more commits to the EE branch, but ask the reviewer to review the EE merge request
-additionally to the CE MR. If there are many EE-only changes though, start a new MR
-to EE only.
-
-## Previewing the changes live
-
-To preview your changes to documentation locally, please follow
-this [development guide](https://gitlab.com/gitlab-com/gitlab-docs/blob/master/README.md#development).
-
-If you want to preview the doc changes of your merge request live, you can use
-the manual `review-docs-deploy` job in your merge request. You will need at
-least Master permissions to be able to run it and is currently enabled for the
-following projects:
-
-- https://gitlab.com/gitlab-org/gitlab-ce
-- https://gitlab.com/gitlab-org/gitlab-ee
-
-NOTE: **Note:**
-You will need to push a branch to those repositories, it doesn't work for forks.
-
-TIP: **Tip:**
-If your branch contains only documentation changes, you can use
-[special branch names](#branch-naming) to avoid long running pipelines.
-
-In the mini pipeline graph, you should see an `>>` icon. Clicking on it will
-reveal the `review-docs-deploy` job. Hit the play button for the job to start.
-
-![Manual trigger a docs build](img/manual_build_docs.png)
-
-This job will:
-
-1. Create a new branch in the [gitlab-docs](https://gitlab.com/gitlab-com/gitlab-docs)
- project named after the scheme: `preview-<branch-slug>`
-1. Trigger a cross project pipeline and build the docs site with your changes
-
-After a few minutes, the Review App will be deployed and you will be able to
-preview the changes. The docs URL can be found in two places:
-
-- In the merge request widget
-- In the output of the `review-docs-deploy` job, which also includes the
- triggered pipeline so that you can investigate whether something went wrong
-
-In case the Review App URL returns 404, follow these steps to debug:
-
-1. **Did you follow the URL from the merge request widget?** If yes, then check if
- the link is the same as the one in the job output. It can happen that if the
- branch name slug is longer than 35 characters, it is automatically
- truncated. That means that the merge request widget will not show the proper
- URL due to a limitation of how `environment: url` works, but you can find the
- real URL from the output of the `review-docs-deploy` job.
-1. **Did you follow the URL from the job output?** If yes, then it means that
- either the site is not yet deployed or something went wrong with the remote
- pipeline. Give it a few minutes and it should appear online, otherwise you
- can check the status of the remote pipeline from the link in the job output.
- If the pipeline failed or got stuck, drop a line in the `#docs` chat channel.
-
-TIP: **Tip:**
-Someone that has no merge rights to the CE/EE projects (think of forks from
-contributors) will not be able to run the manual job. In that case, you can
-ask someone from the GitLab team who has the permissions to do that for you.
-
-NOTE: **Note:**
-Make sure that you always delete the branch of the merge request you were
-working on. If you don't, the remote docs branch won't be removed either,
-and the server where the Review Apps are hosted will eventually be out of
-disk space.
-
-### Technical aspects
-
-If you want to know the hot details, here's what's really happening:
-
-1. You manually run the `review-docs-deploy` job in a CE/EE merge request.
-1. The job runs the [`scripts/trigger-build-docs`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/scripts/trigger-build-docs)
- script with the `deploy` flag, which in turn:
- 1. Takes your branch name and applies the following:
- - The slug of the branch name is used to avoid special characters since
- ultimately this will be used by NGINX.
- - The `preview-` prefix is added to avoid conflicts if there's a remote branch
- with the same name that you created in the merge request.
- - The final branch name is truncated to 42 characters to avoid filesystem
- limitations with long branch names (> 63 chars).
- 1. The remote branch is then created if it doesn't exist (meaning you can
- re-run the manual job as many times as you want and this step will be skipped).
- 1. A new cross-project pipeline is triggered in the docs project.
- 1. The preview URL is shown both at the job output and in the merge request
- widget. You also get the link to the remote pipeline.
-1. In the docs project, the pipeline is created and it
- [skips the test jobs](https://gitlab.com/gitlab-com/gitlab-docs/blob/8d5d5c750c602a835614b02f9db42ead1c4b2f5e/.gitlab-ci.yml#L50-55)
- to lower the build time.
-1. Once the docs site is built, the HTML files are uploaded as artifacts.
-1. A specific Runner tied only to the docs project, runs the Review App job
- that downloads the artifacts and uses `rsync` to transfer the files over
- to a location where NGINX serves them.
-
-The following GitLab features are used among others:
-
-- [Manual actions](../ci/yaml/README.md#manual-actions)
-- [Multi project pipelines](https://docs.gitlab.com/ee/ci/multi_project_pipeline_graphs.html)
-- [Review Apps](../ci/review_apps/index.md)
-- [Artifacts](../ci/yaml/README.md#artifacts)
-- [Specific Runner](../ci/runners/README.md#locking-a-specific-runner-from-being-enabled-for-other-projects)
-
-## GitLab `/help`
-
-Every GitLab instance includes the documentation, which is available from `/help`
-(`http://my-instance.com/help`), e.g., <https://gitlab.com/help>.
-
-When you're building a new feature, you may need to link the documentation
-from GitLab, the application. This is normally done in files inside the
-`app/views/` directory with the help of the `help_page_path` helper method.
-
-In its simplest form, the HAML code to generate a link to the `/help` page is:
-
-```haml
-= link_to 'Help page', help_page_path('user/permissions')
-```
-
-The `help_page_path` contains the path to the document you want to link to with
-the following conventions:
-
-- it is relative to the `doc/` directory in the GitLab repository
-- the `.md` extension must be omitted
-- it must not end with a slash (`/`)
-
-Below are some special cases where should be used depending on the context.
-You can combine one or more of the following:
-
-1. **Linking to an anchor link.** Use `anchor` as part of the `help_page_path`
- method:
-
- ```haml
- = link_to 'Help page', help_page_path('user/permissions', anchor: 'anchor-link')
- ```
-
-1. **Opening links in a new tab.** This should be the default behavior:
-
- ```haml
- = link_to 'Help page', help_page_path('user/permissions'), target: '_blank'
- ```
-
-1. **Linking to a circle icon.** Usually used in settings where a long
- description cannot be used, like near checkboxes. You can basically use
- any font awesome icon, but prefer the `question-circle`:
-
- ```haml
- = link_to icon('question-circle'), help_page_path('user/permissions')
- ```
-
-1. **Using a button link.** Useful in places where text would be out of context
- with the rest of the page layout:
-
- ```haml
- = link_to 'Help page', help_page_path('user/permissions'), class: 'btn btn-info'
- ```
-
-1. **Using links inline of some text.**
-
- ```haml
- Description to #{link_to 'Help page', help_page_path('user/permissions')}.
- ```
-
-1. **Adding a period at the end of the sentence.** Useful when you don't want
- the period to be part of the link:
-
- ```haml
- = succeed '.' do
- Learn more in the
- = link_to 'Help page', help_page_path('user/permissions')
- ```
-
-## General Documentation vs Technical Articles
-
-### General documentation
-
-General documentation is categorized by _User_, _Admin_, and _Contributor_, and describe what that feature is, what it does, and its available settings.
-
-### Technical Articles
-
-Technical articles replace technical content that once lived in the [GitLab Blog](https://about.gitlab.com/blog/), where they got out-of-date and weren't easily found.
-
-They are topic-related documentation, written with an user-friendly approach and language, aiming to provide the community with guidance on specific processes to achieve certain objectives.
-
-A technical article guides users and/or admins to achieve certain objectives (within guides and tutorials), or provide an overview of that particular topic or feature (within technical overviews). It can also describe the use, implementation, or integration of third-party tools with GitLab.
-
-They should be placed in a new directory named `/article-title/index.md` under a topic-related folder, and their images should be placed in `/article-title/img/`. For example, a new article on GitLab Pages should be placed in `doc/user/project/pages/article-title/` and a new article on GitLab CI/CD should be placed in `doc/ci/examples/article-title/`.
-
-#### Types of Technical Articles
-
-- **User guides**: technical content to guide regular users from point A to point B
-- **Admin guides**: technical content to guide administrators of GitLab instances from point A to point B
-- **Technical Overviews**: technical content describing features, solutions, and third-party integrations
-- **Tutorials**: technical content provided step-by-step on how to do things, or how to reach very specific objectives
-
-#### Understanding guides, tutorials, and technical overviews
-
-Suppose there's a process to go from point A to point B in 5 steps: `(A) 1 > 2 > 3 > 4 > 5 (B)`.
-
-A **guide** can be understood as a description of certain processes to achieve a particular objective. A guide brings you from A to B describing the characteristics of that process, but not necessarily going over each step. It can mention, for example, steps 2 and 3, but does not necessarily explain how to accomplish them.
-
-- Live example: "[Static sites and GitLab Pages domains (Part 1)](../user/project/pages/getting_started_part_one.md) to [Creating and Tweaking GitLab CI/CD for GitLab Pages (Part 4)](../user/project/pages/getting_started_part_four.md)"
-
-A **tutorial** requires a clear **step-by-step** guidance to achieve a singular objective. It brings you from A to B, describing precisely all the necessary steps involved in that process, showing each of the 5 steps to go from A to B.
-It does not only describes steps 2 and 3, but also shows you how to accomplish them.
-
-- Live example (on the blog): [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/)
-
-A **technical overview** is a description of what a certain feature is, and what it does, but does not walk
-through the process of how to use it systematically.
-
-- Live example (on the blog): [GitLab Workflow, an overview](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/)
-
-#### Special format
-
-Every **Technical Article** contains a frontmatter at the beginning of the doc
-with the following information:
-
-- **Type of article** (user guide, admin guide, technical overview, tutorial)
-- **Knowledge level** expected from the reader to be able to follow through (beginner, intermediate, advanced)
-- **Author's name** and **GitLab.com handle**
-- **Publication date** (ISO format YYYY-MM-DD)
-
-For example:
-
-
-```yaml
----
-author: John Doe
-author_gitlab: johnDoe
-level: beginner
-article_type: user guide
-date: 2017-02-01
----
-```
-
-#### Technical Articles - Writing Method
-
-Use the [writing method](https://about.gitlab.com/handbook/product/technical-writing/#writing-method) defined by the Technical Writing team.
-
-[gitlab-map]: https://gitlab.com/gitlab-org/gitlab-design/raw/master/production/resources/gitlab-map.png
-[graffle]: https://gitlab.com/gitlab-org/gitlab-design/blob/d8d39f4a87b90fb9ae89ca12dc565347b4900d5e/production/resources/gitlab-map.graffle
diff --git a/doc/downgrade_ee_to_ce/README.md b/doc/downgrade_ee_to_ce/README.md
index 236408762e3..a187b3cbb07 100644
--- a/doc/downgrade_ee_to_ce/README.md
+++ b/doc/downgrade_ee_to_ce/README.md
@@ -57,7 +57,7 @@ $ sudo gitlab-rails runner "Service.where(type: ['JenkinsService', 'JenkinsDepre
$ bundle exec rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService', 'GithubService']).delete_all" production
```
-### Secret variables environment scopes
+### Variables environment scopes
If you're using this feature and there are variables sharing the same
key, but they have different scopes in a project, then you might want to
diff --git a/doc/gitlab-basics/command-line-commands.md b/doc/gitlab-basics/command-line-commands.md
index c9766040234..4666511d747 100644
--- a/doc/gitlab-basics/command-line-commands.md
+++ b/doc/gitlab-basics/command-line-commands.md
@@ -7,17 +7,19 @@ In Git, when you copy a project you say you "clone" it. To work on a git project
When you are on your Dashboard, click on the project that you'd like to clone.
To work in the project, you can copy a link to the Git repository through a SSH
or a HTTPS protocol. SSH is easier to use after it's been
-[setup](create-your-ssh-keys.md). While you are at the **Project** tab, select
-HTTPS or SSH from the dropdown menu and copy the link using the 'Copy to clipboard'
+[set up](create-your-ssh-keys.md). While you are at the **Project** tab, select
+HTTPS or SSH from the dropdown menu and copy the link using the _Copy URL to clipboard_
button (you'll have to paste it on your shell in the next step).
![Copy the HTTPS or SSH](img/project_clone_url.png)
## On the command line
+This section has examples of some basic shell commands that you might find useful. For more information, search the web for _bash commands_.
+
### Clone your project
-Go to your computer's shell and type the following command:
+Go to your computer's shell and type the following command with your SSH or HTTPS URL:
```
git clone PASTE HTTPS OR SSH HERE
@@ -25,33 +27,45 @@ git clone PASTE HTTPS OR SSH HERE
A clone of the project will be created in your computer.
->**Note:** If you clone your project via an URL that contains special characters, make sure that they are URL-encoded.
+>**Note:** If you clone your project via a URL that contains special characters, make sure that characters are URL-encoded.
-### Go into a project, directory or file to work in it
+### Go into a project directory to work in it
```
-cd NAME-OF-PROJECT-OR-FILE
+cd NAME-OF-PROJECT
```
-### Go back one directory or file
+### Go back one directory
```
-cd ../
+cd ..
```
-### View what’s in the directory that you are in
+### List what’s in the current directory
```
ls
```
-### Create a directory
+### List what’s in the current directory that starts with `a`
+
+```
+ls a*
+```
+
+### List what’s in the current directory that ends with `.md`
+
+```
+ls *.md
+```
+
+### Create a new directory
```
mkdir NAME-OF-YOUR-DIRECTORY
```
-### Create a README.md or file in directory
+### Create a README.md file in the current directory
```
touch README.md
@@ -62,6 +76,12 @@ nano README.md
#### Press: enter
```
+### Show the contents of the README.md file
+
+```
+cat README.md
+```
+
### Remove a file
```
@@ -74,12 +94,18 @@ rm NAME-OF-FILE
rm -r NAME-OF-DIRECTORY
```
-### View history in the command line
+### View command history
```
history
```
+### Execute command 123 from history
+
+```
+!123
+```
+
### Carry out commands for which the account you are using lacks authority
You will be asked for an administrator’s password.
@@ -88,8 +114,14 @@ You will be asked for an administrator’s password.
sudo
```
-### Tell where you are
+### Show which directory I am in
```
pwd
```
+
+### Clear the shell window
+
+```
+clear
+```
diff --git a/doc/gitlab-basics/create-project.md b/doc/gitlab-basics/create-project.md
index 10e8059756d..dd8d95a3bca 100644
--- a/doc/gitlab-basics/create-project.md
+++ b/doc/gitlab-basics/create-project.md
@@ -30,6 +30,9 @@
idea to fill this in.
- Changing the **Visibility Level** modifies the project's
[viewing and access rights](../public_access/public_access.md) for users.
+ - Selecting the **Initialize repository with a README** option creates a
+ README so that the Git repository is initialized, has a default branch and
+ can be cloned.
1. Click **Create project**.
diff --git a/doc/gitlab-basics/img/create_new_project_info.png b/doc/gitlab-basics/img/create_new_project_info.png
index ce4f7d1204b..b4119dc046a 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/start-using-git.md b/doc/gitlab-basics/start-using-git.md
index 42cd8bb3e48..0d9994c9925 100644
--- a/doc/gitlab-basics/start-using-git.md
+++ b/doc/gitlab-basics/start-using-git.md
@@ -17,112 +17,197 @@ Depending on your operating system, you will need to use a shell of your prefere
Git is usually preinstalled on Mac and Linux.
Type the following command and then press enter:
-```
+
+```bash
git --version
```
-You should receive a message that will tell you which Git version you have on your computer. If you don’t receive a "Git version" message, it means that you need to [download Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git).
+You should receive a message that tells you which Git version you have on your computer. If you don’t receive a "Git version" message, it means that you need to [download Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git).
If Git doesn't automatically download, there's an option on the website to [download manually](https://git-scm.com/downloads). Then follow the steps on the installation window.
-After you are finished installing, open a new shell and type "git --version" again to verify that it was correctly installed.
+After you are finished installing Git, open a new shell and type `git --version` again to verify that it was correctly installed.
## Add your Git username and set your email
-It is important to configure your Git username and email address as every Git commit will use this information to identify you as the author.
+It is important to configure your Git username and email address, since every Git commit will use this information to identify you as the author.
On your shell, type the following command to add your username:
-```
+
+```bash
git config --global user.name "YOUR_USERNAME"
```
Then verify that you have the correct username:
-```
+
+```bash
git config --global user.name
```
To set your email address, type the following command:
-```
+
+```bash
git config --global user.email "your_email_address@example.com"
```
To verify that you entered your email correctly, type:
-```
+
+```bash
git config --global user.email
```
-You'll need to do this only once as you are using the `--global` option. It tells Git to always use this information for anything you do on that system. If you want to override this with a different username or email address for specific projects, you can run the command without the `--global` option when you’re in that project.
+You'll need to do this only once, since you are using the `--global` option. It tells Git to always use this information for anything you do on that system. If you want to override this with a different username or email address for specific projects, you can run the command without the `--global` option when you’re in that project.
## Check your information
-To view the information that you entered, type:
-```
+To view the information that you entered, along with other global options, type:
+
+```bash
git config --global --list
```
+
## Basic Git commands
### Go to the master branch to pull the latest changes from there
-```
+```bash
git checkout master
```
### Download the latest changes in the project
-This is for you to work on an up-to-date copy (it is important to do every time you work on a project), while you setup tracking branches.
+
+This is for you to work on an up-to-date copy (it is important to do this every time you start working on a project), while you set up tracking branches. You pull from remote repositories to get all the changes made by users since the last time you cloned or pulled the project. Later, you can push your local commits to the remote repositories.
+
+```bash
+git pull REMOTE NAME-OF-BRANCH
```
-git pull REMOTE NAME-OF-BRANCH -u
+
+When you first clone a repository, REMOTE is typically "origin". This is where the repository came from, and it indicates the SSH or HTTPS URL of the repository on the remote server. NAME-OF-BRANCH is usually "master", but it may be any existing branch.
+
+### View your remote repositories
+
+To view your remote repositories, type:
+
+```bash
+git remote -v
```
-(REMOTE: origin) (NAME-OF-BRANCH: could be "master" or an existing branch)
### Create a branch
-Spaces won't be recognized, so you will need to use a hyphen or underscore.
-```
+
+To create a branch, type the following (spaces won't be recognized in the branch name, so you will need to use a hyphen or underscore):
+
+```bash
git checkout -b NAME-OF-BRANCH
```
-### Work on a branch that has already been created
-```
+### Work on an existing branch
+
+To switch to an existing branch, so you can work on it:
+
+```bash
git checkout NAME-OF-BRANCH
```
### View the changes you've made
-It's important to be aware of what's happening and what's the status of your changes.
-```
+
+It's important to be aware of what's happening and the status of your changes. When you add, change, or delete files/folders, Git knows about it. To check the status of your changes:
+
+```bash
git status
```
-### Add changes to commit
-You'll see your changes in red when you type "git status".
+### View differences
+
+To view the differences between your local, unstaged changes and the repository versions that you cloned or pulled, type:
+
+```bash
+git diff
+```
+
+### Add and commit local changes
+
+You'll see your local changes in red when you type `git status`. These changes may be new, modified, or deleted files/folders. Use `git add` to stage a local file/folder for committing. Then use `git commit` to commit the staged files:
+
+```bash
+git add FILE OR FOLDER
+git commit -m "COMMENT TO DESCRIBE THE INTENTION OF THE COMMIT"
```
-git add CHANGES IN RED
-git commit -m "DESCRIBE THE INTENTION OF THE COMMIT"
+
+### Add all changes to commit
+
+To add and commit all local changes in one command:
+
+```bash
+git add .
+git commit -m "COMMENT TO DESCRIBE THE INTENTION OF THE COMMIT"
```
+NOTE: **Note:**
+The `.` character typically means _all_ in Git.
+
### Send changes to gitlab.com
-```
+
+To push all local commits to the remote repository:
+
+```bash
git push REMOTE NAME-OF-BRANCH
```
-### Delete all changes in the Git repository, but leave unstaged things
+For example, to push your local commits to the _master_ branch of the _origin_ remote:
+
+```bash
+git push origin master
```
+
+### Delete all changes in the Git repository
+
+To delete all local changes in the repository that have not been added to the staging area, and leave unstaged files/folders, type:
+
+```bash
git checkout .
```
-### Delete all changes in the Git repository, including untracked files
-```
+### Delete all untracked changes in the Git repository
+
+```bash
git clean -f
```
+### Unstage all changes that have been added to the staging area
+
+To undo the most recent add, but not committed, files/folders:
+
+```bash
+git reset .
+```
+
+### Undo most recent commit
+
+To undo the most recent commit, type:
+
+```bash
+git reset HEAD~1
+```
+
+This leaves the files and folders unstaged in your local repository.
+
+CAUTION: **Warning:**
+A Git commit is mostly irreversible, particularly if you already pushed it to the remote repository. Although you can undo a commit, the best option is to avoid the situation altogether.
+
### Merge created branch with master branch
+
You need to be in the created branch.
-```
+
+```bash
git checkout NAME-OF-BRANCH
git merge master
```
### Merge master branch with created branch
+
You need to be in the master branch.
-```
+
+```bash
git checkout master
git merge NAME-OF-BRANCH
```
diff --git a/doc/install/installation.md b/doc/install/installation.md
index a0ae9017f71..4b68090f8d3 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -12,9 +12,8 @@ Since installations from source don't have Runit, Sidekiq can't be terminated an
## Select Version to Install
-Make sure you view [this installation guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md) 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 in the top left corner of GitLab (below the menu bar).
+Make sure you view [this installation guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md) from the branch (version) of GitLab you would like to install (e.g., `11-1-stable`).
+You can select the branch in the version dropdown in 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/) for installation guide links by version.
@@ -133,9 +132,9 @@ Remove the old Ruby 1.8 if present:
Download Ruby and compile it:
mkdir /tmp/ruby && cd /tmp/ruby
- curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.7.tar.gz
- echo '540996fec64984ab6099e34d2f5820b14904f15a ruby-2.3.7.tar.gz' | shasum -c - && tar xzf ruby-2.3.7.tar.gz
- cd ruby-2.3.7
+ curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.4/ruby-2.4.4.tar.gz
+ echo 'ec82b0d53bd0adad9b19e6b45e44d54e9ec3f10c ruby-2.4.4.tar.gz' | shasum -c - && tar xzf ruby-2.4.4.tar.gz
+ cd ruby-2.4.4
./configure --disable-install-rdoc
make
@@ -154,12 +153,12 @@ page](https://golang.org/dl).
# 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
+
+ curl --remote-name --progress https://dl.google.com/go/go1.10.3.linux-amd64.tar.gz
+ echo 'fa1b0e45d3b647c252f51f5e1204aba049cde4af177ef9f2181f43004f901035 go1.10.3.linux-amd64.tar.gz' | shasum -a256 -c - && \
+ sudo tar -C /usr/local -xzf go1.10.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
+ rm go1.10.3.linux-amd64.tar.gz
## 4. Node
@@ -301,9 +300,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-8-stable gitlab
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 11-1-stable gitlab
-**Note:** You can change `10-8-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
+**Note:** You can change `11-1-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It
@@ -448,6 +447,15 @@ You can specify a different Git repository by providing it as an extra parameter
sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse,https://example.com/gitlab-workhorse.git]" RAILS_ENV=production
+### Install gitlab-pages
+
+GitLab-Pages uses [GNU Make](https://www.gnu.org/software/make/). This step is optional and only needed if you wish to host static sites from within GitLab. The following commands will install GitLab-Pages in `/home/git/gitlab-pages`. For additional setup steps, please consult the [administration guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/administration/pages/source.md) for your version of GitLab as the GitLab Pages daemon can be ran several different ways.
+
+ cd /home/git
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git
+ cd gitlab-pages
+ sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_PAGES_VERSION)
+ sudo -u git -H make
### Initialize Database and Activate Advanced Features
diff --git a/doc/install/kubernetes/gitlab_chart.md b/doc/install/kubernetes/gitlab_chart.md
index 429519a92e1..48f3df1925a 100644
--- a/doc/install/kubernetes/gitlab_chart.md
+++ b/doc/install/kubernetes/gitlab_chart.md
@@ -1,6 +1,137 @@
# GitLab Helm Chart
-> **Note:** This chart is currently in alpha.
+> **Note:** The chart is currently **beta**, if you encounter any problems please [open an issue](https://gitlab.com/charts/gitlab/issues/new).
-The cloud native `gitlab` chart is the next generation Helm chart, currently in alpha, and will replace the [`gitlab-omnibus`](gitlab_omnibus.md) chart. It will support large deployments with horizontal scaling of individual GitLab components.
+For more information on available GitLab Helm Charts, please see our [overview](index.md#chart-overview).
-Installation instructions and known issues during alpha are available at the [project page](https://gitlab.com/charts/gitlab/). \ No newline at end of file
+## Introduction
+
+The `gitlab` chart is the best way to operate GitLab on Kubernetes. This chart contains all the required components to get started, and can scale to large deployments.
+
+The default deployment includes:
+
+- Core GitLab components: Unicorn, Shell, Workhorse, Registry, Sidekiq, and Gitaly
+- Optional dependencies: Postgres, Redis, Minio
+- An auto-scaling, unprivileged [GitLab Runner](https://docs.gitlab.com/runner/) using the Kubernetes executor
+- Automatically provisioned SSL via [Let's Encrypt](https://letsencrypt.org/).
+
+### Limitations
+
+Some features and functions are not currently available in the beta release:
+* [GitLab Pages](../../user/project/pages/)
+* [Reply by email](../../administration/reply_by_email.html)
+* [Project templates](../../gitlab-basics/create-project.html)
+* [Project import/export](../../user/project/settings/import_export.html)
+* [Geo](https://docs.gitlab.com/ee/administration/geo/replication/)
+
+Currently out of scope:
+* [Mattermost](https://docs.gitlab.com/omnibus/gitlab-mattermost/)
+* [MySQL support](https://docs.gitlab.com/omnibus/settings/database.html#using-a-mysql-database-management-server-enterprise-edition-only)
+
+## Prerequisites
+
+In order to deploy GitLab on Kubernetes, a few prerequisites are required.
+
+1. `helm` and `kubectl` [installed on your computer](preparation/tools_installation.md).
+1. A Kubernetes cluster, version 1.8 or higher. 6vCPU and 16GB of RAM is recommended.
+ * [Google GKE](https://cloud.google.com/kubernetes-engine/docs/how-to/creating-a-container-cluster)
+ * [Amazon EKS](https://docs.aws.amazon.com/eks/latest/userguide/getting-started.html)
+ * [Microsoft AKS](https://docs.microsoft.com/en-us/azure/aks/kubernetes-walkthrough-portal)
+1. A [wildcard DNS entry and external IP address](preparation/networking.md)
+1. [Authenticate and connect](preparation/connect.md) to the cluster
+1. Configure and initialize [Helm Tiller](preparation/tiller.md).
+
+## Configuring and Installing GitLab
+
+> **Note**: For deployments to Amazon EKS, there are [additional configuration requirements](preparation/eks.md).
+
+For simple deployments, running all services within Kubernetes, only three parameters are required:
+- `global.hosts.domain`: the [base domain](preparation/networking.md) of the wildcard host entry. For example, `mycompany.io` if the wild card entry is `*.mycompany.io`.
+- `global.hosts.externalIP`: the [external IP](preparation/networking.md) which the wildcard DNS resolves to.
+- `certmanager-issuer.email`: Email address to use when requesting new SSL certificates from Let's Encrypt.
+
+For enterprise deployments, or to utilize advanced settings, please use the instructions in the [`gitlab` chart project](https://gitlab.com/charts/gitlab) for the most up to date directions.
+- [External Postgres, Redis, and other dependencies](https://gitlab.com/charts/gitlab/tree/master/doc/advanced)
+- [Persistence settings](https://gitlab.com/charts/gitlab/blob/master/doc/installation/storage.md)
+- [Manual TLS certificates](https://gitlab.com/charts/gitlab/blob/master/doc/installation/tls.md)
+- [Manual secret creation](https://gitlab.com/charts/gitlab/blob/master/doc/installation/secrets.md)
+
+For additional configuration options, consult the [full list of settings](https://gitlab.com/charts/gitlab/blob/master/doc/installation/command-line-options.md).
+
+## Installing GitLab using the Helm Chart
+
+Once you have all of your configuration options collected, we can get any dependencies and
+run helm. In this example, we've named our helm release "gitlab".
+
+```
+helm repo add gitlab https://charts.gitlab.io/
+helm update
+helm upgrade --install gitlab gitlab/gitlab \
+ --timeout 600 \
+ --set global.hosts.domain=example.local \
+ --set global.hosts.externalIP=10.10.10.10 \
+ --set certmanager-issuer.email=me@example.local
+```
+
+### Monitoring the Deployment
+
+This will output the list of resources installed once the deployment finishes which may take 5-10 minutes.
+
+The status of the deployment can be checked by running `helm status gitlab` which can also be done while
+the deployment is taking place if you run the command in another terminal.
+
+### Initial login
+
+You can access the GitLab instance by visiting the domain name beginning with `gitlab.` followed by the domain specified during installation. From the example above, the URL would be `https://gitlab.example.local`.
+
+If you manually created the secret for initial root password, you
+can use that to sign in as `root` user. If not, Gitlab automatically
+created a random password for `root` user. This can be extracted by the
+following command (replace `<name>` by name of the release - which is `gitlab`
+if you used the command above).
+
+```
+kubectl get secret <name>-gitlab-initial-root-password -ojsonpath={.data.password} | base64 --decode
+```
+
+## Outgoing email
+
+By default outgoing email is disabled. To enable it, provide details for your SMTP server
+using the `global.smtp` and `global.email` settings. You can find details for these settings in the
+[command line options](https://gitlab.com/charts/gitlab/blob/master/doc/installation/command-line-options.md#email-configuration).
+
+If your SMTP server requires authentication make sure to read the section on providing
+your password in the [secrets documentation](https://gitlab.com/charts/gitlab/blob/master/doc/installation/secrets.md#smtp-password).
+You can disable authentication settings with `--set global.smtp.authentication=""`.
+
+If your Kubernetes cluster is on GKE, be aware that smtp [ports 25, 465, and 587
+are blocked](https://cloud.google.com/compute/docs/tutorials/sending-mail/#using_standard_email_ports).
+
+## Deploying the Community Edition
+
+To deploy the Community Edition, include these options in your `helm install` command:
+
+```shell
+--set gitlab.migrations.image.repository=registry.gitlab.com/gitlab-org/build/cng/gitlab-rails-ce
+--set gitlab.sidekiq.image.repository=registry.gitlab.com/gitlab-org/build/cng/gitlab-sidekiq-ce
+--set gitlab.unicorn.image.repository=registry.gitlab.com/gitlab-org/build/cng/gitlab-unicorn-ce
+```
+
+## Updating GitLab using the Helm Chart
+
+Once your GitLab Chart is installed, configuration changes and chart updates
+should be done using `helm upgrade`:
+
+```bash
+helm upgrade -f values.yaml gitlab gitlab/gitlab
+```
+
+## Uninstalling GitLab using the Helm Chart
+
+To uninstall the GitLab Chart, run the following:
+
+```bash
+helm delete gitlab
+```
+
+[kube-srv]: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services---service-types
+[storageclass]: https://kubernetes.io/docs/concepts/storage/persistent-volumes/#storageclasses
diff --git a/doc/install/kubernetes/gitlab_omnibus.md b/doc/install/kubernetes/gitlab_omnibus.md
index 98af87455ec..9aee6b9dc74 100644
--- a/doc/install/kubernetes/gitlab_omnibus.md
+++ b/doc/install/kubernetes/gitlab_omnibus.md
@@ -71,7 +71,7 @@ For most installations, only two parameters are required:
Other common configuration options:
- `baseIP`: the desired [external IP address](#external-ip-recommended)
-- `gitlab`: Choose the [desired edition](https://about.gitlab.com/products), either `ee` or `ce`. `ce` is the default.
+- `gitlab`: Choose the [desired edition](https://about.gitlab.com/pricing), either `ee` or `ce`. `ce` is the default.
- `gitlabEELicense`: For Enterprise Edition, the [license](https://docs.gitlab.com/ee/user/admin_area/license.html) can be installed directly via the Chart
- `provider`: Optimizes the deployment for a cloud provider. The default is `gke` for [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/), with `acs` also supported for the [Azure Container Service](https://azure.microsoft.com/en-us/services/container-service/).
@@ -120,7 +120,7 @@ gitlabConfigStorageSize: 1Gi
Ingress routing and SSL are automatically configured within this Chart. An NGINX ingress is provisioned and configured, and will route traffic to any service. SSL certificates are automatically created and configured by [kube-lego](https://github.com/kubernetes/charts/tree/master/stable/kube-lego).
> **Note:**
-Let's Encrypt limits a single TLD to five certificate requests within a single week. This means that common DNS wildcard services like [xip.io](http://xip.io) and [nip.io](http://nip.io) are unlikely to work.
+Let's Encrypt limits a single TLD to five certificate requests within a single week. This means that common DNS wildcard services like [nip.io](http://nip.io) and [nip.io](http://nip.io) are unlikely to work.
## Installing GitLab using the Helm Chart
> **Note:**
@@ -144,7 +144,7 @@ helm install --name gitlab -f values.yaml gitlab/gitlab-omnibus
or passing them on the command line:
```bash
-helm install --name gitlab --set baseDomain=gitlab.io,baseIP=1.1.1.1,gitlab=ee,gitlabEELicense=$LICENSE,legoEmail=email@gitlab.com gitlab/gitlab-omnibus
+helm install --name gitlab --set baseDomain=gitlab.io,baseIP=192.0.2.1,gitlab=ee,gitlabEELicense=$LICENSE,legoEmail=email@gitlab.com gitlab/gitlab-omnibus
```
## Updating GitLab using the Helm Chart
diff --git a/doc/install/kubernetes/index.md b/doc/install/kubernetes/index.md
index aeaa739edab..6419a9dcb69 100644
--- a/doc/install/kubernetes/index.md
+++ b/doc/install/kubernetes/index.md
@@ -4,7 +4,7 @@ description: 'Read through the different methods to deploy GitLab on Kubernetes.
# Installing GitLab on Kubernetes
-> **Note**: These charts have been tested on Google Kubernetes 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).
+> **Note**: These charts have been tested on Google Kubernetes Engine. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/issues).
The easiest method to deploy GitLab on [Kubernetes](https://kubernetes.io/) is
to take advantage of GitLab's Helm charts. [Helm] is a package
@@ -14,51 +14,44 @@ should be deployed, upgraded, and configured.
## Chart Overview
-* **[GitLab-Omnibus](gitlab_omnibus.md)**: The best way to run GitLab on Kubernetes today, suited for small 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/gitlab/blob/master/README.md)**: The next generation GitLab chart, currently in alpha. Will support large deployments with horizontal scaling of individual GitLab components.
+* **[GitLab Chart](gitlab_chart.html)**: The recommended GitLab chart, currently in beta. Supports large deployments with horizontal scaling of individual GitLab components, and does not require NFS.
+* **[GitLab Runner Chart](gitlab_runner_chart.md)**: For deploying just the GitLab Runner.
* Other Charts
- * [GitLab Runner Chart](gitlab_runner_chart.md): For deploying just the GitLab Runner.
+ * [GitLab-Omnibus](gitlab_omnibus.md): Chart based on the Omnibus GitLab linux package, only suitable for small deployments. The chart will be deprecated by the [GitLab chart](#gitlab-chart) when it is GA.
* [Community Contributed Charts](#community-contributed-charts): Community contributed charts, deprecated by the official GitLab chart.
-## GitLab-Omnibus Chart (Recommended)
+## GitLab Chart
-> **Note**: This chart is in beta while [additional features](https://gitlab.com/charts/charts.gitlab.io/issues/68) are being added.
+> **Note**: This chart is **beta**, while we work on the [remaining items for GA](https://gitlab.com/groups/charts/-/epics/15).
-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).
+The best way to operate GitLab on Kubernetes. This chart contains all the required components to get started, and can scale to large deployments.
-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).
-
-## Cloud Native GitLab Chart
-
-GitLab is working towards building a [cloud native GitLab chart](https://gitlab.com/charts/gitlab/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,
-* Smaller, more efficient images,
-* Potential for rolling updates and canaries within a service,
+This chart offers a number of benefits:
+* Horizontal scaling of individual components
+* No requirement for shared storage to scale
+* Containers do not need `root` permissions
+* Automatic SSL with Let's Encrypt
* and plenty more.
-Presently this chart is available in alpha for testing, and not recommended for production use.
+Learn more about the [GitLab chart here](gitlab_chart.md) and [here [Video]](https://youtu.be/Z6jWR8Z8dv8).
-Learn more about the [cloud native GitLab chart here ](https://gitlab.com/charts/gitlab/blob/master/README.md) and [here [Video]](https://youtu.be/Z6jWR8Z8dv8).
+## GitLab Runner Chart
-## Other Charts
+If you already have a GitLab instance running, inside or outside of Kubernetes, and you'd like to leverage the Runner's [Kubernetes capabilities](https://docs.gitlab.com/runner/executors/kubernetes.html), it can be deployed with the GitLab Runner chart.
-### GitLab Runner Chart
+Learn more about [gitlab-runner chart](gitlab_runner_chart.md).
-If you already have a GitLab instance running, inside or outside of Kubernetes, and you'd like to leverage the Runner's [Kubernetes capabilities](https://docs.gitlab.com/runner/executors/kubernetes.html), it can be deployed with the GitLab Runner chart.
+## Other Charts
-Learn more about [gitlab-runner chart.](gitlab_runner_chart.md)
+### GitLab-Omnibus Chart
-### Advanced GitLab Installation
+> **Note**: This chart is beta, and **will be deprecated** when the [`gitlab`](#gitlab-chart) chart is GA.
-If advanced configuration of GitLab is required, the beta [gitlab](gitlab_chart.md) chart can be used which deploys the core GitLab service along with optional Postgres and Redis. It offers extensive configuration, but offers limited functionality out-of-the-box; it's lacking Pages support, the container registry, and Mattermost. It requires deep knowledge of Kubernetes and Helm to use.
+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).
-This chart will be deprecated and replaced by the [gitlab-omnibus](gitlab_omnibus.md) chart, once it supports [additional configuration options](https://gitlab.com/charts/charts.gitlab.io/issues/68). It's beta quality, and since it is not actively under development, it will never be GA.
+Once the [GitLab chart](#gitlab-chart) is GA, this chart will be deprecated. Migrating to the `gitlab` chart will require exporting data out of this instance and importing it into a new deployment.
-Learn more about the [gitlab chart.](gitlab_chart.md)
+Learn more about the [gitlab-omnibus chart](gitlab_omnibus.md).
### Community Contributed Charts
diff --git a/doc/install/kubernetes/preparation/connect.md b/doc/install/kubernetes/preparation/connect.md
new file mode 100644
index 00000000000..fb633c456f5
--- /dev/null
+++ b/doc/install/kubernetes/preparation/connect.md
@@ -0,0 +1,31 @@
+# Connecting your computer to a cluster
+
+In order to deploy software and settings to a cluster, you must connect and authenticate to it.
+
+* [GKE cluster](#connect-to-gke-cluster)
+* [EKS cluster](#connect-to-eks-cluster)
+* [Local minikube cluster](#connect-to-local-minikube-cluster)
+
+## Connect to GKE cluster
+
+The command for connection to the cluster can be obtained from the [Google Cloud Platform Console](https://console.cloud.google.com/kubernetes/list) by the individual cluster.
+
+Look for the **Connect** button in the clusters list page.
+
+**Or**
+
+Use the command below, filling in your cluster's informtion:
+
+```
+gcloud container clusters get-credentials <cluster-name> --zone <zone> --project <project-id>
+```
+
+## Connect to EKS cluster
+
+For the most up to date instructions, follow the Amazon EKS documentation on [connecting to a cluster](https://docs.aws.amazon.com/eks/latest/userguide/getting-started.html#eks-configure-kubectl).
+
+## Connect to local minikube cluster
+
+If you are doing local development, you can use `minikube` as your
+local cluster. If `kubectl cluster-info` is not showing `minikube` as the current
+cluster, use `kubectl config set-cluster minikube` to set the active cluster.
diff --git a/doc/install/kubernetes/preparation/eks.md b/doc/install/kubernetes/preparation/eks.md
new file mode 100644
index 00000000000..c40177c4302
--- /dev/null
+++ b/doc/install/kubernetes/preparation/eks.md
@@ -0,0 +1,44 @@
+# Running GitLab on EKS
+
+There are a few nuances to Amazon EKS which are important to be aware of, when deploying GitLab.
+
+## Persistent volume management
+
+There are two methods to manage volume claims on Kubernetes:
+1. Manually creating each persistent volume (recommended on EKS)
+1. Utilizing dynamic provisioning to automatically create the persistent volumes
+
+### Manual provisioning of volumes (Recommended)
+
+Manually creating the volumes allows you to control the zone of each volume, as well as all other details supported by the underlying storage.
+
+Follow our documentation on [manually creating persistent volumes](https://gitlab.com/charts/gitlab/blob/master/doc/installation/storage.md#manually-creating-static-volumes).
+
+### Dynamic provisioning of volumes
+
+Dynamic provisioning utilizes a Kubernetes provisioner, like `aws-ebs`, to automatically create persistent volumes to fulfill each claim.
+
+With EKS, there are a few important details to keep in mind:
+
+1. Clusters are required to span multiple AZ's
+1. Kubernetes volume provisioners create volumes across zones without regard to which pod they belong to. This leads to scenarios where a pod with multiple volumes being unable to start due to the volumes being in different zones.
+1. There is no default Storage Class.
+
+The easiest way to solve this and still utilize dynamic provisioning is to utilize, or create, a Storage Class that is locked to a specific zone.
+
+> **Note**: Restricting volumes to specific zone will cause GitLab and any other application using this Storage Class to only reside in that zone. For multiple zone support, utilize [manually provisioned volumes](#manual-provisioning-of-volumes).
+
+To create the storage class, download and edit Amazon EKS's [sample Storage Class](https://docs.aws.amazon.com/eks/latest/userguide/storage-classes.html) and add the following parameter:
+
+```yaml
+parameters:
+ zone: <desired-zone>
+```
+
+Then [specify the Storage Class](https://gitlab.com/charts/gitlab/blob/master/doc/installation/storage.md#using-a-custom-storage-class) name when deploying GitLab.
+
+## External access to GitLab
+
+By default, GitLab will an deploy an ingress which will create an associated Elastic Load Balancer. Since the DNS names of ELB's cannot be known ahead of time, it is difficult to utilize Let's Encrypt to automatically provision HTTPS certificates.
+
+We recommend [using your own certificates](https://gitlab.com/charts/gitlab/blob/master/doc/installation/tls.md#option-2-use-your-own-wildcard-certificate), and then mapping your desired DNS name to the created ELB using a CNAME record.
diff --git a/doc/install/kubernetes/preparation/networking.md b/doc/install/kubernetes/preparation/networking.md
new file mode 100644
index 00000000000..b157cf31aa9
--- /dev/null
+++ b/doc/install/kubernetes/preparation/networking.md
@@ -0,0 +1,36 @@
+# Networking Prerequisites
+
+> **Note**: Amazon EKS utilizes Elastic Load Balancers, which are addressed by DNS name and cannot be known ahead of time. Skip this section.
+
+The `gitlab` chart configures a GitLab server and Kubernetes cluster which can support dynamic [Review Apps](https://docs.gitlab.com/ee/ci/review_apps/index.html), as well as services like the integrated [Container Registry](https://docs.gitlab.com/ee/user/project/container_registry.html).
+
+To support the GitLab services and dynamic environments, a wildcard DNS entry is required which resolves to the external IP.
+
+## External IP
+
+To provision an external IP on GCP and Azure, simply request a new address from the Networking section. Ensure that the region matches the region your container cluster is created in. Note, it is important that the IP is not assigned at this point in time. It will be automatically assigned once the Helm chart is installed, to the Load Balancer.
+
+Set `global.hosts.externalIP` to this IP address when [deploying GitLab](../gitlab_chart.md#configuring-and-installing-gitlab).
+
+Then, create a [wildcard DNS record](#wildcard-dns-entry) which resolves to this IP address.
+
+### Creating an external IP on GCP
+
+When creating the external IP, it is critical to create it in the same region as your cluster. Otherwise, the IP address will fail to bind to the Load Balancer.
+
+1. Open the [web console](https://console.cloud.google.com)
+1. In the sidebar, browse to `VPC Network > External IP addresses`
+1. Click `Reserve static address`
+1. Choose `Regional` and select the region of your cluster
+1. Leave `Attached to` blank, as it will be automatically assigned during deployment
+
+## Wildcard DNS entry
+
+Now that an external IP address has been allocated, ensure that the wildcard DNS entry you would like to use resolves to this IP. Typically this would be an `A record` for `*`, resolving to the external IP above.
+
+Please consult the documentation for your DNS service for more information on creating DNS records:
+
+* [Google Domains](https://support.google.com/domains/answer/3290350?hl=en)
+* [GoDaddy](https://www.godaddy.com/help/add-an-a-record-19238)
+
+Set `global.hosts.domain` to this DNS name when [deploying GitLab](../gitlab_chart.md#configuring-and-installing-gitlab).
diff --git a/doc/install/kubernetes/preparation/rbac.md b/doc/install/kubernetes/preparation/rbac.md
new file mode 100644
index 00000000000..240893526d3
--- /dev/null
+++ b/doc/install/kubernetes/preparation/rbac.md
@@ -0,0 +1,16 @@
+# Role Based Access Control
+
+Until Kubernetes 1.7, there were no permissions within a cluster. With the launch of 1.7, there is now a role based access control system ([RBAC](https://kubernetes.io/docs/admin/authorization/rbac/)) which determines what services can perform actions within a cluster.
+
+RBAC affects a few different aspects of GitLab:
+* [Installation of GitLab using Helm](tiller.md#preparing-for-helm-with-rbac)
+* Prometheus monitoring
+* GitLab Runner
+
+## Checking that RBAC is enabled
+
+Try listing the current cluster roles, if it fails then `RBAC` is disabled
+
+This command will output `false` if `RBAC` is disabled and `true` otherwise
+
+`kubectl get clusterroles > /dev/null 2>&1 && echo true || echo false`
diff --git a/doc/install/kubernetes/preparation/tiller.md b/doc/install/kubernetes/preparation/tiller.md
new file mode 100644
index 00000000000..c92f8258e41
--- /dev/null
+++ b/doc/install/kubernetes/preparation/tiller.md
@@ -0,0 +1,94 @@
+# Configuring and initializing Helm Tiller
+
+To make use of Helm, you must have a [Kubernetes][k8s-io] cluster. Ensure you can access your cluster using `kubectl`.
+
+Helm consists of two parts, the `helm` client and a `tiller` server inside Kubernetes.
+
+> **Note**: If you are not able to run tiller in your cluster, for example on OpenShift, it is possible to use [tiller locally](#local-tiller) and avoid deploying it into the cluster. This should only be used when Tiller cannot be normally deployed.
+
+## Initialize Helm and Tiller
+
+Tiller is deployed into the cluster and interacts with the Kubernetes API to deploy your applications. If role based access control (RBAC) is enabled, Tiller will need to be [granted permissions](#preparing-for-helm-with-rbac) to allow it to talk to the Kubernetes API.
+
+If RBAC is not enabled, skip to [initalizing Helm](#initialize-helm).
+
+If you are not sure whether RBAC is enabled in your cluster, or to learn more, read through our [RBAC documentation](rbac.md).
+
+## Preparing for Helm with RBAC
+
+Helm's Tiller will need to be granted permissions to perform operations. These instructions grant cluster wide permissions, however for more advanced deployments [permissions can be restricted to a single namespace](https://docs.helm.sh/using_helm/#example-deploy-tiller-in-a-namespace-restricted-to-deploying-resources-only-in-that-namespace). To grant access to the cluster, we will create a new `tiller` service account and bind it to the `cluster-admin` role.
+
+Create a file `rbac-config.yaml` with the following contents:
+
+```yaml
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: tiller
+ namespace: kube-system
+---
+apiVersion: rbac.authorization.k8s.io/v1beta1
+kind: ClusterRoleBinding
+metadata:
+ name: tiller
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: cluster-admin
+subjects:
+ - kind: ServiceAccount
+ name: tiller
+ namespace: kube-system
+```
+
+Next we need to connect to the cluster and upload the RBAC config.
+
+### Upload the RBAC config
+
+Some clusters require authentication to use `kubectl` to create the Tiller roles.
+
+#### Upload the RBAC config as an admin user (GKE)
+
+For GKE, you need to grab the admin credentials:
+
+```
+gcloud container clusters describe <cluster-name> --zone <zone> --project <project-id> --format='value(masterAuth.password)'
+```
+
+This command will output the admin password. We need the password to authenticate with `kubectl` and create the role.
+
+```
+kubectl --username=admin --password=xxxxxxxxxxxxxx create -f rbac-config.yaml
+```
+
+#### Upload the RBAC config (Other clusters)
+
+For other clusters like Amazon EKS, you can directly upload the RBAC configuration.
+
+kubectl create -f rbac-config.yaml
+
+## Initialize Helm
+
+Deploy Helm Tiller with a service account
+
+```
+helm init --service-account tiller
+```
+
+If your cluster
+previously had Helm/Tiller installed, run the following to ensure that the deployed version of Tiller matches the local Helm version:
+
+```
+helm init --upgrade --service-account tiller
+```
+
+### Patching Helm Tiller for EKS
+
+Helm Tiller requires a flag to be enabled to work properly on EKS:
+
+`kubectl -n kube-system patch deployment tiller-deploy -p '{"spec": {"template": {"spec": {"automountServiceAccountToken": true}}}}'`
+
+[helm]: https://helm.sh
+[helm-using]: https://docs.helm.sh/using_helm
+[k8s-io]: https://kubernetes.io/
+[gcp-k8s]: https://console.cloud.google.com/kubernetes/list
diff --git a/doc/install/kubernetes/preparation/tools_installation.md b/doc/install/kubernetes/preparation/tools_installation.md
new file mode 100644
index 00000000000..210bc2f9e58
--- /dev/null
+++ b/doc/install/kubernetes/preparation/tools_installation.md
@@ -0,0 +1,19 @@
+# Installing kubectl and Helm on your computer
+
+In order to work with the GitLab Helm charts, `kubectl` and `helm` must be installed and configured on your computer.
+
+## Installing `kubectl`
+
+`kubectl` is the Kubernetes command line tool, which can be used to deploy settings to the cluster.
+
+Follow the [official documentation](https://kubernetes.io/docs/tasks/tools/install-kubectl/) for the most up to date instructions.
+
+## Installing `helm`
+
+Helm is a package management tool for Kubernetes, and is used to deploy charts.
+
+You can get Helm from the project's [releases page](https://github.com/kubernetes/helm/releases), or follow other options under the official documentation of [Installing Helm](https://docs.helm.sh/using_helm/#installing-helm).
+
+# Next steps
+
+Once installed, proceed to the next [installation step](../gitlab_chart.md#prerequisites).
diff --git a/doc/install/openshift_and_gitlab/index.md b/doc/install/openshift_and_gitlab/index.md
index 1ced1fb513d..60e1e2b5f8a 100644
--- a/doc/install/openshift_and_gitlab/index.md
+++ b/doc/install/openshift_and_gitlab/index.md
@@ -307,10 +307,10 @@ hostname** and use greater values for the volume sizes. If you don't provide a
password for PostgreSQL, it will be created automatically.
>**Note:**
-The `gitlab.apps.10.2.2.2.xip.io` hostname that is used by default will
+The `gitlab.apps.10.2.2.2.nip.io` hostname that is used by default will
resolve to the host with IP `10.2.2.2` which is the IP our VM uses. It is a
trick to have distinct FQDNs pointing to services that are on our local network.
-Read more on how this works in <http://xip.io>.
+Read more on how this works in <http://nip.io>.
Now that we configured this, let's see how to manage and scale GitLab.
@@ -347,7 +347,7 @@ Navigate back to the **Overview** and hopefully all pods will be up and running.
![GitLab running](img/gitlab-running.png)
Congratulations! You can now navigate to your new shinny GitLab instance by
-visiting <http://gitlab.apps.10.2.2.2.xip.io> where you will be asked to
+visiting <http://gitlab.apps.10.2.2.2.nip.io> where you will be asked to
change the root user password. Login using `root` as username and providing the
password you just set, and start using GitLab!
@@ -521,4 +521,4 @@ PaaS and managing your applications with the ease of containers.
[autoscaling]: https://docs.openshift.org/latest/dev_guide/pod_autoscaling.html "Documentation - Autoscale"
[basic-cli]: https://docs.openshift.org/latest/cli_reference/basic_cli_operations.html "Documentation - Basic CLI operations"
[openshift-docs]: https://docs.openshift.org "OpenShift documentation"
-[scc]: https://docs.openshift.org/latest/admin_guide/manage_scc.html "Documentation - Managing Security Context Constraints" \ No newline at end of file
+[scc]: https://docs.openshift.org/latest/admin_guide/manage_scc.html "Documentation - Managing Security Context Constraints"
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index 1f2b4d9d3d9..5531dcde4e9 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -27,8 +27,8 @@ Please see the [installation from source guide](installation.md) and the [instal
### Non-Unix operating systems such as Windows
-GitLab is developed for Unix operating systems.
-GitLab does **not** run on Windows and we have no plans of supporting it in the near future.
+GitLab is developed for Unix operating systems.
+It does **not** run on Windows, and we have no plans to support it in the near future. For the latest development status view this [issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/46567).
Please consider using a virtual machine to run GitLab.
## Ruby versions
@@ -64,16 +64,14 @@ If you have enough RAM memory and a recent CPU the speed of GitLab is mainly lim
### Memory
-You need at least 4GB of addressable memory (RAM + swap) to install and use GitLab!
+You need at least 8GB of addressable memory (RAM + swap) to install and use GitLab!
The operating system and any other running applications will also be using memory
so keep in mind that you need at least 4GB available before running GitLab. With
less memory GitLab will give strange errors during the reconfigure run and 500
errors during usage.
-- 1GB RAM + 3GB of swap is the absolute minimum but we strongly **advise against** this amount of memory. See the [unicorn worker section below](#unicorn-workers) for more advice.
-- 2GB RAM + 2GB swap supports up to 100 users but it will be very slow
-- **4GB RAM** is the **recommended** memory size for all installations and supports up to 100 users
-- 8GB RAM supports up to 1,000 users
+- 4GB RAM + 4GB swap supports up to 100 users but it will be very slow
+- **8GB RAM** is the **recommended** memory size for all installations and supports up to 100 users
- 16GB RAM supports up to 2,000 users
- 32GB RAM supports up to 4,000 users
- 64GB RAM supports up to 8,000 users
diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md
index 2a14c0397ca..2afcb052536 100644
--- a/doc/integration/bitbucket.md
+++ b/doc/integration/bitbucket.md
@@ -1,5 +1,8 @@
# Integrate your GitLab server with Bitbucket
+NOTE: **Note:**
+You need to [enable OmniAuth](omniauth.md) in order to use this.
+
Import projects from Bitbucket.org and login to your GitLab instance with your
Bitbucket.org account.
@@ -19,8 +22,8 @@ Bitbucket.org.
> **Note:**
GitLab 8.15 significantly simplified the way to integrate Bitbucket.org with
-GitLab. You are encouraged to upgrade your GitLab instance if you haven't done
-already. If you're using GitLab 8.14 and below, [use the previous integration
+GitLab. You are encouraged to upgrade your GitLab instance if you haven't done so
+already. If you're using GitLab 8.14 or below, [use the previous integration
docs][bb-old].
To enable the Bitbucket OmniAuth provider you must register your application
@@ -61,7 +64,7 @@ you to use.
1. Select **Save**.
1. Select your newly created OAuth consumer and you should now see a Key and
- Secret in the list of OAuth customers. Keep this page open as you continue
+ Secret in the list of OAuth consumers. Keep this page open as you continue
the configuration.
![Bitbucket OAuth key](img/bitbucket_oauth_keys.png)
@@ -76,13 +79,13 @@ you to use.
sudo -u git -H editor /home/git/gitlab/config/gitlab.yml
```
-1. Follow the [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration)
- for initial settings.
1. Add the Bitbucket provider configuration:
For Omnibus packages:
```ruby
+ gitlab_rails['omniauth_enabled'] = true
+
gitlab_rails['omniauth_providers'] = [
{
"name" => "bitbucket",
@@ -96,10 +99,13 @@ you to use.
For installations from source:
```yaml
- - { name: 'bitbucket',
- app_id: 'BITBUCKET_APP_KEY',
- app_secret: 'BITBUCKET_APP_SECRET',
- url: 'https://bitbucket.org/' }
+ omniauth:
+ enabled: true
+ providers:
+ - { name: 'bitbucket',
+ app_id: 'BITBUCKET_APP_KEY',
+ app_secret: 'BITBUCKET_APP_SECRET',
+ url: 'https://bitbucket.org/' }
```
---
@@ -108,8 +114,8 @@ you to use.
from the Bitbucket application page.
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.
+1. For the changes to take effect, [reconfigure GitLab][] if you installed via
+ Omnibus, or [restart][] if installed from source.
On the sign in page there should now be a Bitbucket icon below the regular sign
in form. Click the icon to begin the authentication process. Bitbucket will ask
@@ -121,9 +127,12 @@ well, the user will be returned to GitLab and will be signed in.
Once the above configuration is set up, you can use Bitbucket to sign into
GitLab and [start importing your projects][bb-import].
+If you want to import projects from Bitbucket, but don't want to enable signing in,
+you can [disable Sign-Ins in the admin panel](omniauth.md#enable-or-disable-sign-in-with-an-omniauth-provider-without-disabling-import-sources).
+
[init-oauth]: omniauth.md#initial-omniauth-configuration
[bb-import]: ../workflow/importing/import_projects_from_bitbucket.md
[bb-old]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-14-stable/doc/integration/bitbucket.md
[bitbucket-docs]: https://confluence.atlassian.com/bitbucket/use-the-ssh-protocol-with-bitbucket-cloud-221449711.html#UsetheSSHprotocolwithBitbucketCloud-KnownhostorBitbucket%27spublickeyfingerprints
-[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
-[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
+[reconfigure GitLab]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart]: ../administration/restart_gitlab.md#installations-from-source
diff --git a/doc/integration/github.md b/doc/integration/github.md
index 23bb8ef9303..680712f9e01 100644
--- a/doc/integration/github.md
+++ b/doc/integration/github.md
@@ -110,7 +110,7 @@ On the sign in page there should now be a GitHub icon below the regular sign in
Click the icon to begin the authentication process. GitHub 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.
-### GitHub Enterprise with Self-Signed Certificate
+## GitHub Enterprise with self-signed Certificate
If you are attempting to import projects from GitHub Enterprise with a self-signed
certificate and the imports are failing, you will need to disable SSL verification.
diff --git a/doc/integration/gitlab.md b/doc/integration/gitlab.md
index eec40a9b8f1..70087576678 100644
--- a/doc/integration/gitlab.md
+++ b/doc/integration/gitlab.md
@@ -7,13 +7,11 @@ GitLab.com will generate an application ID and secret key for you to use.
1. Sign in to GitLab.com
-1. Navigate to your profile settings.
+1. On the upper right corner, click on your avatar and go to your **Settings**.
-1. Select "Applications" in the left menu.
+1. Select **Applications** in the left menu.
-1. Select "New application".
-
-1. Provide the required details.
+1. Provide the required details for **Add new application**.
- Name: This can be anything. Consider something like `<Organization>'s GitLab` or `<Your Name>'s GitLab` or something else descriptive.
- Redirect URI:
@@ -24,9 +22,9 @@ GitLab.com will generate an application ID and secret key for you to use.
The first link is required for the importer and second for the authorization.
-1. Select "Submit".
+1. Select **Save application**.
-1. You should now see a Client ID and Client Secret near the top right of the page (see screenshot).
+1. You should now see a **Application Id** and **Secret** near the top right of the page (see screenshot).
Keep this page open as you continue configuration.
![GitLab app](img/gitlab_app.png)
diff --git a/doc/integration/google.md b/doc/integration/google.md
index ae1d848f439..73e2f5826ff 100644
--- a/doc/integration/google.md
+++ b/doc/integration/google.md
@@ -30,12 +30,17 @@ In Google's side:
```
https://gitlab.example.com/users/auth/google_oauth2/callback
- https://gitlab.exampl.com/-/google_api/auth/callback
+ https://gitlab.example.com/-/google_api/auth/callback
```
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 > Compute > Google Kubernetes Engine API > Enable**
+1. From the **Dashboard** select **ENABLE APIS AND SERVICES > Compute > Google+ API > Enable**
+1. To enable projects to access [Google Kubernetes Engine](../user/project/clusters/index.md), you must also
+ enable these APIs:
+ - Google Kubernetes Engine API
+ - Cloud Resource Manager API
+ - Cloud Billing API
On your GitLab server:
@@ -72,7 +77,7 @@ On your GitLab server:
For installations from source:
- ```
+ ```yaml
- { name: 'google_oauth2', app_id: 'YOUR_APP_ID',
app_secret: 'YOUR_APP_SECRET',
args: { access_type: 'offline', approval_prompt: '' } }
diff --git a/doc/integration/img/gitlab_app.png b/doc/integration/img/gitlab_app.png
index b4958581a9b..8d6a4456fc4 100644
--- a/doc/integration/img/gitlab_app.png
+++ b/doc/integration/img/gitlab_app.png
Binary files differ
diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md
index 3edde3de83d..82e8fbdb93e 100644
--- a/doc/integration/omniauth.md
+++ b/doc/integration/omniauth.md
@@ -168,7 +168,7 @@ want their accounts to be upgraded to full internal accounts.
>**Note:**
The following information only applies for installations from source.
-GitLab uses [Omniauth](http://www.omniauth.org/) for authentication and already ships
+GitLab uses [Omniauth](https://github.com/omniauth/omniauth) for authentication and already ships
with a few providers pre-installed (e.g. LDAP, GitHub, Twitter). But sometimes that
is not enough and you need to integrate with other authentication solutions. For
these cases you can use the Omniauth provider.
diff --git a/doc/integration/openid_connect_provider.md b/doc/integration/openid_connect_provider.md
index ad41be52045..a7f907254a1 100644
--- a/doc/integration/openid_connect_provider.md
+++ b/doc/integration/openid_connect_provider.md
@@ -5,11 +5,11 @@ to sign in to other services.
## Introduction to OpenID Connect
-[OpenID Connect] \(OIC) is a simple identity layer on top of the
+[OpenID Connect] \(OIDC) is a simple identity layer on top of the
OAuth 2.0 protocol. It allows clients to verify the identity of the end-user
based on the authentication performed by GitLab, as well as to obtain
basic profile information about the end-user in an interoperable and
-REST-like manner. OIC performs many of the same tasks as OpenID 2.0,
+REST-like manner. OIDC performs many of the same tasks as OpenID 2.0,
but does so in a way that is API-friendly, and usable by native and
mobile applications.
@@ -23,14 +23,17 @@ are supported.
## Enabling OpenID Connect for OAuth applications
Refer to the [OAuth guide] for basic information on how to set up OAuth
-applications in GitLab. To enable OIC for an application, all you have to do
+applications in GitLab. To enable OIDC for an application, all you have to do
is select the `openid` scope in the application settings.
+## Shared information
+
Currently the following user information is shared with clients:
| Claim | Type | Description |
|:-----------------|:----------|:------------|
-| `sub` | `string` | An opaque token that uniquely identifies the user
+| `sub` | `string` | The ID of the user
+| `sub_legacy` | `string` | An opaque token that uniquely identifies the user<br><br>**Deprecation notice:** this token isn't stable because it's tied to the Rails secret key base, and is provided only for migration to the new stable `sub` value available from GitLab 11.1
| `auth_time` | `integer` | The timestamp for the user's last authentication
| `name` | `string` | The user's full name
| `nickname` | `string` | The user's GitLab username
@@ -41,6 +44,8 @@ Currently the following user information is shared with clients:
| `picture` | `string` | URL for the user's GitLab avatar
| `groups` | `array` | Names of the groups the user is a member of
+Only the `sub` and `sub_legacy` claims are included in the ID token, all other claims are available from the `/oauth/userinfo` endpoint used by OIDC clients.
+
[OpenID Connect]: http://openid.net/connect/ "OpenID Connect website"
[doorkeeper-openid_connect]: https://github.com/doorkeeper-gem/doorkeeper-openid_connect "Doorkeeper::OpenidConnect website"
[OAuth guide]: oauth_provider.md "GitLab as OAuth2 authentication service provider"
diff --git a/doc/integration/recaptcha.md b/doc/integration/recaptcha.md
index a301d1a613c..932cd479d56 100644
--- a/doc/integration/recaptcha.md
+++ b/doc/integration/recaptcha.md
@@ -20,4 +20,21 @@ To use reCAPTCHA, first you must create a site and private key.
6. Check the `Enable reCAPTCHA` checkbox
-7. Save the configuration.
+7. Save the configuration.
+
+## Enabling reCAPTCHA for user logins via passwords
+
+By default, reCAPTCHA is only enabled for user registrations. To enable it for
+user logins via passwords, the `X-GitLab-Show-Login-Captcha` HTTP header must
+be set. For example, in NGINX, this can be done via the `proxy_set_header`
+configuration variable:
+
+```
+proxy_set_header X-GitLab-Show-Login-Captcha 1;
+```
+
+In GitLab Omnibus, this can be configured via `/etc/gitlab/gitlab.rb`:
+
+```ruby
+nginx['proxy_set_headers'] = { 'X-GitLab-Show-Login-Captcha' => 1 }
+```
diff --git a/doc/integration/saml.md b/doc/integration/saml.md
index 3f49432ce93..25f396bc9c4 100644
--- a/doc/integration/saml.md
+++ b/doc/integration/saml.md
@@ -1,5 +1,8 @@
# SAML OmniAuth Provider
+NOTE: **Note:**
+You need to [enable OmniAuth](omniauth.md) in order to use this.
+
GitLab can be configured to act as a SAML 2.0 Service Provider (SP). This allows
GitLab to consume assertions from a SAML 2.0 Identity Provider (IdP) such as
Microsoft ADFS to authenticate users.
@@ -15,33 +18,33 @@ in your SAML IdP:
For omnibus package:
```sh
- sudo editor /etc/gitlab/gitlab.rb
+ sudo editor /etc/gitlab/gitlab.rb
```
For installations from source:
```sh
- cd /home/git/gitlab
+ cd /home/git/gitlab
- sudo -u git -H editor config/gitlab.yml
+ sudo -u git -H editor config/gitlab.yml
```
-1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration)
- for initial settings.
-
1. To allow your users to use SAML to sign up without having to manually create
an account first, don't forget to add the following values to your configuration:
For omnibus package:
```ruby
- gitlab_rails['omniauth_allow_single_sign_on'] = ['saml']
- gitlab_rails['omniauth_block_auto_created_users'] = false
+ gitlab_rails['omniauth_enabled'] = true
+ gitlab_rails['omniauth_allow_single_sign_on'] = ['saml']
+ gitlab_rails['omniauth_block_auto_created_users'] = false
```
For installations from source:
```yaml
+ omniauth:
+ enabled: true
allow_single_sign_on: ["saml"]
block_auto_created_users: false
```
@@ -52,13 +55,13 @@ in your SAML IdP:
For omnibus package:
```ruby
- gitlab_rails['omniauth_auto_link_saml_user'] = true
+ gitlab_rails['omniauth_auto_link_saml_user'] = true
```
For installations from source:
```yaml
- auto_link_saml_user: true
+ auto_link_saml_user: true
```
1. Add the provider configuration:
@@ -66,35 +69,37 @@ in your SAML IdP:
For omnibus package:
```ruby
- gitlab_rails['omniauth_providers'] = [
- {
- name: 'saml',
- args: {
- assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
- idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
- idp_sso_target_url: 'https://login.example.com/idp',
- issuer: 'https://gitlab.example.com',
- name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'
- },
- label: 'Company Login' # optional label for SAML login button, defaults to "Saml"
- }
- ]
- ```
-
- For installations from source:
-
- ```yaml
- - {
- name: 'saml',
- args: {
+ gitlab_rails['omniauth_providers'] = [
+ {
+ name: 'saml',
+ args: {
assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
idp_sso_target_url: 'https://login.example.com/idp',
issuer: 'https://gitlab.example.com',
name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'
},
- label: 'Company Login' # optional label for SAML login button, defaults to "Saml"
- }
+ label: 'Company Login' # optional label for SAML login button, defaults to "Saml"
+ }
+ ]
+ ```
+
+ For installations from source:
+
+ ```yaml
+ omniauth:
+ providers:
+ - {
+ name: 'saml',
+ args: {
+ assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
+ idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
+ idp_sso_target_url: 'https://login.example.com/idp',
+ issuer: 'https://gitlab.example.com',
+ name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'
+ },
+ label: 'Company Login' # optional label for SAML login button, defaults to "Saml"
+ }
```
1. Change the value for `assertion_consumer_service_url` to match the HTTPS endpoint
@@ -140,8 +145,8 @@ This setting is only available on GitLab 8.7 and above.
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
+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
@@ -179,6 +184,82 @@ tell GitLab which groups are external via the `external_groups:` element:
} }
```
+## Bypass two factor authentication
+
+If you want some SAML authentication methods to count as 2FA on a per session basis, you can register them in the
+`upstream_two_factor_authn_contexts` list:
+
+**For Omnibus installations:**
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ gitlab_rails['omniauth_providers'] = [
+ {
+ name: 'saml',
+ args: {
+ assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
+ idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
+ idp_sso_target_url: 'https://login.example.com/idp',
+ issuer: 'https://gitlab.example.com',
+ name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
+ upstream_two_factor_authn_contexts:
+ %w(
+ urn:oasis:names:tc:SAML:2.0:ac:classes:CertificateProtectedTransport
+ urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorOTPSMS
+ urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorIGTOKEN
+ )
+
+ },
+ label: 'Company Login' # optional label for SAML login button, defaults to "Saml"
+ }
+ ]
+ ```
+
+1. Save the file and [reconfigure][] GitLab for the changes to take effect.
+
+---
+
+**For installations from source:**
+
+1. Edit `config/gitlab.yml`:
+
+ ```yaml
+ omniauth:
+ providers:
+ - {
+ name: 'saml',
+ args: {
+ assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
+ idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
+ idp_sso_target_url: 'https://login.example.com/idp',
+ issuer: 'https://gitlab.example.com',
+ name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
+ upstream_two_factor_authn_contexts:
+ [
+ 'urn:oasis:names:tc:SAML:2.0:ac:classes:CertificateProtectedTransport',
+ 'urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorOTPSMS',
+ 'urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorIGTOKEN'
+ ]
+ },
+ label: 'Company Login' # optional label for SAML login button, defaults to "Saml"
+ }
+ ```
+
+1. Save the file and [restart GitLab][] for the changes ot take effect
+
+
+In addition to the changes in GitLab, make sure that your Idp is returning the
+`AuthnContext`. For example:
+
+```xml
+<saml:AuthnStatement>
+ <saml:AuthnContext>
+ <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:MediumStrongCertificateProtectedTransport</saml:AuthnContextClassRef>
+ </saml:AuthnContext>
+</saml:AuthnStatement>
+```
+
## Customization
### `auto_sign_in_with_provider`
diff --git a/doc/integration/shibboleth.md b/doc/integration/shibboleth.md
index 8611d4f7315..41fa63ae6f2 100644
--- a/doc/integration/shibboleth.md
+++ b/doc/integration/shibboleth.md
@@ -43,7 +43,19 @@ exclude shibboleth URLs from rewriting, add "RewriteCond %{REQUEST_URI} !/Shibbo
RequestHeader set X_FORWARDED_PROTO 'https'
```
-1. Edit /etc/gitlab/gitlab.rb configuration file, your shibboleth attributes should be in form of "HTTP_ATTRIBUTE" and you should adjust them to your need and environment. Add any other configuration you need.
+1. Edit /etc/gitlab/gitlab.rb configuration file to enable OmniAuth and add
+Shibboleth as an OmniAuth provider. User attributes will be sent from the
+Apache reverse proxy to GitLab as headers with the names from the Shibboleth
+attribute mapping. Therefore the values of the `args` hash
+should be in the form of `"HTTP_ATTRIBUTE"`. The keys in the hash are arguments
+to the [OmniAuth::Strategies::Shibboleth class](https://github.com/toyokazu/omniauth-shibboleth/blob/master/lib/omniauth/strategies/shibboleth.rb)
+and are documented by the [omniauth-shibboleth gem](https://github.com/toyokazu/omniauth-shibboleth)
+(take care to note the version of the gem packaged with GitLab). If some of
+your users appear to be authenticated by Shibboleth and Apache, but GitLab
+rejects their account with a URI that contains "e-mail is invalid" then your
+Shibboleth Identity Provider or Attribute Authority may be asserting multiple
+e-mail addresses. In this instance, you might consider setting the
+`multi_values` argument to `first`.
File should look like this:
```
@@ -58,14 +70,15 @@ gitlab_rails['omniauth_block_auto_created_users'] = false
gitlab_rails['omniauth_enabled'] = true
gitlab_rails['omniauth_providers'] = [
{
- "name" => 'shibboleth',
- "args" => {
- "shib_session_id_field" => "HTTP_SHIB_SESSION_ID",
+ "name" => "'shibboleth"',
+ "label" => "Text for Login Button",
+ "args" => {
+ "shib_session_id_field" => "HTTP_SHIB_SESSION_ID",
"shib_application_id_field" => "HTTP_SHIB_APPLICATION_ID",
- "uid_field" => 'HTTP_EPPN',
- "name_field" => 'HTTP_CN',
+ "uid_field" => 'HTTP_EPPN',
+ "name_field" => 'HTTP_CN',
"info_fields" => { "email" => 'HTTP_MAIL'}
- }
+ }
}
]
@@ -107,7 +120,7 @@ you will not get a shibboleth session!
RewriteEngine on
#Don't escape encoded characters in api requests
- RewriteCond %{REQUEST_URI} ^/api/v3/.*
+ RewriteCond %{REQUEST_URI} ^/api/v4/.*
RewriteCond %{REQUEST_URI} !/Shibboleth.sso
RewriteCond %{REQUEST_URI} !/shibboleth-sp
RewriteRule .* http://127.0.0.1:8181%{REQUEST_URI} [P,QSA,NE]
diff --git a/doc/public_access/public_access.md b/doc/public_access/public_access.md
index e8f4c73120c..81ee7338e4e 100644
--- a/doc/public_access/public_access.md
+++ b/doc/public_access/public_access.md
@@ -30,6 +30,12 @@ in users.
Any logged in user will have [Guest](../user/permissions.md) permissions
on the repository.
+### Private projects
+
+Private projects can only be cloned and viewed by project members, and
+they will only appear to project members on the public access directory
+(`https://gitlab.example.com/public`).
+
### How to change project visibility
1. Go to your project's **Settings**
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index 77139c50d07..f1881e0f767 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -31,8 +31,8 @@ sudo apt-get install -y rsync
>**Note:**
In GitLab 9.2 the timestamp format was changed from `EPOCH_YYYY_MM_DD` to
-`EPOCH_YYYY_MM_DD_GitLab version`, for example `1493107454_2017_04_25`
-would become `1493107454_2017_04_25_9.1.0`.
+`EPOCH_YYYY_MM_DD_GitLab_version`, for example `1493107454_2018_04_25`
+would become `1493107454_2018_04_25_10.6.4-ce`.
The backup archive will be saved in `backup_path`, which is specified in the
`config/gitlab.yml` file.
@@ -41,8 +41,8 @@ identifies the time at which each backup was created, plus the GitLab version.
The timestamp is needed if you need to restore GitLab and multiple backups are
available.
-For example, if the backup name is `1493107454_2017_04_25_9.1.0_gitlab_backup.tar`,
-then the timestamp is `1493107454_2017_04_25_9.1.0`.
+For example, if the backup name is `1493107454_2018_04_25_10.6.4-ce_gitlab_backup.tar`,
+then the timestamp is `1493107454_2018_04_25_10.6.4-ce`.
### Creating a backup of the GitLab system
@@ -195,6 +195,12 @@ This example can be used for a bucket in Amsterdam (AMS3).
1. [Reconfigure GitLab] for the changes to take effect
+CAUTION: **Warning:**
+If you see `400 Bad Request` by using Digital Ocean Spaces, the cause may be the
+usage of backup encryption. Remove or comment the line that
+contains `gitlab_rails['backup_encryption']` since Digital Ocean Spaces
+doesn't support encryption.
+
#### Other S3 Providers
Not all S3 providers are fully-compatible with the Fog library. For example,
@@ -326,6 +332,16 @@ For installations from source:
1. [Restart GitLab] for the changes to take effect
+#### Specifying a custom directory for backups
+
+Note: This option only works for remote storage. If you want to group your backups
+you can pass a `DIRECTORY` environment variable:
+
+```
+sudo gitlab-rake gitlab:backup:create DIRECTORY=daily
+sudo gitlab-rake gitlab:backup:create DIRECTORY=weekly
+```
+
### Uploading to locally mounted shares
You may also send backups to a mounted share (`NFS` / `CIFS` / `SMB` / etc.) by
@@ -369,15 +385,6 @@ For installations from source:
remote_directory: 'gitlab_backups'
```
-### Specifying a custom directory for backups
-
-If you want to group your backups you can pass a `DIRECTORY` environment variable:
-
-```
-sudo gitlab-rake gitlab:backup:create DIRECTORY=daily
-sudo gitlab-rake gitlab:backup:create DIRECTORY=weekly
-```
-
### Backup archive permissions
The backup archives created by GitLab (`1393513186_2014_02_27_gitlab_backup.tar`)
@@ -492,8 +499,8 @@ directory (repositories, uploads).
To restore a backup, you will also need to restore `/etc/gitlab/gitlab-secrets.json`
(for Omnibus packages) or `/home/git/gitlab/.secret` (for installations
from source). This file contains the database encryption key,
-[CI secret variables](../ci/variables/README.md#secret-variables), and
-secret variables used for [two-factor authentication](../user/profile/account/two_factor_authentication.md).
+[CI/CD variables](../ci/variables/README.md#variables), and
+variables used for [two-factor authentication](../user/profile/account/two_factor_authentication.md).
If you fail to restore this encryption key file along with the application data
backup, users with two-factor authentication enabled and GitLab Runners will
lose access to your GitLab server.
@@ -574,7 +581,7 @@ First make sure your backup tar file is in the backup directory described in the
`/var/opt/gitlab/backups`.
```shell
-sudo cp 1493107454_2017_04_25_9.1.0_gitlab_backup.tar /var/opt/gitlab/backups/
+sudo cp 11493107454_2018_04_25_10.6.4-ce_gitlab_backup.tar /var/opt/gitlab/backups/
```
Stop the processes that are connected to the database. Leave the rest of GitLab
@@ -592,7 +599,7 @@ restore:
```shell
# This command will overwrite the contents of your GitLab database!
-sudo gitlab-rake gitlab:backup:restore BACKUP=1493107454_2017_04_25_9.1.0
+sudo gitlab-rake gitlab:backup:restore BACKUP=1493107454_2018_04_25_10.6.4-ce
```
Next, restore `/etc/gitlab/gitlab-secrets.json` if necessary as mentioned above.
diff --git a/doc/raketasks/user_management.md b/doc/raketasks/user_management.md
index 5554a0c8b78..e1b1912ed47 100644
--- a/doc/raketasks/user_management.md
+++ b/doc/raketasks/user_management.md
@@ -14,7 +14,7 @@ bundle exec rake gitlab:import:user_to_projects[username@domain.tld] RAILS_ENV=p
Notes:
-- admin users are added as masters
+- admin users are added as maintainers
```bash
# omnibus-gitlab
diff --git a/doc/security/information_exclusivity.md b/doc/security/information_exclusivity.md
index f8e7fc3fd0e..22756232025 100644
--- a/doc/security/information_exclusivity.md
+++ b/doc/security/information_exclusivity.md
@@ -2,7 +2,7 @@
Git is a distributed version control system (DVCS).
This means that everyone that works with the source code has a local copy of the complete repository.
-In GitLab every project member that is not a guest (so reporters, developers and masters) can clone the repository to get a local copy.
+In GitLab every project member that is not a guest (so reporters, developers and maintainers) can clone the repository to get a local copy.
After obtaining this local copy the user can upload the full repository anywhere, including another project under their control or another server.
The consequence is that you can't build access controls that prevent the intentional sharing of source code by users that have access to the source code.
This is an inherent feature of a DVCS and all git management systems have this limitation.
diff --git a/doc/security/webhooks.md b/doc/security/webhooks.md
index a573445ab5b..b17b0a4bc4a 100644
--- a/doc/security/webhooks.md
+++ b/doc/security/webhooks.md
@@ -2,7 +2,7 @@
If you have non-GitLab web services running on your GitLab server or within its local network, these may be vulnerable to exploitation via Webhooks.
-With [Webhooks](../user/project/integrations/webhooks.md), you and your project masters and owners can set up URLs to be triggered when specific things happen to projects. Normally, these requests are sent to external web services specifically set up for this purpose, that process the request and its attached data in some appropriate way.
+With [Webhooks](../user/project/integrations/webhooks.md), you and your project maintainers and owners can set up URLs to be triggered when specific things happen to projects. Normally, these requests are sent to external web services specifically set up for this purpose, that process the request and its attached data in some appropriate way.
Things get hairy, however, when a Webhook is set up with a URL that doesn't point to an external, but to an internal service, that may do something completely unintended when the webhook is triggered and the POST request is sent.
diff --git a/doc/ssh/README.md b/doc/ssh/README.md
index b71e9bf3000..63f0a654fcf 100644
--- a/doc/ssh/README.md
+++ b/doc/ssh/README.md
@@ -77,6 +77,8 @@ Note that Public SSH key may also be named as follows:
If you want to change the password of your SSH key pair, you can use
`ssh-keygen -p <keyname>`.
+## Adding a SSH key to your GitLab account
+
1. The next step is to copy the public SSH key as we will need it afterwards.
To copy your public SSH key to the clipboard, use the appropriate code below:
@@ -171,7 +173,7 @@ This is really useful for cloning repositories to your Continuous
Integration (CI) server. By using deploy keys, you don't have to set up a
dummy user account.
-If you are a project master or owner, you can add a deploy key in the
+If you are a project maintainer 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)
@@ -196,7 +198,7 @@ This is really useful for integrating repositories to secured, shared Continuous
Integration (CI) services or other shared services.
GitLab administrators can set up the Global Shared Deploy key in GitLab and
add the private key to any shared systems. Individual repositories opt into
-exposing their repository using these keys when a project masters (or higher)
+exposing their repository using these keys when a project maintainers (or higher)
authorizes a Global Shared Deploy key to be used with their project.
Global Shared Keys can provide greater security compared to Per-Project Deploy
@@ -205,7 +207,7 @@ who needs to know and configure the private key.
GitLab administrators set up Global Deploy keys in the Admin area under the
section **Deploy Keys**. Ensure keys have a meaningful title as that will be
-the primary way for project masters and owners to identify the correct Global
+the primary way for project maintainers and owners to identify the correct Global
Deploy key to add. For instance, if the key gives access to a SaaS CI instance,
use the name of that service in the key name if that is all it is used for.
When creating Global Shared Deploy keys, give some thought to the granularity
@@ -213,7 +215,7 @@ of keys - they could be of very narrow usage such as just a specific service or
of broader usage for something like "Anywhere you need to give read access to
your repository".
-Once a GitLab administrator adds the Global Deployment key, project masters
+Once a GitLab administrator adds the Global Deployment key, project maintainers
and owners can add it in project's **Settings > Repository** section by expanding the
**Deploy Key** section and clicking **Enable** next to the appropriate key listed
under **Public deploy keys available to any project**.
diff --git a/doc/system_hooks/system_hooks.md b/doc/system_hooks/system_hooks.md
index 9ba05c7815b..cce02d218c2 100644
--- a/doc/system_hooks/system_hooks.md
+++ b/doc/system_hooks/system_hooks.md
@@ -138,7 +138,7 @@ Please refer to `group_rename` and `user_rename` for that case.
"created_at": "2012-07-21T07:30:56Z",
"updated_at": "2012-07-21T07:38:22Z",
"event_name": "user_add_to_team",
- "project_access": "Master",
+ "project_access": "Maintainer",
"project_id": 74,
"project_name": "StoreCloud",
"project_path": "storecloud",
@@ -158,7 +158,7 @@ Please refer to `group_rename` and `user_rename` for that case.
"created_at": "2012-07-21T07:30:56Z",
"updated_at": "2012-07-21T07:38:22Z",
"event_name": "user_remove_from_team",
- "project_access": "Master",
+ "project_access": "Maintainer",
"project_id": 74,
"project_name": "StoreCloud",
"project_path": "storecloud",
@@ -318,7 +318,7 @@ If the user is blocked via LDAP, `state` will be `ldap_blocked`.
"created_at": "2012-07-21T07:30:56Z",
"updated_at": "2012-07-21T07:38:22Z",
"event_name": "user_add_to_group",
- "group_access": "Master",
+ "group_access": "Maintainer",
"group_id": 78,
"group_name": "StoreCloud",
"group_path": "storecloud",
@@ -335,7 +335,7 @@ If the user is blocked via LDAP, `state` will be `ldap_blocked`.
"created_at": "2012-07-21T07:30:56Z",
"updated_at": "2012-07-21T07:38:22Z",
"event_name": "user_remove_from_group",
- "group_access": "Master",
+ "group_access": "Maintainer",
"group_id": 78,
"group_name": "StoreCloud",
"group_path": "storecloud",
diff --git a/doc/topics/autodevops/img/auto_monitoring.png b/doc/topics/autodevops/img/auto_monitoring.png
index 92902e3ca72..2900e5d1877 100644
--- a/doc/topics/autodevops/img/auto_monitoring.png
+++ b/doc/topics/autodevops/img/auto_monitoring.png
Binary files differ
diff --git a/doc/topics/autodevops/img/autodevops_domain_variables.png b/doc/topics/autodevops/img/autodevops_domain_variables.png
new file mode 100644
index 00000000000..b6f8864796f
--- /dev/null
+++ b/doc/topics/autodevops/img/autodevops_domain_variables.png
Binary files differ
diff --git a/doc/topics/autodevops/img/autodevops_multiple_clusters.png b/doc/topics/autodevops/img/autodevops_multiple_clusters.png
new file mode 100644
index 00000000000..f4d101ca921
--- /dev/null
+++ b/doc/topics/autodevops/img/autodevops_multiple_clusters.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_choose_gke.png b/doc/topics/autodevops/img/guide_choose_gke.png
new file mode 100644
index 00000000000..6da3a7220da
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_choose_gke.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_cluster_apps.png b/doc/topics/autodevops/img/guide_cluster_apps.png
new file mode 100644
index 00000000000..33d25f2950d
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_cluster_apps.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_connect_cluster.png b/doc/topics/autodevops/img/guide_connect_cluster.png
index b856b81a1d0..703d536f37a 100644
--- a/doc/topics/autodevops/img/guide_connect_cluster.png
+++ b/doc/topics/autodevops/img/guide_connect_cluster.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_create_cluster.png b/doc/topics/autodevops/img/guide_create_cluster.png
new file mode 100644
index 00000000000..cd1d0fdd8da
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_create_cluster.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_create_project.png b/doc/topics/autodevops/img/guide_create_project.png
new file mode 100644
index 00000000000..4ed1071db03
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_create_project.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_enable_autodevops.png b/doc/topics/autodevops/img/guide_enable_autodevops.png
new file mode 100644
index 00000000000..0fc3ecca19a
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_enable_autodevops.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_environments.png b/doc/topics/autodevops/img/guide_environments.png
new file mode 100644
index 00000000000..1d8d5614e64
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_environments.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_environments_metrics.png b/doc/topics/autodevops/img/guide_environments_metrics.png
new file mode 100644
index 00000000000..f0d31f31581
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_environments_metrics.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_first_pipeline.png b/doc/topics/autodevops/img/guide_first_pipeline.png
new file mode 100644
index 00000000000..57459dcc9d9
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_first_pipeline.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_gitlab_gke_details.png b/doc/topics/autodevops/img/guide_gitlab_gke_details.png
new file mode 100644
index 00000000000..bc5a53800f7
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_gitlab_gke_details.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_gke_apis_after.png b/doc/topics/autodevops/img/guide_gke_apis_after.png
new file mode 100644
index 00000000000..380de958867
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_gke_apis_after.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_gke_apis_before.png b/doc/topics/autodevops/img/guide_gke_apis_before.png
new file mode 100644
index 00000000000..d06fc707887
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_gke_apis_before.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_google_auth.png b/doc/topics/autodevops/img/guide_google_auth.png
new file mode 100644
index 00000000000..b97b2be9f15
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_google_auth.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_google_signin.png b/doc/topics/autodevops/img/guide_google_signin.png
new file mode 100644
index 00000000000..e59fc94bd4c
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_google_signin.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_ide_commit.png b/doc/topics/autodevops/img/guide_ide_commit.png
new file mode 100644
index 00000000000..188f60f2a4b
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_ide_commit.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_integration.png b/doc/topics/autodevops/img/guide_integration.png
deleted file mode 100644
index 723b2619ea2..00000000000
--- a/doc/topics/autodevops/img/guide_integration.png
+++ /dev/null
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_merge_request.png b/doc/topics/autodevops/img/guide_merge_request.png
new file mode 100644
index 00000000000..d78e69be776
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_merge_request.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_merge_request_ide.png b/doc/topics/autodevops/img/guide_merge_request_ide.png
new file mode 100644
index 00000000000..c825b0849e1
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_merge_request_ide.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_merge_request_review_app.png b/doc/topics/autodevops/img/guide_merge_request_review_app.png
new file mode 100644
index 00000000000..1b9b854ddac
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_merge_request_review_app.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_pipeline_stages.png b/doc/topics/autodevops/img/guide_pipeline_stages.png
new file mode 100644
index 00000000000..6e2f078152b
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_pipeline_stages.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_project_landing_page.png b/doc/topics/autodevops/img/guide_project_landing_page.png
new file mode 100644
index 00000000000..4f8d2eb10b1
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_project_landing_page.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_project_template.png b/doc/topics/autodevops/img/guide_project_template.png
new file mode 100644
index 00000000000..298ac0f6fcf
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_project_template.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_secret.png b/doc/topics/autodevops/img/guide_secret.png
deleted file mode 100644
index 01f5aa49908..00000000000
--- a/doc/topics/autodevops/img/guide_secret.png
+++ /dev/null
Binary files differ
diff --git a/doc/topics/autodevops/img/rollout_staging_disabled.png b/doc/topics/autodevops/img/rollout_staging_disabled.png
index 71e36b440f0..4c7c6768666 100644
--- a/doc/topics/autodevops/img/rollout_staging_disabled.png
+++ b/doc/topics/autodevops/img/rollout_staging_disabled.png
Binary files differ
diff --git a/doc/topics/autodevops/img/rollout_staging_enabled.png b/doc/topics/autodevops/img/rollout_staging_enabled.png
index d0d1d356627..f45c1c2cb37 100644
--- a/doc/topics/autodevops/img/rollout_staging_enabled.png
+++ b/doc/topics/autodevops/img/rollout_staging_enabled.png
Binary files differ
diff --git a/doc/topics/autodevops/img/staging_enabled.png b/doc/topics/autodevops/img/staging_enabled.png
index 0ef1a67d641..f0e0cd1cfcd 100644
--- a/doc/topics/autodevops/img/staging_enabled.png
+++ b/doc/topics/autodevops/img/staging_enabled.png
Binary files differ
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index f80c82acfa3..d04829eaeb8 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -1,6 +1,6 @@
# Auto DevOps
-> [Introduced][ce-37115] in GitLab 10.0.
+> [Introduced][ce-37115] in GitLab 10.0. Generally available on GitLab 11.0.
Auto DevOps automatically detects, builds, tests, deploys, and monitors your
applications.
@@ -13,6 +13,12 @@ without needing to configure anything. Just push your code and GitLab takes
care of everything else. This makes it easier to start new projects and brings
consistency to how applications are set up throughout a company.
+## Quick start
+
+If you are using GitLab.com, see the [quick start guide](quick_start_guide.md)
+for using Auto DevOps with GitLab.com and a Kubernetes cluster on Google Kubernetes
+Engine.
+
## Comparison to application platforms and PaaS
Auto DevOps provides functionality described by others as an application
@@ -34,18 +40,19 @@ in a couple of ways:
## Features
Comprised of a set of stages, Auto DevOps brings these best practices to your
-project in an easy and automatic way:
+project in a simple and automatic way:
1. [Auto Build](#auto-build)
1. [Auto Test](#auto-test)
-1. [Auto Code Quality](#auto-code-quality)
-1. [Auto SAST (Static Application Security Testing)](#auto-sast)
-1. [Auto Dependency Scanning](#auto-dependency-scanning)
+1. [Auto Code Quality](#auto-code-quality) **[STARTER]**
+1. [Auto SAST (Static Application Security Testing)](#auto-sast) **[ULTIMATE]**
+1. [Auto Dependency Scanning](#auto-dependency-scanning) **[ULTIMATE]**
+1. [Auto License Management](#auto-license-management) **[ULTIMATE]**
1. [Auto Container Scanning](#auto-container-scanning)
1. [Auto Review Apps](#auto-review-apps)
-1. [Auto DAST (Dynamic Application Security Testing)](#auto-dast)
+1. [Auto DAST (Dynamic Application Security Testing)](#auto-dast) **[ULTIMATE]**
1. [Auto Deploy](#auto-deploy)
-1. [Auto Browser Performance Testing](#auto-browser-performance-testing)
+1. [Auto Browser Performance Testing](#auto-browser-performance-testing) **[PREMIUM]**
1. [Auto Monitoring](#auto-monitoring)
As Auto DevOps relies on many different components, it's good to have a basic
@@ -62,7 +69,7 @@ Auto DevOps provides great defaults for all the stages; you can, however,
For an overview on the creation of Auto DevOps, read the blog post [From 2/3 of the Self-Hosted Git Market, to the Next-Generation CI System, to Auto DevOps](https://about.gitlab.com/2017/06/29/whats-next-for-gitlab-ci/).
-## Prerequisites
+## Requirements
TIP: **Tip:**
For self-hosted installations, the easiest way to make use of Auto DevOps is to
@@ -84,7 +91,7 @@ To make full use of Auto DevOps, you will need:
for the entire GitLab instance, or [specific Runners](../../ci/runners/README.md#registering-a-specific-runner)
that are assigned to specific projects.
1. **Base domain** (needed for Auto Review Apps and Auto Deploy) - You will need
- a domain configured with wildcard DNS which is gonna be used by all of your
+ a domain configured with wildcard DNS which is going to be used by all of your
Auto DevOps applications. [Read the specifics](#auto-devops-base-domain).
1. **Kubernetes** (needed for Auto Review Apps, Auto Deploy, and Auto Monitoring) -
To enable deployments, you will need Kubernetes 1.5+. You need a [Kubernetes cluster][kubernetes-clusters]
@@ -112,31 +119,31 @@ NOTE: **Note:**
If you do not have Kubernetes or Prometheus installed, then Auto Review Apps,
Auto Deploy, and Auto Monitoring will be silently skipped.
-### Auto DevOps base domain
+## Auto DevOps base domain
The Auto DevOps base domain is required if you want to make use of [Auto
-Review Apps](#auto-review-apps) and [Auto Deploy](#auto-deploy). It is defined
-either under the project's CI/CD settings while
-[enabling Auto DevOps](#enabling-auto-devops) or in instance-wide settings in
-the CI/CD section.
-It can also be set at the project or group level as a variable, `AUTO_DEVOPS_DOMAIN`.
+Review Apps](#auto-review-apps) and [Auto Deploy](#auto-deploy). It can be defined
+in three places:
-A wildcard DNS A record matching the base domain is required, for example,
+- either under the project's CI/CD settings while [enabling Auto DevOps](#enabling-auto-devops)
+- or in instance-wide settings in the **admin area > Settings** under the "Continuous Integration and Delivery" section
+- or at the project or group level as a variable: `AUTO_DEVOPS_DOMAIN` (required if you want to use [multiple clusters](#using-multiple-kubernetes-clusters))
+
+A wildcard DNS A record matching the base domain(s) is required, for example,
given a base domain of `example.com`, you'd need a DNS entry like:
```
*.example.com 3600 A 1.2.3.4
```
-where `example.com` is the domain name under which the deployed apps will be served,
+In this case, `example.com` is the domain name under which the deployed apps will be served,
and `1.2.3.4` is the IP address of your load balancer; generally NGINX
-([see prerequisites](#prerequisites)). How to set up the DNS record is beyond
+([see requirements](#requirements)). How to set up the DNS record is beyond
the scope of this document; you should check with your DNS provider.
-Alternatively you can use free public services like [xip.io](http://xip.io) or
-[nip.io](http://nip.io) which provide automatic wildcard DNS without any
-configuration. Just set the Auto DevOps base domain to `1.2.3.4.xip.io` or
-`1.2.3.4.nip.io`.
+Alternatively you can use free public services like [nip.io](http://nip.io)
+which provide automatic wildcard DNS without any configuration. Just set the
+Auto DevOps base domain to `1.2.3.4.nip.io`.
Once set up, all requests will hit the load balancer, which in turn will route
them to the Kubernetes pods that run your application(s).
@@ -146,26 +153,70 @@ If GitLab is installed using the [GitLab Omnibus Helm Chart], there are two
options: provide a static IP, or have one assigned. For more information see the
relevant docs on the [network prerequisites](../../install/kubernetes/gitlab_omnibus.md#networking-prerequisites).
-## Quick start
+## Using multiple Kubernetes clusters **[PREMIUM]**
+
+When using Auto DevOps, you may want to deploy different environments to
+different Kubernetes clusters. This is possible due to the 1:1 connection that
+[exists between them](../../user/project/clusters/index.md#multiple-kubernetes-clusters).
+
+In the [Auto DevOps template](https://gitlab.com/gitlab-org/gitlab-ci-yml/blob/master/Auto-DevOps.gitlab-ci.yml)
+(used behind the scenes by Auto DevOps), there are currently 3 defined environment names that you need to know:
+
+- `review/` (every environment starting with `review/`)
+- `staging`
+- `production`
+
+Those environments are tied to jobs that use [Auto Deploy](#auto-deploy), so
+except for the environment scope, they would also need to have a different
+domain they would be deployed to. This is why you need to define a separate
+`AUTO_DEVOPS_DOMAIN` variable for all the above
+[based on the environment](../../ci/variables/README.md#limiting-environment-scopes-of-variables).
-If you are using GitLab.com, see our [quick start guide](quick_start_guide.md)
-for using Auto DevOps with GitLab.com and an external Kubernetes cluster on
-Google Cloud.
+The following table is an example of how the three different clusters would
+be configured.
+
+| Cluster name | Cluster environment scope | `AUTO_DEVOPS_DOMAIN` variable value | Variable environment scope | Notes |
+| ------------ | -------------- | ----------------------------- | ------------- | ------ |
+| review | `review/*` | `review.example.com` | `review/*` | The review cluster which will run all [Review Apps](../../ci/review_apps/index.md). `*` is a wildcard, which means it will be used by every environment name starting with `review/`. |
+| staging | `staging` | `staging.example.com` | `staging` | (Optional) The staging cluster which will run the deployments of the staging environments. You need to [enable it first](#deploy-policy-for-staging-and-production-environments). |
+| production | `production` | `example.com` | `production` | The production cluster which will run the deployments of the production environment. You can use [incremental rollouts](#incremental-rollout-to-production). |
+
+To add a different cluster for each environment:
+
+1. Navigate to your project's **Operations > Kubernetes** and create the Kubernetes clusters
+ with their respective environment scope as described from the table above.
+
+ ![Auto DevOps multiple clusters](img/autodevops_multiple_clusters.png)
+
+1. After the clusters are created, navigate to each one and install Helm Tiller
+ and Ingress.
+1. Make sure you have [configured your DNS](#auto-devops-base-domain) with the
+ specified Auto DevOps domains.
+1. Navigate to your project's **Settings > CI/CD > Variables** and add
+ the `AUTO_DEVOPS_DOMAIN` variables with their respective environment
+ scope.
+
+ ![Auto DevOps domain variables](img/autodevops_domain_variables.png)
+
+Now that all is configured, you can test your setup by creating a merge request
+and verifying that your app is deployed as a review app in the Kubernetes
+cluster with the `review/*` environment scope. Similarly, you can check the
+other environments.
## Enabling Auto DevOps
-If you haven't done already, read the [prerequisites](#prerequisites) to make
+If you haven't done already, read the [requirements](#requirements) to make
full use of Auto DevOps. If this is your fist time, we recommend you follow the
-[quick start guide](#quick-start).
+[quick start guide](quick_start_guide.md).
To enable Auto DevOps to your project:
-1. Check that your project doesn't have a `.gitlab-ci.yml`, and remove it otherwise
-1. Go to your project's **Settings > CI/CD > General pipelines settings** and
- find the Auto DevOps section
+1. Check that your project doesn't have a `.gitlab-ci.yml`, or remove it otherwise
+1. Go to your project's **Settings > CI/CD > Auto DevOps**
1. Select "Enable Auto DevOps"
1. Optionally, but recommended, add in the [base domain](#auto-devops-base-domain)
- that will be used by Kubernetes to deploy your application
+ that will be used by Kubernetes to [deploy your application](#auto-deploy)
+ and choose the [deployment strategy](#deployment-strategy)
1. Hit **Save changes** for the changes to take effect
Once saved, an Auto DevOps pipeline will be triggered on the default branch.
@@ -182,6 +233,24 @@ 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.
+### Deployment strategy
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/38542) in GitLab 11.0.
+
+You can change the deployment strategy used by Auto DevOps by going to your
+project's **Settings > CI/CD > Auto DevOps**.
+
+The available options are:
+
+- **Continuous deployment to production** - enables [Auto Deploy](#auto-deploy)
+ by setting the [`STAGING_ENABLED`](#deploy-policy-for-staging-and-production-environments) and
+ [`INCREMENTAL_ROLLOUT_ENABLED`](#incremental-rollout-to-production) variables
+ to false.
+- **Automatic deployment to staging, manual deployment to production** - sets the
+ [`STAGING_ENABLED`](#deploy-policy-for-staging-and-production-environments) and
+ [`INCREMENTAL_ROLLOUT_ENABLED`](#incremental-rollout-to-production) variables
+ to true, and the user is responsible for manually deploying to staging and production.
+
## Stages of Auto DevOps
The following sections describe the stages of Auto DevOps. Read them carefully
@@ -218,19 +287,19 @@ NOTE: **Note:**
Auto Test uses tests you already have in your application. If there are no
tests, it's up to you to add them.
-### Auto Code Quality
+### Auto Code Quality **[STARTER]**
-Auto Code Quality uses the open source
-[`codeclimate` image](https://hub.docker.com/r/codeclimate/codeclimate/) to run
+Auto Code Quality uses the
+[Code Quality image](https://gitlab.com/gitlab-org/security-products/codequality) to run
static analysis and other code checks on the current code. The report is
created, and is uploaded as an artifact which you can later download and check
out.
In GitLab Starter, differences between the source and
target branches are also
-[shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html).
+[shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality.html).
-### Auto SAST
+### Auto SAST **[ULTIMATE]**
> Introduced in [GitLab Ultimate][ee] 10.3.
@@ -241,9 +310,9 @@ report is created, it's uploaded as an artifact which you can later download and
check out.
In GitLab Ultimate, any security warnings are also
-[shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/sast.html).
+[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/sast.html).
-### Auto Dependency Scanning
+### Auto Dependency Scanning **[ULTIMATE]**
> Introduced in [GitLab Ultimate][ee] 10.7.
@@ -253,8 +322,21 @@ to run analysis on the project dependencies and checks for potential security is
report is created, it's uploaded as an artifact which you can later download and
check out.
-In GitLab Ultimate, any security warnings are also
-[shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/dependency_scanning.html).
+Any security warnings are also
+[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/dependency_scanning.html).
+
+### Auto License Management **[ULTIMATE]**
+
+> Introduced in [GitLab Ultimate][ee] 11.0.
+
+License Management uses the
+[License Management Docker image](https://gitlab.com/gitlab-org/security-products/license-management)
+to search the project dependencies for their license. Once the
+report is created, it's uploaded as an artifact which you can later download and
+check out.
+
+Any licenses are also
+[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/license_management.html).
### Auto Container Scanning
@@ -267,13 +349,13 @@ created, it's uploaded as an artifact which you can later download and
check out.
In GitLab Ultimate, any security warnings are also
-[shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/container_scanning.html).
+[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/container_scanning.html).
### Auto Review Apps
NOTE: **Note:**
This is an optional step, since many projects do not have a Kubernetes cluster
-available. If the [prerequisites](#prerequisites) are not met, the job will
+available. If the [requirements](#requirements) are not met, the job will
silently be skipped.
CAUTION: **Caution:**
@@ -295,7 +377,7 @@ up in the merge request widget for easy discovery. When the branch is deleted,
for example after the merge request is merged, the Review App will automatically
be deleted.
-### Auto DAST
+### Auto DAST **[ULTIMATE]**
> Introduced in [GitLab Ultimate][ee] 10.4.
@@ -306,9 +388,9 @@ issues. Once the report is created, it's uploaded as an artifact which you can
later download and check out.
In GitLab Ultimate, any security warnings are also
-[shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/dast.html).
+[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/dast.html).
-### Auto Browser Performance Testing
+### Auto Browser Performance Testing **[PREMIUM]**
> Introduced in [GitLab Premium][ee] 10.4.
@@ -320,13 +402,14 @@ Auto Browser Performance Testing utilizes the [Sitespeed.io container](https://h
/direction
```
-In GitLab Premium, performance differences between the source and target branches are [shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/browser_performance_testing.html).
+In GitLab Premium, performance differences between the source
+and target branches are [shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/browser_performance_testing.html).
### Auto Deploy
NOTE: **Note:**
This is an optional step, since many projects do not have a Kubernetes cluster
-available. If the [prerequisites](#prerequisites) are not met, the job will
+available. If the [requirements](#requirements) are not met, the job will
silently be skipped.
CAUTION: **Caution:**
@@ -360,10 +443,19 @@ no longer be valid as soon as the deployment job finishes. This means that
Kubernetes can run the application, but in case it should be restarted or
executed somewhere else, it cannot be accessed again.
+> [Introduced][ce-19507] in GitLab 11.0.
+
+For internal and private projects a [GitLab Deploy Token](../../user/project/deploy_tokens/index.md###gitlab-deploy-token)
+will be automatically created, when Auto DevOps is enabled and the Auto DevOps settings are saved. This Deploy Token
+can be used for permanent access to the registry.
+
+Note: **Note**
+When the GitLab Deploy Token has been manually revoked, it won't be automatically created.
+
### Auto Monitoring
NOTE: **Note:**
-Check the [prerequisites](#prerequisites) for Auto Monitoring to make this stage
+Check the [requirements](#requirements) for Auto Monitoring to make this stage
work.
Once your application is deployed, Auto Monitoring makes it possible to monitor
@@ -493,6 +585,8 @@ also be customized, and you can easily use a [custom buildpack](#custom-buildpac
| `POSTGRES_PASSWORD` | The PostgreSQL password; defaults to `testing-password`. Set it to use a custom password. |
| `POSTGRES_DB` | The PostgreSQL database name; defaults to the value of [`$CI_ENVIRONMENT_SLUG`](../../ci/variables/README.md#predefined-variables-environment-variables). Set it to use a custom database name. |
| `BUILDPACK_URL` | The buildpack's full URL. It can point to either Git repositories or a tarball URL. For Git repositories, it is possible to point to a specific `ref`, for example `https://github.com/heroku/heroku-buildpack-ruby.git#v142` |
+| `SAST_CONFIDENCE_LEVEL` | The minimum confidence level of security issues you want to be reported; `1` for Low, `2` for Medium, `3` for High; defaults to `3`.|
+| `DEP_SCAN_DISABLE_REMOTE_CHECKS` | Whether remote Dependency Scanning checks are disabled; defaults to `"false"`. Set to `"true"` to disable checks that send data to GitLab central servers. [Read more about remote checks](https://gitlab.com/gitlab-org/security-products/dependency-scanning#remote-checks).|
| `STAGING_ENABLED` | From GitLab 10.8, this variable can be used to define a [deploy policy for staging and production environments](#deploy-policy-for-staging-and-production-environments). |
| `CANARY_ENABLED` | From GitLab 11.0, this variable can be used to define a [deploy policy for canary environments](#deploy-policy-for-canary-environments). |
| `INCREMENTAL_ROLLOUT_ENABLED`| From GitLab 10.8, this variable can be used to enable an [incremental rollout](#incremental-rollout-to-production) of your application for the production environment. |
@@ -575,6 +669,9 @@ service:
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ci-yml/merge_requests/160)
in GitLab 10.8.
+TIP: **Tip:**
+You can also set this inside your [project's settings](#deployment-strategy).
+
The normal behavior of Auto DevOps is to use Continuous Deployment, pushing
automatically to the `production` environment every time a new pipeline is run
on the default branch. However, there are cases where you might want to use a
@@ -605,6 +702,9 @@ If `CANARY_ENABLED` is defined in your project (e.g., set `CANARY_ENABLED` to
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5415) in GitLab 10.8.
+TIP: **Tip:**
+You can also set this inside your [project's settings](#deployment-strategy).
+
When you have a new version of your app to deploy in production, you may want
to use an incremental rollout to replace just a few pods with the latest code.
This will allow you to first check how the app is behaving, and later manually
@@ -740,4 +840,5 @@ curl --data "value=true" --header "PRIVATE-TOKEN: personal_access_token" https:/
[postgresql]: https://www.postgresql.org/
[Auto DevOps template]: https://gitlab.com/gitlab-org/gitlab-ci-yml/blob/master/Auto-DevOps.gitlab-ci.yml
[GitLab Omnibus Helm Chart]: ../../install/kubernetes/gitlab_omnibus.md
-[ee]: https://about.gitlab.com/products/
+[ee]: https://about.gitlab.com/pricing/
+[ce-19507]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/19507
diff --git a/doc/topics/autodevops/quick_start_guide.md b/doc/topics/autodevops/quick_start_guide.md
index 8989a4c8956..44b0cf758dc 100644
--- a/doc/topics/autodevops/quick_start_guide.md
+++ b/doc/topics/autodevops/quick_start_guide.md
@@ -1,143 +1,290 @@
-# Auto DevOps: quick start guide
+# Getting started with Auto DevOps
-> [Introduced][ce-37115] in GitLab 10.0.
+This is a step-by-step guide that will help you use [Auto DevOps](index.md) to
+deploy a project hosted on GitLab.com to Google Kubernetes Engine.
-This is a step-by-step guide to deploying a project hosted on GitLab.com to
-Google Cloud, using Auto DevOps.
+We will use GitLab's native Kubernetes integration, so you will not need
+to create a Kubernetes cluster manually using the Google Cloud Platform console.
+We will create and deploy a simple application that we create from a GitLab template.
-We made a minimal [Ruby
-application](https://gitlab.com/auto-devops-examples/minimal-ruby-app) to use
-as an example for this guide. It contains two main files:
+These instructions will also work for a self-hosted GitLab instance; you'll just
+need to ensure your own [Runners are configured](../../ci/runners/README.md) and
+[Google OAuth is enabled](../../integration/google.md).
-* `server.rb` - our application. It will start an HTTP server on port 5000 and
- render "Hello, world!"
-* `Dockerfile` - to build our app into a container image. It will use a ruby
- base image and run `server.rb`
+## Configuring your Google account
-## Fork sample project on GitLab.com
+Before creating and connecting your Kubernetes cluster to your GitLab project,
+you need a Google Cloud Platform account. If you don't already have one,
+sign up at https://console.cloud.google.com. You'll need to either sign in with an existing
+Google account (for example, one that you use to access Gmail, Drive, etc.) or create a new one.
-Let’s start by forking our sample application. Go to [the project
-page](https://gitlab.com/auto-devops-examples/minimal-ruby-app) and press the
-**Fork** button. Soon you should have a project under your namespace with the
-necessary files.
+1. Follow the steps as outlined in the ["Before you begin" section of the Kubernetes Engine docs](https://cloud.google.com/kubernetes-engine/docs/quickstart#before-you-begin)
+ in order for the required APIs and related services to be enabled.
+1. Make sure you have created a [billing account](https://cloud.google.com/billing/docs/how-to/manage-billing-account).
-You can also start a new project from a
-[GitLab project template](https://gitlab.com/gitlab-org/project-templates) if
-you want to use a different language.
+TIP: **Tip:**
+Every new Google Cloud Platform (GCP) account receives [$300 in credit](https://console.cloud.google.com/freetrial),
+and in partnership with Google, GitLab is able to offer an additional $200 for new GCP accounts to get started with GitLab's
+Google Kubernetes Engine Integration. All you have to do is [follow this link](https://goo.gl/AaJzRW) and apply for credit.
-## Setup your own cluster on Google Kubernetes Engine
+## Creating a new project from a template
-If you do not already have a Google Cloud account, create one at
-https://console.cloud.google.com.
+We will use one of GitLab's project templates to get started. As the name suggests,
+those projects provide a barebones application built on some well-known frameworks.
-Visit the [**Kubernetes Engine**](https://console.cloud.google.com/kubernetes/list)
-tab and create a new cluster. You can change the name and leave the rest of the
-default settings. Once you have your cluster running, you need to connect to the
-cluster by following the Google interface.
+1. In GitLab, click the plus icon (**+**) at the top of the navigation bar and select
+ **New project**.
+1. Go to the **Create from template** tab where you can choose among a Ruby on
+ Rails, Spring, or NodeJS Express project. For this example,
+ we'll use the Ruby on Rails template.
-## Connect to Kubernetes cluster
+ ![Select project template](img/guide_project_template.png)
-You need to have the Google Cloud SDK installed. e.g.
-On macOS, install [homebrew](https://brew.sh):
+1. Give your project a name, optionally a description, and make it public so that
+ you can take advantage of the features available in the
+ [GitLab Gold plan](https://about.gitlab.com/pricing/#gitlab-com).
-1. Install Brew Caskroom: `brew install caskroom/cask/brew-cask`
-2. Install Google Cloud SDK: `brew cask install google-cloud-sdk`
-3. Add `kubectl` with: `gcloud components install kubectl`
-4. Log in: `gcloud auth login`
+ ![Create project](img/guide_create_project.png)
-Now go back to the Google interface, find your cluster, follow the instructions
-under "Connect to the cluster" and open the Kubernetes Dashboard. It will look
-something like:
+1. Click **Create project**.
-```sh
-gcloud container clusters get-credentials ruby-autodeploy \ --zone europe-west2-c --project api-project-XXXXXXX
-```
+Now that the project is created, the next step is to create the Kubernetes cluster
+under which this application will be deployed.
-Finally, run `kubectl proxy`.
+## Creating a Kubernetes cluster from within GitLab
-![connect to cluster](img/guide_connect_cluster.png)
+1. On the project's landing page, click the button labeled **Add Kubernetes cluster**
+ (note that this option is also available when you navigate to **Operations > Kubernetes**).
-## Copy credentials to GitLab.com project
+ ![Project landing page](img/guide_project_landing_page.png)
-Once you have the Kubernetes Dashboard interface running, you should visit
-**Secrets** under the "Config" section. There, you should find the settings we
-need for GitLab integration: `ca.crt` and token.
+1. Choose **Create on Google Kubernetes Engine**.
-![connect to cluster](img/guide_secret.png)
+ ![Choose GKE](img/guide_choose_gke.png)
-You need to copy-paste the `ca.crt` and token into your project on GitLab.com in
-the Kubernetes integration page under project
-**Settings > Integrations > Project services > Kubernetes**. Don't actually copy
-the namespace though. Each project should have a unique namespace, and by leaving
-it blank, GitLab will create one for you.
+1. Sign in with Google.
-![connect to cluster](img/guide_integration.png)
+ ![Google sign in](img/guide_google_signin.png)
-For the API URL, you should use the "Endpoint" IP from your cluster page on
-Google Cloud Platform.
+1. Connect with your Google account and press **Allow** when asked (this will
+ be shown only the first time you connect GitLab with your Google account).
-## Expose application to the world
+ ![Google auth](img/guide_google_auth.png)
-In order to be able to visit your application, you need to install an NGINX
-ingress controller and point your domain name to its external IP address. Let's
-see how that's done.
+1. The last step is to fill in the cluster details. Give it a name, leave the
+ environment scope as is, and choose the GCP project under which the cluster
+ will be created. (Per the instructions when you
+ [configured your Google account](#configuring-your-google-account), a project
+ should have already been created for you.) Next, choose the
+ [region/zone](https://cloud.google.com/compute/docs/regions-zones/) under which the
+ cluster will be created, enter the number of nodes you want it to have, and
+ finally choose their [machine type](https://cloud.google.com/compute/docs/machine-types).
-### Set up Ingress controller
+ ![GitLab GKE cluster details](img/guide_gitlab_gke_details.png)
-You’ll need to make sure you have an ingress controller. If you don’t have one, do:
+1. Once ready, click **Create Kubernetes cluster**.
-```sh
-brew install kubernetes-helm
-helm init
-helm install --name ruby-app stable/nginx-ingress
-```
+After a couple of minutes, the cluster will be created. You can also see its
+status on your [GCP dashboard](https://console.cloud.google.com/kubernetes).
-This should create several services including `ruby-app-nginx-ingress-controller`.
-You can list your services by running `kubectl get svc` to confirm that.
+The next step is to install some applications on your cluster that are needed
+to take full advantage of Auto DevOps.
-### Point DNS at Cluster IP
+## Installing Helm, Ingress, and Prometheus
-Find out the external IP address of the `ruby-app-nginx-ingress-controller` by
-running:
+GitLab's Kubernetes integration comes with some
+[pre-defined applications](../../user/project/clusters/index.md#installing-applications)
+for you to install.
-```sh
-kubectl get svc ruby-app-nginx-ingress-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip}'
-```
+![Cluster applications](img/guide_cluster_apps.png)
+
+The first one to install is Helm Tiller, a package manager for Kubernetes, which
+is needed in order to install the rest of the applications. Go ahead and click
+its **Install** button.
+
+Once it's installed, the other applications that rely on it will each have their **Install**
+button enabled. For this guide, we need Ingress and Prometheus. Ingress provides
+load balancing, SSL termination, and name-based virtual hosting, using NGINX behind
+the scenes. Prometheus is an open-source monitoring and alerting system that we'll
+use to supervise the deployed application. We will not install GitLab Runner as
+we'll use the shared Runners that GitLab.com provides.
+
+After the Ingress is installed, wait a few seconds and copy the IP address that
+is displayed, which we'll use in the next step when enabling Auto DevOps.
+
+## Enabling Auto DevOps
+
+Now that the Kubernetes cluster is set up and ready, let's enable Auto DevOps.
+
+1. First, navigate to **Settings > CI/CD > Auto DevOps**.
+1. Select **Enable Auto DevOps**.
+1. Add in your base **Domain** by using the one GitLab suggests. Note that
+ generally, you would associate the IP address with a domain name on your
+ registrar's settings. In this case, for the sake of the guide, we will use
+ an alternative DNS that will map any domain name of the scheme
+ `anything.ip_address.nip.io` to the corresponding `ip_address`. For example,
+ if the IP address of the Ingress is `1.2.3.4`, the domain name to fill in
+ would be `1.2.3.4.nip.io`.
+1. Lastly, let's select the [continuous deployment strategy](index.md#deployment-strategy)
+ which will automatically deploy the application to production once the pipeline
+ successfully runs on the `master` branch.
+1. Click **Save changes**.
+
+ ![Auto DevOps settings](img/guide_enable_autodevops.png)
+
+Once you complete all the above and save your changes, a new pipeline is
+automatically created. To view the pipeline, go to **CI/CD > Pipelines**.
+
+![First pipeline](img/guide_first_pipeline.png)
+
+In the next section we'll break down the pipeline and explain what each job does.
+
+## Deploying the application
+
+By now you should see the pipeline running, but what is it running exactly?
+
+To navigate inside the pipeline, click its status badge. (It's status should be "running").
+The pipeline is split into 4 stages, each running a couple of jobs.
+
+![Pipeline stages](img/guide_pipeline_stages.png)
+
+In the **build** stage, the application is built into a Docker image and then
+uploaded to your project's [Container Registry](../../user/project/container_registry.md) ([Auto Build](index.md#auto-build)).
+
+In the **test** stage, GitLab runs various checks on the application:
+
+- The `test` job runs unit and integration tests by detecting the language and
+ framework ([Auto Test](index.md#auto-test))
+- The `code_quality` job checks the code quality and is allowed to fail
+ ([Auto Code Quality](index.md#auto-code-quality)) **[STARTER]**
+- The `container_scanning` job checks the Docker container if it has any
+ vulnerabilities and is allowed to fail ([Auto Container Scanning](index.md#auto-container-scanning))
+- The `dependency_scanning` job checks if the application has any dependencies
+ susceptible to vulnerabilities and is allowed to fail ([Auto Dependency Scanning](index.md#auto-dependency-scanning)) **[ULTIMATE]**
+- The `sast` job runs static analysis on the current code to check for potential
+ security issues and is allowed to fail([Auto SAST](index.md#auto-sast)) **[ULTIMATE]**
+- The `license_management` job searches the application's dependencies to determine each of their
+ licenses and is allowed to fail ([Auto License Management](index.md#auto-license-management)) **[ULTIMATE]**
NOTE: **Note:**
-If your ingress controller has been installed in a different way, you can find
-how to get the external IP address in the
-[Cluster documentation](../../user/project/clusters/index.md#getting-the-external-ip-address).
+As you might have noticed, all jobs except `test` are allowed to fail in the
+test stage.
+
+The **production** stage is run after the tests and checks finish, and it automatically
+deploys the application in Kubernetes ([Auto Deploy](index.md#auto-deploy)).
+
+Lastly, in the **performance** stage, some performance tests will run
+on the deployed application
+([Auto Browser Performance Testing](index.md#auto-browser-performance-testing)). **[PREMIUM]**
+
+---
-Use this IP address to configure your DNS. This part heavily depends on your
-preferences and domain provider. But in case you are not sure, just create an
-A record with a wildcard host like `*.<your-domain>`.
+The URL for the deployed application can be found under the **Environments**
+page where you can also monitor your application. Let's explore that.
+
+### Monitoring
+
+Now that the application is successfully deployed, let's navigate to its
+website. First, go to **Operations > Environments**.
+
+![Environments](img/guide_environments.png)
+
+In **Environments** you can see some details about the deployed
+applications. In the rightmost column for the production environment, you can make use of the three icons:
+
+- The first icon will open the URL of the application that is deployed in
+ production. It's a very simple page, but the important part is that it works!
+- The next icon with the small graph will take you to the metrics page where
+ Prometheus collects data about the Kubernetes cluster and how the application
+ affects it (in terms of memory/CPU usage, latency, etc.).
+
+ ![Environments metrics](img/guide_environments_metrics.png)
+
+- The third icon is the [web terminal](../../ci/environments.md#web-terminals)
+ and it will open a terminal session right inside the container where the
+ application is running.
+
+Right below, there is the
+[Deploy Board](https://docs.gitlab.com/ee/user/project/deploy_boards.md).
+The squares represent pods in your Kubernetes cluster that are associated with
+the given environment. Hovering above each square you can see the state of a
+deployment and clicking a square will take you to the pod's logs page.
+
+TIP: **Tip:**
+There is only one pod hosting the application at the moment, but you can add
+more pods by defining the [`REPLICAS` variable](index.md#environment-variables)
+under **Settings > CI/CD > Variables**.
+
+### Working with branches
+
+Following the [GitLab flow](../../workflow/gitlab_flow.md#working-with-feature-branches)
+let's create a feature branch that will add some content to the application.
+
+Under your repository, navigate to the following file: `app/views/welcome/index.html.erb`.
+By now, it should only contain a paragraph: `<p>You're on Rails!</p>`, so let's
+start adding content. Let's use GitLab's [Web IDE](../../user/project/web_ide/index.md) to make the change. Once
+you're on the Web IDE, make the following change:
+
+```html
+<p>You're on Rails! Powered by GitLab Auto DevOps.</p>
+```
+
+Stage the file, add a commit message, and create a new branch and a merge request
+by clicking **Commit**.
+
+![Web IDE commit](img/guide_ide_commit.png)
+
+Once you submit the merge request, you'll see the pipeline running. This will
+run all the jobs as [described previously](#deploying-the-application), as well
+a few more that run only on branches other than `master`.
+
+![Merge request](img/guide_merge_request.png)
+
+After a few minutes you'll notice that there was a failure in a test.
+This means there's a test that was 'broken' by our change.
+Navigating into the `test` job that failed, you can see what the broken test is:
+
+```
+Failure:
+WelcomeControllerTest#test_should_get_index [/app/test/controllers/welcome_controller_test.rb:7]:
+<You're on Rails!> expected but was
+<You're on Rails! Powered by GitLab Auto DevOps.>..
+Expected 0 to be >= 1.
+
+bin/rails test test/controllers/welcome_controller_test.rb:4
+```
-Use `nslookup minimal-ruby-app-staging.<yourdomain>` to confirm that domain is
-assigned to the cluster IP.
+Let's fix that:
-## Set up Auto DevOps
+1. Back to the merge request, click the **Web IDE** button.
+1. Find the `test/controllers/welcome_controller_test.rb` file and open it.
+1. Change line 7 to say `You're on Rails! Powered by GitLab Auto DevOps.`
+1. Click **Commit**.
+1. On your left, under "Unstaged changes", click the little checkmark icon
+ to stage the changes.
+1. Write a commit message and click **Commit**.
-In your GitLab.com project, go to **Settings > CI/CD** and find the Auto DevOps
-section. Select "Enable Auto DevOps", add in your base domain, and save.
+Now, if you go back to the merge request you should not only see the test passing,
+but also the application deployed as a [review app](index.md#auto-review-apps). You
+can visit it by following the URL in the merge request. The changes that we
+previously made should be there.
-Next, a pipeline needs to be triggered. Since the test project doesn't have a
-`.gitlab-ci.yml`, you need to either push a change to the repository or
-manually visit `https://gitlab.com/<username>/minimal-ruby-app/pipelines/new`,
-where `<username>` is your username.
+![Review app](img/guide_merge_request_review_app.png)
-This will create a new pipeline with several jobs: `build`, `test`, `codequality`,
-and `production`. The `build` job will create a Docker image with your new
-change and push it to the Container Registry. The `test` job will test your
-changes, whereas the `codequality` job will run static analysis on your changes.
-Finally, the `production` job will deploy your changes to a production application.
+Once you merge the merge request, the pipeline will run on the `master` branch,
+and the application will be eventually deployed straight to production.
-Once the deploy job succeeds you should be able to see your application by
-visiting the Kubernetes dashboard. Select the namespace of your project, which
-will look like `minimal-ruby-app-23`, but with a unique ID for your project,
-and your app will be listed as "production" under the Deployment tab.
+## Conclusion
-Once its ready, just visit `http://minimal-ruby-app.example.com` to see the
-famous "Hello, world!"!
+After implementing this project, you should now have a solid understanding of the basics of Auto DevOps.
+We started from building and testing to deploying and monitoring an application
+all within GitLab. Despite its automatic nature, Audo DevOps can also be configured
+and customized to fit your workflow. Here are some helpful resources for further reading:
-[ce-37115]: https://gitlab.com/gitlab-org/gitlab-ce/issues/37115
+1. [Auto DevOps](index.md)
+1. [Multiple Kubernetes clusters](index.md#using-multiple-kubernetes-clusters) **[PREMIUM]**
+1. [Incremental rollout to production](index.md#incremental-rollout-to-production) **[PREMIUM]**
+1. [Disable jobs you don't need with environment variables](index.md#environment-variables)
+1. [Use a static IP for your cluster](../../user/project/clusters/index.md#using-a-static-ip)
+1. [Use your own buildpacks to build your application](index.md#custom-buildpacks)
+1. [Prometheus monitoring](../../user/project/integrations/prometheus.md)
diff --git a/doc/topics/git/index.md b/doc/topics/git/index.md
index 2ca2bf743fb..7707d56764e 100644
--- a/doc/topics/git/index.md
+++ b/doc/topics/git/index.md
@@ -17,7 +17,7 @@ We've gathered some resources to help you to get the best from Git with GitLab.
- [How to install Git](how_to_install_git/index.md)
- [Start using Git on the command line](../../gitlab-basics/start-using-git.md)
- [Command Line basic commands](../../gitlab-basics/command-line-commands.md)
-- [GitLab Git Cheat Sheet (download)](https://gitlab.com/gitlab-com/marketing/raw/master/design/print/git-cheatsheet/print-pdf/git-cheatsheet.pdf)
+- [GitLab Git Cheat Sheet (download)](https://about.gitlab.com/images/press/git-cheat-sheet.pdf)
- Commits
- [Revert a commit](../../user/project/merge_requests/revert_changes.md#reverting-a-commit)
- [Cherry-picking a commit](../../user/project/merge_requests/cherry_pick_changes.md#cherry-picking-a-commit)
diff --git a/doc/university/README.md b/doc/university/README.md
index 55865ac23e8..595fc480887 100644
--- a/doc/university/README.md
+++ b/doc/university/README.md
@@ -15,11 +15,11 @@ Would you like to contribute to GitLab University? Then please take a look at ou
The curriculum is composed of GitLab videos, screencasts, presentations, projects and external GitLab content hosted on other services and has been organized into the following sections.
-1. [GitLab Beginner](#beginner)
-1. [GitLab Intermediate](#intermediate)
-1. [GitLab Advanced](#advanced)
-1. [External Articles](#external)
-1. [Resources for GitLab Team Members](#team)
+1. [GitLab Beginner](#1-gitlab-beginner)
+1. [GitLab Intermediate](#2-gitlab-intermediate)
+1. [GitLab Advanced](#3-gitlab-advanced)
+1. [External Articles](#4-external-articles)
+1. [Resources for GitLab Team Members](#5-resources-for-gitlab-team-members)
---
@@ -28,7 +28,6 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
#### 1.1. Version Control and Git
1. [Version Control Systems](https://docs.google.com/presentation/d/16sX7hUrCZyOFbpvnrAFrg6tVO5_yT98IgdAqOmXwBho/edit#slide=id.g72f2e4906_2_29)
-1. [Operating Systems and How Git Works](https://drive.google.com/a/gitlab.com/file/d/0B41DBToSSIG_OVYxVFJDOGI3Vzg/view?usp=sharing)
1. [Code School: An Introduction to Git](https://www.codeschool.com/account/courses/try-git)
#### 1.2. GitLab Basics
@@ -127,7 +126,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
1. [IBM: Continuous Delivery vs Continuous Deployment - Video](https://www.youtube.com/watch?v=igwFj8PPSnw)
1. [Amazon: Transition to Continuous Delivery - Video](https://www.youtube.com/watch?v=esEFaY0FDKc)
2. [TechBeacon: Doing continuous delivery? Focus first on reducing release cycle times](https://techbeacon.com/doing-continuous-delivery-focus-first-reducing-release-cycle-times)
-1. See **[Integrations](#integrations)** for integrations with other CI services.
+1. See **[Integrations](#39-integrations)** for integrations with other CI services.
#### 2.4. Workflow
diff --git a/doc/university/glossary/README.md b/doc/university/glossary/README.md
index 945d6a578b0..89516dba60b 100644
--- a/doc/university/glossary/README.md
+++ b/doc/university/glossary/README.md
@@ -557,10 +557,6 @@ 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."
-#### 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.
diff --git a/doc/university/high-availability/aws/README.md b/doc/university/high-availability/aws/README.md
index f340164b882..dc045961ed7 100644
--- a/doc/university/high-availability/aws/README.md
+++ b/doc/university/high-availability/aws/README.md
@@ -2,6 +2,10 @@
comments: false
---
+DANGER: This guide exists for reference of how an AWS deployment could work.
+We are currently seeing very slow EFS access performance which causes GitLab to
+be 5-10x slower than using NFS or Local disk. We _do not_ recommend follow this
+guide at this time.
# High Availability on AWS
diff --git a/doc/update/10.6-to-10.7.md b/doc/update/10.6-to-10.7.md
index 4a76ae14d2e..4efbb8c65cf 100644
--- a/doc/update/10.6-to-10.7.md
+++ b/doc/update/10.6-to-10.7.md
@@ -80,8 +80,8 @@ More information can be found on the [yarn website](https://yarnpkg.com/en/docs/
### 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.
+NOTE: GitLab 9.2 and higher only supports Go 1.9 and dropped support for Go
+1.5.x through 1.8.x. Be sure to upgrade your installation if necessary.
You can check which version you are running with `go version`.
@@ -91,11 +91,11 @@ Download and install Go:
# 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
+curl --remote-name --progress https://storage.googleapis.com/golang/go1.9.linux-amd64.tar.gz
+echo 'd70eadefce8e160638a9a6db97f7192d8463069ab33138893ad3bf31b0650a79 go1.9.linux-amd64.tar.gz' | shasum -a256 -c - && \
+ sudo tar -C /usr/local -xzf go1.9.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
+rm go1.9.linux-amd64.tar.gz
```
### 6. Get latest code
diff --git a/doc/update/10.8-to-11.0.md b/doc/update/10.8-to-11.0.md
new file mode 100644
index 00000000000..22a0c9f950c
--- /dev/null
+++ b/doc/update/10.8-to-11.0.md
@@ -0,0 +1,361 @@
+---
+comments: false
+---
+
+# From 10.8 to 11.0
+
+Make sure you view this update guide from the branch (version) of GitLab you would
+like to install (e.g., `11-0-stable`. You can select the branch 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
+
+NOTE: If you installed GitLab from source, make sure `rsync` is installed.
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
+```
+
+### 3. Update Ruby
+
+NOTE: GitLab 11.0 and higher only support Ruby 2.4.x and dropped support for Ruby 2.3.x. Be
+sure to upgrade your interpreter if necessary.
+
+You can check which version you are running with `ruby -v`.
+
+Download Ruby and compile it:
+
+```bash
+mkdir /tmp/ruby && cd /tmp/ruby
+curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.4/ruby-2.4.4.tar.gz
+echo 'ec82b0d53bd0adad9b19e6b45e44d54e9ec3f10c ruby-2.4.4.tar.gz' | shasum -c - && tar xzf ruby-2.4.4.tar.gz
+cd ruby-2.4.4
+
+./configure --disable-install-rdoc
+make
+sudo make install
+```
+
+Install Bundler:
+
+```bash
+sudo gem install bundler --no-ri --no-rdoc
+```
+
+### 4. Update Node
+
+GitLab utilizes [webpack](http://webpack.js.org) to compile frontend assets.
+This requires a minimum version of node v6.0.0.
+
+You can check which version you are running with `node -v`. If you are running
+a version older than `v6.0.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/>
+
+GitLab also requires the use of yarn `>= v1.2.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 11.0 and higher only supports Go 1.9.x and newer, and dropped support for Go
+1.5.x through 1.8.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://dl.google.com/go/go1.10.3.linux-amd64.tar.gz
+echo 'fa1b0e45d3b647c252f51f5e1204aba049cde4af177ef9f2181f43004f901035 go1.10.3.linux-amd64.tar.gz' | shasum -a256 -c - && \
+ sudo tar -C /usr/local -xzf go1.10.3.linux-amd64.tar.gz
+sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/
+rm go1.10.3.linux-amd64.tar.gz
+```
+
+### 6. Get latest code
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git fetch --all --prune
+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 11-0-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 11-0-stable-ee
+```
+
+### 7. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+
+sudo -u git -H git fetch --all --tags --prune
+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 --prune
+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 --prune
+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-8-stable:config/gitlab.yml.example origin/11-0-stable:config/gitlab.yml.example
+```
+
+#### Nginx configuration
+
+Ensure you're still up-to-date with the latest NGINX configuration changes:
+
+```sh
+cd /home/git/gitlab
+
+# For HTTPS configurations
+git diff origin/10-8-stable:lib/support/nginx/gitlab-ssl origin/11-0-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/10-8-stable:lib/support/nginx/gitlab origin/11-0-stable:lib/support/nginx/gitlab
+```
+
+If you are using Strict-Transport-Security in your installation to continue using it you must enable it in your Nginx
+configuration as GitLab application no longer handles setting it.
+
+If you are using Apache instead of NGINX please see the updated [Apache templates].
+Also note that because Apache does not support upstreams behind Unix sockets you
+will need to let gitlab-workhorse listen on a TCP port. You can do this
+via [/etc/default/gitlab].
+
+[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache
+[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-0-stable/lib/support/init.d/gitlab.default.example#L38
+
+#### SMTP configuration
+
+If you're installing from source and use SMTP to deliver mail, you will need to add the following line
+to config/initializers/smtp_settings.rb:
+
+```ruby
+ActionMailer::Base.delivery_method = :smtp
+```
+
+See [smtp_settings.rb.sample] as an example.
+
+[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-0-stable/config/initializers/smtp_settings.rb.sample#L13
+
+#### Init script
+
+There might be new configuration options available for [`gitlab.default.example`][gl-example]. View them with the command below and apply them manually to your current `/etc/default/gitlab`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/10-8-stable:lib/support/init.d/gitlab.default.example origin/11-0-stable:lib/support/init.d/gitlab.default.example
+```
+
+Ensure you're still up-to-date with the latest init script changes:
+
+```bash
+cd /home/git/gitlab
+
+sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+```
+
+For Ubuntu 16.04.1 LTS:
+
+```bash
+sudo systemctl daemon-reload
+```
+
+### 12. Install libs, migrations, etc.
+
+```bash
+cd /home/git/gitlab
+
+# MySQL installations (note: the line below states '--without postgres')
+sudo -u git -H bundle install --without postgres development test --deployment
+
+# PostgreSQL installations (note: the line below states '--without mysql')
+sudo -u git -H bundle install --without mysql development test --deployment
+
+# Optional: clean up old gems
+sudo -u git -H bundle clean
+
+# Run database migrations
+sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
+
+# Compile GetText PO files
+
+sudo -u git -H bundle exec rake gettext:compile RAILS_ENV=production
+
+# Update node dependencies and recompile assets
+sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile RAILS_ENV=production NODE_ENV=production
+
+# Clean up cache
+sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production
+```
+
+**MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md).
+
+### 13. Start application
+
+```bash
+sudo service gitlab start
+sudo service nginx restart
+```
+
+### 14. Check application status
+
+Check if GitLab and its environment are configured correctly:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
+```
+
+To make sure you didn't miss anything run a more thorough check:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
+```
+
+If all items are green, then congratulations, the upgrade is complete!
+
+## Things went south? Revert to previous version (10.8)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 10.7 to 10.8](10.7-to-10.8.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/11-0-stable/config/gitlab.yml.example
+[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-0-stable/lib/support/init.d/gitlab.default.example
diff --git a/doc/update/11.0-to-11.1.md b/doc/update/11.0-to-11.1.md
new file mode 100644
index 00000000000..3f10a7edb8a
--- /dev/null
+++ b/doc/update/11.0-to-11.1.md
@@ -0,0 +1,378 @@
+---
+comments: false
+---
+
+# From 11.0 to 11.1
+
+Make sure you view this update guide from the branch (version) of GitLab you would
+like to install (e.g., `11-1-stable`. You can select the branch 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
+
+NOTE: If you installed GitLab from source, make sure `rsync` is installed.
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
+```
+
+### 3. Update Ruby
+
+NOTE: GitLab 11.0 and higher only support Ruby 2.4.x and dropped support for Ruby 2.3.x. Be
+sure to upgrade your interpreter if necessary.
+
+You can check which version you are running with `ruby -v`.
+
+Download Ruby and compile it:
+
+```bash
+mkdir /tmp/ruby && cd /tmp/ruby
+curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.4/ruby-2.4.4.tar.gz
+echo 'ec82b0d53bd0adad9b19e6b45e44d54e9ec3f10c ruby-2.4.4.tar.gz' | shasum -c - && tar xzf ruby-2.4.4.tar.gz
+cd ruby-2.4.4
+
+./configure --disable-install-rdoc
+make
+sudo make install
+```
+
+Install Bundler:
+
+```bash
+sudo gem install bundler --no-ri --no-rdoc
+```
+
+### 4. Update Node
+
+GitLab utilizes [webpack](http://webpack.js.org) to compile frontend assets.
+This requires a minimum version of node v6.0.0.
+
+You can check which version you are running with `node -v`. If you are running
+a version older than `v6.0.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/>
+
+GitLab also requires the use of yarn `>= v1.2.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 11.0 and higher only supports Go 1.9.x and newer, and dropped support for Go
+1.5.x through 1.8.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://dl.google.com/go/go1.10.3.linux-amd64.tar.gz
+echo 'fa1b0e45d3b647c252f51f5e1204aba049cde4af177ef9f2181f43004f901035 go1.10.3.linux-amd64.tar.gz' | shasum -a256 -c - && \
+ sudo tar -C /usr/local -xzf go1.10.3.linux-amd64.tar.gz
+sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/
+rm go1.10.3.linux-amd64.tar.gz
+```
+
+### 6. Get latest code
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git fetch --all --prune
+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 11-1-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 11-1-stable-ee
+```
+
+### 7. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+
+sudo -u git -H git fetch --all --tags --prune
+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 --prune
+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 --prune
+sudo -u git -H git checkout v$(</home/git/gitlab/GITALY_SERVER_VERSION)
+sudo -u git -H make
+```
+
+### 10. Update gitlab-pages
+
+#### Only needed if you use GitLab Pages.
+
+Install and compile gitlab-pages. GitLab-Pages 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-pages
+
+sudo -u git -H git fetch --all --tags --prune
+sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_PAGES_VERSION)
+sudo -u git -H make
+```
+
+### 11. 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
+```
+
+### 12. 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/11-0-stable:config/gitlab.yml.example origin/11-1-stable:config/gitlab.yml.example
+```
+
+#### Nginx configuration
+
+Ensure you're still up-to-date with the latest NGINX configuration changes:
+
+```sh
+cd /home/git/gitlab
+
+# For HTTPS configurations
+git diff origin/11-0-stable:lib/support/nginx/gitlab-ssl origin/11-1-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/11-0-stable:lib/support/nginx/gitlab origin/11-1-stable:lib/support/nginx/gitlab
+```
+
+If you are using Strict-Transport-Security in your installation to continue using it you must enable it in your Nginx
+configuration as GitLab application no longer handles setting it.
+
+If you are using Apache instead of NGINX please see the updated [Apache templates].
+Also note that because Apache does not support upstreams behind Unix sockets you
+will need to let gitlab-workhorse listen on a TCP port. You can do this
+via [/etc/default/gitlab].
+
+[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache
+[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-1-stable/lib/support/init.d/gitlab.default.example#L38
+
+#### SMTP configuration
+
+If you're installing from source and use SMTP to deliver mail, you will need to add the following line
+to config/initializers/smtp_settings.rb:
+
+```ruby
+ActionMailer::Base.delivery_method = :smtp
+```
+
+See [smtp_settings.rb.sample] as an example.
+
+[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-1-stable/config/initializers/smtp_settings.rb.sample#L13
+
+#### Init script
+
+There might be new configuration options available for [`gitlab.default.example`][gl-example]. View them with the command below and apply them manually to your current `/etc/default/gitlab`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/11-0-stable:lib/support/init.d/gitlab.default.example origin/11-1-stable:lib/support/init.d/gitlab.default.example
+```
+
+Ensure you're still up-to-date with the latest init script changes:
+
+```bash
+cd /home/git/gitlab
+
+sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+```
+
+For Ubuntu 16.04.1 LTS:
+
+```bash
+sudo systemctl daemon-reload
+```
+
+### 13. 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).
+
+### 14. Start application
+
+```bash
+sudo service gitlab start
+sudo service nginx restart
+```
+
+### 15. 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 (11.0)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 10.8 to 11.0](10.8-to-11.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/11-1-stable/config/gitlab.yml.example
+[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-1-stable/lib/support/init.d/gitlab.default.example
diff --git a/doc/user/admin_area/settings/img/enforce_terms.png b/doc/user/admin_area/settings/img/enforce_terms.png
index e5f0a2683b5..3d93e1cc891 100644
--- a/doc/user/admin_area/settings/img/enforce_terms.png
+++ b/doc/user/admin_area/settings/img/enforce_terms.png
Binary files differ
diff --git a/doc/user/admin_area/settings/img/sign_up_terms.png b/doc/user/admin_area/settings/img/sign_up_terms.png
new file mode 100644
index 00000000000..4cac9d426c9
--- /dev/null
+++ b/doc/user/admin_area/settings/img/sign_up_terms.png
Binary files differ
diff --git a/doc/user/admin_area/settings/sign_up_restrictions.md b/doc/user/admin_area/settings/sign_up_restrictions.md
index 26329f20339..9801a0a14ed 100644
--- a/doc/user/admin_area/settings/sign_up_restrictions.md
+++ b/doc/user/admin_area/settings/sign_up_restrictions.md
@@ -38,3 +38,4 @@ semicolon, comma, or a new line.
![Domain Blacklist](img/domain_blacklist.png)
[ce-5259]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5259
+[ce-598]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/598
diff --git a/doc/user/admin_area/settings/terms.md b/doc/user/admin_area/settings/terms.md
index 8e1fb982aba..aa817c9a209 100644
--- a/doc/user/admin_area/settings/terms.md
+++ b/doc/user/admin_area/settings/terms.md
@@ -20,6 +20,19 @@ When an admin enables this feature, they will automattically be
directed to the page to accept the terms themselves. After they
accept, they will be directed back to the settings page.
+## New registrations
+
+When this feature is enabled, a checkbox will be available in the
+sign-up form.
+
+![Sign up form](img/sign_up_terms.png)
+
+This checkbox will be required during sign up.
+
+Users can review the terms entered in the admin panel before
+accepting. The page will be opened in a new window so they can
+continue their registration afterwards.
+
## Accepting terms
When this feature was enabled, the users that have not accepted the
diff --git a/doc/user/admin_area/settings/third_party_offers.md b/doc/user/admin_area/settings/third_party_offers.md
new file mode 100644
index 00000000000..177251b52b9
--- /dev/null
+++ b/doc/user/admin_area/settings/third_party_offers.md
@@ -0,0 +1,6 @@
+# Third party offers
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/20379)
+> in [GitLab Core](https://about.gitlab.com/pricing/) 11.1
+
+The display of third party offers can be controlled in the Admin Area -> Settings page.
diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md
index 159109e8954..9b0ff02f227 100644
--- a/doc/user/discussions/index.md
+++ b/doc/user/discussions/index.md
@@ -11,7 +11,7 @@ You can leave a comment in the following places:
- commit diffs
The comment area supports [Markdown] and [quick actions]. One can edit their
-own comment at any time, and anyone with [Master access level][permissions] or
+own comment at any time, and anyone with [Maintainer access level][permissions] or
higher can also edit a comment made by someone else.
You could also reply to the notification email in order to reply to a comment,
@@ -253,7 +253,7 @@ to newer issues or merge requests.
- The people participating in the discussion are trolling, abusive, or otherwise
being unproductive.
-In these cases, a user with Master permissions or higher in the project can lock (and unlock)
+In these cases, a user with Maintainer permissions or higher in the project can lock (and unlock)
an issue or a merge request, using the "Lock" section in the sidebar:
| Unlock | Lock |
diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md
index 0c1cd113686..d054561d5f3 100644
--- a/doc/user/gitlab_com/index.md
+++ b/doc/user/gitlab_com/index.md
@@ -60,6 +60,7 @@ Below are the current settings regarding [GitLab CI/CD](../../ci/README.md).
| Setting | GitLab.com | Default |
| ----------- | ----------------- | ------------- |
| Artifacts maximum size | 1G | 100M |
+| Artifacts [expiry time](../../ci/yaml/README.md#artifacts-expire_in) | kept forever | deleted after 30 days unless otherwise specified |
## Repository size limit
diff --git a/doc/user/group/index.md b/doc/user/group/index.md
index 30761a66563..e6bf32a2dc5 100644
--- a/doc/user/group/index.md
+++ b/doc/user/group/index.md
@@ -125,7 +125,7 @@ side of your screen.
---
-Group owners and masters will be notified of your request and will be able to approve or
+Group owners and maintainers will be notified of your request and will be able to approve or
decline it on the members page.
![Manage access requests](img/access_requests_management.png)
diff --git a/doc/user/group/subgroups/index.md b/doc/user/group/subgroups/index.md
index 02f8ef08117..08849ac1df4 100644
--- a/doc/user/group/subgroups/index.md
+++ b/doc/user/group/subgroups/index.md
@@ -145,8 +145,8 @@ permissions.
For example, if User0 was first added to group `group-1/group-1-1` with Developer
permissions, then they will inherit those permissions in every other subgroup
-of `group-1/group-1-1`. To give them Master access to `group-1/group-1-1/group1-1-1`,
-you would add them again in that group as Master. Removing them from that group,
+of `group-1/group-1-1`. To give them Maintainer access to `group-1/group-1-1/group1-1-1`,
+you would add them again in that group as Maintainer. Removing them from that group,
the permissions will fallback to those of the ancestor group.
## Mentioning subgroups
diff --git a/doc/user/index.md b/doc/user/index.md
index a50e5e8fbf8..90f0e2285c3 100644
--- a/doc/user/index.md
+++ b/doc/user/index.md
@@ -7,7 +7,7 @@ description: 'Read through the GitLab User documentation to learn how to use, co
Welcome to GitLab! We're glad to have you here!
As a GitLab user you'll have access to all the features
-your [subscription](https://about.gitlab.com/products/)
+your [subscription](https://about.gitlab.com/pricing/)
includes, except [GitLab administrator](../README.md#administrator-documentation)
settings, unless you have admin privileges to install, configure,
and upgrade your GitLab instance.
@@ -23,7 +23,7 @@ documentation.
GitLab is a fully integrated software development platform that enables you
and your team to work cohesively, faster, transparently, and effectively,
since the discussion of a new idea until taking that idea to production all
-all the way through, from within the same platform.
+the way through, from within the same platform.
Please check this page for an overview on [GitLab's features](https://about.gitlab.com/features/).
@@ -110,7 +110,7 @@ personal access tokens, authorized applications, etc.
- [Authentication](../topics/authentication/index.md): Read through the authentication
methods available in GitLab.
- [Permissions](permissions.md): Learn the different set of permissions levels for each
-user type (guest, reporter, developer, master, owner).
+user type (guest, reporter, developer, maintainer, owner).
- [Feature highlight](feature_highlight.md): Learn more about the little blue dots
around the app that explain certain features
@@ -161,13 +161,13 @@ such as Trello, JIRA, etc.
## Webhooks
-Configure [webhooks](project/integrations/webhooks.html) to listen for
+Configure [webhooks](project/integrations/webhooks.md) to listen for
specific events like pushes, issues or merge requests. GitLab will send a
POST request with data to the webhook URL.
## API
-Automate GitLab via [API](../api/README.html).
+Automate GitLab via [API](../api/README.md).
## Git and GitLab
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index 650d60f1585..bd199b55a61 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -3,13 +3,15 @@
## GitLab Flavored Markdown (GFM)
> **Note:**
-Not all of the GitLab-specific extensions to Markdown that are described in
-this document currently work on our documentation website.
+> Not all of the GitLab-specific extensions to Markdown that are described in
+> this document currently work on our documentation website.
>
-For the best result, we encourage you to check this document out as rendered
+> For the best result, we encourage you to check this document out as rendered
by GitLab: [markdown.md]
-_GitLab uses the [Redcarpet Ruby library][redcarpet] for Markdown processing._
+_GitLab uses (as of 11.1) the [CommonMark Ruby Library][commonmarker] for Markdown processing of all new issues, merge requests, comments, and other Markdown content in the GitLab system. Previous content, wiki pages and Markdown files (`.md`) in the repositories are still processed using the [Redcarpet Ruby library][redcarpet]._
+
+_Where there are significant differences, we will try to call them out in this document._
GitLab uses "GitLab Flavored Markdown" (GFM). It extends the standard Markdown in a few significant ways to add some useful functionality. It was inspired by [GitHub Flavored Markdown](https://help.github.com/articles/basic-writing-and-formatting-syntax/).
@@ -20,8 +22,8 @@ You can use GFM in the following areas:
- merge requests
- milestones
- snippets (the snippet must be named with a `.md` extension)
-- wiki pages
-- markdown documents inside the repository
+- wiki pages (currently only rendered by Redcarpet)
+- markdown documents inside the repository (currently only rendered by Redcarpet)
You can also use other rich text files in GitLab. You might have to install a
dependency to do so. Please see the [github-markup gem readme](https://github.com/gitlabhq/markup#markups) for more information.
@@ -34,7 +36,7 @@ https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#newline
GFM honors the markdown specification in how [paragraphs and line breaks are handled](https://daringfireball.net/projects/markdown/syntax#p).
A paragraph is simply one or more consecutive lines of text, separated by one or more blank lines.
-Line-breaks, or softreturns, are rendered if you end a line with two or more spaces:
+Line-breaks, or soft returns, are rendered if you end a line with two or more spaces:
[//]: # (Do *NOT* remove the two ending whitespaces in the following line.)
[//]: # (They are needed for the Markdown text to render correctly.)
@@ -197,7 +199,7 @@ https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#inline-
With inline diffs tags you can display {+ additions +} or [- deletions -].
-The wrapping tags can be either curly braces or square brackets [+ additions +] or {- deletions -}.
+The wrapping tags can be either curly braces or square brackets: [+ additions +] or {- deletions -}.
Examples:
@@ -228,7 +230,7 @@ https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#emoji
You can use it to point out a :bug: or warn about :speak_no_evil: patches. And if someone improves your really :snail: code, send them some :birthday:. People will :heart: you for that.
- If you are new to this, don't be :fearful:. You can easily join the emoji :family:. All you need to do is to look up on the supported codes.
+ If you are new to this, don't be :fearful:. You can easily join the emoji :family:. All you need to do is to look up one of the supported codes.
Consult the [Emoji Cheat Sheet](https://www.emojicopy.com) for a list of all supported emoji codes. :thumbsup:
@@ -238,7 +240,7 @@ Sometimes you want to :monkey: around a bit and add some :star2: to your :speech
You can use it to point out a :bug: or warn about :speak_no_evil: patches. And if someone improves your really :snail: code, send them some :birthday:. People will :heart: you for that.
-If you are new to this, don't be :fearful:. You can easily join the emoji :family:. All you need to do is to look up on the supported codes.
+If you are new to this, don't be :fearful:. You can easily join the emoji :family:. All you need to do is to look up one of the supported codes.
Consult the [Emoji Cheat Sheet](https://www.emojicopy.com) for a list of all supported emoji codes. :thumbsup:
@@ -394,17 +396,17 @@ Color written inside backticks will be followed by a color "chip".
Examples:
- `#F00`
- `#F00A`
- `#FF0000`
- `#FF0000AA`
- `RGB(0,255,0)`
- `RGB(0%,100%,0%)`
- `RGBA(0,255,0,0.7)`
- `HSL(540,70%,50%)`
+ `#F00`
+ `#F00A`
+ `#FF0000`
+ `#FF0000AA`
+ `RGB(0,255,0)`
+ `RGB(0%,100%,0%)`
+ `RGBA(0,255,0,0.7)`
+ `HSL(540,70%,50%)`
`HSLA(540,70%,50%,0.7)`
-Becomes:
+Become:
`#F00`
`#F00A`
@@ -414,7 +416,7 @@ Becomes:
`RGB(0%,100%,0%)`
`RGBA(0,255,0,0.7)`
`HSL(540,70%,50%)`
-`HSLA(540,70%,50%,0.7)`
+`HSLA(540,70%,50%,0.7)`
#### Supported formats:
@@ -481,14 +483,14 @@ Alt-H2
All Markdown-rendered headers automatically get IDs, except in comments.
-On hover a link to those IDs becomes visible to make it easier to copy the link to the header to give it to someone else.
+On hover, a link to those IDs becomes visible to make it easier to copy the link to the header to give it to someone else.
The IDs are generated from the content of the header according to the following rules:
-1. All text is converted to lowercase
-1. All non-word text (e.g., punctuation, HTML) is removed
-1. All spaces are converted to hyphens
-1. Two or more hyphens in a row are converted to one
+1. All text is converted to lowercase.
+1. All non-word text (e.g., punctuation, HTML) is removed.
+1. All spaces are converted to hyphens.
+1. Two or more hyphens in a row are converted to one.
1. If a header with the same ID has already been generated, a unique
incrementing number is appended, starting at 1.
@@ -500,6 +502,7 @@ For example:
# This header has Unicode in it: 한글
## This header has spaces in it
### This header has spaces in it
+## This header has 3.5 in it (and parentheses)
```
Would generate the following link IDs:
@@ -509,11 +512,14 @@ Would generate the following link IDs:
1. `this-header-has-unicode-in-it-한글`
1. `this-header-has-spaces-in-it`
1. `this-header-has-spaces-in-it-1`
+1. `this-header-has-3-5-in-it-and-parentheses`
Note that the Emoji processing happens before the header IDs are generated, so the Emoji is converted to an image which then gets removed from the ID.
### Emphasis
+Examples:
+
```no-highlight
Emphasis, aka italics, with *asterisks* or _underscores_.
@@ -524,6 +530,8 @@ Combined emphasis with **asterisks and _underscores_**.
Strikethrough uses two tildes. ~~Scratch this.~~
```
+Become:
+
Emphasis, aka italics, with *asterisks* or _underscores_.
Strong emphasis, aka bold, with **asterisks** or __underscores__.
@@ -534,12 +542,14 @@ Strikethrough uses two tildes. ~~Scratch this.~~
### Lists
+Examples:
+
```no-highlight
1. First ordered list item
2. Another item
- * Unordered sub-list.
+ * Unordered sub-list.
1. Actual numbers don't matter, just that it's a number
- 1. Ordered sub-list
+ 1. Ordered sub-list
4. And another item.
* Unordered list can use asterisks
@@ -547,11 +557,13 @@ Strikethrough uses two tildes. ~~Scratch this.~~
+ Or pluses
```
+Become:
+
1. First ordered list item
2. Another item
- * Unordered sub-list.
+ * Unordered sub-list.
1. Actual numbers don't matter, just that it's a number
- 1. Ordered sub-list
+ 1. Ordered sub-list
4. And another item.
* Unordered list can use asterisks
@@ -559,33 +571,45 @@ Strikethrough uses two tildes. ~~Scratch this.~~
+ Or pluses
If a list item contains multiple paragraphs,
-each subsequent paragraph should be indented with four spaces.
+each subsequent paragraph should be indented to the same level as the start of the list item text (_Redcarpet: paragraph should be indented with four spaces._)
+
+Example:
```no-highlight
-1. First ordered list item
+1. First ordered list item
- Second paragraph of first item.
-2. Another item
+ Second paragraph of first item.
+
+2. Another item
```
+Becomes:
+
1. First ordered list item
- Second paragraph of first item.
+ Paragraph of first item.
+
2. Another item
-If the second paragraph isn't indented with four spaces,
-the second list item will be incorrectly labeled as `1`.
+If the paragraph of the first item is not indented with the proper number of spaces,
+the paragraph will appear outside the list, instead of properly indented under the list item.
+
+Example:
```no-highlight
1. First ordered list item
- Second paragraph of first item.
+ Paragraph of first item.
+
2. Another item
```
+Becomes:
+
1. First ordered list item
- Second paragraph of first item.
+ Paragraph of first item.
+
2. Another item
### Links
@@ -620,6 +644,8 @@ will point the link to `wikis/style` when the link is inside of a wiki markdown
### Images
+Examples:
+
Here's our logo (hover to see the title text):
Inline-style:
@@ -630,6 +656,8 @@ will point the link to `wikis/style` when the link is inside of a wiki markdown
[logo]: img/markdown_logo.png
+Become:
+
Here's our logo:
Inline-style:
@@ -644,6 +672,8 @@ Reference-style:
### Blockquotes
+Examples:
+
```no-highlight
> Blockquotes are very handy in email to emulate reply text.
> This line is part of the same quote.
@@ -653,6 +683,8 @@ Quote break.
> This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can *put* **Markdown** into a blockquote.
```
+Become:
+
> Blockquotes are very handy in email to emulate reply text.
> This line is part of the same quote.
@@ -666,6 +698,8 @@ You can also use raw HTML in your Markdown, and it'll mostly work pretty well.
See the documentation for HTML::Pipeline's [SanitizationFilter](http://www.rubydoc.info/gems/html-pipeline/1.11.0/HTML/Pipeline/SanitizationFilter#WHITELIST-constant) class for the list of allowed HTML tags and attributes. In addition to the default `SanitizationFilter` whitelist, GitLab allows `span`, `abbr`, `details` and `summary` elements.
+Examples:
+
```no-highlight
<dl>
<dt>Definition list</dt>
@@ -676,6 +710,8 @@ See the documentation for HTML::Pipeline's [SanitizationFilter](http://www.rubyd
</dl>
```
+Become:
+
<dl>
<dt>Definition list</dt>
<dd>Is something people use sometimes.</dd>
@@ -691,25 +727,31 @@ Content can be collapsed using HTML's [`<details>`](https://developer.mozilla.or
<p>
<details>
<summary>Click me to collapse/fold.</summary>
-These details will remain hidden until expanded.
+
+These details <em>will</em> remain <strong>hidden</strong> until expanded.
<pre><code>PASTE LOGS HERE</code></pre>
+
</details>
</p>
-**Note:** Unfortunately Markdown is not supported inside these tags, as described by the [markdown specification](https://daringfireball.net/projects/markdown/syntax#html). You can work around this by using HTML, for example you can use `<pre><code>` tags instead of [code fences](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#code-and-syntax-highlighting).
+**Note:** Markdown inside these tags is supported, as long as you have a blank link after the `</summary>` tag and before the `</details>` tag, as shown in the example. _Redcarpet does not support Markdown inside these tags. You can work around this by using HTML, for example you can use `<pre><code>` tags instead of [code fences](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#code-and-syntax-highlighting)._
```html
<details>
<summary>Click me to collapse/fold.</summary>
-These details will remain hidden until expanded.
-<pre><code>PASTE LOGS HERE</code></pre>
+These details _will_ remain **hidden** until expanded.
+
+ PASTE LOGS HERE
+
</details>
```
### Horizontal Rule
+Examples:
+
```
Three or more...
@@ -726,6 +768,8 @@ ___
Underscores
```
+Become:
+
Three or more...
---
@@ -742,10 +786,12 @@ Underscores
### Line Breaks
-My basic recommendation for learning how line breaks work is to experiment and discover -- hit &lt;Enter&gt; once (i.e., insert one newline), then hit it twice (i.e., insert two newlines), see what happens. You'll soon learn to get what you want. "Markdown Toggle" is your friend.
+A good way to learn how line breaks work is to experiment and discover -- hit <kbd>Enter</kbd> once (i.e., insert one newline), then hit it twice (i.e., insert two newlines), see what happens. You'll soon learn to get what you want. The "Preview" tab is your friend.
Here are some things to try out:
+Examples:
+
```
Here's a line for us to start with.
@@ -760,6 +806,8 @@ This line is *on its own line*, because the previous line ends with two spaces.
spaces.
```
+Become:
+
Here's a line for us to start with.
This line is separated from the one above by two newlines, so it will be a *separate paragraph*.
@@ -774,7 +822,9 @@ spaces.
### Tables
-Tables aren't part of the core Markdown spec, but they are part of GFM and Markdown Here supports them.
+Tables aren't part of the core Markdown spec, but they are part of GFM.
+
+Example:
```
| header 1 | header 2 |
@@ -783,18 +833,18 @@ Tables aren't part of the core Markdown spec, but they are part of GFM and Markd
| cell 3 | cell 4 |
```
-Code above produces next output:
+Becomes:
| header 1 | header 2 |
| -------- | -------- |
| cell 1 | cell 2 |
| cell 3 | cell 4 |
-**Note**
+**Note:** The row of dashes between the table header and body must have at least three dashes in each column.
-The row of dashes between the table header and body must have at least three dashes in each column.
+By including colons in the header row, you can align the text within that column.
-By including colons in the header row, you can align the text within that column:
+Example:
```
| Left Aligned | Centered | Right Aligned | Left Aligned | Centered | Right Aligned |
@@ -803,6 +853,8 @@ By including colons in the header row, you can align the text within that column
| Cell 7 | Cell 8 | Cell 9 | Cell 10 | Cell 11 | Cell 12 |
```
+Becomes:
+
| Left Aligned | Centered | Right Aligned | Left Aligned | Centered | Right Aligned |
| :----------- | :------: | ------------: | :----------- | :------: | ------------: |
| Cell 1 | Cell 2 | Cell 3 | Cell 4 | Cell 5 | Cell 6 |
@@ -810,13 +862,29 @@ By including colons in the header row, you can align the text within that column
### Footnotes
+Example:
+
```
You can add footnotes to your text as follows.[^2]
[^2]: This is my awesome footnote.
```
+Becomes:
+
You can add footnotes to your text as follows.[^2]
+### Superscripts / Subscripts
+
+CommonMark and GFM currently do not support the superscript syntax ( `x^2` ) that Redcarpet does. You can use the standard HTML syntax for superscripts and subscripts.
+
+```
+The formula for water is H<sub>2</sub>O
+while the equation for the theory of relativity is E = mc<sup>2</sup>.
+```
+
+The formula for water is H<sub>2</sub>O while the equation for the theory of relativity is E = mc<sup>2</sup>.
+
+
## Wiki-specific Markdown
The following examples show how links inside wikis behave.
@@ -908,3 +976,4 @@ A link starting with a `/` is relative to the wiki root.
[katex]: https://github.com/Khan/KaTeX "KaTeX website"
[katex-subset]: https://github.com/Khan/KaTeX/wiki/Function-Support-in-KaTeX "Macros supported by KaTeX"
[asciidoctor-manual]: http://asciidoctor.org/docs/user-manual/#activating-stem-support "Asciidoctor user manual"
+[commonmarker]: https://github.com/gjtorikian/commonmarker
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 0808e3949be..b6438397db8 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -25,9 +25,12 @@ See our [product handbook on permissions](https://about.gitlab.com/handbook/prod
## Project members permissions
+NOTE: **Note:**
+In GitLab 11.0, the Master role was renamed to Maintainer.
+
The following table depicts the various user permission levels in a project.
-| Action | Guest | Reporter | Developer | Master | Owner |
+| Action | Guest | Reporter | Developer |Maintainer| Owner |
|---------------------------------------|---------|------------|-------------|----------|--------|
| Create new issue | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
| Create confidential issue | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
@@ -41,7 +44,8 @@ The following table depicts the various user permission levels in a project.
| View wiki pages | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
| Pull project code | [^1] | ✓ | ✓ | ✓ | ✓ |
| Download project | [^1] | ✓ | ✓ | ✓ | ✓ |
-| Assign issues and merge requests | | ✓ | ✓ | ✓ | ✓ |
+| Assign issues | | ✓ | ✓ | ✓ | ✓ |
+| Assign merge requests | | | ✓ | ✓ | ✓ |
| Label issues and merge requests | | ✓ | ✓ | ✓ | ✓ |
| Create code snippets | | ✓ | ✓ | ✓ | ✓ |
| Manage issue tracker | | ✓ | ✓ | ✓ | ✓ |
@@ -50,6 +54,9 @@ The following table depicts the various user permission levels in a project.
| See a container registry | | ✓ | ✓ | ✓ | ✓ |
| See environments | | ✓ | ✓ | ✓ | ✓ |
| See a list of merge requests | | ✓ | ✓ | ✓ | ✓ |
+| Manage related issues **[STARTER]** | | ✓ | ✓ | ✓ | ✓ |
+| Lock issue discussions | | ✓ | ✓ | ✓ | ✓ |
+| Lock merge request discussions | | | ✓ | ✓ | ✓ |
| Create new environments | | | ✓ | ✓ | ✓ |
| Stop environments | | | ✓ | ✓ | ✓ |
| Manage/Accept merge requests | | | ✓ | ✓ | ✓ |
@@ -75,11 +82,12 @@ The following table depicts the various user permission levels in a project.
| Edit project | | | | ✓ | ✓ |
| Add deploy keys to project | | | | ✓ | ✓ |
| Configure project hooks | | | | ✓ | ✓ |
-| Manage runners | | | | ✓ | ✓ |
+| Manage Runners | | | | ✓ | ✓ |
| Manage job triggers | | | | ✓ | ✓ |
| Manage variables | | | | ✓ | ✓ |
-| Manage pages | | | | ✓ | ✓ |
-| Manage pages domains and certificates | | | | ✓ | ✓ |
+| Manage GitLab Pages | | | | ✓ | ✓ |
+| Manage GitLab Pages domains and certificates | | | | ✓ | ✓ |
+| Remove GitLab Pages | | | | | ✓ |
| Manage clusters | | | | ✓ | ✓ |
| Edit comments (posted by any user) | | | | ✓ | ✓ |
| Switch visibility level | | | | | ✓ |
@@ -89,6 +97,7 @@ The following table depicts the various user permission levels in a project.
| Remove pages | | | | | ✓ |
| Force push to protected branches [^4] | | | | | |
| Remove protected branches [^4] | | | | | |
+| View project Audit Events | | | | ✓ | ✓ |
## Project features permissions
@@ -108,7 +117,7 @@ review, we've created protected branches. Read through the documentation on
[protected branches](project/protected_branches.md)
to learn more.
-Additionally, you can allow or forbid users with Master and/or
+Additionally, you can allow or forbid users with Maintainer and/or
Developer permissions to push to a protected branch. Read through the documentation on
[Allowed to Merge and Allowed to Push settings](project/protected_branches.md#using-the-allowed-to-merge-and-allowed-to-push-settings)
to learn more.
@@ -126,17 +135,12 @@ and drag issues around. Read though the
[documentation on Issue Boards permissions](project/issue_board.md#permissions)
to learn more.
-### File Locking permissions
-
-> Available in [GitLab Premium](https://about.gitlab.com/products/).
+### File Locking permissions **[PREMIUM]**
The user that locks a file or directory is the only one that can edit and push their changes back to the repository where the locked objects are located.
Read through the documentation on [permissions for File Locking](https://docs.gitlab.com/ee/user/project/file_lock.html#permissions-on-file-locking) to learn more.
-File Locking is available in
-[GitLab Premium](https://about.gitlab.com/products/) only.
-
### Confidential Issues permissions
Confidential issues can be accessed by reporters and higher permission levels,
@@ -145,11 +149,14 @@ read through the documentation on [permissions and access to confidential issues
## Group members permissions
+NOTE: **Note:**
+In GitLab 11.0, the Master role was renamed to Maintainer.
+
Any user can remove themselves from a group, unless they are the last Owner of
the group. The following table depicts the various user permission levels in a
group.
-| Action | Guest | Reporter | Developer | Master | Owner |
+| Action | Guest | Reporter | Developer | Maintainer | Owner |
|-------------------------|-------|----------|-----------|--------|-------|
| Browse group | ✓ | ✓ | ✓ | ✓ | ✓ |
| Edit group | | | | | ✓ |
@@ -159,6 +166,12 @@ group.
| Remove group | | | | | ✓ |
| Manage group labels | | ✓ | ✓ | ✓ | ✓ |
| Create/edit/delete group milestones | | | ✓ | ✓ | ✓ |
+| View private group epic **[ULTIMATE]** | | ✓ | ✓ | ✓ | ✓ |
+| View internal group epic **[ULTIMATE]** | ✓ | ✓ | ✓ | ✓ | ✓ |
+| View public group epic **[ULTIMATE]** | ✓ | ✓ | ✓ | ✓ | ✓ |
+| Create/edit group epic **[ULTIMATE]** | | ✓ | ✓ | ✓ | ✓ |
+| Delete group epic **[ULTIMATE]** | | | | | ✓ |
+| View group Audit Events | | | | | ✓ |
### Subgroup permissions
@@ -193,13 +206,37 @@ will find the option to flag the user as external.
By default new users are not set as external users. This behavior can be changed
by an administrator under **Admin > Application Settings**.
+## Auditor users **[PREMIUM ONLY]**
+
+>[Introduced][ee-998] in [GitLab Premium][eep] 8.17.
+
+Auditor users are given read-only access to all projects, groups, and other
+resources on the GitLab instance.
+
+An Auditor user should be able to access all projects and groups of a GitLab instance
+with the permissions described on the documentation on [auditor users permissions](https://docs.gitlab.com/ee/administration/auditor_users.html#permissions-and-restrictions-of-an-auditor-user).
+
+[Read more about Auditor users.](https://docs.gitlab.com/ee/administration/auditor_users.html)
+
+## Project features
+
+Project features like wiki and issues can be hidden from users depending on
+which visibility level you select on project settings.
+
+- Disabled: disabled for everyone
+- Only team members: only team members will see even if your project is public or internal
+- Everyone with access: everyone can see depending on your project visibility level
+
## GitLab CI/CD permissions
+NOTE: **Note:**
+In GitLab 11.0, the Master role was renamed to Maintainer.
+
GitLab CI/CD permissions rely on the role the user has in GitLab. There are four
permission levels in total:
- admin
-- master
+- maintainer
- developer
- guest/reporter
@@ -207,7 +244,7 @@ The admin user can perform any action on GitLab CI/CD in scope of the GitLab
instance and project. In addition, all admins can use the admin interface under
`/admin/runners`.
-| Action | Guest, Reporter | Developer | Master | Admin |
+| Action | Guest, Reporter | Developer |Maintainer| Admin |
|---------------------------------------|-----------------|-------------|----------|--------|
| See commits and jobs | ✓ | ✓ | ✓ | ✓ |
| Retry or cancel job | | ✓ | ✓ | ✓ |
@@ -222,6 +259,9 @@ instance and project. In addition, all admins can use the admin interface under
### Job permissions
+NOTE: **Note:**
+In GitLab 11.0, the Master role was renamed to Maintainer.
+
>**Note:**
GitLab 8.12 has a completely redesigned job permissions system.
Read all about the [new model and its implications][new-mod].
@@ -229,7 +269,7 @@ Read all about the [new model and its implications][new-mod].
This table shows granted privileges for jobs triggered by specific types of
users:
-| Action | Guest, Reporter | Developer | Master | Admin |
+| Action | Guest, Reporter | Developer |Maintainer| Admin |
|---------------------------------------------|-----------------|-------------|----------|--------|
| Run CI job | | ✓ | ✓ | ✓ |
| Clone source and LFS from current project | | ✓ | ✓ | ✓ |
@@ -262,23 +302,15 @@ for details about the pipelines security model.
Since GitLab 8.15, LDAP user permissions can now be manually overridden by an admin user.
Read through the documentation on [LDAP users permissions](https://docs.gitlab.com/ee/articles/how_to_configure_ldap_gitlab_ee/index.html#updating-user-permissions-new-feature) to learn more.
-## Auditor users permissions
-
-> Available in [GitLab Premium](https://about.gitlab.com/products/).
-
-An Auditor user should be able to access all projects and groups of a GitLab instance
-with the permissions described on the documentation on [auditor users permissions](https://docs.gitlab.com/ee/administration/auditor_users.html#permissions-and-restrictions-of-an-auditor-user).
-
-Auditor users are available in [GitLab Premium](https://about.gitlab.com/products/)
-only.
-
[^1]: On public and internal projects, all users are able to perform this action
[^2]: Guest users can only view the confidential issues they created themselves
[^3]: If **Public pipelines** is enabled in **Project Settings > CI/CD**
-[^4]: Not allowed for Guest, Reporter, Developer, Master, or Owner
+[^4]: Not allowed for Guest, Reporter, Developer, Maintainer, or Owner
[^5]: Only if the job was triggered by the user
[^6]: Only if user is not external one
[^7]: Only if user is a member of the project
[ce-18994]: https://gitlab.com/gitlab-org/gitlab-ce/issues/18994
[new-mod]: project/new_ci_build_permissions_model.md
+[ee-998]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/998
+[eep]: https://about.gitlab.com/pricing/
diff --git a/doc/user/profile/img/profil-preferences-navigation-theme.png b/doc/user/profile/img/profil-preferences-navigation-theme.png
new file mode 100644
index 00000000000..7adaec33b60
--- /dev/null
+++ b/doc/user/profile/img/profil-preferences-navigation-theme.png
Binary files differ
diff --git a/doc/user/profile/img/profile-preferences-syntax-themes.png b/doc/user/profile/img/profile-preferences-syntax-themes.png
new file mode 100644
index 00000000000..719df9410fc
--- /dev/null
+++ b/doc/user/profile/img/profile-preferences-syntax-themes.png
Binary files differ
diff --git a/doc/user/profile/img/profile_settings_dropdown.png b/doc/user/profile/img/profile_settings_dropdown.png
index a2c620642e2..99b06a1bf58 100644
--- a/doc/user/profile/img/profile_settings_dropdown.png
+++ b/doc/user/profile/img/profile_settings_dropdown.png
Binary files differ
diff --git a/doc/user/profile/preferences.md b/doc/user/profile/preferences.md
index 930e506802a..eb2d731343e 100644
--- a/doc/user/profile/preferences.md
+++ b/doc/user/profile/preferences.md
@@ -4,19 +4,42 @@ A user's profile preferences page allows the user to customize various aspects
of GitLab to their liking.
To navigate to your profile's preferences, click your avatar icon in the top
-right corner and select **Settings**. From there on, choose the **Preferences**
-tab.
+right corner, select **Settings** and then choose **Preferences** from the
+left sidebar.
+
+## Navigation theme
+
+The GitLab navigation theme setting allows you to personalize your GitLab experience.
+You can choose from several color themes that add unique colors to the top navigation
+and left side navigation.
+Using individual color themes might help you differentiate between your different
+GitLab instances.
+
+The default palette is Indigo. You can choose between 10 different themes:
+
+- Indigo
+- Light Indigo
+- Blue
+- Light Blue
+- Green
+- Light Green
+- Red
+- Light Red
+- Dark
+- Light
+
+![Profile preferences navigation themes](img/profil-preferences-navigation-theme.png)
## Syntax highlighting theme
->**Note:**
-GitLab uses the [rouge Ruby library][rouge] for syntax highlighting. For a
-list of supported languages visit the rouge website.
+NOTE: **Note:**
+GitLab uses the [rouge Ruby library](http://rouge.jneen.net/ "Rouge website")
+for syntax highlighting. For a list of supported languages visit the rouge website.
Changing this setting allows you to customize the color theme when viewing any
syntax highlighted code on GitLab.
-The default one is **White**, and you can choose among 5 different colors:
+The default syntax theme is White, and you can choose among 5 different colors:
- White
- Dark
@@ -24,6 +47,8 @@ The default one is **White**, and you can choose among 5 different colors:
- Solarized dark
- Monokai
+![Profile preferences syntax highlighting themes](img/profile-preferences-syntax-themes.png)
+
## Behavior
The following settings allow you to customize the behavior of GitLab's layout
@@ -48,20 +73,17 @@ You have 8 options here that you can use for your default dashboard view:
- Your projects' activity
- Starred projects' activity
- Your groups
-- Your [Todos]
+- Your [Todos](../../workflow/todos.md)
- Assigned Issues
- Assigned Merge Requests
-### Project home page content
+### Project overview content
-The project home page content setting allows you to choose what content you want to
+The project overview content setting allows you to choose what content you want to
see on a project’s home page.
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"
-[todos]: ../../workflow/todos.md
+- Files and Readme (default)
+- Readme
+- Activity
diff --git a/doc/user/project/clusters/eks_and_gitlab/img/add_cluster.png b/doc/user/project/clusters/eks_and_gitlab/img/add_cluster.png
new file mode 100644
index 00000000000..9a0559a19d4
--- /dev/null
+++ b/doc/user/project/clusters/eks_and_gitlab/img/add_cluster.png
Binary files differ
diff --git a/doc/user/project/clusters/eks_and_gitlab/img/create_dns.png b/doc/user/project/clusters/eks_and_gitlab/img/create_dns.png
new file mode 100644
index 00000000000..657ab0d9fa9
--- /dev/null
+++ b/doc/user/project/clusters/eks_and_gitlab/img/create_dns.png
Binary files differ
diff --git a/doc/user/project/clusters/eks_and_gitlab/img/create_project.png b/doc/user/project/clusters/eks_and_gitlab/img/create_project.png
new file mode 100644
index 00000000000..f3446131419
--- /dev/null
+++ b/doc/user/project/clusters/eks_and_gitlab/img/create_project.png
Binary files differ
diff --git a/doc/user/project/clusters/eks_and_gitlab/img/deploy_apps.png b/doc/user/project/clusters/eks_and_gitlab/img/deploy_apps.png
new file mode 100644
index 00000000000..d6c3b1b3a94
--- /dev/null
+++ b/doc/user/project/clusters/eks_and_gitlab/img/deploy_apps.png
Binary files differ
diff --git a/doc/user/project/clusters/eks_and_gitlab/img/environment.png b/doc/user/project/clusters/eks_and_gitlab/img/environment.png
new file mode 100644
index 00000000000..77d711ba8f6
--- /dev/null
+++ b/doc/user/project/clusters/eks_and_gitlab/img/environment.png
Binary files differ
diff --git a/doc/user/project/clusters/eks_and_gitlab/img/new_project.png b/doc/user/project/clusters/eks_and_gitlab/img/new_project.png
new file mode 100644
index 00000000000..d401c4ac2bf
--- /dev/null
+++ b/doc/user/project/clusters/eks_and_gitlab/img/new_project.png
Binary files differ
diff --git a/doc/user/project/clusters/eks_and_gitlab/img/pipeline.png b/doc/user/project/clusters/eks_and_gitlab/img/pipeline.png
new file mode 100644
index 00000000000..5f9c9815c24
--- /dev/null
+++ b/doc/user/project/clusters/eks_and_gitlab/img/pipeline.png
Binary files differ
diff --git a/doc/user/project/clusters/eks_and_gitlab/index.md b/doc/user/project/clusters/eks_and_gitlab/index.md
new file mode 100644
index 00000000000..2d8fdf0d1da
--- /dev/null
+++ b/doc/user/project/clusters/eks_and_gitlab/index.md
@@ -0,0 +1,135 @@
+---
+author: Joshua Lambert
+author_gitlab: joshlambert
+level: intermediate
+article_type: tutorial
+date: 2018-06-05
+---
+
+# Connecting and deploying to an Amazon EKS cluster
+
+## Introduction
+
+In this tutorial, we will show how easy it is to integrate an [Amazon EKS](https://aws.amazon.com/eks/) cluster with GitLab, and begin deploying applications.
+
+For an end-to-end walkthrough we will:
+1. Start with a new project based on the sample Ruby on Rails template
+1. Integrate an EKS cluster
+1. Utilize [Auto DevOps](../../../../topics/autodevops/) to build, test, and deploy our application
+
+You will need:
+1. An account on GitLab, like [GitLab.com](https://gitlab.com)
+1. An Amazon EKS cluster
+1. `kubectl` [installed and configured for access to the EKS cluster](https://docs.aws.amazon.com/eks/latest/userguide/getting-started.html#get-started-kubectl)
+
+If you don't have an Amazon EKS cluster, one can be created by following [the EKS getting started guide](https://docs.aws.amazon.com/eks/latest/userguide/getting-started.html).
+
+## Creating a new project
+
+On GitLab, create a new project by clicking on the `+` icon in the top navigation bar, and selecting `New project`.
+
+![New Project](img/new_project.png)
+
+On the new project screen, click on the `Create from template` tab, and select `Use template` for the Ruby on Rails sample project.
+
+Give the project a name, and then select `Create project`.
+
+![Create Project](img/create_project.png)
+
+## Connecting the EKS cluster
+
+From the left side bar, hover over `Operations` and select `Kubernetes`, then click on `Add Kubernetes cluster`, and finally `Add an existing Kubernetes cluster`.
+
+A few details from the EKS cluster will be required to connect it to GitLab.
+
+1. A valid Kubernetes certificate and token are needed to authenticate to the EKS cluster. A pair is created by default, which can be used. Open a shell and use `kubectl` to retrieve them:
+ * List the secrets with `kubectl get secrets`, and one should named similar to `default-token-xxxxx`. Copy that token name for use below.
+ * Get the certificate with `kubectl get secret <secret name> -o jsonpath="{['data']['ca\.crt']}" | base64 -D`
+ * Retrieve the token with `kubectl get secret <secret name> -o jsonpath="{['data']['token']}" | base64 -D`.
+1. The API server endpoint is also required, so GitLab can connect to the cluster. This is displayed on the AWS EKS console, when viewing the EKS cluster details.
+
+You now have all the information needed to connect the EKS cluster:
+* Kubernetes cluster name: Provide a name for the cluster to identify it within GitLab.
+* Environment scope: Leave this as `*` for now, since we are only connecting a single cluster.
+* API URL: Paste in the API server endpoint retrieved above.
+* CA Certificate: Paste the certificate data from the earlier step, as-is.
+* Paste the token value. Note on some versions of Kubernetes a trailing `%` is output, do not include it.
+* Project namespace: This can be left blank to accept the default namespace, based on the project name.
+
+![Add Cluster](img/add_cluster.png)
+
+Click on `Add Kubernetes cluster`, the cluster is now connected to GitLab. At this point, [Kubernetes deployment variables](../#deployment-variables) will automatically be available during CI jobs, making it easy to interact with the cluster.
+
+If you would like to utilize your own CI/CD scripts to deploy to the cluster, you can stop here.
+
+## Disable Role Based-Access Control (RBAC)
+
+Presently, Auto DevOps and one-click app installs do not support [Kubernetes role-based access control](https://kubernetes.io/docs/reference/access-authn-authz/rbac/). Support is [being worked on](https://gitlab.com/groups/gitlab-org/-/epics/136), but in the interim RBAC must be disabled to utilize for these features.
+
+> **Note**: Disabling RBAC means that any application running in the cluster, or user who can authenticate to the cluster, has full API access. This is a [security concern](https://docs.gitlab.com/ee/user/project/clusters/#security-implications), and may not be desirable.
+
+To effectively disable RBAC, global permissions can be applied granting full access:
+
+```bash
+kubectl create clusterrolebinding permissive-binding \
+ --clusterrole=cluster-admin \
+ --user=admin \
+ --user=kubelet \
+ --group=system:serviceaccounts
+```
+
+## Deploy services to the cluster
+
+GitLab supports one-click deployment of helpful services to the cluster, many of which support Auto DevOps. Back on the Kubernetes cluster screen in GitLab, a list of applications is now available to deploy.
+
+First install Helm Tiller, a package manager for Kubernetes. This enables deployment of the other applications.
+
+![Deploy Apps](img/deploy_apps.png)
+
+### Deploying NGINX Ingress (optional)
+
+Next, if you would like the deployed app to be reachable on the internet, deploy the Ingress. Note that this will also cause an [Elastic Load Balancer](https://aws.amazon.com/documentation/elastic-load-balancing/) to be created, which will incur additional AWS costs.
+
+Once installed, you may see a `?` for `Ingress IP Address`. This is because the created ELB is available at a DNS name, not an IP address. To get the DNS name, run: `kubectl get service ingress-nginx-ingress-controller -n gitlab-managed-apps -o jsonpath="{.status.loadBalancer.ingress[0].hostname}"`. Note, you may see a trailing `%` on some Kubernetes versions, do not include it.
+
+The Ingress is now available at this address, and will route incoming requests to the proper service based on the DNS name in the request. To support this, a wildcard DNS CNAME record should be created for the desired domain name. For example `*.myekscluster.com` would point to the Ingress hostname obtained earlier.
+
+![Create DNS](img/create_dns.png)
+
+### Deploying the GitLab Runner (optional)
+
+If the project is on GitLab.com, free shared runners are available and you do not have to deploy one. If a project specific runner is desired, or there are no shared runners, it is easy to deploy one.
+
+Simply click on the `Install` button for the GitLab Runner. It is important to note that the runner deployed is set as **privileged**, which means it essentially has root access to the underlying machine. This is required to build docker images, and so is on by default.
+
+### Deploying Prometheus (optional)
+
+GitLab is able to monitor applications automatically, utilizing [Prometheus](../../integrations/prometheus.html). Kubernetes container CPU and memory metrics are automatically collected, and response metrics are retrieved from NGINX Ingress as well.
+
+To enable monitoring, simply install Prometheus into the cluster with the `Install` button.
+
+## Create a default Storage Class
+
+Amazon EKS does not have a default Storage Class out of the box, which means requests for persistent volumes will not be automatically fulfilled. As part of Auto DevOps, the deployed Postgres instance requests persistent storage, and without a default storage class it will fail to start.
+
+If a default Storage Class does not already exist and is desired, follow Amazon's [short guide](https://docs.aws.amazon.com/eks/latest/userguide/storage-classes.html) to create one.
+
+Alternatively, disable Postgres by setting the project variable [`POSTGRES_ENABLED`](../../../../topics/autodevops/#environment-variables) to `false`.
+
+## Deploy the app to EKS
+
+With RBAC disabled and services deployed, [Auto DevOps](https://docs.gitlab.com/ee/topics/autodevops/) can now be leveraged to build, test, and deploy the app. To enable, click on `Settings` in the left sidebar, then `CI/CD`. You will see a section for `Auto DevOps`, expand it. Click on the radio button to `Enable Auto DevOps`.
+
+If a wildcard DNS entry was created resolving to the Load Balancer, enter it in the `domain` field. Otherwise, the deployed app will not be externally available outside of the cluster. To save, click `Save changes`.
+
+![Deploy Pipeline](img/pipeline.png)
+
+A new pipeline will automatically be created, which will begin to build, test, and deploy the app.
+
+After the pipeline has finished, your app will be running in EKS and available to users. Click on `CI/CD` tab in the left navigation bar, and choose `Environments`.
+
+![Deployed Environment](img/environment.png)
+
+You will see a list of the environments and their deploy status, as well as options to browse to the app, view monitoring metrics, and even access a shell on the running pod.
+
+To learn more about Auto DevOps, review our [documentation](../../../../topics/autodevops/).
diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md
index edb875bc7e6..cc1d65e4e6c 100644
--- a/doc/user/project/clusters/index.md
+++ b/doc/user/project/clusters/index.md
@@ -7,9 +7,10 @@ cluster in a few steps.
## Overview
-With a Kubernetes cluster associated to your project, you can use
+With one or more Kubernetes clusters associated to your project, you can use
[Review Apps](../../../ci/review_apps/index.md), deploy your applications, run
-your pipelines, and much more, in an easy way.
+your pipelines, use it with [Auto DevOps](../../../topics/autodevops/index.md),
+and much more, all from within GitLab.
There are two options when adding a new cluster to your project; either associate
your account with Google Kubernetes Engine (GKE) so that you can [create new
@@ -18,59 +19,65 @@ or provide the credentials to an [existing Kubernetes cluster](#adding-an-existi
## Adding and creating a new GKE cluster via GitLab
+TIP: **Tip:**
+Every new Google Cloud Platform (GCP) account receives [$300 in credit upon sign up](https://console.cloud.google.com/freetrial),
+and in partnership with Google, GitLab is able to offer an additional $200 for new GCP accounts to get started with GitLab's
+Google Kubernetes Engine Integration. All you have to do is [follow this link](https://goo.gl/AaJzRW) and apply for credit.
+
NOTE: **Note:**
-You need Master [permissions] and above to access the Kubernetes page.
-
-Before proceeding, make sure the following requirements are 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
- GitLab 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 and that you have to have permissions to access it.
-- You must have Master [permissions] in order to be able to access the
- **Kubernetes** page.
-- You must have [Cloud Billing API](https://cloud.google.com/billing/) enabled
-- You must have [Resource Manager
- API](https://cloud.google.com/resource-manager/)
+The [Google authentication integration](../../../integration/google.md) must
+be enabled in GitLab at the instance level. If that's not the case, ask your
+GitLab administrator to enable it. On GitLab.com, this is enabled.
+
+### Requirements
+
+Before creating your first cluster on Google Kubernetes Engine with GitLab's
+integration, make sure the following requirements are met:
+
+- A [billing account](https://cloud.google.com/billing/docs/how-to/manage-billing-account)
+ is set up and you have permissions to access it.
+- The Kubernetes Engine API is enabled. Follow the steps as outlined in the
+ ["Before you begin" section of the Kubernetes Engine docs](https://cloud.google.com/kubernetes-engine/docs/quickstart#before-you-begin).
+
+### Creating the cluster
If all of the above requirements are met, you can proceed to create and add a
-new Kubernetes cluster that will be hosted on GKE to your project:
+new Kubernetes cluster to your project:
+
+1. Navigate to your project's **Operations > Kubernetes** page.
+
+ NOTE: **Note:**
+ You need Maintainer [permissions] and above to access the Kubernetes page.
-1. Navigate to your project's **CI/CD > Kubernetes** page.
1. Click on **Add Kubernetes cluster**.
-1. Click on **Create with GKE**.
+1. Click on **Create with Google Kubernetes Engine**.
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/)
+1. From there on, choose your cluster's settings:
+ - **Kubernetes cluster name** - The name you wish to give the cluster.
+ - **Environment scope** - The [associated environment](#setting-the-environment-scope) to this cluster.
+ - **Google Cloud Platform project** - Choose the project you created in your GCP
+ console that will host the Kubernetes cluster. Learn more about
+ [Google Cloud Platform projects](https://cloud.google.com/resource-manager/docs/creating-managing-projects).
+ - **Zone** - Choose the [region 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.
+ - **Number of nodes** - Enter 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.
- - **Environment scope** - The [associated environment](#setting-the-environment-scope) to this cluster.
1. Finally, click the **Create Kubernetes 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.
+After a couple of minutes, your cluster will be ready to go. You can now proceed
+to install some [pre-defined applications](#installing-applications).
## Adding an existing Kubernetes cluster
-NOTE: **Note:**
-You need Master [permissions] and above to access the Kubernetes page.
-
To add an existing Kubernetes cluster to your project:
-1. Navigate to your project's **CI/CD > Kubernetes** page.
+1. Navigate to your project's **Operations > Kubernetes** page.
+
+ NOTE: **Note:**
+ You need Maintainer [permissions] and above to access the Kubernetes page.
+
1. Click on **Add Kubernetes cluster**.
1. Click on **Add an existing Kubernetes cluster** and fill in the details:
- **Kubernetes cluster name** (required) - The name you wish to give the cluster.
@@ -89,11 +96,11 @@ To add an existing Kubernetes cluster to your project:
you can follow the
[Kubernetes documentation](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/)
to create one. You can also view or create service tokens in the
- [Kubernetes dashboard](https://kubernetes.io/docs/tasks/access-application-cluster/web-ui-dashboard/#config)
- (under **Config > Secrets**).
- - **Project namespace** (optional) - The following apply:
- - By default you don't have to fill it in; by leaving it blank, GitLab will
- create one for you.
+ [Kubernetes dashboard](https://kubernetes.io/docs/tasks/access-application-cluster/web-ui-dashboard/)
+ (under **Config > Secrets**). **The account that will issue the service token
+ must have admin privileges on the cluster.**
+ - **Project namespace** (optional) - You don't have to fill it in; by leaving
+ it blank, GitLab will create one for you. Also:
- Each project should have a unique namespace.
- The project namespace is not necessarily the namespace of the secret, if
you're using a secret with broader permissions, like the secret from `default`.
@@ -103,11 +110,8 @@ To add an existing Kubernetes cluster to your project:
be the same.
1. Finally, click the **Create Kubernetes 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 Kubernetes cluster integration.
+After a couple of minutes, your cluster will be ready to go. You can now proceed
+to install some [pre-defined applications](#installing-applications).
## Security implications
@@ -150,12 +154,20 @@ 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).
+NOTE: **Note:**
+The applications will be installed in a dedicated namespace called
+`gitlab-managed-apps`. In case you have added an existing Kubernetes cluster
+with Tiller already installed, you should be careful as GitLab cannot
+detect it. By installing it via the applications will result into having it
+twice, which can lead to confusion during deployments.
+
| 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. |
+| [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 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] or deploy your own web apps. |
-| [Prometheus](https://prometheus.io/docs/introduction/overview/) | 10.4+ | Prometheus is an open-source monitoring and alerting system useful to supervise your deployed applications |
+| [Prometheus](https://prometheus.io/docs/introduction/overview/) | 10.4+ | Prometheus is an open-source monitoring and alerting system useful to supervise your deployed applications. |
| [GitLab Runner](https://docs.gitlab.com/runner/) | 10.6+ | GitLab Runner is the open source project that is used to run your jobs and send the results back to GitLab. It is used in conjunction with [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/), the open-source continuous integration service included with GitLab that coordinates the jobs. When installing the GitLab Runner via the applications, it will run in **privileged mode** by default. Make sure you read the [security implications](#security-implications) before doing so. |
+| [JupyterHub](http://jupyter.org/) | 11.0+ | [JupyterHub](https://jupyterhub.readthedocs.io/en/stable/) is a multi-user service for managing notebooks across a team. [Jupyter Notebooks](https://jupyter-notebook.readthedocs.io/en/latest/) provide a web-based interactive programming environment used for data analysis, visualization, and machine learning. **Note**: Authentication will be enabled for any user of the GitLab server via OAuth2. HTTPS will be supported in a future release. |
## Getting the external IP address
@@ -200,6 +212,11 @@ Otherwise, you can list the IP addresses of all load balancers:
kubectl get svc --all-namespaces -o jsonpath='{range.items[?(@.status.loadBalancer.ingress)]}{.status.loadBalancer.ingress[*].ip} '
```
+> **Note**: Some Kubernetes clusters return a hostname instead, like [Amazon EKS](https://aws.amazon.com/eks/). For these platforms, run:
+> ```bash
+> kubectl get service ingress-nginx-ingress-controller -n gitlab-managed-apps -o jsonpath="{.status.loadBalancer.ingress[0].hostname}".
+> ```
+
The output is the external IP address of your cluster. This information can then
be used to set up DNS entries and forwarding rules that allow external access to
your deployed applications.
@@ -232,7 +249,7 @@ When adding more than one Kubernetes clusters to your project, you need to
differentiate them with an environment scope. The environment scope associates
clusters and [environments](../../../ci/environments.md) in an 1:1 relationship
similar to how the
-[environment-specific variables](../../../ci/variables/README.md#limiting-environment-scopes-of-secret-variables)
+[environment-specific variables](../../../ci/variables/README.md#limiting-environment-scopes-of-variables)
work.
The default environment scope is `*`, which means all jobs, regardless of their
@@ -324,7 +341,7 @@ To disable the Kubernetes cluster integration, follow the same procedure.
## Removing the Kubernetes cluster integration
NOTE: **Note:**
-You need Master [permissions] and above to remove a Kubernetes cluster integration.
+You need Maintainer [permissions] and above to remove a Kubernetes cluster integration.
NOTE: **Note:**
When you remove a cluster, you only remove its relation to GitLab, not the
@@ -381,7 +398,7 @@ you will need the Kubernetes project integration enabled.
### Web terminals
NOTE: **Note:**
-Introduced in GitLab 8.15. You must be the project owner or have `master` permissions
+Introduced in GitLab 8.15. You must be the project owner or have `maintainer` permissions
to use terminals. Support is limited to the first container in the
first pod of your environment.
@@ -392,6 +409,10 @@ containers. To use this integration, you should deploy to Kubernetes using
the deployment variables above, ensuring any pods you create are labelled with
`app=$CI_ENVIRONMENT_SLUG`. GitLab will do the rest!
+## Read more
+
+- [Connecting and deploying to an Amazon EKS cluster](eks_and_gitlab/index.md)
+
[permissions]: ../../permissions.md
-[ee]: https://about.gitlab.com/products/
+[ee]: https://about.gitlab.com/pricing/
[Auto DevOps]: ../../../topics/autodevops/index.md
diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md
index 9c5e3509046..03302b3815d 100644
--- a/doc/user/project/container_registry.md
+++ b/doc/user/project/container_registry.md
@@ -143,6 +143,24 @@ docker login registry.example.com -u <your_username> -p <your_access_token>
for errors (e.g. `/var/log/gitlab/gitlab-rails/production.log`). You may be able to find clues
there.
+#### Enable the registry debug server
+
+The optional debug server can be enabled by setting the registry debug address
+in your `gitlab.rb` configuration.
+
+```ruby
+registry['debug_addr'] = "localhost:5001"
+```
+
+After adding the setting, [reconfigure] GitLab to apply the change.
+
+Use curl to request debug output from the debug server:
+
+```bash
+curl localhost:5001/debug/health
+curl localhost:5001/debug/vars
+```
+
### Advanced Troubleshooting
>**NOTE:** The following section is only recommended for experts.
@@ -275,3 +293,4 @@ Once the right permissions were set, the error will go away.
[docker-docs]: https://docs.docker.com/engine/userguide/intro/
[pat]: ../profile/personal_access_tokens.md
[pdt]: ../project/deploy_tokens/index.md
+[reconfigure]: ../../administration/restart_gitlab.md#omnibus-gitlab-reconfigure \ No newline at end of file
diff --git a/doc/user/project/deploy_tokens/index.md b/doc/user/project/deploy_tokens/index.md
index c09d5aeba8e..0b9b49f326f 100644
--- a/doc/user/project/deploy_tokens/index.md
+++ b/doc/user/project/deploy_tokens/index.md
@@ -5,7 +5,7 @@
Deploy tokens allow to download (through `git clone`), or read the container registry images of a project without the need of having a user and a password.
Please note, that the expiration of deploy tokens happens on the date you define,
-at midnight UTC and that they can be only managed by [masters](https://docs.gitlab.com/ee/user/permissions.html).
+at midnight UTC and that they can be only managed by [maintainers](https://docs.gitlab.com/ee/user/permissions.html).
## Creating a Deploy Token
diff --git a/doc/user/project/img/group_issue_board.png b/doc/user/project/img/group_issue_board.png
new file mode 100644
index 00000000000..be360d18540
--- /dev/null
+++ b/doc/user/project/img/group_issue_board.png
Binary files differ
diff --git a/doc/user/project/img/issue_board.png b/doc/user/project/img/issue_board.png
index 5f6dc9e4e8b..50e051e25a0 100644
--- a/doc/user/project/img/issue_board.png
+++ b/doc/user/project/img/issue_board.png
Binary files differ
diff --git a/doc/user/project/img/issue_board_add_list.png b/doc/user/project/img/issue_board_add_list.png
index 973d9f7cde4..91098daa1d1 100644
--- a/doc/user/project/img/issue_board_add_list.png
+++ b/doc/user/project/img/issue_board_add_list.png
Binary files differ
diff --git a/doc/user/project/img/issue_board_assignee_lists.png b/doc/user/project/img/issue_board_assignee_lists.png
new file mode 100644
index 00000000000..1ec94d22e33
--- /dev/null
+++ b/doc/user/project/img/issue_board_assignee_lists.png
Binary files differ
diff --git a/doc/user/project/img/issue_board_creation.png b/doc/user/project/img/issue_board_creation.png
new file mode 100644
index 00000000000..9dc4925b0a5
--- /dev/null
+++ b/doc/user/project/img/issue_board_creation.png
Binary files differ
diff --git a/doc/user/project/img/issue_board_edit_button.png b/doc/user/project/img/issue_board_edit_button.png
new file mode 100644
index 00000000000..23883175344
--- /dev/null
+++ b/doc/user/project/img/issue_board_edit_button.png
Binary files differ
diff --git a/doc/user/project/img/issue_board_focus_mode.gif b/doc/user/project/img/issue_board_focus_mode.gif
new file mode 100644
index 00000000000..9565bdb0865
--- /dev/null
+++ b/doc/user/project/img/issue_board_focus_mode.gif
Binary files differ
diff --git a/doc/user/project/img/issue_board_move_issue_card_list.png b/doc/user/project/img/issue_board_move_issue_card_list.png
index 3666dbb87ab..cce252234c1 100644
--- a/doc/user/project/img/issue_board_move_issue_card_list.png
+++ b/doc/user/project/img/issue_board_move_issue_card_list.png
Binary files differ
diff --git a/doc/user/project/img/issue_board_system_notes.png b/doc/user/project/img/issue_board_system_notes.png
index bd0f5f54095..c6ecb498198 100644
--- a/doc/user/project/img/issue_board_system_notes.png
+++ b/doc/user/project/img/issue_board_system_notes.png
Binary files differ
diff --git a/doc/user/project/img/issue_board_view_scope.png b/doc/user/project/img/issue_board_view_scope.png
new file mode 100644
index 00000000000..4e03cecbc2d
--- /dev/null
+++ b/doc/user/project/img/issue_board_view_scope.png
Binary files differ
diff --git a/doc/user/project/img/issue_board_welcome_message.png b/doc/user/project/img/issue_board_welcome_message.png
index 127b9b08cc7..357dff42488 100644
--- a/doc/user/project/img/issue_board_welcome_message.png
+++ b/doc/user/project/img/issue_board_welcome_message.png
Binary files differ
diff --git a/doc/user/project/img/issue_boards_add_issues_modal.png b/doc/user/project/img/issue_boards_add_issues_modal.png
index bedaf724a15..625a4304eaf 100644
--- a/doc/user/project/img/issue_boards_add_issues_modal.png
+++ b/doc/user/project/img/issue_boards_add_issues_modal.png
Binary files differ
diff --git a/doc/user/project/img/issue_boards_multiple.png b/doc/user/project/img/issue_boards_multiple.png
new file mode 100644
index 00000000000..4b2b8d457f1
--- /dev/null
+++ b/doc/user/project/img/issue_boards_multiple.png
Binary files differ
diff --git a/doc/user/project/img/issue_boards_remove_issue.png b/doc/user/project/img/issue_boards_remove_issue.png
index 8b3beca97cf..9a2fad2cc7f 100644
--- a/doc/user/project/img/issue_boards_remove_issue.png
+++ b/doc/user/project/img/issue_boards_remove_issue.png
Binary files differ
diff --git a/doc/user/project/import/bitbucket.md b/doc/user/project/import/bitbucket.md
index b22c7db0047..e3d625cc621 100644
--- a/doc/user/project/import/bitbucket.md
+++ b/doc/user/project/import/bitbucket.md
@@ -9,6 +9,10 @@ The [Bitbucket integration][bb-import] must be first enabled in order to be
able to import your projects from Bitbucket. Ask your GitLab administrator
to enable this if not already.
+>**Note:**
+The BitBucket importer currently only works with BitBucket's cloud offering
+(bitbucket.org) and does not work with BitBucket Server (aka Stash).
+
- At its current state, the Bitbucket importer can import:
- the repository description (GitLab 7.7+)
- the Git repository data (GitLab 7.7+)
diff --git a/doc/user/project/import/github.md b/doc/user/project/import/github.md
index 8c639bd5343..fcd6192e82f 100644
--- a/doc/user/project/import/github.md
+++ b/doc/user/project/import/github.md
@@ -1,154 +1,145 @@
# Import your project from GitHub to GitLab
-Import your projects from GitHub to GitLab with minimal effort.
+Using the importer, you can import your GitHub repositories to GitLab.com or to
+your self-hosted GitLab instance.
## Overview
->**Note:**
-If you are an administrator you can enable the [GitHub integration][gh-import]
-in your GitLab instance sitewide. This configuration is optional, users will
-still be able to import their GitHub repositories with a
-[personal access token][gh-token].
-
->**Note:**
-Administrators of a GitLab instance (Community or Enterprise Edition) can also
-use the [GitHub rake task][gh-rake] to import projects from GitHub without the
-constrains of a Sidekiq worker.
-
-- At its current state, GitHub importer can import:
- - the repository description (GitLab 7.7+)
- - the Git repository data (GitLab 7.7+)
- - the issues (GitLab 7.7+)
- - the pull requests (GitLab 8.4+)
- - the wiki pages (GitLab 8.4+)
- - the milestones (GitLab 8.7+)
- - the labels (GitLab 8.7+)
- - the release note descriptions (GitLab 8.12+)
- - 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.
+NOTE: **Note:**
+While these instructions will always work for users on GitLab.com, if you are an
+administrator of a self-hosted GitLab instance, you will need to enable the
+[GitHub integration][gh-import] in order for users to follow the preferred
+import method described on this page. If this is not enabled, users can alternatively import their
+GitHub repositories using a [personal access token](#using-a-github-token) from GitHub,
+but this method will not be able to associate all user activity (such as issues and pull requests)
+with matching GitLab users. As an administrator of a self-hosted GitLab instance, you can also use
+the [GitHub rake task](../../../administration/raketasks/github_import.md) to import projects from
+GitHub without the constraints of a Sidekiq worker.
+
+The following aspects of a project are imported:
+ * Repository description (GitLab.com & 7.7+)
+ * Git repository data (GitLab.com & 7.7+)
+ * Issues (GitLab.com & 7.7+)
+ * Pull requests (GitLab.com & 8.4+)
+ * Wiki pages (GitLab.com & 8.4+)
+ * Milestones (GitLab.com & 8.7+)
+ * Labels (GitLab.com & 8.7+)
+ * Release note descriptions (GitLab.com & 8.12+)
+ * Pull request review comments (GitLab.com & 10.2+)
+ * Regular issue and pull request comments
+
+References to pull requests and issues are preserved (GitLab.com & 8.7+), and
+each imported repository maintains visibility level unless that [visibility
+level is restricted](../../../public_access/public_access.md#restricting-the-use-of-public-or-internal-projects),
+in which case it defaults to the default project visibility.
## How it works
-When issues/pull requests are being imported, the GitHub importer tries to find
-the GitHub author/assignee in GitLab's database using the GitHub ID. For this
-to work, the GitHub author/assignee should have signed in beforehand in GitLab
-and **associated their GitHub account**. If the user is not
-found in GitLab's database, the project creator (most of the times the current
-user that started the import process) is set as the author, but a reference on
-the issue about the original GitHub author is kept.
+When issues and pull requests are being imported, the importer attempts to find their GitHub authors and
+assignees in the database of the GitLab instance (note that pull requests are called "merge requests" in GitLab).
-The importer will create any new namespaces (groups) if they don't exist or in
-the case the namespace is taken, the repository will be imported under the user's
-namespace that started the import process.
+For this association to succeed, prior to the import, each GitHub author and assignee in the repository must
+have either previously logged in to a GitLab account using the GitHub icon **or** have a GitHub account with
+a [public email address](https://help.github.com/articles/setting-your-commit-email-address-on-github/) that
+matches their GitLab account's email address.
-The importer will also import branches on forks of projects related to open pull
-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 discrepancy
-in branches compared to the GitHub Repository.
+If a user referenced in the project is not found in GitLab's database, the project creator (typically the user
+that initiated the import process) is set as the author/assignee, but a note on the issue mentioning the original
+GitHub author is added.
-For a more technical description and an overview of the architecture you can
-refer to [Working with the GitHub importer][gh-import-dev-docs].
+The importer creates any new namespaces (groups) if they do not exist, or, if the namespace is taken, the
+repository is imported under the namespace of the user who initiated the import process. The namespace/repository
+name can also be edited, with the proper permissions.
-## Importing your GitHub repositories
+The importer will also import branches on forks of projects related to open pull 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 discrepancy in branches compared to those of the GitHub repository.
-The importer page is visible when you create a new project.
+For additional technical details, you can refer to the
+[GitHub Importer](../../../development/github_importer.md "Working with the GitHub importer")
+developer documentation.
-![New project page on GitLab](img/import_projects_from_new_project_page.png)
+## Import your GitHub repository into GitLab
-Click on the **GitHub** link and the import authorization process will start.
-There are two ways to authorize access to your GitHub repositories:
+### Using the GitHub integration
-1. [Using the GitHub integration][gh-integration] (if it's enabled by your
- GitLab administrator). This is the preferred way as it's possible to
- preserve the GitHub authors/assignees. Read more in the [How it works](#how-it-works)
- section.
-1. [Using a personal access token][gh-token] provided by GitHub.
+Before you begin, ensure that any GitHub users who you want to map to GitLab users have either:
-![Select authentication method](img/import_projects_from_github_select_auth_method.png)
+1. A GitLab account that has logged in using the GitHub icon
+\- or -
+2. A GitLab account with an email address that matches the [public email address](https://help.github.com/articles/setting-your-commit-email-address-on-github/) of the GitHub user
-### Authorize access to your repositories using the GitHub integration
+User-matching attempts occur in that order, and if a user is not identified either way, the activity is associated with
+the user account that is performing the import.
-If the [GitHub integration][gh-import] is enabled by your GitLab administrator,
-you can use it instead of the personal access token.
+NOTE: **Note:**
+If you are using a self-hosted GitLab instance, this process requires that you have configured the
+[GitHub integration][gh-import].
-1. First you may want to connect your GitHub account to GitLab in order for
- the username mapping to be correct.
-1. Once you connect GitHub, click the **List your GitHub repositories** button
- and you will be redirected to GitHub for permission to access your projects.
-1. After accepting, you'll be automatically redirected to the importer.
+1. From the top navigation bar, click **+** and select **New project**.
+2. Select the **Import project** tab and then select **GitHub**.
+3. Select the first button to **List your GitHub repositories**. You are redirected to a page on github.com to authorize the GitLab application.
+4. Click **Authorize gitlabhq**. You are redirected back to GitLab's Import page and all of your GitHub repositories are listed.
+5. Continue on to [selecting which repositories to import](#selecting-which-repositories-to-import).
-You can now go on and [select which repositories to import](#select-which-repositories-to-import).
+### Using a GitHub token
-### Authorize access to your repositories using a personal access token
+NOTE: **Note:**
+For a proper author/assignee mapping for issues and pull requests, the [GitHub integration method (above)](#using-the-github-integration)
+should be used instead of the personal access token. If you are using GitLab.com or a self-hosted GitLab instance with the GitHub
+integration enabled, that should be the preferred method to import your repositories. Read more in the [How it works](#how-it-works) section.
->**Note:**
-For a proper author/assignee mapping for issues and pull requests, the
-[GitHub integration][gh-integration] should be used instead of the
-[personal access token][gh-token]. If the GitHub integration is enabled by your
-GitLab administrator, it should be the preferred method to import your repositories.
-Read more in the [How it works](#how-it-works) section.
+If you are not using the GitHub integration, you can still perform an authorization with GitHub to grant GitLab access your repositories:
-If you are not using the GitHub integration, you can still perform a one-off
-authorization with GitHub to grant GitLab access your repositories:
+1. Go to https://github.com/settings/tokens/new
+2. Enter a token description.
+3. Select the repo scope.
+4. Click **Generate token**.
+5. Copy the token hash.
+6. Go back to GitLab and provide the token to the GitHub importer.
+7. Hit the **List Your GitHub Repositories** button and wait while GitLab reads your repositories' information.
+ Once done, you'll be taken to the importer page to select the repositories to import.
-1. Go to <https://github.com/settings/tokens/new>.
-1. Enter a token description.
-1. Check the `repo` scope.
-1. Click **Generate token**.
-1. Copy the token hash.
-1. Go back to GitLab and provide the token to the GitHub importer.
-1. Hit the **List Your GitHub Repositories** button and wait while GitLab reads
- your repositories' information. Once done, you'll be taken to the importer
- page to select the repositories to import.
+### Selecting which repositories to import
-### Select which repositories to import
+After you have authorized access to your GitHub repositories, you are redirected to the GitHub importer page and
+your GitHub repositories are listed.
-After you've authorized access to your GitHub repositories, you will be
-redirected to the GitHub importer page.
+1. By default, the proposed repository namespaces match the names as they exist in GitHub, but based on your permissions,
+ you can choose to edit these names before you proceed to import any of them.
+2. Select the **Import** button next to any number of repositories, or select **Import all repositories**.
+3. The **Status** column shows the import status of each repository. You can choose to leave the page open and it will
+ update in realtime or you can return to it later.
+4. Once a repository has been imported, click its GitLab path to open its GitLab URL.
-From there, you can see the import statuses of your GitHub repositories.
+## Mirroring and pipeline status sharing
-- Those that are being imported will show a _started_ status,
-- those already successfully imported will be green with a _done_ status,
-- whereas those that are not yet imported will have an **Import** button on the
- right side of the table.
+Depending your GitLab tier, [project mirroring](../../../workflow/repository_mirroring.md) can be set up to keep
+your imported project in sync with its GitHub copy.
-If you want, you can import all your GitHub projects in one go by hitting
-**Import all projects** in the upper left corner.
+Additionally, you can configure GitLab to send pipeline status updates back GitHub with the
+[GitHub Project Integration](https://docs.gitlab.com/ee/user/project/integrations/github.html). **[PREMIUM]**
-![GitHub importer page](img/import_projects_from_github_importer.png)
+If you import your project using [CI/CD for external repo](https://docs.gitlab.com/ee/ci/ci_cd_for_external_repos/), then both
+of the above are automatically configured. **[PREMIUM]**
----
+## Improving the speed of imports on self-hosted instances
-You can also choose a different name for the project and a different namespace,
-if you have the privileges to do so.
+NOTE: **Note:**
+Admin access to the GitLab server is required.
-## 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:
+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.
+For an optimal experience, it's recommended having at least 4 Sidekiq processes (each running a number of threads equal
+to the number of CPU cores) that *only* process these queues. It's also recommended 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.
+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/img/import_projects_from_repo_url.png b/doc/user/project/import/img/import_projects_from_repo_url.png
index ec867da1087..c453c7e558a 100644
--- a/doc/user/project/import/img/import_projects_from_repo_url.png
+++ b/doc/user/project/import/img/import_projects_from_repo_url.png
Binary files differ
diff --git a/doc/user/project/index.md b/doc/user/project/index.md
index 557375a1da9..e63ed88249d 100644
--- a/doc/user/project/index.md
+++ b/doc/user/project/index.md
@@ -72,6 +72,8 @@ website with GitLab Pages
**Other features:**
+- [Wiki](wiki/index.md): Document your GitLab project in an integrated Wiki
+- [Snippets](../snippets.md): Store, share and collaborate on code snippets
- [Cycle Analytics](cycle_analytics.md): Review your development lifecycle
- [Syntax highlighting](highlighting.md): An alternative to customize
your code blocks, overriding GitLab's default choice of language
diff --git a/doc/user/project/integrations/bamboo.md b/doc/user/project/integrations/bamboo.md
index 1e28646bc97..9b18eb15599 100644
--- a/doc/user/project/integrations/bamboo.md
+++ b/doc/user/project/integrations/bamboo.md
@@ -41,8 +41,11 @@ service in GitLab.
1. Click 'Atlassian Bamboo CI'
1. Select the 'Active' checkbox.
1. Enter the base URL of your Bamboo server. 'https://bamboo.example.com'
-1. Enter the build key from your Bamboo build plan. Build keys are a short,
- all capital letter, identifier that is unique. It will be something like PR-BLD
+1. Enter the build key from your Bamboo build plan. Build keys are typically made
+ up from the Project Key and Plan Key that are set on project/plan creation and
+ separated with a dash (`-`), for example **PROJ-PLAN**. This is a short, all
+ uppercase identifier that is unique. When viewing a plan within Bamboo, the
+ build key is also shown in the browser URL, for example `https://bamboo.example.com/browse/PROJ-PLAN`.
1. If necessary, enter username and password for a Bamboo user that has
access to trigger the build plan. Leave these fields blank if you do not require
authentication.
diff --git a/doc/user/project/integrations/img/kubernetes_configuration.png b/doc/user/project/integrations/img/kubernetes_configuration.png
deleted file mode 100644
index e535e2b8d46..00000000000
--- a/doc/user/project/integrations/img/kubernetes_configuration.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/integrations/index.md b/doc/user/project/integrations/index.md
index e384ed57de9..363e994f36d 100644
--- a/doc/user/project/integrations/index.md
+++ b/doc/user/project/integrations/index.md
@@ -2,7 +2,7 @@
You can find the available integrations under your project's
**Settings âž” Integrations** page. You need to have at least
-[master permission][permissions] on the project.
+[maintainer permission][permissions] on the project.
## Project services
diff --git a/doc/user/project/integrations/kubernetes.md b/doc/user/project/integrations/kubernetes.md
index f502d1c9821..9342a2cbb00 100644
--- a/doc/user/project/integrations/kubernetes.md
+++ b/doc/user/project/integrations/kubernetes.md
@@ -1,137 +1 @@
----
-last_updated: 2017-12-28
----
-
-# GitLab Kubernetes / OpenShift integration
-
-CAUTION: **Warning:**
-The Kubernetes service integration has been deprecated in GitLab 10.3. If the
-service is active, the cluster information will still be editable, however we
-advise to disable and reconfigure the clusters using the new
-[Clusters](../clusters/index.md) page. If the service is inactive, the fields
-will not be editable. Read [GitLab 10.3 release post](https://about.gitlab.com/2017/12/22/gitlab-10-3-released/#kubernetes-integration-service) for more information.
-
-GitLab can be configured to interact with Kubernetes, or other systems using the
-Kubernetes API (such as OpenShift).
-
-Each project can be configured to connect to a different Kubernetes cluster, see
-the [configuration](#configuration) section.
-
-## Configuration
-
-Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
-of your project and select the **Kubernetes** service to configure it. Fill in
-all the needed parameters, check the "Active" checkbox and hit **Save changes**
-for the changes to take effect.
-
-![Kubernetes configuration settings](img/kubernetes_configuration.png)
-
-The Kubernetes service takes the following parameters:
-
-- **API URL** -
- It's the URL that GitLab uses to access the Kubernetes API. Kubernetes
- exposes several APIs, we want the "base" URL that is common to all of them,
- e.g., `https://kubernetes.example.com` rather than `https://kubernetes.example.com/api/v1`.
-- **CA certificate** (optional) -
- If the API is using a self-signed TLS certificate, you'll also need to include
- the `ca.crt` contents here.
-- **Project namespace** (optional) - The following apply:
- - By default you don't have to fill it in; by leaving it blank, GitLab will
- create one for you.
- - Each project should have a unique namespace.
- - The project namespace is not necessarily the namespace of the secret, if
- you're using a secret with broader permissions, like the secret from `default`.
- - You should **not** use `default` as the project namespace.
- - If you or someone created a secret specifically for the project, usually
- with limited permissions, the secret's namespace and project namespace may
- be the same.
-- **Token** -
- GitLab authenticates against Kubernetes using service tokens, which are
- scoped to a particular `namespace`. If you don't have a service token yet,
- you can follow the
- [Kubernetes documentation](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/)
- to create one. You can also view or create service tokens in the
- [Kubernetes dashboard](https://kubernetes.io/docs/tasks/access-application-cluster/web-ui-dashboard/#config)
- (under **Config > Secrets**).
-
-TIP: **Tip:**
-If you have a single cluster that you want to use for all your projects,
-you can pre-fill the settings page with a default template. To configure the
-template, see [Services Templates](services_templates.md).
-
-## Deployment variables
-
-The Kubernetes service exposes the following
-[deployment variables](../../../ci/variables/README.md#deployment-variables) in the
-GitLab CI/CD build environment:
-
-- `KUBE_URL` - Equal to the API URL.
-- `KUBE_TOKEN` - The Kubernetes token.
-- `KUBE_NAMESPACE` - The Kubernetes namespace is auto-generated if not specified.
- The default value is `<project_name>-<project_id>`. You can overwrite it to
- use different one if needed, otherwise the `KUBE_NAMESPACE` variable will
- receive the default value.
-- `KUBE_CA_PEM_FILE` - Only present if a custom CA bundle was specified. Path
- to a file containing PEM data.
-- `KUBE_CA_PEM` (deprecated) - Only if a custom CA bundle was specified. Raw PEM data.
-- `KUBECONFIG` - Path to a file containing `kubeconfig` for this deployment.
- CA bundle would be embedded if specified.
-
-## What you can get with the Kubernetes integration
-
-Here's what you can do with GitLab if you enable the Kubernetes integration.
-
-### Deploy Boards
-
-> Available in [GitLab Premium][ee].
-
-GitLab's Deploy Boards offer a consolidated view of the current health and
-status of each CI [environment](../../../ci/environments.md) running on Kubernetes,
-displaying the status of the pods in the deployment. Developers and other
-teammates can view the progress and status of a rollout, pod by pod, in the
-workflow they already use without any need to access Kubernetes.
-
-[> Read more about Deploy Boards](https://docs.gitlab.com/ee/user/project/deploy_boards.html)
-
-### Canary Deployments
-
-> Available in [GitLab Premium][ee].
-
-Leverage [Kubernetes' Canary deployments](https://kubernetes.io/docs/concepts/cluster-administration/manage-deployment/#canary-deployments)
-and visualize your canary deployments right inside the Deploy Board, without
-the need to leave GitLab.
-
-[> Read more about Canary Deployments](https://docs.gitlab.com/ee/user/project/canary_deployments.html)
-
-### Kubernetes monitoring
-
-Automatically detect and monitor Kubernetes metrics. Automatic monitoring of
-[NGINX ingress](./prometheus_library/nginx.md) is also supported.
-
-[> Read more about Kubernetes monitoring](prometheus_library/kubernetes.md)
-
-### Auto DevOps
-
-Auto DevOps automatically detects, builds, tests, deploys, and monitors your
-applications.
-
-To make full use of Auto DevOps(Auto Deploy, Auto Review Apps, and Auto Monitoring)
-you will need the Kubernetes project integration enabled.
-
-[> Read more about Auto DevOps](../../../topics/autodevops/index.md)
-
-### Web terminals
-
-NOTE: **Note:**
-Introduced in GitLab 8.15. You must be the project owner or have `master` permissions
-to use terminals. Support is limited to the first container in the
-first pod of your environment.
-
-When enabled, the Kubernetes service adds [web terminal](../../../ci/environments.md#web-terminals)
-support to your [environments](../../../ci/environments.md). This is based on the `exec` functionality found in
-Docker and Kubernetes, so you get a new shell session within your existing
-containers. To use this integration, you should deploy to Kubernetes using
-the deployment variables above, ensuring any pods you create are labelled with
-`app=$CI_ENVIRONMENT_SLUG`. GitLab will do the rest!
-
-[ee]: https://about.gitlab.com/products/
+This document was moved to [another location](../clusters/index.md).
diff --git a/doc/user/project/integrations/microsoft_teams.md b/doc/user/project/integrations/microsoft_teams.md
index eaad2d5138a..5cf80a298ad 100644
--- a/doc/user/project/integrations/microsoft_teams.md
+++ b/doc/user/project/integrations/microsoft_teams.md
@@ -2,7 +2,7 @@
## On Microsoft Teams
-To enable Microsoft Teams integration you must create an incoming webhook integration on Microsoft Teams by following the steps described in this [document](https://msdn.microsoft.com/en-us/microsoft-teams/connectors).
+To enable Microsoft Teams integration you must create an incoming webhook integration on Microsoft Teams by following the steps described in this [document](https://docs.microsoft.com/en-us/microsoftteams/platform/concepts/connectors#setting-up-a-custom-incoming-webhook).
## On GitLab
diff --git a/doc/user/project/integrations/project_services.md b/doc/user/project/integrations/project_services.md
index 074eeb729e3..8c51eb9915e 100644
--- a/doc/user/project/integrations/project_services.md
+++ b/doc/user/project/integrations/project_services.md
@@ -39,7 +39,6 @@ Click on the service links to see further configuration instructions and details
| [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway |
| [JIRA](jira.md) | JIRA issue tracker |
| JetBrains TeamCity CI | A continuous integration and build server |
-| [Kubernetes](kubernetes.md) _(Has been deprecated in GitLab 10.3)_ | A containerized deployment service |
| [Mattermost slash commands](mattermost_slash_commands.md) | Mattermost chat and ChatOps slash commands |
| [Mattermost Notifications](mattermost.md) | Receive event notifications in Mattermost |
| [Microsoft teams](microsoft_teams.md) | Receive notifications for actions that happen on GitLab into a room on Microsoft Teams using Office 365 Connectors |
diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md
index fa7e504c4aa..f687027e8c8 100644
--- a/doc/user/project/integrations/prometheus.md
+++ b/doc/user/project/integrations/prometheus.md
@@ -30,7 +30,7 @@ GitLab can seamlessly deploy and manage Prometheus on a [connected Kubernetes cl
Once you have a connected Kubernetes cluster with Helm installed, deploying a managed Prometheus is as easy as a single click.
-1. Go to the `CI/CD > Kubernetes` page, to view your connected clusters
+1. Go to the `Operations > Kubernetes` page, to view your connected clusters
1. Select the cluster you would like to deploy Prometheus to
1. Click the **Install** button to deploy Prometheus to the cluster
diff --git a/doc/user/project/integrations/prometheus_library/nginx_ingress.md b/doc/user/project/integrations/prometheus_library/nginx_ingress.md
index 590b1c4275a..a1db79538a4 100644
--- a/doc/user/project/integrations/prometheus_library/nginx_ingress.md
+++ b/doc/user/project/integrations/prometheus_library/nginx_ingress.md
@@ -14,7 +14,7 @@ GitLab has support for automatically detecting and monitoring the Kubernetes NGI
| ---- | ----- |
| Throughput (req/sec) | sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) by (status_code) |
| Latency (ms) | avg(nginx_upstream_response_msecs_avg{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}) |
-| HTTP Error Rate (HTTP Errors / sec) | sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) |
+| HTTP Error Rate (%) | sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) / sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) * 100 |
## Configuring NGINX ingress monitoring
diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md
index 19df78f4140..8c09927e2df 100644
--- a/doc/user/project/integrations/webhooks.md
+++ b/doc/user/project/integrations/webhooks.md
@@ -6,6 +6,10 @@ Starting from GitLab 8.5:
- the `project.ssh_url` key is deprecated in favor of the `project.git_ssh_url` key
- the `project.http_url` key is deprecated in favor of the `project.git_http_url` key
+>**Note:**
+Starting from GitLab 11.1, the logs of web hooks are automatically removed after
+one month.
+
Project 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. GitLab will send a POST request with data
@@ -54,11 +58,11 @@ Below are described the supported events.
Triggered when you push to the repository except when pushing tags.
-> **Note:** When more than 20 commits are pushed at once, the `commits` web hook
- attribute will only contain the first 20 for performance reasons. Loading
- detailed commit data is expensive. Note that despite only 20 commits being
+> **Note:** When more than 20 commits are pushed at once, the `commits` web hook
+ attribute will only contain the first 20 for performance reasons. Loading
+ detailed commit data is expensive. Note that despite only 20 commits being
present in the `commits` attribute, the `total_commits_count` attribute will
- contain the actual total.
+ contain the actual total.
**Request header**:
@@ -1149,11 +1153,11 @@ From this page, you can repeat delivery with the same data by clicking `Resend R
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
+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
+gitlab_rails['webhook_timeout'] = 10
```
## Example webhook receiver
diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md
index 7eab825fa32..e97b5d05529 100644
--- a/doc/user/project/issue_board.md
+++ b/doc/user/project/issue_board.md
@@ -1,16 +1,10 @@
-# Issue Board
+# Issue Boards
->**Note:**
-[Introduced][ce-5554] in [GitLab 8.11](https://about.gitlab.com/2016/08/22/gitlab-8-11-released/#issue-board).
+> [Introduced][ce-5554] in [GitLab 8.11](https://about.gitlab.com/2016/08/22/gitlab-8-11-released/#issue-board).
The GitLab Issue Board is a software project management tool used to plan,
organize, and visualize a workflow for a feature or product release.
-It can be seen like a light version of a [Kanban] or a [Scrum] board.
-
-Other interesting links:
-
-- [GitLab Issue Board landing page on about.gitlab.com][landing]
-- [YouTube video introduction to Issue Boards][youtube]
+It can be used as a [Kanban] or a [Scrum] board.
![GitLab Issue Board](img/issue_board.png)
@@ -18,7 +12,7 @@ Other interesting links:
The Issue Board builds on GitLab's existing
[issue tracking functionality](issues/index.md#issue-tracker) and
-leverages the power of [labels] by utilizing them as lists of the scrum board.
+leverages the power of [labels](labels.md) by utilizing them as lists of the scrum board.
With the Issue Board you can have a different view of your issues while
maintaining the same filtering and sorting abilities you see across the
@@ -33,15 +27,23 @@ You create issues, host code, perform reviews, build, test,
and deploy from one single platform. Issue Boards help you to visualize
and manage the entire process _in_ GitLab.
-With [Multiple Issue Boards](https://docs.gitlab.com/ee/user/project/issue_board.html#multiple-issue-boards), available
-only in [GitLab Ultimate](https://about.gitlab.com/products/),
+With [Multiple Issue Boards](#multiple-issue-boards), available
+only in [GitLab Enterprise Edition](#features-per-tier),
you go even further, as you can not only keep yourself and your project
organized from a broader perspective with one Issue Board per project,
but also allow your team members to organize their own workflow by creating
multiple Issue Boards within the same project.
+For a visual overview, see our [Issue Board feature page](https://about.gitlab.com/features/issueboard/)
+on about.gitlab.com or our [video introduction to Issue Boards](https://www.youtube.com/watch?v=UWsJ8tkHAa8).
+
## Use cases
+There are many ways to use GitLab Issue Boards tailored to your own preferred workflow.
+Here are some common use cases for Issue Boards.
+
+### Use cases for a single Issue Board
+
GitLab Workflow allows you to discuss proposals in issues, categorize them
with labels, and from there organize and prioritize them with Issue Boards.
@@ -65,33 +67,66 @@ beginning of the development lifecycle until deployed to production
![issue card moving](img/issue_board_move_issue_card_list.png)
-> **Notes:**
->
->- For a broader use case, please check the blog post
+### Use cases for Multiple Issue Boards
+
+With [Multiple Issue Boards](#multiple-issue-boards), available only in
+[GitLab Enterprise Edition](https://about.gitlab.com/pricing/),
+each team can have their own board to organize their workflow individually.
+
+#### Scrum team
+
+With multiple Issue Boards, each team has one board. Now you can move issues through each
+part of the process. For instance: **To Do**, **Doing**, and **Done**.
+
+#### Organization of topics
+
+Create lists to order things by topic and quickly change them between topics or groups,
+such as between **UX**, **Frontend**, and **Backend**. The changes will be reflected across boards,
+as changing lists will update the label accordingly.
+
+#### Advanced team handover
+
+For example, suppose we have a UX team with an Issue Board that contains:
+
+- **To Do**
+- **Doing**
+- **Frontend**
+
+When done with something, they move the card to **Frontend**. The Frontend team's board looks like:
+
+- **Frontend**
+- **Doing**
+- **Done**
+
+Cards finished by the UX team will automatically appear in the **Frontend** column when they're ready for them.
+
+NOTE: **Note:**
+For a broader use case, please see the blog post
[GitLab Workflow, an Overview](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/#gitlab-workflow-use-case-scenario).
->
->- For a real use case, please check why
+For a real use case example, you can read why
[Codepen decided to adopt Issue Boards](https://about.gitlab.com/2017/01/27/codepen-welcome-to-gitlab/#project-management-everything-in-one-place)
-to improve their workflow with [multiple boards](https://docs.gitlab.com/ee/user/project/issue_board.html#multiple-issue-boards).
+to improve their workflow with multiple boards.
-## Issue Board terminology
+#### Quick assignments
-Below is a table of the definitions used for GitLab's Issue Board.
+Create lists for each of your team members and quickly drag-and-drop issues onto each team member.
-| What we call it | What it means |
-| -------------- | ------------- |
-| **Issue Board** | It represents a different view for your issues. It can have multiple lists with each list consisting of issues represented by cards. |
-| **List** | Each label that exists in the issue tracker can have its own dedicated list. Every list is named after the label it is based on and is represented by a column which contains all the issues associated with that label. You can think of a list like the results you get when you filter the issues by a label in your issue tracker. |
-| **Card** | Every card represents an issue and it is shown under the list for which it has a label. The information you can see on a card consists of the issue number, the issue title, the assignee and the labels associated with it. You can drag cards around from one list to another. You can re-order cards within a list. |
+## Permissions
-There are two types of lists, the ones you create based on your labels, and
-two defaults:
+[Developers and up](../permissions.md) can use all the functionality of the
+Issue Board, that is, create or delete lists and drag issues from one list to another.
-- Label list: a list based on a label. It shows all opened issues with that label.
-- **Backlog** (default): shows all open issues that does not belong to one of lists. Always appears on the very left.
-- **Closed** (default): shows all closed issues. Always appears on the very right.
+## Issue Board terminology
-In short, here's a list of actions you can take in an Issue Board:
+- **Issue Board** - Each board represents a unique view for your issues. It can have multiple lists with each list consisting of issues represented by cards.
+- **List** - A column on the issue board that displays issues matching certain attributes. In addition to the default lists of 'Backlog' and 'Closed' issue, each additional list will show issues matching your chosen label or assignee.
+ - **Label list**: a list based on a label. It shows all opened issues with that label.
+ - **Assignee list**: a list which includes all issues assigned to a user.
+ - **Backlog** (default): shows all open issues that do not belong to one of the other lists. Always appears as the leftmost list.
+ - **Closed** (default): shows all closed issues. Always appears as the rightmost list.
+- **Card** - A box in the list that represents an individual issue. The information you can see on a card consists of the issue number, the issue title, the assignee, and the labels associated with the issue. You can drag cards from one list to another to change their label or assignee from that of the source list to that of the destination list.
+
+## Actions you can take on an Issue Board
- [Create a new list](#creating-a-new-list).
- [Delete an existing list](#deleting-a-list).
@@ -129,7 +164,7 @@ right corner of the Issue Board.
![Issue Board welcome message](img/issue_board_add_list.png)
-Simply choose the label to create the list from. The new list will be inserted
+Simply choose the label or user to create the list from. The new list will be inserted
at the end of the lists, before **Done**. Moving and reordering lists is as
easy as dragging them around.
@@ -174,17 +209,19 @@ to the system so that anybody who visits the same board later will see the reord
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].
+loads a board containing that issue), it will be ordered with
+respect to other issues in that list according to [Priority order](labels.md#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),
+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
@@ -205,8 +242,8 @@ something between lists by changing a label.
A typical workflow of using the Issue Board would be:
-1. You have [created][create-labels] and [prioritized][label-priority] labels
- so that you can easily categorize your issues.
+1. You have [created](labels.md#creating-labels) and [prioritized](labels.md#label-priority)
+ labels so that you can easily categorize your issues.
1. You have a bunch of issues (ideally labeled).
1. You visit the Issue Board and start [creating lists](#creating-a-new-list) to
create a workflow.
@@ -230,40 +267,116 @@ to another list the label changes and a system not is recorded.
![Issue Board system notes](img/issue_board_system_notes.png)
-## Permissions
+## Multiple Issue Boards **[STARTER]**
-[Developers and up](../permissions.md) can use all the functionality of the
-Issue Board, that is create/delete lists and drag issues around.
+> Introduced in [GitLab Enterprise Edition 8.13](https://about.gitlab.com/2016/10/22/gitlab-8-13-released/#multiple-issue-boards-ee).
+
+Multiple Issue Boards, as the name suggests, allow for more than one Issue Board
+for a given project or group. This is great for large projects with more than one team
+or in situations where a repository is used to host the code of multiple
+products.
-## Group Issue Board
+Clicking on the current board name in the upper left corner will reveal a
+menu from where you can create another Issue Board and rename or delete the
+existing one.
->Introduced in GitLab 10.6
+NOTE: **Note:**
+The Multiple Issue Boards feature is available for
+**projects in GitLab Starter Edition** and for **groups in GitLab Premium Edition**.
-Group issue board is analogous to project-level issue board and it is accessible at the group
-navigation level. A group-level issue board allows you to view all issues from all projects in that group or descendant subgroups. Similarly, you can only filter by group labels for these
+![Multiple Issue Boards](img/issue_boards_multiple.png)
+
+## Configurable Issue Boards **[STARTER]**
+
+> Introduced in [GitLab Starter Edition 10.2](https://about.gitlab.com/2017/11/22/gitlab-10-2-released/#issue-boards-configuration).
+
+An Issue Board can be associated with GitLab [Milestone](milestones/index.md#milestones),
+[Labels](labels.md), Assignee and Weight
+which will automatically filter the Board issues according to these fields.
+This allows you to create unique boards according to your team's need.
+
+![Create scoped board](img/issue_board_creation.png)
+
+You can define the scope of your board when creating it or by clicking on the "Edit board" button. Once a milestone, assignee or weight is assigned to an Issue Board, you will no longer be able to filter
+through these in the search bar. In order to do that, you need to remove the desired scope (e.g. milestone, assignee or weight) from the Issue Board.
+
+![Edit board configuration](img/issue_board_edit_button.png)
+
+If you don't have editing permission in a board, you're still able to see the configuration by clicking on "View scope".
+
+![Viewing board configuration](img/issue_board_view_scope.png)
+
+## Focus mode **[STARTER]**
+
+> Introduced in [GitLab Starter 9.1](https://about.gitlab.com/2017/04/22/gitlab-9-1-released/#issue-boards-focus-mode-ees-eep).
+
+Click the button at the top right to toggle focus mode on and off. In focus mode, the navigation UI is hidden, allowing you to focus on issues in the board.
+
+![Board focus mode](img/issue_board_focus_mode.gif)
+
+## Group Issue Boards **[PREMIUM]**
+
+> Introduced in [GitLab Premium 10.0](https://about.gitlab.com/2017/09/22/gitlab-10-0-released/#group-issue-boards).
+
+Accessible at the group navigation level, a group issue board offers the same features as a project-level board,
+but it can display issues from all projects in that
+group and its descendant subgroups. Similarly, you can only filter by group labels for these
boards. When updating milestones and labels for an issue through the sidebar update mechanism, again only
group-level objects are available.
+NOTE: **Note:**
+Multiple group issue boards were originally introduced in [GitLab 10.0 Premium](https://about.gitlab.com/2017/09/22/gitlab-10-0-released/#group-issue-boards) and
+one group issue board per group was made available in GitLab 10.6 Core.
+
+![Group issue board](img/group_issue_board.png)
+
+## Assignee lists **[PREMIUM]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5784) in GitLab 11.0 Premium.
+
+Like a regular list that shows all issues that have the list label, you can add
+an assignee list that shows all issues assigned to the given user.
+You can have a board with both label lists and assignee lists. To add an
+assignee list:
+
+1. Click **Add list**.
+1. Select the **Assignee list** tab.
+1. Search and click on the user you want to add as an assignee.
+
+Now that the assignee list is added, you can assign or unassign issues to that user
+by [dragging issues](#dragging-issues-between-lists) to and/or from an assignee list.
+To remove an assignee list, just as with a label list, click the trash icon.
+
+![Assignee lists](img/issue_board_assignee_lists.png)
+
+## Dragging issues between lists
+
+When dragging issues between lists, different behavior occurs depending on the source list and the target list.
+
+| | To Backlog | To Closed | To label `B` list | To assignee `Bob` list |
+| --- | --- | --- | --- | --- |
+| From Backlog | - | Issue closed | `B` added | `Bob` assigned |
+| From Closed | Issue reopened | - | Issue reopened<br/>`B` added | Issue reopened<br/>`Bob` assigned |
+| From label `A` list | `A` removed | Issue closed | `A` removed<br/>`B` added | `Bob` assigned |
+| From assignee `Alice` list | `Alice` unassigned | Issue closed | `B` added | `Alice` unassigned<br/>`Bob` assigned |
+
## Features per tier
Different issue board features are available in different [GitLab tiers](https://about.gitlab.com/pricing/), as shown in the following table:
-| Tier | Number of project issue boards | Board with configuration in project issue boards | Number of group issue boards | Board with configuration in group issue boards |
-| --- | --- | --- | --- | --- |
-| Core | 1 | No | 1 | No |
-| Starter | Multiple | Yes | 1 | No |
-| Premium | Multiple | Yes | Multiple | Yes |
-| Ultimate | Multiple | Yes | Multiple | Yes |
+| Tier | Number of Project Issue Boards | Number of Group Issue Boards | Configurable Project Issue Boards | Configurable Group Issue Boards | Assignee Lists
+| --- | --- | --- | --- | --- | --- |
+| Core | 1 | 1 | No | No | No |
+| Starter | Multiple | 1 | Yes | No | No |
+| Premium | Multiple | Multiple | Yes | Yes | Yes |
+| Ultimate | Multiple | Multiple | Yes | Yes | Yes |
## Tips
A few things to remember:
-- The label that corresponds to a list is hidden for issues under that list.
- Moving an issue between lists removes the label from the list it came from
and adds the label from the list it goes to.
-- When moving a card to **Done**, the label of the list it came from is removed
- and the issue gets closed.
- An issue can exist in multiple lists if it has more than one label.
- Lists are populated with issues automatically if the issues are labeled.
- Clicking on the issue title inside a card will take you to that issue.
@@ -274,10 +387,5 @@ A few things to remember:
20 will appear.
[ce-5554]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5554
-[labels]: ./labels.md
[scrum]: https://en.wikipedia.org/wiki/Scrum_(software_development)
[kanban]: https://en.wikipedia.org/wiki/Kanban_(development)
-[create-labels]: ./labels.md#create-new-labels
-[label-priority]: ./labels.md#prioritize-labels
-[landing]: https://about.gitlab.com/solutions/issueboard
-[youtube]: https://www.youtube.com/watch?v=UWsJ8tkHAa8
diff --git a/doc/user/project/issues/confidential_issues.md b/doc/user/project/issues/confidential_issues.md
index 0bf1f396f9d..8eada25234f 100644
--- a/doc/user/project/issues/confidential_issues.md
+++ b/doc/user/project/issues/confidential_issues.md
@@ -71,10 +71,10 @@ least [Reporter access][permissions]. However, a guest user can also create
confidential issues, but can only view the ones that they created themselves.
Confidential issues are also hidden in search results for unprivileged users.
-For example, here's what a user with Master and Guest access sees in the
+For example, here's what a user with Maintainer and Guest access sees in the
project's search results respectively.
-| Master access | Guest access |
+| Maintainer access | Guest access |
| :-----------: | :----------: |
| ![Confidential issues search master](img/confidential_issues_search_master.png) | ![Confidential issues search guest](img/confidential_issues_search_guest.png) |
diff --git a/doc/user/project/issues/deleting_issues.md b/doc/user/project/issues/deleting_issues.md
index d7442104c53..536a0de8974 100644
--- a/doc/user/project/issues/deleting_issues.md
+++ b/doc/user/project/issues/deleting_issues.md
@@ -8,4 +8,6 @@ You can delete an issue by editing it and clicking on the delete button.
![delete issue - button](img/delete_issue.png)
->**Note:** Only [project owners](../../permissions.md) can delete issues. \ No newline at end of file
+>**Note:** Only [project owners](../../permissions.md) can delete issues.
+
+[ce-2982]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2982 \ No newline at end of file
diff --git a/doc/user/project/issues/due_dates.md b/doc/user/project/issues/due_dates.md
index 1bf8b776c2e..93306437c6c 100644
--- a/doc/user/project/issues/due_dates.md
+++ b/doc/user/project/issues/due_dates.md
@@ -39,5 +39,13 @@ The day before an open issue is due, an email will be sent to all participants
of the issue. Both the due date and the day before are calculated using the
server's timezone.
+Issues with due dates can also be exported as an iCalendar feed. The URL of the
+feed can be added to calendar applications. The feed is accessible by clicking
+on the _Subscribe to calendar_ button on the following pages:
+- on the **Assigned Issues** page that is linked on the right-hand side of the
+ GitLab header
+- on the **Project Issues** page
+- on the **Group Issues** page
+
[ce-3614]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3614
[permissions]: ../../permissions.md#project
diff --git a/doc/user/project/issues/index.md b/doc/user/project/issues/index.md
index bf17731c523..d71273ba970 100644
--- a/doc/user/project/issues/index.md
+++ b/doc/user/project/issues/index.md
@@ -8,7 +8,7 @@ It allows you, your team, and your collaborators to share
and discuss proposals before and while implementing them.
GitLab Issues and the GitLab Issue Tracker are available in all
-[GitLab Products](https://about.gitlab.com/products/) as
+[GitLab Products](https://about.gitlab.com/pricing/) as
part of the [GitLab Workflow](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/).
## Use cases
@@ -35,7 +35,7 @@ your project public, open to collaboration.
### Streamline collaboration
With [Multiple Assignees for Issues](https://docs.gitlab.com/ee/user/project/issues/multiple_assignees_for_issues.html),
-available in [GitLab Starter](https://about.gitlab.com/products/)
+available in [GitLab Starter](https://about.gitlab.com/pricing/)
you can streamline collaboration and allow shared responsibilities to be clearly displayed.
All assignees are shown across your workflows and receive notifications (as they
would as single assignees), simplifying communication and ownership.
@@ -139,7 +139,7 @@ Find GitLab Issue Boards by navigating to your **Project's Dashboard** > **Issue
Read through the documentation for [Issue Boards](../issue_board.md)
to find out more about this feature.
-With [GitLab Starter](https://about.gitlab.com/products/), you can also
+With [GitLab Starter](https://about.gitlab.com/pricing/), you can also
create various boards per project with [Multiple Issue Boards](https://docs.gitlab.com/ee/user/project/issue_board.html#multiple-issue-boards).
### External Issue Tracker
diff --git a/doc/user/project/issues/issues_functionalities.md b/doc/user/project/issues/issues_functionalities.md
index e9903b01c82..46f25417fde 100644
--- a/doc/user/project/issues/issues_functionalities.md
+++ b/doc/user/project/issues/issues_functionalities.md
@@ -47,7 +47,7 @@ Often multiple people likely work on the same issue together,
which can especially be difficult to track in large teams
where there is shared ownership of an issue.
-In [GitLab Starter](https://about.gitlab.com/products/), you can also
+In [GitLab Starter](https://about.gitlab.com/pricing/), you can also
select multiple assignees to an issue.
Learn more on the [Multiple Assignees documentation](https://docs.gitlab.com/ee/user/project/issues/multiple_assignees_for_issues.html).
diff --git a/doc/user/project/members/index.md b/doc/user/project/members/index.md
index 43713855e26..2c2e8e2d556 100644
--- a/doc/user/project/members/index.md
+++ b/doc/user/project/members/index.md
@@ -4,7 +4,7 @@ You can manage the groups and users and their access levels in all of your
projects. You can also personalize the access level you give each user,
per-project.
-You should have `master` or `owner` [permissions](../../permissions.md) to add
+You should have Maintainer or Owner [permissions](../../permissions.md) to add
or import a new user to your project.
To view, edit, add, and remove project's members, go to your
@@ -43,7 +43,7 @@ level to the project.
You can import another project's users in your own project by hitting the
**Import members** button on the upper right corner of the **Members** menu.
-In the dropdown menu, you can see only the projects you are Master on.
+In the dropdown menu, you can see only the projects you are Maintainer on.
![Import members from another project](img/add_user_import_members_from_another_project.png)
@@ -99,7 +99,7 @@ side of your screen.
---
-Project owners & masters will be notified of your request and will be able to approve or
+Project owners & maintainers will be notified of your request and will be able to approve or
decline it on the members page.
![Manage access requests](img/access_requests_management.png)
diff --git a/doc/user/project/members/share_project_with_groups.md b/doc/user/project/members/share_project_with_groups.md
index 5d819998dd9..611ff0e6bfb 100644
--- a/doc/user/project/members/share_project_with_groups.md
+++ b/doc/user/project/members/share_project_with_groups.md
@@ -42,7 +42,7 @@ Admins are able to share projects with any group in the system.
## Maximum access level
-In the example above, the maximum access level of 'Developer' for members from 'Engineering' means that users with higher access levels in 'Engineering' ('Master' or 'Owner') will only have 'Developer' access to 'Project Acme'.
+In the example above, the maximum access level of 'Developer' for members from 'Engineering' means that users with higher access levels in 'Engineering' ('Maintainer' or 'Owner') will only have 'Developer' access to 'Project Acme'.
## Share project with group lock
diff --git a/doc/user/project/merge_requests/allow_collaboration.md b/doc/user/project/merge_requests/allow_collaboration.md
new file mode 100644
index 00000000000..859ac92ef89
--- /dev/null
+++ b/doc/user/project/merge_requests/allow_collaboration.md
@@ -0,0 +1,20 @@
+# Allow collaboration on merge requests across forks
+
+> [Introduced][ce-17395] in GitLab 10.6.
+
+This feature is available for merge requests across forked projects that are
+publicly accessible. It makes it easier for members of projects to
+collaborate on merge requests across forks.
+
+When enabled for a merge request, members with merge access to the target
+branch of the project will be granted write permissions to the source branch
+of the merge request.
+
+The feature can only be enabled by users who already have push access to the
+source project, and only lasts while the merge request is open.
+
+Enable this functionality while creating or editing a merge request:
+
+![Enable collaboration](./img/allow_collaboration.png)
+
+[ce-17395]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17395
diff --git a/doc/user/project/merge_requests/authorization_for_merge_requests.md b/doc/user/project/merge_requests/authorization_for_merge_requests.md
index 59b3fe7242c..79444ee5682 100644
--- a/doc/user/project/merge_requests/authorization_for_merge_requests.md
+++ b/doc/user/project/merge_requests/authorization_for_merge_requests.md
@@ -9,7 +9,7 @@ There are two main ways to have a merge request flow with GitLab:
With the protected branch flow everybody works within the same GitLab project.
-The project maintainers get Master access and the regular developers get
+The project maintainers get Maintainer access and the regular developers get
Developer access.
The maintainers mark the authoritative branches as 'Protected'.
@@ -18,7 +18,7 @@ The developers push feature branches to the project and create merge requests
to have their feature branches reviewed and merged into one of the protected
branches.
-By default, only users with Master access can merge changes into a protected
+By default, only users with Maintainer access can merge changes into a protected
branch.
**Advantages**
@@ -32,7 +32,7 @@ branch.
## Forking workflow
-With the forking workflow the maintainers get Master access and the regular
+With the forking workflow the maintainers get Maintainer access and the regular
developers get Reporter access to the authoritative repository, which prohibits
them from pushing any changes to it.
diff --git a/doc/user/project/merge_requests/img/allow_collaboration.png b/doc/user/project/merge_requests/img/allow_collaboration.png
new file mode 100644
index 00000000000..75596e7d9ad
--- /dev/null
+++ b/doc/user/project/merge_requests/img/allow_collaboration.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/allow_maintainer_push.png b/doc/user/project/merge_requests/img/allow_maintainer_push.png
deleted file mode 100644
index 91cc399f4ff..00000000000
--- a/doc/user/project/merge_requests/img/allow_maintainer_push.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/merge_requests/img/squash_edit_form.png b/doc/user/project/merge_requests/img/squash_edit_form.png
new file mode 100644
index 00000000000..496c6f44ea7
--- /dev/null
+++ b/doc/user/project/merge_requests/img/squash_edit_form.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/squash_mr_commits.png b/doc/user/project/merge_requests/img/squash_mr_commits.png
new file mode 100644
index 00000000000..5fc6a8c48bb
--- /dev/null
+++ b/doc/user/project/merge_requests/img/squash_mr_commits.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/squash_mr_widget.png b/doc/user/project/merge_requests/img/squash_mr_widget.png
new file mode 100644
index 00000000000..9cb458b2a35
--- /dev/null
+++ b/doc/user/project/merge_requests/img/squash_mr_widget.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/squash_squashed_commit.png b/doc/user/project/merge_requests/img/squash_squashed_commit.png
new file mode 100644
index 00000000000..0cf5875f82c
--- /dev/null
+++ b/doc/user/project/merge_requests/img/squash_squashed_commit.png
Binary files differ
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index 5932f5a2bc1..86ecf33ed31 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -28,14 +28,14 @@ With GitLab merge requests, you can:
- Enable [fast-forward merge requests](#fast-forward-merge-requests)
- Enable [semi-linear history merge requests](#semi-linear-history-merge-requests) as another security layer to guarantee the pipeline is passing in the target branch
- [Create new merge requests by email](#create-new-merge-requests-by-email)
-- Allow maintainers of the target project to push directly to the fork by [allowing edits from maintainers](maintainer_access.md)
+- [Allow collaboration](allow_collaboration.md) so members of the target project can push directly to the fork
+- [Squash and merge](squash_and_merge.md) for a cleaner commit history
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) **[PREMIUM]**
- Request [approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) from your managers **[STARTER]**
-- [Squash and merge](https://docs.gitlab.com/ee/user/project/merge_requests/squash_and_merge.html) for a cleaner commit history **[STARTER]**
-- Analyze the impact of your changes with [Code Quality reports](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html) **[STARTER]**
+- Analyze the impact of your changes with [Code Quality reports](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality.html) **[STARTER]**
## Use cases
@@ -43,7 +43,7 @@ A. Consider you are a software developer working in a team:
1. You checkout a new branch, and submit your changes through a merge request
1. You gather feedback from your team
-1. You work on the implementation optimizing code with [Code Quality reports](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html) **[STARTER]**
+1. You work on the implementation optimizing code with [Code Quality reports](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality.html) **[STARTER]**
1. You build and test your changes with GitLab CI/CD
1. You request the approval from your manager
1. Your manager pushes a commit with his final review, [approves the merge request](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html), and set it to [merge when pipeline succeeds](#merge-when-pipeline-succeeds) (Merge Request Approvals are available in GitLab Starter)
@@ -57,7 +57,7 @@ B. Consider you're a web developer writing a webpage for your company's:
1. Your changes are previewed with [Review Apps](../../../ci/review_apps/index.md)
1. You request your web designers for their implementation
1. You request the [approval](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) from your manager **[STARTER]**
-1. Once approved, your merge request is [squashed and merged](https://docs.gitlab.com/ee/user/project/merge_requests/squash_and_merge.html), and [deployed to staging with GitLab Pages](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/) (Squash and Merge is available in GitLab Starter)
+1. Once approved, your merge request is [squashed and merged](squash_and_merge.md), and [deployed to staging with GitLab Pages](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/)
1. Your production team [cherry picks](#cherry-pick-changes) the merge commit into production
## Merge requests per project
@@ -85,7 +85,7 @@ request is merged.
This option is also visible in an existing merge request next to the merge
request button and can be selected/deselected before merging. It's only visible
-to users with [Master permissions](../../permissions.md) in the source project.
+to users with [Maintainer permissions](../../permissions.md) in the source project.
If the user viewing the merge request does not have the correct permissions to
remove the source branch and the source branch is set for removal, the merge
@@ -325,4 +325,4 @@ git checkout origin/merge-requests/1
```
[protected branches]: ../protected_branches.md
-[ee]: https://about.gitlab.com/products/ "GitLab Enterprise Edition"
+[ee]: https://about.gitlab.com/pricing/ "GitLab Enterprise Edition"
diff --git a/doc/user/project/merge_requests/maintainer_access.md b/doc/user/project/merge_requests/maintainer_access.md
index 89f71e16a50..d59afecd375 100644
--- a/doc/user/project/merge_requests/maintainer_access.md
+++ b/doc/user/project/merge_requests/maintainer_access.md
@@ -1,20 +1 @@
-# Allow maintainer pushes for merge requests across forks
-
-> [Introduced][ce-17395] in GitLab 10.6.
-
-This feature is available for merge requests across forked projects that are
-publicly accessible. It makes it easier for maintainers of projects to
-collaborate on merge requests across forks.
-
-When enabled for a merge request, members with merge access to the target
-branch of the project will be granted write permissions to the source branch
-of the merge request.
-
-The feature can only be enabled by users who already have push access to the
-source project, and only lasts while the merge request is open.
-
-Enable this functionality while creating a merge request:
-
-![Enable maintainer edits](./img/allow_maintainer_push.png)
-
-[ce-17395]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17395
+This document was moved to [another location](allow_collaboration.md).
diff --git a/doc/user/project/merge_requests/squash_and_merge.md b/doc/user/project/merge_requests/squash_and_merge.md
new file mode 100644
index 00000000000..2ec423dcf70
--- /dev/null
+++ b/doc/user/project/merge_requests/squash_and_merge.md
@@ -0,0 +1,80 @@
+# Squash and merge
+
+> [Introduced][ee-1024] in [GitLab Starter][ee] 8.17, and in [GitLab Core][ce] [11.0][ce-18956].
+
+Combine all commits of your merge request into one and retain a clean history.
+
+## Overview
+
+Squashing lets you tidy up the commit history of a branch when accepting a merge
+request. It applies all of the changes in the merge request as a single commit,
+and then merges that commit using the merge method set for the project.
+
+In other words, squashing a merge request turns a long list of commits:
+
+![List of commits from a merge request][mr-commits]
+
+Into a single commit on merge:
+
+![A squashed commit followed by a merge commit][squashed-commit]
+
+The squashed commit's commit message is the merge request title. And note that
+the squashed commit is still followed by a merge commit, as the merge
+method for this example repository uses a merge commit. Squashing also works
+with the fast-forward merge strategy, see
+[squashing and fast-forward merge](#squash-and-fast-forward-merge) for more
+details.
+
+## Use cases
+
+When working on a feature branch, you sometimes want to commit your current
+progress, but don't really care about the commit messages. Those 'work in
+progress commits' don't necessarily contain important information and as such
+you'd rather not include them in your target branch.
+
+With squash and merge, when the merge request is ready to be merged,
+all you have to do is enable squashing before you press merge to join
+the commits include in the merge request into a single commit.
+
+This way, the history of your base branch remains clean with
+meaningful commit messages and is simpler to [revert] if necessary.
+
+## Enabling squash for a merge request
+
+Anyone who can create or edit a merge request can choose for it to be squashed
+on the merge request form:
+
+![Squash commits checkbox on edit form][squash-edit-form]
+
+---
+
+This can then be overridden at the time of accepting the merge request:
+
+![Squash commits checkbox on accept merge request form][squash-mr-widget]
+
+## Commit metadata for squashed commits
+
+The squashed commit has the following metadata:
+
+* Message: the title of the merge request.
+* Author: the author of the merge request.
+* Committer: the user who initiated the squash.
+
+## Squash and fast-forward merge
+
+When a project has the [fast-forward merge setting enabled][ff-merge], the merge
+request must be able to be fast-forwarded without squashing in order to squash
+it. This is because squashing is only available when accepting a merge request,
+so a merge request may need to be rebased before squashing, even though
+squashing can itself be considered equivalent to rebasing.
+
+[ee-1024]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1024
+[ce-18956]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18956
+[mr-commits]: img/squash_mr_commits.png
+[squashed-commit]: img/squash_squashed_commit.png
+[squash-edit-form]: img/squash_edit_form.png
+[squash-mr-widget]: img/squash_mr_widget.png
+[ff-merge]: fast_forward_merge.md#enabling-fast-forward-merges
+[ce]: https://about.gitlab.com/pricing/
+[ee]: https://about.gitlab.com/pricing/
+[revert]: revert_changes.md
diff --git a/doc/user/project/milestones/index.md b/doc/user/project/milestones/index.md
index 64bb33be547..632253db94c 100644
--- a/doc/user/project/milestones/index.md
+++ b/doc/user/project/milestones/index.md
@@ -10,7 +10,6 @@ Milestones allow you to organize issues and merge requests into a cohesive group
- **Project milestones** can be assigned to issues or merge requests in that project only.
- **Group milestones** can be assigned to any issue or merge request of any project in that group.
-- In the [future](https://gitlab.com/gitlab-org/gitlab-ce/issues/36862), you will be able to assign group milestones to issues and merge requests of projects in [subgroups](../../group/subgroups/index.md).
## Creating milestones
diff --git a/doc/user/project/pages/index.md b/doc/user/project/pages/index.md
index 4b5c2539c4b..205f0283107 100644
--- a/doc/user/project/pages/index.md
+++ b/doc/user/project/pages/index.md
@@ -42,7 +42,7 @@ to secure them.
Your files live in a project [repository](../repository/index.md) on GitLab.
[GitLab CI](../../../ci/README.md) picks up those files and makes them available at, typically,
-`http://<username>.gilab.io/<projectname>`. Please read through the docs on
+`https://<username>.gitlab.io/<projectname>`. Please read through the docs on
[GitLab Pages domains](getting_started_part_one.md#gitlab-pages-domain) for more info.
## Explore GitLab Pages
diff --git a/doc/user/project/protected_branches.md b/doc/user/project/protected_branches.md
index 0cbb0c878c2..3bf63a22963 100644
--- a/doc/user/project/protected_branches.md
+++ b/doc/user/project/protected_branches.md
@@ -10,8 +10,8 @@ created protected branches.
By default, a protected branch does four simple things:
- it prevents its creation, if not already created, from everybody except users
- with Master permission
-- it prevents pushes from everybody except users with Master permission
+ with Maintainer permission
+- it prevents pushes from everybody except users with Maintainer permission
- it prevents **anyone** from force pushing to the branch
- it prevents **anyone** from deleting the branch
@@ -24,7 +24,7 @@ See the [Changelog](#changelog) section for changes over time.
## Configuring protected branches
-To protect a branch, you need to have at least Master permission level. Note
+To protect a branch, you need to have at least Maintainer permission level. Note
that the `master` branch is protected by default.
1. Navigate to your project's **Settings âž” Repository**
@@ -45,19 +45,19 @@ that the `master` branch is protected by default.
Since GitLab 8.11, we added another layer of branch protection which provides
more granular management of protected branches. The "Developers can push"
option was replaced by an "Allowed to push" setting which can be set to
-allow/prohibit Masters and/or Developers to push to a protected branch.
+allow/prohibit Maintainers and/or Developers to push to a protected branch.
Using the "Allowed to push" and "Allowed to merge" settings, you can control
the actions that different roles can perform with the protected branch.
For example, you could set "Allowed to push" to "No one", and "Allowed to merge"
-to "Developers + Masters", to require _everyone_ to submit a merge request for
+to "Developers + Maintainers", to require _everyone_ to submit a merge request for
changes going into the protected branch. This is compatible with workflows like
the [GitLab workflow](../../workflow/gitlab_flow.md).
However, there are workflows where that is not needed, and only protecting from
force pushes and branch removal is useful. For those workflows, you can allow
everyone with write access to push to a protected branch by setting
-"Allowed to push" to "Developers + Masters".
+"Allowed to push" to "Developers + Maintainers".
You can set the "Allowed to push" and "Allowed to merge" options while creating
a protected branch or afterwards by selecting the option you want from the
@@ -66,7 +66,7 @@ dropdown list in the "Already protected" area.
![Developers can push](img/protected_branches_devs_can_push.png)
If you don't choose any of those options while creating a protected branch,
-they are set to "Masters" by default.
+they are set to "Maintainers" by default.
## Wildcard protected branches
@@ -101,7 +101,7 @@ all matching branches:
From time to time, it may be required to delete or clean up branches that are
protected.
-User with [Master permissions][perm] and up can manually delete protected
+User with [Maintainer permissions][perm] and up can manually delete protected
branches via GitLab's web interface:
1. Visit **Repository > Branches**
diff --git a/doc/user/project/protected_tags.md b/doc/user/project/protected_tags.md
index 0cb7aefdb2f..a5eaf2e9835 100644
--- a/doc/user/project/protected_tags.md
+++ b/doc/user/project/protected_tags.md
@@ -8,12 +8,12 @@ This feature evolved out of [Protected Branches](protected_branches.md)
## Overview
-Protected tags will prevent anyone from updating or deleting the tag, as and will prevent creation of matching tags based on the permissions you have selected. By default, anyone without Master permission will be prevented from creating tags.
+Protected tags will prevent anyone from updating or deleting the tag, as and will prevent creation of matching tags based on the permissions you have selected. By default, anyone without Maintainer permission will be prevented from creating tags.
## Configuring protected tags
-To protect a tag, you need to have at least Master permission level.
+To protect a tag, you need to have at least Maintainer permission level.
1. Navigate to the project's Settings -> Repository page
diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md
index 2f4ed3493c2..0ef8eddad20 100644
--- a/doc/user/project/quick_actions.md
+++ b/doc/user/project/quick_actions.md
@@ -42,3 +42,4 @@ do.
| `/tableflip` | Append the comment with `(╯°□°)╯︵ â”»â”â”»` |
| `/shrug` | Append the comment with `¯\_(ツ)_/¯` |
| <code>/copy_metadata #issue &#124; !merge_request</code> | Copy labels and milestone from other issue or merge request |
+| `/confidential` | Makes the issue confidential | \ No newline at end of file
diff --git a/doc/user/project/repository/index.md b/doc/user/project/repository/index.md
index 376f4e3cbe4..704c1777e62 100644
--- a/doc/user/project/repository/index.md
+++ b/doc/user/project/repository/index.md
@@ -82,7 +82,7 @@ your implementation with your team.
You can live preview changes submitted to a new branch with
[Review Apps](../../../ci/review_apps/index.md).
-With [GitLab Starter](https://about.gitlab.com/products/), you can also request
+With [GitLab Starter](https://about.gitlab.com/pricing/), you can also request
[approval](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) from your managers.
To create, delete, and [branches](branches/index.md) via GitLab's UI:
@@ -165,15 +165,23 @@ Find it under your project's **Repository > Compare**.
## Locked files
-> Available in [GitLab Premium](https://about.gitlab.com/products/).
+> Available in [GitLab Premium](https://about.gitlab.com/pricing/).
Lock your files to prevent any conflicting changes.
[File Locking](https://docs.gitlab.com/ee/user/project/file_lock.html) is available only in
-[GitLab Premium](https://about.gitlab.com/products/).
+[GitLab Premium](https://about.gitlab.com/pricing/).
## Repository's API
You can access your repos via [repository API](../../../api/repositories.md).
+## Clone in Apple Xcode
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/45820) in GitLab 11.0
+
+Projects that contain a `.xcodeproj` or `.xcworkspace` directory can now be cloned
+in Xcode using the new **Open in Xcode** button, located next to the Git URL
+used for cloning your project. The button is only shown on macOS.
+
[jupyter]: https://jupyter.org
diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md
index b8bac01959e..f94671fcf87 100644
--- a/doc/user/project/settings/import_export.md
+++ b/doc/user/project/settings/import_export.md
@@ -19,7 +19,7 @@
> - The exports are stored in a temporary [shared directory][tmp] and are deleted
> every 24 hours by a specific worker.
> - Group members will get exported as project members, as long as the user has
-> master or admin access to the group where the exported project lives. An admin
+> maintainer or admin access to the group where the exported project lives. An admin
> in the import side is required to map the users, based on email or username.
> Otherwise, a supplementary comment is left to mention the original author and
> the MRs, notes or issues will be owned by the importer.
@@ -32,7 +32,8 @@ with all their related data and be moved into a new GitLab instance.
| GitLab version | Import/Export version |
| ---------------- | --------------------- |
-| 10.8 to current | 0.2.3 |
+| 11.1 to current | 0.2.4 |
+| 10.8 | 0.2.3 |
| 10.4 | 0.2.2 |
| 10.3 | 0.2.1 |
| 10.0 | 0.2.0 |
diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md
index c9d2f8dc32d..084d1161633 100644
--- a/doc/user/project/settings/index.md
+++ b/doc/user/project/settings/index.md
@@ -1,7 +1,7 @@
# Project settings
NOTE: **Note:**
-Only project Masters and Admin users have the [permissions] to access a project
+Only project Maintainers and Admin users have the [permissions] to access a project
settings.
You can adjust your [project](../index.md) settings by navigating
@@ -42,7 +42,7 @@ Set up your project's merge request settings:
### Service Desk
-Enable [Service Desk](https://docs.gitlab.com/ee/user/project/service_desk.html) for your project to offer customer support. Service Desk is available in [GitLab Premium](https://about.gitlab.com/products/).
+Enable [Service Desk](https://docs.gitlab.com/ee/user/project/service_desk.html) for your project to offer customer support. Service Desk is available in [GitLab Premium](https://about.gitlab.com/pricing/).
### Export project
@@ -74,7 +74,7 @@ To archive a project:
#### Renaming a repository
NOTE: **Note:**
-Only project Masters and Admin users have the [permissions] to rename a
+Only project Maintainers and Admin users have the [permissions] to rename a
repository. Not to be confused with a project's name where it can also be
changed from the [general project settings](#general-project-settings).
@@ -98,7 +98,7 @@ Only project Owners and Admin users have the [permissions] to transfer a project
You can transfer an existing project into a [group](../../group/index.md) if:
-1. you have at least **Master** [permissions] to that group
+1. you have at least **Maintainer** [permissions] to that group
1. you are an **Owner** of the project.
Similarly, if you are an owner of a group, you can transfer any of its projects
diff --git a/doc/user/project/web_ide/index.md b/doc/user/project/web_ide/index.md
index 105d8a6ab61..b0143e45ab6 100644
--- a/doc/user/project/web_ide/index.md
+++ b/doc/user/project/web_ide/index.md
@@ -42,5 +42,26 @@ list.
An additional review mode is available when you open a merge request, which
shows you a preview of the merge request diff if you commit your changes.
+## View CI job logs
+
+> [Introduced in](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/19279) [GitLab Core][ce] 11.0.
+
+The Web IDE can be used to quickly fix failing tests by opening the branch or
+merge request in the Web IDE and opening the logs of the failed job. The status
+of all jobs for the most recent pipeline and job traces for the current commit
+can be accessed by clicking the **Pipelines** button in the top right.
+
+The pipeline status is also shown at all times in the status bar in the bottom
+left.
+
+## Switching merge requests
+
+> [Introduced in](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/19318) [GitLab Core][ce] 11.0.
+
+Switching between your authored and assigned merge requests can be done without
+leaving the Web IDE. Click the project name in the top left to open a list of
+merge requests. You will need to commit or discard all your changes before
+switching to a different merge request.
+
[ce]: https://about.gitlab.com/pricing/
[ee]: https://about.gitlab.com/pricing/
diff --git a/doc/user/reserved_names.md b/doc/user/reserved_names.md
index 50ec99be48b..918daee5d9f 100644
--- a/doc/user/reserved_names.md
+++ b/doc/user/reserved_names.md
@@ -59,6 +59,7 @@ Currently the following names are reserved as top level groups:
- deploy.html
- explore
- favicon.ico
+- favicon.png
- groups
- header_logo_dark.png
- header_logo_light.png
diff --git a/doc/user/snippets.md b/doc/user/snippets.md
index 2170b079f62..7efb6bafee7 100644
--- a/doc/user/snippets.md
+++ b/doc/user/snippets.md
@@ -1,29 +1,79 @@
# Snippets
-Snippets are little bits of code or text.
+With GitLab Snippets you can store and share bits of code and text with other users.
![GitLab Snippet](img/gitlab_snippet.png)
-There are 2 types of snippets - project snippets and personal snippets.
+There are 2 types of snippets, personal snippets and project snippets.
-## Comments
-
-With GitLab Snippets you engage in a conversation about that piece of code,
-facilitating the collaboration among users.
+## Personal snippets
-> **Note:**
-Comments on snippets was [introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/12910) in [GitLab Community Edition 9.2](https://about.gitlab.com/2017/05/22/gitlab-9-2-released/#comments-for-personal-snippets).
+Personal snippets are not related to any project and can be created completely
+independently. There are 3 visibility levels that can be set, public, internal
+and private. See [Public access](../public_access/public_access.md) for more information.
## Project snippets
-Project snippets are always related to a specific project - see [Project's features](project/index.md#project-39-s-features) for more information.
+Project snippets are always related to a specific project.
+See [Project's features](project/index.md#project-39-s-features) for more information.
-## Personal snippets
+## Discover snippets
+
+There are two main ways of how you can discover snippets in GitLab.
-Personal snippets are not related to any project and can be created completely independently. There are 3 visibility levels that can be set (public, internal, private - see [Public Access](../public_access/public_access.md) for more information).
+For exploring all snippets that are visible to you, you can go to the Snippets
+dashboard of your GitLab instance via the top navigation. For GitLab.com you can
+find it [here](https://gitlab.com/dashboard/snippets). This navigates you to an
+overview that shows snippets you created and allows you to explore all snippets.
+
+If you want to discover snippets that belong to a specific project, you can navigate
+to the Snippets page via the left side navigation on the project page.
+
+## Snippet comments
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/12910) in GitLab 9.2.
+
+With GitLab Snippets you engage in a conversation about that piece of code,
+facilitating the collaboration among users.
## Downloading snippets
You can download the raw content of a snippet.
-By default snippets will be downloaded with Linux-style line endings (`LF`). If you want to preserve the original line endings you need to add a parameter `line_ending=raw` (eg. `https://gitlab.com/snippets/SNIPPET_ID/raw?line_ending=raw`). In case a snippet was created using the GitLab web interface the original line ending is Windows-like (`CRLF`).
+By default snippets will be downloaded with Linux-style line endings (`LF`). If
+you want to preserve the original line endings you need to add a parameter `line_ending=raw`
+(e.g., `https://gitlab.com/snippets/SNIPPET_ID/raw?line_ending=raw`). In case a
+snippet was created using the GitLab web interface the original line ending is Windows-like (`CRLF`).
+
+## Embedded snippets
+
+> Introduced in GitLab 10.8.
+
+Public snippets can not only be shared, but also embedded on any website. This
+allows to reuse a GitLab snippet in multiple places and any change to the source
+is automatically reflected in the embedded snippet.
+
+To embed a snippet, first make sure that:
+
+- The project is public (if it's a project snippet)
+- The snippet is public
+- In **Project > Settings > Permissions**, the snippets permissions are
+ set to **Everyone with access**
+
+Once the above conditions are met, the "Embed" section will appear in your snippet
+where you can simply click on the "Copy to clipboard" button. This copies a one-line
+script that you can add to any website or blog post.
+
+Here's how an example code looks like:
+
+```html
+<script src="https://gitlab.com/namespace/project/snippets/SNIPPET_ID.js"></script>
+```
+
+Here's how an embedded snippet looks like:
+
+<script src="https://gitlab.com/gitlab-org/gitlab-ce/snippets/1717978.js"></script>
+
+Embedded snippets are displayed with a header that shows the file name if defined,
+the snippet size, a link to GitLab, and the actual snippet content. Actions in
+the header allow users to see the snippet in raw format and download it.
diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md
index 23b67310d25..a7313082fac 100644
--- a/doc/workflow/gitlab_flow.md
+++ b/doc/workflow/gitlab_flow.md
@@ -131,7 +131,7 @@ There is room for more feedback and after the assigned person feels comfortable
If the assigned person does not feel comfortable they can close the merge request without merging.
In GitLab it is common to protect the long-lived branches (e.g. the master branch) so that normal developers [can't modify these protected branches](http://docs.gitlab.com/ce/permissions/permissions.html).
-So if you want to merge it into a protected branch you assign it to someone with master authorizations.
+So if you want to merge it into a protected branch you assign it to someone with maintainer authorizations.
## Issue tracking with GitLab flow
diff --git a/doc/workflow/lfs/lfs_administration.md b/doc/workflow/lfs/lfs_administration.md
index f824756c10c..6ac3bb8c0b4 100644
--- a/doc/workflow/lfs/lfs_administration.md
+++ b/doc/workflow/lfs/lfs_administration.md
@@ -17,7 +17,7 @@ There are various configuration options to help GitLab server administrators:
* Enabling/disabling Git LFS support
* Changing the location of LFS object storage
-* Setting up AWS S3 compatible object storage
+* Setting up object storage supported by [Fog](http://fog.io/about/provider_documentation.html)
### Configuration for Omnibus installations
@@ -44,19 +44,31 @@ In `config/gitlab.yml`:
storage_path: /mnt/storage/lfs-objects
```
-## Storing the LFS objects in an S3-compatible object storage
+## Storing LFS objects in remote object storage
> [Introduced][ee-2760] in [GitLab Premium][eep] 10.0. Brought to GitLab Core
in 10.7.
-It is possible to store LFS objects on a remote object storage which allows you
-to offload storage to an external AWS S3 compatible service, freeing up disk
-space locally. You can also host your own S3 compatible storage decoupled from
-GitLab, with with a service such as [Minio](https://www.minio.io/).
+It is possible to store LFS objects in remote object storage which allows you
+to offload local hard disk R/W operations, and free up disk space significantly.
+GitLab is tightly integrated with `Fog`, so you can refer to its [documentation](http://fog.io/about/provider_documentation.html)
+to check which storage services can be integrated with GitLab.
+You can also use external object storage in a private local network. For example,
+[Minio](https://www.minio.io/) is a standalone object storage service, is easy to setup, and works well with GitLab instances.
-Object storage currently transfers files first to GitLab, and then on the
-object storage in a second stage. This can be done either by using a rake task
-to transfer existing objects, or in a background job after each file is received.
+GitLab provides two different options for the uploading mechanism: "Direct upload" and "Background upload".
+
+**Option 1. Direct upload**
+
+1. User pushes an lfs file to the GitLab instance
+1. GitLab-workhorse uploads the file directly to the external object storage
+1. GitLab-workhorse notifies GitLab-rails that the upload process is complete
+
+**Option 2. Background upload**
+
+1. User pushes an lfs file to the GitLab instance
+1. GitLab-rails stores the file in the local file storage
+1. GitLab-rails then uploads the file to the external object storage asynchronously
The following general settings are supported.
@@ -71,16 +83,51 @@ The following general settings are supported.
The `connection` settings match those provided by [Fog](https://github.com/fog).
-| Setting | Description | Default |
+Here is a configuration example with S3.
+
+| Setting | Description | example |
|---------|-------------|---------|
-| `provider` | Always `AWS` for compatible hosts | AWS |
-| `aws_access_key_id` | AWS credentials, or compatible | |
-| `aws_secret_access_key` | AWS credentials, or compatible | |
+| `provider` | The provider name | AWS |
+| `aws_access_key_id` | AWS credentials, or compatible | `ABC123DEF456` |
+| `aws_secret_access_key` | AWS credentials, or compatible | `ABC123DEF456ABC123DEF456ABC123DEF456` |
+| `aws_signature_version` | AWS signature version to use. 2 or 4 are valid options. Digital Ocean Spaces and other providers may need 2. | 4 |
| `region` | AWS region | us-east-1 |
| `host` | S3 compatible host for when not using AWS, e.g. `localhost` or `storage.example.com` | s3.amazonaws.com |
| `endpoint` | Can be used when configuring an S3 compatible service such as [Minio](https://www.minio.io), by entering a URL such as `http://127.0.0.1:9000` | (optional) |
| `path_style` | Set to true to use `host/bucket_name/object` style paths instead of `bucket_name.host/object`. Leave as false for AWS S3 | false |
+Here is a configuration example with GCS.
+
+| Setting | Description | example |
+|---------|-------------|---------|
+| `provider` | The provider name | `Google` |
+| `google_project` | GCP project name | `gcp-project-12345` |
+| `google_client_email` | The email address of the service account | `foo@gcp-project-12345.iam.gserviceaccount.com` |
+| `google_json_key_location` | The json key path | `/path/to/gcp-project-12345-abcde.json` |
+
+_NOTE: The service account must have permission to access the bucket. [See more](https://cloud.google.com/storage/docs/authentication)_
+
+### Manual uploading to an object storage
+
+There are two ways to manually do the same thing as automatic uploading (described above).
+
+**Option 1: rake task**
+
+```
+$ rake gitlab:lfs:migrate
+```
+
+**Option 2: rails console**
+
+```
+$ sudo gitlab-rails console # Login to rails console
+
+> # Upload LFS files manually
+> LfsObject.where(file_store: [nil, 1]).find_each do |lfs_object|
+> lfs_object.file.migrate!(ObjectStorage::Store::REMOTE) if lfs_object.file.file.exists?
+> end
+```
+
### S3 for Omnibus installations
On Omnibus installations, the settings are prefixed by `lfs_object_store_`:
@@ -156,6 +203,29 @@ You can see the total storage used for LFS objects on groups and projects
in the administration area, as well as through the [groups](../../api/groups.md)
and [projects APIs](../../api/projects.md).
+## Troubleshooting: `Google::Apis::TransmissionError: execution expired`
+
+If LFS integration is configred with Google Cloud Storage and background uploads (`background_upload: true` and `direct_upload: false`),
+sidekiq workers may encouter this error. This is because the uploading timed out with very large files.
+LFS files up to 6Gb can be uploaded without any extra steps, otherwise you need to use the following workaround.
+
+```shell
+$ sudo gitlab-rails console # Login to rails console
+
+> # Set up timeouts. 20 minutes is enough to upload 30GB LFS files.
+> # These settings are only in effect for the same session, i.e. they are not effective for sidekiq workers.
+> ::Google::Apis::ClientOptions.default.open_timeout_sec = 1200
+> ::Google::Apis::ClientOptions.default.read_timeout_sec = 1200
+> ::Google::Apis::ClientOptions.default.send_timeout_sec = 1200
+
+> # Upload LFS files manually. This process does not use sidekiq at all.
+> LfsObject.where(file_store: [nil, 1]).find_each do |lfs_object|
+> lfs_object.file.migrate!(ObjectStorage::Store::REMOTE) if lfs_object.file.file.exists?
+> end
+```
+
+See more information in [!19581](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/19581)
+
## Known limitations
* Support for removing unreferenced LFS objects was added in 8.14 onwards.
@@ -166,5 +236,5 @@ and [projects APIs](../../api/projects.md).
[reconfigure gitlab]: ../../administration/restart_gitlab.md#omnibus-gitlab-reconfigure "How to reconfigure Omnibus GitLab"
[restart gitlab]: ../../administration/restart_gitlab.md#installations-from-source "How to restart GitLab"
-[eep]: https://about.gitlab.com/products/ "GitLab Premium"
+[eep]: https://about.gitlab.com/pricing/ "GitLab Premium"
[ee-2760]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2760
diff --git a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
index 0d592a6d43e..ae161e43233 100644
--- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
+++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
@@ -144,7 +144,7 @@ git lfs unlock --id=123
```
If for some reason you need to unlock a file that was not locked by you,
-you can use the `--force` flag as long as you have a `master` access on
+you can use the `--force` flag as long as you have a `maintainer` access on
the project:
```bash
diff --git a/doc/workflow/merge_requests.md b/doc/workflow/merge_requests.md
index a68bb8b27ca..dc6da1938f3 100644
--- a/doc/workflow/merge_requests.md
+++ b/doc/workflow/merge_requests.md
@@ -1 +1 @@
-This document was moved to [user/project/merge_requests](../user/project/merge_requests.md).
+This document was moved to [user/project/merge_requests/index.md](../user/project/merge_requests/index.md).
diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md
index f1501c81b27..5dc62a30128 100644
--- a/doc/workflow/notifications.md
+++ b/doc/workflow/notifications.md
@@ -34,9 +34,14 @@ anything that is set at Global Settings.
![notification settings](img/notification_group_settings.png)
-Group Settings are taking precedence over Global Settings but are on a level below Project Settings.
+Group Settings are taking precedence over Global Settings but are on a level below Project or Subgroup Settings:
+
+```
+Group < Subgroup < Project
+```
+
This means that you can set a different level of notifications per group while still being able
-to have a finer level setting per project.
+to have a finer level setting per project or subgroup.
Organization like this is suitable for users that belong to different groups but don't have the
same need for being notified for every group they are member of.
These settings can be configured on group page under the name of the group. It will be the dropdown with the bell icon. They can also be configured on the user profile notifications dropdown.
@@ -106,6 +111,10 @@ by yourself (except when an issue is due). You will only receive automatic
notifications when somebody else comments or adds changes to the ones that
you've created or mentions you.
+If an open merge request becomes unmergeable due to conflict, its author will be notified about the cause.
+If a user has also set the merge request to automatically merge once pipeline succeeds,
+then that user will also be notified.
+
### Email Headers
Notification emails include headers that provide extra content about the notification received:
diff --git a/doc/workflow/repository_mirroring.md b/doc/workflow/repository_mirroring.md
index dbe63144e38..8c4e6ea8eab 100644
--- a/doc/workflow/repository_mirroring.md
+++ b/doc/workflow/repository_mirroring.md
@@ -1,6 +1,6 @@
# Repository mirroring
-Repository Mirroring is a way to mirror repositories from external sources.
+Repository mirroring is a way to mirror repositories from external sources.
It can be used to mirror all branches, tags, and commits that you have
in your repository.
@@ -34,14 +34,201 @@ A few things/limitations to consider:
- The Git LFS objects will not be synced. You'll need to push/pull them
manually.
-## Use-case
+## Use cases
+- You migrated to GitLab but still need to keep your project in another source.
+ In that case, you can simply set it up to mirror to GitLab (pull) and all the
+ essential history of commits, tags and branches will be available in your
+ GitLab instance.
- You have old projects in another source that you don't use actively anymore,
but don't want to remove for archiving purposes. In that case, you can create
a push mirror so that your active GitLab repository can push its changes to the
old location.
-## Pushing to a remote repository **[STARTER]**
+## Pulling from a remote repository **[STARTER]**
+
+>[Introduced][ee-51] in GitLab Enterprise Edition 8.2.
+
+You can set up a repository to automatically have its branches, tags, and commits
+updated from an upstream repository. This is useful when a repository you're
+interested in is located on a different server, and you want to be able to
+browse its content and its activity using the familiar GitLab interface.
+
+When creating a new project, you can enable repository mirroring when you choose
+to import the repository from "Any repo by URL". Enter the full URL of the Git
+repository to pull from and click on the **Mirror repository** checkbox.
+
+![New project](repository_mirroring/repository_mirroring_new_project.png)
+
+For an existing project, you can set up mirror pulling by visiting your project's
+**Settings âž” Repository** and searching for the "Pull from a remote repository"
+section. Check the "Mirror repository" box and hit **Save changes** at the bottom.
+You have a few options to choose from one being the user who will be the author
+of all events in the activity feed that are the result of an update. This user
+needs to have at least [master access][perms] to the project. Another option is
+whether you want to trigger builds for mirror updates.
+
+![Pull settings](repository_mirroring/repository_mirroring_pull_settings.png)
+
+Since the repository on GitLab functions as a mirror of the upstream repository,
+you are advised not to push commits directly to the repository on GitLab.
+Instead, any commits should be pushed to the upstream repository, and will end
+up in the GitLab repository automatically within a certain period of time
+or when a [forced update](#forcing-an-update) is initiated.
+
+If you do manually update a branch in the GitLab repository, the branch will
+become diverged from upstream, and GitLab will no longer automatically update
+this branch to prevent any changes from being lost.
+
+![Diverged branch](repository_mirroring/repository_mirroring_diverged_branch.png)
+
+### Trigger update using API **[STARTER]**
+
+>[Introduced][ee-3453] in GitLab Enterprise Edition 10.3.
+
+Pull mirroring uses polling to detect new branches and commits added upstream,
+often many minutes afterwards. If you notify GitLab by [API][pull-api], updates
+will be pulled immediately.
+
+Read the [Pull Mirror Trigger API docs][pull-api].
+
+### Pull only protected branches **[STARTER]**
+
+>[Introduced][ee-3326] in GitLab Enterprise Edition 10.3.
+
+You can choose to only pull the protected branches from your remote repository to GitLab.
+
+To use this option go to your project's repository settings page under pull mirror.
+
+### Overwrite diverged branches **[STARTER]**
+
+>[Introduced][ee-4559] in GitLab Enterprise Edition 10.6.
+
+You can choose to always update your local branch with the remote version even
+if your local version has diverged from the remote.
+
+To use this option go to your project's repository settings page under pull mirror.
+
+### Hard failure **[STARTER]**
+
+>[Introduced][ee-3117] in GitLab Enterprise Edition 10.2.
+
+Once a mirror gets retried 14 times in a row, it will get marked as hard failed,
+this will become visible in either the project main dashboard or in the
+pull mirror settings page.
+
+![Hard failed mirror main notice](repository_mirroring/repository_mirroring_hard_failed_main.png)
+
+![Hard failed mirror settings notice](repository_mirroring/repository_mirroring_hard_failed_settings.png)
+
+When a project is hard failed, it will no longer get picked up for mirroring.
+A user can resume the project mirroring again by either [forcing an update](#forcing-an-update)
+or by changing the import URL in repository settings.
+
+### SSH authentication **[STARTER]**
+
+> [Introduced][ee-2551] in GitLab Starter 9.5
+
+If you're mirroring over SSH (i.e., an `ssh://` URL), you can authenticate using
+password-based authentication, just as over HTTPS, but you can also use public
+key authentication. This is often more secure than password authentication,
+especially when the source repository supports [Deploy Keys][deploy-key].
+
+To get started, navigate to **Settings âž” Repository âž” Pull from a remote repository**,
+enable mirroring (if not already enabled) and enter an `ssh://` URL.
+
+> **NOTE**: SCP-style URLs, e.g., `git@example.com:group/project.git`, are not
+supported at this time.
+
+Entering the URL adds two features to the page - `Fingerprints` and
+`SSH public key authentication`:
+
+![Pull settings for SSH](repository_mirroring/repository_mirroring_pull_settings_for_ssh.png)
+
+SSH authentication is mutual. You have to prove to the server that you're
+allowed to access the repository, but the server also has to prove to *you* that
+it's who it claims to be. You provide your credentials as a password or public
+key. The server that the source repository resides on provides its credentials
+as a "host key", the fingerprint of which needs to be verified manually.
+
+Press the `Detect host keys` button. GitLab will fetch the host keys from the
+server, and display the fingerprints to you:
+
+![Detect SSH host keys](repository_mirroring/repository_mirroring_detect_host_keys.png)
+
+You now need to verify that the fingerprints are those you expect. GitLab.com
+and other code hosting sites publish their fingerprints in the open for you
+to check:
+
+* [AWS CodeCommit](http://docs.aws.amazon.com/codecommit/latest/userguide/regions.html#regions-fingerprints)
+* [Bitbucket](https://confluence.atlassian.com/bitbucket/use-the-ssh-protocol-with-bitbucket-cloud-221449711.html#UsetheSSHprotocolwithBitbucketCloud-KnownhostorBitbucket%27spublickeyfingerprints)
+* [GitHub](https://help.github.com/articles/github-s-ssh-key-fingerprints/)
+* [GitLab.com](https://about.gitlab.com/gitlab-com/settings/#ssh-host-keys-fingerprints)
+* [Launchpad](https://help.launchpad.net/SSHFingerprints)
+* [Savannah](http://savannah.gnu.org/maintenance/SshAccess/)
+* [SourceForge](https://sourceforge.net/p/forge/documentation/SSH%20Key%20Fingerprints/)
+
+Other providers will vary. If you're running on-premises GitLab, or otherwise
+have access to the source server, you can securely gather the key fingerprints:
+
+```
+$ cat /etc/ssh/ssh_host*pub | ssh-keygen -E md5 -l -f -
+256 MD5:f4:28:9f:23:99:15:21:1b:bf:ed:1f:8e:a0:76:b2:9d root@example.com (ECDSA)
+256 MD5:e6:eb:45:8a:3c:59:35:5f:e9:5b:80:12:be:7e:22:73 root@example.com (ED25519)
+2048 MD5:3f:72:be:3d:62:03:5c:62:83:e8:6e:14:34:3a:85:1d root@example.com (RSA)
+```
+
+(You may need to exclude `-E md5` for some older versions of SSH).
+
+If you're an SSH expert and already have a `known_hosts` file you'd like to use
+unaltered, then you can skip these steps. Just press the "Show advanced" button
+and paste in the file contents:
+
+![Advanced SSH host key management](repository_mirroring/repository_mirroring_pull_advanced_host_keys.png)
+
+Once you've **carefully verified** that all the fingerprints match your trusted
+source, you can press `Save changes`. This will record the host keys, along with
+the person who verified them (you!) and the date:
+
+![SSH host keys submitted](repository_mirroring/repository_mirroring_ssh_host_keys_verified.png)
+
+When pulling changes from the source repository, GitLab will now check that at
+least one of the stored host keys matches before connecting. This can prevent
+malicious code from being injected into your mirror, or your password being
+stolen!
+
+To use SSH public key authentication, you'll also need to choose that option
+from the authentication methods dropdown. GitLab will generate a 4096-bit RSA
+key and display the public component of that key to you:
+
+![SSH public key authentication](repository_mirroring/repository_mirroring_ssh_public_key_authentication.png)
+
+You then need to add the public SSH key to the source repository configuration.
+If the source is hosted on GitLab, you should add it as a [Deploy Key][deploy-key].
+Other sources may require you to add the key to your user's `authorized_keys`
+file - just paste the entire `ssh-rsa AAA.... user@host` block into the file on
+its own line and save it.
+
+Once the public key is set up on the source repository, press `Save changes` and your
+mirror will begin working.
+
+If you need to change the key at any time, you can press the `Regenerate key`
+button to do so. You'll have to update the source repository with the new key
+to keep the mirror running.
+
+### How it works
+
+Once you activate the pull mirroring feature, the mirror will be inserted into
+a queue. A scheduler will start every minute and schedule a fixed amount of
+mirrors for update, based on the configured maximum capacity.
+
+If the mirror successfully updates it will be enqueued once again with a small
+backoff period.
+
+If the mirror fails (eg: branch diverged from upstream), the project's backoff
+period will be penalized each time it fails up to a maximum amount of time.
+
+## Pushing to a remote repository
>[Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/249) in
GitLab Enterprise Edition 8.7. [Moved to GitLab Community Edition][ce-18715] in 10.8.
@@ -83,7 +270,7 @@ To use this option go to your project's repository settings page under push mirr
To set up a mirror from GitLab to GitHub, you need to follow these steps:
1. Create a [GitHub personal access token](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/) with the "public_repo" box checked:
-
+
![edit personal access token GitHub](repository_mirroring/repository_mirroring_github_edit_personal_access_token.png)
1. Fill in the "Git repository URL" with the personal access token replacing the password `https://GitHubUsername:GitHubPersonalAccessToken@github.com/group/project.git`:
@@ -94,7 +281,7 @@ To set up a mirror from GitLab to GitHub, you need to follow these steps:
1. And either wait or trigger the "Update Now" button:
![update now](repository_mirroring/repository_mirroring_gitlab_push_to_a_remote_repository_update_now.png)
-
+
## Forcing an update
While mirrors are scheduled to update automatically, you can always force an update
@@ -105,7 +292,60 @@ by using the **Update now** button which is exposed in various places:
- in the tags page
- in the **Mirror repository** settings page
+## Bidirectional mirroring
+
+CAUTION: **Warning:**
+There is no bidirectional support without conflicts. If you
+configure a repository to pull and push to a second remote, there is no
+guarantee that it will update correctly on both remotes. If you configure
+a repository for bidirectional mirroring, you should consider when conflicts
+occur who and how they will be resolved.
+
+Rewriting any mirrored commit on either remote will cause conflicts and
+mirroring to fail. This can be prevented by [only pulling protected branches](
+#pull-only-protected-branches) and [only pushing protected branches](
+#push-only-protected-branches). You should protect the branches you wish to
+mirror on both remotes to prevent conflicts caused by rewriting history.
+
+Bidirectional mirroring also creates a race condition where commits to the same
+branch in close proximity will cause conflicts. The race condition can be
+mitigated by reducing the mirroring delay by using a Push event webhook to
+trigger an immediate pull to GitLab. Push mirroring from GitLab is rate limited
+to once per minute when only push mirroring protected branches.
+
+It may be possible to implement a locking mechanism using the server-side
+`pre-receive` hook to prevent the race condition. Read about [configuring
+custom Git hooks][hooks] on the GitLab server.
+
+### Mirroring with Perforce via GitFusion
+
+CAUTION: **Warning:**
+Bidirectional mirroring should not be used as a permanent
+configuration. There is no bidirectional mirroring without conflicts.
+Refer to [Migrating from Perforce Helix][perforce] for alternative migration
+approaches.
+
+GitFusion provides a Git interface to Perforce which can be used by GitLab to
+bidirectionally mirror projects with GitLab. This may be useful in some
+situations when migrating from Perforce to GitLab where overlapping Perforce
+workspaces cannot be migrated simultaneously to GitLab.
+
+If using mirroring with Perforce you should only mirror protected branches.
+Perforce will reject any pushes that rewrite history. It is recommended that
+only the fewest number of branches are mirrored due to the performance
+limitations of GitFusion.
+
+[ee-51]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/51
+[ee-2551]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2551
+[ee-3117]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3117
+[ee-3326]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3326
[ee-3350]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3350
+[ee-3453]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3453
+[ee-4559]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/4559
[ce-18715]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18715
[perms]: ../user/permissions.md
-
+[hooks]: ../administration/custom_hooks.md
+[deploy-key]: ../ssh/README.md#deploy-keys
+[webhook]: ../user/project/integrations/webhooks.md#push-events
+[pull-api]: ../api/projects.md#start-the-pull-mirroring-process-for-a-project
+[perforce]: ../user/project/import/perforce.md
diff --git a/doc/workflow/repository_mirroring/repository_mirroring_detect_host_keys.png b/doc/workflow/repository_mirroring/repository_mirroring_detect_host_keys.png
new file mode 100644
index 00000000000..333648942f8
--- /dev/null
+++ b/doc/workflow/repository_mirroring/repository_mirroring_detect_host_keys.png
Binary files differ
diff --git a/doc/workflow/repository_mirroring/repository_mirroring_diverged_branch.png b/doc/workflow/repository_mirroring/repository_mirroring_diverged_branch.png
new file mode 100644
index 00000000000..45c9bce0889
--- /dev/null
+++ b/doc/workflow/repository_mirroring/repository_mirroring_diverged_branch.png
Binary files differ
diff --git a/doc/workflow/repository_mirroring/repository_mirroring_hard_failed_main.png b/doc/workflow/repository_mirroring/repository_mirroring_hard_failed_main.png
new file mode 100644
index 00000000000..99d429a1802
--- /dev/null
+++ b/doc/workflow/repository_mirroring/repository_mirroring_hard_failed_main.png
Binary files differ
diff --git a/doc/workflow/repository_mirroring/repository_mirroring_hard_failed_settings.png b/doc/workflow/repository_mirroring/repository_mirroring_hard_failed_settings.png
new file mode 100644
index 00000000000..0ab07afa3cc
--- /dev/null
+++ b/doc/workflow/repository_mirroring/repository_mirroring_hard_failed_settings.png
Binary files differ
diff --git a/doc/workflow/repository_mirroring/repository_mirroring_new_project.png b/doc/workflow/repository_mirroring/repository_mirroring_new_project.png
new file mode 100644
index 00000000000..43bf304838f
--- /dev/null
+++ b/doc/workflow/repository_mirroring/repository_mirroring_new_project.png
Binary files differ
diff --git a/doc/workflow/repository_mirroring/repository_mirroring_pull_advanced_host_keys.png b/doc/workflow/repository_mirroring/repository_mirroring_pull_advanced_host_keys.png
new file mode 100644
index 00000000000..5da5a7436bb
--- /dev/null
+++ b/doc/workflow/repository_mirroring/repository_mirroring_pull_advanced_host_keys.png
Binary files differ
diff --git a/doc/workflow/repository_mirroring/repository_mirroring_pull_settings.png b/doc/workflow/repository_mirroring/repository_mirroring_pull_settings.png
new file mode 100644
index 00000000000..4b9085302a1
--- /dev/null
+++ b/doc/workflow/repository_mirroring/repository_mirroring_pull_settings.png
Binary files differ
diff --git a/doc/workflow/repository_mirroring/repository_mirroring_pull_settings_for_ssh.png b/doc/workflow/repository_mirroring/repository_mirroring_pull_settings_for_ssh.png
new file mode 100644
index 00000000000..8c2efdafa43
--- /dev/null
+++ b/doc/workflow/repository_mirroring/repository_mirroring_pull_settings_for_ssh.png
Binary files differ
diff --git a/doc/workflow/repository_mirroring/repository_mirroring_ssh_host_keys_verified.png b/doc/workflow/repository_mirroring/repository_mirroring_ssh_host_keys_verified.png
new file mode 100644
index 00000000000..93f3a532a0e
--- /dev/null
+++ b/doc/workflow/repository_mirroring/repository_mirroring_ssh_host_keys_verified.png
Binary files differ
diff --git a/doc/workflow/repository_mirroring/repository_mirroring_ssh_public_key_authentication.png b/doc/workflow/repository_mirroring/repository_mirroring_ssh_public_key_authentication.png
new file mode 100644
index 00000000000..6997ad511d9
--- /dev/null
+++ b/doc/workflow/repository_mirroring/repository_mirroring_ssh_public_key_authentication.png
Binary files differ
diff --git a/doc/workflow/todos.md b/doc/workflow/todos.md
index f13d29884d4..760cd87d4cc 100644
--- a/doc/workflow/todos.md
+++ b/doc/workflow/todos.md
@@ -31,6 +31,9 @@ A Todo appears in your Todos dashboard when:
- you are `@mentioned` in a comment on a commit,
- a job in the CI pipeline running for your merge request failed, but this
job is not allowed to fail.
+- an open merge request becomes unmergeable due to conflict, and you are either:
+ - the author, or
+ - have set it to automatically merge once pipeline succeeds.
### Directly addressed Todos
diff --git a/doc_styleguide.md b/doc_styleguide.md
deleted file mode 100644
index 05ff46323ac..00000000000
--- a/doc_styleguide.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Documentation styleguide
-
-Moved to [development/doc_styleguide](doc/development/doc_styleguide.md).
diff --git a/lib/api/api.rb b/lib/api/api.rb
index de20b2b8e67..e2ad3c5f4e3 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -15,54 +15,21 @@ module API
include: [
GrapeLogging::Loggers::FilterParameters.new,
GrapeLogging::Loggers::ClientEnv.new,
- Gitlab::GrapeLogging::Loggers::UserLogger.new
+ Gitlab::GrapeLogging::Loggers::UserLogger.new,
+ Gitlab::GrapeLogging::Loggers::QueueDurationLogger.new
]
allow_access_with_scope :api
prefix :api
- version %w(v3 v4), using: :path
-
version 'v3', using: :path do
- helpers ::API::V3::Helpers
- helpers ::API::Helpers::CommonHelpers
-
- mount ::API::V3::AwardEmoji
- mount ::API::V3::Boards
- mount ::API::V3::Branches
- mount ::API::V3::BroadcastMessages
- mount ::API::V3::Builds
- mount ::API::V3::Commits
- mount ::API::V3::DeployKeys
- mount ::API::V3::Environments
- mount ::API::V3::Files
- mount ::API::V3::Groups
- mount ::API::V3::Issues
- mount ::API::V3::Labels
- mount ::API::V3::Members
- mount ::API::V3::MergeRequestDiffs
- mount ::API::V3::MergeRequests
- mount ::API::V3::Notes
- mount ::API::V3::Pipelines
- mount ::API::V3::ProjectHooks
- mount ::API::V3::Milestones
- mount ::API::V3::Projects
- mount ::API::V3::ProjectSnippets
- mount ::API::V3::Repositories
- mount ::API::V3::Runners
- mount ::API::V3::Services
- mount ::API::V3::Settings
- mount ::API::V3::Snippets
- mount ::API::V3::Subscriptions
- mount ::API::V3::SystemHooks
- mount ::API::V3::Tags
- mount ::API::V3::Templates
- mount ::API::V3::Todos
- mount ::API::V3::Triggers
- mount ::API::V3::Users
- mount ::API::V3::Variables
+ route :any, '*path' do
+ error!('API V3 is no longer supported. Use API V4 instead.', 410)
+ end
end
+ version 'v4', using: :path
+
before do
header['X-Frame-Options'] = 'SAMEORIGIN'
header['X-Content-Type-Options'] = 'nosniff'
@@ -116,6 +83,7 @@ module API
# Keep in alphabetical order
mount ::API::AccessRequests
mount ::API::Applications
+ mount ::API::Avatar
mount ::API::AwardEmoji
mount ::API::Badges
mount ::API::Boards
diff --git a/lib/api/avatar.rb b/lib/api/avatar.rb
new file mode 100644
index 00000000000..70219bc8ea0
--- /dev/null
+++ b/lib/api/avatar.rb
@@ -0,0 +1,21 @@
+module API
+ class Avatar < Grape::API
+ resource :avatar do
+ desc 'Return avatar url for a user' do
+ success Entities::Avatar
+ end
+ params do
+ requires :email, type: String, desc: 'Public email address of the user'
+ optional :size, type: Integer, desc: 'Single pixel dimension for Gravatar images'
+ end
+ get do
+ forbidden!('Unauthorized access') unless can?(current_user, :read_users_list)
+
+ user = User.find_by_public_email(params[:email])
+ user ||= User.new(email: params[:email])
+
+ present user, with: Entities::Avatar, size: params[:size]
+ end
+ end
+ end
+end
diff --git a/lib/api/boards.rb b/lib/api/boards.rb
index 6c706b2b4e1..086d39d5070 100644
--- a/lib/api/boards.rb
+++ b/lib/api/boards.rb
@@ -33,6 +33,7 @@ module API
success Entities::Board
end
get '/:board_id' do
+ authorize!(:read_board, user_project)
present board, with: Entities::Board
end
end
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index 13cfba728fa..4b223a391ae 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -45,6 +45,7 @@ module API
present(
paginate(::Kaminari.paginate_array(branches)),
with: Entities::Branch,
+ current_user: current_user,
project: user_project,
merged_branch_names: merged_branch_names
)
@@ -63,7 +64,7 @@ module API
get do
branch = find_branch!(params[:branch])
- present branch, with: Entities::Branch, project: user_project
+ present branch, with: Entities::Branch, current_user: current_user, project: user_project
end
end
@@ -101,7 +102,7 @@ module API
end
if protected_branch.valid?
- present branch, with: Entities::Branch, project: user_project
+ present branch, with: Entities::Branch, current_user: current_user, project: user_project
else
render_api_error!(protected_branch.errors.full_messages, 422)
end
@@ -121,7 +122,7 @@ module API
protected_branch = user_project.protected_branches.find_by(name: branch.name)
protected_branch&.destroy
- present branch, with: Entities::Branch, project: user_project
+ present branch, with: Entities::Branch, current_user: current_user, project: user_project
end
desc 'Create branch' do
@@ -140,6 +141,7 @@ module API
if result[:status] == :success
present result[:branch],
with: Entities::Branch,
+ current_user: current_user,
project: user_project
else
render_api_error!(result[:message], 400)
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index 684955a1b24..964780cba6a 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -15,19 +15,21 @@ module API
end
params do
optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used'
- optional :since, type: DateTime, desc: 'Only commits after or on this date will be returned'
- optional :until, type: DateTime, desc: 'Only commits before or on this date will be returned'
- optional :path, type: String, desc: 'The file path'
- optional :all, type: Boolean, desc: 'Every commit will be returned'
+ optional :since, type: DateTime, desc: 'Only commits after or on this date will be returned'
+ optional :until, type: DateTime, desc: 'Only commits before or on this date will be returned'
+ optional :path, type: String, desc: 'The file path'
+ optional :all, type: Boolean, desc: 'Every commit will be returned'
+ optional :with_stats, type: Boolean, desc: 'Stats about each commit will be added to the response'
use :pagination
end
get ':id/repository/commits' do
- path = params[:path]
+ path = params[:path]
before = params[:until]
- after = params[:since]
- ref = params[:ref_name] || user_project.try(:default_branch) || 'master' unless params[:all]
+ after = params[:since]
+ ref = params[:ref_name] || user_project.try(:default_branch) || 'master' unless params[:all]
offset = (params[:page] - 1) * params[:per_page]
- all = params[:all]
+ all = params[:all]
+ with_stats = params[:with_stats]
commits = user_project.repository.commits(ref,
path: path,
@@ -47,7 +49,9 @@ module API
paginated_commits = Kaminari.paginate_array(commits, total_count: commit_count)
- present paginate(paginated_commits), with: Entities::Commit
+ serializer = with_stats ? Entities::CommitWithStats : Entities::Commit
+
+ present paginate(paginated_commits), with: serializer
end
desc 'Commit multiple file changes as one commit' do
diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb
index 70d43ac1d79..6769855b899 100644
--- a/lib/api/deploy_keys.rb
+++ b/lib/api/deploy_keys.rb
@@ -112,9 +112,9 @@ module API
can_push = params[:can_push].nil? ? deploy_keys_project.can_push : params[:can_push]
title = params[:title] || deploy_keys_project.deploy_key.title
- result = deploy_keys_project.update_attributes(can_push: can_push,
- deploy_key_attributes: { id: params[:key_id],
- title: title })
+ result = deploy_keys_project.update(can_push: can_push,
+ deploy_key_attributes: { id: params[:key_id],
+ title: title })
if result
present deploy_keys_project, with: Entities::DeployKeysProject
@@ -148,10 +148,10 @@ module API
requires :key_id, type: Integer, desc: 'The ID of the deploy key'
end
delete ":id/deploy_keys/:key_id" do
- key = user_project.deploy_keys.find(params[:key_id])
- not_found!('Deploy Key') unless key
+ deploy_key_project = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id])
+ not_found!('Deploy Key') unless deploy_key_project
- destroy_conditionally!(key)
+ destroy_conditionally!(deploy_key_project)
end
end
end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 174c5af91d5..66b62d9ee2e 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -308,6 +308,10 @@ module API
expose :additions, :deletions, :total
end
+ class CommitWithStats < Commit
+ expose :stats, using: Entities::CommitStats
+ end
+
class CommitDetail < Commit
expose :stats, using: Entities::CommitStats, if: :stats
expose :status
@@ -345,6 +349,10 @@ module API
expose :developers_can_merge do |repo_branch, options|
options[:project].protected_branches.developers_can?(:merge, repo_branch.name)
end
+
+ expose :can_push do |repo_branch, options|
+ Gitlab::UserAccess.new(options[:current_user], project: options[:project]).can_push_to_branch?(repo_branch.name)
+ end
end
class TreeObject < Grape::Entity
@@ -358,7 +366,7 @@ module API
end
class Snippet < Grape::Entity
- expose :id, :title, :file_name, :description
+ expose :id, :title, :file_name, :description, :visibility
expose :author, using: Entities::UserBasic
expose :updated_at, :created_at
expose :project_id
@@ -412,6 +420,10 @@ module API
expose :state, :created_at, :updated_at
expose :due_date
expose :start_date
+
+ expose :web_url do |milestone, _options|
+ Gitlab::UrlBuilder.build(milestone)
+ end
end
class IssueBasic < ProjectEntity
@@ -520,6 +532,12 @@ module API
end
class MergeRequestBasic < ProjectEntity
+ expose :title_html, if: -> (_, options) { options[:render_html] } do |entity|
+ MarkupHelper.markdown_field(entity, :title)
+ end
+ expose :description_html, if: -> (_, options) { options[:render_html] } do |entity|
+ MarkupHelper.markdown_field(entity, :description)
+ end
expose :target_branch, :source_branch
expose :upvotes do |merge_request, options|
if options[:issuable_metadata]
@@ -559,7 +577,9 @@ module API
expose :discussion_locked
expose :should_remove_source_branch?, as: :should_remove_source_branch
expose :force_remove_source_branch?, as: :force_remove_source_branch
- expose :allow_maintainer_to_push, if: -> (merge_request, _) { merge_request.for_fork? }
+ expose :allow_collaboration, if: -> (merge_request, _) { merge_request.for_fork? }
+ # Deprecated
+ expose :allow_collaboration, as: :allow_maintainer_to_push, if: -> (merge_request, _) { merge_request.for_fork? }
expose :web_url do |merge_request, options|
Gitlab::UrlBuilder.build(merge_request)
@@ -568,6 +588,8 @@ module API
expose :time_stats, using: 'API::Entities::IssuableTimeStats' do |merge_request|
merge_request
end
+
+ expose :squash
end
class MergeRequest < MergeRequestBasic
@@ -690,6 +712,12 @@ module API
expose :notes, using: Entities::Note
end
+ class Avatar < Grape::Entity
+ expose :avatar_url do |avatarable, options|
+ avatarable.avatar_url(only_path: false, size: options[:size])
+ end
+ end
+
class AwardEmoji < Grape::Entity
expose :id
expose :name
@@ -830,8 +858,8 @@ module API
class ProjectWithAccess < Project
expose :permissions do
expose :project_access, using: Entities::ProjectAccess do |project, options|
- if options.key?(:project_members)
- (options[:project_members] || []).find { |member| member.source_id == project.id }
+ if options[:project_members]
+ options[:project_members].find { |member| member.source_id == project.id }
else
project.project_member(options[:current_user])
end
@@ -839,8 +867,8 @@ module API
expose :group_access, using: Entities::GroupAccess do |project, options|
if project.group
- if options.key?(:group_members)
- (options[:group_members] || []).find { |member| member.source_id == project.namespace_id }
+ if options[:group_members]
+ options[:group_members].find { |member| member.source_id == project.namespace_id }
else
project.group.group_member(options[:current_user])
end
@@ -851,13 +879,24 @@ module API
def self.preload_relation(projects_relation, options = {})
relation = super(projects_relation, options)
- unless options.key?(:group_members)
- relation = relation.preload(group: [group_members: [:source, user: [notification_settings: :source]]])
+ # MySQL doesn't support LIMIT inside an IN subquery
+ if Gitlab::Database.mysql?
+ project_ids = relation.pluck('projects.id')
+ namespace_ids = relation.pluck(:namespace_id)
+ else
+ project_ids = relation.select('projects.id')
+ namespace_ids = relation.select(:namespace_id)
end
- unless options.key?(:project_members)
- relation = relation.preload(project_members: [:source, user: [notification_settings: :source]])
- end
+ options[:project_members] = options[:current_user]
+ .project_members
+ .where(source_id: project_ids)
+ .preload(:source, user: [notification_settings: :source])
+
+ options[:group_members] = options[:current_user]
+ .group_members
+ .where(source_id: namespace_ids)
+ .preload(:source, user: [notification_settings: :source])
relation
end
@@ -933,8 +972,16 @@ module API
end
class ApplicationSetting < Grape::Entity
- expose :id
- expose(*::ApplicationSettingsHelper.visible_attributes)
+ def self.exposed_attributes
+ attributes = ::ApplicationSettingsHelper.visible_attributes
+ attributes.delete(:performance_bar_allowed_group_path)
+ attributes.delete(:performance_bar_enabled)
+
+ attributes
+ end
+
+ expose :id, :performance_bar_allowed_group_id
+ expose(*exposed_attributes)
expose(:restricted_visibility_levels) do |setting, _options|
setting.restricted_visibility_levels.map { |level| Gitlab::VisibilityLevel.string_level(level) }
end
@@ -969,7 +1016,7 @@ module API
expose :description
expose :ip_address
expose :active
- expose :is_shared
+ expose :instance_type?, as: :is_shared
expose :name
expose :online?, as: :online
expose :status
@@ -983,7 +1030,7 @@ module API
expose :access_level
expose :version, :revision, :platform, :architecture
expose :contacted_at
- expose :token, if: lambda { |runner, options| options[:current_user].admin? || !runner.is_shared? }
+ expose :token, if: lambda { |runner, options| options[:current_user].admin? || !runner.instance_type? }
expose :projects, with: Entities::BasicProjectDetails do |runner, options|
if options[:current_user].admin?
runner.projects
@@ -1020,6 +1067,7 @@ module API
class Job < JobBasic
expose :artifacts_file, using: JobArtifactFile, if: -> (job, opts) { job.artifacts? }
expose :runner, with: Runner
+ expose :artifacts_expire_at
end
class JobBasicWithProject < JobBasic
@@ -1156,6 +1204,7 @@ module API
class RunnerInfo < Grape::Entity
expose :metadata_timeout, as: :timeout
+ expose :runner_session_url
end
class Step < Grape::Entity
diff --git a/lib/api/environments.rb b/lib/api/environments.rb
index 5c63ec028d9..fa828f43001 100644
--- a/lib/api/environments.rb
+++ b/lib/api/environments.rb
@@ -89,9 +89,10 @@ module API
requires :environment_id, type: Integer, desc: 'The environment ID'
end
post ':id/environments/:environment_id/stop' do
- authorize! :create_deployment, user_project
+ authorize! :read_environment, user_project
environment = user_project.environments.find(params[:environment_id])
+ authorize! :stop_environment, environment
environment.stop_with_action!(current_user)
diff --git a/lib/api/events.rb b/lib/api/events.rb
index b0713ff1d54..fc4ba5a3188 100644
--- a/lib/api/events.rb
+++ b/lib/api/events.rb
@@ -17,6 +17,7 @@ module API
def present_events(events)
events = events.reorder(created_at: params[:sort])
+ .with_associations
present paginate(events), with: Entities::Event
end
diff --git a/lib/api/files.rb b/lib/api/files.rb
index 1598d3c00b8..29d7489bd7c 100644
--- a/lib/api/files.rb
+++ b/lib/api/files.rb
@@ -5,6 +5,8 @@ module API
# Prevents returning plain/text responses for files with .txt extension
after_validation { content_type "application/json" }
+ helpers ::API::Helpers::HeadersHelpers
+
helpers do
def commit_params(attrs)
{
@@ -40,6 +42,20 @@ module API
}
end
+ def blob_data
+ {
+ file_name: @blob.name,
+ file_path: @blob.path,
+ size: @blob.size,
+ encoding: "base64",
+ content_sha256: Digest::SHA256.hexdigest(@blob.data),
+ ref: params[:ref],
+ blob_id: @blob.id,
+ commit_id: @commit.id,
+ last_commit_id: @repo.last_commit_id_for_path(@commit.sha, params[:file_path])
+ }
+ end
+
params :simple_file_params do
requires :file_path, type: String, desc: 'The url encoded path to the file. Ex. lib%2Fclass%2Erb'
requires :branch, type: String, desc: 'Name of the branch to commit into. To create a new branch, also provide `start_branch`.'
@@ -61,6 +77,17 @@ module API
requires :id, type: String, desc: 'The project ID'
end
resource :projects, requirements: FILE_ENDPOINT_REQUIREMENTS do
+ desc 'Get raw file metadata from repository'
+ params do
+ requires :file_path, type: String, desc: 'The url encoded path to the file. Ex. lib%2Fclass%2Erb'
+ requires :ref, type: String, desc: 'The name of branch, tag or commit'
+ end
+ head ":id/repository/files/:file_path/raw", requirements: FILE_ENDPOINT_REQUIREMENTS do
+ assign_file_vars!
+
+ set_http_headers(blob_data)
+ end
+
desc 'Get raw file contents from the repository'
params do
requires :file_path, type: String, desc: 'The url encoded path to the file. Ex. lib%2Fclass%2Erb'
@@ -69,9 +96,22 @@ module API
get ":id/repository/files/:file_path/raw", requirements: FILE_ENDPOINT_REQUIREMENTS do
assign_file_vars!
+ set_http_headers(blob_data)
+
send_git_blob @repo, @blob
end
+ desc 'Get file metadata from repository'
+ params do
+ requires :file_path, type: String, desc: 'The url encoded path to the file. Ex. lib%2Fclass%2Erb'
+ requires :ref, type: String, desc: 'The name of branch, tag or commit'
+ end
+ head ":id/repository/files/:file_path", requirements: FILE_ENDPOINT_REQUIREMENTS do
+ assign_file_vars!
+
+ set_http_headers(blob_data)
+ end
+
desc 'Get a file from the repository'
params do
requires :file_path, type: String, desc: 'The url encoded path to the file. Ex. lib%2Fclass%2Erb'
@@ -80,17 +120,11 @@ module API
get ":id/repository/files/:file_path", requirements: FILE_ENDPOINT_REQUIREMENTS do
assign_file_vars!
- {
- file_name: @blob.name,
- file_path: @blob.path,
- size: @blob.size,
- encoding: "base64",
- content: Base64.strict_encode64(@blob.data),
- ref: params[:ref],
- blob_id: @blob.id,
- commit_id: @commit.id,
- last_commit_id: @repo.last_commit_id_for_path(@commit.sha, params[:file_path])
- }
+ data = blob_data
+
+ set_http_headers(data)
+
+ data.merge(content: Base64.strict_encode64(@blob.data))
end
desc 'Create new file in repository'
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index 03b6b30a0d8..797b04df059 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -32,7 +32,7 @@ module API
optional :all_available, type: Boolean, desc: 'Show all group that you have access to'
optional :search, type: String, desc: 'Search for a specific group'
optional :owned, type: Boolean, default: false, desc: 'Limit by owned by authenticated user'
- optional :order_by, type: String, values: %w[name path], default: 'name', desc: 'Order by name or path'
+ optional :order_by, type: String, values: %w[name path id], default: 'name', desc: 'Order by name, path or id'
optional :sort, type: String, values: %w[asc desc], default: 'asc', desc: 'Sort by asc (ascending) or desc (descending)'
use :pagination
end
@@ -46,7 +46,9 @@ module API
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])
+ order_options = { params[:order_by] => params[:sort] }
+ order_options["id"] ||= "asc"
+ groups = groups.reorder(order_options)
groups
end
@@ -54,6 +56,8 @@ module API
def find_group_projects(params)
group = find_group!(params[:id])
projects = GroupProjectsFinder.new(group: group, current_user: current_user, params: project_finder_params).execute
+ projects = projects.with_issues_available_for_user(current_user) if params[:with_issues_enabled]
+ projects = projects.with_merge_requests_enabled if params[:with_merge_requests_enabled]
projects = reorder_projects(projects)
paginate(projects)
end
@@ -146,12 +150,13 @@ module API
end
params do
use :with_custom_attributes
+ optional :with_projects, type: Boolean, default: true, desc: 'Omit project details'
end
get ":id" do
group = find_group!(params[:id])
options = {
- with: Entities::GroupDetail,
+ with: params[:with_projects] ? Entities::GroupDetail : Entities::Group,
current_user: current_user
}
@@ -189,6 +194,8 @@ module API
desc: 'Return only the ID, URL, name, and path of each project'
optional :owned, type: Boolean, default: false, desc: 'Limit by owned by authenticated user'
optional :starred, type: Boolean, default: false, desc: 'Limit by starred status'
+ optional :with_issues_enabled, type: Boolean, default: false, desc: 'Limit by enabled issues feature'
+ optional :with_merge_requests_enabled, type: Boolean, default: false, desc: 'Limit by enabled merge requests feature'
use :pagination
use :with_custom_attributes
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 2ed331d4fd2..9c53b7c3fe7 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -272,7 +272,8 @@ module API
attrs[key] = params_hash[key]
end
end
- ActionController::Parameters.new(attrs).permit!
+ permitted_attrs = ActionController::Parameters.new(attrs).permit!
+ Gitlab.rails5? ? permitted_attrs.to_h : permitted_attrs
end
def filter_by_iid(items, iid)
diff --git a/lib/api/helpers/headers_helpers.rb b/lib/api/helpers/headers_helpers.rb
new file mode 100644
index 00000000000..cde51fccc62
--- /dev/null
+++ b/lib/api/helpers/headers_helpers.rb
@@ -0,0 +1,11 @@
+module API
+ module Helpers
+ module HeadersHelpers
+ def set_http_headers(header_data)
+ header_data.each do |key, value|
+ header "X-Gitlab-#{key.to_s.split('_').collect(&:capitalize).join('-')}", value
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb
index abe3d353984..83151be82ad 100644
--- a/lib/api/helpers/internal_helpers.rb
+++ b/lib/api/helpers/internal_helpers.rb
@@ -89,12 +89,6 @@ module API
end
end
- # Return the repository full path so that gitlab-shell has it when
- # handling ssh commands
- def repository_path
- repository.path_to_repo
- end
-
# Return the Gitaly Address if it is enabled
def gitaly_payload(action)
return unless %w[git-receive-pack git-upload-pack git-upload-archive].include?(action)
diff --git a/lib/api/helpers/related_resources_helpers.rb b/lib/api/helpers/related_resources_helpers.rb
index a2cbed30229..bc7333ca4b3 100644
--- a/lib/api/helpers/related_resources_helpers.rb
+++ b/lib/api/helpers/related_resources_helpers.rb
@@ -1,7 +1,7 @@
module API
module Helpers
module RelatedResourcesHelpers
- include GrapeRouteHelpers::NamedRouteMatcher
+ include GrapePathHelpers::NamedRouteMatcher
def issues_available?(project, options)
available?(:issues, project, options[:current_user])
diff --git a/lib/api/helpers/runner.rb b/lib/api/helpers/runner.rb
index 35ac0b4cbca..61eb88d3331 100644
--- a/lib/api/helpers/runner.rb
+++ b/lib/api/helpers/runner.rb
@@ -59,6 +59,11 @@ module API
def max_artifacts_size
Gitlab::CurrentSettings.max_artifacts_size.megabytes.to_i
end
+
+ def job_forbidden!(job, reason)
+ header 'Job-Status', job.status
+ forbidden!(reason)
+ end
end
end
end
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index 6b72caea8fd..a9803be9f69 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -59,7 +59,11 @@ module API
status: true,
gl_repository: gl_repository,
gl_username: user&.username,
- repository_path: repository_path,
+
+ # This repository_path is a bogus value but gitlab-shell still requires
+ # its presence. https://gitlab.com/gitlab-org/gitlab-shell/issues/135
+ repository_path: '/',
+
gitaly: gitaly_payload(params[:action])
}
end
@@ -113,7 +117,7 @@ module API
{
api_version: API.version,
gitlab_version: Gitlab::VERSION,
- gitlab_rev: Gitlab::REVISION,
+ gitlab_rev: Gitlab.revision,
redis: redis_ping
}
end
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 6d75e8817c4..25185d6edc8 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -16,7 +16,7 @@ module API
args[:scope] = args[:scope].underscore if args[:scope]
issues = IssuesFinder.new(current_user, args).execute
- .preload(:assignees, :labels, :notes, :timelogs)
+ .preload(:assignees, :labels, :notes, :timelogs, :project, :author)
issues.reorder(args[:order_by] => args[:sort])
end
diff --git a/lib/api/jobs.rb b/lib/api/jobs.rb
index 54d1acbd412..e95b0dd5267 100644
--- a/lib/api/jobs.rb
+++ b/lib/api/jobs.rb
@@ -54,6 +54,7 @@ module API
pipeline = user_project.pipelines.find(params[:pipeline_id])
builds = pipeline.builds
builds = filter_builds(builds, params[:scope])
+ builds = builds.preload(:job_artifacts_archive)
present paginate(builds), with: Entities::Job
end
diff --git a/lib/api/markdown.rb b/lib/api/markdown.rb
index b9ed68aa584..5d55224c1a7 100644
--- a/lib/api/markdown.rb
+++ b/lib/api/markdown.rb
@@ -10,9 +10,7 @@ module API
detail "This feature was introduced in GitLab 11.0."
end
post do
- # Explicitly set CommonMark as markdown engine to use.
- # Remove this set when https://gitlab.com/gitlab-org/gitlab-ce/issues/43011 is done.
- context = { markdown_engine: :common_mark, only_path: false }
+ context = { only_path: false }
if params[:project]
project = Project.find_by_full_path(params[:project])
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index bc4df16e3a8..2621c9f8fc2 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -10,12 +10,6 @@ module API
helpers do
params :optional_params_ee do
end
-
- params :merge_params_ee do
- end
-
- def update_merge_request_ee(merge_request)
- end
end
def self.update_params_at_least_one_of
@@ -29,6 +23,7 @@ module API
target_branch
title
discussion_locked
+ squash
]
end
@@ -43,7 +38,7 @@ module API
merge_requests = MergeRequestsFinder.new(current_user, args).execute
.reorder(args[:order_by] => args[:sort])
merge_requests = paginate(merge_requests)
- .preload(:target_project)
+ .preload(:source_project, :target_project)
return merge_requests if args[:view] == 'simple'
@@ -64,9 +59,21 @@ module API
end
end
+ def serializer_options_for(merge_requests)
+ options = { with: Entities::MergeRequestBasic, current_user: current_user }
+
+ if params[:view] == 'simple'
+ options[:with] = Entities::MergeRequestSimple
+ else
+ options[:issuable_metadata] = issuable_meta_data(merge_requests, 'MergeRequest')
+ end
+
+ options
+ end
+
params :merge_requests_params do
- optional :state, type: String, values: %w[opened closed merged all], default: 'all',
- desc: 'Return opened, closed, merged, or all merge requests'
+ optional :state, type: String, values: %w[opened closed locked merged all], default: 'all',
+ desc: 'Return opened, closed, locked, merged, or all merge requests'
optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at',
desc: 'Return merge requests ordered by `created_at` or `updated_at` fields.'
optional :sort, type: String, values: %w[asc desc], default: 'desc',
@@ -103,16 +110,26 @@ module API
authenticate! unless params[:scope] == 'all'
merge_requests = find_merge_requests
- options = { with: Entities::MergeRequestBasic,
- current_user: current_user }
+ present merge_requests, serializer_options_for(merge_requests)
+ end
+ end
- if params[:view] == 'simple'
- options[:with] = Entities::MergeRequestSimple
- else
- options[:issuable_metadata] = issuable_meta_data(merge_requests, 'MergeRequest')
- end
+ params do
+ requires :id, type: String, desc: 'The ID of a group'
+ end
+ resource :groups, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ desc 'Get a list of group merge requests' do
+ success Entities::MergeRequestBasic
+ end
+ params do
+ use :merge_requests_params
+ end
+ get ":id/merge_requests" do
+ group = find_group!(params[:id])
- present merge_requests, options
+ merge_requests = find_merge_requests(group_id: group.id, include_subgroups: true)
+
+ present merge_requests, serializer_options_for(merge_requests)
end
end
@@ -145,7 +162,9 @@ module API
optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign the merge request'
optional :labels, type: String, desc: 'Comma-separated list of label names'
optional :remove_source_branch, type: Boolean, desc: 'Remove source branch when merging'
- optional :allow_maintainer_to_push, type: Boolean, desc: 'Whether a maintainer of the target project can push to the source project'
+ optional :allow_collaboration, type: Boolean, desc: 'Allow commits from members who can merge to the target branch'
+ optional :allow_maintainer_to_push, type: Boolean, as: :allow_collaboration, desc: '[deprecated] See allow_collaboration'
+ optional :squash, type: Grape::API::Boolean, desc: 'When true, the commits will be squashed into a single commit on merge'
use :optional_params_ee
end
@@ -163,15 +182,8 @@ module API
merge_requests = find_merge_requests(project_id: user_project.id)
- options = { with: Entities::MergeRequestBasic,
- current_user: current_user,
- project: user_project }
-
- if params[:view] == 'simple'
- options[:with] = Entities::MergeRequestSimple
- else
- options[:issuable_metadata] = issuable_meta_data(merge_requests, 'MergeRequest')
- end
+ options = serializer_options_for(merge_requests)
+ options[:project] = user_project
present merge_requests, options
end
@@ -220,6 +232,7 @@ module API
params do
requires :merge_request_iid, type: Integer, desc: 'The IID of a merge request'
+ optional :render_html, type: Boolean, desc: 'Returns the description and title rendered HTML'
end
desc 'Get a single merge request' do
success Entities::MergeRequest
@@ -227,7 +240,7 @@ module API
get ':id/merge_requests/:merge_request_iid' do
merge_request = find_merge_request_with_access(params[:merge_request_iid])
- present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
+ present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project, render_html: params[:render_html]
end
desc 'Get the participants of a merge request' do
@@ -308,8 +321,7 @@ module API
optional :merge_when_pipeline_succeeds, type: Boolean,
desc: 'When true, this merge request will be merged when the pipeline succeeds'
optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch'
-
- use :merge_params_ee
+ optional :squash, type: Grape::API::Boolean, desc: 'When true, the commits will be squashed into a single commit on merge'
end
put ':id/merge_requests/:merge_request_iid/merge' do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42317')
@@ -327,7 +339,7 @@ module API
check_sha_param!(params, merge_request)
- update_merge_request_ee(merge_request)
+ merge_request.update(squash: params[:squash]) if params[:squash]
merge_params = {
commit_message: params[:merge_commit_message],
diff --git a/lib/api/milestone_responses.rb b/lib/api/milestone_responses.rb
index c570eace862..a8eb137e46a 100644
--- a/lib/api/milestone_responses.rb
+++ b/lib/api/milestone_responses.rb
@@ -24,7 +24,7 @@ module API
optional :state_event, type: String, values: %w[close activate],
desc: 'The state event of the milestone '
use :optional_params
- at_least_one_of :title, :description, :due_date, :state_event
+ at_least_one_of :title, :description, :start_date, :due_date, :state_event
end
def list_milestones_for(parent)
diff --git a/lib/api/pipelines.rb b/lib/api/pipelines.rb
index 735591fedd5..5d33a13d035 100644
--- a/lib/api/pipelines.rb
+++ b/lib/api/pipelines.rb
@@ -31,7 +31,7 @@ module API
get ':id/pipelines' do
authorize! :read_pipeline, user_project
- pipelines = PipelinesFinder.new(user_project, params).execute
+ pipelines = PipelinesFinder.new(user_project, current_user, params).execute
present paginate(pipelines), with: Entities::PipelineBasic
end
@@ -41,15 +41,20 @@ module API
end
params do
requires :ref, type: String, desc: 'Reference'
+ optional :variables, Array, desc: 'Array of variables available in the pipeline'
end
post ':id/pipeline' do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42124')
authorize! :create_pipeline, user_project
+ pipeline_params = declared_params(include_missing: false)
+ .merge(variables_attributes: params[:variables])
+ .except(:variables)
+
new_pipeline = Ci::CreatePipelineService.new(user_project,
current_user,
- declared_params(include_missing: false))
+ pipeline_params)
.execute(:api, ignore_skip_ci: true, save_on_errors: false)
if new_pipeline.persisted?
diff --git a/lib/api/project_export.rb b/lib/api/project_export.rb
index 5ef4e9d530c..15c57a2fc02 100644
--- a/lib/api/project_export.rb
+++ b/lib/api/project_export.rb
@@ -23,9 +23,13 @@ module API
get ':id/export/download' do
path = user_project.export_project_path
- render_api_error!('404 Not found or has expired', 404) unless path
-
- present_disk_file!(path, File.basename(path), 'application/gzip')
+ if path
+ present_disk_file!(path, File.basename(path), 'application/gzip')
+ elsif user_project.export_project_object_exists?
+ present_carrierwave_file!(user_project.import_export_upload.export_file)
+ else
+ render_api_error!('404 Not found or has expired', 404)
+ end
end
desc 'Start export' do
diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb
index 68921ae439b..4760a1c08d7 100644
--- a/lib/api/project_hooks.rb
+++ b/lib/api/project_hooks.rb
@@ -80,7 +80,7 @@ module API
update_params = declared_params(include_missing: false)
- if hook.update_attributes(update_params)
+ if hook.update(update_params)
present hook, with: Entities::ProjectHook
else
error!("Invalid url given", 422) if hook.errors[:url].present?
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 8871792060b..8273abe48c9 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -58,16 +58,9 @@ module API
projects = paginate(projects)
projects, options = with_custom_attributes(projects, options)
- if current_user
- project_members = current_user.project_members.preload(:source, user: [notification_settings: :source])
- group_members = current_user.group_members.preload(:source, user: [notification_settings: :source])
- end
-
options = options.reverse_merge(
with: current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails,
statistics: params[:statistics],
- project_members: project_members,
- group_members: group_members,
current_user: current_user
)
options[:with] = Entities::BasicProjectDetails if params[:simple]
@@ -267,7 +260,8 @@ module API
:snippets_enabled,
:tag_list,
:visibility,
- :wiki_enabled
+ :wiki_enabled,
+ :avatar
]
optional :name, type: String, desc: 'The name of the project'
optional :default_branch, type: String, desc: 'The default branch of the project'
@@ -466,6 +460,23 @@ module API
conflict!(error.message)
end
end
+
+ desc 'Transfer a project to a new namespace'
+ params do
+ requires :namespace, type: String, desc: 'The ID or path of the new namespace'
+ end
+ put ":id/transfer" do
+ authorize! :change_namespace, user_project
+
+ namespace = find_namespace!(params[:namespace])
+ result = ::Projects::TransferService.new(user_project, current_user).execute(namespace)
+
+ if result
+ present user_project, with: Entities::Project
+ else
+ render_api_error!("Failed to transfer project #{user_project.errors.messages}", 400)
+ end
+ end
end
end
end
diff --git a/lib/api/protected_branches.rb b/lib/api/protected_branches.rb
index aa7cab4a741..a30eb46c220 100644
--- a/lib/api/protected_branches.rb
+++ b/lib/api/protected_branches.rb
@@ -41,10 +41,10 @@ module API
requires :name, type: String, desc: 'The name of the protected branch'
optional :push_access_level, type: Integer,
values: ProtectedRefAccess::ALLOWED_ACCESS_LEVELS,
- desc: 'Access levels allowed to push (defaults: `40`, master access level)'
+ desc: 'Access levels allowed to push (defaults: `40`, maintainer access level)'
optional :merge_access_level, type: Integer,
values: ProtectedRefAccess::ALLOWED_ACCESS_LEVELS,
- desc: 'Access levels allowed to merge (defaults: `40`, master access level)'
+ desc: 'Access levels allowed to merge (defaults: `40`, maintainer access level)'
end
post ':id/protected_branches' do
protected_branch = user_project.protected_branches.find_by(name: params[:name])
diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb
index bb3fa99af38..33a9646ac3b 100644
--- a/lib/api/repositories.rb
+++ b/lib/api/repositories.rb
@@ -100,9 +100,10 @@ module API
params do
requires :from, type: String, desc: 'The commit, branch name, or tag name to start comparison'
requires :to, type: String, desc: 'The commit, branch name, or tag name to stop comparison'
+ optional :straight, type: Boolean, desc: 'Comparison method, `true` for direct comparison between `from` and `to` (`from`..`to`), `false` to compare using merge base (`from`...`to`)', default: false
end
get ':id/repository/compare' do
- compare = Gitlab::Git::Compare.new(user_project.repository.raw_repository, params[:from], params[:to])
+ compare = Gitlab::Git::Compare.new(user_project.repository.raw_repository, params[:from], params[:to], straight: params[:straight])
present compare, with: Entities::Compare
end
diff --git a/lib/api/runner.rb b/lib/api/runner.rb
index a7f1cb1131f..d0cc0945a5f 100644
--- a/lib/api/runner.rb
+++ b/lib/api/runner.rb
@@ -21,24 +21,26 @@ module API
attributes = attributes_for_keys([:description, :active, :locked, :run_untagged, :tag_list, :maximum_timeout])
.merge(get_runner_details_from_request)
- runner =
+ attributes =
if runner_registration_token_valid?
# Create shared runner. Requires admin access
- Ci::Runner.create(attributes.merge(is_shared: true, runner_type: :instance_type))
+ attributes.merge(runner_type: :instance_type)
elsif project = Project.find_by(runners_token: params[:token])
# Create a specific runner for the project
- project.runners.create(attributes.merge(runner_type: :project_type))
+ attributes.merge(runner_type: :project_type, projects: [project])
elsif group = Group.find_by(runners_token: params[:token])
# Create a specific runner for the group
- group.runners.create(attributes.merge(runner_type: :group_type))
+ attributes.merge(runner_type: :group_type, groups: [group])
+ else
+ forbidden!
end
- break forbidden! unless runner
+ runner = Ci::Runner.create(attributes)
- if runner.id
+ if runner.persisted?
present runner, with: Entities::RunnerRegistrationDetails
else
- not_found!
+ render_validation_error!(runner)
end
end
@@ -79,19 +81,30 @@ module API
requires :token, type: String, desc: %q(Runner's authentication token)
optional :last_update, type: String, desc: %q(Runner's queue last_update token)
optional :info, type: Hash, desc: %q(Runner's metadata)
+ optional :session, type: Hash, desc: %q(Runner's session data) do
+ optional :url, type: String, desc: %q(Session's url)
+ optional :certificate, type: String, desc: %q(Session's certificate)
+ optional :authorization, type: String, desc: %q(Session's authorization)
+ end
end
post '/request' do
authenticate_runner!
- no_content! unless current_runner.active?
- if current_runner.runner_queue_value_latest?(params[:last_update])
- header 'X-GitLab-Last-Update', params[:last_update]
+ unless current_runner.active?
+ header 'X-GitLab-Last-Update', current_runner.ensure_runner_queue_value
+ break no_content!
+ end
+
+ runner_params = declared_params(include_missing: false)
+
+ if current_runner.runner_queue_value_latest?(runner_params[:last_update])
+ header 'X-GitLab-Last-Update', runner_params[:last_update]
Gitlab::Metrics.add_event(:build_not_found_cached)
break no_content!
end
new_update = current_runner.ensure_runner_queue_value
- result = ::Ci::RegisterJobService.new(current_runner).execute
+ result = ::Ci::RegisterJobService.new(current_runner).execute(runner_params)
if result.valid?
if result.build
@@ -123,6 +136,7 @@ module API
end
put '/:id' do
job = authenticate_job!
+ job_forbidden!(job, 'Job is not running') unless job.running?
job.trace.set(params[:trace]) if params[:trace]
@@ -130,10 +144,12 @@ module API
project: job.project.full_path)
case params[:state].to_s
+ when 'running'
+ job.touch if job.needs_touch?
when 'success'
- job.success
+ job.success!
when 'failed'
- job.drop(params[:failure_reason] || :unknown_failure)
+ job.drop!(params[:failure_reason] || :unknown_failure)
end
end
@@ -149,7 +165,7 @@ module API
end
patch '/:id/trace' do
job = authenticate_job!
- forbidden!('Job is not running') unless job.running?
+ job_forbidden!(job, 'Job is not running') unless job.running?
error!('400 Missing header Content-Range', 400) unless request.headers.key?('Content-Range')
content_range = request.headers['Content-Range']
@@ -202,7 +218,7 @@ module API
status 200
content_type Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE
- JobArtifactUploader.workhorse_authorize
+ JobArtifactUploader.workhorse_authorize(has_length: false, maximum_size: max_artifacts_size)
end
desc 'Upload artifacts for job' do
diff --git a/lib/api/runners.rb b/lib/api/runners.rb
index 5cb96d467c0..51242341dba 100644
--- a/lib/api/runners.rb
+++ b/lib/api/runners.rb
@@ -58,7 +58,7 @@ module API
optional :access_level, type: String, values: Ci::Runner.access_levels.keys,
desc: 'The access_level of the runner'
optional :maximum_timeout, type: Integer, desc: 'Maximum timeout set when this Runner will handle the job'
- at_least_one_of :description, :active, :tag_list, :run_untagged, :locked, :access_level
+ at_least_one_of :description, :active, :tag_list, :run_untagged, :locked, :access_level, :maximum_timeout
end
put ':id' do
runner = get_runner(params.delete(:id))
@@ -119,7 +119,7 @@ module API
use :pagination
end
get ':id/runners' do
- runners = filter_runners(Ci::Runner.owned_or_shared(user_project.id), params[:scope])
+ runners = filter_runners(Ci::Runner.owned_or_instance_wide(user_project.id), params[:scope])
present paginate(runners), with: Entities::Runner
end
@@ -133,12 +133,10 @@ module API
runner = get_runner(params[:runner_id])
authenticate_enable_runner!(runner)
- runner_project = runner.assign_to(user_project)
-
- if runner_project.persisted?
+ if runner.assign_to(user_project)
present runner, with: Entities::Runner
else
- conflict!("Runner was already enabled for this project")
+ render_validation_error!(runner)
end
end
@@ -172,6 +170,11 @@ module API
render_api_error!('Scope contains invalid value', 400)
end
+ # Support deprecated scopes
+ if runners.respond_to?("deprecated_#{scope}")
+ scope = "deprecated_#{scope}"
+ end
+
runners.public_send(scope) # rubocop:disable GitlabSecurity/PublicSend
end
@@ -182,7 +185,7 @@ module API
end
def authenticate_show_runner!(runner)
- return if runner.is_shared || current_user.admin?
+ return if runner.instance_type? || current_user.admin?
forbidden!("No access granted") unless can?(current_user, :read_runner, runner)
end
diff --git a/lib/api/search.rb b/lib/api/search.rb
index 5d9ec617cb7..37fbabe419c 100644
--- a/lib/api/search.rb
+++ b/lib/api/search.rb
@@ -34,9 +34,7 @@ module API
def process_results(results)
case params[:scope]
- when 'wiki_blobs'
- paginate(results).map { |blob| Gitlab::ProjectSearchResults.parse_search_result(blob, user_project) }
- when 'blobs'
+ when 'blobs', 'wiki_blobs'
paginate(results).map { |blob| blob[1] }
else
paginate(results)
diff --git a/lib/api/services.rb b/lib/api/services.rb
index 794fdab8f2b..553e8dff4b9 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -787,7 +787,7 @@ module API
service = user_project.find_or_initialize_service(service_slug.underscore)
service_params = declared_params(include_missing: false).merge(active: true)
- if service.update_attributes(service_params)
+ if service.update(service_params)
present service, with: Entities::ProjectService
else
render_api_error!('400 Bad Request', 400)
@@ -807,7 +807,7 @@ module API
hash.merge!(key => nil)
end
- unless service.update_attributes(attrs.merge(active: false))
+ unless service.update(attrs.merge(active: false))
render_api_error!('400 Bad Request', 400)
end
end
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index e31c332b6e4..02ef89f997f 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -24,7 +24,7 @@ module API
optional :default_project_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default project visibility'
optional :default_snippet_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default snippet visibility'
optional :default_group_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default group visibility'
- optional :restricted_visibility_levels, type: Array[String], desc: 'Selected levels cannot be used by non-admin users for projects or snippets. If the public level is restricted, user profiles are only visible to logged in users.'
+ optional :restricted_visibility_levels, type: Array[String], desc: 'Selected levels cannot be used by non-admin users for groups, projects or snippets. If the public level is restricted, user profiles are only visible to logged in users.'
optional :import_sources, type: Array[String], values: %w[github bitbucket gitlab google_code fogbugz git gitlab_project],
desc: 'Enabled sources for code import during project creation. OmniAuth must be configured for GitHub, Bitbucket, and GitLab.com'
optional :disabled_oauth_sign_in_sources, type: Array[String], desc: 'Disable certain OAuth sign-in sources'
@@ -49,6 +49,9 @@ module API
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 :performance_bar_allowed_group_path, type: String, desc: 'Path of the group that is allowed to toggle the performance bar.'
+ optional :performance_bar_allowed_group_id, type: String, desc: 'Depreated: Use :performance_bar_allowed_group_path instead. Path of the group that is allowed to toggle the performance bar.' # support legacy names, can be removed in v6
+ optional :performance_bar_enabled, type: String, desc: 'Deprecated: Pass `performance_bar_allowed_group_path: nil` instead. Allow enabling the performance.' # support legacy names, can be removed in v6
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'
@@ -126,6 +129,7 @@ module API
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.'
+ optional :usage_ping_enabled, type: Boolean, desc: 'Every week GitLab will report license usage back to GitLab, Inc.'
ApplicationSetting::SUPPORTED_KEY_TYPES.each do |type|
optional :"#{type}_key_restriction",
@@ -134,12 +138,25 @@ module API
desc: "Restrictions on the complexity of uploaded #{type.upcase} keys. A value of #{ApplicationSetting::FORBIDDEN_KEY_VALUE} disables all #{type.upcase} keys."
end
- optional(*::ApplicationSettingsHelper.visible_attributes)
- at_least_one_of(*::ApplicationSettingsHelper.visible_attributes)
+ optional_attributes = ::ApplicationSettingsHelper.visible_attributes << :performance_bar_allowed_group_id
+
+ optional(*optional_attributes)
+ at_least_one_of(*optional_attributes)
end
put "application/settings" do
attrs = declared_params(include_missing: false)
+ # support legacy names, can be removed in v6
+ if attrs.has_key?(:performance_bar_allowed_group_id)
+ attrs[:performance_bar_allowed_group_path] = attrs.delete(:performance_bar_allowed_group_id)
+ end
+
+ # support legacy names, can be removed in v6
+ if attrs.has_key?(:performance_bar_enabled)
+ performance_bar_enabled = attrs.delete(:performance_bar_allowed_group_id)
+ attrs[:performance_bar_allowed_group_path] = nil unless performance_bar_enabled
+ end
+
# support legacy names, can be removed in v5
if attrs.has_key?(:signin_enabled)
attrs[:password_authentication_enabled_for_web] = attrs.delete(:signin_enabled)
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 14b8a796c8e..5aaaf104dff 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -186,7 +186,7 @@ module API
identity = user.identities.find_by(provider: identity_attrs[:provider])
if identity
- identity.update_attributes(identity_attrs)
+ identity.update(identity_attrs)
else
identity = user.identities.build(identity_attrs)
identity.save
@@ -531,18 +531,22 @@ module API
authenticate!
end
- desc 'Get the currently authenticated user' do
- success Entities::UserPublic
- end
- get do
- entity =
- if current_user.admin?
- Entities::UserWithAdmin
- else
- Entities::UserPublic
- end
+ # Enabling /user endpoint for the v3 version to allow oauth
+ # authentication through this endpoint.
+ version %w(v3 v4), using: :path do
+ desc 'Get the currently authenticated user' do
+ success Entities::UserPublic
+ end
+ get do
+ entity =
+ if current_user.admin?
+ Entities::UserWithAdmin
+ else
+ Entities::UserPublic
+ end
- present current_user, with: entity
+ present current_user, with: entity
+ end
end
desc "Get the currently authenticated user's SSH keys" do
diff --git a/lib/api/v3/award_emoji.rb b/lib/api/v3/award_emoji.rb
deleted file mode 100644
index b96b2d70b12..00000000000
--- a/lib/api/v3/award_emoji.rb
+++ /dev/null
@@ -1,130 +0,0 @@
-module API
- module V3
- class AwardEmoji < Grape::API
- include PaginationParams
-
- before { authenticate! }
- AWARDABLES = %w[issue merge_request snippet].freeze
-
- resource :projects, requirements: { id: %r{[^/]+} } do
- AWARDABLES.each do |awardable_type|
- awardable_string = awardable_type.pluralize
- awardable_id_string = "#{awardable_type}_id"
-
- params do
- requires :id, type: String, desc: 'The ID of a project'
- requires :"#{awardable_id_string}", type: Integer, desc: "The ID of an Issue, Merge Request or Snippet"
- end
-
- [
- ":id/#{awardable_string}/:#{awardable_id_string}/award_emoji",
- ":id/#{awardable_string}/:#{awardable_id_string}/notes/:note_id/award_emoji"
- ].each do |endpoint|
-
- desc 'Get a list of project +awardable+ award emoji' do
- detail 'This feature was introduced in 8.9'
- success Entities::AwardEmoji
- end
- params do
- use :pagination
- end
- get endpoint do
- if can_read_awardable?
- awards = awardable.award_emoji
- present paginate(awards), with: Entities::AwardEmoji
- else
- not_found!("Award Emoji")
- end
- end
-
- desc 'Get a specific award emoji' do
- detail 'This feature was introduced in 8.9'
- success Entities::AwardEmoji
- end
- params do
- requires :award_id, type: Integer, desc: 'The ID of the award'
- end
- get "#{endpoint}/:award_id" do
- if can_read_awardable?
- present awardable.award_emoji.find(params[:award_id]), with: Entities::AwardEmoji
- else
- not_found!("Award Emoji")
- end
- end
-
- desc 'Award a new Emoji' do
- detail 'This feature was introduced in 8.9'
- success Entities::AwardEmoji
- end
- params do
- requires :name, type: String, desc: 'The name of a award_emoji (without colons)'
- end
- post endpoint do
- not_found!('Award Emoji') unless can_read_awardable? && can_award_awardable?
-
- award = awardable.create_award_emoji(params[:name], current_user)
-
- if award.persisted?
- present award, with: Entities::AwardEmoji
- else
- not_found!("Award Emoji #{award.errors.messages}")
- end
- end
-
- desc 'Delete a +awardables+ award emoji' do
- detail 'This feature was introduced in 8.9'
- success Entities::AwardEmoji
- end
- params do
- requires :award_id, type: Integer, desc: 'The ID of an award emoji'
- end
- delete "#{endpoint}/:award_id" do
- award = awardable.award_emoji.find(params[:award_id])
-
- unauthorized! unless award.user == current_user || current_user.admin?
-
- award.destroy
- present award, with: Entities::AwardEmoji
- end
- end
- end
- end
-
- helpers do
- def can_read_awardable?
- can?(current_user, read_ability(awardable), awardable)
- end
-
- def can_award_awardable?
- awardable.user_can_award?(current_user, params[:name])
- end
-
- def awardable
- @awardable ||=
- begin
- if params.include?(:note_id)
- note_id = params.delete(:note_id)
-
- awardable.notes.find(note_id)
- elsif params.include?(:issue_id)
- user_project.issues.find(params[:issue_id])
- elsif params.include?(:merge_request_id)
- user_project.merge_requests.find(params[:merge_request_id])
- else
- user_project.snippets.find(params[:snippet_id])
- end
- end
- end
-
- def read_ability(awardable)
- case awardable
- when Note
- read_ability(awardable.noteable)
- else
- :"read_#{awardable.class.to_s.underscore}"
- end
- end
- end
- end
- end
-end
diff --git a/lib/api/v3/boards.rb b/lib/api/v3/boards.rb
deleted file mode 100644
index 94acc67171e..00000000000
--- a/lib/api/v3/boards.rb
+++ /dev/null
@@ -1,72 +0,0 @@
-module API
- module V3
- class Boards < Grape::API
- before { authenticate! }
-
- params do
- requires :id, type: String, desc: 'The ID of a project'
- end
- resource :projects, requirements: { id: %r{[^/]+} } do
- desc 'Get all project boards' do
- detail 'This feature was introduced in 8.13'
- success ::API::Entities::Board
- end
- get ':id/boards' do
- authorize!(:read_board, user_project)
- present user_project.boards, with: ::API::Entities::Board
- end
-
- params do
- requires :board_id, type: Integer, desc: 'The ID of a board'
- end
- segment ':id/boards/:board_id' do
- helpers do
- def project_board
- board = user_project.boards.first
-
- if params[:board_id] == board.id
- board
- else
- not_found!('Board')
- end
- end
-
- def board_lists
- project_board.lists.destroyable
- end
- end
-
- desc 'Get the lists of a project board' do
- detail 'Does not include `done` list. This feature was introduced in 8.13'
- success ::API::Entities::List
- end
- get '/lists' do
- authorize!(:read_board, user_project)
- present board_lists, with: ::API::Entities::List
- end
-
- desc 'Delete a board list' do
- detail 'This feature was introduced in 8.13'
- success ::API::Entities::List
- end
- params do
- requires :list_id, type: Integer, desc: 'The ID of a board list'
- end
- delete "/lists/:list_id" do
- authorize!(:admin_list, user_project)
-
- list = board_lists.find(params[:list_id])
-
- service = ::Boards::Lists::DestroyService.new(user_project, current_user)
-
- if service.execute(list)
- present list, with: ::API::Entities::List
- else
- render_api_error!({ error: 'List could not be deleted!' }, 400)
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/api/v3/branches.rb b/lib/api/v3/branches.rb
deleted file mode 100644
index 25176c5b38e..00000000000
--- a/lib/api/v3/branches.rb
+++ /dev/null
@@ -1,76 +0,0 @@
-require 'mime/types'
-
-module API
- module V3
- class Branches < Grape::API
- before { authenticate! }
- before { authorize! :download_code, user_project }
-
- params do
- requires :id, type: String, desc: 'The ID of a project'
- end
- resource :projects, requirements: { id: %r{[^/]+} } do
- desc 'Get a project repository branches' do
- success ::API::Entities::Branch
- end
- get ":id/repository/branches" do
- Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42276')
-
- 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, merged_branch_names: merged_branch_names
- end
-
- desc 'Delete a branch'
- params do
- requires :branch, type: String, desc: 'The name of the branch'
- end
- delete ":id/repository/branches/:branch", requirements: { branch: /.+/ } do
- authorize_push_project
-
- result = DeleteBranchService.new(user_project, current_user)
- .execute(params[:branch])
-
- if result[:status] == :success
- status(200)
- {
- branch_name: params[:branch]
- }
- else
- render_api_error!(result[:message], result[:return_code])
- end
- end
-
- desc 'Delete all merged branches'
- delete ":id/repository/merged_branches" do
- DeleteMergedBranchesService.new(user_project, current_user).async_execute
-
- status(200)
- end
-
- desc 'Create branch' do
- success ::API::Entities::Branch
- end
- params do
- requires :branch_name, type: String, desc: 'The name of the branch'
- requires :ref, type: String, desc: 'Create branch from commit sha or existing branch'
- end
- post ":id/repository/branches" do
- authorize_push_project
- result = CreateBranchService.new(user_project, current_user)
- .execute(params[:branch_name], params[:ref])
-
- if result[:status] == :success
- present result[:branch],
- with: ::API::Entities::Branch,
- project: user_project
- else
- render_api_error!(result[:message], 400)
- end
- end
- end
- end
- end
-end
diff --git a/lib/api/v3/broadcast_messages.rb b/lib/api/v3/broadcast_messages.rb
deleted file mode 100644
index 417e4ad0b26..00000000000
--- a/lib/api/v3/broadcast_messages.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-module API
- module V3
- class BroadcastMessages < Grape::API
- include PaginationParams
-
- before { authenticate! }
- before { authenticated_as_admin! }
-
- resource :broadcast_messages do
- helpers do
- def find_message
- BroadcastMessage.find(params[:id])
- end
- end
-
- desc 'Delete a broadcast message' do
- detail 'This feature was introduced in GitLab 8.12.'
- success ::API::Entities::BroadcastMessage
- end
- params do
- requires :id, type: Integer, desc: 'Broadcast message ID'
- end
- delete ':id' do
- message = find_message
-
- present message.destroy, with: ::API::Entities::BroadcastMessage
- end
- end
- end
- end
-end
diff --git a/lib/api/v3/builds.rb b/lib/api/v3/builds.rb
deleted file mode 100644
index b49448e1e67..00000000000
--- a/lib/api/v3/builds.rb
+++ /dev/null
@@ -1,250 +0,0 @@
-module API
- module V3
- class Builds < Grape::API
- include PaginationParams
-
- before { authenticate! }
-
- params do
- requires :id, type: String, desc: 'The ID of a project'
- end
- 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',
- values: %w(pending running failed success canceled skipped),
- coerce_with: ->(scope) {
- if scope.is_a?(String)
- [scope]
- elsif scope.is_a?(::Hash)
- scope.values
- else
- ['unknown']
- end
- }
- end
- end
-
- desc 'Get a project builds' do
- success ::API::V3::Entities::Build
- end
- params do
- use :optional_scope
- use :pagination
- end
- get ':id/builds' do
- builds = user_project.builds.order('id DESC')
- builds = filter_builds(builds, params[:scope])
-
- builds = builds.preload(:user, :job_artifacts_archive, :runner, pipeline: :project)
- present paginate(builds), with: ::API::V3::Entities::Build
- end
-
- desc 'Get builds for a specific commit of a project' do
- success ::API::V3::Entities::Build
- end
- params do
- requires :sha, type: String, desc: 'The SHA id of a commit'
- use :optional_scope
- use :pagination
- end
- get ':id/repository/commits/:sha/builds' do
- authorize_read_builds!
-
- break not_found! unless user_project.commit(params[:sha])
-
- pipelines = user_project.pipelines.where(sha: params[:sha])
- builds = user_project.builds.where(pipeline: pipelines).order('id DESC')
- builds = filter_builds(builds, params[:scope])
-
- present paginate(builds), with: ::API::V3::Entities::Build
- end
-
- desc 'Get a specific build of a project' do
- success ::API::V3::Entities::Build
- end
- params do
- requires :build_id, type: Integer, desc: 'The ID of a build'
- end
- get ':id/builds/:build_id' do
- authorize_read_builds!
-
- build = get_build!(params[:build_id])
-
- present build, with: ::API::V3::Entities::Build
- end
-
- desc 'Download the artifacts file from build' do
- detail 'This feature was introduced in GitLab 8.5'
- end
- params do
- requires :build_id, type: Integer, desc: 'The ID of a build'
- end
- get ':id/builds/:build_id/artifacts' do
- authorize_read_builds!
-
- build = get_build!(params[:build_id])
-
- present_carrierwave_file!(build.artifacts_file)
- end
-
- desc 'Download the artifacts file from build' do
- detail 'This feature was introduced in GitLab 8.10'
- end
- params do
- requires :ref_name, type: String, desc: 'The ref from repository'
- requires :job, type: String, desc: 'The name for the build'
- end
- get ':id/builds/artifacts/:ref_name/download',
- requirements: { ref_name: /.+/ } do
- authorize_read_builds!
-
- builds = user_project.latest_successful_builds_for(params[:ref_name])
- latest_build = builds.find_by!(name: params[:job])
-
- present_carrierwave_file!(latest_build.artifacts_file)
- end
-
- # TODO: We should use `present_disk_file!` and leave this implementation for backward compatibility (when build trace
- # is saved in the DB instead of file). But before that, we need to consider how to replace the value of
- # `runners_token` with some mask (like `xxxxxx`) when sending trace file directly by workhorse.
- desc 'Get a trace of a specific build of a project'
- params do
- requires :build_id, type: Integer, desc: 'The ID of a build'
- end
- get ':id/builds/:build_id/trace' do
- authorize_read_builds!
-
- build = get_build!(params[:build_id])
-
- header 'Content-Disposition', "infile; filename=\"#{build.id}.log\""
- content_type 'text/plain'
- env['api.format'] = :binary
-
- trace = build.trace.raw
- body trace
- end
-
- desc 'Cancel a specific build of a project' do
- success ::API::V3::Entities::Build
- end
- params do
- requires :build_id, type: Integer, desc: 'The ID of a build'
- end
- post ':id/builds/:build_id/cancel' do
- authorize_update_builds!
-
- build = get_build!(params[:build_id])
- authorize!(:update_build, build)
-
- build.cancel
-
- present build, with: ::API::V3::Entities::Build
- end
-
- desc 'Retry a specific build of a project' do
- success ::API::V3::Entities::Build
- end
- params do
- requires :build_id, type: Integer, desc: 'The ID of a build'
- end
- post ':id/builds/:build_id/retry' do
- authorize_update_builds!
-
- build = get_build!(params[:build_id])
- authorize!(:update_build, build)
- break forbidden!('Build is not retryable') unless build.retryable?
-
- build = Ci::Build.retry(build, current_user)
-
- present build, with: ::API::V3::Entities::Build
- end
-
- desc 'Erase build (remove artifacts and build trace)' do
- success ::API::V3::Entities::Build
- end
- params do
- requires :build_id, type: Integer, desc: 'The ID of a build'
- end
- post ':id/builds/:build_id/erase' do
- authorize_update_builds!
-
- build = get_build!(params[:build_id])
- authorize!(:erase_build, build)
- break forbidden!('Build is not erasable!') unless build.erasable?
-
- build.erase(erased_by: current_user)
- present build, with: ::API::V3::Entities::Build
- end
-
- desc 'Keep the artifacts to prevent them from being deleted' do
- success ::API::V3::Entities::Build
- end
- params do
- requires :build_id, type: Integer, desc: 'The ID of a build'
- end
- post ':id/builds/:build_id/artifacts/keep' do
- authorize_update_builds!
-
- build = get_build!(params[:build_id])
- authorize!(:update_build, build)
- break not_found!(build) unless build.artifacts?
-
- build.keep_artifacts!
-
- status 200
- present build, with: ::API::V3::Entities::Build
- end
-
- desc 'Trigger a manual build' do
- success ::API::V3::Entities::Build
- detail 'This feature was added in GitLab 8.11'
- end
- params do
- requires :build_id, type: Integer, desc: 'The ID of a Build'
- end
- post ":id/builds/:build_id/play" do
- authorize_read_builds!
-
- build = get_build!(params[:build_id])
- authorize!(:update_build, build)
- bad_request!("Unplayable Job") unless build.playable?
-
- build.play(current_user)
-
- status 200
- present build, with: ::API::V3::Entities::Build
- end
- end
-
- helpers do
- def find_build(id)
- user_project.builds.find_by(id: id.to_i)
- end
-
- def get_build!(id)
- find_build(id) || not_found!
- end
-
- def filter_builds(builds, scope)
- return builds if scope.nil? || scope.empty?
-
- available_statuses = ::CommitStatus::AVAILABLE_STATUSES
-
- unknown = scope - available_statuses
- render_api_error!('Scope contains invalid value(s)', 400) unless unknown.empty?
-
- builds.where(status: available_statuses && scope)
- end
-
- def authorize_read_builds!
- authorize! :read_build, user_project
- end
-
- def authorize_update_builds!
- authorize! :update_build, user_project
- end
- end
- end
- end
-end
diff --git a/lib/api/v3/commits.rb b/lib/api/v3/commits.rb
deleted file mode 100644
index 4f6ea8f502e..00000000000
--- a/lib/api/v3/commits.rb
+++ /dev/null
@@ -1,199 +0,0 @@
-require 'mime/types'
-
-module API
- module V3
- class Commits < Grape::API
- include PaginationParams
-
- before { authenticate! }
- before { authorize! :download_code, user_project }
-
- params do
- requires :id, type: String, desc: 'The ID of a project'
- end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
- desc 'Get a project repository commits' do
- success ::API::Entities::Commit
- end
- params do
- optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used'
- optional :since, type: DateTime, desc: 'Only commits after or in this date will be returned'
- optional :until, type: DateTime, desc: 'Only commits before or in this date will be returned'
- optional :page, type: Integer, default: 0, desc: 'The page for pagination'
- optional :per_page, type: Integer, default: 20, desc: 'The number of results per page'
- optional :path, type: String, desc: 'The file path'
- end
- get ":id/repository/commits" do
- ref = params[:ref_name] || user_project.try(:default_branch) || 'master'
- offset = params[:page] * params[:per_page]
-
- commits = user_project.repository.commits(ref,
- path: params[:path],
- limit: params[:per_page],
- offset: offset,
- after: params[:since],
- before: params[:until])
-
- present commits, with: ::API::Entities::Commit
- end
-
- desc 'Commit multiple file changes as one commit' do
- success ::API::Entities::CommitDetail
- detail 'This feature was introduced in GitLab 8.13'
- end
- params do
- requires :branch_name, type: String, desc: 'The name of branch'
- requires :commit_message, type: String, desc: 'Commit message'
- requires :actions, type: Array[Hash], desc: 'Actions to perform in commit'
- optional :author_email, type: String, desc: 'Author email for commit'
- optional :author_name, type: String, desc: 'Author name for commit'
- end
- post ":id/repository/commits" do
- authorize! :push_code, user_project
-
- attrs = declared_params.dup
- branch = attrs.delete(:branch_name)
- attrs.merge!(start_branch: branch, branch_name: branch)
-
- result = ::Files::MultiService.new(user_project, current_user, attrs).execute
-
- if result[:status] == :success
- commit_detail = user_project.repository.commits(result[:result], limit: 1).first
- present commit_detail, with: ::API::Entities::CommitDetail
- else
- render_api_error!(result[:message], 400)
- end
- end
-
- desc 'Get a specific commit of a project' do
- success ::API::Entities::CommitDetail
- failure [[404, 'Not Found']]
- end
- params do
- requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag'
- optional :stats, type: Boolean, default: true, desc: 'Include commit stats'
- end
- get ":id/repository/commits/:sha", requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do
- commit = user_project.commit(params[:sha])
-
- not_found! "Commit" unless commit
-
- present commit, with: ::API::Entities::CommitDetail, stats: params[:stats]
- end
-
- desc 'Get the diff for a specific commit of a project' do
- failure [[404, 'Not Found']]
- end
- 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: API::COMMIT_ENDPOINT_REQUIREMENTS do
- commit = user_project.commit(params[:sha])
-
- not_found! "Commit" unless commit
-
- commit.raw_diffs.to_a
- end
-
- desc "Get a commit's comments" do
- success ::API::Entities::CommitNote
- failure [[404, 'Not Found']]
- end
- params do
- 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: API::COMMIT_ENDPOINT_REQUIREMENTS do
- commit = user_project.commit(params[:sha])
-
- not_found! 'Commit' unless commit
- notes = commit.notes.order(:created_at)
-
- present paginate(notes), with: ::API::Entities::CommitNote
- end
-
- desc 'Cherry pick commit into a branch' do
- detail 'This feature was introduced in GitLab 8.15'
- success ::API::Entities::Commit
- end
- params do
- 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', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do
- authorize! :push_code, user_project
-
- commit = user_project.commit(params[:sha])
- not_found!('Commit') unless commit
-
- branch = user_project.repository.find_branch(params[:branch])
- not_found!('Branch') unless branch
-
- commit_params = {
- commit: commit,
- start_branch: params[:branch],
- branch_name: params[:branch]
- }
-
- result = ::Commits::CherryPickService.new(user_project, current_user, commit_params).execute
-
- if result[:status] == :success
- branch = user_project.repository.find_branch(params[:branch])
- present user_project.repository.commit(branch.dereferenced_target), with: ::API::Entities::Commit
- else
- render_api_error!(result[:message], 400)
- end
- end
-
- desc 'Post comment to commit' do
- success ::API::Entities::CommitNote
- end
- params do
- requires :sha, type: String, regexp: /\A\h{6,40}\z/, desc: "The commit's SHA"
- requires :note, type: String, desc: 'The text of the comment'
- optional :path, type: String, desc: 'The file path'
- given :path do
- requires :line, type: Integer, desc: 'The line number'
- 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: API::COMMIT_ENDPOINT_REQUIREMENTS do
- commit = user_project.commit(params[:sha])
- not_found! 'Commit' unless commit
-
- opts = {
- note: params[:note],
- noteable_type: 'Commit',
- commit_id: commit.id
- }
-
- 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::Git.diff_line_code(diff.new_path, line.new_pos, line.old_pos)
- end
-
- break if opts[:line_code]
- end
-
- opts[:type] = LegacyDiffNote.name if opts[:line_code]
- end
-
- note = ::Notes::CreateService.new(user_project, current_user, opts).execute
-
- if note.save
- present note, with: ::API::Entities::CommitNote
- else
- render_api_error!("Failed to save note #{note.errors.messages}", 400)
- end
- end
- end
- end
- end
-end
diff --git a/lib/api/v3/deploy_keys.rb b/lib/api/v3/deploy_keys.rb
deleted file mode 100644
index 47e54ca85a5..00000000000
--- a/lib/api/v3/deploy_keys.rb
+++ /dev/null
@@ -1,143 +0,0 @@
-module API
- module V3
- class DeployKeys < Grape::API
- before { authenticate! }
-
- helpers do
- def add_deploy_keys_project(project, attrs = {})
- project.deploy_keys_projects.create(attrs)
- end
-
- def find_by_deploy_key(project, key_id)
- project.deploy_keys_projects.find_by!(deploy_key: key_id)
- end
- end
-
- get "deploy_keys" do
- authenticated_as_admin!
-
- keys = DeployKey.all
- present keys, with: ::API::Entities::SSHKey
- end
-
- params do
- requires :id, type: String, desc: 'The ID of the project'
- end
- resource :projects, requirements: { id: %r{[^/]+} } do
- before { authorize_admin_project }
-
- %w(keys deploy_keys).each do |path|
- desc "Get a specific project's deploy keys" do
- success ::API::Entities::DeployKeysProject
- end
- get ":id/#{path}" do
- keys = user_project.deploy_keys_projects.preload(:deploy_key)
-
- present keys, with: ::API::Entities::DeployKeysProject
- end
-
- desc 'Get single deploy key' do
- success ::API::Entities::DeployKeysProject
- end
- params do
- requires :key_id, type: Integer, desc: 'The ID of the deploy key'
- end
- get ":id/#{path}/:key_id" do
- key = find_by_deploy_key(user_project, params[:key_id])
-
- present key, with: ::API::Entities::DeployKeysProject
- end
-
- desc 'Add new deploy key to currently authenticated user' do
- success ::API::Entities::DeployKeysProject
- end
- params do
- requires :key, type: String, desc: 'The new deploy key'
- requires :title, type: String, desc: 'The name of the deploy key'
- optional :can_push, type: Boolean, desc: "Can deploy key push to the project's repository"
- end
- post ":id/#{path}" do
- params[:key].strip!
-
- # Check for an existing key joined to this project
- key = user_project.deploy_keys_projects
- .joins(:deploy_key)
- .find_by(keys: { key: params[:key] })
-
- if key
- present key, with: ::API::Entities::DeployKeysProject
- break
- end
-
- # Check for available deploy keys in other projects
- key = current_user.accessible_deploy_keys.find_by(key: params[:key])
- if key
- added_key = add_deploy_keys_project(user_project, deploy_key: key, can_push: !!params[:can_push])
-
- present added_key, with: ::API::Entities::DeployKeysProject
- break
- end
-
- # Create a new deploy key
- key_attributes = { can_push: !!params[:can_push],
- deploy_key_attributes: declared_params.except(:can_push) }
- key = add_deploy_keys_project(user_project, key_attributes)
-
- if key.valid?
- present key, with: ::API::Entities::DeployKeysProject
- else
- render_validation_error!(key)
- end
- end
-
- desc 'Enable a deploy key for a project' do
- detail 'This feature was added in GitLab 8.11'
- success ::API::Entities::SSHKey
- end
- params do
- requires :key_id, type: Integer, desc: 'The ID of the deploy key'
- end
- post ":id/#{path}/:key_id/enable" do
- key = ::Projects::EnableDeployKeyService.new(user_project,
- current_user, declared_params).execute
-
- if key
- present key, with: ::API::Entities::SSHKey
- else
- not_found!('Deploy Key')
- end
- end
-
- desc 'Disable a deploy key for a project' do
- detail 'This feature was added in GitLab 8.11'
- success ::API::Entities::SSHKey
- end
- params do
- requires :key_id, type: Integer, desc: 'The ID of the deploy key'
- end
- delete ":id/#{path}/:key_id/disable" do
- key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id])
- key.destroy
-
- present key.deploy_key, with: ::API::Entities::SSHKey
- end
-
- desc 'Delete deploy key for a project' do
- success Key
- end
- params do
- requires :key_id, type: Integer, desc: 'The ID of the deploy key'
- end
- delete ":id/#{path}/:key_id" do
- key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id])
- if key
- key.destroy
- else
- not_found!('Deploy Key')
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/api/v3/deployments.rb b/lib/api/v3/deployments.rb
deleted file mode 100644
index 1d4972eda26..00000000000
--- a/lib/api/v3/deployments.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-module API
- module V3
- # Deployments RESTful API endpoints
- class Deployments < Grape::API
- include PaginationParams
-
- before { authenticate! }
-
- params do
- requires :id, type: String, desc: 'The project ID'
- end
- resource :projects, requirements: { id: %r{[^/]+} } do
- desc 'Get all deployments of the project' do
- detail 'This feature was introduced in GitLab 8.11.'
- success ::API::V3::Deployments
- end
- params do
- use :pagination
- end
- get ':id/deployments' do
- authorize! :read_deployment, user_project
-
- present paginate(user_project.deployments), with: ::API::V3::Deployments
- end
-
- desc 'Gets a specific deployment' do
- detail 'This feature was introduced in GitLab 8.11.'
- success ::API::V3::Deployments
- end
- params do
- requires :deployment_id, type: Integer, desc: 'The deployment ID'
- end
- get ':id/deployments/:deployment_id' do
- authorize! :read_deployment, user_project
-
- deployment = user_project.deployments.find(params[:deployment_id])
-
- present deployment, with: ::API::V3::Deployments
- end
- end
- end
- end
-end
diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb
deleted file mode 100644
index 68b4d7c3982..00000000000
--- a/lib/api/v3/entities.rb
+++ /dev/null
@@ -1,309 +0,0 @@
-module API
- module V3
- module Entities
- class ProjectSnippet < Grape::Entity
- expose :id, :title, :file_name
- expose :author, using: ::API::Entities::UserBasic
- expose :updated_at, :created_at
- expose(:expires_at) { |snippet| nil }
-
- expose :web_url do |snippet, options|
- Gitlab::UrlBuilder.build(snippet)
- end
- end
-
- class Note < Grape::Entity
- expose :id
- expose :note, as: :body
- expose :attachment_identifier, as: :attachment
- expose :author, using: ::API::Entities::UserBasic
- expose :created_at, :updated_at
- expose :system?, as: :system
- expose :noteable_id, :noteable_type
- # upvote? and downvote? are deprecated, always return false
- expose(:upvote?) { |note| false }
- expose(:downvote?) { |note| false }
- end
-
- class PushEventPayload < Grape::Entity
- expose :commit_count, :action, :ref_type, :commit_from, :commit_to
- expose :ref, :commit_title
- end
-
- class Event < Grape::Entity
- expose :project_id, :action_name
- expose :target_id, :target_type, :author_id
- expose :target_title
- expose :created_at
- expose :note, using: Entities::Note, if: ->(event, options) { event.note? }
- expose :author, using: ::API::Entities::UserBasic, if: ->(event, options) { event.author }
-
- expose :push_event_payload,
- as: :push_data,
- using: PushEventPayload,
- if: -> (event, _) { event.push? }
-
- expose :author_username do |event, options|
- event.author&.username
- end
- end
-
- class AwardEmoji < Grape::Entity
- expose :id
- expose :name
- expose :user, using: ::API::Entities::UserBasic
- expose :created_at, :updated_at
- expose :awardable_id, :awardable_type
- end
-
- class Project < Grape::Entity
- expose :id, :description, :default_branch, :tag_list
- expose :public?, as: :public
- expose :archived?, as: :archived
- expose :visibility_level, :ssh_url_to_repo, :http_url_to_repo, :web_url
- expose :owner, using: ::API::Entities::UserBasic, unless: ->(project, options) { project.group }
- expose :name, :name_with_namespace
- expose :path, :path_with_namespace
- expose :resolve_outdated_diff_discussions
- expose :container_registry_enabled
-
- # Expose old field names with the new permissions methods to keep API compatible
- expose(:issues_enabled) { |project, options| project.feature_available?(:issues, options[:current_user]) }
- expose(:merge_requests_enabled) { |project, options| project.feature_available?(:merge_requests, options[:current_user]) }
- expose(:wiki_enabled) { |project, options| project.feature_available?(:wiki, options[:current_user]) }
- expose(:builds_enabled) { |project, options| project.feature_available?(:builds, options[:current_user]) }
- expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:current_user]) }
-
- expose :created_at, :last_activity_at
- expose :shared_runners_enabled
- expose :lfs_enabled?, as: :lfs_enabled
- expose :creator_id
- expose :namespace, using: 'API::Entities::Namespace'
- expose :forked_from_project, using: ::API::Entities::BasicProjectDetails, if: lambda { |project, options| project.forked? }
- expose :avatar_url do |user, options|
- user.avatar_url(only_path: false)
- end
- expose :star_count, :forks_count
- expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) && project.default_issues_tracker? }
- expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] }
- expose :public_builds
- expose :shared_with_groups do |project, options|
- ::API::Entities::SharedGroup.represent(project.project_group_links.all, options)
- end
- expose :only_allow_merge_if_pipeline_succeeds, as: :only_allow_merge_if_build_succeeds
- expose :request_access_enabled
- expose :only_allow_merge_if_all_discussions_are_resolved
-
- expose :statistics, using: '::API::V3::Entities::ProjectStatistics', if: :statistics
- end
-
- class ProjectWithAccess < Project
- expose :permissions do
- expose :project_access, using: ::API::Entities::ProjectAccess do |project, options|
- project.project_members.find_by(user_id: options[:current_user].id)
- end
-
- expose :group_access, using: ::API::Entities::GroupAccess do |project, options|
- if project.group
- project.group.group_members.find_by(user_id: options[:current_user].id)
- end
- end
- end
- end
-
- class MergeRequest < Grape::Entity
- expose :id, :iid
- expose(:project_id) { |entity| entity.project.id }
- expose :title, :description
- expose :state, :created_at, :updated_at
- expose :target_branch, :source_branch
- expose :upvotes, :downvotes
- expose :author, :assignee, using: ::API::Entities::UserBasic
- expose :source_project_id, :target_project_id
- expose :label_names, as: :labels
- expose :work_in_progress?, as: :work_in_progress
- expose :milestone, using: ::API::Entities::Milestone
- expose :merge_when_pipeline_succeeds, as: :merge_when_build_succeeds
- expose :merge_status
- expose :diff_head_sha, as: :sha
- expose :merge_commit_sha
- expose :subscribed do |merge_request, options|
- merge_request.subscribed?(options[:current_user], options[:project])
- end
- expose :user_notes_count
- expose :should_remove_source_branch?, as: :should_remove_source_branch
- expose :force_remove_source_branch?, as: :force_remove_source_branch
-
- expose :web_url do |merge_request, options|
- Gitlab::UrlBuilder.build(merge_request)
- end
- end
-
- class Group < Grape::Entity
- expose :id, :name, :path, :description, :visibility_level
- expose :lfs_enabled?, as: :lfs_enabled
- expose :avatar_url do |user, options|
- user.avatar_url(only_path: false)
- end
- expose :web_url
- expose :request_access_enabled
- expose :full_name, :full_path
-
- if ::Group.supports_nested_groups?
- expose :parent_id
- end
-
- expose :statistics, if: :statistics do
- with_options format_with: -> (value) { value.to_i } do
- expose :storage_size
- expose :repository_size
- expose :lfs_objects_size
- expose :build_artifacts_size
- end
- end
- end
-
- class GroupDetail < Group
- expose :projects, using: Entities::Project
- expose :shared_projects, using: Entities::Project
- end
-
- class ApplicationSetting < Grape::Entity
- expose :id
- expose :default_projects_limit
- expose :signup_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
- expose :created_at
- expose :updated_at
- expose :home_page_url
- expose :default_branch_protection
- expose :restricted_visibility_levels
- expose :max_attachment_size
- expose :session_expire_delay
- expose :default_project_visibility
- expose :default_snippet_visibility
- expose :default_group_visibility
- expose :domain_whitelist
- expose :domain_blacklist_enabled
- expose :domain_blacklist
- expose :user_oauth_applications
- expose :after_sign_out_path
- expose :container_registry_token_expire_delay
- expose :repository_storage
- expose :repository_storages
- expose :koding_enabled
- expose :koding_url
- expose :plantuml_enabled
- expose :plantuml_url
- expose :terminal_max_session_time
- end
-
- class Environment < ::API::Entities::EnvironmentBasic
- expose :project, using: Entities::Project
- end
-
- class Trigger < Grape::Entity
- expose :token, :created_at, :updated_at, :last_used
- expose :owner, using: ::API::Entities::UserBasic
- end
-
- class TriggerRequest < Grape::Entity
- expose :id, :variables
- end
-
- class Build < Grape::Entity
- expose :id, :status, :stage, :name, :ref, :tag, :coverage
- expose :created_at, :started_at, :finished_at
- expose :user, with: ::API::Entities::User
- expose :artifacts_file, using: ::API::Entities::JobArtifactFile, if: -> (build, opts) { build.artifacts? }
- expose :commit, with: ::API::Entities::Commit
- expose :runner, with: ::API::Entities::Runner
- expose :pipeline, with: ::API::Entities::PipelineBasic
- end
-
- class BuildArtifactFile < Grape::Entity
- expose :filename, :size
- end
-
- class Deployment < Grape::Entity
- expose :id, :iid, :ref, :sha, :created_at
- expose :user, using: ::API::Entities::UserBasic
- expose :environment, using: ::API::Entities::EnvironmentBasic
- expose :deployable, using: Entities::Build
- end
-
- class MergeRequestChanges < MergeRequest
- expose :diffs, as: :changes, using: ::API::Entities::Diff do |compare, _|
- compare.raw_diffs(limits: false).to_a
- end
- end
-
- class ProjectStatistics < Grape::Entity
- expose :commit_count
- expose :storage_size
- expose :repository_size
- expose :lfs_objects_size
- expose :build_artifacts_size
- end
-
- class ProjectService < Grape::Entity
- expose :id, :title, :created_at, :updated_at, :active
- expose :push_events, :issues_events, :confidential_issues_events
- expose :merge_requests_events, :tag_push_events, :note_events
- expose :pipeline_events
- expose :job_events, as: :build_events
- # Expose serialized properties
- expose :properties do |service, options|
- service.properties.slice(*service.api_field_names)
- end
- end
-
- class ProjectHook < ::API::Entities::Hook
- expose :project_id, :issues_events, :confidential_issues_events
- expose :merge_requests_events, :note_events, :pipeline_events
- expose :wiki_page_events
- expose :job_events, as: :build_events
- end
-
- class ProjectEntity < Grape::Entity
- expose :id, :iid
- expose(:project_id) { |entity| entity&.project.try(:id) }
- expose :title, :description
- expose :state, :created_at, :updated_at
- end
-
- class IssueBasic < ProjectEntity
- expose :label_names, as: :labels
- expose :milestone, using: ::API::Entities::Milestone
- expose :assignees, :author, using: ::API::Entities::UserBasic
-
- expose :assignee, using: ::API::Entities::UserBasic do |issue, options|
- issue.assignees.first
- end
-
- expose :user_notes_count
- expose :upvotes, :downvotes
- expose :due_date
- expose :confidential
-
- expose :web_url do |issue, options|
- Gitlab::UrlBuilder.build(issue)
- end
- end
-
- class Issue < IssueBasic
- unexpose :assignees
- expose :assignee do |issue, options|
- ::API::Entities::UserBasic.represent(issue.assignees.first, options)
- end
- expose :subscribed do |issue, options|
- issue.subscribed?(options[:current_user], options[:project] || issue.project)
- end
- end
- end
- end
-end
diff --git a/lib/api/v3/environments.rb b/lib/api/v3/environments.rb
deleted file mode 100644
index 6bb4e016a01..00000000000
--- a/lib/api/v3/environments.rb
+++ /dev/null
@@ -1,87 +0,0 @@
-module API
- module V3
- class Environments < Grape::API
- include ::API::Helpers::CustomValidators
- include PaginationParams
-
- before { authenticate! }
-
- params do
- requires :id, type: String, desc: 'The project ID'
- end
- resource :projects, requirements: { id: %r{[^/]+} } do
- desc 'Get all environments of the project' do
- detail 'This feature was introduced in GitLab 8.11.'
- success Entities::Environment
- end
- params do
- use :pagination
- end
- get ':id/environments' do
- authorize! :read_environment, user_project
-
- present paginate(user_project.environments), with: Entities::Environment
- end
-
- desc 'Creates a new environment' do
- detail 'This feature was introduced in GitLab 8.11.'
- success Entities::Environment
- end
- params do
- requires :name, type: String, desc: 'The name of the environment to be created'
- optional :external_url, type: String, desc: 'URL on which this deployment is viewable'
- optional :slug, absence: { message: "is automatically generated and cannot be changed" }
- end
- post ':id/environments' do
- authorize! :create_environment, user_project
-
- environment = user_project.environments.create(declared_params)
-
- if environment.persisted?
- present environment, with: Entities::Environment
- else
- render_validation_error!(environment)
- end
- end
-
- desc 'Updates an existing environment' do
- detail 'This feature was introduced in GitLab 8.11.'
- success Entities::Environment
- end
- params do
- requires :environment_id, type: Integer, desc: 'The environment ID'
- optional :name, type: String, desc: 'The new environment name'
- optional :external_url, type: String, desc: 'The new URL on which this deployment is viewable'
- optional :slug, absence: { message: "is automatically generated and cannot be changed" }
- end
- put ':id/environments/:environment_id' do
- authorize! :update_environment, user_project
-
- environment = user_project.environments.find(params[:environment_id])
-
- update_params = declared_params(include_missing: false).extract!(:name, :external_url)
- if environment.update(update_params)
- present environment, with: Entities::Environment
- else
- render_validation_error!(environment)
- end
- end
-
- desc 'Deletes an existing environment' do
- detail 'This feature was introduced in GitLab 8.11.'
- success Entities::Environment
- end
- params do
- requires :environment_id, type: Integer, desc: 'The environment ID'
- end
- delete ':id/environments/:environment_id' do
- authorize! :update_environment, user_project
-
- environment = user_project.environments.find(params[:environment_id])
-
- present environment.destroy, with: Entities::Environment
- end
- end
- end
- end
-end
diff --git a/lib/api/v3/files.rb b/lib/api/v3/files.rb
deleted file mode 100644
index 7b4b3448b6d..00000000000
--- a/lib/api/v3/files.rb
+++ /dev/null
@@ -1,138 +0,0 @@
-module API
- module V3
- class Files < Grape::API
- helpers do
- def commit_params(attrs)
- {
- file_path: attrs[:file_path],
- start_branch: attrs[:branch],
- branch_name: attrs[:branch],
- commit_message: attrs[:commit_message],
- file_content: attrs[:content],
- file_content_encoding: attrs[:encoding],
- author_email: attrs[:author_email],
- author_name: attrs[:author_name]
- }
- end
-
- def commit_response(attrs)
- {
- file_path: attrs[:file_path],
- branch: attrs[:branch]
- }
- end
-
- params :simple_file_params do
- requires :file_path, type: String, desc: 'The path to new file. Ex. lib/class.rb'
- requires :branch_name, type: String, desc: 'The name of branch'
- requires :commit_message, type: String, desc: 'Commit Message'
- optional :author_email, type: String, desc: 'The email of the author'
- optional :author_name, type: String, desc: 'The name of the author'
- end
-
- params :extended_file_params do
- use :simple_file_params
- requires :content, type: String, desc: 'File content'
- optional :encoding, type: String, values: %w[base64], desc: 'File encoding'
- end
- end
-
- params do
- requires :id, type: String, desc: 'The project ID'
- end
- resource :projects, requirements: { id: %r{[^/]+} } do
- desc 'Get a file from repository'
- params do
- requires :file_path, type: String, desc: 'The path to the file. Ex. lib/class.rb'
- requires :ref, type: String, desc: 'The name of branch, tag, or commit'
- end
- get ":id/repository/files" do
- authorize! :download_code, user_project
-
- commit = user_project.commit(params[:ref])
- not_found!('Commit') unless commit
-
- repo = user_project.repository
- blob = repo.blob_at(commit.sha, params[:file_path])
- not_found!('File') unless blob
-
- blob.load_all_data!
- status(200)
-
- {
- file_name: blob.name,
- file_path: blob.path,
- size: blob.size,
- encoding: "base64",
- content: Base64.strict_encode64(blob.data),
- ref: params[:ref],
- blob_id: blob.id,
- commit_id: commit.id,
- last_commit_id: repo.last_commit_id_for_path(commit.sha, params[:file_path])
- }
- end
-
- desc 'Create new file in repository'
- params do
- use :extended_file_params
- end
- post ":id/repository/files" do
- authorize! :push_code, user_project
-
- file_params = declared_params(include_missing: false)
- file_params[:branch] = file_params.delete(:branch_name)
-
- result = ::Files::CreateService.new(user_project, current_user, commit_params(file_params)).execute
-
- if result[:status] == :success
- status(201)
- commit_response(file_params)
- else
- render_api_error!(result[:message], 400)
- end
- end
-
- desc 'Update existing file in repository'
- params do
- use :extended_file_params
- end
- put ":id/repository/files" do
- authorize! :push_code, user_project
-
- file_params = declared_params(include_missing: false)
- file_params[:branch] = file_params.delete(:branch_name)
-
- result = ::Files::UpdateService.new(user_project, current_user, commit_params(file_params)).execute
-
- if result[:status] == :success
- status(200)
- commit_response(file_params)
- else
- http_status = result[:http_status] || 400
- render_api_error!(result[:message], http_status)
- end
- end
-
- desc 'Delete an existing file in repository'
- params do
- use :simple_file_params
- end
- delete ":id/repository/files" do
- authorize! :push_code, user_project
-
- file_params = declared_params(include_missing: false)
- file_params[:branch] = file_params.delete(:branch_name)
-
- result = ::Files::DeleteService.new(user_project, current_user, commit_params(file_params)).execute
-
- if result[:status] == :success
- status(200)
- commit_response(file_params)
- else
- render_api_error!(result[:message], 400)
- end
- end
- end
- end
- end
-end
diff --git a/lib/api/v3/groups.rb b/lib/api/v3/groups.rb
deleted file mode 100644
index 4fa7d196e50..00000000000
--- a/lib/api/v3/groups.rb
+++ /dev/null
@@ -1,187 +0,0 @@
-module API
- module V3
- class Groups < Grape::API
- include PaginationParams
-
- before { authenticate! }
-
- helpers do
- params :optional_params do
- optional :description, type: String, desc: 'The description of the group'
- optional :visibility_level, type: Integer, desc: 'The visibility level of the group'
- optional :lfs_enabled, type: Boolean, desc: 'Enable/disable LFS for the projects in this group'
- optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access'
- end
-
- params :statistics_params do
- 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
- 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'
- optional :search, type: String, desc: 'Search for a specific group'
- optional :order_by, type: String, values: %w[name path], default: 'name', desc: 'Order by name or path'
- optional :sort, type: String, values: %w[asc desc], default: 'asc', desc: 'Sort by asc (ascending) or desc (descending)'
- use :pagination
- end
- get do
- groups = if current_user.admin
- Group.all
- elsif params[:all_available]
- GroupsFinder.new(current_user).execute
- else
- current_user.groups
- end
-
- 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?
- end
-
- desc 'Get list of owned groups for authenticated user' do
- success Entities::Group
- end
- params do
- use :pagination
- use :statistics_params
- end
- get '/owned' do
- present_groups current_user.owned_groups, statistics: params[:statistics]
- end
-
- desc 'Create a group. Available only for users who can create groups.' do
- success Entities::Group
- end
- params do
- requires :name, type: String, desc: 'The name of the group'
- requires :path, type: String, desc: 'The path of the group'
-
- if ::Group.supports_nested_groups?
- optional :parent_id, type: Integer, desc: 'The parent group id for creating nested group'
- end
-
- use :optional_params
- end
- post do
- authorize! :create_group
-
- group = ::Groups::CreateService.new(current_user, declared_params(include_missing: false)).execute
-
- if group.persisted?
- present group, with: Entities::Group, current_user: current_user
- else
- render_api_error!("Failed to save group #{group.errors.messages}", 400)
- end
- end
- end
-
- params do
- requires :id, type: String, desc: 'The ID of a group'
- end
- resource :groups, requirements: { id: %r{[^/]+} } do
- desc 'Update a group. Available only for users who can administrate groups.' do
- success Entities::Group
- end
- params do
- optional :name, type: String, desc: 'The name of the group'
- optional :path, type: String, desc: 'The path of the group'
- use :optional_params
- at_least_one_of :name, :path, :description, :visibility_level,
- :lfs_enabled, :request_access_enabled
- end
- put ':id' do
- group = find_group!(params[:id])
- authorize! :admin_group, group
-
- if ::Groups::UpdateService.new(group, current_user, declared_params(include_missing: false)).execute
- present group, with: Entities::GroupDetail, current_user: current_user
- else
- render_validation_error!(group)
- end
- end
-
- desc 'Get a single group, with containing projects.' do
- success Entities::GroupDetail
- end
- get ":id" do
- group = find_group!(params[:id])
- present group, with: Entities::GroupDetail, current_user: current_user
- end
-
- desc 'Remove a group.'
- delete ":id" do
- group = find_group!(params[:id])
- authorize! :admin_group, group
- ::Groups::DestroyService.new(group, current_user).async_execute
-
- accepted!
- end
-
- desc 'Get a list of projects in this group.' do
- success Entities::Project
- end
- params do
- optional :archived, type: Boolean, default: false, desc: 'Limit by archived status'
- optional :visibility, type: String, values: %w[public internal private],
- desc: 'Limit by visibility'
- optional :search, type: String, desc: 'Return list of authorized projects matching the search criteria'
- optional :order_by, type: String, values: %w[id name path created_at updated_at last_activity_at],
- default: 'created_at', desc: 'Return projects ordered by field'
- optional :sort, type: String, values: %w[asc desc], default: 'desc',
- desc: 'Return projects sorted in ascending and descending order'
- optional :simple, type: Boolean, default: false,
- desc: 'Return only the ID, URL, name, and path of each project'
- optional :owned, type: Boolean, default: false, desc: 'Limit by owned by authenticated user'
- optional :starred, type: Boolean, default: false, desc: 'Limit by starred status'
-
- use :pagination
- end
- get ":id/projects" do
- group = find_group!(params[:id])
- projects = GroupProjectsFinder.new(group: group, current_user: current_user).execute
- projects = filter_projects(projects)
- entity = params[:simple] ? ::API::Entities::BasicProjectDetails : Entities::Project
- present paginate(projects), with: entity, current_user: current_user
- end
-
- desc 'Transfer a project to the group namespace. Available only for admin.' do
- success Entities::GroupDetail
- end
- params do
- requires :project_id, type: String, desc: 'The ID or path of the project'
- end
- post ":id/projects/:project_id", requirements: { project_id: /.+/ } do
- authenticated_as_admin!
- group = find_group!(params[:id])
- project = find_project!(params[:project_id])
- result = ::Projects::TransferService.new(project, current_user).execute(group)
-
- if result
- present group, with: Entities::GroupDetail, current_user: current_user
- else
- render_api_error!("Failed to transfer project #{project.errors.messages}", 400)
- end
- end
- end
- end
- end
-end
diff --git a/lib/api/v3/helpers.rb b/lib/api/v3/helpers.rb
deleted file mode 100644
index 4e63aa01c1a..00000000000
--- a/lib/api/v3/helpers.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-module API
- module V3
- module Helpers
- def find_project_issue(id)
- IssuesFinder.new(current_user, project_id: user_project.id).find(id)
- end
-
- def find_project_merge_request(id)
- MergeRequestsFinder.new(current_user, project_id: user_project.id).find(id)
- end
-
- def find_merge_request_with_access(id, access_level = :read_merge_request)
- merge_request = user_project.merge_requests.find(id)
- authorize! access_level, merge_request
- merge_request
- end
-
- # project helpers
-
- def filter_projects(projects)
- if params[:membership]
- projects = projects.merge(current_user.authorized_projects)
- end
-
- if params[:owned]
- projects = projects.merge(current_user.owned_projects)
- end
-
- if params[:starred]
- projects = projects.merge(current_user.starred_projects)
- end
-
- if params[:search].present?
- projects = projects.search(params[:search])
- end
-
- if params[:visibility].present?
- projects = projects.where(visibility_level: Gitlab::VisibilityLevel.level_value(params[:visibility]))
- end
-
- unless params[:archived].nil?
- projects = projects.where(archived: to_boolean(params[:archived]))
- end
-
- projects.reorder(params[:order_by] => params[:sort])
- end
- end
- end
-end
diff --git a/lib/api/v3/issues.rb b/lib/api/v3/issues.rb
deleted file mode 100644
index b59947d81d9..00000000000
--- a/lib/api/v3/issues.rb
+++ /dev/null
@@ -1,240 +0,0 @@
-module API
- module V3
- class Issues < Grape::API
- include PaginationParams
-
- before { authenticate! }
-
- helpers do
- def find_issues(args = {})
- args = params.merge(args)
- args = convert_parameters_from_legacy_format(args)
-
- args.delete(:id)
- args[:milestone_title] = args.delete(:milestone)
-
- match_all_labels = args.delete(:match_all_labels)
- labels = args.delete(:labels)
- args[:label_name] = labels if match_all_labels
-
- # IssuesFinder expects iids
- args[:iids] = args.delete(:iid) if args.key?(:iid)
-
- issues = IssuesFinder.new(current_user, args).execute.inc_notes_with_associations
-
- if !match_all_labels && labels.present?
- issues = issues.includes(:labels).where('labels.title' => labels.split(','))
- end
-
- issues.reorder(args[:order_by] => args[:sort])
- end
-
- params :issues_params do
- optional :labels, type: String, desc: 'Comma-separated list of label names'
- optional :milestone, type: String, desc: 'Milestone title'
- optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at',
- desc: 'Return issues ordered by `created_at` or `updated_at` fields.'
- optional :sort, type: String, values: %w[asc desc], default: 'desc',
- desc: 'Return issues sorted in `asc` or `desc` order.'
- optional :milestone, type: String, desc: 'Return issues for a specific milestone'
- use :pagination
- end
-
- params :issue_params do
- optional :description, type: String, desc: 'The description of an issue'
- optional :assignee_id, type: Integer, desc: 'The ID of a user to assign issue'
- optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign issue'
- optional :labels, type: String, desc: 'Comma-separated list of label names'
- optional :due_date, type: String, desc: 'Date time string in the format YEAR-MONTH-DAY'
- optional :confidential, type: Boolean, desc: 'Boolean parameter if the issue should be confidential'
- end
- end
-
- resource :issues do
- desc "Get currently authenticated user's issues" do
- success ::API::V3::Entities::Issue
- end
- params do
- optional :state, type: String, values: %w[opened closed all], default: 'all',
- desc: 'Return opened, closed, or all issues'
- use :issues_params
- end
- get do
- issues = find_issues(scope: 'authored')
-
- present paginate(issues), with: ::API::V3::Entities::Issue, current_user: current_user
- end
- end
-
- params do
- requires :id, type: String, desc: 'The ID of a group'
- end
- resource :groups, requirements: { id: %r{[^/]+} } do
- desc 'Get a list of group issues' do
- success ::API::V3::Entities::Issue
- end
- params do
- optional :state, type: String, values: %w[opened closed all], default: 'all',
- desc: 'Return opened, closed, or all issues'
- use :issues_params
- end
- get ":id/issues" do
- group = find_group!(params[:id])
-
- issues = find_issues(group_id: group.id, match_all_labels: true)
-
- present paginate(issues), with: ::API::V3::Entities::Issue, current_user: current_user
- end
- end
-
- params do
- requires :id, type: String, desc: 'The ID of a project'
- end
- resource :projects, requirements: { id: %r{[^/]+} } do
- include TimeTrackingEndpoints
-
- desc 'Get a list of project issues' do
- detail 'iid filter is deprecated have been removed on V4'
- success ::API::V3::Entities::Issue
- end
- params do
- optional :state, type: String, values: %w[opened closed all], default: 'all',
- desc: 'Return opened, closed, or all issues'
- optional :iid, type: Integer, desc: 'Return the issue having the given `iid`'
- use :issues_params
- end
- get ":id/issues" do
- project = find_project!(params[:id])
-
- issues = find_issues(project_id: project.id)
-
- present paginate(issues), with: ::API::V3::Entities::Issue, current_user: current_user, project: user_project
- end
-
- desc 'Get a single project issue' do
- success ::API::V3::Entities::Issue
- end
- params do
- requires :issue_id, type: Integer, desc: 'The ID of a project issue'
- end
- get ":id/issues/:issue_id" do
- issue = find_project_issue(params[:issue_id])
- present issue, with: ::API::V3::Entities::Issue, current_user: current_user, project: user_project
- end
-
- desc 'Create a new project issue' do
- success ::API::V3::Entities::Issue
- end
- params do
- requires :title, type: String, desc: 'The title of an issue'
- optional :created_at, type: DateTime,
- desc: 'Date time when the issue was created. Available only for admins and project owners.'
- optional :merge_request_for_resolving_discussions, type: Integer,
- desc: 'The IID of a merge request for which to resolve discussions'
- use :issue_params
- end
- post ':id/issues' do
- Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42131')
-
- # Setting created_at time only allowed for admins and project owners
- unless current_user.admin? || user_project.owner == current_user
- params.delete(:created_at)
- end
-
- issue_params = declared_params(include_missing: false)
- issue_params = issue_params.merge(merge_request_to_resolve_discussions_of: issue_params.delete(:merge_request_for_resolving_discussions))
- issue_params = convert_parameters_from_legacy_format(issue_params)
-
- issue = ::Issues::CreateService.new(user_project,
- current_user,
- issue_params.merge(request: request, api: true)).execute
- render_spam_error! if issue.spam?
-
- if issue.valid?
- present issue, with: ::API::V3::Entities::Issue, current_user: current_user, project: user_project
- else
- render_validation_error!(issue)
- end
- end
-
- desc 'Update an existing issue' do
- success ::API::V3::Entities::Issue
- end
- params do
- requires :issue_id, type: Integer, desc: 'The ID of a project issue'
- optional :title, type: String, desc: 'The title of an issue'
- optional :updated_at, type: DateTime,
- desc: 'Date time when the issue was updated. Available only for admins and project owners.'
- optional :state_event, type: String, values: %w[reopen close], desc: 'State of the issue'
- use :issue_params
- at_least_one_of :title, :description, :assignee_id, :milestone_id,
- :labels, :created_at, :due_date, :confidential, :state_event
- end
- put ':id/issues/:issue_id' do
- Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42132')
-
- issue = user_project.issues.find(params.delete(:issue_id))
- authorize! :update_issue, issue
-
- # Setting created_at time only allowed for admins and project owners
- unless current_user.admin? || user_project.owner == current_user
- params.delete(:updated_at)
- end
-
- update_params = declared_params(include_missing: false).merge(request: request, api: true)
- update_params = convert_parameters_from_legacy_format(update_params)
-
- issue = ::Issues::UpdateService.new(user_project,
- current_user,
- update_params).execute(issue)
-
- render_spam_error! if issue.spam?
-
- if issue.valid?
- present issue, with: ::API::V3::Entities::Issue, current_user: current_user, project: user_project
- else
- render_validation_error!(issue)
- end
- end
-
- desc 'Move an existing issue' do
- success ::API::V3::Entities::Issue
- end
- params do
- requires :issue_id, type: Integer, desc: 'The ID of a project issue'
- requires :to_project_id, type: Integer, desc: 'The ID of the new project'
- end
- post ':id/issues/:issue_id/move' do
- Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42133')
-
- issue = user_project.issues.find_by(id: params[:issue_id])
- not_found!('Issue') unless issue
-
- new_project = Project.find_by(id: params[:to_project_id])
- not_found!('Project') unless new_project
-
- begin
- issue = ::Issues::MoveService.new(user_project, current_user).execute(issue, new_project)
- present issue, with: ::API::V3::Entities::Issue, current_user: current_user, project: user_project
- rescue ::Issues::MoveService::MoveError => error
- render_api_error!(error.message, 400)
- end
- end
-
- desc 'Delete a project issue'
- params do
- requires :issue_id, type: Integer, desc: 'The ID of a project issue'
- end
- delete ":id/issues/:issue_id" do
- issue = user_project.issues.find_by(id: params[:issue_id])
- not_found!('Issue') unless issue
-
- authorize!(:destroy_issue, issue)
-
- status(200)
- issue.destroy
- end
- end
- end
- end
-end
diff --git a/lib/api/v3/labels.rb b/lib/api/v3/labels.rb
deleted file mode 100644
index 4157462ec2a..00000000000
--- a/lib/api/v3/labels.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-module API
- module V3
- class Labels < Grape::API
- before { authenticate! }
-
- params do
- requires :id, type: String, desc: 'The ID of a project'
- end
- resource :projects, requirements: { id: %r{[^/]+} } do
- desc 'Get all labels of the project' do
- success ::API::Entities::Label
- end
- get ':id/labels' do
- present available_labels_for(user_project), with: ::API::Entities::Label, current_user: current_user, project: user_project
- end
-
- desc 'Delete an existing label' do
- success ::API::Entities::Label
- end
- params do
- requires :name, type: String, desc: 'The name of the label to be deleted'
- end
- delete ':id/labels' do
- authorize! :admin_label, user_project
-
- label = user_project.labels.find_by(title: params[:name])
- not_found!('Label') unless label
-
- present label.destroy, with: ::API::Entities::Label, current_user: current_user, project: user_project
- end
- end
- end
- end
-end
diff --git a/lib/api/v3/members.rb b/lib/api/v3/members.rb
deleted file mode 100644
index 88dd598f1e9..00000000000
--- a/lib/api/v3/members.rb
+++ /dev/null
@@ -1,136 +0,0 @@
-module API
- module V3
- class Members < Grape::API
- include PaginationParams
-
- before { authenticate! }
-
- helpers ::API::Helpers::MembersHelpers
-
- %w[group project].each do |source_type|
- params do
- requires :id, type: String, desc: "The #{source_type} ID"
- end
- resource source_type.pluralize, requirements: { id: %r{[^/]+} } do
- desc 'Gets a list of group or project members viewable by the authenticated user.' do
- success ::API::Entities::Member
- end
- params do
- optional :query, type: String, desc: 'A query string to search for members'
- use :pagination
- end
- get ":id/members" do
- source = find_source(source_type, params[:id])
-
- members = source.members.where.not(user_id: nil).includes(:user)
- members = members.joins(:user).merge(User.search(params[:query])) if params[:query].present?
- members = paginate(members)
-
- present members, with: ::API::Entities::Member
- end
-
- desc 'Gets a member of a group or project.' do
- success ::API::Entities::Member
- end
- params do
- requires :user_id, type: Integer, desc: 'The user ID of the member'
- end
- get ":id/members/:user_id" do
- source = find_source(source_type, params[:id])
-
- members = source.members
- member = members.find_by!(user_id: params[:user_id])
-
- present member, with: ::API::Entities::Member
- end
-
- desc 'Adds a member to a group or project.' do
- success ::API::Entities::Member
- end
- params do
- requires :user_id, type: Integer, desc: 'The user ID of the new member'
- requires :access_level, type: Integer, desc: 'A valid access level (defaults: `30`, developer access level)'
- optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY'
- end
- post ":id/members" do
- source = find_source(source_type, params[:id])
- authorize_admin_source!(source_type, source)
-
- member = source.members.find_by(user_id: params[:user_id])
-
- # We need this explicit check because `source.add_user` doesn't
- # currently return the member created so it would return 201 even if
- # the member already existed...
- # The `source_type == 'group'` check is to ensure back-compatibility
- # but 409 behavior should be used for both project and group members in 9.0!
- conflict!('Member already exists') if source_type == 'group' && member
-
- unless member
- member = source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at])
- end
-
- if member.persisted? && member.valid?
- present member, with: ::API::Entities::Member
- else
- # This is to ensure back-compatibility but 400 behavior should be used
- # for all validation errors in 9.0!
- render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level)
- render_validation_error!(member)
- end
- end
-
- desc 'Updates a member of a group or project.' do
- success ::API::Entities::Member
- end
- params do
- requires :user_id, type: Integer, desc: 'The user ID of the new member'
- requires :access_level, type: Integer, desc: 'A valid access level'
- optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY'
- end
- put ":id/members/:user_id" do
- source = find_source(source_type, params.delete(:id))
- authorize_admin_source!(source_type, source)
-
- member = source.members.find_by!(user_id: params.delete(:user_id))
-
- if member.update_attributes(declared_params(include_missing: false))
- present member, with: ::API::Entities::Member
- else
- # This is to ensure back-compatibility but 400 behavior should be used
- # for all validation errors in 9.0!
- render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level)
- render_validation_error!(member)
- end
- end
-
- desc 'Removes a user from a group or project.'
- params do
- requires :user_id, type: Integer, desc: 'The user ID of the member'
- end
- delete ":id/members/:user_id" do
- source = find_source(source_type, params[:id])
-
- # This is to ensure back-compatibility but find_by! should be used
- # in that casse in 9.0!
- member = source.members.find_by(user_id: params[:user_id])
-
- # This is to ensure back-compatibility but this should be removed in
- # favor of find_by! in 9.0!
- not_found!("Member: user_id:#{params[:user_id]}") if source_type == 'group' && member.nil?
-
- # This is to ensure back-compatibility but 204 behavior should be used
- # for all DELETE endpoints in 9.0!
- if member.nil?
- status(200 )
- { message: "Access revoked", id: params[:user_id].to_i }
- else
- ::Members::DestroyService.new(current_user).execute(member)
-
- present member, with: ::API::Entities::Member
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/api/v3/merge_request_diffs.rb b/lib/api/v3/merge_request_diffs.rb
deleted file mode 100644
index 22866fc2845..00000000000
--- a/lib/api/v3/merge_request_diffs.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-module API
- module V3
- # MergeRequestDiff API
- class MergeRequestDiffs < Grape::API
- before { authenticate! }
-
- params do
- requires :id, type: String, desc: 'The ID of a project'
- end
- resource :projects, requirements: { id: %r{[^/]+} } do
- desc 'Get a list of merge request diff versions' do
- detail 'This feature was introduced in GitLab 8.12.'
- success ::API::Entities::MergeRequestDiff
- end
-
- params do
- requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
- end
-
- get ":id/merge_requests/:merge_request_id/versions" do
- merge_request = find_merge_request_with_access(params[:merge_request_id])
-
- present merge_request.merge_request_diffs.order_id_desc, with: ::API::Entities::MergeRequestDiff
- end
-
- desc 'Get a single merge request diff version' do
- detail 'This feature was introduced in GitLab 8.12.'
- success ::API::Entities::MergeRequestDiffFull
- end
-
- params do
- requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
- requires :version_id, type: Integer, desc: 'The ID of a merge request diff version'
- end
-
- get ":id/merge_requests/:merge_request_id/versions/:version_id" do
- merge_request = find_merge_request_with_access(params[:merge_request_id])
-
- present merge_request.merge_request_diffs.find(params[:version_id]), with: ::API::Entities::MergeRequestDiffFull
- end
- end
- end
- end
-end
diff --git a/lib/api/v3/merge_requests.rb b/lib/api/v3/merge_requests.rb
deleted file mode 100644
index 9b0f70e2bfe..00000000000
--- a/lib/api/v3/merge_requests.rb
+++ /dev/null
@@ -1,297 +0,0 @@
-module API
- module V3
- class MergeRequests < Grape::API
- include PaginationParams
-
- DEPRECATION_MESSAGE = 'This endpoint is deprecated and has been removed on V4'.freeze
-
- before { authenticate! }
-
- params do
- requires :id, type: String, desc: 'The ID of a project'
- end
- resource :projects, requirements: { id: %r{[^/]+} } do
- include TimeTrackingEndpoints
-
- helpers do
- def handle_merge_request_errors!(errors)
- if errors[:project_access].any?
- error!(errors[:project_access], 422)
- elsif errors[:branch_conflict].any?
- error!(errors[:branch_conflict], 422)
- elsif errors[:validate_fork].any?
- error!(errors[:validate_fork], 422)
- elsif errors[:validate_branches].any?
- conflict!(errors[:validate_branches])
- elsif errors[:base].any?
- error!(errors[:base], 422)
- end
-
- render_api_error!(errors, 400)
- end
-
- def issue_entity(project)
- if project.has_external_issue_tracker?
- ::API::Entities::ExternalIssue
- else
- ::API::V3::Entities::Issue
- end
- end
-
- params :optional_params do
- optional :description, type: String, desc: 'The description of the merge request'
- optional :assignee_id, type: Integer, desc: 'The ID of a user to assign the merge request'
- optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign the merge request'
- optional :labels, type: String, desc: 'Comma-separated list of label names'
- optional :remove_source_branch, type: Boolean, desc: 'Remove source branch when merging'
- end
- end
-
- desc 'List merge requests' do
- detail 'iid filter is deprecated have been removed on V4'
- success ::API::V3::Entities::MergeRequest
- end
- params do
- optional :state, type: String, values: %w[opened closed merged all], default: 'all',
- desc: 'Return opened, closed, merged, or all merge requests'
- optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at',
- desc: 'Return merge requests ordered by `created_at` or `updated_at` fields.'
- optional :sort, type: String, values: %w[asc desc], default: 'desc',
- desc: 'Return merge requests sorted in `asc` or `desc` order.'
- optional :iid, type: Array[Integer], desc: 'The IID of the merge requests'
- use :pagination
- end
- get ":id/merge_requests" do
- authorize! :read_merge_request, user_project
-
- merge_requests = user_project.merge_requests.inc_notes_with_associations
- merge_requests = filter_by_iid(merge_requests, params[:iid]) if params[:iid].present?
-
- merge_requests =
- case params[:state]
- when 'opened' then merge_requests.opened
- when 'closed' then merge_requests.closed
- when 'merged' then merge_requests.merged
- else merge_requests
- end
-
- merge_requests = merge_requests.reorder(params[:order_by] => params[:sort])
- present paginate(merge_requests), with: ::API::V3::Entities::MergeRequest, current_user: current_user, project: user_project
- end
-
- desc 'Create a merge request' do
- success ::API::V3::Entities::MergeRequest
- end
- params do
- requires :title, type: String, desc: 'The title of the merge request'
- requires :source_branch, type: String, desc: 'The source branch'
- requires :target_branch, type: String, desc: 'The target branch'
- optional :target_project_id, type: Integer,
- desc: 'The target project of the merge request defaults to the :id of the project'
- use :optional_params
- end
- post ":id/merge_requests" do
- Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42126')
-
- authorize! :create_merge_request_from, user_project
-
- mr_params = declared_params(include_missing: false)
- mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present?
-
- merge_request = ::MergeRequests::CreateService.new(user_project, current_user, mr_params).execute
-
- if merge_request.valid?
- present merge_request, with: ::API::V3::Entities::MergeRequest, current_user: current_user, project: user_project
- else
- handle_merge_request_errors! merge_request.errors
- end
- end
-
- desc 'Delete a merge request'
- params do
- requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
- end
- delete ":id/merge_requests/:merge_request_id" do
- merge_request = find_project_merge_request(params[:merge_request_id])
-
- authorize!(:destroy_merge_request, merge_request)
-
- status(200)
- merge_request.destroy
- end
-
- params do
- requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
- end
- { ":id/merge_request/:merge_request_id" => :deprecated, ":id/merge_requests/:merge_request_id" => :ok }.each do |path, status|
- desc 'Get a single merge request' do
- if status == :deprecated
- detail DEPRECATION_MESSAGE
- end
-
- success ::API::V3::Entities::MergeRequest
- end
- get path do
- merge_request = find_merge_request_with_access(params[:merge_request_id])
-
- present merge_request, with: ::API::V3::Entities::MergeRequest, current_user: current_user, project: user_project
- end
-
- desc 'Get the commits of a merge request' do
- success ::API::Entities::Commit
- end
- get "#{path}/commits" do
- merge_request = find_merge_request_with_access(params[:merge_request_id])
-
- present merge_request.commits, with: ::API::Entities::Commit
- end
-
- desc 'Show the merge request changes' do
- success ::API::Entities::MergeRequestChanges
- end
- get "#{path}/changes" do
- merge_request = find_merge_request_with_access(params[:merge_request_id])
-
- present merge_request, with: ::API::Entities::MergeRequestChanges, current_user: current_user
- end
-
- desc 'Update a merge request' do
- success ::API::V3::Entities::MergeRequest
- end
- params do
- optional :title, type: String, allow_blank: false, desc: 'The title of the merge request'
- optional :target_branch, type: String, allow_blank: false, desc: 'The target branch'
- optional :state_event, type: String, values: %w[close reopen merge],
- desc: 'Status of the merge request'
- use :optional_params
- at_least_one_of :title, :target_branch, :description, :assignee_id,
- :milestone_id, :labels, :state_event,
- :remove_source_branch
- end
- put path do
- Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42127')
-
- merge_request = find_merge_request_with_access(params.delete(:merge_request_id), :update_merge_request)
-
- mr_params = declared_params(include_missing: false)
- mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present?
-
- merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, mr_params).execute(merge_request)
-
- if merge_request.valid?
- present merge_request, with: ::API::V3::Entities::MergeRequest, current_user: current_user, project: user_project
- else
- handle_merge_request_errors! merge_request.errors
- end
- end
-
- desc 'Merge a merge request' do
- success ::API::V3::Entities::MergeRequest
- end
- params do
- optional :merge_commit_message, type: String, desc: 'Custom merge commit message'
- optional :should_remove_source_branch, type: Boolean,
- desc: 'When true, the source branch will be deleted if possible'
- optional :merge_when_build_succeeds, type: Boolean,
- desc: 'When true, this merge request will be merged when the build succeeds'
- optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch'
- end
- put "#{path}/merge" do
- merge_request = find_project_merge_request(params[:merge_request_id])
-
- # Merge request can not be merged
- # because user dont have permissions to push into target branch
- unauthorized! unless merge_request.can_be_merged_by?(current_user)
-
- not_allowed! unless merge_request.mergeable_state?
-
- render_api_error!('Branch cannot be merged', 406) unless merge_request.mergeable?
-
- if params[:sha] && merge_request.diff_head_sha != params[:sha]
- render_api_error!("SHA does not match HEAD of source branch: #{merge_request.diff_head_sha}", 409)
- end
-
- merge_params = {
- commit_message: params[:merge_commit_message],
- should_remove_source_branch: params[:should_remove_source_branch]
- }
-
- if params[:merge_when_build_succeeds] && merge_request.head_pipeline && merge_request.head_pipeline.active?
- ::MergeRequests::MergeWhenPipelineSucceedsService
- .new(merge_request.target_project, current_user, merge_params)
- .execute(merge_request)
- else
- ::MergeRequests::MergeService
- .new(merge_request.target_project, current_user, merge_params)
- .execute(merge_request)
- end
-
- present merge_request, with: ::API::V3::Entities::MergeRequest, current_user: current_user, project: user_project
- end
-
- desc 'Cancel merge if "Merge When Build succeeds" is enabled' do
- success ::API::V3::Entities::MergeRequest
- end
- post "#{path}/cancel_merge_when_build_succeeds" do
- merge_request = find_project_merge_request(params[:merge_request_id])
-
- unauthorized! unless merge_request.can_cancel_merge_when_pipeline_succeeds?(current_user)
-
- ::MergeRequest::MergeWhenPipelineSucceedsService
- .new(merge_request.target_project, current_user)
- .cancel(merge_request)
- end
-
- desc 'Get the comments of a merge request' do
- detail 'Duplicate. DEPRECATED and HAS BEEN REMOVED in V4'
- success ::API::Entities::MRNote
- end
- params do
- use :pagination
- end
- get "#{path}/comments" do
- merge_request = find_merge_request_with_access(params[:merge_request_id])
- present paginate(merge_request.notes.fresh), with: ::API::Entities::MRNote
- end
-
- desc 'Post a comment to a merge request' do
- detail 'Duplicate. DEPRECATED and HAS BEEN REMOVED in V4'
- success ::API::Entities::MRNote
- end
- params do
- requires :note, type: String, desc: 'The text of the comment'
- end
- post "#{path}/comments" do
- merge_request = find_merge_request_with_access(params[:merge_request_id], :create_note)
-
- opts = {
- note: params[:note],
- noteable_type: 'MergeRequest',
- noteable_id: merge_request.id
- }
-
- note = ::Notes::CreateService.new(user_project, current_user, opts).execute
-
- if note.save
- present note, with: ::API::Entities::MRNote
- else
- render_api_error!("Failed to save note #{note.errors.messages}", 400)
- end
- end
-
- desc 'List issues that will be closed on merge' do
- success ::API::Entities::MRNote
- end
- params do
- use :pagination
- end
- get "#{path}/closes_issues" do
- merge_request = find_merge_request_with_access(params[:merge_request_id])
- issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user))
- present paginate(issues), with: issue_entity(user_project), current_user: current_user
- end
- end
- end
- end
- end
-end
diff --git a/lib/api/v3/milestones.rb b/lib/api/v3/milestones.rb
deleted file mode 100644
index 9be4cf9d22a..00000000000
--- a/lib/api/v3/milestones.rb
+++ /dev/null
@@ -1,65 +0,0 @@
-module API
- module V3
- class Milestones < Grape::API
- include PaginationParams
-
- before { authenticate! }
-
- helpers do
- def filter_milestones_state(milestones, state)
- case state
- when 'active' then milestones.active
- when 'closed' then milestones.closed
- else milestones
- end
- end
- end
-
- params do
- requires :id, type: String, desc: 'The ID of a project'
- end
- resource :projects, requirements: { id: %r{[^/]+} } do
- desc 'Get a list of project milestones' do
- success ::API::Entities::Milestone
- end
- params do
- optional :state, type: String, values: %w[active closed all], default: 'all',
- desc: 'Return "active", "closed", or "all" milestones'
- optional :iid, type: Array[Integer], desc: 'The IID of the milestone'
- use :pagination
- end
- get ":id/milestones" do
- authorize! :read_milestone, user_project
-
- milestones = user_project.milestones
- milestones = filter_milestones_state(milestones, params[:state])
- milestones = filter_by_iid(milestones, params[:iid]) if params[:iid].present?
- milestones = milestones.order_id_desc
-
- present paginate(milestones), with: ::API::Entities::Milestone
- end
-
- desc 'Get all issues for a single project milestone' do
- success ::API::V3::Entities::Issue
- end
- params do
- requires :milestone_id, type: Integer, desc: 'The ID of a project milestone'
- use :pagination
- end
- get ':id/milestones/:milestone_id/issues' do
- authorize! :read_milestone, user_project
-
- milestone = user_project.milestones.find(params[:milestone_id])
-
- finder_params = {
- project_id: user_project.id,
- milestone_title: milestone.title
- }
-
- issues = IssuesFinder.new(current_user, finder_params).execute
- present paginate(issues), with: ::API::V3::Entities::Issue, current_user: current_user, project: user_project
- end
- end
- end
- end
-end
diff --git a/lib/api/v3/notes.rb b/lib/api/v3/notes.rb
deleted file mode 100644
index d49772b92f2..00000000000
--- a/lib/api/v3/notes.rb
+++ /dev/null
@@ -1,148 +0,0 @@
-module API
- module V3
- class Notes < Grape::API
- include PaginationParams
-
- before { authenticate! }
-
- NOTEABLE_TYPES = [Issue, MergeRequest, Snippet].freeze
-
- params do
- requires :id, type: String, desc: 'The ID of a project'
- end
- resource :projects, requirements: { id: %r{[^/]+} } do
- NOTEABLE_TYPES.each do |noteable_type|
- noteables_str = noteable_type.to_s.underscore.pluralize
-
- desc 'Get a list of project +noteable+ notes' do
- success ::API::V3::Entities::Note
- end
- params do
- requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
- use :pagination
- end
- get ":id/#{noteables_str}/:noteable_id/notes" do
- noteable = user_project.public_send(noteables_str.to_sym).find(params[:noteable_id]) # rubocop:disable GitlabSecurity/PublicSend
-
- if can?(current_user, noteable_read_ability_name(noteable), noteable)
- # We exclude notes that are cross-references and that cannot be viewed
- # by the current user. By doing this exclusion at this level and not
- # 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.
- 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)
- .reject { |n| n.cross_reference_not_visible_for?(current_user) }
- present notes, with: ::API::V3::Entities::Note
- else
- not_found!("Notes")
- end
- end
-
- desc 'Get a single +noteable+ note' do
- success ::API::V3::Entities::Note
- end
- params do
- requires :note_id, type: Integer, desc: 'The ID of a note'
- requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
- end
- get ":id/#{noteables_str}/:noteable_id/notes/:note_id" do
- noteable = user_project.public_send(noteables_str.to_sym).find(params[:noteable_id]) # rubocop:disable GitlabSecurity/PublicSend
- note = noteable.notes.find(params[:note_id])
- can_read_note = can?(current_user, noteable_read_ability_name(noteable), noteable) && !note.cross_reference_not_visible_for?(current_user)
-
- if can_read_note
- present note, with: ::API::V3::Entities::Note
- else
- not_found!("Note")
- end
- end
-
- desc 'Create a new +noteable+ note' do
- success ::API::V3::Entities::Note
- end
- params do
- requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
- requires :body, type: String, desc: 'The content of a note'
- optional :created_at, type: String, desc: 'The creation date of the note'
- end
- post ":id/#{noteables_str}/:noteable_id/notes" do
- opts = {
- note: params[:body],
- noteable_type: noteables_str.classify,
- noteable_id: params[:noteable_id]
- }
-
- noteable = user_project.public_send(noteables_str.to_sym).find(params[:noteable_id]) # rubocop:disable GitlabSecurity/PublicSend
-
- if can?(current_user, noteable_read_ability_name(noteable), noteable)
- if params[:created_at] && (current_user.admin? || user_project.owner == current_user)
- opts[:created_at] = params[:created_at]
- end
-
- note = ::Notes::CreateService.new(user_project, current_user, opts).execute
- if note.valid?
- present note, with: ::API::V3::Entities.const_get(note.class.name)
- else
- not_found!("Note #{note.errors.messages}")
- end
- else
- not_found!("Note")
- end
- end
-
- desc 'Update an existing +noteable+ note' do
- success ::API::V3::Entities::Note
- end
- params do
- requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
- requires :note_id, type: Integer, desc: 'The ID of a note'
- requires :body, type: String, desc: 'The content of a note'
- end
- put ":id/#{noteables_str}/:noteable_id/notes/:note_id" do
- note = user_project.notes.find(params[:note_id])
-
- authorize! :admin_note, note
-
- opts = {
- note: params[:body]
- }
-
- note = ::Notes::UpdateService.new(user_project, current_user, opts).execute(note)
-
- if note.valid?
- present note, with: ::API::V3::Entities::Note
- else
- render_api_error!("Failed to save note #{note.errors.messages}", 400)
- end
- end
-
- desc 'Delete a +noteable+ note' do
- success ::API::V3::Entities::Note
- end
- params do
- requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
- requires :note_id, type: Integer, desc: 'The ID of a note'
- end
- delete ":id/#{noteables_str}/:noteable_id/notes/:note_id" do
- note = user_project.notes.find(params[:note_id])
- authorize! :admin_note, note
-
- ::Notes::DestroyService.new(user_project, current_user).execute(note)
-
- present note, with: ::API::V3::Entities::Note
- end
- end
- end
-
- helpers do
- def noteable_read_ability_name(noteable)
- "read_#{noteable.class.to_s.underscore}".to_sym
- end
- end
- end
- end
-end
diff --git a/lib/api/v3/pipelines.rb b/lib/api/v3/pipelines.rb
deleted file mode 100644
index 6d31c12f572..00000000000
--- a/lib/api/v3/pipelines.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-module API
- module V3
- class Pipelines < Grape::API
- include PaginationParams
-
- before { authenticate! }
-
- params do
- requires :id, type: String, desc: 'The project ID'
- end
- resource :projects, requirements: { id: %r{[^/]+} } do
- desc 'Get all Pipelines of the project' do
- detail 'This feature was introduced in GitLab 8.11.'
- success ::API::Entities::Pipeline
- end
- params do
- use :pagination
- optional :scope, type: String, values: %w(running branches tags),
- desc: 'Either running, branches, or tags'
- end
- get ':id/pipelines' do
- Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42123')
-
- authorize! :read_pipeline, user_project
-
- pipelines = PipelinesFinder.new(user_project, scope: params[:scope]).execute
- present paginate(pipelines), with: ::API::Entities::Pipeline
- end
- end
-
- helpers do
- def pipeline
- @pipeline ||= user_project.pipelines.find(params[:pipeline_id])
- end
- end
- end
- end
-end
diff --git a/lib/api/v3/project_hooks.rb b/lib/api/v3/project_hooks.rb
deleted file mode 100644
index 631944150c7..00000000000
--- a/lib/api/v3/project_hooks.rb
+++ /dev/null
@@ -1,111 +0,0 @@
-module API
- module V3
- class ProjectHooks < Grape::API
- include PaginationParams
-
- before { authenticate! }
- before { authorize_admin_project }
-
- helpers do
- params :project_hook_properties do
- requires :url, type: String, desc: "The URL to send the request to"
- optional :push_events, type: Boolean, desc: "Trigger hook on push events"
- optional :issues_events, type: Boolean, desc: "Trigger hook on issues events"
- optional :confidential_issues_events, type: Boolean, desc: "Trigger hook on confidential issues events"
- optional :merge_requests_events, type: Boolean, desc: "Trigger hook on merge request events"
- optional :tag_push_events, type: Boolean, desc: "Trigger hook on tag push events"
- optional :note_events, type: Boolean, desc: "Trigger hook on note(comment) events"
- optional :build_events, type: Boolean, desc: "Trigger hook on build events"
- optional :pipeline_events, type: Boolean, desc: "Trigger hook on pipeline events"
- optional :wiki_page_events, type: Boolean, desc: "Trigger hook on wiki events"
- optional :enable_ssl_verification, type: Boolean, desc: "Do SSL verification when triggering the hook"
- optional :token, type: String, desc: "Secret token to validate received payloads; this will not be returned in the response"
- end
- end
-
- params do
- requires :id, type: String, desc: 'The ID of a project'
- end
- resource :projects, requirements: { id: %r{[^/]+} } do
- desc 'Get project hooks' do
- success ::API::V3::Entities::ProjectHook
- end
- params do
- use :pagination
- end
- get ":id/hooks" do
- hooks = paginate user_project.hooks
-
- present hooks, with: ::API::V3::Entities::ProjectHook
- end
-
- desc 'Get a project hook' do
- success ::API::V3::Entities::ProjectHook
- end
- params do
- requires :hook_id, type: Integer, desc: 'The ID of a project hook'
- end
- get ":id/hooks/:hook_id" do
- hook = user_project.hooks.find(params[:hook_id])
- present hook, with: ::API::V3::Entities::ProjectHook
- end
-
- desc 'Add hook to project' do
- success ::API::V3::Entities::ProjectHook
- end
- params do
- use :project_hook_properties
- end
- post ":id/hooks" do
- attrs = declared_params(include_missing: false)
- attrs[:job_events] = attrs.delete(:build_events) if attrs.key?(:build_events)
- hook = user_project.hooks.new(attrs)
-
- if hook.save
- present hook, with: ::API::V3::Entities::ProjectHook
- else
- error!("Invalid url given", 422) if hook.errors[:url].present?
-
- not_found!("Project hook #{hook.errors.messages}")
- end
- end
-
- desc 'Update an existing project hook' do
- success ::API::V3::Entities::ProjectHook
- end
- params do
- requires :hook_id, type: Integer, desc: "The ID of the hook to update"
- use :project_hook_properties
- end
- put ":id/hooks/:hook_id" do
- hook = user_project.hooks.find(params.delete(:hook_id))
-
- attrs = declared_params(include_missing: false)
- attrs[:job_events] = attrs.delete(:build_events) if attrs.key?(:build_events)
- if hook.update_attributes(attrs)
- present hook, with: ::API::V3::Entities::ProjectHook
- else
- error!("Invalid url given", 422) if hook.errors[:url].present?
-
- not_found!("Project hook #{hook.errors.messages}")
- end
- end
-
- desc 'Deletes project hook' do
- success ::API::V3::Entities::ProjectHook
- end
- params do
- requires :hook_id, type: Integer, desc: 'The ID of the hook to delete'
- end
- delete ":id/hooks/:hook_id" do
- begin
- present user_project.hooks.destroy(params[:hook_id]), with: ::API::V3::Entities::ProjectHook
- rescue
- # ProjectHook can raise Error if hook_id not found
- not_found!("Error deleting hook #{params[:hook_id]}")
- end
- end
- end
- end
- end
-end
diff --git a/lib/api/v3/project_snippets.rb b/lib/api/v3/project_snippets.rb
deleted file mode 100644
index 6ba425ba8c7..00000000000
--- a/lib/api/v3/project_snippets.rb
+++ /dev/null
@@ -1,143 +0,0 @@
-module API
- module V3
- class ProjectSnippets < Grape::API
- include PaginationParams
-
- before { authenticate! }
-
- params do
- requires :id, type: String, desc: 'The ID of a project'
- end
- resource :projects, requirements: { id: %r{[^/]+} } do
- helpers do
- def handle_project_member_errors(errors)
- if errors[:project_access].any?
- error!(errors[:project_access], 422)
- end
-
- not_found!
- end
-
- def snippets_for_current_user
- SnippetsFinder.new(current_user, project: user_project).execute
- end
- end
-
- desc 'Get all project snippets' do
- success ::API::V3::Entities::ProjectSnippet
- end
- params do
- use :pagination
- end
- get ":id/snippets" do
- present paginate(snippets_for_current_user), with: ::API::V3::Entities::ProjectSnippet
- end
-
- desc 'Get a single project snippet' do
- success ::API::V3::Entities::ProjectSnippet
- end
- params do
- requires :snippet_id, type: Integer, desc: 'The ID of a project snippet'
- end
- get ":id/snippets/:snippet_id" do
- snippet = snippets_for_current_user.find(params[:snippet_id])
- present snippet, with: ::API::V3::Entities::ProjectSnippet
- end
-
- desc 'Create a new project snippet' do
- success ::API::V3::Entities::ProjectSnippet
- end
- params do
- requires :title, type: String, desc: 'The title of the snippet'
- requires :file_name, type: String, desc: 'The file name of the snippet'
- requires :code, type: String, desc: 'The content of the snippet'
- requires :visibility_level, type: Integer,
- values: [Gitlab::VisibilityLevel::PRIVATE,
- Gitlab::VisibilityLevel::INTERNAL,
- Gitlab::VisibilityLevel::PUBLIC],
- desc: 'The visibility level of the snippet'
- end
- post ":id/snippets" do
- authorize! :create_project_snippet, user_project
- snippet_params = declared_params.merge(request: request, api: true)
- snippet_params[:content] = snippet_params.delete(:code)
-
- snippet = CreateSnippetService.new(user_project, current_user, snippet_params).execute
-
- render_spam_error! if snippet.spam?
-
- if snippet.persisted?
- present snippet, with: ::API::V3::Entities::ProjectSnippet
- else
- render_validation_error!(snippet)
- end
- end
-
- desc 'Update an existing project snippet' do
- success ::API::V3::Entities::ProjectSnippet
- end
- params do
- requires :snippet_id, type: Integer, desc: 'The ID of a project snippet'
- optional :title, type: String, desc: 'The title of the snippet'
- optional :file_name, type: String, desc: 'The file name of the snippet'
- optional :code, type: String, desc: 'The content of the snippet'
- optional :visibility_level, type: Integer,
- values: [Gitlab::VisibilityLevel::PRIVATE,
- Gitlab::VisibilityLevel::INTERNAL,
- Gitlab::VisibilityLevel::PUBLIC],
- desc: 'The visibility level of the snippet'
- at_least_one_of :title, :file_name, :code, :visibility_level
- end
- put ":id/snippets/:snippet_id" do
- snippet = snippets_for_current_user.find_by(id: params.delete(:snippet_id))
- not_found!('Snippet') unless snippet
-
- authorize! :update_project_snippet, snippet
-
- snippet_params = declared_params(include_missing: false)
- .merge(request: request, api: true)
-
- snippet_params[:content] = snippet_params.delete(:code) if snippet_params[:code].present?
-
- UpdateSnippetService.new(user_project, current_user, snippet,
- snippet_params).execute
-
- render_spam_error! if snippet.spam?
-
- if snippet.valid?
- present snippet, with: ::API::V3::Entities::ProjectSnippet
- else
- render_validation_error!(snippet)
- end
- end
-
- desc 'Delete a project snippet'
- params do
- requires :snippet_id, type: Integer, desc: 'The ID of a project snippet'
- end
- delete ":id/snippets/:snippet_id" do
- snippet = snippets_for_current_user.find_by(id: params[:snippet_id])
- not_found!('Snippet') unless snippet
-
- authorize! :admin_project_snippet, snippet
- snippet.destroy
-
- status(200)
- end
-
- desc 'Get a raw project snippet'
- params do
- requires :snippet_id, type: Integer, desc: 'The ID of a project snippet'
- end
- get ":id/snippets/:snippet_id/raw" do
- snippet = snippets_for_current_user.find_by(id: params[:snippet_id])
- not_found!('Snippet') unless snippet
-
- env['api.format'] = :txt
- content_type 'text/plain'
- present snippet.content
- end
- end
- end
- end
-end
diff --git a/lib/api/v3/projects.rb b/lib/api/v3/projects.rb
deleted file mode 100644
index eb3dd113524..00000000000
--- a/lib/api/v3/projects.rb
+++ /dev/null
@@ -1,475 +0,0 @@
-module API
- module V3
- class Projects < Grape::API
- include PaginationParams
-
- before { authenticate_non_get! }
-
- after_validation do
- set_only_allow_merge_if_pipeline_succeeds!
- end
-
- helpers do
- params :optional_params do
- optional :description, type: String, desc: 'The description of the project'
- optional :issues_enabled, type: Boolean, desc: 'Flag indication if the issue tracker is enabled'
- optional :merge_requests_enabled, type: Boolean, desc: 'Flag indication if merge requests are enabled'
- optional :wiki_enabled, type: Boolean, desc: 'Flag indication if the wiki is enabled'
- optional :builds_enabled, type: Boolean, desc: 'Flag indication if builds are enabled'
- optional :snippets_enabled, type: Boolean, desc: 'Flag indication if snippets are enabled'
- optional :shared_runners_enabled, type: Boolean, desc: 'Flag indication if shared runners are enabled for that project'
- optional :resolve_outdated_diff_discussions, type: Boolean, desc: 'Automatically resolve merge request diffs discussions on lines changed with a push'
- optional :container_registry_enabled, type: Boolean, desc: 'Flag indication if the container registry is enabled for that project'
- optional :lfs_enabled, type: Boolean, desc: 'Flag indication if Git LFS is enabled for that project'
- optional :public, type: Boolean, desc: 'Create a public project. The same as visibility_level = 20.'
- optional :visibility_level, type: Integer, values: [
- Gitlab::VisibilityLevel::PRIVATE,
- Gitlab::VisibilityLevel::INTERNAL,
- Gitlab::VisibilityLevel::PUBLIC
- ], desc: 'Create a public project. The same as visibility_level = 20.'
- optional :public_builds, type: Boolean, desc: 'Perform public builds'
- optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access'
- optional :only_allow_merge_if_build_succeeds, type: Boolean, desc: 'Only allow to merge if builds succeed'
- optional :only_allow_merge_if_pipeline_succeeds, type: Boolean, desc: 'Only allow to merge if builds succeed'
- optional :only_allow_merge_if_all_discussions_are_resolved, type: Boolean, desc: 'Only allow to merge if all discussions are resolved'
- end
-
- def map_public_to_visibility_level(attrs)
- publik = attrs.delete(:public)
- if !publik.nil? && !attrs[:visibility_level].present?
- # Since setting the public attribute to private could mean either
- # private or internal, use the more conservative option, private.
- attrs[:visibility_level] = (publik == true) ? Gitlab::VisibilityLevel::PUBLIC : Gitlab::VisibilityLevel::PRIVATE
- end
-
- attrs
- end
-
- def set_only_allow_merge_if_pipeline_succeeds!
- if params.key?(:only_allow_merge_if_build_succeeds)
- params[:only_allow_merge_if_pipeline_succeeds] = params.delete(:only_allow_merge_if_build_succeeds)
- end
- end
- end
-
- resource :projects do
- helpers do
- params :collection_params do
- use :sort_params
- use :filter_params
- use :pagination
-
- optional :simple, type: Boolean, default: false,
- desc: 'Return only the ID, URL, name, and path of each project'
- end
-
- params :sort_params do
- optional :order_by, type: String, values: %w[id name path created_at updated_at last_activity_at],
- default: 'created_at', desc: 'Return projects ordered by field'
- optional :sort, type: String, values: %w[asc desc], default: 'desc',
- desc: 'Return projects sorted in ascending and descending order'
- end
-
- params :filter_params do
- optional :archived, type: Boolean, default: nil, desc: 'Limit by archived status'
- optional :visibility, type: String, values: %w[public internal private],
- desc: 'Limit by visibility'
- optional :search, type: String, desc: 'Return list of authorized projects matching the search criteria'
- end
-
- params :statistics_params do
- optional :statistics, type: Boolean, default: false, desc: 'Include project statistics'
- end
-
- params :create_params do
- optional :namespace_id, type: Integer, desc: 'Namespace ID for the new project. Default to the user namespace.'
- optional :import_url, type: String, desc: 'URL from which the project is imported'
- end
-
- def present_projects(projects, options = {})
- options = options.reverse_merge(
- with: ::API::V3::Entities::Project,
- current_user: current_user,
- simple: params[:simple]
- )
-
- projects = filter_projects(projects)
- projects = projects.with_statistics if options[:statistics]
- options[:with] = ::API::Entities::BasicProjectDetails if options[:simple]
-
- present paginate(projects), options
- end
- end
-
- desc 'Get a list of visible projects for authenticated user' do
- success ::API::Entities::BasicProjectDetails
- end
- params do
- use :collection_params
- end
- get '/visible' do
- entity = current_user ? ::API::V3::Entities::ProjectWithAccess : ::API::Entities::BasicProjectDetails
- present_projects ProjectsFinder.new(current_user: current_user).execute, with: entity
- end
-
- desc 'Get a projects list for authenticated user' do
- success ::API::Entities::BasicProjectDetails
- end
- params do
- use :collection_params
- end
- get do
- authenticate!
-
- present_projects current_user.authorized_projects.order_id_desc,
- with: ::API::V3::Entities::ProjectWithAccess
- end
-
- desc 'Get an owned projects list for authenticated user' do
- success ::API::Entities::BasicProjectDetails
- end
- params do
- use :collection_params
- use :statistics_params
- end
- get '/owned' do
- authenticate!
-
- present_projects current_user.owned_projects,
- with: ::API::V3::Entities::ProjectWithAccess,
- statistics: params[:statistics]
- end
-
- desc 'Gets starred project for the authenticated user' do
- success ::API::Entities::BasicProjectDetails
- end
- params do
- use :collection_params
- end
- get '/starred' do
- authenticate!
-
- present_projects ProjectsFinder.new(current_user: current_user, params: { starred: true }).execute
- end
-
- desc 'Get all projects for admin user' do
- success ::API::Entities::BasicProjectDetails
- end
- params do
- use :collection_params
- use :statistics_params
- end
- get '/all' do
- authenticated_as_admin!
-
- present_projects Project.all, with: ::API::V3::Entities::ProjectWithAccess, statistics: params[:statistics]
- end
-
- desc 'Search for projects the current user has access to' do
- success ::API::V3::Entities::Project
- end
- params do
- requires :query, type: String, desc: 'The project name to be searched'
- use :sort_params
- use :pagination
- end
- get "/search/:query", requirements: { query: %r{[^/]+} } do
- search_service = ::Search::GlobalService.new(current_user, search: params[:query]).execute
- projects = search_service.objects('projects', params[:page], false)
- projects = projects.reorder(params[:order_by] => params[:sort])
-
- present paginate(projects), with: ::API::V3::Entities::Project
- end
-
- desc 'Create new project' do
- success ::API::V3::Entities::Project
- end
- params do
- optional :name, type: String, desc: 'The name of the project'
- optional :path, type: String, desc: 'The path of the repository'
- at_least_one_of :name, :path
- use :optional_params
- use :create_params
- end
- post do
- attrs = map_public_to_visibility_level(declared_params(include_missing: false))
- project = ::Projects::CreateService.new(current_user, attrs).execute
-
- if project.saved?
- present project, with: ::API::V3::Entities::Project,
- user_can_admin_project: can?(current_user, :admin_project, project)
- else
- if project.errors[:limit_reached].present?
- error!(project.errors[:limit_reached], 403)
- end
-
- render_validation_error!(project)
- end
- end
-
- desc 'Create new project for a specified user. Only available to admin users.' do
- success ::API::V3::Entities::Project
- end
- params do
- requires :name, type: String, desc: 'The name of the project'
- requires :user_id, type: Integer, desc: 'The ID of a user'
- optional :default_branch, type: String, desc: 'The default branch of the project'
- use :optional_params
- use :create_params
- end
- post "user/:user_id" do
- authenticated_as_admin!
- user = User.find_by(id: params.delete(:user_id))
- not_found!('User') unless user
-
- attrs = map_public_to_visibility_level(declared_params(include_missing: false))
- project = ::Projects::CreateService.new(user, attrs).execute
-
- if project.saved?
- present project, with: ::API::V3::Entities::Project,
- user_can_admin_project: can?(current_user, :admin_project, project)
- else
- render_validation_error!(project)
- end
- end
- end
-
- params do
- requires :id, type: String, desc: 'The ID of a project'
- end
- resource :projects, requirements: { id: %r{[^/]+} } do
- desc 'Get a single project' do
- success ::API::V3::Entities::ProjectWithAccess
- end
- get ":id" do
- entity = current_user ? ::API::V3::Entities::ProjectWithAccess : ::API::Entities::BasicProjectDetails
- present user_project, with: entity, current_user: current_user,
- user_can_admin_project: can?(current_user, :admin_project, user_project)
- end
-
- desc 'Get events for a single project' do
- success ::API::V3::Entities::Event
- end
- params do
- use :pagination
- end
- get ":id/events" do
- present paginate(user_project.events.recent), with: ::API::V3::Entities::Event
- end
-
- desc 'Fork new project for the current user or provided namespace.' do
- success ::API::V3::Entities::Project
- end
- params do
- optional :namespace, type: String, desc: 'The ID or name of the namespace that the project will be forked into'
- end
- post 'fork/:id' do
- fork_params = declared_params(include_missing: false)
- namespace_id = fork_params[:namespace]
-
- if namespace_id.present?
- fork_params[:namespace] = find_namespace(namespace_id)
-
- unless fork_params[:namespace] && can?(current_user, :create_projects, fork_params[:namespace])
- not_found!('Target Namespace')
- end
- end
-
- forked_project = ::Projects::ForkService.new(user_project, current_user, fork_params).execute
-
- if forked_project.errors.any?
- conflict!(forked_project.errors.messages)
- else
- present forked_project, with: ::API::V3::Entities::Project,
- user_can_admin_project: can?(current_user, :admin_project, forked_project)
- end
- end
-
- desc 'Update an existing project' do
- success ::API::V3::Entities::Project
- end
- params do
- optional :name, type: String, desc: 'The name of the project'
- optional :default_branch, type: String, desc: 'The default branch of the project'
- optional :path, type: String, desc: 'The path of the repository'
- use :optional_params
- at_least_one_of :name, :description, :issues_enabled, :merge_requests_enabled,
- :wiki_enabled, :builds_enabled, :snippets_enabled,
- :shared_runners_enabled, :resolve_outdated_diff_discussions,
- :container_registry_enabled, :lfs_enabled, :public, :visibility_level,
- :public_builds, :request_access_enabled, :only_allow_merge_if_build_succeeds,
- :only_allow_merge_if_all_discussions_are_resolved, :path,
- :default_branch
- end
- put ':id' do
- authorize_admin_project
- attrs = map_public_to_visibility_level(declared_params(include_missing: false))
- authorize! :rename_project, user_project if attrs[:name].present?
- authorize! :change_visibility_level, user_project if attrs[:visibility_level].present?
-
- result = ::Projects::UpdateService.new(user_project, current_user, attrs).execute
-
- if result[:status] == :success
- present user_project, with: ::API::V3::Entities::Project,
- user_can_admin_project: can?(current_user, :admin_project, user_project)
- else
- render_validation_error!(user_project)
- end
- end
-
- desc 'Archive a project' do
- success ::API::V3::Entities::Project
- end
- post ':id/archive' do
- authorize!(:archive_project, user_project)
-
- user_project.archive!
-
- present user_project, with: ::API::V3::Entities::Project
- end
-
- desc 'Unarchive a project' do
- success ::API::V3::Entities::Project
- end
- post ':id/unarchive' do
- authorize!(:archive_project, user_project)
-
- user_project.unarchive!
-
- present user_project, with: ::API::V3::Entities::Project
- end
-
- desc 'Star a project' do
- success ::API::V3::Entities::Project
- end
- post ':id/star' do
- if current_user.starred?(user_project)
- not_modified!
- else
- current_user.toggle_star(user_project)
- user_project.reload
-
- present user_project, with: ::API::V3::Entities::Project
- end
- end
-
- desc 'Unstar a project' do
- success ::API::V3::Entities::Project
- end
- delete ':id/star' do
- if current_user.starred?(user_project)
- current_user.toggle_star(user_project)
- user_project.reload
-
- present user_project, with: ::API::V3::Entities::Project
- else
- not_modified!
- end
- end
-
- desc 'Remove a project'
- delete ":id" do
- authorize! :remove_project, user_project
-
- status(200)
- ::Projects::DestroyService.new(user_project, current_user, {}).async_execute
- end
-
- desc 'Mark this project as forked from another'
- params do
- requires :forked_from_id, type: String, desc: 'The ID of the project it was forked from'
- end
- post ":id/fork/:forked_from_id" do
- authenticated_as_admin!
-
- forked_from_project = find_project!(params[:forked_from_id])
- not_found!("Source Project") unless forked_from_project
-
- if user_project.forked_from_project.nil?
- user_project.create_forked_project_link(forked_to_project_id: user_project.id, forked_from_project_id: forked_from_project.id)
-
- ::Projects::ForksCountService.new(forked_from_project).refresh_cache
- else
- render_api_error!("Project already forked", 409)
- end
- end
-
- desc 'Remove a forked_from relationship'
- delete ":id/fork" do
- authorize! :remove_fork_project, user_project
-
- if user_project.forked?
- status(200)
- user_project.forked_project_link.destroy
- else
- not_modified!
- end
- end
-
- desc 'Share the project with a group' do
- success ::API::Entities::ProjectGroupLink
- end
- params do
- requires :group_id, type: Integer, desc: 'The ID of a group'
- requires :group_access, type: Integer, values: Gitlab::Access.values, desc: 'The group access level'
- optional :expires_at, type: Date, desc: 'Share expiration date'
- end
- post ":id/share" do
- authorize! :admin_project, user_project
- group = Group.find_by_id(params[:group_id])
-
- unless group && can?(current_user, :read_group, group)
- not_found!('Group')
- end
-
- unless user_project.allowed_to_share_with_group?
- break render_api_error!("The project sharing with group is disabled", 400)
- end
-
- link = user_project.project_group_links.new(declared_params(include_missing: false))
-
- if link.save
- present link, with: ::API::Entities::ProjectGroupLink
- else
- render_api_error!(link.errors.full_messages.first, 409)
- end
- end
-
- params do
- requires :group_id, type: Integer, desc: 'The ID of the group'
- end
- delete ":id/share/:group_id" do
- authorize! :admin_project, user_project
-
- link = user_project.project_group_links.find_by(group_id: params[:group_id])
- not_found!('Group Link') unless link
-
- link.destroy
- no_content!
- end
-
- desc 'Upload a file'
- params do
- requires :file, type: File, desc: 'The file to be uploaded'
- end
- post ":id/uploads" do
- UploadService.new(user_project, params[:file]).execute
- end
-
- desc 'Get the users list of a project' do
- success ::API::Entities::UserBasic
- end
- params do
- optional :search, type: String, desc: 'Return list of users matching the search criteria'
- use :pagination
- end
- get ':id/users' do
- users = user_project.team.users
- users = users.search(params[:search]) if params[:search].present?
-
- present paginate(users), with: ::API::Entities::UserBasic
- end
- end
- end
- end
-end
diff --git a/lib/api/v3/repositories.rb b/lib/api/v3/repositories.rb
deleted file mode 100644
index f701d64e886..00000000000
--- a/lib/api/v3/repositories.rb
+++ /dev/null
@@ -1,110 +0,0 @@
-require 'mime/types'
-
-module API
- module V3
- class Repositories < Grape::API
- before { authorize! :download_code, user_project }
-
- params do
- requires :id, type: String, desc: 'The ID of a project'
- end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
- helpers do
- def handle_project_member_errors(errors)
- if errors[:project_access].any?
- error!(errors[:project_access], 422)
- end
-
- not_found!
- end
- end
-
- desc 'Get a project repository tree' do
- success ::API::Entities::TreeObject
- end
- params do
- optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used'
- optional :path, type: String, desc: 'The path of the tree'
- optional :recursive, type: Boolean, default: false, desc: 'Used to get a recursive tree'
- end
- get ':id/repository/tree' do
- ref = params[:ref_name] || user_project.try(:default_branch) || 'master'
- path = params[:path] || nil
-
- commit = user_project.commit(ref)
- not_found!('Tree') unless commit
-
- tree = user_project.repository.tree(commit.id, path, recursive: params[:recursive])
-
- present tree.sorted_entries, with: ::API::Entities::TreeObject
- end
-
- desc 'Get a raw file contents'
- params do
- 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"], requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do
- repo = user_project.repository
- commit = repo.commit(params[:sha])
- not_found! "Commit" unless commit
- blob = Gitlab::Git::Blob.find(repo, commit.id, params[:filepath])
- not_found! "File" unless blob
- send_git_blob repo, blob
- end
-
- desc 'Get a raw blob contents by blob sha'
- params do
- requires :sha, type: String, desc: 'The commit, branch name, or tag name'
- end
- 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])
- rescue
- not_found! 'Blob'
- end
- not_found! 'Blob' unless blob
- send_git_blob repo, blob
- end
-
- desc 'Get an archive of the repository'
- params do
- optional :sha, type: String, desc: 'The commit sha of the archive to be downloaded'
- optional :format, type: String, desc: 'The archive format'
- end
- get ':id/repository/archive', requirements: { format: Gitlab::PathRegex.archive_formats_regex } do
- begin
- send_git_archive user_project.repository, ref: params[:sha], format: params[:format], append_sha: true
- rescue
- not_found!('File')
- end
- end
-
- desc 'Compare two branches, tags, or commits' do
- success ::API::Entities::Compare
- end
- params do
- requires :from, type: String, desc: 'The commit, branch name, or tag name to start comparison'
- requires :to, type: String, desc: 'The commit, branch name, or tag name to stop comparison'
- end
- get ':id/repository/compare' do
- compare = Gitlab::Git::Compare.new(user_project.repository.raw_repository, params[:from], params[:to])
- present compare, with: ::API::Entities::Compare
- end
-
- desc 'Get repository contributors' do
- success ::API::Entities::Contributor
- end
- get ':id/repository/contributors' do
- begin
- present user_project.repository.contributors,
- with: ::API::Entities::Contributor
- rescue
- not_found!
- end
- end
- end
- end
- end
-end
diff --git a/lib/api/v3/runners.rb b/lib/api/v3/runners.rb
deleted file mode 100644
index 8a5c46805bd..00000000000
--- a/lib/api/v3/runners.rb
+++ /dev/null
@@ -1,66 +0,0 @@
-module API
- module V3
- class Runners < Grape::API
- include PaginationParams
-
- before { authenticate! }
-
- resource :runners do
- desc 'Remove a runner' do
- success ::API::Entities::Runner
- end
- params do
- requires :id, type: Integer, desc: 'The ID of the runner'
- end
- delete ':id' do
- runner = Ci::Runner.find(params[:id])
- not_found!('Runner') unless runner
-
- authenticate_delete_runner!(runner)
-
- status(200)
- runner.destroy
- end
- end
-
- params do
- requires :id, type: String, desc: 'The ID of a project'
- end
- resource :projects, requirements: { id: %r{[^/]+} } do
- before { authorize_admin_project }
-
- desc "Disable project's runner" do
- success ::API::Entities::Runner
- end
- params do
- requires :runner_id, type: Integer, desc: 'The ID of the runner'
- end
- delete ':id/runners/:runner_id' do
- runner_project = user_project.runner_projects.find_by(runner_id: params[:runner_id])
- not_found!('Runner') unless runner_project
-
- runner = runner_project.runner
- forbidden!("Only one project associated with the runner. Please remove the runner instead") if runner.projects.count == 1
-
- runner_project.destroy
-
- present runner, with: ::API::Entities::Runner
- end
- end
-
- 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)
- end
-
- def user_can_access_runner?(runner)
- current_user.ci_owned_runners.exists?(runner.id)
- end
- end
- end
- end
-end
diff --git a/lib/api/v3/services.rb b/lib/api/v3/services.rb
deleted file mode 100644
index 20ca1021c71..00000000000
--- a/lib/api/v3/services.rb
+++ /dev/null
@@ -1,670 +0,0 @@
-module API
- module V3
- class Services < Grape::API
- services = {
- 'asana' => [
- {
- required: true,
- name: :api_key,
- type: String,
- desc: 'User API token'
- },
- {
- required: false,
- name: :restrict_to_branch,
- type: String,
- desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches'
- }
- ],
- 'assembla' => [
- {
- required: true,
- name: :token,
- type: String,
- desc: 'The authentication token'
- },
- {
- required: false,
- name: :subdomain,
- type: String,
- desc: 'Subdomain setting'
- }
- ],
- 'bamboo' => [
- {
- required: true,
- name: :bamboo_url,
- type: String,
- desc: 'Bamboo root URL like https://bamboo.example.com'
- },
- {
- required: true,
- name: :build_key,
- type: String,
- desc: 'Bamboo build plan key like'
- },
- {
- required: true,
- name: :username,
- type: String,
- desc: 'A user with API access, if applicable'
- },
- {
- required: true,
- name: :password,
- type: String,
- desc: 'Passord of the user'
- }
- ],
- 'bugzilla' => [
- {
- required: true,
- name: :new_issue_url,
- type: String,
- desc: 'New issue URL'
- },
- {
- required: true,
- name: :issues_url,
- type: String,
- desc: 'Issues URL'
- },
- {
- required: true,
- name: :project_url,
- type: String,
- desc: 'Project URL'
- },
- {
- required: false,
- name: :description,
- type: String,
- desc: 'Description'
- },
- {
- required: false,
- name: :title,
- type: String,
- desc: 'Title'
- }
- ],
- 'buildkite' => [
- {
- required: true,
- name: :token,
- type: String,
- desc: 'Buildkite project GitLab token'
- },
- {
- required: true,
- name: :project_url,
- type: String,
- desc: 'The buildkite project URL'
- },
- {
- required: false,
- name: :enable_ssl_verification,
- type: Boolean,
- desc: 'Enable SSL verification for communication'
- }
- ],
- 'builds-email' => [
- {
- required: true,
- name: :recipients,
- type: String,
- desc: 'Comma-separated list of recipient email addresses'
- },
- {
- required: false,
- name: :add_pusher,
- type: Boolean,
- desc: 'Add pusher to recipients list'
- },
- {
- required: false,
- name: :notify_only_broken_builds,
- type: Boolean,
- desc: 'Notify only broken builds'
- }
- ],
- 'campfire' => [
- {
- required: true,
- name: :token,
- type: String,
- desc: 'Campfire token'
- },
- {
- required: false,
- name: :subdomain,
- type: String,
- desc: 'Campfire subdomain'
- },
- {
- required: false,
- name: :room,
- type: String,
- desc: 'Campfire room'
- }
- ],
- 'custom-issue-tracker' => [
- {
- required: true,
- name: :new_issue_url,
- type: String,
- desc: 'New issue URL'
- },
- {
- required: true,
- name: :issues_url,
- type: String,
- desc: 'Issues URL'
- },
- {
- required: true,
- name: :project_url,
- type: String,
- desc: 'Project URL'
- },
- {
- required: false,
- name: :description,
- type: String,
- desc: 'Description'
- },
- {
- required: false,
- name: :title,
- type: String,
- desc: 'Title'
- }
- ],
- 'drone-ci' => [
- {
- required: true,
- name: :token,
- type: String,
- desc: 'Drone CI token'
- },
- {
- required: true,
- name: :drone_url,
- type: String,
- desc: 'Drone CI URL'
- },
- {
- required: false,
- name: :enable_ssl_verification,
- type: Boolean,
- desc: 'Enable SSL verification for communication'
- }
- ],
- 'emails-on-push' => [
- {
- required: true,
- name: :recipients,
- type: String,
- desc: 'Comma-separated list of recipient email addresses'
- },
- {
- required: false,
- name: :disable_diffs,
- type: Boolean,
- desc: 'Disable code diffs'
- },
- {
- required: false,
- name: :send_from_committer_email,
- type: Boolean,
- desc: 'Send from committer'
- }
- ],
- 'external-wiki' => [
- {
- required: true,
- name: :external_wiki_url,
- type: String,
- desc: 'The URL of the external Wiki'
- }
- ],
- 'flowdock' => [
- {
- required: true,
- name: :token,
- type: String,
- desc: 'Flowdock token'
- }
- ],
- 'gemnasium' => [
- {
- required: true,
- name: :api_key,
- type: String,
- desc: 'Your personal API key on gemnasium.com'
- },
- {
- required: true,
- name: :token,
- type: String,
- desc: "The project's slug on gemnasium.com"
- }
- ],
- 'hipchat' => [
- {
- required: true,
- name: :token,
- type: String,
- desc: 'The room token'
- },
- {
- required: false,
- name: :room,
- type: String,
- desc: 'The room name or ID'
- },
- {
- required: false,
- name: :color,
- type: String,
- desc: 'The room color'
- },
- {
- required: false,
- name: :notify,
- type: Boolean,
- desc: 'Enable notifications'
- },
- {
- required: false,
- name: :api_version,
- type: String,
- desc: 'Leave blank for default (v2)'
- },
- {
- required: false,
- name: :server,
- type: String,
- desc: 'Leave blank for default. https://hipchat.example.com'
- }
- ],
- 'irker' => [
- {
- required: true,
- name: :recipients,
- type: String,
- desc: 'Recipients/channels separated by whitespaces'
- },
- {
- required: false,
- name: :default_irc_uri,
- type: String,
- desc: 'Default: irc://irc.network.net:6697'
- },
- {
- required: false,
- name: :server_host,
- type: String,
- desc: 'Server host. Default localhost'
- },
- {
- required: false,
- name: :server_port,
- type: Integer,
- desc: 'Server port. Default 6659'
- },
- {
- required: false,
- name: :colorize_messages,
- type: Boolean,
- desc: 'Colorize messages'
- }
- ],
- 'jira' => [
- {
- required: true,
- name: :url,
- type: String,
- desc: 'The URL to the JIRA project which is being linked to this GitLab project, e.g., https://jira.example.com'
- },
- {
- required: true,
- name: :project_key,
- type: String,
- desc: 'The short identifier for your JIRA project, all uppercase, e.g., PROJ'
- },
- {
- required: false,
- name: :username,
- type: String,
- desc: 'The username of the user created to be used with GitLab/JIRA'
- },
- {
- required: false,
- name: :password,
- type: String,
- desc: 'The password of the user created to be used with GitLab/JIRA'
- },
- {
- required: false,
- name: :jira_issue_transition_id,
- type: Integer,
- desc: '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`'
- }
- ],
-
- 'kubernetes' => [
- {
- required: true,
- name: :namespace,
- type: String,
- desc: 'The Kubernetes namespace to use'
- },
- {
- required: true,
- name: :api_url,
- type: String,
- desc: 'The URL to the Kubernetes cluster API, e.g., https://kubernetes.example.com'
- },
- {
- required: true,
- name: :token,
- type: String,
- desc: 'The service token to authenticate against the Kubernetes cluster with'
- },
- {
- required: false,
- name: :ca_pem,
- type: String,
- desc: 'A custom certificate authority bundle to verify the Kubernetes cluster with (PEM format)'
- }
- ],
- 'mattermost-slash-commands' => [
- {
- required: true,
- name: :token,
- type: String,
- desc: 'The Mattermost token'
- }
- ],
- 'slack-slash-commands' => [
- {
- required: true,
- name: :token,
- type: String,
- 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,
- name: :recipients,
- type: String,
- desc: 'Comma-separated list of recipient email addresses'
- },
- {
- required: false,
- name: :notify_only_broken_builds,
- type: Boolean,
- desc: 'Notify only broken builds'
- }
- ],
- 'pivotaltracker' => [
- {
- required: true,
- name: :token,
- type: String,
- desc: 'The Pivotaltracker token'
- },
- {
- required: false,
- name: :restrict_to_branch,
- type: String,
- desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.'
- }
- ],
- 'pushover' => [
- {
- required: true,
- name: :api_key,
- type: String,
- desc: 'The application key'
- },
- {
- required: true,
- name: :user_key,
- type: String,
- desc: 'The user key'
- },
- {
- required: true,
- name: :priority,
- type: String,
- desc: 'The priority'
- },
- {
- required: true,
- name: :device,
- type: String,
- desc: 'Leave blank for all active devices'
- },
- {
- required: true,
- name: :sound,
- type: String,
- desc: 'The sound of the notification'
- }
- ],
- 'redmine' => [
- {
- required: true,
- name: :new_issue_url,
- type: String,
- desc: 'The new issue URL'
- },
- {
- required: true,
- name: :project_url,
- type: String,
- desc: 'The project URL'
- },
- {
- required: true,
- name: :issues_url,
- type: String,
- desc: 'The issues URL'
- },
- {
- required: false,
- name: :description,
- type: String,
- desc: 'The description of the tracker'
- }
- ],
- 'slack' => [
- {
- required: true,
- name: :webhook,
- type: String,
- desc: 'The Slack webhook. e.g. https://hooks.slack.com/services/...'
- },
- {
- required: false,
- name: :new_issue_url,
- type: String,
- desc: 'The user name'
- },
- {
- required: false,
- name: :channel,
- type: String,
- desc: 'The channel name'
- }
- ],
- 'microsoft-teams' => [
- required: true,
- name: :webhook,
- type: String,
- desc: 'The Microsoft Teams webhook. e.g. https://outlook.office.com/webhook/…'
- ],
- 'mattermost' => [
- {
- required: true,
- name: :webhook,
- type: String,
- desc: 'The Mattermost webhook. e.g. http://mattermost_host/hooks/...'
- }
- ],
- 'teamcity' => [
- {
- required: true,
- name: :teamcity_url,
- type: String,
- desc: 'TeamCity root URL like https://teamcity.example.com'
- },
- {
- required: true,
- name: :build_type,
- type: String,
- desc: 'Build configuration ID'
- },
- {
- required: true,
- name: :username,
- type: String,
- desc: 'A user with permissions to trigger a manual build'
- },
- {
- required: true,
- name: :password,
- type: String,
- desc: 'The password of the user'
- }
- ]
- }
-
- trigger_services = {
- 'mattermost-slash-commands' => [
- {
- name: :token,
- type: String,
- desc: 'The Mattermost token'
- }
- ],
- 'slack-slash-commands' => [
- {
- name: :token,
- type: String,
- desc: 'The Slack token'
- }
- ]
- }.freeze
-
- params do
- requires :id, type: String, desc: 'The ID of a project'
- end
- resource :projects, requirements: { id: %r{[^/]+} } do
- before { authenticate! }
- before { authorize_admin_project }
-
- helpers do
- def service_attributes(service)
- service.fields.inject([]) do |arr, hash|
- arr << hash[:name].to_sym
- end
- end
- end
-
- desc "Delete a service for project"
- params do
- requires :service_slug, type: String, values: services.keys, desc: 'The name of the service'
- end
- delete ":id/services/:service_slug" do
- service = user_project.find_or_initialize_service(params[:service_slug].underscore)
-
- attrs = service_attributes(service).inject({}) do |hash, key|
- hash.merge!(key => nil)
- end
-
- if service.update_attributes(attrs.merge(active: false))
- status(200)
- true
- else
- render_api_error!('400 Bad Request', 400)
- end
- end
-
- desc 'Get the service settings for project' do
- success Entities::ProjectService
- end
- params do
- requires :service_slug, type: String, values: services.keys, desc: 'The name of the service'
- end
- get ":id/services/:service_slug" do
- service = user_project.find_or_initialize_service(params[:service_slug].underscore)
- present service, with: Entities::ProjectService
- end
- end
-
- trigger_services.each do |service_slug, settings|
- helpers do
- def slash_command_service(project, service_slug, params)
- project.services.active.where(template: false).find do |service|
- service.try(:token) == params[:token] && service.to_param == service_slug.underscore
- end
- end
- end
-
- params do
- requires :id, type: String, desc: 'The ID of a project'
- end
- resource :projects, requirements: { id: %r{[^/]+} } do
- desc "Trigger a slash command for #{service_slug}" do
- detail 'Added in GitLab 8.13'
- end
- params do
- settings.each do |setting|
- requires setting[:name], type: setting[:type], desc: setting[:desc]
- end
- end
- post ":id/services/#{service_slug.underscore}/trigger" do
- project = find_project(params[:id])
-
- # This is not accurate, but done to prevent leakage of the project names
- not_found!('Service') unless project
-
- service = slash_command_service(project, service_slug, params)
- result = service.try(:trigger, params)
-
- if result
- status result[:status] || 200
- present result
- else
- not_found!('Service')
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/api/v3/settings.rb b/lib/api/v3/settings.rb
deleted file mode 100644
index fc56495c8b1..00000000000
--- a/lib/api/v3/settings.rb
+++ /dev/null
@@ -1,147 +0,0 @@
-module API
- module V3
- class Settings < Grape::API
- before { authenticated_as_admin! }
-
- helpers do
- def current_settings
- @current_setting ||=
- (ApplicationSetting.current_without_cache || ApplicationSetting.create_from_defaults)
- end
- end
-
- desc 'Get the current application settings' do
- success Entities::ApplicationSetting
- end
- get "application/settings" do
- present current_settings, with: Entities::ApplicationSetting
- end
-
- desc 'Modify application settings' do
- success Entities::ApplicationSetting
- end
- params do
- optional :default_branch_protection, type: Integer, values: [0, 1, 2], desc: 'Determine if developers can push to master'
- optional :default_project_visibility, type: Integer, values: Gitlab::VisibilityLevel.values, desc: 'The default project visibility'
- optional :default_snippet_visibility, type: Integer, values: Gitlab::VisibilityLevel.values, desc: 'The default snippet visibility'
- optional :default_group_visibility, type: Integer, values: Gitlab::VisibilityLevel.values, desc: 'The default group visibility'
- optional :restricted_visibility_levels, type: Array[String], desc: 'Selected levels cannot be used by non-admin users for projects or snippets. If the public level is restricted, user profiles are only visible to logged in users.'
- optional :import_sources, type: Array[String], values: %w[github bitbucket gitlab google_code fogbugz git gitlab_project],
- desc: 'Enabled sources for code import during project creation. OmniAuth must be configured for GitHub, Bitbucket, and GitLab.com'
- optional :disabled_oauth_sign_in_sources, type: Array[String], desc: 'Disable certain OAuth sign-in sources'
- optional :enabled_git_access_protocol, type: String, values: %w[ssh http nil], desc: 'Allow only the selected protocols to be used for Git access.'
- optional :gravatar_enabled, type: Boolean, desc: 'Flag indicating if the Gravatar service is enabled'
- optional :default_projects_limit, type: Integer, desc: 'The maximum number of personal projects'
- optional :max_attachment_size, type: Integer, desc: 'Maximum attachment size in MB'
- optional :session_expire_delay, type: Integer, desc: 'Session duration in minutes. GitLab restart is required to apply changes.'
- optional :user_oauth_applications, type: Boolean, desc: 'Allow users to register any application to use GitLab as an OAuth provider'
- optional :user_default_external, type: Boolean, desc: 'Newly registered users will by default be external'
- optional :signup_enabled, type: Boolean, desc: 'Flag indicating if sign up is enabled'
- optional :send_user_confirmation_email, type: Boolean, desc: 'Send confirmation email on sign-up'
- optional :domain_whitelist, type: String, desc: 'ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com'
- optional :domain_blacklist_enabled, type: Boolean, desc: 'Enable domain blacklist for sign ups'
- given domain_blacklist_enabled: ->(val) { val } do
- 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 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
- 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'
- end
- optional :home_page_url, type: String, desc: 'We will redirect non-logged in users to this page'
- optional :after_sign_out_path, type: String, desc: 'We will redirect users to this page after they sign out'
- optional :sign_in_text, type: String, desc: 'The sign in text of the GitLab application'
- optional :help_page_text, type: String, desc: 'Custom text displayed on the help page'
- optional :shared_runners_enabled, type: Boolean, desc: 'Enable shared runners for new projects'
- given shared_runners_enabled: ->(val) { val } do
- requires :shared_runners_text, type: String, desc: 'Shared runners text '
- end
- optional :max_artifacts_size, type: Integer, desc: "Set the maximum file size each build's artifacts can have"
- optional :max_pages_size, type: Integer, desc: 'Maximum size of pages in MB'
- optional :container_registry_token_expire_delay, type: Integer, desc: 'Authorization token duration (minutes)'
- optional :metrics_enabled, type: Boolean, desc: 'Enable the InfluxDB metrics'
- given metrics_enabled: ->(val) { val } do
- requires :metrics_host, type: String, desc: 'The InfluxDB host'
- requires :metrics_port, type: Integer, desc: 'The UDP port to use for connecting to InfluxDB'
- requires :metrics_pool_size, type: Integer, desc: 'The amount of InfluxDB connections to open'
- requires :metrics_timeout, type: Integer, desc: 'The amount of seconds after which an InfluxDB connection will time out'
- requires :metrics_method_call_threshold, type: Integer, desc: 'A method call is only tracked when it takes longer to complete than the given amount of milliseconds.'
- requires :metrics_sample_interval, type: Integer, desc: 'The sampling interval in seconds'
- requires :metrics_packet_size, type: Integer, desc: 'The amount of points to store in a single UDP packet'
- end
- optional :sidekiq_throttling_enabled, type: Boolean, desc: 'Enable Sidekiq Job Throttling'
- given sidekiq_throttling_enabled: ->(val) { val } do
- requires :sidekiq_throttling_queus, type: Array[String], desc: 'Choose which queues you wish to throttle'
- requires :sidekiq_throttling_factor, type: Float, desc: 'The factor by which the queues should be throttled. A value between 0.0 and 1.0, exclusive.'
- end
- optional :recaptcha_enabled, type: Boolean, desc: 'Helps prevent bots from creating accounts'
- given recaptcha_enabled: ->(val) { val } do
- requires :recaptcha_site_key, type: String, desc: 'Generate site key at http://www.google.com/recaptcha'
- requires :recaptcha_private_key, type: String, desc: 'Generate private key at http://www.google.com/recaptcha'
- end
- optional :akismet_enabled, type: Boolean, desc: 'Helps prevent bots from creating issues'
- given akismet_enabled: ->(val) { val } do
- requires :akismet_api_key, type: String, desc: 'Generate API key at http://www.akismet.com'
- end
- optional :admin_notification_email, type: String, desc: 'Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area.'
- optional :sentry_enabled, type: Boolean, desc: 'Sentry is an error reporting and logging tool which is currently not shipped with GitLab, get it here: https://getsentry.com'
- given sentry_enabled: ->(val) { val } do
- requires :sentry_dsn, type: String, desc: 'Sentry Data Source Name'
- end
- optional :repository_storage, type: String, desc: 'Storage paths for new projects'
- optional :repository_checks_enabled, type: Boolean, desc: "GitLab will periodically run 'git fsck' in all project and wiki repositories to look for silent disk corruption issues."
- optional :koding_enabled, type: Boolean, desc: 'Enable Koding'
- given koding_enabled: ->(val) { val } do
- requires :koding_url, type: String, desc: 'The Koding team URL'
- end
- optional :plantuml_enabled, type: Boolean, desc: 'Enable PlantUML'
- given plantuml_enabled: ->(val) { val } do
- requires :plantuml_url, type: String, desc: 'The PlantUML server URL'
- end
- optional :version_check_enabled, type: Boolean, desc: 'Let GitLab inform you when an update is available.'
- optional :email_author_in_body, type: Boolean, desc: '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.'
- optional :html_emails_enabled, type: Boolean, desc: 'By default GitLab sends emails in HTML and plain text formats so mail clients can choose what format to use. Disable this option if you only want to send emails in plain text format.'
- optional :housekeeping_enabled, type: Boolean, desc: 'Enable automatic repository housekeeping (git repack, git gc)'
- given housekeeping_enabled: ->(val) { val } do
- requires :housekeeping_bitmaps_enabled, type: Boolean, desc: "Creating pack file bitmaps makes housekeeping take a little longer but bitmaps should accelerate 'git clone' performance."
- requires :housekeeping_incremental_repack_period, type: Integer, desc: "Number of Git pushes after which an incremental 'git repack' is run."
- requires :housekeeping_full_repack_period, type: Integer, desc: "Number of Git pushes after which a full 'git repack' is run."
- requires :housekeeping_gc_period, type: Integer, desc: "Number of Git pushes after which 'git gc' is run."
- end
- optional :terminal_max_session_time, type: Integer, desc: 'Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time.'
- at_least_one_of :default_branch_protection, :default_project_visibility, :default_snippet_visibility,
- :default_group_visibility, :restricted_visibility_levels, :import_sources,
- :enabled_git_access_protocol, :gravatar_enabled, :default_projects_limit,
- :max_attachment_size, :session_expire_delay, :disabled_oauth_sign_in_sources,
- :user_oauth_applications, :user_default_external, :signup_enabled,
- :send_user_confirmation_email, :domain_whitelist, :domain_blacklist_enabled,
- :after_sign_up_text, :password_authentication_enabled, :signin_enabled, :require_two_factor_authentication,
- :home_page_url, :after_sign_out_path, :sign_in_text, :help_page_text,
- :shared_runners_enabled, :max_artifacts_size, :max_pages_size, :container_registry_token_expire_delay,
- :metrics_enabled, :sidekiq_throttling_enabled, :recaptcha_enabled,
- :akismet_enabled, :admin_notification_email, :sentry_enabled,
- :repository_storage, :repository_checks_enabled, :koding_enabled, :plantuml_enabled,
- :version_check_enabled, :email_author_in_body, :html_emails_enabled,
- :housekeeping_enabled, :terminal_max_session_time
- end
- put "application/settings" do
- attrs = declared_params(include_missing: false)
-
- if attrs.has_key?(: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)
- present current_settings, with: Entities::ApplicationSetting
- else
- render_validation_error!(current_settings)
- end
- end
- end
- end
-end
diff --git a/lib/api/v3/snippets.rb b/lib/api/v3/snippets.rb
deleted file mode 100644
index 1df8a20e74a..00000000000
--- a/lib/api/v3/snippets.rb
+++ /dev/null
@@ -1,141 +0,0 @@
-module API
- module V3
- class Snippets < Grape::API
- include PaginationParams
-
- before { authenticate! }
-
- resource :snippets do
- helpers do
- def snippets_for_current_user
- SnippetsFinder.new(current_user, author: current_user).execute
- end
-
- def public_snippets
- SnippetsFinder.new(current_user, visibility: Snippet::PUBLIC).execute
- end
- end
-
- desc 'Get a snippets list for authenticated user' do
- detail 'This feature was introduced in GitLab 8.15.'
- success ::API::Entities::PersonalSnippet
- end
- params do
- use :pagination
- end
- get do
- present paginate(snippets_for_current_user), with: ::API::Entities::PersonalSnippet
- end
-
- desc 'List all public snippets current_user has access to' do
- detail 'This feature was introduced in GitLab 8.15.'
- success ::API::Entities::PersonalSnippet
- end
- params do
- use :pagination
- end
- get 'public' do
- present paginate(public_snippets), with: ::API::Entities::PersonalSnippet
- end
-
- desc 'Get a single snippet' do
- detail 'This feature was introduced in GitLab 8.15.'
- success ::API::Entities::PersonalSnippet
- end
- params do
- requires :id, type: Integer, desc: 'The ID of a snippet'
- end
- get ':id' do
- snippet = snippets_for_current_user.find(params[:id])
- present snippet, with: ::API::Entities::PersonalSnippet
- end
-
- desc 'Create new snippet' do
- detail 'This feature was introduced in GitLab 8.15.'
- success ::API::Entities::PersonalSnippet
- end
- params do
- requires :title, type: String, desc: 'The title of a snippet'
- requires :file_name, type: String, desc: 'The name of a snippet file'
- requires :content, type: String, desc: 'The content of a snippet'
- optional :visibility_level, type: Integer,
- values: Gitlab::VisibilityLevel.values,
- default: Gitlab::VisibilityLevel::INTERNAL,
- desc: 'The visibility level of the snippet'
- end
- post do
- attrs = declared_params(include_missing: false).merge(request: request, api: true)
- snippet = CreateSnippetService.new(nil, current_user, attrs).execute
-
- if snippet.persisted?
- present snippet, with: ::API::Entities::PersonalSnippet
- else
- render_validation_error!(snippet)
- end
- end
-
- desc 'Update an existing snippet' do
- detail 'This feature was introduced in GitLab 8.15.'
- success ::API::Entities::PersonalSnippet
- end
- params do
- requires :id, type: Integer, desc: 'The ID of a snippet'
- optional :title, type: String, desc: 'The title of a snippet'
- optional :file_name, type: String, desc: 'The name of a snippet file'
- optional :content, type: String, desc: 'The content of a snippet'
- optional :visibility_level, type: Integer,
- values: Gitlab::VisibilityLevel.values,
- desc: 'The visibility level of the snippet'
- at_least_one_of :title, :file_name, :content, :visibility_level
- end
- put ':id' do
- snippet = snippets_for_current_user.find_by(id: params.delete(:id))
- break not_found!('Snippet') unless snippet
-
- authorize! :update_personal_snippet, snippet
-
- attrs = declared_params(include_missing: false)
-
- UpdateSnippetService.new(nil, current_user, snippet, attrs).execute
-
- if snippet.persisted?
- present snippet, with: ::API::Entities::PersonalSnippet
- else
- render_validation_error!(snippet)
- end
- end
-
- desc 'Remove snippet' do
- detail 'This feature was introduced in GitLab 8.15.'
- success ::API::Entities::PersonalSnippet
- end
- params do
- requires :id, type: Integer, desc: 'The ID of a snippet'
- end
- delete ':id' do
- snippet = snippets_for_current_user.find_by(id: params.delete(:id))
- break not_found!('Snippet') unless snippet
-
- authorize! :destroy_personal_snippet, snippet
- snippet.destroy
- no_content!
- end
-
- desc 'Get a raw snippet' do
- detail 'This feature was introduced in GitLab 8.15.'
- end
- params do
- requires :id, type: Integer, desc: 'The ID of a snippet'
- end
- get ":id/raw" do
- snippet = snippets_for_current_user.find_by(id: params.delete(:id))
- break not_found!('Snippet') unless snippet
-
- env['api.format'] = :txt
- content_type 'text/plain'
- present snippet.content
- end
- end
- end
- end
-end
diff --git a/lib/api/v3/subscriptions.rb b/lib/api/v3/subscriptions.rb
deleted file mode 100644
index 690768db82f..00000000000
--- a/lib/api/v3/subscriptions.rb
+++ /dev/null
@@ -1,53 +0,0 @@
-module API
- module V3
- class Subscriptions < Grape::API
- before { authenticate! }
-
- subscribable_types = {
- 'merge_request' => proc { |id| find_merge_request_with_access(id, :update_merge_request) },
- 'merge_requests' => proc { |id| find_merge_request_with_access(id, :update_merge_request) },
- 'issues' => proc { |id| find_project_issue(id) },
- 'labels' => proc { |id| find_project_label(id) }
- }
-
- params do
- requires :id, type: String, desc: 'The ID of a project'
- requires :subscribable_id, type: String, desc: 'The ID of a resource'
- end
- resource :projects, requirements: { id: %r{[^/]+} } do
- subscribable_types.each do |type, finder|
- type_singularized = type.singularize
- entity_class = ::API::Entities.const_get(type_singularized.camelcase)
-
- desc 'Subscribe to a resource' do
- success entity_class
- end
- post ":id/#{type}/:subscribable_id/subscription" do
- resource = instance_exec(params[:subscribable_id], &finder)
-
- if resource.subscribed?(current_user, user_project)
- not_modified!
- else
- resource.subscribe(current_user, user_project)
- present resource, with: entity_class, current_user: current_user, project: user_project
- end
- end
-
- desc 'Unsubscribe from a resource' do
- success entity_class
- end
- delete ":id/#{type}/:subscribable_id/subscription" do
- resource = instance_exec(params[:subscribable_id], &finder)
-
- if !resource.subscribed?(current_user, user_project)
- not_modified!
- else
- resource.unsubscribe(current_user, user_project)
- present resource, with: entity_class, current_user: current_user, project: user_project
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/api/v3/system_hooks.rb b/lib/api/v3/system_hooks.rb
deleted file mode 100644
index 5787c06fc12..00000000000
--- a/lib/api/v3/system_hooks.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-module API
- module V3
- class SystemHooks < Grape::API
- before do
- authenticate!
- authenticated_as_admin!
- end
-
- resource :hooks do
- desc 'Get the list of system hooks' do
- success ::API::Entities::Hook
- end
- get do
- present SystemHook.all, with: ::API::Entities::Hook
- end
-
- desc 'Delete a hook' do
- success ::API::Entities::Hook
- end
- params do
- requires :id, type: Integer, desc: 'The ID of the system hook'
- end
- delete ":id" do
- hook = SystemHook.find_by(id: params[:id])
- not_found!('System hook') unless hook
-
- present hook.destroy, with: ::API::Entities::Hook
- end
- end
- end
- end
-end
diff --git a/lib/api/v3/tags.rb b/lib/api/v3/tags.rb
deleted file mode 100644
index 6e37d31d153..00000000000
--- a/lib/api/v3/tags.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-module API
- module V3
- class Tags < Grape::API
- before { authorize! :download_code, user_project }
-
- params do
- requires :id, type: String, desc: 'The ID of a project'
- end
- resource :projects, requirements: { id: %r{[^/]+} } do
- desc 'Get a project repository tags' do
- success ::API::Entities::Tag
- end
- get ":id/repository/tags" do
- tags = user_project.repository.tags.sort_by(&:name).reverse
- present tags, with: ::API::Entities::Tag, project: user_project
- end
-
- desc 'Delete a repository tag'
- params do
- requires :tag_name, type: String, desc: 'The name of the tag'
- end
- delete ":id/repository/tags/:tag_name", requirements: { tag_name: /.+/ } do
- authorize_push_project
-
- result = ::Tags::DestroyService.new(user_project, current_user)
- .execute(params[:tag_name])
-
- if result[:status] == :success
- status(200)
- {
- tag_name: params[:tag_name]
- }
- else
- render_api_error!(result[:message], result[:return_code])
- end
- end
- end
- end
- end
-end
diff --git a/lib/api/v3/templates.rb b/lib/api/v3/templates.rb
deleted file mode 100644
index b82b02b5f49..00000000000
--- a/lib/api/v3/templates.rb
+++ /dev/null
@@ -1,122 +0,0 @@
-module API
- module V3
- class Templates < Grape::API
- GLOBAL_TEMPLATE_TYPES = {
- gitignores: {
- klass: Gitlab::Template::GitignoreTemplate,
- gitlab_version: 8.8
- },
- gitlab_ci_ymls: {
- klass: Gitlab::Template::GitlabCiYmlTemplate,
- gitlab_version: 8.9
- },
- dockerfiles: {
- klass: Gitlab::Template::DockerfileTemplate,
- gitlab_version: 8.15
- }
- }.freeze
- PROJECT_TEMPLATE_REGEX =
- %r{[\<\{\[]
- (project|description|
- one\sline\s.+\swhat\sit\sdoes\.) # matching the start and end is enough here
- [\>\}\]]}xi.freeze
- YEAR_TEMPLATE_REGEX = /[<{\[](year|yyyy)[>}\]]/i.freeze
- FULLNAME_TEMPLATE_REGEX =
- %r{[\<\{\[]
- (fullname|name\sof\s(author|copyright\sowner))
- [\>\}\]]}xi.freeze
- DEPRECATION_MESSAGE = ' This endpoint is deprecated and has been removed in V4.'.freeze
-
- helpers do
- def parsed_license_template
- # We create a fresh Licensee::License object since we'll modify its
- # content in place below.
- template = Licensee::License.new(params[:name])
-
- template.content.gsub!(YEAR_TEMPLATE_REGEX, Time.now.year.to_s)
- template.content.gsub!(PROJECT_TEMPLATE_REGEX, params[:project]) if params[:project].present?
-
- fullname = params[:fullname].presence || current_user.try(:name)
- template.content.gsub!(FULLNAME_TEMPLATE_REGEX, fullname) if fullname
- template
- end
-
- def render_response(template_type, template)
- not_found!(template_type.to_s.singularize) unless template
- present template, with: ::API::Entities::Template
- end
- end
-
- { "licenses" => :deprecated, "templates/licenses" => :ok }.each do |route, status|
- desc 'Get the list of the available license template' do
- detailed_desc = 'This feature was introduced in GitLab 8.7.'
- detailed_desc << DEPRECATION_MESSAGE unless status == :ok
- detail detailed_desc
- success ::API::Entities::License
- end
- params do
- optional :popular, type: Boolean, desc: 'If passed, returns only popular licenses'
- end
- get route do
- options = {
- featured: declared(params)[:popular].present? ? true : nil
- }
- present Licensee::License.all(options), with: ::API::Entities::License
- end
- end
-
- { "licenses/:name" => :deprecated, "templates/licenses/:name" => :ok }.each do |route, status|
- desc 'Get the text for a specific license' do
- detailed_desc = 'This feature was introduced in GitLab 8.7.'
- detailed_desc << DEPRECATION_MESSAGE unless status == :ok
- detail detailed_desc
- success ::API::Entities::License
- end
- params do
- requires :name, type: String, desc: 'The name of the template'
- end
- get route, requirements: { name: /[\w\.-]+/ } do
- not_found!('License') unless Licensee::License.find(declared(params)[:name])
-
- template = parsed_license_template
-
- present template, with: ::API::Entities::License
- end
- end
-
- GLOBAL_TEMPLATE_TYPES.each do |template_type, properties|
- klass = properties[:klass]
- gitlab_version = properties[:gitlab_version]
-
- { template_type => :deprecated, "templates/#{template_type}" => :ok }.each do |route, status|
- desc 'Get the list of the available template' do
- detailed_desc = "This feature was introduced in GitLab #{gitlab_version}."
- detailed_desc << DEPRECATION_MESSAGE unless status == :ok
- detail detailed_desc
- success ::API::Entities::TemplatesList
- end
- get route do
- present klass.all, with: ::API::Entities::TemplatesList
- end
- end
-
- { "#{template_type}/:name" => :deprecated, "templates/#{template_type}/:name" => :ok }.each do |route, status|
- desc 'Get the text for a specific template present in local filesystem' do
- detailed_desc = "This feature was introduced in GitLab #{gitlab_version}."
- detailed_desc << DEPRECATION_MESSAGE unless status == :ok
- detail detailed_desc
- success ::API::Entities::Template
- end
- params do
- requires :name, type: String, desc: 'The name of the template'
- end
- get route do
- new_template = klass.find(declared(params)[:name])
-
- render_response(template_type, new_template)
- end
- end
- end
- end
- end
-end
diff --git a/lib/api/v3/time_tracking_endpoints.rb b/lib/api/v3/time_tracking_endpoints.rb
deleted file mode 100644
index 1aad39815f9..00000000000
--- a/lib/api/v3/time_tracking_endpoints.rb
+++ /dev/null
@@ -1,116 +0,0 @@
-module API
- module V3
- module TimeTrackingEndpoints
- extend ActiveSupport::Concern
-
- included do
- helpers do
- def issuable_name
- declared_params.key?(:issue_id) ? 'issue' : 'merge_request'
- end
-
- def issuable_key
- "#{issuable_name}_id".to_sym
- end
-
- def update_issuable_key
- "update_#{issuable_name}".to_sym
- end
-
- def read_issuable_key
- "read_#{issuable_name}".to_sym
- end
-
- def load_issuable
- @issuable ||= begin
- case issuable_name
- when 'issue'
- find_project_issue(params.delete(issuable_key))
- when 'merge_request'
- find_project_merge_request(params.delete(issuable_key))
- end
- end
- end
-
- def update_issuable(attrs)
- custom_params = declared_params(include_missing: false)
- custom_params.merge!(attrs)
-
- issuable = update_service.new(user_project, current_user, custom_params).execute(load_issuable)
- if issuable.valid?
- present issuable, with: ::API::Entities::IssuableTimeStats
- else
- render_validation_error!(issuable)
- end
- end
-
- def update_service
- issuable_name == 'issue' ? ::Issues::UpdateService : ::MergeRequests::UpdateService
- end
- end
-
- issuable_name = name.end_with?('Issues') ? 'issue' : 'merge_request'
- issuable_collection_name = issuable_name.pluralize
- issuable_key = "#{issuable_name}_id".to_sym
-
- desc "Set a time estimate for a project #{issuable_name}"
- params do
- requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
- requires :duration, type: String, desc: 'The duration to be parsed'
- end
- post ":id/#{issuable_collection_name}/:#{issuable_key}/time_estimate" do
- authorize! update_issuable_key, load_issuable
-
- status :ok
- update_issuable(time_estimate: Gitlab::TimeTrackingFormatter.parse(params.delete(:duration)))
- end
-
- desc "Reset the time estimate for a project #{issuable_name}"
- params do
- requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
- end
- post ":id/#{issuable_collection_name}/:#{issuable_key}/reset_time_estimate" do
- authorize! update_issuable_key, load_issuable
-
- status :ok
- update_issuable(time_estimate: 0)
- end
-
- desc "Add spent time for a project #{issuable_name}"
- params do
- requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
- requires :duration, type: String, desc: 'The duration to be parsed'
- end
- post ":id/#{issuable_collection_name}/:#{issuable_key}/add_spent_time" do
- authorize! update_issuable_key, load_issuable
-
- update_issuable(spend_time: {
- duration: Gitlab::TimeTrackingFormatter.parse(params.delete(:duration)),
- user_id: current_user.id
- })
- end
-
- desc "Reset spent time for a project #{issuable_name}"
- params do
- requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
- end
- post ":id/#{issuable_collection_name}/:#{issuable_key}/reset_spent_time" do
- authorize! update_issuable_key, load_issuable
-
- status :ok
- update_issuable(spend_time: { duration: :reset, user_id: current_user.id })
- end
-
- desc "Show time stats for a project #{issuable_name}"
- params do
- requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
- end
- get ":id/#{issuable_collection_name}/:#{issuable_key}/time_stats" do
- authorize! read_issuable_key, load_issuable
-
- present load_issuable, with: ::API::Entities::IssuableTimeStats
- end
- end
- end
- end
-end
diff --git a/lib/api/v3/todos.rb b/lib/api/v3/todos.rb
deleted file mode 100644
index 3e2c61f6dbd..00000000000
--- a/lib/api/v3/todos.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-module API
- module V3
- class Todos < Grape::API
- before { authenticate! }
-
- resource :todos do
- desc 'Mark a todo as done' do
- success ::API::Entities::Todo
- end
- params do
- requires :id, type: Integer, desc: 'The ID of the todo being marked as done'
- end
- delete ':id' do
- TodoService.new.mark_todos_as_done_by_ids(params[:id], current_user)
- todo = current_user.todos.find(params[:id])
-
- present todo, with: ::API::Entities::Todo, current_user: current_user
- end
-
- desc 'Mark all todos as done'
- delete do
- status(200)
-
- todos = TodosFinder.new(current_user, params).execute
- TodoService.new.mark_todos_as_done(todos, current_user).size
- end
- end
- end
- end
-end
diff --git a/lib/api/v3/triggers.rb b/lib/api/v3/triggers.rb
deleted file mode 100644
index 969bb2a05de..00000000000
--- a/lib/api/v3/triggers.rb
+++ /dev/null
@@ -1,112 +0,0 @@
-module API
- module V3
- class Triggers < Grape::API
- include PaginationParams
-
- params do
- requires :id, type: String, desc: 'The ID of a project'
- end
- resource :projects, requirements: { id: %r{[^/]+} } do
- desc 'Trigger a GitLab project build' do
- success ::API::V3::Entities::TriggerRequest
- end
- params do
- requires :ref, type: String, desc: 'The commit sha or name of a branch or tag'
- requires :token, type: String, desc: 'The unique token of trigger'
- optional :variables, type: Hash, desc: 'The list of variables to be injected into build'
- end
- post ":id/(ref/:ref/)trigger/builds", requirements: { ref: /.+/ } do
- Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42121')
-
- # validate variables
- params[:variables] = params[:variables].to_h
- unless params[:variables].all? { |key, value| key.is_a?(String) && value.is_a?(String) }
- render_api_error!('variables needs to be a map of key-valued strings', 400)
- end
-
- project = find_project(params[:id])
- not_found! unless project
-
- result = Ci::PipelineTriggerService.new(project, nil, params).execute
- not_found! unless result
-
- if result[:http_status]
- render_api_error!(result[:message], result[:http_status])
- else
- pipeline = result[:pipeline]
-
- # We switched to Ci::PipelineVariable from Ci::TriggerRequest.variables.
- # Ci::TriggerRequest doesn't save variables anymore.
- # Here is copying Ci::PipelineVariable to Ci::TriggerRequest.variables for presenting the variables.
- # The same endpoint in v4 API pressents Pipeline instead of TriggerRequest, so it doesn't need such a process.
- trigger_request = pipeline.trigger_requests.last
- trigger_request.variables = params[:variables]
-
- present trigger_request, with: ::API::V3::Entities::TriggerRequest
- end
- end
-
- desc 'Get triggers list' do
- success ::API::V3::Entities::Trigger
- end
- params do
- use :pagination
- end
- get ':id/triggers' do
- authenticate!
- authorize! :admin_build, user_project
-
- triggers = user_project.triggers.includes(:trigger_requests)
-
- present paginate(triggers), with: ::API::V3::Entities::Trigger
- end
-
- desc 'Get specific trigger of a project' do
- success ::API::V3::Entities::Trigger
- end
- params do
- requires :token, type: String, desc: 'The unique token of trigger'
- end
- get ':id/triggers/:token' do
- authenticate!
- authorize! :admin_build, user_project
-
- trigger = user_project.triggers.find_by(token: params[:token].to_s)
- break not_found!('Trigger') unless trigger
-
- present trigger, with: ::API::V3::Entities::Trigger
- end
-
- desc 'Create a trigger' do
- success ::API::V3::Entities::Trigger
- end
- post ':id/triggers' do
- authenticate!
- authorize! :admin_build, user_project
-
- trigger = user_project.triggers.create
-
- present trigger, with: ::API::V3::Entities::Trigger
- end
-
- desc 'Delete a trigger' do
- success ::API::V3::Entities::Trigger
- end
- params do
- requires :token, type: String, desc: 'The unique token of trigger'
- end
- delete ':id/triggers/:token' do
- authenticate!
- authorize! :admin_build, user_project
-
- trigger = user_project.triggers.find_by(token: params[:token].to_s)
- break not_found!('Trigger') unless trigger
-
- trigger.destroy
-
- present trigger, with: ::API::V3::Entities::Trigger
- end
- end
- end
- end
-end
diff --git a/lib/api/v3/users.rb b/lib/api/v3/users.rb
deleted file mode 100644
index cf106f2552d..00000000000
--- a/lib/api/v3/users.rb
+++ /dev/null
@@ -1,204 +0,0 @@
-module API
- module V3
- class Users < Grape::API
- include PaginationParams
- include APIGuard
-
- allow_access_with_scope :read_user, if: -> (request) { request.get? }
-
- before do
- authenticate!
- end
-
- resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do
- helpers do
- params :optional_attributes do
- optional :skype, type: String, desc: 'The Skype username'
- optional :linkedin, type: String, desc: 'The LinkedIn username'
- optional :twitter, type: String, desc: 'The Twitter username'
- optional :website_url, type: String, desc: 'The website of the user'
- optional :organization, type: String, desc: 'The organization of the user'
- optional :projects_limit, type: Integer, desc: 'The number of projects a user can create'
- optional :extern_uid, type: String, desc: 'The external authentication provider UID'
- optional :provider, type: String, desc: 'The external provider'
- optional :bio, type: String, desc: 'The biography of the user'
- 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 :confirm, type: Boolean, default: true, desc: 'Flag indicating the account needs to be confirmed'
- optional :external, type: Boolean, desc: 'Flag indicating the user is an external user'
- all_or_none_of :extern_uid, :provider
- end
- end
-
- desc 'Create a user. Available only for admins.' do
- success ::API::Entities::UserPublic
- end
- params do
- 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'
- 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'
- use :optional_attributes
- end
- post do
- authenticated_as_admin!
-
- params = declared_params(include_missing: false)
- user = ::Users::CreateService.new(current_user, params.merge!(skip_confirmation: !params[:confirm])).execute
-
- if user.persisted?
- present user, with: ::API::Entities::UserPublic
- else
- conflict!('Email has already been taken') if User
- .where(email: user.email)
- .count > 0
-
- conflict!('Username has already been taken') if User
- .where(username: user.username)
- .count > 0
-
- render_validation_error!(user)
- end
- end
-
- desc 'Get the SSH keys of a specified user. Available only for admins.' do
- success ::API::Entities::SSHKey
- end
- params do
- requires :id, type: Integer, desc: 'The ID of the user'
- use :pagination
- end
- get ':id/keys' do
- authenticated_as_admin!
-
- user = User.find_by(id: params[:id])
- not_found!('User') unless user
-
- present paginate(user.keys), with: ::API::Entities::SSHKey
- end
-
- desc 'Get the emails addresses of a specified user. Available only for admins.' do
- success ::API::Entities::Email
- end
- params do
- requires :id, type: Integer, desc: 'The ID of the user'
- use :pagination
- end
- get ':id/emails' do
- authenticated_as_admin!
- user = User.find_by(id: params[:id])
- not_found!('User') unless user
-
- present user.emails, with: ::API::Entities::Email
- end
-
- desc 'Block a user. Available only for admins.'
- params do
- requires :id, type: Integer, desc: 'The ID of the user'
- end
- put ':id/block' do
- authenticated_as_admin!
- user = User.find_by(id: params[:id])
- not_found!('User') unless user
-
- if !user.ldap_blocked?
- user.block
- else
- forbidden!('LDAP blocked users cannot be modified by the API')
- end
- end
-
- desc 'Unblock a user. Available only for admins.'
- params do
- requires :id, type: Integer, desc: 'The ID of the user'
- end
- put ':id/unblock' do
- authenticated_as_admin!
- user = User.find_by(id: params[:id])
- not_found!('User') unless user
-
- if user.ldap_blocked?
- forbidden!('LDAP blocked users cannot be unblocked by the API')
- else
- user.activate
- end
- end
-
- desc 'Get the contribution events of a specified user' do
- detail 'This feature was introduced in GitLab 8.13.'
- success ::API::V3::Entities::Event
- end
- params do
- requires :id, type: Integer, desc: 'The ID of the user'
- use :pagination
- end
- get ':id/events' do
- user = User.find_by(id: params[:id])
- not_found!('User') unless user
-
- events = user.events
- .merge(ProjectsFinder.new(current_user: current_user).execute)
- .references(:project)
- .with_associations
- .recent
-
- present paginate(events), with: ::API::V3::Entities::Event
- end
-
- desc 'Delete an existing SSH key from a specified user. Available only for admins.' do
- success ::API::Entities::SSHKey
- end
- params do
- requires :id, type: Integer, desc: 'The ID of the user'
- requires :key_id, type: Integer, desc: 'The ID of the SSH key'
- end
- delete ':id/keys/:key_id' do
- authenticated_as_admin!
-
- user = User.find_by(id: params[:id])
- not_found!('User') unless user
-
- key = user.keys.find_by(id: params[:key_id])
- not_found!('Key') unless key
-
- present key.destroy, with: ::API::Entities::SSHKey
- end
- end
-
- resource :user do
- desc "Get the currently authenticated user's SSH keys" do
- success ::API::Entities::SSHKey
- end
- params do
- use :pagination
- end
- get "keys" do
- present current_user.keys, with: ::API::Entities::SSHKey
- end
-
- desc "Get the currently authenticated user's email addresses" do
- success ::API::Entities::Email
- end
- get "emails" do
- present current_user.emails, with: ::API::Entities::Email
- end
-
- desc 'Delete an SSH key from the currently authenticated user' do
- success ::API::Entities::SSHKey
- end
- params do
- requires :key_id, type: Integer, desc: 'The ID of the SSH key'
- end
- delete "keys/:key_id" do
- key = current_user.keys.find_by(id: params[:key_id])
- not_found!('Key') unless key
-
- present key.destroy, with: ::API::Entities::SSHKey
- end
- end
- end
- end
-end
diff --git a/lib/api/v3/variables.rb b/lib/api/v3/variables.rb
deleted file mode 100644
index 83972b1e7ce..00000000000
--- a/lib/api/v3/variables.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-module API
- module V3
- class Variables < Grape::API
- include PaginationParams
-
- before { authenticate! }
- before { authorize! :admin_build, user_project }
-
- params do
- requires :id, type: String, desc: 'The ID of a project'
- end
-
- resource :projects, requirements: { id: %r{[^/]+} } do
- desc 'Delete an existing variable from a project' do
- success ::API::Entities::Variable
- end
- params do
- requires :key, type: String, desc: 'The key of the variable'
- end
- delete ':id/variables/:key' do
- variable = user_project.variables.find_by(key: params[:key])
- not_found!('Variable') unless variable
-
- present variable.destroy, with: ::API::Entities::Variable
- end
- end
- end
- end
-end
diff --git a/lib/api/version.rb b/lib/api/version.rb
index 9ba576bd828..3b10bfa6a7d 100644
--- a/lib/api/version.rb
+++ b/lib/api/version.rb
@@ -6,7 +6,7 @@ module API
detail 'This feature was introduced in GitLab 8.13.'
end
get '/version' do
- { version: Gitlab::VERSION, revision: Gitlab::REVISION }
+ { version: Gitlab::VERSION, revision: Gitlab.revision }
end
end
end
diff --git a/lib/backup.rb b/lib/backup.rb
new file mode 100644
index 00000000000..e2c62af23ae
--- /dev/null
+++ b/lib/backup.rb
@@ -0,0 +1,3 @@
+module Backup
+ Error = Class.new(StandardError)
+end
diff --git a/lib/backup/artifacts.rb b/lib/backup/artifacts.rb
index 6a5a223a614..45a935ab352 100644
--- a/lib/backup/artifacts.rb
+++ b/lib/backup/artifacts.rb
@@ -2,7 +2,11 @@ require 'backup/files'
module Backup
class Artifacts < Files
- def initialize
+ attr_reader :progress
+
+ def initialize(progress)
+ @progress = progress
+
super('artifacts', JobArtifactUploader.root)
end
end
diff --git a/lib/backup/builds.rb b/lib/backup/builds.rb
index f869916e199..adf85ca4719 100644
--- a/lib/backup/builds.rb
+++ b/lib/backup/builds.rb
@@ -2,7 +2,11 @@ require 'backup/files'
module Backup
class Builds < Files
- def initialize
+ attr_reader :progress
+
+ def initialize(progress)
+ @progress = progress
+
super('builds', Settings.gitlab_ci.builds_path)
end
end
diff --git a/lib/backup/database.rb b/lib/backup/database.rb
index 5e6828de597..086ca5986bd 100644
--- a/lib/backup/database.rb
+++ b/lib/backup/database.rb
@@ -2,9 +2,11 @@ require 'yaml'
module Backup
class Database
+ attr_reader :progress
attr_reader :config, :db_file_name
- def initialize
+ def initialize(progress)
+ @progress = progress
@config = YAML.load_file(File.join(Rails.root, 'config', 'database.yml'))[Rails.env]
@db_file_name = File.join(Gitlab.config.backup.path, 'db', 'database.sql.gz')
end
@@ -19,12 +21,12 @@ module Backup
dump_pid =
case config["adapter"]
when /^mysql/ then
- $progress.print "Dumping MySQL database #{config['database']} ... "
+ progress.print "Dumping MySQL database #{config['database']} ... "
# Workaround warnings from MySQL 5.6 about passwords on cmd line
ENV['MYSQL_PWD'] = config["password"].to_s if config["password"]
spawn('mysqldump', *mysql_args, config['database'], out: compress_wr)
when "postgresql" then
- $progress.print "Dumping PostgreSQL database #{config['database']} ... "
+ progress.print "Dumping PostgreSQL database #{config['database']} ... "
pg_env
pgsql_args = ["--clean"] # Pass '--clean' to include 'DROP TABLE' statements in the DB dump.
if Gitlab.config.backup.pg_schema
@@ -42,7 +44,7 @@ module Backup
end
report_success(success)
- abort 'Backup failed' unless success
+ raise Backup::Error, 'Backup failed' unless success
end
def restore
@@ -53,12 +55,12 @@ module Backup
restore_pid =
case config["adapter"]
when /^mysql/ then
- $progress.print "Restoring MySQL database #{config['database']} ... "
+ progress.print "Restoring MySQL database #{config['database']} ... "
# Workaround warnings from MySQL 5.6 about passwords on cmd line
ENV['MYSQL_PWD'] = config["password"].to_s if config["password"]
spawn('mysql', *mysql_args, config['database'], in: decompress_rd)
when "postgresql" then
- $progress.print "Restoring PostgreSQL database #{config['database']} ... "
+ progress.print "Restoring PostgreSQL database #{config['database']} ... "
pg_env
spawn('psql', config['database'], in: decompress_rd)
end
@@ -70,7 +72,7 @@ module Backup
end
report_success(success)
- abort 'Restore failed' unless success
+ abort Backup::Error, 'Restore failed' unless success
end
protected
@@ -111,9 +113,9 @@ module Backup
def report_success(success)
if success
- $progress.puts '[DONE]'.color(:green)
+ progress.puts '[DONE]'.color(:green)
else
- $progress.puts '[FAILED]'.color(:red)
+ progress.puts '[FAILED]'.color(:red)
end
end
end
diff --git a/lib/backup/files.rb b/lib/backup/files.rb
index 9895db9e451..e287aa1e392 100644
--- a/lib/backup/files.rb
+++ b/lib/backup/files.rb
@@ -26,20 +26,29 @@ module Backup
unless status.zero?
puts output
- abort 'Backup failed'
+ raise Backup::Error, 'Backup failed'
end
- run_pipeline!([%W(tar --exclude=lost+found -C #{@backup_files_dir} -cf - .), %w(gzip -c -1)], out: [backup_tarball, 'w', 0600])
+ run_pipeline!([%W(#{tar} --exclude=lost+found -C #{@backup_files_dir} -cf - .), %w(gzip -c -1)], out: [backup_tarball, 'w', 0600])
FileUtils.rm_rf(@backup_files_dir)
else
- run_pipeline!([%W(tar --exclude=lost+found -C #{app_files_dir} -cf - .), %w(gzip -c -1)], out: [backup_tarball, 'w', 0600])
+ run_pipeline!([%W(#{tar} --exclude=lost+found -C #{app_files_dir} -cf - .), %w(gzip -c -1)], out: [backup_tarball, 'w', 0600])
end
end
def restore
backup_existing_files_dir
- run_pipeline!([%w(gzip -cd), %W(tar --unlink-first --recursive-unlink -C #{app_files_dir} -xf -)], in: backup_tarball)
+ run_pipeline!([%w(gzip -cd), %W(#{tar} --unlink-first --recursive-unlink -C #{app_files_dir} -xf -)], in: backup_tarball)
+ end
+
+ def tar
+ if system(*%w[gtar --version], out: '/dev/null')
+ # It looks like we can get GNU tar by running 'gtar'
+ 'gtar'
+ else
+ 'tar'
+ end
end
def backup_existing_files_dir
@@ -61,7 +70,7 @@ module Backup
def run_pipeline!(cmd_list, options = {})
status_list = Open3.pipeline(*cmd_list, options)
- abort 'Backup failed' unless status_list.compact.all?(&:success?)
+ raise Backup::Error, 'Backup failed' unless status_list.compact.all?(&:success?)
end
end
end
diff --git a/lib/backup/lfs.rb b/lib/backup/lfs.rb
index 4e234e50a7a..185ff8ae6bd 100644
--- a/lib/backup/lfs.rb
+++ b/lib/backup/lfs.rb
@@ -2,7 +2,11 @@ require 'backup/files'
module Backup
class Lfs < Files
- def initialize
+ attr_reader :progress
+
+ def initialize(progress)
+ @progress = progress
+
super('lfs', Settings.lfs.storage_path)
end
end
diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb
index f27ce4d2b2b..a3641505196 100644
--- a/lib/backup/manager.rb
+++ b/lib/backup/manager.rb
@@ -4,6 +4,12 @@ module Backup
FOLDERS_TO_BACKUP = %w[repositories db].freeze
FILE_NAME_SUFFIX = '_gitlab_backup.tar'.freeze
+ attr_reader :progress
+
+ def initialize(progress)
+ @progress = progress
+ end
+
def pack
# Make sure there is a connection
ActiveRecord::Base.connection.reconnect!
@@ -14,14 +20,14 @@ module Backup
end
# create archive
- $progress.print "Creating backup archive: #{tar_file} ... "
+ progress.print "Creating backup archive: #{tar_file} ... "
# Set file permissions on open to prevent chmod races.
tar_system_options = { out: [tar_file, 'w', Gitlab.config.backup.archive_permissions] }
if Kernel.system('tar', '-cf', '-', *backup_contents, tar_system_options)
- $progress.puts "done".color(:green)
+ progress.puts "done".color(:green)
else
puts "creating archive #{tar_file} failed".color(:red)
- abort 'Backup failed'
+ raise Backup::Error, 'Backup failed'
end
upload
@@ -29,11 +35,11 @@ module Backup
end
def upload
- $progress.print "Uploading backup archive to remote storage #{remote_directory} ... "
+ progress.print "Uploading backup archive to remote storage #{remote_directory} ... "
connection_settings = Gitlab.config.backup.upload.connection
if connection_settings.blank?
- $progress.puts "skipped".color(:yellow)
+ progress.puts "skipped".color(:yellow)
return
end
@@ -43,31 +49,31 @@ module Backup
multipart_chunk_size: Gitlab.config.backup.upload.multipart_chunk_size,
encryption: Gitlab.config.backup.upload.encryption,
storage_class: Gitlab.config.backup.upload.storage_class)
- $progress.puts "done".color(:green)
+ progress.puts "done".color(:green)
else
puts "uploading backup to #{remote_directory} failed".color(:red)
- abort 'Backup failed'
+ raise Backup::Error, 'Backup failed'
end
end
def cleanup
- $progress.print "Deleting tmp directories ... "
+ progress.print "Deleting tmp directories ... "
backup_contents.each do |dir|
next unless File.exist?(File.join(backup_path, dir))
if FileUtils.rm_rf(File.join(backup_path, dir))
- $progress.puts "done".color(:green)
+ progress.puts "done".color(:green)
else
puts "deleting tmp directory '#{dir}' failed".color(:red)
- abort 'Backup failed'
+ raise Backup::Error, 'Backup failed'
end
end
end
def remove_old
# delete backups
- $progress.print "Deleting old backups ... "
+ progress.print "Deleting old backups ... "
keep_time = Gitlab.config.backup.keep_time.to_i
if keep_time > 0
@@ -88,31 +94,32 @@ module Backup
FileUtils.rm(file)
removed += 1
rescue => e
- $progress.puts "Deleting #{file} failed: #{e.message}".color(:red)
+ progress.puts "Deleting #{file} failed: #{e.message}".color(:red)
end
end
end
end
- $progress.puts "done. (#{removed} removed)".color(:green)
+ progress.puts "done. (#{removed} removed)".color(:green)
else
- $progress.puts "skipping".color(:yellow)
+ progress.puts "skipping".color(:yellow)
end
end
+ # rubocop: disable Metrics/AbcSize
def unpack
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}"
+ 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:'
+ progress.puts 'Found more than one backup:'
# print list of available backups
- $progress.puts " " + available_timestamps.join("\n ")
- $progress.puts 'Please specify which one you want to restore:'
- $progress.puts 'rake gitlab:backup:restore BACKUP=timestamp_of_backup'
+ progress.puts " " + available_timestamps.join("\n ")
+ progress.puts 'Please specify which one you want to restore:'
+ progress.puts 'rake gitlab:backup:restore BACKUP=timestamp_of_backup'
exit 1
end
@@ -123,31 +130,31 @@ module Backup
end
unless File.exist?(tar_file)
- $progress.puts "The backup file #{tar_file} does not exist!"
+ 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)
+ progress.puts 'unpacking backup failed'.color(:red)
exit 1
else
- $progress.puts 'done'.color(:green)
+ 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(<<~HEREDOC.color(:red))
+ 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]}"
+ progress.puts
+ progress.puts "Hint: git checkout v#{settings[:gitlab_version]}"
exit 1
end
end
diff --git a/lib/backup/pages.rb b/lib/backup/pages.rb
index 5830b209d6e..542e35a7c7c 100644
--- a/lib/backup/pages.rb
+++ b/lib/backup/pages.rb
@@ -2,7 +2,11 @@ require 'backup/files'
module Backup
class Pages < Files
- def initialize
+ attr_reader :progress
+
+ def initialize(progress)
+ @progress = progress
+
super('pages', Gitlab.config.pages.path)
end
end
diff --git a/lib/backup/registry.rb b/lib/backup/registry.rb
index 91698669402..35821805797 100644
--- a/lib/backup/registry.rb
+++ b/lib/backup/registry.rb
@@ -2,7 +2,11 @@ require 'backup/files'
module Backup
class Registry < Files
- def initialize
+ attr_reader :progress
+
+ def initialize(progress)
+ @progress = progress
+
super('registry', Settings.registry.path)
end
end
diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb
index 65e06fd78c0..af762db517c 100644
--- a/lib/backup/repository.rb
+++ b/lib/backup/repository.rb
@@ -4,155 +4,218 @@ require_relative 'helper'
module Backup
class Repository
include Backup::Helper
- # rubocop:disable Metrics/AbcSize
+
+ attr_reader :progress
+
+ def initialize(progress)
+ @progress = progress
+ end
def dump
prepare
Project.find_each(batch_size: 1000) do |project|
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 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)
+ if !empty_repo?(project)
+ backup_project(project)
+ progress.puts "[DONE]".color(:green)
+ else
progress.puts "[SKIPPED]".color(:cyan)
+ end
+
+ wiki = ProjectWiki.new(project)
+
+ if !empty_repo?(wiki)
+ backup_project(wiki)
+ progress.puts "[DONE] Wiki".color(:green)
else
- in_path(path_to_project_repo) do |dir|
- FileUtils.mkdir_p(path_to_tars(project))
- cmd = %W(tar -cf #{path_to_tars(project, dir)} -C #{path_to_project_repo} #{dir})
- output, status = Gitlab::Popen.popen(cmd)
-
- unless status.zero?
- progress_warn(project, cmd.join(' '), output)
- end
- end
+ progress.puts "[SKIPPED] Wiki".color(:cyan)
+ end
+ end
+ end
- cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path_to_project_repo} bundle create #{path_to_project_bundle} --all)
- output, status = Gitlab::Popen.popen(cmd)
+ def prepare_directories
+ Gitlab.config.repositories.storages.each do |name, repository_storage|
+ delete_all_repositories(name, repository_storage)
+ end
+ end
- if status.zero?
- progress.puts "[DONE]".color(:green)
- else
- progress_warn(project, cmd.join(' '), output)
- end
+ def backup_project(project)
+ gitaly_migrate(:repository_backup, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
+ if is_enabled
+ backup_project_gitaly(project)
+ else
+ backup_project_local(project)
end
+ end
- wiki = ProjectWiki.new(project)
- path_to_wiki_repo = path_to_repo(wiki)
- path_to_wiki_bundle = path_to_bundle(wiki)
+ backup_custom_hooks(project)
+ rescue => e
+ progress_warn(project, e, 'Failed to backup repo')
+ end
- if File.exist?(path_to_wiki_repo)
- progress.print " * #{display_repo_path(wiki)} ... "
+ def backup_project_gitaly(project)
+ path_to_project_bundle = path_to_bundle(project)
+ Gitlab::GitalyClient::RepositoryService.new(project.repository)
+ .create_bundle(path_to_project_bundle)
+ end
- if empty_repo?(wiki)
- progress.puts " [SKIPPED]".color(:cyan)
- else
- cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path_to_wiki_repo} bundle create #{path_to_wiki_bundle} --all)
- output, status = Gitlab::Popen.popen(cmd)
- if status.zero?
- progress.puts " [DONE]".color(:green)
- else
- progress_warn(wiki, cmd.join(' '), output)
- end
- end
+ def backup_project_local(project)
+ path_to_project_repo = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ path_to_repo(project)
+ end
+
+ path_to_project_bundle = path_to_bundle(project)
+
+ cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path_to_project_repo} bundle create #{path_to_project_bundle} --all)
+ output, status = Gitlab::Popen.popen(cmd)
+ progress_warn(project, cmd.join(' '), output) unless status.zero?
+ end
+
+ def delete_all_repositories(name, repository_storage)
+ gitaly_migrate(:delete_all_repositories, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
+ if is_enabled
+ Gitlab::GitalyClient::StorageService.new(name).delete_all_repositories
+ else
+ local_delete_all_repositories(name, repository_storage)
end
end
end
- def prepare_directories
- Gitlab.config.repositories.storages.each do |name, repository_storage|
- path = repository_storage.legacy_disk_path
- next unless File.exist?(path)
-
- # Move all files in the existing repos directory except . and .. to
- # repositories.old.<timestamp> directory
- bk_repos_path = File.join(Gitlab.config.backup.path, "tmp", "#{name}-repositories.old." + Time.now.to_i.to_s)
- FileUtils.mkdir_p(bk_repos_path, mode: 0700)
- files = Dir.glob(File.join(path, "*"), File::FNM_DOTMATCH) - [File.join(path, "."), File.join(path, "..")]
-
- begin
- FileUtils.mv(files, bk_repos_path)
- rescue Errno::EACCES
- access_denied_error(path)
- rescue Errno::EBUSY
- resource_busy_error(path)
+ def local_delete_all_repositories(name, repository_storage)
+ path = repository_storage.legacy_disk_path
+ return unless File.exist?(path)
+
+ bk_repos_path = File.join(Gitlab.config.backup.path, "tmp", "#{name}-repositories.old." + Time.now.to_i.to_s)
+ FileUtils.mkdir_p(bk_repos_path, mode: 0700)
+ files = Dir.glob(File.join(path, "*"), File::FNM_DOTMATCH) - [File.join(path, "."), File.join(path, "..")]
+
+ begin
+ FileUtils.mv(files, bk_repos_path)
+ rescue Errno::EACCES
+ access_denied_error(path)
+ rescue Errno::EBUSY
+ resource_busy_error(path)
+ end
+ end
+
+ def local_restore_custom_hooks(project, dir)
+ path_to_project_repo = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ path_to_repo(project)
+ end
+ cmd = %W(tar -xf #{path_to_tars(project, dir)} -C #{path_to_project_repo} #{dir})
+ output, status = Gitlab::Popen.popen(cmd)
+ unless status.zero?
+ progress_warn(project, cmd.join(' '), output)
+ end
+ end
+
+ def gitaly_restore_custom_hooks(project, dir)
+ custom_hooks_path = path_to_tars(project, dir)
+ Gitlab::GitalyClient::RepositoryService.new(project.repository)
+ .restore_custom_hooks(custom_hooks_path)
+ end
+
+ def local_backup_custom_hooks(project)
+ in_path(path_to_tars(project)) do |dir|
+ path_to_project_repo = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ path_to_repo(project)
+ end
+ break unless File.exist?(File.join(path_to_project_repo, dir))
+
+ FileUtils.mkdir_p(path_to_tars(project))
+ cmd = %W(tar -cf #{path_to_tars(project, dir)} -c #{path_to_project_repo} #{dir})
+ output, status = Gitlab::Popen.popen(cmd)
+
+ unless status.zero?
+ progress_warn(project, cmd.join(' '), output)
+ end
+ end
+ end
+
+ def gitaly_backup_custom_hooks(project)
+ FileUtils.mkdir_p(path_to_tars(project))
+ custom_hooks_path = path_to_tars(project, 'custom_hooks')
+ Gitlab::GitalyClient::RepositoryService.new(project.repository)
+ .backup_custom_hooks(custom_hooks_path)
+ end
+
+ def backup_custom_hooks(project)
+ gitaly_migrate(:backup_custom_hooks, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
+ if is_enabled
+ gitaly_backup_custom_hooks(project)
+ else
+ local_backup_custom_hooks(project)
+ end
+ end
+ end
+
+ def restore_custom_hooks(project)
+ in_path(path_to_tars(project)) do |dir|
+ gitaly_migrate(:restore_custom_hooks, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
+ if is_enabled
+ gitaly_restore_custom_hooks(project, dir)
+ else
+ local_restore_custom_hooks(project, dir)
+ end
end
end
end
def restore
prepare_directories
+ gitlab_shell = Gitlab::Shell.new
+
Project.find_each(batch_size: 1000) do |project|
- progress.print " * #{display_repo_path(project)} ... "
- path_to_project_repo = path_to_repo(project)
+ progress.print " * #{project.full_path} ... "
path_to_project_bundle = path_to_bundle(project)
-
project.ensure_storage_path_exists
- cmd = if File.exist?(path_to_project_bundle)
- %W(#{Gitlab.config.git.bin_path} clone --bare --mirror #{path_to_project_bundle} #{path_to_project_repo})
- else
- %W(#{Gitlab.config.git.bin_path} init --bare #{path_to_project_repo})
- end
+ restore_repo_success = nil
+ if File.exist?(path_to_project_bundle)
+ begin
+ project.repository.create_from_bundle path_to_project_bundle
+ restore_repo_success = true
+ rescue => e
+ restore_repo_success = false
+ progress.puts "Error: #{e}".color(:red)
+ end
+ else
+ restore_repo_success = gitlab_shell.create_repository(project.repository_storage, project.disk_path)
+ end
- output, status = Gitlab::Popen.popen(cmd)
- if status.zero?
+ if restore_repo_success
progress.puts "[DONE]".color(:green)
else
- progress_warn(project, cmd.join(' '), output)
+ progress.puts "[Failed] restoring #{project.full_path} repository".color(:red)
end
- in_path(path_to_tars(project)) do |dir|
- cmd = %W(tar -xf #{path_to_tars(project, dir)} -C #{path_to_project_repo} #{dir})
-
- output, status = Gitlab::Popen.popen(cmd)
- unless status.zero?
- progress_warn(project, cmd.join(' '), output)
- end
- end
+ restore_custom_hooks(project)
wiki = ProjectWiki.new(project)
- path_to_wiki_repo = path_to_repo(wiki)
path_to_wiki_bundle = path_to_bundle(wiki)
if File.exist?(path_to_wiki_bundle)
- progress.print " * #{display_repo_path(wiki)} ... "
-
- # If a wiki bundle exists, first remove the empty repo
- # that was initialized with ProjectWiki.new() and then
- # try to restore with 'git clone --bare'.
- FileUtils.rm_rf(path_to_wiki_repo)
- cmd = %W(#{Gitlab.config.git.bin_path} clone --bare #{path_to_wiki_bundle} #{path_to_wiki_repo})
+ progress.print " * #{wiki.full_path} ... "
+ begin
+ wiki.repository.create_from_bundle(path_to_wiki_bundle)
+ restore_custom_hooks(wiki)
- output, status = Gitlab::Popen.popen(cmd)
- if status.zero?
- progress.puts " [DONE]".color(:green)
- else
- progress_warn(project, cmd.join(' '), output)
+ progress.puts "[DONE]".color(:green)
+ rescue => e
+ progress.puts "[Failed] restoring #{wiki.full_path} wiki".color(:red)
+ progress.puts "Error #{e}".color(:red)
end
end
end
-
- progress.print 'Put GitLab hooks in repositories dirs'.color(:yellow)
- cmd = %W(#{Gitlab.config.gitlab_shell.path}/bin/create-hooks) + repository_storage_paths_args
-
- output, status = Gitlab::Popen.popen(cmd)
- if status.zero?
- progress.puts " [DONE]".color(:green)
- else
- puts " [FAILED]".color(:red)
- puts "failed: #{cmd}"
- puts output
- end
end
- # rubocop:enable Metrics/AbcSize
protected
@@ -190,9 +253,7 @@ module Backup
def prepare
FileUtils.rm_rf(backup_repos_path)
- # Ensure the parent dir of backup_repos_path exists
FileUtils.mkdir_p(Gitlab.config.backup.path)
- # Fail if somebody raced to create backup_repos_path before us
FileUtils.mkdir(backup_repos_path, mode: 0700)
end
@@ -208,7 +269,6 @@ module Backup
end
def empty_repo?(project_or_wiki)
- # Protect against stale caches
project_or_wiki.repository.expire_emptiness_caches
project_or_wiki.repository.empty?
end
@@ -217,12 +277,14 @@ module Backup
Gitlab.config.repositories.storages.values.map { |rs| rs.legacy_disk_path }
end
- def progress
- $progress
- end
-
def display_repo_path(project)
project.hashed_storage?(:repository) ? "#{project.full_path} (#{project.disk_path})" : project.full_path
end
+
+ def gitaly_migrate(method, status: Gitlab::GitalyClient::MigrationStatus::OPT_IN, &block)
+ Gitlab::GitalyClient.migrate(method, status: status, &block)
+ rescue GRPC::NotFound, GRPC::BadStatus => e
+ raise Error, e
+ end
end
end
diff --git a/lib/backup/uploads.rb b/lib/backup/uploads.rb
index d46e2cd869d..49b117a7ee3 100644
--- a/lib/backup/uploads.rb
+++ b/lib/backup/uploads.rb
@@ -2,7 +2,11 @@ require 'backup/files'
module Backup
class Uploads < Files
- def initialize
+ attr_reader :progress
+
+ def initialize(progress)
+ @progress = progress
+
super('uploads', Rails.root.join('public/uploads'))
end
end
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index 60a12dca9d3..b39b11009b3 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -100,6 +100,11 @@ module Banzai
ref_pattern = object_class.reference_pattern
link_pattern = object_class.link_reference_pattern
+ # Compile often used regexps only once outside of the loop
+ ref_pattern_anchor = /\A#{ref_pattern}\z/
+ link_pattern_start = /\A#{link_pattern}/
+ link_pattern_anchor = /\A#{link_pattern}\z/
+
nodes.each do |node|
if text_node?(node) && ref_pattern
replace_text_when_pattern_matches(node, ref_pattern) do |content|
@@ -108,7 +113,7 @@ module Banzai
elsif element_node?(node)
yield_valid_link(node) do |link, inner_html|
- if ref_pattern && link =~ /\A#{ref_pattern}\z/
+ if ref_pattern && link =~ ref_pattern_anchor
replace_link_node_with_href(node, link) do
object_link_filter(link, ref_pattern, link_content: inner_html)
end
@@ -118,7 +123,7 @@ module Banzai
next unless link_pattern
- if link == inner_html && inner_html =~ /\A#{link_pattern}/
+ if link == inner_html && inner_html =~ link_pattern_start
replace_link_node_with_text(node, link) do
object_link_filter(inner_html, link_pattern, link_reference: true)
end
@@ -126,7 +131,7 @@ module Banzai
next
end
- if link =~ /\A#{link_pattern}\z/
+ if link =~ link_pattern_anchor
replace_link_node_with_href(node, link) do
object_link_filter(link, link_pattern, link_content: inner_html, link_reference: true)
end
diff --git a/lib/banzai/filter/blockquote_fence_filter.rb b/lib/banzai/filter/blockquote_fence_filter.rb
index d2c4b1e4d76..fbfcd72c916 100644
--- a/lib/banzai/filter/blockquote_fence_filter.rb
+++ b/lib/banzai/filter/blockquote_fence_filter.rb
@@ -10,7 +10,7 @@ module Banzai
^```
.+?
- \n```$
+ \n```\ *$
)
|
(?<html>
@@ -19,9 +19,9 @@ module Banzai
# Anything, including `>>>` blocks which are ignored by this filter
# </tag>
- ^<[^>]+?>\n
+ ^<[^>]+?>\ *\n
.+?
- \n<\/[^>]+?>$
+ \n<\/[^>]+?>\ *$
)
|
(?:
@@ -30,14 +30,14 @@ module Banzai
# Anything, including code and HTML blocks
# >>>
- ^>>>\n
+ ^>>>\ *\n
(?<quote>
(?:
# Any character that doesn't introduce a code or HTML block
(?!
^```
|
- ^<[^>]+?>\n
+ ^<[^>]+?>\ *\n
)
.
|
@@ -48,7 +48,7 @@ module Banzai
\g<html>
)+?
)
- \n>>>$
+ \n>>>\ *$
)
}mx.freeze
diff --git a/lib/banzai/filter/emoji_filter.rb b/lib/banzai/filter/emoji_filter.rb
index e1261e7bbbe..4eccd9d5ed5 100644
--- a/lib/banzai/filter/emoji_filter.rb
+++ b/lib/banzai/filter/emoji_filter.rb
@@ -3,10 +3,6 @@ module Banzai
# HTML filter that replaces :emoji: and unicode with images.
#
# Based on HTML::Pipeline::EmojiFilter
- #
- # Context options:
- # :asset_root
- # :asset_host
class EmojiFilter < HTML::Pipeline::Filter
IGNORED_ANCESTOR_TAGS = %w(pre code tt).to_set
diff --git a/lib/banzai/filter/gollum_tags_filter.rb b/lib/banzai/filter/gollum_tags_filter.rb
index 4bc82ecb4d6..bb9f488cd87 100644
--- a/lib/banzai/filter/gollum_tags_filter.rb
+++ b/lib/banzai/filter/gollum_tags_filter.rb
@@ -56,10 +56,12 @@ module Banzai
# Pattern to match allowed image extensions
ALLOWED_IMAGE_EXTENSIONS = /.+(jpg|png|gif|svg|bmp)\z/i.freeze
+ # Do not perform linking inside these tags.
+ IGNORED_ANCESTOR_TAGS = %w(pre code tt).to_set
+
def call
doc.search(".//text()").each do |node|
- # Do not perform linking inside <code> blocks
- next unless node.ancestors('code').empty?
+ next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS)
# A Gollum ToC tag is `[[_TOC_]]`, but due to MarkdownFilter running
# before this one, it will be converted into `[[<em>TOC</em>]]`, so it
diff --git a/lib/banzai/filter/markdown_engines/common_mark.rb b/lib/banzai/filter/markdown_engines/common_mark.rb
index bc9597df894..dbb25280849 100644
--- a/lib/banzai/filter/markdown_engines/common_mark.rb
+++ b/lib/banzai/filter/markdown_engines/common_mark.rb
@@ -18,7 +18,7 @@ module Banzai
PARSE_OPTIONS = [
:FOOTNOTES, # parse footnotes.
:STRIKETHROUGH_DOUBLE_TILDE, # parse strikethroughs by double tildes (as redcarpet does).
- :VALIDATE_UTF8 # replace illegal sequences with the replacement character U+FFFD.
+ :VALIDATE_UTF8 # replace illegal sequences with the replacement character U+FFFD.
].freeze
# The `:GITHUB_PRE_LANG` option is not used intentionally because
diff --git a/lib/banzai/filter/markdown_filter.rb b/lib/banzai/filter/markdown_filter.rb
index c1e2b680240..944363f17d3 100644
--- a/lib/banzai/filter/markdown_filter.rb
+++ b/lib/banzai/filter/markdown_filter.rb
@@ -14,7 +14,7 @@ module Banzai
private
- DEFAULT_ENGINE = :redcarpet
+ DEFAULT_ENGINE = :common_mark
def engine(engine_from_context)
engine_from_context ||= DEFAULT_ENGINE
diff --git a/lib/banzai/filter/merge_request_reference_filter.rb b/lib/banzai/filter/merge_request_reference_filter.rb
index 5cbdb01c130..10c40568006 100644
--- a/lib/banzai/filter/merge_request_reference_filter.rb
+++ b/lib/banzai/filter/merge_request_reference_filter.rb
@@ -25,7 +25,10 @@ module Banzai
extras = super
if commit_ref = object_link_commit_ref(object, matches)
- return extras.unshift(commit_ref)
+ klass = reference_class(:commit, tooltip: false)
+ commit_ref_tag = %(<span class="#{klass}">#{commit_ref}</span>)
+
+ return extras.unshift(commit_ref_tag)
end
path = matches[:path] if matches.names.include?("path")
diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb
index b144bd8cf54..af8448937b3 100644
--- a/lib/banzai/filter/milestone_reference_filter.rb
+++ b/lib/banzai/filter/milestone_reference_filter.rb
@@ -65,7 +65,7 @@ module Banzai
# We don't support IID lookups for group milestones, because IIDs can
# clash between group and project milestones.
if project.group && !params[:iid]
- finder_params[:group_ids] = [project.group.id]
+ finder_params[:group_ids] = project.group.self_and_ancestors_ids
end
MilestonesFinder.new(finder_params).find_by(params)
diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb
index 2f023f4f242..2411dd2cdfc 100644
--- a/lib/banzai/filter/reference_filter.rb
+++ b/lib/banzai/filter/reference_filter.rb
@@ -65,8 +65,12 @@ module Banzai
context[:skip_project_check]
end
- def reference_class(type)
- "gfm gfm-#{type} has-tooltip"
+ def reference_class(type, tooltip: true)
+ gfm_klass = "gfm gfm-#{type}"
+
+ return gfm_klass unless tooltip
+
+ "#{gfm_klass} has-tooltip"
end
# Ensure that a :project key exists in context
diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb
index 6786b9d07b6..8275bb9e149 100644
--- a/lib/banzai/filter/sanitization_filter.rb
+++ b/lib/banzai/filter/sanitization_filter.rb
@@ -4,31 +4,25 @@ module Banzai
#
# Extends HTML::Pipeline::SanitizationFilter with a custom whitelist.
class SanitizationFilter < HTML::Pipeline::SanitizationFilter
+ include Gitlab::Utils::StrongMemoize
+
UNSAFE_PROTOCOLS = %w(data javascript vbscript).freeze
TABLE_ALIGNMENT_PATTERN = /text-align: (?<alignment>center|left|right)/
def whitelist
- whitelist = super
-
- customize_whitelist(whitelist)
-
- whitelist
+ strong_memoize(:whitelist) do
+ customize_whitelist(super.dup)
+ end
end
private
- def customized?(transformers)
- transformers.last.source_location[0] == __FILE__
- end
-
def customize_whitelist(whitelist)
- # Only push these customizations once
- return if customized?(whitelist[:transformers])
-
- # Allow table alignment; we whitelist specific style properties in a
+ # Allow table alignment; we whitelist specific text-align values in a
# transformer below
whitelist[:attributes]['th'] = %w(style)
whitelist[:attributes]['td'] = %w(style)
+ whitelist[:css] = { properties: ['text-align'] }
# Allow span elements
whitelist[:elements].push('span')
diff --git a/lib/banzai/filter/table_of_contents_filter.rb b/lib/banzai/filter/table_of_contents_filter.rb
index 97244159985..b32660a8341 100644
--- a/lib/banzai/filter/table_of_contents_filter.rb
+++ b/lib/banzai/filter/table_of_contents_filter.rb
@@ -92,7 +92,7 @@ module Banzai
def text
return '' unless node
- @text ||= node.text
+ @text ||= EscapeUtils.escape_html(node.text)
end
private
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index a1f24e8b093..0d9b874ef85 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -44,11 +44,7 @@ module Banzai
def self.transform_context(context)
context[:only_path] = true unless context.key?(:only_path)
- context.merge(
- # EmojiFilter
- asset_host: Gitlab::Application.config.asset_host,
- asset_root: Gitlab.config.gitlab.base_url
- )
+ context
end
end
end
diff --git a/lib/banzai/reference_parser/issue_parser.rb b/lib/banzai/reference_parser/issue_parser.rb
index 6bee5ea15b9..7b5915899cf 100644
--- a/lib/banzai/reference_parser/issue_parser.rb
+++ b/lib/banzai/reference_parser/issue_parser.rb
@@ -69,7 +69,8 @@ module Banzai
{ group: [:owners, :group_members] },
:invited_groups,
:project_members,
- :project_feature
+ :project_feature,
+ :route
]
}
),
diff --git a/lib/bitbucket/representation/issue.rb b/lib/bitbucket/representation/issue.rb
index 054064395c3..44bcbc250b3 100644
--- a/lib/bitbucket/representation/issue.rb
+++ b/lib/bitbucket/representation/issue.rb
@@ -12,7 +12,7 @@ module Bitbucket
end
def author
- raw.fetch('reporter', {}).fetch('username', nil)
+ raw.dig('reporter', 'username')
end
def description
diff --git a/lib/constraints/feature_constrainer.rb b/lib/constraints/feature_constrainer.rb
new file mode 100644
index 00000000000..05d48b0f25a
--- /dev/null
+++ b/lib/constraints/feature_constrainer.rb
@@ -0,0 +1,13 @@
+module Constraints
+ class FeatureConstrainer
+ attr_reader :feature
+
+ def initialize(feature)
+ @feature = feature
+ end
+
+ def matches?(_request)
+ Feature.enabled?(feature)
+ end
+ end
+end
diff --git a/lib/declarative_policy.rb b/lib/declarative_policy.rb
index 1dd2855063d..dda6cd38dcd 100644
--- a/lib/declarative_policy.rb
+++ b/lib/declarative_policy.rb
@@ -21,7 +21,17 @@ module DeclarativePolicy
cache = opts[:cache] || {}
key = Cache.policy_key(user, subject)
- cache[key] ||= class_for(subject).new(user, subject, opts)
+ cache[key] ||=
+ if Gitlab.rails5?
+ # to avoid deadlocks in multi-threaded environment when
+ # autoloading is enabled, we allow concurrent loads,
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/48263
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
+ class_for(subject).new(user, subject, opts)
+ end
+ else
+ class_for(subject).new(user, subject, opts)
+ end
end
def class_for(subject)
diff --git a/lib/declarative_policy/base.rb b/lib/declarative_policy/base.rb
index 47542194497..da3fabba39b 100644
--- a/lib/declarative_policy/base.rb
+++ b/lib/declarative_policy/base.rb
@@ -119,8 +119,8 @@ module DeclarativePolicy
# a PolicyDsl which is used for registering the rule with
# this class. PolicyDsl will call back into Base.enable_when,
# Base.prevent_when, and Base.prevent_all_when.
- def rule(&b)
- rule = RuleDsl.new(self).instance_eval(&b)
+ def rule(&block)
+ rule = RuleDsl.new(self).instance_eval(&block)
PolicyDsl.new(self, rule)
end
@@ -222,8 +222,8 @@ module DeclarativePolicy
# computes the given ability and prints a helpful debugging output
# showing which
- def debug(ability, *a)
- runner(ability).debug(*a)
+ def debug(ability, *args)
+ runner(ability).debug(*args)
end
desc "Unknown user"
@@ -274,7 +274,7 @@ module DeclarativePolicy
#
# NOTE we can't use ||= here because the value might be the
# boolean `false`
- def cache(key, &b)
+ def cache(key)
return @cache[key] if cached?(key)
@cache[key] = yield
diff --git a/lib/declarative_policy/delegate_dsl.rb b/lib/declarative_policy/delegate_dsl.rb
index f544dffe888..ca2eb98e3e8 100644
--- a/lib/declarative_policy/delegate_dsl.rb
+++ b/lib/declarative_policy/delegate_dsl.rb
@@ -7,10 +7,10 @@ module DeclarativePolicy
@delegate_name = delegate_name
end
- def method_missing(m, *a, &b)
- return super unless a.empty? && !block_given?
+ def method_missing(msg, *args)
+ return super unless args.empty? && !block_given?
- @rule_dsl.delegate(@delegate_name, m)
+ @rule_dsl.delegate(@delegate_name, msg)
end
end
end
diff --git a/lib/declarative_policy/policy_dsl.rb b/lib/declarative_policy/policy_dsl.rb
index f11b6e9f730..c96049768a1 100644
--- a/lib/declarative_policy/policy_dsl.rb
+++ b/lib/declarative_policy/policy_dsl.rb
@@ -15,8 +15,8 @@ module DeclarativePolicy
@rule = rule
end
- def policy(&b)
- instance_eval(&b)
+ def policy(&block)
+ instance_eval(&block)
end
def enable(*abilities)
@@ -31,14 +31,14 @@ module DeclarativePolicy
@context_class.prevent_all_when(@rule)
end
- def method_missing(m, *a, &b)
- return super unless @context_class.respond_to?(m)
+ def method_missing(msg, *args, &block)
+ return super unless @context_class.respond_to?(msg)
- @context_class.__send__(m, *a, &b) # rubocop:disable GitlabSecurity/PublicSend
+ @context_class.__send__(msg, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
end
- def respond_to_missing?(m)
- @context_class.respond_to?(m) || super
+ def respond_to_missing?(msg)
+ @context_class.respond_to?(msg) || super
end
end
end
diff --git a/lib/declarative_policy/preferred_scope.rb b/lib/declarative_policy/preferred_scope.rb
index 5c214408dd0..c77784cb49d 100644
--- a/lib/declarative_policy/preferred_scope.rb
+++ b/lib/declarative_policy/preferred_scope.rb
@@ -2,7 +2,7 @@ module DeclarativePolicy # rubocop:disable Naming/FileName
PREFERRED_SCOPE_KEY = :"DeclarativePolicy.preferred_scope"
class << self
- def with_preferred_scope(scope, &b)
+ def with_preferred_scope(scope)
Thread.current[PREFERRED_SCOPE_KEY], old_scope = scope, Thread.current[PREFERRED_SCOPE_KEY]
yield
ensure
@@ -13,12 +13,12 @@ module DeclarativePolicy # rubocop:disable Naming/FileName
Thread.current[PREFERRED_SCOPE_KEY]
end
- def user_scope(&b)
- with_preferred_scope(:user, &b)
+ def user_scope(&block)
+ with_preferred_scope(:user, &block)
end
- def subject_scope(&b)
- with_preferred_scope(:subject, &b)
+ def subject_scope(&block)
+ with_preferred_scope(:subject, &block)
end
def preferred_scope=(scope)
diff --git a/lib/declarative_policy/rule.rb b/lib/declarative_policy/rule.rb
index e309244a3b3..407398cc770 100644
--- a/lib/declarative_policy/rule.rb
+++ b/lib/declarative_policy/rule.rb
@@ -8,8 +8,8 @@ module DeclarativePolicy
# how that affects the actual ability decision - for that, a
# `Step` is used.
class Base
- def self.make(*a)
- new(*a).simplify
+ def self.make(*args)
+ new(*args).simplify
end
# true or false whether this rule passes.
diff --git a/lib/declarative_policy/rule_dsl.rb b/lib/declarative_policy/rule_dsl.rb
index e948b7f2de1..7254b08eda5 100644
--- a/lib/declarative_policy/rule_dsl.rb
+++ b/lib/declarative_policy/rule_dsl.rb
@@ -32,13 +32,13 @@ module DeclarativePolicy
Rule::DelegatedCondition.new(delegate_name, condition)
end
- def method_missing(m, *a, &b)
- return super unless a.empty? && !block_given?
+ def method_missing(msg, *args)
+ return super unless args.empty? && !block_given?
- if @context_class.delegations.key?(m)
- DelegateDsl.new(self, m)
+ if @context_class.delegations.key?(msg)
+ DelegateDsl.new(self, msg)
else
- cond(m.to_sym)
+ cond(msg.to_sym)
end
end
end
diff --git a/lib/declarative_policy/runner.rb b/lib/declarative_policy/runner.rb
index 87f14b3b0d2..fec672f4b8c 100644
--- a/lib/declarative_policy/runner.rb
+++ b/lib/declarative_policy/runner.rb
@@ -127,7 +127,7 @@ module DeclarativePolicy
#
# For each step, we yield the step object along with the computed score
# for debugging purposes.
- def steps_by_score(&b)
+ def steps_by_score
flatten_steps!
if @steps.size > 50
diff --git a/lib/extracts_path.rb b/lib/extracts_path.rb
index a9b04c183ad..e8dbde176ef 100644
--- a/lib/extracts_path.rb
+++ b/lib/extracts_path.rb
@@ -139,6 +139,11 @@ module ExtractsPath
def lfs_blob_ids
blob_ids = tree.blobs.map(&:id)
+
+ # When current endpoint is a Blob then `tree.blobs` will be empty, it means we need to analyze
+ # the current Blob in order to determine if it's a LFS object
+ blob_ids = Array.wrap(@repo.blob_at(@commit.id, @path)&.id) if blob_ids.empty? # rubocop:disable Gitlab/ModuleWithInstanceVariables
+
@lfs_blob_ids = Gitlab::Git::Blob.batch_lfs_pointers(@project.repository, blob_ids).map(&:id) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
diff --git a/lib/feature.rb b/lib/feature.rb
index 6474de6e56d..314ae224d90 100644
--- a/lib/feature.rb
+++ b/lib/feature.rb
@@ -63,8 +63,15 @@ class Feature
end
def flipper
- Thread.current[:flipper] ||=
- Flipper.new(flipper_adapter).tap { |flip| flip.memoize = true }
+ if RequestStore.active?
+ RequestStore[:flipper] ||= build_flipper_instance
+ else
+ @flipper ||= build_flipper_instance
+ end
+ end
+
+ def build_flipper_instance
+ Flipper.new(flipper_adapter).tap { |flip| flip.memoize = true }
end
# This method is called from config/initializers/flipper.rb and can be used
diff --git a/lib/gitaly/server.rb b/lib/gitaly/server.rb
index 605e93022e7..2760211fee8 100644
--- a/lib/gitaly/server.rb
+++ b/lib/gitaly/server.rb
@@ -22,6 +22,18 @@ module Gitaly
server_version == Gitlab::GitalyClient.expected_server_version
end
+ def read_writeable?
+ readable? && writeable?
+ end
+
+ def readable?
+ storage_status&.readable
+ end
+
+ def writeable?
+ storage_status&.writeable
+ end
+
def address
Gitlab::GitalyClient.address(@storage)
rescue RuntimeError => e
@@ -30,13 +42,17 @@ module Gitaly
private
+ def storage_status
+ @storage_status ||= info.storage_statuses.find { |s| s.storage_name == storage }
+ end
+
def info
@info ||=
begin
Gitlab::GitalyClient::ServerService.new(@storage).info
rescue GRPC::Unavailable, GRPC::GRPC::DeadlineExceeded
# This will show the server as being out of date
- Gitaly::ServerInfoResponse.new(git_version: '', server_version: '')
+ Gitaly::ServerInfoResponse.new(git_version: '', server_version: '', storage_statuses: [])
end
end
end
diff --git a/lib/gitlab.rb b/lib/gitlab.rb
index 31119471b8e..ab6b609d099 100644
--- a/lib/gitlab.rb
+++ b/lib/gitlab.rb
@@ -9,15 +9,27 @@ module Gitlab
Settings
end
- def self.migrations_hash
- @_migrations_hash ||= Digest::MD5.hexdigest(ActiveRecord::Migrator.get_all_versions.to_s)
+ def self.revision
+ @_revision ||= begin
+ if File.exist?(root.join("REVISION"))
+ File.read(root.join("REVISION")).strip.freeze
+ else
+ result = Gitlab::Popen.popen_with_detail(%W[#{config.git.bin_path} log --pretty=format:%h -n 1])
+
+ if result.status.success?
+ result.stdout.chomp.freeze
+ else
+ "Unknown".freeze
+ end
+ end
+ end
end
COM_URL = 'https://gitlab.com'.freeze
APP_DIRS_PATTERN = %r{^/?(app|config|ee|lib|spec|\(\w*\))}
SUBDOMAIN_REGEX = %r{\Ahttps://[a-z0-9]+\.gitlab\.com\z}
VERSION = File.read(root.join("VERSION")).strip.freeze
- REVISION = Gitlab::Popen.popen(%W(#{config.git.bin_path} log --pretty=format:%h -n 1)).first.chomp.freeze
+ INSTALLATION_TYPE = File.read(root.join("INSTALLATION_TYPE")).strip.freeze
def self.com?
# Check `gl_subdomain?` as well to keep parity with gitlab.com
diff --git a/lib/gitlab/access.rb b/lib/gitlab/access.rb
index 7127948cf00..b170145f013 100644
--- a/lib/gitlab/access.rb
+++ b/lib/gitlab/access.rb
@@ -7,12 +7,14 @@ module Gitlab
module Access
AccessDeniedError = Class.new(StandardError)
- NO_ACCESS = 0
- GUEST = 10
- REPORTER = 20
- DEVELOPER = 30
- MASTER = 40
- OWNER = 50
+ NO_ACCESS = 0
+ GUEST = 10
+ REPORTER = 20
+ DEVELOPER = 30
+ MAINTAINER = 40
+ # @deprecated
+ MASTER = MAINTAINER
+ OWNER = 50
# Branch protection settings
PROTECTION_NONE = 0
@@ -29,10 +31,10 @@ module Gitlab
def options
{
- "Guest" => GUEST,
- "Reporter" => REPORTER,
- "Developer" => DEVELOPER,
- "Master" => MASTER
+ "Guest" => GUEST,
+ "Reporter" => REPORTER,
+ "Developer" => DEVELOPER,
+ "Maintainer" => MAINTAINER
}
end
@@ -44,10 +46,10 @@ module Gitlab
def sym_options
{
- guest: GUEST,
- reporter: REPORTER,
- developer: DEVELOPER,
- master: MASTER
+ guest: GUEST,
+ reporter: REPORTER,
+ developer: DEVELOPER,
+ maintainer: MAINTAINER
}
end
@@ -57,10 +59,10 @@ 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. 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
+ "Not protected: Both developers and maintainers 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. Maintainers can push to the branch." => PROTECTION_DEV_CAN_MERGE,
+ "Partially protected: Both developers and maintainers can push new commits, but cannot force push or delete the branch." => PROTECTION_DEV_CAN_PUSH,
+ "Fully protected: Developers cannot push new commits, but maintainers 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 8e5a985edd7..7de66539848 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -14,6 +14,25 @@ module Gitlab
DEFAULT_SCOPES = [:api].freeze
class << self
+ def omniauth_customized_providers
+ @omniauth_customized_providers ||= %w[bitbucket jwt]
+ end
+
+ def omniauth_setup_providers(provider_names)
+ provider_names.each do |provider|
+ omniauth_setup_a_provider(provider)
+ end
+ end
+
+ def omniauth_setup_a_provider(provider)
+ case provider
+ when 'kerberos'
+ require 'omniauth-kerberos'
+ when *omniauth_customized_providers
+ require_dependency "omni_auth/strategies/#{provider}"
+ end
+ end
+
def find_for_git_client(login, password, project:, ip:)
raise "Must provide an IP for rate limiting" if ip.nil?
@@ -221,7 +240,7 @@ module Gitlab
return unless login == 'gitlab-ci-token'
return unless password
- build = ::Ci::Build.running.find_by_token(password)
+ build = find_build_by_token(password)
return unless build
return unless build.project.builds_enabled?
@@ -282,6 +301,12 @@ module Gitlab
REGISTRY_SCOPES
end
+
+ private
+
+ def find_build_by_token(token)
+ ::Ci::Build.running.find_by_token(token)
+ end
end
end
end
diff --git a/lib/gitlab/auth/o_auth/user.rb b/lib/gitlab/auth/o_auth/user.rb
index 6c5d0788a0a..589e8062226 100644
--- a/lib/gitlab/auth/o_auth/user.rb
+++ b/lib/gitlab/auth/o_auth/user.rb
@@ -48,7 +48,7 @@ module Gitlab
gl_user
rescue ActiveRecord::RecordInvalid => e
log.info "(#{provider}) Error saving user #{auth_hash.uid} (#{auth_hash.email}): #{gl_user.errors.full_messages}"
- return self, e.record.errors
+ [self, e.record.errors]
end
def gl_user
@@ -74,6 +74,10 @@ module Gitlab
gl_user
end
+ def bypass_two_factor?
+ false
+ end
+
protected
def should_save?
diff --git a/lib/gitlab/auth/request_authenticator.rb b/lib/gitlab/auth/request_authenticator.rb
index a0b5cd868c3..66de52506ce 100644
--- a/lib/gitlab/auth/request_authenticator.rb
+++ b/lib/gitlab/auth/request_authenticator.rb
@@ -16,7 +16,7 @@ module Gitlab
end
def find_sessionless_user
- find_user_from_access_token || find_user_from_rss_token
+ find_user_from_access_token || find_user_from_feed_token
rescue Gitlab::Auth::AuthenticationError
nil
end
diff --git a/lib/gitlab/auth/saml/auth_hash.rb b/lib/gitlab/auth/saml/auth_hash.rb
index c345a7e3f6c..3bc5e2864df 100644
--- a/lib/gitlab/auth/saml/auth_hash.rb
+++ b/lib/gitlab/auth/saml/auth_hash.rb
@@ -6,6 +6,17 @@ module Gitlab
Array.wrap(get_raw(Gitlab::Auth::Saml::Config.groups))
end
+ def authn_context
+ response_object = auth_hash.extra[:response_object]
+ return nil if response_object.blank?
+
+ document = response_object.decrypted_document
+ document ||= response_object.document
+ return nil if document.blank?
+
+ extract_authn_context(document)
+ end
+
private
def get_raw(key)
@@ -13,6 +24,10 @@ module Gitlab
# otherwise just the first value is returned
auth_hash.extra[:raw_info].all[key]
end
+
+ def extract_authn_context(document)
+ REXML::XPath.first(document, "//saml:AuthnStatement/saml:AuthnContext/saml:AuthnContextClassRef/text()").to_s
+ end
end
end
end
diff --git a/lib/gitlab/auth/saml/config.rb b/lib/gitlab/auth/saml/config.rb
index 5fa9581f837..625dab7c6f4 100644
--- a/lib/gitlab/auth/saml/config.rb
+++ b/lib/gitlab/auth/saml/config.rb
@@ -7,6 +7,10 @@ module Gitlab
Gitlab::Auth::OAuth::Provider.config_for('saml')
end
+ def upstream_two_factor_authn_contexts
+ options.args[:upstream_two_factor_authn_contexts]
+ end
+
def groups
options[:groups_attribute]
end
diff --git a/lib/gitlab/auth/saml/user.rb b/lib/gitlab/auth/saml/user.rb
index b8c84c37cd5..6c3b75f3eb0 100644
--- a/lib/gitlab/auth/saml/user.rb
+++ b/lib/gitlab/auth/saml/user.rb
@@ -34,6 +34,10 @@ module Gitlab
gl_user.changed? || gl_user.identities.any?(&:changed?)
end
+ def bypass_two_factor?
+ saml_config.upstream_two_factor_authn_contexts&.include?(auth_hash.authn_context)
+ end
+
protected
def saml_config
diff --git a/lib/gitlab/auth/user_access_denied_reason.rb b/lib/gitlab/auth/user_access_denied_reason.rb
index af310aa12fc..1893cb001b2 100644
--- a/lib/gitlab/auth/user_access_denied_reason.rb
+++ b/lib/gitlab/auth/user_access_denied_reason.rb
@@ -8,12 +8,12 @@ module Gitlab
def rejection_message
case rejection_type
when :internal
- 'This action cannot be performed by internal users'
+ "This action cannot be performed by internal users"
when :terms_not_accepted
- 'You must accept the Terms of Service in order to perform this action. '\
- 'Please access GitLab from a web browser to accept these terms.'
+ "You (#{@user.to_reference}) must accept the Terms of Service in order to perform this action. "\
+ "Please access GitLab from a web browser to accept these terms."
else
- 'Your account has been blocked.'
+ "Your account has been blocked."
end
end
diff --git a/lib/gitlab/auth/user_auth_finders.rb b/lib/gitlab/auth/user_auth_finders.rb
index 4dc23f977da..c7993665421 100644
--- a/lib/gitlab/auth/user_auth_finders.rb
+++ b/lib/gitlab/auth/user_auth_finders.rb
@@ -25,13 +25,15 @@ module Gitlab
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?
+ def find_user_from_feed_token
+ return unless rss_request? || ics_request?
- token = current_request.params[:rss_token].presence
+ # NOTE: feed_token was renamed from rss_token but both needs to be supported because
+ # users might have already added the feed to their RSS reader before the rename
+ token = current_request.params[:feed_token].presence || current_request.params[:rss_token].presence
return unless token
- User.find_by_rss_token(token) || raise(UnauthorizedError)
+ User.find_by_feed_token(token) || raise(UnauthorizedError)
end
def find_user_from_access_token
@@ -104,6 +106,14 @@ module Gitlab
def current_request
@current_request ||= ensure_action_dispatch_request(request)
end
+
+ def rss_request?
+ current_request.path.ends_with?('.atom') || current_request.format.atom?
+ end
+
+ def ics_request?
+ current_request.path.ends_with?('.ics') || current_request.format.ics?
+ end
end
end
end
diff --git a/lib/gitlab/background_migration/add_merge_request_diff_commits_count.rb b/lib/gitlab/background_migration/add_merge_request_diff_commits_count.rb
index d5cf9e0d53a..cb2bdea755c 100644
--- a/lib/gitlab/background_migration/add_merge_request_diff_commits_count.rb
+++ b/lib/gitlab/background_migration/add_merge_request_diff_commits_count.rb
@@ -1,6 +1,5 @@
# frozen_string_literal: true
# rubocop:disable Style/Documentation
-# rubocop:disable Metrics/LineLength
module Gitlab
module BackgroundMigration
diff --git a/lib/gitlab/background_migration/archive_legacy_traces.rb b/lib/gitlab/background_migration/archive_legacy_traces.rb
new file mode 100644
index 00000000000..92096e29ef1
--- /dev/null
+++ b/lib/gitlab/background_migration/archive_legacy_traces.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+# rubocop:disable Style/Documentation
+
+module Gitlab
+ module BackgroundMigration
+ class ArchiveLegacyTraces
+ def perform(start_id, stop_id)
+ # This background migration directly refers to ::Ci::Build model which is defined in application code.
+ # In general, migration code should be isolated as much as possible in order to be idempotent.
+ # However, `archive!` method is too complicated to be replicated by coping its subsequent code.
+ # So we chose a way to use ::Ci::Build directly and we don't change the `archive!` method until 11.1
+ ::Ci::Build.finished.without_archived_trace
+ .where(id: start_id..stop_id).find_each do |build|
+ begin
+ build.trace.archive!
+ rescue => e
+ Rails.logger.error "Failed to archive live trace. id: #{build.id} message: #{e.message}"
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/cleanup_concurrent_rename.rb b/lib/gitlab/background_migration/cleanup_concurrent_rename.rb
new file mode 100644
index 00000000000..d3f366f3480
--- /dev/null
+++ b/lib/gitlab/background_migration/cleanup_concurrent_rename.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # Background migration for cleaning up a concurrent column rename.
+ class CleanupConcurrentRename < CleanupConcurrentSchemaChange
+ RESCHEDULE_DELAY = 10.minutes
+
+ def cleanup_concurrent_schema_change(table, old_column, new_column)
+ cleanup_concurrent_column_rename(table, old_column, new_column)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/cleanup_concurrent_schema_change.rb b/lib/gitlab/background_migration/cleanup_concurrent_schema_change.rb
new file mode 100644
index 00000000000..54f77f184d5
--- /dev/null
+++ b/lib/gitlab/background_migration/cleanup_concurrent_schema_change.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # Base class for cleaning up concurrent schema changes.
+ class CleanupConcurrentSchemaChange
+ include Database::MigrationHelpers
+
+ # table - The name of the table the migration is performed for.
+ # old_column - The name of the old (to drop) column.
+ # new_column - The name of the new column.
+ def perform(table, old_column, new_column)
+ return unless column_exists?(table, new_column)
+
+ rows_to_migrate = define_model_for(table)
+ .where(new_column => nil)
+ .where
+ .not(old_column => nil)
+
+ if rows_to_migrate.any?
+ BackgroundMigrationWorker.perform_in(
+ RESCHEDULE_DELAY,
+ self.class.name,
+ [table, old_column, new_column]
+ )
+ else
+ cleanup_concurrent_schema_change(table, old_column, new_column)
+ end
+ end
+
+ # These methods are necessary so we can re-use the migration helpers in
+ # this class.
+ def connection
+ ActiveRecord::Base.connection
+ end
+
+ def method_missing(name, *args, &block)
+ connection.__send__(name, *args, &block) # rubocop: disable GitlabSecurity/PublicSend
+ end
+
+ def respond_to_missing?(*args)
+ connection.respond_to?(*args) || super
+ end
+
+ def define_model_for(table)
+ Class.new(ActiveRecord::Base) do
+ self.table_name = table
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/cleanup_concurrent_type_change.rb b/lib/gitlab/background_migration/cleanup_concurrent_type_change.rb
index de622f657b2..48411095dbb 100644
--- a/lib/gitlab/background_migration/cleanup_concurrent_type_change.rb
+++ b/lib/gitlab/background_migration/cleanup_concurrent_type_change.rb
@@ -2,52 +2,12 @@
module Gitlab
module BackgroundMigration
- # Background migration for cleaning up a concurrent column rename.
- class CleanupConcurrentTypeChange
- include Database::MigrationHelpers
-
+ # Background migration for cleaning up a concurrent column type changeb.
+ class CleanupConcurrentTypeChange < CleanupConcurrentSchemaChange
RESCHEDULE_DELAY = 10.minutes
- # table - The name of the table the migration is performed for.
- # old_column - The name of the old (to drop) column.
- # new_column - The name of the new column.
- def perform(table, old_column, new_column)
- return unless column_exists?(:issues, new_column)
-
- rows_to_migrate = define_model_for(table)
- .where(new_column => nil)
- .where
- .not(old_column => nil)
-
- if rows_to_migrate.any?
- BackgroundMigrationWorker.perform_in(
- RESCHEDULE_DELAY,
- 'CleanupConcurrentTypeChange',
- [table, old_column, new_column]
- )
- else
- cleanup_concurrent_column_type_change(table, old_column)
- end
- end
-
- # These methods are necessary so we can re-use the migration helpers in
- # this class.
- def connection
- ActiveRecord::Base.connection
- end
-
- def method_missing(name, *args, &block)
- connection.__send__(name, *args, &block) # rubocop: disable GitlabSecurity/PublicSend
- end
-
- def respond_to_missing?(*args)
- connection.respond_to?(*args) || super
- end
-
- def define_model_for(table)
- Class.new(ActiveRecord::Base) do
- self.table_name = table
- end
+ def cleanup_concurrent_schema_change(table, old_column, new_column)
+ cleanup_concurrent_column_type_change(table, old_column)
end
end
end
diff --git a/lib/gitlab/background_migration/create_fork_network_memberships_range.rb b/lib/gitlab/background_migration/create_fork_network_memberships_range.rb
index 1b4a9e8a194..ccd1f9b4dba 100644
--- a/lib/gitlab/background_migration/create_fork_network_memberships_range.rb
+++ b/lib/gitlab/background_migration/create_fork_network_memberships_range.rb
@@ -1,5 +1,4 @@
# frozen_string_literal: true
-# rubocop:disable Metrics/LineLength
# rubocop:disable Style/Documentation
module Gitlab
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 c2bf42f846d..da8265a3a5f 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,5 +1,4 @@
# frozen_string_literal: true
-# rubocop:disable Metrics/LineLength
# rubocop:disable Style/Documentation
class Gitlab::BackgroundMigration::CreateGpgKeySubkeysFromGpgKeys
diff --git a/lib/gitlab/background_migration/delete_diff_files.rb b/lib/gitlab/background_migration/delete_diff_files.rb
new file mode 100644
index 00000000000..664ead1af44
--- /dev/null
+++ b/lib/gitlab/background_migration/delete_diff_files.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+# rubocop:disable Style/Documentation
+
+module Gitlab
+ module BackgroundMigration
+ class DeleteDiffFiles
+ class MergeRequestDiff < ActiveRecord::Base
+ self.table_name = 'merge_request_diffs'
+
+ belongs_to :merge_request
+ has_many :merge_request_diff_files
+ end
+
+ class MergeRequestDiffFile < ActiveRecord::Base
+ self.table_name = 'merge_request_diff_files'
+ end
+
+ DEAD_TUPLES_THRESHOLD = 50_000
+ VACUUM_WAIT_TIME = 5.minutes
+
+ def perform(ids)
+ @ids = ids
+
+ # We should reschedule until deadtuples get in a desirable
+ # state (e.g. < 50_000). That may take more than one reschedule.
+ #
+ if should_wait_deadtuple_vacuum?
+ reschedule
+ return
+ end
+
+ prune_diff_files
+ end
+
+ def should_wait_deadtuple_vacuum?
+ return false unless Gitlab::Database.postgresql?
+
+ diff_files_dead_tuples_count >= DEAD_TUPLES_THRESHOLD
+ end
+
+ private
+
+ def reschedule
+ BackgroundMigrationWorker.perform_in(VACUUM_WAIT_TIME, self.class.name.demodulize, [@ids])
+ end
+
+ def diffs_collection
+ MergeRequestDiff.where(id: @ids)
+ end
+
+ def diff_files_dead_tuples_count
+ dead_tuple =
+ execute_statement("SELECT n_dead_tup FROM pg_stat_all_tables "\
+ "WHERE relname = 'merge_request_diff_files'")[0]
+
+ dead_tuple&.fetch('n_dead_tup', 0).to_i
+ end
+
+ def prune_diff_files
+ removed = 0
+ updated = 0
+
+ MergeRequestDiff.transaction do
+ updated = diffs_collection.update_all(state: 'without_files')
+ removed = MergeRequestDiffFile.where(merge_request_diff_id: @ids).delete_all
+ end
+
+ log_info("Removed #{removed} merge_request_diff_files rows, "\
+ "updated #{updated} merge_request_diffs rows")
+ end
+
+ def execute_statement(sql)
+ ActiveRecord::Base.connection.execute(sql)
+ end
+
+ def log_info(message)
+ Rails.logger.info("BackgroundMigration::DeleteDiffFiles - #{message}")
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits.rb b/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits.rb
index a357538a885..58df74cfa9b 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,6 +1,5 @@
# frozen_string_literal: true
# rubocop:disable Metrics/MethodLength
-# rubocop:disable Metrics/LineLength
# rubocop:disable Metrics/AbcSize
# rubocop:disable Style/Documentation
diff --git a/lib/gitlab/background_migration/fill_file_store_job_artifact.rb b/lib/gitlab/background_migration/fill_file_store_job_artifact.rb
new file mode 100644
index 00000000000..103bd98af14
--- /dev/null
+++ b/lib/gitlab/background_migration/fill_file_store_job_artifact.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+# rubocop:disable Style/Documentation
+
+module Gitlab
+ module BackgroundMigration
+ class FillFileStoreJobArtifact
+ class JobArtifact < ActiveRecord::Base
+ self.table_name = 'ci_job_artifacts'
+ end
+
+ def perform(start_id, stop_id)
+ FillFileStoreJobArtifact::JobArtifact
+ .where(file_store: nil)
+ .where(id: (start_id..stop_id))
+ .update_all(file_store: 1)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/fill_file_store_lfs_object.rb b/lib/gitlab/background_migration/fill_file_store_lfs_object.rb
new file mode 100644
index 00000000000..77c1f1ffaf0
--- /dev/null
+++ b/lib/gitlab/background_migration/fill_file_store_lfs_object.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+# rubocop:disable Style/Documentation
+
+module Gitlab
+ module BackgroundMigration
+ class FillFileStoreLfsObject
+ class LfsObject < ActiveRecord::Base
+ self.table_name = 'lfs_objects'
+ end
+
+ def perform(start_id, stop_id)
+ FillFileStoreLfsObject::LfsObject
+ .where(file_store: nil)
+ .where(id: (start_id..stop_id))
+ .update_all(file_store: 1)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/fill_store_upload.rb b/lib/gitlab/background_migration/fill_store_upload.rb
new file mode 100644
index 00000000000..cba3e21cea6
--- /dev/null
+++ b/lib/gitlab/background_migration/fill_store_upload.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+# rubocop:disable Style/Documentation
+
+module Gitlab
+ module BackgroundMigration
+ class FillStoreUpload
+ class Upload < ActiveRecord::Base
+ self.table_name = 'uploads'
+ self.inheritance_column = :_type_disabled
+ end
+
+ def perform(start_id, stop_id)
+ FillStoreUpload::Upload
+ .where(store: nil)
+ .where(id: (start_id..stop_id))
+ .update_all(store: 1)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/fix_cross_project_label_links.rb b/lib/gitlab/background_migration/fix_cross_project_label_links.rb
new file mode 100644
index 00000000000..0a12401c35f
--- /dev/null
+++ b/lib/gitlab/background_migration/fix_cross_project_label_links.rb
@@ -0,0 +1,140 @@
+# frozen_string_literal: true
+# rubocop:disable Style/Documentation
+
+module Gitlab
+ module BackgroundMigration
+ class FixCrossProjectLabelLinks
+ GROUP_NESTED_LEVEL = 10.freeze
+
+ class Project < ActiveRecord::Base
+ self.table_name = 'projects'
+ end
+
+ class Label < ActiveRecord::Base
+ self.inheritance_column = :_type_disabled
+ self.table_name = 'labels'
+ end
+
+ class LabelLink < ActiveRecord::Base
+ self.table_name = 'label_links'
+ end
+
+ class Issue < ActiveRecord::Base
+ self.table_name = 'issues'
+ end
+
+ class MergeRequest < ActiveRecord::Base
+ self.table_name = 'merge_requests'
+ end
+
+ class Namespace < ActiveRecord::Base
+ self.inheritance_column = :_type_disabled
+ self.table_name = 'namespaces'
+
+ def self.groups_with_descendants_ids(start_id, stop_id)
+ # To isolate migration code, we avoid usage of
+ # Gitlab::GroupHierarchy#base_and_descendants which already
+ # does this job better
+ ids = Namespace.where(type: 'Group', id: Label.where(type: 'GroupLabel').select('distinct group_id')).where(id: start_id..stop_id).pluck(:id)
+ group_ids = ids
+
+ GROUP_NESTED_LEVEL.times do
+ ids = Namespace.where(type: 'Group', parent_id: ids).pluck(:id)
+ break if ids.empty?
+
+ group_ids += ids
+ end
+
+ group_ids.uniq
+ end
+ end
+
+ def perform(start_id, stop_id)
+ group_ids = Namespace.groups_with_descendants_ids(start_id, stop_id)
+ project_ids = Project.where(namespace_id: group_ids).select(:id)
+
+ fix_issues(project_ids)
+ fix_merge_requests(project_ids)
+ end
+
+ private
+
+ # select IDs of issues which reference a label which is:
+ # a) a project label of a different project, or
+ # b) a group label of a different group than issue's project group
+ def fix_issues(project_ids)
+ issue_ids = Label
+ .joins('INNER JOIN label_links ON label_links.label_id = labels.id AND label_links.target_type = \'Issue\'
+ INNER JOIN issues ON issues.id = label_links.target_id
+ INNER JOIN projects ON projects.id = issues.project_id')
+ .where('issues.project_id in (?)', project_ids)
+ .where('(labels.project_id is not null and labels.project_id != issues.project_id) '\
+ 'or (labels.group_id is not null and labels.group_id != projects.namespace_id)')
+ .select('distinct issues.id')
+
+ Issue.where(id: issue_ids).find_each { |issue| check_resource_labels(issue, issue.project_id) }
+ end
+
+ # select IDs of MRs which reference a label which is:
+ # a) a project label of a different project, or
+ # b) a group label of a different group than MR's project group
+ def fix_merge_requests(project_ids)
+ mr_ids = Label
+ .joins('INNER JOIN label_links ON label_links.label_id = labels.id AND label_links.target_type = \'MergeRequest\'
+ INNER JOIN merge_requests ON merge_requests.id = label_links.target_id
+ INNER JOIN projects ON projects.id = merge_requests.target_project_id')
+ .where('merge_requests.target_project_id in (?)', project_ids)
+ .where('(labels.project_id is not null and labels.project_id != merge_requests.target_project_id) '\
+ 'or (labels.group_id is not null and labels.group_id != projects.namespace_id)')
+ .select('distinct merge_requests.id')
+
+ MergeRequest.where(id: mr_ids).find_each { |merge_request| check_resource_labels(merge_request, merge_request.target_project_id) }
+ end
+
+ def check_resource_labels(resource, project_id)
+ local_labels = available_labels(project_id)
+
+ # get all label links for the given resource (issue/MR)
+ # which reference a label not included in avaiable_labels
+ # (other than its project labels and labels of ancestor groups)
+ cross_labels = LabelLink
+ .select('label_id, labels.title as title, labels.color as color, label_links.id as label_link_id')
+ .joins('INNER JOIN labels ON labels.id = label_links.label_id')
+ .where(target_type: resource.class.name.demodulize, target_id: resource.id)
+ .where('labels.id not in (?)', local_labels.select(:id))
+
+ cross_labels.each do |label|
+ matching_label = local_labels.find {|l| l.title == label.title && l.color == label.color}
+
+ next unless matching_label
+
+ Rails.logger.info "#{resource.class.name.demodulize} #{resource.id}: replacing #{label.label_id} with #{matching_label.id}"
+ LabelLink.update(label.label_link_id, label_id: matching_label.id)
+ end
+ end
+
+ # get all labels available for the project (including
+ # group labels of ancestor groups)
+ def available_labels(project_id)
+ @labels ||= {}
+ @labels[project_id] ||= Label
+ .where("(type = 'GroupLabel' and group_id in (?)) or (type = 'ProjectLabel' and id = ?)",
+ project_group_ids(project_id),
+ project_id)
+ end
+
+ def project_group_ids(project_id)
+ ids = [Project.find(project_id).namespace_id]
+
+ GROUP_NESTED_LEVEL.times do
+ group = Namespace.find(ids.last)
+ break unless group.parent_id
+
+ ids << group.parent_id
+ end
+
+ ids
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/migrate_build_stage.rb b/lib/gitlab/background_migration/migrate_build_stage.rb
index 242e3143e71..268c6083d3c 100644
--- a/lib/gitlab/background_migration/migrate_build_stage.rb
+++ b/lib/gitlab/background_migration/migrate_build_stage.rb
@@ -1,5 +1,4 @@
# frozen_string_literal: true
-# rubocop:disable Metrics/AbcSize
# rubocop:disable Style/Documentation
module Gitlab
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 7088aa0860a..38fecac1bfe 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,5 +1,4 @@
# frozen_string_literal: true
-# rubocop:disable Metrics/LineLength
# rubocop:disable Style/Documentation
module Gitlab
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 7f243073fd0..ef50fe4adb1 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,5 +1,4 @@
# frozen_string_literal: true
-# rubocop:disable Metrics/LineLength
# rubocop:disable Style/Documentation
module Gitlab
diff --git a/lib/gitlab/background_migration/move_personal_snippet_files.rb b/lib/gitlab/background_migration/move_personal_snippet_files.rb
index a4ef51fd0e8..5b2b2af718a 100644
--- a/lib/gitlab/background_migration/move_personal_snippet_files.rb
+++ b/lib/gitlab/background_migration/move_personal_snippet_files.rb
@@ -1,5 +1,4 @@
# frozen_string_literal: true
-# rubocop:disable Metrics/LineLength
# rubocop:disable Style/Documentation
module Gitlab
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 d9d3d2e667b..698f5e46c0c 100644
--- a/lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb
+++ b/lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb
@@ -1,6 +1,5 @@
# frozen_string_literal: true
# rubocop:disable Metrics/MethodLength
-# rubocop:disable Metrics/LineLength
# rubocop:disable Metrics/ClassLength
# rubocop:disable Metrics/BlockLength
# rubocop:disable Style/Documentation
diff --git a/lib/gitlab/background_migration/populate_fork_networks_range.rb b/lib/gitlab/background_migration/populate_fork_networks_range.rb
index a976cb4c243..aa4f130538c 100644
--- a/lib/gitlab/background_migration/populate_fork_networks_range.rb
+++ b/lib/gitlab/background_migration/populate_fork_networks_range.rb
@@ -19,7 +19,7 @@ module Gitlab
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
+ delay = BackgroundMigration::CreateForkNetworkMembershipsRange::RESCHEDULE_DELAY
BackgroundMigrationWorker.perform_in(
delay, "CreateForkNetworkMembershipsRange", [start_id, end_id]
)
diff --git a/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data.rb b/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data.rb
index 8a901a9bf39..d89ce358bb9 100644
--- a/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data.rb
+++ b/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data.rb
@@ -1,7 +1,4 @@
# frozen_string_literal: true
-# rubocop:disable Metrics/LineLength
-# rubocop:disable Metrics/MethodLength
-# rubocop:disable Metrics/ClassLength
# rubocop:disable Style/Documentation
module Gitlab
diff --git a/lib/gitlab/background_migration/populate_untracked_uploads.rb b/lib/gitlab/background_migration/populate_untracked_uploads.rb
index 9232f20a063..a19dc9747fb 100644
--- a/lib/gitlab/background_migration/populate_untracked_uploads.rb
+++ b/lib/gitlab/background_migration/populate_untracked_uploads.rb
@@ -4,7 +4,7 @@ module Gitlab
module BackgroundMigration
# This class processes a batch of rows in `untracked_files_for_uploads` by
# adding each file to the `uploads` table if it does not exist.
- class PopulateUntrackedUploads # rubocop:disable Metrics/ClassLength
+ class PopulateUntrackedUploads
def perform(start_id, end_id)
return unless migrate?
diff --git a/lib/gitlab/background_migration/populate_untracked_uploads_dependencies.rb b/lib/gitlab/background_migration/populate_untracked_uploads_dependencies.rb
index a2c5acbde71..4a9a62aaeb5 100644
--- a/lib/gitlab/background_migration/populate_untracked_uploads_dependencies.rb
+++ b/lib/gitlab/background_migration/populate_untracked_uploads_dependencies.rb
@@ -4,7 +4,7 @@ module Gitlab
module PopulateUntrackedUploadsDependencies
# This class is responsible for producing the attributes necessary to
# track an uploaded file in the `uploads` table.
- class UntrackedFile < ActiveRecord::Base # rubocop:disable Metrics/ClassLength, Metrics/LineLength
+ class UntrackedFile < ActiveRecord::Base # rubocop:disable Metrics/ClassLength
self.table_name = 'untracked_files_for_uploads'
# Ends with /:random_hex/:filename
@@ -134,7 +134,7 @@ module Gitlab
# Not including a leading slash
def path_relative_to_upload_dir
- upload_dir = Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR # rubocop:disable Metrics/LineLength
+ upload_dir = Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR
base = %r{\A#{Regexp.escape(upload_dir)}/}
@path_relative_to_upload_dir ||= path.sub(base, '')
end
diff --git a/lib/gitlab/background_migration/prepare_untracked_uploads.rb b/lib/gitlab/background_migration/prepare_untracked_uploads.rb
index 914a9e48a2f..81ca2b0a9b7 100644
--- a/lib/gitlab/background_migration/prepare_untracked_uploads.rb
+++ b/lib/gitlab/background_migration/prepare_untracked_uploads.rb
@@ -54,7 +54,8 @@ module Gitlab
def ensure_temporary_tracking_table_exists
table_name = :untracked_files_for_uploads
- unless UntrackedFile.connection.table_exists?(table_name)
+
+ unless ActiveRecord::Base.connection.data_source_exists?(table_name)
UntrackedFile.connection.create_table table_name do |t|
t.string :path, limit: 600, null: false
t.index :path, unique: true
@@ -143,7 +144,7 @@ module Gitlab
def table_columns_and_values_for_insert(file_paths)
values = file_paths.map do |file_path|
- ActiveRecord::Base.send(:sanitize_sql_array, ['(?)', file_path]) # rubocop:disable GitlabSecurity/PublicSend, Metrics/LineLength
+ ActiveRecord::Base.send(:sanitize_sql_array, ['(?)', file_path]) # rubocop:disable GitlabSecurity/PublicSend
end.join(', ')
"#{UntrackedFile.table_name} (path) VALUES #{values}"
diff --git a/lib/gitlab/background_migration/schedule_diff_files_deletion.rb b/lib/gitlab/background_migration/schedule_diff_files_deletion.rb
new file mode 100644
index 00000000000..609cf19187c
--- /dev/null
+++ b/lib/gitlab/background_migration/schedule_diff_files_deletion.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+# rubocop:disable Style/Documentation
+
+module Gitlab
+ module BackgroundMigration
+ class ScheduleDiffFilesDeletion
+ class MergeRequestDiff < ActiveRecord::Base
+ self.table_name = 'merge_request_diffs'
+
+ belongs_to :merge_request
+
+ include EachBatch
+ end
+
+ DIFF_BATCH_SIZE = 5_000
+ INTERVAL = 5.minutes
+ MIGRATION = 'DeleteDiffFiles'
+
+ def perform
+ diffs = MergeRequestDiff
+ .from("(#{diffs_collection.to_sql}) merge_request_diffs")
+ .where('merge_request_diffs.id != merge_request_diffs.latest_merge_request_diff_id')
+ .select(:id)
+
+ diffs.each_batch(of: DIFF_BATCH_SIZE) do |relation, index|
+ ids = relation.pluck(:id)
+
+ BackgroundMigrationWorker.perform_in(index * INTERVAL, MIGRATION, [ids])
+ end
+ end
+
+ private
+
+ def diffs_collection
+ MergeRequestDiff
+ .joins(:merge_request)
+ .where("merge_requests.state = 'merged'")
+ .where('merge_requests.latest_merge_request_diff_id IS NOT NULL')
+ .where("merge_request_diffs.state NOT IN ('without_files', 'empty')")
+ .select('merge_requests.latest_merge_request_diff_id, merge_request_diffs.id')
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cache/request_cache.rb b/lib/gitlab/cache/request_cache.rb
index ecc85f847d4..671b8e7e1b1 100644
--- a/lib/gitlab/cache/request_cache.rb
+++ b/lib/gitlab/cache/request_cache.rb
@@ -1,41 +1,6 @@
module Gitlab
module Cache
- # This module provides a simple way to cache values in RequestStore,
- # and the cache key would be based on the class name, method name,
- # optionally customized instance level values, optionally customized
- # method level values, and optional method arguments.
- #
- # A simple example:
- #
- # class UserAccess
- # extend Gitlab::Cache::RequestCache
- #
- # request_cache_key do
- # [user&.id, project&.id]
- # end
- #
- # request_cache def can_push_to_branch?(ref)
- # # ...
- # end
- # end
- #
- # This way, the result of `can_push_to_branch?` would be cached in
- # `RequestStore.store` based on the cache key. If RequestStore is not
- # currently active, then it would be stored in a hash saved in an
- # instance variable, so the cache logic would be the same.
- # Here's another example using customized method level values:
- #
- # class Commit
- # extend Gitlab::Cache::RequestCache
- #
- # def author
- # User.find_by_any_email(author_email.downcase)
- # end
- # request_cache(:author) { author_email.downcase }
- # end
- #
- # So that we could have different strategies for different methods
- #
+ # See https://docs.gitlab.com/ee/development/utilities.html#requestcache
module RequestCache
def self.extended(klass)
return if klass < self
diff --git a/lib/gitlab/checks/change_access.rb b/lib/gitlab/checks/change_access.rb
index 51ba09aa129..f76a6fb5f17 100644
--- a/lib/gitlab/checks/change_access.rb
+++ b/lib/gitlab/checks/change_access.rb
@@ -5,7 +5,7 @@ module Gitlab
push_code: 'You are not allowed to push code to this project.',
delete_default_branch: 'The default branch of a project cannot be deleted.',
force_push_protected_branch: 'You are not allowed to force push code to a protected branch on this project.',
- non_master_delete_protected_branch: 'You are not allowed to delete protected branches from this project. Only a project master or owner can delete a protected branch.',
+ non_master_delete_protected_branch: 'You are not allowed to delete protected branches from this project. Only a project maintainer or owner can delete a protected branch.',
non_web_delete_protected_branch: 'You can only delete protected branches using the web interface.',
merge_protected_branch: 'You are not allowed to merge code into protected branches on this project.',
push_protected_branch: 'You are not allowed to push code to protected branches on this project.',
diff --git a/lib/gitlab/checks/commit_check.rb b/lib/gitlab/checks/commit_check.rb
index 43a52b493bb..22310e313ac 100644
--- a/lib/gitlab/checks/commit_check.rb
+++ b/lib/gitlab/checks/commit_check.rb
@@ -37,7 +37,7 @@ module Gitlab
def validate_lfs_file_locks?
strong_memoize(:validate_lfs_file_locks) do
- project.lfs_enabled? && project.lfs_file_locks.any? && newrev && oldrev
+ project.lfs_enabled? && newrev && oldrev && project.any_lfs_file_locks?
end
end
diff --git a/lib/gitlab/checks/force_push.rb b/lib/gitlab/checks/force_push.rb
index c9c3050cfc2..87af4a90572 100644
--- a/lib/gitlab/checks/force_push.rb
+++ b/lib/gitlab/checks/force_push.rb
@@ -7,18 +7,10 @@ module Gitlab
# Created or deleted branch
return false if Gitlab::Git.blank_ref?(oldrev) || Gitlab::Git.blank_ref?(newrev)
- GitalyClient.migrate(:force_push) do |is_enabled|
- if is_enabled
- !project
- .repository
- .gitaly_commit_client
- .ancestor?(oldrev, newrev)
- else
- Gitlab::Git::RevList.new(
- project.repository.raw, oldrev: oldrev, newrev: newrev
- ).missed_ref.present?
- end
- end
+ !project
+ .repository
+ .gitaly_commit_client
+ .ancestor?(oldrev, newrev)
end
end
end
diff --git a/lib/gitlab/ci/ansi2html.rb b/lib/gitlab/ci/ansi2html.rb
index 35eadf6fa93..e780f8c646b 100644
--- a/lib/gitlab/ci/ansi2html.rb
+++ b/lib/gitlab/ci/ansi2html.rb
@@ -29,105 +29,105 @@ module Gitlab
end
class Converter
- def on_0(s) reset() end
+ def on_0(_) reset() end
- def on_1(s) enable(STYLE_SWITCHES[:bold]) end
+ def on_1(_) enable(STYLE_SWITCHES[:bold]) end
- def on_3(s) enable(STYLE_SWITCHES[:italic]) end
+ def on_3(_) enable(STYLE_SWITCHES[:italic]) end
- def on_4(s) enable(STYLE_SWITCHES[:underline]) end
+ def on_4(_) enable(STYLE_SWITCHES[:underline]) end
- def on_8(s) enable(STYLE_SWITCHES[:conceal]) end
+ def on_8(_) enable(STYLE_SWITCHES[:conceal]) end
- def on_9(s) enable(STYLE_SWITCHES[:cross]) end
+ def on_9(_) enable(STYLE_SWITCHES[:cross]) end
- def on_21(s) disable(STYLE_SWITCHES[:bold]) end
+ def on_21(_) disable(STYLE_SWITCHES[:bold]) end
- def on_22(s) disable(STYLE_SWITCHES[:bold]) end
+ def on_22(_) disable(STYLE_SWITCHES[:bold]) end
- def on_23(s) disable(STYLE_SWITCHES[:italic]) end
+ def on_23(_) disable(STYLE_SWITCHES[:italic]) end
- def on_24(s) disable(STYLE_SWITCHES[:underline]) end
+ def on_24(_) disable(STYLE_SWITCHES[:underline]) end
- def on_28(s) disable(STYLE_SWITCHES[:conceal]) end
+ def on_28(_) disable(STYLE_SWITCHES[:conceal]) end
- def on_29(s) disable(STYLE_SWITCHES[:cross]) end
+ def on_29(_) disable(STYLE_SWITCHES[:cross]) end
- def on_30(s) set_fg_color(0) end
+ def on_30(_) set_fg_color(0) end
- def on_31(s) set_fg_color(1) end
+ def on_31(_) set_fg_color(1) end
- def on_32(s) set_fg_color(2) end
+ def on_32(_) set_fg_color(2) end
- def on_33(s) set_fg_color(3) end
+ def on_33(_) set_fg_color(3) end
- def on_34(s) set_fg_color(4) end
+ def on_34(_) set_fg_color(4) end
- def on_35(s) set_fg_color(5) end
+ def on_35(_) set_fg_color(5) end
- def on_36(s) set_fg_color(6) end
+ def on_36(_) set_fg_color(6) end
- def on_37(s) set_fg_color(7) end
+ def on_37(_) set_fg_color(7) end
- def on_38(s) set_fg_color_256(s) end
+ def on_38(stack) set_fg_color_256(stack) end
- def on_39(s) set_fg_color(9) end
+ def on_39(_) set_fg_color(9) end
- def on_40(s) set_bg_color(0) end
+ def on_40(_) set_bg_color(0) end
- def on_41(s) set_bg_color(1) end
+ def on_41(_) set_bg_color(1) end
- def on_42(s) set_bg_color(2) end
+ def on_42(_) set_bg_color(2) end
- def on_43(s) set_bg_color(3) end
+ def on_43(_) set_bg_color(3) end
- def on_44(s) set_bg_color(4) end
+ def on_44(_) set_bg_color(4) end
- def on_45(s) set_bg_color(5) end
+ def on_45(_) set_bg_color(5) end
- def on_46(s) set_bg_color(6) end
+ def on_46(_) set_bg_color(6) end
- def on_47(s) set_bg_color(7) end
+ def on_47(_) set_bg_color(7) end
- def on_48(s) set_bg_color_256(s) end
+ def on_48(stack) set_bg_color_256(stack) end
- def on_49(s) set_bg_color(9) end
+ def on_49(_) set_bg_color(9) end
- def on_90(s) set_fg_color(0, 'l') end
+ def on_90(_) set_fg_color(0, 'l') end
- def on_91(s) set_fg_color(1, 'l') end
+ def on_91(_) set_fg_color(1, 'l') end
- def on_92(s) set_fg_color(2, 'l') end
+ def on_92(_) set_fg_color(2, 'l') end
- def on_93(s) set_fg_color(3, 'l') end
+ def on_93(_) set_fg_color(3, 'l') end
- def on_94(s) set_fg_color(4, 'l') end
+ def on_94(_) set_fg_color(4, 'l') end
- def on_95(s) set_fg_color(5, 'l') end
+ def on_95(_) set_fg_color(5, 'l') end
- def on_96(s) set_fg_color(6, 'l') end
+ def on_96(_) set_fg_color(6, 'l') end
- def on_97(s) set_fg_color(7, 'l') end
+ def on_97(_) set_fg_color(7, 'l') end
- def on_99(s) set_fg_color(9, 'l') end
+ def on_99(_) set_fg_color(9, 'l') end
- def on_100(s) set_bg_color(0, 'l') end
+ def on_100(_) set_bg_color(0, 'l') end
- def on_101(s) set_bg_color(1, 'l') end
+ def on_101(_) set_bg_color(1, 'l') end
- def on_102(s) set_bg_color(2, 'l') end
+ def on_102(_) set_bg_color(2, 'l') end
- def on_103(s) set_bg_color(3, 'l') end
+ def on_103(_) set_bg_color(3, 'l') end
- def on_104(s) set_bg_color(4, 'l') end
+ def on_104(_) set_bg_color(4, 'l') end
- def on_105(s) set_bg_color(5, 'l') end
+ def on_105(_) set_bg_color(5, 'l') end
- def on_106(s) set_bg_color(6, 'l') end
+ def on_106(_) set_bg_color(6, 'l') end
- def on_107(s) set_bg_color(7, 'l') end
+ def on_107(_) set_bg_color(7, 'l') end
- def on_109(s) set_bg_color(9, 'l') end
+ def on_109(_) set_bg_color(9, 'l') end
attr_accessor :offset, :n_open_tags, :fg_color, :bg_color, :style_mask
@@ -188,19 +188,19 @@ module Gitlab
)
end
- def handle_section(s)
- action = s[1]
- timestamp = s[2]
- section = s[3]
- line = s.matched()[0...-5] # strips \r\033[0K
+ def handle_section(scanner)
+ action = scanner[1]
+ timestamp = scanner[2]
+ section = scanner[3]
+ line = scanner.matched()[0...-5] # strips \r\033[0K
@out << %{<div class="hidden" data-action="#{action}" data-timestamp="#{timestamp}" data-section="#{section}">#{line}</div>}
end
- def handle_sequence(s)
- indicator = s[1]
- commands = s[2].split ';'
- terminator = s[3]
+ def handle_sequence(scanner)
+ indicator = scanner[1]
+ commands = scanner[2].split ';'
+ terminator = scanner[3]
# We are only interested in color and text style changes - triggered by
# sequences starting with '\e[' and ending with 'm'. Any other control
diff --git a/lib/gitlab/ci/build/artifacts/metadata.rb b/lib/gitlab/ci/build/artifacts/metadata.rb
index 0bbd60d8ffe..375d8bc1ff5 100644
--- a/lib/gitlab/ci/build/artifacts/metadata.rb
+++ b/lib/gitlab/ci/build/artifacts/metadata.rb
@@ -7,14 +7,15 @@ module Gitlab
module Artifacts
class Metadata
ParserError = Class.new(StandardError)
+ InvalidStreamError = Class.new(StandardError)
VERSION_PATTERN = /^[\w\s]+(\d+\.\d+\.\d+)/
INVALID_PATH_PATTERN = %r{(^\.?\.?/)|(/\.?\.?/)}
- attr_reader :file, :path, :full_version
+ attr_reader :stream, :path, :full_version
- def initialize(file, path, **opts)
- @file, @path, @opts = file, path, opts
+ def initialize(stream, path, **opts)
+ @stream, @path, @opts = stream, path, opts
@full_version = read_version
end
@@ -103,7 +104,17 @@ module Gitlab
end
def gzip(&block)
- Zlib::GzipReader.open(@file, &block)
+ raise InvalidStreamError, "Invalid stream" unless @stream
+
+ # restart gzip reading
+ @stream.seek(0)
+
+ gz = Zlib::GzipReader.new(@stream)
+ yield(gz)
+ rescue Zlib::Error => e
+ raise InvalidStreamError, e.message
+ ensure
+ gz&.finish
end
end
end
diff --git a/lib/gitlab/ci/config/entry/configurable.rb b/lib/gitlab/ci/config/entry/configurable.rb
index db47c2f6185..7cddd2c7b7e 100644
--- a/lib/gitlab/ci/config/entry/configurable.rb
+++ b/lib/gitlab/ci/config/entry/configurable.rb
@@ -47,7 +47,7 @@ module Gitlab
Hash[(@nodes || {}).map { |key, factory| [key, factory.dup] }]
end
- private # rubocop:disable Lint/UselessAccessModifier
+ private
def entry(key, entry, metadata)
factory = Entry::Factory.new(entry)
diff --git a/lib/gitlab/ci/pipeline/chain/populate.rb b/lib/gitlab/ci/pipeline/chain/populate.rb
index 69b8a8fc68f..f34c11ca3c2 100644
--- a/lib/gitlab/ci/pipeline/chain/populate.rb
+++ b/lib/gitlab/ci/pipeline/chain/populate.rb
@@ -8,6 +8,9 @@ module Gitlab
PopulateError = Class.new(StandardError)
def perform!
+ # Allocate next IID. This operation must be outside of transactions of pipeline creations.
+ pipeline.ensure_project_iid!
+
##
# Populate pipeline with block argument of CreatePipelineService#execute.
#
diff --git a/lib/gitlab/ci/pipeline/preloader.rb b/lib/gitlab/ci/pipeline/preloader.rb
index e7a2e5511cf..db0a1ea4dab 100644
--- a/lib/gitlab/ci/pipeline/preloader.rb
+++ b/lib/gitlab/ci/pipeline/preloader.rb
@@ -5,23 +5,47 @@ module Gitlab
module Pipeline
# Class for preloading data associated with pipelines such as commit
# authors.
- module Preloader
- def self.preload(pipelines)
- # This ensures that all the pipeline commits are eager loaded before we
- # start using them.
+ class Preloader
+ def self.preload!(pipelines)
+ ##
+ # This preloads all commits at once, because `Ci::Pipeline#commit` is
+ # using a lazy batch loading, what results in only one batched Gitaly
+ # call.
+ #
pipelines.each(&:commit)
pipelines.each do |pipeline|
- # This preloads the author of every commit. We're using "lazy_author"
- # here since "author" immediately loads the data on the first call.
- pipeline.commit.try(:lazy_author)
-
- # This preloads the number of warnings for every pipeline, ensuring
- # that Ci::Pipeline#has_warnings? doesn't execute any additional
- # queries.
- pipeline.number_of_warnings
+ self.new(pipeline).tap do |preloader|
+ preloader.preload_commit_authors
+ preloader.preload_pipeline_warnings
+ preloader.preload_stages_warnings
+ end
end
end
+
+ def initialize(pipeline)
+ @pipeline = pipeline
+ end
+
+ def preload_commit_authors
+ # This also preloads the author of every commit. We're using "lazy_author"
+ # here since "author" immediately loads the data on the first call.
+ @pipeline.commit.try(:lazy_author)
+ end
+
+ def preload_pipeline_warnings
+ # This preloads the number of warnings for every pipeline, ensuring
+ # that Ci::Pipeline#has_warnings? doesn't execute any additional
+ # queries.
+ @pipeline.number_of_warnings
+ end
+
+ def preload_stages_warnings
+ # This preloads the number of warnings for every stage, ensuring
+ # that Ci::Stage#has_warnings? doesn't execute any additional
+ # queries.
+ @pipeline.stages.each { |stage| stage.number_of_warnings }
+ end
end
end
end
diff --git a/lib/gitlab/ci/status/stage/common.rb b/lib/gitlab/ci/status/stage/common.rb
index bc99d925347..f60a7662075 100644
--- a/lib/gitlab/ci/status/stage/common.rb
+++ b/lib/gitlab/ci/status/stage/common.rb
@@ -8,7 +8,9 @@ module Gitlab
end
def details_path
- project_pipeline_path(subject.project, subject.pipeline, anchor: subject.name)
+ project_pipeline_path(subject.pipeline.project,
+ subject.pipeline,
+ anchor: subject.name)
end
def has_action?
diff --git a/lib/gitlab/ci/trace.rb b/lib/gitlab/ci/trace.rb
index fe15fabc2e8..ee54b893598 100644
--- a/lib/gitlab/ci/trace.rb
+++ b/lib/gitlab/ci/trace.rb
@@ -1,7 +1,12 @@
module Gitlab
module Ci
class Trace
+ include ExclusiveLeaseGuard
+
+ LEASE_TIMEOUT = 1.hour
+
ArchiveError = Class.new(StandardError)
+ AlreadyArchivedError = Class.new(StandardError)
attr_reader :job
@@ -77,7 +82,9 @@ module Gitlab
def write(mode)
stream = Gitlab::Ci::Trace::Stream.new do
- if current_path
+ if trace_artifact
+ raise AlreadyArchivedError, 'Could not write to the archived trace'
+ elsif current_path
File.open(current_path, mode)
elsif Feature.enabled?('ci_enable_live_trace')
Gitlab::Ci::Trace::ChunkedIO.new(job)
@@ -94,18 +101,29 @@ module Gitlab
end
def erase!
- trace_artifact&.destroy
+ ##
+ # Erase the archived trace
+ trace_artifact&.destroy!
+
+ ##
+ # Erase the live trace
+ job.trace_chunks.fast_destroy_all # Destroy chunks of a live trace
+ FileUtils.rm_f(current_path) if current_path # Remove a trace file of a live trace
+ job.erase_old_trace! if job.has_old_trace? # Remove a trace in database of a live trace
+ ensure
+ @current_path = nil
+ end
- paths.each do |trace_path|
- FileUtils.rm(trace_path, force: true)
+ def archive!
+ try_obtain_lease do
+ unsafe_archive!
end
-
- job.trace_chunks.fast_destroy_all
- job.erase_old_trace!
end
- def archive!
- raise ArchiveError, 'Already archived' if trace_artifact
+ private
+
+ def unsafe_archive!
+ raise AlreadyArchivedError, 'Could not archive again' if trace_artifact
raise ArchiveError, 'Job is not finished yet' unless job.complete?
if job.trace_chunks.any?
@@ -126,8 +144,6 @@ module Gitlab
end
end
- private
-
def archive_stream!(stream)
clone_file!(stream, JobArtifactUploader.workhorse_upload_path) do |clone_path|
create_build_trace!(job, clone_path)
@@ -206,6 +222,16 @@ module Gitlab
def trace_artifact
job.job_artifacts_trace
end
+
+ # For ExclusiveLeaseGuard concern
+ def lease_key
+ @lease_key ||= "trace:archive:#{job.id}"
+ end
+
+ # For ExclusiveLeaseGuard concern
+ def lease_timeout
+ LEASE_TIMEOUT
+ end
end
end
end
diff --git a/lib/gitlab/ci/trace/http_io.rb b/lib/gitlab/ci/trace/http_io.rb
deleted file mode 100644
index cff924e27ef..00000000000
--- a/lib/gitlab/ci/trace/http_io.rb
+++ /dev/null
@@ -1,197 +0,0 @@
-##
-# This class is compatible with IO class (https://ruby-doc.org/core-2.3.1/IO.html)
-# source: https://gitlab.com/snippets/1685610
-module Gitlab
- module Ci
- class Trace
- class HttpIO
- BUFFER_SIZE = 128.kilobytes
-
- InvalidURLError = Class.new(StandardError)
- FailedToGetChunkError = Class.new(StandardError)
-
- attr_reader :uri, :size
- attr_reader :tell
- attr_reader :chunk, :chunk_range
-
- alias_method :pos, :tell
-
- def initialize(url, size)
- raise InvalidURLError unless ::Gitlab::UrlSanitizer.valid?(url)
-
- @uri = URI(url)
- @size = size
- @tell = 0
- end
-
- def close
- # no-op
- end
-
- def binmode
- # no-op
- end
-
- def binmode?
- true
- end
-
- def path
- nil
- end
-
- def url
- @uri.to_s
- end
-
- def seek(pos, where = IO::SEEK_SET)
- new_pos =
- case where
- when IO::SEEK_END
- size + pos
- when IO::SEEK_SET
- pos
- when IO::SEEK_CUR
- tell + pos
- else
- -1
- end
-
- raise 'new position is outside of file' if new_pos < 0 || new_pos > size
-
- @tell = new_pos
- end
-
- def eof?
- tell == size
- end
-
- def each_line
- until eof?
- line = readline
- break if line.nil?
-
- yield(line)
- end
- end
-
- def read(length = nil, outbuf = "")
- out = ""
-
- length ||= size - tell
-
- until length <= 0 || eof?
- data = get_chunk
- break if data.empty?
-
- chunk_bytes = [BUFFER_SIZE - chunk_offset, length].min
- chunk_data = data.byteslice(0, chunk_bytes)
-
- out << chunk_data
- @tell += chunk_data.bytesize
- length -= chunk_data.bytesize
- end
-
- # If outbuf is passed, we put the output into the buffer. This supports IO.copy_stream functionality
- if outbuf
- outbuf.slice!(0, outbuf.bytesize)
- outbuf << out
- end
-
- out
- end
-
- def readline
- out = ""
-
- until eof?
- data = get_chunk
- new_line = data.index("\n")
-
- if !new_line.nil?
- out << data[0..new_line]
- @tell += new_line + 1
- break
- else
- out << data
- @tell += data.bytesize
- end
- end
-
- out
- end
-
- def write(data)
- raise NotImplementedError
- end
-
- def truncate(offset)
- raise NotImplementedError
- end
-
- def flush
- raise NotImplementedError
- end
-
- def present?
- true
- end
-
- private
-
- ##
- # The below methods are not implemented in IO class
- #
- def in_range?
- @chunk_range&.include?(tell)
- end
-
- def get_chunk
- unless in_range?
- response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
- http.request(request)
- end
-
- raise FailedToGetChunkError unless response.code == '200' || response.code == '206'
-
- @chunk = response.body.force_encoding(Encoding::BINARY)
- @chunk_range = response.content_range
-
- ##
- # Note: If provider does not return content_range, then we set it as we requested
- # Provider: minio
- # - When the file size is larger than requested Content-range, the Content-range is included in responces with Net::HTTPPartialContent 206
- # - When the file size is smaller than requested Content-range, the Content-range is included in responces with Net::HTTPPartialContent 206
- # Provider: AWS
- # - When the file size is larger than requested Content-range, the Content-range is included in responces with Net::HTTPPartialContent 206
- # - When the file size is smaller than requested Content-range, the Content-range is included in responces with Net::HTTPPartialContent 206
- # Provider: GCS
- # - When the file size is larger than requested Content-range, the Content-range is included in responces with Net::HTTPPartialContent 206
- # - When the file size is smaller than requested Content-range, the Content-range is included in responces with Net::HTTPOK 200
- @chunk_range ||= (chunk_start...(chunk_start + @chunk.bytesize))
- end
-
- @chunk[chunk_offset..BUFFER_SIZE]
- end
-
- def request
- Net::HTTP::Get.new(uri).tap do |request|
- request.set_range(chunk_start, BUFFER_SIZE)
- end
- end
-
- def chunk_offset
- tell % BUFFER_SIZE
- end
-
- def chunk_start
- (tell / BUFFER_SIZE) * BUFFER_SIZE
- end
-
- def chunk_end
- [chunk_start + BUFFER_SIZE, size].min
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/trace/section_parser.rb b/lib/gitlab/ci/trace/section_parser.rb
index 9bb0166c9e3..c09089d6475 100644
--- a/lib/gitlab/ci/trace/section_parser.rb
+++ b/lib/gitlab/ci/trace/section_parser.rb
@@ -75,19 +75,19 @@ module Gitlab
@beginning_of_section_regex ||= /section_/.freeze
end
- def find_next_marker(s)
+ def find_next_marker(scanner)
beginning_of_section_len = 8
- maybe_marker = s.exist?(beginning_of_section_regex)
+ maybe_marker = scanner.exist?(beginning_of_section_regex)
if maybe_marker.nil?
- s.terminate
+ scanner.terminate
else
# repositioning at the beginning of the match
- s.pos += maybe_marker - beginning_of_section_len
+ scanner.pos += maybe_marker - beginning_of_section_len
if block_given?
- good_marker = yield(s)
+ good_marker = yield(scanner)
# if not a good marker: Consuming the matched beginning_of_section_regex
- s.pos += beginning_of_section_len unless good_marker
+ scanner.pos += beginning_of_section_len unless good_marker
end
end
end
diff --git a/lib/gitlab/ci/variables/collection/item.rb b/lib/gitlab/ci/variables/collection/item.rb
index d00e5b07f95..222aa06b800 100644
--- a/lib/gitlab/ci/variables/collection/item.rb
+++ b/lib/gitlab/ci/variables/collection/item.rb
@@ -4,6 +4,9 @@ module Gitlab
class Collection
class Item
def initialize(key:, value:, public: true, file: false)
+ raise ArgumentError, "`value` must be of type String, while it was: #{value.class}" unless
+ value.is_a?(String) || value.nil?
+
@variable = {
key: key, value: value, public: public, file: file
}
diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb
index d7369060cc5..4c28489f45a 100644
--- a/lib/gitlab/contributions_calendar.rb
+++ b/lib/gitlab/contributions_calendar.rb
@@ -85,7 +85,7 @@ module Gitlab
.select(t[:project_id], t[:target_type], t[:action], "date(created_at + #{date_interval}) AS date", 'count(id) as total_amount')
.group(t[:project_id], t[:target_type], t[:action], "date(created_at + #{date_interval})")
.where(conditions)
- .having(t[:project_id].in(Arel::Nodes::SqlLiteral.new(authed_projects.to_sql)))
+ .where("events.project_id in (#{authed_projects.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
end
end
end
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index 6cf7aa1bf0d..9147ef401da 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -24,7 +24,22 @@ module Gitlab
private
def ensure_application_settings!
+ cached_application_settings || uncached_application_settings
+ end
+
+ def cached_application_settings
return in_memory_application_settings if ENV['IN_MEMORY_APPLICATION_SETTINGS'] == 'true'
+
+ begin
+ ::ApplicationSetting.cached
+ rescue
+ # In case Redis isn't running
+ # or the Redis UNIX socket file is not available
+ # or the DB is not running (we use migrations in the cache key)
+ end
+ end
+
+ def uncached_application_settings
return fake_application_settings unless connect_to_db?
current_settings = ::ApplicationSetting.current
@@ -45,7 +60,7 @@ module Gitlab
def in_memory_application_settings
with_fallback_to_fake_application_settings do
- @in_memory_application_settings ||= ::ApplicationSetting.build_from_defaults # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ @in_memory_application_settings ||= ::ApplicationSetting.build_from_defaults
end
end
diff --git a/lib/gitlab/cycle_analytics/summary/commit.rb b/lib/gitlab/cycle_analytics/summary/commit.rb
index bea78862757..550c1755a71 100644
--- a/lib/gitlab/cycle_analytics/summary/commit.rb
+++ b/lib/gitlab/cycle_analytics/summary/commit.rb
@@ -19,19 +19,11 @@ module Gitlab
def count_commits
return unless ref
- repository = @project.repository.raw_repository
- sha = @project.repository.commit(ref).sha
-
- cmd = %W(git --git-dir=#{repository.path} log)
- cmd << '--format=%H'
- cmd << "--after=#{@from.iso8601}"
- cmd << sha
-
- output, status = Gitlab::Popen.popen(cmd)
-
- raise IOError, output unless status.zero?
+ gitaly_commit_client.commit_count(ref, after: @from)
+ end
- output.lines.count
+ def gitaly_commit_client
+ Gitlab::GitalyClient::CommitService.new(@project.repository.raw_repository)
end
def ref
diff --git a/lib/gitlab/data_builder/pipeline.rb b/lib/gitlab/data_builder/pipeline.rb
index 1e283cc092b..eb246d393a1 100644
--- a/lib/gitlab/data_builder/pipeline.rb
+++ b/lib/gitlab/data_builder/pipeline.rb
@@ -55,7 +55,7 @@ module Gitlab
id: runner.id,
description: runner.description,
active: runner.active?,
- is_shared: runner.is_shared?
+ is_shared: runner.instance_type?
}
end
end
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
index d49d055c3f2..872e70f9a5d 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -42,6 +42,21 @@ module Gitlab
!self.read_only?
end
+ # check whether the underlying database is in read-only mode
+ def self.db_read_only?
+ if postgresql?
+ ActiveRecord::Base.connection.execute('SELECT pg_is_in_recovery()')
+ .first
+ .fetch('pg_is_in_recovery') == 't'
+ else
+ false
+ end
+ end
+
+ def self.db_read_write?
+ !self.db_read_only?
+ end
+
def self.version
@version ||= database_version.match(/\A(?:PostgreSQL |)([^\s]+).*\z/)[1]
end
@@ -188,8 +203,11 @@ module Gitlab
end
def self.cached_table_exists?(table_name)
- # Rails 5 uses data_source_exists? instead of table_exists?
- connection.schema_cache.table_exists?(table_name)
+ if Gitlab.rails5?
+ connection.schema_cache.data_source_exists?(table_name)
+ else
+ connection.schema_cache.table_exists?(table_name)
+ end
end
private_class_method :connection
diff --git a/lib/gitlab/database/count.rb b/lib/gitlab/database/count.rb
index 3374203960e..5f549ed2b3c 100644
--- a/lib/gitlab/database/count.rb
+++ b/lib/gitlab/database/count.rb
@@ -17,31 +17,69 @@ module Gitlab
].freeze
end
- def self.approximate_count(model)
- return model.count unless Gitlab::Database.postgresql?
+ # Takes in an array of models and returns a Hash for the approximate
+ # counts for them. If the model's table has not been vacuumed or
+ # analyzed recently, simply run the Model.count to get the data.
+ #
+ # @param [Array]
+ # @return [Hash] of Model -> count mapping
+ def self.approximate_counts(models)
+ table_to_model_map = models.each_with_object({}) do |model, hash|
+ hash[model.table_name] = model
+ end
- execute_estimate_if_updated_recently(model) || model.count
- end
+ table_names = table_to_model_map.keys
+ counts_by_table_name = Gitlab::Database.postgresql? ? reltuples_from_recently_updated(table_names) : {}
- def self.execute_estimate_if_updated_recently(model)
- ActiveRecord::Base.connection.select_value(postgresql_estimate_query(model)).to_i if reltuples_updated_recently?(model)
- rescue *CONNECTION_ERRORS
+ # Convert table -> count to Model -> count
+ counts_by_model = counts_by_table_name.each_with_object({}) do |pair, hash|
+ model = table_to_model_map[pair.first]
+ hash[model] = pair.second
+ end
+
+ missing_tables = table_names - counts_by_table_name.keys
+
+ missing_tables.each do |table|
+ model = table_to_model_map[table]
+ counts_by_model[model] = model.count
+ end
+
+ counts_by_model
end
- def self.reltuples_updated_recently?(model)
- time = "to_timestamp(#{1.hour.ago.to_i})"
- query = <<~SQL
- SELECT 1 FROM pg_stat_user_tables WHERE relname = '#{model.table_name}' AND
- (last_vacuum > #{time} OR last_autovacuum > #{time} OR last_analyze > #{time} OR last_autoanalyze > #{time})
- SQL
+ # Returns a hash of the table names that have recently updated tuples.
+ #
+ # @param [Array] table names
+ # @returns [Hash] Table name to count mapping (e.g. { 'projects' => 5, 'users' => 100 })
+ def self.reltuples_from_recently_updated(table_names)
+ query = postgresql_estimate_query(table_names)
+ rows = []
- ActiveRecord::Base.connection.select_all(query).count > 0
+ # Querying tuple stats only works on the primary. Due to load
+ # balancing, we need to ensure this query hits the load balancer. The
+ # easiest way to do this is to start a transaction.
+ ActiveRecord::Base.transaction do
+ rows = ActiveRecord::Base.connection.select_all(query)
+ end
+
+ rows.each_with_object({}) { |row, data| data[row['table_name']] = row['estimate'].to_i }
rescue *CONNECTION_ERRORS
- false
+ {}
end
- def self.postgresql_estimate_query(model)
- "SELECT reltuples::bigint AS estimate FROM pg_class where relname = '#{model.table_name}'"
+ # Generates the PostgreSQL query to return the tuples for tables
+ # that have been vacuumed or analyzed in the last hour.
+ #
+ # @param [Array] table names
+ # @returns [Hash] Table name to count mapping (e.g. { 'projects' => 5, 'users' => 100 })
+ def self.postgresql_estimate_query(table_names)
+ time = "to_timestamp(#{1.hour.ago.to_i})"
+ <<~SQL
+ SELECT pg_class.relname AS table_name, reltuples::bigint AS estimate FROM pg_class
+ LEFT JOIN pg_stat_user_tables ON pg_class.relname = pg_stat_user_tables.relname
+ WHERE pg_class.relname IN (#{table_names.map { |table| "'#{table}'" }.join(',')})
+ AND (last_vacuum > #{time} OR last_autovacuum > #{time} OR last_analyze > #{time} OR last_autoanalyze > #{time})
+ SQL
end
end
end
diff --git a/lib/gitlab/database/median.rb b/lib/gitlab/database/median.rb
index 74fed447289..f64e3d53138 100644
--- a/lib/gitlab/database/median.rb
+++ b/lib/gitlab/database/median.rb
@@ -33,7 +33,13 @@ module Gitlab
end
def mysql_median_datetime_sql(arel_table, query_so_far, column_sym)
- query = arel_table
+ arel_from = if Gitlab.rails5?
+ arel_table.from
+ else
+ arel_table
+ end
+
+ query = arel_from
.from(arel_table.project(Arel.sql('*')).order(arel_table[column_sym]).as(arel_table.table_name))
.project(average([arel_table[column_sym]], 'median'))
.where(
@@ -143,8 +149,13 @@ module Gitlab
.order(arel_table[column_sym])
).as('row_id')
- count = arel_table.from(arel_table.alias)
- .project('COUNT(*)')
+ arel_from = if Gitlab.rails5?
+ arel_table.from.from(arel_table.alias)
+ else
+ arel_table.from(arel_table.alias)
+ end
+
+ count = arel_from.project('COUNT(*)')
.where(arel_table[partition_column].eq(arel_table.alias[partition_column]))
.as('ct')
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index c21bae5e16b..4fe5b4cc835 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -596,6 +596,97 @@ module Gitlab
end
end
+ # Renames a column using a background migration.
+ #
+ # Because this method uses a background migration it's more suitable for
+ # large tables. For small tables it's better to use
+ # `rename_column_concurrently` since it can complete its work in a much
+ # shorter amount of time and doesn't rely on Sidekiq.
+ #
+ # Example usage:
+ #
+ # rename_column_using_background_migration(
+ # :users,
+ # :feed_token,
+ # :rss_token
+ # )
+ #
+ # table - The name of the database table containing the column.
+ #
+ # old - The old column name.
+ #
+ # new - The new column name.
+ #
+ # type - The type of the new column. If no type is given the old column's
+ # type is used.
+ #
+ # batch_size - The number of rows to schedule in a single background
+ # migration.
+ #
+ # interval - The time interval between every background migration.
+ def rename_column_using_background_migration(
+ table,
+ old_column,
+ new_column,
+ type: nil,
+ batch_size: 10_000,
+ interval: 10.minutes
+ )
+
+ check_trigger_permissions!(table)
+
+ old_col = column_for(table, old_column)
+ new_type = type || old_col.type
+ max_index = 0
+
+ add_column(table, new_column, new_type,
+ limit: old_col.limit,
+ precision: old_col.precision,
+ scale: old_col.scale)
+
+ # We set the default value _after_ adding the column so we don't end up
+ # updating any existing data with the default value. This isn't
+ # necessary since we copy over old values further down.
+ change_column_default(table, new_column, old_col.default) if old_col.default
+
+ install_rename_triggers(table, old_column, new_column)
+
+ model = Class.new(ActiveRecord::Base) do
+ self.table_name = table
+
+ include ::EachBatch
+ end
+
+ # Schedule the jobs that will copy the data from the old column to the
+ # new one. Rows with NULL values in our source column are skipped since
+ # the target column is already NULL at this point.
+ model.where.not(old_column => nil).each_batch(of: batch_size) do |batch, index|
+ start_id, end_id = batch.pluck('MIN(id), MAX(id)').first
+ max_index = index
+
+ BackgroundMigrationWorker.perform_in(
+ index * interval,
+ 'CopyColumn',
+ [table, old_column, new_column, start_id, end_id]
+ )
+ end
+
+ # Schedule the renaming of the column to happen (initially) 1 hour after
+ # the last batch finished.
+ BackgroundMigrationWorker.perform_in(
+ (max_index * interval) + 1.hour,
+ 'CleanupConcurrentRename',
+ [table, old_column, new_column]
+ )
+
+ if perform_background_migration_inline?
+ # To ensure the schema is up to date immediately we perform the
+ # migration inline in dev / test environments.
+ Gitlab::BackgroundMigration.steal('CopyColumn')
+ Gitlab::BackgroundMigration.steal('CleanupConcurrentRename')
+ end
+ end
+
def perform_background_migration_inline?
Rails.env.test? || Rails.env.development?
end
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 62d4d0a92a6..26ae6966746 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
@@ -37,6 +37,7 @@ module Gitlab
class Namespace < ActiveRecord::Base
include MigrationClasses::Routable
self.table_name = 'namespaces'
+ self.inheritance_column = :_type_disabled
belongs_to :parent,
class_name: "#{MigrationClasses.name}::Namespace"
has_one :route, as: :source
diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb
index 014854da55c..d16a55720b7 100644
--- a/lib/gitlab/diff/file.rb
+++ b/lib/gitlab/diff/file.rb
@@ -76,6 +76,16 @@ module Gitlab
line_code(line) if line
end
+ # Returns the raw diff content up to the given line index
+ def diff_hunk(diff_line)
+ diff_line_index = diff_line.index
+ # @@ (match) header is not kept if it's found in the top of the file,
+ # therefore we should keep an extra line on this scenario.
+ diff_line_index += 1 unless diff_lines.first.match?
+
+ diff_lines.select { |line| line.index <= diff_line_index }.map(&:text).join("\n")
+ end
+
def old_sha
diff_refs&.base_sha
end
@@ -120,11 +130,13 @@ module Gitlab
# Array of Gitlab::Diff::Line objects
def diff_lines
- @diff_lines ||= Gitlab::Diff::Parser.new.parse(raw_diff.each_line).to_a
+ @diff_lines ||=
+ Gitlab::Diff::Parser.new.parse(raw_diff.each_line, diff_file: self).to_a
end
def highlighted_diff_lines
- @highlighted_diff_lines ||= Gitlab::Diff::Highlight.new(self, repository: self.repository).highlight
+ @highlighted_diff_lines ||=
+ Gitlab::Diff::Highlight.new(self, repository: self.repository).highlight
end
# Array[<Hash>] with right/left keys that contains Gitlab::Diff::Line objects which text is hightlighted
@@ -229,8 +241,34 @@ module Gitlab
simple_viewer.is_a?(DiffViewer::Text) && (ignore_errors || simple_viewer.render_error.nil?)
end
+ # This adds the bottom match line to the array if needed. It contains
+ # the data to load more context lines.
+ def diff_lines_for_serializer
+ lines = highlighted_diff_lines
+
+ return if lines.empty?
+ return if blob.nil?
+
+ last_line = lines.last
+
+ if last_line.new_pos < total_blob_lines(blob) && !deleted_file?
+ match_line = Gitlab::Diff::Line.new("", 'match', nil, last_line.old_pos, last_line.new_pos)
+ lines.push(match_line)
+ end
+
+ lines
+ end
+
private
+ def total_blob_lines(blob)
+ @total_lines ||= begin
+ line_count = blob.lines.size
+ line_count -= 1 if line_count > 0 && blob.lines.last.blank?
+ line_count
+ end
+ end
+
# We can't use Object#try because Blob doesn't inherit from Object, but
# from BasicObject (via SimpleDelegator).
def try_blobs(meth)
diff --git a/lib/gitlab/diff/file_collection/merge_request_diff.rb b/lib/gitlab/diff/file_collection/merge_request_diff.rb
index c358ae428cf..be25e1bab21 100644
--- a/lib/gitlab/diff/file_collection/merge_request_diff.rb
+++ b/lib/gitlab/diff/file_collection/merge_request_diff.rb
@@ -34,7 +34,7 @@ module Gitlab
end
def cache_key
- [@merge_request_diff, 'highlighted-diff-files', diff_options]
+ [@merge_request_diff, 'highlighted-diff-files', Gitlab::Diff::Line::SERIALIZE_KEYS, diff_options]
end
private
diff --git a/lib/gitlab/diff/image_point.rb b/lib/gitlab/diff/image_point.rb
index 65332dfd239..1f157354ea4 100644
--- a/lib/gitlab/diff/image_point.rb
+++ b/lib/gitlab/diff/image_point.rb
@@ -3,11 +3,11 @@ module Gitlab
class ImagePoint
attr_reader :width, :height, :x, :y
- def initialize(width, height, x, y)
+ def initialize(width, height, new_x, new_y)
@width = width
@height = height
- @x = x
- @y = y
+ @x = new_x
+ @y = new_y
end
def to_h
diff --git a/lib/gitlab/diff/inline_diff.rb b/lib/gitlab/diff/inline_diff.rb
index 54783a07919..99970779c67 100644
--- a/lib/gitlab/diff/inline_diff.rb
+++ b/lib/gitlab/diff/inline_diff.rb
@@ -93,7 +93,7 @@ module Gitlab
private
- def longest_common_prefix(a, b)
+ def longest_common_prefix(a, b) # rubocop:disable Naming/UncommunicativeMethodParamName
max_length = [a.length, b.length].max
length = 0
@@ -109,7 +109,7 @@ module Gitlab
length
end
- def longest_common_suffix(a, b)
+ def longest_common_suffix(a, b) # rubocop:disable Naming/UncommunicativeMethodParamName
longest_common_prefix(a.reverse, b.reverse)
end
end
diff --git a/lib/gitlab/diff/line.rb b/lib/gitlab/diff/line.rb
index 0603141e441..1faf7770634 100644
--- a/lib/gitlab/diff/line.rb
+++ b/lib/gitlab/diff/line.rb
@@ -1,27 +1,29 @@
module Gitlab
module Diff
class Line
- attr_reader :type, :index, :old_pos, :new_pos
+ SERIALIZE_KEYS = %i(line_code text type index old_pos new_pos).freeze
+
+ attr_reader :line_code, :type, :index, :old_pos, :new_pos
attr_writer :rich_text
attr_accessor :text
- def initialize(text, type, index, old_pos, new_pos, parent_file: nil)
+ def initialize(text, type, index, old_pos, new_pos, parent_file: nil, line_code: nil)
@text, @type, @index = text, type, index
@old_pos, @new_pos = old_pos, new_pos
@parent_file = parent_file
- end
- def self.init_from_hash(hash)
- new(hash[:text], hash[:type], hash[:index], hash[:old_pos], hash[:new_pos])
+ # When line code is not provided from cache store we build it
+ # using the parent_file(Diff::File or Conflict::File).
+ @line_code = line_code || calculate_line_code
end
- def serialize_keys
- @serialize_keys ||= %i(text type index old_pos new_pos)
+ def self.init_from_hash(hash)
+ new(hash[:text], hash[:type], hash[:index], hash[:old_pos], hash[:new_pos], line_code: hash[:line_code])
end
def to_hash
hash = {}
- serialize_keys.each { |key| hash[key] = send(key) } # rubocop:disable GitlabSecurity/PublicSend
+ SERIALIZE_KEYS.each { |key| hash[key] = send(key) } # rubocop:disable GitlabSecurity/PublicSend
hash
end
@@ -53,25 +55,46 @@ module Gitlab
%w[match new-nonewline old-nonewline].include?(type)
end
+ def match?
+ type == :match
+ end
+
def discussable?
!meta?
end
def rich_text
- @parent_file.highlight_lines! if @parent_file && !@rich_text
+ @parent_file.try(:highlight_lines!) if @parent_file && !@rich_text
@rich_text
end
+ def meta_positions
+ return unless meta?
+
+ {
+ old_pos: old_pos,
+ new_pos: new_pos
+ }
+ end
+
def as_json(opts = nil)
{
+ line_code: line_code,
type: type,
old_line: old_line,
new_line: new_line,
text: text,
- rich_text: rich_text || text
+ rich_text: rich_text || text,
+ meta_data: meta_positions
}
end
+
+ private
+
+ def calculate_line_code
+ @parent_file&.line_code(self)
+ end
end
end
end
diff --git a/lib/gitlab/diff/parser.rb b/lib/gitlab/diff/parser.rb
index 8302f30a0a2..7ae7ed286ed 100644
--- a/lib/gitlab/diff/parser.rb
+++ b/lib/gitlab/diff/parser.rb
@@ -3,7 +3,7 @@ module Gitlab
class Parser
include Enumerable
- def parse(lines)
+ def parse(lines, diff_file: nil)
return [] if lines.blank?
@lines = lines
@@ -31,17 +31,17 @@ module Gitlab
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)
+ yielder << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new, parent_file: diff_file)
line_obj_index += 1
next
elsif line[0] == '\\'
type = "#{context}-nonewline"
- yielder << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new)
+ yielder << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new, parent_file: diff_file)
line_obj_index += 1
else
type = identification_type(line)
- yielder << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new)
+ yielder << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new, parent_file: diff_file)
line_obj_index += 1
end
diff --git a/lib/gitlab/ee_compat_check.rb b/lib/gitlab/ee_compat_check.rb
index 8cf59fa8e28..ee604e66154 100644
--- a/lib/gitlab/ee_compat_check.rb
+++ b/lib/gitlab/ee_compat_check.rb
@@ -138,15 +138,23 @@ module Gitlab
def ee_branch_presence_check!
ee_remotes.keys.each do |remote|
- [ee_branch_prefix, ee_branch_suffix].each do |branch|
- _, status = step("Fetching #{remote}/#{ee_branch_prefix}", %W[git fetch #{remote} #{branch}])
+ output, _ = step(
+ "Searching #{remote}",
+ %W[git ls-remote #{remote} *#{minimal_ee_branch_name}*])
- if status.zero?
- @ee_remote_with_branch = remote
- @ee_branch_found = branch
- return true
- end
- end
+ branches =
+ output.scan(%r{(?<=refs/heads/|refs/tags/).+}).sort_by(&:size)
+
+ next if branches.empty?
+
+ branch = branches.first
+
+ step("Fetching #{remote}/#{branch}", %W[git fetch #{remote} #{branch}])
+
+ @ee_remote_with_branch = remote
+ @ee_branch_found = branch
+
+ return true
end
puts
@@ -271,6 +279,10 @@ module Gitlab
@ee_patch_full_path ||= patches_dir.join(ee_patch_name)
end
+ def minimal_ee_branch_name
+ @minimal_ee_branch_name ||= ce_branch.sub(/(\Ace\-|\-ce\z)/, '')
+ end
+
def patch_name_from_branch(branch_name)
branch_name.parameterize << '.patch'
end
diff --git a/lib/gitlab/encoding_helper.rb b/lib/gitlab/encoding_helper.rb
index 0b8f6cfe3cb..d1fd5dfe0cb 100644
--- a/lib/gitlab/encoding_helper.rb
+++ b/lib/gitlab/encoding_helper.rb
@@ -65,17 +65,17 @@ module Gitlab
clean(message)
end
rescue ArgumentError
- return nil
+ nil
end
- def encode_binary(s)
- return "" if s.nil?
+ def encode_binary(str)
+ return "" if str.nil?
- s.dup.force_encoding(Encoding::ASCII_8BIT)
+ str.dup.force_encoding(Encoding::ASCII_8BIT)
end
- def binary_stringio(s)
- StringIO.new(s || '').tap { |io| io.set_encoding(Encoding::ASCII_8BIT) }
+ def binary_stringio(str)
+ StringIO.new(str || '').tap { |io| io.set_encoding(Encoding::ASCII_8BIT) }
end
private
diff --git a/lib/gitlab/exclusive_lease_helpers.rb b/lib/gitlab/exclusive_lease_helpers.rb
new file mode 100644
index 00000000000..e998548cff9
--- /dev/null
+++ b/lib/gitlab/exclusive_lease_helpers.rb
@@ -0,0 +1,29 @@
+module Gitlab
+ # This module provides helper methods which are intregrated with GitLab::ExclusiveLease
+ module ExclusiveLeaseHelpers
+ FailedToObtainLockError = Class.new(StandardError)
+
+ ##
+ # This helper method blocks a process/thread until the other process cancel the obrainted lease key.
+ #
+ # Note: It's basically discouraged to use this method in the unicorn's thread,
+ # because it holds the connection until all `retries` is consumed.
+ # This could potentially eat up all connection pools.
+ def in_lock(key, ttl: 1.minute, retries: 10, sleep_sec: 0.01.seconds)
+ lease = Gitlab::ExclusiveLease.new(key, timeout: ttl)
+
+ until uuid = lease.try_obtain
+ # Keep trying until we obtain the lease. To prevent hammering Redis too
+ # much we'll wait for a bit.
+ sleep(sleep_sec)
+ break if (retries -= 1) < 0
+ end
+
+ raise FailedToObtainLockError, 'Failed to obtain a lock' unless uuid
+
+ yield
+ ensure
+ Gitlab::ExclusiveLease.cancel(key, uuid)
+ end
+ end
+end
diff --git a/lib/gitlab/favicon.rb b/lib/gitlab/favicon.rb
new file mode 100644
index 00000000000..4850a6c0430
--- /dev/null
+++ b/lib/gitlab/favicon.rb
@@ -0,0 +1,58 @@
+module Gitlab
+ class Favicon
+ class << self
+ def main
+ image_name =
+ if appearance_favicon.exists?
+ appearance_favicon.url
+ elsif Gitlab::Utils.to_boolean(ENV['CANARY'])
+ 'favicon-yellow.png'
+ elsif Rails.env.development?
+ 'favicon-blue.png'
+ else
+ 'favicon.png'
+ end
+
+ ActionController::Base.helpers.image_path(image_name, host: host)
+ end
+
+ def status_overlay(status_name)
+ path = File.join(
+ 'ci_favicons',
+ "#{status_name}.png"
+ )
+
+ ActionController::Base.helpers.image_path(path, host: host)
+ end
+
+ def available_status_names
+ @available_status_names ||= begin
+ Dir.glob(Rails.root.join('app', 'assets', 'images', 'ci_favicons', '*.png'))
+ .map { |file| File.basename(file, '.png') }
+ .sort
+ end
+ end
+
+ private
+
+ # we only want to create full urls when there's a different asset_host
+ # configured.
+ def host
+ asset_host = ActionController::Base.asset_host
+ if asset_host.nil? || asset_host == Gitlab.config.gitlab.base_url
+ nil
+ else
+ Gitlab.config.gitlab.base_url
+ end
+ end
+
+ def appearance
+ RequestStore.store[:appearance] ||= (Appearance.current || Appearance.new)
+ end
+
+ def appearance_favicon
+ appearance.favicon
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/file_finder.rb b/lib/gitlab/file_finder.rb
index 8c082c0c336..af8270c8db8 100644
--- a/lib/gitlab/file_finder.rb
+++ b/lib/gitlab/file_finder.rb
@@ -14,14 +14,21 @@ module Gitlab
end
def find(query)
- by_content = find_by_content(query)
+ query = Gitlab::Search::Query.new(query) do
+ filter :filename, matcher: ->(filter, blob) { blob.filename =~ /#{filter[:regex_value]}$/i }
+ filter :path, matcher: ->(filter, blob) { blob.filename =~ /#{filter[:regex_value]}/i }
+ filter :extension, matcher: ->(filter, blob) { blob.filename =~ /\.#{filter[:regex_value]}$/i }
+ end
+
+ by_content = find_by_content(query.term)
already_found = Set.new(by_content.map(&:filename))
- by_filename = find_by_filename(query, except: already_found)
+ by_filename = find_by_filename(query.term, except: already_found)
- (by_content + by_filename)
- .sort_by(&:filename)
- .map { |blob| [blob.filename, blob] }
+ files = (by_content + by_filename)
+ .sort_by(&:filename)
+
+ query.filter_results(files).map { |blob| [blob.filename, blob] }
end
private
@@ -32,17 +39,13 @@ module Gitlab
end
def find_by_filename(query, except: [])
- filenames = repository.search_files_by_name(query, ref).first(BATCH_SIZE)
- filenames.delete_if { |filename| except.include?(filename) } unless except.empty?
-
- blob_refs = filenames.map { |filename| [ref, filename] }
- blobs = Gitlab::Git::Blob.batch(repository, blob_refs, blob_size_limit: 1024)
+ filenames = search_filenames(query, except)
- blobs.map do |blob|
+ blobs(filenames).map do |blob|
Gitlab::SearchResults::FoundBlob.new(
id: blob.id,
filename: blob.path,
- basename: File.basename(blob.path),
+ basename: File.basename(blob.path, File.extname(blob.path)),
ref: ref,
startline: 1,
data: blob.data,
@@ -50,5 +53,21 @@ module Gitlab
)
end
end
+
+ def search_filenames(query, except)
+ filenames = repository.search_files_by_name(query, ref).first(BATCH_SIZE)
+
+ filenames.delete_if { |filename| except.include?(filename) } unless except.empty?
+
+ filenames
+ end
+
+ def blob_refs(filenames)
+ filenames.map { |filename| [ref, filename] }
+ end
+
+ def blobs(filenames)
+ Gitlab::Git::Blob.batch(repository, blob_refs(filenames), blob_size_limit: 1024)
+ end
end
end
diff --git a/lib/gitlab/fogbugz_import/importer.rb b/lib/gitlab/fogbugz_import/importer.rb
index 8953bc8c148..a91de278cf3 100644
--- a/lib/gitlab/fogbugz_import/importer.rb
+++ b/lib/gitlab/fogbugz_import/importer.rb
@@ -191,19 +191,19 @@ module Gitlab
end
end
- def linkify_issues(s)
- s = s.gsub(/([Ii]ssue) ([0-9]+)/, '\1 #\2')
- s = s.gsub(/([Cc]ase) ([0-9]+)/, '\1 #\2')
- s
+ def linkify_issues(str)
+ str = str.gsub(/([Ii]ssue) ([0-9]+)/, '\1 #\2')
+ str = str.gsub(/([Cc]ase) ([0-9]+)/, '\1 #\2')
+ str
end
- def escape_for_markdown(s)
- s = s.gsub(/^#/, "\\#")
- s = s.gsub(/^-/, "\\-")
- s = s.gsub("`", "\\~")
- s = s.delete("\r")
- s = s.gsub("\n", " \n")
- s
+ def escape_for_markdown(str)
+ str = str.gsub(/^#/, "\\#")
+ str = str.gsub(/^-/, "\\-")
+ str = str.gsub("`", "\\~")
+ str = str.delete("\r")
+ str = str.gsub("\n", " \n")
+ str
end
def format_content(raw_content)
diff --git a/lib/gitlab/gfm/uploads_rewriter.rb b/lib/gitlab/gfm/uploads_rewriter.rb
index b6eeb5d9a2b..f7e66697da3 100644
--- a/lib/gitlab/gfm/uploads_rewriter.rb
+++ b/lib/gitlab/gfm/uploads_rewriter.rb
@@ -23,11 +23,8 @@ module Gitlab
file = find_file(@source_project, $~[:secret], $~[:file])
break markdown unless file.try(:exists?)
- new_uploader = FileUploader.new(target_project)
- with_link_in_tmp_dir(file.file) do |open_tmp_file|
- new_uploader.store!(open_tmp_file)
- end
- new_uploader.markdown_link
+ moved = FileUploader.copy_to(file, target_project)
+ moved.markdown_link
end
end
@@ -48,20 +45,7 @@ module Gitlab
def find_file(project, secret, file)
uploader = FileUploader.new(project, secret: secret)
uploader.retrieve_from_store!(file)
- uploader.file
- end
-
- # Because the uploaders use 'move_to_store' we must have a temporary
- # file that is allowed to be (re)moved.
- def with_link_in_tmp_dir(file)
- dir = Dir.mktmpdir('UploadsRewriter', File.dirname(file))
- # The filename matters to Carrierwave so we make sure to preserve it
- tmp_file = File.join(dir, File.basename(file))
- File.link(file, tmp_file)
- # Open the file to placate Carrierwave
- File.open(tmp_file) { |open_file| yield open_file }
- ensure
- FileUtils.rm_rf(dir)
+ uploader
end
end
end
diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb
index e85e87a54af..55236a1122f 100644
--- a/lib/gitlab/git.rb
+++ b/lib/gitlab/git.rb
@@ -62,7 +62,7 @@ module Gitlab
end
def version
- Gitlab::VersionInfo.parse(Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} --version)).first)
+ Gitlab::Git::Version.git_version
end
def check_namespace!(*objects)
diff --git a/lib/gitlab/git/blame.rb b/lib/gitlab/git/blame.rb
index 4158d50cd9e..e25e15f5c80 100644
--- a/lib/gitlab/git/blame.rb
+++ b/lib/gitlab/git/blame.rb
@@ -22,24 +22,9 @@ module Gitlab
private
def load_blame
- raw_output = @repo.gitaly_migrate(:blame) do |is_enabled|
- if is_enabled
- load_blame_by_gitaly
- else
- load_blame_by_shelling_out
- end
- end
-
- output = encode_utf8(raw_output)
- process_raw_blame output
- end
-
- def load_blame_by_gitaly
- @repo.gitaly_commit_client.raw_blame(@sha, @path)
- end
+ output = encode_utf8(@repo.gitaly_commit_client.raw_blame(@sha, @path))
- def load_blame_by_shelling_out
- @repo.shell_blame(@sha, @path)
+ process_raw_blame(output)
end
def process_raw_blame(output)
diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb
index 156d077a69c..71857bd2d87 100644
--- a/lib/gitlab/git/blob.rb
+++ b/lib/gitlab/git/blob.rb
@@ -21,24 +21,36 @@ module Gitlab
attr_accessor :name, :path, :size, :data, :mode, :id, :commit_id, :loaded_size, :binary
class << self
- def find(repository, sha, path)
- Gitlab::GitalyClient.migrate(:project_raw_show) do |is_enabled|
- if is_enabled
- find_by_gitaly(repository, sha, path)
- else
- find_by_rugged(repository, sha, path, limit: MAX_DATA_DISPLAY_SIZE)
- end
+ def find(repository, sha, path, limit: MAX_DATA_DISPLAY_SIZE)
+ return unless path
+
+ path = path.sub(%r{\A/*}, '')
+ path = '/' if path.empty?
+ name = File.basename(path)
+
+ # Gitaly will think that setting the limit to 0 means unlimited, while
+ # the client might only need the metadata and thus set the limit to 0.
+ # In this method we'll then set the limit to 1, but clear the byte of data
+ # that we got back so for the outside world it looks like the limit was
+ # actually 0.
+ req_limit = limit == 0 ? 1 : limit
+
+ entry = Gitlab::GitalyClient::CommitService.new(repository).tree_entry(sha, path, req_limit)
+ return unless entry
+
+ entry.data = "" if limit == 0
+
+ case entry.type
+ when :COMMIT
+ new(id: entry.oid, name: name, size: 0, data: '', path: path, commit_id: sha)
+ when :BLOB
+ new(id: entry.oid, name: name, size: entry.size, data: entry.data.dup, mode: entry.mode.to_s(8),
+ path: path, commit_id: sha, binary: binary?(entry.data))
end
end
def raw(repository, sha)
- Gitlab::GitalyClient.migrate(:git_blob_raw) do |is_enabled|
- if is_enabled
- repository.gitaly_blob_client.get_blob(oid: sha, limit: MAX_DATA_DISPLAY_SIZE)
- else
- rugged_raw(repository, sha, limit: MAX_DATA_DISPLAY_SIZE)
- end
- end
+ repository.gitaly_blob_client.get_blob(oid: sha, limit: MAX_DATA_DISPLAY_SIZE)
end
# Returns an array of Blob instances, specified in blob_references as
@@ -49,17 +61,8 @@ module Gitlab
# Keep in mind that this method may allocate a lot of memory. It is up
# to the caller to limit the number of blobs and blob_size_limit.
#
- # Gitaly migration issue: https://gitlab.com/gitlab-org/gitaly/issues/798
def batch(repository, blob_references, blob_size_limit: MAX_DATA_DISPLAY_SIZE)
- Gitlab::GitalyClient.migrate(:list_blobs_by_sha_path) do |is_enabled|
- if is_enabled
- repository.gitaly_blob_client.get_blobs(blob_references, blob_size_limit).to_a
- else
- blob_references.map do |sha, path|
- find_by_rugged(repository, sha, path, limit: blob_size_limit)
- end
- end
- end
+ repository.gitaly_blob_client.get_blobs(blob_references, blob_size_limit).to_a
end
# Returns an array of Blob instances just with the metadata, that means
@@ -72,16 +75,8 @@ module Gitlab
# Returns array of Gitlab::Git::Blob
# Does not guarantee blob data will be set
def batch_lfs_pointers(repository, blob_ids)
- repository.gitaly_migrate(:batch_lfs_pointers) do |is_enabled|
- if is_enabled
- repository.gitaly_blob_client.batch_lfs_pointers(blob_ids.to_a)
- else
- 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
+ repository.wrapped_gitaly_errors do
+ repository.gitaly_blob_client.batch_lfs_pointers(blob_ids.to_a)
end
end
@@ -92,151 +87,6 @@ module Gitlab
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
- #
- # Ex.
- # blog/ # oid: 1a
- # app/ # oid: 2a
- # models/ # oid: 3a
- # file.rb # oid: 4a
- #
- #
- # Blob.find_entry_by_path(repo, '1a', 'blog', 'app', 'file.rb') # => '4a'
- #
- def find_entry_by_path(repository, root_id, *path_parts)
- root_tree = repository.lookup(root_id)
-
- entry = root_tree.find do |entry|
- entry[:name] == path_parts[0]
- end
-
- return nil unless entry
-
- if path_parts.size > 1
- return nil unless entry[:type] == :tree
-
- path_parts.shift
- find_entry_by_path(repository, entry[:oid], *path_parts)
- else
- [:blob, :commit].include?(entry[:type]) ? entry : nil
- end
- end
-
- def submodule_blob(blob_entry, path, sha)
- new(
- id: blob_entry[:oid],
- name: blob_entry[:name],
- size: 0,
- data: '',
- path: path,
- commit_id: sha
- )
- end
-
- def find_by_gitaly(repository, sha, path, limit: MAX_DATA_DISPLAY_SIZE)
- return unless path
-
- path = path.sub(%r{\A/*}, '')
- path = '/' if path.empty?
- name = File.basename(path)
-
- # Gitaly will think that setting the limit to 0 means unlimited, while
- # the client might only need the metadata and thus set the limit to 0.
- # In this method we'll then set the limit to 1, but clear the byte of data
- # that we got back so for the outside world it looks like the limit was
- # actually 0.
- req_limit = limit == 0 ? 1 : limit
-
- entry = Gitlab::GitalyClient::CommitService.new(repository).tree_entry(sha, path, req_limit)
- return unless entry
-
- entry.data = "" if limit == 0
-
- case entry.type
- when :COMMIT
- new(
- id: entry.oid,
- name: name,
- size: 0,
- data: '',
- path: path,
- commit_id: sha
- )
- when :BLOB
- new(
- id: entry.oid,
- name: name,
- size: entry.size,
- data: entry.data.dup,
- mode: entry.mode.to_s(8),
- path: path,
- commit_id: sha,
- binary: binary?(entry.data)
- )
- end
- end
-
- def find_by_rugged(repository, sha, path, limit:)
- return unless path
-
- # Strip any leading / characters from the path
- path = path.sub(%r{\A/*}, '')
-
- rugged_commit = repository.lookup(sha)
- root_tree = rugged_commit.tree
-
- blob_entry = find_entry_by_path(repository, root_tree.oid, *path.split('/'))
-
- return nil unless blob_entry
-
- if blob_entry[:type] == :commit
- submodule_blob(blob_entry, path, sha)
- else
- blob = repository.lookup(blob_entry[:oid])
-
- if blob
- new(
- id: blob.oid,
- name: blob_entry[:name],
- size: blob.size,
- # Rugged::Blob#content is expensive; don't call it if we don't have to.
- data: limit.zero? ? '' : blob.content(limit),
- mode: blob_entry[:filemode].to_s(8),
- path: path,
- commit_id: sha,
- binary: blob.binary?
- )
- 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
def initialize(options)
@@ -268,16 +118,7 @@ module Gitlab
return if @loaded_all_data
- @data = Gitlab::GitalyClient.migrate(:git_blob_load_all_data) do |is_enabled|
- begin
- if is_enabled
- repository.gitaly_blob_client.get_blob(oid: id, limit: -1).data
- else
- repository.lookup(id).content
- end
- end
- end
-
+ @data = repository.gitaly_blob_client.get_blob(oid: id, limit: -1).data
@loaded_all_data = true
@loaded_size = @data.bytesize
end
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
index 89f761dd515..4e2d817d12c 100644
--- a/lib/gitlab/git/commit.rb
+++ b/lib/gitlab/git/commit.rb
@@ -60,12 +60,11 @@ module Gitlab
# Some weird thing?
return nil unless commit_id.is_a?(String)
- commit = repo.gitaly_migrate(:find_commit) do |is_enabled|
- if is_enabled
- repo.gitaly_commit_client.find_commit(commit_id)
- else
- rugged_find(repo, commit_id)
- end
+ # This saves us an RPC round trip.
+ return nil if commit_id.include?(':')
+
+ commit = repo.wrapped_gitaly_errors do
+ repo.gitaly_commit_client.find_commit(commit_id)
end
decorate(repo, commit) if commit
@@ -75,12 +74,6 @@ module Gitlab
nil
end
- def rugged_find(repo, commit_id)
- obj = repo.rev_parse_target(commit_id)
-
- obj.is_a?(Rugged::Commit) ? obj : nil
- end
-
# Get last commit for HEAD
#
# Ex.
@@ -113,15 +106,9 @@ module Gitlab
# Commit.between(repo, '29eda46b', 'master')
#
def between(repo, base, head)
- Gitlab::GitalyClient.migrate(:commits_between) do |is_enabled|
- if is_enabled
- repo.gitaly_commit_client.between(base, head)
- else
- repo.rugged_commits_between(base, head).map { |c| decorate(repo, c) }
- end
+ repo.wrapped_gitaly_errors do
+ repo.gitaly_commit_client.between(base, head)
end
- rescue Rugged::ReferenceError
- []
end
# Returns commits collection
@@ -146,58 +133,11 @@ module Gitlab
#
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/326
def find_all(repo, options = {})
- Gitlab::GitalyClient.migrate(:find_all_commits) do |is_enabled|
- if is_enabled
- find_all_by_gitaly(repo, options)
- else
- find_all_by_rugged(repo, options)
- end
+ repo.wrapped_gitaly_errors do
+ Gitlab::GitalyClient::CommitService.new(repo).find_all_commits(options)
end
end
- def find_all_by_rugged(repo, options = {})
- actual_options = options.dup
-
- allowed_options = [:ref, :max_count, :skip, :order]
-
- actual_options.keep_if do |key|
- allowed_options.include?(key)
- end
-
- default_options = { skip: 0 }
- actual_options = default_options.merge(actual_options)
-
- rugged = repo.rugged
- walker = Rugged::Walker.new(rugged)
-
- if actual_options[:ref]
- walker.push(rugged.rev_parse_oid(actual_options[:ref]))
- else
- rugged.references.each("refs/heads/*") do |ref|
- walker.push(ref.target_id)
- end
- end
-
- walker.sorting(rugged_sort_type(actual_options[:order]))
-
- commits = []
- offset = actual_options[:skip]
- limit = actual_options[:max_count]
- walker.each(offset: offset, limit: limit) do |commit|
- commits.push(decorate(repo, commit))
- end
-
- walker.reset
-
- commits
- rescue Rugged::OdbError
- []
- end
-
- def find_all_by_gitaly(repo, options = {})
- Gitlab::GitalyClient::CommitService.new(repo).find_all_commits(options)
- end
-
def decorate(repository, commit, ref = nil)
Gitlab::Git::Commit.new(repository, commit, ref)
end
@@ -217,43 +157,20 @@ module Gitlab
end
def shas_with_signatures(repository, shas)
- GitalyClient.migrate(:filter_shas_with_signatures) do |is_enabled|
- if is_enabled
- Gitlab::GitalyClient::CommitService.new(repository).filter_shas_with_signatures(shas)
- else
- shas.select do |sha|
- begin
- Rugged::Commit.extract_signature(repository.rugged, sha)
- rescue Rugged::OdbError
- false
- end
- end
- end
- end
+ Gitlab::GitalyClient::CommitService.new(repository).filter_shas_with_signatures(shas)
end
# Only to be used when the object ids will not necessarily have a
# relation to each other. The last 10 commits for a branch for example,
# should go through .where
def batch_by_oid(repo, oids)
- repo.gitaly_migrate(:list_commits_by_oid,
- status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
- if is_enabled
- repo.gitaly_commit_client.list_commits_by_oid(oids)
- else
- oids.map { |oid| find(repo, oid) }.compact
- end
+ repo.wrapped_gitaly_errors do
+ repo.gitaly_commit_client.list_commits_by_oid(oids)
end
end
def extract_signature(repository, commit_id)
- repository.gitaly_migrate(:extract_commit_signature) do |is_enabled|
- if is_enabled
- repository.gitaly_commit_client.extract_signature(commit_id)
- else
- rugged_extract_signature(repository, commit_id)
- end
- end
+ repository.gitaly_commit_client.extract_signature(commit_id)
end
def extract_signature_lazily(repository, commit_id)
@@ -273,36 +190,9 @@ module Gitlab
end
def batch_signature_extraction(repository, commit_ids)
- repository.gitaly_migrate(:extract_commit_signature_in_batch) do |is_enabled|
- if is_enabled
- gitaly_batch_signature_extraction(repository, commit_ids)
- else
- rugged_batch_signature_extraction(repository, commit_ids)
- end
- end
- end
-
- def gitaly_batch_signature_extraction(repository, commit_ids)
repository.gitaly_commit_client.get_commit_signatures(commit_ids)
end
- def rugged_batch_signature_extraction(repository, commit_ids)
- commit_ids.each_with_object({}) do |commit_id, signatures|
- signature_data = rugged_extract_signature(repository, commit_id)
- next unless signature_data
-
- signatures[commit_id] = signature_data
- end
- end
-
- def rugged_extract_signature(repository, commit_id)
- begin
- Rugged::Commit.extract_signature(repository.rugged, commit_id)
- rescue Rugged::OdbError
- nil
- end
- end
-
def get_message(repository, commit_id)
BatchLoader.for({ repository: repository, commit_id: commit_id }).batch do |items, loader|
items_by_repo = items.group_by { |i| i[:repository] }
@@ -320,13 +210,7 @@ module Gitlab
end
def get_messages(repository, commit_ids)
- repository.gitaly_migrate(:commit_messages) do |is_enabled|
- if is_enabled
- repository.gitaly_commit_client.get_commit_messages(commit_ids)
- else
- commit_ids.map { |id| [id, rugged_find(repository, id).message] }.to_h
- end
- end
+ repository.gitaly_commit_client.get_commit_messages(commit_ids)
end
end
@@ -378,15 +262,11 @@ module Gitlab
# empty repo. See Repository#diff for keys allowed in the +options+
# hash.
def diff_from_parent(options = {})
- Gitlab::GitalyClient.migrate(:commit_raw_diffs) do |is_enabled|
- if is_enabled
- @repository.gitaly_commit_client.diff_from_parent(self, options)
- else
- rugged_diff_from_parent(options)
- end
- end
+ @repository.gitaly_commit_client.diff_from_parent(self, options)
end
+ # Not to be called directly, but right now its used for tests and in old
+ # migrations
def rugged_diff_from_parent(options = {})
options ||= {}
break_rewrites = options[:break_rewrites]
@@ -404,14 +284,7 @@ module Gitlab
def deltas
@deltas ||= begin
- deltas = Gitlab::GitalyClient.migrate(:commit_deltas) do |is_enabled|
- if is_enabled
- @repository.gitaly_commit_client.commit_deltas(self)
- else
- rugged_diff_from_parent.each_delta
- end
- end
-
+ deltas = @repository.gitaly_commit_client.commit_deltas(self)
deltas.map { |delta| Gitlab::Git::Diff.new(delta) }
end
end
@@ -494,13 +367,18 @@ module Gitlab
def tree_entry(path)
return unless path.present?
- @repository.gitaly_migrate(:commit_tree_entry) do |is_migrated|
- if is_migrated
- gitaly_tree_entry(path)
- else
- rugged_tree_entry(path)
- end
- end
+ # We're only interested in metadata, so limit actual data to 1 byte
+ # since Gitaly doesn't support "send no data" option.
+ entry = @repository.gitaly_commit_client.tree_entry(id, path, 1)
+ return unless entry
+
+ # To be compatible with the rugged format
+ entry = entry.to_h
+ entry.delete(:data)
+ entry[:name] = File.basename(path)
+ entry[:type] = entry[:type].downcase
+
+ entry
end
def to_gitaly_commit
@@ -563,28 +441,6 @@ module Gitlab
SERIALIZE_KEYS
end
- def gitaly_tree_entry(path)
- # We're only interested in metadata, so limit actual data to 1 byte
- # since Gitaly doesn't support "send no data" option.
- entry = @repository.gitaly_commit_client.tree_entry(id, path, 1)
- return unless entry
-
- # To be compatible with the rugged format
- entry = entry.to_h
- entry.delete(:data)
- entry[:name] = File.basename(path)
- entry[:type] = entry[:type].downcase
-
- entry
- end
-
- # Is this the same as Blob.find_entry_by_path ?
- def rugged_tree_entry(path)
- rugged_commit.tree.path(path)
- rescue Rugged::TreeError
- nil
- end
-
def gitaly_commit_author_from_rugged(author_or_committer)
Gitaly::CommitAuthor.new(
name: author_or_committer[:name].b,
diff --git a/lib/gitlab/git/commit_stats.rb b/lib/gitlab/git/commit_stats.rb
index 8463b1eb794..ae6f554bc06 100644
--- a/lib/gitlab/git/commit_stats.rb
+++ b/lib/gitlab/git/commit_stats.rb
@@ -1,5 +1,3 @@
-# Gitaly note: JV: 1 RPC, migration in progress.
-
# Gitlab::Git::CommitStats counts the additions, deletions, and total changes
# in a commit.
module Gitlab
@@ -16,12 +14,8 @@ module Gitlab
@deletions = 0
@total = 0
- repo.gitaly_migrate(:commit_stats) do |is_enabled|
- if is_enabled
- gitaly_stats(repo, commit)
- else
- rugged_stats(commit)
- end
+ repo.wrapped_gitaly_errors do
+ gitaly_stats(repo, commit)
end
end
@@ -31,12 +25,6 @@ module Gitlab
@deletions = stats.deletions
@total = @additions + @deletions
end
-
- def rugged_stats(commit)
- diff = commit.rugged_diff_from_parent
- _files_changed, @additions, @deletions = diff.stat
- @total = @additions + @deletions
- end
end
end
end
diff --git a/lib/gitlab/git/committer_with_hooks.rb b/lib/gitlab/git/committer_with_hooks.rb
index a8a59f998cd..4198be7c9c9 100644
--- a/lib/gitlab/git/committer_with_hooks.rb
+++ b/lib/gitlab/git/committer_with_hooks.rb
@@ -20,7 +20,7 @@ module Gitlab
end
result[:newrev]
- rescue Gitlab::Git::HooksService::PreReceiveError => e
+ rescue Gitlab::Git::PreReceiveError => e
message = "Custom Hook failed: #{e.message}"
raise Gitlab::Git::Wiki::OperationError, message
end
diff --git a/lib/gitlab/git/conflict/resolver.rb b/lib/gitlab/git/conflict/resolver.rb
index c3cb0264112..0e4a973301f 100644
--- a/lib/gitlab/git/conflict/resolver.rb
+++ b/lib/gitlab/git/conflict/resolver.rb
@@ -12,14 +12,8 @@ module Gitlab
end
def conflicts
- @conflicts ||= begin
- @target_repository.gitaly_migrate(:conflicts_list_conflict_files) do |is_enabled|
- if is_enabled
- gitaly_conflicts_client(@target_repository).list_conflict_files.to_a
- else
- rugged_list_conflict_files
- end
- end
+ @conflicts ||= @target_repository.wrapped_gitaly_errors do
+ gitaly_conflicts_client(@target_repository).list_conflict_files.to_a
end
rescue GRPC::FailedPrecondition => e
raise Gitlab::Git::Conflict::Resolver::ConflictSideMissing.new(e.message)
@@ -28,12 +22,8 @@ module Gitlab
end
def resolve_conflicts(source_repository, resolution, source_branch:, target_branch:)
- source_repository.gitaly_migrate(:conflicts_resolve_conflicts) do |is_enabled|
- if is_enabled
- gitaly_conflicts_client(source_repository).resolve_conflicts(@target_repository, resolution, source_branch, target_branch)
- else
- rugged_resolve_conflicts(source_repository, resolution, source_branch, target_branch)
- end
+ source_repository.wrapped_gitaly_errors do
+ gitaly_conflicts_client(source_repository).resolve_conflicts(@target_repository, resolution, source_branch, target_branch)
end
end
@@ -61,57 +51,6 @@ module Gitlab
def gitaly_conflicts_client(repository)
repository.gitaly_conflicts_client(@our_commit_oid, @their_commit_oid)
end
-
- def write_resolved_file_to_index(repository, 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.end_with?("\n")
- elsif params[:content]
- new_file = file.resolve_content(params[:content])
- end
-
- our_path = file.our_path
-
- oid = repository.rugged.write(new_file, :blob)
- index.add(path: our_path, oid: oid, mode: file.our_mode)
- index.conflict_remove(our_path)
- end
-
- def rugged_list_conflict_files
- target_index = @target_repository.rugged.merge_commits(@our_commit_oid, @their_commit_oid)
-
- # We don't need to do `with_repo_branch_commit` here, because the target
- # project always fetches source refs when creating merge request diffs.
- conflict_files(@target_repository, target_index)
- end
-
- def rugged_resolve_conflicts(source_repository, resolution, source_branch, target_branch)
- source_repository.with_repo_branch_commit(@target_repository, target_branch) do
- index = source_repository.rugged.merge_commits(@our_commit_oid, @their_commit_oid)
- conflicts = conflict_files(source_repository, index)
-
- resolution.files.each do |file_params|
- conflict_file = conflict_for_path(conflicts, file_params[:old_path], file_params[:new_path])
-
- write_resolved_file_to_index(source_repository, 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: resolution.commit_message,
- parents: [@our_commit_oid, @their_commit_oid]
- }
-
- source_repository.commit_index(resolution.user, source_branch, index, commit_params)
- end
- end
end
end
end
diff --git a/lib/gitlab/git/diff_collection.rb b/lib/gitlab/git/diff_collection.rb
index 6a601561c2a..219c69893ad 100644
--- a/lib/gitlab/git/diff_collection.rb
+++ b/lib/gitlab/git/diff_collection.rb
@@ -42,12 +42,10 @@ module Gitlab
return if @overflow
return if @iterator.nil?
- Gitlab::GitalyClient.migrate(:commit_raw_diffs) do |is_enabled|
- if is_enabled && @iterator.is_a?(Gitlab::GitalyClient::DiffStitcher)
- each_gitaly_patch(&block)
- else
- each_rugged_patch(&block)
- end
+ if @iterator.is_a?(Gitlab::GitalyClient::DiffStitcher)
+ each_gitaly_patch(&block)
+ else
+ each_serialized_patch(&block)
end
@populated = true
@@ -118,7 +116,7 @@ module Gitlab
end
end
- def each_rugged_patch
+ def each_serialized_patch
i = @array.length
@iterator.each do |raw|
diff --git a/lib/gitlab/git/gitlab_projects.rb b/lib/gitlab/git/gitlab_projects.rb
index 00c943fdb25..5ff15a787f0 100644
--- a/lib/gitlab/git/gitlab_projects.rb
+++ b/lib/gitlab/git/gitlab_projects.rb
@@ -53,43 +53,23 @@ module Gitlab
# Import project via git clone --bare
# URL must be publicly cloneable
def import_project(source, timeout)
- Gitlab::GitalyClient.migrate(:import_repository, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
- if is_enabled
- gitaly_import_repository(source)
- else
- git_import_repository(source, timeout)
- end
- end
+ git_import_repository(source, timeout)
end
def fork_repository(new_shard_name, new_repository_relative_path)
- Gitlab::GitalyClient.migrate(:fork_repository,
- status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
- if is_enabled
- gitaly_fork_repository(new_shard_name, new_repository_relative_path)
- else
- git_fork_repository(new_shard_name, new_repository_relative_path)
- end
- end
+ git_fork_repository(new_shard_name, new_repository_relative_path)
end
def fetch_remote(name, timeout, force:, tags:, ssh_key: nil, known_hosts: nil, prune: true)
- tags_option = tags ? '--tags' : '--no-tags'
-
logger.info "Fetching remote #{name} for repository #{repository_absolute_path}."
- cmd = %W(#{Gitlab.config.git.bin_path} fetch #{name} --quiet)
- cmd << '--prune' if prune
- cmd << '--force' if force
- cmd << tags_option
+ cmd = fetch_remote_command(name, tags, prune, force)
setup_ssh_auth(ssh_key, known_hosts) do |env|
- success = run_with_timeout(cmd, timeout, repository_absolute_path, env)
-
- unless success
- logger.error "Fetching remote #{name} for repository #{repository_absolute_path} failed."
+ run_with_timeout(cmd, timeout, repository_absolute_path, env).tap do |success|
+ unless success
+ logger.error "Fetching remote #{name} for repository #{repository_absolute_path} failed."
+ end
end
-
- success
end
end
@@ -215,6 +195,14 @@ module Gitlab
private
+ def fetch_remote_command(name, tags, prune, force)
+ %W(#{Gitlab.config.git.bin_path} fetch #{name} --quiet).tap do |cmd|
+ cmd << '--prune' if prune
+ cmd << '--force' if force
+ cmd << (tags ? '--tags' : '--no-tags')
+ end
+ end
+
def git_import_repository(source, timeout)
# Skip import if repo already exists
return false if File.exist?(repository_absolute_path)
@@ -241,16 +229,6 @@ module Gitlab
true
end
- def gitaly_import_repository(source)
- raw_repository = Gitlab::Git::Repository.new(shard_name, repository_relative_path, nil)
-
- Gitlab::GitalyClient::RepositoryService.new(raw_repository).import_repository(source)
- true
- rescue GRPC::BadStatus => e
- @output << e.message
- false
- end
-
def git_fork_repository(new_shard_name, new_repository_relative_path)
from_path = repository_absolute_path
new_shard_path = Gitlab.config.repositories.storages.fetch(new_shard_name).legacy_disk_path
@@ -270,16 +248,6 @@ module Gitlab
run(cmd, nil) && Gitlab::Git::Repository.create_hooks(to_path, global_hooks_path)
end
-
- def gitaly_fork_repository(new_shard_name, new_repository_relative_path)
- target_repository = Gitlab::Git::Repository.new(new_shard_name, new_repository_relative_path, nil)
- raw_repository = Gitlab::Git::Repository.new(shard_name, repository_relative_path, nil)
-
- Gitlab::GitalyClient::RepositoryService.new(target_repository).fork_repository(raw_repository)
- rescue GRPC::BadStatus => e
- logger.error "fork-repository failed: #{e.message}"
- false
- end
end
end
end
diff --git a/lib/gitlab/git/hook.rb b/lib/gitlab/git/hook.rb
index 7c201c6169b..94ff5b4980a 100644
--- a/lib/gitlab/git/hook.rb
+++ b/lib/gitlab/git/hook.rb
@@ -11,7 +11,7 @@ module Gitlab
def initialize(name, repository)
@name = name
@repository = repository
- @path = File.join(repo_path.strip, 'hooks', name)
+ @path = File.join(repo_path, 'hooks', name)
end
def repo_path
@@ -95,13 +95,13 @@ module Gitlab
args = [ref, oldrev, newrev]
stdout, stderr, status = Open3.capture3(env, path, *args, options)
- [status.success?, Gitlab::Utils.nlbr(stderr.presence || stdout)]
+ [status.success?, stderr.presence || stdout]
end
def retrieve_error_message(stderr, stdout)
err_message = stderr.read
err_message = err_message.blank? ? stdout.read : err_message
- Gitlab::Utils.nlbr(err_message)
+ err_message
end
end
end
diff --git a/lib/gitlab/git/hooks_service.rb b/lib/gitlab/git/hooks_service.rb
index f302b852b35..e67cacdb95a 100644
--- a/lib/gitlab/git/hooks_service.rb
+++ b/lib/gitlab/git/hooks_service.rb
@@ -1,8 +1,6 @@
module Gitlab
module Git
class HooksService
- PreReceiveError = Class.new(StandardError)
-
attr_accessor :oldrev, :newrev, :ref
def execute(pusher, repository, oldrev, newrev, ref)
diff --git a/lib/gitlab/git/lfs_changes.rb b/lib/gitlab/git/lfs_changes.rb
index b9e5cf258f4..f0fab1e76a3 100644
--- a/lib/gitlab/git/lfs_changes.rb
+++ b/lib/gitlab/git/lfs_changes.rb
@@ -7,45 +7,11 @@ module Gitlab
end
def new_pointers(object_limit: nil, not_in: nil)
- @repository.gitaly_migrate(:blob_get_new_lfs_pointers) do |is_enabled|
- if is_enabled
- @repository.gitaly_blob_client.get_new_lfs_pointers(@newrev, object_limit, not_in)
- else
- git_new_pointers(object_limit, not_in)
- end
- end
+ @repository.gitaly_blob_client.get_new_lfs_pointers(@newrev, object_limit, not_in)
end
def all_pointers
- @repository.gitaly_migrate(:blob_get_all_lfs_pointers) do |is_enabled|
- if is_enabled
- @repository.gitaly_blob_client.get_all_lfs_pointers(@newrev)
- else
- git_all_pointers
- end
- end
- end
-
- private
-
- def git_new_pointers(object_limit, not_in)
- @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 git_all_pointers
- rev_list.all_objects(require_path: true) do |object_ids|
- Gitlab::Git::Blob.batch_lfs_pointers(@repository, object_ids)
- end
- end
-
- def rev_list
- Gitlab::Git::RevList.new(@repository, newrev: @newrev)
+ @repository.gitaly_blob_client.get_all_lfs_pointers(@newrev)
end
end
end
diff --git a/lib/gitlab/git/pre_receive_error.rb b/lib/gitlab/git/pre_receive_error.rb
new file mode 100644
index 00000000000..ac1ab7c39d5
--- /dev/null
+++ b/lib/gitlab/git/pre_receive_error.rb
@@ -0,0 +1,21 @@
+module Gitlab
+ module Git
+ #
+ # PreReceiveError is special because its message gets displayed to users
+ # in the web UI. To prevent XSS we sanitize the message on
+ # initialization.
+ class PreReceiveError < StandardError
+ def initialize(msg = '')
+ super(nlbr(msg))
+ end
+
+ private
+
+ # In gitaly-ruby we override this method to do nothing, so that
+ # sanitization happens in gitlab-rails only.
+ def nlbr(str)
+ Gitlab::Utils.nlbr(str)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/remote_mirror.rb b/lib/gitlab/git/remote_mirror.rb
index ebe46722890..e4743b4db0a 100644
--- a/lib/gitlab/git/remote_mirror.rb
+++ b/lib/gitlab/git/remote_mirror.rb
@@ -7,81 +7,8 @@ module Gitlab
end
def update(only_branches_matching: [])
- @repository.gitaly_migrate(:remote_update_remote_mirror) do |is_enabled|
- if is_enabled
- gitaly_update(only_branches_matching)
- else
- rugged_update(only_branches_matching)
- end
- end
- end
-
- private
-
- def gitaly_update(only_branches_matching)
- @repository.gitaly_remote_client.update_remote_mirror(@ref_name, only_branches_matching)
- end
-
- def rugged_update(only_branches_matching)
- local_branches = refs_obj(@repository.local_branches, only_refs_matching: only_branches_matching)
- remote_branches = refs_obj(@repository.remote_branches(@ref_name), only_refs_matching: only_branches_matching)
-
- updated_branches = changed_refs(local_branches, remote_branches)
- push_branches(updated_branches.keys) if updated_branches.present?
-
- delete_refs(local_branches, remote_branches)
-
- local_tags = refs_obj(@repository.tags)
- remote_tags = refs_obj(@repository.remote_tags(@ref_name))
-
- updated_tags = changed_refs(local_tags, remote_tags)
- @repository.push_remote_branches(@ref_name, updated_tags.keys) if updated_tags.present?
-
- delete_refs(local_tags, remote_tags)
- end
-
- def refs_obj(refs, only_refs_matching: [])
- refs.each_with_object({}) do |ref, refs|
- next if only_refs_matching.present? && !only_refs_matching.include?(ref.name)
-
- refs[ref.name] = ref
- end
- end
-
- def changed_refs(local_refs, remote_refs)
- local_refs.select do |ref_name, ref|
- remote_ref = remote_refs[ref_name]
-
- remote_ref.nil? || ref.dereferenced_target != remote_ref.dereferenced_target
- end
- end
-
- def push_branches(branches)
- default_branch, branches = branches.partition do |branch|
- @repository.root_ref == branch
- end
-
- # Push the default branch first so it works fine when remote mirror is empty.
- branches.unshift(*default_branch)
-
- @repository.push_remote_branches(@ref_name, branches)
- end
-
- def delete_refs(local_refs, remote_refs)
- refs = refs_to_delete(local_refs, remote_refs)
-
- @repository.delete_remote_branches(@ref_name, refs.keys) if refs.present?
- end
-
- def refs_to_delete(local_refs, remote_refs)
- default_branch_id = @repository.commit.id
-
- remote_refs.select do |remote_ref_name, remote_ref|
- next false if local_refs[remote_ref_name] # skip if branch or tag exist in local repo
-
- remote_ref_id = remote_ref.dereferenced_target.try(:id)
-
- remote_ref_id && @repository.rugged_is_ancestor?(remote_ref_id, default_branch_id)
+ @repository.wrapped_gitaly_errors do
+ @repository.gitaly_remote_client.update_remote_mirror(@ref_name, only_branches_matching)
end
end
end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 1a21625a322..a1a050647b9 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -109,7 +109,7 @@ module Gitlab
end
def ==(other)
- path == other.path
+ [storage, relative_path] == [other.storage, other.relative_path]
end
def path
@@ -120,13 +120,11 @@ module Gitlab
# Default branch in the repository
def root_ref
- @root_ref ||= gitaly_migrate(:root_ref) do |is_enabled|
- if is_enabled
- gitaly_ref_client.default_branch_name
- else
- discover_default_branch
- end
- end
+ gitaly_ref_client.default_branch_name
+ rescue GRPC::NotFound => e
+ raise NoRepository.new(e.message)
+ rescue GRPC::Unknown => e
+ raise Gitlab::Git::CommandError.new(e.message)
end
def rugged
@@ -152,23 +150,15 @@ module Gitlab
# Returns an Array of branch names
# sorted by name ASC
def branch_names
- gitaly_migrate(:branch_names) do |is_enabled|
- if is_enabled
- gitaly_ref_client.branch_names
- else
- branches.map(&:name)
- end
+ wrapped_gitaly_errors do
+ gitaly_ref_client.branch_names
end
end
# Returns an Array of Branches
def branches
- gitaly_migrate(:branches) do |is_enabled|
- if is_enabled
- gitaly_ref_client.branches
- else
- branches_filter
- end
+ wrapped_gitaly_errors do
+ gitaly_ref_client.branches
end
end
@@ -200,12 +190,8 @@ module Gitlab
end
def local_branches(sort_by: nil)
- gitaly_migrate(:local_branches) do |is_enabled|
- if is_enabled
- gitaly_ref_client.local_branches(sort_by: sort_by)
- else
- branches_filter(filter: :local, sort_by: sort_by)
- end
+ wrapped_gitaly_errors do
+ gitaly_ref_client.local_branches(sort_by: sort_by)
end
end
@@ -245,18 +231,6 @@ module Gitlab
# This refs by default not visible in project page and not cloned to client side.
alias_method :has_visible_content?, :has_local_branches?
- def has_local_branches_rugged?
- rugged.branches.each(:local).any? do |ref|
- begin
- ref.name && ref.target # ensures the branch is valid
-
- true
- rescue Rugged::ReferenceError
- false
- end
- end
- end
-
# Returns the number of valid tags
def tag_count
gitaly_migrate(:tag_names) do |is_enabled|
@@ -270,25 +244,16 @@ module Gitlab
# Returns an Array of tag names
def tag_names
- gitaly_migrate(:tag_names) do |is_enabled|
- if is_enabled
- gitaly_ref_client.tag_names
- else
- rugged.tags.map { |t| t.name }
- end
+ wrapped_gitaly_errors do
+ gitaly_ref_client.tag_names
end
end
# Returns an Array of Tags
#
- # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/390
def tags
- gitaly_migrate(:tags) do |is_enabled|
- if is_enabled
- tags_from_gitaly
- else
- tags_from_rugged
- end
+ wrapped_gitaly_errors do
+ gitaly_ref_client.tags
end
end
@@ -310,7 +275,7 @@ module Gitlab
#
# name - The name of the tag as a String.
def tag_exists?(name)
- gitaly_migrate(:ref_exists_tags) do |is_enabled|
+ gitaly_migrate(:ref_exists_tags, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled
gitaly_ref_exists?("refs/tags/#{name}")
else
@@ -323,7 +288,7 @@ module Gitlab
#
# name - The name of the branch as a String.
def branch_exists?(name)
- gitaly_migrate(:ref_exists_branches) do |is_enabled|
+ gitaly_migrate(:ref_exists_branches, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled
gitaly_ref_exists?("refs/heads/#{name}")
else
@@ -364,43 +329,18 @@ module Gitlab
end.map(&:name)
end
- # Discovers the default branch based on the repository's available branches
- #
- # - If no branches are present, returns nil
- # - If one branch is present, returns its name
- # - If two or more branches are present, returns current HEAD or master or first branch
- def discover_default_branch
- names = branch_names
-
- return if names.empty?
-
- return names[0] if names.length == 1
-
- if rugged_head
- extracted_name = Ref.extract_branch_name(rugged_head.name)
-
- return extracted_name if names.include?(extracted_name)
- end
-
- if names.include?('master')
- 'master'
- else
- names[0]
- end
- end
-
def rugged_head
rugged.head
rescue Rugged::ReferenceError
nil
end
- def archive_metadata(ref, storage_path, format = "tar.gz", append_sha:)
+ def archive_metadata(ref, storage_path, project_path, format = "tar.gz", append_sha:)
ref ||= root_ref
commit = Gitlab::Git::Commit.find(self, ref)
return {} if commit.nil?
- prefix = archive_prefix(ref, commit.id, append_sha: append_sha)
+ prefix = archive_prefix(ref, commit.id, project_path, append_sha: append_sha)
{
'ArchivePrefix' => prefix,
@@ -412,16 +352,12 @@ module Gitlab
# This is both the filename of the archive (missing the extension) and the
# name of the top-level member of the archive under which all files go
- #
- # FIXME: The generated prefix is incorrect for projects with hashed
- # storage enabled
- def archive_prefix(ref, sha, append_sha:)
+ def archive_prefix(ref, sha, project_path, append_sha:)
append_sha = (ref != sha) if append_sha.nil?
- project_name = self.name.chomp('.git')
formatted_ref = ref.tr('/', '-')
- prefix_segments = [project_name, formatted_ref]
+ prefix_segments = [project_path, formatted_ref]
prefix_segments << sha if append_sha
prefix_segments.join('-')
@@ -466,13 +402,7 @@ module Gitlab
# Return repo size in megabytes
def size
- size = gitaly_migrate(:repository_size) do |is_enabled|
- if is_enabled
- size_by_gitaly
- else
- size_by_shelling_out
- end
- end
+ size = gitaly_repository_client.repository_size
(size.to_f / 1024).round(2)
end
@@ -509,39 +439,27 @@ module Gitlab
raise ArgumentError.new("invalid Repository#log limit: #{limit.inspect}")
end
- gitaly_migrate(:find_commits) do |is_enabled|
- if is_enabled
- gitaly_commit_client.find_commits(options)
- else
- raw_log(options).map { |c| Commit.decorate(self, c) }
- end
+ wrapped_gitaly_errors do
+ gitaly_commit_client.find_commits(options)
end
end
- # Used in gitaly-ruby
- def raw_log(options)
- sha =
- unless options[:all]
- actual_ref = options[:ref] || root_ref
- begin
- sha_from_ref(actual_ref)
- rescue Rugged::OdbError, Rugged::InvalidError, Rugged::ReferenceError
- # Return an empty array if the ref wasn't found
- return []
- end
- end
+ def count_commits(options)
+ options = process_count_commits_options(options.dup)
- log_by_shell(sha, options)
- end
+ wrapped_gitaly_errors do
+ if options[:left_right]
+ from = options[:from]
+ to = options[:to]
- def count_commits(options)
- count_commits_options = process_count_commits_options(options)
+ right_count = gitaly_commit_client
+ .commit_count("#{from}..#{to}", options)
+ left_count = gitaly_commit_client
+ .commit_count("#{to}..#{from}", options)
- gitaly_migrate(:count_commits) do |is_enabled|
- if is_enabled
- count_commits_by_gitaly(count_commits_options)
+ [left_count, right_count]
else
- count_commits_by_shelling_out(count_commits_options)
+ gitaly_commit_client.commit_count(options[:ref], options)
end
end
end
@@ -553,27 +471,6 @@ module Gitlab
Ref.dereference_object(obj)
end
- # Return a collection of Rugged::Commits between the two revspec arguments.
- # See http://git-scm.com/docs/git-rev-parse.html#_specifying_revisions for
- # a detailed list of valid arguments.
- #
- # Gitaly note: JV: to be deprecated in favor of Commit.between
- def rugged_commits_between(from, to)
- walker = Rugged::Walker.new(rugged)
- walker.sorting(Rugged::SORT_NONE | Rugged::SORT_REVERSE)
-
- sha_from = sha_from_ref(from)
- sha_to = sha_from_ref(to)
-
- walker.push(sha_to)
- walker.hide(sha_from)
-
- commits = walker.to_a
- walker.reset
-
- commits
- end
-
# Counts the amount of commits between `from` and `to`.
def count_commits_between(from, to, options = {})
count_commits(from: from, to: to, **options)
@@ -584,32 +481,17 @@ module Gitlab
def raw_changes_between(old_rev, new_rev)
@raw_changes_between ||= {}
- @raw_changes_between[[old_rev, new_rev]] ||= begin
- return [] if new_rev.blank? || new_rev == Gitlab::Git::BLANK_SHA
+ @raw_changes_between[[old_rev, new_rev]] ||=
+ begin
+ return [] if new_rev.blank? || new_rev == Gitlab::Git::BLANK_SHA
- gitaly_migrate(:raw_changes_between) do |is_enabled|
- if is_enabled
+ wrapped_gitaly_errors do
gitaly_repository_client.raw_changes_between(old_rev, new_rev)
.each_with_object([]) do |msg, arr|
msg.raw_changes.each { |change| arr << ::Gitlab::Git::RawDiffChange.new(change) }
end
- else
- result = []
-
- circuit_breaker.perform do
- Open3.pipeline_r(git_diff_cmd(old_rev, new_rev), format_git_cat_file_script, git_cat_file_cmd) do |last_stdout, wait_threads|
- last_stdout.each_line { |line| result << ::Gitlab::Git::RawDiffChange.new(line.chomp!) }
-
- if wait_threads.any? { |waiter| !waiter.value&.success? }
- raise ::Gitlab::Git::Repository::GitError, "Unabled to obtain changes between #{old_rev} and #{new_rev}"
- end
- end
- end
-
- result
end
end
- end
rescue ArgumentError => e
raise Gitlab::Git::Repository::GitError.new(e)
end
@@ -625,24 +507,9 @@ module Gitlab
end
end
- # Gitaly note: JV: check gitlab-ee before removing this method.
- def rugged_is_ancestor?(ancestor_id, descendant_id)
- return false if ancestor_id.nil? || descendant_id.nil?
-
- rugged_merge_base(ancestor_id, descendant_id) == ancestor_id
- rescue Rugged::OdbError
- false
- end
-
# Returns true is +from+ is direct ancestor to +to+, otherwise false
def ancestor?(from, to)
- Gitlab::GitalyClient.migrate(:is_ancestor) do |is_enabled|
- if is_enabled
- gitaly_commit_client.ancestor?(from, to)
- else
- rugged_is_ancestor?(from, to)
- end
- end
+ gitaly_commit_client.ancestor?(from, to)
end
def merged_branch_names(branch_names = [])
@@ -668,13 +535,7 @@ module Gitlab
# diff options. The +options+ hash can also include :break_rewrites to
# split larger rewrites into delete/add pairs.
def diff(from, to, options = {}, *paths)
- iterator = gitaly_migrate(:diff_between) do |is_enabled|
- if is_enabled
- gitaly_commit_client.diff(from, to, options.merge(paths: paths))
- else
- diff_patches(from, to, options, *paths)
- end
- end
+ iterator = gitaly_commit_client.diff(from, to, options.merge(paths: paths))
Gitlab::Git::DiffCollection.new(iterator, options)
end
@@ -683,17 +544,7 @@ module Gitlab
def ref_name_for_sha(ref_path, sha)
raise ArgumentError, "sha can't be empty" unless sha.present?
- gitaly_migrate(:find_ref_name) do |is_enabled|
- if is_enabled
- gitaly_ref_client.find_ref_name(sha, ref_path)
- else
- args = %W(for-each-ref --count=1 #{ref_path} --contains #{sha})
-
- # Not found -> ["", 0]
- # Found -> ["b8d95eb4969eefacb0a58f6a28f6803f8070e7b9 commit\trefs/environments/production/77\n", 0]
- run_git(args).first.split.last
- end
- end
+ gitaly_ref_client.find_ref_name(sha, ref_path)
end
# Get refs hash which key is is the commit id
@@ -724,30 +575,16 @@ module Gitlab
# @repository.submodule_url_for('master', 'rack')
# # => git@localhost:rack.git
#
- # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/329
def submodule_url_for(ref, path)
- Gitlab::GitalyClient.migrate(:submodule_url_for) do |is_enabled|
- if is_enabled
- gitaly_submodule_url_for(ref, path)
- else
- if submodules(ref).any?
- submodule = submodules(ref)[path]
- submodule['url'] if submodule
- end
- end
+ wrapped_gitaly_errors do
+ gitaly_submodule_url_for(ref, path)
end
end
# Return total commits count accessible from passed ref
- #
- # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/330
def commit_count(ref)
- gitaly_migrate(:commit_count) do |is_enabled|
- if is_enabled
- gitaly_commit_client.commit_count(ref)
- else
- rugged_commit_count(ref)
- end
+ wrapped_gitaly_errors do
+ gitaly_commit_client.commit_count(ref)
end
end
@@ -776,38 +613,36 @@ module Gitlab
end
def add_branch(branch_name, user:, target:)
- gitaly_operation_client.user_create_branch(branch_name, user, target)
- rescue GRPC::FailedPrecondition => ex
- raise InvalidRef, ex
+ wrapped_gitaly_errors do
+ gitaly_operation_client.user_create_branch(branch_name, user, target)
+ end
end
def add_tag(tag_name, user:, target:, message: nil)
- gitaly_migrate(:operation_user_add_tag, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
+ wrapped_gitaly_errors do
+ gitaly_operation_client.add_tag(tag_name, user, target, message)
+ end
+ end
+
+ def update_branch(branch_name, user:, newrev:, oldrev:)
+ gitaly_migrate(:operation_user_update_branch) do |is_enabled|
if is_enabled
- gitaly_add_tag(tag_name, user: user, target: target, message: message)
+ gitaly_operation_client.user_update_branch(branch_name, user, newrev, oldrev)
else
- rugged_add_tag(tag_name, user: user, target: target, message: message)
+ OperationService.new(user, self).update_branch(branch_name, newrev, oldrev)
end
end
end
def rm_branch(branch_name, user:)
- gitaly_migrate(:operation_user_delete_branch, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
- if is_enabled
- gitaly_operations_client.user_delete_branch(branch_name, user)
- else
- OperationService.new(user, self).rm_branch(find_branch(branch_name))
- end
+ wrapped_gitaly_errors do
+ gitaly_operation_client.user_delete_branch(branch_name, user)
end
end
def rm_tag(tag_name, user:)
- gitaly_migrate(:operation_user_delete_tag) do |is_enabled|
- if is_enabled
- gitaly_operations_client.rm_tag(tag_name, user)
- else
- Gitlab::Git::OperationService.new(user, self).rm_tag(find_tag(tag_name))
- end
+ wrapped_gitaly_errors do
+ gitaly_operation_client.rm_tag(tag_name, user)
end
end
@@ -816,72 +651,29 @@ module Gitlab
end
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
+ wrapped_gitaly_errors do
+ gitaly_operation_client.user_merge_branch(user, source_sha, target_branch, message, &block)
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|
- our_commit = start_commit.sha
- their_commit = source_sha
-
- raise 'Invalid merge target' unless our_commit
- raise 'Invalid merge source' unless their_commit
-
- merge_index = rugged.merge_commits(our_commit, their_commit)
- break if merge_index.conflicts?
-
- options = {
- parents: [our_commit, their_commit],
- tree: merge_index.write_tree(rugged),
- message: message,
- author: committer,
- committer: committer
- }
-
- commit_id = create_commit(options)
-
- yield commit_id
-
- commit_id
- end
- rescue Gitlab::Git::CommitError # when merge_index.conflicts?
- nil
- end
-
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
+ wrapped_gitaly_errors do
+ gitaly_operation_client.user_ff_branch(user, source_sha, target_branch)
end
end
def revert(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:)
- gitaly_migrate(:revert, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
- args = {
- user: user,
- commit: commit,
- branch_name: branch_name,
- message: message,
- start_branch_name: start_branch_name,
- start_repository: start_repository
- }
+ args = {
+ user: user,
+ commit: commit,
+ branch_name: branch_name,
+ message: message,
+ start_branch_name: start_branch_name,
+ start_repository: start_repository
+ }
- if is_enabled
- gitaly_operations_client.user_revert(args)
- else
- rugged_revert(args)
- end
+ wrapped_gitaly_errors do
+ gitaly_operation_client.user_revert(args)
end
end
@@ -899,21 +691,17 @@ module Gitlab
end
def cherry_pick(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:)
- gitaly_migrate(:cherry_pick) do |is_enabled|
- args = {
- user: user,
- commit: commit,
- branch_name: branch_name,
- message: message,
- start_branch_name: start_branch_name,
- start_repository: start_repository
- }
+ args = {
+ user: user,
+ commit: commit,
+ branch_name: branch_name,
+ message: message,
+ start_branch_name: start_branch_name,
+ start_repository: start_repository
+ }
- if is_enabled
- gitaly_operations_client.user_cherry_pick(args)
- else
- rugged_cherry_pick(args)
- end
+ wrapped_gitaly_errors do
+ gitaly_operation_client.user_cherry_pick(args)
end
end
@@ -1020,48 +808,20 @@ module Gitlab
# Ex.
# repo.ls_files('master')
#
- # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/327
def ls_files(ref)
- gitaly_migrate(:ls_files) do |is_enabled|
- if is_enabled
- gitaly_ls_files(ref)
- else
- git_ls_files(ref)
- end
- end
+ gitaly_commit_client.ls_files(ref)
end
- # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/328
def copy_gitattributes(ref)
- Gitlab::GitalyClient.migrate(:apply_gitattributes) do |is_enabled|
- if is_enabled
- gitaly_copy_gitattributes(ref)
- else
- rugged_copy_gitattributes(ref)
- end
+ wrapped_gitaly_errors do
+ gitaly_repository_client.apply_gitattributes(ref)
end
- rescue GRPC::InvalidArgument
- raise InvalidRef
end
def info_attributes
return @info_attributes if @info_attributes
- content =
- gitaly_migrate(:get_info_attributes, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
- if is_enabled
- gitaly_repository_client.info_attributes
- else
- attributes_path = File.join(File.expand_path(path), 'info', 'attributes')
-
- if File.exist?(attributes_path)
- File.read(attributes_path)
- else
- ""
- end
- end
- end
-
+ content = gitaly_repository_client.info_attributes
@info_attributes = AttributesParser.new(content)
end
@@ -1090,45 +850,14 @@ module Gitlab
end
def languages(ref = nil)
- gitaly_migrate(:commit_languages, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
- if is_enabled
- gitaly_commit_client.languages(ref)
- else
- ref ||= rugged.head.target_id
- languages = Linguist::Repository.new(rugged, ref).languages
- total = languages.map(&:last).sum
-
- languages = languages.map do |language|
- name, share = language
- color = Linguist::Language[name].color || "##{Digest::SHA256.hexdigest(name)[0...6]}"
- {
- value: (share.to_f * 100 / total).round(2),
- label: name,
- color: color,
- highlight: color
- }
- end
-
- languages.sort do |x, y|
- y[:value] <=> x[:value]
- end
- end
+ wrapped_gitaly_errors do
+ gitaly_commit_client.languages(ref)
end
end
def license_short_name
- gitaly_migrate(:license_short_name,
- status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
- if is_enabled
- gitaly_repository_client.license_short_name
- else
- begin
- # The licensee gem creates a Rugged object from the path:
- # https://github.com/benbalter/licensee/blob/v8.7.0/lib/licensee/projects/git_project.rb
- Licensee.license(path).try(:key)
- rescue Rugged::Error
- end
- end
+ wrapped_gitaly_errors do
+ gitaly_repository_client.license_short_name
end
end
@@ -1175,26 +904,24 @@ module Gitlab
end
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
- rugged_fetch_source_branch(source_repository, source_branch, local_ref)
- end
+ wrapped_gitaly_errors do
+ gitaly_repository_client.fetch_source_branch(source_repository, source_branch, local_ref)
end
end
def compare_source_branch(target_branch_name, source_repository, source_branch_name, straight:)
- with_repo_branch_commit(source_repository, source_branch_name) do |commit|
- break unless commit
-
- Gitlab::Git::Compare.new(
- self,
- target_branch_name,
- commit.sha,
- straight: straight
- )
- end
+ tmp_ref = "refs/tmp/#{SecureRandom.hex}"
+
+ return unless fetch_source_branch!(source_repository, source_branch_name, tmp_ref)
+
+ Gitlab::Git::Compare.new(
+ self,
+ target_branch_name,
+ tmp_ref,
+ straight: straight
+ )
+ ensure
+ delete_refs(tmp_ref)
end
def write_ref(ref_path, ref, old_ref: nil, shell: true)
@@ -1278,16 +1005,7 @@ module Gitlab
end
def create_from_bundle(bundle_path)
- gitaly_migrate(:create_repo_from_bundle) do |is_enabled|
- if is_enabled
- gitaly_repository_client.create_from_bundle(bundle_path)
- else
- run_git!(%W(clone --bare -- #{bundle_path} #{path}), chdir: nil)
- self.class.create_hooks(path, File.expand_path(Gitlab.config.gitlab_shell.hooks_path))
- end
- end
-
- true
+ gitaly_repository_client.create_from_bundle(bundle_path)
end
def create_from_snapshot(url, auth)
@@ -1295,51 +1013,31 @@ module Gitlab
end
def rebase(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:)
- gitaly_migrate(:rebase) do |is_enabled|
- if is_enabled
- gitaly_rebase(user, rebase_id,
- branch: branch,
- branch_sha: branch_sha,
- remote_repository: remote_repository,
- remote_branch: remote_branch)
- else
- git_rebase(user, rebase_id,
- branch: branch,
- branch_sha: branch_sha,
- remote_repository: remote_repository,
- remote_branch: remote_branch)
- end
+ wrapped_gitaly_errors do
+ gitaly_operation_client.user_rebase(user, rebase_id,
+ branch: branch,
+ branch_sha: branch_sha,
+ remote_repository: remote_repository,
+ remote_branch: remote_branch)
end
end
def rebase_in_progress?(rebase_id)
- gitaly_migrate(:rebase_in_progress) do |is_enabled|
- if is_enabled
- gitaly_repository_client.rebase_in_progress?(rebase_id)
- else
- fresh_worktree?(worktree_path(REBASE_WORKTREE_PREFIX, rebase_id))
- end
+ wrapped_gitaly_errors do
+ gitaly_repository_client.rebase_in_progress?(rebase_id)
end
end
def squash(user, squash_id, branch:, start_sha:, end_sha:, author:, message:)
- gitaly_migrate(:squash) do |is_enabled|
- if is_enabled
- gitaly_operation_client.user_squash(user, squash_id, branch,
+ wrapped_gitaly_errors do
+ gitaly_operation_client.user_squash(user, squash_id, branch,
start_sha, end_sha, author, message)
- else
- git_squash(user, squash_id, branch, start_sha, end_sha, author, message)
- end
end
end
def squash_in_progress?(squash_id)
- gitaly_migrate(:squash_in_progress, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
- if is_enabled
- gitaly_repository_client.squash_in_progress?(squash_id)
- else
- fresh_worktree?(worktree_path(SQUASH_WORKTREE_PREFIX, squash_id))
- end
+ wrapped_gitaly_errors do
+ gitaly_repository_client.squash_in_progress?(squash_id)
end
end
@@ -1362,54 +1060,48 @@ module Gitlab
end
def bundle_to_disk(save_path)
- gitaly_migrate(:bundle_to_disk) do |is_enabled|
- if is_enabled
- gitaly_repository_client.create_bundle(save_path)
- else
- run_git!(%W(bundle create #{save_path} --all))
- end
+ wrapped_gitaly_errors do
+ gitaly_repository_client.create_bundle(save_path)
end
true
end
- # rubocop:disable Metrics/ParameterLists
def multi_action(
user, branch_name:, message:, actions:,
author_email: nil, author_name: nil,
start_branch_name: nil, start_repository: self)
- gitaly_migrate(:operation_user_commit_files) do |is_enabled|
- if is_enabled
- gitaly_operation_client.user_commit_files(user, branch_name,
+ wrapped_gitaly_errors do
+ gitaly_operation_client.user_commit_files(user, branch_name,
message, actions, author_email, author_name,
start_branch_name, start_repository)
- else
- rugged_multi_action(user, branch_name, message, actions,
- author_email, author_name, start_branch_name, start_repository)
- end
end
end
- # rubocop:enable Metrics/ParameterLists
def write_config(full_path:)
return unless full_path.present?
- gitaly_migrate(:write_config) do |is_enabled|
- if is_enabled
- gitaly_repository_client.write_config(full_path: full_path)
- else
- rugged_write_config(full_path: full_path)
- end
+ # This guard avoids Gitaly log/error spam
+ raise NoRepository, 'repository does not exist' unless exists?
+
+ set_config('gitlab.fullpath' => full_path)
+ end
+
+ def set_config(entries)
+ wrapped_gitaly_errors do
+ gitaly_repository_client.set_config(entries)
end
end
- def gitaly_repository
- Gitlab::GitalyClient::Util.repository(@storage, @relative_path, @gl_repository)
+ def delete_config(*keys)
+ wrapped_gitaly_errors do
+ gitaly_repository_client.delete_config(keys)
+ end
end
- def gitaly_operations_client
- @gitaly_operations_client ||= Gitlab::GitalyClient::OperationService.new(self)
+ def gitaly_repository
+ Gitlab::GitalyClient::Util.repository(@storage, @relative_path, @gl_repository)
end
def gitaly_ref_client
@@ -1450,12 +1142,22 @@ module Gitlab
raise CommandError.new(e)
end
+ def wrapped_gitaly_errors(&block)
+ yield block
+ rescue GRPC::NotFound => e
+ raise NoRepository.new(e)
+ rescue GRPC::InvalidArgument => e
+ raise ArgumentError.new(e)
+ rescue GRPC::BadStatus => e
+ raise CommandError.new(e)
+ end
+
def clean_stale_repository_files
gitaly_migrate(:repository_cleanup, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
gitaly_repository_client.cleanup if is_enabled && exists?
end
rescue Gitlab::Git::CommandError => e # Don't fail if we can't cleanup
- Rails.logger.error("Unable to clean repository on storage #{storage} with path #{path}: #{e.message}")
+ Rails.logger.error("Unable to clean repository on storage #{storage} with relative path #{relative_path}: #{e.message}")
Gitlab::Metrics.counter(
:failed_repository_cleanup_total,
'Number of failed repository cleanup events'
@@ -1476,25 +1178,14 @@ module Gitlab
safe_query = Regexp.escape(query)
ref ||= root_ref
- gitaly_migrate(:search_files_by_content) do |is_enabled|
- if is_enabled
- gitaly_repository_client.search_files_by_content(ref, safe_query)
- else
- offset = 2
- args = %W(grep -i -I -n -z --before-context #{offset} --after-context #{offset} -E -e #{safe_query} #{ref})
-
- run_git(args).first.scrub.split(/^--\n/)
- end
- end
+ gitaly_repository_client.search_files_by_content(ref, safe_query)
end
def can_be_merged?(source_sha, target_branch)
- gitaly_migrate(:can_be_merged) do |is_enabled|
- if is_enabled
- gitaly_can_be_merged?(source_sha, find_branch(target_branch, true).target)
- else
- rugged_can_be_merged?(source_sha, target_branch)
- end
+ if target_sha = find_branch(target_branch, true)&.target
+ !gitaly_conflicts_client(source_sha, target_sha).conflicts?
+ else
+ false
end
end
@@ -1504,24 +1195,14 @@ module Gitlab
return [] if empty? || safe_query.blank?
- gitaly_migrate(:search_files_by_name) do |is_enabled|
- if is_enabled
- gitaly_repository_client.search_files_by_name(ref, safe_query)
- else
- args = %W(ls-tree -r --name-status --full-tree #{ref} -- #{safe_query})
-
- run_git(args).first.lines.map(&:strip)
- end
- end
+ gitaly_repository_client.search_files_by_name(ref, safe_query)
end
def find_commits_by_message(query, ref, path, limit, offset)
- 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
+ wrapped_gitaly_errors do
+ gitaly_commit_client
+ .commits_by_message(query, revision: ref, path: path, limit: limit, offset: offset)
+ .map { |c| commit(c) }
end
end
@@ -1531,16 +1212,12 @@ module Gitlab
end
def last_commit_for_path(sha, path)
- gitaly_migrate(:last_commit_for_path) do |is_enabled|
- if is_enabled
- last_commit_for_path_by_gitaly(sha, path)
- else
- last_commit_for_path_by_rugged(sha, path)
- end
+ wrapped_gitaly_errors do
+ gitaly_commit_client.last_commit_for_path(sha, path)
end
end
- def rev_list(including: [], excluding: [], objects: false, &block)
+ def rev_list(including: [], excluding: [], options: [], objects: false, &block)
args = ['rev-list']
args.push(*rev_list_param(including))
@@ -1553,11 +1230,11 @@ module Gitlab
args.push('--objects') if objects
- run_git!(args, lazy_block: block)
- end
+ if options.any?
+ args.push(*options)
+ end
- def missed_ref(oldrev, newrev)
- run_git!(['rev-list', '--max-count=1', oldrev, "^#{newrev}"])
+ run_git!(args, lazy_block: block)
end
def with_worktree(worktree_path, branch, sparse_checkout_files: nil, env:)
@@ -1599,12 +1276,8 @@ module Gitlab
private
def uncached_has_local_branches?
- gitaly_migrate(:has_local_branches, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
- if is_enabled
- gitaly_repository_client.has_local_branches?
- else
- has_local_branches_rugged?
- end
+ wrapped_gitaly_errors do
+ gitaly_repository_client.has_local_branches?
end
end
@@ -1667,21 +1340,6 @@ module Gitlab
end
end
- # This function is duplicated in Gitaly-Go, don't change it!
- # https://gitlab.com/gitlab-org/gitaly/merge_requests/698
- def fresh_worktree?(path)
- File.exist?(path) && !clean_stuck_worktree(path)
- end
-
- # This function is duplicated in Gitaly-Go, don't change it!
- # https://gitlab.com/gitlab-org/gitaly/merge_requests/698
- def clean_stuck_worktree(path)
- return false unless File.mtime(path) < 15.minutes.ago
-
- FileUtils.rm_rf(path)
- true
- end
-
# Adding a worktree means checking out the repository. For large repos,
# this can be very expensive, so set up sparse checkout for the worktree
# to only check out the files we're interested in.
@@ -1724,20 +1382,6 @@ module Gitlab
}
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)
- branches = rugged.branches.each(filter).map do |rugged_ref|
- begin
- target_commit = Gitlab::Git::Commit.find(self, rugged_ref.target)
- Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target, target_commit)
- rescue Rugged::ReferenceError
- # Omit invalid branch
- end
- end.compact
-
- sort_branches(branches, sort_by)
- end
-
def git_merged_branch_names(branch_names, root_sha)
git_arguments =
%W[branch --merged #{root_sha}
@@ -1780,46 +1424,6 @@ module Gitlab
end
end
- # Gitaly note: JV: although #log_by_shell shells out to Git I think the
- # complexity is such that we should migrate it as Ruby before trying to
- # do it in Go.
- def log_by_shell(sha, options)
- limit = options[:limit].to_i
- offset = options[:offset].to_i
- use_follow_flag = options[:follow] && options[:path].present?
-
- # We will perform the offset in Ruby because --follow doesn't play well with --skip.
- # See: https://gitlab.com/gitlab-org/gitlab-ce/issues/3574#note_3040520
- offset_in_ruby = use_follow_flag && options[:offset].present?
- limit += offset if offset_in_ruby
-
- cmd = %w[log]
- cmd << "--max-count=#{limit}"
- cmd << '--format=%H'
- cmd << "--skip=#{offset}" unless offset_in_ruby
- cmd << '--follow' if use_follow_flag
- cmd << '--no-merges' if options[:skip_merges]
- cmd << "--after=#{options[:after].iso8601}" if options[:after]
- cmd << "--before=#{options[:before].iso8601}" if options[:before]
-
- if options[:all]
- cmd += %w[--all --reverse]
- else
- cmd << sha
- end
-
- # :path can be a string or an array of strings
- if options[:path].present?
- cmd << '--'
- cmd += Array(options[:path])
- end
-
- raw_output, _status = run_git(cmd)
- lines = offset_in_ruby ? raw_output.lines.drop(offset) : raw_output.lines
-
- lines.map! { |c| Rugged::Commit.new(rugged, c.strip) }
- end
-
# We are trying to deprecate this method because it does a lot of work
# but it seems to be used only to look up submodule URL's.
# https://gitlab.com/gitlab-org/gitaly/issues/329
@@ -1949,137 +1553,6 @@ module Gitlab
end
end
- def tags_from_rugged
- rugged.references.each("refs/tags/*").map do |ref|
- message = nil
-
- if ref.target.is_a?(Rugged::Tag::Annotation)
- tag_message = ref.target.message
-
- if tag_message.respond_to?(:chomp)
- message = tag_message.chomp
- end
- end
-
- target_commit = Gitlab::Git::Commit.find(self, ref.target)
- Gitlab::Git::Tag.new(self, {
- name: ref.name,
- target: ref.target,
- target_commit: target_commit,
- message: message
- })
- end.sort_by(&:name)
- end
-
- def last_commit_for_path_by_rugged(sha, path)
- sha = last_commit_id_for_path_by_shelling_out(sha, path)
- commit(sha)
- end
-
- def tags_from_gitaly
- gitaly_ref_client.tags
- end
-
- def size_by_shelling_out
- popen(%w(du -sk), path).first.strip.to_i
- end
-
- def size_by_gitaly
- gitaly_repository_client.repository_size
- end
-
- def count_commits_by_gitaly(options)
- if options[:left_right]
- from = options[:from]
- to = options[:to]
-
- right_count = gitaly_commit_client
- .commit_count("#{from}..#{to}", options)
- left_count = gitaly_commit_client
- .commit_count("#{to}..#{from}", options)
-
- [left_count, right_count]
- else
- gitaly_commit_client.commit_count(options[:ref], options)
- end
- end
-
- def count_commits_by_shelling_out(options)
- cmd = count_commits_shelling_command(options)
-
- raw_output, _status = run_git(cmd)
-
- process_count_commits_raw_output(raw_output, options)
- end
-
- def count_commits_shelling_command(options)
- cmd = %w[rev-list]
- cmd << "--after=#{options[:after].iso8601}" if options[:after]
- cmd << "--before=#{options[:before].iso8601}" if options[:before]
- cmd << "--max-count=#{options[:max_count]}" if options[:max_count]
- cmd << "--left-right" if options[:left_right]
- cmd << '--count'
-
- cmd << if options[:all]
- '--all'
- elsif options[:ref]
- options[:ref]
- else
- raise ArgumentError, "Please specify a valid ref or set the 'all' attribute to true"
- end
-
- cmd += %W[-- #{options[:path]}] if options[:path].present?
- cmd
- end
-
- def process_count_commits_raw_output(raw_output, options)
- if options[:left_right]
- result = raw_output.scan(/\d+/).map(&:to_i)
-
- if result.sum != options[:max_count]
- result
- else # Reaching max count, right is not accurate
- right_option =
- process_count_commits_options(options
- .except(:left_right, :from, :to)
- .merge(ref: options[:to]))
-
- right = count_commits_by_shelling_out(right_option)
-
- [result.first, right] # left should be accurate in the first call
- end
- else
- raw_output.to_i
- end
- end
-
- def gitaly_ls_files(ref)
- gitaly_commit_client.ls_files(ref)
- end
-
- def git_ls_files(ref)
- actual_ref = ref || root_ref
-
- begin
- sha_from_ref(actual_ref)
- rescue Rugged::OdbError, Rugged::InvalidError, Rugged::ReferenceError
- # Return an empty array if the ref wasn't found
- return []
- end
-
- cmd = %W(ls-tree -r --full-tree --full-name -- #{actual_ref})
- raw_output, _status = run_git(cmd)
-
- lines = raw_output.split("\n").map do |f|
- stuff, path = f.split("\t")
- _mode, type, _sha = stuff.split(" ")
- path if type == "blob"
- # Contain only blob type
- end
-
- lines.compact
- end
-
# Returns true if the given ref name exists
#
# Ref names must start with `refs/`.
@@ -2119,33 +1592,6 @@ module Gitlab
false
end
- def gitaly_add_tag(tag_name, user:, target:, message: nil)
- gitaly_operations_client.add_tag(tag_name, user, target, message)
- end
-
- def rugged_add_tag(tag_name, user:, target:, message: nil)
- target_object = Ref.dereference_object(lookup(target))
- raise InvalidRef.new("target not found: #{target}") unless target_object
-
- user = Gitlab::Git::User.from_gitlab(user) unless user.respond_to?(:gl_id)
-
- options = nil # Use nil, not the empty hash. Rugged cares about this.
- if message
- options = {
- message: message,
- tagger: Gitlab::Git.committer_hash(email: user.email, name: user.name)
- }
- end
-
- Gitlab::Git::OperationService.new(user, self).add_tag(tag_name, target_object.oid, options)
-
- find_tag(tag_name)
- rescue Rugged::ReferenceError => ex
- raise InvalidRef, ex
- rescue Rugged::TagError
- raise TagExistsError
- end
-
def rugged_create_branch(ref, start_point)
rugged_ref = rugged.branches.create(ref, start_point)
target_commit = Gitlab::Git::Commit.find(self, rugged_ref.target)
@@ -2190,28 +1636,6 @@ module Gitlab
end
end
- def rugged_revert(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:)
- OperationService.new(user, self).with_branch(
- branch_name,
- start_branch_name: start_branch_name,
- start_repository: start_repository
- ) do |start_commit|
-
- Gitlab::Git.check_namespace!(commit, start_repository)
-
- revert_tree_id = check_revert_content(commit, start_commit.sha)
- raise CreateTreeError unless revert_tree_id
-
- committer = user_to_committer(user)
-
- create_commit(message: message,
- author: committer,
- committer: committer,
- tree: revert_tree_id,
- parents: [start_commit.sha])
- end
- end
-
def rugged_cherry_pick(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:)
OperationService.new(user, self).with_branch(
branch_name,
@@ -2251,72 +1675,6 @@ module Gitlab
tree_id
end
- def gitaly_rebase(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:)
- gitaly_operation_client.user_rebase(user, rebase_id,
- branch: branch,
- branch_sha: branch_sha,
- remote_repository: remote_repository,
- remote_branch: remote_branch)
- end
-
- def git_rebase(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:)
- rebase_path = worktree_path(REBASE_WORKTREE_PREFIX, rebase_id)
- env = git_env_for_user(user)
-
- if remote_repository.is_a?(RemoteRepository)
- env.merge!(remote_repository.fetch_env)
- remote_repo_path = GITALY_INTERNAL_URL
- else
- remote_repo_path = remote_repository.path
- end
-
- with_worktree(rebase_path, branch, env: env) do
- run_git!(
- %W(pull --rebase #{remote_repo_path} #{remote_branch}),
- chdir: rebase_path, env: env
- )
-
- rebase_sha = run_git!(%w(rev-parse HEAD), chdir: rebase_path, env: env).strip
-
- Gitlab::Git::OperationService.new(user, self)
- .update_branch(branch, rebase_sha, branch_sha)
-
- rebase_sha
- end
- end
-
- def git_squash(user, squash_id, branch, start_sha, end_sha, author, message)
- squash_path = worktree_path(SQUASH_WORKTREE_PREFIX, squash_id)
- env = git_env_for_user(user).merge(
- 'GIT_AUTHOR_NAME' => author.name,
- 'GIT_AUTHOR_EMAIL' => author.email
- )
- diff_range = "#{start_sha}...#{end_sha}"
- diff_files = run_git!(
- %W(diff --name-only --diff-filter=ar --binary #{diff_range})
- ).chomp
-
- with_worktree(squash_path, branch, sparse_checkout_files: diff_files, env: env) do
- # Apply diff of the `diff_range` to the worktree
- diff = run_git!(%W(diff --binary #{diff_range}))
- run_git!(%w(apply --index --whitespace=nowarn), chdir: squash_path, env: env) do |stdin|
- stdin.binmode
- stdin.write(diff)
- end
-
- # Commit the `diff_range` diff
- run_git!(%W(commit --no-verify --message #{message}), chdir: squash_path, env: env)
-
- # Return the squash sha. May print a warning for ambiguous refs, but
- # we can ignore that with `--quiet` and just take the SHA, if present.
- # HEAD here always refers to the current HEAD commit, even if there is
- # another ref called HEAD.
- run_git!(
- %w(rev-parse --quiet --verify HEAD), chdir: squash_path, env: env
- ).chomp
- end
- end
-
def local_fetch_ref(source_path, source_ref:, target_ref:)
args = %W(fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref})
run_git(args)
@@ -2328,22 +1686,6 @@ module Gitlab
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, InvalidRef
- raise ArgumentError, 'Invalid merge source'
- end
-
def rugged_add_remote(remote_name, url, mirror_refmap)
rugged.remotes.create(remote_name, url)
@@ -2395,100 +1737,20 @@ module Gitlab
remove_remote(remote_name)
end
- def rugged_multi_action(
- user, branch_name, message, actions, author_email, author_name,
- start_branch_name, start_repository)
-
- OperationService.new(user, self).with_branch(
- branch_name,
- start_branch_name: start_branch_name,
- start_repository: start_repository
- ) do |start_commit|
- index = Gitlab::Git::Index.new(self)
- parents = []
-
- if start_commit
- index.read_tree(start_commit.rugged_commit.tree)
- parents = [start_commit.sha]
- end
-
- actions.each { |opts| index.apply(opts.delete(:action), opts) }
-
- committer = user_to_committer(user)
- author = Gitlab::Git.committer_hash(email: author_email, name: author_name) || committer
- options = {
- tree: index.write_tree,
- message: message,
- parents: parents,
- author: author,
- committer: committer
- }
-
- create_commit(options)
- end
- end
-
def fetch_remote(remote_name = 'origin', env: nil)
run_git(['fetch', remote_name], env: env).last.zero?
end
- def gitaly_can_be_merged?(their_commit, our_commit)
- !gitaly_conflicts_client(our_commit, their_commit).conflicts?
- end
-
- def rugged_can_be_merged?(their_commit, our_commit)
- !rugged.merge_commits(our_commit, their_commit).conflicts?
- end
-
def gitlab_projects_error
raise CommandError, @gitlab_projects.output
end
- def find_commits_by_message_by_shelling_out(query, ref, path, limit, offset)
- ref ||= root_ref
-
- args = %W(
- log #{ref} --pretty=%H --skip #{offset}
- --max-count #{limit} --grep=#{query} --regexp-ignore-case
- )
- args = args.concat(%W(-- #{path})) if path.present?
-
- git_log_results = run_git(args).first.lines
-
- git_log_results.map { |c| commit(c.chomp) }.compact
- end
-
- def find_commits_by_message_by_gitaly(query, ref, path, limit, offset)
- gitaly_commit_client
- .commits_by_message(query, revision: ref, path: path, limit: limit, offset: offset)
- .map { |c| commit(c) }
- end
-
- def last_commit_for_path_by_gitaly(sha, path)
- gitaly_commit_client.last_commit_for_path(sha, path)
- end
-
- def last_commit_id_for_path_by_shelling_out(sha, path)
- args = %W(rev-list --max-count=1 #{sha} -- #{path})
- run_git_with_timeout(args, Gitlab::Git::Popen::FAST_GIT_PROCESS_TIMEOUT).first.strip
- end
-
def rugged_merge_base(from, to)
rugged.merge_base(from, to)
rescue Rugged::ReferenceError
nil
end
- def rugged_commit_count(ref)
- walker = Rugged::Walker.new(rugged)
- walker.sorting(Rugged::SORT_TOPO | Rugged::SORT_REVERSE)
- oid = rugged.rev_parse_oid(ref)
- walker.push(oid)
- walker.count
- rescue Rugged::ReferenceError
- 0
- end
-
def rev_list_param(spec)
spec == :all ? ['--all'] : spec
end
@@ -2496,35 +1758,6 @@ module Gitlab
def sha_from_ref(ref)
rev_parse_target(ref).oid
end
-
- def build_git_cmd(*args)
- object_directories = alternate_object_directories.join(File::PATH_SEPARATOR)
-
- env = { 'PWD' => self.path }
- env['GIT_ALTERNATE_OBJECT_DIRECTORIES'] = object_directories if object_directories.present?
-
- [
- env,
- ::Gitlab.config.git.bin_path,
- *args,
- { chdir: self.path }
- ]
- end
-
- def git_diff_cmd(old_rev, new_rev)
- old_rev = old_rev == ::Gitlab::Git::BLANK_SHA ? ::Gitlab::Git::EMPTY_TREE_ID : old_rev
-
- build_git_cmd('diff', old_rev, new_rev, '--raw')
- end
-
- def git_cat_file_cmd
- format = '%(objectname) %(objectsize) %(rest)'
- build_git_cmd('cat-file', "--batch-check=#{format}")
- end
-
- def format_git_cat_file_script
- File.expand_path('../support/format-git-cat-file-input', __FILE__)
- end
end
end
end
diff --git a/lib/gitlab/git/rev_list.rb b/lib/gitlab/git/rev_list.rb
index 38c3a55f96f..5fdad077eea 100644
--- a/lib/gitlab/git/rev_list.rb
+++ b/lib/gitlab/git/rev_list.rb
@@ -1,5 +1,3 @@
-# Gitaly note: JV: will probably be migrated indirectly by migrating the call sites.
-
module Gitlab
module Git
class RevList
@@ -27,9 +25,10 @@ module Gitlab
#
# 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)
+ def new_objects(options: [], require_path: nil, not_in: nil, &lazy_block)
opts = {
including: newrev,
+ options: options,
excluding: not_in.nil? ? :all : not_in,
require_path: require_path
}
@@ -37,15 +36,11 @@ module Gitlab
get_objects(opts, &lazy_block)
end
- def all_objects(require_path: nil, &lazy_block)
- get_objects(including: :all, 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.
- def missed_ref
- repository.missed_ref(oldrev, newrev).split("\n")
+ def all_objects(options: [], require_path: nil, &lazy_block)
+ get_objects(including: :all,
+ options: options,
+ require_path: require_path,
+ &lazy_block)
end
private
@@ -54,8 +49,8 @@ module Gitlab
repository.rev_list(args).split("\n")
end
- def get_objects(including: [], excluding: [], require_path: nil)
- opts = { including: including, excluding: excluding, objects: true }
+ def get_objects(including: [], excluding: [], options: [], require_path: nil)
+ opts = { including: including, excluding: excluding, options: options, objects: true }
repository.rev_list(opts) do |lazy_output|
objects = objects_from_output(lazy_output, require_path: require_path)
diff --git a/lib/gitlab/git/storage/checker.rb b/lib/gitlab/git/storage/checker.rb
index 2f611cef37b..391f0d70583 100644
--- a/lib/gitlab/git/storage/checker.rb
+++ b/lib/gitlab/git/storage/checker.rb
@@ -35,7 +35,7 @@ module Gitlab
def initialize(storage, logger = Rails.logger)
@storage = storage
config = Gitlab.config.repositories.storages[@storage]
- @storage_path = config.legacy_disk_path
+ @storage_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access { config.legacy_disk_path }
@logger = logger
@hostname = Gitlab::Environment.hostname
diff --git a/lib/gitlab/git/storage/circuit_breaker.rb b/lib/gitlab/git/storage/circuit_breaker.rb
index e35054466ff..62427ac9cc4 100644
--- a/lib/gitlab/git/storage/circuit_breaker.rb
+++ b/lib/gitlab/git/storage/circuit_breaker.rb
@@ -22,13 +22,14 @@ module Gitlab
def self.build(storage, hostname = Gitlab::Environment.hostname)
config = Gitlab.config.repositories.storages[storage]
-
- if !config.present?
- NullCircuitBreaker.new(storage, hostname, error: Misconfiguration.new("Storage '#{storage}' is not configured"))
- elsif !config.legacy_disk_path.present?
- NullCircuitBreaker.new(storage, hostname, error: Misconfiguration.new("Path for storage '#{storage}' is not configured"))
- else
- new(storage, hostname)
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ if !config.present?
+ NullCircuitBreaker.new(storage, hostname, error: Misconfiguration.new("Storage '#{storage}' is not configured"))
+ elsif !config.legacy_disk_path.present?
+ NullCircuitBreaker.new(storage, hostname, error: Misconfiguration.new("Path for storage '#{storage}' is not configured"))
+ else
+ new(storage, hostname)
+ end
end
end
diff --git a/lib/gitlab/git/support/format-git-cat-file-input b/lib/gitlab/git/support/format-git-cat-file-input
deleted file mode 100755
index 2e93c646d0f..00000000000
--- a/lib/gitlab/git/support/format-git-cat-file-input
+++ /dev/null
@@ -1,21 +0,0 @@
-#!/usr/bin/env ruby
-
-# This script formats the output of the `git diff <old_rev> <new_rev> --raw`
-# command so it can be processed by `git cat-file`
-
-# We need to convert this:
-# ":100644 100644 5f53439... 85bc2f9... R060\tfiles/js/commit.js.coffee\tfiles/js/commit.coffee"
-# To:
-# "85bc2f9 R\tfiles/js/commit.js.coffee\tfiles/js/commit.coffee"
-
-ARGF.each do |line|
- _, _, old_blob_id, new_blob_id, rest = line.split(/\s/, 5)
-
- old_blob_id.gsub!(/[^\h]/, '')
- new_blob_id.gsub!(/[^\h]/, '')
-
- # We can't pass '0000000...' to `git cat-file` given it will not return info about the deleted file
- blob_id = new_blob_id =~ /\A0+\z/ ? old_blob_id : new_blob_id
-
- $stdout.puts "#{blob_id} #{rest}"
-end
diff --git a/lib/gitlab/git/tag.rb b/lib/gitlab/git/tag.rb
index e44284572fd..bbf2ecdb1fa 100644
--- a/lib/gitlab/git/tag.rb
+++ b/lib/gitlab/git/tag.rb
@@ -28,18 +28,7 @@ module Gitlab
end
def get_messages(repository, tag_ids)
- repository.gitaly_migrate(:tag_messages) do |is_enabled|
- if is_enabled
- repository.gitaly_ref_client.get_tag_messages(tag_ids)
- else
- tag_ids.map do |id|
- tag = repository.rugged.lookup(id)
- message = tag.is_a?(Rugged::Commit) ? "" : tag.message
-
- [id, message]
- end.to_h
- end
- end
+ repository.gitaly_ref_client.get_tag_messages(tag_ids)
end
end
diff --git a/lib/gitlab/git/tree.rb b/lib/gitlab/git/tree.rb
index b6ceb542dd1..cb851b76a23 100644
--- a/lib/gitlab/git/tree.rb
+++ b/lib/gitlab/git/tree.rb
@@ -1,5 +1,3 @@
-# Gitaly note: JV: needs 1 RPC, migration is in progress.
-
module Gitlab
module Git
class Tree
@@ -17,12 +15,8 @@ module Gitlab
def where(repository, sha, path = nil, recursive = false)
path = nil if path == '' || path == '/'
- Gitlab::GitalyClient.migrate(:tree_entries) do |is_enabled|
- if is_enabled
- repository.gitaly_commit_client.tree_entries(repository, sha, path, recursive)
- else
- tree_entries_from_rugged(repository, sha, path, recursive)
- end
+ repository.wrapped_gitaly_errors do
+ repository.gitaly_commit_client.tree_entries(repository, sha, path, recursive)
end
end
diff --git a/lib/gitlab/git/version.rb b/lib/gitlab/git/version.rb
new file mode 100644
index 00000000000..1e14e8b652a
--- /dev/null
+++ b/lib/gitlab/git/version.rb
@@ -0,0 +1,11 @@
+module Gitlab
+ module Git
+ module Version
+ extend Gitlab::Git::Popen
+
+ def self.git_version
+ Gitlab::VersionInfo.parse(Gitaly::Server.all.first.git_binary_version)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb
index 1ab8c4e0229..8ee46b59830 100644
--- a/lib/gitlab/git/wiki.rb
+++ b/lib/gitlab/git/wiki.rb
@@ -27,63 +27,38 @@ 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)
- else
- gollum_write_page(name, format, content, commit_details)
- end
+ @repository.wrapped_gitaly_errors do
+ gitaly_write_page(name, format, content, commit_details)
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)
- else
- gollum_delete_page(page_path, commit_details)
- end
+ @repository.wrapped_gitaly_errors do
+ gitaly_delete_page(page_path, commit_details)
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)
- else
- gollum_update_page(page_path, title, format, content, commit_details)
- end
+ @repository.wrapped_gitaly_errors do
+ gitaly_update_page(page_path, title, format, content, commit_details)
end
end
def pages(limit: nil)
- @repository.gitaly_migrate(:wiki_get_all_pages) do |is_enabled|
- if is_enabled
- gitaly_get_all_pages
- else
- gollum_get_all_pages(limit: limit)
- end
+ @repository.wrapped_gitaly_errors do
+ gitaly_get_all_pages
end
end
def page(title:, version: nil, dir: nil)
- @repository.gitaly_migrate(:wiki_find_page,
- status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) 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
+ @repository.wrapped_gitaly_errors do
+ gitaly_find_page(title: title, version: version, dir: dir)
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
+ @repository.wrapped_gitaly_errors do
+ gitaly_find_file(name, version)
end
end
@@ -92,24 +67,15 @@ module Gitlab
# :per_page - The number of items per page.
# :limit - Total number of items to return.
def page_versions(page_path, options = {})
- @repository.gitaly_migrate(:wiki_page_versions) do |is_enabled|
- if is_enabled
- versions = gitaly_wiki_client.page_versions(page_path, options)
-
- # Gitaly uses gollum-lib to get the versions. Gollum defaults to 20
- # per page, but also fetches 20 if `limit` or `per_page` < 20.
- # Slicing returns an array with the expected number of items.
- slice_bound = options[:limit] || options[:per_page] || Gollum::Page.per_page
- versions[0..slice_bound]
- else
- 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
+ versions = @repository.wrapped_gitaly_errors do
+ gitaly_wiki_client.page_versions(page_path, options)
end
+
+ # Gitaly uses gollum-lib to get the versions. Gollum defaults to 20
+ # per page, but also fetches 20 if `limit` or `per_page` < 20.
+ # Slicing returns an array with the expected number of items.
+ slice_bound = options[:limit] || options[:per_page] || Gollum::Page.per_page
+ versions[0..slice_bound]
end
def count_page_versions(page_path)
@@ -131,46 +97,13 @@ module Gitlab
def page_formatted_data(title:, dir: nil, version: nil)
version = version&.id
- @repository.gitaly_migrate(:wiki_page_formatted_data, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
- if is_enabled
- gitaly_wiki_client.get_formatted_data(title: title, dir: dir, version: version)
- else
- # We don't use #page because if wiki_find_page feature is enabled, we would
- # get a page without formatted_data.
- gollum_find_page(title: title, dir: dir, version: version)&.formatted_data
- end
+ @repository.wrapped_gitaly_errors do
+ gitaly_wiki_client.get_formatted_data(title: title, dir: dir, version: version)
end
end
- def gollum_wiki
- @gollum_wiki ||= Gollum::Wiki.new(@repository.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_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
@@ -199,65 +132,6 @@ module Gitlab
@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)
-
- with_committer_with_hooks(commit_details) do |committer|
- filename = File.basename(name)
- dir = (tmp_dir = File.dirname(name)) == '.' ? '' : tmp_dir
-
- gollum_wiki.write_page(filename, format, content, { committer: committer }, dir)
- end
- rescue Gollum::DuplicatePageError => e
- raise Gitlab::Git::Wiki::DuplicatePageError, e.message
- end
-
- def gollum_delete_page(page_path, commit_details)
- assert_type!(commit_details, CommitDetails)
-
- with_committer_with_hooks(commit_details) do |committer|
- gollum_wiki.delete_page(gollum_page_by_path(page_path), committer: committer)
- end
- end
-
- def gollum_update_page(page_path, title, format, content, commit_details)
- assert_type!(format, Symbol)
- assert_type!(commit_details, CommitDetails)
-
- with_committer_with_hooks(commit_details) do |committer|
- page = gollum_page_by_path(page_path)
- # Instead of performing two renames if the title has changed,
- # the update_page will only update the format and content and
- # the rename_page will do anything related to moving/renaming
- gollum_wiki.update_page(page, page.name, format, content, committer: committer)
- gollum_wiki.rename_page(page, title, committer: committer)
- end
- end
-
- def gollum_find_page(title:, version: nil, dir: nil)
- if version
- version = Gitlab::Git::Commit.find(@repository, version).id
- end
-
- gollum_page = gollum_wiki.page(title, version, dir)
- return unless gollum_page
-
- new_page(gollum_page)
- end
-
- def gollum_find_file(name, version)
- version ||= self.class.default_ref
- gollum_file = gollum_wiki.file(name, version)
- return unless gollum_file
-
- Gitlab::Git::WikiFile.new(gollum_file)
- end
-
- def gollum_get_all_pages(limit: nil)
- gollum_wiki.pages(limit: limit).map { |gollum_page| new_page(gollum_page) }
- end
-
def gitaly_write_page(name, format, content, commit_details)
gitaly_wiki_client.write_page(name, format, content, commit_details)
end
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index db7c29be94b..35808149b90 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -2,6 +2,8 @@
# class return an instance of `GitlabAccessStatus`
module Gitlab
class GitAccess
+ include Gitlab::Utils::StrongMemoize
+
UnauthorizedError = Class.new(StandardError)
NotFoundError = Class.new(StandardError)
ProjectCreationError = Class.new(StandardError)
@@ -26,7 +28,7 @@ module Gitlab
PUSH_COMMANDS = %w{ git-receive-pack }.freeze
ALL_COMMANDS = DOWNLOAD_COMMANDS + PUSH_COMMANDS
- attr_reader :actor, :project, :protocol, :authentication_abilities, :namespace_path, :project_path, :redirected_path, :auth_result_type
+ attr_reader :actor, :project, :protocol, :authentication_abilities, :namespace_path, :project_path, :redirected_path, :auth_result_type, :changes
def initialize(actor, project, protocol, authentication_abilities:, namespace_path: nil, project_path: nil, redirected_path: nil, auth_result_type: nil)
@actor = actor
@@ -40,6 +42,8 @@ module Gitlab
end
def check(cmd, changes)
+ @changes = changes
+
check_protocol!
check_valid_actor!
check_active_user!
@@ -58,7 +62,7 @@ module Gitlab
when *DOWNLOAD_COMMANDS
check_download_access!
when *PUSH_COMMANDS
- check_push_access!(changes)
+ check_push_access!
end
true
@@ -218,7 +222,7 @@ module Gitlab
end
end
- def check_push_access!(changes)
+ def check_push_access!
if project.repository_read_only?
raise UnauthorizedError, ERROR_MESSAGES[:read_only]
end
@@ -235,17 +239,15 @@ module Gitlab
return if changes.blank? # Allow access this is needed for EE.
- check_change_access!(changes)
+ check_change_access!
end
- def check_change_access!(changes)
+ def check_change_access!
# If there are worktrees with a HEAD pointing to a non-existent object,
# calls to `git rev-list --all` will fail in git 2.15+. This should also
# clear stale lock files.
project.repository.clean_stale_repository_files
- changes_list = Gitlab::ChangesList.new(changes)
-
# Iterate over all changes to find if user allowed all of them to be applied
changes_list.each.with_index do |change, index|
first_change = index == 0
@@ -321,6 +323,10 @@ module Gitlab
protected
+ def changes_list
+ @changes_list ||= Gitlab::ChangesList.new(changes)
+ end
+
def user
return @user if defined?(@user)
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index 0abae70c443..58a4060cc96 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -186,6 +186,8 @@ module Gitlab
metadata['call_site'] = feature.to_s if feature
metadata['gitaly-servers'] = address_metadata(remote_storage) if remote_storage
+ metadata.merge!(server_feature_flags)
+
result = { metadata: metadata }
# nil timeout indicates that we should use the default
@@ -204,6 +206,14 @@ module Gitlab
result
end
+ SERVER_FEATURE_FLAGS = %w[gogit_findcommit].freeze
+
+ def self.server_feature_flags
+ SERVER_FEATURE_FLAGS.map do |f|
+ ["gitaly-feature-#{f.tr('_', '-')}", feature_enabled?(f).to_s]
+ end.to_h
+ end
+
def self.token(storage)
params = Gitlab.config.repositories.storages[storage]
raise "storage not found: #{storage.inspect}" if params.nil?
@@ -234,10 +244,21 @@ module Gitlab
when MigrationStatus::OPT_OUT
true
when MigrationStatus::OPT_IN
- opt_into_all_features?
+ opt_into_all_features? && !explicit_opt_in_required.include?(feature_name)
else
false
end
+ rescue => ex
+ # During application startup feature lookups in SQL can fail
+ Rails.logger.warn "exception while checking Gitaly feature status for #{feature_name}: #{ex}"
+ false
+ end
+
+ # We have a mechanism to let GitLab automatically opt in to all Gitaly
+ # features. We want to be able to exclude some features from automatic
+ # opt-in. This function has an override in EE.
+ def self.explicit_opt_in_required
+ []
end
# opt_into_all_features? returns true when the current environment
@@ -380,8 +401,8 @@ module Gitlab
path.read.chomp
end
- def self.timestamp(t)
- Google::Protobuf::Timestamp.new(seconds: t.to_i)
+ def self.timestamp(time)
+ Google::Protobuf::Timestamp.new(seconds: time.to_i)
end
# The default timeout on all Gitaly calls
@@ -408,7 +429,7 @@ module Gitlab
def self.count_stack
return unless RequestStore.active?
- stack_string = caller.drop(1).join("\n")
+ stack_string = Gitlab::Profiler.clean_backtrace(caller).drop(1).join("\n")
RequestStore.store[:stack_counter] ||= Hash.new
diff --git a/lib/gitlab/gitaly_client/blob_service.rb b/lib/gitlab/gitaly_client/blob_service.rb
index 28554208984..1840bf45154 100644
--- a/lib/gitlab/gitaly_client/blob_service.rb
+++ b/lib/gitlab/gitaly_client/blob_service.rb
@@ -13,7 +13,7 @@ module Gitlab
oid: oid,
limit: limit
)
- response = GitalyClient.call(@gitaly_repo.storage_name, :blob_service, :get_blob, request)
+ response = GitalyClient.call(@gitaly_repo.storage_name, :blob_service, :get_blob, request, timeout: GitalyClient.fast_timeout)
data = ''
blob = nil
@@ -43,7 +43,7 @@ module Gitlab
blob_ids: blob_ids
)
- response = GitalyClient.call(@gitaly_repo.storage_name, :blob_service, :get_lfs_pointers, request)
+ response = GitalyClient.call(@gitaly_repo.storage_name, :blob_service, :get_lfs_pointers, request, timeout: GitalyClient.medium_timeout)
map_lfs_pointers(response)
end
@@ -66,7 +66,7 @@ module Gitlab
:blob_service,
:get_blobs,
request,
- timeout: GitalyClient.default_timeout
+ timeout: GitalyClient.fast_timeout
)
GitalyClient::BlobsStitcher.new(response)
@@ -85,7 +85,7 @@ module Gitlab
request.not_in_refs += not_in
end
- response = GitalyClient.call(@gitaly_repo.storage_name, :blob_service, :get_new_lfs_pointers, request)
+ response = GitalyClient.call(@gitaly_repo.storage_name, :blob_service, :get_new_lfs_pointers, request, timeout: GitalyClient.medium_timeout)
map_lfs_pointers(response)
end
@@ -96,7 +96,7 @@ module Gitlab
revision: encode_binary(revision)
)
- response = GitalyClient.call(@gitaly_repo.storage_name, :blob_service, :get_all_lfs_pointers, request)
+ response = GitalyClient.call(@gitaly_repo.storage_name, :blob_service, :get_all_lfs_pointers, request, timeout: GitalyClient.medium_timeout)
map_lfs_pointers(response)
end
diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb
index 1f5f88bf792..6a97cd8ed17 100644
--- a/lib/gitlab/gitaly_client/commit_service.rb
+++ b/lib/gitlab/gitaly_client/commit_service.rb
@@ -70,15 +70,22 @@ module Gitlab
def commit_deltas(commit)
request = Gitaly::CommitDeltaRequest.new(diff_from_parent_request_params(commit))
- response = GitalyClient.call(@repository.storage, :diff_service, :commit_delta, request)
+ response = GitalyClient.call(@repository.storage, :diff_service, :commit_delta, request, timeout: GitalyClient.fast_timeout)
response.flat_map { |msg| msg.deltas }
end
def tree_entry(ref, path, limit = nil)
+ if Pathname.new(path).cleanpath.to_s.start_with?('../')
+ # The TreeEntry RPC should return an empty reponse in this case but in
+ # Gitaly 0.107.0 and earlier we get an exception instead. This early return
+ # saves us a Gitaly roundtrip while also avoiding the exception.
+ return
+ end
+
request = Gitaly::TreeEntryRequest.new(
repository: @gitaly_repo,
- revision: ref,
+ revision: encode_binary(ref),
path: encode_binary(path),
limit: limit.to_i
)
@@ -179,6 +186,8 @@ module Gitlab
end
def list_commits_by_oid(oids)
+ return [] if oids.empty?
+
request = Gitaly::ListCommitsByOidRequest.new(repository: @gitaly_repo, oid: oids)
response = GitalyClient.call(@repository.storage, :commit_service, :list_commits_by_oid, request, timeout: GitalyClient.medium_timeout)
@@ -293,7 +302,7 @@ module Gitlab
end
end
- response = GitalyClient.call(@repository.storage, :commit_service, :filter_shas_with_signatures, enum)
+ response = GitalyClient.call(@repository.storage, :commit_service, :filter_shas_with_signatures, enum, timeout: GitalyClient.fast_timeout)
response.flat_map do |msg|
msg.shas.map { |sha| EncodingHelper.encode!(sha) }
@@ -315,11 +324,13 @@ module Gitlab
return if signature.blank? && signed_text.blank?
[signature, signed_text]
+ rescue GRPC::InvalidArgument => ex
+ raise ArgumentError, ex
end
def get_commit_signatures(commit_ids)
request = Gitaly::GetCommitSignaturesRequest.new(repository: @gitaly_repo, commit_ids: commit_ids)
- response = GitalyClient.call(@repository.storage, :commit_service, :get_commit_signatures, request)
+ response = GitalyClient.call(@repository.storage, :commit_service, :get_commit_signatures, request, timeout: GitalyClient.fast_timeout)
signatures = Hash.new { |h, k| h[k] = [''.b, ''.b] }
current_commit_id = nil
@@ -332,11 +343,13 @@ module Gitlab
end
signatures
+ rescue GRPC::InvalidArgument => ex
+ raise ArgumentError, ex
end
def get_commit_messages(commit_ids)
request = Gitaly::GetCommitMessagesRequest.new(repository: @gitaly_repo, commit_ids: commit_ids)
- response = GitalyClient.call(@repository.storage, :commit_service, :get_commit_messages, request)
+ response = GitalyClient.call(@repository.storage, :commit_service, :get_commit_messages, request, timeout: GitalyClient.fast_timeout)
messages = Hash.new { |h, k| h[k] = ''.b }
current_commit_id = nil
@@ -355,7 +368,7 @@ module Gitlab
def call_commit_diff(request_params, options = {})
request_params[:ignore_whitespace_change] = options.fetch(:ignore_whitespace_change, false)
request_params[:enforce_limits] = options.fetch(:limits, true)
- request_params[:collapse_diffs] = request_params[:enforce_limits] || !options.fetch(:expanded, true)
+ request_params[:collapse_diffs] = !options.fetch(:expanded, true)
request_params.merge!(Gitlab::Git::DiffCollection.collection_limits(options).to_h)
request = Gitaly::CommitDiffRequest.new(request_params)
@@ -386,8 +399,8 @@ module Gitlab
end
end
- def encode_repeated(a)
- Google::Protobuf::RepeatedField.new(:bytes, a.map { |s| encode_binary(s) } )
+ def encode_repeated(array)
+ Google::Protobuf::RepeatedField.new(:bytes, array.map { |s| encode_binary(s) } )
end
def call_find_commit(revision)
diff --git a/lib/gitlab/gitaly_client/conflicts_service.rb b/lib/gitlab/gitaly_client/conflicts_service.rb
index e14734495a8..aa7e03301f5 100644
--- a/lib/gitlab/gitaly_client/conflicts_service.rb
+++ b/lib/gitlab/gitaly_client/conflicts_service.rb
@@ -25,10 +25,12 @@ module Gitlab
def conflicts?
list_conflict_files.any?
- rescue GRPC::FailedPrecondition
- # The server raises this exception when it encounters ConflictSideMissing, which
- # means a conflict exists but its `theirs` or `ours` data is nil due to a non-existent
- # file in one of the trees.
+ rescue GRPC::FailedPrecondition, GRPC::Unknown
+ # The server raises FailedPrecondition when it encounters
+ # ConflictSideMissing, which means a conflict exists but its `theirs` or
+ # `ours` data is nil due to a non-existent file in one of the trees.
+ #
+ # GRPC::Unknown comes from Rugged::ReferenceError and Rugged::OdbError.
true
end
@@ -46,7 +48,7 @@ module Gitlab
end
end
- response = GitalyClient.call(@repository.storage, :conflicts_service, :resolve_conflicts, req_enum, remote_storage: target_repository.storage)
+ response = GitalyClient.call(@repository.storage, :conflicts_service, :resolve_conflicts, req_enum, remote_storage: target_repository.storage, timeout: GitalyClient.medium_timeout)
if response.resolution_error.present?
raise Gitlab::Git::Conflict::Resolver::ResolutionError, response.resolution_error
diff --git a/lib/gitlab/gitaly_client/namespace_service.rb b/lib/gitlab/gitaly_client/namespace_service.rb
index bd7c345ac01..d4e982b649a 100644
--- a/lib/gitlab/gitaly_client/namespace_service.rb
+++ b/lib/gitlab/gitaly_client/namespace_service.rb
@@ -8,31 +8,31 @@ module Gitlab
def exists?(name)
request = Gitaly::NamespaceExistsRequest.new(storage_name: @storage, name: name)
- gitaly_client_call(:namespace_exists, request).exists
+ gitaly_client_call(:namespace_exists, request, timeout: GitalyClient.fast_timeout).exists
end
def add(name)
request = Gitaly::AddNamespaceRequest.new(storage_name: @storage, name: name)
- gitaly_client_call(:add_namespace, request)
+ gitaly_client_call(:add_namespace, request, timeout: GitalyClient.fast_timeout)
end
def remove(name)
request = Gitaly::RemoveNamespaceRequest.new(storage_name: @storage, name: name)
- gitaly_client_call(:remove_namespace, request)
+ gitaly_client_call(:remove_namespace, request, timeout: nil)
end
def rename(from, to)
request = Gitaly::RenameNamespaceRequest.new(storage_name: @storage, from: from, to: to)
- gitaly_client_call(:rename_namespace, request)
+ gitaly_client_call(:rename_namespace, request, timeout: GitalyClient.fast_timeout)
end
private
- def gitaly_client_call(type, request)
- GitalyClient.call(@storage, :namespace_service, type, request)
+ def gitaly_client_call(type, request, timeout: nil)
+ GitalyClient.call(@storage, :namespace_service, type, request, timeout: timeout)
end
end
end
diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb
index 44b0e517bf0..555733d1834 100644
--- a/lib/gitlab/gitaly_client/operation_service.rb
+++ b/lib/gitlab/gitaly_client/operation_service.rb
@@ -17,10 +17,10 @@ module Gitlab
user: Gitlab::Git::User.from_gitlab(user).to_gitaly
)
- response = GitalyClient.call(@repository.storage, :operation_service, :user_delete_tag, request)
+ response = GitalyClient.call(@repository.storage, :operation_service, :user_delete_tag, request, timeout: GitalyClient.medium_timeout)
if pre_receive_error = response.pre_receive_error.presence
- raise Gitlab::Git::HooksService::PreReceiveError, pre_receive_error
+ raise Gitlab::Git::PreReceiveError, pre_receive_error
end
end
@@ -33,9 +33,9 @@ module Gitlab
message: encode_binary(message.to_s)
)
- response = GitalyClient.call(@repository.storage, :operation_service, :user_create_tag, request)
+ response = GitalyClient.call(@repository.storage, :operation_service, :user_create_tag, request, timeout: GitalyClient.medium_timeout)
if pre_receive_error = response.pre_receive_error.presence
- raise Gitlab::Git::HooksService::PreReceiveError, pre_receive_error
+ raise Gitlab::Git::PreReceiveError, pre_receive_error
elsif response.exists
raise Gitlab::Git::Repository::TagExistsError
end
@@ -56,7 +56,7 @@ module Gitlab
:user_create_branch, request)
if response.pre_receive_error.present?
- raise Gitlab::Git::HooksService::PreReceiveError.new(response.pre_receive_error)
+ raise Gitlab::Git::PreReceiveError.new(response.pre_receive_error)
end
branch = response.branch
@@ -64,6 +64,24 @@ module Gitlab
target_commit = Gitlab::Git::Commit.decorate(@repository, branch.target_commit)
Gitlab::Git::Branch.new(@repository, branch.name, target_commit.id, target_commit)
+ rescue GRPC::FailedPrecondition => ex
+ raise Gitlab::Git::Repository::InvalidRef, ex
+ end
+
+ def user_update_branch(branch_name, user, newrev, oldrev)
+ request = Gitaly::UserUpdateBranchRequest.new(
+ repository: @gitaly_repo,
+ branch_name: encode_binary(branch_name),
+ user: Gitlab::Git::User.from_gitlab(user).to_gitaly,
+ newrev: encode_binary(newrev),
+ oldrev: encode_binary(oldrev)
+ )
+
+ response = GitalyClient.call(@repository.storage, :operation_service, :user_update_branch, request)
+
+ if pre_receive_error = response.pre_receive_error.presence
+ raise Gitlab::Git::PreReceiveError, pre_receive_error
+ end
end
def user_delete_branch(branch_name, user)
@@ -76,7 +94,7 @@ module Gitlab
response = GitalyClient.call(@repository.storage, :operation_service, :user_delete_branch, request)
if pre_receive_error = response.pre_receive_error.presence
- raise Gitlab::Git::HooksService::PreReceiveError, pre_receive_error
+ raise Gitlab::Git::PreReceiveError, pre_receive_error
end
end
@@ -106,7 +124,7 @@ module Gitlab
second_response = response_enum.next
if second_response.pre_receive_error.present?
- raise Gitlab::Git::HooksService::PreReceiveError, second_response.pre_receive_error
+ raise Gitlab::Git::PreReceiveError, second_response.pre_receive_error
end
branch_update = second_response.branch_update
@@ -133,6 +151,8 @@ module Gitlab
request
).branch_update
Gitlab::Git::OperationService::BranchUpdate.from_gitaly(branch_update)
+ rescue GRPC::FailedPrecondition => e
+ raise Gitlab::Git::CommitError, e
end
def user_cherry_pick(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:)
@@ -175,7 +195,7 @@ module Gitlab
)
if response.pre_receive_error.presence
- raise Gitlab::Git::HooksService::PreReceiveError, response.pre_receive_error
+ raise Gitlab::Git::PreReceiveError, response.pre_receive_error
elsif response.git_error.presence
raise Gitlab::Git::Repository::GitError, response.git_error
else
@@ -242,7 +262,7 @@ module Gitlab
:user_commit_files, req_enum, remote_storage: start_repository.storage)
if (pre_receive_error = response.pre_receive_error.presence)
- raise Gitlab::Git::HooksService::PreReceiveError, pre_receive_error
+ raise Gitlab::Git::PreReceiveError, pre_receive_error
end
if (index_error = response.index_error.presence)
@@ -272,7 +292,8 @@ module Gitlab
:operation_service,
:"user_#{rpc}",
request,
- remote_storage: start_repository.storage
+ remote_storage: start_repository.storage,
+ timeout: GitalyClient.medium_timeout
)
handle_cherry_pick_or_revert_response(response)
@@ -280,7 +301,7 @@ module Gitlab
def handle_cherry_pick_or_revert_response(response)
if response.pre_receive_error.presence
- raise Gitlab::Git::HooksService::PreReceiveError, response.pre_receive_error
+ raise Gitlab::Git::PreReceiveError, response.pre_receive_error
elsif response.commit_error.presence
raise Gitlab::Git::CommitError, response.commit_error
elsif response.create_tree_error.presence
diff --git a/lib/gitlab/gitaly_client/ref_service.rb b/lib/gitlab/gitaly_client/ref_service.rb
index 3ac46be6208..7f4eed9222a 100644
--- a/lib/gitlab/gitaly_client/ref_service.rb
+++ b/lib/gitlab/gitaly_client/ref_service.rb
@@ -12,7 +12,7 @@ module Gitlab
def branches
request = Gitaly::FindAllBranchesRequest.new(repository: @gitaly_repo)
- response = GitalyClient.call(@storage, :ref_service, :find_all_branches, request)
+ response = GitalyClient.call(@storage, :ref_service, :find_all_branches, request, timeout: GitalyClient.fast_timeout)
consume_find_all_branches_response(response)
end
@@ -23,26 +23,26 @@ module Gitlab
merged_only: true,
merged_branches: branch_names.map { |s| encode_binary(s) }
)
- response = GitalyClient.call(@storage, :ref_service, :find_all_branches, request)
+ response = GitalyClient.call(@storage, :ref_service, :find_all_branches, request, timeout: GitalyClient.fast_timeout)
consume_find_all_branches_response(response)
end
def default_branch_name
request = Gitaly::FindDefaultBranchNameRequest.new(repository: @gitaly_repo)
- response = GitalyClient.call(@storage, :ref_service, :find_default_branch_name, request)
+ response = GitalyClient.call(@storage, :ref_service, :find_default_branch_name, request, timeout: GitalyClient.fast_timeout)
Gitlab::Git.branch_name(response.name)
end
def branch_names
request = Gitaly::FindAllBranchNamesRequest.new(repository: @gitaly_repo)
- response = GitalyClient.call(@storage, :ref_service, :find_all_branch_names, request)
+ response = GitalyClient.call(@storage, :ref_service, :find_all_branch_names, request, timeout: GitalyClient.fast_timeout)
consume_refs_response(response) { |name| Gitlab::Git.branch_name(name) }
end
def tag_names
request = Gitaly::FindAllTagNamesRequest.new(repository: @gitaly_repo)
- response = GitalyClient.call(@storage, :ref_service, :find_all_tag_names, request)
+ response = GitalyClient.call(@storage, :ref_service, :find_all_tag_names, request, timeout: GitalyClient.fast_timeout)
consume_refs_response(response) { |name| Gitlab::Git.tag_name(name) }
end
@@ -67,19 +67,19 @@ module Gitlab
def local_branches(sort_by: nil)
request = Gitaly::FindLocalBranchesRequest.new(repository: @gitaly_repo)
request.sort_by = sort_by_param(sort_by) if sort_by
- response = GitalyClient.call(@storage, :ref_service, :find_local_branches, request)
+ response = GitalyClient.call(@storage, :ref_service, :find_local_branches, request, timeout: GitalyClient.fast_timeout)
consume_find_local_branches_response(response)
end
def tags
request = Gitaly::FindAllTagsRequest.new(repository: @gitaly_repo)
- response = GitalyClient.call(@storage, :ref_service, :find_all_tags, request)
+ response = GitalyClient.call(@storage, :ref_service, :find_all_tags, request, timeout: GitalyClient.medium_timeout)
consume_tags_response(response)
end
def ref_exists?(ref_name)
request = Gitaly::RefExistsRequest.new(repository: @gitaly_repo, ref: encode_binary(ref_name))
- response = GitalyClient.call(@storage, :ref_service, :ref_exists, request)
+ response = GitalyClient.call(@storage, :ref_service, :ref_exists, request, timeout: GitalyClient.fast_timeout)
response.value
rescue GRPC::InvalidArgument => e
raise ArgumentError, e.message
@@ -91,7 +91,7 @@ module Gitlab
name: encode_binary(branch_name)
)
- response = GitalyClient.call(@repository.storage, :ref_service, :find_branch, request)
+ response = GitalyClient.call(@repository.storage, :ref_service, :find_branch, request, timeout: GitalyClient.medium_timeout)
branch = response.branch
return unless branch
@@ -140,7 +140,7 @@ module Gitlab
except_with_prefix: except_with_prefixes.map { |r| encode_binary(r) }
)
- response = GitalyClient.call(@repository.storage, :ref_service, :delete_refs, request)
+ response = GitalyClient.call(@repository.storage, :ref_service, :delete_refs, request, timeout: GitalyClient.fast_timeout)
raise Gitlab::Git::Repository::GitError, response.git_error if response.git_error.present?
end
@@ -153,7 +153,7 @@ module Gitlab
limit: limit
)
- stream = GitalyClient.call(@repository.storage, :ref_service, :list_tag_names_containing_commit, request)
+ stream = GitalyClient.call(@repository.storage, :ref_service, :list_tag_names_containing_commit, request, timeout: GitalyClient.medium_timeout)
consume_ref_contains_sha_response(stream, :tag_names)
end
@@ -166,14 +166,14 @@ module Gitlab
limit: limit
)
- stream = GitalyClient.call(@repository.storage, :ref_service, :list_branch_names_containing_commit, request)
+ stream = GitalyClient.call(@repository.storage, :ref_service, :list_branch_names_containing_commit, request, timeout: GitalyClient.medium_timeout)
consume_ref_contains_sha_response(stream, :branch_names)
end
def get_tag_messages(tag_ids)
request = Gitaly::GetTagMessagesRequest.new(repository: @gitaly_repo, tag_ids: tag_ids)
- response = GitalyClient.call(@repository.storage, :ref_service, :get_tag_messages, request)
+ response = GitalyClient.call(@repository.storage, :ref_service, :get_tag_messages, request, timeout: GitalyClient.fast_timeout)
messages = Hash.new { |h, k| h[k] = ''.b }
current_tag_id = nil
diff --git a/lib/gitlab/gitaly_client/remote_service.rb b/lib/gitlab/gitaly_client/remote_service.rb
index f2d699d9dfb..1381e033d4b 100644
--- a/lib/gitlab/gitaly_client/remote_service.rb
+++ b/lib/gitlab/gitaly_client/remote_service.rb
@@ -28,13 +28,13 @@ module Gitlab
mirror_refmaps: Array.wrap(mirror_refmaps).map(&:to_s)
)
- GitalyClient.call(@storage, :remote_service, :add_remote, request)
+ GitalyClient.call(@storage, :remote_service, :add_remote, request, timeout: GitalyClient.fast_timeout)
end
def remove_remote(name)
request = Gitaly::RemoveRemoteRequest.new(repository: @gitaly_repo, name: name)
- response = GitalyClient.call(@storage, :remote_service, :remove_remote, request)
+ response = GitalyClient.call(@storage, :remote_service, :remove_remote, request, timeout: GitalyClient.fast_timeout)
response.result
end
diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb
index ee01f5a5bd9..64b9af4d70c 100644
--- a/lib/gitlab/gitaly_client/repository_service.rb
+++ b/lib/gitlab/gitaly_client/repository_service.rb
@@ -21,7 +21,7 @@ module Gitlab
def cleanup
request = Gitaly::CleanupRequest.new(repository: @gitaly_repo)
- GitalyClient.call(@storage, :repository_service, :cleanup, request)
+ GitalyClient.call(@storage, :repository_service, :cleanup, request, timeout: GitalyClient.fast_timeout)
end
def garbage_collect(create_bitmap)
@@ -41,19 +41,21 @@ module Gitlab
def repository_size
request = Gitaly::RepositorySizeRequest.new(repository: @gitaly_repo)
- response = GitalyClient.call(@storage, :repository_service, :repository_size, request)
+ response = GitalyClient.call(@storage, :repository_service, :repository_size, request, timeout: GitalyClient.medium_timeout)
response.size
end
def apply_gitattributes(revision)
request = Gitaly::ApplyGitattributesRequest.new(repository: @gitaly_repo, revision: encode_binary(revision))
- GitalyClient.call(@storage, :repository_service, :apply_gitattributes, request)
+ GitalyClient.call(@storage, :repository_service, :apply_gitattributes, request, timeout: GitalyClient.fast_timeout)
+ rescue GRPC::InvalidArgument => ex
+ raise Gitlab::Git::Repository::InvalidRef, ex
end
def info_attributes
request = Gitaly::GetInfoAttributesRequest.new(repository: @gitaly_repo)
- response = GitalyClient.call(@storage, :repository_service, :get_info_attributes, request)
+ response = GitalyClient.call(@storage, :repository_service, :get_info_attributes, request, timeout: GitalyClient.fast_timeout)
response.each_with_object("") do |message, attributes|
attributes << message.attributes
end
@@ -80,7 +82,7 @@ module Gitlab
def create_repository
request = Gitaly::CreateRepositoryRequest.new(repository: @gitaly_repo)
- GitalyClient.call(@storage, :repository_service, :create_repository, request)
+ GitalyClient.call(@storage, :repository_service, :create_repository, request, timeout: GitalyClient.medium_timeout)
end
def has_local_branches?
@@ -96,7 +98,7 @@ module Gitlab
revisions: revisions.map { |r| encode_binary(r) }
)
- response = GitalyClient.call(@storage, :repository_service, :find_merge_base, request)
+ response = GitalyClient.call(@storage, :repository_service, :find_merge_base, request, timeout: GitalyClient.fast_timeout)
response.base.presence
end
@@ -196,42 +198,38 @@ module Gitlab
end
def create_bundle(save_path)
- request = Gitaly::CreateBundleRequest.new(repository: @gitaly_repo)
- response = GitalyClient.call(
- @storage,
- :repository_service,
+ gitaly_fetch_stream_to_file(
+ save_path,
:create_bundle,
- request,
- timeout: GitalyClient.default_timeout
+ Gitaly::CreateBundleRequest,
+ GitalyClient.default_timeout
)
+ end
- File.open(save_path, 'wb') do |f|
- response.each do |message|
- f.write(message.data)
- end
- end
+ def backup_custom_hooks(save_path)
+ gitaly_fetch_stream_to_file(
+ save_path,
+ :backup_custom_hooks,
+ Gitaly::BackupCustomHooksRequest,
+ GitalyClient.default_timeout
+ )
end
def create_from_bundle(bundle_path)
- request = Gitaly::CreateRepositoryFromBundleRequest.new(repository: @gitaly_repo)
- enum = Enumerator.new do |y|
- File.open(bundle_path, 'rb') do |f|
- while data = f.read(MAX_MSG_SIZE)
- request.data = data
-
- y.yield request
-
- request = Gitaly::CreateRepositoryFromBundleRequest.new
- end
- end
- end
-
- GitalyClient.call(
- @storage,
- :repository_service,
+ gitaly_repo_stream_request(
+ bundle_path,
:create_repository_from_bundle,
- enum,
- timeout: GitalyClient.default_timeout
+ Gitaly::CreateRepositoryFromBundleRequest,
+ GitalyClient.default_timeout
+ )
+ end
+
+ def restore_custom_hooks(custom_hooks_path)
+ gitaly_repo_stream_request(
+ custom_hooks_path,
+ :restore_custom_hooks,
+ Gitaly::RestoreCustomHooksRequest,
+ GitalyClient.default_timeout
)
end
@@ -260,24 +258,46 @@ module Gitlab
)
request.old_revision = old_ref.b unless old_ref.nil?
- response = GitalyClient.call(@storage, :repository_service, :write_ref, request)
+ response = GitalyClient.call(@storage, :repository_service, :write_ref, request, timeout: GitalyClient.fast_timeout)
raise Gitlab::Git::CommandError, encode!(response.error) if response.error.present?
true
end
- def write_config(full_path:)
- request = Gitaly::WriteConfigRequest.new(repository: @gitaly_repo, full_path: full_path)
- response = GitalyClient.call(
+ def set_config(entries)
+ return if entries.empty?
+
+ request = Gitaly::SetConfigRequest.new(repository: @gitaly_repo)
+ entries.each do |key, value|
+ request.entries << build_set_config_entry(key, value)
+ end
+
+ GitalyClient.call(
+ @storage,
+ :repository_service,
+ :set_config,
+ request,
+ timeout: GitalyClient.fast_timeout
+ )
+
+ nil
+ end
+
+ def delete_config(keys)
+ return if keys.empty?
+
+ request = Gitaly::DeleteConfigRequest.new(repository: @gitaly_repo, keys: keys)
+
+ GitalyClient.call(
@storage,
:repository_service,
- :write_config,
+ :delete_config,
request,
timeout: GitalyClient.fast_timeout
)
- raise Gitlab::Git::OSError.new(response.error) unless response.error.empty?
+ nil
end
def license_short_name
@@ -290,7 +310,7 @@ module Gitlab
def calculate_checksum
request = Gitaly::CalculateChecksumRequest.new(repository: @gitaly_repo)
- response = GitalyClient.call(@storage, :repository_service, :calculate_checksum, request)
+ response = GitalyClient.call(@storage, :repository_service, :calculate_checksum, request, timeout: GitalyClient.fast_timeout)
response.checksum.presence
rescue GRPC::DataLoss => e
raise Gitlab::Git::Repository::InvalidRepository.new(e)
@@ -299,18 +319,78 @@ module Gitlab
def raw_changes_between(from, to)
request = Gitaly::GetRawChangesRequest.new(repository: @gitaly_repo, from_revision: from, to_revision: to)
- GitalyClient.call(@storage, :repository_service, :get_raw_changes, request)
+ GitalyClient.call(@storage, :repository_service, :get_raw_changes, request, timeout: GitalyClient.fast_timeout)
end
def search_files_by_name(ref, query)
request = Gitaly::SearchFilesByNameRequest.new(repository: @gitaly_repo, ref: ref, query: query)
- GitalyClient.call(@storage, :repository_service, :search_files_by_name, request).flat_map(&:files)
+ GitalyClient.call(@storage, :repository_service, :search_files_by_name, request, timeout: GitalyClient.fast_timeout).flat_map(&:files)
end
def search_files_by_content(ref, query)
request = Gitaly::SearchFilesByContentRequest.new(repository: @gitaly_repo, ref: ref, query: query)
GitalyClient.call(@storage, :repository_service, :search_files_by_content, request).flat_map(&:matches)
end
+
+ private
+
+ def gitaly_fetch_stream_to_file(save_path, rpc_name, request_class, timeout)
+ request = request_class.new(repository: @gitaly_repo)
+ response = GitalyClient.call(
+ @storage,
+ :repository_service,
+ rpc_name,
+ request,
+ timeout: timeout
+ )
+
+ File.open(save_path, 'wb') do |f|
+ response.each do |message|
+ f.write(message.data)
+ end
+ end
+ # If the file is empty means that we recieved an empty stream, we delete the file
+ FileUtils.rm(save_path) if File.zero?(save_path)
+ end
+
+ def gitaly_repo_stream_request(file_path, rpc_name, request_class, timeout)
+ request = request_class.new(repository: @gitaly_repo)
+ enum = Enumerator.new do |y|
+ File.open(file_path, 'rb') do |f|
+ while data = f.read(MAX_MSG_SIZE)
+ request.data = data
+
+ y.yield request
+ request = request_class.new
+ end
+ end
+ end
+
+ GitalyClient.call(
+ @storage,
+ :repository_service,
+ rpc_name,
+ enum,
+ timeout: timeout
+ )
+ end
+
+ def build_set_config_entry(key, value)
+ entry = Gitaly::SetConfigRequest::Entry.new(key: key)
+
+ case value
+ when String
+ entry.value_str = value
+ when Integer
+ entry.value_int32 = value
+ when TrueClass, FalseClass
+ entry.value_bool = value
+ else
+ raise InvalidArgument, "invalid git config value: #{value.inspect}"
+ end
+
+ entry
+ end
end
end
end
diff --git a/lib/gitlab/gitaly_client/storage_service.rb b/lib/gitlab/gitaly_client/storage_service.rb
new file mode 100644
index 00000000000..eb0e910665b
--- /dev/null
+++ b/lib/gitlab/gitaly_client/storage_service.rb
@@ -0,0 +1,15 @@
+module Gitlab
+ module GitalyClient
+ class StorageService
+ def initialize(storage)
+ @storage = storage
+ end
+
+ # Delete all repositories in the storage. This is a slow and VERY DESTRUCTIVE operation.
+ def delete_all_repositories
+ request = Gitaly::DeleteAllRepositoriesRequest.new(storage_name: @storage)
+ GitalyClient.call(@storage, :storage_service, :delete_all_repositories, request)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/gitaly_client/storage_settings.rb b/lib/gitlab/gitaly_client/storage_settings.rb
index 9a576e463e3..8e530de174d 100644
--- a/lib/gitlab/gitaly_client/storage_settings.rb
+++ b/lib/gitlab/gitaly_client/storage_settings.rb
@@ -4,6 +4,8 @@ module Gitlab
# where production code (app, config, db, lib) touches Git repositories
# directly.
class StorageSettings
+ extend Gitlab::TemporarilyAllow
+
DirectPathAccessError = Class.new(StandardError)
InvalidConfigurationError = Class.new(StandardError)
@@ -17,7 +19,21 @@ module Gitlab
# This class will give easily recognizable NoMethodErrors
Deprecated = Class.new
- attr_reader :legacy_disk_path
+ MUTEX = Mutex.new
+
+ DISK_ACCESS_DENIED_FLAG = :deny_disk_access
+ ALLOW_KEY = :allow_disk_access
+
+ # If your code needs this method then your code needs to be fixed.
+ def self.allow_disk_access
+ temporarily_allow(ALLOW_KEY) { yield }
+ end
+
+ def self.disk_access_denied?
+ !temporarily_allowed?(ALLOW_KEY) && GitalyClient.feature_enabled?(DISK_ACCESS_DENIED_FLAG)
+ rescue
+ false # Err on the side of caution, don't break gitlab for people
+ end
def initialize(storage)
raise InvalidConfigurationError, "expected a Hash, got a #{storage.class.name}" unless storage.is_a?(Hash)
@@ -34,10 +50,18 @@ module Gitlab
@hash.fetch(:gitaly_address)
end
+ def legacy_disk_path
+ if self.class.disk_access_denied?
+ raise DirectPathAccessError, "git disk access denied via the gitaly_#{DISK_ACCESS_DENIED_FLAG} feature"
+ end
+
+ @legacy_disk_path
+ end
+
private
- def method_missing(m, *args, &block)
- @hash.public_send(m, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
+ def method_missing(msg, *args, &block)
+ @hash.public_send(msg, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
end
end
end
diff --git a/lib/gitlab/gitaly_client/wiki_service.rb b/lib/gitlab/gitaly_client/wiki_service.rb
index 2dfe055a496..6cb049c1f68 100644
--- a/lib/gitlab/gitaly_client/wiki_service.rb
+++ b/lib/gitlab/gitaly_client/wiki_service.rb
@@ -69,7 +69,7 @@ module Gitlab
commit_details: gitaly_commit_details(commit_details)
)
- GitalyClient.call(@repository.storage, :wiki_service, :wiki_delete_page, request)
+ GitalyClient.call(@repository.storage, :wiki_service, :wiki_delete_page, request, timeout: GitalyClient.medium_timeout)
end
def find_page(title:, version: nil, dir: nil)
@@ -80,14 +80,14 @@ module Gitlab
directory: encode_binary(dir)
)
- response = GitalyClient.call(@repository.storage, :wiki_service, :wiki_find_page, request)
+ response = GitalyClient.call(@repository.storage, :wiki_service, :wiki_find_page, request, timeout: GitalyClient.fast_timeout)
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)
+ response = GitalyClient.call(@repository.storage, :wiki_service, :wiki_get_all_pages, request, timeout: GitalyClient.medium_timeout)
pages = []
loop do
@@ -113,7 +113,7 @@ module Gitlab
per_page: options[:per_page] || Gollum::Page.per_page
)
- stream = GitalyClient.call(@repository.storage, :wiki_service, :wiki_get_page_versions, request)
+ stream = GitalyClient.call(@repository.storage, :wiki_service, :wiki_get_page_versions, request, timeout: GitalyClient.medium_timeout)
versions = []
stream.each do |message|
@@ -132,7 +132,7 @@ module Gitlab
revision: encode_binary(revision)
)
- response = GitalyClient.call(@repository.storage, :wiki_service, :wiki_find_file, request)
+ response = GitalyClient.call(@repository.storage, :wiki_service, :wiki_find_file, request, timeout: GitalyClient.fast_timeout)
wiki_file = nil
response.each do |message|
diff --git a/lib/gitlab/github_import/importer/lfs_object_importer.rb b/lib/gitlab/github_import/importer/lfs_object_importer.rb
new file mode 100644
index 00000000000..a88c17aaf82
--- /dev/null
+++ b/lib/gitlab/github_import/importer/lfs_object_importer.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Importer
+ class LfsObjectImporter
+ attr_reader :lfs_object, :project
+
+ # lfs_object - An instance of `Gitlab::GithubImport::Representation::LfsObject`.
+ # project - An instance of `Project`.
+ def initialize(lfs_object, project, _)
+ @lfs_object = lfs_object
+ @project = project
+ end
+
+ def execute
+ Projects::LfsPointers::LfsDownloadService
+ .new(project)
+ .execute(lfs_object.oid, lfs_object.download_link)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/importer/lfs_objects_importer.rb b/lib/gitlab/github_import/importer/lfs_objects_importer.rb
new file mode 100644
index 00000000000..6046e30d4ef
--- /dev/null
+++ b/lib/gitlab/github_import/importer/lfs_objects_importer.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Importer
+ class LfsObjectsImporter
+ include ParallelScheduling
+
+ def importer_class
+ LfsObjectImporter
+ end
+
+ def representation_class
+ Representation::LfsObject
+ end
+
+ def sidekiq_worker_class
+ ImportLfsObjectWorker
+ end
+
+ def collection_method
+ :lfs_objects
+ end
+
+ def each_object_to_import
+ lfs_objects = Projects::LfsPointers::LfsImportService.new(project).execute
+
+ lfs_objects.each do |object|
+ yield object
+ end
+ rescue StandardError => e
+ Rails.logger.error("The Lfs import process failed. #{e.message}")
+ 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
index 49d859f9624..6b3688c4381 100644
--- a/lib/gitlab/github_import/importer/pull_request_importer.rb
+++ b/lib/gitlab/github_import/importer/pull_request_importer.rb
@@ -22,15 +22,22 @@ module Gitlab
end
def execute
- if (mr_id = create_merge_request)
- issuable_finder.cache_database_id(mr_id)
+ mr, already_exists = create_merge_request
+
+ if mr
+ insert_git_data(mr, already_exists)
+ 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.
+ # created, otherwise it will return an Array containing the following
+ # values:
+ #
+ # 1. A MergeRequest instance.
+ # 2. A boolean indicating if the MR already exists.
def create_merge_request
author_id, author_found = user_finder.author_id_for(pull_request)
@@ -69,21 +76,43 @@ module Gitlab
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
+ [project.merge_requests.find(merge_request_id), false]
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.
+ []
+ rescue ActiveRecord::RecordNotUnique
+ # It's possible we previously created the MR, but failed when updating
+ # the Git data. In this case we'll just continue working on the
+ # existing row.
+ [project.merge_requests.find_by(iid: pull_request.iid), true]
+ end
+
+ def insert_git_data(merge_request, already_exists = false)
+ # 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
+
+ # MR diffs normally use an "after_save" hook to pull data from Git.
+ # All of this happens in the transaction started by calling
+ # create/save/etc. This in turn can lead to these transactions being
+ # held open for much longer than necessary. To work around this we
+ # first save the diff, then populate it.
+ diff =
+ if already_exists
+ merge_request.merge_request_diffs.take ||
+ merge_request.merge_request_diffs.build
+ else
+ merge_request.merge_request_diffs.build
+ end
+
+ diff.importing = true
+ diff.save
+ diff.save_git_content
end
end
end
diff --git a/lib/gitlab/github_import/parallel_importer.rb b/lib/gitlab/github_import/parallel_importer.rb
index b02b123c98e..a77ac1e4fa6 100644
--- a/lib/gitlab/github_import/parallel_importer.rb
+++ b/lib/gitlab/github_import/parallel_importer.rb
@@ -15,6 +15,15 @@ module Gitlab
true
end
+ # This is a workaround for a Ruby 2.3.7 bug. rspec-mocks cannot restore
+ # the visibility of prepended modules. See
+ # https://github.com/rspec/rspec-mocks/issues/1231 for more details.
+ if Rails.env.test?
+ def self.requires_ci_cd_setup?
+ raise NotImplementedError
+ end
+ end
+
def initialize(project)
@project = project
end
diff --git a/lib/gitlab/github_import/representation/lfs_object.rb b/lib/gitlab/github_import/representation/lfs_object.rb
new file mode 100644
index 00000000000..debe0fa0baf
--- /dev/null
+++ b/lib/gitlab/github_import/representation/lfs_object.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Representation
+ class LfsObject
+ include ToHash
+ include ExposeAttribute
+
+ attr_reader :attributes
+
+ expose_attribute :oid, :download_link
+
+ # Builds a lfs_object
+ def self.from_api_response(lfs_object)
+ new({ oid: lfs_object[0], download_link: lfs_object[1] })
+ end
+
+ # Builds a new lfs_object 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 raw lfs_object details. The keys of this
+ # Hash 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
index 4f7324536a0..6a181caf65d 100644
--- a/lib/gitlab/github_import/sequential_importer.rb
+++ b/lib/gitlab/github_import/sequential_importer.rb
@@ -19,7 +19,8 @@ module Gitlab
Importer::PullRequestsImporter,
Importer::IssuesImporter,
Importer::DiffNotesImporter,
- Importer::NotesImporter
+ Importer::NotesImporter,
+ Importer::LfsObjectsImporter
].freeze
# project - The project to import the data into.
@@ -41,8 +42,6 @@ module Gitlab
klass.new(project, client, parallel: false).execute
end
- project.repository.after_import
-
true
end
end
diff --git a/lib/gitlab/gitlab_import/client.rb b/lib/gitlab/gitlab_import/client.rb
index 5482504e72e..22719e9a003 100644
--- a/lib/gitlab/gitlab_import/client.rb
+++ b/lib/gitlab/gitlab_import/client.rb
@@ -29,28 +29,28 @@ module Gitlab
end
def user
- api.get("/api/v3/user").parsed
+ api.get("/api/v4/user").parsed
end
def issues(project_identifier)
lazy_page_iterator(PER_PAGE) do |page|
- api.get("/api/v3/projects/#{project_identifier}/issues?per_page=#{PER_PAGE}&page=#{page}").parsed
+ api.get("/api/v4/projects/#{project_identifier}/issues?per_page=#{PER_PAGE}&page=#{page}").parsed
end
end
def issue_comments(project_identifier, issue_id)
lazy_page_iterator(PER_PAGE) do |page|
- api.get("/api/v3/projects/#{project_identifier}/issues/#{issue_id}/notes?per_page=#{PER_PAGE}&page=#{page}").parsed
+ api.get("/api/v4/projects/#{project_identifier}/issues/#{issue_id}/notes?per_page=#{PER_PAGE}&page=#{page}").parsed
end
end
def project(id)
- api.get("/api/v3/projects/#{id}").parsed
+ api.get("/api/v4/projects/#{id}").parsed
end
def projects
lazy_page_iterator(PER_PAGE) do |page|
- api.get("/api/v3/projects?per_page=#{PER_PAGE}&page=#{page}").parsed
+ api.get("/api/v4/projects?per_page=#{PER_PAGE}&page=#{page}").parsed
end
end
diff --git a/lib/gitlab/gitlab_import/importer.rb b/lib/gitlab/gitlab_import/importer.rb
index e44d7934fda..195672f5a12 100644
--- a/lib/gitlab/gitlab_import/importer.rb
+++ b/lib/gitlab/gitlab_import/importer.rb
@@ -25,7 +25,7 @@ module Gitlab
body = @formatter.author_line(issue["author"]["name"])
body += issue["description"]
- comments = client.issue_comments(project_identifier, issue["id"])
+ comments = client.issue_comments(project_identifier, issue["iid"])
if comments.any?
body += @formatter.comments_header
diff --git a/lib/gitlab/gitlab_import/project_creator.rb b/lib/gitlab/gitlab_import/project_creator.rb
index 3d0418261bb..430b8c10058 100644
--- a/lib/gitlab/gitlab_import/project_creator.rb
+++ b/lib/gitlab/gitlab_import/project_creator.rb
@@ -17,7 +17,7 @@ module Gitlab
path: repo["path"],
description: repo["description"],
namespace_id: namespace.id,
- visibility_level: repo["visibility_level"],
+ visibility_level: Gitlab::VisibilityLevel.level_value(repo["visibility"]),
import_type: "gitlab",
import_source: repo["path_with_namespace"],
import_url: repo["http_url_to_repo"].sub("://", "://oauth2:#{@session_data[:gitlab_access_token]}@")
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index c741dabe168..deaa14c8434 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -11,11 +11,11 @@ module Gitlab
gon.asset_host = ActionController::Base.asset_host
gon.webpack_public_path = webpack_public_path
gon.relative_url_root = Gitlab.config.gitlab.relative_url_root
- gon.shortcuts_path = help_page_path('shortcuts')
+ gon.shortcuts_path = Gitlab::Routing.url_helpers.help_page_path('shortcuts')
gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class
gon.sentry_dsn = Gitlab::CurrentSettings.clientside_sentry_dsn if Gitlab::CurrentSettings.clientside_sentry_enabled
gon.gitlab_url = Gitlab.config.gitlab.url
- gon.revision = Gitlab::REVISION
+ gon.revision = Gitlab.revision
gon.gitlab_logo = ActionController::Base.helpers.asset_path('gitlab_logo.png')
gon.sprite_icons = IconsHelper.sprite_icon_path
gon.sprite_file_icons = IconsHelper.sprite_file_icons_path
diff --git a/lib/gitlab/google_code_import/importer.rb b/lib/gitlab/google_code_import/importer.rb
index 46b49128140..5070f4e3cfe 100644
--- a/lib/gitlab/google_code_import/importer.rb
+++ b/lib/gitlab/google_code_import/importer.rb
@@ -200,27 +200,27 @@ module Gitlab
"Status: #{name}"
end
- def linkify_issues(s)
- s = s.gsub(/([Ii]ssue) ([0-9]+)/, '\1 #\2')
- s = s.gsub(/([Cc]omment) #([0-9]+)/, '\1 \2')
- s
+ def linkify_issues(str)
+ str = str.gsub(/([Ii]ssue) ([0-9]+)/, '\1 #\2')
+ str = str.gsub(/([Cc]omment) #([0-9]+)/, '\1 \2')
+ str
end
- def escape_for_markdown(s)
+ def escape_for_markdown(str)
# No headings and lists
- s = s.gsub(/^#/, "\\#")
- s = s.gsub(/^-/, "\\-")
+ str = str.gsub(/^#/, "\\#")
+ str = str.gsub(/^-/, "\\-")
# No inline code
- s = s.gsub("`", "\\`")
+ str = str.gsub("`", "\\`")
# Carriage returns make me sad
- s = s.delete("\r")
+ str = str.delete("\r")
# Markdown ignores single newlines, but we need them as <br />.
- s = s.gsub("\n", " \n")
+ str = str.gsub("\n", " \n")
- s
+ str
end
def create_label(name)
diff --git a/lib/gitlab/gpg.rb b/lib/gitlab/gpg.rb
index 413872d7e08..a4263369269 100644
--- a/lib/gitlab/gpg.rb
+++ b/lib/gitlab/gpg.rb
@@ -54,7 +54,11 @@ module Gitlab
fingerprints = CurrentKeyChain.fingerprints_from_key(key)
GPGME::Key.find(:public, fingerprints).flat_map do |raw_key|
- raw_key.uids.map { |uid| { name: uid.name, email: uid.email.downcase } }
+ raw_key.uids.each_with_object([]) do |uid, arr|
+ name = uid.name.force_encoding('UTF-8')
+ email = uid.email.force_encoding('UTF-8')
+ arr << { name: name, email: email.downcase } if name.valid_encoding? && email.valid_encoding?
+ end
end
end
end
diff --git a/lib/gitlab/gpg/commit.rb b/lib/gitlab/gpg/commit.rb
index 6d2278d0876..2716834f566 100644
--- a/lib/gitlab/gpg/commit.rb
+++ b/lib/gitlab/gpg/commit.rb
@@ -39,7 +39,7 @@ module Gitlab
def update_signature!(cached_signature)
using_keychain do |gpg_key|
- cached_signature.update_attributes!(attributes(gpg_key))
+ cached_signature.update!(attributes(gpg_key))
end
@signature = cached_signature
diff --git a/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb b/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb
index 1e1fdabca93..0014ce2689b 100644
--- a/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb
+++ b/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb
@@ -2,8 +2,12 @@ module Gitlab
module GrapeLogging
module Formatters
class LogrageWithTimestamp
+ include Gitlab::EncodingHelper
+
def call(severity, datetime, _, data)
time = data.delete :time
+ data[:params] = utf8_encode_values(data[:params]) if data.has_key?(:params)
+
attributes = {
time: datetime.utc.iso8601(3),
severity: severity,
@@ -13,6 +17,19 @@ module Gitlab
}.merge(data)
::Lograge.formatter.call(attributes) + "\n"
end
+
+ private
+
+ def utf8_encode_values(data)
+ case data
+ when Hash
+ data.merge(data) { |k, v| utf8_encode_values(v) }
+ when Array
+ data.map { |v| utf8_encode_values(v) }
+ when String
+ encode_utf8(data)
+ end
+ end
end
end
end
diff --git a/lib/gitlab/grape_logging/loggers/queue_duration_logger.rb b/lib/gitlab/grape_logging/loggers/queue_duration_logger.rb
new file mode 100644
index 00000000000..0adac79f25a
--- /dev/null
+++ b/lib/gitlab/grape_logging/loggers/queue_duration_logger.rb
@@ -0,0 +1,26 @@
+# This grape_logging module (https://github.com/aserafin/grape_logging) makes it
+# possible to log how much time an API request was queued by Workhorse.
+module Gitlab
+ module GrapeLogging
+ module Loggers
+ class QueueDurationLogger < ::GrapeLogging::Loggers::Base
+ attr_accessor :start_time
+
+ def before
+ @start_time = Time.now
+ end
+
+ def parameters(request, _)
+ proxy_start = request.env['HTTP_GITLAB_WORKHORSE_PROXY_START'].presence
+
+ return {} unless proxy_start && start_time
+
+ # Time in milliseconds since gitlab-workhorse started the request
+ duration = (start_time.to_f * 1_000 - proxy_start.to_f / 1_000_000).round(2)
+
+ { 'queue_duration': duration }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql.rb b/lib/gitlab/graphql.rb
new file mode 100644
index 00000000000..04a89432230
--- /dev/null
+++ b/lib/gitlab/graphql.rb
@@ -0,0 +1,5 @@
+module Gitlab
+ module Graphql
+ StandardGraphqlError = Class.new(StandardError)
+ end
+end
diff --git a/lib/gitlab/graphql/authorize.rb b/lib/gitlab/graphql/authorize.rb
new file mode 100644
index 00000000000..04f25c53e49
--- /dev/null
+++ b/lib/gitlab/graphql/authorize.rb
@@ -0,0 +1,21 @@
+module Gitlab
+ module Graphql
+ # Allow fields to declare permissions their objects must have. The field
+ # will be set to nil unless all required permissions are present.
+ module Authorize
+ extend ActiveSupport::Concern
+
+ def self.use(schema_definition)
+ schema_definition.instrument(:field, Instrumentation.new)
+ end
+
+ def required_permissions
+ @required_permissions ||= []
+ end
+
+ def authorize(*permissions)
+ required_permissions.concat(permissions)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/authorize/instrumentation.rb b/lib/gitlab/graphql/authorize/instrumentation.rb
new file mode 100644
index 00000000000..6cb8e617f62
--- /dev/null
+++ b/lib/gitlab/graphql/authorize/instrumentation.rb
@@ -0,0 +1,45 @@
+module Gitlab
+ module Graphql
+ module Authorize
+ class Instrumentation
+ # Replace the resolver for the field with one that will only return the
+ # resolved object if the permissions check is successful.
+ #
+ # Collections are not supported. Apply permissions checks for those at the
+ # database level instead, to avoid loading superfluous data from the DB
+ def instrument(_type, field)
+ field_definition = field.metadata[:type_class]
+ return field unless field_definition.respond_to?(:required_permissions)
+ return field if field_definition.required_permissions.empty?
+
+ old_resolver = field.resolve_proc
+
+ new_resolver = -> (obj, args, ctx) do
+ resolved_obj = old_resolver.call(obj, args, ctx)
+ checker = build_checker(ctx[:current_user], field_definition.required_permissions)
+
+ if resolved_obj.respond_to?(:then)
+ resolved_obj.then(&checker)
+ else
+ checker.call(resolved_obj)
+ end
+ end
+
+ field.redefine do
+ resolve(new_resolver)
+ end
+ end
+
+ private
+
+ def build_checker(current_user, abilities)
+ proc do |obj|
+ # Load the elements if they weren't loaded by BatchLoader yet
+ obj = obj.sync if obj.respond_to?(:sync)
+ obj if abilities.all? { |ability| Ability.allowed?(current_user, ability, obj) }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/connections.rb b/lib/gitlab/graphql/connections.rb
new file mode 100644
index 00000000000..2582ffeb2a8
--- /dev/null
+++ b/lib/gitlab/graphql/connections.rb
@@ -0,0 +1,12 @@
+module Gitlab
+ module Graphql
+ module Connections
+ def self.use(_schema)
+ GraphQL::Relay::BaseConnection.register_connection_implementation(
+ ActiveRecord::Relation,
+ Gitlab::Graphql::Connections::KeysetConnection
+ )
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/connections/keyset_connection.rb b/lib/gitlab/graphql/connections/keyset_connection.rb
new file mode 100644
index 00000000000..abee2afe144
--- /dev/null
+++ b/lib/gitlab/graphql/connections/keyset_connection.rb
@@ -0,0 +1,73 @@
+module Gitlab
+ module Graphql
+ module Connections
+ class KeysetConnection < GraphQL::Relay::BaseConnection
+ def cursor_from_node(node)
+ encode(node[order_field].to_s)
+ end
+
+ def sliced_nodes
+ @sliced_nodes ||=
+ begin
+ sliced = nodes
+
+ sliced = sliced.where(before_slice) if before.present?
+ sliced = sliced.where(after_slice) if after.present?
+
+ sliced
+ end
+ end
+
+ def paged_nodes
+ if first && last
+ raise Gitlab::Graphql::Errors::ArgumentError.new("Can only provide either `first` or `last`, not both")
+ end
+
+ if last
+ sliced_nodes.last(limit_value)
+ else
+ sliced_nodes.limit(limit_value)
+ end
+ end
+
+ private
+
+ def before_slice
+ if sort_direction == :asc
+ table[order_field].lt(decode(before))
+ else
+ table[order_field].gt(decode(before))
+ end
+ end
+
+ def after_slice
+ if sort_direction == :asc
+ table[order_field].gt(decode(after))
+ else
+ table[order_field].lt(decode(after))
+ end
+ end
+
+ def limit_value
+ @limit_value ||= [first, last, max_page_size].compact.min
+ end
+
+ def table
+ nodes.arel_table
+ end
+
+ def order_info
+ @order_info ||= nodes.order_values.first
+ end
+
+ def order_field
+ @order_field ||= order_info&.expr&.name || nodes.primary_key
+ end
+
+ def sort_direction
+ @order_direction ||= order_info&.direction || :desc
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/errors.rb b/lib/gitlab/graphql/errors.rb
new file mode 100644
index 00000000000..1d8e8307ab9
--- /dev/null
+++ b/lib/gitlab/graphql/errors.rb
@@ -0,0 +1,8 @@
+module Gitlab
+ module Graphql
+ module Errors
+ BaseError = Class.new(GraphQL::ExecutionError)
+ ArgumentError = Class.new(BaseError)
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/expose_permissions.rb b/lib/gitlab/graphql/expose_permissions.rb
new file mode 100644
index 00000000000..e3779995406
--- /dev/null
+++ b/lib/gitlab/graphql/expose_permissions.rb
@@ -0,0 +1,15 @@
+module Gitlab
+ module Graphql
+ module ExposePermissions
+ extend ActiveSupport::Concern
+ prepended do
+ def self.expose_permissions(permission_type, description: 'Permissions for the current user on the resource')
+ field :user_permissions, permission_type,
+ description: description,
+ null: false,
+ resolve: -> (obj, _, _) { obj }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/present.rb b/lib/gitlab/graphql/present.rb
new file mode 100644
index 00000000000..2c7b64f1be9
--- /dev/null
+++ b/lib/gitlab/graphql/present.rb
@@ -0,0 +1,20 @@
+module Gitlab
+ module Graphql
+ module Present
+ extend ActiveSupport::Concern
+ prepended do
+ def self.present_using(kls)
+ @presenter_class = kls
+ end
+
+ def self.presenter_class
+ @presenter_class
+ end
+ end
+
+ def self.use(schema_definition)
+ schema_definition.instrument(:field, Instrumentation.new)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/present/instrumentation.rb b/lib/gitlab/graphql/present/instrumentation.rb
new file mode 100644
index 00000000000..f87fd147b15
--- /dev/null
+++ b/lib/gitlab/graphql/present/instrumentation.rb
@@ -0,0 +1,36 @@
+module Gitlab
+ module Graphql
+ module Present
+ class Instrumentation
+ def instrument(type, field)
+ return field unless field.metadata[:type_class]
+
+ presented_in = field.metadata[:type_class].owner
+ return field unless presented_in.respond_to?(:presenter_class)
+ return field unless presented_in.presenter_class
+
+ old_resolver = field.resolve_proc
+
+ resolve_with_presenter = -> (presented_type, args, context) do
+ # We need to wrap the original presentation type into a type that
+ # uses the presenter as an object.
+ object = presented_type.object
+
+ if object.is_a?(presented_in.presenter_class)
+ next old_resolver.call(presented_type, args, context)
+ end
+
+ presenter = presented_in.presenter_class.new(object, **context.to_h)
+ wrapped = presented_type.class.new(presenter, context)
+
+ old_resolver.call(wrapped, args, context)
+ end
+
+ field.redefine do
+ resolve(resolve_with_presenter)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/variables.rb b/lib/gitlab/graphql/variables.rb
new file mode 100644
index 00000000000..ffbaf65b512
--- /dev/null
+++ b/lib/gitlab/graphql/variables.rb
@@ -0,0 +1,37 @@
+module Gitlab
+ module Graphql
+ class Variables
+ Invalid = Class.new(Gitlab::Graphql::StandardGraphqlError)
+
+ def initialize(param)
+ @param = param
+ end
+
+ def to_h
+ ensure_hash(@param)
+ end
+
+ private
+
+ # Handle form data, JSON body, or a blank value
+ def ensure_hash(ambiguous_param)
+ case ambiguous_param
+ when String
+ if ambiguous_param.present?
+ ensure_hash(JSON.parse(ambiguous_param))
+ else
+ {}
+ end
+ when Hash, ActionController::Parameters
+ ambiguous_param
+ when nil
+ {}
+ else
+ raise Invalid, "Unexpected parameter: #{ambiguous_param}"
+ end
+ rescue JSON::ParserError => e
+ raise Invalid.new(e)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/hashed_storage/migrator.rb b/lib/gitlab/hashed_storage/migrator.rb
new file mode 100644
index 00000000000..9251ed654cd
--- /dev/null
+++ b/lib/gitlab/hashed_storage/migrator.rb
@@ -0,0 +1,57 @@
+module Gitlab
+ module HashedStorage
+ # Hashed Storage Migrator
+ #
+ # This is responsible for scheduling and flagging projects
+ # to be migrated from Legacy to Hashed storage, either one by one or in bulk.
+ class Migrator
+ BATCH_SIZE = 100
+
+ # Schedule a range of projects to be bulk migrated with #bulk_migrate asynchronously
+ #
+ # @param [Object] start first project id for the range
+ # @param [Object] finish last project id for the range
+ def bulk_schedule(start, finish)
+ StorageMigratorWorker.perform_async(start, finish)
+ end
+
+ # Start migration of projects from specified range
+ #
+ # Flagging a project to be migrated is a synchronous action,
+ # but the migration runs through async jobs
+ #
+ # @param [Object] start first project id for the range
+ # @param [Object] finish last project id for the range
+ def bulk_migrate(start, finish)
+ projects = build_relation(start, finish)
+
+ projects.with_route.find_each(batch_size: BATCH_SIZE) do |project|
+ migrate(project)
+ end
+ end
+
+ # Flag a project to me migrated
+ #
+ # @param [Object] project that will be migrated
+ def migrate(project)
+ Rails.logger.info "Starting storage migration of #{project.full_path} (ID=#{project.id})..."
+
+ project.migrate_to_hashed_storage!
+ rescue => err
+ Rails.logger.error("#{err.message} migrating storage of #{project.full_path} (ID=#{project.id}), trace - #{err.backtrace}")
+ end
+
+ private
+
+ def build_relation(start, finish)
+ relation = Project
+ table = Project.arel_table
+
+ relation = relation.where(table[:id].gteq(start)) if start
+ relation = relation.where(table[:id].lteq(finish)) if finish
+
+ relation
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/hashed_storage/rake_helper.rb b/lib/gitlab/hashed_storage/rake_helper.rb
new file mode 100644
index 00000000000..303b05e6a9a
--- /dev/null
+++ b/lib/gitlab/hashed_storage/rake_helper.rb
@@ -0,0 +1,83 @@
+module Gitlab
+ module HashedStorage
+ module RakeHelper
+ def self.batch_size
+ ENV.fetch('BATCH', 200).to_i
+ end
+
+ def self.listing_limit
+ ENV.fetch('LIMIT', 500).to_i
+ end
+
+ def self.range_from
+ ENV['ID_FROM']
+ end
+
+ def self.range_to
+ ENV['ID_TO']
+ end
+
+ def self.range_single_item?
+ !range_from.nil? && range_from == range_to
+ end
+
+ def self.project_id_batches(&block)
+ Project.with_unmigrated_storage.in_batches(of: batch_size, start: range_from, finish: range_to) do |relation| # rubocop: disable Cop/InBatches
+ ids = relation.pluck(:id)
+
+ yield ids.min, ids.max
+ end
+ end
+
+ def self.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 self.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 self.relation_summary(relation_name, relation)
+ relation_count = relation.count
+ $stdout.puts "* Found #{relation_count} #{relation_name}".color(:green)
+
+ relation_count
+ end
+
+ def self.projects_list(relation_name, relation)
+ listing(relation_name, relation.with_route) do |project|
+ $stdout.puts " - #{project.full_path} (id: #{project.id})".color(:red)
+ end
+ end
+
+ def self.attachments_list(relation_name, relation)
+ listing(relation_name, relation) do |upload|
+ $stdout.puts " - #{upload.path} (id: #{upload.id})".color(:red)
+ end
+ end
+
+ def self.listing(relation_name, relation)
+ relation_count = relation_summary(relation_name, relation)
+ return unless relation_count > 0
+
+ limit = listing_limit
+
+ if relation_count > limit
+ $stdout.puts " ! Displaying first #{limit} #{relation_name}..."
+ end
+
+ relation.find_each(batch_size: batch_size).with_index do |element, index|
+ yield element
+
+ break if index + 1 >= limit
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/health_checks/db_check.rb b/lib/gitlab/health_checks/db_check.rb
index e27e16ddaf6..08495c0a59e 100644
--- a/lib/gitlab/health_checks/db_check.rb
+++ b/lib/gitlab/health_checks/db_check.rb
@@ -17,7 +17,7 @@ module Gitlab
def check
catch_timeout 10.seconds do
if Gitlab::Database.postgresql?
- ActiveRecord::Base.connection.execute('SELECT 1 as ping')&.first&.[]('ping')
+ ActiveRecord::Base.connection.execute('SELECT 1 as ping')&.first&.[]('ping')&.to_s
else
ActiveRecord::Base.connection.execute('SELECT 1 as ping')&.first&.first&.to_s
end
diff --git a/lib/gitlab/health_checks/fs_shards_check.rb b/lib/gitlab/health_checks/fs_shards_check.rb
deleted file mode 100644
index 6e554383270..00000000000
--- a/lib/gitlab/health_checks/fs_shards_check.rb
+++ /dev/null
@@ -1,166 +0,0 @@
-module Gitlab
- module HealthChecks
- class FsShardsCheck
- extend BaseAbstractCheck
- RANDOM_STRING = SecureRandom.hex(1000).freeze
- COMMAND_TIMEOUT = '1'.freeze
- TIMEOUT_EXECUTABLE = 'timeout'.freeze
-
- class << self
- def readiness
- repository_storages.map do |storage_name|
- begin
- if !storage_circuitbreaker_test(storage_name)
- HealthChecks::Result.new(false, 'circuitbreaker tripped', shard: storage_name)
- elsif !storage_stat_test(storage_name)
- HealthChecks::Result.new(false, 'cannot stat storage', shard: storage_name)
- else
- with_temp_file(storage_name) do |tmp_file_path|
- if !storage_write_test(tmp_file_path)
- HealthChecks::Result.new(false, 'cannot write to storage', shard: storage_name)
- elsif !storage_read_test(tmp_file_path)
- HealthChecks::Result.new(false, 'cannot read from storage', shard: storage_name)
- else
- HealthChecks::Result.new(true, nil, shard: storage_name)
- end
- end
- end
- rescue RuntimeError => ex
- message = "unexpected error #{ex} when checking storage #{storage_name}"
- Rails.logger.error(message)
- HealthChecks::Result.new(false, message, shard: storage_name)
- end
- end
- end
-
- def metrics
- repository_storages.flat_map do |storage_name|
- [
- storage_stat_metrics(storage_name),
- storage_write_metrics(storage_name),
- storage_read_metrics(storage_name),
- storage_circuitbreaker_metrics(storage_name)
- ].flatten
- end
- end
-
- private
-
- def operation_metrics(ok_metric, latency_metric, **labels)
- result, elapsed = yield
- [
- metric(latency_metric, elapsed, **labels),
- metric(ok_metric, result ? 1 : 0, **labels)
- ]
- rescue RuntimeError => ex
- Rails.logger.error("unexpected error #{ex} when checking #{ok_metric}")
- [metric(ok_metric, 0, **labels)]
- end
-
- def repository_storages
- storages_paths.keys
- end
-
- def storages_paths
- Gitlab.config.repositories.storages
- end
-
- def exec_with_timeout(cmd_args, *args, &block)
- Gitlab::Popen.popen([TIMEOUT_EXECUTABLE, COMMAND_TIMEOUT].concat(cmd_args), *args, &block)
- end
-
- def with_temp_file(storage_name)
- temp_file_path = Dir::Tmpname.create(%w(fs_shards_check +deleted), storage_path(storage_name)) { |path| path }
- yield temp_file_path
- ensure
- delete_test_file(temp_file_path)
- end
-
- def storage_path(storage_name)
- storages_paths[storage_name]&.legacy_disk_path
- end
-
- # All below test methods use shell commands to perform actions on storage volumes.
- # In case a storage volume have connectivity problems causing pure Ruby IO operation to wait indefinitely,
- # we can rely on shell commands to be terminated once `timeout` kills them.
- #
- # However we also fallback to pure Ruby file operations in case a specific shell command is missing
- # so we are still able to perform healthchecks and gather metrics from such system.
-
- def delete_test_file(tmp_path)
- _, status = exec_with_timeout(%W{ rm -f #{tmp_path} })
- status.zero?
- rescue Errno::ENOENT
- File.delete(tmp_path) rescue Errno::ENOENT
- end
-
- def storage_stat_test(storage_name)
- stat_path = File.join(storage_path(storage_name), '.')
- begin
- _, status = exec_with_timeout(%W{ stat #{stat_path} })
- status.zero?
- rescue Errno::ENOENT
- File.exist?(stat_path) && File::Stat.new(stat_path).readable?
- end
- end
-
- def storage_write_test(tmp_path)
- _, status = exec_with_timeout(%W{ tee #{tmp_path} }) do |stdin|
- stdin.write(RANDOM_STRING)
- end
- status.zero?
- rescue Errno::ENOENT
- written_bytes = File.write(tmp_path, RANDOM_STRING) rescue Errno::ENOENT
- written_bytes == RANDOM_STRING.length
- end
-
- def storage_read_test(tmp_path)
- _, status = exec_with_timeout(%W{ diff #{tmp_path} - }) do |stdin|
- stdin.write(RANDOM_STRING)
- end
- status.zero?
- rescue Errno::ENOENT
- file_contents = File.read(tmp_path) rescue Errno::ENOENT
- file_contents == RANDOM_STRING
- end
-
- def storage_circuitbreaker_test(storage_name)
- Gitlab::Git::Storage::CircuitBreaker.build(storage_name).perform { "OK" }
- rescue Gitlab::Git::Storage::Inaccessible
- nil
- end
-
- def storage_stat_metrics(storage_name)
- operation_metrics(:filesystem_accessible, :filesystem_access_latency_seconds, shard: storage_name) do
- with_timing { storage_stat_test(storage_name) }
- end
- end
-
- def storage_write_metrics(storage_name)
- operation_metrics(:filesystem_writable, :filesystem_write_latency_seconds, shard: storage_name) do
- with_temp_file(storage_name) do |tmp_file_path|
- with_timing { storage_write_test(tmp_file_path) }
- end
- end
- end
-
- def storage_read_metrics(storage_name)
- operation_metrics(:filesystem_readable, :filesystem_read_latency_seconds, shard: storage_name) do
- with_temp_file(storage_name) do |tmp_file_path|
- storage_write_test(tmp_file_path) # writes data used by read test
- with_timing { storage_read_test(tmp_file_path) }
- end
- end
- end
-
- def storage_circuitbreaker_metrics(storage_name)
- operation_metrics(:filesystem_circuitbreaker,
- :filesystem_circuitbreaker_latency_seconds,
- shard: storage_name) do
- with_timing { storage_circuitbreaker_test(storage_name) }
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/health_checks/gitaly_check.rb b/lib/gitlab/health_checks/gitaly_check.rb
index 11416c002e3..1f623e0b6ec 100644
--- a/lib/gitlab/health_checks/gitaly_check.rb
+++ b/lib/gitlab/health_checks/gitaly_check.rb
@@ -13,14 +13,14 @@ module Gitlab
end
def metrics
- repository_storages.flat_map do |storage_name|
- result, elapsed = with_timing { check(storage_name) }
- labels = { shard: storage_name }
+ Gitaly::Server.all.flat_map do |server|
+ result, elapsed = with_timing { server.read_writeable? }
+ labels = { shard: server.storage }
[
- metric("#{metric_prefix}_success", successful?(result) ? 1 : 0, **labels),
+ metric("#{metric_prefix}_success", result ? 1 : 0, **labels),
metric("#{metric_prefix}_latency_seconds", elapsed, **labels)
- ].flatten
+ ]
end
end
@@ -36,10 +36,6 @@ module Gitlab
METRIC_PREFIX
end
- def successful?(result)
- result[:success]
- end
-
def repository_storages
storages.keys
end
diff --git a/lib/gitlab/http_io.rb b/lib/gitlab/http_io.rb
new file mode 100644
index 00000000000..ce24817db54
--- /dev/null
+++ b/lib/gitlab/http_io.rb
@@ -0,0 +1,193 @@
+##
+# This class is compatible with IO class (https://ruby-doc.org/core-2.3.1/IO.html)
+# source: https://gitlab.com/snippets/1685610
+module Gitlab
+ class HttpIO
+ BUFFER_SIZE = 128.kilobytes
+
+ InvalidURLError = Class.new(StandardError)
+ FailedToGetChunkError = Class.new(StandardError)
+
+ attr_reader :uri, :size
+ attr_reader :tell
+ attr_reader :chunk, :chunk_range
+
+ alias_method :pos, :tell
+
+ def initialize(url, size)
+ raise InvalidURLError unless ::Gitlab::UrlSanitizer.valid?(url)
+
+ @uri = URI(url)
+ @size = size
+ @tell = 0
+ end
+
+ def close
+ # no-op
+ end
+
+ def binmode
+ # no-op
+ end
+
+ def binmode?
+ true
+ end
+
+ def path
+ nil
+ end
+
+ def url
+ @uri.to_s
+ end
+
+ def seek(pos, where = IO::SEEK_SET)
+ new_pos =
+ case where
+ when IO::SEEK_END
+ size + pos
+ when IO::SEEK_SET
+ pos
+ when IO::SEEK_CUR
+ tell + pos
+ else
+ -1
+ end
+
+ raise 'new position is outside of file' if new_pos < 0 || new_pos > size
+
+ @tell = new_pos
+ end
+
+ def eof?
+ tell == size
+ end
+
+ def each_line
+ until eof?
+ line = readline
+ break if line.nil?
+
+ yield(line)
+ end
+ end
+
+ def read(length = nil, outbuf = "")
+ out = ""
+
+ length ||= size - tell
+
+ until length <= 0 || eof?
+ data = get_chunk
+ break if data.empty?
+
+ chunk_bytes = [BUFFER_SIZE - chunk_offset, length].min
+ chunk_data = data.byteslice(0, chunk_bytes)
+
+ out << chunk_data
+ @tell += chunk_data.bytesize
+ length -= chunk_data.bytesize
+ end
+
+ # If outbuf is passed, we put the output into the buffer. This supports IO.copy_stream functionality
+ if outbuf
+ outbuf.slice!(0, outbuf.bytesize)
+ outbuf << out
+ end
+
+ out
+ end
+
+ def readline
+ out = ""
+
+ until eof?
+ data = get_chunk
+ new_line = data.index("\n")
+
+ if !new_line.nil?
+ out << data[0..new_line]
+ @tell += new_line + 1
+ break
+ else
+ out << data
+ @tell += data.bytesize
+ end
+ end
+
+ out
+ end
+
+ def write(data)
+ raise NotImplementedError
+ end
+
+ def truncate(offset)
+ raise NotImplementedError
+ end
+
+ def flush
+ raise NotImplementedError
+ end
+
+ def present?
+ true
+ end
+
+ private
+
+ ##
+ # The below methods are not implemented in IO class
+ #
+ def in_range?
+ @chunk_range&.include?(tell)
+ end
+
+ def get_chunk
+ unless in_range?
+ response = Net::HTTP.start(uri.hostname, uri.port, proxy_from_env: true, use_ssl: uri.scheme == 'https') do |http|
+ http.request(request)
+ end
+
+ raise FailedToGetChunkError unless response.code == '200' || response.code == '206'
+
+ @chunk = response.body.force_encoding(Encoding::BINARY)
+ @chunk_range = response.content_range
+
+ ##
+ # Note: If provider does not return content_range, then we set it as we requested
+ # Provider: minio
+ # - When the file size is larger than requested Content-range, the Content-range is included in responces with Net::HTTPPartialContent 206
+ # - When the file size is smaller than requested Content-range, the Content-range is included in responces with Net::HTTPPartialContent 206
+ # Provider: AWS
+ # - When the file size is larger than requested Content-range, the Content-range is included in responces with Net::HTTPPartialContent 206
+ # - When the file size is smaller than requested Content-range, the Content-range is included in responces with Net::HTTPPartialContent 206
+ # Provider: GCS
+ # - When the file size is larger than requested Content-range, the Content-range is included in responces with Net::HTTPPartialContent 206
+ # - When the file size is smaller than requested Content-range, the Content-range is included in responces with Net::HTTPOK 200
+ @chunk_range ||= (chunk_start...(chunk_start + @chunk.bytesize))
+ end
+
+ @chunk[chunk_offset..BUFFER_SIZE]
+ end
+
+ def request
+ Net::HTTP::Get.new(uri).tap do |request|
+ request.set_range(chunk_start, BUFFER_SIZE)
+ end
+ end
+
+ def chunk_offset
+ tell % BUFFER_SIZE
+ end
+
+ def chunk_start
+ (tell / BUFFER_SIZE) * BUFFER_SIZE
+ end
+
+ def chunk_end
+ [chunk_start + BUFFER_SIZE, size].min
+ end
+ end
+end
diff --git a/lib/gitlab/i18n.rb b/lib/gitlab/i18n.rb
index 3772ef11c7f..343487bc361 100644
--- a/lib/gitlab/i18n.rb
+++ b/lib/gitlab/i18n.rb
@@ -21,7 +21,8 @@ module Gitlab
'nl_NL' => 'Nederlands',
'tr_TR' => 'Türkçe',
'id_ID' => 'Bahasa Indonesia',
- 'fil_PH' => 'Filipino'
+ 'fil_PH' => 'Filipino',
+ 'pl_PL' => 'Polski'
}.freeze
def available_locales
diff --git a/lib/gitlab/i18n/metadata_entry.rb b/lib/gitlab/i18n/metadata_entry.rb
index 35d57459a3d..36fc1bcdcb7 100644
--- a/lib/gitlab/i18n/metadata_entry.rb
+++ b/lib/gitlab/i18n/metadata_entry.rb
@@ -3,16 +3,25 @@ module Gitlab
class MetadataEntry
attr_reader :entry_data
+ # Avoid testing too many plurals if `nplurals` was incorrectly set.
+ # Based on info on https://www.gnu.org/software/gettext/manual/html_node/Plural-forms.html
+ # which mentions special cases for numbers ending in 2 digits
+ MAX_FORMS_TO_TEST = 101
+
def initialize(entry_data)
@entry_data = entry_data
end
- def expected_plurals
+ def expected_forms
return nil unless plural_information
plural_information['nplurals'].to_i
end
+ def forms_to_test
+ @forms_to_test ||= [expected_forms, MAX_FORMS_TO_TEST].compact.min
+ end
+
private
def plural_information
diff --git a/lib/gitlab/i18n/po_linter.rb b/lib/gitlab/i18n/po_linter.rb
index 7d3ff8c7f58..d8e7269a2c2 100644
--- a/lib/gitlab/i18n/po_linter.rb
+++ b/lib/gitlab/i18n/po_linter.rb
@@ -1,6 +1,8 @@
module Gitlab
module I18n
class PoLinter
+ include Gitlab::Utils::StrongMemoize
+
attr_reader :po_path, :translation_entries, :metadata_entry, :locale
VARIABLE_REGEX = /%{\w*}|%[a-z]/.freeze
@@ -34,7 +36,7 @@ module Gitlab
end
@translation_entries = entries.map do |entry_data|
- Gitlab::I18n::TranslationEntry.new(entry_data, metadata_entry.expected_plurals)
+ Gitlab::I18n::TranslationEntry.new(entry_data, metadata_entry.expected_forms)
end
nil
@@ -48,7 +50,7 @@ module Gitlab
translation_entries.each do |entry|
errors_for_entry = validate_entry(entry)
- errors[join_message(entry.msgid)] = errors_for_entry if errors_for_entry.any?
+ errors[entry.msgid] = errors_for_entry if errors_for_entry.any?
end
errors
@@ -62,6 +64,7 @@ module Gitlab
validate_newlines(errors, entry)
validate_number_of_plurals(errors, entry)
validate_unescaped_chars(errors, entry)
+ validate_translation(errors, entry)
errors
end
@@ -81,35 +84,39 @@ module Gitlab
end
def validate_number_of_plurals(errors, entry)
- return unless metadata_entry&.expected_plurals
+ return unless metadata_entry&.expected_forms
return unless entry.translated?
- if entry.has_plural? && entry.all_translations.size != metadata_entry.expected_plurals
- errors << "should have #{metadata_entry.expected_plurals} "\
- "#{'translations'.pluralize(metadata_entry.expected_plurals)}"
+ if entry.has_plural? && entry.all_translations.size != metadata_entry.expected_forms
+ errors << "should have #{metadata_entry.expected_forms} "\
+ "#{'translations'.pluralize(metadata_entry.expected_forms)}"
end
end
def validate_newlines(errors, entry)
- if entry.msgid_contains_newlines?
+ if entry.msgid_has_multiple_lines?
errors << 'is defined over multiple lines, this breaks some tooling.'
end
- if entry.plural_id_contains_newlines?
+ if entry.plural_id_has_multiple_lines?
errors << 'plural is defined over multiple lines, this breaks some tooling.'
end
- if entry.translations_contain_newlines?
+ if entry.translations_have_multiple_lines?
errors << 'has translations defined over multiple lines, this breaks some tooling.'
end
end
def validate_variables(errors, entry)
if entry.has_singular_translation?
+ validate_variables_in_message(errors, entry.msgid, entry.msgid)
+
validate_variables_in_message(errors, entry.msgid, entry.singular_translation)
end
if entry.has_plural?
+ validate_variables_in_message(errors, entry.plural_id, entry.plural_id)
+
entry.plural_translations.each do |translation|
validate_variables_in_message(errors, entry.plural_id, translation)
end
@@ -117,41 +124,98 @@ module Gitlab
end
def validate_variables_in_message(errors, message_id, message_translation)
- message_id = join_message(message_id)
required_variables = message_id.scan(VARIABLE_REGEX)
validate_unnamed_variables(errors, required_variables)
- validate_translation(errors, message_id, required_variables)
validate_variable_usage(errors, message_translation, required_variables)
end
- def validate_translation(errors, message_id, used_variables)
+ def validate_translation(errors, entry)
+ Gitlab::I18n.with_locale(locale) do
+ if entry.has_plural?
+ translate_plural(entry)
+ else
+ translate_singular(entry)
+ end
+ end
+
+ # `sprintf` could raise an `ArgumentError` when invalid passing something
+ # other than a Hash when using named variables
+ #
+ # `sprintf` could raise `TypeError` when passing a wrong type when using
+ # unnamed variables
+ #
+ # FastGettext::Translation could raise `RuntimeError` (raised as a string),
+ # or as subclassess `NoTextDomainConfigured` & `InvalidFormat`
+ #
+ # `FastGettext::Translation` could raise `ArgumentError` as subclassess
+ # `InvalidEncoding`, `IllegalSequence` & `InvalidCharacter`
+ rescue ArgumentError, TypeError, RuntimeError => e
+ errors << "Failure translating to #{locale}: #{e.message}"
+ end
+
+ def translate_singular(entry)
+ used_variables = entry.msgid.scan(VARIABLE_REGEX)
variables = fill_in_variables(used_variables)
- begin
- Gitlab::I18n.with_locale(locale) do
- translated = if message_id.include?('|')
- FastGettext::Translation.s_(message_id)
- else
- FastGettext::Translation._(message_id)
- end
+ translation = if entry.msgid.include?('|')
+ FastGettext::Translation.s_(entry.msgid)
+ else
+ FastGettext::Translation._(entry.msgid)
+ end
- translated % variables
+ translation % variables if used_variables.any?
+ end
+
+ def translate_plural(entry)
+ used_variables = entry.plural_id.scan(VARIABLE_REGEX)
+ variables = fill_in_variables(used_variables)
+
+ numbers_covering_all_plurals.map do |number|
+ translation = FastGettext::Translation.n_(entry.msgid, entry.plural_id, number)
+
+ translation % variables if used_variables.any?
+ end
+ end
+
+ def numbers_covering_all_plurals
+ @numbers_covering_all_plurals ||= calculate_numbers_covering_all_plurals
+ end
+
+ def calculate_numbers_covering_all_plurals
+ required_numbers = []
+ discovered_indexes = []
+ counter = 0
+
+ while discovered_indexes.size < metadata_entry.forms_to_test && counter < Gitlab::I18n::MetadataEntry::MAX_FORMS_TO_TEST
+ index_for_count = index_for_pluralization(counter)
+
+ unless discovered_indexes.include?(index_for_count)
+ discovered_indexes << index_for_count
+ required_numbers << counter
end
- # `sprintf` could raise an `ArgumentError` when invalid passing something
- # other than a Hash when using named variables
- #
- # `sprintf` could raise `TypeError` when passing a wrong type when using
- # unnamed variables
- #
- # FastGettext::Translation could raise `RuntimeError` (raised as a string),
- # or as subclassess `NoTextDomainConfigured` & `InvalidFormat`
- #
- # `FastGettext::Translation` could raise `ArgumentError` as subclassess
- # `InvalidEncoding`, `IllegalSequence` & `InvalidCharacter`
- rescue ArgumentError, TypeError, RuntimeError => e
- errors << "Failure translating to #{locale} with #{variables}: #{e.message}"
+ counter += 1
+ end
+
+ required_numbers
+ end
+
+ def index_for_pluralization(counter)
+ # This calls the C function that defines the pluralization rule, it can
+ # return a boolean (`false` represents 0, `true` represents 1) or an integer
+ # that specifies the plural form to be used for the given number
+ pluralization_result = Gitlab::I18n.with_locale(locale) do
+ FastGettext.pluralisation_rule.call(counter)
+ end
+
+ case pluralization_result
+ when false
+ 0
+ when true
+ 1
+ else
+ pluralization_result
end
end
@@ -172,14 +236,18 @@ module Gitlab
end
def validate_unnamed_variables(errors, variables)
- if variables.size > 1 && variables.any? { |variable_name| unnamed_variable?(variable_name) }
+ unnamed_variables, named_variables = variables.partition { |name| unnamed_variable?(name) }
+
+ if unnamed_variables.any? && named_variables.any?
+ errors << 'is combining named variables with unnamed variables'
+ end
+
+ if unnamed_variables.size > 1
errors << 'is combining multiple unnamed variables'
end
end
def validate_variable_usage(errors, translation, required_variables)
- translation = join_message(translation)
-
# We don't need to validate when the message is empty.
# In this case we fall back to the default, which has all the the
# required variables.
@@ -205,10 +273,6 @@ module Gitlab
def validate_flags(errors, entry)
errors << "is marked #{entry.flag}" if entry.flag
end
-
- def join_message(message)
- Array(message).join
- end
end
end
end
diff --git a/lib/gitlab/i18n/translation_entry.rb b/lib/gitlab/i18n/translation_entry.rb
index e6c95afca7e..54adb98f42d 100644
--- a/lib/gitlab/i18n/translation_entry.rb
+++ b/lib/gitlab/i18n/translation_entry.rb
@@ -11,11 +11,11 @@ module Gitlab
end
def msgid
- entry_data[:msgid]
+ @msgid ||= Array(entry_data[:msgid]).join
end
def plural_id
- entry_data[:msgid_plural]
+ @plural_id ||= Array(entry_data[:msgid_plural]).join
end
def has_plural?
@@ -23,12 +23,11 @@ module Gitlab
end
def singular_translation
- all_translations.first if has_singular_translation?
+ all_translations.first.to_s if has_singular_translation?
end
def all_translations
- @all_translations ||= entry_data.fetch_values(*translation_keys)
- .reject(&:empty?)
+ @all_translations ||= translation_entries.map { |translation| Array(translation).join }
end
def translated?
@@ -54,16 +53,16 @@ module Gitlab
nplurals > 1 || !has_plural?
end
- def msgid_contains_newlines?
- msgid.is_a?(Array)
+ def msgid_has_multiple_lines?
+ entry_data[:msgid].is_a?(Array)
end
- def plural_id_contains_newlines?
- plural_id.is_a?(Array)
+ def plural_id_has_multiple_lines?
+ entry_data[:msgid_plural].is_a?(Array)
end
- def translations_contain_newlines?
- all_translations.any? { |translation| translation.is_a?(Array) }
+ def translations_have_multiple_lines?
+ translation_entries.any? { |translation| translation.is_a?(Array) }
end
def msgid_contains_unescaped_chars?
@@ -84,6 +83,11 @@ module Gitlab
private
+ def translation_entries
+ @translation_entries ||= entry_data.fetch_values(*translation_keys)
+ .reject(&:empty?)
+ end
+
def translation_keys
@translation_keys ||= entry_data.keys.select { |key| key.to_s =~ /\Amsgstr(\[\d+\])?\z/ }
end
diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb
index b713fa7e1cd..be3710c5b7f 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.3'.freeze
+ VERSION = '0.2.4'.freeze
FILENAME_LIMIT = 50
def export_path(relative_path:)
@@ -40,6 +40,10 @@ module Gitlab
"#{basename[0..FILENAME_LIMIT]}_export.tar.gz"
end
+ def object_storage?
+ Feature.enabled?(:import_export_object_storage)
+ end
+
def version
VERSION
end
diff --git a/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb b/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb
index aef371d81eb..83134bb0769 100644
--- a/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb
+++ b/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb
@@ -2,6 +2,7 @@ module Gitlab
module ImportExport
module AfterExportStrategies
class BaseAfterExportStrategy
+ extend Gitlab::ImportExport::CommandLineUtil
include ActiveModel::Validations
extend Forwardable
@@ -24,9 +25,10 @@ module Gitlab
end
def execute(current_user, project)
- return unless project&.export_project_path
-
@project = project
+
+ return unless @project.export_status == :finished
+
@current_user = current_user
if invalid?
@@ -51,9 +53,12 @@ module Gitlab
end
def self.lock_file_path(project)
- return unless project&.export_path
+ return unless project.export_path || object_storage?
- File.join(project.export_path, AFTER_EXPORT_LOCK_FILE_NAME)
+ lock_path = project.import_export_shared.archive_path
+
+ mkdir_p(lock_path)
+ File.join(lock_path, AFTER_EXPORT_LOCK_FILE_NAME)
end
protected
@@ -77,6 +82,10 @@ module Gitlab
def log_validation_errors
errors.full_messages.each { |msg| project.import_export_shared.add_error_message(msg) }
end
+
+ def object_storage?
+ project.export_project_object_exists?
+ end
end
end
end
diff --git a/lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb b/lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb
index 938664a95a1..dce8f89c0ab 100644
--- a/lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb
+++ b/lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb
@@ -38,14 +38,20 @@ module Gitlab
private
def send_file
- export_file = File.open(project.export_project_path)
-
- Gitlab::HTTP.public_send(http_method.downcase, url, send_file_options(export_file)) # rubocop:disable GitlabSecurity/PublicSend
+ Gitlab::HTTP.public_send(http_method.downcase, url, send_file_options) # rubocop:disable GitlabSecurity/PublicSend
ensure
- export_file.close if export_file
+ export_file.close if export_file && !object_storage?
+ end
+
+ def export_file
+ if object_storage?
+ project.import_export_upload.export_file.file.open
+ else
+ File.open(project.export_project_path)
+ end
end
- def send_file_options(export_file)
+ def send_file_options
{
body_stream: export_file,
headers: headers
@@ -53,7 +59,15 @@ module Gitlab
end
def headers
- { 'Content-Length' => File.size(project.export_project_path).to_s }
+ { 'Content-Length' => export_size.to_s }
+ end
+
+ def export_size
+ if object_storage?
+ project.import_export_upload.export_file.file.size
+ else
+ File.size(project.export_project_path)
+ end
end
end
end
diff --git a/lib/gitlab/import_export/attribute_cleaner.rb b/lib/gitlab/import_export/attribute_cleaner.rb
index 34169319b26..7c9fc5c15bb 100644
--- a/lib/gitlab/import_export/attribute_cleaner.rb
+++ b/lib/gitlab/import_export/attribute_cleaner.rb
@@ -7,14 +7,15 @@ module Gitlab
new(*args).clean
end
- def initialize(relation_hash:, relation_class:)
+ def initialize(relation_hash:, relation_class:, excluded_keys: [])
@relation_hash = relation_hash
@relation_class = relation_class
+ @excluded_keys = excluded_keys
end
def clean
@relation_hash.reject do |key, _value|
- prohibited_key?(key) || !@relation_class.attribute_method?(key)
+ prohibited_key?(key) || !@relation_class.attribute_method?(key) || excluded_key?(key)
end.except('id')
end
@@ -23,6 +24,12 @@ module Gitlab
def prohibited_key?(key)
key.end_with?('_id') && !ALLOWED_REFERENCES.include?(key)
end
+
+ def excluded_key?(key)
+ return false if @excluded_keys.empty?
+
+ @excluded_keys.include?(key)
+ end
end
end
end
diff --git a/lib/gitlab/import_export/attributes_finder.rb b/lib/gitlab/import_export/attributes_finder.rb
index 56042ddecbf..0c8fda07294 100644
--- a/lib/gitlab/import_export/attributes_finder.rb
+++ b/lib/gitlab/import_export/attributes_finder.rb
@@ -32,6 +32,10 @@ module Gitlab
@methods[key].nil? ? {} : { methods: @methods[key] }
end
+ def find_excluded_keys(klass_name)
+ @excluded_attributes[klass_name.to_sym]&.map(&:to_s) || []
+ end
+
private
def find_attributes_only(value)
diff --git a/lib/gitlab/import_export/group_project_object_builder.rb b/lib/gitlab/import_export/group_project_object_builder.rb
new file mode 100644
index 00000000000..6c2af770119
--- /dev/null
+++ b/lib/gitlab/import_export/group_project_object_builder.rb
@@ -0,0 +1,90 @@
+module Gitlab
+ module ImportExport
+ # Given a class, it finds or creates a new object
+ # (initializes in the case of Label) at group or project level.
+ # If it does not exist in the group, it creates it at project level.
+ #
+ # Example:
+ # `GroupProjectObjectBuilder.build(Label, label_attributes)`
+ # finds or initializes a label with the given attributes.
+ #
+ # It also adds some logic around Group Labels/Milestones for edge cases.
+ class GroupProjectObjectBuilder
+ def self.build(*args)
+ Project.transaction do
+ new(*args).find
+ end
+ end
+
+ def initialize(klass, attributes)
+ @klass = klass < Label ? Label : klass
+ @attributes = attributes
+ @group = @attributes['group']
+ @project = @attributes['project']
+ end
+
+ def find
+ find_object || @klass.create(project_attributes)
+ end
+
+ private
+
+ def find_object
+ @klass.where(where_clause).first
+ end
+
+ def where_clause
+ @attributes.slice('title').map do |key, value|
+ scope_clause = table[:project_id].eq(@project.id)
+ scope_clause = scope_clause.or(table[:group_id].eq(@group.id)) if @group
+
+ table[key].eq(value).and(scope_clause)
+ end.reduce(:or)
+ end
+
+ def table
+ @table ||= @klass.arel_table
+ end
+
+ def project_attributes
+ @attributes.except('group').tap do |atts|
+ if label?
+ atts['type'] = 'ProjectLabel' # Always create project labels
+ elsif milestone?
+ if atts['group_id'] # Transform new group milestones into project ones
+ atts['iid'] = nil
+ atts.delete('group_id')
+ else
+ claim_iid
+ end
+ end
+ end
+ end
+
+ def label?
+ @klass == Label
+ end
+
+ def milestone?
+ @klass == Milestone
+ end
+
+ # If an existing group milestone used the IID
+ # claim the IID back and set the group milestone to use one available
+ # This is necessary to fix situations like the following:
+ # - Importing into a user namespace project with exported group milestones
+ # where the IID of the Group milestone could conflict with a project one.
+ def claim_iid
+ # The milestone has to be a group milestone, as it's the only case where
+ # we set the IID as the maximum. The rest of them are fixed.
+ milestone = @project.milestones.find_by(iid: @attributes['iid'])
+
+ return unless milestone
+
+ milestone.iid = nil
+ milestone.ensure_project_iid!
+ milestone.save!
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index 21ac7f7e0b6..da3667faf7a 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -28,6 +28,7 @@ project_tree:
- project_members:
- :user
- merge_requests:
+ - :metrics
- notes:
- :author
- events:
@@ -98,8 +99,6 @@ excluded_attributes:
- :import_jid
- :created_at
- :updated_at
- - :import_jid
- - :import_jid
- :id
- :star_count
- :last_activity_at
diff --git a/lib/gitlab/import_export/members_mapper.rb b/lib/gitlab/import_export/members_mapper.rb
index 8b8e48aac76..ac827cbe1ca 100644
--- a/lib/gitlab/import_export/members_mapper.rb
+++ b/lib/gitlab/import_export/members_mapper.rb
@@ -47,7 +47,7 @@ module Gitlab
def ensure_default_member!
@project.project_members.destroy_all
- ProjectMember.create!(user: @user, access_level: ProjectMember::MASTER, source_id: @project.id, importing: true)
+ ProjectMember.create!(user: @user, access_level: ProjectMember::MAINTAINER, source_id: @project.id, importing: true)
end
def add_team_member(member, existing_user = nil)
diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb
index d5590dde40f..76b99b1de16 100644
--- a/lib/gitlab/import_export/project_tree_restorer.rb
+++ b/lib/gitlab/import_export/project_tree_restorer.rb
@@ -1,8 +1,8 @@
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(milestone milestones).freeze
+ # Relations which cannot be saved at project level (and have a group assigned)
+ GROUP_MODELS = [GroupLabel, Milestone].freeze
def initialize(user:, shared:, project:)
@path = File.join(shared.export_path, 'project.json')
@@ -70,12 +70,23 @@ module Gitlab
def save_relation_hash(relation_hash_batch, relation_key)
relation_hash = create_relation(relation_key, relation_hash_batch)
+ remove_group_models(relation_hash) if relation_hash.is_a?(Array)
+
@saved = false unless restored_project.append_or_update_attribute(relation_key, relation_hash)
# Restore the project again, extra query that skips holding the AR objects in memory
@restored_project = Project.find(@project_id)
end
+ # Remove project models that became group models as we found them at group level.
+ # This no longer required saving them at the root project level.
+ # For example, in the case of an existing group label that matched the title.
+ def remove_group_models(relation_hash)
+ relation_hash.reject! do |value|
+ GROUP_MODELS.include?(value.class) && value.group_id
+ end
+ end
+
def default_relation_list
reader.tree.reject do |model|
model.is_a?(Hash) && model[:project_members]
@@ -88,16 +99,18 @@ module Gitlab
end
def project_params
- @project_params ||= json_params.merge(override_params)
+ @project_params ||= begin
+ attrs = json_params.merge(override_params)
+
+ # Cleaning all imported and overridden params
+ Gitlab::ImportExport::AttributeCleaner.clean(relation_hash: attrs,
+ relation_class: Project,
+ excluded_keys: excluded_keys_for_relation(:project))
+ end
end
def override_params
- return {} unless params = @project.import_data&.data&.fetch('override_params', nil)
-
- @override_params ||= params.select do |key, _value|
- Project.column_names.include?(key.to_s) &&
- !reader.project_tree[:except].include?(key.to_sym)
- end
+ @override_params ||= @project.import_data&.data&.fetch('override_params', nil) || {}
end
def json_params
@@ -168,30 +181,23 @@ module Gitlab
def create_relation(relation, relation_hash_list)
relation_array = [relation_hash_list].flatten.map do |relation_hash|
Gitlab::ImportExport::RelationFactory.create(relation_sym: relation.to_sym,
- relation_hash: parsed_relation_hash(relation_hash, relation.to_sym),
+ relation_hash: relation_hash,
members_mapper: members_mapper,
user: @user,
- project: @restored_project)
+ project: @restored_project,
+ excluded_keys: excluded_keys_for_relation(relation))
end.compact
relation_hash_list.is_a?(Array) ? relation_array : relation_array.first
end
- def parsed_relation_hash(relation_hash, relation_type)
- if RESTRICT_PROJECT_AND_GROUP.include?(relation_type)
- params = {}
- params['group_id'] = restored_project.group.try(:id) if relation_hash['group_id']
- params['project_id'] = restored_project.id if relation_hash['project_id']
- else
- params = { 'group_id' => restored_project.group.try(:id), 'project_id' => restored_project.id }
- end
-
- relation_hash.merge(params)
- end
-
def reader
@reader ||= Gitlab::ImportExport::Reader.new(shared: @shared)
end
+
+ def excluded_keys_for_relation(relation)
+ @reader.attributes_finder.find_excluded_keys(relation)
+ end
end
end
end
diff --git a/lib/gitlab/import_export/reader.rb b/lib/gitlab/import_export/reader.rb
index eb7f5120592..e621c40fc7a 100644
--- a/lib/gitlab/import_export/reader.rb
+++ b/lib/gitlab/import_export/reader.rb
@@ -1,7 +1,7 @@
module Gitlab
module ImportExport
class Reader
- attr_reader :tree
+ attr_reader :tree, :attributes_finder
def initialize(shared:)
@shared = shared
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index 4a41a69840b..091e81028bb 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -18,9 +18,10 @@ module Gitlab
label: :project_label,
custom_attributes: 'ProjectCustomAttribute',
project_badges: 'Badge',
+ metrics: 'MergeRequest::Metrics',
ci_cd_settings: 'ProjectCiCdSetting' }.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 closed_by_id].freeze
+ USER_REFERENCES = %w[author_id assignee_id updated_by_id merged_by_id latest_closed_by_id user_id created_by_id last_edited_by_id merge_user_id resolved_by_id closed_by_id].freeze
PROJECT_REFERENCES = %w[project_id source_project_id target_project_id].freeze
@@ -36,13 +37,32 @@ module Gitlab
new(*args).create
end
- def initialize(relation_sym:, relation_hash:, members_mapper:, user:, project:)
+ def self.relation_class(relation_name)
+ # There are scenarios where the model is pluralized (e.g.
+ # MergeRequest::Metrics), and we don't want to force it to singular
+ # with #classify.
+ relation_name.to_s.classify.constantize
+ rescue NameError
+ relation_name.to_s.constantize
+ end
+
+ def initialize(relation_sym:, relation_hash:, members_mapper:, user:, project:, excluded_keys: [])
@relation_name = OVERRIDES[relation_sym] || relation_sym
@relation_hash = relation_hash.except('noteable_id')
@members_mapper = members_mapper
@user = user
@project = project
@imported_object_retries = 0
+
+ @relation_hash['project_id'] = @project.id
+
+ # Remove excluded keys from relation_hash
+ # We don't do this in the parsed_relation_hash because of the 'transformed attributes'
+ # For example, MergeRequestDiffFiles exports its diff attribute as utf8_diff. Then,
+ # in the create method that attribute is renamed to diff. And because diff is an excluded key,
+ # if we clean the excluded keys in the parsed_relation_hash, it will be removed
+ # from the object attributes and the export will fail.
+ @relation_hash.except!(*excluded_keys)
end
# Creates an object from an actual model with name "relation_sym" with params from
@@ -62,15 +82,12 @@ module Gitlab
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
when 'Ci::Pipeline' then setup_pipeline
- else
- @relation_hash['project_id'] = @project.id
end
update_user_references
update_project_references
+ update_group_references
remove_duplicate_assignees
reset_tokens!
@@ -133,39 +150,23 @@ module Gitlab
end
def update_project_references
- project_id = @relation_hash.delete('project_id')
-
# If source and target are the same, populate them with the new project ID.
if @relation_hash['source_project_id']
- @relation_hash['source_project_id'] = same_source_and_target? ? project_id : MergeRequestParser::FORKED_PROJECT_ID
+ @relation_hash['source_project_id'] = same_source_and_target? ? @relation_hash['project_id'] : MergeRequestParser::FORKED_PROJECT_ID
end
- # project_id may not be part of the export, but we always need to populate it if required.
- @relation_hash['project_id'] = project_id
- @relation_hash['target_project_id'] = project_id if @relation_hash['target_project_id']
+ @relation_hash['target_project_id'] = @relation_hash['project_id'] if @relation_hash['target_project_id']
end
def same_source_and_target?
@relation_hash['target_project_id'] && @relation_hash['target_project_id'] == @relation_hash['source_project_id']
end
- def setup_label
- # If there's no group, move the label to a project label
- if @relation_hash['type'] == 'GroupLabel' && @relation_hash['group_id']
- @relation_hash['project_id'] = nil
- @relation_name = :group_label
- else
- @relation_hash['group_id'] = nil
- @relation_hash['type'] = 'ProjectLabel'
- end
- end
+ def update_group_references
+ return unless EXISTING_OBJECT_CHECK.include?(@relation_name)
+ return unless @relation_hash['group_id']
- def setup_milestone
- if @relation_hash['group_id']
- @relation_hash['group_id'] = @project.group.id
- else
- @relation_hash['project_id'] = @project.id
- end
+ @relation_hash['group_id'] = @project.group&.id
end
def reset_tokens!
@@ -187,7 +188,7 @@ module Gitlab
end
def relation_class
- @relation_class ||= @relation_name.to_s.classify.constantize
+ @relation_class ||= self.class.relation_class(@relation_name)
end
def imported_object
@@ -253,15 +254,7 @@ module Gitlab
end
def existing_object
- @existing_object ||=
- begin
- existing_object = find_or_create_object!
-
- # Done in two steps, as MySQL behaves differently than PostgreSQL using
- # the +find_or_create_by+ method and does not return the ID the second time.
- existing_object.update!(parsed_relation_hash)
- existing_object
- end
+ @existing_object ||= find_or_create_object!
end
def unknown_service?
@@ -270,29 +263,16 @@ module Gitlab
end
def find_or_create_object!
- 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?
- label = relation_class.find_or_initialize_by(finder_hash)
- parsed_relation_hash.delete('priorities') if label.persisted?
-
- label.save!
- label
- else
- relation_class.find_or_create_by(finder_hash)
+ return relation_class.find_or_create_by(project_id: @project.id) if @relation_name == :project_feature
+
+ # Can't use IDs as validation exists calling `group` or `project` attributes
+ finder_hash = parsed_relation_hash.tap do |hash|
+ hash['group'] = @project.group if relation_class.attribute_method?('group_id')
+ hash['project'] = @project
+ hash.delete('project_id')
end
- end
- def label?
- @relation_name.to_s.include?('label')
+ GroupProjectObjectBuilder.build(relation_class, finder_hash)
end
end
end
diff --git a/lib/gitlab/import_export/repo_saver.rb b/lib/gitlab/import_export/repo_saver.rb
index 695462c7dd2..0c224bd1971 100644
--- a/lib/gitlab/import_export/repo_saver.rb
+++ b/lib/gitlab/import_export/repo_saver.rb
@@ -26,10 +26,6 @@ module Gitlab
@shared.error(e)
false
end
-
- def path_to_repo
- @project.repository.path_to_repo
- end
end
end
end
diff --git a/lib/gitlab/import_export/saver.rb b/lib/gitlab/import_export/saver.rb
index 2daeba90a51..3cd153a4fd2 100644
--- a/lib/gitlab/import_export/saver.rb
+++ b/lib/gitlab/import_export/saver.rb
@@ -15,15 +15,22 @@ module Gitlab
def save
if compress_and_save
remove_export_path
+
Rails.logger.info("Saved project export #{archive_file}")
- archive_file
+
+ save_on_object_storage if use_object_storage?
else
- @shared.error(Gitlab::ImportExport::Error.new("Unable to save #{archive_file} into #{@shared.export_path}"))
+ @shared.error(Gitlab::ImportExport::Error.new(error_message))
false
end
rescue => e
@shared.error(e)
false
+ ensure
+ if use_object_storage?
+ remove_archive
+ remove_export_path
+ end
end
private
@@ -36,9 +43,29 @@ module Gitlab
FileUtils.rm_rf(@shared.export_path)
end
+ def remove_archive
+ FileUtils.rm_rf(@shared.archive_path)
+ end
+
def archive_file
@archive_file ||= File.join(@shared.archive_path, Gitlab::ImportExport.export_filename(project: @project))
end
+
+ def save_on_object_storage
+ upload = ImportExportUpload.find_or_initialize_by(project: @project)
+
+ File.open(archive_file) { |file| upload.export_file = file }
+
+ upload.save!
+ end
+
+ def use_object_storage?
+ Gitlab::ImportExport.object_storage?
+ end
+
+ def error_message
+ "Unable to save #{archive_file} into #{@shared.export_path}. Object storage enabled: #{use_object_storage?}"
+ end
end
end
end
diff --git a/lib/gitlab/import_export/wiki_repo_saver.rb b/lib/gitlab/import_export/wiki_repo_saver.rb
index 5fa2e101e29..2fd62c0fc7b 100644
--- a/lib/gitlab/import_export/wiki_repo_saver.rb
+++ b/lib/gitlab/import_export/wiki_repo_saver.rb
@@ -22,12 +22,8 @@ module Gitlab
"project.wiki.bundle"
end
- def path_to_repo
- @wiki.repository.path_to_repo
- end
-
def wiki_repository_exists?
- File.exist?(@wiki.repository.path_to_repo) && !@wiki.repository.empty?
+ @wiki.repository.exists? && !@wiki.repository.empty?
end
end
end
diff --git a/lib/gitlab/import_formatter.rb b/lib/gitlab/import_formatter.rb
index 3e54456e936..4e611e7f16c 100644
--- a/lib/gitlab/import_formatter.rb
+++ b/lib/gitlab/import_formatter.rb
@@ -9,6 +9,7 @@ module Gitlab
end
def author_line(author)
+ author ||= "Anonymous"
"*Created by: #{author}*\n\n"
end
end
diff --git a/lib/gitlab/kubernetes.rb b/lib/gitlab/kubernetes.rb
index da43bd0af4b..15c5ece2350 100644
--- a/lib/gitlab/kubernetes.rb
+++ b/lib/gitlab/kubernetes.rb
@@ -1,6 +1,10 @@
module Gitlab
# Helper methods to do with Kubernetes network services & resources
module Kubernetes
+ def self.build_header_hash
+ Hash.new { |h, k| h[k] = [] }
+ end
+
# This is the comand that is run to start a terminal session. Kubernetes
# expects `command=foo&command=bar, not `command[]=foo&command[]=bar`
EXEC_COMMAND = URI.encode_www_form(
@@ -37,13 +41,14 @@ module Gitlab
selectors: { pod: pod_name, container: container["name"] },
url: container_exec_url(api_url, namespace, pod_name, container["name"]),
subprotocols: ['channel.k8s.io'],
- headers: Hash.new { |h, k| h[k] = [] },
+ headers: ::Gitlab::Kubernetes.build_header_hash,
created_at: created_at
}
end
end
def add_terminal_auth(terminal, token:, max_session_time:, ca_pem: nil)
+ terminal[:headers] ||= ::Gitlab::Kubernetes.build_header_hash
terminal[:headers]['Authorization'] << "Bearer #{token}"
terminal[:max_session_time] = max_session_time
terminal[:ca_pem] = ca_pem if ca_pem.present?
diff --git a/lib/gitlab/kubernetes/helm/base_command.rb b/lib/gitlab/kubernetes/helm/base_command.rb
index 3d778da90c7..f9ebe53d6af 100644
--- a/lib/gitlab/kubernetes/helm/base_command.rb
+++ b/lib/gitlab/kubernetes/helm/base_command.rb
@@ -18,7 +18,7 @@ module Gitlab
ALPINE_VERSION=$(cat /etc/alpine-release | cut -d '.' -f 1,2)
echo http://mirror.clarkson.edu/alpine/v$ALPINE_VERSION/main >> /etc/apk/repositories
echo http://mirror1.hs-esslingen.de/pub/Mirrors/alpine/v$ALPINE_VERSION/main >> /etc/apk/repositories
- apk add -U ca-certificates openssl >/dev/null
+ apk add -U wget ca-certificates openssl >/dev/null
wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v#{Gitlab::Kubernetes::Helm::HELM_VERSION}-linux-amd64.tar.gz | tar zxC /tmp >/dev/null
mv /tmp/linux-amd64/helm /usr/bin/
HEREDOC
diff --git a/lib/gitlab/kubernetes/helm/install_command.rb b/lib/gitlab/kubernetes/helm/install_command.rb
index 30af3e97b4a..d2133a6d65b 100644
--- a/lib/gitlab/kubernetes/helm/install_command.rb
+++ b/lib/gitlab/kubernetes/helm/install_command.rb
@@ -2,11 +2,12 @@ module Gitlab
module Kubernetes
module Helm
class InstallCommand < BaseCommand
- attr_reader :name, :chart, :repository, :values
+ attr_reader :name, :chart, :version, :repository, :values
- def initialize(name, chart:, values:, repository: nil)
+ def initialize(name, chart:, values:, version: nil, repository: nil)
@name = name
@chart = chart
+ @version = version
@values = values
@repository = repository
end
@@ -39,9 +40,13 @@ module Gitlab
def script_command
<<~HEREDOC
- helm install #{chart} --name #{name} --namespace #{Gitlab::Kubernetes::Helm::NAMESPACE} -f /data/helm/#{name}/config/values.yaml >/dev/null
+ helm install #{chart} --name #{name}#{optional_version_flag} --namespace #{Gitlab::Kubernetes::Helm::NAMESPACE} -f /data/helm/#{name}/config/values.yaml >/dev/null
HEREDOC
end
+
+ def optional_version_flag
+ " --version #{version}" if version
+ end
end
end
end
diff --git a/lib/gitlab/legacy_github_import/project_creator.rb b/lib/gitlab/legacy_github_import/project_creator.rb
index 3ce245a8050..5e96eb16754 100644
--- a/lib/gitlab/legacy_github_import/project_creator.rb
+++ b/lib/gitlab/legacy_github_import/project_creator.rb
@@ -35,7 +35,10 @@ module Gitlab
end
def visibility_level
- repo.private ? Gitlab::VisibilityLevel::PRIVATE : Gitlab::CurrentSettings.default_project_visibility
+ visibility_level = repo.private ? Gitlab::VisibilityLevel::PRIVATE : Gitlab::VisibilityLevel::PUBLIC
+ visibility_level = Gitlab::CurrentSettings.default_project_visibility if Gitlab::CurrentSettings.restricted_visibility_levels.include?(visibility_level)
+
+ visibility_level
end
#
diff --git a/lib/gitlab/mail_room.rb b/lib/gitlab/mail_room.rb
index 344784c866f..db04356a5e9 100644
--- a/lib/gitlab/mail_room.rb
+++ b/lib/gitlab/mail_room.rb
@@ -53,7 +53,7 @@ module Gitlab
end
def config_file
- ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] || File.expand_path('../../../config/gitlab.yml', __FILE__)
+ ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] || File.expand_path('../../config/gitlab.yml', __dir__)
end
end
end
diff --git a/lib/gitlab/metrics/influx_db.rb b/lib/gitlab/metrics/influx_db.rb
index 66f30e3b397..04135dac4ff 100644
--- a/lib/gitlab/metrics/influx_db.rb
+++ b/lib/gitlab/metrics/influx_db.rb
@@ -162,7 +162,6 @@ module Gitlab
# When enabled this should be set before being used as the usual pattern
# "@foo ||= bar" is _not_ thread-safe.
- # rubocop:disable Gitlab/ModuleWithInstanceVariables
def pool
if influx_metrics_enabled?
if @pool.nil?
@@ -180,7 +179,6 @@ module Gitlab
@pool
end
end
- # rubocop:enable Gitlab/ModuleWithInstanceVariables
end
end
end
diff --git a/lib/gitlab/metrics/method_call.rb b/lib/gitlab/metrics/method_call.rb
index b11520a79bb..f3290e3149c 100644
--- a/lib/gitlab/metrics/method_call.rb
+++ b/lib/gitlab/metrics/method_call.rb
@@ -1,5 +1,3 @@
-# rubocop:disable Style/ClassVars
-
module Gitlab
module Metrics
# Class for tracking timing information about method calls
diff --git a/lib/gitlab/metrics/samplers/influx_sampler.rb b/lib/gitlab/metrics/samplers/influx_sampler.rb
index 5a0f7f28fc8..ad97632e4eb 100644
--- a/lib/gitlab/metrics/samplers/influx_sampler.rb
+++ b/lib/gitlab/metrics/samplers/influx_sampler.rb
@@ -16,12 +16,6 @@ module Gitlab
@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
diff --git a/lib/gitlab/metrics/samplers/ruby_sampler.rb b/lib/gitlab/metrics/samplers/ruby_sampler.rb
index 4e1ea62351f..7b2b3bedf04 100644
--- a/lib/gitlab/metrics/samplers/ruby_sampler.rb
+++ b/lib/gitlab/metrics/samplers/ruby_sampler.rb
@@ -20,39 +20,29 @@ module Gitlab
{}
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', { worker: nil })
- metrics[:total_time] = Metrics.gauge(with_prefix(:gc, :time_total), 'Total GC time', labels, :livesum)
+ metrics[:sampler_duration] = Metrics.counter(with_prefix(:sampler, :duration_seconds_total), 'Sampler time', labels)
+ metrics[:total_time] = Metrics.counter(with_prefix(:gc, :duration_seconds_total), 'Total GC time', labels)
GC.stat.keys.each do |key|
- metrics[key] = Metrics.gauge(with_prefix(:gc, key), to_doc_string(key), labels, :livesum)
+ metrics[key] = Metrics.gauge(with_prefix(:gc_stat, 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[:memory_usage] = Metrics.gauge(with_prefix(:memory, :bytes), 'Memory used', labels, :livesum)
+ metrics[:file_descriptors] = Metrics.gauge(with_prefix(:file, :descriptors), 'File descriptors used', labels, :livesum)
metrics
end
def sample
start_time = System.monotonic_time
- sample_gc
- metrics[:memory_usage].set(labels, System.memory_usage)
- metrics[:file_descriptors].set(labels, System.file_descriptor_count)
+ metrics[:memory_usage].set(labels.merge(worker_label), System.memory_usage)
+ metrics[:file_descriptors].set(labels.merge(worker_label), System.file_descriptor_count)
+
+ sample_gc
- metrics[:sampler_duration].observe(labels.merge(worker_label), System.monotonic_time - start_time)
+ metrics[:sampler_duration].increment(labels, System.monotonic_time - start_time)
ensure
GC::Profiler.clear
end
@@ -60,11 +50,13 @@ module Gitlab
private
def sample_gc
- metrics[:total_time].set(labels, GC::Profiler.total_time * 1000)
-
+ # Collect generic GC stats.
GC.stat.each do |key, value|
metrics[key].set(labels, value)
end
+
+ # Collect the GC time since last sample in float seconds.
+ metrics[:total_time].increment(labels, GC::Profiler.total_time)
end
def worker_label
diff --git a/lib/gitlab/metrics/subscribers/active_record.rb b/lib/gitlab/metrics/subscribers/active_record.rb
index 4b3e8d0a6a0..38f119cf06d 100644
--- a/lib/gitlab/metrics/subscribers/active_record.rb
+++ b/lib/gitlab/metrics/subscribers/active_record.rb
@@ -20,7 +20,7 @@ module Gitlab
define_histogram :gitlab_sql_duration_seconds do
docstring 'SQL time'
base_labels Transaction::BASE_LABELS
- buckets [0.001, 0.01, 0.1, 1.0, 10.0]
+ buckets [0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0]
end
def current_transaction
diff --git a/lib/gitlab/metrics/transaction.rb b/lib/gitlab/metrics/transaction.rb
index f3e48083c19..9f903e96585 100644
--- a/lib/gitlab/metrics/transaction.rb
+++ b/lib/gitlab/metrics/transaction.rb
@@ -140,7 +140,7 @@ module Gitlab
define_histogram :gitlab_transaction_duration_seconds do
docstring 'Transaction duration'
base_labels BASE_LABELS
- buckets [0.001, 0.01, 0.1, 1.0, 10.0]
+ buckets [0.1, 0.25, 0.5, 1.0, 2.5, 5.0]
end
define_histogram :gitlab_transaction_allocated_memory_bytes do
diff --git a/lib/gitlab/metrics/web_transaction.rb b/lib/gitlab/metrics/web_transaction.rb
index 3799aaebf1c..723ca576aab 100644
--- a/lib/gitlab/metrics/web_transaction.rb
+++ b/lib/gitlab/metrics/web_transaction.rb
@@ -3,6 +3,7 @@ module Gitlab
class WebTransaction < Transaction
CONTROLLER_KEY = 'action_controller.instance'.freeze
ENDPOINT_KEY = 'api.endpoint'.freeze
+ ALLOWED_SUFFIXES = Set.new(%w[json js atom rss xml zip])
def initialize(env)
super()
@@ -32,9 +33,13 @@ module Gitlab
# Devise exposes a method called "request_format" that does the below.
# However, this method is not available to all controllers (e.g. certain
# Doorkeeper controllers). As such we use the underlying code directly.
- suffix = controller.request.format.try(:ref)
+ suffix = controller.request.format.try(:ref).to_s
- if suffix && suffix != :html
+ # Sometimes the request format is set to silly data such as
+ # "application/xrds+xml" or actual URLs. To prevent such values from
+ # increasing the cardinality of our metrics, we limit the number of
+ # possible suffixes.
+ if suffix && ALLOWED_SUFFIXES.include?(suffix)
action += ".#{suffix}"
end
diff --git a/lib/gitlab/middleware/multipart.rb b/lib/gitlab/middleware/multipart.rb
index a5f5d719cc1..18f91db98fc 100644
--- a/lib/gitlab/middleware/multipart.rb
+++ b/lib/gitlab/middleware/multipart.rb
@@ -42,10 +42,10 @@ module Gitlab
key, value = parsed_field.first
if value.nil?
- value = open_file(tmp_path, @request.params["#{key}.name"])
+ value = open_file(@request.params, key)
@open_files << value
else
- value = decorate_params_value(value, @request.params[key], tmp_path)
+ value = decorate_params_value(value, @request.params[key])
end
@request.update_param(key, value)
@@ -57,7 +57,7 @@ module Gitlab
end
# This function calls itself recursively
- def decorate_params_value(path_hash, value_hash, tmp_path)
+ def decorate_params_value(path_hash, value_hash)
unless path_hash.is_a?(Hash) && path_hash.count == 1
raise "invalid path: #{path_hash.inspect}"
end
@@ -70,19 +70,21 @@ module Gitlab
case path_value
when nil
- value_hash[path_key] = open_file(tmp_path, value_hash.dig(path_key, '.name'))
+ value_hash[path_key] = open_file(value_hash.dig(path_key), '')
@open_files << value_hash[path_key]
value_hash
when Hash
- decorate_params_value(path_value, value_hash[path_key], tmp_path)
+ decorate_params_value(path_value, value_hash[path_key])
value_hash
else
raise "unexpected path value: #{path_value.inspect}"
end
end
- def open_file(path, name)
- ::UploadedFile.new(path, filename: name || File.basename(path), content_type: 'application/octet-stream')
+ def open_file(params, key)
+ ::UploadedFile.from_params(
+ params, key,
+ [FileUploader.root, Gitlab.config.uploads.storage_path])
end
end
diff --git a/lib/gitlab/middleware/read_only/controller.rb b/lib/gitlab/middleware/read_only/controller.rb
index 45b644e6510..8dca431c005 100644
--- a/lib/gitlab/middleware/read_only/controller.rb
+++ b/lib/gitlab/middleware/read_only/controller.rb
@@ -4,8 +4,18 @@ module Gitlab
class Controller
DISALLOWED_METHODS = %w(POST PATCH PUT DELETE).freeze
APPLICATION_JSON = 'application/json'.freeze
+ APPLICATION_JSON_TYPES = %W{#{APPLICATION_JSON} application/vnd.git-lfs+json}.freeze
ERROR_MESSAGE = 'You cannot perform write operations on a read-only instance'.freeze
+ WHITELISTED_GIT_ROUTES = {
+ 'projects/git_http' => %w{git_upload_pack git_receive_pack}
+ }.freeze
+
+ WHITELISTED_GIT_LFS_ROUTES = {
+ 'projects/lfs_api' => %w{batch},
+ 'projects/lfs_locks_api' => %w{verify create unlock}
+ }.freeze
+
def initialize(app, env)
@app = app
@env = env
@@ -36,7 +46,7 @@ module Gitlab
end
def json_request?
- request.media_type == APPLICATION_JSON
+ APPLICATION_JSON_TYPES.include?(request.media_type)
end
def rack_flash
@@ -59,26 +69,32 @@ module Gitlab
@route_hash ||= Rails.application.routes.recognize_path(request.url, { method: request.request_method }) rescue {}
end
+ # Overridden in EE module
def whitelisted_routes
grack_route || ReadOnly.internal_routes.any? { |path| request.path.include?(path) } || lfs_route || sidekiq_route
end
- def sidekiq_route
- request.path.start_with?('/admin/sidekiq')
- end
-
def grack_route
# 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')
+ return false unless
+ request.path.end_with?('.git/git-upload-pack', '.git/git-receive-pack')
- route_hash[:controller] == 'projects/git_http' && route_hash[:action] == 'git_upload_pack'
+ WHITELISTED_GIT_ROUTES[route_hash[:controller]]&.include?(route_hash[:action])
end
def lfs_route
# 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')
+ unless request.path.end_with?('/info/lfs/objects/batch',
+ '/info/lfs/locks', '/info/lfs/locks/verify') ||
+ %r{/info/lfs/locks/\d+/unlock\z}.match?(request.path)
+ return false
+ end
+
+ WHITELISTED_GIT_LFS_ROUTES[route_hash[:controller]]&.include?(route_hash[:action])
+ end
- route_hash[:controller] == 'projects/lfs_api' && route_hash[:action] == 'batch'
+ def sidekiq_route
+ request.path.start_with?('/admin/sidekiq')
end
end
end
diff --git a/lib/gitlab/omniauth_initializer.rb b/lib/gitlab/omniauth_initializer.rb
index 35ed3a5ac05..a71acda8701 100644
--- a/lib/gitlab/omniauth_initializer.rb
+++ b/lib/gitlab/omniauth_initializer.rb
@@ -1,5 +1,10 @@
module Gitlab
class OmniauthInitializer
+ def self.enabled?
+ Gitlab.config.omniauth.enabled ||
+ Gitlab.config.omniauth.auto_sign_in_with_provider.present?
+ end
+
def initialize(devise_config)
@devise_config = devise_config
end
diff --git a/lib/gitlab/path_regex.rb b/lib/gitlab/path_regex.rb
index 4dc38aae61e..61653044433 100644
--- a/lib/gitlab/path_regex.rb
+++ b/lib/gitlab/path_regex.rb
@@ -31,6 +31,7 @@ module Gitlab
deploy.html
explore
favicon.ico
+ favicon.png
files
groups
health_check
diff --git a/lib/gitlab/profiler.rb b/lib/gitlab/profiler.rb
index 18540e64d4c..ecff6ab5d5e 100644
--- a/lib/gitlab/profiler.rb
+++ b/lib/gitlab/profiler.rb
@@ -11,6 +11,7 @@ module Gitlab
lib/gitlab/etag_caching/
lib/gitlab/metrics/
lib/gitlab/middleware/
+ ee/lib/gitlab/middleware/
lib/gitlab/performance_bar/
lib/gitlab/request_profiler/
lib/gitlab/profiler.rb
@@ -98,11 +99,7 @@ module Gitlab
super
- backtrace = Rails.backtrace_cleaner.clean(caller)
-
- backtrace.each do |caller_line|
- next if caller_line.match(Regexp.union(IGNORE_BACKTRACES))
-
+ Gitlab::Profiler.clean_backtrace(caller).each do |caller_line|
stripped_caller_line = caller_line.sub("#{Rails.root}/", '')
super(" ↳ #{stripped_caller_line}")
@@ -112,6 +109,12 @@ module Gitlab
end
end
+ def self.clean_backtrace(backtrace)
+ Array(Rails.backtrace_cleaner.clean(backtrace)).reject do |line|
+ line.match(Regexp.union(IGNORE_BACKTRACES))
+ end
+ end
+
def self.with_custom_logger(logger)
original_colorize_logging = ActiveSupport::LogSubscriber.colorize_logging
original_activerecord_logger = ActiveRecord::Base.logger
diff --git a/lib/gitlab/project_authorizations/with_nested_groups.rb b/lib/gitlab/project_authorizations/with_nested_groups.rb
index 15b8beacf60..e3da1634fa5 100644
--- a/lib/gitlab/project_authorizations/with_nested_groups.rb
+++ b/lib/gitlab/project_authorizations/with_nested_groups.rb
@@ -24,7 +24,7 @@ module Gitlab
user.projects.select_for_project_authorization,
# The personal projects of the user.
- user.personal_projects.select_as_master_for_project_authorization,
+ user.personal_projects.select_as_maintainer_for_project_authorization,
# Projects that belong directly to any of the groups the user has
# access to.
diff --git a/lib/gitlab/project_authorizations/without_nested_groups.rb b/lib/gitlab/project_authorizations/without_nested_groups.rb
index ad87540e6c2..7d0c00c7f36 100644
--- a/lib/gitlab/project_authorizations/without_nested_groups.rb
+++ b/lib/gitlab/project_authorizations/without_nested_groups.rb
@@ -15,7 +15,7 @@ module Gitlab
user.projects.select_for_project_authorization,
# Personal projects
- user.personal_projects.select_as_master_for_project_authorization,
+ user.personal_projects.select_as_maintainer_for_project_authorization,
# Projects of groups the user is a member of
user.groups_projects.select_for_project_authorization,
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index 2e9b6e302f5..38bdc61d8ab 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -106,7 +106,8 @@ module Gitlab
project_wiki = ProjectWiki.new(project)
unless project_wiki.empty?
- project_wiki.search_files(query)
+ ref = repository_ref || project.wiki.default_branch
+ Gitlab::WikiFileFinder.new(project, ref).find(query)
else
[]
end
diff --git a/lib/gitlab/query_limiting/active_support_subscriber.rb b/lib/gitlab/query_limiting/active_support_subscriber.rb
index 4c83581c4b1..3c4ff5d1928 100644
--- a/lib/gitlab/query_limiting/active_support_subscriber.rb
+++ b/lib/gitlab/query_limiting/active_support_subscriber.rb
@@ -4,7 +4,7 @@ module Gitlab
attach_to :active_record
def sql(event)
- unless event.payload[:name] == 'CACHE'
+ unless event.payload.fetch(:cached, event.payload[:name] == 'CACHE')
Transaction.current&.increment
end
end
diff --git a/lib/gitlab/quick_actions/extractor.rb b/lib/gitlab/quick_actions/extractor.rb
index 075ff91700c..30c6806b68e 100644
--- a/lib/gitlab/quick_actions/extractor.rb
+++ b/lib/gitlab/quick_actions/extractor.rb
@@ -39,7 +39,7 @@ module Gitlab
content.delete!("\r")
content.gsub!(commands_regex) do
if $~[:cmd]
- commands << [$~[:cmd], $~[:arg]].reject(&:blank?)
+ commands << [$~[:cmd].downcase, $~[:arg]].reject(&:blank?)
''
else
$~[0]
@@ -102,14 +102,14 @@ module Gitlab
# /close
^\/
- (?<cmd>#{Regexp.union(names)})
+ (?<cmd>#{Regexp.new(Regexp.union(names).source, Regexp::IGNORECASE)})
(?:
[ ]
(?<arg>[^\n]*)
)?
(?:\n|$)
)
- }mx
+ }mix
end
def perform_substitutions(content, commands)
@@ -120,7 +120,7 @@ module Gitlab
end
substitution_definitions.each do |substitution|
- match_data = substitution.match(content)
+ match_data = substitution.match(content.downcase)
if match_data
command = [substitution.name.to_s]
command << match_data[1] unless match_data[1].empty?
diff --git a/lib/gitlab/quick_actions/substitution_definition.rb b/lib/gitlab/quick_actions/substitution_definition.rb
index 032c49ed159..688056e5d73 100644
--- a/lib/gitlab/quick_actions/substitution_definition.rb
+++ b/lib/gitlab/quick_actions/substitution_definition.rb
@@ -15,7 +15,7 @@ module Gitlab
return unless content
all_names.each do |a_name|
- content.gsub!(%r{/#{a_name} ?(.*)$}, execute_block(action_block, context, '\1'))
+ content.gsub!(%r{/#{a_name} ?(.*)$}i, execute_block(action_block, context, '\1'))
end
content
end
diff --git a/lib/gitlab/repository_cache_adapter.rb b/lib/gitlab/repository_cache_adapter.rb
index 7f64a8c9e46..2ec871f0754 100644
--- a/lib/gitlab/repository_cache_adapter.rb
+++ b/lib/gitlab/repository_cache_adapter.rb
@@ -25,6 +25,11 @@ module Gitlab
raise NotImplementedError
end
+ # List of cached methods. Should be overridden by the including class
+ def cached_methods
+ raise NotImplementedError
+ end
+
# Caches the supplied block both in a cache and in an instance variable.
#
# The cache key and instance variable are named the same way as the value of
@@ -67,6 +72,11 @@ module Gitlab
# Expires the caches of a specific set of methods
def expire_method_caches(methods)
methods.each do |key|
+ unless cached_methods.include?(key.to_sym)
+ Rails.logger.error "Requested to expire non-existent method '#{key}' for Repository"
+ next
+ end
+
cache.expire(key)
ivar = cache_instance_variable_name(key)
diff --git a/lib/gitlab/request_forgery_protection.rb b/lib/gitlab/request_forgery_protection.rb
index ccfe0d6bed3..a502ad8a541 100644
--- a/lib/gitlab/request_forgery_protection.rb
+++ b/lib/gitlab/request_forgery_protection.rb
@@ -5,7 +5,7 @@
module Gitlab
module RequestForgeryProtection
class Controller < ActionController::Base
- protect_from_forgery with: :exception
+ protect_from_forgery with: :exception, prepend: true
rescue_from ActionController::InvalidAuthenticityToken do |e|
logger.warn "This CSRF token verification failure is handled internally by `GitLab::RequestForgeryProtection`"
diff --git a/lib/gitlab/search/parsed_query.rb b/lib/gitlab/search/parsed_query.rb
new file mode 100644
index 00000000000..23595f23f01
--- /dev/null
+++ b/lib/gitlab/search/parsed_query.rb
@@ -0,0 +1,23 @@
+module Gitlab
+ module Search
+ class ParsedQuery
+ attr_reader :term, :filters
+
+ def initialize(term, filters)
+ @term = term
+ @filters = filters
+ end
+
+ def filter_results(results)
+ filters = @filters.reject { |filter| filter[:matcher].nil? }
+ return unless filters
+
+ results.select do |result|
+ filters.all? do |filter|
+ filter[:matcher].call(filter, result)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/search/query.rb b/lib/gitlab/search/query.rb
new file mode 100644
index 00000000000..8583bce7792
--- /dev/null
+++ b/lib/gitlab/search/query.rb
@@ -0,0 +1,55 @@
+module Gitlab
+ module Search
+ class Query < SimpleDelegator
+ def initialize(query, filter_opts = {}, &block)
+ @raw_query = query.dup
+ @filters = []
+ @filter_options = { default_parser: :downcase.to_proc }.merge(filter_opts)
+
+ self.instance_eval(&block) if block_given?
+
+ @query = Gitlab::Search::ParsedQuery.new(*extract_filters)
+ # set the ParsedQuery as our default delegator thanks to SimpleDelegator
+ super(@query)
+ end
+
+ private
+
+ def filter(name, **attributes)
+ filter = { parser: @filter_options[:default_parser], name: name }.merge(attributes)
+
+ @filters << filter
+ end
+
+ def filter_options(**options)
+ @filter_options.merge!(options)
+ end
+
+ def extract_filters
+ fragments = []
+
+ filters = @filters.each_with_object([]) do |filter, parsed_filters|
+ match = @raw_query.split.find { |part| part =~ /\A#{filter[:name]}:/ }
+ next unless match
+
+ input = match.split(':')[1..-1].join
+ next if input.empty?
+
+ filter[:value] = parse_filter(filter, input)
+ filter[:regex_value] = Regexp.escape(filter[:value]).gsub('\*', '.*?')
+ fragments << match
+
+ parsed_filters << filter
+ end
+
+ query = (@raw_query.split - fragments).join(' ')
+
+ [query, filters]
+ end
+
+ def parse_filter(filter, input)
+ filter[:parser].call(input)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/setup_helper.rb b/lib/gitlab/setup_helper.rb
index e5c02dd8ecc..b2d75aac1d0 100644
--- a/lib/gitlab/setup_helper.rb
+++ b/lib/gitlab/setup_helper.rb
@@ -24,7 +24,10 @@ module Gitlab
address = val['gitaly_address']
end
- storages << { name: key, path: val.legacy_disk_path }
+ # https://gitlab.com/gitlab-org/gitaly/issues/1238
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ storages << { name: key, path: val.legacy_disk_path }
+ end
end
if Rails.env.test?
diff --git a/lib/gitlab/shard_health_cache.rb b/lib/gitlab/shard_health_cache.rb
new file mode 100644
index 00000000000..3f03f46d4b1
--- /dev/null
+++ b/lib/gitlab/shard_health_cache.rb
@@ -0,0 +1,41 @@
+module Gitlab
+ class ShardHealthCache
+ HEALTHY_SHARDS_KEY = 'gitlab-healthy-shards'.freeze
+ HEALTHY_SHARDS_TIMEOUT = 300
+
+ # Clears the Redis set storing the list of healthy shards
+ def self.clear
+ Gitlab::Redis::Cache.with { |redis| redis.del(HEALTHY_SHARDS_KEY) }
+ end
+
+ # Updates the list of healthy shards using a Redis set
+ #
+ # shards - An array of shard names to store
+ def self.update(shards)
+ Gitlab::Redis::Cache.with do |redis|
+ redis.multi do |m|
+ m.del(HEALTHY_SHARDS_KEY)
+ shards.each { |shard_name| m.sadd(HEALTHY_SHARDS_KEY, shard_name) }
+ m.expire(HEALTHY_SHARDS_KEY, HEALTHY_SHARDS_TIMEOUT)
+ end
+ end
+ end
+
+ # Returns an array of strings of healthy shards
+ def self.cached_healthy_shards
+ Gitlab::Redis::Cache.with { |redis| redis.smembers(HEALTHY_SHARDS_KEY) }
+ end
+
+ # Checks whether the given shard name is in the list of healthy shards.
+ #
+ # shard_name - The string to check
+ def self.healthy_shard?(shard_name)
+ Gitlab::Redis::Cache.with { |redis| redis.sismember(HEALTHY_SHARDS_KEY, shard_name) }
+ end
+
+ # Returns the number of healthy shards in the Redis set
+ def self.healthy_shard_count
+ Gitlab::Redis::Cache.with { |redis| redis.scard(HEALTHY_SHARDS_KEY) }
+ end
+ end
+end
diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb
index 4a691d640b3..a17cd27e82d 100644
--- a/lib/gitlab/shell.rb
+++ b/lib/gitlab/shell.rb
@@ -1,5 +1,4 @@
-# Gitaly note: JV: two sets of straightforward RPC's. 1 Hard RPC: fork_repository.
-# SSH key operations are not part of Gitaly so will never be migrated.
+# Gitaly note: SSH key operations are not part of Gitaly so will never be migrated.
require 'securerandom'
@@ -75,17 +74,10 @@ module Gitlab
relative_path = name.dup
relative_path << '.git' unless relative_path.end_with?('.git')
- gitaly_migrate(:create_repository,
- status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
- if is_enabled
- repository = Gitlab::Git::Repository.new(storage, relative_path, '')
- repository.gitaly_repository_client.create_repository
- true
- else
- repo_path = File.join(Gitlab.config.repositories.storages[storage].legacy_disk_path, relative_path)
- Gitlab::Git::Repository.create(repo_path, bare: true, symlink_hooks_to: gitlab_shell_hooks_path)
- end
- end
+ repository = Gitlab::Git::Repository.new(storage, relative_path, '')
+ wrapped_gitaly_errors { repository.gitaly_repository_client.create_repository }
+
+ true
rescue => err # Once the Rugged codes gets removes this can be improved
Rails.logger.error("Failed to add repository #{storage}/#{name}: #{err}")
false
@@ -100,16 +92,15 @@ module Gitlab
# Ex.
# import_repository("nfs-file06", "gitlab/gitlab-ci", "https://gitlab.com/gitlab-org/gitlab-test.git")
#
- # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/874
def import_repository(storage, name, url)
if url.start_with?('.', '/')
raise Error.new("don't use disk paths with import_repository: #{url.inspect}")
end
- # The timeout ensures the subprocess won't hang forever
- cmd = gitlab_projects(storage, "#{name}.git")
- success = cmd.import_project(url, git_timeout)
+ relative_path = "#{name}.git"
+ cmd = GitalyGitlabProjects.new(storage, relative_path)
+ success = cmd.import_project(url, git_timeout)
raise Error, cmd.output unless success
success
@@ -127,12 +118,8 @@ module Gitlab
# fetch_remote(my_repo, "upstream")
#
def fetch_remote(repository, remote, ssh_auth: nil, forced: false, no_tags: false, prune: true)
- gitaly_migrate(:fetch_remote) do |is_enabled|
- if is_enabled
- repository.gitaly_repository_client.fetch_remote(remote, ssh_auth: ssh_auth, forced: forced, no_tags: no_tags, timeout: git_timeout, prune: prune)
- else
- local_fetch_remote(repository.storage, repository.relative_path, remote, ssh_auth: ssh_auth, forced: forced, no_tags: no_tags, prune: prune)
- end
+ wrapped_gitaly_errors do
+ repository.gitaly_repository_client.fetch_remote(remote, ssh_auth: ssh_auth, forced: forced, no_tags: no_tags, timeout: git_timeout, prune: prune)
end
end
@@ -146,8 +133,6 @@ module Gitlab
#
# Ex.
# mv_repository("/path/to/storage", "gitlab/gitlab-ci", "randx/gitlab-ci-new")
- #
- # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/873
def mv_repository(storage, path, new_path)
return false if path.empty? || new_path.empty?
@@ -162,11 +147,11 @@ module Gitlab
#
# Ex.
# fork_repository("nfs-file06", "gitlab/gitlab-ci", "nfs-file07", "new-namespace/gitlab-ci")
- #
- # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/817
def fork_repository(forked_from_storage, forked_from_disk_path, forked_to_storage, forked_to_disk_path)
- gitlab_projects(forked_from_storage, "#{forked_from_disk_path}.git")
- .fork_repository(forked_to_storage, "#{forked_to_disk_path}.git")
+ forked_from_relative_path = "#{forked_from_disk_path}.git"
+ fork_args = [forked_to_storage, "#{forked_to_disk_path}.git"]
+
+ GitalyGitlabProjects.new(forked_from_storage, forked_from_relative_path).fork_repository(*fork_args)
end
# Removes a repository from file system, using rm_diretory which is an alias
@@ -178,8 +163,6 @@ module Gitlab
#
# Ex.
# remove_repository("/path/to/storage", "gitlab/gitlab-ci")
- #
- # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/873
def remove_repository(storage, name)
return false if name.empty?
@@ -394,28 +377,6 @@ module Gitlab
)
end
- def local_fetch_remote(storage_name, repository_relative_path, remote, ssh_auth: nil, forced: false, no_tags: false, prune: true)
- vars = { force: forced, tags: !no_tags, prune: prune }
-
- if ssh_auth&.ssh_import?
- if ssh_auth.ssh_key_auth? && ssh_auth.ssh_private_key.present?
- vars[:ssh_key] = ssh_auth.ssh_private_key
- end
-
- if ssh_auth.ssh_known_hosts.present?
- vars[:known_hosts] = ssh_auth.ssh_known_hosts
- end
- end
-
- cmd = gitlab_projects(storage_name, repository_relative_path)
-
- success = cmd.fetch_remote(remote, git_timeout, vars)
-
- raise Error, cmd.output unless success
-
- success
- end
-
def gitlab_shell_fast_execute(cmd)
output, status = gitlab_shell_fast_execute_helper(cmd)
@@ -445,12 +406,46 @@ module Gitlab
Gitlab.config.gitlab_shell.git_timeout
end
- def gitaly_migrate(method, status: Gitlab::GitalyClient::MigrationStatus::OPT_IN, &block)
- Gitlab::GitalyClient.migrate(method, status: status, &block)
+ def wrapped_gitaly_errors
+ yield
rescue GRPC::NotFound, GRPC::BadStatus => e
# Old Popen code returns [Error, output] to the caller, so we
# need to do the same here...
raise Error, e
end
+
+ class GitalyGitlabProjects
+ attr_reader :shard_name, :repository_relative_path, :output
+
+ def initialize(shard_name, repository_relative_path)
+ @shard_name = shard_name
+ @repository_relative_path = repository_relative_path
+ @output = ''
+ end
+
+ def import_project(source, _timeout)
+ raw_repository = Gitlab::Git::Repository.new(shard_name, repository_relative_path, nil)
+
+ Gitlab::GitalyClient::RepositoryService.new(raw_repository).import_repository(source)
+ true
+ rescue GRPC::BadStatus => e
+ @output = e.message
+ false
+ end
+
+ def fork_repository(new_shard_name, new_repository_relative_path)
+ target_repository = Gitlab::Git::Repository.new(new_shard_name, new_repository_relative_path, nil)
+ raw_repository = Gitlab::Git::Repository.new(shard_name, repository_relative_path, nil)
+
+ Gitlab::GitalyClient::RepositoryService.new(target_repository).fork_repository(raw_repository)
+ rescue GRPC::BadStatus => e
+ logger.error "fork-repository failed: #{e.message}"
+ false
+ end
+
+ def logger
+ Rails.logger
+ end
+ end
end
end
diff --git a/lib/gitlab/slash_commands/command.rb b/lib/gitlab/slash_commands/command.rb
index bb778f37096..c82320a6036 100644
--- a/lib/gitlab/slash_commands/command.rb
+++ b/lib/gitlab/slash_commands/command.rb
@@ -1,13 +1,15 @@
module Gitlab
module SlashCommands
class Command < BaseCommand
- COMMANDS = [
- Gitlab::SlashCommands::IssueShow,
- Gitlab::SlashCommands::IssueNew,
- Gitlab::SlashCommands::IssueSearch,
- Gitlab::SlashCommands::IssueMove,
- Gitlab::SlashCommands::Deploy
- ].freeze
+ def self.commands
+ [
+ Gitlab::SlashCommands::IssueShow,
+ Gitlab::SlashCommands::IssueNew,
+ Gitlab::SlashCommands::IssueSearch,
+ Gitlab::SlashCommands::IssueMove,
+ Gitlab::SlashCommands::Deploy
+ ]
+ end
def execute
command, match = match_command
@@ -37,7 +39,7 @@ module Gitlab
private
def available_commands
- COMMANDS.select do |klass|
+ self.class.commands.keep_if do |klass|
klass.available?(project)
end
end
diff --git a/lib/gitlab/slash_commands/presenters/access.rb b/lib/gitlab/slash_commands/presenters/access.rb
index 1a817eb735b..81f7cd3ffe8 100644
--- a/lib/gitlab/slash_commands/presenters/access.rb
+++ b/lib/gitlab/slash_commands/presenters/access.rb
@@ -15,7 +15,7 @@ module Gitlab
if @resource
":wave: Hi there! Before I do anything for you, please [connect your GitLab account](#{@resource})."
else
- ":sweat_smile: Couldn't identify you, nor can I autorize you!"
+ ":sweat_smile: Couldn't identify you, nor can I authorize you!"
end
ephemeral_response(text: message)
diff --git a/lib/gitlab/sql/cte.rb b/lib/gitlab/sql/cte.rb
new file mode 100644
index 00000000000..f357829ba3f
--- /dev/null
+++ b/lib/gitlab/sql/cte.rb
@@ -0,0 +1,50 @@
+module Gitlab
+ module SQL
+ # Class for easily building CTE statements.
+ #
+ # Example:
+ #
+ # cte = CTE.new(:my_cte_name)
+ # ns = Arel::Table.new(:namespaces)
+ #
+ # cte << Namespace.
+ # where(ns[:parent_id].eq(some_namespace_id))
+ #
+ # Namespace
+ # with(cte.to_arel).
+ # from(cte.alias_to(ns))
+ class CTE
+ attr_reader :table, :query
+
+ # name - The name of the CTE as a String or Symbol.
+ def initialize(name, query)
+ @table = Arel::Table.new(name)
+ @query = query
+ end
+
+ # Returns the Arel relation for this CTE.
+ def to_arel
+ sql = Arel::Nodes::SqlLiteral.new("(#{query.to_sql})")
+
+ Arel::Nodes::As.new(table, sql)
+ end
+
+ # Returns an "AS" statement that aliases the CTE name as the given table
+ # name. This allows one to trick ActiveRecord into thinking it's selecting
+ # from an actual table, when in reality it's selecting from a CTE.
+ #
+ # alias_table - The Arel table to use as the alias.
+ def alias_to(alias_table)
+ Arel::Nodes::As.new(table, alias_table)
+ end
+
+ # Applies the CTE to the given relation, returning a new one that will
+ # query from it.
+ def apply_to(relation)
+ relation.except(:where)
+ .with(to_arel)
+ .from(alias_to(relation.model.arel_table))
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/task_helpers.rb b/lib/gitlab/task_helpers.rb
index 42be301fd9b..922418966e9 100644
--- a/lib/gitlab/task_helpers.rb
+++ b/lib/gitlab/task_helpers.rb
@@ -128,17 +128,21 @@ module Gitlab
end
def all_repos
- Gitlab.config.repositories.storages.each_value do |repository_storage|
- IO.popen(%W(find #{repository_storage.legacy_disk_path} -mindepth 2 -type d -name *.git)) do |find|
- find.each_line do |path|
- yield path.chomp
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ Gitlab.config.repositories.storages.each_value do |repository_storage|
+ IO.popen(%W(find #{repository_storage.legacy_disk_path} -mindepth 2 -type d -name *.git)) do |find|
+ find.each_line do |path|
+ yield path.chomp
+ end
end
end
end
end
def repository_storage_paths_args
- Gitlab.config.repositories.storages.values.map { |rs| rs.legacy_disk_path }
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ Gitlab.config.repositories.storages.values.map { |rs| rs.legacy_disk_path }
+ end
end
def user_home
diff --git a/lib/gitlab/temporarily_allow.rb b/lib/gitlab/temporarily_allow.rb
new file mode 100644
index 00000000000..880e55f71df
--- /dev/null
+++ b/lib/gitlab/temporarily_allow.rb
@@ -0,0 +1,42 @@
+module Gitlab
+ module TemporarilyAllow
+ TEMPORARILY_ALLOW_MUTEX = Mutex.new
+
+ def temporarily_allow(key)
+ temporarily_allow_add(key, 1)
+ yield
+ ensure
+ temporarily_allow_add(key, -1)
+ end
+
+ def temporarily_allowed?(key)
+ if RequestStore.active?
+ temporarily_allow_request_store[key] > 0
+ else
+ TEMPORARILY_ALLOW_MUTEX.synchronize do
+ temporarily_allow_ivar[key] > 0
+ end
+ end
+ end
+
+ private
+
+ def temporarily_allow_ivar
+ @temporarily_allow ||= Hash.new(0)
+ end
+
+ def temporarily_allow_request_store
+ RequestStore[:temporarily_allow] ||= Hash.new(0)
+ end
+
+ def temporarily_allow_add(key, value)
+ if RequestStore.active?
+ temporarily_allow_request_store[key] += value
+ else
+ TEMPORARILY_ALLOW_MUTEX.synchronize do
+ temporarily_allow_ivar[key] += value
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/themes.rb b/lib/gitlab/themes.rb
index d43eff5ba4a..694b01b272c 100644
--- a/lib/gitlab/themes.rb
+++ b/lib/gitlab/themes.rb
@@ -12,11 +12,16 @@ module Gitlab
# All available Themes
THEMES = [
- Theme.new(1, 'Indigo', 'ui_indigo'),
- Theme.new(2, 'Dark', 'ui_dark'),
- Theme.new(3, 'Light', 'ui_light'),
- Theme.new(4, 'Blue', 'ui_blue'),
- Theme.new(5, 'Green', 'ui_green')
+ Theme.new(1, 'Indigo', 'ui-indigo'),
+ Theme.new(6, 'Light Indigo', 'ui-light-indigo'),
+ Theme.new(4, 'Blue', 'ui-blue'),
+ Theme.new(7, 'Light Blue', 'ui-light-blue'),
+ Theme.new(5, 'Green', 'ui-green'),
+ Theme.new(8, 'Light Green', 'ui-light-green'),
+ Theme.new(9, 'Red', 'ui-red'),
+ Theme.new(10, 'Light Red', 'ui-light-red'),
+ Theme.new(2, 'Dark', 'ui-dark'),
+ Theme.new(3, 'Light', 'ui-light')
].freeze
# Convenience method to get a space-separated String of all the theme
diff --git a/lib/gitlab/url_blocker.rb b/lib/gitlab/url_blocker.rb
index db97f65bd54..38be75b7482 100644
--- a/lib/gitlab/url_blocker.rb
+++ b/lib/gitlab/url_blocker.rb
@@ -5,7 +5,7 @@ module Gitlab
BlockedUrlError = Class.new(StandardError)
class << self
- def validate!(url, allow_localhost: false, allow_local_network: true, valid_ports: [])
+ def validate!(url, allow_localhost: false, allow_local_network: true, enforce_user: false, ports: [], protocols: [])
return true if url.nil?
begin
@@ -18,8 +18,9 @@ module Gitlab
return true if internal?(uri)
port = uri.port || uri.default_port
- validate_port!(port, valid_ports) if valid_ports.any?
- validate_user!(uri.user)
+ validate_protocol!(uri.scheme, protocols)
+ validate_port!(port, ports) if ports.any?
+ validate_user!(uri.user) if enforce_user
validate_hostname!(uri.hostname)
begin
@@ -44,13 +45,19 @@ module Gitlab
private
- def validate_port!(port, valid_ports)
+ def validate_port!(port, ports)
return if port.blank?
# Only ports under 1024 are restricted
return if port >= 1024
- return if valid_ports.include?(port)
+ return if ports.include?(port)
- raise BlockedUrlError, "Only allowed ports are #{valid_ports.join(', ')}, and any over 1024"
+ raise BlockedUrlError, "Only allowed ports are #{ports.join(', ')}, and any over 1024"
+ end
+
+ def validate_protocol!(protocol, protocols)
+ if protocol.blank? || (protocols.any? && !protocols.include?(protocol))
+ raise BlockedUrlError, "Only allowed protocols are #{protocols.join(', ')}"
+ end
end
def validate_user!(value)
diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb
index 824e2d7251f..e64033b0dba 100644
--- a/lib/gitlab/url_builder.rb
+++ b/lib/gitlab/url_builder.rb
@@ -26,6 +26,8 @@ module Gitlab
project_snippet_url(object.project, object)
when Snippet
snippet_url(object)
+ when Milestone
+ milestone_url(object)
else
raise NotImplementedError.new("No URL builder defined for #{object.class}")
end
diff --git a/lib/gitlab/url_sanitizer.rb b/lib/gitlab/url_sanitizer.rb
index 59331c827af..de8b6ec69ce 100644
--- a/lib/gitlab/url_sanitizer.rb
+++ b/lib/gitlab/url_sanitizer.rb
@@ -58,7 +58,7 @@ module Gitlab
if raw_credentials.present?
url.sub!("#{raw_credentials}@", '')
- user, password = raw_credentials.split(':')
+ user, _, password = raw_credentials.partition(':')
@credentials ||= { user: user.presence, password: password.presence }
end
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index e294f3c4ebc..dff0c97eeb4 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -21,9 +21,9 @@ module Gitlab
uuid: Gitlab::CurrentSettings.uuid,
hostname: Gitlab.config.gitlab.host,
version: Gitlab::VERSION,
+ installation_type: Gitlab::INSTALLATION_TYPE,
active_user_count: User.active.count,
recorded_at: Time.now,
- mattermost_enabled: Gitlab.config.mattermost.enabled,
edition: 'CE'
}
@@ -90,13 +90,14 @@ module Gitlab
def features_usage_data_ce
{
- signup: Gitlab::CurrentSettings.allow_signup?,
- ldap: Gitlab.config.ldap.enabled,
- gravatar: Gitlab::CurrentSettings.gravatar_enabled?,
- omniauth: Gitlab.config.omniauth.enabled,
- reply_by_email: Gitlab::IncomingEmail.enabled?,
- container_registry: Gitlab.config.registry.enabled,
- gitlab_shared_runners: Gitlab.config.gitlab_ci.shared_runners_enabled
+ container_registry_enabled: Gitlab.config.registry.enabled,
+ gitlab_shared_runners_enabled: Gitlab.config.gitlab_ci.shared_runners_enabled,
+ gravatar_enabled: Gitlab::CurrentSettings.gravatar_enabled?,
+ ldap_enabled: Gitlab.config.ldap.enabled,
+ mattermost_enabled: Gitlab.config.mattermost.enabled,
+ omniauth_enabled: Gitlab.config.omniauth.enabled,
+ reply_by_email_enabled: Gitlab::IncomingEmail.enabled?,
+ signup_enabled: Gitlab::CurrentSettings.allow_signup?
}
end
diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb
index 8cf5d636743..27560abfb96 100644
--- a/lib/gitlab/user_access.rb
+++ b/lib/gitlab/user_access.rb
@@ -65,7 +65,7 @@ module Gitlab
return false unless can_access_git?
return false unless project
- return false if !user.can?(:push_code, project) && !project.branch_allows_maintainer_push?(user, ref)
+ return false if !user.can?(:push_code, project) && !project.branch_allows_collaboration?(user, ref)
if protected?(ProtectedBranch, project, ref)
protected_branch_accessible_to?(ref, action: :push)
diff --git a/lib/gitlab/utils/override.rb b/lib/gitlab/utils/override.rb
index 8bf6bcb1fe2..7b2a62fed48 100644
--- a/lib/gitlab/utils/override.rb
+++ b/lib/gitlab/utils/override.rb
@@ -87,18 +87,28 @@ module Gitlab
end
def included(base = nil)
- return super if base.nil? # Rails concern, ignoring it
+ super
+
+ queue_verification(base)
+ end
+ alias_method :prepended, :included
+
+ def extended(mod)
super
+ queue_verification(mod.singleton_class)
+ end
+
+ def queue_verification(base)
+ return unless ENV['STATIC_VERIFICATION']
+
if base.is_a?(Class) # We could check for Class in `override`
# This could be `nil` if `override` was never called
Override.extensions[self]&.add_class(base)
end
end
- alias_method :prepended, :included
-
def self.extensions
@extensions ||= {}
end
diff --git a/lib/gitlab/verify/batch_verifier.rb b/lib/gitlab/verify/batch_verifier.rb
index 1ef369a4b67..167ba1b3149 100644
--- a/lib/gitlab/verify/batch_verifier.rb
+++ b/lib/gitlab/verify/batch_verifier.rb
@@ -7,13 +7,15 @@ module Gitlab
@batch_size = batch_size
@start = start
@finish = finish
+
+ fix_google_api_logger
end
# Yields a Range of IDs and a Hash of failed verifications (object => error)
def run_batches(&blk)
- relation.in_batches(of: batch_size, start: start, finish: finish) do |relation| # rubocop: disable Cop/InBatches
- range = relation.first.id..relation.last.id
- failures = run_batch(relation)
+ all_relation.in_batches(of: batch_size, start: start, finish: finish) do |batch| # rubocop: disable Cop/InBatches
+ range = batch.first.id..batch.last.id
+ failures = run_batch_for(batch)
yield(range, failures)
end
@@ -29,24 +31,56 @@ module Gitlab
private
- def run_batch(relation)
- relation.map { |upload| verify(upload) }.compact.to_h
+ def run_batch_for(batch)
+ batch.map { |upload| verify(upload) }.compact.to_h
end
def verify(object)
+ local?(object) ? verify_local(object) : verify_remote(object)
+ rescue => err
+ failure(object, err.inspect)
+ end
+
+ def verify_local(object)
expected = expected_checksum(object)
actual = actual_checksum(object)
- raise 'Checksum missing' unless expected.present?
- raise 'Checksum mismatch' unless expected == actual
+ return failure(object, 'Checksum missing') unless expected.present?
+ return failure(object, 'Checksum mismatch') unless expected == actual
+
+ success
+ end
+ # We don't calculate checksum for remote objects, so just check existence
+ def verify_remote(object)
+ return failure(object, 'Remote object does not exist') unless remote_object_exists?(object)
+
+ success
+ end
+
+ def success
nil
- rescue => err
- [object, err]
+ end
+
+ def failure(object, message)
+ [object, message]
+ end
+
+ # It's already set to Logger::INFO, but acts as if it is set to
+ # Logger::DEBUG, and this fixes it...
+ def fix_google_api_logger
+ if Object.const_defined?('Google::Apis')
+ Google::Apis.logger.level = Logger::INFO
+ end
end
# This should return an ActiveRecord::Relation suitable for calling #in_batches on
- def relation
+ def all_relation
+ raise NotImplementedError.new
+ end
+
+ # Should return true if the object is stored locally
+ def local?(_object)
raise NotImplementedError.new
end
@@ -59,6 +93,11 @@ module Gitlab
def actual_checksum(_object)
raise NotImplementedError.new
end
+
+ # Be sure to perform a hard check of the remote object (don't just check DB value)
+ def remote_object_exists?(object)
+ raise NotImplementedError.new
+ end
end
end
end
diff --git a/lib/gitlab/verify/job_artifacts.rb b/lib/gitlab/verify/job_artifacts.rb
index 03500a61074..dbadfbde9e3 100644
--- a/lib/gitlab/verify/job_artifacts.rb
+++ b/lib/gitlab/verify/job_artifacts.rb
@@ -11,10 +11,14 @@ module Gitlab
private
- def relation
+ def all_relation
::Ci::JobArtifact.all
end
+ def local?(artifact)
+ artifact.local_store?
+ end
+
def expected_checksum(artifact)
artifact.file_sha256
end
@@ -22,6 +26,10 @@ module Gitlab
def actual_checksum(artifact)
Digest::SHA256.file(artifact.file.path).hexdigest
end
+
+ def remote_object_exists?(artifact)
+ artifact.file.file.exists?
+ end
end
end
end
diff --git a/lib/gitlab/verify/lfs_objects.rb b/lib/gitlab/verify/lfs_objects.rb
index 970e2a7b718..d3f58a73ac7 100644
--- a/lib/gitlab/verify/lfs_objects.rb
+++ b/lib/gitlab/verify/lfs_objects.rb
@@ -11,8 +11,12 @@ module Gitlab
private
- def relation
- LfsObject.with_files_stored_locally
+ def all_relation
+ LfsObject.all
+ end
+
+ def local?(lfs_object)
+ lfs_object.local_store?
end
def expected_checksum(lfs_object)
@@ -22,6 +26,10 @@ module Gitlab
def actual_checksum(lfs_object)
LfsObject.calculate_oid(lfs_object.file.path)
end
+
+ def remote_object_exists?(lfs_object)
+ lfs_object.file.file.exists?
+ end
end
end
end
diff --git a/lib/gitlab/verify/rake_task.rb b/lib/gitlab/verify/rake_task.rb
index dd138e6b92b..e190eaddc79 100644
--- a/lib/gitlab/verify/rake_task.rb
+++ b/lib/gitlab/verify/rake_task.rb
@@ -45,7 +45,7 @@ module Gitlab
return unless verbose?
failures.each do |object, error|
- say " - #{verifier.describe(object)}: #{error.inspect}".color(:red)
+ say " - #{verifier.describe(object)}: #{error}".color(:red)
end
end
end
diff --git a/lib/gitlab/verify/uploads.rb b/lib/gitlab/verify/uploads.rb
index 0ffa71a6d72..73fc43cb590 100644
--- a/lib/gitlab/verify/uploads.rb
+++ b/lib/gitlab/verify/uploads.rb
@@ -11,8 +11,12 @@ module Gitlab
private
- def relation
- Upload.with_files_stored_locally
+ def all_relation
+ Upload.all.preload(:model)
+ end
+
+ def local?(upload)
+ upload.local?
end
def expected_checksum(upload)
@@ -22,6 +26,10 @@ module Gitlab
def actual_checksum(upload)
Upload.hexdigest(upload.absolute_path)
end
+
+ def remote_object_exists?(upload)
+ upload.build_uploader.file.exists?
+ end
end
end
end
diff --git a/lib/gitlab/webpack/dev_server_middleware.rb b/lib/gitlab/webpack/dev_server_middleware.rb
index b9a75eaac63..529f7d6a8d6 100644
--- a/lib/gitlab/webpack/dev_server_middleware.rb
+++ b/lib/gitlab/webpack/dev_server_middleware.rb
@@ -15,6 +15,11 @@ module Gitlab
def perform_request(env)
if @proxy_path && env['PATH_INFO'].start_with?("/#{@proxy_path}")
+ if relative_url_root = Rails.application.config.relative_url_root
+ env['SCRIPT_NAME'] = ""
+ env['REQUEST_PATH'].sub!(/\A#{Regexp.escape(relative_url_root)}/, '')
+ end
+
super(env)
else
@app.call(env)
diff --git a/lib/gitlab/wiki_file_finder.rb b/lib/gitlab/wiki_file_finder.rb
new file mode 100644
index 00000000000..f97278f05cd
--- /dev/null
+++ b/lib/gitlab/wiki_file_finder.rb
@@ -0,0 +1,23 @@
+module Gitlab
+ class WikiFileFinder < FileFinder
+ attr_reader :repository
+
+ def initialize(project, ref)
+ @project = project
+ @ref = ref
+ @repository = project.wiki.repository
+ end
+
+ private
+
+ def search_filenames(query, except)
+ safe_query = Regexp.escape(query.tr(' ', '-'))
+ safe_query = Regexp.new(safe_query, Regexp::IGNORECASE)
+ filenames = repository.ls_files(ref)
+
+ filenames.delete_if { |filename| except.include?(filename) } unless except.empty?
+
+ filenames.grep(safe_query).first(BATCH_SIZE)
+ end
+ end
+end
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index e893e46ee86..a9629a92a50 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -37,21 +37,14 @@ module Gitlab
end
def send_git_blob(repository, blob)
- params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_raw_show, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT)
- {
- 'GitalyServer' => gitaly_server_hash(repository),
- 'GetBlobRequest' => {
- repository: repository.gitaly_repository.to_h,
- oid: blob.id,
- limit: -1
- }
- }
- else
- {
- 'RepoPath' => repository.path_to_repo,
- 'BlobId' => blob.id
- }
- end
+ params = {
+ 'GitalyServer' => gitaly_server_hash(repository),
+ 'GetBlobRequest' => {
+ repository: repository.gitaly_repository.to_h,
+ oid: blob.id,
+ limit: -1
+ }
+ }
[
SEND_DATA_HEADER,
@@ -91,16 +84,12 @@ module Gitlab
end
def send_git_diff(repository, diff_refs)
- params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_send_git_diff, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT)
- {
- '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
+ params = {
+ 'GitalyServer' => gitaly_server_hash(repository),
+ 'RawDiffRequest' => Gitaly::RawDiffRequest.new(
+ gitaly_diff_or_patch_hash(repository, diff_refs)
+ ).to_json
+ }
[
SEND_DATA_HEADER,
@@ -109,16 +98,12 @@ module Gitlab
end
def send_git_patch(repository, diff_refs)
- params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_send_git_patch, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT)
- {
- '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
+ params = {
+ 'GitalyServer' => gitaly_server_hash(repository),
+ 'RawPatchRequest' => Gitaly::RawPatchRequest.new(
+ gitaly_diff_or_patch_hash(repository, diff_refs)
+ ).to_json
+ }
[
SEND_DATA_HEADER,
@@ -231,14 +216,6 @@ module Gitlab
}
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,
diff --git a/lib/google_api/cloud_platform/client.rb b/lib/google_api/cloud_platform/client.rb
index f30dd995695..36859b4d025 100644
--- a/lib/google_api/cloud_platform/client.rb
+++ b/lib/google_api/cloud_platform/client.rb
@@ -1,3 +1,4 @@
+require 'google/apis/compute_v1'
require 'google/apis/container_v1'
require 'google/apis/cloudbilling_v1'
require 'google/apis/cloudresourcemanager_v1'
@@ -42,22 +43,6 @@ module GoogleApi
true
end
- def projects_list
- service = Google::Apis::CloudresourcemanagerV1::CloudResourceManagerService.new
- service.authorization = access_token
-
- service.fetch_all(items: :projects) do |token|
- service.list_projects(page_token: token, options: user_agent_header)
- end
- end
-
- def projects_get_billing_info(project_id)
- service = Google::Apis::CloudbillingV1::CloudbillingService.new
- service.authorization = access_token
-
- service.get_project_billing_info("projects/#{project_id}", options: user_agent_header)
- end
-
def projects_zones_clusters_get(project_id, zone, cluster_id)
service = Google::Apis::ContainerV1::ContainerService.new
service.authorization = access_token
diff --git a/lib/mattermost/command.rb b/lib/mattermost/command.rb
index 33e450d7f0a..704813dfdf0 100644
--- a/lib/mattermost/command.rb
+++ b/lib/mattermost/command.rb
@@ -1,7 +1,7 @@
module Mattermost
class Command < Client
def create(params)
- response = session_post("/api/v3/teams/#{params[:team_id]}/commands/create",
+ response = session_post('/api/v4/commands',
body: params.to_json)
response['token']
diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb
index 85f78e44f32..2aa7a2f64d8 100644
--- a/lib/mattermost/session.rb
+++ b/lib/mattermost/session.rb
@@ -112,7 +112,7 @@ module Mattermost
end
def destroy
- post('/api/v3/users/logout')
+ post('/api/v4/users/logout')
end
def oauth_uri
@@ -120,7 +120,7 @@ module Mattermost
@oauth_uri = nil
- response = get("/api/v3/oauth/gitlab/login", follow_redirects: false)
+ response = get('/oauth/gitlab/login', follow_redirects: false, format: 'text/html')
return unless (300...400) === response.code
redirect_uri = response.headers['location']
diff --git a/lib/mattermost/team.rb b/lib/mattermost/team.rb
index 75513a9ba04..95c2f6f9d6b 100644
--- a/lib/mattermost/team.rb
+++ b/lib/mattermost/team.rb
@@ -1,14 +1,14 @@
module Mattermost
class Team < Client
- # Returns **all** teams for an admin
+ # Returns all teams that the current user is a member of
def all
- session_get('/api/v3/teams/all').values
+ session_get("/api/v4/users/me/teams")
end
# Creates a team on the linked Mattermost instance, the team admin will be the
# `current_user` passed to the Mattermost::Client instance
def create(name:, display_name:, type:)
- session_post('/api/v3/teams/create', body: {
+ session_post('/api/v4/teams', body: {
name: name,
display_name: display_name,
type: type
diff --git a/lib/microsoft_teams/notifier.rb b/lib/microsoft_teams/notifier.rb
index c08d3e933a8..226ee1373db 100644
--- a/lib/microsoft_teams/notifier.rb
+++ b/lib/microsoft_teams/notifier.rb
@@ -30,7 +30,7 @@ module MicrosoftTeams
result = { 'sections' => [] }
result['title'] = options[:title]
- result['summary'] = options[:pretext]
+ result['summary'] = options[:summary]
result['sections'] << MicrosoftTeams::Activity.new(options[:activity]).prepare
attachments = options[:attachments]
diff --git a/lib/mysql_zero_date.rb b/lib/mysql_zero_date.rb
new file mode 100644
index 00000000000..64634f789da
--- /dev/null
+++ b/lib/mysql_zero_date.rb
@@ -0,0 +1,18 @@
+# Disable NO_ZERO_DATE mode for mysql in rails 5.
+# We use zero date as a default value
+# (config/initializers/active_record_mysql_timestamp.rb), in
+# Rails 5 using zero date fails by default (https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/75450216)
+# and NO_ZERO_DATE has to be explicitly disabled. Disabling strict mode
+# is not sufficient.
+
+require 'active_record/connection_adapters/abstract_mysql_adapter'
+
+module MysqlZeroDate
+ def configure_connection
+ super
+
+ @connection.query "SET @@SESSION.sql_mode = REPLACE(@@SESSION.sql_mode, 'NO_ZERO_DATE', '');" # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ end
+end
+
+ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter.prepend(MysqlZeroDate) if Gitlab.rails5?
diff --git a/lib/object_storage/direct_upload.rb b/lib/object_storage/direct_upload.rb
new file mode 100644
index 00000000000..61a69e7ffe4
--- /dev/null
+++ b/lib/object_storage/direct_upload.rb
@@ -0,0 +1,166 @@
+module ObjectStorage
+ #
+ # The DirectUpload c;ass generates a set of presigned URLs
+ # that can be used to upload data to object storage from untrusted component: Workhorse, Runner?
+ #
+ # For Google it assumes that the platform supports variable Content-Length.
+ #
+ # For AWS it initiates Multipart Upload and presignes a set of part uploads.
+ # Class calculates the best part size to be able to upload up to asked maximum size.
+ # The number of generated parts will never go above 100,
+ # but we will always try to reduce amount of generated parts.
+ # The part size is rounded-up to 5MB.
+ #
+ class DirectUpload
+ include Gitlab::Utils::StrongMemoize
+
+ TIMEOUT = 4.hours
+ EXPIRE_OFFSET = 15.minutes
+
+ MAXIMUM_MULTIPART_PARTS = 100
+ MINIMUM_MULTIPART_SIZE = 5.megabytes
+
+ attr_reader :credentials, :bucket_name, :object_name
+ attr_reader :has_length, :maximum_size
+
+ def initialize(credentials, bucket_name, object_name, has_length:, maximum_size: nil)
+ unless has_length
+ raise ArgumentError, 'maximum_size has to be specified if length is unknown' unless maximum_size
+ end
+
+ @credentials = credentials
+ @bucket_name = bucket_name
+ @object_name = object_name
+ @has_length = has_length
+ @maximum_size = maximum_size
+ end
+
+ def to_hash
+ {
+ Timeout: TIMEOUT,
+ GetURL: get_url,
+ StoreURL: store_url,
+ DeleteURL: delete_url,
+ MultipartUpload: multipart_upload_hash
+ }.compact
+ end
+
+ def multipart_upload_hash
+ return unless requires_multipart_upload?
+
+ {
+ PartSize: rounded_multipart_part_size,
+ PartURLs: multipart_part_urls,
+ CompleteURL: multipart_complete_url,
+ AbortURL: multipart_abort_url
+ }
+ end
+
+ def provider
+ credentials[:provider].to_s
+ end
+
+ # Implements https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectGET.html
+ def get_url
+ connection.get_object_url(bucket_name, object_name, expire_at)
+ end
+
+ # Implements https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectDELETE.html
+ def delete_url
+ connection.delete_object_url(bucket_name, object_name, expire_at)
+ end
+
+ # Implements https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectPUT.html
+ def store_url
+ connection.put_object_url(bucket_name, object_name, expire_at, upload_options)
+ end
+
+ def multipart_part_urls
+ Array.new(number_of_multipart_parts) do |part_index|
+ multipart_part_upload_url(part_index + 1)
+ end
+ end
+
+ # Implements https://docs.aws.amazon.com/AmazonS3/latest/API/mpUploadUploadPart.html
+ def multipart_part_upload_url(part_number)
+ connection.signed_url({
+ method: 'PUT',
+ bucket_name: bucket_name,
+ object_name: object_name,
+ query: { uploadId: upload_id, partNumber: part_number },
+ headers: upload_options
+ }, expire_at)
+ end
+
+ # Implements https://docs.aws.amazon.com/AmazonS3/latest/API/mpUploadComplete.html
+ def multipart_complete_url
+ connection.signed_url({
+ method: 'POST',
+ bucket_name: bucket_name,
+ object_name: object_name,
+ query: { uploadId: upload_id },
+ headers: { 'Content-Type' => 'application/xml' }
+ }, expire_at)
+ end
+
+ # Implements https://docs.aws.amazon.com/AmazonS3/latest/API/mpUploadAbort.html
+ def multipart_abort_url
+ connection.signed_url({
+ method: 'DELETE',
+ bucket_name: bucket_name,
+ object_name: object_name,
+ query: { uploadId: upload_id }
+ }, expire_at)
+ end
+
+ private
+
+ def rounded_multipart_part_size
+ # round multipart_part_size up to minimum_mulitpart_size
+ (multipart_part_size + MINIMUM_MULTIPART_SIZE - 1) / MINIMUM_MULTIPART_SIZE * MINIMUM_MULTIPART_SIZE
+ end
+
+ def multipart_part_size
+ maximum_size / number_of_multipart_parts
+ end
+
+ def number_of_multipart_parts
+ [
+ # round maximum_size up to minimum_mulitpart_size
+ (maximum_size + MINIMUM_MULTIPART_SIZE - 1) / MINIMUM_MULTIPART_SIZE,
+ MAXIMUM_MULTIPART_PARTS
+ ].min
+ end
+
+ def aws?
+ provider == 'AWS'
+ end
+
+ def requires_multipart_upload?
+ aws? && !has_length
+ end
+
+ def upload_id
+ return unless requires_multipart_upload?
+
+ strong_memoize(:upload_id) do
+ new_upload = connection.initiate_multipart_upload(bucket_name, object_name)
+ new_upload.body["UploadId"]
+ end
+ end
+
+ def expire_at
+ strong_memoize(:expire_at) do
+ Time.now + TIMEOUT + EXPIRE_OFFSET
+ end
+ end
+
+ def upload_options
+ { 'Content-Type' => 'application/octet-stream' }
+ end
+
+ def connection
+ @connection ||= ::Fog::Storage.new(credentials)
+ end
+ end
+end
diff --git a/lib/omni_auth/strategies/jwt.rb b/lib/omni_auth/strategies/jwt.rb
index 2349b2a28aa..ebdb5c7faf0 100644
--- a/lib/omni_auth/strategies/jwt.rb
+++ b/lib/omni_auth/strategies/jwt.rb
@@ -3,7 +3,7 @@ require 'jwt'
module OmniAuth
module Strategies
- class JWT
+ class Jwt
ClaimInvalid = Class.new(StandardError)
include OmniAuth::Strategy
@@ -56,7 +56,5 @@ module OmniAuth
fail! :claim_invalid, e
end
end
-
- class Jwt < JWT; end
end
end
diff --git a/lib/peek/rblineprof/custom_controller_helpers.rb b/lib/peek/rblineprof/custom_controller_helpers.rb
index 7cfe76b7b71..9beb442bfa3 100644
--- a/lib/peek/rblineprof/custom_controller_helpers.rb
+++ b/lib/peek/rblineprof/custom_controller_helpers.rb
@@ -41,10 +41,10 @@ module Peek
]
end.sort_by{ |a,b,c,d,e,f| -f }
- output = "<div class='modal-dialog modal-full'><div class='modal-content'>"
+ output = "<div class='modal-dialog modal-xl'><div class='modal-content'>"
output << "<div class='modal-header'>"
- output << "<button class='close btn btn-link btn-sm' type='button' data-dismiss='modal'>X</button>"
output << "<h4>Line profiling: #{human_description(params[:lineprofiler])}</h4>"
+ output << "<button class='close' type='button' data-dismiss='modal' aria-label='close'><span aria-hidden='true'>&times;</span></button>"
output << "</div>"
output << "<div class='modal-body'>"
diff --git a/lib/rouge/formatters/html_gitlab.rb b/lib/rouge/formatters/html_gitlab.rb
index be0d97370d0..e877ab10248 100644
--- a/lib/rouge/formatters/html_gitlab.rb
+++ b/lib/rouge/formatters/html_gitlab.rb
@@ -11,7 +11,7 @@ module Rouge
@tag = tag
end
- def stream(tokens, &b)
+ def stream(tokens)
is_first = true
token_lines(tokens) do |line|
yield "\n" unless is_first
diff --git a/lib/rspec_flaky/listener.rb b/lib/rspec_flaky/listener.rb
index 5b5e4f7c7de..9cd0c38cb55 100644
--- a/lib/rspec_flaky/listener.rb
+++ b/lib/rspec_flaky/listener.rb
@@ -1,10 +1,10 @@
require 'json'
-require_relative 'config'
-require_relative 'example'
-require_relative 'flaky_example'
-require_relative 'flaky_examples_collection'
-require_relative 'report'
+require_dependency 'rspec_flaky/config'
+require_dependency 'rspec_flaky/example'
+require_dependency 'rspec_flaky/flaky_example'
+require_dependency 'rspec_flaky/flaky_examples_collection'
+require_dependency 'rspec_flaky/report'
module RspecFlaky
class Listener
diff --git a/lib/rspec_flaky/report.rb b/lib/rspec_flaky/report.rb
index a8730d3b7c7..1c362fdd20d 100644
--- a/lib/rspec_flaky/report.rb
+++ b/lib/rspec_flaky/report.rb
@@ -1,8 +1,8 @@
require 'json'
require 'time'
-require_relative 'config'
-require_relative 'flaky_examples_collection'
+require_dependency 'rspec_flaky/config'
+require_dependency 'rspec_flaky/flaky_examples_collection'
module RspecFlaky
# This class is responsible for loading/saving JSON reports, and pruning
diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab
index 0e27a28ea6e..72eb8adcce2 100644
--- a/lib/support/nginx/gitlab
+++ b/lib/support/nginx/gitlab
@@ -31,27 +31,27 @@ map $http_upgrade $connection_upgrade_gitlab {
log_format gitlab_access $remote_addr - $remote_user [$time_local] "$request_method $gitlab_filtered_request_uri $server_protocol" $status $body_bytes_sent "$gitlab_filtered_http_referer" "$http_user_agent";
## Remove private_token from the request URI
-# In: /foo?private_token=unfiltered&authenticity_token=unfiltered&rss_token=unfiltered&...
-# Out: /foo?private_token=[FILTERED]&authenticity_token=unfiltered&rss_token=unfiltered&...
+# In: /foo?private_token=unfiltered&authenticity_token=unfiltered&feed_token=unfiltered&...
+# Out: /foo?private_token=[FILTERED]&authenticity_token=unfiltered&feed_token=unfiltered&...
map $request_uri $gitlab_temp_request_uri_1 {
default $request_uri;
~(?i)^(?<start>.*)(?<temp>[\?&]private[\-_]token)=[^&]*(?<rest>.*)$ "$start$temp=[FILTERED]$rest";
}
## Remove authenticity_token from the request URI
-# In: /foo?private_token=[FILTERED]&authenticity_token=unfiltered&rss_token=unfiltered&...
-# Out: /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&rss_token=unfiltered&...
+# In: /foo?private_token=[FILTERED]&authenticity_token=unfiltered&feed_token=unfiltered&...
+# Out: /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&feed_token=unfiltered&...
map $gitlab_temp_request_uri_1 $gitlab_temp_request_uri_2 {
default $gitlab_temp_request_uri_1;
~(?i)^(?<start>.*)(?<temp>[\?&]authenticity[\-_]token)=[^&]*(?<rest>.*)$ "$start$temp=[FILTERED]$rest";
}
-## Remove rss_token from the request URI
-# In: /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&rss_token=unfiltered&...
-# Out: /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&rss_token=[FILTERED]&...
+## Remove feed_token from the request URI
+# In: /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&feed_token=unfiltered&...
+# Out: /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&feed_token=[FILTERED]&...
map $gitlab_temp_request_uri_2 $gitlab_filtered_request_uri {
default $gitlab_temp_request_uri_2;
- ~(?i)^(?<start>.*)(?<temp>[\?&]rss[\-_]token)=[^&]*(?<rest>.*)$ "$start$temp=[FILTERED]$rest";
+ ~(?i)^(?<start>.*)(?<temp>[\?&]feed[\-_]token)=[^&]*(?<rest>.*)$ "$start$temp=[FILTERED]$rest";
}
## A version of the referer without the query string
diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl
index 8218d68f9ba..2e3799d5e1b 100644
--- a/lib/support/nginx/gitlab-ssl
+++ b/lib/support/nginx/gitlab-ssl
@@ -36,27 +36,27 @@ map $http_upgrade $connection_upgrade_gitlab_ssl {
log_format gitlab_ssl_access $remote_addr - $remote_user [$time_local] "$request_method $gitlab_ssl_filtered_request_uri $server_protocol" $status $body_bytes_sent "$gitlab_ssl_filtered_http_referer" "$http_user_agent";
## Remove private_token from the request URI
-# In: /foo?private_token=unfiltered&authenticity_token=unfiltered&rss_token=unfiltered&...
-# Out: /foo?private_token=[FILTERED]&authenticity_token=unfiltered&rss_token=unfiltered&...
+# In: /foo?private_token=unfiltered&authenticity_token=unfiltered&feed_token=unfiltered&...
+# Out: /foo?private_token=[FILTERED]&authenticity_token=unfiltered&feed_token=unfiltered&...
map $request_uri $gitlab_ssl_temp_request_uri_1 {
default $request_uri;
~(?i)^(?<start>.*)(?<temp>[\?&]private[\-_]token)=[^&]*(?<rest>.*)$ "$start$temp=[FILTERED]$rest";
}
## Remove authenticity_token from the request URI
-# In: /foo?private_token=[FILTERED]&authenticity_token=unfiltered&rss_token=unfiltered&...
-# Out: /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&rss_token=unfiltered&...
+# In: /foo?private_token=[FILTERED]&authenticity_token=unfiltered&feed_token=unfiltered&...
+# Out: /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&feed_token=unfiltered&...
map $gitlab_ssl_temp_request_uri_1 $gitlab_ssl_temp_request_uri_2 {
default $gitlab_ssl_temp_request_uri_1;
~(?i)^(?<start>.*)(?<temp>[\?&]authenticity[\-_]token)=[^&]*(?<rest>.*)$ "$start$temp=[FILTERED]$rest";
}
-## Remove rss_token from the request URI
-# In: /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&rss_token=unfiltered&...
-# Out: /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&rss_token=[FILTERED]&...
+## Remove feed_token from the request URI
+# In: /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&feed_token=unfiltered&...
+# Out: /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&feed_token=[FILTERED]&...
map $gitlab_ssl_temp_request_uri_2 $gitlab_ssl_filtered_request_uri {
default $gitlab_ssl_temp_request_uri_2;
- ~(?i)^(?<start>.*)(?<temp>[\?&]rss[\-_]token)=[^&]*(?<rest>.*)$ "$start$temp=[FILTERED]$rest";
+ ~(?i)^(?<start>.*)(?<temp>[\?&]feed[\-_]token)=[^&]*(?<rest>.*)$ "$start$temp=[FILTERED]$rest";
}
## A version of the referer without the query string
diff --git a/lib/system_check/orphans/namespace_check.rb b/lib/system_check/orphans/namespace_check.rb
index b5f443abe06..09b57c7b408 100644
--- a/lib/system_check/orphans/namespace_check.rb
+++ b/lib/system_check/orphans/namespace_check.rb
@@ -4,13 +4,15 @@ module SystemCheck
set_name 'Orphaned namespaces:'
def multi_check
- Gitlab.config.repositories.storages.each do |storage_name, repository_storage|
- $stdout.puts
- $stdout.puts "* Storage: #{storage_name} (#{repository_storage.legacy_disk_path})".color(:yellow)
- toplevel_namespace_dirs = disk_namespaces(repository_storage.legacy_disk_path)
-
- orphans = (toplevel_namespace_dirs - existing_namespaces)
- print_orphans(orphans, storage_name)
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ Gitlab.config.repositories.storages.each do |storage_name, repository_storage|
+ $stdout.puts
+ $stdout.puts "* Storage: #{storage_name} (#{repository_storage.legacy_disk_path})".color(:yellow)
+ toplevel_namespace_dirs = disk_namespaces(repository_storage.legacy_disk_path)
+
+ orphans = (toplevel_namespace_dirs - existing_namespaces)
+ print_orphans(orphans, storage_name)
+ end
end
clear_namespaces! # releases memory when check finishes
diff --git a/lib/system_check/orphans/repository_check.rb b/lib/system_check/orphans/repository_check.rb
index 5ef0b93ad08..2695c658874 100644
--- a/lib/system_check/orphans/repository_check.rb
+++ b/lib/system_check/orphans/repository_check.rb
@@ -5,16 +5,18 @@ module SystemCheck
attr_accessor :orphans
def multi_check
- Gitlab.config.repositories.storages.each do |storage_name, repository_storage|
- storage_path = repository_storage.legacy_disk_path
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ Gitlab.config.repositories.storages.each do |storage_name, repository_storage|
+ storage_path = repository_storage.legacy_disk_path
- $stdout.puts
- $stdout.puts "* Storage: #{storage_name} (#{storage_path})".color(:yellow)
+ $stdout.puts
+ $stdout.puts "* Storage: #{storage_name} (#{storage_path})".color(:yellow)
- repositories = disk_repositories(storage_path)
- orphans = (repositories - fetch_repositories(storage_name))
+ repositories = disk_repositories(storage_path)
+ orphans = (repositories - fetch_repositories(storage_name))
- print_orphans(orphans, storage_name)
+ print_orphans(orphans, storage_name)
+ end
end
end
diff --git a/lib/system_check/simple_executor.rb b/lib/system_check/simple_executor.rb
index d268f501b4a..99c9e984107 100644
--- a/lib/system_check/simple_executor.rb
+++ b/lib/system_check/simple_executor.rb
@@ -43,7 +43,7 @@ module SystemCheck
#
# @param [SystemCheck::BaseCheck] check_klass
def run_check(check_klass)
- $stdout.print "#{check_klass.display_name} ... "
+ print_display_name(check_klass)
check = check_klass.new
@@ -60,18 +60,18 @@ module SystemCheck
end
if check.check?
- $stdout.puts check_klass.check_pass.color(:green)
+ print_check_pass(check_klass)
else
- $stdout.puts check_klass.check_fail.color(:red)
+ print_check_failure(check_klass)
if check.can_repair?
$stdout.print 'Trying to fix error automatically. ...'
if check.repair!
- $stdout.puts 'Success'.color(:green)
+ print_success
return
else
- $stdout.puts 'Failed'.color(:red)
+ print_failure
end
end
@@ -83,6 +83,26 @@ module SystemCheck
private
+ def print_display_name(check_klass)
+ $stdout.print "#{check_klass.display_name} ... "
+ end
+
+ def print_check_pass(check_klass)
+ $stdout.puts check_klass.check_pass.color(:green)
+ end
+
+ def print_check_failure(check_klass)
+ $stdout.puts check_klass.check_fail.color(:red)
+ end
+
+ def print_success
+ $stdout.puts 'Success'.color(:green)
+ end
+
+ def print_failure
+ $stdout.puts 'Failed'.color(:red)
+ end
+
# Prints header content for the series of checks to be executed for this component
#
# @param [String] component name of the component relative to the checks being executed
diff --git a/lib/tasks/flay.rake b/lib/tasks/flay.rake
index 4b4881cecb8..4bec013a141 100644
--- a/lib/tasks/flay.rake
+++ b/lib/tasks/flay.rake
@@ -1,6 +1,6 @@
desc 'Code duplication analyze via flay'
task :flay do
- output = `bundle exec flay --mass 35 app/ lib/gitlab/ 2> #{File::NULL}`
+ output = `bundle exec flay --mass 35 app/ lib/gitlab/ ee/ 2> #{File::NULL}`
if output.include?("Similar code found") || output.include?("IDENTICAL code found")
puts output
diff --git a/lib/tasks/gettext.rake b/lib/tasks/gettext.rake
index 247d7be7d78..f431352b61e 100644
--- a/lib/tasks/gettext.rake
+++ b/lib/tasks/gettext.rake
@@ -4,7 +4,7 @@ namespace :gettext do
# Customize list of translatable files
# See: https://github.com/grosser/gettext_i18n_rails#customizing-list-of-translatable-files
def files_to_translate
- folders = %W(app lib config #{locale_path}).join(',')
+ folders = %W(ee app lib config #{locale_path}).join(',')
exts = %w(rb erb haml slim rhtml js jsx vue handlebars hbs mustache).join(',')
Dir.glob(
@@ -16,10 +16,32 @@ namespace :gettext do
# See: https://gitlab.com/gitlab-org/gitlab-ce/issues/33014#note_31218998
FileUtils.touch(File.join(Rails.root, 'locale/gitlab.pot'))
- Rake::Task['gettext:pack'].invoke
Rake::Task['gettext:po_to_json'].invoke
end
+ task :regenerate do
+ pot_file = 'locale/gitlab.pot'
+ # Remove all translated files, this speeds up finding
+ FileUtils.rm Dir['locale/**/gitlab.*']
+ # remove the `pot` file to ensure it's completely regenerated
+ FileUtils.rm_f pot_file
+
+ Rake::Task['gettext:find'].invoke
+
+ # leave only the required changes.
+ `git checkout -- locale/*/gitlab.po`
+
+ # Remove timestamps from the pot file
+ pot_content = File.read pot_file
+ pot_content.gsub!(/^"POT?\-(?:Creation|Revision)\-Date\:.*\n/, '')
+ File.write pot_file, pot_content
+
+ puts <<~MSG
+ All done. Please commit the changes to `locale/gitlab.pot`.
+
+ MSG
+ end
+
desc 'Lint all po files in `locale/'
task lint: :environment do
require 'simple_po_parser'
@@ -50,6 +72,40 @@ namespace :gettext do
end
end
+ task :updated_check do
+ pot_file = 'locale/gitlab.pot'
+ # Removing all pre-translated files speeds up `gettext:find` as the
+ # files don't need to be merged.
+ # Having `LC_MESSAGES/gitlab.mo files present also confuses the output.
+ FileUtils.rm Dir['locale/**/gitlab.*']
+ FileUtils.rm_f pot_file
+
+ # `gettext:find` writes touches to temp files to `stderr` which would cause
+ # `static-analysis` to report failures. We can ignore these.
+ silence_stream($stderr) do
+ Rake::Task['gettext:find'].invoke
+ end
+
+ pot_diff = `git diff -- #{pot_file} | grep -E '^(\\+|-)msgid'`.strip
+
+ # reset the locale folder for potential next tasks
+ `git checkout -- locale`
+
+ if pot_diff.present?
+ raise <<~MSG
+ Newly translated strings found, please add them to `#{pot_file}` by running:
+
+ bin/rake gettext:regenerate
+
+ Then commit and push the resulting changes to `#{pot_file}`.
+
+ The diff was:
+
+ #{pot_diff}
+ MSG
+ end
+ end
+
def report_errors_for_file(file, errors_for_file)
puts "Errors in `#{file}`:"
diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake
index 24e37f6c6cc..e96fbb64372 100644
--- a/lib/tasks/gitlab/backup.rake
+++ b/lib/tasks/gitlab/backup.rake
@@ -6,7 +6,6 @@ namespace :gitlab do
desc "GitLab | Create a backup of the GitLab system"
task create: :gitlab_environment do
warn_user_is_not_gitlab
- configure_cron_mode
Rake::Task["gitlab:backup:db:create"].invoke
Rake::Task["gitlab:backup:repo:create"].invoke
@@ -17,7 +16,7 @@ namespace :gitlab do
Rake::Task["gitlab:backup:lfs:create"].invoke
Rake::Task["gitlab:backup:registry:create"].invoke
- backup = Backup::Manager.new
+ backup = Backup::Manager.new(progress)
backup.pack
backup.cleanup
backup.remove_old
@@ -27,9 +26,8 @@ namespace :gitlab do
desc 'GitLab | Restore a previously created backup'
task restore: :gitlab_environment do
warn_user_is_not_gitlab
- configure_cron_mode
- backup = Backup::Manager.new
+ backup = Backup::Manager.new(progress)
backup.unpack
unless backup.skipped?('db')
@@ -49,9 +47,9 @@ namespace :gitlab do
# 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)
+ progress.puts 'Cleaning the database ... '.color(:blue)
Rake::Task['gitlab:db:drop_tables'].invoke
- $progress.puts 'done'.color(:green)
+ progress.puts 'done'.color(:green)
Rake::Task['gitlab:backup:db:restore'].invoke
rescue Gitlab::TaskAbortedByUserError
puts "Quitting...".color(:red)
@@ -74,173 +72,173 @@ namespace :gitlab do
namespace :repo do
task create: :gitlab_environment do
- $progress.puts "Dumping repositories ...".color(:blue)
+ progress.puts "Dumping repositories ...".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("repositories")
- $progress.puts "[SKIPPED]".color(:cyan)
+ progress.puts "[SKIPPED]".color(:cyan)
else
- Backup::Repository.new.dump
- $progress.puts "done".color(:green)
+ Backup::Repository.new(progress).dump
+ progress.puts "done".color(:green)
end
end
task restore: :gitlab_environment do
- $progress.puts "Restoring repositories ...".color(:blue)
- Backup::Repository.new.restore
- $progress.puts "done".color(:green)
+ progress.puts "Restoring repositories ...".color(:blue)
+ Backup::Repository.new(progress).restore
+ progress.puts "done".color(:green)
end
end
namespace :db do
task create: :gitlab_environment do
- $progress.puts "Dumping database ... ".color(:blue)
+ progress.puts "Dumping database ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("db")
- $progress.puts "[SKIPPED]".color(:cyan)
+ progress.puts "[SKIPPED]".color(:cyan)
else
- Backup::Database.new.dump
- $progress.puts "done".color(:green)
+ Backup::Database.new(progress).dump
+ progress.puts "done".color(:green)
end
end
task restore: :gitlab_environment do
- $progress.puts "Restoring database ... ".color(:blue)
- Backup::Database.new.restore
- $progress.puts "done".color(:green)
+ progress.puts "Restoring database ... ".color(:blue)
+ Backup::Database.new(progress).restore
+ progress.puts "done".color(:green)
end
end
namespace :builds do
task create: :gitlab_environment do
- $progress.puts "Dumping builds ... ".color(:blue)
+ progress.puts "Dumping builds ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("builds")
- $progress.puts "[SKIPPED]".color(:cyan)
+ progress.puts "[SKIPPED]".color(:cyan)
else
- Backup::Builds.new.dump
- $progress.puts "done".color(:green)
+ Backup::Builds.new(progress).dump
+ progress.puts "done".color(:green)
end
end
task restore: :gitlab_environment do
- $progress.puts "Restoring builds ... ".color(:blue)
- Backup::Builds.new.restore
- $progress.puts "done".color(:green)
+ progress.puts "Restoring builds ... ".color(:blue)
+ Backup::Builds.new(progress).restore
+ progress.puts "done".color(:green)
end
end
namespace :uploads do
task create: :gitlab_environment do
- $progress.puts "Dumping uploads ... ".color(:blue)
+ progress.puts "Dumping uploads ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("uploads")
- $progress.puts "[SKIPPED]".color(:cyan)
+ progress.puts "[SKIPPED]".color(:cyan)
else
- Backup::Uploads.new.dump
- $progress.puts "done".color(:green)
+ Backup::Uploads.new(progress).dump
+ progress.puts "done".color(:green)
end
end
task restore: :gitlab_environment do
- $progress.puts "Restoring uploads ... ".color(:blue)
- Backup::Uploads.new.restore
- $progress.puts "done".color(:green)
+ progress.puts "Restoring uploads ... ".color(:blue)
+ Backup::Uploads.new(progress).restore
+ progress.puts "done".color(:green)
end
end
namespace :artifacts do
task create: :gitlab_environment do
- $progress.puts "Dumping artifacts ... ".color(:blue)
+ progress.puts "Dumping artifacts ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("artifacts")
- $progress.puts "[SKIPPED]".color(:cyan)
+ progress.puts "[SKIPPED]".color(:cyan)
else
- Backup::Artifacts.new.dump
- $progress.puts "done".color(:green)
+ Backup::Artifacts.new(progress).dump
+ progress.puts "done".color(:green)
end
end
task restore: :gitlab_environment do
- $progress.puts "Restoring artifacts ... ".color(:blue)
- Backup::Artifacts.new.restore
- $progress.puts "done".color(:green)
+ progress.puts "Restoring artifacts ... ".color(:blue)
+ Backup::Artifacts.new(progress).restore
+ progress.puts "done".color(:green)
end
end
namespace :pages do
task create: :gitlab_environment do
- $progress.puts "Dumping pages ... ".color(:blue)
+ progress.puts "Dumping pages ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("pages")
- $progress.puts "[SKIPPED]".color(:cyan)
+ progress.puts "[SKIPPED]".color(:cyan)
else
- Backup::Pages.new.dump
- $progress.puts "done".color(:green)
+ Backup::Pages.new(progress).dump
+ progress.puts "done".color(:green)
end
end
task restore: :gitlab_environment do
- $progress.puts "Restoring pages ... ".color(:blue)
- Backup::Pages.new.restore
- $progress.puts "done".color(:green)
+ progress.puts "Restoring pages ... ".color(:blue)
+ Backup::Pages.new(progress).restore
+ progress.puts "done".color(:green)
end
end
namespace :lfs do
task create: :gitlab_environment do
- $progress.puts "Dumping lfs objects ... ".color(:blue)
+ progress.puts "Dumping lfs objects ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("lfs")
- $progress.puts "[SKIPPED]".color(:cyan)
+ progress.puts "[SKIPPED]".color(:cyan)
else
- Backup::Lfs.new.dump
- $progress.puts "done".color(:green)
+ Backup::Lfs.new(progress).dump
+ progress.puts "done".color(:green)
end
end
task restore: :gitlab_environment do
- $progress.puts "Restoring lfs objects ... ".color(:blue)
- Backup::Lfs.new.restore
- $progress.puts "done".color(:green)
+ progress.puts "Restoring lfs objects ... ".color(:blue)
+ Backup::Lfs.new(progress).restore
+ progress.puts "done".color(:green)
end
end
namespace :registry do
task create: :gitlab_environment do
- $progress.puts "Dumping container registry images ... ".color(:blue)
+ progress.puts "Dumping container registry images ... ".color(:blue)
if Gitlab.config.registry.enabled
if ENV["SKIP"] && ENV["SKIP"].include?("registry")
- $progress.puts "[SKIPPED]".color(:cyan)
+ progress.puts "[SKIPPED]".color(:cyan)
else
- Backup::Registry.new.dump
- $progress.puts "done".color(:green)
+ Backup::Registry.new(progress).dump
+ progress.puts "done".color(:green)
end
else
- $progress.puts "[DISABLED]".color(:cyan)
+ progress.puts "[DISABLED]".color(:cyan)
end
end
task restore: :gitlab_environment do
- $progress.puts "Restoring container registry images ... ".color(:blue)
+ progress.puts "Restoring container registry images ... ".color(:blue)
if Gitlab.config.registry.enabled
- Backup::Registry.new.restore
- $progress.puts "done".color(:green)
+ Backup::Registry.new(progress).restore
+ progress.puts "done".color(:green)
else
- $progress.puts "[DISABLED]".color(:cyan)
+ progress.puts "[DISABLED]".color(:cyan)
end
end
end
- def configure_cron_mode
+ def progress
if ENV['CRON']
# We need an object we can say 'puts' and 'print' to; let's use a
# StringIO.
require 'stringio'
- $progress = StringIO.new
+ StringIO.new
else
- $progress = $stdout
+ $stdout
end
end
end # namespace end: backup
diff --git a/lib/tasks/gitlab/bulk_add_permission.rake b/lib/tasks/gitlab/bulk_add_permission.rake
index 83dd870fa31..26cbf0740b6 100644
--- a/lib/tasks/gitlab/bulk_add_permission.rake
+++ b/lib/tasks/gitlab/bulk_add_permission.rake
@@ -1,6 +1,6 @@
namespace :gitlab do
namespace :import do
- desc "GitLab | Add all users to all projects (admin users are added as masters)"
+ desc "GitLab | Add all users to all projects (admin users are added as maintainers)"
task all_users_to_all_projects: :environment do |t, args|
user_ids = User.where(admin: false).pluck(:id)
admin_ids = User.where(admin: true).pluck(:id)
@@ -10,7 +10,7 @@ namespace :gitlab do
ProjectMember.add_users_to_projects(project_ids, user_ids, ProjectMember::DEVELOPER)
puts "Importing #{admin_ids.size} admins into #{project_ids.size} projects"
- ProjectMember.add_users_to_projects(project_ids, admin_ids, ProjectMember::MASTER)
+ ProjectMember.add_users_to_projects(project_ids, admin_ids, ProjectMember::MAINTAINER)
end
desc "GitLab | Add a specific user to all projects (as a developer)"
diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index c04dae7446f..a8acafa9cd9 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -44,11 +44,13 @@ namespace :gitlab do
start_checking "GitLab Shell"
check_gitlab_shell
- check_repo_base_exists
- check_repo_base_is_not_symlink
- check_repo_base_user_and_group
- check_repo_base_permissions
- check_repos_hooks_directory_is_link
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ check_repo_base_exists
+ check_repo_base_is_not_symlink
+ check_repo_base_user_and_group
+ check_repo_base_permissions
+ check_repos_hooks_directory_is_link
+ end
check_gitlab_shell_self_test
finished_checking "GitLab Shell"
diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake
index d6d15285489..52ae1330d7f 100644
--- a/lib/tasks/gitlab/cleanup.rake
+++ b/lib/tasks/gitlab/cleanup.rake
@@ -12,7 +12,7 @@ namespace :gitlab do
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.legacy_disk_path
+ git_base_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access { repository_storage.legacy_disk_path }
all_dirs = Dir.glob(git_base_path + '/*')
puts git_base_path.color(:yellow)
@@ -54,7 +54,8 @@ namespace :gitlab do
move_suffix = "+orphaned+#{Time.now.to_i}"
Gitlab.config.repositories.storages.each do |name, repository_storage|
- repo_root = repository_storage.legacy_disk_path
+ repo_root = Gitlab::GitalyClient::StorageSettings.allow_disk_access { repository_storage.legacy_disk_path }
+
# Look for global repos (legacy, depth 1) and normal repos (depth 2)
IO.popen(%W(find #{repo_root} -mindepth 1 -maxdepth 2 -name *.git)) do |find|
find.each_line do |path|
diff --git a/lib/tasks/gitlab/db.rake b/lib/tasks/gitlab/db.rake
index 139ab70e125..69166851816 100644
--- a/lib/tasks/gitlab/db.rake
+++ b/lib/tasks/gitlab/db.rake
@@ -46,7 +46,9 @@ namespace :gitlab do
desc 'Configures the database by running migrate, or by loading the schema and seeding if needed'
task configure: :environment do
- if ActiveRecord::Base.connection.tables.any?
+ # Check if we have existing db tables
+ # The schema_migrations table will still exist if drop_tables was called
+ if ActiveRecord::Base.connection.tables.count > 1
Rake::Task['db:migrate'].invoke
else
Rake::Task['db:schema:load'].invoke
diff --git a/lib/tasks/gitlab/import_export.rake b/lib/tasks/gitlab/import_export.rake
index 44074397c05..900dbf7be24 100644
--- a/lib/tasks/gitlab/import_export.rake
+++ b/lib/tasks/gitlab/import_export.rake
@@ -10,15 +10,22 @@ namespace :gitlab do
puts YAML.load_file(Gitlab::ImportExport.config_file)['project_tree'].to_yaml(SortKeys: true)
end
- desc 'GitLab | Bumps the Import/Export version for test_project_export.tar.gz'
- task bump_test_version: :environment do
- Dir.mktmpdir do |tmp_dir|
- system("tar -zxf spec/features/projects/import_export/test_project_export.tar.gz -C #{tmp_dir} > /dev/null")
- File.write(File.join(tmp_dir, 'VERSION'), Gitlab::ImportExport.version, mode: 'w')
- system("tar -zcvf spec/features/projects/import_export/test_project_export.tar.gz -C #{tmp_dir} . > /dev/null")
+ desc 'GitLab | Bumps the Import/Export version in fixtures and project templates'
+ task bump_version: :environment do
+ archives = Dir['vendor/project_templates/*.tar.gz']
+ archives.push('spec/features/projects/import_export/test_project_export.tar.gz')
+
+ archives.each do |archive|
+ raise ArgumentError unless File.exist?(archive)
+
+ Dir.mktmpdir do |tmp_dir|
+ system("tar -zxf #{archive} -C #{tmp_dir} > /dev/null")
+ File.write(File.join(tmp_dir, 'VERSION'), Gitlab::ImportExport.version, mode: 'w')
+ system("tar -zcvf #{archive} -C #{tmp_dir} . > /dev/null")
+ end
end
- puts "Updated to #{Gitlab::ImportExport.version}"
+ puts "Updated #{archives} to #{Gitlab::ImportExport.version}."
end
end
end
diff --git a/lib/tasks/gitlab/info.rake b/lib/tasks/gitlab/info.rake
index 47ed522aec3..6de739e9515 100644
--- a/lib/tasks/gitlab/info.rake
+++ b/lib/tasks/gitlab/info.rake
@@ -47,7 +47,7 @@ namespace :gitlab do
puts ""
puts "GitLab information".color(:yellow)
puts "Version:\t#{Gitlab::VERSION}"
- puts "Revision:\t#{Gitlab::REVISION}"
+ puts "Revision:\t#{Gitlab.revision}"
puts "Directory:\t#{Rails.root}"
puts "DB Adapter:\t#{database_adapter}"
puts "URL:\t\t#{Gitlab.config.gitlab.url}"
@@ -67,8 +67,10 @@ namespace :gitlab do
puts "GitLab Shell".color(:yellow)
puts "Version:\t#{gitlab_shell_version || "unknown".color(:red)}"
puts "Repository storage paths:"
- Gitlab.config.repositories.storages.each do |name, repository_storage|
- puts "- #{name}: \t#{repository_storage.legacy_disk_path}"
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ Gitlab.config.repositories.storages.each do |name, repository_storage|
+ puts "- #{name}: \t#{repository_storage.legacy_disk_path}"
+ end
end
puts "Hooks:\t\t#{Gitlab.config.gitlab_shell.hooks_path}"
puts "Git:\t\t#{Gitlab.config.git.bin_path}"
diff --git a/lib/tasks/gitlab/storage.rake b/lib/tasks/gitlab/storage.rake
index 6e8bd9078c8..f539b1df955 100644
--- a/lib/tasks/gitlab/storage.rake
+++ b/lib/tasks/gitlab/storage.rake
@@ -2,6 +2,24 @@ namespace :gitlab do
namespace :storage do
desc 'GitLab | Storage | Migrate existing projects to Hashed Storage'
task migrate_to_hashed: :environment do
+ storage_migrator = Gitlab::HashedStorage::Migrator.new
+ helper = Gitlab::HashedStorage::RakeHelper
+
+ if helper.range_single_item?
+ project = Project.with_unmigrated_storage.find_by(id: helper.range_from)
+
+ unless project
+ puts "There are no projects requiring storage migration with ID=#{helper.range_from}"
+
+ next
+ end
+
+ puts "Enqueueing storage migration of #{project.full_path} (ID=#{project.id})..."
+ storage_migrator.migrate(project)
+
+ next
+ end
+
legacy_projects_count = Project.with_unmigrated_storage.count
if legacy_projects_count == 0
@@ -10,10 +28,10 @@ namespace :gitlab do
next
end
- print "Enqueuing migration of #{legacy_projects_count} projects in batches of #{batch_size}"
+ print "Enqueuing migration of #{legacy_projects_count} projects in batches of #{helper.batch_size}"
- project_id_batches do |start, finish|
- StorageMigratorWorker.perform_async(start, finish)
+ helper.project_id_batches do |start, finish|
+ storage_migrator.bulk_schedule(start, finish)
print '.'
end
@@ -23,118 +41,50 @@ namespace :gitlab do
desc 'Gitlab | Storage | Summary of existing projects using Legacy Storage'
task legacy_projects: :environment do
- relation_summary('projects', Project.without_storage_feature(:repository))
+ helper = Gitlab::HashedStorage::RakeHelper
+ helper.relation_summary('projects using Legacy Storage', Project.without_storage_feature(:repository))
end
desc 'Gitlab | Storage | List existing projects using Legacy Storage'
task list_legacy_projects: :environment do
- projects_list('projects using Legacy Storage', Project.without_storage_feature(:repository))
+ helper = Gitlab::HashedStorage::RakeHelper
+ helper.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
- relation_summary('projects using Hashed Storage', Project.with_storage_feature(:repository))
+ helper = Gitlab::HashedStorage::RakeHelper
+ helper.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('projects using Hashed Storage', Project.with_storage_feature(:repository))
+ helper = Gitlab::HashedStorage::RakeHelper
+ helper.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)
+ helper = Gitlab::HashedStorage::RakeHelper
+ helper.relation_summary('attachments using Legacy Storage', helper.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)
+ helper = Gitlab::HashedStorage::RakeHelper
+ helper.attachments_list('attachments using Legacy Storage', helper.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)
+ helper = Gitlab::HashedStorage::RakeHelper
+ helper.relation_summary('attachments using Hashed Storage', helper.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
- ENV.fetch('BATCH', 200).to_i
- end
-
- def project_id_batches(&block)
- 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 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)
-
- relation_count
- end
-
- 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 relation_count > 0
-
- puts " ! Displaying first #{limit} #{relation_name}..." if relation_count > limit
-
- counter = 0
- projects.find_in_batches(batch_size: batch_size) do |batch|
- batch.each do |project|
- counter += 1
-
- puts " - #{project.full_path} (id: #{project.id})".color(:red)
-
- return if counter >= limit # rubocop:disable Lint/NonLocalExitFromIterator, Cop/AvoidReturnFromBlocks
- 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, Cop/AvoidReturnFromBlocks
- end
- end
+ helper = Gitlab::HashedStorage::RakeHelper
+ helper.attachments_list('attachments using Hashed Storage', helper.hashed_attachments_relation)
end
end
end
diff --git a/lib/tasks/gitlab/traces.rake b/lib/tasks/gitlab/traces.rake
index fd2a4f2d11a..ddcca69711f 100644
--- a/lib/tasks/gitlab/traces.rake
+++ b/lib/tasks/gitlab/traces.rake
@@ -8,9 +8,7 @@ namespace :gitlab do
logger = Logger.new(STDOUT)
logger.info('Archiving legacy traces')
- Ci::Build.finished
- .where('NOT EXISTS (?)',
- Ci::JobArtifact.select(1).trace.where('ci_builds.id = ci_job_artifacts.job_id'))
+ Ci::Build.finished.without_archived_trace
.order(id: :asc)
.find_in_batches(batch_size: 1000) do |jobs|
job_ids = jobs.map { |job| [job.id] }
diff --git a/lib/tasks/gitlab/uploads/migrate.rake b/lib/tasks/gitlab/uploads/migrate.rake
index 78e18992a8e..f548a266b99 100644
--- a/lib/tasks/gitlab/uploads/migrate.rake
+++ b/lib/tasks/gitlab/uploads/migrate.rake
@@ -8,7 +8,7 @@ namespace :gitlab do
@uploader_class = args.uploader_class.constantize
@model_class = args.model_class.constantize
- uploads.each_batch(of: batch_size, &method(:enqueue_batch)) # rubocop: disable Cop/InBatches
+ uploads.each_batch(of: batch_size, &method(:enqueue_batch))
end
def enqueue_batch(batch, index)
diff --git a/lib/tasks/import.rake b/lib/tasks/import.rake
index aafbe52e5f8..fc59b3f937d 100644
--- a/lib/tasks/import.rake
+++ b/lib/tasks/import.rake
@@ -9,7 +9,10 @@ class GithubImport
def initialize(token, gitlab_username, project_path, extras)
@options = { token: token }
@project_path = project_path
- @current_user = User.find_by_username(gitlab_username)
+ @current_user = User.find_by(username: gitlab_username)
+
+ raise "GitLab user #{gitlab_username} not found. Please specify a valid username." unless @current_user
+
@github_repo = extras.empty? ? nil : extras.first
end
@@ -50,7 +53,7 @@ class GithubImport
end
if import_success
- @project.import_finish
+ @project.after_import
puts "Import finished. Timings: #{timings}".color(:green)
else
puts "Import was not successful. Errors were as follows:"
diff --git a/lib/tasks/lint.rake b/lib/tasks/lint.rake
index fe5032cae18..006fcdd31a4 100644
--- a/lib/tasks/lint.rake
+++ b/lib/tasks/lint.rake
@@ -17,45 +17,54 @@ unless Rails.env.production?
Rake::Task['eslint'].invoke
end
+ desc "GitLab | lint | Lint HAML files"
+ task :haml do
+ begin
+ Rake::Task['haml_lint'].invoke
+ rescue RuntimeError # The haml_lint tasks raise a RuntimeError
+ exit(1)
+ end
+ end
+
desc "GitLab | lint | Run several lint checks"
task :all do
status = 0
%w[
config_lint
- haml_lint
+ lint:haml
scss_lint
flay
gettext:lint
+ gettext:updated_check
lint:static_verification
].each do |task|
pid = Process.fork do
- rd, wr = IO.pipe
+ rd_out, wr_out = IO.pipe
+ rd_err, wr_err = IO.pipe
stdout = $stdout.dup
stderr = $stderr.dup
- $stdout.reopen(wr)
- $stderr.reopen(wr)
+ $stdout.reopen(wr_out)
+ $stderr.reopen(wr_err)
begin
- begin
- Rake::Task[task].invoke
- rescue RuntimeError # The haml_lint tasks raise a RuntimeError
- exit(1)
- end
+ Rake::Task[task].invoke
rescue SystemExit => ex
- msg = "*** Rake task #{task} failed with the following error(s):"
+ msg = "*** Rake task #{task} exited:"
+ raise ex
+ rescue => ex
+ msg = "*** Rake task #{task} raised #{ex.class}:"
raise ex
ensure
$stdout.reopen(stdout)
$stderr.reopen(stderr)
- wr.close
+ wr_out.close
+ wr_err.close
+
+ warn "\n#{msg}\n\n" if msg
- if msg
- warn "\n#{msg}\n\n"
- IO.copy_stream(rd, $stderr)
- else
- IO.copy_stream(rd, $stdout)
- end
+ IO.copy_stream(rd_out, $stdout)
+ IO.copy_stream(rd_err, $stderr)
end
end
diff --git a/lib/tasks/migrate/setup_postgresql.rake b/lib/tasks/migrate/setup_postgresql.rake
index e7aab50e42a..f69d204c579 100644
--- a/lib/tasks/migrate/setup_postgresql.rake
+++ b/lib/tasks/migrate/setup_postgresql.rake
@@ -22,3 +22,18 @@ task setup_postgresql: :environment do
ProjectNameLowerIndex.new.up
AddPathIndexToRedirectRoutes.new.up
end
+
+desc 'GitLab | Generate PostgreSQL Password Hash'
+task :postgresql_md5_hash do
+ require 'digest'
+ username = ENV.fetch('USERNAME') do |missing|
+ puts "You must provide an username with '#{missing}' ENV variable"
+ exit(1)
+ end
+ password = ENV.fetch('PASSWORD') do |missing|
+ puts "You must provide a password with '#{missing}' ENV variable"
+ exit(1)
+ end
+ hash = Digest::MD5.hexdigest("#{password}#{username}")
+ puts "The MD5 hash of your database password for user: #{username} -> #{hash}"
+end
diff --git a/lib/tasks/tokens.rake b/lib/tasks/tokens.rake
index 693597afdf8..81829668de8 100644
--- a/lib/tasks/tokens.rake
+++ b/lib/tasks/tokens.rake
@@ -6,9 +6,9 @@ namespace :tokens do
reset_all_users_token(:reset_incoming_email_token!)
end
- desc "Reset all GitLab RSS tokens"
- task reset_all_rss: :environment do
- reset_all_users_token(:reset_rss_token!)
+ desc "Reset all GitLab feed tokens"
+ task reset_all_feed: :environment do
+ reset_all_users_token(:reset_feed_token!)
end
def reset_all_users_token(reset_token_method)
@@ -31,8 +31,8 @@ class TmpUser < ActiveRecord::Base
save!(validate: false)
end
- def reset_rss_token!
- write_new_token(:rss_token)
+ def reset_feed_token!
+ write_new_token(:feed_token)
save!(validate: false)
end
end
diff --git a/lib/uploaded_file.rb b/lib/uploaded_file.rb
index 5dc85b2baea..4b9cb59eab5 100644
--- a/lib/uploaded_file.rb
+++ b/lib/uploaded_file.rb
@@ -28,7 +28,7 @@ class UploadedFile
@tempfile = File.new(path, 'rb')
end
- def self.from_params(params, field, upload_path)
+ def self.from_params(params, field, upload_paths)
unless params["#{field}.path"]
raise InvalidPathError, "file is invalid" if params["#{field}.remote_id"]
@@ -37,7 +37,8 @@ class UploadedFile
file_path = File.realpath(params["#{field}.path"])
- unless self.allowed_path?(file_path, [upload_path, Dir.tmpdir].compact)
+ paths = Array(upload_paths) << Dir.tmpdir
+ unless self.allowed_path?(file_path, paths.compact)
raise InvalidPathError, "insecure path used '#{file_path}'"
end
diff --git a/locale/bg/gitlab.po b/locale/bg/gitlab.po
index a170014844f..e257da6e1a3 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: 2018-04-04 19:35+0200\n"
-"PO-Revision-Date: 2018-04-05 03:34-0400\n"
+"POT-Creation-Date: 2018-07-01 16:35+1000\n"
+"PO-Revision-Date: 2018-07-01 11:01\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Bulgarian\n"
"Language: bg_BG\n"
@@ -16,8 +16,10 @@ msgstr ""
"X-Crowdin-Language: bg\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-msgid " and"
-msgstr ""
+msgid "%d changed file"
+msgid_plural "%d changed files"
+msgstr[0] ""
+msgstr[1] ""
msgid "%d commit"
msgid_plural "%d commits"
@@ -54,6 +56,16 @@ msgid_plural "%d metrics"
msgstr[0] ""
msgstr[1] ""
+msgid "%d staged change"
+msgid_plural "%d staged changes"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "%d unstaged change"
+msgid_plural "%d unstaged changes"
+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 подаване беше пропуÑнато, за да не Ñе натоварва ÑиÑтемата."
@@ -70,12 +82,21 @@ msgid_plural "%{count} participants"
msgstr[0] ""
msgstr[1] ""
+msgid "%{filePath} deleted"
+msgstr ""
+
+msgid "%{group_docs_link_start}Groups%{group_docs_link_end} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects."
+msgstr ""
+
msgid "%{loadingIcon} Started"
msgstr ""
msgid "%{lock_path} is locked by GitLab User %{lock_user_id}"
msgstr ""
+msgid "%{nip_domain} can be used as an alternative to a custom domain."
+msgstr ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -88,6 +109,9 @@ msgstr ""
msgid "%{openOrClose} %{noteable}"
msgstr ""
+msgid "%{percent}%% complete"
+msgstr ""
+
msgid "%{storage_name}: failed storage access attempt on host:"
msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:"
msgstr[0] ""
@@ -96,15 +120,62 @@ msgstr[1] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{title} changes"
+msgstr ""
+
+msgid "%{unstaged} unstaged and %{staged} staged changes"
+msgstr ""
+
msgid "(checkout the %{link} for information on how to install it)."
msgstr ""
msgid "+ %{moreCount} more"
msgstr ""
+msgid "- Runner is active and can process any new jobs"
+msgstr ""
+
+msgid "- Runner is paused and will not receive any new jobs"
+msgstr ""
+
msgid "- show less"
msgstr ""
+msgid "1 %{type} addition"
+msgid_plural "%{count} %{type} additions"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 %{type} modification"
+msgid_plural "%{count} %{type} modifications"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 closed issue"
+msgid_plural "%d closed issues"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 closed merge request"
+msgid_plural "%d closed merge requests"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 merged merge request"
+msgid_plural "%d merged merge requests"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 open issue"
+msgid_plural "%d open issues"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 open merge request"
+msgid_plural "%d open merge requests"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] "1 Ñхема"
@@ -116,9 +187,27 @@ msgstr ""
msgid "2FA enabled"
msgstr ""
+msgid "403|Please contact your GitLab administrator to get the permission."
+msgstr ""
+
+msgid "403|You don't have the permission to access this page."
+msgstr ""
+
+msgid "404|Make sure the address is correct and the page hasn't moved."
+msgstr ""
+
+msgid "404|Page Not Found"
+msgstr ""
+
+msgid "404|Please contact your GitLab administrator if you think this is a mistake."
+msgstr ""
+
msgid "<strong>Removes</strong> source branch"
msgstr ""
+msgid "A 'Runner' is a process which runs a job. You can setup as many Runners as you need."
+msgstr ""
+
msgid "A collection of graphs regarding Continuous Integration"
msgstr "Ðабор от графики отноÑно непрекъÑнатата интеграциÑ"
@@ -128,6 +217,9 @@ msgstr ""
msgid "A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}."
msgstr ""
+msgid "A regular expression that will be used to find the test coverage output in the job trace. Leave blank to disable"
+msgstr ""
+
msgid "A user with write access to the source branch selected this option"
msgstr ""
@@ -140,36 +232,39 @@ msgstr ""
msgid "Abuse reports"
msgstr ""
+msgid "Accept terms"
+msgstr ""
+
msgid "Access Tokens"
msgstr ""
msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again."
msgstr ""
+msgid "Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report."
+msgstr ""
+
msgid "Account"
msgstr ""
-msgid "Account and limit settings"
+msgid "Account and limit"
msgstr ""
msgid "Active"
msgstr "Ðктивно"
+msgid "Active Sessions"
+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 Kubernetes cluster"
msgstr ""
@@ -182,6 +277,9 @@ msgstr ""
msgid "Add new directory"
msgstr "ДобавÑне на нова папка"
+msgid "Add reaction"
+msgstr ""
+
msgid "Add todo"
msgstr ""
@@ -251,7 +349,10 @@ msgstr ""
msgid "All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings."
msgstr ""
-msgid "Allow edits from maintainers."
+msgid "Allow commits from members who can merge to the target branch."
+msgstr ""
+
+msgid "Allow public access to pipelines and job details, including output logs and artifacts"
msgstr ""
msgid "Allow rendering of PlantUML diagrams in Asciidoc documents."
@@ -263,31 +364,37 @@ msgstr ""
msgid "Allows you to add and manage Kubernetes clusters."
msgstr ""
-msgid "Also called \"Issuer\" or \"Relying party trust identifier\""
+msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
msgstr ""
-msgid "Also called \"Relying party service URL\" or \"Reply URL\""
+msgid "An error occured creating the new branch."
msgstr ""
-msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to connect."
+msgid "An error occured whilst loading all the files."
msgstr ""
-msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
+msgid "An error occured whilst loading the file content."
msgstr ""
-msgid "An error occurred previewing the blob"
+msgid "An error occured whilst loading the file."
msgstr ""
-msgid "An error occurred when toggling the notification subscription"
+msgid "An error occured whilst loading the merge request changes."
+msgstr ""
+
+msgid "An error occured whilst loading the merge request version data."
+msgstr ""
+
+msgid "An error occured whilst loading the merge request."
msgstr ""
-msgid "An error occurred when updating the issue weight"
+msgid "An error occurred previewing the blob"
msgstr ""
-msgid "An error occurred while adding approver"
+msgid "An error occurred when toggling the notification subscription"
msgstr ""
-msgid "An error occurred while detecting host keys"
+msgid "An error occurred while dismissing the alert. Refresh the page and try again."
msgstr ""
msgid "An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again."
@@ -305,10 +412,7 @@ msgstr ""
msgid "An error occurred while getting projects"
msgstr ""
-msgid "An error occurred while importing project"
-msgstr ""
-
-msgid "An error occurred while initializing path locks"
+msgid "An error occurred while importing project: ${details}"
msgstr ""
msgid "An error occurred while loading commits"
@@ -326,9 +430,6 @@ msgstr ""
msgid "An error occurred while making the request."
msgstr ""
-msgid "An error occurred while removing approver"
-msgstr ""
-
msgid "An error occurred while rendering KaTeX"
msgstr ""
@@ -341,9 +442,6 @@ msgstr ""
msgid "An error occurred while retrieving diff"
msgstr ""
-msgid "An error occurred while saving LDAP override status. Please try again."
-msgstr ""
-
msgid "An error occurred while saving assignees"
msgstr ""
@@ -353,9 +451,6 @@ msgstr ""
msgid "An error occurred. Please try again."
msgstr ""
-msgid "Any Label"
-msgstr ""
-
msgid "Appearance"
msgstr ""
@@ -368,19 +463,19 @@ msgstr ""
msgid "April"
msgstr ""
-msgid "Archived project! Repository is read-only"
-msgstr "Ðрхивиран проект! Хранилището е Ñамо за четене"
+msgid "Archived project! Repository and other project resources are read-only"
+msgstr ""
msgid "Are you sure you want to delete this pipeline schedule?"
msgstr "ÐаиÑтина ли иÑкате да изтриете този план за Ñхема?"
-msgid "Are you sure you want to reset registration token?"
+msgid "Are you sure you want to remove this identity?"
msgstr ""
-msgid "Are you sure you want to reset the health check token?"
+msgid "Are you sure you want to reset registration token?"
msgstr ""
-msgid "Are you sure you want to unlock %{path_lock_path}?"
+msgid "Are you sure you want to reset the health check token?"
msgstr ""
msgid "Are you sure?"
@@ -389,7 +484,7 @@ msgstr ""
msgid "Artifacts"
msgstr ""
-msgid "Assertion consumer service URL"
+msgid "Ask your group maintainer to setup a group Runner."
msgstr ""
msgid "Assign custom color like #FF0000"
@@ -413,9 +508,15 @@ msgstr ""
msgid "Assigned to :name"
msgstr ""
+msgid "Assigned to me"
+msgstr ""
+
msgid "Assignee"
msgstr ""
+msgid "Assignee(s)"
+msgstr ""
+
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "Прикачете файл чрез влачене и пуÑкане или %{upload_link}"
@@ -449,7 +550,10 @@ msgstr ""
msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
msgstr ""
-msgid "AutoDevOps|Auto DevOps (Beta)"
+msgid "Auto-cancel redundant, pending pipelines"
+msgstr ""
+
+msgid "AutoDevOps|Auto DevOps"
msgstr ""
msgid "AutoDevOps|Auto DevOps documentation"
@@ -470,79 +574,109 @@ msgstr ""
msgid "AutoDevOps|add a Kubernetes cluster"
msgstr ""
-msgid "AutoDevOps|enable Auto DevOps (Beta)"
+msgid "AutoDevOps|enable Auto DevOps"
msgstr ""
msgid "Available"
msgstr ""
+msgid "Available group Runners : %{runners}"
+msgstr ""
+
+msgid "Available group Runners : %{runners}."
+msgstr ""
+
msgid "Avatar will be removed. Are you sure?"
msgstr ""
msgid "Average per day: %{average}"
msgstr ""
-msgid "Background Color"
+msgid "Background color"
msgstr ""
msgid "Background jobs"
msgstr ""
-msgid "Begin with the selected commit"
+msgid "Badges"
msgstr ""
-msgid "Billing"
+msgid "Badges|A new badge was added."
msgstr ""
-msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan."
+msgid "Badges|Add badge"
msgstr ""
-msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available."
+msgid "Badges|Adding the badge failed, please check the entered URLs and try again."
msgstr ""
-msgid "BillingPlans|Current plan"
+msgid "Badges|Badge image URL"
msgstr ""
-msgid "BillingPlans|Customer Support"
+msgid "Badges|Badge image preview"
msgstr ""
-msgid "BillingPlans|Downgrade"
+msgid "Badges|Delete badge"
msgstr ""
-msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
+msgid "Badges|Delete badge?"
msgstr ""
-msgid "BillingPlans|Manage plan"
+msgid "Badges|Deleting the badge failed, please try again."
msgstr ""
-msgid "BillingPlans|Please contact %{customer_support_link} in that case."
+msgid "Badges|Group Badge"
msgstr ""
-msgid "BillingPlans|See all %{plan_name} features"
+msgid "Badges|Link"
msgstr ""
-msgid "BillingPlans|This group uses the plan associated with its parent group."
+msgid "Badges|No badge image"
msgstr ""
-msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
+msgid "Badges|No image to preview"
msgstr ""
-msgid "BillingPlans|Upgrade"
+msgid "Badges|Project Badge"
msgstr ""
-msgid "BillingPlans|You are currently on the %{plan_link} plan."
+msgid "Badges|Reload badge image"
msgstr ""
-msgid "BillingPlans|frequently asked questions"
+msgid "Badges|Save changes"
msgstr ""
-msgid "BillingPlans|monthly"
+msgid "Badges|Saving the badge failed, please check the entered URLs and try again."
msgstr ""
-msgid "BillingPlans|paid annually at %{price_per_year}"
+msgid "Badges|The %{docsLinkStart}variables%{docsLinkEnd} GitLab supports: %{placeholders}"
msgstr ""
-msgid "BillingPlans|per user"
+msgid "Badges|The badge was deleted."
+msgstr ""
+
+msgid "Badges|The badge was saved."
+msgstr ""
+
+msgid "Badges|This group has no badges"
+msgstr ""
+
+msgid "Badges|This project has no badges"
+msgstr ""
+
+msgid "Badges|Your badges"
+msgstr ""
+
+msgid "Begin with the selected commit"
+msgstr ""
+
+msgid "Below are examples of regex for existing tools:"
+msgstr ""
+
+msgid "Boards"
+msgstr ""
+
+msgid "Branch %{branchName} was not found in this project's repository."
msgstr ""
msgid "Branch (%{branch_count})"
@@ -622,7 +756,7 @@ msgstr ""
msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
msgstr ""
-msgid "Branches|Only a project master or owner can delete a protected branch"
+msgid "Branches|Only a project maintainer or owner can delete a protected branch"
msgstr ""
msgid "Branches|Overview"
@@ -658,9 +792,6 @@ msgstr ""
msgid "Branches|Stale branches"
msgstr ""
-msgid "Branches|The branch could not be updated automatically because it has diverged from its upstream counterpart."
-msgstr ""
-
msgid "Branches|The default branch cannot be deleted"
msgstr ""
@@ -673,15 +804,9 @@ msgstr ""
msgid "Branches|To confirm, type %{branch_name_confirmation}:"
msgstr ""
-msgid "Branches|To discard the local changes and overwrite the branch with the upstream version, delete it here and choose 'Update Now' above."
-msgstr ""
-
msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
msgstr ""
-msgid "Branches|diverged from upstream"
-msgstr ""
-
msgid "Branches|merged"
msgstr ""
@@ -703,40 +828,79 @@ msgstr "Преглед на файловете"
msgid "Browse files"
msgstr "Разглеждане на файловете"
-msgid "Business"
-msgstr ""
-
msgid "ByAuthor|by"
msgstr "от"
msgid "CI / CD"
msgstr ""
-msgid "CI/CD"
+msgid "CI / CD Settings"
msgstr ""
msgid "CI/CD configuration"
msgstr ""
-msgid "CI/CD for external repo"
+msgid "CI/CD settings"
+msgstr ""
+
+msgid "CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery."
+msgstr ""
+
+msgid "CICD|Auto DevOps"
+msgstr ""
+
+msgid "CICD|Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration."
+msgstr ""
+
+msgid "CICD|Automatic deployment to staging, manual deployment to production"
+msgstr ""
+
+msgid "CICD|Continuous deployment to production"
+msgstr ""
+
+msgid "CICD|Deployment strategy"
+msgstr ""
+
+msgid "CICD|Deployment strategy needs a domain name to work correctly."
+msgstr ""
+
+msgid "CICD|Disable Auto DevOps"
+msgstr ""
+
+msgid "CICD|Enable Auto DevOps"
+msgstr ""
+
+msgid "CICD|Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific %{ci_file}."
+msgstr ""
+
+msgid "CICD|Instance default (%{state})"
msgstr ""
msgid "CICD|Jobs"
msgstr ""
-msgid "Cancel"
-msgstr "Отказ"
+msgid "CICD|Learn more about Auto DevOps"
+msgstr ""
-msgid "Cannot be merged automatically"
+msgid "CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project."
msgstr ""
-msgid "Cannot modify managed Kubernetes cluster"
+msgid "CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages."
+msgstr ""
+
+msgid "Can't find HEAD commit for this branch"
msgstr ""
-msgid "Certificate fingerprint"
+msgid "Cancel"
+msgstr "Отказ"
+
+msgid "Cancel this job"
msgstr ""
-msgid "Change Weight"
+msgid "Cannot be merged automatically"
+msgstr ""
+
+msgid "Cannot modify managed Kubernetes cluster"
msgstr ""
msgid "Change this value to influence how frequently the GitLab UI polls for updates."
@@ -790,21 +954,18 @@ msgstr ""
msgid "Choose a branch/tag (e.g. %{master}) or enter a commit (e.g. %{sha}) to see what's changed or to create a merge request."
msgstr ""
-msgid "Choose file..."
+msgid "Choose any color."
msgstr ""
-msgid "Choose which groups you wish to synchronize to this secondary node."
+msgid "Choose between <code>clone</code> or <code>fetch</code> to get the recent application code"
msgstr ""
-msgid "Choose which repositories you want to connect and run CI/CD pipelines."
+msgid "Choose file..."
msgstr ""
msgid "Choose which repositories you want to import."
msgstr ""
-msgid "Choose which shards you wish to synchronize to this secondary node."
-msgstr ""
-
msgid "CiStatusLabel|canceled"
msgstr "отказано"
@@ -874,21 +1035,12 @@ msgstr ""
msgid "CiVariable|All environments"
msgstr ""
-msgid "CiVariable|Create wildcard"
-msgstr ""
-
msgid "CiVariable|Error occured while saving variables"
msgstr ""
-msgid "CiVariable|New environment"
-msgstr ""
-
msgid "CiVariable|Protected"
msgstr ""
-msgid "CiVariable|Search environments"
-msgstr ""
-
msgid "CiVariable|Toggle protected"
msgstr ""
@@ -898,28 +1050,28 @@ msgstr ""
msgid "CircuitBreakerApiLink|circuitbreaker api"
msgstr ""
-msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
+msgid "Clear search input"
msgstr ""
-msgid "Click to expand text"
+msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Client authentication certificate"
+msgid "Click the <strong>Promote</strong> button in the top right corner to promote it to a group milestone."
msgstr ""
-msgid "Client authentication key"
+msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
msgstr ""
-msgid "Client authentication key password"
+msgid "Click to expand it."
msgstr ""
-msgid "Clone repository"
+msgid "Click to expand text"
msgstr ""
-msgid "Close"
+msgid "Clone repository"
msgstr ""
-msgid "Closed"
+msgid "Close"
msgstr ""
msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster"
@@ -937,6 +1089,12 @@ msgstr ""
msgid "ClusterIntegration|Advanced options on this Kubernetes cluster's integration"
msgstr ""
+msgid "ClusterIntegration|An error occured while trying to fetch project zones: %{error}"
+msgstr ""
+
+msgid "ClusterIntegration|An error occured while trying to fetch your projects: %{error}"
+msgstr ""
+
msgid "ClusterIntegration|Applications"
msgstr ""
@@ -967,6 +1125,9 @@ msgstr ""
msgid "ClusterIntegration|Copy Ingress IP Address to clipboard"
msgstr ""
+msgid "ClusterIntegration|Copy Jupyter Hostname to clipboard"
+msgstr ""
+
msgid "ClusterIntegration|Copy Kubernetes cluster name"
msgstr ""
@@ -982,7 +1143,7 @@ msgstr ""
msgid "ClusterIntegration|Create a new Kubernetes cluster on Google Kubernetes Engine right from GitLab"
msgstr ""
-msgid "ClusterIntegration|Create on GKE"
+msgid "ClusterIntegration|Create on Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
@@ -994,13 +1155,25 @@ msgstr ""
msgid "ClusterIntegration|Environment scope"
msgstr ""
+msgid "ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab's Google Kubernetes Engine Integration."
+msgstr ""
+
+msgid "ClusterIntegration|Fetching machine types"
+msgstr ""
+
+msgid "ClusterIntegration|Fetching projects"
+msgstr ""
+
+msgid "ClusterIntegration|Fetching zones"
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
msgid "ClusterIntegration|GitLab Runner"
msgstr ""
-msgid "ClusterIntegration|Google Cloud Platform project ID"
+msgid "ClusterIntegration|Google Cloud Platform project"
msgstr ""
msgid "ClusterIntegration|Google Kubernetes Engine"
@@ -1012,9 +1185,6 @@ msgstr ""
msgid "ClusterIntegration|Helm Tiller"
msgstr ""
-msgid "ClusterIntegration|In order to show the health of the cluster, we'll need to provision your cluster with Prometheus to collect the required data."
-msgstr ""
-
msgid "ClusterIntegration|Ingress"
msgstr ""
@@ -1024,9 +1194,6 @@ msgstr ""
msgid "ClusterIntegration|Install"
msgstr ""
-msgid "ClusterIntegration|Install Prometheus"
-msgstr ""
-
msgid "ClusterIntegration|Installed"
msgstr ""
@@ -1039,13 +1206,16 @@ msgstr ""
msgid "ClusterIntegration|Integration status"
msgstr ""
-msgid "ClusterIntegration|Kubernetes cluster"
+msgid "ClusterIntegration|Jupyter Hostname"
msgstr ""
-msgid "ClusterIntegration|Kubernetes cluster details"
+msgid "ClusterIntegration|JupyterHub"
msgstr ""
-msgid "ClusterIntegration|Kubernetes cluster health"
+msgid "ClusterIntegration|Kubernetes cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Kubernetes cluster details"
msgstr ""
msgid "ClusterIntegration|Kubernetes cluster integration"
@@ -1075,7 +1245,13 @@ msgstr ""
msgid "ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project"
msgstr ""
-msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
+msgid "ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}."
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{help_link_start}Kubernetes%{help_link_end}."
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}."
msgstr ""
msgid "ClusterIntegration|Learn more about environments"
@@ -1099,7 +1275,16 @@ msgstr ""
msgid "ClusterIntegration|More information"
msgstr ""
-msgid "ClusterIntegration|Multiple Kubernetes clusters are available in GitLab Enterprise Edition Premium and Ultimate"
+msgid "ClusterIntegration|No machine types matched your search"
+msgstr ""
+
+msgid "ClusterIntegration|No projects found"
+msgstr ""
+
+msgid "ClusterIntegration|No projects matched your search"
+msgstr ""
+
+msgid "ClusterIntegration|No zones matched your search"
msgstr ""
msgid "ClusterIntegration|Note:"
@@ -1114,9 +1299,6 @@ msgstr ""
msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
msgstr ""
-msgid "ClusterIntegration|Project ID"
-msgstr ""
-
msgid "ClusterIntegration|Project namespace"
msgstr ""
@@ -1129,6 +1311,9 @@ msgstr ""
msgid "ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration."
msgstr ""
+msgid "ClusterIntegration|Redeem up to $500 in free credit for Google Cloud Platform"
+msgstr ""
+
msgid "ClusterIntegration|Remove Kubernetes cluster integration"
msgstr ""
@@ -1144,19 +1329,37 @@ msgstr ""
msgid "ClusterIntegration|Save changes"
msgstr ""
+msgid "ClusterIntegration|Search machine types"
+msgstr ""
+
+msgid "ClusterIntegration|Search projects"
+msgstr ""
+
+msgid "ClusterIntegration|Search zones"
+msgstr ""
+
msgid "ClusterIntegration|Security"
msgstr ""
msgid "ClusterIntegration|See and edit the details for your Kubernetes cluster"
msgstr ""
-msgid "ClusterIntegration|See machine types"
+msgid "ClusterIntegration|Select machine type"
+msgstr ""
+
+msgid "ClusterIntegration|Select project"
+msgstr ""
+
+msgid "ClusterIntegration|Select project and zone to choose machine type"
+msgstr ""
+
+msgid "ClusterIntegration|Select project to choose zone"
msgstr ""
-msgid "ClusterIntegration|See your projects"
+msgid "ClusterIntegration|Select zone"
msgstr ""
-msgid "ClusterIntegration|See zones"
+msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
msgid "ClusterIntegration|Service token"
@@ -1189,6 +1392,9 @@ msgstr ""
msgid "ClusterIntegration|Token"
msgstr ""
+msgid "ClusterIntegration|Validating project billing status"
+msgstr ""
+
msgid "ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
@@ -1219,13 +1425,19 @@ msgstr ""
msgid "ClusterIntegration|properly configured"
msgstr ""
+msgid "ClusterIntegration|sign up"
+msgstr ""
+
msgid "Collapse"
msgstr ""
-msgid "Comment and resolve discussion"
+msgid "Collapse sidebar"
+msgstr ""
+
+msgid "Comment & resolve discussion"
msgstr ""
-msgid "Comment and unresolve discussion"
+msgid "Comment & unresolve discussion"
msgstr ""
msgid "Comments"
@@ -1292,6 +1504,9 @@ msgstr ""
msgid "Committed by"
msgstr "Подадено от"
+msgid "Commit…"
+msgstr ""
+
msgid "Compare"
msgstr "Сравнение"
@@ -1340,6 +1555,9 @@ msgstr ""
msgid "Configure limits for web and API requests."
msgstr ""
+msgid "Configure push mirrors."
+msgstr ""
+
msgid "Configure storage path and circuit breaker settings."
msgstr ""
@@ -1349,18 +1567,9 @@ msgstr ""
msgid "Connect"
msgstr ""
-msgid "Connect all repositories"
-msgstr ""
-
msgid "Connect repositories from GitHub"
msgstr ""
-msgid "Connect your external repositories, and CI/CD pipelines will run for new commits. A GitLab project will be created with only CI/CD features enabled."
-msgstr ""
-
-msgid "Connecting..."
-msgstr ""
-
msgid "Container Registry"
msgstr ""
@@ -1406,9 +1615,18 @@ 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 "ContainerRegistry|You can also use a %{deploy_token} for read-only access to the registry images."
+msgstr ""
+
+msgid "Continue"
+msgstr ""
+
msgid "Continuous Integration and Deployment"
msgstr ""
+msgid "Contribute to GitLab"
+msgstr ""
+
msgid "Contribution"
msgstr ""
@@ -1430,15 +1648,6 @@ msgstr ""
msgid "ContributorsPage|Please wait a moment, this page will automatically refresh when ready."
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 ""
-
msgid "Copy URL to clipboard"
msgstr "Копиране на адреÑа в буфера за обмен"
@@ -1451,9 +1660,18 @@ msgstr ""
msgid "Copy commit SHA to clipboard"
msgstr "Копиране на идентификатора на подаването в буфера за обмен"
+msgid "Copy file name to clipboard"
+msgstr ""
+
+msgid "Copy file path to clipboard"
+msgstr ""
+
msgid "Copy reference to clipboard"
msgstr ""
+msgid "Copy to clipboard"
+msgstr ""
+
msgid "Create"
msgstr ""
@@ -1472,15 +1690,15 @@ msgstr "Създайте Ñи личен жетон за доÑтъп в акаÑ
msgid "Create branch"
msgstr ""
+msgid "Create commit"
+msgstr ""
+
msgid "Create directory"
msgstr "Създаване на папка"
msgid "Create empty repository"
msgstr ""
-msgid "Create epic"
-msgstr ""
-
msgid "Create file"
msgstr ""
@@ -1523,13 +1741,10 @@ msgstr "Етикет"
msgid "CreateTokenToCloneLink|create a personal access token"
msgstr "Ñи Ñъздадете личен жетон за доÑтъп"
-msgid "Creates a new branch from %{branchName}"
+msgid "Created"
msgstr ""
-msgid "Creates a new branch from %{branchName} and re-directs to create a new merge request"
-msgstr ""
-
-msgid "Creating epic"
+msgid "Created by me"
msgstr ""
msgid "Cron Timezone"
@@ -1538,7 +1753,13 @@ msgstr "ЧаÑова зона за „Cron“"
msgid "Cron syntax"
msgstr "СинтакÑÐ¸Ñ Ð½Ð° „Cron“"
-msgid "Current node"
+msgid "CurrentUser|Profile"
+msgstr ""
+
+msgid "CurrentUser|Settings"
+msgstr ""
+
+msgid "Custom CI config path"
msgstr ""
msgid "Custom notification events"
@@ -1547,9 +1768,6 @@ 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 "Customize colors"
-msgstr ""
-
msgid "Cycle Analytics"
msgstr "Ðнализ на циклите"
@@ -1586,7 +1804,7 @@ msgstr ""
msgid "December"
msgstr ""
-msgid "Default classification label"
+msgid "Decline and sign out"
msgstr ""
msgid "Define a custom pattern with cron syntax"
@@ -1595,6 +1813,9 @@ msgstr "Задайте потребителÑки шаблон, използва
msgid "Delete"
msgstr "Изтриване"
+msgid "Delete list"
+msgstr ""
+
msgid "Deploy"
msgid_plural "Deploys"
msgstr[0] "ВнедрÑване"
@@ -1603,37 +1824,163 @@ msgstr[1] "ВнедрÑваниÑ"
msgid "Deploy Keys"
msgstr ""
-msgid "Description"
-msgstr "ОпиÑание"
+msgid "DeployKeys|+%{count} others"
+msgstr ""
+
+msgid "DeployKeys|Current project"
+msgstr ""
+
+msgid "DeployKeys|Deploy key"
+msgstr ""
+
+msgid "DeployKeys|Enabled deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Error enabling deploy key"
+msgstr ""
+
+msgid "DeployKeys|Error getting deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Error removing deploy key"
+msgstr ""
+
+msgid "DeployKeys|Expand %{count} other projects"
+msgstr ""
+
+msgid "DeployKeys|Loading deploy keys"
+msgstr ""
+
+msgid "DeployKeys|No deploy keys found. Create one with the form above."
+msgstr ""
+
+msgid "DeployKeys|Privately accessible deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Project usage"
+msgstr ""
+
+msgid "DeployKeys|Publicly accessible deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Read access only"
+msgstr ""
+
+msgid "DeployKeys|Write access allowed"
+msgstr ""
+
+msgid "DeployKeys|You are going to remove this deploy key. Are you sure?"
+msgstr ""
+
+msgid "DeployTokens|Active Deploy Tokens (%{active_tokens})"
+msgstr ""
+
+msgid "DeployTokens|Add a deploy token"
+msgstr ""
+
+msgid "DeployTokens|Allows read-only access to the registry images"
+msgstr ""
+
+msgid "DeployTokens|Allows read-only access to the repository"
+msgstr ""
+
+msgid "DeployTokens|Copy deploy token to clipboard"
+msgstr ""
+
+msgid "DeployTokens|Copy username to clipboard"
+msgstr ""
+
+msgid "DeployTokens|Create deploy token"
+msgstr ""
+
+msgid "DeployTokens|Created"
+msgstr ""
+
+msgid "DeployTokens|Deploy Tokens"
+msgstr ""
+
+msgid "DeployTokens|Deploy tokens allow read-only access to your repository and registry images."
+msgstr ""
+
+msgid "DeployTokens|Expires"
+msgstr ""
+
+msgid "DeployTokens|Name"
+msgstr ""
+
+msgid "DeployTokens|Pick a name for the application, and we'll give you a unique deploy token."
+msgstr ""
+
+msgid "DeployTokens|Revoke"
+msgstr ""
+
+msgid "DeployTokens|Revoke %{name}"
+msgstr ""
+
+msgid "DeployTokens|Scopes"
+msgstr ""
+
+msgid "DeployTokens|This action cannot be undone."
+msgstr ""
+
+msgid "DeployTokens|This project has no active Deploy Tokens."
+msgstr ""
+
+msgid "DeployTokens|Use this token as a password. Make sure you save it - you won't be able to access it again."
+msgstr ""
+
+msgid "DeployTokens|Use this username as a login."
+msgstr ""
+
+msgid "DeployTokens|Username"
+msgstr ""
+
+msgid "DeployTokens|You are about to revoke"
+msgstr ""
+
+msgid "DeployTokens|Your New Deploy Token"
+msgstr ""
-msgid "Description templates allow you to define context-specific templates for issue and merge request description fields for your project."
+msgid "DeployTokens|Your new project deploy token has been created."
msgstr ""
+msgid "Deprioritize label"
+msgstr ""
+
+msgid "Description"
+msgstr "ОпиÑание"
+
msgid "Details"
msgstr ""
msgid "Diffs|No file name available"
msgstr ""
+msgid "Diffs|Something went wrong while fetching diff lines."
+msgstr ""
+
msgid "Directory name"
msgstr "Име на папката"
msgid "Disable"
msgstr ""
-msgid "Discard draft"
+msgid "Disable for this project"
msgstr ""
-msgid "Discover GitLab Geo."
+msgid "Disable group Runners"
msgstr ""
-msgid "Dismiss Cycle Analytics introduction box"
+msgid "Discard changes"
msgstr ""
-msgid "Dismiss Merge Request promotion"
+msgid "Discard draft"
+msgstr ""
+
+msgid "Dismiss Cycle Analytics introduction box"
msgstr ""
-msgid "Documentation for popular identity providers"
+msgid "Domain"
msgstr ""
msgid "Don't show again"
@@ -1675,31 +2022,34 @@ msgstr ""
msgid "Due date"
msgstr ""
-msgid "During this process, you’ll be asked for URLs from GitLab’s side. Use the URLs shown below."
+msgid "Each Runner can be in one of the following states:"
msgstr ""
msgid "Edit"
msgstr "Редактиране"
+msgid "Edit Label"
+msgstr ""
+
msgid "Edit Pipeline Schedule %{id}"
msgstr "Редактиране на плана %{id} за Ñхема"
msgid "Edit files in the editor and commit changes here"
msgstr ""
-msgid "Editing"
+msgid "Edit identity for %{user_name}"
msgstr ""
-msgid "Elasticsearch"
+msgid "Email"
msgstr ""
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "Email patch"
msgstr ""
-msgid "Email"
+msgid "Emails"
msgstr ""
-msgid "Emails"
+msgid "Embed"
msgstr ""
msgid "Enable"
@@ -1708,9 +2058,6 @@ msgstr ""
msgid "Enable Auto DevOps"
msgstr ""
-msgid "Enable SAML authentication for this group"
-msgstr ""
-
msgid "Enable Sentry for error reporting and logging."
msgstr ""
@@ -1720,7 +2067,13 @@ msgstr ""
msgid "Enable and configure Prometheus metrics."
msgstr ""
-msgid "Enable classification control using an external service"
+msgid "Enable for this project"
+msgstr ""
+
+msgid "Enable group Runners"
+msgstr ""
+
+msgid "Enable or disable certain group features and choose access levels."
msgstr ""
msgid "Enable or disable version check and usage ping."
@@ -1732,7 +2085,10 @@ msgstr ""
msgid "Enable the Performance Bar for a given group."
msgstr ""
-msgid "Enabled"
+msgid "Ends at (UTC)"
+msgstr ""
+
+msgid "Environments"
msgstr ""
msgid "Environments|An error occurred while fetching the environments."
@@ -1783,43 +2139,40 @@ msgstr ""
msgid "Environments|You don't have any environments right now."
msgstr ""
-msgid "Epic will be removed! Are you sure?"
-msgstr ""
-
-msgid "Epics"
+msgid "Error Reporting and Logging"
msgstr ""
-msgid "Epics Roadmap"
+msgid "Error committing changes. Please try again."
msgstr ""
-msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
+msgid "Error fetching contributors data."
msgstr ""
-msgid "Error Reporting and Logging"
+msgid "Error fetching job trace"
msgstr ""
-msgid "Error checking branch data. Please try again."
+msgid "Error fetching labels."
msgstr ""
-msgid "Error committing changes. Please try again."
+msgid "Error fetching network graph."
msgstr ""
-msgid "Error creating epic"
+msgid "Error fetching refs"
msgstr ""
-msgid "Error fetching contributors data."
+msgid "Error fetching usage ping data."
msgstr ""
-msgid "Error fetching labels."
+msgid "Error loading branch data. Please try again."
msgstr ""
-msgid "Error fetching network graph."
+msgid "Error loading last commit."
msgstr ""
-msgid "Error fetching refs"
+msgid "Error loading merge requests."
msgstr ""
-msgid "Error fetching usage ping data."
+msgid "Error loading project data. Please try again."
msgstr ""
msgid "Error occurred when toggling the notification subscription"
@@ -1834,6 +2187,9 @@ msgstr ""
msgid "Error updating todo status."
msgstr ""
+msgid "Estimated"
+msgstr ""
+
msgid "EventFilterBy|Filter by all"
msgstr ""
@@ -1864,31 +2220,16 @@ msgstr "Ð’ÑÑка Ñедмица (в неделÑ, в 4 ч. Ñутринта)"
msgid "Expand"
msgstr ""
-msgid "Explore projects"
-msgstr ""
-
-msgid "Explore public groups"
-msgstr ""
-
-msgid "External Classification Policy Authorization"
+msgid "Expand all"
msgstr ""
-msgid "External authentication"
+msgid "Expand sidebar"
msgstr ""
-msgid "External authorization denied access to this project"
-msgstr ""
-
-msgid "External authorization request timeout"
-msgstr ""
-
-msgid "ExternalAuthorizationService|Classification Label"
-msgstr ""
-
-msgid "ExternalAuthorizationService|Classification label"
+msgid "Explore projects"
msgstr ""
-msgid "ExternalAuthorizationService|When no classification label is set the default label `%{default_label}` will be used."
+msgid "Explore public groups"
msgstr ""
msgid "Failed"
@@ -1900,6 +2241,9 @@ msgstr ""
msgid "Failed to change the owner"
msgstr "СобÑтвеникът не може да бъде променен"
+msgid "Failed to check related branches."
+msgstr ""
+
msgid "Failed to remove issue from board, please try again."
msgstr ""
@@ -1909,6 +2253,12 @@ msgstr "Планът за Ñхема не може да бъде премахнÐ
msgid "Failed to update issues, please try again."
msgstr ""
+msgid "Failure"
+msgstr ""
+
+msgid "Faster as it re-uses the project workspace (falling back to clone if it doesn't exist)"
+msgstr ""
+
msgid "Feb"
msgstr ""
@@ -1918,18 +2268,12 @@ msgstr ""
msgid "Fields on this page are now uneditable, you can configure"
msgstr ""
-msgid "File name"
-msgstr ""
-
msgid "Files"
msgstr "Файлове"
msgid "Files (%{human_size})"
msgstr ""
-msgid "Fill in the fields below, turn on <strong>%{enable_label}</strong>, and press <strong>%{save_changes}</strong>"
-msgstr ""
-
msgid "Filter by commit message"
msgstr "Филтриране по Ñъобщение"
@@ -1948,10 +2292,13 @@ msgstr "Първо"
msgid "FirstPushedBy|pushed by"
msgstr "изпращане на промени от"
-msgid "Font Color"
+msgid "For internal projects, any logged in user can view pipelines and access job details (output logs and artifacts)"
msgstr ""
-msgid "Footer message"
+msgid "For private projects, any member (guest or higher) can view pipelines and access job details (output logs and artifacts)"
+msgstr ""
+
+msgid "For public projects, anyone can view pipelines and access job details (output logs and artifacts)"
msgstr ""
msgid "Fork"
@@ -1971,6 +2318,9 @@ msgstr ""
msgid "Format"
msgstr ""
+msgid "Found errors in your .gitlab-ci.yml:"
+msgstr ""
+
msgid "From %{provider_title}"
msgstr ""
@@ -1986,166 +2336,13 @@ msgstr ""
msgid "GPG Keys"
msgstr ""
-msgid "Generate a default set of labels"
-msgstr ""
-
-msgid "Geo Nodes"
-msgstr ""
-
-msgid "Geo allows you to replicate your GitLab instance to other geographical locations."
-msgstr ""
-
-msgid "GeoNodeSyncStatus|Node is failing or broken."
-msgstr ""
-
-msgid "GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage."
-msgstr ""
-
-msgid "GeoNodes|Checksummed"
-msgstr ""
-
-msgid "GeoNodes|Database replication lag:"
-msgstr ""
-
-msgid "GeoNodes|Disabling a node stops the sync process. Are you sure?"
-msgstr ""
-
-msgid "GeoNodes|Does not match the primary storage configuration"
-msgstr ""
-
-msgid "GeoNodes|Failed"
-msgstr ""
-
-msgid "GeoNodes|Full"
-msgstr ""
-
-msgid "GeoNodes|GitLab version does not match the primary node version"
-msgstr ""
-
-msgid "GeoNodes|GitLab version:"
-msgstr ""
-
-msgid "GeoNodes|Health status:"
-msgstr ""
-
-msgid "GeoNodes|Last event ID processed by cursor:"
-msgstr ""
-
-msgid "GeoNodes|Last event ID seen from primary:"
-msgstr ""
-
-msgid "GeoNodes|Loading nodes"
-msgstr ""
-
-msgid "GeoNodes|Local Attachments:"
-msgstr ""
-
-msgid "GeoNodes|Local LFS objects:"
-msgstr ""
-
-msgid "GeoNodes|Local job artifacts:"
-msgstr ""
-
-msgid "GeoNodes|New node"
-msgstr ""
-
-msgid "GeoNodes|Node Authentication was successfully repaired."
-msgstr ""
-
-msgid "GeoNodes|Node was successfully removed."
-msgstr ""
-
-msgid "GeoNodes|Not checksummed"
-msgstr ""
-
-msgid "GeoNodes|Out of sync"
-msgstr ""
-
-msgid "GeoNodes|Removing a node stops the sync process. Are you sure?"
-msgstr ""
-
-msgid "GeoNodes|Replication slot WAL:"
-msgstr ""
-
-msgid "GeoNodes|Replication slots:"
-msgstr ""
-
-msgid "GeoNodes|Repositories checksummed:"
+msgid "General"
msgstr ""
-msgid "GeoNodes|Repositories:"
+msgid "General pipelines"
msgstr ""
-msgid "GeoNodes|Repository checksums verified:"
-msgstr ""
-
-msgid "GeoNodes|Selective"
-msgstr ""
-
-msgid "GeoNodes|Something went wrong while changing node status"
-msgstr ""
-
-msgid "GeoNodes|Something went wrong while removing node"
-msgstr ""
-
-msgid "GeoNodes|Something went wrong while repairing node"
-msgstr ""
-
-msgid "GeoNodes|Storage config:"
-msgstr ""
-
-msgid "GeoNodes|Sync settings:"
-msgstr ""
-
-msgid "GeoNodes|Synced"
-msgstr ""
-
-msgid "GeoNodes|Unused slots"
-msgstr ""
-
-msgid "GeoNodes|Unverified"
-msgstr ""
-
-msgid "GeoNodes|Used slots"
-msgstr ""
-
-msgid "GeoNodes|Verified"
-msgstr ""
-
-msgid "GeoNodes|Wiki checksums verified:"
-msgstr ""
-
-msgid "GeoNodes|Wikis checksummed:"
-msgstr ""
-
-msgid "GeoNodes|Wikis:"
-msgstr ""
-
-msgid "GeoNodes|You have configured Geo nodes using an insecure HTTP connection. We recommend the use of HTTPS."
-msgstr ""
-
-msgid "Geo|All projects"
-msgstr ""
-
-msgid "Geo|File sync capacity"
-msgstr ""
-
-msgid "Geo|Groups to synchronize"
-msgstr ""
-
-msgid "Geo|Projects in certain groups"
-msgstr ""
-
-msgid "Geo|Projects in certain storage shards"
-msgstr ""
-
-msgid "Geo|Repository sync capacity"
-msgstr ""
-
-msgid "Geo|Select groups to replicate."
-msgstr ""
-
-msgid "Geo|Shards to synchronize"
+msgid "Generate a default set of labels"
msgstr ""
msgid "Git repository URL"
@@ -2157,6 +2354,9 @@ msgstr ""
msgid "Git storage health information has been reset"
msgstr ""
+msgid "Git strategy for pipelines"
+msgstr ""
+
msgid "Git version"
msgstr ""
@@ -2166,21 +2366,24 @@ msgstr ""
msgid "GitLab CI Linter has been moved"
msgstr ""
-msgid "GitLab Geo"
+msgid "GitLab Group Runners can execute code for all the projects in this group."
msgstr ""
msgid "GitLab Runner section"
msgstr ""
-msgid "GitLab single sign on URL"
-msgstr ""
-
msgid "Gitaly"
msgstr ""
msgid "Gitaly Servers"
msgstr ""
+msgid "Gitaly|Address"
+msgstr ""
+
+msgid "Go Back"
+msgstr ""
+
msgid "Go back"
msgstr ""
@@ -2196,22 +2399,19 @@ msgstr ""
msgid "Got it!"
msgstr ""
-msgid "GroupRoadmap|Epics let you manage your portfolio of projects more efficiently and with less effort"
-msgstr ""
-
-msgid "GroupRoadmap|From %{dateWord}"
+msgid "Graph"
msgstr ""
-msgid "GroupRoadmap|Loading roadmap"
+msgid "Group CI/CD settings"
msgstr ""
-msgid "GroupRoadmap|Something went wrong while fetching epics"
+msgid "Group ID"
msgstr ""
-msgid "GroupRoadmap|To view the roadmap, add a planned start or finish date to one of your epics in this group or its subgroups. Only epics in the past 3 months and the next 3 months are shown &ndash; from %{startDate} to %{endDate}."
+msgid "Group Runners"
msgstr ""
-msgid "GroupRoadmap|Until %{dateWord}"
+msgid "Group maintainers can register group runners in the %{link}"
msgstr ""
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
@@ -2238,6 +2438,9 @@ msgstr ""
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
+msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
+msgstr ""
+
msgid "GroupsEmptyState|A group is a collection of several projects."
msgstr ""
@@ -2277,12 +2480,6 @@ msgstr ""
msgid "GroupsTree|Sorry, no groups or projects matched your search"
msgstr ""
-msgid "Have your users email"
-msgstr ""
-
-msgid "Header message"
-msgstr ""
-
msgid "Health Check"
msgstr ""
@@ -2315,19 +2512,49 @@ msgid_plural "Hide values"
msgstr[0] ""
msgstr[1] ""
+msgid "Hide whitespace changes"
+msgstr ""
+
msgid "History"
msgstr ""
msgid "Housekeeping successfully started"
msgstr "ОÑвежаването започна уÑпешно"
-msgid "Identity provider single sign on URL"
+msgid "I accept the %{terms_link}"
+msgstr ""
+
+msgid "I accept the|Terms of Service and Privacy Policy"
+msgstr ""
+
+msgid "ID"
+msgstr ""
+
+msgid "IDE|Commit"
+msgstr ""
+
+msgid "IDE|Edit"
+msgstr ""
+
+msgid "IDE|Go back"
+msgstr ""
+
+msgid "IDE|Open in file view"
+msgstr ""
+
+msgid "IDE|Review"
+msgstr ""
+
+msgid "Identifier"
+msgstr ""
+
+msgid "Identities"
msgstr ""
-msgid "If enabled, access to projects will be validated on an external service using their classification label."
+msgid "If disabled, the access level will depend on the user's permissions in the project."
msgstr ""
-msgid "If using GitHub, you’ll see pipeline statuses on GitHub for your commits and pull requests. %{more_info_link}"
+msgid "If enabled"
msgstr ""
msgid "If you already have files you can push them using the %{link_to_cli} below."
@@ -2336,6 +2563,15 @@ msgstr ""
msgid "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>."
msgstr ""
+msgid "ImageDiffViewer|2-up"
+msgstr ""
+
+msgid "ImageDiffViewer|Onion skin"
+msgstr ""
+
+msgid "ImageDiffViewer|Swipe"
+msgstr ""
+
msgid "Import"
msgstr ""
@@ -2351,16 +2587,10 @@ msgstr ""
msgid "Import repository"
msgstr "ВнаÑÑне на хранилище"
-msgid "ImportButtons|Connect repositories from"
+msgid "Include a Terms of Service agreement and Privacy Policy that all users must accept."
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."
+msgid "Inline"
msgstr ""
msgid "Install Runner on Kubernetes"
@@ -2369,17 +2599,15 @@ msgstr ""
msgid "Install a Runner compatible with GitLab CI"
msgstr ""
-msgid "Instance"
-msgid_plural "Instances"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "Instance does not support multiple Kubernetes clusters"
msgstr ""
msgid "Integrations"
msgstr ""
+msgid "Integrations Settings"
+msgstr ""
+
msgid "Interested parties can even contribute by pushing commits if they want to."
msgstr ""
@@ -2395,7 +2623,7 @@ msgstr "Шаблон за интервала"
msgid "Introducing Cycle Analytics"
msgstr "ПредÑтавÑме Ви анализа на циклите"
-msgid "Issue board focus mode"
+msgid "Issue Board"
msgstr ""
msgid "Issue events"
@@ -2404,9 +2632,6 @@ msgstr ""
msgid "IssueBoards|Board"
msgstr ""
-msgid "IssueBoards|Boards"
-msgstr ""
-
msgid "Issues"
msgstr ""
@@ -2419,6 +2644,12 @@ msgstr ""
msgid "January"
msgstr ""
+msgid "Job"
+msgstr ""
+
+msgid "Job has been erased"
+msgstr ""
+
msgid "Jobs"
msgstr ""
@@ -2461,6 +2692,9 @@ msgstr ""
msgid "Kubernetes service integration has been deprecated. %{deprecated_message_content} your Kubernetes clusters using the new <a href=\"%{url}\"/>Kubernetes Clusters</a> page"
msgstr ""
+msgid "LFS"
+msgstr ""
+
msgid "LFSStatus|Disabled"
msgstr "Изключено"
@@ -2470,6 +2704,9 @@ msgstr "Включено"
msgid "Label"
msgstr ""
+msgid "Label actions dropdown"
+msgstr ""
+
msgid "LabelSelect|%{firstLabelName} +%{remainingLabelCount} more"
msgstr ""
@@ -2485,6 +2722,9 @@ msgstr ""
msgid "Labels can be applied to issues and merge requests to categorize them."
msgstr ""
+msgid "Labels can be applied to issues and merge requests."
+msgstr ""
+
msgid "Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>"
msgstr ""
@@ -2520,6 +2760,9 @@ msgstr ""
msgid "LastPushEvent|at"
msgstr ""
+msgid "Latest changes"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -2544,9 +2787,6 @@ msgstr "ÐапуÑкане на групата"
msgid "Leave project"
msgstr "ÐапуÑкане на проекта"
-msgid "License"
-msgstr ""
-
msgid "List"
msgstr ""
@@ -2556,6 +2796,9 @@ msgstr ""
msgid "Loading the GitLab IDE..."
msgstr ""
+msgid "Loading..."
+msgstr ""
+
msgid "Lock"
msgstr ""
@@ -2565,21 +2808,18 @@ msgstr ""
msgid "Lock not found"
msgstr ""
-msgid "Locked"
+msgid "Lock to current projects"
msgstr ""
-msgid "Locked Files"
+msgid "Locked"
msgstr ""
-msgid "Locks give the ability to lock specific file or folder."
+msgid "Locked to current projects"
msgstr ""
msgid "Login"
msgstr ""
-msgid "Make everyone on your team more productive regardless of their location. GitLab Geo creates read-only mirrors of your GitLab instance so you can reduce the time it takes to clone and fetch large repos."
-msgstr ""
-
msgid "Manage all notifications"
msgstr ""
@@ -2592,16 +2832,16 @@ msgstr ""
msgid "Manage project labels"
msgstr ""
-msgid "Manage your group’s membership while adding another level of security with SAML."
-msgstr ""
-
msgid "Mar"
msgstr ""
msgid "March"
msgstr ""
-msgid "Mark done"
+msgid "Mark todo as done"
+msgstr ""
+
+msgid "Markdown enabled"
msgstr ""
msgid "Maximum git storage failures"
@@ -2616,7 +2856,7 @@ msgstr "Медиана"
msgid "Members"
msgstr ""
-msgid "Members will be forwarded here when signing in to your group. Get this from your identity provider, where it can also be called \"SSO Service Location\", \"SAML Token Issuance Endpoint\", or \"SAML 2.0/W-Federation URL\"."
+msgid "Merge Request:"
msgstr ""
msgid "Merge Requests"
@@ -2628,91 +2868,46 @@ msgstr ""
msgid "Merge request"
msgstr ""
-msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
-msgstr ""
-
-msgid "Merged"
-msgstr ""
-
-msgid "Messages"
-msgstr ""
-
-msgid "Metrics - Influx"
-msgstr ""
-
-msgid "Metrics - Prometheus"
-msgstr ""
-
-msgid "Metrics|Business"
-msgstr ""
-
-msgid "Metrics|Create metric"
-msgstr ""
-
-msgid "Metrics|Edit metric"
-msgstr ""
-
-msgid "Metrics|For grouping similar metrics"
-msgstr ""
-
-msgid "Metrics|Label of the chart's vertical axis. Usually the type of the unit being charted. The horizontal axis (X-axis) always represents time."
-msgstr ""
-
-msgid "Metrics|Legend label (optional)"
-msgstr ""
-
-msgid "Metrics|Must be a valid PromQL query."
-msgstr ""
-
-msgid "Metrics|Name"
-msgstr ""
-
-msgid "Metrics|New metric"
-msgstr ""
-
-msgid "Metrics|Prometheus Query Documentation"
+msgid "Merge requests"
msgstr ""
-msgid "Metrics|Query"
-msgstr ""
-
-msgid "Metrics|Response"
+msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
msgstr ""
-msgid "Metrics|System"
+msgid "MergeRequests|Resolve this discussion in a new issue"
msgstr ""
-msgid "Metrics|Type"
+msgid "MergeRequests|Saving the comment failed"
msgstr ""
-msgid "Metrics|Unit label"
+msgid "MergeRequests|Toggle comments for this file"
msgstr ""
-msgid "Metrics|Used as a title for the chart"
+msgid "MergeRequests|Updating discussions failed"
msgstr ""
-msgid "Metrics|Used if the query returns a single series. If it returns multiple series, their legend labels will be picked up from the response."
+msgid "MergeRequests|View file @ %{commitId}"
msgstr ""
-msgid "Metrics|Y-axis label"
+msgid "MergeRequests|View replaced file @ %{commitId}"
msgstr ""
-msgid "Metrics|e.g. HTTP requests"
+msgid "Merged"
msgstr ""
-msgid "Metrics|e.g. Requests/second"
+msgid "Messages"
msgstr ""
-msgid "Metrics|e.g. Throughput"
+msgid "Metrics - Influx"
msgstr ""
-msgid "Metrics|e.g. rate(http_requests_total[5m])"
+msgid "Metrics - Prometheus"
msgstr ""
-msgid "Metrics|e.g. req/sec"
+msgid "Milestone"
msgstr ""
-msgid "Milestone"
+msgid "Milestones"
msgstr ""
msgid "Milestones|Delete milestone"
@@ -2733,9 +2928,6 @@ msgstr ""
msgid "Milestones|Promote Milestone"
msgstr ""
-msgid "Milestones|This action cannot be reversed."
-msgstr ""
-
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "добавите SSH ключ"
@@ -2748,7 +2940,7 @@ msgstr ""
msgid "Monitoring"
msgstr ""
-msgid "More info"
+msgid "More actions"
msgstr ""
msgid "More information"
@@ -2763,12 +2955,30 @@ msgstr ""
msgid "Move issue"
msgstr ""
-msgid "Multiple issue boards"
+msgid "Name"
msgstr ""
msgid "Name new label"
msgstr ""
+msgid "Name your individual key via a title"
+msgstr ""
+
+msgid "Nav|Help"
+msgstr ""
+
+msgid "Nav|Home"
+msgstr ""
+
+msgid "Nav|Sign In / Register"
+msgstr ""
+
+msgid "Nav|Sign out and sign in with a different account"
+msgstr ""
+
+msgid "New Identity"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "Ðов проблем"
@@ -2780,6 +2990,9 @@ msgstr ""
msgid "New Kubernetes cluster"
msgstr ""
+msgid "New Label"
+msgstr ""
+
msgid "New Pipeline Schedule"
msgstr "Ðов план за Ñхема"
@@ -2792,15 +3005,15 @@ msgstr ""
msgid "New directory"
msgstr "Ðова папка"
-msgid "New epic"
-msgstr ""
-
msgid "New file"
msgstr "Ðов файл"
msgid "New group"
msgstr ""
+msgid "New identity"
+msgstr ""
+
msgid "New issue"
msgstr "Ðов проблем"
@@ -2810,6 +3023,9 @@ msgstr ""
msgid "New merge request"
msgstr "Ðова заÑвка за Ñливане"
+msgid "New pipelines will cancel older, pending pipelines on the same branch"
+msgstr ""
+
msgid "New project"
msgstr ""
@@ -2825,7 +3041,7 @@ msgstr ""
msgid "New tag"
msgstr "Ðов етикет"
-msgid "No Label"
+msgid "No"
msgstr ""
msgid "No assignee"
@@ -2846,7 +3062,16 @@ msgstr ""
msgid "No file chosen"
msgstr ""
-msgid "No labels created yet."
+msgid "No files found"
+msgstr ""
+
+msgid "No files found."
+msgstr ""
+
+msgid "No merge requests found"
+msgstr ""
+
+msgid "No messages were logged"
msgstr ""
msgid "No repository"
@@ -2879,15 +3104,9 @@ msgstr "ÐÑма доÑтатъчно данни"
msgid "Note that the master branch is automatically protected. %{link_to_protected_branches}"
msgstr ""
-msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow connecting repositories without generating a Personal Access Token."
-msgstr ""
-
msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
msgstr ""
-msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow connecting repositories without generating a Personal Access Token."
-msgstr ""
-
msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
msgstr ""
@@ -2963,9 +3182,6 @@ msgstr ""
msgid "Number of access attempts"
msgstr ""
-msgid "OK"
-msgstr ""
-
msgid "Oct"
msgstr ""
@@ -2975,19 +3191,16 @@ msgstr ""
msgid "OfSearchInADropdown|Filter"
msgstr "Филтър"
-msgid "Once imported, repositories can be mirrored over SSH. Read more %{ssh_link}"
-msgstr ""
-
msgid "Online IDE integration settings."
msgstr ""
-msgid "Only project members can comment."
+msgid "Only comments from the following commit are shown below"
msgstr ""
-msgid "Open"
+msgid "Only project members can comment."
msgstr ""
-msgid "Opened"
+msgid "Open in Xcode"
msgstr ""
msgid "OpenedNDaysAgo|Opened"
@@ -2996,9 +3209,18 @@ msgstr "Отворен"
msgid "Opens in a new window"
msgstr ""
+msgid "Operations"
+msgstr ""
+
msgid "Options"
msgstr "Опции"
+msgid "Or you can choose one of the suggested colors below"
+msgstr ""
+
+msgid "Other Labels"
+msgstr ""
+
msgid "Otherwise it is recommended you start with one of the options below."
msgstr ""
@@ -3032,12 +3254,27 @@ msgstr ""
msgid "Password"
msgstr ""
+msgid "Paste your public SSH key, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'. Don't use your private SSH key."
+msgstr ""
+
+msgid "Pause"
+msgstr ""
+
msgid "Pending"
msgstr ""
+msgid "Per job. If a job passes this threshold, it will be marked as failed"
+msgstr ""
+
+msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgstr ""
+
msgid "Performance optimization"
msgstr ""
+msgid "Permissions"
+msgstr ""
+
msgid "Personal Access Token"
msgstr ""
@@ -3053,7 +3290,7 @@ msgstr "План за Ñхема"
msgid "Pipeline Schedules"
msgstr "Планове за Ñхема"
-msgid "Pipeline quota"
+msgid "Pipeline triggers"
msgstr ""
msgid "PipelineCharts|Failed:"
@@ -3152,10 +3389,22 @@ msgstr ""
msgid "Pipelines|This project is not currently set up to run pipelines."
msgstr ""
-msgid "Pipeline|Retry pipeline"
+msgid "Pipeline|Create for"
+msgstr ""
+
+msgid "Pipeline|Create pipeline"
+msgstr ""
+
+msgid "Pipeline|Existing branch name or tag"
msgstr ""
-msgid "Pipeline|Retry pipeline #%{pipelineId}?"
+msgid "Pipeline|Run Pipeline"
+msgstr ""
+
+msgid "Pipeline|Search branches"
+msgstr ""
+
+msgid "Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default."
msgstr ""
msgid "Pipeline|Stop pipeline"
@@ -3164,7 +3413,7 @@ msgstr ""
msgid "Pipeline|Stop pipeline #%{pipelineId}?"
msgstr ""
-msgid "Pipeline|You’re about to retry pipeline %{pipelineId}."
+msgid "Pipeline|Variables"
msgstr ""
msgid "Pipeline|You’re about to stop pipeline %{pipelineId}."
@@ -3182,19 +3431,25 @@ msgstr "Ñ ÐµÑ‚Ð°Ð¿"
msgid "Pipeline|with stages"
msgstr "Ñ ÐµÑ‚Ð°Ð¿Ð¸"
+msgid "Plain diff"
+msgstr ""
+
msgid "PlantUML"
msgstr ""
msgid "Play"
msgstr ""
-msgid "Please <a href=%{link_to_billing} target=\"_blank\" rel=\"noopener noreferrer\">enable billing for one of your projects to be able to create a Kubernetes cluster</a>, then try again."
+msgid "Please accept the Terms of Service before continuing."
+msgstr ""
+
+msgid "Please select at least one filter to see results"
msgstr ""
msgid "Please solve the reCAPTCHA"
msgstr ""
-msgid "Please wait while we connect to your repository. Refresh at will."
+msgid "Please try again"
msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
@@ -3203,7 +3458,19 @@ msgstr ""
msgid "Preferences"
msgstr ""
-msgid "Primary"
+msgid "Preferences|Navigation theme"
+msgstr ""
+
+msgid "Prioritize"
+msgstr ""
+
+msgid "Prioritize label"
+msgstr ""
+
+msgid "Prioritized Labels"
+msgstr ""
+
+msgid "Prioritized label"
msgstr ""
msgid "Private - Project access must be granted explicitly to each user."
@@ -3221,6 +3488,12 @@ msgstr ""
msgid "Profiles|Account scheduled for removal."
msgstr ""
+msgid "Profiles|Change username"
+msgstr ""
+
+msgid "Profiles|Current path: %{path}"
+msgstr ""
+
msgid "Profiles|Delete Account"
msgstr ""
@@ -3239,9 +3512,21 @@ msgstr ""
msgid "Profiles|Invalid username"
msgstr ""
+msgid "Profiles|Path"
+msgstr ""
+
msgid "Profiles|Type your %{confirmationValue} to confirm:"
msgstr ""
+msgid "Profiles|Update username"
+msgstr ""
+
+msgid "Profiles|Username change failed - %{message}"
+msgstr ""
+
+msgid "Profiles|Username successfully changed"
+msgstr ""
+
msgid "Profiles|You don't have access to delete this user."
msgstr ""
@@ -3260,6 +3545,12 @@ msgstr ""
msgid "Programming languages used in this repository"
msgstr ""
+msgid "Progress"
+msgstr ""
+
+msgid "Project"
+msgstr ""
+
msgid "Project '%{project_name}' is in the process of being deleted."
msgstr ""
@@ -3272,6 +3563,9 @@ msgstr "Проектът „%{project_name}“ беше Ñъздаден уÑпÐ
msgid "Project '%{project_name}' was successfully updated."
msgstr "Проектът „%{project_name}“ беше обновен уÑпешно."
+msgid "Project Badges"
+msgstr ""
+
msgid "Project access must be granted explicitly to each user."
msgstr "ДоÑтъпът до проекта трÑбва да бъде даван поотделно на вÑеки потребител."
@@ -3299,30 +3593,6 @@ msgstr "ИзнаÑÑнето на проекта започна. Ще получ
msgid "ProjectActivityRSS|Subscribe"
msgstr ""
-msgid "ProjectCreationLevel|Allowed to create projects"
-msgstr ""
-
-msgid "ProjectCreationLevel|Default project creation protection"
-msgstr ""
-
-msgid "ProjectCreationLevel|Developers + Masters"
-msgstr ""
-
-msgid "ProjectCreationLevel|Masters"
-msgstr ""
-
-msgid "ProjectCreationLevel|No one"
-msgstr ""
-
-msgid "ProjectFeature|Disabled"
-msgstr "Изключено"
-
-msgid "ProjectFeature|Everyone with access"
-msgstr "Ð’Ñеки Ñ Ð´Ð¾Ñтъп"
-
-msgid "ProjectFeature|Only team members"
-msgstr "Само членовете на екипа"
-
msgid "ProjectFileTree|Name"
msgstr "Име"
@@ -3332,27 +3602,6 @@ msgstr "Ðикога"
msgid "ProjectLifecycle|Stage"
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 ""
@@ -3377,6 +3626,9 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr ""
+msgid "PrometheusDashboard|Time"
+msgstr ""
+
msgid "PrometheusService|%{exporters} with %{metrics} were found"
msgstr ""
@@ -3398,18 +3650,9 @@ msgstr ""
msgid "PrometheusService|Common metrics"
msgstr ""
-msgid "PrometheusService|Common metrics are automatically monitored based on a library of metrics from popular exporters."
-msgstr ""
-
-msgid "PrometheusService|Custom metrics"
-msgstr ""
-
msgid "PrometheusService|Finding and configuring metrics..."
msgstr ""
-msgid "PrometheusService|Finding custom metrics..."
-msgstr ""
-
msgid "PrometheusService|Install Prometheus on clusters"
msgstr ""
@@ -3422,13 +3665,13 @@ msgstr ""
msgid "PrometheusService|Metrics"
msgstr ""
-msgid "PrometheusService|Missing environment variable"
+msgid "PrometheusService|Metrics are automatically configured and monitored based on a library of metrics from popular exporters."
msgstr ""
-msgid "PrometheusService|More information"
+msgid "PrometheusService|Missing environment variable"
msgstr ""
-msgid "PrometheusService|New metric"
+msgid "PrometheusService|More information"
msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
@@ -3437,9 +3680,6 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
-msgstr ""
-
msgid "PrometheusService|Time-series monitoring service"
msgstr ""
@@ -3455,22 +3695,28 @@ msgstr ""
msgid "Promote"
msgstr ""
-msgid "Promote to Group Label"
+msgid "Promote these project milestones into a group milestone."
msgstr ""
msgid "Promote to Group Milestone"
msgstr ""
+msgid "Promote to group label"
+msgstr ""
+
msgid "Protip:"
msgstr ""
+msgid "Provider"
+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"
+msgid "Public pipelines"
msgstr ""
msgid "Push events"
@@ -3482,10 +3728,10 @@ msgstr ""
msgid "Push to create a project"
msgstr ""
-msgid "PushRule|Committer restriction"
+msgid "Quick actions can be used in the issues description and comment boxes."
msgstr ""
-msgid "Quick actions can be used in the issues description and comment boxes."
+msgid "Re-deploy"
msgstr ""
msgid "Read more"
@@ -3497,18 +3743,18 @@ msgstr "ПрочетиМе"
msgid "Real-time features"
msgstr ""
-msgid "RefSwitcher|Branches"
-msgstr "Клони"
-
-msgid "RefSwitcher|Tags"
-msgstr "Етикети"
-
msgid "Reference:"
msgstr ""
msgid "Register / Sign In"
msgstr ""
+msgid "Register and see your runners for this group."
+msgstr ""
+
+msgid "Register and see your runners for this project."
+msgstr ""
+
msgid "Registry"
msgstr ""
@@ -3539,28 +3785,28 @@ msgstr "ÐапомнÑне по-къÑно"
msgid "Remove"
msgstr ""
-msgid "Remove avatar"
+msgid "Remove Runner"
msgstr ""
-msgid "Remove project"
-msgstr "Премахване на проекта"
-
-msgid "Repair authentication"
+msgid "Remove avatar"
msgstr ""
-msgid "Repo by URL"
+msgid "Remove priority"
msgstr ""
+msgid "Remove project"
+msgstr "Премахване на проекта"
+
msgid "Repository"
msgstr ""
-msgid "Repository has no locks."
+msgid "Repository Settings"
msgstr ""
msgid "Repository maintenance"
msgstr ""
-msgid "Repository mirror settings"
+msgid "Repository mirror"
msgstr ""
msgid "Repository storage"
@@ -3569,6 +3815,9 @@ msgstr ""
msgid "Request Access"
msgstr "ЗаÑвка за доÑтъп"
+msgid "Require all users to accept Terms of Service and Privacy Policy when they access GitLab."
+msgstr ""
+
msgid "Reset git storage health information"
msgstr ""
@@ -3578,10 +3827,25 @@ msgstr ""
msgid "Reset runners registration token"
msgstr ""
+msgid "Resolve all discussions in new issue"
+msgstr ""
+
+msgid "Resolve conflicts on source branch"
+msgstr ""
+
msgid "Resolve discussion"
msgstr ""
-msgid "Response"
+msgid "Resume"
+msgstr ""
+
+msgid "Retry"
+msgstr ""
+
+msgid "Retry this job"
+msgstr ""
+
+msgid "Retry verification"
msgstr ""
msgid "Reveal value"
@@ -3595,7 +3859,7 @@ msgstr "ОтмÑна на това подаване"
msgid "Revert this merge request"
msgstr "ОтмÑна на тази заÑвка за Ñливане"
-msgid "Review the process for configuring service providers in your identity provider — in this case, GitLab is the \"service provider\" or \"relying party\"."
+msgid "Review"
msgstr ""
msgid "Reviewing"
@@ -3604,28 +3868,31 @@ msgstr ""
msgid "Reviewing (merge request !%{mergeRequestId})"
msgstr ""
-msgid "Roadmap"
+msgid "Rollback"
msgstr ""
-msgid "Run CI/CD pipelines for external repositories"
+msgid "Runner token"
msgstr ""
msgid "Runners"
msgstr ""
-msgid "Running"
+msgid "Runners API"
msgstr ""
-msgid "SAML Single Sign On"
+msgid "Runners can be placed on separate users, servers, and even on your local machine."
msgstr ""
-msgid "SAML Single Sign On Settings"
+msgid "Running"
msgstr ""
-msgid "SHA1 fingerprint of the SAML token signing certificate. Get this from your identity provider, where it can also be called \"Thumbprint\"."
+msgid "SSH Keys"
msgstr ""
-msgid "SSH Keys"
+msgid "SSL Verification"
+msgstr ""
+
+msgid "Save"
msgstr ""
msgid "Save changes"
@@ -3649,15 +3916,30 @@ msgstr ""
msgid "Scheduling Pipelines"
msgstr "Планиране на Ñхемите"
-msgid "Scoped issue boards"
+msgid "Scroll to bottom"
+msgstr ""
+
+msgid "Scroll to top"
msgstr ""
msgid "Search"
msgstr ""
+msgid "Search branches"
+msgstr ""
+
msgid "Search branches and tags"
msgstr "ТърÑете в клоните и етикетите"
+msgid "Search files"
+msgstr ""
+
+msgid "Search for projects, issues, etc."
+msgstr ""
+
+msgid "Search merge requests"
+msgstr ""
+
msgid "Search milestones"
msgstr ""
@@ -3673,15 +3955,15 @@ msgstr ""
msgid "Seconds to wait for a storage access attempt"
msgstr ""
-msgid "Secret variables"
-msgstr ""
-
-msgid "Security report"
+msgid "Select"
msgstr ""
msgid "Select Archive Format"
msgstr "Изберете формата на архива"
+msgid "Select a namespace to fork the project"
+msgstr ""
+
msgid "Select a timezone"
msgstr "Изберете чаÑова зона"
@@ -3694,12 +3976,21 @@ msgstr ""
msgid "Select branch/tag"
msgstr ""
-msgid "Select target branch"
-msgstr "Изберете целеви клон"
+msgid "Select project"
+msgstr ""
+
+msgid "Select project and zone to choose machine type"
+msgstr ""
-msgid "Selective synchronization"
+msgid "Select project to choose zone"
msgstr ""
+msgid "Select source branch"
+msgstr ""
+
+msgid "Select target branch"
+msgstr "Изберете целеви клон"
+
msgid "Send email"
msgstr ""
@@ -3715,9 +4006,6 @@ msgstr ""
msgid "Service Templates"
msgstr ""
-msgid "Service URL"
-msgstr ""
-
msgid "Session expiration, projects limit and attachment size."
msgstr ""
@@ -3742,9 +4030,6 @@ msgstr ""
msgid "Set up Koding"
msgstr "ÐаÑтройка на „Koding“"
-msgid "Set up assertions/attributes/claims (email, first_name, last_name) and NameID according to %{docsLinkStart}the documentation %{icon}%{docsLinkEnd}"
-msgstr ""
-
msgid "SetPasswordToCloneLink|set a password"
msgstr "зададете парола"
@@ -3754,19 +4039,22 @@ msgstr ""
msgid "Setup a specific Runner automatically"
msgstr ""
-msgid "Share the <strong>%{sso_label}</strong> with members so they can sign in to your group through your identity provider"
+msgid "Share"
msgstr ""
-msgid "SharedRunnersMinutesSettings|By resetting the pipeline minutes for this namespace, the currently used minutes will be set to zero."
+msgid "Shared Runners"
msgstr ""
-msgid "SharedRunnersMinutesSettings|Reset pipeline minutes"
+msgid "Show command"
msgstr ""
-msgid "SharedRunnersMinutesSettings|Reset used pipeline minutes"
+msgid "Show complete raw log"
msgstr ""
-msgid "Show command"
+msgid "Show latest version"
+msgstr ""
+
+msgid "Show latest version of the diff"
msgstr ""
msgid "Show parent pages"
@@ -3775,21 +4063,18 @@ msgstr ""
msgid "Show parent subgroups"
msgstr ""
+msgid "Show whitespace changes"
+msgstr ""
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "Показване на %d Ñъбитие"
msgstr[1] "Показване на %d ÑъбитиÑ"
-msgid "Sidebar|Change weight"
-msgstr ""
-
-msgid "Sidebar|No"
-msgstr ""
-
-msgid "Sidebar|None"
+msgid "Side-by-side"
msgstr ""
-msgid "Sidebar|Weight"
+msgid "Sign out"
msgstr ""
msgid "Sign-in restrictions"
@@ -3801,7 +4086,7 @@ msgstr ""
msgid "Size and domain settings for static websites"
msgstr ""
-msgid "Slack application"
+msgid "Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job"
msgstr ""
msgid "Snippets"
@@ -3813,13 +4098,13 @@ msgstr ""
msgid "Something went wrong on our end."
msgstr ""
-msgid "Something went wrong when toggling the button"
+msgid "Something went wrong on our end. Please try again!"
msgstr ""
-msgid "Something went wrong while fetching Dependency Scanning."
+msgid "Something went wrong when toggling the button"
msgstr ""
-msgid "Something went wrong while fetching SAST."
+msgid "Something went wrong while closing the %{issuable}. Please try again later"
msgstr ""
msgid "Something went wrong while fetching the projects."
@@ -3828,6 +4113,12 @@ msgstr ""
msgid "Something went wrong while fetching the registry list."
msgstr ""
+msgid "Something went wrong while reopening the %{issuable}. Please try again later"
+msgstr ""
+
+msgid "Something went wrong while resolving this discussion. Please try again."
+msgstr ""
+
msgid "Something went wrong. Please try again."
msgstr ""
@@ -3873,9 +4164,6 @@ msgstr ""
msgid "SortOptions|Least popular"
msgstr ""
-msgid "SortOptions|Less weight"
-msgstr ""
-
msgid "SortOptions|Milestone"
msgstr ""
@@ -3885,9 +4173,6 @@ msgstr ""
msgid "SortOptions|Milestone due soon"
msgstr ""
-msgid "SortOptions|More weight"
-msgstr ""
-
msgid "SortOptions|Most popular"
msgstr ""
@@ -3927,9 +4212,6 @@ msgstr ""
msgid "SortOptions|Start soon"
msgstr ""
-msgid "SortOptions|Weight"
-msgstr ""
-
msgid "Source"
msgstr ""
@@ -3948,9 +4230,36 @@ msgstr ""
msgid "Spam and Anti-bot Protection"
msgstr ""
+msgid "Specific Runners"
+msgstr ""
+
msgid "Specify the following URL during the Runner setup:"
msgstr ""
+msgid "Squash commits"
+msgstr ""
+
+msgid "Stage"
+msgstr ""
+
+msgid "Stage & Commit"
+msgstr ""
+
+msgid "Stage all changes"
+msgstr ""
+
+msgid "Stage changes"
+msgstr ""
+
+msgid "Staged"
+msgstr ""
+
+msgid "Staged %{type}"
+msgstr ""
+
+msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
+msgstr ""
+
msgid "StarProject|Star"
msgstr "Звезда"
@@ -3972,12 +4281,15 @@ msgstr ""
msgid "Started"
msgstr ""
-msgid "State your message to activate"
+msgid "Starts at (UTC)"
msgstr ""
msgid "Status"
msgstr ""
+msgid "Stop this environment"
+msgstr ""
+
msgid "Stopped"
msgstr ""
@@ -3987,16 +4299,19 @@ msgstr ""
msgid "Subgroups"
msgstr ""
-msgid "Switch branch/tag"
-msgstr "Преминаване към клон/етикет"
+msgid "Subscribe"
+msgstr ""
-msgid "System"
+msgid "Subscribe at group level"
msgstr ""
-msgid "System Hooks"
+msgid "Subscribe at project level"
msgstr ""
-msgid "System header and footer:"
+msgid "Switch branch/tag"
+msgstr "Преминаване към клон/етикет"
+
+msgid "System Hooks"
msgstr ""
msgid "Tag (%{tag_count})"
@@ -4007,6 +4322,9 @@ msgstr[1] ""
msgid "Tags"
msgstr "Етикети"
+msgid "Tags:"
+msgstr ""
+
msgid "TagsPage|Browse commits"
msgstr ""
@@ -4070,7 +4388,7 @@ msgstr ""
msgid "TagsPage|Use git tag command to add a new one:"
msgstr ""
-msgid "TagsPage|Write your release notes or drag files here..."
+msgid "TagsPage|Write your release notes or drag files here…"
msgstr ""
msgid "TagsPage|protected"
@@ -4085,19 +4403,19 @@ msgstr ""
msgid "Team"
msgstr ""
-msgid "Thanks! Don't show me this again"
+msgid "Terms of Service Agreement and Privacy Policy"
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."
+msgid "Terms of Service and Privacy Policy"
msgstr ""
-msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project"
+msgid "Test coverage parsing"
msgstr ""
-msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
+msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project"
msgstr ""
-msgid "The X509 Certificate to use when mutual TLS is required to communicate with the external authorization service. If left blank, the server certificate is still validated when accessing over HTTPS."
+msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
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."
@@ -4106,9 +4424,6 @@ msgstr "Етапът на програмиране показва времето
msgid "The collection of events added to the data gathered for that stage."
msgstr "СъвкупноÑтта от ÑÑŠÐ±Ð¸Ñ‚Ð¸Ñ Ð´Ð¾Ð±Ð°Ð²ÐµÐ½Ð¸ към данните Ñъбрани за този етап."
-msgid "The connection will time out after %{timeout}. For repositories that take longer, use a clone/push combination."
-msgstr ""
-
msgid "The fork relationship has been removed."
msgstr "Връзката на разклонение беше премахната."
@@ -4127,7 +4442,7 @@ 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 passphrase required to decrypt the private key. This is optional and the value is encrypted at rest."
+msgid "The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>"
msgstr ""
msgid "The phase of the development lifecycle."
@@ -4136,9 +4451,6 @@ msgstr "Етапът от цикъла на разработка"
msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit."
msgstr "Етапът на планиране показва колко е времето от преходната Ñтъпка до изпращането на първото подаване. Това време ще бъде добавено автоматично Ñлед като изпратите първото Ñи подаване."
-msgid "The private key to use when a client certificate is provided. This value is encrypted at rest."
-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 "Етапът на издаване показва общото време, което е нужно от Ñъздаването на проблем до внедрÑването на кода в крайната верÑиÑ. Данните ще бъдат добавени автоматично Ñлед като завършите един пълен цикъл и превърнете първата Ñи Ð¸Ð´ÐµÑ Ð² реалноÑÑ‚."
@@ -4160,7 +4472,7 @@ msgstr ""
msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request."
msgstr "Етапът на преглед и одобрение показва времето от Ñъздаването на заÑвката за Ñливане до прилагането Ñ. Данните ще бъдат добавени автоматично Ñлед като приложите първата Ñи заÑвка за Ñливане."
-msgid "The roadmap shows the progress of your epics along a timeline"
+msgid "The secure token used by the Runner to checkout the project"
msgstr ""
msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time."
@@ -4187,13 +4499,19 @@ msgstr "СтойноÑтта, коÑто Ñе намира в Ñредата нÐ
msgid "There are no issues to show"
msgstr ""
+msgid "There are no labels yet"
+msgstr ""
+
msgid "There are no merge requests to show"
msgstr ""
msgid "There are problems accessing Git storage: "
msgstr ""
-msgid "There was an error loading results"
+msgid "There was an error loading jobs"
+msgstr ""
+
+msgid "There was an error loading latest pipeline"
msgstr ""
msgid "There was an error loading users activity calendar."
@@ -4214,12 +4532,21 @@ msgstr ""
msgid "There was an error when unsubscribing from this label."
msgstr ""
-msgid "This board\\'s scope is reduced"
+msgid "They can be managed using the %{link}."
+msgstr ""
+
+msgid "This GitLab instance does not provide any shared Runners yet. Instance administrators can register shared Runners in the admin area."
+msgstr ""
+
+msgid "This diff is collapsed."
msgstr ""
msgid "This directory"
msgstr ""
+msgid "This group does not provide any group Runners yet."
+msgstr ""
+
msgid "This is a confidential issue."
msgstr ""
@@ -4241,6 +4568,15 @@ msgstr ""
msgid "This job depends on upstream jobs that need to succeed in order for this job to be triggered"
msgstr ""
+msgid "This job does not have a trace."
+msgstr ""
+
+msgid "This job has been canceled"
+msgstr ""
+
+msgid "This job has been skipped"
+msgstr ""
+
msgid "This job has not been triggered yet"
msgstr ""
@@ -4259,19 +4595,28 @@ msgstr "Това означава, че нÑма да можете да изпр
msgid "This merge request is locked."
msgstr ""
+msgid "This option is disabled while you still have unstaged changes"
+msgstr ""
+
msgid "This page is unavailable because you are not allowed to read information across multiple projects."
msgstr ""
+msgid "This page will be removed in a future release."
+msgstr ""
+
msgid "This project"
msgstr ""
+msgid "This project does not belong to a group and can therefore not make use of group Runners."
+msgstr ""
+
msgid "This repository"
msgstr ""
-msgid "This will delete the custom metric, Are you sure?"
+msgid "This source diff could not be displayed because it is too large."
msgstr ""
-msgid "Those emails automatically become issues (with the comments becoming the email conversation) listed here."
+msgid "This user has no identities"
msgstr ""
msgid "Time before an issue gets scheduled"
@@ -4283,10 +4628,10 @@ msgstr "Време преди работата по проблем да запо
msgid "Time between merge request creation and merge/close"
msgstr "Време между Ñъздаване на заÑвка за Ñливане и прилагането/отхвърлÑнето Ñ"
-msgid "Time between updates and capacity settings."
+msgid "Time remaining"
msgstr ""
-msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
+msgid "Time spent"
msgstr ""
msgid "Time tracking"
@@ -4310,6 +4655,9 @@ msgstr "преди %s дни"
msgid "Timeago|%s days remaining"
msgstr "оÑтават %s дни"
+msgid "Timeago|%s hours ago"
+msgstr ""
+
msgid "Timeago|%s hours remaining"
msgstr "оÑтават %s чаÑа"
@@ -4325,6 +4673,9 @@ msgstr "преди %s меÑеца"
msgid "Timeago|%s months remaining"
msgstr "оÑтават %s меÑеца"
+msgid "Timeago|%s seconds ago"
+msgstr ""
+
msgid "Timeago|%s seconds remaining"
msgstr "оÑтават %s Ñекунди"
@@ -4340,48 +4691,45 @@ msgstr "преди %s години"
msgid "Timeago|%s years remaining"
msgstr "оÑтават %s години"
+msgid "Timeago|1 day ago"
+msgstr ""
+
msgid "Timeago|1 day remaining"
msgstr "оÑтава 1 ден"
+msgid "Timeago|1 hour ago"
+msgstr ""
+
msgid "Timeago|1 hour remaining"
msgstr "оÑтава 1 чаÑ"
+msgid "Timeago|1 minute ago"
+msgstr ""
+
msgid "Timeago|1 minute remaining"
msgstr "оÑтава 1 минута"
+msgid "Timeago|1 month ago"
+msgstr ""
+
msgid "Timeago|1 month remaining"
msgstr "оÑтава 1 меÑец"
+msgid "Timeago|1 week ago"
+msgstr ""
+
msgid "Timeago|1 week remaining"
msgstr "оÑтава 1 Ñедмица"
+msgid "Timeago|1 year ago"
+msgstr ""
+
msgid "Timeago|1 year remaining"
msgstr "оÑтава 1 година"
msgid "Timeago|Past due"
msgstr "ПроÑрочено"
-msgid "Timeago|a day ago"
-msgstr "преди един ден"
-
-msgid "Timeago|a month ago"
-msgstr "преди един меÑец"
-
-msgid "Timeago|a week ago"
-msgstr "преди една Ñедмица"
-
-msgid "Timeago|a year ago"
-msgstr "преди една година"
-
-msgid "Timeago|about %s hours ago"
-msgstr "преди около %s чаÑа"
-
-msgid "Timeago|about a minute ago"
-msgstr "преди около една минута"
-
-msgid "Timeago|about an hour ago"
-msgstr "преди около един чаÑ"
-
msgid "Timeago|in %s days"
msgstr "Ñлед %s дни"
@@ -4421,11 +4769,14 @@ msgstr "Ñлед 1 Ñедмица"
msgid "Timeago|in 1 year"
msgstr "Ñлед 1 година"
-msgid "Timeago|in a while"
+msgid "Timeago|just now"
+msgstr ""
+
+msgid "Timeago|right now"
msgstr ""
-msgid "Timeago|less than a minute ago"
-msgstr "преди по-малко от минута"
+msgid "Timeout"
+msgstr ""
msgid "Time|hr"
msgid_plural "Time|hrs"
@@ -4443,19 +4794,10 @@ msgstr "Ñек"
msgid "Tip:"
msgstr ""
-msgid "Title"
-msgstr ""
-
msgid "To GitLab"
msgstr ""
-msgid "To connect GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to connect."
-msgstr ""
-
-msgid "To connect GitHub repositories, you first need to authorize GitLab to access the list of your GitHub repositories:"
-msgstr ""
-
-msgid "To connect an SVN repository, check out %{svn_link}."
+msgid "To add an SSH key you need to %{generate_link_start}generate one%{link_end} or use an %{existing_link_start}existing key%{link_end}."
msgstr ""
msgid "To import GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
@@ -4467,19 +4809,19 @@ msgstr ""
msgid "To import an SVN repository, check out %{svn_link}."
msgstr ""
-msgid "To only use CI/CD features for an external repository, choose <strong>CI/CD for external repo</strong>."
+msgid "To start serving your jobs you can add Runners to your group"
msgstr ""
-msgid "To set up SAML authentication for your group through an identity provider like Azure, Okta, Onelogin, Ping Identity, or your custom SAML 2.0 provider:"
+msgid "To validate your GitLab CI configurations, go to 'CI/CD → Pipelines' inside your project, and click on the 'CI Lint' button."
msgstr ""
-msgid "To validate your GitLab CI configurations, go to 'CI/CD → Pipelines' inside your project, and click on the 'CI Lint' button."
+msgid "Todo"
msgstr ""
-msgid "To view the roadmap, add a planned start or finish date to one of your epics in this group or its subgroups. Only epics in the past 3 months and the next 3 months are shown."
+msgid "Toggle Sidebar"
msgstr ""
-msgid "Todo"
+msgid "Toggle discussion"
msgstr ""
msgid "Toggle sidebar"
@@ -4491,6 +4833,9 @@ msgstr ""
msgid "ToggleButton|Toggle Status: ON"
msgstr ""
+msgid "Too many changes to show."
+msgstr ""
+
msgid "Total Time"
msgstr "Общо време"
@@ -4500,22 +4845,19 @@ msgstr "Общо време за теÑтване на вÑички подава
msgid "Total: %{total}"
msgstr ""
-msgid "Track activity with Contribution Analytics."
-msgstr ""
-
-msgid "Track groups of issues that share a theme, across projects and milestones"
-msgstr ""
-
msgid "Track time with quick actions"
msgstr ""
msgid "Trigger this manual action"
msgstr ""
-msgid "Turn on Service Desk"
+msgid "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."
+msgstr ""
+
+msgid "Try again"
msgstr ""
-msgid "Unknown"
+msgid "Unable to load the diff. %{button_try_again}"
msgstr ""
msgid "Unlock"
@@ -4527,25 +4869,45 @@ msgstr ""
msgid "Unresolve discussion"
msgstr ""
+msgid "Unstage all changes"
+msgstr ""
+
+msgid "Unstage changes"
+msgstr ""
+
+msgid "Unstaged"
+msgstr ""
+
+msgid "Unstaged %{type}"
+msgstr ""
+
+msgid "Unstaged and staged %{type}"
+msgstr ""
+
msgid "Unstar"
msgstr "Без звезда"
-msgid "Up to date"
+msgid "Unsubscribe"
msgstr ""
-msgid "Upgrade your plan to activate Advanced Global Search."
+msgid "Unsubscribe at group level"
msgstr ""
-msgid "Upgrade your plan to activate Contribution Analytics."
+msgid "Unsubscribe at project level"
msgstr ""
-msgid "Upgrade your plan to activate Group Webhooks."
+msgid "Unverified"
msgstr ""
-msgid "Upgrade your plan to activate Issue weight."
+msgid "Up to date"
msgstr ""
-msgid "Upgrade your plan to improve Issue boards."
+msgid "Update %{files}"
+msgid_plural "Update %{files} files"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Update your group name, description, avatar, and other general settings."
msgstr ""
msgid "Upload New File"
@@ -4566,7 +4928,7 @@ msgstr ""
msgid "Usage statistics"
msgstr ""
-msgid "Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab"
+msgid "Use group milestones to manage issues from multiple projects in the same milestone."
msgstr ""
msgid "Use the following registration token during setup:"
@@ -4575,10 +4937,13 @@ msgstr ""
msgid "Use your global notification setting"
msgstr "Използване на глобалната Ви наÑтройка за извеÑтиÑта"
-msgid "Used by members to sign in to your group in GitLab"
+msgid "User and IP Rate Limits"
msgstr ""
-msgid "User and IP Rate Limits"
+msgid "Users"
+msgstr ""
+
+msgid "Variables"
msgstr ""
msgid "Variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. You can use variables for passwords, secret keys, or whatever you want."
@@ -4593,10 +4958,7 @@ msgstr ""
msgid "Various settings that affect GitLab performance."
msgstr ""
-msgid "View and edit lines"
-msgstr ""
-
-msgid "View epics list"
+msgid "Verified"
msgstr ""
msgid "View file @ "
@@ -4605,9 +4967,15 @@ msgstr ""
msgid "View group labels"
msgstr ""
+msgid "View jobs"
+msgstr ""
+
msgid "View labels"
msgstr ""
+msgid "View log"
+msgstr ""
+
msgid "View open merge request"
msgstr "Преглед на отворената заÑвка за Ñливане"
@@ -4635,9 +5003,6 @@ msgstr "ÐеизвеÑтно"
msgid "Want to see the data? Please ask an administrator for access."
msgstr "ИÑкате ли да видите данните? Помолете админиÑтратор за доÑтъп."
-msgid "We could not verify that one of your projects on GCP has billing enabled. Please try again."
-msgstr ""
-
msgid "We don't have enough data to show this stage."
msgstr "ÐÑма доÑтатъчно данни за този етап."
@@ -4650,13 +5015,10 @@ msgstr ""
msgid "Web terminal"
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."
+msgid "When a runner is locked, it cannot be assigned to other projects"
msgstr ""
-msgid "Weight"
-msgstr ""
-
-msgid "When leaving the URL blank, classification labels can still be specified whitout disabling cross project features or performing external authorization checks."
+msgid "When enabled, users cannot use GitLab until the terms have been accepted."
msgstr ""
msgid "Wiki"
@@ -4683,7 +5045,31 @@ msgstr ""
msgid "WikiEdit|There is already a page with the same title in that path."
msgstr ""
-msgid "WikiEmptyPageError|You are not allowed to create wiki pages"
+msgid "WikiEmptyIssueMessage|Suggest wiki improvement"
+msgstr ""
+
+msgid "WikiEmptyIssueMessage|You must be a project member in order to add wiki pages. If you have suggestions for how to improve the wiki for this project, consider opening an issue in the %{issues_link}."
+msgstr ""
+
+msgid "WikiEmptyIssueMessage|issue tracker"
+msgstr ""
+
+msgid "WikiEmpty|A wiki is where you can store all the details about your project. This can include why you've created it, its principles, how to use it, and so on."
+msgstr ""
+
+msgid "WikiEmpty|Create your first page"
+msgstr ""
+
+msgid "WikiEmpty|Suggest wiki improvement"
+msgstr ""
+
+msgid "WikiEmpty|The wiki lets you write documentation for your project"
+msgstr ""
+
+msgid "WikiEmpty|This project has no wiki pages"
+msgstr ""
+
+msgid "WikiEmpty|You must be a project member in order to add wiki pages."
msgstr ""
msgid "WikiHistoricalPage|This is an old version of this page."
@@ -4719,6 +5105,12 @@ msgstr ""
msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?"
msgstr ""
+msgid "WikiPageConfirmDelete|Delete page"
+msgstr ""
+
+msgid "WikiPageConfirmDelete|Delete page %{pageTitle}?"
+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 ""
@@ -4734,7 +5126,7 @@ msgstr ""
msgid "WikiPage|Page slug"
msgstr ""
-msgid "WikiPage|Write your content or drag files here..."
+msgid "WikiPage|Write your content or drag files here…"
msgstr ""
msgid "Wiki|Create Page"
@@ -4746,9 +5138,6 @@ msgstr ""
msgid "Wiki|Edit Page"
msgstr ""
-msgid "Wiki|Empty page"
-msgstr ""
-
msgid "Wiki|More Pages"
msgstr ""
@@ -4767,13 +5156,10 @@ 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 "ОттеглÑне на заÑвката за доÑтъп"
-msgid "Write a commit message..."
+msgid "Yes"
msgstr ""
msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
@@ -4791,7 +5177,7 @@ msgstr ""
msgid "You are on a read-only GitLab instance."
msgstr ""
-msgid "You are on a secondary (read-only) Geo node. If you want to make any changes, you must visit the %{primary_node}."
+msgid "You can %{linkStart}view the blob%{linkEnd} instead."
msgstr ""
msgid "You can also create a project from the command line."
@@ -4800,6 +5186,9 @@ msgstr ""
msgid "You can also star a label to make it a priority label."
msgstr ""
+msgid "You can also test your .gitlab-ci.yml in the %{linkStart}Lint%{linkEnd}"
+msgstr ""
+
msgid "You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}"
msgstr ""
@@ -4812,30 +5201,33 @@ msgstr "Можете да добавÑте файлове Ñамо когато
msgid "You can only edit 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."
+msgid "You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}"
msgstr ""
msgid "You cannot write to this read-only GitLab instance."
msgstr ""
-msgid "You do not have the correct permissions to override the settings from the LDAP group sync."
+msgid "You do not have any assigned merge requests"
msgstr ""
msgid "You have no permissions"
msgstr ""
+msgid "You have not created any merge requests"
+msgstr ""
+
msgid "You have reached your project limit"
msgstr "Ðе можете да Ñъздавате повече проекти"
-msgid "You must have master access to force delete a lock"
+msgid "You must accept our Terms of Service and privacy policy in order to register an account"
+msgstr ""
+
+msgid "You must have maintainer access to force delete a lock"
msgstr ""
msgid "You must sign in to star a project"
msgstr "ТрÑбва да Ñе впишете, за да отбележите проект ÑÑŠÑ Ð·Ð²ÐµÐ·Ð´Ð°"
-msgid "You need a different license to enable FileLocks feature"
-msgstr ""
-
msgid "You need permission."
msgstr "Ðуждаете Ñе от разрешение."
@@ -4902,13 +5294,11 @@ msgstr "Вашето име"
msgid "Your projects"
msgstr ""
-msgid "among other things"
+msgid "ago"
msgstr ""
-msgid "and %d fixed vulnerability"
-msgid_plural "and %d fixed vulnerabilities"
-msgstr[0] ""
-msgstr[1] ""
+msgid "among other things"
+msgstr ""
msgid "assign yourself"
msgstr ""
@@ -4916,136 +5306,36 @@ msgstr ""
msgid "branch name"
msgstr ""
-msgid "by"
-msgstr ""
-
-msgid "ciReport|%{type} detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|%{type} detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Code quality"
-msgstr ""
-
-msgid "ciReport|DAST detected no alerts by analyzing the review app"
-msgstr ""
-
-msgid "ciReport|Dependency scanning"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Failed to load %{reportName} report"
-msgstr ""
-
-msgid "ciReport|Fixed:"
-msgstr ""
-
-msgid "ciReport|Instances"
-msgstr ""
-
-msgid "ciReport|Learn more about whitelisting"
-msgstr ""
-
-msgid "ciReport|Loading %{reportName} report"
-msgstr ""
-
-msgid "ciReport|No changes to code quality"
-msgstr ""
-
-msgid "ciReport|No changes to performance metrics"
-msgstr ""
-
-msgid "ciReport|Performance metrics"
-msgstr ""
-
-msgid "ciReport|SAST"
-msgstr ""
-
-msgid "ciReport|SAST detected"
-msgstr ""
-
-msgid "ciReport|SAST detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|SAST detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|SAST:container no vulnerabilities were found"
-msgstr ""
-
-msgid "ciReport|Security scanning"
-msgstr ""
-
-msgid "ciReport|Security scanning failed loading any results"
-msgstr ""
-
-msgid "ciReport|Show complete code vulnerabilities report"
-msgstr ""
-
-msgid "ciReport|Unapproved vulnerabilities (red) can be marked as approved. %{helpLink}"
-msgstr ""
-
-msgid "ciReport|no vulnerabilities"
-msgstr ""
-
msgid "command line instructions"
msgstr ""
msgid "connecting"
msgstr ""
-msgid "could not read private key, is the passphrase correct?"
-msgstr ""
-
msgid "day"
msgid_plural "days"
msgstr[0] "ден"
msgstr[1] "дни"
-msgid "detected %d fixed vulnerability"
-msgid_plural "detected %d fixed vulnerabilities"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "detected %d new vulnerability"
-msgid_plural "detected %d new vulnerabilities"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "detected no vulnerabilities"
+msgid "deploy token"
msgstr ""
-msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
+msgid "disabled"
msgstr ""
-msgid "here"
+msgid "enabled"
msgstr ""
-msgid "importing"
-msgstr ""
-
-msgid "in progress"
-msgstr ""
-
-msgid "is invalid because there is downstream lock"
+msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
msgstr ""
-msgid "is invalid because there is upstream lock"
+msgid "for this project"
msgstr ""
-msgid "is not a valid X509 certificate."
+msgid "importing"
msgstr ""
-msgid "locked by %{path_lock_user_name} %{created_at}"
+msgid "latest version"
msgstr ""
msgid "merge request"
@@ -5065,28 +5355,7 @@ msgstr ""
msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage is %{emphasisStart} unchanged %{emphasisEnd} at %{memoryFrom}MB"
msgstr ""
-msgid "mrWidget|Add approval"
-msgstr ""
-
-msgid "mrWidget|Allows edits from maintainers"
-msgstr ""
-
-msgid "mrWidget|An error occured while removing your approval."
-msgstr ""
-
-msgid "mrWidget|An error occured while retrieving approval data for this merge request."
-msgstr ""
-
-msgid "mrWidget|An error occured while submitting your approval."
-msgstr ""
-
-msgid "mrWidget|Approve"
-msgstr ""
-
-msgid "mrWidget|Approved"
-msgstr ""
-
-msgid "mrWidget|Approved by"
+msgid "mrWidget|Allows commits from members who can merge to the target branch"
msgstr ""
msgid "mrWidget|Cancel automatic merge"
@@ -5113,6 +5382,9 @@ msgstr ""
msgid "mrWidget|Closes"
msgstr ""
+msgid "mrWidget|Create an issue to resolve them later"
+msgstr ""
+
msgid "mrWidget|Deployment statistics are not available currently"
msgstr ""
@@ -5167,9 +5439,6 @@ msgstr ""
msgid "mrWidget|Remove source branch"
msgstr ""
-msgid "mrWidget|Remove your approval"
-msgstr ""
-
msgid "mrWidget|Request to merge"
msgstr ""
@@ -5209,6 +5478,9 @@ msgstr ""
msgid "mrWidget|There are merge conflicts"
msgstr ""
+msgid "mrWidget|There are unresolved discussions. Please resolve these discussions"
+msgstr ""
+
msgid "mrWidget|This merge request failed to be merged automatically"
msgstr ""
@@ -5259,7 +5531,7 @@ msgstr ""
msgid "personal access token"
msgstr ""
-msgid "private key does not match certificate."
+msgid "remaining"
msgstr ""
msgid "remove due date"
@@ -5274,9 +5546,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "to help your contributors communicate effectively!"
-msgstr ""
-
msgid "username"
msgstr ""
@@ -5286,3 +5555,8 @@ msgstr ""
msgid "with %{additions} additions, %{deletions} deletions."
msgstr ""
+msgid "within %d minute "
+msgid_plural "within %d minutes "
+msgstr[0] ""
+msgstr[1] ""
+
diff --git a/locale/cs_CZ/gitlab.po b/locale/cs_CZ/gitlab.po
new file mode 100644
index 00000000000..5fdd7ce8a1a
--- /dev/null
+++ b/locale/cs_CZ/gitlab.po
@@ -0,0 +1,5640 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: gitlab-ee\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-07-01 16:35+1000\n"
+"PO-Revision-Date: 2018-07-01 11:01\n"
+"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
+"Language-Team: Czech\n"
+"Language: cs_CZ\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=4; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 3;\n"
+"X-Generator: crowdin.com\n"
+"X-Crowdin-Project: gitlab-ee\n"
+"X-Crowdin-Language: cs\n"
+"X-Crowdin-File: /master/locale/gitlab.pot\n"
+
+msgid "%d changed file"
+msgid_plural "%d changed files"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "%d commit"
+msgid_plural "%d commits"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "%d commit behind"
+msgid_plural "%d commits behind"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "%d exporter"
+msgid_plural "%d exporters"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "%d issue"
+msgid_plural "%d issues"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "%d layer"
+msgid_plural "%d layers"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "%d merge request"
+msgid_plural "%d merge requests"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "%d metric"
+msgid_plural "%d metrics"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "%d staged change"
+msgid_plural "%d staged changes"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "%d unstaged change"
+msgid_plural "%d unstaged changes"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "%s additional commit has been omitted to prevent performance issues."
+msgid_plural "%s additional commits have been omitted to prevent performance issues."
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "%{actionText} & %{openOrClose} %{noteable}"
+msgstr ""
+
+msgid "%{commit_author_link} authored %{commit_timeago}"
+msgstr ""
+
+msgid "%{count} participant"
+msgid_plural "%{count} participants"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "%{filePath} deleted"
+msgstr ""
+
+msgid "%{group_docs_link_start}Groups%{group_docs_link_end} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects."
+msgstr ""
+
+msgid "%{loadingIcon} Started"
+msgstr ""
+
+msgid "%{lock_path} is locked by GitLab User %{lock_user_id}"
+msgstr ""
+
+msgid "%{nip_domain} can be used as an alternative to a custom domain."
+msgstr ""
+
+msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
+msgstr ""
+
+msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will allow access on the next attempt."
+msgstr ""
+
+msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will not retry automatically. Reset storage information when the problem is resolved."
+msgstr ""
+
+msgid "%{openOrClose} %{noteable}"
+msgstr ""
+
+msgid "%{percent}%% complete"
+msgstr ""
+
+msgid "%{storage_name}: failed storage access attempt on host:"
+msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "%{text} is available"
+msgstr ""
+
+msgid "%{title} changes"
+msgstr ""
+
+msgid "%{unstaged} unstaged and %{staged} staged changes"
+msgstr ""
+
+msgid "(checkout the %{link} for information on how to install it)."
+msgstr ""
+
+msgid "+ %{moreCount} more"
+msgstr ""
+
+msgid "- Runner is active and can process any new jobs"
+msgstr ""
+
+msgid "- Runner is paused and will not receive any new jobs"
+msgstr ""
+
+msgid "- show less"
+msgstr ""
+
+msgid "1 %{type} addition"
+msgid_plural "%{count} %{type} additions"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "1 %{type} modification"
+msgid_plural "%{count} %{type} modifications"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "1 closed issue"
+msgid_plural "%d closed issues"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "1 closed merge request"
+msgid_plural "%d closed merge requests"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "1 merged merge request"
+msgid_plural "%d merged merge requests"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "1 open issue"
+msgid_plural "%d open issues"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "1 open merge request"
+msgid_plural "%d open merge requests"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "1 pipeline"
+msgid_plural "%d pipelines"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "1st contribution!"
+msgstr ""
+
+msgid "2FA enabled"
+msgstr ""
+
+msgid "403|Please contact your GitLab administrator to get the permission."
+msgstr ""
+
+msgid "403|You don't have the permission to access this page."
+msgstr ""
+
+msgid "404|Make sure the address is correct and the page hasn't moved."
+msgstr ""
+
+msgid "404|Page Not Found"
+msgstr ""
+
+msgid "404|Please contact your GitLab administrator if you think this is a mistake."
+msgstr ""
+
+msgid "<strong>Removes</strong> source branch"
+msgstr ""
+
+msgid "A 'Runner' is a process which runs a job. You can setup as many Runners as you need."
+msgstr ""
+
+msgid "A collection of graphs regarding Continuous Integration"
+msgstr ""
+
+msgid "A new branch will be created in your fork and a new merge request will be started."
+msgstr ""
+
+msgid "A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}."
+msgstr ""
+
+msgid "A regular expression that will be used to find the test coverage output in the job trace. Leave blank to disable"
+msgstr ""
+
+msgid "A user with write access to the source branch selected this option"
+msgstr ""
+
+msgid "About auto deploy"
+msgstr ""
+
+msgid "Abuse Reports"
+msgstr ""
+
+msgid "Abuse reports"
+msgstr ""
+
+msgid "Accept terms"
+msgstr ""
+
+msgid "Access Tokens"
+msgstr ""
+
+msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again."
+msgstr ""
+
+msgid "Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report."
+msgstr ""
+
+msgid "Account"
+msgstr ""
+
+msgid "Account and limit"
+msgstr ""
+
+msgid "Active"
+msgstr ""
+
+msgid "Active Sessions"
+msgstr ""
+
+msgid "Activity"
+msgstr ""
+
+msgid "Add Changelog"
+msgstr ""
+
+msgid "Add Contribution guide"
+msgstr ""
+
+msgid "Add Kubernetes cluster"
+msgstr ""
+
+msgid "Add License"
+msgstr ""
+
+msgid "Add Readme"
+msgstr ""
+
+msgid "Add new directory"
+msgstr ""
+
+msgid "Add reaction"
+msgstr ""
+
+msgid "Add todo"
+msgstr ""
+
+msgid "AdminArea|Stop all jobs"
+msgstr ""
+
+msgid "AdminArea|Stop all jobs?"
+msgstr ""
+
+msgid "AdminArea|Stop jobs"
+msgstr ""
+
+msgid "AdminArea|Stopping jobs failed"
+msgstr ""
+
+msgid "AdminArea|You’re about to stop all jobs.This will halt all current jobs that are running."
+msgstr ""
+
+msgid "AdminHealthPageLink|health page"
+msgstr ""
+
+msgid "AdminProjects|Delete"
+msgstr ""
+
+msgid "AdminProjects|Delete Project %{projectName}?"
+msgstr ""
+
+msgid "AdminProjects|Delete project"
+msgstr ""
+
+msgid "AdminSettings|Specify a domain to use by default for every project's Auto Review Apps and Auto Deploy stages."
+msgstr ""
+
+msgid "AdminUsers|Block user"
+msgstr ""
+
+msgid "AdminUsers|Delete User %{username} and contributions?"
+msgstr ""
+
+msgid "AdminUsers|Delete User %{username}?"
+msgstr ""
+
+msgid "AdminUsers|Delete user"
+msgstr ""
+
+msgid "AdminUsers|Delete user and contributions"
+msgstr ""
+
+msgid "AdminUsers|To confirm, type %{projectName}"
+msgstr ""
+
+msgid "AdminUsers|To confirm, type %{username}"
+msgstr ""
+
+msgid "Advanced"
+msgstr ""
+
+msgid "Advanced settings"
+msgstr ""
+
+msgid "All"
+msgstr ""
+
+msgid "All changes are committed"
+msgstr ""
+
+msgid "All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings."
+msgstr ""
+
+msgid "Allow commits from members who can merge to the target branch."
+msgstr ""
+
+msgid "Allow public access to pipelines and job details, including output logs and artifacts"
+msgstr ""
+
+msgid "Allow rendering of PlantUML diagrams in Asciidoc documents."
+msgstr ""
+
+msgid "Allow requests to the local network from hooks and services."
+msgstr ""
+
+msgid "Allows you to add and manage Kubernetes clusters."
+msgstr ""
+
+msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
+msgstr ""
+
+msgid "An error occured creating the new branch."
+msgstr ""
+
+msgid "An error occured whilst loading all the files."
+msgstr ""
+
+msgid "An error occured whilst loading the file content."
+msgstr ""
+
+msgid "An error occured whilst loading the file."
+msgstr ""
+
+msgid "An error occured whilst loading the merge request changes."
+msgstr ""
+
+msgid "An error occured whilst loading the merge request version data."
+msgstr ""
+
+msgid "An error occured whilst loading the merge request."
+msgstr ""
+
+msgid "An error occurred previewing the blob"
+msgstr ""
+
+msgid "An error occurred when toggling the notification subscription"
+msgstr ""
+
+msgid "An error occurred while dismissing the alert. Refresh the page and try again."
+msgstr ""
+
+msgid "An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again."
+msgstr ""
+
+msgid "An error occurred while fetching markdown preview"
+msgstr ""
+
+msgid "An error occurred while fetching sidebar data"
+msgstr ""
+
+msgid "An error occurred while fetching the pipeline."
+msgstr ""
+
+msgid "An error occurred while getting projects"
+msgstr ""
+
+msgid "An error occurred while importing project: ${details}"
+msgstr ""
+
+msgid "An error occurred while loading commits"
+msgstr ""
+
+msgid "An error occurred while loading diff"
+msgstr ""
+
+msgid "An error occurred while loading filenames"
+msgstr ""
+
+msgid "An error occurred while loading the file"
+msgstr ""
+
+msgid "An error occurred while making the request."
+msgstr ""
+
+msgid "An error occurred while rendering KaTeX"
+msgstr ""
+
+msgid "An error occurred while rendering preview broadcast message"
+msgstr ""
+
+msgid "An error occurred while retrieving calendar activity"
+msgstr ""
+
+msgid "An error occurred while retrieving diff"
+msgstr ""
+
+msgid "An error occurred while saving assignees"
+msgstr ""
+
+msgid "An error occurred while validating username"
+msgstr ""
+
+msgid "An error occurred. Please try again."
+msgstr ""
+
+msgid "Appearance"
+msgstr ""
+
+msgid "Applications"
+msgstr ""
+
+msgid "Apr"
+msgstr ""
+
+msgid "April"
+msgstr ""
+
+msgid "Archived project! Repository and other project resources are read-only"
+msgstr ""
+
+msgid "Are you sure you want to delete this pipeline schedule?"
+msgstr ""
+
+msgid "Are you sure you want to remove this identity?"
+msgstr ""
+
+msgid "Are you sure you want to reset registration token?"
+msgstr ""
+
+msgid "Are you sure you want to reset the health check token?"
+msgstr ""
+
+msgid "Are you sure?"
+msgstr ""
+
+msgid "Artifacts"
+msgstr ""
+
+msgid "Ask your group maintainer to setup a group Runner."
+msgstr ""
+
+msgid "Assign custom color like #FF0000"
+msgstr ""
+
+msgid "Assign labels"
+msgstr ""
+
+msgid "Assign milestone"
+msgstr ""
+
+msgid "Assign to"
+msgstr ""
+
+msgid "Assigned Issues"
+msgstr ""
+
+msgid "Assigned Merge Requests"
+msgstr ""
+
+msgid "Assigned to :name"
+msgstr ""
+
+msgid "Assigned to me"
+msgstr ""
+
+msgid "Assignee"
+msgstr ""
+
+msgid "Assignee(s)"
+msgstr ""
+
+msgid "Attach a file by drag &amp; drop or %{upload_link}"
+msgstr ""
+
+msgid "Aug"
+msgstr ""
+
+msgid "August"
+msgstr ""
+
+msgid "Authentication Log"
+msgstr ""
+
+msgid "Author"
+msgstr ""
+
+msgid "Authors: %{authors}"
+msgstr ""
+
+msgid "Auto DevOps enabled"
+msgstr ""
+
+msgid "Auto DevOps, runners and job artifacts"
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need a %{kubernetes} to work correctly."
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need a domain name and a %{kubernetes} to work correctly."
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
+msgstr ""
+
+msgid "Auto-cancel redundant, pending pipelines"
+msgstr ""
+
+msgid "AutoDevOps|Auto DevOps"
+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 automatically build and test your application if you %{link_to_auto_devops_settings} for this project. You can automatically deploy it as well, if you %{link_to_add_kubernetes_cluster}."
+msgstr ""
+
+msgid "AutoDevOps|add a Kubernetes cluster"
+msgstr ""
+
+msgid "AutoDevOps|enable Auto DevOps"
+msgstr ""
+
+msgid "Available"
+msgstr ""
+
+msgid "Available group Runners : %{runners}"
+msgstr ""
+
+msgid "Available group Runners : %{runners}."
+msgstr ""
+
+msgid "Avatar will be removed. Are you sure?"
+msgstr ""
+
+msgid "Average per day: %{average}"
+msgstr ""
+
+msgid "Background color"
+msgstr ""
+
+msgid "Background jobs"
+msgstr ""
+
+msgid "Badges"
+msgstr ""
+
+msgid "Badges|A new badge was added."
+msgstr ""
+
+msgid "Badges|Add badge"
+msgstr ""
+
+msgid "Badges|Adding the badge failed, please check the entered URLs and try again."
+msgstr ""
+
+msgid "Badges|Badge image URL"
+msgstr ""
+
+msgid "Badges|Badge image preview"
+msgstr ""
+
+msgid "Badges|Delete badge"
+msgstr ""
+
+msgid "Badges|Delete badge?"
+msgstr ""
+
+msgid "Badges|Deleting the badge failed, please try again."
+msgstr ""
+
+msgid "Badges|Group Badge"
+msgstr ""
+
+msgid "Badges|Link"
+msgstr ""
+
+msgid "Badges|No badge image"
+msgstr ""
+
+msgid "Badges|No image to preview"
+msgstr ""
+
+msgid "Badges|Project Badge"
+msgstr ""
+
+msgid "Badges|Reload badge image"
+msgstr ""
+
+msgid "Badges|Save changes"
+msgstr ""
+
+msgid "Badges|Saving the badge failed, please check the entered URLs and try again."
+msgstr ""
+
+msgid "Badges|The %{docsLinkStart}variables%{docsLinkEnd} GitLab supports: %{placeholders}"
+msgstr ""
+
+msgid "Badges|The badge was deleted."
+msgstr ""
+
+msgid "Badges|The badge was saved."
+msgstr ""
+
+msgid "Badges|This group has no badges"
+msgstr ""
+
+msgid "Badges|This project has no badges"
+msgstr ""
+
+msgid "Badges|Your badges"
+msgstr ""
+
+msgid "Begin with the selected commit"
+msgstr ""
+
+msgid "Below are examples of regex for existing tools:"
+msgstr ""
+
+msgid "Boards"
+msgstr ""
+
+msgid "Branch %{branchName} was not found in this project's repository."
+msgstr ""
+
+msgid "Branch (%{branch_count})"
+msgid_plural "Branches (%{branch_count})"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+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 "Branch is already taken"
+msgstr ""
+
+msgid "Branch name"
+msgstr ""
+
+msgid "BranchSwitcherPlaceholder|Search branches"
+msgstr ""
+
+msgid "BranchSwitcherTitle|Switch branch"
+msgstr ""
+
+msgid "Branches"
+msgstr ""
+
+msgid "Branches|Active"
+msgstr ""
+
+msgid "Branches|Active branches"
+msgstr ""
+
+msgid "Branches|All"
+msgstr ""
+
+msgid "Branches|Cant find HEAD commit for this branch"
+msgstr ""
+
+msgid "Branches|Compare"
+msgstr ""
+
+msgid "Branches|Delete all branches that are merged into '%{default_branch}'"
+msgstr ""
+
+msgid "Branches|Delete branch"
+msgstr ""
+
+msgid "Branches|Delete merged branches"
+msgstr ""
+
+msgid "Branches|Delete protected branch"
+msgstr ""
+
+msgid "Branches|Delete protected branch '%{branch_name}'?"
+msgstr ""
+
+msgid "Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?"
+msgstr ""
+
+msgid "Branches|Deleting the merged branches cannot be undone. Are you sure?"
+msgstr ""
+
+msgid "Branches|Filter by branch name"
+msgstr ""
+
+msgid "Branches|Merged into %{default_branch}"
+msgstr ""
+
+msgid "Branches|New branch"
+msgstr ""
+
+msgid "Branches|No branches to show"
+msgstr ""
+
+msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
+msgstr ""
+
+msgid "Branches|Only a project maintainer or owner can delete a protected branch"
+msgstr ""
+
+msgid "Branches|Overview"
+msgstr ""
+
+msgid "Branches|Protected branches can be managed in %{project_settings_link}."
+msgstr ""
+
+msgid "Branches|Show active branches"
+msgstr ""
+
+msgid "Branches|Show all branches"
+msgstr ""
+
+msgid "Branches|Show more active branches"
+msgstr ""
+
+msgid "Branches|Show more stale branches"
+msgstr ""
+
+msgid "Branches|Show overview of the branches"
+msgstr ""
+
+msgid "Branches|Show stale branches"
+msgstr ""
+
+msgid "Branches|Sort by"
+msgstr ""
+
+msgid "Branches|Stale"
+msgstr ""
+
+msgid "Branches|Stale branches"
+msgstr ""
+
+msgid "Branches|The default branch cannot be deleted"
+msgstr ""
+
+msgid "Branches|This branch hasn’t been merged into %{default_branch}."
+msgstr ""
+
+msgid "Branches|To avoid data loss, consider merging this branch before deleting it."
+msgstr ""
+
+msgid "Branches|To confirm, type %{branch_name_confirmation}:"
+msgstr ""
+
+msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
+msgstr ""
+
+msgid "Branches|merged"
+msgstr ""
+
+msgid "Branches|project settings"
+msgstr ""
+
+msgid "Branches|protected"
+msgstr ""
+
+msgid "Browse Directory"
+msgstr ""
+
+msgid "Browse File"
+msgstr ""
+
+msgid "Browse Files"
+msgstr ""
+
+msgid "Browse files"
+msgstr ""
+
+msgid "ByAuthor|by"
+msgstr ""
+
+msgid "CI / CD"
+msgstr ""
+
+msgid "CI / CD Settings"
+msgstr ""
+
+msgid "CI/CD configuration"
+msgstr ""
+
+msgid "CI/CD settings"
+msgstr ""
+
+msgid "CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery."
+msgstr ""
+
+msgid "CICD|Auto DevOps"
+msgstr ""
+
+msgid "CICD|Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration."
+msgstr ""
+
+msgid "CICD|Automatic deployment to staging, manual deployment to production"
+msgstr ""
+
+msgid "CICD|Continuous deployment to production"
+msgstr ""
+
+msgid "CICD|Deployment strategy"
+msgstr ""
+
+msgid "CICD|Deployment strategy needs a domain name to work correctly."
+msgstr ""
+
+msgid "CICD|Disable Auto DevOps"
+msgstr ""
+
+msgid "CICD|Enable Auto DevOps"
+msgstr ""
+
+msgid "CICD|Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific %{ci_file}."
+msgstr ""
+
+msgid "CICD|Instance default (%{state})"
+msgstr ""
+
+msgid "CICD|Jobs"
+msgstr ""
+
+msgid "CICD|Learn more about Auto DevOps"
+msgstr ""
+
+msgid "CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project."
+msgstr ""
+
+msgid "CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages."
+msgstr ""
+
+msgid "Can't find HEAD commit for this branch"
+msgstr ""
+
+msgid "Cancel"
+msgstr ""
+
+msgid "Cancel this job"
+msgstr ""
+
+msgid "Cannot be merged automatically"
+msgstr ""
+
+msgid "Cannot modify managed Kubernetes cluster"
+msgstr ""
+
+msgid "Change this value to influence how frequently the GitLab UI polls for updates."
+msgstr ""
+
+msgid "ChangeTypeActionLabel|Pick into branch"
+msgstr ""
+
+msgid "ChangeTypeActionLabel|Revert in branch"
+msgstr ""
+
+msgid "ChangeTypeAction|Cherry-pick"
+msgstr ""
+
+msgid "ChangeTypeAction|Revert"
+msgstr ""
+
+msgid "ChangeTypeAction|This will create a new commit in order to revert the existing changes."
+msgstr ""
+
+msgid "Changelog"
+msgstr ""
+
+msgid "Changes are shown as if the <b>source</b> revision was being merged into the <b>target</b> revision."
+msgstr ""
+
+msgid "Charts"
+msgstr ""
+
+msgid "Chat"
+msgstr ""
+
+msgid "Check interval"
+msgstr ""
+
+msgid "Checking %{text} availability…"
+msgstr ""
+
+msgid "Checking branch availability..."
+msgstr ""
+
+msgid "Cherry-pick this commit"
+msgstr ""
+
+msgid "Cherry-pick this merge request"
+msgstr ""
+
+msgid "Choose File ..."
+msgstr ""
+
+msgid "Choose a branch/tag (e.g. %{master}) or enter a commit (e.g. %{sha}) to see what's changed or to create a merge request."
+msgstr ""
+
+msgid "Choose any color."
+msgstr ""
+
+msgid "Choose between <code>clone</code> or <code>fetch</code> to get the recent application code"
+msgstr ""
+
+msgid "Choose file..."
+msgstr ""
+
+msgid "Choose which repositories you want to import."
+msgstr ""
+
+msgid "CiStatusLabel|canceled"
+msgstr ""
+
+msgid "CiStatusLabel|created"
+msgstr ""
+
+msgid "CiStatusLabel|failed"
+msgstr ""
+
+msgid "CiStatusLabel|manual action"
+msgstr ""
+
+msgid "CiStatusLabel|passed"
+msgstr ""
+
+msgid "CiStatusLabel|passed with warnings"
+msgstr ""
+
+msgid "CiStatusLabel|pending"
+msgstr ""
+
+msgid "CiStatusLabel|skipped"
+msgstr ""
+
+msgid "CiStatusLabel|waiting for manual action"
+msgstr ""
+
+msgid "CiStatusText|blocked"
+msgstr ""
+
+msgid "CiStatusText|canceled"
+msgstr ""
+
+msgid "CiStatusText|created"
+msgstr ""
+
+msgid "CiStatusText|failed"
+msgstr ""
+
+msgid "CiStatusText|manual"
+msgstr ""
+
+msgid "CiStatusText|passed"
+msgstr ""
+
+msgid "CiStatusText|pending"
+msgstr ""
+
+msgid "CiStatusText|skipped"
+msgstr ""
+
+msgid "CiStatus|running"
+msgstr ""
+
+msgid "CiVariables|Input variable key"
+msgstr ""
+
+msgid "CiVariables|Input variable value"
+msgstr ""
+
+msgid "CiVariables|Remove variable row"
+msgstr ""
+
+msgid "CiVariable|* (All environments)"
+msgstr ""
+
+msgid "CiVariable|All environments"
+msgstr ""
+
+msgid "CiVariable|Error occured while saving variables"
+msgstr ""
+
+msgid "CiVariable|Protected"
+msgstr ""
+
+msgid "CiVariable|Toggle protected"
+msgstr ""
+
+msgid "CiVariable|Validation failed"
+msgstr ""
+
+msgid "CircuitBreakerApiLink|circuitbreaker api"
+msgstr ""
+
+msgid "Clear search input"
+msgstr ""
+
+msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
+msgstr ""
+
+msgid "Click the <strong>Promote</strong> button in the top right corner to promote it to a group milestone."
+msgstr ""
+
+msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
+msgstr ""
+
+msgid "Click to expand it."
+msgstr ""
+
+msgid "Click to expand text"
+msgstr ""
+
+msgid "Clone repository"
+msgstr ""
+
+msgid "Close"
+msgstr ""
+
+msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster"
+msgstr ""
+
+msgid "ClusterIntegration|API URL"
+msgstr ""
+
+msgid "ClusterIntegration|Add Kubernetes cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Add an existing Kubernetes cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Advanced options on this Kubernetes cluster's integration"
+msgstr ""
+
+msgid "ClusterIntegration|An error occured while trying to fetch project zones: %{error}"
+msgstr ""
+
+msgid "ClusterIntegration|An error occured while trying to fetch your projects: %{error}"
+msgstr ""
+
+msgid "ClusterIntegration|Applications"
+msgstr ""
+
+msgid "ClusterIntegration|Are you sure you want to remove this Kubernetes cluster's integration? This will not delete your actual Kubernetes cluster."
+msgstr ""
+
+msgid "ClusterIntegration|CA Certificate"
+msgstr ""
+
+msgid "ClusterIntegration|Certificate Authority bundle (PEM format)"
+msgstr ""
+
+msgid "ClusterIntegration|Choose how to set up Kubernetes cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Choose which of your project's environments will use this Kubernetes cluster."
+msgstr ""
+
+msgid "ClusterIntegration|Control how your Kubernetes cluster integrates with GitLab"
+msgstr ""
+
+msgid "ClusterIntegration|Copy API URL"
+msgstr ""
+
+msgid "ClusterIntegration|Copy CA Certificate"
+msgstr ""
+
+msgid "ClusterIntegration|Copy Ingress IP Address to clipboard"
+msgstr ""
+
+msgid "ClusterIntegration|Copy Jupyter Hostname to clipboard"
+msgstr ""
+
+msgid "ClusterIntegration|Copy Kubernetes cluster name"
+msgstr ""
+
+msgid "ClusterIntegration|Copy Token"
+msgstr ""
+
+msgid "ClusterIntegration|Create Kubernetes cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Create Kubernetes cluster on Google Kubernetes Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Create a new Kubernetes cluster on Google Kubernetes Engine right from GitLab"
+msgstr ""
+
+msgid "ClusterIntegration|Create on Google Kubernetes Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Enter the details for your Kubernetes cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Environment scope"
+msgstr ""
+
+msgid "ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab's Google Kubernetes Engine Integration."
+msgstr ""
+
+msgid "ClusterIntegration|Fetching machine types"
+msgstr ""
+
+msgid "ClusterIntegration|Fetching projects"
+msgstr ""
+
+msgid "ClusterIntegration|Fetching zones"
+msgstr ""
+
+msgid "ClusterIntegration|GitLab Integration"
+msgstr ""
+
+msgid "ClusterIntegration|GitLab Runner"
+msgstr ""
+
+msgid "ClusterIntegration|Google Cloud Platform project"
+msgstr ""
+
+msgid "ClusterIntegration|Google Kubernetes Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Google Kubernetes Engine project"
+msgstr ""
+
+msgid "ClusterIntegration|Helm Tiller"
+msgstr ""
+
+msgid "ClusterIntegration|Ingress"
+msgstr ""
+
+msgid "ClusterIntegration|Ingress IP Address"
+msgstr ""
+
+msgid "ClusterIntegration|Install"
+msgstr ""
+
+msgid "ClusterIntegration|Installed"
+msgstr ""
+
+msgid "ClusterIntegration|Installing"
+msgstr ""
+
+msgid "ClusterIntegration|Integrate Kubernetes cluster automation"
+msgstr ""
+
+msgid "ClusterIntegration|Integration status"
+msgstr ""
+
+msgid "ClusterIntegration|Jupyter Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|JupyterHub"
+msgstr ""
+
+msgid "ClusterIntegration|Kubernetes cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Kubernetes cluster details"
+msgstr ""
+
+msgid "ClusterIntegration|Kubernetes cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Kubernetes cluster integration is disabled for this project."
+msgstr ""
+
+msgid "ClusterIntegration|Kubernetes cluster integration is enabled for this project."
+msgstr ""
+
+msgid "ClusterIntegration|Kubernetes cluster integration is enabled for this project. Disabling this integration will not affect your Kubernetes cluster, it will only temporarily turn off GitLab's connection to it."
+msgstr ""
+
+msgid "ClusterIntegration|Kubernetes cluster is being created on Google Kubernetes Engine..."
+msgstr ""
+
+msgid "ClusterIntegration|Kubernetes cluster name"
+msgstr ""
+
+msgid "ClusterIntegration|Kubernetes cluster was successfully created on Google Kubernetes Engine. Refresh the page to see Kubernetes cluster's details"
+msgstr ""
+
+msgid "ClusterIntegration|Kubernetes clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way. %{link_to_help_page}"
+msgstr ""
+
+msgid "ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project"
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}."
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{help_link_start}Kubernetes%{help_link_end}."
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}."
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about environments"
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about security configuration"
+msgstr ""
+
+msgid "ClusterIntegration|Machine type"
+msgstr ""
+
+msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create Kubernetes clusters"
+msgstr ""
+
+msgid "ClusterIntegration|Manage"
+msgstr ""
+
+msgid "ClusterIntegration|Manage your Kubernetes cluster by visiting %{link_gke}"
+msgstr ""
+
+msgid "ClusterIntegration|More information"
+msgstr ""
+
+msgid "ClusterIntegration|No machine types matched your search"
+msgstr ""
+
+msgid "ClusterIntegration|No projects found"
+msgstr ""
+
+msgid "ClusterIntegration|No projects matched your search"
+msgstr ""
+
+msgid "ClusterIntegration|No zones matched your search"
+msgstr ""
+
+msgid "ClusterIntegration|Note:"
+msgstr ""
+
+msgid "ClusterIntegration|Number of nodes"
+msgstr ""
+
+msgid "ClusterIntegration|Please enter access information for your Kubernetes cluster. If you need help, you can read our %{link_to_help_page} on Kubernetes"
+msgstr ""
+
+msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgstr ""
+
+msgid "ClusterIntegration|Project namespace"
+msgstr ""
+
+msgid "ClusterIntegration|Project namespace (optional, unique)"
+msgstr ""
+
+msgid "ClusterIntegration|Prometheus"
+msgstr ""
+
+msgid "ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration."
+msgstr ""
+
+msgid "ClusterIntegration|Redeem up to $500 in free credit for Google Cloud Platform"
+msgstr ""
+
+msgid "ClusterIntegration|Remove Kubernetes cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Remove integration"
+msgstr ""
+
+msgid "ClusterIntegration|Remove this Kubernetes cluster's configuration from this project. This will not delete your actual Kubernetes cluster."
+msgstr ""
+
+msgid "ClusterIntegration|Request to begin installing failed"
+msgstr ""
+
+msgid "ClusterIntegration|Save changes"
+msgstr ""
+
+msgid "ClusterIntegration|Search machine types"
+msgstr ""
+
+msgid "ClusterIntegration|Search projects"
+msgstr ""
+
+msgid "ClusterIntegration|Search zones"
+msgstr ""
+
+msgid "ClusterIntegration|Security"
+msgstr ""
+
+msgid "ClusterIntegration|See and edit the details for your Kubernetes cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Select machine type"
+msgstr ""
+
+msgid "ClusterIntegration|Select project"
+msgstr ""
+
+msgid "ClusterIntegration|Select project and zone to choose machine type"
+msgstr ""
+
+msgid "ClusterIntegration|Select project to choose zone"
+msgstr ""
+
+msgid "ClusterIntegration|Select zone"
+msgstr ""
+
+msgid "ClusterIntegration|Select zone to choose machine type"
+msgstr ""
+
+msgid "ClusterIntegration|Service token"
+msgstr ""
+
+msgid "ClusterIntegration|Show"
+msgstr ""
+
+msgid "ClusterIntegration|Something went wrong on our end."
+msgstr ""
+
+msgid "ClusterIntegration|Something went wrong while creating your Kubernetes cluster on Google Kubernetes Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Something went wrong while installing %{title}"
+msgstr ""
+
+msgid "ClusterIntegration|The default cluster configuration grants access to a wide set of functionalities needed to successfully build and deploy a containerised application."
+msgstr ""
+
+msgid "ClusterIntegration|This account must have permissions to create a Kubernetes cluster in the %{link_to_container_project} specified below"
+msgstr ""
+
+msgid "ClusterIntegration|Toggle Kubernetes Cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Toggle Kubernetes cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Token"
+msgstr ""
+
+msgid "ClusterIntegration|Validating project billing status"
+msgstr ""
+
+msgid "ClusterIntegration|With a Kubernetes 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_kubernetes_engine}"
+msgstr ""
+
+msgid "ClusterIntegration|Zone"
+msgstr ""
+
+msgid "ClusterIntegration|access to Google Kubernetes Engine"
+msgstr ""
+
+msgid "ClusterIntegration|check the pricing here"
+msgstr ""
+
+msgid "ClusterIntegration|documentation"
+msgstr ""
+
+msgid "ClusterIntegration|help page"
+msgstr ""
+
+msgid "ClusterIntegration|installing applications"
+msgstr ""
+
+msgid "ClusterIntegration|meets the requirements"
+msgstr ""
+
+msgid "ClusterIntegration|properly configured"
+msgstr ""
+
+msgid "ClusterIntegration|sign up"
+msgstr ""
+
+msgid "Collapse"
+msgstr ""
+
+msgid "Collapse sidebar"
+msgstr ""
+
+msgid "Comment & resolve discussion"
+msgstr ""
+
+msgid "Comment & unresolve discussion"
+msgstr ""
+
+msgid "Comments"
+msgstr ""
+
+msgid "Commit"
+msgid_plural "Commits"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "Commit (%{commit_count})"
+msgid_plural "Commits (%{commit_count})"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "Commit Message"
+msgstr ""
+
+msgid "Commit duration in minutes for last 30 commits"
+msgstr ""
+
+msgid "Commit message"
+msgstr ""
+
+msgid "Commit statistics for %{ref} %{start_time} - %{end_time}"
+msgstr ""
+
+msgid "Commit to %{branchName} branch"
+msgstr ""
+
+msgid "CommitBoxTitle|Commit"
+msgstr ""
+
+msgid "CommitMessage|Add %{file_name}"
+msgstr ""
+
+msgid "Commits"
+msgstr ""
+
+msgid "Commits feed"
+msgstr ""
+
+msgid "Commits per day hour (UTC)"
+msgstr ""
+
+msgid "Commits per day of month"
+msgstr ""
+
+msgid "Commits per weekday"
+msgstr ""
+
+msgid "Commits|An error occurred while fetching merge requests data."
+msgstr ""
+
+msgid "Commits|Commit: %{commitText}"
+msgstr ""
+
+msgid "Commits|History"
+msgstr ""
+
+msgid "Commits|No related merge requests found"
+msgstr ""
+
+msgid "Committed by"
+msgstr ""
+
+msgid "Commit…"
+msgstr ""
+
+msgid "Compare"
+msgstr ""
+
+msgid "Compare Git revisions"
+msgstr ""
+
+msgid "Compare Revisions"
+msgstr ""
+
+msgid "Compare changes with the last commit"
+msgstr ""
+
+msgid "Compare changes with the merge request target branch"
+msgstr ""
+
+msgid "CompareBranches|%{source_branch} and %{target_branch} are the same."
+msgstr ""
+
+msgid "CompareBranches|Compare"
+msgstr ""
+
+msgid "CompareBranches|Source"
+msgstr ""
+
+msgid "CompareBranches|Target"
+msgstr ""
+
+msgid "CompareBranches|There isn't anything to compare."
+msgstr ""
+
+msgid "Confidential"
+msgstr ""
+
+msgid "Confidentiality"
+msgstr ""
+
+msgid "Configure Gitaly timeouts."
+msgstr ""
+
+msgid "Configure Sidekiq job throttling."
+msgstr ""
+
+msgid "Configure automatic git checks and housekeeping on repositories."
+msgstr ""
+
+msgid "Configure limits for web and API requests."
+msgstr ""
+
+msgid "Configure push mirrors."
+msgstr ""
+
+msgid "Configure storage path and circuit breaker settings."
+msgstr ""
+
+msgid "Configure the way a user creates a new account."
+msgstr ""
+
+msgid "Connect"
+msgstr ""
+
+msgid "Connect repositories from GitHub"
+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 "ContainerRegistry|You can also use a %{deploy_token} for read-only access to the registry images."
+msgstr ""
+
+msgid "Continue"
+msgstr ""
+
+msgid "Continuous Integration and Deployment"
+msgstr ""
+
+msgid "Contribute to GitLab"
+msgstr ""
+
+msgid "Contribution"
+msgstr ""
+
+msgid "Contribution guide"
+msgstr ""
+
+msgid "Contributors"
+msgstr ""
+
+msgid "ContributorsPage|%{startDate} – %{endDate}"
+msgstr ""
+
+msgid "ContributorsPage|Building repository graph."
+msgstr ""
+
+msgid "ContributorsPage|Commits to %{branch_name}, excluding merge commits. Limited to 6,000 commits."
+msgstr ""
+
+msgid "ContributorsPage|Please wait a moment, this page will automatically refresh when ready."
+msgstr ""
+
+msgid "Copy URL to clipboard"
+msgstr ""
+
+msgid "Copy branch name to clipboard"
+msgstr ""
+
+msgid "Copy command to clipboard"
+msgstr ""
+
+msgid "Copy commit SHA to clipboard"
+msgstr ""
+
+msgid "Copy file name to clipboard"
+msgstr ""
+
+msgid "Copy file path to clipboard"
+msgstr ""
+
+msgid "Copy reference to clipboard"
+msgstr ""
+
+msgid "Copy to clipboard"
+msgstr ""
+
+msgid "Create"
+msgstr ""
+
+msgid "Create New Directory"
+msgstr ""
+
+msgid "Create a new branch"
+msgstr ""
+
+msgid "Create a new branch and merge request"
+msgstr ""
+
+msgid "Create a personal access token on your account to pull or push via %{protocol}."
+msgstr ""
+
+msgid "Create branch"
+msgstr ""
+
+msgid "Create commit"
+msgstr ""
+
+msgid "Create directory"
+msgstr ""
+
+msgid "Create empty repository"
+msgstr ""
+
+msgid "Create file"
+msgstr ""
+
+msgid "Create group label"
+msgstr ""
+
+msgid "Create lists from labels. Issues with that label appear in that list."
+msgstr ""
+
+msgid "Create merge request"
+msgstr ""
+
+msgid "Create merge request and branch"
+msgstr ""
+
+msgid "Create new branch"
+msgstr ""
+
+msgid "Create new directory"
+msgstr ""
+
+msgid "Create new file"
+msgstr ""
+
+msgid "Create new label"
+msgstr ""
+
+msgid "Create new..."
+msgstr ""
+
+msgid "Create project label"
+msgstr ""
+
+msgid "CreateNewFork|Fork"
+msgstr ""
+
+msgid "CreateTag|Tag"
+msgstr ""
+
+msgid "CreateTokenToCloneLink|create a personal access token"
+msgstr ""
+
+msgid "Created"
+msgstr ""
+
+msgid "Created by me"
+msgstr ""
+
+msgid "Cron Timezone"
+msgstr ""
+
+msgid "Cron syntax"
+msgstr ""
+
+msgid "CurrentUser|Profile"
+msgstr ""
+
+msgid "CurrentUser|Settings"
+msgstr ""
+
+msgid "Custom CI config path"
+msgstr ""
+
+msgid "Custom notification events"
+msgstr ""
+
+msgid "Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notification_link}."
+msgstr ""
+
+msgid "Cycle Analytics"
+msgstr ""
+
+msgid "CycleAnalyticsStage|Code"
+msgstr ""
+
+msgid "CycleAnalyticsStage|Issue"
+msgstr ""
+
+msgid "CycleAnalyticsStage|Plan"
+msgstr ""
+
+msgid "CycleAnalyticsStage|Production"
+msgstr ""
+
+msgid "CycleAnalyticsStage|Review"
+msgstr ""
+
+msgid "CycleAnalyticsStage|Staging"
+msgstr ""
+
+msgid "CycleAnalyticsStage|Test"
+msgstr ""
+
+msgid "DashboardProjects|All"
+msgstr ""
+
+msgid "DashboardProjects|Personal"
+msgstr ""
+
+msgid "Dec"
+msgstr ""
+
+msgid "December"
+msgstr ""
+
+msgid "Decline and sign out"
+msgstr ""
+
+msgid "Define a custom pattern with cron syntax"
+msgstr ""
+
+msgid "Delete"
+msgstr ""
+
+msgid "Delete list"
+msgstr ""
+
+msgid "Deploy"
+msgid_plural "Deploys"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "Deploy Keys"
+msgstr ""
+
+msgid "DeployKeys|+%{count} others"
+msgstr ""
+
+msgid "DeployKeys|Current project"
+msgstr ""
+
+msgid "DeployKeys|Deploy key"
+msgstr ""
+
+msgid "DeployKeys|Enabled deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Error enabling deploy key"
+msgstr ""
+
+msgid "DeployKeys|Error getting deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Error removing deploy key"
+msgstr ""
+
+msgid "DeployKeys|Expand %{count} other projects"
+msgstr ""
+
+msgid "DeployKeys|Loading deploy keys"
+msgstr ""
+
+msgid "DeployKeys|No deploy keys found. Create one with the form above."
+msgstr ""
+
+msgid "DeployKeys|Privately accessible deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Project usage"
+msgstr ""
+
+msgid "DeployKeys|Publicly accessible deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Read access only"
+msgstr ""
+
+msgid "DeployKeys|Write access allowed"
+msgstr ""
+
+msgid "DeployKeys|You are going to remove this deploy key. Are you sure?"
+msgstr ""
+
+msgid "DeployTokens|Active Deploy Tokens (%{active_tokens})"
+msgstr ""
+
+msgid "DeployTokens|Add a deploy token"
+msgstr ""
+
+msgid "DeployTokens|Allows read-only access to the registry images"
+msgstr ""
+
+msgid "DeployTokens|Allows read-only access to the repository"
+msgstr ""
+
+msgid "DeployTokens|Copy deploy token to clipboard"
+msgstr ""
+
+msgid "DeployTokens|Copy username to clipboard"
+msgstr ""
+
+msgid "DeployTokens|Create deploy token"
+msgstr ""
+
+msgid "DeployTokens|Created"
+msgstr ""
+
+msgid "DeployTokens|Deploy Tokens"
+msgstr ""
+
+msgid "DeployTokens|Deploy tokens allow read-only access to your repository and registry images."
+msgstr ""
+
+msgid "DeployTokens|Expires"
+msgstr ""
+
+msgid "DeployTokens|Name"
+msgstr ""
+
+msgid "DeployTokens|Pick a name for the application, and we'll give you a unique deploy token."
+msgstr ""
+
+msgid "DeployTokens|Revoke"
+msgstr ""
+
+msgid "DeployTokens|Revoke %{name}"
+msgstr ""
+
+msgid "DeployTokens|Scopes"
+msgstr ""
+
+msgid "DeployTokens|This action cannot be undone."
+msgstr ""
+
+msgid "DeployTokens|This project has no active Deploy Tokens."
+msgstr ""
+
+msgid "DeployTokens|Use this token as a password. Make sure you save it - you won't be able to access it again."
+msgstr ""
+
+msgid "DeployTokens|Use this username as a login."
+msgstr ""
+
+msgid "DeployTokens|Username"
+msgstr ""
+
+msgid "DeployTokens|You are about to revoke"
+msgstr ""
+
+msgid "DeployTokens|Your New Deploy Token"
+msgstr ""
+
+msgid "DeployTokens|Your new project deploy token has been created."
+msgstr ""
+
+msgid "Deprioritize label"
+msgstr ""
+
+msgid "Description"
+msgstr ""
+
+msgid "Details"
+msgstr ""
+
+msgid "Diffs|No file name available"
+msgstr ""
+
+msgid "Diffs|Something went wrong while fetching diff lines."
+msgstr ""
+
+msgid "Directory name"
+msgstr ""
+
+msgid "Disable"
+msgstr ""
+
+msgid "Disable for this project"
+msgstr ""
+
+msgid "Disable group Runners"
+msgstr ""
+
+msgid "Discard changes"
+msgstr ""
+
+msgid "Discard draft"
+msgstr ""
+
+msgid "Dismiss Cycle Analytics introduction box"
+msgstr ""
+
+msgid "Domain"
+msgstr ""
+
+msgid "Don't show again"
+msgstr ""
+
+msgid "Done"
+msgstr ""
+
+msgid "Download"
+msgstr ""
+
+msgid "Download tar"
+msgstr ""
+
+msgid "Download tar.bz2"
+msgstr ""
+
+msgid "Download tar.gz"
+msgstr ""
+
+msgid "Download zip"
+msgstr ""
+
+msgid "DownloadArtifacts|Download"
+msgstr ""
+
+msgid "DownloadCommit|Email Patches"
+msgstr ""
+
+msgid "DownloadCommit|Plain Diff"
+msgstr ""
+
+msgid "DownloadSource|Download"
+msgstr ""
+
+msgid "Downvotes"
+msgstr ""
+
+msgid "Due date"
+msgstr ""
+
+msgid "Each Runner can be in one of the following states:"
+msgstr ""
+
+msgid "Edit"
+msgstr ""
+
+msgid "Edit Label"
+msgstr ""
+
+msgid "Edit Pipeline Schedule %{id}"
+msgstr ""
+
+msgid "Edit files in the editor and commit changes here"
+msgstr ""
+
+msgid "Edit identity for %{user_name}"
+msgstr ""
+
+msgid "Email"
+msgstr ""
+
+msgid "Email patch"
+msgstr ""
+
+msgid "Emails"
+msgstr ""
+
+msgid "Embed"
+msgstr ""
+
+msgid "Enable"
+msgstr ""
+
+msgid "Enable Auto DevOps"
+msgstr ""
+
+msgid "Enable Sentry for error reporting and logging."
+msgstr ""
+
+msgid "Enable and configure InfluxDB metrics."
+msgstr ""
+
+msgid "Enable and configure Prometheus metrics."
+msgstr ""
+
+msgid "Enable for this project"
+msgstr ""
+
+msgid "Enable group Runners"
+msgstr ""
+
+msgid "Enable or disable certain group features and choose access levels."
+msgstr ""
+
+msgid "Enable or disable version check and usage ping."
+msgstr ""
+
+msgid "Enable reCAPTCHA or Akismet and set IP limits."
+msgstr ""
+
+msgid "Enable the Performance Bar for a given group."
+msgstr ""
+
+msgid "Ends at (UTC)"
+msgstr ""
+
+msgid "Environments"
+msgstr ""
+
+msgid "Environments|An error occurred while fetching the environments."
+msgstr ""
+
+msgid "Environments|An error occurred while making the request."
+msgstr ""
+
+msgid "Environments|Commit"
+msgstr ""
+
+msgid "Environments|Deployment"
+msgstr ""
+
+msgid "Environments|Environment"
+msgstr ""
+
+msgid "Environments|Environments"
+msgstr ""
+
+msgid "Environments|Job"
+msgstr ""
+
+msgid "Environments|New environment"
+msgstr ""
+
+msgid "Environments|No deployments yet"
+msgstr ""
+
+msgid "Environments|Open"
+msgstr ""
+
+msgid "Environments|Re-deploy"
+msgstr ""
+
+msgid "Environments|Read more about environments"
+msgstr ""
+
+msgid "Environments|Rollback"
+msgstr ""
+
+msgid "Environments|Show all"
+msgstr ""
+
+msgid "Environments|Updated"
+msgstr ""
+
+msgid "Environments|You don't have any environments right now."
+msgstr ""
+
+msgid "Error Reporting and Logging"
+msgstr ""
+
+msgid "Error committing changes. Please try again."
+msgstr ""
+
+msgid "Error fetching contributors data."
+msgstr ""
+
+msgid "Error fetching job trace"
+msgstr ""
+
+msgid "Error fetching labels."
+msgstr ""
+
+msgid "Error fetching network graph."
+msgstr ""
+
+msgid "Error fetching refs"
+msgstr ""
+
+msgid "Error fetching usage ping data."
+msgstr ""
+
+msgid "Error loading branch data. Please try again."
+msgstr ""
+
+msgid "Error loading last commit."
+msgstr ""
+
+msgid "Error loading merge requests."
+msgstr ""
+
+msgid "Error loading project data. Please try again."
+msgstr ""
+
+msgid "Error occurred when toggling the notification subscription"
+msgstr ""
+
+msgid "Error saving label update."
+msgstr ""
+
+msgid "Error updating status for all todos."
+msgstr ""
+
+msgid "Error updating todo status."
+msgstr ""
+
+msgid "Estimated"
+msgstr ""
+
+msgid "EventFilterBy|Filter by all"
+msgstr ""
+
+msgid "EventFilterBy|Filter by comments"
+msgstr ""
+
+msgid "EventFilterBy|Filter by issue events"
+msgstr ""
+
+msgid "EventFilterBy|Filter by merge events"
+msgstr ""
+
+msgid "EventFilterBy|Filter by push events"
+msgstr ""
+
+msgid "EventFilterBy|Filter by team"
+msgstr ""
+
+msgid "Every day (at 4:00am)"
+msgstr ""
+
+msgid "Every month (on the 1st at 4:00am)"
+msgstr ""
+
+msgid "Every week (Sundays at 4:00am)"
+msgstr ""
+
+msgid "Expand"
+msgstr ""
+
+msgid "Expand all"
+msgstr ""
+
+msgid "Expand sidebar"
+msgstr ""
+
+msgid "Explore projects"
+msgstr ""
+
+msgid "Explore public groups"
+msgstr ""
+
+msgid "Failed"
+msgstr ""
+
+msgid "Failed Jobs"
+msgstr ""
+
+msgid "Failed to change the owner"
+msgstr ""
+
+msgid "Failed to check related branches."
+msgstr ""
+
+msgid "Failed to remove issue from board, please try again."
+msgstr ""
+
+msgid "Failed to remove the pipeline schedule"
+msgstr ""
+
+msgid "Failed to update issues, please try again."
+msgstr ""
+
+msgid "Failure"
+msgstr ""
+
+msgid "Faster as it re-uses the project workspace (falling back to clone if it doesn't exist)"
+msgstr ""
+
+msgid "Feb"
+msgstr ""
+
+msgid "February"
+msgstr ""
+
+msgid "Fields on this page are now uneditable, you can configure"
+msgstr ""
+
+msgid "Files"
+msgstr ""
+
+msgid "Files (%{human_size})"
+msgstr ""
+
+msgid "Filter by commit message"
+msgstr ""
+
+msgid "Find by path"
+msgstr ""
+
+msgid "Find file"
+msgstr ""
+
+msgid "Finished"
+msgstr ""
+
+msgid "FirstPushedBy|First"
+msgstr ""
+
+msgid "FirstPushedBy|pushed by"
+msgstr ""
+
+msgid "For internal projects, any logged in user can view pipelines and access job details (output logs and artifacts)"
+msgstr ""
+
+msgid "For private projects, any member (guest or higher) can view pipelines and access job details (output logs and artifacts)"
+msgstr ""
+
+msgid "For public projects, anyone can view pipelines and access job details (output logs and artifacts)"
+msgstr ""
+
+msgid "Fork"
+msgid_plural "Forks"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "ForkedFromProjectPath|Forked from"
+msgstr ""
+
+msgid "ForkedFromProjectPath|Forked from %{project_name} (deleted)"
+msgstr ""
+
+msgid "Forking in progress"
+msgstr ""
+
+msgid "Format"
+msgstr ""
+
+msgid "Found errors in your .gitlab-ci.yml:"
+msgstr ""
+
+msgid "From %{provider_title}"
+msgstr ""
+
+msgid "From issue creation until deploy to production"
+msgstr ""
+
+msgid "From merge request merge until deploy to production"
+msgstr ""
+
+msgid "From the Kubernetes cluster details view, install Runner from the applications list"
+msgstr ""
+
+msgid "GPG Keys"
+msgstr ""
+
+msgid "General"
+msgstr ""
+
+msgid "General pipelines"
+msgstr ""
+
+msgid "Generate a default set of labels"
+msgstr ""
+
+msgid "Git repository URL"
+msgstr ""
+
+msgid "Git revision"
+msgstr ""
+
+msgid "Git storage health information has been reset"
+msgstr ""
+
+msgid "Git strategy for pipelines"
+msgstr ""
+
+msgid "Git version"
+msgstr ""
+
+msgid "GitHub import"
+msgstr ""
+
+msgid "GitLab CI Linter has been moved"
+msgstr ""
+
+msgid "GitLab Group Runners can execute code for all the projects in this group."
+msgstr ""
+
+msgid "GitLab Runner section"
+msgstr ""
+
+msgid "Gitaly"
+msgstr ""
+
+msgid "Gitaly Servers"
+msgstr ""
+
+msgid "Gitaly|Address"
+msgstr ""
+
+msgid "Go Back"
+msgstr ""
+
+msgid "Go back"
+msgstr ""
+
+msgid "Go to your fork"
+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 "Got it!"
+msgstr ""
+
+msgid "Graph"
+msgstr ""
+
+msgid "Group CI/CD settings"
+msgstr ""
+
+msgid "Group ID"
+msgstr ""
+
+msgid "Group Runners"
+msgstr ""
+
+msgid "Group maintainers can register group runners in the %{link}"
+msgstr ""
+
+msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
+msgstr ""
+
+msgid "GroupSettings|Share with group lock"
+msgstr ""
+
+msgid "GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup."
+msgstr ""
+
+msgid "GroupSettings|This setting is applied on %{ancestor_group}. To share projects in this group with another group, ask the owner to override the setting or %{remove_ancestor_share_with_group_lock}."
+msgstr ""
+
+msgid "GroupSettings|This setting is applied on %{ancestor_group}. You can override the setting or %{remove_ancestor_share_with_group_lock}."
+msgstr ""
+
+msgid "GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner. Groups that already have access to the project will continue to have access unless removed manually."
+msgstr ""
+
+msgid "GroupSettings|cannot be disabled when the parent group \"Share with group lock\" is enabled, except by the owner of the parent group"
+msgstr ""
+
+msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
+msgstr ""
+
+msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
+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 "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 ""
+
+msgid "Health information can be retrieved from the following endpoints. More information is available"
+msgstr ""
+
+msgid "HealthCheck|Access token is"
+msgstr ""
+
+msgid "HealthCheck|Healthy"
+msgstr ""
+
+msgid "HealthCheck|No Health Problems Detected"
+msgstr ""
+
+msgid "HealthCheck|Unhealthy"
+msgstr ""
+
+msgid "Help"
+msgstr ""
+
+msgid "Help page"
+msgstr ""
+
+msgid "Help page text and support page url."
+msgstr ""
+
+msgid "Hide value"
+msgid_plural "Hide values"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "Hide whitespace changes"
+msgstr ""
+
+msgid "History"
+msgstr ""
+
+msgid "Housekeeping successfully started"
+msgstr ""
+
+msgid "I accept the %{terms_link}"
+msgstr ""
+
+msgid "I accept the|Terms of Service and Privacy Policy"
+msgstr ""
+
+msgid "ID"
+msgstr ""
+
+msgid "IDE|Commit"
+msgstr ""
+
+msgid "IDE|Edit"
+msgstr ""
+
+msgid "IDE|Go back"
+msgstr ""
+
+msgid "IDE|Open in file view"
+msgstr ""
+
+msgid "IDE|Review"
+msgstr ""
+
+msgid "Identifier"
+msgstr ""
+
+msgid "Identities"
+msgstr ""
+
+msgid "If disabled, the access level will depend on the user's permissions in the project."
+msgstr ""
+
+msgid "If enabled"
+msgstr ""
+
+msgid "If you already have files you can push them using the %{link_to_cli} below."
+msgstr ""
+
+msgid "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>."
+msgstr ""
+
+msgid "ImageDiffViewer|2-up"
+msgstr ""
+
+msgid "ImageDiffViewer|Onion skin"
+msgstr ""
+
+msgid "ImageDiffViewer|Swipe"
+msgstr ""
+
+msgid "Import"
+msgstr ""
+
+msgid "Import all repositories"
+msgstr ""
+
+msgid "Import in progress"
+msgstr ""
+
+msgid "Import repositories from GitHub"
+msgstr ""
+
+msgid "Import repository"
+msgstr ""
+
+msgid "Include a Terms of Service agreement and Privacy Policy that all users must accept."
+msgstr ""
+
+msgid "Inline"
+msgstr ""
+
+msgid "Install Runner on Kubernetes"
+msgstr ""
+
+msgid "Install a Runner compatible with GitLab CI"
+msgstr ""
+
+msgid "Instance does not support multiple Kubernetes clusters"
+msgstr ""
+
+msgid "Integrations"
+msgstr ""
+
+msgid "Integrations Settings"
+msgstr ""
+
+msgid "Interested parties can even contribute by pushing commits if they want to."
+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 ""
+
+msgid "Introducing Cycle Analytics"
+msgstr ""
+
+msgid "Issue Board"
+msgstr ""
+
+msgid "Issue events"
+msgstr ""
+
+msgid "IssueBoards|Board"
+msgstr ""
+
+msgid "Issues"
+msgstr ""
+
+msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
+msgstr ""
+
+msgid "Jan"
+msgstr ""
+
+msgid "January"
+msgstr ""
+
+msgid "Job"
+msgstr ""
+
+msgid "Job has been erased"
+msgstr ""
+
+msgid "Jobs"
+msgstr ""
+
+msgid "Jul"
+msgstr ""
+
+msgid "July"
+msgstr ""
+
+msgid "Jun"
+msgstr ""
+
+msgid "June"
+msgstr ""
+
+msgid "Koding"
+msgstr ""
+
+msgid "Kubernetes"
+msgstr ""
+
+msgid "Kubernetes Cluster"
+msgstr ""
+
+msgid "Kubernetes cluster creation time exceeds timeout; %{timeout}"
+msgstr ""
+
+msgid "Kubernetes cluster integration was not removed."
+msgstr ""
+
+msgid "Kubernetes cluster integration was successfully removed."
+msgstr ""
+
+msgid "Kubernetes cluster was successfully updated."
+msgstr ""
+
+msgid "Kubernetes configured"
+msgstr ""
+
+msgid "Kubernetes service integration has been deprecated. %{deprecated_message_content} your Kubernetes clusters using the new <a href=\"%{url}\"/>Kubernetes Clusters</a> page"
+msgstr ""
+
+msgid "LFS"
+msgstr ""
+
+msgid "LFSStatus|Disabled"
+msgstr ""
+
+msgid "LFSStatus|Enabled"
+msgstr ""
+
+msgid "Label"
+msgstr ""
+
+msgid "Label actions dropdown"
+msgstr ""
+
+msgid "LabelSelect|%{firstLabelName} +%{remainingLabelCount} more"
+msgstr ""
+
+msgid "LabelSelect|%{labelsString}, and %{remainingLabelCount} more"
+msgstr ""
+
+msgid "Labels"
+msgstr ""
+
+msgid "Labels can be applied to %{features}. Group labels are available for any project within the group."
+msgstr ""
+
+msgid "Labels can be applied to issues and merge requests to categorize them."
+msgstr ""
+
+msgid "Labels can be applied to issues and merge requests."
+msgstr ""
+
+msgid "Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>"
+msgstr ""
+
+msgid "Labels|Promote Label"
+msgstr ""
+
+msgid "Last %d day"
+msgid_plural "Last %d days"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "Last Pipeline"
+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 ""
+
+msgid "LastPushEvent|at"
+msgstr ""
+
+msgid "Latest changes"
+msgstr ""
+
+msgid "Learn more"
+msgstr ""
+
+msgid "Learn more about Kubernetes"
+msgstr ""
+
+msgid "Learn more about protected branches"
+msgstr ""
+
+msgid "Learn more in the"
+msgstr ""
+
+msgid "Learn more in the|pipeline schedules documentation"
+msgstr ""
+
+msgid "Leave"
+msgstr ""
+
+msgid "Leave group"
+msgstr ""
+
+msgid "Leave project"
+msgstr ""
+
+msgid "List"
+msgstr ""
+
+msgid "List your GitHub repositories"
+msgstr ""
+
+msgid "Loading the GitLab IDE..."
+msgstr ""
+
+msgid "Loading..."
+msgstr ""
+
+msgid "Lock"
+msgstr ""
+
+msgid "Lock %{issuableDisplayName}"
+msgstr ""
+
+msgid "Lock not found"
+msgstr ""
+
+msgid "Lock to current projects"
+msgstr ""
+
+msgid "Locked"
+msgstr ""
+
+msgid "Locked to current projects"
+msgstr ""
+
+msgid "Login"
+msgstr ""
+
+msgid "Manage all notifications"
+msgstr ""
+
+msgid "Manage group labels"
+msgstr ""
+
+msgid "Manage labels"
+msgstr ""
+
+msgid "Manage project labels"
+msgstr ""
+
+msgid "Mar"
+msgstr ""
+
+msgid "March"
+msgstr ""
+
+msgid "Mark todo as done"
+msgstr ""
+
+msgid "Markdown enabled"
+msgstr ""
+
+msgid "Maximum git storage failures"
+msgstr ""
+
+msgid "May"
+msgstr ""
+
+msgid "Median"
+msgstr ""
+
+msgid "Members"
+msgstr ""
+
+msgid "Merge Request:"
+msgstr ""
+
+msgid "Merge Requests"
+msgstr ""
+
+msgid "Merge events"
+msgstr ""
+
+msgid "Merge request"
+msgstr ""
+
+msgid "Merge requests"
+msgstr ""
+
+msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
+msgstr ""
+
+msgid "MergeRequests|Resolve this discussion in a new issue"
+msgstr ""
+
+msgid "MergeRequests|Saving the comment failed"
+msgstr ""
+
+msgid "MergeRequests|Toggle comments for this file"
+msgstr ""
+
+msgid "MergeRequests|Updating discussions failed"
+msgstr ""
+
+msgid "MergeRequests|View file @ %{commitId}"
+msgstr ""
+
+msgid "MergeRequests|View replaced file @ %{commitId}"
+msgstr ""
+
+msgid "Merged"
+msgstr ""
+
+msgid "Messages"
+msgstr ""
+
+msgid "Metrics - Influx"
+msgstr ""
+
+msgid "Metrics - Prometheus"
+msgstr ""
+
+msgid "Milestone"
+msgstr ""
+
+msgid "Milestones"
+msgstr ""
+
+msgid "Milestones|Delete milestone"
+msgstr ""
+
+msgid "Milestones|Delete milestone %{milestoneTitle}?"
+msgstr ""
+
+msgid "Milestones|Failed to delete milestone %{milestoneTitle}"
+msgstr ""
+
+msgid "Milestones|Milestone %{milestoneTitle} was not found"
+msgstr ""
+
+msgid "Milestones|Promote %{milestoneTitle} to group milestone?"
+msgstr ""
+
+msgid "Milestones|Promote Milestone"
+msgstr ""
+
+msgid "MissingSSHKeyWarningLink|add an SSH key"
+msgstr ""
+
+msgid "Modal|Cancel"
+msgstr ""
+
+msgid "Modal|Close"
+msgstr ""
+
+msgid "Monitoring"
+msgstr ""
+
+msgid "More actions"
+msgstr ""
+
+msgid "More information"
+msgstr ""
+
+msgid "More information is available|here"
+msgstr ""
+
+msgid "Move"
+msgstr ""
+
+msgid "Move issue"
+msgstr ""
+
+msgid "Name"
+msgstr ""
+
+msgid "Name new label"
+msgstr ""
+
+msgid "Name your individual key via a title"
+msgstr ""
+
+msgid "Nav|Help"
+msgstr ""
+
+msgid "Nav|Home"
+msgstr ""
+
+msgid "Nav|Sign In / Register"
+msgstr ""
+
+msgid "Nav|Sign out and sign in with a different account"
+msgstr ""
+
+msgid "New Identity"
+msgstr ""
+
+msgid "New Issue"
+msgid_plural "New Issues"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "New Kubernetes Cluster"
+msgstr ""
+
+msgid "New Kubernetes cluster"
+msgstr ""
+
+msgid "New Label"
+msgstr ""
+
+msgid "New Pipeline Schedule"
+msgstr ""
+
+msgid "New branch"
+msgstr ""
+
+msgid "New branch unavailable"
+msgstr ""
+
+msgid "New directory"
+msgstr ""
+
+msgid "New file"
+msgstr ""
+
+msgid "New group"
+msgstr ""
+
+msgid "New identity"
+msgstr ""
+
+msgid "New issue"
+msgstr ""
+
+msgid "New label"
+msgstr ""
+
+msgid "New merge request"
+msgstr ""
+
+msgid "New pipelines will cancel older, pending pipelines on the same branch"
+msgstr ""
+
+msgid "New project"
+msgstr ""
+
+msgid "New schedule"
+msgstr ""
+
+msgid "New snippet"
+msgstr ""
+
+msgid "New subgroup"
+msgstr ""
+
+msgid "New tag"
+msgstr ""
+
+msgid "No"
+msgstr ""
+
+msgid "No assignee"
+msgstr ""
+
+msgid "No changes"
+msgstr ""
+
+msgid "No connection could be made to a Gitaly Server, please check your logs!"
+msgstr ""
+
+msgid "No due date"
+msgstr ""
+
+msgid "No estimate or time spent"
+msgstr ""
+
+msgid "No file chosen"
+msgstr ""
+
+msgid "No files found"
+msgstr ""
+
+msgid "No files found."
+msgstr ""
+
+msgid "No merge requests found"
+msgstr ""
+
+msgid "No messages were logged"
+msgstr ""
+
+msgid "No repository"
+msgstr ""
+
+msgid "No schedules"
+msgstr ""
+
+msgid "None"
+msgstr ""
+
+msgid "Not allowed to merge"
+msgstr ""
+
+msgid "Not available"
+msgstr ""
+
+msgid "Not available for private projects"
+msgstr ""
+
+msgid "Not available for protected branches"
+msgstr ""
+
+msgid "Not confidential"
+msgstr ""
+
+msgid "Not enough data"
+msgstr ""
+
+msgid "Note that the master branch is automatically protected. %{link_to_protected_branches}"
+msgstr ""
+
+msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
+msgstr ""
+
+msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
+msgstr ""
+
+msgid "Notification events"
+msgstr ""
+
+msgid "NotificationEvent|Close issue"
+msgstr ""
+
+msgid "NotificationEvent|Close merge request"
+msgstr ""
+
+msgid "NotificationEvent|Failed pipeline"
+msgstr ""
+
+msgid "NotificationEvent|Merge merge request"
+msgstr ""
+
+msgid "NotificationEvent|New issue"
+msgstr ""
+
+msgid "NotificationEvent|New merge request"
+msgstr ""
+
+msgid "NotificationEvent|New note"
+msgstr ""
+
+msgid "NotificationEvent|Reassign issue"
+msgstr ""
+
+msgid "NotificationEvent|Reassign merge request"
+msgstr ""
+
+msgid "NotificationEvent|Reopen issue"
+msgstr ""
+
+msgid "NotificationEvent|Successful pipeline"
+msgstr ""
+
+msgid "NotificationLevel|Custom"
+msgstr ""
+
+msgid "NotificationLevel|Disabled"
+msgstr ""
+
+msgid "NotificationLevel|Global"
+msgstr ""
+
+msgid "NotificationLevel|On mention"
+msgstr ""
+
+msgid "NotificationLevel|Participate"
+msgstr ""
+
+msgid "NotificationLevel|Watch"
+msgstr ""
+
+msgid "Notifications"
+msgstr ""
+
+msgid "Notifications off"
+msgstr ""
+
+msgid "Notifications on"
+msgstr ""
+
+msgid "Nov"
+msgstr ""
+
+msgid "November"
+msgstr ""
+
+msgid "Number of access attempts"
+msgstr ""
+
+msgid "Oct"
+msgstr ""
+
+msgid "October"
+msgstr ""
+
+msgid "OfSearchInADropdown|Filter"
+msgstr ""
+
+msgid "Online IDE integration settings."
+msgstr ""
+
+msgid "Only comments from the following commit are shown below"
+msgstr ""
+
+msgid "Only project members can comment."
+msgstr ""
+
+msgid "Open in Xcode"
+msgstr ""
+
+msgid "OpenedNDaysAgo|Opened"
+msgstr ""
+
+msgid "Opens in a new window"
+msgstr ""
+
+msgid "Operations"
+msgstr ""
+
+msgid "Options"
+msgstr ""
+
+msgid "Or you can choose one of the suggested colors below"
+msgstr ""
+
+msgid "Other Labels"
+msgstr ""
+
+msgid "Otherwise it is recommended you start with one of the options below."
+msgstr ""
+
+msgid "Outbound requests"
+msgstr ""
+
+msgid "Overview"
+msgstr ""
+
+msgid "Owner"
+msgstr ""
+
+msgid "Pages"
+msgstr ""
+
+msgid "Pagination|Last »"
+msgstr ""
+
+msgid "Pagination|Next"
+msgstr ""
+
+msgid "Pagination|Prev"
+msgstr ""
+
+msgid "Pagination|« First"
+msgstr ""
+
+msgid "Part of merge request changes"
+msgstr ""
+
+msgid "Password"
+msgstr ""
+
+msgid "Paste your public SSH key, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'. Don't use your private SSH key."
+msgstr ""
+
+msgid "Pause"
+msgstr ""
+
+msgid "Pending"
+msgstr ""
+
+msgid "Per job. If a job passes this threshold, it will be marked as failed"
+msgstr ""
+
+msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgstr ""
+
+msgid "Performance optimization"
+msgstr ""
+
+msgid "Permissions"
+msgstr ""
+
+msgid "Personal Access Token"
+msgstr ""
+
+msgid "Pipeline"
+msgstr ""
+
+msgid "Pipeline Health"
+msgstr ""
+
+msgid "Pipeline Schedule"
+msgstr ""
+
+msgid "Pipeline Schedules"
+msgstr ""
+
+msgid "Pipeline triggers"
+msgstr ""
+
+msgid "PipelineCharts|Failed:"
+msgstr ""
+
+msgid "PipelineCharts|Overall statistics"
+msgstr ""
+
+msgid "PipelineCharts|Success ratio:"
+msgstr ""
+
+msgid "PipelineCharts|Successful:"
+msgstr ""
+
+msgid "PipelineCharts|Total:"
+msgstr ""
+
+msgid "PipelineSchedules|Activated"
+msgstr ""
+
+msgid "PipelineSchedules|Active"
+msgstr ""
+
+msgid "PipelineSchedules|All"
+msgstr ""
+
+msgid "PipelineSchedules|Inactive"
+msgstr ""
+
+msgid "PipelineSchedules|Next Run"
+msgstr ""
+
+msgid "PipelineSchedules|None"
+msgstr ""
+
+msgid "PipelineSchedules|Provide a short description for this pipeline"
+msgstr ""
+
+msgid "PipelineSchedules|Take ownership"
+msgstr ""
+
+msgid "PipelineSchedules|Target"
+msgstr ""
+
+msgid "PipelineSchedules|Variables"
+msgstr ""
+
+msgid "PipelineSheduleIntervalPattern|Custom"
+msgstr ""
+
+msgid "Pipelines"
+msgstr ""
+
+msgid "Pipelines charts"
+msgstr ""
+
+msgid "Pipelines for last month"
+msgstr ""
+
+msgid "Pipelines for last week"
+msgstr ""
+
+msgid "Pipelines for last year"
+msgstr ""
+
+msgid "Pipelines|Build with confidence"
+msgstr ""
+
+msgid "Pipelines|CI Lint"
+msgstr ""
+
+msgid "Pipelines|Clear Runner Caches"
+msgstr ""
+
+msgid "Pipelines|Get started with Pipelines"
+msgstr ""
+
+msgid "Pipelines|Loading Pipelines"
+msgstr ""
+
+msgid "Pipelines|Project cache successfully reset."
+msgstr ""
+
+msgid "Pipelines|Run Pipeline"
+msgstr ""
+
+msgid "Pipelines|Something went wrong while cleaning runners cache."
+msgstr ""
+
+msgid "Pipelines|There are currently no %{scope} pipelines."
+msgstr ""
+
+msgid "Pipelines|There are currently no pipelines."
+msgstr ""
+
+msgid "Pipelines|This project is not currently set up to run pipelines."
+msgstr ""
+
+msgid "Pipeline|Create for"
+msgstr ""
+
+msgid "Pipeline|Create pipeline"
+msgstr ""
+
+msgid "Pipeline|Existing branch name or tag"
+msgstr ""
+
+msgid "Pipeline|Run Pipeline"
+msgstr ""
+
+msgid "Pipeline|Search branches"
+msgstr ""
+
+msgid "Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default."
+msgstr ""
+
+msgid "Pipeline|Stop pipeline"
+msgstr ""
+
+msgid "Pipeline|Stop pipeline #%{pipelineId}?"
+msgstr ""
+
+msgid "Pipeline|Variables"
+msgstr ""
+
+msgid "Pipeline|You’re about to stop pipeline %{pipelineId}."
+msgstr ""
+
+msgid "Pipeline|all"
+msgstr ""
+
+msgid "Pipeline|success"
+msgstr ""
+
+msgid "Pipeline|with stage"
+msgstr ""
+
+msgid "Pipeline|with stages"
+msgstr ""
+
+msgid "Plain diff"
+msgstr ""
+
+msgid "PlantUML"
+msgstr ""
+
+msgid "Play"
+msgstr ""
+
+msgid "Please accept the Terms of Service before continuing."
+msgstr ""
+
+msgid "Please select at least one filter to see results"
+msgstr ""
+
+msgid "Please solve the reCAPTCHA"
+msgstr ""
+
+msgid "Please try again"
+msgstr ""
+
+msgid "Please wait while we import the repository for you. Refresh at will."
+msgstr ""
+
+msgid "Preferences"
+msgstr ""
+
+msgid "Preferences|Navigation theme"
+msgstr ""
+
+msgid "Prioritize"
+msgstr ""
+
+msgid "Prioritize label"
+msgstr ""
+
+msgid "Prioritized Labels"
+msgstr ""
+
+msgid "Prioritized label"
+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 "Private projects can be created in your personal namespace with:"
+msgstr ""
+
+msgid "Profile"
+msgstr ""
+
+msgid "Profiles|Account scheduled for removal."
+msgstr ""
+
+msgid "Profiles|Change username"
+msgstr ""
+
+msgid "Profiles|Current path: %{path}"
+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|Path"
+msgstr ""
+
+msgid "Profiles|Type your %{confirmationValue} to confirm:"
+msgstr ""
+
+msgid "Profiles|Update username"
+msgstr ""
+
+msgid "Profiles|Username change failed - %{message}"
+msgstr ""
+
+msgid "Profiles|Username successfully changed"
+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 "Profiling - Performance bar"
+msgstr ""
+
+msgid "Programming languages used in this repository"
+msgstr ""
+
+msgid "Progress"
+msgstr ""
+
+msgid "Project"
+msgstr ""
+
+msgid "Project '%{project_name}' is in the process of being deleted."
+msgstr ""
+
+msgid "Project '%{project_name}' queued for deletion."
+msgstr ""
+
+msgid "Project '%{project_name}' was successfully created."
+msgstr ""
+
+msgid "Project '%{project_name}' was successfully updated."
+msgstr ""
+
+msgid "Project Badges"
+msgstr ""
+
+msgid "Project access must be granted explicitly to each user."
+msgstr ""
+
+msgid "Project avatar"
+msgstr ""
+
+msgid "Project avatar in repository: %{link}"
+msgstr ""
+
+msgid "Project details"
+msgstr ""
+
+msgid "Project export could not be deleted."
+msgstr ""
+
+msgid "Project export has been deleted."
+msgstr ""
+
+msgid "Project export link has expired. Please generate a new export from your project settings."
+msgstr ""
+
+msgid "Project export started. A download link will be sent by email."
+msgstr ""
+
+msgid "ProjectActivityRSS|Subscribe"
+msgstr ""
+
+msgid "ProjectFileTree|Name"
+msgstr ""
+
+msgid "ProjectLastActivity|Never"
+msgstr ""
+
+msgid "ProjectLifecycle|Stage"
+msgstr ""
+
+msgid "Projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Frequently visited"
+msgstr ""
+
+msgid "ProjectsDropdown|Loading projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Projects you visit often will appear here"
+msgstr ""
+
+msgid "ProjectsDropdown|Search your projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Something went wrong on our end."
+msgstr ""
+
+msgid "ProjectsDropdown|Sorry, no projects matched your search"
+msgstr ""
+
+msgid "ProjectsDropdown|This feature requires browser localStorage support"
+msgstr ""
+
+msgid "PrometheusDashboard|Time"
+msgstr ""
+
+msgid "PrometheusService|%{exporters} with %{metrics} were found"
+msgstr ""
+
+msgid "PrometheusService|<p class=\"text-tertiary\">No <a href=\"%{docsUrl}\">common metrics</a> were found</p>"
+msgstr ""
+
+msgid "PrometheusService|Active"
+msgstr ""
+
+msgid "PrometheusService|Auto configuration"
+msgstr ""
+
+msgid "PrometheusService|Automatically deploy and configure Prometheus on your clusters to monitor your project’s environments"
+msgstr ""
+
+msgid "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."
+msgstr ""
+
+msgid "PrometheusService|Common metrics"
+msgstr ""
+
+msgid "PrometheusService|Finding and configuring metrics..."
+msgstr ""
+
+msgid "PrometheusService|Install Prometheus on clusters"
+msgstr ""
+
+msgid "PrometheusService|Manage clusters"
+msgstr ""
+
+msgid "PrometheusService|Manual configuration"
+msgstr ""
+
+msgid "PrometheusService|Metrics"
+msgstr ""
+
+msgid "PrometheusService|Metrics are automatically configured and monitored based on a library of metrics from popular exporters."
+msgstr ""
+
+msgid "PrometheusService|Missing environment variable"
+msgstr ""
+
+msgid "PrometheusService|More information"
+msgstr ""
+
+msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
+msgstr ""
+
+msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
+msgstr ""
+
+msgid "PrometheusService|Time-series monitoring service"
+msgstr ""
+
+msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgstr ""
+
+msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
+msgstr ""
+
+msgid "PrometheusService|Waiting for your first deployment to an environment to find common metrics"
+msgstr ""
+
+msgid "Promote"
+msgstr ""
+
+msgid "Promote these project milestones into a group milestone."
+msgstr ""
+
+msgid "Promote to Group Milestone"
+msgstr ""
+
+msgid "Promote to group label"
+msgstr ""
+
+msgid "Protip:"
+msgstr ""
+
+msgid "Provider"
+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 "Public pipelines"
+msgstr ""
+
+msgid "Push events"
+msgstr ""
+
+msgid "Push project from command line"
+msgstr ""
+
+msgid "Push to create a project"
+msgstr ""
+
+msgid "Quick actions can be used in the issues description and comment boxes."
+msgstr ""
+
+msgid "Re-deploy"
+msgstr ""
+
+msgid "Read more"
+msgstr ""
+
+msgid "Readme"
+msgstr ""
+
+msgid "Real-time features"
+msgstr ""
+
+msgid "Reference:"
+msgstr ""
+
+msgid "Register / Sign In"
+msgstr ""
+
+msgid "Register and see your runners for this group."
+msgstr ""
+
+msgid "Register and see your runners for this project."
+msgstr ""
+
+msgid "Registry"
+msgstr ""
+
+msgid "Related Commits"
+msgstr ""
+
+msgid "Related Deployed Jobs"
+msgstr ""
+
+msgid "Related Issues"
+msgstr ""
+
+msgid "Related Jobs"
+msgstr ""
+
+msgid "Related Merge Requests"
+msgstr ""
+
+msgid "Related Merged Requests"
+msgstr ""
+
+msgid "Related merge requests"
+msgstr ""
+
+msgid "Remind later"
+msgstr ""
+
+msgid "Remove"
+msgstr ""
+
+msgid "Remove Runner"
+msgstr ""
+
+msgid "Remove avatar"
+msgstr ""
+
+msgid "Remove priority"
+msgstr ""
+
+msgid "Remove project"
+msgstr ""
+
+msgid "Repository"
+msgstr ""
+
+msgid "Repository Settings"
+msgstr ""
+
+msgid "Repository maintenance"
+msgstr ""
+
+msgid "Repository mirror"
+msgstr ""
+
+msgid "Repository storage"
+msgstr ""
+
+msgid "Request Access"
+msgstr ""
+
+msgid "Require all users to accept Terms of Service and Privacy Policy when they access GitLab."
+msgstr ""
+
+msgid "Reset git storage health information"
+msgstr ""
+
+msgid "Reset health check access token"
+msgstr ""
+
+msgid "Reset runners registration token"
+msgstr ""
+
+msgid "Resolve all discussions in new issue"
+msgstr ""
+
+msgid "Resolve conflicts on source branch"
+msgstr ""
+
+msgid "Resolve discussion"
+msgstr ""
+
+msgid "Resume"
+msgstr ""
+
+msgid "Retry"
+msgstr ""
+
+msgid "Retry this job"
+msgstr ""
+
+msgid "Retry verification"
+msgstr ""
+
+msgid "Reveal value"
+msgid_plural "Reveal values"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "Revert this commit"
+msgstr ""
+
+msgid "Revert this merge request"
+msgstr ""
+
+msgid "Review"
+msgstr ""
+
+msgid "Reviewing"
+msgstr ""
+
+msgid "Reviewing (merge request !%{mergeRequestId})"
+msgstr ""
+
+msgid "Rollback"
+msgstr ""
+
+msgid "Runner token"
+msgstr ""
+
+msgid "Runners"
+msgstr ""
+
+msgid "Runners API"
+msgstr ""
+
+msgid "Runners can be placed on separate users, servers, and even on your local machine."
+msgstr ""
+
+msgid "Running"
+msgstr ""
+
+msgid "SSH Keys"
+msgstr ""
+
+msgid "SSL Verification"
+msgstr ""
+
+msgid "Save"
+msgstr ""
+
+msgid "Save changes"
+msgstr ""
+
+msgid "Save pipeline schedule"
+msgstr ""
+
+msgid "Save variables"
+msgstr ""
+
+msgid "Schedule a new pipeline"
+msgstr ""
+
+msgid "Scheduled"
+msgstr ""
+
+msgid "Schedules"
+msgstr ""
+
+msgid "Scheduling Pipelines"
+msgstr ""
+
+msgid "Scroll to bottom"
+msgstr ""
+
+msgid "Scroll to top"
+msgstr ""
+
+msgid "Search"
+msgstr ""
+
+msgid "Search branches"
+msgstr ""
+
+msgid "Search branches and tags"
+msgstr ""
+
+msgid "Search files"
+msgstr ""
+
+msgid "Search for projects, issues, etc."
+msgstr ""
+
+msgid "Search merge requests"
+msgstr ""
+
+msgid "Search milestones"
+msgstr ""
+
+msgid "Search project"
+msgstr ""
+
+msgid "Search users"
+msgstr ""
+
+msgid "Seconds before reseting failure information"
+msgstr ""
+
+msgid "Seconds to wait for a storage access attempt"
+msgstr ""
+
+msgid "Select"
+msgstr ""
+
+msgid "Select Archive Format"
+msgstr ""
+
+msgid "Select a namespace to fork the project"
+msgstr ""
+
+msgid "Select a timezone"
+msgstr ""
+
+msgid "Select an existing Kubernetes cluster or create a new one"
+msgstr ""
+
+msgid "Select assignee"
+msgstr ""
+
+msgid "Select branch/tag"
+msgstr ""
+
+msgid "Select project"
+msgstr ""
+
+msgid "Select project and zone to choose machine type"
+msgstr ""
+
+msgid "Select project to choose zone"
+msgstr ""
+
+msgid "Select source branch"
+msgstr ""
+
+msgid "Select target branch"
+msgstr ""
+
+msgid "Send email"
+msgstr ""
+
+msgid "Sep"
+msgstr ""
+
+msgid "September"
+msgstr ""
+
+msgid "Server version"
+msgstr ""
+
+msgid "Service Templates"
+msgstr ""
+
+msgid "Session expiration, projects limit and attachment size."
+msgstr ""
+
+msgid "Set a password on your account to pull or push via %{protocol}."
+msgstr ""
+
+msgid "Set default and restrict visibility levels. Configure import sources and git access protocol."
+msgstr ""
+
+msgid "Set max session time for web terminal."
+msgstr ""
+
+msgid "Set notification email for abuse reports."
+msgstr ""
+
+msgid "Set requirements for a user to sign-in. Enable mandatory two-factor authentication."
+msgstr ""
+
+msgid "Set up CI/CD"
+msgstr ""
+
+msgid "Set up Koding"
+msgstr ""
+
+msgid "SetPasswordToCloneLink|set a password"
+msgstr ""
+
+msgid "Settings"
+msgstr ""
+
+msgid "Setup a specific Runner automatically"
+msgstr ""
+
+msgid "Share"
+msgstr ""
+
+msgid "Shared Runners"
+msgstr ""
+
+msgid "Show command"
+msgstr ""
+
+msgid "Show complete raw log"
+msgstr ""
+
+msgid "Show latest version"
+msgstr ""
+
+msgid "Show latest version of the diff"
+msgstr ""
+
+msgid "Show parent pages"
+msgstr ""
+
+msgid "Show parent subgroups"
+msgstr ""
+
+msgid "Show whitespace changes"
+msgstr ""
+
+msgid "Showing %d event"
+msgid_plural "Showing %d events"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "Side-by-side"
+msgstr ""
+
+msgid "Sign out"
+msgstr ""
+
+msgid "Sign-in restrictions"
+msgstr ""
+
+msgid "Sign-up restrictions"
+msgstr ""
+
+msgid "Size and domain settings for static websites"
+msgstr ""
+
+msgid "Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job"
+msgstr ""
+
+msgid "Snippets"
+msgstr ""
+
+msgid "Something went wrong on our end"
+msgstr ""
+
+msgid "Something went wrong on our end."
+msgstr ""
+
+msgid "Something went wrong on our end. Please try again!"
+msgstr ""
+
+msgid "Something went wrong when toggling the button"
+msgstr ""
+
+msgid "Something went wrong while closing the %{issuable}. Please try again later"
+msgstr ""
+
+msgid "Something went wrong while fetching the projects."
+msgstr ""
+
+msgid "Something went wrong while fetching the registry list."
+msgstr ""
+
+msgid "Something went wrong while reopening the %{issuable}. Please try again later"
+msgstr ""
+
+msgid "Something went wrong while resolving this discussion. Please try again."
+msgstr ""
+
+msgid "Something went wrong. Please try again."
+msgstr ""
+
+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 ""
+
+msgid "SortOptions|Due later"
+msgstr ""
+
+msgid "SortOptions|Due soon"
+msgstr ""
+
+msgid "SortOptions|Label priority"
+msgstr ""
+
+msgid "SortOptions|Largest group"
+msgstr ""
+
+msgid "SortOptions|Largest repository"
+msgstr ""
+
+msgid "SortOptions|Last created"
+msgstr ""
+
+msgid "SortOptions|Last joined"
+msgstr ""
+
+msgid "SortOptions|Last updated"
+msgstr ""
+
+msgid "SortOptions|Least popular"
+msgstr ""
+
+msgid "SortOptions|Milestone"
+msgstr ""
+
+msgid "SortOptions|Milestone due later"
+msgstr ""
+
+msgid "SortOptions|Milestone due soon"
+msgstr ""
+
+msgid "SortOptions|Most popular"
+msgstr ""
+
+msgid "SortOptions|Name"
+msgstr ""
+
+msgid "SortOptions|Name, ascending"
+msgstr ""
+
+msgid "SortOptions|Name, descending"
+msgstr ""
+
+msgid "SortOptions|Oldest created"
+msgstr ""
+
+msgid "SortOptions|Oldest joined"
+msgstr ""
+
+msgid "SortOptions|Oldest sign in"
+msgstr ""
+
+msgid "SortOptions|Oldest updated"
+msgstr ""
+
+msgid "SortOptions|Popularity"
+msgstr ""
+
+msgid "SortOptions|Priority"
+msgstr ""
+
+msgid "SortOptions|Recent sign in"
+msgstr ""
+
+msgid "SortOptions|Start later"
+msgstr ""
+
+msgid "SortOptions|Start soon"
+msgstr ""
+
+msgid "Source"
+msgstr ""
+
+msgid "Source (branch or tag)"
+msgstr ""
+
+msgid "Source code"
+msgstr ""
+
+msgid "Source is not available"
+msgstr ""
+
+msgid "Spam Logs"
+msgstr ""
+
+msgid "Spam and Anti-bot Protection"
+msgstr ""
+
+msgid "Specific Runners"
+msgstr ""
+
+msgid "Specify the following URL during the Runner setup:"
+msgstr ""
+
+msgid "Squash commits"
+msgstr ""
+
+msgid "Stage"
+msgstr ""
+
+msgid "Stage & Commit"
+msgstr ""
+
+msgid "Stage all changes"
+msgstr ""
+
+msgid "Stage changes"
+msgstr ""
+
+msgid "Staged"
+msgstr ""
+
+msgid "Staged %{type}"
+msgstr ""
+
+msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
+msgstr ""
+
+msgid "StarProject|Star"
+msgstr ""
+
+msgid "Starred Projects"
+msgstr ""
+
+msgid "Starred Projects' Activity"
+msgstr ""
+
+msgid "Starred projects"
+msgstr ""
+
+msgid "Start a %{new_merge_request} with these changes"
+msgstr ""
+
+msgid "Start the Runner!"
+msgstr ""
+
+msgid "Started"
+msgstr ""
+
+msgid "Starts at (UTC)"
+msgstr ""
+
+msgid "Status"
+msgstr ""
+
+msgid "Stop this environment"
+msgstr ""
+
+msgid "Stopped"
+msgstr ""
+
+msgid "Storage"
+msgstr ""
+
+msgid "Subgroups"
+msgstr ""
+
+msgid "Subscribe"
+msgstr ""
+
+msgid "Subscribe at group level"
+msgstr ""
+
+msgid "Subscribe at project level"
+msgstr ""
+
+msgid "Switch branch/tag"
+msgstr ""
+
+msgid "System Hooks"
+msgstr ""
+
+msgid "Tag (%{tag_count})"
+msgid_plural "Tags (%{tag_count})"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "Tags"
+msgstr ""
+
+msgid "Tags:"
+msgstr ""
+
+msgid "TagsPage|Browse commits"
+msgstr ""
+
+msgid "TagsPage|Browse files"
+msgstr ""
+
+msgid "TagsPage|Can't find HEAD commit for this tag"
+msgstr ""
+
+msgid "TagsPage|Cancel"
+msgstr ""
+
+msgid "TagsPage|Create tag"
+msgstr ""
+
+msgid "TagsPage|Delete tag"
+msgstr ""
+
+msgid "TagsPage|Deleting the %{tag_name} tag cannot be undone. Are you sure?"
+msgstr ""
+
+msgid "TagsPage|Edit release notes"
+msgstr ""
+
+msgid "TagsPage|Existing branch name, tag, or commit SHA"
+msgstr ""
+
+msgid "TagsPage|Filter by tag name"
+msgstr ""
+
+msgid "TagsPage|New Tag"
+msgstr ""
+
+msgid "TagsPage|New tag"
+msgstr ""
+
+msgid "TagsPage|Optionally, add a message to the tag."
+msgstr ""
+
+msgid "TagsPage|Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page."
+msgstr ""
+
+msgid "TagsPage|Release notes"
+msgstr ""
+
+msgid "TagsPage|Repository has no tags yet."
+msgstr ""
+
+msgid "TagsPage|Sort by"
+msgstr ""
+
+msgid "TagsPage|Tags"
+msgstr ""
+
+msgid "TagsPage|Tags give the ability to mark specific points in history as being important"
+msgstr ""
+
+msgid "TagsPage|This tag has no release notes."
+msgstr ""
+
+msgid "TagsPage|Use git tag command to add a new one:"
+msgstr ""
+
+msgid "TagsPage|Write your release notes or drag files here…"
+msgstr ""
+
+msgid "TagsPage|protected"
+msgstr ""
+
+msgid "Target Branch"
+msgstr ""
+
+msgid "Target branch"
+msgstr ""
+
+msgid "Team"
+msgstr ""
+
+msgid "Terms of Service Agreement and Privacy Policy"
+msgstr ""
+
+msgid "Terms of Service and Privacy Policy"
+msgstr ""
+
+msgid "Test coverage parsing"
+msgstr ""
+
+msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project"
+msgstr ""
+
+msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
+msgstr ""
+
+msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request."
+msgstr ""
+
+msgid "The collection of events added to the data gathered for that stage."
+msgstr ""
+
+msgid "The fork relationship has been removed."
+msgstr ""
+
+msgid "The import will time out after %{timeout}. For repositories that take longer, use a clone/push combination."
+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 maximum file size allowed is 200KB."
+msgstr ""
+
+msgid "The number of attempts GitLab will make to access a storage."
+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 path to CI config file. Defaults to <code>.gitlab-ci.yml</code>"
+msgstr ""
+
+msgid "The phase of the development lifecycle."
+msgstr ""
+
+msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit."
+msgstr ""
+
+msgid "The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle."
+msgstr ""
+
+msgid "The project can be accessed by any logged in user."
+msgstr ""
+
+msgid "The project can be accessed without any authentication."
+msgstr ""
+
+msgid "The repository for this project does not exist."
+msgstr ""
+
+msgid "The repository for this project is empty"
+msgstr ""
+
+msgid "The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>."
+msgstr ""
+
+msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request."
+msgstr ""
+
+msgid "The secure token used by the Runner to checkout the project"
+msgstr ""
+
+msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time."
+msgstr ""
+
+msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running."
+msgstr ""
+
+msgid "The time 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 in seconds between storage checks. When a previous check did complete yet, GitLab will skip a check."
+msgstr ""
+
+msgid "The time taken by each data entry gathered by that stage."
+msgstr ""
+
+msgid "The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6."
+msgstr ""
+
+msgid "There are no issues to show"
+msgstr ""
+
+msgid "There are no labels yet"
+msgstr ""
+
+msgid "There are no merge requests to show"
+msgstr ""
+
+msgid "There are problems accessing Git storage: "
+msgstr ""
+
+msgid "There was an error loading jobs"
+msgstr ""
+
+msgid "There was an error loading latest pipeline"
+msgstr ""
+
+msgid "There was an error loading users activity calendar."
+msgstr ""
+
+msgid "There was an error saving your notification settings."
+msgstr ""
+
+msgid "There was an error subscribing to this label."
+msgstr ""
+
+msgid "There was an error when reseting email token."
+msgstr ""
+
+msgid "There was an error when subscribing to this label."
+msgstr ""
+
+msgid "There was an error when unsubscribing from this label."
+msgstr ""
+
+msgid "They can be managed using the %{link}."
+msgstr ""
+
+msgid "This GitLab instance does not provide any shared Runners yet. Instance administrators can register shared Runners in the admin area."
+msgstr ""
+
+msgid "This diff is collapsed."
+msgstr ""
+
+msgid "This directory"
+msgstr ""
+
+msgid "This group does not provide any group Runners yet."
+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"
+msgstr ""
+
+msgid "This issue is confidential and locked."
+msgstr ""
+
+msgid "This issue is locked."
+msgstr ""
+
+msgid "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+msgstr ""
+
+msgid "This job depends on upstream jobs that need to succeed in order for this job to be triggered"
+msgstr ""
+
+msgid "This job does not have a trace."
+msgstr ""
+
+msgid "This job has been canceled"
+msgstr ""
+
+msgid "This job has been skipped"
+msgstr ""
+
+msgid "This job has not been triggered yet"
+msgstr ""
+
+msgid "This job has not started yet"
+msgstr ""
+
+msgid "This job is in pending state and is waiting to be picked by a runner"
+msgstr ""
+
+msgid "This job requires a manual action"
+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 "This option is disabled while you still have unstaged changes"
+msgstr ""
+
+msgid "This page is unavailable because you are not allowed to read information across multiple projects."
+msgstr ""
+
+msgid "This page will be removed in a future release."
+msgstr ""
+
+msgid "This project"
+msgstr ""
+
+msgid "This project does not belong to a group and can therefore not make use of group Runners."
+msgstr ""
+
+msgid "This repository"
+msgstr ""
+
+msgid "This source diff could not be displayed because it is too large."
+msgstr ""
+
+msgid "This user has no identities"
+msgstr ""
+
+msgid "Time before an issue gets scheduled"
+msgstr ""
+
+msgid "Time before an issue starts implementation"
+msgstr ""
+
+msgid "Time between merge request creation and merge/close"
+msgstr ""
+
+msgid "Time remaining"
+msgstr ""
+
+msgid "Time spent"
+msgstr ""
+
+msgid "Time tracking"
+msgstr ""
+
+msgid "Time until first merge request"
+msgstr ""
+
+msgid "TimeTrackingEstimated|Est"
+msgstr ""
+
+msgid "TimeTracking|Estimated:"
+msgstr ""
+
+msgid "TimeTracking|Spent"
+msgstr ""
+
+msgid "Timeago|%s days ago"
+msgstr ""
+
+msgid "Timeago|%s days remaining"
+msgstr ""
+
+msgid "Timeago|%s hours ago"
+msgstr ""
+
+msgid "Timeago|%s hours remaining"
+msgstr ""
+
+msgid "Timeago|%s minutes ago"
+msgstr ""
+
+msgid "Timeago|%s minutes remaining"
+msgstr ""
+
+msgid "Timeago|%s months ago"
+msgstr ""
+
+msgid "Timeago|%s months remaining"
+msgstr ""
+
+msgid "Timeago|%s seconds ago"
+msgstr ""
+
+msgid "Timeago|%s seconds remaining"
+msgstr ""
+
+msgid "Timeago|%s weeks ago"
+msgstr ""
+
+msgid "Timeago|%s weeks remaining"
+msgstr ""
+
+msgid "Timeago|%s years ago"
+msgstr ""
+
+msgid "Timeago|%s years remaining"
+msgstr ""
+
+msgid "Timeago|1 day ago"
+msgstr ""
+
+msgid "Timeago|1 day remaining"
+msgstr ""
+
+msgid "Timeago|1 hour ago"
+msgstr ""
+
+msgid "Timeago|1 hour remaining"
+msgstr ""
+
+msgid "Timeago|1 minute ago"
+msgstr ""
+
+msgid "Timeago|1 minute remaining"
+msgstr ""
+
+msgid "Timeago|1 month ago"
+msgstr ""
+
+msgid "Timeago|1 month remaining"
+msgstr ""
+
+msgid "Timeago|1 week ago"
+msgstr ""
+
+msgid "Timeago|1 week remaining"
+msgstr ""
+
+msgid "Timeago|1 year ago"
+msgstr ""
+
+msgid "Timeago|1 year remaining"
+msgstr ""
+
+msgid "Timeago|Past due"
+msgstr ""
+
+msgid "Timeago|in %s days"
+msgstr ""
+
+msgid "Timeago|in %s hours"
+msgstr ""
+
+msgid "Timeago|in %s minutes"
+msgstr ""
+
+msgid "Timeago|in %s months"
+msgstr ""
+
+msgid "Timeago|in %s seconds"
+msgstr ""
+
+msgid "Timeago|in %s weeks"
+msgstr ""
+
+msgid "Timeago|in %s years"
+msgstr ""
+
+msgid "Timeago|in 1 day"
+msgstr ""
+
+msgid "Timeago|in 1 hour"
+msgstr ""
+
+msgid "Timeago|in 1 minute"
+msgstr ""
+
+msgid "Timeago|in 1 month"
+msgstr ""
+
+msgid "Timeago|in 1 week"
+msgstr ""
+
+msgid "Timeago|in 1 year"
+msgstr ""
+
+msgid "Timeago|just now"
+msgstr ""
+
+msgid "Timeago|right now"
+msgstr ""
+
+msgid "Timeout"
+msgstr ""
+
+msgid "Time|hr"
+msgid_plural "Time|hrs"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "Time|min"
+msgid_plural "Time|mins"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "Time|s"
+msgstr ""
+
+msgid "Tip:"
+msgstr ""
+
+msgid "To GitLab"
+msgstr ""
+
+msgid "To add an SSH key you need to %{generate_link_start}generate one%{link_end} or use an %{existing_link_start}existing key%{link_end}."
+msgstr ""
+
+msgid "To import GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
+msgstr ""
+
+msgid "To import GitHub repositories, you first need to authorize GitLab to access the list of your GitHub repositories:"
+msgstr ""
+
+msgid "To import an SVN repository, check out %{svn_link}."
+msgstr ""
+
+msgid "To start serving your jobs you can add Runners to your group"
+msgstr ""
+
+msgid "To validate your GitLab CI configurations, go to 'CI/CD → Pipelines' inside your project, and click on the 'CI Lint' button."
+msgstr ""
+
+msgid "Todo"
+msgstr ""
+
+msgid "Toggle Sidebar"
+msgstr ""
+
+msgid "Toggle discussion"
+msgstr ""
+
+msgid "Toggle sidebar"
+msgstr ""
+
+msgid "ToggleButton|Toggle Status: OFF"
+msgstr ""
+
+msgid "ToggleButton|Toggle Status: ON"
+msgstr ""
+
+msgid "Too many changes to show."
+msgstr ""
+
+msgid "Total Time"
+msgstr ""
+
+msgid "Total test time for all commits/merges"
+msgstr ""
+
+msgid "Total: %{total}"
+msgstr ""
+
+msgid "Track time with quick actions"
+msgstr ""
+
+msgid "Trigger this manual action"
+msgstr ""
+
+msgid "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."
+msgstr ""
+
+msgid "Try again"
+msgstr ""
+
+msgid "Unable to load the diff. %{button_try_again}"
+msgstr ""
+
+msgid "Unlock"
+msgstr ""
+
+msgid "Unlocked"
+msgstr ""
+
+msgid "Unresolve discussion"
+msgstr ""
+
+msgid "Unstage all changes"
+msgstr ""
+
+msgid "Unstage changes"
+msgstr ""
+
+msgid "Unstaged"
+msgstr ""
+
+msgid "Unstaged %{type}"
+msgstr ""
+
+msgid "Unstaged and staged %{type}"
+msgstr ""
+
+msgid "Unstar"
+msgstr ""
+
+msgid "Unsubscribe"
+msgstr ""
+
+msgid "Unsubscribe at group level"
+msgstr ""
+
+msgid "Unsubscribe at project level"
+msgstr ""
+
+msgid "Unverified"
+msgstr ""
+
+msgid "Up to date"
+msgstr ""
+
+msgid "Update %{files}"
+msgid_plural "Update %{files} files"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "Update your group name, description, avatar, and other general settings."
+msgstr ""
+
+msgid "Upload New File"
+msgstr ""
+
+msgid "Upload file"
+msgstr ""
+
+msgid "Upload new avatar"
+msgstr ""
+
+msgid "UploadLink|click to upload"
+msgstr ""
+
+msgid "Upvotes"
+msgstr ""
+
+msgid "Usage statistics"
+msgstr ""
+
+msgid "Use group milestones to manage issues from multiple projects in the same milestone."
+msgstr ""
+
+msgid "Use the following registration token during setup:"
+msgstr ""
+
+msgid "Use your global notification setting"
+msgstr ""
+
+msgid "User and IP Rate Limits"
+msgstr ""
+
+msgid "Users"
+msgstr ""
+
+msgid "Variables"
+msgstr ""
+
+msgid "Variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. You can use variables for passwords, secret keys, or whatever you want."
+msgstr ""
+
+msgid "Various container registry settings."
+msgstr ""
+
+msgid "Various email settings."
+msgstr ""
+
+msgid "Various settings that affect GitLab performance."
+msgstr ""
+
+msgid "Verified"
+msgstr ""
+
+msgid "View file @ "
+msgstr ""
+
+msgid "View group labels"
+msgstr ""
+
+msgid "View jobs"
+msgstr ""
+
+msgid "View labels"
+msgstr ""
+
+msgid "View log"
+msgstr ""
+
+msgid "View open merge request"
+msgstr ""
+
+msgid "View project labels"
+msgstr ""
+
+msgid "View replaced file @ "
+msgstr ""
+
+msgid "Visibility and access controls"
+msgstr ""
+
+msgid "VisibilityLevel|Internal"
+msgstr ""
+
+msgid "VisibilityLevel|Private"
+msgstr ""
+
+msgid "VisibilityLevel|Public"
+msgstr ""
+
+msgid "VisibilityLevel|Unknown"
+msgstr ""
+
+msgid "Want to see the data? Please ask an administrator for access."
+msgstr ""
+
+msgid "We don't have enough data to show this stage."
+msgstr ""
+
+msgid "We want to be sure it is you, please confirm you are not a robot."
+msgstr ""
+
+msgid "Web IDE"
+msgstr ""
+
+msgid "Web terminal"
+msgstr ""
+
+msgid "When a runner is locked, it cannot be assigned to other projects"
+msgstr ""
+
+msgid "When enabled, users cannot use GitLab until the terms have been accepted."
+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 "WikiEditPageTip|Tip: You can move this page by adding the path to the beginning of the title."
+msgstr ""
+
+msgid "WikiEdit|There is already a page with the same title in that path."
+msgstr ""
+
+msgid "WikiEmptyIssueMessage|Suggest wiki improvement"
+msgstr ""
+
+msgid "WikiEmptyIssueMessage|You must be a project member in order to add wiki pages. If you have suggestions for how to improve the wiki for this project, consider opening an issue in the %{issues_link}."
+msgstr ""
+
+msgid "WikiEmptyIssueMessage|issue tracker"
+msgstr ""
+
+msgid "WikiEmpty|A wiki is where you can store all the details about your project. This can include why you've created it, its principles, how to use it, and so on."
+msgstr ""
+
+msgid "WikiEmpty|Create your first page"
+msgstr ""
+
+msgid "WikiEmpty|Suggest wiki improvement"
+msgstr ""
+
+msgid "WikiEmpty|The wiki lets you write documentation for your project"
+msgstr ""
+
+msgid "WikiEmpty|This project has no wiki pages"
+msgstr ""
+
+msgid "WikiEmpty|You must be a project member in order to add 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 "WikiPageConfirmDelete|Delete page"
+msgstr ""
+
+msgid "WikiPageConfirmDelete|Delete page %{pageTitle}?"
+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|More Pages"
+msgstr ""
+
+msgid "Wiki|New page"
+msgstr ""
+
+msgid "Wiki|Page history"
+msgstr ""
+
+msgid "Wiki|Page version"
+msgstr ""
+
+msgid "Wiki|Pages"
+msgstr ""
+
+msgid "Wiki|Wiki Pages"
+msgstr ""
+
+msgid "Withdraw Access Request"
+msgstr ""
+
+msgid "Yes"
+msgstr ""
+
+msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
+msgstr ""
+
+msgid "You are going to remove %{project_full_name}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?"
+msgstr ""
+
+msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?"
+msgstr ""
+
+msgid "You are going to transfer %{project_full_name} to another owner. Are you ABSOLUTELY sure?"
+msgstr ""
+
+msgid "You are on a read-only GitLab instance."
+msgstr ""
+
+msgid "You can %{linkStart}view the blob%{linkEnd} instead."
+msgstr ""
+
+msgid "You can also create a project from the command line."
+msgstr ""
+
+msgid "You can also star a label to make it a priority label."
+msgstr ""
+
+msgid "You can also test your .gitlab-ci.yml in the %{linkStart}Lint%{linkEnd}"
+msgstr ""
+
+msgid "You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}"
+msgstr ""
+
+msgid "You can move around the graph by using the arrow keys."
+msgstr ""
+
+msgid "You can only add files when you are on a branch"
+msgstr ""
+
+msgid "You can only edit files when you are on a branch"
+msgstr ""
+
+msgid "You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}"
+msgstr ""
+
+msgid "You cannot write to this read-only GitLab instance."
+msgstr ""
+
+msgid "You do not have any assigned merge requests"
+msgstr ""
+
+msgid "You have no permissions"
+msgstr ""
+
+msgid "You have not created any merge requests"
+msgstr ""
+
+msgid "You have reached your project limit"
+msgstr ""
+
+msgid "You must accept our Terms of Service and privacy policy in order to register an account"
+msgstr ""
+
+msgid "You must have maintainer access to force delete a lock"
+msgstr ""
+
+msgid "You must sign in to star a project"
+msgstr ""
+
+msgid "You need permission."
+msgstr ""
+
+msgid "You will not get any notifications via email"
+msgstr ""
+
+msgid "You will only receive notifications for the events you choose"
+msgstr ""
+
+msgid "You will only receive notifications for threads you have participated in"
+msgstr ""
+
+msgid "You will receive notifications for any activity"
+msgstr ""
+
+msgid "You will receive notifications only for comments in which you were @mentioned"
+msgstr ""
+
+msgid "You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account"
+msgstr ""
+
+msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile"
+msgstr ""
+
+msgid "You won't be able to pull or push project code via SSH until you add an SSH key to your profile"
+msgstr ""
+
+msgid "You'll need to use different branch names to get a valid comparison."
+msgstr ""
+
+msgid "You're receiving this email because of your account on %{host}. %{manage_notifications_link} &middot; %{help_link}"
+msgstr ""
+
+msgid "Your Groups"
+msgstr ""
+
+msgid "Your Kubernetes cluster information on this page is still editable, but you are advised to disable and reconfigure"
+msgstr ""
+
+msgid "Your Projects (default)"
+msgstr ""
+
+msgid "Your Projects' Activity"
+msgstr ""
+
+msgid "Your Todos"
+msgstr ""
+
+msgid "Your changes can be committed to %{branch_name} because a merge request is open."
+msgstr ""
+
+msgid "Your changes have been committed. Commit %{commitId} %{commitStats}"
+msgstr ""
+
+msgid "Your comment will not be visible to the public."
+msgstr ""
+
+msgid "Your groups"
+msgstr ""
+
+msgid "Your name"
+msgstr ""
+
+msgid "Your projects"
+msgstr ""
+
+msgid "ago"
+msgstr ""
+
+msgid "among other things"
+msgstr ""
+
+msgid "assign yourself"
+msgstr ""
+
+msgid "branch name"
+msgstr ""
+
+msgid "command line instructions"
+msgstr ""
+
+msgid "connecting"
+msgstr ""
+
+msgid "day"
+msgid_plural "days"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "deploy token"
+msgstr ""
+
+msgid "disabled"
+msgstr ""
+
+msgid "enabled"
+msgstr ""
+
+msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
+msgstr ""
+
+msgid "for this project"
+msgstr ""
+
+msgid "importing"
+msgstr ""
+
+msgid "latest version"
+msgstr ""
+
+msgid "merge request"
+msgid_plural "merge requests"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "mrWidget| Please restore it or use a different %{missingBranchName} branch"
+msgstr ""
+
+msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} decreased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB"
+msgstr ""
+
+msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} increased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB"
+msgstr ""
+
+msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage is %{emphasisStart} unchanged %{emphasisEnd} at %{memoryFrom}MB"
+msgstr ""
+
+msgid "mrWidget|Allows commits from members who can merge to the target branch"
+msgstr ""
+
+msgid "mrWidget|Cancel automatic merge"
+msgstr ""
+
+msgid "mrWidget|Check out branch"
+msgstr ""
+
+msgid "mrWidget|Checking ability to merge automatically"
+msgstr ""
+
+msgid "mrWidget|Cherry-pick"
+msgstr ""
+
+msgid "mrWidget|Cherry-pick this merge request in a new merge request"
+msgstr ""
+
+msgid "mrWidget|Closed"
+msgstr ""
+
+msgid "mrWidget|Closed by"
+msgstr ""
+
+msgid "mrWidget|Closes"
+msgstr ""
+
+msgid "mrWidget|Create an issue to resolve them later"
+msgstr ""
+
+msgid "mrWidget|Deployment statistics are not available currently"
+msgstr ""
+
+msgid "mrWidget|Did not close"
+msgstr ""
+
+msgid "mrWidget|Email patches"
+msgstr ""
+
+msgid "mrWidget|Failed to load deployment statistics"
+msgstr ""
+
+msgid "mrWidget|If the %{branch} branch exists in your local repository, you can merge this merge request manually using the"
+msgstr ""
+
+msgid "mrWidget|If the %{missingBranchName} branch exists in your local repository, you can merge this merge request manually using the command line"
+msgstr ""
+
+msgid "mrWidget|Loading deployment statistics"
+msgstr ""
+
+msgid "mrWidget|Mentions"
+msgstr ""
+
+msgid "mrWidget|Merge"
+msgstr ""
+
+msgid "mrWidget|Merge failed."
+msgstr ""
+
+msgid "mrWidget|Merge locally"
+msgstr ""
+
+msgid "mrWidget|Merged by"
+msgstr ""
+
+msgid "mrWidget|Plain diff"
+msgstr ""
+
+msgid "mrWidget|Refresh"
+msgstr ""
+
+msgid "mrWidget|Refresh now"
+msgstr ""
+
+msgid "mrWidget|Refreshing now"
+msgstr ""
+
+msgid "mrWidget|Remove Source Branch"
+msgstr ""
+
+msgid "mrWidget|Remove source branch"
+msgstr ""
+
+msgid "mrWidget|Request to merge"
+msgstr ""
+
+msgid "mrWidget|Resolve conflicts"
+msgstr ""
+
+msgid "mrWidget|Revert"
+msgstr ""
+
+msgid "mrWidget|Revert this merge request in a new merge request"
+msgstr ""
+
+msgid "mrWidget|Set by"
+msgstr ""
+
+msgid "mrWidget|The changes were merged into"
+msgstr ""
+
+msgid "mrWidget|The changes were not merged into"
+msgstr ""
+
+msgid "mrWidget|The changes will be merged into"
+msgstr ""
+
+msgid "mrWidget|The source branch has been removed"
+msgstr ""
+
+msgid "mrWidget|The source branch is being removed"
+msgstr ""
+
+msgid "mrWidget|The source branch will be removed"
+msgstr ""
+
+msgid "mrWidget|The source branch will not be removed"
+msgstr ""
+
+msgid "mrWidget|There are merge conflicts"
+msgstr ""
+
+msgid "mrWidget|There are unresolved discussions. Please resolve these discussions"
+msgstr ""
+
+msgid "mrWidget|This merge request failed to be merged automatically"
+msgstr ""
+
+msgid "mrWidget|This merge request is in the process of being merged"
+msgstr ""
+
+msgid "mrWidget|This project is archived, write access has been disabled"
+msgstr ""
+
+msgid "mrWidget|Web IDE"
+msgstr ""
+
+msgid "mrWidget|You can merge this merge request manually using the"
+msgstr ""
+
+msgid "mrWidget|You can remove source branch now"
+msgstr ""
+
+msgid "mrWidget|branch does not exist."
+msgstr ""
+
+msgid "mrWidget|command line"
+msgstr ""
+
+msgid "mrWidget|into"
+msgstr ""
+
+msgid "mrWidget|to be merged automatically when the pipeline succeeds"
+msgstr ""
+
+msgid "new merge request"
+msgstr ""
+
+msgid "notification emails"
+msgstr ""
+
+msgid "or"
+msgstr ""
+
+msgid "parent"
+msgid_plural "parents"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "password"
+msgstr ""
+
+msgid "personal access token"
+msgstr ""
+
+msgid "remaining"
+msgstr ""
+
+msgid "remove due date"
+msgstr ""
+
+msgid "source"
+msgstr ""
+
+msgid "spendCommand|%{slash_command} will update the sum of the time spent."
+msgstr ""
+
+msgid "this document"
+msgstr ""
+
+msgid "username"
+msgstr ""
+
+msgid "uses Kubernetes clusters to deploy your code!"
+msgstr ""
+
+msgid "with %{additions} additions, %{deletions} deletions."
+msgstr ""
+
+msgid "within %d minute "
+msgid_plural "within %d minutes "
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
diff --git a/locale/de/gitlab.po b/locale/de/gitlab.po
index 5a5cf1a19a3..c6487a3a77d 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: 2018-04-04 19:35+0200\n"
-"PO-Revision-Date: 2018-04-05 03:37-0400\n"
+"POT-Creation-Date: 2018-07-01 16:35+1000\n"
+"PO-Revision-Date: 2018-07-01 11:02\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: German\n"
"Language: de_DE\n"
@@ -16,8 +16,10 @@ msgstr ""
"X-Crowdin-Language: de\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-msgid " and"
-msgstr ""
+msgid "%d changed file"
+msgid_plural "%d changed files"
+msgstr[0] ""
+msgstr[1] ""
msgid "%d commit"
msgid_plural "%d commits"
@@ -54,6 +56,16 @@ msgid_plural "%d metrics"
msgstr[0] ""
msgstr[1] ""
+msgid "%d staged change"
+msgid_plural "%d staged changes"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "%d unstaged change"
+msgid_plural "%d unstaged changes"
+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."
@@ -70,12 +82,21 @@ msgid_plural "%{count} participants"
msgstr[0] ""
msgstr[1] ""
+msgid "%{filePath} deleted"
+msgstr ""
+
+msgid "%{group_docs_link_start}Groups%{group_docs_link_end} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects."
+msgstr ""
+
msgid "%{loadingIcon} Started"
msgstr ""
msgid "%{lock_path} is locked by GitLab User %{lock_user_id}"
msgstr ""
+msgid "%{nip_domain} can be used as an alternative to a custom domain."
+msgstr ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -88,6 +109,9 @@ msgstr "%{number_of_failures} von %{maximum_failures} Fehlschlägen. GitLab wird
msgid "%{openOrClose} %{noteable}"
msgstr ""
+msgid "%{percent}%% complete"
+msgstr ""
+
msgid "%{storage_name}: failed storage access attempt on host:"
msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:"
msgstr[0] "%{storage_name}: fehlgeschlagener Speicherzugriff auf Host:"
@@ -96,15 +120,62 @@ msgstr[1] "%{storage_name}: %{failed_attempts} fehlgeschlagene Speicherzugriffe:
msgid "%{text} is available"
msgstr ""
+msgid "%{title} changes"
+msgstr ""
+
+msgid "%{unstaged} unstaged and %{staged} staged changes"
+msgstr ""
+
msgid "(checkout the %{link} for information on how to install it)."
msgstr "(beachte die Informationen zur Installation auf %{link})."
msgid "+ %{moreCount} more"
msgstr ""
+msgid "- Runner is active and can process any new jobs"
+msgstr ""
+
+msgid "- Runner is paused and will not receive any new jobs"
+msgstr ""
+
msgid "- show less"
msgstr ""
+msgid "1 %{type} addition"
+msgid_plural "%{count} %{type} additions"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 %{type} modification"
+msgid_plural "%{count} %{type} modifications"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 closed issue"
+msgid_plural "%d closed issues"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 closed merge request"
+msgid_plural "%d closed merge requests"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 merged merge request"
+msgid_plural "%d merged merge requests"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 open issue"
+msgid_plural "%d open issues"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 open merge request"
+msgid_plural "%d open merge requests"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] ""
@@ -116,9 +187,27 @@ msgstr ""
msgid "2FA enabled"
msgstr ""
+msgid "403|Please contact your GitLab administrator to get the permission."
+msgstr ""
+
+msgid "403|You don't have the permission to access this page."
+msgstr ""
+
+msgid "404|Make sure the address is correct and the page hasn't moved."
+msgstr ""
+
+msgid "404|Page Not Found"
+msgstr ""
+
+msgid "404|Please contact your GitLab administrator if you think this is a mistake."
+msgstr ""
+
msgid "<strong>Removes</strong> source branch"
msgstr ""
+msgid "A 'Runner' is a process which runs a job. You can setup as many Runners as you need."
+msgstr ""
+
msgid "A collection of graphs regarding Continuous Integration"
msgstr "Eine Sammlung von Graphen bezüglich kontinuierlicher Integration"
@@ -128,6 +217,9 @@ msgstr ""
msgid "A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}."
msgstr ""
+msgid "A regular expression that will be used to find the test coverage output in the job trace. Leave blank to disable"
+msgstr ""
+
msgid "A user with write access to the source branch selected this option"
msgstr ""
@@ -140,36 +232,39 @@ msgstr ""
msgid "Abuse reports"
msgstr ""
+msgid "Accept terms"
+msgstr ""
+
msgid "Access Tokens"
msgstr ""
msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again."
msgstr "Zugriff auf fehlerhafte Speicher wurde vorübergehend deaktiviert, um die Wiederherstellung zu ermöglichen. Für den zukünftigen Zugriff, behebe bitte das Problem und setze danach die Speicherinformationen zurück."
+msgid "Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report."
+msgstr ""
+
msgid "Account"
msgstr "Konto"
-msgid "Account and limit settings"
+msgid "Account and limit"
msgstr ""
msgid "Active"
msgstr "Aktiv"
+msgid "Active Sessions"
+msgstr ""
+
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 Kubernetes cluster"
msgstr ""
@@ -182,6 +277,9 @@ msgstr ""
msgid "Add new directory"
msgstr "Erstelle eine neues Verzeichnis"
+msgid "Add reaction"
+msgstr ""
+
msgid "Add todo"
msgstr ""
@@ -251,7 +349,10 @@ msgstr ""
msgid "All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings."
msgstr ""
-msgid "Allow edits from maintainers."
+msgid "Allow commits from members who can merge to the target branch."
+msgstr ""
+
+msgid "Allow public access to pipelines and job details, including output logs and artifacts"
msgstr ""
msgid "Allow rendering of PlantUML diagrams in Asciidoc documents."
@@ -263,31 +364,37 @@ msgstr ""
msgid "Allows you to add and manage Kubernetes clusters."
msgstr ""
-msgid "Also called \"Issuer\" or \"Relying party trust identifier\""
+msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
msgstr ""
-msgid "Also called \"Relying party service URL\" or \"Reply URL\""
+msgid "An error occured creating the new branch."
msgstr ""
-msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to connect."
+msgid "An error occured whilst loading all the files."
msgstr ""
-msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
+msgid "An error occured whilst loading the file content."
msgstr ""
-msgid "An error occurred previewing the blob"
+msgid "An error occured whilst loading the file."
msgstr ""
-msgid "An error occurred when toggling the notification subscription"
+msgid "An error occured whilst loading the merge request changes."
+msgstr ""
+
+msgid "An error occured whilst loading the merge request version data."
+msgstr ""
+
+msgid "An error occured whilst loading the merge request."
msgstr ""
-msgid "An error occurred when updating the issue weight"
+msgid "An error occurred previewing the blob"
msgstr ""
-msgid "An error occurred while adding approver"
+msgid "An error occurred when toggling the notification subscription"
msgstr ""
-msgid "An error occurred while detecting host keys"
+msgid "An error occurred while dismissing the alert. Refresh the page and try again."
msgstr ""
msgid "An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again."
@@ -305,10 +412,7 @@ msgstr ""
msgid "An error occurred while getting projects"
msgstr ""
-msgid "An error occurred while importing project"
-msgstr ""
-
-msgid "An error occurred while initializing path locks"
+msgid "An error occurred while importing project: ${details}"
msgstr ""
msgid "An error occurred while loading commits"
@@ -326,9 +430,6 @@ msgstr ""
msgid "An error occurred while making the request."
msgstr ""
-msgid "An error occurred while removing approver"
-msgstr ""
-
msgid "An error occurred while rendering KaTeX"
msgstr ""
@@ -341,9 +442,6 @@ msgstr ""
msgid "An error occurred while retrieving diff"
msgstr ""
-msgid "An error occurred while saving LDAP override status. Please try again."
-msgstr ""
-
msgid "An error occurred while saving assignees"
msgstr ""
@@ -353,9 +451,6 @@ msgstr ""
msgid "An error occurred. Please try again."
msgstr ""
-msgid "Any Label"
-msgstr ""
-
msgid "Appearance"
msgstr ""
@@ -368,28 +463,28 @@ msgstr ""
msgid "April"
msgstr ""
-msgid "Archived project! Repository is read-only"
-msgstr "Archiviertes Projekt! Repository ist nicht änderbar."
+msgid "Archived project! Repository and other project resources are read-only"
+msgstr ""
msgid "Are you sure you want to delete this pipeline schedule?"
msgstr "Bist Du sicher, dass Du diesen Pipeline-Zeitplan löschen möchtest?"
+msgid "Are you sure you want to remove this identity?"
+msgstr ""
+
msgid "Are you sure you want to reset registration token?"
msgstr "Bist Du sicher, dass Du den Registrierungstoken zurücksetzen willst?"
msgid "Are you sure you want to reset the health check token?"
msgstr "Bist Du sicher, dass Du den Systemüberwachungstoken zurücksetzen willst?"
-msgid "Are you sure you want to unlock %{path_lock_path}?"
-msgstr ""
-
msgid "Are you sure?"
msgstr "Bist Du sicher?"
msgid "Artifacts"
msgstr ""
-msgid "Assertion consumer service URL"
+msgid "Ask your group maintainer to setup a group Runner."
msgstr ""
msgid "Assign custom color like #FF0000"
@@ -413,9 +508,15 @@ msgstr ""
msgid "Assigned to :name"
msgstr ""
+msgid "Assigned to me"
+msgstr ""
+
msgid "Assignee"
msgstr ""
+msgid "Assignee(s)"
+msgstr ""
+
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "Datei mittels Drag &amp; Drop oder %{upload_link} hinzufügen"
@@ -449,7 +550,10 @@ msgstr ""
msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
msgstr ""
-msgid "AutoDevOps|Auto DevOps (Beta)"
+msgid "Auto-cancel redundant, pending pipelines"
+msgstr ""
+
+msgid "AutoDevOps|Auto DevOps"
msgstr ""
msgid "AutoDevOps|Auto DevOps documentation"
@@ -470,79 +574,109 @@ msgstr ""
msgid "AutoDevOps|add a Kubernetes cluster"
msgstr ""
-msgid "AutoDevOps|enable Auto DevOps (Beta)"
+msgid "AutoDevOps|enable Auto DevOps"
msgstr ""
msgid "Available"
msgstr ""
+msgid "Available group Runners : %{runners}"
+msgstr ""
+
+msgid "Available group Runners : %{runners}."
+msgstr ""
+
msgid "Avatar will be removed. Are you sure?"
msgstr ""
msgid "Average per day: %{average}"
msgstr ""
-msgid "Background Color"
+msgid "Background color"
msgstr ""
msgid "Background jobs"
msgstr ""
-msgid "Begin with the selected commit"
+msgid "Badges"
msgstr ""
-msgid "Billing"
+msgid "Badges|A new badge was added."
msgstr ""
-msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan."
+msgid "Badges|Add badge"
msgstr ""
-msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available."
+msgid "Badges|Adding the badge failed, please check the entered URLs and try again."
msgstr ""
-msgid "BillingPlans|Current plan"
+msgid "Badges|Badge image URL"
msgstr ""
-msgid "BillingPlans|Customer Support"
+msgid "Badges|Badge image preview"
msgstr ""
-msgid "BillingPlans|Downgrade"
+msgid "Badges|Delete badge"
msgstr ""
-msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
+msgid "Badges|Delete badge?"
msgstr ""
-msgid "BillingPlans|Manage plan"
+msgid "Badges|Deleting the badge failed, please try again."
msgstr ""
-msgid "BillingPlans|Please contact %{customer_support_link} in that case."
+msgid "Badges|Group Badge"
msgstr ""
-msgid "BillingPlans|See all %{plan_name} features"
+msgid "Badges|Link"
msgstr ""
-msgid "BillingPlans|This group uses the plan associated with its parent group."
+msgid "Badges|No badge image"
msgstr ""
-msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
+msgid "Badges|No image to preview"
msgstr ""
-msgid "BillingPlans|Upgrade"
+msgid "Badges|Project Badge"
msgstr ""
-msgid "BillingPlans|You are currently on the %{plan_link} plan."
+msgid "Badges|Reload badge image"
msgstr ""
-msgid "BillingPlans|frequently asked questions"
+msgid "Badges|Save changes"
msgstr ""
-msgid "BillingPlans|monthly"
+msgid "Badges|Saving the badge failed, please check the entered URLs and try again."
msgstr ""
-msgid "BillingPlans|paid annually at %{price_per_year}"
+msgid "Badges|The %{docsLinkStart}variables%{docsLinkEnd} GitLab supports: %{placeholders}"
msgstr ""
-msgid "BillingPlans|per user"
+msgid "Badges|The badge was deleted."
+msgstr ""
+
+msgid "Badges|The badge was saved."
+msgstr ""
+
+msgid "Badges|This group has no badges"
+msgstr ""
+
+msgid "Badges|This project has no badges"
+msgstr ""
+
+msgid "Badges|Your badges"
+msgstr ""
+
+msgid "Begin with the selected commit"
+msgstr ""
+
+msgid "Below are examples of regex for existing tools:"
+msgstr ""
+
+msgid "Boards"
+msgstr ""
+
+msgid "Branch %{branchName} was not found in this project's repository."
msgstr ""
msgid "Branch (%{branch_count})"
@@ -622,7 +756,7 @@ msgstr ""
msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
msgstr ""
-msgid "Branches|Only a project master or owner can delete a protected branch"
+msgid "Branches|Only a project maintainer or owner can delete a protected branch"
msgstr ""
msgid "Branches|Overview"
@@ -658,9 +792,6 @@ msgstr ""
msgid "Branches|Stale branches"
msgstr ""
-msgid "Branches|The branch could not be updated automatically because it has diverged from its upstream counterpart."
-msgstr ""
-
msgid "Branches|The default branch cannot be deleted"
msgstr ""
@@ -673,15 +804,9 @@ msgstr ""
msgid "Branches|To confirm, type %{branch_name_confirmation}:"
msgstr ""
-msgid "Branches|To discard the local changes and overwrite the branch with the upstream version, delete it here and choose 'Update Now' above."
-msgstr ""
-
msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
msgstr ""
-msgid "Branches|diverged from upstream"
-msgstr ""
-
msgid "Branches|merged"
msgstr ""
@@ -703,40 +828,79 @@ msgstr "Dateien durchsuchen"
msgid "Browse files"
msgstr "Dateien durchsuchen"
-msgid "Business"
-msgstr ""
-
msgid "ByAuthor|by"
msgstr "von"
msgid "CI / CD"
msgstr "CI / CD"
-msgid "CI/CD"
+msgid "CI / CD Settings"
msgstr ""
msgid "CI/CD configuration"
msgstr ""
-msgid "CI/CD for external repo"
+msgid "CI/CD settings"
+msgstr ""
+
+msgid "CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery."
+msgstr ""
+
+msgid "CICD|Auto DevOps"
+msgstr ""
+
+msgid "CICD|Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration."
+msgstr ""
+
+msgid "CICD|Automatic deployment to staging, manual deployment to production"
+msgstr ""
+
+msgid "CICD|Continuous deployment to production"
+msgstr ""
+
+msgid "CICD|Deployment strategy"
+msgstr ""
+
+msgid "CICD|Deployment strategy needs a domain name to work correctly."
+msgstr ""
+
+msgid "CICD|Disable Auto DevOps"
+msgstr ""
+
+msgid "CICD|Enable Auto DevOps"
+msgstr ""
+
+msgid "CICD|Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific %{ci_file}."
+msgstr ""
+
+msgid "CICD|Instance default (%{state})"
msgstr ""
msgid "CICD|Jobs"
msgstr ""
-msgid "Cancel"
-msgstr "Abbrechen"
+msgid "CICD|Learn more about Auto DevOps"
+msgstr ""
-msgid "Cannot be merged automatically"
+msgid "CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project."
msgstr ""
-msgid "Cannot modify managed Kubernetes cluster"
+msgid "CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages."
+msgstr ""
+
+msgid "Can't find HEAD commit for this branch"
msgstr ""
-msgid "Certificate fingerprint"
+msgid "Cancel"
+msgstr "Abbrechen"
+
+msgid "Cancel this job"
msgstr ""
-msgid "Change Weight"
+msgid "Cannot be merged automatically"
+msgstr ""
+
+msgid "Cannot modify managed Kubernetes cluster"
msgstr ""
msgid "Change this value to influence how frequently the GitLab UI polls for updates."
@@ -790,21 +954,18 @@ msgstr ""
msgid "Choose a branch/tag (e.g. %{master}) or enter a commit (e.g. %{sha}) to see what's changed or to create a merge request."
msgstr ""
-msgid "Choose file..."
+msgid "Choose any color."
msgstr ""
-msgid "Choose which groups you wish to synchronize to this secondary node."
+msgid "Choose between <code>clone</code> or <code>fetch</code> to get the recent application code"
msgstr ""
-msgid "Choose which repositories you want to connect and run CI/CD pipelines."
+msgid "Choose file..."
msgstr ""
msgid "Choose which repositories you want to import."
msgstr ""
-msgid "Choose which shards you wish to synchronize to this secondary node."
-msgstr ""
-
msgid "CiStatusLabel|canceled"
msgstr "abgebrochen"
@@ -874,21 +1035,12 @@ msgstr ""
msgid "CiVariable|All environments"
msgstr ""
-msgid "CiVariable|Create wildcard"
-msgstr ""
-
msgid "CiVariable|Error occured while saving variables"
msgstr ""
-msgid "CiVariable|New environment"
-msgstr ""
-
msgid "CiVariable|Protected"
msgstr ""
-msgid "CiVariable|Search environments"
-msgstr ""
-
msgid "CiVariable|Toggle protected"
msgstr ""
@@ -898,28 +1050,28 @@ msgstr ""
msgid "CircuitBreakerApiLink|circuitbreaker api"
msgstr ""
-msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
+msgid "Clear search input"
msgstr ""
-msgid "Click to expand text"
+msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Client authentication certificate"
+msgid "Click the <strong>Promote</strong> button in the top right corner to promote it to a group milestone."
msgstr ""
-msgid "Client authentication key"
+msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
msgstr ""
-msgid "Client authentication key password"
+msgid "Click to expand it."
msgstr ""
-msgid "Clone repository"
+msgid "Click to expand text"
msgstr ""
-msgid "Close"
+msgid "Clone repository"
msgstr ""
-msgid "Closed"
+msgid "Close"
msgstr ""
msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster"
@@ -937,6 +1089,12 @@ msgstr ""
msgid "ClusterIntegration|Advanced options on this Kubernetes cluster's integration"
msgstr ""
+msgid "ClusterIntegration|An error occured while trying to fetch project zones: %{error}"
+msgstr ""
+
+msgid "ClusterIntegration|An error occured while trying to fetch your projects: %{error}"
+msgstr ""
+
msgid "ClusterIntegration|Applications"
msgstr ""
@@ -967,6 +1125,9 @@ msgstr ""
msgid "ClusterIntegration|Copy Ingress IP Address to clipboard"
msgstr ""
+msgid "ClusterIntegration|Copy Jupyter Hostname to clipboard"
+msgstr ""
+
msgid "ClusterIntegration|Copy Kubernetes cluster name"
msgstr ""
@@ -982,7 +1143,7 @@ msgstr ""
msgid "ClusterIntegration|Create a new Kubernetes cluster on Google Kubernetes Engine right from GitLab"
msgstr ""
-msgid "ClusterIntegration|Create on GKE"
+msgid "ClusterIntegration|Create on Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
@@ -994,13 +1155,25 @@ msgstr ""
msgid "ClusterIntegration|Environment scope"
msgstr ""
+msgid "ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab's Google Kubernetes Engine Integration."
+msgstr ""
+
+msgid "ClusterIntegration|Fetching machine types"
+msgstr ""
+
+msgid "ClusterIntegration|Fetching projects"
+msgstr ""
+
+msgid "ClusterIntegration|Fetching zones"
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
msgid "ClusterIntegration|GitLab Runner"
msgstr ""
-msgid "ClusterIntegration|Google Cloud Platform project ID"
+msgid "ClusterIntegration|Google Cloud Platform project"
msgstr ""
msgid "ClusterIntegration|Google Kubernetes Engine"
@@ -1012,9 +1185,6 @@ msgstr ""
msgid "ClusterIntegration|Helm Tiller"
msgstr ""
-msgid "ClusterIntegration|In order to show the health of the cluster, we'll need to provision your cluster with Prometheus to collect the required data."
-msgstr ""
-
msgid "ClusterIntegration|Ingress"
msgstr ""
@@ -1024,9 +1194,6 @@ msgstr ""
msgid "ClusterIntegration|Install"
msgstr ""
-msgid "ClusterIntegration|Install Prometheus"
-msgstr ""
-
msgid "ClusterIntegration|Installed"
msgstr ""
@@ -1039,13 +1206,16 @@ msgstr ""
msgid "ClusterIntegration|Integration status"
msgstr ""
-msgid "ClusterIntegration|Kubernetes cluster"
+msgid "ClusterIntegration|Jupyter Hostname"
msgstr ""
-msgid "ClusterIntegration|Kubernetes cluster details"
+msgid "ClusterIntegration|JupyterHub"
msgstr ""
-msgid "ClusterIntegration|Kubernetes cluster health"
+msgid "ClusterIntegration|Kubernetes cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Kubernetes cluster details"
msgstr ""
msgid "ClusterIntegration|Kubernetes cluster integration"
@@ -1075,7 +1245,13 @@ msgstr ""
msgid "ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project"
msgstr ""
-msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
+msgid "ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}."
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{help_link_start}Kubernetes%{help_link_end}."
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}."
msgstr ""
msgid "ClusterIntegration|Learn more about environments"
@@ -1099,7 +1275,16 @@ msgstr ""
msgid "ClusterIntegration|More information"
msgstr ""
-msgid "ClusterIntegration|Multiple Kubernetes clusters are available in GitLab Enterprise Edition Premium and Ultimate"
+msgid "ClusterIntegration|No machine types matched your search"
+msgstr ""
+
+msgid "ClusterIntegration|No projects found"
+msgstr ""
+
+msgid "ClusterIntegration|No projects matched your search"
+msgstr ""
+
+msgid "ClusterIntegration|No zones matched your search"
msgstr ""
msgid "ClusterIntegration|Note:"
@@ -1114,9 +1299,6 @@ msgstr ""
msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
msgstr ""
-msgid "ClusterIntegration|Project ID"
-msgstr ""
-
msgid "ClusterIntegration|Project namespace"
msgstr ""
@@ -1129,6 +1311,9 @@ msgstr ""
msgid "ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration."
msgstr ""
+msgid "ClusterIntegration|Redeem up to $500 in free credit for Google Cloud Platform"
+msgstr ""
+
msgid "ClusterIntegration|Remove Kubernetes cluster integration"
msgstr ""
@@ -1144,19 +1329,37 @@ msgstr ""
msgid "ClusterIntegration|Save changes"
msgstr ""
+msgid "ClusterIntegration|Search machine types"
+msgstr ""
+
+msgid "ClusterIntegration|Search projects"
+msgstr ""
+
+msgid "ClusterIntegration|Search zones"
+msgstr ""
+
msgid "ClusterIntegration|Security"
msgstr ""
msgid "ClusterIntegration|See and edit the details for your Kubernetes cluster"
msgstr ""
-msgid "ClusterIntegration|See machine types"
+msgid "ClusterIntegration|Select machine type"
+msgstr ""
+
+msgid "ClusterIntegration|Select project"
+msgstr ""
+
+msgid "ClusterIntegration|Select project and zone to choose machine type"
+msgstr ""
+
+msgid "ClusterIntegration|Select project to choose zone"
msgstr ""
-msgid "ClusterIntegration|See your projects"
+msgid "ClusterIntegration|Select zone"
msgstr ""
-msgid "ClusterIntegration|See zones"
+msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
msgid "ClusterIntegration|Service token"
@@ -1189,6 +1392,9 @@ msgstr ""
msgid "ClusterIntegration|Token"
msgstr ""
+msgid "ClusterIntegration|Validating project billing status"
+msgstr ""
+
msgid "ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
@@ -1219,13 +1425,19 @@ msgstr ""
msgid "ClusterIntegration|properly configured"
msgstr ""
+msgid "ClusterIntegration|sign up"
+msgstr ""
+
msgid "Collapse"
msgstr ""
-msgid "Comment and resolve discussion"
+msgid "Collapse sidebar"
+msgstr ""
+
+msgid "Comment & resolve discussion"
msgstr ""
-msgid "Comment and unresolve discussion"
+msgid "Comment & unresolve discussion"
msgstr ""
msgid "Comments"
@@ -1292,6 +1504,9 @@ msgstr ""
msgid "Committed by"
msgstr "Committed von"
+msgid "Commit…"
+msgstr ""
+
msgid "Compare"
msgstr "Vergleichen"
@@ -1340,6 +1555,9 @@ msgstr ""
msgid "Configure limits for web and API requests."
msgstr ""
+msgid "Configure push mirrors."
+msgstr ""
+
msgid "Configure storage path and circuit breaker settings."
msgstr ""
@@ -1349,18 +1567,9 @@ msgstr ""
msgid "Connect"
msgstr ""
-msgid "Connect all repositories"
-msgstr ""
-
msgid "Connect repositories from GitHub"
msgstr ""
-msgid "Connect your external repositories, and CI/CD pipelines will run for new commits. A GitLab project will be created with only CI/CD features enabled."
-msgstr ""
-
-msgid "Connecting..."
-msgstr ""
-
msgid "Container Registry"
msgstr ""
@@ -1406,9 +1615,18 @@ 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 "ContainerRegistry|You can also use a %{deploy_token} for read-only access to the registry images."
+msgstr ""
+
+msgid "Continue"
+msgstr ""
+
msgid "Continuous Integration and Deployment"
msgstr ""
+msgid "Contribute to GitLab"
+msgstr ""
+
msgid "Contribution"
msgstr ""
@@ -1430,15 +1648,6 @@ msgstr ""
msgid "ContributorsPage|Please wait a moment, this page will automatically refresh when ready."
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 ""
-
msgid "Copy URL to clipboard"
msgstr "Kopiere URL in die Zwischenablage"
@@ -1451,9 +1660,18 @@ msgstr ""
msgid "Copy commit SHA to clipboard"
msgstr "Kopiere Commit SHA in die Zwischenablage"
+msgid "Copy file name to clipboard"
+msgstr ""
+
+msgid "Copy file path to clipboard"
+msgstr ""
+
msgid "Copy reference to clipboard"
msgstr ""
+msgid "Copy to clipboard"
+msgstr ""
+
msgid "Create"
msgstr ""
@@ -1472,15 +1690,15 @@ msgstr "Erstelle einen persönlichen Zugriffstoken in Deinem Konto um mittels %{
msgid "Create branch"
msgstr ""
+msgid "Create commit"
+msgstr ""
+
msgid "Create directory"
msgstr "Erstelle Verzeichnis"
msgid "Create empty repository"
msgstr ""
-msgid "Create epic"
-msgstr ""
-
msgid "Create file"
msgstr ""
@@ -1523,13 +1741,10 @@ msgstr "Tag "
msgid "CreateTokenToCloneLink|create a personal access token"
msgstr "Erstelle einen persönlichen Zugriffstoken"
-msgid "Creates a new branch from %{branchName}"
+msgid "Created"
msgstr ""
-msgid "Creates a new branch from %{branchName} and re-directs to create a new merge request"
-msgstr ""
-
-msgid "Creating epic"
+msgid "Created by me"
msgstr ""
msgid "Cron Timezone"
@@ -1538,7 +1753,13 @@ msgstr "Cron Zeitzone"
msgid "Cron syntax"
msgstr "Cron Syntax"
-msgid "Current node"
+msgid "CurrentUser|Profile"
+msgstr ""
+
+msgid "CurrentUser|Settings"
+msgstr ""
+
+msgid "Custom CI config path"
msgstr ""
msgid "Custom notification events"
@@ -1547,9 +1768,6 @@ msgstr "Individuelle Benachrichtigungsereignisse"
msgid "Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notification_link}."
msgstr "Individuelle Benachrichtigungsstufen sind identisch mit den Beteiligungsstufen. Mit individuellen Benachrichtigungsstufen erhältst Du ebenfalls Mitteilungen für ausgewählte Ereignisse. Für weitere Informationen lies %{notification_link}. "
-msgid "Customize colors"
-msgstr ""
-
msgid "Cycle Analytics"
msgstr "Arbeitsablaufsanalysen"
@@ -1586,7 +1804,7 @@ msgstr ""
msgid "December"
msgstr ""
-msgid "Default classification label"
+msgid "Decline and sign out"
msgstr ""
msgid "Define a custom pattern with cron syntax"
@@ -1595,6 +1813,9 @@ msgstr "Erstelle ein individuelles Muster mittels Cron Syntax"
msgid "Delete"
msgstr "Löschen"
+msgid "Delete list"
+msgstr ""
+
msgid "Deploy"
msgid_plural "Deploys"
msgstr[0] "Bereitstellung"
@@ -1603,37 +1824,163 @@ msgstr[1] "Bereitstellungen"
msgid "Deploy Keys"
msgstr ""
-msgid "Description"
-msgstr "Beschreibung"
+msgid "DeployKeys|+%{count} others"
+msgstr ""
-msgid "Description templates allow you to define context-specific templates for issue and merge request description fields for your project."
+msgid "DeployKeys|Current project"
msgstr ""
+msgid "DeployKeys|Deploy key"
+msgstr ""
+
+msgid "DeployKeys|Enabled deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Error enabling deploy key"
+msgstr ""
+
+msgid "DeployKeys|Error getting deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Error removing deploy key"
+msgstr ""
+
+msgid "DeployKeys|Expand %{count} other projects"
+msgstr ""
+
+msgid "DeployKeys|Loading deploy keys"
+msgstr ""
+
+msgid "DeployKeys|No deploy keys found. Create one with the form above."
+msgstr ""
+
+msgid "DeployKeys|Privately accessible deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Project usage"
+msgstr ""
+
+msgid "DeployKeys|Publicly accessible deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Read access only"
+msgstr ""
+
+msgid "DeployKeys|Write access allowed"
+msgstr ""
+
+msgid "DeployKeys|You are going to remove this deploy key. Are you sure?"
+msgstr ""
+
+msgid "DeployTokens|Active Deploy Tokens (%{active_tokens})"
+msgstr ""
+
+msgid "DeployTokens|Add a deploy token"
+msgstr ""
+
+msgid "DeployTokens|Allows read-only access to the registry images"
+msgstr ""
+
+msgid "DeployTokens|Allows read-only access to the repository"
+msgstr ""
+
+msgid "DeployTokens|Copy deploy token to clipboard"
+msgstr ""
+
+msgid "DeployTokens|Copy username to clipboard"
+msgstr ""
+
+msgid "DeployTokens|Create deploy token"
+msgstr ""
+
+msgid "DeployTokens|Created"
+msgstr ""
+
+msgid "DeployTokens|Deploy Tokens"
+msgstr ""
+
+msgid "DeployTokens|Deploy tokens allow read-only access to your repository and registry images."
+msgstr ""
+
+msgid "DeployTokens|Expires"
+msgstr ""
+
+msgid "DeployTokens|Name"
+msgstr ""
+
+msgid "DeployTokens|Pick a name for the application, and we'll give you a unique deploy token."
+msgstr ""
+
+msgid "DeployTokens|Revoke"
+msgstr ""
+
+msgid "DeployTokens|Revoke %{name}"
+msgstr ""
+
+msgid "DeployTokens|Scopes"
+msgstr ""
+
+msgid "DeployTokens|This action cannot be undone."
+msgstr ""
+
+msgid "DeployTokens|This project has no active Deploy Tokens."
+msgstr ""
+
+msgid "DeployTokens|Use this token as a password. Make sure you save it - you won't be able to access it again."
+msgstr ""
+
+msgid "DeployTokens|Use this username as a login."
+msgstr ""
+
+msgid "DeployTokens|Username"
+msgstr ""
+
+msgid "DeployTokens|You are about to revoke"
+msgstr ""
+
+msgid "DeployTokens|Your New Deploy Token"
+msgstr ""
+
+msgid "DeployTokens|Your new project deploy token has been created."
+msgstr ""
+
+msgid "Deprioritize label"
+msgstr ""
+
+msgid "Description"
+msgstr "Beschreibung"
+
msgid "Details"
msgstr "Details"
msgid "Diffs|No file name available"
msgstr ""
+msgid "Diffs|Something went wrong while fetching diff lines."
+msgstr ""
+
msgid "Directory name"
msgstr "Verzeichnisname"
msgid "Disable"
msgstr ""
-msgid "Discard draft"
+msgid "Disable for this project"
msgstr ""
-msgid "Discover GitLab Geo."
+msgid "Disable group Runners"
msgstr ""
-msgid "Dismiss Cycle Analytics introduction box"
+msgid "Discard changes"
msgstr ""
-msgid "Dismiss Merge Request promotion"
+msgid "Discard draft"
msgstr ""
-msgid "Documentation for popular identity providers"
+msgid "Dismiss Cycle Analytics introduction box"
+msgstr ""
+
+msgid "Domain"
msgstr ""
msgid "Don't show again"
@@ -1675,40 +2022,40 @@ msgstr ""
msgid "Due date"
msgstr ""
-msgid "During this process, you’ll be asked for URLs from GitLab’s side. Use the URLs shown below."
+msgid "Each Runner can be in one of the following states:"
msgstr ""
msgid "Edit"
msgstr "Bearbeiten"
+msgid "Edit Label"
+msgstr ""
+
msgid "Edit Pipeline Schedule %{id}"
msgstr "Pipeline Zeitplan bearbeiten %{id}"
msgid "Edit files in the editor and commit changes here"
msgstr ""
-msgid "Editing"
-msgstr ""
-
-msgid "Elasticsearch"
+msgid "Edit identity for %{user_name}"
msgstr ""
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "Email"
msgstr ""
-msgid "Email"
+msgid "Email patch"
msgstr ""
msgid "Emails"
msgstr "E-Mails"
-msgid "Enable"
+msgid "Embed"
msgstr ""
-msgid "Enable Auto DevOps"
+msgid "Enable"
msgstr ""
-msgid "Enable SAML authentication for this group"
+msgid "Enable Auto DevOps"
msgstr ""
msgid "Enable Sentry for error reporting and logging."
@@ -1720,7 +2067,13 @@ msgstr ""
msgid "Enable and configure Prometheus metrics."
msgstr ""
-msgid "Enable classification control using an external service"
+msgid "Enable for this project"
+msgstr ""
+
+msgid "Enable group Runners"
+msgstr ""
+
+msgid "Enable or disable certain group features and choose access levels."
msgstr ""
msgid "Enable or disable version check and usage ping."
@@ -1732,7 +2085,10 @@ msgstr ""
msgid "Enable the Performance Bar for a given group."
msgstr ""
-msgid "Enabled"
+msgid "Ends at (UTC)"
+msgstr ""
+
+msgid "Environments"
msgstr ""
msgid "Environments|An error occurred while fetching the environments."
@@ -1783,43 +2139,40 @@ msgstr ""
msgid "Environments|You don't have any environments right now."
msgstr ""
-msgid "Epic will be removed! Are you sure?"
-msgstr ""
-
-msgid "Epics"
+msgid "Error Reporting and Logging"
msgstr ""
-msgid "Epics Roadmap"
+msgid "Error committing changes. Please try again."
msgstr ""
-msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
+msgid "Error fetching contributors data."
msgstr ""
-msgid "Error Reporting and Logging"
+msgid "Error fetching job trace"
msgstr ""
-msgid "Error checking branch data. Please try again."
+msgid "Error fetching labels."
msgstr ""
-msgid "Error committing changes. Please try again."
+msgid "Error fetching network graph."
msgstr ""
-msgid "Error creating epic"
+msgid "Error fetching refs"
msgstr ""
-msgid "Error fetching contributors data."
+msgid "Error fetching usage ping data."
msgstr ""
-msgid "Error fetching labels."
+msgid "Error loading branch data. Please try again."
msgstr ""
-msgid "Error fetching network graph."
+msgid "Error loading last commit."
msgstr ""
-msgid "Error fetching refs"
+msgid "Error loading merge requests."
msgstr ""
-msgid "Error fetching usage ping data."
+msgid "Error loading project data. Please try again."
msgstr ""
msgid "Error occurred when toggling the notification subscription"
@@ -1834,6 +2187,9 @@ msgstr ""
msgid "Error updating todo status."
msgstr ""
+msgid "Estimated"
+msgstr ""
+
msgid "EventFilterBy|Filter by all"
msgstr "Filtere alle"
@@ -1864,31 +2220,16 @@ msgstr "Wöchentlich (Sonntags um 4:00 Uhr)"
msgid "Expand"
msgstr ""
-msgid "Explore projects"
-msgstr ""
-
-msgid "Explore public groups"
-msgstr ""
-
-msgid "External Classification Policy Authorization"
-msgstr ""
-
-msgid "External authentication"
-msgstr ""
-
-msgid "External authorization denied access to this project"
-msgstr ""
-
-msgid "External authorization request timeout"
+msgid "Expand all"
msgstr ""
-msgid "ExternalAuthorizationService|Classification Label"
+msgid "Expand sidebar"
msgstr ""
-msgid "ExternalAuthorizationService|Classification label"
+msgid "Explore projects"
msgstr ""
-msgid "ExternalAuthorizationService|When no classification label is set the default label `%{default_label}` will be used."
+msgid "Explore public groups"
msgstr ""
msgid "Failed"
@@ -1900,6 +2241,9 @@ msgstr ""
msgid "Failed to change the owner"
msgstr "Wechsel des Besitzers fehlgeschlagen"
+msgid "Failed to check related branches."
+msgstr ""
+
msgid "Failed to remove issue from board, please try again."
msgstr ""
@@ -1909,6 +2253,12 @@ msgstr "Entfernung der Pipelineplanung fehlgeschlagen"
msgid "Failed to update issues, please try again."
msgstr ""
+msgid "Failure"
+msgstr ""
+
+msgid "Faster as it re-uses the project workspace (falling back to clone if it doesn't exist)"
+msgstr ""
+
msgid "Feb"
msgstr ""
@@ -1918,18 +2268,12 @@ msgstr ""
msgid "Fields on this page are now uneditable, you can configure"
msgstr ""
-msgid "File name"
-msgstr ""
-
msgid "Files"
msgstr "Dateien"
msgid "Files (%{human_size})"
msgstr ""
-msgid "Fill in the fields below, turn on <strong>%{enable_label}</strong>, and press <strong>%{save_changes}</strong>"
-msgstr ""
-
msgid "Filter by commit message"
msgstr "Filter nach Commit Nachricht"
@@ -1948,10 +2292,13 @@ msgstr "Erster"
msgid "FirstPushedBy|pushed by"
msgstr "übertragen von"
-msgid "Font Color"
+msgid "For internal projects, any logged in user can view pipelines and access job details (output logs and artifacts)"
+msgstr ""
+
+msgid "For private projects, any member (guest or higher) can view pipelines and access job details (output logs and artifacts)"
msgstr ""
-msgid "Footer message"
+msgid "For public projects, anyone can view pipelines and access job details (output logs and artifacts)"
msgstr ""
msgid "Fork"
@@ -1971,6 +2318,9 @@ msgstr ""
msgid "Format"
msgstr ""
+msgid "Found errors in your .gitlab-ci.yml:"
+msgstr ""
+
msgid "From %{provider_title}"
msgstr ""
@@ -1986,166 +2336,13 @@ msgstr ""
msgid "GPG Keys"
msgstr ""
-msgid "Generate a default set of labels"
-msgstr ""
-
-msgid "Geo Nodes"
-msgstr ""
-
-msgid "Geo allows you to replicate your GitLab instance to other geographical locations."
-msgstr ""
-
-msgid "GeoNodeSyncStatus|Node is failing or broken."
-msgstr ""
-
-msgid "GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage."
-msgstr ""
-
-msgid "GeoNodes|Checksummed"
-msgstr ""
-
-msgid "GeoNodes|Database replication lag:"
-msgstr ""
-
-msgid "GeoNodes|Disabling a node stops the sync process. Are you sure?"
-msgstr ""
-
-msgid "GeoNodes|Does not match the primary storage configuration"
-msgstr ""
-
-msgid "GeoNodes|Failed"
-msgstr ""
-
-msgid "GeoNodes|Full"
-msgstr ""
-
-msgid "GeoNodes|GitLab version does not match the primary node version"
-msgstr ""
-
-msgid "GeoNodes|GitLab version:"
-msgstr ""
-
-msgid "GeoNodes|Health status:"
-msgstr ""
-
-msgid "GeoNodes|Last event ID processed by cursor:"
-msgstr ""
-
-msgid "GeoNodes|Last event ID seen from primary:"
-msgstr ""
-
-msgid "GeoNodes|Loading nodes"
-msgstr ""
-
-msgid "GeoNodes|Local Attachments:"
-msgstr ""
-
-msgid "GeoNodes|Local LFS objects:"
-msgstr ""
-
-msgid "GeoNodes|Local job artifacts:"
-msgstr ""
-
-msgid "GeoNodes|New node"
-msgstr ""
-
-msgid "GeoNodes|Node Authentication was successfully repaired."
-msgstr ""
-
-msgid "GeoNodes|Node was successfully removed."
-msgstr ""
-
-msgid "GeoNodes|Not checksummed"
-msgstr ""
-
-msgid "GeoNodes|Out of sync"
-msgstr ""
-
-msgid "GeoNodes|Removing a node stops the sync process. Are you sure?"
-msgstr ""
-
-msgid "GeoNodes|Replication slot WAL:"
-msgstr ""
-
-msgid "GeoNodes|Replication slots:"
-msgstr ""
-
-msgid "GeoNodes|Repositories checksummed:"
-msgstr ""
-
-msgid "GeoNodes|Repositories:"
-msgstr ""
-
-msgid "GeoNodes|Repository checksums verified:"
-msgstr ""
-
-msgid "GeoNodes|Selective"
-msgstr ""
-
-msgid "GeoNodes|Something went wrong while changing node status"
+msgid "General"
msgstr ""
-msgid "GeoNodes|Something went wrong while removing node"
+msgid "General pipelines"
msgstr ""
-msgid "GeoNodes|Something went wrong while repairing node"
-msgstr ""
-
-msgid "GeoNodes|Storage config:"
-msgstr ""
-
-msgid "GeoNodes|Sync settings:"
-msgstr ""
-
-msgid "GeoNodes|Synced"
-msgstr ""
-
-msgid "GeoNodes|Unused slots"
-msgstr ""
-
-msgid "GeoNodes|Unverified"
-msgstr ""
-
-msgid "GeoNodes|Used slots"
-msgstr ""
-
-msgid "GeoNodes|Verified"
-msgstr ""
-
-msgid "GeoNodes|Wiki checksums verified:"
-msgstr ""
-
-msgid "GeoNodes|Wikis checksummed:"
-msgstr ""
-
-msgid "GeoNodes|Wikis:"
-msgstr ""
-
-msgid "GeoNodes|You have configured Geo nodes using an insecure HTTP connection. We recommend the use of HTTPS."
-msgstr ""
-
-msgid "Geo|All projects"
-msgstr ""
-
-msgid "Geo|File sync capacity"
-msgstr ""
-
-msgid "Geo|Groups to synchronize"
-msgstr ""
-
-msgid "Geo|Projects in certain groups"
-msgstr ""
-
-msgid "Geo|Projects in certain storage shards"
-msgstr ""
-
-msgid "Geo|Repository sync capacity"
-msgstr ""
-
-msgid "Geo|Select groups to replicate."
-msgstr ""
-
-msgid "Geo|Shards to synchronize"
+msgid "Generate a default set of labels"
msgstr ""
msgid "Git repository URL"
@@ -2157,6 +2354,9 @@ msgstr ""
msgid "Git storage health information has been reset"
msgstr "Informationen über den Speicherzustand von Gitlab wurden zurückgesetzt."
+msgid "Git strategy for pipelines"
+msgstr ""
+
msgid "Git version"
msgstr ""
@@ -2166,21 +2366,24 @@ msgstr ""
msgid "GitLab CI Linter has been moved"
msgstr ""
-msgid "GitLab Geo"
+msgid "GitLab Group Runners can execute code for all the projects in this group."
msgstr ""
msgid "GitLab Runner section"
msgstr "GitLab Runner Bereich"
-msgid "GitLab single sign on URL"
-msgstr ""
-
msgid "Gitaly"
msgstr ""
msgid "Gitaly Servers"
msgstr ""
+msgid "Gitaly|Address"
+msgstr ""
+
+msgid "Go Back"
+msgstr ""
+
msgid "Go back"
msgstr ""
@@ -2196,22 +2399,19 @@ msgstr ""
msgid "Got it!"
msgstr ""
-msgid "GroupRoadmap|Epics let you manage your portfolio of projects more efficiently and with less effort"
+msgid "Graph"
msgstr ""
-msgid "GroupRoadmap|From %{dateWord}"
+msgid "Group CI/CD settings"
msgstr ""
-msgid "GroupRoadmap|Loading roadmap"
+msgid "Group ID"
msgstr ""
-msgid "GroupRoadmap|Something went wrong while fetching epics"
+msgid "Group Runners"
msgstr ""
-msgid "GroupRoadmap|To view the roadmap, add a planned start or finish date to one of your epics in this group or its subgroups. Only epics in the past 3 months and the next 3 months are shown &ndash; from %{startDate} to %{endDate}."
-msgstr ""
-
-msgid "GroupRoadmap|Until %{dateWord}"
+msgid "Group maintainers can register group runners in the %{link}"
msgstr ""
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
@@ -2238,6 +2438,9 @@ msgstr ""
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
+msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
+msgstr ""
+
msgid "GroupsEmptyState|A group is a collection of several projects."
msgstr ""
@@ -2277,12 +2480,6 @@ msgstr ""
msgid "GroupsTree|Sorry, no groups or projects matched your search"
msgstr ""
-msgid "Have your users email"
-msgstr ""
-
-msgid "Header message"
-msgstr ""
-
msgid "Health Check"
msgstr "Systemzustand"
@@ -2315,19 +2512,49 @@ msgid_plural "Hide values"
msgstr[0] ""
msgstr[1] ""
+msgid "Hide whitespace changes"
+msgstr ""
+
msgid "History"
msgstr ""
msgid "Housekeeping successfully started"
msgstr "Aufräumen erfolgreich gestartet"
-msgid "Identity provider single sign on URL"
+msgid "I accept the %{terms_link}"
+msgstr ""
+
+msgid "I accept the|Terms of Service and Privacy Policy"
+msgstr ""
+
+msgid "ID"
+msgstr ""
+
+msgid "IDE|Commit"
msgstr ""
-msgid "If enabled, access to projects will be validated on an external service using their classification label."
+msgid "IDE|Edit"
msgstr ""
-msgid "If using GitHub, you’ll see pipeline statuses on GitHub for your commits and pull requests. %{more_info_link}"
+msgid "IDE|Go back"
+msgstr ""
+
+msgid "IDE|Open in file view"
+msgstr ""
+
+msgid "IDE|Review"
+msgstr ""
+
+msgid "Identifier"
+msgstr ""
+
+msgid "Identities"
+msgstr ""
+
+msgid "If disabled, the access level will depend on the user's permissions in the project."
+msgstr ""
+
+msgid "If enabled"
msgstr ""
msgid "If you already have files you can push them using the %{link_to_cli} below."
@@ -2336,6 +2563,15 @@ msgstr ""
msgid "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>."
msgstr ""
+msgid "ImageDiffViewer|2-up"
+msgstr ""
+
+msgid "ImageDiffViewer|Onion skin"
+msgstr ""
+
+msgid "ImageDiffViewer|Swipe"
+msgstr ""
+
msgid "Import"
msgstr ""
@@ -2351,16 +2587,10 @@ msgstr ""
msgid "Import repository"
msgstr "Repository importieren"
-msgid "ImportButtons|Connect repositories from"
+msgid "Include a Terms of Service agreement and Privacy Policy that all users must accept."
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."
+msgid "Inline"
msgstr ""
msgid "Install Runner on Kubernetes"
@@ -2369,17 +2599,15 @@ 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 "Instance does not support multiple Kubernetes clusters"
msgstr ""
msgid "Integrations"
msgstr ""
+msgid "Integrations Settings"
+msgstr ""
+
msgid "Interested parties can even contribute by pushing commits if they want to."
msgstr ""
@@ -2395,7 +2623,7 @@ msgstr "Intervallmuster"
msgid "Introducing Cycle Analytics"
msgstr "Arbeitsablaufsanalysen vorgestellt"
-msgid "Issue board focus mode"
+msgid "Issue Board"
msgstr ""
msgid "Issue events"
@@ -2404,9 +2632,6 @@ msgstr "Ticketereignisse"
msgid "IssueBoards|Board"
msgstr ""
-msgid "IssueBoards|Boards"
-msgstr ""
-
msgid "Issues"
msgstr ""
@@ -2419,6 +2644,12 @@ msgstr ""
msgid "January"
msgstr ""
+msgid "Job"
+msgstr ""
+
+msgid "Job has been erased"
+msgstr ""
+
msgid "Jobs"
msgstr ""
@@ -2461,6 +2692,9 @@ msgstr ""
msgid "Kubernetes service integration has been deprecated. %{deprecated_message_content} your Kubernetes clusters using the new <a href=\"%{url}\"/>Kubernetes Clusters</a> page"
msgstr ""
+msgid "LFS"
+msgstr ""
+
msgid "LFSStatus|Disabled"
msgstr "Deaktiviert"
@@ -2470,6 +2704,9 @@ msgstr "Aktiviert"
msgid "Label"
msgstr ""
+msgid "Label actions dropdown"
+msgstr ""
+
msgid "LabelSelect|%{firstLabelName} +%{remainingLabelCount} more"
msgstr ""
@@ -2485,6 +2722,9 @@ msgstr ""
msgid "Labels can be applied to issues and merge requests to categorize them."
msgstr ""
+msgid "Labels can be applied to issues and merge requests."
+msgstr ""
+
msgid "Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>"
msgstr ""
@@ -2520,6 +2760,9 @@ msgstr "Du übertrugst an"
msgid "LastPushEvent|at"
msgstr "am"
+msgid "Latest changes"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -2544,9 +2787,6 @@ msgstr "Verlasse die Gruppe"
msgid "Leave project"
msgstr "Verlasse das Projekt"
-msgid "License"
-msgstr ""
-
msgid "List"
msgstr ""
@@ -2556,6 +2796,9 @@ msgstr ""
msgid "Loading the GitLab IDE..."
msgstr ""
+msgid "Loading..."
+msgstr ""
+
msgid "Lock"
msgstr ""
@@ -2565,21 +2808,18 @@ msgstr ""
msgid "Lock not found"
msgstr ""
-msgid "Locked"
+msgid "Lock to current projects"
msgstr ""
-msgid "Locked Files"
+msgid "Locked"
msgstr ""
-msgid "Locks give the ability to lock specific file or folder."
+msgid "Locked to current projects"
msgstr ""
msgid "Login"
msgstr ""
-msgid "Make everyone on your team more productive regardless of their location. GitLab Geo creates read-only mirrors of your GitLab instance so you can reduce the time it takes to clone and fetch large repos."
-msgstr ""
-
msgid "Manage all notifications"
msgstr ""
@@ -2592,16 +2832,16 @@ msgstr ""
msgid "Manage project labels"
msgstr ""
-msgid "Manage your group’s membership while adding another level of security with SAML."
-msgstr ""
-
msgid "Mar"
msgstr ""
msgid "March"
msgstr ""
-msgid "Mark done"
+msgid "Mark todo as done"
+msgstr ""
+
+msgid "Markdown enabled"
msgstr ""
msgid "Maximum git storage failures"
@@ -2616,7 +2856,7 @@ msgstr "Median"
msgid "Members"
msgstr "Mitglieder"
-msgid "Members will be forwarded here when signing in to your group. Get this from your identity provider, where it can also be called \"SSO Service Location\", \"SAML Token Issuance Endpoint\", or \"SAML 2.0/W-Federation URL\"."
+msgid "Merge Request:"
msgstr ""
msgid "Merge Requests"
@@ -2628,91 +2868,46 @@ msgstr "Ereignisse zusammenführen"
msgid "Merge request"
msgstr ""
-msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
-msgstr ""
-
-msgid "Merged"
-msgstr ""
-
-msgid "Messages"
-msgstr "Nachrichten"
-
-msgid "Metrics - Influx"
-msgstr ""
-
-msgid "Metrics - Prometheus"
-msgstr ""
-
-msgid "Metrics|Business"
-msgstr ""
-
-msgid "Metrics|Create metric"
-msgstr ""
-
-msgid "Metrics|Edit metric"
-msgstr ""
-
-msgid "Metrics|For grouping similar metrics"
-msgstr ""
-
-msgid "Metrics|Label of the chart's vertical axis. Usually the type of the unit being charted. The horizontal axis (X-axis) always represents time."
-msgstr ""
-
-msgid "Metrics|Legend label (optional)"
-msgstr ""
-
-msgid "Metrics|Must be a valid PromQL query."
-msgstr ""
-
-msgid "Metrics|Name"
-msgstr ""
-
-msgid "Metrics|New metric"
-msgstr ""
-
-msgid "Metrics|Prometheus Query Documentation"
+msgid "Merge requests"
msgstr ""
-msgid "Metrics|Query"
-msgstr ""
-
-msgid "Metrics|Response"
+msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
msgstr ""
-msgid "Metrics|System"
+msgid "MergeRequests|Resolve this discussion in a new issue"
msgstr ""
-msgid "Metrics|Type"
+msgid "MergeRequests|Saving the comment failed"
msgstr ""
-msgid "Metrics|Unit label"
+msgid "MergeRequests|Toggle comments for this file"
msgstr ""
-msgid "Metrics|Used as a title for the chart"
+msgid "MergeRequests|Updating discussions failed"
msgstr ""
-msgid "Metrics|Used if the query returns a single series. If it returns multiple series, their legend labels will be picked up from the response."
+msgid "MergeRequests|View file @ %{commitId}"
msgstr ""
-msgid "Metrics|Y-axis label"
+msgid "MergeRequests|View replaced file @ %{commitId}"
msgstr ""
-msgid "Metrics|e.g. HTTP requests"
+msgid "Merged"
msgstr ""
-msgid "Metrics|e.g. Requests/second"
-msgstr ""
+msgid "Messages"
+msgstr "Nachrichten"
-msgid "Metrics|e.g. Throughput"
+msgid "Metrics - Influx"
msgstr ""
-msgid "Metrics|e.g. rate(http_requests_total[5m])"
+msgid "Metrics - Prometheus"
msgstr ""
-msgid "Metrics|e.g. req/sec"
+msgid "Milestone"
msgstr ""
-msgid "Milestone"
+msgid "Milestones"
msgstr ""
msgid "Milestones|Delete milestone"
@@ -2733,9 +2928,6 @@ msgstr ""
msgid "Milestones|Promote Milestone"
msgstr ""
-msgid "Milestones|This action cannot be reversed."
-msgstr ""
-
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "einen SSH Schlüssel hinzufügst"
@@ -2748,7 +2940,7 @@ msgstr ""
msgid "Monitoring"
msgstr "Überwachung"
-msgid "More info"
+msgid "More actions"
msgstr ""
msgid "More information"
@@ -2763,12 +2955,30 @@ msgstr ""
msgid "Move issue"
msgstr ""
-msgid "Multiple issue boards"
+msgid "Name"
msgstr ""
msgid "Name new label"
msgstr ""
+msgid "Name your individual key via a title"
+msgstr ""
+
+msgid "Nav|Help"
+msgstr ""
+
+msgid "Nav|Home"
+msgstr ""
+
+msgid "Nav|Sign In / Register"
+msgstr ""
+
+msgid "Nav|Sign out and sign in with a different account"
+msgstr ""
+
+msgid "New Identity"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "Neues Ticket"
@@ -2780,6 +2990,9 @@ msgstr ""
msgid "New Kubernetes cluster"
msgstr ""
+msgid "New Label"
+msgstr ""
+
msgid "New Pipeline Schedule"
msgstr "Neuer Pipeline Zeitplan"
@@ -2792,15 +3005,15 @@ msgstr ""
msgid "New directory"
msgstr "Neues Verzeichnis"
-msgid "New epic"
-msgstr ""
-
msgid "New file"
msgstr "Neue Datei"
msgid "New group"
msgstr ""
+msgid "New identity"
+msgstr ""
+
msgid "New issue"
msgstr "Neues Ticket"
@@ -2810,6 +3023,9 @@ msgstr ""
msgid "New merge request"
msgstr "Neuer Merge Request"
+msgid "New pipelines will cancel older, pending pipelines on the same branch"
+msgstr ""
+
msgid "New project"
msgstr ""
@@ -2825,7 +3041,7 @@ msgstr ""
msgid "New tag"
msgstr "Neuer Tag"
-msgid "No Label"
+msgid "No"
msgstr ""
msgid "No assignee"
@@ -2846,7 +3062,16 @@ msgstr ""
msgid "No file chosen"
msgstr ""
-msgid "No labels created yet."
+msgid "No files found"
+msgstr ""
+
+msgid "No files found."
+msgstr ""
+
+msgid "No merge requests found"
+msgstr ""
+
+msgid "No messages were logged"
msgstr ""
msgid "No repository"
@@ -2879,15 +3104,9 @@ msgstr "Nicht genügend Daten"
msgid "Note that the master branch is automatically protected. %{link_to_protected_branches}"
msgstr ""
-msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow connecting repositories without generating a Personal Access Token."
-msgstr ""
-
msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
msgstr ""
-msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow connecting repositories without generating a Personal Access Token."
-msgstr ""
-
msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
msgstr ""
@@ -2963,9 +3182,6 @@ msgstr ""
msgid "Number of access attempts"
msgstr ""
-msgid "OK"
-msgstr ""
-
msgid "Oct"
msgstr ""
@@ -2975,19 +3191,16 @@ msgstr ""
msgid "OfSearchInADropdown|Filter"
msgstr "Filter"
-msgid "Once imported, repositories can be mirrored over SSH. Read more %{ssh_link}"
-msgstr ""
-
msgid "Online IDE integration settings."
msgstr ""
-msgid "Only project members can comment."
+msgid "Only comments from the following commit are shown below"
msgstr ""
-msgid "Open"
+msgid "Only project members can comment."
msgstr ""
-msgid "Opened"
+msgid "Open in Xcode"
msgstr ""
msgid "OpenedNDaysAgo|Opened"
@@ -2996,9 +3209,18 @@ msgstr "Ungelöst"
msgid "Opens in a new window"
msgstr ""
+msgid "Operations"
+msgstr ""
+
msgid "Options"
msgstr "Optionen"
+msgid "Or you can choose one of the suggested colors below"
+msgstr ""
+
+msgid "Other Labels"
+msgstr ""
+
msgid "Otherwise it is recommended you start with one of the options below."
msgstr ""
@@ -3032,12 +3254,27 @@ msgstr ""
msgid "Password"
msgstr "Passwort"
+msgid "Paste your public SSH key, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'. Don't use your private SSH key."
+msgstr ""
+
+msgid "Pause"
+msgstr ""
+
msgid "Pending"
msgstr ""
+msgid "Per job. If a job passes this threshold, it will be marked as failed"
+msgstr ""
+
+msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgstr ""
+
msgid "Performance optimization"
msgstr ""
+msgid "Permissions"
+msgstr ""
+
msgid "Personal Access Token"
msgstr ""
@@ -3053,7 +3290,7 @@ msgstr "Zeitplan der Pipeline"
msgid "Pipeline Schedules"
msgstr "Zustände der Pipeline"
-msgid "Pipeline quota"
+msgid "Pipeline triggers"
msgstr ""
msgid "PipelineCharts|Failed:"
@@ -3152,10 +3389,22 @@ msgstr ""
msgid "Pipelines|This project is not currently set up to run pipelines."
msgstr ""
-msgid "Pipeline|Retry pipeline"
+msgid "Pipeline|Create for"
+msgstr ""
+
+msgid "Pipeline|Create pipeline"
msgstr ""
-msgid "Pipeline|Retry pipeline #%{pipelineId}?"
+msgid "Pipeline|Existing branch name or tag"
+msgstr ""
+
+msgid "Pipeline|Run Pipeline"
+msgstr ""
+
+msgid "Pipeline|Search branches"
+msgstr ""
+
+msgid "Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default."
msgstr ""
msgid "Pipeline|Stop pipeline"
@@ -3164,7 +3413,7 @@ msgstr ""
msgid "Pipeline|Stop pipeline #%{pipelineId}?"
msgstr ""
-msgid "Pipeline|You’re about to retry pipeline %{pipelineId}."
+msgid "Pipeline|Variables"
msgstr ""
msgid "Pipeline|You’re about to stop pipeline %{pipelineId}."
@@ -3182,19 +3431,25 @@ msgstr "mit Stage"
msgid "Pipeline|with stages"
msgstr "mit Stages"
+msgid "Plain diff"
+msgstr ""
+
msgid "PlantUML"
msgstr ""
msgid "Play"
msgstr ""
-msgid "Please <a href=%{link_to_billing} target=\"_blank\" rel=\"noopener noreferrer\">enable billing for one of your projects to be able to create a Kubernetes cluster</a>, then try again."
+msgid "Please accept the Terms of Service before continuing."
+msgstr ""
+
+msgid "Please select at least one filter to see results"
msgstr ""
msgid "Please solve the reCAPTCHA"
msgstr ""
-msgid "Please wait while we connect to your repository. Refresh at will."
+msgid "Please try again"
msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
@@ -3203,7 +3458,19 @@ msgstr ""
msgid "Preferences"
msgstr ""
-msgid "Primary"
+msgid "Preferences|Navigation theme"
+msgstr ""
+
+msgid "Prioritize"
+msgstr ""
+
+msgid "Prioritize label"
+msgstr ""
+
+msgid "Prioritized Labels"
+msgstr ""
+
+msgid "Prioritized label"
msgstr ""
msgid "Private - Project access must be granted explicitly to each user."
@@ -3221,6 +3488,12 @@ msgstr "Profil"
msgid "Profiles|Account scheduled for removal."
msgstr ""
+msgid "Profiles|Change username"
+msgstr ""
+
+msgid "Profiles|Current path: %{path}"
+msgstr ""
+
msgid "Profiles|Delete Account"
msgstr ""
@@ -3239,9 +3512,21 @@ msgstr ""
msgid "Profiles|Invalid username"
msgstr ""
+msgid "Profiles|Path"
+msgstr ""
+
msgid "Profiles|Type your %{confirmationValue} to confirm:"
msgstr ""
+msgid "Profiles|Update username"
+msgstr ""
+
+msgid "Profiles|Username change failed - %{message}"
+msgstr ""
+
+msgid "Profiles|Username successfully changed"
+msgstr ""
+
msgid "Profiles|You don't have access to delete this user."
msgstr ""
@@ -3260,6 +3545,12 @@ msgstr ""
msgid "Programming languages used in this repository"
msgstr ""
+msgid "Progress"
+msgstr ""
+
+msgid "Project"
+msgstr ""
+
msgid "Project '%{project_name}' is in the process of being deleted."
msgstr ""
@@ -3272,6 +3563,9 @@ 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 Badges"
+msgstr ""
+
msgid "Project access must be granted explicitly to each user."
msgstr "Jedem Nutzer muss explizit der Zugriff auf das Projekt gewährt werden."
@@ -3299,30 +3593,6 @@ msgstr "Export des Projektes gestartet. Ein Link zum herunterladen wir Dir per E
msgid "ProjectActivityRSS|Subscribe"
msgstr "Abonnieren"
-msgid "ProjectCreationLevel|Allowed to create projects"
-msgstr ""
-
-msgid "ProjectCreationLevel|Default project creation protection"
-msgstr ""
-
-msgid "ProjectCreationLevel|Developers + Masters"
-msgstr ""
-
-msgid "ProjectCreationLevel|Masters"
-msgstr ""
-
-msgid "ProjectCreationLevel|No one"
-msgstr ""
-
-msgid "ProjectFeature|Disabled"
-msgstr "Dekativiert"
-
-msgid "ProjectFeature|Everyone with access"
-msgstr "Jeder mit Zugriff"
-
-msgid "ProjectFeature|Only team members"
-msgstr "Nur Teammitglieder"
-
msgid "ProjectFileTree|Name"
msgstr "Name"
@@ -3332,27 +3602,6 @@ msgstr "Niemals"
msgid "ProjectLifecycle|Stage"
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 ""
@@ -3377,6 +3626,9 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr ""
+msgid "PrometheusDashboard|Time"
+msgstr ""
+
msgid "PrometheusService|%{exporters} with %{metrics} were found"
msgstr ""
@@ -3398,18 +3650,9 @@ msgstr ""
msgid "PrometheusService|Common metrics"
msgstr ""
-msgid "PrometheusService|Common metrics are automatically monitored based on a library of metrics from popular exporters."
-msgstr ""
-
-msgid "PrometheusService|Custom metrics"
-msgstr ""
-
msgid "PrometheusService|Finding and configuring metrics..."
msgstr ""
-msgid "PrometheusService|Finding custom metrics..."
-msgstr ""
-
msgid "PrometheusService|Install Prometheus on clusters"
msgstr ""
@@ -3422,13 +3665,13 @@ msgstr ""
msgid "PrometheusService|Metrics"
msgstr ""
-msgid "PrometheusService|Missing environment variable"
+msgid "PrometheusService|Metrics are automatically configured and monitored based on a library of metrics from popular exporters."
msgstr ""
-msgid "PrometheusService|More information"
+msgid "PrometheusService|Missing environment variable"
msgstr ""
-msgid "PrometheusService|New metric"
+msgid "PrometheusService|More information"
msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
@@ -3437,9 +3680,6 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
-msgstr ""
-
msgid "PrometheusService|Time-series monitoring service"
msgstr ""
@@ -3455,22 +3695,28 @@ msgstr ""
msgid "Promote"
msgstr ""
-msgid "Promote to Group Label"
+msgid "Promote these project milestones into a group milestone."
msgstr ""
msgid "Promote to Group Milestone"
msgstr ""
+msgid "Promote to group label"
+msgstr ""
+
msgid "Protip:"
msgstr ""
+msgid "Provider"
+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"
+msgid "Public pipelines"
msgstr ""
msgid "Push events"
@@ -3482,10 +3728,10 @@ msgstr ""
msgid "Push to create a project"
msgstr ""
-msgid "PushRule|Committer restriction"
+msgid "Quick actions can be used in the issues description and comment boxes."
msgstr ""
-msgid "Quick actions can be used in the issues description and comment boxes."
+msgid "Re-deploy"
msgstr ""
msgid "Read more"
@@ -3497,18 +3743,18 @@ msgstr "Lies mich"
msgid "Real-time features"
msgstr ""
-msgid "RefSwitcher|Branches"
-msgstr "Branches"
-
-msgid "RefSwitcher|Tags"
-msgstr "Tags"
-
msgid "Reference:"
msgstr ""
msgid "Register / Sign In"
msgstr ""
+msgid "Register and see your runners for this group."
+msgstr ""
+
+msgid "Register and see your runners for this project."
+msgstr ""
+
msgid "Registry"
msgstr ""
@@ -3539,28 +3785,28 @@ msgstr "Später erinnern"
msgid "Remove"
msgstr ""
-msgid "Remove avatar"
+msgid "Remove Runner"
msgstr ""
-msgid "Remove project"
-msgstr "Projekt entfernen"
-
-msgid "Repair authentication"
+msgid "Remove avatar"
msgstr ""
-msgid "Repo by URL"
+msgid "Remove priority"
msgstr ""
+msgid "Remove project"
+msgstr "Projekt entfernen"
+
msgid "Repository"
msgstr ""
-msgid "Repository has no locks."
+msgid "Repository Settings"
msgstr ""
msgid "Repository maintenance"
msgstr ""
-msgid "Repository mirror settings"
+msgid "Repository mirror"
msgstr ""
msgid "Repository storage"
@@ -3569,6 +3815,9 @@ msgstr ""
msgid "Request Access"
msgstr "Anfrage auf Zugriff"
+msgid "Require all users to accept Terms of Service and Privacy Policy when they access GitLab."
+msgstr ""
+
msgid "Reset git storage health information"
msgstr "Informationen über Speicherzustand zurücksetzen"
@@ -3578,10 +3827,25 @@ msgstr "Zugriffstoken für Systemzustand zurücksetzen"
msgid "Reset runners registration token"
msgstr "Registrierungstoken für Runner zurücksetzen"
+msgid "Resolve all discussions in new issue"
+msgstr ""
+
+msgid "Resolve conflicts on source branch"
+msgstr ""
+
msgid "Resolve discussion"
msgstr ""
-msgid "Response"
+msgid "Resume"
+msgstr ""
+
+msgid "Retry"
+msgstr ""
+
+msgid "Retry this job"
+msgstr ""
+
+msgid "Retry verification"
msgstr ""
msgid "Reveal value"
@@ -3595,7 +3859,7 @@ msgstr "Commit zurücksetzen"
msgid "Revert this merge request"
msgstr "Merge Request zurücksetzen"
-msgid "Review the process for configuring service providers in your identity provider — in this case, GitLab is the \"service provider\" or \"relying party\"."
+msgid "Review"
msgstr ""
msgid "Reviewing"
@@ -3604,30 +3868,33 @@ msgstr ""
msgid "Reviewing (merge request !%{mergeRequestId})"
msgstr ""
-msgid "Roadmap"
+msgid "Rollback"
msgstr ""
-msgid "Run CI/CD pipelines for external repositories"
+msgid "Runner token"
msgstr ""
msgid "Runners"
msgstr ""
-msgid "Running"
-msgstr ""
-
-msgid "SAML Single Sign On"
+msgid "Runners API"
msgstr ""
-msgid "SAML Single Sign On Settings"
+msgid "Runners can be placed on separate users, servers, and even on your local machine."
msgstr ""
-msgid "SHA1 fingerprint of the SAML token signing certificate. Get this from your identity provider, where it can also be called \"Thumbprint\"."
+msgid "Running"
msgstr ""
msgid "SSH Keys"
msgstr "SSH-Schlüssel"
+msgid "SSL Verification"
+msgstr ""
+
+msgid "Save"
+msgstr ""
+
msgid "Save changes"
msgstr ""
@@ -3649,15 +3916,30 @@ msgstr ""
msgid "Scheduling Pipelines"
msgstr "Pipelines planen"
-msgid "Scoped issue boards"
+msgid "Scroll to bottom"
+msgstr ""
+
+msgid "Scroll to top"
msgstr ""
msgid "Search"
msgstr ""
+msgid "Search branches"
+msgstr ""
+
msgid "Search branches and tags"
msgstr "Suche nach Branches und Tags"
+msgid "Search files"
+msgstr ""
+
+msgid "Search for projects, issues, etc."
+msgstr ""
+
+msgid "Search merge requests"
+msgstr ""
+
msgid "Search milestones"
msgstr ""
@@ -3673,15 +3955,15 @@ msgstr ""
msgid "Seconds to wait for a storage access attempt"
msgstr ""
-msgid "Secret variables"
-msgstr ""
-
-msgid "Security report"
+msgid "Select"
msgstr ""
msgid "Select Archive Format"
msgstr "Archivierungsformat auswählen"
+msgid "Select a namespace to fork the project"
+msgstr ""
+
msgid "Select a timezone"
msgstr "Zeitzone auswählen"
@@ -3694,12 +3976,21 @@ msgstr ""
msgid "Select branch/tag"
msgstr ""
-msgid "Select target branch"
-msgstr "Zielbranch auswählen"
+msgid "Select project"
+msgstr ""
+
+msgid "Select project and zone to choose machine type"
+msgstr ""
-msgid "Selective synchronization"
+msgid "Select project to choose zone"
msgstr ""
+msgid "Select source branch"
+msgstr ""
+
+msgid "Select target branch"
+msgstr "Zielbranch auswählen"
+
msgid "Send email"
msgstr ""
@@ -3715,9 +4006,6 @@ msgstr ""
msgid "Service Templates"
msgstr ""
-msgid "Service URL"
-msgstr ""
-
msgid "Session expiration, projects limit and attachment size."
msgstr ""
@@ -3742,9 +4030,6 @@ msgstr ""
msgid "Set up Koding"
msgstr "Koding einrichten"
-msgid "Set up assertions/attributes/claims (email, first_name, last_name) and NameID according to %{docsLinkStart}the documentation %{icon}%{docsLinkEnd}"
-msgstr ""
-
msgid "SetPasswordToCloneLink|set a password"
msgstr "ein Passwort festlegst"
@@ -3754,19 +4039,22 @@ msgstr "Einstellungen"
msgid "Setup a specific Runner automatically"
msgstr ""
-msgid "Share the <strong>%{sso_label}</strong> with members so they can sign in to your group through your identity provider"
+msgid "Share"
msgstr ""
-msgid "SharedRunnersMinutesSettings|By resetting the pipeline minutes for this namespace, the currently used minutes will be set to zero."
+msgid "Shared Runners"
msgstr ""
-msgid "SharedRunnersMinutesSettings|Reset pipeline minutes"
+msgid "Show command"
msgstr ""
-msgid "SharedRunnersMinutesSettings|Reset used pipeline minutes"
+msgid "Show complete raw log"
msgstr ""
-msgid "Show command"
+msgid "Show latest version"
+msgstr ""
+
+msgid "Show latest version of the diff"
msgstr ""
msgid "Show parent pages"
@@ -3775,21 +4063,18 @@ msgstr ""
msgid "Show parent subgroups"
msgstr ""
+msgid "Show whitespace changes"
+msgstr ""
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "Zeige %d Ereignis"
msgstr[1] "Zeige %d Ereignisse"
-msgid "Sidebar|Change weight"
-msgstr ""
-
-msgid "Sidebar|No"
-msgstr ""
-
-msgid "Sidebar|None"
+msgid "Side-by-side"
msgstr ""
-msgid "Sidebar|Weight"
+msgid "Sign out"
msgstr ""
msgid "Sign-in restrictions"
@@ -3801,7 +4086,7 @@ msgstr ""
msgid "Size and domain settings for static websites"
msgstr ""
-msgid "Slack application"
+msgid "Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job"
msgstr ""
msgid "Snippets"
@@ -3813,13 +4098,13 @@ msgstr ""
msgid "Something went wrong on our end."
msgstr ""
-msgid "Something went wrong when toggling the button"
+msgid "Something went wrong on our end. Please try again!"
msgstr ""
-msgid "Something went wrong while fetching Dependency Scanning."
+msgid "Something went wrong when toggling the button"
msgstr ""
-msgid "Something went wrong while fetching SAST."
+msgid "Something went wrong while closing the %{issuable}. Please try again later"
msgstr ""
msgid "Something went wrong while fetching the projects."
@@ -3828,6 +4113,12 @@ msgstr ""
msgid "Something went wrong while fetching the registry list."
msgstr ""
+msgid "Something went wrong while reopening the %{issuable}. Please try again later"
+msgstr ""
+
+msgid "Something went wrong while resolving this discussion. Please try again."
+msgstr ""
+
msgid "Something went wrong. Please try again."
msgstr ""
@@ -3873,9 +4164,6 @@ msgstr ""
msgid "SortOptions|Least popular"
msgstr ""
-msgid "SortOptions|Less weight"
-msgstr ""
-
msgid "SortOptions|Milestone"
msgstr ""
@@ -3885,9 +4173,6 @@ msgstr ""
msgid "SortOptions|Milestone due soon"
msgstr ""
-msgid "SortOptions|More weight"
-msgstr ""
-
msgid "SortOptions|Most popular"
msgstr ""
@@ -3927,9 +4212,6 @@ msgstr ""
msgid "SortOptions|Start soon"
msgstr ""
-msgid "SortOptions|Weight"
-msgstr ""
-
msgid "Source"
msgstr ""
@@ -3948,9 +4230,36 @@ msgstr "Spam-Protokolle"
msgid "Spam and Anti-bot Protection"
msgstr ""
+msgid "Specific Runners"
+msgstr ""
+
msgid "Specify the following URL during the Runner setup:"
msgstr "Lege die folgende URL während des Runner Setups fest:"
+msgid "Squash commits"
+msgstr ""
+
+msgid "Stage"
+msgstr ""
+
+msgid "Stage & Commit"
+msgstr ""
+
+msgid "Stage all changes"
+msgstr ""
+
+msgid "Stage changes"
+msgstr ""
+
+msgid "Staged"
+msgstr ""
+
+msgid "Staged %{type}"
+msgstr ""
+
+msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
+msgstr ""
+
msgid "StarProject|Star"
msgstr "Favorisieren"
@@ -3972,12 +4281,15 @@ msgstr "Starte den Runner!"
msgid "Started"
msgstr ""
-msgid "State your message to activate"
+msgid "Starts at (UTC)"
msgstr ""
msgid "Status"
msgstr ""
+msgid "Stop this environment"
+msgstr ""
+
msgid "Stopped"
msgstr ""
@@ -3987,16 +4299,19 @@ msgstr ""
msgid "Subgroups"
msgstr ""
-msgid "Switch branch/tag"
-msgstr "Zu Branch/Tag wechseln"
+msgid "Subscribe"
+msgstr ""
-msgid "System"
+msgid "Subscribe at group level"
msgstr ""
-msgid "System Hooks"
+msgid "Subscribe at project level"
msgstr ""
-msgid "System header and footer:"
+msgid "Switch branch/tag"
+msgstr "Zu Branch/Tag wechseln"
+
+msgid "System Hooks"
msgstr ""
msgid "Tag (%{tag_count})"
@@ -4007,6 +4322,9 @@ msgstr[1] ""
msgid "Tags"
msgstr ""
+msgid "Tags:"
+msgstr ""
+
msgid "TagsPage|Browse commits"
msgstr ""
@@ -4070,7 +4388,7 @@ msgstr ""
msgid "TagsPage|Use git tag command to add a new one:"
msgstr ""
-msgid "TagsPage|Write your release notes or drag files here..."
+msgid "TagsPage|Write your release notes or drag files here…"
msgstr ""
msgid "TagsPage|protected"
@@ -4085,19 +4403,19 @@ msgstr ""
msgid "Team"
msgstr ""
-msgid "Thanks! Don't show me this again"
+msgid "Terms of Service Agreement and Privacy Policy"
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."
+msgid "Terms of Service and Privacy Policy"
msgstr ""
-msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project"
+msgid "Test coverage parsing"
msgstr ""
-msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
+msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project"
msgstr ""
-msgid "The X509 Certificate to use when mutual TLS is required to communicate with the external authorization service. If left blank, the server certificate is still validated when accessing over HTTPS."
+msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
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."
@@ -4106,9 +4424,6 @@ msgstr "Die Entwicklungsphase stellt die Zeit vom ersten Commit bis zum Erstelle
msgid "The collection of events added to the data gathered for that stage."
msgstr "Ereignisse, die für diese Phase ausgewertet wurden."
-msgid "The connection will time out after %{timeout}. For repositories that take longer, use a clone/push combination."
-msgstr ""
-
msgid "The fork relationship has been removed."
msgstr "Die Beziehung des Ablegers wurde entfernt."
@@ -4127,7 +4442,7 @@ 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 passphrase required to decrypt the private key. This is optional and the value is encrypted at rest."
+msgid "The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>"
msgstr ""
msgid "The phase of the development lifecycle."
@@ -4136,9 +4451,6 @@ msgstr "Die Phase des Entwicklungslebenszyklus."
msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit."
msgstr "Die Planungsphase stellt die Zeit von der vorherigen Phase bis zum Übertragen des ersten Commits dar. Sobald Du den ersten Commit überträgst, werden dessen Daten hier erscheinen."
-msgid "The private key to use when a client certificate is provided. This value is encrypted at rest."
-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 "Die Produktionsphase stellt die Gesamtzeit vom Anlegen eines Tickets bis zur Bereitstellung des Codes auf dem Produktivsystem dar. Sobald Du den vollständigen Entwicklungszyklus, von einer Idee bis zur Fertigstellung, durchlaufen hast, erscheinen die zugehörigen Daten hier."
@@ -4160,7 +4472,7 @@ msgstr ""
msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request."
msgstr "Die Überprüfungsphase stellt die Zeit vom Anlegen eines Merge Requests bis dessen Umsetzung dar. Sobald Du Deinen ersten Merge Request abschließt, werden dessen Daten hier automatisch angezeigt."
-msgid "The roadmap shows the progress of your epics along a timeline"
+msgid "The secure token used by the Runner to checkout the project"
msgstr ""
msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time."
@@ -4187,13 +4499,19 @@ msgstr "Der mittlere aller erfassten Werte. Zum Beispiel ist für 3, 5, 9 der Me
msgid "There are no issues to show"
msgstr ""
+msgid "There are no labels yet"
+msgstr ""
+
msgid "There are no merge requests to show"
msgstr ""
msgid "There are problems accessing Git storage: "
msgstr "Es gibt ein Problem beim Zugriff auf den Gitspeicher:"
-msgid "There was an error loading results"
+msgid "There was an error loading jobs"
+msgstr ""
+
+msgid "There was an error loading latest pipeline"
msgstr ""
msgid "There was an error loading users activity calendar."
@@ -4214,12 +4532,21 @@ msgstr ""
msgid "There was an error when unsubscribing from this label."
msgstr ""
-msgid "This board\\'s scope is reduced"
+msgid "They can be managed using the %{link}."
+msgstr ""
+
+msgid "This GitLab instance does not provide any shared Runners yet. Instance administrators can register shared Runners in the admin area."
+msgstr ""
+
+msgid "This diff is collapsed."
msgstr ""
msgid "This directory"
msgstr ""
+msgid "This group does not provide any group Runners yet."
+msgstr ""
+
msgid "This is a confidential issue."
msgstr ""
@@ -4241,6 +4568,15 @@ msgstr ""
msgid "This job depends on upstream jobs that need to succeed in order for this job to be triggered"
msgstr ""
+msgid "This job does not have a trace."
+msgstr ""
+
+msgid "This job has been canceled"
+msgstr ""
+
+msgid "This job has been skipped"
+msgstr ""
+
msgid "This job has not been triggered yet"
msgstr ""
@@ -4259,19 +4595,28 @@ msgstr "Dies bedeutet, dass Du keinen Code übertragen kannst, bevor Du kein lee
msgid "This merge request is locked."
msgstr ""
+msgid "This option is disabled while you still have unstaged changes"
+msgstr ""
+
msgid "This page is unavailable because you are not allowed to read information across multiple projects."
msgstr ""
+msgid "This page will be removed in a future release."
+msgstr ""
+
msgid "This project"
msgstr ""
+msgid "This project does not belong to a group and can therefore not make use of group Runners."
+msgstr ""
+
msgid "This repository"
msgstr ""
-msgid "This will delete the custom metric, Are you sure?"
+msgid "This source diff could not be displayed because it is too large."
msgstr ""
-msgid "Those emails automatically become issues (with the comments becoming the email conversation) listed here."
+msgid "This user has no identities"
msgstr ""
msgid "Time before an issue gets scheduled"
@@ -4283,10 +4628,10 @@ msgstr "Zeit bis die Implementierung für ein Ticket beginnt"
msgid "Time between merge request creation and merge/close"
msgstr "Zeit zwischen einem Merge Request und dessen Umsetzung / Schließung"
-msgid "Time between updates and capacity settings."
+msgid "Time remaining"
msgstr ""
-msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
+msgid "Time spent"
msgstr ""
msgid "Time tracking"
@@ -4310,6 +4655,9 @@ msgstr "vor %s Tagen"
msgid "Timeago|%s days remaining"
msgstr "%s Tage verbleibend"
+msgid "Timeago|%s hours ago"
+msgstr ""
+
msgid "Timeago|%s hours remaining"
msgstr "%s Stunden verbleibend"
@@ -4325,6 +4673,9 @@ msgstr ""
msgid "Timeago|%s months remaining"
msgstr "%s Monate verbleibend"
+msgid "Timeago|%s seconds ago"
+msgstr ""
+
msgid "Timeago|%s seconds remaining"
msgstr "%s Sekunden verbleibend"
@@ -4340,48 +4691,45 @@ msgstr ""
msgid "Timeago|%s years remaining"
msgstr "%s Jahre verbleibend"
+msgid "Timeago|1 day ago"
+msgstr ""
+
msgid "Timeago|1 day remaining"
msgstr "1 Tag verbleibend"
+msgid "Timeago|1 hour ago"
+msgstr ""
+
msgid "Timeago|1 hour remaining"
msgstr "1 Stunde verbleibend"
+msgid "Timeago|1 minute ago"
+msgstr ""
+
msgid "Timeago|1 minute remaining"
msgstr "1 Minute verbleibend"
+msgid "Timeago|1 month ago"
+msgstr ""
+
msgid "Timeago|1 month remaining"
msgstr "1 Monat verbleibend"
+msgid "Timeago|1 week ago"
+msgstr ""
+
msgid "Timeago|1 week remaining"
msgstr "1 Woche verbleibend"
+msgid "Timeago|1 year ago"
+msgstr ""
+
msgid "Timeago|1 year remaining"
msgstr "1 Jahr verbleibend"
msgid "Timeago|Past due"
msgstr "Fällig"
-msgid "Timeago|a day ago"
-msgstr "vor einem Tag"
-
-msgid "Timeago|a month ago"
-msgstr "vor einem Monat"
-
-msgid "Timeago|a week ago"
-msgstr "vor einer Woche"
-
-msgid "Timeago|a year ago"
-msgstr "vor einem Jahr"
-
-msgid "Timeago|about %s hours ago"
-msgstr "vor ungefähr %s Stunden"
-
-msgid "Timeago|about a minute ago"
-msgstr "vor ungefähr einer Minute"
-
-msgid "Timeago|about an hour ago"
-msgstr "vor ungefähr einer Stunde"
-
msgid "Timeago|in %s days"
msgstr "in %s Tagen"
@@ -4421,11 +4769,14 @@ msgstr "in 1 Woche"
msgid "Timeago|in 1 year"
msgstr "in 1 Jahr"
-msgid "Timeago|in a while"
+msgid "Timeago|just now"
+msgstr ""
+
+msgid "Timeago|right now"
msgstr ""
-msgid "Timeago|less than a minute ago"
-msgstr "vor weniger als einer Minute"
+msgid "Timeout"
+msgstr ""
msgid "Time|hr"
msgid_plural "Time|hrs"
@@ -4443,19 +4794,10 @@ msgstr "Sek."
msgid "Tip:"
msgstr ""
-msgid "Title"
-msgstr ""
-
msgid "To GitLab"
msgstr ""
-msgid "To connect GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to connect."
-msgstr ""
-
-msgid "To connect GitHub repositories, you first need to authorize GitLab to access the list of your GitHub repositories:"
-msgstr ""
-
-msgid "To connect an SVN repository, check out %{svn_link}."
+msgid "To add an SSH key you need to %{generate_link_start}generate one%{link_end} or use an %{existing_link_start}existing key%{link_end}."
msgstr ""
msgid "To import GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
@@ -4467,19 +4809,19 @@ msgstr ""
msgid "To import an SVN repository, check out %{svn_link}."
msgstr ""
-msgid "To only use CI/CD features for an external repository, choose <strong>CI/CD for external repo</strong>."
+msgid "To start serving your jobs you can add Runners to your group"
msgstr ""
-msgid "To set up SAML authentication for your group through an identity provider like Azure, Okta, Onelogin, Ping Identity, or your custom SAML 2.0 provider:"
+msgid "To validate your GitLab CI configurations, go to 'CI/CD → Pipelines' inside your project, and click on the 'CI Lint' button."
msgstr ""
-msgid "To validate your GitLab CI configurations, go to 'CI/CD → Pipelines' inside your project, and click on the 'CI Lint' button."
+msgid "Todo"
msgstr ""
-msgid "To view the roadmap, add a planned start or finish date to one of your epics in this group or its subgroups. Only epics in the past 3 months and the next 3 months are shown."
+msgid "Toggle Sidebar"
msgstr ""
-msgid "Todo"
+msgid "Toggle discussion"
msgstr ""
msgid "Toggle sidebar"
@@ -4491,6 +4833,9 @@ msgstr ""
msgid "ToggleButton|Toggle Status: ON"
msgstr ""
+msgid "Too many changes to show."
+msgstr ""
+
msgid "Total Time"
msgstr "Gesamtzeit"
@@ -4500,22 +4845,19 @@ msgstr "Gesamte Testzeit für alle Commits/Merges"
msgid "Total: %{total}"
msgstr ""
-msgid "Track activity with Contribution Analytics."
-msgstr ""
-
-msgid "Track groups of issues that share a theme, across projects and milestones"
-msgstr ""
-
msgid "Track time with quick actions"
msgstr ""
msgid "Trigger this manual action"
msgstr ""
-msgid "Turn on Service Desk"
+msgid "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."
+msgstr ""
+
+msgid "Try again"
msgstr ""
-msgid "Unknown"
+msgid "Unable to load the diff. %{button_try_again}"
msgstr ""
msgid "Unlock"
@@ -4527,25 +4869,45 @@ msgstr ""
msgid "Unresolve discussion"
msgstr ""
+msgid "Unstage all changes"
+msgstr ""
+
+msgid "Unstage changes"
+msgstr ""
+
+msgid "Unstaged"
+msgstr ""
+
+msgid "Unstaged %{type}"
+msgstr ""
+
+msgid "Unstaged and staged %{type}"
+msgstr ""
+
msgid "Unstar"
msgstr "Entfavorisieren"
-msgid "Up to date"
+msgid "Unsubscribe"
msgstr ""
-msgid "Upgrade your plan to activate Advanced Global Search."
+msgid "Unsubscribe at group level"
msgstr ""
-msgid "Upgrade your plan to activate Contribution Analytics."
+msgid "Unsubscribe at project level"
msgstr ""
-msgid "Upgrade your plan to activate Group Webhooks."
+msgid "Unverified"
msgstr ""
-msgid "Upgrade your plan to activate Issue weight."
+msgid "Up to date"
msgstr ""
-msgid "Upgrade your plan to improve Issue boards."
+msgid "Update %{files}"
+msgid_plural "Update %{files} files"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Update your group name, description, avatar, and other general settings."
msgstr ""
msgid "Upload New File"
@@ -4566,7 +4928,7 @@ msgstr ""
msgid "Usage statistics"
msgstr ""
-msgid "Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab"
+msgid "Use group milestones to manage issues from multiple projects in the same milestone."
msgstr ""
msgid "Use the following registration token during setup:"
@@ -4575,10 +4937,13 @@ msgstr "Benutze den folgenden Registrierungstoken während des Setups:"
msgid "Use your global notification setting"
msgstr "Benutze Deine globalen Benachrichtigungseinstellungen"
-msgid "Used by members to sign in to your group in GitLab"
+msgid "User and IP Rate Limits"
msgstr ""
-msgid "User and IP Rate Limits"
+msgid "Users"
+msgstr ""
+
+msgid "Variables"
msgstr ""
msgid "Variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. You can use variables for passwords, secret keys, or whatever you want."
@@ -4593,10 +4958,7 @@ msgstr ""
msgid "Various settings that affect GitLab performance."
msgstr ""
-msgid "View and edit lines"
-msgstr ""
-
-msgid "View epics list"
+msgid "Verified"
msgstr ""
msgid "View file @ "
@@ -4605,9 +4967,15 @@ msgstr ""
msgid "View group labels"
msgstr ""
+msgid "View jobs"
+msgstr ""
+
msgid "View labels"
msgstr ""
+msgid "View log"
+msgstr ""
+
msgid "View open merge request"
msgstr "Zeige offene Merge Requests."
@@ -4635,9 +5003,6 @@ msgstr "Unbekannt"
msgid "Want to see the data? Please ask an administrator for access."
msgstr "Du möchtest diese Daten sehen? Bitte frage einen Administrator nach dem Zugang."
-msgid "We could not verify that one of your projects on GCP has billing enabled. Please try again."
-msgstr ""
-
msgid "We don't have enough data to show this stage."
msgstr "Es liegen nicht genügend Daten vor, um diese Phase anzuzeigen."
@@ -4650,13 +5015,10 @@ msgstr ""
msgid "Web terminal"
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."
+msgid "When a runner is locked, it cannot be assigned to other projects"
msgstr ""
-msgid "Weight"
-msgstr ""
-
-msgid "When leaving the URL blank, classification labels can still be specified whitout disabling cross project features or performing external authorization checks."
+msgid "When enabled, users cannot use GitLab until the terms have been accepted."
msgstr ""
msgid "Wiki"
@@ -4683,7 +5045,31 @@ msgstr ""
msgid "WikiEdit|There is already a page with the same title in that path."
msgstr ""
-msgid "WikiEmptyPageError|You are not allowed to create wiki pages"
+msgid "WikiEmptyIssueMessage|Suggest wiki improvement"
+msgstr ""
+
+msgid "WikiEmptyIssueMessage|You must be a project member in order to add wiki pages. If you have suggestions for how to improve the wiki for this project, consider opening an issue in the %{issues_link}."
+msgstr ""
+
+msgid "WikiEmptyIssueMessage|issue tracker"
+msgstr ""
+
+msgid "WikiEmpty|A wiki is where you can store all the details about your project. This can include why you've created it, its principles, how to use it, and so on."
+msgstr ""
+
+msgid "WikiEmpty|Create your first page"
+msgstr ""
+
+msgid "WikiEmpty|Suggest wiki improvement"
+msgstr ""
+
+msgid "WikiEmpty|The wiki lets you write documentation for your project"
+msgstr ""
+
+msgid "WikiEmpty|This project has no wiki pages"
+msgstr ""
+
+msgid "WikiEmpty|You must be a project member in order to add wiki pages."
msgstr ""
msgid "WikiHistoricalPage|This is an old version of this page."
@@ -4719,6 +5105,12 @@ msgstr ""
msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?"
msgstr ""
+msgid "WikiPageConfirmDelete|Delete page"
+msgstr ""
+
+msgid "WikiPageConfirmDelete|Delete page %{pageTitle}?"
+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 ""
@@ -4734,7 +5126,7 @@ msgstr ""
msgid "WikiPage|Page slug"
msgstr ""
-msgid "WikiPage|Write your content or drag files here..."
+msgid "WikiPage|Write your content or drag files here…"
msgstr ""
msgid "Wiki|Create Page"
@@ -4746,9 +5138,6 @@ msgstr ""
msgid "Wiki|Edit Page"
msgstr ""
-msgid "Wiki|Empty page"
-msgstr ""
-
msgid "Wiki|More Pages"
msgstr ""
@@ -4767,13 +5156,10 @@ 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 "Zugriffsanfrage widerrufen"
-msgid "Write a commit message..."
+msgid "Yes"
msgstr ""
msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
@@ -4791,7 +5177,7 @@ msgstr ""
msgid "You are on a read-only GitLab instance."
msgstr ""
-msgid "You are on a secondary (read-only) Geo node. If you want to make any changes, you must visit the %{primary_node}."
+msgid "You can %{linkStart}view the blob%{linkEnd} instead."
msgstr ""
msgid "You can also create a project from the command line."
@@ -4800,6 +5186,9 @@ msgstr ""
msgid "You can also star a label to make it a priority label."
msgstr ""
+msgid "You can also test your .gitlab-ci.yml in the %{linkStart}Lint%{linkEnd}"
+msgstr ""
+
msgid "You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}"
msgstr ""
@@ -4812,30 +5201,33 @@ msgstr "Du kannst Dateien nur hinzufügen, wenn Du dich auf einem Branch befinde
msgid "You can only edit 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."
+msgid "You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}"
msgstr ""
msgid "You cannot write to this read-only GitLab instance."
msgstr ""
-msgid "You do not have the correct permissions to override the settings from the LDAP group sync."
+msgid "You do not have any assigned merge requests"
msgstr ""
msgid "You have no permissions"
msgstr ""
+msgid "You have not created any merge requests"
+msgstr ""
+
msgid "You have reached your project limit"
msgstr "Du hast die Projektbegrenzung erreicht."
-msgid "You must have master access to force delete a lock"
+msgid "You must accept our Terms of Service and privacy policy in order to register an account"
+msgstr ""
+
+msgid "You must have maintainer access to force delete a lock"
msgstr ""
msgid "You must sign in to star a project"
msgstr "Du musst angemeldet sein, um ein Projekt zu favorisieren."
-msgid "You need a different license to enable FileLocks feature"
-msgstr ""
-
msgid "You need permission."
msgstr "Du brauchst eine Genehmigung."
@@ -4902,13 +5294,11 @@ msgstr "Dein Name"
msgid "Your projects"
msgstr "Deine Projekte"
-msgid "among other things"
+msgid "ago"
msgstr ""
-msgid "and %d fixed vulnerability"
-msgid_plural "and %d fixed vulnerabilities"
-msgstr[0] ""
-msgstr[1] ""
+msgid "among other things"
+msgstr ""
msgid "assign yourself"
msgstr ""
@@ -4916,136 +5306,36 @@ msgstr ""
msgid "branch name"
msgstr ""
-msgid "by"
-msgstr ""
-
-msgid "ciReport|%{type} detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|%{type} detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Code quality"
-msgstr ""
-
-msgid "ciReport|DAST detected no alerts by analyzing the review app"
-msgstr ""
-
-msgid "ciReport|Dependency scanning"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Failed to load %{reportName} report"
-msgstr ""
-
-msgid "ciReport|Fixed:"
-msgstr ""
-
-msgid "ciReport|Instances"
-msgstr ""
-
-msgid "ciReport|Learn more about whitelisting"
-msgstr ""
-
-msgid "ciReport|Loading %{reportName} report"
-msgstr ""
-
-msgid "ciReport|No changes to code quality"
-msgstr ""
-
-msgid "ciReport|No changes to performance metrics"
-msgstr ""
-
-msgid "ciReport|Performance metrics"
-msgstr ""
-
-msgid "ciReport|SAST"
-msgstr ""
-
-msgid "ciReport|SAST detected"
-msgstr ""
-
-msgid "ciReport|SAST detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|SAST detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|SAST:container no vulnerabilities were found"
-msgstr ""
-
-msgid "ciReport|Security scanning"
-msgstr ""
-
-msgid "ciReport|Security scanning failed loading any results"
-msgstr ""
-
-msgid "ciReport|Show complete code vulnerabilities report"
-msgstr ""
-
-msgid "ciReport|Unapproved vulnerabilities (red) can be marked as approved. %{helpLink}"
-msgstr ""
-
-msgid "ciReport|no vulnerabilities"
-msgstr ""
-
msgid "command line instructions"
msgstr ""
msgid "connecting"
msgstr ""
-msgid "could not read private key, is the passphrase correct?"
-msgstr ""
-
msgid "day"
msgid_plural "days"
msgstr[0] "Tag"
msgstr[1] "Tage"
-msgid "detected %d fixed vulnerability"
-msgid_plural "detected %d fixed vulnerabilities"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "detected %d new vulnerability"
-msgid_plural "detected %d new vulnerabilities"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "detected no vulnerabilities"
+msgid "deploy token"
msgstr ""
-msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
+msgid "disabled"
msgstr ""
-msgid "here"
+msgid "enabled"
msgstr ""
-msgid "importing"
-msgstr ""
-
-msgid "in progress"
-msgstr ""
-
-msgid "is invalid because there is downstream lock"
+msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
msgstr ""
-msgid "is invalid because there is upstream lock"
+msgid "for this project"
msgstr ""
-msgid "is not a valid X509 certificate."
+msgid "importing"
msgstr ""
-msgid "locked by %{path_lock_user_name} %{created_at}"
+msgid "latest version"
msgstr ""
msgid "merge request"
@@ -5065,28 +5355,7 @@ msgstr ""
msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage is %{emphasisStart} unchanged %{emphasisEnd} at %{memoryFrom}MB"
msgstr ""
-msgid "mrWidget|Add approval"
-msgstr ""
-
-msgid "mrWidget|Allows edits from maintainers"
-msgstr ""
-
-msgid "mrWidget|An error occured while removing your approval."
-msgstr ""
-
-msgid "mrWidget|An error occured while retrieving approval data for this merge request."
-msgstr ""
-
-msgid "mrWidget|An error occured while submitting your approval."
-msgstr ""
-
-msgid "mrWidget|Approve"
-msgstr ""
-
-msgid "mrWidget|Approved"
-msgstr ""
-
-msgid "mrWidget|Approved by"
+msgid "mrWidget|Allows commits from members who can merge to the target branch"
msgstr ""
msgid "mrWidget|Cancel automatic merge"
@@ -5113,6 +5382,9 @@ msgstr ""
msgid "mrWidget|Closes"
msgstr ""
+msgid "mrWidget|Create an issue to resolve them later"
+msgstr ""
+
msgid "mrWidget|Deployment statistics are not available currently"
msgstr ""
@@ -5167,9 +5439,6 @@ msgstr ""
msgid "mrWidget|Remove source branch"
msgstr ""
-msgid "mrWidget|Remove your approval"
-msgstr ""
-
msgid "mrWidget|Request to merge"
msgstr ""
@@ -5209,6 +5478,9 @@ msgstr ""
msgid "mrWidget|There are merge conflicts"
msgstr ""
+msgid "mrWidget|There are unresolved discussions. Please resolve these discussions"
+msgstr ""
+
msgid "mrWidget|This merge request failed to be merged automatically"
msgstr ""
@@ -5259,7 +5531,7 @@ msgstr ""
msgid "personal access token"
msgstr ""
-msgid "private key does not match certificate."
+msgid "remaining"
msgstr ""
msgid "remove due date"
@@ -5274,9 +5546,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "to help your contributors communicate effectively!"
-msgstr ""
-
msgid "username"
msgstr ""
@@ -5286,3 +5555,8 @@ msgstr ""
msgid "with %{additions} additions, %{deletions} deletions."
msgstr ""
+msgid "within %d minute "
+msgid_plural "within %d minutes "
+msgstr[0] ""
+msgstr[1] ""
+
diff --git a/locale/eo/gitlab.po b/locale/eo/gitlab.po
index 009ad31d28b..39e985b244c 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: 2018-04-04 19:35+0200\n"
-"PO-Revision-Date: 2018-04-05 03:38-0400\n"
+"POT-Creation-Date: 2018-07-01 16:35+1000\n"
+"PO-Revision-Date: 2018-07-01 11:02\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Esperanto\n"
"Language: eo_UY\n"
@@ -16,8 +16,10 @@ msgstr ""
"X-Crowdin-Language: eo\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-msgid " and"
-msgstr ""
+msgid "%d changed file"
+msgid_plural "%d changed files"
+msgstr[0] ""
+msgstr[1] ""
msgid "%d commit"
msgid_plural "%d commits"
@@ -54,6 +56,16 @@ msgid_plural "%d metrics"
msgstr[0] ""
msgstr[1] ""
+msgid "%d staged change"
+msgid_plural "%d staged changes"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "%d unstaged change"
+msgid_plural "%d unstaged changes"
+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."
@@ -70,12 +82,21 @@ msgid_plural "%{count} participants"
msgstr[0] ""
msgstr[1] ""
+msgid "%{filePath} deleted"
+msgstr ""
+
+msgid "%{group_docs_link_start}Groups%{group_docs_link_end} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects."
+msgstr ""
+
msgid "%{loadingIcon} Started"
msgstr ""
msgid "%{lock_path} is locked by GitLab User %{lock_user_id}"
msgstr ""
+msgid "%{nip_domain} can be used as an alternative to a custom domain."
+msgstr ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -88,6 +109,9 @@ msgstr ""
msgid "%{openOrClose} %{noteable}"
msgstr ""
+msgid "%{percent}%% complete"
+msgstr ""
+
msgid "%{storage_name}: failed storage access attempt on host:"
msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:"
msgstr[0] ""
@@ -96,15 +120,62 @@ msgstr[1] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{title} changes"
+msgstr ""
+
+msgid "%{unstaged} unstaged and %{staged} staged changes"
+msgstr ""
+
msgid "(checkout the %{link} for information on how to install it)."
msgstr ""
msgid "+ %{moreCount} more"
msgstr ""
+msgid "- Runner is active and can process any new jobs"
+msgstr ""
+
+msgid "- Runner is paused and will not receive any new jobs"
+msgstr ""
+
msgid "- show less"
msgstr ""
+msgid "1 %{type} addition"
+msgid_plural "%{count} %{type} additions"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 %{type} modification"
+msgid_plural "%{count} %{type} modifications"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 closed issue"
+msgid_plural "%d closed issues"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 closed merge request"
+msgid_plural "%d closed merge requests"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 merged merge request"
+msgid_plural "%d merged merge requests"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 open issue"
+msgid_plural "%d open issues"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 open merge request"
+msgid_plural "%d open merge requests"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] "1 ĉenstablo"
@@ -116,9 +187,27 @@ msgstr ""
msgid "2FA enabled"
msgstr ""
+msgid "403|Please contact your GitLab administrator to get the permission."
+msgstr ""
+
+msgid "403|You don't have the permission to access this page."
+msgstr ""
+
+msgid "404|Make sure the address is correct and the page hasn't moved."
+msgstr ""
+
+msgid "404|Page Not Found"
+msgstr ""
+
+msgid "404|Please contact your GitLab administrator if you think this is a mistake."
+msgstr ""
+
msgid "<strong>Removes</strong> source branch"
msgstr ""
+msgid "A 'Runner' is a process which runs a job. You can setup as many Runners as you need."
+msgstr ""
+
msgid "A collection of graphs regarding Continuous Integration"
msgstr "Aro da diagramoj pri la seninterrompa integrado"
@@ -128,6 +217,9 @@ msgstr ""
msgid "A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}."
msgstr ""
+msgid "A regular expression that will be used to find the test coverage output in the job trace. Leave blank to disable"
+msgstr ""
+
msgid "A user with write access to the source branch selected this option"
msgstr ""
@@ -140,36 +232,39 @@ msgstr ""
msgid "Abuse reports"
msgstr ""
+msgid "Accept terms"
+msgstr ""
+
msgid "Access Tokens"
msgstr ""
msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again."
msgstr ""
+msgid "Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report."
+msgstr ""
+
msgid "Account"
msgstr ""
-msgid "Account and limit settings"
+msgid "Account and limit"
msgstr ""
msgid "Active"
msgstr "Aktiva"
+msgid "Active Sessions"
+msgstr ""
+
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 Kubernetes cluster"
msgstr ""
@@ -182,6 +277,9 @@ msgstr ""
msgid "Add new directory"
msgstr "Aldoni novan dosierujon"
+msgid "Add reaction"
+msgstr ""
+
msgid "Add todo"
msgstr ""
@@ -251,7 +349,10 @@ msgstr ""
msgid "All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings."
msgstr ""
-msgid "Allow edits from maintainers."
+msgid "Allow commits from members who can merge to the target branch."
+msgstr ""
+
+msgid "Allow public access to pipelines and job details, including output logs and artifacts"
msgstr ""
msgid "Allow rendering of PlantUML diagrams in Asciidoc documents."
@@ -263,31 +364,37 @@ msgstr ""
msgid "Allows you to add and manage Kubernetes clusters."
msgstr ""
-msgid "Also called \"Issuer\" or \"Relying party trust identifier\""
+msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
msgstr ""
-msgid "Also called \"Relying party service URL\" or \"Reply URL\""
+msgid "An error occured creating the new branch."
msgstr ""
-msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to connect."
+msgid "An error occured whilst loading all the files."
msgstr ""
-msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
+msgid "An error occured whilst loading the file content."
msgstr ""
-msgid "An error occurred previewing the blob"
+msgid "An error occured whilst loading the file."
msgstr ""
-msgid "An error occurred when toggling the notification subscription"
+msgid "An error occured whilst loading the merge request changes."
+msgstr ""
+
+msgid "An error occured whilst loading the merge request version data."
+msgstr ""
+
+msgid "An error occured whilst loading the merge request."
msgstr ""
-msgid "An error occurred when updating the issue weight"
+msgid "An error occurred previewing the blob"
msgstr ""
-msgid "An error occurred while adding approver"
+msgid "An error occurred when toggling the notification subscription"
msgstr ""
-msgid "An error occurred while detecting host keys"
+msgid "An error occurred while dismissing the alert. Refresh the page and try again."
msgstr ""
msgid "An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again."
@@ -305,10 +412,7 @@ msgstr ""
msgid "An error occurred while getting projects"
msgstr ""
-msgid "An error occurred while importing project"
-msgstr ""
-
-msgid "An error occurred while initializing path locks"
+msgid "An error occurred while importing project: ${details}"
msgstr ""
msgid "An error occurred while loading commits"
@@ -326,9 +430,6 @@ msgstr ""
msgid "An error occurred while making the request."
msgstr ""
-msgid "An error occurred while removing approver"
-msgstr ""
-
msgid "An error occurred while rendering KaTeX"
msgstr ""
@@ -341,9 +442,6 @@ msgstr ""
msgid "An error occurred while retrieving diff"
msgstr ""
-msgid "An error occurred while saving LDAP override status. Please try again."
-msgstr ""
-
msgid "An error occurred while saving assignees"
msgstr ""
@@ -353,9 +451,6 @@ msgstr ""
msgid "An error occurred. Please try again."
msgstr ""
-msgid "Any Label"
-msgstr ""
-
msgid "Appearance"
msgstr ""
@@ -368,19 +463,19 @@ msgstr ""
msgid "April"
msgstr ""
-msgid "Archived project! Repository is read-only"
-msgstr "Arkivita projekto! La deponejo permesas nur legadon"
+msgid "Archived project! Repository and other project resources are read-only"
+msgstr ""
msgid "Are you sure you want to delete this pipeline schedule?"
msgstr "Ĉu vi certe volas forigi ĉi tiun ĉenstablan planon?"
-msgid "Are you sure you want to reset registration token?"
+msgid "Are you sure you want to remove this identity?"
msgstr ""
-msgid "Are you sure you want to reset the health check token?"
+msgid "Are you sure you want to reset registration token?"
msgstr ""
-msgid "Are you sure you want to unlock %{path_lock_path}?"
+msgid "Are you sure you want to reset the health check token?"
msgstr ""
msgid "Are you sure?"
@@ -389,7 +484,7 @@ msgstr ""
msgid "Artifacts"
msgstr ""
-msgid "Assertion consumer service URL"
+msgid "Ask your group maintainer to setup a group Runner."
msgstr ""
msgid "Assign custom color like #FF0000"
@@ -413,9 +508,15 @@ msgstr ""
msgid "Assigned to :name"
msgstr ""
+msgid "Assigned to me"
+msgstr ""
+
msgid "Assignee"
msgstr ""
+msgid "Assignee(s)"
+msgstr ""
+
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "Alkroĉu dosieron per Åovmetado aÅ­ %{upload_link}"
@@ -449,7 +550,10 @@ msgstr ""
msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
msgstr ""
-msgid "AutoDevOps|Auto DevOps (Beta)"
+msgid "Auto-cancel redundant, pending pipelines"
+msgstr ""
+
+msgid "AutoDevOps|Auto DevOps"
msgstr ""
msgid "AutoDevOps|Auto DevOps documentation"
@@ -470,79 +574,109 @@ msgstr ""
msgid "AutoDevOps|add a Kubernetes cluster"
msgstr ""
-msgid "AutoDevOps|enable Auto DevOps (Beta)"
+msgid "AutoDevOps|enable Auto DevOps"
msgstr ""
msgid "Available"
msgstr ""
+msgid "Available group Runners : %{runners}"
+msgstr ""
+
+msgid "Available group Runners : %{runners}."
+msgstr ""
+
msgid "Avatar will be removed. Are you sure?"
msgstr ""
msgid "Average per day: %{average}"
msgstr ""
-msgid "Background Color"
+msgid "Background color"
msgstr ""
msgid "Background jobs"
msgstr ""
-msgid "Begin with the selected commit"
+msgid "Badges"
msgstr ""
-msgid "Billing"
+msgid "Badges|A new badge was added."
msgstr ""
-msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan."
+msgid "Badges|Add badge"
msgstr ""
-msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available."
+msgid "Badges|Adding the badge failed, please check the entered URLs and try again."
msgstr ""
-msgid "BillingPlans|Current plan"
+msgid "Badges|Badge image URL"
msgstr ""
-msgid "BillingPlans|Customer Support"
+msgid "Badges|Badge image preview"
msgstr ""
-msgid "BillingPlans|Downgrade"
+msgid "Badges|Delete badge"
msgstr ""
-msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
+msgid "Badges|Delete badge?"
msgstr ""
-msgid "BillingPlans|Manage plan"
+msgid "Badges|Deleting the badge failed, please try again."
msgstr ""
-msgid "BillingPlans|Please contact %{customer_support_link} in that case."
+msgid "Badges|Group Badge"
msgstr ""
-msgid "BillingPlans|See all %{plan_name} features"
+msgid "Badges|Link"
msgstr ""
-msgid "BillingPlans|This group uses the plan associated with its parent group."
+msgid "Badges|No badge image"
msgstr ""
-msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
+msgid "Badges|No image to preview"
msgstr ""
-msgid "BillingPlans|Upgrade"
+msgid "Badges|Project Badge"
msgstr ""
-msgid "BillingPlans|You are currently on the %{plan_link} plan."
+msgid "Badges|Reload badge image"
msgstr ""
-msgid "BillingPlans|frequently asked questions"
+msgid "Badges|Save changes"
msgstr ""
-msgid "BillingPlans|monthly"
+msgid "Badges|Saving the badge failed, please check the entered URLs and try again."
msgstr ""
-msgid "BillingPlans|paid annually at %{price_per_year}"
+msgid "Badges|The %{docsLinkStart}variables%{docsLinkEnd} GitLab supports: %{placeholders}"
msgstr ""
-msgid "BillingPlans|per user"
+msgid "Badges|The badge was deleted."
+msgstr ""
+
+msgid "Badges|The badge was saved."
+msgstr ""
+
+msgid "Badges|This group has no badges"
+msgstr ""
+
+msgid "Badges|This project has no badges"
+msgstr ""
+
+msgid "Badges|Your badges"
+msgstr ""
+
+msgid "Begin with the selected commit"
+msgstr ""
+
+msgid "Below are examples of regex for existing tools:"
+msgstr ""
+
+msgid "Boards"
+msgstr ""
+
+msgid "Branch %{branchName} was not found in this project's repository."
msgstr ""
msgid "Branch (%{branch_count})"
@@ -622,7 +756,7 @@ msgstr ""
msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
msgstr ""
-msgid "Branches|Only a project master or owner can delete a protected branch"
+msgid "Branches|Only a project maintainer or owner can delete a protected branch"
msgstr ""
msgid "Branches|Overview"
@@ -658,9 +792,6 @@ msgstr ""
msgid "Branches|Stale branches"
msgstr ""
-msgid "Branches|The branch could not be updated automatically because it has diverged from its upstream counterpart."
-msgstr ""
-
msgid "Branches|The default branch cannot be deleted"
msgstr ""
@@ -673,15 +804,9 @@ msgstr ""
msgid "Branches|To confirm, type %{branch_name_confirmation}:"
msgstr ""
-msgid "Branches|To discard the local changes and overwrite the branch with the upstream version, delete it here and choose 'Update Now' above."
-msgstr ""
-
msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
msgstr ""
-msgid "Branches|diverged from upstream"
-msgstr ""
-
msgid "Branches|merged"
msgstr ""
@@ -703,40 +828,79 @@ msgstr "Foliumi dosierojn"
msgid "Browse files"
msgstr "Elekti dosierojn"
-msgid "Business"
-msgstr ""
-
msgid "ByAuthor|by"
msgstr "de"
msgid "CI / CD"
msgstr ""
-msgid "CI/CD"
+msgid "CI / CD Settings"
msgstr ""
msgid "CI/CD configuration"
msgstr ""
-msgid "CI/CD for external repo"
+msgid "CI/CD settings"
+msgstr ""
+
+msgid "CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery."
+msgstr ""
+
+msgid "CICD|Auto DevOps"
+msgstr ""
+
+msgid "CICD|Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration."
+msgstr ""
+
+msgid "CICD|Automatic deployment to staging, manual deployment to production"
+msgstr ""
+
+msgid "CICD|Continuous deployment to production"
+msgstr ""
+
+msgid "CICD|Deployment strategy"
+msgstr ""
+
+msgid "CICD|Deployment strategy needs a domain name to work correctly."
+msgstr ""
+
+msgid "CICD|Disable Auto DevOps"
+msgstr ""
+
+msgid "CICD|Enable Auto DevOps"
+msgstr ""
+
+msgid "CICD|Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific %{ci_file}."
+msgstr ""
+
+msgid "CICD|Instance default (%{state})"
msgstr ""
msgid "CICD|Jobs"
msgstr ""
-msgid "Cancel"
-msgstr "Nuligi"
+msgid "CICD|Learn more about Auto DevOps"
+msgstr ""
-msgid "Cannot be merged automatically"
+msgid "CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project."
msgstr ""
-msgid "Cannot modify managed Kubernetes cluster"
+msgid "CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages."
+msgstr ""
+
+msgid "Can't find HEAD commit for this branch"
msgstr ""
-msgid "Certificate fingerprint"
+msgid "Cancel"
+msgstr "Nuligi"
+
+msgid "Cancel this job"
msgstr ""
-msgid "Change Weight"
+msgid "Cannot be merged automatically"
+msgstr ""
+
+msgid "Cannot modify managed Kubernetes cluster"
msgstr ""
msgid "Change this value to influence how frequently the GitLab UI polls for updates."
@@ -790,21 +954,18 @@ msgstr ""
msgid "Choose a branch/tag (e.g. %{master}) or enter a commit (e.g. %{sha}) to see what's changed or to create a merge request."
msgstr ""
-msgid "Choose file..."
+msgid "Choose any color."
msgstr ""
-msgid "Choose which groups you wish to synchronize to this secondary node."
+msgid "Choose between <code>clone</code> or <code>fetch</code> to get the recent application code"
msgstr ""
-msgid "Choose which repositories you want to connect and run CI/CD pipelines."
+msgid "Choose file..."
msgstr ""
msgid "Choose which repositories you want to import."
msgstr ""
-msgid "Choose which shards you wish to synchronize to this secondary node."
-msgstr ""
-
msgid "CiStatusLabel|canceled"
msgstr "nuligita"
@@ -874,21 +1035,12 @@ msgstr ""
msgid "CiVariable|All environments"
msgstr ""
-msgid "CiVariable|Create wildcard"
-msgstr ""
-
msgid "CiVariable|Error occured while saving variables"
msgstr ""
-msgid "CiVariable|New environment"
-msgstr ""
-
msgid "CiVariable|Protected"
msgstr ""
-msgid "CiVariable|Search environments"
-msgstr ""
-
msgid "CiVariable|Toggle protected"
msgstr ""
@@ -898,28 +1050,28 @@ msgstr ""
msgid "CircuitBreakerApiLink|circuitbreaker api"
msgstr ""
-msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
+msgid "Clear search input"
msgstr ""
-msgid "Click to expand text"
+msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Client authentication certificate"
+msgid "Click the <strong>Promote</strong> button in the top right corner to promote it to a group milestone."
msgstr ""
-msgid "Client authentication key"
+msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
msgstr ""
-msgid "Client authentication key password"
+msgid "Click to expand it."
msgstr ""
-msgid "Clone repository"
+msgid "Click to expand text"
msgstr ""
-msgid "Close"
+msgid "Clone repository"
msgstr ""
-msgid "Closed"
+msgid "Close"
msgstr ""
msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster"
@@ -937,6 +1089,12 @@ msgstr ""
msgid "ClusterIntegration|Advanced options on this Kubernetes cluster's integration"
msgstr ""
+msgid "ClusterIntegration|An error occured while trying to fetch project zones: %{error}"
+msgstr ""
+
+msgid "ClusterIntegration|An error occured while trying to fetch your projects: %{error}"
+msgstr ""
+
msgid "ClusterIntegration|Applications"
msgstr ""
@@ -967,6 +1125,9 @@ msgstr ""
msgid "ClusterIntegration|Copy Ingress IP Address to clipboard"
msgstr ""
+msgid "ClusterIntegration|Copy Jupyter Hostname to clipboard"
+msgstr ""
+
msgid "ClusterIntegration|Copy Kubernetes cluster name"
msgstr ""
@@ -982,7 +1143,7 @@ msgstr ""
msgid "ClusterIntegration|Create a new Kubernetes cluster on Google Kubernetes Engine right from GitLab"
msgstr ""
-msgid "ClusterIntegration|Create on GKE"
+msgid "ClusterIntegration|Create on Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
@@ -994,13 +1155,25 @@ msgstr ""
msgid "ClusterIntegration|Environment scope"
msgstr ""
+msgid "ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab's Google Kubernetes Engine Integration."
+msgstr ""
+
+msgid "ClusterIntegration|Fetching machine types"
+msgstr ""
+
+msgid "ClusterIntegration|Fetching projects"
+msgstr ""
+
+msgid "ClusterIntegration|Fetching zones"
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
msgid "ClusterIntegration|GitLab Runner"
msgstr ""
-msgid "ClusterIntegration|Google Cloud Platform project ID"
+msgid "ClusterIntegration|Google Cloud Platform project"
msgstr ""
msgid "ClusterIntegration|Google Kubernetes Engine"
@@ -1012,9 +1185,6 @@ msgstr ""
msgid "ClusterIntegration|Helm Tiller"
msgstr ""
-msgid "ClusterIntegration|In order to show the health of the cluster, we'll need to provision your cluster with Prometheus to collect the required data."
-msgstr ""
-
msgid "ClusterIntegration|Ingress"
msgstr ""
@@ -1024,9 +1194,6 @@ msgstr ""
msgid "ClusterIntegration|Install"
msgstr ""
-msgid "ClusterIntegration|Install Prometheus"
-msgstr ""
-
msgid "ClusterIntegration|Installed"
msgstr ""
@@ -1039,13 +1206,16 @@ msgstr ""
msgid "ClusterIntegration|Integration status"
msgstr ""
-msgid "ClusterIntegration|Kubernetes cluster"
+msgid "ClusterIntegration|Jupyter Hostname"
msgstr ""
-msgid "ClusterIntegration|Kubernetes cluster details"
+msgid "ClusterIntegration|JupyterHub"
msgstr ""
-msgid "ClusterIntegration|Kubernetes cluster health"
+msgid "ClusterIntegration|Kubernetes cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Kubernetes cluster details"
msgstr ""
msgid "ClusterIntegration|Kubernetes cluster integration"
@@ -1075,7 +1245,13 @@ msgstr ""
msgid "ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project"
msgstr ""
-msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
+msgid "ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}."
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{help_link_start}Kubernetes%{help_link_end}."
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}."
msgstr ""
msgid "ClusterIntegration|Learn more about environments"
@@ -1099,7 +1275,16 @@ msgstr ""
msgid "ClusterIntegration|More information"
msgstr ""
-msgid "ClusterIntegration|Multiple Kubernetes clusters are available in GitLab Enterprise Edition Premium and Ultimate"
+msgid "ClusterIntegration|No machine types matched your search"
+msgstr ""
+
+msgid "ClusterIntegration|No projects found"
+msgstr ""
+
+msgid "ClusterIntegration|No projects matched your search"
+msgstr ""
+
+msgid "ClusterIntegration|No zones matched your search"
msgstr ""
msgid "ClusterIntegration|Note:"
@@ -1114,9 +1299,6 @@ msgstr ""
msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
msgstr ""
-msgid "ClusterIntegration|Project ID"
-msgstr ""
-
msgid "ClusterIntegration|Project namespace"
msgstr ""
@@ -1129,6 +1311,9 @@ msgstr ""
msgid "ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration."
msgstr ""
+msgid "ClusterIntegration|Redeem up to $500 in free credit for Google Cloud Platform"
+msgstr ""
+
msgid "ClusterIntegration|Remove Kubernetes cluster integration"
msgstr ""
@@ -1144,19 +1329,37 @@ msgstr ""
msgid "ClusterIntegration|Save changes"
msgstr ""
+msgid "ClusterIntegration|Search machine types"
+msgstr ""
+
+msgid "ClusterIntegration|Search projects"
+msgstr ""
+
+msgid "ClusterIntegration|Search zones"
+msgstr ""
+
msgid "ClusterIntegration|Security"
msgstr ""
msgid "ClusterIntegration|See and edit the details for your Kubernetes cluster"
msgstr ""
-msgid "ClusterIntegration|See machine types"
+msgid "ClusterIntegration|Select machine type"
+msgstr ""
+
+msgid "ClusterIntegration|Select project"
+msgstr ""
+
+msgid "ClusterIntegration|Select project and zone to choose machine type"
+msgstr ""
+
+msgid "ClusterIntegration|Select project to choose zone"
msgstr ""
-msgid "ClusterIntegration|See your projects"
+msgid "ClusterIntegration|Select zone"
msgstr ""
-msgid "ClusterIntegration|See zones"
+msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
msgid "ClusterIntegration|Service token"
@@ -1189,6 +1392,9 @@ msgstr ""
msgid "ClusterIntegration|Token"
msgstr ""
+msgid "ClusterIntegration|Validating project billing status"
+msgstr ""
+
msgid "ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
@@ -1219,13 +1425,19 @@ msgstr ""
msgid "ClusterIntegration|properly configured"
msgstr ""
+msgid "ClusterIntegration|sign up"
+msgstr ""
+
msgid "Collapse"
msgstr ""
-msgid "Comment and resolve discussion"
+msgid "Collapse sidebar"
+msgstr ""
+
+msgid "Comment & resolve discussion"
msgstr ""
-msgid "Comment and unresolve discussion"
+msgid "Comment & unresolve discussion"
msgstr ""
msgid "Comments"
@@ -1292,6 +1504,9 @@ msgstr ""
msgid "Committed by"
msgstr "Enmetita de"
+msgid "Commit…"
+msgstr ""
+
msgid "Compare"
msgstr "Kompari"
@@ -1340,6 +1555,9 @@ msgstr ""
msgid "Configure limits for web and API requests."
msgstr ""
+msgid "Configure push mirrors."
+msgstr ""
+
msgid "Configure storage path and circuit breaker settings."
msgstr ""
@@ -1349,18 +1567,9 @@ msgstr ""
msgid "Connect"
msgstr ""
-msgid "Connect all repositories"
-msgstr ""
-
msgid "Connect repositories from GitHub"
msgstr ""
-msgid "Connect your external repositories, and CI/CD pipelines will run for new commits. A GitLab project will be created with only CI/CD features enabled."
-msgstr ""
-
-msgid "Connecting..."
-msgstr ""
-
msgid "Container Registry"
msgstr ""
@@ -1406,9 +1615,18 @@ 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 "ContainerRegistry|You can also use a %{deploy_token} for read-only access to the registry images."
+msgstr ""
+
+msgid "Continue"
+msgstr ""
+
msgid "Continuous Integration and Deployment"
msgstr ""
+msgid "Contribute to GitLab"
+msgstr ""
+
msgid "Contribution"
msgstr ""
@@ -1430,15 +1648,6 @@ msgstr ""
msgid "ContributorsPage|Please wait a moment, this page will automatically refresh when ready."
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 ""
-
msgid "Copy URL to clipboard"
msgstr "Kopii la adreson en la kopibufron"
@@ -1451,9 +1660,18 @@ msgstr ""
msgid "Copy commit SHA to clipboard"
msgstr "Kopii la identigilon de la enmetado"
+msgid "Copy file name to clipboard"
+msgstr ""
+
+msgid "Copy file path to clipboard"
+msgstr ""
+
msgid "Copy reference to clipboard"
msgstr ""
+msgid "Copy to clipboard"
+msgstr ""
+
msgid "Create"
msgstr ""
@@ -1472,15 +1690,15 @@ msgstr "Kreu propran atingoĵetonon en via konto por ebligi al vi eltiri kaj alp
msgid "Create branch"
msgstr ""
+msgid "Create commit"
+msgstr ""
+
msgid "Create directory"
msgstr "Krei dosierujon"
msgid "Create empty repository"
msgstr ""
-msgid "Create epic"
-msgstr ""
-
msgid "Create file"
msgstr ""
@@ -1523,13 +1741,10 @@ msgstr "Etikedo"
msgid "CreateTokenToCloneLink|create a personal access token"
msgstr "kreos propran atingoĵetonon"
-msgid "Creates a new branch from %{branchName}"
+msgid "Created"
msgstr ""
-msgid "Creates a new branch from %{branchName} and re-directs to create a new merge request"
-msgstr ""
-
-msgid "Creating epic"
+msgid "Created by me"
msgstr ""
msgid "Cron Timezone"
@@ -1538,7 +1753,13 @@ msgstr "Horzono por Cron"
msgid "Cron syntax"
msgstr "La sintakso de Cron"
-msgid "Current node"
+msgid "CurrentUser|Profile"
+msgstr ""
+
+msgid "CurrentUser|Settings"
+msgstr ""
+
+msgid "Custom CI config path"
msgstr ""
msgid "Custom notification events"
@@ -1547,9 +1768,6 @@ msgstr "Propraj sciigaj eventoj"
msgid "Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notification_link}."
msgstr "La propraj sciigaj niveloj estas la samaj kiel la niveloj de partoprenado. Uzante la proprajn sciigajn nivelojn, vi ricevos ankaÅ­ sciigojn por elektitaj de vi eventoj. Por lerni pli, bonvolu vidi %{notification_link}."
-msgid "Customize colors"
-msgstr ""
-
msgid "Cycle Analytics"
msgstr "Cikla analizo"
@@ -1586,7 +1804,7 @@ msgstr ""
msgid "December"
msgstr ""
-msgid "Default classification label"
+msgid "Decline and sign out"
msgstr ""
msgid "Define a custom pattern with cron syntax"
@@ -1595,6 +1813,9 @@ msgstr "Difini propran Åablonon, uzante la sintakson de Cron"
msgid "Delete"
msgstr "Forigi"
+msgid "Delete list"
+msgstr ""
+
msgid "Deploy"
msgid_plural "Deploys"
msgstr[0] "Disponigado"
@@ -1603,37 +1824,163 @@ msgstr[1] "Disponigadoj"
msgid "Deploy Keys"
msgstr ""
-msgid "Description"
-msgstr "Priskribo"
+msgid "DeployKeys|+%{count} others"
+msgstr ""
+
+msgid "DeployKeys|Current project"
+msgstr ""
+
+msgid "DeployKeys|Deploy key"
+msgstr ""
+
+msgid "DeployKeys|Enabled deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Error enabling deploy key"
+msgstr ""
+
+msgid "DeployKeys|Error getting deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Error removing deploy key"
+msgstr ""
+
+msgid "DeployKeys|Expand %{count} other projects"
+msgstr ""
+
+msgid "DeployKeys|Loading deploy keys"
+msgstr ""
+
+msgid "DeployKeys|No deploy keys found. Create one with the form above."
+msgstr ""
+
+msgid "DeployKeys|Privately accessible deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Project usage"
+msgstr ""
+
+msgid "DeployKeys|Publicly accessible deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Read access only"
+msgstr ""
+
+msgid "DeployKeys|Write access allowed"
+msgstr ""
+
+msgid "DeployKeys|You are going to remove this deploy key. Are you sure?"
+msgstr ""
+
+msgid "DeployTokens|Active Deploy Tokens (%{active_tokens})"
+msgstr ""
+
+msgid "DeployTokens|Add a deploy token"
+msgstr ""
+
+msgid "DeployTokens|Allows read-only access to the registry images"
+msgstr ""
+
+msgid "DeployTokens|Allows read-only access to the repository"
+msgstr ""
+
+msgid "DeployTokens|Copy deploy token to clipboard"
+msgstr ""
+
+msgid "DeployTokens|Copy username to clipboard"
+msgstr ""
+
+msgid "DeployTokens|Create deploy token"
+msgstr ""
+
+msgid "DeployTokens|Created"
+msgstr ""
+
+msgid "DeployTokens|Deploy Tokens"
+msgstr ""
+
+msgid "DeployTokens|Deploy tokens allow read-only access to your repository and registry images."
+msgstr ""
+
+msgid "DeployTokens|Expires"
+msgstr ""
+
+msgid "DeployTokens|Name"
+msgstr ""
+
+msgid "DeployTokens|Pick a name for the application, and we'll give you a unique deploy token."
+msgstr ""
+
+msgid "DeployTokens|Revoke"
+msgstr ""
+
+msgid "DeployTokens|Revoke %{name}"
+msgstr ""
+
+msgid "DeployTokens|Scopes"
+msgstr ""
+
+msgid "DeployTokens|This action cannot be undone."
+msgstr ""
+
+msgid "DeployTokens|This project has no active Deploy Tokens."
+msgstr ""
+
+msgid "DeployTokens|Use this token as a password. Make sure you save it - you won't be able to access it again."
+msgstr ""
+
+msgid "DeployTokens|Use this username as a login."
+msgstr ""
+
+msgid "DeployTokens|Username"
+msgstr ""
+
+msgid "DeployTokens|You are about to revoke"
+msgstr ""
+
+msgid "DeployTokens|Your New Deploy Token"
+msgstr ""
-msgid "Description templates allow you to define context-specific templates for issue and merge request description fields for your project."
+msgid "DeployTokens|Your new project deploy token has been created."
msgstr ""
+msgid "Deprioritize label"
+msgstr ""
+
+msgid "Description"
+msgstr "Priskribo"
+
msgid "Details"
msgstr ""
msgid "Diffs|No file name available"
msgstr ""
+msgid "Diffs|Something went wrong while fetching diff lines."
+msgstr ""
+
msgid "Directory name"
msgstr "Nomo de dosierujo"
msgid "Disable"
msgstr ""
-msgid "Discard draft"
+msgid "Disable for this project"
msgstr ""
-msgid "Discover GitLab Geo."
+msgid "Disable group Runners"
msgstr ""
-msgid "Dismiss Cycle Analytics introduction box"
+msgid "Discard changes"
msgstr ""
-msgid "Dismiss Merge Request promotion"
+msgid "Discard draft"
+msgstr ""
+
+msgid "Dismiss Cycle Analytics introduction box"
msgstr ""
-msgid "Documentation for popular identity providers"
+msgid "Domain"
msgstr ""
msgid "Don't show again"
@@ -1675,31 +2022,34 @@ msgstr ""
msgid "Due date"
msgstr ""
-msgid "During this process, you’ll be asked for URLs from GitLab’s side. Use the URLs shown below."
+msgid "Each Runner can be in one of the following states:"
msgstr ""
msgid "Edit"
msgstr "Redakti"
+msgid "Edit Label"
+msgstr ""
+
msgid "Edit Pipeline Schedule %{id}"
msgstr "Redakti ĉenstablan planon %{id}"
msgid "Edit files in the editor and commit changes here"
msgstr ""
-msgid "Editing"
+msgid "Edit identity for %{user_name}"
msgstr ""
-msgid "Elasticsearch"
+msgid "Email"
msgstr ""
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "Email patch"
msgstr ""
-msgid "Email"
+msgid "Emails"
msgstr ""
-msgid "Emails"
+msgid "Embed"
msgstr ""
msgid "Enable"
@@ -1708,9 +2058,6 @@ msgstr ""
msgid "Enable Auto DevOps"
msgstr ""
-msgid "Enable SAML authentication for this group"
-msgstr ""
-
msgid "Enable Sentry for error reporting and logging."
msgstr ""
@@ -1720,7 +2067,13 @@ msgstr ""
msgid "Enable and configure Prometheus metrics."
msgstr ""
-msgid "Enable classification control using an external service"
+msgid "Enable for this project"
+msgstr ""
+
+msgid "Enable group Runners"
+msgstr ""
+
+msgid "Enable or disable certain group features and choose access levels."
msgstr ""
msgid "Enable or disable version check and usage ping."
@@ -1732,7 +2085,10 @@ msgstr ""
msgid "Enable the Performance Bar for a given group."
msgstr ""
-msgid "Enabled"
+msgid "Ends at (UTC)"
+msgstr ""
+
+msgid "Environments"
msgstr ""
msgid "Environments|An error occurred while fetching the environments."
@@ -1783,43 +2139,40 @@ msgstr ""
msgid "Environments|You don't have any environments right now."
msgstr ""
-msgid "Epic will be removed! Are you sure?"
-msgstr ""
-
-msgid "Epics"
+msgid "Error Reporting and Logging"
msgstr ""
-msgid "Epics Roadmap"
+msgid "Error committing changes. Please try again."
msgstr ""
-msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
+msgid "Error fetching contributors data."
msgstr ""
-msgid "Error Reporting and Logging"
+msgid "Error fetching job trace"
msgstr ""
-msgid "Error checking branch data. Please try again."
+msgid "Error fetching labels."
msgstr ""
-msgid "Error committing changes. Please try again."
+msgid "Error fetching network graph."
msgstr ""
-msgid "Error creating epic"
+msgid "Error fetching refs"
msgstr ""
-msgid "Error fetching contributors data."
+msgid "Error fetching usage ping data."
msgstr ""
-msgid "Error fetching labels."
+msgid "Error loading branch data. Please try again."
msgstr ""
-msgid "Error fetching network graph."
+msgid "Error loading last commit."
msgstr ""
-msgid "Error fetching refs"
+msgid "Error loading merge requests."
msgstr ""
-msgid "Error fetching usage ping data."
+msgid "Error loading project data. Please try again."
msgstr ""
msgid "Error occurred when toggling the notification subscription"
@@ -1834,6 +2187,9 @@ msgstr ""
msgid "Error updating todo status."
msgstr ""
+msgid "Estimated"
+msgstr ""
+
msgid "EventFilterBy|Filter by all"
msgstr ""
@@ -1864,31 +2220,16 @@ msgstr "Ĉiusemajne (en dimanĉo, je 4:00)"
msgid "Expand"
msgstr ""
-msgid "Explore projects"
-msgstr ""
-
-msgid "Explore public groups"
-msgstr ""
-
-msgid "External Classification Policy Authorization"
+msgid "Expand all"
msgstr ""
-msgid "External authentication"
+msgid "Expand sidebar"
msgstr ""
-msgid "External authorization denied access to this project"
-msgstr ""
-
-msgid "External authorization request timeout"
-msgstr ""
-
-msgid "ExternalAuthorizationService|Classification Label"
-msgstr ""
-
-msgid "ExternalAuthorizationService|Classification label"
+msgid "Explore projects"
msgstr ""
-msgid "ExternalAuthorizationService|When no classification label is set the default label `%{default_label}` will be used."
+msgid "Explore public groups"
msgstr ""
msgid "Failed"
@@ -1900,6 +2241,9 @@ msgstr ""
msgid "Failed to change the owner"
msgstr "Ne eblas ÅanÄi la posedanton"
+msgid "Failed to check related branches."
+msgstr ""
+
msgid "Failed to remove issue from board, please try again."
msgstr ""
@@ -1909,6 +2253,12 @@ msgstr "Ne eblas forigi la ĉenstablan planon"
msgid "Failed to update issues, please try again."
msgstr ""
+msgid "Failure"
+msgstr ""
+
+msgid "Faster as it re-uses the project workspace (falling back to clone if it doesn't exist)"
+msgstr ""
+
msgid "Feb"
msgstr ""
@@ -1918,18 +2268,12 @@ msgstr ""
msgid "Fields on this page are now uneditable, you can configure"
msgstr ""
-msgid "File name"
-msgstr ""
-
msgid "Files"
msgstr "Dosieroj"
msgid "Files (%{human_size})"
msgstr ""
-msgid "Fill in the fields below, turn on <strong>%{enable_label}</strong>, and press <strong>%{save_changes}</strong>"
-msgstr ""
-
msgid "Filter by commit message"
msgstr "Filtri per mesaÄo"
@@ -1948,10 +2292,13 @@ msgstr "Unue"
msgid "FirstPushedBy|pushed by"
msgstr "alpuÅita de"
-msgid "Font Color"
+msgid "For internal projects, any logged in user can view pipelines and access job details (output logs and artifacts)"
msgstr ""
-msgid "Footer message"
+msgid "For private projects, any member (guest or higher) can view pipelines and access job details (output logs and artifacts)"
+msgstr ""
+
+msgid "For public projects, anyone can view pipelines and access job details (output logs and artifacts)"
msgstr ""
msgid "Fork"
@@ -1971,6 +2318,9 @@ msgstr ""
msgid "Format"
msgstr ""
+msgid "Found errors in your .gitlab-ci.yml:"
+msgstr ""
+
msgid "From %{provider_title}"
msgstr ""
@@ -1986,166 +2336,13 @@ msgstr ""
msgid "GPG Keys"
msgstr ""
-msgid "Generate a default set of labels"
-msgstr ""
-
-msgid "Geo Nodes"
-msgstr ""
-
-msgid "Geo allows you to replicate your GitLab instance to other geographical locations."
-msgstr ""
-
-msgid "GeoNodeSyncStatus|Node is failing or broken."
-msgstr ""
-
-msgid "GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage."
-msgstr ""
-
-msgid "GeoNodes|Checksummed"
-msgstr ""
-
-msgid "GeoNodes|Database replication lag:"
-msgstr ""
-
-msgid "GeoNodes|Disabling a node stops the sync process. Are you sure?"
-msgstr ""
-
-msgid "GeoNodes|Does not match the primary storage configuration"
-msgstr ""
-
-msgid "GeoNodes|Failed"
-msgstr ""
-
-msgid "GeoNodes|Full"
-msgstr ""
-
-msgid "GeoNodes|GitLab version does not match the primary node version"
-msgstr ""
-
-msgid "GeoNodes|GitLab version:"
-msgstr ""
-
-msgid "GeoNodes|Health status:"
-msgstr ""
-
-msgid "GeoNodes|Last event ID processed by cursor:"
-msgstr ""
-
-msgid "GeoNodes|Last event ID seen from primary:"
-msgstr ""
-
-msgid "GeoNodes|Loading nodes"
-msgstr ""
-
-msgid "GeoNodes|Local Attachments:"
-msgstr ""
-
-msgid "GeoNodes|Local LFS objects:"
-msgstr ""
-
-msgid "GeoNodes|Local job artifacts:"
-msgstr ""
-
-msgid "GeoNodes|New node"
-msgstr ""
-
-msgid "GeoNodes|Node Authentication was successfully repaired."
-msgstr ""
-
-msgid "GeoNodes|Node was successfully removed."
-msgstr ""
-
-msgid "GeoNodes|Not checksummed"
-msgstr ""
-
-msgid "GeoNodes|Out of sync"
-msgstr ""
-
-msgid "GeoNodes|Removing a node stops the sync process. Are you sure?"
-msgstr ""
-
-msgid "GeoNodes|Replication slot WAL:"
-msgstr ""
-
-msgid "GeoNodes|Replication slots:"
-msgstr ""
-
-msgid "GeoNodes|Repositories checksummed:"
+msgid "General"
msgstr ""
-msgid "GeoNodes|Repositories:"
+msgid "General pipelines"
msgstr ""
-msgid "GeoNodes|Repository checksums verified:"
-msgstr ""
-
-msgid "GeoNodes|Selective"
-msgstr ""
-
-msgid "GeoNodes|Something went wrong while changing node status"
-msgstr ""
-
-msgid "GeoNodes|Something went wrong while removing node"
-msgstr ""
-
-msgid "GeoNodes|Something went wrong while repairing node"
-msgstr ""
-
-msgid "GeoNodes|Storage config:"
-msgstr ""
-
-msgid "GeoNodes|Sync settings:"
-msgstr ""
-
-msgid "GeoNodes|Synced"
-msgstr ""
-
-msgid "GeoNodes|Unused slots"
-msgstr ""
-
-msgid "GeoNodes|Unverified"
-msgstr ""
-
-msgid "GeoNodes|Used slots"
-msgstr ""
-
-msgid "GeoNodes|Verified"
-msgstr ""
-
-msgid "GeoNodes|Wiki checksums verified:"
-msgstr ""
-
-msgid "GeoNodes|Wikis checksummed:"
-msgstr ""
-
-msgid "GeoNodes|Wikis:"
-msgstr ""
-
-msgid "GeoNodes|You have configured Geo nodes using an insecure HTTP connection. We recommend the use of HTTPS."
-msgstr ""
-
-msgid "Geo|All projects"
-msgstr ""
-
-msgid "Geo|File sync capacity"
-msgstr ""
-
-msgid "Geo|Groups to synchronize"
-msgstr ""
-
-msgid "Geo|Projects in certain groups"
-msgstr ""
-
-msgid "Geo|Projects in certain storage shards"
-msgstr ""
-
-msgid "Geo|Repository sync capacity"
-msgstr ""
-
-msgid "Geo|Select groups to replicate."
-msgstr ""
-
-msgid "Geo|Shards to synchronize"
+msgid "Generate a default set of labels"
msgstr ""
msgid "Git repository URL"
@@ -2157,6 +2354,9 @@ msgstr ""
msgid "Git storage health information has been reset"
msgstr ""
+msgid "Git strategy for pipelines"
+msgstr ""
+
msgid "Git version"
msgstr ""
@@ -2166,21 +2366,24 @@ msgstr ""
msgid "GitLab CI Linter has been moved"
msgstr ""
-msgid "GitLab Geo"
+msgid "GitLab Group Runners can execute code for all the projects in this group."
msgstr ""
msgid "GitLab Runner section"
msgstr ""
-msgid "GitLab single sign on URL"
-msgstr ""
-
msgid "Gitaly"
msgstr ""
msgid "Gitaly Servers"
msgstr ""
+msgid "Gitaly|Address"
+msgstr ""
+
+msgid "Go Back"
+msgstr ""
+
msgid "Go back"
msgstr ""
@@ -2196,22 +2399,19 @@ msgstr ""
msgid "Got it!"
msgstr ""
-msgid "GroupRoadmap|Epics let you manage your portfolio of projects more efficiently and with less effort"
-msgstr ""
-
-msgid "GroupRoadmap|From %{dateWord}"
+msgid "Graph"
msgstr ""
-msgid "GroupRoadmap|Loading roadmap"
+msgid "Group CI/CD settings"
msgstr ""
-msgid "GroupRoadmap|Something went wrong while fetching epics"
+msgid "Group ID"
msgstr ""
-msgid "GroupRoadmap|To view the roadmap, add a planned start or finish date to one of your epics in this group or its subgroups. Only epics in the past 3 months and the next 3 months are shown &ndash; from %{startDate} to %{endDate}."
+msgid "Group Runners"
msgstr ""
-msgid "GroupRoadmap|Until %{dateWord}"
+msgid "Group maintainers can register group runners in the %{link}"
msgstr ""
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
@@ -2238,6 +2438,9 @@ msgstr ""
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
+msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
+msgstr ""
+
msgid "GroupsEmptyState|A group is a collection of several projects."
msgstr ""
@@ -2277,12 +2480,6 @@ msgstr ""
msgid "GroupsTree|Sorry, no groups or projects matched your search"
msgstr ""
-msgid "Have your users email"
-msgstr ""
-
-msgid "Header message"
-msgstr ""
-
msgid "Health Check"
msgstr ""
@@ -2315,19 +2512,49 @@ msgid_plural "Hide values"
msgstr[0] ""
msgstr[1] ""
+msgid "Hide whitespace changes"
+msgstr ""
+
msgid "History"
msgstr ""
msgid "Housekeeping successfully started"
msgstr "La refreÅigo komenciÄis sukcese"
-msgid "Identity provider single sign on URL"
+msgid "I accept the %{terms_link}"
+msgstr ""
+
+msgid "I accept the|Terms of Service and Privacy Policy"
+msgstr ""
+
+msgid "ID"
+msgstr ""
+
+msgid "IDE|Commit"
+msgstr ""
+
+msgid "IDE|Edit"
+msgstr ""
+
+msgid "IDE|Go back"
+msgstr ""
+
+msgid "IDE|Open in file view"
+msgstr ""
+
+msgid "IDE|Review"
+msgstr ""
+
+msgid "Identifier"
+msgstr ""
+
+msgid "Identities"
msgstr ""
-msgid "If enabled, access to projects will be validated on an external service using their classification label."
+msgid "If disabled, the access level will depend on the user's permissions in the project."
msgstr ""
-msgid "If using GitHub, you’ll see pipeline statuses on GitHub for your commits and pull requests. %{more_info_link}"
+msgid "If enabled"
msgstr ""
msgid "If you already have files you can push them using the %{link_to_cli} below."
@@ -2336,6 +2563,15 @@ msgstr ""
msgid "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>."
msgstr ""
+msgid "ImageDiffViewer|2-up"
+msgstr ""
+
+msgid "ImageDiffViewer|Onion skin"
+msgstr ""
+
+msgid "ImageDiffViewer|Swipe"
+msgstr ""
+
msgid "Import"
msgstr ""
@@ -2351,16 +2587,10 @@ msgstr ""
msgid "Import repository"
msgstr "Enporti deponejon"
-msgid "ImportButtons|Connect repositories from"
+msgid "Include a Terms of Service agreement and Privacy Policy that all users must accept."
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."
+msgid "Inline"
msgstr ""
msgid "Install Runner on Kubernetes"
@@ -2369,17 +2599,15 @@ msgstr ""
msgid "Install a Runner compatible with GitLab CI"
msgstr ""
-msgid "Instance"
-msgid_plural "Instances"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "Instance does not support multiple Kubernetes clusters"
msgstr ""
msgid "Integrations"
msgstr ""
+msgid "Integrations Settings"
+msgstr ""
+
msgid "Interested parties can even contribute by pushing commits if they want to."
msgstr ""
@@ -2395,7 +2623,7 @@ msgstr "Intervala Åablono"
msgid "Introducing Cycle Analytics"
msgstr "Ni prezentas al vi la ciklan analizon"
-msgid "Issue board focus mode"
+msgid "Issue Board"
msgstr ""
msgid "Issue events"
@@ -2404,9 +2632,6 @@ msgstr ""
msgid "IssueBoards|Board"
msgstr ""
-msgid "IssueBoards|Boards"
-msgstr ""
-
msgid "Issues"
msgstr ""
@@ -2419,6 +2644,12 @@ msgstr ""
msgid "January"
msgstr ""
+msgid "Job"
+msgstr ""
+
+msgid "Job has been erased"
+msgstr ""
+
msgid "Jobs"
msgstr ""
@@ -2461,6 +2692,9 @@ msgstr ""
msgid "Kubernetes service integration has been deprecated. %{deprecated_message_content} your Kubernetes clusters using the new <a href=\"%{url}\"/>Kubernetes Clusters</a> page"
msgstr ""
+msgid "LFS"
+msgstr ""
+
msgid "LFSStatus|Disabled"
msgstr "MalÅaltita"
@@ -2470,6 +2704,9 @@ msgstr "Åœaltita"
msgid "Label"
msgstr ""
+msgid "Label actions dropdown"
+msgstr ""
+
msgid "LabelSelect|%{firstLabelName} +%{remainingLabelCount} more"
msgstr ""
@@ -2485,6 +2722,9 @@ msgstr ""
msgid "Labels can be applied to issues and merge requests to categorize them."
msgstr ""
+msgid "Labels can be applied to issues and merge requests."
+msgstr ""
+
msgid "Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>"
msgstr ""
@@ -2520,6 +2760,9 @@ msgstr ""
msgid "LastPushEvent|at"
msgstr ""
+msgid "Latest changes"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -2544,9 +2787,6 @@ msgstr "Forlasi la grupon"
msgid "Leave project"
msgstr "Forlasi la projekton"
-msgid "License"
-msgstr ""
-
msgid "List"
msgstr ""
@@ -2556,6 +2796,9 @@ msgstr ""
msgid "Loading the GitLab IDE..."
msgstr ""
+msgid "Loading..."
+msgstr ""
+
msgid "Lock"
msgstr ""
@@ -2565,21 +2808,18 @@ msgstr ""
msgid "Lock not found"
msgstr ""
-msgid "Locked"
+msgid "Lock to current projects"
msgstr ""
-msgid "Locked Files"
+msgid "Locked"
msgstr ""
-msgid "Locks give the ability to lock specific file or folder."
+msgid "Locked to current projects"
msgstr ""
msgid "Login"
msgstr ""
-msgid "Make everyone on your team more productive regardless of their location. GitLab Geo creates read-only mirrors of your GitLab instance so you can reduce the time it takes to clone and fetch large repos."
-msgstr ""
-
msgid "Manage all notifications"
msgstr ""
@@ -2592,16 +2832,16 @@ msgstr ""
msgid "Manage project labels"
msgstr ""
-msgid "Manage your group’s membership while adding another level of security with SAML."
-msgstr ""
-
msgid "Mar"
msgstr ""
msgid "March"
msgstr ""
-msgid "Mark done"
+msgid "Mark todo as done"
+msgstr ""
+
+msgid "Markdown enabled"
msgstr ""
msgid "Maximum git storage failures"
@@ -2616,7 +2856,7 @@ msgstr "Mediano"
msgid "Members"
msgstr ""
-msgid "Members will be forwarded here when signing in to your group. Get this from your identity provider, where it can also be called \"SSO Service Location\", \"SAML Token Issuance Endpoint\", or \"SAML 2.0/W-Federation URL\"."
+msgid "Merge Request:"
msgstr ""
msgid "Merge Requests"
@@ -2628,91 +2868,46 @@ msgstr ""
msgid "Merge request"
msgstr ""
-msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
-msgstr ""
-
-msgid "Merged"
-msgstr ""
-
-msgid "Messages"
-msgstr ""
-
-msgid "Metrics - Influx"
-msgstr ""
-
-msgid "Metrics - Prometheus"
-msgstr ""
-
-msgid "Metrics|Business"
-msgstr ""
-
-msgid "Metrics|Create metric"
-msgstr ""
-
-msgid "Metrics|Edit metric"
-msgstr ""
-
-msgid "Metrics|For grouping similar metrics"
-msgstr ""
-
-msgid "Metrics|Label of the chart's vertical axis. Usually the type of the unit being charted. The horizontal axis (X-axis) always represents time."
-msgstr ""
-
-msgid "Metrics|Legend label (optional)"
-msgstr ""
-
-msgid "Metrics|Must be a valid PromQL query."
-msgstr ""
-
-msgid "Metrics|Name"
-msgstr ""
-
-msgid "Metrics|New metric"
-msgstr ""
-
-msgid "Metrics|Prometheus Query Documentation"
+msgid "Merge requests"
msgstr ""
-msgid "Metrics|Query"
-msgstr ""
-
-msgid "Metrics|Response"
+msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
msgstr ""
-msgid "Metrics|System"
+msgid "MergeRequests|Resolve this discussion in a new issue"
msgstr ""
-msgid "Metrics|Type"
+msgid "MergeRequests|Saving the comment failed"
msgstr ""
-msgid "Metrics|Unit label"
+msgid "MergeRequests|Toggle comments for this file"
msgstr ""
-msgid "Metrics|Used as a title for the chart"
+msgid "MergeRequests|Updating discussions failed"
msgstr ""
-msgid "Metrics|Used if the query returns a single series. If it returns multiple series, their legend labels will be picked up from the response."
+msgid "MergeRequests|View file @ %{commitId}"
msgstr ""
-msgid "Metrics|Y-axis label"
+msgid "MergeRequests|View replaced file @ %{commitId}"
msgstr ""
-msgid "Metrics|e.g. HTTP requests"
+msgid "Merged"
msgstr ""
-msgid "Metrics|e.g. Requests/second"
+msgid "Messages"
msgstr ""
-msgid "Metrics|e.g. Throughput"
+msgid "Metrics - Influx"
msgstr ""
-msgid "Metrics|e.g. rate(http_requests_total[5m])"
+msgid "Metrics - Prometheus"
msgstr ""
-msgid "Metrics|e.g. req/sec"
+msgid "Milestone"
msgstr ""
-msgid "Milestone"
+msgid "Milestones"
msgstr ""
msgid "Milestones|Delete milestone"
@@ -2733,9 +2928,6 @@ msgstr ""
msgid "Milestones|Promote Milestone"
msgstr ""
-msgid "Milestones|This action cannot be reversed."
-msgstr ""
-
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "aldonos SSH-Ålosilon"
@@ -2748,7 +2940,7 @@ msgstr ""
msgid "Monitoring"
msgstr ""
-msgid "More info"
+msgid "More actions"
msgstr ""
msgid "More information"
@@ -2763,12 +2955,30 @@ msgstr ""
msgid "Move issue"
msgstr ""
-msgid "Multiple issue boards"
+msgid "Name"
msgstr ""
msgid "Name new label"
msgstr ""
+msgid "Name your individual key via a title"
+msgstr ""
+
+msgid "Nav|Help"
+msgstr ""
+
+msgid "Nav|Home"
+msgstr ""
+
+msgid "Nav|Sign In / Register"
+msgstr ""
+
+msgid "Nav|Sign out and sign in with a different account"
+msgstr ""
+
+msgid "New Identity"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "Nova problemo"
@@ -2780,6 +2990,9 @@ msgstr ""
msgid "New Kubernetes cluster"
msgstr ""
+msgid "New Label"
+msgstr ""
+
msgid "New Pipeline Schedule"
msgstr "Nova ĉenstabla plano"
@@ -2792,15 +3005,15 @@ msgstr ""
msgid "New directory"
msgstr "Nova dosierujo"
-msgid "New epic"
-msgstr ""
-
msgid "New file"
msgstr "Nova dosiero"
msgid "New group"
msgstr ""
+msgid "New identity"
+msgstr ""
+
msgid "New issue"
msgstr "Nova problemo"
@@ -2810,6 +3023,9 @@ msgstr ""
msgid "New merge request"
msgstr "Nova peto pri kunfando"
+msgid "New pipelines will cancel older, pending pipelines on the same branch"
+msgstr ""
+
msgid "New project"
msgstr ""
@@ -2825,7 +3041,7 @@ msgstr ""
msgid "New tag"
msgstr "Nova etikedo"
-msgid "No Label"
+msgid "No"
msgstr ""
msgid "No assignee"
@@ -2846,7 +3062,16 @@ msgstr ""
msgid "No file chosen"
msgstr ""
-msgid "No labels created yet."
+msgid "No files found"
+msgstr ""
+
+msgid "No files found."
+msgstr ""
+
+msgid "No merge requests found"
+msgstr ""
+
+msgid "No messages were logged"
msgstr ""
msgid "No repository"
@@ -2879,15 +3104,9 @@ msgstr "Ne estas sufiĉe da datenoj"
msgid "Note that the master branch is automatically protected. %{link_to_protected_branches}"
msgstr ""
-msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow connecting repositories without generating a Personal Access Token."
-msgstr ""
-
msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
msgstr ""
-msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow connecting repositories without generating a Personal Access Token."
-msgstr ""
-
msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
msgstr ""
@@ -2963,9 +3182,6 @@ msgstr ""
msgid "Number of access attempts"
msgstr ""
-msgid "OK"
-msgstr ""
-
msgid "Oct"
msgstr ""
@@ -2975,19 +3191,16 @@ msgstr ""
msgid "OfSearchInADropdown|Filter"
msgstr "Filtrilo"
-msgid "Once imported, repositories can be mirrored over SSH. Read more %{ssh_link}"
-msgstr ""
-
msgid "Online IDE integration settings."
msgstr ""
-msgid "Only project members can comment."
+msgid "Only comments from the following commit are shown below"
msgstr ""
-msgid "Open"
+msgid "Only project members can comment."
msgstr ""
-msgid "Opened"
+msgid "Open in Xcode"
msgstr ""
msgid "OpenedNDaysAgo|Opened"
@@ -2996,9 +3209,18 @@ msgstr "Malfermita"
msgid "Opens in a new window"
msgstr ""
+msgid "Operations"
+msgstr ""
+
msgid "Options"
msgstr "Opcioj"
+msgid "Or you can choose one of the suggested colors below"
+msgstr ""
+
+msgid "Other Labels"
+msgstr ""
+
msgid "Otherwise it is recommended you start with one of the options below."
msgstr ""
@@ -3032,12 +3254,27 @@ msgstr ""
msgid "Password"
msgstr ""
+msgid "Paste your public SSH key, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'. Don't use your private SSH key."
+msgstr ""
+
+msgid "Pause"
+msgstr ""
+
msgid "Pending"
msgstr ""
+msgid "Per job. If a job passes this threshold, it will be marked as failed"
+msgstr ""
+
+msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgstr ""
+
msgid "Performance optimization"
msgstr ""
+msgid "Permissions"
+msgstr ""
+
msgid "Personal Access Token"
msgstr ""
@@ -3053,7 +3290,7 @@ msgstr "Ĉenstabla plano"
msgid "Pipeline Schedules"
msgstr "Ĉenstablaj planoj"
-msgid "Pipeline quota"
+msgid "Pipeline triggers"
msgstr ""
msgid "PipelineCharts|Failed:"
@@ -3152,10 +3389,22 @@ msgstr ""
msgid "Pipelines|This project is not currently set up to run pipelines."
msgstr ""
-msgid "Pipeline|Retry pipeline"
+msgid "Pipeline|Create for"
+msgstr ""
+
+msgid "Pipeline|Create pipeline"
+msgstr ""
+
+msgid "Pipeline|Existing branch name or tag"
msgstr ""
-msgid "Pipeline|Retry pipeline #%{pipelineId}?"
+msgid "Pipeline|Run Pipeline"
+msgstr ""
+
+msgid "Pipeline|Search branches"
+msgstr ""
+
+msgid "Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default."
msgstr ""
msgid "Pipeline|Stop pipeline"
@@ -3164,7 +3413,7 @@ msgstr ""
msgid "Pipeline|Stop pipeline #%{pipelineId}?"
msgstr ""
-msgid "Pipeline|You’re about to retry pipeline %{pipelineId}."
+msgid "Pipeline|Variables"
msgstr ""
msgid "Pipeline|You’re about to stop pipeline %{pipelineId}."
@@ -3182,19 +3431,25 @@ msgstr "kun etapo"
msgid "Pipeline|with stages"
msgstr "kun etapoj"
+msgid "Plain diff"
+msgstr ""
+
msgid "PlantUML"
msgstr ""
msgid "Play"
msgstr ""
-msgid "Please <a href=%{link_to_billing} target=\"_blank\" rel=\"noopener noreferrer\">enable billing for one of your projects to be able to create a Kubernetes cluster</a>, then try again."
+msgid "Please accept the Terms of Service before continuing."
+msgstr ""
+
+msgid "Please select at least one filter to see results"
msgstr ""
msgid "Please solve the reCAPTCHA"
msgstr ""
-msgid "Please wait while we connect to your repository. Refresh at will."
+msgid "Please try again"
msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
@@ -3203,7 +3458,19 @@ msgstr ""
msgid "Preferences"
msgstr ""
-msgid "Primary"
+msgid "Preferences|Navigation theme"
+msgstr ""
+
+msgid "Prioritize"
+msgstr ""
+
+msgid "Prioritize label"
+msgstr ""
+
+msgid "Prioritized Labels"
+msgstr ""
+
+msgid "Prioritized label"
msgstr ""
msgid "Private - Project access must be granted explicitly to each user."
@@ -3221,6 +3488,12 @@ msgstr ""
msgid "Profiles|Account scheduled for removal."
msgstr ""
+msgid "Profiles|Change username"
+msgstr ""
+
+msgid "Profiles|Current path: %{path}"
+msgstr ""
+
msgid "Profiles|Delete Account"
msgstr ""
@@ -3239,9 +3512,21 @@ msgstr ""
msgid "Profiles|Invalid username"
msgstr ""
+msgid "Profiles|Path"
+msgstr ""
+
msgid "Profiles|Type your %{confirmationValue} to confirm:"
msgstr ""
+msgid "Profiles|Update username"
+msgstr ""
+
+msgid "Profiles|Username change failed - %{message}"
+msgstr ""
+
+msgid "Profiles|Username successfully changed"
+msgstr ""
+
msgid "Profiles|You don't have access to delete this user."
msgstr ""
@@ -3260,6 +3545,12 @@ msgstr ""
msgid "Programming languages used in this repository"
msgstr ""
+msgid "Progress"
+msgstr ""
+
+msgid "Project"
+msgstr ""
+
msgid "Project '%{project_name}' is in the process of being deleted."
msgstr ""
@@ -3272,6 +3563,9 @@ 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 Badges"
+msgstr ""
+
msgid "Project access must be granted explicitly to each user."
msgstr "Ĉiu uzanto devas akiri propran atingon al la projekto."
@@ -3299,30 +3593,6 @@ msgstr "La elporto de la projekto komenciÄis. Vi ricevos ligilon per retpoÅto
msgid "ProjectActivityRSS|Subscribe"
msgstr ""
-msgid "ProjectCreationLevel|Allowed to create projects"
-msgstr ""
-
-msgid "ProjectCreationLevel|Default project creation protection"
-msgstr ""
-
-msgid "ProjectCreationLevel|Developers + Masters"
-msgstr ""
-
-msgid "ProjectCreationLevel|Masters"
-msgstr ""
-
-msgid "ProjectCreationLevel|No one"
-msgstr ""
-
-msgid "ProjectFeature|Disabled"
-msgstr "MalÅaltita"
-
-msgid "ProjectFeature|Everyone with access"
-msgstr "Ĉiu, kiu havas atingon"
-
-msgid "ProjectFeature|Only team members"
-msgstr "Nur skipanoj"
-
msgid "ProjectFileTree|Name"
msgstr "Nomo"
@@ -3332,27 +3602,6 @@ msgstr "Neniam"
msgid "ProjectLifecycle|Stage"
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 ""
@@ -3377,6 +3626,9 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr ""
+msgid "PrometheusDashboard|Time"
+msgstr ""
+
msgid "PrometheusService|%{exporters} with %{metrics} were found"
msgstr ""
@@ -3398,18 +3650,9 @@ msgstr ""
msgid "PrometheusService|Common metrics"
msgstr ""
-msgid "PrometheusService|Common metrics are automatically monitored based on a library of metrics from popular exporters."
-msgstr ""
-
-msgid "PrometheusService|Custom metrics"
-msgstr ""
-
msgid "PrometheusService|Finding and configuring metrics..."
msgstr ""
-msgid "PrometheusService|Finding custom metrics..."
-msgstr ""
-
msgid "PrometheusService|Install Prometheus on clusters"
msgstr ""
@@ -3422,13 +3665,13 @@ msgstr ""
msgid "PrometheusService|Metrics"
msgstr ""
-msgid "PrometheusService|Missing environment variable"
+msgid "PrometheusService|Metrics are automatically configured and monitored based on a library of metrics from popular exporters."
msgstr ""
-msgid "PrometheusService|More information"
+msgid "PrometheusService|Missing environment variable"
msgstr ""
-msgid "PrometheusService|New metric"
+msgid "PrometheusService|More information"
msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
@@ -3437,9 +3680,6 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
-msgstr ""
-
msgid "PrometheusService|Time-series monitoring service"
msgstr ""
@@ -3455,22 +3695,28 @@ msgstr ""
msgid "Promote"
msgstr ""
-msgid "Promote to Group Label"
+msgid "Promote these project milestones into a group milestone."
msgstr ""
msgid "Promote to Group Milestone"
msgstr ""
+msgid "Promote to group label"
+msgstr ""
+
msgid "Protip:"
msgstr ""
+msgid "Provider"
+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"
+msgid "Public pipelines"
msgstr ""
msgid "Push events"
@@ -3482,10 +3728,10 @@ msgstr ""
msgid "Push to create a project"
msgstr ""
-msgid "PushRule|Committer restriction"
+msgid "Quick actions can be used in the issues description and comment boxes."
msgstr ""
-msgid "Quick actions can be used in the issues description and comment boxes."
+msgid "Re-deploy"
msgstr ""
msgid "Read more"
@@ -3497,18 +3743,18 @@ msgstr "LeguMin"
msgid "Real-time features"
msgstr ""
-msgid "RefSwitcher|Branches"
-msgstr "Branĉoj"
-
-msgid "RefSwitcher|Tags"
-msgstr "Etikedoj"
-
msgid "Reference:"
msgstr ""
msgid "Register / Sign In"
msgstr ""
+msgid "Register and see your runners for this group."
+msgstr ""
+
+msgid "Register and see your runners for this project."
+msgstr ""
+
msgid "Registry"
msgstr ""
@@ -3539,28 +3785,28 @@ msgstr "Rememorigu denove"
msgid "Remove"
msgstr ""
-msgid "Remove avatar"
+msgid "Remove Runner"
msgstr ""
-msgid "Remove project"
-msgstr "Forigi la projekton"
-
-msgid "Repair authentication"
+msgid "Remove avatar"
msgstr ""
-msgid "Repo by URL"
+msgid "Remove priority"
msgstr ""
+msgid "Remove project"
+msgstr "Forigi la projekton"
+
msgid "Repository"
msgstr ""
-msgid "Repository has no locks."
+msgid "Repository Settings"
msgstr ""
msgid "Repository maintenance"
msgstr ""
-msgid "Repository mirror settings"
+msgid "Repository mirror"
msgstr ""
msgid "Repository storage"
@@ -3569,6 +3815,9 @@ msgstr ""
msgid "Request Access"
msgstr "Peti atingeblon"
+msgid "Require all users to accept Terms of Service and Privacy Policy when they access GitLab."
+msgstr ""
+
msgid "Reset git storage health information"
msgstr ""
@@ -3578,10 +3827,25 @@ msgstr ""
msgid "Reset runners registration token"
msgstr ""
+msgid "Resolve all discussions in new issue"
+msgstr ""
+
+msgid "Resolve conflicts on source branch"
+msgstr ""
+
msgid "Resolve discussion"
msgstr ""
-msgid "Response"
+msgid "Resume"
+msgstr ""
+
+msgid "Retry"
+msgstr ""
+
+msgid "Retry this job"
+msgstr ""
+
+msgid "Retry verification"
msgstr ""
msgid "Reveal value"
@@ -3595,7 +3859,7 @@ msgstr "Malfari ĉi tiun enmetadon"
msgid "Revert this merge request"
msgstr "Malfari ĉi tiun peton pri kunfando"
-msgid "Review the process for configuring service providers in your identity provider — in this case, GitLab is the \"service provider\" or \"relying party\"."
+msgid "Review"
msgstr ""
msgid "Reviewing"
@@ -3604,28 +3868,31 @@ msgstr ""
msgid "Reviewing (merge request !%{mergeRequestId})"
msgstr ""
-msgid "Roadmap"
+msgid "Rollback"
msgstr ""
-msgid "Run CI/CD pipelines for external repositories"
+msgid "Runner token"
msgstr ""
msgid "Runners"
msgstr ""
-msgid "Running"
+msgid "Runners API"
msgstr ""
-msgid "SAML Single Sign On"
+msgid "Runners can be placed on separate users, servers, and even on your local machine."
msgstr ""
-msgid "SAML Single Sign On Settings"
+msgid "Running"
msgstr ""
-msgid "SHA1 fingerprint of the SAML token signing certificate. Get this from your identity provider, where it can also be called \"Thumbprint\"."
+msgid "SSH Keys"
msgstr ""
-msgid "SSH Keys"
+msgid "SSL Verification"
+msgstr ""
+
+msgid "Save"
msgstr ""
msgid "Save changes"
@@ -3649,15 +3916,30 @@ msgstr ""
msgid "Scheduling Pipelines"
msgstr "Planado de la ĉenstabloj"
-msgid "Scoped issue boards"
+msgid "Scroll to bottom"
+msgstr ""
+
+msgid "Scroll to top"
msgstr ""
msgid "Search"
msgstr ""
+msgid "Search branches"
+msgstr ""
+
msgid "Search branches and tags"
msgstr "Serĉu branĉon aŭ etikedon"
+msgid "Search files"
+msgstr ""
+
+msgid "Search for projects, issues, etc."
+msgstr ""
+
+msgid "Search merge requests"
+msgstr ""
+
msgid "Search milestones"
msgstr ""
@@ -3673,15 +3955,15 @@ msgstr ""
msgid "Seconds to wait for a storage access attempt"
msgstr ""
-msgid "Secret variables"
-msgstr ""
-
-msgid "Security report"
+msgid "Select"
msgstr ""
msgid "Select Archive Format"
msgstr "Elektu formaton de arkivo"
+msgid "Select a namespace to fork the project"
+msgstr ""
+
msgid "Select a timezone"
msgstr "Elektu horzonon"
@@ -3694,12 +3976,21 @@ msgstr ""
msgid "Select branch/tag"
msgstr ""
-msgid "Select target branch"
-msgstr "Elektu celan branĉon"
+msgid "Select project"
+msgstr ""
+
+msgid "Select project and zone to choose machine type"
+msgstr ""
-msgid "Selective synchronization"
+msgid "Select project to choose zone"
msgstr ""
+msgid "Select source branch"
+msgstr ""
+
+msgid "Select target branch"
+msgstr "Elektu celan branĉon"
+
msgid "Send email"
msgstr ""
@@ -3715,9 +4006,6 @@ msgstr ""
msgid "Service Templates"
msgstr ""
-msgid "Service URL"
-msgstr ""
-
msgid "Session expiration, projects limit and attachment size."
msgstr ""
@@ -3742,9 +4030,6 @@ msgstr ""
msgid "Set up Koding"
msgstr "Agordi „Koding“"
-msgid "Set up assertions/attributes/claims (email, first_name, last_name) and NameID according to %{docsLinkStart}the documentation %{icon}%{docsLinkEnd}"
-msgstr ""
-
msgid "SetPasswordToCloneLink|set a password"
msgstr "kreos pasvorton"
@@ -3754,19 +4039,22 @@ msgstr ""
msgid "Setup a specific Runner automatically"
msgstr ""
-msgid "Share the <strong>%{sso_label}</strong> with members so they can sign in to your group through your identity provider"
+msgid "Share"
msgstr ""
-msgid "SharedRunnersMinutesSettings|By resetting the pipeline minutes for this namespace, the currently used minutes will be set to zero."
+msgid "Shared Runners"
msgstr ""
-msgid "SharedRunnersMinutesSettings|Reset pipeline minutes"
+msgid "Show command"
msgstr ""
-msgid "SharedRunnersMinutesSettings|Reset used pipeline minutes"
+msgid "Show complete raw log"
msgstr ""
-msgid "Show command"
+msgid "Show latest version"
+msgstr ""
+
+msgid "Show latest version of the diff"
msgstr ""
msgid "Show parent pages"
@@ -3775,21 +4063,18 @@ msgstr ""
msgid "Show parent subgroups"
msgstr ""
+msgid "Show whitespace changes"
+msgstr ""
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "Estas montrata %d evento"
msgstr[1] "Estas montrataj %d eventoj"
-msgid "Sidebar|Change weight"
-msgstr ""
-
-msgid "Sidebar|No"
-msgstr ""
-
-msgid "Sidebar|None"
+msgid "Side-by-side"
msgstr ""
-msgid "Sidebar|Weight"
+msgid "Sign out"
msgstr ""
msgid "Sign-in restrictions"
@@ -3801,7 +4086,7 @@ msgstr ""
msgid "Size and domain settings for static websites"
msgstr ""
-msgid "Slack application"
+msgid "Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job"
msgstr ""
msgid "Snippets"
@@ -3813,13 +4098,13 @@ msgstr ""
msgid "Something went wrong on our end."
msgstr ""
-msgid "Something went wrong when toggling the button"
+msgid "Something went wrong on our end. Please try again!"
msgstr ""
-msgid "Something went wrong while fetching Dependency Scanning."
+msgid "Something went wrong when toggling the button"
msgstr ""
-msgid "Something went wrong while fetching SAST."
+msgid "Something went wrong while closing the %{issuable}. Please try again later"
msgstr ""
msgid "Something went wrong while fetching the projects."
@@ -3828,6 +4113,12 @@ msgstr ""
msgid "Something went wrong while fetching the registry list."
msgstr ""
+msgid "Something went wrong while reopening the %{issuable}. Please try again later"
+msgstr ""
+
+msgid "Something went wrong while resolving this discussion. Please try again."
+msgstr ""
+
msgid "Something went wrong. Please try again."
msgstr ""
@@ -3873,9 +4164,6 @@ msgstr ""
msgid "SortOptions|Least popular"
msgstr ""
-msgid "SortOptions|Less weight"
-msgstr ""
-
msgid "SortOptions|Milestone"
msgstr ""
@@ -3885,9 +4173,6 @@ msgstr ""
msgid "SortOptions|Milestone due soon"
msgstr ""
-msgid "SortOptions|More weight"
-msgstr ""
-
msgid "SortOptions|Most popular"
msgstr ""
@@ -3927,9 +4212,6 @@ msgstr ""
msgid "SortOptions|Start soon"
msgstr ""
-msgid "SortOptions|Weight"
-msgstr ""
-
msgid "Source"
msgstr ""
@@ -3948,9 +4230,36 @@ msgstr ""
msgid "Spam and Anti-bot Protection"
msgstr ""
+msgid "Specific Runners"
+msgstr ""
+
msgid "Specify the following URL during the Runner setup:"
msgstr ""
+msgid "Squash commits"
+msgstr ""
+
+msgid "Stage"
+msgstr ""
+
+msgid "Stage & Commit"
+msgstr ""
+
+msgid "Stage all changes"
+msgstr ""
+
+msgid "Stage changes"
+msgstr ""
+
+msgid "Staged"
+msgstr ""
+
+msgid "Staged %{type}"
+msgstr ""
+
+msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
+msgstr ""
+
msgid "StarProject|Star"
msgstr "Steligi"
@@ -3972,12 +4281,15 @@ msgstr ""
msgid "Started"
msgstr ""
-msgid "State your message to activate"
+msgid "Starts at (UTC)"
msgstr ""
msgid "Status"
msgstr ""
+msgid "Stop this environment"
+msgstr ""
+
msgid "Stopped"
msgstr ""
@@ -3987,16 +4299,19 @@ msgstr ""
msgid "Subgroups"
msgstr ""
-msgid "Switch branch/tag"
-msgstr "Iri al branĉo/etikedo"
+msgid "Subscribe"
+msgstr ""
-msgid "System"
+msgid "Subscribe at group level"
msgstr ""
-msgid "System Hooks"
+msgid "Subscribe at project level"
msgstr ""
-msgid "System header and footer:"
+msgid "Switch branch/tag"
+msgstr "Iri al branĉo/etikedo"
+
+msgid "System Hooks"
msgstr ""
msgid "Tag (%{tag_count})"
@@ -4007,6 +4322,9 @@ msgstr[1] ""
msgid "Tags"
msgstr "Etikedoj"
+msgid "Tags:"
+msgstr ""
+
msgid "TagsPage|Browse commits"
msgstr ""
@@ -4070,7 +4388,7 @@ msgstr ""
msgid "TagsPage|Use git tag command to add a new one:"
msgstr ""
-msgid "TagsPage|Write your release notes or drag files here..."
+msgid "TagsPage|Write your release notes or drag files here…"
msgstr ""
msgid "TagsPage|protected"
@@ -4085,19 +4403,19 @@ msgstr ""
msgid "Team"
msgstr ""
-msgid "Thanks! Don't show me this again"
+msgid "Terms of Service Agreement and Privacy Policy"
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."
+msgid "Terms of Service and Privacy Policy"
msgstr ""
-msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project"
+msgid "Test coverage parsing"
msgstr ""
-msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
+msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project"
msgstr ""
-msgid "The X509 Certificate to use when mutual TLS is required to communicate with the external authorization service. If left blank, the server certificate is still validated when accessing over HTTPS."
+msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
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."
@@ -4106,9 +4424,6 @@ msgstr "La etapo de programado montras la tempon de la unua enmetado Äis la kre
msgid "The collection of events added to the data gathered for that stage."
msgstr "La aro da eventoj, kiuj estas aldonitaj al la datenoj kolektitaj por la etapo."
-msgid "The connection will time out after %{timeout}. For repositories that take longer, use a clone/push combination."
-msgstr ""
-
msgid "The fork relationship has been removed."
msgstr "La rilato de disbranĉigo estis forigita."
@@ -4127,7 +4442,7 @@ 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 passphrase required to decrypt the private key. This is optional and the value is encrypted at rest."
+msgid "The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>"
msgstr ""
msgid "The phase of the development lifecycle."
@@ -4136,9 +4451,6 @@ msgstr "La etapo de la disvolva ciklo."
msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit."
msgstr "La etapo de la plano montras la tempon de la antaÅ­a Åtupo Äis la alpuÅado de via unua enmetado. Ĉi tiu tempo aldoniÄos aÅ­tomate post kiam vi alpuÅas la unuan enmetadon."
-msgid "The private key to use when a client certificate is provided. This value is encrypted at rest."
-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 "La etapo de eldonado montras la tutan tempon de la kreado de problemo Äis la disponigado en la publika versio. La datenoj aldoniÄos aÅ­tomate post kiam vi kompletigos plenan ciklon de ideo Äis realaĵo."
@@ -4160,7 +4472,7 @@ msgstr ""
msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request."
msgstr "La etapo de la kontrolo montras la tempon de la kreado de la peto pri kunfando Äis Äia aplikado. La datenoj aldoniÄos aÅ­tomate post kiam vi aplikos la unuan peton pri kunfando."
-msgid "The roadmap shows the progress of your epics along a timeline"
+msgid "The secure token used by the Runner to checkout the project"
msgstr ""
msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time."
@@ -4187,13 +4499,19 @@ msgstr "La valoro, kiu troviÄas en la mezo de aro da rigardataj valoroj. Ekzemp
msgid "There are no issues to show"
msgstr ""
+msgid "There are no labels yet"
+msgstr ""
+
msgid "There are no merge requests to show"
msgstr ""
msgid "There are problems accessing Git storage: "
msgstr ""
-msgid "There was an error loading results"
+msgid "There was an error loading jobs"
+msgstr ""
+
+msgid "There was an error loading latest pipeline"
msgstr ""
msgid "There was an error loading users activity calendar."
@@ -4214,12 +4532,21 @@ msgstr ""
msgid "There was an error when unsubscribing from this label."
msgstr ""
-msgid "This board\\'s scope is reduced"
+msgid "They can be managed using the %{link}."
+msgstr ""
+
+msgid "This GitLab instance does not provide any shared Runners yet. Instance administrators can register shared Runners in the admin area."
+msgstr ""
+
+msgid "This diff is collapsed."
msgstr ""
msgid "This directory"
msgstr ""
+msgid "This group does not provide any group Runners yet."
+msgstr ""
+
msgid "This is a confidential issue."
msgstr ""
@@ -4241,6 +4568,15 @@ msgstr ""
msgid "This job depends on upstream jobs that need to succeed in order for this job to be triggered"
msgstr ""
+msgid "This job does not have a trace."
+msgstr ""
+
+msgid "This job has been canceled"
+msgstr ""
+
+msgid "This job has been skipped"
+msgstr ""
+
msgid "This job has not been triggered yet"
msgstr ""
@@ -4259,19 +4595,28 @@ msgstr "Ĉi tiu signifas, ke vi ne povos alpuÅi kodon, antaÅ­ ol vi kreos malpl
msgid "This merge request is locked."
msgstr ""
+msgid "This option is disabled while you still have unstaged changes"
+msgstr ""
+
msgid "This page is unavailable because you are not allowed to read information across multiple projects."
msgstr ""
+msgid "This page will be removed in a future release."
+msgstr ""
+
msgid "This project"
msgstr ""
+msgid "This project does not belong to a group and can therefore not make use of group Runners."
+msgstr ""
+
msgid "This repository"
msgstr ""
-msgid "This will delete the custom metric, Are you sure?"
+msgid "This source diff could not be displayed because it is too large."
msgstr ""
-msgid "Those emails automatically become issues (with the comments becoming the email conversation) listed here."
+msgid "This user has no identities"
msgstr ""
msgid "Time before an issue gets scheduled"
@@ -4283,10 +4628,10 @@ msgstr "Tempo antaÅ­ la komenco de laboro super problemo"
msgid "Time between merge request creation and merge/close"
msgstr "Tempo inter la kreado de poeto pri kunfando kaj Äia aplikado/fermado"
-msgid "Time between updates and capacity settings."
+msgid "Time remaining"
msgstr ""
-msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
+msgid "Time spent"
msgstr ""
msgid "Time tracking"
@@ -4310,6 +4655,9 @@ msgstr "antaÅ­ %s tagoj"
msgid "Timeago|%s days remaining"
msgstr "restas %s tagoj"
+msgid "Timeago|%s hours ago"
+msgstr ""
+
msgid "Timeago|%s hours remaining"
msgstr "restas %s horoj"
@@ -4325,6 +4673,9 @@ msgstr "antaÅ­ %s monatoj"
msgid "Timeago|%s months remaining"
msgstr "restas %s monatoj"
+msgid "Timeago|%s seconds ago"
+msgstr ""
+
msgid "Timeago|%s seconds remaining"
msgstr "restas %s sekundoj"
@@ -4340,48 +4691,45 @@ msgstr "antaÅ­ %s jaroj"
msgid "Timeago|%s years remaining"
msgstr "restas %s jaroj"
+msgid "Timeago|1 day ago"
+msgstr ""
+
msgid "Timeago|1 day remaining"
msgstr "restas 1 tago"
+msgid "Timeago|1 hour ago"
+msgstr ""
+
msgid "Timeago|1 hour remaining"
msgstr "restas 1 horo"
+msgid "Timeago|1 minute ago"
+msgstr ""
+
msgid "Timeago|1 minute remaining"
msgstr "restas 1 minuto"
+msgid "Timeago|1 month ago"
+msgstr ""
+
msgid "Timeago|1 month remaining"
msgstr "restas 1 monato"
+msgid "Timeago|1 week ago"
+msgstr ""
+
msgid "Timeago|1 week remaining"
msgstr "restas 1 semajno"
+msgid "Timeago|1 year ago"
+msgstr ""
+
msgid "Timeago|1 year remaining"
msgstr "restas 1 jaro"
msgid "Timeago|Past due"
msgstr "MalfruiÄis"
-msgid "Timeago|a day ago"
-msgstr "antaÅ­ unu tago"
-
-msgid "Timeago|a month ago"
-msgstr "antaÅ­ unu monato"
-
-msgid "Timeago|a week ago"
-msgstr "antaÅ­ unu semajno"
-
-msgid "Timeago|a year ago"
-msgstr "antaÅ­ unu jaro"
-
-msgid "Timeago|about %s hours ago"
-msgstr "antaŭ ĉirkaŭ %s horoj"
-
-msgid "Timeago|about a minute ago"
-msgstr "antaŭ ĉirkaŭ unu minuto"
-
-msgid "Timeago|about an hour ago"
-msgstr "antaŭ ĉirkaŭ unu horo"
-
msgid "Timeago|in %s days"
msgstr "post %s tagoj"
@@ -4421,11 +4769,14 @@ msgstr "post 1 semajno"
msgid "Timeago|in 1 year"
msgstr "post 1 jaro"
-msgid "Timeago|in a while"
+msgid "Timeago|just now"
+msgstr ""
+
+msgid "Timeago|right now"
msgstr ""
-msgid "Timeago|less than a minute ago"
-msgstr "antaÅ­ malpli ol minuto"
+msgid "Timeout"
+msgstr ""
msgid "Time|hr"
msgid_plural "Time|hrs"
@@ -4443,19 +4794,10 @@ msgstr "s"
msgid "Tip:"
msgstr ""
-msgid "Title"
-msgstr ""
-
msgid "To GitLab"
msgstr ""
-msgid "To connect GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to connect."
-msgstr ""
-
-msgid "To connect GitHub repositories, you first need to authorize GitLab to access the list of your GitHub repositories:"
-msgstr ""
-
-msgid "To connect an SVN repository, check out %{svn_link}."
+msgid "To add an SSH key you need to %{generate_link_start}generate one%{link_end} or use an %{existing_link_start}existing key%{link_end}."
msgstr ""
msgid "To import GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
@@ -4467,19 +4809,19 @@ msgstr ""
msgid "To import an SVN repository, check out %{svn_link}."
msgstr ""
-msgid "To only use CI/CD features for an external repository, choose <strong>CI/CD for external repo</strong>."
+msgid "To start serving your jobs you can add Runners to your group"
msgstr ""
-msgid "To set up SAML authentication for your group through an identity provider like Azure, Okta, Onelogin, Ping Identity, or your custom SAML 2.0 provider:"
+msgid "To validate your GitLab CI configurations, go to 'CI/CD → Pipelines' inside your project, and click on the 'CI Lint' button."
msgstr ""
-msgid "To validate your GitLab CI configurations, go to 'CI/CD → Pipelines' inside your project, and click on the 'CI Lint' button."
+msgid "Todo"
msgstr ""
-msgid "To view the roadmap, add a planned start or finish date to one of your epics in this group or its subgroups. Only epics in the past 3 months and the next 3 months are shown."
+msgid "Toggle Sidebar"
msgstr ""
-msgid "Todo"
+msgid "Toggle discussion"
msgstr ""
msgid "Toggle sidebar"
@@ -4491,6 +4833,9 @@ msgstr ""
msgid "ToggleButton|Toggle Status: ON"
msgstr ""
+msgid "Too many changes to show."
+msgstr ""
+
msgid "Total Time"
msgstr "Totala tempo"
@@ -4500,22 +4845,19 @@ msgstr "Totala tempo por la testado de ĉiuj enmetadoj/kunfandoj"
msgid "Total: %{total}"
msgstr ""
-msgid "Track activity with Contribution Analytics."
-msgstr ""
-
-msgid "Track groups of issues that share a theme, across projects and milestones"
-msgstr ""
-
msgid "Track time with quick actions"
msgstr ""
msgid "Trigger this manual action"
msgstr ""
-msgid "Turn on Service Desk"
+msgid "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."
+msgstr ""
+
+msgid "Try again"
msgstr ""
-msgid "Unknown"
+msgid "Unable to load the diff. %{button_try_again}"
msgstr ""
msgid "Unlock"
@@ -4527,25 +4869,45 @@ msgstr ""
msgid "Unresolve discussion"
msgstr ""
+msgid "Unstage all changes"
+msgstr ""
+
+msgid "Unstage changes"
+msgstr ""
+
+msgid "Unstaged"
+msgstr ""
+
+msgid "Unstaged %{type}"
+msgstr ""
+
+msgid "Unstaged and staged %{type}"
+msgstr ""
+
msgid "Unstar"
msgstr "Malsteligi"
-msgid "Up to date"
+msgid "Unsubscribe"
msgstr ""
-msgid "Upgrade your plan to activate Advanced Global Search."
+msgid "Unsubscribe at group level"
msgstr ""
-msgid "Upgrade your plan to activate Contribution Analytics."
+msgid "Unsubscribe at project level"
msgstr ""
-msgid "Upgrade your plan to activate Group Webhooks."
+msgid "Unverified"
msgstr ""
-msgid "Upgrade your plan to activate Issue weight."
+msgid "Up to date"
msgstr ""
-msgid "Upgrade your plan to improve Issue boards."
+msgid "Update %{files}"
+msgid_plural "Update %{files} files"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Update your group name, description, avatar, and other general settings."
msgstr ""
msgid "Upload New File"
@@ -4566,7 +4928,7 @@ msgstr ""
msgid "Usage statistics"
msgstr ""
-msgid "Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab"
+msgid "Use group milestones to manage issues from multiple projects in the same milestone."
msgstr ""
msgid "Use the following registration token during setup:"
@@ -4575,10 +4937,13 @@ msgstr ""
msgid "Use your global notification setting"
msgstr "Uzi vian Äeneralan agordon pri la sciigoj"
-msgid "Used by members to sign in to your group in GitLab"
+msgid "User and IP Rate Limits"
msgstr ""
-msgid "User and IP Rate Limits"
+msgid "Users"
+msgstr ""
+
+msgid "Variables"
msgstr ""
msgid "Variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. You can use variables for passwords, secret keys, or whatever you want."
@@ -4593,10 +4958,7 @@ msgstr ""
msgid "Various settings that affect GitLab performance."
msgstr ""
-msgid "View and edit lines"
-msgstr ""
-
-msgid "View epics list"
+msgid "Verified"
msgstr ""
msgid "View file @ "
@@ -4605,9 +4967,15 @@ msgstr ""
msgid "View group labels"
msgstr ""
+msgid "View jobs"
+msgstr ""
+
msgid "View labels"
msgstr ""
+msgid "View log"
+msgstr ""
+
msgid "View open merge request"
msgstr "Vidi la malfermitan peton pri kunfando"
@@ -4635,9 +5003,6 @@ msgstr "Nekonata"
msgid "Want to see the data? Please ask an administrator for access."
msgstr "Ĉu vi volas vidi la datenojn? Bonvolu peti atingeblon de administranto."
-msgid "We could not verify that one of your projects on GCP has billing enabled. Please try again."
-msgstr ""
-
msgid "We don't have enough data to show this stage."
msgstr "Ne estas sufiĉe da datenoj por montri ĉi tiun etapon."
@@ -4650,13 +5015,10 @@ msgstr ""
msgid "Web terminal"
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."
+msgid "When a runner is locked, it cannot be assigned to other projects"
msgstr ""
-msgid "Weight"
-msgstr ""
-
-msgid "When leaving the URL blank, classification labels can still be specified whitout disabling cross project features or performing external authorization checks."
+msgid "When enabled, users cannot use GitLab until the terms have been accepted."
msgstr ""
msgid "Wiki"
@@ -4683,7 +5045,31 @@ msgstr ""
msgid "WikiEdit|There is already a page with the same title in that path."
msgstr ""
-msgid "WikiEmptyPageError|You are not allowed to create wiki pages"
+msgid "WikiEmptyIssueMessage|Suggest wiki improvement"
+msgstr ""
+
+msgid "WikiEmptyIssueMessage|You must be a project member in order to add wiki pages. If you have suggestions for how to improve the wiki for this project, consider opening an issue in the %{issues_link}."
+msgstr ""
+
+msgid "WikiEmptyIssueMessage|issue tracker"
+msgstr ""
+
+msgid "WikiEmpty|A wiki is where you can store all the details about your project. This can include why you've created it, its principles, how to use it, and so on."
+msgstr ""
+
+msgid "WikiEmpty|Create your first page"
+msgstr ""
+
+msgid "WikiEmpty|Suggest wiki improvement"
+msgstr ""
+
+msgid "WikiEmpty|The wiki lets you write documentation for your project"
+msgstr ""
+
+msgid "WikiEmpty|This project has no wiki pages"
+msgstr ""
+
+msgid "WikiEmpty|You must be a project member in order to add wiki pages."
msgstr ""
msgid "WikiHistoricalPage|This is an old version of this page."
@@ -4719,6 +5105,12 @@ msgstr ""
msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?"
msgstr ""
+msgid "WikiPageConfirmDelete|Delete page"
+msgstr ""
+
+msgid "WikiPageConfirmDelete|Delete page %{pageTitle}?"
+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 ""
@@ -4734,7 +5126,7 @@ msgstr ""
msgid "WikiPage|Page slug"
msgstr ""
-msgid "WikiPage|Write your content or drag files here..."
+msgid "WikiPage|Write your content or drag files here…"
msgstr ""
msgid "Wiki|Create Page"
@@ -4746,9 +5138,6 @@ msgstr ""
msgid "Wiki|Edit Page"
msgstr ""
-msgid "Wiki|Empty page"
-msgstr ""
-
msgid "Wiki|More Pages"
msgstr ""
@@ -4767,13 +5156,10 @@ 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"
-msgid "Write a commit message..."
+msgid "Yes"
msgstr ""
msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
@@ -4791,7 +5177,7 @@ msgstr ""
msgid "You are on a read-only GitLab instance."
msgstr ""
-msgid "You are on a secondary (read-only) Geo node. If you want to make any changes, you must visit the %{primary_node}."
+msgid "You can %{linkStart}view the blob%{linkEnd} instead."
msgstr ""
msgid "You can also create a project from the command line."
@@ -4800,6 +5186,9 @@ msgstr ""
msgid "You can also star a label to make it a priority label."
msgstr ""
+msgid "You can also test your .gitlab-ci.yml in the %{linkStart}Lint%{linkEnd}"
+msgstr ""
+
msgid "You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}"
msgstr ""
@@ -4812,30 +5201,33 @@ msgstr "Oni povas aldoni dosierojn nur kiam oni estas en branĉo"
msgid "You can only edit 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."
+msgid "You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}"
msgstr ""
msgid "You cannot write to this read-only GitLab instance."
msgstr ""
-msgid "You do not have the correct permissions to override the settings from the LDAP group sync."
+msgid "You do not have any assigned merge requests"
msgstr ""
msgid "You have no permissions"
msgstr ""
+msgid "You have not created any merge requests"
+msgstr ""
+
msgid "You have reached your project limit"
msgstr "Vi ne povas krei pliajn projektojn"
-msgid "You must have master access to force delete a lock"
+msgid "You must accept our Terms of Service and privacy policy in order to register an account"
+msgstr ""
+
+msgid "You must have maintainer access to force delete a lock"
msgstr ""
msgid "You must sign in to star a project"
msgstr "Oni devas ensaluti por steligi projekton"
-msgid "You need a different license to enable FileLocks feature"
-msgstr ""
-
msgid "You need permission."
msgstr "VI bezonas permeson."
@@ -4902,13 +5294,11 @@ msgstr "Via nomo"
msgid "Your projects"
msgstr ""
-msgid "among other things"
+msgid "ago"
msgstr ""
-msgid "and %d fixed vulnerability"
-msgid_plural "and %d fixed vulnerabilities"
-msgstr[0] ""
-msgstr[1] ""
+msgid "among other things"
+msgstr ""
msgid "assign yourself"
msgstr ""
@@ -4916,136 +5306,36 @@ msgstr ""
msgid "branch name"
msgstr ""
-msgid "by"
-msgstr ""
-
-msgid "ciReport|%{type} detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|%{type} detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Code quality"
-msgstr ""
-
-msgid "ciReport|DAST detected no alerts by analyzing the review app"
-msgstr ""
-
-msgid "ciReport|Dependency scanning"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Failed to load %{reportName} report"
-msgstr ""
-
-msgid "ciReport|Fixed:"
-msgstr ""
-
-msgid "ciReport|Instances"
-msgstr ""
-
-msgid "ciReport|Learn more about whitelisting"
-msgstr ""
-
-msgid "ciReport|Loading %{reportName} report"
-msgstr ""
-
-msgid "ciReport|No changes to code quality"
-msgstr ""
-
-msgid "ciReport|No changes to performance metrics"
-msgstr ""
-
-msgid "ciReport|Performance metrics"
-msgstr ""
-
-msgid "ciReport|SAST"
-msgstr ""
-
-msgid "ciReport|SAST detected"
-msgstr ""
-
-msgid "ciReport|SAST detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|SAST detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|SAST:container no vulnerabilities were found"
-msgstr ""
-
-msgid "ciReport|Security scanning"
-msgstr ""
-
-msgid "ciReport|Security scanning failed loading any results"
-msgstr ""
-
-msgid "ciReport|Show complete code vulnerabilities report"
-msgstr ""
-
-msgid "ciReport|Unapproved vulnerabilities (red) can be marked as approved. %{helpLink}"
-msgstr ""
-
-msgid "ciReport|no vulnerabilities"
-msgstr ""
-
msgid "command line instructions"
msgstr ""
msgid "connecting"
msgstr ""
-msgid "could not read private key, is the passphrase correct?"
-msgstr ""
-
msgid "day"
msgid_plural "days"
msgstr[0] "tago"
msgstr[1] "tagoj"
-msgid "detected %d fixed vulnerability"
-msgid_plural "detected %d fixed vulnerabilities"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "detected %d new vulnerability"
-msgid_plural "detected %d new vulnerabilities"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "detected no vulnerabilities"
+msgid "deploy token"
msgstr ""
-msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
+msgid "disabled"
msgstr ""
-msgid "here"
+msgid "enabled"
msgstr ""
-msgid "importing"
-msgstr ""
-
-msgid "in progress"
-msgstr ""
-
-msgid "is invalid because there is downstream lock"
+msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
msgstr ""
-msgid "is invalid because there is upstream lock"
+msgid "for this project"
msgstr ""
-msgid "is not a valid X509 certificate."
+msgid "importing"
msgstr ""
-msgid "locked by %{path_lock_user_name} %{created_at}"
+msgid "latest version"
msgstr ""
msgid "merge request"
@@ -5065,28 +5355,7 @@ msgstr ""
msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage is %{emphasisStart} unchanged %{emphasisEnd} at %{memoryFrom}MB"
msgstr ""
-msgid "mrWidget|Add approval"
-msgstr ""
-
-msgid "mrWidget|Allows edits from maintainers"
-msgstr ""
-
-msgid "mrWidget|An error occured while removing your approval."
-msgstr ""
-
-msgid "mrWidget|An error occured while retrieving approval data for this merge request."
-msgstr ""
-
-msgid "mrWidget|An error occured while submitting your approval."
-msgstr ""
-
-msgid "mrWidget|Approve"
-msgstr ""
-
-msgid "mrWidget|Approved"
-msgstr ""
-
-msgid "mrWidget|Approved by"
+msgid "mrWidget|Allows commits from members who can merge to the target branch"
msgstr ""
msgid "mrWidget|Cancel automatic merge"
@@ -5113,6 +5382,9 @@ msgstr ""
msgid "mrWidget|Closes"
msgstr ""
+msgid "mrWidget|Create an issue to resolve them later"
+msgstr ""
+
msgid "mrWidget|Deployment statistics are not available currently"
msgstr ""
@@ -5167,9 +5439,6 @@ msgstr ""
msgid "mrWidget|Remove source branch"
msgstr ""
-msgid "mrWidget|Remove your approval"
-msgstr ""
-
msgid "mrWidget|Request to merge"
msgstr ""
@@ -5209,6 +5478,9 @@ msgstr ""
msgid "mrWidget|There are merge conflicts"
msgstr ""
+msgid "mrWidget|There are unresolved discussions. Please resolve these discussions"
+msgstr ""
+
msgid "mrWidget|This merge request failed to be merged automatically"
msgstr ""
@@ -5259,7 +5531,7 @@ msgstr ""
msgid "personal access token"
msgstr ""
-msgid "private key does not match certificate."
+msgid "remaining"
msgstr ""
msgid "remove due date"
@@ -5274,9 +5546,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "to help your contributors communicate effectively!"
-msgstr ""
-
msgid "username"
msgstr ""
@@ -5286,3 +5555,8 @@ msgstr ""
msgid "with %{additions} additions, %{deletions} deletions."
msgstr ""
+msgid "within %d minute "
+msgid_plural "within %d minutes "
+msgstr[0] ""
+msgstr[1] ""
+
diff --git a/locale/es/gitlab.po b/locale/es/gitlab.po
index e2af97f1b83..1df233173a1 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: 2018-04-04 19:35+0200\n"
-"PO-Revision-Date: 2018-04-05 03:35-0400\n"
+"POT-Creation-Date: 2018-07-01 16:35+1000\n"
+"PO-Revision-Date: 2018-07-01 11:02\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Spanish\n"
"Language: es_ES\n"
@@ -16,8 +16,10 @@ msgstr ""
"X-Crowdin-Language: es-ES\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-msgid " and"
-msgstr ""
+msgid "%d changed file"
+msgid_plural "%d changed files"
+msgstr[0] ""
+msgstr[1] ""
msgid "%d commit"
msgid_plural "%d commits"
@@ -54,6 +56,16 @@ msgid_plural "%d metrics"
msgstr[0] ""
msgstr[1] ""
+msgid "%d staged change"
+msgid_plural "%d staged changes"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "%d unstaged change"
+msgid_plural "%d unstaged changes"
+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."
@@ -70,12 +82,21 @@ msgid_plural "%{count} participants"
msgstr[0] "%{count} participante"
msgstr[1] "%{count} participantes"
+msgid "%{filePath} deleted"
+msgstr ""
+
+msgid "%{group_docs_link_start}Groups%{group_docs_link_end} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects."
+msgstr ""
+
msgid "%{loadingIcon} Started"
msgstr ""
msgid "%{lock_path} is locked by GitLab User %{lock_user_id}"
msgstr ""
+msgid "%{nip_domain} can be used as an alternative to a custom domain."
+msgstr ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr "%{number_commits_behind} commits detrás de %{default_branch}, %{number_commits_ahead} commits por delante"
@@ -88,6 +109,9 @@ msgstr "%{number_of_failures} de %{maximum_failures} intentos fallidos. GitLab n
msgid "%{openOrClose} %{noteable}"
msgstr ""
+msgid "%{percent}%% complete"
+msgstr ""
+
msgid "%{storage_name}: failed storage access attempt on host:"
msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:"
msgstr[0] "%{storage_name}: intento de acceso fallido al almacenamiento en host:"
@@ -96,15 +120,62 @@ msgstr[1] "%{storage_name}: %{failed_attempts} intentos de acceso fallido al alm
msgid "%{text} is available"
msgstr "%{text} esta disponible"
+msgid "%{title} changes"
+msgstr ""
+
+msgid "%{unstaged} unstaged and %{staged} staged changes"
+msgstr ""
+
msgid "(checkout the %{link} for information on how to install it)."
msgstr "(para obtener información sobre cómo instalarlo visite %{link})."
msgid "+ %{moreCount} more"
msgstr "+ %{moreCount} más"
+msgid "- Runner is active and can process any new jobs"
+msgstr ""
+
+msgid "- Runner is paused and will not receive any new jobs"
+msgstr ""
+
msgid "- show less"
msgstr "- mostrar menos"
+msgid "1 %{type} addition"
+msgid_plural "%{count} %{type} additions"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 %{type} modification"
+msgid_plural "%{count} %{type} modifications"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 closed issue"
+msgid_plural "%d closed issues"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 closed merge request"
+msgid_plural "%d closed merge requests"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 merged merge request"
+msgid_plural "%d merged merge requests"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 open issue"
+msgid_plural "%d open issues"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 open merge request"
+msgid_plural "%d open merge requests"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] "1 pipeline"
@@ -116,9 +187,27 @@ msgstr "¡1ra contribución!"
msgid "2FA enabled"
msgstr ""
+msgid "403|Please contact your GitLab administrator to get the permission."
+msgstr ""
+
+msgid "403|You don't have the permission to access this page."
+msgstr ""
+
+msgid "404|Make sure the address is correct and the page hasn't moved."
+msgstr ""
+
+msgid "404|Page Not Found"
+msgstr ""
+
+msgid "404|Please contact your GitLab administrator if you think this is a mistake."
+msgstr ""
+
msgid "<strong>Removes</strong> source branch"
msgstr ""
+msgid "A 'Runner' is a process which runs a job. You can setup as many Runners as you need."
+msgstr ""
+
msgid "A collection of graphs regarding Continuous Integration"
msgstr "Una colección de gráficos sobre Integración Continua"
@@ -128,6 +217,9 @@ msgstr ""
msgid "A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}."
msgstr ""
+msgid "A regular expression that will be used to find the test coverage output in the job trace. Leave blank to disable"
+msgstr ""
+
msgid "A user with write access to the source branch selected this option"
msgstr ""
@@ -140,36 +232,39 @@ msgstr "Reportes de abuso"
msgid "Abuse reports"
msgstr ""
+msgid "Accept terms"
+msgstr ""
+
msgid "Access Tokens"
msgstr "Tokens de acceso"
msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again."
msgstr ""
+msgid "Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report."
+msgstr ""
+
msgid "Account"
msgstr "Cuenta"
-msgid "Account and limit settings"
+msgid "Account and limit"
msgstr ""
msgid "Active"
msgstr "Activo"
+msgid "Active Sessions"
+msgstr ""
+
msgid "Activity"
msgstr "Actividad"
-msgid "Add"
-msgstr "Añadir"
-
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 "Añadir Webhooks Grupales y Gitlab Enterprise Edition."
-
msgid "Add Kubernetes cluster"
msgstr ""
@@ -182,6 +277,9 @@ msgstr ""
msgid "Add new directory"
msgstr "Agregar nuevo directorio"
+msgid "Add reaction"
+msgstr ""
+
msgid "Add todo"
msgstr ""
@@ -237,7 +335,7 @@ msgid "AdminUsers|To confirm, type %{username}"
msgstr ""
msgid "Advanced"
-msgstr "Avanzado"
+msgstr ""
msgid "Advanced settings"
msgstr "Configuración avanzada"
@@ -251,7 +349,10 @@ msgstr ""
msgid "All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings."
msgstr ""
-msgid "Allow edits from maintainers."
+msgid "Allow commits from members who can merge to the target branch."
+msgstr ""
+
+msgid "Allow public access to pipelines and job details, including output logs and artifacts"
msgstr ""
msgid "Allow rendering of PlantUML diagrams in Asciidoc documents."
@@ -263,16 +364,28 @@ msgstr ""
msgid "Allows you to add and manage Kubernetes clusters."
msgstr ""
-msgid "Also called \"Issuer\" or \"Relying party trust identifier\""
+msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
msgstr ""
-msgid "Also called \"Relying party service URL\" or \"Reply URL\""
+msgid "An error occured creating the new branch."
msgstr ""
-msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to connect."
+msgid "An error occured whilst loading all the files."
msgstr ""
-msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
+msgid "An error occured whilst loading the file content."
+msgstr ""
+
+msgid "An error occured whilst loading the file."
+msgstr ""
+
+msgid "An error occured whilst loading the merge request changes."
+msgstr ""
+
+msgid "An error occured whilst loading the merge request version data."
+msgstr ""
+
+msgid "An error occured whilst loading the merge request."
msgstr ""
msgid "An error occurred previewing the blob"
@@ -281,13 +394,7 @@ msgstr "Ha ocurrido un error visualizando el blob"
msgid "An error occurred when toggling the notification subscription"
msgstr "Se produjo un error al activar/desactivar la suscripción de notificación"
-msgid "An error occurred when updating the issue weight"
-msgstr "Se produjo un error al actualizar el peso de la incidencia"
-
-msgid "An error occurred while adding approver"
-msgstr ""
-
-msgid "An error occurred while detecting host keys"
+msgid "An error occurred while dismissing the alert. Refresh the page and try again."
msgstr ""
msgid "An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again."
@@ -305,10 +412,7 @@ msgstr ""
msgid "An error occurred while getting projects"
msgstr ""
-msgid "An error occurred while importing project"
-msgstr ""
-
-msgid "An error occurred while initializing path locks"
+msgid "An error occurred while importing project: ${details}"
msgstr ""
msgid "An error occurred while loading commits"
@@ -326,9 +430,6 @@ msgstr ""
msgid "An error occurred while making the request."
msgstr ""
-msgid "An error occurred while removing approver"
-msgstr ""
-
msgid "An error occurred while rendering KaTeX"
msgstr ""
@@ -341,9 +442,6 @@ msgstr ""
msgid "An error occurred while retrieving diff"
msgstr ""
-msgid "An error occurred while saving LDAP override status. Please try again."
-msgstr ""
-
msgid "An error occurred while saving assignees"
msgstr ""
@@ -353,9 +451,6 @@ msgstr ""
msgid "An error occurred. Please try again."
msgstr "Se produjo un error. Por favor inténtelo de nuevo."
-msgid "Any Label"
-msgstr ""
-
msgid "Appearance"
msgstr "Apariencia"
@@ -368,28 +463,28 @@ msgstr ""
msgid "April"
msgstr "Abril"
-msgid "Archived project! Repository is read-only"
-msgstr "¡Proyecto archivado! El repositorio es de solo lectura"
+msgid "Archived project! Repository and other project resources are read-only"
+msgstr ""
msgid "Are you sure you want to delete this pipeline schedule?"
msgstr "¿Estás seguro que deseas eliminar esta programación del pipeline?"
+msgid "Are you sure you want to remove this identity?"
+msgstr ""
+
msgid "Are you sure you want to reset registration token?"
msgstr "¿Está seguro que desea reinicializar el token de registro?"
msgid "Are you sure you want to reset the health check token?"
msgstr "¿Está seguro que desea reinicializar el token de Verificación de Estado?"
-msgid "Are you sure you want to unlock %{path_lock_path}?"
-msgstr ""
-
msgid "Are you sure?"
msgstr "¿Estás seguro?"
msgid "Artifacts"
msgstr "Artefactos"
-msgid "Assertion consumer service URL"
+msgid "Ask your group maintainer to setup a group Runner."
msgstr ""
msgid "Assign custom color like #FF0000"
@@ -413,9 +508,15 @@ msgstr ""
msgid "Assigned to :name"
msgstr ""
+msgid "Assigned to me"
+msgstr ""
+
msgid "Assignee"
msgstr "Asignado a"
+msgid "Assignee(s)"
+msgstr ""
+
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "Adjunte un archivo arrastrando &amp; soltando o %{upload_link}"
@@ -449,8 +550,11 @@ msgstr ""
msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
msgstr "Tanto las Auto Review Apps como Auto Deploy necesitan un dominio para funcionar correctamente."
-msgid "AutoDevOps|Auto DevOps (Beta)"
-msgstr "Auto DevOps (Beta)"
+msgid "Auto-cancel redundant, pending pipelines"
+msgstr ""
+
+msgid "AutoDevOps|Auto DevOps"
+msgstr ""
msgid "AutoDevOps|Auto DevOps documentation"
msgstr "Documentación de Auto DevOps"
@@ -470,80 +574,110 @@ msgstr ""
msgid "AutoDevOps|add a Kubernetes cluster"
msgstr ""
-msgid "AutoDevOps|enable Auto DevOps (Beta)"
+msgid "AutoDevOps|enable Auto DevOps"
msgstr ""
msgid "Available"
msgstr "Disponible"
+msgid "Available group Runners : %{runners}"
+msgstr ""
+
+msgid "Available group Runners : %{runners}."
+msgstr ""
+
msgid "Avatar will be removed. Are you sure?"
msgstr ""
msgid "Average per day: %{average}"
msgstr "Promedio por día: %{average}"
-msgid "Background Color"
+msgid "Background color"
msgstr ""
msgid "Background jobs"
msgstr ""
-msgid "Begin with the selected commit"
-msgstr "Iniciar con el commit seleccionado"
+msgid "Badges"
+msgstr ""
+
+msgid "Badges|A new badge was added."
+msgstr ""
+
+msgid "Badges|Add badge"
+msgstr ""
+
+msgid "Badges|Adding the badge failed, please check the entered URLs and try again."
+msgstr ""
+
+msgid "Badges|Badge image URL"
+msgstr ""
+
+msgid "Badges|Badge image preview"
+msgstr ""
+
+msgid "Badges|Delete badge"
+msgstr ""
+
+msgid "Badges|Delete badge?"
+msgstr ""
+
+msgid "Badges|Deleting the badge failed, please try again."
+msgstr ""
-msgid "Billing"
-msgstr "Facturación"
+msgid "Badges|Group Badge"
+msgstr ""
-msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan."
-msgstr "%{group_name} está actualmente en el plan %{plan_link}."
+msgid "Badges|Link"
+msgstr ""
-msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available."
-msgstr "Aumentar o disminuir automáticamente las características de algunos Planes no está disponible actualmente."
+msgid "Badges|No badge image"
+msgstr ""
-msgid "BillingPlans|Current plan"
-msgstr "Plan actual"
+msgid "Badges|No image to preview"
+msgstr ""
-msgid "BillingPlans|Customer Support"
-msgstr "Atención al cliente"
+msgid "Badges|Project Badge"
+msgstr ""
-msgid "BillingPlans|Downgrade"
+msgid "Badges|Reload badge image"
msgstr ""
-msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
-msgstr "Obtenga más información sobre cada plan al leer nuestro %{faq_link}."
+msgid "Badges|Save changes"
+msgstr ""
-msgid "BillingPlans|Manage plan"
-msgstr "Gestionar plan"
+msgid "Badges|Saving the badge failed, please check the entered URLs and try again."
+msgstr ""
-msgid "BillingPlans|Please contact %{customer_support_link} in that case."
-msgstr "Por favor contacte a %{customer_support_link} en ese caso."
+msgid "Badges|The %{docsLinkStart}variables%{docsLinkEnd} GitLab supports: %{placeholders}"
+msgstr ""
-msgid "BillingPlans|See all %{plan_name} features"
-msgstr "Ver todas la funcionalidades del plan %{plan_name}"
+msgid "Badges|The badge was deleted."
+msgstr ""
-msgid "BillingPlans|This group uses the plan associated with its parent group."
+msgid "Badges|The badge was saved."
msgstr ""
-msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
-msgstr "Para gestionar el plan para este grupo, visite la sección de facturación de %{parent_billing_page_link}."
+msgid "Badges|This group has no badges"
+msgstr ""
-msgid "BillingPlans|Upgrade"
+msgid "Badges|This project has no badges"
msgstr ""
-msgid "BillingPlans|You are currently on the %{plan_link} plan."
-msgstr "Actualmente estás en el plan %{plan_link}."
+msgid "Badges|Your badges"
+msgstr ""
-msgid "BillingPlans|frequently asked questions"
-msgstr "preguntas frecuentes"
+msgid "Begin with the selected commit"
+msgstr "Iniciar con el commit seleccionado"
-msgid "BillingPlans|monthly"
-msgstr "mensual"
+msgid "Below are examples of regex for existing tools:"
+msgstr ""
-msgid "BillingPlans|paid annually at %{price_per_year}"
-msgstr "%{price_per_year} pagados anualmente"
+msgid "Boards"
+msgstr ""
-msgid "BillingPlans|per user"
-msgstr "por usuario"
+msgid "Branch %{branchName} was not found in this project's repository."
+msgstr ""
msgid "Branch (%{branch_count})"
msgid_plural "Branches (%{branch_count})"
@@ -622,7 +756,7 @@ msgstr ""
msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
msgstr ""
-msgid "Branches|Only a project master or owner can delete a protected branch"
+msgid "Branches|Only a project maintainer or owner can delete a protected branch"
msgstr ""
msgid "Branches|Overview"
@@ -658,9 +792,6 @@ msgstr ""
msgid "Branches|Stale branches"
msgstr ""
-msgid "Branches|The branch could not be updated automatically because it has diverged from its upstream counterpart."
-msgstr ""
-
msgid "Branches|The default branch cannot be deleted"
msgstr ""
@@ -673,15 +804,9 @@ msgstr ""
msgid "Branches|To confirm, type %{branch_name_confirmation}:"
msgstr ""
-msgid "Branches|To discard the local changes and overwrite the branch with the upstream version, delete it here and choose 'Update Now' above."
-msgstr ""
-
msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
msgstr ""
-msgid "Branches|diverged from upstream"
-msgstr ""
-
msgid "Branches|merged"
msgstr ""
@@ -703,42 +828,81 @@ msgstr "Examinar archivos"
msgid "Browse files"
msgstr "Examinar archivos"
-msgid "Business"
-msgstr ""
-
msgid "ByAuthor|by"
msgstr "por"
msgid "CI / CD"
msgstr "CI / CD"
-msgid "CI/CD"
+msgid "CI / CD Settings"
msgstr ""
msgid "CI/CD configuration"
msgstr "Configuración de CI/CD"
-msgid "CI/CD for external repo"
+msgid "CI/CD settings"
+msgstr ""
+
+msgid "CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery."
+msgstr ""
+
+msgid "CICD|Auto DevOps"
+msgstr ""
+
+msgid "CICD|Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration."
+msgstr ""
+
+msgid "CICD|Automatic deployment to staging, manual deployment to production"
+msgstr ""
+
+msgid "CICD|Continuous deployment to production"
+msgstr ""
+
+msgid "CICD|Deployment strategy"
+msgstr ""
+
+msgid "CICD|Deployment strategy needs a domain name to work correctly."
+msgstr ""
+
+msgid "CICD|Disable Auto DevOps"
+msgstr ""
+
+msgid "CICD|Enable Auto DevOps"
+msgstr ""
+
+msgid "CICD|Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific %{ci_file}."
+msgstr ""
+
+msgid "CICD|Instance default (%{state})"
msgstr ""
msgid "CICD|Jobs"
msgstr ""
+msgid "CICD|Learn more about Auto DevOps"
+msgstr ""
+
+msgid "CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project."
+msgstr ""
+
+msgid "CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages."
+msgstr ""
+
+msgid "Can't find HEAD commit for this branch"
+msgstr ""
+
msgid "Cancel"
msgstr "Cancelar"
-msgid "Cannot be merged automatically"
+msgid "Cancel this job"
msgstr ""
-msgid "Cannot modify managed Kubernetes cluster"
+msgid "Cannot be merged automatically"
msgstr ""
-msgid "Certificate fingerprint"
+msgid "Cannot modify managed Kubernetes cluster"
msgstr ""
-msgid "Change Weight"
-msgstr "Cambiar peso"
-
msgid "Change this value to influence how frequently the GitLab UI polls for updates."
msgstr ""
@@ -790,21 +954,18 @@ msgstr ""
msgid "Choose a branch/tag (e.g. %{master}) or enter a commit (e.g. %{sha}) to see what's changed or to create a merge request."
msgstr ""
-msgid "Choose file..."
-msgstr "Elegir archivo..."
-
-msgid "Choose which groups you wish to synchronize to this secondary node."
-msgstr "Elija qué grupos desea sincronizar a este nodo secundario."
+msgid "Choose any color."
+msgstr ""
-msgid "Choose which repositories you want to connect and run CI/CD pipelines."
+msgid "Choose between <code>clone</code> or <code>fetch</code> to get the recent application code"
msgstr ""
+msgid "Choose file..."
+msgstr "Elegir archivo..."
+
msgid "Choose which repositories you want to import."
msgstr ""
-msgid "Choose which shards you wish to synchronize to this secondary node."
-msgstr "Elija qué fragmentos desea sincronizar a este nodo secundario."
-
msgid "CiStatusLabel|canceled"
msgstr "cancelado"
@@ -874,21 +1035,12 @@ msgstr ""
msgid "CiVariable|All environments"
msgstr ""
-msgid "CiVariable|Create wildcard"
-msgstr ""
-
msgid "CiVariable|Error occured while saving variables"
msgstr ""
-msgid "CiVariable|New environment"
-msgstr ""
-
msgid "CiVariable|Protected"
msgstr ""
-msgid "CiVariable|Search environments"
-msgstr ""
-
msgid "CiVariable|Toggle protected"
msgstr ""
@@ -898,30 +1050,30 @@ msgstr ""
msgid "CircuitBreakerApiLink|circuitbreaker api"
msgstr ""
-msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
+msgid "Clear search input"
msgstr ""
-msgid "Click to expand text"
-msgstr "Haga clic para expandir el texto"
+msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
+msgstr ""
-msgid "Client authentication certificate"
+msgid "Click the <strong>Promote</strong> button in the top right corner to promote it to a group milestone."
msgstr ""
-msgid "Client authentication key"
+msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
msgstr ""
-msgid "Client authentication key password"
+msgid "Click to expand it."
msgstr ""
+msgid "Click to expand text"
+msgstr "Haga clic para expandir el texto"
+
msgid "Clone repository"
msgstr ""
msgid "Close"
msgstr "Cerrar"
-msgid "Closed"
-msgstr "Cerrado"
-
msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster"
msgstr ""
@@ -937,6 +1089,12 @@ msgstr ""
msgid "ClusterIntegration|Advanced options on this Kubernetes cluster's integration"
msgstr ""
+msgid "ClusterIntegration|An error occured while trying to fetch project zones: %{error}"
+msgstr ""
+
+msgid "ClusterIntegration|An error occured while trying to fetch your projects: %{error}"
+msgstr ""
+
msgid "ClusterIntegration|Applications"
msgstr ""
@@ -967,6 +1125,9 @@ msgstr "Copiar Certificado CA"
msgid "ClusterIntegration|Copy Ingress IP Address to clipboard"
msgstr ""
+msgid "ClusterIntegration|Copy Jupyter Hostname to clipboard"
+msgstr ""
+
msgid "ClusterIntegration|Copy Kubernetes cluster name"
msgstr ""
@@ -982,8 +1143,8 @@ msgstr ""
msgid "ClusterIntegration|Create a new Kubernetes cluster on Google Kubernetes Engine right from GitLab"
msgstr ""
-msgid "ClusterIntegration|Create on GKE"
-msgstr "Crear en GKE"
+msgid "ClusterIntegration|Create on Google Kubernetes Engine"
+msgstr ""
msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
msgstr "Ingrese los detalles para un cluster Kubernetes existente"
@@ -994,14 +1155,26 @@ msgstr ""
msgid "ClusterIntegration|Environment scope"
msgstr ""
+msgid "ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab's Google Kubernetes Engine Integration."
+msgstr ""
+
+msgid "ClusterIntegration|Fetching machine types"
+msgstr ""
+
+msgid "ClusterIntegration|Fetching projects"
+msgstr ""
+
+msgid "ClusterIntegration|Fetching zones"
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr "Integración GitLab"
msgid "ClusterIntegration|GitLab Runner"
msgstr "GitLab Runner"
-msgid "ClusterIntegration|Google Cloud Platform project ID"
-msgstr "ID del proyecto de Google Cloud Platform"
+msgid "ClusterIntegration|Google Cloud Platform project"
+msgstr ""
msgid "ClusterIntegration|Google Kubernetes Engine"
msgstr "Google Kubernetes Engine"
@@ -1012,9 +1185,6 @@ msgstr "Proyecto Google Kubernetes Engine"
msgid "ClusterIntegration|Helm Tiller"
msgstr "Helm Tiller"
-msgid "ClusterIntegration|In order to show the health of the cluster, we'll need to provision your cluster with Prometheus to collect the required data."
-msgstr ""
-
msgid "ClusterIntegration|Ingress"
msgstr ""
@@ -1024,9 +1194,6 @@ msgstr ""
msgid "ClusterIntegration|Install"
msgstr "Instalar"
-msgid "ClusterIntegration|Install Prometheus"
-msgstr ""
-
msgid "ClusterIntegration|Installed"
msgstr "Instalado"
@@ -1039,15 +1206,18 @@ msgstr ""
msgid "ClusterIntegration|Integration status"
msgstr "Estado de integración"
+msgid "ClusterIntegration|Jupyter Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|JupyterHub"
+msgstr ""
+
msgid "ClusterIntegration|Kubernetes cluster"
msgstr "cluster de Kubernetes"
msgid "ClusterIntegration|Kubernetes cluster details"
msgstr "Detalles del cluster de Kubernetes"
-msgid "ClusterIntegration|Kubernetes cluster health"
-msgstr ""
-
msgid "ClusterIntegration|Kubernetes cluster integration"
msgstr "Integración de cluster de Kubernetes"
@@ -1075,8 +1245,14 @@ msgstr ""
msgid "ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project"
msgstr ""
-msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
-msgstr "Conozca más sobre %{link_to_documentation}"
+msgid "ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}."
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{help_link_start}Kubernetes%{help_link_end}."
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}."
+msgstr ""
msgid "ClusterIntegration|Learn more about environments"
msgstr "Conozca más sobre los entornos"
@@ -1099,7 +1275,16 @@ msgstr "Administre su cluster de Kubernetes visitando %{link_gke}"
msgid "ClusterIntegration|More information"
msgstr ""
-msgid "ClusterIntegration|Multiple Kubernetes clusters are available in GitLab Enterprise Edition Premium and Ultimate"
+msgid "ClusterIntegration|No machine types matched your search"
+msgstr ""
+
+msgid "ClusterIntegration|No projects found"
+msgstr ""
+
+msgid "ClusterIntegration|No projects matched your search"
+msgstr ""
+
+msgid "ClusterIntegration|No zones matched your search"
msgstr ""
msgid "ClusterIntegration|Note:"
@@ -1114,9 +1299,6 @@ msgstr ""
msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
msgstr "Asegúrese de que su cuenta de Google cumpla con los siguientes requisitos:"
-msgid "ClusterIntegration|Project ID"
-msgstr "ID de Proyecto"
-
msgid "ClusterIntegration|Project namespace"
msgstr ""
@@ -1129,6 +1311,9 @@ msgstr ""
msgid "ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration."
msgstr ""
+msgid "ClusterIntegration|Redeem up to $500 in free credit for Google Cloud Platform"
+msgstr ""
+
msgid "ClusterIntegration|Remove Kubernetes cluster integration"
msgstr ""
@@ -1144,20 +1329,38 @@ msgstr "Falló la solicitud para iniciar la instalación"
msgid "ClusterIntegration|Save changes"
msgstr "Guardar cambios"
+msgid "ClusterIntegration|Search machine types"
+msgstr ""
+
+msgid "ClusterIntegration|Search projects"
+msgstr ""
+
+msgid "ClusterIntegration|Search zones"
+msgstr ""
+
msgid "ClusterIntegration|Security"
msgstr ""
msgid "ClusterIntegration|See and edit the details for your Kubernetes cluster"
msgstr ""
-msgid "ClusterIntegration|See machine types"
-msgstr "Ver tipos de máquina"
+msgid "ClusterIntegration|Select machine type"
+msgstr ""
-msgid "ClusterIntegration|See your projects"
-msgstr "Ver tus proyectos"
+msgid "ClusterIntegration|Select project"
+msgstr ""
+
+msgid "ClusterIntegration|Select project and zone to choose machine type"
+msgstr ""
+
+msgid "ClusterIntegration|Select project to choose zone"
+msgstr ""
+
+msgid "ClusterIntegration|Select zone"
+msgstr ""
-msgid "ClusterIntegration|See zones"
-msgstr "Ver zonas"
+msgid "ClusterIntegration|Select zone to choose machine type"
+msgstr ""
msgid "ClusterIntegration|Service token"
msgstr ""
@@ -1189,6 +1392,9 @@ msgstr ""
msgid "ClusterIntegration|Token"
msgstr "Token"
+msgid "ClusterIntegration|Validating project billing status"
+msgstr ""
+
msgid "ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
@@ -1219,13 +1425,19 @@ msgstr "cumple con los requisitos"
msgid "ClusterIntegration|properly configured"
msgstr ""
+msgid "ClusterIntegration|sign up"
+msgstr ""
+
msgid "Collapse"
msgstr "Contraer"
-msgid "Comment and resolve discussion"
+msgid "Collapse sidebar"
msgstr ""
-msgid "Comment and unresolve discussion"
+msgid "Comment & resolve discussion"
+msgstr ""
+
+msgid "Comment & unresolve discussion"
msgstr ""
msgid "Comments"
@@ -1292,6 +1504,9 @@ msgstr ""
msgid "Committed by"
msgstr "Enviado por"
+msgid "Commit…"
+msgstr ""
+
msgid "Compare"
msgstr "Comparar"
@@ -1340,6 +1555,9 @@ msgstr ""
msgid "Configure limits for web and API requests."
msgstr ""
+msgid "Configure push mirrors."
+msgstr ""
+
msgid "Configure storage path and circuit breaker settings."
msgstr ""
@@ -1349,18 +1567,9 @@ msgstr ""
msgid "Connect"
msgstr ""
-msgid "Connect all repositories"
-msgstr ""
-
msgid "Connect repositories from GitHub"
msgstr ""
-msgid "Connect your external repositories, and CI/CD pipelines will run for new commits. A GitLab project will be created with only CI/CD features enabled."
-msgstr ""
-
-msgid "Connecting..."
-msgstr ""
-
msgid "Container Registry"
msgstr ""
@@ -1406,9 +1615,18 @@ msgstr "Usar nombres de imagen diferentes"
msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images."
msgstr ""
+msgid "ContainerRegistry|You can also use a %{deploy_token} for read-only access to the registry images."
+msgstr ""
+
+msgid "Continue"
+msgstr ""
+
msgid "Continuous Integration and Deployment"
msgstr ""
+msgid "Contribute to GitLab"
+msgstr ""
+
msgid "Contribution"
msgstr ""
@@ -1430,15 +1648,6 @@ msgstr ""
msgid "ContributorsPage|Please wait a moment, this page will automatically refresh when ready."
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 "Copiar la clave pública SSH al portapapeles"
-
msgid "Copy URL to clipboard"
msgstr "Copiar URL al portapapeles"
@@ -1451,9 +1660,18 @@ msgstr ""
msgid "Copy commit SHA to clipboard"
msgstr "Copiar SHA del cambio al portapapeles"
+msgid "Copy file name to clipboard"
+msgstr ""
+
+msgid "Copy file path to clipboard"
+msgstr ""
+
msgid "Copy reference to clipboard"
msgstr ""
+msgid "Copy to clipboard"
+msgstr ""
+
msgid "Create"
msgstr "Crear"
@@ -1472,15 +1690,15 @@ msgstr "Crear un token de acceso personal en tu cuenta para actualizar o enviar
msgid "Create branch"
msgstr ""
+msgid "Create commit"
+msgstr ""
+
msgid "Create directory"
msgstr "Crear directorio"
msgid "Create empty repository"
msgstr ""
-msgid "Create epic"
-msgstr "Crear epic"
-
msgid "Create file"
msgstr "Crear archivo"
@@ -1523,23 +1741,26 @@ msgstr "Etiqueta"
msgid "CreateTokenToCloneLink|create a personal access token"
msgstr "crear un token de acceso personal"
-msgid "Creates a new branch from %{branchName}"
+msgid "Created"
msgstr ""
-msgid "Creates a new branch from %{branchName} and re-directs to create a new merge request"
+msgid "Created by me"
msgstr ""
-msgid "Creating epic"
-msgstr "Creando epic"
-
msgid "Cron Timezone"
msgstr "Zona horaria del Cron"
msgid "Cron syntax"
msgstr "Sintaxis de Cron"
-msgid "Current node"
-msgstr "Nodo actual"
+msgid "CurrentUser|Profile"
+msgstr ""
+
+msgid "CurrentUser|Settings"
+msgstr ""
+
+msgid "Custom CI config path"
+msgstr ""
msgid "Custom notification events"
msgstr "Eventos de notificaciones personalizadas"
@@ -1547,9 +1768,6 @@ msgstr "Eventos de notificaciones personalizadas"
msgid "Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notification_link}."
msgstr "Los niveles de notificación personalizados son los mismos que los niveles participantes. Con los niveles de notificación personalizados, también recibirá notificaciones para eventos seleccionados. Para obtener más información, consulte %{notification_link}."
-msgid "Customize colors"
-msgstr ""
-
msgid "Cycle Analytics"
msgstr ""
@@ -1586,7 +1804,7 @@ msgstr "Dic"
msgid "December"
msgstr "Diciembre"
-msgid "Default classification label"
+msgid "Decline and sign out"
msgstr ""
msgid "Define a custom pattern with cron syntax"
@@ -1595,6 +1813,9 @@ msgstr "Definir un patrón personalizado con la sintaxis de cron"
msgid "Delete"
msgstr "Eliminar"
+msgid "Delete list"
+msgstr ""
+
msgid "Deploy"
msgid_plural "Deploys"
msgstr[0] "Despliegue"
@@ -1603,37 +1824,163 @@ msgstr[1] "Despliegues"
msgid "Deploy Keys"
msgstr ""
-msgid "Description"
-msgstr "Descripción"
+msgid "DeployKeys|+%{count} others"
+msgstr ""
+
+msgid "DeployKeys|Current project"
+msgstr ""
+
+msgid "DeployKeys|Deploy key"
+msgstr ""
+
+msgid "DeployKeys|Enabled deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Error enabling deploy key"
+msgstr ""
+
+msgid "DeployKeys|Error getting deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Error removing deploy key"
+msgstr ""
+
+msgid "DeployKeys|Expand %{count} other projects"
+msgstr ""
+
+msgid "DeployKeys|Loading deploy keys"
+msgstr ""
+
+msgid "DeployKeys|No deploy keys found. Create one with the form above."
+msgstr ""
+
+msgid "DeployKeys|Privately accessible deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Project usage"
+msgstr ""
+
+msgid "DeployKeys|Publicly accessible deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Read access only"
+msgstr ""
+
+msgid "DeployKeys|Write access allowed"
+msgstr ""
+
+msgid "DeployKeys|You are going to remove this deploy key. Are you sure?"
+msgstr ""
+
+msgid "DeployTokens|Active Deploy Tokens (%{active_tokens})"
+msgstr ""
+
+msgid "DeployTokens|Add a deploy token"
+msgstr ""
+
+msgid "DeployTokens|Allows read-only access to the registry images"
+msgstr ""
+
+msgid "DeployTokens|Allows read-only access to the repository"
+msgstr ""
+
+msgid "DeployTokens|Copy deploy token to clipboard"
+msgstr ""
+
+msgid "DeployTokens|Copy username to clipboard"
+msgstr ""
+
+msgid "DeployTokens|Create deploy token"
+msgstr ""
+
+msgid "DeployTokens|Created"
+msgstr ""
+
+msgid "DeployTokens|Deploy Tokens"
+msgstr ""
+
+msgid "DeployTokens|Deploy tokens allow read-only access to your repository and registry images."
+msgstr ""
-msgid "Description templates allow you to define context-specific templates for issue and merge request description fields for your project."
+msgid "DeployTokens|Expires"
msgstr ""
+msgid "DeployTokens|Name"
+msgstr ""
+
+msgid "DeployTokens|Pick a name for the application, and we'll give you a unique deploy token."
+msgstr ""
+
+msgid "DeployTokens|Revoke"
+msgstr ""
+
+msgid "DeployTokens|Revoke %{name}"
+msgstr ""
+
+msgid "DeployTokens|Scopes"
+msgstr ""
+
+msgid "DeployTokens|This action cannot be undone."
+msgstr ""
+
+msgid "DeployTokens|This project has no active Deploy Tokens."
+msgstr ""
+
+msgid "DeployTokens|Use this token as a password. Make sure you save it - you won't be able to access it again."
+msgstr ""
+
+msgid "DeployTokens|Use this username as a login."
+msgstr ""
+
+msgid "DeployTokens|Username"
+msgstr ""
+
+msgid "DeployTokens|You are about to revoke"
+msgstr ""
+
+msgid "DeployTokens|Your New Deploy Token"
+msgstr ""
+
+msgid "DeployTokens|Your new project deploy token has been created."
+msgstr ""
+
+msgid "Deprioritize label"
+msgstr ""
+
+msgid "Description"
+msgstr "Descripción"
+
msgid "Details"
msgstr "Detalles"
msgid "Diffs|No file name available"
msgstr ""
+msgid "Diffs|Something went wrong while fetching diff lines."
+msgstr ""
+
msgid "Directory name"
msgstr "Nombre del directorio"
msgid "Disable"
msgstr "Deshabilitar"
-msgid "Discard draft"
+msgid "Disable for this project"
msgstr ""
-msgid "Discover GitLab Geo."
+msgid "Disable group Runners"
msgstr ""
-msgid "Dismiss Cycle Analytics introduction box"
+msgid "Discard changes"
msgstr ""
-msgid "Dismiss Merge Request promotion"
+msgid "Discard draft"
msgstr ""
-msgid "Documentation for popular identity providers"
+msgid "Dismiss Cycle Analytics introduction box"
+msgstr ""
+
+msgid "Domain"
msgstr ""
msgid "Don't show again"
@@ -1675,31 +2022,34 @@ msgstr ""
msgid "Due date"
msgstr ""
-msgid "During this process, you’ll be asked for URLs from GitLab’s side. Use the URLs shown below."
+msgid "Each Runner can be in one of the following states:"
msgstr ""
msgid "Edit"
msgstr "Editar"
+msgid "Edit Label"
+msgstr ""
+
msgid "Edit Pipeline Schedule %{id}"
msgstr "Editar Programación del Pipeline %{id}"
msgid "Edit files in the editor and commit changes here"
msgstr ""
-msgid "Editing"
+msgid "Edit identity for %{user_name}"
msgstr ""
-msgid "Elasticsearch"
+msgid "Email"
msgstr ""
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "Email patch"
msgstr ""
-msgid "Email"
+msgid "Emails"
msgstr ""
-msgid "Emails"
+msgid "Embed"
msgstr ""
msgid "Enable"
@@ -1708,9 +2058,6 @@ msgstr "Habilitar"
msgid "Enable Auto DevOps"
msgstr ""
-msgid "Enable SAML authentication for this group"
-msgstr ""
-
msgid "Enable Sentry for error reporting and logging."
msgstr ""
@@ -1720,7 +2067,13 @@ msgstr ""
msgid "Enable and configure Prometheus metrics."
msgstr ""
-msgid "Enable classification control using an external service"
+msgid "Enable for this project"
+msgstr ""
+
+msgid "Enable group Runners"
+msgstr ""
+
+msgid "Enable or disable certain group features and choose access levels."
msgstr ""
msgid "Enable or disable version check and usage ping."
@@ -1732,7 +2085,10 @@ msgstr ""
msgid "Enable the Performance Bar for a given group."
msgstr ""
-msgid "Enabled"
+msgid "Ends at (UTC)"
+msgstr ""
+
+msgid "Environments"
msgstr ""
msgid "Environments|An error occurred while fetching the environments."
@@ -1783,43 +2139,40 @@ msgstr "Actualizado"
msgid "Environments|You don't have any environments right now."
msgstr "No tiene ningún entorno en este momento."
-msgid "Epic will be removed! Are you sure?"
+msgid "Error Reporting and Logging"
msgstr ""
-msgid "Epics"
-msgstr "Epics"
-
-msgid "Epics Roadmap"
+msgid "Error committing changes. Please try again."
msgstr ""
-msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
+msgid "Error fetching contributors data."
msgstr ""
-msgid "Error Reporting and Logging"
+msgid "Error fetching job trace"
msgstr ""
-msgid "Error checking branch data. Please try again."
+msgid "Error fetching labels."
msgstr ""
-msgid "Error committing changes. Please try again."
+msgid "Error fetching network graph."
msgstr ""
-msgid "Error creating epic"
-msgstr "Error creando epic"
+msgid "Error fetching refs"
+msgstr ""
-msgid "Error fetching contributors data."
+msgid "Error fetching usage ping data."
msgstr ""
-msgid "Error fetching labels."
+msgid "Error loading branch data. Please try again."
msgstr ""
-msgid "Error fetching network graph."
+msgid "Error loading last commit."
msgstr ""
-msgid "Error fetching refs"
+msgid "Error loading merge requests."
msgstr ""
-msgid "Error fetching usage ping data."
+msgid "Error loading project data. Please try again."
msgstr ""
msgid "Error occurred when toggling the notification subscription"
@@ -1834,6 +2187,9 @@ msgstr ""
msgid "Error updating todo status."
msgstr ""
+msgid "Estimated"
+msgstr ""
+
msgid "EventFilterBy|Filter by all"
msgstr ""
@@ -1864,32 +2220,17 @@ msgstr "Todas las semanas (domingos a las 4:00 am)"
msgid "Expand"
msgstr "Expandir"
-msgid "Explore projects"
-msgstr "Explorar proyectos"
-
-msgid "Explore public groups"
-msgstr "Explorar grupos públicos"
-
-msgid "External Classification Policy Authorization"
-msgstr ""
-
-msgid "External authentication"
-msgstr ""
-
-msgid "External authorization denied access to this project"
-msgstr ""
-
-msgid "External authorization request timeout"
+msgid "Expand all"
msgstr ""
-msgid "ExternalAuthorizationService|Classification Label"
+msgid "Expand sidebar"
msgstr ""
-msgid "ExternalAuthorizationService|Classification label"
-msgstr ""
+msgid "Explore projects"
+msgstr "Explorar proyectos"
-msgid "ExternalAuthorizationService|When no classification label is set the default label `%{default_label}` will be used."
-msgstr ""
+msgid "Explore public groups"
+msgstr "Explorar grupos públicos"
msgid "Failed"
msgstr ""
@@ -1900,6 +2241,9 @@ msgstr ""
msgid "Failed to change the owner"
msgstr "Error al cambiar el propietario"
+msgid "Failed to check related branches."
+msgstr ""
+
msgid "Failed to remove issue from board, please try again."
msgstr ""
@@ -1909,6 +2253,12 @@ msgstr "Error al eliminar la programación del pipeline"
msgid "Failed to update issues, please try again."
msgstr ""
+msgid "Failure"
+msgstr ""
+
+msgid "Faster as it re-uses the project workspace (falling back to clone if it doesn't exist)"
+msgstr ""
+
msgid "Feb"
msgstr ""
@@ -1918,18 +2268,12 @@ msgstr "Febrero"
msgid "Fields on this page are now uneditable, you can configure"
msgstr ""
-msgid "File name"
-msgstr "Nombre de archivo"
-
msgid "Files"
msgstr "Archivos"
msgid "Files (%{human_size})"
msgstr ""
-msgid "Fill in the fields below, turn on <strong>%{enable_label}</strong>, and press <strong>%{save_changes}</strong>"
-msgstr ""
-
msgid "Filter by commit message"
msgstr "Filtrar por mensaje del cambio"
@@ -1948,10 +2292,13 @@ msgstr "Primer"
msgid "FirstPushedBy|pushed by"
msgstr "enviado por"
-msgid "Font Color"
+msgid "For internal projects, any logged in user can view pipelines and access job details (output logs and artifacts)"
+msgstr ""
+
+msgid "For private projects, any member (guest or higher) can view pipelines and access job details (output logs and artifacts)"
msgstr ""
-msgid "Footer message"
+msgid "For public projects, anyone can view pipelines and access job details (output logs and artifacts)"
msgstr ""
msgid "Fork"
@@ -1971,6 +2318,9 @@ msgstr ""
msgid "Format"
msgstr "Formato"
+msgid "Found errors in your .gitlab-ci.yml:"
+msgstr ""
+
msgid "From %{provider_title}"
msgstr ""
@@ -1986,166 +2336,13 @@ msgstr ""
msgid "GPG Keys"
msgstr "Llaves GPG"
-msgid "Generate a default set of labels"
-msgstr ""
-
-msgid "Geo Nodes"
-msgstr ""
-
-msgid "Geo allows you to replicate your GitLab instance to other geographical locations."
-msgstr ""
-
-msgid "GeoNodeSyncStatus|Node is failing or broken."
-msgstr "El nodo está fallando o dañado."
-
-msgid "GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage."
-msgstr "El nodo está lento, sobrecargado o se esta recuperando de una interrupción."
-
-msgid "GeoNodes|Checksummed"
-msgstr ""
-
-msgid "GeoNodes|Database replication lag:"
-msgstr ""
-
-msgid "GeoNodes|Disabling a node stops the sync process. Are you sure?"
-msgstr ""
-
-msgid "GeoNodes|Does not match the primary storage configuration"
-msgstr ""
-
-msgid "GeoNodes|Failed"
-msgstr ""
-
-msgid "GeoNodes|Full"
-msgstr ""
-
-msgid "GeoNodes|GitLab version does not match the primary node version"
-msgstr ""
-
-msgid "GeoNodes|GitLab version:"
-msgstr ""
-
-msgid "GeoNodes|Health status:"
-msgstr ""
-
-msgid "GeoNodes|Last event ID processed by cursor:"
-msgstr ""
-
-msgid "GeoNodes|Last event ID seen from primary:"
-msgstr ""
-
-msgid "GeoNodes|Loading nodes"
-msgstr ""
-
-msgid "GeoNodes|Local Attachments:"
-msgstr ""
-
-msgid "GeoNodes|Local LFS objects:"
-msgstr ""
-
-msgid "GeoNodes|Local job artifacts:"
-msgstr ""
-
-msgid "GeoNodes|New node"
-msgstr ""
-
-msgid "GeoNodes|Node Authentication was successfully repaired."
-msgstr ""
-
-msgid "GeoNodes|Node was successfully removed."
-msgstr ""
-
-msgid "GeoNodes|Not checksummed"
-msgstr ""
-
-msgid "GeoNodes|Out of sync"
-msgstr ""
-
-msgid "GeoNodes|Removing a node stops the sync process. Are you sure?"
-msgstr ""
-
-msgid "GeoNodes|Replication slot WAL:"
-msgstr ""
-
-msgid "GeoNodes|Replication slots:"
-msgstr ""
-
-msgid "GeoNodes|Repositories checksummed:"
-msgstr ""
-
-msgid "GeoNodes|Repositories:"
-msgstr ""
-
-msgid "GeoNodes|Repository checksums verified:"
-msgstr ""
-
-msgid "GeoNodes|Selective"
-msgstr ""
-
-msgid "GeoNodes|Something went wrong while changing node status"
-msgstr ""
-
-msgid "GeoNodes|Something went wrong while removing node"
-msgstr ""
-
-msgid "GeoNodes|Something went wrong while repairing node"
-msgstr ""
-
-msgid "GeoNodes|Storage config:"
-msgstr ""
-
-msgid "GeoNodes|Sync settings:"
-msgstr ""
-
-msgid "GeoNodes|Synced"
-msgstr ""
-
-msgid "GeoNodes|Unused slots"
-msgstr ""
-
-msgid "GeoNodes|Unverified"
-msgstr ""
-
-msgid "GeoNodes|Used slots"
-msgstr ""
-
-msgid "GeoNodes|Verified"
+msgid "General"
msgstr ""
-msgid "GeoNodes|Wiki checksums verified:"
+msgid "General pipelines"
msgstr ""
-msgid "GeoNodes|Wikis checksummed:"
-msgstr ""
-
-msgid "GeoNodes|Wikis:"
-msgstr ""
-
-msgid "GeoNodes|You have configured Geo nodes using an insecure HTTP connection. We recommend the use of HTTPS."
-msgstr ""
-
-msgid "Geo|All projects"
-msgstr ""
-
-msgid "Geo|File sync capacity"
-msgstr "Capacidad de sincronización de archivos"
-
-msgid "Geo|Groups to synchronize"
-msgstr ""
-
-msgid "Geo|Projects in certain groups"
-msgstr ""
-
-msgid "Geo|Projects in certain storage shards"
-msgstr ""
-
-msgid "Geo|Repository sync capacity"
-msgstr "Capacidad de sincronización del Repositorio"
-
-msgid "Geo|Select groups to replicate."
-msgstr "Seleccionar grupos a replicar."
-
-msgid "Geo|Shards to synchronize"
+msgid "Generate a default set of labels"
msgstr ""
msgid "Git repository URL"
@@ -2157,6 +2354,9 @@ msgstr ""
msgid "Git storage health information has been reset"
msgstr ""
+msgid "Git strategy for pipelines"
+msgstr ""
+
msgid "Git version"
msgstr ""
@@ -2166,21 +2366,24 @@ msgstr ""
msgid "GitLab CI Linter has been moved"
msgstr ""
-msgid "GitLab Geo"
+msgid "GitLab Group Runners can execute code for all the projects in this group."
msgstr ""
msgid "GitLab Runner section"
msgstr "Sección GitLab Runner"
-msgid "GitLab single sign on URL"
-msgstr ""
-
msgid "Gitaly"
msgstr ""
msgid "Gitaly Servers"
msgstr ""
+msgid "Gitaly|Address"
+msgstr ""
+
+msgid "Go Back"
+msgstr ""
+
msgid "Go back"
msgstr ""
@@ -2196,22 +2399,19 @@ msgstr "La autenticación de Google no se encuentra %{link_to_documentation}. Pr
msgid "Got it!"
msgstr ""
-msgid "GroupRoadmap|Epics let you manage your portfolio of projects more efficiently and with less effort"
-msgstr ""
-
-msgid "GroupRoadmap|From %{dateWord}"
+msgid "Graph"
msgstr ""
-msgid "GroupRoadmap|Loading roadmap"
+msgid "Group CI/CD settings"
msgstr ""
-msgid "GroupRoadmap|Something went wrong while fetching epics"
+msgid "Group ID"
msgstr ""
-msgid "GroupRoadmap|To view the roadmap, add a planned start or finish date to one of your epics in this group or its subgroups. Only epics in the past 3 months and the next 3 months are shown &ndash; from %{startDate} to %{endDate}."
+msgid "Group Runners"
msgstr ""
-msgid "GroupRoadmap|Until %{dateWord}"
+msgid "Group maintainers can register group runners in the %{link}"
msgstr ""
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
@@ -2238,6 +2438,9 @@ msgstr ""
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
+msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
+msgstr ""
+
msgid "GroupsEmptyState|A group is a collection of several projects."
msgstr ""
@@ -2277,12 +2480,6 @@ msgstr "Lo sentimos, no existen grupos que coincidan con su búsqueda"
msgid "GroupsTree|Sorry, no groups or projects matched your search"
msgstr "Lo sentimos, no existen grupos ni proyectos que coincidan con su búsqueda"
-msgid "Have your users email"
-msgstr ""
-
-msgid "Header message"
-msgstr ""
-
msgid "Health Check"
msgstr ""
@@ -2315,19 +2512,49 @@ msgid_plural "Hide values"
msgstr[0] ""
msgstr[1] ""
+msgid "Hide whitespace changes"
+msgstr ""
+
msgid "History"
msgstr "Historial"
msgid "Housekeeping successfully started"
msgstr "Servicio de limpieza iniciado con éxito"
-msgid "Identity provider single sign on URL"
+msgid "I accept the %{terms_link}"
msgstr ""
-msgid "If enabled, access to projects will be validated on an external service using their classification label."
+msgid "I accept the|Terms of Service and Privacy Policy"
msgstr ""
-msgid "If using GitHub, you’ll see pipeline statuses on GitHub for your commits and pull requests. %{more_info_link}"
+msgid "ID"
+msgstr ""
+
+msgid "IDE|Commit"
+msgstr ""
+
+msgid "IDE|Edit"
+msgstr ""
+
+msgid "IDE|Go back"
+msgstr ""
+
+msgid "IDE|Open in file view"
+msgstr ""
+
+msgid "IDE|Review"
+msgstr ""
+
+msgid "Identifier"
+msgstr ""
+
+msgid "Identities"
+msgstr ""
+
+msgid "If disabled, the access level will depend on the user's permissions in the project."
+msgstr ""
+
+msgid "If enabled"
msgstr ""
msgid "If you already have files you can push them using the %{link_to_cli} below."
@@ -2336,6 +2563,15 @@ msgstr ""
msgid "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>."
msgstr ""
+msgid "ImageDiffViewer|2-up"
+msgstr ""
+
+msgid "ImageDiffViewer|Onion skin"
+msgstr ""
+
+msgid "ImageDiffViewer|Swipe"
+msgstr ""
+
msgid "Import"
msgstr ""
@@ -2351,16 +2587,10 @@ msgstr ""
msgid "Import repository"
msgstr "Importar repositorio"
-msgid "ImportButtons|Connect repositories from"
+msgid "Include a Terms of Service agreement and Privacy Policy that all users must accept."
msgstr ""
-msgid "Improve Issue boards with GitLab Enterprise Edition."
-msgstr "Mejore los tableros de Incidencias con GitLab Enterprise Edition."
-
-msgid "Improve issues management with Issue weight and GitLab Enterprise Edition."
-msgstr "Mejore la gestión de incidencias con Peso de Incidencias y GitLab Enterprise Edition."
-
-msgid "Improve search with Advanced Global Search and GitLab Enterprise Edition."
+msgid "Inline"
msgstr ""
msgid "Install Runner on Kubernetes"
@@ -2369,17 +2599,15 @@ msgstr ""
msgid "Install a Runner compatible with GitLab CI"
msgstr "Instala un Runner compatible con GitLab CI"
-msgid "Instance"
-msgid_plural "Instances"
-msgstr[0] "Instancia"
-msgstr[1] "Instancias"
-
msgid "Instance does not support multiple Kubernetes clusters"
msgstr ""
msgid "Integrations"
msgstr ""
+msgid "Integrations Settings"
+msgstr ""
+
msgid "Interested parties can even contribute by pushing commits if they want to."
msgstr ""
@@ -2395,8 +2623,8 @@ msgstr "Patrón de intervalo"
msgid "Introducing Cycle Analytics"
msgstr "Introducción a Cycle Analytics"
-msgid "Issue board focus mode"
-msgstr "Modo enfocado del Tablero de Incidencias"
+msgid "Issue Board"
+msgstr ""
msgid "Issue events"
msgstr "Eventos de incidencia"
@@ -2404,9 +2632,6 @@ msgstr "Eventos de incidencia"
msgid "IssueBoards|Board"
msgstr "Tablero"
-msgid "IssueBoards|Boards"
-msgstr "Tableros"
-
msgid "Issues"
msgstr ""
@@ -2419,6 +2644,12 @@ msgstr ""
msgid "January"
msgstr "Enero"
+msgid "Job"
+msgstr ""
+
+msgid "Job has been erased"
+msgstr ""
+
msgid "Jobs"
msgstr ""
@@ -2461,6 +2692,9 @@ msgstr ""
msgid "Kubernetes service integration has been deprecated. %{deprecated_message_content} your Kubernetes clusters using the new <a href=\"%{url}\"/>Kubernetes Clusters</a> page"
msgstr ""
+msgid "LFS"
+msgstr ""
+
msgid "LFSStatus|Disabled"
msgstr "Deshabilitado"
@@ -2470,6 +2704,9 @@ msgstr "Habilitado"
msgid "Label"
msgstr ""
+msgid "Label actions dropdown"
+msgstr ""
+
msgid "LabelSelect|%{firstLabelName} +%{remainingLabelCount} more"
msgstr ""
@@ -2485,6 +2722,9 @@ msgstr ""
msgid "Labels can be applied to issues and merge requests to categorize them."
msgstr ""
+msgid "Labels can be applied to issues and merge requests."
+msgstr ""
+
msgid "Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>"
msgstr ""
@@ -2520,6 +2760,9 @@ msgstr ""
msgid "LastPushEvent|at"
msgstr ""
+msgid "Latest changes"
+msgstr ""
+
msgid "Learn more"
msgstr "Conozca más"
@@ -2544,9 +2787,6 @@ msgstr "Abandonar grupo"
msgid "Leave project"
msgstr "Abandonar proyecto"
-msgid "License"
-msgstr "Licencia"
-
msgid "List"
msgstr ""
@@ -2556,6 +2796,9 @@ msgstr ""
msgid "Loading the GitLab IDE..."
msgstr ""
+msgid "Loading..."
+msgstr ""
+
msgid "Lock"
msgstr "Bloquear"
@@ -2565,21 +2808,18 @@ msgstr ""
msgid "Lock not found"
msgstr ""
+msgid "Lock to current projects"
+msgstr ""
+
msgid "Locked"
msgstr "Bloqueado"
-msgid "Locked Files"
-msgstr "Archivos Bloqueados"
-
-msgid "Locks give the ability to lock specific file or folder."
+msgid "Locked to current projects"
msgstr ""
msgid "Login"
msgstr "Iniciar sesión"
-msgid "Make everyone on your team more productive regardless of their location. GitLab Geo creates read-only mirrors of your GitLab instance so you can reduce the time it takes to clone and fetch large repos."
-msgstr ""
-
msgid "Manage all notifications"
msgstr ""
@@ -2592,16 +2832,16 @@ msgstr ""
msgid "Manage project labels"
msgstr ""
-msgid "Manage your group’s membership while adding another level of security with SAML."
-msgstr ""
-
msgid "Mar"
msgstr ""
msgid "March"
msgstr "Marzo"
-msgid "Mark done"
+msgid "Mark todo as done"
+msgstr ""
+
+msgid "Markdown enabled"
msgstr ""
msgid "Maximum git storage failures"
@@ -2616,7 +2856,7 @@ msgstr "Mediana"
msgid "Members"
msgstr "Miembros"
-msgid "Members will be forwarded here when signing in to your group. Get this from your identity provider, where it can also be called \"SSO Service Location\", \"SAML Token Issuance Endpoint\", or \"SAML 2.0/W-Federation URL\"."
+msgid "Merge Request:"
msgstr ""
msgid "Merge Requests"
@@ -2628,91 +2868,46 @@ msgstr ""
msgid "Merge request"
msgstr ""
-msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
-msgstr ""
-
-msgid "Merged"
-msgstr ""
-
-msgid "Messages"
-msgstr "Mensajes"
-
-msgid "Metrics - Influx"
-msgstr ""
-
-msgid "Metrics - Prometheus"
-msgstr ""
-
-msgid "Metrics|Business"
-msgstr ""
-
-msgid "Metrics|Create metric"
-msgstr ""
-
-msgid "Metrics|Edit metric"
-msgstr ""
-
-msgid "Metrics|For grouping similar metrics"
-msgstr ""
-
-msgid "Metrics|Label of the chart's vertical axis. Usually the type of the unit being charted. The horizontal axis (X-axis) always represents time."
+msgid "Merge requests"
msgstr ""
-msgid "Metrics|Legend label (optional)"
-msgstr ""
-
-msgid "Metrics|Must be a valid PromQL query."
-msgstr ""
-
-msgid "Metrics|Name"
-msgstr ""
-
-msgid "Metrics|New metric"
-msgstr ""
-
-msgid "Metrics|Prometheus Query Documentation"
-msgstr ""
-
-msgid "Metrics|Query"
-msgstr ""
-
-msgid "Metrics|Response"
+msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
msgstr ""
-msgid "Metrics|System"
+msgid "MergeRequests|Resolve this discussion in a new issue"
msgstr ""
-msgid "Metrics|Type"
+msgid "MergeRequests|Saving the comment failed"
msgstr ""
-msgid "Metrics|Unit label"
+msgid "MergeRequests|Toggle comments for this file"
msgstr ""
-msgid "Metrics|Used as a title for the chart"
+msgid "MergeRequests|Updating discussions failed"
msgstr ""
-msgid "Metrics|Used if the query returns a single series. If it returns multiple series, their legend labels will be picked up from the response."
+msgid "MergeRequests|View file @ %{commitId}"
msgstr ""
-msgid "Metrics|Y-axis label"
+msgid "MergeRequests|View replaced file @ %{commitId}"
msgstr ""
-msgid "Metrics|e.g. HTTP requests"
+msgid "Merged"
msgstr ""
-msgid "Metrics|e.g. Requests/second"
-msgstr ""
+msgid "Messages"
+msgstr "Mensajes"
-msgid "Metrics|e.g. Throughput"
+msgid "Metrics - Influx"
msgstr ""
-msgid "Metrics|e.g. rate(http_requests_total[5m])"
+msgid "Metrics - Prometheus"
msgstr ""
-msgid "Metrics|e.g. req/sec"
+msgid "Milestone"
msgstr ""
-msgid "Milestone"
+msgid "Milestones"
msgstr ""
msgid "Milestones|Delete milestone"
@@ -2733,9 +2928,6 @@ msgstr ""
msgid "Milestones|Promote Milestone"
msgstr ""
-msgid "Milestones|This action cannot be reversed."
-msgstr ""
-
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "agregar una clave SSH"
@@ -2748,7 +2940,7 @@ msgstr ""
msgid "Monitoring"
msgstr ""
-msgid "More info"
+msgid "More actions"
msgstr ""
msgid "More information"
@@ -2763,12 +2955,30 @@ msgstr ""
msgid "Move issue"
msgstr ""
-msgid "Multiple issue boards"
+msgid "Name"
msgstr ""
msgid "Name new label"
msgstr ""
+msgid "Name your individual key via a title"
+msgstr ""
+
+msgid "Nav|Help"
+msgstr ""
+
+msgid "Nav|Home"
+msgstr ""
+
+msgid "Nav|Sign In / Register"
+msgstr ""
+
+msgid "Nav|Sign out and sign in with a different account"
+msgstr ""
+
+msgid "New Identity"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "Nueva incidencia"
@@ -2780,6 +2990,9 @@ msgstr ""
msgid "New Kubernetes cluster"
msgstr ""
+msgid "New Label"
+msgstr ""
+
msgid "New Pipeline Schedule"
msgstr "Nueva Programación del Pipeline"
@@ -2792,15 +3005,15 @@ msgstr "Nueva rama no disponible"
msgid "New directory"
msgstr "Nuevo directorio"
-msgid "New epic"
-msgstr "Nuevo epic"
-
msgid "New file"
msgstr "Nuevo archivo"
msgid "New group"
msgstr "Nuevo grupo"
+msgid "New identity"
+msgstr ""
+
msgid "New issue"
msgstr "Nueva incidencia"
@@ -2810,6 +3023,9 @@ msgstr ""
msgid "New merge request"
msgstr "Nueva solicitud de fusión"
+msgid "New pipelines will cancel older, pending pipelines on the same branch"
+msgstr ""
+
msgid "New project"
msgstr "Nuevo proyecto"
@@ -2825,7 +3041,7 @@ msgstr "Nuevo sub-grupo"
msgid "New tag"
msgstr "Nueva etiqueta"
-msgid "No Label"
+msgid "No"
msgstr ""
msgid "No assignee"
@@ -2846,7 +3062,16 @@ msgstr ""
msgid "No file chosen"
msgstr ""
-msgid "No labels created yet."
+msgid "No files found"
+msgstr ""
+
+msgid "No files found."
+msgstr ""
+
+msgid "No merge requests found"
+msgstr ""
+
+msgid "No messages were logged"
msgstr ""
msgid "No repository"
@@ -2879,15 +3104,9 @@ msgstr "No hay suficientes datos"
msgid "Note that the master branch is automatically protected. %{link_to_protected_branches}"
msgstr ""
-msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow connecting repositories without generating a Personal Access Token."
-msgstr ""
-
msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
msgstr ""
-msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow connecting repositories without generating a Personal Access Token."
-msgstr ""
-
msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
msgstr ""
@@ -2963,9 +3182,6 @@ msgstr "Noviembre"
msgid "Number of access attempts"
msgstr ""
-msgid "OK"
-msgstr ""
-
msgid "Oct"
msgstr ""
@@ -2975,30 +3191,36 @@ msgstr "Octubre"
msgid "OfSearchInADropdown|Filter"
msgstr "Filtrar"
-msgid "Once imported, repositories can be mirrored over SSH. Read more %{ssh_link}"
+msgid "Online IDE integration settings."
msgstr ""
-msgid "Online IDE integration settings."
+msgid "Only comments from the following commit are shown below"
msgstr ""
msgid "Only project members can comment."
msgstr "Sólo los miembros de proyecto pueden comentar."
-msgid "Open"
+msgid "Open in Xcode"
msgstr ""
-msgid "Opened"
-msgstr "Abierto"
-
msgid "OpenedNDaysAgo|Opened"
msgstr "Abierto"
msgid "Opens in a new window"
msgstr "Abre en una nueva ventana"
+msgid "Operations"
+msgstr ""
+
msgid "Options"
msgstr "Opciones"
+msgid "Or you can choose one of the suggested colors below"
+msgstr ""
+
+msgid "Other Labels"
+msgstr ""
+
msgid "Otherwise it is recommended you start with one of the options below."
msgstr ""
@@ -3032,12 +3254,27 @@ msgstr ""
msgid "Password"
msgstr "Contraseña"
+msgid "Paste your public SSH key, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'. Don't use your private SSH key."
+msgstr ""
+
+msgid "Pause"
+msgstr ""
+
msgid "Pending"
msgstr ""
+msgid "Per job. If a job passes this threshold, it will be marked as failed"
+msgstr ""
+
+msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgstr ""
+
msgid "Performance optimization"
msgstr ""
+msgid "Permissions"
+msgstr ""
+
msgid "Personal Access Token"
msgstr ""
@@ -3053,7 +3290,7 @@ msgstr "Programación del Pipeline"
msgid "Pipeline Schedules"
msgstr "Programaciones de los Pipelines"
-msgid "Pipeline quota"
+msgid "Pipeline triggers"
msgstr ""
msgid "PipelineCharts|Failed:"
@@ -3152,10 +3389,22 @@ msgstr ""
msgid "Pipelines|This project is not currently set up to run pipelines."
msgstr ""
-msgid "Pipeline|Retry pipeline"
+msgid "Pipeline|Create for"
+msgstr ""
+
+msgid "Pipeline|Create pipeline"
+msgstr ""
+
+msgid "Pipeline|Existing branch name or tag"
msgstr ""
-msgid "Pipeline|Retry pipeline #%{pipelineId}?"
+msgid "Pipeline|Run Pipeline"
+msgstr ""
+
+msgid "Pipeline|Search branches"
+msgstr ""
+
+msgid "Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default."
msgstr ""
msgid "Pipeline|Stop pipeline"
@@ -3164,7 +3413,7 @@ msgstr ""
msgid "Pipeline|Stop pipeline #%{pipelineId}?"
msgstr ""
-msgid "Pipeline|You’re about to retry pipeline %{pipelineId}."
+msgid "Pipeline|Variables"
msgstr ""
msgid "Pipeline|You’re about to stop pipeline %{pipelineId}."
@@ -3182,19 +3431,25 @@ msgstr "con etapa"
msgid "Pipeline|with stages"
msgstr "con etapas"
+msgid "Plain diff"
+msgstr ""
+
msgid "PlantUML"
msgstr ""
msgid "Play"
msgstr ""
-msgid "Please <a href=%{link_to_billing} target=\"_blank\" rel=\"noopener noreferrer\">enable billing for one of your projects to be able to create a Kubernetes cluster</a>, then try again."
+msgid "Please accept the Terms of Service before continuing."
+msgstr ""
+
+msgid "Please select at least one filter to see results"
msgstr ""
msgid "Please solve the reCAPTCHA"
msgstr ""
-msgid "Please wait while we connect to your repository. Refresh at will."
+msgid "Please try again"
msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
@@ -3203,7 +3458,19 @@ msgstr ""
msgid "Preferences"
msgstr "Preferencias"
-msgid "Primary"
+msgid "Preferences|Navigation theme"
+msgstr ""
+
+msgid "Prioritize"
+msgstr ""
+
+msgid "Prioritize label"
+msgstr ""
+
+msgid "Prioritized Labels"
+msgstr ""
+
+msgid "Prioritized label"
msgstr ""
msgid "Private - Project access must be granted explicitly to each user."
@@ -3221,6 +3488,12 @@ msgstr "Perfil"
msgid "Profiles|Account scheduled for removal."
msgstr ""
+msgid "Profiles|Change username"
+msgstr ""
+
+msgid "Profiles|Current path: %{path}"
+msgstr ""
+
msgid "Profiles|Delete Account"
msgstr ""
@@ -3239,9 +3512,21 @@ msgstr "Contraseña inválida"
msgid "Profiles|Invalid username"
msgstr "Nombre de usuario inválido"
+msgid "Profiles|Path"
+msgstr ""
+
msgid "Profiles|Type your %{confirmationValue} to confirm:"
msgstr "Escribe tu %{confirmationValue} para confirmar:"
+msgid "Profiles|Update username"
+msgstr ""
+
+msgid "Profiles|Username change failed - %{message}"
+msgstr ""
+
+msgid "Profiles|Username successfully changed"
+msgstr ""
+
msgid "Profiles|You don't have access to delete this user."
msgstr "No tienes acceso para eliminar este usuario."
@@ -3260,6 +3545,12 @@ msgstr ""
msgid "Programming languages used in this repository"
msgstr ""
+msgid "Progress"
+msgstr ""
+
+msgid "Project"
+msgstr ""
+
msgid "Project '%{project_name}' is in the process of being deleted."
msgstr "Proyecto '%{project_name}' está en proceso de ser eliminado."
@@ -3272,6 +3563,9 @@ msgstr "Proyecto ‘%{project_name}’ fue creado satisfactoriamente."
msgid "Project '%{project_name}' was successfully updated."
msgstr "Proyecto ‘%{project_name}’ fue actualizado satisfactoriamente."
+msgid "Project Badges"
+msgstr ""
+
msgid "Project access must be granted explicitly to each user."
msgstr "El acceso al proyecto debe concederse explícitamente a cada usuario."
@@ -3299,30 +3593,6 @@ msgstr "Se inició la exportación del proyecto. Se enviará un enlace de descar
msgid "ProjectActivityRSS|Subscribe"
msgstr "Suscribirse"
-msgid "ProjectCreationLevel|Allowed to create projects"
-msgstr ""
-
-msgid "ProjectCreationLevel|Default project creation protection"
-msgstr ""
-
-msgid "ProjectCreationLevel|Developers + Masters"
-msgstr ""
-
-msgid "ProjectCreationLevel|Masters"
-msgstr ""
-
-msgid "ProjectCreationLevel|No one"
-msgstr ""
-
-msgid "ProjectFeature|Disabled"
-msgstr "Deshabilitada"
-
-msgid "ProjectFeature|Everyone with access"
-msgstr "Todos con acceso"
-
-msgid "ProjectFeature|Only team members"
-msgstr "Solo miembros del equipo"
-
msgid "ProjectFileTree|Name"
msgstr "Nombre"
@@ -3332,27 +3602,6 @@ msgstr "Nunca"
msgid "ProjectLifecycle|Stage"
msgstr "Etapa"
-msgid "ProjectNetworkGraph|Graph"
-msgstr "Historial gráfico"
-
-msgid "ProjectSettings|Contact an admin to change this setting."
-msgstr "Póngase en contacto con un administrador para cambiar esta opción."
-
-msgid "ProjectSettings|Only signed commits can be pushed to this repository."
-msgstr "Solo se pueden enviar commits firmados a este repositorio."
-
-msgid "ProjectSettings|This setting is applied on the server level and can be overridden by an admin."
-msgstr "Esta configuración se aplica a nivel del servidor y puede ser redefinida por un administrador."
-
-msgid "ProjectSettings|This setting is applied on the server level but has been overridden for this project."
-msgstr "Esta configuración se aplica a nivel del servidor pero se ha redefinido para este proyecto."
-
-msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
-msgstr "Esta configuración se aplicará a todos los proyectos a menos que sea redefinida por un administrador."
-
-msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails."
-msgstr ""
-
msgid "Projects"
msgstr "Proyectos"
@@ -3377,6 +3626,9 @@ msgstr "Lo sentimos, no hay proyectos que coincidan con su búsqueda"
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr "Esta función requiere que el navegador soporte localStorage"
+msgid "PrometheusDashboard|Time"
+msgstr ""
+
msgid "PrometheusService|%{exporters} with %{metrics} were found"
msgstr ""
@@ -3398,18 +3650,9 @@ msgstr "De manera predeterminada, Prometheus escucha en 'http://localhost:9090'.
msgid "PrometheusService|Common metrics"
msgstr ""
-msgid "PrometheusService|Common metrics are automatically monitored based on a library of metrics from popular exporters."
-msgstr ""
-
-msgid "PrometheusService|Custom metrics"
-msgstr ""
-
msgid "PrometheusService|Finding and configuring metrics..."
msgstr "Encontrar y configurar métricas..."
-msgid "PrometheusService|Finding custom metrics..."
-msgstr ""
-
msgid "PrometheusService|Install Prometheus on clusters"
msgstr ""
@@ -3422,24 +3665,21 @@ msgstr ""
msgid "PrometheusService|Metrics"
msgstr "Métricas"
+msgid "PrometheusService|Metrics are automatically configured and monitored based on a library of metrics from popular exporters."
+msgstr ""
+
msgid "PrometheusService|Missing environment variable"
msgstr "Falta la variable de entorno"
msgid "PrometheusService|More information"
msgstr "Más información"
-msgid "PrometheusService|New metric"
-msgstr ""
-
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr "URL Base de Prometheus, como http://prometheus.example.com/"
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
-msgstr ""
-
msgid "PrometheusService|Time-series monitoring service"
msgstr ""
@@ -3455,23 +3695,29 @@ msgstr ""
msgid "Promote"
msgstr ""
-msgid "Promote to Group Label"
+msgid "Promote these project milestones into a group milestone."
msgstr ""
msgid "Promote to Group Milestone"
msgstr ""
+msgid "Promote to group label"
+msgstr ""
+
msgid "Protip:"
msgstr ""
+msgid "Provider"
+msgstr ""
+
msgid "Public - The group and any public projects can be viewed without any authentication."
msgstr "Público - El grupo y cualquier proyecto público puede ser visto sin autenticación."
msgid "Public - The project can be accessed without any authentication."
msgstr "Público - El proyecto puede ser accedido sin ninguna autenticación."
-msgid "Push Rules"
-msgstr "Reglas Push"
+msgid "Public pipelines"
+msgstr ""
msgid "Push events"
msgstr "Eventos Push"
@@ -3482,12 +3728,12 @@ msgstr ""
msgid "Push to create a project"
msgstr ""
-msgid "PushRule|Committer restriction"
-msgstr "Restricción de Committer"
-
msgid "Quick actions can be used in the issues description and comment boxes."
msgstr ""
+msgid "Re-deploy"
+msgstr ""
+
msgid "Read more"
msgstr "Leer más"
@@ -3497,18 +3743,18 @@ msgstr "Léeme"
msgid "Real-time features"
msgstr ""
-msgid "RefSwitcher|Branches"
-msgstr "Ramas"
-
-msgid "RefSwitcher|Tags"
-msgstr "Etiquetas"
-
msgid "Reference:"
msgstr ""
msgid "Register / Sign In"
msgstr ""
+msgid "Register and see your runners for this group."
+msgstr ""
+
+msgid "Register and see your runners for this project."
+msgstr ""
+
msgid "Registry"
msgstr "Registro"
@@ -3539,28 +3785,28 @@ msgstr "Recordar después"
msgid "Remove"
msgstr ""
-msgid "Remove avatar"
+msgid "Remove Runner"
msgstr ""
-msgid "Remove project"
-msgstr "Eliminar proyecto"
-
-msgid "Repair authentication"
+msgid "Remove avatar"
msgstr ""
-msgid "Repo by URL"
+msgid "Remove priority"
msgstr ""
+msgid "Remove project"
+msgstr "Eliminar proyecto"
+
msgid "Repository"
msgstr "Repositorio"
-msgid "Repository has no locks."
+msgid "Repository Settings"
msgstr ""
msgid "Repository maintenance"
msgstr ""
-msgid "Repository mirror settings"
+msgid "Repository mirror"
msgstr ""
msgid "Repository storage"
@@ -3569,6 +3815,9 @@ msgstr ""
msgid "Request Access"
msgstr "Solicitar acceso"
+msgid "Require all users to accept Terms of Service and Privacy Policy when they access GitLab."
+msgstr ""
+
msgid "Reset git storage health information"
msgstr ""
@@ -3578,10 +3827,25 @@ msgstr ""
msgid "Reset runners registration token"
msgstr "Reinicializar el token de registro del runner"
+msgid "Resolve all discussions in new issue"
+msgstr ""
+
+msgid "Resolve conflicts on source branch"
+msgstr ""
+
msgid "Resolve discussion"
msgstr ""
-msgid "Response"
+msgid "Resume"
+msgstr ""
+
+msgid "Retry"
+msgstr ""
+
+msgid "Retry this job"
+msgstr ""
+
+msgid "Retry verification"
msgstr ""
msgid "Reveal value"
@@ -3595,7 +3859,7 @@ msgstr "Revertir este cambio"
msgid "Revert this merge request"
msgstr "Revertir esta solicitud de fusión"
-msgid "Review the process for configuring service providers in your identity provider — in this case, GitLab is the \"service provider\" or \"relying party\"."
+msgid "Review"
msgstr ""
msgid "Reviewing"
@@ -3604,30 +3868,33 @@ msgstr ""
msgid "Reviewing (merge request !%{mergeRequestId})"
msgstr ""
-msgid "Roadmap"
+msgid "Rollback"
msgstr ""
-msgid "Run CI/CD pipelines for external repositories"
+msgid "Runner token"
msgstr ""
msgid "Runners"
msgstr ""
-msgid "Running"
-msgstr ""
-
-msgid "SAML Single Sign On"
+msgid "Runners API"
msgstr ""
-msgid "SAML Single Sign On Settings"
+msgid "Runners can be placed on separate users, servers, and even on your local machine."
msgstr ""
-msgid "SHA1 fingerprint of the SAML token signing certificate. Get this from your identity provider, where it can also be called \"Thumbprint\"."
+msgid "Running"
msgstr ""
msgid "SSH Keys"
msgstr "Llaves SSH"
+msgid "SSL Verification"
+msgstr ""
+
+msgid "Save"
+msgstr ""
+
msgid "Save changes"
msgstr "Guardar los cambios"
@@ -3649,15 +3916,30 @@ msgstr ""
msgid "Scheduling Pipelines"
msgstr "Programación de Pipelines"
-msgid "Scoped issue boards"
+msgid "Scroll to bottom"
+msgstr ""
+
+msgid "Scroll to top"
msgstr ""
msgid "Search"
msgstr ""
+msgid "Search branches"
+msgstr ""
+
msgid "Search branches and tags"
msgstr "Buscar ramas y etiquetas"
+msgid "Search files"
+msgstr ""
+
+msgid "Search for projects, issues, etc."
+msgstr ""
+
+msgid "Search merge requests"
+msgstr ""
+
msgid "Search milestones"
msgstr ""
@@ -3673,15 +3955,15 @@ msgstr "Segundos antes de reinicializar información de fallas"
msgid "Seconds to wait for a storage access attempt"
msgstr "Segundos a esperar para intentar acceder al almacenamiento"
-msgid "Secret variables"
-msgstr ""
-
-msgid "Security report"
+msgid "Select"
msgstr ""
msgid "Select Archive Format"
msgstr "Seleccionar formato de archivo"
+msgid "Select a namespace to fork the project"
+msgstr ""
+
msgid "Select a timezone"
msgstr "Selecciona una zona horaria"
@@ -3694,12 +3976,21 @@ msgstr ""
msgid "Select branch/tag"
msgstr ""
-msgid "Select target branch"
-msgstr "Selecciona una rama de destino"
+msgid "Select project"
+msgstr ""
+
+msgid "Select project and zone to choose machine type"
+msgstr ""
-msgid "Selective synchronization"
+msgid "Select project to choose zone"
msgstr ""
+msgid "Select source branch"
+msgstr ""
+
+msgid "Select target branch"
+msgstr "Selecciona una rama de destino"
+
msgid "Send email"
msgstr ""
@@ -3715,9 +4006,6 @@ msgstr ""
msgid "Service Templates"
msgstr "Plantillas de Servicio"
-msgid "Service URL"
-msgstr ""
-
msgid "Session expiration, projects limit and attachment size."
msgstr ""
@@ -3742,9 +4030,6 @@ msgstr ""
msgid "Set up Koding"
msgstr "Configurar Koding"
-msgid "Set up assertions/attributes/claims (email, first_name, last_name) and NameID according to %{docsLinkStart}the documentation %{icon}%{docsLinkEnd}"
-msgstr ""
-
msgid "SetPasswordToCloneLink|set a password"
msgstr "establecer una contraseña"
@@ -3754,19 +4039,22 @@ msgstr "Configuración"
msgid "Setup a specific Runner automatically"
msgstr ""
-msgid "Share the <strong>%{sso_label}</strong> with members so they can sign in to your group through your identity provider"
+msgid "Share"
msgstr ""
-msgid "SharedRunnersMinutesSettings|By resetting the pipeline minutes for this namespace, the currently used minutes will be set to zero."
+msgid "Shared Runners"
msgstr ""
-msgid "SharedRunnersMinutesSettings|Reset pipeline minutes"
+msgid "Show command"
msgstr ""
-msgid "SharedRunnersMinutesSettings|Reset used pipeline minutes"
+msgid "Show complete raw log"
msgstr ""
-msgid "Show command"
+msgid "Show latest version"
+msgstr ""
+
+msgid "Show latest version of the diff"
msgstr ""
msgid "Show parent pages"
@@ -3775,21 +4063,18 @@ msgstr "Mostrar páginas padre"
msgid "Show parent subgroups"
msgstr ""
+msgid "Show whitespace changes"
+msgstr ""
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "Mostrando %d evento"
msgstr[1] "Mostrando %d eventos"
-msgid "Sidebar|Change weight"
-msgstr ""
-
-msgid "Sidebar|No"
-msgstr ""
-
-msgid "Sidebar|None"
+msgid "Side-by-side"
msgstr ""
-msgid "Sidebar|Weight"
+msgid "Sign out"
msgstr ""
msgid "Sign-in restrictions"
@@ -3801,7 +4086,7 @@ msgstr ""
msgid "Size and domain settings for static websites"
msgstr ""
-msgid "Slack application"
+msgid "Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job"
msgstr ""
msgid "Snippets"
@@ -3813,13 +4098,13 @@ msgstr ""
msgid "Something went wrong on our end."
msgstr ""
-msgid "Something went wrong when toggling the button"
+msgid "Something went wrong on our end. Please try again!"
msgstr ""
-msgid "Something went wrong while fetching Dependency Scanning."
+msgid "Something went wrong when toggling the button"
msgstr ""
-msgid "Something went wrong while fetching SAST."
+msgid "Something went wrong while closing the %{issuable}. Please try again later"
msgstr ""
msgid "Something went wrong while fetching the projects."
@@ -3828,6 +4113,12 @@ msgstr ""
msgid "Something went wrong while fetching the registry list."
msgstr ""
+msgid "Something went wrong while reopening the %{issuable}. Please try again later"
+msgstr ""
+
+msgid "Something went wrong while resolving this discussion. Please try again."
+msgstr ""
+
msgid "Something went wrong. Please try again."
msgstr ""
@@ -3873,9 +4164,6 @@ msgstr ""
msgid "SortOptions|Least popular"
msgstr ""
-msgid "SortOptions|Less weight"
-msgstr ""
-
msgid "SortOptions|Milestone"
msgstr ""
@@ -3885,9 +4173,6 @@ msgstr ""
msgid "SortOptions|Milestone due soon"
msgstr ""
-msgid "SortOptions|More weight"
-msgstr ""
-
msgid "SortOptions|Most popular"
msgstr ""
@@ -3927,9 +4212,6 @@ msgstr ""
msgid "SortOptions|Start soon"
msgstr ""
-msgid "SortOptions|Weight"
-msgstr ""
-
msgid "Source"
msgstr "Fuente"
@@ -3948,9 +4230,36 @@ msgstr ""
msgid "Spam and Anti-bot Protection"
msgstr ""
+msgid "Specific Runners"
+msgstr ""
+
msgid "Specify the following URL during the Runner setup:"
msgstr ""
+msgid "Squash commits"
+msgstr ""
+
+msgid "Stage"
+msgstr ""
+
+msgid "Stage & Commit"
+msgstr ""
+
+msgid "Stage all changes"
+msgstr ""
+
+msgid "Stage changes"
+msgstr ""
+
+msgid "Staged"
+msgstr ""
+
+msgid "Staged %{type}"
+msgstr ""
+
+msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
+msgstr ""
+
msgid "StarProject|Star"
msgstr "Destacar"
@@ -3972,12 +4281,15 @@ msgstr ""
msgid "Started"
msgstr ""
-msgid "State your message to activate"
+msgid "Starts at (UTC)"
msgstr ""
msgid "Status"
msgstr ""
+msgid "Stop this environment"
+msgstr ""
+
msgid "Stopped"
msgstr "Detenido"
@@ -3987,18 +4299,21 @@ msgstr ""
msgid "Subgroups"
msgstr "Sub-grupos"
-msgid "Switch branch/tag"
-msgstr "Cambiar rama/etiqueta"
+msgid "Subscribe"
+msgstr ""
-msgid "System"
+msgid "Subscribe at group level"
msgstr ""
+msgid "Subscribe at project level"
+msgstr ""
+
+msgid "Switch branch/tag"
+msgstr "Cambiar rama/etiqueta"
+
msgid "System Hooks"
msgstr "Hooks de sistema"
-msgid "System header and footer:"
-msgstr ""
-
msgid "Tag (%{tag_count})"
msgid_plural "Tags (%{tag_count})"
msgstr[0] ""
@@ -4007,6 +4322,9 @@ msgstr[1] ""
msgid "Tags"
msgstr "Etiquetas"
+msgid "Tags:"
+msgstr ""
+
msgid "TagsPage|Browse commits"
msgstr ""
@@ -4070,7 +4388,7 @@ msgstr ""
msgid "TagsPage|Use git tag command to add a new one:"
msgstr ""
-msgid "TagsPage|Write your release notes or drag files here..."
+msgid "TagsPage|Write your release notes or drag files here…"
msgstr ""
msgid "TagsPage|protected"
@@ -4085,19 +4403,19 @@ msgstr ""
msgid "Team"
msgstr "Equipo"
-msgid "Thanks! Don't show me this again"
-msgstr "Gracias! No mostrar esto nuevamente"
+msgid "Terms of Service Agreement and Privacy Policy"
+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."
+msgid "Terms of Service and Privacy Policy"
msgstr ""
-msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project"
+msgid "Test coverage parsing"
msgstr ""
-msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
+msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project"
msgstr ""
-msgid "The X509 Certificate to use when mutual TLS is required to communicate with the external authorization service. If left blank, the server certificate is still validated when accessing over HTTPS."
+msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
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."
@@ -4106,9 +4424,6 @@ msgstr "La etapa de desarrollo muestra el tiempo desde el primer cambio hasta la
msgid "The collection of events added to the data gathered for that stage."
msgstr "La colección de eventos agregados a los datos recopilados para esa etapa."
-msgid "The connection will time out after %{timeout}. For repositories that take longer, use a clone/push combination."
-msgstr ""
-
msgid "The fork relationship has been removed."
msgstr "La relación con la bifurcación se ha eliminado."
@@ -4127,7 +4442,7 @@ 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 passphrase required to decrypt the private key. This is optional and the value is encrypted at rest."
+msgid "The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>"
msgstr ""
msgid "The phase of the development lifecycle."
@@ -4136,9 +4451,6 @@ msgstr "La etapa del ciclo de vida de desarrollo."
msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit."
msgstr "La etapa de planificación muestra el tiempo desde el paso anterior hasta el envío de tu primera confirmación. Este tiempo se añadirá automáticamente una vez que usted envíe el primer cambio."
-msgid "The private key to use when a client certificate is provided. This value is encrypted at rest."
-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 "La etapa de producción muestra el tiempo total que tarda entre la creación de una incidencia y el despliegue del código a producción. Los datos se añadirán automáticamente una vez haya finalizado por completo el ciclo de idea a producción."
@@ -4160,7 +4472,7 @@ msgstr ""
msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request."
msgstr "La etapa de revisión muestra el tiempo desde la creación de la solicitud de fusión hasta que los cambios se fusionaron. Los datos se añadirán automáticamente después de fusionar su primera solicitud de fusión."
-msgid "The roadmap shows the progress of your epics along a timeline"
+msgid "The secure token used by the Runner to checkout the project"
msgstr ""
msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time."
@@ -4187,13 +4499,19 @@ msgstr "El valor en el punto medio de una serie de valores observados. Por ejemp
msgid "There are no issues to show"
msgstr ""
+msgid "There are no labels yet"
+msgstr ""
+
msgid "There are no merge requests to show"
msgstr ""
msgid "There are problems accessing Git storage: "
msgstr ""
-msgid "There was an error loading results"
+msgid "There was an error loading jobs"
+msgstr ""
+
+msgid "There was an error loading latest pipeline"
msgstr ""
msgid "There was an error loading users activity calendar."
@@ -4214,12 +4532,21 @@ msgstr ""
msgid "There was an error when unsubscribing from this label."
msgstr ""
-msgid "This board\\'s scope is reduced"
+msgid "They can be managed using the %{link}."
+msgstr ""
+
+msgid "This GitLab instance does not provide any shared Runners yet. Instance administrators can register shared Runners in the admin area."
+msgstr ""
+
+msgid "This diff is collapsed."
msgstr ""
msgid "This directory"
msgstr ""
+msgid "This group does not provide any group Runners yet."
+msgstr ""
+
msgid "This is a confidential issue."
msgstr ""
@@ -4241,6 +4568,15 @@ msgstr ""
msgid "This job depends on upstream jobs that need to succeed in order for this job to be triggered"
msgstr ""
+msgid "This job does not have a trace."
+msgstr ""
+
+msgid "This job has been canceled"
+msgstr ""
+
+msgid "This job has been skipped"
+msgstr ""
+
msgid "This job has not been triggered yet"
msgstr ""
@@ -4259,19 +4595,28 @@ msgstr "Esto significa que no puede enviar código hasta que cree un repositorio
msgid "This merge request is locked."
msgstr ""
+msgid "This option is disabled while you still have unstaged changes"
+msgstr ""
+
msgid "This page is unavailable because you are not allowed to read information across multiple projects."
msgstr ""
+msgid "This page will be removed in a future release."
+msgstr ""
+
msgid "This project"
msgstr ""
+msgid "This project does not belong to a group and can therefore not make use of group Runners."
+msgstr ""
+
msgid "This repository"
msgstr ""
-msgid "This will delete the custom metric, Are you sure?"
+msgid "This source diff could not be displayed because it is too large."
msgstr ""
-msgid "Those emails automatically become issues (with the comments becoming the email conversation) listed here."
+msgid "This user has no identities"
msgstr ""
msgid "Time before an issue gets scheduled"
@@ -4283,10 +4628,10 @@ msgstr "Tiempo antes de que empieze la implementación de una incidencia"
msgid "Time between merge request creation and merge/close"
msgstr "Tiempo entre la creación de la solicitud de fusión y la integración o cierre de ésta"
-msgid "Time between updates and capacity settings."
+msgid "Time remaining"
msgstr ""
-msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
+msgid "Time spent"
msgstr ""
msgid "Time tracking"
@@ -4310,6 +4655,9 @@ msgstr "hace %s días"
msgid "Timeago|%s days remaining"
msgstr "%s días restantes"
+msgid "Timeago|%s hours ago"
+msgstr ""
+
msgid "Timeago|%s hours remaining"
msgstr "%s horas restantes"
@@ -4325,6 +4673,9 @@ msgstr "hace %s meses"
msgid "Timeago|%s months remaining"
msgstr "%s meses restantes"
+msgid "Timeago|%s seconds ago"
+msgstr ""
+
msgid "Timeago|%s seconds remaining"
msgstr "%s segundos restantes"
@@ -4340,48 +4691,45 @@ msgstr "hace %s años"
msgid "Timeago|%s years remaining"
msgstr "%s años restantes"
+msgid "Timeago|1 day ago"
+msgstr ""
+
msgid "Timeago|1 day remaining"
msgstr "1 día restante"
+msgid "Timeago|1 hour ago"
+msgstr ""
+
msgid "Timeago|1 hour remaining"
msgstr "1 hora restante"
+msgid "Timeago|1 minute ago"
+msgstr ""
+
msgid "Timeago|1 minute remaining"
msgstr "1 minuto restante"
+msgid "Timeago|1 month ago"
+msgstr ""
+
msgid "Timeago|1 month remaining"
msgstr "1 mes restante"
+msgid "Timeago|1 week ago"
+msgstr ""
+
msgid "Timeago|1 week remaining"
msgstr "1 semana restante"
+msgid "Timeago|1 year ago"
+msgstr ""
+
msgid "Timeago|1 year remaining"
msgstr "1 año restante"
msgid "Timeago|Past due"
msgstr "Atrasado"
-msgid "Timeago|a day ago"
-msgstr "hace un día"
-
-msgid "Timeago|a month ago"
-msgstr "hace un mes"
-
-msgid "Timeago|a week ago"
-msgstr "hace una semana"
-
-msgid "Timeago|a year ago"
-msgstr "hace un año"
-
-msgid "Timeago|about %s hours ago"
-msgstr "hace alrededor de %s horas"
-
-msgid "Timeago|about a minute ago"
-msgstr "hace alrededor de 1 minuto"
-
-msgid "Timeago|about an hour ago"
-msgstr "hace alrededor de 1 hora"
-
msgid "Timeago|in %s days"
msgstr "en %s días"
@@ -4421,11 +4769,14 @@ msgstr "en 1 semana"
msgid "Timeago|in 1 year"
msgstr "en 1 año"
-msgid "Timeago|in a while"
+msgid "Timeago|just now"
msgstr ""
-msgid "Timeago|less than a minute ago"
-msgstr "hace menos de 1 minuto"
+msgid "Timeago|right now"
+msgstr ""
+
+msgid "Timeout"
+msgstr ""
msgid "Time|hr"
msgid_plural "Time|hrs"
@@ -4443,19 +4794,10 @@ msgstr "s"
msgid "Tip:"
msgstr ""
-msgid "Title"
-msgstr "Título"
-
msgid "To GitLab"
msgstr ""
-msgid "To connect GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to connect."
-msgstr ""
-
-msgid "To connect GitHub repositories, you first need to authorize GitLab to access the list of your GitHub repositories:"
-msgstr ""
-
-msgid "To connect an SVN repository, check out %{svn_link}."
+msgid "To add an SSH key you need to %{generate_link_start}generate one%{link_end} or use an %{existing_link_start}existing key%{link_end}."
msgstr ""
msgid "To import GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
@@ -4467,19 +4809,19 @@ msgstr ""
msgid "To import an SVN repository, check out %{svn_link}."
msgstr ""
-msgid "To only use CI/CD features for an external repository, choose <strong>CI/CD for external repo</strong>."
+msgid "To start serving your jobs you can add Runners to your group"
msgstr ""
-msgid "To set up SAML authentication for your group through an identity provider like Azure, Okta, Onelogin, Ping Identity, or your custom SAML 2.0 provider:"
+msgid "To validate your GitLab CI configurations, go to 'CI/CD → Pipelines' inside your project, and click on the 'CI Lint' button."
msgstr ""
-msgid "To validate your GitLab CI configurations, go to 'CI/CD → Pipelines' inside your project, and click on the 'CI Lint' button."
+msgid "Todo"
msgstr ""
-msgid "To view the roadmap, add a planned start or finish date to one of your epics in this group or its subgroups. Only epics in the past 3 months and the next 3 months are shown."
+msgid "Toggle Sidebar"
msgstr ""
-msgid "Todo"
+msgid "Toggle discussion"
msgstr ""
msgid "Toggle sidebar"
@@ -4491,6 +4833,9 @@ msgstr ""
msgid "ToggleButton|Toggle Status: ON"
msgstr ""
+msgid "Too many changes to show."
+msgstr ""
+
msgid "Total Time"
msgstr "Tiempo Total"
@@ -4500,22 +4845,19 @@ msgstr "Tiempo total de pruebas para todos los cambios o integraciones"
msgid "Total: %{total}"
msgstr ""
-msgid "Track activity with Contribution Analytics."
-msgstr ""
-
-msgid "Track groups of issues that share a theme, across projects and milestones"
-msgstr ""
-
msgid "Track time with quick actions"
msgstr ""
msgid "Trigger this manual action"
msgstr ""
-msgid "Turn on Service Desk"
+msgid "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."
+msgstr ""
+
+msgid "Try again"
msgstr ""
-msgid "Unknown"
+msgid "Unable to load the diff. %{button_try_again}"
msgstr ""
msgid "Unlock"
@@ -4527,25 +4869,45 @@ msgstr "Desbloqueado"
msgid "Unresolve discussion"
msgstr ""
+msgid "Unstage all changes"
+msgstr ""
+
+msgid "Unstage changes"
+msgstr ""
+
+msgid "Unstaged"
+msgstr ""
+
+msgid "Unstaged %{type}"
+msgstr ""
+
+msgid "Unstaged and staged %{type}"
+msgstr ""
+
msgid "Unstar"
msgstr "No Destacar"
-msgid "Up to date"
+msgid "Unsubscribe"
msgstr ""
-msgid "Upgrade your plan to activate Advanced Global Search."
+msgid "Unsubscribe at group level"
msgstr ""
-msgid "Upgrade your plan to activate Contribution Analytics."
+msgid "Unsubscribe at project level"
msgstr ""
-msgid "Upgrade your plan to activate Group Webhooks."
+msgid "Unverified"
msgstr ""
-msgid "Upgrade your plan to activate Issue weight."
+msgid "Up to date"
msgstr ""
-msgid "Upgrade your plan to improve Issue boards."
+msgid "Update %{files}"
+msgid_plural "Update %{files} files"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Update your group name, description, avatar, and other general settings."
msgstr ""
msgid "Upload New File"
@@ -4566,7 +4928,7 @@ msgstr ""
msgid "Usage statistics"
msgstr ""
-msgid "Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab"
+msgid "Use group milestones to manage issues from multiple projects in the same milestone."
msgstr ""
msgid "Use the following registration token during setup:"
@@ -4575,10 +4937,13 @@ msgstr ""
msgid "Use your global notification setting"
msgstr "Utiliza tu configuración de notificación global"
-msgid "Used by members to sign in to your group in GitLab"
+msgid "User and IP Rate Limits"
msgstr ""
-msgid "User and IP Rate Limits"
+msgid "Users"
+msgstr ""
+
+msgid "Variables"
msgstr ""
msgid "Variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. You can use variables for passwords, secret keys, or whatever you want."
@@ -4593,10 +4958,7 @@ msgstr ""
msgid "Various settings that affect GitLab performance."
msgstr ""
-msgid "View and edit lines"
-msgstr ""
-
-msgid "View epics list"
+msgid "Verified"
msgstr ""
msgid "View file @ "
@@ -4605,9 +4967,15 @@ msgstr "Ver archivo @ "
msgid "View group labels"
msgstr ""
+msgid "View jobs"
+msgstr ""
+
msgid "View labels"
msgstr ""
+msgid "View log"
+msgstr ""
+
msgid "View open merge request"
msgstr "Ver solicitud de fusión abierta"
@@ -4635,9 +5003,6 @@ msgstr "Desconocido"
msgid "Want to see the data? Please ask an administrator for access."
msgstr "¿Quieres ver los datos? Por favor pide acceso al administrador."
-msgid "We could not verify that one of your projects on GCP has billing enabled. Please try again."
-msgstr ""
-
msgid "We don't have enough data to show this stage."
msgstr "No hay suficientes datos para mostrar en esta etapa."
@@ -4650,13 +5015,10 @@ msgstr ""
msgid "Web terminal"
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."
+msgid "When a runner is locked, it cannot be assigned to other projects"
msgstr ""
-msgid "Weight"
-msgstr "Peso"
-
-msgid "When leaving the URL blank, classification labels can still be specified whitout disabling cross project features or performing external authorization checks."
+msgid "When enabled, users cannot use GitLab until the terms have been accepted."
msgstr ""
msgid "Wiki"
@@ -4683,7 +5045,31 @@ msgstr ""
msgid "WikiEdit|There is already a page with the same title in that path."
msgstr ""
-msgid "WikiEmptyPageError|You are not allowed to create wiki pages"
+msgid "WikiEmptyIssueMessage|Suggest wiki improvement"
+msgstr ""
+
+msgid "WikiEmptyIssueMessage|You must be a project member in order to add wiki pages. If you have suggestions for how to improve the wiki for this project, consider opening an issue in the %{issues_link}."
+msgstr ""
+
+msgid "WikiEmptyIssueMessage|issue tracker"
+msgstr ""
+
+msgid "WikiEmpty|A wiki is where you can store all the details about your project. This can include why you've created it, its principles, how to use it, and so on."
+msgstr ""
+
+msgid "WikiEmpty|Create your first page"
+msgstr ""
+
+msgid "WikiEmpty|Suggest wiki improvement"
+msgstr ""
+
+msgid "WikiEmpty|The wiki lets you write documentation for your project"
+msgstr ""
+
+msgid "WikiEmpty|This project has no wiki pages"
+msgstr ""
+
+msgid "WikiEmpty|You must be a project member in order to add wiki pages."
msgstr ""
msgid "WikiHistoricalPage|This is an old version of this page."
@@ -4719,6 +5105,12 @@ msgstr ""
msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?"
msgstr ""
+msgid "WikiPageConfirmDelete|Delete page"
+msgstr ""
+
+msgid "WikiPageConfirmDelete|Delete page %{pageTitle}?"
+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 ""
@@ -4734,7 +5126,7 @@ msgstr ""
msgid "WikiPage|Page slug"
msgstr ""
-msgid "WikiPage|Write your content or drag files here..."
+msgid "WikiPage|Write your content or drag files here…"
msgstr ""
msgid "Wiki|Create Page"
@@ -4746,9 +5138,6 @@ msgstr ""
msgid "Wiki|Edit Page"
msgstr ""
-msgid "Wiki|Empty page"
-msgstr ""
-
msgid "Wiki|More Pages"
msgstr ""
@@ -4767,13 +5156,10 @@ 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"
-msgid "Write a commit message..."
+msgid "Yes"
msgstr ""
msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
@@ -4791,7 +5177,7 @@ msgstr ""
msgid "You are on a read-only GitLab instance."
msgstr ""
-msgid "You are on a secondary (read-only) Geo node. If you want to make any changes, you must visit the %{primary_node}."
+msgid "You can %{linkStart}view the blob%{linkEnd} instead."
msgstr ""
msgid "You can also create a project from the command line."
@@ -4800,6 +5186,9 @@ msgstr ""
msgid "You can also star a label to make it a priority label."
msgstr ""
+msgid "You can also test your .gitlab-ci.yml in the %{linkStart}Lint%{linkEnd}"
+msgstr ""
+
msgid "You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}"
msgstr ""
@@ -4812,30 +5201,33 @@ msgstr "Solo puedes agregar archivos cuando estás en una rama"
msgid "You can only edit 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."
+msgid "You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}"
msgstr ""
msgid "You cannot write to this read-only GitLab instance."
msgstr ""
-msgid "You do not have the correct permissions to override the settings from the LDAP group sync."
+msgid "You do not have any assigned merge requests"
msgstr ""
msgid "You have no permissions"
msgstr ""
+msgid "You have not created any merge requests"
+msgstr ""
+
msgid "You have reached your project limit"
msgstr "Has alcanzado el límite de tu proyecto"
-msgid "You must have master access to force delete a lock"
+msgid "You must accept our Terms of Service and privacy policy in order to register an account"
+msgstr ""
+
+msgid "You must have maintainer access to force delete a lock"
msgstr ""
msgid "You must sign in to star a project"
msgstr "Debes iniciar sesión para destacar un proyecto"
-msgid "You need a different license to enable FileLocks feature"
-msgstr ""
-
msgid "You need permission."
msgstr "Necesitas permisos."
@@ -4902,13 +5294,11 @@ msgstr "Tu nombre"
msgid "Your projects"
msgstr "Tus proyectos"
-msgid "among other things"
+msgid "ago"
msgstr ""
-msgid "and %d fixed vulnerability"
-msgid_plural "and %d fixed vulnerabilities"
-msgstr[0] ""
-msgstr[1] ""
+msgid "among other things"
+msgstr ""
msgid "assign yourself"
msgstr ""
@@ -4916,136 +5306,36 @@ msgstr ""
msgid "branch name"
msgstr "nombre de la rama"
-msgid "by"
-msgstr "por"
-
-msgid "ciReport|%{type} detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|%{type} detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Code quality"
-msgstr ""
-
-msgid "ciReport|DAST detected no alerts by analyzing the review app"
-msgstr ""
-
-msgid "ciReport|Dependency scanning"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Failed to load %{reportName} report"
-msgstr ""
-
-msgid "ciReport|Fixed:"
-msgstr ""
-
-msgid "ciReport|Instances"
-msgstr ""
-
-msgid "ciReport|Learn more about whitelisting"
-msgstr ""
-
-msgid "ciReport|Loading %{reportName} report"
-msgstr ""
-
-msgid "ciReport|No changes to code quality"
-msgstr ""
-
-msgid "ciReport|No changes to performance metrics"
-msgstr ""
-
-msgid "ciReport|Performance metrics"
-msgstr ""
-
-msgid "ciReport|SAST"
-msgstr ""
-
-msgid "ciReport|SAST detected"
-msgstr ""
-
-msgid "ciReport|SAST detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|SAST detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|SAST:container no vulnerabilities were found"
-msgstr ""
-
-msgid "ciReport|Security scanning"
-msgstr ""
-
-msgid "ciReport|Security scanning failed loading any results"
-msgstr ""
-
-msgid "ciReport|Show complete code vulnerabilities report"
-msgstr ""
-
-msgid "ciReport|Unapproved vulnerabilities (red) can be marked as approved. %{helpLink}"
-msgstr ""
-
-msgid "ciReport|no vulnerabilities"
-msgstr ""
-
msgid "command line instructions"
msgstr ""
msgid "connecting"
msgstr ""
-msgid "could not read private key, is the passphrase correct?"
-msgstr ""
-
msgid "day"
msgid_plural "days"
msgstr[0] "día"
msgstr[1] "días"
-msgid "detected %d fixed vulnerability"
-msgid_plural "detected %d fixed vulnerabilities"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "detected %d new vulnerability"
-msgid_plural "detected %d new vulnerabilities"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "detected no vulnerabilities"
-msgstr ""
-
-msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
-msgstr ""
-
-msgid "here"
+msgid "deploy token"
msgstr ""
-msgid "importing"
+msgid "disabled"
msgstr ""
-msgid "in progress"
+msgid "enabled"
msgstr ""
-msgid "is invalid because there is downstream lock"
+msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
msgstr ""
-msgid "is invalid because there is upstream lock"
+msgid "for this project"
msgstr ""
-msgid "is not a valid X509 certificate."
+msgid "importing"
msgstr ""
-msgid "locked by %{path_lock_user_name} %{created_at}"
+msgid "latest version"
msgstr ""
msgid "merge request"
@@ -5065,28 +5355,7 @@ msgstr ""
msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage is %{emphasisStart} unchanged %{emphasisEnd} at %{memoryFrom}MB"
msgstr ""
-msgid "mrWidget|Add approval"
-msgstr ""
-
-msgid "mrWidget|Allows edits from maintainers"
-msgstr ""
-
-msgid "mrWidget|An error occured while removing your approval."
-msgstr ""
-
-msgid "mrWidget|An error occured while retrieving approval data for this merge request."
-msgstr ""
-
-msgid "mrWidget|An error occured while submitting your approval."
-msgstr ""
-
-msgid "mrWidget|Approve"
-msgstr ""
-
-msgid "mrWidget|Approved"
-msgstr ""
-
-msgid "mrWidget|Approved by"
+msgid "mrWidget|Allows commits from members who can merge to the target branch"
msgstr ""
msgid "mrWidget|Cancel automatic merge"
@@ -5113,6 +5382,9 @@ msgstr ""
msgid "mrWidget|Closes"
msgstr ""
+msgid "mrWidget|Create an issue to resolve them later"
+msgstr ""
+
msgid "mrWidget|Deployment statistics are not available currently"
msgstr ""
@@ -5167,9 +5439,6 @@ msgstr ""
msgid "mrWidget|Remove source branch"
msgstr ""
-msgid "mrWidget|Remove your approval"
-msgstr ""
-
msgid "mrWidget|Request to merge"
msgstr ""
@@ -5209,6 +5478,9 @@ msgstr ""
msgid "mrWidget|There are merge conflicts"
msgstr ""
+msgid "mrWidget|There are unresolved discussions. Please resolve these discussions"
+msgstr ""
+
msgid "mrWidget|This merge request failed to be merged automatically"
msgstr ""
@@ -5259,7 +5531,7 @@ msgstr "contraseña"
msgid "personal access token"
msgstr "token de acceso personal"
-msgid "private key does not match certificate."
+msgid "remaining"
msgstr ""
msgid "remove due date"
@@ -5274,9 +5546,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "to help your contributors communicate effectively!"
-msgstr "para ayudar a sus colaboradores a comunicarse de manera efectiva!"
-
msgid "username"
msgstr "usuario"
@@ -5286,3 +5555,8 @@ msgstr ""
msgid "with %{additions} additions, %{deletions} deletions."
msgstr ""
+msgid "within %d minute "
+msgid_plural "within %d minutes "
+msgstr[0] ""
+msgstr[1] ""
+
diff --git a/locale/fil_PH/gitlab.po b/locale/fil_PH/gitlab.po
index ed955f68381..77e7824cb8e 100644
--- a/locale/fil_PH/gitlab.po
+++ b/locale/fil_PH/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2018-04-04 19:35+0200\n"
-"PO-Revision-Date: 2018-04-05 03:34-0400\n"
+"POT-Creation-Date: 2018-07-01 16:35+1000\n"
+"PO-Revision-Date: 2018-07-01 11:01\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Filipino\n"
"Language: fil_PH\n"
@@ -16,8 +16,10 @@ msgstr ""
"X-Crowdin-Language: fil\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-msgid " and"
-msgstr ""
+msgid "%d changed file"
+msgid_plural "%d changed files"
+msgstr[0] ""
+msgstr[1] ""
msgid "%d commit"
msgid_plural "%d commits"
@@ -54,6 +56,16 @@ msgid_plural "%d metrics"
msgstr[0] ""
msgstr[1] ""
+msgid "%d staged change"
+msgid_plural "%d staged changes"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "%d unstaged change"
+msgid_plural "%d unstaged changes"
+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] ""
@@ -70,12 +82,21 @@ msgid_plural "%{count} participants"
msgstr[0] ""
msgstr[1] ""
+msgid "%{filePath} deleted"
+msgstr ""
+
+msgid "%{group_docs_link_start}Groups%{group_docs_link_end} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects."
+msgstr ""
+
msgid "%{loadingIcon} Started"
msgstr ""
msgid "%{lock_path} is locked by GitLab User %{lock_user_id}"
msgstr ""
+msgid "%{nip_domain} can be used as an alternative to a custom domain."
+msgstr ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -88,6 +109,9 @@ msgstr ""
msgid "%{openOrClose} %{noteable}"
msgstr ""
+msgid "%{percent}%% complete"
+msgstr ""
+
msgid "%{storage_name}: failed storage access attempt on host:"
msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:"
msgstr[0] ""
@@ -96,15 +120,62 @@ msgstr[1] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{title} changes"
+msgstr ""
+
+msgid "%{unstaged} unstaged and %{staged} staged changes"
+msgstr ""
+
msgid "(checkout the %{link} for information on how to install it)."
msgstr ""
msgid "+ %{moreCount} more"
msgstr ""
+msgid "- Runner is active and can process any new jobs"
+msgstr ""
+
+msgid "- Runner is paused and will not receive any new jobs"
+msgstr ""
+
msgid "- show less"
msgstr ""
+msgid "1 %{type} addition"
+msgid_plural "%{count} %{type} additions"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 %{type} modification"
+msgid_plural "%{count} %{type} modifications"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 closed issue"
+msgid_plural "%d closed issues"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 closed merge request"
+msgid_plural "%d closed merge requests"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 merged merge request"
+msgid_plural "%d merged merge requests"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 open issue"
+msgid_plural "%d open issues"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 open merge request"
+msgid_plural "%d open merge requests"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] ""
@@ -116,9 +187,27 @@ msgstr ""
msgid "2FA enabled"
msgstr ""
+msgid "403|Please contact your GitLab administrator to get the permission."
+msgstr ""
+
+msgid "403|You don't have the permission to access this page."
+msgstr ""
+
+msgid "404|Make sure the address is correct and the page hasn't moved."
+msgstr ""
+
+msgid "404|Page Not Found"
+msgstr ""
+
+msgid "404|Please contact your GitLab administrator if you think this is a mistake."
+msgstr ""
+
msgid "<strong>Removes</strong> source branch"
msgstr ""
+msgid "A 'Runner' is a process which runs a job. You can setup as many Runners as you need."
+msgstr ""
+
msgid "A collection of graphs regarding Continuous Integration"
msgstr ""
@@ -128,6 +217,9 @@ msgstr ""
msgid "A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}."
msgstr ""
+msgid "A regular expression that will be used to find the test coverage output in the job trace. Leave blank to disable"
+msgstr ""
+
msgid "A user with write access to the source branch selected this option"
msgstr ""
@@ -140,25 +232,31 @@ msgstr ""
msgid "Abuse reports"
msgstr ""
+msgid "Accept terms"
+msgstr ""
+
msgid "Access Tokens"
msgstr ""
msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again."
msgstr ""
+msgid "Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report."
+msgstr ""
+
msgid "Account"
msgstr ""
-msgid "Account and limit settings"
+msgid "Account and limit"
msgstr ""
msgid "Active"
msgstr ""
-msgid "Activity"
+msgid "Active Sessions"
msgstr ""
-msgid "Add"
+msgid "Activity"
msgstr ""
msgid "Add Changelog"
@@ -167,9 +265,6 @@ msgstr ""
msgid "Add Contribution guide"
msgstr ""
-msgid "Add Group Webhooks and GitLab Enterprise Edition."
-msgstr ""
-
msgid "Add Kubernetes cluster"
msgstr ""
@@ -182,6 +277,9 @@ msgstr ""
msgid "Add new directory"
msgstr ""
+msgid "Add reaction"
+msgstr ""
+
msgid "Add todo"
msgstr ""
@@ -251,7 +349,10 @@ msgstr ""
msgid "All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings."
msgstr ""
-msgid "Allow edits from maintainers."
+msgid "Allow commits from members who can merge to the target branch."
+msgstr ""
+
+msgid "Allow public access to pipelines and job details, including output logs and artifacts"
msgstr ""
msgid "Allow rendering of PlantUML diagrams in Asciidoc documents."
@@ -263,31 +364,37 @@ msgstr ""
msgid "Allows you to add and manage Kubernetes clusters."
msgstr ""
-msgid "Also called \"Issuer\" or \"Relying party trust identifier\""
+msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
msgstr ""
-msgid "Also called \"Relying party service URL\" or \"Reply URL\""
+msgid "An error occured creating the new branch."
msgstr ""
-msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to connect."
+msgid "An error occured whilst loading all the files."
msgstr ""
-msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
+msgid "An error occured whilst loading the file content."
msgstr ""
-msgid "An error occurred previewing the blob"
+msgid "An error occured whilst loading the file."
msgstr ""
-msgid "An error occurred when toggling the notification subscription"
+msgid "An error occured whilst loading the merge request changes."
msgstr ""
-msgid "An error occurred when updating the issue weight"
+msgid "An error occured whilst loading the merge request version data."
msgstr ""
-msgid "An error occurred while adding approver"
+msgid "An error occured whilst loading the merge request."
msgstr ""
-msgid "An error occurred while detecting host keys"
+msgid "An error occurred previewing the blob"
+msgstr ""
+
+msgid "An error occurred when toggling the notification subscription"
+msgstr ""
+
+msgid "An error occurred while dismissing the alert. Refresh the page and try again."
msgstr ""
msgid "An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again."
@@ -305,10 +412,7 @@ msgstr ""
msgid "An error occurred while getting projects"
msgstr ""
-msgid "An error occurred while importing project"
-msgstr ""
-
-msgid "An error occurred while initializing path locks"
+msgid "An error occurred while importing project: ${details}"
msgstr ""
msgid "An error occurred while loading commits"
@@ -326,9 +430,6 @@ msgstr ""
msgid "An error occurred while making the request."
msgstr ""
-msgid "An error occurred while removing approver"
-msgstr ""
-
msgid "An error occurred while rendering KaTeX"
msgstr ""
@@ -341,9 +442,6 @@ msgstr ""
msgid "An error occurred while retrieving diff"
msgstr ""
-msgid "An error occurred while saving LDAP override status. Please try again."
-msgstr ""
-
msgid "An error occurred while saving assignees"
msgstr ""
@@ -353,9 +451,6 @@ msgstr ""
msgid "An error occurred. Please try again."
msgstr ""
-msgid "Any Label"
-msgstr ""
-
msgid "Appearance"
msgstr ""
@@ -368,19 +463,19 @@ msgstr ""
msgid "April"
msgstr ""
-msgid "Archived project! Repository is read-only"
+msgid "Archived project! Repository and other project resources are read-only"
msgstr ""
msgid "Are you sure you want to delete this pipeline schedule?"
msgstr ""
-msgid "Are you sure you want to reset registration token?"
+msgid "Are you sure you want to remove this identity?"
msgstr ""
-msgid "Are you sure you want to reset the health check token?"
+msgid "Are you sure you want to reset registration token?"
msgstr ""
-msgid "Are you sure you want to unlock %{path_lock_path}?"
+msgid "Are you sure you want to reset the health check token?"
msgstr ""
msgid "Are you sure?"
@@ -389,7 +484,7 @@ msgstr ""
msgid "Artifacts"
msgstr ""
-msgid "Assertion consumer service URL"
+msgid "Ask your group maintainer to setup a group Runner."
msgstr ""
msgid "Assign custom color like #FF0000"
@@ -413,9 +508,15 @@ msgstr ""
msgid "Assigned to :name"
msgstr ""
+msgid "Assigned to me"
+msgstr ""
+
msgid "Assignee"
msgstr ""
+msgid "Assignee(s)"
+msgstr ""
+
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr ""
@@ -449,7 +550,10 @@ msgstr ""
msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
msgstr ""
-msgid "AutoDevOps|Auto DevOps (Beta)"
+msgid "Auto-cancel redundant, pending pipelines"
+msgstr ""
+
+msgid "AutoDevOps|Auto DevOps"
msgstr ""
msgid "AutoDevOps|Auto DevOps documentation"
@@ -470,79 +574,109 @@ msgstr ""
msgid "AutoDevOps|add a Kubernetes cluster"
msgstr ""
-msgid "AutoDevOps|enable Auto DevOps (Beta)"
+msgid "AutoDevOps|enable Auto DevOps"
msgstr ""
msgid "Available"
msgstr ""
+msgid "Available group Runners : %{runners}"
+msgstr ""
+
+msgid "Available group Runners : %{runners}."
+msgstr ""
+
msgid "Avatar will be removed. Are you sure?"
msgstr ""
msgid "Average per day: %{average}"
msgstr ""
-msgid "Background Color"
+msgid "Background color"
msgstr ""
msgid "Background jobs"
msgstr ""
-msgid "Begin with the selected commit"
+msgid "Badges"
+msgstr ""
+
+msgid "Badges|A new badge was added."
+msgstr ""
+
+msgid "Badges|Add badge"
+msgstr ""
+
+msgid "Badges|Adding the badge failed, please check the entered URLs and try again."
+msgstr ""
+
+msgid "Badges|Badge image URL"
+msgstr ""
+
+msgid "Badges|Badge image preview"
+msgstr ""
+
+msgid "Badges|Delete badge"
+msgstr ""
+
+msgid "Badges|Delete badge?"
+msgstr ""
+
+msgid "Badges|Deleting the badge failed, please try again."
msgstr ""
-msgid "Billing"
+msgid "Badges|Group Badge"
msgstr ""
-msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan."
+msgid "Badges|Link"
msgstr ""
-msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available."
+msgid "Badges|No badge image"
msgstr ""
-msgid "BillingPlans|Current plan"
+msgid "Badges|No image to preview"
msgstr ""
-msgid "BillingPlans|Customer Support"
+msgid "Badges|Project Badge"
msgstr ""
-msgid "BillingPlans|Downgrade"
+msgid "Badges|Reload badge image"
msgstr ""
-msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
+msgid "Badges|Save changes"
msgstr ""
-msgid "BillingPlans|Manage plan"
+msgid "Badges|Saving the badge failed, please check the entered URLs and try again."
msgstr ""
-msgid "BillingPlans|Please contact %{customer_support_link} in that case."
+msgid "Badges|The %{docsLinkStart}variables%{docsLinkEnd} GitLab supports: %{placeholders}"
msgstr ""
-msgid "BillingPlans|See all %{plan_name} features"
+msgid "Badges|The badge was deleted."
msgstr ""
-msgid "BillingPlans|This group uses the plan associated with its parent group."
+msgid "Badges|The badge was saved."
msgstr ""
-msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
+msgid "Badges|This group has no badges"
msgstr ""
-msgid "BillingPlans|Upgrade"
+msgid "Badges|This project has no badges"
msgstr ""
-msgid "BillingPlans|You are currently on the %{plan_link} plan."
+msgid "Badges|Your badges"
msgstr ""
-msgid "BillingPlans|frequently asked questions"
+msgid "Begin with the selected commit"
msgstr ""
-msgid "BillingPlans|monthly"
+msgid "Below are examples of regex for existing tools:"
msgstr ""
-msgid "BillingPlans|paid annually at %{price_per_year}"
+msgid "Boards"
msgstr ""
-msgid "BillingPlans|per user"
+msgid "Branch %{branchName} was not found in this project's repository."
msgstr ""
msgid "Branch (%{branch_count})"
@@ -622,7 +756,7 @@ msgstr ""
msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
msgstr ""
-msgid "Branches|Only a project master or owner can delete a protected branch"
+msgid "Branches|Only a project maintainer or owner can delete a protected branch"
msgstr ""
msgid "Branches|Overview"
@@ -658,9 +792,6 @@ msgstr ""
msgid "Branches|Stale branches"
msgstr ""
-msgid "Branches|The branch could not be updated automatically because it has diverged from its upstream counterpart."
-msgstr ""
-
msgid "Branches|The default branch cannot be deleted"
msgstr ""
@@ -673,15 +804,9 @@ msgstr ""
msgid "Branches|To confirm, type %{branch_name_confirmation}:"
msgstr ""
-msgid "Branches|To discard the local changes and overwrite the branch with the upstream version, delete it here and choose 'Update Now' above."
-msgstr ""
-
msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
msgstr ""
-msgid "Branches|diverged from upstream"
-msgstr ""
-
msgid "Branches|merged"
msgstr ""
@@ -703,40 +828,79 @@ msgstr ""
msgid "Browse files"
msgstr ""
-msgid "Business"
-msgstr ""
-
msgid "ByAuthor|by"
msgstr ""
msgid "CI / CD"
msgstr ""
-msgid "CI/CD"
+msgid "CI / CD Settings"
msgstr ""
msgid "CI/CD configuration"
msgstr ""
-msgid "CI/CD for external repo"
+msgid "CI/CD settings"
+msgstr ""
+
+msgid "CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery."
+msgstr ""
+
+msgid "CICD|Auto DevOps"
+msgstr ""
+
+msgid "CICD|Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration."
+msgstr ""
+
+msgid "CICD|Automatic deployment to staging, manual deployment to production"
+msgstr ""
+
+msgid "CICD|Continuous deployment to production"
+msgstr ""
+
+msgid "CICD|Deployment strategy"
+msgstr ""
+
+msgid "CICD|Deployment strategy needs a domain name to work correctly."
+msgstr ""
+
+msgid "CICD|Disable Auto DevOps"
+msgstr ""
+
+msgid "CICD|Enable Auto DevOps"
+msgstr ""
+
+msgid "CICD|Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific %{ci_file}."
+msgstr ""
+
+msgid "CICD|Instance default (%{state})"
msgstr ""
msgid "CICD|Jobs"
msgstr ""
-msgid "Cancel"
+msgid "CICD|Learn more about Auto DevOps"
msgstr ""
-msgid "Cannot be merged automatically"
+msgid "CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project."
msgstr ""
-msgid "Cannot modify managed Kubernetes cluster"
+msgid "CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages."
msgstr ""
-msgid "Certificate fingerprint"
+msgid "Can't find HEAD commit for this branch"
msgstr ""
-msgid "Change Weight"
+msgid "Cancel"
+msgstr ""
+
+msgid "Cancel this job"
+msgstr ""
+
+msgid "Cannot be merged automatically"
+msgstr ""
+
+msgid "Cannot modify managed Kubernetes cluster"
msgstr ""
msgid "Change this value to influence how frequently the GitLab UI polls for updates."
@@ -790,21 +954,18 @@ msgstr ""
msgid "Choose a branch/tag (e.g. %{master}) or enter a commit (e.g. %{sha}) to see what's changed or to create a merge request."
msgstr ""
-msgid "Choose file..."
+msgid "Choose any color."
msgstr ""
-msgid "Choose which groups you wish to synchronize to this secondary node."
+msgid "Choose between <code>clone</code> or <code>fetch</code> to get the recent application code"
msgstr ""
-msgid "Choose which repositories you want to connect and run CI/CD pipelines."
+msgid "Choose file..."
msgstr ""
msgid "Choose which repositories you want to import."
msgstr ""
-msgid "Choose which shards you wish to synchronize to this secondary node."
-msgstr ""
-
msgid "CiStatusLabel|canceled"
msgstr ""
@@ -874,21 +1035,12 @@ msgstr ""
msgid "CiVariable|All environments"
msgstr ""
-msgid "CiVariable|Create wildcard"
-msgstr ""
-
msgid "CiVariable|Error occured while saving variables"
msgstr ""
-msgid "CiVariable|New environment"
-msgstr ""
-
msgid "CiVariable|Protected"
msgstr ""
-msgid "CiVariable|Search environments"
-msgstr ""
-
msgid "CiVariable|Toggle protected"
msgstr ""
@@ -898,28 +1050,28 @@ msgstr ""
msgid "CircuitBreakerApiLink|circuitbreaker api"
msgstr ""
-msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
+msgid "Clear search input"
msgstr ""
-msgid "Click to expand text"
+msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Client authentication certificate"
+msgid "Click the <strong>Promote</strong> button in the top right corner to promote it to a group milestone."
msgstr ""
-msgid "Client authentication key"
+msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
msgstr ""
-msgid "Client authentication key password"
+msgid "Click to expand it."
msgstr ""
-msgid "Clone repository"
+msgid "Click to expand text"
msgstr ""
-msgid "Close"
+msgid "Clone repository"
msgstr ""
-msgid "Closed"
+msgid "Close"
msgstr ""
msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster"
@@ -937,6 +1089,12 @@ msgstr ""
msgid "ClusterIntegration|Advanced options on this Kubernetes cluster's integration"
msgstr ""
+msgid "ClusterIntegration|An error occured while trying to fetch project zones: %{error}"
+msgstr ""
+
+msgid "ClusterIntegration|An error occured while trying to fetch your projects: %{error}"
+msgstr ""
+
msgid "ClusterIntegration|Applications"
msgstr ""
@@ -967,6 +1125,9 @@ msgstr ""
msgid "ClusterIntegration|Copy Ingress IP Address to clipboard"
msgstr ""
+msgid "ClusterIntegration|Copy Jupyter Hostname to clipboard"
+msgstr ""
+
msgid "ClusterIntegration|Copy Kubernetes cluster name"
msgstr ""
@@ -982,7 +1143,7 @@ msgstr ""
msgid "ClusterIntegration|Create a new Kubernetes cluster on Google Kubernetes Engine right from GitLab"
msgstr ""
-msgid "ClusterIntegration|Create on GKE"
+msgid "ClusterIntegration|Create on Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
@@ -994,13 +1155,25 @@ msgstr ""
msgid "ClusterIntegration|Environment scope"
msgstr ""
+msgid "ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab's Google Kubernetes Engine Integration."
+msgstr ""
+
+msgid "ClusterIntegration|Fetching machine types"
+msgstr ""
+
+msgid "ClusterIntegration|Fetching projects"
+msgstr ""
+
+msgid "ClusterIntegration|Fetching zones"
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
msgid "ClusterIntegration|GitLab Runner"
msgstr ""
-msgid "ClusterIntegration|Google Cloud Platform project ID"
+msgid "ClusterIntegration|Google Cloud Platform project"
msgstr ""
msgid "ClusterIntegration|Google Kubernetes Engine"
@@ -1012,9 +1185,6 @@ msgstr ""
msgid "ClusterIntegration|Helm Tiller"
msgstr ""
-msgid "ClusterIntegration|In order to show the health of the cluster, we'll need to provision your cluster with Prometheus to collect the required data."
-msgstr ""
-
msgid "ClusterIntegration|Ingress"
msgstr ""
@@ -1024,9 +1194,6 @@ msgstr ""
msgid "ClusterIntegration|Install"
msgstr ""
-msgid "ClusterIntegration|Install Prometheus"
-msgstr ""
-
msgid "ClusterIntegration|Installed"
msgstr ""
@@ -1039,13 +1206,16 @@ msgstr ""
msgid "ClusterIntegration|Integration status"
msgstr ""
-msgid "ClusterIntegration|Kubernetes cluster"
+msgid "ClusterIntegration|Jupyter Hostname"
msgstr ""
-msgid "ClusterIntegration|Kubernetes cluster details"
+msgid "ClusterIntegration|JupyterHub"
+msgstr ""
+
+msgid "ClusterIntegration|Kubernetes cluster"
msgstr ""
-msgid "ClusterIntegration|Kubernetes cluster health"
+msgid "ClusterIntegration|Kubernetes cluster details"
msgstr ""
msgid "ClusterIntegration|Kubernetes cluster integration"
@@ -1075,7 +1245,13 @@ msgstr ""
msgid "ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project"
msgstr ""
-msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
+msgid "ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}."
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{help_link_start}Kubernetes%{help_link_end}."
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}."
msgstr ""
msgid "ClusterIntegration|Learn more about environments"
@@ -1099,7 +1275,16 @@ msgstr ""
msgid "ClusterIntegration|More information"
msgstr ""
-msgid "ClusterIntegration|Multiple Kubernetes clusters are available in GitLab Enterprise Edition Premium and Ultimate"
+msgid "ClusterIntegration|No machine types matched your search"
+msgstr ""
+
+msgid "ClusterIntegration|No projects found"
+msgstr ""
+
+msgid "ClusterIntegration|No projects matched your search"
+msgstr ""
+
+msgid "ClusterIntegration|No zones matched your search"
msgstr ""
msgid "ClusterIntegration|Note:"
@@ -1114,9 +1299,6 @@ msgstr ""
msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
msgstr ""
-msgid "ClusterIntegration|Project ID"
-msgstr ""
-
msgid "ClusterIntegration|Project namespace"
msgstr ""
@@ -1129,6 +1311,9 @@ msgstr ""
msgid "ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration."
msgstr ""
+msgid "ClusterIntegration|Redeem up to $500 in free credit for Google Cloud Platform"
+msgstr ""
+
msgid "ClusterIntegration|Remove Kubernetes cluster integration"
msgstr ""
@@ -1144,19 +1329,37 @@ msgstr ""
msgid "ClusterIntegration|Save changes"
msgstr ""
+msgid "ClusterIntegration|Search machine types"
+msgstr ""
+
+msgid "ClusterIntegration|Search projects"
+msgstr ""
+
+msgid "ClusterIntegration|Search zones"
+msgstr ""
+
msgid "ClusterIntegration|Security"
msgstr ""
msgid "ClusterIntegration|See and edit the details for your Kubernetes cluster"
msgstr ""
-msgid "ClusterIntegration|See machine types"
+msgid "ClusterIntegration|Select machine type"
msgstr ""
-msgid "ClusterIntegration|See your projects"
+msgid "ClusterIntegration|Select project"
msgstr ""
-msgid "ClusterIntegration|See zones"
+msgid "ClusterIntegration|Select project and zone to choose machine type"
+msgstr ""
+
+msgid "ClusterIntegration|Select project to choose zone"
+msgstr ""
+
+msgid "ClusterIntegration|Select zone"
+msgstr ""
+
+msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
msgid "ClusterIntegration|Service token"
@@ -1189,6 +1392,9 @@ msgstr ""
msgid "ClusterIntegration|Token"
msgstr ""
+msgid "ClusterIntegration|Validating project billing status"
+msgstr ""
+
msgid "ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
@@ -1219,13 +1425,19 @@ msgstr ""
msgid "ClusterIntegration|properly configured"
msgstr ""
+msgid "ClusterIntegration|sign up"
+msgstr ""
+
msgid "Collapse"
msgstr ""
-msgid "Comment and resolve discussion"
+msgid "Collapse sidebar"
+msgstr ""
+
+msgid "Comment & resolve discussion"
msgstr ""
-msgid "Comment and unresolve discussion"
+msgid "Comment & unresolve discussion"
msgstr ""
msgid "Comments"
@@ -1292,6 +1504,9 @@ msgstr ""
msgid "Committed by"
msgstr ""
+msgid "Commit…"
+msgstr ""
+
msgid "Compare"
msgstr ""
@@ -1340,6 +1555,9 @@ msgstr ""
msgid "Configure limits for web and API requests."
msgstr ""
+msgid "Configure push mirrors."
+msgstr ""
+
msgid "Configure storage path and circuit breaker settings."
msgstr ""
@@ -1349,18 +1567,9 @@ msgstr ""
msgid "Connect"
msgstr ""
-msgid "Connect all repositories"
-msgstr ""
-
msgid "Connect repositories from GitHub"
msgstr ""
-msgid "Connect your external repositories, and CI/CD pipelines will run for new commits. A GitLab project will be created with only CI/CD features enabled."
-msgstr ""
-
-msgid "Connecting..."
-msgstr ""
-
msgid "Container Registry"
msgstr ""
@@ -1406,9 +1615,18 @@ 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 "ContainerRegistry|You can also use a %{deploy_token} for read-only access to the registry images."
+msgstr ""
+
+msgid "Continue"
+msgstr ""
+
msgid "Continuous Integration and Deployment"
msgstr ""
+msgid "Contribute to GitLab"
+msgstr ""
+
msgid "Contribution"
msgstr ""
@@ -1430,28 +1648,28 @@ msgstr ""
msgid "ContributorsPage|Please wait a moment, this page will automatically refresh when ready."
msgstr ""
-msgid "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
+msgid "Copy URL to clipboard"
msgstr ""
-msgid "Control the maximum concurrency of repository backfill for this secondary node"
+msgid "Copy branch name to clipboard"
msgstr ""
-msgid "Copy SSH public key to clipboard"
+msgid "Copy command to clipboard"
msgstr ""
-msgid "Copy URL to clipboard"
+msgid "Copy commit SHA to clipboard"
msgstr ""
-msgid "Copy branch name to clipboard"
+msgid "Copy file name to clipboard"
msgstr ""
-msgid "Copy command to clipboard"
+msgid "Copy file path to clipboard"
msgstr ""
-msgid "Copy commit SHA to clipboard"
+msgid "Copy reference to clipboard"
msgstr ""
-msgid "Copy reference to clipboard"
+msgid "Copy to clipboard"
msgstr ""
msgid "Create"
@@ -1472,13 +1690,13 @@ msgstr ""
msgid "Create branch"
msgstr ""
-msgid "Create directory"
+msgid "Create commit"
msgstr ""
-msgid "Create empty repository"
+msgid "Create directory"
msgstr ""
-msgid "Create epic"
+msgid "Create empty repository"
msgstr ""
msgid "Create file"
@@ -1523,13 +1741,10 @@ msgstr ""
msgid "CreateTokenToCloneLink|create a personal access token"
msgstr ""
-msgid "Creates a new branch from %{branchName}"
-msgstr ""
-
-msgid "Creates a new branch from %{branchName} and re-directs to create a new merge request"
+msgid "Created"
msgstr ""
-msgid "Creating epic"
+msgid "Created by me"
msgstr ""
msgid "Cron Timezone"
@@ -1538,16 +1753,19 @@ msgstr ""
msgid "Cron syntax"
msgstr ""
-msgid "Current node"
+msgid "CurrentUser|Profile"
msgstr ""
-msgid "Custom notification events"
+msgid "CurrentUser|Settings"
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}."
+msgid "Custom CI config path"
msgstr ""
-msgid "Customize colors"
+msgid "Custom notification events"
+msgstr ""
+
+msgid "Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notification_link}."
msgstr ""
msgid "Cycle Analytics"
@@ -1586,7 +1804,7 @@ msgstr ""
msgid "December"
msgstr ""
-msgid "Default classification label"
+msgid "Decline and sign out"
msgstr ""
msgid "Define a custom pattern with cron syntax"
@@ -1595,6 +1813,9 @@ msgstr ""
msgid "Delete"
msgstr ""
+msgid "Delete list"
+msgstr ""
+
msgid "Deploy"
msgid_plural "Deploys"
msgstr[0] ""
@@ -1603,549 +1824,525 @@ msgstr[1] ""
msgid "Deploy Keys"
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 "Diffs|No file name available"
-msgstr ""
-
-msgid "Directory name"
-msgstr ""
-
-msgid "Disable"
+msgid "DeployKeys|+%{count} others"
msgstr ""
-msgid "Discard draft"
+msgid "DeployKeys|Current project"
msgstr ""
-msgid "Discover GitLab Geo."
+msgid "DeployKeys|Deploy key"
msgstr ""
-msgid "Dismiss Cycle Analytics introduction box"
+msgid "DeployKeys|Enabled deploy keys"
msgstr ""
-msgid "Dismiss Merge Request promotion"
+msgid "DeployKeys|Error enabling deploy key"
msgstr ""
-msgid "Documentation for popular identity providers"
+msgid "DeployKeys|Error getting deploy keys"
msgstr ""
-msgid "Don't show again"
+msgid "DeployKeys|Error removing deploy key"
msgstr ""
-msgid "Done"
+msgid "DeployKeys|Expand %{count} other projects"
msgstr ""
-msgid "Download"
+msgid "DeployKeys|Loading deploy keys"
msgstr ""
-msgid "Download tar"
+msgid "DeployKeys|No deploy keys found. Create one with the form above."
msgstr ""
-msgid "Download tar.bz2"
-msgstr ""
-
-msgid "Download tar.gz"
+msgid "DeployKeys|Privately accessible deploy keys"
msgstr ""
-msgid "Download zip"
-msgstr ""
-
-msgid "DownloadArtifacts|Download"
+msgid "DeployKeys|Project usage"
msgstr ""
-msgid "DownloadCommit|Email Patches"
+msgid "DeployKeys|Publicly accessible deploy keys"
msgstr ""
-msgid "DownloadCommit|Plain Diff"
+msgid "DeployKeys|Read access only"
msgstr ""
-msgid "DownloadSource|Download"
+msgid "DeployKeys|Write access allowed"
msgstr ""
-msgid "Downvotes"
+msgid "DeployKeys|You are going to remove this deploy key. Are you sure?"
msgstr ""
-msgid "Due date"
+msgid "DeployTokens|Active Deploy Tokens (%{active_tokens})"
msgstr ""
-msgid "During this process, you’ll be asked for URLs from GitLab’s side. Use the URLs shown below."
+msgid "DeployTokens|Add a deploy token"
msgstr ""
-msgid "Edit"
+msgid "DeployTokens|Allows read-only access to the registry images"
msgstr ""
-msgid "Edit Pipeline Schedule %{id}"
+msgid "DeployTokens|Allows read-only access to the repository"
msgstr ""
-msgid "Edit files in the editor and commit changes here"
+msgid "DeployTokens|Copy deploy token to clipboard"
msgstr ""
-msgid "Editing"
+msgid "DeployTokens|Copy username to clipboard"
msgstr ""
-msgid "Elasticsearch"
+msgid "DeployTokens|Create deploy token"
msgstr ""
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "DeployTokens|Created"
msgstr ""
-msgid "Email"
+msgid "DeployTokens|Deploy Tokens"
msgstr ""
-msgid "Emails"
+msgid "DeployTokens|Deploy tokens allow read-only access to your repository and registry images."
msgstr ""
-msgid "Enable"
+msgid "DeployTokens|Expires"
msgstr ""
-msgid "Enable Auto DevOps"
+msgid "DeployTokens|Name"
msgstr ""
-msgid "Enable SAML authentication for this group"
+msgid "DeployTokens|Pick a name for the application, and we'll give you a unique deploy token."
msgstr ""
-msgid "Enable Sentry for error reporting and logging."
+msgid "DeployTokens|Revoke"
msgstr ""
-msgid "Enable and configure InfluxDB metrics."
+msgid "DeployTokens|Revoke %{name}"
msgstr ""
-msgid "Enable and configure Prometheus metrics."
+msgid "DeployTokens|Scopes"
msgstr ""
-msgid "Enable classification control using an external service"
+msgid "DeployTokens|This action cannot be undone."
msgstr ""
-msgid "Enable or disable version check and usage ping."
+msgid "DeployTokens|This project has no active Deploy Tokens."
msgstr ""
-msgid "Enable reCAPTCHA or Akismet and set IP limits."
+msgid "DeployTokens|Use this token as a password. Make sure you save it - you won't be able to access it again."
msgstr ""
-msgid "Enable the Performance Bar for a given group."
+msgid "DeployTokens|Use this username as a login."
msgstr ""
-msgid "Enabled"
+msgid "DeployTokens|Username"
msgstr ""
-msgid "Environments|An error occurred while fetching the environments."
+msgid "DeployTokens|You are about to revoke"
msgstr ""
-msgid "Environments|An error occurred while making the request."
+msgid "DeployTokens|Your New Deploy Token"
msgstr ""
-msgid "Environments|Commit"
+msgid "DeployTokens|Your new project deploy token has been created."
msgstr ""
-msgid "Environments|Deployment"
+msgid "Deprioritize label"
msgstr ""
-msgid "Environments|Environment"
+msgid "Description"
msgstr ""
-msgid "Environments|Environments"
+msgid "Details"
msgstr ""
-msgid "Environments|Job"
+msgid "Diffs|No file name available"
msgstr ""
-msgid "Environments|New environment"
+msgid "Diffs|Something went wrong while fetching diff lines."
msgstr ""
-msgid "Environments|No deployments yet"
+msgid "Directory name"
msgstr ""
-msgid "Environments|Open"
+msgid "Disable"
msgstr ""
-msgid "Environments|Re-deploy"
+msgid "Disable for this project"
msgstr ""
-msgid "Environments|Read more about environments"
+msgid "Disable group Runners"
msgstr ""
-msgid "Environments|Rollback"
+msgid "Discard changes"
msgstr ""
-msgid "Environments|Show all"
+msgid "Discard draft"
msgstr ""
-msgid "Environments|Updated"
+msgid "Dismiss Cycle Analytics introduction box"
msgstr ""
-msgid "Environments|You don't have any environments right now."
+msgid "Domain"
msgstr ""
-msgid "Epic will be removed! Are you sure?"
+msgid "Don't show again"
msgstr ""
-msgid "Epics"
+msgid "Done"
msgstr ""
-msgid "Epics Roadmap"
+msgid "Download"
msgstr ""
-msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
+msgid "Download tar"
msgstr ""
-msgid "Error Reporting and Logging"
+msgid "Download tar.bz2"
msgstr ""
-msgid "Error checking branch data. Please try again."
+msgid "Download tar.gz"
msgstr ""
-msgid "Error committing changes. Please try again."
+msgid "Download zip"
msgstr ""
-msgid "Error creating epic"
+msgid "DownloadArtifacts|Download"
msgstr ""
-msgid "Error fetching contributors data."
+msgid "DownloadCommit|Email Patches"
msgstr ""
-msgid "Error fetching labels."
+msgid "DownloadCommit|Plain Diff"
msgstr ""
-msgid "Error fetching network graph."
+msgid "DownloadSource|Download"
msgstr ""
-msgid "Error fetching refs"
+msgid "Downvotes"
msgstr ""
-msgid "Error fetching usage ping data."
+msgid "Due date"
msgstr ""
-msgid "Error occurred when toggling the notification subscription"
+msgid "Each Runner can be in one of the following states:"
msgstr ""
-msgid "Error saving label update."
+msgid "Edit"
msgstr ""
-msgid "Error updating status for all todos."
+msgid "Edit Label"
msgstr ""
-msgid "Error updating todo status."
+msgid "Edit Pipeline Schedule %{id}"
msgstr ""
-msgid "EventFilterBy|Filter by all"
+msgid "Edit files in the editor and commit changes here"
msgstr ""
-msgid "EventFilterBy|Filter by comments"
+msgid "Edit identity for %{user_name}"
msgstr ""
-msgid "EventFilterBy|Filter by issue events"
+msgid "Email"
msgstr ""
-msgid "EventFilterBy|Filter by merge events"
+msgid "Email patch"
msgstr ""
-msgid "EventFilterBy|Filter by push events"
+msgid "Emails"
msgstr ""
-msgid "EventFilterBy|Filter by team"
+msgid "Embed"
msgstr ""
-msgid "Every day (at 4:00am)"
+msgid "Enable"
msgstr ""
-msgid "Every month (on the 1st at 4:00am)"
+msgid "Enable Auto DevOps"
msgstr ""
-msgid "Every week (Sundays at 4:00am)"
+msgid "Enable Sentry for error reporting and logging."
msgstr ""
-msgid "Expand"
+msgid "Enable and configure InfluxDB metrics."
msgstr ""
-msgid "Explore projects"
+msgid "Enable and configure Prometheus metrics."
msgstr ""
-msgid "Explore public groups"
+msgid "Enable for this project"
msgstr ""
-msgid "External Classification Policy Authorization"
+msgid "Enable group Runners"
msgstr ""
-msgid "External authentication"
+msgid "Enable or disable certain group features and choose access levels."
msgstr ""
-msgid "External authorization denied access to this project"
+msgid "Enable or disable version check and usage ping."
msgstr ""
-msgid "External authorization request timeout"
+msgid "Enable reCAPTCHA or Akismet and set IP limits."
msgstr ""
-msgid "ExternalAuthorizationService|Classification Label"
+msgid "Enable the Performance Bar for a given group."
msgstr ""
-msgid "ExternalAuthorizationService|Classification label"
+msgid "Ends at (UTC)"
msgstr ""
-msgid "ExternalAuthorizationService|When no classification label is set the default label `%{default_label}` will be used."
+msgid "Environments"
msgstr ""
-msgid "Failed"
+msgid "Environments|An error occurred while fetching the environments."
msgstr ""
-msgid "Failed Jobs"
+msgid "Environments|An error occurred while making the request."
msgstr ""
-msgid "Failed to change the owner"
+msgid "Environments|Commit"
msgstr ""
-msgid "Failed to remove issue from board, please try again."
+msgid "Environments|Deployment"
msgstr ""
-msgid "Failed to remove the pipeline schedule"
+msgid "Environments|Environment"
msgstr ""
-msgid "Failed to update issues, please try again."
+msgid "Environments|Environments"
msgstr ""
-msgid "Feb"
+msgid "Environments|Job"
msgstr ""
-msgid "February"
+msgid "Environments|New environment"
msgstr ""
-msgid "Fields on this page are now uneditable, you can configure"
+msgid "Environments|No deployments yet"
msgstr ""
-msgid "File name"
+msgid "Environments|Open"
msgstr ""
-msgid "Files"
+msgid "Environments|Re-deploy"
msgstr ""
-msgid "Files (%{human_size})"
+msgid "Environments|Read more about environments"
msgstr ""
-msgid "Fill in the fields below, turn on <strong>%{enable_label}</strong>, and press <strong>%{save_changes}</strong>"
+msgid "Environments|Rollback"
msgstr ""
-msgid "Filter by commit message"
+msgid "Environments|Show all"
msgstr ""
-msgid "Find by path"
+msgid "Environments|Updated"
msgstr ""
-msgid "Find file"
+msgid "Environments|You don't have any environments right now."
msgstr ""
-msgid "Finished"
+msgid "Error Reporting and Logging"
msgstr ""
-msgid "FirstPushedBy|First"
+msgid "Error committing changes. Please try again."
msgstr ""
-msgid "FirstPushedBy|pushed by"
+msgid "Error fetching contributors data."
msgstr ""
-msgid "Font Color"
+msgid "Error fetching job trace"
msgstr ""
-msgid "Footer message"
+msgid "Error fetching labels."
msgstr ""
-msgid "Fork"
-msgid_plural "Forks"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "ForkedFromProjectPath|Forked from"
+msgid "Error fetching network graph."
msgstr ""
-msgid "ForkedFromProjectPath|Forked from %{project_name} (deleted)"
+msgid "Error fetching refs"
msgstr ""
-msgid "Forking in progress"
+msgid "Error fetching usage ping data."
msgstr ""
-msgid "Format"
+msgid "Error loading branch data. Please try again."
msgstr ""
-msgid "From %{provider_title}"
+msgid "Error loading last commit."
msgstr ""
-msgid "From issue creation until deploy to production"
+msgid "Error loading merge requests."
msgstr ""
-msgid "From merge request merge until deploy to production"
+msgid "Error loading project data. Please try again."
msgstr ""
-msgid "From the Kubernetes cluster details view, install Runner from the applications list"
+msgid "Error occurred when toggling the notification subscription"
msgstr ""
-msgid "GPG Keys"
+msgid "Error saving label update."
msgstr ""
-msgid "Generate a default set of labels"
+msgid "Error updating status for all todos."
msgstr ""
-msgid "Geo Nodes"
+msgid "Error updating todo status."
msgstr ""
-msgid "Geo allows you to replicate your GitLab instance to other geographical locations."
+msgid "Estimated"
msgstr ""
-msgid "GeoNodeSyncStatus|Node is failing or broken."
+msgid "EventFilterBy|Filter by all"
msgstr ""
-msgid "GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage."
+msgid "EventFilterBy|Filter by comments"
msgstr ""
-msgid "GeoNodes|Checksummed"
+msgid "EventFilterBy|Filter by issue events"
msgstr ""
-msgid "GeoNodes|Database replication lag:"
+msgid "EventFilterBy|Filter by merge events"
msgstr ""
-msgid "GeoNodes|Disabling a node stops the sync process. Are you sure?"
+msgid "EventFilterBy|Filter by push events"
msgstr ""
-msgid "GeoNodes|Does not match the primary storage configuration"
+msgid "EventFilterBy|Filter by team"
msgstr ""
-msgid "GeoNodes|Failed"
+msgid "Every day (at 4:00am)"
msgstr ""
-msgid "GeoNodes|Full"
+msgid "Every month (on the 1st at 4:00am)"
msgstr ""
-msgid "GeoNodes|GitLab version does not match the primary node version"
+msgid "Every week (Sundays at 4:00am)"
msgstr ""
-msgid "GeoNodes|GitLab version:"
+msgid "Expand"
msgstr ""
-msgid "GeoNodes|Health status:"
+msgid "Expand all"
msgstr ""
-msgid "GeoNodes|Last event ID processed by cursor:"
+msgid "Expand sidebar"
msgstr ""
-msgid "GeoNodes|Last event ID seen from primary:"
+msgid "Explore projects"
msgstr ""
-msgid "GeoNodes|Loading nodes"
+msgid "Explore public groups"
msgstr ""
-msgid "GeoNodes|Local Attachments:"
+msgid "Failed"
msgstr ""
-msgid "GeoNodes|Local LFS objects:"
+msgid "Failed Jobs"
msgstr ""
-msgid "GeoNodes|Local job artifacts:"
+msgid "Failed to change the owner"
msgstr ""
-msgid "GeoNodes|New node"
+msgid "Failed to check related branches."
msgstr ""
-msgid "GeoNodes|Node Authentication was successfully repaired."
+msgid "Failed to remove issue from board, please try again."
msgstr ""
-msgid "GeoNodes|Node was successfully removed."
+msgid "Failed to remove the pipeline schedule"
msgstr ""
-msgid "GeoNodes|Not checksummed"
+msgid "Failed to update issues, please try again."
msgstr ""
-msgid "GeoNodes|Out of sync"
+msgid "Failure"
msgstr ""
-msgid "GeoNodes|Removing a node stops the sync process. Are you sure?"
+msgid "Faster as it re-uses the project workspace (falling back to clone if it doesn't exist)"
msgstr ""
-msgid "GeoNodes|Replication slot WAL:"
+msgid "Feb"
msgstr ""
-msgid "GeoNodes|Replication slots:"
+msgid "February"
msgstr ""
-msgid "GeoNodes|Repositories checksummed:"
+msgid "Fields on this page are now uneditable, you can configure"
msgstr ""
-msgid "GeoNodes|Repositories:"
+msgid "Files"
msgstr ""
-msgid "GeoNodes|Repository checksums verified:"
+msgid "Files (%{human_size})"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "Filter by commit message"
msgstr ""
-msgid "GeoNodes|Something went wrong while changing node status"
+msgid "Find by path"
msgstr ""
-msgid "GeoNodes|Something went wrong while removing node"
+msgid "Find file"
msgstr ""
-msgid "GeoNodes|Something went wrong while repairing node"
+msgid "Finished"
msgstr ""
-msgid "GeoNodes|Storage config:"
+msgid "FirstPushedBy|First"
msgstr ""
-msgid "GeoNodes|Sync settings:"
+msgid "FirstPushedBy|pushed by"
msgstr ""
-msgid "GeoNodes|Synced"
+msgid "For internal projects, any logged in user can view pipelines and access job details (output logs and artifacts)"
msgstr ""
-msgid "GeoNodes|Unused slots"
+msgid "For private projects, any member (guest or higher) can view pipelines and access job details (output logs and artifacts)"
msgstr ""
-msgid "GeoNodes|Unverified"
+msgid "For public projects, anyone can view pipelines and access job details (output logs and artifacts)"
msgstr ""
-msgid "GeoNodes|Used slots"
-msgstr ""
+msgid "Fork"
+msgid_plural "Forks"
+msgstr[0] ""
+msgstr[1] ""
-msgid "GeoNodes|Verified"
+msgid "ForkedFromProjectPath|Forked from"
msgstr ""
-msgid "GeoNodes|Wiki checksums verified:"
+msgid "ForkedFromProjectPath|Forked from %{project_name} (deleted)"
msgstr ""
-msgid "GeoNodes|Wikis checksummed:"
+msgid "Forking in progress"
msgstr ""
-msgid "GeoNodes|Wikis:"
+msgid "Format"
msgstr ""
-msgid "GeoNodes|You have configured Geo nodes using an insecure HTTP connection. We recommend the use of HTTPS."
+msgid "Found errors in your .gitlab-ci.yml:"
msgstr ""
-msgid "Geo|All projects"
+msgid "From %{provider_title}"
msgstr ""
-msgid "Geo|File sync capacity"
+msgid "From issue creation until deploy to production"
msgstr ""
-msgid "Geo|Groups to synchronize"
+msgid "From merge request merge until deploy to production"
msgstr ""
-msgid "Geo|Projects in certain groups"
+msgid "From the Kubernetes cluster details view, install Runner from the applications list"
msgstr ""
-msgid "Geo|Projects in certain storage shards"
+msgid "GPG Keys"
msgstr ""
-msgid "Geo|Repository sync capacity"
+msgid "General"
msgstr ""
-msgid "Geo|Select groups to replicate."
+msgid "General pipelines"
msgstr ""
-msgid "Geo|Shards to synchronize"
+msgid "Generate a default set of labels"
msgstr ""
msgid "Git repository URL"
@@ -2157,6 +2354,9 @@ msgstr ""
msgid "Git storage health information has been reset"
msgstr ""
+msgid "Git strategy for pipelines"
+msgstr ""
+
msgid "Git version"
msgstr ""
@@ -2166,21 +2366,24 @@ msgstr ""
msgid "GitLab CI Linter has been moved"
msgstr ""
-msgid "GitLab Geo"
+msgid "GitLab Group Runners can execute code for all the projects in this group."
msgstr ""
msgid "GitLab Runner section"
msgstr ""
-msgid "GitLab single sign on URL"
-msgstr ""
-
msgid "Gitaly"
msgstr ""
msgid "Gitaly Servers"
msgstr ""
+msgid "Gitaly|Address"
+msgstr ""
+
+msgid "Go Back"
+msgstr ""
+
msgid "Go back"
msgstr ""
@@ -2196,22 +2399,19 @@ msgstr ""
msgid "Got it!"
msgstr ""
-msgid "GroupRoadmap|Epics let you manage your portfolio of projects more efficiently and with less effort"
-msgstr ""
-
-msgid "GroupRoadmap|From %{dateWord}"
+msgid "Graph"
msgstr ""
-msgid "GroupRoadmap|Loading roadmap"
+msgid "Group CI/CD settings"
msgstr ""
-msgid "GroupRoadmap|Something went wrong while fetching epics"
+msgid "Group ID"
msgstr ""
-msgid "GroupRoadmap|To view the roadmap, add a planned start or finish date to one of your epics in this group or its subgroups. Only epics in the past 3 months and the next 3 months are shown &ndash; from %{startDate} to %{endDate}."
+msgid "Group Runners"
msgstr ""
-msgid "GroupRoadmap|Until %{dateWord}"
+msgid "Group maintainers can register group runners in the %{link}"
msgstr ""
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
@@ -2238,6 +2438,9 @@ msgstr ""
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
+msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
+msgstr ""
+
msgid "GroupsEmptyState|A group is a collection of several projects."
msgstr ""
@@ -2277,12 +2480,6 @@ msgstr ""
msgid "GroupsTree|Sorry, no groups or projects matched your search"
msgstr ""
-msgid "Have your users email"
-msgstr ""
-
-msgid "Header message"
-msgstr ""
-
msgid "Health Check"
msgstr ""
@@ -2315,19 +2512,49 @@ msgid_plural "Hide values"
msgstr[0] ""
msgstr[1] ""
+msgid "Hide whitespace changes"
+msgstr ""
+
msgid "History"
msgstr ""
msgid "Housekeeping successfully started"
msgstr ""
-msgid "Identity provider single sign on URL"
+msgid "I accept the %{terms_link}"
+msgstr ""
+
+msgid "I accept the|Terms of Service and Privacy Policy"
+msgstr ""
+
+msgid "ID"
+msgstr ""
+
+msgid "IDE|Commit"
+msgstr ""
+
+msgid "IDE|Edit"
+msgstr ""
+
+msgid "IDE|Go back"
msgstr ""
-msgid "If enabled, access to projects will be validated on an external service using their classification label."
+msgid "IDE|Open in file view"
msgstr ""
-msgid "If using GitHub, you’ll see pipeline statuses on GitHub for your commits and pull requests. %{more_info_link}"
+msgid "IDE|Review"
+msgstr ""
+
+msgid "Identifier"
+msgstr ""
+
+msgid "Identities"
+msgstr ""
+
+msgid "If disabled, the access level will depend on the user's permissions in the project."
+msgstr ""
+
+msgid "If enabled"
msgstr ""
msgid "If you already have files you can push them using the %{link_to_cli} below."
@@ -2336,6 +2563,15 @@ msgstr ""
msgid "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>."
msgstr ""
+msgid "ImageDiffViewer|2-up"
+msgstr ""
+
+msgid "ImageDiffViewer|Onion skin"
+msgstr ""
+
+msgid "ImageDiffViewer|Swipe"
+msgstr ""
+
msgid "Import"
msgstr ""
@@ -2351,16 +2587,10 @@ msgstr ""
msgid "Import repository"
msgstr ""
-msgid "ImportButtons|Connect repositories from"
-msgstr ""
-
-msgid "Improve Issue boards with GitLab Enterprise Edition."
-msgstr ""
-
-msgid "Improve issues management with Issue weight and GitLab Enterprise Edition."
+msgid "Include a Terms of Service agreement and Privacy Policy that all users must accept."
msgstr ""
-msgid "Improve search with Advanced Global Search and GitLab Enterprise Edition."
+msgid "Inline"
msgstr ""
msgid "Install Runner on Kubernetes"
@@ -2369,17 +2599,15 @@ msgstr ""
msgid "Install a Runner compatible with GitLab CI"
msgstr ""
-msgid "Instance"
-msgid_plural "Instances"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "Instance does not support multiple Kubernetes clusters"
msgstr ""
msgid "Integrations"
msgstr ""
+msgid "Integrations Settings"
+msgstr ""
+
msgid "Interested parties can even contribute by pushing commits if they want to."
msgstr ""
@@ -2395,7 +2623,7 @@ msgstr ""
msgid "Introducing Cycle Analytics"
msgstr ""
-msgid "Issue board focus mode"
+msgid "Issue Board"
msgstr ""
msgid "Issue events"
@@ -2404,9 +2632,6 @@ msgstr ""
msgid "IssueBoards|Board"
msgstr ""
-msgid "IssueBoards|Boards"
-msgstr ""
-
msgid "Issues"
msgstr ""
@@ -2419,6 +2644,12 @@ msgstr ""
msgid "January"
msgstr ""
+msgid "Job"
+msgstr ""
+
+msgid "Job has been erased"
+msgstr ""
+
msgid "Jobs"
msgstr ""
@@ -2461,6 +2692,9 @@ msgstr ""
msgid "Kubernetes service integration has been deprecated. %{deprecated_message_content} your Kubernetes clusters using the new <a href=\"%{url}\"/>Kubernetes Clusters</a> page"
msgstr ""
+msgid "LFS"
+msgstr ""
+
msgid "LFSStatus|Disabled"
msgstr ""
@@ -2470,6 +2704,9 @@ msgstr ""
msgid "Label"
msgstr ""
+msgid "Label actions dropdown"
+msgstr ""
+
msgid "LabelSelect|%{firstLabelName} +%{remainingLabelCount} more"
msgstr ""
@@ -2485,6 +2722,9 @@ msgstr ""
msgid "Labels can be applied to issues and merge requests to categorize them."
msgstr ""
+msgid "Labels can be applied to issues and merge requests."
+msgstr ""
+
msgid "Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>"
msgstr ""
@@ -2520,6 +2760,9 @@ msgstr ""
msgid "LastPushEvent|at"
msgstr ""
+msgid "Latest changes"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -2544,9 +2787,6 @@ msgstr ""
msgid "Leave project"
msgstr ""
-msgid "License"
-msgstr ""
-
msgid "List"
msgstr ""
@@ -2556,6 +2796,9 @@ msgstr ""
msgid "Loading the GitLab IDE..."
msgstr ""
+msgid "Loading..."
+msgstr ""
+
msgid "Lock"
msgstr ""
@@ -2565,21 +2808,18 @@ msgstr ""
msgid "Lock not found"
msgstr ""
-msgid "Locked"
+msgid "Lock to current projects"
msgstr ""
-msgid "Locked Files"
+msgid "Locked"
msgstr ""
-msgid "Locks give the ability to lock specific file or folder."
+msgid "Locked to current projects"
msgstr ""
msgid "Login"
msgstr ""
-msgid "Make everyone on your team more productive regardless of their location. GitLab Geo creates read-only mirrors of your GitLab instance so you can reduce the time it takes to clone and fetch large repos."
-msgstr ""
-
msgid "Manage all notifications"
msgstr ""
@@ -2592,16 +2832,16 @@ msgstr ""
msgid "Manage project labels"
msgstr ""
-msgid "Manage your group’s membership while adding another level of security with SAML."
-msgstr ""
-
msgid "Mar"
msgstr ""
msgid "March"
msgstr ""
-msgid "Mark done"
+msgid "Mark todo as done"
+msgstr ""
+
+msgid "Markdown enabled"
msgstr ""
msgid "Maximum git storage failures"
@@ -2616,7 +2856,7 @@ msgstr ""
msgid "Members"
msgstr ""
-msgid "Members will be forwarded here when signing in to your group. Get this from your identity provider, where it can also be called \"SSO Service Location\", \"SAML Token Issuance Endpoint\", or \"SAML 2.0/W-Federation URL\"."
+msgid "Merge Request:"
msgstr ""
msgid "Merge Requests"
@@ -2628,91 +2868,46 @@ msgstr ""
msgid "Merge request"
msgstr ""
-msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
+msgid "Merge requests"
msgstr ""
-msgid "Merged"
-msgstr ""
-
-msgid "Messages"
-msgstr ""
-
-msgid "Metrics - Influx"
-msgstr ""
-
-msgid "Metrics - Prometheus"
-msgstr ""
-
-msgid "Metrics|Business"
-msgstr ""
-
-msgid "Metrics|Create metric"
-msgstr ""
-
-msgid "Metrics|Edit metric"
-msgstr ""
-
-msgid "Metrics|For grouping similar metrics"
-msgstr ""
-
-msgid "Metrics|Label of the chart's vertical axis. Usually the type of the unit being charted. The horizontal axis (X-axis) always represents time."
-msgstr ""
-
-msgid "Metrics|Legend label (optional)"
-msgstr ""
-
-msgid "Metrics|Must be a valid PromQL query."
-msgstr ""
-
-msgid "Metrics|Name"
-msgstr ""
-
-msgid "Metrics|New metric"
-msgstr ""
-
-msgid "Metrics|Prometheus Query Documentation"
-msgstr ""
-
-msgid "Metrics|Query"
-msgstr ""
-
-msgid "Metrics|Response"
+msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
msgstr ""
-msgid "Metrics|System"
+msgid "MergeRequests|Resolve this discussion in a new issue"
msgstr ""
-msgid "Metrics|Type"
+msgid "MergeRequests|Saving the comment failed"
msgstr ""
-msgid "Metrics|Unit label"
+msgid "MergeRequests|Toggle comments for this file"
msgstr ""
-msgid "Metrics|Used as a title for the chart"
+msgid "MergeRequests|Updating discussions failed"
msgstr ""
-msgid "Metrics|Used if the query returns a single series. If it returns multiple series, their legend labels will be picked up from the response."
+msgid "MergeRequests|View file @ %{commitId}"
msgstr ""
-msgid "Metrics|Y-axis label"
+msgid "MergeRequests|View replaced file @ %{commitId}"
msgstr ""
-msgid "Metrics|e.g. HTTP requests"
+msgid "Merged"
msgstr ""
-msgid "Metrics|e.g. Requests/second"
+msgid "Messages"
msgstr ""
-msgid "Metrics|e.g. Throughput"
+msgid "Metrics - Influx"
msgstr ""
-msgid "Metrics|e.g. rate(http_requests_total[5m])"
+msgid "Metrics - Prometheus"
msgstr ""
-msgid "Metrics|e.g. req/sec"
+msgid "Milestone"
msgstr ""
-msgid "Milestone"
+msgid "Milestones"
msgstr ""
msgid "Milestones|Delete milestone"
@@ -2733,9 +2928,6 @@ msgstr ""
msgid "Milestones|Promote Milestone"
msgstr ""
-msgid "Milestones|This action cannot be reversed."
-msgstr ""
-
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr ""
@@ -2748,7 +2940,7 @@ msgstr ""
msgid "Monitoring"
msgstr ""
-msgid "More info"
+msgid "More actions"
msgstr ""
msgid "More information"
@@ -2763,12 +2955,30 @@ msgstr ""
msgid "Move issue"
msgstr ""
-msgid "Multiple issue boards"
+msgid "Name"
msgstr ""
msgid "Name new label"
msgstr ""
+msgid "Name your individual key via a title"
+msgstr ""
+
+msgid "Nav|Help"
+msgstr ""
+
+msgid "Nav|Home"
+msgstr ""
+
+msgid "Nav|Sign In / Register"
+msgstr ""
+
+msgid "Nav|Sign out and sign in with a different account"
+msgstr ""
+
+msgid "New Identity"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] ""
@@ -2780,6 +2990,9 @@ msgstr ""
msgid "New Kubernetes cluster"
msgstr ""
+msgid "New Label"
+msgstr ""
+
msgid "New Pipeline Schedule"
msgstr ""
@@ -2792,15 +3005,15 @@ msgstr ""
msgid "New directory"
msgstr ""
-msgid "New epic"
-msgstr ""
-
msgid "New file"
msgstr ""
msgid "New group"
msgstr ""
+msgid "New identity"
+msgstr ""
+
msgid "New issue"
msgstr ""
@@ -2810,6 +3023,9 @@ msgstr ""
msgid "New merge request"
msgstr ""
+msgid "New pipelines will cancel older, pending pipelines on the same branch"
+msgstr ""
+
msgid "New project"
msgstr ""
@@ -2825,7 +3041,7 @@ msgstr ""
msgid "New tag"
msgstr ""
-msgid "No Label"
+msgid "No"
msgstr ""
msgid "No assignee"
@@ -2846,7 +3062,16 @@ msgstr ""
msgid "No file chosen"
msgstr ""
-msgid "No labels created yet."
+msgid "No files found"
+msgstr ""
+
+msgid "No files found."
+msgstr ""
+
+msgid "No merge requests found"
+msgstr ""
+
+msgid "No messages were logged"
msgstr ""
msgid "No repository"
@@ -2879,15 +3104,9 @@ msgstr ""
msgid "Note that the master branch is automatically protected. %{link_to_protected_branches}"
msgstr ""
-msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow connecting repositories without generating a Personal Access Token."
-msgstr ""
-
msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
msgstr ""
-msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow connecting repositories without generating a Personal Access Token."
-msgstr ""
-
msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
msgstr ""
@@ -2963,9 +3182,6 @@ msgstr ""
msgid "Number of access attempts"
msgstr ""
-msgid "OK"
-msgstr ""
-
msgid "Oct"
msgstr ""
@@ -2975,19 +3191,16 @@ msgstr ""
msgid "OfSearchInADropdown|Filter"
msgstr ""
-msgid "Once imported, repositories can be mirrored over SSH. Read more %{ssh_link}"
-msgstr ""
-
msgid "Online IDE integration settings."
msgstr ""
-msgid "Only project members can comment."
+msgid "Only comments from the following commit are shown below"
msgstr ""
-msgid "Open"
+msgid "Only project members can comment."
msgstr ""
-msgid "Opened"
+msgid "Open in Xcode"
msgstr ""
msgid "OpenedNDaysAgo|Opened"
@@ -2996,9 +3209,18 @@ msgstr ""
msgid "Opens in a new window"
msgstr ""
+msgid "Operations"
+msgstr ""
+
msgid "Options"
msgstr ""
+msgid "Or you can choose one of the suggested colors below"
+msgstr ""
+
+msgid "Other Labels"
+msgstr ""
+
msgid "Otherwise it is recommended you start with one of the options below."
msgstr ""
@@ -3032,12 +3254,27 @@ msgstr ""
msgid "Password"
msgstr ""
+msgid "Paste your public SSH key, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'. Don't use your private SSH key."
+msgstr ""
+
+msgid "Pause"
+msgstr ""
+
msgid "Pending"
msgstr ""
+msgid "Per job. If a job passes this threshold, it will be marked as failed"
+msgstr ""
+
+msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgstr ""
+
msgid "Performance optimization"
msgstr ""
+msgid "Permissions"
+msgstr ""
+
msgid "Personal Access Token"
msgstr ""
@@ -3053,7 +3290,7 @@ msgstr ""
msgid "Pipeline Schedules"
msgstr ""
-msgid "Pipeline quota"
+msgid "Pipeline triggers"
msgstr ""
msgid "PipelineCharts|Failed:"
@@ -3152,10 +3389,22 @@ msgstr ""
msgid "Pipelines|This project is not currently set up to run pipelines."
msgstr ""
-msgid "Pipeline|Retry pipeline"
+msgid "Pipeline|Create for"
msgstr ""
-msgid "Pipeline|Retry pipeline #%{pipelineId}?"
+msgid "Pipeline|Create pipeline"
+msgstr ""
+
+msgid "Pipeline|Existing branch name or tag"
+msgstr ""
+
+msgid "Pipeline|Run Pipeline"
+msgstr ""
+
+msgid "Pipeline|Search branches"
+msgstr ""
+
+msgid "Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default."
msgstr ""
msgid "Pipeline|Stop pipeline"
@@ -3164,7 +3413,7 @@ msgstr ""
msgid "Pipeline|Stop pipeline #%{pipelineId}?"
msgstr ""
-msgid "Pipeline|You’re about to retry pipeline %{pipelineId}."
+msgid "Pipeline|Variables"
msgstr ""
msgid "Pipeline|You’re about to stop pipeline %{pipelineId}."
@@ -3182,19 +3431,25 @@ msgstr ""
msgid "Pipeline|with stages"
msgstr ""
+msgid "Plain diff"
+msgstr ""
+
msgid "PlantUML"
msgstr ""
msgid "Play"
msgstr ""
-msgid "Please <a href=%{link_to_billing} target=\"_blank\" rel=\"noopener noreferrer\">enable billing for one of your projects to be able to create a Kubernetes cluster</a>, then try again."
+msgid "Please accept the Terms of Service before continuing."
+msgstr ""
+
+msgid "Please select at least one filter to see results"
msgstr ""
msgid "Please solve the reCAPTCHA"
msgstr ""
-msgid "Please wait while we connect to your repository. Refresh at will."
+msgid "Please try again"
msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
@@ -3203,7 +3458,19 @@ msgstr ""
msgid "Preferences"
msgstr ""
-msgid "Primary"
+msgid "Preferences|Navigation theme"
+msgstr ""
+
+msgid "Prioritize"
+msgstr ""
+
+msgid "Prioritize label"
+msgstr ""
+
+msgid "Prioritized Labels"
+msgstr ""
+
+msgid "Prioritized label"
msgstr ""
msgid "Private - Project access must be granted explicitly to each user."
@@ -3221,6 +3488,12 @@ msgstr ""
msgid "Profiles|Account scheduled for removal."
msgstr ""
+msgid "Profiles|Change username"
+msgstr ""
+
+msgid "Profiles|Current path: %{path}"
+msgstr ""
+
msgid "Profiles|Delete Account"
msgstr ""
@@ -3239,9 +3512,21 @@ msgstr ""
msgid "Profiles|Invalid username"
msgstr ""
+msgid "Profiles|Path"
+msgstr ""
+
msgid "Profiles|Type your %{confirmationValue} to confirm:"
msgstr ""
+msgid "Profiles|Update username"
+msgstr ""
+
+msgid "Profiles|Username change failed - %{message}"
+msgstr ""
+
+msgid "Profiles|Username successfully changed"
+msgstr ""
+
msgid "Profiles|You don't have access to delete this user."
msgstr ""
@@ -3260,6 +3545,12 @@ msgstr ""
msgid "Programming languages used in this repository"
msgstr ""
+msgid "Progress"
+msgstr ""
+
+msgid "Project"
+msgstr ""
+
msgid "Project '%{project_name}' is in the process of being deleted."
msgstr ""
@@ -3272,6 +3563,9 @@ msgstr ""
msgid "Project '%{project_name}' was successfully updated."
msgstr ""
+msgid "Project Badges"
+msgstr ""
+
msgid "Project access must be granted explicitly to each user."
msgstr ""
@@ -3299,30 +3593,6 @@ msgstr ""
msgid "ProjectActivityRSS|Subscribe"
msgstr ""
-msgid "ProjectCreationLevel|Allowed to create projects"
-msgstr ""
-
-msgid "ProjectCreationLevel|Default project creation protection"
-msgstr ""
-
-msgid "ProjectCreationLevel|Developers + Masters"
-msgstr ""
-
-msgid "ProjectCreationLevel|Masters"
-msgstr ""
-
-msgid "ProjectCreationLevel|No one"
-msgstr ""
-
-msgid "ProjectFeature|Disabled"
-msgstr ""
-
-msgid "ProjectFeature|Everyone with access"
-msgstr ""
-
-msgid "ProjectFeature|Only team members"
-msgstr ""
-
msgid "ProjectFileTree|Name"
msgstr ""
@@ -3332,27 +3602,6 @@ msgstr ""
msgid "ProjectLifecycle|Stage"
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 ""
@@ -3377,6 +3626,9 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr ""
+msgid "PrometheusDashboard|Time"
+msgstr ""
+
msgid "PrometheusService|%{exporters} with %{metrics} were found"
msgstr ""
@@ -3398,18 +3650,9 @@ msgstr ""
msgid "PrometheusService|Common metrics"
msgstr ""
-msgid "PrometheusService|Common metrics are automatically monitored based on a library of metrics from popular exporters."
-msgstr ""
-
-msgid "PrometheusService|Custom metrics"
-msgstr ""
-
msgid "PrometheusService|Finding and configuring metrics..."
msgstr ""
-msgid "PrometheusService|Finding custom metrics..."
-msgstr ""
-
msgid "PrometheusService|Install Prometheus on clusters"
msgstr ""
@@ -3422,13 +3665,13 @@ msgstr ""
msgid "PrometheusService|Metrics"
msgstr ""
-msgid "PrometheusService|Missing environment variable"
+msgid "PrometheusService|Metrics are automatically configured and monitored based on a library of metrics from popular exporters."
msgstr ""
-msgid "PrometheusService|More information"
+msgid "PrometheusService|Missing environment variable"
msgstr ""
-msgid "PrometheusService|New metric"
+msgid "PrometheusService|More information"
msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
@@ -3437,9 +3680,6 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
-msgstr ""
-
msgid "PrometheusService|Time-series monitoring service"
msgstr ""
@@ -3455,22 +3695,28 @@ msgstr ""
msgid "Promote"
msgstr ""
-msgid "Promote to Group Label"
+msgid "Promote these project milestones into a group milestone."
msgstr ""
msgid "Promote to Group Milestone"
msgstr ""
+msgid "Promote to group label"
+msgstr ""
+
msgid "Protip:"
msgstr ""
+msgid "Provider"
+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"
+msgid "Public pipelines"
msgstr ""
msgid "Push events"
@@ -3482,10 +3728,10 @@ msgstr ""
msgid "Push to create a project"
msgstr ""
-msgid "PushRule|Committer restriction"
+msgid "Quick actions can be used in the issues description and comment boxes."
msgstr ""
-msgid "Quick actions can be used in the issues description and comment boxes."
+msgid "Re-deploy"
msgstr ""
msgid "Read more"
@@ -3497,16 +3743,16 @@ msgstr ""
msgid "Real-time features"
msgstr ""
-msgid "RefSwitcher|Branches"
+msgid "Reference:"
msgstr ""
-msgid "RefSwitcher|Tags"
+msgid "Register / Sign In"
msgstr ""
-msgid "Reference:"
+msgid "Register and see your runners for this group."
msgstr ""
-msgid "Register / Sign In"
+msgid "Register and see your runners for this project."
msgstr ""
msgid "Registry"
@@ -3539,28 +3785,28 @@ msgstr ""
msgid "Remove"
msgstr ""
-msgid "Remove avatar"
+msgid "Remove Runner"
msgstr ""
-msgid "Remove project"
+msgid "Remove avatar"
msgstr ""
-msgid "Repair authentication"
+msgid "Remove priority"
msgstr ""
-msgid "Repo by URL"
+msgid "Remove project"
msgstr ""
msgid "Repository"
msgstr ""
-msgid "Repository has no locks."
+msgid "Repository Settings"
msgstr ""
msgid "Repository maintenance"
msgstr ""
-msgid "Repository mirror settings"
+msgid "Repository mirror"
msgstr ""
msgid "Repository storage"
@@ -3569,6 +3815,9 @@ msgstr ""
msgid "Request Access"
msgstr ""
+msgid "Require all users to accept Terms of Service and Privacy Policy when they access GitLab."
+msgstr ""
+
msgid "Reset git storage health information"
msgstr ""
@@ -3578,10 +3827,25 @@ msgstr ""
msgid "Reset runners registration token"
msgstr ""
+msgid "Resolve all discussions in new issue"
+msgstr ""
+
+msgid "Resolve conflicts on source branch"
+msgstr ""
+
msgid "Resolve discussion"
msgstr ""
-msgid "Response"
+msgid "Resume"
+msgstr ""
+
+msgid "Retry"
+msgstr ""
+
+msgid "Retry this job"
+msgstr ""
+
+msgid "Retry verification"
msgstr ""
msgid "Reveal value"
@@ -3595,7 +3859,7 @@ msgstr ""
msgid "Revert this merge request"
msgstr ""
-msgid "Review the process for configuring service providers in your identity provider — in this case, GitLab is the \"service provider\" or \"relying party\"."
+msgid "Review"
msgstr ""
msgid "Reviewing"
@@ -3604,28 +3868,31 @@ msgstr ""
msgid "Reviewing (merge request !%{mergeRequestId})"
msgstr ""
-msgid "Roadmap"
+msgid "Rollback"
msgstr ""
-msgid "Run CI/CD pipelines for external repositories"
+msgid "Runner token"
msgstr ""
msgid "Runners"
msgstr ""
-msgid "Running"
+msgid "Runners API"
+msgstr ""
+
+msgid "Runners can be placed on separate users, servers, and even on your local machine."
msgstr ""
-msgid "SAML Single Sign On"
+msgid "Running"
msgstr ""
-msgid "SAML Single Sign On Settings"
+msgid "SSH Keys"
msgstr ""
-msgid "SHA1 fingerprint of the SAML token signing certificate. Get this from your identity provider, where it can also be called \"Thumbprint\"."
+msgid "SSL Verification"
msgstr ""
-msgid "SSH Keys"
+msgid "Save"
msgstr ""
msgid "Save changes"
@@ -3649,15 +3916,30 @@ msgstr ""
msgid "Scheduling Pipelines"
msgstr ""
-msgid "Scoped issue boards"
+msgid "Scroll to bottom"
+msgstr ""
+
+msgid "Scroll to top"
msgstr ""
msgid "Search"
msgstr ""
+msgid "Search branches"
+msgstr ""
+
msgid "Search branches and tags"
msgstr ""
+msgid "Search files"
+msgstr ""
+
+msgid "Search for projects, issues, etc."
+msgstr ""
+
+msgid "Search merge requests"
+msgstr ""
+
msgid "Search milestones"
msgstr ""
@@ -3673,13 +3955,13 @@ msgstr ""
msgid "Seconds to wait for a storage access attempt"
msgstr ""
-msgid "Secret variables"
+msgid "Select"
msgstr ""
-msgid "Security report"
+msgid "Select Archive Format"
msgstr ""
-msgid "Select Archive Format"
+msgid "Select a namespace to fork the project"
msgstr ""
msgid "Select a timezone"
@@ -3694,10 +3976,19 @@ msgstr ""
msgid "Select branch/tag"
msgstr ""
-msgid "Select target branch"
+msgid "Select project"
+msgstr ""
+
+msgid "Select project and zone to choose machine type"
+msgstr ""
+
+msgid "Select project to choose zone"
msgstr ""
-msgid "Selective synchronization"
+msgid "Select source branch"
+msgstr ""
+
+msgid "Select target branch"
msgstr ""
msgid "Send email"
@@ -3715,9 +4006,6 @@ msgstr ""
msgid "Service Templates"
msgstr ""
-msgid "Service URL"
-msgstr ""
-
msgid "Session expiration, projects limit and attachment size."
msgstr ""
@@ -3742,9 +4030,6 @@ msgstr ""
msgid "Set up Koding"
msgstr ""
-msgid "Set up assertions/attributes/claims (email, first_name, last_name) and NameID according to %{docsLinkStart}the documentation %{icon}%{docsLinkEnd}"
-msgstr ""
-
msgid "SetPasswordToCloneLink|set a password"
msgstr ""
@@ -3754,19 +4039,22 @@ msgstr ""
msgid "Setup a specific Runner automatically"
msgstr ""
-msgid "Share the <strong>%{sso_label}</strong> with members so they can sign in to your group through your identity provider"
+msgid "Share"
msgstr ""
-msgid "SharedRunnersMinutesSettings|By resetting the pipeline minutes for this namespace, the currently used minutes will be set to zero."
+msgid "Shared Runners"
msgstr ""
-msgid "SharedRunnersMinutesSettings|Reset pipeline minutes"
+msgid "Show command"
msgstr ""
-msgid "SharedRunnersMinutesSettings|Reset used pipeline minutes"
+msgid "Show complete raw log"
msgstr ""
-msgid "Show command"
+msgid "Show latest version"
+msgstr ""
+
+msgid "Show latest version of the diff"
msgstr ""
msgid "Show parent pages"
@@ -3775,21 +4063,18 @@ msgstr ""
msgid "Show parent subgroups"
msgstr ""
+msgid "Show whitespace changes"
+msgstr ""
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] ""
msgstr[1] ""
-msgid "Sidebar|Change weight"
+msgid "Side-by-side"
msgstr ""
-msgid "Sidebar|No"
-msgstr ""
-
-msgid "Sidebar|None"
-msgstr ""
-
-msgid "Sidebar|Weight"
+msgid "Sign out"
msgstr ""
msgid "Sign-in restrictions"
@@ -3801,7 +4086,7 @@ msgstr ""
msgid "Size and domain settings for static websites"
msgstr ""
-msgid "Slack application"
+msgid "Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job"
msgstr ""
msgid "Snippets"
@@ -3813,13 +4098,13 @@ msgstr ""
msgid "Something went wrong on our end."
msgstr ""
-msgid "Something went wrong when toggling the button"
+msgid "Something went wrong on our end. Please try again!"
msgstr ""
-msgid "Something went wrong while fetching Dependency Scanning."
+msgid "Something went wrong when toggling the button"
msgstr ""
-msgid "Something went wrong while fetching SAST."
+msgid "Something went wrong while closing the %{issuable}. Please try again later"
msgstr ""
msgid "Something went wrong while fetching the projects."
@@ -3828,6 +4113,12 @@ msgstr ""
msgid "Something went wrong while fetching the registry list."
msgstr ""
+msgid "Something went wrong while reopening the %{issuable}. Please try again later"
+msgstr ""
+
+msgid "Something went wrong while resolving this discussion. Please try again."
+msgstr ""
+
msgid "Something went wrong. Please try again."
msgstr ""
@@ -3873,9 +4164,6 @@ msgstr ""
msgid "SortOptions|Least popular"
msgstr ""
-msgid "SortOptions|Less weight"
-msgstr ""
-
msgid "SortOptions|Milestone"
msgstr ""
@@ -3885,9 +4173,6 @@ msgstr ""
msgid "SortOptions|Milestone due soon"
msgstr ""
-msgid "SortOptions|More weight"
-msgstr ""
-
msgid "SortOptions|Most popular"
msgstr ""
@@ -3927,9 +4212,6 @@ msgstr ""
msgid "SortOptions|Start soon"
msgstr ""
-msgid "SortOptions|Weight"
-msgstr ""
-
msgid "Source"
msgstr ""
@@ -3948,9 +4230,36 @@ msgstr ""
msgid "Spam and Anti-bot Protection"
msgstr ""
+msgid "Specific Runners"
+msgstr ""
+
msgid "Specify the following URL during the Runner setup:"
msgstr ""
+msgid "Squash commits"
+msgstr ""
+
+msgid "Stage"
+msgstr ""
+
+msgid "Stage & Commit"
+msgstr ""
+
+msgid "Stage all changes"
+msgstr ""
+
+msgid "Stage changes"
+msgstr ""
+
+msgid "Staged"
+msgstr ""
+
+msgid "Staged %{type}"
+msgstr ""
+
+msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
+msgstr ""
+
msgid "StarProject|Star"
msgstr ""
@@ -3972,12 +4281,15 @@ msgstr ""
msgid "Started"
msgstr ""
-msgid "State your message to activate"
+msgid "Starts at (UTC)"
msgstr ""
msgid "Status"
msgstr ""
+msgid "Stop this environment"
+msgstr ""
+
msgid "Stopped"
msgstr ""
@@ -3987,16 +4299,19 @@ msgstr ""
msgid "Subgroups"
msgstr ""
-msgid "Switch branch/tag"
+msgid "Subscribe"
msgstr ""
-msgid "System"
+msgid "Subscribe at group level"
msgstr ""
-msgid "System Hooks"
+msgid "Subscribe at project level"
msgstr ""
-msgid "System header and footer:"
+msgid "Switch branch/tag"
+msgstr ""
+
+msgid "System Hooks"
msgstr ""
msgid "Tag (%{tag_count})"
@@ -4007,6 +4322,9 @@ msgstr[1] ""
msgid "Tags"
msgstr ""
+msgid "Tags:"
+msgstr ""
+
msgid "TagsPage|Browse commits"
msgstr ""
@@ -4070,7 +4388,7 @@ msgstr ""
msgid "TagsPage|Use git tag command to add a new one:"
msgstr ""
-msgid "TagsPage|Write your release notes or drag files here..."
+msgid "TagsPage|Write your release notes or drag files here…"
msgstr ""
msgid "TagsPage|protected"
@@ -4085,19 +4403,19 @@ msgstr ""
msgid "Team"
msgstr ""
-msgid "Thanks! Don't show me this again"
+msgid "Terms of Service Agreement and Privacy Policy"
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."
+msgid "Terms of Service and Privacy Policy"
msgstr ""
-msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project"
+msgid "Test coverage parsing"
msgstr ""
-msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
+msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project"
msgstr ""
-msgid "The X509 Certificate to use when mutual TLS is required to communicate with the external authorization service. If left blank, the server certificate is still validated when accessing over HTTPS."
+msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
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."
@@ -4106,9 +4424,6 @@ msgstr ""
msgid "The collection of events added to the data gathered for that stage."
msgstr ""
-msgid "The connection will time out after %{timeout}. For repositories that take longer, use a clone/push combination."
-msgstr ""
-
msgid "The fork relationship has been removed."
msgstr ""
@@ -4127,7 +4442,7 @@ 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 passphrase required to decrypt the private key. This is optional and the value is encrypted at rest."
+msgid "The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>"
msgstr ""
msgid "The phase of the development lifecycle."
@@ -4136,9 +4451,6 @@ msgstr ""
msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit."
msgstr ""
-msgid "The private key to use when a client certificate is provided. This value is encrypted at rest."
-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 ""
@@ -4160,7 +4472,7 @@ msgstr ""
msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request."
msgstr ""
-msgid "The roadmap shows the progress of your epics along a timeline"
+msgid "The secure token used by the Runner to checkout the project"
msgstr ""
msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time."
@@ -4187,13 +4499,19 @@ msgstr ""
msgid "There are no issues to show"
msgstr ""
+msgid "There are no labels yet"
+msgstr ""
+
msgid "There are no merge requests to show"
msgstr ""
msgid "There are problems accessing Git storage: "
msgstr ""
-msgid "There was an error loading results"
+msgid "There was an error loading jobs"
+msgstr ""
+
+msgid "There was an error loading latest pipeline"
msgstr ""
msgid "There was an error loading users activity calendar."
@@ -4214,12 +4532,21 @@ msgstr ""
msgid "There was an error when unsubscribing from this label."
msgstr ""
-msgid "This board\\'s scope is reduced"
+msgid "They can be managed using the %{link}."
+msgstr ""
+
+msgid "This GitLab instance does not provide any shared Runners yet. Instance administrators can register shared Runners in the admin area."
+msgstr ""
+
+msgid "This diff is collapsed."
msgstr ""
msgid "This directory"
msgstr ""
+msgid "This group does not provide any group Runners yet."
+msgstr ""
+
msgid "This is a confidential issue."
msgstr ""
@@ -4241,6 +4568,15 @@ msgstr ""
msgid "This job depends on upstream jobs that need to succeed in order for this job to be triggered"
msgstr ""
+msgid "This job does not have a trace."
+msgstr ""
+
+msgid "This job has been canceled"
+msgstr ""
+
+msgid "This job has been skipped"
+msgstr ""
+
msgid "This job has not been triggered yet"
msgstr ""
@@ -4259,19 +4595,28 @@ msgstr ""
msgid "This merge request is locked."
msgstr ""
+msgid "This option is disabled while you still have unstaged changes"
+msgstr ""
+
msgid "This page is unavailable because you are not allowed to read information across multiple projects."
msgstr ""
+msgid "This page will be removed in a future release."
+msgstr ""
+
msgid "This project"
msgstr ""
+msgid "This project does not belong to a group and can therefore not make use of group Runners."
+msgstr ""
+
msgid "This repository"
msgstr ""
-msgid "This will delete the custom metric, Are you sure?"
+msgid "This source diff could not be displayed because it is too large."
msgstr ""
-msgid "Those emails automatically become issues (with the comments becoming the email conversation) listed here."
+msgid "This user has no identities"
msgstr ""
msgid "Time before an issue gets scheduled"
@@ -4283,10 +4628,10 @@ msgstr ""
msgid "Time between merge request creation and merge/close"
msgstr ""
-msgid "Time between updates and capacity settings."
+msgid "Time remaining"
msgstr ""
-msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
+msgid "Time spent"
msgstr ""
msgid "Time tracking"
@@ -4310,6 +4655,9 @@ msgstr ""
msgid "Timeago|%s days remaining"
msgstr ""
+msgid "Timeago|%s hours ago"
+msgstr ""
+
msgid "Timeago|%s hours remaining"
msgstr ""
@@ -4325,6 +4673,9 @@ msgstr ""
msgid "Timeago|%s months remaining"
msgstr ""
+msgid "Timeago|%s seconds ago"
+msgstr ""
+
msgid "Timeago|%s seconds remaining"
msgstr ""
@@ -4340,46 +4691,43 @@ msgstr ""
msgid "Timeago|%s years remaining"
msgstr ""
-msgid "Timeago|1 day remaining"
-msgstr ""
-
-msgid "Timeago|1 hour remaining"
+msgid "Timeago|1 day ago"
msgstr ""
-msgid "Timeago|1 minute remaining"
+msgid "Timeago|1 day remaining"
msgstr ""
-msgid "Timeago|1 month remaining"
+msgid "Timeago|1 hour ago"
msgstr ""
-msgid "Timeago|1 week remaining"
+msgid "Timeago|1 hour remaining"
msgstr ""
-msgid "Timeago|1 year remaining"
+msgid "Timeago|1 minute ago"
msgstr ""
-msgid "Timeago|Past due"
+msgid "Timeago|1 minute remaining"
msgstr ""
-msgid "Timeago|a day ago"
+msgid "Timeago|1 month ago"
msgstr ""
-msgid "Timeago|a month ago"
+msgid "Timeago|1 month remaining"
msgstr ""
-msgid "Timeago|a week ago"
+msgid "Timeago|1 week ago"
msgstr ""
-msgid "Timeago|a year ago"
+msgid "Timeago|1 week remaining"
msgstr ""
-msgid "Timeago|about %s hours ago"
+msgid "Timeago|1 year ago"
msgstr ""
-msgid "Timeago|about a minute ago"
+msgid "Timeago|1 year remaining"
msgstr ""
-msgid "Timeago|about an hour ago"
+msgid "Timeago|Past due"
msgstr ""
msgid "Timeago|in %s days"
@@ -4421,10 +4769,13 @@ msgstr ""
msgid "Timeago|in 1 year"
msgstr ""
-msgid "Timeago|in a while"
+msgid "Timeago|just now"
msgstr ""
-msgid "Timeago|less than a minute ago"
+msgid "Timeago|right now"
+msgstr ""
+
+msgid "Timeout"
msgstr ""
msgid "Time|hr"
@@ -4443,19 +4794,10 @@ msgstr ""
msgid "Tip:"
msgstr ""
-msgid "Title"
-msgstr ""
-
msgid "To GitLab"
msgstr ""
-msgid "To connect GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to connect."
-msgstr ""
-
-msgid "To connect GitHub repositories, you first need to authorize GitLab to access the list of your GitHub repositories:"
-msgstr ""
-
-msgid "To connect an SVN repository, check out %{svn_link}."
+msgid "To add an SSH key you need to %{generate_link_start}generate one%{link_end} or use an %{existing_link_start}existing key%{link_end}."
msgstr ""
msgid "To import GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
@@ -4467,19 +4809,19 @@ msgstr ""
msgid "To import an SVN repository, check out %{svn_link}."
msgstr ""
-msgid "To only use CI/CD features for an external repository, choose <strong>CI/CD for external repo</strong>."
+msgid "To start serving your jobs you can add Runners to your group"
msgstr ""
-msgid "To set up SAML authentication for your group through an identity provider like Azure, Okta, Onelogin, Ping Identity, or your custom SAML 2.0 provider:"
+msgid "To validate your GitLab CI configurations, go to 'CI/CD → Pipelines' inside your project, and click on the 'CI Lint' button."
msgstr ""
-msgid "To validate your GitLab CI configurations, go to 'CI/CD → Pipelines' inside your project, and click on the 'CI Lint' button."
+msgid "Todo"
msgstr ""
-msgid "To view the roadmap, add a planned start or finish date to one of your epics in this group or its subgroups. Only epics in the past 3 months and the next 3 months are shown."
+msgid "Toggle Sidebar"
msgstr ""
-msgid "Todo"
+msgid "Toggle discussion"
msgstr ""
msgid "Toggle sidebar"
@@ -4491,6 +4833,9 @@ msgstr ""
msgid "ToggleButton|Toggle Status: ON"
msgstr ""
+msgid "Too many changes to show."
+msgstr ""
+
msgid "Total Time"
msgstr ""
@@ -4500,22 +4845,19 @@ msgstr ""
msgid "Total: %{total}"
msgstr ""
-msgid "Track activity with Contribution Analytics."
-msgstr ""
-
-msgid "Track groups of issues that share a theme, across projects and milestones"
-msgstr ""
-
msgid "Track time with quick actions"
msgstr ""
msgid "Trigger this manual action"
msgstr ""
-msgid "Turn on Service Desk"
+msgid "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."
msgstr ""
-msgid "Unknown"
+msgid "Try again"
+msgstr ""
+
+msgid "Unable to load the diff. %{button_try_again}"
msgstr ""
msgid "Unlock"
@@ -4527,25 +4869,45 @@ msgstr ""
msgid "Unresolve discussion"
msgstr ""
+msgid "Unstage all changes"
+msgstr ""
+
+msgid "Unstage changes"
+msgstr ""
+
+msgid "Unstaged"
+msgstr ""
+
+msgid "Unstaged %{type}"
+msgstr ""
+
+msgid "Unstaged and staged %{type}"
+msgstr ""
+
msgid "Unstar"
msgstr ""
-msgid "Up to date"
+msgid "Unsubscribe"
msgstr ""
-msgid "Upgrade your plan to activate Advanced Global Search."
+msgid "Unsubscribe at group level"
msgstr ""
-msgid "Upgrade your plan to activate Contribution Analytics."
+msgid "Unsubscribe at project level"
msgstr ""
-msgid "Upgrade your plan to activate Group Webhooks."
+msgid "Unverified"
msgstr ""
-msgid "Upgrade your plan to activate Issue weight."
+msgid "Up to date"
msgstr ""
-msgid "Upgrade your plan to improve Issue boards."
+msgid "Update %{files}"
+msgid_plural "Update %{files} files"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Update your group name, description, avatar, and other general settings."
msgstr ""
msgid "Upload New File"
@@ -4566,7 +4928,7 @@ msgstr ""
msgid "Usage statistics"
msgstr ""
-msgid "Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab"
+msgid "Use group milestones to manage issues from multiple projects in the same milestone."
msgstr ""
msgid "Use the following registration token during setup:"
@@ -4575,10 +4937,13 @@ msgstr ""
msgid "Use your global notification setting"
msgstr ""
-msgid "Used by members to sign in to your group in GitLab"
+msgid "User and IP Rate Limits"
msgstr ""
-msgid "User and IP Rate Limits"
+msgid "Users"
+msgstr ""
+
+msgid "Variables"
msgstr ""
msgid "Variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. You can use variables for passwords, secret keys, or whatever you want."
@@ -4593,10 +4958,7 @@ msgstr ""
msgid "Various settings that affect GitLab performance."
msgstr ""
-msgid "View and edit lines"
-msgstr ""
-
-msgid "View epics list"
+msgid "Verified"
msgstr ""
msgid "View file @ "
@@ -4605,9 +4967,15 @@ msgstr ""
msgid "View group labels"
msgstr ""
+msgid "View jobs"
+msgstr ""
+
msgid "View labels"
msgstr ""
+msgid "View log"
+msgstr ""
+
msgid "View open merge request"
msgstr ""
@@ -4635,9 +5003,6 @@ msgstr ""
msgid "Want to see the data? Please ask an administrator for access."
msgstr ""
-msgid "We could not verify that one of your projects on GCP has billing enabled. Please try again."
-msgstr ""
-
msgid "We don't have enough data to show this stage."
msgstr ""
@@ -4650,13 +5015,10 @@ msgstr ""
msgid "Web terminal"
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."
+msgid "When a runner is locked, it cannot be assigned to other projects"
msgstr ""
-msgid "Weight"
-msgstr ""
-
-msgid "When leaving the URL blank, classification labels can still be specified whitout disabling cross project features or performing external authorization checks."
+msgid "When enabled, users cannot use GitLab until the terms have been accepted."
msgstr ""
msgid "Wiki"
@@ -4683,7 +5045,31 @@ msgstr ""
msgid "WikiEdit|There is already a page with the same title in that path."
msgstr ""
-msgid "WikiEmptyPageError|You are not allowed to create wiki pages"
+msgid "WikiEmptyIssueMessage|Suggest wiki improvement"
+msgstr ""
+
+msgid "WikiEmptyIssueMessage|You must be a project member in order to add wiki pages. If you have suggestions for how to improve the wiki for this project, consider opening an issue in the %{issues_link}."
+msgstr ""
+
+msgid "WikiEmptyIssueMessage|issue tracker"
+msgstr ""
+
+msgid "WikiEmpty|A wiki is where you can store all the details about your project. This can include why you've created it, its principles, how to use it, and so on."
+msgstr ""
+
+msgid "WikiEmpty|Create your first page"
+msgstr ""
+
+msgid "WikiEmpty|Suggest wiki improvement"
+msgstr ""
+
+msgid "WikiEmpty|The wiki lets you write documentation for your project"
+msgstr ""
+
+msgid "WikiEmpty|This project has no wiki pages"
+msgstr ""
+
+msgid "WikiEmpty|You must be a project member in order to add wiki pages."
msgstr ""
msgid "WikiHistoricalPage|This is an old version of this page."
@@ -4719,6 +5105,12 @@ msgstr ""
msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?"
msgstr ""
+msgid "WikiPageConfirmDelete|Delete page"
+msgstr ""
+
+msgid "WikiPageConfirmDelete|Delete page %{pageTitle}?"
+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 ""
@@ -4734,7 +5126,7 @@ msgstr ""
msgid "WikiPage|Page slug"
msgstr ""
-msgid "WikiPage|Write your content or drag files here..."
+msgid "WikiPage|Write your content or drag files here…"
msgstr ""
msgid "Wiki|Create Page"
@@ -4746,9 +5138,6 @@ msgstr ""
msgid "Wiki|Edit Page"
msgstr ""
-msgid "Wiki|Empty page"
-msgstr ""
-
msgid "Wiki|More Pages"
msgstr ""
@@ -4767,13 +5156,10 @@ 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 ""
-msgid "Write a commit message..."
+msgid "Yes"
msgstr ""
msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
@@ -4791,7 +5177,7 @@ msgstr ""
msgid "You are on a read-only GitLab instance."
msgstr ""
-msgid "You are on a secondary (read-only) Geo node. If you want to make any changes, you must visit the %{primary_node}."
+msgid "You can %{linkStart}view the blob%{linkEnd} instead."
msgstr ""
msgid "You can also create a project from the command line."
@@ -4800,6 +5186,9 @@ msgstr ""
msgid "You can also star a label to make it a priority label."
msgstr ""
+msgid "You can also test your .gitlab-ci.yml in the %{linkStart}Lint%{linkEnd}"
+msgstr ""
+
msgid "You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}"
msgstr ""
@@ -4812,28 +5201,31 @@ msgstr ""
msgid "You can only edit 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."
+msgid "You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}"
msgstr ""
msgid "You cannot write to this read-only GitLab instance."
msgstr ""
-msgid "You do not have the correct permissions to override the settings from the LDAP group sync."
+msgid "You do not have any assigned merge requests"
msgstr ""
msgid "You have no permissions"
msgstr ""
+msgid "You have not created any merge requests"
+msgstr ""
+
msgid "You have reached your project limit"
msgstr ""
-msgid "You must have master access to force delete a lock"
+msgid "You must accept our Terms of Service and privacy policy in order to register an account"
msgstr ""
-msgid "You must sign in to star a project"
+msgid "You must have maintainer access to force delete a lock"
msgstr ""
-msgid "You need a different license to enable FileLocks feature"
+msgid "You must sign in to star a project"
msgstr ""
msgid "You need permission."
@@ -4902,13 +5294,11 @@ msgstr ""
msgid "Your projects"
msgstr ""
-msgid "among other things"
+msgid "ago"
msgstr ""
-msgid "and %d fixed vulnerability"
-msgid_plural "and %d fixed vulnerabilities"
-msgstr[0] ""
-msgstr[1] ""
+msgid "among other things"
+msgstr ""
msgid "assign yourself"
msgstr ""
@@ -4916,136 +5306,36 @@ msgstr ""
msgid "branch name"
msgstr ""
-msgid "by"
-msgstr ""
-
-msgid "ciReport|%{type} detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|%{type} detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Code quality"
-msgstr ""
-
-msgid "ciReport|DAST detected no alerts by analyzing the review app"
-msgstr ""
-
-msgid "ciReport|Dependency scanning"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Failed to load %{reportName} report"
-msgstr ""
-
-msgid "ciReport|Fixed:"
-msgstr ""
-
-msgid "ciReport|Instances"
-msgstr ""
-
-msgid "ciReport|Learn more about whitelisting"
-msgstr ""
-
-msgid "ciReport|Loading %{reportName} report"
-msgstr ""
-
-msgid "ciReport|No changes to code quality"
-msgstr ""
-
-msgid "ciReport|No changes to performance metrics"
-msgstr ""
-
-msgid "ciReport|Performance metrics"
-msgstr ""
-
-msgid "ciReport|SAST"
-msgstr ""
-
-msgid "ciReport|SAST detected"
-msgstr ""
-
-msgid "ciReport|SAST detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|SAST detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|SAST:container no vulnerabilities were found"
-msgstr ""
-
-msgid "ciReport|Security scanning"
-msgstr ""
-
-msgid "ciReport|Security scanning failed loading any results"
-msgstr ""
-
-msgid "ciReport|Show complete code vulnerabilities report"
-msgstr ""
-
-msgid "ciReport|Unapproved vulnerabilities (red) can be marked as approved. %{helpLink}"
-msgstr ""
-
-msgid "ciReport|no vulnerabilities"
-msgstr ""
-
msgid "command line instructions"
msgstr ""
msgid "connecting"
msgstr ""
-msgid "could not read private key, is the passphrase correct?"
-msgstr ""
-
msgid "day"
msgid_plural "days"
msgstr[0] ""
msgstr[1] ""
-msgid "detected %d fixed vulnerability"
-msgid_plural "detected %d fixed vulnerabilities"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "detected %d new vulnerability"
-msgid_plural "detected %d new vulnerabilities"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "detected no vulnerabilities"
+msgid "deploy token"
msgstr ""
-msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
+msgid "disabled"
msgstr ""
-msgid "here"
-msgstr ""
-
-msgid "importing"
+msgid "enabled"
msgstr ""
-msgid "in progress"
-msgstr ""
-
-msgid "is invalid because there is downstream lock"
+msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
msgstr ""
-msgid "is invalid because there is upstream lock"
+msgid "for this project"
msgstr ""
-msgid "is not a valid X509 certificate."
+msgid "importing"
msgstr ""
-msgid "locked by %{path_lock_user_name} %{created_at}"
+msgid "latest version"
msgstr ""
msgid "merge request"
@@ -5065,28 +5355,7 @@ msgstr ""
msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage is %{emphasisStart} unchanged %{emphasisEnd} at %{memoryFrom}MB"
msgstr ""
-msgid "mrWidget|Add approval"
-msgstr ""
-
-msgid "mrWidget|Allows edits from maintainers"
-msgstr ""
-
-msgid "mrWidget|An error occured while removing your approval."
-msgstr ""
-
-msgid "mrWidget|An error occured while retrieving approval data for this merge request."
-msgstr ""
-
-msgid "mrWidget|An error occured while submitting your approval."
-msgstr ""
-
-msgid "mrWidget|Approve"
-msgstr ""
-
-msgid "mrWidget|Approved"
-msgstr ""
-
-msgid "mrWidget|Approved by"
+msgid "mrWidget|Allows commits from members who can merge to the target branch"
msgstr ""
msgid "mrWidget|Cancel automatic merge"
@@ -5113,6 +5382,9 @@ msgstr ""
msgid "mrWidget|Closes"
msgstr ""
+msgid "mrWidget|Create an issue to resolve them later"
+msgstr ""
+
msgid "mrWidget|Deployment statistics are not available currently"
msgstr ""
@@ -5167,9 +5439,6 @@ msgstr ""
msgid "mrWidget|Remove source branch"
msgstr ""
-msgid "mrWidget|Remove your approval"
-msgstr ""
-
msgid "mrWidget|Request to merge"
msgstr ""
@@ -5209,6 +5478,9 @@ msgstr ""
msgid "mrWidget|There are merge conflicts"
msgstr ""
+msgid "mrWidget|There are unresolved discussions. Please resolve these discussions"
+msgstr ""
+
msgid "mrWidget|This merge request failed to be merged automatically"
msgstr ""
@@ -5259,7 +5531,7 @@ msgstr ""
msgid "personal access token"
msgstr ""
-msgid "private key does not match certificate."
+msgid "remaining"
msgstr ""
msgid "remove due date"
@@ -5274,9 +5546,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "to help your contributors communicate effectively!"
-msgstr ""
-
msgid "username"
msgstr ""
@@ -5286,3 +5555,8 @@ msgstr ""
msgid "with %{additions} additions, %{deletions} deletions."
msgstr ""
+msgid "within %d minute "
+msgid_plural "within %d minutes "
+msgstr[0] ""
+msgstr[1] ""
+
diff --git a/locale/fr/gitlab.po b/locale/fr/gitlab.po
index 3f30c5c4b87..a7627d3f774 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: 2018-04-04 19:35+0200\n"
-"PO-Revision-Date: 2018-04-05 03:38-0400\n"
+"POT-Creation-Date: 2018-07-01 16:35+1000\n"
+"PO-Revision-Date: 2018-07-02 17:23\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: French\n"
"Language: fr_FR\n"
@@ -16,8 +16,10 @@ msgstr ""
"X-Crowdin-Language: fr\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-msgid " and"
-msgstr " et"
+msgid "%d changed file"
+msgid_plural "%d changed files"
+msgstr[0] "%d fichier modifié"
+msgstr[1] "%d fichiers modifiés"
msgid "%d commit"
msgid_plural "%d commits"
@@ -27,7 +29,7 @@ msgstr[1] "%d commits"
msgid "%d commit behind"
msgid_plural "%d commits behind"
msgstr[0] "%d commit de retard"
-msgstr[1] "%d commits de retard"
+msgstr[1] "%d commits de retard"
msgid "%d exporter"
msgid_plural "%d exporters"
@@ -37,27 +39,37 @@ msgstr[1] "%d exportateurs"
msgid "%d issue"
msgid_plural "%d issues"
msgstr[0] "%d ticket"
-msgstr[1] "%d tickets"
+msgstr[1] "%d tickets"
msgid "%d layer"
msgid_plural "%d layers"
msgstr[0] "%d couche"
-msgstr[1] "%d couches"
+msgstr[1] "%d couches"
msgid "%d merge request"
msgid_plural "%d merge requests"
msgstr[0] "%d demande de fusion"
-msgstr[1] "%d demandes de fusion"
+msgstr[1] "%d demandes de fusion"
msgid "%d metric"
msgid_plural "%d metrics"
msgstr[0] "%d métrique"
-msgstr[1] "%d métriques"
+msgstr[1] "%d métriques"
+
+msgid "%d staged change"
+msgid_plural "%d staged changes"
+msgstr[0] "%d changement à valider"
+msgstr[1] "%d changements à valider"
+
+msgid "%d unstaged change"
+msgid_plural "%d unstaged changes"
+msgstr[0] "%d changement qui ne sera pas validé"
+msgstr[1] "%d changements qui ne seront pas validés"
msgid "%s additional commit has been omitted to prevent performance issues."
msgid_plural "%s additional commits have been omitted to prevent performance issues."
-msgstr[0] "%s validation supplémentaire a été masquée afin d'éviter de créer de problèmes de performances."
-msgstr[1] "%s commits supplémentaires ont été masqués afin d'éviter un problème de performance."
+msgstr[0] "%s commit supplémentaire a été ignoré afin d’éviter de causer des problèmes de performance."
+msgstr[1] "%s commits supplémentaires ont été ignorés afin d’éviter de causer des problèmes de performance."
msgid "%{actionText} & %{openOrClose} %{noteable}"
msgstr "%{actionText} et %{openOrClose} %{noteable}"
@@ -67,8 +79,14 @@ msgstr "%{commit_author_link} a créé %{commit_timeago}"
msgid "%{count} participant"
msgid_plural "%{count} participants"
-msgstr[0] "%{count} participant•e"
-msgstr[1] "%{count} participant•e•s"
+msgstr[0] "%{count} participant·e"
+msgstr[1] "%{count} participant·e·s"
+
+msgid "%{filePath} deleted"
+msgstr "%{filePath} supprimé"
+
+msgid "%{group_docs_link_start}Groups%{group_docs_link_end} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects."
+msgstr "Les %{group_docs_link_start}groupes%{group_docs_link_end} vous permettent de gérer plusieurs projets et d’y collaborer. Les membres d’un groupe ont accès à tous ses projets."
msgid "%{loadingIcon} Started"
msgstr "%{loadingIcon} Démarré"
@@ -76,11 +94,14 @@ msgstr "%{loadingIcon} Démarré"
msgid "%{lock_path} is locked by GitLab User %{lock_user_id}"
msgstr "%{lock_path} est verrouillé par l’utilisateur GitLab %{lock_user_id}"
+msgid "%{nip_domain} can be used as an alternative to a custom domain."
+msgstr "%{nip_domain} peut être utilisé comme alternative à un domaine personnalisé."
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
-msgstr "%{number_commits_behind} commits de retard sur %{default_branch}, %{number_commits_ahead} commits d'avance"
+msgstr "%{number_commits_behind} commits de retard sur %{default_branch}, %{number_commits_ahead} commits 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."
+msgstr "%{number_of_failures} sur %{maximum_failures} tentative(s). GitLab va vous permettre d’accéder à la prochaine tentative."
msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will not retry automatically. Reset storage information when the problem is resolved."
msgstr "%{number_of_failures} échecs sur %{maximum_failures}. GitLab ne va plus réessayer automatiquement. Réinitialisez les informations de stockage lorsque le problème est résolu."
@@ -88,90 +109,164 @@ msgstr "%{number_of_failures} échecs sur %{maximum_failures}. GitLab ne va plus
msgid "%{openOrClose} %{noteable}"
msgstr "%{openOrClose} %{noteable}"
+msgid "%{percent}%% complete"
+msgstr "%{percent} %% effectués"
+
msgid "%{storage_name}: failed storage access attempt on host:"
msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:"
-msgstr[0] "%{storage_name} : la tentative d’accès au stockage a échouée sur l’hôte :"
-msgstr[1] "%{storage_name} : %{failed_attempts} tentatives d’accès au stockage ont échouées :"
+msgstr[0] "%{storage_name} : la tentative d’accès au stockage a échoué sur l’hôte :"
+msgstr[1] "%{storage_name} : %{failed_attempts} tentatives d’accès au stockage ont échoué :"
msgid "%{text} is available"
msgstr "%{text} est disponible"
+msgid "%{title} changes"
+msgstr "Changements %{title}"
+
+msgid "%{unstaged} unstaged and %{staged} staged changes"
+msgstr "%{staged} changements prêts à être validés et %{unstaged} autres changements"
+
msgid "(checkout the %{link} for information on how to install it)."
-msgstr "(Lisez %{link} pour savoir comment l'installer)."
+msgstr "(lisez %{link} pour savoir comment l’installer)."
msgid "+ %{moreCount} more"
-msgstr "+ %{moreCount} de plus"
+msgstr "+ %{moreCount} de plus"
+
+msgid "- Runner is active and can process any new jobs"
+msgstr "- l’exécuteur est actif et peut traiter de nouvelles tâches"
+
+msgid "- Runner is paused and will not receive any new jobs"
+msgstr "- l’exécuteur est en pause et ne recevra pas de nouvelles tâches"
msgid "- show less"
msgstr "- en montrer moins"
+msgid "1 %{type} addition"
+msgid_plural "%{count} %{type} additions"
+msgstr[0] "un ajout de %{type}"
+msgstr[1] "%{count} ajouts de %{type}"
+
+msgid "1 %{type} modification"
+msgid_plural "%{count} %{type} modifications"
+msgstr[0] "une modification de %{type}"
+msgstr[1] "%{count} modifications de %{type}"
+
+msgid "1 closed issue"
+msgid_plural "%d closed issues"
+msgstr[0] "un ticket fermé"
+msgstr[1] "%d tickets fermés"
+
+msgid "1 closed merge request"
+msgid_plural "%d closed merge requests"
+msgstr[0] "une demande de fusion fermée"
+msgstr[1] "%d demandes de fusion fermées"
+
+msgid "1 merged merge request"
+msgid_plural "%d merged merge requests"
+msgstr[0] "une demande de fusion fusionnée"
+msgstr[1] "%d demandes de fusion fusionnées"
+
+msgid "1 open issue"
+msgid_plural "%d open issues"
+msgstr[0] "un ticket ouvert"
+msgstr[1] "%d tickets ouverts"
+
+msgid "1 open merge request"
+msgid_plural "%d open merge requests"
+msgstr[0] "une demande de fusion ouverte"
+msgstr[1] "%d demandes de fusion ouvertes"
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] "1 pipeline"
msgstr[1] "%d pipelines"
msgid "1st contribution!"
-msgstr "1ère contribution !"
+msgstr "1ʳᵉ contribution !"
msgid "2FA enabled"
msgstr "2FA activé"
+msgid "403|Please contact your GitLab administrator to get the permission."
+msgstr "Veuillez contacter votre administrateur GitLab afin d’obtenir l’autorisation."
+
+msgid "403|You don't have the permission to access this page."
+msgstr "Vous n’avez pas l’autorisation d’accéder à cette page."
+
+msgid "404|Make sure the address is correct and the page hasn't moved."
+msgstr "Assurezâ€vous que l’adresse est correcte et que la page n’a pas été déplacée."
+
+msgid "404|Page Not Found"
+msgstr "Page introuvable"
+
+msgid "404|Please contact your GitLab administrator if you think this is a mistake."
+msgstr "Veuillez contacter votre administrateur GitLab si vous pensez qu’il s’agit d’une erreur."
+
msgid "<strong>Removes</strong> source branch"
msgstr "<strong>Supprime</strong> la branche source"
+msgid "A 'Runner' is a process which runs a job. You can setup as many Runners as you need."
+msgstr "Un « exécuteur » est un processus qui exécute une tâche. Vous pouvez configurer autant d’exécuteurs que nécessaire."
+
msgid "A collection of graphs regarding Continuous Integration"
-msgstr "Un ensemble de graphiques concernant l’Intégration Continue (CI)"
+msgstr "Un ensemble de graphiques concernant l’intégration continue (CI)"
msgid "A new branch will be created in your fork and a new merge request will be started."
-msgstr "Une nouvelle branche sera créée dans votre fourche et une nouvelle demande de fusion sera lancée."
+msgstr "Une nouvelle branche sera créée dans votre dépôt divergent (fork) et une nouvelle demande de fusion sera lancée."
msgid "A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}."
msgstr "Un projet est l’endroit où vous hébergez vos fichiers (dépôt), planifiez votre travail (tickets) et publiez votre documentation (wiki), %{among_other_things_link}."
+msgid "A regular expression that will be used to find the test coverage output in the job trace. Leave blank to disable"
+msgstr ""
+
msgid "A user with write access to the source branch selected this option"
-msgstr "Un•e utilisateur•rice avec un accès en écriture à la branche source a sélectionné cette option"
+msgstr "Une personne avec un accès en écriture à la branche source a sélectionné cette option"
msgid "About auto deploy"
-msgstr "À propos de l'auto-déploiement"
+msgstr "À propos de l’autoâ€déploiement"
msgid "Abuse Reports"
msgstr "Rapports d’abus"
msgid "Abuse reports"
-msgstr ""
+msgstr "Rapports d’abus"
+
+msgid "Accept terms"
+msgstr "Accepter les conditions"
msgid "Access Tokens"
-msgstr "Jetons d'Accès"
+msgstr "Jetons d’accès"
msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again."
-msgstr "L'accès aux stockages défaillants a été temporairement désactivé pour permettre au montage de récupérer. Réinitialiser les informations de stockage dès que le problème est résolu pour permettre l’accès à nouveau."
+msgstr "L’accès aux stockages défaillants a été temporairement désactivé pour permettre la récupération du montage. Réinitialisez les informations de stockage quand le problème sera résolu pour permettre à nouveau l’accès."
+
+msgid "Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report."
+msgstr ""
msgid "Account"
msgstr "Compte"
-msgid "Account and limit settings"
-msgstr "Paramètres de compte et de limitation"
+msgid "Account and limit"
+msgstr "Limitations du compte"
msgid "Active"
msgstr "Actif"
+msgid "Active Sessions"
+msgstr "Sessions actives"
+
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 Kubernetes cluster"
-msgstr "Ajouter un cluster Kubernetes"
+msgstr "Ajouter une grappe de serveurs Kubernetes"
msgid "Add License"
msgstr "Ajouter une licence"
@@ -182,23 +277,26 @@ msgstr "Ajouter un fichier Readme"
msgid "Add new directory"
msgstr "Ajouter un nouveau dossier"
+msgid "Add reaction"
+msgstr "Ajouter une réaction"
+
msgid "Add todo"
-msgstr "Ajouter à la liste à faire"
+msgstr "Ajouter à la liste « à faire »"
msgid "AdminArea|Stop all jobs"
-msgstr "Arrêtez tous les travaux"
+msgstr "Arrêter toutes les tâches"
msgid "AdminArea|Stop all jobs?"
-msgstr "Arrêtez tous les travaux?"
+msgstr "Arrêter toutes les tâches ?"
msgid "AdminArea|Stop jobs"
-msgstr "Arrêtez les travaux"
+msgstr "Arrêter les tâches"
msgid "AdminArea|Stopping jobs failed"
-msgstr "L’arrêt des travaux a échoué"
+msgstr "L’arrêt des tâches a échoué"
msgid "AdminArea|You’re about to stop all jobs.This will halt all current jobs that are running."
-msgstr "Vous êtes sur le point d’arrêter tous les travaux. Tous les travaux en cours seront interrompus."
+msgstr "Vous êtes sur le point d’arrêter toutes les tâches. Toutes les tâches en cours seront interrompues."
msgid "AdminHealthPageLink|health page"
msgstr "État des services"
@@ -207,7 +305,7 @@ msgid "AdminProjects|Delete"
msgstr "Supprimer"
msgid "AdminProjects|Delete Project %{projectName}?"
-msgstr "Supprimer le projet %{projectName} ?"
+msgstr "Supprimer le projet %{projectName} ?"
msgid "AdminProjects|Delete project"
msgstr "Supprimer le projet"
@@ -216,19 +314,19 @@ msgid "AdminSettings|Specify a domain to use by default for every project's Auto
msgstr "Spécifiez un domaine à utiliser par défaut pour les étapes Auto Review Apps et Auto Deploy de chaque projet."
msgid "AdminUsers|Block user"
-msgstr "Bloquer l’utilisateur•rice"
+msgstr "Bloquer ce compte"
msgid "AdminUsers|Delete User %{username} and contributions?"
-msgstr "Supprimer l’utilisateur•rice %{username} et ses contributions ?"
+msgstr "Supprimer le compte %{username} et ses contributions ?"
msgid "AdminUsers|Delete User %{username}?"
-msgstr "Supprimer l’utilisateur•rice %{username} ?"
+msgstr "Supprimer le compte %{username} ?"
msgid "AdminUsers|Delete user"
-msgstr "Supprimer un•e utilisateur•rice"
+msgstr "Supprimer un compte utilisateur"
msgid "AdminUsers|Delete user and contributions"
-msgstr "Supprimer l’utilisateur•rice et ses contributions"
+msgstr "Supprimer le compte et ses contributions"
msgid "AdminUsers|To confirm, type %{projectName}"
msgstr "Pour confirmer, veuillez saisir %{projectName}"
@@ -237,7 +335,7 @@ msgid "AdminUsers|To confirm, type %{username}"
msgstr "Pour confirmer, veuillez saisir %{username}"
msgid "Advanced"
-msgstr "Avancé"
+msgstr "Paramètres avancés"
msgid "Advanced settings"
msgstr "Paramètres avancés"
@@ -251,53 +349,62 @@ msgstr "Toutes les modifications sont validées"
msgid "All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings."
msgstr "Toutes les fonctionnalités sont activées pour les projets vierges, à partir de modèles ou lors de l’importation, mais vous pouvez les désactiver ultérieurement dans les paramètres du projet."
-msgid "Allow edits from maintainers."
-msgstr "Autoriser les modifications par les mainteneur•se•s."
+msgid "Allow commits from members who can merge to the target branch."
+msgstr "Autoriser les commits des membres qui peuvent fusionner dans la branche cible."
+
+msgid "Allow public access to pipelines and job details, including output logs and artifacts"
+msgstr "Autoriser l’accès public aux pipelines et aux détails des tâches, y compris les journaux de sortie et les artefacts"
msgid "Allow rendering of PlantUML diagrams in Asciidoc documents."
-msgstr ""
+msgstr "Permettre le rendu des diagrammes PlantUML dans les documents Asciidoc."
msgid "Allow requests to the local network from hooks and services."
-msgstr ""
+msgstr "Autoriser les requêtes sur le réseau local à partir de hooks et de services."
msgid "Allows you to add and manage Kubernetes clusters."
-msgstr "Vous permet d’ajouter et de gérer des clusters Kubernetes."
+msgstr "Vous permet d’ajouter et de gérer des grappes de serveurs Kubernetes."
-msgid "Also called \"Issuer\" or \"Relying party trust identifier\""
-msgstr ""
+msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
+msgstr "Vous pouvez également utiliser un %{personal_access_token_link}. Lorsque vous créerez votre jeton d’accès, vous devrez sélectionner le champ <code>repo</code>, afin que nous puissions afficher une liste de vos dépôts publics et privés qui sont disponibles pour être importés."
-msgid "Also called \"Relying party service URL\" or \"Reply URL\""
-msgstr ""
+msgid "An error occured creating the new branch."
+msgstr "Une erreur est survenue lors de la création de la nouvelle branche."
-msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to connect."
-msgstr "Alternativement, vous pouvez utiliser un %{personal_access_token_link}. Lorsque vous créez votre jeton d’accès, vous devrez sélectionner le champ <code>repo</code>, afin que nous puissions afficher une liste de vos dépôts publics et privés qui sont disponibles pour se connecter."
+msgid "An error occured whilst loading all the files."
+msgstr "Une erreur est survenue lors du chargement de l’ensemble des fichiers."
-msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
-msgstr "Alternativement, vous pouvez utiliser un %{personal_access_token_link}. Lorsque vous créez votre jeton d’accès, vous devrez sélectionner le champ <code>repo</code>, afin que nous puissions afficher une liste de vos dépôts publics et privés qui sont disponibles pour être importés."
+msgid "An error occured whilst loading the file content."
+msgstr "Une erreur est survenue lors du chargement du contenu du fichier."
+
+msgid "An error occured whilst loading the file."
+msgstr "Une erreur est survenue lors du chargement du fichier."
+
+msgid "An error occured whilst loading the merge request changes."
+msgstr "Une erreur est survenue lors du chargement des modifications de la demande de fusion."
+
+msgid "An error occured whilst loading the merge request version data."
+msgstr "Une erreur est survenue lors du chargement des données de version de la demande de fusion."
+
+msgid "An error occured whilst loading the merge request."
+msgstr "Une erreur est survenue lors du chargement de la demande de fusion."
msgid "An error occurred previewing the blob"
msgstr "Une erreur s’est produite lors de la prévisualisation du blob"
msgid "An error occurred when toggling the notification subscription"
-msgstr "Une erreur s’est produite lors de l’activation/désactivation de l’abonnement aux notifications"
-
-msgid "An error occurred when updating the issue weight"
-msgstr "Une erreur s'est produite lors de la mise à jour du poids du ticket"
-
-msgid "An error occurred while adding approver"
-msgstr "Une erreur s’est produite lors de l’ajout de l'approbateur•rice"
+msgstr "Une erreur s’est produite lors de l’activation ou la désactivation de l’abonnement aux notifications"
-msgid "An error occurred while detecting host keys"
-msgstr "Une erreur s’est produite lors de la détection des clés de l'hôte"
+msgid "An error occurred while dismissing the alert. Refresh the page and try again."
+msgstr "Une erreur s’est produite lors de la révocation de l’alerte. Actualisez la page et essayez à nouveau."
msgid "An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again."
msgstr "Une erreur s’est produite lors de la révocation de la mise en avant de la fonctionnalité. Actualisez la page et essayez de la révoquer à nouveau."
msgid "An error occurred while fetching markdown preview"
-msgstr "Une erreur s'est produite lors de la prévisualisation markdown"
+msgstr "Une erreur s’est produite lors de la prévisualisation markdown"
msgid "An error occurred while fetching sidebar data"
-msgstr "Une erreur s'est produite lors de la récupération des données de la barre latérale"
+msgstr "Une erreur s’est produite lors de la récupération des données de la barre latérale"
msgid "An error occurred while fetching the pipeline."
msgstr "Une erreur est survenue pendant la récupération du pipeline."
@@ -305,11 +412,8 @@ msgstr "Une erreur est survenue pendant la récupération du pipeline."
msgid "An error occurred while getting projects"
msgstr "Une erreur s’est produite lors de la récupération des projets"
-msgid "An error occurred while importing project"
-msgstr "Une erreur s’est produite lors de l’importation du projet"
-
-msgid "An error occurred while initializing path locks"
-msgstr "Une erreur s’est produite lors de l’initialisation des verrous de chemin"
+msgid "An error occurred while importing project: ${details}"
+msgstr "Une erreur s’est produite lors de l’importation du projet : ${details}"
msgid "An error occurred while loading commits"
msgstr "Une erreur s’est produite lors du chargement des commits"
@@ -326,9 +430,6 @@ msgstr "Une erreur s’est produite lors du chargement du fichier"
msgid "An error occurred while making the request."
msgstr "Une erreur s’est produite lors de la requête."
-msgid "An error occurred while removing approver"
-msgstr "Une erreur s’est produite lors de la suppression de l’approbateur•rice"
-
msgid "An error occurred while rendering KaTeX"
msgstr "Une erreur s’est produite lors du rendu de KaTeX"
@@ -341,20 +442,14 @@ msgstr "Une erreur s’est produite lors de la récupération de l’activité d
msgid "An error occurred while retrieving diff"
msgstr "Une erreur s’est produite lors de la récupération du diff"
-msgid "An error occurred while saving LDAP override status. Please try again."
-msgstr "Une erreur s’est produite lors de l’enregistrement du statut de remplacement LDAP. Veuillez réessayer."
-
msgid "An error occurred while saving assignees"
msgstr "Une erreur s’est produite lors de l’enregistrement des destinataires"
msgid "An error occurred while validating username"
-msgstr "Une erreur s’est produite lors de la validation du nom d’utilisateur•rice"
+msgstr "Une erreur s’est produite lors de la validation du nom d’utilisateur·rice"
msgid "An error occurred. Please try again."
-msgstr "Une erreur s’est produite. Veuillez réessayer."
-
-msgid "Any Label"
-msgstr "Tout label"
+msgstr "Une erreur est survenue. Merci de réessayer."
msgid "Appearance"
msgstr "Apparence"
@@ -363,40 +458,40 @@ msgid "Applications"
msgstr "Applications"
msgid "Apr"
-msgstr "Avr."
+msgstr "avr."
msgid "April"
-msgstr "Avril"
+msgstr "avril"
-msgid "Archived project! Repository is read-only"
-msgstr "Projet archivé ! Le dépôt est en lecture seule"
+msgid "Archived project! Repository and other project resources are read-only"
+msgstr "Projet archivé ! Le dépôt et les autres ressources du projet sont en lecture seule"
msgid "Are you sure you want to delete this pipeline schedule?"
-msgstr "Êtes-vous sûr·e 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 remove this identity?"
+msgstr "Êtes-vous sûr·e de vouloir supprimer cette identité ?"
msgid "Are you sure you want to reset registration token?"
-msgstr "Êtes-vous sûr·e 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é ?"
-
-msgid "Are you sure you want to unlock %{path_lock_path}?"
-msgstr "Êtes-vous sûr•e de vouloir déverrouiller %{path_lock_path} ?"
+msgstr "Êtesâ€vous sûr·e de vouloir réinitialiser le jeton de bilan de santé ?"
msgid "Are you sure?"
-msgstr "Êtes-vous certain ?"
+msgstr "Êtesâ€vous certain(e) ?"
msgid "Artifacts"
msgstr "Artéfacts"
-msgid "Assertion consumer service URL"
-msgstr ""
+msgid "Ask your group maintainer to setup a group Runner."
+msgstr "Demandez au responsable du groupe de configurer un exécuteur de groupe."
msgid "Assign custom color like #FF0000"
msgstr "Attribuer une couleur personnalisée comme #FF0000"
msgid "Assign labels"
-msgstr "Attribuer des labels"
+msgstr "Attribuer des étiquettes"
msgid "Assign milestone"
msgstr "Attribuer un jalon"
@@ -411,34 +506,40 @@ msgid "Assigned Merge Requests"
msgstr "Demandes de fusion assignées"
msgid "Assigned to :name"
-msgstr "Assigné•e à :name"
+msgstr "Assigné·e à :name"
+
+msgid "Assigned to me"
+msgstr "Assigné à moi"
msgid "Assignee"
-msgstr "Assigné•e"
+msgstr "Assigné·e"
+
+msgid "Assignee(s)"
+msgstr "Assigné·e(s)"
msgid "Attach a file by drag &amp; drop or %{upload_link}"
-msgstr "Attachez un fichier par glisser &amp; déposer ou %{upload_link}"
+msgstr "Attachez un fichier par glisserâ€déposer ou %{upload_link}"
msgid "Aug"
-msgstr "Août"
+msgstr "août"
msgid "August"
-msgstr "Août"
+msgstr "août"
msgid "Authentication Log"
-msgstr "Journal d'authentification"
+msgstr "Journal d’authentification"
msgid "Author"
-msgstr "Auteur"
+msgstr "Auteur·e"
msgid "Authors: %{authors}"
-msgstr "Auteur•e•s : %{authors}"
+msgstr "Auteur·e·s : %{authors}"
msgid "Auto DevOps enabled"
msgstr "Auto DevOps activé"
msgid "Auto DevOps, runners and job artifacts"
-msgstr ""
+msgstr "Auto DevOps, exécuteurs et artéfacts de tâches"
msgid "Auto Review Apps and Auto Deploy need a %{kubernetes} to work correctly."
msgstr "Auto Review Apps et Auto Deploy ont besoin d’un %{kubernetes} qui fonctionne correctement."
@@ -447,103 +548,136 @@ msgid "Auto Review Apps and Auto Deploy need a domain name and a %{kubernetes} t
msgstr "Auto Review Apps et Auto Deploy ont besoin d’un nom de domaine et d’un %{kubernetes} qui fonctionne correctement."
msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
-msgstr "Les applications de révision automatique et de déploiement automatique requièrent un nom de domaine pour fonctionner correctement."
+msgstr "Auto Review Apps et Auto Deploy ont besoin d’un nom de domaine qui fonctionne correctement."
+
+msgid "Auto-cancel redundant, pending pipelines"
+msgstr ""
-msgid "AutoDevOps|Auto DevOps (Beta)"
-msgstr "Auto DevOps (Béta)"
+msgid "AutoDevOps|Auto DevOps"
+msgstr "Auto DevOps"
msgid "AutoDevOps|Auto DevOps documentation"
-msgstr "Documentation Auto DevOps"
+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."
+msgstr "Auto DevOps va automatiquement construire, tester et déployer votre application selon une configuration prédéfinie d’intégration et de livraison continues."
msgid "AutoDevOps|Learn more in the %{link_to_documentation}"
-msgstr "En savoir plus dans %{link_to_documentation}"
+msgstr "Apprenezâ€en davantage en consultant la %{link_to_documentation}"
msgid "AutoDevOps|You can automatically build and test your application if you %{link_to_auto_devops_settings} for this project. You can automatically deploy it as well, if you %{link_to_add_kubernetes_cluster}."
msgstr "Vous pouvez automatiquement générer et tester votre application si vous %{link_to_auto_devops_settings} pour ce projet. Vous pouvez aussi la déployer automatiquement, si vous %{link_to_add_kubernetes_cluster}."
msgid "AutoDevOps|add a Kubernetes cluster"
-msgstr "ajouter un cluster Kubernetes"
+msgstr "ajoutez une grappe de serveurs Kubernetes"
-msgid "AutoDevOps|enable Auto DevOps (Beta)"
-msgstr "activer Auto DevOps (Bêta)"
+msgid "AutoDevOps|enable Auto DevOps"
+msgstr "activez Auto DevOps"
msgid "Available"
msgstr "Disponible"
+msgid "Available group Runners : %{runners}"
+msgstr "Exécuteurs de groupe disponibles : %{runners}"
+
+msgid "Available group Runners : %{runners}."
+msgstr "Exécuteurs de groupe disponibles : %{runners}."
+
msgid "Avatar will be removed. Are you sure?"
-msgstr "L’avatar sera supprimé. Êtes-vous sûr•e ?"
+msgstr "L’avatar sera supprimé. Êtesâ€vous sûr·e ?"
msgid "Average per day: %{average}"
-msgstr "Moyenne par jour : %{average}"
+msgstr "Moyenne par jour : %{average}"
-msgid "Background Color"
-msgstr ""
+msgid "Background color"
+msgstr "Couleur d’arrièreâ€plan"
msgid "Background jobs"
-msgstr ""
+msgstr "Tâches de fond"
-msgid "Begin with the selected commit"
-msgstr "Commencer avec le commit sélectionné"
+msgid "Badges"
+msgstr "Badges"
-msgid "Billing"
-msgstr "Facturation"
+msgid "Badges|A new badge was added."
+msgstr "Un nouveau badge a été ajouté."
-msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan."
-msgstr "%{group_name} est actuellement abonné au forfait %{plan_link}."
+msgid "Badges|Add badge"
+msgstr "Ajouter un badge"
-msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available."
-msgstr "La mise à niveau de certains abonnements n’est actuellement pas disponible."
+msgid "Badges|Adding the badge failed, please check the entered URLs and try again."
+msgstr "L’ajout du badge a échoué, veuillez vérifier les URL entrées et réessayez."
-msgid "BillingPlans|Current plan"
-msgstr "Abonnement actuel"
+msgid "Badges|Badge image URL"
+msgstr "URL de l’image du badge"
-msgid "BillingPlans|Customer Support"
-msgstr "Support client"
+msgid "Badges|Badge image preview"
+msgstr "Aperçu de l’image du badge"
-msgid "BillingPlans|Downgrade"
-msgstr "Retour à un forfait inférieur"
+msgid "Badges|Delete badge"
+msgstr "Supprimer le badge"
-msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
-msgstr "En savoir plus sur chaque forfait en lisant nos %{faq_link}."
+msgid "Badges|Delete badge?"
+msgstr "Supprimer le badge ?"
-msgid "BillingPlans|Manage plan"
-msgstr "Gérer l'abonnement"
+msgid "Badges|Deleting the badge failed, please try again."
+msgstr "La suppression du badge a échoué, veuillez réessayer."
-msgid "BillingPlans|Please contact %{customer_support_link} in that case."
-msgstr "Merci de contacter %{customer_support_link} à ce sujet."
+msgid "Badges|Group Badge"
+msgstr "Badge de groupe"
-msgid "BillingPlans|See all %{plan_name} features"
-msgstr "Voir toutes les fonctionnalités du forfait %{plan_name}"
+msgid "Badges|Link"
+msgstr "Lien"
-msgid "BillingPlans|This group uses the plan associated with its parent group."
-msgstr "Ce groupe utilise le forfait associé à son groupe parent."
+msgid "Badges|No badge image"
+msgstr "Pas d’image de badge"
-msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
-msgstr "Pour gérer l‘abonnement de ce groupe, visitez la section facturation de %{parent_billing_page_link}."
+msgid "Badges|No image to preview"
+msgstr "Pas d’image à prévisualiser"
-msgid "BillingPlans|Upgrade"
-msgstr "Mise à niveau"
+msgid "Badges|Project Badge"
+msgstr "Badge de projet"
-msgid "BillingPlans|You are currently on the %{plan_link} plan."
-msgstr "Vous êtes actuellement abonné·e au forfait %{plan_link}."
+msgid "Badges|Reload badge image"
+msgstr "Recharger l’image de badge"
-msgid "BillingPlans|frequently asked questions"
-msgstr "foire aux questions"
+msgid "Badges|Save changes"
+msgstr "Enregistrer les modifications"
+
+msgid "Badges|Saving the badge failed, please check the entered URLs and try again."
+msgstr "L’enregistrement du badge a échoué. Veuillez vérifier les URL entrées et réessayer."
+
+msgid "Badges|The %{docsLinkStart}variables%{docsLinkEnd} GitLab supports: %{placeholders}"
+msgstr "Les %{docsLinkStart}variables%{docsLinkEnd} que GitLab prend en charge : %{placeholders}"
+
+msgid "Badges|The badge was deleted."
+msgstr "Le badge a été supprimé."
+
+msgid "Badges|The badge was saved."
+msgstr "Le badge a été enregistré."
+
+msgid "Badges|This group has no badges"
+msgstr "Ce groupe n’a pas de badge"
-msgid "BillingPlans|monthly"
-msgstr "mensuel"
+msgid "Badges|This project has no badges"
+msgstr "Ce projet n’a pas de badge"
-msgid "BillingPlans|paid annually at %{price_per_year}"
-msgstr "payé annuellement pour %{price_per_year}"
+msgid "Badges|Your badges"
+msgstr "Vos badges"
+
+msgid "Begin with the selected commit"
+msgstr "Commencer avec le commit sélectionné"
+
+msgid "Below are examples of regex for existing tools:"
+msgstr "Voici quelques exemples d’expressions rationnelles pour des outils existants :"
+
+msgid "Boards"
+msgstr "Tableaux"
-msgid "BillingPlans|per user"
-msgstr "par utilisateur"
+msgid "Branch %{branchName} was not found in this project's repository."
+msgstr "La branche %{branchName} n’a pas été trouvée dans le dépôt de ce projet."
msgid "Branch (%{branch_count})"
msgid_plural "Branches (%{branch_count})"
@@ -551,7 +685,7 @@ msgstr[0] "Branche (%{branch_count})"
msgstr[1] "Branches (%{branch_count})"
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"
@@ -563,7 +697,7 @@ msgid "Branch name"
msgstr "Nom de la branche"
msgid "BranchSwitcherPlaceholder|Search branches"
-msgstr "Rechercher la branche"
+msgstr "Rechercher les branches"
msgid "BranchSwitcherTitle|Switch branch"
msgstr "Changer de branche"
@@ -587,7 +721,7 @@ msgid "Branches|Compare"
msgstr "Comparer"
msgid "Branches|Delete all branches that are merged into '%{default_branch}'"
-msgstr "Supprimer toutes les branches qui ont été fusionnées dans '%{default_branch}'"
+msgstr "Supprimer toutes les branches qui ont été fusionnées dans « %{default_branch} »"
msgid "Branches|Delete branch"
msgstr "Supprimer cette branche"
@@ -599,13 +733,13 @@ msgid "Branches|Delete protected branch"
msgstr "Supprimer cette branche protégée"
msgid "Branches|Delete protected branch '%{branch_name}'?"
-msgstr "Supprimer la branche protégée '%{branch_name}' ?"
+msgstr "Supprimer la branche protégée « %{branch_name} » ?"
msgid "Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?"
-msgstr "La suppression de la branche '%{branch_name}' ne peut être annulée. Êtes-vous sûr ?"
+msgstr "La suppression de la branche « %{branch_name} » ne peut être annulée. Êtesâ€vous sûr·e ?"
msgid "Branches|Deleting the merged branches cannot be undone. Are you sure?"
-msgstr "La suppression des branches fusionnées ne peut être annulée. Êtes-vous sûr ?"
+msgstr "La suppression des branches fusionnées ne peut être annulée. Êtesâ€vous sûr·e ?"
msgid "Branches|Filter by branch name"
msgstr "Filtrer par nom de branche"
@@ -622,8 +756,8 @@ msgstr "Aucune branche à afficher"
msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
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 "Seulement un maître ou un propriétaire du projet peut supprimer une branche protégée"
+msgid "Branches|Only a project maintainer or owner can delete a protected branch"
+msgstr "Seul un responsable du projet ou son propriétaire peut supprimer une branche protégée"
msgid "Branches|Overview"
msgstr "Vue d’ensemble"
@@ -658,29 +792,20 @@ msgstr "Périmée"
msgid "Branches|Stale branches"
msgstr "Branches périmées"
-msgid "Branches|The branch could not be updated automatically because it has diverged from its upstream counterpart."
-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 "La branche par défaut ne peut pas être supprimée"
msgid "Branches|This branch hasn’t been merged into %{default_branch}."
-msgstr "Cette branche n'a pas été fusionnée dans %{default_branch}."
+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 "Afin d'éviter de perdre des données, il est conseillé de fusionner cette branche avant de la supprimer."
+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 "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 "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'."
+msgstr "Pour confirmer, veuillez saisir %{branch_name_confirmation} :"
msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
-msgstr "Vous êtes sur le point de supprimer définitivement la branche protégée %{branch_name}."
-
-msgid "Branches|diverged from upstream"
-msgstr "a dévié du dépôt en amont"
+msgstr "Vous êtes sur le point de supprimer définitivement la branche protégée « %{branch_name} »."
msgid "Branches|merged"
msgstr "fusionnée"
@@ -703,44 +828,83 @@ msgstr "Parcourir les fichiers"
msgid "Browse files"
msgstr "Parcourir les fichiers"
-msgid "Business"
-msgstr "Entreprise"
-
msgid "ByAuthor|by"
msgstr "par"
msgid "CI / CD"
-msgstr "Intégration continu / Déploiement continu"
+msgstr "Intégration et livraison continues"
-msgid "CI/CD"
-msgstr "CI/CD"
+msgid "CI / CD Settings"
+msgstr "Paramètres CI / CD (intégration et livraison continues)"
msgid "CI/CD configuration"
-msgstr "Configuration CI/CD"
+msgstr "Configuration de l’intégration et de la livraison continues"
+
+msgid "CI/CD settings"
+msgstr "Paramètres de l’intégration et de la livraison continues"
+
+msgid "CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery."
+msgstr "Un %{ci_file} explicite a besoin d’être spécifié avant que vous puissiez commencer à utiliser l’intégration et la livraison continues."
+
+msgid "CICD|Auto DevOps"
+msgstr "Auto DevOps"
+
+msgid "CICD|Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration."
+msgstr "Auto DevOps va automatiquement construire, tester et déployer votre application selon une configuration prédéfinie d’intégration et de livraison continues."
+
+msgid "CICD|Automatic deployment to staging, manual deployment to production"
+msgstr "Déploiement automatique pour « staging », déploiement manuel pour la production"
+
+msgid "CICD|Continuous deployment to production"
+msgstr "Déploiement continu en production"
+
+msgid "CICD|Deployment strategy"
+msgstr "Stratégie de déploiement"
-msgid "CI/CD for external repo"
-msgstr "CI / CD pour dépôt externe"
+msgid "CICD|Deployment strategy needs a domain name to work correctly."
+msgstr "La stratégie de déploiement nécessite un nom de domaine pour fonctionner correctement."
+
+msgid "CICD|Disable Auto DevOps"
+msgstr "Désactiver Auto DevOps"
+
+msgid "CICD|Enable Auto DevOps"
+msgstr "Activer Auto DevOps"
+
+msgid "CICD|Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific %{ci_file}."
+msgstr "Utiliser la valeur par défaut de l’instance pour avoir Auto DevOps activé ou désactivé quand il n’y a pas de %{ci_file} spécifique au projet."
+
+msgid "CICD|Instance default (%{state})"
+msgstr "Valeur par défaut de l’instance (%{state})"
msgid "CICD|Jobs"
msgstr "Tâches"
+msgid "CICD|Learn more about Auto DevOps"
+msgstr "En savoir plus à propos d’Auto DevOps"
+
+msgid "CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project."
+msgstr "La configuration de pipeline de l’Auto DevOps sera utilisée lorsqu’il n’y a pas de %{ci_file} dans le projet."
+
+msgid "CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages."
+msgstr "Vous devez spécifier un domaine si vous voulez utiliser la revue automatique d’applications Auto Review Apps et le déploiement automatique d’étapes Auto Deploy stages."
+
+msgid "Can't find HEAD commit for this branch"
+msgstr "Impossible de trouver le dernier commit (HEAD) pour cette branche"
+
msgid "Cancel"
msgstr "Annuler"
+msgid "Cancel this job"
+msgstr "Annuler cette tâche"
+
msgid "Cannot be merged automatically"
msgstr "Ne peut être fusionnée automatiquement"
msgid "Cannot modify managed Kubernetes cluster"
-msgstr "Impossible de modifier le cluster géré par Kubernetes"
-
-msgid "Certificate fingerprint"
-msgstr ""
-
-msgid "Change Weight"
-msgstr "Changer le poids"
+msgstr "Impossible de modifier la grappe de serveurs gérée par Kubernetes"
msgid "Change this value to influence how frequently the GitLab UI polls for updates."
-msgstr ""
+msgstr "Modifiez cette valeur pour influencer la fréquence d’interrogation de l’interface utilisateur GitLab pour les mises à jour."
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "Picorer dans la branche"
@@ -767,7 +931,7 @@ msgid "Charts"
msgstr "Statistiques"
msgid "Chat"
-msgstr "Chat"
+msgstr "Discussion"
msgid "Check interval"
msgstr "Intervalle de vérification"
@@ -776,7 +940,7 @@ msgid "Checking %{text} availability…"
msgstr "Vérification de la disponibilité de %{text}…"
msgid "Checking branch availability..."
-msgstr "Vérification de la disponibilité du nom de branche..."
+msgstr "Vérification de la disponibilité du nom de branche…"
msgid "Cherry-pick this commit"
msgstr "Picorer ce commit"
@@ -788,23 +952,20 @@ msgid "Choose File ..."
msgstr "Choisir le fichier…"
msgid "Choose a branch/tag (e.g. %{master}) or enter a commit (e.g. %{sha}) to see what's changed or to create a merge request."
-msgstr "Choisissez une branche / tag (par exemple %{master}) ou entrez un commit (par exemple %{sha}) pour voir ce qui a changé ou pour créer une demande de fusion."
+msgstr "Choisissez une branche ou une étiquette (par exemple %{master}) ou entrez un commit (par exemple %{sha}) pour voir ce qui a changé ou pour créer une demande de fusion."
-msgid "Choose file..."
-msgstr "Choisir le fichier…"
+msgid "Choose any color."
+msgstr "Choisissez n’importe quelle couleur."
-msgid "Choose which groups you wish to synchronize to this secondary node."
-msgstr "Choisissez les groupes que vous souhaitez synchroniser avec ce nœud secondaire."
+msgid "Choose between <code>clone</code> or <code>fetch</code> to get the recent application code"
+msgstr "Choisissez entre <code>clone</code> ou <code>fetch</code> pour obtenir les dernières modifications du code de l’application"
-msgid "Choose which repositories you want to connect and run CI/CD pipelines."
-msgstr "Choisissez à quels dépôts vous voulez connecter et exécuter des pipelines CI/CD."
+msgid "Choose file..."
+msgstr "Choisir le fichier…"
msgid "Choose which repositories you want to import."
msgstr "Choisissez les dépôts que vous voulez importer."
-msgid "Choose which shards you wish to synchronize to this secondary node."
-msgstr "Choisissez les partitions que vous souhaitez synchroniser avec ce nœud secondaire."
-
msgid "CiStatusLabel|canceled"
msgstr "annulé"
@@ -830,13 +991,13 @@ msgid "CiStatusLabel|skipped"
msgstr "ignoré"
msgid "CiStatusLabel|waiting for manual action"
-msgstr "en attente d'action manuelle"
+msgstr "en attente d’une action manuelle"
msgid "CiStatusText|blocked"
msgstr "bloqué"
msgid "CiStatusText|canceled"
-msgstr "annulé"
+msgstr "annulé "
msgid "CiStatusText|created"
msgstr "créé"
@@ -869,26 +1030,17 @@ msgid "CiVariables|Remove variable row"
msgstr "Supprimer cette variable"
msgid "CiVariable|* (All environments)"
-msgstr "* (Tous les environnements)"
+msgstr "* (tout environnement)"
msgid "CiVariable|All environments"
msgstr "Tous les environnements"
-msgid "CiVariable|Create wildcard"
-msgstr "Créer un caractère générique"
-
msgid "CiVariable|Error occured while saving variables"
msgstr "Une erreur s’est produite pendant la sauvegarde des variables"
-msgid "CiVariable|New environment"
-msgstr "Nouvel environnement"
-
msgid "CiVariable|Protected"
msgstr "Protégée"
-msgid "CiVariable|Search environments"
-msgstr "Chercher dans les environnements"
-
msgid "CiVariable|Toggle protected"
msgstr "Changer l’état de protection"
@@ -898,110 +1050,131 @@ msgstr "La validation a échoué"
msgid "CircuitBreakerApiLink|circuitbreaker api"
msgstr "CircuitBreaker API"
+msgid "Clear search input"
+msgstr "Vider le champ de recherche"
+
+msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
+msgstr "Cliquez sur n’importe quel <strong>nom de projet</strong> dans la liste des projets ciâ€dessous pour naviguer jusqu’au jalon du projet."
+
+msgid "Click the <strong>Promote</strong> button in the top right corner to promote it to a group milestone."
+msgstr "Cliquez sur le bouton <strong>Promouvoir</strong> en haut à droite pour le promouvoir en tant que jalon de groupe."
+
msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
msgstr "Cliquez sur le bouton ci-dessous pour lancer le processus d’installation en accédant à la page Kubernetes"
+msgid "Click to expand it."
+msgstr "Cliquez pour l’agrandir."
+
msgid "Click to expand text"
msgstr "Cliquez pour agrandir le texte"
-msgid "Client authentication certificate"
-msgstr "Certificat d’authentification du client"
-
-msgid "Client authentication key"
-msgstr "Clé d’authentification du client"
-
-msgid "Client authentication key password"
-msgstr "Mot de passe de la clé d’authentification client"
-
msgid "Clone repository"
msgstr "Cloner le dépôt"
msgid "Close"
msgstr "Fermer"
-msgid "Closed"
-msgstr "Fermée"
-
msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster"
-msgstr "%{appList} a été installé avec succès sur votre cluster Kubernetes"
+msgstr "%{appList} a été installé avec succès sur votre grappe de serveurs Kubernetes"
msgid "ClusterIntegration|API URL"
-msgstr "URL de l'API"
+msgstr "URL de l’API"
msgid "ClusterIntegration|Add Kubernetes cluster"
-msgstr "Ajouter un cluster Kubernetes"
+msgstr "Ajouter une grappe de serveurs Kubernetes"
msgid "ClusterIntegration|Add an existing Kubernetes cluster"
-msgstr "Ajouter un cluster Kubernetes existant"
+msgstr "Ajouter une grappe de serveurs Kubernetes existante"
msgid "ClusterIntegration|Advanced options on this Kubernetes cluster's integration"
-msgstr "Options avancées sur l’intégration de ce cluster Kubernetes"
+msgstr "Options avancées concernant l’intégration de cette grappe de serveurs Kubernetes"
+
+msgid "ClusterIntegration|An error occured while trying to fetch project zones: %{error}"
+msgstr "Une erreur est survenue lors de la tentative de récupération des zones du projet : %{error}"
+
+msgid "ClusterIntegration|An error occured while trying to fetch your projects: %{error}"
+msgstr "Une erreur est survenue lors de la tentative de récupération de vos projets : %{error}"
msgid "ClusterIntegration|Applications"
msgstr "Applications"
msgid "ClusterIntegration|Are you sure you want to remove this Kubernetes cluster's integration? This will not delete your actual Kubernetes cluster."
-msgstr "Êtes-vous sûr•e de vouloir supprimer l'intégration de ce cluster Kubernetes? Cela ne supprimera pas votre cluster Kubernetes."
+msgstr "Êtesâ€vous sûr·e de vouloir supprimer l’intégration de cette grappe de serveurs Kubernetes ? Cela ne supprimera pas votre grappe de serveurs Kubernetes."
msgid "ClusterIntegration|CA Certificate"
-msgstr "Certificat d‘autorité de certification"
+msgstr "Certificat d’autorité de certification"
msgid "ClusterIntegration|Certificate Authority bundle (PEM format)"
-msgstr "Paquet de l‘Autorité de certification (format PEM)"
+msgstr "Ensemble de certificats des autorités de certification (format PEM)"
msgid "ClusterIntegration|Choose how to set up Kubernetes cluster integration"
-msgstr "Choisissez comment configurer l’intégration de cluster Kubernetes"
+msgstr "Choisissez comment configurer l’intégration de la grappe de serveurs Kubernetes"
msgid "ClusterIntegration|Choose which of your project's environments will use this Kubernetes cluster."
-msgstr "Choisissez les environnements de votre projet qui utiliseront ce cluster Kubernetes."
+msgstr "Choisissez les environnements de votre projet qui utiliseront cette grappe de serveurs Kubernetes."
msgid "ClusterIntegration|Control how your Kubernetes cluster integrates with GitLab"
-msgstr "Contrôlez l’intégration de votre cluster Kubernetes avec GitLab"
+msgstr "Contrôlez l’intégration de votre grappe de serveurs Kubernetes avec GitLab"
msgid "ClusterIntegration|Copy API URL"
msgstr "Copier l’URL de l’API"
msgid "ClusterIntegration|Copy CA Certificate"
-msgstr "Copier le certificat CA"
+msgstr "Copier le certificat de l’AC"
msgid "ClusterIntegration|Copy Ingress IP Address to clipboard"
-msgstr "Copier l’adresse IP entrante dans le presse-papiers"
+msgstr "Copier l’adresse IP Ingress dans le presseâ€papiers"
+
+msgid "ClusterIntegration|Copy Jupyter Hostname to clipboard"
+msgstr "Copier le nom d’hôte Jupyter dans le presseâ€papiers"
msgid "ClusterIntegration|Copy Kubernetes cluster name"
-msgstr "Copier le nom du cluster Kubernetes"
+msgstr "Copier le nom de la grappe de serveurs Kubernetes"
msgid "ClusterIntegration|Copy Token"
msgstr "Copier le jeton"
msgid "ClusterIntegration|Create Kubernetes cluster"
-msgstr "Créer un cluster Kubernetes"
+msgstr "Créer une grappe de serveurs Kubernetes"
msgid "ClusterIntegration|Create Kubernetes cluster on Google Kubernetes Engine"
msgstr "Créer un cluster Kubernetes sur Google Kubernetes Engine"
msgid "ClusterIntegration|Create a new Kubernetes cluster on Google Kubernetes Engine right from GitLab"
-msgstr "Créer un nouveau cluster Kubernetes sur Google Kubernetes Engine directement depuis GitLab"
+msgstr "Créer une nouvelle grappe de serveurs Kubernetes sur Google Kubernetes Engine directement depuis GitLab"
-msgid "ClusterIntegration|Create on GKE"
-msgstr "Créer sur GKE"
+msgid "ClusterIntegration|Create on Google Kubernetes Engine"
+msgstr "Créer sur Google Kubernetes Engine"
msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
-msgstr "Entrer les détails pour le cluster Kubernetes existant"
+msgstr "Entrer les détails pour la grappe de serveurs Kubernetes existante"
msgid "ClusterIntegration|Enter the details for your Kubernetes cluster"
-msgstr "Entrez les détails de votre cluster Kubernetes"
+msgstr "Entrez les détails de votre grappe de serveurs Kubernetes"
msgid "ClusterIntegration|Environment scope"
msgstr "Portée de l’environnement"
+msgid "ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab's Google Kubernetes Engine Integration."
+msgstr "Chaque nouveau compte Google Cloud Platform (GCP) reçoit un crédit de 300 US$ sur %{sign_up_link}. En partenariat avec Google, GitLab est en mesure de vous offrir 200 US$ supplémentaires, à la fois pour les nouveaux et les anciens comptes GCP, afin de vous permettre de commencer l’intégration de Google Kubernetes Engine sur GitLab."
+
+msgid "ClusterIntegration|Fetching machine types"
+msgstr "Récupération des types de machines"
+
+msgid "ClusterIntegration|Fetching projects"
+msgstr "Récupération des projets"
+
+msgid "ClusterIntegration|Fetching zones"
+msgstr "Récupération des zones"
+
msgid "ClusterIntegration|GitLab Integration"
msgstr "Intégration GitLab"
msgid "ClusterIntegration|GitLab Runner"
-msgstr "Éxécuteur GitLab"
+msgstr "Exécuteur GitLab"
-msgid "ClusterIntegration|Google Cloud Platform project ID"
-msgstr "ID de projet Google Cloud Platform"
+msgid "ClusterIntegration|Google Cloud Platform project"
+msgstr "Projet Google Cloud Platform"
msgid "ClusterIntegration|Google Kubernetes Engine"
msgstr "Google Kubernetes Engine"
@@ -1012,21 +1185,15 @@ msgstr "Projet Google Kubernetes Engine"
msgid "ClusterIntegration|Helm Tiller"
msgstr "Helm Tiller"
-msgid "ClusterIntegration|In order to show the health of the cluster, we'll need to provision your cluster with Prometheus to collect the required data."
-msgstr "Afin d’afficher l’état du cluster, nous devons mettre votre cluster à disposition de Prometheus pour récupérer les données nécessaires."
-
msgid "ClusterIntegration|Ingress"
msgstr "Ingress"
msgid "ClusterIntegration|Ingress IP Address"
-msgstr "Adresse IP entrante"
+msgstr "Adresse IP Ingress"
msgid "ClusterIntegration|Install"
msgstr "Installer"
-msgid "ClusterIntegration|Install Prometheus"
-msgstr "Installer Prometheus"
-
msgid "ClusterIntegration|Installed"
msgstr "Installé"
@@ -1034,49 +1201,58 @@ msgid "ClusterIntegration|Installing"
msgstr "En cours d’installation"
msgid "ClusterIntegration|Integrate Kubernetes cluster automation"
-msgstr "Intégrez l’automatisation du cluster Kubernetes"
+msgstr "Intégrez l’automatisation de la grappe de serveurs Kubernetes"
msgid "ClusterIntegration|Integration status"
-msgstr "Statut d’intégration"
+msgstr "Statut de l’intégration"
+
+msgid "ClusterIntegration|Jupyter Hostname"
+msgstr "Nom de l’hôte Jupyter"
+
+msgid "ClusterIntegration|JupyterHub"
+msgstr "JupyterHub"
msgid "ClusterIntegration|Kubernetes cluster"
-msgstr "Cluster Kubernetes"
+msgstr "Grappe de serveurs Kubernetes"
msgid "ClusterIntegration|Kubernetes cluster details"
-msgstr "Détails du cluster Kubernetes"
-
-msgid "ClusterIntegration|Kubernetes cluster health"
-msgstr "État du cluster Kubernetes"
+msgstr "Détails de la grappe de serveurs Kubernetes"
msgid "ClusterIntegration|Kubernetes cluster integration"
-msgstr "Intégration d’un cluster Kubernetes"
+msgstr "Intégration d’une grappe de serveurs Kubernetes"
msgid "ClusterIntegration|Kubernetes cluster integration is disabled for this project."
-msgstr "L’intégration de cluster Kubernetes est désactivée pour ce projet."
+msgstr "L’intégration de la grappe de serveurs Kubernetes est désactivée pour ce projet."
msgid "ClusterIntegration|Kubernetes cluster integration is enabled for this project."
-msgstr "L’intégration de cluster Kubernetes est activée pour ce projet."
+msgstr "L’intégration de la grappe de serveurs Kubernetes est activée pour ce projet."
msgid "ClusterIntegration|Kubernetes cluster integration is enabled for this project. Disabling this integration will not affect your Kubernetes cluster, it will only temporarily turn off GitLab's connection to it."
-msgstr "L’intégration de cluster Kubernetes est activée pour ce projet. La désactivation de cette intégration n’affectera pas votre cluster Kubernetes, elle ne désactivera que temporairement la connexion de GitLab."
+msgstr "L’intégration d’une grappe de serveurs Kubernetes est activée pour ce projet. La désactivation de cette intégration n’affectera pas votre grappe de serveurs Kubernetes, elle ne désactivera que temporairement sa connexion à GitLab."
msgid "ClusterIntegration|Kubernetes cluster is being created on Google Kubernetes Engine..."
-msgstr "Le cluster Kubernetes est en cours de création sur Google Kubernetes Engine…"
+msgstr "La grappe de serveurs Kubernetes est en cours de création sur Google Kubernetes Engine…"
msgid "ClusterIntegration|Kubernetes cluster name"
-msgstr "Nom du cluster Kubernetes"
+msgstr "Nom de la grappe de serveurs Kubernetes"
msgid "ClusterIntegration|Kubernetes cluster was successfully created on Google Kubernetes Engine. Refresh the page to see Kubernetes cluster's details"
-msgstr "Le cluster Kubernetes a été créé avec succès sur Google Kubernetes Engine. Actualisez la page pour voir les détails du cluster Kubernetes"
+msgstr "La grappe de serveurs Kubernetes a été créée avec succès sur Google Kubernetes Engine. Actualisez la page pour voir les détails de la grappe de serveurs Kubernetes"
msgid "ClusterIntegration|Kubernetes clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way. %{link_to_help_page}"
-msgstr "Les clusters Kubernetes vous permettent d’utiliser des applications de révision, de déployer vos applications, d’exécuter vos pipelines et bien plus encore. %{link_to_help_page}"
+msgstr "Les grappes de serveurs Kubernetes vous permettent d’utiliser des applications de révision, de déployer vos applications, d’exécuter vos pipelines et bien plus encore. %{link_to_help_page}"
msgid "ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project"
-msgstr "Les clusters Kubernetes peuvent être utilisés pour déployer des applications et fournir des applications de révision pour ce projet"
+msgstr "Les grappes de serveurs Kubernetes peuvent être utilisées pour déployer des applications et fournir des applications de revue Review Apps pour ce projet"
+
+msgid "ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}."
+msgstr "En savoir plus sur les %{help_link_start_machine_type}types de machines%{help_link_end} et la %{help_link_start_pricing}tarification%{help_link_end}."
-msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
-msgstr "En savoir plus sur %{link_to_documentation}"
+msgid "ClusterIntegration|Learn more about %{help_link_start}Kubernetes%{help_link_end}."
+msgstr "En savoir plus sur %{help_link_start}Kubernetes%{help_link_end}."
+
+msgid "ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}."
+msgstr "En savoir plus sur %{help_link_start}les zones%{help_link_end}."
msgid "ClusterIntegration|Learn more about environments"
msgstr "En savoir plus sur les environnements"
@@ -1088,34 +1264,40 @@ msgid "ClusterIntegration|Machine type"
msgstr "Type de machine"
msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create Kubernetes clusters"
-msgstr "Assurez-vous que votre compte %{link_to_requirements} pour créer des clusters Kubernetes"
+msgstr "Assurezâ€vous que votre compte %{link_to_requirements} pour créer des grappes de serveurs Kubernetes"
msgid "ClusterIntegration|Manage"
msgstr "Gérer"
msgid "ClusterIntegration|Manage your Kubernetes cluster by visiting %{link_gke}"
-msgstr "Gérez votre cluster Kubernetes en visitant %{link_gke}"
+msgstr "Gérez votre grappe de serveurs Kubernetes en visitant %{link_gke}"
msgid "ClusterIntegration|More information"
msgstr "Plus d’informations"
-msgid "ClusterIntegration|Multiple Kubernetes clusters are available in GitLab Enterprise Edition Premium and Ultimate"
-msgstr "Plusieurs clusters Kubernetes sont disponibles dans GitLab Enterprise Edition Premium et Ultimate"
+msgid "ClusterIntegration|No machine types matched your search"
+msgstr "Aucun type de machine ne correspond à votre recherche"
+
+msgid "ClusterIntegration|No projects found"
+msgstr "Aucun projet trouvé"
+
+msgid "ClusterIntegration|No projects matched your search"
+msgstr "Aucun projet ne correspond à votre recherche"
+
+msgid "ClusterIntegration|No zones matched your search"
+msgstr "Aucune zone ne correspond à votre recherche"
msgid "ClusterIntegration|Note:"
-msgstr "Remarque :"
+msgstr "Remarque :"
msgid "ClusterIntegration|Number of nodes"
msgstr "Nombre de nœuds"
msgid "ClusterIntegration|Please enter access information for your Kubernetes cluster. If you need help, you can read our %{link_to_help_page} on Kubernetes"
-msgstr "Veuillez entrer les informations d’accès de votre cluster Kubernetes. Si vous avez besoin d'aide, vous pouvez lire notre %{link_to_help_page} sur Kubernetes"
+msgstr "Veuillez entrer les informations d’accès de votre grappe de serveurs Kubernetes. Si vous avez besoin d’aide, vous pouvez lire notre %{link_to_help_page} sur Kubernetes"
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 ID"
-msgstr "ID du projet"
+msgstr "Veuillez vous assurer que votre compte Google répond aux exigences suivantes :"
msgid "ClusterIntegration|Project namespace"
msgstr "Espace de noms du projet"
@@ -1127,37 +1309,58 @@ msgid "ClusterIntegration|Prometheus"
msgstr "Prometheus"
msgid "ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration."
-msgstr "Lisez notre %{link_to_help_page} sur l’intégration d’un cluster Kubernetes."
+msgstr "Lisez notre %{link_to_help_page} sur l’intégration d’une grappe de serveurs Kubernetes."
+
+msgid "ClusterIntegration|Redeem up to $500 in free credit for Google Cloud Platform"
+msgstr "Recevez jusqu’à 500 US$ de crédit gratuit pour Google Cloud Platform"
msgid "ClusterIntegration|Remove Kubernetes cluster integration"
-msgstr "Supprimer l’intégration du cluster Kubernetes"
+msgstr "Supprimer l’intégration de la grappe de serveurs Kubernetes"
msgid "ClusterIntegration|Remove integration"
msgstr "Retirer l’intégration"
msgid "ClusterIntegration|Remove this Kubernetes cluster's configuration from this project. This will not delete your actual Kubernetes cluster."
-msgstr "Supprimer la configuration de ce cluster Kubernetes de ce projet. Cela ne supprimera pas votre cluster Kubernetes actuel."
+msgstr "Supprimer la configuration de cette grappe de serveurs Kubernetes de ce projet. Cela ne supprimera pas votre grappe de serveurs Kubernetes actuelle."
msgid "ClusterIntegration|Request to begin installing failed"
-msgstr "La demande de lancement d'installation a échoué"
+msgstr "La demande de lancement de l’installation a échoué"
msgid "ClusterIntegration|Save changes"
msgstr "Enregistrer les modifications"
+msgid "ClusterIntegration|Search machine types"
+msgstr "Rechercher les types de machines"
+
+msgid "ClusterIntegration|Search projects"
+msgstr "Rechercher des projets"
+
+msgid "ClusterIntegration|Search zones"
+msgstr "Rechercher les zones"
+
msgid "ClusterIntegration|Security"
msgstr "Sécurité"
msgid "ClusterIntegration|See and edit the details for your Kubernetes cluster"
-msgstr "Voir et modifier les détails de votre cluster Kubernetes"
+msgstr "Voir et modifier les détails de votre grappe de serveurs Kubernetes"
-msgid "ClusterIntegration|See machine types"
-msgstr "Voir les types de machine"
+msgid "ClusterIntegration|Select machine type"
+msgstr "Sélectionnez le type de machine"
-msgid "ClusterIntegration|See your projects"
-msgstr "Voir vos projets"
+msgid "ClusterIntegration|Select project"
+msgstr "Sélectionnez un projet"
-msgid "ClusterIntegration|See zones"
-msgstr "Voir les zones"
+msgid "ClusterIntegration|Select project and zone to choose machine type"
+msgstr "Sélectionnez le projet et la zone afin de choisir le type de machine"
+
+msgid "ClusterIntegration|Select project to choose zone"
+msgstr "Sélectionnez le projet afin de choisir la zone"
+
+msgid "ClusterIntegration|Select zone"
+msgstr "Sélectionnez la zone"
+
+msgid "ClusterIntegration|Select zone to choose machine type"
+msgstr "Sélectionnez la zone afin de choisir le type de machine"
msgid "ClusterIntegration|Service token"
msgstr "Jeton de service"
@@ -1169,28 +1372,31 @@ 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 Kubernetes cluster on Google Kubernetes Engine"
-msgstr "Une erreur s’est produite lors de la création de votre cluster Kubernetes sur Google Kubernetes Engine"
+msgstr "Une erreur s’est produite lors de la création de votre grappe de serveurs Kubernetes sur Google Kubernetes Engine"
msgid "ClusterIntegration|Something went wrong while installing %{title}"
-msgstr "Une erreur s’est produite lors de l'installation de %{title}"
+msgstr "Une erreur s’est produite lors de l’installation de %{title}"
msgid "ClusterIntegration|The default cluster configuration grants access to a wide set of functionalities needed to successfully build and deploy a containerised application."
-msgstr "La configuration par défaut du cluster permet d’accéder à un large éventail de fonctionnalités nécessaires pour construire et déployer, avec succès, une application conteneurisée."
+msgstr "La configuration par défaut de la grappe de serveurs permet d’accéder à un large éventail de fonctionnalités nécessaires pour construire et déployer, avec succès, une application conteneurisée."
msgid "ClusterIntegration|This account must have permissions to create a Kubernetes cluster in the %{link_to_container_project} specified below"
-msgstr "Ce compte doit disposer des autorisations pour créer un cluster Kubernetes dans le %{link_to_container_project} spécifié ci-dessous"
+msgstr "Ce compte doit disposer des autorisations pour créer une grappe de serveurs Kubernetes dans le %{link_to_container_project} spécifié ciâ€dessous"
msgid "ClusterIntegration|Toggle Kubernetes Cluster"
-msgstr "Activer le cluster Kubernetes"
+msgstr "Activer la grappe de serveurs Kubernetes"
msgid "ClusterIntegration|Toggle Kubernetes cluster"
-msgstr "Activer/désactiver le cluster Kubernetes"
+msgstr "Activer/désactiver la grappe de serveurs Kubernetes"
msgid "ClusterIntegration|Token"
msgstr "Jeton"
+msgid "ClusterIntegration|Validating project billing status"
+msgstr "Validation de l’état de la facturation du projet"
+
msgid "ClusterIntegration|With a Kubernetes 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 Kubernetes associé à ce projet, vous pouvez utiliser des applications de révision, déployer vos applications, exécuter vos pipelines, et bien plus encore."
+msgstr "Avec une grappe de serveurs Kubernetes associée à ce projet, vous pouvez utiliser des applications de revue, déployer vos applications, exécuter vos pipelines, et bien plus encore."
msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}"
msgstr "Votre compte doit disposer de %{link_to_kubernetes_engine}"
@@ -1211,7 +1417,7 @@ msgid "ClusterIntegration|help page"
msgstr "page d’aide"
msgid "ClusterIntegration|installing applications"
-msgstr "Installation des applications"
+msgstr "installation des applications"
msgid "ClusterIntegration|meets the requirements"
msgstr "répond aux exigences"
@@ -1219,14 +1425,20 @@ msgstr "répond aux exigences"
msgid "ClusterIntegration|properly configured"
msgstr "correctement configuré"
+msgid "ClusterIntegration|sign up"
+msgstr "s’inscrire"
+
msgid "Collapse"
msgstr "Réduire"
-msgid "Comment and resolve discussion"
-msgstr "Commenter et résoudre la discussion"
+msgid "Collapse sidebar"
+msgstr "Réduire la barre latérale"
+
+msgid "Comment & resolve discussion"
+msgstr "Commenter et marquer la discussion comme résolue"
-msgid "Comment and unresolve discussion"
-msgstr "Commenter et marquer la discussion comme non résolue"
+msgid "Comment & unresolve discussion"
+msgstr "Commenter et marquer la discussion comme non résolue"
msgid "Comments"
msgstr "Commentaires"
@@ -1245,13 +1457,13 @@ msgid "Commit Message"
msgstr "Message du commit"
msgid "Commit duration in minutes for last 30 commits"
-msgstr "Durée des 30 derniers pipelines en minutes"
+msgstr "Durée des 30 derniers commits en minutes"
msgid "Commit message"
msgstr "Message de commit"
msgid "Commit statistics for %{ref} %{start_time} - %{end_time}"
-msgstr "Statistiques des commits pour %{ref} %{start_time} - %{end_time}"
+msgstr "Statistiques des commits pour %{ref} de %{start_time} à %{end_time}"
msgid "Commit to %{branchName} branch"
msgstr "Valider dans la branche %{branchName}"
@@ -1269,7 +1481,7 @@ msgid "Commits feed"
msgstr "Flux des commits"
msgid "Commits per day hour (UTC)"
-msgstr "Commits par heure du jour (UTC)"
+msgstr "Commits pour chaque heure de la journée (UTC)"
msgid "Commits per day of month"
msgstr "Commits par jour du mois"
@@ -1278,10 +1490,10 @@ msgid "Commits per weekday"
msgstr "Commits par jour de la semaine"
msgid "Commits|An error occurred while fetching merge requests data."
-msgstr "Une erreur s'est produite lors de la récupération des données de demandes de fusion."
+msgstr "Une erreur s’est produite lors de la récupération des données de demandes de fusion."
msgid "Commits|Commit: %{commitText}"
-msgstr "Commit : %{commitText}"
+msgstr "Commit : %{commitText}"
msgid "Commits|History"
msgstr "Historique"
@@ -1290,7 +1502,10 @@ msgid "Commits|No related merge requests found"
msgstr "Aucune demande de fusion associée trouvée"
msgid "Committed by"
-msgstr "Validé par"
+msgstr "Commit de"
+
+msgid "Commit…"
+msgstr ""
msgid "Compare"
msgstr "Comparer"
@@ -1305,7 +1520,7 @@ msgid "Compare changes with the last commit"
msgstr "Comparer les changements avec le dernier commit"
msgid "Compare changes with the merge request target branch"
-msgstr ""
+msgstr "Comparer les modifications avec la branche cible de la demande de fusion"
msgid "CompareBranches|%{source_branch} and %{target_branch} are the same."
msgstr "%{source_branch} et %{target_branch} sont identiques."
@@ -1329,38 +1544,32 @@ msgid "Confidentiality"
msgstr "Confidentialité"
msgid "Configure Gitaly timeouts."
-msgstr ""
+msgstr "Configurer les délais d’expiration de Gitaly."
msgid "Configure Sidekiq job throttling."
-msgstr ""
+msgstr "Configurer la limitation des tâches Sidekiq."
msgid "Configure automatic git checks and housekeeping on repositories."
-msgstr ""
+msgstr "Configurer les vérifications Git automatiques et la maintenance des dépôts."
msgid "Configure limits for web and API requests."
-msgstr ""
+msgstr "Configurer les limites pour les requêtes Web et d’API."
+
+msgid "Configure push mirrors."
+msgstr "Configurez les miroirs où pousser le code."
msgid "Configure storage path and circuit breaker settings."
-msgstr ""
+msgstr "Configurez les paramètres du chemin de stockage et du disjoncteur."
msgid "Configure the way a user creates a new account."
-msgstr "Configurez la façon dont un•e utilisateur•rice crée un nouveau compte."
+msgstr "Configurez la façon dont un·e utilisateur·rice crée un nouveau compte."
msgid "Connect"
msgstr "Connecter"
-msgid "Connect all repositories"
-msgstr "Connecter tous les dépôts"
-
msgid "Connect repositories from GitHub"
msgstr "Se connecter à des dépôts à partir de GitHub"
-msgid "Connect your external repositories, and CI/CD pipelines will run for new commits. A GitLab project will be created with only CI/CD features enabled."
-msgstr "Connectez vos dépôts externes et les pipelines CI / CD s’exécuteront pour les nouveaux commits. Un projet GitLab sera créé avec uniquement les fonctionnalités CI / CD activées."
-
-msgid "Connecting..."
-msgstr "Connexion en cours…"
-
msgid "Container Registry"
msgstr "Registre de conteneur"
@@ -1368,10 +1577,10 @@ 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} :"
+msgstr "Inscrivezâ€vous d’abord au registre de conteneur GitLab à l’aide de votre nom d’utilisa·teur·trice 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 :"
+msgstr "GitLab prend en charge jusqu’à trois 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"
@@ -1380,7 +1589,7 @@ 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."
+msgstr "Aucune étiquette 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}"
@@ -1395,20 +1604,29 @@ msgid "ContainerRegistry|Size"
msgstr "Taille"
msgid "ContainerRegistry|Tag"
-msgstr "Tag"
+msgstr "Étiquette"
msgid "ContainerRegistry|Tag ID"
-msgstr "ID du tag"
+msgstr "Identifiant de l’étiquette"
msgid "ContainerRegistry|Use different image names"
-msgstr "Utilisez des noms d’images différents"
+msgstr "Utilisez des noms d’image 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 "ContainerRegistry|You can also use a %{deploy_token} for read-only access to the registry images."
+msgstr "Vous pouvez également utiliser un %{deploy_token} pour un accès en lecture seule aux images du registre."
+
+msgid "Continue"
+msgstr "Continuer"
+
msgid "Continuous Integration and Deployment"
msgstr "Intégration et déploiement continus"
+msgid "Contribute to GitLab"
+msgstr "Contribuer à GitLab"
+
msgid "Contribution"
msgstr "Contribution"
@@ -1419,40 +1637,40 @@ msgid "Contributors"
msgstr "Contributeurs"
msgid "ContributorsPage|%{startDate} – %{endDate}"
-msgstr "%{startDate} - %{endDate}"
+msgstr "Du %{startDate} au %{endDate}"
msgid "ContributorsPage|Building repository graph."
msgstr "Construction du graphique du dépôt."
msgid "ContributorsPage|Commits to %{branch_name}, excluding merge commits. Limited to 6,000 commits."
-msgstr "Commit sur %{branch_name}, à l'exclusion des commits de fusion. Limité à 6 000 commits."
+msgstr "Commits sur %{branch_name}, à l’exclusion des commits de fusion (limité à 6 000 commits)."
msgid "ContributorsPage|Please wait a moment, this page will automatically refresh when ready."
msgstr "Veuillez patienter, cette page va être automatiquement actualisée."
-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 "Copier la clé publique SSH dans le presse-papier"
-
msgid "Copy URL to clipboard"
-msgstr "Copier l'URL dans le presse-papier"
+msgstr "Copier l’URL dans le presseâ€papiers"
msgid "Copy branch name to clipboard"
-msgstr "Copier le nom de la branche dans le presse-papiers"
+msgstr "Copier le nom de la branche dans le presseâ€papiers"
msgid "Copy command to clipboard"
-msgstr "Copier la commande dans le presse-papiers"
+msgstr "Copier la commande dans le presseâ€papiers"
msgid "Copy commit SHA to clipboard"
-msgstr "Copier le SHA du commit"
+msgstr "Copier le condensat SHA du commit"
+
+msgid "Copy file name to clipboard"
+msgstr "Copier le nom du fichier dans le presseâ€papiers"
+
+msgid "Copy file path to clipboard"
+msgstr "Copier le chemin d’accès du fichier dans le presseâ€papiers"
msgid "Copy reference to clipboard"
-msgstr "Copier la référence dans le presse-papiers"
+msgstr "Copier la référence dans le presseâ€papiers"
+
+msgid "Copy to clipboard"
+msgstr "Copier dans le presseâ€papiers"
msgid "Create"
msgstr "Créer"
@@ -1464,7 +1682,7 @@ msgid "Create a new branch"
msgstr "Créer une nouvelle branche"
msgid "Create a new branch and merge request"
-msgstr "Créer une nouvelle branche et demande de fusion"
+msgstr "Créer une nouvelle branche et une nouvelle demande de fusion"
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}."
@@ -1472,23 +1690,23 @@ msgstr "Créer un jeton d’accès personnel pour votre compte afin de récupér
msgid "Create branch"
msgstr "Créer une branche"
+msgid "Create commit"
+msgstr "Créer un commit"
+
msgid "Create directory"
msgstr "Créer un dossier"
msgid "Create empty repository"
msgstr "Créer un dépôt vide"
-msgid "Create epic"
-msgstr "Créer l'épopée"
-
msgid "Create file"
msgstr "Créer un fichier"
msgid "Create group label"
-msgstr "Créer un label de groupe"
+msgstr "Créer une étiquette de groupe"
msgid "Create lists from labels. Issues with that label appear in that list."
-msgstr "Créer des listes à partir de labels. Les tickets avec ce label apparaissent dans cette liste."
+msgstr "Créer des listes à partir d’étiquettes. Les tickets avec l’étiquette sélectionnée apparaissent dans cette liste."
msgid "Create merge request"
msgstr "Créer une demande de fusion"
@@ -1506,52 +1724,52 @@ msgid "Create new file"
msgstr "Créer un nouveau fichier"
msgid "Create new label"
-msgstr "Créer un nouveau label"
+msgstr "Créer une nouvelle étiquette"
msgid "Create new..."
-msgstr "Créer nouveau..."
+msgstr "Créer un nouveau…"
msgid "Create project label"
-msgstr "Créer un label de projet"
+msgstr "Créer une étiquette de projet"
msgid "CreateNewFork|Fork"
-msgstr "Fourcher"
+msgstr "Créer une divergence"
msgid "CreateTag|Tag"
-msgstr "Tag"
+msgstr "Étiquette"
msgid "CreateTokenToCloneLink|create a personal access token"
-msgstr "Créer un jeton d'accès personnel"
-
-msgid "Creates a new branch from %{branchName}"
-msgstr "Crée une nouvelle branche à partir de %{branchName}"
+msgstr "Créer un jeton d’accès personnel"
-msgid "Creates a new branch from %{branchName} and re-directs to create a new merge request"
-msgstr "Crée une nouvelle branche à partir de %{branchName} et redirige pour créer une nouvelle demande de fusion"
+msgid "Created"
+msgstr "Créé"
-msgid "Creating epic"
-msgstr "Création de l'épopée en cours"
+msgid "Created by me"
+msgstr "Créé par moi"
msgid "Cron Timezone"
-msgstr "Fuseau horaire de Cron"
+msgstr "Fuseau horaire des tâches planifiées cron"
msgid "Cron syntax"
-msgstr "Syntaxe Cron"
+msgstr "Syntaxe de la planification cron"
+
+msgid "CurrentUser|Profile"
+msgstr "Profil"
-msgid "Current node"
-msgstr "Nœud actuel"
+msgid "CurrentUser|Settings"
+msgstr "Paramètres"
+
+msgid "Custom CI config path"
+msgstr "Chemin d’accès de la config d’intégration continue personnalisée"
msgid "Custom notification events"
msgstr "Événements de notification personnalisés"
msgid "Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notification_link}."
-msgstr "Le niveau de notification Personnalisé est similaire au niveau Participation. Cependant, il permet également de recevoir des notifications pour des événements sélectionnés. Pour plus d’information, vous pouvez consulter %{notification_link}."
-
-msgid "Customize colors"
-msgstr ""
+msgstr "Les niveaux de notification personnalisés sont similaires aux niveaux de participation. Cependant, ils permettent de recevoir également des notifications pour une sélection d’événements. Pour plus d’information, vous pouvez consulter %{notification_link}."
msgid "Cycle Analytics"
-msgstr "Analyseur de cycle"
+msgstr "Analyse de cycle"
msgid "CycleAnalyticsStage|Code"
msgstr "Code"
@@ -1569,7 +1787,7 @@ msgid "CycleAnalyticsStage|Review"
msgstr "Examen"
msgid "CycleAnalyticsStage|Staging"
-msgstr "Pré-production"
+msgstr "Préproduction"
msgid "CycleAnalyticsStage|Test"
msgstr "Test"
@@ -1581,13 +1799,13 @@ msgid "DashboardProjects|Personal"
msgstr "Personnels"
msgid "Dec"
-msgstr "Déc."
+msgstr "déc."
msgid "December"
-msgstr "Décembre"
+msgstr "décembre"
-msgid "Default classification label"
-msgstr "Label de classement par défaut"
+msgid "Decline and sign out"
+msgstr "Refuser et se déconnecter"
msgid "Define a custom pattern with cron syntax"
msgstr "Définir un schéma personnalisé avec une syntaxe Cron"
@@ -1595,76 +1813,205 @@ msgstr "Définir un schéma personnalisé avec une syntaxe Cron"
msgid "Delete"
msgstr "Supprimer"
+msgid "Delete list"
+msgstr "Supprimer la liste"
+
msgid "Deploy"
msgid_plural "Deploys"
msgstr[0] "Déploiement"
msgstr[1] "Déploiements"
msgid "Deploy Keys"
-msgstr "Clés de déploiement"
+msgstr "Clefs de déploiement"
+
+msgid "DeployKeys|+%{count} others"
+msgstr "+%{count} autres"
+
+msgid "DeployKeys|Current project"
+msgstr "Projet actuel"
+
+msgid "DeployKeys|Deploy key"
+msgstr "Clef de déploiement"
+
+msgid "DeployKeys|Enabled deploy keys"
+msgstr "Clefs de déploiement activées"
+
+msgid "DeployKeys|Error enabling deploy key"
+msgstr "Erreur lors de l’activation de la clef de déploiement"
+
+msgid "DeployKeys|Error getting deploy keys"
+msgstr "Erreur lors de l’obtention des clefs de déploiement"
+
+msgid "DeployKeys|Error removing deploy key"
+msgstr "Erreur lors de la suppression de la clef de déploiement"
+
+msgid "DeployKeys|Expand %{count} other projects"
+msgstr "Afficher %{count} autres projets"
+
+msgid "DeployKeys|Loading deploy keys"
+msgstr "Chargement des clefs de déploiement"
+
+msgid "DeployKeys|No deploy keys found. Create one with the form above."
+msgstr "Aucune clef de déploiement trouvée. Créezâ€en une avec le formulaire ciâ€dessous."
+
+msgid "DeployKeys|Privately accessible deploy keys"
+msgstr "Clefs de déploiement à accès privé"
+
+msgid "DeployKeys|Project usage"
+msgstr "À l’usage du projet"
+
+msgid "DeployKeys|Publicly accessible deploy keys"
+msgstr "Clefs de déploiement à accès publique"
+
+msgid "DeployKeys|Read access only"
+msgstr "Accès en lecture seule"
+
+msgid "DeployKeys|Write access allowed"
+msgstr "Accès en écriture autorisé"
+
+msgid "DeployKeys|You are going to remove this deploy key. Are you sure?"
+msgstr "Vous êtes sur le point de supprimer cette clef de déploiement. Êtesâ€vous sûr·e ?"
+
+msgid "DeployTokens|Active Deploy Tokens (%{active_tokens})"
+msgstr "Jetons de déploiement actifs : (%{active_tokens})"
+
+msgid "DeployTokens|Add a deploy token"
+msgstr "Ajouter un jeton de déploiement"
+
+msgid "DeployTokens|Allows read-only access to the registry images"
+msgstr "Autorise l’accès en lecture seule aux images du registre"
+
+msgid "DeployTokens|Allows read-only access to the repository"
+msgstr "Autorise l’accès en lecture seule au dépôt"
+
+msgid "DeployTokens|Copy deploy token to clipboard"
+msgstr "Copier le jeton de déploiement dans le presseâ€papiers"
+
+msgid "DeployTokens|Copy username to clipboard"
+msgstr "Copier le nom d’utilisateur·rice dans le presseâ€papiers"
+
+msgid "DeployTokens|Create deploy token"
+msgstr "Créer un jeton de déploiement"
+
+msgid "DeployTokens|Created"
+msgstr "Créé"
+
+msgid "DeployTokens|Deploy Tokens"
+msgstr "Jetons de déploiement"
+
+msgid "DeployTokens|Deploy tokens allow read-only access to your repository and registry images."
+msgstr "Les jetons de déploiement autorisent l’accès en lecture seule à votre dépôt et vos images de registre."
+
+msgid "DeployTokens|Expires"
+msgstr "Expire"
+
+msgid "DeployTokens|Name"
+msgstr "Nom"
+
+msgid "DeployTokens|Pick a name for the application, and we'll give you a unique deploy token."
+msgstr "Choisissez un nom pour l’application et nous vous donnerons un jeton de déploiement unique."
+
+msgid "DeployTokens|Revoke"
+msgstr "Révoquer"
+
+msgid "DeployTokens|Revoke %{name}"
+msgstr "Révoquer %{name}"
+
+msgid "DeployTokens|Scopes"
+msgstr "Champs d’application"
+
+msgid "DeployTokens|This action cannot be undone."
+msgstr "Cette action ne peut pas être annulée."
+
+msgid "DeployTokens|This project has no active Deploy Tokens."
+msgstr "Ce projet n’a pas de jetons de déploiement actif."
+
+msgid "DeployTokens|Use this token as a password. Make sure you save it - you won't be able to access it again."
+msgstr "Utilisez ce jeton en tant que mot de passe. Assurezâ€vous de le sauvegarder, vous n’aurez pas la possibilité d’y accéder à nouveau."
+
+msgid "DeployTokens|Use this username as a login."
+msgstr "Utiliser ce nom d’utilisateur·rice comme identifiant."
+
+msgid "DeployTokens|Username"
+msgstr "Nom d’utilisateur·rice"
+
+msgid "DeployTokens|You are about to revoke"
+msgstr "Vous êtes sur le point de révoquer"
+
+msgid "DeployTokens|Your New Deploy Token"
+msgstr "Votre nouveau jeton de déploiement"
+
+msgid "DeployTokens|Your new project deploy token has been created."
+msgstr "Votre nouveau jeton de déploiement pour votre projet a été créé."
+
+msgid "Deprioritize label"
+msgstr "Déprioriser l’étiquette"
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"
msgid "Diffs|No file name available"
msgstr "Aucun nom de fichier disponible"
+msgid "Diffs|Something went wrong while fetching diff lines."
+msgstr "Quelque chose s’est mal passé lors de la rapatriement des lignes du diff."
+
msgid "Directory name"
msgstr "Nom du dossier"
msgid "Disable"
msgstr "Désactiver"
-msgid "Discard draft"
-msgstr "Supprimer le brouillon"
+msgid "Disable for this project"
+msgstr "Désactiver pour ce projet"
-msgid "Discover GitLab Geo."
-msgstr "Découvrez GitLab Geo."
+msgid "Disable group Runners"
+msgstr "Désactiver les exécuteurs de groupe"
-msgid "Dismiss Cycle Analytics introduction box"
-msgstr "Passer l’introduction Cycle Analytics"
+msgid "Discard changes"
+msgstr "Abandonner les modifications"
+
+msgid "Discard draft"
+msgstr "Abandonner le brouillon"
-msgid "Dismiss Merge Request promotion"
-msgstr "Rejeter la promotion de la demande de fusion"
+msgid "Dismiss Cycle Analytics introduction box"
+msgstr "Passer l’introduction à Cycle Analytics"
-msgid "Documentation for popular identity providers"
-msgstr ""
+msgid "Domain"
+msgstr "Domaine"
msgid "Don't show again"
-msgstr "Ne plus montrer"
+msgstr "Ne plus afficher"
msgid "Done"
-msgstr "Fait"
+msgstr "Effectué"
msgid "Download"
msgstr "Télécharger"
msgid "Download tar"
-msgstr "Télécharger tar"
+msgstr "Télécharger une archive TAR"
msgid "Download tar.bz2"
-msgstr "Télécharger tar.bz2"
+msgstr "Télécharger une archive tar.bz2"
msgid "Download tar.gz"
-msgstr "Télécharger tar.gz"
+msgstr "Télécharger une archive tar.gz"
msgid "Download zip"
-msgstr "Télécharger zip"
+msgstr "Télécharger une archive ZIP"
msgid "DownloadArtifacts|Download"
msgstr "Télécharger"
msgid "DownloadCommit|Email Patches"
-msgstr "Patch email"
+msgstr "Envoyer les correctifs par courriel"
msgid "DownloadCommit|Plain Diff"
-msgstr "Diff simple"
+msgstr "Pur diff"
msgid "DownloadSource|Download"
msgstr "Télécharger"
@@ -1675,44 +2022,44 @@ msgstr "Votes négatifs"
msgid "Due date"
msgstr "Date d’échéance"
-msgid "During this process, you’ll be asked for URLs from GitLab’s side. Use the URLs shown below."
-msgstr ""
+msgid "Each Runner can be in one of the following states:"
+msgstr "Chaque exécuteur peut être dans l’un des états suivants :"
msgid "Edit"
msgstr "Éditer"
+msgid "Edit Label"
+msgstr "Modifier l’étiquette"
+
msgid "Edit Pipeline Schedule %{id}"
msgstr "Éditer le pipeline programmé %{id}"
msgid "Edit files in the editor and commit changes here"
-msgstr "Modifier les fichiers dans l'éditeur et valider les modifications ici"
-
-msgid "Editing"
-msgstr "En cours de modification"
-
-msgid "Elasticsearch"
-msgstr ""
+msgstr "Modifier les fichiers dans l’éditeur et valider les modifications ici"
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
-msgstr ""
+msgid "Edit identity for %{user_name}"
+msgstr "Modifier l’identité de %{user_name}"
msgid "Email"
-msgstr ""
+msgstr "Courriel"
+
+msgid "Email patch"
+msgstr "Correctif par courriel"
msgid "Emails"
msgstr "Courriels"
+msgid "Embed"
+msgstr "Embarquer"
+
msgid "Enable"
msgstr "Activer"
msgid "Enable Auto DevOps"
msgstr "Activer Auto DevOps"
-msgid "Enable SAML authentication for this group"
-msgstr ""
-
msgid "Enable Sentry for error reporting and logging."
-msgstr ""
+msgstr "Activer Sentry pour les rapports d’erreurs et la journalisation."
msgid "Enable and configure InfluxDB metrics."
msgstr "Activer et configurer les métriques InfluxDB."
@@ -1720,23 +2067,32 @@ msgstr "Activer et configurer les métriques InfluxDB."
msgid "Enable and configure Prometheus metrics."
msgstr "Activer et configurer les métriques Prometheus."
-msgid "Enable classification control using an external service"
-msgstr "Activer le contrôle de classification à l’aide d’un service externe"
+msgid "Enable for this project"
+msgstr "Activer pour ce projet"
+
+msgid "Enable group Runners"
+msgstr "Activer les exécuteurs de groupe"
+
+msgid "Enable or disable certain group features and choose access levels."
+msgstr "Activer ou désactiver certaines fonctions de groupe et choisir les niveaux d’accès."
msgid "Enable or disable version check and usage ping."
-msgstr ""
+msgstr "Activer ou désactiver le contrôle de version et l’envoi des données d’utilisation."
msgid "Enable reCAPTCHA or Akismet and set IP limits."
-msgstr ""
+msgstr "Activer reCAPTCHA ou Akismet et définir des limites d’adresse IP."
msgid "Enable the Performance Bar for a given group."
-msgstr ""
+msgstr "Activer la barre de performance pour un groupe donné."
-msgid "Enabled"
-msgstr ""
+msgid "Ends at (UTC)"
+msgstr "Se termine à (UTC)"
+
+msgid "Environments"
+msgstr "Environnements"
msgid "Environments|An error occurred while fetching the environments."
-msgstr "Une erreur s‘est produite lors de la récupération des environnements."
+msgstr "Une erreur s’est produite lors de la récupération des environnements."
msgid "Environments|An error occurred while making the request."
msgstr "Une erreur s’est produite lors de la requête."
@@ -1763,10 +2119,10 @@ msgid "Environments|No deployments yet"
msgstr "Aucun déploiement pour le moment"
msgid "Environments|Open"
-msgstr "Ouvert"
+msgstr "Ouvrir"
msgid "Environments|Re-deploy"
-msgstr "Re-déployer"
+msgstr "Redéployer"
msgid "Environments|Read more about environments"
msgstr "En savoir plus sur les environnements"
@@ -1775,43 +2131,28 @@ msgid "Environments|Rollback"
msgstr "Revenir en arrière"
msgid "Environments|Show all"
-msgstr "Afficher tous"
+msgstr "Tout afficher"
msgid "Environments|Updated"
-msgstr "Mise à jour"
+msgstr "Mis à jour"
msgid "Environments|You don't have any environments right now."
msgstr "Vous n’avez aucun environnement pour le moment."
-msgid "Epic will be removed! Are you sure?"
-msgstr "L’épopée sera supprimée ! Êtes-vous sûr•e ?"
-
-msgid "Epics"
-msgstr "Épopées"
-
-msgid "Epics Roadmap"
-msgstr "Feuille de route des épopées"
-
-msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
-msgstr "Les épopées vous permettent de gérer votre portefeuille de projets plus efficacement et avec moins d'effort"
-
msgid "Error Reporting and Logging"
-msgstr ""
-
-msgid "Error checking branch data. Please try again."
-msgstr "Erreur lors de la vérification des données de branche. Veuillez réessayer."
+msgstr "Rapport d’erreur et journalisation"
msgid "Error committing changes. Please try again."
msgstr "Erreur lors de la validation des modifications. Veuillez réessayer."
-msgid "Error creating epic"
-msgstr "Erreur lors de la création de l’épopée"
-
msgid "Error fetching contributors data."
-msgstr "Erreur lors de l’extraction des données des contributeur•rice•s."
+msgstr "Erreur lors de l’extraction des données des contributeur·rice·s."
+
+msgid "Error fetching job trace"
+msgstr "Erreur lors de la récupération de la trace de la tâche"
msgid "Error fetching labels."
-msgstr "Erreur lors de la récupération des labels."
+msgstr "Erreur lors de la récupération des étiquettes."
msgid "Error fetching network graph."
msgstr "Erreur lors de la récupération du graphique du réseau."
@@ -1822,20 +2163,35 @@ msgstr "Erreur lors de la récupération des refs"
msgid "Error fetching usage ping data."
msgstr "Erreur lors de la récupération des données d’utilisation."
+msgid "Error loading branch data. Please try again."
+msgstr "Erreur lors du chargement des données de branche. Veuillez réessayer."
+
+msgid "Error loading last commit."
+msgstr "Erreur lors du chargement du dernier commit."
+
+msgid "Error loading merge requests."
+msgstr "Erreur lors du chargement des demandes de fusion."
+
+msgid "Error loading project data. Please try again."
+msgstr "Erreur lors du chargement des données du projet. Veuillez réessayer."
+
msgid "Error occurred when toggling the notification subscription"
-msgstr "Une erreur s’est produite lors de l’activation/désactivation de l’abonnement aux notifications"
+msgstr "Une erreur s’est produite lors de l’activation ou de la désactivation de l’abonnement aux notifications"
msgid "Error saving label update."
-msgstr "Erreur lors de la mise à jour du label."
+msgstr "Erreur lors de la mise à jour de l’étiquette."
msgid "Error updating status for all todos."
msgstr "Erreur lors de la mise à jour de l’état de la liste de tâches à faire."
msgid "Error updating todo status."
-msgstr "Erreur lors de la mise à jour du statut de la tâche à faire."
+msgstr "Erreur lors de la mise à jour du statut de tâche à faire."
+
+msgid "Estimated"
+msgstr "Estimé"
msgid "EventFilterBy|Filter by all"
-msgstr "Aucun filtre"
+msgstr "Tout filtrer"
msgid "EventFilterBy|Filter by comments"
msgstr "Filtrer par commentaires"
@@ -1847,22 +2203,28 @@ msgid "EventFilterBy|Filter by merge events"
msgstr "Filtrer par événements de fusion"
msgid "EventFilterBy|Filter by push events"
-msgstr "Filtrer par événements de poussée"
+msgstr "Filtrer par événements Git push"
msgid "EventFilterBy|Filter by team"
msgstr "Filtrer par équipe"
msgid "Every day (at 4:00am)"
-msgstr "Chaque jour (à 4h00 du matin)"
+msgstr "Chaque jour (à 4 h du matin)"
msgid "Every month (on the 1st at 4:00am)"
-msgstr "Chaque mois (le 1er à 4:00 du matin)"
+msgstr "Chaque mois (le 1ᵉʳ à 4 h du matin)"
msgid "Every week (Sundays at 4:00am)"
-msgstr "Chaque semaine (dimanche à 4h00 du matin)"
+msgstr "Chaque semaine (dimanche à 4 h du matin)"
msgid "Expand"
-msgstr "Ouvrir"
+msgstr "Étendre"
+
+msgid "Expand all"
+msgstr "Tout étendre"
+
+msgid "Expand sidebar"
+msgstr "Étendre la barre latérale"
msgid "Explore projects"
msgstr "Explorer les projets"
@@ -1870,27 +2232,6 @@ msgstr "Explorer les projets"
msgid "Explore public groups"
msgstr "Explorer les groupes publics"
-msgid "External Classification Policy Authorization"
-msgstr "Autorisation de politique de classification externe"
-
-msgid "External authentication"
-msgstr ""
-
-msgid "External authorization denied access to this project"
-msgstr "L’autorisation externe a refusé l’accès à ce projet"
-
-msgid "External authorization request timeout"
-msgstr "Expiration de la demande d’autorisation externe"
-
-msgid "ExternalAuthorizationService|Classification Label"
-msgstr "Label de classification"
-
-msgid "ExternalAuthorizationService|Classification label"
-msgstr "Label de classification"
-
-msgid "ExternalAuthorizationService|When no classification label is set the default label `%{default_label}` will be used."
-msgstr "Quand aucun label de classification n’est défini, le label par défaut `%{default_label}` sera utilisé."
-
msgid "Failed"
msgstr "Échec"
@@ -1900,6 +2241,9 @@ msgstr "Tâches ayant échoué"
msgid "Failed to change the owner"
msgstr "Échec du changement de propriétaire"
+msgid "Failed to check related branches."
+msgstr "Échec de la vérification des branches liées."
+
msgid "Failed to remove issue from board, please try again."
msgstr "Impossible de supprimer le ticket du tableau, veuillez réessayer."
@@ -1909,17 +2253,20 @@ msgstr "Échec de la suppression du pipeline programmé"
msgid "Failed to update issues, please try again."
msgstr "Échec de la mise à jour du ticket. Veuillez réessayer."
+msgid "Failure"
+msgstr "Échec"
+
+msgid "Faster as it re-uses the project workspace (falling back to clone if it doesn't exist)"
+msgstr "Plus rapide parce qu’il réutilise l’espace de travail du projet (faire un clone en solution de secours s’il n’existe pas)"
+
msgid "Feb"
-msgstr "Févr."
+msgstr "févr."
msgid "February"
-msgstr "Février"
+msgstr "février"
msgid "Fields on this page are now uneditable, you can configure"
-msgstr "Les champs sur cette page sont désormais non modifiables, vous pouvez configurer"
-
-msgid "File name"
-msgstr "Nom du fichier"
+msgstr "Les champs de cette page sont désormais non modifiables, vous pouvez configurer"
msgid "Files"
msgstr "Fichiers"
@@ -1927,14 +2274,11 @@ msgstr "Fichiers"
msgid "Files (%{human_size})"
msgstr "Fichiers (%{human_size})"
-msgid "Fill in the fields below, turn on <strong>%{enable_label}</strong>, and press <strong>%{save_changes}</strong>"
-msgstr ""
-
msgid "Filter by commit message"
msgstr "Filtrer par message de commit"
msgid "Find by path"
-msgstr "Rechercher par chemin"
+msgstr "Rechercher par chemin d’accès"
msgid "Find file"
msgstr "Rechercher un fichier"
@@ -1948,205 +2292,58 @@ msgstr "En premier"
msgid "FirstPushedBy|pushed by"
msgstr "poussé par"
-msgid "Font Color"
-msgstr ""
+msgid "For internal projects, any logged in user can view pipelines and access job details (output logs and artifacts)"
+msgstr "Pour les projets internes, tout utilisateur connecté peut afficher les pipelines et accéder aux détails des tâches (journaux de sortie et artefacts)"
-msgid "Footer message"
-msgstr ""
+msgid "For private projects, any member (guest or higher) can view pipelines and access job details (output logs and artifacts)"
+msgstr "Pour les projets privés, tout membre (invité ou supérieur) peut afficher les pipelines et accéder aux détails des tâches (journaux de sortie et artefacts)"
+
+msgid "For public projects, anyone can view pipelines and access job details (output logs and artifacts)"
+msgstr "Pour les projets publics, tout le monde peut afficher des pipelines et accéder aux détails des tâches (journaux de sortie et artefacts)"
msgid "Fork"
msgid_plural "Forks"
-msgstr[0] "Fourche"
-msgstr[1] "Fourches"
+msgstr[0] "Divergence"
+msgstr[1] "Divergences"
msgid "ForkedFromProjectPath|Forked from"
-msgstr "Fourché depuis"
+msgstr "Divergence issue de"
msgid "ForkedFromProjectPath|Forked from %{project_name} (deleted)"
-msgstr "Fourché depuis %{project_name} (supprimé)"
+msgstr "Divergence issue de %{project_name} (supprimé)"
msgid "Forking in progress"
-msgstr "Fourchage en cours"
+msgstr "Divergence en cours"
msgid "Format"
msgstr "Format"
+msgid "Found errors in your .gitlab-ci.yml:"
+msgstr "Erreurs trouvées dans votre fichier .gitlab-ci.yml :"
+
msgid "From %{provider_title}"
msgstr "De %{provider_title}"
msgid "From issue creation until deploy to production"
-msgstr "Depuis la création du ticket 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"
+msgstr "Depuis la fusion de la demande de fusion jusqu’au déploiement en production"
msgid "From the Kubernetes cluster details view, install Runner from the applications list"
-msgstr "À partir de l’affichage des détails du cluster Kubernetes, installez un Exécuteur à partir de la liste des applications"
+msgstr "À partir de l’affichage des détails de la grappe de serveurs Kubernetes, installez un exécuteur à partir de la liste des applications"
msgid "GPG Keys"
-msgstr "Clés GPG"
+msgstr "Clefs GPG"
-msgid "Generate a default set of labels"
-msgstr "Générer un ensemble de labels par défaut"
+msgid "General"
+msgstr "Général"
-msgid "Geo Nodes"
-msgstr "Nœuds Geo"
+msgid "General pipelines"
+msgstr "Pipelines généraux"
-msgid "Geo allows you to replicate your GitLab instance to other geographical locations."
-msgstr ""
-
-msgid "GeoNodeSyncStatus|Node is failing or broken."
-msgstr "Le nœud est défaillant ou cassé."
-
-msgid "GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage."
-msgstr "Le nœud est lent, surchargé, ou il vient juste de récupérer après un problème."
-
-msgid "GeoNodes|Checksummed"
-msgstr "Vérifié par somme de contrôle"
-
-msgid "GeoNodes|Database replication lag:"
-msgstr "Retard de réplication de la base de données :"
-
-msgid "GeoNodes|Disabling a node stops the sync process. Are you sure?"
-msgstr "La désactivation d’un nœud arrête le processus de synchronisation. Êtes-vous sûr•e?"
-
-msgid "GeoNodes|Does not match the primary storage configuration"
-msgstr "Ne correspond pas à la configuration de stockage principale"
-
-msgid "GeoNodes|Failed"
-msgstr "Échec"
-
-msgid "GeoNodes|Full"
-msgstr "Complet"
-
-msgid "GeoNodes|GitLab version does not match the primary node version"
-msgstr "La version de GitLab ne correspond pas à la version du nœud principal"
-
-msgid "GeoNodes|GitLab version:"
-msgstr "Version de GitLab :"
-
-msgid "GeoNodes|Health status:"
-msgstr "État :"
-
-msgid "GeoNodes|Last event ID processed by cursor:"
-msgstr "Dernier ID d’événement traité par le curseur :"
-
-msgid "GeoNodes|Last event ID seen from primary:"
-msgstr "Dernier événement vu du nœud primaire :"
-
-msgid "GeoNodes|Loading nodes"
-msgstr "Chargement des nœuds"
-
-msgid "GeoNodes|Local Attachments:"
-msgstr "Pièces jointes locales :"
-
-msgid "GeoNodes|Local LFS objects:"
-msgstr "Objets LFS locaux :"
-
-msgid "GeoNodes|Local job artifacts:"
-msgstr "Artefacts de tâches locaux :"
-
-msgid "GeoNodes|New node"
-msgstr "Nouveau nœud"
-
-msgid "GeoNodes|Node Authentication was successfully repaired."
-msgstr "Le nœud d'Authentification a été réparé avec succès."
-
-msgid "GeoNodes|Node was successfully removed."
-msgstr "Le nœud a été supprimé avec succès."
-
-msgid "GeoNodes|Not checksummed"
-msgstr "Non vérifié par somme de contrôle"
-
-msgid "GeoNodes|Out of sync"
-msgstr "Désynchronisé"
-
-msgid "GeoNodes|Removing a node stops the sync process. Are you sure?"
-msgstr "Supprimer un nœud arrête le processus de synchronisation. Êtes-vous sûr•e ?"
-
-msgid "GeoNodes|Replication slot WAL:"
-msgstr "Emplacement de réplication WAL :"
-
-msgid "GeoNodes|Replication slots:"
-msgstr "Emplacements de réplication :"
-
-msgid "GeoNodes|Repositories checksummed:"
-msgstr "Dépôts avec une somme de contrôle calculée:"
-
-msgid "GeoNodes|Repositories:"
-msgstr "Dépôts :"
-
-msgid "GeoNodes|Repository checksums verified:"
-msgstr "Dépôts avec une somme de contrôle vérifiée :"
-
-msgid "GeoNodes|Selective"
-msgstr "Sélectif"
-
-msgid "GeoNodes|Something went wrong while changing node status"
-msgstr "Une erreur s’est produite lors du changement de statut du nœud"
-
-msgid "GeoNodes|Something went wrong while removing node"
-msgstr "Une erreur s’est produite lors de la suppression du nœud"
-
-msgid "GeoNodes|Something went wrong while repairing node"
-msgstr "Une erreur s’est produite lors de la réparation du nœud"
-
-msgid "GeoNodes|Storage config:"
-msgstr "Configuration de stockage :"
-
-msgid "GeoNodes|Sync settings:"
-msgstr "Paramètres de synchronisation :"
-
-msgid "GeoNodes|Synced"
-msgstr "Synchronisé"
-
-msgid "GeoNodes|Unused slots"
-msgstr "Emplacements non utilisés"
-
-msgid "GeoNodes|Unverified"
-msgstr "Non vérifié"
-
-msgid "GeoNodes|Used slots"
-msgstr "Emplacements utilisés"
-
-msgid "GeoNodes|Verified"
-msgstr "Vérifié"
-
-msgid "GeoNodes|Wiki checksums verified:"
-msgstr "Wiki avec une somme de contrôle vérifiée :"
-
-msgid "GeoNodes|Wikis checksummed:"
-msgstr "Wiki avec une somme de contrôle calculée :"
-
-msgid "GeoNodes|Wikis:"
-msgstr "Wikis :"
-
-msgid "GeoNodes|You have configured Geo nodes using an insecure HTTP connection. We recommend the use of HTTPS."
-msgstr "Vous avez configuré des nœuds Geo en utilisant une connexion HTTP non sécurisée. Nous recommandons l’utilisation de HTTPS."
-
-msgid "Geo|All projects"
-msgstr "Tous les projets"
-
-msgid "Geo|File sync capacity"
-msgstr "Capacité de synchronisation de fichier"
-
-msgid "Geo|Groups to synchronize"
-msgstr "Groupes à synchroniser"
-
-msgid "Geo|Projects in certain groups"
-msgstr "Projets dans certains groupes"
-
-msgid "Geo|Projects in certain storage shards"
-msgstr "Projets dans certains fragments de stockage"
-
-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 "Geo|Shards to synchronize"
-msgstr "Fragments à synchroniser"
+msgid "Generate a default set of labels"
+msgstr "Générer un jeu d’étiquettes par défaut"
msgid "Git repository URL"
msgstr "URL du dépôt Git"
@@ -2155,7 +2352,10 @@ msgid "Git revision"
msgstr "Révision de Git"
msgid "Git storage health information has been reset"
-msgstr "Les informations de santé du stockage Git ont été réinitialisées"
+msgstr "Les informations sur l’état de santé du stockage Git ont été réinitialisées"
+
+msgid "Git strategy for pipelines"
+msgstr "Stratégie Git pour les pipelines"
msgid "Git version"
msgstr "Version de Git"
@@ -2166,83 +2366,86 @@ msgstr "Importation de GitHub"
msgid "GitLab CI Linter has been moved"
msgstr "GitLab CI Linter a été déplacé"
-msgid "GitLab Geo"
-msgstr ""
+msgid "GitLab Group Runners can execute code for all the projects in this group."
+msgstr "Les exécuteurs de groupe peuvent exécuter du code pour tous les projets de ce groupe."
msgid "GitLab Runner section"
-msgstr "Section de l'Exécuteur GitLab"
-
-msgid "GitLab single sign on URL"
-msgstr ""
+msgstr "Section de l’exécuteur GitLab"
msgid "Gitaly"
-msgstr ""
+msgstr "Gitaly"
msgid "Gitaly Servers"
msgstr "Serveurs Gitaly"
+msgid "Gitaly|Address"
+msgstr "Adresse"
+
+msgid "Go Back"
+msgstr "Retour"
+
msgid "Go back"
-msgstr "Retour en arrière"
+msgstr "Retour"
msgid "Go to your fork"
-msgstr "Aller à votre fourche"
+msgstr "Aller à votre dépôt divergent"
msgid "GoToYourFork|Fork"
-msgstr "Fourche"
+msgstr "Dépôt divergent"
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."
+msgstr "L’authentification Google n’est pas %{link_to_documentation}. Demandez à votre administrat·eur·rice GitLab si vous souhaitez utiliser ce service."
msgid "Got it!"
-msgstr "Compris !"
-
-msgid "GroupRoadmap|Epics let you manage your portfolio of projects more efficiently and with less effort"
-msgstr "Les épopées vous permettent de gérer votre portefeuille de projets plus efficacement et avec moins d’effort"
+msgstr "Compris !"
-msgid "GroupRoadmap|From %{dateWord}"
-msgstr "À partir de %{dateWord}"
+msgid "Graph"
+msgstr "Graphique"
-msgid "GroupRoadmap|Loading roadmap"
-msgstr "Chargement de la feuille de route"
+msgid "Group CI/CD settings"
+msgstr "Paramètres du groupe CI/CD"
-msgid "GroupRoadmap|Something went wrong while fetching epics"
-msgstr "Une erreur s’est produite lors de la récupération des épopées"
+msgid "Group ID"
+msgstr "Identifiant du groupe"
-msgid "GroupRoadmap|To view the roadmap, add a planned start or finish date to one of your epics in this group or its subgroups. Only epics in the past 3 months and the next 3 months are shown &ndash; from %{startDate} to %{endDate}."
-msgstr "Pour afficher la feuille de route, ajoutez une date de début ou de fin planifiée à l'une de vos épopées dans ce groupe ou ses sous-groupes. Seules les épopées des 3 derniers mois et des 3 prochains mois sont affichées &ndash; de %{startDate} à %{endDate}."
+msgid "Group Runners"
+msgstr "Exécuteurs de groupe"
-msgid "GroupRoadmap|Until %{dateWord}"
-msgstr "Jusqu’au %{dateWord}"
+msgid "Group maintainers can register group runners in the %{link}"
+msgstr "Les responsables de groupe peuvent créer des exécuteurs de groupe via %{link}"
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
-msgstr "Empêcher le partage d'un projet de %{group} avec d'autres groupes"
+msgstr "Empêcher le partage d’un projet du groupe %{group} avec d’autres groupes"
msgid "GroupSettings|Share with group lock"
-msgstr "Verrou de partage avec un groupe"
+msgstr "Partager avec un verrou de groupe"
msgid "GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup."
-msgstr "Ce paramètre est appliqué sur %{ancestor_group} et a été modifié dans ce sous-groupe."
+msgstr "Ce paramètre s’applique au groupe %{ancestor_group} et a été forcé pour 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 "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}."
+msgstr "Ce paramètre s’applique au groupe %{ancestor_group}. Pour partager des projets de ce groupe avec un autre groupe, demandez au propriétaire d’écraser ce paramètre ou de %{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 "Ce paramètre s’applique sur %{ancestor_group}. Vous pouvez modifier le paramètre ou le %{remove_ancestor_share_with_group_lock}."
+msgstr "Ce paramètre s’applique au groupe %{ancestor_group}. Vous pouvez écraser 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 "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."
+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 "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"
+msgstr "ne peut pas être désactivé lorsque le groupe parent a activé le « Partage avec verrou de groupe », sauf par le propriétaire du groupe parent."
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
-msgstr "supprimer le verrou de partage avec un groupe pour %{ancestor_group_name}"
+msgstr "supprimer le partage avec verrou de groupe pour %{ancestor_group_name}"
+
+msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
+msgstr "Les groupes peuvent également être imbriqués en créant des %{subgroup_docs_link_start}sousâ€groupes%{subgroup_docs_link_end}."
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."
+msgstr "Si vous organisez vos projets au sein d’un groupe, cela fonctionne comme un dossier."
msgid "GroupsEmptyState|No groups found"
msgstr "Aucun groupe trouvé"
@@ -2254,7 +2457,7 @@ 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."
+msgstr "Créer un sousâ€groupe de ce groupe."
msgid "GroupsTree|Edit group"
msgstr "Modifier le groupe"
@@ -2277,17 +2480,11 @@ 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 "Have your users email"
-msgstr "Lister les emails utilisateurs"
-
-msgid "Header message"
-msgstr ""
-
msgid "Health Check"
msgstr "État des services"
msgid "Health information can be retrieved from the following endpoints. More information is available"
-msgstr "L’état des service peut être récupéré depuis les adresses suivantes. Plus d’information disponible."
+msgstr "L’état des services peut être récupéré depuis les emplacements suivants. Plus d’information disponible."
msgid "HealthCheck|Access token is"
msgstr "Le jeton d’accès est"
@@ -2315,26 +2512,65 @@ msgid_plural "Hide values"
msgstr[0] "Masquer la valeur"
msgstr[1] "Masquer les valeurs"
+msgid "Hide whitespace changes"
+msgstr "Masquer les modifications des espaces"
+
msgid "History"
msgstr "Historique"
msgid "Housekeeping successfully started"
msgstr "Maintenance démarrée avec succès"
-msgid "Identity provider single sign on URL"
-msgstr ""
+msgid "I accept the %{terms_link}"
+msgstr "J’accepte les %{terms_link}"
-msgid "If enabled, access to projects will be validated on an external service using their classification label."
-msgstr "Si activé, l’accès aux projets sera validé sur un service externe en utilisant leur étiquette de classification."
+msgid "I accept the|Terms of Service and Privacy Policy"
+msgstr "Conditions générales d’utilisation et politique de confidentialité"
-msgid "If using GitHub, you’ll see pipeline statuses on GitHub for your commits and pull requests. %{more_info_link}"
-msgstr "Si vous utilisez GitHub, vous verrez les statuts des pipelines sur GitHub pour vos commit et demandes de fusion. %{more_info_link}"
+msgid "ID"
+msgstr "Identifiant"
+
+msgid "IDE|Commit"
+msgstr "Valider"
+
+msgid "IDE|Edit"
+msgstr "Modifier"
+
+msgid "IDE|Go back"
+msgstr "Revenir en arrière"
+
+msgid "IDE|Open in file view"
+msgstr "Ouvrir dans le visionneur de fichiers"
+
+msgid "IDE|Review"
+msgstr "Examiner"
+
+msgid "Identifier"
+msgstr "Identifiant"
+
+msgid "Identities"
+msgstr "Identités"
+
+msgid "If disabled, the access level will depend on the user's permissions in the project."
+msgstr "Si désactivé, le niveau d’accès dépendra des autorisations de l’utilisa·teur·trice dans le projet."
+
+msgid "If enabled"
+msgstr ""
msgid "If you already have files you can push them using the %{link_to_cli} below."
-msgstr "Si vous avez déjà des fichiers, vous pouvez les pousser à l’aide de la %{link_to_cli} ci-dessous."
+msgstr "Si vous avez déjà des fichiers, vous pouvez les pousser à l’aide de la %{link_to_cli} ciâ€dessous."
msgid "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>."
-msgstr "Si votre dépôt HTTP n’est pas accessible publiquement, ajoutez des informations d’authentification à l’URL: <code>https: //utilisateur:mot_de_passe@gitlab.company.com/group/project.git</code>."
+msgstr "Si votre dépôt HTTP n’est pas accessible publiquement, ajoutez des informations d’authentification à l’URL : <code>https://utilisateur:mot_de_passe@gitlab.company.com/group/project.git</code>."
+
+msgid "ImageDiffViewer|2-up"
+msgstr "côte à côte"
+
+msgid "ImageDiffViewer|Onion skin"
+msgstr "en pelure d’oignon"
+
+msgid "ImageDiffViewer|Swipe"
+msgstr "par balayage"
msgid "Import"
msgstr "Importer"
@@ -2351,52 +2587,44 @@ msgstr "Importer des dépôts à partir de GitHub"
msgid "Import repository"
msgstr "Importer un dépôt"
-msgid "ImportButtons|Connect repositories from"
-msgstr "Connecter des dépôts à partir de"
+msgid "Include a Terms of Service agreement and Privacy Policy that all users must accept."
+msgstr "Inclure un accord sur les conditions générales d’utilisation et la politique de confidentialité que tous les utilisateurs doivent accepter."
-msgid "Improve Issue boards with GitLab Enterprise Edition."
-msgstr "Améliorer les tableaux 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 "Inline"
+msgstr ""
msgid "Install Runner on Kubernetes"
-msgstr "Installez un Exécuteur sur Kubernetes"
+msgstr "Installez un exécuteur sur Kubernetes"
msgid "Install a Runner compatible with GitLab CI"
-msgstr "Installez un Exécuteur compatible avec l'intégration continue de GitLab"
-
-msgid "Instance"
-msgid_plural "Instances"
-msgstr[0] "Instance"
-msgstr[1] "Instances"
+msgstr "Installez un exécuteur compatible avec l’intégration continue de GitLab"
msgid "Instance does not support multiple Kubernetes clusters"
-msgstr "L’instance ne prend pas en charge plusieurs clusters Kubernetes"
+msgstr "L’instance ne prend pas en charge plusieurs grappes de serveurs Kubernetes"
msgid "Integrations"
msgstr "Intégrations"
+msgid "Integrations Settings"
+msgstr "Paramètres des intégrations"
+
msgid "Interested parties can even contribute by pushing commits if they want to."
-msgstr "Les parties intéressées peuvent même contribuer en poussant des commits si elles le souhaitent."
+msgstr "Les personnes intéressées peuvent même contribuer en poussant des commits si elles le souhaitent."
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."
+msgstr "Interne — le groupe ainsi que tous les projets internes sont accessibles à 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."
+msgstr "Interne — le projet est accessible à n’importe quel·le utilisa·teur·trice connecté·e."
msgid "Interval Pattern"
-msgstr "Schéma d’intervalle"
+msgstr "Modèle d’intervalle"
msgid "Introducing Cycle Analytics"
-msgstr "Introduction à l'analyseur de cycle"
+msgstr "Introduction à l’analyseur de cycle"
-msgid "Issue board focus mode"
-msgstr "Mode focus du tableau de tickets"
+msgid "Issue Board"
+msgstr "Tableau des tickets"
msgid "Issue events"
msgstr "Événements du ticket"
@@ -2404,62 +2632,68 @@ msgstr "Événements du ticket"
msgid "IssueBoards|Board"
msgstr "Tableau"
-msgid "IssueBoards|Boards"
-msgstr "Tableaux"
-
msgid "Issues"
msgstr "Tickets"
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
-msgstr "Les tickets peuvent être des bugs, des tâches ou des idées à discuter. De plus, les tickets sont consultables et filtrables."
+msgstr "Les tickets peuvent être des bogues, des tâches ou des sujets de discussion. De plus, les tickets sont consultables et filtrables."
msgid "Jan"
-msgstr "Janv."
+msgstr "janv."
msgid "January"
-msgstr "Janvier"
+msgstr "janvier"
+
+msgid "Job"
+msgstr "Tâche"
+
+msgid "Job has been erased"
+msgstr "La tâche a été supprimée"
msgid "Jobs"
msgstr "Tâches"
msgid "Jul"
-msgstr "Juill."
+msgstr "juill."
msgid "July"
-msgstr "Juillet"
+msgstr "juillet"
msgid "Jun"
-msgstr "Juin"
+msgstr "juin"
msgid "June"
-msgstr "Juin"
+msgstr "juin"
msgid "Koding"
-msgstr ""
+msgstr "Koding"
msgid "Kubernetes"
msgstr "Kubernetes"
msgid "Kubernetes Cluster"
-msgstr "Cluster Kubernetes"
+msgstr "Grappe de serveurs Kubernetes"
msgid "Kubernetes cluster creation time exceeds timeout; %{timeout}"
-msgstr "Le temps de création du cluster Kubernetes dépasse le délai d’expiration ; %{timeout}"
+msgstr "Le temps de création de la grappe de serveurs Kubernetes dépasse le délai d’expiration : %{timeout}"
msgid "Kubernetes cluster integration was not removed."
-msgstr "L’intégration du cluster Kubernetes n’a pas été supprimée."
+msgstr "L’intégration de la grappe de serveurs Kubernetes n’a pas été supprimée."
msgid "Kubernetes cluster integration was successfully removed."
-msgstr "L’intégration du cluster Kubernetes a été supprimée avec succès."
+msgstr "L’intégration de la grappe de serveurs Kubernetes a été supprimée avec succès."
msgid "Kubernetes cluster was successfully updated."
-msgstr "Le cluster Kubernetes a été mis à jour avec succès."
+msgstr "La grappe de serveurs Kubernetes a été mise à jour avec succès."
msgid "Kubernetes configured"
msgstr "Kubernetes configuré"
msgid "Kubernetes service integration has been deprecated. %{deprecated_message_content} your Kubernetes clusters using the new <a href=\"%{url}\"/>Kubernetes Clusters</a> page"
-msgstr "L’intégration du service Kubernetes est obsolète. %{deprecated_message_content} vos clusters Kubernetes en utilisant la nouvelle page <a href=\"%{url}\"/>Clusters Kubernetes</a>"
+msgstr "L’intégration du service Kubernetes est obsolète. %{deprecated_message_content} vos grappes de serveurs Kubernetes en utilisant la nouvelle page <a href=\"%{url}\"/>Clusters Kubernetes</a>"
+
+msgid "LFS"
+msgstr ""
msgid "LFSStatus|Disabled"
msgstr "Désactivé"
@@ -2468,7 +2702,10 @@ msgid "LFSStatus|Enabled"
msgstr "Activé"
msgid "Label"
-msgstr "Label"
+msgstr "Étiquette"
+
+msgid "Label actions dropdown"
+msgstr ""
msgid "LabelSelect|%{firstLabelName} +%{remainingLabelCount} more"
msgstr "%{firstLabelName} +%{remainingLabelCount} de plus"
@@ -2477,24 +2714,27 @@ msgid "LabelSelect|%{labelsString}, and %{remainingLabelCount} more"
msgstr "%{labelsString} et %{remainingLabelCount} de plus"
msgid "Labels"
-msgstr "Labels"
+msgstr "Étiquettes"
msgid "Labels can be applied to %{features}. Group labels are available for any project within the group."
-msgstr "Les labels peuvent être appliqués à %{features}. Des labels de groupe sont disponibles pour tout type de projet au sein du groupe."
+msgstr "Des étiquettes peuvent être appliquées à %{features}. Les étiquettes de groupe sont disponibles pour tout projet au sein du groupe."
msgid "Labels can be applied to issues and merge requests to categorize them."
-msgstr "Les labels peuvent être appliqués aux tickets et aux demandes de fusion pour les classer."
+msgstr "Les étiquettes peuvent être appliquées aux tickets et aux demandes de fusion afin de les catégoriser."
+
+msgid "Labels can be applied to issues and merge requests."
+msgstr "Les étiquettes peuvent être appliquées aux tickets et aux demandes de fusion."
msgid "Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>"
-msgstr "<span>Promouvoir le label</span> %{labelTitle} <span>en label de groupe ?</span>"
+msgstr "<span>Promouvoir l’étiquette</span> %{labelTitle} <span>en étiquette de groupe ?</span>"
msgid "Labels|Promote Label"
-msgstr "Promouvoir le label"
+msgstr "Promouvoir l’étiquette"
msgid "Last %d day"
msgid_plural "Last %d days"
msgstr[0] "Le dernier %d jour"
-msgstr[1] "Les derniers %d jours"
+msgstr[1] "Les %d derniers jours"
msgid "Last Pipeline"
msgstr "Dernier pipeline"
@@ -2520,6 +2760,9 @@ msgstr "Vous avez poussé sur"
msgid "LastPushEvent|at"
msgstr "à"
+msgid "Latest changes"
+msgstr "Derniers changements"
+
msgid "Learn more"
msgstr "En savoir plus"
@@ -2530,10 +2773,10 @@ msgid "Learn more about protected branches"
msgstr "En savoir plus sur les branches protégées"
msgid "Learn more in the"
-msgstr "En apprendre plus dans le"
+msgstr "Apprenezâ€en plus dans la"
msgid "Learn more in the|pipeline schedules documentation"
-msgstr "documentation concernant la programmation de pipeline"
+msgstr "documentation sur la programmation de pipelines"
msgid "Leave"
msgstr "Quitter"
@@ -2544,9 +2787,6 @@ msgstr "Quitter le groupe"
msgid "Leave project"
msgstr "Quitter le projet"
-msgid "License"
-msgstr "Licence"
-
msgid "List"
msgstr "Liste"
@@ -2556,6 +2796,9 @@ msgstr "Lister vos dépôts GitHub"
msgid "Loading the GitLab IDE..."
msgstr "Chargement de l’IDE GitLab…"
+msgid "Loading..."
+msgstr "Chargement…"
+
msgid "Lock"
msgstr "Verrouiller"
@@ -2565,50 +2808,47 @@ msgstr "Verrouiller %{issuableDisplayName}"
msgid "Lock not found"
msgstr "Verrou non trouvé"
+msgid "Lock to current projects"
+msgstr "Verrouiller aux projets en cours"
+
msgid "Locked"
msgstr "Verrouillé"
-msgid "Locked Files"
-msgstr "Fichiers verrouillés"
-
-msgid "Locks give the ability to lock specific file or folder."
-msgstr "Les verrous donnent la possibilité de verrouiller un fichier ou un dossier spécifique."
+msgid "Locked to current projects"
+msgstr "Verrouillé aux projets en cours"
msgid "Login"
msgstr "Se connecter"
-msgid "Make everyone on your team more productive regardless of their location. GitLab Geo creates read-only mirrors of your GitLab instance so you can reduce the time it takes to clone and fetch large repos."
-msgstr "Rendez votre équipe plus productive quel que soit son emplacement. GitLab Geo crée des miroirs en lecture seule de votre instance GitLab afin que vous puissiez réduire le temps nécessaire pour cloner et récupérer de gros dépôts."
-
msgid "Manage all notifications"
msgstr "Gérer toutes les notifications"
msgid "Manage group labels"
-msgstr "Gérer les labels de groupe"
+msgstr "Gérer les étiquettes de groupe"
msgid "Manage labels"
-msgstr "Gérer les labels"
+msgstr "Gérer les étiquettes"
msgid "Manage project labels"
-msgstr "Gérer les labels de projet"
-
-msgid "Manage your group’s membership while adding another level of security with SAML."
-msgstr ""
+msgstr "Gérer les étiquettes de projet"
msgid "Mar"
-msgstr "Mars"
+msgstr "mars"
msgid "March"
-msgstr "Mars"
+msgstr "mars"
-msgid "Mark done"
+msgid "Mark todo as done"
msgstr "Marquer comme fait"
+msgid "Markdown enabled"
+msgstr "Markdown activé"
+
msgid "Maximum git storage failures"
-msgstr "Nombre maximum d’échecs du stockage git"
+msgstr "Nombre maximum d’échecs du stockage Git"
msgid "May"
-msgstr "Mai"
+msgstr "mai"
msgid "Median"
msgstr "Médian"
@@ -2616,8 +2856,8 @@ msgstr "Médian"
msgid "Members"
msgstr "Membres"
-msgid "Members will be forwarded here when signing in to your group. Get this from your identity provider, where it can also be called \"SSO Service Location\", \"SAML Token Issuance Endpoint\", or \"SAML 2.0/W-Federation URL\"."
-msgstr ""
+msgid "Merge Request:"
+msgstr "Demande de fusion :"
msgid "Merge Requests"
msgstr "Demandes de fusion"
@@ -2628,98 +2868,53 @@ msgstr "Événements de fusion"
msgid "Merge request"
msgstr "Demande de fusion"
+msgid "Merge requests"
+msgstr "Demandes de fusion"
+
msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
msgstr "Les demandes de fusion permettent de proposer les modifications que vous avez apportées à un projet et de discuter de ces modifications avec les autres"
-msgid "Merged"
-msgstr "Fusionnée"
-
-msgid "Messages"
-msgstr "Messages"
-
-msgid "Metrics - Influx"
-msgstr "Métriques - Influx"
-
-msgid "Metrics - Prometheus"
-msgstr "Métriques - Prometheus"
-
-msgid "Metrics|Business"
-msgstr "Affaires"
-
-msgid "Metrics|Create metric"
-msgstr "Créer une métrique"
-
-msgid "Metrics|Edit metric"
-msgstr "Modifier la métrique"
-
-msgid "Metrics|For grouping similar metrics"
-msgstr "Pour regrouper des métriques similaires"
-
-msgid "Metrics|Label of the chart's vertical axis. Usually the type of the unit being charted. The horizontal axis (X-axis) always represents time."
-msgstr "Etiquette de l’axe vertical du graphique. Habituellement, le type de l’unité étant cartographiée. L’axe horizontal (axe X) représente toujours le temps."
-
-msgid "Metrics|Legend label (optional)"
-msgstr "Légende de l’étiquette (facultatif)"
-
-msgid "Metrics|Must be a valid PromQL query."
-msgstr "Doit être une requête PromQL valide."
-
-msgid "Metrics|Name"
-msgstr "Nom"
-
-msgid "Metrics|New metric"
-msgstr "Nouvelle métrique"
+msgid "MergeRequests|Resolve this discussion in a new issue"
+msgstr "Résoudre cette discussion avec un nouveau ticket"
-msgid "Metrics|Prometheus Query Documentation"
-msgstr "Documentation de requête Prometheus"
+msgid "MergeRequests|Saving the comment failed"
+msgstr "L’enregistrement du commentaire a échoué"
-msgid "Metrics|Query"
-msgstr "Requête"
+msgid "MergeRequests|Toggle comments for this file"
+msgstr "Activer/désactiver les commentaires pour ce fichier"
-msgid "Metrics|Response"
-msgstr "Réponse"
+msgid "MergeRequests|Updating discussions failed"
+msgstr "Échec de la mise à jour des discussions"
-msgid "Metrics|System"
-msgstr "Système"
-
-msgid "Metrics|Type"
-msgstr "Type"
-
-msgid "Metrics|Unit label"
-msgstr "Étiquette de l’unité"
-
-msgid "Metrics|Used as a title for the chart"
-msgstr "Utilisé comme titre pour le graphique"
-
-msgid "Metrics|Used if the query returns a single series. If it returns multiple series, their legend labels will be picked up from the response."
-msgstr "Utilisé si la requête renvoie une seule série. Si elle renvoie plusieurs séries, leurs étiquettes de légende seront récupérées à partir de la réponse."
-
-msgid "Metrics|Y-axis label"
-msgstr "Étiquette de l’axe Y"
+msgid "MergeRequests|View file @ %{commitId}"
+msgstr ""
-msgid "Metrics|e.g. HTTP requests"
-msgstr "Par exemple, requêtes HTTP"
+msgid "MergeRequests|View replaced file @ %{commitId}"
+msgstr ""
-msgid "Metrics|e.g. Requests/second"
-msgstr "Par exemple, requêtes / seconde"
+msgid "Merged"
+msgstr "Fusionnée"
-msgid "Metrics|e.g. Throughput"
-msgstr "Par exemple, débit"
+msgid "Messages"
+msgstr "Messages"
-msgid "Metrics|e.g. rate(http_requests_total[5m])"
-msgstr "Par exemple, rate(http_requests_total[5m])"
+msgid "Metrics - Influx"
+msgstr "Métriques — Influx"
-msgid "Metrics|e.g. req/sec"
-msgstr "Par exemple, req/sec"
+msgid "Metrics - Prometheus"
+msgstr "Métriques — Prometheus"
msgid "Milestone"
msgstr "Jalon"
+msgid "Milestones"
+msgstr "Jalons"
+
msgid "Milestones|Delete milestone"
msgstr "Supprimer le jalon"
msgid "Milestones|Delete milestone %{milestoneTitle}?"
-msgstr "Supprimer le jalon %{milestoneTitle} ?"
+msgstr "Supprimer le jalon %{milestoneTitle} ?"
msgid "Milestones|Failed to delete milestone %{milestoneTitle}"
msgstr "Impossible de supprimer le jalon %{milestoneTitle}"
@@ -2728,16 +2923,13 @@ msgid "Milestones|Milestone %{milestoneTitle} was not found"
msgstr "Le jalon %{milestoneTitle} est introuvable"
msgid "Milestones|Promote %{milestoneTitle} to group milestone?"
-msgstr "Promouvoir %{milestoneTitle} à un jalon de groupe ?"
+msgstr "Promouvoir %{milestoneTitle} en tant que jalon de groupe ?"
msgid "Milestones|Promote Milestone"
msgstr "Promouvoir le jalon"
-msgid "Milestones|This action cannot be reversed."
-msgstr "Cette action ne peut pas être annulée."
-
msgid "MissingSSHKeyWarningLink|add an SSH key"
-msgstr "ajouter une clé SSH"
+msgstr "ajouter une clef SSH"
msgid "Modal|Cancel"
msgstr "Annuler"
@@ -2746,10 +2938,10 @@ msgid "Modal|Close"
msgstr "Fermer"
msgid "Monitoring"
-msgstr "Surveillance"
+msgstr "Supervision"
-msgid "More info"
-msgstr "En savoir plus"
+msgid "More actions"
+msgstr "Plus d’actions"
msgid "More information"
msgstr "Plus d’informations"
@@ -2763,11 +2955,29 @@ msgstr "Déplacer"
msgid "Move issue"
msgstr "Déplacer le ticket"
-msgid "Multiple issue boards"
-msgstr "Multiple tableaux de tickets"
+msgid "Name"
+msgstr "Nom"
msgid "Name new label"
-msgstr "Nommez le nouveau label"
+msgstr "Nommez la nouvelle étiquette"
+
+msgid "Name your individual key via a title"
+msgstr "Nommez votre clef personnelle avec un titre"
+
+msgid "Nav|Help"
+msgstr "Aide"
+
+msgid "Nav|Home"
+msgstr "Accueil"
+
+msgid "Nav|Sign In / Register"
+msgstr "Connexion / Inscription"
+
+msgid "Nav|Sign out and sign in with a different account"
+msgstr "Se déconnecter et se reconnecter avec un autre compte"
+
+msgid "New Identity"
+msgstr "Nouvelle identité"
msgid "New Issue"
msgid_plural "New Issues"
@@ -2775,13 +2985,16 @@ msgstr[0] "Nouveau ticket"
msgstr[1] "Nouveaux tickets"
msgid "New Kubernetes Cluster"
-msgstr "Nouveau cluster Kubernetes"
+msgstr "Nouvelle grappe de serveurs Kubernetes"
msgid "New Kubernetes cluster"
-msgstr "Nouveau cluster Kubernetes"
+msgstr "Nouvelle grappe de serveurs Kubernetes"
+
+msgid "New Label"
+msgstr "Nouvelle étiquette"
msgid "New Pipeline Schedule"
-msgstr "Nouveau pipeline programmé"
+msgstr "Nouvelle planification de pipeline"
msgid "New branch"
msgstr "Nouvelle branche"
@@ -2792,41 +3005,44 @@ msgstr "Nouvelle branche indisponible"
msgid "New directory"
msgstr "Nouveau dossier"
-msgid "New epic"
-msgstr "Nouvelle épopée"
-
msgid "New file"
msgstr "Nouveau fichier"
msgid "New group"
msgstr "Nouveau groupe"
+msgid "New identity"
+msgstr "Nouvelle identité"
+
msgid "New issue"
msgstr "Nouveau ticket"
msgid "New label"
-msgstr "Nouveau label"
+msgstr "Nouvelle étiquette"
msgid "New merge request"
msgstr "Nouvelle demande de fusion"
+msgid "New pipelines will cancel older, pending pipelines on the same branch"
+msgstr "De nouveaux pipelines annuleront les anciens pipelines en attente sur la même branche"
+
msgid "New project"
msgstr "Nouveau projet"
msgid "New schedule"
-msgstr "Nouveau programme"
+msgstr "Nouvelle planification"
msgid "New snippet"
msgstr "Nouvel extrait de code"
msgid "New subgroup"
-msgstr "Nouveau sous-groupe"
+msgstr "Nouveau sousâ€groupe"
msgid "New tag"
-msgstr "Nouveau tag"
+msgstr "Nouvelle étiquette"
-msgid "No Label"
-msgstr "Pas de label"
+msgid "No"
+msgstr "Non"
msgid "No assignee"
msgstr "Aucune personne assignée"
@@ -2835,7 +3051,7 @@ msgid "No changes"
msgstr "Aucun changement"
msgid "No connection could be made to a Gitaly Server, please check your logs!"
-msgstr "Aucune connexion n’a pu être établie avec un serveur Gitaly, veuillez vérifier vos logs !"
+msgstr "Aucune connexion n’a pu être établie avec un serveur Gitaly, veuillez vérifier votre journal !"
msgid "No due date"
msgstr "Aucune date d’échéance"
@@ -2846,20 +3062,29 @@ msgstr "Aucune estimation ou temps passé"
msgid "No file chosen"
msgstr "Aucun fichier sélectionné"
-msgid "No labels created yet."
-msgstr "Aucun label créé pour le moment."
+msgid "No files found"
+msgstr "Aucun fichier trouvé"
+
+msgid "No files found."
+msgstr "Aucun fichier trouvé."
+
+msgid "No merge requests found"
+msgstr "Aucune demande de fusion trouvée"
+
+msgid "No messages were logged"
+msgstr "Aucun message n’a été enregistré"
msgid "No repository"
-msgstr "Pas de dépôt"
+msgstr "Aucun dépôt"
msgid "No schedules"
-msgstr "Aucun programme"
+msgstr "Aucune planification"
msgid "None"
msgstr "Aucun·e"
msgid "Not allowed to merge"
-msgstr "Non autorisé•e à fusionner"
+msgstr "Non autorisé·e à fusionner"
msgid "Not available"
msgstr "Indisponible"
@@ -2871,25 +3096,19 @@ msgid "Not available for protected branches"
msgstr "Non disponible pour les branches protégées"
msgid "Not confidential"
-msgstr "Pas confidentiel•le"
+msgstr "Pas confidentiel·le"
msgid "Not enough data"
msgstr "Données insuffisantes"
msgid "Note that the master branch is automatically protected. %{link_to_protected_branches}"
-msgstr "Notez que la branche principale est automatiquement protégée. %{link_to_protected_branches}"
-
-msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow connecting repositories without generating a Personal Access Token."
-msgstr "Remarque : En tant qu’administrateur•rice, vous pouvez configurer %{github_integration_link}, ce qui vous permettra de vous connecter via GitHub et de permettre la connexion de dépôts sans générer de jeton d’accès personnel."
+msgstr "Notez que la branche principale « master » est automatiquement protégée. %{link_to_protected_branches}"
msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
-msgstr "Remarque : En tant qu’administrateur•rice, vous pouvez configurer %{github_integration_link}, ce qui vous permettra de vous connecter via GitHub et de permettre l’importation de dépôts sans générer de jeton d’accès personnel."
-
-msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow connecting repositories without generating a Personal Access Token."
-msgstr "Remarque : Envisagez de demander à votre administrateur•rice GitLab de configurer %{github_integration_link}, ce qui vous permettra de vous connecter via GitHub et de permettre la connexion des dépôts sans générer de jeton d’accès personnel."
+msgstr "Remarque : En tant qu’administra·teur·trice, vous pouvez configurer %{github_integration_link}, ce qui vous permettra de vous connecter via GitHub et de permettre l’importation de dépôts sans générer de jeton d’accès personnel."
msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
-msgstr "Remarque : Envisagez de demander à votre administrateur•rice GitLab de configurer %{github_integration_link}, ce qui vous permettra de vous connecter via GitHub et d’importer des dépôts sans générer de jeton d’accès personnel."
+msgstr "Remarque : Envisagez de demander à votre administra·teur·trice GitLab de configurer %{github_integration_link}, ce qui vous permettra de vous connecter via GitHub et d’importer des dépôts sans générer de jeton d’accès personnel."
msgid "Notification events"
msgstr "Événement de notifications"
@@ -2901,10 +3120,10 @@ msgid "NotificationEvent|Close merge request"
msgstr "Clore la demande de fusion"
msgid "NotificationEvent|Failed pipeline"
-msgstr "Pipeline échoué"
+msgstr "Pipeline en échec"
msgid "NotificationEvent|Merge merge request"
-msgstr "Fusionner le demande de fusion"
+msgstr "Fusionner la demande de fusion"
msgid "NotificationEvent|New issue"
msgstr "Nouveau ticket"
@@ -2922,7 +3141,7 @@ msgid "NotificationEvent|Reassign merge request"
msgstr "Réassigner la demande de fusion"
msgid "NotificationEvent|Reopen issue"
-msgstr "Ré-ouvrir le ticket"
+msgstr "Rouvrir le ticket"
msgid "NotificationEvent|Successful pipeline"
msgstr "Pipeline réussi"
@@ -2937,13 +3156,13 @@ msgid "NotificationLevel|Global"
msgstr "Global"
msgid "NotificationLevel|On mention"
-msgstr "En cas de mention"
+msgstr "En cas de citation"
msgid "NotificationLevel|Participate"
-msgstr "Participation"
+msgstr "Participer"
msgid "NotificationLevel|Watch"
-msgstr "Surveillé"
+msgstr "Surveiller"
msgid "Notifications"
msgstr "Notifications"
@@ -2955,40 +3174,34 @@ msgid "Notifications on"
msgstr "Notifications activées"
msgid "Nov"
-msgstr "Nov."
+msgstr "nov."
msgid "November"
-msgstr "Novembre"
+msgstr "novembre"
msgid "Number of access attempts"
-msgstr "Nombre de tentatives d'accès"
-
-msgid "OK"
-msgstr "OK"
+msgstr "Nombre de tentatives d’accès"
msgid "Oct"
-msgstr "Oct."
+msgstr "oct."
msgid "October"
-msgstr "Octobre"
+msgstr "octobre"
msgid "OfSearchInADropdown|Filter"
msgstr "Filtre"
-msgid "Once imported, repositories can be mirrored over SSH. Read more %{ssh_link}"
-msgstr "Une fois importés, les dépôts peuvent être mis en miroir via SSH. Lire plus %{ssh_link}"
-
msgid "Online IDE integration settings."
-msgstr ""
+msgstr "Paramètres d’intégration de l’EDI en ligne."
+
+msgid "Only comments from the following commit are shown below"
+msgstr "Seuls les commentaires du commit suivant sont affichés ciâ€dessous"
msgid "Only project members can comment."
msgstr "Seuls les membres du projet peuvent commenter."
-msgid "Open"
-msgstr "Ouvrir"
-
-msgid "Opened"
-msgstr "Ouvert"
+msgid "Open in Xcode"
+msgstr "Ouvrir dans Xcode"
msgid "OpenedNDaysAgo|Opened"
msgstr "Ouvert"
@@ -2996,17 +3209,26 @@ msgstr "Ouvert"
msgid "Opens in a new window"
msgstr "Ouvrir dans une nouvelle fenêtre"
+msgid "Operations"
+msgstr "Opérations"
+
msgid "Options"
-msgstr "Paramètres"
+msgstr "Options"
+
+msgid "Or you can choose one of the suggested colors below"
+msgstr "Ou vous pouvez choisir l’une des couleurs suggérées ciâ€dessous"
+
+msgid "Other Labels"
+msgstr "Autres étiquettes"
msgid "Otherwise it is recommended you start with one of the options below."
-msgstr "Sinon, il est recommandé de commencer avec l’une des options ci-dessous."
+msgstr "Sinon, il est recommandé de commencer avec l’une des options ciâ€dessous."
msgid "Outbound requests"
-msgstr ""
+msgstr "Requêtes sortantes"
msgid "Overview"
-msgstr "Vue d'ensemble"
+msgstr "Vue d’ensemble"
msgid "Owner"
msgstr "Propriétaire"
@@ -3027,16 +3249,31 @@ msgid "Pagination|« First"
msgstr "« Première"
msgid "Part of merge request changes"
-msgstr ""
+msgstr "Partie des modifications de la demande de fusion"
msgid "Password"
msgstr "Mot de Passe"
+msgid "Paste your public SSH key, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'. Don't use your private SSH key."
+msgstr "Collez votre clef SSH publique, qui est habituellement située dans le fichier « ~/.ssh/id_rsa.pub » et commence par « ssh-rsa ». N’utilisez pas votre clef SSH privée !"
+
+msgid "Pause"
+msgstr "Pause"
+
msgid "Pending"
msgstr "En attente"
+msgid "Per job. If a job passes this threshold, it will be marked as failed"
+msgstr "Par tâche. Si une tâche dépasse ce seuil, elle sera marquée comme ayant échoué"
+
+msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgstr "Effectuer des actions avancées telles que la modification du chemin d’accès, le transfert ou la suppression du groupe."
+
msgid "Performance optimization"
-msgstr ""
+msgstr "Optimisation des performances"
+
+msgid "Permissions"
+msgstr "Droits d’accès"
msgid "Personal Access Token"
msgstr "Jeton d’accès personnel"
@@ -3045,28 +3282,28 @@ msgid "Pipeline"
msgstr "Pipeline"
msgid "Pipeline Health"
-msgstr "Santé du Pipeline"
+msgstr "État de santé du pipeline"
msgid "Pipeline Schedule"
-msgstr "Programmation de pipeline"
+msgstr "Planification de pipeline"
msgid "Pipeline Schedules"
-msgstr "Programmations de pipeline"
+msgstr "Planifications de pipelines"
-msgid "Pipeline quota"
-msgstr "Quota de pipeline"
+msgid "Pipeline triggers"
+msgstr "Déclencheurs de pipeline"
msgid "PipelineCharts|Failed:"
-msgstr "Échecs : "
+msgstr "Échecs :"
msgid "PipelineCharts|Overall statistics"
msgstr "Statistiques générales"
msgid "PipelineCharts|Success ratio:"
-msgstr "Ratio de succès : "
+msgstr "Taux de réussite :"
msgid "PipelineCharts|Successful:"
-msgstr "Succès :"
+msgstr "Réussites :"
msgid "PipelineCharts|Total:"
msgstr "Total :"
@@ -3087,7 +3324,7 @@ msgid "PipelineSchedules|Next Run"
msgstr "Prochaine exécution"
msgid "PipelineSchedules|None"
-msgstr "Aucune"
+msgstr "Aucun"
msgid "PipelineSchedules|Provide a short description for this pipeline"
msgstr "Indiquez une courte description"
@@ -3108,10 +3345,10 @@ msgid "Pipelines"
msgstr "Pipelines"
msgid "Pipelines charts"
-msgstr "Graphique des pipelines"
+msgstr "Graphiques des pipelines"
msgid "Pipelines for last month"
-msgstr "Pipelines du dernier mois"
+msgstr "Pipelines du mois dernier"
msgid "Pipelines for last week"
msgstr "Pipelines de la semaine dernière"
@@ -3126,19 +3363,19 @@ msgid "Pipelines|CI Lint"
msgstr "CI Lint"
msgid "Pipelines|Clear Runner Caches"
-msgstr "Vider les caches des Exécuteurs"
+msgstr "Vider les caches des exécuteurs"
msgid "Pipelines|Get started with Pipelines"
-msgstr "Premiers pas avec les Pipelines"
+msgstr "Premiers pas avec les pipelines"
msgid "Pipelines|Loading Pipelines"
msgstr "Chargement des pipelines"
msgid "Pipelines|Project cache successfully reset."
-msgstr "Réinitialisation réussie du cache de projet."
+msgstr "Réinitialisation du cache de projet réussie."
msgid "Pipelines|Run Pipeline"
-msgstr "Éxécuter un pipeline"
+msgstr "Exécuter un pipeline"
msgid "Pipelines|Something went wrong while cleaning runners cache."
msgstr "Une erreur s’est produite lors du nettoyage du cache des exécuteurs."
@@ -3147,88 +3384,124 @@ msgid "Pipelines|There are currently no %{scope} pipelines."
msgstr "Il n’y a actuellement pas de pipelines %{scope}."
msgid "Pipelines|There are currently no pipelines."
-msgstr "Il n’y a actuellement pas de pipelines."
+msgstr "Il n’y a actuellement aucun pipeline."
msgid "Pipelines|This project is not currently set up to run pipelines."
msgstr "Ce projet n’est actuellement pas configuré pour exécuter des pipelines."
-msgid "Pipeline|Retry pipeline"
-msgstr "Relancer le pipeline"
+msgid "Pipeline|Create for"
+msgstr "Créer pour"
+
+msgid "Pipeline|Create pipeline"
+msgstr "Créer un pipeline"
+
+msgid "Pipeline|Existing branch name or tag"
+msgstr "Nom de branche ou d’étiquette existant"
+
+msgid "Pipeline|Run Pipeline"
+msgstr "Exécuter le pipeline"
+
+msgid "Pipeline|Search branches"
+msgstr "Chercher des branches"
-msgid "Pipeline|Retry pipeline #%{pipelineId}?"
-msgstr "Relancer le pipeline #%{pipelineId} ?"
+msgid "Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default."
+msgstr "Spécifier la valeur de la variable à utiliser lors de l’exécution. Les valeurs spécifiées dans %{settings_link} seront utilisées par défaut."
msgid "Pipeline|Stop pipeline"
msgstr "Arrêter le pipeline"
msgid "Pipeline|Stop pipeline #%{pipelineId}?"
-msgstr "Arrêter le pipeline #%{pipelineId} ?"
+msgstr "Arrêter le pipeline numéro %{pipelineId} ?"
-msgid "Pipeline|You’re about to retry pipeline %{pipelineId}."
-msgstr "Vous êtes sur le point de relancer le pipeline %{pipelineId}."
+msgid "Pipeline|Variables"
+msgstr "Variables"
msgid "Pipeline|You’re about to stop pipeline %{pipelineId}."
msgstr "Vous êtes sur le point d’arrêter le pipeline %{pipelineId}."
msgid "Pipeline|all"
-msgstr "Tous"
+msgstr "tous"
msgid "Pipeline|success"
-msgstr "Succès"
+msgstr "réussi"
msgid "Pipeline|with stage"
-msgstr "avec l'étape"
+msgstr "avec l’étape"
msgid "Pipeline|with stages"
msgstr "avec les étapes"
+msgid "Plain diff"
+msgstr "Diff brut"
+
msgid "PlantUML"
-msgstr ""
+msgstr "PlantUML"
msgid "Play"
msgstr "Lancer"
-msgid "Please <a href=%{link_to_billing} target=\"_blank\" rel=\"noopener noreferrer\">enable billing for one of your projects to be able to create a Kubernetes cluster</a>, then try again."
-msgstr "Merci d’<a href=%{link_to_billing} target=\"_blank\" rel=\"noopener noreferrer\">activer la facturation pour un de vos projets afin d’être en mesure de créer un cluster Kubernetes</a>, puis essayez à nouveau."
+msgid "Please accept the Terms of Service before continuing."
+msgstr "Veuillez accepter les conditions générales d’utilisation avant de continuer."
+
+msgid "Please select at least one filter to see results"
+msgstr "Veuillez sélectionner au moins un filtre pour voir les résultats"
msgid "Please solve the reCAPTCHA"
msgstr "Veuillez résoudre le reCAPTCHA"
-msgid "Please wait while we connect to your repository. Refresh at will."
-msgstr "Veuillez patienter pendant que nous nous connectons à votre dépôt. Actualisez à volonté."
+msgid "Please try again"
+msgstr "Veuillez réessayer"
msgid "Please wait while we import the repository for you. Refresh at will."
-msgstr "Veuillez patienter pendant que nous importons le dépôt pour vous. Actualisez à volonté."
+msgstr "Veuillez patienter pendant l’importation de votre dépôt. Actualisez à votre guise."
msgid "Preferences"
msgstr "Préférences"
-msgid "Primary"
-msgstr "Principal"
+msgid "Preferences|Navigation theme"
+msgstr "Thème de navigation"
+
+msgid "Prioritize"
+msgstr "Prioriser"
+
+msgid "Prioritize label"
+msgstr "Prioriser l’étiquette"
+
+msgid "Prioritized Labels"
+msgstr "Étiquettes prioritaires"
+
+msgid "Prioritized label"
+msgstr "Étiquette prioritaire"
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."
+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."
+msgstr "Privé — le groupe ainsi que ses projets ne sont accessibles qu’à ses membres."
msgid "Private projects can be created in your personal namespace with:"
-msgstr "Des projets privés peuvent être créés dans votre espace de noms personnel avec :"
+msgstr "Des projets privés peuvent être créés dans votre espace de noms personnel avec :"
msgid "Profile"
msgstr "Profil"
msgid "Profiles|Account scheduled for removal."
-msgstr "Compte programmé pour sa suppression."
+msgstr "Compte programmé pour suppression."
+
+msgid "Profiles|Change username"
+msgstr "Changer le nom d’utilisateur·rice"
+
+msgid "Profiles|Current path: %{path}"
+msgstr "Chemin d’accès actuel : %{path}"
msgid "Profiles|Delete Account"
-msgstr "Supprimer le compte"
+msgstr "Supprimer un compte"
msgid "Profiles|Delete account"
msgstr "Supprimer le compte"
msgid "Profiles|Delete your account?"
-msgstr "Supprimer votre compte ?"
+msgstr "Supprimer votre compte ?"
msgid "Profiles|Deleting an account has the following effects:"
msgstr "Supprimer un compte aura les conséquences suivantes :"
@@ -3239,9 +3512,21 @@ msgstr "Mot de passe incorrect"
msgid "Profiles|Invalid username"
msgstr "Nom d’utilisateur incorrect"
+msgid "Profiles|Path"
+msgstr "Chemin d’accès"
+
msgid "Profiles|Type your %{confirmationValue} to confirm:"
msgstr "Saisissez votre %{confirmationValue} pour confirmer :"
+msgid "Profiles|Update username"
+msgstr "Mettre à jour le nom d’utilisateur·rice"
+
+msgid "Profiles|Username change failed - %{message}"
+msgstr "Le changement de nom d’utilisateur·rice a échoué : %{message}"
+
+msgid "Profiles|Username successfully changed"
+msgstr "Changement de nom d’utilisa·teur·trice effectué"
+
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."
@@ -3255,22 +3540,31 @@ msgid "Profiles|your account"
msgstr "votre compte"
msgid "Profiling - Performance bar"
-msgstr ""
+msgstr "Profilage — barre de performance"
msgid "Programming languages used in this repository"
msgstr "Langages de programmation utilisés dans ce dépôt"
+msgid "Progress"
+msgstr "Progression"
+
+msgid "Project"
+msgstr "Projet"
+
msgid "Project '%{project_name}' is in the process of being deleted."
-msgstr "Le projet “%{project_name}†est en train d’être supprimé."
+msgstr "Le projet « %{project_name} » est en cours de suppression."
msgid "Project '%{project_name}' queued for deletion."
-msgstr "Projet '%{project_name}' en attente de suppression."
+msgstr "Projet « %{project_name} » en attente de suppression."
msgid "Project '%{project_name}' was successfully created."
-msgstr "Projet '%{project_name}' créé avec succès."
+msgstr "Création du projet « %{project_name} » effectuée."
msgid "Project '%{project_name}' was successfully updated."
-msgstr "Projet '%{project_name}' mis à jour avec succès."
+msgstr "Mise à jour du projet « %{project_name} » effectuée."
+
+msgid "Project Badges"
+msgstr "Badges de projet"
msgid "Project access must be granted explicitly to each user."
msgstr "L’accès au projet doit être explicitement accordé à chaque utilisateur."
@@ -3279,50 +3573,26 @@ msgid "Project avatar"
msgstr "Avatar du projet"
msgid "Project avatar in repository: %{link}"
-msgstr "Avatar du project dans le dépôt : %{link}"
+msgstr "Avatar du projet dans le dépôt : %{link}"
msgid "Project details"
msgstr "Détails du projet"
msgid "Project export could not be deleted."
-msgstr "L'export du projet n'a pas pu être supprimé."
+msgstr "L’exportation du projet n’a pas pu être supprimée."
msgid "Project export has been deleted."
-msgstr "L'export du projet a été supprimé."
+msgstr "L’exportation du projet a été supprimée."
msgid "Project export link has expired. Please generate a new export from your project settings."
-msgstr "Le lien de l’export du projet a expiré. Merci de générer un nouvel export depuis les paramètres du projet."
+msgstr "Le lien de l’exportation du projet a expiré. Merci de générer une nouvelle exportation depuis les paramètres du projet."
msgid "Project export started. A download link will be sent by email."
-msgstr "L'export du projet a débuté. Un lien de téléchargement sera envoyé par courriel."
+msgstr "L’exportation du projet a débuté. Un lien de téléchargement sera envoyé par courriel."
msgid "ProjectActivityRSS|Subscribe"
msgstr "S’abonner"
-msgid "ProjectCreationLevel|Allowed to create projects"
-msgstr "Autorisé•e à créer des projets"
-
-msgid "ProjectCreationLevel|Default project creation protection"
-msgstr "Protection de création de projet par défaut"
-
-msgid "ProjectCreationLevel|Developers + Masters"
-msgstr "Développeur•euse•s + Expert•e•s"
-
-msgid "ProjectCreationLevel|Masters"
-msgstr "Expert•e•s"
-
-msgid "ProjectCreationLevel|No one"
-msgstr "Personne"
-
-msgid "ProjectFeature|Disabled"
-msgstr "Désactivé"
-
-msgid "ProjectFeature|Everyone with access"
-msgstr "Toute personne ayant accès"
-
-msgid "ProjectFeature|Only team members"
-msgstr "Seulement les membres de l'équipe"
-
msgid "ProjectFileTree|Name"
msgstr "Nom"
@@ -3332,32 +3602,11 @@ msgstr "Jamais"
msgid "ProjectLifecycle|Stage"
msgstr "Étape"
-msgid "ProjectNetworkGraph|Graph"
-msgstr "Graphes"
-
-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 commits signés peuvent être poussés 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•e administrat•eur•rice ne le modifie."
-
-msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails."
-msgstr "Les utilisateurs peuvent uniquement pousser sur ce dépôt des commits qui ont été validés avec une de leurs adresses courriels vérifiées."
-
msgid "Projects"
msgstr "Projets"
msgid "ProjectsDropdown|Frequently visited"
-msgstr "Fréquemment visité"
+msgstr "Les plus consultés"
msgid "ProjectsDropdown|Loading projects"
msgstr "Chargement des projets"
@@ -3375,7 +3624,10 @@ msgid "ProjectsDropdown|Sorry, no projects matched your search"
msgstr "Désolé, aucun projet ne correspond à votre recherche"
msgid "ProjectsDropdown|This feature requires browser localStorage support"
-msgstr "Cette fonctionnalité requiert le support du localStorage par votre navigateur"
+msgstr "Cette fonctionnalité requiert la prise en charge du localStorage par votre navigateur"
+
+msgid "PrometheusDashboard|Time"
+msgstr "Heure"
msgid "PrometheusService|%{exporters} with %{metrics} were found"
msgstr "%{exporters} avec %{metrics} ont été trouvés"
@@ -3390,31 +3642,22 @@ msgid "PrometheusService|Auto configuration"
msgstr "Configuration automatique"
msgid "PrometheusService|Automatically deploy and configure Prometheus on your clusters to monitor your project’s environments"
-msgstr "Déployer et configurer automatiquement Prometheus sur vos clusters pour surveiller les environnements de vos projets"
+msgstr "Déployer et configurer automatiquement Prometheus sur vos grappes de serveurs pour surveiller les environnements de vos projets"
msgid "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."
-msgstr "Par défaut, Prometheus écoute sur 'http://localhost:9090'. Il n’est pas recommandé de changer l’adresse et le port par défaut car cela pourrait affecter ou entrer en conflit avec d'autres services fonctionnant sur le serveur GitLab."
+msgstr "Par défaut, Prometheus écoute sur « http://localhost:9090 ». Il n’est pas recommandé de changer l’adresse et le port par défaut car cela pourrait affecter ou entrer en conflit avec d’autres services fonctionnant sur le serveur GitLab."
msgid "PrometheusService|Common metrics"
msgstr "Métriques communes"
-msgid "PrometheusService|Common metrics are automatically monitored based on a library of metrics from popular exporters."
-msgstr "Les métriques communes sont automatiquement surveillées en fonction d’une bibliothèque de métriques provenant d’exportateurs populaires."
-
-msgid "PrometheusService|Custom metrics"
-msgstr "Métriques personnalisés"
-
msgid "PrometheusService|Finding and configuring metrics..."
msgstr "Recherche et configuration des métriques en cours…"
-msgid "PrometheusService|Finding custom metrics..."
-msgstr "Recherche de métriques personnalisées…"
-
msgid "PrometheusService|Install Prometheus on clusters"
-msgstr "Installer Prometheus sur les clusters"
+msgstr "Installer Prometheus sur les grappes de serveurs"
msgid "PrometheusService|Manage clusters"
-msgstr "Gérer les clusters"
+msgstr "Gérer les grappes de serveurs"
msgid "PrometheusService|Manual configuration"
msgstr "Configuration manuelle"
@@ -3422,32 +3665,29 @@ msgstr "Configuration manuelle"
msgid "PrometheusService|Metrics"
msgstr "Métriques"
+msgid "PrometheusService|Metrics are automatically configured and monitored based on a library of metrics from popular exporters."
+msgstr "Les métriques sont automatiquement configurées et supervisées en fonction d’une bibliothèque de métriques provenant d’exportateurs populaires."
+
msgid "PrometheusService|Missing environment variable"
msgstr "Variable d’environnement manquante"
msgid "PrometheusService|More information"
msgstr "Plus d’informations"
-msgid "PrometheusService|New metric"
-msgstr "Nouvelle métrique"
-
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
-msgstr "URL de base de l’API Prometheus, comme http://prometheus.example.com/"
+msgstr "URL de base de l’API Prometheus, telle que http://prometheus.example.com/"
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
-msgstr "Prometheus est géré automatiquement sur vos clusters"
-
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
-msgstr "Ces métriques ne seront surveillés qu’après votre premier déploiement dans un environnement"
+msgstr "Prometheus est géré automatiquement sur vos grappes de serveurs"
msgid "PrometheusService|Time-series monitoring service"
-msgstr "Service de surveillance de séries temporelles"
+msgstr "Service de supervision de séries temporelles"
msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
-msgstr "Pour activer la configuration manuelle, désinstallez Prometheus de vos clusters"
+msgstr "Pour activer la configuration manuelle, désinstallez Prometheus de vos grappes de serveurs"
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
-msgstr "Pour activer l’installation de Prometheus sur vos clusters, désactivez la configuration manuelle ci-dessous"
+msgstr "Pour activer l’installation de Prometheus sur vos grappes de serveurs, désactivez la configuration manuelle ciâ€dessous"
msgid "PrometheusService|Waiting for your first deployment to an environment to find common metrics"
msgstr "En attente de votre premier déploiement dans un environnement pour trouver des métriques communes"
@@ -3455,26 +3695,32 @@ msgstr "En attente de votre premier déploiement dans un environnement pour trou
msgid "Promote"
msgstr "Promouvoir"
-msgid "Promote to Group Label"
-msgstr "Promouvoir en label de groupe"
+msgid "Promote these project milestones into a group milestone."
+msgstr "Promouvoir ces jalons de projets en jalon de groupe."
msgid "Promote to Group Milestone"
msgstr "Promouvoir en jalon de groupe"
+msgid "Promote to group label"
+msgstr "Promouvoir en tant qu’étiquette de groupe"
+
msgid "Protip:"
-msgstr "Astuce :"
+msgstr "Astuce :"
+
+msgid "Provider"
+msgstr "Fournisseur"
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."
+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."
+msgstr "Public - le projet est accessible sans aucune authentification."
-msgid "Push Rules"
-msgstr "Règles de poussée"
+msgid "Public pipelines"
+msgstr "Pipelines publics"
msgid "Push events"
-msgstr "Évènements de poussée"
+msgstr "Événements de poussée"
msgid "Push project from command line"
msgstr "Pousser le projet en ligne de commande"
@@ -3482,12 +3728,12 @@ msgstr "Pousser le projet en ligne de commande"
msgid "Push to create a project"
msgstr "Pousser pour créer un projet"
-msgid "PushRule|Committer restriction"
-msgstr "Restriction du validateur"
-
msgid "Quick actions can be used in the issues description and comment boxes."
msgstr "Les actions rapides peuvent être utilisées dans la description des tickets et dans les zones de commentaire."
+msgid "Re-deploy"
+msgstr "Redéployer"
+
msgid "Read more"
msgstr "Lire plus"
@@ -3495,20 +3741,20 @@ msgid "Readme"
msgstr "LisezMoi"
msgid "Real-time features"
-msgstr ""
-
-msgid "RefSwitcher|Branches"
-msgstr "Branches"
-
-msgid "RefSwitcher|Tags"
-msgstr "Tags"
+msgstr "Fonctionnalités en temps réel"
msgid "Reference:"
-msgstr "Référence :"
+msgstr "Référence :"
msgid "Register / Sign In"
msgstr "Inscription / Connexion"
+msgid "Register and see your runners for this group."
+msgstr "Identifiezâ€vous et accédez à vos exécuteurs pour ce groupe."
+
+msgid "Register and see your runners for this project."
+msgstr "Enregistrer et afficher vos exécuteurs pour ce projet."
+
msgid "Registry"
msgstr "Registre"
@@ -3516,7 +3762,7 @@ msgid "Related Commits"
msgstr "Commits liés"
msgid "Related Deployed Jobs"
-msgstr "Tâches de déploiement liées"
+msgstr "Tâches déployées liées"
msgid "Related Issues"
msgstr "Tickets liés"
@@ -3539,35 +3785,38 @@ msgstr "Me le rappeler ultérieurement"
msgid "Remove"
msgstr "Supprimer"
+msgid "Remove Runner"
+msgstr "Supprimer l’exécuteur"
+
msgid "Remove avatar"
msgstr "Supprimer l’avatar"
+msgid "Remove priority"
+msgstr "Supprimer la priorité"
+
msgid "Remove project"
msgstr "Supprimer le projet"
-msgid "Repair authentication"
-msgstr "Réparer l’authentification"
-
-msgid "Repo by URL"
-msgstr "Dépôt par URL"
-
msgid "Repository"
msgstr "Dépôt"
-msgid "Repository has no locks."
-msgstr "Le dépôt n’a pas de verrous."
+msgid "Repository Settings"
+msgstr "Paramètres du dépôt"
msgid "Repository maintenance"
-msgstr ""
+msgstr "Maintenance du dépôt"
-msgid "Repository mirror settings"
-msgstr ""
+msgid "Repository mirror"
+msgstr "Miroir du dépôt"
msgid "Repository storage"
-msgstr ""
+msgstr "Stockage du dépôt"
msgid "Request Access"
-msgstr "Demander l'accès"
+msgstr "Demander l’accès"
+
+msgid "Require all users to accept Terms of Service and Privacy Policy when they access GitLab."
+msgstr "Exiger que tous les utilisateurs acceptent les conditions générales d’utilisation et la politique de confidentialité quand ils accèdent à GitLab."
msgid "Reset git storage health information"
msgstr "Réinitialiser les informations de santé du stockage Git"
@@ -3578,11 +3827,26 @@ 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 exécuteurs"
+msgid "Resolve all discussions in new issue"
+msgstr "Résoudre toutes les discussions dans un nouveau ticket"
+
+msgid "Resolve conflicts on source branch"
+msgstr "Résoudre les conflits sur la branche source"
+
msgid "Resolve discussion"
msgstr "Résoudre la discussion"
-msgid "Response"
-msgstr "Réponse"
+msgid "Resume"
+msgstr "Reprendre"
+
+msgid "Retry"
+msgstr "Réessayer"
+
+msgid "Retry this job"
+msgstr "Relancer cette tâche"
+
+msgid "Retry verification"
+msgstr "Relancer la vérification"
msgid "Reveal value"
msgid_plural "Reveal values"
@@ -3595,119 +3859,146 @@ msgstr "Défaire ce commit"
msgid "Revert this merge request"
msgstr "Défaire cette demande de fusion"
-msgid "Review the process for configuring service providers in your identity provider — in this case, GitLab is the \"service provider\" or \"relying party\"."
-msgstr ""
+msgid "Review"
+msgstr "Examiner"
msgid "Reviewing"
-msgstr "Examiner"
+msgstr "Examen"
msgid "Reviewing (merge request !%{mergeRequestId})"
-msgstr ""
+msgstr "Examen (demande de fusion !%{mergeRequestId})"
-msgid "Roadmap"
-msgstr "Feuille de route"
+msgid "Rollback"
+msgstr "Restaurer (rollback)"
-msgid "Run CI/CD pipelines for external repositories"
-msgstr "Exécuter des pipelines CI / CD pour les dépôts externes"
+msgid "Runner token"
+msgstr "Jeton de l’exécuteur"
msgid "Runners"
msgstr "Exécuteurs"
-msgid "Running"
-msgstr "En cours"
-
-msgid "SAML Single Sign On"
-msgstr ""
+msgid "Runners API"
+msgstr "API des exécuteurs"
-msgid "SAML Single Sign On Settings"
-msgstr ""
+msgid "Runners can be placed on separate users, servers, and even on your local machine."
+msgstr "Les exécuteurs peuvent être placés sur différents utilisateurs et serveurs, voire sur votre machine locale."
-msgid "SHA1 fingerprint of the SAML token signing certificate. Get this from your identity provider, where it can also be called \"Thumbprint\"."
-msgstr ""
+msgid "Running"
+msgstr "En cours d’exécution"
msgid "SSH Keys"
-msgstr "Clés SSH"
+msgstr "Clefs SSH"
+
+msgid "SSL Verification"
+msgstr "Vérification SSL"
+
+msgid "Save"
+msgstr "Enregistrer"
msgid "Save changes"
msgstr "Enregistrer les modifications"
msgid "Save pipeline schedule"
-msgstr "Sauvegarder le pipeline programmé"
+msgstr "Sauvegarder la planification du pipeline"
msgid "Save variables"
msgstr "Enregistrer les variables"
msgid "Schedule a new pipeline"
-msgstr "Programmer un nouveau pipeline"
+msgstr "Planifier un nouveau pipeline"
msgid "Scheduled"
msgstr "Planifié"
msgid "Schedules"
-msgstr "Programmes"
+msgstr "Planifications"
msgid "Scheduling Pipelines"
-msgstr "Programmer des pipelines"
+msgstr "Planification des pipelines"
-msgid "Scoped issue boards"
-msgstr "Tableaux de tickets avec portée limitée"
+msgid "Scroll to bottom"
+msgstr "Faire défiler vers le bas"
+
+msgid "Scroll to top"
+msgstr "Faire défiler vers le haut"
msgid "Search"
msgstr "Rechercher"
+msgid "Search branches"
+msgstr "Rechercher des branches"
+
msgid "Search branches and tags"
msgstr "Rechercher dans les branches et les étiquettes"
+msgid "Search files"
+msgstr "Rechercher des fichiers"
+
+msgid "Search for projects, issues, etc."
+msgstr "Rechercher des projets, des tickets, etc."
+
+msgid "Search merge requests"
+msgstr "Rechercher des demandes de fusion"
+
msgid "Search milestones"
-msgstr "Recherche de jalons"
+msgstr "Rechercher des jalons"
msgid "Search project"
-msgstr "Recherche de projets"
+msgstr "Rechercher des projets"
msgid "Search users"
-msgstr "Recherche d’utilisateur•rice•s"
+msgstr "Rechercher des utilisa·teur·trice·s"
msgid "Seconds before reseting failure information"
-msgstr "Nombre de secondes avant de réinitialiser les informations d’échec"
+msgstr "Nombre de secondes avant réinitialisation des informations d’échec"
msgid "Seconds to wait for a storage access attempt"
-msgstr "Nombre de secondes d’attente pour un essai d'accès au stockage"
-
-msgid "Secret variables"
-msgstr "Variables secrètes"
+msgstr "Nombre de secondes d’attente avant une tentative d’accès au stockage"
-msgid "Security report"
-msgstr "Rapport de sécurité"
+msgid "Select"
+msgstr "Sélectionner"
msgid "Select Archive Format"
-msgstr "Sélectionnez le format de l'archive"
+msgstr "Sélectionnez le format de l’archive"
+
+msgid "Select a namespace to fork the project"
+msgstr "Sélectionnez un espace de noms afin de créer une divergence du projet"
msgid "Select a timezone"
msgstr "Sélectionnez un fuseau horaire"
msgid "Select an existing Kubernetes cluster or create a new one"
-msgstr "Sélectionnez un cluster Kubernetes existant ou créez-en un nouveau"
+msgstr "Sélectionnez une grappe de serveurs Kubernetes existante ou créezâ€en une nouvelle"
msgid "Select assignee"
msgstr "Sélectionner une personne assignée"
msgid "Select branch/tag"
-msgstr "Sélectionnez une branche / un tag"
+msgstr "Sélectionner une branche ou une étiquette"
-msgid "Select target branch"
-msgstr "Sélectionnez une branche cible"
+msgid "Select project"
+msgstr "Sélectionner un projet"
+
+msgid "Select project and zone to choose machine type"
+msgstr "Sélectionnez le projet et la zone afin de choisir le type de machine"
-msgid "Selective synchronization"
-msgstr "Synchronisation sélective"
+msgid "Select project to choose zone"
+msgstr "Sélectionnez le projet afin de choisir la zone"
+
+msgid "Select source branch"
+msgstr "Sélectionner une branche source"
+
+msgid "Select target branch"
+msgstr "Sélectionner une branche cible"
msgid "Send email"
msgstr "Envoyer un courriel"
msgid "Sep"
-msgstr "Sept."
+msgstr "sept."
msgid "September"
-msgstr "Septembre"
+msgstr "septembre"
msgid "Server version"
msgstr "Version du serveur"
@@ -3715,35 +4006,29 @@ msgstr "Version du serveur"
msgid "Service Templates"
msgstr "Modèles de service"
-msgid "Service URL"
-msgstr "URL du service"
-
msgid "Session expiration, projects limit and attachment size."
-msgstr "Expiration de la session, limite des projets et taille des pièces jointes."
+msgstr "Expiration de la session, restrictions des projets et taille des pièces jointes."
msgid "Set a password on your account to pull or push via %{protocol}."
-msgstr "Définissez un mot de passe pour votre compte pour pouvoir tirer ou pousser par %{protocol}."
+msgstr "Définissez un mot de passe pour votre compte afin de pouvoir récupérer ou pousser vos modification via %{protocol}."
msgid "Set default and restrict visibility levels. Configure import sources and git access protocol."
-msgstr "Définir les valeurs par défaut et limiter les niveaux de visibilité. Configurer les sources d’importation et le protocole d’accès git."
+msgstr "Définissez les valeurs par défaut et restreignez les niveaux de visibilité. Configurez les sources d’importation et le protocole d’accès pour Git."
msgid "Set max session time for web terminal."
-msgstr ""
+msgstr "Définissez le temps maximal de la session pour le terminal Web."
msgid "Set notification email for abuse reports."
-msgstr ""
+msgstr "Définissez un courriel de notification pour les rapports d’abus."
msgid "Set requirements for a user to sign-in. Enable mandatory two-factor authentication."
-msgstr "Définir les exigences pour la connexion d’un utilisateur. Activer l’authentification obligatoire à deux facteurs."
+msgstr "Définissez les exigences pour la connexion d’un utilisateur. Activez l’authentification obligatoire à deux facteurs."
msgid "Set up CI/CD"
-msgstr "Configurer CI/CD"
+msgstr "Configuration CI/CD"
msgid "Set up Koding"
-msgstr "Mettre en place Koding"
-
-msgid "Set up assertions/attributes/claims (email, first_name, last_name) and NameID according to %{docsLinkStart}the documentation %{icon}%{docsLinkEnd}"
-msgstr ""
+msgstr "Configurer Koding"
msgid "SetPasswordToCloneLink|set a password"
msgstr "définir un mot de passe"
@@ -3752,45 +4037,45 @@ msgid "Settings"
msgstr "Paramètres"
msgid "Setup a specific Runner automatically"
-msgstr "Configurer un Exécuteur spécifique automatiquement"
-
-msgid "Share the <strong>%{sso_label}</strong> with members so they can sign in to your group through your identity provider"
-msgstr ""
-
-msgid "SharedRunnersMinutesSettings|By resetting the pipeline minutes for this namespace, the currently used minutes will be set to zero."
-msgstr "En réinitialisant les minutes du pipeline pour cet espace de noms, les minutes actuellement utilisées seront remises à zéro."
+msgstr "Configurer automatiquement un exécuteur spécifique"
-msgid "SharedRunnersMinutesSettings|Reset pipeline minutes"
-msgstr "Réinitialiser les minutes du pipeline"
+msgid "Share"
+msgstr "Partager"
-msgid "SharedRunnersMinutesSettings|Reset used pipeline minutes"
-msgstr "Réinitialiser les minutes de pipeline utilisées"
+msgid "Shared Runners"
+msgstr "Exécuteurs partagés"
msgid "Show command"
msgstr "Afficher la commande"
+msgid "Show complete raw log"
+msgstr "Afficher le journal brut complet"
+
+msgid "Show latest version"
+msgstr "Afficher la dernière version"
+
+msgid "Show latest version of the diff"
+msgstr "Afficher la dernière version du diff"
+
msgid "Show parent pages"
msgstr "Afficher les pages parentes"
msgid "Show parent subgroups"
-msgstr "Afficher les sous-groupes parents"
+msgstr "Afficher les sousâ€groupes parents"
+
+msgid "Show whitespace changes"
+msgstr "Afficher les modifications des espaces"
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "Affichage de %d évènement"
-msgstr[1] "Affichage de %d évènements"
-
-msgid "Sidebar|Change weight"
-msgstr "Changer le poids"
-
-msgid "Sidebar|No"
-msgstr "Non"
+msgstr[1] "Affichage de %d événements"
-msgid "Sidebar|None"
-msgstr "Aucun"
+msgid "Side-by-side"
+msgstr "côte à côte"
-msgid "Sidebar|Weight"
-msgstr "Poids"
+msgid "Sign out"
+msgstr "Se déconnecter"
msgid "Sign-in restrictions"
msgstr "Restrictions de connexion"
@@ -3799,10 +4084,10 @@ msgid "Sign-up restrictions"
msgstr "Restrictions d’inscription"
msgid "Size and domain settings for static websites"
-msgstr "Paramètres de taille et de domaine pour les sites web statiques"
+msgstr "Paramètres de taille et de domaine pour les sites Web statiques"
-msgid "Slack application"
-msgstr ""
+msgid "Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job"
+msgstr "Plus lent, mais permet de s’assurer que l’espace de travail du projet est vierge, comme il clone le dépôt à partir de zéro pour chaque tâche"
msgid "Snippets"
msgstr "Extraits de code"
@@ -3813,20 +4098,26 @@ msgstr "Une erreur est survenue de notre côté"
msgid "Something went wrong on our end."
msgstr "Une erreur est survenue de notre côté."
+msgid "Something went wrong on our end. Please try again!"
+msgstr "Quelque chose s’est mal passé de notre côté. Veuillez réessayer."
+
msgid "Something went wrong when toggling the button"
msgstr "Une erreur s’est produite lors du basculement du bouton"
-msgid "Something went wrong while fetching Dependency Scanning."
-msgstr "Une erreur s’est produite lors de la recherche des dépendances."
-
-msgid "Something went wrong while fetching SAST."
-msgstr "Une erreur s’est produite lors de la récupération de la SAST."
+msgid "Something went wrong while closing the %{issuable}. Please try again later"
+msgstr "Une erreur s’est produite lors de la fermeture du / de la %{issuable}. Veuillez réessayer plus tard"
msgid "Something went wrong while fetching the projects."
-msgstr "Une erreur s'est produite lors de la récupération des projets."
+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."
+msgstr "Une erreur s’est produite lors de la récupération de la liste du registre."
+
+msgid "Something went wrong while reopening the %{issuable}. Please try again later"
+msgstr ""
+
+msgid "Something went wrong while resolving this discussion. Please try again."
+msgstr ""
msgid "Something went wrong. Please try again."
msgstr "Quelque chose s’est mal passé. Veuillez réessayer."
@@ -3838,13 +4129,13 @@ msgid "SortOptions|Access level, ascending"
msgstr "Niveau d’accès, croissant"
msgid "SortOptions|Access level, descending"
-msgstr "Niveau d’accès, decroissant"
+msgstr "Niveau d’accès décroissant"
msgid "SortOptions|Created date"
msgstr "Date de création"
msgid "SortOptions|Due date"
-msgstr "Date d'échéance"
+msgstr "Date d’échéance"
msgid "SortOptions|Due later"
msgstr "Échéance lointaine"
@@ -3853,7 +4144,7 @@ msgid "SortOptions|Due soon"
msgstr "Échéance proche"
msgid "SortOptions|Label priority"
-msgstr "Priorité du label"
+msgstr "Priorité de l’étiquette"
msgid "SortOptions|Largest group"
msgstr "Taille de groupe"
@@ -3865,16 +4156,13 @@ msgid "SortOptions|Last created"
msgstr "Créé récemment"
msgid "SortOptions|Last joined"
-msgstr "Rejoint récemment"
+msgstr "Date de participation décroissante"
msgid "SortOptions|Last updated"
-msgstr "Mise à jour récemment"
+msgstr "Mis à jour récemment"
msgid "SortOptions|Least popular"
-msgstr "Moins populaire"
-
-msgid "SortOptions|Less weight"
-msgstr "Poids croissant"
+msgstr "Popularité croissante"
msgid "SortOptions|Milestone"
msgstr "Jalon"
@@ -3885,11 +4173,8 @@ msgstr "Jalon avec une échéance lointaine"
msgid "SortOptions|Milestone due soon"
msgstr "Jalon avec une échéance proche"
-msgid "SortOptions|More weight"
-msgstr "Poids décroissant"
-
msgid "SortOptions|Most popular"
-msgstr "Populaire"
+msgstr "Popularité décroissante"
msgid "SortOptions|Name"
msgstr "Nom"
@@ -3901,16 +4186,16 @@ msgid "SortOptions|Name, descending"
msgstr "Nom, par ordre décroissant"
msgid "SortOptions|Oldest created"
-msgstr "Créé depuis longtemps"
+msgstr "Date de création croissante"
msgid "SortOptions|Oldest joined"
-msgstr "Rejoint depuis longtemps"
+msgstr "Ancienneté des participants croissante"
msgid "SortOptions|Oldest sign in"
-msgstr "Authentifié·e depuis longtemps"
+msgstr "Date d’inscription croissante"
msgid "SortOptions|Oldest updated"
-msgstr "Mise à jour depuis longtemps"
+msgstr "Date de mise à jour croissante"
msgid "SortOptions|Popularity"
msgstr "Popularité"
@@ -3919,7 +4204,7 @@ msgid "SortOptions|Priority"
msgstr "Priorité"
msgid "SortOptions|Recent sign in"
-msgstr "Authentifié·e récemment"
+msgstr "Date d’inscription décroissante"
msgid "SortOptions|Start later"
msgstr "Commence plus tard"
@@ -3927,14 +4212,11 @@ msgstr "Commence plus tard"
msgid "SortOptions|Start soon"
msgstr "Commence bientôt"
-msgid "SortOptions|Weight"
-msgstr "Poids"
-
msgid "Source"
msgstr "Source"
msgid "Source (branch or tag)"
-msgstr "Source (branche ou tag)"
+msgstr "Source (branche ou étiquette)"
msgid "Source code"
msgstr "Code source"
@@ -3946,10 +4228,37 @@ msgid "Spam Logs"
msgstr "Journaux des messages indésirables"
msgid "Spam and Anti-bot Protection"
-msgstr ""
+msgstr "Protection antiâ€pourriel et antiâ€robot"
+
+msgid "Specific Runners"
+msgstr "Exécuteurs spécifiques"
msgid "Specify the following URL during the Runner setup:"
-msgstr "Spécifiez l’URL suivante lors de la configuration de l'Exécuteur :"
+msgstr "Spécifiez l’URL suivante lors de la configuration de l’exécuteur :"
+
+msgid "Squash commits"
+msgstr "Compiler les modifications (squash commits)"
+
+msgid "Stage"
+msgstr ""
+
+msgid "Stage & Commit"
+msgstr ""
+
+msgid "Stage all changes"
+msgstr "Marquer toutes les modifications comme une étape"
+
+msgid "Stage changes"
+msgstr "Changements d’étape"
+
+msgid "Staged"
+msgstr "Prêt à valider"
+
+msgid "Staged %{type}"
+msgstr "%{type} est prêt à être valider"
+
+msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
+msgstr "Mettez une étoile sur une étiquette pour en faire une étiquette prioritaire. Ordonnez les étiquettes prioritaires pour changer leurs priorités relatives en les glissant."
msgid "StarProject|Star"
msgstr "Mettre en favori"
@@ -3967,16 +4276,19 @@ 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 l'Exécuteur !"
+msgstr "Démarrer l’exécuteur !"
msgid "Started"
msgstr "Démarré"
-msgid "State your message to activate"
-msgstr ""
+msgid "Starts at (UTC)"
+msgstr "Commence à (UTC)"
msgid "Status"
-msgstr "État"
+msgstr "État "
+
+msgid "Stop this environment"
+msgstr "Arrêter cet environnement"
msgid "Stopped"
msgstr "Arrêté"
@@ -3985,27 +4297,33 @@ msgid "Storage"
msgstr "Stockage"
msgid "Subgroups"
-msgstr "Sous-groupes"
+msgstr "Sousâ€groupes"
-msgid "Switch branch/tag"
-msgstr "Changer de branche / tag"
+msgid "Subscribe"
+msgstr "S’abonner"
-msgid "System"
-msgstr "Système"
+msgid "Subscribe at group level"
+msgstr "S’abonner au niveau du groupe"
-msgid "System Hooks"
-msgstr "Crochets système"
+msgid "Subscribe at project level"
+msgstr "S’abonner au niveau du projet"
-msgid "System header and footer:"
-msgstr ""
+msgid "Switch branch/tag"
+msgstr "Changer de branche ou d’étiquette"
+
+msgid "System Hooks"
+msgstr "« Hooks » système"
msgid "Tag (%{tag_count})"
msgid_plural "Tags (%{tag_count})"
-msgstr[0] "Tag (%{tag_count})"
-msgstr[1] "Tags (%{tag_count})"
+msgstr[0] "Étiquette (%{tag_count})"
+msgstr[1] "Étiquettes (%{tag_count})"
msgid "Tags"
-msgstr "Tags"
+msgstr "Étiquettes"
+
+msgid "Tags:"
+msgstr "Étiquettes :"
msgid "TagsPage|Browse commits"
msgstr "Parcourir les commits"
@@ -4014,64 +4332,64 @@ msgid "TagsPage|Browse files"
msgstr "Parcourir les fichiers"
msgid "TagsPage|Can't find HEAD commit for this tag"
-msgstr "Impossible de trouver le commit HEAD pour ce tag"
+msgstr "Impossible de trouver le commit HEAD pour cette étiquette"
msgid "TagsPage|Cancel"
msgstr "Annuler"
msgid "TagsPage|Create tag"
-msgstr "Créer le tag"
+msgstr "Créer l’étiquette"
msgid "TagsPage|Delete tag"
-msgstr "Supprimer le tag"
+msgstr "Supprimer l’étiquette"
msgid "TagsPage|Deleting the %{tag_name} tag cannot be undone. Are you sure?"
-msgstr "La suppression du tag %{tag_name} ne peut être annulée. Êtes-vous sûr•e ?"
+msgstr "La suppression de l’étiquette %{tag_name} ne peut être annulée. Êtesâ€vous sûr·e ?"
msgid "TagsPage|Edit release notes"
msgstr "Modifier les notes de version"
msgid "TagsPage|Existing branch name, tag, or commit SHA"
-msgstr "Nom de branche, tag, ou SHA de commit existant"
+msgstr "Nom de branche, d’étiquette ou SHA de commit existant"
msgid "TagsPage|Filter by tag name"
-msgstr "Filtrer par nom de tag"
+msgstr "Filtrer par nom d’étiquette"
msgid "TagsPage|New Tag"
-msgstr "Nouveau tag"
+msgstr "Nouvelle étiquette"
msgid "TagsPage|New tag"
-msgstr "Nouveau tag"
+msgstr "Nouvelle étiquette"
msgid "TagsPage|Optionally, add a message to the tag."
-msgstr "Éventuellement, ajoutez un message au tag."
+msgstr "Éventuellement, ajoutez un message à l’étiquette."
msgid "TagsPage|Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page."
-msgstr "Éventuellement, ajouter des notes de version pour le tag. Elles seront stockées dans la base de données de GitLab et affichées sur la page des tags."
+msgstr "Éventuellement, ajouter des notes de version pour l’étiquette. Elles seront stockées dans la base de données de GitLab et affichées sur la page des étiquettes."
msgid "TagsPage|Release notes"
msgstr "Notes de version"
msgid "TagsPage|Repository has no tags yet."
-msgstr "Le dépôt n‘a pas de tags pour le moment."
+msgstr "Le dépôt n’a pour le moment aucune étiquette."
msgid "TagsPage|Sort by"
msgstr "Trier par"
msgid "TagsPage|Tags"
-msgstr "Tags"
+msgstr "Étiquettes"
msgid "TagsPage|Tags give the ability to mark specific points in history as being important"
-msgstr "Les tags permettent de marquer des validations spécifiques commes importantes dans l‘historique du project"
+msgstr "Les étiquettes permettent de marquer des moments spécifiques de l’historique du projet comme étant importants"
msgid "TagsPage|This tag has no release notes."
-msgstr "Ce tag n‘a pas de notes de version."
+msgstr "Cette étiquette n’a pas de notes de version."
msgid "TagsPage|Use git tag command to add a new one:"
-msgstr "Utilisez la commande git tag pour en ajouter un nouveau :"
+msgstr "Utilisez la commande « git tag » pour en ajouter un nouveau :"
-msgid "TagsPage|Write your release notes or drag files here..."
-msgstr "Écrivez les notes de version ou faîtes glisser des fichiers ici…"
+msgid "TagsPage|Write your release notes or drag files here…"
+msgstr "Rédigez vos notes de version ou faites glisser des fichiers ici…"
msgid "TagsPage|protected"
msgstr "protégé"
@@ -4085,11 +4403,14 @@ msgstr "Branche cible"
msgid "Team"
msgstr "Équipe"
-msgid "Thanks! Don't show me this again"
-msgstr "Merci ! Ne plus afficher ce message"
+msgid "Terms of Service Agreement and Privacy Policy"
+msgstr "Conditions générales d’utilisation et politique de confidentialité"
-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 puissant 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 "Terms of Service and Privacy Policy"
+msgstr "Conditions générales d’utilisation et politique de confidentialité"
+
+msgid "Test coverage parsing"
+msgstr ""
msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project"
msgstr "Le suivi des tickets est l’endroit où ajouter des éléments à améliorer ou à résoudre dans un projet"
@@ -4097,29 +4418,23 @@ msgstr "Le suivi des tickets est l’endroit où ajouter des éléments à amél
msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
msgstr "Le suivi des tickets est l’endroit où ajouter des éléments à améliorer ou à résoudre dans un projet. Vous pouvez vous inscrire ou vous connecter pour créer des tickets pour ce projet."
-msgid "The X509 Certificate to use when mutual TLS is required to communicate with the external authorization service. If left blank, the server certificate is still validated when accessing over HTTPS."
-msgstr "Le certificat X509 à utiliser lorsque le protocole TLS est requis pour communiquer avec le service d’autorisation externe. Si ce champ est vide, le certificat du serveur est utilisé lors de l’accès via HTTPS."
-
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 le premier commit et la création de la demande de fusion. Les données seront automatiquement ajoutées ici une fois que vous aurez créé votre première demande de fusion."
msgid "The collection of events added to the data gathered for that stage."
-msgstr "L’ensemble d’évènements ajoutés aux données récupérées pour cette étape."
-
-msgid "The connection will time out after %{timeout}. For repositories that take longer, use a clone/push combination."
-msgstr "La connexion expirera après %{timeout}. Pour les dépôts qui prennent plus de temps, utilisez une combinaison cloner / pousser."
+msgstr "L’ensemble d’événements ajoutés aux données recueillies pour cette étape."
msgid "The fork relationship has been removed."
-msgstr "La relation de fourche a été supprimée."
+msgstr "La relation de divergence a été supprimée."
msgid "The import will time out after %{timeout}. For repositories that take longer, use a clone/push combination."
msgstr "L’importation expirera après %{timeout}. Pour les dépôts qui prennent plus de temps, utilisez une combinaison clone / push."
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 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."
+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 de cette étape."
msgid "The maximum file size allowed is 200KB."
-msgstr "La taille de fichier maximale autorisée est de 200 Ko."
+msgstr "La taille de fichier maximale autorisée est de 200 Kio."
msgid "The number of attempts GitLab will make to access a storage."
msgstr "Le nombre de tentatives que GitLab va effectuer pour accéder au stockage."
@@ -4127,18 +4442,15 @@ msgstr "Le nombre de tentatives que GitLab va effectuer pour accéder au stockag
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 passphrase required to decrypt the private key. This is optional and the value is encrypted at rest."
-msgstr "La phrase secrète est requise pour déchiffrer la clé privée. Ceci est facultatif et la valeur est cryptée à l’arrêt."
+msgid "The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>"
+msgstr "Le chemin d’accès au fichier de configuration de l’intégration continue. Par défaut, <code>.gitlab-ci.yml</code>"
msgid "The phase of the development lifecycle."
-msgstr "Les étapes du cycle de développement."
+msgstr "La phase du cycle de développement."
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 premier commit. Ce temps sera automatiquement ajouté quand vous pousserez votre premier commit."
-msgid "The private key to use when a client certificate is provided. This value is encrypted at rest."
-msgstr "La clé privée à utiliser lorsqu’un certificat client est fourni. Cette valeur est cryptée à l’arrêt."
-
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 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."
@@ -4158,10 +4470,10 @@ msgid "The repository must be accessible over <code>http://</code>, <code>https:
msgstr "Le dépôt doit être accessible via <code>http://</code>, <code>https://</code> ou <code>git://</code>."
msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request."
-msgstr "L’étape d’évaluation montre le temps entre la création de la demande de fusion et la fusion effective de celle-ci. Ces données seront automatiquement ajoutées après que vous ayez fusionné votre première demande de fusion."
+msgstr "L’étape d’évaluation montre le temps entre la création de la demande de fusion et la fusion effective de celleâ€ci. Ces données seront automatiquement ajoutées après que vous ayez fusionné votre première demande de fusion."
-msgid "The roadmap shows the progress of your epics along a timeline"
-msgstr "La feuille de route montre la progression de vos épopées dans le temps"
+msgid "The secure token used by the Runner to checkout the project"
+msgstr "Le jeton sécurisé utilisé par l’exécuteur pour vérifier (checkout) le projet"
msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time."
msgstr "L’étape de pré-production indique le temps entre la fusion de la DF et le déploiement du code dans l’environnent de production. Les données seront automatiquement ajoutées une fois que vous déploierez en production pour la première fois."
@@ -4187,14 +4499,20 @@ msgstr "La valeur située au point médian d’une série de valeur observée. C
msgid "There are no issues to show"
msgstr "Il n’y a aucun ticket à afficher"
+msgid "There are no labels yet"
+msgstr "Il n’y a pas encore d’étiquette"
+
msgid "There are no merge requests to show"
msgstr "Il n’y a aucune demande de fusion à afficher"
msgid "There are problems accessing Git storage: "
msgstr "Il y a des difficultés à accéder aux données Git : "
-msgid "There was an error loading results"
-msgstr "Une erreur s’est produite lors du chargement des résultats"
+msgid "There was an error loading jobs"
+msgstr "Une erreur est survenue lors du chargement des tâches"
+
+msgid "There was an error loading latest pipeline"
+msgstr "Une erreur est survenue lors du chargement du dernier pipeline"
msgid "There was an error loading users activity calendar."
msgstr "Une erreur s’est produite lors du chargement du calendrier d’activité des utilisateurs."
@@ -4214,12 +4532,21 @@ msgstr "Une erreur s’est produite lors de l’abonnement à ce label."
msgid "There was an error when unsubscribing from this label."
msgstr "Une erreur s’est produite lors de la désinscription à ce label."
-msgid "This board\\'s scope is reduced"
-msgstr "La portée de ce tableau est limitée"
+msgid "They can be managed using the %{link}."
+msgstr "Ils peuvent être gérés en utilisant %{link}."
+
+msgid "This GitLab instance does not provide any shared Runners yet. Instance administrators can register shared Runners in the admin area."
+msgstr "Cette instance de GitLab ne fournit aucun exécuteur partagé pour le moment. Les administrateur•rice•s de l’instance peuvent enregistrer des exécuteurs partagés dans la zone d’administration."
+
+msgid "This diff is collapsed."
+msgstr "Ce diff est replié."
msgid "This directory"
msgstr "Ce répertoire"
+msgid "This group does not provide any group Runners yet."
+msgstr "Ce groupe ne fournit pas encore d’exécuteurs de groupe."
+
msgid "This is a confidential issue."
msgstr "Ce ticket est confidentiel."
@@ -4241,6 +4568,15 @@ msgstr "Cette tâche dépend de l’utilisateur•rice pour déclencher son proc
msgid "This job depends on upstream jobs that need to succeed in order for this job to be triggered"
msgstr "Cette tâche dépend des tâches en amont qui doivent réussir pour que celle-ci soit déclenchée"
+msgid "This job does not have a trace."
+msgstr "Cette tâche n’a pas de trace."
+
+msgid "This job has been canceled"
+msgstr "Cette tâche a été annulée"
+
+msgid "This job has been skipped"
+msgstr "Cette tâche a été sautée"
+
msgid "This job has not been triggered yet"
msgstr "Cette tâche n’a pas encore été déclenchée"
@@ -4259,20 +4595,29 @@ msgstr "Cela signifie que vous ne pouvez pas pousser du code tant que vous n’a
msgid "This merge request is locked."
msgstr "Cette demande de fusion est verrouillée."
+msgid "This option is disabled while you still have unstaged changes"
+msgstr "Cette option est désactivée tant que vous avez des changements non-indexés"
+
msgid "This page is unavailable because you are not allowed to read information across multiple projects."
msgstr "Cette page n’est pas disponible car vous n’êtes pas autorisé à lire des informations dans plusieurs projets."
+msgid "This page will be removed in a future release."
+msgstr "Cette page sera supprimée dans une future mise à jour."
+
msgid "This project"
msgstr "Ce projet"
+msgid "This project does not belong to a group and can therefore not make use of group Runners."
+msgstr "Ce projet n’appartient pas à un groupe et ne peut donc pas faire appel à un exécuteur de groupe."
+
msgid "This repository"
msgstr "Ce dépôt"
-msgid "This will delete the custom metric, Are you sure?"
-msgstr "Cela supprimera la métrique personnalisée, êtes-vous sûr•e ?"
+msgid "This source diff could not be displayed because it is too large."
+msgstr "Ce diff n’a pas pu être affiché car il est trop grand."
-msgid "Those emails automatically become issues (with the comments becoming the email conversation) listed here."
-msgstr "Ces emails deviennent automatiquement des tickets (les commentaires étant extrait de la conversation par email) répertoriés ici."
+msgid "This user has no identities"
+msgstr "Cet utilisa·teur·trice n’a aucune identité"
msgid "Time before an issue gets scheduled"
msgstr "Temps avant qu’un ticket ne soit planifié"
@@ -4283,11 +4628,11 @@ 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"
-msgid "Time between updates and capacity settings."
-msgstr ""
+msgid "Time remaining"
+msgstr "Temp restant"
-msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
-msgstr "Temps en secondes où GitLab attendra une réponse du service externe. Si le service ne répond pas à temps, l’accès sera refusé."
+msgid "Time spent"
+msgstr "Temps passé"
msgid "Time tracking"
msgstr "Suivi du temps"
@@ -4310,26 +4655,32 @@ msgstr "il y a %s jours"
msgid "Timeago|%s days remaining"
msgstr "Il reste %s jours"
+msgid "Timeago|%s hours ago"
+msgstr "il y a %s heures"
+
msgid "Timeago|%s hours remaining"
msgstr "Il reste %s heures"
msgid "Timeago|%s minutes ago"
-msgstr "Il y a %s minutes"
+msgstr "il y a %s minutes"
msgid "Timeago|%s minutes remaining"
msgstr "Il reste %s minutes"
msgid "Timeago|%s months ago"
-msgstr "Il y a %s mois"
+msgstr "il y a %s mois"
msgid "Timeago|%s months remaining"
msgstr "Il reste %s mois"
+msgid "Timeago|%s seconds ago"
+msgstr "il y a %s secondes"
+
msgid "Timeago|%s seconds remaining"
msgstr "Il reste %s secondes"
msgid "Timeago|%s weeks ago"
-msgstr "Il y a %s semaines"
+msgstr "il y a %s semaines"
msgid "Timeago|%s weeks remaining"
msgstr "Il reste %s semaines"
@@ -4340,92 +4691,92 @@ msgstr "il y a %s ans"
msgid "Timeago|%s years remaining"
msgstr "Il reste %s ans"
+msgid "Timeago|1 day ago"
+msgstr "il y a une journée"
+
msgid "Timeago|1 day remaining"
msgstr "Il reste un jour"
+msgid "Timeago|1 hour ago"
+msgstr "il y a une heure"
+
msgid "Timeago|1 hour remaining"
msgstr "Il reste une heure"
+msgid "Timeago|1 minute ago"
+msgstr "il y a une minute"
+
msgid "Timeago|1 minute remaining"
msgstr "Il reste une minute"
+msgid "Timeago|1 month ago"
+msgstr "il y a un mois"
+
msgid "Timeago|1 month remaining"
msgstr "Il reste un mois"
+msgid "Timeago|1 week ago"
+msgstr "il y a une semaine"
+
msgid "Timeago|1 week remaining"
msgstr "Il reste une semaine"
+msgid "Timeago|1 year ago"
+msgstr "il y a un an"
+
msgid "Timeago|1 year remaining"
msgstr "Il reste un an"
msgid "Timeago|Past due"
msgstr "En retard"
-msgid "Timeago|a day ago"
-msgstr "Il y a un jour"
-
-msgid "Timeago|a month ago"
-msgstr "Il y a un mois"
-
-msgid "Timeago|a week ago"
-msgstr "Il y a une semaine"
-
-msgid "Timeago|a year ago"
-msgstr "Il y a un an"
-
-msgid "Timeago|about %s hours ago"
-msgstr "Il y a environ %s heures"
-
-msgid "Timeago|about a minute ago"
-msgstr "Il y a environ une minute"
-
-msgid "Timeago|about an hour ago"
-msgstr "Il y a environ une heure"
-
msgid "Timeago|in %s days"
-msgstr "Dans %s jours"
+msgstr "dans %s jours"
msgid "Timeago|in %s hours"
-msgstr "Dans %s heures"
+msgstr "dans %s heures"
msgid "Timeago|in %s minutes"
-msgstr "Dans %s minutes"
+msgstr "dans %s minutes"
msgid "Timeago|in %s months"
-msgstr "Dans %s mois"
+msgstr "dans %s mois"
msgid "Timeago|in %s seconds"
-msgstr "Dans %s secondes"
+msgstr "dans %s secondes"
msgid "Timeago|in %s weeks"
-msgstr "Dans %s semaines"
+msgstr "dans %s semaines"
msgid "Timeago|in %s years"
-msgstr "Dans %s années"
+msgstr "dans %s années"
msgid "Timeago|in 1 day"
-msgstr "Dans 1 jour"
+msgstr "dans 1 jour"
msgid "Timeago|in 1 hour"
-msgstr "Dans 1 heure"
+msgstr "dans 1 heure"
msgid "Timeago|in 1 minute"
-msgstr "Dans 1 minute"
+msgstr "dans 1 minute"
msgid "Timeago|in 1 month"
-msgstr "Dans 1 mois"
+msgstr "dans 1 mois"
msgid "Timeago|in 1 week"
-msgstr "Dans 1 semaine"
+msgstr "dans 1 semaine"
msgid "Timeago|in 1 year"
-msgstr "Dans 1 an"
+msgstr "dans 1 an"
+
+msgid "Timeago|just now"
+msgstr "à l’instant"
-msgid "Timeago|in a while"
-msgstr "il y a longtemps"
+msgid "Timeago|right now"
+msgstr "immédiatement"
-msgid "Timeago|less than a minute ago"
-msgstr "il y a moins d'une minute"
+msgid "Timeout"
+msgstr "Délai d’attente"
msgid "Time|hr"
msgid_plural "Time|hrs"
@@ -4443,20 +4794,11 @@ msgstr "s"
msgid "Tip:"
msgstr "Astuce :"
-msgid "Title"
-msgstr "Titre"
-
msgid "To GitLab"
msgstr "Vers GitLab"
-msgid "To connect GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to connect."
-msgstr "Pour connecter les dépôts GitHub, vous pouvez utiliser un %{personal_access_token_link}. Lorsque vous créez votre jeton d’accès, vous devrez sélectionner le champ <code>repo</code>, afin que nous puissions afficher une liste de vos dépôts publics et privés qui sont disponibles pour se connecter."
-
-msgid "To connect GitHub repositories, you first need to authorize GitLab to access the list of your GitHub repositories:"
-msgstr "Pour connecter les dépôts GitHub, vous devez d’abord autoriser GitLab à accéder à la liste de vos dépôts GitHub :"
-
-msgid "To connect an SVN repository, check out %{svn_link}."
-msgstr "Pour connecter un dépôt SVN, consultez %{svn_link}."
+msgid "To add an SSH key you need to %{generate_link_start}generate one%{link_end} or use an %{existing_link_start}existing key%{link_end}."
+msgstr "Afin d’ajouter une clef SSH, vous devez soit %{generate_link_start}en génèrer une%{link_end}, soit utiliser une %{existing_link_start}clef existante%{link_end}."
msgid "To import GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
msgstr "Pour importer les dépôts GitHub, vous pouvez utiliser un %{personal_access_token_link}. Lorsque vous créez votre jeton d’accès, vous devrez sélectionner le champ <code>repo</code>, afin que nous puissions afficher une liste de vos dépôts publics et privés qui sont disponibles pour être importés."
@@ -4467,21 +4809,21 @@ msgstr "Pour importer des dépôts GitHub, vous devez d’abord autoriser GitLab
msgid "To import an SVN repository, check out %{svn_link}."
msgstr "Pour importer un dépôt SVN, consultez %{svn_link}."
-msgid "To only use CI/CD features for an external repository, choose <strong>CI/CD for external repo</strong>."
-msgstr "Pour utiliser uniquement les fonctionnalités CI / CD pour un dépôt externe, choisissez <strong>CI / CD pour le dépôt externe</strong>."
-
-msgid "To set up SAML authentication for your group through an identity provider like Azure, Okta, Onelogin, Ping Identity, or your custom SAML 2.0 provider:"
-msgstr ""
+msgid "To start serving your jobs you can add Runners to your group"
+msgstr "Pour commencer à exécuter vos tâches, vous pouvez ajouter des exécuteurs à votre groupe"
msgid "To validate your GitLab CI configurations, go to 'CI/CD → Pipelines' inside your project, and click on the 'CI Lint' button."
msgstr "Pour valider vos configurations GitLab CI, allez dans 'CI / CD → Pipelines' dans votre projet, et cliquez sur le bouton 'CI Lint'."
-msgid "To view the roadmap, add a planned start or finish date to one of your epics in this group or its subgroups. Only epics in the past 3 months and the next 3 months are shown."
-msgstr "Pour afficher la feuille de route, ajoutez une date de début ou de fin planifiée à l’une de vos épopées dans ce groupe ou ses sous-groupes. Seules les épopées des 3 derniers mois et des 3 prochains mois sont affichées."
-
msgid "Todo"
msgstr "Tâche"
+msgid "Toggle Sidebar"
+msgstr "Afficher/masquer la barre latérale"
+
+msgid "Toggle discussion"
+msgstr "Basculer la discussion"
+
msgid "Toggle sidebar"
msgstr "Afficher/masquer la barre latérale"
@@ -4491,6 +4833,9 @@ msgstr "État du commutateur : Inactif"
msgid "ToggleButton|Toggle Status: ON"
msgstr "État du commutateur : Actif"
+msgid "Too many changes to show."
+msgstr "Trop de changements à afficher."
+
msgid "Total Time"
msgstr "Temps total"
@@ -4500,23 +4845,20 @@ msgstr "Temps total de test pour tous les commits/fusions"
msgid "Total: %{total}"
msgstr "Total : %{total}"
-msgid "Track activity with Contribution Analytics."
-msgstr "Suivre l’activité avec Contribution Analytics."
-
-msgid "Track groups of issues that share a theme, across projects and milestones"
-msgstr "Suivez les groupes de tickets qui partagent un thème, entre projets et jalons"
-
msgid "Track time with quick actions"
msgstr "Suivre le temps estimé/passé avec les actions rapides"
msgid "Trigger this manual action"
msgstr "Déclencher cette action manuelle"
-msgid "Turn on Service Desk"
-msgstr "Activer le Service Desk"
+msgid "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."
+msgstr "Les déclencheurs peuvent forcer la reconstruction pour une branche ou une étiquette spécifique via l’appel à une API. Ces jetons emprunteront l’identité de l’utilisateur auquel ils sont associés, ainsi que ses accès et permissions sur les projets."
-msgid "Unknown"
-msgstr "Inconnu"
+msgid "Try again"
+msgstr "Veuillez réessayer"
+
+msgid "Unable to load the diff. %{button_try_again}"
+msgstr "Impossible de charger le diff. %{button_try_again}"
msgid "Unlock"
msgstr "Déverrouiller"
@@ -4527,26 +4869,46 @@ msgstr "Déverrouillé"
msgid "Unresolve discussion"
msgstr "Marquer la discussion comme non résolue"
+msgid "Unstage all changes"
+msgstr "Désindexer les changements en étape"
+
+msgid "Unstage changes"
+msgstr "Désindexer les changements"
+
+msgid "Unstaged"
+msgstr "Désindexé"
+
+msgid "Unstaged %{type}"
+msgstr "Désindexation de %{type}"
+
+msgid "Unstaged and staged %{type}"
+msgstr "%{type} non-indéxés et indexés"
+
msgid "Unstar"
msgstr "Supprimer des favoris"
-msgid "Up to date"
-msgstr "À jour"
+msgid "Unsubscribe"
+msgstr "Se désabonner"
-msgid "Upgrade your plan to activate Advanced Global Search."
-msgstr "Mettez à jour votre abonnement pour activer la Recherche Globale Avancée."
+msgid "Unsubscribe at group level"
+msgstr "Se désabonner au niveau du groupe"
-msgid "Upgrade your plan to activate Contribution Analytics."
-msgstr "Mettez à jour votre abonnement pour activer Contribution Analytics."
+msgid "Unsubscribe at project level"
+msgstr "Se désabonner au niveau du projet"
-msgid "Upgrade your plan to activate Group Webhooks."
-msgstr "Mettez à jour votre abonnement pour activer les Webhooks de groupe."
+msgid "Unverified"
+msgstr "Non vérifié"
+
+msgid "Up to date"
+msgstr "À jour"
-msgid "Upgrade your plan to activate Issue weight."
-msgstr "Mettez à niveau votre abonnement pour activer les poids de ticket."
+msgid "Update %{files}"
+msgid_plural "Update %{files} files"
+msgstr[0] ""
+msgstr[1] ""
-msgid "Upgrade your plan to improve Issue boards."
-msgstr "Mettez à niveau votre forfait pour améliorer les tableaux de tickets."
+msgid "Update your group name, description, avatar, and other general settings."
+msgstr "Modifiez le nom du groupe, sa description, son logo et d’autres paramètres généraux."
msgid "Upload New File"
msgstr "Téléverser un nouveau fichier"
@@ -4564,10 +4926,10 @@ msgid "Upvotes"
msgstr "Votes positifs"
msgid "Usage statistics"
-msgstr ""
+msgstr "Statistiques d’utilisation"
-msgid "Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab"
-msgstr "Utilisez Service Desk pour intéragir avec vos utilisateurs (par exemple pour offrir un support client) par email directement dans GitLab"
+msgid "Use group milestones to manage issues from multiple projects in the same milestone."
+msgstr "Utilisez les jalons de groupe pour gérer les tickets de plusieurs projets dans le même jalon."
msgid "Use the following registration token during setup:"
msgstr "Utiliser le jeton d’inscription suivant pendant l’installation :"
@@ -4575,29 +4937,29 @@ msgstr "Utiliser le jeton d’inscription suivant pendant l’installation :"
msgid "Use your global notification setting"
msgstr "Utiliser vos paramètres de notification globaux"
-msgid "Used by members to sign in to your group in GitLab"
-msgstr ""
-
msgid "User and IP Rate Limits"
-msgstr ""
+msgstr "Limites de l’utilisateur et du débit IP"
+
+msgid "Users"
+msgstr "Utilisa·teur·trice·s"
+
+msgid "Variables"
+msgstr "Variables"
msgid "Variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. You can use variables for passwords, secret keys, or whatever you want."
msgstr "Les variables sont appliquées aux environnements via l’exécuteur. Elles peuvent être protégées en les exposant uniquement à des branches ou des tags protégées. Vous pouvez utiliser des variables pour les mots de passe, les clés secrètes ou tout ce que vous voulez."
msgid "Various container registry settings."
-msgstr ""
+msgstr "Divers paramètres de registre de conteneur."
msgid "Various email settings."
-msgstr ""
+msgstr "Divers paramètres de courriel."
msgid "Various settings that affect GitLab performance."
-msgstr ""
-
-msgid "View and edit lines"
-msgstr "Afficher et modifier les lignes"
+msgstr "Divers paramètres qui affectent les performances de GitLab."
-msgid "View epics list"
-msgstr "Afficher la liste des épopées"
+msgid "Verified"
+msgstr "Vérifié"
msgid "View file @ "
msgstr "Voir le fichier @ "
@@ -4605,9 +4967,15 @@ msgstr "Voir le fichier @ "
msgid "View group labels"
msgstr "Afficher les labels de groupe"
+msgid "View jobs"
+msgstr "Afficher les tâches"
+
msgid "View labels"
msgstr "Afficher les labels"
+msgid "View log"
+msgstr "Afficher le journal"
+
msgid "View open merge request"
msgstr "Afficher la demande de fusion"
@@ -4635,9 +5003,6 @@ msgstr "Inconnu"
msgid "Want to see the data? Please ask an administrator for access."
msgstr "Vous voulez voir les données ? Merci de contacter un administrateur pour en obtenir l’accès."
-msgid "We could not verify that one of your projects on GCP has billing enabled. Please try again."
-msgstr "Nous n’avons pas pu vérifier que l’un de vos projets sur GCP est activé pour la facturation. Veuillez réessayer."
-
msgid "We don't have enough data to show this stage."
msgstr "Nous n'avons pas suffisamment de données pour afficher cette étape."
@@ -4648,16 +5013,13 @@ msgid "Web IDE"
msgstr "Web IDE"
msgid "Web terminal"
-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 "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."
+msgstr "Terminal Web"
-msgid "Weight"
-msgstr "Poids"
+msgid "When a runner is locked, it cannot be assigned to other projects"
+msgstr "Lorsqu’un exécuteur est verrouillé, il ne peut pas être affecté à d’autres projets"
-msgid "When leaving the URL blank, classification labels can still be specified whitout disabling cross project features or performing external authorization checks."
-msgstr "Lorsque vous laissez l’URL vide, les étiquettes de classification peuvent toujours être spécifiées sans désactiver les fonctionnalités inter-projets ou effectuer des vérifications d’autorisation externes."
+msgid "When enabled, users cannot use GitLab until the terms have been accepted."
+msgstr "Lorsque cette option est activée, les utilisateur•rice•s ne pourront pas utiliser GitLab tant que les conditions générales d’utilisation ne seront pas acceptés."
msgid "Wiki"
msgstr "Wiki"
@@ -4672,7 +5034,7 @@ 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 :"
+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"
@@ -4683,8 +5045,32 @@ msgstr "Astuce : Vous pouvez déplacer cette page en ajoutant le chemin au débu
msgid "WikiEdit|There is already a page with the same title in that path."
msgstr "Il y a déjà une page avec le même titre pour ce chemin."
-msgid "WikiEmptyPageError|You are not allowed to create wiki pages"
-msgstr "Vous n’êtes pas autorisé·e à créer des pages wiki"
+msgid "WikiEmptyIssueMessage|Suggest wiki improvement"
+msgstr "Suggérer une amélioration du wiki"
+
+msgid "WikiEmptyIssueMessage|You must be a project member in order to add wiki pages. If you have suggestions for how to improve the wiki for this project, consider opening an issue in the %{issues_link}."
+msgstr "Vous devez être un membre du projet pour ajouter des pages wiki. Si vous avez des suggestions sur la manière d’améliorer le wiki pour ce projet, veuillez envisager l’ouverture d’un ticket sur %{issues_link}."
+
+msgid "WikiEmptyIssueMessage|issue tracker"
+msgstr "Suivi d’incidents"
+
+msgid "WikiEmpty|A wiki is where you can store all the details about your project. This can include why you've created it, its principles, how to use it, and so on."
+msgstr "Un wiki est un endroit où vous pouvez stocker tous les détails concernant votre projet. Ceci peut inclure sa raison d’être, ses principes, comment l’utiliser, etc."
+
+msgid "WikiEmpty|Create your first page"
+msgstr "Créer votre première page"
+
+msgid "WikiEmpty|Suggest wiki improvement"
+msgstr "Suggérer une amélioration du wiki"
+
+msgid "WikiEmpty|The wiki lets you write documentation for your project"
+msgstr "Le wiki vous permet d’écrire la documentation de votre projet"
+
+msgid "WikiEmpty|This project has no wiki pages"
+msgstr "Ce projet n’a pas de pages wiki"
+
+msgid "WikiEmpty|You must be a project member in order to add wiki pages."
+msgstr "Vous devez être un membre du projet pour ajouter des pages wiki."
msgid "WikiHistoricalPage|This is an old version of this page."
msgstr "Il s’agit d’une ancienne version de la page."
@@ -4719,6 +5105,12 @@ 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 "WikiPageConfirmDelete|Delete page"
+msgstr "Supprimer la page"
+
+msgid "WikiPageConfirmDelete|Delete page %{pageTitle}?"
+msgstr "Supprimer la page %{pageTitle} ?"
+
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."
@@ -4734,8 +5126,8 @@ 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 "WikiPage|Write your content or drag files here…"
+msgstr "Écrivez du contenu ou faites glisser des fichiers ici…"
msgid "Wiki|Create Page"
msgstr "Créer une Page"
@@ -4744,10 +5136,7 @@ msgid "Wiki|Create page"
msgstr "Créer la page"
msgid "Wiki|Edit Page"
-msgstr "Modifier cette Page"
-
-msgid "Wiki|Empty page"
-msgstr "Page vide"
+msgstr "Modifier cette page"
msgid "Wiki|More Pages"
msgstr "Plus de Pages"
@@ -4767,23 +5156,20 @@ 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 "Write a commit message..."
-msgstr "Écrire un message de commit…"
+msgid "Yes"
+msgstr "Oui"
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·e ?"
+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_full_name}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?"
-msgstr "Vous êtes sur le point de supprimer %{project_full_name}. Les projets supprimés NE PEUVENT PAS être restaurés ! Êtes-vous ABSOLUMENT sûr•e ?"
+msgstr "Vous êtes sur le point de supprimer %{project_full_name}. 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 ABSOLUMENT sûr·e ?"
+msgstr "Vous êtes sur le point de supprimer la relation de divergence avec le projet source %{forked_from_project}. En êtesâ€vous ABSOLUMENT sûr·e ?"
msgid "You are going to transfer %{project_full_name} to another owner. Are you ABSOLUTELY sure?"
msgstr "Vous allez transférer %{project_full_name} à un•e nouveau•elle propriétaire. Êtes-vous VRAIMENT sûr•e ?"
@@ -4791,8 +5177,8 @@ msgstr "Vous allez transférer %{project_full_name} à un•e nouveau•elle pro
msgid "You are on a read-only GitLab instance."
msgstr "Vous êtes sur une instance GitLab en lecture seule."
-msgid "You are on a secondary (read-only) Geo node. If you want to make any changes, you must visit the %{primary_node}."
-msgstr "Vous êtes sur un nœud Geo secondaire (en lecture seule). Si vous voulez apporter des modifications, vous devez visiter le %{primary_node}."
+msgid "You can %{linkStart}view the blob%{linkEnd} instead."
+msgstr "Vous pouvez %{linkStart}afficher les données brutes%{linkEnd} à la place."
msgid "You can also create a project from the command line."
msgstr "Vous pouvez également créer un projet en ligne de commande."
@@ -4800,6 +5186,9 @@ msgstr "Vous pouvez également créer un projet en ligne de commande."
msgid "You can also star a label to make it a priority label."
msgstr "Vous pouvez marquer un label comme important pour en faire un label prioritaire."
+msgid "You can also test your .gitlab-ci.yml in the %{linkStart}Lint%{linkEnd}"
+msgstr "Vous pouvez également tester votre .gitlab-ci.yml avec %{linkStart}Lint%{linkEnd}"
+
msgid "You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}"
msgstr "Vous pouvez facilement installer un Exécuteur sur un cluster Kubernetes. %{link_to_help_page}"
@@ -4810,32 +5199,35 @@ 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 can only edit files when you are on a branch"
-msgstr "Vous ne pouvez éditer de fichier que dans une branche"
+msgstr "Vous ne pouvez modifier des fichiers 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 can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}"
+msgstr "Vous pouvez résoudre le conflit de fusion Git soit en mode interactif, en cliquant sur les boutons « %{use_ours} » ou « %{use_theirs} », soit en modifiant directement les fichiers. Valider ces modifications dans la branche « %{branch_name} »"
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 do not have the correct permissions to override the settings from the LDAP group sync."
-msgstr "Vous ne disposez pas des autorisations appropriées pour remplacer les paramètres de synchronisation du groupe LDAP."
+msgid "You do not have any assigned merge requests"
+msgstr "Aucune demande de fusion ne vous a été affectée"
msgid "You have no permissions"
msgstr "Vous n’avez pas les autorisations"
+msgid "You have not created any merge requests"
+msgstr "Vous n’avez créé aucune demande de fusion"
+
msgid "You have reached your project limit"
msgstr "Vous avez atteint votre limite de projet"
-msgid "You must have master access to force delete a lock"
-msgstr "Vous devez avoir un accès Expert pour forcer la suppression d’un verrou"
+msgid "You must accept our Terms of Service and privacy policy in order to register an account"
+msgstr "Vous devez accepter les conditions générales d’utilisation et la politique de confidentialité afin de pouvoir vous créer un compte"
+
+msgid "You must have maintainer access to force delete a lock"
+msgstr "Seul un responsable peut forcer la suppression d’un verrou"
msgid "You must sign in to star a project"
msgstr "Vous devez vous connecter pour mettre un projet en favori"
-msgid "You need a different license to enable FileLocks feature"
-msgstr "Vous avez besoin d’une licence différente pour activer la fonctionnalité FileLocks"
-
msgid "You need permission."
msgstr "Vous avez besoin d’une autorisation."
@@ -4902,151 +5294,49 @@ msgstr "Votre nom"
msgid "Your projects"
msgstr "Vos projets"
+msgid "ago"
+msgstr "auparavant "
+
msgid "among other things"
msgstr "entre autres choses"
-msgid "and %d fixed vulnerability"
-msgid_plural "and %d fixed vulnerabilities"
-msgstr[0] "et %d vulnérabilité corrigée"
-msgstr[1] "et %d vulnérabilités corrigées"
-
msgid "assign yourself"
msgstr "assignez vous"
msgid "branch name"
msgstr "nom de la branche"
-msgid "by"
-msgstr "par"
-
-msgid "ciReport|%{type} detected no new security vulnerabilities"
-msgstr "%{type} n’a détecté aucune nouvelle vulnérabilité de sécurité"
-
-msgid "ciReport|%{type} detected no security vulnerabilities"
-msgstr "%{type} n’a détecté aucune vulnérabilité de sécurité"
-
-msgid "ciReport|Code quality"
-msgstr "Qualité du code"
-
-msgid "ciReport|DAST detected no alerts by analyzing the review app"
-msgstr "DAST n’a détecté aucune alerte en analysant l’application de revue"
-
-msgid "ciReport|Dependency scanning"
-msgstr "Analyse de dépendance"
-
-msgid "ciReport|Dependency scanning detected"
-msgstr "Analyse de dépendance détectée"
-
-msgid "ciReport|Dependency scanning detected no new security vulnerabilities"
-msgstr "L’analyse des dépendances n’a détecté aucune nouvelle vulnérabilité de sécurité"
-
-msgid "ciReport|Dependency scanning detected no security vulnerabilities"
-msgstr "L’analyse des dépendances n’a détecté aucune vulnérabilité de sécurité"
-
-msgid "ciReport|Failed to load %{reportName} report"
-msgstr "Impossible de charger le rapport %{reportName}"
-
-msgid "ciReport|Fixed:"
-msgstr "Réparé :"
-
-msgid "ciReport|Instances"
-msgstr "Instances"
-
-msgid "ciReport|Learn more about whitelisting"
-msgstr "En savoir plus sur la liste blanche"
-
-msgid "ciReport|Loading %{reportName} report"
-msgstr "Chargement du rapport %{reportName}"
-
-msgid "ciReport|No changes to code quality"
-msgstr "Aucun changement dans la qualité du code"
-
-msgid "ciReport|No changes to performance metrics"
-msgstr "Aucun changement dans les indicateurs de performance"
-
-msgid "ciReport|Performance metrics"
-msgstr "Indicateurs de performance"
-
-msgid "ciReport|SAST"
-msgstr "SAST"
-
-msgid "ciReport|SAST detected"
-msgstr "SAST détectée"
-
-msgid "ciReport|SAST detected no new security vulnerabilities"
-msgstr "SAST n’a détecté aucune nouvelle vulnérabilité de sécurité"
-
-msgid "ciReport|SAST detected no security vulnerabilities"
-msgstr "SAST n’a détecté aucune vulnérabilité de sécurité"
-
-msgid "ciReport|SAST:container no vulnerabilities were found"
-msgstr "SAST:conteneur aucune vulnérabilité n’a été trouvée"
-
-msgid "ciReport|Security scanning"
-msgstr "Analyse de sécurité"
-
-msgid "ciReport|Security scanning failed loading any results"
-msgstr "L’analyse de sécurité n’a pas réussi à charger de résultats"
-
-msgid "ciReport|Show complete code vulnerabilities report"
-msgstr "Afficher le rapport complet sur les vulnérabilités du code"
-
-msgid "ciReport|Unapproved vulnerabilities (red) can be marked as approved. %{helpLink}"
-msgstr "Les vulnérabilités non approuvées (en rouge) peuvent être marquées comme approuvées. %{helpLink}"
-
-msgid "ciReport|no vulnerabilities"
-msgstr "aucune vulnérabilité"
-
msgid "command line instructions"
msgstr "instructions en ligne de commande"
msgid "connecting"
msgstr "connexion en cours"
-msgid "could not read private key, is the passphrase correct?"
-msgstr "impossible de lire la clé privée, la phrase secrète est-elle correcte ?"
-
msgid "day"
msgid_plural "days"
msgstr[0] "jour"
msgstr[1] "jours"
-msgid "detected %d fixed vulnerability"
-msgid_plural "detected %d fixed vulnerabilities"
-msgstr[0] "a détecté %d vulnérabilité corrigée"
-msgstr[1] "a détecté %d vulnérabilités corrigées"
+msgid "deploy token"
+msgstr "jeton de déploiement"
-msgid "detected %d new vulnerability"
-msgid_plural "detected %d new vulnerabilities"
-msgstr[0] "a détecté %d nouvelle vulnérabilité"
-msgstr[1] "a détecté %d nouvelle vulnérabilité"
+msgid "disabled"
+msgstr "désactivé"
-msgid "detected no vulnerabilities"
-msgstr "n’a détecté aucune vulnérabilité"
+msgid "enabled"
+msgstr "activé"
msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
msgstr "%{slash_command} mettra à jour la durée estimée avec la dernière commande."
-msgid "here"
-msgstr "ici"
+msgid "for this project"
+msgstr "pour ce projet"
msgid "importing"
msgstr "importation en cours"
-msgid "in progress"
-msgstr "en cours"
-
-msgid "is invalid because there is downstream lock"
-msgstr "est invalide car il y a un verrou en aval"
-
-msgid "is invalid because there is upstream lock"
-msgstr "est invalide car il y a un verrou en amont"
-
-msgid "is not a valid X509 certificate."
-msgstr "n’est pas un certificat X509 valide."
-
-msgid "locked by %{path_lock_user_name} %{created_at}"
-msgstr "verrouillé par %{path_lock_user_name} %{created_at}"
+msgid "latest version"
+msgstr "dernière version"
msgid "merge request"
msgid_plural "merge requests"
@@ -5065,29 +5355,8 @@ msgstr "%{metricsLinkStart}L’usage mémoire%{metricsLinkEnd} %{emphasisStart}a
msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage is %{emphasisStart} unchanged %{emphasisEnd} at %{memoryFrom}MB"
msgstr "%{metricsLinkStart}L’usage mémoire%{metricsLinkEnd} %{emphasisStart}est resté stable%{emphasisEnd} à %{memoryFrom}MO"
-msgid "mrWidget|Add approval"
-msgstr "Ajouter une approbation"
-
-msgid "mrWidget|Allows edits from maintainers"
-msgstr "Autoriser les modifications par les mainteneurs"
-
-msgid "mrWidget|An error occured while removing your approval."
-msgstr "Une erreur est survenue lors de la suppression de votre approbation."
-
-msgid "mrWidget|An error occured while retrieving approval data for this merge request."
-msgstr "Une erreur s’est produite lors de la récupération des données d’approbation pour cette demande de fusion."
-
-msgid "mrWidget|An error occured while submitting your approval."
-msgstr "Une erreur est survenue pendant l’envoi de votre approbation."
-
-msgid "mrWidget|Approve"
-msgstr "Approuver"
-
-msgid "mrWidget|Approved"
-msgstr ""
-
-msgid "mrWidget|Approved by"
-msgstr "Approuvée par"
+msgid "mrWidget|Allows commits from members who can merge to the target branch"
+msgstr "Autoriser les commits des membres qui peuvent fusionner dans la branche cible"
msgid "mrWidget|Cancel automatic merge"
msgstr "Annuler la fusion automatique"
@@ -5113,6 +5382,9 @@ msgstr "Fermée par"
msgid "mrWidget|Closes"
msgstr "Résout"
+msgid "mrWidget|Create an issue to resolve them later"
+msgstr "Créer un ticket pour les résoudre plus tard"
+
msgid "mrWidget|Deployment statistics are not available currently"
msgstr "Les statistiques de déploiement ne sont pas disponibles pour le moment"
@@ -5167,9 +5439,6 @@ msgstr "Supprimer la branche source"
msgid "mrWidget|Remove source branch"
msgstr "Supprimer la branche source"
-msgid "mrWidget|Remove your approval"
-msgstr "Supprimer votre approbation"
-
msgid "mrWidget|Request to merge"
msgstr "Demande de fusion de"
@@ -5209,6 +5478,9 @@ msgstr "La branche source ne sera pas supprimée"
msgid "mrWidget|There are merge conflicts"
msgstr "Il y a des conflits de fusion"
+msgid "mrWidget|There are unresolved discussions. Please resolve these discussions"
+msgstr "Il y a des discussions non résolues. Veuillez résoudre ces discussions"
+
msgid "mrWidget|This merge request failed to be merged automatically"
msgstr "Cette demande de fusion n’a pas pu être fusionnée automatiquement"
@@ -5219,7 +5491,7 @@ msgid "mrWidget|This project is archived, write access has been disabled"
msgstr "Ce projet est archivé, l’accès en écriture a été désactivé"
msgid "mrWidget|Web IDE"
-msgstr ""
+msgstr "Web IDE"
msgid "mrWidget|You can merge this merge request manually using the"
msgstr "Vous pouvez fusionner cette demande de fusion manuellement à l’aide de la"
@@ -5259,8 +5531,8 @@ msgstr "mot de passe"
msgid "personal access token"
msgstr "jeton d’accès personnel"
-msgid "private key does not match certificate."
-msgstr "clé privée ne correspond pas au certificat."
+msgid "remaining"
+msgstr "restant"
msgid "remove due date"
msgstr "supprimer la date d’échéance"
@@ -5274,11 +5546,8 @@ msgstr "%{slash_command} mettra à jour la somme du temps passé."
msgid "this document"
msgstr "ce document"
-msgid "to help your contributors communicate effectively!"
-msgstr "pour aider vos contributeurs à communiquer efficacement !"
-
msgid "username"
-msgstr "nom d’utilisateur"
+msgstr "nom d’utilisa·teur·trice"
msgid "uses Kubernetes clusters to deploy your code!"
msgstr "utilise les clusters Kubernetes pour déployer votre code !"
@@ -5286,3 +5555,8 @@ msgstr "utilise les clusters Kubernetes pour déployer votre code !"
msgid "with %{additions} additions, %{deletions} deletions."
msgstr "avec %{additions} ajouts, %{deletions} suppressions."
+msgid "within %d minute "
+msgid_plural "within %d minutes "
+msgstr[0] "dans %d minute "
+msgstr[1] "dans les %d minutes"
+
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 608d2a584ba..83ff735580e 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -8,8 +8,6 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2018-05-21 12:38-0700\n"
-"PO-Revision-Date: 2018-05-21 12:38-0700\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
@@ -18,6 +16,11 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
+msgid "%d changed file"
+msgid_plural "%d changed files"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d commit"
msgid_plural "%d commits"
msgstr[0] ""
@@ -79,6 +82,12 @@ msgid_plural "%{count} participants"
msgstr[0] ""
msgstr[1] ""
+msgid "%{filePath} deleted"
+msgstr ""
+
+msgid "%{group_docs_link_start}Groups%{group_docs_link_end} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects."
+msgstr ""
+
msgid "%{loadingIcon} Started"
msgstr ""
@@ -117,7 +126,7 @@ msgstr ""
msgid "%{unstaged} unstaged and %{staged} staged changes"
msgstr ""
-msgid "(checkout the %{link} for information on how to install it)."
+msgid "(check out the %{link} for information on how to install it)."
msgstr ""
msgid "+ %{moreCount} more"
@@ -133,12 +142,12 @@ msgid "- show less"
msgstr ""
msgid "1 %{type} addition"
-msgid_plural "%d %{type} additions"
+msgid_plural "%{count} %{type} additions"
msgstr[0] ""
msgstr[1] ""
msgid "1 %{type} modification"
-msgid_plural "%d %{type} modifications"
+msgid_plural "%{count} %{type} modifications"
msgstr[0] ""
msgstr[1] ""
@@ -178,6 +187,21 @@ msgstr ""
msgid "2FA enabled"
msgstr ""
+msgid "403|Please contact your GitLab administrator to get the permission."
+msgstr ""
+
+msgid "403|You don't have the permission to access this page."
+msgstr ""
+
+msgid "404|Make sure the address is correct and the page hasn't moved."
+msgstr ""
+
+msgid "404|Page Not Found"
+msgstr ""
+
+msgid "404|Please contact your GitLab administrator if you think this is a mistake."
+msgstr ""
+
msgid "<strong>Removes</strong> source branch"
msgstr ""
@@ -193,6 +217,9 @@ msgstr ""
msgid "A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}."
msgstr ""
+msgid "A regular expression that will be used to find the test coverage output in the job trace. Leave blank to disable"
+msgstr ""
+
msgid "A user with write access to the source branch selected this option"
msgstr ""
@@ -214,10 +241,13 @@ msgstr ""
msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again."
msgstr ""
+msgid "Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report."
+msgstr ""
+
msgid "Account"
msgstr ""
-msgid "Account and limit settings"
+msgid "Account and limit"
msgstr ""
msgid "Active"
@@ -247,6 +277,9 @@ msgstr ""
msgid "Add new directory"
msgstr ""
+msgid "Add reaction"
+msgstr ""
+
msgid "Add todo"
msgstr ""
@@ -301,6 +334,9 @@ msgstr ""
msgid "AdminUsers|To confirm, type %{username}"
msgstr ""
+msgid "Advanced"
+msgstr ""
+
msgid "Advanced settings"
msgstr ""
@@ -313,7 +349,10 @@ msgstr ""
msgid "All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings."
msgstr ""
-msgid "Allow edits from maintainers."
+msgid "Allow commits from members who can merge to the target branch."
+msgstr ""
+
+msgid "Allow public access to pipelines and job details, including output logs and artifacts"
msgstr ""
msgid "Allow rendering of PlantUML diagrams in Asciidoc documents."
@@ -328,6 +367,39 @@ msgstr ""
msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
msgstr ""
+msgid "An error accured whilst committing your changes."
+msgstr ""
+
+msgid "An error occured creating the new branch."
+msgstr ""
+
+msgid "An error occured whilst fetching the job trace."
+msgstr ""
+
+msgid "An error occured whilst fetching the latest pipline."
+msgstr ""
+
+msgid "An error occured whilst loading all the files."
+msgstr ""
+
+msgid "An error occured whilst loading the file content."
+msgstr ""
+
+msgid "An error occured whilst loading the file."
+msgstr ""
+
+msgid "An error occured whilst loading the merge request changes."
+msgstr ""
+
+msgid "An error occured whilst loading the merge request version data."
+msgstr ""
+
+msgid "An error occured whilst loading the merge request."
+msgstr ""
+
+msgid "An error occured whilst loading the pipelines jobs."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -352,7 +424,7 @@ msgstr ""
msgid "An error occurred while getting projects"
msgstr ""
-msgid "An error occurred while importing project"
+msgid "An error occurred while importing project: ${details}"
msgstr ""
msgid "An error occurred while loading commits"
@@ -409,6 +481,9 @@ msgstr ""
msgid "Are you sure you want to delete this pipeline schedule?"
msgstr ""
+msgid "Are you sure you want to remove this identity?"
+msgstr ""
+
msgid "Are you sure you want to reset registration token?"
msgstr ""
@@ -421,7 +496,7 @@ msgstr ""
msgid "Artifacts"
msgstr ""
-msgid "Ask your group master to setup a group Runner."
+msgid "Ask your group maintainer to setup a group Runner."
msgstr ""
msgid "Assign custom color like #FF0000"
@@ -445,6 +520,9 @@ msgstr ""
msgid "Assigned to :name"
msgstr ""
+msgid "Assigned to me"
+msgstr ""
+
msgid "Assignee"
msgstr ""
@@ -484,7 +562,10 @@ msgstr ""
msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
msgstr ""
-msgid "AutoDevOps|Auto DevOps (Beta)"
+msgid "Auto-cancel redundant, pending pipelines"
+msgstr ""
+
+msgid "AutoDevOps|Auto DevOps"
msgstr ""
msgid "AutoDevOps|Auto DevOps documentation"
@@ -505,7 +586,7 @@ msgstr ""
msgid "AutoDevOps|add a Kubernetes cluster"
msgstr ""
-msgid "AutoDevOps|enable Auto DevOps (Beta)"
+msgid "AutoDevOps|enable Auto DevOps"
msgstr ""
msgid "Available"
@@ -523,6 +604,9 @@ msgstr ""
msgid "Average per day: %{average}"
msgstr ""
+msgid "Background color"
+msgstr ""
+
msgid "Background jobs"
msgstr ""
@@ -598,7 +682,13 @@ msgstr ""
msgid "Begin with the selected commit"
msgstr ""
-msgid "Blame"
+msgid "Below are examples of regex for existing tools:"
+msgstr ""
+
+msgid "Boards"
+msgstr ""
+
+msgid "Branch %{branchName} was not found in this project's repository."
msgstr ""
msgid "Branch (%{branch_count})"
@@ -678,7 +768,7 @@ msgstr ""
msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
msgstr ""
-msgid "Branches|Only a project master or owner can delete a protected branch"
+msgid "Branches|Only a project maintainer or owner can delete a protected branch"
msgstr ""
msgid "Branches|Overview"
@@ -756,24 +846,36 @@ msgstr ""
msgid "CI / CD"
msgstr ""
-msgid "CI/CD configuration"
+msgid "CI / CD Settings"
msgstr ""
-msgid "CI/CD settings"
+msgid "CI/CD configuration"
msgstr ""
-msgid "CICD|A domain is required to use Auto Review Apps and Auto Deploy Stages."
+msgid "CI/CD settings"
msgstr ""
msgid "CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery."
msgstr ""
-msgid "CICD|Auto DevOps (Beta)"
+msgid "CICD|Auto DevOps"
msgstr ""
msgid "CICD|Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration."
msgstr ""
+msgid "CICD|Automatic deployment to staging, manual deployment to production"
+msgstr ""
+
+msgid "CICD|Continuous deployment to production"
+msgstr ""
+
+msgid "CICD|Deployment strategy"
+msgstr ""
+
+msgid "CICD|Deployment strategy needs a domain name to work correctly."
+msgstr ""
+
msgid "CICD|Disable Auto DevOps"
msgstr ""
@@ -795,7 +897,10 @@ msgstr ""
msgid "CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project."
msgstr ""
-msgid "Can run untagged jobs"
+msgid "CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages."
+msgstr ""
+
+msgid "Can't find HEAD commit for this branch"
msgstr ""
msgid "Cancel"
@@ -861,6 +966,12 @@ msgstr ""
msgid "Choose a branch/tag (e.g. %{master}) or enter a commit (e.g. %{sha}) to see what's changed or to create a merge request."
msgstr ""
+msgid "Choose any color."
+msgstr ""
+
+msgid "Choose between <code>clone</code> or <code>fetch</code> to get the recent application code"
+msgstr ""
+
msgid "Choose file..."
msgstr ""
@@ -963,6 +1074,9 @@ msgstr ""
msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
msgstr ""
+msgid "Click to expand it."
+msgstr ""
+
msgid "Click to expand text"
msgstr ""
@@ -981,10 +1095,13 @@ msgstr ""
msgid "ClusterIntegration|Add Kubernetes cluster"
msgstr ""
-msgid "ClusterIntegration|Add an existing Kubernetes cluster"
+msgid "ClusterIntegration|Advanced options on this Kubernetes cluster's integration"
msgstr ""
-msgid "ClusterIntegration|Advanced options on this Kubernetes cluster's integration"
+msgid "ClusterIntegration|An error occured while trying to fetch project zones: %{error}"
+msgstr ""
+
+msgid "ClusterIntegration|An error occured while trying to fetch your projects: %{error}"
msgstr ""
msgid "ClusterIntegration|Applications"
@@ -999,9 +1116,6 @@ msgstr ""
msgid "ClusterIntegration|Certificate Authority bundle (PEM format)"
msgstr ""
-msgid "ClusterIntegration|Choose how to set up Kubernetes cluster integration"
-msgstr ""
-
msgid "ClusterIntegration|Choose which of your project's environments will use this Kubernetes cluster."
msgstr ""
@@ -1017,6 +1131,9 @@ msgstr ""
msgid "ClusterIntegration|Copy Ingress IP Address to clipboard"
msgstr ""
+msgid "ClusterIntegration|Copy Jupyter Hostname to clipboard"
+msgstr ""
+
msgid "ClusterIntegration|Copy Kubernetes cluster name"
msgstr ""
@@ -1026,25 +1143,22 @@ msgstr ""
msgid "ClusterIntegration|Create Kubernetes cluster"
msgstr ""
-msgid "ClusterIntegration|Create Kubernetes cluster on Google Kubernetes Engine"
+msgid "ClusterIntegration|Enter the details for your Kubernetes cluster"
msgstr ""
-msgid "ClusterIntegration|Create a new Kubernetes cluster on Google Kubernetes Engine right from GitLab"
+msgid "ClusterIntegration|Environment scope"
msgstr ""
-msgid "ClusterIntegration|Create on Google Kubernetes Engine"
+msgid "ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab's Google Kubernetes Engine Integration."
msgstr ""
-msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
+msgid "ClusterIntegration|Fetching machine types"
msgstr ""
-msgid "ClusterIntegration|Enter the details for your Kubernetes cluster"
+msgid "ClusterIntegration|Fetching projects"
msgstr ""
-msgid "ClusterIntegration|Environment scope"
-msgstr ""
-
-msgid "ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for new GCP accounts to get started with GitLab's Google Kubernetes Engine Integration."
+msgid "ClusterIntegration|Fetching zones"
msgstr ""
msgid "ClusterIntegration|GitLab Integration"
@@ -1053,7 +1167,7 @@ msgstr ""
msgid "ClusterIntegration|GitLab Runner"
msgstr ""
-msgid "ClusterIntegration|Google Cloud Platform project ID"
+msgid "ClusterIntegration|Google Cloud Platform project"
msgstr ""
msgid "ClusterIntegration|Google Kubernetes Engine"
@@ -1086,6 +1200,12 @@ msgstr ""
msgid "ClusterIntegration|Integration status"
msgstr ""
+msgid "ClusterIntegration|Jupyter Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|JupyterHub"
+msgstr ""
+
msgid "ClusterIntegration|Kubernetes cluster"
msgstr ""
@@ -1119,7 +1239,13 @@ msgstr ""
msgid "ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project"
msgstr ""
-msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
+msgid "ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}."
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{help_link_start}Kubernetes%{help_link_end}."
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}."
msgstr ""
msgid "ClusterIntegration|Learn more about environments"
@@ -1143,6 +1269,18 @@ msgstr ""
msgid "ClusterIntegration|More information"
msgstr ""
+msgid "ClusterIntegration|No machine types matched your search"
+msgstr ""
+
+msgid "ClusterIntegration|No projects found"
+msgstr ""
+
+msgid "ClusterIntegration|No projects matched your search"
+msgstr ""
+
+msgid "ClusterIntegration|No zones matched your search"
+msgstr ""
+
msgid "ClusterIntegration|Note:"
msgstr ""
@@ -1155,9 +1293,6 @@ msgstr ""
msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
msgstr ""
-msgid "ClusterIntegration|Project ID"
-msgstr ""
-
msgid "ClusterIntegration|Project namespace"
msgstr ""
@@ -1188,19 +1323,37 @@ msgstr ""
msgid "ClusterIntegration|Save changes"
msgstr ""
+msgid "ClusterIntegration|Search machine types"
+msgstr ""
+
+msgid "ClusterIntegration|Search projects"
+msgstr ""
+
+msgid "ClusterIntegration|Search zones"
+msgstr ""
+
msgid "ClusterIntegration|Security"
msgstr ""
msgid "ClusterIntegration|See and edit the details for your Kubernetes cluster"
msgstr ""
-msgid "ClusterIntegration|See machine types"
+msgid "ClusterIntegration|Select machine type"
+msgstr ""
+
+msgid "ClusterIntegration|Select project"
+msgstr ""
+
+msgid "ClusterIntegration|Select project and zone to choose machine type"
+msgstr ""
+
+msgid "ClusterIntegration|Select project to choose zone"
msgstr ""
-msgid "ClusterIntegration|See your projects"
+msgid "ClusterIntegration|Select zone"
msgstr ""
-msgid "ClusterIntegration|See zones"
+msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
msgid "ClusterIntegration|Service token"
@@ -1233,6 +1386,9 @@ msgstr ""
msgid "ClusterIntegration|Token"
msgstr ""
+msgid "ClusterIntegration|Validating project billing status"
+msgstr ""
+
msgid "ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
@@ -1272,10 +1428,10 @@ msgstr ""
msgid "Collapse sidebar"
msgstr ""
-msgid "Comment and resolve discussion"
+msgid "Comment & resolve discussion"
msgstr ""
-msgid "Comment and unresolve discussion"
+msgid "Comment & unresolve discussion"
msgstr ""
msgid "Comments"
@@ -1342,6 +1498,9 @@ msgstr ""
msgid "Committed by"
msgstr ""
+msgid "Commit…"
+msgstr ""
+
msgid "Compare"
msgstr ""
@@ -1453,9 +1612,15 @@ msgstr ""
msgid "ContainerRegistry|You can also use a %{deploy_token} for read-only access to the registry images."
msgstr ""
+msgid "Continue"
+msgstr ""
+
msgid "Continuous Integration and Deployment"
msgstr ""
+msgid "Contribute to GitLab"
+msgstr ""
+
msgid "Contribution"
msgstr ""
@@ -1477,6 +1642,9 @@ msgstr ""
msgid "ContributorsPage|Please wait a moment, this page will automatically refresh when ready."
msgstr ""
+msgid "Control the display of third party offers."
+msgstr ""
+
msgid "Copy URL to clipboard"
msgstr ""
@@ -1489,9 +1657,18 @@ msgstr ""
msgid "Copy commit SHA to clipboard"
msgstr ""
+msgid "Copy file name to clipboard"
+msgstr ""
+
+msgid "Copy file path to clipboard"
+msgstr ""
+
msgid "Copy reference to clipboard"
msgstr ""
+msgid "Copy to clipboard"
+msgstr ""
+
msgid "Create"
msgstr ""
@@ -1510,6 +1687,9 @@ msgstr ""
msgid "Create branch"
msgstr ""
+msgid "Create commit"
+msgstr ""
+
msgid "Create directory"
msgstr ""
@@ -1561,6 +1741,9 @@ msgstr ""
msgid "Created"
msgstr ""
+msgid "Created by me"
+msgstr ""
+
msgid "Cron Timezone"
msgstr ""
@@ -1573,6 +1756,9 @@ msgstr ""
msgid "CurrentUser|Settings"
msgstr ""
+msgid "Custom CI config path"
+msgstr ""
+
msgid "Custom notification events"
msgstr ""
@@ -1624,6 +1810,12 @@ msgstr ""
msgid "Delete"
msgstr ""
+msgid "Delete Snippet"
+msgstr ""
+
+msgid "Delete list"
+msgstr ""
+
msgid "Deploy"
msgid_plural "Deploys"
msgstr[0] ""
@@ -1752,6 +1944,9 @@ msgstr ""
msgid "DeployTokens|Your new project deploy token has been created."
msgstr ""
+msgid "Deprioritize label"
+msgstr ""
+
msgid "Description"
msgstr ""
@@ -1761,6 +1956,9 @@ msgstr ""
msgid "Diffs|No file name available"
msgstr ""
+msgid "Diffs|Something went wrong while fetching diff lines."
+msgstr ""
+
msgid "Directory name"
msgstr ""
@@ -1830,15 +2028,27 @@ msgstr ""
msgid "Edit"
msgstr ""
+msgid "Edit Label"
+msgstr ""
+
msgid "Edit Pipeline Schedule %{id}"
msgstr ""
+msgid "Edit Snippet"
+msgstr ""
+
msgid "Edit files in the editor and commit changes here"
msgstr ""
+msgid "Edit identity for %{user_name}"
+msgstr ""
+
msgid "Email"
msgstr ""
+msgid "Email patch"
+msgstr ""
+
msgid "Emails"
msgstr ""
@@ -1866,6 +2076,9 @@ msgstr ""
msgid "Enable group Runners"
msgstr ""
+msgid "Enable or disable certain group features and choose access levels."
+msgstr ""
+
msgid "Enable or disable version check and usage ping."
msgstr ""
@@ -1887,9 +2100,18 @@ msgstr ""
msgid "Environments|An error occurred while making the request."
msgstr ""
+msgid "Environments|An error occurred while stopping the environment, please try again"
+msgstr ""
+
+msgid "Environments|Are you sure you want to stop this environment?"
+msgstr ""
+
msgid "Environments|Commit"
msgstr ""
+msgid "Environments|Deploy to..."
+msgstr ""
+
msgid "Environments|Deployment"
msgstr ""
@@ -1902,40 +2124,46 @@ msgstr ""
msgid "Environments|Job"
msgstr ""
+msgid "Environments|Learn more about stopping environments"
+msgstr ""
+
msgid "Environments|New environment"
msgstr ""
msgid "Environments|No deployments yet"
msgstr ""
-msgid "Environments|Open"
+msgid "Environments|Note that this action will stop the environment, but it will %{emphasis_start}not%{emphasis_end} have an effect on any existing deployment due to no “stop environment action†being defined in the %{ci_config_link_start}.gitlab-ci.yml%{ci_config_link_end} file."
msgstr ""
-msgid "Environments|Re-deploy"
+msgid "Environments|Open live environment"
+msgstr ""
+
+msgid "Environments|Re-deploy to environment"
msgstr ""
msgid "Environments|Read more about environments"
msgstr ""
-msgid "Environments|Rollback"
+msgid "Environments|Rollback environment"
msgstr ""
msgid "Environments|Show all"
msgstr ""
-msgid "Environments|Updated"
+msgid "Environments|Stop"
msgstr ""
-msgid "Environments|You don't have any environments right now."
+msgid "Environments|Stop environment"
msgstr ""
-msgid "Error Reporting and Logging"
+msgid "Environments|Updated"
msgstr ""
-msgid "Error checking branch data. Please try again."
+msgid "Environments|You don't have any environments right now."
msgstr ""
-msgid "Error committing changes. Please try again."
+msgid "Error Reporting and Logging"
msgstr ""
msgid "Error fetching contributors data."
@@ -1953,6 +2181,21 @@ msgstr ""
msgid "Error fetching usage ping data."
msgstr ""
+msgid "Error loading branch data. Please try again."
+msgstr ""
+
+msgid "Error loading last commit."
+msgstr ""
+
+msgid "Error loading markdown preview"
+msgstr ""
+
+msgid "Error loading merge requests."
+msgstr ""
+
+msgid "Error loading project data. Please try again."
+msgstr ""
+
msgid "Error occurred when toggling the notification subscription"
msgstr ""
@@ -1998,9 +2241,15 @@ msgstr ""
msgid "Expand"
msgstr ""
+msgid "Expand all"
+msgstr ""
+
msgid "Expand sidebar"
msgstr ""
+msgid "Explore groups"
+msgstr ""
+
msgid "Explore projects"
msgstr ""
@@ -2028,6 +2277,12 @@ msgstr ""
msgid "Failed to update issues, please try again."
msgstr ""
+msgid "Failure"
+msgstr ""
+
+msgid "Faster as it re-uses the project workspace (falling back to clone if it doesn't exist)"
+msgstr ""
+
msgid "Feb"
msgstr ""
@@ -2061,6 +2316,15 @@ msgstr ""
msgid "FirstPushedBy|pushed by"
msgstr ""
+msgid "For internal projects, any logged in user can view pipelines and access job details (output logs and artifacts)"
+msgstr ""
+
+msgid "For private projects, any member (guest or higher) can view pipelines and access job details (output logs and artifacts)"
+msgstr ""
+
+msgid "For public projects, anyone can view pipelines and access job details (output logs and artifacts)"
+msgstr ""
+
msgid "Fork"
msgid_plural "Forks"
msgstr[0] ""
@@ -2078,6 +2342,9 @@ msgstr ""
msgid "Format"
msgstr ""
+msgid "Found errors in your .gitlab-ci.yml:"
+msgstr ""
+
msgid "From %{provider_title}"
msgstr ""
@@ -2096,6 +2363,9 @@ msgstr ""
msgid "General"
msgstr ""
+msgid "General pipelines"
+msgstr ""
+
msgid "Generate a default set of labels"
msgstr ""
@@ -2108,6 +2378,9 @@ msgstr ""
msgid "Git storage health information has been reset"
msgstr ""
+msgid "Git strategy for pipelines"
+msgstr ""
+
msgid "Git version"
msgstr ""
@@ -2129,6 +2402,12 @@ msgstr ""
msgid "Gitaly Servers"
msgstr ""
+msgid "Gitaly|Address"
+msgstr ""
+
+msgid "Go Back"
+msgstr ""
+
msgid "Go back"
msgstr ""
@@ -2156,7 +2435,7 @@ msgstr ""
msgid "Group Runners"
msgstr ""
-msgid "Group masters can register group runners in the %{link}"
+msgid "Group maintainers can register group runners in the %{link}"
msgstr ""
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
@@ -2183,6 +2462,30 @@ msgstr ""
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
+msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
+msgstr ""
+
+msgid "GroupsDropdown|Frequently visited"
+msgstr ""
+
+msgid "GroupsDropdown|Groups you visit often will appear here"
+msgstr ""
+
+msgid "GroupsDropdown|Loading groups"
+msgstr ""
+
+msgid "GroupsDropdown|Search your groups"
+msgstr ""
+
+msgid "GroupsDropdown|Something went wrong on our end."
+msgstr ""
+
+msgid "GroupsDropdown|Sorry, no groups matched your search"
+msgstr ""
+
+msgid "GroupsDropdown|This feature requires browser localStorage support"
+msgstr ""
+
msgid "GroupsEmptyState|A group is a collection of several projects."
msgstr ""
@@ -2254,12 +2557,24 @@ msgid_plural "Hide values"
msgstr[0] ""
msgstr[1] ""
+msgid "Hide whitespace changes"
+msgstr ""
+
msgid "History"
msgstr ""
msgid "Housekeeping successfully started"
msgstr ""
+msgid "I accept the %{terms_link}"
+msgstr ""
+
+msgid "I accept the|Terms of Service and Privacy Policy"
+msgstr ""
+
+msgid "ID"
+msgstr ""
+
msgid "IDE|Commit"
msgstr ""
@@ -2269,15 +2584,39 @@ msgstr ""
msgid "IDE|Go back"
msgstr ""
+msgid "IDE|Open in file view"
+msgstr ""
+
msgid "IDE|Review"
msgstr ""
+msgid "Identifier"
+msgstr ""
+
+msgid "Identities"
+msgstr ""
+
+msgid "If disabled, the access level will depend on the user's permissions in the project."
+msgstr ""
+
+msgid "If enabled"
+msgstr ""
+
msgid "If you already have files you can push them using the %{link_to_cli} below."
msgstr ""
msgid "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>."
msgstr ""
+msgid "ImageDiffViewer|2-up"
+msgstr ""
+
+msgid "ImageDiffViewer|Onion skin"
+msgstr ""
+
+msgid "ImageDiffViewer|Swipe"
+msgstr ""
+
msgid "Import"
msgstr ""
@@ -2293,7 +2632,10 @@ msgstr ""
msgid "Import repository"
msgstr ""
-msgid "Include a Terms of Service agreement that all users must accept."
+msgid "Include a Terms of Service agreement and Privacy Policy that all users must accept."
+msgstr ""
+
+msgid "Inline"
msgstr ""
msgid "Install Runner on Kubernetes"
@@ -2308,6 +2650,9 @@ msgstr ""
msgid "Integrations"
msgstr ""
+msgid "Integrations Settings"
+msgstr ""
+
msgid "Interested parties can even contribute by pushing commits if they want to."
msgstr ""
@@ -2323,6 +2668,9 @@ msgstr ""
msgid "Introducing Cycle Analytics"
msgstr ""
+msgid "Issue Boards"
+msgstr ""
+
msgid "Issue events"
msgstr ""
@@ -2341,6 +2689,9 @@ msgstr ""
msgid "January"
msgstr ""
+msgid "Job"
+msgstr ""
+
msgid "Job has been erased"
msgstr ""
@@ -2386,6 +2737,9 @@ msgstr ""
msgid "Kubernetes service integration has been deprecated. %{deprecated_message_content} your Kubernetes clusters using the new <a href=\"%{url}\"/>Kubernetes Clusters</a> page"
msgstr ""
+msgid "LFS"
+msgstr ""
+
msgid "LFSStatus|Disabled"
msgstr ""
@@ -2395,6 +2749,9 @@ msgstr ""
msgid "Label"
msgstr ""
+msgid "Label actions dropdown"
+msgstr ""
+
msgid "LabelSelect|%{firstLabelName} +%{remainingLabelCount} more"
msgstr ""
@@ -2410,6 +2767,9 @@ msgstr ""
msgid "Labels can be applied to issues and merge requests to categorize them."
msgstr ""
+msgid "Labels can be applied to issues and merge requests."
+msgstr ""
+
msgid "Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>"
msgstr ""
@@ -2502,12 +2862,6 @@ msgstr ""
msgid "Locked to current projects"
msgstr ""
-msgid "Locked to this project"
-msgstr ""
-
-msgid "Login"
-msgstr ""
-
msgid "Manage all notifications"
msgstr ""
@@ -2535,9 +2889,6 @@ msgstr ""
msgid "Maximum git storage failures"
msgstr ""
-msgid "Maximum job timeout"
-msgstr ""
-
msgid "May"
msgstr ""
@@ -2547,6 +2898,9 @@ msgstr ""
msgid "Members"
msgstr ""
+msgid "Merge Request"
+msgstr ""
+
msgid "Merge Request:"
msgstr ""
@@ -2565,18 +2919,66 @@ msgstr ""
msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
msgstr ""
+msgid "MergeRequests|Resolve this discussion in a new issue"
+msgstr ""
+
+msgid "MergeRequests|Saving the comment failed"
+msgstr ""
+
+msgid "MergeRequests|Toggle comments for this file"
+msgstr ""
+
+msgid "MergeRequests|Updating discussions failed"
+msgstr ""
+
+msgid "MergeRequests|View file @ %{commitId}"
+msgstr ""
+
+msgid "MergeRequests|View replaced file @ %{commitId}"
+msgstr ""
+
msgid "Merged"
msgstr ""
msgid "Messages"
msgstr ""
+msgid "Metrics"
+msgstr ""
+
msgid "Metrics - Influx"
msgstr ""
msgid "Metrics - Prometheus"
msgstr ""
+msgid "Metrics|Check out the CI/CD documentation on deploying to an environment"
+msgstr ""
+
+msgid "Metrics|Environment"
+msgstr ""
+
+msgid "Metrics|Learn about environments"
+msgstr ""
+
+msgid "Metrics|No deployed environments"
+msgstr ""
+
+msgid "Metrics|There was an error fetching the environments data, please try again"
+msgstr ""
+
+msgid "Metrics|There was an error getting deployment information."
+msgstr ""
+
+msgid "Metrics|There was an error getting environments information."
+msgstr ""
+
+msgid "Metrics|Unexpected deployment data response from prometheus endpoint"
+msgstr ""
+
+msgid "Metrics|Unexpected metrics data response from prometheus endpoint"
+msgstr ""
+
msgid "Milestone"
msgstr ""
@@ -2613,6 +3015,9 @@ msgstr ""
msgid "Monitoring"
msgstr ""
+msgid "More actions"
+msgstr ""
+
msgid "More information"
msgstr ""
@@ -2631,18 +3036,42 @@ msgstr ""
msgid "Name new label"
msgstr ""
+msgid "Name your individual key via a title"
+msgstr ""
+
+msgid "Nav|Help"
+msgstr ""
+
+msgid "Nav|Home"
+msgstr ""
+
+msgid "Nav|Sign In / Register"
+msgstr ""
+
+msgid "Nav|Sign out and sign in with a different account"
+msgstr ""
+
+msgid "New"
+msgstr ""
+
+msgid "New Identity"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] ""
msgstr[1] ""
-msgid "New Kubernetes Cluster"
+msgid "New Label"
msgstr ""
-msgid "New Kubernetes cluster"
+msgid "New Pipeline Schedule"
msgstr ""
-msgid "New Pipeline Schedule"
+msgid "New Snippet"
+msgstr ""
+
+msgid "New Snippets"
msgstr ""
msgid "New branch"
@@ -2660,6 +3089,9 @@ msgstr ""
msgid "New group"
msgstr ""
+msgid "New identity"
+msgstr ""
+
msgid "New issue"
msgstr ""
@@ -2669,6 +3101,9 @@ msgstr ""
msgid "New merge request"
msgstr ""
+msgid "New pipelines will cancel older, pending pipelines on the same branch"
+msgstr ""
+
msgid "New project"
msgstr ""
@@ -2705,10 +3140,16 @@ msgstr ""
msgid "No file chosen"
msgstr ""
+msgid "No files found"
+msgstr ""
+
msgid "No files found."
msgstr ""
-msgid "No labels created yet."
+msgid "No merge requests found"
+msgstr ""
+
+msgid "No messages were logged"
msgstr ""
msgid "No repository"
@@ -2831,9 +3272,18 @@ msgstr ""
msgid "Online IDE integration settings."
msgstr ""
+msgid "Only comments from the following commit are shown below"
+msgstr ""
+
msgid "Only project members can comment."
msgstr ""
+msgid "Oops, are you sure?"
+msgstr ""
+
+msgid "Open in Xcode"
+msgstr ""
+
msgid "OpenedNDaysAgo|Opened"
msgstr ""
@@ -2846,6 +3296,12 @@ msgstr ""
msgid "Options"
msgstr ""
+msgid "Or you can choose one of the suggested colors below"
+msgstr ""
+
+msgid "Other Labels"
+msgstr ""
+
msgid "Otherwise it is recommended you start with one of the options below."
msgstr ""
@@ -2879,16 +3335,25 @@ msgstr ""
msgid "Password"
msgstr ""
+msgid "Paste your public SSH key, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'. Don't use your private SSH key."
+msgstr ""
+
msgid "Pause"
msgstr ""
msgid "Pending"
msgstr ""
+msgid "Per job. If a job passes this threshold, it will be marked as failed"
+msgstr ""
+
+msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgstr ""
+
msgid "Performance optimization"
msgstr ""
-msgid "Permalink"
+msgid "Permissions"
msgstr ""
msgid "Personal Access Token"
@@ -2906,6 +3371,9 @@ msgstr ""
msgid "Pipeline Schedules"
msgstr ""
+msgid "Pipeline triggers"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -3044,13 +3512,13 @@ msgstr ""
msgid "Pipeline|with stages"
msgstr ""
-msgid "PlantUML"
+msgid "Plain diff"
msgstr ""
-msgid "Play"
+msgid "PlantUML"
msgstr ""
-msgid "Please <a href=%{link_to_billing} target=\"_blank\" rel=\"noopener noreferrer\">enable billing for one of your projects to be able to create a Kubernetes cluster</a>, then try again."
+msgid "Play"
msgstr ""
msgid "Please accept the Terms of Service before continuing."
@@ -3062,12 +3530,30 @@ msgstr ""
msgid "Please solve the reCAPTCHA"
msgstr ""
+msgid "Please try again"
+msgstr ""
+
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
msgid "Preferences"
msgstr ""
+msgid "Preferences|Navigation theme"
+msgstr ""
+
+msgid "Prioritize"
+msgstr ""
+
+msgid "Prioritize label"
+msgstr ""
+
+msgid "Prioritized Labels"
+msgstr ""
+
+msgid "Prioritized label"
+msgstr ""
+
msgid "Private - Project access must be granted explicitly to each user."
msgstr ""
@@ -3083,6 +3569,9 @@ msgstr ""
msgid "Profiles|Account scheduled for removal."
msgstr ""
+msgid "Profiles|Add key"
+msgstr ""
+
msgid "Profiles|Change username"
msgstr ""
@@ -3110,9 +3599,15 @@ msgstr ""
msgid "Profiles|Path"
msgstr ""
+msgid "Profiles|This doesn't look like a public SSH key, are you sure you want to add it?"
+msgstr ""
+
msgid "Profiles|Type your %{confirmationValue} to confirm:"
msgstr ""
+msgid "Profiles|Typically starts with \"ssh-rsa …\""
+msgstr ""
+
msgid "Profiles|Update username"
msgstr ""
@@ -3131,6 +3626,9 @@ msgstr ""
msgid "Profiles|Your account is currently an owner in these groups:"
msgstr ""
+msgid "Profiles|e.g. My MacBook key"
+msgstr ""
+
msgid "Profiles|your account"
msgstr ""
@@ -3218,9 +3716,6 @@ msgstr ""
msgid "ProjectsDropdown|Sorry, no projects matched your search"
msgstr ""
-msgid "ProjectsDropdown|This feature requires browser localStorage support"
-msgstr ""
-
msgid "PrometheusDashboard|Time"
msgstr ""
@@ -3293,21 +3788,27 @@ msgstr ""
msgid "Promote these project milestones into a group milestone."
msgstr ""
-msgid "Promote to Group Label"
+msgid "Promote to Group Milestone"
msgstr ""
-msgid "Promote to Group Milestone"
+msgid "Promote to group label"
msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Provider"
+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 "Public pipelines"
+msgstr ""
+
msgid "Push events"
msgstr ""
@@ -3320,9 +3821,6 @@ msgstr ""
msgid "Quick actions can be used in the issues description and comment boxes."
msgstr ""
-msgid "Raw"
-msgstr ""
-
msgid "Read more"
msgstr ""
@@ -3332,12 +3830,6 @@ msgstr ""
msgid "Real-time features"
msgstr ""
-msgid "RefSwitcher|Branches"
-msgstr ""
-
-msgid "RefSwitcher|Tags"
-msgstr ""
-
msgid "Reference:"
msgstr ""
@@ -3347,6 +3839,9 @@ msgstr ""
msgid "Register and see your runners for this group."
msgstr ""
+msgid "Register and see your runners for this project."
+msgstr ""
+
msgid "Registry"
msgstr ""
@@ -3383,16 +3878,22 @@ msgstr ""
msgid "Remove avatar"
msgstr ""
+msgid "Remove priority"
+msgstr ""
+
msgid "Remove project"
msgstr ""
msgid "Repository"
msgstr ""
+msgid "Repository Settings"
+msgstr ""
+
msgid "Repository maintenance"
msgstr ""
-msgid "Repository mirror settings"
+msgid "Repository mirror"
msgstr ""
msgid "Repository storage"
@@ -3401,7 +3902,7 @@ msgstr ""
msgid "Request Access"
msgstr ""
-msgid "Require all users to accept Terms of Service when they access GitLab."
+msgid "Require all users to accept Terms of Service and Privacy Policy when they access GitLab."
msgstr ""
msgid "Reset git storage health information"
@@ -3413,6 +3914,9 @@ msgstr ""
msgid "Reset runners registration token"
msgstr ""
+msgid "Resolve all discussions in new issue"
+msgstr ""
+
msgid "Resolve conflicts on source branch"
msgstr ""
@@ -3451,6 +3955,9 @@ msgstr ""
msgid "Reviewing (merge request !%{mergeRequestId})"
msgstr ""
+msgid "Runner token"
+msgstr ""
+
msgid "Runners"
msgstr ""
@@ -3460,15 +3967,18 @@ msgstr ""
msgid "Runners can be placed on separate users, servers, and even on your local machine."
msgstr ""
-msgid "Runners settings"
-msgstr ""
-
msgid "Running"
msgstr ""
msgid "SSH Keys"
msgstr ""
+msgid "SSL Verification"
+msgstr ""
+
+msgid "Save"
+msgstr ""
+
msgid "Save changes"
msgstr ""
@@ -3490,6 +4000,12 @@ msgstr ""
msgid "Scheduling Pipelines"
msgstr ""
+msgid "Scroll to bottom"
+msgstr ""
+
+msgid "Scroll to top"
+msgstr ""
+
msgid "Search"
msgstr ""
@@ -3502,6 +4018,12 @@ msgstr ""
msgid "Search files"
msgstr ""
+msgid "Search for projects, issues, etc."
+msgstr ""
+
+msgid "Search merge requests"
+msgstr ""
+
msgid "Search milestones"
msgstr ""
@@ -3517,7 +4039,7 @@ msgstr ""
msgid "Seconds to wait for a storage access attempt"
msgstr ""
-msgid "Secret variables"
+msgid "Select"
msgstr ""
msgid "Select Archive Format"
@@ -3538,6 +4060,15 @@ msgstr ""
msgid "Select branch/tag"
msgstr ""
+msgid "Select project"
+msgstr ""
+
+msgid "Select project and zone to choose machine type"
+msgstr ""
+
+msgid "Select project to choose zone"
+msgstr ""
+
msgid "Select source branch"
msgstr ""
@@ -3601,17 +4132,32 @@ msgstr ""
msgid "Show command"
msgstr ""
+msgid "Show complete raw log"
+msgstr ""
+
+msgid "Show latest version"
+msgstr ""
+
+msgid "Show latest version of the diff"
+msgstr ""
+
msgid "Show parent pages"
msgstr ""
msgid "Show parent subgroups"
msgstr ""
+msgid "Show whitespace changes"
+msgstr ""
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] ""
msgstr[1] ""
+msgid "Side-by-side"
+msgstr ""
+
msgid "Sign out"
msgstr ""
@@ -3624,6 +4170,9 @@ msgstr ""
msgid "Size and domain settings for static websites"
msgstr ""
+msgid "Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job"
+msgstr ""
+
msgid "Snippets"
msgstr ""
@@ -3633,15 +4182,27 @@ msgstr ""
msgid "Something went wrong on our end."
msgstr ""
+msgid "Something went wrong on our end. Please try again!"
+msgstr ""
+
msgid "Something went wrong when toggling the button"
msgstr ""
+msgid "Something went wrong while closing the %{issuable}. Please try again later"
+msgstr ""
+
msgid "Something went wrong while fetching the projects."
msgstr ""
msgid "Something went wrong while fetching the registry list."
msgstr ""
+msgid "Something went wrong while reopening the %{issuable}. Please try again later"
+msgstr ""
+
+msgid "Something went wrong while resolving this discussion. Please try again."
+msgstr ""
+
msgid "Something went wrong. Please try again."
msgstr ""
@@ -3759,7 +4320,16 @@ msgstr ""
msgid "Specify the following URL during the Runner setup:"
msgstr ""
-msgid "Stage all"
+msgid "Squash commits"
+msgstr ""
+
+msgid "Stage"
+msgstr ""
+
+msgid "Stage & Commit"
+msgstr ""
+
+msgid "Stage all changes"
msgstr ""
msgid "Stage changes"
@@ -3771,6 +4341,9 @@ msgstr ""
msgid "Staged %{type}"
msgstr ""
+msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
+msgstr ""
+
msgid "StarProject|Star"
msgstr ""
@@ -3810,6 +4383,18 @@ msgstr ""
msgid "Subgroups"
msgstr ""
+msgid "Submit as spam"
+msgstr ""
+
+msgid "Subscribe"
+msgstr ""
+
+msgid "Subscribe at group level"
+msgstr ""
+
+msgid "Subscribe at project level"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr ""
@@ -3890,7 +4475,7 @@ msgstr ""
msgid "TagsPage|Use git tag command to add a new one:"
msgstr ""
-msgid "TagsPage|Write your release notes or drag files here..."
+msgid "TagsPage|Write your release notes or drag files here…"
msgstr ""
msgid "TagsPage|protected"
@@ -3905,10 +4490,13 @@ msgstr ""
msgid "Team"
msgstr ""
-msgid "Terms of Service"
+msgid "Terms of Service Agreement and Privacy Policy"
msgstr ""
-msgid "Terms of Service Agreement"
+msgid "Terms of Service and Privacy Policy"
+msgstr ""
+
+msgid "Test coverage parsing"
msgstr ""
msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project"
@@ -3941,6 +4529,9 @@ 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 path to CI config file. Defaults to <code>.gitlab-ci.yml</code>"
+msgstr ""
+
msgid "The phase of the development lifecycle."
msgstr ""
@@ -3968,6 +4559,9 @@ msgstr ""
msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request."
msgstr ""
+msgid "The secure token used by the Runner to checkout the project"
+msgstr ""
+
msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time."
msgstr ""
@@ -3992,6 +4586,9 @@ msgstr ""
msgid "There are no issues to show"
msgstr ""
+msgid "There are no labels yet"
+msgstr ""
+
msgid "There are no merge requests to show"
msgstr ""
@@ -4019,9 +4616,15 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third party offers"
+msgstr ""
+
msgid "This GitLab instance does not provide any shared Runners yet. Instance administrators can register shared Runners in the admin area."
msgstr ""
+msgid "This diff is collapsed."
+msgstr ""
+
msgid "This directory"
msgstr ""
@@ -4094,6 +4697,12 @@ msgstr ""
msgid "This repository"
msgstr ""
+msgid "This source diff could not be displayed because it is too large."
+msgstr ""
+
+msgid "This user has no identities"
+msgstr ""
+
msgid "Time before an issue gets scheduled"
msgstr ""
@@ -4130,6 +4739,9 @@ msgstr ""
msgid "Timeago|%s days remaining"
msgstr ""
+msgid "Timeago|%s hours ago"
+msgstr ""
+
msgid "Timeago|%s hours remaining"
msgstr ""
@@ -4145,6 +4757,9 @@ msgstr ""
msgid "Timeago|%s months remaining"
msgstr ""
+msgid "Timeago|%s seconds ago"
+msgstr ""
+
msgid "Timeago|%s seconds remaining"
msgstr ""
@@ -4160,46 +4775,43 @@ msgstr ""
msgid "Timeago|%s years remaining"
msgstr ""
-msgid "Timeago|1 day remaining"
-msgstr ""
-
-msgid "Timeago|1 hour remaining"
+msgid "Timeago|1 day ago"
msgstr ""
-msgid "Timeago|1 minute remaining"
+msgid "Timeago|1 day remaining"
msgstr ""
-msgid "Timeago|1 month remaining"
+msgid "Timeago|1 hour ago"
msgstr ""
-msgid "Timeago|1 week remaining"
+msgid "Timeago|1 hour remaining"
msgstr ""
-msgid "Timeago|1 year remaining"
+msgid "Timeago|1 minute ago"
msgstr ""
-msgid "Timeago|Past due"
+msgid "Timeago|1 minute remaining"
msgstr ""
-msgid "Timeago|a day ago"
+msgid "Timeago|1 month ago"
msgstr ""
-msgid "Timeago|a month ago"
+msgid "Timeago|1 month remaining"
msgstr ""
-msgid "Timeago|a week ago"
+msgid "Timeago|1 week ago"
msgstr ""
-msgid "Timeago|a year ago"
+msgid "Timeago|1 week remaining"
msgstr ""
-msgid "Timeago|about %s hours ago"
+msgid "Timeago|1 year ago"
msgstr ""
-msgid "Timeago|about a minute ago"
+msgid "Timeago|1 year remaining"
msgstr ""
-msgid "Timeago|about an hour ago"
+msgid "Timeago|Past due"
msgstr ""
msgid "Timeago|in %s days"
@@ -4241,10 +4853,13 @@ msgstr ""
msgid "Timeago|in 1 year"
msgstr ""
-msgid "Timeago|in a while"
+msgid "Timeago|just now"
msgstr ""
-msgid "Timeago|less than a minute ago"
+msgid "Timeago|right now"
+msgstr ""
+
+msgid "Timeout"
msgstr ""
msgid "Time|hr"
@@ -4263,9 +4878,15 @@ msgstr ""
msgid "Tip:"
msgstr ""
+msgid "Title"
+msgstr ""
+
msgid "To GitLab"
msgstr ""
+msgid "To add an SSH key you need to %{generate_link_start}generate one%{link_end} or use an %{existing_link_start}existing key%{link_end}."
+msgstr ""
+
msgid "To import GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
msgstr ""
@@ -4299,6 +4920,9 @@ msgstr ""
msgid "ToggleButton|Toggle Status: ON"
msgstr ""
+msgid "Too many changes to show."
+msgstr ""
+
msgid "Total Time"
msgstr ""
@@ -4314,6 +4938,9 @@ msgstr ""
msgid "Trigger this manual action"
msgstr ""
+msgid "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."
+msgstr ""
+
msgid "Try again"
msgstr ""
@@ -4329,7 +4956,7 @@ msgstr ""
msgid "Unresolve discussion"
msgstr ""
-msgid "Unstage all"
+msgid "Unstage all changes"
msgstr ""
msgid "Unstage changes"
@@ -4347,12 +4974,29 @@ msgstr ""
msgid "Unstar"
msgstr ""
+msgid "Unsubscribe"
+msgstr ""
+
+msgid "Unsubscribe at group level"
+msgstr ""
+
+msgid "Unsubscribe at project level"
+msgstr ""
+
msgid "Unverified"
msgstr ""
msgid "Up to date"
msgstr ""
+msgid "Update %{files}"
+msgid_plural "Update %{files} files"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Update your group name, description, avatar, and other general settings."
+msgstr ""
+
msgid "Upload New File"
msgstr ""
@@ -4383,6 +5027,12 @@ msgstr ""
msgid "User and IP Rate Limits"
msgstr ""
+msgid "Users"
+msgstr ""
+
+msgid "Variables"
+msgstr ""
+
msgid "Variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. You can use variables for passwords, secret keys, or whatever you want."
msgstr ""
@@ -4404,9 +5054,15 @@ msgstr ""
msgid "View group labels"
msgstr ""
+msgid "View jobs"
+msgstr ""
+
msgid "View labels"
msgstr ""
+msgid "View log"
+msgstr ""
+
msgid "View open merge request"
msgstr ""
@@ -4434,9 +5090,6 @@ msgstr ""
msgid "Want to see the data? Please ask an administrator for access."
msgstr ""
-msgid "We could not verify that one of your projects on GCP has billing enabled. Please try again."
-msgstr ""
-
msgid "We don't have enough data to show this stage."
msgstr ""
@@ -4479,7 +5132,31 @@ msgstr ""
msgid "WikiEdit|There is already a page with the same title in that path."
msgstr ""
-msgid "WikiEmptyPageError|You are not allowed to create wiki pages"
+msgid "WikiEmptyIssueMessage|Suggest wiki improvement"
+msgstr ""
+
+msgid "WikiEmptyIssueMessage|You must be a project member in order to add wiki pages. If you have suggestions for how to improve the wiki for this project, consider opening an issue in the %{issues_link}."
+msgstr ""
+
+msgid "WikiEmptyIssueMessage|issue tracker"
+msgstr ""
+
+msgid "WikiEmpty|A wiki is where you can store all the details about your project. This can include why you've created it, its principles, how to use it, and so on."
+msgstr ""
+
+msgid "WikiEmpty|Create your first page"
+msgstr ""
+
+msgid "WikiEmpty|Suggest wiki improvement"
+msgstr ""
+
+msgid "WikiEmpty|The wiki lets you write documentation for your project"
+msgstr ""
+
+msgid "WikiEmpty|This project has no wiki pages"
+msgstr ""
+
+msgid "WikiEmpty|You must be a project member in order to add wiki pages."
msgstr ""
msgid "WikiHistoricalPage|This is an old version of this page."
@@ -4536,7 +5213,7 @@ msgstr ""
msgid "WikiPage|Page slug"
msgstr ""
-msgid "WikiPage|Write your content or drag files here..."
+msgid "WikiPage|Write your content or drag files here…"
msgstr ""
msgid "Wiki|Create Page"
@@ -4548,9 +5225,6 @@ msgstr ""
msgid "Wiki|Edit Page"
msgstr ""
-msgid "Wiki|Empty page"
-msgstr ""
-
msgid "Wiki|More Pages"
msgstr ""
@@ -4572,10 +5246,10 @@ msgstr ""
msgid "Withdraw Access Request"
msgstr ""
-msgid "Write a commit message..."
+msgid "Yes"
msgstr ""
-msgid "Yes"
+msgid "Yes, add it"
msgstr ""
msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
@@ -4593,12 +5267,18 @@ msgstr ""
msgid "You are on a read-only GitLab instance."
msgstr ""
+msgid "You can %{linkStart}view the blob%{linkEnd} instead."
+msgstr ""
+
msgid "You can also create a project from the command line."
msgstr ""
msgid "You can also star a label to make it a priority label."
msgstr ""
+msgid "You can also test your .gitlab-ci.yml in the %{linkStart}Lint%{linkEnd}"
+msgstr ""
+
msgid "You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}"
msgstr ""
@@ -4617,13 +5297,22 @@ msgstr ""
msgid "You cannot write to this read-only GitLab instance."
msgstr ""
+msgid "You do not have any assigned merge requests"
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
+msgid "You have not created any merge requests"
+msgstr ""
+
msgid "You have reached your project limit"
msgstr ""
-msgid "You must have master access to force delete a lock"
+msgid "You must accept our Terms of Service and privacy policy in order to register an account"
+msgstr ""
+
+msgid "You must have maintainer access to force delete a lock"
msgstr ""
msgid "You must sign in to star a project"
@@ -4736,6 +5425,9 @@ msgstr ""
msgid "importing"
msgstr ""
+msgid "latest version"
+msgstr ""
+
msgid "merge request"
msgid_plural "merge requests"
msgstr[0] ""
@@ -4753,7 +5445,7 @@ msgstr ""
msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage is %{emphasisStart} unchanged %{emphasisEnd} at %{memoryFrom}MB"
msgstr ""
-msgid "mrWidget|Allows edits from maintainers"
+msgid "mrWidget|Allows commits from members who can merge to the target branch"
msgstr ""
msgid "mrWidget|Cancel automatic merge"
@@ -4819,6 +5511,9 @@ msgstr ""
msgid "mrWidget|Merged by"
msgstr ""
+msgid "mrWidget|Open in Web IDE"
+msgstr ""
+
msgid "mrWidget|Plain diff"
msgstr ""
@@ -4888,9 +5583,6 @@ msgstr ""
msgid "mrWidget|This project is archived, write access has been disabled"
msgstr ""
-msgid "mrWidget|Web IDE"
-msgstr ""
-
msgid "mrWidget|You can merge this merge request manually using the"
msgstr ""
@@ -4952,3 +5644,8 @@ msgstr ""
msgid "with %{additions} additions, %{deletions} deletions."
msgstr ""
+
+msgid "within %d minute "
+msgid_plural "within %d minutes "
+msgstr[0] ""
+msgstr[1] ""
diff --git a/locale/gl_ES/gitlab.po b/locale/gl_ES/gitlab.po
new file mode 100644
index 00000000000..6b1139aa799
--- /dev/null
+++ b/locale/gl_ES/gitlab.po
@@ -0,0 +1,5562 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: gitlab-ee\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-07-01 16:35+1000\n"
+"PO-Revision-Date: 2018-07-01 11:02\n"
+"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
+"Language-Team: Galician\n"
+"Language: gl_ES\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Generator: crowdin.com\n"
+"X-Crowdin-Project: gitlab-ee\n"
+"X-Crowdin-Language: gl\n"
+"X-Crowdin-File: /master/locale/gitlab.pot\n"
+
+msgid "%d changed file"
+msgid_plural "%d changed files"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "%d commit"
+msgid_plural "%d commits"
+msgstr[0] "%d commit"
+msgstr[1] "%d commits"
+
+msgid "%d commit behind"
+msgid_plural "%d commits behind"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "%d exporter"
+msgid_plural "%d exporters"
+msgstr[0] "%d exportador"
+msgstr[1] "%d exportadores"
+
+msgid "%d issue"
+msgid_plural "%d issues"
+msgstr[0] "%d incidencia"
+msgstr[1] "%d incidencias"
+
+msgid "%d layer"
+msgid_plural "%d layers"
+msgstr[0] "%d capa"
+msgstr[1] "%d capas"
+
+msgid "%d merge request"
+msgid_plural "%d merge requests"
+msgstr[0] "%d merge request"
+msgstr[1] "%d merge requests"
+
+msgid "%d metric"
+msgid_plural "%d metrics"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "%d staged change"
+msgid_plural "%d staged changes"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "%d unstaged change"
+msgid_plural "%d unstaged changes"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "%s additional commit has been omitted to prevent performance issues."
+msgid_plural "%s additional commits have been omitted to prevent performance issues."
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "%{actionText} & %{openOrClose} %{noteable}"
+msgstr ""
+
+msgid "%{commit_author_link} authored %{commit_timeago}"
+msgstr ""
+
+msgid "%{count} participant"
+msgid_plural "%{count} participants"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "%{filePath} deleted"
+msgstr ""
+
+msgid "%{group_docs_link_start}Groups%{group_docs_link_end} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects."
+msgstr ""
+
+msgid "%{loadingIcon} Started"
+msgstr ""
+
+msgid "%{lock_path} is locked by GitLab User %{lock_user_id}"
+msgstr ""
+
+msgid "%{nip_domain} can be used as an alternative to a custom domain."
+msgstr ""
+
+msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
+msgstr ""
+
+msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will allow access on the next attempt."
+msgstr ""
+
+msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will not retry automatically. Reset storage information when the problem is resolved."
+msgstr ""
+
+msgid "%{openOrClose} %{noteable}"
+msgstr ""
+
+msgid "%{percent}%% complete"
+msgstr ""
+
+msgid "%{storage_name}: failed storage access attempt on host:"
+msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "%{text} is available"
+msgstr ""
+
+msgid "%{title} changes"
+msgstr ""
+
+msgid "%{unstaged} unstaged and %{staged} staged changes"
+msgstr ""
+
+msgid "(checkout the %{link} for information on how to install it)."
+msgstr ""
+
+msgid "+ %{moreCount} more"
+msgstr ""
+
+msgid "- Runner is active and can process any new jobs"
+msgstr ""
+
+msgid "- Runner is paused and will not receive any new jobs"
+msgstr ""
+
+msgid "- show less"
+msgstr ""
+
+msgid "1 %{type} addition"
+msgid_plural "%{count} %{type} additions"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 %{type} modification"
+msgid_plural "%{count} %{type} modifications"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 closed issue"
+msgid_plural "%d closed issues"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 closed merge request"
+msgid_plural "%d closed merge requests"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 merged merge request"
+msgid_plural "%d merged merge requests"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 open issue"
+msgid_plural "%d open issues"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 open merge request"
+msgid_plural "%d open merge requests"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 pipeline"
+msgid_plural "%d pipelines"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1st contribution!"
+msgstr ""
+
+msgid "2FA enabled"
+msgstr ""
+
+msgid "403|Please contact your GitLab administrator to get the permission."
+msgstr ""
+
+msgid "403|You don't have the permission to access this page."
+msgstr ""
+
+msgid "404|Make sure the address is correct and the page hasn't moved."
+msgstr ""
+
+msgid "404|Page Not Found"
+msgstr ""
+
+msgid "404|Please contact your GitLab administrator if you think this is a mistake."
+msgstr ""
+
+msgid "<strong>Removes</strong> source branch"
+msgstr ""
+
+msgid "A 'Runner' is a process which runs a job. You can setup as many Runners as you need."
+msgstr ""
+
+msgid "A collection of graphs regarding Continuous Integration"
+msgstr ""
+
+msgid "A new branch will be created in your fork and a new merge request will be started."
+msgstr ""
+
+msgid "A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}."
+msgstr ""
+
+msgid "A regular expression that will be used to find the test coverage output in the job trace. Leave blank to disable"
+msgstr ""
+
+msgid "A user with write access to the source branch selected this option"
+msgstr ""
+
+msgid "About auto deploy"
+msgstr ""
+
+msgid "Abuse Reports"
+msgstr ""
+
+msgid "Abuse reports"
+msgstr ""
+
+msgid "Accept terms"
+msgstr ""
+
+msgid "Access Tokens"
+msgstr ""
+
+msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again."
+msgstr ""
+
+msgid "Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report."
+msgstr ""
+
+msgid "Account"
+msgstr ""
+
+msgid "Account and limit"
+msgstr ""
+
+msgid "Active"
+msgstr ""
+
+msgid "Active Sessions"
+msgstr ""
+
+msgid "Activity"
+msgstr ""
+
+msgid "Add Changelog"
+msgstr ""
+
+msgid "Add Contribution guide"
+msgstr ""
+
+msgid "Add Kubernetes cluster"
+msgstr ""
+
+msgid "Add License"
+msgstr ""
+
+msgid "Add Readme"
+msgstr ""
+
+msgid "Add new directory"
+msgstr ""
+
+msgid "Add reaction"
+msgstr ""
+
+msgid "Add todo"
+msgstr ""
+
+msgid "AdminArea|Stop all jobs"
+msgstr ""
+
+msgid "AdminArea|Stop all jobs?"
+msgstr ""
+
+msgid "AdminArea|Stop jobs"
+msgstr ""
+
+msgid "AdminArea|Stopping jobs failed"
+msgstr ""
+
+msgid "AdminArea|You’re about to stop all jobs.This will halt all current jobs that are running."
+msgstr ""
+
+msgid "AdminHealthPageLink|health page"
+msgstr ""
+
+msgid "AdminProjects|Delete"
+msgstr ""
+
+msgid "AdminProjects|Delete Project %{projectName}?"
+msgstr ""
+
+msgid "AdminProjects|Delete project"
+msgstr ""
+
+msgid "AdminSettings|Specify a domain to use by default for every project's Auto Review Apps and Auto Deploy stages."
+msgstr ""
+
+msgid "AdminUsers|Block user"
+msgstr ""
+
+msgid "AdminUsers|Delete User %{username} and contributions?"
+msgstr ""
+
+msgid "AdminUsers|Delete User %{username}?"
+msgstr ""
+
+msgid "AdminUsers|Delete user"
+msgstr ""
+
+msgid "AdminUsers|Delete user and contributions"
+msgstr ""
+
+msgid "AdminUsers|To confirm, type %{projectName}"
+msgstr ""
+
+msgid "AdminUsers|To confirm, type %{username}"
+msgstr ""
+
+msgid "Advanced"
+msgstr ""
+
+msgid "Advanced settings"
+msgstr ""
+
+msgid "All"
+msgstr ""
+
+msgid "All changes are committed"
+msgstr ""
+
+msgid "All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings."
+msgstr ""
+
+msgid "Allow commits from members who can merge to the target branch."
+msgstr ""
+
+msgid "Allow public access to pipelines and job details, including output logs and artifacts"
+msgstr ""
+
+msgid "Allow rendering of PlantUML diagrams in Asciidoc documents."
+msgstr ""
+
+msgid "Allow requests to the local network from hooks and services."
+msgstr ""
+
+msgid "Allows you to add and manage Kubernetes clusters."
+msgstr ""
+
+msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
+msgstr ""
+
+msgid "An error occured creating the new branch."
+msgstr ""
+
+msgid "An error occured whilst loading all the files."
+msgstr ""
+
+msgid "An error occured whilst loading the file content."
+msgstr ""
+
+msgid "An error occured whilst loading the file."
+msgstr ""
+
+msgid "An error occured whilst loading the merge request changes."
+msgstr ""
+
+msgid "An error occured whilst loading the merge request version data."
+msgstr ""
+
+msgid "An error occured whilst loading the merge request."
+msgstr ""
+
+msgid "An error occurred previewing the blob"
+msgstr ""
+
+msgid "An error occurred when toggling the notification subscription"
+msgstr ""
+
+msgid "An error occurred while dismissing the alert. Refresh the page and try again."
+msgstr ""
+
+msgid "An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again."
+msgstr ""
+
+msgid "An error occurred while fetching markdown preview"
+msgstr ""
+
+msgid "An error occurred while fetching sidebar data"
+msgstr ""
+
+msgid "An error occurred while fetching the pipeline."
+msgstr ""
+
+msgid "An error occurred while getting projects"
+msgstr ""
+
+msgid "An error occurred while importing project: ${details}"
+msgstr ""
+
+msgid "An error occurred while loading commits"
+msgstr ""
+
+msgid "An error occurred while loading diff"
+msgstr ""
+
+msgid "An error occurred while loading filenames"
+msgstr ""
+
+msgid "An error occurred while loading the file"
+msgstr ""
+
+msgid "An error occurred while making the request."
+msgstr ""
+
+msgid "An error occurred while rendering KaTeX"
+msgstr ""
+
+msgid "An error occurred while rendering preview broadcast message"
+msgstr ""
+
+msgid "An error occurred while retrieving calendar activity"
+msgstr ""
+
+msgid "An error occurred while retrieving diff"
+msgstr ""
+
+msgid "An error occurred while saving assignees"
+msgstr ""
+
+msgid "An error occurred while validating username"
+msgstr ""
+
+msgid "An error occurred. Please try again."
+msgstr ""
+
+msgid "Appearance"
+msgstr ""
+
+msgid "Applications"
+msgstr ""
+
+msgid "Apr"
+msgstr ""
+
+msgid "April"
+msgstr ""
+
+msgid "Archived project! Repository and other project resources are read-only"
+msgstr ""
+
+msgid "Are you sure you want to delete this pipeline schedule?"
+msgstr ""
+
+msgid "Are you sure you want to remove this identity?"
+msgstr ""
+
+msgid "Are you sure you want to reset registration token?"
+msgstr ""
+
+msgid "Are you sure you want to reset the health check token?"
+msgstr ""
+
+msgid "Are you sure?"
+msgstr ""
+
+msgid "Artifacts"
+msgstr ""
+
+msgid "Ask your group maintainer to setup a group Runner."
+msgstr ""
+
+msgid "Assign custom color like #FF0000"
+msgstr ""
+
+msgid "Assign labels"
+msgstr ""
+
+msgid "Assign milestone"
+msgstr ""
+
+msgid "Assign to"
+msgstr ""
+
+msgid "Assigned Issues"
+msgstr ""
+
+msgid "Assigned Merge Requests"
+msgstr ""
+
+msgid "Assigned to :name"
+msgstr ""
+
+msgid "Assigned to me"
+msgstr ""
+
+msgid "Assignee"
+msgstr ""
+
+msgid "Assignee(s)"
+msgstr ""
+
+msgid "Attach a file by drag &amp; drop or %{upload_link}"
+msgstr ""
+
+msgid "Aug"
+msgstr ""
+
+msgid "August"
+msgstr ""
+
+msgid "Authentication Log"
+msgstr ""
+
+msgid "Author"
+msgstr ""
+
+msgid "Authors: %{authors}"
+msgstr ""
+
+msgid "Auto DevOps enabled"
+msgstr ""
+
+msgid "Auto DevOps, runners and job artifacts"
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need a %{kubernetes} to work correctly."
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need a domain name and a %{kubernetes} to work correctly."
+msgstr ""
+
+msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
+msgstr ""
+
+msgid "Auto-cancel redundant, pending pipelines"
+msgstr ""
+
+msgid "AutoDevOps|Auto DevOps"
+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 automatically build and test your application if you %{link_to_auto_devops_settings} for this project. You can automatically deploy it as well, if you %{link_to_add_kubernetes_cluster}."
+msgstr ""
+
+msgid "AutoDevOps|add a Kubernetes cluster"
+msgstr ""
+
+msgid "AutoDevOps|enable Auto DevOps"
+msgstr ""
+
+msgid "Available"
+msgstr ""
+
+msgid "Available group Runners : %{runners}"
+msgstr ""
+
+msgid "Available group Runners : %{runners}."
+msgstr ""
+
+msgid "Avatar will be removed. Are you sure?"
+msgstr ""
+
+msgid "Average per day: %{average}"
+msgstr ""
+
+msgid "Background color"
+msgstr ""
+
+msgid "Background jobs"
+msgstr ""
+
+msgid "Badges"
+msgstr ""
+
+msgid "Badges|A new badge was added."
+msgstr ""
+
+msgid "Badges|Add badge"
+msgstr ""
+
+msgid "Badges|Adding the badge failed, please check the entered URLs and try again."
+msgstr ""
+
+msgid "Badges|Badge image URL"
+msgstr ""
+
+msgid "Badges|Badge image preview"
+msgstr ""
+
+msgid "Badges|Delete badge"
+msgstr ""
+
+msgid "Badges|Delete badge?"
+msgstr ""
+
+msgid "Badges|Deleting the badge failed, please try again."
+msgstr ""
+
+msgid "Badges|Group Badge"
+msgstr ""
+
+msgid "Badges|Link"
+msgstr ""
+
+msgid "Badges|No badge image"
+msgstr ""
+
+msgid "Badges|No image to preview"
+msgstr ""
+
+msgid "Badges|Project Badge"
+msgstr ""
+
+msgid "Badges|Reload badge image"
+msgstr ""
+
+msgid "Badges|Save changes"
+msgstr ""
+
+msgid "Badges|Saving the badge failed, please check the entered URLs and try again."
+msgstr ""
+
+msgid "Badges|The %{docsLinkStart}variables%{docsLinkEnd} GitLab supports: %{placeholders}"
+msgstr ""
+
+msgid "Badges|The badge was deleted."
+msgstr ""
+
+msgid "Badges|The badge was saved."
+msgstr ""
+
+msgid "Badges|This group has no badges"
+msgstr ""
+
+msgid "Badges|This project has no badges"
+msgstr ""
+
+msgid "Badges|Your badges"
+msgstr ""
+
+msgid "Begin with the selected commit"
+msgstr ""
+
+msgid "Below are examples of regex for existing tools:"
+msgstr ""
+
+msgid "Boards"
+msgstr ""
+
+msgid "Branch %{branchName} was not found in this project's repository."
+msgstr ""
+
+msgid "Branch (%{branch_count})"
+msgid_plural "Branches (%{branch_count})"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}"
+msgstr ""
+
+msgid "Branch has changed"
+msgstr ""
+
+msgid "Branch is already taken"
+msgstr ""
+
+msgid "Branch name"
+msgstr ""
+
+msgid "BranchSwitcherPlaceholder|Search branches"
+msgstr ""
+
+msgid "BranchSwitcherTitle|Switch branch"
+msgstr ""
+
+msgid "Branches"
+msgstr ""
+
+msgid "Branches|Active"
+msgstr ""
+
+msgid "Branches|Active branches"
+msgstr ""
+
+msgid "Branches|All"
+msgstr ""
+
+msgid "Branches|Cant find HEAD commit for this branch"
+msgstr ""
+
+msgid "Branches|Compare"
+msgstr ""
+
+msgid "Branches|Delete all branches that are merged into '%{default_branch}'"
+msgstr ""
+
+msgid "Branches|Delete branch"
+msgstr ""
+
+msgid "Branches|Delete merged branches"
+msgstr ""
+
+msgid "Branches|Delete protected branch"
+msgstr ""
+
+msgid "Branches|Delete protected branch '%{branch_name}'?"
+msgstr ""
+
+msgid "Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?"
+msgstr ""
+
+msgid "Branches|Deleting the merged branches cannot be undone. Are you sure?"
+msgstr ""
+
+msgid "Branches|Filter by branch name"
+msgstr ""
+
+msgid "Branches|Merged into %{default_branch}"
+msgstr ""
+
+msgid "Branches|New branch"
+msgstr ""
+
+msgid "Branches|No branches to show"
+msgstr ""
+
+msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
+msgstr ""
+
+msgid "Branches|Only a project maintainer or owner can delete a protected branch"
+msgstr ""
+
+msgid "Branches|Overview"
+msgstr ""
+
+msgid "Branches|Protected branches can be managed in %{project_settings_link}."
+msgstr ""
+
+msgid "Branches|Show active branches"
+msgstr ""
+
+msgid "Branches|Show all branches"
+msgstr ""
+
+msgid "Branches|Show more active branches"
+msgstr ""
+
+msgid "Branches|Show more stale branches"
+msgstr ""
+
+msgid "Branches|Show overview of the branches"
+msgstr ""
+
+msgid "Branches|Show stale branches"
+msgstr ""
+
+msgid "Branches|Sort by"
+msgstr ""
+
+msgid "Branches|Stale"
+msgstr ""
+
+msgid "Branches|Stale branches"
+msgstr ""
+
+msgid "Branches|The default branch cannot be deleted"
+msgstr ""
+
+msgid "Branches|This branch hasn’t been merged into %{default_branch}."
+msgstr ""
+
+msgid "Branches|To avoid data loss, consider merging this branch before deleting it."
+msgstr ""
+
+msgid "Branches|To confirm, type %{branch_name_confirmation}:"
+msgstr ""
+
+msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
+msgstr ""
+
+msgid "Branches|merged"
+msgstr ""
+
+msgid "Branches|project settings"
+msgstr ""
+
+msgid "Branches|protected"
+msgstr ""
+
+msgid "Browse Directory"
+msgstr ""
+
+msgid "Browse File"
+msgstr ""
+
+msgid "Browse Files"
+msgstr ""
+
+msgid "Browse files"
+msgstr ""
+
+msgid "ByAuthor|by"
+msgstr ""
+
+msgid "CI / CD"
+msgstr ""
+
+msgid "CI / CD Settings"
+msgstr ""
+
+msgid "CI/CD configuration"
+msgstr ""
+
+msgid "CI/CD settings"
+msgstr ""
+
+msgid "CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery."
+msgstr ""
+
+msgid "CICD|Auto DevOps"
+msgstr ""
+
+msgid "CICD|Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration."
+msgstr ""
+
+msgid "CICD|Automatic deployment to staging, manual deployment to production"
+msgstr ""
+
+msgid "CICD|Continuous deployment to production"
+msgstr ""
+
+msgid "CICD|Deployment strategy"
+msgstr ""
+
+msgid "CICD|Deployment strategy needs a domain name to work correctly."
+msgstr ""
+
+msgid "CICD|Disable Auto DevOps"
+msgstr ""
+
+msgid "CICD|Enable Auto DevOps"
+msgstr ""
+
+msgid "CICD|Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific %{ci_file}."
+msgstr ""
+
+msgid "CICD|Instance default (%{state})"
+msgstr ""
+
+msgid "CICD|Jobs"
+msgstr ""
+
+msgid "CICD|Learn more about Auto DevOps"
+msgstr ""
+
+msgid "CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project."
+msgstr ""
+
+msgid "CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages."
+msgstr ""
+
+msgid "Can't find HEAD commit for this branch"
+msgstr ""
+
+msgid "Cancel"
+msgstr ""
+
+msgid "Cancel this job"
+msgstr ""
+
+msgid "Cannot be merged automatically"
+msgstr ""
+
+msgid "Cannot modify managed Kubernetes cluster"
+msgstr ""
+
+msgid "Change this value to influence how frequently the GitLab UI polls for updates."
+msgstr ""
+
+msgid "ChangeTypeActionLabel|Pick into branch"
+msgstr ""
+
+msgid "ChangeTypeActionLabel|Revert in branch"
+msgstr ""
+
+msgid "ChangeTypeAction|Cherry-pick"
+msgstr ""
+
+msgid "ChangeTypeAction|Revert"
+msgstr ""
+
+msgid "ChangeTypeAction|This will create a new commit in order to revert the existing changes."
+msgstr ""
+
+msgid "Changelog"
+msgstr ""
+
+msgid "Changes are shown as if the <b>source</b> revision was being merged into the <b>target</b> revision."
+msgstr ""
+
+msgid "Charts"
+msgstr ""
+
+msgid "Chat"
+msgstr ""
+
+msgid "Check interval"
+msgstr ""
+
+msgid "Checking %{text} availability…"
+msgstr ""
+
+msgid "Checking branch availability..."
+msgstr ""
+
+msgid "Cherry-pick this commit"
+msgstr ""
+
+msgid "Cherry-pick this merge request"
+msgstr ""
+
+msgid "Choose File ..."
+msgstr ""
+
+msgid "Choose a branch/tag (e.g. %{master}) or enter a commit (e.g. %{sha}) to see what's changed or to create a merge request."
+msgstr ""
+
+msgid "Choose any color."
+msgstr ""
+
+msgid "Choose between <code>clone</code> or <code>fetch</code> to get the recent application code"
+msgstr ""
+
+msgid "Choose file..."
+msgstr ""
+
+msgid "Choose which repositories you want to import."
+msgstr ""
+
+msgid "CiStatusLabel|canceled"
+msgstr ""
+
+msgid "CiStatusLabel|created"
+msgstr ""
+
+msgid "CiStatusLabel|failed"
+msgstr ""
+
+msgid "CiStatusLabel|manual action"
+msgstr ""
+
+msgid "CiStatusLabel|passed"
+msgstr ""
+
+msgid "CiStatusLabel|passed with warnings"
+msgstr ""
+
+msgid "CiStatusLabel|pending"
+msgstr ""
+
+msgid "CiStatusLabel|skipped"
+msgstr ""
+
+msgid "CiStatusLabel|waiting for manual action"
+msgstr ""
+
+msgid "CiStatusText|blocked"
+msgstr ""
+
+msgid "CiStatusText|canceled"
+msgstr ""
+
+msgid "CiStatusText|created"
+msgstr ""
+
+msgid "CiStatusText|failed"
+msgstr ""
+
+msgid "CiStatusText|manual"
+msgstr ""
+
+msgid "CiStatusText|passed"
+msgstr ""
+
+msgid "CiStatusText|pending"
+msgstr ""
+
+msgid "CiStatusText|skipped"
+msgstr ""
+
+msgid "CiStatus|running"
+msgstr ""
+
+msgid "CiVariables|Input variable key"
+msgstr ""
+
+msgid "CiVariables|Input variable value"
+msgstr ""
+
+msgid "CiVariables|Remove variable row"
+msgstr ""
+
+msgid "CiVariable|* (All environments)"
+msgstr ""
+
+msgid "CiVariable|All environments"
+msgstr ""
+
+msgid "CiVariable|Error occured while saving variables"
+msgstr ""
+
+msgid "CiVariable|Protected"
+msgstr ""
+
+msgid "CiVariable|Toggle protected"
+msgstr ""
+
+msgid "CiVariable|Validation failed"
+msgstr ""
+
+msgid "CircuitBreakerApiLink|circuitbreaker api"
+msgstr ""
+
+msgid "Clear search input"
+msgstr ""
+
+msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
+msgstr ""
+
+msgid "Click the <strong>Promote</strong> button in the top right corner to promote it to a group milestone."
+msgstr ""
+
+msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
+msgstr ""
+
+msgid "Click to expand it."
+msgstr ""
+
+msgid "Click to expand text"
+msgstr ""
+
+msgid "Clone repository"
+msgstr ""
+
+msgid "Close"
+msgstr ""
+
+msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster"
+msgstr ""
+
+msgid "ClusterIntegration|API URL"
+msgstr ""
+
+msgid "ClusterIntegration|Add Kubernetes cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Add an existing Kubernetes cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Advanced options on this Kubernetes cluster's integration"
+msgstr ""
+
+msgid "ClusterIntegration|An error occured while trying to fetch project zones: %{error}"
+msgstr ""
+
+msgid "ClusterIntegration|An error occured while trying to fetch your projects: %{error}"
+msgstr ""
+
+msgid "ClusterIntegration|Applications"
+msgstr ""
+
+msgid "ClusterIntegration|Are you sure you want to remove this Kubernetes cluster's integration? This will not delete your actual Kubernetes cluster."
+msgstr ""
+
+msgid "ClusterIntegration|CA Certificate"
+msgstr ""
+
+msgid "ClusterIntegration|Certificate Authority bundle (PEM format)"
+msgstr ""
+
+msgid "ClusterIntegration|Choose how to set up Kubernetes cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Choose which of your project's environments will use this Kubernetes cluster."
+msgstr ""
+
+msgid "ClusterIntegration|Control how your Kubernetes cluster integrates with GitLab"
+msgstr ""
+
+msgid "ClusterIntegration|Copy API URL"
+msgstr ""
+
+msgid "ClusterIntegration|Copy CA Certificate"
+msgstr ""
+
+msgid "ClusterIntegration|Copy Ingress IP Address to clipboard"
+msgstr ""
+
+msgid "ClusterIntegration|Copy Jupyter Hostname to clipboard"
+msgstr ""
+
+msgid "ClusterIntegration|Copy Kubernetes cluster name"
+msgstr ""
+
+msgid "ClusterIntegration|Copy Token"
+msgstr ""
+
+msgid "ClusterIntegration|Create Kubernetes cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Create Kubernetes cluster on Google Kubernetes Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Create a new Kubernetes cluster on Google Kubernetes Engine right from GitLab"
+msgstr ""
+
+msgid "ClusterIntegration|Create on Google Kubernetes Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Enter the details for your Kubernetes cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Environment scope"
+msgstr ""
+
+msgid "ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab's Google Kubernetes Engine Integration."
+msgstr ""
+
+msgid "ClusterIntegration|Fetching machine types"
+msgstr ""
+
+msgid "ClusterIntegration|Fetching projects"
+msgstr ""
+
+msgid "ClusterIntegration|Fetching zones"
+msgstr ""
+
+msgid "ClusterIntegration|GitLab Integration"
+msgstr ""
+
+msgid "ClusterIntegration|GitLab Runner"
+msgstr ""
+
+msgid "ClusterIntegration|Google Cloud Platform project"
+msgstr ""
+
+msgid "ClusterIntegration|Google Kubernetes Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Google Kubernetes Engine project"
+msgstr ""
+
+msgid "ClusterIntegration|Helm Tiller"
+msgstr ""
+
+msgid "ClusterIntegration|Ingress"
+msgstr ""
+
+msgid "ClusterIntegration|Ingress IP Address"
+msgstr ""
+
+msgid "ClusterIntegration|Install"
+msgstr ""
+
+msgid "ClusterIntegration|Installed"
+msgstr ""
+
+msgid "ClusterIntegration|Installing"
+msgstr ""
+
+msgid "ClusterIntegration|Integrate Kubernetes cluster automation"
+msgstr ""
+
+msgid "ClusterIntegration|Integration status"
+msgstr ""
+
+msgid "ClusterIntegration|Jupyter Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|JupyterHub"
+msgstr ""
+
+msgid "ClusterIntegration|Kubernetes cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Kubernetes cluster details"
+msgstr ""
+
+msgid "ClusterIntegration|Kubernetes cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Kubernetes cluster integration is disabled for this project."
+msgstr ""
+
+msgid "ClusterIntegration|Kubernetes cluster integration is enabled for this project."
+msgstr ""
+
+msgid "ClusterIntegration|Kubernetes cluster integration is enabled for this project. Disabling this integration will not affect your Kubernetes cluster, it will only temporarily turn off GitLab's connection to it."
+msgstr ""
+
+msgid "ClusterIntegration|Kubernetes cluster is being created on Google Kubernetes Engine..."
+msgstr ""
+
+msgid "ClusterIntegration|Kubernetes cluster name"
+msgstr ""
+
+msgid "ClusterIntegration|Kubernetes cluster was successfully created on Google Kubernetes Engine. Refresh the page to see Kubernetes cluster's details"
+msgstr ""
+
+msgid "ClusterIntegration|Kubernetes clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way. %{link_to_help_page}"
+msgstr ""
+
+msgid "ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project"
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}."
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{help_link_start}Kubernetes%{help_link_end}."
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}."
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about environments"
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about security configuration"
+msgstr ""
+
+msgid "ClusterIntegration|Machine type"
+msgstr ""
+
+msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create Kubernetes clusters"
+msgstr ""
+
+msgid "ClusterIntegration|Manage"
+msgstr ""
+
+msgid "ClusterIntegration|Manage your Kubernetes cluster by visiting %{link_gke}"
+msgstr ""
+
+msgid "ClusterIntegration|More information"
+msgstr ""
+
+msgid "ClusterIntegration|No machine types matched your search"
+msgstr ""
+
+msgid "ClusterIntegration|No projects found"
+msgstr ""
+
+msgid "ClusterIntegration|No projects matched your search"
+msgstr ""
+
+msgid "ClusterIntegration|No zones matched your search"
+msgstr ""
+
+msgid "ClusterIntegration|Note:"
+msgstr ""
+
+msgid "ClusterIntegration|Number of nodes"
+msgstr ""
+
+msgid "ClusterIntegration|Please enter access information for your Kubernetes cluster. If you need help, you can read our %{link_to_help_page} on Kubernetes"
+msgstr ""
+
+msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgstr ""
+
+msgid "ClusterIntegration|Project namespace"
+msgstr ""
+
+msgid "ClusterIntegration|Project namespace (optional, unique)"
+msgstr ""
+
+msgid "ClusterIntegration|Prometheus"
+msgstr ""
+
+msgid "ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration."
+msgstr ""
+
+msgid "ClusterIntegration|Redeem up to $500 in free credit for Google Cloud Platform"
+msgstr ""
+
+msgid "ClusterIntegration|Remove Kubernetes cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Remove integration"
+msgstr ""
+
+msgid "ClusterIntegration|Remove this Kubernetes cluster's configuration from this project. This will not delete your actual Kubernetes cluster."
+msgstr ""
+
+msgid "ClusterIntegration|Request to begin installing failed"
+msgstr ""
+
+msgid "ClusterIntegration|Save changes"
+msgstr ""
+
+msgid "ClusterIntegration|Search machine types"
+msgstr ""
+
+msgid "ClusterIntegration|Search projects"
+msgstr ""
+
+msgid "ClusterIntegration|Search zones"
+msgstr ""
+
+msgid "ClusterIntegration|Security"
+msgstr ""
+
+msgid "ClusterIntegration|See and edit the details for your Kubernetes cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Select machine type"
+msgstr ""
+
+msgid "ClusterIntegration|Select project"
+msgstr ""
+
+msgid "ClusterIntegration|Select project and zone to choose machine type"
+msgstr ""
+
+msgid "ClusterIntegration|Select project to choose zone"
+msgstr ""
+
+msgid "ClusterIntegration|Select zone"
+msgstr ""
+
+msgid "ClusterIntegration|Select zone to choose machine type"
+msgstr ""
+
+msgid "ClusterIntegration|Service token"
+msgstr ""
+
+msgid "ClusterIntegration|Show"
+msgstr ""
+
+msgid "ClusterIntegration|Something went wrong on our end."
+msgstr ""
+
+msgid "ClusterIntegration|Something went wrong while creating your Kubernetes cluster on Google Kubernetes Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Something went wrong while installing %{title}"
+msgstr ""
+
+msgid "ClusterIntegration|The default cluster configuration grants access to a wide set of functionalities needed to successfully build and deploy a containerised application."
+msgstr ""
+
+msgid "ClusterIntegration|This account must have permissions to create a Kubernetes cluster in the %{link_to_container_project} specified below"
+msgstr ""
+
+msgid "ClusterIntegration|Toggle Kubernetes Cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Toggle Kubernetes cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Token"
+msgstr ""
+
+msgid "ClusterIntegration|Validating project billing status"
+msgstr ""
+
+msgid "ClusterIntegration|With a Kubernetes 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_kubernetes_engine}"
+msgstr ""
+
+msgid "ClusterIntegration|Zone"
+msgstr ""
+
+msgid "ClusterIntegration|access to Google Kubernetes Engine"
+msgstr ""
+
+msgid "ClusterIntegration|check the pricing here"
+msgstr ""
+
+msgid "ClusterIntegration|documentation"
+msgstr ""
+
+msgid "ClusterIntegration|help page"
+msgstr ""
+
+msgid "ClusterIntegration|installing applications"
+msgstr ""
+
+msgid "ClusterIntegration|meets the requirements"
+msgstr ""
+
+msgid "ClusterIntegration|properly configured"
+msgstr ""
+
+msgid "ClusterIntegration|sign up"
+msgstr ""
+
+msgid "Collapse"
+msgstr ""
+
+msgid "Collapse sidebar"
+msgstr ""
+
+msgid "Comment & resolve discussion"
+msgstr ""
+
+msgid "Comment & unresolve discussion"
+msgstr ""
+
+msgid "Comments"
+msgstr ""
+
+msgid "Commit"
+msgid_plural "Commits"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Commit (%{commit_count})"
+msgid_plural "Commits (%{commit_count})"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Commit Message"
+msgstr ""
+
+msgid "Commit duration in minutes for last 30 commits"
+msgstr ""
+
+msgid "Commit message"
+msgstr ""
+
+msgid "Commit statistics for %{ref} %{start_time} - %{end_time}"
+msgstr ""
+
+msgid "Commit to %{branchName} branch"
+msgstr ""
+
+msgid "CommitBoxTitle|Commit"
+msgstr ""
+
+msgid "CommitMessage|Add %{file_name}"
+msgstr ""
+
+msgid "Commits"
+msgstr ""
+
+msgid "Commits feed"
+msgstr ""
+
+msgid "Commits per day hour (UTC)"
+msgstr ""
+
+msgid "Commits per day of month"
+msgstr ""
+
+msgid "Commits per weekday"
+msgstr ""
+
+msgid "Commits|An error occurred while fetching merge requests data."
+msgstr ""
+
+msgid "Commits|Commit: %{commitText}"
+msgstr ""
+
+msgid "Commits|History"
+msgstr ""
+
+msgid "Commits|No related merge requests found"
+msgstr ""
+
+msgid "Committed by"
+msgstr ""
+
+msgid "Commit…"
+msgstr ""
+
+msgid "Compare"
+msgstr ""
+
+msgid "Compare Git revisions"
+msgstr ""
+
+msgid "Compare Revisions"
+msgstr ""
+
+msgid "Compare changes with the last commit"
+msgstr ""
+
+msgid "Compare changes with the merge request target branch"
+msgstr ""
+
+msgid "CompareBranches|%{source_branch} and %{target_branch} are the same."
+msgstr ""
+
+msgid "CompareBranches|Compare"
+msgstr ""
+
+msgid "CompareBranches|Source"
+msgstr ""
+
+msgid "CompareBranches|Target"
+msgstr ""
+
+msgid "CompareBranches|There isn't anything to compare."
+msgstr ""
+
+msgid "Confidential"
+msgstr ""
+
+msgid "Confidentiality"
+msgstr ""
+
+msgid "Configure Gitaly timeouts."
+msgstr ""
+
+msgid "Configure Sidekiq job throttling."
+msgstr ""
+
+msgid "Configure automatic git checks and housekeeping on repositories."
+msgstr ""
+
+msgid "Configure limits for web and API requests."
+msgstr ""
+
+msgid "Configure push mirrors."
+msgstr ""
+
+msgid "Configure storage path and circuit breaker settings."
+msgstr ""
+
+msgid "Configure the way a user creates a new account."
+msgstr ""
+
+msgid "Connect"
+msgstr ""
+
+msgid "Connect repositories from GitHub"
+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 "ContainerRegistry|You can also use a %{deploy_token} for read-only access to the registry images."
+msgstr ""
+
+msgid "Continue"
+msgstr ""
+
+msgid "Continuous Integration and Deployment"
+msgstr ""
+
+msgid "Contribute to GitLab"
+msgstr ""
+
+msgid "Contribution"
+msgstr ""
+
+msgid "Contribution guide"
+msgstr ""
+
+msgid "Contributors"
+msgstr ""
+
+msgid "ContributorsPage|%{startDate} – %{endDate}"
+msgstr ""
+
+msgid "ContributorsPage|Building repository graph."
+msgstr ""
+
+msgid "ContributorsPage|Commits to %{branch_name}, excluding merge commits. Limited to 6,000 commits."
+msgstr ""
+
+msgid "ContributorsPage|Please wait a moment, this page will automatically refresh when ready."
+msgstr ""
+
+msgid "Copy URL to clipboard"
+msgstr ""
+
+msgid "Copy branch name to clipboard"
+msgstr ""
+
+msgid "Copy command to clipboard"
+msgstr ""
+
+msgid "Copy commit SHA to clipboard"
+msgstr ""
+
+msgid "Copy file name to clipboard"
+msgstr ""
+
+msgid "Copy file path to clipboard"
+msgstr ""
+
+msgid "Copy reference to clipboard"
+msgstr ""
+
+msgid "Copy to clipboard"
+msgstr ""
+
+msgid "Create"
+msgstr ""
+
+msgid "Create New Directory"
+msgstr ""
+
+msgid "Create a new branch"
+msgstr ""
+
+msgid "Create a new branch and merge request"
+msgstr ""
+
+msgid "Create a personal access token on your account to pull or push via %{protocol}."
+msgstr ""
+
+msgid "Create branch"
+msgstr ""
+
+msgid "Create commit"
+msgstr ""
+
+msgid "Create directory"
+msgstr ""
+
+msgid "Create empty repository"
+msgstr ""
+
+msgid "Create file"
+msgstr ""
+
+msgid "Create group label"
+msgstr ""
+
+msgid "Create lists from labels. Issues with that label appear in that list."
+msgstr ""
+
+msgid "Create merge request"
+msgstr ""
+
+msgid "Create merge request and branch"
+msgstr ""
+
+msgid "Create new branch"
+msgstr ""
+
+msgid "Create new directory"
+msgstr ""
+
+msgid "Create new file"
+msgstr ""
+
+msgid "Create new label"
+msgstr ""
+
+msgid "Create new..."
+msgstr ""
+
+msgid "Create project label"
+msgstr ""
+
+msgid "CreateNewFork|Fork"
+msgstr ""
+
+msgid "CreateTag|Tag"
+msgstr ""
+
+msgid "CreateTokenToCloneLink|create a personal access token"
+msgstr ""
+
+msgid "Created"
+msgstr ""
+
+msgid "Created by me"
+msgstr ""
+
+msgid "Cron Timezone"
+msgstr ""
+
+msgid "Cron syntax"
+msgstr ""
+
+msgid "CurrentUser|Profile"
+msgstr ""
+
+msgid "CurrentUser|Settings"
+msgstr ""
+
+msgid "Custom CI config path"
+msgstr ""
+
+msgid "Custom notification events"
+msgstr ""
+
+msgid "Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notification_link}."
+msgstr ""
+
+msgid "Cycle Analytics"
+msgstr ""
+
+msgid "CycleAnalyticsStage|Code"
+msgstr ""
+
+msgid "CycleAnalyticsStage|Issue"
+msgstr ""
+
+msgid "CycleAnalyticsStage|Plan"
+msgstr ""
+
+msgid "CycleAnalyticsStage|Production"
+msgstr ""
+
+msgid "CycleAnalyticsStage|Review"
+msgstr ""
+
+msgid "CycleAnalyticsStage|Staging"
+msgstr ""
+
+msgid "CycleAnalyticsStage|Test"
+msgstr ""
+
+msgid "DashboardProjects|All"
+msgstr ""
+
+msgid "DashboardProjects|Personal"
+msgstr ""
+
+msgid "Dec"
+msgstr ""
+
+msgid "December"
+msgstr ""
+
+msgid "Decline and sign out"
+msgstr ""
+
+msgid "Define a custom pattern with cron syntax"
+msgstr ""
+
+msgid "Delete"
+msgstr ""
+
+msgid "Delete list"
+msgstr ""
+
+msgid "Deploy"
+msgid_plural "Deploys"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Deploy Keys"
+msgstr ""
+
+msgid "DeployKeys|+%{count} others"
+msgstr ""
+
+msgid "DeployKeys|Current project"
+msgstr ""
+
+msgid "DeployKeys|Deploy key"
+msgstr ""
+
+msgid "DeployKeys|Enabled deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Error enabling deploy key"
+msgstr ""
+
+msgid "DeployKeys|Error getting deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Error removing deploy key"
+msgstr ""
+
+msgid "DeployKeys|Expand %{count} other projects"
+msgstr ""
+
+msgid "DeployKeys|Loading deploy keys"
+msgstr ""
+
+msgid "DeployKeys|No deploy keys found. Create one with the form above."
+msgstr ""
+
+msgid "DeployKeys|Privately accessible deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Project usage"
+msgstr ""
+
+msgid "DeployKeys|Publicly accessible deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Read access only"
+msgstr ""
+
+msgid "DeployKeys|Write access allowed"
+msgstr ""
+
+msgid "DeployKeys|You are going to remove this deploy key. Are you sure?"
+msgstr ""
+
+msgid "DeployTokens|Active Deploy Tokens (%{active_tokens})"
+msgstr ""
+
+msgid "DeployTokens|Add a deploy token"
+msgstr ""
+
+msgid "DeployTokens|Allows read-only access to the registry images"
+msgstr ""
+
+msgid "DeployTokens|Allows read-only access to the repository"
+msgstr ""
+
+msgid "DeployTokens|Copy deploy token to clipboard"
+msgstr ""
+
+msgid "DeployTokens|Copy username to clipboard"
+msgstr ""
+
+msgid "DeployTokens|Create deploy token"
+msgstr ""
+
+msgid "DeployTokens|Created"
+msgstr ""
+
+msgid "DeployTokens|Deploy Tokens"
+msgstr ""
+
+msgid "DeployTokens|Deploy tokens allow read-only access to your repository and registry images."
+msgstr ""
+
+msgid "DeployTokens|Expires"
+msgstr ""
+
+msgid "DeployTokens|Name"
+msgstr ""
+
+msgid "DeployTokens|Pick a name for the application, and we'll give you a unique deploy token."
+msgstr ""
+
+msgid "DeployTokens|Revoke"
+msgstr ""
+
+msgid "DeployTokens|Revoke %{name}"
+msgstr ""
+
+msgid "DeployTokens|Scopes"
+msgstr ""
+
+msgid "DeployTokens|This action cannot be undone."
+msgstr ""
+
+msgid "DeployTokens|This project has no active Deploy Tokens."
+msgstr ""
+
+msgid "DeployTokens|Use this token as a password. Make sure you save it - you won't be able to access it again."
+msgstr ""
+
+msgid "DeployTokens|Use this username as a login."
+msgstr ""
+
+msgid "DeployTokens|Username"
+msgstr ""
+
+msgid "DeployTokens|You are about to revoke"
+msgstr ""
+
+msgid "DeployTokens|Your New Deploy Token"
+msgstr ""
+
+msgid "DeployTokens|Your new project deploy token has been created."
+msgstr ""
+
+msgid "Deprioritize label"
+msgstr ""
+
+msgid "Description"
+msgstr ""
+
+msgid "Details"
+msgstr ""
+
+msgid "Diffs|No file name available"
+msgstr ""
+
+msgid "Diffs|Something went wrong while fetching diff lines."
+msgstr ""
+
+msgid "Directory name"
+msgstr ""
+
+msgid "Disable"
+msgstr ""
+
+msgid "Disable for this project"
+msgstr ""
+
+msgid "Disable group Runners"
+msgstr ""
+
+msgid "Discard changes"
+msgstr ""
+
+msgid "Discard draft"
+msgstr ""
+
+msgid "Dismiss Cycle Analytics introduction box"
+msgstr ""
+
+msgid "Domain"
+msgstr ""
+
+msgid "Don't show again"
+msgstr ""
+
+msgid "Done"
+msgstr ""
+
+msgid "Download"
+msgstr ""
+
+msgid "Download tar"
+msgstr ""
+
+msgid "Download tar.bz2"
+msgstr ""
+
+msgid "Download tar.gz"
+msgstr ""
+
+msgid "Download zip"
+msgstr ""
+
+msgid "DownloadArtifacts|Download"
+msgstr ""
+
+msgid "DownloadCommit|Email Patches"
+msgstr ""
+
+msgid "DownloadCommit|Plain Diff"
+msgstr ""
+
+msgid "DownloadSource|Download"
+msgstr ""
+
+msgid "Downvotes"
+msgstr ""
+
+msgid "Due date"
+msgstr ""
+
+msgid "Each Runner can be in one of the following states:"
+msgstr ""
+
+msgid "Edit"
+msgstr ""
+
+msgid "Edit Label"
+msgstr ""
+
+msgid "Edit Pipeline Schedule %{id}"
+msgstr ""
+
+msgid "Edit files in the editor and commit changes here"
+msgstr ""
+
+msgid "Edit identity for %{user_name}"
+msgstr ""
+
+msgid "Email"
+msgstr ""
+
+msgid "Email patch"
+msgstr ""
+
+msgid "Emails"
+msgstr ""
+
+msgid "Embed"
+msgstr ""
+
+msgid "Enable"
+msgstr ""
+
+msgid "Enable Auto DevOps"
+msgstr ""
+
+msgid "Enable Sentry for error reporting and logging."
+msgstr ""
+
+msgid "Enable and configure InfluxDB metrics."
+msgstr ""
+
+msgid "Enable and configure Prometheus metrics."
+msgstr ""
+
+msgid "Enable for this project"
+msgstr ""
+
+msgid "Enable group Runners"
+msgstr ""
+
+msgid "Enable or disable certain group features and choose access levels."
+msgstr ""
+
+msgid "Enable or disable version check and usage ping."
+msgstr ""
+
+msgid "Enable reCAPTCHA or Akismet and set IP limits."
+msgstr ""
+
+msgid "Enable the Performance Bar for a given group."
+msgstr ""
+
+msgid "Ends at (UTC)"
+msgstr ""
+
+msgid "Environments"
+msgstr ""
+
+msgid "Environments|An error occurred while fetching the environments."
+msgstr ""
+
+msgid "Environments|An error occurred while making the request."
+msgstr ""
+
+msgid "Environments|Commit"
+msgstr ""
+
+msgid "Environments|Deployment"
+msgstr ""
+
+msgid "Environments|Environment"
+msgstr ""
+
+msgid "Environments|Environments"
+msgstr ""
+
+msgid "Environments|Job"
+msgstr ""
+
+msgid "Environments|New environment"
+msgstr ""
+
+msgid "Environments|No deployments yet"
+msgstr ""
+
+msgid "Environments|Open"
+msgstr ""
+
+msgid "Environments|Re-deploy"
+msgstr ""
+
+msgid "Environments|Read more about environments"
+msgstr ""
+
+msgid "Environments|Rollback"
+msgstr ""
+
+msgid "Environments|Show all"
+msgstr ""
+
+msgid "Environments|Updated"
+msgstr ""
+
+msgid "Environments|You don't have any environments right now."
+msgstr ""
+
+msgid "Error Reporting and Logging"
+msgstr ""
+
+msgid "Error committing changes. Please try again."
+msgstr ""
+
+msgid "Error fetching contributors data."
+msgstr ""
+
+msgid "Error fetching job trace"
+msgstr ""
+
+msgid "Error fetching labels."
+msgstr ""
+
+msgid "Error fetching network graph."
+msgstr ""
+
+msgid "Error fetching refs"
+msgstr ""
+
+msgid "Error fetching usage ping data."
+msgstr ""
+
+msgid "Error loading branch data. Please try again."
+msgstr ""
+
+msgid "Error loading last commit."
+msgstr ""
+
+msgid "Error loading merge requests."
+msgstr ""
+
+msgid "Error loading project data. Please try again."
+msgstr ""
+
+msgid "Error occurred when toggling the notification subscription"
+msgstr ""
+
+msgid "Error saving label update."
+msgstr ""
+
+msgid "Error updating status for all todos."
+msgstr ""
+
+msgid "Error updating todo status."
+msgstr ""
+
+msgid "Estimated"
+msgstr ""
+
+msgid "EventFilterBy|Filter by all"
+msgstr ""
+
+msgid "EventFilterBy|Filter by comments"
+msgstr ""
+
+msgid "EventFilterBy|Filter by issue events"
+msgstr ""
+
+msgid "EventFilterBy|Filter by merge events"
+msgstr ""
+
+msgid "EventFilterBy|Filter by push events"
+msgstr ""
+
+msgid "EventFilterBy|Filter by team"
+msgstr ""
+
+msgid "Every day (at 4:00am)"
+msgstr ""
+
+msgid "Every month (on the 1st at 4:00am)"
+msgstr ""
+
+msgid "Every week (Sundays at 4:00am)"
+msgstr ""
+
+msgid "Expand"
+msgstr ""
+
+msgid "Expand all"
+msgstr ""
+
+msgid "Expand sidebar"
+msgstr ""
+
+msgid "Explore projects"
+msgstr ""
+
+msgid "Explore public groups"
+msgstr ""
+
+msgid "Failed"
+msgstr ""
+
+msgid "Failed Jobs"
+msgstr ""
+
+msgid "Failed to change the owner"
+msgstr ""
+
+msgid "Failed to check related branches."
+msgstr ""
+
+msgid "Failed to remove issue from board, please try again."
+msgstr ""
+
+msgid "Failed to remove the pipeline schedule"
+msgstr ""
+
+msgid "Failed to update issues, please try again."
+msgstr ""
+
+msgid "Failure"
+msgstr ""
+
+msgid "Faster as it re-uses the project workspace (falling back to clone if it doesn't exist)"
+msgstr ""
+
+msgid "Feb"
+msgstr ""
+
+msgid "February"
+msgstr ""
+
+msgid "Fields on this page are now uneditable, you can configure"
+msgstr ""
+
+msgid "Files"
+msgstr ""
+
+msgid "Files (%{human_size})"
+msgstr ""
+
+msgid "Filter by commit message"
+msgstr ""
+
+msgid "Find by path"
+msgstr ""
+
+msgid "Find file"
+msgstr ""
+
+msgid "Finished"
+msgstr ""
+
+msgid "FirstPushedBy|First"
+msgstr ""
+
+msgid "FirstPushedBy|pushed by"
+msgstr ""
+
+msgid "For internal projects, any logged in user can view pipelines and access job details (output logs and artifacts)"
+msgstr ""
+
+msgid "For private projects, any member (guest or higher) can view pipelines and access job details (output logs and artifacts)"
+msgstr ""
+
+msgid "For public projects, anyone can view pipelines and access job details (output logs and artifacts)"
+msgstr ""
+
+msgid "Fork"
+msgid_plural "Forks"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "ForkedFromProjectPath|Forked from"
+msgstr ""
+
+msgid "ForkedFromProjectPath|Forked from %{project_name} (deleted)"
+msgstr ""
+
+msgid "Forking in progress"
+msgstr ""
+
+msgid "Format"
+msgstr ""
+
+msgid "Found errors in your .gitlab-ci.yml:"
+msgstr ""
+
+msgid "From %{provider_title}"
+msgstr ""
+
+msgid "From issue creation until deploy to production"
+msgstr ""
+
+msgid "From merge request merge until deploy to production"
+msgstr ""
+
+msgid "From the Kubernetes cluster details view, install Runner from the applications list"
+msgstr ""
+
+msgid "GPG Keys"
+msgstr ""
+
+msgid "General"
+msgstr ""
+
+msgid "General pipelines"
+msgstr ""
+
+msgid "Generate a default set of labels"
+msgstr ""
+
+msgid "Git repository URL"
+msgstr ""
+
+msgid "Git revision"
+msgstr ""
+
+msgid "Git storage health information has been reset"
+msgstr ""
+
+msgid "Git strategy for pipelines"
+msgstr ""
+
+msgid "Git version"
+msgstr ""
+
+msgid "GitHub import"
+msgstr ""
+
+msgid "GitLab CI Linter has been moved"
+msgstr ""
+
+msgid "GitLab Group Runners can execute code for all the projects in this group."
+msgstr ""
+
+msgid "GitLab Runner section"
+msgstr ""
+
+msgid "Gitaly"
+msgstr ""
+
+msgid "Gitaly Servers"
+msgstr ""
+
+msgid "Gitaly|Address"
+msgstr ""
+
+msgid "Go Back"
+msgstr ""
+
+msgid "Go back"
+msgstr ""
+
+msgid "Go to your fork"
+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 "Got it!"
+msgstr ""
+
+msgid "Graph"
+msgstr ""
+
+msgid "Group CI/CD settings"
+msgstr ""
+
+msgid "Group ID"
+msgstr ""
+
+msgid "Group Runners"
+msgstr ""
+
+msgid "Group maintainers can register group runners in the %{link}"
+msgstr ""
+
+msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
+msgstr ""
+
+msgid "GroupSettings|Share with group lock"
+msgstr ""
+
+msgid "GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup."
+msgstr ""
+
+msgid "GroupSettings|This setting is applied on %{ancestor_group}. To share projects in this group with another group, ask the owner to override the setting or %{remove_ancestor_share_with_group_lock}."
+msgstr ""
+
+msgid "GroupSettings|This setting is applied on %{ancestor_group}. You can override the setting or %{remove_ancestor_share_with_group_lock}."
+msgstr ""
+
+msgid "GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner. Groups that already have access to the project will continue to have access unless removed manually."
+msgstr ""
+
+msgid "GroupSettings|cannot be disabled when the parent group \"Share with group lock\" is enabled, except by the owner of the parent group"
+msgstr ""
+
+msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
+msgstr ""
+
+msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
+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 "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 ""
+
+msgid "Health information can be retrieved from the following endpoints. More information is available"
+msgstr ""
+
+msgid "HealthCheck|Access token is"
+msgstr ""
+
+msgid "HealthCheck|Healthy"
+msgstr ""
+
+msgid "HealthCheck|No Health Problems Detected"
+msgstr ""
+
+msgid "HealthCheck|Unhealthy"
+msgstr ""
+
+msgid "Help"
+msgstr ""
+
+msgid "Help page"
+msgstr ""
+
+msgid "Help page text and support page url."
+msgstr ""
+
+msgid "Hide value"
+msgid_plural "Hide values"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Hide whitespace changes"
+msgstr ""
+
+msgid "History"
+msgstr ""
+
+msgid "Housekeeping successfully started"
+msgstr ""
+
+msgid "I accept the %{terms_link}"
+msgstr ""
+
+msgid "I accept the|Terms of Service and Privacy Policy"
+msgstr ""
+
+msgid "ID"
+msgstr ""
+
+msgid "IDE|Commit"
+msgstr ""
+
+msgid "IDE|Edit"
+msgstr ""
+
+msgid "IDE|Go back"
+msgstr ""
+
+msgid "IDE|Open in file view"
+msgstr ""
+
+msgid "IDE|Review"
+msgstr ""
+
+msgid "Identifier"
+msgstr ""
+
+msgid "Identities"
+msgstr ""
+
+msgid "If disabled, the access level will depend on the user's permissions in the project."
+msgstr ""
+
+msgid "If enabled"
+msgstr ""
+
+msgid "If you already have files you can push them using the %{link_to_cli} below."
+msgstr ""
+
+msgid "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>."
+msgstr ""
+
+msgid "ImageDiffViewer|2-up"
+msgstr ""
+
+msgid "ImageDiffViewer|Onion skin"
+msgstr ""
+
+msgid "ImageDiffViewer|Swipe"
+msgstr ""
+
+msgid "Import"
+msgstr ""
+
+msgid "Import all repositories"
+msgstr ""
+
+msgid "Import in progress"
+msgstr ""
+
+msgid "Import repositories from GitHub"
+msgstr ""
+
+msgid "Import repository"
+msgstr ""
+
+msgid "Include a Terms of Service agreement and Privacy Policy that all users must accept."
+msgstr ""
+
+msgid "Inline"
+msgstr ""
+
+msgid "Install Runner on Kubernetes"
+msgstr ""
+
+msgid "Install a Runner compatible with GitLab CI"
+msgstr ""
+
+msgid "Instance does not support multiple Kubernetes clusters"
+msgstr ""
+
+msgid "Integrations"
+msgstr ""
+
+msgid "Integrations Settings"
+msgstr ""
+
+msgid "Interested parties can even contribute by pushing commits if they want to."
+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 ""
+
+msgid "Introducing Cycle Analytics"
+msgstr ""
+
+msgid "Issue Board"
+msgstr ""
+
+msgid "Issue events"
+msgstr ""
+
+msgid "IssueBoards|Board"
+msgstr ""
+
+msgid "Issues"
+msgstr ""
+
+msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
+msgstr ""
+
+msgid "Jan"
+msgstr ""
+
+msgid "January"
+msgstr ""
+
+msgid "Job"
+msgstr ""
+
+msgid "Job has been erased"
+msgstr ""
+
+msgid "Jobs"
+msgstr ""
+
+msgid "Jul"
+msgstr ""
+
+msgid "July"
+msgstr ""
+
+msgid "Jun"
+msgstr ""
+
+msgid "June"
+msgstr ""
+
+msgid "Koding"
+msgstr ""
+
+msgid "Kubernetes"
+msgstr ""
+
+msgid "Kubernetes Cluster"
+msgstr ""
+
+msgid "Kubernetes cluster creation time exceeds timeout; %{timeout}"
+msgstr ""
+
+msgid "Kubernetes cluster integration was not removed."
+msgstr ""
+
+msgid "Kubernetes cluster integration was successfully removed."
+msgstr ""
+
+msgid "Kubernetes cluster was successfully updated."
+msgstr ""
+
+msgid "Kubernetes configured"
+msgstr ""
+
+msgid "Kubernetes service integration has been deprecated. %{deprecated_message_content} your Kubernetes clusters using the new <a href=\"%{url}\"/>Kubernetes Clusters</a> page"
+msgstr ""
+
+msgid "LFS"
+msgstr ""
+
+msgid "LFSStatus|Disabled"
+msgstr ""
+
+msgid "LFSStatus|Enabled"
+msgstr ""
+
+msgid "Label"
+msgstr ""
+
+msgid "Label actions dropdown"
+msgstr ""
+
+msgid "LabelSelect|%{firstLabelName} +%{remainingLabelCount} more"
+msgstr ""
+
+msgid "LabelSelect|%{labelsString}, and %{remainingLabelCount} more"
+msgstr ""
+
+msgid "Labels"
+msgstr ""
+
+msgid "Labels can be applied to %{features}. Group labels are available for any project within the group."
+msgstr ""
+
+msgid "Labels can be applied to issues and merge requests to categorize them."
+msgstr ""
+
+msgid "Labels can be applied to issues and merge requests."
+msgstr ""
+
+msgid "Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>"
+msgstr ""
+
+msgid "Labels|Promote Label"
+msgstr ""
+
+msgid "Last %d day"
+msgid_plural "Last %d days"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Last Pipeline"
+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 ""
+
+msgid "LastPushEvent|at"
+msgstr ""
+
+msgid "Latest changes"
+msgstr ""
+
+msgid "Learn more"
+msgstr ""
+
+msgid "Learn more about Kubernetes"
+msgstr ""
+
+msgid "Learn more about protected branches"
+msgstr ""
+
+msgid "Learn more in the"
+msgstr ""
+
+msgid "Learn more in the|pipeline schedules documentation"
+msgstr ""
+
+msgid "Leave"
+msgstr ""
+
+msgid "Leave group"
+msgstr ""
+
+msgid "Leave project"
+msgstr ""
+
+msgid "List"
+msgstr ""
+
+msgid "List your GitHub repositories"
+msgstr ""
+
+msgid "Loading the GitLab IDE..."
+msgstr ""
+
+msgid "Loading..."
+msgstr ""
+
+msgid "Lock"
+msgstr ""
+
+msgid "Lock %{issuableDisplayName}"
+msgstr ""
+
+msgid "Lock not found"
+msgstr ""
+
+msgid "Lock to current projects"
+msgstr ""
+
+msgid "Locked"
+msgstr ""
+
+msgid "Locked to current projects"
+msgstr ""
+
+msgid "Login"
+msgstr ""
+
+msgid "Manage all notifications"
+msgstr ""
+
+msgid "Manage group labels"
+msgstr ""
+
+msgid "Manage labels"
+msgstr ""
+
+msgid "Manage project labels"
+msgstr ""
+
+msgid "Mar"
+msgstr ""
+
+msgid "March"
+msgstr ""
+
+msgid "Mark todo as done"
+msgstr ""
+
+msgid "Markdown enabled"
+msgstr ""
+
+msgid "Maximum git storage failures"
+msgstr ""
+
+msgid "May"
+msgstr ""
+
+msgid "Median"
+msgstr ""
+
+msgid "Members"
+msgstr ""
+
+msgid "Merge Request:"
+msgstr ""
+
+msgid "Merge Requests"
+msgstr ""
+
+msgid "Merge events"
+msgstr ""
+
+msgid "Merge request"
+msgstr ""
+
+msgid "Merge requests"
+msgstr ""
+
+msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
+msgstr ""
+
+msgid "MergeRequests|Resolve this discussion in a new issue"
+msgstr ""
+
+msgid "MergeRequests|Saving the comment failed"
+msgstr ""
+
+msgid "MergeRequests|Toggle comments for this file"
+msgstr ""
+
+msgid "MergeRequests|Updating discussions failed"
+msgstr ""
+
+msgid "MergeRequests|View file @ %{commitId}"
+msgstr ""
+
+msgid "MergeRequests|View replaced file @ %{commitId}"
+msgstr ""
+
+msgid "Merged"
+msgstr ""
+
+msgid "Messages"
+msgstr ""
+
+msgid "Metrics - Influx"
+msgstr ""
+
+msgid "Metrics - Prometheus"
+msgstr ""
+
+msgid "Milestone"
+msgstr ""
+
+msgid "Milestones"
+msgstr ""
+
+msgid "Milestones|Delete milestone"
+msgstr ""
+
+msgid "Milestones|Delete milestone %{milestoneTitle}?"
+msgstr ""
+
+msgid "Milestones|Failed to delete milestone %{milestoneTitle}"
+msgstr ""
+
+msgid "Milestones|Milestone %{milestoneTitle} was not found"
+msgstr ""
+
+msgid "Milestones|Promote %{milestoneTitle} to group milestone?"
+msgstr ""
+
+msgid "Milestones|Promote Milestone"
+msgstr ""
+
+msgid "MissingSSHKeyWarningLink|add an SSH key"
+msgstr ""
+
+msgid "Modal|Cancel"
+msgstr ""
+
+msgid "Modal|Close"
+msgstr ""
+
+msgid "Monitoring"
+msgstr ""
+
+msgid "More actions"
+msgstr ""
+
+msgid "More information"
+msgstr ""
+
+msgid "More information is available|here"
+msgstr ""
+
+msgid "Move"
+msgstr ""
+
+msgid "Move issue"
+msgstr ""
+
+msgid "Name"
+msgstr ""
+
+msgid "Name new label"
+msgstr ""
+
+msgid "Name your individual key via a title"
+msgstr ""
+
+msgid "Nav|Help"
+msgstr ""
+
+msgid "Nav|Home"
+msgstr ""
+
+msgid "Nav|Sign In / Register"
+msgstr ""
+
+msgid "Nav|Sign out and sign in with a different account"
+msgstr ""
+
+msgid "New Identity"
+msgstr ""
+
+msgid "New Issue"
+msgid_plural "New Issues"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "New Kubernetes Cluster"
+msgstr ""
+
+msgid "New Kubernetes cluster"
+msgstr ""
+
+msgid "New Label"
+msgstr ""
+
+msgid "New Pipeline Schedule"
+msgstr ""
+
+msgid "New branch"
+msgstr ""
+
+msgid "New branch unavailable"
+msgstr ""
+
+msgid "New directory"
+msgstr ""
+
+msgid "New file"
+msgstr ""
+
+msgid "New group"
+msgstr ""
+
+msgid "New identity"
+msgstr ""
+
+msgid "New issue"
+msgstr ""
+
+msgid "New label"
+msgstr ""
+
+msgid "New merge request"
+msgstr ""
+
+msgid "New pipelines will cancel older, pending pipelines on the same branch"
+msgstr ""
+
+msgid "New project"
+msgstr ""
+
+msgid "New schedule"
+msgstr ""
+
+msgid "New snippet"
+msgstr ""
+
+msgid "New subgroup"
+msgstr ""
+
+msgid "New tag"
+msgstr ""
+
+msgid "No"
+msgstr ""
+
+msgid "No assignee"
+msgstr ""
+
+msgid "No changes"
+msgstr ""
+
+msgid "No connection could be made to a Gitaly Server, please check your logs!"
+msgstr ""
+
+msgid "No due date"
+msgstr ""
+
+msgid "No estimate or time spent"
+msgstr ""
+
+msgid "No file chosen"
+msgstr ""
+
+msgid "No files found"
+msgstr ""
+
+msgid "No files found."
+msgstr ""
+
+msgid "No merge requests found"
+msgstr ""
+
+msgid "No messages were logged"
+msgstr ""
+
+msgid "No repository"
+msgstr ""
+
+msgid "No schedules"
+msgstr ""
+
+msgid "None"
+msgstr ""
+
+msgid "Not allowed to merge"
+msgstr ""
+
+msgid "Not available"
+msgstr ""
+
+msgid "Not available for private projects"
+msgstr ""
+
+msgid "Not available for protected branches"
+msgstr ""
+
+msgid "Not confidential"
+msgstr ""
+
+msgid "Not enough data"
+msgstr ""
+
+msgid "Note that the master branch is automatically protected. %{link_to_protected_branches}"
+msgstr ""
+
+msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
+msgstr ""
+
+msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
+msgstr ""
+
+msgid "Notification events"
+msgstr ""
+
+msgid "NotificationEvent|Close issue"
+msgstr ""
+
+msgid "NotificationEvent|Close merge request"
+msgstr ""
+
+msgid "NotificationEvent|Failed pipeline"
+msgstr ""
+
+msgid "NotificationEvent|Merge merge request"
+msgstr ""
+
+msgid "NotificationEvent|New issue"
+msgstr ""
+
+msgid "NotificationEvent|New merge request"
+msgstr ""
+
+msgid "NotificationEvent|New note"
+msgstr ""
+
+msgid "NotificationEvent|Reassign issue"
+msgstr ""
+
+msgid "NotificationEvent|Reassign merge request"
+msgstr ""
+
+msgid "NotificationEvent|Reopen issue"
+msgstr ""
+
+msgid "NotificationEvent|Successful pipeline"
+msgstr ""
+
+msgid "NotificationLevel|Custom"
+msgstr ""
+
+msgid "NotificationLevel|Disabled"
+msgstr ""
+
+msgid "NotificationLevel|Global"
+msgstr ""
+
+msgid "NotificationLevel|On mention"
+msgstr ""
+
+msgid "NotificationLevel|Participate"
+msgstr ""
+
+msgid "NotificationLevel|Watch"
+msgstr ""
+
+msgid "Notifications"
+msgstr ""
+
+msgid "Notifications off"
+msgstr ""
+
+msgid "Notifications on"
+msgstr ""
+
+msgid "Nov"
+msgstr ""
+
+msgid "November"
+msgstr ""
+
+msgid "Number of access attempts"
+msgstr ""
+
+msgid "Oct"
+msgstr ""
+
+msgid "October"
+msgstr ""
+
+msgid "OfSearchInADropdown|Filter"
+msgstr ""
+
+msgid "Online IDE integration settings."
+msgstr ""
+
+msgid "Only comments from the following commit are shown below"
+msgstr ""
+
+msgid "Only project members can comment."
+msgstr ""
+
+msgid "Open in Xcode"
+msgstr ""
+
+msgid "OpenedNDaysAgo|Opened"
+msgstr ""
+
+msgid "Opens in a new window"
+msgstr ""
+
+msgid "Operations"
+msgstr ""
+
+msgid "Options"
+msgstr ""
+
+msgid "Or you can choose one of the suggested colors below"
+msgstr ""
+
+msgid "Other Labels"
+msgstr ""
+
+msgid "Otherwise it is recommended you start with one of the options below."
+msgstr ""
+
+msgid "Outbound requests"
+msgstr ""
+
+msgid "Overview"
+msgstr ""
+
+msgid "Owner"
+msgstr ""
+
+msgid "Pages"
+msgstr ""
+
+msgid "Pagination|Last »"
+msgstr ""
+
+msgid "Pagination|Next"
+msgstr ""
+
+msgid "Pagination|Prev"
+msgstr ""
+
+msgid "Pagination|« First"
+msgstr ""
+
+msgid "Part of merge request changes"
+msgstr ""
+
+msgid "Password"
+msgstr ""
+
+msgid "Paste your public SSH key, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'. Don't use your private SSH key."
+msgstr ""
+
+msgid "Pause"
+msgstr ""
+
+msgid "Pending"
+msgstr ""
+
+msgid "Per job. If a job passes this threshold, it will be marked as failed"
+msgstr ""
+
+msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgstr ""
+
+msgid "Performance optimization"
+msgstr ""
+
+msgid "Permissions"
+msgstr ""
+
+msgid "Personal Access Token"
+msgstr ""
+
+msgid "Pipeline"
+msgstr ""
+
+msgid "Pipeline Health"
+msgstr ""
+
+msgid "Pipeline Schedule"
+msgstr ""
+
+msgid "Pipeline Schedules"
+msgstr ""
+
+msgid "Pipeline triggers"
+msgstr ""
+
+msgid "PipelineCharts|Failed:"
+msgstr ""
+
+msgid "PipelineCharts|Overall statistics"
+msgstr ""
+
+msgid "PipelineCharts|Success ratio:"
+msgstr ""
+
+msgid "PipelineCharts|Successful:"
+msgstr ""
+
+msgid "PipelineCharts|Total:"
+msgstr ""
+
+msgid "PipelineSchedules|Activated"
+msgstr ""
+
+msgid "PipelineSchedules|Active"
+msgstr ""
+
+msgid "PipelineSchedules|All"
+msgstr ""
+
+msgid "PipelineSchedules|Inactive"
+msgstr ""
+
+msgid "PipelineSchedules|Next Run"
+msgstr ""
+
+msgid "PipelineSchedules|None"
+msgstr ""
+
+msgid "PipelineSchedules|Provide a short description for this pipeline"
+msgstr ""
+
+msgid "PipelineSchedules|Take ownership"
+msgstr ""
+
+msgid "PipelineSchedules|Target"
+msgstr ""
+
+msgid "PipelineSchedules|Variables"
+msgstr ""
+
+msgid "PipelineSheduleIntervalPattern|Custom"
+msgstr ""
+
+msgid "Pipelines"
+msgstr ""
+
+msgid "Pipelines charts"
+msgstr ""
+
+msgid "Pipelines for last month"
+msgstr ""
+
+msgid "Pipelines for last week"
+msgstr ""
+
+msgid "Pipelines for last year"
+msgstr ""
+
+msgid "Pipelines|Build with confidence"
+msgstr ""
+
+msgid "Pipelines|CI Lint"
+msgstr ""
+
+msgid "Pipelines|Clear Runner Caches"
+msgstr ""
+
+msgid "Pipelines|Get started with Pipelines"
+msgstr ""
+
+msgid "Pipelines|Loading Pipelines"
+msgstr ""
+
+msgid "Pipelines|Project cache successfully reset."
+msgstr ""
+
+msgid "Pipelines|Run Pipeline"
+msgstr ""
+
+msgid "Pipelines|Something went wrong while cleaning runners cache."
+msgstr ""
+
+msgid "Pipelines|There are currently no %{scope} pipelines."
+msgstr ""
+
+msgid "Pipelines|There are currently no pipelines."
+msgstr ""
+
+msgid "Pipelines|This project is not currently set up to run pipelines."
+msgstr ""
+
+msgid "Pipeline|Create for"
+msgstr ""
+
+msgid "Pipeline|Create pipeline"
+msgstr ""
+
+msgid "Pipeline|Existing branch name or tag"
+msgstr ""
+
+msgid "Pipeline|Run Pipeline"
+msgstr ""
+
+msgid "Pipeline|Search branches"
+msgstr ""
+
+msgid "Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default."
+msgstr ""
+
+msgid "Pipeline|Stop pipeline"
+msgstr ""
+
+msgid "Pipeline|Stop pipeline #%{pipelineId}?"
+msgstr ""
+
+msgid "Pipeline|Variables"
+msgstr ""
+
+msgid "Pipeline|You’re about to stop pipeline %{pipelineId}."
+msgstr ""
+
+msgid "Pipeline|all"
+msgstr ""
+
+msgid "Pipeline|success"
+msgstr ""
+
+msgid "Pipeline|with stage"
+msgstr ""
+
+msgid "Pipeline|with stages"
+msgstr ""
+
+msgid "Plain diff"
+msgstr ""
+
+msgid "PlantUML"
+msgstr ""
+
+msgid "Play"
+msgstr ""
+
+msgid "Please accept the Terms of Service before continuing."
+msgstr ""
+
+msgid "Please select at least one filter to see results"
+msgstr ""
+
+msgid "Please solve the reCAPTCHA"
+msgstr ""
+
+msgid "Please try again"
+msgstr ""
+
+msgid "Please wait while we import the repository for you. Refresh at will."
+msgstr ""
+
+msgid "Preferences"
+msgstr ""
+
+msgid "Preferences|Navigation theme"
+msgstr ""
+
+msgid "Prioritize"
+msgstr ""
+
+msgid "Prioritize label"
+msgstr ""
+
+msgid "Prioritized Labels"
+msgstr ""
+
+msgid "Prioritized label"
+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 "Private projects can be created in your personal namespace with:"
+msgstr ""
+
+msgid "Profile"
+msgstr ""
+
+msgid "Profiles|Account scheduled for removal."
+msgstr ""
+
+msgid "Profiles|Change username"
+msgstr ""
+
+msgid "Profiles|Current path: %{path}"
+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|Path"
+msgstr ""
+
+msgid "Profiles|Type your %{confirmationValue} to confirm:"
+msgstr ""
+
+msgid "Profiles|Update username"
+msgstr ""
+
+msgid "Profiles|Username change failed - %{message}"
+msgstr ""
+
+msgid "Profiles|Username successfully changed"
+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 "Profiling - Performance bar"
+msgstr ""
+
+msgid "Programming languages used in this repository"
+msgstr ""
+
+msgid "Progress"
+msgstr ""
+
+msgid "Project"
+msgstr ""
+
+msgid "Project '%{project_name}' is in the process of being deleted."
+msgstr ""
+
+msgid "Project '%{project_name}' queued for deletion."
+msgstr ""
+
+msgid "Project '%{project_name}' was successfully created."
+msgstr ""
+
+msgid "Project '%{project_name}' was successfully updated."
+msgstr ""
+
+msgid "Project Badges"
+msgstr ""
+
+msgid "Project access must be granted explicitly to each user."
+msgstr ""
+
+msgid "Project avatar"
+msgstr ""
+
+msgid "Project avatar in repository: %{link}"
+msgstr ""
+
+msgid "Project details"
+msgstr ""
+
+msgid "Project export could not be deleted."
+msgstr ""
+
+msgid "Project export has been deleted."
+msgstr ""
+
+msgid "Project export link has expired. Please generate a new export from your project settings."
+msgstr ""
+
+msgid "Project export started. A download link will be sent by email."
+msgstr ""
+
+msgid "ProjectActivityRSS|Subscribe"
+msgstr ""
+
+msgid "ProjectFileTree|Name"
+msgstr ""
+
+msgid "ProjectLastActivity|Never"
+msgstr ""
+
+msgid "ProjectLifecycle|Stage"
+msgstr ""
+
+msgid "Projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Frequently visited"
+msgstr ""
+
+msgid "ProjectsDropdown|Loading projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Projects you visit often will appear here"
+msgstr ""
+
+msgid "ProjectsDropdown|Search your projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Something went wrong on our end."
+msgstr ""
+
+msgid "ProjectsDropdown|Sorry, no projects matched your search"
+msgstr ""
+
+msgid "ProjectsDropdown|This feature requires browser localStorage support"
+msgstr ""
+
+msgid "PrometheusDashboard|Time"
+msgstr ""
+
+msgid "PrometheusService|%{exporters} with %{metrics} were found"
+msgstr ""
+
+msgid "PrometheusService|<p class=\"text-tertiary\">No <a href=\"%{docsUrl}\">common metrics</a> were found</p>"
+msgstr ""
+
+msgid "PrometheusService|Active"
+msgstr ""
+
+msgid "PrometheusService|Auto configuration"
+msgstr ""
+
+msgid "PrometheusService|Automatically deploy and configure Prometheus on your clusters to monitor your project’s environments"
+msgstr ""
+
+msgid "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."
+msgstr ""
+
+msgid "PrometheusService|Common metrics"
+msgstr ""
+
+msgid "PrometheusService|Finding and configuring metrics..."
+msgstr ""
+
+msgid "PrometheusService|Install Prometheus on clusters"
+msgstr ""
+
+msgid "PrometheusService|Manage clusters"
+msgstr ""
+
+msgid "PrometheusService|Manual configuration"
+msgstr ""
+
+msgid "PrometheusService|Metrics"
+msgstr ""
+
+msgid "PrometheusService|Metrics are automatically configured and monitored based on a library of metrics from popular exporters."
+msgstr ""
+
+msgid "PrometheusService|Missing environment variable"
+msgstr ""
+
+msgid "PrometheusService|More information"
+msgstr ""
+
+msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
+msgstr ""
+
+msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
+msgstr ""
+
+msgid "PrometheusService|Time-series monitoring service"
+msgstr ""
+
+msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgstr ""
+
+msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
+msgstr ""
+
+msgid "PrometheusService|Waiting for your first deployment to an environment to find common metrics"
+msgstr ""
+
+msgid "Promote"
+msgstr ""
+
+msgid "Promote these project milestones into a group milestone."
+msgstr ""
+
+msgid "Promote to Group Milestone"
+msgstr ""
+
+msgid "Promote to group label"
+msgstr ""
+
+msgid "Protip:"
+msgstr ""
+
+msgid "Provider"
+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 "Public pipelines"
+msgstr ""
+
+msgid "Push events"
+msgstr ""
+
+msgid "Push project from command line"
+msgstr ""
+
+msgid "Push to create a project"
+msgstr ""
+
+msgid "Quick actions can be used in the issues description and comment boxes."
+msgstr ""
+
+msgid "Re-deploy"
+msgstr ""
+
+msgid "Read more"
+msgstr ""
+
+msgid "Readme"
+msgstr ""
+
+msgid "Real-time features"
+msgstr ""
+
+msgid "Reference:"
+msgstr ""
+
+msgid "Register / Sign In"
+msgstr ""
+
+msgid "Register and see your runners for this group."
+msgstr ""
+
+msgid "Register and see your runners for this project."
+msgstr ""
+
+msgid "Registry"
+msgstr ""
+
+msgid "Related Commits"
+msgstr ""
+
+msgid "Related Deployed Jobs"
+msgstr ""
+
+msgid "Related Issues"
+msgstr ""
+
+msgid "Related Jobs"
+msgstr ""
+
+msgid "Related Merge Requests"
+msgstr ""
+
+msgid "Related Merged Requests"
+msgstr ""
+
+msgid "Related merge requests"
+msgstr ""
+
+msgid "Remind later"
+msgstr ""
+
+msgid "Remove"
+msgstr ""
+
+msgid "Remove Runner"
+msgstr ""
+
+msgid "Remove avatar"
+msgstr ""
+
+msgid "Remove priority"
+msgstr ""
+
+msgid "Remove project"
+msgstr ""
+
+msgid "Repository"
+msgstr ""
+
+msgid "Repository Settings"
+msgstr ""
+
+msgid "Repository maintenance"
+msgstr ""
+
+msgid "Repository mirror"
+msgstr ""
+
+msgid "Repository storage"
+msgstr ""
+
+msgid "Request Access"
+msgstr ""
+
+msgid "Require all users to accept Terms of Service and Privacy Policy when they access GitLab."
+msgstr ""
+
+msgid "Reset git storage health information"
+msgstr ""
+
+msgid "Reset health check access token"
+msgstr ""
+
+msgid "Reset runners registration token"
+msgstr ""
+
+msgid "Resolve all discussions in new issue"
+msgstr ""
+
+msgid "Resolve conflicts on source branch"
+msgstr ""
+
+msgid "Resolve discussion"
+msgstr ""
+
+msgid "Resume"
+msgstr ""
+
+msgid "Retry"
+msgstr ""
+
+msgid "Retry this job"
+msgstr ""
+
+msgid "Retry verification"
+msgstr ""
+
+msgid "Reveal value"
+msgid_plural "Reveal values"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Revert this commit"
+msgstr ""
+
+msgid "Revert this merge request"
+msgstr ""
+
+msgid "Review"
+msgstr ""
+
+msgid "Reviewing"
+msgstr ""
+
+msgid "Reviewing (merge request !%{mergeRequestId})"
+msgstr ""
+
+msgid "Rollback"
+msgstr ""
+
+msgid "Runner token"
+msgstr ""
+
+msgid "Runners"
+msgstr ""
+
+msgid "Runners API"
+msgstr ""
+
+msgid "Runners can be placed on separate users, servers, and even on your local machine."
+msgstr ""
+
+msgid "Running"
+msgstr ""
+
+msgid "SSH Keys"
+msgstr ""
+
+msgid "SSL Verification"
+msgstr ""
+
+msgid "Save"
+msgstr ""
+
+msgid "Save changes"
+msgstr ""
+
+msgid "Save pipeline schedule"
+msgstr ""
+
+msgid "Save variables"
+msgstr ""
+
+msgid "Schedule a new pipeline"
+msgstr ""
+
+msgid "Scheduled"
+msgstr ""
+
+msgid "Schedules"
+msgstr ""
+
+msgid "Scheduling Pipelines"
+msgstr ""
+
+msgid "Scroll to bottom"
+msgstr ""
+
+msgid "Scroll to top"
+msgstr ""
+
+msgid "Search"
+msgstr ""
+
+msgid "Search branches"
+msgstr ""
+
+msgid "Search branches and tags"
+msgstr ""
+
+msgid "Search files"
+msgstr ""
+
+msgid "Search for projects, issues, etc."
+msgstr ""
+
+msgid "Search merge requests"
+msgstr ""
+
+msgid "Search milestones"
+msgstr ""
+
+msgid "Search project"
+msgstr ""
+
+msgid "Search users"
+msgstr ""
+
+msgid "Seconds before reseting failure information"
+msgstr ""
+
+msgid "Seconds to wait for a storage access attempt"
+msgstr ""
+
+msgid "Select"
+msgstr ""
+
+msgid "Select Archive Format"
+msgstr ""
+
+msgid "Select a namespace to fork the project"
+msgstr ""
+
+msgid "Select a timezone"
+msgstr ""
+
+msgid "Select an existing Kubernetes cluster or create a new one"
+msgstr ""
+
+msgid "Select assignee"
+msgstr ""
+
+msgid "Select branch/tag"
+msgstr ""
+
+msgid "Select project"
+msgstr ""
+
+msgid "Select project and zone to choose machine type"
+msgstr ""
+
+msgid "Select project to choose zone"
+msgstr ""
+
+msgid "Select source branch"
+msgstr ""
+
+msgid "Select target branch"
+msgstr ""
+
+msgid "Send email"
+msgstr ""
+
+msgid "Sep"
+msgstr ""
+
+msgid "September"
+msgstr ""
+
+msgid "Server version"
+msgstr ""
+
+msgid "Service Templates"
+msgstr ""
+
+msgid "Session expiration, projects limit and attachment size."
+msgstr ""
+
+msgid "Set a password on your account to pull or push via %{protocol}."
+msgstr ""
+
+msgid "Set default and restrict visibility levels. Configure import sources and git access protocol."
+msgstr ""
+
+msgid "Set max session time for web terminal."
+msgstr ""
+
+msgid "Set notification email for abuse reports."
+msgstr ""
+
+msgid "Set requirements for a user to sign-in. Enable mandatory two-factor authentication."
+msgstr ""
+
+msgid "Set up CI/CD"
+msgstr ""
+
+msgid "Set up Koding"
+msgstr ""
+
+msgid "SetPasswordToCloneLink|set a password"
+msgstr ""
+
+msgid "Settings"
+msgstr ""
+
+msgid "Setup a specific Runner automatically"
+msgstr ""
+
+msgid "Share"
+msgstr ""
+
+msgid "Shared Runners"
+msgstr ""
+
+msgid "Show command"
+msgstr ""
+
+msgid "Show complete raw log"
+msgstr ""
+
+msgid "Show latest version"
+msgstr ""
+
+msgid "Show latest version of the diff"
+msgstr ""
+
+msgid "Show parent pages"
+msgstr ""
+
+msgid "Show parent subgroups"
+msgstr ""
+
+msgid "Show whitespace changes"
+msgstr ""
+
+msgid "Showing %d event"
+msgid_plural "Showing %d events"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Side-by-side"
+msgstr ""
+
+msgid "Sign out"
+msgstr ""
+
+msgid "Sign-in restrictions"
+msgstr ""
+
+msgid "Sign-up restrictions"
+msgstr ""
+
+msgid "Size and domain settings for static websites"
+msgstr ""
+
+msgid "Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job"
+msgstr ""
+
+msgid "Snippets"
+msgstr ""
+
+msgid "Something went wrong on our end"
+msgstr ""
+
+msgid "Something went wrong on our end."
+msgstr ""
+
+msgid "Something went wrong on our end. Please try again!"
+msgstr ""
+
+msgid "Something went wrong when toggling the button"
+msgstr ""
+
+msgid "Something went wrong while closing the %{issuable}. Please try again later"
+msgstr ""
+
+msgid "Something went wrong while fetching the projects."
+msgstr ""
+
+msgid "Something went wrong while fetching the registry list."
+msgstr ""
+
+msgid "Something went wrong while reopening the %{issuable}. Please try again later"
+msgstr ""
+
+msgid "Something went wrong while resolving this discussion. Please try again."
+msgstr ""
+
+msgid "Something went wrong. Please try again."
+msgstr ""
+
+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 ""
+
+msgid "SortOptions|Due later"
+msgstr ""
+
+msgid "SortOptions|Due soon"
+msgstr ""
+
+msgid "SortOptions|Label priority"
+msgstr ""
+
+msgid "SortOptions|Largest group"
+msgstr ""
+
+msgid "SortOptions|Largest repository"
+msgstr ""
+
+msgid "SortOptions|Last created"
+msgstr ""
+
+msgid "SortOptions|Last joined"
+msgstr ""
+
+msgid "SortOptions|Last updated"
+msgstr ""
+
+msgid "SortOptions|Least popular"
+msgstr ""
+
+msgid "SortOptions|Milestone"
+msgstr ""
+
+msgid "SortOptions|Milestone due later"
+msgstr ""
+
+msgid "SortOptions|Milestone due soon"
+msgstr ""
+
+msgid "SortOptions|Most popular"
+msgstr ""
+
+msgid "SortOptions|Name"
+msgstr ""
+
+msgid "SortOptions|Name, ascending"
+msgstr ""
+
+msgid "SortOptions|Name, descending"
+msgstr ""
+
+msgid "SortOptions|Oldest created"
+msgstr ""
+
+msgid "SortOptions|Oldest joined"
+msgstr ""
+
+msgid "SortOptions|Oldest sign in"
+msgstr ""
+
+msgid "SortOptions|Oldest updated"
+msgstr ""
+
+msgid "SortOptions|Popularity"
+msgstr ""
+
+msgid "SortOptions|Priority"
+msgstr ""
+
+msgid "SortOptions|Recent sign in"
+msgstr ""
+
+msgid "SortOptions|Start later"
+msgstr ""
+
+msgid "SortOptions|Start soon"
+msgstr ""
+
+msgid "Source"
+msgstr ""
+
+msgid "Source (branch or tag)"
+msgstr ""
+
+msgid "Source code"
+msgstr ""
+
+msgid "Source is not available"
+msgstr ""
+
+msgid "Spam Logs"
+msgstr ""
+
+msgid "Spam and Anti-bot Protection"
+msgstr ""
+
+msgid "Specific Runners"
+msgstr ""
+
+msgid "Specify the following URL during the Runner setup:"
+msgstr ""
+
+msgid "Squash commits"
+msgstr ""
+
+msgid "Stage"
+msgstr ""
+
+msgid "Stage & Commit"
+msgstr ""
+
+msgid "Stage all changes"
+msgstr ""
+
+msgid "Stage changes"
+msgstr ""
+
+msgid "Staged"
+msgstr ""
+
+msgid "Staged %{type}"
+msgstr ""
+
+msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
+msgstr ""
+
+msgid "StarProject|Star"
+msgstr ""
+
+msgid "Starred Projects"
+msgstr ""
+
+msgid "Starred Projects' Activity"
+msgstr ""
+
+msgid "Starred projects"
+msgstr ""
+
+msgid "Start a %{new_merge_request} with these changes"
+msgstr ""
+
+msgid "Start the Runner!"
+msgstr ""
+
+msgid "Started"
+msgstr ""
+
+msgid "Starts at (UTC)"
+msgstr ""
+
+msgid "Status"
+msgstr ""
+
+msgid "Stop this environment"
+msgstr ""
+
+msgid "Stopped"
+msgstr ""
+
+msgid "Storage"
+msgstr ""
+
+msgid "Subgroups"
+msgstr ""
+
+msgid "Subscribe"
+msgstr ""
+
+msgid "Subscribe at group level"
+msgstr ""
+
+msgid "Subscribe at project level"
+msgstr ""
+
+msgid "Switch branch/tag"
+msgstr ""
+
+msgid "System Hooks"
+msgstr ""
+
+msgid "Tag (%{tag_count})"
+msgid_plural "Tags (%{tag_count})"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Tags"
+msgstr ""
+
+msgid "Tags:"
+msgstr ""
+
+msgid "TagsPage|Browse commits"
+msgstr ""
+
+msgid "TagsPage|Browse files"
+msgstr ""
+
+msgid "TagsPage|Can't find HEAD commit for this tag"
+msgstr ""
+
+msgid "TagsPage|Cancel"
+msgstr ""
+
+msgid "TagsPage|Create tag"
+msgstr ""
+
+msgid "TagsPage|Delete tag"
+msgstr ""
+
+msgid "TagsPage|Deleting the %{tag_name} tag cannot be undone. Are you sure?"
+msgstr ""
+
+msgid "TagsPage|Edit release notes"
+msgstr ""
+
+msgid "TagsPage|Existing branch name, tag, or commit SHA"
+msgstr ""
+
+msgid "TagsPage|Filter by tag name"
+msgstr ""
+
+msgid "TagsPage|New Tag"
+msgstr ""
+
+msgid "TagsPage|New tag"
+msgstr ""
+
+msgid "TagsPage|Optionally, add a message to the tag."
+msgstr ""
+
+msgid "TagsPage|Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page."
+msgstr ""
+
+msgid "TagsPage|Release notes"
+msgstr ""
+
+msgid "TagsPage|Repository has no tags yet."
+msgstr ""
+
+msgid "TagsPage|Sort by"
+msgstr ""
+
+msgid "TagsPage|Tags"
+msgstr ""
+
+msgid "TagsPage|Tags give the ability to mark specific points in history as being important"
+msgstr ""
+
+msgid "TagsPage|This tag has no release notes."
+msgstr ""
+
+msgid "TagsPage|Use git tag command to add a new one:"
+msgstr ""
+
+msgid "TagsPage|Write your release notes or drag files here…"
+msgstr ""
+
+msgid "TagsPage|protected"
+msgstr ""
+
+msgid "Target Branch"
+msgstr ""
+
+msgid "Target branch"
+msgstr ""
+
+msgid "Team"
+msgstr ""
+
+msgid "Terms of Service Agreement and Privacy Policy"
+msgstr ""
+
+msgid "Terms of Service and Privacy Policy"
+msgstr ""
+
+msgid "Test coverage parsing"
+msgstr ""
+
+msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project"
+msgstr ""
+
+msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
+msgstr ""
+
+msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request."
+msgstr ""
+
+msgid "The collection of events added to the data gathered for that stage."
+msgstr ""
+
+msgid "The fork relationship has been removed."
+msgstr ""
+
+msgid "The import will time out after %{timeout}. For repositories that take longer, use a clone/push combination."
+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 maximum file size allowed is 200KB."
+msgstr ""
+
+msgid "The number of attempts GitLab will make to access a storage."
+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 path to CI config file. Defaults to <code>.gitlab-ci.yml</code>"
+msgstr ""
+
+msgid "The phase of the development lifecycle."
+msgstr ""
+
+msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit."
+msgstr ""
+
+msgid "The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle."
+msgstr ""
+
+msgid "The project can be accessed by any logged in user."
+msgstr ""
+
+msgid "The project can be accessed without any authentication."
+msgstr ""
+
+msgid "The repository for this project does not exist."
+msgstr ""
+
+msgid "The repository for this project is empty"
+msgstr ""
+
+msgid "The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>."
+msgstr ""
+
+msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request."
+msgstr ""
+
+msgid "The secure token used by the Runner to checkout the project"
+msgstr ""
+
+msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time."
+msgstr ""
+
+msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running."
+msgstr ""
+
+msgid "The time 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 in seconds between storage checks. When a previous check did complete yet, GitLab will skip a check."
+msgstr ""
+
+msgid "The time taken by each data entry gathered by that stage."
+msgstr ""
+
+msgid "The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6."
+msgstr ""
+
+msgid "There are no issues to show"
+msgstr ""
+
+msgid "There are no labels yet"
+msgstr ""
+
+msgid "There are no merge requests to show"
+msgstr ""
+
+msgid "There are problems accessing Git storage: "
+msgstr ""
+
+msgid "There was an error loading jobs"
+msgstr ""
+
+msgid "There was an error loading latest pipeline"
+msgstr ""
+
+msgid "There was an error loading users activity calendar."
+msgstr ""
+
+msgid "There was an error saving your notification settings."
+msgstr ""
+
+msgid "There was an error subscribing to this label."
+msgstr ""
+
+msgid "There was an error when reseting email token."
+msgstr ""
+
+msgid "There was an error when subscribing to this label."
+msgstr ""
+
+msgid "There was an error when unsubscribing from this label."
+msgstr ""
+
+msgid "They can be managed using the %{link}."
+msgstr ""
+
+msgid "This GitLab instance does not provide any shared Runners yet. Instance administrators can register shared Runners in the admin area."
+msgstr ""
+
+msgid "This diff is collapsed."
+msgstr ""
+
+msgid "This directory"
+msgstr ""
+
+msgid "This group does not provide any group Runners yet."
+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"
+msgstr ""
+
+msgid "This issue is confidential and locked."
+msgstr ""
+
+msgid "This issue is locked."
+msgstr ""
+
+msgid "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+msgstr ""
+
+msgid "This job depends on upstream jobs that need to succeed in order for this job to be triggered"
+msgstr ""
+
+msgid "This job does not have a trace."
+msgstr ""
+
+msgid "This job has been canceled"
+msgstr ""
+
+msgid "This job has been skipped"
+msgstr ""
+
+msgid "This job has not been triggered yet"
+msgstr ""
+
+msgid "This job has not started yet"
+msgstr ""
+
+msgid "This job is in pending state and is waiting to be picked by a runner"
+msgstr ""
+
+msgid "This job requires a manual action"
+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 "This option is disabled while you still have unstaged changes"
+msgstr ""
+
+msgid "This page is unavailable because you are not allowed to read information across multiple projects."
+msgstr ""
+
+msgid "This page will be removed in a future release."
+msgstr ""
+
+msgid "This project"
+msgstr ""
+
+msgid "This project does not belong to a group and can therefore not make use of group Runners."
+msgstr ""
+
+msgid "This repository"
+msgstr ""
+
+msgid "This source diff could not be displayed because it is too large."
+msgstr ""
+
+msgid "This user has no identities"
+msgstr ""
+
+msgid "Time before an issue gets scheduled"
+msgstr ""
+
+msgid "Time before an issue starts implementation"
+msgstr ""
+
+msgid "Time between merge request creation and merge/close"
+msgstr ""
+
+msgid "Time remaining"
+msgstr ""
+
+msgid "Time spent"
+msgstr ""
+
+msgid "Time tracking"
+msgstr ""
+
+msgid "Time until first merge request"
+msgstr ""
+
+msgid "TimeTrackingEstimated|Est"
+msgstr ""
+
+msgid "TimeTracking|Estimated:"
+msgstr ""
+
+msgid "TimeTracking|Spent"
+msgstr ""
+
+msgid "Timeago|%s days ago"
+msgstr ""
+
+msgid "Timeago|%s days remaining"
+msgstr ""
+
+msgid "Timeago|%s hours ago"
+msgstr ""
+
+msgid "Timeago|%s hours remaining"
+msgstr ""
+
+msgid "Timeago|%s minutes ago"
+msgstr ""
+
+msgid "Timeago|%s minutes remaining"
+msgstr ""
+
+msgid "Timeago|%s months ago"
+msgstr ""
+
+msgid "Timeago|%s months remaining"
+msgstr ""
+
+msgid "Timeago|%s seconds ago"
+msgstr ""
+
+msgid "Timeago|%s seconds remaining"
+msgstr ""
+
+msgid "Timeago|%s weeks ago"
+msgstr ""
+
+msgid "Timeago|%s weeks remaining"
+msgstr ""
+
+msgid "Timeago|%s years ago"
+msgstr ""
+
+msgid "Timeago|%s years remaining"
+msgstr ""
+
+msgid "Timeago|1 day ago"
+msgstr ""
+
+msgid "Timeago|1 day remaining"
+msgstr ""
+
+msgid "Timeago|1 hour ago"
+msgstr ""
+
+msgid "Timeago|1 hour remaining"
+msgstr ""
+
+msgid "Timeago|1 minute ago"
+msgstr ""
+
+msgid "Timeago|1 minute remaining"
+msgstr ""
+
+msgid "Timeago|1 month ago"
+msgstr ""
+
+msgid "Timeago|1 month remaining"
+msgstr ""
+
+msgid "Timeago|1 week ago"
+msgstr ""
+
+msgid "Timeago|1 week remaining"
+msgstr ""
+
+msgid "Timeago|1 year ago"
+msgstr ""
+
+msgid "Timeago|1 year remaining"
+msgstr ""
+
+msgid "Timeago|Past due"
+msgstr ""
+
+msgid "Timeago|in %s days"
+msgstr ""
+
+msgid "Timeago|in %s hours"
+msgstr ""
+
+msgid "Timeago|in %s minutes"
+msgstr ""
+
+msgid "Timeago|in %s months"
+msgstr ""
+
+msgid "Timeago|in %s seconds"
+msgstr ""
+
+msgid "Timeago|in %s weeks"
+msgstr ""
+
+msgid "Timeago|in %s years"
+msgstr ""
+
+msgid "Timeago|in 1 day"
+msgstr ""
+
+msgid "Timeago|in 1 hour"
+msgstr ""
+
+msgid "Timeago|in 1 minute"
+msgstr ""
+
+msgid "Timeago|in 1 month"
+msgstr ""
+
+msgid "Timeago|in 1 week"
+msgstr ""
+
+msgid "Timeago|in 1 year"
+msgstr ""
+
+msgid "Timeago|just now"
+msgstr ""
+
+msgid "Timeago|right now"
+msgstr ""
+
+msgid "Timeout"
+msgstr ""
+
+msgid "Time|hr"
+msgid_plural "Time|hrs"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Time|min"
+msgid_plural "Time|mins"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Time|s"
+msgstr ""
+
+msgid "Tip:"
+msgstr ""
+
+msgid "To GitLab"
+msgstr ""
+
+msgid "To add an SSH key you need to %{generate_link_start}generate one%{link_end} or use an %{existing_link_start}existing key%{link_end}."
+msgstr ""
+
+msgid "To import GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
+msgstr ""
+
+msgid "To import GitHub repositories, you first need to authorize GitLab to access the list of your GitHub repositories:"
+msgstr ""
+
+msgid "To import an SVN repository, check out %{svn_link}."
+msgstr ""
+
+msgid "To start serving your jobs you can add Runners to your group"
+msgstr ""
+
+msgid "To validate your GitLab CI configurations, go to 'CI/CD → Pipelines' inside your project, and click on the 'CI Lint' button."
+msgstr ""
+
+msgid "Todo"
+msgstr ""
+
+msgid "Toggle Sidebar"
+msgstr ""
+
+msgid "Toggle discussion"
+msgstr ""
+
+msgid "Toggle sidebar"
+msgstr ""
+
+msgid "ToggleButton|Toggle Status: OFF"
+msgstr ""
+
+msgid "ToggleButton|Toggle Status: ON"
+msgstr ""
+
+msgid "Too many changes to show."
+msgstr ""
+
+msgid "Total Time"
+msgstr ""
+
+msgid "Total test time for all commits/merges"
+msgstr ""
+
+msgid "Total: %{total}"
+msgstr ""
+
+msgid "Track time with quick actions"
+msgstr ""
+
+msgid "Trigger this manual action"
+msgstr ""
+
+msgid "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."
+msgstr ""
+
+msgid "Try again"
+msgstr ""
+
+msgid "Unable to load the diff. %{button_try_again}"
+msgstr ""
+
+msgid "Unlock"
+msgstr ""
+
+msgid "Unlocked"
+msgstr ""
+
+msgid "Unresolve discussion"
+msgstr ""
+
+msgid "Unstage all changes"
+msgstr ""
+
+msgid "Unstage changes"
+msgstr ""
+
+msgid "Unstaged"
+msgstr ""
+
+msgid "Unstaged %{type}"
+msgstr ""
+
+msgid "Unstaged and staged %{type}"
+msgstr ""
+
+msgid "Unstar"
+msgstr ""
+
+msgid "Unsubscribe"
+msgstr ""
+
+msgid "Unsubscribe at group level"
+msgstr ""
+
+msgid "Unsubscribe at project level"
+msgstr ""
+
+msgid "Unverified"
+msgstr ""
+
+msgid "Up to date"
+msgstr ""
+
+msgid "Update %{files}"
+msgid_plural "Update %{files} files"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Update your group name, description, avatar, and other general settings."
+msgstr ""
+
+msgid "Upload New File"
+msgstr ""
+
+msgid "Upload file"
+msgstr ""
+
+msgid "Upload new avatar"
+msgstr ""
+
+msgid "UploadLink|click to upload"
+msgstr ""
+
+msgid "Upvotes"
+msgstr ""
+
+msgid "Usage statistics"
+msgstr ""
+
+msgid "Use group milestones to manage issues from multiple projects in the same milestone."
+msgstr ""
+
+msgid "Use the following registration token during setup:"
+msgstr ""
+
+msgid "Use your global notification setting"
+msgstr ""
+
+msgid "User and IP Rate Limits"
+msgstr ""
+
+msgid "Users"
+msgstr ""
+
+msgid "Variables"
+msgstr ""
+
+msgid "Variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. You can use variables for passwords, secret keys, or whatever you want."
+msgstr ""
+
+msgid "Various container registry settings."
+msgstr ""
+
+msgid "Various email settings."
+msgstr ""
+
+msgid "Various settings that affect GitLab performance."
+msgstr ""
+
+msgid "Verified"
+msgstr ""
+
+msgid "View file @ "
+msgstr ""
+
+msgid "View group labels"
+msgstr ""
+
+msgid "View jobs"
+msgstr ""
+
+msgid "View labels"
+msgstr ""
+
+msgid "View log"
+msgstr ""
+
+msgid "View open merge request"
+msgstr ""
+
+msgid "View project labels"
+msgstr ""
+
+msgid "View replaced file @ "
+msgstr ""
+
+msgid "Visibility and access controls"
+msgstr ""
+
+msgid "VisibilityLevel|Internal"
+msgstr ""
+
+msgid "VisibilityLevel|Private"
+msgstr ""
+
+msgid "VisibilityLevel|Public"
+msgstr ""
+
+msgid "VisibilityLevel|Unknown"
+msgstr ""
+
+msgid "Want to see the data? Please ask an administrator for access."
+msgstr ""
+
+msgid "We don't have enough data to show this stage."
+msgstr ""
+
+msgid "We want to be sure it is you, please confirm you are not a robot."
+msgstr ""
+
+msgid "Web IDE"
+msgstr ""
+
+msgid "Web terminal"
+msgstr ""
+
+msgid "When a runner is locked, it cannot be assigned to other projects"
+msgstr ""
+
+msgid "When enabled, users cannot use GitLab until the terms have been accepted."
+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 "WikiEditPageTip|Tip: You can move this page by adding the path to the beginning of the title."
+msgstr ""
+
+msgid "WikiEdit|There is already a page with the same title in that path."
+msgstr ""
+
+msgid "WikiEmptyIssueMessage|Suggest wiki improvement"
+msgstr ""
+
+msgid "WikiEmptyIssueMessage|You must be a project member in order to add wiki pages. If you have suggestions for how to improve the wiki for this project, consider opening an issue in the %{issues_link}."
+msgstr ""
+
+msgid "WikiEmptyIssueMessage|issue tracker"
+msgstr ""
+
+msgid "WikiEmpty|A wiki is where you can store all the details about your project. This can include why you've created it, its principles, how to use it, and so on."
+msgstr ""
+
+msgid "WikiEmpty|Create your first page"
+msgstr ""
+
+msgid "WikiEmpty|Suggest wiki improvement"
+msgstr ""
+
+msgid "WikiEmpty|The wiki lets you write documentation for your project"
+msgstr ""
+
+msgid "WikiEmpty|This project has no wiki pages"
+msgstr ""
+
+msgid "WikiEmpty|You must be a project member in order to add 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 "WikiPageConfirmDelete|Delete page"
+msgstr ""
+
+msgid "WikiPageConfirmDelete|Delete page %{pageTitle}?"
+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|More Pages"
+msgstr ""
+
+msgid "Wiki|New page"
+msgstr ""
+
+msgid "Wiki|Page history"
+msgstr ""
+
+msgid "Wiki|Page version"
+msgstr ""
+
+msgid "Wiki|Pages"
+msgstr ""
+
+msgid "Wiki|Wiki Pages"
+msgstr ""
+
+msgid "Withdraw Access Request"
+msgstr ""
+
+msgid "Yes"
+msgstr ""
+
+msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
+msgstr ""
+
+msgid "You are going to remove %{project_full_name}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?"
+msgstr ""
+
+msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?"
+msgstr ""
+
+msgid "You are going to transfer %{project_full_name} to another owner. Are you ABSOLUTELY sure?"
+msgstr ""
+
+msgid "You are on a read-only GitLab instance."
+msgstr ""
+
+msgid "You can %{linkStart}view the blob%{linkEnd} instead."
+msgstr ""
+
+msgid "You can also create a project from the command line."
+msgstr ""
+
+msgid "You can also star a label to make it a priority label."
+msgstr ""
+
+msgid "You can also test your .gitlab-ci.yml in the %{linkStart}Lint%{linkEnd}"
+msgstr ""
+
+msgid "You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}"
+msgstr ""
+
+msgid "You can move around the graph by using the arrow keys."
+msgstr ""
+
+msgid "You can only add files when you are on a branch"
+msgstr ""
+
+msgid "You can only edit files when you are on a branch"
+msgstr ""
+
+msgid "You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}"
+msgstr ""
+
+msgid "You cannot write to this read-only GitLab instance."
+msgstr ""
+
+msgid "You do not have any assigned merge requests"
+msgstr ""
+
+msgid "You have no permissions"
+msgstr ""
+
+msgid "You have not created any merge requests"
+msgstr ""
+
+msgid "You have reached your project limit"
+msgstr ""
+
+msgid "You must accept our Terms of Service and privacy policy in order to register an account"
+msgstr ""
+
+msgid "You must have maintainer access to force delete a lock"
+msgstr ""
+
+msgid "You must sign in to star a project"
+msgstr ""
+
+msgid "You need permission."
+msgstr ""
+
+msgid "You will not get any notifications via email"
+msgstr ""
+
+msgid "You will only receive notifications for the events you choose"
+msgstr ""
+
+msgid "You will only receive notifications for threads you have participated in"
+msgstr ""
+
+msgid "You will receive notifications for any activity"
+msgstr ""
+
+msgid "You will receive notifications only for comments in which you were @mentioned"
+msgstr ""
+
+msgid "You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account"
+msgstr ""
+
+msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile"
+msgstr ""
+
+msgid "You won't be able to pull or push project code via SSH until you add an SSH key to your profile"
+msgstr ""
+
+msgid "You'll need to use different branch names to get a valid comparison."
+msgstr ""
+
+msgid "You're receiving this email because of your account on %{host}. %{manage_notifications_link} &middot; %{help_link}"
+msgstr ""
+
+msgid "Your Groups"
+msgstr ""
+
+msgid "Your Kubernetes cluster information on this page is still editable, but you are advised to disable and reconfigure"
+msgstr ""
+
+msgid "Your Projects (default)"
+msgstr ""
+
+msgid "Your Projects' Activity"
+msgstr ""
+
+msgid "Your Todos"
+msgstr ""
+
+msgid "Your changes can be committed to %{branch_name} because a merge request is open."
+msgstr ""
+
+msgid "Your changes have been committed. Commit %{commitId} %{commitStats}"
+msgstr ""
+
+msgid "Your comment will not be visible to the public."
+msgstr ""
+
+msgid "Your groups"
+msgstr ""
+
+msgid "Your name"
+msgstr ""
+
+msgid "Your projects"
+msgstr ""
+
+msgid "ago"
+msgstr ""
+
+msgid "among other things"
+msgstr ""
+
+msgid "assign yourself"
+msgstr ""
+
+msgid "branch name"
+msgstr ""
+
+msgid "command line instructions"
+msgstr ""
+
+msgid "connecting"
+msgstr ""
+
+msgid "day"
+msgid_plural "days"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "deploy token"
+msgstr ""
+
+msgid "disabled"
+msgstr ""
+
+msgid "enabled"
+msgstr ""
+
+msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
+msgstr ""
+
+msgid "for this project"
+msgstr ""
+
+msgid "importing"
+msgstr ""
+
+msgid "latest version"
+msgstr ""
+
+msgid "merge request"
+msgid_plural "merge requests"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "mrWidget| Please restore it or use a different %{missingBranchName} branch"
+msgstr ""
+
+msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} decreased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB"
+msgstr ""
+
+msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} increased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB"
+msgstr ""
+
+msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage is %{emphasisStart} unchanged %{emphasisEnd} at %{memoryFrom}MB"
+msgstr ""
+
+msgid "mrWidget|Allows commits from members who can merge to the target branch"
+msgstr ""
+
+msgid "mrWidget|Cancel automatic merge"
+msgstr ""
+
+msgid "mrWidget|Check out branch"
+msgstr ""
+
+msgid "mrWidget|Checking ability to merge automatically"
+msgstr ""
+
+msgid "mrWidget|Cherry-pick"
+msgstr ""
+
+msgid "mrWidget|Cherry-pick this merge request in a new merge request"
+msgstr ""
+
+msgid "mrWidget|Closed"
+msgstr ""
+
+msgid "mrWidget|Closed by"
+msgstr ""
+
+msgid "mrWidget|Closes"
+msgstr ""
+
+msgid "mrWidget|Create an issue to resolve them later"
+msgstr ""
+
+msgid "mrWidget|Deployment statistics are not available currently"
+msgstr ""
+
+msgid "mrWidget|Did not close"
+msgstr ""
+
+msgid "mrWidget|Email patches"
+msgstr ""
+
+msgid "mrWidget|Failed to load deployment statistics"
+msgstr ""
+
+msgid "mrWidget|If the %{branch} branch exists in your local repository, you can merge this merge request manually using the"
+msgstr ""
+
+msgid "mrWidget|If the %{missingBranchName} branch exists in your local repository, you can merge this merge request manually using the command line"
+msgstr ""
+
+msgid "mrWidget|Loading deployment statistics"
+msgstr ""
+
+msgid "mrWidget|Mentions"
+msgstr ""
+
+msgid "mrWidget|Merge"
+msgstr ""
+
+msgid "mrWidget|Merge failed."
+msgstr ""
+
+msgid "mrWidget|Merge locally"
+msgstr ""
+
+msgid "mrWidget|Merged by"
+msgstr ""
+
+msgid "mrWidget|Plain diff"
+msgstr ""
+
+msgid "mrWidget|Refresh"
+msgstr ""
+
+msgid "mrWidget|Refresh now"
+msgstr ""
+
+msgid "mrWidget|Refreshing now"
+msgstr ""
+
+msgid "mrWidget|Remove Source Branch"
+msgstr ""
+
+msgid "mrWidget|Remove source branch"
+msgstr ""
+
+msgid "mrWidget|Request to merge"
+msgstr ""
+
+msgid "mrWidget|Resolve conflicts"
+msgstr ""
+
+msgid "mrWidget|Revert"
+msgstr ""
+
+msgid "mrWidget|Revert this merge request in a new merge request"
+msgstr ""
+
+msgid "mrWidget|Set by"
+msgstr ""
+
+msgid "mrWidget|The changes were merged into"
+msgstr ""
+
+msgid "mrWidget|The changes were not merged into"
+msgstr ""
+
+msgid "mrWidget|The changes will be merged into"
+msgstr ""
+
+msgid "mrWidget|The source branch has been removed"
+msgstr ""
+
+msgid "mrWidget|The source branch is being removed"
+msgstr ""
+
+msgid "mrWidget|The source branch will be removed"
+msgstr ""
+
+msgid "mrWidget|The source branch will not be removed"
+msgstr ""
+
+msgid "mrWidget|There are merge conflicts"
+msgstr ""
+
+msgid "mrWidget|There are unresolved discussions. Please resolve these discussions"
+msgstr ""
+
+msgid "mrWidget|This merge request failed to be merged automatically"
+msgstr ""
+
+msgid "mrWidget|This merge request is in the process of being merged"
+msgstr ""
+
+msgid "mrWidget|This project is archived, write access has been disabled"
+msgstr ""
+
+msgid "mrWidget|Web IDE"
+msgstr ""
+
+msgid "mrWidget|You can merge this merge request manually using the"
+msgstr ""
+
+msgid "mrWidget|You can remove source branch now"
+msgstr ""
+
+msgid "mrWidget|branch does not exist."
+msgstr ""
+
+msgid "mrWidget|command line"
+msgstr ""
+
+msgid "mrWidget|into"
+msgstr ""
+
+msgid "mrWidget|to be merged automatically when the pipeline succeeds"
+msgstr ""
+
+msgid "new merge request"
+msgstr ""
+
+msgid "notification emails"
+msgstr ""
+
+msgid "or"
+msgstr ""
+
+msgid "parent"
+msgid_plural "parents"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "password"
+msgstr ""
+
+msgid "personal access token"
+msgstr ""
+
+msgid "remaining"
+msgstr ""
+
+msgid "remove due date"
+msgstr ""
+
+msgid "source"
+msgstr ""
+
+msgid "spendCommand|%{slash_command} will update the sum of the time spent."
+msgstr ""
+
+msgid "this document"
+msgstr ""
+
+msgid "username"
+msgstr ""
+
+msgid "uses Kubernetes clusters to deploy your code!"
+msgstr ""
+
+msgid "with %{additions} additions, %{deletions} deletions."
+msgstr ""
+
+msgid "within %d minute "
+msgid_plural "within %d minutes "
+msgstr[0] ""
+msgstr[1] ""
+
diff --git a/locale/id_ID/gitlab.po b/locale/id_ID/gitlab.po
index adb9746e854..9a2df080a9e 100644
--- a/locale/id_ID/gitlab.po
+++ b/locale/id_ID/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2018-04-04 19:35+0200\n"
-"PO-Revision-Date: 2018-04-05 03:39-0400\n"
+"POT-Creation-Date: 2018-07-01 16:35+1000\n"
+"PO-Revision-Date: 2018-07-01 11:01\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Indonesian\n"
"Language: id_ID\n"
@@ -16,8 +16,9 @@ msgstr ""
"X-Crowdin-Language: id\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-msgid " and"
-msgstr ""
+msgid "%d changed file"
+msgid_plural "%d changed files"
+msgstr[0] ""
msgid "%d commit"
msgid_plural "%d commits"
@@ -47,6 +48,14 @@ msgid "%d metric"
msgid_plural "%d metrics"
msgstr[0] ""
+msgid "%d staged change"
+msgid_plural "%d staged changes"
+msgstr[0] ""
+
+msgid "%d unstaged change"
+msgid_plural "%d unstaged changes"
+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] ""
@@ -61,12 +70,21 @@ msgid "%{count} participant"
msgid_plural "%{count} participants"
msgstr[0] ""
+msgid "%{filePath} deleted"
+msgstr ""
+
+msgid "%{group_docs_link_start}Groups%{group_docs_link_end} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects."
+msgstr ""
+
msgid "%{loadingIcon} Started"
msgstr ""
msgid "%{lock_path} is locked by GitLab User %{lock_user_id}"
msgstr ""
+msgid "%{nip_domain} can be used as an alternative to a custom domain."
+msgstr ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -79,6 +97,9 @@ msgstr ""
msgid "%{openOrClose} %{noteable}"
msgstr ""
+msgid "%{percent}%% complete"
+msgstr ""
+
msgid "%{storage_name}: failed storage access attempt on host:"
msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:"
msgstr[0] ""
@@ -86,15 +107,55 @@ msgstr[0] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{title} changes"
+msgstr ""
+
+msgid "%{unstaged} unstaged and %{staged} staged changes"
+msgstr ""
+
msgid "(checkout the %{link} for information on how to install it)."
msgstr ""
msgid "+ %{moreCount} more"
msgstr ""
+msgid "- Runner is active and can process any new jobs"
+msgstr ""
+
+msgid "- Runner is paused and will not receive any new jobs"
+msgstr ""
+
msgid "- show less"
msgstr ""
+msgid "1 %{type} addition"
+msgid_plural "%{count} %{type} additions"
+msgstr[0] ""
+
+msgid "1 %{type} modification"
+msgid_plural "%{count} %{type} modifications"
+msgstr[0] ""
+
+msgid "1 closed issue"
+msgid_plural "%d closed issues"
+msgstr[0] ""
+
+msgid "1 closed merge request"
+msgid_plural "%d closed merge requests"
+msgstr[0] ""
+
+msgid "1 merged merge request"
+msgid_plural "%d merged merge requests"
+msgstr[0] ""
+
+msgid "1 open issue"
+msgid_plural "%d open issues"
+msgstr[0] ""
+
+msgid "1 open merge request"
+msgid_plural "%d open merge requests"
+msgstr[0] ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] ""
@@ -105,9 +166,27 @@ msgstr ""
msgid "2FA enabled"
msgstr ""
+msgid "403|Please contact your GitLab administrator to get the permission."
+msgstr ""
+
+msgid "403|You don't have the permission to access this page."
+msgstr ""
+
+msgid "404|Make sure the address is correct and the page hasn't moved."
+msgstr ""
+
+msgid "404|Page Not Found"
+msgstr ""
+
+msgid "404|Please contact your GitLab administrator if you think this is a mistake."
+msgstr ""
+
msgid "<strong>Removes</strong> source branch"
msgstr ""
+msgid "A 'Runner' is a process which runs a job. You can setup as many Runners as you need."
+msgstr ""
+
msgid "A collection of graphs regarding Continuous Integration"
msgstr ""
@@ -117,6 +196,9 @@ msgstr ""
msgid "A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}."
msgstr ""
+msgid "A regular expression that will be used to find the test coverage output in the job trace. Leave blank to disable"
+msgstr ""
+
msgid "A user with write access to the source branch selected this option"
msgstr ""
@@ -129,25 +211,31 @@ msgstr ""
msgid "Abuse reports"
msgstr ""
+msgid "Accept terms"
+msgstr ""
+
msgid "Access Tokens"
msgstr ""
msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again."
msgstr ""
+msgid "Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report."
+msgstr ""
+
msgid "Account"
msgstr ""
-msgid "Account and limit settings"
+msgid "Account and limit"
msgstr ""
msgid "Active"
msgstr ""
-msgid "Activity"
+msgid "Active Sessions"
msgstr ""
-msgid "Add"
+msgid "Activity"
msgstr ""
msgid "Add Changelog"
@@ -156,9 +244,6 @@ msgstr ""
msgid "Add Contribution guide"
msgstr ""
-msgid "Add Group Webhooks and GitLab Enterprise Edition."
-msgstr ""
-
msgid "Add Kubernetes cluster"
msgstr ""
@@ -171,6 +256,9 @@ msgstr ""
msgid "Add new directory"
msgstr ""
+msgid "Add reaction"
+msgstr ""
+
msgid "Add todo"
msgstr ""
@@ -240,7 +328,10 @@ msgstr ""
msgid "All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings."
msgstr ""
-msgid "Allow edits from maintainers."
+msgid "Allow commits from members who can merge to the target branch."
+msgstr ""
+
+msgid "Allow public access to pipelines and job details, including output logs and artifacts"
msgstr ""
msgid "Allow rendering of PlantUML diagrams in Asciidoc documents."
@@ -252,31 +343,37 @@ msgstr ""
msgid "Allows you to add and manage Kubernetes clusters."
msgstr ""
-msgid "Also called \"Issuer\" or \"Relying party trust identifier\""
+msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
msgstr ""
-msgid "Also called \"Relying party service URL\" or \"Reply URL\""
+msgid "An error occured creating the new branch."
msgstr ""
-msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to connect."
+msgid "An error occured whilst loading all the files."
msgstr ""
-msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
+msgid "An error occured whilst loading the file content."
msgstr ""
-msgid "An error occurred previewing the blob"
+msgid "An error occured whilst loading the file."
msgstr ""
-msgid "An error occurred when toggling the notification subscription"
+msgid "An error occured whilst loading the merge request changes."
+msgstr ""
+
+msgid "An error occured whilst loading the merge request version data."
msgstr ""
-msgid "An error occurred when updating the issue weight"
+msgid "An error occured whilst loading the merge request."
msgstr ""
-msgid "An error occurred while adding approver"
+msgid "An error occurred previewing the blob"
+msgstr ""
+
+msgid "An error occurred when toggling the notification subscription"
msgstr ""
-msgid "An error occurred while detecting host keys"
+msgid "An error occurred while dismissing the alert. Refresh the page and try again."
msgstr ""
msgid "An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again."
@@ -294,10 +391,7 @@ msgstr ""
msgid "An error occurred while getting projects"
msgstr ""
-msgid "An error occurred while importing project"
-msgstr ""
-
-msgid "An error occurred while initializing path locks"
+msgid "An error occurred while importing project: ${details}"
msgstr ""
msgid "An error occurred while loading commits"
@@ -315,9 +409,6 @@ msgstr ""
msgid "An error occurred while making the request."
msgstr ""
-msgid "An error occurred while removing approver"
-msgstr ""
-
msgid "An error occurred while rendering KaTeX"
msgstr ""
@@ -330,9 +421,6 @@ msgstr ""
msgid "An error occurred while retrieving diff"
msgstr ""
-msgid "An error occurred while saving LDAP override status. Please try again."
-msgstr ""
-
msgid "An error occurred while saving assignees"
msgstr ""
@@ -342,9 +430,6 @@ msgstr ""
msgid "An error occurred. Please try again."
msgstr ""
-msgid "Any Label"
-msgstr ""
-
msgid "Appearance"
msgstr ""
@@ -357,19 +442,19 @@ msgstr ""
msgid "April"
msgstr ""
-msgid "Archived project! Repository is read-only"
+msgid "Archived project! Repository and other project resources are read-only"
msgstr ""
msgid "Are you sure you want to delete this pipeline schedule?"
msgstr ""
-msgid "Are you sure you want to reset registration token?"
+msgid "Are you sure you want to remove this identity?"
msgstr ""
-msgid "Are you sure you want to reset the health check token?"
+msgid "Are you sure you want to reset registration token?"
msgstr ""
-msgid "Are you sure you want to unlock %{path_lock_path}?"
+msgid "Are you sure you want to reset the health check token?"
msgstr ""
msgid "Are you sure?"
@@ -378,7 +463,7 @@ msgstr ""
msgid "Artifacts"
msgstr ""
-msgid "Assertion consumer service URL"
+msgid "Ask your group maintainer to setup a group Runner."
msgstr ""
msgid "Assign custom color like #FF0000"
@@ -402,9 +487,15 @@ msgstr ""
msgid "Assigned to :name"
msgstr ""
+msgid "Assigned to me"
+msgstr ""
+
msgid "Assignee"
msgstr ""
+msgid "Assignee(s)"
+msgstr ""
+
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr ""
@@ -438,7 +529,10 @@ msgstr ""
msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
msgstr ""
-msgid "AutoDevOps|Auto DevOps (Beta)"
+msgid "Auto-cancel redundant, pending pipelines"
+msgstr ""
+
+msgid "AutoDevOps|Auto DevOps"
msgstr ""
msgid "AutoDevOps|Auto DevOps documentation"
@@ -459,79 +553,109 @@ msgstr ""
msgid "AutoDevOps|add a Kubernetes cluster"
msgstr ""
-msgid "AutoDevOps|enable Auto DevOps (Beta)"
+msgid "AutoDevOps|enable Auto DevOps"
msgstr ""
msgid "Available"
msgstr ""
+msgid "Available group Runners : %{runners}"
+msgstr ""
+
+msgid "Available group Runners : %{runners}."
+msgstr ""
+
msgid "Avatar will be removed. Are you sure?"
msgstr ""
msgid "Average per day: %{average}"
msgstr ""
-msgid "Background Color"
+msgid "Background color"
msgstr ""
msgid "Background jobs"
msgstr ""
-msgid "Begin with the selected commit"
+msgid "Badges"
+msgstr ""
+
+msgid "Badges|A new badge was added."
+msgstr ""
+
+msgid "Badges|Add badge"
+msgstr ""
+
+msgid "Badges|Adding the badge failed, please check the entered URLs and try again."
msgstr ""
-msgid "Billing"
+msgid "Badges|Badge image URL"
msgstr ""
-msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan."
+msgid "Badges|Badge image preview"
msgstr ""
-msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available."
+msgid "Badges|Delete badge"
msgstr ""
-msgid "BillingPlans|Current plan"
+msgid "Badges|Delete badge?"
msgstr ""
-msgid "BillingPlans|Customer Support"
+msgid "Badges|Deleting the badge failed, please try again."
msgstr ""
-msgid "BillingPlans|Downgrade"
+msgid "Badges|Group Badge"
msgstr ""
-msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
+msgid "Badges|Link"
msgstr ""
-msgid "BillingPlans|Manage plan"
+msgid "Badges|No badge image"
msgstr ""
-msgid "BillingPlans|Please contact %{customer_support_link} in that case."
+msgid "Badges|No image to preview"
msgstr ""
-msgid "BillingPlans|See all %{plan_name} features"
+msgid "Badges|Project Badge"
msgstr ""
-msgid "BillingPlans|This group uses the plan associated with its parent group."
+msgid "Badges|Reload badge image"
msgstr ""
-msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
+msgid "Badges|Save changes"
msgstr ""
-msgid "BillingPlans|Upgrade"
+msgid "Badges|Saving the badge failed, please check the entered URLs and try again."
msgstr ""
-msgid "BillingPlans|You are currently on the %{plan_link} plan."
+msgid "Badges|The %{docsLinkStart}variables%{docsLinkEnd} GitLab supports: %{placeholders}"
msgstr ""
-msgid "BillingPlans|frequently asked questions"
+msgid "Badges|The badge was deleted."
msgstr ""
-msgid "BillingPlans|monthly"
+msgid "Badges|The badge was saved."
msgstr ""
-msgid "BillingPlans|paid annually at %{price_per_year}"
+msgid "Badges|This group has no badges"
msgstr ""
-msgid "BillingPlans|per user"
+msgid "Badges|This project has no badges"
+msgstr ""
+
+msgid "Badges|Your badges"
+msgstr ""
+
+msgid "Begin with the selected commit"
+msgstr ""
+
+msgid "Below are examples of regex for existing tools:"
+msgstr ""
+
+msgid "Boards"
+msgstr ""
+
+msgid "Branch %{branchName} was not found in this project's repository."
msgstr ""
msgid "Branch (%{branch_count})"
@@ -610,7 +734,7 @@ msgstr ""
msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
msgstr ""
-msgid "Branches|Only a project master or owner can delete a protected branch"
+msgid "Branches|Only a project maintainer or owner can delete a protected branch"
msgstr ""
msgid "Branches|Overview"
@@ -646,9 +770,6 @@ msgstr ""
msgid "Branches|Stale branches"
msgstr ""
-msgid "Branches|The branch could not be updated automatically because it has diverged from its upstream counterpart."
-msgstr ""
-
msgid "Branches|The default branch cannot be deleted"
msgstr ""
@@ -661,15 +782,9 @@ msgstr ""
msgid "Branches|To confirm, type %{branch_name_confirmation}:"
msgstr ""
-msgid "Branches|To discard the local changes and overwrite the branch with the upstream version, delete it here and choose 'Update Now' above."
-msgstr ""
-
msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
msgstr ""
-msgid "Branches|diverged from upstream"
-msgstr ""
-
msgid "Branches|merged"
msgstr ""
@@ -691,40 +806,79 @@ msgstr ""
msgid "Browse files"
msgstr ""
-msgid "Business"
-msgstr ""
-
msgid "ByAuthor|by"
msgstr ""
msgid "CI / CD"
msgstr ""
-msgid "CI/CD"
+msgid "CI / CD Settings"
msgstr ""
msgid "CI/CD configuration"
msgstr ""
-msgid "CI/CD for external repo"
+msgid "CI/CD settings"
+msgstr ""
+
+msgid "CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery."
+msgstr ""
+
+msgid "CICD|Auto DevOps"
+msgstr ""
+
+msgid "CICD|Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration."
+msgstr ""
+
+msgid "CICD|Automatic deployment to staging, manual deployment to production"
+msgstr ""
+
+msgid "CICD|Continuous deployment to production"
+msgstr ""
+
+msgid "CICD|Deployment strategy"
+msgstr ""
+
+msgid "CICD|Deployment strategy needs a domain name to work correctly."
+msgstr ""
+
+msgid "CICD|Disable Auto DevOps"
+msgstr ""
+
+msgid "CICD|Enable Auto DevOps"
+msgstr ""
+
+msgid "CICD|Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific %{ci_file}."
+msgstr ""
+
+msgid "CICD|Instance default (%{state})"
msgstr ""
msgid "CICD|Jobs"
msgstr ""
-msgid "Cancel"
+msgid "CICD|Learn more about Auto DevOps"
msgstr ""
-msgid "Cannot be merged automatically"
+msgid "CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project."
msgstr ""
-msgid "Cannot modify managed Kubernetes cluster"
+msgid "CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages."
+msgstr ""
+
+msgid "Can't find HEAD commit for this branch"
+msgstr ""
+
+msgid "Cancel"
+msgstr ""
+
+msgid "Cancel this job"
msgstr ""
-msgid "Certificate fingerprint"
+msgid "Cannot be merged automatically"
msgstr ""
-msgid "Change Weight"
+msgid "Cannot modify managed Kubernetes cluster"
msgstr ""
msgid "Change this value to influence how frequently the GitLab UI polls for updates."
@@ -778,21 +932,18 @@ msgstr ""
msgid "Choose a branch/tag (e.g. %{master}) or enter a commit (e.g. %{sha}) to see what's changed or to create a merge request."
msgstr ""
-msgid "Choose file..."
+msgid "Choose any color."
msgstr ""
-msgid "Choose which groups you wish to synchronize to this secondary node."
+msgid "Choose between <code>clone</code> or <code>fetch</code> to get the recent application code"
msgstr ""
-msgid "Choose which repositories you want to connect and run CI/CD pipelines."
+msgid "Choose file..."
msgstr ""
msgid "Choose which repositories you want to import."
msgstr ""
-msgid "Choose which shards you wish to synchronize to this secondary node."
-msgstr ""
-
msgid "CiStatusLabel|canceled"
msgstr ""
@@ -862,21 +1013,12 @@ msgstr ""
msgid "CiVariable|All environments"
msgstr ""
-msgid "CiVariable|Create wildcard"
-msgstr ""
-
msgid "CiVariable|Error occured while saving variables"
msgstr ""
-msgid "CiVariable|New environment"
-msgstr ""
-
msgid "CiVariable|Protected"
msgstr ""
-msgid "CiVariable|Search environments"
-msgstr ""
-
msgid "CiVariable|Toggle protected"
msgstr ""
@@ -886,28 +1028,28 @@ msgstr ""
msgid "CircuitBreakerApiLink|circuitbreaker api"
msgstr ""
-msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
+msgid "Clear search input"
msgstr ""
-msgid "Click to expand text"
+msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Client authentication certificate"
+msgid "Click the <strong>Promote</strong> button in the top right corner to promote it to a group milestone."
msgstr ""
-msgid "Client authentication key"
+msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
msgstr ""
-msgid "Client authentication key password"
+msgid "Click to expand it."
msgstr ""
-msgid "Clone repository"
+msgid "Click to expand text"
msgstr ""
-msgid "Close"
+msgid "Clone repository"
msgstr ""
-msgid "Closed"
+msgid "Close"
msgstr ""
msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster"
@@ -925,6 +1067,12 @@ msgstr ""
msgid "ClusterIntegration|Advanced options on this Kubernetes cluster's integration"
msgstr ""
+msgid "ClusterIntegration|An error occured while trying to fetch project zones: %{error}"
+msgstr ""
+
+msgid "ClusterIntegration|An error occured while trying to fetch your projects: %{error}"
+msgstr ""
+
msgid "ClusterIntegration|Applications"
msgstr ""
@@ -955,6 +1103,9 @@ msgstr ""
msgid "ClusterIntegration|Copy Ingress IP Address to clipboard"
msgstr ""
+msgid "ClusterIntegration|Copy Jupyter Hostname to clipboard"
+msgstr ""
+
msgid "ClusterIntegration|Copy Kubernetes cluster name"
msgstr ""
@@ -970,7 +1121,7 @@ msgstr ""
msgid "ClusterIntegration|Create a new Kubernetes cluster on Google Kubernetes Engine right from GitLab"
msgstr ""
-msgid "ClusterIntegration|Create on GKE"
+msgid "ClusterIntegration|Create on Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
@@ -982,13 +1133,25 @@ msgstr ""
msgid "ClusterIntegration|Environment scope"
msgstr ""
+msgid "ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab's Google Kubernetes Engine Integration."
+msgstr ""
+
+msgid "ClusterIntegration|Fetching machine types"
+msgstr ""
+
+msgid "ClusterIntegration|Fetching projects"
+msgstr ""
+
+msgid "ClusterIntegration|Fetching zones"
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
msgid "ClusterIntegration|GitLab Runner"
msgstr ""
-msgid "ClusterIntegration|Google Cloud Platform project ID"
+msgid "ClusterIntegration|Google Cloud Platform project"
msgstr ""
msgid "ClusterIntegration|Google Kubernetes Engine"
@@ -1000,9 +1163,6 @@ msgstr ""
msgid "ClusterIntegration|Helm Tiller"
msgstr ""
-msgid "ClusterIntegration|In order to show the health of the cluster, we'll need to provision your cluster with Prometheus to collect the required data."
-msgstr ""
-
msgid "ClusterIntegration|Ingress"
msgstr ""
@@ -1012,9 +1172,6 @@ msgstr ""
msgid "ClusterIntegration|Install"
msgstr ""
-msgid "ClusterIntegration|Install Prometheus"
-msgstr ""
-
msgid "ClusterIntegration|Installed"
msgstr ""
@@ -1027,13 +1184,16 @@ msgstr ""
msgid "ClusterIntegration|Integration status"
msgstr ""
-msgid "ClusterIntegration|Kubernetes cluster"
+msgid "ClusterIntegration|Jupyter Hostname"
msgstr ""
-msgid "ClusterIntegration|Kubernetes cluster details"
+msgid "ClusterIntegration|JupyterHub"
+msgstr ""
+
+msgid "ClusterIntegration|Kubernetes cluster"
msgstr ""
-msgid "ClusterIntegration|Kubernetes cluster health"
+msgid "ClusterIntegration|Kubernetes cluster details"
msgstr ""
msgid "ClusterIntegration|Kubernetes cluster integration"
@@ -1063,7 +1223,13 @@ msgstr ""
msgid "ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project"
msgstr ""
-msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
+msgid "ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}."
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{help_link_start}Kubernetes%{help_link_end}."
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}."
msgstr ""
msgid "ClusterIntegration|Learn more about environments"
@@ -1087,7 +1253,16 @@ msgstr ""
msgid "ClusterIntegration|More information"
msgstr ""
-msgid "ClusterIntegration|Multiple Kubernetes clusters are available in GitLab Enterprise Edition Premium and Ultimate"
+msgid "ClusterIntegration|No machine types matched your search"
+msgstr ""
+
+msgid "ClusterIntegration|No projects found"
+msgstr ""
+
+msgid "ClusterIntegration|No projects matched your search"
+msgstr ""
+
+msgid "ClusterIntegration|No zones matched your search"
msgstr ""
msgid "ClusterIntegration|Note:"
@@ -1102,9 +1277,6 @@ msgstr ""
msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
msgstr ""
-msgid "ClusterIntegration|Project ID"
-msgstr ""
-
msgid "ClusterIntegration|Project namespace"
msgstr ""
@@ -1117,6 +1289,9 @@ msgstr ""
msgid "ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration."
msgstr ""
+msgid "ClusterIntegration|Redeem up to $500 in free credit for Google Cloud Platform"
+msgstr ""
+
msgid "ClusterIntegration|Remove Kubernetes cluster integration"
msgstr ""
@@ -1132,19 +1307,37 @@ msgstr ""
msgid "ClusterIntegration|Save changes"
msgstr ""
+msgid "ClusterIntegration|Search machine types"
+msgstr ""
+
+msgid "ClusterIntegration|Search projects"
+msgstr ""
+
+msgid "ClusterIntegration|Search zones"
+msgstr ""
+
msgid "ClusterIntegration|Security"
msgstr ""
msgid "ClusterIntegration|See and edit the details for your Kubernetes cluster"
msgstr ""
-msgid "ClusterIntegration|See machine types"
+msgid "ClusterIntegration|Select machine type"
+msgstr ""
+
+msgid "ClusterIntegration|Select project"
msgstr ""
-msgid "ClusterIntegration|See your projects"
+msgid "ClusterIntegration|Select project and zone to choose machine type"
msgstr ""
-msgid "ClusterIntegration|See zones"
+msgid "ClusterIntegration|Select project to choose zone"
+msgstr ""
+
+msgid "ClusterIntegration|Select zone"
+msgstr ""
+
+msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
msgid "ClusterIntegration|Service token"
@@ -1177,6 +1370,9 @@ msgstr ""
msgid "ClusterIntegration|Token"
msgstr ""
+msgid "ClusterIntegration|Validating project billing status"
+msgstr ""
+
msgid "ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
@@ -1207,13 +1403,19 @@ msgstr ""
msgid "ClusterIntegration|properly configured"
msgstr ""
+msgid "ClusterIntegration|sign up"
+msgstr ""
+
msgid "Collapse"
msgstr ""
-msgid "Comment and resolve discussion"
+msgid "Collapse sidebar"
msgstr ""
-msgid "Comment and unresolve discussion"
+msgid "Comment & resolve discussion"
+msgstr ""
+
+msgid "Comment & unresolve discussion"
msgstr ""
msgid "Comments"
@@ -1278,6 +1480,9 @@ msgstr ""
msgid "Committed by"
msgstr ""
+msgid "Commit…"
+msgstr ""
+
msgid "Compare"
msgstr ""
@@ -1326,6 +1531,9 @@ msgstr ""
msgid "Configure limits for web and API requests."
msgstr ""
+msgid "Configure push mirrors."
+msgstr ""
+
msgid "Configure storage path and circuit breaker settings."
msgstr ""
@@ -1335,18 +1543,9 @@ msgstr ""
msgid "Connect"
msgstr ""
-msgid "Connect all repositories"
-msgstr ""
-
msgid "Connect repositories from GitHub"
msgstr ""
-msgid "Connect your external repositories, and CI/CD pipelines will run for new commits. A GitLab project will be created with only CI/CD features enabled."
-msgstr ""
-
-msgid "Connecting..."
-msgstr ""
-
msgid "Container Registry"
msgstr ""
@@ -1392,9 +1591,18 @@ 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 "ContainerRegistry|You can also use a %{deploy_token} for read-only access to the registry images."
+msgstr ""
+
+msgid "Continue"
+msgstr ""
+
msgid "Continuous Integration and Deployment"
msgstr ""
+msgid "Contribute to GitLab"
+msgstr ""
+
msgid "Contribution"
msgstr ""
@@ -1416,28 +1624,28 @@ msgstr ""
msgid "ContributorsPage|Please wait a moment, this page will automatically refresh when ready."
msgstr ""
-msgid "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
+msgid "Copy URL to clipboard"
msgstr ""
-msgid "Control the maximum concurrency of repository backfill for this secondary node"
+msgid "Copy branch name to clipboard"
msgstr ""
-msgid "Copy SSH public key to clipboard"
+msgid "Copy command to clipboard"
msgstr ""
-msgid "Copy URL to clipboard"
+msgid "Copy commit SHA to clipboard"
msgstr ""
-msgid "Copy branch name to clipboard"
+msgid "Copy file name to clipboard"
msgstr ""
-msgid "Copy command to clipboard"
+msgid "Copy file path to clipboard"
msgstr ""
-msgid "Copy commit SHA to clipboard"
+msgid "Copy reference to clipboard"
msgstr ""
-msgid "Copy reference to clipboard"
+msgid "Copy to clipboard"
msgstr ""
msgid "Create"
@@ -1458,13 +1666,13 @@ msgstr ""
msgid "Create branch"
msgstr ""
-msgid "Create directory"
+msgid "Create commit"
msgstr ""
-msgid "Create empty repository"
+msgid "Create directory"
msgstr ""
-msgid "Create epic"
+msgid "Create empty repository"
msgstr ""
msgid "Create file"
@@ -1509,13 +1717,10 @@ msgstr ""
msgid "CreateTokenToCloneLink|create a personal access token"
msgstr ""
-msgid "Creates a new branch from %{branchName}"
-msgstr ""
-
-msgid "Creates a new branch from %{branchName} and re-directs to create a new merge request"
+msgid "Created"
msgstr ""
-msgid "Creating epic"
+msgid "Created by me"
msgstr ""
msgid "Cron Timezone"
@@ -1524,16 +1729,19 @@ msgstr ""
msgid "Cron syntax"
msgstr ""
-msgid "Current node"
+msgid "CurrentUser|Profile"
msgstr ""
-msgid "Custom notification events"
+msgid "CurrentUser|Settings"
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}."
+msgid "Custom CI config path"
+msgstr ""
+
+msgid "Custom notification events"
msgstr ""
-msgid "Customize colors"
+msgid "Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notification_link}."
msgstr ""
msgid "Cycle Analytics"
@@ -1572,7 +1780,7 @@ msgstr ""
msgid "December"
msgstr ""
-msgid "Default classification label"
+msgid "Decline and sign out"
msgstr ""
msgid "Define a custom pattern with cron syntax"
@@ -1581,6 +1789,9 @@ msgstr ""
msgid "Delete"
msgstr ""
+msgid "Delete list"
+msgstr ""
+
msgid "Deploy"
msgid_plural "Deploys"
msgstr[0] ""
@@ -1588,548 +1799,524 @@ msgstr[0] ""
msgid "Deploy Keys"
msgstr ""
-msgid "Description"
+msgid "DeployKeys|+%{count} others"
msgstr ""
-msgid "Description templates allow you to define context-specific templates for issue and merge request description fields for your project."
+msgid "DeployKeys|Current project"
msgstr ""
-msgid "Details"
+msgid "DeployKeys|Deploy key"
msgstr ""
-msgid "Diffs|No file name available"
+msgid "DeployKeys|Enabled deploy keys"
msgstr ""
-msgid "Directory name"
+msgid "DeployKeys|Error enabling deploy key"
msgstr ""
-msgid "Disable"
+msgid "DeployKeys|Error getting deploy keys"
msgstr ""
-msgid "Discard draft"
+msgid "DeployKeys|Error removing deploy key"
msgstr ""
-msgid "Discover GitLab Geo."
+msgid "DeployKeys|Expand %{count} other projects"
msgstr ""
-msgid "Dismiss Cycle Analytics introduction box"
+msgid "DeployKeys|Loading deploy keys"
msgstr ""
-msgid "Dismiss Merge Request promotion"
+msgid "DeployKeys|No deploy keys found. Create one with the form above."
msgstr ""
-msgid "Documentation for popular identity providers"
+msgid "DeployKeys|Privately accessible deploy keys"
msgstr ""
-msgid "Don't show again"
+msgid "DeployKeys|Project usage"
msgstr ""
-msgid "Done"
+msgid "DeployKeys|Publicly accessible deploy keys"
msgstr ""
-msgid "Download"
-msgstr ""
-
-msgid "Download tar"
+msgid "DeployKeys|Read access only"
msgstr ""
-msgid "Download tar.bz2"
+msgid "DeployKeys|Write access allowed"
msgstr ""
-msgid "Download tar.gz"
+msgid "DeployKeys|You are going to remove this deploy key. Are you sure?"
msgstr ""
-msgid "Download zip"
+msgid "DeployTokens|Active Deploy Tokens (%{active_tokens})"
msgstr ""
-msgid "DownloadArtifacts|Download"
+msgid "DeployTokens|Add a deploy token"
msgstr ""
-msgid "DownloadCommit|Email Patches"
+msgid "DeployTokens|Allows read-only access to the registry images"
msgstr ""
-msgid "DownloadCommit|Plain Diff"
+msgid "DeployTokens|Allows read-only access to the repository"
msgstr ""
-msgid "DownloadSource|Download"
+msgid "DeployTokens|Copy deploy token to clipboard"
msgstr ""
-msgid "Downvotes"
+msgid "DeployTokens|Copy username to clipboard"
msgstr ""
-msgid "Due date"
+msgid "DeployTokens|Create deploy token"
msgstr ""
-msgid "During this process, you’ll be asked for URLs from GitLab’s side. Use the URLs shown below."
+msgid "DeployTokens|Created"
msgstr ""
-msgid "Edit"
+msgid "DeployTokens|Deploy Tokens"
msgstr ""
-msgid "Edit Pipeline Schedule %{id}"
+msgid "DeployTokens|Deploy tokens allow read-only access to your repository and registry images."
msgstr ""
-msgid "Edit files in the editor and commit changes here"
+msgid "DeployTokens|Expires"
msgstr ""
-msgid "Editing"
+msgid "DeployTokens|Name"
msgstr ""
-msgid "Elasticsearch"
+msgid "DeployTokens|Pick a name for the application, and we'll give you a unique deploy token."
msgstr ""
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "DeployTokens|Revoke"
msgstr ""
-msgid "Email"
+msgid "DeployTokens|Revoke %{name}"
msgstr ""
-msgid "Emails"
+msgid "DeployTokens|Scopes"
msgstr ""
-msgid "Enable"
+msgid "DeployTokens|This action cannot be undone."
msgstr ""
-msgid "Enable Auto DevOps"
+msgid "DeployTokens|This project has no active Deploy Tokens."
msgstr ""
-msgid "Enable SAML authentication for this group"
+msgid "DeployTokens|Use this token as a password. Make sure you save it - you won't be able to access it again."
msgstr ""
-msgid "Enable Sentry for error reporting and logging."
+msgid "DeployTokens|Use this username as a login."
msgstr ""
-msgid "Enable and configure InfluxDB metrics."
+msgid "DeployTokens|Username"
msgstr ""
-msgid "Enable and configure Prometheus metrics."
+msgid "DeployTokens|You are about to revoke"
msgstr ""
-msgid "Enable classification control using an external service"
+msgid "DeployTokens|Your New Deploy Token"
msgstr ""
-msgid "Enable or disable version check and usage ping."
+msgid "DeployTokens|Your new project deploy token has been created."
msgstr ""
-msgid "Enable reCAPTCHA or Akismet and set IP limits."
+msgid "Deprioritize label"
msgstr ""
-msgid "Enable the Performance Bar for a given group."
-msgstr ""
-
-msgid "Enabled"
-msgstr ""
-
-msgid "Environments|An error occurred while fetching the environments."
-msgstr ""
-
-msgid "Environments|An error occurred while making the request."
-msgstr ""
-
-msgid "Environments|Commit"
-msgstr ""
-
-msgid "Environments|Deployment"
-msgstr ""
-
-msgid "Environments|Environment"
+msgid "Description"
msgstr ""
-msgid "Environments|Environments"
+msgid "Details"
msgstr ""
-msgid "Environments|Job"
+msgid "Diffs|No file name available"
msgstr ""
-msgid "Environments|New environment"
+msgid "Diffs|Something went wrong while fetching diff lines."
msgstr ""
-msgid "Environments|No deployments yet"
+msgid "Directory name"
msgstr ""
-msgid "Environments|Open"
+msgid "Disable"
msgstr ""
-msgid "Environments|Re-deploy"
+msgid "Disable for this project"
msgstr ""
-msgid "Environments|Read more about environments"
+msgid "Disable group Runners"
msgstr ""
-msgid "Environments|Rollback"
+msgid "Discard changes"
msgstr ""
-msgid "Environments|Show all"
+msgid "Discard draft"
msgstr ""
-msgid "Environments|Updated"
+msgid "Dismiss Cycle Analytics introduction box"
msgstr ""
-msgid "Environments|You don't have any environments right now."
+msgid "Domain"
msgstr ""
-msgid "Epic will be removed! Are you sure?"
+msgid "Don't show again"
msgstr ""
-msgid "Epics"
+msgid "Done"
msgstr ""
-msgid "Epics Roadmap"
+msgid "Download"
msgstr ""
-msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
+msgid "Download tar"
msgstr ""
-msgid "Error Reporting and Logging"
+msgid "Download tar.bz2"
msgstr ""
-msgid "Error checking branch data. Please try again."
+msgid "Download tar.gz"
msgstr ""
-msgid "Error committing changes. Please try again."
+msgid "Download zip"
msgstr ""
-msgid "Error creating epic"
+msgid "DownloadArtifacts|Download"
msgstr ""
-msgid "Error fetching contributors data."
+msgid "DownloadCommit|Email Patches"
msgstr ""
-msgid "Error fetching labels."
+msgid "DownloadCommit|Plain Diff"
msgstr ""
-msgid "Error fetching network graph."
+msgid "DownloadSource|Download"
msgstr ""
-msgid "Error fetching refs"
+msgid "Downvotes"
msgstr ""
-msgid "Error fetching usage ping data."
+msgid "Due date"
msgstr ""
-msgid "Error occurred when toggling the notification subscription"
+msgid "Each Runner can be in one of the following states:"
msgstr ""
-msgid "Error saving label update."
+msgid "Edit"
msgstr ""
-msgid "Error updating status for all todos."
+msgid "Edit Label"
msgstr ""
-msgid "Error updating todo status."
+msgid "Edit Pipeline Schedule %{id}"
msgstr ""
-msgid "EventFilterBy|Filter by all"
+msgid "Edit files in the editor and commit changes here"
msgstr ""
-msgid "EventFilterBy|Filter by comments"
+msgid "Edit identity for %{user_name}"
msgstr ""
-msgid "EventFilterBy|Filter by issue events"
+msgid "Email"
msgstr ""
-msgid "EventFilterBy|Filter by merge events"
+msgid "Email patch"
msgstr ""
-msgid "EventFilterBy|Filter by push events"
+msgid "Emails"
msgstr ""
-msgid "EventFilterBy|Filter by team"
+msgid "Embed"
msgstr ""
-msgid "Every day (at 4:00am)"
+msgid "Enable"
msgstr ""
-msgid "Every month (on the 1st at 4:00am)"
+msgid "Enable Auto DevOps"
msgstr ""
-msgid "Every week (Sundays at 4:00am)"
+msgid "Enable Sentry for error reporting and logging."
msgstr ""
-msgid "Expand"
+msgid "Enable and configure InfluxDB metrics."
msgstr ""
-msgid "Explore projects"
+msgid "Enable and configure Prometheus metrics."
msgstr ""
-msgid "Explore public groups"
+msgid "Enable for this project"
msgstr ""
-msgid "External Classification Policy Authorization"
+msgid "Enable group Runners"
msgstr ""
-msgid "External authentication"
+msgid "Enable or disable certain group features and choose access levels."
msgstr ""
-msgid "External authorization denied access to this project"
+msgid "Enable or disable version check and usage ping."
msgstr ""
-msgid "External authorization request timeout"
+msgid "Enable reCAPTCHA or Akismet and set IP limits."
msgstr ""
-msgid "ExternalAuthorizationService|Classification Label"
+msgid "Enable the Performance Bar for a given group."
msgstr ""
-msgid "ExternalAuthorizationService|Classification label"
+msgid "Ends at (UTC)"
msgstr ""
-msgid "ExternalAuthorizationService|When no classification label is set the default label `%{default_label}` will be used."
+msgid "Environments"
msgstr ""
-msgid "Failed"
+msgid "Environments|An error occurred while fetching the environments."
msgstr ""
-msgid "Failed Jobs"
+msgid "Environments|An error occurred while making the request."
msgstr ""
-msgid "Failed to change the owner"
+msgid "Environments|Commit"
msgstr ""
-msgid "Failed to remove issue from board, please try again."
+msgid "Environments|Deployment"
msgstr ""
-msgid "Failed to remove the pipeline schedule"
+msgid "Environments|Environment"
msgstr ""
-msgid "Failed to update issues, please try again."
+msgid "Environments|Environments"
msgstr ""
-msgid "Feb"
+msgid "Environments|Job"
msgstr ""
-msgid "February"
+msgid "Environments|New environment"
msgstr ""
-msgid "Fields on this page are now uneditable, you can configure"
+msgid "Environments|No deployments yet"
msgstr ""
-msgid "File name"
+msgid "Environments|Open"
msgstr ""
-msgid "Files"
+msgid "Environments|Re-deploy"
msgstr ""
-msgid "Files (%{human_size})"
+msgid "Environments|Read more about environments"
msgstr ""
-msgid "Fill in the fields below, turn on <strong>%{enable_label}</strong>, and press <strong>%{save_changes}</strong>"
+msgid "Environments|Rollback"
msgstr ""
-msgid "Filter by commit message"
+msgid "Environments|Show all"
msgstr ""
-msgid "Find by path"
+msgid "Environments|Updated"
msgstr ""
-msgid "Find file"
+msgid "Environments|You don't have any environments right now."
msgstr ""
-msgid "Finished"
+msgid "Error Reporting and Logging"
msgstr ""
-msgid "FirstPushedBy|First"
+msgid "Error committing changes. Please try again."
msgstr ""
-msgid "FirstPushedBy|pushed by"
+msgid "Error fetching contributors data."
msgstr ""
-msgid "Font Color"
+msgid "Error fetching job trace"
msgstr ""
-msgid "Footer message"
+msgid "Error fetching labels."
msgstr ""
-msgid "Fork"
-msgid_plural "Forks"
-msgstr[0] ""
-
-msgid "ForkedFromProjectPath|Forked from"
+msgid "Error fetching network graph."
msgstr ""
-msgid "ForkedFromProjectPath|Forked from %{project_name} (deleted)"
+msgid "Error fetching refs"
msgstr ""
-msgid "Forking in progress"
+msgid "Error fetching usage ping data."
msgstr ""
-msgid "Format"
+msgid "Error loading branch data. Please try again."
msgstr ""
-msgid "From %{provider_title}"
+msgid "Error loading last commit."
msgstr ""
-msgid "From issue creation until deploy to production"
+msgid "Error loading merge requests."
msgstr ""
-msgid "From merge request merge until deploy to production"
+msgid "Error loading project data. Please try again."
msgstr ""
-msgid "From the Kubernetes cluster details view, install Runner from the applications list"
+msgid "Error occurred when toggling the notification subscription"
msgstr ""
-msgid "GPG Keys"
+msgid "Error saving label update."
msgstr ""
-msgid "Generate a default set of labels"
+msgid "Error updating status for all todos."
msgstr ""
-msgid "Geo Nodes"
+msgid "Error updating todo status."
msgstr ""
-msgid "Geo allows you to replicate your GitLab instance to other geographical locations."
+msgid "Estimated"
msgstr ""
-msgid "GeoNodeSyncStatus|Node is failing or broken."
+msgid "EventFilterBy|Filter by all"
msgstr ""
-msgid "GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage."
+msgid "EventFilterBy|Filter by comments"
msgstr ""
-msgid "GeoNodes|Checksummed"
+msgid "EventFilterBy|Filter by issue events"
msgstr ""
-msgid "GeoNodes|Database replication lag:"
+msgid "EventFilterBy|Filter by merge events"
msgstr ""
-msgid "GeoNodes|Disabling a node stops the sync process. Are you sure?"
+msgid "EventFilterBy|Filter by push events"
msgstr ""
-msgid "GeoNodes|Does not match the primary storage configuration"
+msgid "EventFilterBy|Filter by team"
msgstr ""
-msgid "GeoNodes|Failed"
+msgid "Every day (at 4:00am)"
msgstr ""
-msgid "GeoNodes|Full"
+msgid "Every month (on the 1st at 4:00am)"
msgstr ""
-msgid "GeoNodes|GitLab version does not match the primary node version"
+msgid "Every week (Sundays at 4:00am)"
msgstr ""
-msgid "GeoNodes|GitLab version:"
+msgid "Expand"
msgstr ""
-msgid "GeoNodes|Health status:"
+msgid "Expand all"
msgstr ""
-msgid "GeoNodes|Last event ID processed by cursor:"
+msgid "Expand sidebar"
msgstr ""
-msgid "GeoNodes|Last event ID seen from primary:"
+msgid "Explore projects"
msgstr ""
-msgid "GeoNodes|Loading nodes"
+msgid "Explore public groups"
msgstr ""
-msgid "GeoNodes|Local Attachments:"
+msgid "Failed"
msgstr ""
-msgid "GeoNodes|Local LFS objects:"
+msgid "Failed Jobs"
msgstr ""
-msgid "GeoNodes|Local job artifacts:"
+msgid "Failed to change the owner"
msgstr ""
-msgid "GeoNodes|New node"
+msgid "Failed to check related branches."
msgstr ""
-msgid "GeoNodes|Node Authentication was successfully repaired."
+msgid "Failed to remove issue from board, please try again."
msgstr ""
-msgid "GeoNodes|Node was successfully removed."
+msgid "Failed to remove the pipeline schedule"
msgstr ""
-msgid "GeoNodes|Not checksummed"
+msgid "Failed to update issues, please try again."
msgstr ""
-msgid "GeoNodes|Out of sync"
+msgid "Failure"
msgstr ""
-msgid "GeoNodes|Removing a node stops the sync process. Are you sure?"
+msgid "Faster as it re-uses the project workspace (falling back to clone if it doesn't exist)"
msgstr ""
-msgid "GeoNodes|Replication slot WAL:"
+msgid "Feb"
msgstr ""
-msgid "GeoNodes|Replication slots:"
+msgid "February"
msgstr ""
-msgid "GeoNodes|Repositories checksummed:"
+msgid "Fields on this page are now uneditable, you can configure"
msgstr ""
-msgid "GeoNodes|Repositories:"
+msgid "Files"
msgstr ""
-msgid "GeoNodes|Repository checksums verified:"
+msgid "Files (%{human_size})"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "Filter by commit message"
msgstr ""
-msgid "GeoNodes|Something went wrong while changing node status"
+msgid "Find by path"
msgstr ""
-msgid "GeoNodes|Something went wrong while removing node"
+msgid "Find file"
msgstr ""
-msgid "GeoNodes|Something went wrong while repairing node"
+msgid "Finished"
msgstr ""
-msgid "GeoNodes|Storage config:"
+msgid "FirstPushedBy|First"
msgstr ""
-msgid "GeoNodes|Sync settings:"
+msgid "FirstPushedBy|pushed by"
msgstr ""
-msgid "GeoNodes|Synced"
+msgid "For internal projects, any logged in user can view pipelines and access job details (output logs and artifacts)"
msgstr ""
-msgid "GeoNodes|Unused slots"
+msgid "For private projects, any member (guest or higher) can view pipelines and access job details (output logs and artifacts)"
msgstr ""
-msgid "GeoNodes|Unverified"
+msgid "For public projects, anyone can view pipelines and access job details (output logs and artifacts)"
msgstr ""
-msgid "GeoNodes|Used slots"
-msgstr ""
+msgid "Fork"
+msgid_plural "Forks"
+msgstr[0] ""
-msgid "GeoNodes|Verified"
+msgid "ForkedFromProjectPath|Forked from"
msgstr ""
-msgid "GeoNodes|Wiki checksums verified:"
+msgid "ForkedFromProjectPath|Forked from %{project_name} (deleted)"
msgstr ""
-msgid "GeoNodes|Wikis checksummed:"
+msgid "Forking in progress"
msgstr ""
-msgid "GeoNodes|Wikis:"
+msgid "Format"
msgstr ""
-msgid "GeoNodes|You have configured Geo nodes using an insecure HTTP connection. We recommend the use of HTTPS."
+msgid "Found errors in your .gitlab-ci.yml:"
msgstr ""
-msgid "Geo|All projects"
+msgid "From %{provider_title}"
msgstr ""
-msgid "Geo|File sync capacity"
+msgid "From issue creation until deploy to production"
msgstr ""
-msgid "Geo|Groups to synchronize"
+msgid "From merge request merge until deploy to production"
msgstr ""
-msgid "Geo|Projects in certain groups"
+msgid "From the Kubernetes cluster details view, install Runner from the applications list"
msgstr ""
-msgid "Geo|Projects in certain storage shards"
+msgid "GPG Keys"
msgstr ""
-msgid "Geo|Repository sync capacity"
+msgid "General"
msgstr ""
-msgid "Geo|Select groups to replicate."
+msgid "General pipelines"
msgstr ""
-msgid "Geo|Shards to synchronize"
+msgid "Generate a default set of labels"
msgstr ""
msgid "Git repository URL"
@@ -2141,6 +2328,9 @@ msgstr ""
msgid "Git storage health information has been reset"
msgstr ""
+msgid "Git strategy for pipelines"
+msgstr ""
+
msgid "Git version"
msgstr ""
@@ -2150,21 +2340,24 @@ msgstr ""
msgid "GitLab CI Linter has been moved"
msgstr ""
-msgid "GitLab Geo"
+msgid "GitLab Group Runners can execute code for all the projects in this group."
msgstr ""
msgid "GitLab Runner section"
msgstr ""
-msgid "GitLab single sign on URL"
-msgstr ""
-
msgid "Gitaly"
msgstr ""
msgid "Gitaly Servers"
msgstr ""
+msgid "Gitaly|Address"
+msgstr ""
+
+msgid "Go Back"
+msgstr ""
+
msgid "Go back"
msgstr ""
@@ -2180,22 +2373,19 @@ msgstr ""
msgid "Got it!"
msgstr ""
-msgid "GroupRoadmap|Epics let you manage your portfolio of projects more efficiently and with less effort"
-msgstr ""
-
-msgid "GroupRoadmap|From %{dateWord}"
+msgid "Graph"
msgstr ""
-msgid "GroupRoadmap|Loading roadmap"
+msgid "Group CI/CD settings"
msgstr ""
-msgid "GroupRoadmap|Something went wrong while fetching epics"
+msgid "Group ID"
msgstr ""
-msgid "GroupRoadmap|To view the roadmap, add a planned start or finish date to one of your epics in this group or its subgroups. Only epics in the past 3 months and the next 3 months are shown &ndash; from %{startDate} to %{endDate}."
+msgid "Group Runners"
msgstr ""
-msgid "GroupRoadmap|Until %{dateWord}"
+msgid "Group maintainers can register group runners in the %{link}"
msgstr ""
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
@@ -2222,6 +2412,9 @@ msgstr ""
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
+msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
+msgstr ""
+
msgid "GroupsEmptyState|A group is a collection of several projects."
msgstr ""
@@ -2261,12 +2454,6 @@ msgstr ""
msgid "GroupsTree|Sorry, no groups or projects matched your search"
msgstr ""
-msgid "Have your users email"
-msgstr ""
-
-msgid "Header message"
-msgstr ""
-
msgid "Health Check"
msgstr ""
@@ -2298,19 +2485,49 @@ msgid "Hide value"
msgid_plural "Hide values"
msgstr[0] ""
+msgid "Hide whitespace changes"
+msgstr ""
+
msgid "History"
msgstr ""
msgid "Housekeeping successfully started"
msgstr ""
-msgid "Identity provider single sign on URL"
+msgid "I accept the %{terms_link}"
+msgstr ""
+
+msgid "I accept the|Terms of Service and Privacy Policy"
+msgstr ""
+
+msgid "ID"
+msgstr ""
+
+msgid "IDE|Commit"
+msgstr ""
+
+msgid "IDE|Edit"
+msgstr ""
+
+msgid "IDE|Go back"
+msgstr ""
+
+msgid "IDE|Open in file view"
+msgstr ""
+
+msgid "IDE|Review"
msgstr ""
-msgid "If enabled, access to projects will be validated on an external service using their classification label."
+msgid "Identifier"
msgstr ""
-msgid "If using GitHub, you’ll see pipeline statuses on GitHub for your commits and pull requests. %{more_info_link}"
+msgid "Identities"
+msgstr ""
+
+msgid "If disabled, the access level will depend on the user's permissions in the project."
+msgstr ""
+
+msgid "If enabled"
msgstr ""
msgid "If you already have files you can push them using the %{link_to_cli} below."
@@ -2319,6 +2536,15 @@ msgstr ""
msgid "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>."
msgstr ""
+msgid "ImageDiffViewer|2-up"
+msgstr ""
+
+msgid "ImageDiffViewer|Onion skin"
+msgstr ""
+
+msgid "ImageDiffViewer|Swipe"
+msgstr ""
+
msgid "Import"
msgstr ""
@@ -2334,16 +2560,10 @@ msgstr ""
msgid "Import repository"
msgstr ""
-msgid "ImportButtons|Connect repositories from"
-msgstr ""
-
-msgid "Improve Issue boards with GitLab Enterprise Edition."
+msgid "Include a Terms of Service agreement and Privacy Policy that all users must accept."
msgstr ""
-msgid "Improve issues management with Issue weight and GitLab Enterprise Edition."
-msgstr ""
-
-msgid "Improve search with Advanced Global Search and GitLab Enterprise Edition."
+msgid "Inline"
msgstr ""
msgid "Install Runner on Kubernetes"
@@ -2352,16 +2572,15 @@ msgstr ""
msgid "Install a Runner compatible with GitLab CI"
msgstr ""
-msgid "Instance"
-msgid_plural "Instances"
-msgstr[0] ""
-
msgid "Instance does not support multiple Kubernetes clusters"
msgstr ""
msgid "Integrations"
msgstr ""
+msgid "Integrations Settings"
+msgstr ""
+
msgid "Interested parties can even contribute by pushing commits if they want to."
msgstr ""
@@ -2377,7 +2596,7 @@ msgstr ""
msgid "Introducing Cycle Analytics"
msgstr ""
-msgid "Issue board focus mode"
+msgid "Issue Board"
msgstr ""
msgid "Issue events"
@@ -2386,9 +2605,6 @@ msgstr ""
msgid "IssueBoards|Board"
msgstr ""
-msgid "IssueBoards|Boards"
-msgstr ""
-
msgid "Issues"
msgstr ""
@@ -2401,6 +2617,12 @@ msgstr ""
msgid "January"
msgstr ""
+msgid "Job"
+msgstr ""
+
+msgid "Job has been erased"
+msgstr ""
+
msgid "Jobs"
msgstr ""
@@ -2443,6 +2665,9 @@ msgstr ""
msgid "Kubernetes service integration has been deprecated. %{deprecated_message_content} your Kubernetes clusters using the new <a href=\"%{url}\"/>Kubernetes Clusters</a> page"
msgstr ""
+msgid "LFS"
+msgstr ""
+
msgid "LFSStatus|Disabled"
msgstr ""
@@ -2452,6 +2677,9 @@ msgstr ""
msgid "Label"
msgstr ""
+msgid "Label actions dropdown"
+msgstr ""
+
msgid "LabelSelect|%{firstLabelName} +%{remainingLabelCount} more"
msgstr ""
@@ -2467,6 +2695,9 @@ msgstr ""
msgid "Labels can be applied to issues and merge requests to categorize them."
msgstr ""
+msgid "Labels can be applied to issues and merge requests."
+msgstr ""
+
msgid "Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>"
msgstr ""
@@ -2501,6 +2732,9 @@ msgstr ""
msgid "LastPushEvent|at"
msgstr ""
+msgid "Latest changes"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -2525,9 +2759,6 @@ msgstr ""
msgid "Leave project"
msgstr ""
-msgid "License"
-msgstr ""
-
msgid "List"
msgstr ""
@@ -2537,6 +2768,9 @@ msgstr ""
msgid "Loading the GitLab IDE..."
msgstr ""
+msgid "Loading..."
+msgstr ""
+
msgid "Lock"
msgstr ""
@@ -2546,21 +2780,18 @@ msgstr ""
msgid "Lock not found"
msgstr ""
-msgid "Locked"
+msgid "Lock to current projects"
msgstr ""
-msgid "Locked Files"
+msgid "Locked"
msgstr ""
-msgid "Locks give the ability to lock specific file or folder."
+msgid "Locked to current projects"
msgstr ""
msgid "Login"
msgstr ""
-msgid "Make everyone on your team more productive regardless of their location. GitLab Geo creates read-only mirrors of your GitLab instance so you can reduce the time it takes to clone and fetch large repos."
-msgstr ""
-
msgid "Manage all notifications"
msgstr ""
@@ -2573,16 +2804,16 @@ msgstr ""
msgid "Manage project labels"
msgstr ""
-msgid "Manage your group’s membership while adding another level of security with SAML."
-msgstr ""
-
msgid "Mar"
msgstr ""
msgid "March"
msgstr ""
-msgid "Mark done"
+msgid "Mark todo as done"
+msgstr ""
+
+msgid "Markdown enabled"
msgstr ""
msgid "Maximum git storage failures"
@@ -2597,7 +2828,7 @@ msgstr ""
msgid "Members"
msgstr ""
-msgid "Members will be forwarded here when signing in to your group. Get this from your identity provider, where it can also be called \"SSO Service Location\", \"SAML Token Issuance Endpoint\", or \"SAML 2.0/W-Federation URL\"."
+msgid "Merge Request:"
msgstr ""
msgid "Merge Requests"
@@ -2609,91 +2840,46 @@ msgstr ""
msgid "Merge request"
msgstr ""
-msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
-msgstr ""
-
-msgid "Merged"
+msgid "Merge requests"
msgstr ""
-msgid "Messages"
-msgstr ""
-
-msgid "Metrics - Influx"
-msgstr ""
-
-msgid "Metrics - Prometheus"
-msgstr ""
-
-msgid "Metrics|Business"
-msgstr ""
-
-msgid "Metrics|Create metric"
-msgstr ""
-
-msgid "Metrics|Edit metric"
-msgstr ""
-
-msgid "Metrics|For grouping similar metrics"
-msgstr ""
-
-msgid "Metrics|Label of the chart's vertical axis. Usually the type of the unit being charted. The horizontal axis (X-axis) always represents time."
-msgstr ""
-
-msgid "Metrics|Legend label (optional)"
-msgstr ""
-
-msgid "Metrics|Must be a valid PromQL query."
-msgstr ""
-
-msgid "Metrics|Name"
-msgstr ""
-
-msgid "Metrics|New metric"
-msgstr ""
-
-msgid "Metrics|Prometheus Query Documentation"
-msgstr ""
-
-msgid "Metrics|Query"
-msgstr ""
-
-msgid "Metrics|Response"
+msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
msgstr ""
-msgid "Metrics|System"
+msgid "MergeRequests|Resolve this discussion in a new issue"
msgstr ""
-msgid "Metrics|Type"
+msgid "MergeRequests|Saving the comment failed"
msgstr ""
-msgid "Metrics|Unit label"
+msgid "MergeRequests|Toggle comments for this file"
msgstr ""
-msgid "Metrics|Used as a title for the chart"
+msgid "MergeRequests|Updating discussions failed"
msgstr ""
-msgid "Metrics|Used if the query returns a single series. If it returns multiple series, their legend labels will be picked up from the response."
+msgid "MergeRequests|View file @ %{commitId}"
msgstr ""
-msgid "Metrics|Y-axis label"
+msgid "MergeRequests|View replaced file @ %{commitId}"
msgstr ""
-msgid "Metrics|e.g. HTTP requests"
+msgid "Merged"
msgstr ""
-msgid "Metrics|e.g. Requests/second"
+msgid "Messages"
msgstr ""
-msgid "Metrics|e.g. Throughput"
+msgid "Metrics - Influx"
msgstr ""
-msgid "Metrics|e.g. rate(http_requests_total[5m])"
+msgid "Metrics - Prometheus"
msgstr ""
-msgid "Metrics|e.g. req/sec"
+msgid "Milestone"
msgstr ""
-msgid "Milestone"
+msgid "Milestones"
msgstr ""
msgid "Milestones|Delete milestone"
@@ -2714,9 +2900,6 @@ msgstr ""
msgid "Milestones|Promote Milestone"
msgstr ""
-msgid "Milestones|This action cannot be reversed."
-msgstr ""
-
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr ""
@@ -2729,7 +2912,7 @@ msgstr ""
msgid "Monitoring"
msgstr ""
-msgid "More info"
+msgid "More actions"
msgstr ""
msgid "More information"
@@ -2744,12 +2927,30 @@ msgstr ""
msgid "Move issue"
msgstr ""
-msgid "Multiple issue boards"
+msgid "Name"
msgstr ""
msgid "Name new label"
msgstr ""
+msgid "Name your individual key via a title"
+msgstr ""
+
+msgid "Nav|Help"
+msgstr ""
+
+msgid "Nav|Home"
+msgstr ""
+
+msgid "Nav|Sign In / Register"
+msgstr ""
+
+msgid "Nav|Sign out and sign in with a different account"
+msgstr ""
+
+msgid "New Identity"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] ""
@@ -2760,6 +2961,9 @@ msgstr ""
msgid "New Kubernetes cluster"
msgstr ""
+msgid "New Label"
+msgstr ""
+
msgid "New Pipeline Schedule"
msgstr ""
@@ -2772,15 +2976,15 @@ msgstr ""
msgid "New directory"
msgstr ""
-msgid "New epic"
-msgstr ""
-
msgid "New file"
msgstr ""
msgid "New group"
msgstr ""
+msgid "New identity"
+msgstr ""
+
msgid "New issue"
msgstr ""
@@ -2790,6 +2994,9 @@ msgstr ""
msgid "New merge request"
msgstr ""
+msgid "New pipelines will cancel older, pending pipelines on the same branch"
+msgstr ""
+
msgid "New project"
msgstr ""
@@ -2805,7 +3012,7 @@ msgstr ""
msgid "New tag"
msgstr ""
-msgid "No Label"
+msgid "No"
msgstr ""
msgid "No assignee"
@@ -2826,7 +3033,16 @@ msgstr ""
msgid "No file chosen"
msgstr ""
-msgid "No labels created yet."
+msgid "No files found"
+msgstr ""
+
+msgid "No files found."
+msgstr ""
+
+msgid "No merge requests found"
+msgstr ""
+
+msgid "No messages were logged"
msgstr ""
msgid "No repository"
@@ -2859,15 +3075,9 @@ msgstr ""
msgid "Note that the master branch is automatically protected. %{link_to_protected_branches}"
msgstr ""
-msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow connecting repositories without generating a Personal Access Token."
-msgstr ""
-
msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
msgstr ""
-msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow connecting repositories without generating a Personal Access Token."
-msgstr ""
-
msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
msgstr ""
@@ -2943,9 +3153,6 @@ msgstr ""
msgid "Number of access attempts"
msgstr ""
-msgid "OK"
-msgstr ""
-
msgid "Oct"
msgstr ""
@@ -2955,19 +3162,16 @@ msgstr ""
msgid "OfSearchInADropdown|Filter"
msgstr ""
-msgid "Once imported, repositories can be mirrored over SSH. Read more %{ssh_link}"
-msgstr ""
-
msgid "Online IDE integration settings."
msgstr ""
-msgid "Only project members can comment."
+msgid "Only comments from the following commit are shown below"
msgstr ""
-msgid "Open"
+msgid "Only project members can comment."
msgstr ""
-msgid "Opened"
+msgid "Open in Xcode"
msgstr ""
msgid "OpenedNDaysAgo|Opened"
@@ -2976,9 +3180,18 @@ msgstr ""
msgid "Opens in a new window"
msgstr ""
+msgid "Operations"
+msgstr ""
+
msgid "Options"
msgstr ""
+msgid "Or you can choose one of the suggested colors below"
+msgstr ""
+
+msgid "Other Labels"
+msgstr ""
+
msgid "Otherwise it is recommended you start with one of the options below."
msgstr ""
@@ -3012,12 +3225,27 @@ msgstr ""
msgid "Password"
msgstr ""
+msgid "Paste your public SSH key, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'. Don't use your private SSH key."
+msgstr ""
+
+msgid "Pause"
+msgstr ""
+
msgid "Pending"
msgstr ""
+msgid "Per job. If a job passes this threshold, it will be marked as failed"
+msgstr ""
+
+msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgstr ""
+
msgid "Performance optimization"
msgstr ""
+msgid "Permissions"
+msgstr ""
+
msgid "Personal Access Token"
msgstr ""
@@ -3033,7 +3261,7 @@ msgstr ""
msgid "Pipeline Schedules"
msgstr ""
-msgid "Pipeline quota"
+msgid "Pipeline triggers"
msgstr ""
msgid "PipelineCharts|Failed:"
@@ -3132,10 +3360,22 @@ msgstr ""
msgid "Pipelines|This project is not currently set up to run pipelines."
msgstr ""
-msgid "Pipeline|Retry pipeline"
+msgid "Pipeline|Create for"
+msgstr ""
+
+msgid "Pipeline|Create pipeline"
msgstr ""
-msgid "Pipeline|Retry pipeline #%{pipelineId}?"
+msgid "Pipeline|Existing branch name or tag"
+msgstr ""
+
+msgid "Pipeline|Run Pipeline"
+msgstr ""
+
+msgid "Pipeline|Search branches"
+msgstr ""
+
+msgid "Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default."
msgstr ""
msgid "Pipeline|Stop pipeline"
@@ -3144,7 +3384,7 @@ msgstr ""
msgid "Pipeline|Stop pipeline #%{pipelineId}?"
msgstr ""
-msgid "Pipeline|You’re about to retry pipeline %{pipelineId}."
+msgid "Pipeline|Variables"
msgstr ""
msgid "Pipeline|You’re about to stop pipeline %{pipelineId}."
@@ -3162,19 +3402,25 @@ msgstr ""
msgid "Pipeline|with stages"
msgstr ""
+msgid "Plain diff"
+msgstr ""
+
msgid "PlantUML"
msgstr ""
msgid "Play"
msgstr ""
-msgid "Please <a href=%{link_to_billing} target=\"_blank\" rel=\"noopener noreferrer\">enable billing for one of your projects to be able to create a Kubernetes cluster</a>, then try again."
+msgid "Please accept the Terms of Service before continuing."
+msgstr ""
+
+msgid "Please select at least one filter to see results"
msgstr ""
msgid "Please solve the reCAPTCHA"
msgstr ""
-msgid "Please wait while we connect to your repository. Refresh at will."
+msgid "Please try again"
msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
@@ -3183,7 +3429,19 @@ msgstr ""
msgid "Preferences"
msgstr ""
-msgid "Primary"
+msgid "Preferences|Navigation theme"
+msgstr ""
+
+msgid "Prioritize"
+msgstr ""
+
+msgid "Prioritize label"
+msgstr ""
+
+msgid "Prioritized Labels"
+msgstr ""
+
+msgid "Prioritized label"
msgstr ""
msgid "Private - Project access must be granted explicitly to each user."
@@ -3201,6 +3459,12 @@ msgstr ""
msgid "Profiles|Account scheduled for removal."
msgstr ""
+msgid "Profiles|Change username"
+msgstr ""
+
+msgid "Profiles|Current path: %{path}"
+msgstr ""
+
msgid "Profiles|Delete Account"
msgstr ""
@@ -3219,9 +3483,21 @@ msgstr ""
msgid "Profiles|Invalid username"
msgstr ""
+msgid "Profiles|Path"
+msgstr ""
+
msgid "Profiles|Type your %{confirmationValue} to confirm:"
msgstr ""
+msgid "Profiles|Update username"
+msgstr ""
+
+msgid "Profiles|Username change failed - %{message}"
+msgstr ""
+
+msgid "Profiles|Username successfully changed"
+msgstr ""
+
msgid "Profiles|You don't have access to delete this user."
msgstr ""
@@ -3240,6 +3516,12 @@ msgstr ""
msgid "Programming languages used in this repository"
msgstr ""
+msgid "Progress"
+msgstr ""
+
+msgid "Project"
+msgstr ""
+
msgid "Project '%{project_name}' is in the process of being deleted."
msgstr ""
@@ -3252,6 +3534,9 @@ msgstr ""
msgid "Project '%{project_name}' was successfully updated."
msgstr ""
+msgid "Project Badges"
+msgstr ""
+
msgid "Project access must be granted explicitly to each user."
msgstr ""
@@ -3279,30 +3564,6 @@ msgstr ""
msgid "ProjectActivityRSS|Subscribe"
msgstr ""
-msgid "ProjectCreationLevel|Allowed to create projects"
-msgstr ""
-
-msgid "ProjectCreationLevel|Default project creation protection"
-msgstr ""
-
-msgid "ProjectCreationLevel|Developers + Masters"
-msgstr ""
-
-msgid "ProjectCreationLevel|Masters"
-msgstr ""
-
-msgid "ProjectCreationLevel|No one"
-msgstr ""
-
-msgid "ProjectFeature|Disabled"
-msgstr ""
-
-msgid "ProjectFeature|Everyone with access"
-msgstr ""
-
-msgid "ProjectFeature|Only team members"
-msgstr ""
-
msgid "ProjectFileTree|Name"
msgstr ""
@@ -3312,27 +3573,6 @@ msgstr ""
msgid "ProjectLifecycle|Stage"
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 ""
@@ -3357,6 +3597,9 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr ""
+msgid "PrometheusDashboard|Time"
+msgstr ""
+
msgid "PrometheusService|%{exporters} with %{metrics} were found"
msgstr ""
@@ -3378,18 +3621,9 @@ msgstr ""
msgid "PrometheusService|Common metrics"
msgstr ""
-msgid "PrometheusService|Common metrics are automatically monitored based on a library of metrics from popular exporters."
-msgstr ""
-
-msgid "PrometheusService|Custom metrics"
-msgstr ""
-
msgid "PrometheusService|Finding and configuring metrics..."
msgstr ""
-msgid "PrometheusService|Finding custom metrics..."
-msgstr ""
-
msgid "PrometheusService|Install Prometheus on clusters"
msgstr ""
@@ -3402,13 +3636,13 @@ msgstr ""
msgid "PrometheusService|Metrics"
msgstr ""
-msgid "PrometheusService|Missing environment variable"
+msgid "PrometheusService|Metrics are automatically configured and monitored based on a library of metrics from popular exporters."
msgstr ""
-msgid "PrometheusService|More information"
+msgid "PrometheusService|Missing environment variable"
msgstr ""
-msgid "PrometheusService|New metric"
+msgid "PrometheusService|More information"
msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
@@ -3417,9 +3651,6 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
-msgstr ""
-
msgid "PrometheusService|Time-series monitoring service"
msgstr ""
@@ -3435,22 +3666,28 @@ msgstr ""
msgid "Promote"
msgstr ""
-msgid "Promote to Group Label"
+msgid "Promote these project milestones into a group milestone."
msgstr ""
msgid "Promote to Group Milestone"
msgstr ""
+msgid "Promote to group label"
+msgstr ""
+
msgid "Protip:"
msgstr ""
+msgid "Provider"
+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"
+msgid "Public pipelines"
msgstr ""
msgid "Push events"
@@ -3462,10 +3699,10 @@ msgstr ""
msgid "Push to create a project"
msgstr ""
-msgid "PushRule|Committer restriction"
+msgid "Quick actions can be used in the issues description and comment boxes."
msgstr ""
-msgid "Quick actions can be used in the issues description and comment boxes."
+msgid "Re-deploy"
msgstr ""
msgid "Read more"
@@ -3477,16 +3714,16 @@ msgstr ""
msgid "Real-time features"
msgstr ""
-msgid "RefSwitcher|Branches"
+msgid "Reference:"
msgstr ""
-msgid "RefSwitcher|Tags"
+msgid "Register / Sign In"
msgstr ""
-msgid "Reference:"
+msgid "Register and see your runners for this group."
msgstr ""
-msgid "Register / Sign In"
+msgid "Register and see your runners for this project."
msgstr ""
msgid "Registry"
@@ -3519,28 +3756,28 @@ msgstr ""
msgid "Remove"
msgstr ""
-msgid "Remove avatar"
+msgid "Remove Runner"
msgstr ""
-msgid "Remove project"
+msgid "Remove avatar"
msgstr ""
-msgid "Repair authentication"
+msgid "Remove priority"
msgstr ""
-msgid "Repo by URL"
+msgid "Remove project"
msgstr ""
msgid "Repository"
msgstr ""
-msgid "Repository has no locks."
+msgid "Repository Settings"
msgstr ""
msgid "Repository maintenance"
msgstr ""
-msgid "Repository mirror settings"
+msgid "Repository mirror"
msgstr ""
msgid "Repository storage"
@@ -3549,6 +3786,9 @@ msgstr ""
msgid "Request Access"
msgstr ""
+msgid "Require all users to accept Terms of Service and Privacy Policy when they access GitLab."
+msgstr ""
+
msgid "Reset git storage health information"
msgstr ""
@@ -3558,10 +3798,25 @@ msgstr ""
msgid "Reset runners registration token"
msgstr ""
+msgid "Resolve all discussions in new issue"
+msgstr ""
+
+msgid "Resolve conflicts on source branch"
+msgstr ""
+
msgid "Resolve discussion"
msgstr ""
-msgid "Response"
+msgid "Resume"
+msgstr ""
+
+msgid "Retry"
+msgstr ""
+
+msgid "Retry this job"
+msgstr ""
+
+msgid "Retry verification"
msgstr ""
msgid "Reveal value"
@@ -3574,7 +3829,7 @@ msgstr ""
msgid "Revert this merge request"
msgstr ""
-msgid "Review the process for configuring service providers in your identity provider — in this case, GitLab is the \"service provider\" or \"relying party\"."
+msgid "Review"
msgstr ""
msgid "Reviewing"
@@ -3583,28 +3838,31 @@ msgstr ""
msgid "Reviewing (merge request !%{mergeRequestId})"
msgstr ""
-msgid "Roadmap"
+msgid "Rollback"
msgstr ""
-msgid "Run CI/CD pipelines for external repositories"
+msgid "Runner token"
msgstr ""
msgid "Runners"
msgstr ""
-msgid "Running"
+msgid "Runners API"
msgstr ""
-msgid "SAML Single Sign On"
+msgid "Runners can be placed on separate users, servers, and even on your local machine."
msgstr ""
-msgid "SAML Single Sign On Settings"
+msgid "Running"
msgstr ""
-msgid "SHA1 fingerprint of the SAML token signing certificate. Get this from your identity provider, where it can also be called \"Thumbprint\"."
+msgid "SSH Keys"
msgstr ""
-msgid "SSH Keys"
+msgid "SSL Verification"
+msgstr ""
+
+msgid "Save"
msgstr ""
msgid "Save changes"
@@ -3628,15 +3886,30 @@ msgstr ""
msgid "Scheduling Pipelines"
msgstr ""
-msgid "Scoped issue boards"
+msgid "Scroll to bottom"
+msgstr ""
+
+msgid "Scroll to top"
msgstr ""
msgid "Search"
msgstr ""
+msgid "Search branches"
+msgstr ""
+
msgid "Search branches and tags"
msgstr ""
+msgid "Search files"
+msgstr ""
+
+msgid "Search for projects, issues, etc."
+msgstr ""
+
+msgid "Search merge requests"
+msgstr ""
+
msgid "Search milestones"
msgstr ""
@@ -3652,13 +3925,13 @@ msgstr ""
msgid "Seconds to wait for a storage access attempt"
msgstr ""
-msgid "Secret variables"
+msgid "Select"
msgstr ""
-msgid "Security report"
+msgid "Select Archive Format"
msgstr ""
-msgid "Select Archive Format"
+msgid "Select a namespace to fork the project"
msgstr ""
msgid "Select a timezone"
@@ -3673,10 +3946,19 @@ msgstr ""
msgid "Select branch/tag"
msgstr ""
-msgid "Select target branch"
+msgid "Select project"
msgstr ""
-msgid "Selective synchronization"
+msgid "Select project and zone to choose machine type"
+msgstr ""
+
+msgid "Select project to choose zone"
+msgstr ""
+
+msgid "Select source branch"
+msgstr ""
+
+msgid "Select target branch"
msgstr ""
msgid "Send email"
@@ -3694,9 +3976,6 @@ msgstr ""
msgid "Service Templates"
msgstr ""
-msgid "Service URL"
-msgstr ""
-
msgid "Session expiration, projects limit and attachment size."
msgstr ""
@@ -3721,9 +4000,6 @@ msgstr ""
msgid "Set up Koding"
msgstr ""
-msgid "Set up assertions/attributes/claims (email, first_name, last_name) and NameID according to %{docsLinkStart}the documentation %{icon}%{docsLinkEnd}"
-msgstr ""
-
msgid "SetPasswordToCloneLink|set a password"
msgstr ""
@@ -3733,19 +4009,22 @@ msgstr ""
msgid "Setup a specific Runner automatically"
msgstr ""
-msgid "Share the <strong>%{sso_label}</strong> with members so they can sign in to your group through your identity provider"
+msgid "Share"
msgstr ""
-msgid "SharedRunnersMinutesSettings|By resetting the pipeline minutes for this namespace, the currently used minutes will be set to zero."
+msgid "Shared Runners"
msgstr ""
-msgid "SharedRunnersMinutesSettings|Reset pipeline minutes"
+msgid "Show command"
msgstr ""
-msgid "SharedRunnersMinutesSettings|Reset used pipeline minutes"
+msgid "Show complete raw log"
msgstr ""
-msgid "Show command"
+msgid "Show latest version"
+msgstr ""
+
+msgid "Show latest version of the diff"
msgstr ""
msgid "Show parent pages"
@@ -3754,20 +4033,17 @@ msgstr ""
msgid "Show parent subgroups"
msgstr ""
+msgid "Show whitespace changes"
+msgstr ""
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] ""
-msgid "Sidebar|Change weight"
+msgid "Side-by-side"
msgstr ""
-msgid "Sidebar|No"
-msgstr ""
-
-msgid "Sidebar|None"
-msgstr ""
-
-msgid "Sidebar|Weight"
+msgid "Sign out"
msgstr ""
msgid "Sign-in restrictions"
@@ -3779,7 +4055,7 @@ msgstr ""
msgid "Size and domain settings for static websites"
msgstr ""
-msgid "Slack application"
+msgid "Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job"
msgstr ""
msgid "Snippets"
@@ -3791,13 +4067,13 @@ msgstr ""
msgid "Something went wrong on our end."
msgstr ""
-msgid "Something went wrong when toggling the button"
+msgid "Something went wrong on our end. Please try again!"
msgstr ""
-msgid "Something went wrong while fetching Dependency Scanning."
+msgid "Something went wrong when toggling the button"
msgstr ""
-msgid "Something went wrong while fetching SAST."
+msgid "Something went wrong while closing the %{issuable}. Please try again later"
msgstr ""
msgid "Something went wrong while fetching the projects."
@@ -3806,6 +4082,12 @@ msgstr ""
msgid "Something went wrong while fetching the registry list."
msgstr ""
+msgid "Something went wrong while reopening the %{issuable}. Please try again later"
+msgstr ""
+
+msgid "Something went wrong while resolving this discussion. Please try again."
+msgstr ""
+
msgid "Something went wrong. Please try again."
msgstr ""
@@ -3851,9 +4133,6 @@ msgstr ""
msgid "SortOptions|Least popular"
msgstr ""
-msgid "SortOptions|Less weight"
-msgstr ""
-
msgid "SortOptions|Milestone"
msgstr ""
@@ -3863,9 +4142,6 @@ msgstr ""
msgid "SortOptions|Milestone due soon"
msgstr ""
-msgid "SortOptions|More weight"
-msgstr ""
-
msgid "SortOptions|Most popular"
msgstr ""
@@ -3905,9 +4181,6 @@ msgstr ""
msgid "SortOptions|Start soon"
msgstr ""
-msgid "SortOptions|Weight"
-msgstr ""
-
msgid "Source"
msgstr ""
@@ -3926,9 +4199,36 @@ msgstr ""
msgid "Spam and Anti-bot Protection"
msgstr ""
+msgid "Specific Runners"
+msgstr ""
+
msgid "Specify the following URL during the Runner setup:"
msgstr ""
+msgid "Squash commits"
+msgstr ""
+
+msgid "Stage"
+msgstr ""
+
+msgid "Stage & Commit"
+msgstr ""
+
+msgid "Stage all changes"
+msgstr ""
+
+msgid "Stage changes"
+msgstr ""
+
+msgid "Staged"
+msgstr ""
+
+msgid "Staged %{type}"
+msgstr ""
+
+msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
+msgstr ""
+
msgid "StarProject|Star"
msgstr ""
@@ -3950,12 +4250,15 @@ msgstr ""
msgid "Started"
msgstr ""
-msgid "State your message to activate"
+msgid "Starts at (UTC)"
msgstr ""
msgid "Status"
msgstr ""
+msgid "Stop this environment"
+msgstr ""
+
msgid "Stopped"
msgstr ""
@@ -3965,16 +4268,19 @@ msgstr ""
msgid "Subgroups"
msgstr ""
-msgid "Switch branch/tag"
+msgid "Subscribe"
msgstr ""
-msgid "System"
+msgid "Subscribe at group level"
msgstr ""
-msgid "System Hooks"
+msgid "Subscribe at project level"
msgstr ""
-msgid "System header and footer:"
+msgid "Switch branch/tag"
+msgstr ""
+
+msgid "System Hooks"
msgstr ""
msgid "Tag (%{tag_count})"
@@ -3984,6 +4290,9 @@ msgstr[0] ""
msgid "Tags"
msgstr ""
+msgid "Tags:"
+msgstr ""
+
msgid "TagsPage|Browse commits"
msgstr ""
@@ -4047,7 +4356,7 @@ msgstr ""
msgid "TagsPage|Use git tag command to add a new one:"
msgstr ""
-msgid "TagsPage|Write your release notes or drag files here..."
+msgid "TagsPage|Write your release notes or drag files here…"
msgstr ""
msgid "TagsPage|protected"
@@ -4062,19 +4371,19 @@ msgstr ""
msgid "Team"
msgstr ""
-msgid "Thanks! Don't show me this again"
+msgid "Terms of Service Agreement and Privacy Policy"
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."
+msgid "Terms of Service and Privacy Policy"
msgstr ""
-msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project"
+msgid "Test coverage parsing"
msgstr ""
-msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
+msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project"
msgstr ""
-msgid "The X509 Certificate to use when mutual TLS is required to communicate with the external authorization service. If left blank, the server certificate is still validated when accessing over HTTPS."
+msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
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."
@@ -4083,9 +4392,6 @@ msgstr ""
msgid "The collection of events added to the data gathered for that stage."
msgstr ""
-msgid "The connection will time out after %{timeout}. For repositories that take longer, use a clone/push combination."
-msgstr ""
-
msgid "The fork relationship has been removed."
msgstr ""
@@ -4104,7 +4410,7 @@ 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 passphrase required to decrypt the private key. This is optional and the value is encrypted at rest."
+msgid "The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>"
msgstr ""
msgid "The phase of the development lifecycle."
@@ -4113,9 +4419,6 @@ msgstr ""
msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit."
msgstr ""
-msgid "The private key to use when a client certificate is provided. This value is encrypted at rest."
-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 ""
@@ -4137,7 +4440,7 @@ msgstr ""
msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request."
msgstr ""
-msgid "The roadmap shows the progress of your epics along a timeline"
+msgid "The secure token used by the Runner to checkout the project"
msgstr ""
msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time."
@@ -4164,13 +4467,19 @@ msgstr ""
msgid "There are no issues to show"
msgstr ""
+msgid "There are no labels yet"
+msgstr ""
+
msgid "There are no merge requests to show"
msgstr ""
msgid "There are problems accessing Git storage: "
msgstr ""
-msgid "There was an error loading results"
+msgid "There was an error loading jobs"
+msgstr ""
+
+msgid "There was an error loading latest pipeline"
msgstr ""
msgid "There was an error loading users activity calendar."
@@ -4191,12 +4500,21 @@ msgstr ""
msgid "There was an error when unsubscribing from this label."
msgstr ""
-msgid "This board\\'s scope is reduced"
+msgid "They can be managed using the %{link}."
+msgstr ""
+
+msgid "This GitLab instance does not provide any shared Runners yet. Instance administrators can register shared Runners in the admin area."
+msgstr ""
+
+msgid "This diff is collapsed."
msgstr ""
msgid "This directory"
msgstr ""
+msgid "This group does not provide any group Runners yet."
+msgstr ""
+
msgid "This is a confidential issue."
msgstr ""
@@ -4218,6 +4536,15 @@ msgstr ""
msgid "This job depends on upstream jobs that need to succeed in order for this job to be triggered"
msgstr ""
+msgid "This job does not have a trace."
+msgstr ""
+
+msgid "This job has been canceled"
+msgstr ""
+
+msgid "This job has been skipped"
+msgstr ""
+
msgid "This job has not been triggered yet"
msgstr ""
@@ -4236,19 +4563,28 @@ msgstr ""
msgid "This merge request is locked."
msgstr ""
+msgid "This option is disabled while you still have unstaged changes"
+msgstr ""
+
msgid "This page is unavailable because you are not allowed to read information across multiple projects."
msgstr ""
+msgid "This page will be removed in a future release."
+msgstr ""
+
msgid "This project"
msgstr ""
+msgid "This project does not belong to a group and can therefore not make use of group Runners."
+msgstr ""
+
msgid "This repository"
msgstr ""
-msgid "This will delete the custom metric, Are you sure?"
+msgid "This source diff could not be displayed because it is too large."
msgstr ""
-msgid "Those emails automatically become issues (with the comments becoming the email conversation) listed here."
+msgid "This user has no identities"
msgstr ""
msgid "Time before an issue gets scheduled"
@@ -4260,10 +4596,10 @@ msgstr ""
msgid "Time between merge request creation and merge/close"
msgstr ""
-msgid "Time between updates and capacity settings."
+msgid "Time remaining"
msgstr ""
-msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
+msgid "Time spent"
msgstr ""
msgid "Time tracking"
@@ -4287,6 +4623,9 @@ msgstr ""
msgid "Timeago|%s days remaining"
msgstr ""
+msgid "Timeago|%s hours ago"
+msgstr ""
+
msgid "Timeago|%s hours remaining"
msgstr ""
@@ -4302,6 +4641,9 @@ msgstr ""
msgid "Timeago|%s months remaining"
msgstr ""
+msgid "Timeago|%s seconds ago"
+msgstr ""
+
msgid "Timeago|%s seconds remaining"
msgstr ""
@@ -4317,46 +4659,43 @@ msgstr ""
msgid "Timeago|%s years remaining"
msgstr ""
-msgid "Timeago|1 day remaining"
-msgstr ""
-
-msgid "Timeago|1 hour remaining"
+msgid "Timeago|1 day ago"
msgstr ""
-msgid "Timeago|1 minute remaining"
+msgid "Timeago|1 day remaining"
msgstr ""
-msgid "Timeago|1 month remaining"
+msgid "Timeago|1 hour ago"
msgstr ""
-msgid "Timeago|1 week remaining"
+msgid "Timeago|1 hour remaining"
msgstr ""
-msgid "Timeago|1 year remaining"
+msgid "Timeago|1 minute ago"
msgstr ""
-msgid "Timeago|Past due"
+msgid "Timeago|1 minute remaining"
msgstr ""
-msgid "Timeago|a day ago"
+msgid "Timeago|1 month ago"
msgstr ""
-msgid "Timeago|a month ago"
+msgid "Timeago|1 month remaining"
msgstr ""
-msgid "Timeago|a week ago"
+msgid "Timeago|1 week ago"
msgstr ""
-msgid "Timeago|a year ago"
+msgid "Timeago|1 week remaining"
msgstr ""
-msgid "Timeago|about %s hours ago"
+msgid "Timeago|1 year ago"
msgstr ""
-msgid "Timeago|about a minute ago"
+msgid "Timeago|1 year remaining"
msgstr ""
-msgid "Timeago|about an hour ago"
+msgid "Timeago|Past due"
msgstr ""
msgid "Timeago|in %s days"
@@ -4398,10 +4737,13 @@ msgstr ""
msgid "Timeago|in 1 year"
msgstr ""
-msgid "Timeago|in a while"
+msgid "Timeago|just now"
+msgstr ""
+
+msgid "Timeago|right now"
msgstr ""
-msgid "Timeago|less than a minute ago"
+msgid "Timeout"
msgstr ""
msgid "Time|hr"
@@ -4418,19 +4760,10 @@ msgstr ""
msgid "Tip:"
msgstr ""
-msgid "Title"
-msgstr ""
-
msgid "To GitLab"
msgstr ""
-msgid "To connect GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to connect."
-msgstr ""
-
-msgid "To connect GitHub repositories, you first need to authorize GitLab to access the list of your GitHub repositories:"
-msgstr ""
-
-msgid "To connect an SVN repository, check out %{svn_link}."
+msgid "To add an SSH key you need to %{generate_link_start}generate one%{link_end} or use an %{existing_link_start}existing key%{link_end}."
msgstr ""
msgid "To import GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
@@ -4442,19 +4775,19 @@ msgstr ""
msgid "To import an SVN repository, check out %{svn_link}."
msgstr ""
-msgid "To only use CI/CD features for an external repository, choose <strong>CI/CD for external repo</strong>."
+msgid "To start serving your jobs you can add Runners to your group"
msgstr ""
-msgid "To set up SAML authentication for your group through an identity provider like Azure, Okta, Onelogin, Ping Identity, or your custom SAML 2.0 provider:"
+msgid "To validate your GitLab CI configurations, go to 'CI/CD → Pipelines' inside your project, and click on the 'CI Lint' button."
msgstr ""
-msgid "To validate your GitLab CI configurations, go to 'CI/CD → Pipelines' inside your project, and click on the 'CI Lint' button."
+msgid "Todo"
msgstr ""
-msgid "To view the roadmap, add a planned start or finish date to one of your epics in this group or its subgroups. Only epics in the past 3 months and the next 3 months are shown."
+msgid "Toggle Sidebar"
msgstr ""
-msgid "Todo"
+msgid "Toggle discussion"
msgstr ""
msgid "Toggle sidebar"
@@ -4466,6 +4799,9 @@ msgstr ""
msgid "ToggleButton|Toggle Status: ON"
msgstr ""
+msgid "Too many changes to show."
+msgstr ""
+
msgid "Total Time"
msgstr ""
@@ -4475,22 +4811,19 @@ msgstr ""
msgid "Total: %{total}"
msgstr ""
-msgid "Track activity with Contribution Analytics."
-msgstr ""
-
-msgid "Track groups of issues that share a theme, across projects and milestones"
-msgstr ""
-
msgid "Track time with quick actions"
msgstr ""
msgid "Trigger this manual action"
msgstr ""
-msgid "Turn on Service Desk"
+msgid "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."
+msgstr ""
+
+msgid "Try again"
msgstr ""
-msgid "Unknown"
+msgid "Unable to load the diff. %{button_try_again}"
msgstr ""
msgid "Unlock"
@@ -4502,25 +4835,44 @@ msgstr ""
msgid "Unresolve discussion"
msgstr ""
+msgid "Unstage all changes"
+msgstr ""
+
+msgid "Unstage changes"
+msgstr ""
+
+msgid "Unstaged"
+msgstr ""
+
+msgid "Unstaged %{type}"
+msgstr ""
+
+msgid "Unstaged and staged %{type}"
+msgstr ""
+
msgid "Unstar"
msgstr ""
-msgid "Up to date"
+msgid "Unsubscribe"
msgstr ""
-msgid "Upgrade your plan to activate Advanced Global Search."
+msgid "Unsubscribe at group level"
msgstr ""
-msgid "Upgrade your plan to activate Contribution Analytics."
+msgid "Unsubscribe at project level"
msgstr ""
-msgid "Upgrade your plan to activate Group Webhooks."
+msgid "Unverified"
msgstr ""
-msgid "Upgrade your plan to activate Issue weight."
+msgid "Up to date"
msgstr ""
-msgid "Upgrade your plan to improve Issue boards."
+msgid "Update %{files}"
+msgid_plural "Update %{files} files"
+msgstr[0] ""
+
+msgid "Update your group name, description, avatar, and other general settings."
msgstr ""
msgid "Upload New File"
@@ -4541,7 +4893,7 @@ msgstr ""
msgid "Usage statistics"
msgstr ""
-msgid "Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab"
+msgid "Use group milestones to manage issues from multiple projects in the same milestone."
msgstr ""
msgid "Use the following registration token during setup:"
@@ -4550,10 +4902,13 @@ msgstr ""
msgid "Use your global notification setting"
msgstr ""
-msgid "Used by members to sign in to your group in GitLab"
+msgid "User and IP Rate Limits"
msgstr ""
-msgid "User and IP Rate Limits"
+msgid "Users"
+msgstr ""
+
+msgid "Variables"
msgstr ""
msgid "Variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. You can use variables for passwords, secret keys, or whatever you want."
@@ -4568,10 +4923,7 @@ msgstr ""
msgid "Various settings that affect GitLab performance."
msgstr ""
-msgid "View and edit lines"
-msgstr ""
-
-msgid "View epics list"
+msgid "Verified"
msgstr ""
msgid "View file @ "
@@ -4580,9 +4932,15 @@ msgstr ""
msgid "View group labels"
msgstr ""
+msgid "View jobs"
+msgstr ""
+
msgid "View labels"
msgstr ""
+msgid "View log"
+msgstr ""
+
msgid "View open merge request"
msgstr ""
@@ -4610,9 +4968,6 @@ msgstr ""
msgid "Want to see the data? Please ask an administrator for access."
msgstr ""
-msgid "We could not verify that one of your projects on GCP has billing enabled. Please try again."
-msgstr ""
-
msgid "We don't have enough data to show this stage."
msgstr ""
@@ -4625,13 +4980,10 @@ msgstr ""
msgid "Web terminal"
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"
+msgid "When a runner is locked, it cannot be assigned to other projects"
msgstr ""
-msgid "When leaving the URL blank, classification labels can still be specified whitout disabling cross project features or performing external authorization checks."
+msgid "When enabled, users cannot use GitLab until the terms have been accepted."
msgstr ""
msgid "Wiki"
@@ -4658,7 +5010,31 @@ msgstr ""
msgid "WikiEdit|There is already a page with the same title in that path."
msgstr ""
-msgid "WikiEmptyPageError|You are not allowed to create wiki pages"
+msgid "WikiEmptyIssueMessage|Suggest wiki improvement"
+msgstr ""
+
+msgid "WikiEmptyIssueMessage|You must be a project member in order to add wiki pages. If you have suggestions for how to improve the wiki for this project, consider opening an issue in the %{issues_link}."
+msgstr ""
+
+msgid "WikiEmptyIssueMessage|issue tracker"
+msgstr ""
+
+msgid "WikiEmpty|A wiki is where you can store all the details about your project. This can include why you've created it, its principles, how to use it, and so on."
+msgstr ""
+
+msgid "WikiEmpty|Create your first page"
+msgstr ""
+
+msgid "WikiEmpty|Suggest wiki improvement"
+msgstr ""
+
+msgid "WikiEmpty|The wiki lets you write documentation for your project"
+msgstr ""
+
+msgid "WikiEmpty|This project has no wiki pages"
+msgstr ""
+
+msgid "WikiEmpty|You must be a project member in order to add wiki pages."
msgstr ""
msgid "WikiHistoricalPage|This is an old version of this page."
@@ -4694,6 +5070,12 @@ msgstr ""
msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?"
msgstr ""
+msgid "WikiPageConfirmDelete|Delete page"
+msgstr ""
+
+msgid "WikiPageConfirmDelete|Delete page %{pageTitle}?"
+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 ""
@@ -4709,7 +5091,7 @@ msgstr ""
msgid "WikiPage|Page slug"
msgstr ""
-msgid "WikiPage|Write your content or drag files here..."
+msgid "WikiPage|Write your content or drag files here…"
msgstr ""
msgid "Wiki|Create Page"
@@ -4721,9 +5103,6 @@ msgstr ""
msgid "Wiki|Edit Page"
msgstr ""
-msgid "Wiki|Empty page"
-msgstr ""
-
msgid "Wiki|More Pages"
msgstr ""
@@ -4742,13 +5121,10 @@ 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 ""
-msgid "Write a commit message..."
+msgid "Yes"
msgstr ""
msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
@@ -4766,7 +5142,7 @@ msgstr ""
msgid "You are on a read-only GitLab instance."
msgstr ""
-msgid "You are on a secondary (read-only) Geo node. If you want to make any changes, you must visit the %{primary_node}."
+msgid "You can %{linkStart}view the blob%{linkEnd} instead."
msgstr ""
msgid "You can also create a project from the command line."
@@ -4775,6 +5151,9 @@ msgstr ""
msgid "You can also star a label to make it a priority label."
msgstr ""
+msgid "You can also test your .gitlab-ci.yml in the %{linkStart}Lint%{linkEnd}"
+msgstr ""
+
msgid "You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}"
msgstr ""
@@ -4787,28 +5166,31 @@ msgstr ""
msgid "You can only edit 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."
+msgid "You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}"
msgstr ""
msgid "You cannot write to this read-only GitLab instance."
msgstr ""
-msgid "You do not have the correct permissions to override the settings from the LDAP group sync."
+msgid "You do not have any assigned merge requests"
msgstr ""
msgid "You have no permissions"
msgstr ""
+msgid "You have not created any merge requests"
+msgstr ""
+
msgid "You have reached your project limit"
msgstr ""
-msgid "You must have master access to force delete a lock"
+msgid "You must accept our Terms of Service and privacy policy in order to register an account"
msgstr ""
-msgid "You must sign in to star a project"
+msgid "You must have maintainer access to force delete a lock"
msgstr ""
-msgid "You need a different license to enable FileLocks feature"
+msgid "You must sign in to star a project"
msgstr ""
msgid "You need permission."
@@ -4877,12 +5259,11 @@ msgstr ""
msgid "Your projects"
msgstr ""
-msgid "among other things"
+msgid "ago"
msgstr ""
-msgid "and %d fixed vulnerability"
-msgid_plural "and %d fixed vulnerabilities"
-msgstr[0] ""
+msgid "among other things"
+msgstr ""
msgid "assign yourself"
msgstr ""
@@ -4890,133 +5271,35 @@ msgstr ""
msgid "branch name"
msgstr ""
-msgid "by"
-msgstr ""
-
-msgid "ciReport|%{type} detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|%{type} detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Code quality"
-msgstr ""
-
-msgid "ciReport|DAST detected no alerts by analyzing the review app"
-msgstr ""
-
-msgid "ciReport|Dependency scanning"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Failed to load %{reportName} report"
-msgstr ""
-
-msgid "ciReport|Fixed:"
-msgstr ""
-
-msgid "ciReport|Instances"
-msgstr ""
-
-msgid "ciReport|Learn more about whitelisting"
-msgstr ""
-
-msgid "ciReport|Loading %{reportName} report"
-msgstr ""
-
-msgid "ciReport|No changes to code quality"
-msgstr ""
-
-msgid "ciReport|No changes to performance metrics"
-msgstr ""
-
-msgid "ciReport|Performance metrics"
-msgstr ""
-
-msgid "ciReport|SAST"
-msgstr ""
-
-msgid "ciReport|SAST detected"
-msgstr ""
-
-msgid "ciReport|SAST detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|SAST detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|SAST:container no vulnerabilities were found"
-msgstr ""
-
-msgid "ciReport|Security scanning"
-msgstr ""
-
-msgid "ciReport|Security scanning failed loading any results"
-msgstr ""
-
-msgid "ciReport|Show complete code vulnerabilities report"
-msgstr ""
-
-msgid "ciReport|Unapproved vulnerabilities (red) can be marked as approved. %{helpLink}"
-msgstr ""
-
-msgid "ciReport|no vulnerabilities"
-msgstr ""
-
msgid "command line instructions"
msgstr ""
msgid "connecting"
msgstr ""
-msgid "could not read private key, is the passphrase correct?"
-msgstr ""
-
msgid "day"
msgid_plural "days"
msgstr[0] ""
-msgid "detected %d fixed vulnerability"
-msgid_plural "detected %d fixed vulnerabilities"
-msgstr[0] ""
-
-msgid "detected %d new vulnerability"
-msgid_plural "detected %d new vulnerabilities"
-msgstr[0] ""
-
-msgid "detected no vulnerabilities"
+msgid "deploy token"
msgstr ""
-msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
+msgid "disabled"
msgstr ""
-msgid "here"
-msgstr ""
-
-msgid "importing"
+msgid "enabled"
msgstr ""
-msgid "in progress"
-msgstr ""
-
-msgid "is invalid because there is downstream lock"
+msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
msgstr ""
-msgid "is invalid because there is upstream lock"
+msgid "for this project"
msgstr ""
-msgid "is not a valid X509 certificate."
+msgid "importing"
msgstr ""
-msgid "locked by %{path_lock_user_name} %{created_at}"
+msgid "latest version"
msgstr ""
msgid "merge request"
@@ -5035,28 +5318,7 @@ msgstr ""
msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage is %{emphasisStart} unchanged %{emphasisEnd} at %{memoryFrom}MB"
msgstr ""
-msgid "mrWidget|Add approval"
-msgstr ""
-
-msgid "mrWidget|Allows edits from maintainers"
-msgstr ""
-
-msgid "mrWidget|An error occured while removing your approval."
-msgstr ""
-
-msgid "mrWidget|An error occured while retrieving approval data for this merge request."
-msgstr ""
-
-msgid "mrWidget|An error occured while submitting your approval."
-msgstr ""
-
-msgid "mrWidget|Approve"
-msgstr ""
-
-msgid "mrWidget|Approved"
-msgstr ""
-
-msgid "mrWidget|Approved by"
+msgid "mrWidget|Allows commits from members who can merge to the target branch"
msgstr ""
msgid "mrWidget|Cancel automatic merge"
@@ -5083,6 +5345,9 @@ msgstr ""
msgid "mrWidget|Closes"
msgstr ""
+msgid "mrWidget|Create an issue to resolve them later"
+msgstr ""
+
msgid "mrWidget|Deployment statistics are not available currently"
msgstr ""
@@ -5137,9 +5402,6 @@ msgstr ""
msgid "mrWidget|Remove source branch"
msgstr ""
-msgid "mrWidget|Remove your approval"
-msgstr ""
-
msgid "mrWidget|Request to merge"
msgstr ""
@@ -5179,6 +5441,9 @@ msgstr ""
msgid "mrWidget|There are merge conflicts"
msgstr ""
+msgid "mrWidget|There are unresolved discussions. Please resolve these discussions"
+msgstr ""
+
msgid "mrWidget|This merge request failed to be merged automatically"
msgstr ""
@@ -5228,7 +5493,7 @@ msgstr ""
msgid "personal access token"
msgstr ""
-msgid "private key does not match certificate."
+msgid "remaining"
msgstr ""
msgid "remove due date"
@@ -5243,9 +5508,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "to help your contributors communicate effectively!"
-msgstr ""
-
msgid "username"
msgstr ""
@@ -5255,3 +5517,7 @@ msgstr ""
msgid "with %{additions} additions, %{deletions} deletions."
msgstr ""
+msgid "within %d minute "
+msgid_plural "within %d minutes "
+msgstr[0] ""
+
diff --git a/locale/it/gitlab.po b/locale/it/gitlab.po
index 41d6a76be66..9db2619c625 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: 2018-04-04 19:35+0200\n"
-"PO-Revision-Date: 2018-04-05 03:37-0400\n"
+"POT-Creation-Date: 2018-07-01 16:35+1000\n"
+"PO-Revision-Date: 2018-07-01 11:02\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Italian\n"
"Language: it_IT\n"
@@ -16,8 +16,10 @@ msgstr ""
"X-Crowdin-Language: it\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-msgid " and"
-msgstr ""
+msgid "%d changed file"
+msgid_plural "%d changed files"
+msgstr[0] ""
+msgstr[1] ""
msgid "%d commit"
msgid_plural "%d commits"
@@ -51,6 +53,16 @@ msgstr[1] ""
msgid "%d metric"
msgid_plural "%d metrics"
+msgstr[0] "%d metrica"
+msgstr[1] "%d metriche"
+
+msgid "%d staged change"
+msgid_plural "%d staged changes"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "%d unstaged change"
+msgid_plural "%d unstaged changes"
msgstr[0] ""
msgstr[1] ""
@@ -63,19 +75,28 @@ msgid "%{actionText} & %{openOrClose} %{noteable}"
msgstr ""
msgid "%{commit_author_link} authored %{commit_timeago}"
-msgstr ""
+msgstr "%{commit_author_link} fatto %{commit_timeago}"
msgid "%{count} participant"
msgid_plural "%{count} participants"
msgstr[0] "%{count} partecipante"
msgstr[1] "%{count} partecipanti"
+msgid "%{filePath} deleted"
+msgstr ""
+
+msgid "%{group_docs_link_start}Groups%{group_docs_link_end} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects."
+msgstr ""
+
msgid "%{loadingIcon} Started"
msgstr ""
msgid "%{lock_path} is locked by GitLab User %{lock_user_id}"
msgstr ""
+msgid "%{nip_domain} can be used as an alternative to a custom domain."
+msgstr ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr "%{number_commits_behind} commits precedenti %{default_branch}, %{number_commits_ahead} commits avanti"
@@ -88,6 +109,9 @@ msgstr "%{number_of_failures} di %{maximum_failures} fallimenti. Gitlab non rite
msgid "%{openOrClose} %{noteable}"
msgstr ""
+msgid "%{percent}%% complete"
+msgstr ""
+
msgid "%{storage_name}: failed storage access attempt on host:"
msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:"
msgstr[0] "%{storage_name}: tentativo d'accesso all'archiviazione fallito da parte dell'host:"
@@ -96,15 +120,62 @@ msgstr[1] "%{storage_name}: %{failed_attempts} tentativi d'accesso all'archiviaz
msgid "%{text} is available"
msgstr "%{text} è disponibile"
+msgid "%{title} changes"
+msgstr ""
+
+msgid "%{unstaged} unstaged and %{staged} staged changes"
+msgstr ""
+
msgid "(checkout the %{link} for information on how to install it)."
msgstr "(vedi il %{link} su come installarlo)."
msgid "+ %{moreCount} more"
msgstr "+ %{moreCount} più"
+msgid "- Runner is active and can process any new jobs"
+msgstr ""
+
+msgid "- Runner is paused and will not receive any new jobs"
+msgstr ""
+
msgid "- show less"
msgstr "- riduci"
+msgid "1 %{type} addition"
+msgid_plural "%{count} %{type} additions"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 %{type} modification"
+msgid_plural "%{count} %{type} modifications"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 closed issue"
+msgid_plural "%d closed issues"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 closed merge request"
+msgid_plural "%d closed merge requests"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 merged merge request"
+msgid_plural "%d merged merge requests"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 open issue"
+msgid_plural "%d open issues"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 open merge request"
+msgid_plural "%d open merge requests"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] "1 pipeline"
@@ -116,9 +187,27 @@ msgstr "Primo contributo!"
msgid "2FA enabled"
msgstr "2FA abilitata"
+msgid "403|Please contact your GitLab administrator to get the permission."
+msgstr ""
+
+msgid "403|You don't have the permission to access this page."
+msgstr ""
+
+msgid "404|Make sure the address is correct and the page hasn't moved."
+msgstr ""
+
+msgid "404|Page Not Found"
+msgstr ""
+
+msgid "404|Please contact your GitLab administrator if you think this is a mistake."
+msgstr ""
+
msgid "<strong>Removes</strong> source branch"
msgstr ""
+msgid "A 'Runner' is a process which runs a job. You can setup as many Runners as you need."
+msgstr ""
+
msgid "A collection of graphs regarding Continuous Integration"
msgstr "Un insieme di grafici riguardo la Continuous Integration"
@@ -128,6 +217,9 @@ msgstr ""
msgid "A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}."
msgstr ""
+msgid "A regular expression that will be used to find the test coverage output in the job trace. Leave blank to disable"
+msgstr ""
+
msgid "A user with write access to the source branch selected this option"
msgstr ""
@@ -140,36 +232,39 @@ msgstr "Segnalazioni di abuso"
msgid "Abuse reports"
msgstr ""
+msgid "Accept terms"
+msgstr ""
+
msgid "Access Tokens"
msgstr "Token di accesso"
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 "L'accesso agli storages è stato temporaneamente disabilitato per consentire il mount di ripristino. Resetta le info d'archiviazione dopo che l'issue è stato risolto per consentire nuovamente l'accesso."
+msgid "Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report."
+msgstr ""
+
msgid "Account"
msgstr "Account"
-msgid "Account and limit settings"
+msgid "Account and limit"
msgstr ""
msgid "Active"
msgstr "Attivo"
+msgid "Active Sessions"
+msgstr ""
+
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 Kubernetes cluster"
msgstr ""
@@ -182,6 +277,9 @@ msgstr ""
msgid "Add new directory"
msgstr "Aggiungi una directory (cartella)"
+msgid "Add reaction"
+msgstr ""
+
msgid "Add todo"
msgstr ""
@@ -251,7 +349,10 @@ msgstr ""
msgid "All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings."
msgstr ""
-msgid "Allow edits from maintainers."
+msgid "Allow commits from members who can merge to the target branch."
+msgstr ""
+
+msgid "Allow public access to pipelines and job details, including output logs and artifacts"
msgstr ""
msgid "Allow rendering of PlantUML diagrams in Asciidoc documents."
@@ -263,31 +364,37 @@ msgstr ""
msgid "Allows you to add and manage Kubernetes clusters."
msgstr ""
-msgid "Also called \"Issuer\" or \"Relying party trust identifier\""
+msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
msgstr ""
-msgid "Also called \"Relying party service URL\" or \"Reply URL\""
+msgid "An error occured creating the new branch."
msgstr ""
-msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to connect."
+msgid "An error occured whilst loading all the files."
msgstr ""
-msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
+msgid "An error occured whilst loading the file content."
msgstr ""
-msgid "An error occurred previewing the blob"
+msgid "An error occured whilst loading the file."
msgstr ""
-msgid "An error occurred when toggling the notification subscription"
-msgstr "Errore durante l'attivazione/disattivazione della sottoscrizione per l'iscrizione"
+msgid "An error occured whilst loading the merge request changes."
+msgstr ""
+
+msgid "An error occured whilst loading the merge request version data."
+msgstr ""
-msgid "An error occurred when updating the issue weight"
+msgid "An error occured whilst loading the merge request."
msgstr ""
-msgid "An error occurred while adding approver"
+msgid "An error occurred previewing the blob"
msgstr ""
-msgid "An error occurred while detecting host keys"
+msgid "An error occurred when toggling the notification subscription"
+msgstr "Errore durante l'attivazione/disattivazione della sottoscrizione per l'iscrizione"
+
+msgid "An error occurred while dismissing the alert. Refresh the page and try again."
msgstr ""
msgid "An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again."
@@ -305,10 +412,7 @@ msgstr ""
msgid "An error occurred while getting projects"
msgstr ""
-msgid "An error occurred while importing project"
-msgstr ""
-
-msgid "An error occurred while initializing path locks"
+msgid "An error occurred while importing project: ${details}"
msgstr ""
msgid "An error occurred while loading commits"
@@ -326,9 +430,6 @@ msgstr ""
msgid "An error occurred while making the request."
msgstr ""
-msgid "An error occurred while removing approver"
-msgstr ""
-
msgid "An error occurred while rendering KaTeX"
msgstr ""
@@ -341,9 +442,6 @@ msgstr ""
msgid "An error occurred while retrieving diff"
msgstr ""
-msgid "An error occurred while saving LDAP override status. Please try again."
-msgstr ""
-
msgid "An error occurred while saving assignees"
msgstr ""
@@ -353,9 +451,6 @@ msgstr ""
msgid "An error occurred. Please try again."
msgstr "Si è verificato un errore. Riprova."
-msgid "Any Label"
-msgstr ""
-
msgid "Appearance"
msgstr "Aspetto"
@@ -368,28 +463,28 @@ msgstr "Apr"
msgid "April"
msgstr "Aprile"
-msgid "Archived project! Repository is read-only"
-msgstr "Progetto archiviato! La Repository è sola-lettura"
+msgid "Archived project! Repository and other project resources are read-only"
+msgstr ""
msgid "Are you sure you want to delete this pipeline schedule?"
msgstr "Sei sicuro di voler cancellare questa pipeline programmata?"
+msgid "Are you sure you want to remove this identity?"
+msgstr ""
+
msgid "Are you sure you want to reset registration token?"
msgstr "Sei sicuro di voler ripristinare il token di registrazione?"
msgid "Are you sure you want to reset the health check token?"
msgstr "Confermi di voler resettare il token di controllo di stato?"
-msgid "Are you sure you want to unlock %{path_lock_path}?"
-msgstr ""
-
msgid "Are you sure?"
msgstr "Sei sicuro?"
msgid "Artifacts"
msgstr "Artefatti"
-msgid "Assertion consumer service URL"
+msgid "Ask your group maintainer to setup a group Runner."
msgstr ""
msgid "Assign custom color like #FF0000"
@@ -413,9 +508,15 @@ msgstr ""
msgid "Assigned to :name"
msgstr ""
+msgid "Assigned to me"
+msgstr ""
+
msgid "Assignee"
msgstr ""
+msgid "Assignee(s)"
+msgstr ""
+
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "Aggiungi un file tramite trascina &amp; rilascia ( drag &amp; drop) o %{upload_link}"
@@ -449,8 +550,11 @@ msgstr ""
msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
msgstr "Le app d'auto-review e l'Auto Deploy (rilascio automatico) necessita di un nome dominio per funzionare correttamente."
-msgid "AutoDevOps|Auto DevOps (Beta)"
-msgstr "Auto DevOps (Béta)"
+msgid "Auto-cancel redundant, pending pipelines"
+msgstr ""
+
+msgid "AutoDevOps|Auto DevOps"
+msgstr ""
msgid "AutoDevOps|Auto DevOps documentation"
msgstr "Documentazione Auto DevOps"
@@ -470,79 +574,109 @@ msgstr ""
msgid "AutoDevOps|add a Kubernetes cluster"
msgstr ""
-msgid "AutoDevOps|enable Auto DevOps (Beta)"
+msgid "AutoDevOps|enable Auto DevOps"
msgstr ""
msgid "Available"
msgstr "Disponibile"
+msgid "Available group Runners : %{runners}"
+msgstr ""
+
+msgid "Available group Runners : %{runners}."
+msgstr ""
+
msgid "Avatar will be removed. Are you sure?"
msgstr ""
msgid "Average per day: %{average}"
msgstr ""
-msgid "Background Color"
+msgid "Background color"
msgstr ""
msgid "Background jobs"
msgstr ""
-msgid "Begin with the selected commit"
+msgid "Badges"
+msgstr ""
+
+msgid "Badges|A new badge was added."
+msgstr ""
+
+msgid "Badges|Add badge"
+msgstr ""
+
+msgid "Badges|Adding the badge failed, please check the entered URLs and try again."
+msgstr ""
+
+msgid "Badges|Badge image URL"
+msgstr ""
+
+msgid "Badges|Badge image preview"
+msgstr ""
+
+msgid "Badges|Delete badge"
+msgstr ""
+
+msgid "Badges|Delete badge?"
+msgstr ""
+
+msgid "Badges|Deleting the badge failed, please try again."
msgstr ""
-msgid "Billing"
+msgid "Badges|Group Badge"
msgstr ""
-msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan."
+msgid "Badges|Link"
msgstr ""
-msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available."
+msgid "Badges|No badge image"
msgstr ""
-msgid "BillingPlans|Current plan"
+msgid "Badges|No image to preview"
msgstr ""
-msgid "BillingPlans|Customer Support"
+msgid "Badges|Project Badge"
msgstr ""
-msgid "BillingPlans|Downgrade"
+msgid "Badges|Reload badge image"
msgstr ""
-msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
+msgid "Badges|Save changes"
msgstr ""
-msgid "BillingPlans|Manage plan"
+msgid "Badges|Saving the badge failed, please check the entered URLs and try again."
msgstr ""
-msgid "BillingPlans|Please contact %{customer_support_link} in that case."
+msgid "Badges|The %{docsLinkStart}variables%{docsLinkEnd} GitLab supports: %{placeholders}"
msgstr ""
-msgid "BillingPlans|See all %{plan_name} features"
+msgid "Badges|The badge was deleted."
msgstr ""
-msgid "BillingPlans|This group uses the plan associated with its parent group."
+msgid "Badges|The badge was saved."
msgstr ""
-msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
+msgid "Badges|This group has no badges"
msgstr ""
-msgid "BillingPlans|Upgrade"
+msgid "Badges|This project has no badges"
msgstr ""
-msgid "BillingPlans|You are currently on the %{plan_link} plan."
+msgid "Badges|Your badges"
msgstr ""
-msgid "BillingPlans|frequently asked questions"
+msgid "Begin with the selected commit"
msgstr ""
-msgid "BillingPlans|monthly"
+msgid "Below are examples of regex for existing tools:"
msgstr ""
-msgid "BillingPlans|paid annually at %{price_per_year}"
+msgid "Boards"
msgstr ""
-msgid "BillingPlans|per user"
+msgid "Branch %{branchName} was not found in this project's repository."
msgstr ""
msgid "Branch (%{branch_count})"
@@ -622,8 +756,8 @@ msgstr "Nessuna branch da mostrare"
msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
msgstr "Una volta confermato e premuto %{delete_protected_branch} non sarà possibile ripristinare allo stato precedente."
-msgid "Branches|Only a project master or owner can delete a protected branch"
-msgstr "Solo gli Owner e i Master possono eliminare una branch protetta"
+msgid "Branches|Only a project maintainer or owner can delete a protected branch"
+msgstr ""
msgid "Branches|Overview"
msgstr ""
@@ -658,9 +792,6 @@ msgstr ""
msgid "Branches|Stale branches"
msgstr ""
-msgid "Branches|The branch could not be updated automatically because it has diverged from its upstream counterpart."
-msgstr ""
-
msgid "Branches|The default branch cannot be deleted"
msgstr "La branch predefinita non può esser eliminata"
@@ -673,15 +804,9 @@ msgstr "Per evitare perdita di dati considera di mergiare questa branch prima di
msgid "Branches|To confirm, type %{branch_name_confirmation}:"
msgstr "Per confermare, scrivi %{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 ""
-
msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
msgstr "Stai per eliminare la branch protetta (%{branch_name}) in maniera permanente."
-msgid "Branches|diverged from upstream"
-msgstr ""
-
msgid "Branches|merged"
msgstr "mergiata"
@@ -703,40 +828,79 @@ msgstr "Esplora Files"
msgid "Browse files"
msgstr "Guarda i files"
-msgid "Business"
-msgstr ""
-
msgid "ByAuthor|by"
msgstr "per"
msgid "CI / CD"
msgstr "CI / CD"
-msgid "CI/CD"
+msgid "CI / CD Settings"
msgstr ""
msgid "CI/CD configuration"
msgstr ""
-msgid "CI/CD for external repo"
+msgid "CI/CD settings"
+msgstr ""
+
+msgid "CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery."
+msgstr ""
+
+msgid "CICD|Auto DevOps"
+msgstr ""
+
+msgid "CICD|Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration."
+msgstr ""
+
+msgid "CICD|Automatic deployment to staging, manual deployment to production"
+msgstr ""
+
+msgid "CICD|Continuous deployment to production"
+msgstr ""
+
+msgid "CICD|Deployment strategy"
+msgstr ""
+
+msgid "CICD|Deployment strategy needs a domain name to work correctly."
+msgstr ""
+
+msgid "CICD|Disable Auto DevOps"
+msgstr ""
+
+msgid "CICD|Enable Auto DevOps"
+msgstr ""
+
+msgid "CICD|Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific %{ci_file}."
+msgstr ""
+
+msgid "CICD|Instance default (%{state})"
msgstr ""
msgid "CICD|Jobs"
msgstr "Jobs"
-msgid "Cancel"
-msgstr "Cancella"
+msgid "CICD|Learn more about Auto DevOps"
+msgstr ""
-msgid "Cannot be merged automatically"
+msgid "CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project."
msgstr ""
-msgid "Cannot modify managed Kubernetes cluster"
+msgid "CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages."
+msgstr ""
+
+msgid "Can't find HEAD commit for this branch"
msgstr ""
-msgid "Certificate fingerprint"
+msgid "Cancel"
+msgstr "Cancella"
+
+msgid "Cancel this job"
+msgstr ""
+
+msgid "Cannot be merged automatically"
msgstr ""
-msgid "Change Weight"
+msgid "Cannot modify managed Kubernetes cluster"
msgstr ""
msgid "Change this value to influence how frequently the GitLab UI polls for updates."
@@ -790,21 +954,18 @@ msgstr ""
msgid "Choose a branch/tag (e.g. %{master}) or enter a commit (e.g. %{sha}) to see what's changed or to create a merge request."
msgstr ""
-msgid "Choose file..."
+msgid "Choose any color."
msgstr ""
-msgid "Choose which groups you wish to synchronize to this secondary node."
+msgid "Choose between <code>clone</code> or <code>fetch</code> to get the recent application code"
msgstr ""
-msgid "Choose which repositories you want to connect and run CI/CD pipelines."
+msgid "Choose file..."
msgstr ""
msgid "Choose which repositories you want to import."
msgstr ""
-msgid "Choose which shards you wish to synchronize to this secondary node."
-msgstr ""
-
msgid "CiStatusLabel|canceled"
msgstr "cancellato"
@@ -874,21 +1035,12 @@ msgstr ""
msgid "CiVariable|All environments"
msgstr ""
-msgid "CiVariable|Create wildcard"
-msgstr ""
-
msgid "CiVariable|Error occured while saving variables"
msgstr ""
-msgid "CiVariable|New environment"
-msgstr ""
-
msgid "CiVariable|Protected"
msgstr ""
-msgid "CiVariable|Search environments"
-msgstr ""
-
msgid "CiVariable|Toggle protected"
msgstr ""
@@ -898,19 +1050,22 @@ msgstr ""
msgid "CircuitBreakerApiLink|circuitbreaker api"
msgstr "api circuitbreaker"
-msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
+msgid "Clear search input"
msgstr ""
-msgid "Click to expand text"
+msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Client authentication certificate"
+msgid "Click the <strong>Promote</strong> button in the top right corner to promote it to a group milestone."
msgstr ""
-msgid "Client authentication key"
+msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
msgstr ""
-msgid "Client authentication key password"
+msgid "Click to expand it."
+msgstr ""
+
+msgid "Click to expand text"
msgstr ""
msgid "Clone repository"
@@ -919,9 +1074,6 @@ msgstr "Clona repository"
msgid "Close"
msgstr ""
-msgid "Closed"
-msgstr ""
-
msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster"
msgstr ""
@@ -937,6 +1089,12 @@ msgstr ""
msgid "ClusterIntegration|Advanced options on this Kubernetes cluster's integration"
msgstr ""
+msgid "ClusterIntegration|An error occured while trying to fetch project zones: %{error}"
+msgstr ""
+
+msgid "ClusterIntegration|An error occured while trying to fetch your projects: %{error}"
+msgstr ""
+
msgid "ClusterIntegration|Applications"
msgstr "Applicazioni"
@@ -967,6 +1125,9 @@ msgstr "Copia Certificato CA"
msgid "ClusterIntegration|Copy Ingress IP Address to clipboard"
msgstr ""
+msgid "ClusterIntegration|Copy Jupyter Hostname to clipboard"
+msgstr ""
+
msgid "ClusterIntegration|Copy Kubernetes cluster name"
msgstr ""
@@ -982,8 +1143,8 @@ msgstr ""
msgid "ClusterIntegration|Create a new Kubernetes cluster on Google Kubernetes Engine right from GitLab"
msgstr ""
-msgid "ClusterIntegration|Create on GKE"
-msgstr "Crea su GKE"
+msgid "ClusterIntegration|Create on Google Kubernetes Engine"
+msgstr ""
msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
msgstr "Inserisci i dettagli per un cluster Kubernetes esistente"
@@ -994,14 +1155,26 @@ msgstr ""
msgid "ClusterIntegration|Environment scope"
msgstr ""
+msgid "ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab's Google Kubernetes Engine Integration."
+msgstr ""
+
+msgid "ClusterIntegration|Fetching machine types"
+msgstr ""
+
+msgid "ClusterIntegration|Fetching projects"
+msgstr ""
+
+msgid "ClusterIntegration|Fetching zones"
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
msgid "ClusterIntegration|GitLab Runner"
msgstr "Gitlab Runner"
-msgid "ClusterIntegration|Google Cloud Platform project ID"
-msgstr "ID Progetto di Google Cloud Platform"
+msgid "ClusterIntegration|Google Cloud Platform project"
+msgstr ""
msgid "ClusterIntegration|Google Kubernetes Engine"
msgstr ""
@@ -1012,9 +1185,6 @@ msgstr ""
msgid "ClusterIntegration|Helm Tiller"
msgstr "Helm Tiller"
-msgid "ClusterIntegration|In order to show the health of the cluster, we'll need to provision your cluster with Prometheus to collect the required data."
-msgstr ""
-
msgid "ClusterIntegration|Ingress"
msgstr "Ingresso"
@@ -1024,9 +1194,6 @@ msgstr ""
msgid "ClusterIntegration|Install"
msgstr "Installa"
-msgid "ClusterIntegration|Install Prometheus"
-msgstr ""
-
msgid "ClusterIntegration|Installed"
msgstr "Installato"
@@ -1039,13 +1206,16 @@ msgstr ""
msgid "ClusterIntegration|Integration status"
msgstr ""
-msgid "ClusterIntegration|Kubernetes cluster"
+msgid "ClusterIntegration|Jupyter Hostname"
msgstr ""
-msgid "ClusterIntegration|Kubernetes cluster details"
+msgid "ClusterIntegration|JupyterHub"
msgstr ""
-msgid "ClusterIntegration|Kubernetes cluster health"
+msgid "ClusterIntegration|Kubernetes cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Kubernetes cluster details"
msgstr ""
msgid "ClusterIntegration|Kubernetes cluster integration"
@@ -1075,7 +1245,13 @@ msgstr ""
msgid "ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project"
msgstr ""
-msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
+msgid "ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}."
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{help_link_start}Kubernetes%{help_link_end}."
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}."
msgstr ""
msgid "ClusterIntegration|Learn more about environments"
@@ -1099,7 +1275,16 @@ msgstr ""
msgid "ClusterIntegration|More information"
msgstr ""
-msgid "ClusterIntegration|Multiple Kubernetes clusters are available in GitLab Enterprise Edition Premium and Ultimate"
+msgid "ClusterIntegration|No machine types matched your search"
+msgstr ""
+
+msgid "ClusterIntegration|No projects found"
+msgstr ""
+
+msgid "ClusterIntegration|No projects matched your search"
+msgstr ""
+
+msgid "ClusterIntegration|No zones matched your search"
msgstr ""
msgid "ClusterIntegration|Note:"
@@ -1114,9 +1299,6 @@ msgstr ""
msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
msgstr ""
-msgid "ClusterIntegration|Project ID"
-msgstr ""
-
msgid "ClusterIntegration|Project namespace"
msgstr ""
@@ -1129,6 +1311,9 @@ msgstr ""
msgid "ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration."
msgstr ""
+msgid "ClusterIntegration|Redeem up to $500 in free credit for Google Cloud Platform"
+msgstr ""
+
msgid "ClusterIntegration|Remove Kubernetes cluster integration"
msgstr ""
@@ -1144,19 +1329,37 @@ msgstr ""
msgid "ClusterIntegration|Save changes"
msgstr ""
+msgid "ClusterIntegration|Search machine types"
+msgstr ""
+
+msgid "ClusterIntegration|Search projects"
+msgstr ""
+
+msgid "ClusterIntegration|Search zones"
+msgstr ""
+
msgid "ClusterIntegration|Security"
msgstr ""
msgid "ClusterIntegration|See and edit the details for your Kubernetes cluster"
msgstr ""
-msgid "ClusterIntegration|See machine types"
+msgid "ClusterIntegration|Select machine type"
msgstr ""
-msgid "ClusterIntegration|See your projects"
+msgid "ClusterIntegration|Select project"
msgstr ""
-msgid "ClusterIntegration|See zones"
+msgid "ClusterIntegration|Select project and zone to choose machine type"
+msgstr ""
+
+msgid "ClusterIntegration|Select project to choose zone"
+msgstr ""
+
+msgid "ClusterIntegration|Select zone"
+msgstr ""
+
+msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
msgid "ClusterIntegration|Service token"
@@ -1189,6 +1392,9 @@ msgstr ""
msgid "ClusterIntegration|Token"
msgstr ""
+msgid "ClusterIntegration|Validating project billing status"
+msgstr ""
+
msgid "ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
@@ -1219,13 +1425,19 @@ msgstr ""
msgid "ClusterIntegration|properly configured"
msgstr ""
+msgid "ClusterIntegration|sign up"
+msgstr ""
+
msgid "Collapse"
msgstr ""
-msgid "Comment and resolve discussion"
+msgid "Collapse sidebar"
+msgstr ""
+
+msgid "Comment & resolve discussion"
msgstr ""
-msgid "Comment and unresolve discussion"
+msgid "Comment & unresolve discussion"
msgstr ""
msgid "Comments"
@@ -1292,6 +1504,9 @@ msgstr ""
msgid "Committed by"
msgstr "Committato da "
+msgid "Commit…"
+msgstr ""
+
msgid "Compare"
msgstr "Confronta"
@@ -1340,6 +1555,9 @@ msgstr ""
msgid "Configure limits for web and API requests."
msgstr ""
+msgid "Configure push mirrors."
+msgstr ""
+
msgid "Configure storage path and circuit breaker settings."
msgstr ""
@@ -1349,18 +1567,9 @@ msgstr ""
msgid "Connect"
msgstr ""
-msgid "Connect all repositories"
-msgstr ""
-
msgid "Connect repositories from GitHub"
msgstr ""
-msgid "Connect your external repositories, and CI/CD pipelines will run for new commits. A GitLab project will be created with only CI/CD features enabled."
-msgstr ""
-
-msgid "Connecting..."
-msgstr ""
-
msgid "Container Registry"
msgstr ""
@@ -1406,9 +1615,18 @@ msgstr "Utilizza nomi d'immagine differenti"
msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images."
msgstr "Con il Docker Container Registry integrato in Gitlab, ogni progetto può avere il suo spazio d'archiviazione sulle immagini Docker."
+msgid "ContainerRegistry|You can also use a %{deploy_token} for read-only access to the registry images."
+msgstr ""
+
+msgid "Continue"
+msgstr ""
+
msgid "Continuous Integration and Deployment"
msgstr ""
+msgid "Contribute to GitLab"
+msgstr ""
+
msgid "Contribution"
msgstr ""
@@ -1430,15 +1648,6 @@ msgstr "Esegui i commit su %{branch_name}, escludendo i commit di merge. Limitat
msgid "ContributorsPage|Please wait a moment, this page will automatically refresh when ready."
msgstr "Attendere prego, questa pagina si ricaricherà automaticamente appena pronta."
-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 ""
-
msgid "Copy URL to clipboard"
msgstr "Copia URL negli appunti"
@@ -1451,9 +1660,18 @@ msgstr ""
msgid "Copy commit SHA to clipboard"
msgstr "Copia l'SHA del commit negli appunti"
+msgid "Copy file name to clipboard"
+msgstr ""
+
+msgid "Copy file path to clipboard"
+msgstr ""
+
msgid "Copy reference to clipboard"
msgstr ""
+msgid "Copy to clipboard"
+msgstr ""
+
msgid "Create"
msgstr ""
@@ -1472,15 +1690,15 @@ msgstr "Creare un token di accesso sul tuo account per eseguire pull o push tram
msgid "Create branch"
msgstr ""
+msgid "Create commit"
+msgstr ""
+
msgid "Create directory"
msgstr "Crea cartella"
msgid "Create empty repository"
msgstr ""
-msgid "Create epic"
-msgstr ""
-
msgid "Create file"
msgstr "Crea file"
@@ -1523,13 +1741,10 @@ msgstr "Tag"
msgid "CreateTokenToCloneLink|create a personal access token"
msgstr "Crea token d'accesso personale"
-msgid "Creates a new branch from %{branchName}"
-msgstr ""
-
-msgid "Creates a new branch from %{branchName} and re-directs to create a new merge request"
+msgid "Created"
msgstr ""
-msgid "Creating epic"
+msgid "Created by me"
msgstr ""
msgid "Cron Timezone"
@@ -1538,7 +1753,13 @@ msgstr "Timezone del Cron"
msgid "Cron syntax"
msgstr "Sintassi Cron"
-msgid "Current node"
+msgid "CurrentUser|Profile"
+msgstr ""
+
+msgid "CurrentUser|Settings"
+msgstr ""
+
+msgid "Custom CI config path"
msgstr ""
msgid "Custom notification events"
@@ -1547,9 +1768,6 @@ msgstr "Eventi-Notifica personalizzati"
msgid "Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notification_link}."
msgstr "I livelli di notifica personalizzati sono uguali a quelli di partecipazione. Con i livelli di notifica personalizzati riceverai anche notifiche per gli eventi da te scelti %{notification_link}."
-msgid "Customize colors"
-msgstr ""
-
msgid "Cycle Analytics"
msgstr "Statistiche Cicliche"
@@ -1586,7 +1804,7 @@ msgstr "Dic"
msgid "December"
msgstr "Dicembre"
-msgid "Default classification label"
+msgid "Decline and sign out"
msgstr ""
msgid "Define a custom pattern with cron syntax"
@@ -1595,6 +1813,9 @@ msgstr "Definisci un patter personalizzato mediante la sintassi cron"
msgid "Delete"
msgstr "Elimina"
+msgid "Delete list"
+msgstr ""
+
msgid "Deploy"
msgid_plural "Deploys"
msgstr[0] "Rilascio"
@@ -1603,37 +1824,163 @@ msgstr[1] "Rilasci"
msgid "Deploy Keys"
msgstr "Chiavi di Deploy (rilascio)"
-msgid "Description"
-msgstr "Descrizione"
+msgid "DeployKeys|+%{count} others"
+msgstr ""
+
+msgid "DeployKeys|Current project"
+msgstr ""
+
+msgid "DeployKeys|Deploy key"
+msgstr ""
+
+msgid "DeployKeys|Enabled deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Error enabling deploy key"
+msgstr ""
+
+msgid "DeployKeys|Error getting deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Error removing deploy key"
+msgstr ""
+
+msgid "DeployKeys|Expand %{count} other projects"
+msgstr ""
+
+msgid "DeployKeys|Loading deploy keys"
+msgstr ""
+
+msgid "DeployKeys|No deploy keys found. Create one with the form above."
+msgstr ""
+
+msgid "DeployKeys|Privately accessible deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Project usage"
+msgstr ""
+
+msgid "DeployKeys|Publicly accessible deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Read access only"
+msgstr ""
+
+msgid "DeployKeys|Write access allowed"
+msgstr ""
+
+msgid "DeployKeys|You are going to remove this deploy key. Are you sure?"
+msgstr ""
+
+msgid "DeployTokens|Active Deploy Tokens (%{active_tokens})"
+msgstr ""
+
+msgid "DeployTokens|Add a deploy token"
+msgstr ""
+
+msgid "DeployTokens|Allows read-only access to the registry images"
+msgstr ""
+
+msgid "DeployTokens|Allows read-only access to the repository"
+msgstr ""
+
+msgid "DeployTokens|Copy deploy token to clipboard"
+msgstr ""
+
+msgid "DeployTokens|Copy username to clipboard"
+msgstr ""
+
+msgid "DeployTokens|Create deploy token"
+msgstr ""
+
+msgid "DeployTokens|Created"
+msgstr ""
+
+msgid "DeployTokens|Deploy Tokens"
+msgstr ""
+
+msgid "DeployTokens|Deploy tokens allow read-only access to your repository and registry images."
+msgstr ""
+
+msgid "DeployTokens|Expires"
+msgstr ""
+
+msgid "DeployTokens|Name"
+msgstr ""
+
+msgid "DeployTokens|Pick a name for the application, and we'll give you a unique deploy token."
+msgstr ""
+
+msgid "DeployTokens|Revoke"
+msgstr ""
+
+msgid "DeployTokens|Revoke %{name}"
+msgstr ""
+
+msgid "DeployTokens|Scopes"
+msgstr ""
+
+msgid "DeployTokens|This action cannot be undone."
+msgstr ""
+
+msgid "DeployTokens|This project has no active Deploy Tokens."
+msgstr ""
+
+msgid "DeployTokens|Use this token as a password. Make sure you save it - you won't be able to access it again."
+msgstr ""
+
+msgid "DeployTokens|Use this username as a login."
+msgstr ""
+
+msgid "DeployTokens|Username"
+msgstr ""
+
+msgid "DeployTokens|You are about to revoke"
+msgstr ""
+
+msgid "DeployTokens|Your New Deploy Token"
+msgstr ""
-msgid "Description templates allow you to define context-specific templates for issue and merge request description fields for your project."
+msgid "DeployTokens|Your new project deploy token has been created."
msgstr ""
+msgid "Deprioritize label"
+msgstr ""
+
+msgid "Description"
+msgstr "Descrizione"
+
msgid "Details"
msgstr "Dettagli"
msgid "Diffs|No file name available"
msgstr ""
+msgid "Diffs|Something went wrong while fetching diff lines."
+msgstr ""
+
msgid "Directory name"
msgstr "Nome cartella"
msgid "Disable"
msgstr ""
-msgid "Discard draft"
+msgid "Disable for this project"
msgstr ""
-msgid "Discover GitLab Geo."
+msgid "Disable group Runners"
msgstr ""
-msgid "Dismiss Cycle Analytics introduction box"
-msgstr "Chiudi l'introduzione alle Analisi Cicliche"
+msgid "Discard changes"
+msgstr ""
-msgid "Dismiss Merge Request promotion"
+msgid "Discard draft"
msgstr ""
-msgid "Documentation for popular identity providers"
+msgid "Dismiss Cycle Analytics introduction box"
+msgstr "Chiudi l'introduzione alle Analisi Cicliche"
+
+msgid "Domain"
msgstr ""
msgid "Don't show again"
@@ -1675,40 +2022,40 @@ msgstr ""
msgid "Due date"
msgstr ""
-msgid "During this process, you’ll be asked for URLs from GitLab’s side. Use the URLs shown below."
+msgid "Each Runner can be in one of the following states:"
msgstr ""
msgid "Edit"
msgstr "Modifica"
+msgid "Edit Label"
+msgstr ""
+
msgid "Edit Pipeline Schedule %{id}"
msgstr "Cambia programmazione della pipeline %{id}"
msgid "Edit files in the editor and commit changes here"
msgstr ""
-msgid "Editing"
-msgstr ""
-
-msgid "Elasticsearch"
+msgid "Edit identity for %{user_name}"
msgstr ""
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "Email"
msgstr ""
-msgid "Email"
+msgid "Email patch"
msgstr ""
msgid "Emails"
msgstr "E-mail"
-msgid "Enable"
+msgid "Embed"
msgstr ""
-msgid "Enable Auto DevOps"
+msgid "Enable"
msgstr ""
-msgid "Enable SAML authentication for this group"
+msgid "Enable Auto DevOps"
msgstr ""
msgid "Enable Sentry for error reporting and logging."
@@ -1720,7 +2067,13 @@ msgstr ""
msgid "Enable and configure Prometheus metrics."
msgstr ""
-msgid "Enable classification control using an external service"
+msgid "Enable for this project"
+msgstr ""
+
+msgid "Enable group Runners"
+msgstr ""
+
+msgid "Enable or disable certain group features and choose access levels."
msgstr ""
msgid "Enable or disable version check and usage ping."
@@ -1732,7 +2085,10 @@ msgstr ""
msgid "Enable the Performance Bar for a given group."
msgstr ""
-msgid "Enabled"
+msgid "Ends at (UTC)"
+msgstr ""
+
+msgid "Environments"
msgstr ""
msgid "Environments|An error occurred while fetching the environments."
@@ -1783,43 +2139,40 @@ msgstr "Aggiornato"
msgid "Environments|You don't have any environments right now."
msgstr "Attualmente non hai alcun ambiente."
-msgid "Epic will be removed! Are you sure?"
-msgstr ""
-
-msgid "Epics"
+msgid "Error Reporting and Logging"
msgstr ""
-msgid "Epics Roadmap"
+msgid "Error committing changes. Please try again."
msgstr ""
-msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
+msgid "Error fetching contributors data."
msgstr ""
-msgid "Error Reporting and Logging"
+msgid "Error fetching job trace"
msgstr ""
-msgid "Error checking branch data. Please try again."
+msgid "Error fetching labels."
msgstr ""
-msgid "Error committing changes. Please try again."
+msgid "Error fetching network graph."
msgstr ""
-msgid "Error creating epic"
+msgid "Error fetching refs"
msgstr ""
-msgid "Error fetching contributors data."
+msgid "Error fetching usage ping data."
msgstr ""
-msgid "Error fetching labels."
+msgid "Error loading branch data. Please try again."
msgstr ""
-msgid "Error fetching network graph."
+msgid "Error loading last commit."
msgstr ""
-msgid "Error fetching refs"
+msgid "Error loading merge requests."
msgstr ""
-msgid "Error fetching usage ping data."
+msgid "Error loading project data. Please try again."
msgstr ""
msgid "Error occurred when toggling the notification subscription"
@@ -1834,6 +2187,9 @@ msgstr ""
msgid "Error updating todo status."
msgstr ""
+msgid "Estimated"
+msgstr ""
+
msgid "EventFilterBy|Filter by all"
msgstr "Filtra per tutti"
@@ -1864,32 +2220,17 @@ msgstr "Ogni settimana (Di domenica alle 4 del mattino)"
msgid "Expand"
msgstr ""
-msgid "Explore projects"
-msgstr "Esplora progetti"
-
-msgid "Explore public groups"
-msgstr "Esplora gruppi pubblici"
-
-msgid "External Classification Policy Authorization"
-msgstr ""
-
-msgid "External authentication"
-msgstr ""
-
-msgid "External authorization denied access to this project"
+msgid "Expand all"
msgstr ""
-msgid "External authorization request timeout"
+msgid "Expand sidebar"
msgstr ""
-msgid "ExternalAuthorizationService|Classification Label"
-msgstr ""
-
-msgid "ExternalAuthorizationService|Classification label"
-msgstr ""
+msgid "Explore projects"
+msgstr "Esplora progetti"
-msgid "ExternalAuthorizationService|When no classification label is set the default label `%{default_label}` will be used."
-msgstr ""
+msgid "Explore public groups"
+msgstr "Esplora gruppi pubblici"
msgid "Failed"
msgstr ""
@@ -1900,6 +2241,9 @@ msgstr ""
msgid "Failed to change the owner"
msgstr "Impossibile cambiare owner"
+msgid "Failed to check related branches."
+msgstr ""
+
msgid "Failed to remove issue from board, please try again."
msgstr ""
@@ -1909,6 +2253,12 @@ msgstr "Impossibile rimuovere la pipeline pianificata"
msgid "Failed to update issues, please try again."
msgstr ""
+msgid "Failure"
+msgstr ""
+
+msgid "Faster as it re-uses the project workspace (falling back to clone if it doesn't exist)"
+msgstr ""
+
msgid "Feb"
msgstr "Feb"
@@ -1918,18 +2268,12 @@ msgstr "Febbraio"
msgid "Fields on this page are now uneditable, you can configure"
msgstr ""
-msgid "File name"
-msgstr "Nome file"
-
msgid "Files"
msgstr "Files"
msgid "Files (%{human_size})"
msgstr ""
-msgid "Fill in the fields below, turn on <strong>%{enable_label}</strong>, and press <strong>%{save_changes}</strong>"
-msgstr ""
-
msgid "Filter by commit message"
msgstr "Filtra per messaggio di commit"
@@ -1948,10 +2292,13 @@ msgstr "Primo"
msgid "FirstPushedBy|pushed by"
msgstr "Push di"
-msgid "Font Color"
+msgid "For internal projects, any logged in user can view pipelines and access job details (output logs and artifacts)"
msgstr ""
-msgid "Footer message"
+msgid "For private projects, any member (guest or higher) can view pipelines and access job details (output logs and artifacts)"
+msgstr ""
+
+msgid "For public projects, anyone can view pipelines and access job details (output logs and artifacts)"
msgstr ""
msgid "Fork"
@@ -1971,6 +2318,9 @@ msgstr ""
msgid "Format"
msgstr "Formato"
+msgid "Found errors in your .gitlab-ci.yml:"
+msgstr ""
+
msgid "From %{provider_title}"
msgstr ""
@@ -1986,166 +2336,13 @@ msgstr ""
msgid "GPG Keys"
msgstr "Chiavi GPG"
-msgid "Generate a default set of labels"
-msgstr ""
-
-msgid "Geo Nodes"
-msgstr ""
-
-msgid "Geo allows you to replicate your GitLab instance to other geographical locations."
-msgstr ""
-
-msgid "GeoNodeSyncStatus|Node is failing or broken."
-msgstr ""
-
-msgid "GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage."
-msgstr ""
-
-msgid "GeoNodes|Checksummed"
-msgstr ""
-
-msgid "GeoNodes|Database replication lag:"
-msgstr ""
-
-msgid "GeoNodes|Disabling a node stops the sync process. Are you sure?"
-msgstr ""
-
-msgid "GeoNodes|Does not match the primary storage configuration"
-msgstr ""
-
-msgid "GeoNodes|Failed"
-msgstr ""
-
-msgid "GeoNodes|Full"
-msgstr ""
-
-msgid "GeoNodes|GitLab version does not match the primary node version"
-msgstr ""
-
-msgid "GeoNodes|GitLab version:"
-msgstr ""
-
-msgid "GeoNodes|Health status:"
-msgstr ""
-
-msgid "GeoNodes|Last event ID processed by cursor:"
-msgstr ""
-
-msgid "GeoNodes|Last event ID seen from primary:"
-msgstr ""
-
-msgid "GeoNodes|Loading nodes"
-msgstr ""
-
-msgid "GeoNodes|Local Attachments:"
-msgstr ""
-
-msgid "GeoNodes|Local LFS objects:"
-msgstr ""
-
-msgid "GeoNodes|Local job artifacts:"
-msgstr ""
-
-msgid "GeoNodes|New node"
-msgstr ""
-
-msgid "GeoNodes|Node Authentication was successfully repaired."
-msgstr ""
-
-msgid "GeoNodes|Node was successfully removed."
-msgstr ""
-
-msgid "GeoNodes|Not checksummed"
-msgstr ""
-
-msgid "GeoNodes|Out of sync"
-msgstr ""
-
-msgid "GeoNodes|Removing a node stops the sync process. Are you sure?"
-msgstr ""
-
-msgid "GeoNodes|Replication slot WAL:"
-msgstr ""
-
-msgid "GeoNodes|Replication slots:"
+msgid "General"
msgstr ""
-msgid "GeoNodes|Repositories checksummed:"
+msgid "General pipelines"
msgstr ""
-msgid "GeoNodes|Repositories:"
-msgstr ""
-
-msgid "GeoNodes|Repository checksums verified:"
-msgstr ""
-
-msgid "GeoNodes|Selective"
-msgstr ""
-
-msgid "GeoNodes|Something went wrong while changing node status"
-msgstr ""
-
-msgid "GeoNodes|Something went wrong while removing node"
-msgstr ""
-
-msgid "GeoNodes|Something went wrong while repairing node"
-msgstr ""
-
-msgid "GeoNodes|Storage config:"
-msgstr ""
-
-msgid "GeoNodes|Sync settings:"
-msgstr ""
-
-msgid "GeoNodes|Synced"
-msgstr ""
-
-msgid "GeoNodes|Unused slots"
-msgstr ""
-
-msgid "GeoNodes|Unverified"
-msgstr ""
-
-msgid "GeoNodes|Used slots"
-msgstr ""
-
-msgid "GeoNodes|Verified"
-msgstr ""
-
-msgid "GeoNodes|Wiki checksums verified:"
-msgstr ""
-
-msgid "GeoNodes|Wikis checksummed:"
-msgstr ""
-
-msgid "GeoNodes|Wikis:"
-msgstr ""
-
-msgid "GeoNodes|You have configured Geo nodes using an insecure HTTP connection. We recommend the use of HTTPS."
-msgstr ""
-
-msgid "Geo|All projects"
-msgstr ""
-
-msgid "Geo|File sync capacity"
-msgstr ""
-
-msgid "Geo|Groups to synchronize"
-msgstr ""
-
-msgid "Geo|Projects in certain groups"
-msgstr ""
-
-msgid "Geo|Projects in certain storage shards"
-msgstr ""
-
-msgid "Geo|Repository sync capacity"
-msgstr ""
-
-msgid "Geo|Select groups to replicate."
-msgstr ""
-
-msgid "Geo|Shards to synchronize"
+msgid "Generate a default set of labels"
msgstr ""
msgid "Git repository URL"
@@ -2157,6 +2354,9 @@ msgstr ""
msgid "Git storage health information has been reset"
msgstr "Le informazioni sullo stato dell'archiviazione Git è stata ripristinata"
+msgid "Git strategy for pipelines"
+msgstr ""
+
msgid "Git version"
msgstr ""
@@ -2166,21 +2366,24 @@ msgstr ""
msgid "GitLab CI Linter has been moved"
msgstr ""
-msgid "GitLab Geo"
+msgid "GitLab Group Runners can execute code for all the projects in this group."
msgstr ""
msgid "GitLab Runner section"
msgstr "Sezione Gitlab Runner"
-msgid "GitLab single sign on URL"
-msgstr ""
-
msgid "Gitaly"
msgstr ""
msgid "Gitaly Servers"
msgstr ""
+msgid "Gitaly|Address"
+msgstr ""
+
+msgid "Go Back"
+msgstr ""
+
msgid "Go back"
msgstr ""
@@ -2196,22 +2399,19 @@ msgstr "L'autenticazione Google non è %{link_to_documentation}. Richiedi al tuo
msgid "Got it!"
msgstr ""
-msgid "GroupRoadmap|Epics let you manage your portfolio of projects more efficiently and with less effort"
-msgstr ""
-
-msgid "GroupRoadmap|From %{dateWord}"
+msgid "Graph"
msgstr ""
-msgid "GroupRoadmap|Loading roadmap"
+msgid "Group CI/CD settings"
msgstr ""
-msgid "GroupRoadmap|Something went wrong while fetching epics"
+msgid "Group ID"
msgstr ""
-msgid "GroupRoadmap|To view the roadmap, add a planned start or finish date to one of your epics in this group or its subgroups. Only epics in the past 3 months and the next 3 months are shown &ndash; from %{startDate} to %{endDate}."
+msgid "Group Runners"
msgstr ""
-msgid "GroupRoadmap|Until %{dateWord}"
+msgid "Group maintainers can register group runners in the %{link}"
msgstr ""
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
@@ -2238,6 +2438,9 @@ msgstr ""
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
+msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
+msgstr ""
+
msgid "GroupsEmptyState|A group is a collection of several projects."
msgstr ""
@@ -2277,12 +2480,6 @@ msgstr ""
msgid "GroupsTree|Sorry, no groups or projects matched your search"
msgstr ""
-msgid "Have your users email"
-msgstr ""
-
-msgid "Header message"
-msgstr ""
-
msgid "Health Check"
msgstr "Verifica stato"
@@ -2315,19 +2512,49 @@ msgid_plural "Hide values"
msgstr[0] ""
msgstr[1] ""
+msgid "Hide whitespace changes"
+msgstr ""
+
msgid "History"
msgstr "Cronologia"
msgid "Housekeeping successfully started"
msgstr "Housekeeping iniziato con successo"
-msgid "Identity provider single sign on URL"
+msgid "I accept the %{terms_link}"
+msgstr ""
+
+msgid "I accept the|Terms of Service and Privacy Policy"
+msgstr ""
+
+msgid "ID"
+msgstr ""
+
+msgid "IDE|Commit"
+msgstr ""
+
+msgid "IDE|Edit"
+msgstr ""
+
+msgid "IDE|Go back"
+msgstr ""
+
+msgid "IDE|Open in file view"
+msgstr ""
+
+msgid "IDE|Review"
msgstr ""
-msgid "If enabled, access to projects will be validated on an external service using their classification label."
+msgid "Identifier"
msgstr ""
-msgid "If using GitHub, you’ll see pipeline statuses on GitHub for your commits and pull requests. %{more_info_link}"
+msgid "Identities"
+msgstr ""
+
+msgid "If disabled, the access level will depend on the user's permissions in the project."
+msgstr ""
+
+msgid "If enabled"
msgstr ""
msgid "If you already have files you can push them using the %{link_to_cli} below."
@@ -2336,6 +2563,15 @@ msgstr ""
msgid "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>."
msgstr ""
+msgid "ImageDiffViewer|2-up"
+msgstr ""
+
+msgid "ImageDiffViewer|Onion skin"
+msgstr ""
+
+msgid "ImageDiffViewer|Swipe"
+msgstr ""
+
msgid "Import"
msgstr ""
@@ -2351,16 +2587,10 @@ msgstr ""
msgid "Import repository"
msgstr "Importa repository"
-msgid "ImportButtons|Connect repositories from"
-msgstr ""
-
-msgid "Improve Issue boards with GitLab Enterprise Edition."
-msgstr ""
-
-msgid "Improve issues management with Issue weight and GitLab Enterprise Edition."
+msgid "Include a Terms of Service agreement and Privacy Policy that all users must accept."
msgstr ""
-msgid "Improve search with Advanced Global Search and GitLab Enterprise Edition."
+msgid "Inline"
msgstr ""
msgid "Install Runner on Kubernetes"
@@ -2369,17 +2599,15 @@ msgstr ""
msgid "Install a Runner compatible with GitLab CI"
msgstr ""
-msgid "Instance"
-msgid_plural "Instances"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "Instance does not support multiple Kubernetes clusters"
msgstr ""
msgid "Integrations"
msgstr ""
+msgid "Integrations Settings"
+msgstr ""
+
msgid "Interested parties can even contribute by pushing commits if they want to."
msgstr ""
@@ -2395,7 +2623,7 @@ msgstr "Intervallo di Pattern"
msgid "Introducing Cycle Analytics"
msgstr "Introduzione delle Analisi Cicliche"
-msgid "Issue board focus mode"
+msgid "Issue Board"
msgstr ""
msgid "Issue events"
@@ -2404,9 +2632,6 @@ msgstr ""
msgid "IssueBoards|Board"
msgstr ""
-msgid "IssueBoards|Boards"
-msgstr ""
-
msgid "Issues"
msgstr ""
@@ -2419,6 +2644,12 @@ msgstr "Gen"
msgid "January"
msgstr "Gennaio"
+msgid "Job"
+msgstr ""
+
+msgid "Job has been erased"
+msgstr ""
+
msgid "Jobs"
msgstr ""
@@ -2461,6 +2692,9 @@ msgstr ""
msgid "Kubernetes service integration has been deprecated. %{deprecated_message_content} your Kubernetes clusters using the new <a href=\"%{url}\"/>Kubernetes Clusters</a> page"
msgstr ""
+msgid "LFS"
+msgstr ""
+
msgid "LFSStatus|Disabled"
msgstr "Disabilitato"
@@ -2470,6 +2704,9 @@ msgstr "Abilitato"
msgid "Label"
msgstr ""
+msgid "Label actions dropdown"
+msgstr ""
+
msgid "LabelSelect|%{firstLabelName} +%{remainingLabelCount} more"
msgstr ""
@@ -2485,6 +2722,9 @@ msgstr ""
msgid "Labels can be applied to issues and merge requests to categorize them."
msgstr ""
+msgid "Labels can be applied to issues and merge requests."
+msgstr ""
+
msgid "Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>"
msgstr ""
@@ -2520,6 +2760,9 @@ msgstr ""
msgid "LastPushEvent|at"
msgstr ""
+msgid "Latest changes"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -2544,9 +2787,6 @@ msgstr "Abbandona il gruppo"
msgid "Leave project"
msgstr "Abbandona il progetto"
-msgid "License"
-msgstr ""
-
msgid "List"
msgstr ""
@@ -2556,6 +2796,9 @@ msgstr ""
msgid "Loading the GitLab IDE..."
msgstr ""
+msgid "Loading..."
+msgstr ""
+
msgid "Lock"
msgstr ""
@@ -2565,21 +2808,18 @@ msgstr ""
msgid "Lock not found"
msgstr ""
+msgid "Lock to current projects"
+msgstr ""
+
msgid "Locked"
msgstr "Bloccato"
-msgid "Locked Files"
-msgstr ""
-
-msgid "Locks give the ability to lock specific file or folder."
+msgid "Locked to current projects"
msgstr ""
msgid "Login"
msgstr "Login"
-msgid "Make everyone on your team more productive regardless of their location. GitLab Geo creates read-only mirrors of your GitLab instance so you can reduce the time it takes to clone and fetch large repos."
-msgstr ""
-
msgid "Manage all notifications"
msgstr ""
@@ -2592,16 +2832,16 @@ msgstr ""
msgid "Manage project labels"
msgstr ""
-msgid "Manage your group’s membership while adding another level of security with SAML."
-msgstr ""
-
msgid "Mar"
msgstr "Mar"
msgid "March"
msgstr "Marzo"
-msgid "Mark done"
+msgid "Mark todo as done"
+msgstr ""
+
+msgid "Markdown enabled"
msgstr ""
msgid "Maximum git storage failures"
@@ -2616,7 +2856,7 @@ msgstr "Mediano"
msgid "Members"
msgstr "Membri"
-msgid "Members will be forwarded here when signing in to your group. Get this from your identity provider, where it can also be called \"SSO Service Location\", \"SAML Token Issuance Endpoint\", or \"SAML 2.0/W-Federation URL\"."
+msgid "Merge Request:"
msgstr ""
msgid "Merge Requests"
@@ -2628,91 +2868,46 @@ msgstr ""
msgid "Merge request"
msgstr "Richiesta di merge"
-msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
-msgstr ""
-
-msgid "Merged"
-msgstr ""
-
-msgid "Messages"
-msgstr "Messaggi"
-
-msgid "Metrics - Influx"
-msgstr ""
-
-msgid "Metrics - Prometheus"
-msgstr ""
-
-msgid "Metrics|Business"
-msgstr ""
-
-msgid "Metrics|Create metric"
-msgstr ""
-
-msgid "Metrics|Edit metric"
-msgstr ""
-
-msgid "Metrics|For grouping similar metrics"
-msgstr ""
-
-msgid "Metrics|Label of the chart's vertical axis. Usually the type of the unit being charted. The horizontal axis (X-axis) always represents time."
-msgstr ""
-
-msgid "Metrics|Legend label (optional)"
-msgstr ""
-
-msgid "Metrics|Must be a valid PromQL query."
-msgstr ""
-
-msgid "Metrics|Name"
-msgstr ""
-
-msgid "Metrics|New metric"
-msgstr ""
-
-msgid "Metrics|Prometheus Query Documentation"
-msgstr ""
-
-msgid "Metrics|Query"
+msgid "Merge requests"
msgstr ""
-msgid "Metrics|Response"
+msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
msgstr ""
-msgid "Metrics|System"
+msgid "MergeRequests|Resolve this discussion in a new issue"
msgstr ""
-msgid "Metrics|Type"
+msgid "MergeRequests|Saving the comment failed"
msgstr ""
-msgid "Metrics|Unit label"
+msgid "MergeRequests|Toggle comments for this file"
msgstr ""
-msgid "Metrics|Used as a title for the chart"
+msgid "MergeRequests|Updating discussions failed"
msgstr ""
-msgid "Metrics|Used if the query returns a single series. If it returns multiple series, their legend labels will be picked up from the response."
+msgid "MergeRequests|View file @ %{commitId}"
msgstr ""
-msgid "Metrics|Y-axis label"
+msgid "MergeRequests|View replaced file @ %{commitId}"
msgstr ""
-msgid "Metrics|e.g. HTTP requests"
+msgid "Merged"
msgstr ""
-msgid "Metrics|e.g. Requests/second"
-msgstr ""
+msgid "Messages"
+msgstr "Messaggi"
-msgid "Metrics|e.g. Throughput"
+msgid "Metrics - Influx"
msgstr ""
-msgid "Metrics|e.g. rate(http_requests_total[5m])"
+msgid "Metrics - Prometheus"
msgstr ""
-msgid "Metrics|e.g. req/sec"
+msgid "Milestone"
msgstr ""
-msgid "Milestone"
+msgid "Milestones"
msgstr ""
msgid "Milestones|Delete milestone"
@@ -2733,9 +2928,6 @@ msgstr ""
msgid "Milestones|Promote Milestone"
msgstr ""
-msgid "Milestones|This action cannot be reversed."
-msgstr ""
-
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "aggiungi una chiave SSH"
@@ -2748,7 +2940,7 @@ msgstr ""
msgid "Monitoring"
msgstr "Monitoraggio"
-msgid "More info"
+msgid "More actions"
msgstr ""
msgid "More information"
@@ -2763,12 +2955,30 @@ msgstr ""
msgid "Move issue"
msgstr ""
-msgid "Multiple issue boards"
+msgid "Name"
msgstr ""
msgid "Name new label"
msgstr ""
+msgid "Name your individual key via a title"
+msgstr ""
+
+msgid "Nav|Help"
+msgstr ""
+
+msgid "Nav|Home"
+msgstr ""
+
+msgid "Nav|Sign In / Register"
+msgstr ""
+
+msgid "Nav|Sign out and sign in with a different account"
+msgstr ""
+
+msgid "New Identity"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "Nuovo Issue"
@@ -2780,6 +2990,9 @@ msgstr ""
msgid "New Kubernetes cluster"
msgstr ""
+msgid "New Label"
+msgstr ""
+
msgid "New Pipeline Schedule"
msgstr "Nuova pianificazione Pipeline"
@@ -2792,15 +3005,15 @@ msgstr ""
msgid "New directory"
msgstr "Nuova directory"
-msgid "New epic"
-msgstr ""
-
msgid "New file"
msgstr "Nuovo file"
msgid "New group"
msgstr "Nuovo gruppo"
+msgid "New identity"
+msgstr ""
+
msgid "New issue"
msgstr "Nuovo Issue"
@@ -2810,6 +3023,9 @@ msgstr ""
msgid "New merge request"
msgstr "Nuova richiesta di merge"
+msgid "New pipelines will cancel older, pending pipelines on the same branch"
+msgstr ""
+
msgid "New project"
msgstr "Nuovo progetto"
@@ -2825,7 +3041,7 @@ msgstr "Nuovo sottogruppo"
msgid "New tag"
msgstr "Nuovo tag"
-msgid "No Label"
+msgid "No"
msgstr ""
msgid "No assignee"
@@ -2846,7 +3062,16 @@ msgstr ""
msgid "No file chosen"
msgstr ""
-msgid "No labels created yet."
+msgid "No files found"
+msgstr ""
+
+msgid "No files found."
+msgstr ""
+
+msgid "No merge requests found"
+msgstr ""
+
+msgid "No messages were logged"
msgstr ""
msgid "No repository"
@@ -2879,15 +3104,9 @@ msgstr "Dati insufficienti "
msgid "Note that the master branch is automatically protected. %{link_to_protected_branches}"
msgstr ""
-msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow connecting repositories without generating a Personal Access Token."
-msgstr ""
-
msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
msgstr ""
-msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow connecting repositories without generating a Personal Access Token."
-msgstr ""
-
msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
msgstr ""
@@ -2963,9 +3182,6 @@ msgstr "Novembre"
msgid "Number of access attempts"
msgstr "Numero di tentativi di accesso raggiunto"
-msgid "OK"
-msgstr ""
-
msgid "Oct"
msgstr "Ott"
@@ -2975,19 +3191,16 @@ msgstr "Ottobre"
msgid "OfSearchInADropdown|Filter"
msgstr "Filtra"
-msgid "Once imported, repositories can be mirrored over SSH. Read more %{ssh_link}"
+msgid "Online IDE integration settings."
msgstr ""
-msgid "Online IDE integration settings."
+msgid "Only comments from the following commit are shown below"
msgstr ""
msgid "Only project members can comment."
msgstr "Solo i membri del progetto possono commentare."
-msgid "Open"
-msgstr ""
-
-msgid "Opened"
+msgid "Open in Xcode"
msgstr ""
msgid "OpenedNDaysAgo|Opened"
@@ -2996,9 +3209,18 @@ msgstr "Aperto"
msgid "Opens in a new window"
msgstr "Si apre in una nuova finestra"
+msgid "Operations"
+msgstr ""
+
msgid "Options"
msgstr "Opzioni"
+msgid "Or you can choose one of the suggested colors below"
+msgstr ""
+
+msgid "Other Labels"
+msgstr ""
+
msgid "Otherwise it is recommended you start with one of the options below."
msgstr ""
@@ -3032,12 +3254,27 @@ msgstr ""
msgid "Password"
msgstr "Password"
+msgid "Paste your public SSH key, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'. Don't use your private SSH key."
+msgstr ""
+
+msgid "Pause"
+msgstr ""
+
msgid "Pending"
msgstr ""
+msgid "Per job. If a job passes this threshold, it will be marked as failed"
+msgstr ""
+
+msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgstr ""
+
msgid "Performance optimization"
msgstr ""
+msgid "Permissions"
+msgstr ""
+
msgid "Personal Access Token"
msgstr ""
@@ -3053,7 +3290,7 @@ msgstr "Pianificazione Pipeline"
msgid "Pipeline Schedules"
msgstr "Pianificazione multipla Pipeline"
-msgid "Pipeline quota"
+msgid "Pipeline triggers"
msgstr ""
msgid "PipelineCharts|Failed:"
@@ -3152,10 +3389,22 @@ msgstr ""
msgid "Pipelines|This project is not currently set up to run pipelines."
msgstr ""
-msgid "Pipeline|Retry pipeline"
+msgid "Pipeline|Create for"
+msgstr ""
+
+msgid "Pipeline|Create pipeline"
+msgstr ""
+
+msgid "Pipeline|Existing branch name or tag"
+msgstr ""
+
+msgid "Pipeline|Run Pipeline"
+msgstr ""
+
+msgid "Pipeline|Search branches"
msgstr ""
-msgid "Pipeline|Retry pipeline #%{pipelineId}?"
+msgid "Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default."
msgstr ""
msgid "Pipeline|Stop pipeline"
@@ -3164,7 +3413,7 @@ msgstr ""
msgid "Pipeline|Stop pipeline #%{pipelineId}?"
msgstr ""
-msgid "Pipeline|You’re about to retry pipeline %{pipelineId}."
+msgid "Pipeline|Variables"
msgstr ""
msgid "Pipeline|You’re about to stop pipeline %{pipelineId}."
@@ -3182,19 +3431,25 @@ msgstr "con stadio"
msgid "Pipeline|with stages"
msgstr "con più stadi"
+msgid "Plain diff"
+msgstr ""
+
msgid "PlantUML"
msgstr ""
msgid "Play"
msgstr ""
-msgid "Please <a href=%{link_to_billing} target=\"_blank\" rel=\"noopener noreferrer\">enable billing for one of your projects to be able to create a Kubernetes cluster</a>, then try again."
+msgid "Please accept the Terms of Service before continuing."
+msgstr ""
+
+msgid "Please select at least one filter to see results"
msgstr ""
msgid "Please solve the reCAPTCHA"
msgstr ""
-msgid "Please wait while we connect to your repository. Refresh at will."
+msgid "Please try again"
msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
@@ -3203,7 +3458,19 @@ msgstr ""
msgid "Preferences"
msgstr "Preferenze"
-msgid "Primary"
+msgid "Preferences|Navigation theme"
+msgstr ""
+
+msgid "Prioritize"
+msgstr ""
+
+msgid "Prioritize label"
+msgstr ""
+
+msgid "Prioritized Labels"
+msgstr ""
+
+msgid "Prioritized label"
msgstr ""
msgid "Private - Project access must be granted explicitly to each user."
@@ -3221,6 +3488,12 @@ msgstr "Profilo"
msgid "Profiles|Account scheduled for removal."
msgstr "Account pianificato per la rimozione."
+msgid "Profiles|Change username"
+msgstr ""
+
+msgid "Profiles|Current path: %{path}"
+msgstr ""
+
msgid "Profiles|Delete Account"
msgstr "Elimina account"
@@ -3239,9 +3512,21 @@ msgstr "Password non valida"
msgid "Profiles|Invalid username"
msgstr "Username non valido"
+msgid "Profiles|Path"
+msgstr ""
+
msgid "Profiles|Type your %{confirmationValue} to confirm:"
msgstr "Inserisci il tuo %{confirmationValue} per confermare:"
+msgid "Profiles|Update username"
+msgstr ""
+
+msgid "Profiles|Username change failed - %{message}"
+msgstr ""
+
+msgid "Profiles|Username successfully changed"
+msgstr ""
+
msgid "Profiles|You don't have access to delete this user."
msgstr "Non hai i permessi per eliminare questo utente."
@@ -3260,6 +3545,12 @@ msgstr ""
msgid "Programming languages used in this repository"
msgstr ""
+msgid "Progress"
+msgstr ""
+
+msgid "Project"
+msgstr ""
+
msgid "Project '%{project_name}' is in the process of being deleted."
msgstr "Il progetto '%{project_name}' è in fase di eliminazione."
@@ -3272,6 +3563,9 @@ 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 Badges"
+msgstr ""
+
msgid "Project access must be granted explicitly to each user."
msgstr "L'accesso al progetto dev'esser fornito esplicitamente ad ogni utente"
@@ -3299,30 +3593,6 @@ msgstr "Esportazione del progetto iniziata. Un link di download sarà inviato vi
msgid "ProjectActivityRSS|Subscribe"
msgstr "Iscriviti"
-msgid "ProjectCreationLevel|Allowed to create projects"
-msgstr ""
-
-msgid "ProjectCreationLevel|Default project creation protection"
-msgstr ""
-
-msgid "ProjectCreationLevel|Developers + Masters"
-msgstr ""
-
-msgid "ProjectCreationLevel|Masters"
-msgstr ""
-
-msgid "ProjectCreationLevel|No one"
-msgstr ""
-
-msgid "ProjectFeature|Disabled"
-msgstr "Disabilitato"
-
-msgid "ProjectFeature|Everyone with access"
-msgstr "Chiunque con accesso"
-
-msgid "ProjectFeature|Only team members"
-msgstr "Solo i membri del team"
-
msgid "ProjectFileTree|Name"
msgstr "Nome"
@@ -3332,27 +3602,6 @@ msgstr "Mai"
msgid "ProjectLifecycle|Stage"
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 "Progetti"
@@ -3377,6 +3626,9 @@ msgstr "Siamo spiacenti, non ci sono progetti che corrispondono alla tua ricerca
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr "Questa feature richiede il supporto del localStorage del browser"
+msgid "PrometheusDashboard|Time"
+msgstr ""
+
msgid "PrometheusService|%{exporters} with %{metrics} were found"
msgstr ""
@@ -3398,18 +3650,9 @@ msgstr "Di default, Prometheus è in ascolto su ‘http://localhost:9090‘. Non
msgid "PrometheusService|Common metrics"
msgstr ""
-msgid "PrometheusService|Common metrics are automatically monitored based on a library of metrics from popular exporters."
-msgstr ""
-
-msgid "PrometheusService|Custom metrics"
-msgstr ""
-
msgid "PrometheusService|Finding and configuring metrics..."
msgstr "Ricerco e configuro le metriche..."
-msgid "PrometheusService|Finding custom metrics..."
-msgstr ""
-
msgid "PrometheusService|Install Prometheus on clusters"
msgstr ""
@@ -3422,24 +3665,21 @@ msgstr ""
msgid "PrometheusService|Metrics"
msgstr "Metriche"
+msgid "PrometheusService|Metrics are automatically configured and monitored based on a library of metrics from popular exporters."
+msgstr ""
+
msgid "PrometheusService|Missing environment variable"
msgstr "Variabile d'ambiente mancante"
msgid "PrometheusService|More information"
msgstr "Ulteriori informazioni"
-msgid "PrometheusService|New metric"
-msgstr ""
-
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
-msgstr ""
-
msgid "PrometheusService|Time-series monitoring service"
msgstr ""
@@ -3455,22 +3695,28 @@ msgstr ""
msgid "Promote"
msgstr ""
-msgid "Promote to Group Label"
+msgid "Promote these project milestones into a group milestone."
msgstr ""
msgid "Promote to Group Milestone"
msgstr ""
+msgid "Promote to group label"
+msgstr ""
+
msgid "Protip:"
msgstr ""
+msgid "Provider"
+msgstr ""
+
msgid "Public - The group and any public projects can be viewed without any authentication."
msgstr "Pubblico - il gruppo e tutti i progetti pubblici possono essere visualizzati senza alcuna autenticazione."
msgid "Public - The project can be accessed without any authentication."
msgstr "Public - Chiunque può accedere a questo progetto senza alcuna autenticazione."
-msgid "Push Rules"
+msgid "Public pipelines"
msgstr ""
msgid "Push events"
@@ -3482,10 +3728,10 @@ msgstr ""
msgid "Push to create a project"
msgstr ""
-msgid "PushRule|Committer restriction"
+msgid "Quick actions can be used in the issues description and comment boxes."
msgstr ""
-msgid "Quick actions can be used in the issues description and comment boxes."
+msgid "Re-deploy"
msgstr ""
msgid "Read more"
@@ -3497,18 +3743,18 @@ msgstr "Leggimi"
msgid "Real-time features"
msgstr ""
-msgid "RefSwitcher|Branches"
-msgstr "Branches"
-
-msgid "RefSwitcher|Tags"
-msgstr "Tags"
-
msgid "Reference:"
msgstr ""
msgid "Register / Sign In"
msgstr ""
+msgid "Register and see your runners for this group."
+msgstr ""
+
+msgid "Register and see your runners for this project."
+msgstr ""
+
msgid "Registry"
msgstr ""
@@ -3539,28 +3785,28 @@ msgstr "Ricordamelo più tardi"
msgid "Remove"
msgstr ""
-msgid "Remove avatar"
+msgid "Remove Runner"
msgstr ""
-msgid "Remove project"
-msgstr "Rimuovi progetto"
-
-msgid "Repair authentication"
+msgid "Remove avatar"
msgstr ""
-msgid "Repo by URL"
+msgid "Remove priority"
msgstr ""
+msgid "Remove project"
+msgstr "Rimuovi progetto"
+
msgid "Repository"
msgstr ""
-msgid "Repository has no locks."
+msgid "Repository Settings"
msgstr ""
msgid "Repository maintenance"
msgstr ""
-msgid "Repository mirror settings"
+msgid "Repository mirror"
msgstr ""
msgid "Repository storage"
@@ -3569,6 +3815,9 @@ msgstr ""
msgid "Request Access"
msgstr "Richiedi accesso"
+msgid "Require all users to accept Terms of Service and Privacy Policy when they access GitLab."
+msgstr ""
+
msgid "Reset git storage health information"
msgstr ""
@@ -3578,10 +3827,25 @@ msgstr ""
msgid "Reset runners registration token"
msgstr ""
+msgid "Resolve all discussions in new issue"
+msgstr ""
+
+msgid "Resolve conflicts on source branch"
+msgstr ""
+
msgid "Resolve discussion"
msgstr ""
-msgid "Response"
+msgid "Resume"
+msgstr ""
+
+msgid "Retry"
+msgstr ""
+
+msgid "Retry this job"
+msgstr ""
+
+msgid "Retry verification"
msgstr ""
msgid "Reveal value"
@@ -3595,7 +3859,7 @@ msgstr "Ripristina questo commit"
msgid "Revert this merge request"
msgstr "Ripristina questa richiesta di merge"
-msgid "Review the process for configuring service providers in your identity provider — in this case, GitLab is the \"service provider\" or \"relying party\"."
+msgid "Review"
msgstr ""
msgid "Reviewing"
@@ -3604,30 +3868,33 @@ msgstr ""
msgid "Reviewing (merge request !%{mergeRequestId})"
msgstr ""
-msgid "Roadmap"
+msgid "Rollback"
msgstr ""
-msgid "Run CI/CD pipelines for external repositories"
+msgid "Runner token"
msgstr ""
msgid "Runners"
msgstr ""
-msgid "Running"
+msgid "Runners API"
msgstr ""
-msgid "SAML Single Sign On"
+msgid "Runners can be placed on separate users, servers, and even on your local machine."
msgstr ""
-msgid "SAML Single Sign On Settings"
-msgstr ""
-
-msgid "SHA1 fingerprint of the SAML token signing certificate. Get this from your identity provider, where it can also be called \"Thumbprint\"."
+msgid "Running"
msgstr ""
msgid "SSH Keys"
msgstr "Chiavi SSH"
+msgid "SSL Verification"
+msgstr ""
+
+msgid "Save"
+msgstr ""
+
msgid "Save changes"
msgstr "Salva modifiche"
@@ -3649,15 +3916,30 @@ msgstr ""
msgid "Scheduling Pipelines"
msgstr "Pianificazione pipelines"
-msgid "Scoped issue boards"
+msgid "Scroll to bottom"
+msgstr ""
+
+msgid "Scroll to top"
msgstr ""
msgid "Search"
msgstr ""
+msgid "Search branches"
+msgstr ""
+
msgid "Search branches and tags"
msgstr "Ricerca branches e tags"
+msgid "Search files"
+msgstr ""
+
+msgid "Search for projects, issues, etc."
+msgstr ""
+
+msgid "Search merge requests"
+msgstr ""
+
msgid "Search milestones"
msgstr ""
@@ -3673,15 +3955,15 @@ msgstr ""
msgid "Seconds to wait for a storage access attempt"
msgstr ""
-msgid "Secret variables"
-msgstr ""
-
-msgid "Security report"
+msgid "Select"
msgstr ""
msgid "Select Archive Format"
msgstr "Seleziona formato d'archivio"
+msgid "Select a namespace to fork the project"
+msgstr ""
+
msgid "Select a timezone"
msgstr "Seleziona una timezone"
@@ -3694,12 +3976,21 @@ msgstr ""
msgid "Select branch/tag"
msgstr ""
-msgid "Select target branch"
-msgstr "Seleziona una branch di destinazione"
+msgid "Select project"
+msgstr ""
-msgid "Selective synchronization"
+msgid "Select project and zone to choose machine type"
msgstr ""
+msgid "Select project to choose zone"
+msgstr ""
+
+msgid "Select source branch"
+msgstr ""
+
+msgid "Select target branch"
+msgstr "Seleziona una branch di destinazione"
+
msgid "Send email"
msgstr ""
@@ -3715,9 +4006,6 @@ msgstr ""
msgid "Service Templates"
msgstr ""
-msgid "Service URL"
-msgstr ""
-
msgid "Session expiration, projects limit and attachment size."
msgstr ""
@@ -3742,9 +4030,6 @@ msgstr ""
msgid "Set up Koding"
msgstr "Configura Koding"
-msgid "Set up assertions/attributes/claims (email, first_name, last_name) and NameID according to %{docsLinkStart}the documentation %{icon}%{docsLinkEnd}"
-msgstr ""
-
msgid "SetPasswordToCloneLink|set a password"
msgstr "imposta una password"
@@ -3754,19 +4039,22 @@ msgstr "Impostazioni"
msgid "Setup a specific Runner automatically"
msgstr ""
-msgid "Share the <strong>%{sso_label}</strong> with members so they can sign in to your group through your identity provider"
+msgid "Share"
msgstr ""
-msgid "SharedRunnersMinutesSettings|By resetting the pipeline minutes for this namespace, the currently used minutes will be set to zero."
+msgid "Shared Runners"
msgstr ""
-msgid "SharedRunnersMinutesSettings|Reset pipeline minutes"
+msgid "Show command"
msgstr ""
-msgid "SharedRunnersMinutesSettings|Reset used pipeline minutes"
+msgid "Show complete raw log"
msgstr ""
-msgid "Show command"
+msgid "Show latest version"
+msgstr ""
+
+msgid "Show latest version of the diff"
msgstr ""
msgid "Show parent pages"
@@ -3775,21 +4063,18 @@ msgstr ""
msgid "Show parent subgroups"
msgstr ""
+msgid "Show whitespace changes"
+msgstr ""
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "Visualizza %d evento"
msgstr[1] "Visualizza %d eventi"
-msgid "Sidebar|Change weight"
-msgstr ""
-
-msgid "Sidebar|No"
-msgstr ""
-
-msgid "Sidebar|None"
+msgid "Side-by-side"
msgstr ""
-msgid "Sidebar|Weight"
+msgid "Sign out"
msgstr ""
msgid "Sign-in restrictions"
@@ -3801,7 +4086,7 @@ msgstr ""
msgid "Size and domain settings for static websites"
msgstr ""
-msgid "Slack application"
+msgid "Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job"
msgstr ""
msgid "Snippets"
@@ -3813,13 +4098,13 @@ msgstr ""
msgid "Something went wrong on our end."
msgstr ""
-msgid "Something went wrong when toggling the button"
+msgid "Something went wrong on our end. Please try again!"
msgstr ""
-msgid "Something went wrong while fetching Dependency Scanning."
+msgid "Something went wrong when toggling the button"
msgstr ""
-msgid "Something went wrong while fetching SAST."
+msgid "Something went wrong while closing the %{issuable}. Please try again later"
msgstr ""
msgid "Something went wrong while fetching the projects."
@@ -3828,6 +4113,12 @@ msgstr "Qualcosa è andato storto durante il fetch dei progetti."
msgid "Something went wrong while fetching the registry list."
msgstr "Qualcosa è andato storto durante il recupero dell'elenco dei registri."
+msgid "Something went wrong while reopening the %{issuable}. Please try again later"
+msgstr ""
+
+msgid "Something went wrong while resolving this discussion. Please try again."
+msgstr ""
+
msgid "Something went wrong. Please try again."
msgstr ""
@@ -3873,9 +4164,6 @@ msgstr ""
msgid "SortOptions|Least popular"
msgstr ""
-msgid "SortOptions|Less weight"
-msgstr ""
-
msgid "SortOptions|Milestone"
msgstr ""
@@ -3885,9 +4173,6 @@ msgstr ""
msgid "SortOptions|Milestone due soon"
msgstr ""
-msgid "SortOptions|More weight"
-msgstr ""
-
msgid "SortOptions|Most popular"
msgstr ""
@@ -3927,9 +4212,6 @@ msgstr ""
msgid "SortOptions|Start soon"
msgstr ""
-msgid "SortOptions|Weight"
-msgstr ""
-
msgid "Source"
msgstr ""
@@ -3948,9 +4230,36 @@ msgstr ""
msgid "Spam and Anti-bot Protection"
msgstr ""
+msgid "Specific Runners"
+msgstr ""
+
msgid "Specify the following URL during the Runner setup:"
msgstr ""
+msgid "Squash commits"
+msgstr ""
+
+msgid "Stage"
+msgstr ""
+
+msgid "Stage & Commit"
+msgstr ""
+
+msgid "Stage all changes"
+msgstr ""
+
+msgid "Stage changes"
+msgstr ""
+
+msgid "Staged"
+msgstr ""
+
+msgid "Staged %{type}"
+msgstr ""
+
+msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
+msgstr ""
+
msgid "StarProject|Star"
msgstr "Star"
@@ -3972,12 +4281,15 @@ msgstr ""
msgid "Started"
msgstr ""
-msgid "State your message to activate"
+msgid "Starts at (UTC)"
msgstr ""
msgid "Status"
msgstr ""
+msgid "Stop this environment"
+msgstr ""
+
msgid "Stopped"
msgstr ""
@@ -3987,16 +4299,19 @@ msgstr ""
msgid "Subgroups"
msgstr ""
-msgid "Switch branch/tag"
-msgstr "Cambia branch/tag"
+msgid "Subscribe"
+msgstr ""
-msgid "System"
+msgid "Subscribe at group level"
msgstr ""
-msgid "System Hooks"
+msgid "Subscribe at project level"
msgstr ""
-msgid "System header and footer:"
+msgid "Switch branch/tag"
+msgstr "Cambia branch/tag"
+
+msgid "System Hooks"
msgstr ""
msgid "Tag (%{tag_count})"
@@ -4007,6 +4322,9 @@ msgstr[1] ""
msgid "Tags"
msgstr ""
+msgid "Tags:"
+msgstr ""
+
msgid "TagsPage|Browse commits"
msgstr ""
@@ -4070,7 +4388,7 @@ msgstr ""
msgid "TagsPage|Use git tag command to add a new one:"
msgstr ""
-msgid "TagsPage|Write your release notes or drag files here..."
+msgid "TagsPage|Write your release notes or drag files here…"
msgstr ""
msgid "TagsPage|protected"
@@ -4085,19 +4403,19 @@ msgstr ""
msgid "Team"
msgstr ""
-msgid "Thanks! Don't show me this again"
+msgid "Terms of Service Agreement and Privacy Policy"
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."
+msgid "Terms of Service and Privacy Policy"
msgstr ""
-msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project"
+msgid "Test coverage parsing"
msgstr ""
-msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
+msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project"
msgstr ""
-msgid "The X509 Certificate to use when mutual TLS is required to communicate with the external authorization service. If left blank, the server certificate is still validated when accessing over HTTPS."
+msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
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."
@@ -4106,9 +4424,6 @@ msgstr "Lo stadio di programmazione mostra il tempo trascorso dal primo commit a
msgid "The collection of events added to the data gathered for that stage."
msgstr "L'insieme di eventi aggiunti ai dati raccolti per quello stadio."
-msgid "The connection will time out after %{timeout}. For repositories that take longer, use a clone/push combination."
-msgstr ""
-
msgid "The fork relationship has been removed."
msgstr "La relazione del fork è stata rimossa"
@@ -4127,7 +4442,7 @@ 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 passphrase required to decrypt the private key. This is optional and the value is encrypted at rest."
+msgid "The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>"
msgstr ""
msgid "The phase of the development lifecycle."
@@ -4136,9 +4451,6 @@ msgstr "Il ciclo vitale della fase di sviluppo."
msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit."
msgstr "Lo stadio di pianificazione mostra il tempo trascorso dal primo commit al suo step precedente. Questo periodo sarà disponibile automaticamente nel momento in cui farai il primo commit."
-msgid "The private key to use when a client certificate is provided. This value is encrypted at rest."
-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 "Lo stadio di produzione mostra il tempo totale che trascorre tra la creazione di un issue il suo rilascio (inteso come codice) in produzione. Questo dato sarà disponibile automaticamente nel momento in cui avrai completato l'intero processo ideale del ciclo di produzione"
@@ -4160,7 +4472,7 @@ msgstr ""
msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request."
msgstr "Lo stadio di revisione mostra il tempo tra una richiesta di merge al suo svolgimento effettivo. Questo dato sarà disponibile appena avrai completato una MR (Merger Request)"
-msgid "The roadmap shows the progress of your epics along a timeline"
+msgid "The secure token used by the Runner to checkout the project"
msgstr ""
msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time."
@@ -4187,13 +4499,19 @@ msgstr "Il valore falsato nel mezzo di una serie di dati osservati. ES: tra 3,5,
msgid "There are no issues to show"
msgstr ""
+msgid "There are no labels yet"
+msgstr ""
+
msgid "There are no merge requests to show"
msgstr ""
msgid "There are problems accessing Git storage: "
msgstr ""
-msgid "There was an error loading results"
+msgid "There was an error loading jobs"
+msgstr ""
+
+msgid "There was an error loading latest pipeline"
msgstr ""
msgid "There was an error loading users activity calendar."
@@ -4214,12 +4532,21 @@ msgstr ""
msgid "There was an error when unsubscribing from this label."
msgstr ""
-msgid "This board\\'s scope is reduced"
+msgid "They can be managed using the %{link}."
+msgstr ""
+
+msgid "This GitLab instance does not provide any shared Runners yet. Instance administrators can register shared Runners in the admin area."
+msgstr ""
+
+msgid "This diff is collapsed."
msgstr ""
msgid "This directory"
msgstr ""
+msgid "This group does not provide any group Runners yet."
+msgstr ""
+
msgid "This is a confidential issue."
msgstr ""
@@ -4241,6 +4568,15 @@ msgstr ""
msgid "This job depends on upstream jobs that need to succeed in order for this job to be triggered"
msgstr ""
+msgid "This job does not have a trace."
+msgstr ""
+
+msgid "This job has been canceled"
+msgstr ""
+
+msgid "This job has been skipped"
+msgstr ""
+
msgid "This job has not been triggered yet"
msgstr ""
@@ -4259,19 +4595,28 @@ msgstr "Questo significa che non è possibile effettuare push di codice fino a c
msgid "This merge request is locked."
msgstr ""
+msgid "This option is disabled while you still have unstaged changes"
+msgstr ""
+
msgid "This page is unavailable because you are not allowed to read information across multiple projects."
msgstr ""
+msgid "This page will be removed in a future release."
+msgstr ""
+
msgid "This project"
msgstr ""
+msgid "This project does not belong to a group and can therefore not make use of group Runners."
+msgstr ""
+
msgid "This repository"
msgstr ""
-msgid "This will delete the custom metric, Are you sure?"
+msgid "This source diff could not be displayed because it is too large."
msgstr ""
-msgid "Those emails automatically become issues (with the comments becoming the email conversation) listed here."
+msgid "This user has no identities"
msgstr ""
msgid "Time before an issue gets scheduled"
@@ -4283,10 +4628,10 @@ msgstr "Il tempo che impiega un issue per esser implementato"
msgid "Time between merge request creation and merge/close"
msgstr "Il tempo tra la creazione di una richiesta di merge ed il merge/close"
-msgid "Time between updates and capacity settings."
+msgid "Time remaining"
msgstr ""
-msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
+msgid "Time spent"
msgstr ""
msgid "Time tracking"
@@ -4310,6 +4655,9 @@ msgstr "%s giorni fa"
msgid "Timeago|%s days remaining"
msgstr "%s giorni rimanenti"
+msgid "Timeago|%s hours ago"
+msgstr "%s ore fa"
+
msgid "Timeago|%s hours remaining"
msgstr "%s ore rimanenti"
@@ -4325,6 +4673,9 @@ msgstr "%s mesi fa"
msgid "Timeago|%s months remaining"
msgstr "%s mesi rimanenti"
+msgid "Timeago|%s seconds ago"
+msgstr "%s secondi fa"
+
msgid "Timeago|%s seconds remaining"
msgstr "%s secondi rimanenti"
@@ -4340,48 +4691,45 @@ msgstr "%s anni fa"
msgid "Timeago|%s years remaining"
msgstr "%s anni rimanenti"
+msgid "Timeago|1 day ago"
+msgstr "1 giorno fa"
+
msgid "Timeago|1 day remaining"
msgstr "1 giorno rimanente"
+msgid "Timeago|1 hour ago"
+msgstr "1 ora fa"
+
msgid "Timeago|1 hour remaining"
msgstr "1 ora rimanente"
+msgid "Timeago|1 minute ago"
+msgstr "1 minuto fa"
+
msgid "Timeago|1 minute remaining"
msgstr "1 minuto rimanente"
+msgid "Timeago|1 month ago"
+msgstr "1 mese fa"
+
msgid "Timeago|1 month remaining"
msgstr "1 mese rimanente"
+msgid "Timeago|1 week ago"
+msgstr "1 settimana fa"
+
msgid "Timeago|1 week remaining"
msgstr "1 settimana rimanente"
+msgid "Timeago|1 year ago"
+msgstr "1 anno fa"
+
msgid "Timeago|1 year remaining"
msgstr "1 anno rimanente"
msgid "Timeago|Past due"
msgstr "Entro"
-msgid "Timeago|a day ago"
-msgstr "un giorno fa"
-
-msgid "Timeago|a month ago"
-msgstr "un mese fa"
-
-msgid "Timeago|a week ago"
-msgstr "una settimana fa"
-
-msgid "Timeago|a year ago"
-msgstr "un anno fa"
-
-msgid "Timeago|about %s hours ago"
-msgstr "circa %s ore fa"
-
-msgid "Timeago|about a minute ago"
-msgstr "circa un minuto fa"
-
-msgid "Timeago|about an hour ago"
-msgstr "circa un ora fa"
-
msgid "Timeago|in %s days"
msgstr "in %s giorni"
@@ -4421,11 +4769,14 @@ msgstr "in 1 settimana"
msgid "Timeago|in 1 year"
msgstr "in 1 anno"
-msgid "Timeago|in a while"
-msgstr ""
+msgid "Timeago|just now"
+msgstr "poco fa"
-msgid "Timeago|less than a minute ago"
-msgstr "meno di un minuto fa"
+msgid "Timeago|right now"
+msgstr "adesso"
+
+msgid "Timeout"
+msgstr ""
msgid "Time|hr"
msgid_plural "Time|hrs"
@@ -4443,19 +4794,10 @@ msgstr "s"
msgid "Tip:"
msgstr ""
-msgid "Title"
-msgstr "Titolo"
-
msgid "To GitLab"
msgstr ""
-msgid "To connect GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to connect."
-msgstr ""
-
-msgid "To connect GitHub repositories, you first need to authorize GitLab to access the list of your GitHub repositories:"
-msgstr ""
-
-msgid "To connect an SVN repository, check out %{svn_link}."
+msgid "To add an SSH key you need to %{generate_link_start}generate one%{link_end} or use an %{existing_link_start}existing key%{link_end}."
msgstr ""
msgid "To import GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
@@ -4467,19 +4809,19 @@ msgstr ""
msgid "To import an SVN repository, check out %{svn_link}."
msgstr ""
-msgid "To only use CI/CD features for an external repository, choose <strong>CI/CD for external repo</strong>."
+msgid "To start serving your jobs you can add Runners to your group"
msgstr ""
-msgid "To set up SAML authentication for your group through an identity provider like Azure, Okta, Onelogin, Ping Identity, or your custom SAML 2.0 provider:"
+msgid "To validate your GitLab CI configurations, go to 'CI/CD → Pipelines' inside your project, and click on the 'CI Lint' button."
msgstr ""
-msgid "To validate your GitLab CI configurations, go to 'CI/CD → Pipelines' inside your project, and click on the 'CI Lint' button."
+msgid "Todo"
msgstr ""
-msgid "To view the roadmap, add a planned start or finish date to one of your epics in this group or its subgroups. Only epics in the past 3 months and the next 3 months are shown."
+msgid "Toggle Sidebar"
msgstr ""
-msgid "Todo"
+msgid "Toggle discussion"
msgstr ""
msgid "Toggle sidebar"
@@ -4491,6 +4833,9 @@ msgstr ""
msgid "ToggleButton|Toggle Status: ON"
msgstr ""
+msgid "Too many changes to show."
+msgstr ""
+
msgid "Total Time"
msgstr "Tempo Totale"
@@ -4500,22 +4845,19 @@ msgstr "Tempo totale di test per tutti i commits/merges"
msgid "Total: %{total}"
msgstr ""
-msgid "Track activity with Contribution Analytics."
-msgstr ""
-
-msgid "Track groups of issues that share a theme, across projects and milestones"
-msgstr ""
-
msgid "Track time with quick actions"
msgstr ""
msgid "Trigger this manual action"
msgstr ""
-msgid "Turn on Service Desk"
+msgid "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."
msgstr ""
-msgid "Unknown"
+msgid "Try again"
+msgstr ""
+
+msgid "Unable to load the diff. %{button_try_again}"
msgstr ""
msgid "Unlock"
@@ -4527,25 +4869,45 @@ msgstr ""
msgid "Unresolve discussion"
msgstr ""
+msgid "Unstage all changes"
+msgstr ""
+
+msgid "Unstage changes"
+msgstr ""
+
+msgid "Unstaged"
+msgstr ""
+
+msgid "Unstaged %{type}"
+msgstr ""
+
+msgid "Unstaged and staged %{type}"
+msgstr ""
+
msgid "Unstar"
msgstr ""
-msgid "Up to date"
+msgid "Unsubscribe"
msgstr ""
-msgid "Upgrade your plan to activate Advanced Global Search."
+msgid "Unsubscribe at group level"
msgstr ""
-msgid "Upgrade your plan to activate Contribution Analytics."
+msgid "Unsubscribe at project level"
msgstr ""
-msgid "Upgrade your plan to activate Group Webhooks."
+msgid "Unverified"
msgstr ""
-msgid "Upgrade your plan to activate Issue weight."
+msgid "Up to date"
msgstr ""
-msgid "Upgrade your plan to improve Issue boards."
+msgid "Update %{files}"
+msgid_plural "Update %{files} files"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Update your group name, description, avatar, and other general settings."
msgstr ""
msgid "Upload New File"
@@ -4566,7 +4928,7 @@ msgstr ""
msgid "Usage statistics"
msgstr ""
-msgid "Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab"
+msgid "Use group milestones to manage issues from multiple projects in the same milestone."
msgstr ""
msgid "Use the following registration token during setup:"
@@ -4575,10 +4937,13 @@ msgstr ""
msgid "Use your global notification setting"
msgstr "Usa le tue impostazioni globali "
-msgid "Used by members to sign in to your group in GitLab"
+msgid "User and IP Rate Limits"
msgstr ""
-msgid "User and IP Rate Limits"
+msgid "Users"
+msgstr ""
+
+msgid "Variables"
msgstr ""
msgid "Variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. You can use variables for passwords, secret keys, or whatever you want."
@@ -4593,10 +4958,7 @@ msgstr ""
msgid "Various settings that affect GitLab performance."
msgstr ""
-msgid "View and edit lines"
-msgstr ""
-
-msgid "View epics list"
+msgid "Verified"
msgstr ""
msgid "View file @ "
@@ -4605,9 +4967,15 @@ msgstr ""
msgid "View group labels"
msgstr ""
+msgid "View jobs"
+msgstr ""
+
msgid "View labels"
msgstr ""
+msgid "View log"
+msgstr ""
+
msgid "View open merge request"
msgstr "Mostra la richieste di merge aperte"
@@ -4635,9 +5003,6 @@ msgstr "Sconosciuto"
msgid "Want to see the data? Please ask an administrator for access."
msgstr "Vuoi visualizzare i dati? Richiedi l'accesso ad un amministratore, grazie."
-msgid "We could not verify that one of your projects on GCP has billing enabled. Please try again."
-msgstr ""
-
msgid "We don't have enough data to show this stage."
msgstr "Non ci sono sufficienti dati da mostrare su questo stadio"
@@ -4650,13 +5015,10 @@ msgstr ""
msgid "Web terminal"
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."
+msgid "When a runner is locked, it cannot be assigned to other projects"
msgstr ""
-msgid "Weight"
-msgstr ""
-
-msgid "When leaving the URL blank, classification labels can still be specified whitout disabling cross project features or performing external authorization checks."
+msgid "When enabled, users cannot use GitLab until the terms have been accepted."
msgstr ""
msgid "Wiki"
@@ -4683,7 +5045,31 @@ msgstr ""
msgid "WikiEdit|There is already a page with the same title in that path."
msgstr ""
-msgid "WikiEmptyPageError|You are not allowed to create wiki pages"
+msgid "WikiEmptyIssueMessage|Suggest wiki improvement"
+msgstr ""
+
+msgid "WikiEmptyIssueMessage|You must be a project member in order to add wiki pages. If you have suggestions for how to improve the wiki for this project, consider opening an issue in the %{issues_link}."
+msgstr ""
+
+msgid "WikiEmptyIssueMessage|issue tracker"
+msgstr ""
+
+msgid "WikiEmpty|A wiki is where you can store all the details about your project. This can include why you've created it, its principles, how to use it, and so on."
+msgstr ""
+
+msgid "WikiEmpty|Create your first page"
+msgstr ""
+
+msgid "WikiEmpty|Suggest wiki improvement"
+msgstr ""
+
+msgid "WikiEmpty|The wiki lets you write documentation for your project"
+msgstr ""
+
+msgid "WikiEmpty|This project has no wiki pages"
+msgstr ""
+
+msgid "WikiEmpty|You must be a project member in order to add wiki pages."
msgstr ""
msgid "WikiHistoricalPage|This is an old version of this page."
@@ -4719,6 +5105,12 @@ msgstr ""
msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?"
msgstr ""
+msgid "WikiPageConfirmDelete|Delete page"
+msgstr ""
+
+msgid "WikiPageConfirmDelete|Delete page %{pageTitle}?"
+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 ""
@@ -4734,7 +5126,7 @@ msgstr ""
msgid "WikiPage|Page slug"
msgstr ""
-msgid "WikiPage|Write your content or drag files here..."
+msgid "WikiPage|Write your content or drag files here…"
msgstr ""
msgid "Wiki|Create Page"
@@ -4746,9 +5138,6 @@ msgstr ""
msgid "Wiki|Edit Page"
msgstr ""
-msgid "Wiki|Empty page"
-msgstr ""
-
msgid "Wiki|More Pages"
msgstr ""
@@ -4767,13 +5156,10 @@ 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"
-msgid "Write a commit message..."
+msgid "Yes"
msgstr ""
msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
@@ -4791,7 +5177,7 @@ msgstr ""
msgid "You are on a read-only GitLab instance."
msgstr ""
-msgid "You are on a secondary (read-only) Geo node. If you want to make any changes, you must visit the %{primary_node}."
+msgid "You can %{linkStart}view the blob%{linkEnd} instead."
msgstr ""
msgid "You can also create a project from the command line."
@@ -4800,6 +5186,9 @@ msgstr ""
msgid "You can also star a label to make it a priority label."
msgstr ""
+msgid "You can also test your .gitlab-ci.yml in the %{linkStart}Lint%{linkEnd}"
+msgstr ""
+
msgid "You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}"
msgstr ""
@@ -4812,30 +5201,33 @@ msgstr "Puoi aggiungere files solo quando sei in una branch"
msgid "You can only edit 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."
+msgid "You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}"
msgstr ""
msgid "You cannot write to this read-only GitLab instance."
msgstr ""
-msgid "You do not have the correct permissions to override the settings from the LDAP group sync."
+msgid "You do not have any assigned merge requests"
msgstr ""
msgid "You have no permissions"
msgstr ""
+msgid "You have not created any merge requests"
+msgstr ""
+
msgid "You have reached your project limit"
msgstr "Hai raggiunto il tuo limite di progetto"
-msgid "You must have master access to force delete a lock"
+msgid "You must accept our Terms of Service and privacy policy in order to register an account"
+msgstr ""
+
+msgid "You must have maintainer access to force delete a lock"
msgstr ""
msgid "You must sign in to star a project"
msgstr "Devi accedere per porre una star al progetto"
-msgid "You need a different license to enable FileLocks feature"
-msgstr ""
-
msgid "You need permission."
msgstr "Necessiti del permesso."
@@ -4902,150 +5294,48 @@ msgstr "Il tuo nome"
msgid "Your projects"
msgstr ""
+msgid "ago"
+msgstr "fa"
+
msgid "among other things"
msgstr ""
-msgid "and %d fixed vulnerability"
-msgid_plural "and %d fixed vulnerabilities"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "assign yourself"
msgstr ""
msgid "branch name"
msgstr ""
-msgid "by"
-msgstr ""
-
-msgid "ciReport|%{type} detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|%{type} detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Code quality"
-msgstr ""
-
-msgid "ciReport|DAST detected no alerts by analyzing the review app"
-msgstr ""
-
-msgid "ciReport|Dependency scanning"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Failed to load %{reportName} report"
-msgstr ""
-
-msgid "ciReport|Fixed:"
-msgstr ""
-
-msgid "ciReport|Instances"
-msgstr ""
-
-msgid "ciReport|Learn more about whitelisting"
-msgstr ""
-
-msgid "ciReport|Loading %{reportName} report"
-msgstr ""
-
-msgid "ciReport|No changes to code quality"
-msgstr ""
-
-msgid "ciReport|No changes to performance metrics"
-msgstr ""
-
-msgid "ciReport|Performance metrics"
-msgstr ""
-
-msgid "ciReport|SAST"
-msgstr ""
-
-msgid "ciReport|SAST detected"
-msgstr ""
-
-msgid "ciReport|SAST detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|SAST detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|SAST:container no vulnerabilities were found"
-msgstr ""
-
-msgid "ciReport|Security scanning"
-msgstr ""
-
-msgid "ciReport|Security scanning failed loading any results"
-msgstr ""
-
-msgid "ciReport|Show complete code vulnerabilities report"
-msgstr ""
-
-msgid "ciReport|Unapproved vulnerabilities (red) can be marked as approved. %{helpLink}"
-msgstr ""
-
-msgid "ciReport|no vulnerabilities"
-msgstr ""
-
msgid "command line instructions"
msgstr ""
msgid "connecting"
msgstr ""
-msgid "could not read private key, is the passphrase correct?"
-msgstr ""
-
msgid "day"
msgid_plural "days"
msgstr[0] "giorno"
msgstr[1] "giorni"
-msgid "detected %d fixed vulnerability"
-msgid_plural "detected %d fixed vulnerabilities"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "detected %d new vulnerability"
-msgid_plural "detected %d new vulnerabilities"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "detected no vulnerabilities"
-msgstr ""
-
-msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
-msgstr ""
-
-msgid "here"
+msgid "deploy token"
msgstr ""
-msgid "importing"
+msgid "disabled"
msgstr ""
-msgid "in progress"
+msgid "enabled"
msgstr ""
-msgid "is invalid because there is downstream lock"
+msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
msgstr ""
-msgid "is invalid because there is upstream lock"
+msgid "for this project"
msgstr ""
-msgid "is not a valid X509 certificate."
+msgid "importing"
msgstr ""
-msgid "locked by %{path_lock_user_name} %{created_at}"
+msgid "latest version"
msgstr ""
msgid "merge request"
@@ -5065,28 +5355,7 @@ msgstr ""
msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage is %{emphasisStart} unchanged %{emphasisEnd} at %{memoryFrom}MB"
msgstr ""
-msgid "mrWidget|Add approval"
-msgstr ""
-
-msgid "mrWidget|Allows edits from maintainers"
-msgstr ""
-
-msgid "mrWidget|An error occured while removing your approval."
-msgstr ""
-
-msgid "mrWidget|An error occured while retrieving approval data for this merge request."
-msgstr ""
-
-msgid "mrWidget|An error occured while submitting your approval."
-msgstr ""
-
-msgid "mrWidget|Approve"
-msgstr ""
-
-msgid "mrWidget|Approved"
-msgstr ""
-
-msgid "mrWidget|Approved by"
+msgid "mrWidget|Allows commits from members who can merge to the target branch"
msgstr ""
msgid "mrWidget|Cancel automatic merge"
@@ -5113,6 +5382,9 @@ msgstr ""
msgid "mrWidget|Closes"
msgstr ""
+msgid "mrWidget|Create an issue to resolve them later"
+msgstr ""
+
msgid "mrWidget|Deployment statistics are not available currently"
msgstr ""
@@ -5167,9 +5439,6 @@ msgstr ""
msgid "mrWidget|Remove source branch"
msgstr ""
-msgid "mrWidget|Remove your approval"
-msgstr ""
-
msgid "mrWidget|Request to merge"
msgstr ""
@@ -5209,6 +5478,9 @@ msgstr ""
msgid "mrWidget|There are merge conflicts"
msgstr ""
+msgid "mrWidget|There are unresolved discussions. Please resolve these discussions"
+msgstr ""
+
msgid "mrWidget|This merge request failed to be merged automatically"
msgstr ""
@@ -5259,7 +5531,7 @@ msgstr ""
msgid "personal access token"
msgstr ""
-msgid "private key does not match certificate."
+msgid "remaining"
msgstr ""
msgid "remove due date"
@@ -5274,9 +5546,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "to help your contributors communicate effectively!"
-msgstr ""
-
msgid "username"
msgstr ""
@@ -5286,3 +5555,8 @@ msgstr ""
msgid "with %{additions} additions, %{deletions} deletions."
msgstr ""
+msgid "within %d minute "
+msgid_plural "within %d minutes "
+msgstr[0] "in %d minuto "
+msgstr[1] "entro %d minuti "
+
diff --git a/locale/ja/gitlab.po b/locale/ja/gitlab.po
index b526b0ba202..ccdb620bd1e 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: 2018-04-04 19:35+0200\n"
-"PO-Revision-Date: 2018-04-05 03:36-0400\n"
+"POT-Creation-Date: 2018-07-01 16:35+1000\n"
+"PO-Revision-Date: 2018-07-01 11:02\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Japanese\n"
"Language: ja_JP\n"
@@ -16,8 +16,9 @@ msgstr ""
"X-Crowdin-Language: ja\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-msgid " and"
-msgstr ""
+msgid "%d changed file"
+msgid_plural "%d changed files"
+msgstr[0] ""
msgid "%d commit"
msgid_plural "%d commits"
@@ -25,28 +26,36 @@ msgstr[0] "%d個ã®ã‚³ãƒŸãƒƒãƒˆ"
msgid "%d commit behind"
msgid_plural "%d commits behind"
-msgstr[0] ""
+msgstr[0] "%d個ã®ã‚³ãƒŸãƒƒãƒˆå¾…ã¡"
msgid "%d exporter"
msgid_plural "%d exporters"
-msgstr[0] ""
+msgstr[0] "%d exporter"
msgid "%d issue"
msgid_plural "%d issues"
-msgstr[0] ""
+msgstr[0] "%d個ã®èª²é¡Œ"
msgid "%d layer"
msgid_plural "%d layers"
-msgstr[0] ""
+msgstr[0] "%d 個ã®ãƒ¬ã‚¤ãƒ¤ãƒ¼"
msgid "%d merge request"
msgid_plural "%d merge requests"
-msgstr[0] ""
+msgstr[0] "%d 個ã®ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆ"
msgid "%d metric"
msgid_plural "%d metrics"
msgstr[0] ""
+msgid "%d staged change"
+msgid_plural "%d staged changes"
+msgstr[0] ""
+
+msgid "%d unstaged change"
+msgid_plural "%d unstaged changes"
+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 個ã®ã‚³ãƒŸãƒƒãƒˆã‚’çœç•¥ã—ã¾ã—ãŸã€‚"
@@ -55,196 +64,278 @@ msgid "%{actionText} & %{openOrClose} %{noteable}"
msgstr ""
msgid "%{commit_author_link} authored %{commit_timeago}"
-msgstr ""
+msgstr "%{commit_author_link} ㌠%{commit_timeago} ã«ã‚³ãƒŸãƒƒãƒˆã—ã¾ã—ãŸ"
msgid "%{count} participant"
msgid_plural "%{count} participants"
-msgstr[0] ""
+msgstr[0] "%{count} 人ã®å‚加者"
-msgid "%{loadingIcon} Started"
+msgid "%{filePath} deleted"
msgstr ""
-msgid "%{lock_path} is locked by GitLab User %{lock_user_id}"
+msgid "%{group_docs_link_start}Groups%{group_docs_link_end} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects."
msgstr ""
+msgid "%{loadingIcon} Started"
+msgstr "%{loadingIcon} é–‹å§‹"
+
+msgid "%{lock_path} is locked by GitLab User %{lock_user_id}"
+msgstr "%{lock_path} ã¯GitLab ユーザー %{lock_user_id} ã«ã‚ˆã£ã¦ãƒ­ãƒƒã‚¯ã•れã¦ã„ã¾ã™"
+
+msgid "%{nip_domain} can be used as an alternative to a custom domain."
+msgstr "%{nip_domain} ã¯ã€ã‚«ã‚¹ã‚¿ãƒ ãƒ‰ãƒ¡ã‚¤ãƒ³ã®ä»£æ›¿ã¨ã—ã¦ä½¿ç”¨ã§ãã¾ã™ã€‚"
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will allow access on the next attempt."
-msgstr ""
+msgstr "%{maximum_failures}回中 %{number_of_failures}回失敗。次回アクセスå¯èƒ½ã§ã™ã€‚"
msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will not retry automatically. Reset storage information when the problem is resolved."
-msgstr ""
+msgstr "%{maximum_failures}回中 %{number_of_failures}回失敗。GitLab ã¯å†è©¦è¡Œã—ã¾ã›ã‚“。å•題を解決ã—ã¦ã‹ã‚‰ã‚¹ãƒˆãƒ¬ãƒ¼ã‚¸æƒ…報をリセットã—ã¦ãã ã•ã„。"
msgid "%{openOrClose} %{noteable}"
-msgstr ""
+msgstr "%{openOrClose} %{noteable}"
+
+msgid "%{percent}%% complete"
+msgstr "%{percent}%% 完了"
msgid "%{storage_name}: failed storage access attempt on host:"
msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:"
-msgstr[0] ""
+msgstr[0] "%{storage_name}: 失敗ã—ãŸã‚¢ã‚¯ã‚»ã‚¹ã®è©¦è¡Œå›žæ•° %{failed_attempts}:"
msgid "%{text} is available"
+msgstr "%{text} ãŒåˆ©ç”¨ã§ãã¾ã™ã€‚"
+
+msgid "%{title} changes"
+msgstr "%{title} ã®å¤‰æ›´"
+
+msgid "%{unstaged} unstaged and %{staged} staged changes"
msgstr ""
msgid "(checkout the %{link} for information on how to install it)."
-msgstr ""
+msgstr "(インストール方法㯠%{link} ã‚’ã”覧ãã ã•ã„)。"
msgid "+ %{moreCount} more"
+msgstr "+ ä»– %{moreCount} ä»¶"
+
+msgid "- Runner is active and can process any new jobs"
+msgstr ""
+
+msgid "- Runner is paused and will not receive any new jobs"
msgstr ""
msgid "- show less"
msgstr ""
+msgid "1 %{type} addition"
+msgid_plural "%{count} %{type} additions"
+msgstr[0] "%{count} %{type} ä»¶ã®è¿½åŠ æƒ…å ±"
+
+msgid "1 %{type} modification"
+msgid_plural "%{count} %{type} modifications"
+msgstr[0] "%{count} ä»¶ %{type} ã®ä¿®æ­£"
+
+msgid "1 closed issue"
+msgid_plural "%d closed issues"
+msgstr[0] ""
+
+msgid "1 closed merge request"
+msgid_plural "%d closed merge requests"
+msgstr[0] "%dä»¶ã®ã‚¯ãƒ­ãƒ¼ã‚ºã•れãŸãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆ"
+
+msgid "1 merged merge request"
+msgid_plural "%d merged merge requests"
+msgstr[0] "%dä»¶ã®ãƒžãƒ¼ã‚¸ã•れãŸãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆ"
+
+msgid "1 open issue"
+msgid_plural "%d open issues"
+msgstr[0] "%dä»¶ã®èª²é¡Œ"
+
+msgid "1 open merge request"
+msgid_plural "%d open merge requests"
+msgstr[0] "%dä»¶ã®ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆ"
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] "%d 個ã®ãƒ‘イプライン"
msgid "1st contribution!"
-msgstr ""
+msgstr "最åˆã®è²¢çŒ®!"
msgid "2FA enabled"
msgstr ""
+msgid "403|Please contact your GitLab administrator to get the permission."
+msgstr ""
+
+msgid "403|You don't have the permission to access this page."
+msgstr ""
+
+msgid "404|Make sure the address is correct and the page hasn't moved."
+msgstr ""
+
+msgid "404|Page Not Found"
+msgstr ""
+
+msgid "404|Please contact your GitLab administrator if you think this is a mistake."
+msgstr ""
+
msgid "<strong>Removes</strong> source branch"
+msgstr "ソースブランãƒã‚’<strong>削除</strong>"
+
+msgid "A 'Runner' is a process which runs a job. You can setup as many Runners as you need."
msgstr ""
msgid "A collection of graphs regarding Continuous Integration"
msgstr "CIã«ã¤ã„ã¦ã®ã‚°ãƒ©ãƒ•"
msgid "A new branch will be created in your fork and a new merge request will be started."
-msgstr ""
+msgstr "ãƒ•ã‚©ãƒ¼ã‚¯ã«æ–°ã—ã„ブランãƒãŒä½œæˆã•ã‚Œã€æ–°ã—ã„マージリクエストãŒé–‹å§‹ã—ã¾ã™ã€‚"
msgid "A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}."
+msgstr "プロジェクトã¨ã¯ãƒ•ァイルを格ç´(リãƒã‚¸ãƒˆãƒª) ã—ã€è¨ˆç”»ã‚’ç«‹ã¦(課題)ã€ãƒ‰ã‚­ãƒ¥ãƒ¡ãƒ³ãƒˆã‚’公開(wiki) ã™ã‚‹å ´æ‰€ã§ã™ã€‚ %{among_other_things_link}"
+
+msgid "A regular expression that will be used to find the test coverage output in the job trace. Leave blank to disable"
msgstr ""
msgid "A user with write access to the source branch selected this option"
-msgstr ""
+msgstr "ã“ã®ã‚ªãƒ—ã‚·ãƒ§ãƒ³ã‚’é¸æŠžã—ãŸã‚½ãƒ¼ã‚¹ãƒ–ランãƒã¸ã®æ›¸ãè¾¼ã¿ã‚’許å¯ã•れãŸãƒ¦ãƒ¼ã‚¶ãƒ¼"
msgid "About auto deploy"
msgstr "自動デプロイã«ã¤ã„ã¦"
msgid "Abuse Reports"
-msgstr ""
+msgstr "䏿­£è¡Œç‚ºãƒ¬ãƒãƒ¼ãƒˆ"
msgid "Abuse reports"
msgstr ""
-msgid "Access Tokens"
+msgid "Accept terms"
msgstr ""
+msgid "Access Tokens"
+msgstr "アクセス トークン"
+
msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again."
+msgstr "mount ã«ã‚ˆã£ã¦å¾©æ—§ã§ãるよã†ã«ã€å¤±æ•—ãŒç™ºç”Ÿã—ã¦ã„るストレージã¸ã®ã‚¢ã‚¯ã‚»ã‚¹ã‚’ä¸€æ™‚çš„ã«æŠ‘æ­¢ã—ã¾ã—ãŸã€‚å†åº¦ã‚¢ã‚¯ã‚»ã‚¹ã™ã‚‹ãŸã‚ã«ã¯ã€å•題を解決ã—ã¦ã‹ã‚‰ã‚¹ãƒˆãƒ¬ãƒ¼ã‚¸æƒ…報をリセットã—ã¦ãã ã•ã„。"
+
+msgid "Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report."
msgstr ""
msgid "Account"
-msgstr ""
+msgstr "アカウント"
-msgid "Account and limit settings"
+msgid "Account and limit"
msgstr ""
msgid "Active"
msgstr "有効"
+msgid "Active Sessions"
+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 Kubernetes cluster"
-msgstr ""
+msgstr "Kubernetes クラスターを追加"
msgid "Add License"
msgstr "ライセンスを追加"
msgid "Add Readme"
-msgstr ""
+msgstr "Readmeを追加"
msgid "Add new directory"
msgstr "æ–°è¦ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒªã‚’追加"
-msgid "Add todo"
+msgid "Add reaction"
msgstr ""
+msgid "Add todo"
+msgstr "Todoを追加"
+
msgid "AdminArea|Stop all jobs"
-msgstr ""
+msgstr "å…¨ã¦ã®ã‚¸ãƒ§ãƒ–ã‚’åœæ­¢"
msgid "AdminArea|Stop all jobs?"
-msgstr ""
+msgstr "å…¨ã¦ã®ã‚¸ãƒ§ãƒ–ã‚’åœæ­¢ã—ã¾ã™ã‹?"
msgid "AdminArea|Stop jobs"
-msgstr ""
+msgstr "ã‚¸ãƒ§ãƒ–ã‚’åœæ­¢"
msgid "AdminArea|Stopping jobs failed"
-msgstr ""
+msgstr "ジョブã®åœæ­¢ã«å¤±æ•—ã—ã¾ã—ãŸ"
msgid "AdminArea|You’re about to stop all jobs.This will halt all current jobs that are running."
-msgstr ""
+msgstr "å…¨ã¦ã®ã‚¸ãƒ§ãƒ–ã‚’åœæ­¢ã—ã¾ã™ã€‚ã“れã«ã‚ˆã‚Šç¾åœ¨å®Ÿè¡Œä¸­ã®ã‚¸ãƒ§ãƒ–ã¯åœæ­¢ã•れã¾ã™ã€‚"
msgid "AdminHealthPageLink|health page"
msgstr ""
msgid "AdminProjects|Delete"
-msgstr ""
+msgstr "削除"
msgid "AdminProjects|Delete Project %{projectName}?"
-msgstr ""
+msgstr "プロジェクト %{projectName} を削除ã—ã¾ã™ã‹?"
msgid "AdminProjects|Delete project"
-msgstr ""
+msgstr "プロジェクトã®å‰Šé™¤"
msgid "AdminSettings|Specify a domain to use by default for every project's Auto Review Apps and Auto Deploy stages."
msgstr ""
msgid "AdminUsers|Block user"
-msgstr ""
+msgstr "ブロックユーザー"
msgid "AdminUsers|Delete User %{username} and contributions?"
-msgstr ""
+msgstr "ユーザー %{username} ã¨è²¢çŒ®åº¦ã‚’削除ã—ã¾ã™ã‹?"
msgid "AdminUsers|Delete User %{username}?"
-msgstr ""
+msgstr "ユーザー %{username} を削除ã—ã¾ã™ã‹?"
msgid "AdminUsers|Delete user"
-msgstr ""
+msgstr "ユーザーã®å‰Šé™¤"
msgid "AdminUsers|Delete user and contributions"
-msgstr ""
+msgstr "ユーザーã¨è²¢çŒ®åº¦ã®å‰Šé™¤"
msgid "AdminUsers|To confirm, type %{projectName}"
-msgstr ""
+msgstr "確èªã®ãŸã‚ã€%{projectName} を入力ã—ã¦ãã ã•ã„"
msgid "AdminUsers|To confirm, type %{username}"
-msgstr ""
+msgstr "確èªã®ãŸã‚ã€%{username} を入力ã—ã¦ãã ã•ã„"
msgid "Advanced"
msgstr ""
msgid "Advanced settings"
-msgstr ""
+msgstr "高度ãªè¨­å®š"
msgid "All"
-msgstr ""
+msgstr "ã™ã¹ã¦"
msgid "All changes are committed"
-msgstr ""
+msgstr "ã™ã¹ã¦ã®å¤‰æ›´ãŒã‚³ãƒŸãƒƒãƒˆã•れã¦ã„ã¾ã™"
msgid "All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings."
+msgstr "空ã®ãƒ—ロジェクトã€ãƒ†ãƒ³ãƒ—レートã‹ã‚‰ã€ã¾ãŸã¯ã‚¤ãƒ³ãƒãƒ¼ãƒˆæ™‚ã«ã™ã¹ã¦ã®æ©Ÿèƒ½ãŒæœ‰åйã«ãªã£ã¦ã„ã¾ã™ãŒã€å¾Œã§ãƒ—ロジェクト設定ã§ç„¡åйã«ã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚"
+
+msgid "Allow commits from members who can merge to the target branch."
msgstr ""
-msgid "Allow edits from maintainers."
+msgid "Allow public access to pipelines and job details, including output logs and artifacts"
msgstr ""
msgid "Allow rendering of PlantUML diagrams in Asciidoc documents."
-msgstr ""
+msgstr "Asciidocドキュメントã§ã®PlantUML図ã®ãƒ¬ãƒ³ãƒ€ãƒªãƒ³ã‚°ã‚’許å¯ã—ã¾ã™ã€‚"
msgid "Allow requests to the local network from hooks and services."
msgstr ""
@@ -252,146 +343,140 @@ msgstr ""
msgid "Allows you to add and manage Kubernetes clusters."
msgstr ""
-msgid "Also called \"Issuer\" or \"Relying party trust identifier\""
+msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
msgstr ""
-msgid "Also called \"Relying party service URL\" or \"Reply URL\""
+msgid "An error occured creating the new branch."
msgstr ""
-msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to connect."
+msgid "An error occured whilst loading all the files."
msgstr ""
-msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
+msgid "An error occured whilst loading the file content."
msgstr ""
-msgid "An error occurred previewing the blob"
+msgid "An error occured whilst loading the file."
msgstr ""
-msgid "An error occurred when toggling the notification subscription"
+msgid "An error occured whilst loading the merge request changes."
msgstr ""
-msgid "An error occurred when updating the issue weight"
+msgid "An error occured whilst loading the merge request version data."
msgstr ""
-msgid "An error occurred while adding approver"
+msgid "An error occured whilst loading the merge request."
msgstr ""
-msgid "An error occurred while detecting host keys"
+msgid "An error occurred previewing the blob"
+msgstr "Blobã®ãƒ—レビュー中ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸ"
+
+msgid "An error occurred when toggling the notification subscription"
msgstr ""
-msgid "An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again."
+msgid "An error occurred while dismissing the alert. Refresh the page and try again."
msgstr ""
+msgid "An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again."
+msgstr "ãƒã‚¤ãƒ©ã‚¤ãƒˆã‚’消去ã—ã¦ã„ã‚‹ã¨ãã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ページを更新ã—ã¦ã‚‚ã†ä¸€åº¦æ¶ˆåŽ»ã—ã¦ãã ã•ã„。"
+
msgid "An error occurred while fetching markdown preview"
msgstr ""
msgid "An error occurred while fetching sidebar data"
-msgstr ""
+msgstr "サイドãƒãƒ¼ã®ãƒ‡ãƒ¼ã‚¿å–得中ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚"
msgid "An error occurred while fetching the pipeline."
-msgstr ""
+msgstr "パイプラインã®å–得中ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚"
msgid "An error occurred while getting projects"
msgstr ""
-msgid "An error occurred while importing project"
-msgstr ""
-
-msgid "An error occurred while initializing path locks"
+msgid "An error occurred while importing project: ${details}"
msgstr ""
msgid "An error occurred while loading commits"
-msgstr ""
+msgstr "コミットを読ã¿è¾¼ã¿ä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸ"
msgid "An error occurred while loading diff"
-msgstr ""
+msgstr "差分ã®èª­ã¿è¾¼ã¿ä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸ"
msgid "An error occurred while loading filenames"
-msgstr ""
+msgstr "ファイルåã®èª­ã¿è¾¼ã¿ä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸ"
msgid "An error occurred while loading the file"
-msgstr ""
+msgstr "ファイルã®èª­ã¿è¾¼ã¿ä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸ"
msgid "An error occurred while making the request."
-msgstr ""
-
-msgid "An error occurred while removing approver"
-msgstr ""
+msgstr "リクエスト作æˆä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚"
msgid "An error occurred while rendering KaTeX"
-msgstr ""
+msgstr "KaTeXã®ãƒ¬ãƒ³ãƒ€ãƒªãƒ³ã‚°ä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸ"
msgid "An error occurred while rendering preview broadcast message"
msgstr ""
msgid "An error occurred while retrieving calendar activity"
-msgstr ""
+msgstr "カレンダーアクティビティーå–å¾—ã®éš›ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸ"
msgid "An error occurred while retrieving diff"
-msgstr ""
-
-msgid "An error occurred while saving LDAP override status. Please try again."
-msgstr ""
+msgstr "差分をå–å¾—ã®éš›ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸ"
msgid "An error occurred while saving assignees"
-msgstr ""
+msgstr "担当者ã®ç™»éŒ²ä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸ"
msgid "An error occurred while validating username"
-msgstr ""
+msgstr "ユーザåã®æ¤œè¨¼ä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸ"
msgid "An error occurred. Please try again."
-msgstr ""
-
-msgid "Any Label"
-msgstr ""
+msgstr "エラーãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚å†åº¦ãŠè©¦ã—ãã ã•ã„。"
msgid "Appearance"
-msgstr ""
+msgstr "外観"
msgid "Applications"
-msgstr ""
+msgstr "アプリケーション"
msgid "Apr"
-msgstr ""
+msgstr "4月"
msgid "April"
-msgstr ""
+msgstr "4月"
-msgid "Archived project! Repository is read-only"
-msgstr "アーカイブ済ã¿ãƒ—ロジェクトï¼ï¼ˆãƒ¬ãƒã‚¸ãƒˆãƒªãƒ¼ã¯èª­ã¿å–り専用ã§ã™ï¼‰"
+msgid "Archived project! Repository and other project resources are read-only"
+msgstr ""
msgid "Are you sure you want to delete this pipeline schedule?"
msgstr "ã“ã®ãƒ‘イプラインスケジュールを削除ã—ã¾ã™ã‹ï¼Ÿ"
-msgid "Are you sure you want to reset registration token?"
+msgid "Are you sure you want to remove this identity?"
msgstr ""
-msgid "Are you sure you want to reset the health check token?"
-msgstr ""
+msgid "Are you sure you want to reset registration token?"
+msgstr "本当ã«ç™»éŒ²ãƒˆãƒ¼ã‚¯ãƒ³ã‚’リセットã—ã¾ã™ã‹ï¼Ÿ"
-msgid "Are you sure you want to unlock %{path_lock_path}?"
+msgid "Are you sure you want to reset the health check token?"
msgstr ""
msgid "Are you sure?"
-msgstr ""
+msgstr "本当ã«ã‚ˆã‚ã—ã„ã§ã™ã‹ï¼Ÿ"
msgid "Artifacts"
-msgstr ""
+msgstr "アーティファクト"
-msgid "Assertion consumer service URL"
+msgid "Ask your group maintainer to setup a group Runner."
msgstr ""
msgid "Assign custom color like #FF0000"
-msgstr ""
+msgstr "#FF0000ã®ã‚ˆã†ãªã‚«ã‚¹ã‚¿ãƒ ã‚«ãƒ©ãƒ¼ã‚’割り当ã¦ã‚‹"
msgid "Assign labels"
-msgstr ""
+msgstr "ラベルを割り当ã¦ã‚‹"
msgid "Assign milestone"
-msgstr ""
+msgstr "マイルストーンを割り当ã¦ã‚‹"
msgid "Assign to"
-msgstr ""
+msgstr "割り当ã¦å…ˆ"
msgid "Assigned Issues"
msgstr ""
@@ -402,20 +487,26 @@ msgstr ""
msgid "Assigned to :name"
msgstr ""
+msgid "Assigned to me"
+msgstr ""
+
msgid "Assignee"
+msgstr "担当者"
+
+msgid "Assignee(s)"
msgstr ""
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "ドラッグ&ドロップã¾ãŸã¯ %{upload_link} ã§ãƒ•ァイルを添付"
msgid "Aug"
-msgstr ""
+msgstr "8月"
msgid "August"
-msgstr ""
+msgstr "8月"
msgid "Authentication Log"
-msgstr ""
+msgstr "èªè¨¼ãƒ­ã‚°"
msgid "Author"
msgstr ""
@@ -438,14 +529,17 @@ msgstr ""
msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
msgstr ""
-msgid "AutoDevOps|Auto DevOps (Beta)"
+msgid "Auto-cancel redundant, pending pipelines"
msgstr ""
+msgid "AutoDevOps|Auto DevOps"
+msgstr "Auto DevOps"
+
msgid "AutoDevOps|Auto DevOps documentation"
msgstr ""
msgid "AutoDevOps|Enable in settings"
-msgstr ""
+msgstr "設定を有効ã«ã™ã‚‹"
msgid "AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
msgstr ""
@@ -454,101 +548,131 @@ msgid "AutoDevOps|Learn more in the %{link_to_documentation}"
msgstr ""
msgid "AutoDevOps|You can automatically build and test your application if you %{link_to_auto_devops_settings} for this project. You can automatically deploy it as well, if you %{link_to_add_kubernetes_cluster}."
-msgstr ""
+msgstr "ã“ã®ãƒ—ロジェクト㮠%{link_to_auto_devops_settings} ã‚’ã™ã‚‹å ´åˆã€è‡ªå‹•ビルドãŠã‚ˆã³ãƒ†ã‚¹ãƒˆã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚%{link_to_add_kubernetes_cluster} ã—ãŸå ´åˆã€è‡ªå‹•デプロイã™ã‚‹ã“ã¨ã‚‚ã§ãã¾ã™ã€‚"
msgid "AutoDevOps|add a Kubernetes cluster"
-msgstr ""
+msgstr "Kubernetes クラスターを追加"
-msgid "AutoDevOps|enable Auto DevOps (Beta)"
+msgid "AutoDevOps|enable Auto DevOps"
msgstr ""
msgid "Available"
+msgstr "利用å¯èƒ½"
+
+msgid "Available group Runners : %{runners}"
msgstr ""
-msgid "Avatar will be removed. Are you sure?"
+msgid "Available group Runners : %{runners}."
msgstr ""
+msgid "Avatar will be removed. Are you sure?"
+msgstr "ã‚¢ãƒã‚¿ãƒ¼ã‚’削除ã—ã¾ã™ã€‚よã‚ã—ã„ã§ã™ã‹ï¼Ÿ"
+
msgid "Average per day: %{average}"
msgstr ""
-msgid "Background Color"
+msgid "Background color"
msgstr ""
msgid "Background jobs"
msgstr ""
-msgid "Begin with the selected commit"
-msgstr ""
+msgid "Badges"
+msgstr "ãƒãƒƒã‚¸"
-msgid "Billing"
-msgstr ""
+msgid "Badges|A new badge was added."
+msgstr "æ–°ã—ã„ãƒãƒƒã‚¸ãŒè¿½åŠ ã•れã¾ã—ãŸã€‚"
-msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan."
-msgstr ""
+msgid "Badges|Add badge"
+msgstr "ãƒãƒƒã‚¸ã‚’追加"
-msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available."
-msgstr ""
+msgid "Badges|Adding the badge failed, please check the entered URLs and try again."
+msgstr "ãƒãƒƒã‚¸ã‚’追加ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚入力ã—ãŸURLを確èªã—ã¦ã€ã‚‚ã†ä¸€åº¦ãŠè©¦ã—ãã ã•ã„。"
-msgid "BillingPlans|Current plan"
-msgstr ""
+msgid "Badges|Badge image URL"
+msgstr "ãƒãƒƒã‚¸ç”»åƒã®URL"
-msgid "BillingPlans|Customer Support"
-msgstr ""
+msgid "Badges|Badge image preview"
+msgstr "ãƒãƒƒã‚¸ç”»åƒãƒ—レビュー"
-msgid "BillingPlans|Downgrade"
-msgstr ""
+msgid "Badges|Delete badge"
+msgstr "ãƒãƒƒã‚¸ã‚’削除"
-msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
-msgstr ""
+msgid "Badges|Delete badge?"
+msgstr "ãƒãƒƒã‚¸ã‚’削除ã—ã¾ã™ã‹ï¼Ÿ"
-msgid "BillingPlans|Manage plan"
-msgstr ""
+msgid "Badges|Deleting the badge failed, please try again."
+msgstr "ãƒãƒƒã‚¸ã‚’削除ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ã‚‚ã†ä¸€åº¦ãŠè©¦ã—ãã ã•ã„。"
-msgid "BillingPlans|Please contact %{customer_support_link} in that case."
-msgstr ""
+msgid "Badges|Group Badge"
+msgstr "グループãƒãƒƒã‚¸"
-msgid "BillingPlans|See all %{plan_name} features"
-msgstr ""
+msgid "Badges|Link"
+msgstr "リンク"
-msgid "BillingPlans|This group uses the plan associated with its parent group."
-msgstr ""
+msgid "Badges|No badge image"
+msgstr "ãƒãƒƒã‚¸ç”»åƒãªã—"
-msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
-msgstr ""
+msgid "Badges|No image to preview"
+msgstr "プレビューã™ã‚‹ç”»åƒã¯ã‚りã¾ã›ã‚“"
-msgid "BillingPlans|Upgrade"
-msgstr ""
+msgid "Badges|Project Badge"
+msgstr "プロジェクト ãƒãƒƒã‚¸"
-msgid "BillingPlans|You are currently on the %{plan_link} plan."
-msgstr ""
+msgid "Badges|Reload badge image"
+msgstr "ãƒãƒƒã‚¸ç”»åƒã®å†èª­ã¿è¾¼ã¿"
-msgid "BillingPlans|frequently asked questions"
+msgid "Badges|Save changes"
+msgstr "変更ã®ä¿å­˜"
+
+msgid "Badges|Saving the badge failed, please check the entered URLs and try again."
+msgstr "ãƒãƒƒã‚¸ã‚’ä¿å­˜ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚入力ã—ãŸURLを確èªã—ã¦ã€ã‚‚ã†ä¸€åº¦ãŠè©¦ã—ãã ã•ã„。"
+
+msgid "Badges|The %{docsLinkStart}variables%{docsLinkEnd} GitLab supports: %{placeholders}"
+msgstr "GitLabã¯æ¬¡ã® %{docsLinkStart}変数%{docsLinkEnd} をサãƒãƒ¼ãƒˆã—ã¦ã„ã¾ã™: %{placeholders}"
+
+msgid "Badges|The badge was deleted."
+msgstr "ãƒãƒƒã‚¸ãŒå‰Šé™¤ã•れã¾ã—ãŸã€‚"
+
+msgid "Badges|The badge was saved."
+msgstr "ãƒãƒƒã‚¸ãŒä¿å­˜ã•れã¾ã—ãŸã€‚"
+
+msgid "Badges|This group has no badges"
+msgstr "ã“ã®ã‚°ãƒ«ãƒ¼ãƒ—ã«ãƒãƒƒã‚¸ã¯ã‚りã¾ã›ã‚“"
+
+msgid "Badges|This project has no badges"
+msgstr "ã“ã®ãƒ—ロジェクトã«ãƒãƒƒã‚¸ã¯ã‚りã¾ã›ã‚“"
+
+msgid "Badges|Your badges"
+msgstr "ãƒãƒƒã‚¸"
+
+msgid "Begin with the selected commit"
msgstr ""
-msgid "BillingPlans|monthly"
+msgid "Below are examples of regex for existing tools:"
msgstr ""
-msgid "BillingPlans|paid annually at %{price_per_year}"
+msgid "Boards"
msgstr ""
-msgid "BillingPlans|per user"
+msgid "Branch %{branchName} was not found in this project's repository."
msgstr ""
msgid "Branch (%{branch_count})"
msgid_plural "Branches (%{branch_count})"
-msgstr[0] ""
+msgstr[0] "ブランム(%{branch_count})"
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 ""
+msgstr "ブランãƒãŒå¤‰æ›´ã•れã¾ã—ãŸ"
msgid "Branch is already taken"
-msgstr ""
+msgstr "ブランãƒã¯æ—¢ã«ä½¿ç”¨ã•れã¦ã„ã¾ã™"
msgid "Branch name"
-msgstr ""
+msgstr "ブランãƒå"
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "ブランãƒã‚’検索"
@@ -560,28 +684,28 @@ msgid "Branches"
msgstr "ブランãƒ"
msgid "Branches|Active"
-msgstr ""
+msgstr "アクティブ"
msgid "Branches|Active branches"
-msgstr ""
+msgstr "アクティブブランãƒ"
msgid "Branches|All"
-msgstr ""
+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 ""
msgid "Branches|Delete branch"
-msgstr ""
+msgstr "ブランãƒã‚’削除"
msgid "Branches|Delete merged branches"
-msgstr ""
+msgstr "マージã•れãŸãƒ–ランãƒã‚’削除"
msgid "Branches|Delete protected branch"
msgstr ""
@@ -590,52 +714,52 @@ msgid "Branches|Delete protected branch '%{branch_name}'?"
msgstr ""
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"
+msgid "Branches|Only a project maintainer or owner can delete a protected branch"
msgstr ""
msgid "Branches|Overview"
-msgstr ""
+msgstr "概è¦"
msgid "Branches|Protected branches can be managed in %{project_settings_link}."
-msgstr ""
+msgstr "ä¿è­·ã•れãŸãƒ–ランãƒã¯ %{project_settings_link} ã‹ã‚‰è¨­å®šå¤‰æ›´ã§ãã¾ã™ã€‚"
msgid "Branches|Show active branches"
-msgstr ""
+msgstr "アクティブブランãƒã‚’表示"
msgid "Branches|Show all branches"
-msgstr ""
+msgstr "ã™ã¹ã¦ã®ãƒ–ランãƒã‚’表示"
msgid "Branches|Show more active branches"
-msgstr ""
+msgstr "アクティブブランãƒã‚’ã•らã«è¡¨ç¤º"
msgid "Branches|Show more stale branches"
msgstr ""
msgid "Branches|Show overview of the branches"
-msgstr ""
+msgstr "ブランãƒã®æ¦‚è¦ã‚’表示"
msgid "Branches|Show stale branches"
-msgstr ""
+msgstr "éŽåŽ»ã®ãƒ–ランãƒã‚’表示"
msgid "Branches|Sort by"
msgstr ""
@@ -646,38 +770,29 @@ msgstr ""
msgid "Branches|Stale branches"
msgstr ""
-msgid "Branches|The branch could not be updated automatically because it has diverged from its upstream counterpart."
-msgstr ""
-
msgid "Branches|The default branch cannot be deleted"
-msgstr ""
+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 ""
msgid "Branches|To confirm, type %{branch_name_confirmation}:"
-msgstr ""
-
-msgid "Branches|To discard the local changes and overwrite the branch with the upstream version, delete it here and choose 'Update Now' above."
-msgstr ""
+msgstr "確èªã®ãŸã‚ã€%{branch_name_confirmation} を入力ã—ã¦ãã ã•ã„"
msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
msgstr ""
-msgid "Branches|diverged from upstream"
-msgstr ""
-
msgid "Branches|merged"
-msgstr ""
+msgstr "マージ済ã¿"
msgid "Branches|project settings"
-msgstr ""
+msgstr "プロジェクト設定"
msgid "Branches|protected"
-msgstr ""
+msgstr "ä¿è­·"
msgid "Browse Directory"
msgstr "ディレクトリを表示"
@@ -691,40 +806,79 @@ msgstr "ファイルを表示"
msgid "Browse files"
msgstr "ファイルを表示"
-msgid "Business"
-msgstr ""
-
msgid "ByAuthor|by"
msgstr "作者"
msgid "CI / CD"
-msgstr ""
+msgstr "CI / CD"
-msgid "CI/CD"
+msgid "CI / CD Settings"
msgstr ""
msgid "CI/CD configuration"
msgstr ""
-msgid "CI/CD for external repo"
+msgid "CI/CD settings"
+msgstr "CI/CD 設定"
+
+msgid "CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery."
msgstr ""
-msgid "CICD|Jobs"
+msgid "CICD|Auto DevOps"
+msgstr "Auto DevOps"
+
+msgid "CICD|Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration."
msgstr ""
-msgid "Cancel"
-msgstr "キャンセル"
+msgid "CICD|Automatic deployment to staging, manual deployment to production"
+msgstr ""
-msgid "Cannot be merged automatically"
+msgid "CICD|Continuous deployment to production"
msgstr ""
-msgid "Cannot modify managed Kubernetes cluster"
+msgid "CICD|Deployment strategy"
+msgstr ""
+
+msgid "CICD|Deployment strategy needs a domain name to work correctly."
+msgstr ""
+
+msgid "CICD|Disable Auto DevOps"
+msgstr ""
+
+msgid "CICD|Enable Auto DevOps"
+msgstr ""
+
+msgid "CICD|Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific %{ci_file}."
msgstr ""
-msgid "Certificate fingerprint"
+msgid "CICD|Instance default (%{state})"
msgstr ""
-msgid "Change Weight"
+msgid "CICD|Jobs"
+msgstr "ジョブ"
+
+msgid "CICD|Learn more about Auto DevOps"
+msgstr ""
+
+msgid "CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project."
+msgstr ""
+
+msgid "CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages."
+msgstr ""
+
+msgid "Can't find HEAD commit for this branch"
+msgstr ""
+
+msgid "Cancel"
+msgstr "キャンセル"
+
+msgid "Cancel this job"
+msgstr "ã“ã®ã‚¸ãƒ§ãƒ–をキャンセルã™ã‚‹"
+
+msgid "Cannot be merged automatically"
+msgstr "自動的ã«ãƒžãƒ¼ã‚¸ã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“"
+
+msgid "Cannot modify managed Kubernetes cluster"
msgstr ""
msgid "Change this value to influence how frequently the GitLab UI polls for updates."
@@ -755,16 +909,16 @@ msgid "Charts"
msgstr "ãƒãƒ£ãƒ¼ãƒˆ"
msgid "Chat"
-msgstr ""
+msgstr "ãƒãƒ£ãƒƒãƒˆ"
msgid "Check interval"
-msgstr ""
+msgstr "ãƒã‚§ãƒƒã‚¯ã®é–“éš”"
msgid "Checking %{text} availability…"
-msgstr ""
+msgstr "%{text} ãŒåˆ©ç”¨å¯èƒ½ã‹ç¢ºèªã—ã¦ã„ã¾ã™â€¦"
msgid "Checking branch availability..."
-msgstr ""
+msgstr "ブランãƒãŒåˆ©ç”¨å¯èƒ½ã‹ç¢ºèªã—ã¦ã„ã¾ã™â€¦"
msgid "Cherry-pick this commit"
msgstr "ã“ã®ã‚³ãƒŸãƒƒãƒˆã‚’ãƒã‚§ãƒªãƒ¼ãƒ”ック"
@@ -773,25 +927,22 @@ msgid "Cherry-pick this merge request"
msgstr "ã“ã®ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’ãƒã‚§ãƒªãƒ¼ãƒ”ック"
msgid "Choose File ..."
-msgstr ""
+msgstr "ãƒ•ã‚¡ã‚¤ãƒ«ã‚’é¸æŠž..."
msgid "Choose a branch/tag (e.g. %{master}) or enter a commit (e.g. %{sha}) to see what's changed or to create a merge request."
msgstr ""
-msgid "Choose file..."
+msgid "Choose any color."
msgstr ""
-msgid "Choose which groups you wish to synchronize to this secondary node."
+msgid "Choose between <code>clone</code> or <code>fetch</code> to get the recent application code"
msgstr ""
-msgid "Choose which repositories you want to connect and run CI/CD pipelines."
-msgstr ""
+msgid "Choose file..."
+msgstr "ãƒ•ã‚¡ã‚¤ãƒ«ã‚’é¸æŠž..."
msgid "Choose which repositories you want to import."
-msgstr ""
-
-msgid "Choose which shards you wish to synchronize to this secondary node."
-msgstr ""
+msgstr "インãƒãƒ¼ãƒˆã—ãŸã„リãƒã‚¸ãƒˆãƒªã‚’é¸æŠžã—ã¦ãã ã•ã„。"
msgid "CiStatusLabel|canceled"
msgstr "キャンセル"
@@ -857,64 +1008,55 @@ msgid "CiVariables|Remove variable row"
msgstr ""
msgid "CiVariable|* (All environments)"
-msgstr ""
+msgstr "* (ã™ã¹ã¦ã®ç’°å¢ƒ)"
msgid "CiVariable|All environments"
-msgstr ""
-
-msgid "CiVariable|Create wildcard"
-msgstr ""
+msgstr "ã™ã¹ã¦ã®ç’°å¢ƒ"
msgid "CiVariable|Error occured while saving variables"
-msgstr ""
-
-msgid "CiVariable|New environment"
-msgstr ""
+msgstr "変数ä¿å­˜ä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸ"
msgid "CiVariable|Protected"
-msgstr ""
-
-msgid "CiVariable|Search environments"
-msgstr ""
+msgstr "ä¿è­·"
msgid "CiVariable|Toggle protected"
msgstr ""
msgid "CiVariable|Validation failed"
-msgstr ""
+msgstr "検証ã«å¤±æ•—ã—ã¾ã—ãŸ"
msgid "CircuitBreakerApiLink|circuitbreaker api"
msgstr ""
-msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
+msgid "Clear search input"
msgstr ""
-msgid "Click to expand text"
-msgstr ""
+msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
+msgstr "プロジェクトリストã§<strong>プロジェクトå</strong>をクリックã™ã‚‹ã¨ã€ãƒ—ロジェクトã®ãƒžã‚¤ãƒ«ã‚¹ãƒˆãƒ¼ãƒ³ã«ç§»å‹•ã—ã¾ã™ã€‚"
-msgid "Client authentication certificate"
+msgid "Click the <strong>Promote</strong> button in the top right corner to promote it to a group milestone."
msgstr ""
-msgid "Client authentication key"
-msgstr ""
+msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
+msgstr "下ã®ãƒœã‚¿ãƒ³ã‚’クリックã™ã‚‹ã¨ã€Kubernetesã®ãƒšãƒ¼ã‚¸ã«é·ç§»ã—ã€ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ãƒ—ロセスを開始ã—ã¾ã™"
-msgid "Client authentication key password"
+msgid "Click to expand it."
msgstr ""
+msgid "Click to expand text"
+msgstr "クリックã—ã¦ãƒ†ã‚­ã‚¹ãƒˆã‚’展開ã™ã‚‹"
+
msgid "Clone repository"
-msgstr ""
+msgstr "リãƒã‚¸ãƒˆãƒªã‚’クローン"
msgid "Close"
msgstr ""
-msgid "Closed"
-msgstr ""
-
msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster"
msgstr ""
msgid "ClusterIntegration|API URL"
-msgstr ""
+msgstr "API URL"
msgid "ClusterIntegration|Add Kubernetes cluster"
msgstr ""
@@ -925,9 +1067,15 @@ msgstr ""
msgid "ClusterIntegration|Advanced options on this Kubernetes cluster's integration"
msgstr ""
-msgid "ClusterIntegration|Applications"
+msgid "ClusterIntegration|An error occured while trying to fetch project zones: %{error}"
+msgstr ""
+
+msgid "ClusterIntegration|An error occured while trying to fetch your projects: %{error}"
msgstr ""
+msgid "ClusterIntegration|Applications"
+msgstr "アプリケーション"
+
msgid "ClusterIntegration|Are you sure you want to remove this Kubernetes cluster's integration? This will not delete your actual Kubernetes cluster."
msgstr ""
@@ -935,7 +1083,7 @@ msgid "ClusterIntegration|CA Certificate"
msgstr ""
msgid "ClusterIntegration|Certificate Authority bundle (PEM format)"
-msgstr ""
+msgstr "èªè¨¼å±€ãƒãƒ³ãƒ‰ãƒ« (PEMå½¢å¼)"
msgid "ClusterIntegration|Choose how to set up Kubernetes cluster integration"
msgstr ""
@@ -947,7 +1095,7 @@ msgid "ClusterIntegration|Control how your Kubernetes cluster integrates with Gi
msgstr ""
msgid "ClusterIntegration|Copy API URL"
-msgstr ""
+msgstr "API URLをコピー"
msgid "ClusterIntegration|Copy CA Certificate"
msgstr ""
@@ -955,11 +1103,14 @@ msgstr ""
msgid "ClusterIntegration|Copy Ingress IP Address to clipboard"
msgstr ""
+msgid "ClusterIntegration|Copy Jupyter Hostname to clipboard"
+msgstr ""
+
msgid "ClusterIntegration|Copy Kubernetes cluster name"
msgstr ""
msgid "ClusterIntegration|Copy Token"
-msgstr ""
+msgstr "トークンをコピー"
msgid "ClusterIntegration|Create Kubernetes cluster"
msgstr ""
@@ -970,7 +1121,7 @@ msgstr ""
msgid "ClusterIntegration|Create a new Kubernetes cluster on Google Kubernetes Engine right from GitLab"
msgstr ""
-msgid "ClusterIntegration|Create on GKE"
+msgid "ClusterIntegration|Create on Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
@@ -980,28 +1131,37 @@ msgid "ClusterIntegration|Enter the details for your Kubernetes cluster"
msgstr ""
msgid "ClusterIntegration|Environment scope"
+msgstr "環境スコープ"
+
+msgid "ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab's Google Kubernetes Engine Integration."
+msgstr ""
+
+msgid "ClusterIntegration|Fetching machine types"
+msgstr ""
+
+msgid "ClusterIntegration|Fetching projects"
+msgstr ""
+
+msgid "ClusterIntegration|Fetching zones"
msgstr ""
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
msgid "ClusterIntegration|GitLab Runner"
-msgstr ""
+msgstr "GitLab Runner"
-msgid "ClusterIntegration|Google Cloud Platform project ID"
+msgid "ClusterIntegration|Google Cloud Platform project"
msgstr ""
msgid "ClusterIntegration|Google Kubernetes Engine"
-msgstr ""
+msgstr "Google Kubernetes Engine"
msgid "ClusterIntegration|Google Kubernetes Engine project"
msgstr ""
msgid "ClusterIntegration|Helm Tiller"
-msgstr ""
-
-msgid "ClusterIntegration|In order to show the health of the cluster, we'll need to provision your cluster with Prometheus to collect the required data."
-msgstr ""
+msgstr "Helm Tiller"
msgid "ClusterIntegration|Ingress"
msgstr ""
@@ -1010,16 +1170,13 @@ msgid "ClusterIntegration|Ingress IP Address"
msgstr ""
msgid "ClusterIntegration|Install"
-msgstr ""
-
-msgid "ClusterIntegration|Install Prometheus"
-msgstr ""
+msgstr "インストール"
msgid "ClusterIntegration|Installed"
-msgstr ""
+msgstr "インストール済ã¿"
msgid "ClusterIntegration|Installing"
-msgstr ""
+msgstr "インストール中"
msgid "ClusterIntegration|Integrate Kubernetes cluster automation"
msgstr ""
@@ -1027,13 +1184,16 @@ msgstr ""
msgid "ClusterIntegration|Integration status"
msgstr ""
-msgid "ClusterIntegration|Kubernetes cluster"
+msgid "ClusterIntegration|Jupyter Hostname"
msgstr ""
-msgid "ClusterIntegration|Kubernetes cluster details"
+msgid "ClusterIntegration|JupyterHub"
msgstr ""
-msgid "ClusterIntegration|Kubernetes cluster health"
+msgid "ClusterIntegration|Kubernetes cluster"
+msgstr "Kubernetes クラスター"
+
+msgid "ClusterIntegration|Kubernetes cluster details"
msgstr ""
msgid "ClusterIntegration|Kubernetes cluster integration"
@@ -1063,7 +1223,13 @@ msgstr ""
msgid "ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project"
msgstr ""
-msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
+msgid "ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}."
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{help_link_start}Kubernetes%{help_link_end}."
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}."
msgstr ""
msgid "ClusterIntegration|Learn more about environments"
@@ -1079,80 +1245,107 @@ msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to crea
msgstr ""
msgid "ClusterIntegration|Manage"
-msgstr ""
+msgstr "管ç†"
msgid "ClusterIntegration|Manage your Kubernetes cluster by visiting %{link_gke}"
msgstr ""
msgid "ClusterIntegration|More information"
+msgstr "詳細情報"
+
+msgid "ClusterIntegration|No machine types matched your search"
+msgstr ""
+
+msgid "ClusterIntegration|No projects found"
+msgstr ""
+
+msgid "ClusterIntegration|No projects matched your search"
msgstr ""
-msgid "ClusterIntegration|Multiple Kubernetes clusters are available in GitLab Enterprise Edition Premium and Ultimate"
+msgid "ClusterIntegration|No zones matched your search"
msgstr ""
msgid "ClusterIntegration|Note:"
msgstr ""
msgid "ClusterIntegration|Number of nodes"
-msgstr ""
+msgstr "ノード数"
msgid "ClusterIntegration|Please enter access information for your Kubernetes cluster. If you need help, you can read our %{link_to_help_page} on Kubernetes"
msgstr ""
msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
-msgstr ""
-
-msgid "ClusterIntegration|Project ID"
-msgstr ""
+msgstr "Google ã‚¢ã‚«ã‚¦ãƒ³ãƒˆãŒæ¬¡ã®è¦ä»¶ã‚’満ãŸã—ã¦ã„ã‚‹ã“ã¨ã‚’確èªã—ã¦ãã ã•ã„。"
msgid "ClusterIntegration|Project namespace"
-msgstr ""
+msgstr "プロジェクトã®åå‰ç©ºé–“"
msgid "ClusterIntegration|Project namespace (optional, unique)"
-msgstr ""
+msgstr "プロジェクトã®åå‰ç©ºé–“ (çœç•¥å¯èƒ½ã€ä¸€æ„)"
msgid "ClusterIntegration|Prometheus"
-msgstr ""
+msgstr "Prometheus"
msgid "ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration."
msgstr ""
+msgid "ClusterIntegration|Redeem up to $500 in free credit for Google Cloud Platform"
+msgstr ""
+
msgid "ClusterIntegration|Remove Kubernetes cluster integration"
msgstr ""
msgid "ClusterIntegration|Remove integration"
-msgstr ""
+msgstr "çµ±åˆã‚’削除"
msgid "ClusterIntegration|Remove this Kubernetes cluster's configuration from this project. This will not delete your actual Kubernetes cluster."
msgstr ""
msgid "ClusterIntegration|Request to begin installing failed"
-msgstr ""
+msgstr "インストール開始ã«å¤±æ•—ã—ã¾ã—ãŸ"
msgid "ClusterIntegration|Save changes"
+msgstr "変更をä¿å­˜"
+
+msgid "ClusterIntegration|Search machine types"
msgstr ""
-msgid "ClusterIntegration|Security"
+msgid "ClusterIntegration|Search projects"
+msgstr ""
+
+msgid "ClusterIntegration|Search zones"
msgstr ""
+msgid "ClusterIntegration|Security"
+msgstr "セキュリティ"
+
msgid "ClusterIntegration|See and edit the details for your Kubernetes cluster"
msgstr ""
-msgid "ClusterIntegration|See machine types"
+msgid "ClusterIntegration|Select machine type"
msgstr ""
-msgid "ClusterIntegration|See your projects"
+msgid "ClusterIntegration|Select project"
msgstr ""
-msgid "ClusterIntegration|See zones"
+msgid "ClusterIntegration|Select project and zone to choose machine type"
msgstr ""
-msgid "ClusterIntegration|Service token"
+msgid "ClusterIntegration|Select project to choose zone"
msgstr ""
-msgid "ClusterIntegration|Show"
+msgid "ClusterIntegration|Select zone"
+msgstr ""
+
+msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Service token"
+msgstr "サービストークン"
+
+msgid "ClusterIntegration|Show"
+msgstr "表示"
+
msgid "ClusterIntegration|Something went wrong on our end."
msgstr ""
@@ -1175,60 +1368,69 @@ msgid "ClusterIntegration|Toggle Kubernetes cluster"
msgstr ""
msgid "ClusterIntegration|Token"
+msgstr "トークン"
+
+msgid "ClusterIntegration|Validating project billing status"
msgstr ""
msgid "ClusterIntegration|With a Kubernetes 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_kubernetes_engine}"
-msgstr ""
+msgstr "アカウント㫠%{link_to_kubernetes_engine} ãŒå¿…è¦ã§ã™ã€‚"
msgid "ClusterIntegration|Zone"
-msgstr ""
+msgstr "ゾーン"
msgid "ClusterIntegration|access to Google Kubernetes Engine"
-msgstr ""
+msgstr "Google Kubernetes Engineã«ã‚¢ã‚¯ã‚»ã‚¹"
msgid "ClusterIntegration|check the pricing here"
-msgstr ""
+msgstr "価格を確èªã™ã‚‹"
msgid "ClusterIntegration|documentation"
-msgstr ""
+msgstr "ドキュメント"
msgid "ClusterIntegration|help page"
-msgstr ""
+msgstr "ヘルプ ページ"
msgid "ClusterIntegration|installing applications"
msgstr ""
msgid "ClusterIntegration|meets the requirements"
-msgstr ""
+msgstr "å¿…è¦æ¡ä»¶"
msgid "ClusterIntegration|properly configured"
-msgstr ""
+msgstr "æ­£ã—ã設定ã•れã¦ã„ã‚‹"
+
+msgid "ClusterIntegration|sign up"
+msgstr "æ–°è¦ç™»éŒ²"
msgid "Collapse"
msgstr ""
-msgid "Comment and resolve discussion"
+msgid "Collapse sidebar"
msgstr ""
-msgid "Comment and unresolve discussion"
+msgid "Comment & resolve discussion"
msgstr ""
-msgid "Comments"
+msgid "Comment & unresolve discussion"
msgstr ""
+msgid "Comments"
+msgstr "コメント"
+
msgid "Commit"
msgid_plural "Commits"
msgstr[0] "コミット"
msgid "Commit (%{commit_count})"
msgid_plural "Commits (%{commit_count})"
-msgstr[0] ""
+msgstr[0] "コミット (%{commit_count})"
msgid "Commit Message"
-msgstr ""
+msgstr "コミットメッセージ"
msgid "Commit duration in minutes for last 30 commits"
msgstr "ç›´è¿‘30ã‚³ãƒŸãƒƒãƒˆã®æ‰€è¦æ™‚é–“(分)"
@@ -1240,7 +1442,7 @@ msgid "Commit statistics for %{ref} %{start_time} - %{end_time}"
msgstr ""
msgid "Commit to %{branchName} branch"
-msgstr ""
+msgstr "%{branchName} ブランãƒã«ã‚³ãƒŸãƒƒãƒˆ"
msgid "CommitBoxTitle|Commit"
msgstr "コミット"
@@ -1264,7 +1466,7 @@ msgid "Commits per weekday"
msgstr ""
msgid "Commits|An error occurred while fetching merge requests data."
-msgstr ""
+msgstr "マージリクエストデータã®å–得中ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚"
msgid "Commits|Commit: %{commitText}"
msgstr ""
@@ -1273,19 +1475,22 @@ msgid "Commits|History"
msgstr "履歴"
msgid "Commits|No related merge requests found"
-msgstr ""
+msgstr "関連ã™ã‚‹ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ã§ã—ãŸ"
msgid "Committed by"
msgstr "コミット担当者: "
+msgid "Commit…"
+msgstr ""
+
msgid "Compare"
msgstr "比較"
msgid "Compare Git revisions"
-msgstr ""
+msgstr "Git リビジョンを比較"
msgid "Compare Revisions"
-msgstr ""
+msgstr "リビジョンを比較"
msgid "Compare changes with the last commit"
msgstr ""
@@ -1294,19 +1499,19 @@ msgid "Compare changes with the merge request target branch"
msgstr ""
msgid "CompareBranches|%{source_branch} and %{target_branch} are the same."
-msgstr ""
+msgstr "%{source_branch} 㨠%{target_branch} ã¯ä¸€è‡´ã—ã¾ã™ã€‚"
msgid "CompareBranches|Compare"
-msgstr ""
+msgstr "比較"
msgid "CompareBranches|Source"
-msgstr ""
+msgstr "比較元"
msgid "CompareBranches|Target"
-msgstr ""
+msgstr "比較先"
msgid "CompareBranches|There isn't anything to compare."
-msgstr ""
+msgstr "比較ã™ã‚‹ã‚‚ã®ã¯ã‚りã¾ã›ã‚“。"
msgid "Confidential"
msgstr ""
@@ -1326,6 +1531,9 @@ msgstr ""
msgid "Configure limits for web and API requests."
msgstr ""
+msgid "Configure push mirrors."
+msgstr ""
+
msgid "Configure storage path and circuit breaker settings."
msgstr ""
@@ -1333,20 +1541,11 @@ msgid "Configure the way a user creates a new account."
msgstr ""
msgid "Connect"
-msgstr ""
-
-msgid "Connect all repositories"
-msgstr ""
+msgstr "接続"
msgid "Connect repositories from GitHub"
msgstr ""
-msgid "Connect your external repositories, and CI/CD pipelines will run for new commits. A GitLab project will be created with only CI/CD features enabled."
-msgstr ""
-
-msgid "Connecting..."
-msgstr ""
-
msgid "Container Registry"
msgstr ""
@@ -1360,10 +1559,10 @@ msgid "ContainerRegistry|GitLab supports up to 3 levels of image names. The foll
msgstr ""
msgid "ContainerRegistry|How to use the Container Registry"
-msgstr ""
+msgstr "コンテナ レジストリ使用方法"
msgid "ContainerRegistry|Learn more about"
-msgstr ""
+msgstr "詳細ã«ã¤ã„ã¦"
msgid "ContainerRegistry|No tags in Container Registry for this container image."
msgstr ""
@@ -1372,19 +1571,19 @@ msgid "ContainerRegistry|Once you log in, you&rsquo;re free to create and upload
msgstr ""
msgid "ContainerRegistry|Remove repository"
-msgstr ""
+msgstr "リãƒã‚¸ãƒˆãƒªã®å‰Šé™¤"
msgid "ContainerRegistry|Remove tag"
-msgstr ""
+msgstr "ã‚¿ã‚°ã®å‰Šé™¤"
msgid "ContainerRegistry|Size"
-msgstr ""
+msgstr "サイズ"
msgid "ContainerRegistry|Tag"
-msgstr ""
+msgstr "ã‚¿ã‚°"
msgid "ContainerRegistry|Tag ID"
-msgstr ""
+msgstr "ã‚¿ã‚°ID"
msgid "ContainerRegistry|Use different image names"
msgstr ""
@@ -1392,9 +1591,18 @@ 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 "ContainerRegistry|You can also use a %{deploy_token} for read-only access to the registry images."
+msgstr ""
+
+msgid "Continue"
+msgstr ""
+
msgid "Continuous Integration and Deployment"
msgstr ""
+msgid "Contribute to GitLab"
+msgstr ""
+
msgid "Contribution"
msgstr ""
@@ -1405,7 +1613,7 @@ msgid "Contributors"
msgstr "貢献者"
msgid "ContributorsPage|%{startDate} – %{endDate}"
-msgstr ""
+msgstr "%{startDate} – %{endDate}"
msgid "ContributorsPage|Building repository graph."
msgstr ""
@@ -1416,62 +1624,62 @@ msgstr ""
msgid "ContributorsPage|Please wait a moment, this page will automatically refresh when ready."
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 ""
-
msgid "Copy URL to clipboard"
msgstr "クリップボードã«URLをコピー"
msgid "Copy branch name to clipboard"
-msgstr ""
+msgstr "ブランãƒåをクリップボードã«ã‚³ãƒ”ー"
msgid "Copy command to clipboard"
-msgstr ""
+msgstr "コマンドをクリップボードã«ã‚³ãƒ”ー"
msgid "Copy commit SHA to clipboard"
msgstr "コミットã®SHAをクリップボードã«ã‚³ãƒ”ー"
+msgid "Copy file name to clipboard"
+msgstr ""
+
+msgid "Copy file path to clipboard"
+msgstr ""
+
msgid "Copy reference to clipboard"
msgstr ""
-msgid "Create"
+msgid "Copy to clipboard"
msgstr ""
+msgid "Create"
+msgstr "作æˆ"
+
msgid "Create New Directory"
msgstr "æ–°è¦ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒªã‚’作æˆ"
msgid "Create a new branch"
-msgstr ""
+msgstr "æ–°ã—ã„ブランãƒã‚’作æˆ"
msgid "Create a new branch and merge request"
-msgstr ""
+msgstr "ブランãƒã®æ–°è¦ä½œæˆã¨ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆ"
msgid "Create a personal access token on your account to pull or push via %{protocol}."
msgstr "%{protocol} ã§ãƒ—ッシュやプルã™ã‚‹ãŸã‚ã®ã‚ãªãŸå€‹äººç”¨ã‚¢ã‚¯ã‚»ã‚¹ãƒˆãƒ¼ã‚¯ãƒ³ã‚’作æˆ"
msgid "Create branch"
+msgstr "ブランãƒä½œæˆ"
+
+msgid "Create commit"
msgstr ""
msgid "Create directory"
msgstr "ディレクトリを作æˆ"
msgid "Create empty repository"
-msgstr ""
-
-msgid "Create epic"
-msgstr ""
+msgstr "空ã®ãƒªãƒã‚¸ãƒˆãƒªã‚’作æˆã™ã‚‹"
msgid "Create file"
-msgstr ""
+msgstr "ファイルを作æˆ"
msgid "Create group label"
-msgstr ""
+msgstr "グループラベルを作æˆ"
msgid "Create lists from labels. Issues with that label appear in that list."
msgstr ""
@@ -1480,25 +1688,25 @@ msgid "Create merge request"
msgstr "マージリクエストを作æˆ"
msgid "Create merge request and branch"
-msgstr ""
+msgstr "マージリクエストã¨ãƒ–ランãƒã‚’作æˆ"
msgid "Create new branch"
-msgstr ""
+msgstr "æ–°ã—ã„ブランãƒã‚’作æˆ"
msgid "Create new directory"
-msgstr ""
+msgstr "æ–°ã—ã„ディレクトリを作æˆ"
msgid "Create new file"
-msgstr ""
+msgstr "æ–°è¦ãƒ•ァイル作æˆ"
msgid "Create new label"
-msgstr ""
+msgstr "ãƒ©ãƒ™ãƒ«ã®æ–°è¦ä½œæˆ"
msgid "Create new..."
msgstr "æ–°è¦ä½œæˆ"
msgid "Create project label"
-msgstr ""
+msgstr "プロジェクトラベルを作æˆ"
msgid "CreateNewFork|Fork"
msgstr "フォーク"
@@ -1509,13 +1717,10 @@ msgstr "ã‚¿ã‚°"
msgid "CreateTokenToCloneLink|create a personal access token"
msgstr "個人用アクセストークンを作æˆ"
-msgid "Creates a new branch from %{branchName}"
-msgstr ""
-
-msgid "Creates a new branch from %{branchName} and re-directs to create a new merge request"
-msgstr ""
+msgid "Created"
+msgstr "ä½œæˆæ¸ˆã¿"
-msgid "Creating epic"
+msgid "Created by me"
msgstr ""
msgid "Cron Timezone"
@@ -1524,7 +1729,13 @@ msgstr "Cron ã®ã‚¿ã‚¤ãƒ ã‚¾ãƒ¼ãƒ³"
msgid "Cron syntax"
msgstr "Cron ã®æ§‹æ–‡"
-msgid "Current node"
+msgid "CurrentUser|Profile"
+msgstr ""
+
+msgid "CurrentUser|Settings"
+msgstr "設定"
+
+msgid "Custom CI config path"
msgstr ""
msgid "Custom notification events"
@@ -1533,9 +1744,6 @@ 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 "Customize colors"
-msgstr ""
-
msgid "Cycle Analytics"
msgstr "サイクル分æž"
@@ -1561,18 +1769,18 @@ msgid "CycleAnalyticsStage|Test"
msgstr "テスト"
msgid "DashboardProjects|All"
-msgstr ""
+msgstr "ã™ã¹ã¦"
msgid "DashboardProjects|Personal"
msgstr ""
msgid "Dec"
-msgstr ""
+msgstr "12月"
msgid "December"
-msgstr ""
+msgstr "12月"
-msgid "Default classification label"
+msgid "Decline and sign out"
msgstr ""
msgid "Define a custom pattern with cron syntax"
@@ -1581,51 +1789,180 @@ msgstr "Cron æ§‹æ–‡ã§ã‚«ã‚¹ã‚¿ãƒ ãªãƒ‘ターンを指定ã™ã‚‹"
msgid "Delete"
msgstr "削除"
+msgid "Delete list"
+msgstr ""
+
msgid "Deploy"
msgid_plural "Deploys"
msgstr[0] "デプロイ"
msgid "Deploy Keys"
+msgstr "デプロイキー"
+
+msgid "DeployKeys|+%{count} others"
msgstr ""
-msgid "Description"
-msgstr "説明"
+msgid "DeployKeys|Current project"
+msgstr "ç¾åœ¨ã®ãƒ—ロジェクト"
-msgid "Description templates allow you to define context-specific templates for issue and merge request description fields for your project."
+msgid "DeployKeys|Deploy key"
msgstr ""
-msgid "Details"
+msgid "DeployKeys|Enabled deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Error enabling deploy key"
+msgstr ""
+
+msgid "DeployKeys|Error getting deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Error removing deploy key"
+msgstr ""
+
+msgid "DeployKeys|Expand %{count} other projects"
+msgstr ""
+
+msgid "DeployKeys|Loading deploy keys"
+msgstr ""
+
+msgid "DeployKeys|No deploy keys found. Create one with the form above."
+msgstr ""
+
+msgid "DeployKeys|Privately accessible deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Project usage"
+msgstr ""
+
+msgid "DeployKeys|Publicly accessible deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Read access only"
+msgstr ""
+
+msgid "DeployKeys|Write access allowed"
+msgstr ""
+
+msgid "DeployKeys|You are going to remove this deploy key. Are you sure?"
msgstr ""
+msgid "DeployTokens|Active Deploy Tokens (%{active_tokens})"
+msgstr ""
+
+msgid "DeployTokens|Add a deploy token"
+msgstr ""
+
+msgid "DeployTokens|Allows read-only access to the registry images"
+msgstr ""
+
+msgid "DeployTokens|Allows read-only access to the repository"
+msgstr ""
+
+msgid "DeployTokens|Copy deploy token to clipboard"
+msgstr ""
+
+msgid "DeployTokens|Copy username to clipboard"
+msgstr "ユーザーåをクリップボードã«ã‚³ãƒ”ーã™ã‚‹"
+
+msgid "DeployTokens|Create deploy token"
+msgstr ""
+
+msgid "DeployTokens|Created"
+msgstr ""
+
+msgid "DeployTokens|Deploy Tokens"
+msgstr ""
+
+msgid "DeployTokens|Deploy tokens allow read-only access to your repository and registry images."
+msgstr ""
+
+msgid "DeployTokens|Expires"
+msgstr ""
+
+msgid "DeployTokens|Name"
+msgstr "åå‰"
+
+msgid "DeployTokens|Pick a name for the application, and we'll give you a unique deploy token."
+msgstr ""
+
+msgid "DeployTokens|Revoke"
+msgstr ""
+
+msgid "DeployTokens|Revoke %{name}"
+msgstr "%{name} ã‚’å–り消ã™"
+
+msgid "DeployTokens|Scopes"
+msgstr "スコープ"
+
+msgid "DeployTokens|This action cannot be undone."
+msgstr "ã“ã®æ“作ã¯å…ƒã«æˆ»ã›ã¾ã›ã‚“。"
+
+msgid "DeployTokens|This project has no active Deploy Tokens."
+msgstr ""
+
+msgid "DeployTokens|Use this token as a password. Make sure you save it - you won't be able to access it again."
+msgstr ""
+
+msgid "DeployTokens|Use this username as a login."
+msgstr "ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼åをログインã«ä½¿ç”¨ã—ã¾ã™ã€‚"
+
+msgid "DeployTokens|Username"
+msgstr "ユーザーå"
+
+msgid "DeployTokens|You are about to revoke"
+msgstr ""
+
+msgid "DeployTokens|Your New Deploy Token"
+msgstr ""
+
+msgid "DeployTokens|Your new project deploy token has been created."
+msgstr ""
+
+msgid "Deprioritize label"
+msgstr ""
+
+msgid "Description"
+msgstr "説明"
+
+msgid "Details"
+msgstr "詳細"
+
msgid "Diffs|No file name available"
+msgstr "使用å¯èƒ½ãªãƒ•ァイルåãŒã‚りã¾ã›ã‚“"
+
+msgid "Diffs|Something went wrong while fetching diff lines."
msgstr ""
msgid "Directory name"
msgstr "ディレクトリå"
msgid "Disable"
-msgstr ""
+msgstr "無効"
-msgid "Discard draft"
+msgid "Disable for this project"
msgstr ""
-msgid "Discover GitLab Geo."
+msgid "Disable group Runners"
msgstr ""
-msgid "Dismiss Cycle Analytics introduction box"
-msgstr ""
+msgid "Discard changes"
+msgstr "変更を破棄ã™ã‚‹"
+
+msgid "Discard draft"
+msgstr "下書ãを破棄"
-msgid "Dismiss Merge Request promotion"
+msgid "Dismiss Cycle Analytics introduction box"
msgstr ""
-msgid "Documentation for popular identity providers"
+msgid "Domain"
msgstr ""
msgid "Don't show again"
msgstr "次回ã‹ã‚‰è¡¨ç¤ºã—ãªã„"
msgid "Done"
-msgstr ""
+msgstr "完了"
msgid "Download"
msgstr "ダウンロード"
@@ -1658,44 +1995,44 @@ msgid "Downvotes"
msgstr ""
msgid "Due date"
-msgstr ""
+msgstr "期é™"
-msgid "During this process, you’ll be asked for URLs from GitLab’s side. Use the URLs shown below."
+msgid "Each Runner can be in one of the following states:"
msgstr ""
msgid "Edit"
msgstr "編集"
+msgid "Edit Label"
+msgstr ""
+
msgid "Edit Pipeline Schedule %{id}"
msgstr "パイプラインスケジュール %{id} を編集"
msgid "Edit files in the editor and commit changes here"
msgstr ""
-msgid "Editing"
-msgstr ""
-
-msgid "Elasticsearch"
-msgstr ""
-
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "Edit identity for %{user_name}"
msgstr ""
msgid "Email"
+msgstr "メール"
+
+msgid "Email patch"
msgstr ""
msgid "Emails"
+msgstr "メール"
+
+msgid "Embed"
msgstr ""
msgid "Enable"
-msgstr ""
+msgstr "有効化ã™ã‚‹"
msgid "Enable Auto DevOps"
msgstr ""
-msgid "Enable SAML authentication for this group"
-msgstr ""
-
msgid "Enable Sentry for error reporting and logging."
msgstr ""
@@ -1705,7 +2042,13 @@ msgstr ""
msgid "Enable and configure Prometheus metrics."
msgstr ""
-msgid "Enable classification control using an external service"
+msgid "Enable for this project"
+msgstr ""
+
+msgid "Enable group Runners"
+msgstr ""
+
+msgid "Enable or disable certain group features and choose access levels."
msgstr ""
msgid "Enable or disable version check and usage ping."
@@ -1717,94 +2060,94 @@ msgstr ""
msgid "Enable the Performance Bar for a given group."
msgstr ""
-msgid "Enabled"
+msgid "Ends at (UTC)"
msgstr ""
-msgid "Environments|An error occurred while fetching the environments."
+msgid "Environments"
msgstr ""
+msgid "Environments|An error occurred while fetching the environments."
+msgstr "環境をå–得中ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚"
+
msgid "Environments|An error occurred while making the request."
-msgstr ""
+msgstr "リクエスト作æˆä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚"
msgid "Environments|Commit"
-msgstr ""
+msgstr "コミット"
msgid "Environments|Deployment"
-msgstr ""
+msgstr "デプロイ"
msgid "Environments|Environment"
-msgstr ""
+msgstr "環境"
msgid "Environments|Environments"
-msgstr ""
+msgstr "環境一覧"
msgid "Environments|Job"
-msgstr ""
+msgstr "ジョブ"
msgid "Environments|New environment"
-msgstr ""
+msgstr "æ–°ã—ã„環境"
msgid "Environments|No deployments yet"
-msgstr ""
+msgstr "未デプロイ"
msgid "Environments|Open"
-msgstr ""
+msgstr "オープン"
msgid "Environments|Re-deploy"
-msgstr ""
+msgstr "å†ãƒ‡ãƒ—ロイ"
msgid "Environments|Read more about environments"
msgstr ""
msgid "Environments|Rollback"
-msgstr ""
+msgstr "ロールãƒãƒƒã‚¯"
msgid "Environments|Show all"
msgstr ""
msgid "Environments|Updated"
-msgstr ""
+msgstr "更新済ã¿"
msgid "Environments|You don't have any environments right now."
msgstr ""
-msgid "Epic will be removed! Are you sure?"
-msgstr ""
-
-msgid "Epics"
-msgstr ""
+msgid "Error Reporting and Logging"
+msgstr "エラー報告ã¨ãƒ­ã‚°"
-msgid "Epics Roadmap"
+msgid "Error committing changes. Please try again."
msgstr ""
-msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
+msgid "Error fetching contributors data."
msgstr ""
-msgid "Error Reporting and Logging"
+msgid "Error fetching job trace"
msgstr ""
-msgid "Error checking branch data. Please try again."
-msgstr ""
+msgid "Error fetching labels."
+msgstr "ラベルã®å–得中ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚"
-msgid "Error committing changes. Please try again."
+msgid "Error fetching network graph."
msgstr ""
-msgid "Error creating epic"
+msgid "Error fetching refs"
msgstr ""
-msgid "Error fetching contributors data."
+msgid "Error fetching usage ping data."
msgstr ""
-msgid "Error fetching labels."
+msgid "Error loading branch data. Please try again."
msgstr ""
-msgid "Error fetching network graph."
+msgid "Error loading last commit."
msgstr ""
-msgid "Error fetching refs"
+msgid "Error loading merge requests."
msgstr ""
-msgid "Error fetching usage ping data."
+msgid "Error loading project data. Please try again."
msgstr ""
msgid "Error occurred when toggling the notification subscription"
@@ -1819,23 +2162,26 @@ msgstr ""
msgid "Error updating todo status."
msgstr ""
-msgid "EventFilterBy|Filter by all"
+msgid "Estimated"
msgstr ""
+msgid "EventFilterBy|Filter by all"
+msgstr "ã™ã¹ã¦"
+
msgid "EventFilterBy|Filter by comments"
-msgstr ""
+msgstr "コメントã§ãƒ•ィルター"
msgid "EventFilterBy|Filter by issue events"
-msgstr ""
+msgstr "課題イベントã§ãƒ•ィルター"
msgid "EventFilterBy|Filter by merge events"
-msgstr ""
+msgstr "マージイベントã§ãƒ•ィルター"
msgid "EventFilterBy|Filter by push events"
-msgstr ""
+msgstr "プッシュイベントã§ãƒ•ィルター"
msgid "EventFilterBy|Filter by team"
-msgstr ""
+msgstr "ãƒãƒ¼ãƒ ã§ãƒ•ィルター"
msgid "Every day (at 4:00am)"
msgstr "毎日 (åˆå‰4:00)"
@@ -1847,44 +2193,32 @@ msgid "Every week (Sundays at 4:00am)"
msgstr "毎週 (日曜日ã®åˆå‰4:00)"
msgid "Expand"
-msgstr ""
-
-msgid "Explore projects"
-msgstr ""
-
-msgid "Explore public groups"
-msgstr ""
-
-msgid "External Classification Policy Authorization"
-msgstr ""
-
-msgid "External authentication"
-msgstr ""
+msgstr "展開"
-msgid "External authorization denied access to this project"
+msgid "Expand all"
msgstr ""
-msgid "External authorization request timeout"
+msgid "Expand sidebar"
msgstr ""
-msgid "ExternalAuthorizationService|Classification Label"
-msgstr ""
-
-msgid "ExternalAuthorizationService|Classification label"
+msgid "Explore projects"
msgstr ""
-msgid "ExternalAuthorizationService|When no classification label is set the default label `%{default_label}` will be used."
+msgid "Explore public groups"
msgstr ""
msgid "Failed"
-msgstr ""
+msgstr "失敗"
msgid "Failed Jobs"
-msgstr ""
+msgstr "失敗ã—ãŸã‚¸ãƒ§ãƒ–"
msgid "Failed to change the owner"
msgstr "オーナーを変更ã§ãã¾ã›ã‚“ã§ã—ãŸ"
+msgid "Failed to check related branches."
+msgstr ""
+
msgid "Failed to remove issue from board, please try again."
msgstr ""
@@ -1894,26 +2228,26 @@ msgstr "パイプラインスケジュールを削除ã§ãã¾ã›ã‚“ã§ã—ãŸ"
msgid "Failed to update issues, please try again."
msgstr ""
-msgid "Feb"
+msgid "Failure"
msgstr ""
-msgid "February"
+msgid "Faster as it re-uses the project workspace (falling back to clone if it doesn't exist)"
msgstr ""
-msgid "Fields on this page are now uneditable, you can configure"
-msgstr ""
+msgid "Feb"
+msgstr "2月"
-msgid "File name"
+msgid "February"
+msgstr "2月"
+
+msgid "Fields on this page are now uneditable, you can configure"
msgstr ""
msgid "Files"
msgstr "ファイル"
msgid "Files (%{human_size})"
-msgstr ""
-
-msgid "Fill in the fields below, turn on <strong>%{enable_label}</strong>, and press <strong>%{save_changes}</strong>"
-msgstr ""
+msgstr "ファイル (%{human_size})"
msgid "Filter by commit message"
msgstr "コミットメッセージã§çµžã‚Šè¾¼ã¿"
@@ -1925,7 +2259,7 @@ msgid "Find file"
msgstr "ファイルを検索"
msgid "Finished"
-msgstr ""
+msgstr "完了"
msgid "FirstPushedBy|First"
msgstr "åˆå›ž"
@@ -1933,10 +2267,13 @@ msgstr "åˆå›ž"
msgid "FirstPushedBy|pushed by"
msgstr "プッシュã—ãŸäºº"
-msgid "Font Color"
+msgid "For internal projects, any logged in user can view pipelines and access job details (output logs and artifacts)"
msgstr ""
-msgid "Footer message"
+msgid "For private projects, any member (guest or higher) can view pipelines and access job details (output logs and artifacts)"
+msgstr ""
+
+msgid "For public projects, anyone can view pipelines and access job details (output logs and artifacts)"
msgstr ""
msgid "Fork"
@@ -1947,16 +2284,19 @@ msgid "ForkedFromProjectPath|Forked from"
msgstr "フォーク元"
msgid "ForkedFromProjectPath|Forked from %{project_name} (deleted)"
-msgstr ""
+msgstr "%{project_name} ã‹ã‚‰ãƒ•ォーク (削除済)"
msgid "Forking in progress"
-msgstr ""
+msgstr "フォーク中ã§ã™"
msgid "Format"
+msgstr "フォーマット"
+
+msgid "Found errors in your .gitlab-ci.yml:"
msgstr ""
msgid "From %{provider_title}"
-msgstr ""
+msgstr "%{provider_title}ã‹ã‚‰"
msgid "From issue creation until deploy to production"
msgstr "課題ãŒç™»éŒ²ã•れã¦ã‹ã‚‰ãƒ—ロダクションã«ãƒ‡ãƒ—ロイã•れるã¾ã§"
@@ -1968,206 +2308,59 @@ msgid "From the Kubernetes cluster details view, install Runner from the applica
msgstr ""
msgid "GPG Keys"
-msgstr ""
-
-msgid "Generate a default set of labels"
-msgstr ""
-
-msgid "Geo Nodes"
-msgstr ""
-
-msgid "Geo allows you to replicate your GitLab instance to other geographical locations."
-msgstr ""
-
-msgid "GeoNodeSyncStatus|Node is failing or broken."
-msgstr ""
-
-msgid "GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage."
-msgstr ""
-
-msgid "GeoNodes|Checksummed"
-msgstr ""
-
-msgid "GeoNodes|Database replication lag:"
-msgstr ""
-
-msgid "GeoNodes|Disabling a node stops the sync process. Are you sure?"
-msgstr ""
-
-msgid "GeoNodes|Does not match the primary storage configuration"
-msgstr ""
-
-msgid "GeoNodes|Failed"
-msgstr ""
-
-msgid "GeoNodes|Full"
-msgstr ""
-
-msgid "GeoNodes|GitLab version does not match the primary node version"
-msgstr ""
-
-msgid "GeoNodes|GitLab version:"
-msgstr ""
-
-msgid "GeoNodes|Health status:"
-msgstr ""
-
-msgid "GeoNodes|Last event ID processed by cursor:"
-msgstr ""
-
-msgid "GeoNodes|Last event ID seen from primary:"
-msgstr ""
-
-msgid "GeoNodes|Loading nodes"
-msgstr ""
-
-msgid "GeoNodes|Local Attachments:"
-msgstr ""
-
-msgid "GeoNodes|Local LFS objects:"
-msgstr ""
-
-msgid "GeoNodes|Local job artifacts:"
-msgstr ""
-
-msgid "GeoNodes|New node"
-msgstr ""
-
-msgid "GeoNodes|Node Authentication was successfully repaired."
-msgstr ""
-
-msgid "GeoNodes|Node was successfully removed."
-msgstr ""
-
-msgid "GeoNodes|Not checksummed"
-msgstr ""
-
-msgid "GeoNodes|Out of sync"
-msgstr ""
-
-msgid "GeoNodes|Removing a node stops the sync process. Are you sure?"
-msgstr ""
-
-msgid "GeoNodes|Replication slot WAL:"
-msgstr ""
-
-msgid "GeoNodes|Replication slots:"
-msgstr ""
-
-msgid "GeoNodes|Repositories checksummed:"
-msgstr ""
-
-msgid "GeoNodes|Repositories:"
-msgstr ""
-
-msgid "GeoNodes|Repository checksums verified:"
-msgstr ""
-
-msgid "GeoNodes|Selective"
-msgstr ""
-
-msgid "GeoNodes|Something went wrong while changing node status"
-msgstr ""
-
-msgid "GeoNodes|Something went wrong while removing node"
-msgstr ""
-
-msgid "GeoNodes|Something went wrong while repairing node"
-msgstr ""
-
-msgid "GeoNodes|Storage config:"
-msgstr ""
-
-msgid "GeoNodes|Sync settings:"
-msgstr ""
-
-msgid "GeoNodes|Synced"
-msgstr ""
-
-msgid "GeoNodes|Unused slots"
-msgstr ""
+msgstr "GPG キー"
-msgid "GeoNodes|Unverified"
+msgid "General"
msgstr ""
-msgid "GeoNodes|Used slots"
+msgid "General pipelines"
msgstr ""
-msgid "GeoNodes|Verified"
-msgstr ""
-
-msgid "GeoNodes|Wiki checksums verified:"
-msgstr ""
-
-msgid "GeoNodes|Wikis checksummed:"
-msgstr ""
-
-msgid "GeoNodes|Wikis:"
-msgstr ""
-
-msgid "GeoNodes|You have configured Geo nodes using an insecure HTTP connection. We recommend the use of HTTPS."
-msgstr ""
-
-msgid "Geo|All projects"
-msgstr ""
-
-msgid "Geo|File sync capacity"
-msgstr ""
-
-msgid "Geo|Groups to synchronize"
-msgstr ""
-
-msgid "Geo|Projects in certain groups"
-msgstr ""
-
-msgid "Geo|Projects in certain storage shards"
-msgstr ""
-
-msgid "Geo|Repository sync capacity"
-msgstr ""
-
-msgid "Geo|Select groups to replicate."
-msgstr ""
-
-msgid "Geo|Shards to synchronize"
-msgstr ""
+msgid "Generate a default set of labels"
+msgstr "åˆæœŸè¨­å®šãƒ©ãƒ™ãƒ«ã‚»ãƒƒãƒˆã‚’生æˆã™ã‚‹"
msgid "Git repository URL"
-msgstr ""
+msgstr "Gitリãƒã‚¸ãƒˆãƒªURL"
msgid "Git revision"
-msgstr ""
+msgstr "Git リビジョン"
msgid "Git storage health information has been reset"
+msgstr "git ã‚¹ãƒˆãƒ¬ãƒ¼ã‚¸ã®æ­£å¸¸æ€§æƒ…å ±ãŒãƒªã‚»ãƒƒãƒˆã•れã¾ã—ãŸ"
+
+msgid "Git strategy for pipelines"
msgstr ""
msgid "Git version"
-msgstr ""
+msgstr "Git ãƒãƒ¼ã‚¸ãƒ§ãƒ³"
msgid "GitHub import"
-msgstr ""
+msgstr "GitHub インãƒãƒ¼ãƒˆ"
msgid "GitLab CI Linter has been moved"
msgstr ""
-msgid "GitLab Geo"
+msgid "GitLab Group Runners can execute code for all the projects in this group."
msgstr ""
msgid "GitLab Runner section"
-msgstr ""
-
-msgid "GitLab single sign on URL"
-msgstr ""
+msgstr "GitLab Runner セクション"
msgid "Gitaly"
-msgstr ""
+msgstr "Gitaly"
msgid "Gitaly Servers"
+msgstr "Gitaly サーãƒãƒ¼"
+
+msgid "Gitaly|Address"
msgstr ""
-msgid "Go back"
+msgid "Go Back"
msgstr ""
+msgid "Go back"
+msgstr "å‰ã«æˆ»ã‚‹"
+
msgid "Go to your fork"
msgstr "自分ã®ãƒ•ォークã¸ç§»å‹•"
@@ -2175,27 +2368,24 @@ msgid "GoToYourFork|Fork"
msgstr "フォーク"
msgid "Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service."
-msgstr ""
+msgstr "Googleèªè¨¼ã¯ %{link_to_documentation} ã§ã¯ã‚りã¾ã›ã‚“。ã“ã®ã‚µãƒ¼ãƒ“スã«ã¤ã„ã¦ã¯GitLab管ç†è€…ã«å•ã„åˆã‚ã›ã¦ãã ã•ã„。"
msgid "Got it!"
-msgstr ""
-
-msgid "GroupRoadmap|Epics let you manage your portfolio of projects more efficiently and with less effort"
-msgstr ""
+msgstr "入手ã—ã¾ã—ょã†!"
-msgid "GroupRoadmap|From %{dateWord}"
+msgid "Graph"
msgstr ""
-msgid "GroupRoadmap|Loading roadmap"
+msgid "Group CI/CD settings"
msgstr ""
-msgid "GroupRoadmap|Something went wrong while fetching epics"
-msgstr ""
+msgid "Group ID"
+msgstr "グループ ID"
-msgid "GroupRoadmap|To view the roadmap, add a planned start or finish date to one of your epics in this group or its subgroups. Only epics in the past 3 months and the next 3 months are shown &ndash; from %{startDate} to %{endDate}."
+msgid "Group Runners"
msgstr ""
-msgid "GroupRoadmap|Until %{dateWord}"
+msgid "Group maintainers can register group runners in the %{link}"
msgstr ""
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
@@ -2222,6 +2412,9 @@ msgstr ""
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
+msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
+msgstr ""
+
msgid "GroupsEmptyState|A group is a collection of several projects."
msgstr ""
@@ -2238,10 +2431,10 @@ msgid "GroupsTree|Create a project in this group."
msgstr ""
msgid "GroupsTree|Create a subgroup in this group."
-msgstr ""
+msgstr "ã“ã®ã‚°ãƒ«ãƒ¼ãƒ—ã«ã‚µãƒ–グループを作æˆã™ã‚‹"
msgid "GroupsTree|Edit group"
-msgstr ""
+msgstr "グループを編集"
msgid "GroupsTree|Failed to leave the group. Please make sure you are not the only owner."
msgstr ""
@@ -2261,100 +2454,123 @@ msgstr ""
msgid "GroupsTree|Sorry, no groups or projects matched your search"
msgstr ""
-msgid "Have your users email"
-msgstr ""
-
-msgid "Header message"
-msgstr ""
-
msgid "Health Check"
-msgstr ""
+msgstr "正常性ãƒã‚§ãƒƒã‚¯"
msgid "Health information can be retrieved from the following endpoints. More information is available"
-msgstr ""
+msgstr "æ­£å¸¸æ€§æƒ…å ±ã¯æ¬¡ã®ã‚¨ãƒ³ãƒ‰ãƒã‚¤ãƒ³ãƒˆã‹ã‚‰å–å¾—ã§ãã¾ã™ã€‚è©³ç´°ãªæƒ…å ±ã¯"
msgid "HealthCheck|Access token is"
-msgstr ""
+msgstr "アクセストークンã¯"
msgid "HealthCheck|Healthy"
-msgstr ""
+msgstr "正常"
msgid "HealthCheck|No Health Problems Detected"
-msgstr ""
+msgstr "正常性ã«å•題ã¯ã‚りã¾ã›ã‚“"
msgid "HealthCheck|Unhealthy"
-msgstr ""
+msgstr "異常"
msgid "Help"
-msgstr ""
+msgstr "ヘルプ"
msgid "Help page"
msgstr ""
msgid "Help page text and support page url."
-msgstr ""
+msgstr "ヘルプページテキストã¨ã‚µãƒãƒ¼ãƒˆãƒšãƒ¼ã‚¸URL。"
msgid "Hide value"
msgid_plural "Hide values"
msgstr[0] ""
-msgid "History"
+msgid "Hide whitespace changes"
msgstr ""
+msgid "History"
+msgstr "履歴"
+
msgid "Housekeeping successfully started"
msgstr "ãƒã‚¦ã‚¹ã‚­ãƒ¼ãƒ”ãƒ³ã‚°ã¯æ­£å¸¸ã«èµ·å‹•ã—ã¾ã—ãŸã€‚"
-msgid "Identity provider single sign on URL"
+msgid "I accept the %{terms_link}"
msgstr ""
-msgid "If enabled, access to projects will be validated on an external service using their classification label."
+msgid "I accept the|Terms of Service and Privacy Policy"
msgstr ""
-msgid "If using GitHub, you’ll see pipeline statuses on GitHub for your commits and pull requests. %{more_info_link}"
+msgid "ID"
msgstr ""
-msgid "If you already have files you can push them using the %{link_to_cli} below."
+msgid "IDE|Commit"
+msgstr "コミット"
+
+msgid "IDE|Edit"
+msgstr "編集"
+
+msgid "IDE|Go back"
+msgstr "戻る"
+
+msgid "IDE|Open in file view"
msgstr ""
-msgid "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>."
+msgid "IDE|Review"
+msgstr "レビュー"
+
+msgid "Identifier"
msgstr ""
-msgid "Import"
+msgid "Identities"
msgstr ""
-msgid "Import all repositories"
+msgid "If disabled, the access level will depend on the user's permissions in the project."
msgstr ""
-msgid "Import in progress"
+msgid "If enabled"
msgstr ""
-msgid "Import repositories from GitHub"
+msgid "If you already have files you can push them using the %{link_to_cli} below."
msgstr ""
-msgid "Import repository"
-msgstr "レãƒã‚¸ãƒˆãƒªãƒ¼ã‚’インãƒãƒ¼ãƒˆ"
+msgid "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>."
+msgstr ""
+
+msgid "ImageDiffViewer|2-up"
+msgstr ""
-msgid "ImportButtons|Connect repositories from"
+msgid "ImageDiffViewer|Onion skin"
msgstr ""
-msgid "Improve Issue boards with GitLab Enterprise Edition."
+msgid "ImageDiffViewer|Swipe"
+msgstr ""
+
+msgid "Import"
+msgstr "インãƒãƒ¼ãƒˆ"
+
+msgid "Import all repositories"
+msgstr "ã™ã¹ã¦ã®ãƒªãƒã‚¸ãƒˆãƒªã‚’インãƒãƒ¼ãƒˆ"
+
+msgid "Import in progress"
+msgstr "インãƒãƒ¼ãƒˆä¸­ã§ã™"
+
+msgid "Import repositories from GitHub"
+msgstr "リãƒã‚¸ãƒˆãƒªã‚’GitHubã‹ã‚‰ã‚¤ãƒ³ãƒãƒ¼ãƒˆã™ã‚‹"
+
+msgid "Import repository"
msgstr ""
-msgid "Improve issues management with Issue weight and GitLab Enterprise Edition."
+msgid "Include a Terms of Service agreement and Privacy Policy that all users must accept."
msgstr ""
-msgid "Improve search with Advanced Global Search and GitLab Enterprise Edition."
+msgid "Inline"
msgstr ""
msgid "Install Runner on Kubernetes"
msgstr ""
msgid "Install a Runner compatible with GitLab CI"
-msgstr ""
-
-msgid "Instance"
-msgid_plural "Instances"
-msgstr[0] ""
+msgstr "GitLab CI ã¨äº’æ›ã® Runner をインストール"
msgid "Instance does not support multiple Kubernetes clusters"
msgstr ""
@@ -2362,6 +2578,9 @@ msgstr ""
msgid "Integrations"
msgstr ""
+msgid "Integrations Settings"
+msgstr ""
+
msgid "Interested parties can even contribute by pushing commits if they want to."
msgstr ""
@@ -2377,53 +2596,56 @@ msgstr "é–“éš”ã®ãƒ‘ターン"
msgid "Introducing Cycle Analytics"
msgstr "サイクル分æžã®ã”紹介"
-msgid "Issue board focus mode"
+msgid "Issue Board"
msgstr ""
msgid "Issue events"
-msgstr ""
+msgstr "課題イベント"
msgid "IssueBoards|Board"
msgstr ""
-msgid "IssueBoards|Boards"
-msgstr ""
-
msgid "Issues"
-msgstr ""
+msgstr "課題"
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
msgid "Jan"
-msgstr ""
+msgstr "1月"
msgid "January"
+msgstr "1月"
+
+msgid "Job"
msgstr ""
+msgid "Job has been erased"
+msgstr "ã‚¸ãƒ§ãƒ–ãŒæ¶ˆåŽ»ã•れã¾ã—ãŸ"
+
msgid "Jobs"
-msgstr ""
+msgstr "ジョブ"
msgid "Jul"
-msgstr ""
+msgstr "7月"
msgid "July"
-msgstr ""
+msgstr "7月"
msgid "Jun"
-msgstr ""
+msgstr "6月"
msgid "June"
-msgstr ""
+msgstr "6月"
msgid "Koding"
-msgstr ""
+msgstr "Koding"
msgid "Kubernetes"
-msgstr ""
+msgstr "Kubernetes"
msgid "Kubernetes Cluster"
-msgstr ""
+msgstr "Kubernetes クラスター"
msgid "Kubernetes cluster creation time exceeds timeout; %{timeout}"
msgstr ""
@@ -2438,11 +2660,14 @@ msgid "Kubernetes cluster was successfully updated."
msgstr ""
msgid "Kubernetes configured"
-msgstr ""
+msgstr "Kubernetes æ§‹æˆæ¸ˆã¿"
msgid "Kubernetes service integration has been deprecated. %{deprecated_message_content} your Kubernetes clusters using the new <a href=\"%{url}\"/>Kubernetes Clusters</a> page"
msgstr ""
+msgid "LFS"
+msgstr ""
+
msgid "LFSStatus|Disabled"
msgstr "無効"
@@ -2450,6 +2675,9 @@ msgid "LFSStatus|Enabled"
msgstr "有効"
msgid "Label"
+msgstr "ラベル"
+
+msgid "Label actions dropdown"
msgstr ""
msgid "LabelSelect|%{firstLabelName} +%{remainingLabelCount} more"
@@ -2459,7 +2687,7 @@ msgid "LabelSelect|%{labelsString}, and %{remainingLabelCount} more"
msgstr ""
msgid "Labels"
-msgstr ""
+msgstr "ラベル"
msgid "Labels can be applied to %{features}. Group labels are available for any project within the group."
msgstr ""
@@ -2467,6 +2695,9 @@ msgstr ""
msgid "Labels can be applied to issues and merge requests to categorize them."
msgstr ""
+msgid "Labels can be applied to issues and merge requests."
+msgstr ""
+
msgid "Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>"
msgstr ""
@@ -2484,10 +2715,10 @@ msgid "Last commit"
msgstr "最新コミット"
msgid "Last edited %{date}"
-msgstr ""
+msgstr "最終編集日 %{date}"
msgid "Last edited by %{name}"
-msgstr ""
+msgstr "最終編集者 %{name}"
msgid "Last update"
msgstr ""
@@ -2496,9 +2727,12 @@ msgid "Last updated"
msgstr ""
msgid "LastPushEvent|You pushed to"
-msgstr ""
+msgstr "ã“ã“ã¸ãƒ—ッシュã—ã¾ã—ãŸ"
msgid "LastPushEvent|at"
+msgstr "ã“ã®æ™‚刻ã«"
+
+msgid "Latest changes"
msgstr ""
msgid "Learn more"
@@ -2517,7 +2751,7 @@ msgid "Learn more in the|pipeline schedules documentation"
msgstr "詳ã—ãã¯ãƒ‘イプラインスケジュールã®ãƒ‰ã‚­ãƒ¥ãƒ¡ãƒ³ãƒˆã‚’å‚ç…§"
msgid "Leave"
-msgstr ""
+msgstr "離れる"
msgid "Leave group"
msgstr "グループを離脱"
@@ -2525,17 +2759,17 @@ msgstr "グループを離脱"
msgid "Leave project"
msgstr "プロジェクトを離脱"
-msgid "License"
-msgstr ""
-
msgid "List"
-msgstr ""
+msgstr "リスト"
msgid "List your GitHub repositories"
msgstr ""
msgid "Loading the GitLab IDE..."
-msgstr ""
+msgstr "GitLab IDEã®èª­ã¿è¾¼ã¿ä¸­..."
+
+msgid "Loading..."
+msgstr "読ã¿è¾¼ã¿ä¸­..."
msgid "Lock"
msgstr ""
@@ -2546,20 +2780,17 @@ msgstr ""
msgid "Lock not found"
msgstr ""
-msgid "Locked"
+msgid "Lock to current projects"
msgstr ""
-msgid "Locked Files"
-msgstr ""
+msgid "Locked"
+msgstr "ロック中"
-msgid "Locks give the ability to lock specific file or folder."
+msgid "Locked to current projects"
msgstr ""
msgid "Login"
-msgstr ""
-
-msgid "Make everyone on your team more productive regardless of their location. GitLab Geo creates read-only mirrors of your GitLab instance so you can reduce the time it takes to clone and fetch large repos."
-msgstr ""
+msgstr "ログイン"
msgid "Manage all notifications"
msgstr ""
@@ -2573,137 +2804,92 @@ msgstr ""
msgid "Manage project labels"
msgstr ""
-msgid "Manage your group’s membership while adding another level of security with SAML."
-msgstr ""
-
msgid "Mar"
-msgstr ""
+msgstr "3月"
msgid "March"
+msgstr "3月"
+
+msgid "Mark todo as done"
msgstr ""
-msgid "Mark done"
+msgid "Markdown enabled"
msgstr ""
msgid "Maximum git storage failures"
msgstr ""
msgid "May"
-msgstr ""
+msgstr "5月"
msgid "Median"
msgstr "中央値"
msgid "Members"
-msgstr ""
+msgstr "メンãƒãƒ¼"
-msgid "Members will be forwarded here when signing in to your group. Get this from your identity provider, where it can also be called \"SSO Service Location\", \"SAML Token Issuance Endpoint\", or \"SAML 2.0/W-Federation URL\"."
-msgstr ""
+msgid "Merge Request:"
+msgstr "マージリクエスト:"
msgid "Merge Requests"
-msgstr ""
+msgstr "マージリクエスト"
msgid "Merge events"
-msgstr ""
+msgstr "マージイベント"
msgid "Merge request"
-msgstr ""
-
-msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
-msgstr ""
-
-msgid "Merged"
-msgstr ""
-
-msgid "Messages"
-msgstr ""
-
-msgid "Metrics - Influx"
-msgstr ""
-
-msgid "Metrics - Prometheus"
-msgstr ""
-
-msgid "Metrics|Business"
-msgstr ""
-
-msgid "Metrics|Create metric"
-msgstr ""
-
-msgid "Metrics|Edit metric"
-msgstr ""
-
-msgid "Metrics|For grouping similar metrics"
-msgstr ""
-
-msgid "Metrics|Label of the chart's vertical axis. Usually the type of the unit being charted. The horizontal axis (X-axis) always represents time."
-msgstr ""
-
-msgid "Metrics|Legend label (optional)"
-msgstr ""
-
-msgid "Metrics|Must be a valid PromQL query."
-msgstr ""
-
-msgid "Metrics|Name"
-msgstr ""
-
-msgid "Metrics|New metric"
-msgstr ""
-
-msgid "Metrics|Prometheus Query Documentation"
-msgstr ""
-
-msgid "Metrics|Query"
-msgstr ""
+msgstr "マージリクエスト"
-msgid "Metrics|Response"
-msgstr ""
+msgid "Merge requests"
+msgstr "マージリクエスト"
-msgid "Metrics|System"
+msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
msgstr ""
-msgid "Metrics|Type"
+msgid "MergeRequests|Resolve this discussion in a new issue"
msgstr ""
-msgid "Metrics|Unit label"
+msgid "MergeRequests|Saving the comment failed"
msgstr ""
-msgid "Metrics|Used as a title for the chart"
+msgid "MergeRequests|Toggle comments for this file"
msgstr ""
-msgid "Metrics|Used if the query returns a single series. If it returns multiple series, their legend labels will be picked up from the response."
+msgid "MergeRequests|Updating discussions failed"
msgstr ""
-msgid "Metrics|Y-axis label"
+msgid "MergeRequests|View file @ %{commitId}"
msgstr ""
-msgid "Metrics|e.g. HTTP requests"
+msgid "MergeRequests|View replaced file @ %{commitId}"
msgstr ""
-msgid "Metrics|e.g. Requests/second"
-msgstr ""
+msgid "Merged"
+msgstr "マージ済ã¿"
-msgid "Metrics|e.g. Throughput"
-msgstr ""
+msgid "Messages"
+msgstr "メッセージ"
-msgid "Metrics|e.g. rate(http_requests_total[5m])"
+msgid "Metrics - Influx"
msgstr ""
-msgid "Metrics|e.g. req/sec"
+msgid "Metrics - Prometheus"
msgstr ""
msgid "Milestone"
+msgstr "マイルストーン"
+
+msgid "Milestones"
msgstr ""
msgid "Milestones|Delete milestone"
-msgstr ""
+msgstr "マイルストーンã®å‰Šé™¤"
msgid "Milestones|Delete milestone %{milestoneTitle}?"
-msgstr ""
+msgstr "マイルストーン %{milestoneTitle} を削除ã—ã¾ã™ã‹?"
msgid "Milestones|Failed to delete milestone %{milestoneTitle}"
-msgstr ""
+msgstr "マイルストーン %{milestoneTitle} ã®å‰Šé™¤ã«å¤±æ•—ã—ã¾ã—ãŸ"
msgid "Milestones|Milestone %{milestoneTitle} was not found"
msgstr ""
@@ -2714,42 +2900,57 @@ msgstr ""
msgid "Milestones|Promote Milestone"
msgstr ""
-msgid "Milestones|This action cannot be reversed."
-msgstr ""
-
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "SSH éµã‚’追加"
msgid "Modal|Cancel"
-msgstr ""
+msgstr "キャンセル"
msgid "Modal|Close"
msgstr ""
msgid "Monitoring"
-msgstr ""
+msgstr "監視"
-msgid "More info"
+msgid "More actions"
msgstr ""
msgid "More information"
-msgstr ""
+msgstr "詳ã—ã„æƒ…å ±"
msgid "More information is available|here"
-msgstr ""
+msgstr "ã“ã“ã‚’å‚ç…§"
msgid "Move"
-msgstr ""
+msgstr "移動"
msgid "Move issue"
msgstr ""
-msgid "Multiple issue boards"
-msgstr ""
+msgid "Name"
+msgstr "åå‰"
msgid "Name new label"
msgstr ""
+msgid "Name your individual key via a title"
+msgstr ""
+
+msgid "Nav|Help"
+msgstr ""
+
+msgid "Nav|Home"
+msgstr ""
+
+msgid "Nav|Sign In / Register"
+msgstr ""
+
+msgid "Nav|Sign out and sign in with a different account"
+msgstr ""
+
+msgid "New Identity"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "æ–°è¦èª²é¡Œ"
@@ -2760,6 +2961,9 @@ msgstr ""
msgid "New Kubernetes cluster"
msgstr ""
+msgid "New Label"
+msgstr ""
+
msgid "New Pipeline Schedule"
msgstr "æ–°è¦ãƒ‘イプラインスケジュール"
@@ -2767,32 +2971,35 @@ msgid "New branch"
msgstr "æ–°è¦ãƒ–ランãƒ"
msgid "New branch unavailable"
-msgstr ""
+msgstr "æ–°ã—ã„ブランãƒã¯åˆ©ç”¨ã§ãã¾ã›ã‚“"
msgid "New directory"
msgstr "æ–°è¦ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒª"
-msgid "New epic"
-msgstr ""
-
msgid "New file"
msgstr "æ–°è¦ãƒ•ァイル"
msgid "New group"
+msgstr "æ–°è¦ã‚°ãƒ«ãƒ¼ãƒ—"
+
+msgid "New identity"
msgstr ""
msgid "New issue"
msgstr "æ–°è¦èª²é¡Œ"
msgid "New label"
-msgstr ""
+msgstr "æ–°ã—ã„ラベル"
msgid "New merge request"
msgstr "æ–°è¦ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆ"
-msgid "New project"
+msgid "New pipelines will cancel older, pending pipelines on the same branch"
msgstr ""
+msgid "New project"
+msgstr "æ–°è¦ãƒ—ロジェクト"
+
msgid "New schedule"
msgstr "æ–°è¦ã‚¹ã‚±ã‚¸ãƒ¥ãƒ¼ãƒ«"
@@ -2800,19 +3007,19 @@ msgid "New snippet"
msgstr "æ–°è¦ã‚¹ãƒ‹ãƒšãƒƒãƒˆ"
msgid "New subgroup"
-msgstr ""
+msgstr "æ–°è¦ã‚µãƒ–グループ"
msgid "New tag"
msgstr "æ–°è¦ã‚¿ã‚°"
-msgid "No Label"
+msgid "No"
msgstr ""
msgid "No assignee"
-msgstr ""
+msgstr "担当者ãªã—"
msgid "No changes"
-msgstr ""
+msgstr "変更ãªã—"
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -2824,19 +3031,28 @@ msgid "No estimate or time spent"
msgstr ""
msgid "No file chosen"
+msgstr "ファイルãŒé¸æŠžã•れã¦ã„ã¾ã›ã‚“"
+
+msgid "No files found"
+msgstr ""
+
+msgid "No files found."
+msgstr "ファイルãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“。"
+
+msgid "No merge requests found"
msgstr ""
-msgid "No labels created yet."
+msgid "No messages were logged"
msgstr ""
msgid "No repository"
-msgstr "レãƒã‚¸ãƒˆãƒªãƒ¼ã¯ã‚りã¾ã›ã‚“"
+msgstr "リãƒã‚¸ãƒˆãƒªãŒã‚りã¾ã›ã‚“"
msgid "No schedules"
msgstr "スケジュールãªã—"
msgid "None"
-msgstr ""
+msgstr "ãªã—"
msgid "Not allowed to merge"
msgstr ""
@@ -2859,15 +3075,9 @@ msgstr "データä¸è¶³"
msgid "Note that the master branch is automatically protected. %{link_to_protected_branches}"
msgstr ""
-msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow connecting repositories without generating a Personal Access Token."
-msgstr ""
-
msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
msgstr ""
-msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow connecting repositories without generating a Personal Access Token."
-msgstr ""
-
msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
msgstr ""
@@ -2926,7 +3136,7 @@ msgid "NotificationLevel|Watch"
msgstr "ã™ã¹ã¦é€šçŸ¥"
msgid "Notifications"
-msgstr ""
+msgstr "通知"
msgid "Notifications off"
msgstr ""
@@ -2935,50 +3145,53 @@ msgid "Notifications on"
msgstr ""
msgid "Nov"
-msgstr ""
+msgstr "11月"
msgid "November"
-msgstr ""
+msgstr "11月"
msgid "Number of access attempts"
msgstr ""
-msgid "OK"
-msgstr ""
-
msgid "Oct"
-msgstr ""
+msgstr "10月"
msgid "October"
-msgstr ""
+msgstr "10月"
msgid "OfSearchInADropdown|Filter"
msgstr "フィルター"
-msgid "Once imported, repositories can be mirrored over SSH. Read more %{ssh_link}"
-msgstr ""
-
msgid "Online IDE integration settings."
msgstr ""
-msgid "Only project members can comment."
+msgid "Only comments from the following commit are shown below"
msgstr ""
-msgid "Open"
-msgstr ""
+msgid "Only project members can comment."
+msgstr "プロジェクトメンãƒãƒ¼ã®ã¿ã‚³ãƒ¡ãƒ³ãƒˆã§ãã¾ã™"
-msgid "Opened"
+msgid "Open in Xcode"
msgstr ""
msgid "OpenedNDaysAgo|Opened"
msgstr "オープンã•れãŸã®ã¯"
msgid "Opens in a new window"
+msgstr "æ–°è¦ã‚¦ã‚£ãƒ³ãƒ‰ã‚¦ã§é–‹ã"
+
+msgid "Operations"
msgstr ""
msgid "Options"
msgstr "オプション"
+msgid "Or you can choose one of the suggested colors below"
+msgstr ""
+
+msgid "Other Labels"
+msgstr ""
+
msgid "Otherwise it is recommended you start with one of the options below."
msgstr ""
@@ -2986,13 +3199,13 @@ msgid "Outbound requests"
msgstr ""
msgid "Overview"
-msgstr ""
+msgstr "概è¦"
msgid "Owner"
msgstr "オーナー"
msgid "Pages"
-msgstr ""
+msgstr "Pages"
msgid "Pagination|Last »"
msgstr ""
@@ -3010,12 +3223,27 @@ msgid "Part of merge request changes"
msgstr ""
msgid "Password"
+msgstr "パスワード"
+
+msgid "Paste your public SSH key, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'. Don't use your private SSH key."
+msgstr ""
+
+msgid "Pause"
msgstr ""
msgid "Pending"
+msgstr "ä¿ç•™ä¸­"
+
+msgid "Per job. If a job passes this threshold, it will be marked as failed"
+msgstr ""
+
+msgid "Perform advanced options such as changing path, transferring, or removing the group."
msgstr ""
msgid "Performance optimization"
+msgstr "ãƒ‘ãƒ•ã‚©ãƒ¼ãƒžãƒ³ã‚¹ã®æœ€é©åŒ–"
+
+msgid "Permissions"
msgstr ""
msgid "Personal Access Token"
@@ -3033,7 +3261,7 @@ msgstr "パイプラインスケジュール"
msgid "Pipeline Schedules"
msgstr "パイプラインスケジュール"
-msgid "Pipeline quota"
+msgid "Pipeline triggers"
msgstr ""
msgid "PipelineCharts|Failed:"
@@ -3091,19 +3319,19 @@ msgid "Pipelines charts"
msgstr "パイプラインãƒãƒ£ãƒ¼ãƒˆ"
msgid "Pipelines for last month"
-msgstr ""
+msgstr "先月ã®ãƒ‘イプライン"
msgid "Pipelines for last week"
-msgstr ""
+msgstr "先週ã®ãƒ‘イプライン"
msgid "Pipelines for last year"
-msgstr ""
+msgstr "昨年ã®ãƒ‘イプライン"
msgid "Pipelines|Build with confidence"
msgstr ""
msgid "Pipelines|CI Lint"
-msgstr ""
+msgstr "CI Lint"
msgid "Pipelines|Clear Runner Caches"
msgstr ""
@@ -3118,7 +3346,7 @@ msgid "Pipelines|Project cache successfully reset."
msgstr ""
msgid "Pipelines|Run Pipeline"
-msgstr ""
+msgstr "パイプライン実行"
msgid "Pipelines|Something went wrong while cleaning runners cache."
msgstr ""
@@ -3132,19 +3360,31 @@ msgstr ""
msgid "Pipelines|This project is not currently set up to run pipelines."
msgstr ""
-msgid "Pipeline|Retry pipeline"
+msgid "Pipeline|Create for"
msgstr ""
-msgid "Pipeline|Retry pipeline #%{pipelineId}?"
+msgid "Pipeline|Create pipeline"
msgstr ""
-msgid "Pipeline|Stop pipeline"
+msgid "Pipeline|Existing branch name or tag"
+msgstr ""
+
+msgid "Pipeline|Run Pipeline"
+msgstr ""
+
+msgid "Pipeline|Search branches"
+msgstr ""
+
+msgid "Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default."
msgstr ""
+msgid "Pipeline|Stop pipeline"
+msgstr "パイプラインã®åœæ­¢"
+
msgid "Pipeline|Stop pipeline #%{pipelineId}?"
msgstr ""
-msgid "Pipeline|You’re about to retry pipeline %{pipelineId}."
+msgid "Pipeline|Variables"
msgstr ""
msgid "Pipeline|You’re about to stop pipeline %{pipelineId}."
@@ -3162,28 +3402,46 @@ msgstr "ステージã‚り"
msgid "Pipeline|with stages"
msgstr "ステージã‚り"
-msgid "PlantUML"
+msgid "Plain diff"
msgstr ""
+msgid "PlantUML"
+msgstr "PlantUML"
+
msgid "Play"
msgstr ""
-msgid "Please <a href=%{link_to_billing} target=\"_blank\" rel=\"noopener noreferrer\">enable billing for one of your projects to be able to create a Kubernetes cluster</a>, then try again."
+msgid "Please accept the Terms of Service before continuing."
msgstr ""
-msgid "Please solve the reCAPTCHA"
+msgid "Please select at least one filter to see results"
msgstr ""
-msgid "Please wait while we connect to your repository. Refresh at will."
+msgid "Please solve the reCAPTCHA"
+msgstr "reCAPTCHAを解決ã—ã¦ãã ã•ã„"
+
+msgid "Please try again"
msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
msgid "Preferences"
+msgstr "基本設定"
+
+msgid "Preferences|Navigation theme"
msgstr ""
-msgid "Primary"
+msgid "Prioritize"
+msgstr ""
+
+msgid "Prioritize label"
+msgstr ""
+
+msgid "Prioritized Labels"
+msgstr ""
+
+msgid "Prioritized label"
msgstr ""
msgid "Private - Project access must be granted explicitly to each user."
@@ -3201,29 +3459,47 @@ msgstr ""
msgid "Profiles|Account scheduled for removal."
msgstr ""
-msgid "Profiles|Delete Account"
+msgid "Profiles|Change username"
+msgstr "ユーザーåã®å¤‰æ›´"
+
+msgid "Profiles|Current path: %{path}"
msgstr ""
+msgid "Profiles|Delete Account"
+msgstr "アカウント削除"
+
msgid "Profiles|Delete account"
-msgstr ""
+msgstr "アカウント削除"
msgid "Profiles|Delete your account?"
-msgstr ""
+msgstr "ã‚ãªãŸã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‚’削除ã—ã¾ã™ã‹ï¼Ÿ"
msgid "Profiles|Deleting an account has the following effects:"
msgstr ""
msgid "Profiles|Invalid password"
-msgstr ""
+msgstr "ãƒ‘ã‚¹ãƒ¯ãƒ¼ãƒ‰ãŒæ­£ã—ãã‚りã¾ã›ã‚“"
msgid "Profiles|Invalid username"
-msgstr ""
+msgstr "ユーザーåãŒæ­£ã—ãã‚りã¾ã›ã‚“"
+
+msgid "Profiles|Path"
+msgstr "パス"
msgid "Profiles|Type your %{confirmationValue} to confirm:"
-msgstr ""
+msgstr "確èªã®ãŸã‚ %{confirmationValue} を入力ã—ã¦ãã ã•ã„:"
+
+msgid "Profiles|Update username"
+msgstr "ユーザーåã‚’æ›´æ–°"
+
+msgid "Profiles|Username change failed - %{message}"
+msgstr "ユーザーåã®å¤‰æ›´ã«å¤±æ•—ã—ã¾ã—㟠- %{message}"
+
+msgid "Profiles|Username successfully changed"
+msgstr "ユーザーåã¯æ­£å¸¸ã«å¤‰æ›´ã•れã¾ã—ãŸ"
msgid "Profiles|You don't have access to delete this user."
-msgstr ""
+msgstr "ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’削除ã™ã‚‹æ¨©é™ãŒã‚りã¾ã›ã‚“"
msgid "Profiles|You must transfer ownership or delete these groups before you can delete your account."
msgstr ""
@@ -3232,7 +3508,7 @@ msgid "Profiles|Your account is currently an owner in these groups:"
msgstr ""
msgid "Profiles|your account"
-msgstr ""
+msgstr "ã‚ãªãŸã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆ"
msgid "Profiling - Performance bar"
msgstr ""
@@ -3240,9 +3516,15 @@ msgstr ""
msgid "Programming languages used in this repository"
msgstr ""
-msgid "Project '%{project_name}' is in the process of being deleted."
+msgid "Progress"
msgstr ""
+msgid "Project"
+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}' プロジェクトã¯å‰Šé™¤å‡¦ç†å¾…ã¡ã§ã™ã€‚"
@@ -3252,17 +3534,20 @@ msgstr "'%{project_name}' ãƒ—ãƒ­ã‚¸ã‚§ã‚¯ãƒˆã¯æ­£å¸¸ã«ä½œæˆã•れã¾ã—ãŸã€‚
msgid "Project '%{project_name}' was successfully updated."
msgstr "'%{project_name}' ãƒ—ãƒ­ã‚¸ã‚§ã‚¯ãƒˆã¯æ­£å¸¸ã«æ›´æ–°ã•れã¾ã—ãŸã€‚"
+msgid "Project Badges"
+msgstr "プロジェクトãƒãƒƒã‚¸"
+
msgid "Project access must be granted explicitly to each user."
msgstr "ユーザーã”ã¨ã«ãƒ—ãƒ­ã‚¸ã‚§ã‚¯ãƒˆã‚¢ã‚¯ã‚»ã‚¹ã®æ¨©é™ã‚’指定ã—ãªã‘れã°ãªã‚Šã¾ã›ã‚“。"
msgid "Project avatar"
-msgstr ""
+msgstr "プロジェクトアãƒã‚¿ãƒ¼"
msgid "Project avatar in repository: %{link}"
-msgstr ""
+msgstr "リãƒã‚¸ãƒˆãƒªå†…ã®ãƒ—ロジェクトアãƒã‚¿ãƒ¼: %{link}"
msgid "Project details"
-msgstr ""
+msgstr "プロジェクトã®è©³ç´°"
msgid "Project export could not be deleted."
msgstr "プロジェクトã®ã‚¨ã‚¯ã‚¹ãƒãƒ¼ãƒˆã‚’削除ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚"
@@ -3277,31 +3562,7 @@ msgid "Project export started. A download link will be sent by email."
msgstr "プロジェクトã®ã‚¨ã‚¯ã‚¹ãƒãƒ¼ãƒˆã‚’é–‹å§‹ã—ã¾ã—ãŸã€‚ダウンロードã®ãƒªãƒ³ã‚¯ã¯ãƒ¡ãƒ¼ãƒ«ã§é€ä¿¡ã—ã¾ã™"
msgid "ProjectActivityRSS|Subscribe"
-msgstr ""
-
-msgid "ProjectCreationLevel|Allowed to create projects"
-msgstr ""
-
-msgid "ProjectCreationLevel|Default project creation protection"
-msgstr ""
-
-msgid "ProjectCreationLevel|Developers + Masters"
-msgstr ""
-
-msgid "ProjectCreationLevel|Masters"
-msgstr ""
-
-msgid "ProjectCreationLevel|No one"
-msgstr ""
-
-msgid "ProjectFeature|Disabled"
-msgstr "無効"
-
-msgid "ProjectFeature|Everyone with access"
-msgstr "アクセス権é™ã‚’æŒã£ã¦ã„る人"
-
-msgid "ProjectFeature|Only team members"
-msgstr "ãƒãƒ¼ãƒ ãƒ¡ãƒ³ãƒãƒ¼ã®ã¿"
+msgstr "講読"
msgid "ProjectFileTree|Name"
msgstr "åå‰"
@@ -3312,29 +3573,8 @@ msgstr "記録ãªã—"
msgid "ProjectLifecycle|Stage"
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 ""
+msgstr "プロジェクト"
msgid "ProjectsDropdown|Frequently visited"
msgstr ""
@@ -3346,7 +3586,7 @@ msgid "ProjectsDropdown|Projects you visit often will appear here"
msgstr ""
msgid "ProjectsDropdown|Search your projects"
-msgstr ""
+msgstr "プロジェクトを検索"
msgid "ProjectsDropdown|Something went wrong on our end."
msgstr ""
@@ -3357,6 +3597,9 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr ""
+msgid "PrometheusDashboard|Time"
+msgstr ""
+
msgid "PrometheusService|%{exporters} with %{metrics} were found"
msgstr ""
@@ -3378,18 +3621,9 @@ msgstr ""
msgid "PrometheusService|Common metrics"
msgstr ""
-msgid "PrometheusService|Common metrics are automatically monitored based on a library of metrics from popular exporters."
-msgstr ""
-
-msgid "PrometheusService|Custom metrics"
-msgstr ""
-
msgid "PrometheusService|Finding and configuring metrics..."
msgstr ""
-msgid "PrometheusService|Finding custom metrics..."
-msgstr ""
-
msgid "PrometheusService|Install Prometheus on clusters"
msgstr ""
@@ -3397,18 +3631,18 @@ msgid "PrometheusService|Manage clusters"
msgstr ""
msgid "PrometheusService|Manual configuration"
-msgstr ""
+msgstr "手動構æˆ"
msgid "PrometheusService|Metrics"
msgstr ""
-msgid "PrometheusService|Missing environment variable"
+msgid "PrometheusService|Metrics are automatically configured and monitored based on a library of metrics from popular exporters."
msgstr ""
-msgid "PrometheusService|More information"
+msgid "PrometheusService|Missing environment variable"
msgstr ""
-msgid "PrometheusService|New metric"
+msgid "PrometheusService|More information"
msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
@@ -3417,9 +3651,6 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
-msgstr ""
-
msgid "PrometheusService|Time-series monitoring service"
msgstr ""
@@ -3435,62 +3666,68 @@ msgstr ""
msgid "Promote"
msgstr ""
-msgid "Promote to Group Label"
+msgid "Promote these project milestones into a group milestone."
msgstr ""
msgid "Promote to Group Milestone"
msgstr ""
+msgid "Promote to group label"
+msgstr ""
+
msgid "Protip:"
msgstr ""
+msgid "Provider"
+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"
+msgid "Public pipelines"
msgstr ""
msgid "Push events"
-msgstr ""
+msgstr "プッシュイベント"
msgid "Push project from command line"
-msgstr ""
+msgstr "コマンドラインã‹ã‚‰ãƒ—ロジェクトをプッシュ"
msgid "Push to create a project"
msgstr ""
-msgid "PushRule|Committer restriction"
+msgid "Quick actions can be used in the issues description and comment boxes."
msgstr ""
-msgid "Quick actions can be used in the issues description and comment boxes."
+msgid "Re-deploy"
msgstr ""
msgid "Read more"
msgstr "ç¶šãを読む"
msgid "Readme"
-msgstr ""
+msgstr "Readme"
msgid "Real-time features"
msgstr ""
-msgid "RefSwitcher|Branches"
-msgstr "ブランãƒ"
+msgid "Reference:"
+msgstr "å‚ç…§:"
-msgid "RefSwitcher|Tags"
-msgstr "ã‚¿ã‚°"
+msgid "Register / Sign In"
+msgstr "登録 / サインイン"
-msgid "Reference:"
+msgid "Register and see your runners for this group."
msgstr ""
-msgid "Register / Sign In"
+msgid "Register and see your runners for this project."
msgstr ""
msgid "Registry"
-msgstr ""
+msgstr "レジストリ"
msgid "Related Commits"
msgstr "関連ã™ã‚‹ã‚³ãƒŸãƒƒãƒˆ"
@@ -3511,36 +3748,36 @@ msgid "Related Merged Requests"
msgstr "関連ã™ã‚‹ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆ"
msgid "Related merge requests"
-msgstr ""
+msgstr "関連ã™ã‚‹ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆ"
msgid "Remind later"
msgstr "後ã§é€šçŸ¥"
msgid "Remove"
+msgstr "削除"
+
+msgid "Remove Runner"
msgstr ""
msgid "Remove avatar"
+msgstr "ã‚¢ãƒã‚¿ãƒ¼ã‚’削除"
+
+msgid "Remove priority"
msgstr ""
msgid "Remove project"
msgstr "プロジェクトを削除"
-msgid "Repair authentication"
-msgstr ""
-
-msgid "Repo by URL"
-msgstr ""
-
msgid "Repository"
-msgstr ""
+msgstr "リãƒã‚¸ãƒˆãƒª"
-msgid "Repository has no locks."
+msgid "Repository Settings"
msgstr ""
msgid "Repository maintenance"
msgstr ""
-msgid "Repository mirror settings"
+msgid "Repository mirror"
msgstr ""
msgid "Repository storage"
@@ -3549,19 +3786,37 @@ msgstr ""
msgid "Request Access"
msgstr "アクセス権é™ã‚’リクエストã™ã‚‹"
-msgid "Reset git storage health information"
+msgid "Require all users to accept Terms of Service and Privacy Policy when they access GitLab."
msgstr ""
+msgid "Reset git storage health information"
+msgstr "git ã‚¹ãƒˆãƒ¬ãƒ¼ã‚¸ã®æ­£å¸¸æ€§æƒ…報をリセット"
+
msgid "Reset health check access token"
-msgstr ""
+msgstr "正常性ãƒã‚§ãƒƒã‚¯ã‚¢ã‚¯ã‚»ã‚¹ãƒˆãƒ¼ã‚¯ãƒ³ã‚’リセット"
msgid "Reset runners registration token"
+msgstr "Runner 登録トークンをリセット"
+
+msgid "Resolve all discussions in new issue"
+msgstr ""
+
+msgid "Resolve conflicts on source branch"
msgstr ""
msgid "Resolve discussion"
msgstr ""
-msgid "Response"
+msgid "Resume"
+msgstr ""
+
+msgid "Retry"
+msgstr ""
+
+msgid "Retry this job"
+msgstr ""
+
+msgid "Retry verification"
msgstr ""
msgid "Reveal value"
@@ -3574,7 +3829,7 @@ msgstr "ã“ã®ã‚³ãƒŸãƒƒãƒˆã‚’リãƒãƒ¼ãƒˆ"
msgid "Revert this merge request"
msgstr "ã“ã®ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’リãƒãƒ¼ãƒˆ"
-msgid "Review the process for configuring service providers in your identity provider — in this case, GitLab is the \"service provider\" or \"relying party\"."
+msgid "Review"
msgstr ""
msgid "Reviewing"
@@ -3583,84 +3838,102 @@ msgstr ""
msgid "Reviewing (merge request !%{mergeRequestId})"
msgstr ""
-msgid "Roadmap"
+msgid "Rollback"
msgstr ""
-msgid "Run CI/CD pipelines for external repositories"
+msgid "Runner token"
msgstr ""
msgid "Runners"
msgstr ""
-msgid "Running"
+msgid "Runners API"
msgstr ""
-msgid "SAML Single Sign On"
+msgid "Runners can be placed on separate users, servers, and even on your local machine."
msgstr ""
-msgid "SAML Single Sign On Settings"
+msgid "Running"
msgstr ""
-msgid "SHA1 fingerprint of the SAML token signing certificate. Get this from your identity provider, where it can also be called \"Thumbprint\"."
+msgid "SSH Keys"
+msgstr "SSH éµ"
+
+msgid "SSL Verification"
msgstr ""
-msgid "SSH Keys"
+msgid "Save"
msgstr ""
msgid "Save changes"
-msgstr ""
+msgstr "変更をä¿å­˜"
msgid "Save pipeline schedule"
msgstr "パイプラインスケジュールをä¿å­˜"
msgid "Save variables"
-msgstr ""
+msgstr "変数をä¿å­˜ã™ã‚‹"
msgid "Schedule a new pipeline"
msgstr "æ–°ã—ã„パイプラインã®ã‚¹ã‚±ã‚¸ãƒ¥ãƒ¼ãƒ«ã‚’作æˆ"
msgid "Scheduled"
-msgstr ""
+msgstr "スケジュール済"
msgid "Schedules"
-msgstr ""
+msgstr "スケジュール"
msgid "Scheduling Pipelines"
msgstr "パイプラインスケジューリング"
-msgid "Scoped issue boards"
+msgid "Scroll to bottom"
+msgstr ""
+
+msgid "Scroll to top"
msgstr ""
msgid "Search"
+msgstr "検索"
+
+msgid "Search branches"
msgstr ""
msgid "Search branches and tags"
msgstr "ブランãƒã¾ãŸã¯ã‚¿ã‚°ã‚’検索"
-msgid "Search milestones"
+msgid "Search files"
msgstr ""
-msgid "Search project"
+msgid "Search for projects, issues, etc."
msgstr ""
-msgid "Search users"
+msgid "Search merge requests"
msgstr ""
+msgid "Search milestones"
+msgstr "マイルストーンを検索"
+
+msgid "Search project"
+msgstr "プロジェクトを検索"
+
+msgid "Search users"
+msgstr "ユーザーを検索"
+
msgid "Seconds before reseting failure information"
msgstr ""
msgid "Seconds to wait for a storage access attempt"
msgstr ""
-msgid "Secret variables"
-msgstr ""
-
-msgid "Security report"
+msgid "Select"
msgstr ""
msgid "Select Archive Format"
msgstr "アーカイブã®ãƒ•ã‚©ãƒ¼ãƒžãƒƒãƒˆã‚’é¸æŠž"
+msgid "Select a namespace to fork the project"
+msgstr ""
+
msgid "Select a timezone"
msgstr "ã‚¿ã‚¤ãƒ ã‚¾ãƒ¼ãƒ³ã‚’é¸æŠž"
@@ -3668,34 +3941,40 @@ msgid "Select an existing Kubernetes cluster or create a new one"
msgstr ""
msgid "Select assignee"
-msgstr ""
+msgstr "æ‹…å½“è€…ã‚’é¸æŠž"
msgid "Select branch/tag"
+msgstr "ブランãƒãƒ»ã‚¿ã‚°é¸æŠž"
+
+msgid "Select project"
msgstr ""
-msgid "Select target branch"
-msgstr "ターゲットブランãƒã‚’é¸æŠž"
+msgid "Select project and zone to choose machine type"
+msgstr ""
-msgid "Selective synchronization"
+msgid "Select project to choose zone"
msgstr ""
-msgid "Send email"
+msgid "Select source branch"
msgstr ""
+msgid "Select target branch"
+msgstr "ターゲットブランãƒã‚’é¸æŠž"
+
+msgid "Send email"
+msgstr "メールをé€ä¿¡"
+
msgid "Sep"
-msgstr ""
+msgstr "9月"
msgid "September"
-msgstr ""
+msgstr "9月"
msgid "Server version"
-msgstr ""
+msgstr "サーãƒãƒ¼ã®ãƒãƒ¼ã‚¸ãƒ§ãƒ³"
msgid "Service Templates"
-msgstr ""
-
-msgid "Service URL"
-msgstr ""
+msgstr "サービス テンプレート"
msgid "Session expiration, projects limit and attachment size."
msgstr ""
@@ -3716,59 +3995,56 @@ msgid "Set requirements for a user to sign-in. Enable mandatory two-factor authe
msgstr ""
msgid "Set up CI/CD"
-msgstr ""
+msgstr "CI/CDを設定"
msgid "Set up Koding"
msgstr "Koding を設定"
-msgid "Set up assertions/attributes/claims (email, first_name, last_name) and NameID according to %{docsLinkStart}the documentation %{icon}%{docsLinkEnd}"
-msgstr ""
-
msgid "SetPasswordToCloneLink|set a password"
msgstr "パスワードを設定"
msgid "Settings"
-msgstr ""
+msgstr "設定"
msgid "Setup a specific Runner automatically"
msgstr ""
-msgid "Share the <strong>%{sso_label}</strong> with members so they can sign in to your group through your identity provider"
+msgid "Share"
msgstr ""
-msgid "SharedRunnersMinutesSettings|By resetting the pipeline minutes for this namespace, the currently used minutes will be set to zero."
+msgid "Shared Runners"
msgstr ""
-msgid "SharedRunnersMinutesSettings|Reset pipeline minutes"
+msgid "Show command"
+msgstr "コマンドを表示"
+
+msgid "Show complete raw log"
msgstr ""
-msgid "SharedRunnersMinutesSettings|Reset used pipeline minutes"
+msgid "Show latest version"
msgstr ""
-msgid "Show command"
+msgid "Show latest version of the diff"
msgstr ""
msgid "Show parent pages"
-msgstr ""
+msgstr "親ページを表示"
msgid "Show parent subgroups"
+msgstr "親ã®ã‚µãƒ–グループを表示"
+
+msgid "Show whitespace changes"
msgstr ""
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "%d ã®ã‚¤ãƒ™ãƒ³ãƒˆã‚’表示中"
-msgid "Sidebar|Change weight"
-msgstr ""
-
-msgid "Sidebar|No"
+msgid "Side-by-side"
msgstr ""
-msgid "Sidebar|None"
-msgstr ""
-
-msgid "Sidebar|Weight"
-msgstr ""
+msgid "Sign out"
+msgstr "サインアウト"
msgid "Sign-in restrictions"
msgstr ""
@@ -3779,11 +4055,11 @@ msgstr ""
msgid "Size and domain settings for static websites"
msgstr ""
-msgid "Slack application"
+msgid "Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job"
msgstr ""
msgid "Snippets"
-msgstr ""
+msgstr "スニペット"
msgid "Something went wrong on our end"
msgstr ""
@@ -3791,13 +4067,13 @@ msgstr ""
msgid "Something went wrong on our end."
msgstr ""
-msgid "Something went wrong when toggling the button"
+msgid "Something went wrong on our end. Please try again!"
msgstr ""
-msgid "Something went wrong while fetching Dependency Scanning."
-msgstr ""
+msgid "Something went wrong when toggling the button"
+msgstr "ボタンã®åˆ‡ã‚Šæ›¿ãˆä¸­ã«å•題ãŒç™ºç”Ÿã—ã¾ã—ãŸ"
-msgid "Something went wrong while fetching SAST."
+msgid "Something went wrong while closing the %{issuable}. Please try again later"
msgstr ""
msgid "Something went wrong while fetching the projects."
@@ -3806,6 +4082,12 @@ msgstr ""
msgid "Something went wrong while fetching the registry list."
msgstr ""
+msgid "Something went wrong while reopening the %{issuable}. Please try again later"
+msgstr ""
+
+msgid "Something went wrong while resolving this discussion. Please try again."
+msgstr ""
+
msgid "Something went wrong. Please try again."
msgstr ""
@@ -3843,16 +4125,13 @@ msgid "SortOptions|Last created"
msgstr ""
msgid "SortOptions|Last joined"
-msgstr ""
+msgstr "æ–°ã—ãå‚加ã—ãŸé †"
msgid "SortOptions|Last updated"
msgstr ""
msgid "SortOptions|Least popular"
-msgstr ""
-
-msgid "SortOptions|Less weight"
-msgstr ""
+msgstr "人気順"
msgid "SortOptions|Milestone"
msgstr ""
@@ -3863,20 +4142,17 @@ msgstr ""
msgid "SortOptions|Milestone due soon"
msgstr ""
-msgid "SortOptions|More weight"
-msgstr ""
-
msgid "SortOptions|Most popular"
msgstr ""
msgid "SortOptions|Name"
-msgstr ""
+msgstr "åå‰"
msgid "SortOptions|Name, ascending"
-msgstr ""
+msgstr "åå‰ã€æ˜‡é †"
msgid "SortOptions|Name, descending"
-msgstr ""
+msgstr "åå‰ã€é™é †"
msgid "SortOptions|Oldest created"
msgstr ""
@@ -3891,7 +4167,7 @@ msgid "SortOptions|Oldest updated"
msgstr ""
msgid "SortOptions|Popularity"
-msgstr ""
+msgstr "人気順"
msgid "SortOptions|Priority"
msgstr ""
@@ -3900,16 +4176,13 @@ msgid "SortOptions|Recent sign in"
msgstr ""
msgid "SortOptions|Start later"
-msgstr ""
+msgstr "å¤ã„é †"
msgid "SortOptions|Start soon"
-msgstr ""
-
-msgid "SortOptions|Weight"
-msgstr ""
+msgstr "æ–°ã—ã„é †"
msgid "Source"
-msgstr ""
+msgstr "ソース"
msgid "Source (branch or tag)"
msgstr ""
@@ -3918,15 +4191,42 @@ msgid "Source code"
msgstr "ソースコード"
msgid "Source is not available"
-msgstr ""
+msgstr "ソースã¯åˆ©ç”¨ã§ãã¾ã›ã‚“"
msgid "Spam Logs"
-msgstr ""
+msgstr "スパム ログ"
msgid "Spam and Anti-bot Protection"
msgstr ""
+msgid "Specific Runners"
+msgstr ""
+
msgid "Specify the following URL during the Runner setup:"
+msgstr "Runner セットアップã®éš›ã«æ¬¡ã® URL を指定ã—ã¦ãã ã•ã„:"
+
+msgid "Squash commits"
+msgstr ""
+
+msgid "Stage"
+msgstr ""
+
+msgid "Stage & Commit"
+msgstr ""
+
+msgid "Stage all changes"
+msgstr ""
+
+msgid "Stage changes"
+msgstr ""
+
+msgid "Staged"
+msgstr ""
+
+msgid "Staged %{type}"
+msgstr ""
+
+msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
msgid "StarProject|Star"
@@ -3945,45 +4245,54 @@ msgid "Start a %{new_merge_request} with these changes"
msgstr "ã“ã®å¤‰æ›´ã§ %{new_merge_request} を作æˆã™ã‚‹"
msgid "Start the Runner!"
-msgstr ""
+msgstr "Runner ã‚’èµ·å‹•!"
msgid "Started"
msgstr ""
-msgid "State your message to activate"
+msgid "Starts at (UTC)"
msgstr ""
msgid "Status"
msgstr ""
-msgid "Stopped"
+msgid "Stop this environment"
msgstr ""
+msgid "Stopped"
+msgstr "åœæ­¢ä¸­"
+
msgid "Storage"
-msgstr ""
+msgstr "ストレージ"
msgid "Subgroups"
-msgstr ""
+msgstr "サブグループ"
-msgid "Switch branch/tag"
-msgstr "ブランãƒãƒ»ã‚¿ã‚°åˆ‡ã‚Šæ›¿ãˆ"
+msgid "Subscribe"
+msgstr ""
-msgid "System"
+msgid "Subscribe at group level"
msgstr ""
-msgid "System Hooks"
+msgid "Subscribe at project level"
msgstr ""
-msgid "System header and footer:"
+msgid "Switch branch/tag"
+msgstr "ブランãƒãƒ»ã‚¿ã‚°åˆ‡ã‚Šæ›¿ãˆ"
+
+msgid "System Hooks"
msgstr ""
msgid "Tag (%{tag_count})"
msgid_plural "Tags (%{tag_count})"
-msgstr[0] ""
+msgstr[0] "ã‚¿ã‚° (%{tag_count})"
msgid "Tags"
msgstr "ã‚¿ã‚°"
+msgid "Tags:"
+msgstr "ã‚¿ã‚°:"
+
msgid "TagsPage|Browse commits"
msgstr ""
@@ -3994,19 +4303,19 @@ msgid "TagsPage|Can't find HEAD commit for this tag"
msgstr ""
msgid "TagsPage|Cancel"
-msgstr ""
+msgstr "キャンセル"
msgid "TagsPage|Create tag"
-msgstr ""
+msgstr "タグ作æˆ"
msgid "TagsPage|Delete tag"
-msgstr ""
+msgstr "タグ削除"
msgid "TagsPage|Deleting the %{tag_name} tag cannot be undone. Are you sure?"
msgstr ""
msgid "TagsPage|Edit release notes"
-msgstr ""
+msgstr "リリースノートを編集"
msgid "TagsPage|Existing branch name, tag, or commit SHA"
msgstr ""
@@ -4015,10 +4324,10 @@ msgid "TagsPage|Filter by tag name"
msgstr ""
msgid "TagsPage|New Tag"
-msgstr ""
+msgstr "æ–°ã—ã„ã‚¿ã‚°"
msgid "TagsPage|New tag"
-msgstr ""
+msgstr "æ–°ã—ã„ã‚¿ã‚°"
msgid "TagsPage|Optionally, add a message to the tag."
msgstr ""
@@ -4036,18 +4345,18 @@ msgid "TagsPage|Sort by"
msgstr ""
msgid "TagsPage|Tags"
-msgstr ""
+msgstr "タグ一覧"
msgid "TagsPage|Tags give the ability to mark specific points in history as being important"
msgstr ""
msgid "TagsPage|This tag has no release notes."
-msgstr ""
+msgstr "ã“ã®ã‚¿ã‚°ã«ã¯ãƒªãƒªãƒ¼ã‚¹ãƒŽãƒ¼ãƒˆãŒã‚りã¾ã›ã‚“。"
msgid "TagsPage|Use git tag command to add a new one:"
msgstr ""
-msgid "TagsPage|Write your release notes or drag files here..."
+msgid "TagsPage|Write your release notes or drag files here…"
msgstr ""
msgid "TagsPage|protected"
@@ -4060,12 +4369,15 @@ msgid "Target branch"
msgstr ""
msgid "Team"
+msgstr "ãƒãƒ¼ãƒ "
+
+msgid "Terms of Service Agreement and Privacy Policy"
msgstr ""
-msgid "Thanks! Don't show me this again"
+msgid "Terms of Service and Privacy Policy"
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."
+msgid "Test coverage parsing"
msgstr ""
msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project"
@@ -4074,18 +4386,12 @@ msgstr ""
msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
msgstr ""
-msgid "The X509 Certificate to use when mutual TLS is required to communicate with the external authorization service. If left blank, the server certificate is still validated when accessing over HTTPS."
-msgstr ""
-
msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request."
msgstr "コーディングステージã§ã¯ã€æœ€åˆã®ã‚³ãƒŸãƒƒãƒˆã‹ã‚‰ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆãŒä½œæˆã•れるã¾ã§ã®æ™‚é–“ãŒè¡¨ç¤ºã•れã¾ã™ã€‚ã“ã®ãƒ‡ãƒ¼ã‚¿ã¯æœ€åˆã®ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆãŒä½œæˆã•れãŸã¨ãã«è‡ªå‹•çš„ã«è¿½åŠ ã•れã¾ã™ã€‚"
msgid "The collection of events added to the data gathered for that stage."
msgstr "ã“ã®ã‚¹ãƒ†ãƒ¼ã‚¸ã§è¨ˆæ¸¬ãƒ‡ãƒ¼ã‚¿ã«è¿½åŠ ã•れãŸã‚¤ãƒ™ãƒ³ãƒˆãƒªã‚¹ãƒˆ"
-msgid "The connection will time out after %{timeout}. For repositories that take longer, use a clone/push combination."
-msgstr ""
-
msgid "The fork relationship has been removed."
msgstr "フォークã®ãƒªãƒ¬ãƒ¼ã‚·ãƒ§ãƒ³ãŒå‰Šé™¤ã•れã¾ã—ãŸã€‚"
@@ -4104,7 +4410,7 @@ 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 passphrase required to decrypt the private key. This is optional and the value is encrypted at rest."
+msgid "The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>"
msgstr ""
msgid "The phase of the development lifecycle."
@@ -4113,9 +4419,6 @@ msgstr "é–‹ç™ºãƒ©ã‚¤ãƒ•ã‚µã‚¤ã‚¯ãƒ«ã®æ®µéšŽ"
msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit."
msgstr "計画ステージã§ã¯ã€èª²é¡Œã‚¹ãƒ†ãƒ¼ã‚¸ã«ç™»éŒ²ã•れã¦ã‹ã‚‰ãƒ—ッシュã•ã‚ŒãŸæœ€åˆã®ã‚³ãƒŸãƒƒãƒˆæ™‚刻ã¾ã§ã®æ™‚é–“ãŒè¡¨ç¤ºã•れã¾ã™ã€‚最åˆã®ã‚³ãƒŸãƒƒãƒˆãŒãƒ—ッシュã•れã¨ãã«è‡ªå‹•çš„ã«è¿½åŠ ã•れã¾ã™ã€‚"
-msgid "The private key to use when a client certificate is provided. This value is encrypted at rest."
-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 "プロダクションステージã§ã¯ã€èª²é¡ŒãŒä½œæˆã•れã¦ã‹ã‚‰ãƒ—ロダクションã¸ãƒ‡ãƒ—ロイã•れるã¾ã§ã®æ™‚é–“ãŒè¡¨ç¤ºã•れã¾ã™ã€‚ã‚¢ã‚¤ãƒ‡ã‚£ã‚¢ã®æ™‚点ã‹ã‚‰ãƒ—ロダクションã¾ã§ã®å…¨ã‚¹ãƒ†ãƒ¼ã‚¸ãŒå®Œäº†ã—ãŸã¨ãã«è‡ªå‹•çš„ã«è¿½åŠ ã•れã¾ã™ã€‚"
@@ -4126,7 +4429,7 @@ msgid "The project can be accessed without any authentication."
msgstr "プロジェクトã¯ã€ãƒ­ã‚°ã‚¤ãƒ³ãªã—ã«èª°ã§ã‚‚アクセスã§ãã¾ã™ã€‚"
msgid "The repository for this project does not exist."
-msgstr "ã“ã®ãƒ—ロジェクトã«ãƒ¬ãƒã‚¸ãƒˆãƒªãƒ¼ã¯ã‚りã¾ã›ã‚“。"
+msgstr "ã“ã®ãƒ—ロジェクトã«ãƒªãƒã‚¸ãƒˆãƒªãƒ¼ã¯ã‚りã¾ã›ã‚“。"
msgid "The repository for this project is empty"
msgstr ""
@@ -4137,7 +4440,7 @@ msgstr ""
msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request."
msgstr "レビューステージã¨ã¯ã€ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’作æˆã—ã¦ã‹ã‚‰ãƒžãƒ¼ã‚¸ã™ã‚‹ã¾ã§ã®æ™‚é–“ã§ã™ã€‚ã“ã®ãƒ‡ãƒ¼ã‚¿ã¯æœ€åˆã®ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆãŒãƒžãƒ¼ã‚¸ã•れãŸã¨ãã«è‡ªå‹•çš„ã«è¿½åŠ ã•れã¾ã™ã€‚"
-msgid "The roadmap shows the progress of your epics along a timeline"
+msgid "The secure token used by the Runner to checkout the project"
msgstr ""
msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time."
@@ -4164,26 +4467,32 @@ msgstr "得られãŸä¸€é€£ã®ãƒ‡ãƒ¼ã‚¿ã‚’å°ã•ã„é †ã«ä¸¦ã¹ãŸã¨ãã«ä¸­å¤®
msgid "There are no issues to show"
msgstr ""
+msgid "There are no labels yet"
+msgstr ""
+
msgid "There are no merge requests to show"
msgstr ""
msgid "There are problems accessing Git storage: "
+msgstr "Git ストレージã¸ã®ã‚¢ã‚¯ã‚»ã‚¹ã«å•題ãŒã‚りã¾ã™: "
+
+msgid "There was an error loading jobs"
msgstr ""
-msgid "There was an error loading results"
+msgid "There was an error loading latest pipeline"
msgstr ""
msgid "There was an error loading users activity calendar."
msgstr ""
msgid "There was an error saving your notification settings."
-msgstr ""
+msgstr "通知設定をä¿å­˜ä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚"
msgid "There was an error subscribing to this label."
msgstr ""
msgid "There was an error when reseting email token."
-msgstr ""
+msgstr "メールトークンã®ãƒªã‚»ãƒƒãƒˆä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚"
msgid "There was an error when subscribing to this label."
msgstr ""
@@ -4191,10 +4500,19 @@ msgstr ""
msgid "There was an error when unsubscribing from this label."
msgstr ""
-msgid "This board\\'s scope is reduced"
+msgid "They can be managed using the %{link}."
+msgstr ""
+
+msgid "This GitLab instance does not provide any shared Runners yet. Instance administrators can register shared Runners in the admin area."
+msgstr ""
+
+msgid "This diff is collapsed."
msgstr ""
msgid "This directory"
+msgstr "ディレクトリ"
+
+msgid "This group does not provide any group Runners yet."
msgstr ""
msgid "This is a confidential issue."
@@ -4210,7 +4528,7 @@ msgid "This issue is confidential and locked."
msgstr ""
msgid "This issue is locked."
-msgstr ""
+msgstr "ã“ã®èª²é¡Œã¯ãƒ­ãƒƒã‚¯ã•れã¦ã„ã¾ã™"
msgid "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
msgstr ""
@@ -4218,37 +4536,55 @@ msgstr ""
msgid "This job depends on upstream jobs that need to succeed in order for this job to be triggered"
msgstr ""
-msgid "This job has not been triggered yet"
+msgid "This job does not have a trace."
msgstr ""
+msgid "This job has been canceled"
+msgstr "ã“ã®ã‚¸ãƒ§ãƒ–ã¯ã‚­ãƒ£ãƒ³ã‚»ãƒ«ã•れã¾ã—ãŸ"
+
+msgid "This job has been skipped"
+msgstr "ã“ã®ã‚¸ãƒ§ãƒ–ã¯ã‚¹ã‚­ãƒƒãƒ—ã•れã¾ã—ãŸ"
+
+msgid "This job has not been triggered yet"
+msgstr "ã“ã®ã‚¸ãƒ§ãƒ–ã¯ã¾ã å®Ÿè¡Œã•れã¦ã„ã¾ã›ã‚“"
+
msgid "This job has not started yet"
-msgstr ""
+msgstr "ジョブã¯ã¾ã é–‹å§‹ã•れã¦ã„ã¾ã›ã‚“"
msgid "This job is in pending state and is waiting to be picked by a runner"
msgstr ""
msgid "This job requires a manual action"
-msgstr ""
+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 "This option is disabled while you still have unstaged changes"
+msgstr ""
+
msgid "This page is unavailable because you are not allowed to read information across multiple projects."
+msgstr "複数ã®ãƒ—ロジェクト間ã§èª­ã¿è¾¼ã¿ãŒè¨±å¯ã•れã¦ã„ãªã„ãŸã‚ã€ã“ã®ãƒšãƒ¼ã‚¸ã¯åˆ©ç”¨ã§ãã¾ã›ã‚“。"
+
+msgid "This page will be removed in a future release."
msgstr ""
msgid "This project"
+msgstr "プロジェクト"
+
+msgid "This project does not belong to a group and can therefore not make use of group Runners."
msgstr ""
msgid "This repository"
-msgstr ""
+msgstr "リãƒã‚¸ãƒˆãƒª"
-msgid "This will delete the custom metric, Are you sure?"
+msgid "This source diff could not be displayed because it is too large."
msgstr ""
-msgid "Those emails automatically become issues (with the comments becoming the email conversation) listed here."
+msgid "This user has no identities"
msgstr ""
msgid "Time before an issue gets scheduled"
@@ -4260,11 +4596,11 @@ msgstr "課題ã®å®Ÿè£…ãŒé–‹å§‹ã•れるã¾ã§ã®æ™‚é–“"
msgid "Time between merge request creation and merge/close"
msgstr "マージリクエストãŒä½œæˆã•れã¦ã‹ã‚‰ãƒžãƒ¼ã‚¸ã¾ãŸã¯ã‚¯ãƒ­ãƒ¼ã‚ºã•れるã¾ã§ã®æ™‚é–“"
-msgid "Time between updates and capacity settings."
-msgstr ""
+msgid "Time remaining"
+msgstr "残り時間"
-msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
-msgstr ""
+msgid "Time spent"
+msgstr "çµŒéŽæ™‚é–“"
msgid "Time tracking"
msgstr ""
@@ -4287,6 +4623,9 @@ msgstr "%sæ—¥å‰"
msgid "Timeago|%s days remaining"
msgstr "残り %s日間"
+msgid "Timeago|%s hours ago"
+msgstr ""
+
msgid "Timeago|%s hours remaining"
msgstr "残り %s時間"
@@ -4302,8 +4641,11 @@ msgstr "%sヶ月å‰"
msgid "Timeago|%s months remaining"
msgstr "残り %sヶ月"
+msgid "Timeago|%s seconds ago"
+msgstr "%sç§’å‰"
+
msgid "Timeago|%s seconds remaining"
-msgstr "残り %s 秒"
+msgstr "残り%s秒"
msgid "Timeago|%s weeks ago"
msgstr "%s週間å‰"
@@ -4317,48 +4659,45 @@ msgstr "%så¹´å‰"
msgid "Timeago|%s years remaining"
msgstr "残り %s年間"
+msgid "Timeago|1 day ago"
+msgstr "1æ—¥å‰"
+
msgid "Timeago|1 day remaining"
msgstr "残り 1日間"
+msgid "Timeago|1 hour ago"
+msgstr "1時間å‰"
+
msgid "Timeago|1 hour remaining"
msgstr "残り 1時間"
+msgid "Timeago|1 minute ago"
+msgstr "1分å‰"
+
msgid "Timeago|1 minute remaining"
msgstr "残り 1分間"
+msgid "Timeago|1 month ago"
+msgstr "1ヶ月å‰"
+
msgid "Timeago|1 month remaining"
msgstr "残り 1ヶ月"
+msgid "Timeago|1 week ago"
+msgstr "1週間å‰"
+
msgid "Timeago|1 week remaining"
msgstr "残り 1週間"
+msgid "Timeago|1 year ago"
+msgstr "1å¹´å‰"
+
msgid "Timeago|1 year remaining"
msgstr "残り 1年間"
msgid "Timeago|Past due"
msgstr "期é™ã‚ªãƒ¼ãƒãƒ¼"
-msgid "Timeago|a day ago"
-msgstr "1æ—¥å‰"
-
-msgid "Timeago|a month ago"
-msgstr "1ヶ月å‰"
-
-msgid "Timeago|a week ago"
-msgstr "1週間å‰"
-
-msgid "Timeago|a year ago"
-msgstr "1å¹´å‰"
-
-msgid "Timeago|about %s hours ago"
-msgstr "ç´„%s時間å‰"
-
-msgid "Timeago|about a minute ago"
-msgstr "ç´„1分間å‰"
-
-msgid "Timeago|about an hour ago"
-msgstr "ç´„1時間å‰"
-
msgid "Timeago|in %s days"
msgstr "%s日間以内"
@@ -4398,11 +4737,14 @@ msgstr "1週間以内"
msgid "Timeago|in 1 year"
msgstr "1年以内"
-msgid "Timeago|in a while"
-msgstr ""
+msgid "Timeago|just now"
+msgstr "ãŸã£ãŸä»Š"
-msgid "Timeago|less than a minute ago"
-msgstr "1分未満"
+msgid "Timeago|right now"
+msgstr "今"
+
+msgid "Timeout"
+msgstr ""
msgid "Time|hr"
msgid_plural "Time|hrs"
@@ -4416,21 +4758,12 @@ msgid "Time|s"
msgstr "ç§’"
msgid "Tip:"
-msgstr ""
-
-msgid "Title"
-msgstr ""
+msgstr "ヒント:"
msgid "To GitLab"
-msgstr ""
-
-msgid "To connect GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to connect."
-msgstr ""
-
-msgid "To connect GitHub repositories, you first need to authorize GitLab to access the list of your GitHub repositories:"
-msgstr ""
+msgstr "GitLabã¸"
-msgid "To connect an SVN repository, check out %{svn_link}."
+msgid "To add an SSH key you need to %{generate_link_start}generate one%{link_end} or use an %{existing_link_start}existing key%{link_end}."
msgstr ""
msgid "To import GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
@@ -4442,19 +4775,19 @@ msgstr ""
msgid "To import an SVN repository, check out %{svn_link}."
msgstr ""
-msgid "To only use CI/CD features for an external repository, choose <strong>CI/CD for external repo</strong>."
-msgstr ""
-
-msgid "To set up SAML authentication for your group through an identity provider like Azure, Okta, Onelogin, Ping Identity, or your custom SAML 2.0 provider:"
+msgid "To start serving your jobs you can add Runners to your group"
msgstr ""
msgid "To validate your GitLab CI configurations, go to 'CI/CD → Pipelines' inside your project, and click on the 'CI Lint' button."
msgstr ""
-msgid "To view the roadmap, add a planned start or finish date to one of your epics in this group or its subgroups. Only epics in the past 3 months and the next 3 months are shown."
+msgid "Todo"
+msgstr "Todo"
+
+msgid "Toggle Sidebar"
msgstr ""
-msgid "Todo"
+msgid "Toggle discussion"
msgstr ""
msgid "Toggle sidebar"
@@ -4466,6 +4799,9 @@ msgstr ""
msgid "ToggleButton|Toggle Status: ON"
msgstr ""
+msgid "Too many changes to show."
+msgstr ""
+
msgid "Total Time"
msgstr "åˆè¨ˆæ™‚é–“"
@@ -4473,13 +4809,7 @@ msgid "Total test time for all commits/merges"
msgstr "ã™ã¹ã¦ã®ã‚³ãƒŸãƒƒãƒˆ/マージã®åˆè¨ˆãƒ†ã‚¹ãƒˆæ™‚é–“"
msgid "Total: %{total}"
-msgstr ""
-
-msgid "Track activity with Contribution Analytics."
-msgstr ""
-
-msgid "Track groups of issues that share a theme, across projects and milestones"
-msgstr ""
+msgstr "åˆè¨ˆ: %{total}"
msgid "Track time with quick actions"
msgstr ""
@@ -4487,10 +4817,13 @@ msgstr ""
msgid "Trigger this manual action"
msgstr ""
-msgid "Turn on Service Desk"
+msgid "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."
+msgstr ""
+
+msgid "Try again"
msgstr ""
-msgid "Unknown"
+msgid "Unable to load the diff. %{button_try_again}"
msgstr ""
msgid "Unlock"
@@ -4502,25 +4835,44 @@ msgstr ""
msgid "Unresolve discussion"
msgstr ""
+msgid "Unstage all changes"
+msgstr ""
+
+msgid "Unstage changes"
+msgstr ""
+
+msgid "Unstaged"
+msgstr ""
+
+msgid "Unstaged %{type}"
+msgstr ""
+
+msgid "Unstaged and staged %{type}"
+msgstr ""
+
msgid "Unstar"
msgstr "スターを外ã™"
-msgid "Up to date"
+msgid "Unsubscribe"
msgstr ""
-msgid "Upgrade your plan to activate Advanced Global Search."
+msgid "Unsubscribe at group level"
msgstr ""
-msgid "Upgrade your plan to activate Contribution Analytics."
+msgid "Unsubscribe at project level"
msgstr ""
-msgid "Upgrade your plan to activate Group Webhooks."
+msgid "Unverified"
msgstr ""
-msgid "Upgrade your plan to activate Issue weight."
+msgid "Up to date"
msgstr ""
-msgid "Upgrade your plan to improve Issue boards."
+msgid "Update %{files}"
+msgid_plural "Update %{files} files"
+msgstr[0] ""
+
+msgid "Update your group name, description, avatar, and other general settings."
msgstr ""
msgid "Upload New File"
@@ -4530,30 +4882,33 @@ msgid "Upload file"
msgstr "ファイルをアップロード"
msgid "Upload new avatar"
-msgstr ""
+msgstr "æ–°ã—ã„ã‚¢ãƒã‚¿ãƒ¼ã‚’アップロード"
msgid "UploadLink|click to upload"
msgstr "クリックã—ã¦ã‚¢ãƒƒãƒ—ロード"
msgid "Upvotes"
-msgstr ""
+msgstr "ã„ã„ã­"
msgid "Usage statistics"
-msgstr ""
+msgstr "使用状æ³ã®çµ±è¨ˆ"
-msgid "Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab"
+msgid "Use group milestones to manage issues from multiple projects in the same milestone."
msgstr ""
msgid "Use the following registration token during setup:"
-msgstr ""
+msgstr "セットアップã®éš›ã«æ¬¡ã®ç™»éŒ²ãƒˆãƒ¼ã‚¯ãƒ³ã‚’使用ã—ã¦ãã ã•ã„:"
msgid "Use your global notification setting"
msgstr "全体通知設定を利用"
-msgid "Used by members to sign in to your group in GitLab"
+msgid "User and IP Rate Limits"
msgstr ""
-msgid "User and IP Rate Limits"
+msgid "Users"
+msgstr ""
+
+msgid "Variables"
msgstr ""
msgid "Variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. You can use variables for passwords, secret keys, or whatever you want."
@@ -4568,10 +4923,7 @@ msgstr ""
msgid "Various settings that affect GitLab performance."
msgstr ""
-msgid "View and edit lines"
-msgstr ""
-
-msgid "View epics list"
+msgid "Verified"
msgstr ""
msgid "View file @ "
@@ -4580,9 +4932,15 @@ msgstr ""
msgid "View group labels"
msgstr ""
+msgid "View jobs"
+msgstr ""
+
msgid "View labels"
msgstr ""
+msgid "View log"
+msgstr ""
+
msgid "View open merge request"
msgstr "オープンãªãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’表示"
@@ -4610,9 +4968,6 @@ msgstr "䏿˜Ž"
msgid "Want to see the data? Please ask an administrator for access."
msgstr "ã“ã®ãƒ‡ãƒ¼ã‚¿ã‚’å‚ç…§ã—ãŸã„ã§ã™ã‹ï¼Ÿã‚¢ã‚¯ã‚»ã‚¹ã™ã‚‹ã«ã¯ç®¡ç†è€…ã«å•ã„åˆã‚ã›ã¦ãã ã•ã„。"
-msgid "We could not verify that one of your projects on GCP has billing enabled. Please try again."
-msgstr ""
-
msgid "We don't have enough data to show this stage."
msgstr "データä¸è¶³ã®ãŸã‚ã€ã“ã®ã‚¹ãƒ†ãƒ¼ã‚¸ã®è¡¨ç¤ºã¯ã§ãã¾ã›ã‚“。"
@@ -4620,31 +4975,28 @@ msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
msgid "Web IDE"
-msgstr ""
+msgstr "Web IDE"
msgid "Web terminal"
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"
+msgid "When a runner is locked, it cannot be assigned to other projects"
msgstr ""
-msgid "When leaving the URL blank, classification labels can still be specified whitout disabling cross project features or performing external authorization checks."
+msgid "When enabled, users cannot use GitLab until the terms have been accepted."
msgstr ""
msgid "Wiki"
-msgstr ""
+msgstr "Wiki"
msgid "WikiClone|Clone your wiki"
-msgstr ""
+msgstr "Wikiをクローン"
msgid "WikiClone|Git Access"
-msgstr ""
+msgstr "Git アクセス"
msgid "WikiClone|Install Gollum"
-msgstr ""
+msgstr "Gollumをインストール"
msgid "WikiClone|It is recommended to install %{markdown} so that GFM features render locally:"
msgstr ""
@@ -4658,7 +5010,31 @@ msgstr ""
msgid "WikiEdit|There is already a page with the same title in that path."
msgstr ""
-msgid "WikiEmptyPageError|You are not allowed to create wiki pages"
+msgid "WikiEmptyIssueMessage|Suggest wiki improvement"
+msgstr ""
+
+msgid "WikiEmptyIssueMessage|You must be a project member in order to add wiki pages. If you have suggestions for how to improve the wiki for this project, consider opening an issue in the %{issues_link}."
+msgstr ""
+
+msgid "WikiEmptyIssueMessage|issue tracker"
+msgstr ""
+
+msgid "WikiEmpty|A wiki is where you can store all the details about your project. This can include why you've created it, its principles, how to use it, and so on."
+msgstr ""
+
+msgid "WikiEmpty|Create your first page"
+msgstr ""
+
+msgid "WikiEmpty|Suggest wiki improvement"
+msgstr ""
+
+msgid "WikiEmpty|The wiki lets you write documentation for your project"
+msgstr ""
+
+msgid "WikiEmpty|This project has no wiki pages"
+msgstr ""
+
+msgid "WikiEmpty|You must be a project member in order to add wiki pages."
msgstr ""
msgid "WikiHistoricalPage|This is an old version of this page."
@@ -4683,72 +5059,72 @@ msgid "WikiMarkdownTip|To link to a (new) page, simply type %{link_example}"
msgstr ""
msgid "WikiNewPagePlaceholder|how-to-setup"
-msgstr ""
+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 ""
+msgstr "æ–°ã—ã„ Wiki ページ"
msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?"
+msgstr "ã“ã®ãƒšãƒ¼ã‚¸ã‚’削除ã—ã¦ã‚‚よã‚ã—ã„ã§ã™ã‹ï¼Ÿ"
+
+msgid "WikiPageConfirmDelete|Delete page"
+msgstr ""
+
+msgid "WikiPageConfirmDelete|Delete page %{pageTitle}?"
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 ""
+msgstr "ページ"
msgid "WikiPageCreate|Create %{page_title}"
-msgstr ""
+msgstr "%{page_title} を作æˆ"
msgid "WikiPageEdit|Update %{page_title}"
-msgstr ""
+msgstr "%{page_title} ã‚’æ›´æ–°"
msgid "WikiPage|Page slug"
msgstr ""
-msgid "WikiPage|Write your content or drag files here..."
+msgid "WikiPage|Write your content or drag files here…"
msgstr ""
msgid "Wiki|Create Page"
-msgstr ""
+msgstr "ページを作æˆ"
msgid "Wiki|Create page"
-msgstr ""
+msgstr "ページを作æˆ"
msgid "Wiki|Edit Page"
-msgstr ""
-
-msgid "Wiki|Empty page"
-msgstr ""
+msgstr "ページを編集"
msgid "Wiki|More Pages"
msgstr ""
msgid "Wiki|New page"
-msgstr ""
+msgstr "æ–°ã—ã„ページ"
msgid "Wiki|Page history"
-msgstr ""
+msgstr "ページã®å±¥æ­´"
msgid "Wiki|Page version"
-msgstr ""
+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 ""
+msgstr "Wikiページ"
msgid "Withdraw Access Request"
msgstr "アクセスリクエストをå–り消ã™"
-msgid "Write a commit message..."
+msgid "Yes"
msgstr ""
msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
@@ -4766,15 +5142,18 @@ msgstr ""
msgid "You are on a read-only GitLab instance."
msgstr ""
-msgid "You are on a secondary (read-only) Geo node. If you want to make any changes, you must visit the %{primary_node}."
+msgid "You can %{linkStart}view the blob%{linkEnd} instead."
msgstr ""
msgid "You can also create a project from the command line."
-msgstr ""
+msgstr "コマンドラインã‹ã‚‰ãƒ—ロジェクトを作æˆã™ã‚‹ã“ã¨ã‚‚ã§ãã¾ã™ã€‚"
msgid "You can also star a label to make it a priority label."
msgstr ""
+msgid "You can also test your .gitlab-ci.yml in the %{linkStart}Lint%{linkEnd}"
+msgstr ""
+
msgid "You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}"
msgstr ""
@@ -4787,30 +5166,33 @@ msgstr "ファイルを追加ã™ã‚‹ã«ã¯ã€ã©ã“ã‹ã®ãƒ–ランãƒã«ã„ãªã‘
msgid "You can only edit 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."
+msgid "You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}"
msgstr ""
msgid "You cannot write to this read-only GitLab instance."
msgstr ""
-msgid "You do not have the correct permissions to override the settings from the LDAP group sync."
+msgid "You do not have any assigned merge requests"
msgstr ""
msgid "You have no permissions"
msgstr ""
+msgid "You have not created any merge requests"
+msgstr ""
+
msgid "You have reached your project limit"
msgstr "プロジェクト数ã®ä¸Šé™ã«é”ã—ã¦ã„ã¾ã™"
-msgid "You must have master access to force delete a lock"
+msgid "You must accept our Terms of Service and privacy policy in order to register an account"
+msgstr ""
+
+msgid "You must have maintainer access to force delete a lock"
msgstr ""
msgid "You must sign in to star a project"
msgstr "プロジェクトã«ã‚¹ã‚¿ãƒ¼ã‚’ã¤ã‘ãŸã„å ´åˆã¯ãƒ­ã‚°ã‚¤ãƒ³ã—ã¦ãã ã•ã„"
-msgid "You need a different license to enable FileLocks feature"
-msgstr ""
-
msgid "You need permission."
msgstr "権é™ãŒå¿…è¦ã§ã™"
@@ -4845,19 +5227,19 @@ msgid "You're receiving this email because of your account on %{host}. %{manage_
msgstr ""
msgid "Your Groups"
-msgstr ""
+msgstr "所属グループ"
msgid "Your Kubernetes cluster information on this page is still editable, but you are advised to disable and reconfigure"
msgstr ""
msgid "Your Projects (default)"
-msgstr ""
+msgstr "プロジェクト(デフォルト)"
msgid "Your Projects' Activity"
-msgstr ""
+msgstr "ãƒ—ãƒ­ã‚¸ã‚§ã‚¯ãƒˆã®æ´»å‹•"
msgid "Your Todos"
-msgstr ""
+msgstr "ã‚ãªãŸã®Todo"
msgid "Your changes can be committed to %{branch_name} because a merge request is open."
msgstr ""
@@ -4869,159 +5251,60 @@ msgid "Your comment will not be visible to the public."
msgstr ""
msgid "Your groups"
-msgstr ""
+msgstr "所属グループ"
msgid "Your name"
msgstr "åå‰"
msgid "Your projects"
+msgstr "ã‚ãªãŸã®ãƒ—ロジェクト"
+
+msgid "ago"
msgstr ""
msgid "among other things"
msgstr ""
-msgid "and %d fixed vulnerability"
-msgid_plural "and %d fixed vulnerabilities"
-msgstr[0] ""
-
msgid "assign yourself"
-msgstr ""
+msgstr "自分ã«å‰²ã‚Šå½“ã¦"
msgid "branch name"
-msgstr ""
-
-msgid "by"
-msgstr ""
-
-msgid "ciReport|%{type} detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|%{type} detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Code quality"
-msgstr ""
-
-msgid "ciReport|DAST detected no alerts by analyzing the review app"
-msgstr ""
-
-msgid "ciReport|Dependency scanning"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Failed to load %{reportName} report"
-msgstr ""
-
-msgid "ciReport|Fixed:"
-msgstr ""
-
-msgid "ciReport|Instances"
-msgstr ""
-
-msgid "ciReport|Learn more about whitelisting"
-msgstr ""
-
-msgid "ciReport|Loading %{reportName} report"
-msgstr ""
-
-msgid "ciReport|No changes to code quality"
-msgstr ""
-
-msgid "ciReport|No changes to performance metrics"
-msgstr ""
-
-msgid "ciReport|Performance metrics"
-msgstr ""
-
-msgid "ciReport|SAST"
-msgstr ""
-
-msgid "ciReport|SAST detected"
-msgstr ""
-
-msgid "ciReport|SAST detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|SAST detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|SAST:container no vulnerabilities were found"
-msgstr ""
-
-msgid "ciReport|Security scanning"
-msgstr ""
-
-msgid "ciReport|Security scanning failed loading any results"
-msgstr ""
-
-msgid "ciReport|Show complete code vulnerabilities report"
-msgstr ""
-
-msgid "ciReport|Unapproved vulnerabilities (red) can be marked as approved. %{helpLink}"
-msgstr ""
-
-msgid "ciReport|no vulnerabilities"
-msgstr ""
+msgstr "ブランãƒå"
msgid "command line instructions"
msgstr ""
msgid "connecting"
-msgstr ""
-
-msgid "could not read private key, is the passphrase correct?"
-msgstr ""
+msgstr "接続中"
msgid "day"
msgid_plural "days"
msgstr[0] "æ—¥"
-msgid "detected %d fixed vulnerability"
-msgid_plural "detected %d fixed vulnerabilities"
-msgstr[0] ""
-
-msgid "detected %d new vulnerability"
-msgid_plural "detected %d new vulnerabilities"
-msgstr[0] ""
-
-msgid "detected no vulnerabilities"
-msgstr ""
-
-msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
+msgid "deploy token"
msgstr ""
-msgid "here"
+msgid "disabled"
msgstr ""
-msgid "importing"
+msgid "enabled"
msgstr ""
-msgid "in progress"
-msgstr ""
-
-msgid "is invalid because there is downstream lock"
+msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
msgstr ""
-msgid "is invalid because there is upstream lock"
+msgid "for this project"
msgstr ""
-msgid "is not a valid X509 certificate."
-msgstr ""
+msgid "importing"
+msgstr "インãƒãƒ¼ãƒˆä¸­"
-msgid "locked by %{path_lock_user_name} %{created_at}"
+msgid "latest version"
msgstr ""
msgid "merge request"
msgid_plural "merge requests"
-msgstr[0] ""
+msgstr[0] "マージリクエスト"
msgid "mrWidget| Please restore it or use a different %{missingBranchName} branch"
msgstr ""
@@ -5035,35 +5318,14 @@ msgstr ""
msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage is %{emphasisStart} unchanged %{emphasisEnd} at %{memoryFrom}MB"
msgstr ""
-msgid "mrWidget|Add approval"
-msgstr ""
-
-msgid "mrWidget|Allows edits from maintainers"
-msgstr ""
-
-msgid "mrWidget|An error occured while removing your approval."
-msgstr ""
-
-msgid "mrWidget|An error occured while retrieving approval data for this merge request."
-msgstr ""
-
-msgid "mrWidget|An error occured while submitting your approval."
-msgstr ""
-
-msgid "mrWidget|Approve"
-msgstr ""
-
-msgid "mrWidget|Approved"
-msgstr ""
-
-msgid "mrWidget|Approved by"
+msgid "mrWidget|Allows commits from members who can merge to the target branch"
msgstr ""
msgid "mrWidget|Cancel automatic merge"
msgstr ""
msgid "mrWidget|Check out branch"
-msgstr ""
+msgstr "ブランãƒã®ãƒã‚§ãƒƒã‚¯ã‚¢ã‚¦ãƒˆ"
msgid "mrWidget|Checking ability to merge automatically"
msgstr ""
@@ -5083,6 +5345,9 @@ msgstr ""
msgid "mrWidget|Closes"
msgstr ""
+msgid "mrWidget|Create an issue to resolve them later"
+msgstr ""
+
msgid "mrWidget|Deployment statistics are not available currently"
msgstr ""
@@ -5108,7 +5373,7 @@ msgid "mrWidget|Mentions"
msgstr ""
msgid "mrWidget|Merge"
-msgstr ""
+msgstr "マージ"
msgid "mrWidget|Merge failed."
msgstr ""
@@ -5126,7 +5391,7 @@ msgid "mrWidget|Refresh"
msgstr ""
msgid "mrWidget|Refresh now"
-msgstr ""
+msgstr "ã™ãã«æ›´æ–°"
msgid "mrWidget|Refreshing now"
msgstr ""
@@ -5135,16 +5400,13 @@ msgid "mrWidget|Remove Source Branch"
msgstr ""
msgid "mrWidget|Remove source branch"
-msgstr ""
-
-msgid "mrWidget|Remove your approval"
-msgstr ""
+msgstr "ソースブランãƒã‚’削除ã™ã‚‹"
msgid "mrWidget|Request to merge"
msgstr ""
msgid "mrWidget|Resolve conflicts"
-msgstr ""
+msgstr "ç«¶åˆã‚’解決ã™ã‚‹"
msgid "mrWidget|Revert"
msgstr ""
@@ -5165,20 +5427,23 @@ msgid "mrWidget|The changes will be merged into"
msgstr ""
msgid "mrWidget|The source branch has been removed"
-msgstr ""
+msgstr "ã“ã®ã‚½ãƒ¼ã‚¹ãƒ–ランãƒã¯å‰Šé™¤ã•れã¾ã—ãŸ"
msgid "mrWidget|The source branch is being removed"
-msgstr ""
+msgstr "ã“ã®ã‚½ãƒ¼ã‚¹ãƒ–ランãƒã¯å‰Šé™¤ã•れよã†ã¨ã—ã¦ã„ã¾ã™"
msgid "mrWidget|The source branch will be removed"
-msgstr ""
+msgstr "ã“ã®ã‚½ãƒ¼ã‚¹ãƒ–ランãƒã¯å‰Šé™¤ã•れã¾ã™"
msgid "mrWidget|The source branch will not be removed"
-msgstr ""
+msgstr "ã“ã®ã‚½ãƒ¼ã‚¹ãƒ–ランãƒã¯å‰Šé™¤ã•れã¾ã›ã‚“"
msgid "mrWidget|There are merge conflicts"
msgstr ""
+msgid "mrWidget|There are unresolved discussions. Please resolve these discussions"
+msgstr ""
+
msgid "mrWidget|This merge request failed to be merged automatically"
msgstr ""
@@ -5189,7 +5454,7 @@ msgid "mrWidget|This project is archived, write access has been disabled"
msgstr ""
msgid "mrWidget|Web IDE"
-msgstr ""
+msgstr "Web IDE"
msgid "mrWidget|You can merge this merge request manually using the"
msgstr ""
@@ -5198,10 +5463,10 @@ msgid "mrWidget|You can remove source branch now"
msgstr ""
msgid "mrWidget|branch does not exist."
-msgstr ""
+msgstr "ブランãƒãŒå­˜åœ¨ã—ã¾ã›ã‚“。"
msgid "mrWidget|command line"
-msgstr ""
+msgstr "コマンド ライン"
msgid "mrWidget|into"
msgstr ""
@@ -5216,38 +5481,35 @@ msgid "notification emails"
msgstr "メール通知"
msgid "or"
-msgstr ""
+msgstr "ã¾ãŸã¯"
msgid "parent"
msgid_plural "parents"
msgstr[0] "親"
msgid "password"
-msgstr ""
+msgstr "パスワード"
msgid "personal access token"
-msgstr ""
+msgstr "個人ã®ã‚¢ã‚¯ã‚»ã‚¹ãƒˆãƒ¼ã‚¯ãƒ³"
-msgid "private key does not match certificate."
+msgid "remaining"
msgstr ""
msgid "remove due date"
msgstr ""
msgid "source"
-msgstr ""
+msgstr "ソース"
msgid "spendCommand|%{slash_command} will update the sum of the time spent."
msgstr ""
msgid "this document"
-msgstr ""
-
-msgid "to help your contributors communicate effectively!"
-msgstr ""
+msgstr "ã“ã®ãƒ‰ã‚­ãƒ¥ãƒ¡ãƒ³ãƒˆ"
msgid "username"
-msgstr ""
+msgstr "ユーザーå"
msgid "uses Kubernetes clusters to deploy your code!"
msgstr ""
@@ -5255,3 +5517,7 @@ msgstr ""
msgid "with %{additions} additions, %{deletions} deletions."
msgstr ""
+msgid "within %d minute "
+msgid_plural "within %d minutes "
+msgstr[0] ""
+
diff --git a/locale/ko/gitlab.po b/locale/ko/gitlab.po
index 91f68dfdee1..87195cc72e3 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: 2018-04-04 19:35+0200\n"
-"PO-Revision-Date: 2018-04-05 03:34-0400\n"
+"POT-Creation-Date: 2018-07-01 16:35+1000\n"
+"PO-Revision-Date: 2018-07-01 11:02\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Korean\n"
"Language: ko_KR\n"
@@ -16,8 +16,9 @@ msgstr ""
"X-Crowdin-Language: ko\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-msgid " and"
-msgstr ""
+msgid "%d changed file"
+msgid_plural "%d changed files"
+msgstr[0] ""
msgid "%d commit"
msgid_plural "%d commits"
@@ -47,6 +48,14 @@ msgid "%d metric"
msgid_plural "%d metrics"
msgstr[0] ""
+msgid "%d staged change"
+msgid_plural "%d staged changes"
+msgstr[0] ""
+
+msgid "%d unstaged change"
+msgid_plural "%d unstaged changes"
+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 추가 ì»¤ë°‹ì€ ì„±ëŠ¥ ì´ìŠˆë¥¼ 방지하기 위해 ìƒëžµë˜ì—ˆìŠµë‹ˆë‹¤."
@@ -61,12 +70,21 @@ msgid "%{count} participant"
msgid_plural "%{count} participants"
msgstr[0] ""
+msgid "%{filePath} deleted"
+msgstr ""
+
+msgid "%{group_docs_link_start}Groups%{group_docs_link_end} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects."
+msgstr ""
+
msgid "%{loadingIcon} Started"
msgstr ""
msgid "%{lock_path} is locked by GitLab User %{lock_user_id}"
msgstr ""
+msgid "%{nip_domain} can be used as an alternative to a custom domain."
+msgstr ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -79,6 +97,9 @@ msgstr "%{number_of_failures} / %{maximum_failures} 실패. GitLab ì€ ìžë™ìœ¼
msgid "%{openOrClose} %{noteable}"
msgstr ""
+msgid "%{percent}%% complete"
+msgstr ""
+
msgid "%{storage_name}: failed storage access attempt on host:"
msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:"
msgstr[0] ""
@@ -86,15 +107,55 @@ msgstr[0] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{title} changes"
+msgstr ""
+
+msgid "%{unstaged} unstaged and %{staged} staged changes"
+msgstr ""
+
msgid "(checkout the %{link} for information on how to install it)."
msgstr "설치 ë°©ë²•ì— ëŒ€í•œ 정보를 얻기 위해 %{link} 를 ì²´í¬ì•„웃하세요."
msgid "+ %{moreCount} more"
msgstr ""
+msgid "- Runner is active and can process any new jobs"
+msgstr ""
+
+msgid "- Runner is paused and will not receive any new jobs"
+msgstr ""
+
msgid "- show less"
msgstr ""
+msgid "1 %{type} addition"
+msgid_plural "%{count} %{type} additions"
+msgstr[0] ""
+
+msgid "1 %{type} modification"
+msgid_plural "%{count} %{type} modifications"
+msgstr[0] ""
+
+msgid "1 closed issue"
+msgid_plural "%d closed issues"
+msgstr[0] ""
+
+msgid "1 closed merge request"
+msgid_plural "%d closed merge requests"
+msgstr[0] ""
+
+msgid "1 merged merge request"
+msgid_plural "%d merged merge requests"
+msgstr[0] ""
+
+msgid "1 open issue"
+msgid_plural "%d open issues"
+msgstr[0] ""
+
+msgid "1 open merge request"
+msgid_plural "%d open merge requests"
+msgstr[0] ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] "%d 파ì´í”„ë¼ì¸"
@@ -105,9 +166,27 @@ msgstr ""
msgid "2FA enabled"
msgstr ""
+msgid "403|Please contact your GitLab administrator to get the permission."
+msgstr ""
+
+msgid "403|You don't have the permission to access this page."
+msgstr ""
+
+msgid "404|Make sure the address is correct and the page hasn't moved."
+msgstr ""
+
+msgid "404|Page Not Found"
+msgstr ""
+
+msgid "404|Please contact your GitLab administrator if you think this is a mistake."
+msgstr ""
+
msgid "<strong>Removes</strong> source branch"
msgstr ""
+msgid "A 'Runner' is a process which runs a job. You can setup as many Runners as you need."
+msgstr ""
+
msgid "A collection of graphs regarding Continuous Integration"
msgstr "ì§€ì†ì ì¸ í†µí•©ì— ê´€í•œ 그래프 모ìŒ"
@@ -117,6 +196,9 @@ msgstr ""
msgid "A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}."
msgstr ""
+msgid "A regular expression that will be used to find the test coverage output in the job trace. Leave blank to disable"
+msgstr ""
+
msgid "A user with write access to the source branch selected this option"
msgstr ""
@@ -129,36 +211,39 @@ msgstr ""
msgid "Abuse reports"
msgstr ""
+msgid "Accept terms"
+msgstr ""
+
msgid "Access Tokens"
msgstr ""
msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again."
msgstr "오ë™ìž‘ì¤‘ì¸ ì €ìž¥ê³µê°„ì— ëŒ€í•œ ì ‘ê·¼ì´ ë³µêµ¬ ìž‘ì—…ì„ ìœ„í•´ 마운트할 수 있ë„ë¡ ìž„ì‹œë¡œ 허용ë˜ì—ˆìŠµë‹ˆë‹¤. 문제가 í•´ê²°ëœ í›„ 다시 ì ‘ê·¼ì„ í—ˆìš©í•  수 있게 저장공간 정보를 리셋 해주세요."
+msgid "Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report."
+msgstr ""
+
msgid "Account"
msgstr ""
-msgid "Account and limit settings"
+msgid "Account and limit"
msgstr ""
msgid "Active"
msgstr "활성"
+msgid "Active Sessions"
+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 Kubernetes cluster"
msgstr ""
@@ -171,6 +256,9 @@ msgstr ""
msgid "Add new directory"
msgstr "새 디렉토리 추가"
+msgid "Add reaction"
+msgstr ""
+
msgid "Add todo"
msgstr ""
@@ -240,7 +328,10 @@ msgstr ""
msgid "All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings."
msgstr ""
-msgid "Allow edits from maintainers."
+msgid "Allow commits from members who can merge to the target branch."
+msgstr ""
+
+msgid "Allow public access to pipelines and job details, including output logs and artifacts"
msgstr ""
msgid "Allow rendering of PlantUML diagrams in Asciidoc documents."
@@ -252,31 +343,37 @@ msgstr ""
msgid "Allows you to add and manage Kubernetes clusters."
msgstr ""
-msgid "Also called \"Issuer\" or \"Relying party trust identifier\""
+msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
msgstr ""
-msgid "Also called \"Relying party service URL\" or \"Reply URL\""
+msgid "An error occured creating the new branch."
msgstr ""
-msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to connect."
+msgid "An error occured whilst loading all the files."
msgstr ""
-msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
+msgid "An error occured whilst loading the file content."
msgstr ""
-msgid "An error occurred previewing the blob"
+msgid "An error occured whilst loading the file."
msgstr ""
-msgid "An error occurred when toggling the notification subscription"
+msgid "An error occured whilst loading the merge request changes."
+msgstr ""
+
+msgid "An error occured whilst loading the merge request version data."
+msgstr ""
+
+msgid "An error occured whilst loading the merge request."
msgstr ""
-msgid "An error occurred when updating the issue weight"
+msgid "An error occurred previewing the blob"
msgstr ""
-msgid "An error occurred while adding approver"
+msgid "An error occurred when toggling the notification subscription"
msgstr ""
-msgid "An error occurred while detecting host keys"
+msgid "An error occurred while dismissing the alert. Refresh the page and try again."
msgstr ""
msgid "An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again."
@@ -294,10 +391,7 @@ msgstr ""
msgid "An error occurred while getting projects"
msgstr ""
-msgid "An error occurred while importing project"
-msgstr ""
-
-msgid "An error occurred while initializing path locks"
+msgid "An error occurred while importing project: ${details}"
msgstr ""
msgid "An error occurred while loading commits"
@@ -315,9 +409,6 @@ msgstr ""
msgid "An error occurred while making the request."
msgstr ""
-msgid "An error occurred while removing approver"
-msgstr ""
-
msgid "An error occurred while rendering KaTeX"
msgstr ""
@@ -330,9 +421,6 @@ msgstr ""
msgid "An error occurred while retrieving diff"
msgstr ""
-msgid "An error occurred while saving LDAP override status. Please try again."
-msgstr ""
-
msgid "An error occurred while saving assignees"
msgstr ""
@@ -342,9 +430,6 @@ msgstr ""
msgid "An error occurred. Please try again."
msgstr ""
-msgid "Any Label"
-msgstr ""
-
msgid "Appearance"
msgstr ""
@@ -357,28 +442,28 @@ msgstr ""
msgid "April"
msgstr ""
-msgid "Archived project! Repository is read-only"
-msgstr "프로ì íŠ¸ê°€ ë³´ê´€ë˜ì—ˆìŠµë‹ˆë‹¤! 저장소는 ì½ê¸°ë§Œ 가능합니다."
+msgid "Archived project! Repository and other project resources are read-only"
+msgstr ""
msgid "Are you sure you want to delete this pipeline schedule?"
msgstr "ì´ íŒŒì´í”„ë¼ì¸ ìŠ¤ì¼€ì¥´ì„ ì‚­ì œ 하시겠습니까?"
+msgid "Are you sure you want to remove this identity?"
+msgstr ""
+
msgid "Are you sure you want to reset registration token?"
msgstr "ë“±ë¡ í† í°ì„ 초기화 하시겠습니까?"
msgid "Are you sure you want to reset the health check token?"
msgstr "헬스 ì²´í¬ í† í°ì„ 초기화 하시겠습니까?"
-msgid "Are you sure you want to unlock %{path_lock_path}?"
-msgstr ""
-
msgid "Are you sure?"
msgstr "확실합니까?"
msgid "Artifacts"
msgstr ""
-msgid "Assertion consumer service URL"
+msgid "Ask your group maintainer to setup a group Runner."
msgstr ""
msgid "Assign custom color like #FF0000"
@@ -402,9 +487,15 @@ msgstr ""
msgid "Assigned to :name"
msgstr ""
+msgid "Assigned to me"
+msgstr ""
+
msgid "Assignee"
msgstr ""
+msgid "Assignee(s)"
+msgstr ""
+
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "드래그 &amp; 드롭 ë˜ëŠ” %{upload_link}"
@@ -438,7 +529,10 @@ msgstr ""
msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
msgstr ""
-msgid "AutoDevOps|Auto DevOps (Beta)"
+msgid "Auto-cancel redundant, pending pipelines"
+msgstr ""
+
+msgid "AutoDevOps|Auto DevOps"
msgstr ""
msgid "AutoDevOps|Auto DevOps documentation"
@@ -459,79 +553,109 @@ msgstr ""
msgid "AutoDevOps|add a Kubernetes cluster"
msgstr ""
-msgid "AutoDevOps|enable Auto DevOps (Beta)"
+msgid "AutoDevOps|enable Auto DevOps"
msgstr ""
msgid "Available"
msgstr ""
+msgid "Available group Runners : %{runners}"
+msgstr ""
+
+msgid "Available group Runners : %{runners}."
+msgstr ""
+
msgid "Avatar will be removed. Are you sure?"
msgstr ""
msgid "Average per day: %{average}"
msgstr ""
-msgid "Background Color"
+msgid "Background color"
msgstr ""
msgid "Background jobs"
msgstr ""
-msgid "Begin with the selected commit"
+msgid "Badges"
msgstr ""
-msgid "Billing"
+msgid "Badges|A new badge was added."
msgstr ""
-msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan."
+msgid "Badges|Add badge"
msgstr ""
-msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available."
+msgid "Badges|Adding the badge failed, please check the entered URLs and try again."
msgstr ""
-msgid "BillingPlans|Current plan"
+msgid "Badges|Badge image URL"
msgstr ""
-msgid "BillingPlans|Customer Support"
+msgid "Badges|Badge image preview"
msgstr ""
-msgid "BillingPlans|Downgrade"
+msgid "Badges|Delete badge"
msgstr ""
-msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
+msgid "Badges|Delete badge?"
msgstr ""
-msgid "BillingPlans|Manage plan"
+msgid "Badges|Deleting the badge failed, please try again."
msgstr ""
-msgid "BillingPlans|Please contact %{customer_support_link} in that case."
+msgid "Badges|Group Badge"
msgstr ""
-msgid "BillingPlans|See all %{plan_name} features"
+msgid "Badges|Link"
msgstr ""
-msgid "BillingPlans|This group uses the plan associated with its parent group."
+msgid "Badges|No badge image"
msgstr ""
-msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
+msgid "Badges|No image to preview"
msgstr ""
-msgid "BillingPlans|Upgrade"
+msgid "Badges|Project Badge"
msgstr ""
-msgid "BillingPlans|You are currently on the %{plan_link} plan."
+msgid "Badges|Reload badge image"
msgstr ""
-msgid "BillingPlans|frequently asked questions"
+msgid "Badges|Save changes"
msgstr ""
-msgid "BillingPlans|monthly"
+msgid "Badges|Saving the badge failed, please check the entered URLs and try again."
msgstr ""
-msgid "BillingPlans|paid annually at %{price_per_year}"
+msgid "Badges|The %{docsLinkStart}variables%{docsLinkEnd} GitLab supports: %{placeholders}"
msgstr ""
-msgid "BillingPlans|per user"
+msgid "Badges|The badge was deleted."
+msgstr ""
+
+msgid "Badges|The badge was saved."
+msgstr ""
+
+msgid "Badges|This group has no badges"
+msgstr ""
+
+msgid "Badges|This project has no badges"
+msgstr ""
+
+msgid "Badges|Your badges"
+msgstr ""
+
+msgid "Begin with the selected commit"
+msgstr ""
+
+msgid "Below are examples of regex for existing tools:"
+msgstr ""
+
+msgid "Boards"
+msgstr ""
+
+msgid "Branch %{branchName} was not found in this project's repository."
msgstr ""
msgid "Branch (%{branch_count})"
@@ -610,7 +734,7 @@ msgstr ""
msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
msgstr ""
-msgid "Branches|Only a project master or owner can delete a protected branch"
+msgid "Branches|Only a project maintainer or owner can delete a protected branch"
msgstr ""
msgid "Branches|Overview"
@@ -646,9 +770,6 @@ msgstr ""
msgid "Branches|Stale branches"
msgstr ""
-msgid "Branches|The branch could not be updated automatically because it has diverged from its upstream counterpart."
-msgstr ""
-
msgid "Branches|The default branch cannot be deleted"
msgstr ""
@@ -661,15 +782,9 @@ msgstr ""
msgid "Branches|To confirm, type %{branch_name_confirmation}:"
msgstr ""
-msgid "Branches|To discard the local changes and overwrite the branch with the upstream version, delete it here and choose 'Update Now' above."
-msgstr ""
-
msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
msgstr ""
-msgid "Branches|diverged from upstream"
-msgstr ""
-
msgid "Branches|merged"
msgstr ""
@@ -691,40 +806,79 @@ msgstr "íŒŒì¼ ì°¾ì•„ë³´ê¸°"
msgid "Browse files"
msgstr "íŒŒì¼ ì°¾ì•„ë³´ê¸°"
-msgid "Business"
-msgstr ""
-
msgid "ByAuthor|by"
msgstr "작성ìž"
msgid "CI / CD"
msgstr ""
-msgid "CI/CD"
+msgid "CI / CD Settings"
msgstr ""
msgid "CI/CD configuration"
msgstr ""
-msgid "CI/CD for external repo"
+msgid "CI/CD settings"
+msgstr ""
+
+msgid "CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery."
+msgstr ""
+
+msgid "CICD|Auto DevOps"
+msgstr ""
+
+msgid "CICD|Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration."
+msgstr ""
+
+msgid "CICD|Automatic deployment to staging, manual deployment to production"
+msgstr ""
+
+msgid "CICD|Continuous deployment to production"
+msgstr ""
+
+msgid "CICD|Deployment strategy"
+msgstr ""
+
+msgid "CICD|Deployment strategy needs a domain name to work correctly."
+msgstr ""
+
+msgid "CICD|Disable Auto DevOps"
+msgstr ""
+
+msgid "CICD|Enable Auto DevOps"
+msgstr ""
+
+msgid "CICD|Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific %{ci_file}."
+msgstr ""
+
+msgid "CICD|Instance default (%{state})"
msgstr ""
msgid "CICD|Jobs"
msgstr ""
-msgid "Cancel"
-msgstr "취소"
+msgid "CICD|Learn more about Auto DevOps"
+msgstr ""
-msgid "Cannot be merged automatically"
+msgid "CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project."
msgstr ""
-msgid "Cannot modify managed Kubernetes cluster"
+msgid "CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages."
+msgstr ""
+
+msgid "Can't find HEAD commit for this branch"
msgstr ""
-msgid "Certificate fingerprint"
+msgid "Cancel"
+msgstr "취소"
+
+msgid "Cancel this job"
msgstr ""
-msgid "Change Weight"
+msgid "Cannot be merged automatically"
+msgstr ""
+
+msgid "Cannot modify managed Kubernetes cluster"
msgstr ""
msgid "Change this value to influence how frequently the GitLab UI polls for updates."
@@ -778,21 +932,18 @@ msgstr ""
msgid "Choose a branch/tag (e.g. %{master}) or enter a commit (e.g. %{sha}) to see what's changed or to create a merge request."
msgstr ""
-msgid "Choose file..."
+msgid "Choose any color."
msgstr ""
-msgid "Choose which groups you wish to synchronize to this secondary node."
+msgid "Choose between <code>clone</code> or <code>fetch</code> to get the recent application code"
msgstr ""
-msgid "Choose which repositories you want to connect and run CI/CD pipelines."
+msgid "Choose file..."
msgstr ""
msgid "Choose which repositories you want to import."
msgstr ""
-msgid "Choose which shards you wish to synchronize to this secondary node."
-msgstr ""
-
msgid "CiStatusLabel|canceled"
msgstr "취소ë¨"
@@ -862,21 +1013,12 @@ msgstr ""
msgid "CiVariable|All environments"
msgstr ""
-msgid "CiVariable|Create wildcard"
-msgstr ""
-
msgid "CiVariable|Error occured while saving variables"
msgstr ""
-msgid "CiVariable|New environment"
-msgstr ""
-
msgid "CiVariable|Protected"
msgstr ""
-msgid "CiVariable|Search environments"
-msgstr ""
-
msgid "CiVariable|Toggle protected"
msgstr ""
@@ -886,28 +1028,28 @@ msgstr ""
msgid "CircuitBreakerApiLink|circuitbreaker api"
msgstr ""
-msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
+msgid "Clear search input"
msgstr ""
-msgid "Click to expand text"
+msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Client authentication certificate"
+msgid "Click the <strong>Promote</strong> button in the top right corner to promote it to a group milestone."
msgstr ""
-msgid "Client authentication key"
+msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
msgstr ""
-msgid "Client authentication key password"
+msgid "Click to expand it."
msgstr ""
-msgid "Clone repository"
+msgid "Click to expand text"
msgstr ""
-msgid "Close"
+msgid "Clone repository"
msgstr ""
-msgid "Closed"
+msgid "Close"
msgstr ""
msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster"
@@ -925,6 +1067,12 @@ msgstr ""
msgid "ClusterIntegration|Advanced options on this Kubernetes cluster's integration"
msgstr ""
+msgid "ClusterIntegration|An error occured while trying to fetch project zones: %{error}"
+msgstr ""
+
+msgid "ClusterIntegration|An error occured while trying to fetch your projects: %{error}"
+msgstr ""
+
msgid "ClusterIntegration|Applications"
msgstr ""
@@ -955,6 +1103,9 @@ msgstr ""
msgid "ClusterIntegration|Copy Ingress IP Address to clipboard"
msgstr ""
+msgid "ClusterIntegration|Copy Jupyter Hostname to clipboard"
+msgstr ""
+
msgid "ClusterIntegration|Copy Kubernetes cluster name"
msgstr ""
@@ -970,7 +1121,7 @@ msgstr ""
msgid "ClusterIntegration|Create a new Kubernetes cluster on Google Kubernetes Engine right from GitLab"
msgstr ""
-msgid "ClusterIntegration|Create on GKE"
+msgid "ClusterIntegration|Create on Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
@@ -982,13 +1133,25 @@ msgstr ""
msgid "ClusterIntegration|Environment scope"
msgstr ""
+msgid "ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab's Google Kubernetes Engine Integration."
+msgstr ""
+
+msgid "ClusterIntegration|Fetching machine types"
+msgstr ""
+
+msgid "ClusterIntegration|Fetching projects"
+msgstr ""
+
+msgid "ClusterIntegration|Fetching zones"
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
msgid "ClusterIntegration|GitLab Runner"
msgstr ""
-msgid "ClusterIntegration|Google Cloud Platform project ID"
+msgid "ClusterIntegration|Google Cloud Platform project"
msgstr ""
msgid "ClusterIntegration|Google Kubernetes Engine"
@@ -1000,9 +1163,6 @@ msgstr ""
msgid "ClusterIntegration|Helm Tiller"
msgstr ""
-msgid "ClusterIntegration|In order to show the health of the cluster, we'll need to provision your cluster with Prometheus to collect the required data."
-msgstr ""
-
msgid "ClusterIntegration|Ingress"
msgstr ""
@@ -1012,9 +1172,6 @@ msgstr ""
msgid "ClusterIntegration|Install"
msgstr ""
-msgid "ClusterIntegration|Install Prometheus"
-msgstr ""
-
msgid "ClusterIntegration|Installed"
msgstr ""
@@ -1027,13 +1184,16 @@ msgstr ""
msgid "ClusterIntegration|Integration status"
msgstr ""
-msgid "ClusterIntegration|Kubernetes cluster"
+msgid "ClusterIntegration|Jupyter Hostname"
msgstr ""
-msgid "ClusterIntegration|Kubernetes cluster details"
+msgid "ClusterIntegration|JupyterHub"
msgstr ""
-msgid "ClusterIntegration|Kubernetes cluster health"
+msgid "ClusterIntegration|Kubernetes cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Kubernetes cluster details"
msgstr ""
msgid "ClusterIntegration|Kubernetes cluster integration"
@@ -1063,7 +1223,13 @@ msgstr ""
msgid "ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project"
msgstr ""
-msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
+msgid "ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}."
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{help_link_start}Kubernetes%{help_link_end}."
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}."
msgstr ""
msgid "ClusterIntegration|Learn more about environments"
@@ -1087,7 +1253,16 @@ msgstr ""
msgid "ClusterIntegration|More information"
msgstr ""
-msgid "ClusterIntegration|Multiple Kubernetes clusters are available in GitLab Enterprise Edition Premium and Ultimate"
+msgid "ClusterIntegration|No machine types matched your search"
+msgstr ""
+
+msgid "ClusterIntegration|No projects found"
+msgstr ""
+
+msgid "ClusterIntegration|No projects matched your search"
+msgstr ""
+
+msgid "ClusterIntegration|No zones matched your search"
msgstr ""
msgid "ClusterIntegration|Note:"
@@ -1102,9 +1277,6 @@ msgstr ""
msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
msgstr ""
-msgid "ClusterIntegration|Project ID"
-msgstr ""
-
msgid "ClusterIntegration|Project namespace"
msgstr ""
@@ -1117,6 +1289,9 @@ msgstr ""
msgid "ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration."
msgstr ""
+msgid "ClusterIntegration|Redeem up to $500 in free credit for Google Cloud Platform"
+msgstr ""
+
msgid "ClusterIntegration|Remove Kubernetes cluster integration"
msgstr ""
@@ -1132,19 +1307,37 @@ msgstr ""
msgid "ClusterIntegration|Save changes"
msgstr ""
+msgid "ClusterIntegration|Search machine types"
+msgstr ""
+
+msgid "ClusterIntegration|Search projects"
+msgstr ""
+
+msgid "ClusterIntegration|Search zones"
+msgstr ""
+
msgid "ClusterIntegration|Security"
msgstr ""
msgid "ClusterIntegration|See and edit the details for your Kubernetes cluster"
msgstr ""
-msgid "ClusterIntegration|See machine types"
+msgid "ClusterIntegration|Select machine type"
+msgstr ""
+
+msgid "ClusterIntegration|Select project"
+msgstr ""
+
+msgid "ClusterIntegration|Select project and zone to choose machine type"
+msgstr ""
+
+msgid "ClusterIntegration|Select project to choose zone"
msgstr ""
-msgid "ClusterIntegration|See your projects"
+msgid "ClusterIntegration|Select zone"
msgstr ""
-msgid "ClusterIntegration|See zones"
+msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
msgid "ClusterIntegration|Service token"
@@ -1177,6 +1370,9 @@ msgstr ""
msgid "ClusterIntegration|Token"
msgstr ""
+msgid "ClusterIntegration|Validating project billing status"
+msgstr ""
+
msgid "ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
@@ -1207,13 +1403,19 @@ msgstr ""
msgid "ClusterIntegration|properly configured"
msgstr ""
+msgid "ClusterIntegration|sign up"
+msgstr ""
+
msgid "Collapse"
msgstr ""
-msgid "Comment and resolve discussion"
+msgid "Collapse sidebar"
msgstr ""
-msgid "Comment and unresolve discussion"
+msgid "Comment & resolve discussion"
+msgstr ""
+
+msgid "Comment & unresolve discussion"
msgstr ""
msgid "Comments"
@@ -1278,6 +1480,9 @@ msgstr ""
msgid "Committed by"
msgstr "커밋한 사용ìž"
+msgid "Commit…"
+msgstr ""
+
msgid "Compare"
msgstr "비êµ"
@@ -1326,6 +1531,9 @@ msgstr ""
msgid "Configure limits for web and API requests."
msgstr ""
+msgid "Configure push mirrors."
+msgstr ""
+
msgid "Configure storage path and circuit breaker settings."
msgstr ""
@@ -1335,18 +1543,9 @@ msgstr ""
msgid "Connect"
msgstr ""
-msgid "Connect all repositories"
-msgstr ""
-
msgid "Connect repositories from GitHub"
msgstr ""
-msgid "Connect your external repositories, and CI/CD pipelines will run for new commits. A GitLab project will be created with only CI/CD features enabled."
-msgstr ""
-
-msgid "Connecting..."
-msgstr ""
-
msgid "Container Registry"
msgstr ""
@@ -1392,9 +1591,18 @@ 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 "ContainerRegistry|You can also use a %{deploy_token} for read-only access to the registry images."
+msgstr ""
+
+msgid "Continue"
+msgstr ""
+
msgid "Continuous Integration and Deployment"
msgstr ""
+msgid "Contribute to GitLab"
+msgstr ""
+
msgid "Contribution"
msgstr ""
@@ -1416,15 +1624,6 @@ msgstr ""
msgid "ContributorsPage|Please wait a moment, this page will automatically refresh when ready."
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 ""
-
msgid "Copy URL to clipboard"
msgstr "URLì„ í´ë¦½ë³´ë“œì— 복사"
@@ -1437,9 +1636,18 @@ msgstr ""
msgid "Copy commit SHA to clipboard"
msgstr "ì»¤ë°‹ì˜ SHA를 í´ë¦½ë³´ë“œë¡œ 복사합니다"
+msgid "Copy file name to clipboard"
+msgstr ""
+
+msgid "Copy file path to clipboard"
+msgstr ""
+
msgid "Copy reference to clipboard"
msgstr ""
+msgid "Copy to clipboard"
+msgstr ""
+
msgid "Create"
msgstr ""
@@ -1458,15 +1666,15 @@ msgstr "%{protocol}ì„ (를) 통해 Pull 하거나 Push í•  ê°œì¸ ì•¡ì„¸ìŠ¤ 토
msgid "Create branch"
msgstr ""
+msgid "Create commit"
+msgstr ""
+
msgid "Create directory"
msgstr "디렉토리 만들기"
msgid "Create empty repository"
msgstr ""
-msgid "Create epic"
-msgstr ""
-
msgid "Create file"
msgstr ""
@@ -1509,13 +1717,10 @@ msgstr "태그"
msgid "CreateTokenToCloneLink|create a personal access token"
msgstr "ê°œì¸ ì•¡ì„¸ìŠ¤ í† í° ë§Œë“¤ê¸°"
-msgid "Creates a new branch from %{branchName}"
+msgid "Created"
msgstr ""
-msgid "Creates a new branch from %{branchName} and re-directs to create a new merge request"
-msgstr ""
-
-msgid "Creating epic"
+msgid "Created by me"
msgstr ""
msgid "Cron Timezone"
@@ -1524,7 +1729,13 @@ msgstr "Cron 시간대"
msgid "Cron syntax"
msgstr "í¬ë¡  구문"
-msgid "Current node"
+msgid "CurrentUser|Profile"
+msgstr ""
+
+msgid "CurrentUser|Settings"
+msgstr ""
+
+msgid "Custom CI config path"
msgstr ""
msgid "Custom notification events"
@@ -1533,9 +1744,6 @@ 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 "Customize colors"
-msgstr ""
-
msgid "Cycle Analytics"
msgstr ""
@@ -1572,7 +1780,7 @@ msgstr ""
msgid "December"
msgstr ""
-msgid "Default classification label"
+msgid "Decline and sign out"
msgstr ""
msgid "Define a custom pattern with cron syntax"
@@ -1581,6 +1789,9 @@ msgstr "cron êµ¬ë¬¸ì„ ì‚¬ìš©í•˜ì—¬ ì‚¬ìš©ìž ì •ì˜ íŒ¨í„´ ì •ì˜"
msgid "Delete"
msgstr "삭제 "
+msgid "Delete list"
+msgstr ""
+
msgid "Deploy"
msgid_plural "Deploys"
msgstr[0] "ë°°í¬"
@@ -1588,37 +1799,163 @@ msgstr[0] "ë°°í¬"
msgid "Deploy Keys"
msgstr ""
-msgid "Description"
-msgstr "설명"
+msgid "DeployKeys|+%{count} others"
+msgstr ""
+
+msgid "DeployKeys|Current project"
+msgstr ""
+
+msgid "DeployKeys|Deploy key"
+msgstr ""
+
+msgid "DeployKeys|Enabled deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Error enabling deploy key"
+msgstr ""
+
+msgid "DeployKeys|Error getting deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Error removing deploy key"
+msgstr ""
+
+msgid "DeployKeys|Expand %{count} other projects"
+msgstr ""
+
+msgid "DeployKeys|Loading deploy keys"
+msgstr ""
+
+msgid "DeployKeys|No deploy keys found. Create one with the form above."
+msgstr ""
+
+msgid "DeployKeys|Privately accessible deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Project usage"
+msgstr ""
+
+msgid "DeployKeys|Publicly accessible deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Read access only"
+msgstr ""
+
+msgid "DeployKeys|Write access allowed"
+msgstr ""
+
+msgid "DeployKeys|You are going to remove this deploy key. Are you sure?"
+msgstr ""
+
+msgid "DeployTokens|Active Deploy Tokens (%{active_tokens})"
+msgstr ""
+
+msgid "DeployTokens|Add a deploy token"
+msgstr ""
+
+msgid "DeployTokens|Allows read-only access to the registry images"
+msgstr ""
+
+msgid "DeployTokens|Allows read-only access to the repository"
+msgstr ""
+
+msgid "DeployTokens|Copy deploy token to clipboard"
+msgstr ""
+
+msgid "DeployTokens|Copy username to clipboard"
+msgstr ""
+
+msgid "DeployTokens|Create deploy token"
+msgstr ""
+
+msgid "DeployTokens|Created"
+msgstr ""
+
+msgid "DeployTokens|Deploy Tokens"
+msgstr ""
+
+msgid "DeployTokens|Deploy tokens allow read-only access to your repository and registry images."
+msgstr ""
+
+msgid "DeployTokens|Expires"
+msgstr ""
+
+msgid "DeployTokens|Name"
+msgstr ""
+
+msgid "DeployTokens|Pick a name for the application, and we'll give you a unique deploy token."
+msgstr ""
+
+msgid "DeployTokens|Revoke"
+msgstr ""
+
+msgid "DeployTokens|Revoke %{name}"
+msgstr ""
+
+msgid "DeployTokens|Scopes"
+msgstr ""
+
+msgid "DeployTokens|This action cannot be undone."
+msgstr ""
+
+msgid "DeployTokens|This project has no active Deploy Tokens."
+msgstr ""
+
+msgid "DeployTokens|Use this token as a password. Make sure you save it - you won't be able to access it again."
+msgstr ""
+
+msgid "DeployTokens|Use this username as a login."
+msgstr ""
+
+msgid "DeployTokens|Username"
+msgstr ""
+
+msgid "DeployTokens|You are about to revoke"
+msgstr ""
+
+msgid "DeployTokens|Your New Deploy Token"
+msgstr ""
-msgid "Description templates allow you to define context-specific templates for issue and merge request description fields for your project."
+msgid "DeployTokens|Your new project deploy token has been created."
msgstr ""
+msgid "Deprioritize label"
+msgstr ""
+
+msgid "Description"
+msgstr "설명"
+
msgid "Details"
msgstr "ìƒì„¸"
msgid "Diffs|No file name available"
msgstr ""
+msgid "Diffs|Something went wrong while fetching diff lines."
+msgstr ""
+
msgid "Directory name"
msgstr "디렉토리 ì´ë¦„"
msgid "Disable"
msgstr ""
-msgid "Discard draft"
+msgid "Disable for this project"
msgstr ""
-msgid "Discover GitLab Geo."
+msgid "Disable group Runners"
msgstr ""
-msgid "Dismiss Cycle Analytics introduction box"
+msgid "Discard changes"
msgstr ""
-msgid "Dismiss Merge Request promotion"
+msgid "Discard draft"
msgstr ""
-msgid "Documentation for popular identity providers"
+msgid "Dismiss Cycle Analytics introduction box"
+msgstr ""
+
+msgid "Domain"
msgstr ""
msgid "Don't show again"
@@ -1660,31 +1997,34 @@ msgstr ""
msgid "Due date"
msgstr ""
-msgid "During this process, you’ll be asked for URLs from GitLab’s side. Use the URLs shown below."
+msgid "Each Runner can be in one of the following states:"
msgstr ""
msgid "Edit"
msgstr "편집"
+msgid "Edit Label"
+msgstr ""
+
msgid "Edit Pipeline Schedule %{id}"
msgstr "파ì´í”„ë¼ì¸ 스케줄 편집 %{id}"
msgid "Edit files in the editor and commit changes here"
msgstr ""
-msgid "Editing"
+msgid "Edit identity for %{user_name}"
msgstr ""
-msgid "Elasticsearch"
+msgid "Email"
msgstr ""
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "Email patch"
msgstr ""
-msgid "Email"
+msgid "Emails"
msgstr ""
-msgid "Emails"
+msgid "Embed"
msgstr ""
msgid "Enable"
@@ -1693,9 +2033,6 @@ msgstr ""
msgid "Enable Auto DevOps"
msgstr ""
-msgid "Enable SAML authentication for this group"
-msgstr ""
-
msgid "Enable Sentry for error reporting and logging."
msgstr ""
@@ -1705,7 +2042,13 @@ msgstr ""
msgid "Enable and configure Prometheus metrics."
msgstr ""
-msgid "Enable classification control using an external service"
+msgid "Enable for this project"
+msgstr ""
+
+msgid "Enable group Runners"
+msgstr ""
+
+msgid "Enable or disable certain group features and choose access levels."
msgstr ""
msgid "Enable or disable version check and usage ping."
@@ -1717,7 +2060,10 @@ msgstr ""
msgid "Enable the Performance Bar for a given group."
msgstr ""
-msgid "Enabled"
+msgid "Ends at (UTC)"
+msgstr ""
+
+msgid "Environments"
msgstr ""
msgid "Environments|An error occurred while fetching the environments."
@@ -1768,43 +2114,40 @@ msgstr ""
msgid "Environments|You don't have any environments right now."
msgstr ""
-msgid "Epic will be removed! Are you sure?"
+msgid "Error Reporting and Logging"
msgstr ""
-msgid "Epics"
+msgid "Error committing changes. Please try again."
msgstr ""
-msgid "Epics Roadmap"
+msgid "Error fetching contributors data."
msgstr ""
-msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
+msgid "Error fetching job trace"
msgstr ""
-msgid "Error Reporting and Logging"
-msgstr ""
-
-msgid "Error checking branch data. Please try again."
+msgid "Error fetching labels."
msgstr ""
-msgid "Error committing changes. Please try again."
+msgid "Error fetching network graph."
msgstr ""
-msgid "Error creating epic"
+msgid "Error fetching refs"
msgstr ""
-msgid "Error fetching contributors data."
+msgid "Error fetching usage ping data."
msgstr ""
-msgid "Error fetching labels."
+msgid "Error loading branch data. Please try again."
msgstr ""
-msgid "Error fetching network graph."
+msgid "Error loading last commit."
msgstr ""
-msgid "Error fetching refs"
+msgid "Error loading merge requests."
msgstr ""
-msgid "Error fetching usage ping data."
+msgid "Error loading project data. Please try again."
msgstr ""
msgid "Error occurred when toggling the notification subscription"
@@ -1819,6 +2162,9 @@ msgstr ""
msgid "Error updating todo status."
msgstr ""
+msgid "Estimated"
+msgstr ""
+
msgid "EventFilterBy|Filter by all"
msgstr "모든 ê°’ì„ ê¸°ì¤€ìœ¼ë¡œ í•„í„°"
@@ -1849,31 +2195,16 @@ msgstr "매주 (ì¼ìš”ì¼ ì˜¤ì „ 4시ì—)"
msgid "Expand"
msgstr ""
-msgid "Explore projects"
+msgid "Expand all"
msgstr ""
-msgid "Explore public groups"
+msgid "Expand sidebar"
msgstr ""
-msgid "External Classification Policy Authorization"
-msgstr ""
-
-msgid "External authentication"
-msgstr ""
-
-msgid "External authorization denied access to this project"
-msgstr ""
-
-msgid "External authorization request timeout"
-msgstr ""
-
-msgid "ExternalAuthorizationService|Classification Label"
-msgstr ""
-
-msgid "ExternalAuthorizationService|Classification label"
+msgid "Explore projects"
msgstr ""
-msgid "ExternalAuthorizationService|When no classification label is set the default label `%{default_label}` will be used."
+msgid "Explore public groups"
msgstr ""
msgid "Failed"
@@ -1885,6 +2216,9 @@ msgstr ""
msgid "Failed to change the owner"
msgstr "소유ìžë¥¼ 변경하지 못했습니다"
+msgid "Failed to check related branches."
+msgstr ""
+
msgid "Failed to remove issue from board, please try again."
msgstr ""
@@ -1894,6 +2228,12 @@ msgstr "파ì´í”„ë¼ì¸ ìŠ¤ì¼€ì¤„ì„ ì œê±°í•˜ì§€ 못했습니다."
msgid "Failed to update issues, please try again."
msgstr ""
+msgid "Failure"
+msgstr ""
+
+msgid "Faster as it re-uses the project workspace (falling back to clone if it doesn't exist)"
+msgstr ""
+
msgid "Feb"
msgstr ""
@@ -1903,18 +2243,12 @@ msgstr ""
msgid "Fields on this page are now uneditable, you can configure"
msgstr ""
-msgid "File name"
-msgstr ""
-
msgid "Files"
msgstr "파ì¼"
msgid "Files (%{human_size})"
msgstr ""
-msgid "Fill in the fields below, turn on <strong>%{enable_label}</strong>, and press <strong>%{save_changes}</strong>"
-msgstr ""
-
msgid "Filter by commit message"
msgstr "커밋 메시지로 필터"
@@ -1933,10 +2267,13 @@ msgstr "처ìŒ"
msgid "FirstPushedBy|pushed by"
msgstr "푸시한 사용ìž"
-msgid "Font Color"
+msgid "For internal projects, any logged in user can view pipelines and access job details (output logs and artifacts)"
+msgstr ""
+
+msgid "For private projects, any member (guest or higher) can view pipelines and access job details (output logs and artifacts)"
msgstr ""
-msgid "Footer message"
+msgid "For public projects, anyone can view pipelines and access job details (output logs and artifacts)"
msgstr ""
msgid "Fork"
@@ -1955,6 +2292,9 @@ msgstr ""
msgid "Format"
msgstr ""
+msgid "Found errors in your .gitlab-ci.yml:"
+msgstr ""
+
msgid "From %{provider_title}"
msgstr ""
@@ -1970,166 +2310,13 @@ msgstr ""
msgid "GPG Keys"
msgstr ""
-msgid "Generate a default set of labels"
-msgstr ""
-
-msgid "Geo Nodes"
-msgstr ""
-
-msgid "Geo allows you to replicate your GitLab instance to other geographical locations."
-msgstr ""
-
-msgid "GeoNodeSyncStatus|Node is failing or broken."
-msgstr ""
-
-msgid "GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage."
-msgstr ""
-
-msgid "GeoNodes|Checksummed"
-msgstr ""
-
-msgid "GeoNodes|Database replication lag:"
-msgstr ""
-
-msgid "GeoNodes|Disabling a node stops the sync process. Are you sure?"
-msgstr ""
-
-msgid "GeoNodes|Does not match the primary storage configuration"
-msgstr ""
-
-msgid "GeoNodes|Failed"
-msgstr ""
-
-msgid "GeoNodes|Full"
-msgstr ""
-
-msgid "GeoNodes|GitLab version does not match the primary node version"
-msgstr ""
-
-msgid "GeoNodes|GitLab version:"
-msgstr ""
-
-msgid "GeoNodes|Health status:"
-msgstr ""
-
-msgid "GeoNodes|Last event ID processed by cursor:"
-msgstr ""
-
-msgid "GeoNodes|Last event ID seen from primary:"
-msgstr ""
-
-msgid "GeoNodes|Loading nodes"
-msgstr ""
-
-msgid "GeoNodes|Local Attachments:"
-msgstr ""
-
-msgid "GeoNodes|Local LFS objects:"
-msgstr ""
-
-msgid "GeoNodes|Local job artifacts:"
-msgstr ""
-
-msgid "GeoNodes|New node"
-msgstr ""
-
-msgid "GeoNodes|Node Authentication was successfully repaired."
-msgstr ""
-
-msgid "GeoNodes|Node was successfully removed."
-msgstr ""
-
-msgid "GeoNodes|Not checksummed"
-msgstr ""
-
-msgid "GeoNodes|Out of sync"
-msgstr ""
-
-msgid "GeoNodes|Removing a node stops the sync process. Are you sure?"
-msgstr ""
-
-msgid "GeoNodes|Replication slot WAL:"
-msgstr ""
-
-msgid "GeoNodes|Replication slots:"
-msgstr ""
-
-msgid "GeoNodes|Repositories checksummed:"
-msgstr ""
-
-msgid "GeoNodes|Repositories:"
-msgstr ""
-
-msgid "GeoNodes|Repository checksums verified:"
-msgstr ""
-
-msgid "GeoNodes|Selective"
-msgstr ""
-
-msgid "GeoNodes|Something went wrong while changing node status"
-msgstr ""
-
-msgid "GeoNodes|Something went wrong while removing node"
-msgstr ""
-
-msgid "GeoNodes|Something went wrong while repairing node"
+msgid "General"
msgstr ""
-msgid "GeoNodes|Storage config:"
+msgid "General pipelines"
msgstr ""
-msgid "GeoNodes|Sync settings:"
-msgstr ""
-
-msgid "GeoNodes|Synced"
-msgstr ""
-
-msgid "GeoNodes|Unused slots"
-msgstr ""
-
-msgid "GeoNodes|Unverified"
-msgstr ""
-
-msgid "GeoNodes|Used slots"
-msgstr ""
-
-msgid "GeoNodes|Verified"
-msgstr ""
-
-msgid "GeoNodes|Wiki checksums verified:"
-msgstr ""
-
-msgid "GeoNodes|Wikis checksummed:"
-msgstr ""
-
-msgid "GeoNodes|Wikis:"
-msgstr ""
-
-msgid "GeoNodes|You have configured Geo nodes using an insecure HTTP connection. We recommend the use of HTTPS."
-msgstr ""
-
-msgid "Geo|All projects"
-msgstr ""
-
-msgid "Geo|File sync capacity"
-msgstr ""
-
-msgid "Geo|Groups to synchronize"
-msgstr ""
-
-msgid "Geo|Projects in certain groups"
-msgstr ""
-
-msgid "Geo|Projects in certain storage shards"
-msgstr ""
-
-msgid "Geo|Repository sync capacity"
-msgstr ""
-
-msgid "Geo|Select groups to replicate."
-msgstr ""
-
-msgid "Geo|Shards to synchronize"
+msgid "Generate a default set of labels"
msgstr ""
msgid "Git repository URL"
@@ -2141,6 +2328,9 @@ msgstr ""
msgid "Git storage health information has been reset"
msgstr "git storage ìƒíƒœ ì •ë³´ê°€ 초기화ë˜ì—ˆìŠµë‹ˆë‹¤."
+msgid "Git strategy for pipelines"
+msgstr ""
+
msgid "Git version"
msgstr ""
@@ -2150,21 +2340,24 @@ msgstr ""
msgid "GitLab CI Linter has been moved"
msgstr ""
-msgid "GitLab Geo"
+msgid "GitLab Group Runners can execute code for all the projects in this group."
msgstr ""
msgid "GitLab Runner section"
msgstr "GitLab Runner 섹션"
-msgid "GitLab single sign on URL"
-msgstr ""
-
msgid "Gitaly"
msgstr ""
msgid "Gitaly Servers"
msgstr ""
+msgid "Gitaly|Address"
+msgstr ""
+
+msgid "Go Back"
+msgstr ""
+
msgid "Go back"
msgstr ""
@@ -2180,22 +2373,19 @@ msgstr ""
msgid "Got it!"
msgstr ""
-msgid "GroupRoadmap|Epics let you manage your portfolio of projects more efficiently and with less effort"
-msgstr ""
-
-msgid "GroupRoadmap|From %{dateWord}"
+msgid "Graph"
msgstr ""
-msgid "GroupRoadmap|Loading roadmap"
+msgid "Group CI/CD settings"
msgstr ""
-msgid "GroupRoadmap|Something went wrong while fetching epics"
+msgid "Group ID"
msgstr ""
-msgid "GroupRoadmap|To view the roadmap, add a planned start or finish date to one of your epics in this group or its subgroups. Only epics in the past 3 months and the next 3 months are shown &ndash; from %{startDate} to %{endDate}."
+msgid "Group Runners"
msgstr ""
-msgid "GroupRoadmap|Until %{dateWord}"
+msgid "Group maintainers can register group runners in the %{link}"
msgstr ""
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
@@ -2222,6 +2412,9 @@ msgstr ""
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
+msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
+msgstr ""
+
msgid "GroupsEmptyState|A group is a collection of several projects."
msgstr ""
@@ -2261,12 +2454,6 @@ msgstr ""
msgid "GroupsTree|Sorry, no groups or projects matched your search"
msgstr ""
-msgid "Have your users email"
-msgstr ""
-
-msgid "Header message"
-msgstr ""
-
msgid "Health Check"
msgstr "헬스 ì²´í¬"
@@ -2298,19 +2485,49 @@ msgid "Hide value"
msgid_plural "Hide values"
msgstr[0] ""
+msgid "Hide whitespace changes"
+msgstr ""
+
msgid "History"
msgstr ""
msgid "Housekeeping successfully started"
msgstr "Housekeepingì´ ì„±ê³µì ìœ¼ë¡œ 시작ë˜ì—ˆìŠµë‹ˆë‹¤"
-msgid "Identity provider single sign on URL"
+msgid "I accept the %{terms_link}"
+msgstr ""
+
+msgid "I accept the|Terms of Service and Privacy Policy"
+msgstr ""
+
+msgid "ID"
+msgstr ""
+
+msgid "IDE|Commit"
+msgstr ""
+
+msgid "IDE|Edit"
+msgstr ""
+
+msgid "IDE|Go back"
+msgstr ""
+
+msgid "IDE|Open in file view"
+msgstr ""
+
+msgid "IDE|Review"
+msgstr ""
+
+msgid "Identifier"
+msgstr ""
+
+msgid "Identities"
msgstr ""
-msgid "If enabled, access to projects will be validated on an external service using their classification label."
+msgid "If disabled, the access level will depend on the user's permissions in the project."
msgstr ""
-msgid "If using GitHub, you’ll see pipeline statuses on GitHub for your commits and pull requests. %{more_info_link}"
+msgid "If enabled"
msgstr ""
msgid "If you already have files you can push them using the %{link_to_cli} below."
@@ -2319,6 +2536,15 @@ msgstr ""
msgid "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>."
msgstr ""
+msgid "ImageDiffViewer|2-up"
+msgstr ""
+
+msgid "ImageDiffViewer|Onion skin"
+msgstr ""
+
+msgid "ImageDiffViewer|Swipe"
+msgstr ""
+
msgid "Import"
msgstr ""
@@ -2334,16 +2560,10 @@ msgstr ""
msgid "Import repository"
msgstr "저장소 가져 오기"
-msgid "ImportButtons|Connect repositories from"
-msgstr ""
-
-msgid "Improve Issue boards with GitLab Enterprise Edition."
+msgid "Include a Terms of Service agreement and Privacy Policy that all users must accept."
msgstr ""
-msgid "Improve issues management with Issue weight and GitLab Enterprise Edition."
-msgstr ""
-
-msgid "Improve search with Advanced Global Search and GitLab Enterprise Edition."
+msgid "Inline"
msgstr ""
msgid "Install Runner on Kubernetes"
@@ -2352,16 +2572,15 @@ msgstr ""
msgid "Install a Runner compatible with GitLab CI"
msgstr "GitLab CI 와 호환ë˜ëŠ” Runner 설치"
-msgid "Instance"
-msgid_plural "Instances"
-msgstr[0] ""
-
msgid "Instance does not support multiple Kubernetes clusters"
msgstr ""
msgid "Integrations"
msgstr ""
+msgid "Integrations Settings"
+msgstr ""
+
msgid "Interested parties can even contribute by pushing commits if they want to."
msgstr ""
@@ -2377,7 +2596,7 @@ msgstr "주기 패턴"
msgid "Introducing Cycle Analytics"
msgstr "Cycle Analytics 소개"
-msgid "Issue board focus mode"
+msgid "Issue Board"
msgstr ""
msgid "Issue events"
@@ -2386,9 +2605,6 @@ msgstr "ì´ìŠˆ ì´ë²¤íЏ"
msgid "IssueBoards|Board"
msgstr ""
-msgid "IssueBoards|Boards"
-msgstr ""
-
msgid "Issues"
msgstr ""
@@ -2401,6 +2617,12 @@ msgstr ""
msgid "January"
msgstr ""
+msgid "Job"
+msgstr ""
+
+msgid "Job has been erased"
+msgstr ""
+
msgid "Jobs"
msgstr ""
@@ -2443,6 +2665,9 @@ msgstr ""
msgid "Kubernetes service integration has been deprecated. %{deprecated_message_content} your Kubernetes clusters using the new <a href=\"%{url}\"/>Kubernetes Clusters</a> page"
msgstr ""
+msgid "LFS"
+msgstr ""
+
msgid "LFSStatus|Disabled"
msgstr "Disabled"
@@ -2452,6 +2677,9 @@ msgstr "Enabled"
msgid "Label"
msgstr ""
+msgid "Label actions dropdown"
+msgstr ""
+
msgid "LabelSelect|%{firstLabelName} +%{remainingLabelCount} more"
msgstr ""
@@ -2467,6 +2695,9 @@ msgstr ""
msgid "Labels can be applied to issues and merge requests to categorize them."
msgstr ""
+msgid "Labels can be applied to issues and merge requests."
+msgstr ""
+
msgid "Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>"
msgstr ""
@@ -2501,6 +2732,9 @@ msgstr "푸쉬: "
msgid "LastPushEvent|at"
msgstr "at"
+msgid "Latest changes"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -2525,9 +2759,6 @@ msgstr "그룹 떠나기"
msgid "Leave project"
msgstr "프로ì íЏì—서 나가기"
-msgid "License"
-msgstr ""
-
msgid "List"
msgstr ""
@@ -2537,6 +2768,9 @@ msgstr ""
msgid "Loading the GitLab IDE..."
msgstr ""
+msgid "Loading..."
+msgstr ""
+
msgid "Lock"
msgstr ""
@@ -2546,21 +2780,18 @@ msgstr ""
msgid "Lock not found"
msgstr ""
-msgid "Locked"
+msgid "Lock to current projects"
msgstr ""
-msgid "Locked Files"
+msgid "Locked"
msgstr ""
-msgid "Locks give the ability to lock specific file or folder."
+msgid "Locked to current projects"
msgstr ""
msgid "Login"
msgstr ""
-msgid "Make everyone on your team more productive regardless of their location. GitLab Geo creates read-only mirrors of your GitLab instance so you can reduce the time it takes to clone and fetch large repos."
-msgstr ""
-
msgid "Manage all notifications"
msgstr ""
@@ -2573,16 +2804,16 @@ msgstr ""
msgid "Manage project labels"
msgstr ""
-msgid "Manage your group’s membership while adding another level of security with SAML."
-msgstr ""
-
msgid "Mar"
msgstr ""
msgid "March"
msgstr ""
-msgid "Mark done"
+msgid "Mark todo as done"
+msgstr ""
+
+msgid "Markdown enabled"
msgstr ""
msgid "Maximum git storage failures"
@@ -2597,7 +2828,7 @@ msgstr "중앙값"
msgid "Members"
msgstr ""
-msgid "Members will be forwarded here when signing in to your group. Get this from your identity provider, where it can also be called \"SSO Service Location\", \"SAML Token Issuance Endpoint\", or \"SAML 2.0/W-Federation URL\"."
+msgid "Merge Request:"
msgstr ""
msgid "Merge Requests"
@@ -2609,91 +2840,46 @@ msgstr "머지 ì´ë²¤íЏ"
msgid "Merge request"
msgstr ""
-msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
-msgstr ""
-
-msgid "Merged"
-msgstr ""
-
-msgid "Messages"
-msgstr ""
-
-msgid "Metrics - Influx"
-msgstr ""
-
-msgid "Metrics - Prometheus"
-msgstr ""
-
-msgid "Metrics|Business"
-msgstr ""
-
-msgid "Metrics|Create metric"
-msgstr ""
-
-msgid "Metrics|Edit metric"
-msgstr ""
-
-msgid "Metrics|For grouping similar metrics"
-msgstr ""
-
-msgid "Metrics|Label of the chart's vertical axis. Usually the type of the unit being charted. The horizontal axis (X-axis) always represents time."
-msgstr ""
-
-msgid "Metrics|Legend label (optional)"
-msgstr ""
-
-msgid "Metrics|Must be a valid PromQL query."
-msgstr ""
-
-msgid "Metrics|Name"
-msgstr ""
-
-msgid "Metrics|New metric"
-msgstr ""
-
-msgid "Metrics|Prometheus Query Documentation"
-msgstr ""
-
-msgid "Metrics|Query"
+msgid "Merge requests"
msgstr ""
-msgid "Metrics|Response"
+msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
msgstr ""
-msgid "Metrics|System"
+msgid "MergeRequests|Resolve this discussion in a new issue"
msgstr ""
-msgid "Metrics|Type"
+msgid "MergeRequests|Saving the comment failed"
msgstr ""
-msgid "Metrics|Unit label"
+msgid "MergeRequests|Toggle comments for this file"
msgstr ""
-msgid "Metrics|Used as a title for the chart"
+msgid "MergeRequests|Updating discussions failed"
msgstr ""
-msgid "Metrics|Used if the query returns a single series. If it returns multiple series, their legend labels will be picked up from the response."
+msgid "MergeRequests|View file @ %{commitId}"
msgstr ""
-msgid "Metrics|Y-axis label"
+msgid "MergeRequests|View replaced file @ %{commitId}"
msgstr ""
-msgid "Metrics|e.g. HTTP requests"
+msgid "Merged"
msgstr ""
-msgid "Metrics|e.g. Requests/second"
+msgid "Messages"
msgstr ""
-msgid "Metrics|e.g. Throughput"
+msgid "Metrics - Influx"
msgstr ""
-msgid "Metrics|e.g. rate(http_requests_total[5m])"
+msgid "Metrics - Prometheus"
msgstr ""
-msgid "Metrics|e.g. req/sec"
+msgid "Milestone"
msgstr ""
-msgid "Milestone"
+msgid "Milestones"
msgstr ""
msgid "Milestones|Delete milestone"
@@ -2714,9 +2900,6 @@ msgstr ""
msgid "Milestones|Promote Milestone"
msgstr ""
-msgid "Milestones|This action cannot be reversed."
-msgstr ""
-
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "SSH 키 추가"
@@ -2729,7 +2912,7 @@ msgstr ""
msgid "Monitoring"
msgstr ""
-msgid "More info"
+msgid "More actions"
msgstr ""
msgid "More information"
@@ -2744,12 +2927,30 @@ msgstr ""
msgid "Move issue"
msgstr ""
-msgid "Multiple issue boards"
+msgid "Name"
msgstr ""
msgid "Name new label"
msgstr ""
+msgid "Name your individual key via a title"
+msgstr ""
+
+msgid "Nav|Help"
+msgstr ""
+
+msgid "Nav|Home"
+msgstr ""
+
+msgid "Nav|Sign In / Register"
+msgstr ""
+
+msgid "Nav|Sign out and sign in with a different account"
+msgstr ""
+
+msgid "New Identity"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "새 ì´ìŠˆ"
@@ -2760,6 +2961,9 @@ msgstr ""
msgid "New Kubernetes cluster"
msgstr ""
+msgid "New Label"
+msgstr ""
+
msgid "New Pipeline Schedule"
msgstr "새로운 파ì´í”„ë¼ì¸ ì¼ì •"
@@ -2772,15 +2976,15 @@ msgstr ""
msgid "New directory"
msgstr "새 디렉토리"
-msgid "New epic"
-msgstr ""
-
msgid "New file"
msgstr "새 파ì¼"
msgid "New group"
msgstr ""
+msgid "New identity"
+msgstr ""
+
msgid "New issue"
msgstr "새 ì´ìŠˆ"
@@ -2790,6 +2994,9 @@ msgstr ""
msgid "New merge request"
msgstr "새 머지 리퀘스트"
+msgid "New pipelines will cancel older, pending pipelines on the same branch"
+msgstr ""
+
msgid "New project"
msgstr ""
@@ -2805,7 +3012,7 @@ msgstr ""
msgid "New tag"
msgstr "새 태그 "
-msgid "No Label"
+msgid "No"
msgstr ""
msgid "No assignee"
@@ -2826,7 +3033,16 @@ msgstr ""
msgid "No file chosen"
msgstr ""
-msgid "No labels created yet."
+msgid "No files found"
+msgstr ""
+
+msgid "No files found."
+msgstr ""
+
+msgid "No merge requests found"
+msgstr ""
+
+msgid "No messages were logged"
msgstr ""
msgid "No repository"
@@ -2859,15 +3075,9 @@ msgstr "ë°ì´í„°ê°€ 충분하지 않습니다."
msgid "Note that the master branch is automatically protected. %{link_to_protected_branches}"
msgstr ""
-msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow connecting repositories without generating a Personal Access Token."
-msgstr ""
-
msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
msgstr ""
-msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow connecting repositories without generating a Personal Access Token."
-msgstr ""
-
msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
msgstr ""
@@ -2943,9 +3153,6 @@ msgstr ""
msgid "Number of access attempts"
msgstr ""
-msgid "OK"
-msgstr ""
-
msgid "Oct"
msgstr ""
@@ -2955,19 +3162,16 @@ msgstr ""
msgid "OfSearchInADropdown|Filter"
msgstr "í•„í„°"
-msgid "Once imported, repositories can be mirrored over SSH. Read more %{ssh_link}"
-msgstr ""
-
msgid "Online IDE integration settings."
msgstr ""
-msgid "Only project members can comment."
+msgid "Only comments from the following commit are shown below"
msgstr ""
-msgid "Open"
+msgid "Only project members can comment."
msgstr ""
-msgid "Opened"
+msgid "Open in Xcode"
msgstr ""
msgid "OpenedNDaysAgo|Opened"
@@ -2976,9 +3180,18 @@ msgstr "열린"
msgid "Opens in a new window"
msgstr ""
+msgid "Operations"
+msgstr ""
+
msgid "Options"
msgstr "옵션 "
+msgid "Or you can choose one of the suggested colors below"
+msgstr ""
+
+msgid "Other Labels"
+msgstr ""
+
msgid "Otherwise it is recommended you start with one of the options below."
msgstr ""
@@ -3012,12 +3225,27 @@ msgstr ""
msgid "Password"
msgstr ""
+msgid "Paste your public SSH key, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'. Don't use your private SSH key."
+msgstr ""
+
+msgid "Pause"
+msgstr ""
+
msgid "Pending"
msgstr ""
+msgid "Per job. If a job passes this threshold, it will be marked as failed"
+msgstr ""
+
+msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgstr ""
+
msgid "Performance optimization"
msgstr ""
+msgid "Permissions"
+msgstr ""
+
msgid "Personal Access Token"
msgstr ""
@@ -3033,7 +3261,7 @@ msgstr "파ì´í”„ë¼ì¸ 스케쥴"
msgid "Pipeline Schedules"
msgstr "파ì´í”„ë¼ì¸ 스케쥴"
-msgid "Pipeline quota"
+msgid "Pipeline triggers"
msgstr ""
msgid "PipelineCharts|Failed:"
@@ -3132,10 +3360,22 @@ msgstr ""
msgid "Pipelines|This project is not currently set up to run pipelines."
msgstr ""
-msgid "Pipeline|Retry pipeline"
+msgid "Pipeline|Create for"
+msgstr ""
+
+msgid "Pipeline|Create pipeline"
+msgstr ""
+
+msgid "Pipeline|Existing branch name or tag"
msgstr ""
-msgid "Pipeline|Retry pipeline #%{pipelineId}?"
+msgid "Pipeline|Run Pipeline"
+msgstr ""
+
+msgid "Pipeline|Search branches"
+msgstr ""
+
+msgid "Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default."
msgstr ""
msgid "Pipeline|Stop pipeline"
@@ -3144,7 +3384,7 @@ msgstr ""
msgid "Pipeline|Stop pipeline #%{pipelineId}?"
msgstr ""
-msgid "Pipeline|You’re about to retry pipeline %{pipelineId}."
+msgid "Pipeline|Variables"
msgstr ""
msgid "Pipeline|You’re about to stop pipeline %{pipelineId}."
@@ -3162,19 +3402,25 @@ msgstr "스테ì´ì§•"
msgid "Pipeline|with stages"
msgstr "스테ì´ì§•"
+msgid "Plain diff"
+msgstr ""
+
msgid "PlantUML"
msgstr ""
msgid "Play"
msgstr ""
-msgid "Please <a href=%{link_to_billing} target=\"_blank\" rel=\"noopener noreferrer\">enable billing for one of your projects to be able to create a Kubernetes cluster</a>, then try again."
+msgid "Please accept the Terms of Service before continuing."
+msgstr ""
+
+msgid "Please select at least one filter to see results"
msgstr ""
msgid "Please solve the reCAPTCHA"
msgstr ""
-msgid "Please wait while we connect to your repository. Refresh at will."
+msgid "Please try again"
msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
@@ -3183,7 +3429,19 @@ msgstr ""
msgid "Preferences"
msgstr ""
-msgid "Primary"
+msgid "Preferences|Navigation theme"
+msgstr ""
+
+msgid "Prioritize"
+msgstr ""
+
+msgid "Prioritize label"
+msgstr ""
+
+msgid "Prioritized Labels"
+msgstr ""
+
+msgid "Prioritized label"
msgstr ""
msgid "Private - Project access must be granted explicitly to each user."
@@ -3201,6 +3459,12 @@ msgstr ""
msgid "Profiles|Account scheduled for removal."
msgstr ""
+msgid "Profiles|Change username"
+msgstr ""
+
+msgid "Profiles|Current path: %{path}"
+msgstr ""
+
msgid "Profiles|Delete Account"
msgstr ""
@@ -3219,9 +3483,21 @@ msgstr ""
msgid "Profiles|Invalid username"
msgstr ""
+msgid "Profiles|Path"
+msgstr ""
+
msgid "Profiles|Type your %{confirmationValue} to confirm:"
msgstr ""
+msgid "Profiles|Update username"
+msgstr ""
+
+msgid "Profiles|Username change failed - %{message}"
+msgstr ""
+
+msgid "Profiles|Username successfully changed"
+msgstr ""
+
msgid "Profiles|You don't have access to delete this user."
msgstr ""
@@ -3240,6 +3516,12 @@ msgstr ""
msgid "Programming languages used in this repository"
msgstr ""
+msgid "Progress"
+msgstr ""
+
+msgid "Project"
+msgstr ""
+
msgid "Project '%{project_name}' is in the process of being deleted."
msgstr ""
@@ -3252,6 +3534,9 @@ msgstr "'%{project_name}'프로ì íŠ¸ê°€ 성공ì ìœ¼ë¡œ ìƒì„±ë˜ì—ˆìŠµë‹ˆë‹¤."
msgid "Project '%{project_name}' was successfully updated."
msgstr "'%{project_name}'프로ì íŠ¸ê°€ 성공ì ìœ¼ë¡œ ì—…ë°ì´íЏë˜ì—ˆìŠµë‹ˆë‹¤."
+msgid "Project Badges"
+msgstr ""
+
msgid "Project access must be granted explicitly to each user."
msgstr "프로ì íЏ 액세스는 ê° ì‚¬ìš©ìžì—게 명시ì ìœ¼ë¡œ 부여ë˜ì–´ì•¼í•©ë‹ˆë‹¤."
@@ -3279,30 +3564,6 @@ msgstr "프로ì íЏ 내보내기가 시작ë˜ì—ˆìŠµë‹ˆë‹¤. 다운로드 ë§í¬ë
msgid "ProjectActivityRSS|Subscribe"
msgstr "구ë…"
-msgid "ProjectCreationLevel|Allowed to create projects"
-msgstr ""
-
-msgid "ProjectCreationLevel|Default project creation protection"
-msgstr ""
-
-msgid "ProjectCreationLevel|Developers + Masters"
-msgstr ""
-
-msgid "ProjectCreationLevel|Masters"
-msgstr ""
-
-msgid "ProjectCreationLevel|No one"
-msgstr ""
-
-msgid "ProjectFeature|Disabled"
-msgstr "사용 안 함"
-
-msgid "ProjectFeature|Everyone with access"
-msgstr "ì ‘ê·¼ ê¶Œí•œì„ ê°€ì§„ 모든 ì´ë“¤"
-
-msgid "ProjectFeature|Only team members"
-msgstr "íŒ€ì› ë§Œ"
-
msgid "ProjectFileTree|Name"
msgstr "ì´ë¦„"
@@ -3312,27 +3573,6 @@ msgstr "Never"
msgid "ProjectLifecycle|Stage"
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 ""
@@ -3357,6 +3597,9 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr ""
+msgid "PrometheusDashboard|Time"
+msgstr ""
+
msgid "PrometheusService|%{exporters} with %{metrics} were found"
msgstr ""
@@ -3378,18 +3621,9 @@ msgstr ""
msgid "PrometheusService|Common metrics"
msgstr ""
-msgid "PrometheusService|Common metrics are automatically monitored based on a library of metrics from popular exporters."
-msgstr ""
-
-msgid "PrometheusService|Custom metrics"
-msgstr ""
-
msgid "PrometheusService|Finding and configuring metrics..."
msgstr ""
-msgid "PrometheusService|Finding custom metrics..."
-msgstr ""
-
msgid "PrometheusService|Install Prometheus on clusters"
msgstr ""
@@ -3402,13 +3636,13 @@ msgstr ""
msgid "PrometheusService|Metrics"
msgstr ""
-msgid "PrometheusService|Missing environment variable"
+msgid "PrometheusService|Metrics are automatically configured and monitored based on a library of metrics from popular exporters."
msgstr ""
-msgid "PrometheusService|More information"
+msgid "PrometheusService|Missing environment variable"
msgstr ""
-msgid "PrometheusService|New metric"
+msgid "PrometheusService|More information"
msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
@@ -3417,9 +3651,6 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
-msgstr ""
-
msgid "PrometheusService|Time-series monitoring service"
msgstr ""
@@ -3435,22 +3666,28 @@ msgstr ""
msgid "Promote"
msgstr ""
-msgid "Promote to Group Label"
+msgid "Promote these project milestones into a group milestone."
msgstr ""
msgid "Promote to Group Milestone"
msgstr ""
+msgid "Promote to group label"
+msgstr ""
+
msgid "Protip:"
msgstr ""
+msgid "Provider"
+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"
+msgid "Public pipelines"
msgstr ""
msgid "Push events"
@@ -3462,10 +3699,10 @@ msgstr ""
msgid "Push to create a project"
msgstr ""
-msgid "PushRule|Committer restriction"
+msgid "Quick actions can be used in the issues description and comment boxes."
msgstr ""
-msgid "Quick actions can be used in the issues description and comment boxes."
+msgid "Re-deploy"
msgstr ""
msgid "Read more"
@@ -3477,18 +3714,18 @@ msgstr ""
msgid "Real-time features"
msgstr ""
-msgid "RefSwitcher|Branches"
-msgstr "브랜치"
-
-msgid "RefSwitcher|Tags"
-msgstr "태그"
-
msgid "Reference:"
msgstr ""
msgid "Register / Sign In"
msgstr ""
+msgid "Register and see your runners for this group."
+msgstr ""
+
+msgid "Register and see your runners for this project."
+msgstr ""
+
msgid "Registry"
msgstr ""
@@ -3519,28 +3756,28 @@ msgstr "ë‚˜ì¤‘ì— ë‹¤ì‹œ 알림"
msgid "Remove"
msgstr ""
-msgid "Remove avatar"
+msgid "Remove Runner"
msgstr ""
-msgid "Remove project"
-msgstr "프로ì íЏ ì‚­ì œ"
-
-msgid "Repair authentication"
+msgid "Remove avatar"
msgstr ""
-msgid "Repo by URL"
+msgid "Remove priority"
msgstr ""
+msgid "Remove project"
+msgstr "프로ì íЏ ì‚­ì œ"
+
msgid "Repository"
msgstr ""
-msgid "Repository has no locks."
+msgid "Repository Settings"
msgstr ""
msgid "Repository maintenance"
msgstr ""
-msgid "Repository mirror settings"
+msgid "Repository mirror"
msgstr ""
msgid "Repository storage"
@@ -3549,6 +3786,9 @@ msgstr ""
msgid "Request Access"
msgstr "액세스 요청"
+msgid "Require all users to accept Terms of Service and Privacy Policy when they access GitLab."
+msgstr ""
+
msgid "Reset git storage health information"
msgstr "git storage 헬스 정보 초기화"
@@ -3558,10 +3798,25 @@ msgstr "헬스 ì²´í¬ ì ‘ê·¼ í† í° ì´ˆê¸°í™”"
msgid "Reset runners registration token"
msgstr "runner ë“±ë¡ í† í° ì´ˆê¸°í™”"
+msgid "Resolve all discussions in new issue"
+msgstr ""
+
+msgid "Resolve conflicts on source branch"
+msgstr ""
+
msgid "Resolve discussion"
msgstr ""
-msgid "Response"
+msgid "Resume"
+msgstr ""
+
+msgid "Retry"
+msgstr ""
+
+msgid "Retry this job"
+msgstr ""
+
+msgid "Retry verification"
msgstr ""
msgid "Reveal value"
@@ -3574,7 +3829,7 @@ msgstr "ì´ ì»¤ë°‹ ë˜ëŒë¦¬ê¸°"
msgid "Revert this merge request"
msgstr "ì´ ë¨¸ì§€ 리퀘스트 ë˜ëŒë¦¬ê¸°"
-msgid "Review the process for configuring service providers in your identity provider — in this case, GitLab is the \"service provider\" or \"relying party\"."
+msgid "Review"
msgstr ""
msgid "Reviewing"
@@ -3583,28 +3838,31 @@ msgstr ""
msgid "Reviewing (merge request !%{mergeRequestId})"
msgstr ""
-msgid "Roadmap"
+msgid "Rollback"
msgstr ""
-msgid "Run CI/CD pipelines for external repositories"
+msgid "Runner token"
msgstr ""
msgid "Runners"
msgstr ""
-msgid "Running"
+msgid "Runners API"
msgstr ""
-msgid "SAML Single Sign On"
+msgid "Runners can be placed on separate users, servers, and even on your local machine."
msgstr ""
-msgid "SAML Single Sign On Settings"
+msgid "Running"
msgstr ""
-msgid "SHA1 fingerprint of the SAML token signing certificate. Get this from your identity provider, where it can also be called \"Thumbprint\"."
+msgid "SSH Keys"
msgstr ""
-msgid "SSH Keys"
+msgid "SSL Verification"
+msgstr ""
+
+msgid "Save"
msgstr ""
msgid "Save changes"
@@ -3628,15 +3886,30 @@ msgstr ""
msgid "Scheduling Pipelines"
msgstr "파ì´í”„ë¼ì¸ 스케줄ë§"
-msgid "Scoped issue boards"
+msgid "Scroll to bottom"
+msgstr ""
+
+msgid "Scroll to top"
msgstr ""
msgid "Search"
msgstr ""
+msgid "Search branches"
+msgstr ""
+
msgid "Search branches and tags"
msgstr "브랜치 ë° íƒœê·¸ 검색"
+msgid "Search files"
+msgstr ""
+
+msgid "Search for projects, issues, etc."
+msgstr ""
+
+msgid "Search merge requests"
+msgstr ""
+
msgid "Search milestones"
msgstr ""
@@ -3652,15 +3925,15 @@ msgstr ""
msgid "Seconds to wait for a storage access attempt"
msgstr ""
-msgid "Secret variables"
-msgstr ""
-
-msgid "Security report"
+msgid "Select"
msgstr ""
msgid "Select Archive Format"
msgstr "ì•„ì¹´ì´ë¸Œ í¬ë§· ì„ íƒ"
+msgid "Select a namespace to fork the project"
+msgstr ""
+
msgid "Select a timezone"
msgstr "시간대 ì„ íƒ"
@@ -3673,12 +3946,21 @@ msgstr ""
msgid "Select branch/tag"
msgstr ""
-msgid "Select target branch"
-msgstr "ëŒ€ìƒ ë¸Œëžœì¹˜ ì„ íƒ"
+msgid "Select project"
+msgstr ""
+
+msgid "Select project and zone to choose machine type"
+msgstr ""
-msgid "Selective synchronization"
+msgid "Select project to choose zone"
msgstr ""
+msgid "Select source branch"
+msgstr ""
+
+msgid "Select target branch"
+msgstr "ëŒ€ìƒ ë¸Œëžœì¹˜ ì„ íƒ"
+
msgid "Send email"
msgstr ""
@@ -3694,9 +3976,6 @@ msgstr ""
msgid "Service Templates"
msgstr ""
-msgid "Service URL"
-msgstr ""
-
msgid "Session expiration, projects limit and attachment size."
msgstr ""
@@ -3721,9 +4000,6 @@ msgstr ""
msgid "Set up Koding"
msgstr "Koding 설정"
-msgid "Set up assertions/attributes/claims (email, first_name, last_name) and NameID according to %{docsLinkStart}the documentation %{icon}%{docsLinkEnd}"
-msgstr ""
-
msgid "SetPasswordToCloneLink|set a password"
msgstr "패스워드 설정"
@@ -3733,19 +4009,22 @@ msgstr ""
msgid "Setup a specific Runner automatically"
msgstr ""
-msgid "Share the <strong>%{sso_label}</strong> with members so they can sign in to your group through your identity provider"
+msgid "Share"
msgstr ""
-msgid "SharedRunnersMinutesSettings|By resetting the pipeline minutes for this namespace, the currently used minutes will be set to zero."
+msgid "Shared Runners"
msgstr ""
-msgid "SharedRunnersMinutesSettings|Reset pipeline minutes"
+msgid "Show command"
msgstr ""
-msgid "SharedRunnersMinutesSettings|Reset used pipeline minutes"
+msgid "Show complete raw log"
msgstr ""
-msgid "Show command"
+msgid "Show latest version"
+msgstr ""
+
+msgid "Show latest version of the diff"
msgstr ""
msgid "Show parent pages"
@@ -3754,20 +4033,17 @@ msgstr ""
msgid "Show parent subgroups"
msgstr ""
+msgid "Show whitespace changes"
+msgstr ""
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "%d ê°œì˜ ì´ë²¤íЏ 표시 중"
-msgid "Sidebar|Change weight"
-msgstr ""
-
-msgid "Sidebar|No"
+msgid "Side-by-side"
msgstr ""
-msgid "Sidebar|None"
-msgstr ""
-
-msgid "Sidebar|Weight"
+msgid "Sign out"
msgstr ""
msgid "Sign-in restrictions"
@@ -3779,7 +4055,7 @@ msgstr ""
msgid "Size and domain settings for static websites"
msgstr ""
-msgid "Slack application"
+msgid "Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job"
msgstr ""
msgid "Snippets"
@@ -3791,13 +4067,13 @@ msgstr ""
msgid "Something went wrong on our end."
msgstr ""
-msgid "Something went wrong when toggling the button"
+msgid "Something went wrong on our end. Please try again!"
msgstr ""
-msgid "Something went wrong while fetching Dependency Scanning."
+msgid "Something went wrong when toggling the button"
msgstr ""
-msgid "Something went wrong while fetching SAST."
+msgid "Something went wrong while closing the %{issuable}. Please try again later"
msgstr ""
msgid "Something went wrong while fetching the projects."
@@ -3806,6 +4082,12 @@ msgstr ""
msgid "Something went wrong while fetching the registry list."
msgstr ""
+msgid "Something went wrong while reopening the %{issuable}. Please try again later"
+msgstr ""
+
+msgid "Something went wrong while resolving this discussion. Please try again."
+msgstr ""
+
msgid "Something went wrong. Please try again."
msgstr ""
@@ -3851,9 +4133,6 @@ msgstr ""
msgid "SortOptions|Least popular"
msgstr ""
-msgid "SortOptions|Less weight"
-msgstr ""
-
msgid "SortOptions|Milestone"
msgstr ""
@@ -3863,9 +4142,6 @@ msgstr ""
msgid "SortOptions|Milestone due soon"
msgstr ""
-msgid "SortOptions|More weight"
-msgstr ""
-
msgid "SortOptions|Most popular"
msgstr ""
@@ -3905,9 +4181,6 @@ msgstr ""
msgid "SortOptions|Start soon"
msgstr ""
-msgid "SortOptions|Weight"
-msgstr ""
-
msgid "Source"
msgstr ""
@@ -3926,9 +4199,36 @@ msgstr ""
msgid "Spam and Anti-bot Protection"
msgstr ""
+msgid "Specific Runners"
+msgstr ""
+
msgid "Specify the following URL during the Runner setup:"
msgstr "Runner 설정 중 ë‹¤ìŒ URLì„ ì§€ì •í•˜ì„¸ìš”."
+msgid "Squash commits"
+msgstr ""
+
+msgid "Stage"
+msgstr ""
+
+msgid "Stage & Commit"
+msgstr ""
+
+msgid "Stage all changes"
+msgstr ""
+
+msgid "Stage changes"
+msgstr ""
+
+msgid "Staged"
+msgstr ""
+
+msgid "Staged %{type}"
+msgstr ""
+
+msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
+msgstr ""
+
msgid "StarProject|Star"
msgstr "별표"
@@ -3950,12 +4250,15 @@ msgstr "Runner 시작!"
msgid "Started"
msgstr ""
-msgid "State your message to activate"
+msgid "Starts at (UTC)"
msgstr ""
msgid "Status"
msgstr ""
+msgid "Stop this environment"
+msgstr ""
+
msgid "Stopped"
msgstr ""
@@ -3965,16 +4268,19 @@ msgstr ""
msgid "Subgroups"
msgstr ""
-msgid "Switch branch/tag"
-msgstr "스위치 브랜치/태그"
+msgid "Subscribe"
+msgstr ""
-msgid "System"
+msgid "Subscribe at group level"
msgstr ""
-msgid "System Hooks"
+msgid "Subscribe at project level"
msgstr ""
-msgid "System header and footer:"
+msgid "Switch branch/tag"
+msgstr "스위치 브랜치/태그"
+
+msgid "System Hooks"
msgstr ""
msgid "Tag (%{tag_count})"
@@ -3984,6 +4290,9 @@ msgstr[0] ""
msgid "Tags"
msgstr "태그 "
+msgid "Tags:"
+msgstr ""
+
msgid "TagsPage|Browse commits"
msgstr ""
@@ -4047,7 +4356,7 @@ msgstr ""
msgid "TagsPage|Use git tag command to add a new one:"
msgstr ""
-msgid "TagsPage|Write your release notes or drag files here..."
+msgid "TagsPage|Write your release notes or drag files here…"
msgstr ""
msgid "TagsPage|protected"
@@ -4062,19 +4371,19 @@ msgstr ""
msgid "Team"
msgstr ""
-msgid "Thanks! Don't show me this again"
+msgid "Terms of Service Agreement and Privacy Policy"
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."
+msgid "Terms of Service and Privacy Policy"
msgstr ""
-msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project"
+msgid "Test coverage parsing"
msgstr ""
-msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
+msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project"
msgstr ""
-msgid "The X509 Certificate to use when mutual TLS is required to communicate with the external authorization service. If left blank, the server certificate is still validated when accessing over HTTPS."
+msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
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."
@@ -4083,9 +4392,6 @@ msgstr "Coding Stage는 첫 번째 커밋ì—서부터 머지 리퀘스트 ìƒì„±
msgid "The collection of events added to the data gathered for that stage."
msgstr "해당 단계ì—서 수집 ëœ ë°ì´í„°ê°€ ì´ë²¤íЏ 모ìŒì— 추가ë˜ì—ˆìŠµë‹ˆë‹¤."
-msgid "The connection will time out after %{timeout}. For repositories that take longer, use a clone/push combination."
-msgstr ""
-
msgid "The fork relationship has been removed."
msgstr "í¬í¬ 관계가 제거ë˜ì—ˆìŠµë‹ˆë‹¤."
@@ -4104,7 +4410,7 @@ 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 passphrase required to decrypt the private key. This is optional and the value is encrypted at rest."
+msgid "The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>"
msgstr ""
msgid "The phase of the development lifecycle."
@@ -4113,9 +4419,6 @@ msgstr "개발 ìˆ˜ëª…ì£¼ê¸°ì˜ ë‹¨ê³„."
msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit."
msgstr "ê³„íš ë‹¨ê³„ì—서는 ì´ì „ 단계ì—서 첫 번째 커밋 ì‹œê°„ì´ í‘œì‹œë©ë‹ˆë‹¤. ì´ ì‹œê°„ì€ ì²« 번째 ì»¤ë°‹ì„ ëˆ„ë¥´ë©´ ìžë™ìœ¼ë¡œ 추가ë©ë‹ˆë‹¤."
-msgid "The private key to use when a client certificate is provided. This value is encrypted at rest."
-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 "프로ë•ì…˜ 단계ì—서는 문제를 만들고 코드를 프로ë•ì…˜ 환경으로 ë°°í¬í•˜ëŠ” ë° ê±¸ë¦¬ëŠ” ì´ ì‹œê°„ì„ ë³´ì—¬ì¤ë‹ˆë‹¤. ìƒì‚°ì£¼ê¸°ì— 대한 완전한 ì•„ì´ë””어를 ì–»ì€ í›„ì—는 ë°ì´í„°ê°€ ìžë™ìœ¼ë¡œ 추가ë©ë‹ˆë‹¤."
@@ -4137,7 +4440,7 @@ msgstr ""
msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request."
msgstr "Review 단계ì—서는 머지 리퀘스트를 작성한 후 ë¨¸ì§€í•˜ê¸°ê¹Œì§€ì˜ ì‹œê°„ì„ ë³´ì—¬ì¤ë‹ˆë‹¤. ë°ì´í„°ëŠ” 첫 번째 머지 ë¦¬í€˜ìŠ¤íŠ¸ì„ ë¨¸ì§€ 한 í›„ì— ìžë™ìœ¼ë¡œ 추가ë©ë‹ˆë‹¤."
-msgid "The roadmap shows the progress of your epics along a timeline"
+msgid "The secure token used by the Runner to checkout the project"
msgstr ""
msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time."
@@ -4164,13 +4467,19 @@ msgstr "ê°’ì€ ì¼ë ¨ì˜ 관측 ê°’ 중ì ì— 있습니다. 예를 들어, 3, 5,
msgid "There are no issues to show"
msgstr ""
+msgid "There are no labels yet"
+msgstr ""
+
msgid "There are no merge requests to show"
msgstr ""
msgid "There are problems accessing Git storage: "
msgstr "git storageì— ì ‘ê·¼í•˜ëŠ”ë° ë¬¸ì œê°€ ë°œìƒí–ˆìŠµë‹ˆë‹¤. "
-msgid "There was an error loading results"
+msgid "There was an error loading jobs"
+msgstr ""
+
+msgid "There was an error loading latest pipeline"
msgstr ""
msgid "There was an error loading users activity calendar."
@@ -4191,12 +4500,21 @@ msgstr ""
msgid "There was an error when unsubscribing from this label."
msgstr ""
-msgid "This board\\'s scope is reduced"
+msgid "They can be managed using the %{link}."
+msgstr ""
+
+msgid "This GitLab instance does not provide any shared Runners yet. Instance administrators can register shared Runners in the admin area."
+msgstr ""
+
+msgid "This diff is collapsed."
msgstr ""
msgid "This directory"
msgstr ""
+msgid "This group does not provide any group Runners yet."
+msgstr ""
+
msgid "This is a confidential issue."
msgstr ""
@@ -4218,6 +4536,15 @@ msgstr ""
msgid "This job depends on upstream jobs that need to succeed in order for this job to be triggered"
msgstr ""
+msgid "This job does not have a trace."
+msgstr ""
+
+msgid "This job has been canceled"
+msgstr ""
+
+msgid "This job has been skipped"
+msgstr ""
+
msgid "This job has not been triggered yet"
msgstr ""
@@ -4236,19 +4563,28 @@ msgstr "즉, 빈 저장소를 만들거나 기존 저장소를 가져올 때까ì
msgid "This merge request is locked."
msgstr ""
+msgid "This option is disabled while you still have unstaged changes"
+msgstr ""
+
msgid "This page is unavailable because you are not allowed to read information across multiple projects."
msgstr ""
+msgid "This page will be removed in a future release."
+msgstr ""
+
msgid "This project"
msgstr ""
+msgid "This project does not belong to a group and can therefore not make use of group Runners."
+msgstr ""
+
msgid "This repository"
msgstr ""
-msgid "This will delete the custom metric, Are you sure?"
+msgid "This source diff could not be displayed because it is too large."
msgstr ""
-msgid "Those emails automatically become issues (with the comments becoming the email conversation) listed here."
+msgid "This user has no identities"
msgstr ""
msgid "Time before an issue gets scheduled"
@@ -4260,10 +4596,10 @@ msgstr "ì´ìŠˆê°€ 구현ë˜ê¸° ì „ì˜ ì‹œê°„"
msgid "Time between merge request creation and merge/close"
msgstr "머지 리퀘스트 ìƒì„±ê³¼ 머지 / 닫기 사ì´ì˜ 시간"
-msgid "Time between updates and capacity settings."
+msgid "Time remaining"
msgstr ""
-msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
+msgid "Time spent"
msgstr ""
msgid "Time tracking"
@@ -4287,6 +4623,9 @@ msgstr "%s ì¼ ì „"
msgid "Timeago|%s days remaining"
msgstr "%s ì¼ ë‚¨ìŒ"
+msgid "Timeago|%s hours ago"
+msgstr ""
+
msgid "Timeago|%s hours remaining"
msgstr "%s 시간 남ìŒ"
@@ -4302,6 +4641,9 @@ msgstr "%s 개월 전"
msgid "Timeago|%s months remaining"
msgstr "%s 개월 남ìŒ"
+msgid "Timeago|%s seconds ago"
+msgstr ""
+
msgid "Timeago|%s seconds remaining"
msgstr "%s ì´ˆ 남ìŒ"
@@ -4317,48 +4659,45 @@ msgstr "%s ë…„ ì „"
msgid "Timeago|%s years remaining"
msgstr "%s ë…„ 남ìŒ"
+msgid "Timeago|1 day ago"
+msgstr ""
+
msgid "Timeago|1 day remaining"
msgstr "1 ì¼ ë‚¨ìŒ"
+msgid "Timeago|1 hour ago"
+msgstr ""
+
msgid "Timeago|1 hour remaining"
msgstr "1 시간 남ìŒ"
+msgid "Timeago|1 minute ago"
+msgstr ""
+
msgid "Timeago|1 minute remaining"
msgstr "1 ë¶„ 남ìŒ"
+msgid "Timeago|1 month ago"
+msgstr ""
+
msgid "Timeago|1 month remaining"
msgstr "1 개월 남ìŒ"
+msgid "Timeago|1 week ago"
+msgstr ""
+
msgid "Timeago|1 week remaining"
msgstr "1 주 남ìŒ"
+msgid "Timeago|1 year ago"
+msgstr ""
+
msgid "Timeago|1 year remaining"
msgstr "1 ë…„ 남ìŒ"
msgid "Timeago|Past due"
msgstr "기한 초과"
-msgid "Timeago|a day ago"
-msgstr "1 ì¼ ì „"
-
-msgid "Timeago|a month ago"
-msgstr "1 달 전"
-
-msgid "Timeago|a week ago"
-msgstr "1 ì£¼ì¼ ì „"
-
-msgid "Timeago|a year ago"
-msgstr "1 ë…„ ì „"
-
-msgid "Timeago|about %s hours ago"
-msgstr "약 %s 시간 전"
-
-msgid "Timeago|about a minute ago"
-msgstr "약 1 분 전"
-
-msgid "Timeago|about an hour ago"
-msgstr "약 1 시간 전"
-
msgid "Timeago|in %s days"
msgstr "%s ì¼ ì´ë‚´"
@@ -4398,11 +4737,14 @@ msgstr "1 ì£¼ì¼ ì´ë‚´"
msgid "Timeago|in 1 year"
msgstr "1 ë…„ ì´ë‚´"
-msgid "Timeago|in a while"
+msgid "Timeago|just now"
msgstr ""
-msgid "Timeago|less than a minute ago"
-msgstr "1 분미만"
+msgid "Timeago|right now"
+msgstr ""
+
+msgid "Timeout"
+msgstr ""
msgid "Time|hr"
msgid_plural "Time|hrs"
@@ -4418,19 +4760,10 @@ msgstr "ì´ˆ"
msgid "Tip:"
msgstr ""
-msgid "Title"
-msgstr ""
-
msgid "To GitLab"
msgstr ""
-msgid "To connect GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to connect."
-msgstr ""
-
-msgid "To connect GitHub repositories, you first need to authorize GitLab to access the list of your GitHub repositories:"
-msgstr ""
-
-msgid "To connect an SVN repository, check out %{svn_link}."
+msgid "To add an SSH key you need to %{generate_link_start}generate one%{link_end} or use an %{existing_link_start}existing key%{link_end}."
msgstr ""
msgid "To import GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
@@ -4442,19 +4775,19 @@ msgstr ""
msgid "To import an SVN repository, check out %{svn_link}."
msgstr ""
-msgid "To only use CI/CD features for an external repository, choose <strong>CI/CD for external repo</strong>."
+msgid "To start serving your jobs you can add Runners to your group"
msgstr ""
-msgid "To set up SAML authentication for your group through an identity provider like Azure, Okta, Onelogin, Ping Identity, or your custom SAML 2.0 provider:"
+msgid "To validate your GitLab CI configurations, go to 'CI/CD → Pipelines' inside your project, and click on the 'CI Lint' button."
msgstr ""
-msgid "To validate your GitLab CI configurations, go to 'CI/CD → Pipelines' inside your project, and click on the 'CI Lint' button."
+msgid "Todo"
msgstr ""
-msgid "To view the roadmap, add a planned start or finish date to one of your epics in this group or its subgroups. Only epics in the past 3 months and the next 3 months are shown."
+msgid "Toggle Sidebar"
msgstr ""
-msgid "Todo"
+msgid "Toggle discussion"
msgstr ""
msgid "Toggle sidebar"
@@ -4466,6 +4799,9 @@ msgstr ""
msgid "ToggleButton|Toggle Status: ON"
msgstr ""
+msgid "Too many changes to show."
+msgstr ""
+
msgid "Total Time"
msgstr "시간 합계:"
@@ -4475,22 +4811,19 @@ msgstr "모든 커밋 / ë¨¸ì§€ì˜ ì´ í…ŒìŠ¤íŠ¸ 시간"
msgid "Total: %{total}"
msgstr ""
-msgid "Track activity with Contribution Analytics."
-msgstr ""
-
-msgid "Track groups of issues that share a theme, across projects and milestones"
-msgstr ""
-
msgid "Track time with quick actions"
msgstr ""
msgid "Trigger this manual action"
msgstr ""
-msgid "Turn on Service Desk"
+msgid "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."
msgstr ""
-msgid "Unknown"
+msgid "Try again"
+msgstr ""
+
+msgid "Unable to load the diff. %{button_try_again}"
msgstr ""
msgid "Unlock"
@@ -4502,25 +4835,44 @@ msgstr ""
msgid "Unresolve discussion"
msgstr ""
+msgid "Unstage all changes"
+msgstr ""
+
+msgid "Unstage changes"
+msgstr ""
+
+msgid "Unstaged"
+msgstr ""
+
+msgid "Unstaged %{type}"
+msgstr ""
+
+msgid "Unstaged and staged %{type}"
+msgstr ""
+
msgid "Unstar"
msgstr "별표 제거"
-msgid "Up to date"
+msgid "Unsubscribe"
msgstr ""
-msgid "Upgrade your plan to activate Advanced Global Search."
+msgid "Unsubscribe at group level"
msgstr ""
-msgid "Upgrade your plan to activate Contribution Analytics."
+msgid "Unsubscribe at project level"
msgstr ""
-msgid "Upgrade your plan to activate Group Webhooks."
+msgid "Unverified"
msgstr ""
-msgid "Upgrade your plan to activate Issue weight."
+msgid "Up to date"
msgstr ""
-msgid "Upgrade your plan to improve Issue boards."
+msgid "Update %{files}"
+msgid_plural "Update %{files} files"
+msgstr[0] ""
+
+msgid "Update your group name, description, avatar, and other general settings."
msgstr ""
msgid "Upload New File"
@@ -4541,7 +4893,7 @@ msgstr ""
msgid "Usage statistics"
msgstr ""
-msgid "Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab"
+msgid "Use group milestones to manage issues from multiple projects in the same milestone."
msgstr ""
msgid "Use the following registration token during setup:"
@@ -4550,10 +4902,13 @@ msgstr "설정 ì¤‘ì— ë‹¤ìŒ ë“±ë¡ í† í° ì´ìš© : "
msgid "Use your global notification setting"
msgstr "전체 알림 설정 사용"
-msgid "Used by members to sign in to your group in GitLab"
+msgid "User and IP Rate Limits"
msgstr ""
-msgid "User and IP Rate Limits"
+msgid "Users"
+msgstr ""
+
+msgid "Variables"
msgstr ""
msgid "Variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. You can use variables for passwords, secret keys, or whatever you want."
@@ -4568,10 +4923,7 @@ msgstr ""
msgid "Various settings that affect GitLab performance."
msgstr ""
-msgid "View and edit lines"
-msgstr ""
-
-msgid "View epics list"
+msgid "Verified"
msgstr ""
msgid "View file @ "
@@ -4580,9 +4932,15 @@ msgstr ""
msgid "View group labels"
msgstr ""
+msgid "View jobs"
+msgstr ""
+
msgid "View labels"
msgstr ""
+msgid "View log"
+msgstr ""
+
msgid "View open merge request"
msgstr "열린 머지 리퀘스트보기"
@@ -4610,9 +4968,6 @@ msgstr "알 수 ì—†ìŒ"
msgid "Want to see the data? Please ask an administrator for access."
msgstr "ì´ ë°ì´í„°ë¥¼ ë³´ê³  ì‹¶ì€ê°€ìš”? 관리ìžì—게 액세스 ê¶Œí•œì„ ìš”ì²­í•˜ì„¸ìš”."
-msgid "We could not verify that one of your projects on GCP has billing enabled. Please try again."
-msgstr ""
-
msgid "We don't have enough data to show this stage."
msgstr "ì´ ë‹¨ê³„ë¥¼ ë³´ì—¬ì£¼ê¸°ì— ì¶©ë¶„í•œ ë°ì´í„°ê°€ 없습니다."
@@ -4625,13 +4980,10 @@ msgstr ""
msgid "Web terminal"
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."
+msgid "When a runner is locked, it cannot be assigned to other projects"
msgstr ""
-msgid "Weight"
-msgstr ""
-
-msgid "When leaving the URL blank, classification labels can still be specified whitout disabling cross project features or performing external authorization checks."
+msgid "When enabled, users cannot use GitLab until the terms have been accepted."
msgstr ""
msgid "Wiki"
@@ -4658,7 +5010,31 @@ msgstr ""
msgid "WikiEdit|There is already a page with the same title in that path."
msgstr ""
-msgid "WikiEmptyPageError|You are not allowed to create wiki pages"
+msgid "WikiEmptyIssueMessage|Suggest wiki improvement"
+msgstr ""
+
+msgid "WikiEmptyIssueMessage|You must be a project member in order to add wiki pages. If you have suggestions for how to improve the wiki for this project, consider opening an issue in the %{issues_link}."
+msgstr ""
+
+msgid "WikiEmptyIssueMessage|issue tracker"
+msgstr ""
+
+msgid "WikiEmpty|A wiki is where you can store all the details about your project. This can include why you've created it, its principles, how to use it, and so on."
+msgstr ""
+
+msgid "WikiEmpty|Create your first page"
+msgstr ""
+
+msgid "WikiEmpty|Suggest wiki improvement"
+msgstr ""
+
+msgid "WikiEmpty|The wiki lets you write documentation for your project"
+msgstr ""
+
+msgid "WikiEmpty|This project has no wiki pages"
+msgstr ""
+
+msgid "WikiEmpty|You must be a project member in order to add wiki pages."
msgstr ""
msgid "WikiHistoricalPage|This is an old version of this page."
@@ -4694,6 +5070,12 @@ msgstr ""
msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?"
msgstr ""
+msgid "WikiPageConfirmDelete|Delete page"
+msgstr ""
+
+msgid "WikiPageConfirmDelete|Delete page %{pageTitle}?"
+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 ""
@@ -4709,7 +5091,7 @@ msgstr ""
msgid "WikiPage|Page slug"
msgstr ""
-msgid "WikiPage|Write your content or drag files here..."
+msgid "WikiPage|Write your content or drag files here…"
msgstr ""
msgid "Wiki|Create Page"
@@ -4721,9 +5103,6 @@ msgstr ""
msgid "Wiki|Edit Page"
msgstr ""
-msgid "Wiki|Empty page"
-msgstr ""
-
msgid "Wiki|More Pages"
msgstr ""
@@ -4742,13 +5121,10 @@ 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 "액세스 요청 철회"
-msgid "Write a commit message..."
+msgid "Yes"
msgstr ""
msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
@@ -4766,7 +5142,7 @@ msgstr ""
msgid "You are on a read-only GitLab instance."
msgstr ""
-msgid "You are on a secondary (read-only) Geo node. If you want to make any changes, you must visit the %{primary_node}."
+msgid "You can %{linkStart}view the blob%{linkEnd} instead."
msgstr ""
msgid "You can also create a project from the command line."
@@ -4775,6 +5151,9 @@ msgstr ""
msgid "You can also star a label to make it a priority label."
msgstr ""
+msgid "You can also test your .gitlab-ci.yml in the %{linkStart}Lint%{linkEnd}"
+msgstr ""
+
msgid "You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}"
msgstr ""
@@ -4787,30 +5166,33 @@ msgstr "ë¸Œëžœì¹˜ì— ìžˆì„ ë•Œì—ë§Œ 파ì¼ì„ 추가 í•  수 있습니다."
msgid "You can only edit 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."
+msgid "You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}"
msgstr ""
msgid "You cannot write to this read-only GitLab instance."
msgstr ""
-msgid "You do not have the correct permissions to override the settings from the LDAP group sync."
+msgid "You do not have any assigned merge requests"
msgstr ""
msgid "You have no permissions"
msgstr ""
+msgid "You have not created any merge requests"
+msgstr ""
+
msgid "You have reached your project limit"
msgstr "프로ì íЏ ìˆ«ìž í•œë„ì— ë„달했습니다."
-msgid "You must have master access to force delete a lock"
+msgid "You must accept our Terms of Service and privacy policy in order to register an account"
+msgstr ""
+
+msgid "You must have maintainer access to force delete a lock"
msgstr ""
msgid "You must sign in to star a project"
msgstr "프로ì íŠ¸ì— ë³„í‘œë¥¼ 표시하려면 ë¡œê·¸ì¸ í•´ì•¼ 합니다."
-msgid "You need a different license to enable FileLocks feature"
-msgstr ""
-
msgid "You need permission."
msgstr "ê¶Œí•œì´ í•„ìš”í•©ë‹ˆë‹¤."
@@ -4877,12 +5259,11 @@ msgstr "ê·€í•˜ì˜ ì´ë¦„"
msgid "Your projects"
msgstr ""
-msgid "among other things"
+msgid "ago"
msgstr ""
-msgid "and %d fixed vulnerability"
-msgid_plural "and %d fixed vulnerabilities"
-msgstr[0] ""
+msgid "among other things"
+msgstr ""
msgid "assign yourself"
msgstr ""
@@ -4890,133 +5271,35 @@ msgstr ""
msgid "branch name"
msgstr ""
-msgid "by"
-msgstr ""
-
-msgid "ciReport|%{type} detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|%{type} detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Code quality"
-msgstr ""
-
-msgid "ciReport|DAST detected no alerts by analyzing the review app"
-msgstr ""
-
-msgid "ciReport|Dependency scanning"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Failed to load %{reportName} report"
-msgstr ""
-
-msgid "ciReport|Fixed:"
-msgstr ""
-
-msgid "ciReport|Instances"
-msgstr ""
-
-msgid "ciReport|Learn more about whitelisting"
-msgstr ""
-
-msgid "ciReport|Loading %{reportName} report"
-msgstr ""
-
-msgid "ciReport|No changes to code quality"
-msgstr ""
-
-msgid "ciReport|No changes to performance metrics"
-msgstr ""
-
-msgid "ciReport|Performance metrics"
-msgstr ""
-
-msgid "ciReport|SAST"
-msgstr ""
-
-msgid "ciReport|SAST detected"
-msgstr ""
-
-msgid "ciReport|SAST detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|SAST detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|SAST:container no vulnerabilities were found"
-msgstr ""
-
-msgid "ciReport|Security scanning"
-msgstr ""
-
-msgid "ciReport|Security scanning failed loading any results"
-msgstr ""
-
-msgid "ciReport|Show complete code vulnerabilities report"
-msgstr ""
-
-msgid "ciReport|Unapproved vulnerabilities (red) can be marked as approved. %{helpLink}"
-msgstr ""
-
-msgid "ciReport|no vulnerabilities"
-msgstr ""
-
msgid "command line instructions"
msgstr ""
msgid "connecting"
msgstr ""
-msgid "could not read private key, is the passphrase correct?"
-msgstr ""
-
msgid "day"
msgid_plural "days"
msgstr[0] "ì¼"
-msgid "detected %d fixed vulnerability"
-msgid_plural "detected %d fixed vulnerabilities"
-msgstr[0] ""
-
-msgid "detected %d new vulnerability"
-msgid_plural "detected %d new vulnerabilities"
-msgstr[0] ""
-
-msgid "detected no vulnerabilities"
+msgid "deploy token"
msgstr ""
-msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
-msgstr ""
-
-msgid "here"
-msgstr ""
-
-msgid "importing"
+msgid "disabled"
msgstr ""
-msgid "in progress"
+msgid "enabled"
msgstr ""
-msgid "is invalid because there is downstream lock"
+msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
msgstr ""
-msgid "is invalid because there is upstream lock"
+msgid "for this project"
msgstr ""
-msgid "is not a valid X509 certificate."
+msgid "importing"
msgstr ""
-msgid "locked by %{path_lock_user_name} %{created_at}"
+msgid "latest version"
msgstr ""
msgid "merge request"
@@ -5035,28 +5318,7 @@ msgstr ""
msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage is %{emphasisStart} unchanged %{emphasisEnd} at %{memoryFrom}MB"
msgstr ""
-msgid "mrWidget|Add approval"
-msgstr ""
-
-msgid "mrWidget|Allows edits from maintainers"
-msgstr ""
-
-msgid "mrWidget|An error occured while removing your approval."
-msgstr ""
-
-msgid "mrWidget|An error occured while retrieving approval data for this merge request."
-msgstr ""
-
-msgid "mrWidget|An error occured while submitting your approval."
-msgstr ""
-
-msgid "mrWidget|Approve"
-msgstr ""
-
-msgid "mrWidget|Approved"
-msgstr ""
-
-msgid "mrWidget|Approved by"
+msgid "mrWidget|Allows commits from members who can merge to the target branch"
msgstr ""
msgid "mrWidget|Cancel automatic merge"
@@ -5083,6 +5345,9 @@ msgstr ""
msgid "mrWidget|Closes"
msgstr ""
+msgid "mrWidget|Create an issue to resolve them later"
+msgstr ""
+
msgid "mrWidget|Deployment statistics are not available currently"
msgstr ""
@@ -5137,9 +5402,6 @@ msgstr ""
msgid "mrWidget|Remove source branch"
msgstr ""
-msgid "mrWidget|Remove your approval"
-msgstr ""
-
msgid "mrWidget|Request to merge"
msgstr ""
@@ -5179,6 +5441,9 @@ msgstr ""
msgid "mrWidget|There are merge conflicts"
msgstr ""
+msgid "mrWidget|There are unresolved discussions. Please resolve these discussions"
+msgstr ""
+
msgid "mrWidget|This merge request failed to be merged automatically"
msgstr ""
@@ -5228,7 +5493,7 @@ msgstr ""
msgid "personal access token"
msgstr ""
-msgid "private key does not match certificate."
+msgid "remaining"
msgstr ""
msgid "remove due date"
@@ -5243,9 +5508,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "to help your contributors communicate effectively!"
-msgstr ""
-
msgid "username"
msgstr ""
@@ -5255,3 +5517,7 @@ msgstr ""
msgid "with %{additions} additions, %{deletions} deletions."
msgstr ""
+msgid "within %d minute "
+msgid_plural "within %d minutes "
+msgstr[0] ""
+
diff --git a/locale/nl_NL/gitlab.po b/locale/nl_NL/gitlab.po
index 1b3811198a8..174fb63c55f 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: 2018-04-04 19:35+0200\n"
-"PO-Revision-Date: 2018-04-05 03:39-0400\n"
+"POT-Creation-Date: 2018-07-01 16:35+1000\n"
+"PO-Revision-Date: 2018-07-01 11:02\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Dutch\n"
"Language: nl_NL\n"
@@ -16,8 +16,10 @@ msgstr ""
"X-Crowdin-Language: nl\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-msgid " and"
-msgstr ""
+msgid "%d changed file"
+msgid_plural "%d changed files"
+msgstr[0] ""
+msgstr[1] ""
msgid "%d commit"
msgid_plural "%d commits"
@@ -54,6 +56,16 @@ msgid_plural "%d metrics"
msgstr[0] ""
msgstr[1] ""
+msgid "%d staged change"
+msgid_plural "%d staged changes"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "%d unstaged change"
+msgid_plural "%d unstaged changes"
+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."
@@ -70,12 +82,21 @@ msgid_plural "%{count} participants"
msgstr[0] ""
msgstr[1] ""
+msgid "%{filePath} deleted"
+msgstr ""
+
+msgid "%{group_docs_link_start}Groups%{group_docs_link_end} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects."
+msgstr ""
+
msgid "%{loadingIcon} Started"
msgstr ""
msgid "%{lock_path} is locked by GitLab User %{lock_user_id}"
msgstr ""
+msgid "%{nip_domain} can be used as an alternative to a custom domain."
+msgstr ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -88,6 +109,9 @@ msgstr ""
msgid "%{openOrClose} %{noteable}"
msgstr ""
+msgid "%{percent}%% complete"
+msgstr ""
+
msgid "%{storage_name}: failed storage access attempt on host:"
msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:"
msgstr[0] ""
@@ -96,15 +120,62 @@ msgstr[1] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{title} changes"
+msgstr ""
+
+msgid "%{unstaged} unstaged and %{staged} staged changes"
+msgstr ""
+
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 "- Runner is active and can process any new jobs"
+msgstr ""
+
+msgid "- Runner is paused and will not receive any new jobs"
+msgstr ""
+
msgid "- show less"
msgstr ""
+msgid "1 %{type} addition"
+msgid_plural "%{count} %{type} additions"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 %{type} modification"
+msgid_plural "%{count} %{type} modifications"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 closed issue"
+msgid_plural "%d closed issues"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 closed merge request"
+msgid_plural "%d closed merge requests"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 merged merge request"
+msgid_plural "%d merged merge requests"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 open issue"
+msgid_plural "%d open issues"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 open merge request"
+msgid_plural "%d open merge requests"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] ""
@@ -116,9 +187,27 @@ msgstr ""
msgid "2FA enabled"
msgstr ""
+msgid "403|Please contact your GitLab administrator to get the permission."
+msgstr ""
+
+msgid "403|You don't have the permission to access this page."
+msgstr ""
+
+msgid "404|Make sure the address is correct and the page hasn't moved."
+msgstr ""
+
+msgid "404|Page Not Found"
+msgstr ""
+
+msgid "404|Please contact your GitLab administrator if you think this is a mistake."
+msgstr ""
+
msgid "<strong>Removes</strong> source branch"
msgstr ""
+msgid "A 'Runner' is a process which runs a job. You can setup as many Runners as you need."
+msgstr ""
+
msgid "A collection of graphs regarding Continuous Integration"
msgstr ""
@@ -128,6 +217,9 @@ msgstr ""
msgid "A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}."
msgstr ""
+msgid "A regular expression that will be used to find the test coverage output in the job trace. Leave blank to disable"
+msgstr ""
+
msgid "A user with write access to the source branch selected this option"
msgstr ""
@@ -140,36 +232,39 @@ msgstr "Misbruik rapporten"
msgid "Abuse reports"
msgstr ""
+msgid "Accept terms"
+msgstr ""
+
msgid "Access Tokens"
msgstr "Toegangstokens"
msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again."
msgstr ""
+msgid "Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report."
+msgstr ""
+
msgid "Account"
msgstr "Account"
-msgid "Account and limit settings"
+msgid "Account and limit"
msgstr ""
msgid "Active"
msgstr "Actief"
+msgid "Active Sessions"
+msgstr ""
+
msgid "Activity"
msgstr "Activiteit"
-msgid "Add"
-msgstr ""
-
msgid "Add Changelog"
msgstr "Changelog toevoegen"
msgid "Add Contribution guide"
msgstr ""
-msgid "Add Group Webhooks and GitLab Enterprise Edition."
-msgstr ""
-
msgid "Add Kubernetes cluster"
msgstr ""
@@ -182,6 +277,9 @@ msgstr ""
msgid "Add new directory"
msgstr "Nieuwe map toevoegen"
+msgid "Add reaction"
+msgstr ""
+
msgid "Add todo"
msgstr ""
@@ -251,7 +349,10 @@ msgstr ""
msgid "All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings."
msgstr ""
-msgid "Allow edits from maintainers."
+msgid "Allow commits from members who can merge to the target branch."
+msgstr ""
+
+msgid "Allow public access to pipelines and job details, including output logs and artifacts"
msgstr ""
msgid "Allow rendering of PlantUML diagrams in Asciidoc documents."
@@ -263,31 +364,37 @@ msgstr ""
msgid "Allows you to add and manage Kubernetes clusters."
msgstr ""
-msgid "Also called \"Issuer\" or \"Relying party trust identifier\""
+msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
msgstr ""
-msgid "Also called \"Relying party service URL\" or \"Reply URL\""
+msgid "An error occured creating the new branch."
msgstr ""
-msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to connect."
+msgid "An error occured whilst loading all the files."
msgstr ""
-msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
+msgid "An error occured whilst loading the file content."
msgstr ""
-msgid "An error occurred previewing the blob"
+msgid "An error occured whilst loading the file."
msgstr ""
-msgid "An error occurred when toggling the notification subscription"
+msgid "An error occured whilst loading the merge request changes."
msgstr ""
-msgid "An error occurred when updating the issue weight"
+msgid "An error occured whilst loading the merge request version data."
msgstr ""
-msgid "An error occurred while adding approver"
+msgid "An error occured whilst loading the merge request."
msgstr ""
-msgid "An error occurred while detecting host keys"
+msgid "An error occurred previewing the blob"
+msgstr ""
+
+msgid "An error occurred when toggling the notification subscription"
+msgstr ""
+
+msgid "An error occurred while dismissing the alert. Refresh the page and try again."
msgstr ""
msgid "An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again."
@@ -305,10 +412,7 @@ msgstr ""
msgid "An error occurred while getting projects"
msgstr ""
-msgid "An error occurred while importing project"
-msgstr ""
-
-msgid "An error occurred while initializing path locks"
+msgid "An error occurred while importing project: ${details}"
msgstr ""
msgid "An error occurred while loading commits"
@@ -326,9 +430,6 @@ msgstr ""
msgid "An error occurred while making the request."
msgstr ""
-msgid "An error occurred while removing approver"
-msgstr ""
-
msgid "An error occurred while rendering KaTeX"
msgstr ""
@@ -341,9 +442,6 @@ msgstr ""
msgid "An error occurred while retrieving diff"
msgstr ""
-msgid "An error occurred while saving LDAP override status. Please try again."
-msgstr ""
-
msgid "An error occurred while saving assignees"
msgstr ""
@@ -353,9 +451,6 @@ msgstr ""
msgid "An error occurred. Please try again."
msgstr ""
-msgid "Any Label"
-msgstr ""
-
msgid "Appearance"
msgstr "Uiterlijk"
@@ -368,19 +463,19 @@ msgstr ""
msgid "April"
msgstr ""
-msgid "Archived project! Repository is read-only"
+msgid "Archived project! Repository and other project resources are read-only"
msgstr ""
msgid "Are you sure you want to delete this pipeline schedule?"
msgstr ""
-msgid "Are you sure you want to reset registration token?"
+msgid "Are you sure you want to remove this identity?"
msgstr ""
-msgid "Are you sure you want to reset the health check token?"
+msgid "Are you sure you want to reset registration token?"
msgstr ""
-msgid "Are you sure you want to unlock %{path_lock_path}?"
+msgid "Are you sure you want to reset the health check token?"
msgstr ""
msgid "Are you sure?"
@@ -389,7 +484,7 @@ msgstr ""
msgid "Artifacts"
msgstr ""
-msgid "Assertion consumer service URL"
+msgid "Ask your group maintainer to setup a group Runner."
msgstr ""
msgid "Assign custom color like #FF0000"
@@ -413,9 +508,15 @@ msgstr ""
msgid "Assigned to :name"
msgstr ""
+msgid "Assigned to me"
+msgstr ""
+
msgid "Assignee"
msgstr ""
+msgid "Assignee(s)"
+msgstr ""
+
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr ""
@@ -449,7 +550,10 @@ msgstr ""
msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
msgstr ""
-msgid "AutoDevOps|Auto DevOps (Beta)"
+msgid "Auto-cancel redundant, pending pipelines"
+msgstr ""
+
+msgid "AutoDevOps|Auto DevOps"
msgstr ""
msgid "AutoDevOps|Auto DevOps documentation"
@@ -470,79 +574,109 @@ msgstr ""
msgid "AutoDevOps|add a Kubernetes cluster"
msgstr ""
-msgid "AutoDevOps|enable Auto DevOps (Beta)"
+msgid "AutoDevOps|enable Auto DevOps"
msgstr ""
msgid "Available"
msgstr ""
+msgid "Available group Runners : %{runners}"
+msgstr ""
+
+msgid "Available group Runners : %{runners}."
+msgstr ""
+
msgid "Avatar will be removed. Are you sure?"
msgstr ""
msgid "Average per day: %{average}"
msgstr ""
-msgid "Background Color"
+msgid "Background color"
msgstr ""
msgid "Background jobs"
msgstr ""
-msgid "Begin with the selected commit"
+msgid "Badges"
+msgstr ""
+
+msgid "Badges|A new badge was added."
+msgstr ""
+
+msgid "Badges|Add badge"
+msgstr ""
+
+msgid "Badges|Adding the badge failed, please check the entered URLs and try again."
+msgstr ""
+
+msgid "Badges|Badge image URL"
+msgstr ""
+
+msgid "Badges|Badge image preview"
+msgstr ""
+
+msgid "Badges|Delete badge"
+msgstr ""
+
+msgid "Badges|Delete badge?"
msgstr ""
-msgid "Billing"
+msgid "Badges|Deleting the badge failed, please try again."
msgstr ""
-msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan."
+msgid "Badges|Group Badge"
msgstr ""
-msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available."
+msgid "Badges|Link"
msgstr ""
-msgid "BillingPlans|Current plan"
+msgid "Badges|No badge image"
msgstr ""
-msgid "BillingPlans|Customer Support"
+msgid "Badges|No image to preview"
msgstr ""
-msgid "BillingPlans|Downgrade"
+msgid "Badges|Project Badge"
msgstr ""
-msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
+msgid "Badges|Reload badge image"
msgstr ""
-msgid "BillingPlans|Manage plan"
+msgid "Badges|Save changes"
msgstr ""
-msgid "BillingPlans|Please contact %{customer_support_link} in that case."
+msgid "Badges|Saving the badge failed, please check the entered URLs and try again."
msgstr ""
-msgid "BillingPlans|See all %{plan_name} features"
+msgid "Badges|The %{docsLinkStart}variables%{docsLinkEnd} GitLab supports: %{placeholders}"
msgstr ""
-msgid "BillingPlans|This group uses the plan associated with its parent group."
+msgid "Badges|The badge was deleted."
msgstr ""
-msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
+msgid "Badges|The badge was saved."
msgstr ""
-msgid "BillingPlans|Upgrade"
+msgid "Badges|This group has no badges"
msgstr ""
-msgid "BillingPlans|You are currently on the %{plan_link} plan."
+msgid "Badges|This project has no badges"
msgstr ""
-msgid "BillingPlans|frequently asked questions"
+msgid "Badges|Your badges"
msgstr ""
-msgid "BillingPlans|monthly"
+msgid "Begin with the selected commit"
msgstr ""
-msgid "BillingPlans|paid annually at %{price_per_year}"
+msgid "Below are examples of regex for existing tools:"
msgstr ""
-msgid "BillingPlans|per user"
+msgid "Boards"
+msgstr ""
+
+msgid "Branch %{branchName} was not found in this project's repository."
msgstr ""
msgid "Branch (%{branch_count})"
@@ -622,7 +756,7 @@ msgstr ""
msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
msgstr ""
-msgid "Branches|Only a project master or owner can delete a protected branch"
+msgid "Branches|Only a project maintainer or owner can delete a protected branch"
msgstr ""
msgid "Branches|Overview"
@@ -658,9 +792,6 @@ msgstr ""
msgid "Branches|Stale branches"
msgstr ""
-msgid "Branches|The branch could not be updated automatically because it has diverged from its upstream counterpart."
-msgstr ""
-
msgid "Branches|The default branch cannot be deleted"
msgstr ""
@@ -673,15 +804,9 @@ msgstr ""
msgid "Branches|To confirm, type %{branch_name_confirmation}:"
msgstr ""
-msgid "Branches|To discard the local changes and overwrite the branch with the upstream version, delete it here and choose 'Update Now' above."
-msgstr ""
-
msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
msgstr ""
-msgid "Branches|diverged from upstream"
-msgstr ""
-
msgid "Branches|merged"
msgstr ""
@@ -703,40 +828,79 @@ msgstr "Door bestanden bladeren"
msgid "Browse files"
msgstr "Door bestanden bladeren"
-msgid "Business"
-msgstr ""
-
msgid "ByAuthor|by"
msgstr "door"
msgid "CI / CD"
msgstr "CI / CD"
-msgid "CI/CD"
+msgid "CI / CD Settings"
msgstr ""
msgid "CI/CD configuration"
msgstr ""
-msgid "CI/CD for external repo"
+msgid "CI/CD settings"
+msgstr ""
+
+msgid "CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery."
+msgstr ""
+
+msgid "CICD|Auto DevOps"
+msgstr ""
+
+msgid "CICD|Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration."
+msgstr ""
+
+msgid "CICD|Automatic deployment to staging, manual deployment to production"
+msgstr ""
+
+msgid "CICD|Continuous deployment to production"
+msgstr ""
+
+msgid "CICD|Deployment strategy"
+msgstr ""
+
+msgid "CICD|Deployment strategy needs a domain name to work correctly."
+msgstr ""
+
+msgid "CICD|Disable Auto DevOps"
+msgstr ""
+
+msgid "CICD|Enable Auto DevOps"
+msgstr ""
+
+msgid "CICD|Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific %{ci_file}."
+msgstr ""
+
+msgid "CICD|Instance default (%{state})"
msgstr ""
msgid "CICD|Jobs"
msgstr ""
-msgid "Cancel"
-msgstr "Annuleren"
+msgid "CICD|Learn more about Auto DevOps"
+msgstr ""
-msgid "Cannot be merged automatically"
+msgid "CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project."
msgstr ""
-msgid "Cannot modify managed Kubernetes cluster"
+msgid "CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages."
msgstr ""
-msgid "Certificate fingerprint"
+msgid "Can't find HEAD commit for this branch"
msgstr ""
-msgid "Change Weight"
+msgid "Cancel"
+msgstr "Annuleren"
+
+msgid "Cancel this job"
+msgstr ""
+
+msgid "Cannot be merged automatically"
+msgstr ""
+
+msgid "Cannot modify managed Kubernetes cluster"
msgstr ""
msgid "Change this value to influence how frequently the GitLab UI polls for updates."
@@ -790,21 +954,18 @@ msgstr ""
msgid "Choose a branch/tag (e.g. %{master}) or enter a commit (e.g. %{sha}) to see what's changed or to create a merge request."
msgstr ""
-msgid "Choose file..."
+msgid "Choose any color."
msgstr ""
-msgid "Choose which groups you wish to synchronize to this secondary node."
+msgid "Choose between <code>clone</code> or <code>fetch</code> to get the recent application code"
msgstr ""
-msgid "Choose which repositories you want to connect and run CI/CD pipelines."
+msgid "Choose file..."
msgstr ""
msgid "Choose which repositories you want to import."
msgstr ""
-msgid "Choose which shards you wish to synchronize to this secondary node."
-msgstr ""
-
msgid "CiStatusLabel|canceled"
msgstr "geannuleerd"
@@ -874,21 +1035,12 @@ msgstr ""
msgid "CiVariable|All environments"
msgstr ""
-msgid "CiVariable|Create wildcard"
-msgstr ""
-
msgid "CiVariable|Error occured while saving variables"
msgstr ""
-msgid "CiVariable|New environment"
-msgstr ""
-
msgid "CiVariable|Protected"
msgstr ""
-msgid "CiVariable|Search environments"
-msgstr ""
-
msgid "CiVariable|Toggle protected"
msgstr ""
@@ -898,28 +1050,28 @@ msgstr ""
msgid "CircuitBreakerApiLink|circuitbreaker api"
msgstr ""
-msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
+msgid "Clear search input"
msgstr ""
-msgid "Click to expand text"
+msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Client authentication certificate"
+msgid "Click the <strong>Promote</strong> button in the top right corner to promote it to a group milestone."
msgstr ""
-msgid "Client authentication key"
+msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
msgstr ""
-msgid "Client authentication key password"
+msgid "Click to expand it."
msgstr ""
-msgid "Clone repository"
+msgid "Click to expand text"
msgstr ""
-msgid "Close"
+msgid "Clone repository"
msgstr ""
-msgid "Closed"
+msgid "Close"
msgstr ""
msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster"
@@ -937,6 +1089,12 @@ msgstr ""
msgid "ClusterIntegration|Advanced options on this Kubernetes cluster's integration"
msgstr ""
+msgid "ClusterIntegration|An error occured while trying to fetch project zones: %{error}"
+msgstr ""
+
+msgid "ClusterIntegration|An error occured while trying to fetch your projects: %{error}"
+msgstr ""
+
msgid "ClusterIntegration|Applications"
msgstr ""
@@ -967,6 +1125,9 @@ msgstr ""
msgid "ClusterIntegration|Copy Ingress IP Address to clipboard"
msgstr ""
+msgid "ClusterIntegration|Copy Jupyter Hostname to clipboard"
+msgstr ""
+
msgid "ClusterIntegration|Copy Kubernetes cluster name"
msgstr ""
@@ -982,7 +1143,7 @@ msgstr ""
msgid "ClusterIntegration|Create a new Kubernetes cluster on Google Kubernetes Engine right from GitLab"
msgstr ""
-msgid "ClusterIntegration|Create on GKE"
+msgid "ClusterIntegration|Create on Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
@@ -994,13 +1155,25 @@ msgstr ""
msgid "ClusterIntegration|Environment scope"
msgstr ""
+msgid "ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab's Google Kubernetes Engine Integration."
+msgstr ""
+
+msgid "ClusterIntegration|Fetching machine types"
+msgstr ""
+
+msgid "ClusterIntegration|Fetching projects"
+msgstr ""
+
+msgid "ClusterIntegration|Fetching zones"
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
msgid "ClusterIntegration|GitLab Runner"
msgstr ""
-msgid "ClusterIntegration|Google Cloud Platform project ID"
+msgid "ClusterIntegration|Google Cloud Platform project"
msgstr ""
msgid "ClusterIntegration|Google Kubernetes Engine"
@@ -1012,9 +1185,6 @@ msgstr ""
msgid "ClusterIntegration|Helm Tiller"
msgstr ""
-msgid "ClusterIntegration|In order to show the health of the cluster, we'll need to provision your cluster with Prometheus to collect the required data."
-msgstr ""
-
msgid "ClusterIntegration|Ingress"
msgstr ""
@@ -1024,9 +1194,6 @@ msgstr ""
msgid "ClusterIntegration|Install"
msgstr ""
-msgid "ClusterIntegration|Install Prometheus"
-msgstr ""
-
msgid "ClusterIntegration|Installed"
msgstr ""
@@ -1039,13 +1206,16 @@ msgstr ""
msgid "ClusterIntegration|Integration status"
msgstr ""
-msgid "ClusterIntegration|Kubernetes cluster"
+msgid "ClusterIntegration|Jupyter Hostname"
msgstr ""
-msgid "ClusterIntegration|Kubernetes cluster details"
+msgid "ClusterIntegration|JupyterHub"
msgstr ""
-msgid "ClusterIntegration|Kubernetes cluster health"
+msgid "ClusterIntegration|Kubernetes cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Kubernetes cluster details"
msgstr ""
msgid "ClusterIntegration|Kubernetes cluster integration"
@@ -1075,7 +1245,13 @@ msgstr ""
msgid "ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project"
msgstr ""
-msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
+msgid "ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}."
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{help_link_start}Kubernetes%{help_link_end}."
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}."
msgstr ""
msgid "ClusterIntegration|Learn more about environments"
@@ -1099,7 +1275,16 @@ msgstr ""
msgid "ClusterIntegration|More information"
msgstr ""
-msgid "ClusterIntegration|Multiple Kubernetes clusters are available in GitLab Enterprise Edition Premium and Ultimate"
+msgid "ClusterIntegration|No machine types matched your search"
+msgstr ""
+
+msgid "ClusterIntegration|No projects found"
+msgstr ""
+
+msgid "ClusterIntegration|No projects matched your search"
+msgstr ""
+
+msgid "ClusterIntegration|No zones matched your search"
msgstr ""
msgid "ClusterIntegration|Note:"
@@ -1114,9 +1299,6 @@ msgstr ""
msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
msgstr ""
-msgid "ClusterIntegration|Project ID"
-msgstr ""
-
msgid "ClusterIntegration|Project namespace"
msgstr ""
@@ -1129,6 +1311,9 @@ msgstr ""
msgid "ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration."
msgstr ""
+msgid "ClusterIntegration|Redeem up to $500 in free credit for Google Cloud Platform"
+msgstr ""
+
msgid "ClusterIntegration|Remove Kubernetes cluster integration"
msgstr ""
@@ -1144,19 +1329,37 @@ msgstr ""
msgid "ClusterIntegration|Save changes"
msgstr ""
+msgid "ClusterIntegration|Search machine types"
+msgstr ""
+
+msgid "ClusterIntegration|Search projects"
+msgstr ""
+
+msgid "ClusterIntegration|Search zones"
+msgstr ""
+
msgid "ClusterIntegration|Security"
msgstr ""
msgid "ClusterIntegration|See and edit the details for your Kubernetes cluster"
msgstr ""
-msgid "ClusterIntegration|See machine types"
+msgid "ClusterIntegration|Select machine type"
+msgstr ""
+
+msgid "ClusterIntegration|Select project"
msgstr ""
-msgid "ClusterIntegration|See your projects"
+msgid "ClusterIntegration|Select project and zone to choose machine type"
msgstr ""
-msgid "ClusterIntegration|See zones"
+msgid "ClusterIntegration|Select project to choose zone"
+msgstr ""
+
+msgid "ClusterIntegration|Select zone"
+msgstr ""
+
+msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
msgid "ClusterIntegration|Service token"
@@ -1189,6 +1392,9 @@ msgstr ""
msgid "ClusterIntegration|Token"
msgstr ""
+msgid "ClusterIntegration|Validating project billing status"
+msgstr ""
+
msgid "ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
@@ -1219,13 +1425,19 @@ msgstr ""
msgid "ClusterIntegration|properly configured"
msgstr ""
+msgid "ClusterIntegration|sign up"
+msgstr ""
+
msgid "Collapse"
msgstr ""
-msgid "Comment and resolve discussion"
+msgid "Collapse sidebar"
msgstr ""
-msgid "Comment and unresolve discussion"
+msgid "Comment & resolve discussion"
+msgstr ""
+
+msgid "Comment & unresolve discussion"
msgstr ""
msgid "Comments"
@@ -1292,6 +1504,9 @@ msgstr ""
msgid "Committed by"
msgstr "Gecommit door"
+msgid "Commit…"
+msgstr ""
+
msgid "Compare"
msgstr "Vergelijk"
@@ -1340,6 +1555,9 @@ msgstr ""
msgid "Configure limits for web and API requests."
msgstr ""
+msgid "Configure push mirrors."
+msgstr ""
+
msgid "Configure storage path and circuit breaker settings."
msgstr ""
@@ -1349,18 +1567,9 @@ msgstr ""
msgid "Connect"
msgstr ""
-msgid "Connect all repositories"
-msgstr ""
-
msgid "Connect repositories from GitHub"
msgstr ""
-msgid "Connect your external repositories, and CI/CD pipelines will run for new commits. A GitLab project will be created with only CI/CD features enabled."
-msgstr ""
-
-msgid "Connecting..."
-msgstr ""
-
msgid "Container Registry"
msgstr ""
@@ -1406,9 +1615,18 @@ 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 "ContainerRegistry|You can also use a %{deploy_token} for read-only access to the registry images."
+msgstr ""
+
+msgid "Continue"
+msgstr ""
+
msgid "Continuous Integration and Deployment"
msgstr ""
+msgid "Contribute to GitLab"
+msgstr ""
+
msgid "Contribution"
msgstr ""
@@ -1430,28 +1648,28 @@ msgstr ""
msgid "ContributorsPage|Please wait a moment, this page will automatically refresh when ready."
msgstr ""
-msgid "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
+msgid "Copy URL to clipboard"
msgstr ""
-msgid "Control the maximum concurrency of repository backfill for this secondary node"
+msgid "Copy branch name to clipboard"
msgstr ""
-msgid "Copy SSH public key to clipboard"
+msgid "Copy command to clipboard"
msgstr ""
-msgid "Copy URL to clipboard"
+msgid "Copy commit SHA to clipboard"
msgstr ""
-msgid "Copy branch name to clipboard"
+msgid "Copy file name to clipboard"
msgstr ""
-msgid "Copy command to clipboard"
+msgid "Copy file path to clipboard"
msgstr ""
-msgid "Copy commit SHA to clipboard"
+msgid "Copy reference to clipboard"
msgstr ""
-msgid "Copy reference to clipboard"
+msgid "Copy to clipboard"
msgstr ""
msgid "Create"
@@ -1472,15 +1690,15 @@ msgstr ""
msgid "Create branch"
msgstr ""
+msgid "Create commit"
+msgstr ""
+
msgid "Create directory"
msgstr "Maak map aan"
msgid "Create empty repository"
msgstr ""
-msgid "Create epic"
-msgstr ""
-
msgid "Create file"
msgstr ""
@@ -1523,13 +1741,10 @@ msgstr ""
msgid "CreateTokenToCloneLink|create a personal access token"
msgstr ""
-msgid "Creates a new branch from %{branchName}"
-msgstr ""
-
-msgid "Creates a new branch from %{branchName} and re-directs to create a new merge request"
+msgid "Created"
msgstr ""
-msgid "Creating epic"
+msgid "Created by me"
msgstr ""
msgid "Cron Timezone"
@@ -1538,16 +1753,19 @@ msgstr ""
msgid "Cron syntax"
msgstr ""
-msgid "Current node"
+msgid "CurrentUser|Profile"
msgstr ""
-msgid "Custom notification events"
+msgid "CurrentUser|Settings"
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}."
+msgid "Custom CI config path"
msgstr ""
-msgid "Customize colors"
+msgid "Custom notification events"
+msgstr ""
+
+msgid "Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notification_link}."
msgstr ""
msgid "Cycle Analytics"
@@ -1586,7 +1804,7 @@ msgstr ""
msgid "December"
msgstr ""
-msgid "Default classification label"
+msgid "Decline and sign out"
msgstr ""
msgid "Define a custom pattern with cron syntax"
@@ -1595,6 +1813,9 @@ msgstr ""
msgid "Delete"
msgstr ""
+msgid "Delete list"
+msgstr ""
+
msgid "Deploy"
msgid_plural "Deploys"
msgstr[0] ""
@@ -1603,549 +1824,525 @@ msgstr[1] ""
msgid "Deploy Keys"
msgstr ""
-msgid "Description"
+msgid "DeployKeys|+%{count} others"
msgstr ""
-msgid "Description templates allow you to define context-specific templates for issue and merge request description fields for your project."
+msgid "DeployKeys|Current project"
msgstr ""
-msgid "Details"
+msgid "DeployKeys|Deploy key"
msgstr ""
-msgid "Diffs|No file name available"
+msgid "DeployKeys|Enabled deploy keys"
msgstr ""
-msgid "Directory name"
+msgid "DeployKeys|Error enabling deploy key"
msgstr ""
-msgid "Disable"
+msgid "DeployKeys|Error getting deploy keys"
msgstr ""
-msgid "Discard draft"
+msgid "DeployKeys|Error removing deploy key"
msgstr ""
-msgid "Discover GitLab Geo."
+msgid "DeployKeys|Expand %{count} other projects"
msgstr ""
-msgid "Dismiss Cycle Analytics introduction box"
+msgid "DeployKeys|Loading deploy keys"
msgstr ""
-msgid "Dismiss Merge Request promotion"
+msgid "DeployKeys|No deploy keys found. Create one with the form above."
msgstr ""
-msgid "Documentation for popular identity providers"
+msgid "DeployKeys|Privately accessible deploy keys"
msgstr ""
-msgid "Don't show again"
+msgid "DeployKeys|Project usage"
msgstr ""
-msgid "Done"
+msgid "DeployKeys|Publicly accessible deploy keys"
msgstr ""
-msgid "Download"
+msgid "DeployKeys|Read access only"
msgstr ""
-msgid "Download tar"
+msgid "DeployKeys|Write access allowed"
msgstr ""
-msgid "Download tar.bz2"
+msgid "DeployKeys|You are going to remove this deploy key. Are you sure?"
msgstr ""
-msgid "Download tar.gz"
+msgid "DeployTokens|Active Deploy Tokens (%{active_tokens})"
msgstr ""
-msgid "Download zip"
-msgstr "Download zip"
-
-msgid "DownloadArtifacts|Download"
+msgid "DeployTokens|Add a deploy token"
msgstr ""
-msgid "DownloadCommit|Email Patches"
+msgid "DeployTokens|Allows read-only access to the registry images"
msgstr ""
-msgid "DownloadCommit|Plain Diff"
+msgid "DeployTokens|Allows read-only access to the repository"
msgstr ""
-msgid "DownloadSource|Download"
+msgid "DeployTokens|Copy deploy token to clipboard"
msgstr ""
-msgid "Downvotes"
+msgid "DeployTokens|Copy username to clipboard"
msgstr ""
-msgid "Due date"
+msgid "DeployTokens|Create deploy token"
msgstr ""
-msgid "During this process, you’ll be asked for URLs from GitLab’s side. Use the URLs shown below."
+msgid "DeployTokens|Created"
msgstr ""
-msgid "Edit"
+msgid "DeployTokens|Deploy Tokens"
msgstr ""
-msgid "Edit Pipeline Schedule %{id}"
+msgid "DeployTokens|Deploy tokens allow read-only access to your repository and registry images."
msgstr ""
-msgid "Edit files in the editor and commit changes here"
+msgid "DeployTokens|Expires"
msgstr ""
-msgid "Editing"
+msgid "DeployTokens|Name"
msgstr ""
-msgid "Elasticsearch"
+msgid "DeployTokens|Pick a name for the application, and we'll give you a unique deploy token."
msgstr ""
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "DeployTokens|Revoke"
msgstr ""
-msgid "Email"
+msgid "DeployTokens|Revoke %{name}"
msgstr ""
-msgid "Emails"
+msgid "DeployTokens|Scopes"
msgstr ""
-msgid "Enable"
-msgstr ""
-
-msgid "Enable Auto DevOps"
-msgstr ""
-
-msgid "Enable SAML authentication for this group"
-msgstr ""
-
-msgid "Enable Sentry for error reporting and logging."
-msgstr ""
-
-msgid "Enable and configure InfluxDB metrics."
-msgstr ""
-
-msgid "Enable and configure Prometheus metrics."
-msgstr ""
-
-msgid "Enable classification control using an external service"
+msgid "DeployTokens|This action cannot be undone."
msgstr ""
-msgid "Enable or disable version check and usage ping."
+msgid "DeployTokens|This project has no active Deploy Tokens."
msgstr ""
-msgid "Enable reCAPTCHA or Akismet and set IP limits."
+msgid "DeployTokens|Use this token as a password. Make sure you save it - you won't be able to access it again."
msgstr ""
-msgid "Enable the Performance Bar for a given group."
+msgid "DeployTokens|Use this username as a login."
msgstr ""
-msgid "Enabled"
+msgid "DeployTokens|Username"
msgstr ""
-msgid "Environments|An error occurred while fetching the environments."
+msgid "DeployTokens|You are about to revoke"
msgstr ""
-msgid "Environments|An error occurred while making the request."
+msgid "DeployTokens|Your New Deploy Token"
msgstr ""
-msgid "Environments|Commit"
+msgid "DeployTokens|Your new project deploy token has been created."
msgstr ""
-msgid "Environments|Deployment"
+msgid "Deprioritize label"
msgstr ""
-msgid "Environments|Environment"
+msgid "Description"
msgstr ""
-msgid "Environments|Environments"
+msgid "Details"
msgstr ""
-msgid "Environments|Job"
+msgid "Diffs|No file name available"
msgstr ""
-msgid "Environments|New environment"
+msgid "Diffs|Something went wrong while fetching diff lines."
msgstr ""
-msgid "Environments|No deployments yet"
+msgid "Directory name"
msgstr ""
-msgid "Environments|Open"
+msgid "Disable"
msgstr ""
-msgid "Environments|Re-deploy"
+msgid "Disable for this project"
msgstr ""
-msgid "Environments|Read more about environments"
+msgid "Disable group Runners"
msgstr ""
-msgid "Environments|Rollback"
+msgid "Discard changes"
msgstr ""
-msgid "Environments|Show all"
+msgid "Discard draft"
msgstr ""
-msgid "Environments|Updated"
+msgid "Dismiss Cycle Analytics introduction box"
msgstr ""
-msgid "Environments|You don't have any environments right now."
+msgid "Domain"
msgstr ""
-msgid "Epic will be removed! Are you sure?"
+msgid "Don't show again"
msgstr ""
-msgid "Epics"
+msgid "Done"
msgstr ""
-msgid "Epics Roadmap"
+msgid "Download"
msgstr ""
-msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
+msgid "Download tar"
msgstr ""
-msgid "Error Reporting and Logging"
+msgid "Download tar.bz2"
msgstr ""
-msgid "Error checking branch data. Please try again."
+msgid "Download tar.gz"
msgstr ""
-msgid "Error committing changes. Please try again."
-msgstr ""
+msgid "Download zip"
+msgstr "Download zip"
-msgid "Error creating epic"
+msgid "DownloadArtifacts|Download"
msgstr ""
-msgid "Error fetching contributors data."
+msgid "DownloadCommit|Email Patches"
msgstr ""
-msgid "Error fetching labels."
+msgid "DownloadCommit|Plain Diff"
msgstr ""
-msgid "Error fetching network graph."
+msgid "DownloadSource|Download"
msgstr ""
-msgid "Error fetching refs"
+msgid "Downvotes"
msgstr ""
-msgid "Error fetching usage ping data."
+msgid "Due date"
msgstr ""
-msgid "Error occurred when toggling the notification subscription"
+msgid "Each Runner can be in one of the following states:"
msgstr ""
-msgid "Error saving label update."
+msgid "Edit"
msgstr ""
-msgid "Error updating status for all todos."
+msgid "Edit Label"
msgstr ""
-msgid "Error updating todo status."
+msgid "Edit Pipeline Schedule %{id}"
msgstr ""
-msgid "EventFilterBy|Filter by all"
+msgid "Edit files in the editor and commit changes here"
msgstr ""
-msgid "EventFilterBy|Filter by comments"
+msgid "Edit identity for %{user_name}"
msgstr ""
-msgid "EventFilterBy|Filter by issue events"
+msgid "Email"
msgstr ""
-msgid "EventFilterBy|Filter by merge events"
+msgid "Email patch"
msgstr ""
-msgid "EventFilterBy|Filter by push events"
+msgid "Emails"
msgstr ""
-msgid "EventFilterBy|Filter by team"
+msgid "Embed"
msgstr ""
-msgid "Every day (at 4:00am)"
+msgid "Enable"
msgstr ""
-msgid "Every month (on the 1st at 4:00am)"
+msgid "Enable Auto DevOps"
msgstr ""
-msgid "Every week (Sundays at 4:00am)"
+msgid "Enable Sentry for error reporting and logging."
msgstr ""
-msgid "Expand"
+msgid "Enable and configure InfluxDB metrics."
msgstr ""
-msgid "Explore projects"
+msgid "Enable and configure Prometheus metrics."
msgstr ""
-msgid "Explore public groups"
+msgid "Enable for this project"
msgstr ""
-msgid "External Classification Policy Authorization"
+msgid "Enable group Runners"
msgstr ""
-msgid "External authentication"
+msgid "Enable or disable certain group features and choose access levels."
msgstr ""
-msgid "External authorization denied access to this project"
+msgid "Enable or disable version check and usage ping."
msgstr ""
-msgid "External authorization request timeout"
+msgid "Enable reCAPTCHA or Akismet and set IP limits."
msgstr ""
-msgid "ExternalAuthorizationService|Classification Label"
+msgid "Enable the Performance Bar for a given group."
msgstr ""
-msgid "ExternalAuthorizationService|Classification label"
+msgid "Ends at (UTC)"
msgstr ""
-msgid "ExternalAuthorizationService|When no classification label is set the default label `%{default_label}` will be used."
+msgid "Environments"
msgstr ""
-msgid "Failed"
+msgid "Environments|An error occurred while fetching the environments."
msgstr ""
-msgid "Failed Jobs"
+msgid "Environments|An error occurred while making the request."
msgstr ""
-msgid "Failed to change the owner"
+msgid "Environments|Commit"
msgstr ""
-msgid "Failed to remove issue from board, please try again."
+msgid "Environments|Deployment"
msgstr ""
-msgid "Failed to remove the pipeline schedule"
+msgid "Environments|Environment"
msgstr ""
-msgid "Failed to update issues, please try again."
+msgid "Environments|Environments"
msgstr ""
-msgid "Feb"
+msgid "Environments|Job"
msgstr ""
-msgid "February"
+msgid "Environments|New environment"
msgstr ""
-msgid "Fields on this page are now uneditable, you can configure"
+msgid "Environments|No deployments yet"
msgstr ""
-msgid "File name"
+msgid "Environments|Open"
msgstr ""
-msgid "Files"
+msgid "Environments|Re-deploy"
msgstr ""
-msgid "Files (%{human_size})"
+msgid "Environments|Read more about environments"
msgstr ""
-msgid "Fill in the fields below, turn on <strong>%{enable_label}</strong>, and press <strong>%{save_changes}</strong>"
+msgid "Environments|Rollback"
msgstr ""
-msgid "Filter by commit message"
+msgid "Environments|Show all"
msgstr ""
-msgid "Find by path"
+msgid "Environments|Updated"
msgstr ""
-msgid "Find file"
+msgid "Environments|You don't have any environments right now."
msgstr ""
-msgid "Finished"
+msgid "Error Reporting and Logging"
msgstr ""
-msgid "FirstPushedBy|First"
+msgid "Error committing changes. Please try again."
msgstr ""
-msgid "FirstPushedBy|pushed by"
+msgid "Error fetching contributors data."
msgstr ""
-msgid "Font Color"
+msgid "Error fetching job trace"
msgstr ""
-msgid "Footer message"
+msgid "Error fetching labels."
msgstr ""
-msgid "Fork"
-msgid_plural "Forks"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "ForkedFromProjectPath|Forked from"
+msgid "Error fetching network graph."
msgstr ""
-msgid "ForkedFromProjectPath|Forked from %{project_name} (deleted)"
+msgid "Error fetching refs"
msgstr ""
-msgid "Forking in progress"
+msgid "Error fetching usage ping data."
msgstr ""
-msgid "Format"
+msgid "Error loading branch data. Please try again."
msgstr ""
-msgid "From %{provider_title}"
+msgid "Error loading last commit."
msgstr ""
-msgid "From issue creation until deploy to production"
+msgid "Error loading merge requests."
msgstr ""
-msgid "From merge request merge until deploy to production"
+msgid "Error loading project data. Please try again."
msgstr ""
-msgid "From the Kubernetes cluster details view, install Runner from the applications list"
+msgid "Error occurred when toggling the notification subscription"
msgstr ""
-msgid "GPG Keys"
+msgid "Error saving label update."
msgstr ""
-msgid "Generate a default set of labels"
+msgid "Error updating status for all todos."
msgstr ""
-msgid "Geo Nodes"
+msgid "Error updating todo status."
msgstr ""
-msgid "Geo allows you to replicate your GitLab instance to other geographical locations."
+msgid "Estimated"
msgstr ""
-msgid "GeoNodeSyncStatus|Node is failing or broken."
+msgid "EventFilterBy|Filter by all"
msgstr ""
-msgid "GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage."
+msgid "EventFilterBy|Filter by comments"
msgstr ""
-msgid "GeoNodes|Checksummed"
+msgid "EventFilterBy|Filter by issue events"
msgstr ""
-msgid "GeoNodes|Database replication lag:"
+msgid "EventFilterBy|Filter by merge events"
msgstr ""
-msgid "GeoNodes|Disabling a node stops the sync process. Are you sure?"
+msgid "EventFilterBy|Filter by push events"
msgstr ""
-msgid "GeoNodes|Does not match the primary storage configuration"
+msgid "EventFilterBy|Filter by team"
msgstr ""
-msgid "GeoNodes|Failed"
+msgid "Every day (at 4:00am)"
msgstr ""
-msgid "GeoNodes|Full"
+msgid "Every month (on the 1st at 4:00am)"
msgstr ""
-msgid "GeoNodes|GitLab version does not match the primary node version"
+msgid "Every week (Sundays at 4:00am)"
msgstr ""
-msgid "GeoNodes|GitLab version:"
+msgid "Expand"
msgstr ""
-msgid "GeoNodes|Health status:"
+msgid "Expand all"
msgstr ""
-msgid "GeoNodes|Last event ID processed by cursor:"
+msgid "Expand sidebar"
msgstr ""
-msgid "GeoNodes|Last event ID seen from primary:"
+msgid "Explore projects"
msgstr ""
-msgid "GeoNodes|Loading nodes"
+msgid "Explore public groups"
msgstr ""
-msgid "GeoNodes|Local Attachments:"
+msgid "Failed"
msgstr ""
-msgid "GeoNodes|Local LFS objects:"
+msgid "Failed Jobs"
msgstr ""
-msgid "GeoNodes|Local job artifacts:"
+msgid "Failed to change the owner"
msgstr ""
-msgid "GeoNodes|New node"
+msgid "Failed to check related branches."
msgstr ""
-msgid "GeoNodes|Node Authentication was successfully repaired."
+msgid "Failed to remove issue from board, please try again."
msgstr ""
-msgid "GeoNodes|Node was successfully removed."
+msgid "Failed to remove the pipeline schedule"
msgstr ""
-msgid "GeoNodes|Not checksummed"
+msgid "Failed to update issues, please try again."
msgstr ""
-msgid "GeoNodes|Out of sync"
+msgid "Failure"
msgstr ""
-msgid "GeoNodes|Removing a node stops the sync process. Are you sure?"
+msgid "Faster as it re-uses the project workspace (falling back to clone if it doesn't exist)"
msgstr ""
-msgid "GeoNodes|Replication slot WAL:"
+msgid "Feb"
msgstr ""
-msgid "GeoNodes|Replication slots:"
+msgid "February"
msgstr ""
-msgid "GeoNodes|Repositories checksummed:"
+msgid "Fields on this page are now uneditable, you can configure"
msgstr ""
-msgid "GeoNodes|Repositories:"
+msgid "Files"
msgstr ""
-msgid "GeoNodes|Repository checksums verified:"
+msgid "Files (%{human_size})"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "Filter by commit message"
msgstr ""
-msgid "GeoNodes|Something went wrong while changing node status"
+msgid "Find by path"
msgstr ""
-msgid "GeoNodes|Something went wrong while removing node"
+msgid "Find file"
msgstr ""
-msgid "GeoNodes|Something went wrong while repairing node"
+msgid "Finished"
msgstr ""
-msgid "GeoNodes|Storage config:"
+msgid "FirstPushedBy|First"
msgstr ""
-msgid "GeoNodes|Sync settings:"
+msgid "FirstPushedBy|pushed by"
msgstr ""
-msgid "GeoNodes|Synced"
+msgid "For internal projects, any logged in user can view pipelines and access job details (output logs and artifacts)"
msgstr ""
-msgid "GeoNodes|Unused slots"
+msgid "For private projects, any member (guest or higher) can view pipelines and access job details (output logs and artifacts)"
msgstr ""
-msgid "GeoNodes|Unverified"
+msgid "For public projects, anyone can view pipelines and access job details (output logs and artifacts)"
msgstr ""
-msgid "GeoNodes|Used slots"
-msgstr ""
+msgid "Fork"
+msgid_plural "Forks"
+msgstr[0] ""
+msgstr[1] ""
-msgid "GeoNodes|Verified"
+msgid "ForkedFromProjectPath|Forked from"
msgstr ""
-msgid "GeoNodes|Wiki checksums verified:"
+msgid "ForkedFromProjectPath|Forked from %{project_name} (deleted)"
msgstr ""
-msgid "GeoNodes|Wikis checksummed:"
+msgid "Forking in progress"
msgstr ""
-msgid "GeoNodes|Wikis:"
+msgid "Format"
msgstr ""
-msgid "GeoNodes|You have configured Geo nodes using an insecure HTTP connection. We recommend the use of HTTPS."
+msgid "Found errors in your .gitlab-ci.yml:"
msgstr ""
-msgid "Geo|All projects"
+msgid "From %{provider_title}"
msgstr ""
-msgid "Geo|File sync capacity"
+msgid "From issue creation until deploy to production"
msgstr ""
-msgid "Geo|Groups to synchronize"
+msgid "From merge request merge until deploy to production"
msgstr ""
-msgid "Geo|Projects in certain groups"
+msgid "From the Kubernetes cluster details view, install Runner from the applications list"
msgstr ""
-msgid "Geo|Projects in certain storage shards"
+msgid "GPG Keys"
msgstr ""
-msgid "Geo|Repository sync capacity"
+msgid "General"
msgstr ""
-msgid "Geo|Select groups to replicate."
+msgid "General pipelines"
msgstr ""
-msgid "Geo|Shards to synchronize"
+msgid "Generate a default set of labels"
msgstr ""
msgid "Git repository URL"
@@ -2157,6 +2354,9 @@ msgstr ""
msgid "Git storage health information has been reset"
msgstr ""
+msgid "Git strategy for pipelines"
+msgstr ""
+
msgid "Git version"
msgstr ""
@@ -2166,21 +2366,24 @@ msgstr ""
msgid "GitLab CI Linter has been moved"
msgstr ""
-msgid "GitLab Geo"
+msgid "GitLab Group Runners can execute code for all the projects in this group."
msgstr ""
msgid "GitLab Runner section"
msgstr ""
-msgid "GitLab single sign on URL"
-msgstr ""
-
msgid "Gitaly"
msgstr ""
msgid "Gitaly Servers"
msgstr ""
+msgid "Gitaly|Address"
+msgstr ""
+
+msgid "Go Back"
+msgstr ""
+
msgid "Go back"
msgstr ""
@@ -2196,22 +2399,19 @@ msgstr ""
msgid "Got it!"
msgstr ""
-msgid "GroupRoadmap|Epics let you manage your portfolio of projects more efficiently and with less effort"
+msgid "Graph"
msgstr ""
-msgid "GroupRoadmap|From %{dateWord}"
+msgid "Group CI/CD settings"
msgstr ""
-msgid "GroupRoadmap|Loading roadmap"
+msgid "Group ID"
msgstr ""
-msgid "GroupRoadmap|Something went wrong while fetching epics"
+msgid "Group Runners"
msgstr ""
-msgid "GroupRoadmap|To view the roadmap, add a planned start or finish date to one of your epics in this group or its subgroups. Only epics in the past 3 months and the next 3 months are shown &ndash; from %{startDate} to %{endDate}."
-msgstr ""
-
-msgid "GroupRoadmap|Until %{dateWord}"
+msgid "Group maintainers can register group runners in the %{link}"
msgstr ""
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
@@ -2238,6 +2438,9 @@ msgstr ""
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
+msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
+msgstr ""
+
msgid "GroupsEmptyState|A group is a collection of several projects."
msgstr ""
@@ -2277,12 +2480,6 @@ msgstr ""
msgid "GroupsTree|Sorry, no groups or projects matched your search"
msgstr ""
-msgid "Have your users email"
-msgstr ""
-
-msgid "Header message"
-msgstr ""
-
msgid "Health Check"
msgstr ""
@@ -2315,19 +2512,49 @@ msgid_plural "Hide values"
msgstr[0] ""
msgstr[1] ""
+msgid "Hide whitespace changes"
+msgstr ""
+
msgid "History"
msgstr ""
msgid "Housekeeping successfully started"
msgstr ""
-msgid "Identity provider single sign on URL"
+msgid "I accept the %{terms_link}"
+msgstr ""
+
+msgid "I accept the|Terms of Service and Privacy Policy"
+msgstr ""
+
+msgid "ID"
+msgstr ""
+
+msgid "IDE|Commit"
+msgstr ""
+
+msgid "IDE|Edit"
+msgstr ""
+
+msgid "IDE|Go back"
+msgstr ""
+
+msgid "IDE|Open in file view"
+msgstr ""
+
+msgid "IDE|Review"
+msgstr ""
+
+msgid "Identifier"
msgstr ""
-msgid "If enabled, access to projects will be validated on an external service using their classification label."
+msgid "Identities"
msgstr ""
-msgid "If using GitHub, you’ll see pipeline statuses on GitHub for your commits and pull requests. %{more_info_link}"
+msgid "If disabled, the access level will depend on the user's permissions in the project."
+msgstr ""
+
+msgid "If enabled"
msgstr ""
msgid "If you already have files you can push them using the %{link_to_cli} below."
@@ -2336,6 +2563,15 @@ msgstr ""
msgid "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>."
msgstr ""
+msgid "ImageDiffViewer|2-up"
+msgstr ""
+
+msgid "ImageDiffViewer|Onion skin"
+msgstr ""
+
+msgid "ImageDiffViewer|Swipe"
+msgstr ""
+
msgid "Import"
msgstr ""
@@ -2351,16 +2587,10 @@ msgstr ""
msgid "Import repository"
msgstr ""
-msgid "ImportButtons|Connect repositories from"
+msgid "Include a Terms of Service agreement and Privacy Policy that all users must accept."
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."
+msgid "Inline"
msgstr ""
msgid "Install Runner on Kubernetes"
@@ -2369,17 +2599,15 @@ msgstr ""
msgid "Install a Runner compatible with GitLab CI"
msgstr ""
-msgid "Instance"
-msgid_plural "Instances"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "Instance does not support multiple Kubernetes clusters"
msgstr ""
msgid "Integrations"
msgstr ""
+msgid "Integrations Settings"
+msgstr ""
+
msgid "Interested parties can even contribute by pushing commits if they want to."
msgstr ""
@@ -2395,7 +2623,7 @@ msgstr ""
msgid "Introducing Cycle Analytics"
msgstr ""
-msgid "Issue board focus mode"
+msgid "Issue Board"
msgstr ""
msgid "Issue events"
@@ -2404,9 +2632,6 @@ msgstr ""
msgid "IssueBoards|Board"
msgstr ""
-msgid "IssueBoards|Boards"
-msgstr ""
-
msgid "Issues"
msgstr ""
@@ -2419,6 +2644,12 @@ msgstr ""
msgid "January"
msgstr ""
+msgid "Job"
+msgstr ""
+
+msgid "Job has been erased"
+msgstr ""
+
msgid "Jobs"
msgstr ""
@@ -2461,6 +2692,9 @@ msgstr ""
msgid "Kubernetes service integration has been deprecated. %{deprecated_message_content} your Kubernetes clusters using the new <a href=\"%{url}\"/>Kubernetes Clusters</a> page"
msgstr ""
+msgid "LFS"
+msgstr ""
+
msgid "LFSStatus|Disabled"
msgstr ""
@@ -2470,6 +2704,9 @@ msgstr ""
msgid "Label"
msgstr ""
+msgid "Label actions dropdown"
+msgstr ""
+
msgid "LabelSelect|%{firstLabelName} +%{remainingLabelCount} more"
msgstr ""
@@ -2485,6 +2722,9 @@ msgstr ""
msgid "Labels can be applied to issues and merge requests to categorize them."
msgstr ""
+msgid "Labels can be applied to issues and merge requests."
+msgstr ""
+
msgid "Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>"
msgstr ""
@@ -2520,6 +2760,9 @@ msgstr ""
msgid "LastPushEvent|at"
msgstr ""
+msgid "Latest changes"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -2544,9 +2787,6 @@ msgstr ""
msgid "Leave project"
msgstr ""
-msgid "License"
-msgstr ""
-
msgid "List"
msgstr ""
@@ -2556,6 +2796,9 @@ msgstr ""
msgid "Loading the GitLab IDE..."
msgstr ""
+msgid "Loading..."
+msgstr ""
+
msgid "Lock"
msgstr ""
@@ -2565,21 +2808,18 @@ msgstr ""
msgid "Lock not found"
msgstr ""
-msgid "Locked"
+msgid "Lock to current projects"
msgstr ""
-msgid "Locked Files"
+msgid "Locked"
msgstr ""
-msgid "Locks give the ability to lock specific file or folder."
+msgid "Locked to current projects"
msgstr ""
msgid "Login"
msgstr ""
-msgid "Make everyone on your team more productive regardless of their location. GitLab Geo creates read-only mirrors of your GitLab instance so you can reduce the time it takes to clone and fetch large repos."
-msgstr ""
-
msgid "Manage all notifications"
msgstr ""
@@ -2592,16 +2832,16 @@ msgstr ""
msgid "Manage project labels"
msgstr ""
-msgid "Manage your group’s membership while adding another level of security with SAML."
-msgstr ""
-
msgid "Mar"
msgstr ""
msgid "March"
msgstr ""
-msgid "Mark done"
+msgid "Mark todo as done"
+msgstr ""
+
+msgid "Markdown enabled"
msgstr ""
msgid "Maximum git storage failures"
@@ -2616,7 +2856,7 @@ msgstr ""
msgid "Members"
msgstr ""
-msgid "Members will be forwarded here when signing in to your group. Get this from your identity provider, where it can also be called \"SSO Service Location\", \"SAML Token Issuance Endpoint\", or \"SAML 2.0/W-Federation URL\"."
+msgid "Merge Request:"
msgstr ""
msgid "Merge Requests"
@@ -2628,91 +2868,46 @@ msgstr ""
msgid "Merge request"
msgstr ""
-msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
-msgstr ""
-
-msgid "Merged"
-msgstr ""
-
-msgid "Messages"
-msgstr ""
-
-msgid "Metrics - Influx"
-msgstr ""
-
-msgid "Metrics - Prometheus"
-msgstr ""
-
-msgid "Metrics|Business"
-msgstr ""
-
-msgid "Metrics|Create metric"
+msgid "Merge requests"
msgstr ""
-msgid "Metrics|Edit metric"
-msgstr ""
-
-msgid "Metrics|For grouping similar metrics"
-msgstr ""
-
-msgid "Metrics|Label of the chart's vertical axis. Usually the type of the unit being charted. The horizontal axis (X-axis) always represents time."
-msgstr ""
-
-msgid "Metrics|Legend label (optional)"
-msgstr ""
-
-msgid "Metrics|Must be a valid PromQL query."
-msgstr ""
-
-msgid "Metrics|Name"
-msgstr ""
-
-msgid "Metrics|New metric"
-msgstr ""
-
-msgid "Metrics|Prometheus Query Documentation"
-msgstr ""
-
-msgid "Metrics|Query"
-msgstr ""
-
-msgid "Metrics|Response"
+msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
msgstr ""
-msgid "Metrics|System"
+msgid "MergeRequests|Resolve this discussion in a new issue"
msgstr ""
-msgid "Metrics|Type"
+msgid "MergeRequests|Saving the comment failed"
msgstr ""
-msgid "Metrics|Unit label"
+msgid "MergeRequests|Toggle comments for this file"
msgstr ""
-msgid "Metrics|Used as a title for the chart"
+msgid "MergeRequests|Updating discussions failed"
msgstr ""
-msgid "Metrics|Used if the query returns a single series. If it returns multiple series, their legend labels will be picked up from the response."
+msgid "MergeRequests|View file @ %{commitId}"
msgstr ""
-msgid "Metrics|Y-axis label"
+msgid "MergeRequests|View replaced file @ %{commitId}"
msgstr ""
-msgid "Metrics|e.g. HTTP requests"
+msgid "Merged"
msgstr ""
-msgid "Metrics|e.g. Requests/second"
+msgid "Messages"
msgstr ""
-msgid "Metrics|e.g. Throughput"
+msgid "Metrics - Influx"
msgstr ""
-msgid "Metrics|e.g. rate(http_requests_total[5m])"
+msgid "Metrics - Prometheus"
msgstr ""
-msgid "Metrics|e.g. req/sec"
+msgid "Milestone"
msgstr ""
-msgid "Milestone"
+msgid "Milestones"
msgstr ""
msgid "Milestones|Delete milestone"
@@ -2733,9 +2928,6 @@ msgstr ""
msgid "Milestones|Promote Milestone"
msgstr ""
-msgid "Milestones|This action cannot be reversed."
-msgstr ""
-
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr ""
@@ -2748,7 +2940,7 @@ msgstr ""
msgid "Monitoring"
msgstr ""
-msgid "More info"
+msgid "More actions"
msgstr ""
msgid "More information"
@@ -2763,12 +2955,30 @@ msgstr ""
msgid "Move issue"
msgstr ""
-msgid "Multiple issue boards"
+msgid "Name"
msgstr ""
msgid "Name new label"
msgstr ""
+msgid "Name your individual key via a title"
+msgstr ""
+
+msgid "Nav|Help"
+msgstr ""
+
+msgid "Nav|Home"
+msgstr ""
+
+msgid "Nav|Sign In / Register"
+msgstr ""
+
+msgid "Nav|Sign out and sign in with a different account"
+msgstr ""
+
+msgid "New Identity"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "Nieuwe issue"
@@ -2780,6 +2990,9 @@ msgstr ""
msgid "New Kubernetes cluster"
msgstr ""
+msgid "New Label"
+msgstr ""
+
msgid "New Pipeline Schedule"
msgstr ""
@@ -2792,15 +3005,15 @@ msgstr ""
msgid "New directory"
msgstr ""
-msgid "New epic"
-msgstr ""
-
msgid "New file"
msgstr ""
msgid "New group"
msgstr ""
+msgid "New identity"
+msgstr ""
+
msgid "New issue"
msgstr ""
@@ -2810,6 +3023,9 @@ msgstr ""
msgid "New merge request"
msgstr ""
+msgid "New pipelines will cancel older, pending pipelines on the same branch"
+msgstr ""
+
msgid "New project"
msgstr ""
@@ -2825,7 +3041,7 @@ msgstr ""
msgid "New tag"
msgstr ""
-msgid "No Label"
+msgid "No"
msgstr ""
msgid "No assignee"
@@ -2846,7 +3062,16 @@ msgstr ""
msgid "No file chosen"
msgstr ""
-msgid "No labels created yet."
+msgid "No files found"
+msgstr ""
+
+msgid "No files found."
+msgstr ""
+
+msgid "No merge requests found"
+msgstr ""
+
+msgid "No messages were logged"
msgstr ""
msgid "No repository"
@@ -2879,15 +3104,9 @@ msgstr ""
msgid "Note that the master branch is automatically protected. %{link_to_protected_branches}"
msgstr ""
-msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow connecting repositories without generating a Personal Access Token."
-msgstr ""
-
msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
msgstr ""
-msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow connecting repositories without generating a Personal Access Token."
-msgstr ""
-
msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
msgstr ""
@@ -2963,9 +3182,6 @@ msgstr ""
msgid "Number of access attempts"
msgstr ""
-msgid "OK"
-msgstr ""
-
msgid "Oct"
msgstr ""
@@ -2975,19 +3191,16 @@ msgstr ""
msgid "OfSearchInADropdown|Filter"
msgstr ""
-msgid "Once imported, repositories can be mirrored over SSH. Read more %{ssh_link}"
-msgstr ""
-
msgid "Online IDE integration settings."
msgstr ""
-msgid "Only project members can comment."
+msgid "Only comments from the following commit are shown below"
msgstr ""
-msgid "Open"
+msgid "Only project members can comment."
msgstr ""
-msgid "Opened"
+msgid "Open in Xcode"
msgstr ""
msgid "OpenedNDaysAgo|Opened"
@@ -2996,9 +3209,18 @@ msgstr "Geopend"
msgid "Opens in a new window"
msgstr ""
+msgid "Operations"
+msgstr ""
+
msgid "Options"
msgstr ""
+msgid "Or you can choose one of the suggested colors below"
+msgstr ""
+
+msgid "Other Labels"
+msgstr ""
+
msgid "Otherwise it is recommended you start with one of the options below."
msgstr ""
@@ -3032,12 +3254,27 @@ msgstr ""
msgid "Password"
msgstr ""
+msgid "Paste your public SSH key, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'. Don't use your private SSH key."
+msgstr ""
+
+msgid "Pause"
+msgstr ""
+
msgid "Pending"
msgstr ""
+msgid "Per job. If a job passes this threshold, it will be marked as failed"
+msgstr ""
+
+msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgstr ""
+
msgid "Performance optimization"
msgstr ""
+msgid "Permissions"
+msgstr ""
+
msgid "Personal Access Token"
msgstr ""
@@ -3053,7 +3290,7 @@ msgstr ""
msgid "Pipeline Schedules"
msgstr ""
-msgid "Pipeline quota"
+msgid "Pipeline triggers"
msgstr ""
msgid "PipelineCharts|Failed:"
@@ -3152,10 +3389,22 @@ msgstr ""
msgid "Pipelines|This project is not currently set up to run pipelines."
msgstr ""
-msgid "Pipeline|Retry pipeline"
+msgid "Pipeline|Create for"
+msgstr ""
+
+msgid "Pipeline|Create pipeline"
+msgstr ""
+
+msgid "Pipeline|Existing branch name or tag"
+msgstr ""
+
+msgid "Pipeline|Run Pipeline"
msgstr ""
-msgid "Pipeline|Retry pipeline #%{pipelineId}?"
+msgid "Pipeline|Search branches"
+msgstr ""
+
+msgid "Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default."
msgstr ""
msgid "Pipeline|Stop pipeline"
@@ -3164,7 +3413,7 @@ msgstr ""
msgid "Pipeline|Stop pipeline #%{pipelineId}?"
msgstr ""
-msgid "Pipeline|You’re about to retry pipeline %{pipelineId}."
+msgid "Pipeline|Variables"
msgstr ""
msgid "Pipeline|You’re about to stop pipeline %{pipelineId}."
@@ -3182,19 +3431,25 @@ msgstr ""
msgid "Pipeline|with stages"
msgstr ""
+msgid "Plain diff"
+msgstr ""
+
msgid "PlantUML"
msgstr ""
msgid "Play"
msgstr ""
-msgid "Please <a href=%{link_to_billing} target=\"_blank\" rel=\"noopener noreferrer\">enable billing for one of your projects to be able to create a Kubernetes cluster</a>, then try again."
+msgid "Please accept the Terms of Service before continuing."
+msgstr ""
+
+msgid "Please select at least one filter to see results"
msgstr ""
msgid "Please solve the reCAPTCHA"
msgstr ""
-msgid "Please wait while we connect to your repository. Refresh at will."
+msgid "Please try again"
msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
@@ -3203,7 +3458,19 @@ msgstr ""
msgid "Preferences"
msgstr ""
-msgid "Primary"
+msgid "Preferences|Navigation theme"
+msgstr ""
+
+msgid "Prioritize"
+msgstr ""
+
+msgid "Prioritize label"
+msgstr ""
+
+msgid "Prioritized Labels"
+msgstr ""
+
+msgid "Prioritized label"
msgstr ""
msgid "Private - Project access must be granted explicitly to each user."
@@ -3221,6 +3488,12 @@ msgstr ""
msgid "Profiles|Account scheduled for removal."
msgstr ""
+msgid "Profiles|Change username"
+msgstr ""
+
+msgid "Profiles|Current path: %{path}"
+msgstr ""
+
msgid "Profiles|Delete Account"
msgstr ""
@@ -3239,9 +3512,21 @@ msgstr ""
msgid "Profiles|Invalid username"
msgstr ""
+msgid "Profiles|Path"
+msgstr ""
+
msgid "Profiles|Type your %{confirmationValue} to confirm:"
msgstr ""
+msgid "Profiles|Update username"
+msgstr ""
+
+msgid "Profiles|Username change failed - %{message}"
+msgstr ""
+
+msgid "Profiles|Username successfully changed"
+msgstr ""
+
msgid "Profiles|You don't have access to delete this user."
msgstr ""
@@ -3260,6 +3545,12 @@ msgstr ""
msgid "Programming languages used in this repository"
msgstr ""
+msgid "Progress"
+msgstr ""
+
+msgid "Project"
+msgstr ""
+
msgid "Project '%{project_name}' is in the process of being deleted."
msgstr ""
@@ -3272,6 +3563,9 @@ msgstr ""
msgid "Project '%{project_name}' was successfully updated."
msgstr ""
+msgid "Project Badges"
+msgstr ""
+
msgid "Project access must be granted explicitly to each user."
msgstr ""
@@ -3299,30 +3593,6 @@ msgstr ""
msgid "ProjectActivityRSS|Subscribe"
msgstr ""
-msgid "ProjectCreationLevel|Allowed to create projects"
-msgstr ""
-
-msgid "ProjectCreationLevel|Default project creation protection"
-msgstr ""
-
-msgid "ProjectCreationLevel|Developers + Masters"
-msgstr ""
-
-msgid "ProjectCreationLevel|Masters"
-msgstr ""
-
-msgid "ProjectCreationLevel|No one"
-msgstr ""
-
-msgid "ProjectFeature|Disabled"
-msgstr ""
-
-msgid "ProjectFeature|Everyone with access"
-msgstr ""
-
-msgid "ProjectFeature|Only team members"
-msgstr ""
-
msgid "ProjectFileTree|Name"
msgstr ""
@@ -3332,27 +3602,6 @@ msgstr ""
msgid "ProjectLifecycle|Stage"
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 ""
@@ -3377,6 +3626,9 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr ""
+msgid "PrometheusDashboard|Time"
+msgstr ""
+
msgid "PrometheusService|%{exporters} with %{metrics} were found"
msgstr ""
@@ -3398,18 +3650,9 @@ msgstr ""
msgid "PrometheusService|Common metrics"
msgstr ""
-msgid "PrometheusService|Common metrics are automatically monitored based on a library of metrics from popular exporters."
-msgstr ""
-
-msgid "PrometheusService|Custom metrics"
-msgstr ""
-
msgid "PrometheusService|Finding and configuring metrics..."
msgstr ""
-msgid "PrometheusService|Finding custom metrics..."
-msgstr ""
-
msgid "PrometheusService|Install Prometheus on clusters"
msgstr ""
@@ -3422,13 +3665,13 @@ msgstr ""
msgid "PrometheusService|Metrics"
msgstr ""
-msgid "PrometheusService|Missing environment variable"
+msgid "PrometheusService|Metrics are automatically configured and monitored based on a library of metrics from popular exporters."
msgstr ""
-msgid "PrometheusService|More information"
+msgid "PrometheusService|Missing environment variable"
msgstr ""
-msgid "PrometheusService|New metric"
+msgid "PrometheusService|More information"
msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
@@ -3437,9 +3680,6 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
-msgstr ""
-
msgid "PrometheusService|Time-series monitoring service"
msgstr ""
@@ -3455,22 +3695,28 @@ msgstr ""
msgid "Promote"
msgstr ""
-msgid "Promote to Group Label"
+msgid "Promote these project milestones into a group milestone."
msgstr ""
msgid "Promote to Group Milestone"
msgstr ""
+msgid "Promote to group label"
+msgstr ""
+
msgid "Protip:"
msgstr ""
+msgid "Provider"
+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"
+msgid "Public pipelines"
msgstr ""
msgid "Push events"
@@ -3482,10 +3728,10 @@ msgstr ""
msgid "Push to create a project"
msgstr ""
-msgid "PushRule|Committer restriction"
+msgid "Quick actions can be used in the issues description and comment boxes."
msgstr ""
-msgid "Quick actions can be used in the issues description and comment boxes."
+msgid "Re-deploy"
msgstr ""
msgid "Read more"
@@ -3497,16 +3743,16 @@ msgstr ""
msgid "Real-time features"
msgstr ""
-msgid "RefSwitcher|Branches"
+msgid "Reference:"
msgstr ""
-msgid "RefSwitcher|Tags"
+msgid "Register / Sign In"
msgstr ""
-msgid "Reference:"
+msgid "Register and see your runners for this group."
msgstr ""
-msgid "Register / Sign In"
+msgid "Register and see your runners for this project."
msgstr ""
msgid "Registry"
@@ -3539,28 +3785,28 @@ msgstr ""
msgid "Remove"
msgstr ""
-msgid "Remove avatar"
+msgid "Remove Runner"
msgstr ""
-msgid "Remove project"
+msgid "Remove avatar"
msgstr ""
-msgid "Repair authentication"
+msgid "Remove priority"
msgstr ""
-msgid "Repo by URL"
+msgid "Remove project"
msgstr ""
msgid "Repository"
msgstr ""
-msgid "Repository has no locks."
+msgid "Repository Settings"
msgstr ""
msgid "Repository maintenance"
msgstr ""
-msgid "Repository mirror settings"
+msgid "Repository mirror"
msgstr ""
msgid "Repository storage"
@@ -3569,6 +3815,9 @@ msgstr ""
msgid "Request Access"
msgstr ""
+msgid "Require all users to accept Terms of Service and Privacy Policy when they access GitLab."
+msgstr ""
+
msgid "Reset git storage health information"
msgstr ""
@@ -3578,10 +3827,25 @@ msgstr ""
msgid "Reset runners registration token"
msgstr ""
+msgid "Resolve all discussions in new issue"
+msgstr ""
+
+msgid "Resolve conflicts on source branch"
+msgstr ""
+
msgid "Resolve discussion"
msgstr ""
-msgid "Response"
+msgid "Resume"
+msgstr ""
+
+msgid "Retry"
+msgstr ""
+
+msgid "Retry this job"
+msgstr ""
+
+msgid "Retry verification"
msgstr ""
msgid "Reveal value"
@@ -3595,7 +3859,7 @@ msgstr ""
msgid "Revert this merge request"
msgstr ""
-msgid "Review the process for configuring service providers in your identity provider — in this case, GitLab is the \"service provider\" or \"relying party\"."
+msgid "Review"
msgstr ""
msgid "Reviewing"
@@ -3604,28 +3868,31 @@ msgstr ""
msgid "Reviewing (merge request !%{mergeRequestId})"
msgstr ""
-msgid "Roadmap"
+msgid "Rollback"
msgstr ""
-msgid "Run CI/CD pipelines for external repositories"
+msgid "Runner token"
msgstr ""
msgid "Runners"
msgstr ""
-msgid "Running"
+msgid "Runners API"
msgstr ""
-msgid "SAML Single Sign On"
+msgid "Runners can be placed on separate users, servers, and even on your local machine."
msgstr ""
-msgid "SAML Single Sign On Settings"
+msgid "Running"
msgstr ""
-msgid "SHA1 fingerprint of the SAML token signing certificate. Get this from your identity provider, where it can also be called \"Thumbprint\"."
+msgid "SSH Keys"
msgstr ""
-msgid "SSH Keys"
+msgid "SSL Verification"
+msgstr ""
+
+msgid "Save"
msgstr ""
msgid "Save changes"
@@ -3649,15 +3916,30 @@ msgstr ""
msgid "Scheduling Pipelines"
msgstr ""
-msgid "Scoped issue boards"
+msgid "Scroll to bottom"
+msgstr ""
+
+msgid "Scroll to top"
msgstr ""
msgid "Search"
msgstr ""
+msgid "Search branches"
+msgstr ""
+
msgid "Search branches and tags"
msgstr ""
+msgid "Search files"
+msgstr ""
+
+msgid "Search for projects, issues, etc."
+msgstr ""
+
+msgid "Search merge requests"
+msgstr ""
+
msgid "Search milestones"
msgstr ""
@@ -3673,13 +3955,13 @@ msgstr ""
msgid "Seconds to wait for a storage access attempt"
msgstr ""
-msgid "Secret variables"
+msgid "Select"
msgstr ""
-msgid "Security report"
+msgid "Select Archive Format"
msgstr ""
-msgid "Select Archive Format"
+msgid "Select a namespace to fork the project"
msgstr ""
msgid "Select a timezone"
@@ -3694,10 +3976,19 @@ msgstr ""
msgid "Select branch/tag"
msgstr ""
-msgid "Select target branch"
+msgid "Select project"
+msgstr ""
+
+msgid "Select project and zone to choose machine type"
+msgstr ""
+
+msgid "Select project to choose zone"
msgstr ""
-msgid "Selective synchronization"
+msgid "Select source branch"
+msgstr ""
+
+msgid "Select target branch"
msgstr ""
msgid "Send email"
@@ -3715,9 +4006,6 @@ msgstr ""
msgid "Service Templates"
msgstr ""
-msgid "Service URL"
-msgstr ""
-
msgid "Session expiration, projects limit and attachment size."
msgstr ""
@@ -3742,9 +4030,6 @@ msgstr ""
msgid "Set up Koding"
msgstr ""
-msgid "Set up assertions/attributes/claims (email, first_name, last_name) and NameID according to %{docsLinkStart}the documentation %{icon}%{docsLinkEnd}"
-msgstr ""
-
msgid "SetPasswordToCloneLink|set a password"
msgstr ""
@@ -3754,19 +4039,22 @@ msgstr ""
msgid "Setup a specific Runner automatically"
msgstr ""
-msgid "Share the <strong>%{sso_label}</strong> with members so they can sign in to your group through your identity provider"
+msgid "Share"
+msgstr ""
+
+msgid "Shared Runners"
msgstr ""
-msgid "SharedRunnersMinutesSettings|By resetting the pipeline minutes for this namespace, the currently used minutes will be set to zero."
+msgid "Show command"
msgstr ""
-msgid "SharedRunnersMinutesSettings|Reset pipeline minutes"
+msgid "Show complete raw log"
msgstr ""
-msgid "SharedRunnersMinutesSettings|Reset used pipeline minutes"
+msgid "Show latest version"
msgstr ""
-msgid "Show command"
+msgid "Show latest version of the diff"
msgstr ""
msgid "Show parent pages"
@@ -3775,21 +4063,18 @@ msgstr ""
msgid "Show parent subgroups"
msgstr ""
+msgid "Show whitespace changes"
+msgstr ""
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] ""
msgstr[1] ""
-msgid "Sidebar|Change weight"
-msgstr ""
-
-msgid "Sidebar|No"
-msgstr ""
-
-msgid "Sidebar|None"
+msgid "Side-by-side"
msgstr ""
-msgid "Sidebar|Weight"
+msgid "Sign out"
msgstr ""
msgid "Sign-in restrictions"
@@ -3801,7 +4086,7 @@ msgstr ""
msgid "Size and domain settings for static websites"
msgstr ""
-msgid "Slack application"
+msgid "Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job"
msgstr ""
msgid "Snippets"
@@ -3813,13 +4098,13 @@ msgstr ""
msgid "Something went wrong on our end."
msgstr ""
-msgid "Something went wrong when toggling the button"
+msgid "Something went wrong on our end. Please try again!"
msgstr ""
-msgid "Something went wrong while fetching Dependency Scanning."
+msgid "Something went wrong when toggling the button"
msgstr ""
-msgid "Something went wrong while fetching SAST."
+msgid "Something went wrong while closing the %{issuable}. Please try again later"
msgstr ""
msgid "Something went wrong while fetching the projects."
@@ -3828,6 +4113,12 @@ msgstr ""
msgid "Something went wrong while fetching the registry list."
msgstr ""
+msgid "Something went wrong while reopening the %{issuable}. Please try again later"
+msgstr ""
+
+msgid "Something went wrong while resolving this discussion. Please try again."
+msgstr ""
+
msgid "Something went wrong. Please try again."
msgstr ""
@@ -3873,9 +4164,6 @@ msgstr ""
msgid "SortOptions|Least popular"
msgstr ""
-msgid "SortOptions|Less weight"
-msgstr ""
-
msgid "SortOptions|Milestone"
msgstr ""
@@ -3885,9 +4173,6 @@ msgstr ""
msgid "SortOptions|Milestone due soon"
msgstr ""
-msgid "SortOptions|More weight"
-msgstr ""
-
msgid "SortOptions|Most popular"
msgstr ""
@@ -3927,9 +4212,6 @@ msgstr ""
msgid "SortOptions|Start soon"
msgstr ""
-msgid "SortOptions|Weight"
-msgstr ""
-
msgid "Source"
msgstr ""
@@ -3948,9 +4230,36 @@ msgstr ""
msgid "Spam and Anti-bot Protection"
msgstr ""
+msgid "Specific Runners"
+msgstr ""
+
msgid "Specify the following URL during the Runner setup:"
msgstr ""
+msgid "Squash commits"
+msgstr ""
+
+msgid "Stage"
+msgstr ""
+
+msgid "Stage & Commit"
+msgstr ""
+
+msgid "Stage all changes"
+msgstr ""
+
+msgid "Stage changes"
+msgstr ""
+
+msgid "Staged"
+msgstr ""
+
+msgid "Staged %{type}"
+msgstr ""
+
+msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
+msgstr ""
+
msgid "StarProject|Star"
msgstr ""
@@ -3972,12 +4281,15 @@ msgstr ""
msgid "Started"
msgstr ""
-msgid "State your message to activate"
+msgid "Starts at (UTC)"
msgstr ""
msgid "Status"
msgstr ""
+msgid "Stop this environment"
+msgstr ""
+
msgid "Stopped"
msgstr ""
@@ -3987,16 +4299,19 @@ msgstr ""
msgid "Subgroups"
msgstr ""
-msgid "Switch branch/tag"
+msgid "Subscribe"
msgstr ""
-msgid "System"
+msgid "Subscribe at group level"
msgstr ""
-msgid "System Hooks"
+msgid "Subscribe at project level"
msgstr ""
-msgid "System header and footer:"
+msgid "Switch branch/tag"
+msgstr ""
+
+msgid "System Hooks"
msgstr ""
msgid "Tag (%{tag_count})"
@@ -4007,6 +4322,9 @@ msgstr[1] ""
msgid "Tags"
msgstr ""
+msgid "Tags:"
+msgstr ""
+
msgid "TagsPage|Browse commits"
msgstr ""
@@ -4070,7 +4388,7 @@ msgstr ""
msgid "TagsPage|Use git tag command to add a new one:"
msgstr ""
-msgid "TagsPage|Write your release notes or drag files here..."
+msgid "TagsPage|Write your release notes or drag files here…"
msgstr ""
msgid "TagsPage|protected"
@@ -4085,19 +4403,19 @@ msgstr ""
msgid "Team"
msgstr ""
-msgid "Thanks! Don't show me this again"
+msgid "Terms of Service Agreement and Privacy Policy"
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."
+msgid "Terms of Service and Privacy Policy"
msgstr ""
-msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project"
+msgid "Test coverage parsing"
msgstr ""
-msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
+msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project"
msgstr ""
-msgid "The X509 Certificate to use when mutual TLS is required to communicate with the external authorization service. If left blank, the server certificate is still validated when accessing over HTTPS."
+msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
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."
@@ -4106,9 +4424,6 @@ msgstr ""
msgid "The collection of events added to the data gathered for that stage."
msgstr ""
-msgid "The connection will time out after %{timeout}. For repositories that take longer, use a clone/push combination."
-msgstr ""
-
msgid "The fork relationship has been removed."
msgstr ""
@@ -4127,7 +4442,7 @@ 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 passphrase required to decrypt the private key. This is optional and the value is encrypted at rest."
+msgid "The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>"
msgstr ""
msgid "The phase of the development lifecycle."
@@ -4136,9 +4451,6 @@ msgstr ""
msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit."
msgstr ""
-msgid "The private key to use when a client certificate is provided. This value is encrypted at rest."
-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 ""
@@ -4160,7 +4472,7 @@ msgstr ""
msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request."
msgstr ""
-msgid "The roadmap shows the progress of your epics along a timeline"
+msgid "The secure token used by the Runner to checkout the project"
msgstr ""
msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time."
@@ -4187,13 +4499,19 @@ msgstr ""
msgid "There are no issues to show"
msgstr ""
+msgid "There are no labels yet"
+msgstr ""
+
msgid "There are no merge requests to show"
msgstr ""
msgid "There are problems accessing Git storage: "
msgstr ""
-msgid "There was an error loading results"
+msgid "There was an error loading jobs"
+msgstr ""
+
+msgid "There was an error loading latest pipeline"
msgstr ""
msgid "There was an error loading users activity calendar."
@@ -4214,12 +4532,21 @@ msgstr ""
msgid "There was an error when unsubscribing from this label."
msgstr ""
-msgid "This board\\'s scope is reduced"
+msgid "They can be managed using the %{link}."
+msgstr ""
+
+msgid "This GitLab instance does not provide any shared Runners yet. Instance administrators can register shared Runners in the admin area."
+msgstr ""
+
+msgid "This diff is collapsed."
msgstr ""
msgid "This directory"
msgstr ""
+msgid "This group does not provide any group Runners yet."
+msgstr ""
+
msgid "This is a confidential issue."
msgstr ""
@@ -4241,6 +4568,15 @@ msgstr ""
msgid "This job depends on upstream jobs that need to succeed in order for this job to be triggered"
msgstr ""
+msgid "This job does not have a trace."
+msgstr ""
+
+msgid "This job has been canceled"
+msgstr ""
+
+msgid "This job has been skipped"
+msgstr ""
+
msgid "This job has not been triggered yet"
msgstr ""
@@ -4259,19 +4595,28 @@ msgstr ""
msgid "This merge request is locked."
msgstr ""
+msgid "This option is disabled while you still have unstaged changes"
+msgstr ""
+
msgid "This page is unavailable because you are not allowed to read information across multiple projects."
msgstr ""
+msgid "This page will be removed in a future release."
+msgstr ""
+
msgid "This project"
msgstr ""
+msgid "This project does not belong to a group and can therefore not make use of group Runners."
+msgstr ""
+
msgid "This repository"
msgstr ""
-msgid "This will delete the custom metric, Are you sure?"
+msgid "This source diff could not be displayed because it is too large."
msgstr ""
-msgid "Those emails automatically become issues (with the comments becoming the email conversation) listed here."
+msgid "This user has no identities"
msgstr ""
msgid "Time before an issue gets scheduled"
@@ -4283,10 +4628,10 @@ msgstr ""
msgid "Time between merge request creation and merge/close"
msgstr ""
-msgid "Time between updates and capacity settings."
+msgid "Time remaining"
msgstr ""
-msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
+msgid "Time spent"
msgstr ""
msgid "Time tracking"
@@ -4310,6 +4655,9 @@ msgstr ""
msgid "Timeago|%s days remaining"
msgstr ""
+msgid "Timeago|%s hours ago"
+msgstr ""
+
msgid "Timeago|%s hours remaining"
msgstr ""
@@ -4325,6 +4673,9 @@ msgstr ""
msgid "Timeago|%s months remaining"
msgstr ""
+msgid "Timeago|%s seconds ago"
+msgstr ""
+
msgid "Timeago|%s seconds remaining"
msgstr ""
@@ -4340,46 +4691,43 @@ msgstr ""
msgid "Timeago|%s years remaining"
msgstr ""
-msgid "Timeago|1 day remaining"
-msgstr ""
-
-msgid "Timeago|1 hour remaining"
+msgid "Timeago|1 day ago"
msgstr ""
-msgid "Timeago|1 minute remaining"
+msgid "Timeago|1 day remaining"
msgstr ""
-msgid "Timeago|1 month remaining"
+msgid "Timeago|1 hour ago"
msgstr ""
-msgid "Timeago|1 week remaining"
+msgid "Timeago|1 hour remaining"
msgstr ""
-msgid "Timeago|1 year remaining"
+msgid "Timeago|1 minute ago"
msgstr ""
-msgid "Timeago|Past due"
+msgid "Timeago|1 minute remaining"
msgstr ""
-msgid "Timeago|a day ago"
+msgid "Timeago|1 month ago"
msgstr ""
-msgid "Timeago|a month ago"
+msgid "Timeago|1 month remaining"
msgstr ""
-msgid "Timeago|a week ago"
+msgid "Timeago|1 week ago"
msgstr ""
-msgid "Timeago|a year ago"
+msgid "Timeago|1 week remaining"
msgstr ""
-msgid "Timeago|about %s hours ago"
+msgid "Timeago|1 year ago"
msgstr ""
-msgid "Timeago|about a minute ago"
+msgid "Timeago|1 year remaining"
msgstr ""
-msgid "Timeago|about an hour ago"
+msgid "Timeago|Past due"
msgstr ""
msgid "Timeago|in %s days"
@@ -4421,10 +4769,13 @@ msgstr ""
msgid "Timeago|in 1 year"
msgstr ""
-msgid "Timeago|in a while"
+msgid "Timeago|just now"
msgstr ""
-msgid "Timeago|less than a minute ago"
+msgid "Timeago|right now"
+msgstr ""
+
+msgid "Timeout"
msgstr ""
msgid "Time|hr"
@@ -4443,19 +4794,10 @@ msgstr "s"
msgid "Tip:"
msgstr ""
-msgid "Title"
-msgstr ""
-
msgid "To GitLab"
msgstr ""
-msgid "To connect GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to connect."
-msgstr ""
-
-msgid "To connect GitHub repositories, you first need to authorize GitLab to access the list of your GitHub repositories:"
-msgstr ""
-
-msgid "To connect an SVN repository, check out %{svn_link}."
+msgid "To add an SSH key you need to %{generate_link_start}generate one%{link_end} or use an %{existing_link_start}existing key%{link_end}."
msgstr ""
msgid "To import GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
@@ -4467,19 +4809,19 @@ msgstr ""
msgid "To import an SVN repository, check out %{svn_link}."
msgstr ""
-msgid "To only use CI/CD features for an external repository, choose <strong>CI/CD for external repo</strong>."
+msgid "To start serving your jobs you can add Runners to your group"
msgstr ""
-msgid "To set up SAML authentication for your group through an identity provider like Azure, Okta, Onelogin, Ping Identity, or your custom SAML 2.0 provider:"
+msgid "To validate your GitLab CI configurations, go to 'CI/CD → Pipelines' inside your project, and click on the 'CI Lint' button."
msgstr ""
-msgid "To validate your GitLab CI configurations, go to 'CI/CD → Pipelines' inside your project, and click on the 'CI Lint' button."
+msgid "Todo"
msgstr ""
-msgid "To view the roadmap, add a planned start or finish date to one of your epics in this group or its subgroups. Only epics in the past 3 months and the next 3 months are shown."
+msgid "Toggle Sidebar"
msgstr ""
-msgid "Todo"
+msgid "Toggle discussion"
msgstr ""
msgid "Toggle sidebar"
@@ -4491,6 +4833,9 @@ msgstr ""
msgid "ToggleButton|Toggle Status: ON"
msgstr ""
+msgid "Too many changes to show."
+msgstr ""
+
msgid "Total Time"
msgstr ""
@@ -4500,22 +4845,19 @@ msgstr ""
msgid "Total: %{total}"
msgstr ""
-msgid "Track activity with Contribution Analytics."
-msgstr ""
-
-msgid "Track groups of issues that share a theme, across projects and milestones"
-msgstr ""
-
msgid "Track time with quick actions"
msgstr ""
msgid "Trigger this manual action"
msgstr ""
-msgid "Turn on Service Desk"
+msgid "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."
+msgstr ""
+
+msgid "Try again"
msgstr ""
-msgid "Unknown"
+msgid "Unable to load the diff. %{button_try_again}"
msgstr ""
msgid "Unlock"
@@ -4527,25 +4869,45 @@ msgstr ""
msgid "Unresolve discussion"
msgstr ""
+msgid "Unstage all changes"
+msgstr ""
+
+msgid "Unstage changes"
+msgstr ""
+
+msgid "Unstaged"
+msgstr ""
+
+msgid "Unstaged %{type}"
+msgstr ""
+
+msgid "Unstaged and staged %{type}"
+msgstr ""
+
msgid "Unstar"
msgstr ""
-msgid "Up to date"
+msgid "Unsubscribe"
msgstr ""
-msgid "Upgrade your plan to activate Advanced Global Search."
+msgid "Unsubscribe at group level"
msgstr ""
-msgid "Upgrade your plan to activate Contribution Analytics."
+msgid "Unsubscribe at project level"
msgstr ""
-msgid "Upgrade your plan to activate Group Webhooks."
+msgid "Unverified"
msgstr ""
-msgid "Upgrade your plan to activate Issue weight."
+msgid "Up to date"
msgstr ""
-msgid "Upgrade your plan to improve Issue boards."
+msgid "Update %{files}"
+msgid_plural "Update %{files} files"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Update your group name, description, avatar, and other general settings."
msgstr ""
msgid "Upload New File"
@@ -4566,7 +4928,7 @@ msgstr ""
msgid "Usage statistics"
msgstr ""
-msgid "Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab"
+msgid "Use group milestones to manage issues from multiple projects in the same milestone."
msgstr ""
msgid "Use the following registration token during setup:"
@@ -4575,10 +4937,13 @@ msgstr ""
msgid "Use your global notification setting"
msgstr ""
-msgid "Used by members to sign in to your group in GitLab"
+msgid "User and IP Rate Limits"
msgstr ""
-msgid "User and IP Rate Limits"
+msgid "Users"
+msgstr ""
+
+msgid "Variables"
msgstr ""
msgid "Variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. You can use variables for passwords, secret keys, or whatever you want."
@@ -4593,10 +4958,7 @@ msgstr ""
msgid "Various settings that affect GitLab performance."
msgstr ""
-msgid "View and edit lines"
-msgstr ""
-
-msgid "View epics list"
+msgid "Verified"
msgstr ""
msgid "View file @ "
@@ -4605,9 +4967,15 @@ msgstr ""
msgid "View group labels"
msgstr ""
+msgid "View jobs"
+msgstr ""
+
msgid "View labels"
msgstr ""
+msgid "View log"
+msgstr ""
+
msgid "View open merge request"
msgstr ""
@@ -4635,9 +5003,6 @@ msgstr ""
msgid "Want to see the data? Please ask an administrator for access."
msgstr ""
-msgid "We could not verify that one of your projects on GCP has billing enabled. Please try again."
-msgstr ""
-
msgid "We don't have enough data to show this stage."
msgstr ""
@@ -4650,13 +5015,10 @@ msgstr ""
msgid "Web terminal"
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."
+msgid "When a runner is locked, it cannot be assigned to other projects"
msgstr ""
-msgid "Weight"
-msgstr ""
-
-msgid "When leaving the URL blank, classification labels can still be specified whitout disabling cross project features or performing external authorization checks."
+msgid "When enabled, users cannot use GitLab until the terms have been accepted."
msgstr ""
msgid "Wiki"
@@ -4683,7 +5045,31 @@ msgstr ""
msgid "WikiEdit|There is already a page with the same title in that path."
msgstr ""
-msgid "WikiEmptyPageError|You are not allowed to create wiki pages"
+msgid "WikiEmptyIssueMessage|Suggest wiki improvement"
+msgstr ""
+
+msgid "WikiEmptyIssueMessage|You must be a project member in order to add wiki pages. If you have suggestions for how to improve the wiki for this project, consider opening an issue in the %{issues_link}."
+msgstr ""
+
+msgid "WikiEmptyIssueMessage|issue tracker"
+msgstr ""
+
+msgid "WikiEmpty|A wiki is where you can store all the details about your project. This can include why you've created it, its principles, how to use it, and so on."
+msgstr ""
+
+msgid "WikiEmpty|Create your first page"
+msgstr ""
+
+msgid "WikiEmpty|Suggest wiki improvement"
+msgstr ""
+
+msgid "WikiEmpty|The wiki lets you write documentation for your project"
+msgstr ""
+
+msgid "WikiEmpty|This project has no wiki pages"
+msgstr ""
+
+msgid "WikiEmpty|You must be a project member in order to add wiki pages."
msgstr ""
msgid "WikiHistoricalPage|This is an old version of this page."
@@ -4719,6 +5105,12 @@ msgstr ""
msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?"
msgstr ""
+msgid "WikiPageConfirmDelete|Delete page"
+msgstr ""
+
+msgid "WikiPageConfirmDelete|Delete page %{pageTitle}?"
+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 ""
@@ -4734,7 +5126,7 @@ msgstr ""
msgid "WikiPage|Page slug"
msgstr ""
-msgid "WikiPage|Write your content or drag files here..."
+msgid "WikiPage|Write your content or drag files here…"
msgstr ""
msgid "Wiki|Create Page"
@@ -4746,9 +5138,6 @@ msgstr ""
msgid "Wiki|Edit Page"
msgstr ""
-msgid "Wiki|Empty page"
-msgstr ""
-
msgid "Wiki|More Pages"
msgstr ""
@@ -4767,13 +5156,10 @@ 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 ""
-msgid "Write a commit message..."
+msgid "Yes"
msgstr ""
msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
@@ -4791,7 +5177,7 @@ msgstr ""
msgid "You are on a read-only GitLab instance."
msgstr ""
-msgid "You are on a secondary (read-only) Geo node. If you want to make any changes, you must visit the %{primary_node}."
+msgid "You can %{linkStart}view the blob%{linkEnd} instead."
msgstr ""
msgid "You can also create a project from the command line."
@@ -4800,6 +5186,9 @@ msgstr ""
msgid "You can also star a label to make it a priority label."
msgstr ""
+msgid "You can also test your .gitlab-ci.yml in the %{linkStart}Lint%{linkEnd}"
+msgstr ""
+
msgid "You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}"
msgstr ""
@@ -4812,28 +5201,31 @@ msgstr ""
msgid "You can only edit 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."
+msgid "You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}"
msgstr ""
msgid "You cannot write to this read-only GitLab instance."
msgstr ""
-msgid "You do not have the correct permissions to override the settings from the LDAP group sync."
+msgid "You do not have any assigned merge requests"
msgstr ""
msgid "You have no permissions"
msgstr ""
+msgid "You have not created any merge requests"
+msgstr ""
+
msgid "You have reached your project limit"
msgstr ""
-msgid "You must have master access to force delete a lock"
+msgid "You must accept our Terms of Service and privacy policy in order to register an account"
msgstr ""
-msgid "You must sign in to star a project"
+msgid "You must have maintainer access to force delete a lock"
msgstr ""
-msgid "You need a different license to enable FileLocks feature"
+msgid "You must sign in to star a project"
msgstr ""
msgid "You need permission."
@@ -4902,13 +5294,11 @@ msgstr ""
msgid "Your projects"
msgstr ""
-msgid "among other things"
+msgid "ago"
msgstr ""
-msgid "and %d fixed vulnerability"
-msgid_plural "and %d fixed vulnerabilities"
-msgstr[0] ""
-msgstr[1] ""
+msgid "among other things"
+msgstr ""
msgid "assign yourself"
msgstr ""
@@ -4916,136 +5306,36 @@ msgstr ""
msgid "branch name"
msgstr ""
-msgid "by"
-msgstr ""
-
-msgid "ciReport|%{type} detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|%{type} detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Code quality"
-msgstr ""
-
-msgid "ciReport|DAST detected no alerts by analyzing the review app"
-msgstr ""
-
-msgid "ciReport|Dependency scanning"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Failed to load %{reportName} report"
-msgstr ""
-
-msgid "ciReport|Fixed:"
-msgstr ""
-
-msgid "ciReport|Instances"
-msgstr ""
-
-msgid "ciReport|Learn more about whitelisting"
-msgstr ""
-
-msgid "ciReport|Loading %{reportName} report"
-msgstr ""
-
-msgid "ciReport|No changes to code quality"
-msgstr ""
-
-msgid "ciReport|No changes to performance metrics"
-msgstr ""
-
-msgid "ciReport|Performance metrics"
-msgstr ""
-
-msgid "ciReport|SAST"
-msgstr ""
-
-msgid "ciReport|SAST detected"
-msgstr ""
-
-msgid "ciReport|SAST detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|SAST detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|SAST:container no vulnerabilities were found"
-msgstr ""
-
-msgid "ciReport|Security scanning"
-msgstr ""
-
-msgid "ciReport|Security scanning failed loading any results"
-msgstr ""
-
-msgid "ciReport|Show complete code vulnerabilities report"
-msgstr ""
-
-msgid "ciReport|Unapproved vulnerabilities (red) can be marked as approved. %{helpLink}"
-msgstr ""
-
-msgid "ciReport|no vulnerabilities"
-msgstr ""
-
msgid "command line instructions"
msgstr ""
msgid "connecting"
msgstr ""
-msgid "could not read private key, is the passphrase correct?"
-msgstr ""
-
msgid "day"
msgid_plural "days"
msgstr[0] ""
msgstr[1] ""
-msgid "detected %d fixed vulnerability"
-msgid_plural "detected %d fixed vulnerabilities"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "detected %d new vulnerability"
-msgid_plural "detected %d new vulnerabilities"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "detected no vulnerabilities"
+msgid "deploy token"
msgstr ""
-msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
+msgid "disabled"
msgstr ""
-msgid "here"
-msgstr ""
-
-msgid "importing"
+msgid "enabled"
msgstr ""
-msgid "in progress"
-msgstr ""
-
-msgid "is invalid because there is downstream lock"
+msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
msgstr ""
-msgid "is invalid because there is upstream lock"
+msgid "for this project"
msgstr ""
-msgid "is not a valid X509 certificate."
+msgid "importing"
msgstr ""
-msgid "locked by %{path_lock_user_name} %{created_at}"
+msgid "latest version"
msgstr ""
msgid "merge request"
@@ -5065,28 +5355,7 @@ msgstr ""
msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage is %{emphasisStart} unchanged %{emphasisEnd} at %{memoryFrom}MB"
msgstr ""
-msgid "mrWidget|Add approval"
-msgstr ""
-
-msgid "mrWidget|Allows edits from maintainers"
-msgstr ""
-
-msgid "mrWidget|An error occured while removing your approval."
-msgstr ""
-
-msgid "mrWidget|An error occured while retrieving approval data for this merge request."
-msgstr ""
-
-msgid "mrWidget|An error occured while submitting your approval."
-msgstr ""
-
-msgid "mrWidget|Approve"
-msgstr ""
-
-msgid "mrWidget|Approved"
-msgstr ""
-
-msgid "mrWidget|Approved by"
+msgid "mrWidget|Allows commits from members who can merge to the target branch"
msgstr ""
msgid "mrWidget|Cancel automatic merge"
@@ -5113,6 +5382,9 @@ msgstr ""
msgid "mrWidget|Closes"
msgstr ""
+msgid "mrWidget|Create an issue to resolve them later"
+msgstr ""
+
msgid "mrWidget|Deployment statistics are not available currently"
msgstr ""
@@ -5167,9 +5439,6 @@ msgstr ""
msgid "mrWidget|Remove source branch"
msgstr ""
-msgid "mrWidget|Remove your approval"
-msgstr ""
-
msgid "mrWidget|Request to merge"
msgstr ""
@@ -5209,6 +5478,9 @@ msgstr ""
msgid "mrWidget|There are merge conflicts"
msgstr ""
+msgid "mrWidget|There are unresolved discussions. Please resolve these discussions"
+msgstr ""
+
msgid "mrWidget|This merge request failed to be merged automatically"
msgstr ""
@@ -5259,7 +5531,7 @@ msgstr ""
msgid "personal access token"
msgstr ""
-msgid "private key does not match certificate."
+msgid "remaining"
msgstr ""
msgid "remove due date"
@@ -5274,9 +5546,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "to help your contributors communicate effectively!"
-msgstr ""
-
msgid "username"
msgstr ""
@@ -5286,3 +5555,8 @@ msgstr ""
msgid "with %{additions} additions, %{deletions} deletions."
msgstr ""
+msgid "within %d minute "
+msgid_plural "within %d minutes "
+msgstr[0] ""
+msgstr[1] ""
+
diff --git a/locale/pl_PL/gitlab.po b/locale/pl_PL/gitlab.po
index 4ae57235f90..0daeb419e55 100644
--- a/locale/pl_PL/gitlab.po
+++ b/locale/pl_PL/gitlab.po
@@ -2,22 +2,26 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2018-04-04 19:35+0200\n"
-"PO-Revision-Date: 2018-04-05 03:36-0400\n"
+"POT-Creation-Date: 2018-07-01 16:35+1000\n"
+"PO-Revision-Date: 2018-07-01 11:01\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Polish\n"
"Language: pl_PL\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=4; plural=((n == 1) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || n%10 == 1 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 12 && n%100 <= 14)) ? 2 : 3));\n"
+"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
"X-Generator: crowdin.com\n"
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: pl\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-msgid " and"
-msgstr ""
+msgid "%d changed file"
+msgid_plural "%d changed files"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
msgid "%d commit"
msgid_plural "%d commits"
@@ -68,6 +72,20 @@ msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
+msgid "%d staged change"
+msgid_plural "%d staged changes"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "%d unstaged change"
+msgid_plural "%d unstaged changes"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
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] ""
@@ -88,12 +106,21 @@ msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
+msgid "%{filePath} deleted"
+msgstr ""
+
+msgid "%{group_docs_link_start}Groups%{group_docs_link_end} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects."
+msgstr ""
+
msgid "%{loadingIcon} Started"
msgstr ""
msgid "%{lock_path} is locked by GitLab User %{lock_user_id}"
msgstr ""
+msgid "%{nip_domain} can be used as an alternative to a custom domain."
+msgstr ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -106,6 +133,9 @@ msgstr ""
msgid "%{openOrClose} %{noteable}"
msgstr ""
+msgid "%{percent}%% complete"
+msgstr ""
+
msgid "%{storage_name}: failed storage access attempt on host:"
msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:"
msgstr[0] ""
@@ -116,15 +146,76 @@ msgstr[3] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{title} changes"
+msgstr ""
+
+msgid "%{unstaged} unstaged and %{staged} staged changes"
+msgstr ""
+
msgid "(checkout the %{link} for information on how to install it)."
msgstr ""
msgid "+ %{moreCount} more"
msgstr ""
+msgid "- Runner is active and can process any new jobs"
+msgstr ""
+
+msgid "- Runner is paused and will not receive any new jobs"
+msgstr ""
+
msgid "- show less"
msgstr ""
+msgid "1 %{type} addition"
+msgid_plural "%{count} %{type} additions"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "1 %{type} modification"
+msgid_plural "%{count} %{type} modifications"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "1 closed issue"
+msgid_plural "%d closed issues"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "1 closed merge request"
+msgid_plural "%d closed merge requests"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "1 merged merge request"
+msgid_plural "%d merged merge requests"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "1 open issue"
+msgid_plural "%d open issues"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "1 open merge request"
+msgid_plural "%d open merge requests"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] ""
@@ -138,9 +229,27 @@ msgstr ""
msgid "2FA enabled"
msgstr ""
+msgid "403|Please contact your GitLab administrator to get the permission."
+msgstr ""
+
+msgid "403|You don't have the permission to access this page."
+msgstr ""
+
+msgid "404|Make sure the address is correct and the page hasn't moved."
+msgstr ""
+
+msgid "404|Page Not Found"
+msgstr ""
+
+msgid "404|Please contact your GitLab administrator if you think this is a mistake."
+msgstr ""
+
msgid "<strong>Removes</strong> source branch"
msgstr ""
+msgid "A 'Runner' is a process which runs a job. You can setup as many Runners as you need."
+msgstr ""
+
msgid "A collection of graphs regarding Continuous Integration"
msgstr ""
@@ -150,6 +259,9 @@ msgstr ""
msgid "A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}."
msgstr ""
+msgid "A regular expression that will be used to find the test coverage output in the job trace. Leave blank to disable"
+msgstr ""
+
msgid "A user with write access to the source branch selected this option"
msgstr ""
@@ -162,25 +274,31 @@ msgstr ""
msgid "Abuse reports"
msgstr ""
+msgid "Accept terms"
+msgstr ""
+
msgid "Access Tokens"
msgstr ""
msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again."
msgstr ""
+msgid "Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report."
+msgstr ""
+
msgid "Account"
msgstr ""
-msgid "Account and limit settings"
+msgid "Account and limit"
msgstr ""
msgid "Active"
msgstr ""
-msgid "Activity"
+msgid "Active Sessions"
msgstr ""
-msgid "Add"
+msgid "Activity"
msgstr ""
msgid "Add Changelog"
@@ -189,9 +307,6 @@ msgstr ""
msgid "Add Contribution guide"
msgstr ""
-msgid "Add Group Webhooks and GitLab Enterprise Edition."
-msgstr ""
-
msgid "Add Kubernetes cluster"
msgstr ""
@@ -204,6 +319,9 @@ msgstr ""
msgid "Add new directory"
msgstr ""
+msgid "Add reaction"
+msgstr ""
+
msgid "Add todo"
msgstr ""
@@ -273,7 +391,10 @@ msgstr ""
msgid "All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings."
msgstr ""
-msgid "Allow edits from maintainers."
+msgid "Allow commits from members who can merge to the target branch."
+msgstr ""
+
+msgid "Allow public access to pipelines and job details, including output logs and artifacts"
msgstr ""
msgid "Allow rendering of PlantUML diagrams in Asciidoc documents."
@@ -285,31 +406,37 @@ msgstr ""
msgid "Allows you to add and manage Kubernetes clusters."
msgstr ""
-msgid "Also called \"Issuer\" or \"Relying party trust identifier\""
+msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
msgstr ""
-msgid "Also called \"Relying party service URL\" or \"Reply URL\""
+msgid "An error occured creating the new branch."
msgstr ""
-msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to connect."
+msgid "An error occured whilst loading all the files."
msgstr ""
-msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
+msgid "An error occured whilst loading the file content."
msgstr ""
-msgid "An error occurred previewing the blob"
+msgid "An error occured whilst loading the file."
msgstr ""
-msgid "An error occurred when toggling the notification subscription"
+msgid "An error occured whilst loading the merge request changes."
msgstr ""
-msgid "An error occurred when updating the issue weight"
+msgid "An error occured whilst loading the merge request version data."
msgstr ""
-msgid "An error occurred while adding approver"
+msgid "An error occured whilst loading the merge request."
msgstr ""
-msgid "An error occurred while detecting host keys"
+msgid "An error occurred previewing the blob"
+msgstr ""
+
+msgid "An error occurred when toggling the notification subscription"
+msgstr ""
+
+msgid "An error occurred while dismissing the alert. Refresh the page and try again."
msgstr ""
msgid "An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again."
@@ -327,10 +454,7 @@ msgstr ""
msgid "An error occurred while getting projects"
msgstr ""
-msgid "An error occurred while importing project"
-msgstr ""
-
-msgid "An error occurred while initializing path locks"
+msgid "An error occurred while importing project: ${details}"
msgstr ""
msgid "An error occurred while loading commits"
@@ -348,9 +472,6 @@ msgstr ""
msgid "An error occurred while making the request."
msgstr ""
-msgid "An error occurred while removing approver"
-msgstr ""
-
msgid "An error occurred while rendering KaTeX"
msgstr ""
@@ -363,9 +484,6 @@ msgstr ""
msgid "An error occurred while retrieving diff"
msgstr ""
-msgid "An error occurred while saving LDAP override status. Please try again."
-msgstr ""
-
msgid "An error occurred while saving assignees"
msgstr ""
@@ -375,9 +493,6 @@ msgstr ""
msgid "An error occurred. Please try again."
msgstr ""
-msgid "Any Label"
-msgstr ""
-
msgid "Appearance"
msgstr ""
@@ -390,19 +505,19 @@ msgstr ""
msgid "April"
msgstr ""
-msgid "Archived project! Repository is read-only"
+msgid "Archived project! Repository and other project resources are read-only"
msgstr ""
msgid "Are you sure you want to delete this pipeline schedule?"
msgstr ""
-msgid "Are you sure you want to reset registration token?"
+msgid "Are you sure you want to remove this identity?"
msgstr ""
-msgid "Are you sure you want to reset the health check token?"
+msgid "Are you sure you want to reset registration token?"
msgstr ""
-msgid "Are you sure you want to unlock %{path_lock_path}?"
+msgid "Are you sure you want to reset the health check token?"
msgstr ""
msgid "Are you sure?"
@@ -411,7 +526,7 @@ msgstr ""
msgid "Artifacts"
msgstr ""
-msgid "Assertion consumer service URL"
+msgid "Ask your group maintainer to setup a group Runner."
msgstr ""
msgid "Assign custom color like #FF0000"
@@ -435,9 +550,15 @@ msgstr ""
msgid "Assigned to :name"
msgstr ""
+msgid "Assigned to me"
+msgstr ""
+
msgid "Assignee"
msgstr ""
+msgid "Assignee(s)"
+msgstr ""
+
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr ""
@@ -471,7 +592,10 @@ msgstr ""
msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
msgstr ""
-msgid "AutoDevOps|Auto DevOps (Beta)"
+msgid "Auto-cancel redundant, pending pipelines"
+msgstr ""
+
+msgid "AutoDevOps|Auto DevOps"
msgstr ""
msgid "AutoDevOps|Auto DevOps documentation"
@@ -492,79 +616,109 @@ msgstr ""
msgid "AutoDevOps|add a Kubernetes cluster"
msgstr ""
-msgid "AutoDevOps|enable Auto DevOps (Beta)"
+msgid "AutoDevOps|enable Auto DevOps"
msgstr ""
msgid "Available"
msgstr ""
+msgid "Available group Runners : %{runners}"
+msgstr ""
+
+msgid "Available group Runners : %{runners}."
+msgstr ""
+
msgid "Avatar will be removed. Are you sure?"
msgstr ""
msgid "Average per day: %{average}"
msgstr ""
-msgid "Background Color"
+msgid "Background color"
msgstr ""
msgid "Background jobs"
msgstr ""
-msgid "Begin with the selected commit"
+msgid "Badges"
+msgstr ""
+
+msgid "Badges|A new badge was added."
+msgstr ""
+
+msgid "Badges|Add badge"
+msgstr ""
+
+msgid "Badges|Adding the badge failed, please check the entered URLs and try again."
+msgstr ""
+
+msgid "Badges|Badge image URL"
+msgstr ""
+
+msgid "Badges|Badge image preview"
+msgstr ""
+
+msgid "Badges|Delete badge"
+msgstr ""
+
+msgid "Badges|Delete badge?"
+msgstr ""
+
+msgid "Badges|Deleting the badge failed, please try again."
msgstr ""
-msgid "Billing"
+msgid "Badges|Group Badge"
msgstr ""
-msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan."
+msgid "Badges|Link"
msgstr ""
-msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available."
+msgid "Badges|No badge image"
msgstr ""
-msgid "BillingPlans|Current plan"
+msgid "Badges|No image to preview"
msgstr ""
-msgid "BillingPlans|Customer Support"
+msgid "Badges|Project Badge"
msgstr ""
-msgid "BillingPlans|Downgrade"
+msgid "Badges|Reload badge image"
msgstr ""
-msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
+msgid "Badges|Save changes"
msgstr ""
-msgid "BillingPlans|Manage plan"
+msgid "Badges|Saving the badge failed, please check the entered URLs and try again."
msgstr ""
-msgid "BillingPlans|Please contact %{customer_support_link} in that case."
+msgid "Badges|The %{docsLinkStart}variables%{docsLinkEnd} GitLab supports: %{placeholders}"
msgstr ""
-msgid "BillingPlans|See all %{plan_name} features"
+msgid "Badges|The badge was deleted."
msgstr ""
-msgid "BillingPlans|This group uses the plan associated with its parent group."
+msgid "Badges|The badge was saved."
msgstr ""
-msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
+msgid "Badges|This group has no badges"
msgstr ""
-msgid "BillingPlans|Upgrade"
+msgid "Badges|This project has no badges"
msgstr ""
-msgid "BillingPlans|You are currently on the %{plan_link} plan."
+msgid "Badges|Your badges"
msgstr ""
-msgid "BillingPlans|frequently asked questions"
+msgid "Begin with the selected commit"
msgstr ""
-msgid "BillingPlans|monthly"
+msgid "Below are examples of regex for existing tools:"
msgstr ""
-msgid "BillingPlans|paid annually at %{price_per_year}"
+msgid "Boards"
msgstr ""
-msgid "BillingPlans|per user"
+msgid "Branch %{branchName} was not found in this project's repository."
msgstr ""
msgid "Branch (%{branch_count})"
@@ -646,7 +800,7 @@ msgstr ""
msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
msgstr ""
-msgid "Branches|Only a project master or owner can delete a protected branch"
+msgid "Branches|Only a project maintainer or owner can delete a protected branch"
msgstr ""
msgid "Branches|Overview"
@@ -682,9 +836,6 @@ msgstr ""
msgid "Branches|Stale branches"
msgstr ""
-msgid "Branches|The branch could not be updated automatically because it has diverged from its upstream counterpart."
-msgstr ""
-
msgid "Branches|The default branch cannot be deleted"
msgstr ""
@@ -697,15 +848,9 @@ msgstr ""
msgid "Branches|To confirm, type %{branch_name_confirmation}:"
msgstr ""
-msgid "Branches|To discard the local changes and overwrite the branch with the upstream version, delete it here and choose 'Update Now' above."
-msgstr ""
-
msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
msgstr ""
-msgid "Branches|diverged from upstream"
-msgstr ""
-
msgid "Branches|merged"
msgstr ""
@@ -727,40 +872,79 @@ msgstr ""
msgid "Browse files"
msgstr ""
-msgid "Business"
-msgstr ""
-
msgid "ByAuthor|by"
msgstr ""
msgid "CI / CD"
msgstr ""
-msgid "CI/CD"
+msgid "CI / CD Settings"
msgstr ""
msgid "CI/CD configuration"
msgstr ""
-msgid "CI/CD for external repo"
+msgid "CI/CD settings"
+msgstr ""
+
+msgid "CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery."
+msgstr ""
+
+msgid "CICD|Auto DevOps"
+msgstr ""
+
+msgid "CICD|Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration."
+msgstr ""
+
+msgid "CICD|Automatic deployment to staging, manual deployment to production"
+msgstr ""
+
+msgid "CICD|Continuous deployment to production"
+msgstr ""
+
+msgid "CICD|Deployment strategy"
+msgstr ""
+
+msgid "CICD|Deployment strategy needs a domain name to work correctly."
+msgstr ""
+
+msgid "CICD|Disable Auto DevOps"
+msgstr ""
+
+msgid "CICD|Enable Auto DevOps"
+msgstr ""
+
+msgid "CICD|Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific %{ci_file}."
+msgstr ""
+
+msgid "CICD|Instance default (%{state})"
msgstr ""
msgid "CICD|Jobs"
msgstr ""
-msgid "Cancel"
+msgid "CICD|Learn more about Auto DevOps"
msgstr ""
-msgid "Cannot be merged automatically"
+msgid "CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project."
msgstr ""
-msgid "Cannot modify managed Kubernetes cluster"
+msgid "CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages."
msgstr ""
-msgid "Certificate fingerprint"
+msgid "Can't find HEAD commit for this branch"
msgstr ""
-msgid "Change Weight"
+msgid "Cancel"
+msgstr ""
+
+msgid "Cancel this job"
+msgstr ""
+
+msgid "Cannot be merged automatically"
+msgstr ""
+
+msgid "Cannot modify managed Kubernetes cluster"
msgstr ""
msgid "Change this value to influence how frequently the GitLab UI polls for updates."
@@ -814,21 +998,18 @@ msgstr ""
msgid "Choose a branch/tag (e.g. %{master}) or enter a commit (e.g. %{sha}) to see what's changed or to create a merge request."
msgstr ""
-msgid "Choose file..."
+msgid "Choose any color."
msgstr ""
-msgid "Choose which groups you wish to synchronize to this secondary node."
+msgid "Choose between <code>clone</code> or <code>fetch</code> to get the recent application code"
msgstr ""
-msgid "Choose which repositories you want to connect and run CI/CD pipelines."
+msgid "Choose file..."
msgstr ""
msgid "Choose which repositories you want to import."
msgstr ""
-msgid "Choose which shards you wish to synchronize to this secondary node."
-msgstr ""
-
msgid "CiStatusLabel|canceled"
msgstr ""
@@ -898,21 +1079,12 @@ msgstr ""
msgid "CiVariable|All environments"
msgstr ""
-msgid "CiVariable|Create wildcard"
-msgstr ""
-
msgid "CiVariable|Error occured while saving variables"
msgstr ""
-msgid "CiVariable|New environment"
-msgstr ""
-
msgid "CiVariable|Protected"
msgstr ""
-msgid "CiVariable|Search environments"
-msgstr ""
-
msgid "CiVariable|Toggle protected"
msgstr ""
@@ -922,28 +1094,28 @@ msgstr ""
msgid "CircuitBreakerApiLink|circuitbreaker api"
msgstr ""
-msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
+msgid "Clear search input"
msgstr ""
-msgid "Click to expand text"
+msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Client authentication certificate"
+msgid "Click the <strong>Promote</strong> button in the top right corner to promote it to a group milestone."
msgstr ""
-msgid "Client authentication key"
+msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
msgstr ""
-msgid "Client authentication key password"
+msgid "Click to expand it."
msgstr ""
-msgid "Clone repository"
+msgid "Click to expand text"
msgstr ""
-msgid "Close"
+msgid "Clone repository"
msgstr ""
-msgid "Closed"
+msgid "Close"
msgstr ""
msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster"
@@ -961,6 +1133,12 @@ msgstr ""
msgid "ClusterIntegration|Advanced options on this Kubernetes cluster's integration"
msgstr ""
+msgid "ClusterIntegration|An error occured while trying to fetch project zones: %{error}"
+msgstr ""
+
+msgid "ClusterIntegration|An error occured while trying to fetch your projects: %{error}"
+msgstr ""
+
msgid "ClusterIntegration|Applications"
msgstr ""
@@ -991,6 +1169,9 @@ msgstr ""
msgid "ClusterIntegration|Copy Ingress IP Address to clipboard"
msgstr ""
+msgid "ClusterIntegration|Copy Jupyter Hostname to clipboard"
+msgstr ""
+
msgid "ClusterIntegration|Copy Kubernetes cluster name"
msgstr ""
@@ -1006,7 +1187,7 @@ msgstr ""
msgid "ClusterIntegration|Create a new Kubernetes cluster on Google Kubernetes Engine right from GitLab"
msgstr ""
-msgid "ClusterIntegration|Create on GKE"
+msgid "ClusterIntegration|Create on Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
@@ -1018,13 +1199,25 @@ msgstr ""
msgid "ClusterIntegration|Environment scope"
msgstr ""
+msgid "ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab's Google Kubernetes Engine Integration."
+msgstr ""
+
+msgid "ClusterIntegration|Fetching machine types"
+msgstr ""
+
+msgid "ClusterIntegration|Fetching projects"
+msgstr ""
+
+msgid "ClusterIntegration|Fetching zones"
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
msgid "ClusterIntegration|GitLab Runner"
msgstr ""
-msgid "ClusterIntegration|Google Cloud Platform project ID"
+msgid "ClusterIntegration|Google Cloud Platform project"
msgstr ""
msgid "ClusterIntegration|Google Kubernetes Engine"
@@ -1036,9 +1229,6 @@ msgstr ""
msgid "ClusterIntegration|Helm Tiller"
msgstr ""
-msgid "ClusterIntegration|In order to show the health of the cluster, we'll need to provision your cluster with Prometheus to collect the required data."
-msgstr ""
-
msgid "ClusterIntegration|Ingress"
msgstr ""
@@ -1048,9 +1238,6 @@ msgstr ""
msgid "ClusterIntegration|Install"
msgstr ""
-msgid "ClusterIntegration|Install Prometheus"
-msgstr ""
-
msgid "ClusterIntegration|Installed"
msgstr ""
@@ -1063,13 +1250,16 @@ msgstr ""
msgid "ClusterIntegration|Integration status"
msgstr ""
-msgid "ClusterIntegration|Kubernetes cluster"
+msgid "ClusterIntegration|Jupyter Hostname"
msgstr ""
-msgid "ClusterIntegration|Kubernetes cluster details"
+msgid "ClusterIntegration|JupyterHub"
msgstr ""
-msgid "ClusterIntegration|Kubernetes cluster health"
+msgid "ClusterIntegration|Kubernetes cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Kubernetes cluster details"
msgstr ""
msgid "ClusterIntegration|Kubernetes cluster integration"
@@ -1099,7 +1289,13 @@ msgstr ""
msgid "ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project"
msgstr ""
-msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
+msgid "ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}."
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{help_link_start}Kubernetes%{help_link_end}."
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}."
msgstr ""
msgid "ClusterIntegration|Learn more about environments"
@@ -1123,7 +1319,16 @@ msgstr ""
msgid "ClusterIntegration|More information"
msgstr ""
-msgid "ClusterIntegration|Multiple Kubernetes clusters are available in GitLab Enterprise Edition Premium and Ultimate"
+msgid "ClusterIntegration|No machine types matched your search"
+msgstr ""
+
+msgid "ClusterIntegration|No projects found"
+msgstr ""
+
+msgid "ClusterIntegration|No projects matched your search"
+msgstr ""
+
+msgid "ClusterIntegration|No zones matched your search"
msgstr ""
msgid "ClusterIntegration|Note:"
@@ -1138,9 +1343,6 @@ msgstr ""
msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
msgstr ""
-msgid "ClusterIntegration|Project ID"
-msgstr ""
-
msgid "ClusterIntegration|Project namespace"
msgstr ""
@@ -1153,6 +1355,9 @@ msgstr ""
msgid "ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration."
msgstr ""
+msgid "ClusterIntegration|Redeem up to $500 in free credit for Google Cloud Platform"
+msgstr ""
+
msgid "ClusterIntegration|Remove Kubernetes cluster integration"
msgstr ""
@@ -1168,19 +1373,37 @@ msgstr ""
msgid "ClusterIntegration|Save changes"
msgstr ""
+msgid "ClusterIntegration|Search machine types"
+msgstr ""
+
+msgid "ClusterIntegration|Search projects"
+msgstr ""
+
+msgid "ClusterIntegration|Search zones"
+msgstr ""
+
msgid "ClusterIntegration|Security"
msgstr ""
msgid "ClusterIntegration|See and edit the details for your Kubernetes cluster"
msgstr ""
-msgid "ClusterIntegration|See machine types"
+msgid "ClusterIntegration|Select machine type"
+msgstr ""
+
+msgid "ClusterIntegration|Select project"
+msgstr ""
+
+msgid "ClusterIntegration|Select project and zone to choose machine type"
+msgstr ""
+
+msgid "ClusterIntegration|Select project to choose zone"
msgstr ""
-msgid "ClusterIntegration|See your projects"
+msgid "ClusterIntegration|Select zone"
msgstr ""
-msgid "ClusterIntegration|See zones"
+msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
msgid "ClusterIntegration|Service token"
@@ -1213,6 +1436,9 @@ msgstr ""
msgid "ClusterIntegration|Token"
msgstr ""
+msgid "ClusterIntegration|Validating project billing status"
+msgstr ""
+
msgid "ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
@@ -1243,13 +1469,19 @@ msgstr ""
msgid "ClusterIntegration|properly configured"
msgstr ""
+msgid "ClusterIntegration|sign up"
+msgstr ""
+
msgid "Collapse"
msgstr ""
-msgid "Comment and resolve discussion"
+msgid "Collapse sidebar"
msgstr ""
-msgid "Comment and unresolve discussion"
+msgid "Comment & resolve discussion"
+msgstr ""
+
+msgid "Comment & unresolve discussion"
msgstr ""
msgid "Comments"
@@ -1320,6 +1552,9 @@ msgstr ""
msgid "Committed by"
msgstr ""
+msgid "Commit…"
+msgstr ""
+
msgid "Compare"
msgstr ""
@@ -1368,6 +1603,9 @@ msgstr ""
msgid "Configure limits for web and API requests."
msgstr ""
+msgid "Configure push mirrors."
+msgstr ""
+
msgid "Configure storage path and circuit breaker settings."
msgstr ""
@@ -1377,18 +1615,9 @@ msgstr ""
msgid "Connect"
msgstr ""
-msgid "Connect all repositories"
-msgstr ""
-
msgid "Connect repositories from GitHub"
msgstr ""
-msgid "Connect your external repositories, and CI/CD pipelines will run for new commits. A GitLab project will be created with only CI/CD features enabled."
-msgstr ""
-
-msgid "Connecting..."
-msgstr ""
-
msgid "Container Registry"
msgstr ""
@@ -1434,9 +1663,18 @@ 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 "ContainerRegistry|You can also use a %{deploy_token} for read-only access to the registry images."
+msgstr ""
+
+msgid "Continue"
+msgstr ""
+
msgid "Continuous Integration and Deployment"
msgstr ""
+msgid "Contribute to GitLab"
+msgstr ""
+
msgid "Contribution"
msgstr ""
@@ -1458,28 +1696,28 @@ msgstr ""
msgid "ContributorsPage|Please wait a moment, this page will automatically refresh when ready."
msgstr ""
-msgid "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
+msgid "Copy URL to clipboard"
msgstr ""
-msgid "Control the maximum concurrency of repository backfill for this secondary node"
+msgid "Copy branch name to clipboard"
msgstr ""
-msgid "Copy SSH public key to clipboard"
+msgid "Copy command to clipboard"
msgstr ""
-msgid "Copy URL to clipboard"
+msgid "Copy commit SHA to clipboard"
msgstr ""
-msgid "Copy branch name to clipboard"
+msgid "Copy file name to clipboard"
msgstr ""
-msgid "Copy command to clipboard"
+msgid "Copy file path to clipboard"
msgstr ""
-msgid "Copy commit SHA to clipboard"
+msgid "Copy reference to clipboard"
msgstr ""
-msgid "Copy reference to clipboard"
+msgid "Copy to clipboard"
msgstr ""
msgid "Create"
@@ -1500,13 +1738,13 @@ msgstr ""
msgid "Create branch"
msgstr ""
-msgid "Create directory"
+msgid "Create commit"
msgstr ""
-msgid "Create empty repository"
+msgid "Create directory"
msgstr ""
-msgid "Create epic"
+msgid "Create empty repository"
msgstr ""
msgid "Create file"
@@ -1551,13 +1789,10 @@ msgstr ""
msgid "CreateTokenToCloneLink|create a personal access token"
msgstr ""
-msgid "Creates a new branch from %{branchName}"
-msgstr ""
-
-msgid "Creates a new branch from %{branchName} and re-directs to create a new merge request"
+msgid "Created"
msgstr ""
-msgid "Creating epic"
+msgid "Created by me"
msgstr ""
msgid "Cron Timezone"
@@ -1566,16 +1801,19 @@ msgstr ""
msgid "Cron syntax"
msgstr ""
-msgid "Current node"
+msgid "CurrentUser|Profile"
msgstr ""
-msgid "Custom notification events"
+msgid "CurrentUser|Settings"
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}."
+msgid "Custom CI config path"
msgstr ""
-msgid "Customize colors"
+msgid "Custom notification events"
+msgstr ""
+
+msgid "Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notification_link}."
msgstr ""
msgid "Cycle Analytics"
@@ -1614,7 +1852,7 @@ msgstr ""
msgid "December"
msgstr ""
-msgid "Default classification label"
+msgid "Decline and sign out"
msgstr ""
msgid "Define a custom pattern with cron syntax"
@@ -1623,6 +1861,9 @@ msgstr ""
msgid "Delete"
msgstr ""
+msgid "Delete list"
+msgstr ""
+
msgid "Deploy"
msgid_plural "Deploys"
msgstr[0] ""
@@ -1633,551 +1874,527 @@ msgstr[3] ""
msgid "Deploy Keys"
msgstr ""
-msgid "Description"
+msgid "DeployKeys|+%{count} others"
msgstr ""
-msgid "Description templates allow you to define context-specific templates for issue and merge request description fields for your project."
+msgid "DeployKeys|Current project"
msgstr ""
-msgid "Details"
+msgid "DeployKeys|Deploy key"
msgstr ""
-msgid "Diffs|No file name available"
+msgid "DeployKeys|Enabled deploy keys"
msgstr ""
-msgid "Directory name"
+msgid "DeployKeys|Error enabling deploy key"
msgstr ""
-msgid "Disable"
+msgid "DeployKeys|Error getting deploy keys"
msgstr ""
-msgid "Discard draft"
+msgid "DeployKeys|Error removing deploy key"
msgstr ""
-msgid "Discover GitLab Geo."
+msgid "DeployKeys|Expand %{count} other projects"
msgstr ""
-msgid "Dismiss Cycle Analytics introduction box"
+msgid "DeployKeys|Loading deploy keys"
msgstr ""
-msgid "Dismiss Merge Request promotion"
+msgid "DeployKeys|No deploy keys found. Create one with the form above."
msgstr ""
-msgid "Documentation for popular identity providers"
+msgid "DeployKeys|Privately accessible deploy keys"
msgstr ""
-msgid "Don't show again"
+msgid "DeployKeys|Project usage"
msgstr ""
-msgid "Done"
+msgid "DeployKeys|Publicly accessible deploy keys"
msgstr ""
-msgid "Download"
+msgid "DeployKeys|Read access only"
msgstr ""
-msgid "Download tar"
+msgid "DeployKeys|Write access allowed"
msgstr ""
-msgid "Download tar.bz2"
+msgid "DeployKeys|You are going to remove this deploy key. Are you sure?"
msgstr ""
-msgid "Download tar.gz"
+msgid "DeployTokens|Active Deploy Tokens (%{active_tokens})"
msgstr ""
-msgid "Download zip"
+msgid "DeployTokens|Add a deploy token"
msgstr ""
-msgid "DownloadArtifacts|Download"
+msgid "DeployTokens|Allows read-only access to the registry images"
msgstr ""
-msgid "DownloadCommit|Email Patches"
+msgid "DeployTokens|Allows read-only access to the repository"
msgstr ""
-msgid "DownloadCommit|Plain Diff"
+msgid "DeployTokens|Copy deploy token to clipboard"
msgstr ""
-msgid "DownloadSource|Download"
+msgid "DeployTokens|Copy username to clipboard"
msgstr ""
-msgid "Downvotes"
+msgid "DeployTokens|Create deploy token"
msgstr ""
-msgid "Due date"
+msgid "DeployTokens|Created"
msgstr ""
-msgid "During this process, you’ll be asked for URLs from GitLab’s side. Use the URLs shown below."
+msgid "DeployTokens|Deploy Tokens"
msgstr ""
-msgid "Edit"
+msgid "DeployTokens|Deploy tokens allow read-only access to your repository and registry images."
msgstr ""
-msgid "Edit Pipeline Schedule %{id}"
-msgstr ""
-
-msgid "Edit files in the editor and commit changes here"
+msgid "DeployTokens|Expires"
msgstr ""
-msgid "Editing"
+msgid "DeployTokens|Name"
msgstr ""
-msgid "Elasticsearch"
+msgid "DeployTokens|Pick a name for the application, and we'll give you a unique deploy token."
msgstr ""
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "DeployTokens|Revoke"
msgstr ""
-msgid "Email"
-msgstr ""
-
-msgid "Emails"
+msgid "DeployTokens|Revoke %{name}"
msgstr ""
-msgid "Enable"
+msgid "DeployTokens|Scopes"
msgstr ""
-msgid "Enable Auto DevOps"
+msgid "DeployTokens|This action cannot be undone."
msgstr ""
-msgid "Enable SAML authentication for this group"
+msgid "DeployTokens|This project has no active Deploy Tokens."
msgstr ""
-msgid "Enable Sentry for error reporting and logging."
+msgid "DeployTokens|Use this token as a password. Make sure you save it - you won't be able to access it again."
msgstr ""
-msgid "Enable and configure InfluxDB metrics."
+msgid "DeployTokens|Use this username as a login."
msgstr ""
-msgid "Enable and configure Prometheus metrics."
+msgid "DeployTokens|Username"
msgstr ""
-msgid "Enable classification control using an external service"
+msgid "DeployTokens|You are about to revoke"
msgstr ""
-msgid "Enable or disable version check and usage ping."
+msgid "DeployTokens|Your New Deploy Token"
msgstr ""
-msgid "Enable reCAPTCHA or Akismet and set IP limits."
+msgid "DeployTokens|Your new project deploy token has been created."
msgstr ""
-msgid "Enable the Performance Bar for a given group."
+msgid "Deprioritize label"
msgstr ""
-msgid "Enabled"
-msgstr ""
-
-msgid "Environments|An error occurred while fetching the environments."
-msgstr ""
-
-msgid "Environments|An error occurred while making the request."
-msgstr ""
-
-msgid "Environments|Commit"
-msgstr ""
-
-msgid "Environments|Deployment"
-msgstr ""
-
-msgid "Environments|Environment"
+msgid "Description"
msgstr ""
-msgid "Environments|Environments"
+msgid "Details"
msgstr ""
-msgid "Environments|Job"
+msgid "Diffs|No file name available"
msgstr ""
-msgid "Environments|New environment"
+msgid "Diffs|Something went wrong while fetching diff lines."
msgstr ""
-msgid "Environments|No deployments yet"
+msgid "Directory name"
msgstr ""
-msgid "Environments|Open"
+msgid "Disable"
msgstr ""
-msgid "Environments|Re-deploy"
+msgid "Disable for this project"
msgstr ""
-msgid "Environments|Read more about environments"
+msgid "Disable group Runners"
msgstr ""
-msgid "Environments|Rollback"
+msgid "Discard changes"
msgstr ""
-msgid "Environments|Show all"
+msgid "Discard draft"
msgstr ""
-msgid "Environments|Updated"
+msgid "Dismiss Cycle Analytics introduction box"
msgstr ""
-msgid "Environments|You don't have any environments right now."
+msgid "Domain"
msgstr ""
-msgid "Epic will be removed! Are you sure?"
+msgid "Don't show again"
msgstr ""
-msgid "Epics"
+msgid "Done"
msgstr ""
-msgid "Epics Roadmap"
+msgid "Download"
msgstr ""
-msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
+msgid "Download tar"
msgstr ""
-msgid "Error Reporting and Logging"
+msgid "Download tar.bz2"
msgstr ""
-msgid "Error checking branch data. Please try again."
+msgid "Download tar.gz"
msgstr ""
-msgid "Error committing changes. Please try again."
+msgid "Download zip"
msgstr ""
-msgid "Error creating epic"
+msgid "DownloadArtifacts|Download"
msgstr ""
-msgid "Error fetching contributors data."
+msgid "DownloadCommit|Email Patches"
msgstr ""
-msgid "Error fetching labels."
+msgid "DownloadCommit|Plain Diff"
msgstr ""
-msgid "Error fetching network graph."
+msgid "DownloadSource|Download"
msgstr ""
-msgid "Error fetching refs"
+msgid "Downvotes"
msgstr ""
-msgid "Error fetching usage ping data."
+msgid "Due date"
msgstr ""
-msgid "Error occurred when toggling the notification subscription"
+msgid "Each Runner can be in one of the following states:"
msgstr ""
-msgid "Error saving label update."
+msgid "Edit"
msgstr ""
-msgid "Error updating status for all todos."
+msgid "Edit Label"
msgstr ""
-msgid "Error updating todo status."
+msgid "Edit Pipeline Schedule %{id}"
msgstr ""
-msgid "EventFilterBy|Filter by all"
+msgid "Edit files in the editor and commit changes here"
msgstr ""
-msgid "EventFilterBy|Filter by comments"
+msgid "Edit identity for %{user_name}"
msgstr ""
-msgid "EventFilterBy|Filter by issue events"
+msgid "Email"
msgstr ""
-msgid "EventFilterBy|Filter by merge events"
+msgid "Email patch"
msgstr ""
-msgid "EventFilterBy|Filter by push events"
+msgid "Emails"
msgstr ""
-msgid "EventFilterBy|Filter by team"
+msgid "Embed"
msgstr ""
-msgid "Every day (at 4:00am)"
+msgid "Enable"
msgstr ""
-msgid "Every month (on the 1st at 4:00am)"
+msgid "Enable Auto DevOps"
msgstr ""
-msgid "Every week (Sundays at 4:00am)"
+msgid "Enable Sentry for error reporting and logging."
msgstr ""
-msgid "Expand"
+msgid "Enable and configure InfluxDB metrics."
msgstr ""
-msgid "Explore projects"
+msgid "Enable and configure Prometheus metrics."
msgstr ""
-msgid "Explore public groups"
+msgid "Enable for this project"
msgstr ""
-msgid "External Classification Policy Authorization"
+msgid "Enable group Runners"
msgstr ""
-msgid "External authentication"
+msgid "Enable or disable certain group features and choose access levels."
msgstr ""
-msgid "External authorization denied access to this project"
+msgid "Enable or disable version check and usage ping."
msgstr ""
-msgid "External authorization request timeout"
+msgid "Enable reCAPTCHA or Akismet and set IP limits."
msgstr ""
-msgid "ExternalAuthorizationService|Classification Label"
+msgid "Enable the Performance Bar for a given group."
msgstr ""
-msgid "ExternalAuthorizationService|Classification label"
+msgid "Ends at (UTC)"
msgstr ""
-msgid "ExternalAuthorizationService|When no classification label is set the default label `%{default_label}` will be used."
+msgid "Environments"
msgstr ""
-msgid "Failed"
+msgid "Environments|An error occurred while fetching the environments."
msgstr ""
-msgid "Failed Jobs"
+msgid "Environments|An error occurred while making the request."
msgstr ""
-msgid "Failed to change the owner"
+msgid "Environments|Commit"
msgstr ""
-msgid "Failed to remove issue from board, please try again."
+msgid "Environments|Deployment"
msgstr ""
-msgid "Failed to remove the pipeline schedule"
+msgid "Environments|Environment"
msgstr ""
-msgid "Failed to update issues, please try again."
+msgid "Environments|Environments"
msgstr ""
-msgid "Feb"
+msgid "Environments|Job"
msgstr ""
-msgid "February"
+msgid "Environments|New environment"
msgstr ""
-msgid "Fields on this page are now uneditable, you can configure"
+msgid "Environments|No deployments yet"
msgstr ""
-msgid "File name"
+msgid "Environments|Open"
msgstr ""
-msgid "Files"
+msgid "Environments|Re-deploy"
msgstr ""
-msgid "Files (%{human_size})"
+msgid "Environments|Read more about environments"
msgstr ""
-msgid "Fill in the fields below, turn on <strong>%{enable_label}</strong>, and press <strong>%{save_changes}</strong>"
+msgid "Environments|Rollback"
msgstr ""
-msgid "Filter by commit message"
+msgid "Environments|Show all"
msgstr ""
-msgid "Find by path"
+msgid "Environments|Updated"
msgstr ""
-msgid "Find file"
+msgid "Environments|You don't have any environments right now."
msgstr ""
-msgid "Finished"
+msgid "Error Reporting and Logging"
msgstr ""
-msgid "FirstPushedBy|First"
+msgid "Error committing changes. Please try again."
msgstr ""
-msgid "FirstPushedBy|pushed by"
+msgid "Error fetching contributors data."
msgstr ""
-msgid "Font Color"
+msgid "Error fetching job trace"
msgstr ""
-msgid "Footer message"
+msgid "Error fetching labels."
msgstr ""
-msgid "Fork"
-msgid_plural "Forks"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
-
-msgid "ForkedFromProjectPath|Forked from"
+msgid "Error fetching network graph."
msgstr ""
-msgid "ForkedFromProjectPath|Forked from %{project_name} (deleted)"
+msgid "Error fetching refs"
msgstr ""
-msgid "Forking in progress"
+msgid "Error fetching usage ping data."
msgstr ""
-msgid "Format"
+msgid "Error loading branch data. Please try again."
msgstr ""
-msgid "From %{provider_title}"
+msgid "Error loading last commit."
msgstr ""
-msgid "From issue creation until deploy to production"
+msgid "Error loading merge requests."
msgstr ""
-msgid "From merge request merge until deploy to production"
+msgid "Error loading project data. Please try again."
msgstr ""
-msgid "From the Kubernetes cluster details view, install Runner from the applications list"
+msgid "Error occurred when toggling the notification subscription"
msgstr ""
-msgid "GPG Keys"
+msgid "Error saving label update."
msgstr ""
-msgid "Generate a default set of labels"
+msgid "Error updating status for all todos."
msgstr ""
-msgid "Geo Nodes"
+msgid "Error updating todo status."
msgstr ""
-msgid "Geo allows you to replicate your GitLab instance to other geographical locations."
+msgid "Estimated"
msgstr ""
-msgid "GeoNodeSyncStatus|Node is failing or broken."
+msgid "EventFilterBy|Filter by all"
msgstr ""
-msgid "GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage."
+msgid "EventFilterBy|Filter by comments"
msgstr ""
-msgid "GeoNodes|Checksummed"
+msgid "EventFilterBy|Filter by issue events"
msgstr ""
-msgid "GeoNodes|Database replication lag:"
+msgid "EventFilterBy|Filter by merge events"
msgstr ""
-msgid "GeoNodes|Disabling a node stops the sync process. Are you sure?"
+msgid "EventFilterBy|Filter by push events"
msgstr ""
-msgid "GeoNodes|Does not match the primary storage configuration"
+msgid "EventFilterBy|Filter by team"
msgstr ""
-msgid "GeoNodes|Failed"
+msgid "Every day (at 4:00am)"
msgstr ""
-msgid "GeoNodes|Full"
+msgid "Every month (on the 1st at 4:00am)"
msgstr ""
-msgid "GeoNodes|GitLab version does not match the primary node version"
+msgid "Every week (Sundays at 4:00am)"
msgstr ""
-msgid "GeoNodes|GitLab version:"
+msgid "Expand"
msgstr ""
-msgid "GeoNodes|Health status:"
+msgid "Expand all"
msgstr ""
-msgid "GeoNodes|Last event ID processed by cursor:"
+msgid "Expand sidebar"
msgstr ""
-msgid "GeoNodes|Last event ID seen from primary:"
+msgid "Explore projects"
msgstr ""
-msgid "GeoNodes|Loading nodes"
+msgid "Explore public groups"
msgstr ""
-msgid "GeoNodes|Local Attachments:"
+msgid "Failed"
msgstr ""
-msgid "GeoNodes|Local LFS objects:"
+msgid "Failed Jobs"
msgstr ""
-msgid "GeoNodes|Local job artifacts:"
+msgid "Failed to change the owner"
msgstr ""
-msgid "GeoNodes|New node"
+msgid "Failed to check related branches."
msgstr ""
-msgid "GeoNodes|Node Authentication was successfully repaired."
+msgid "Failed to remove issue from board, please try again."
msgstr ""
-msgid "GeoNodes|Node was successfully removed."
+msgid "Failed to remove the pipeline schedule"
msgstr ""
-msgid "GeoNodes|Not checksummed"
+msgid "Failed to update issues, please try again."
msgstr ""
-msgid "GeoNodes|Out of sync"
+msgid "Failure"
msgstr ""
-msgid "GeoNodes|Removing a node stops the sync process. Are you sure?"
+msgid "Faster as it re-uses the project workspace (falling back to clone if it doesn't exist)"
msgstr ""
-msgid "GeoNodes|Replication slot WAL:"
+msgid "Feb"
msgstr ""
-msgid "GeoNodes|Replication slots:"
+msgid "February"
msgstr ""
-msgid "GeoNodes|Repositories checksummed:"
+msgid "Fields on this page are now uneditable, you can configure"
msgstr ""
-msgid "GeoNodes|Repositories:"
+msgid "Files"
msgstr ""
-msgid "GeoNodes|Repository checksums verified:"
+msgid "Files (%{human_size})"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "Filter by commit message"
msgstr ""
-msgid "GeoNodes|Something went wrong while changing node status"
+msgid "Find by path"
msgstr ""
-msgid "GeoNodes|Something went wrong while removing node"
+msgid "Find file"
msgstr ""
-msgid "GeoNodes|Something went wrong while repairing node"
+msgid "Finished"
msgstr ""
-msgid "GeoNodes|Storage config:"
+msgid "FirstPushedBy|First"
msgstr ""
-msgid "GeoNodes|Sync settings:"
+msgid "FirstPushedBy|pushed by"
msgstr ""
-msgid "GeoNodes|Synced"
+msgid "For internal projects, any logged in user can view pipelines and access job details (output logs and artifacts)"
msgstr ""
-msgid "GeoNodes|Unused slots"
+msgid "For private projects, any member (guest or higher) can view pipelines and access job details (output logs and artifacts)"
msgstr ""
-msgid "GeoNodes|Unverified"
+msgid "For public projects, anyone can view pipelines and access job details (output logs and artifacts)"
msgstr ""
-msgid "GeoNodes|Used slots"
-msgstr ""
+msgid "Fork"
+msgid_plural "Forks"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
-msgid "GeoNodes|Verified"
+msgid "ForkedFromProjectPath|Forked from"
msgstr ""
-msgid "GeoNodes|Wiki checksums verified:"
+msgid "ForkedFromProjectPath|Forked from %{project_name} (deleted)"
msgstr ""
-msgid "GeoNodes|Wikis checksummed:"
+msgid "Forking in progress"
msgstr ""
-msgid "GeoNodes|Wikis:"
+msgid "Format"
msgstr ""
-msgid "GeoNodes|You have configured Geo nodes using an insecure HTTP connection. We recommend the use of HTTPS."
+msgid "Found errors in your .gitlab-ci.yml:"
msgstr ""
-msgid "Geo|All projects"
+msgid "From %{provider_title}"
msgstr ""
-msgid "Geo|File sync capacity"
+msgid "From issue creation until deploy to production"
msgstr ""
-msgid "Geo|Groups to synchronize"
+msgid "From merge request merge until deploy to production"
msgstr ""
-msgid "Geo|Projects in certain groups"
+msgid "From the Kubernetes cluster details view, install Runner from the applications list"
msgstr ""
-msgid "Geo|Projects in certain storage shards"
+msgid "GPG Keys"
msgstr ""
-msgid "Geo|Repository sync capacity"
+msgid "General"
msgstr ""
-msgid "Geo|Select groups to replicate."
+msgid "General pipelines"
msgstr ""
-msgid "Geo|Shards to synchronize"
+msgid "Generate a default set of labels"
msgstr ""
msgid "Git repository URL"
@@ -2189,6 +2406,9 @@ msgstr ""
msgid "Git storage health information has been reset"
msgstr ""
+msgid "Git strategy for pipelines"
+msgstr ""
+
msgid "Git version"
msgstr ""
@@ -2198,21 +2418,24 @@ msgstr ""
msgid "GitLab CI Linter has been moved"
msgstr ""
-msgid "GitLab Geo"
+msgid "GitLab Group Runners can execute code for all the projects in this group."
msgstr ""
msgid "GitLab Runner section"
msgstr ""
-msgid "GitLab single sign on URL"
-msgstr ""
-
msgid "Gitaly"
msgstr ""
msgid "Gitaly Servers"
msgstr ""
+msgid "Gitaly|Address"
+msgstr ""
+
+msgid "Go Back"
+msgstr ""
+
msgid "Go back"
msgstr ""
@@ -2228,22 +2451,19 @@ msgstr ""
msgid "Got it!"
msgstr ""
-msgid "GroupRoadmap|Epics let you manage your portfolio of projects more efficiently and with less effort"
+msgid "Graph"
msgstr ""
-msgid "GroupRoadmap|From %{dateWord}"
+msgid "Group CI/CD settings"
msgstr ""
-msgid "GroupRoadmap|Loading roadmap"
+msgid "Group ID"
msgstr ""
-msgid "GroupRoadmap|Something went wrong while fetching epics"
+msgid "Group Runners"
msgstr ""
-msgid "GroupRoadmap|To view the roadmap, add a planned start or finish date to one of your epics in this group or its subgroups. Only epics in the past 3 months and the next 3 months are shown &ndash; from %{startDate} to %{endDate}."
-msgstr ""
-
-msgid "GroupRoadmap|Until %{dateWord}"
+msgid "Group maintainers can register group runners in the %{link}"
msgstr ""
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
@@ -2270,6 +2490,9 @@ msgstr ""
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
+msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
+msgstr ""
+
msgid "GroupsEmptyState|A group is a collection of several projects."
msgstr ""
@@ -2309,12 +2532,6 @@ msgstr ""
msgid "GroupsTree|Sorry, no groups or projects matched your search"
msgstr ""
-msgid "Have your users email"
-msgstr ""
-
-msgid "Header message"
-msgstr ""
-
msgid "Health Check"
msgstr ""
@@ -2349,19 +2566,49 @@ msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
+msgid "Hide whitespace changes"
+msgstr ""
+
msgid "History"
msgstr ""
msgid "Housekeeping successfully started"
msgstr ""
-msgid "Identity provider single sign on URL"
+msgid "I accept the %{terms_link}"
+msgstr ""
+
+msgid "I accept the|Terms of Service and Privacy Policy"
+msgstr ""
+
+msgid "ID"
msgstr ""
-msgid "If enabled, access to projects will be validated on an external service using their classification label."
+msgid "IDE|Commit"
msgstr ""
-msgid "If using GitHub, you’ll see pipeline statuses on GitHub for your commits and pull requests. %{more_info_link}"
+msgid "IDE|Edit"
+msgstr ""
+
+msgid "IDE|Go back"
+msgstr ""
+
+msgid "IDE|Open in file view"
+msgstr ""
+
+msgid "IDE|Review"
+msgstr ""
+
+msgid "Identifier"
+msgstr ""
+
+msgid "Identities"
+msgstr ""
+
+msgid "If disabled, the access level will depend on the user's permissions in the project."
+msgstr ""
+
+msgid "If enabled"
msgstr ""
msgid "If you already have files you can push them using the %{link_to_cli} below."
@@ -2370,6 +2617,15 @@ msgstr ""
msgid "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>."
msgstr ""
+msgid "ImageDiffViewer|2-up"
+msgstr ""
+
+msgid "ImageDiffViewer|Onion skin"
+msgstr ""
+
+msgid "ImageDiffViewer|Swipe"
+msgstr ""
+
msgid "Import"
msgstr ""
@@ -2385,16 +2641,10 @@ msgstr ""
msgid "Import repository"
msgstr ""
-msgid "ImportButtons|Connect repositories from"
-msgstr ""
-
-msgid "Improve Issue boards with GitLab Enterprise Edition."
+msgid "Include a Terms of Service agreement and Privacy Policy that all users must accept."
msgstr ""
-msgid "Improve issues management with Issue weight and GitLab Enterprise Edition."
-msgstr ""
-
-msgid "Improve search with Advanced Global Search and GitLab Enterprise Edition."
+msgid "Inline"
msgstr ""
msgid "Install Runner on Kubernetes"
@@ -2403,19 +2653,15 @@ msgstr ""
msgid "Install a Runner compatible with GitLab CI"
msgstr ""
-msgid "Instance"
-msgid_plural "Instances"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
-
msgid "Instance does not support multiple Kubernetes clusters"
msgstr ""
msgid "Integrations"
msgstr ""
+msgid "Integrations Settings"
+msgstr ""
+
msgid "Interested parties can even contribute by pushing commits if they want to."
msgstr ""
@@ -2431,7 +2677,7 @@ msgstr ""
msgid "Introducing Cycle Analytics"
msgstr ""
-msgid "Issue board focus mode"
+msgid "Issue Board"
msgstr ""
msgid "Issue events"
@@ -2440,9 +2686,6 @@ msgstr ""
msgid "IssueBoards|Board"
msgstr ""
-msgid "IssueBoards|Boards"
-msgstr ""
-
msgid "Issues"
msgstr ""
@@ -2455,6 +2698,12 @@ msgstr ""
msgid "January"
msgstr ""
+msgid "Job"
+msgstr ""
+
+msgid "Job has been erased"
+msgstr ""
+
msgid "Jobs"
msgstr ""
@@ -2497,6 +2746,9 @@ msgstr ""
msgid "Kubernetes service integration has been deprecated. %{deprecated_message_content} your Kubernetes clusters using the new <a href=\"%{url}\"/>Kubernetes Clusters</a> page"
msgstr ""
+msgid "LFS"
+msgstr ""
+
msgid "LFSStatus|Disabled"
msgstr ""
@@ -2506,6 +2758,9 @@ msgstr ""
msgid "Label"
msgstr ""
+msgid "Label actions dropdown"
+msgstr ""
+
msgid "LabelSelect|%{firstLabelName} +%{remainingLabelCount} more"
msgstr ""
@@ -2521,6 +2776,9 @@ msgstr ""
msgid "Labels can be applied to issues and merge requests to categorize them."
msgstr ""
+msgid "Labels can be applied to issues and merge requests."
+msgstr ""
+
msgid "Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>"
msgstr ""
@@ -2558,6 +2816,9 @@ msgstr ""
msgid "LastPushEvent|at"
msgstr ""
+msgid "Latest changes"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -2582,9 +2843,6 @@ msgstr ""
msgid "Leave project"
msgstr ""
-msgid "License"
-msgstr ""
-
msgid "List"
msgstr ""
@@ -2594,6 +2852,9 @@ msgstr ""
msgid "Loading the GitLab IDE..."
msgstr ""
+msgid "Loading..."
+msgstr ""
+
msgid "Lock"
msgstr ""
@@ -2603,21 +2864,18 @@ msgstr ""
msgid "Lock not found"
msgstr ""
-msgid "Locked"
+msgid "Lock to current projects"
msgstr ""
-msgid "Locked Files"
+msgid "Locked"
msgstr ""
-msgid "Locks give the ability to lock specific file or folder."
+msgid "Locked to current projects"
msgstr ""
msgid "Login"
msgstr ""
-msgid "Make everyone on your team more productive regardless of their location. GitLab Geo creates read-only mirrors of your GitLab instance so you can reduce the time it takes to clone and fetch large repos."
-msgstr ""
-
msgid "Manage all notifications"
msgstr ""
@@ -2630,16 +2888,16 @@ msgstr ""
msgid "Manage project labels"
msgstr ""
-msgid "Manage your group’s membership while adding another level of security with SAML."
-msgstr ""
-
msgid "Mar"
msgstr ""
msgid "March"
msgstr ""
-msgid "Mark done"
+msgid "Mark todo as done"
+msgstr ""
+
+msgid "Markdown enabled"
msgstr ""
msgid "Maximum git storage failures"
@@ -2654,7 +2912,7 @@ msgstr ""
msgid "Members"
msgstr ""
-msgid "Members will be forwarded here when signing in to your group. Get this from your identity provider, where it can also be called \"SSO Service Location\", \"SAML Token Issuance Endpoint\", or \"SAML 2.0/W-Federation URL\"."
+msgid "Merge Request:"
msgstr ""
msgid "Merge Requests"
@@ -2666,91 +2924,46 @@ msgstr ""
msgid "Merge request"
msgstr ""
-msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
-msgstr ""
-
-msgid "Merged"
-msgstr ""
-
-msgid "Messages"
-msgstr ""
-
-msgid "Metrics - Influx"
-msgstr ""
-
-msgid "Metrics - Prometheus"
-msgstr ""
-
-msgid "Metrics|Business"
-msgstr ""
-
-msgid "Metrics|Create metric"
-msgstr ""
-
-msgid "Metrics|Edit metric"
-msgstr ""
-
-msgid "Metrics|For grouping similar metrics"
-msgstr ""
-
-msgid "Metrics|Label of the chart's vertical axis. Usually the type of the unit being charted. The horizontal axis (X-axis) always represents time."
-msgstr ""
-
-msgid "Metrics|Legend label (optional)"
-msgstr ""
-
-msgid "Metrics|Must be a valid PromQL query."
-msgstr ""
-
-msgid "Metrics|Name"
-msgstr ""
-
-msgid "Metrics|New metric"
+msgid "Merge requests"
msgstr ""
-msgid "Metrics|Prometheus Query Documentation"
-msgstr ""
-
-msgid "Metrics|Query"
-msgstr ""
-
-msgid "Metrics|Response"
+msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
msgstr ""
-msgid "Metrics|System"
+msgid "MergeRequests|Resolve this discussion in a new issue"
msgstr ""
-msgid "Metrics|Type"
+msgid "MergeRequests|Saving the comment failed"
msgstr ""
-msgid "Metrics|Unit label"
+msgid "MergeRequests|Toggle comments for this file"
msgstr ""
-msgid "Metrics|Used as a title for the chart"
+msgid "MergeRequests|Updating discussions failed"
msgstr ""
-msgid "Metrics|Used if the query returns a single series. If it returns multiple series, their legend labels will be picked up from the response."
+msgid "MergeRequests|View file @ %{commitId}"
msgstr ""
-msgid "Metrics|Y-axis label"
+msgid "MergeRequests|View replaced file @ %{commitId}"
msgstr ""
-msgid "Metrics|e.g. HTTP requests"
+msgid "Merged"
msgstr ""
-msgid "Metrics|e.g. Requests/second"
+msgid "Messages"
msgstr ""
-msgid "Metrics|e.g. Throughput"
+msgid "Metrics - Influx"
msgstr ""
-msgid "Metrics|e.g. rate(http_requests_total[5m])"
+msgid "Metrics - Prometheus"
msgstr ""
-msgid "Metrics|e.g. req/sec"
+msgid "Milestone"
msgstr ""
-msgid "Milestone"
+msgid "Milestones"
msgstr ""
msgid "Milestones|Delete milestone"
@@ -2771,9 +2984,6 @@ msgstr ""
msgid "Milestones|Promote Milestone"
msgstr ""
-msgid "Milestones|This action cannot be reversed."
-msgstr ""
-
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr ""
@@ -2786,7 +2996,7 @@ msgstr ""
msgid "Monitoring"
msgstr ""
-msgid "More info"
+msgid "More actions"
msgstr ""
msgid "More information"
@@ -2801,12 +3011,30 @@ msgstr ""
msgid "Move issue"
msgstr ""
-msgid "Multiple issue boards"
+msgid "Name"
msgstr ""
msgid "Name new label"
msgstr ""
+msgid "Name your individual key via a title"
+msgstr ""
+
+msgid "Nav|Help"
+msgstr ""
+
+msgid "Nav|Home"
+msgstr ""
+
+msgid "Nav|Sign In / Register"
+msgstr ""
+
+msgid "Nav|Sign out and sign in with a different account"
+msgstr ""
+
+msgid "New Identity"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] ""
@@ -2820,6 +3048,9 @@ msgstr ""
msgid "New Kubernetes cluster"
msgstr ""
+msgid "New Label"
+msgstr ""
+
msgid "New Pipeline Schedule"
msgstr ""
@@ -2832,15 +3063,15 @@ msgstr ""
msgid "New directory"
msgstr ""
-msgid "New epic"
-msgstr ""
-
msgid "New file"
msgstr ""
msgid "New group"
msgstr ""
+msgid "New identity"
+msgstr ""
+
msgid "New issue"
msgstr ""
@@ -2850,6 +3081,9 @@ msgstr ""
msgid "New merge request"
msgstr ""
+msgid "New pipelines will cancel older, pending pipelines on the same branch"
+msgstr ""
+
msgid "New project"
msgstr ""
@@ -2865,7 +3099,7 @@ msgstr ""
msgid "New tag"
msgstr ""
-msgid "No Label"
+msgid "No"
msgstr ""
msgid "No assignee"
@@ -2886,7 +3120,16 @@ msgstr ""
msgid "No file chosen"
msgstr ""
-msgid "No labels created yet."
+msgid "No files found"
+msgstr ""
+
+msgid "No files found."
+msgstr ""
+
+msgid "No merge requests found"
+msgstr ""
+
+msgid "No messages were logged"
msgstr ""
msgid "No repository"
@@ -2919,15 +3162,9 @@ msgstr ""
msgid "Note that the master branch is automatically protected. %{link_to_protected_branches}"
msgstr ""
-msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow connecting repositories without generating a Personal Access Token."
-msgstr ""
-
msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
msgstr ""
-msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow connecting repositories without generating a Personal Access Token."
-msgstr ""
-
msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
msgstr ""
@@ -3003,9 +3240,6 @@ msgstr ""
msgid "Number of access attempts"
msgstr ""
-msgid "OK"
-msgstr ""
-
msgid "Oct"
msgstr ""
@@ -3015,19 +3249,16 @@ msgstr ""
msgid "OfSearchInADropdown|Filter"
msgstr ""
-msgid "Once imported, repositories can be mirrored over SSH. Read more %{ssh_link}"
-msgstr ""
-
msgid "Online IDE integration settings."
msgstr ""
-msgid "Only project members can comment."
+msgid "Only comments from the following commit are shown below"
msgstr ""
-msgid "Open"
+msgid "Only project members can comment."
msgstr ""
-msgid "Opened"
+msgid "Open in Xcode"
msgstr ""
msgid "OpenedNDaysAgo|Opened"
@@ -3036,9 +3267,18 @@ msgstr ""
msgid "Opens in a new window"
msgstr ""
+msgid "Operations"
+msgstr ""
+
msgid "Options"
msgstr ""
+msgid "Or you can choose one of the suggested colors below"
+msgstr ""
+
+msgid "Other Labels"
+msgstr ""
+
msgid "Otherwise it is recommended you start with one of the options below."
msgstr ""
@@ -3072,12 +3312,27 @@ msgstr ""
msgid "Password"
msgstr ""
+msgid "Paste your public SSH key, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'. Don't use your private SSH key."
+msgstr ""
+
+msgid "Pause"
+msgstr ""
+
msgid "Pending"
msgstr ""
+msgid "Per job. If a job passes this threshold, it will be marked as failed"
+msgstr ""
+
+msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgstr ""
+
msgid "Performance optimization"
msgstr ""
+msgid "Permissions"
+msgstr ""
+
msgid "Personal Access Token"
msgstr ""
@@ -3093,7 +3348,7 @@ msgstr ""
msgid "Pipeline Schedules"
msgstr ""
-msgid "Pipeline quota"
+msgid "Pipeline triggers"
msgstr ""
msgid "PipelineCharts|Failed:"
@@ -3192,10 +3447,22 @@ msgstr ""
msgid "Pipelines|This project is not currently set up to run pipelines."
msgstr ""
-msgid "Pipeline|Retry pipeline"
+msgid "Pipeline|Create for"
+msgstr ""
+
+msgid "Pipeline|Create pipeline"
+msgstr ""
+
+msgid "Pipeline|Existing branch name or tag"
+msgstr ""
+
+msgid "Pipeline|Run Pipeline"
+msgstr ""
+
+msgid "Pipeline|Search branches"
msgstr ""
-msgid "Pipeline|Retry pipeline #%{pipelineId}?"
+msgid "Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default."
msgstr ""
msgid "Pipeline|Stop pipeline"
@@ -3204,7 +3471,7 @@ msgstr ""
msgid "Pipeline|Stop pipeline #%{pipelineId}?"
msgstr ""
-msgid "Pipeline|You’re about to retry pipeline %{pipelineId}."
+msgid "Pipeline|Variables"
msgstr ""
msgid "Pipeline|You’re about to stop pipeline %{pipelineId}."
@@ -3222,19 +3489,25 @@ msgstr ""
msgid "Pipeline|with stages"
msgstr ""
+msgid "Plain diff"
+msgstr ""
+
msgid "PlantUML"
msgstr ""
msgid "Play"
msgstr ""
-msgid "Please <a href=%{link_to_billing} target=\"_blank\" rel=\"noopener noreferrer\">enable billing for one of your projects to be able to create a Kubernetes cluster</a>, then try again."
+msgid "Please accept the Terms of Service before continuing."
+msgstr ""
+
+msgid "Please select at least one filter to see results"
msgstr ""
msgid "Please solve the reCAPTCHA"
msgstr ""
-msgid "Please wait while we connect to your repository. Refresh at will."
+msgid "Please try again"
msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
@@ -3243,7 +3516,19 @@ msgstr ""
msgid "Preferences"
msgstr ""
-msgid "Primary"
+msgid "Preferences|Navigation theme"
+msgstr ""
+
+msgid "Prioritize"
+msgstr ""
+
+msgid "Prioritize label"
+msgstr ""
+
+msgid "Prioritized Labels"
+msgstr ""
+
+msgid "Prioritized label"
msgstr ""
msgid "Private - Project access must be granted explicitly to each user."
@@ -3261,6 +3546,12 @@ msgstr ""
msgid "Profiles|Account scheduled for removal."
msgstr ""
+msgid "Profiles|Change username"
+msgstr ""
+
+msgid "Profiles|Current path: %{path}"
+msgstr ""
+
msgid "Profiles|Delete Account"
msgstr ""
@@ -3279,9 +3570,21 @@ msgstr ""
msgid "Profiles|Invalid username"
msgstr ""
+msgid "Profiles|Path"
+msgstr ""
+
msgid "Profiles|Type your %{confirmationValue} to confirm:"
msgstr ""
+msgid "Profiles|Update username"
+msgstr ""
+
+msgid "Profiles|Username change failed - %{message}"
+msgstr ""
+
+msgid "Profiles|Username successfully changed"
+msgstr ""
+
msgid "Profiles|You don't have access to delete this user."
msgstr ""
@@ -3300,6 +3603,12 @@ msgstr ""
msgid "Programming languages used in this repository"
msgstr ""
+msgid "Progress"
+msgstr ""
+
+msgid "Project"
+msgstr ""
+
msgid "Project '%{project_name}' is in the process of being deleted."
msgstr ""
@@ -3312,6 +3621,9 @@ msgstr ""
msgid "Project '%{project_name}' was successfully updated."
msgstr ""
+msgid "Project Badges"
+msgstr ""
+
msgid "Project access must be granted explicitly to each user."
msgstr ""
@@ -3339,30 +3651,6 @@ msgstr ""
msgid "ProjectActivityRSS|Subscribe"
msgstr ""
-msgid "ProjectCreationLevel|Allowed to create projects"
-msgstr ""
-
-msgid "ProjectCreationLevel|Default project creation protection"
-msgstr ""
-
-msgid "ProjectCreationLevel|Developers + Masters"
-msgstr ""
-
-msgid "ProjectCreationLevel|Masters"
-msgstr ""
-
-msgid "ProjectCreationLevel|No one"
-msgstr ""
-
-msgid "ProjectFeature|Disabled"
-msgstr ""
-
-msgid "ProjectFeature|Everyone with access"
-msgstr ""
-
-msgid "ProjectFeature|Only team members"
-msgstr ""
-
msgid "ProjectFileTree|Name"
msgstr ""
@@ -3372,27 +3660,6 @@ msgstr ""
msgid "ProjectLifecycle|Stage"
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 ""
@@ -3417,6 +3684,9 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr ""
+msgid "PrometheusDashboard|Time"
+msgstr ""
+
msgid "PrometheusService|%{exporters} with %{metrics} were found"
msgstr ""
@@ -3438,18 +3708,9 @@ msgstr ""
msgid "PrometheusService|Common metrics"
msgstr ""
-msgid "PrometheusService|Common metrics are automatically monitored based on a library of metrics from popular exporters."
-msgstr ""
-
-msgid "PrometheusService|Custom metrics"
-msgstr ""
-
msgid "PrometheusService|Finding and configuring metrics..."
msgstr ""
-msgid "PrometheusService|Finding custom metrics..."
-msgstr ""
-
msgid "PrometheusService|Install Prometheus on clusters"
msgstr ""
@@ -3462,13 +3723,13 @@ msgstr ""
msgid "PrometheusService|Metrics"
msgstr ""
-msgid "PrometheusService|Missing environment variable"
+msgid "PrometheusService|Metrics are automatically configured and monitored based on a library of metrics from popular exporters."
msgstr ""
-msgid "PrometheusService|More information"
+msgid "PrometheusService|Missing environment variable"
msgstr ""
-msgid "PrometheusService|New metric"
+msgid "PrometheusService|More information"
msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
@@ -3477,9 +3738,6 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
-msgstr ""
-
msgid "PrometheusService|Time-series monitoring service"
msgstr ""
@@ -3495,22 +3753,28 @@ msgstr ""
msgid "Promote"
msgstr ""
-msgid "Promote to Group Label"
+msgid "Promote these project milestones into a group milestone."
msgstr ""
msgid "Promote to Group Milestone"
msgstr ""
+msgid "Promote to group label"
+msgstr ""
+
msgid "Protip:"
msgstr ""
+msgid "Provider"
+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"
+msgid "Public pipelines"
msgstr ""
msgid "Push events"
@@ -3522,10 +3786,10 @@ msgstr ""
msgid "Push to create a project"
msgstr ""
-msgid "PushRule|Committer restriction"
+msgid "Quick actions can be used in the issues description and comment boxes."
msgstr ""
-msgid "Quick actions can be used in the issues description and comment boxes."
+msgid "Re-deploy"
msgstr ""
msgid "Read more"
@@ -3537,16 +3801,16 @@ msgstr ""
msgid "Real-time features"
msgstr ""
-msgid "RefSwitcher|Branches"
+msgid "Reference:"
msgstr ""
-msgid "RefSwitcher|Tags"
+msgid "Register / Sign In"
msgstr ""
-msgid "Reference:"
+msgid "Register and see your runners for this group."
msgstr ""
-msgid "Register / Sign In"
+msgid "Register and see your runners for this project."
msgstr ""
msgid "Registry"
@@ -3579,28 +3843,28 @@ msgstr ""
msgid "Remove"
msgstr ""
-msgid "Remove avatar"
+msgid "Remove Runner"
msgstr ""
-msgid "Remove project"
+msgid "Remove avatar"
msgstr ""
-msgid "Repair authentication"
+msgid "Remove priority"
msgstr ""
-msgid "Repo by URL"
+msgid "Remove project"
msgstr ""
msgid "Repository"
msgstr ""
-msgid "Repository has no locks."
+msgid "Repository Settings"
msgstr ""
msgid "Repository maintenance"
msgstr ""
-msgid "Repository mirror settings"
+msgid "Repository mirror"
msgstr ""
msgid "Repository storage"
@@ -3609,6 +3873,9 @@ msgstr ""
msgid "Request Access"
msgstr ""
+msgid "Require all users to accept Terms of Service and Privacy Policy when they access GitLab."
+msgstr ""
+
msgid "Reset git storage health information"
msgstr ""
@@ -3618,10 +3885,25 @@ msgstr ""
msgid "Reset runners registration token"
msgstr ""
+msgid "Resolve all discussions in new issue"
+msgstr ""
+
+msgid "Resolve conflicts on source branch"
+msgstr ""
+
msgid "Resolve discussion"
msgstr ""
-msgid "Response"
+msgid "Resume"
+msgstr ""
+
+msgid "Retry"
+msgstr ""
+
+msgid "Retry this job"
+msgstr ""
+
+msgid "Retry verification"
msgstr ""
msgid "Reveal value"
@@ -3637,7 +3919,7 @@ msgstr ""
msgid "Revert this merge request"
msgstr ""
-msgid "Review the process for configuring service providers in your identity provider — in this case, GitLab is the \"service provider\" or \"relying party\"."
+msgid "Review"
msgstr ""
msgid "Reviewing"
@@ -3646,28 +3928,31 @@ msgstr ""
msgid "Reviewing (merge request !%{mergeRequestId})"
msgstr ""
-msgid "Roadmap"
+msgid "Rollback"
msgstr ""
-msgid "Run CI/CD pipelines for external repositories"
+msgid "Runner token"
msgstr ""
msgid "Runners"
msgstr ""
-msgid "Running"
+msgid "Runners API"
+msgstr ""
+
+msgid "Runners can be placed on separate users, servers, and even on your local machine."
msgstr ""
-msgid "SAML Single Sign On"
+msgid "Running"
msgstr ""
-msgid "SAML Single Sign On Settings"
+msgid "SSH Keys"
msgstr ""
-msgid "SHA1 fingerprint of the SAML token signing certificate. Get this from your identity provider, where it can also be called \"Thumbprint\"."
+msgid "SSL Verification"
msgstr ""
-msgid "SSH Keys"
+msgid "Save"
msgstr ""
msgid "Save changes"
@@ -3691,15 +3976,30 @@ msgstr ""
msgid "Scheduling Pipelines"
msgstr ""
-msgid "Scoped issue boards"
+msgid "Scroll to bottom"
+msgstr ""
+
+msgid "Scroll to top"
msgstr ""
msgid "Search"
msgstr ""
+msgid "Search branches"
+msgstr ""
+
msgid "Search branches and tags"
msgstr ""
+msgid "Search files"
+msgstr ""
+
+msgid "Search for projects, issues, etc."
+msgstr ""
+
+msgid "Search merge requests"
+msgstr ""
+
msgid "Search milestones"
msgstr ""
@@ -3715,13 +4015,13 @@ msgstr ""
msgid "Seconds to wait for a storage access attempt"
msgstr ""
-msgid "Secret variables"
+msgid "Select"
msgstr ""
-msgid "Security report"
+msgid "Select Archive Format"
msgstr ""
-msgid "Select Archive Format"
+msgid "Select a namespace to fork the project"
msgstr ""
msgid "Select a timezone"
@@ -3736,10 +4036,19 @@ msgstr ""
msgid "Select branch/tag"
msgstr ""
-msgid "Select target branch"
+msgid "Select project"
+msgstr ""
+
+msgid "Select project and zone to choose machine type"
msgstr ""
-msgid "Selective synchronization"
+msgid "Select project to choose zone"
+msgstr ""
+
+msgid "Select source branch"
+msgstr ""
+
+msgid "Select target branch"
msgstr ""
msgid "Send email"
@@ -3757,9 +4066,6 @@ msgstr ""
msgid "Service Templates"
msgstr ""
-msgid "Service URL"
-msgstr ""
-
msgid "Session expiration, projects limit and attachment size."
msgstr ""
@@ -3784,9 +4090,6 @@ msgstr ""
msgid "Set up Koding"
msgstr ""
-msgid "Set up assertions/attributes/claims (email, first_name, last_name) and NameID according to %{docsLinkStart}the documentation %{icon}%{docsLinkEnd}"
-msgstr ""
-
msgid "SetPasswordToCloneLink|set a password"
msgstr ""
@@ -3796,19 +4099,22 @@ msgstr ""
msgid "Setup a specific Runner automatically"
msgstr ""
-msgid "Share the <strong>%{sso_label}</strong> with members so they can sign in to your group through your identity provider"
+msgid "Share"
msgstr ""
-msgid "SharedRunnersMinutesSettings|By resetting the pipeline minutes for this namespace, the currently used minutes will be set to zero."
+msgid "Shared Runners"
msgstr ""
-msgid "SharedRunnersMinutesSettings|Reset pipeline minutes"
+msgid "Show command"
msgstr ""
-msgid "SharedRunnersMinutesSettings|Reset used pipeline minutes"
+msgid "Show complete raw log"
msgstr ""
-msgid "Show command"
+msgid "Show latest version"
+msgstr ""
+
+msgid "Show latest version of the diff"
msgstr ""
msgid "Show parent pages"
@@ -3817,6 +4123,9 @@ msgstr ""
msgid "Show parent subgroups"
msgstr ""
+msgid "Show whitespace changes"
+msgstr ""
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] ""
@@ -3824,16 +4133,10 @@ msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
-msgid "Sidebar|Change weight"
-msgstr ""
-
-msgid "Sidebar|No"
+msgid "Side-by-side"
msgstr ""
-msgid "Sidebar|None"
-msgstr ""
-
-msgid "Sidebar|Weight"
+msgid "Sign out"
msgstr ""
msgid "Sign-in restrictions"
@@ -3845,7 +4148,7 @@ msgstr ""
msgid "Size and domain settings for static websites"
msgstr ""
-msgid "Slack application"
+msgid "Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job"
msgstr ""
msgid "Snippets"
@@ -3857,13 +4160,13 @@ msgstr ""
msgid "Something went wrong on our end."
msgstr ""
-msgid "Something went wrong when toggling the button"
+msgid "Something went wrong on our end. Please try again!"
msgstr ""
-msgid "Something went wrong while fetching Dependency Scanning."
+msgid "Something went wrong when toggling the button"
msgstr ""
-msgid "Something went wrong while fetching SAST."
+msgid "Something went wrong while closing the %{issuable}. Please try again later"
msgstr ""
msgid "Something went wrong while fetching the projects."
@@ -3872,6 +4175,12 @@ msgstr ""
msgid "Something went wrong while fetching the registry list."
msgstr ""
+msgid "Something went wrong while reopening the %{issuable}. Please try again later"
+msgstr ""
+
+msgid "Something went wrong while resolving this discussion. Please try again."
+msgstr ""
+
msgid "Something went wrong. Please try again."
msgstr ""
@@ -3917,9 +4226,6 @@ msgstr ""
msgid "SortOptions|Least popular"
msgstr ""
-msgid "SortOptions|Less weight"
-msgstr ""
-
msgid "SortOptions|Milestone"
msgstr ""
@@ -3929,9 +4235,6 @@ msgstr ""
msgid "SortOptions|Milestone due soon"
msgstr ""
-msgid "SortOptions|More weight"
-msgstr ""
-
msgid "SortOptions|Most popular"
msgstr ""
@@ -3971,9 +4274,6 @@ msgstr ""
msgid "SortOptions|Start soon"
msgstr ""
-msgid "SortOptions|Weight"
-msgstr ""
-
msgid "Source"
msgstr ""
@@ -3992,9 +4292,36 @@ msgstr ""
msgid "Spam and Anti-bot Protection"
msgstr ""
+msgid "Specific Runners"
+msgstr ""
+
msgid "Specify the following URL during the Runner setup:"
msgstr ""
+msgid "Squash commits"
+msgstr ""
+
+msgid "Stage"
+msgstr ""
+
+msgid "Stage & Commit"
+msgstr ""
+
+msgid "Stage all changes"
+msgstr ""
+
+msgid "Stage changes"
+msgstr ""
+
+msgid "Staged"
+msgstr ""
+
+msgid "Staged %{type}"
+msgstr ""
+
+msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
+msgstr ""
+
msgid "StarProject|Star"
msgstr ""
@@ -4016,12 +4343,15 @@ msgstr ""
msgid "Started"
msgstr ""
-msgid "State your message to activate"
+msgid "Starts at (UTC)"
msgstr ""
msgid "Status"
msgstr ""
+msgid "Stop this environment"
+msgstr ""
+
msgid "Stopped"
msgstr ""
@@ -4031,16 +4361,19 @@ msgstr ""
msgid "Subgroups"
msgstr ""
-msgid "Switch branch/tag"
+msgid "Subscribe"
msgstr ""
-msgid "System"
+msgid "Subscribe at group level"
msgstr ""
-msgid "System Hooks"
+msgid "Subscribe at project level"
msgstr ""
-msgid "System header and footer:"
+msgid "Switch branch/tag"
+msgstr ""
+
+msgid "System Hooks"
msgstr ""
msgid "Tag (%{tag_count})"
@@ -4053,6 +4386,9 @@ msgstr[3] ""
msgid "Tags"
msgstr ""
+msgid "Tags:"
+msgstr ""
+
msgid "TagsPage|Browse commits"
msgstr ""
@@ -4116,7 +4452,7 @@ msgstr ""
msgid "TagsPage|Use git tag command to add a new one:"
msgstr ""
-msgid "TagsPage|Write your release notes or drag files here..."
+msgid "TagsPage|Write your release notes or drag files here…"
msgstr ""
msgid "TagsPage|protected"
@@ -4131,19 +4467,19 @@ msgstr ""
msgid "Team"
msgstr ""
-msgid "Thanks! Don't show me this again"
+msgid "Terms of Service Agreement and Privacy Policy"
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."
+msgid "Terms of Service and Privacy Policy"
msgstr ""
-msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project"
+msgid "Test coverage parsing"
msgstr ""
-msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
+msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project"
msgstr ""
-msgid "The X509 Certificate to use when mutual TLS is required to communicate with the external authorization service. If left blank, the server certificate is still validated when accessing over HTTPS."
+msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
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."
@@ -4152,9 +4488,6 @@ msgstr ""
msgid "The collection of events added to the data gathered for that stage."
msgstr ""
-msgid "The connection will time out after %{timeout}. For repositories that take longer, use a clone/push combination."
-msgstr ""
-
msgid "The fork relationship has been removed."
msgstr ""
@@ -4173,7 +4506,7 @@ 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 passphrase required to decrypt the private key. This is optional and the value is encrypted at rest."
+msgid "The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>"
msgstr ""
msgid "The phase of the development lifecycle."
@@ -4182,9 +4515,6 @@ msgstr ""
msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit."
msgstr ""
-msgid "The private key to use when a client certificate is provided. This value is encrypted at rest."
-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 ""
@@ -4206,7 +4536,7 @@ msgstr ""
msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request."
msgstr ""
-msgid "The roadmap shows the progress of your epics along a timeline"
+msgid "The secure token used by the Runner to checkout the project"
msgstr ""
msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time."
@@ -4233,13 +4563,19 @@ msgstr ""
msgid "There are no issues to show"
msgstr ""
+msgid "There are no labels yet"
+msgstr ""
+
msgid "There are no merge requests to show"
msgstr ""
msgid "There are problems accessing Git storage: "
msgstr ""
-msgid "There was an error loading results"
+msgid "There was an error loading jobs"
+msgstr ""
+
+msgid "There was an error loading latest pipeline"
msgstr ""
msgid "There was an error loading users activity calendar."
@@ -4260,12 +4596,21 @@ msgstr ""
msgid "There was an error when unsubscribing from this label."
msgstr ""
-msgid "This board\\'s scope is reduced"
+msgid "They can be managed using the %{link}."
+msgstr ""
+
+msgid "This GitLab instance does not provide any shared Runners yet. Instance administrators can register shared Runners in the admin area."
+msgstr ""
+
+msgid "This diff is collapsed."
msgstr ""
msgid "This directory"
msgstr ""
+msgid "This group does not provide any group Runners yet."
+msgstr ""
+
msgid "This is a confidential issue."
msgstr ""
@@ -4287,6 +4632,15 @@ msgstr ""
msgid "This job depends on upstream jobs that need to succeed in order for this job to be triggered"
msgstr ""
+msgid "This job does not have a trace."
+msgstr ""
+
+msgid "This job has been canceled"
+msgstr ""
+
+msgid "This job has been skipped"
+msgstr ""
+
msgid "This job has not been triggered yet"
msgstr ""
@@ -4305,19 +4659,28 @@ msgstr ""
msgid "This merge request is locked."
msgstr ""
+msgid "This option is disabled while you still have unstaged changes"
+msgstr ""
+
msgid "This page is unavailable because you are not allowed to read information across multiple projects."
msgstr ""
+msgid "This page will be removed in a future release."
+msgstr ""
+
msgid "This project"
msgstr ""
+msgid "This project does not belong to a group and can therefore not make use of group Runners."
+msgstr ""
+
msgid "This repository"
msgstr ""
-msgid "This will delete the custom metric, Are you sure?"
+msgid "This source diff could not be displayed because it is too large."
msgstr ""
-msgid "Those emails automatically become issues (with the comments becoming the email conversation) listed here."
+msgid "This user has no identities"
msgstr ""
msgid "Time before an issue gets scheduled"
@@ -4329,10 +4692,10 @@ msgstr ""
msgid "Time between merge request creation and merge/close"
msgstr ""
-msgid "Time between updates and capacity settings."
+msgid "Time remaining"
msgstr ""
-msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
+msgid "Time spent"
msgstr ""
msgid "Time tracking"
@@ -4356,6 +4719,9 @@ msgstr ""
msgid "Timeago|%s days remaining"
msgstr ""
+msgid "Timeago|%s hours ago"
+msgstr ""
+
msgid "Timeago|%s hours remaining"
msgstr ""
@@ -4371,6 +4737,9 @@ msgstr ""
msgid "Timeago|%s months remaining"
msgstr ""
+msgid "Timeago|%s seconds ago"
+msgstr ""
+
msgid "Timeago|%s seconds remaining"
msgstr ""
@@ -4386,46 +4755,43 @@ msgstr ""
msgid "Timeago|%s years remaining"
msgstr ""
-msgid "Timeago|1 day remaining"
-msgstr ""
-
-msgid "Timeago|1 hour remaining"
+msgid "Timeago|1 day ago"
msgstr ""
-msgid "Timeago|1 minute remaining"
+msgid "Timeago|1 day remaining"
msgstr ""
-msgid "Timeago|1 month remaining"
+msgid "Timeago|1 hour ago"
msgstr ""
-msgid "Timeago|1 week remaining"
+msgid "Timeago|1 hour remaining"
msgstr ""
-msgid "Timeago|1 year remaining"
+msgid "Timeago|1 minute ago"
msgstr ""
-msgid "Timeago|Past due"
+msgid "Timeago|1 minute remaining"
msgstr ""
-msgid "Timeago|a day ago"
+msgid "Timeago|1 month ago"
msgstr ""
-msgid "Timeago|a month ago"
+msgid "Timeago|1 month remaining"
msgstr ""
-msgid "Timeago|a week ago"
+msgid "Timeago|1 week ago"
msgstr ""
-msgid "Timeago|a year ago"
+msgid "Timeago|1 week remaining"
msgstr ""
-msgid "Timeago|about %s hours ago"
+msgid "Timeago|1 year ago"
msgstr ""
-msgid "Timeago|about a minute ago"
+msgid "Timeago|1 year remaining"
msgstr ""
-msgid "Timeago|about an hour ago"
+msgid "Timeago|Past due"
msgstr ""
msgid "Timeago|in %s days"
@@ -4467,10 +4833,13 @@ msgstr ""
msgid "Timeago|in 1 year"
msgstr ""
-msgid "Timeago|in a while"
+msgid "Timeago|just now"
+msgstr ""
+
+msgid "Timeago|right now"
msgstr ""
-msgid "Timeago|less than a minute ago"
+msgid "Timeout"
msgstr ""
msgid "Time|hr"
@@ -4493,19 +4862,10 @@ msgstr ""
msgid "Tip:"
msgstr ""
-msgid "Title"
-msgstr ""
-
msgid "To GitLab"
msgstr ""
-msgid "To connect GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to connect."
-msgstr ""
-
-msgid "To connect GitHub repositories, you first need to authorize GitLab to access the list of your GitHub repositories:"
-msgstr ""
-
-msgid "To connect an SVN repository, check out %{svn_link}."
+msgid "To add an SSH key you need to %{generate_link_start}generate one%{link_end} or use an %{existing_link_start}existing key%{link_end}."
msgstr ""
msgid "To import GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
@@ -4517,19 +4877,19 @@ msgstr ""
msgid "To import an SVN repository, check out %{svn_link}."
msgstr ""
-msgid "To only use CI/CD features for an external repository, choose <strong>CI/CD for external repo</strong>."
+msgid "To start serving your jobs you can add Runners to your group"
msgstr ""
-msgid "To set up SAML authentication for your group through an identity provider like Azure, Okta, Onelogin, Ping Identity, or your custom SAML 2.0 provider:"
+msgid "To validate your GitLab CI configurations, go to 'CI/CD → Pipelines' inside your project, and click on the 'CI Lint' button."
msgstr ""
-msgid "To validate your GitLab CI configurations, go to 'CI/CD → Pipelines' inside your project, and click on the 'CI Lint' button."
+msgid "Todo"
msgstr ""
-msgid "To view the roadmap, add a planned start or finish date to one of your epics in this group or its subgroups. Only epics in the past 3 months and the next 3 months are shown."
+msgid "Toggle Sidebar"
msgstr ""
-msgid "Todo"
+msgid "Toggle discussion"
msgstr ""
msgid "Toggle sidebar"
@@ -4541,6 +4901,9 @@ msgstr ""
msgid "ToggleButton|Toggle Status: ON"
msgstr ""
+msgid "Too many changes to show."
+msgstr ""
+
msgid "Total Time"
msgstr ""
@@ -4550,22 +4913,19 @@ msgstr ""
msgid "Total: %{total}"
msgstr ""
-msgid "Track activity with Contribution Analytics."
-msgstr ""
-
-msgid "Track groups of issues that share a theme, across projects and milestones"
-msgstr ""
-
msgid "Track time with quick actions"
msgstr ""
msgid "Trigger this manual action"
msgstr ""
-msgid "Turn on Service Desk"
+msgid "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."
msgstr ""
-msgid "Unknown"
+msgid "Try again"
+msgstr ""
+
+msgid "Unable to load the diff. %{button_try_again}"
msgstr ""
msgid "Unlock"
@@ -4577,25 +4937,47 @@ msgstr ""
msgid "Unresolve discussion"
msgstr ""
+msgid "Unstage all changes"
+msgstr ""
+
+msgid "Unstage changes"
+msgstr ""
+
+msgid "Unstaged"
+msgstr ""
+
+msgid "Unstaged %{type}"
+msgstr ""
+
+msgid "Unstaged and staged %{type}"
+msgstr ""
+
msgid "Unstar"
msgstr ""
-msgid "Up to date"
+msgid "Unsubscribe"
msgstr ""
-msgid "Upgrade your plan to activate Advanced Global Search."
+msgid "Unsubscribe at group level"
msgstr ""
-msgid "Upgrade your plan to activate Contribution Analytics."
+msgid "Unsubscribe at project level"
msgstr ""
-msgid "Upgrade your plan to activate Group Webhooks."
+msgid "Unverified"
msgstr ""
-msgid "Upgrade your plan to activate Issue weight."
+msgid "Up to date"
msgstr ""
-msgid "Upgrade your plan to improve Issue boards."
+msgid "Update %{files}"
+msgid_plural "Update %{files} files"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "Update your group name, description, avatar, and other general settings."
msgstr ""
msgid "Upload New File"
@@ -4616,7 +4998,7 @@ msgstr ""
msgid "Usage statistics"
msgstr ""
-msgid "Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab"
+msgid "Use group milestones to manage issues from multiple projects in the same milestone."
msgstr ""
msgid "Use the following registration token during setup:"
@@ -4625,10 +5007,13 @@ msgstr ""
msgid "Use your global notification setting"
msgstr ""
-msgid "Used by members to sign in to your group in GitLab"
+msgid "User and IP Rate Limits"
msgstr ""
-msgid "User and IP Rate Limits"
+msgid "Users"
+msgstr ""
+
+msgid "Variables"
msgstr ""
msgid "Variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. You can use variables for passwords, secret keys, or whatever you want."
@@ -4643,10 +5028,7 @@ msgstr ""
msgid "Various settings that affect GitLab performance."
msgstr ""
-msgid "View and edit lines"
-msgstr ""
-
-msgid "View epics list"
+msgid "Verified"
msgstr ""
msgid "View file @ "
@@ -4655,9 +5037,15 @@ msgstr ""
msgid "View group labels"
msgstr ""
+msgid "View jobs"
+msgstr ""
+
msgid "View labels"
msgstr ""
+msgid "View log"
+msgstr ""
+
msgid "View open merge request"
msgstr ""
@@ -4685,9 +5073,6 @@ msgstr ""
msgid "Want to see the data? Please ask an administrator for access."
msgstr ""
-msgid "We could not verify that one of your projects on GCP has billing enabled. Please try again."
-msgstr ""
-
msgid "We don't have enough data to show this stage."
msgstr ""
@@ -4700,13 +5085,10 @@ msgstr ""
msgid "Web terminal"
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."
+msgid "When a runner is locked, it cannot be assigned to other projects"
msgstr ""
-msgid "Weight"
-msgstr ""
-
-msgid "When leaving the URL blank, classification labels can still be specified whitout disabling cross project features or performing external authorization checks."
+msgid "When enabled, users cannot use GitLab until the terms have been accepted."
msgstr ""
msgid "Wiki"
@@ -4733,7 +5115,31 @@ msgstr ""
msgid "WikiEdit|There is already a page with the same title in that path."
msgstr ""
-msgid "WikiEmptyPageError|You are not allowed to create wiki pages"
+msgid "WikiEmptyIssueMessage|Suggest wiki improvement"
+msgstr ""
+
+msgid "WikiEmptyIssueMessage|You must be a project member in order to add wiki pages. If you have suggestions for how to improve the wiki for this project, consider opening an issue in the %{issues_link}."
+msgstr ""
+
+msgid "WikiEmptyIssueMessage|issue tracker"
+msgstr ""
+
+msgid "WikiEmpty|A wiki is where you can store all the details about your project. This can include why you've created it, its principles, how to use it, and so on."
+msgstr ""
+
+msgid "WikiEmpty|Create your first page"
+msgstr ""
+
+msgid "WikiEmpty|Suggest wiki improvement"
+msgstr ""
+
+msgid "WikiEmpty|The wiki lets you write documentation for your project"
+msgstr ""
+
+msgid "WikiEmpty|This project has no wiki pages"
+msgstr ""
+
+msgid "WikiEmpty|You must be a project member in order to add wiki pages."
msgstr ""
msgid "WikiHistoricalPage|This is an old version of this page."
@@ -4769,6 +5175,12 @@ msgstr ""
msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?"
msgstr ""
+msgid "WikiPageConfirmDelete|Delete page"
+msgstr ""
+
+msgid "WikiPageConfirmDelete|Delete page %{pageTitle}?"
+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 ""
@@ -4784,7 +5196,7 @@ msgstr ""
msgid "WikiPage|Page slug"
msgstr ""
-msgid "WikiPage|Write your content or drag files here..."
+msgid "WikiPage|Write your content or drag files here…"
msgstr ""
msgid "Wiki|Create Page"
@@ -4796,9 +5208,6 @@ msgstr ""
msgid "Wiki|Edit Page"
msgstr ""
-msgid "Wiki|Empty page"
-msgstr ""
-
msgid "Wiki|More Pages"
msgstr ""
@@ -4817,13 +5226,10 @@ 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 ""
-msgid "Write a commit message..."
+msgid "Yes"
msgstr ""
msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
@@ -4841,7 +5247,7 @@ msgstr ""
msgid "You are on a read-only GitLab instance."
msgstr ""
-msgid "You are on a secondary (read-only) Geo node. If you want to make any changes, you must visit the %{primary_node}."
+msgid "You can %{linkStart}view the blob%{linkEnd} instead."
msgstr ""
msgid "You can also create a project from the command line."
@@ -4850,6 +5256,9 @@ msgstr ""
msgid "You can also star a label to make it a priority label."
msgstr ""
+msgid "You can also test your .gitlab-ci.yml in the %{linkStart}Lint%{linkEnd}"
+msgstr ""
+
msgid "You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}"
msgstr ""
@@ -4862,28 +5271,31 @@ msgstr ""
msgid "You can only edit 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."
+msgid "You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}"
msgstr ""
msgid "You cannot write to this read-only GitLab instance."
msgstr ""
-msgid "You do not have the correct permissions to override the settings from the LDAP group sync."
+msgid "You do not have any assigned merge requests"
msgstr ""
msgid "You have no permissions"
msgstr ""
+msgid "You have not created any merge requests"
+msgstr ""
+
msgid "You have reached your project limit"
msgstr ""
-msgid "You must have master access to force delete a lock"
+msgid "You must accept our Terms of Service and privacy policy in order to register an account"
msgstr ""
-msgid "You must sign in to star a project"
+msgid "You must have maintainer access to force delete a lock"
msgstr ""
-msgid "You need a different license to enable FileLocks feature"
+msgid "You must sign in to star a project"
msgstr ""
msgid "You need permission."
@@ -4952,15 +5364,11 @@ msgstr ""
msgid "Your projects"
msgstr ""
-msgid "among other things"
+msgid "ago"
msgstr ""
-msgid "and %d fixed vulnerability"
-msgid_plural "and %d fixed vulnerabilities"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
+msgid "among other things"
+msgstr ""
msgid "assign yourself"
msgstr ""
@@ -4968,96 +5376,12 @@ msgstr ""
msgid "branch name"
msgstr ""
-msgid "by"
-msgstr ""
-
-msgid "ciReport|%{type} detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|%{type} detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Code quality"
-msgstr ""
-
-msgid "ciReport|DAST detected no alerts by analyzing the review app"
-msgstr ""
-
-msgid "ciReport|Dependency scanning"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Failed to load %{reportName} report"
-msgstr ""
-
-msgid "ciReport|Fixed:"
-msgstr ""
-
-msgid "ciReport|Instances"
-msgstr ""
-
-msgid "ciReport|Learn more about whitelisting"
-msgstr ""
-
-msgid "ciReport|Loading %{reportName} report"
-msgstr ""
-
-msgid "ciReport|No changes to code quality"
-msgstr ""
-
-msgid "ciReport|No changes to performance metrics"
-msgstr ""
-
-msgid "ciReport|Performance metrics"
-msgstr ""
-
-msgid "ciReport|SAST"
-msgstr ""
-
-msgid "ciReport|SAST detected"
-msgstr ""
-
-msgid "ciReport|SAST detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|SAST detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|SAST:container no vulnerabilities were found"
-msgstr ""
-
-msgid "ciReport|Security scanning"
-msgstr ""
-
-msgid "ciReport|Security scanning failed loading any results"
-msgstr ""
-
-msgid "ciReport|Show complete code vulnerabilities report"
-msgstr ""
-
-msgid "ciReport|Unapproved vulnerabilities (red) can be marked as approved. %{helpLink}"
-msgstr ""
-
-msgid "ciReport|no vulnerabilities"
-msgstr ""
-
msgid "command line instructions"
msgstr ""
msgid "connecting"
msgstr ""
-msgid "could not read private key, is the passphrase correct?"
-msgstr ""
-
msgid "day"
msgid_plural "days"
msgstr[0] ""
@@ -5065,45 +5389,25 @@ msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
-msgid "detected %d fixed vulnerability"
-msgid_plural "detected %d fixed vulnerabilities"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
-
-msgid "detected %d new vulnerability"
-msgid_plural "detected %d new vulnerabilities"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
-
-msgid "detected no vulnerabilities"
+msgid "deploy token"
msgstr ""
-msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
+msgid "disabled"
msgstr ""
-msgid "here"
+msgid "enabled"
msgstr ""
-msgid "importing"
-msgstr ""
-
-msgid "in progress"
-msgstr ""
-
-msgid "is invalid because there is downstream lock"
+msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
msgstr ""
-msgid "is invalid because there is upstream lock"
+msgid "for this project"
msgstr ""
-msgid "is not a valid X509 certificate."
+msgid "importing"
msgstr ""
-msgid "locked by %{path_lock_user_name} %{created_at}"
+msgid "latest version"
msgstr ""
msgid "merge request"
@@ -5125,28 +5429,7 @@ msgstr ""
msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage is %{emphasisStart} unchanged %{emphasisEnd} at %{memoryFrom}MB"
msgstr ""
-msgid "mrWidget|Add approval"
-msgstr ""
-
-msgid "mrWidget|Allows edits from maintainers"
-msgstr ""
-
-msgid "mrWidget|An error occured while removing your approval."
-msgstr ""
-
-msgid "mrWidget|An error occured while retrieving approval data for this merge request."
-msgstr ""
-
-msgid "mrWidget|An error occured while submitting your approval."
-msgstr ""
-
-msgid "mrWidget|Approve"
-msgstr ""
-
-msgid "mrWidget|Approved"
-msgstr ""
-
-msgid "mrWidget|Approved by"
+msgid "mrWidget|Allows commits from members who can merge to the target branch"
msgstr ""
msgid "mrWidget|Cancel automatic merge"
@@ -5173,6 +5456,9 @@ msgstr ""
msgid "mrWidget|Closes"
msgstr ""
+msgid "mrWidget|Create an issue to resolve them later"
+msgstr ""
+
msgid "mrWidget|Deployment statistics are not available currently"
msgstr ""
@@ -5227,9 +5513,6 @@ msgstr ""
msgid "mrWidget|Remove source branch"
msgstr ""
-msgid "mrWidget|Remove your approval"
-msgstr ""
-
msgid "mrWidget|Request to merge"
msgstr ""
@@ -5269,6 +5552,9 @@ msgstr ""
msgid "mrWidget|There are merge conflicts"
msgstr ""
+msgid "mrWidget|There are unresolved discussions. Please resolve these discussions"
+msgstr ""
+
msgid "mrWidget|This merge request failed to be merged automatically"
msgstr ""
@@ -5321,7 +5607,7 @@ msgstr ""
msgid "personal access token"
msgstr ""
-msgid "private key does not match certificate."
+msgid "remaining"
msgstr ""
msgid "remove due date"
@@ -5336,9 +5622,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "to help your contributors communicate effectively!"
-msgstr ""
-
msgid "username"
msgstr ""
@@ -5348,3 +5631,10 @@ msgstr ""
msgid "with %{additions} additions, %{deletions} deletions."
msgstr ""
+msgid "within %d minute "
+msgid_plural "within %d minutes "
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
diff --git a/locale/pt_BR/gitlab.po b/locale/pt_BR/gitlab.po
index 96a59d6d0d3..05bcae792ce 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: 2018-04-04 19:35+0200\n"
-"PO-Revision-Date: 2018-04-05 03:36-0400\n"
+"POT-Creation-Date: 2018-07-01 16:35+1000\n"
+"PO-Revision-Date: 2018-07-01 11:02\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Portuguese, Brazilian\n"
"Language: pt_BR\n"
@@ -16,8 +16,10 @@ msgstr ""
"X-Crowdin-Language: pt-BR\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-msgid " and"
-msgstr " e"
+msgid "%d changed file"
+msgid_plural "%d changed files"
+msgstr[0] ""
+msgstr[1] ""
msgid "%d commit"
msgid_plural "%d commits"
@@ -54,6 +56,16 @@ msgid_plural "%d metrics"
msgstr[0] "%d métrica"
msgstr[1] "%d métricas"
+msgid "%d staged change"
+msgid_plural "%d staged changes"
+msgstr[0] "%d mudança na lista de commit"
+msgstr[1] "%d mudanças na lista de commit"
+
+msgid "%d unstaged change"
+msgid_plural "%d unstaged changes"
+msgstr[0] "%d mudança fora da lista de commit"
+msgstr[1] "%d mudanças fora da lista de commit"
+
msgid "%s additional commit has been omitted to prevent performance issues."
msgid_plural "%s additional commits have been omitted to prevent performance issues."
msgstr[0] "%s commit adicional foi omitido para prevenir problemas de performance."
@@ -70,12 +82,21 @@ msgid_plural "%{count} participants"
msgstr[0] "%{count} participante"
msgstr[1] "%{count} participantes"
+msgid "%{filePath} deleted"
+msgstr ""
+
+msgid "%{group_docs_link_start}Groups%{group_docs_link_end} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects."
+msgstr ""
+
msgid "%{loadingIcon} Started"
msgstr "%{loadingIcon} Iniciado"
msgid "%{lock_path} is locked by GitLab User %{lock_user_id}"
msgstr "%{lock_path} está bloqueado pelo usuário do GitLab %{lock_user_id}"
+msgid "%{nip_domain} can be used as an alternative to a custom domain."
+msgstr "%{nip_domain} pode ser utilizado como um domínio personalizado."
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr "%{number_commits_behind} commits atrás de %{default_branch}, %{number_commits_ahead} commits à frente"
@@ -88,6 +109,9 @@ msgstr "%{number_of_failures} de %{maximum_failures} falhas. O GitLab não tenta
msgid "%{openOrClose} %{noteable}"
msgstr "%{openOrClose} %{noteable}"
+msgid "%{percent}%% complete"
+msgstr "%{percent}%% completado"
+
msgid "%{storage_name}: failed storage access attempt on host:"
msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:"
msgstr[0] "%{storage_name}: falha na tentativa de acesso ao storage no host:"
@@ -96,15 +120,62 @@ msgstr[1] "%{storage_name}: %{failed_attempts} falhas de acesso ao storage:"
msgid "%{text} is available"
msgstr "%{text} está disponível"
+msgid "%{title} changes"
+msgstr "Alterações de %{title}"
+
+msgid "%{unstaged} unstaged and %{staged} staged changes"
+msgstr "%{unstaged} mudanças fora e %{staged} mudanças dentro da lista de commit"
+
msgid "(checkout the %{link} for information on how to install it)."
msgstr "(veja o %{link} para informações de como instalar)."
msgid "+ %{moreCount} more"
msgstr "%{moreCount} mais"
+msgid "- Runner is active and can process any new jobs"
+msgstr "- O runner está ativo e pode processar novos jobs"
+
+msgid "- Runner is paused and will not receive any new jobs"
+msgstr "- O runner está pausado e não receberá nenhum novo jobs"
+
msgid "- show less"
msgstr "- exibir menos"
+msgid "1 %{type} addition"
+msgid_plural "%{count} %{type} additions"
+msgstr[0] "1 adição de %{type}"
+msgstr[1] "%{count} adições %{type}"
+
+msgid "1 %{type} modification"
+msgid_plural "%{count} %{type} modifications"
+msgstr[0] "1 mudança de %{type}"
+msgstr[1] "%{count} mudanças de %{type}"
+
+msgid "1 closed issue"
+msgid_plural "%d closed issues"
+msgstr[0] "1 issue fechada"
+msgstr[1] "%d issues fechadas"
+
+msgid "1 closed merge request"
+msgid_plural "%d closed merge requests"
+msgstr[0] "1 merge request fechado"
+msgstr[1] "%d merge requests fechados"
+
+msgid "1 merged merge request"
+msgid_plural "%d merged merge requests"
+msgstr[0] "1 merge request com merge"
+msgstr[1] "%d merge requests com merge"
+
+msgid "1 open issue"
+msgid_plural "%d open issues"
+msgstr[0] "1 issue aberta"
+msgstr[1] "%d issues abertas"
+
+msgid "1 open merge request"
+msgid_plural "%d open merge requests"
+msgstr[0] "1 merge request aberto"
+msgstr[1] "%d merge request aberto"
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] "1 pipeline"
@@ -116,9 +187,27 @@ msgstr "1ª contribuição!"
msgid "2FA enabled"
msgstr "Autenticação de 2 passos ativada"
+msgid "403|Please contact your GitLab administrator to get the permission."
+msgstr "Entre em contato com o administrador do GitLab para ter permissão."
+
+msgid "403|You don't have the permission to access this page."
+msgstr "Você não tem permissão para acessar essa página."
+
+msgid "404|Make sure the address is correct and the page hasn't moved."
+msgstr "Certifique-se de que o endereço está correto e a página não foi movida."
+
+msgid "404|Page Not Found"
+msgstr "Página não encontrada"
+
+msgid "404|Please contact your GitLab administrator if you think this is a mistake."
+msgstr "Por favor, contacte o administrador do GitLab se você acha que isso é um erro."
+
msgid "<strong>Removes</strong> source branch"
msgstr "<strong>Remover</strong> branch de origem"
+msgid "A 'Runner' is a process which runs a job. You can setup as many Runners as you need."
+msgstr "'Runner' é um processo que executa um job. Você pode configurar quantos Runners você precisar."
+
msgid "A collection of graphs regarding Continuous Integration"
msgstr "Uma coleção de gráficos sobre Integração Contínua"
@@ -128,6 +217,9 @@ msgstr "Um novo \"branch\" será criado no seu \"fork\" e um novo merge request
msgid "A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}."
msgstr "Um projeto é onde você armazena seus arquivos (repositório), planeja seu trabalho (issues), e publica sua documentação (wiki), %{among_other_things_link}."
+msgid "A regular expression that will be used to find the test coverage output in the job trace. Leave blank to disable"
+msgstr ""
+
msgid "A user with write access to the source branch selected this option"
msgstr "Um usuário com permissão de escrita no branch de origem selecionou esta opção"
@@ -138,7 +230,10 @@ msgid "Abuse Reports"
msgstr "Relatórios de abuso"
msgid "Abuse reports"
-msgstr ""
+msgstr "Relatórios de abuso"
+
+msgid "Accept terms"
+msgstr "Aceitar os temos"
msgid "Access Tokens"
msgstr "Tokens de acesso"
@@ -146,30 +241,30 @@ 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 "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 "Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report."
+msgstr ""
+
msgid "Account"
msgstr "Conta"
-msgid "Account and limit settings"
-msgstr ""
+msgid "Account and limit"
+msgstr "Conta e limites"
msgid "Active"
msgstr "Ativo"
+msgid "Active Sessions"
+msgstr "Sessões ativas"
+
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 Webhooks de Grupos e GitLab Enterprise Edition."
-
msgid "Add Kubernetes cluster"
msgstr "Adicionar cluster Kubernetes"
@@ -182,6 +277,9 @@ msgstr "Adicionar leia-me"
msgid "Add new directory"
msgstr "Adicionar novo diretório"
+msgid "Add reaction"
+msgstr "Adicionar reação"
+
msgid "Add todo"
msgstr "Adicionar tarefa"
@@ -251,29 +349,44 @@ msgstr "Houve commit com todas as mudanças"
msgid "All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings."
msgstr "Todas as funcionalidades estão habilitadas para projetos em branco, a partir de templates ou ao importar, mas você pode desativá-los posteriormente nas configurações do projeto."
-msgid "Allow edits from maintainers."
-msgstr "Permitir as edições dos mantenedores."
+msgid "Allow commits from members who can merge to the target branch."
+msgstr "Permitir commits de membros que podem fazer merge ao branch de destino."
-msgid "Allow rendering of PlantUML diagrams in Asciidoc documents."
+msgid "Allow public access to pipelines and job details, including output logs and artifacts"
msgstr ""
+msgid "Allow rendering of PlantUML diagrams in Asciidoc documents."
+msgstr "Permitir renderização de diagramas PlantUML em documentos Asciidoc."
+
msgid "Allow requests to the local network from hooks and services."
-msgstr ""
+msgstr "Permitir requisições de hooks e serviços para a rede local."
msgid "Allows you to add and manage Kubernetes clusters."
msgstr "Permite adicionar e gerenciar clusters do Kubernetes."
-msgid "Also called \"Issuer\" or \"Relying party trust identifier\""
+msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
+msgstr "Alternativamente, você pode usar um %{personal_access_token_link}. Quando você cria seu Token de Acesso Pessoal, você precisará selecionar o escopo do <code>repositório</code>, para que possamos exibir uma lista de seus repositórios públicos e privados que estão disponíveis para se importar."
+
+msgid "An error occured creating the new branch."
msgstr ""
-msgid "Also called \"Relying party service URL\" or \"Reply URL\""
+msgid "An error occured whilst loading all the files."
msgstr ""
-msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to connect."
-msgstr "Alternativamente, você pode usar um %{personal_access_token_link}. Quando você cria seu Token de Acesso Pessoal, você precisará selecionar o escopo do <code>repositório</code>, para que possamos exibir uma lista de seus repositórios públicos e privados que estão disponíveis para se conectar."
+msgid "An error occured whilst loading the file content."
+msgstr ""
-msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
-msgstr "Alternativamente, você pode usar um %{personal_access_token_link}. Quando você cria seu Token de Acesso Pessoal, você precisará selecionar o escopo do <code>repositório</code>, para que possamos exibir uma lista de seus repositórios públicos e privados que estão disponíveis para se importar."
+msgid "An error occured whilst loading the file."
+msgstr ""
+
+msgid "An error occured whilst loading the merge request changes."
+msgstr ""
+
+msgid "An error occured whilst loading the merge request version data."
+msgstr ""
+
+msgid "An error occured whilst loading the merge request."
+msgstr ""
msgid "An error occurred previewing the blob"
msgstr "Erro ao pré-visualizar o blob"
@@ -281,14 +394,8 @@ msgstr "Erro ao pré-visualizar o blob"
msgid "An error occurred when toggling the notification subscription"
msgstr "Erro ao modificar notificação de assinatura"
-msgid "An error occurred when updating the issue weight"
-msgstr "Um erro aconteceu ao atualizar o peso da issue"
-
-msgid "An error occurred while adding approver"
-msgstr "Erro ao adicionar o aprovador"
-
-msgid "An error occurred while detecting host keys"
-msgstr "Erro ao detectar a chave do host"
+msgid "An error occurred while dismissing the alert. Refresh the page and try again."
+msgstr "Erro ao ignorar alerta. Atualize a página e tente novamente."
msgid "An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again."
msgstr "Erro ao remover alerta. Atualize a página e tente novamente."
@@ -305,11 +412,8 @@ msgstr "Erro ao recuperar informações da pipeline."
msgid "An error occurred while getting projects"
msgstr "Erro ao recuperar projetos"
-msgid "An error occurred while importing project"
-msgstr "Erro ao importar o projeto"
-
-msgid "An error occurred while initializing path locks"
-msgstr "Erro ao iniciar o bloqueio do Path"
+msgid "An error occurred while importing project: ${details}"
+msgstr "Ocorreu um erro ao importar projeto: ${details}"
msgid "An error occurred while loading commits"
msgstr "Erro ao carregar os Commits"
@@ -326,9 +430,6 @@ msgstr "Erro ao carregar o arquivo"
msgid "An error occurred while making the request."
msgstr "Erro ao fazer a requisição."
-msgid "An error occurred while removing approver"
-msgstr "Erro ao remover o aprovador"
-
msgid "An error occurred while rendering KaTeX"
msgstr "Erro ao renderizar o KaTeX"
@@ -341,9 +442,6 @@ msgstr "Erro ao recuperar calendário de atividades"
msgid "An error occurred while retrieving diff"
msgstr "Erro ao recuperar o diff"
-msgid "An error occurred while saving LDAP override status. Please try again."
-msgstr "Um erro ocorreu ao sobrescrever o status LDAP."
-
msgid "An error occurred while saving assignees"
msgstr "Erro ao salvar assignees"
@@ -353,9 +451,6 @@ msgstr "Erro ao validar o nome de usuário"
msgid "An error occurred. Please try again."
msgstr "Ocorreu um erro. Tente novamente."
-msgid "Any Label"
-msgstr "Qualquer Label"
-
msgid "Appearance"
msgstr "Aparência"
@@ -368,29 +463,29 @@ msgstr "Abr"
msgid "April"
msgstr "Abril"
-msgid "Archived project! Repository is read-only"
-msgstr "Projeto arquivado! O repositório é somente leitura"
+msgid "Archived project! Repository and other project resources are read-only"
+msgstr "Projeto arquivado! Repositório e outros recursos de projeto são somente leitura"
msgid "Are you sure you want to delete this pipeline schedule?"
msgstr "Tem certeza que deseja excluir este agendamento de pipeline?"
+msgid "Are you sure you want to remove this identity?"
+msgstr ""
+
msgid "Are you sure you want to reset registration token?"
msgstr "Você tem certeza que quer recriar o token de registro?"
msgid "Are you sure you want to reset the health check token?"
msgstr "Você tem certeza que quer reiniciar o token de status de saúde?"
-msgid "Are you sure you want to unlock %{path_lock_path}?"
-msgstr "Tem certeza que deseja desbloquear %{path_lock_path}?"
-
msgid "Are you sure?"
msgstr "Você tem certeza?"
msgid "Artifacts"
msgstr "Artefatos"
-msgid "Assertion consumer service URL"
-msgstr ""
+msgid "Ask your group maintainer to setup a group Runner."
+msgstr "Solicite a um mantenedor do grupo para configurar um Runner de grupo."
msgid "Assign custom color like #FF0000"
msgstr "Coloque uma cor personalizada, como #FF0000"
@@ -405,17 +500,23 @@ msgid "Assign to"
msgstr "Atribuir à"
msgid "Assigned Issues"
-msgstr ""
+msgstr "Problemas Atribuídos"
msgid "Assigned Merge Requests"
-msgstr ""
+msgstr "Merge requests com responsável"
msgid "Assigned to :name"
-msgstr ""
+msgstr "Atribuído a :name"
+
+msgid "Assigned to me"
+msgstr "Atribuído a mim"
msgid "Assignee"
msgstr "Responsável"
+msgid "Assignee(s)"
+msgstr "Responsável(is)"
+
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "Para anexar arquivo, arraste e solte ou %{upload_link}"
@@ -438,7 +539,7 @@ msgid "Auto DevOps enabled"
msgstr "Auto DevOps ativo"
msgid "Auto DevOps, runners and job artifacts"
-msgstr ""
+msgstr "Auto DevOps, runners e artefatos de builds"
msgid "Auto Review Apps and Auto Deploy need a %{kubernetes} to work correctly."
msgstr "Apps de revisão e deploy automáticos precisam de um %{kubernetes} para funcionar corretamente."
@@ -449,8 +550,11 @@ msgstr "Apps de revisão e deploy automáticos precisam de um nome de domínio e
msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
msgstr "Apps de revisão automática e Auto Deploy precisam de um nome de domínio para que funcione corretamente."
-msgid "AutoDevOps|Auto DevOps (Beta)"
-msgstr "Auto DevOps (Beta)"
+msgid "Auto-cancel redundant, pending pipelines"
+msgstr ""
+
+msgid "AutoDevOps|Auto DevOps"
+msgstr "Auto DevOps"
msgid "AutoDevOps|Auto DevOps documentation"
msgstr "Documentação de auto DevOps"
@@ -470,80 +574,110 @@ msgstr "Você pode automaticamente construir e testar sua aplicação, se você
msgid "AutoDevOps|add a Kubernetes cluster"
msgstr "adicionar um cluster Kubernetes"
-msgid "AutoDevOps|enable Auto DevOps (Beta)"
-msgstr "ativar auto DevOps (Beta)"
+msgid "AutoDevOps|enable Auto DevOps"
+msgstr "ativar Auto DevOps"
msgid "Available"
msgstr "Disponível"
+msgid "Available group Runners : %{runners}"
+msgstr "Runners de grupo disponíveis: %{runners}"
+
+msgid "Available group Runners : %{runners}."
+msgstr "Runners de grupo disponíveis: %{runners}."
+
msgid "Avatar will be removed. Are you sure?"
msgstr "Foto de perfil será removida. Tem certeza?"
msgid "Average per day: %{average}"
msgstr "Média diária: %{average}"
-msgid "Background Color"
+msgid "Background color"
msgstr ""
msgid "Background jobs"
-msgstr ""
+msgstr "Tarefas em segundo plano"
-msgid "Begin with the selected commit"
-msgstr "Comece com o commit selecionado"
+msgid "Badges"
+msgstr "Selos"
+
+msgid "Badges|A new badge was added."
+msgstr "Novo selo adicionado."
+
+msgid "Badges|Add badge"
+msgstr "Novo selo"
-msgid "Billing"
-msgstr "Cobrança"
+msgid "Badges|Adding the badge failed, please check the entered URLs and try again."
+msgstr "Fala ao adicionar selo, por favor, cheque as URLs inseridas e tente novamente."
-msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan."
-msgstr "Grupo %{group_name} está atualmente no plano %{plan_link}."
+msgid "Badges|Badge image URL"
+msgstr "URL da imagem do selo"
-msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available."
-msgstr "Downgrade e upgrade automático para alguns planos ainda não está disponível."
+msgid "Badges|Badge image preview"
+msgstr "Pré-visualização da imagem do selo"
-msgid "BillingPlans|Current plan"
-msgstr "Plano atual"
+msgid "Badges|Delete badge"
+msgstr "Apagar selo"
-msgid "BillingPlans|Customer Support"
-msgstr "Suporte ao cliente"
+msgid "Badges|Delete badge?"
+msgstr "Apagar selo?"
-msgid "BillingPlans|Downgrade"
-msgstr "Downgrade"
+msgid "Badges|Deleting the badge failed, please try again."
+msgstr "Erro ao apagar selo, por favor, tente novamente."
-msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
-msgstr "Saiba mais sobre cada plano lendo nossa %{faq_link}."
+msgid "Badges|Group Badge"
+msgstr "Selo de grupo"
-msgid "BillingPlans|Manage plan"
-msgstr "Gerenciar plano"
+msgid "Badges|Link"
+msgstr "Link"
-msgid "BillingPlans|Please contact %{customer_support_link} in that case."
-msgstr "Por favor, entre em contato com %{customer_support_link} para resolver seu caso."
+msgid "Badges|No badge image"
+msgstr "Sem imagem"
-msgid "BillingPlans|See all %{plan_name} features"
-msgstr "Veja todas as funcionalidades de %{plan_name}"
+msgid "Badges|No image to preview"
+msgstr "Sem imagem para pré-visualizar"
-msgid "BillingPlans|This group uses the plan associated with its parent group."
-msgstr "Esse grupo utiliza o plano associado ao seu grupo pai."
+msgid "Badges|Project Badge"
+msgstr "Selo de projeto"
-msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
-msgstr "Para gerenciar o plano desse grupo, visite a sessão de cobrança de %{parent_billing_page_link}."
+msgid "Badges|Reload badge image"
+msgstr "Recarregar imagem do selo"
-msgid "BillingPlans|Upgrade"
-msgstr "Upgrade"
+msgid "Badges|Save changes"
+msgstr "Salvar alterações"
+
+msgid "Badges|Saving the badge failed, please check the entered URLs and try again."
+msgstr "Erro ao salvar selo, por favor, cheque as URLs digitadas e tente novamente."
+
+msgid "Badges|The %{docsLinkStart}variables%{docsLinkEnd} GitLab supports: %{placeholders}"
+msgstr "Badges | As variáveis de %{docsLinkStart}%{docsLinkEnd} oferece suporte a GitLab: %{placeholders}"
+
+msgid "Badges|The badge was deleted."
+msgstr "O selo foi apagado."
+
+msgid "Badges|The badge was saved."
+msgstr "O selo foi salvo."
-msgid "BillingPlans|You are currently on the %{plan_link} plan."
-msgstr "Você está atualmente no plano %{plan_link}."
+msgid "Badges|This group has no badges"
+msgstr "Esse grupo não tem selos"
-msgid "BillingPlans|frequently asked questions"
-msgstr "perguntas frequentes"
+msgid "Badges|This project has no badges"
+msgstr "Esse projeto não tem selos"
-msgid "BillingPlans|monthly"
-msgstr "mensalmente"
+msgid "Badges|Your badges"
+msgstr "Seus selos"
-msgid "BillingPlans|paid annually at %{price_per_year}"
-msgstr "pago %{price_per_year} anualmente"
+msgid "Begin with the selected commit"
+msgstr "Comece com o commit selecionado"
-msgid "BillingPlans|per user"
-msgstr "por usuário"
+msgid "Below are examples of regex for existing tools:"
+msgstr ""
+
+msgid "Boards"
+msgstr ""
+
+msgid "Branch %{branchName} was not found in this project's repository."
+msgstr ""
msgid "Branch (%{branch_count})"
msgid_plural "Branches (%{branch_count})"
@@ -622,8 +756,8 @@ msgstr "Nenhuma branch para mostrar"
msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
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 "Somente alguém master ou dono do projeto poderá apagar branches protegidas"
+msgid "Branches|Only a project maintainer or owner can delete a protected branch"
+msgstr "Somente um mantenedor ou dono do projeto poderá apagar branches protegidas"
msgid "Branches|Overview"
msgstr "Visão Geral"
@@ -658,9 +792,6 @@ msgstr "Obsoleto"
msgid "Branches|Stale branches"
msgstr "Branches obsoletos"
-msgid "Branches|The branch could not be updated automatically because it has diverged from its upstream counterpart."
-msgstr "O branch não pode ser atualizado automaticamente porque diverge do seu upstream."
-
msgid "Branches|The default branch cannot be deleted"
msgstr "A branch padrão não pode ser apagada"
@@ -673,15 +804,9 @@ msgstr "Para evitar perda de dados, considere fazer um merge dessa branch antes
msgid "Branches|To confirm, type %{branch_name_confirmation}:"
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 "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 "Você irá apagar irreparavelmente a branch protegida '%{branch_name}'."
-msgid "Branches|diverged from upstream"
-msgstr "divergiu do upstream"
-
msgid "Branches|merged"
msgstr "merge realizado"
@@ -703,44 +828,83 @@ msgstr "Acessar arquivos"
msgid "Browse files"
msgstr "Navegar pelos arquivos"
-msgid "Business"
-msgstr ""
-
msgid "ByAuthor|by"
msgstr "por"
msgid "CI / CD"
msgstr "CI / CD"
-msgid "CI/CD"
-msgstr "CI/CD"
+msgid "CI / CD Settings"
+msgstr ""
msgid "CI/CD configuration"
msgstr "Configuração de CI/CD"
-msgid "CI/CD for external repo"
-msgstr "CI/CD para um repositório externo"
+msgid "CI/CD settings"
+msgstr "Configurações de CI/CD"
+
+msgid "CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery."
+msgstr "Um arquivo %{ci_file} precisa ser especificado antes de você conseguir utilizar a integração e entrega contínua."
+
+msgid "CICD|Auto DevOps"
+msgstr "Auto DevOps"
+
+msgid "CICD|Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration."
+msgstr "Auto DevOps irá automaticamente gerar build, testar e fazer deploy de sua aplicação baseada numa configuração de integração e entrega contínua."
+
+msgid "CICD|Automatic deployment to staging, manual deployment to production"
+msgstr ""
+
+msgid "CICD|Continuous deployment to production"
+msgstr ""
+
+msgid "CICD|Deployment strategy"
+msgstr ""
+
+msgid "CICD|Deployment strategy needs a domain name to work correctly."
+msgstr ""
+
+msgid "CICD|Disable Auto DevOps"
+msgstr "Desabilitar DevOps"
+
+msgid "CICD|Enable Auto DevOps"
+msgstr "Ativar DevOps"
+
+msgid "CICD|Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific %{ci_file}."
+msgstr "Siga o padrão da instância para ter o Auto DevOps ativado ou desativado quando não houver nenhum %{ci_file} específico do projeto."
+
+msgid "CICD|Instance default (%{state})"
+msgstr "Instância padrão (%{state})"
msgid "CICD|Jobs"
msgstr "Jobs"
+msgid "CICD|Learn more about Auto DevOps"
+msgstr "Aprender mais sobre Auto DevOps"
+
+msgid "CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project."
+msgstr "As configurações de pipeline do Auto DevOps serão utilizadas quando não existir %{ci_file} no projeto."
+
+msgid "CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages."
+msgstr ""
+
+msgid "Can't find HEAD commit for this branch"
+msgstr ""
+
msgid "Cancel"
msgstr "Cancelar"
+msgid "Cancel this job"
+msgstr "Cancelar esse job"
+
msgid "Cannot be merged automatically"
-msgstr ""
+msgstr "Não pode ser feito o merge automaticamente"
msgid "Cannot modify managed Kubernetes cluster"
msgstr "Não se pode modificar um cluster Kubernetes gerenciado"
-msgid "Certificate fingerprint"
-msgstr ""
-
-msgid "Change Weight"
-msgstr "Alterar peso"
-
msgid "Change this value to influence how frequently the GitLab UI polls for updates."
-msgstr ""
+msgstr "Altere esse valor para influenciar com que frequência a interface do usuário do GitLab pesquisa atualizações."
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "Pick para um branch"
@@ -790,21 +954,18 @@ msgstr "Escolha o arquivo ..."
msgid "Choose a branch/tag (e.g. %{master}) or enter a commit (e.g. %{sha}) to see what's changed or to create a merge request."
msgstr "Escolha a branch/tag (ex: %{master}) ou número do commit (ex: %{sha}) para ver o que mudou ou para criar um merge request."
-msgid "Choose file..."
-msgstr "Escolha o arquivo..."
+msgid "Choose any color."
+msgstr ""
-msgid "Choose which groups you wish to synchronize to this secondary node."
-msgstr "Escolha quais grupos você deseja sincronizar nesse nó secundário."
+msgid "Choose between <code>clone</code> or <code>fetch</code> to get the recent application code"
+msgstr ""
-msgid "Choose which repositories you want to connect and run CI/CD pipelines."
-msgstr "Escolha quais repositórios você deseja se conectar e executar CI/CD pipeline."
+msgid "Choose file..."
+msgstr "Escolha o arquivo..."
msgid "Choose which repositories you want to import."
msgstr "Escolha quais repositórios você deseja importar."
-msgid "Choose which shards you wish to synchronize to this secondary node."
-msgstr "Escolha quais shards você deseja que sincronizem com esse nó secundário."
-
msgid "CiStatusLabel|canceled"
msgstr "cancelado"
@@ -874,21 +1035,12 @@ msgstr "* (Todos os ambientes)"
msgid "CiVariable|All environments"
msgstr "Todos os ambientes"
-msgid "CiVariable|Create wildcard"
-msgstr "Criar um curinga"
-
msgid "CiVariable|Error occured while saving variables"
msgstr "Erro ao salvar variáveis"
-msgid "CiVariable|New environment"
-msgstr "Novo ambiente"
-
msgid "CiVariable|Protected"
msgstr "Protegido"
-msgid "CiVariable|Search environments"
-msgstr "Procurar ambientes"
-
msgid "CiVariable|Toggle protected"
msgstr "Alternar proteção"
@@ -898,30 +1050,30 @@ msgstr "Falha na validação"
msgid "CircuitBreakerApiLink|circuitbreaker api"
msgstr "interruptor da api"
-msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
-msgstr "Clique no botão abaixo para iniciar o processo de instalação navegando para a página do Kubernetes"
+msgid "Clear search input"
+msgstr "Limpar campo de pesquisa"
-msgid "Click to expand text"
-msgstr "Cliquei pra expandir o texto"
+msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
+msgstr "Clique em qualquer <strong>nome de projeto</strong> na lista a seguir para navegar para o milestone do projeto."
-msgid "Client authentication certificate"
-msgstr ""
+msgid "Click the <strong>Promote</strong> button in the top right corner to promote it to a group milestone."
+msgstr "Clique no botão <strong>Promover</strong> no canto superior direito para promover a um milestone de grupo."
-msgid "Client authentication key"
-msgstr ""
+msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
+msgstr "Clique no botão abaixo para iniciar o processo de instalação navegando para a página do Kubernetes"
-msgid "Client authentication key password"
+msgid "Click to expand it."
msgstr ""
+msgid "Click to expand text"
+msgstr "Cliquei pra expandir o texto"
+
msgid "Clone repository"
msgstr "Clonar repositório"
msgid "Close"
msgstr "Fechar"
-msgid "Closed"
-msgstr "Fechado"
-
msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster"
msgstr "%{appList} foi instalado com sucesso no seu cluster Kubernetes"
@@ -937,6 +1089,12 @@ msgstr "Adicionar um cluster Kubernetes existente"
msgid "ClusterIntegration|Advanced options on this Kubernetes cluster's integration"
msgstr "Opções avançadas na integração deste cluster Kubernetes"
+msgid "ClusterIntegration|An error occured while trying to fetch project zones: %{error}"
+msgstr "Erro ao recuperar zonas de projeto: %{error}"
+
+msgid "ClusterIntegration|An error occured while trying to fetch your projects: %{error}"
+msgstr "Erro ao recuperar seus projetos: %{error}"
+
msgid "ClusterIntegration|Applications"
msgstr "Aplicações"
@@ -956,7 +1114,7 @@ msgid "ClusterIntegration|Choose which of your project's environments will use t
msgstr "Escolha qual dos ambientes do seu projeto usará este cluster Kubernetes."
msgid "ClusterIntegration|Control how your Kubernetes cluster integrates with GitLab"
-msgstr ""
+msgstr "Controle como seu cluster Kubernetes se integra com o GitLab"
msgid "ClusterIntegration|Copy API URL"
msgstr "Copiar URL da API"
@@ -965,43 +1123,58 @@ msgid "ClusterIntegration|Copy CA Certificate"
msgstr "Copiar certificado CA"
msgid "ClusterIntegration|Copy Ingress IP Address to clipboard"
-msgstr ""
+msgstr "Copiar o endereço IP de entrada para a área de transferência"
+
+msgid "ClusterIntegration|Copy Jupyter Hostname to clipboard"
+msgstr "Copiar nome do host Jupyter para a área de transferência"
msgid "ClusterIntegration|Copy Kubernetes cluster name"
-msgstr ""
+msgstr "Copiar nome do cluster Kubernetes"
msgid "ClusterIntegration|Copy Token"
msgstr "Copiar token"
msgid "ClusterIntegration|Create Kubernetes cluster"
-msgstr ""
+msgstr "Integração de Clusters | Criar cluster Kubernetes"
msgid "ClusterIntegration|Create Kubernetes cluster on Google Kubernetes Engine"
-msgstr ""
+msgstr "Criar cluster Kubernetes no Google Kubernetes Engine"
msgid "ClusterIntegration|Create a new Kubernetes cluster on Google Kubernetes Engine right from GitLab"
-msgstr ""
+msgstr "Criar novo cluster Kubernetes no Google Kubernetes Engine pelo GitLab"
-msgid "ClusterIntegration|Create on GKE"
-msgstr "Criar no GKE"
+msgid "ClusterIntegration|Create on Google Kubernetes Engine"
+msgstr "Criar no Google Kubernetes Engine"
msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
msgstr "Insira os detalhes para o cluster Kubernetes existente"
msgid "ClusterIntegration|Enter the details for your Kubernetes cluster"
-msgstr ""
+msgstr "Digite detalhes para seu cluster Kubernetes"
msgid "ClusterIntegration|Environment scope"
+msgstr "Escopo de ambiente"
+
+msgid "ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab's Google Kubernetes Engine Integration."
msgstr ""
+msgid "ClusterIntegration|Fetching machine types"
+msgstr "Recuperando tipos de máquina"
+
+msgid "ClusterIntegration|Fetching projects"
+msgstr "Recuperando projetos"
+
+msgid "ClusterIntegration|Fetching zones"
+msgstr "Recuperando zonas"
+
msgid "ClusterIntegration|GitLab Integration"
-msgstr ""
+msgstr "Integração GitLab"
msgid "ClusterIntegration|GitLab Runner"
msgstr "Gitlab Runner"
-msgid "ClusterIntegration|Google Cloud Platform project ID"
-msgstr "ID do projeto no Google Cloud Platform"
+msgid "ClusterIntegration|Google Cloud Platform project"
+msgstr "Google Cloud Platform projeto"
msgid "ClusterIntegration|Google Kubernetes Engine"
msgstr "Google Kubernetes Engine"
@@ -1012,21 +1185,15 @@ msgstr "Projeto do Google Kubernetes Engine"
msgid "ClusterIntegration|Helm Tiller"
msgstr "Helm Tiller"
-msgid "ClusterIntegration|In order to show the health of the cluster, we'll need to provision your cluster with Prometheus to collect the required data."
-msgstr ""
-
msgid "ClusterIntegration|Ingress"
msgstr "Ingressar"
msgid "ClusterIntegration|Ingress IP Address"
-msgstr ""
+msgstr "Endereço IP de entrada"
msgid "ClusterIntegration|Install"
msgstr "Instalar"
-msgid "ClusterIntegration|Install Prometheus"
-msgstr ""
-
msgid "ClusterIntegration|Installed"
msgstr "Instalado"
@@ -1034,73 +1201,91 @@ msgid "ClusterIntegration|Installing"
msgstr "Instalando"
msgid "ClusterIntegration|Integrate Kubernetes cluster automation"
-msgstr ""
+msgstr "Integrar automação de cluster Kubernetes"
msgid "ClusterIntegration|Integration status"
-msgstr ""
+msgstr "Status de integração"
+
+msgid "ClusterIntegration|Jupyter Hostname"
+msgstr "Nome do host Jupyter"
+
+msgid "ClusterIntegration|JupyterHub"
+msgstr "JupyterHub"
msgid "ClusterIntegration|Kubernetes cluster"
-msgstr ""
+msgstr "Cluter Kubernetes"
msgid "ClusterIntegration|Kubernetes cluster details"
-msgstr ""
-
-msgid "ClusterIntegration|Kubernetes cluster health"
-msgstr ""
+msgstr "Detalhes do cluster Kubernetes"
msgid "ClusterIntegration|Kubernetes cluster integration"
-msgstr ""
+msgstr "Integração com o cluster Kubernetes"
msgid "ClusterIntegration|Kubernetes cluster integration is disabled for this project."
-msgstr ""
+msgstr "Integração com o cluster Kubernetes está desabilitada para esse projeto."
msgid "ClusterIntegration|Kubernetes cluster integration is enabled for this project."
-msgstr ""
+msgstr "Integração com o cluster Kubernetes está ativada para esse projeto."
msgid "ClusterIntegration|Kubernetes cluster integration is enabled for this project. Disabling this integration will not affect your Kubernetes cluster, it will only temporarily turn off GitLab's connection to it."
-msgstr ""
+msgstr "Integração com cluster Kubernetes está ativada para esse projeto. Desabilitar essa integração não afetará seu cluster Kubernetes, somente desligará a conexão do GitLab com o mesmo."
msgid "ClusterIntegration|Kubernetes cluster is being created on Google Kubernetes Engine..."
-msgstr ""
+msgstr "O cluster Kubernetes está sendo criado no Google Kubernetes Engine..."
msgid "ClusterIntegration|Kubernetes cluster name"
-msgstr ""
+msgstr "Nome do cluster Kubernetes"
msgid "ClusterIntegration|Kubernetes cluster was successfully created on Google Kubernetes Engine. Refresh the page to see Kubernetes cluster's details"
-msgstr ""
+msgstr "Cluster Kubernetes foi criado com sucesso no Google Kubernetes Engine. Atualize a página para ver os detalhes do cluster"
msgid "ClusterIntegration|Kubernetes clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way. %{link_to_help_page}"
-msgstr ""
+msgstr "Clusters Kubernetes te permitem usar apps de revisão, publicar suas aplicações, executar pipelines e muito mais de um jeito fácil. %{link_to_help_page}"
msgid "ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project"
+msgstr "Cluster Kubernetes podem ser usados para publicar aplicações e permitir app de revisão para esse projeto"
+
+msgid "ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}."
msgstr ""
-msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
-msgstr "Leia mais sobre %{link_to_documentation}"
+msgid "ClusterIntegration|Learn more about %{help_link_start}Kubernetes%{help_link_end}."
+msgstr ""
-msgid "ClusterIntegration|Learn more about environments"
+msgid "ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}."
msgstr ""
+msgid "ClusterIntegration|Learn more about environments"
+msgstr "Ler mais sobre ambientes"
+
msgid "ClusterIntegration|Learn more about security configuration"
-msgstr ""
+msgstr "Ler mais sobre configurações de segurança"
msgid "ClusterIntegration|Machine type"
msgstr "Tipo de máquina"
msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create Kubernetes clusters"
-msgstr ""
+msgstr "Tenha certeza de que sua conta %{link_to_requirements} para criar clusters Kubernetes"
msgid "ClusterIntegration|Manage"
-msgstr ""
+msgstr "Gerenciar"
msgid "ClusterIntegration|Manage your Kubernetes cluster by visiting %{link_gke}"
-msgstr ""
+msgstr "Gerenciar seu cluster Kubernetes visitando %{link_gke}"
msgid "ClusterIntegration|More information"
msgstr "Mais informações"
-msgid "ClusterIntegration|Multiple Kubernetes clusters are available in GitLab Enterprise Edition Premium and Ultimate"
-msgstr "Multiplos clusters Kubernetes estão disponíveis no GitLab Enterprise Edition Premium e Ultimate"
+msgid "ClusterIntegration|No machine types matched your search"
+msgstr "Nenhum tipo de máquina corresponde à sua pesquisa"
+
+msgid "ClusterIntegration|No projects found"
+msgstr "Nenhum projeto encontrado"
+
+msgid "ClusterIntegration|No projects matched your search"
+msgstr "Nenhum projeto corresponde à sua pesquisa"
+
+msgid "ClusterIntegration|No zones matched your search"
+msgstr "Nenhuma zona corresponde à sua pesquisa"
msgid "ClusterIntegration|Note:"
msgstr "Nota:"
@@ -1109,14 +1294,11 @@ msgid "ClusterIntegration|Number of nodes"
msgstr "Número de nós"
msgid "ClusterIntegration|Please enter access information for your Kubernetes cluster. If you need help, you can read our %{link_to_help_page} on Kubernetes"
-msgstr ""
+msgstr "Por favor, entre com as informações de acesso para seu cluter Kubernetes. Se precisar de ajuda, você pode ler nosso %{link_to_help_page} em Kubernetes"
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 ID"
-msgstr "ID do projeto"
-
msgid "ClusterIntegration|Project namespace"
msgstr "Namespace do projeto"
@@ -1127,16 +1309,19 @@ msgid "ClusterIntegration|Prometheus"
msgstr "Prometheus"
msgid "ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration."
-msgstr ""
+msgstr "Leia nosso %{link_to_help_page} em integração de cluter Kubernetes."
+
+msgid "ClusterIntegration|Redeem up to $500 in free credit for Google Cloud Platform"
+msgstr "Resgatar até $500 em crédito livre no Google Cloud Platform"
msgid "ClusterIntegration|Remove Kubernetes cluster integration"
-msgstr ""
+msgstr "Remover integração com o cluster Kubernetes"
msgid "ClusterIntegration|Remove integration"
msgstr "Remover integração"
msgid "ClusterIntegration|Remove this Kubernetes cluster's configuration from this project. This will not delete your actual Kubernetes cluster."
-msgstr ""
+msgstr "Remover configuração desse cluster Kubernetes para esse projeto. Isso não apagará seu cluster Kubernetes atual."
msgid "ClusterIntegration|Request to begin installing failed"
msgstr "Solicitação para início de instalação falhou"
@@ -1144,20 +1329,38 @@ msgstr "Solicitação para início de instalação falhou"
msgid "ClusterIntegration|Save changes"
msgstr "Salvar alterações"
+msgid "ClusterIntegration|Search machine types"
+msgstr "Pesquisar tipos de máquina"
+
+msgid "ClusterIntegration|Search projects"
+msgstr "Pesquisar projetos"
+
+msgid "ClusterIntegration|Search zones"
+msgstr "Pesquisar zonas"
+
msgid "ClusterIntegration|Security"
-msgstr ""
+msgstr "Segurança"
msgid "ClusterIntegration|See and edit the details for your Kubernetes cluster"
msgstr "Veja e edite os detalhes de seus cluster Kubernates"
-msgid "ClusterIntegration|See machine types"
-msgstr "Ver tipos de máquina"
+msgid "ClusterIntegration|Select machine type"
+msgstr "Selecionar tipo de máquina"
+
+msgid "ClusterIntegration|Select project"
+msgstr "Selecionar projeto"
-msgid "ClusterIntegration|See your projects"
-msgstr "Ver seus projetos"
+msgid "ClusterIntegration|Select project and zone to choose machine type"
+msgstr "Selecione projeto e zona para escolher o tipo de máquina"
-msgid "ClusterIntegration|See zones"
-msgstr "Ver zonas"
+msgid "ClusterIntegration|Select project to choose zone"
+msgstr "Selecione o projeto para escolher a zona"
+
+msgid "ClusterIntegration|Select zone"
+msgstr "Selecione a zona"
+
+msgid "ClusterIntegration|Select zone to choose machine type"
+msgstr "Selecione a zone para escolher o tipo de máquina"
msgid "ClusterIntegration|Service token"
msgstr "Token de serviço"
@@ -1175,22 +1378,25 @@ msgid "ClusterIntegration|Something went wrong while installing %{title}"
msgstr "Algo deu errado ao instalar %{title}"
msgid "ClusterIntegration|The default cluster configuration grants access to a wide set of functionalities needed to successfully build and deploy a containerised application."
-msgstr ""
+msgstr "O configuração de cluster padrão permite acesso a uma gama de funcionalidades necessárias para gerar build e publicar aplicações em container."
msgid "ClusterIntegration|This account must have permissions to create a Kubernetes cluster in the %{link_to_container_project} specified below"
-msgstr ""
+msgstr "Essa conta precisa de permissões para criar um cluster Kubernetes no %{link_to_container_project} especificado"
msgid "ClusterIntegration|Toggle Kubernetes Cluster"
-msgstr ""
+msgstr "Alternar cluster Kubernetes"
msgid "ClusterIntegration|Toggle Kubernetes cluster"
-msgstr ""
+msgstr "Alternar cluster Kubernetes"
msgid "ClusterIntegration|Token"
msgstr "Token"
+msgid "ClusterIntegration|Validating project billing status"
+msgstr "Validando status de faturamento do projeto"
+
msgid "ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
-msgstr ""
+msgstr "Com um cluster Kubernetes associado a esse projeto, você pode utilizar apps de revisão, publicar suas aplicações, executar suas pipelines e muito mais de um jeito simples."
msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}"
msgstr "Sua conta precisa de %{link_to_kubernetes_engine}"
@@ -1202,7 +1408,7 @@ msgid "ClusterIntegration|access to Google Kubernetes Engine"
msgstr "acesso ao Google Container Engine"
msgid "ClusterIntegration|check the pricing here"
-msgstr ""
+msgstr "verifique os preços aqui"
msgid "ClusterIntegration|documentation"
msgstr "documentação"
@@ -1219,14 +1425,20 @@ msgstr "atende aos requisitos"
msgid "ClusterIntegration|properly configured"
msgstr "configurado corretamente"
+msgid "ClusterIntegration|sign up"
+msgstr "cadastrar"
+
msgid "Collapse"
msgstr "Recolher"
-msgid "Comment and resolve discussion"
+msgid "Collapse sidebar"
+msgstr "Minimizar barra lateral"
+
+msgid "Comment & resolve discussion"
msgstr ""
-msgid "Comment and unresolve discussion"
-msgstr "Comente e marque discussão como não resolvida"
+msgid "Comment & unresolve discussion"
+msgstr ""
msgid "Comments"
msgstr "Comentários"
@@ -1292,6 +1504,9 @@ msgstr "Nenhum merge request relacionado foi encontrado"
msgid "Committed by"
msgstr "Commit feito por"
+msgid "Commit…"
+msgstr ""
+
msgid "Compare"
msgstr "Comparar"
@@ -1302,10 +1517,10 @@ msgid "Compare Revisions"
msgstr "Comparar revisões"
msgid "Compare changes with the last commit"
-msgstr ""
+msgstr "Compare as mudanças do último commit"
msgid "Compare changes with the merge request target branch"
-msgstr ""
+msgstr "Compare as mudanças do merge request com o branch de destino"
msgid "CompareBranches|%{source_branch} and %{target_branch} are the same."
msgstr "%{source_branch} e %{target_branch} são o mesmo."
@@ -1323,44 +1538,38 @@ msgid "CompareBranches|There isn't anything to compare."
msgstr "Não há nada para comparar."
msgid "Confidential"
-msgstr ""
+msgstr "Confidencial"
msgid "Confidentiality"
msgstr "Confidencialidade"
msgid "Configure Gitaly timeouts."
-msgstr ""
+msgstr "Configurar timeouts do Gitaly."
msgid "Configure Sidekiq job throttling."
-msgstr ""
+msgstr "Configurar otimização de jobs no Sidekiq."
msgid "Configure automatic git checks and housekeeping on repositories."
-msgstr ""
+msgstr "Configurar housekeeping e checagens do git nos repositórios."
msgid "Configure limits for web and API requests."
+msgstr "Configurar limites para web e requisições para API."
+
+msgid "Configure push mirrors."
msgstr ""
msgid "Configure storage path and circuit breaker settings."
-msgstr ""
+msgstr "Configurar caminho de armazenamento e circuit breaker."
msgid "Configure the way a user creates a new account."
-msgstr ""
+msgstr "Configurar a forma como o usuário cria uma nova conta."
msgid "Connect"
msgstr "Conectar"
-msgid "Connect all repositories"
-msgstr "Conectar todos repositórios"
-
msgid "Connect repositories from GitHub"
msgstr "Conectar repositórios do GitHub"
-msgid "Connect your external repositories, and CI/CD pipelines will run for new commits. A GitLab project will be created with only CI/CD features enabled."
-msgstr ""
-
-msgid "Connecting..."
-msgstr "Conectando..."
-
msgid "Container Registry"
msgstr "Container Registry"
@@ -1406,7 +1615,16 @@ 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 "ContainerRegistry|You can also use a %{deploy_token} for read-only access to the registry images."
+msgstr "Você pode usar também um %{deploy_token} para acesso somente-leitura às imagens do registry."
+
+msgid "Continue"
+msgstr "Continuar"
+
msgid "Continuous Integration and Deployment"
+msgstr "Integração contínua e Implantação"
+
+msgid "Contribute to GitLab"
msgstr ""
msgid "Contribution"
@@ -1430,15 +1648,6 @@ msgstr "Commits à %{branch_name}, excluindo commits de merge. Limitado à 6000
msgid "ContributorsPage|Please wait a moment, this page will automatically refresh when ready."
msgstr "Por favor, espere um momento, essa página será atualizada automaticamente."
-msgid "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
-msgstr "Controle a concorrência máxima de LFS/preenchimento de repositórios para esse 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ório para esse nó secundário"
-
-msgid "Copy SSH public key to clipboard"
-msgstr "Copiar chave públic SSH para área de transferência"
-
msgid "Copy URL to clipboard"
msgstr "Copiar URL para área de transferência"
@@ -1451,9 +1660,18 @@ msgstr "Copiar o comando para área de transferência"
msgid "Copy commit SHA to clipboard"
msgstr "Copiar SHA do commit para a área de transferência"
+msgid "Copy file name to clipboard"
+msgstr ""
+
+msgid "Copy file path to clipboard"
+msgstr ""
+
msgid "Copy reference to clipboard"
msgstr "Copiar referência para área de transferência"
+msgid "Copy to clipboard"
+msgstr "Copiar para área de transferência"
+
msgid "Create"
msgstr "Criar"
@@ -1472,20 +1690,20 @@ msgstr "Crie um token de acesso pessoal na sua conta para dar pull ou push via %
msgid "Create branch"
msgstr "Criar a branch"
+msgid "Create commit"
+msgstr ""
+
msgid "Create directory"
msgstr "Criar diretório"
msgid "Create empty repository"
-msgstr ""
-
-msgid "Create epic"
-msgstr "Criar épico"
+msgstr "Criar repositório vazio"
msgid "Create file"
msgstr "Criar arquivo"
msgid "Create group label"
-msgstr ""
+msgstr "Criar Label de grupo"
msgid "Create lists from labels. Issues with that label appear in that list."
msgstr "Criar lista a partir de labels. Issues com labels aparecem nestas listas."
@@ -1512,7 +1730,7 @@ msgid "Create new..."
msgstr "Criar novo..."
msgid "Create project label"
-msgstr ""
+msgstr "Criar Label de projeto"
msgid "CreateNewFork|Fork"
msgstr "Fork"
@@ -1523,14 +1741,11 @@ msgstr "Tag"
msgid "CreateTokenToCloneLink|create a personal access token"
msgstr "criar um token de acesso pessoal"
-msgid "Creates a new branch from %{branchName}"
-msgstr "Cria um novo branch de %{branchName}"
-
-msgid "Creates a new branch from %{branchName} and re-directs to create a new merge request"
-msgstr ""
+msgid "Created"
+msgstr "Feito"
-msgid "Creating epic"
-msgstr "Criando épico"
+msgid "Created by me"
+msgstr "Criado por mim"
msgid "Cron Timezone"
msgstr "Fuso horário do cron"
@@ -1538,8 +1753,14 @@ msgstr "Fuso horário do cron"
msgid "Cron syntax"
msgstr "Sintaxe do cron"
-msgid "Current node"
-msgstr "Nó atual"
+msgid "CurrentUser|Profile"
+msgstr "Perfil"
+
+msgid "CurrentUser|Settings"
+msgstr "Configurações"
+
+msgid "Custom CI config path"
+msgstr ""
msgid "Custom notification events"
msgstr "Eventos de notificação personalizados"
@@ -1547,9 +1768,6 @@ msgstr "Eventos de notificação personalizados"
msgid "Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notification_link}."
msgstr "Níveis de notificação personalizados são equivalentes a níveis de participação. Com níveis de notificação personalizados você também será notificado sobre eventos selecionados. Para mais informações, visite %{notification_link}."
-msgid "Customize colors"
-msgstr ""
-
msgid "Cycle Analytics"
msgstr "Análise de Ciclo"
@@ -1586,8 +1804,8 @@ msgstr "Dez"
msgid "December"
msgstr "Dezembro"
-msgid "Default classification label"
-msgstr "Label de classificação padrão"
+msgid "Decline and sign out"
+msgstr "Recusar e sair"
msgid "Define a custom pattern with cron syntax"
msgstr "Defina um padrão personalizado utilizando a sintaxe do cron"
@@ -1595,6 +1813,9 @@ msgstr "Defina um padrão personalizado utilizando a sintaxe do cron"
msgid "Delete"
msgstr "Excluir"
+msgid "Delete list"
+msgstr ""
+
msgid "Deploy"
msgid_plural "Deploys"
msgstr[0] "Implantação"
@@ -1603,38 +1824,164 @@ msgstr[1] "Implantações"
msgid "Deploy Keys"
msgstr "Chaves para deploy"
+msgid "DeployKeys|+%{count} others"
+msgstr "+%{count} outros"
+
+msgid "DeployKeys|Current project"
+msgstr "Projeto Atual"
+
+msgid "DeployKeys|Deploy key"
+msgstr "Chave de deploy"
+
+msgid "DeployKeys|Enabled deploy keys"
+msgstr "Chaves de deploy ativas"
+
+msgid "DeployKeys|Error enabling deploy key"
+msgstr "Erro ao ativar chaves de deploy"
+
+msgid "DeployKeys|Error getting deploy keys"
+msgstr "Erro ao obter chaves de deploy"
+
+msgid "DeployKeys|Error removing deploy key"
+msgstr "Erro ao remover chave de deploy"
+
+msgid "DeployKeys|Expand %{count} other projects"
+msgstr "Expandir %{count} outros projetos"
+
+msgid "DeployKeys|Loading deploy keys"
+msgstr "Carregando chaves de deploy"
+
+msgid "DeployKeys|No deploy keys found. Create one with the form above."
+msgstr "Nenhuma chave de deploy encontrada. Crie uma com o formulário acima."
+
+msgid "DeployKeys|Privately accessible deploy keys"
+msgstr "Chaves de deploy acessíveis de forma privada"
+
+msgid "DeployKeys|Project usage"
+msgstr "Uso do projeto"
+
+msgid "DeployKeys|Publicly accessible deploy keys"
+msgstr "Chaves de deploy acessíveis publicamente"
+
+msgid "DeployKeys|Read access only"
+msgstr "Apenas acesso de leitura"
+
+msgid "DeployKeys|Write access allowed"
+msgstr "Acesso de edição permitido"
+
+msgid "DeployKeys|You are going to remove this deploy key. Are you sure?"
+msgstr "Você removerá essa chave de deploy. Tem certeza?"
+
+msgid "DeployTokens|Active Deploy Tokens (%{active_tokens})"
+msgstr "Tokens de deploy ativos (%{active_tokens})"
+
+msgid "DeployTokens|Add a deploy token"
+msgstr "Adicionar um token de deploy"
+
+msgid "DeployTokens|Allows read-only access to the registry images"
+msgstr "Permite acesso somente-leitura às imagens do registry"
+
+msgid "DeployTokens|Allows read-only access to the repository"
+msgstr "Permite acesso somente-leitura ao repositório"
+
+msgid "DeployTokens|Copy deploy token to clipboard"
+msgstr "Copiar token de deploy para área de transferência"
+
+msgid "DeployTokens|Copy username to clipboard"
+msgstr "Copiar nome de usuário para área de transferência"
+
+msgid "DeployTokens|Create deploy token"
+msgstr "Criar token de deploy"
+
+msgid "DeployTokens|Created"
+msgstr "Criado"
+
+msgid "DeployTokens|Deploy Tokens"
+msgstr "Tokens de deploy"
+
+msgid "DeployTokens|Deploy tokens allow read-only access to your repository and registry images."
+msgstr "Tokens de deploy permitem acesso somente-leitura ao seu repositório e imagens do registry."
+
+msgid "DeployTokens|Expires"
+msgstr "Expira"
+
+msgid "DeployTokens|Name"
+msgstr "Nome"
+
+msgid "DeployTokens|Pick a name for the application, and we'll give you a unique deploy token."
+msgstr "Escolha um nome para a aplicação e lhe forneceremos um token de deploy exclusivo."
+
+msgid "DeployTokens|Revoke"
+msgstr "Revogar"
+
+msgid "DeployTokens|Revoke %{name}"
+msgstr "Revogar %{name}"
+
+msgid "DeployTokens|Scopes"
+msgstr "Escopos"
+
+msgid "DeployTokens|This action cannot be undone."
+msgstr "Esta ação não pode ser desfeita."
+
+msgid "DeployTokens|This project has no active Deploy Tokens."
+msgstr "Este projeto não possui tokens de deploy ativos."
+
+msgid "DeployTokens|Use this token as a password. Make sure you save it - you won't be able to access it again."
+msgstr "Use este token como uma senha. Certifique-se de salvá-lo, pois, não será possível acessá-lo novamente."
+
+msgid "DeployTokens|Use this username as a login."
+msgstr "Utilize este nome de usuário como um login."
+
+msgid "DeployTokens|Username"
+msgstr "Nome de Usuário"
+
+msgid "DeployTokens|You are about to revoke"
+msgstr "Você está prestes a revogar"
+
+msgid "DeployTokens|Your New Deploy Token"
+msgstr "Seu novo token de deploy"
+
+msgid "DeployTokens|Your new project deploy token has been created."
+msgstr "Seu novo token de deploy de projeto foi criado."
+
+msgid "Deprioritize label"
+msgstr "Despriorizar label"
+
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 campos de descrição de merge requests para seu projeto."
-
msgid "Details"
msgstr "Detalhes"
msgid "Diffs|No file name available"
msgstr "Nenhum nome de arquivo disponível"
+msgid "Diffs|Something went wrong while fetching diff lines."
+msgstr ""
+
msgid "Directory name"
msgstr "Nome do diretório"
msgid "Disable"
msgstr "Desabilitar"
+msgid "Disable for this project"
+msgstr "Desativar para este projeto"
+
+msgid "Disable group Runners"
+msgstr "Desabilitar runners de grupo"
+
+msgid "Discard changes"
+msgstr "Rejeitar alterações"
+
msgid "Discard draft"
msgstr "Descartar rascunho"
-msgid "Discover GitLab Geo."
-msgstr "Descubra Gitlab Geo."
-
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 "Documentation for popular identity providers"
-msgstr ""
+msgid "Domain"
+msgstr "Domínio"
msgid "Don't show again"
msgstr "Não exibir novamente"
@@ -1670,70 +2017,79 @@ msgid "DownloadSource|Download"
msgstr "Baixar"
msgid "Downvotes"
-msgstr ""
+msgstr "Votos negativos"
msgid "Due date"
msgstr "Validade"
-msgid "During this process, you’ll be asked for URLs from GitLab’s side. Use the URLs shown below."
-msgstr ""
+msgid "Each Runner can be in one of the following states:"
+msgstr "Cada runner pode estar em um dos seguintes estados:"
msgid "Edit"
msgstr "Alterar"
+msgid "Edit Label"
+msgstr ""
+
msgid "Edit Pipeline Schedule %{id}"
msgstr "Alterar Agendamento do Pipeline %{id}"
msgid "Edit files in the editor and commit changes here"
msgstr "Alterar arquivos no editor e fazer commit das alterações aqui"
-msgid "Editing"
-msgstr ""
-
-msgid "Elasticsearch"
-msgstr ""
-
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "Edit identity for %{user_name}"
msgstr ""
msgid "Email"
+msgstr "E-mail"
+
+msgid "Email patch"
msgstr ""
msgid "Emails"
msgstr "Emails"
+msgid "Embed"
+msgstr "Embutido"
+
msgid "Enable"
msgstr "Ativar"
msgid "Enable Auto DevOps"
msgstr "Ativar Auto DevOps"
-msgid "Enable SAML authentication for this group"
-msgstr ""
-
msgid "Enable Sentry for error reporting and logging."
-msgstr ""
+msgstr "Ativar Sentry para report de erro e log."
msgid "Enable and configure InfluxDB metrics."
-msgstr ""
+msgstr "Habilitar e configurar métricas InfluxDB."
msgid "Enable and configure Prometheus metrics."
-msgstr ""
+msgstr "Ativar e configurar métricas do Prometheus."
-msgid "Enable classification control using an external service"
-msgstr ""
+msgid "Enable for this project"
+msgstr "Ativar para este projeto"
+
+msgid "Enable group Runners"
+msgstr "Ativar grupo de runners"
+
+msgid "Enable or disable certain group features and choose access levels."
+msgstr "Ative ou desative certos tipos de funcionalidades e escolha os níveis de acesso."
msgid "Enable or disable version check and usage ping."
-msgstr ""
+msgstr "Ativar ou desativar checagem de versão e uso de ping."
msgid "Enable reCAPTCHA or Akismet and set IP limits."
-msgstr ""
+msgstr "Ativar reCAPTCHA ou Akismet e definir seus limites de IP."
msgid "Enable the Performance Bar for a given group."
-msgstr ""
+msgstr "Ative a barra de desempenho para um determinado grupo."
-msgid "Enabled"
-msgstr ""
+msgid "Ends at (UTC)"
+msgstr "Termina em (UTC)"
+
+msgid "Environments"
+msgstr "Ambientes"
msgid "Environments|An error occurred while fetching the environments."
msgstr "Um erro ocorreu ao recuperar ambientes."
@@ -1783,33 +2139,18 @@ msgstr "Atualizado"
msgid "Environments|You don't have any environments right now."
msgstr "Você não tem nenhum ambiente."
-msgid "Epic will be removed! Are you sure?"
-msgstr "Épico será removido! Tem certeza?"
-
-msgid "Epics"
-msgstr "Épicos"
-
-msgid "Epics Roadmap"
-msgstr ""
-
-msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
-msgstr "Epics permite que você gerencie seu portfólio de projetos de forma mais eficiente e com menos esforço"
-
msgid "Error Reporting and Logging"
-msgstr ""
-
-msgid "Error checking branch data. Please try again."
-msgstr "Erro ao verificar dados do branch. Favor tentar novamente."
+msgstr "Relatório e registro de erros"
msgid "Error committing changes. Please try again."
msgstr "Erro ao realizar o commit das alterações. Favor tentar novamente."
-msgid "Error creating epic"
-msgstr "Erro ao criar épico"
-
msgid "Error fetching contributors data."
msgstr "Erro ao recuperar informações de contribuintes."
+msgid "Error fetching job trace"
+msgstr ""
+
msgid "Error fetching labels."
msgstr "Erro ao carregar labels."
@@ -1822,6 +2163,18 @@ msgstr "Erro ao recuperar refs"
msgid "Error fetching usage ping data."
msgstr "Erro ao recupera dados de ping."
+msgid "Error loading branch data. Please try again."
+msgstr "Erro ao carregar dados de branch. Por favor, tente novamente."
+
+msgid "Error loading last commit."
+msgstr "Erro ao carregar último commit."
+
+msgid "Error loading merge requests."
+msgstr "Erro ao carregar merge requests."
+
+msgid "Error loading project data. Please try again."
+msgstr "Erro ao carregar dados do projeto. Por favor, tente novamente."
+
msgid "Error occurred when toggling the notification subscription"
msgstr "Erro ao alterar configuração de notificação de assinatura"
@@ -1834,6 +2187,9 @@ msgstr "Erro ao atualizar status para todas as tarefas."
msgid "Error updating todo status."
msgstr "Erro ao atualizar status das tarefas."
+msgid "Estimated"
+msgstr "Estimativa"
+
msgid "EventFilterBy|Filter by all"
msgstr "EventFilterBy|Filtrar por tudo"
@@ -1864,33 +2220,18 @@ msgstr "Toda semana (domingos às 4:00)"
msgid "Expand"
msgstr "Expandir"
+msgid "Expand all"
+msgstr ""
+
+msgid "Expand sidebar"
+msgstr "Expandir barra lateral"
+
msgid "Explore projects"
msgstr "Explorar projetos"
msgid "Explore public groups"
msgstr "Explorar grupos públicos"
-msgid "External Classification Policy Authorization"
-msgstr ""
-
-msgid "External authentication"
-msgstr ""
-
-msgid "External authorization denied access to this project"
-msgstr "O acesso a este projeto foi negado para autorização externa"
-
-msgid "External authorization request timeout"
-msgstr ""
-
-msgid "ExternalAuthorizationService|Classification Label"
-msgstr ""
-
-msgid "ExternalAuthorizationService|Classification label"
-msgstr ""
-
-msgid "ExternalAuthorizationService|When no classification label is set the default label `%{default_label}` will be used."
-msgstr ""
-
msgid "Failed"
msgstr "Falha"
@@ -1900,13 +2241,22 @@ msgstr "Jobs falharam"
msgid "Failed to change the owner"
msgstr "Erro ao alterar o proprietário"
+msgid "Failed to check related branches."
+msgstr "Falha ao procurar por branches relacionadas."
+
msgid "Failed to remove issue from board, please try again."
-msgstr ""
+msgstr "Falha ao remover issue do board, por favor, tente novamente."
msgid "Failed to remove the pipeline schedule"
msgstr "Erro ao excluir o agendamento do pipeline"
msgid "Failed to update issues, please try again."
+msgstr "Falha ao atualizar Issues. Por favor, tente novamente."
+
+msgid "Failure"
+msgstr "Falha"
+
+msgid "Faster as it re-uses the project workspace (falling back to clone if it doesn't exist)"
msgstr ""
msgid "Feb"
@@ -1918,18 +2268,12 @@ msgstr "Fevereiro"
msgid "Fields on this page are now uneditable, you can configure"
msgstr "Campos nessa página não são mais editáveis, você pode configurar"
-msgid "File name"
-msgstr "Nome do arquivo"
-
msgid "Files"
msgstr "Arquivos"
msgid "Files (%{human_size})"
msgstr "Arquivos (%{human_size})"
-msgid "Fill in the fields below, turn on <strong>%{enable_label}</strong>, and press <strong>%{save_changes}</strong>"
-msgstr ""
-
msgid "Filter by commit message"
msgstr "Filtrar por mensagem de commit"
@@ -1948,10 +2292,13 @@ msgstr "Primeiro"
msgid "FirstPushedBy|pushed by"
msgstr "publicado por"
-msgid "Font Color"
+msgid "For internal projects, any logged in user can view pipelines and access job details (output logs and artifacts)"
+msgstr ""
+
+msgid "For private projects, any member (guest or higher) can view pipelines and access job details (output logs and artifacts)"
msgstr ""
-msgid "Footer message"
+msgid "For public projects, anyone can view pipelines and access job details (output logs and artifacts)"
msgstr ""
msgid "Fork"
@@ -1971,6 +2318,9 @@ msgstr "Fork em andamento"
msgid "Format"
msgstr "Formato"
+msgid "Found errors in your .gitlab-ci.yml:"
+msgstr "Erros encontrados em seu .gitlab-ci.yml:"
+
msgid "From %{provider_title}"
msgstr "De %{provider_title}"
@@ -1981,175 +2331,22 @@ msgid "From merge request merge until deploy to production"
msgstr "Do merge request até a implantação em produção"
msgid "From the Kubernetes cluster details view, install Runner from the applications list"
-msgstr ""
+msgstr "Na visualização de detalhes do cluster do Kubernetes, instale o Runner pela lista de aplicativos"
msgid "GPG Keys"
msgstr "Chaves GPG"
-msgid "Generate a default set of labels"
-msgstr "Gerar labels padrão"
-
-msgid "Geo Nodes"
-msgstr "Nós de geo"
-
-msgid "Geo allows you to replicate your GitLab instance to other geographical locations."
-msgstr ""
-
-msgid "GeoNodeSyncStatus|Node is failing or broken."
-msgstr "Nó está falhando ou quebrado."
-
-msgid "GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage."
-msgstr "Nó está lento, sobrecarregado, ou acabou de recuperar após uma interrupção."
-
-msgid "GeoNodes|Checksummed"
-msgstr ""
-
-msgid "GeoNodes|Database replication lag:"
-msgstr "Atraso na replicação do banco de dados:"
-
-msgid "GeoNodes|Disabling a node stops the sync process. Are you sure?"
-msgstr "Desabilitar um nó para o processo de sincronização. Você tem certeza?"
-
-msgid "GeoNodes|Does not match the primary storage configuration"
-msgstr "Não corresponde á configuração de armazenamento primário"
-
-msgid "GeoNodes|Failed"
-msgstr "Falha"
-
-msgid "GeoNodes|Full"
-msgstr "Completo"
-
-msgid "GeoNodes|GitLab version does not match the primary node version"
-msgstr "Versão do GitLab não corresponde a versão do nó primário"
+msgid "General"
+msgstr "Geral"
-msgid "GeoNodes|GitLab version:"
-msgstr "Versão do GitLab:"
-
-msgid "GeoNodes|Health status:"
-msgstr "Saúde dos serviços:"
-
-msgid "GeoNodes|Last event ID processed by cursor:"
-msgstr "Último ID de evento processado pelo cursor:"
-
-msgid "GeoNodes|Last event ID seen from primary:"
-msgstr "Último ID de evento visto pelo primário:"
-
-msgid "GeoNodes|Loading nodes"
-msgstr "Carregando nós"
-
-msgid "GeoNodes|Local Attachments:"
-msgstr "Anexos locais:"
-
-msgid "GeoNodes|Local LFS objects:"
-msgstr "Objetos LFS locais:"
-
-msgid "GeoNodes|Local job artifacts:"
-msgstr "Artefatos de processos locais:"
-
-msgid "GeoNodes|New node"
-msgstr "Novo nó"
-
-msgid "GeoNodes|Node Authentication was successfully repaired."
+msgid "General pipelines"
msgstr ""
-msgid "GeoNodes|Node was successfully removed."
-msgstr ""
-
-msgid "GeoNodes|Not checksummed"
-msgstr ""
-
-msgid "GeoNodes|Out of sync"
-msgstr "Fora de sincronia"
-
-msgid "GeoNodes|Removing a node stops the sync process. Are you sure?"
-msgstr ""
-
-msgid "GeoNodes|Replication slot WAL:"
-msgstr ""
-
-msgid "GeoNodes|Replication slots:"
-msgstr ""
-
-msgid "GeoNodes|Repositories checksummed:"
-msgstr ""
-
-msgid "GeoNodes|Repositories:"
-msgstr ""
-
-msgid "GeoNodes|Repository checksums verified:"
-msgstr ""
-
-msgid "GeoNodes|Selective"
-msgstr ""
-
-msgid "GeoNodes|Something went wrong while changing node status"
-msgstr ""
-
-msgid "GeoNodes|Something went wrong while removing node"
-msgstr ""
-
-msgid "GeoNodes|Something went wrong while repairing node"
-msgstr ""
-
-msgid "GeoNodes|Storage config:"
-msgstr ""
-
-msgid "GeoNodes|Sync settings:"
-msgstr ""
-
-msgid "GeoNodes|Synced"
-msgstr ""
-
-msgid "GeoNodes|Unused slots"
-msgstr ""
-
-msgid "GeoNodes|Unverified"
-msgstr ""
-
-msgid "GeoNodes|Used slots"
-msgstr ""
-
-msgid "GeoNodes|Verified"
-msgstr ""
-
-msgid "GeoNodes|Wiki checksums verified:"
-msgstr ""
-
-msgid "GeoNodes|Wikis checksummed:"
-msgstr ""
-
-msgid "GeoNodes|Wikis:"
-msgstr "Wikis:"
-
-msgid "GeoNodes|You have configured Geo nodes using an insecure HTTP connection. We recommend the use of HTTPS."
-msgstr "Você configurou Geo nodes usando uma conexão HTTP insegura. Recomendamos o uso de HTTPS."
-
-msgid "Geo|All projects"
-msgstr "Todos os projetos"
-
-msgid "Geo|File sync capacity"
-msgstr "Capacidade de sincronização de arquivos"
-
-msgid "Geo|Groups to synchronize"
-msgstr "Grupos para sincronizar"
-
-msgid "Geo|Projects in certain groups"
-msgstr "Projetos em certos grupos"
-
-msgid "Geo|Projects in certain storage shards"
-msgstr ""
-
-msgid "Geo|Repository sync capacity"
-msgstr "Capacidade de sincronização do repositório"
-
-msgid "Geo|Select groups to replicate."
-msgstr "Selecione grupos para replicar."
-
-msgid "Geo|Shards to synchronize"
-msgstr ""
+msgid "Generate a default set of labels"
+msgstr "Gerar labels padrão"
msgid "Git repository URL"
-msgstr ""
+msgstr "URL do repositório Git"
msgid "Git revision"
msgstr "Revisão do Git"
@@ -2157,30 +2354,36 @@ msgstr "Revisão do Git"
msgid "Git storage health information has been reset"
msgstr "Informações sobre o status de saúde do storage Git foram reiniciadas"
+msgid "Git strategy for pipelines"
+msgstr ""
+
msgid "Git version"
msgstr "Versão do Git"
msgid "GitHub import"
-msgstr ""
+msgstr "Importação do GitHub"
msgid "GitLab CI Linter has been moved"
-msgstr ""
+msgstr "Linter do GitLab CI foi movido"
-msgid "GitLab Geo"
-msgstr ""
+msgid "GitLab Group Runners can execute code for all the projects in this group."
+msgstr "Os GitLab Group Runners podem executar código para todos os projetos neste grupo."
msgid "GitLab Runner section"
msgstr "Seção GitLab Runner"
-msgid "GitLab single sign on URL"
-msgstr ""
-
msgid "Gitaly"
-msgstr ""
+msgstr "Gitaly"
msgid "Gitaly Servers"
msgstr "Servidores Gitaly"
+msgid "Gitaly|Address"
+msgstr "Endereço"
+
+msgid "Go Back"
+msgstr "Voltar"
+
msgid "Go back"
msgstr "Voltar"
@@ -2196,23 +2399,20 @@ msgstr "Autenticação do Google não está %{link_to_documentation}. Peça ao a
msgid "Got it!"
msgstr "Entendi!"
-msgid "GroupRoadmap|Epics let you manage your portfolio of projects more efficiently and with less effort"
-msgstr ""
-
-msgid "GroupRoadmap|From %{dateWord}"
-msgstr ""
+msgid "Graph"
+msgstr "Gráfico"
-msgid "GroupRoadmap|Loading roadmap"
-msgstr ""
+msgid "Group CI/CD settings"
+msgstr "Configurações de CI/CD do grupo"
-msgid "GroupRoadmap|Something went wrong while fetching epics"
-msgstr ""
+msgid "Group ID"
+msgstr "ID do grupo"
-msgid "GroupRoadmap|To view the roadmap, add a planned start or finish date to one of your epics in this group or its subgroups. Only epics in the past 3 months and the next 3 months are shown &ndash; from %{startDate} to %{endDate}."
-msgstr ""
+msgid "Group Runners"
+msgstr "Group Runners"
-msgid "GroupRoadmap|Until %{dateWord}"
-msgstr ""
+msgid "Group maintainers can register group runners in the %{link}"
+msgstr "Os mantenedores podem registrar grupos de runners em %{link}"
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
msgstr "Bloquear compartilhamento de projetos do grupo %{group} com outros grupos"
@@ -2238,6 +2438,9 @@ msgstr "não pode ser desativado quando a configuração \"Travar compartilhamen
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr "retirar a trava de compartilhamento de grupo de %{ancestor_group_name}"
+msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
+msgstr ""
+
msgid "GroupsEmptyState|A group is a collection of several projects."
msgstr "Um grupo é uma coleção de vários projetos."
@@ -2277,12 +2480,6 @@ 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 "Have your users email"
-msgstr "E-mail para abertura de issues"
-
-msgid "Header message"
-msgstr ""
-
msgid "Health Check"
msgstr "Status de Saúde"
@@ -2302,38 +2499,77 @@ msgid "HealthCheck|Unhealthy"
msgstr "Não saudável"
msgid "Help"
-msgstr ""
+msgstr "Ajuda"
msgid "Help page"
-msgstr ""
+msgstr "Página de ajuda"
msgid "Help page text and support page url."
-msgstr ""
+msgstr "Texto da página de ajuda e Url da página de suporte."
msgid "Hide value"
msgid_plural "Hide values"
msgstr[0] "Ocultar valor"
msgstr[1] "Ocultar valores"
+msgid "Hide whitespace changes"
+msgstr ""
+
msgid "History"
msgstr "Histórico"
msgid "Housekeeping successfully started"
msgstr "Manutenção iniciada com sucesso"
-msgid "Identity provider single sign on URL"
+msgid "I accept the %{terms_link}"
+msgstr "Eu aceito o %{terms_link}"
+
+msgid "I accept the|Terms of Service and Privacy Policy"
+msgstr "Termos de Serviço e Política de Privacidade"
+
+msgid "ID"
msgstr ""
-msgid "If enabled, access to projects will be validated on an external service using their classification label."
+msgid "IDE|Commit"
+msgstr "Commit"
+
+msgid "IDE|Edit"
+msgstr "Editar"
+
+msgid "IDE|Go back"
+msgstr "Voltar"
+
+msgid "IDE|Open in file view"
+msgstr "Abrir na visualização de arquivos"
+
+msgid "IDE|Review"
+msgstr "Revisar"
+
+msgid "Identifier"
msgstr ""
-msgid "If using GitHub, you’ll see pipeline statuses on GitHub for your commits and pull requests. %{more_info_link}"
+msgid "Identities"
msgstr ""
-msgid "If you already have files you can push them using the %{link_to_cli} below."
+msgid "If disabled, the access level will depend on the user's permissions in the project."
msgstr ""
+msgid "If enabled"
+msgstr ""
+
+msgid "If you already have files you can push them using the %{link_to_cli} below."
+msgstr "Se você já tem arquivos, pode realizar um push usando o %{link_to_cli} abaixo."
+
msgid "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>."
+msgstr "Se o seu repositório HTTP não estiver acessível publicamente, adicione informações de autenticação à URL:<code>https://username:password@gitlab.company.com/group/project.git</code>."
+
+msgid "ImageDiffViewer|2-up"
+msgstr ""
+
+msgid "ImageDiffViewer|Onion skin"
+msgstr ""
+
+msgid "ImageDiffViewer|Swipe"
msgstr ""
msgid "Import"
@@ -2351,17 +2587,11 @@ msgstr "Importar repositórios do GitHub"
msgid "Import repository"
msgstr "Importar repositório"
-msgid "ImportButtons|Connect repositories from"
-msgstr ""
-
-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 a gerência de issues com pesos no GitLab Enterprise Edition."
+msgid "Include a Terms of Service agreement and Privacy Policy that all users must accept."
+msgstr "Inclua um contrato de Termos de Serviço e uma Política de Privacidade que todos os usuários devem aceitar."
-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 "Inline"
+msgstr ""
msgid "Install Runner on Kubernetes"
msgstr "Instalar Runner no Kubernates"
@@ -2369,19 +2599,17 @@ msgstr "Instalar Runner no Kubernates"
msgid "Install a Runner compatible with GitLab CI"
msgstr "Instalar um Runner compatível com o GitLab CI"
-msgid "Instance"
-msgid_plural "Instances"
-msgstr[0] "Instância"
-msgstr[1] "Instâncias"
-
msgid "Instance does not support multiple Kubernetes clusters"
msgstr "A instância não suporta múltiplos clusters Kubernetes"
msgid "Integrations"
+msgstr "Integrações"
+
+msgid "Integrations Settings"
msgstr ""
msgid "Interested parties can even contribute by pushing commits if they want to."
-msgstr ""
+msgstr "As partes interessadas podem até contribuir enviando commits, caso queiram."
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."
@@ -2395,8 +2623,8 @@ msgstr "Padrão de intervalo"
msgid "Introducing Cycle Analytics"
msgstr "Apresentando a Análise de Ciclo"
-msgid "Issue board focus mode"
-msgstr "Focus mode no issue board"
+msgid "Issue Board"
+msgstr ""
msgid "Issue events"
msgstr "Eventos de issue"
@@ -2404,14 +2632,11 @@ msgstr "Eventos de issue"
msgid "IssueBoards|Board"
msgstr "Board"
-msgid "IssueBoards|Boards"
-msgstr "Boards"
-
msgid "Issues"
msgstr "Issues"
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
-msgstr ""
+msgstr "Issues podem ser bugs, tarefas ou ideias a serem discutidas. Além disso, issues são pesquisáveis e filtráveis."
msgid "Jan"
msgstr "Jan"
@@ -2419,6 +2644,12 @@ msgstr "Jan"
msgid "January"
msgstr "Janeiro"
+msgid "Job"
+msgstr ""
+
+msgid "Job has been erased"
+msgstr "O job foi apagado"
+
msgid "Jobs"
msgstr "Jobs"
@@ -2435,7 +2666,7 @@ msgid "June"
msgstr "Junho"
msgid "Koding"
-msgstr ""
+msgstr "Koding"
msgid "Kubernetes"
msgstr "Kubernetes"
@@ -2444,21 +2675,24 @@ msgid "Kubernetes Cluster"
msgstr "Cluster Kubernetes"
msgid "Kubernetes cluster creation time exceeds timeout; %{timeout}"
-msgstr ""
+msgstr "O tempo de criação do cluster de Kubernetes excedeu o tempo limite; %{timeout}"
msgid "Kubernetes cluster integration was not removed."
-msgstr ""
+msgstr "A integração do cluster do Kubernetes não foi removida."
msgid "Kubernetes cluster integration was successfully removed."
-msgstr ""
+msgstr "A integração do cluster do Kubernetes foi removida com sucesso."
msgid "Kubernetes cluster was successfully updated."
-msgstr ""
+msgstr "O cluster do Kubernetes foi atualizado com sucesso."
msgid "Kubernetes configured"
-msgstr ""
+msgstr "Kubernetes configurado"
msgid "Kubernetes service integration has been deprecated. %{deprecated_message_content} your Kubernetes clusters using the new <a href=\"%{url}\"/>Kubernetes Clusters</a> page"
+msgstr "Integração de serviço Kubernetes foi depreciada. %{deprecated_message_content} seus clusters Kubernetes usando a nova página <a href=\"%{url}\"/>Clusters Kubernetes</a>"
+
+msgid "LFS"
msgstr ""
msgid "LFSStatus|Disabled"
@@ -2470,26 +2704,32 @@ msgstr "Habilitado"
msgid "Label"
msgstr "Label"
-msgid "LabelSelect|%{firstLabelName} +%{remainingLabelCount} more"
+msgid "Label actions dropdown"
msgstr ""
+msgid "LabelSelect|%{firstLabelName} +%{remainingLabelCount} more"
+msgstr "%{firstLabelName} +%{remainingLabelCount} mais"
+
msgid "LabelSelect|%{labelsString}, and %{remainingLabelCount} more"
-msgstr ""
+msgstr "%{labelsString} e %{remainingLabelCount} mais"
msgid "Labels"
msgstr "Etiquetas"
msgid "Labels can be applied to %{features}. Group labels are available for any project within the group."
-msgstr ""
+msgstr "Labels podem ser aplicadas a %{features}. Labels de grupo estão disponíveis para qualquer projeto dentro do grupo."
msgid "Labels can be applied to issues and merge requests to categorize them."
+msgstr "Labels podem ser aplicadas a issues e merge requests para categorizá-los."
+
+msgid "Labels can be applied to issues and merge requests."
msgstr ""
msgid "Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>"
-msgstr ""
+msgstr "<span>Promover label</span> %{labelTitle} <span>para Label do Grupo?</span>"
msgid "Labels|Promote Label"
-msgstr ""
+msgstr "Promover Label"
msgid "Last %d day"
msgid_plural "Last %d days"
@@ -2520,14 +2760,17 @@ msgstr "Você fez o push para"
msgid "LastPushEvent|at"
msgstr "em"
+msgid "Latest changes"
+msgstr "Últimas modificações"
+
msgid "Learn more"
msgstr "Saiba mais"
msgid "Learn more about Kubernetes"
-msgstr ""
+msgstr "Saiba mais sobre o Kubernetes"
msgid "Learn more about protected branches"
-msgstr ""
+msgstr "Saiba mais sobre branches protegidos"
msgid "Learn more in the"
msgstr "Saiba mais em"
@@ -2544,18 +2787,18 @@ msgstr "Sair do grupo"
msgid "Leave project"
msgstr "Sair do projeto"
-msgid "License"
-msgstr "Licença"
-
msgid "List"
msgstr "Lista"
msgid "List your GitHub repositories"
-msgstr ""
+msgstr "Listar os seus repositórios no GitHub"
msgid "Loading the GitLab IDE..."
msgstr "Carregando IDE do GitLab..."
+msgid "Loading..."
+msgstr "Carregando..."
+
msgid "Lock"
msgstr "Bloquear"
@@ -2565,35 +2808,29 @@ msgstr "Bloquear %{issuableDisplayName}"
msgid "Lock not found"
msgstr "Bloqueio não encontrado"
+msgid "Lock to current projects"
+msgstr "Travar para projetos existentes"
+
msgid "Locked"
msgstr "Bloqueado"
-msgid "Locked Files"
-msgstr "Arquivos bloqueados"
-
-msgid "Locks give the ability to lock specific file or folder."
-msgstr ""
+msgid "Locked to current projects"
+msgstr "Travado para projetos existentes"
msgid "Login"
msgstr "Entrar"
-msgid "Make everyone on your team more productive regardless of their location. GitLab Geo creates read-only mirrors of your GitLab instance so you can reduce the time it takes to clone and fetch large repos."
-msgstr ""
-
msgid "Manage all notifications"
-msgstr ""
+msgstr "Gerenciar todas as notificações"
msgid "Manage group labels"
-msgstr ""
+msgstr "Gerenciar Labels de grupo"
msgid "Manage labels"
msgstr "Gerenciar etiquetas"
msgid "Manage project labels"
-msgstr ""
-
-msgid "Manage your group’s membership while adding another level of security with SAML."
-msgstr ""
+msgstr "Gerenciar Labels de projetos"
msgid "Mar"
msgstr "Mar"
@@ -2601,8 +2838,11 @@ msgstr "Mar"
msgid "March"
msgstr "Março"
-msgid "Mark done"
-msgstr "Marcar como feito"
+msgid "Mark todo as done"
+msgstr "Marcar como concluído"
+
+msgid "Markdown enabled"
+msgstr "Markdown habilitado"
msgid "Maximum git storage failures"
msgstr "Máximo de falhas do git storage"
@@ -2616,8 +2856,8 @@ msgstr "Mediana"
msgid "Members"
msgstr "Membros"
-msgid "Members will be forwarded here when signing in to your group. Get this from your identity provider, where it can also be called \"SSO Service Location\", \"SAML Token Issuance Endpoint\", or \"SAML 2.0/W-Federation URL\"."
-msgstr ""
+msgid "Merge Request:"
+msgstr "Merge Request:"
msgid "Merge Requests"
msgstr "Merge Requests"
@@ -2628,131 +2868,83 @@ msgstr "Eventos de merge"
msgid "Merge request"
msgstr "Merge requests"
+msgid "Merge requests"
+msgstr "Merge requests"
+
msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
msgstr "A tela de Merge request é um lugar para propor mudanças em um projeto e discutir essas mudanças com outros"
-msgid "Merged"
-msgstr "Merge realizado"
-
-msgid "Messages"
-msgstr "Mensagens"
-
-msgid "Metrics - Influx"
-msgstr ""
-
-msgid "Metrics - Prometheus"
-msgstr ""
-
-msgid "Metrics|Business"
-msgstr ""
-
-msgid "Metrics|Create metric"
-msgstr ""
-
-msgid "Metrics|Edit metric"
-msgstr ""
-
-msgid "Metrics|For grouping similar metrics"
-msgstr ""
-
-msgid "Metrics|Label of the chart's vertical axis. Usually the type of the unit being charted. The horizontal axis (X-axis) always represents time."
-msgstr ""
-
-msgid "Metrics|Legend label (optional)"
-msgstr ""
-
-msgid "Metrics|Must be a valid PromQL query."
-msgstr ""
-
-msgid "Metrics|Name"
+msgid "MergeRequests|Resolve this discussion in a new issue"
msgstr ""
-msgid "Metrics|New metric"
+msgid "MergeRequests|Saving the comment failed"
msgstr ""
-msgid "Metrics|Prometheus Query Documentation"
+msgid "MergeRequests|Toggle comments for this file"
msgstr ""
-msgid "Metrics|Query"
+msgid "MergeRequests|Updating discussions failed"
msgstr ""
-msgid "Metrics|Response"
+msgid "MergeRequests|View file @ %{commitId}"
msgstr ""
-msgid "Metrics|System"
+msgid "MergeRequests|View replaced file @ %{commitId}"
msgstr ""
-msgid "Metrics|Type"
-msgstr ""
-
-msgid "Metrics|Unit label"
-msgstr ""
-
-msgid "Metrics|Used as a title for the chart"
-msgstr ""
-
-msgid "Metrics|Used if the query returns a single series. If it returns multiple series, their legend labels will be picked up from the response."
-msgstr ""
-
-msgid "Metrics|Y-axis label"
-msgstr ""
-
-msgid "Metrics|e.g. HTTP requests"
-msgstr ""
-
-msgid "Metrics|e.g. Requests/second"
-msgstr ""
+msgid "Merged"
+msgstr "Merge realizado"
-msgid "Metrics|e.g. Throughput"
-msgstr ""
+msgid "Messages"
+msgstr "Mensagens"
-msgid "Metrics|e.g. rate(http_requests_total[5m])"
-msgstr ""
+msgid "Metrics - Influx"
+msgstr "Métricas - Influx"
-msgid "Metrics|e.g. req/sec"
-msgstr ""
+msgid "Metrics - Prometheus"
+msgstr "Métricas - Prometheus"
msgid "Milestone"
msgstr "Milestone"
+msgid "Milestones"
+msgstr "Milestones"
+
msgid "Milestones|Delete milestone"
-msgstr ""
+msgstr "Excluir Milestone"
msgid "Milestones|Delete milestone %{milestoneTitle}?"
-msgstr ""
+msgstr "Excluir Milestone %{milestoneTitle}?"
msgid "Milestones|Failed to delete milestone %{milestoneTitle}"
-msgstr ""
+msgstr "Falha ao excluir Milestone %{milestoneTitle}"
msgid "Milestones|Milestone %{milestoneTitle} was not found"
-msgstr ""
+msgstr "Milestone %{milestoneTitle} não foi encontrado"
msgid "Milestones|Promote %{milestoneTitle} to group milestone?"
-msgstr ""
+msgstr "Promover Milestone %{milestoneTitle} de projeto para Milestone de grupo?"
msgid "Milestones|Promote Milestone"
-msgstr ""
-
-msgid "Milestones|This action cannot be reversed."
-msgstr ""
+msgstr "Promover Milestone"
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "adicione uma chave SSH"
msgid "Modal|Cancel"
-msgstr ""
+msgstr "Cancelar"
msgid "Modal|Close"
-msgstr ""
+msgstr "Fechar"
msgid "Monitoring"
msgstr "Monitoramento"
-msgid "More info"
-msgstr ""
+msgid "More actions"
+msgstr "Mais ações"
msgid "More information"
-msgstr ""
+msgstr "Mais informações"
msgid "More information is available|here"
msgstr "Mais informações estão disponíveis|aqui"
@@ -2763,12 +2955,30 @@ msgstr "Mover"
msgid "Move issue"
msgstr "Mover issue"
-msgid "Multiple issue boards"
-msgstr "Múltiplos issue boards"
+msgid "Name"
+msgstr "Nome"
msgid "Name new label"
msgstr "Nome da nova label"
+msgid "Name your individual key via a title"
+msgstr ""
+
+msgid "Nav|Help"
+msgstr "Ajuda"
+
+msgid "Nav|Home"
+msgstr "Início"
+
+msgid "Nav|Sign In / Register"
+msgstr "Entrar / Registar"
+
+msgid "Nav|Sign out and sign in with a different account"
+msgstr "Saia e faça login com uma conta diferente"
+
+msgid "New Identity"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "Nova Issue"
@@ -2780,6 +2990,9 @@ msgstr "Novo cluster Kubernetes"
msgid "New Kubernetes cluster"
msgstr "Novo cluster Kubernetes"
+msgid "New Label"
+msgstr ""
+
msgid "New Pipeline Schedule"
msgstr "Novo Agendamento de Pipeline"
@@ -2792,15 +3005,15 @@ msgstr "Novo branch indisponível"
msgid "New directory"
msgstr "Novo diretório"
-msgid "New epic"
-msgstr "Novo épico"
-
msgid "New file"
msgstr "Novo arquivo"
msgid "New group"
msgstr "Novo grupo"
+msgid "New identity"
+msgstr ""
+
msgid "New issue"
msgstr "Nova issue"
@@ -2810,6 +3023,9 @@ msgstr "Nova label"
msgid "New merge request"
msgstr "Novo merge request"
+msgid "New pipelines will cancel older, pending pipelines on the same branch"
+msgstr ""
+
msgid "New project"
msgstr "Novo projeto"
@@ -2825,8 +3041,8 @@ msgstr "Novo subgrupo"
msgid "New tag"
msgstr "Nova tag"
-msgid "No Label"
-msgstr ""
+msgid "No"
+msgstr "Não"
msgid "No assignee"
msgstr "Sem responsável"
@@ -2846,9 +3062,18 @@ msgstr "Sem estimativa de tempo gasto"
msgid "No file chosen"
msgstr "Nenhum arquivo escolhido"
-msgid "No labels created yet."
+msgid "No files found"
msgstr ""
+msgid "No files found."
+msgstr "Nenhum arquivo encontrado."
+
+msgid "No merge requests found"
+msgstr "Nenhum merge requests encontrado"
+
+msgid "No messages were logged"
+msgstr "Nenhuma mensagem foi registrada"
+
msgid "No repository"
msgstr "Nenhum repositório"
@@ -2865,10 +3090,10 @@ msgid "Not available"
msgstr "Não disponível"
msgid "Not available for private projects"
-msgstr ""
+msgstr "Não disponível para projetos privados"
msgid "Not available for protected branches"
-msgstr ""
+msgstr "Não disponível para Branches protegidas"
msgid "Not confidential"
msgstr "Não confidencial"
@@ -2877,19 +3102,13 @@ msgid "Not enough data"
msgstr "Dados insuficientes"
msgid "Note that the master branch is automatically protected. %{link_to_protected_branches}"
-msgstr ""
-
-msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow connecting repositories without generating a Personal Access Token."
-msgstr ""
+msgstr "Observe que o branch master é automaticamente protegido. %{link_to_protected_branches}"
msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
-msgstr ""
-
-msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow connecting repositories without generating a Personal Access Token."
-msgstr ""
+msgstr "Nota: Como administrador, você pode configurar o %{github_integration_link}, que permitirá o login via GitHub e permitirá a importação de repositórios sem gerar um Token de Acesso Pessoal."
msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
-msgstr ""
+msgstr "Nota: Considere pedir ao seu administrador do GitLab para configurar %{github_integration_link}, o que permitirá o login via GitHub e permitir a importação de repositórios sem gerar um Token de Acesso Pessoal."
msgid "Notification events"
msgstr "Eventos de notificação"
@@ -2963,9 +3182,6 @@ msgstr "Novembro"
msgid "Number of access attempts"
msgstr "Número de tentativas de acesso"
-msgid "OK"
-msgstr "OK"
-
msgid "Oct"
msgstr "Out"
@@ -2975,20 +3191,17 @@ msgstr "Outubro"
msgid "OfSearchInADropdown|Filter"
msgstr "Filtrar"
-msgid "Once imported, repositories can be mirrored over SSH. Read more %{ssh_link}"
-msgstr ""
-
msgid "Online IDE integration settings."
+msgstr "Configurações de integração on-line do IDE."
+
+msgid "Only comments from the following commit are shown below"
msgstr ""
msgid "Only project members can comment."
msgstr "Somente membros do projeto podem comentar."
-msgid "Open"
-msgstr "Abrir"
-
-msgid "Opened"
-msgstr "Aberto"
+msgid "Open in Xcode"
+msgstr "Abrir no Xcode"
msgid "OpenedNDaysAgo|Opened"
msgstr "Aberto"
@@ -2996,14 +3209,23 @@ msgstr "Aberto"
msgid "Opens in a new window"
msgstr "Abrir em nova janela"
+msgid "Operations"
+msgstr "Operações"
+
msgid "Options"
msgstr "Opções"
-msgid "Otherwise it is recommended you start with one of the options below."
+msgid "Or you can choose one of the suggested colors below"
msgstr ""
+msgid "Other Labels"
+msgstr "Outros Labels"
+
+msgid "Otherwise it is recommended you start with one of the options below."
+msgstr "Caso contrário, é recomendado que você inicie com uma das opções abaixo."
+
msgid "Outbound requests"
-msgstr ""
+msgstr "Pedidos de saída"
msgid "Overview"
msgstr "Visão geral"
@@ -3012,7 +3234,7 @@ msgid "Owner"
msgstr "Proprietário"
msgid "Pages"
-msgstr ""
+msgstr "Páginas"
msgid "Pagination|Last »"
msgstr "Último >>"
@@ -3027,19 +3249,34 @@ msgid "Pagination|« First"
msgstr "<< Primeiro"
msgid "Part of merge request changes"
-msgstr ""
+msgstr "Parte das mudanças do merge request"
msgid "Password"
msgstr "Senha"
+msgid "Paste your public SSH key, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'. Don't use your private SSH key."
+msgstr ""
+
+msgid "Pause"
+msgstr "Pausar"
+
msgid "Pending"
+msgstr "Pendente"
+
+msgid "Per job. If a job passes this threshold, it will be marked as failed"
msgstr ""
+msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgstr "Execute opções avançadas, como alterar o caminho, transferir ou remover o grupo."
+
msgid "Performance optimization"
-msgstr ""
+msgstr "Otimização de performance"
+
+msgid "Permissions"
+msgstr "Permissões"
msgid "Personal Access Token"
-msgstr ""
+msgstr "Token de Acesso Pessoal"
msgid "Pipeline"
msgstr "Pipeline"
@@ -3053,8 +3290,8 @@ msgstr "Agendamento da Pipeline"
msgid "Pipeline Schedules"
msgstr "Agendamentos da Pipeline"
-msgid "Pipeline quota"
-msgstr "Cota de pipeline"
+msgid "Pipeline triggers"
+msgstr ""
msgid "PipelineCharts|Failed:"
msgstr "Falhou:"
@@ -3123,52 +3360,64 @@ msgid "Pipelines|Build with confidence"
msgstr "Construa com confiança"
msgid "Pipelines|CI Lint"
-msgstr ""
+msgstr "CI Lint"
msgid "Pipelines|Clear Runner Caches"
-msgstr ""
+msgstr "Limpar cache dos Runners"
msgid "Pipelines|Get started with Pipelines"
msgstr "Saiba como funcionam as pipelines"
msgid "Pipelines|Loading Pipelines"
-msgstr ""
+msgstr "Carregando Pipelines"
msgid "Pipelines|Project cache successfully reset."
-msgstr ""
+msgstr "Cache do projeto redefinido com sucesso."
msgid "Pipelines|Run Pipeline"
-msgstr ""
+msgstr "Executar Pipeline"
msgid "Pipelines|Something went wrong while cleaning runners cache."
-msgstr ""
+msgstr "Algo deu errado ao limpar o cache dos runners."
msgid "Pipelines|There are currently no %{scope} pipelines."
-msgstr ""
+msgstr "Atualmente, não há pipelines de %{scope}."
msgid "Pipelines|There are currently no pipelines."
-msgstr ""
+msgstr "Atualmente não há pipelines."
msgid "Pipelines|This project is not currently set up to run pipelines."
-msgstr ""
+msgstr "Este projeto não está atualmente configurado para executar pipelines."
-msgid "Pipeline|Retry pipeline"
-msgstr ""
+msgid "Pipeline|Create for"
+msgstr "Criar para"
-msgid "Pipeline|Retry pipeline #%{pipelineId}?"
-msgstr ""
+msgid "Pipeline|Create pipeline"
+msgstr "Criar pipeline"
+
+msgid "Pipeline|Existing branch name or tag"
+msgstr "Nome de branch ou tag existente"
+
+msgid "Pipeline|Run Pipeline"
+msgstr "Executar Pipeline"
+
+msgid "Pipeline|Search branches"
+msgstr "Pesquisar branches"
+
+msgid "Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default."
+msgstr "Especifique valores de variáveis ​​a serem usados ​​nesta execução. Os valores especificados em %{settings_link} serão usados ​​por padrão."
msgid "Pipeline|Stop pipeline"
-msgstr ""
+msgstr "Parar pipeline"
msgid "Pipeline|Stop pipeline #%{pipelineId}?"
-msgstr ""
+msgstr "Parar pipeline #%{pipelineId}?"
-msgid "Pipeline|You’re about to retry pipeline %{pipelineId}."
-msgstr ""
+msgid "Pipeline|Variables"
+msgstr "Variáveis"
msgid "Pipeline|You’re about to stop pipeline %{pipelineId}."
-msgstr ""
+msgstr "Você está prestes a interromper o pipeline %{pipelineId}."
msgid "Pipeline|all"
msgstr "todos"
@@ -3182,29 +3431,47 @@ msgstr "com etapa"
msgid "Pipeline|with stages"
msgstr "com etapas"
-msgid "PlantUML"
+msgid "Plain diff"
msgstr ""
+msgid "PlantUML"
+msgstr "PlantUML"
+
msgid "Play"
msgstr "Iniciar"
-msgid "Please <a href=%{link_to_billing} target=\"_blank\" rel=\"noopener noreferrer\">enable billing for one of your projects to be able to create a Kubernetes cluster</a>, then try again."
-msgstr "Por favor, <a href=%{link_to_billing} target=\"_blank\" rel=\"noopener no referrer\">ative a cobrança para um de seus projetos para ser possível criar um cluster Kubernetes</a>, depois tente novamente."
+msgid "Please accept the Terms of Service before continuing."
+msgstr "Por favor, aceite os Termos de Serviço antes de continuar."
+
+msgid "Please select at least one filter to see results"
+msgstr "Por favor selecione pelo menos um filtro para ver os resultados"
msgid "Please solve the reCAPTCHA"
msgstr "Por favor, resolva o reCAPTCHA"
-msgid "Please wait while we connect to your repository. Refresh at will."
+msgid "Please try again"
msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
-msgstr ""
+msgstr "Por favor, aguarde enquanto importamos o repositório para você. Atualize à vontade."
msgid "Preferences"
msgstr "Preferências"
-msgid "Primary"
-msgstr "Primário"
+msgid "Preferences|Navigation theme"
+msgstr "Tema de navegação"
+
+msgid "Prioritize"
+msgstr "Priorizar"
+
+msgid "Prioritize label"
+msgstr "Priorizar label"
+
+msgid "Prioritized Labels"
+msgstr "Labels Priorizadas"
+
+msgid "Prioritized label"
+msgstr "Label priorizada"
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."
@@ -3213,7 +3480,7 @@ 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 "Private projects can be created in your personal namespace with:"
-msgstr ""
+msgstr "Projetos privados podem ser criados em seu namespace pessoal com:"
msgid "Profile"
msgstr "Perfil"
@@ -3221,6 +3488,12 @@ msgstr "Perfil"
msgid "Profiles|Account scheduled for removal."
msgstr "Conta agendada para remoção."
+msgid "Profiles|Change username"
+msgstr "Alterar nome de usuário"
+
+msgid "Profiles|Current path: %{path}"
+msgstr "Caminho atual: %{path}"
+
msgid "Profiles|Delete Account"
msgstr "Excluir conta"
@@ -3239,9 +3512,21 @@ msgstr "Senha inválida"
msgid "Profiles|Invalid username"
msgstr "Nome de usuário inválido"
+msgid "Profiles|Path"
+msgstr "Caminho"
+
msgid "Profiles|Type your %{confirmationValue} to confirm:"
msgstr "Escreva %{confirmationValue} para confirmar:"
+msgid "Profiles|Update username"
+msgstr "Atualizar nome de usuário"
+
+msgid "Profiles|Username change failed - %{message}"
+msgstr "Falha na alteração de nome de usuário - %{message}"
+
+msgid "Profiles|Username successfully changed"
+msgstr "Alteração de nome de usuário realizada com sucesso"
+
msgid "Profiles|You don't have access to delete this user."
msgstr "Você não tem permissão para apagar esse usuário."
@@ -3255,11 +3540,17 @@ msgid "Profiles|your account"
msgstr "sua conta"
msgid "Profiling - Performance bar"
-msgstr ""
+msgstr "Barra de performance"
msgid "Programming languages used in this repository"
msgstr "Linguagens de programação usadas nesse repositório"
+msgid "Progress"
+msgstr "Progresso"
+
+msgid "Project"
+msgstr "Projeto"
+
msgid "Project '%{project_name}' is in the process of being deleted."
msgstr "O projeto '%{project_name}' está sendo excluído."
@@ -3272,6 +3563,9 @@ msgstr "Projeto '%{project_name}' criado com sucesso."
msgid "Project '%{project_name}' was successfully updated."
msgstr "Projeto '%{project_name}' atualizado com sucesso."
+msgid "Project Badges"
+msgstr "Selos de projeto"
+
msgid "Project access must be granted explicitly to each user."
msgstr "Acesso ao projeto deve ser concedido explicitamente para cada usuário."
@@ -3299,30 +3593,6 @@ msgstr "Exportação do projeto iniciada. Um link para baixá-la será enviado p
msgid "ProjectActivityRSS|Subscribe"
msgstr "Inscreva-se"
-msgid "ProjectCreationLevel|Allowed to create projects"
-msgstr "Permitido a criação de projetos"
-
-msgid "ProjectCreationLevel|Default project creation protection"
-msgstr "Proteção de criação de projeto padrão"
-
-msgid "ProjectCreationLevel|Developers + Masters"
-msgstr "Desenvolvedores + Masters"
-
-msgid "ProjectCreationLevel|Masters"
-msgstr "Masters"
-
-msgid "ProjectCreationLevel|No one"
-msgstr "Ninguém"
-
-msgid "ProjectFeature|Disabled"
-msgstr "Desabilitado"
-
-msgid "ProjectFeature|Everyone with access"
-msgstr "Todos que possuem acesso"
-
-msgid "ProjectFeature|Only team members"
-msgstr "Apenas membros do time"
-
msgid "ProjectFileTree|Name"
msgstr "Nome"
@@ -3332,27 +3602,6 @@ msgstr "Nunca"
msgid "ProjectLifecycle|Stage"
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 é aplicada em nível de servidor e pode ser sobrescrita por qualquer administrador."
-
-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 foi 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 seja sobrescrita pelo administrador."
-
-msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails."
-msgstr "Usuários só podem fazer push de commits para esse repositório se os commits estiverem assinados com um de seus próprios e-mails verificados."
-
msgid "Projects"
msgstr "Projetos"
@@ -3377,117 +3626,114 @@ msgstr "Desculpe, nenhum projeto corresponde a sua pesquisa"
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr "Esta funcionalidade necessita de suporte à localStorage do navegador"
+msgid "PrometheusDashboard|Time"
+msgstr "Tempo"
+
msgid "PrometheusService|%{exporters} with %{metrics} were found"
-msgstr ""
+msgstr "%{exporters} com %{metrics} foram encontrados"
msgid "PrometheusService|<p class=\"text-tertiary\">No <a href=\"%{docsUrl}\">common metrics</a> were found</p>"
-msgstr ""
+msgstr "<p class=\"text-tertiary\">Nenhuma <a href=\"%{docsUrl}\">métrica comum</a> foi encontrada</p>"
msgid "PrometheusService|Active"
-msgstr ""
+msgstr "Ativo"
msgid "PrometheusService|Auto configuration"
-msgstr ""
+msgstr "Configuração automática"
msgid "PrometheusService|Automatically deploy and configure Prometheus on your clusters to monitor your project’s environments"
-msgstr ""
+msgstr "Fazer deploy automático e configurar o Prometheus nos seus clusters para monitorar o ambiente do seu projeto"
msgid "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."
msgstr "Por padrão, Prometheus escuta em 'http://localhost:9090'. Não é recomendado mudar o endereço padrão e sua porta, porque pode conflitar com outros serviços que estão executando no sevidor do Gitlab."
msgid "PrometheusService|Common metrics"
-msgstr ""
-
-msgid "PrometheusService|Common metrics are automatically monitored based on a library of metrics from popular exporters."
-msgstr ""
-
-msgid "PrometheusService|Custom metrics"
-msgstr ""
+msgstr "Métricas comuns"
msgid "PrometheusService|Finding and configuring metrics..."
msgstr "Encontrando e configurando métricas..."
-msgid "PrometheusService|Finding custom metrics..."
-msgstr ""
-
msgid "PrometheusService|Install Prometheus on clusters"
-msgstr ""
+msgstr "Instale o Prometheus nos clusters"
msgid "PrometheusService|Manage clusters"
-msgstr ""
+msgstr "Gerenciar clusters"
msgid "PrometheusService|Manual configuration"
-msgstr ""
+msgstr "Configuração manual"
msgid "PrometheusService|Metrics"
msgstr "Métricas"
+msgid "PrometheusService|Metrics are automatically configured and monitored based on a library of metrics from popular exporters."
+msgstr ""
+
msgid "PrometheusService|Missing environment variable"
msgstr "Variável de ambiente ausente"
msgid "PrometheusService|More information"
msgstr "Mais informações"
-msgid "PrometheusService|New metric"
-msgstr ""
-
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr "URL da API base do Prometheus. como http://prometheus.example.com/"
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
-msgstr ""
-
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
-msgstr ""
+msgstr "Prometheus está sendo automaticamente gerenciado nos seus clusters"
msgid "PrometheusService|Time-series monitoring service"
msgstr "Serviço de monitoramento de tempo-de-série"
msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
-msgstr ""
+msgstr "Para ativar a configuração manual, desinstale o Prometheus dos seus clusters"
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
-msgstr ""
+msgstr "Para ativar a instalação do Prometheus nos seus clusters, desative a configuração manual abaixo"
msgid "PrometheusService|Waiting for your first deployment to an environment to find common metrics"
-msgstr ""
+msgstr "PrometheusService| Aguardando sua primeira implantação em um ambiente para encontrar métricas comuns"
msgid "Promote"
-msgstr ""
+msgstr "Promover"
-msgid "Promote to Group Label"
-msgstr ""
+msgid "Promote these project milestones into a group milestone."
+msgstr "Promova esses milestones de projeto em milestones de grupo."
msgid "Promote to Group Milestone"
+msgstr "Promover para Milestone de Grupo"
+
+msgid "Promote to group label"
msgstr ""
msgid "Protip:"
msgstr "Dicas:"
+msgid "Provider"
+msgstr ""
+
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 "Regras de push"
+msgid "Public pipelines"
+msgstr ""
msgid "Push events"
msgstr "Eventos de push"
msgid "Push project from command line"
-msgstr ""
+msgstr "Fazer push do projeto por linha de comando"
msgid "Push to create a project"
-msgstr ""
-
-msgid "PushRule|Committer restriction"
-msgstr "Restrição de commit"
+msgstr "Push para criar um projeto"
msgid "Quick actions can be used in the issues description and comment boxes."
msgstr "Ações rápidas podem ser usadas nas descrições das issues e nas caixas de comentário."
+msgid "Re-deploy"
+msgstr ""
+
msgid "Read more"
msgstr "Leia mais"
@@ -3495,13 +3741,7 @@ msgid "Readme"
msgstr "Leia-me"
msgid "Real-time features"
-msgstr ""
-
-msgid "RefSwitcher|Branches"
-msgstr "Branches"
-
-msgid "RefSwitcher|Tags"
-msgstr "Tags"
+msgstr "Recursos em tempo real"
msgid "Reference:"
msgstr "Referência:"
@@ -3509,6 +3749,12 @@ msgstr "Referência:"
msgid "Register / Sign In"
msgstr "Registrar/Login"
+msgid "Register and see your runners for this group."
+msgstr "Registre-se e veja seus runners para este grupo."
+
+msgid "Register and see your runners for this project."
+msgstr ""
+
msgid "Registry"
msgstr "Registro"
@@ -3531,7 +3777,7 @@ msgid "Related Merged Requests"
msgstr "Merge Requests Relacionados"
msgid "Related merge requests"
-msgstr ""
+msgstr "Merge requests relacionados"
msgid "Remind later"
msgstr "Lembrar mais tarde"
@@ -3539,36 +3785,39 @@ msgstr "Lembrar mais tarde"
msgid "Remove"
msgstr "Remover"
+msgid "Remove Runner"
+msgstr "Remover Runner"
+
msgid "Remove avatar"
msgstr "Remover imagem"
+msgid "Remove priority"
+msgstr "Remover prioridade"
+
msgid "Remove project"
msgstr "Remover projeto"
-msgid "Repair authentication"
-msgstr "Reparar autenticação"
-
-msgid "Repo by URL"
-msgstr ""
-
msgid "Repository"
msgstr "Repositório"
-msgid "Repository has no locks."
+msgid "Repository Settings"
msgstr ""
msgid "Repository maintenance"
-msgstr ""
+msgstr "Manutenção do repositório"
-msgid "Repository mirror settings"
-msgstr ""
+msgid "Repository mirror"
+msgstr "Espelhamento do repositório"
msgid "Repository storage"
-msgstr ""
+msgstr "Armazenamento do Repositório"
msgid "Request Access"
msgstr "Solicitar acesso"
+msgid "Require all users to accept Terms of Service and Privacy Policy when they access GitLab."
+msgstr "Exija que todos os usuários aceitem Termos de Serviço e Política de Privacidade quando acessarem o GitLab."
+
msgid "Reset git storage health information"
msgstr "Reiniciar informações de status do storage Git"
@@ -3578,11 +3827,26 @@ msgstr "Recriar o token de status de saúde"
msgid "Reset runners registration token"
msgstr "Recriar o token de registro de runners"
-msgid "Resolve discussion"
+msgid "Resolve all discussions in new issue"
msgstr ""
-msgid "Response"
-msgstr ""
+msgid "Resolve conflicts on source branch"
+msgstr "Resolver conflitos na branch de origem"
+
+msgid "Resolve discussion"
+msgstr "Resolver discussão"
+
+msgid "Resume"
+msgstr "Continuar"
+
+msgid "Retry"
+msgstr "Tentar novamente"
+
+msgid "Retry this job"
+msgstr "Tentar novamente este trabalho"
+
+msgid "Retry verification"
+msgstr "Tentar novamente a verificação"
msgid "Reveal value"
msgid_plural "Reveal values"
@@ -3595,39 +3859,42 @@ msgstr "Reverter este commit"
msgid "Revert this merge request"
msgstr "Reverter esse merge request"
-msgid "Review the process for configuring service providers in your identity provider — in this case, GitLab is the \"service provider\" or \"relying party\"."
-msgstr ""
+msgid "Review"
+msgstr "Revisar"
msgid "Reviewing"
-msgstr ""
+msgstr "Revisão"
msgid "Reviewing (merge request !%{mergeRequestId})"
-msgstr ""
+msgstr "Revisando (merge request !%{mergeRequestId})"
-msgid "Roadmap"
+msgid "Rollback"
msgstr ""
-msgid "Run CI/CD pipelines for external repositories"
+msgid "Runner token"
msgstr ""
msgid "Runners"
-msgstr ""
+msgstr "Runners"
+
+msgid "Runners API"
+msgstr "Runners de API"
+
+msgid "Runners can be placed on separate users, servers, and even on your local machine."
+msgstr "Os corredores podem ser colocados em usuários, servidores e até mesmo em sua máquina local."
msgid "Running"
msgstr "Executando"
-msgid "SAML Single Sign On"
-msgstr ""
+msgid "SSH Keys"
+msgstr "Chaves SSH"
-msgid "SAML Single Sign On Settings"
+msgid "SSL Verification"
msgstr ""
-msgid "SHA1 fingerprint of the SAML token signing certificate. Get this from your identity provider, where it can also be called \"Thumbprint\"."
+msgid "Save"
msgstr ""
-msgid "SSH Keys"
-msgstr "Chaves SSH"
-
msgid "Save changes"
msgstr "Salvar alterações"
@@ -3649,15 +3916,30 @@ msgstr "Agendamentos"
msgid "Scheduling Pipelines"
msgstr "Agendando pipelines"
-msgid "Scoped issue boards"
-msgstr "Issue board de escopo"
+msgid "Scroll to bottom"
+msgstr "Rolar até o final"
+
+msgid "Scroll to top"
+msgstr "Voltar ao topo"
msgid "Search"
msgstr "Pesquisar"
+msgid "Search branches"
+msgstr "Pesquisar branches"
+
msgid "Search branches and tags"
msgstr "Procurar branch e tags"
+msgid "Search files"
+msgstr "Procurar arquivos"
+
+msgid "Search for projects, issues, etc."
+msgstr "Pesquise por projetos, issues, etc."
+
+msgid "Search merge requests"
+msgstr "Pesquisar merge requests"
+
msgid "Search milestones"
msgstr "Pesquisar milestones"
@@ -3673,20 +3955,20 @@ msgstr "Segundos antes de redefinir as informações de falha"
msgid "Seconds to wait for a storage access attempt"
msgstr "Segundo de espera para tentativa de acesso ao storage"
-msgid "Secret variables"
-msgstr "Variáveis secretas"
-
-msgid "Security report"
-msgstr "Relatório de segurança"
+msgid "Select"
+msgstr "Selecionar"
msgid "Select Archive Format"
msgstr "Selecionar Formato do Arquivo"
+msgid "Select a namespace to fork the project"
+msgstr "Selecione um namespace para realizar o fork do projeto"
+
msgid "Select a timezone"
msgstr "Selecionar fuso horário"
msgid "Select an existing Kubernetes cluster or create a new one"
-msgstr ""
+msgstr "Selecione um cluster existente do Kubernetes ou crie um novo"
msgid "Select assignee"
msgstr "Selecione o responsável"
@@ -3694,12 +3976,21 @@ msgstr "Selecione o responsável"
msgid "Select branch/tag"
msgstr "Selecionar o branch/tag"
+msgid "Select project"
+msgstr "Selecionar projeto"
+
+msgid "Select project and zone to choose machine type"
+msgstr "Selecione projeto e zona para escolher o tipo de máquina"
+
+msgid "Select project to choose zone"
+msgstr "Selecione o projeto para escolher a zona"
+
+msgid "Select source branch"
+msgstr "Selecionar branch de origem"
+
msgid "Select target branch"
msgstr "Selecionar branch de destino"
-msgid "Selective synchronization"
-msgstr "Sincronização seletiva"
-
msgid "Send email"
msgstr "Enviar e-mail"
@@ -3715,26 +4006,23 @@ msgstr "Versão do servidor"
msgid "Service Templates"
msgstr "Modelos de serviço"
-msgid "Service URL"
-msgstr ""
-
msgid "Session expiration, projects limit and attachment size."
-msgstr ""
+msgstr "Expiração de sessão, limite de projetos e tamanho de anexo."
msgid "Set a password on your account to pull or push via %{protocol}."
msgstr "Defina uma senha para sua conta para aceitar ou entregar código via %{protocol}."
msgid "Set default and restrict visibility levels. Configure import sources and git access protocol."
-msgstr ""
+msgstr "Definir padrão e restringir os níveis de visibilidade. Configurar fontes de importação e protocolo de acesso git."
msgid "Set max session time for web terminal."
-msgstr ""
+msgstr "Defina o tempo máximo da sessão para o terminal da web."
msgid "Set notification email for abuse reports."
-msgstr ""
+msgstr "Definir notificação por e-mail para relatórios de abuso."
msgid "Set requirements for a user to sign-in. Enable mandatory two-factor authentication."
-msgstr ""
+msgstr "Definir requisitos para um usuário entrar. Ative a autenticação obrigatória de dois fatores."
msgid "Set up CI/CD"
msgstr "Configurar CI/CD"
@@ -3742,9 +4030,6 @@ msgstr "Configurar CI/CD"
msgid "Set up Koding"
msgstr "Configurar Koding"
-msgid "Set up assertions/attributes/claims (email, first_name, last_name) and NameID according to %{docsLinkStart}the documentation %{icon}%{docsLinkEnd}"
-msgstr ""
-
msgid "SetPasswordToCloneLink|set a password"
msgstr "defina uma senha"
@@ -3752,21 +4037,24 @@ msgid "Settings"
msgstr "Configurações"
msgid "Setup a specific Runner automatically"
-msgstr ""
+msgstr "Configurar um Runner específico automaticamente"
-msgid "Share the <strong>%{sso_label}</strong> with members so they can sign in to your group through your identity provider"
-msgstr ""
+msgid "Share"
+msgstr "Compartilhar"
-msgid "SharedRunnersMinutesSettings|By resetting the pipeline minutes for this namespace, the currently used minutes will be set to zero."
-msgstr ""
+msgid "Shared Runners"
+msgstr "Runners Compartilhados"
-msgid "SharedRunnersMinutesSettings|Reset pipeline minutes"
-msgstr ""
+msgid "Show command"
+msgstr "Exibir comando"
+
+msgid "Show complete raw log"
+msgstr "Visualizar raw log completo"
-msgid "SharedRunnersMinutesSettings|Reset used pipeline minutes"
+msgid "Show latest version"
msgstr ""
-msgid "Show command"
+msgid "Show latest version of the diff"
msgstr ""
msgid "Show parent pages"
@@ -3775,51 +4063,48 @@ msgstr "Mostrar páginas acima"
msgid "Show parent subgroups"
msgstr "Mostrar subgrupos acima"
+msgid "Show whitespace changes"
+msgstr ""
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "Mostrando %d evento"
msgstr[1] "Mostrando %d eventos"
-msgid "Sidebar|Change weight"
-msgstr "Mudar peso"
-
-msgid "Sidebar|No"
-msgstr "Não"
-
-msgid "Sidebar|None"
-msgstr "Nenhum"
+msgid "Side-by-side"
+msgstr ""
-msgid "Sidebar|Weight"
-msgstr "Peso"
+msgid "Sign out"
+msgstr "Sair"
msgid "Sign-in restrictions"
-msgstr ""
+msgstr "Restrições de login"
msgid "Sign-up restrictions"
-msgstr ""
+msgstr "Restrições de cadastro"
msgid "Size and domain settings for static websites"
-msgstr ""
+msgstr "Configurações de tamanho e domínio para sites estáticos"
-msgid "Slack application"
+msgid "Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job"
msgstr ""
msgid "Snippets"
msgstr "Snippets"
msgid "Something went wrong on our end"
-msgstr ""
+msgstr "Algo deu errado do nosso lado"
msgid "Something went wrong on our end."
-msgstr ""
+msgstr "Algo deu errado do nosso lado."
-msgid "Something went wrong when toggling the button"
+msgid "Something went wrong on our end. Please try again!"
msgstr ""
-msgid "Something went wrong while fetching Dependency Scanning."
-msgstr ""
+msgid "Something went wrong when toggling the button"
+msgstr "Algo deu errado ao alternar o botão"
-msgid "Something went wrong while fetching SAST."
+msgid "Something went wrong while closing the %{issuable}. Please try again later"
msgstr ""
msgid "Something went wrong while fetching the projects."
@@ -3828,6 +4113,12 @@ 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 "Something went wrong while reopening the %{issuable}. Please try again later"
+msgstr ""
+
+msgid "Something went wrong while resolving this discussion. Please try again."
+msgstr ""
+
msgid "Something went wrong. Please try again."
msgstr "Algo deu errado. Por favor, tente novamente."
@@ -3873,9 +4164,6 @@ msgstr "Últimos atualizados"
msgid "SortOptions|Least popular"
msgstr "Menos populares"
-msgid "SortOptions|Less weight"
-msgstr "Menos peso"
-
msgid "SortOptions|Milestone"
msgstr "Milestone"
@@ -3885,9 +4173,6 @@ msgstr "Milestone de fim mais longo"
msgid "SortOptions|Milestone due soon"
msgstr "Milestone de fim mais próximo"
-msgid "SortOptions|More weight"
-msgstr "Mais peso"
-
msgid "SortOptions|Most popular"
msgstr "Mais populares"
@@ -3927,9 +4212,6 @@ msgstr "Iniciar mais tarde"
msgid "SortOptions|Start soon"
msgstr "Iniciar mais próximo"
-msgid "SortOptions|Weight"
-msgstr "Peso"
-
msgid "Source"
msgstr "Origem"
@@ -3946,19 +4228,46 @@ msgid "Spam Logs"
msgstr "Logs de spam"
msgid "Spam and Anti-bot Protection"
-msgstr ""
+msgstr "Proteção contra spam e anti-bot"
+
+msgid "Specific Runners"
+msgstr "Runners Específicos"
msgid "Specify the following URL during the Runner setup:"
msgstr "Especifique a seguinte URL durante a configuração do Runner:"
+msgid "Squash commits"
+msgstr "Squash commits"
+
+msgid "Stage"
+msgstr ""
+
+msgid "Stage & Commit"
+msgstr ""
+
+msgid "Stage all changes"
+msgstr ""
+
+msgid "Stage changes"
+msgstr "Mudanças na lista de commit"
+
+msgid "Staged"
+msgstr "Na lista para commit"
+
+msgid "Staged %{type}"
+msgstr ""
+
+msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
+msgstr "Coloque uma estrela em um label para dar prioridade. Altere a ordem das labels priorizadas arrastando-as para alterar sua prioridade."
+
msgid "StarProject|Star"
msgstr "Marcar"
msgid "Starred Projects"
-msgstr ""
+msgstr "Projetos favoritos"
msgid "Starred Projects' Activity"
-msgstr ""
+msgstr "Atividade dos projetos favoritos"
msgid "Starred projects"
msgstr "Projetos favoritos"
@@ -3972,12 +4281,15 @@ msgstr "Inicie o Runner!"
msgid "Started"
msgstr "Iniciado"
-msgid "State your message to activate"
-msgstr ""
+msgid "Starts at (UTC)"
+msgstr "Começa em (UTC)"
msgid "Status"
msgstr "Status"
+msgid "Stop this environment"
+msgstr "Parar este ambiente"
+
msgid "Stopped"
msgstr "Parado"
@@ -3987,18 +4299,21 @@ msgstr "Armazenamento"
msgid "Subgroups"
msgstr "Subgrupos"
+msgid "Subscribe"
+msgstr "Inscrever-se"
+
+msgid "Subscribe at group level"
+msgstr "Inscrever-se no nível de grupo"
+
+msgid "Subscribe at project level"
+msgstr "Inscrever-se no nível do projeto"
+
msgid "Switch branch/tag"
msgstr "Trocar branch/tag"
-msgid "System"
-msgstr ""
-
msgid "System Hooks"
msgstr "Hooks do sistema"
-msgid "System header and footer:"
-msgstr ""
-
msgid "Tag (%{tag_count})"
msgid_plural "Tags (%{tag_count})"
msgstr[0] "Tag (%{tag_count})"
@@ -4007,6 +4322,9 @@ msgstr[1] "Tags (%{tag_count})"
msgid "Tags"
msgstr "Tags"
+msgid "Tags:"
+msgstr "Tags:"
+
msgid "TagsPage|Browse commits"
msgstr "Navegar nos commits"
@@ -4070,8 +4388,8 @@ msgstr "Essa tag não tem release notes."
msgid "TagsPage|Use git tag command to add a new one:"
msgstr "Use o comando \"git tag\" para adiciona uma nova tag:"
-msgid "TagsPage|Write your release notes or drag files here..."
-msgstr "Escreve seu release notes ou arraste o arquivo aqui..."
+msgid "TagsPage|Write your release notes or drag files here…"
+msgstr ""
msgid "TagsPage|protected"
msgstr "protegido"
@@ -4080,16 +4398,19 @@ msgid "Target Branch"
msgstr "Branch de destino"
msgid "Target branch"
-msgstr ""
+msgstr "Branch de destino"
msgid "Team"
msgstr "Equipe"
-msgid "Thanks! Don't show me this again"
-msgstr "Obrigado! Não mostrar novamente"
+msgid "Terms of Service Agreement and Privacy Policy"
+msgstr "Contrato de Termos de Serviço e Política de Privacidade"
-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çada no GitLab é um serviço de pesquisa poderoso que economiza seu tempo. Ao invés de criar códigos duplicados e perder seu tempo, você pode agora pesquisar códigos de outros times que podem ajudar em seu projeto."
+msgid "Terms of Service and Privacy Policy"
+msgstr "Termos de Serviço e Política de Privacidade"
+
+msgid "Test coverage parsing"
+msgstr ""
msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project"
msgstr "Issue Tracker é o lugar para adicionar coisas que precisam ser melhoradas ou resolvidas em um projeto"
@@ -4097,29 +4418,23 @@ msgstr "Issue Tracker é o lugar para adicionar coisas que precisam ser melhorad
msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
msgstr "Issue Tracker é o lugar para adicionar coisas que precisam ser melhoradas ou resolvidas em um projeto. Você precisa se registrar ou fazer login para criar alguma Issue para este projeto."
-msgid "The X509 Certificate to use when mutual TLS is required to communicate with the external authorization service. If left blank, the server certificate is still validated when accessing over HTTPS."
-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 "A etapa de codificação mostra o tempo desde a entrega do primeiro commit até a criação do merge request. Os dados serão automaticamente adicionados aqui desde o momento de criação do merge request."
msgid "The collection of events added to the data gathered for that stage."
msgstr "A coleção de eventos adicionados aos dados coletados para essa etapa."
-msgid "The connection will time out after %{timeout}. For repositories that take longer, use a clone/push combination."
-msgstr ""
-
msgid "The fork relationship has been removed."
msgstr "O relacionamento como fork foi removido."
msgid "The import will time out after %{timeout}. For repositories that take longer, use a clone/push combination."
-msgstr ""
+msgstr "A importação expirará após %{timeout}. Para repositórios que demoram mais tempo, use a combinação clone/push."
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 maximum file size allowed is 200KB."
-msgstr ""
+msgstr "O tamanho máximo do arquivo é de 200KB."
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."
@@ -4127,7 +4442,7 @@ msgstr "O número de tentativas que gitlab fará para acessar um storage."
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 passphrase required to decrypt the private key. This is optional and the value is encrypted at rest."
+msgid "The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>"
msgstr ""
msgid "The phase of the development lifecycle."
@@ -4136,9 +4451,6 @@ msgstr "A fase do ciclo de vida do desenvolvimento."
msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit."
msgstr "A etapa de planejamento mostra o tempo do passo anterior até a publicação de seu primeiro conjunto de mudanças. Este tempo será adicionado automaticamente assim que você enviar seu primeiro conjunto de mudanças."
-msgid "The private key to use when a client certificate is provided. This value is encrypted at rest."
-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 "A etapa de produção mostra o tempo total que leva entre criar uma issue e implantar o código em produção. Os dados serão adicionados automaticamente assim que você completar todo o ciclo de produção."
@@ -4152,15 +4464,15 @@ msgid "The repository for this project does not exist."
msgstr "Não existe repositório para este projeto."
msgid "The repository for this project is empty"
-msgstr ""
+msgstr "O repositório para este projeto está vazio"
msgid "The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>."
-msgstr ""
+msgstr "O repositório deve ser acessível por <code>http://</code>, <code>https://</code> ou <code>git://</code>."
msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request."
msgstr "A etapa de revisão mostra o tempo de criação de uma solicitação de incorporação até sua aceitação. Os dados serão automaticamente adicionados depois que sua primeira solicitação de incorporação for aceita."
-msgid "The roadmap shows the progress of your epics along a timeline"
+msgid "The secure token used by the Runner to checkout the project"
msgstr ""
msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time."
@@ -4176,7 +4488,7 @@ msgid "The time in seconds GitLab will try to access storage. After this time a
msgstr "Tempo em segundos que o GitLab tentará acessar o storage. Depois desse tempo, um erro de tempo excedido será disparado."
msgid "The time in seconds between storage checks. When a previous check did complete yet, GitLab will skip a check."
-msgstr ""
+msgstr "O tempo em segundos entre as verificações de armazenamento. Quando uma verificação anterior foi concluída ainda, o GitLab pulará uma verificação."
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."
@@ -4187,14 +4499,20 @@ msgstr "O valor situado no ponto médio de uma série de valores observados. Ex.
msgid "There are no issues to show"
msgstr "Não há issues para mostrar"
+msgid "There are no labels yet"
+msgstr ""
+
msgid "There are no merge requests to show"
msgstr "Não há merge requests pra mostrar"
msgid "There are problems accessing Git storage: "
msgstr "Há problemas para acessar o storage Git: "
-msgid "There was an error loading results"
-msgstr ""
+msgid "There was an error loading jobs"
+msgstr "Ocorreu um erro ao carregar tarefas"
+
+msgid "There was an error loading latest pipeline"
+msgstr "Ocorreu um erro ao carregar o pipeline mais recente"
msgid "There was an error loading users activity calendar."
msgstr "Erro ao carregar calendário de atividades."
@@ -4214,12 +4532,21 @@ msgstr "Erro ao se inscrever nessa label."
msgid "There was an error when unsubscribing from this label."
msgstr "Erro ao se anular a inscrição dessa label."
-msgid "This board\\'s scope is reduced"
-msgstr "O escopo desse board está reduzido"
+msgid "They can be managed using the %{link}."
+msgstr "Eles podem ser gerenciados usando o %{link}."
+
+msgid "This GitLab instance does not provide any shared Runners yet. Instance administrators can register shared Runners in the admin area."
+msgstr "Esta instância do GitLab ainda não fornece nenhum Runner compartilhado. Os administradores da instância podem registrar os Runners compartilhados na área de administração."
+
+msgid "This diff is collapsed."
+msgstr ""
msgid "This directory"
msgstr "Esse diretório"
+msgid "This group does not provide any group Runners yet."
+msgstr "Este grupo não fornece nenhum grupo de Runners ainda."
+
msgid "This is a confidential issue."
msgstr "Essa issue é confidencial."
@@ -4236,19 +4563,28 @@ msgid "This issue is locked."
msgstr "Essa issue está bloqueada."
msgid "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
-msgstr ""
+msgstr "Esta tarefa depende de um usuário para acionar seu processo. Geralmente eles são usados ​​para implantar código em ambientes de produção"
msgid "This job depends on upstream jobs that need to succeed in order for this job to be triggered"
-msgstr ""
+msgstr "Esta tarefa depende das tarefas do upstream que precisam ser bem-sucedidos para que essa tarefa seja acionada"
+
+msgid "This job does not have a trace."
+msgstr "Esta tarefa não possui um traço."
+
+msgid "This job has been canceled"
+msgstr "Esta etapa foi cancelada"
+
+msgid "This job has been skipped"
+msgstr "Este trabalho foi pulado"
msgid "This job has not been triggered yet"
-msgstr ""
+msgstr "Esta build ainda não foi acionada"
msgid "This job has not started yet"
msgstr "Esse processo ainda não começou"
msgid "This job is in pending state and is waiting to be picked by a runner"
-msgstr ""
+msgstr "Esta tarefa está em estado pendente e está esperando para ser escolhido por um runner"
msgid "This job requires a manual action"
msgstr "Este Job exige uma ação manual"
@@ -4259,20 +4595,29 @@ msgstr "Isto significa que você não pode entregar código até que crie um rep
msgid "This merge request is locked."
msgstr "Esse merge request está bloqueado."
+msgid "This option is disabled while you still have unstaged changes"
+msgstr "Esta opção ficará desativada enquanto você ainda tiver alterações fora da lista para commit"
+
msgid "This page is unavailable because you are not allowed to read information across multiple projects."
msgstr "Esta página não está disponível porque você não tem permissão para ler informações de vários projetos."
+msgid "This page will be removed in a future release."
+msgstr "Esta página será removida em uma versão futura."
+
msgid "This project"
msgstr "Esse projeto"
+msgid "This project does not belong to a group and can therefore not make use of group Runners."
+msgstr "Este projeto não pertence a um grupo e, portanto, não pode usar os Runners do grupo."
+
msgid "This repository"
msgstr "Esse repositório"
-msgid "This will delete the custom metric, Are you sure?"
-msgstr "Esta ação excluirá uma métrica personalizada, você tem certeza?"
+msgid "This source diff could not be displayed because it is too large."
+msgstr ""
-msgid "Those emails automatically become issues (with the comments becoming the email conversation) listed here."
-msgstr "Esses e-mails se tornarão issues automaticamente (com os comentários se tornando uma conversa de e-mail) listadas aqui."
+msgid "This user has no identities"
+msgstr ""
msgid "Time before an issue gets scheduled"
msgstr "Tempo até que uma issue seja agendada"
@@ -4283,105 +4628,108 @@ msgstr "Tempo até que uma issue comece a ser implementado"
msgid "Time between merge request creation and merge/close"
msgstr "Tempo entre a criação da solicitação de incorporação e a aceitação/fechamento"
-msgid "Time between updates and capacity settings."
-msgstr ""
+msgid "Time remaining"
+msgstr "Tempo restante"
-msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
-msgstr ""
+msgid "Time spent"
+msgstr "Tempo gasto"
msgid "Time tracking"
-msgstr ""
+msgstr "Acompanhamento de tempo"
msgid "Time until first merge request"
msgstr "Tempo até a primeira solicitação de incorporação"
msgid "TimeTrackingEstimated|Est"
-msgstr ""
+msgstr "Est"
msgid "TimeTracking|Estimated:"
-msgstr ""
+msgstr "Estimado:"
msgid "TimeTracking|Spent"
-msgstr ""
+msgstr "Gasto"
msgid "Timeago|%s days ago"
-msgstr "há %s dias"
+msgstr "%s dias"
msgid "Timeago|%s days remaining"
msgstr "%s dias restantes"
+msgid "Timeago|%s hours ago"
+msgstr "%s horas atrás"
+
msgid "Timeago|%s hours remaining"
msgstr "%s horas restantes"
msgid "Timeago|%s minutes ago"
-msgstr "há %s minutos"
+msgstr "%s minutos"
msgid "Timeago|%s minutes remaining"
msgstr "%s minutos restantes"
msgid "Timeago|%s months ago"
-msgstr "há %s meses"
+msgstr "%s meses"
msgid "Timeago|%s months remaining"
msgstr "%s meses restantes"
+msgid "Timeago|%s seconds ago"
+msgstr "%s segundos atrás"
+
msgid "Timeago|%s seconds remaining"
msgstr "%s segundos restantes"
msgid "Timeago|%s weeks ago"
-msgstr "há %s semanas"
+msgstr "%s semanas"
msgid "Timeago|%s weeks remaining"
msgstr "%s semanas restantes"
msgid "Timeago|%s years ago"
-msgstr "há %s anos"
+msgstr "%s anos"
msgid "Timeago|%s years remaining"
msgstr "%s anos restantes"
+msgid "Timeago|1 day ago"
+msgstr "1 dia atrás"
+
msgid "Timeago|1 day remaining"
msgstr "1 dia restante"
+msgid "Timeago|1 hour ago"
+msgstr "1 hora atrás"
+
msgid "Timeago|1 hour remaining"
msgstr "1 hora restante"
+msgid "Timeago|1 minute ago"
+msgstr "1 minuto atrás"
+
msgid "Timeago|1 minute remaining"
msgstr "1 minuto restante"
+msgid "Timeago|1 month ago"
+msgstr "1 mês atrás"
+
msgid "Timeago|1 month remaining"
msgstr "1 mês restante"
+msgid "Timeago|1 week ago"
+msgstr "1 semana atrás"
+
msgid "Timeago|1 week remaining"
msgstr "1 semana restante"
+msgid "Timeago|1 year ago"
+msgstr "1 ano atrás"
+
msgid "Timeago|1 year remaining"
msgstr "1 ano restante"
msgid "Timeago|Past due"
msgstr "Venceu"
-msgid "Timeago|a day ago"
-msgstr "há um dia"
-
-msgid "Timeago|a month ago"
-msgstr "há um mês"
-
-msgid "Timeago|a week ago"
-msgstr "há uma semana"
-
-msgid "Timeago|a year ago"
-msgstr "há um ano"
-
-msgid "Timeago|about %s hours ago"
-msgstr "há cerca de %s horas"
-
-msgid "Timeago|about a minute ago"
-msgstr "há cerca de um minuto"
-
-msgid "Timeago|about an hour ago"
-msgstr "há cerca de uma hora"
-
msgid "Timeago|in %s days"
msgstr "em %s dias"
@@ -4421,11 +4769,14 @@ msgstr "em 1 semana"
msgid "Timeago|in 1 year"
msgstr "em 1 ano"
-msgid "Timeago|in a while"
-msgstr "há algum tempo"
+msgid "Timeago|just now"
+msgstr "agora"
-msgid "Timeago|less than a minute ago"
-msgstr "há menos de um minuto"
+msgid "Timeago|right now"
+msgstr "agora mesmo"
+
+msgid "Timeout"
+msgstr ""
msgid "Time|hr"
msgid_plural "Time|hrs"
@@ -4441,47 +4792,38 @@ msgid "Time|s"
msgstr "s"
msgid "Tip:"
-msgstr ""
-
-msgid "Title"
-msgstr "Título"
+msgstr "Dica:"
msgid "To GitLab"
-msgstr ""
+msgstr "Para o GitLab"
-msgid "To connect GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to connect."
-msgstr ""
-
-msgid "To connect GitHub repositories, you first need to authorize GitLab to access the list of your GitHub repositories:"
-msgstr ""
-
-msgid "To connect an SVN repository, check out %{svn_link}."
+msgid "To add an SSH key you need to %{generate_link_start}generate one%{link_end} or use an %{existing_link_start}existing key%{link_end}."
msgstr ""
msgid "To import GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
-msgstr ""
+msgstr "Para importar repositórios do GitHub, você pode usar um %{personal_access_token_link}. Ao criar seu Token de Acesso Pessoal, você precisará selecionar o escopo <code>repo</code>, para que possamos exibir uma lista de seus repositórios públicos e privados que estão disponíveis para importação."
msgid "To import GitHub repositories, you first need to authorize GitLab to access the list of your GitHub repositories:"
-msgstr ""
+msgstr "Para importar repositórios do GitHub, primeiro você precisa autorizar o GitLab a acessar a lista de seus repositórios do GitHub:"
msgid "To import an SVN repository, check out %{svn_link}."
-msgstr ""
+msgstr "Para importar um repositório SVN, confira %{svn_link}."
-msgid "To only use CI/CD features for an external repository, choose <strong>CI/CD for external repo</strong>."
-msgstr ""
-
-msgid "To set up SAML authentication for your group through an identity provider like Azure, Okta, Onelogin, Ping Identity, or your custom SAML 2.0 provider:"
-msgstr ""
+msgid "To start serving your jobs you can add Runners to your group"
+msgstr "Para começar a servir suas tarefas, você pode adicionar Runners ao seu grupo"
msgid "To validate your GitLab CI configurations, go to 'CI/CD → Pipelines' inside your project, and click on the 'CI Lint' button."
-msgstr ""
-
-msgid "To view the roadmap, add a planned start or finish date to one of your epics in this group or its subgroups. Only epics in the past 3 months and the next 3 months are shown."
-msgstr ""
+msgstr "Para validar suas configurações de CI do GitLab, vá para 'CI/CD → Pipelines' dentro do seu projeto e clique no botão 'CI Lint'."
msgid "Todo"
msgstr "Pendente"
+msgid "Toggle Sidebar"
+msgstr "Alternar barra lateral"
+
+msgid "Toggle discussion"
+msgstr "Alternar discussão"
+
msgid "Toggle sidebar"
msgstr "Ativar/Desativar barra lateral"
@@ -4491,6 +4833,9 @@ msgstr "Mudar Status: Desligado"
msgid "ToggleButton|Toggle Status: ON"
msgstr "Mudar Status: Ligado"
+msgid "Too many changes to show."
+msgstr ""
+
msgid "Total Time"
msgstr "Tempo Total"
@@ -4500,23 +4845,20 @@ msgstr "Tempo de teste total para todos os commits/merges"
msgid "Total: %{total}"
msgstr "Total: %{total}"
-msgid "Track activity with Contribution Analytics."
-msgstr "Acompanhe a atividade com o Contribution Analytics."
-
-msgid "Track groups of issues that share a theme, across projects and milestones"
-msgstr "Acompanhe grupos de questões que compartilhem um tema, em projetos e milestones"
-
msgid "Track time with quick actions"
-msgstr ""
+msgstr "Acompanhe o tempo com ações rápidas"
msgid "Trigger this manual action"
+msgstr "Acionar esta ação manual"
+
+msgid "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."
msgstr ""
-msgid "Turn on Service Desk"
-msgstr "Ativar Service Desk"
+msgid "Try again"
+msgstr "Tente novamente"
-msgid "Unknown"
-msgstr "Desconhecido"
+msgid "Unable to load the diff. %{button_try_again}"
+msgstr "Não é possível carregar o diff. %{button_try_again}"
msgid "Unlock"
msgstr "Desbloquear"
@@ -4525,28 +4867,48 @@ msgid "Unlocked"
msgstr "Desbloqueado"
msgid "Unresolve discussion"
+msgstr "Reabrir discussão"
+
+msgid "Unstage all changes"
+msgstr ""
+
+msgid "Unstage changes"
+msgstr "Retirar mudanças da lista de commit"
+
+msgid "Unstaged"
+msgstr "Fora da lista de commit"
+
+msgid "Unstaged %{type}"
+msgstr ""
+
+msgid "Unstaged and staged %{type}"
msgstr ""
msgid "Unstar"
msgstr "Desmarcar"
-msgid "Up to date"
-msgstr "Atualizado"
+msgid "Unsubscribe"
+msgstr "Cancelar inscrição"
-msgid "Upgrade your plan to activate Advanced Global Search."
-msgstr "Atualize seu plano para ativar a Pesquisa Global Avançada."
+msgid "Unsubscribe at group level"
+msgstr "Cancelar inscrição no nível do grupo"
-msgid "Upgrade your plan to activate Contribution Analytics."
-msgstr "Atualize seu plano para ativar o Contribution Analytics."
+msgid "Unsubscribe at project level"
+msgstr "Cancelar inscrição no nível do projeto"
-msgid "Upgrade your plan to activate Group Webhooks."
-msgstr "Atualize seu plano para ativar Webhooks do grupo."
+msgid "Unverified"
+msgstr "Não verificado"
-msgid "Upgrade your plan to activate Issue weight."
-msgstr "Atualize seu plano para ativar peso nas issues."
+msgid "Up to date"
+msgstr "Atualizado"
-msgid "Upgrade your plan to improve Issue boards."
-msgstr "Atualize seu plano para melhorar os issue boards."
+msgid "Update %{files}"
+msgid_plural "Update %{files} files"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Update your group name, description, avatar, and other general settings."
+msgstr "Atualize o nome do seu grupo, descrição, avatar e outras configurações gerais."
msgid "Upload New File"
msgstr "Enviar Novo Arquivo"
@@ -4561,13 +4923,13 @@ msgid "UploadLink|click to upload"
msgstr "clique para fazer upload"
msgid "Upvotes"
-msgstr ""
+msgstr "Votos positivos"
msgid "Usage statistics"
-msgstr ""
+msgstr "Estatísticas de uso"
-msgid "Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab"
-msgstr "Use o Service Desk para se conectar com seus usuários (por exemplo, para oferecer suporte ao cliente) por email dentro do GitLab"
+msgid "Use group milestones to manage issues from multiple projects in the same milestone."
+msgstr "Use milestones de grupo para gerenciar problemas de vários projetos no mesmo milestone."
msgid "Use the following registration token during setup:"
msgstr "Use o seguinte token de registro durante a configuração:"
@@ -4575,50 +4937,56 @@ 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 "Used by members to sign in to your group in GitLab"
-msgstr ""
-
msgid "User and IP Rate Limits"
+msgstr "Limites de Taxa de Usuário e IP"
+
+msgid "Users"
msgstr ""
+msgid "Variables"
+msgstr "Variáveis"
+
msgid "Variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. You can use variables for passwords, secret keys, or whatever you want."
-msgstr ""
+msgstr "As variáveis ​​são aplicadas aos ambientes por meio do runner. Eles podem ser protegidos apenas expondo-os a branches protegidas ou tags. Você pode usar variáveis ​​para senhas, chaves secretas ou o que você quiser."
msgid "Various container registry settings."
-msgstr ""
+msgstr "Várias configurações de registry container."
msgid "Various email settings."
-msgstr ""
+msgstr "Várias configurações de email."
msgid "Various settings that affect GitLab performance."
-msgstr ""
+msgstr "Várias configurações que afetam o desempenho do GitLab."
-msgid "View and edit lines"
-msgstr ""
-
-msgid "View epics list"
-msgstr ""
+msgid "Verified"
+msgstr "Verificado"
msgid "View file @ "
msgstr "Ver arquivo @ "
msgid "View group labels"
-msgstr ""
+msgstr "Visualizar labels do grupo"
+
+msgid "View jobs"
+msgstr "Visualizar tarefas"
msgid "View labels"
msgstr "Visualizar etiquetas"
+msgid "View log"
+msgstr "Visualizar log"
+
msgid "View open merge request"
msgstr "Ver merge request aberto"
msgid "View project labels"
-msgstr ""
+msgstr "Ver labels do projeto"
msgid "View replaced file @ "
msgstr "Ver arquivo substituído @ "
msgid "Visibility and access controls"
-msgstr ""
+msgstr "Visibilidade e controles de acesso"
msgid "VisibilityLevel|Internal"
msgstr "Interno"
@@ -4635,9 +5003,6 @@ msgstr "Desconhecido"
msgid "Want to see the data? Please ask an administrator for access."
msgstr "Precisa visualizar os dados? Solicite acesso ao administrador."
-msgid "We could not verify that one of your projects on GCP has billing enabled. Please try again."
-msgstr "Não foi possível verificar se um dos seus projetos no GCP possui o faturamento ativado. Por favor, tente novamente."
-
msgid "We don't have enough data to show this stage."
msgstr "Esta etapa não possui dados suficientes para exibição."
@@ -4645,19 +5010,16 @@ msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr "Queremos ter certeza de que é você, confirme que você não é um robô."
msgid "Web IDE"
-msgstr ""
+msgstr "IDE Web"
msgid "Web terminal"
-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 "Webhooks permitem que você acione uma URL se, por exemplo, quando 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."
+msgstr "Terminal Web"
-msgid "Weight"
-msgstr "Peso"
+msgid "When a runner is locked, it cannot be assigned to other projects"
+msgstr "Quando um runner está bloqueado, não pode ser atribuído a outros projetos"
-msgid "When leaving the URL blank, classification labels can still be specified whitout disabling cross project features or performing external authorization checks."
-msgstr ""
+msgid "When enabled, users cannot use GitLab until the terms have been accepted."
+msgstr "Quando ativado, os usuários não podem usar o GitLab até que os termos tenham sido aceitos."
msgid "Wiki"
msgstr "Wiki"
@@ -4678,13 +5040,37 @@ msgid "WikiClone|Start Gollum and edit locally"
msgstr "Inicie o Gollum e edite localmente"
msgid "WikiEditPageTip|Tip: You can move this page by adding the path to the beginning of the title."
-msgstr ""
+msgstr "Dica: Você pode mover esta página adicionando o caminho para o início do título."
msgid "WikiEdit|There is already a page with the same title in that path."
+msgstr "Já existe uma página com o mesmo título nesse caminho."
+
+msgid "WikiEmptyIssueMessage|Suggest wiki improvement"
+msgstr "Sugira uma melhoria na wiki"
+
+msgid "WikiEmptyIssueMessage|You must be a project member in order to add wiki pages. If you have suggestions for how to improve the wiki for this project, consider opening an issue in the %{issues_link}."
+msgstr "Você deve ser um membro do projeto para adicionar páginas na wiki. Se você tiver sugestões de como melhorar o wiki para este projeto, considere a possibilidade de abrir uma issue no %{issues_link}."
+
+msgid "WikiEmptyIssueMessage|issue tracker"
+msgstr "issue tracker"
+
+msgid "WikiEmpty|A wiki is where you can store all the details about your project. This can include why you've created it, its principles, how to use it, and so on."
msgstr ""
-msgid "WikiEmptyPageError|You are not allowed to create wiki pages"
-msgstr "Você não tem permissão para criar páginas web"
+msgid "WikiEmpty|Create your first page"
+msgstr "Crie sua primeira página"
+
+msgid "WikiEmpty|Suggest wiki improvement"
+msgstr "Sugira a melhoria do wiki"
+
+msgid "WikiEmpty|The wiki lets you write documentation for your project"
+msgstr "O wiki permite que você escreva documentação para seu projeto"
+
+msgid "WikiEmpty|This project has no wiki pages"
+msgstr "Este projeto não tem páginas wiki"
+
+msgid "WikiEmpty|You must be a project member in order to add wiki pages."
+msgstr "Você deve ser um membro do projeto para adicionar páginas wiki."
msgid "WikiHistoricalPage|This is an old version of this page."
msgstr "Essa é uma versão antiga dessa página."
@@ -4719,6 +5105,12 @@ msgstr "Nova página Wiki"
msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?"
msgstr "Quer mesmo apagar essa página?"
+msgid "WikiPageConfirmDelete|Delete page"
+msgstr "Excluir página"
+
+msgid "WikiPageConfirmDelete|Delete page %{pageTitle}?"
+msgstr "Excluir página %{pageTitle}?"
+
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."
@@ -4734,8 +5126,8 @@ 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 "WikiPage|Write your content or drag files here…"
+msgstr "Escreva seu conteúdo ou arraste arquivos aqui…"
msgid "Wiki|Create Page"
msgstr "Criar página"
@@ -4746,9 +5138,6 @@ 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"
@@ -4767,14 +5156,11 @@ 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 a análise de contribuição, você pode ter uma visão geral da atividade de issues, merge requests e eventos push de sua organização e seus membros."
-
msgid "Withdraw Access Request"
msgstr "Remover Requisição de Acesso"
-msgid "Write a commit message..."
-msgstr "Escrever uma mensagem de commit..."
+msgid "Yes"
+msgstr "Sim"
msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
msgstr "Você vai remover %{group_name}. Grupos removidos NÃO PODEM ser restaurados! Você está ABSOLUTAMENTE certo?"
@@ -4789,9 +5175,9 @@ msgid "You are going to transfer %{project_full_name} to another owner. Are you
msgstr "Você irá transferir %{project_full_name} para outro proprietário. Tem certeza ABSOLUTA?"
msgid "You are on a read-only GitLab instance."
-msgstr ""
+msgstr "Você está em uma instância somente-leitura do GitLab."
-msgid "You are on a secondary (read-only) Geo node. If you want to make any changes, you must visit the %{primary_node}."
+msgid "You can %{linkStart}view the blob%{linkEnd} instead."
msgstr ""
msgid "You can also create a project from the command line."
@@ -4800,6 +5186,9 @@ msgstr "Você também pode criar um projeto a partir da linha de comando."
msgid "You can also star a label to make it a priority label."
msgstr "Você também pode marcar uma label para torná-la uma label de prioridade."
+msgid "You can also test your .gitlab-ci.yml in the %{linkStart}Lint%{linkEnd}"
+msgstr "Você também pode testar o seu .gitlab-ci.yml no %{linkStart}Lint%{linkEnd}"
+
msgid "You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}"
msgstr "Você pode instalar facilmente um Runner em um cluster Kubernetes. %{link_to_help_page}"
@@ -4812,30 +5201,33 @@ msgstr "Você somente pode adicionar arquivos quando estiver em um branch"
msgid "You can only edit files when you are on a branch"
msgstr "Você só pode editar 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 can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}"
+msgstr "Você pode resolver o conflito de merge usando o modo Interativo, escolhendo os botões %{use_ours} ou %{use_theirs} ou editando os arquivos diretamente. Confirme essas alterações em %{branch_name}"
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 do not have the correct permissions to override the settings from the LDAP group sync."
-msgstr ""
+msgid "You do not have any assigned merge requests"
+msgstr "Você não atribuiu nenhum merge request"
msgid "You have no permissions"
msgstr "Você não tem permissão"
+msgid "You have not created any merge requests"
+msgstr "Você não criou merge request"
+
msgid "You have reached your project limit"
msgstr "Você atingiu o limite de seu projeto"
-msgid "You must have master access to force delete a lock"
-msgstr "Você deve ter o acesso master para apagar um bloqueio"
+msgid "You must accept our Terms of Service and privacy policy in order to register an account"
+msgstr "Você deve aceitar nossos Termos de Serviço e política de privacidade para registrar uma conta"
+
+msgid "You must have maintainer access to force delete a lock"
+msgstr "Você deve ter o acesso de mantenedor para apagar um bloqueio"
msgid "You must sign in to star a project"
msgstr "Você deve estar autenticado para marcar um projeto"
-msgid "You need a different license to enable FileLocks feature"
-msgstr ""
-
msgid "You need permission."
msgstr "Você precisa de permissão."
@@ -4867,25 +5259,25 @@ msgid "You'll need to use different branch names to get a valid comparison."
msgstr "Você precisará usar nomes de branch diferentes para obter uma comparação válida."
msgid "You're receiving this email because of your account on %{host}. %{manage_notifications_link} &middot; %{help_link}"
-msgstr ""
+msgstr "Você está recebendo este e-mail devido à sua conta no %{host}. %{manage_notifications_link} &middot; %{help_link}"
msgid "Your Groups"
-msgstr ""
+msgstr "Seus Grupos"
msgid "Your Kubernetes cluster information on this page is still editable, but you are advised to disable and reconfigure"
-msgstr ""
+msgstr "As informações do cluster do Kubernetes nesta página ainda são editáveis, mas é recomendado que você desative e reconfigure"
msgid "Your Projects (default)"
-msgstr ""
+msgstr "Seus projetos (padrão)"
msgid "Your Projects' Activity"
-msgstr ""
+msgstr "Atividade dos seus projetos"
msgid "Your Todos"
-msgstr ""
+msgstr "Seus lembretes"
msgid "Your changes can be committed to %{branch_name} because a merge request is open."
-msgstr ""
+msgstr "Você pode fazer commit de suas alterações para %{branch_name} porque um merge request está aberto."
msgid "Your changes have been committed. Commit %{commitId} %{commitStats}"
msgstr "Commit com suas alterações realizado. Commit %{commitId} %{commitStats}"
@@ -4902,13 +5294,11 @@ msgstr "Seu nome"
msgid "Your projects"
msgstr "Seus projetos"
-msgid "among other things"
-msgstr ""
+msgid "ago"
+msgstr "atrás"
-msgid "and %d fixed vulnerability"
-msgid_plural "and %d fixed vulnerabilities"
-msgstr[0] ""
-msgstr[1] ""
+msgid "among other things"
+msgstr "entre outras coisas"
msgid "assign yourself"
msgstr "atribuir a si mesmo"
@@ -4916,328 +5306,210 @@ msgstr "atribuir a si mesmo"
msgid "branch name"
msgstr "nome da branch"
-msgid "by"
-msgstr "por"
-
-msgid "ciReport|%{type} detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|%{type} detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Code quality"
-msgstr "Qualidade de código"
-
-msgid "ciReport|DAST detected no alerts by analyzing the review app"
-msgstr ""
-
-msgid "ciReport|Dependency scanning"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Failed to load %{reportName} report"
-msgstr ""
-
-msgid "ciReport|Fixed:"
-msgstr ""
-
-msgid "ciReport|Instances"
-msgstr ""
-
-msgid "ciReport|Learn more about whitelisting"
-msgstr ""
-
-msgid "ciReport|Loading %{reportName} report"
-msgstr ""
-
-msgid "ciReport|No changes to code quality"
-msgstr ""
-
-msgid "ciReport|No changes to performance metrics"
-msgstr ""
-
-msgid "ciReport|Performance metrics"
-msgstr ""
-
-msgid "ciReport|SAST"
-msgstr ""
-
-msgid "ciReport|SAST detected"
-msgstr ""
-
-msgid "ciReport|SAST detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|SAST detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|SAST:container no vulnerabilities were found"
-msgstr ""
-
-msgid "ciReport|Security scanning"
-msgstr ""
-
-msgid "ciReport|Security scanning failed loading any results"
-msgstr ""
-
-msgid "ciReport|Show complete code vulnerabilities report"
-msgstr ""
-
-msgid "ciReport|Unapproved vulnerabilities (red) can be marked as approved. %{helpLink}"
-msgstr ""
-
-msgid "ciReport|no vulnerabilities"
-msgstr ""
-
msgid "command line instructions"
-msgstr ""
+msgstr "instruções da linha de comando"
msgid "connecting"
-msgstr ""
-
-msgid "could not read private key, is the passphrase correct?"
-msgstr ""
+msgstr "conectando"
msgid "day"
msgid_plural "days"
msgstr[0] "dia"
msgstr[1] "dias"
-msgid "detected %d fixed vulnerability"
-msgid_plural "detected %d fixed vulnerabilities"
-msgstr[0] ""
-msgstr[1] ""
+msgid "deploy token"
+msgstr "token de deploy"
-msgid "detected %d new vulnerability"
-msgid_plural "detected %d new vulnerabilities"
-msgstr[0] ""
-msgstr[1] ""
+msgid "disabled"
+msgstr "desabilitado"
-msgid "detected no vulnerabilities"
-msgstr ""
+msgid "enabled"
+msgstr "habilitado"
msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
-msgstr ""
+msgstr "%{slash_command} irá atualizar o tempo estimado com o último comando."
-msgid "here"
-msgstr ""
+msgid "for this project"
+msgstr "para este projeto"
msgid "importing"
-msgstr ""
-
-msgid "in progress"
-msgstr ""
-
-msgid "is invalid because there is downstream lock"
-msgstr ""
-
-msgid "is invalid because there is upstream lock"
-msgstr ""
-
-msgid "is not a valid X509 certificate."
-msgstr ""
+msgstr "importando"
-msgid "locked by %{path_lock_user_name} %{created_at}"
+msgid "latest version"
msgstr ""
msgid "merge request"
msgid_plural "merge requests"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "merge request"
+msgstr[1] "merge requests"
msgid "mrWidget| Please restore it or use a different %{missingBranchName} branch"
-msgstr ""
+msgstr "Por favor, restaurar ou usar um branch %{missingBranchName} diferente"
msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} decreased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB"
-msgstr ""
+msgstr "%{metricsLinkStart} Memória %{metricsLinkEnd} uso %{emphasisStart} diminuição %{emphasisEnd} de %{memoryFrom}MB para %{memoryTo}MB"
msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} increased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB"
-msgstr ""
+msgstr "%{metricsLinkStart} Memória %{metricsLinkEnd} uso %{emphasisStart} aumento %{emphasisEnd} de %{memoryFrom}MB para %{memoryTo}MB"
msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage is %{emphasisStart} unchanged %{emphasisEnd} at %{memoryFrom}MB"
-msgstr ""
+msgstr "%{metricsLinkStart} Memória %{metricsLinkEnd} uso é %{emphasisStart} inalterado %{emphasisEnd} em %{memoryFrom}MB"
-msgid "mrWidget|Add approval"
-msgstr ""
-
-msgid "mrWidget|Allows edits from maintainers"
-msgstr ""
-
-msgid "mrWidget|An error occured while removing your approval."
-msgstr ""
-
-msgid "mrWidget|An error occured while retrieving approval data for this merge request."
-msgstr ""
-
-msgid "mrWidget|An error occured while submitting your approval."
-msgstr ""
-
-msgid "mrWidget|Approve"
-msgstr ""
-
-msgid "mrWidget|Approved"
-msgstr ""
-
-msgid "mrWidget|Approved by"
-msgstr ""
+msgid "mrWidget|Allows commits from members who can merge to the target branch"
+msgstr "Permite commits de membros que podem fazer merge ao branch de destino"
msgid "mrWidget|Cancel automatic merge"
-msgstr ""
+msgstr "Cancelar merge request automático"
msgid "mrWidget|Check out branch"
-msgstr ""
+msgstr "Fazer checkout de branch"
msgid "mrWidget|Checking ability to merge automatically"
-msgstr ""
+msgstr "Verificando a capacidade de merge automaticamente"
msgid "mrWidget|Cherry-pick"
-msgstr ""
+msgstr "Cherry-pick"
msgid "mrWidget|Cherry-pick this merge request in a new merge request"
-msgstr ""
+msgstr "Fazer cherry-pick desse merge request em um novo merge request"
msgid "mrWidget|Closed"
-msgstr ""
+msgstr "Fechado"
msgid "mrWidget|Closed by"
-msgstr ""
+msgstr "Fechado por"
msgid "mrWidget|Closes"
msgstr ""
+msgid "mrWidget|Create an issue to resolve them later"
+msgstr "Criar uma issue para resolvê-los mais tarde"
+
msgid "mrWidget|Deployment statistics are not available currently"
-msgstr ""
+msgstr "Estatísticas de deploy não estão disponíveis atualmente"
msgid "mrWidget|Did not close"
-msgstr ""
+msgstr "mrWidget|Não foi possível fechar"
msgid "mrWidget|Email patches"
-msgstr ""
+msgstr "Email patches"
msgid "mrWidget|Failed to load deployment statistics"
-msgstr ""
+msgstr "Falha ao carregar estatísticas de deploy"
msgid "mrWidget|If the %{branch} branch exists in your local repository, you can merge this merge request manually using the"
-msgstr ""
+msgstr "Se o branch %{branch} existir em seu repositório local, você poderá fazer o merge request manualmente usando o"
msgid "mrWidget|If the %{missingBranchName} branch exists in your local repository, you can merge this merge request manually using the command line"
-msgstr ""
+msgstr "Se o branch %{missingBranchName} existir em seu repositório local, você poderá fazer merge request manualmente usando a linha de comando"
msgid "mrWidget|Loading deployment statistics"
-msgstr ""
+msgstr "Carregando estatísticas de implantação"
msgid "mrWidget|Mentions"
-msgstr ""
+msgstr "Menções"
msgid "mrWidget|Merge"
-msgstr ""
+msgstr "Fazer merge"
msgid "mrWidget|Merge failed."
-msgstr ""
+msgstr "Falha ao fazer merge."
msgid "mrWidget|Merge locally"
-msgstr ""
+msgstr "Fazer merge localmente"
msgid "mrWidget|Merged by"
-msgstr ""
+msgstr "Merge realizado por"
msgid "mrWidget|Plain diff"
-msgstr ""
+msgstr "Diff em texto"
msgid "mrWidget|Refresh"
-msgstr ""
+msgstr "Atualizar"
msgid "mrWidget|Refresh now"
-msgstr ""
+msgstr "Atualizar agora"
msgid "mrWidget|Refreshing now"
-msgstr ""
+msgstr "Atualizando agora"
msgid "mrWidget|Remove Source Branch"
-msgstr ""
+msgstr "Remover branch de origem"
msgid "mrWidget|Remove source branch"
-msgstr ""
-
-msgid "mrWidget|Remove your approval"
-msgstr ""
+msgstr "Remover branch de origem"
msgid "mrWidget|Request to merge"
-msgstr ""
+msgstr "Solicitar merge"
msgid "mrWidget|Resolve conflicts"
-msgstr ""
+msgstr "Resolver conflitos"
msgid "mrWidget|Revert"
-msgstr ""
+msgstr "Reverter"
msgid "mrWidget|Revert this merge request in a new merge request"
-msgstr ""
+msgstr "Reverter esse merge request com um novo merge request"
msgid "mrWidget|Set by"
-msgstr ""
+msgstr "Definir por"
msgid "mrWidget|The changes were merged into"
-msgstr ""
+msgstr "Houve merge das alterações em"
msgid "mrWidget|The changes were not merged into"
-msgstr ""
+msgstr "Não houve merge para as mudanças em"
msgid "mrWidget|The changes will be merged into"
-msgstr ""
+msgstr "Será feito merge das alterações em"
msgid "mrWidget|The source branch has been removed"
-msgstr ""
+msgstr "O branch de origem foi removido"
msgid "mrWidget|The source branch is being removed"
-msgstr ""
+msgstr "O branch de origem está sendo removido"
msgid "mrWidget|The source branch will be removed"
-msgstr ""
+msgstr "O branch de origem será removido"
msgid "mrWidget|The source branch will not be removed"
-msgstr ""
+msgstr "O branch de origem não será removido"
msgid "mrWidget|There are merge conflicts"
-msgstr ""
+msgstr "Existem conflitos de merge"
+
+msgid "mrWidget|There are unresolved discussions. Please resolve these discussions"
+msgstr "Há discussões não resolvidas. Por favor, resolva estas discussões"
msgid "mrWidget|This merge request failed to be merged automatically"
-msgstr ""
+msgstr "Falha ao realizar merge automaticamente"
msgid "mrWidget|This merge request is in the process of being merged"
-msgstr ""
+msgstr "Esse merge request está em processamento"
msgid "mrWidget|This project is archived, write access has been disabled"
-msgstr ""
+msgstr "Este projeto está arquivado, a escrita foi desativada"
msgid "mrWidget|Web IDE"
-msgstr ""
+msgstr "IDE Web"
msgid "mrWidget|You can merge this merge request manually using the"
-msgstr ""
+msgstr "Você pode fazer merge manualmente usando o"
msgid "mrWidget|You can remove source branch now"
-msgstr ""
+msgstr "Agora você pode remover o branch de origem"
msgid "mrWidget|branch does not exist."
-msgstr ""
+msgstr "branch não existe."
msgid "mrWidget|command line"
-msgstr ""
+msgstr "linha de comando"
msgid "mrWidget|into"
-msgstr ""
+msgstr "dentro"
msgid "mrWidget|to be merged automatically when the pipeline succeeds"
-msgstr ""
+msgstr "para ser realizado merge automaticamente quando o pipeline for bem sucedido"
msgid "new merge request"
msgstr "novo merge request"
@@ -5259,8 +5531,8 @@ msgstr "senha"
msgid "personal access token"
msgstr "token de acesso pessoal"
-msgid "private key does not match certificate."
-msgstr ""
+msgid "remaining"
+msgstr "restante"
msgid "remove due date"
msgstr "remover a data de vencimento"
@@ -5272,17 +5544,19 @@ msgid "spendCommand|%{slash_command} will update the sum of the time spent."
msgstr "%{slash_command} irá atualizar a soma do tempo gasto."
msgid "this document"
-msgstr ""
-
-msgid "to help your contributors communicate effectively!"
-msgstr "para ajudar seus contribuintes à se comunicar de maneira eficaz!"
+msgstr "este documento"
msgid "username"
msgstr "nome do usuário"
msgid "uses Kubernetes clusters to deploy your code!"
-msgstr ""
+msgstr "use clusters Kubernetes para deploy do seu código!"
msgid "with %{additions} additions, %{deletions} deletions."
-msgstr ""
+msgstr "com %{additions} adições, %{deletions} remoções."
+
+msgid "within %d minute "
+msgid_plural "within %d minutes "
+msgstr[0] "dentro de %d minuto "
+msgstr[1] "dentro de %d minutos "
diff --git a/locale/ru/gitlab.po b/locale/ru/gitlab.po
index a5e145a06ed..d2c990ec220 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: 2018-04-04 19:35+0200\n"
-"PO-Revision-Date: 2018-04-05 03:35-0400\n"
+"POT-Creation-Date: 2018-07-01 16:35+1000\n"
+"PO-Revision-Date: 2018-07-01 11:02\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Russian\n"
"Language: ru_RU\n"
@@ -16,8 +16,12 @@ msgstr ""
"X-Crowdin-Language: ru\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-msgid " and"
-msgstr " и"
+msgid "%d changed file"
+msgid_plural "%d changed files"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
msgid "%d commit"
msgid_plural "%d commits"
@@ -35,10 +39,10 @@ msgstr[3] "на %d коммитов позади"
msgid "%d exporter"
msgid_plural "%d exporters"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
+msgstr[0] "%d ÑкÑпортер"
+msgstr[1] "%d ÑкÑпортера"
+msgstr[2] "%d ÑкÑпортеров"
+msgstr[3] "%d ÑкÑпортеров"
msgid "%d issue"
msgid_plural "%d issues"
@@ -63,10 +67,24 @@ msgstr[3] "%d запроÑов на ÑлиÑние"
msgid "%d metric"
msgid_plural "%d metrics"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
+msgstr[0] "%d метрика"
+msgstr[1] "%d метрики"
+msgstr[2] "%d метрик"
+msgstr[3] "%d метрик"
+
+msgid "%d staged change"
+msgid_plural "%d staged changes"
+msgstr[0] "%d запиÑанное изменение"
+msgstr[1] "%d запиÑанных изменений"
+msgstr[2] "%d запиÑанных изменений"
+msgstr[3] "%d запиÑанных изменений"
+
+msgid "%d unstaged change"
+msgid_plural "%d unstaged changes"
+msgstr[0] "%d не запиÑанное изменение"
+msgstr[1] "%d не запиÑанных изменений"
+msgstr[2] "%d не запиÑанных изменений"
+msgstr[3] "%d не запиÑанных изменений"
msgid "%s additional commit has been omitted to prevent performance issues."
msgid_plural "%s additional commits have been omitted to prevent performance issues."
@@ -76,10 +94,10 @@ msgstr[2] "%s дополнительных коммитов было пропуÑ
msgstr[3] "%s дополнительных коммитов было пропущено Ð´Ð»Ñ Ð¿Ñ€ÐµÐ´Ð¾Ñ‚Ð²Ñ€Ð°Ñ‰ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼ Ñ Ð¿Ñ€Ð¾Ð¸Ð·Ð²Ð¾Ð´Ð¸Ñ‚ÐµÐ»ÑŒÐ½Ð¾Ñтью."
msgid "%{actionText} & %{openOrClose} %{noteable}"
-msgstr ""
+msgstr "%{actionText} & %{openOrClose} %{noteable}"
msgid "%{commit_author_link} authored %{commit_timeago}"
-msgstr ""
+msgstr "%{commit_author_link} Ñоздан %{commit_timeago}"
msgid "%{count} participant"
msgid_plural "%{count} participants"
@@ -88,12 +106,21 @@ msgstr[1] "%{count} учаÑтника"
msgstr[2] "%{count} учаÑтников"
msgstr[3] "%{count} учаÑтников"
-msgid "%{loadingIcon} Started"
+msgid "%{filePath} deleted"
msgstr ""
+msgid "%{group_docs_link_start}Groups%{group_docs_link_end} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects."
+msgstr ""
+
+msgid "%{loadingIcon} Started"
+msgstr "%{loadingIcon} Запущено"
+
msgid "%{lock_path} is locked by GitLab User %{lock_user_id}"
msgstr "%{lock_path} заблокирован пользователем GitLab %{lock_user_id}"
+msgid "%{nip_domain} can be used as an alternative to a custom domain."
+msgstr "%{nip_domain} может иÑпользоватьÑÑ ÐºÐ°Ðº альтернатива пользовательÑкому домену."
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr "на %{number_commits_behind} коммитов позади %{default_branch}, на %{number_commits_ahead} коммитов впереди"
@@ -104,7 +131,10 @@ msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will not re
msgstr "%{number_of_failures} из %{maximum_failures} возможных неудачных попыток. GitLab не будет автоматичеÑки повторÑть попытку. СброÑьте информацию хранилища поÑле уÑÑ‚Ñ€Ð°Ð½ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼Ñ‹."
msgid "%{openOrClose} %{noteable}"
-msgstr ""
+msgstr "%{openOrClose} %{noteable}"
+
+msgid "%{percent}%% complete"
+msgstr "%{percent}%% выполнено"
msgid "%{storage_name}: failed storage access attempt on host:"
msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:"
@@ -116,15 +146,76 @@ msgstr[3] "%{storage_name}: %{failed_attempts} неудачных попыток
msgid "%{text} is available"
msgstr "%{text} доÑтупен"
+msgid "%{title} changes"
+msgstr "Ð˜Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð² %{title}"
+
+msgid "%{unstaged} unstaged and %{staged} staged changes"
+msgstr "%{unstaged} не зафикÑированных и %{staged} зафикÑированных изменений"
+
msgid "(checkout the %{link} for information on how to install it)."
msgstr "(перейдите по ÑÑылке %{link} Ð´Ð»Ñ Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ð¸ об уÑтановке)."
msgid "+ %{moreCount} more"
msgstr "+ ещё %{moreCount}"
+msgid "- Runner is active and can process any new jobs"
+msgstr "- Runner активен и может обрабатывать любые новые заданиÑ"
+
+msgid "- Runner is paused and will not receive any new jobs"
+msgstr "- Runner приоÑтановлен и не получит новые заданиÑ"
+
msgid "- show less"
msgstr "- Ñвернуть"
+msgid "1 %{type} addition"
+msgid_plural "%{count} %{type} additions"
+msgstr[0] "1 дополнение типа %{type}"
+msgstr[1] "%{count} дополнений типа %{type}"
+msgstr[2] "%{count} дополнений типа %{type}"
+msgstr[3] "%{count} дополнений типа %{type}"
+
+msgid "1 %{type} modification"
+msgid_plural "%{count} %{type} modifications"
+msgstr[0] "1 Ð¼Ð¾Ð´Ð¸Ñ„Ð¸ÐºÐ°Ñ†Ð¸Ñ Ñ‚Ð¸Ð¿Ð° %{type}"
+msgstr[1] "%{count} модификаций типа %{type}"
+msgstr[2] "%{count} модификаций типа %{type}"
+msgstr[3] "%{count} модификаций типа %{type}"
+
+msgid "1 closed issue"
+msgid_plural "%d closed issues"
+msgstr[0] "1 закрытое обÑуждение"
+msgstr[1] "%d закрытых обÑуждений"
+msgstr[2] "%d закрытых обÑуждений"
+msgstr[3] "%d закрытых обÑуждений"
+
+msgid "1 closed merge request"
+msgid_plural "%d closed merge requests"
+msgstr[0] "1 закрытый Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние"
+msgstr[1] "%d закрытых запроÑов на ÑлиÑние"
+msgstr[2] "%d закрытых запроÑов на ÑлиÑние"
+msgstr[3] "%d закрытых запроÑов на ÑлиÑние"
+
+msgid "1 merged merge request"
+msgid_plural "%d merged merge requests"
+msgstr[0] "1 объединенный Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ðµ ÑлиÑние"
+msgstr[1] "%d объединенных запроÑов не ÑлиÑние"
+msgstr[2] "%d объединенных запроÑов не ÑлиÑние"
+msgstr[3] "%d объединенных запроÑов не ÑлиÑние"
+
+msgid "1 open issue"
+msgid_plural "%d open issues"
+msgstr[0] "1 открытое обÑуждение"
+msgstr[1] "%d открытых обÑуждений"
+msgstr[2] "%d открытых обÑуждений"
+msgstr[3] "%d открытых обÑуждений"
+
+msgid "1 open merge request"
+msgid_plural "%d open merge requests"
+msgstr[0] "1 открытый Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние"
+msgstr[1] "%d открытых запроÑов на ÑлиÑние"
+msgstr[2] "%d открытых запроÑов на ÑлиÑние"
+msgstr[3] "%d открытых запроÑов на ÑлиÑние"
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] "1 ÑÐ±Ð¾Ñ€Ð¾Ñ‡Ð½Ð°Ñ Ð»Ð¸Ð½Ð¸Ñ"
@@ -138,9 +229,27 @@ msgstr "Первый вклад!"
msgid "2FA enabled"
msgstr "Ð”Ð²ÑƒÑ…Ñ„Ð°ÐºÑ‚Ð¾Ñ€Ð½Ð°Ñ Ð°Ð²Ñ‚Ð¾Ñ€Ð¸Ð·Ð°Ñ†Ð¸Ñ Ð²ÐºÐ»ÑŽÑ‡ÐµÐ½Ð°"
-msgid "<strong>Removes</strong> source branch"
+msgid "403|Please contact your GitLab administrator to get the permission."
+msgstr "ПожалуйÑта ÑвÑжитеÑÑŒ Ñ Ð²Ð°ÑˆÐ¸Ð¼ админиÑтратором GitLab Ñервера чтобы получить разрешение к данному реÑурÑу."
+
+msgid "403|You don't have the permission to access this page."
+msgstr "У Ð’Ð°Ñ Ð½ÐµÑ‚ права доÑтупа к Ñтой Ñтранице."
+
+msgid "404|Make sure the address is correct and the page hasn't moved."
+msgstr "ПожалуйÑта убедитеÑÑŒ что Ð°Ð´Ñ€ÐµÑ Ñтраницы верный и то что Ñтраница не была перемещена."
+
+msgid "404|Page Not Found"
+msgstr "Страница Ðе Ðайдена"
+
+msgid "404|Please contact your GitLab administrator if you think this is a mistake."
msgstr ""
+msgid "<strong>Removes</strong> source branch"
+msgstr "<strong>УдалÑет</strong> иÑходную ветку"
+
+msgid "A 'Runner' is a process which runs a job. You can setup as many Runners as you need."
+msgstr "«Runner» - Ñто процеÑÑ, который выполнÑет задание (обработчиков заданий). Ð’Ñ‹ можете наÑтроить Ñтолько таких процеÑÑов, Ñколько вам нужно."
+
msgid "A collection of graphs regarding Continuous Integration"
msgstr "Графики непрерывной интеграции (CI)"
@@ -148,10 +257,13 @@ msgid "A new branch will be created in your fork and a new merge request will be
msgstr ""
msgid "A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}."
+msgstr "Ð’ проекте вы размещаете Ñвои файлы (репозиторий), планируете Ñвою работу (обÑуждениÑ), и публикуете документацию (wiki), %{among_other_things_link}."
+
+msgid "A regular expression that will be used to find the test coverage output in the job trace. Leave blank to disable"
msgstr ""
msgid "A user with write access to the source branch selected this option"
-msgstr ""
+msgstr "Пользователь Ñ Ñ€Ð°Ð·Ñ€ÐµÑˆÐµÐ½Ð¸ÐµÐ¼ на запиÑÑŒ в ветку иÑточника выбрал Ñтот вариант"
msgid "About auto deploy"
msgstr "Об автоматичеÑком развёртывании"
@@ -162,36 +274,39 @@ msgstr "Отчёты о Жалобах"
msgid "Abuse reports"
msgstr ""
+msgid "Accept terms"
+msgstr "ПринÑть уÑловиÑ"
+
msgid "Access Tokens"
msgstr "Токены ДоÑтупа"
msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again."
msgstr "ДоÑтуп к вышедшим из ÑÑ‚Ñ€Ð¾Ñ Ñ…Ñ€Ð°Ð½Ð¸Ð»Ð¸Ñ‰Ð°Ð¼ временно отключен Ð´Ð»Ñ Ð²Ð¾Ð·Ð¼Ð¾Ð¶Ð½Ð¾Ñти Ð¼Ð¾Ð½Ñ‚Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð¸Ñ Ð² целÑÑ… воÑÑтановлениÑ. СброÑьте информацию о хранилищах поÑле уÑÑ‚Ñ€Ð°Ð½ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼Ñ‹, чтобы разрешить доÑтуп."
+msgid "Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report."
+msgstr ""
+
msgid "Account"
msgstr "Ð£Ñ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ"
-msgid "Account and limit settings"
+msgid "Account and limit"
msgstr ""
msgid "Active"
msgstr "Ðктивный"
+msgid "Active Sessions"
+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 "Добавить групповые веб-обработчики и GitLab Enterprise Edition."
-
msgid "Add Kubernetes cluster"
msgstr "Добавить Kubernetes клаÑтер"
@@ -204,8 +319,11 @@ msgstr "Добавить Информацию"
msgid "Add new directory"
msgstr "Добавить новый каталог"
+msgid "Add reaction"
+msgstr ""
+
msgid "Add todo"
-msgstr "Добавить todo"
+msgstr "Добавить в дела"
msgid "AdminArea|Stop all jobs"
msgstr "ОÑтановить вÑе заданиÑ"
@@ -259,7 +377,7 @@ msgid "AdminUsers|To confirm, type %{username}"
msgstr "Ð”Ð»Ñ Ð¿Ð¾Ð´Ñ‚Ð²ÐµÑ€Ð¶Ð´ÐµÐ½Ð¸Ñ, введите %{username}"
msgid "Advanced"
-msgstr "Дополнительно"
+msgstr "РаÑширенный режим"
msgid "Advanced settings"
msgstr "РаÑширенные наÑтройки"
@@ -271,13 +389,16 @@ msgid "All changes are committed"
msgstr "Ð’Ñе Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð·Ð°Ñ„Ð¸ÐºÑированы"
msgid "All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings."
+msgstr "Ð’Ñе функции ÑиÑтемы включаютÑÑ Ð´Ð»Ñ Ð¿ÑƒÑтых проектов, Ñозданных из шаблонов или импортированных, но вы можете отключить их впоÑледÑтвии в наÑтройках проекта."
+
+msgid "Allow commits from members who can merge to the target branch."
msgstr ""
-msgid "Allow edits from maintainers."
+msgid "Allow public access to pipelines and job details, including output logs and artifacts"
msgstr ""
msgid "Allow rendering of PlantUML diagrams in Asciidoc documents."
-msgstr ""
+msgstr "Разрешить рендеринг диаграмм PlantUML в документах Asciidoc."
msgid "Allow requests to the local network from hooks and services."
msgstr ""
@@ -285,16 +406,28 @@ msgstr ""
msgid "Allows you to add and manage Kubernetes clusters."
msgstr "ПозволÑет добавлÑть и управлÑть клаÑтерами Kubernetes."
-msgid "Also called \"Issuer\" or \"Relying party trust identifier\""
+msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
msgstr ""
-msgid "Also called \"Relying party service URL\" or \"Reply URL\""
+msgid "An error occured creating the new branch."
msgstr ""
-msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to connect."
+msgid "An error occured whilst loading all the files."
msgstr ""
-msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
+msgid "An error occured whilst loading the file content."
+msgstr ""
+
+msgid "An error occured whilst loading the file."
+msgstr ""
+
+msgid "An error occured whilst loading the merge request changes."
+msgstr ""
+
+msgid "An error occured whilst loading the merge request version data."
+msgstr ""
+
+msgid "An error occured whilst loading the merge request."
msgstr ""
msgid "An error occurred previewing the blob"
@@ -303,14 +436,8 @@ msgstr "Произошла ошибка при предварительном п
msgid "An error occurred when toggling the notification subscription"
msgstr "Произошла ошибка при переключении подпиÑки на оповещениÑ"
-msgid "An error occurred when updating the issue weight"
-msgstr "Произошла ошибка при обновлении веÑа обÑуждениÑ"
-
-msgid "An error occurred while adding approver"
-msgstr "Произошла ошибка при добавлении утверждающего"
-
-msgid "An error occurred while detecting host keys"
-msgstr "Произошла ошибка при обнаружении ключей хоÑта"
+msgid "An error occurred while dismissing the alert. Refresh the page and try again."
+msgstr "При отключении Ð¿Ñ€ÐµÐ´ÑƒÐ¿Ñ€ÐµÐ¶Ð´ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¾Ð¸Ð·Ð¾ÑˆÐ»Ð° ошибка. Обновите Ñтраницу и повторите попытку."
msgid "An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again."
msgstr "Произошла ошибка при отключении ÑƒÐ²ÐµÐ´Ð¾Ð¼Ð»ÐµÐ½Ð¸Ñ Ð¾ функции. Обновите Ñтраницу и повторите попытку."
@@ -327,11 +454,8 @@ msgstr "Произошла ошибка при получении Ñборочн
msgid "An error occurred while getting projects"
msgstr "Произошла ошибка при получении ÑпиÑка проектов"
-msgid "An error occurred while importing project"
-msgstr "Произошла ошибка при импорте проекта"
-
-msgid "An error occurred while initializing path locks"
-msgstr "Произошла ошибка при инициализации блокировок пути"
+msgid "An error occurred while importing project: ${details}"
+msgstr ""
msgid "An error occurred while loading commits"
msgstr "Произошла ошибка при загрузке коммитов"
@@ -348,9 +472,6 @@ msgstr "Произошла ошибка при загрузке файла"
msgid "An error occurred while making the request."
msgstr "Произошла ошибка при выполнении запроÑа."
-msgid "An error occurred while removing approver"
-msgstr "Произошла ошибка при удалении утверждающего"
-
msgid "An error occurred while rendering KaTeX"
msgstr "Произошла ошибка при рендеринге KaTeX"
@@ -363,11 +484,8 @@ msgstr "Произошла ошибка при получении календа
msgid "An error occurred while retrieving diff"
msgstr "Произошла ошибка при получении различий"
-msgid "An error occurred while saving LDAP override status. Please try again."
-msgstr "Произошла ошибка при Ñохранении ÑтатуÑа наÑтроенного LDAP. ПожалуйÑта, попробуйте еще раз."
-
msgid "An error occurred while saving assignees"
-msgstr "Произошла ошибка при Ñохранении иÑполнителÑ"
+msgstr "Произошла ошибка при Ñохранении ответÑтвенных"
msgid "An error occurred while validating username"
msgstr "Произошла ошибка при проверке имени пользователÑ"
@@ -375,9 +493,6 @@ msgstr "Произошла ошибка при проверке имени поÐ
msgid "An error occurred. Please try again."
msgstr "Произошла ошибка. ПожалуйÑта, попробуйте Ñнова."
-msgid "Any Label"
-msgstr "Ð›ÑŽÐ±Ð°Ñ ÐœÐµÑ‚ÐºÐ°"
-
msgid "Appearance"
msgstr "Оформление"
@@ -390,28 +505,28 @@ msgstr "Ðпр."
msgid "April"
msgstr "Ðпрель"
-msgid "Archived project! Repository is read-only"
-msgstr "Ðрхивный проект! Репозиторий доÑтупен только Ð´Ð»Ñ Ñ‡Ñ‚ÐµÐ½Ð¸Ñ"
+msgid "Archived project! Repository and other project resources are read-only"
+msgstr "Ðрхивный проект! Репозиторий и другие реÑурÑÑ‹ проекта доÑтупны только Ð´Ð»Ñ Ñ‡Ñ‚ÐµÐ½Ð¸Ñ"
msgid "Are you sure you want to delete this pipeline schedule?"
msgstr "Ð’Ñ‹ дейÑтвительно хотите удалить Ñто раÑпиÑание Ñборочной линии?"
+msgid "Are you sure you want to remove this identity?"
+msgstr ""
+
msgid "Are you sure you want to reset registration token?"
msgstr "Ð’Ñ‹ уверены, что хотите ÑброÑить Ñтот региÑтрационный токен?"
msgid "Are you sure you want to reset the health check token?"
msgstr "Ð’Ñ‹ уверены, что хотите ÑброÑить Ñтот токен проверки работоÑпоÑобноÑти?"
-msgid "Are you sure you want to unlock %{path_lock_path}?"
-msgstr "Ð’Ñ‹ дейÑтвительно хотите разблокировать %{path_lock_path}?"
-
msgid "Are you sure?"
msgstr "Вы уверены?"
msgid "Artifacts"
msgstr "Ðртефакты"
-msgid "Assertion consumer service URL"
+msgid "Ask your group maintainer to setup a group Runner."
msgstr ""
msgid "Assign custom color like #FF0000"
@@ -427,16 +542,22 @@ msgid "Assign to"
msgstr "Ðазначить"
msgid "Assigned Issues"
-msgstr ""
+msgstr "Ðазначенные обÑуждениÑ"
msgid "Assigned Merge Requests"
-msgstr ""
+msgstr "Ðазначенные запроÑÑ‹ на ÑлиÑние"
msgid "Assigned to :name"
+msgstr "Ðазначено на :name"
+
+msgid "Assigned to me"
msgstr ""
msgid "Assignee"
-msgstr ""
+msgstr "ОтветÑтвенный"
+
+msgid "Assignee(s)"
+msgstr "ОтветÑтвенный(ные)"
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "Приложить файл через drag &amp; drop или %{upload_link}"
@@ -460,7 +581,7 @@ msgid "Auto DevOps enabled"
msgstr "Auto DevOps включен"
msgid "Auto DevOps, runners and job artifacts"
-msgstr ""
+msgstr "ÐвтоматичеÑкий DevOps, обработчики и артефакты заданий"
msgid "Auto Review Apps and Auto Deploy need a %{kubernetes} to work correctly."
msgstr "ÐŸÑ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð´Ð»Ñ Ð°Ð²Ñ‚Ð¾Ð¼Ð°Ñ‚Ð¸Ñ‡ÐµÑкого ревью и автоматичеÑкого Ñ€Ð°Ð·Ð²Ñ‘Ñ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ñ‚Ñ€ÐµÐ±ÑƒÑŽÑ‚ ÑƒÐºÐ°Ð·Ð°Ð½Ð¸Ñ %{kubernetes} Ð´Ð»Ñ ÐºÐ¾Ñ€Ñ€ÐµÐºÑ‚Ð½Ð¾Ð¹ работы."
@@ -471,8 +592,11 @@ msgstr "ÐŸÑ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð´Ð»Ñ Ð°Ð²Ñ‚Ð¾Ð¼Ð°Ñ‚Ð¸Ñ‡ÐµÑкого ревью и
msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
msgstr "ÐŸÑ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð´Ð»Ñ Ð°Ð²Ñ‚Ð¾Ð¼Ð°Ñ‚Ð¸Ñ‡ÐµÑкого ревью и автоматичеÑкого Ñ€Ð°Ð·Ð²Ñ‘Ñ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ñ‚Ñ€ÐµÐ±ÑƒÑŽÑ‚ ÑƒÐºÐ°Ð·Ð°Ð½Ð¸Ñ Ð¸Ð¼ÐµÐ½Ð¸ домена Ð´Ð»Ñ ÐºÐ¾Ñ€Ñ€ÐµÐºÑ‚Ð½Ð¾Ð¹ работы."
-msgid "AutoDevOps|Auto DevOps (Beta)"
-msgstr "Auto DevOps (бета)"
+msgid "Auto-cancel redundant, pending pipelines"
+msgstr ""
+
+msgid "AutoDevOps|Auto DevOps"
+msgstr "ÐвтоматичеÑкий DevOps"
msgid "AutoDevOps|Auto DevOps documentation"
msgstr "Ð”Ð¾ÐºÑƒÐ¼ÐµÐ½Ñ‚Ð°Ñ†Ð¸Ñ Ð¿Ð¾ Auto DevOps"
@@ -492,80 +616,110 @@ msgstr "Ð’Ñ‹ можете автоматичеÑки Ñобирать и теÑÑ
msgid "AutoDevOps|add a Kubernetes cluster"
msgstr "добавите клаÑтер Kubernetes"
-msgid "AutoDevOps|enable Auto DevOps (Beta)"
-msgstr "включен Auto DevOps (Beta)"
+msgid "AutoDevOps|enable Auto DevOps"
+msgstr " включить ÐвтоматичеÑкий DevOps"
msgid "Available"
msgstr "ДоÑтупен"
+msgid "Available group Runners : %{runners}"
+msgstr "ДоÑтупные обработчики заданий Ð´Ð»Ñ Ð³Ñ€ÑƒÐ¿Ð¿Ñ‹: %{runners}"
+
+msgid "Available group Runners : %{runners}."
+msgstr "ДоÑтупные обработчики заданий Ð´Ð»Ñ Ð³Ñ€ÑƒÐ¿Ð¿Ñ‹: %{runners}."
+
msgid "Avatar will be removed. Are you sure?"
msgstr "Ðватар будет удален. Ð’Ñ‹ уверены?"
msgid "Average per day: %{average}"
msgstr "Ð’ Ñреднем за день: %{average}"
-msgid "Background Color"
+msgid "Background color"
msgstr ""
msgid "Background jobs"
-msgstr ""
+msgstr "Фоновые заданиÑ"
-msgid "Begin with the selected commit"
-msgstr "Ðачать Ñ Ð²Ñ‹Ð±Ñ€Ð°Ð½Ð½Ð¾Ð³Ð¾ коммита"
+msgid "Badges"
+msgstr "Значки"
-msgid "Billing"
-msgstr "Тариф"
+msgid "Badges|A new badge was added."
+msgstr "Ðовый значок был добавлен."
-msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan."
-msgstr "%{group_name} иÑпользует тарифный план %{plan_link}."
+msgid "Badges|Add badge"
+msgstr "Добавить значок"
-msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available."
-msgstr "Ð’ наÑтоÑщее Ð²Ñ€ÐµÐ¼Ñ Ð°Ð²Ñ‚Ð¾Ð¼Ð°Ñ‚Ð¸Ñ‡ÐµÑкое повышение или понижение недоÑтупно Ð´Ð»Ñ Ð½ÐµÐºÐ¾Ñ‚Ð¾Ñ€Ñ‹Ñ… тарифных планов."
+msgid "Badges|Adding the badge failed, please check the entered URLs and try again."
+msgstr "Добавление значка не удалоÑÑŒ, пожалуйÑта, проверьте введенный URL и попробуйте еще раз."
-msgid "BillingPlans|Current plan"
-msgstr "Текущий тарифный план"
+msgid "Badges|Badge image URL"
+msgstr "URL-Ð°Ð´Ñ€ÐµÑ Ð¸Ð·Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð¸Ñ Ð·Ð½Ð°Ñ‡ÐºÐ°"
-msgid "BillingPlans|Customer Support"
-msgstr "Поддержка Клиентов"
+msgid "Badges|Badge image preview"
+msgstr "Предварительный проÑмотра Ð¸Ð·Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð¸Ñ Ð·Ð½Ð°Ñ‡ÐºÐ°"
-msgid "BillingPlans|Downgrade"
-msgstr "Понизить"
+msgid "Badges|Delete badge"
+msgstr "Удалить значок"
-msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
-msgstr "Узнайте больше о каждом тарифном плане, прочитав нашу Ñтраницу %{faq_link}."
+msgid "Badges|Delete badge?"
+msgstr "Удалить значок?"
-msgid "BillingPlans|Manage plan"
-msgstr "Управление тарифным планом"
+msgid "Badges|Deleting the badge failed, please try again."
+msgstr "Удаление значка не удалоÑÑŒ, повторите попытку."
-msgid "BillingPlans|Please contact %{customer_support_link} in that case."
-msgstr "ПожалуйÑта, в Ñтом Ñлучае обратитеÑÑŒ в %{customer_support_link}."
+msgid "Badges|Group Badge"
+msgstr "Значок Группы"
-msgid "BillingPlans|See all %{plan_name} features"
-msgstr "ПоÑмотреть возможноÑти %{plan_name} полноÑтью"
+msgid "Badges|Link"
+msgstr "СÑылка"
-msgid "BillingPlans|This group uses the plan associated with its parent group."
-msgstr "Эта группа иÑпользует тарифный план, ÑвÑзанный Ñ Ñ€Ð¾Ð´Ð¸Ñ‚ÐµÐ»ÑŒÑкой группой."
+msgid "Badges|No badge image"
+msgstr "Ðет Ð¸Ð·Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð¸Ñ Ð·Ð½Ð°Ñ‡ÐºÐ°"
-msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
-msgstr "Ð”Ð»Ñ ÑƒÐ¿Ñ€Ð°Ð²Ð»ÐµÐ½Ð¸Ñ Ñ‚Ð°Ñ€Ð¸Ñ„Ð½Ñ‹Ð¼ планом Ñтой группы поÑетите раздел тарификации %{parent_billing_page_link}."
+msgid "Badges|No image to preview"
+msgstr "Ðет Ð¸Ð·Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð¸Ñ Ð´Ð»Ñ Ð¿Ñ€ÐµÐ´Ð²Ð°Ñ€Ð¸Ñ‚ÐµÐ»ÑŒÐ½Ð¾Ð³Ð¾ проÑмотра"
-msgid "BillingPlans|Upgrade"
-msgstr "ПовыÑить"
+msgid "Badges|Project Badge"
+msgstr "Значок Проекта"
-msgid "BillingPlans|You are currently on the %{plan_link} plan."
-msgstr "Ð’ наÑтоÑщее Ð²Ñ€ÐµÐ¼Ñ Ð²Ñ‹ иÑпользуете тарифный план %{plan_link}."
+msgid "Badges|Reload badge image"
+msgstr "Обновить изображение значка"
-msgid "BillingPlans|frequently asked questions"
-msgstr "чаÑто задаваемых вопроÑов"
+msgid "Badges|Save changes"
+msgstr "Сохранить изменениÑ"
+
+msgid "Badges|Saving the badge failed, please check the entered URLs and try again."
+msgstr "Сохранение значка не удалоÑÑŒ, проверьте введенные URL-адреÑа и повторите попытку."
+
+msgid "Badges|The %{docsLinkStart}variables%{docsLinkEnd} GitLab supports: %{placeholders}"
+msgstr "%{docsLinkStart}переменные%{docsLinkEnd} GitLab поддерживает: %{placeholders}"
+
+msgid "Badges|The badge was deleted."
+msgstr "Значок был удален."
+
+msgid "Badges|The badge was saved."
+msgstr "Значок был Ñохранен."
+
+msgid "Badges|This group has no badges"
+msgstr "Эта группа не имеет значков"
+
+msgid "Badges|This project has no badges"
+msgstr "Этот проект не имеет значков"
+
+msgid "Badges|Your badges"
+msgstr "Ваши значки"
+
+msgid "Begin with the selected commit"
+msgstr "Ðачать Ñ Ð²Ñ‹Ð±Ñ€Ð°Ð½Ð½Ð¾Ð³Ð¾ коммита"
-msgid "BillingPlans|monthly"
-msgstr "ежемеÑÑчно"
+msgid "Below are examples of regex for existing tools:"
+msgstr ""
-msgid "BillingPlans|paid annually at %{price_per_year}"
-msgstr "оплачиваетÑÑ ÐµÐ¶ÐµÐ³Ð¾Ð´Ð½Ð¾ в размере %{price_per_year}"
+msgid "Boards"
+msgstr "ДоÑки"
-msgid "BillingPlans|per user"
-msgstr "за пользователÑ"
+msgid "Branch %{branchName} was not found in this project's repository."
+msgstr ""
msgid "Branch (%{branch_count})"
msgid_plural "Branches (%{branch_count})"
@@ -596,13 +750,13 @@ msgid "Branches"
msgstr "Ветки"
msgid "Branches|Active"
-msgstr ""
+msgstr "Ðктивные"
msgid "Branches|Active branches"
-msgstr ""
+msgstr "Ðктивные ветки"
msgid "Branches|All"
-msgstr ""
+msgstr "Ð’Ñе"
msgid "Branches|Cant find HEAD commit for this branch"
msgstr "Ðевозможно найти HEAD-коммит Ñтой ветки"
@@ -646,44 +800,41 @@ msgstr "Ðет веток Ð´Ð»Ñ Ð¾Ñ‚Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð¸Ñ"
msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
msgstr "Как только вы подтвердите и нажмёте %{delete_protected_branch}, данные будут удалены без возможноÑти воÑÑтановлениÑ."
-msgid "Branches|Only a project master or owner can delete a protected branch"
-msgstr "Только маÑтер или владелец проекта может удалить защищённую ветку"
+msgid "Branches|Only a project maintainer or owner can delete a protected branch"
+msgstr ""
msgid "Branches|Overview"
-msgstr ""
+msgstr "Обзор"
msgid "Branches|Protected branches can be managed in %{project_settings_link}."
-msgstr ""
+msgstr "Управление защищёнными ветками возможно в %{project_settings_link}."
msgid "Branches|Show active branches"
-msgstr ""
+msgstr "Показать активные ветки"
msgid "Branches|Show all branches"
-msgstr ""
+msgstr "Показать вÑе ветки"
msgid "Branches|Show more active branches"
-msgstr ""
+msgstr "Показать больше активных веток"
msgid "Branches|Show more stale branches"
-msgstr ""
+msgstr "Показать больше заÑтаревших веток"
msgid "Branches|Show overview of the branches"
-msgstr ""
+msgstr "Показать опиÑание веток"
msgid "Branches|Show stale branches"
-msgstr ""
+msgstr "Показать заÑтаревшие ветки"
msgid "Branches|Sort by"
msgstr "Сортировать по"
msgid "Branches|Stale"
-msgstr ""
+msgstr "ЗаÑтаревшие"
msgid "Branches|Stale branches"
-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 "Ветка \"по умолчанию\" не может быть удалена"
@@ -697,15 +848,9 @@ msgstr "Чтобы избежать потери данных, раÑÑмотрÐ
msgid "Branches|To confirm, type %{branch_name_confirmation}:"
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 "Чтобы отменить локальные Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð¸ перезапиÑать ветвь верÑией из родительÑкого репозиториÑ, удалите её здеÑÑŒ и выберите \"Обновить ÑейчаÑ\" выше."
-
msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
msgstr "Ð’Ñ‹ ÑобираетеÑÑŒ безвозвратно удалить защищённую ветку %{branch_name}."
-msgid "Branches|diverged from upstream"
-msgstr "раÑходитÑÑ Ñ Ñ€Ð¾Ð´Ð¸Ñ‚ÐµÐ»ÑŒÑким репозиторием"
-
msgid "Branches|merged"
msgstr "влита"
@@ -727,44 +872,83 @@ msgstr "ПроÑмотр файлов"
msgid "Browse files"
msgstr "ПроÑмотр файлов"
-msgid "Business"
-msgstr ""
-
msgid "ByAuthor|by"
msgstr "по автору"
msgid "CI / CD"
msgstr "CI / CD"
-msgid "CI/CD"
-msgstr "CI/CD"
+msgid "CI / CD Settings"
+msgstr ""
msgid "CI/CD configuration"
msgstr "ÐšÐ¾Ð½Ñ„Ð¸Ð³ÑƒÑ€Ð°Ñ†Ð¸Ñ CI/CD"
-msgid "CI/CD for external repo"
-msgstr "CI/CD Ð´Ð»Ñ Ð²Ð½ÐµÑˆÐ½ÐµÐ³Ð¾ репозиториÑ"
+msgid "CI/CD settings"
+msgstr "ÐаÑтройки CI/CD"
+
+msgid "CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery."
+msgstr ""
+
+msgid "CICD|Auto DevOps"
+msgstr "ÐвтоматичеÑкий DevOps"
+
+msgid "CICD|Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration."
+msgstr ""
+
+msgid "CICD|Automatic deployment to staging, manual deployment to production"
+msgstr ""
+
+msgid "CICD|Continuous deployment to production"
+msgstr ""
+
+msgid "CICD|Deployment strategy"
+msgstr ""
+
+msgid "CICD|Deployment strategy needs a domain name to work correctly."
+msgstr ""
+
+msgid "CICD|Disable Auto DevOps"
+msgstr "Отключить Auto DevOps"
+
+msgid "CICD|Enable Auto DevOps"
+msgstr "Включить Auto DevOps"
+
+msgid "CICD|Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific %{ci_file}."
+msgstr ""
+
+msgid "CICD|Instance default (%{state})"
+msgstr "ЭкземплÑÑ€ по умолчанию (%{state})"
msgid "CICD|Jobs"
msgstr "ЗаданиÑ"
+msgid "CICD|Learn more about Auto DevOps"
+msgstr ""
+
+msgid "CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project."
+msgstr ""
+
+msgid "CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages."
+msgstr ""
+
+msgid "Can't find HEAD commit for this branch"
+msgstr ""
+
msgid "Cancel"
msgstr "Отмена"
+msgid "Cancel this job"
+msgstr "Отменить Ñто задание"
+
msgid "Cannot be merged automatically"
-msgstr ""
+msgstr "Ðет возможноÑти объединить автоматичеÑки"
msgid "Cannot modify managed Kubernetes cluster"
msgstr "Ðевозможно изменить управлÑемый клаÑтер Kubernetes"
-msgid "Certificate fingerprint"
-msgstr ""
-
-msgid "Change Weight"
-msgstr "Изменить ВеÑ"
-
msgid "Change this value to influence how frequently the GitLab UI polls for updates."
-msgstr ""
+msgstr "Измените Ñто значение, чтобы уÑтановить как чаÑто GitLab UI запрашивает обновлениÑ."
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "Выбрать в ветке"
@@ -812,22 +996,19 @@ msgid "Choose File ..."
msgstr "Выберите Файл..."
msgid "Choose a branch/tag (e.g. %{master}) or enter a commit (e.g. %{sha}) to see what's changed or to create a merge request."
-msgstr "Выберите ветку/тег (например, %{master}) или введите коммит(например, %{sha}), чтобы увидеть, что изменилоÑÑŒ или Ñоздать Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние."
+msgstr "Выберите ветку/тег (например, %{master}) или введите коммит (например, %{sha}), чтобы увидеть, что изменилоÑÑŒ или Ñоздать Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние."
-msgid "Choose file..."
-msgstr "Выберите файл..."
-
-msgid "Choose which groups you wish to synchronize to this secondary node."
-msgstr "Выберите группы, которые вы хотите Ñинхронизировать Ñ Ñтим вторичным узлом."
-
-msgid "Choose which repositories you want to connect and run CI/CD pipelines."
+msgid "Choose any color."
msgstr ""
-msgid "Choose which repositories you want to import."
+msgid "Choose between <code>clone</code> or <code>fetch</code> to get the recent application code"
msgstr ""
-msgid "Choose which shards you wish to synchronize to this secondary node."
-msgstr ""
+msgid "Choose file..."
+msgstr "Выберите файл..."
+
+msgid "Choose which repositories you want to import."
+msgstr "Выберите, какие репозитории вы хотите импортировать."
msgid "CiStatusLabel|canceled"
msgstr "отменено"
@@ -898,21 +1079,12 @@ msgstr " * (Ð’Ñе Ñреды)"
msgid "CiVariable|All environments"
msgstr "Ð’Ñе Ñреды"
-msgid "CiVariable|Create wildcard"
-msgstr "Создать шаблон"
-
msgid "CiVariable|Error occured while saving variables"
msgstr "Произошла ошибка при Ñохранении переменных"
-msgid "CiVariable|New environment"
-msgstr "ÐÐ¾Ð²Ð°Ñ Ñреда"
-
msgid "CiVariable|Protected"
msgstr "Защищено"
-msgid "CiVariable|Search environments"
-msgstr "ПоиÑк Ñред"
-
msgid "CiVariable|Toggle protected"
msgstr "Включить защиту"
@@ -922,30 +1094,30 @@ msgstr "Проверка не удалаÑÑŒ"
msgid "CircuitBreakerApiLink|circuitbreaker api"
msgstr "CircuitBreaker API"
-msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
-msgstr "Ðажмите кнопку ниже, чтобы начать процеÑÑ ÑƒÑтановки, Ð¿ÐµÑ€ÐµÐ¹Ð´Ñ Ð½Ð° Ñтраницу Kubernetes"
+msgid "Clear search input"
+msgstr "ОчиÑтить Ñтроку поиÑка"
-msgid "Click to expand text"
-msgstr "Ðажмите, чтобы раÑкрыть текÑÑ‚"
+msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
+msgstr "Выберите из ÑпиÑка любой <strong>проект</strong>, чтобы перейти к Ñтапу проекта."
-msgid "Client authentication certificate"
+msgid "Click the <strong>Promote</strong> button in the top right corner to promote it to a group milestone."
msgstr ""
-msgid "Client authentication key"
-msgstr ""
+msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
+msgstr "Ðажмите кнопку ниже, чтобы начать процеÑÑ ÑƒÑтановки, Ð¿ÐµÑ€ÐµÐ¹Ð´Ñ Ð½Ð° Ñтраницу Kubernetes"
-msgid "Client authentication key password"
+msgid "Click to expand it."
msgstr ""
+msgid "Click to expand text"
+msgstr "Ðажмите, чтобы раÑкрыть текÑÑ‚"
+
msgid "Clone repository"
msgstr "Клонировать репозиторий"
msgid "Close"
msgstr "Закрыть"
-msgid "Closed"
-msgstr "Закрыто"
-
msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster"
msgstr "%{appList} уÑпешно уÑтановлены на вашем клаÑтере Kubernetes"
@@ -961,6 +1133,12 @@ msgstr "Добавить ÑущеÑтвующий клаÑтер Kubernetes"
msgid "ClusterIntegration|Advanced options on this Kubernetes cluster's integration"
msgstr "Дополнительные опции Ð´Ð»Ñ Ñтой интеграции Ñ ÐºÐ»Ð°Ñтером Kubernetes"
+msgid "ClusterIntegration|An error occured while trying to fetch project zones: %{error}"
+msgstr "Произошла ошибка при попытке Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ð·Ð¾Ð½ проекта: %{error}"
+
+msgid "ClusterIntegration|An error occured while trying to fetch your projects: %{error}"
+msgstr "Произошла ошибка при попытке Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ð²Ð°ÑˆÐ¸Ñ… проектов: %{error}"
+
msgid "ClusterIntegration|Applications"
msgstr "ПриложениÑ"
@@ -991,6 +1169,9 @@ msgstr "Копировать Сертификат УдоÑтоверÑющего
msgid "ClusterIntegration|Copy Ingress IP Address to clipboard"
msgstr "Скопировать IP-Ð°Ð´Ñ€ÐµÑ Ingress в буфер обмена"
+msgid "ClusterIntegration|Copy Jupyter Hostname to clipboard"
+msgstr ""
+
msgid "ClusterIntegration|Copy Kubernetes cluster name"
msgstr "Скопировать Ð¸Ð¼Ñ ÐºÐ»Ð°Ñтера Kubernetes"
@@ -1006,8 +1187,8 @@ msgstr "Создать клаÑтер при помощи Google Kubernetes Engi
msgid "ClusterIntegration|Create a new Kubernetes cluster on Google Kubernetes Engine right from GitLab"
msgstr "Создайте новый клаÑтер Kubernetes в Google Kubernetes Engine прÑмо из GitLab"
-msgid "ClusterIntegration|Create on GKE"
-msgstr "Создать в GKE"
+msgid "ClusterIntegration|Create on Google Kubernetes Engine"
+msgstr "Создать в Google Kubernetes Engine"
msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
msgstr "Укажите параметры ÑущеÑтвующего клаÑтера Kubernetes"
@@ -1016,16 +1197,28 @@ msgid "ClusterIntegration|Enter the details for your Kubernetes cluster"
msgstr "Введите ÑÐ²ÐµÐ´ÐµÐ½Ð¸Ñ Ð¾ вашем клаÑтере Kubernetes"
msgid "ClusterIntegration|Environment scope"
+msgstr "Среда окружениÑ"
+
+msgid "ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab's Google Kubernetes Engine Integration."
msgstr ""
+msgid "ClusterIntegration|Fetching machine types"
+msgstr "Получение типов машин клаÑтера"
+
+msgid "ClusterIntegration|Fetching projects"
+msgstr "Получение проектов"
+
+msgid "ClusterIntegration|Fetching zones"
+msgstr "Получение зон"
+
msgid "ClusterIntegration|GitLab Integration"
msgstr "Ð˜Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ð¸Ñ Ñ GitLab"
msgid "ClusterIntegration|GitLab Runner"
msgstr "GitLab Runner"
-msgid "ClusterIntegration|Google Cloud Platform project ID"
-msgstr "Идентификатор проекта в Google Cloud Platform"
+msgid "ClusterIntegration|Google Cloud Platform project"
+msgstr ""
msgid "ClusterIntegration|Google Kubernetes Engine"
msgstr "Google Kubernetes Engine"
@@ -1036,9 +1229,6 @@ msgstr "Проект Google Kubernetes Engine"
msgid "ClusterIntegration|Helm Tiller"
msgstr "Helm Tiller"
-msgid "ClusterIntegration|In order to show the health of the cluster, we'll need to provision your cluster with Prometheus to collect the required data."
-msgstr ""
-
msgid "ClusterIntegration|Ingress"
msgstr "Ingress"
@@ -1048,9 +1238,6 @@ msgstr "IP-Ð°Ð´Ñ€ÐµÑ Ingress"
msgid "ClusterIntegration|Install"
msgstr "УÑтановить"
-msgid "ClusterIntegration|Install Prometheus"
-msgstr ""
-
msgid "ClusterIntegration|Installed"
msgstr "УÑтановлен"
@@ -1063,15 +1250,18 @@ msgstr "Ð˜Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ð¸Ñ Ð°Ð²Ñ‚Ð¾Ð¼Ð°Ñ‚Ð¸Ð·Ð°Ñ†Ð¸Ð¸ клаÑтеров Kuber
msgid "ClusterIntegration|Integration status"
msgstr " Ð¡Ñ‚Ð°Ñ‚ÑƒÑ Ð¸Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ð¸Ð¸"
+msgid "ClusterIntegration|Jupyter Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|JupyterHub"
+msgstr ""
+
msgid "ClusterIntegration|Kubernetes cluster"
msgstr "КлаÑтер Kubernetes"
msgid "ClusterIntegration|Kubernetes cluster details"
msgstr "Ð¡Ð²ÐµÐ´ÐµÐ½Ð¸Ñ Ð¾ клаÑтере Kubernetes"
-msgid "ClusterIntegration|Kubernetes cluster health"
-msgstr ""
-
msgid "ClusterIntegration|Kubernetes cluster integration"
msgstr "Ð˜Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ð¸Ñ ÐºÐ»Ð°Ñтера Kubernetes"
@@ -1099,20 +1289,26 @@ msgstr "КлаÑтер Kubernetes позволÑет вам иÑпользова
msgid "ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project"
msgstr "КлаÑтеры Kubernetes могут иÑпользоватьÑÑ Ð´Ð»Ñ Ñ€Ð°Ð·Ð²ÐµÑ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ð¹ и предоÑÑ‚Ð°Ð²Ð»ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ð¹ Review Ð´Ð»Ñ Ñтого проекта"
-msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
-msgstr "Узнайте больше на %{link_to_documentation}"
+msgid "ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}."
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{help_link_start}Kubernetes%{help_link_end}."
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}."
+msgstr ""
msgid "ClusterIntegration|Learn more about environments"
msgstr "Узнайте больше о Ñредах"
msgid "ClusterIntegration|Learn more about security configuration"
-msgstr ""
+msgstr "Узнайте больше о наÑтройке безопаÑноÑти"
msgid "ClusterIntegration|Machine type"
msgstr "Тип машины"
msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create Kubernetes clusters"
-msgstr ""
+msgstr "УбедитеÑÑŒ, что ваша ÑƒÑ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ %{link_to_requirements} Ð´Ð»Ñ ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ ÐºÐ»Ð°Ñтеров"
msgid "ClusterIntegration|Manage"
msgstr "Управление"
@@ -1123,8 +1319,17 @@ msgstr "УправлÑйте вашему клаÑтером Kubernetes, перÐ
msgid "ClusterIntegration|More information"
msgstr "Ð”Ð¾Ð¿Ð¾Ð»Ð½Ð¸Ñ‚ÐµÐ»ÑŒÐ½Ð°Ñ Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ"
-msgid "ClusterIntegration|Multiple Kubernetes clusters are available in GitLab Enterprise Edition Premium and Ultimate"
-msgstr "ÐеÑколько клаÑтеров Kubernetes доÑтупно в GitLab Entreprise Edition Premium и Ultimate"
+msgid "ClusterIntegration|No machine types matched your search"
+msgstr "Ðет типов машин, ÑоответÑтвующих вашему поиÑковому запроÑу"
+
+msgid "ClusterIntegration|No projects found"
+msgstr "Проекты не найдены"
+
+msgid "ClusterIntegration|No projects matched your search"
+msgstr "Ðет проектов, ÑоответÑтвующих вашему поиÑковому запроÑу"
+
+msgid "ClusterIntegration|No zones matched your search"
+msgstr "Ðет зон, ÑоответÑтвующих вашему поиÑковому запроÑу"
msgid "ClusterIntegration|Note:"
msgstr "Примечание:"
@@ -1138,9 +1343,6 @@ msgstr "ПожалуйÑта, укажите параметры доÑтупа Ð
msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
msgstr "ПожалуйÑта, убедитеÑÑŒ, что ваш аккаунт Google отвечает Ñледующим требованиÑм:"
-msgid "ClusterIntegration|Project ID"
-msgstr "ID проекта"
-
msgid "ClusterIntegration|Project namespace"
msgstr "ПроÑтранÑтво имён проекта"
@@ -1148,10 +1350,13 @@ msgid "ClusterIntegration|Project namespace (optional, unique)"
msgstr "ПроÑтранÑтво имен проекта (необÑзательное, уникальное)"
msgid "ClusterIntegration|Prometheus"
-msgstr ""
+msgstr "Prometheus"
msgid "ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration."
-msgstr ""
+msgstr "Прочтите нашу документацию %{link_to_help_page} по интеграции клаÑтера Kubernetes."
+
+msgid "ClusterIntegration|Redeem up to $500 in free credit for Google Cloud Platform"
+msgstr "Получите до $500 беÑплатного кредита Ð´Ð»Ñ Google Cloud Platform"
msgid "ClusterIntegration|Remove Kubernetes cluster integration"
msgstr "Удалить интеграцию клаÑтера Kubernetes"
@@ -1168,20 +1373,38 @@ msgstr "Ðе удалоÑÑŒ выполнить Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° запуÑк п
msgid "ClusterIntegration|Save changes"
msgstr "Сохранить изменениÑ"
+msgid "ClusterIntegration|Search machine types"
+msgstr "ПоиÑк типов машин клаÑтера"
+
+msgid "ClusterIntegration|Search projects"
+msgstr "ПоиÑк проектов"
+
+msgid "ClusterIntegration|Search zones"
+msgstr "ПоиÑк зон"
+
msgid "ClusterIntegration|Security"
-msgstr ""
+msgstr "БезопаÑноÑть"
msgid "ClusterIntegration|See and edit the details for your Kubernetes cluster"
msgstr "ПроÑмотр и редактирование информации о вашем клаÑтере Kubernetes"
-msgid "ClusterIntegration|See machine types"
-msgstr "См. типы машин"
+msgid "ClusterIntegration|Select machine type"
+msgstr "Выберите тип машины"
+
+msgid "ClusterIntegration|Select project"
+msgstr "Выберите проект"
+
+msgid "ClusterIntegration|Select project and zone to choose machine type"
+msgstr "Выберите проект и зону, чтобы выбрать тип машины"
-msgid "ClusterIntegration|See your projects"
-msgstr "См. ваши проекты"
+msgid "ClusterIntegration|Select project to choose zone"
+msgstr "Выберите проект, чтобы выбрать зону"
-msgid "ClusterIntegration|See zones"
-msgstr "См. зоны"
+msgid "ClusterIntegration|Select zone"
+msgstr "Выберете зону"
+
+msgid "ClusterIntegration|Select zone to choose machine type"
+msgstr "Выберите зону, чтобы выбрать тип машины"
msgid "ClusterIntegration|Service token"
msgstr "Служебный токен"
@@ -1199,10 +1422,10 @@ msgid "ClusterIntegration|Something went wrong while installing %{title}"
msgstr "Произошли ошибки во Ð²Ñ€ÐµÐ¼Ñ ÑƒÑтановки %{title}"
msgid "ClusterIntegration|The default cluster configuration grants access to a wide set of functionalities needed to successfully build and deploy a containerised application."
-msgstr ""
+msgstr "По умолчанию ÐºÐ¾Ð½Ñ„Ð¸Ð³ÑƒÑ€Ð°Ñ†Ð¸Ñ ÐºÐ»Ð°Ñтера предоÑтавлÑет доÑтуп к широкому набору функциональных возможноÑтей, необходимых Ð´Ð»Ñ ÑƒÑпешного ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ Ð¸ Ñ€Ð°Ð·Ð²ÐµÑ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ð¹ в контейнерах."
msgid "ClusterIntegration|This account must have permissions to create a Kubernetes cluster in the %{link_to_container_project} specified below"
-msgstr ""
+msgstr " У Ñтой учетной запиÑи должны быть Ñ€Ð°Ð·Ñ€ÐµÑˆÐµÐ½Ð¸Ñ Ð½Ð° Ñоздание клаÑтера Kubernetes в %{link_to_container_project} указанных ниже"
msgid "ClusterIntegration|Toggle Kubernetes Cluster"
msgstr "Переключить КлаÑтер Kubernetes"
@@ -1213,6 +1436,9 @@ msgstr "Переключить клаÑтер Kubernetes"
msgid "ClusterIntegration|Token"
msgstr "Токен"
+msgid "ClusterIntegration|Validating project billing status"
+msgstr "Проверка ÑтатуÑа тарификации проекта"
+
msgid "ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr "ЕÑли привÑзать клаÑтер Kubernetes к Ñтому проекту, вы Ñ Ð»Ñ‘Ð³ÐºÐ¾Ñтью Ñможете иÑпользовать Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð´Ð»Ñ Ñ€ÐµÐ²ÑŒÑŽ, развертывать ваши приложениÑ, запуÑкать Ñборочные линии и многое другое."
@@ -1226,7 +1452,7 @@ msgid "ClusterIntegration|access to Google Kubernetes Engine"
msgstr "доÑтуп к Google Kubernetes Engine"
msgid "ClusterIntegration|check the pricing here"
-msgstr ""
+msgstr "поÑмотрите цены здеÑÑŒ"
msgid "ClusterIntegration|documentation"
msgstr "документациÑ"
@@ -1243,14 +1469,20 @@ msgstr "отвечает требованиÑм"
msgid "ClusterIntegration|properly configured"
msgstr "правильно наÑтроен"
+msgid "ClusterIntegration|sign up"
+msgstr "зарегиÑтрироватьÑÑ"
+
msgid "Collapse"
msgstr "Свернуть"
-msgid "Comment and resolve discussion"
-msgstr "Прокомментировать и закрыть диÑкуÑÑию"
+msgid "Collapse sidebar"
+msgstr "Свернуть боковую панель"
-msgid "Comment and unresolve discussion"
-msgstr "Прокомментировать и переоткрыть диÑкуÑÑию"
+msgid "Comment & resolve discussion"
+msgstr ""
+
+msgid "Comment & unresolve discussion"
+msgstr ""
msgid "Comments"
msgstr "Комментарии"
@@ -1279,7 +1511,7 @@ msgid "Commit message"
msgstr "ОпиÑание коммита"
msgid "Commit statistics for %{ref} %{start_time} - %{end_time}"
-msgstr ""
+msgstr "СтатиÑтика коммитов Ð´Ð»Ñ %{ref} в период Ñ %{start_time} по %{end_time}"
msgid "Commit to %{branchName} branch"
msgstr ""
@@ -1297,46 +1529,49 @@ msgid "Commits feed"
msgstr "Лента коммитов"
msgid "Commits per day hour (UTC)"
-msgstr ""
+msgstr "Коммиты по чаÑам Ð´Ð½Ñ (UTC)"
msgid "Commits per day of month"
-msgstr ""
+msgstr "Коммиты по днÑм меÑÑца"
msgid "Commits per weekday"
-msgstr ""
+msgstr "Коммиты по днÑм недели"
msgid "Commits|An error occurred while fetching merge requests data."
msgstr "Произошла ошибка при получении данных запроÑа на ÑлиÑниÑ."
msgid "Commits|Commit: %{commitText}"
-msgstr ""
+msgstr "Коммит: %{commitText}"
msgid "Commits|History"
msgstr "ИÑториÑ"
msgid "Commits|No related merge requests found"
-msgstr ""
+msgstr "Ðе найдено ÑвÑзанных запроÑов на ÑлиÑние"
msgid "Committed by"
msgstr "ЗафикÑировано автором"
+msgid "Commit…"
+msgstr ""
+
msgid "Compare"
msgstr "Сравнить"
msgid "Compare Git revisions"
-msgstr ""
+msgstr "Сравнение верÑий Git"
msgid "Compare Revisions"
-msgstr ""
+msgstr "Сравнить верÑии"
msgid "Compare changes with the last commit"
-msgstr ""
+msgstr "Сравнить Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ñ Ð¿Ð¾Ñледним коммитом"
msgid "Compare changes with the merge request target branch"
msgstr ""
msgid "CompareBranches|%{source_branch} and %{target_branch} are the same."
-msgstr ""
+msgstr "%{source_branch} и %{target_branch} одинаковы."
msgid "CompareBranches|Compare"
msgstr "Сравнить"
@@ -1351,21 +1586,24 @@ msgid "CompareBranches|There isn't anything to compare."
msgstr "Ðечего Ñравнивать."
msgid "Confidential"
-msgstr ""
+msgstr "Конфиденциально"
msgid "Confidentiality"
msgstr "КонфиденциальноÑть"
msgid "Configure Gitaly timeouts."
-msgstr ""
+msgstr "ÐаÑтройка таймаутов Gitaly."
msgid "Configure Sidekiq job throttling."
-msgstr ""
+msgstr "ÐаÑтройте фактор уÑÐºÐ¾Ñ€ÐµÐ½Ð¸Ñ Ð¾Ð±Ñ€Ð°Ð±Ð¾Ñ‚ÐºÐ¸ очереди иÑполнителей фоновых заданий Sidekiq."
msgid "Configure automatic git checks and housekeeping on repositories."
msgstr ""
msgid "Configure limits for web and API requests."
+msgstr "ÐаÑтройка ограничений Ð´Ð»Ñ Web и API запроÑов."
+
+msgid "Configure push mirrors."
msgstr ""
msgid "Configure storage path and circuit breaker settings."
@@ -1375,19 +1613,10 @@ msgid "Configure the way a user creates a new account."
msgstr ""
msgid "Connect"
-msgstr ""
-
-msgid "Connect all repositories"
-msgstr ""
+msgstr "Подключить"
msgid "Connect repositories from GitHub"
-msgstr ""
-
-msgid "Connect your external repositories, and CI/CD pipelines will run for new commits. A GitLab project will be created with only CI/CD features enabled."
-msgstr ""
-
-msgid "Connecting..."
-msgstr "Подключение..."
+msgstr "Подключить репозитории Ñ GitHub"
msgid "Container Registry"
msgstr "РееÑтр Контейнеров"
@@ -1434,11 +1663,20 @@ 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 "ContainerRegistry|You can also use a %{deploy_token} for read-only access to the registry images."
+msgstr "Ð’Ñ‹ также можете иÑпользовать %{deploy_token} Ð´Ð»Ñ Ð´Ð¾Ñтупа в режиме только чтение к рееÑтру образов."
+
+msgid "Continue"
+msgstr "Продолжить"
+
msgid "Continuous Integration and Deployment"
+msgstr "ÐÐµÐ¿Ñ€ÐµÑ€Ñ‹Ð²Ð½Ð°Ñ Ð¸Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ð¸Ñ Ð¸ развертывание"
+
+msgid "Contribute to GitLab"
msgstr ""
msgid "Contribution"
-msgstr ""
+msgstr "УчаÑтие"
msgid "Contribution guide"
msgstr "РуководÑтво учаÑтника"
@@ -1458,15 +1696,6 @@ msgstr "Коммиты в %{branch_name}, за иÑключением комми
msgid "ContributorsPage|Please wait a moment, this page will automatically refresh when ready."
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 "Скопировать публичный ключ SSH в буфер обмена"
-
msgid "Copy URL to clipboard"
msgstr "Копировать URL в буфер обмена"
@@ -1479,9 +1708,18 @@ msgstr "Копировать команду в буфер обмена"
msgid "Copy commit SHA to clipboard"
msgstr "Копировать SHA коммита в буфер обмена"
+msgid "Copy file name to clipboard"
+msgstr ""
+
+msgid "Copy file path to clipboard"
+msgstr ""
+
msgid "Copy reference to clipboard"
msgstr "Скопировать ÑÑылку в буфер обмена"
+msgid "Copy to clipboard"
+msgstr "Скопировать в буфер обмена"
+
msgid "Create"
msgstr "Создать"
@@ -1500,20 +1738,20 @@ msgstr "Создать личный токен на аккаунте Ð´Ð»Ñ Ð¿Ð¾
msgid "Create branch"
msgstr "Создать ветку"
+msgid "Create commit"
+msgstr ""
+
msgid "Create directory"
msgstr "Создать каталог"
msgid "Create empty repository"
-msgstr ""
-
-msgid "Create epic"
-msgstr "Создать Ñпик"
+msgstr "Создать пуÑтой репозиторий"
msgid "Create file"
msgstr "Создать файл"
msgid "Create group label"
-msgstr ""
+msgstr "Создать метку группы"
msgid "Create lists from labels. Issues with that label appear in that list."
msgstr "Создать ÑпиÑок из меток. ОбÑÑƒÐ¶Ð´ÐµÐ½Ð¸Ñ Ñ Ñтой меткой поÑвлÑÑŽÑ‚ÑÑ Ð² Ñтом ÑпиÑке."
@@ -1540,7 +1778,7 @@ msgid "Create new..."
msgstr "Ðовый"
msgid "Create project label"
-msgstr ""
+msgstr "Создать метку проекта"
msgid "CreateNewFork|Fork"
msgstr "Ответвить"
@@ -1551,14 +1789,11 @@ msgstr "Тег"
msgid "CreateTokenToCloneLink|create a personal access token"
msgstr "Ñоздать перÑональный токен доÑтупа"
-msgid "Creates a new branch from %{branchName}"
-msgstr "Создает новую ветку из %{branchName}"
-
-msgid "Creates a new branch from %{branchName} and re-directs to create a new merge request"
-msgstr "Создает новую ветку из %{branchName} и перенаправлÑет на Ñоздание нового запроÑа на ÑлиÑние"
+msgid "Created"
+msgstr "Создан"
-msgid "Creating epic"
-msgstr "Создание Ñпика"
+msgid "Created by me"
+msgstr ""
msgid "Cron Timezone"
msgstr "Ð’Ñ€ÐµÐ¼ÐµÐ½Ð½Ð°Ñ Ð·Ð¾Ð½Ð° Cron"
@@ -1566,8 +1801,14 @@ msgstr "Ð’Ñ€ÐµÐ¼ÐµÐ½Ð½Ð°Ñ Ð·Ð¾Ð½Ð° Cron"
msgid "Cron syntax"
msgstr "СинтакÑÐ¸Ñ Cron"
-msgid "Current node"
-msgstr "Текущий узел"
+msgid "CurrentUser|Profile"
+msgstr "Профиль"
+
+msgid "CurrentUser|Settings"
+msgstr "ÐаÑтройки"
+
+msgid "Custom CI config path"
+msgstr ""
msgid "Custom notification events"
msgstr "Ð¡Ð¾Ð±Ñ‹Ñ‚Ð¸Ñ Ð½Ð°Ñтраиваемых уведомлений"
@@ -1575,9 +1816,6 @@ 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 "Customize colors"
-msgstr ""
-
msgid "Cycle Analytics"
msgstr "Ðналитика Цикла"
@@ -1614,8 +1852,8 @@ msgstr "Дек."
msgid "December"
msgstr "Декабрь"
-msgid "Default classification label"
-msgstr "Метка клаÑÑификации по умолчанию"
+msgid "Decline and sign out"
+msgstr "Отклонить и выйти"
msgid "Define a custom pattern with cron syntax"
msgstr "Определить наÑтраиваемый шаблон Ñ ÑинтакÑиÑом cron"
@@ -1623,6 +1861,9 @@ msgstr "Определить наÑтраиваемый шаблон Ñ Ñинт
msgid "Delete"
msgstr "Удалить"
+msgid "Delete list"
+msgstr ""
+
msgid "Deploy"
msgid_plural "Deploys"
msgstr[0] "Развертывание"
@@ -1633,44 +1874,170 @@ msgstr[3] "РазвертываниÑ"
msgid "Deploy Keys"
msgstr "Ключи РазвертываниÑ"
+msgid "DeployKeys|+%{count} others"
+msgstr "+%{count} других"
+
+msgid "DeployKeys|Current project"
+msgstr "Текущий проект"
+
+msgid "DeployKeys|Deploy key"
+msgstr "Ключ развертываниÑ"
+
+msgid "DeployKeys|Enabled deploy keys"
+msgstr "Примененные ключи развертываниÑ"
+
+msgid "DeployKeys|Error enabling deploy key"
+msgstr "Ошибка Ð¿Ñ€Ð¸Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ ÐºÐ»ÑŽÑ‡Ð° развертываниÑ"
+
+msgid "DeployKeys|Error getting deploy keys"
+msgstr "Ошибка Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ ÐºÐ»ÑŽÑ‡Ð° развертываниÑ"
+
+msgid "DeployKeys|Error removing deploy key"
+msgstr "Ошибка ÑƒÐ´Ð°Ð»ÐµÐ½Ð¸Ñ ÐºÐ»ÑŽÑ‡Ð° развертываниÑ"
+
+msgid "DeployKeys|Expand %{count} other projects"
+msgstr "Развернуть в %{count} других проектах"
+
+msgid "DeployKeys|Loading deploy keys"
+msgstr "Загрузка ключей развертываниÑ"
+
+msgid "DeployKeys|No deploy keys found. Create one with the form above."
+msgstr "Ðе найдено ключей развертываниÑ. Создайте первый Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ данной формы."
+
+msgid "DeployKeys|Privately accessible deploy keys"
+msgstr "Приватные ключи развертываниÑ"
+
+msgid "DeployKeys|Project usage"
+msgstr "ИÑпользование в проекте"
+
+msgid "DeployKeys|Publicly accessible deploy keys"
+msgstr "Публичные ключи развертываниÑ"
+
+msgid "DeployKeys|Read access only"
+msgstr "ДоÑтуп только на чтение"
+
+msgid "DeployKeys|Write access allowed"
+msgstr "Разрешен доÑтуп Ð´Ð»Ñ Ð·Ð°Ð¿Ð¸Ñи"
+
+msgid "DeployKeys|You are going to remove this deploy key. Are you sure?"
+msgstr "Ð’Ñ‹ ÑобираетеÑÑŒ удалить Ñтот ключ развертываниÑ. Ð’Ñ‹ уверены?"
+
+msgid "DeployTokens|Active Deploy Tokens (%{active_tokens})"
+msgstr "Ðктивные токены Ñ€Ð°Ð·Ð²ÐµÑ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ (%{active_tokens})"
+
+msgid "DeployTokens|Add a deploy token"
+msgstr "Создать токен развертываниÑ"
+
+msgid "DeployTokens|Allows read-only access to the registry images"
+msgstr "Разрешает доÑтуп только Ð´Ð»Ñ Ñ‡Ñ‚ÐµÐ½Ð¸Ñ Ðº региÑтру образов"
+
+msgid "DeployTokens|Allows read-only access to the repository"
+msgstr "Разрешает доÑтуп только Ð´Ð»Ñ Ñ‡Ñ‚ÐµÐ½Ð¸Ñ Ðº репозиторию"
+
+msgid "DeployTokens|Copy deploy token to clipboard"
+msgstr "Копировать токен Ñ€Ð°Ð·Ð²Ñ‘Ñ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ð² буфер обмена"
+
+msgid "DeployTokens|Copy username to clipboard"
+msgstr "Копировать Ð¸Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ Ð² буфер обмена"
+
+msgid "DeployTokens|Create deploy token"
+msgstr "Создать токен развертываниÑ"
+
+msgid "DeployTokens|Created"
+msgstr "Создан"
+
+msgid "DeployTokens|Deploy Tokens"
+msgstr "Токены развертываниÑ"
+
+msgid "DeployTokens|Deploy tokens allow read-only access to your repository and registry images."
+msgstr "Токены Ñ€Ð°Ð·Ð²ÐµÑ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ð´Ð°ÑŽÑ‚ доÑтуп только Ð´Ð»Ñ Ñ‡Ñ‚ÐµÐ½Ð¸Ñ Ðº вашим репозиториÑм и региÑтру образов."
+
+msgid "DeployTokens|Expires"
+msgstr "ИÑтекает"
+
+msgid "DeployTokens|Name"
+msgstr "Ðаименование"
+
+msgid "DeployTokens|Pick a name for the application, and we'll give you a unique deploy token."
+msgstr "Выберите Ð¸Ð¼Ñ Ð´Ð»Ñ Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ, и мы предоÑтавим вам уникальный токен развертываниÑ."
+
+msgid "DeployTokens|Revoke"
+msgstr "Отозвать"
+
+msgid "DeployTokens|Revoke %{name}"
+msgstr "Отозвать %{name}"
+
+msgid "DeployTokens|Scopes"
+msgstr "ОблаÑти"
+
+msgid "DeployTokens|This action cannot be undone."
+msgstr "Это дейÑтвие Ð½ÐµÐ»ÑŒÐ·Ñ Ð¾Ñ‚Ð¼ÐµÐ½Ð¸Ñ‚ÑŒ."
+
+msgid "DeployTokens|This project has no active Deploy Tokens."
+msgstr "Этот проект не имеет активных токенов развертываниÑ."
+
+msgid "DeployTokens|Use this token as a password. Make sure you save it - you won't be able to access it again."
+msgstr "ИÑпользуйте Ñтот токен в качеÑтве паролÑ. УбедитеÑÑŒ, что вы Ñохраните его - вы не Ñможете получить доÑтуп к нему Ñнова."
+
+msgid "DeployTokens|Use this username as a login."
+msgstr "ИÑпользуйте Ñто Ð¸Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ ÐºÐ°Ðº логин."
+
+msgid "DeployTokens|Username"
+msgstr "Ð˜Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ"
+
+msgid "DeployTokens|You are about to revoke"
+msgstr "Ð’Ñ‹ ÑобираетеÑÑŒ отозвать"
+
+msgid "DeployTokens|Your New Deploy Token"
+msgstr "Ваш Ðовый Токен РазвертываниÑ"
+
+msgid "DeployTokens|Your new project deploy token has been created."
+msgstr "Создан новый токен Ð´Ð»Ñ Ñ€Ð°Ð·Ð²ÐµÑ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ð¿Ñ€Ð¾ÐµÐºÑ‚Ð°."
+
+msgid "Deprioritize label"
+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 "Diffs|No file name available"
msgstr "Ð˜Ð¼Ñ Ñ„Ð°Ð¹Ð»Ð° недоÑтупно"
+msgid "Diffs|Something went wrong while fetching diff lines."
+msgstr ""
+
msgid "Directory name"
msgstr "Ð˜Ð¼Ñ ÐºÐ°Ñ‚Ð°Ð»Ð¾Ð³Ð°"
msgid "Disable"
msgstr "Отключить"
+msgid "Disable for this project"
+msgstr "Отключить Ð´Ð»Ñ Ñтого проекта"
+
+msgid "Disable group Runners"
+msgstr "Выключить групповые обработчиков заданий"
+
+msgid "Discard changes"
+msgstr "Отменить изменениÑ"
+
msgid "Discard draft"
msgstr "Удалить черновик"
-msgid "Discover GitLab Geo."
-msgstr "Откройте Ð´Ð»Ñ ÑÐµÐ±Ñ GitLab Geo."
-
msgid "Dismiss Cycle Analytics introduction box"
msgstr "Отключить блок Ð²Ð²ÐµÐ´ÐµÐ½Ð¸Ñ Ð² Ðналитику Цикла"
-msgid "Dismiss Merge Request promotion"
-msgstr "Отключить анонÑÑ‹ Ð´Ð»Ñ Ð—Ð°Ð¿Ñ€Ð¾Ñов на ÑлиÑние"
-
-msgid "Documentation for popular identity providers"
-msgstr ""
+msgid "Domain"
+msgstr "Домен"
msgid "Don't show again"
msgstr "Ðе показывать Ñнова"
msgid "Done"
-msgstr ""
+msgstr "Выполнено"
msgid "Download"
msgstr "Скачать"
@@ -1700,57 +2067,63 @@ msgid "DownloadSource|Download"
msgstr "Скачать"
msgid "Downvotes"
-msgstr ""
+msgstr "ГолоÑа \"против\""
msgid "Due date"
-msgstr "Срок"
+msgstr "Плановый Ñрок"
-msgid "During this process, you’ll be asked for URLs from GitLab’s side. Use the URLs shown below."
+msgid "Each Runner can be in one of the following states:"
msgstr ""
msgid "Edit"
msgstr "Редактировать"
+msgid "Edit Label"
+msgstr ""
+
msgid "Edit Pipeline Schedule %{id}"
msgstr "Изменить раÑпиÑание Ñборочной линии %{id}"
msgid "Edit files in the editor and commit changes here"
msgstr "Редактируйте файлы в редакторе и зафикÑируйте Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð·Ð´ÐµÑÑŒ"
-msgid "Editing"
-msgstr ""
-
-msgid "Elasticsearch"
-msgstr ""
-
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "Edit identity for %{user_name}"
msgstr ""
msgid "Email"
+msgstr "Ð­Ð»ÐµÐºÑ‚Ñ€Ð¾Ð½Ð½Ð°Ñ Ð¿Ð¾Ñ‡Ñ‚Ð°"
+
+msgid "Email patch"
msgstr ""
msgid "Emails"
msgstr "Email-адреÑа"
+msgid "Embed"
+msgstr "Ð’Ñтроить"
+
msgid "Enable"
msgstr "Включить"
msgid "Enable Auto DevOps"
msgstr "Включить Auto DevOps"
-msgid "Enable SAML authentication for this group"
-msgstr ""
-
msgid "Enable Sentry for error reporting and logging."
-msgstr ""
+msgstr "Включить Sentry Ð´Ð»Ñ Ð¾Ñ‚Ñ‡ÐµÑ‚Ð¾Ð² об ошибках и журналированиÑ."
msgid "Enable and configure InfluxDB metrics."
-msgstr ""
+msgstr "Включить и наÑтроить метрики InfluxDB."
msgid "Enable and configure Prometheus metrics."
-msgstr ""
+msgstr "Включить и наÑтроить метрики Prometheus."
+
+msgid "Enable for this project"
+msgstr "Включить Ð´Ð»Ñ Ñтого проекта"
+
+msgid "Enable group Runners"
+msgstr "Включить групповые обработчики заданий"
-msgid "Enable classification control using an external service"
+msgid "Enable or disable certain group features and choose access levels."
msgstr ""
msgid "Enable or disable version check and usage ping."
@@ -1760,10 +2133,13 @@ msgid "Enable reCAPTCHA or Akismet and set IP limits."
msgstr ""
msgid "Enable the Performance Bar for a given group."
-msgstr ""
+msgstr "Включите панель производительноÑти Ð´Ð»Ñ Ð´Ð°Ð½Ð½Ð¾Ð¹ группы."
-msgid "Enabled"
-msgstr ""
+msgid "Ends at (UTC)"
+msgstr "ЗаканчиваетÑÑ Ð² (UTC)"
+
+msgid "Environments"
+msgstr "Среды"
msgid "Environments|An error occurred while fetching the environments."
msgstr "Произошла ошибка при получении окружений."
@@ -1813,33 +2189,18 @@ msgstr "Обновлено"
msgid "Environments|You don't have any environments right now."
msgstr "Ð’Ñ‹ пока не наÑтроили ни одного окружениÑ."
-msgid "Epic will be removed! Are you sure?"
-msgstr "Эпик будет удален! Вы уверены?"
-
-msgid "Epics"
-msgstr "Эпики"
-
-msgid "Epics Roadmap"
-msgstr ""
-
-msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
-msgstr "Эпики позволÑÑ‚ вам управлÑть портфелем проектов более Ñффективно и Ñ Ð¼ÐµÐ½ÑŒÑˆÐ¸Ð¼Ð¸ уÑилиÑми"
-
msgid "Error Reporting and Logging"
-msgstr ""
-
-msgid "Error checking branch data. Please try again."
-msgstr "Ошибка проверки данных ветки. ПожалуйÑта, попробуйте еще раз."
+msgstr "Отчеты об ошибках и журналирование"
msgid "Error committing changes. Please try again."
msgstr "Ошибка при Ñохранении изменений. ПожалуйÑта, попробуйте еще раз."
-msgid "Error creating epic"
-msgstr "Ошибка ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ Ñпика"
-
msgid "Error fetching contributors data."
msgstr "Ошибка Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ð´Ð°Ð½Ð½Ñ‹Ñ… учаÑтников."
+msgid "Error fetching job trace"
+msgstr ""
+
msgid "Error fetching labels."
msgstr "Ошибка Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ð¼ÐµÑ‚Ð¾Ðº."
@@ -1852,6 +2213,18 @@ msgstr "Ошибка Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ ÑÑылок"
msgid "Error fetching usage ping data."
msgstr "Ошибка при получении данных об иÑпользовании ping."
+msgid "Error loading branch data. Please try again."
+msgstr "Ошибка загрузки данных ветки. ПожалуйÑта, попробуйте еще раз."
+
+msgid "Error loading last commit."
+msgstr "Ошибка загрузки поÑледнего коммита."
+
+msgid "Error loading merge requests."
+msgstr ""
+
+msgid "Error loading project data. Please try again."
+msgstr "Ошибка загрузки данных проекта. ПожалуйÑта, попробуйте еще раз."
+
msgid "Error occurred when toggling the notification subscription"
msgstr "Произошла ошибка при переключении подпиÑки на оповещение"
@@ -1859,10 +2232,13 @@ msgid "Error saving label update."
msgstr "Ошибка при обновлении метки."
msgid "Error updating status for all todos."
-msgstr "Ошибка при обновлении ÑтатуÑа Ð´Ð»Ñ Ð²Ñех todo."
+msgstr "Ошибка при обновлении ÑтатуÑа Ð´Ð»Ñ Ð²Ñех дел."
msgid "Error updating todo status."
-msgstr "Ошибка при обновлении ÑтатуÑа todo."
+msgstr "Ошибка при обновлении ÑтатуÑа дела."
+
+msgid "Estimated"
+msgstr ""
msgid "EventFilterBy|Filter by all"
msgstr "Фильтр по вÑему"
@@ -1894,35 +2270,20 @@ msgstr "Еженедельно (по воÑкреÑениÑм в 4:00)"
msgid "Expand"
msgstr "Развернуть"
+msgid "Expand all"
+msgstr ""
+
+msgid "Expand sidebar"
+msgstr "Развернуть боковую панель"
+
msgid "Explore projects"
msgstr "Обзор проектов"
msgid "Explore public groups"
msgstr "ИÑÑледовать публичные группы"
-msgid "External Classification Policy Authorization"
-msgstr ""
-
-msgid "External authentication"
-msgstr ""
-
-msgid "External authorization denied access to this project"
-msgstr ""
-
-msgid "External authorization request timeout"
-msgstr ""
-
-msgid "ExternalAuthorizationService|Classification Label"
-msgstr ""
-
-msgid "ExternalAuthorizationService|Classification label"
-msgstr ""
-
-msgid "ExternalAuthorizationService|When no classification label is set the default label `%{default_label}` will be used."
-msgstr ""
-
msgid "Failed"
-msgstr ""
+msgstr "Ðеудачно"
msgid "Failed Jobs"
msgstr "Ðевыполненные ЗаданиÑ"
@@ -1930,6 +2291,9 @@ msgstr "Ðевыполненные ЗаданиÑ"
msgid "Failed to change the owner"
msgstr "Ðе удалоÑÑŒ изменить владельца"
+msgid "Failed to check related branches."
+msgstr "Ðе удалоÑÑŒ проверить ÑвÑзанные ветки."
+
msgid "Failed to remove issue from board, please try again."
msgstr "Ошибка при удалении обÑÑƒÐ¶Ð´ÐµÐ½Ð¸Ñ Ñ Ð´Ð¾Ñки, повторите попытку."
@@ -1939,6 +2303,12 @@ msgstr "Ðе удалоÑÑŒ удалить раÑпиÑание Ñборочно
msgid "Failed to update issues, please try again."
msgstr "Ошибка Ð¾Ð±Ð½Ð¾Ð²Ð»ÐµÐ½Ð¸Ñ Ð¾Ð±Ñуждений, пожалуйÑта, попробуйте Ñнова."
+msgid "Failure"
+msgstr ""
+
+msgid "Faster as it re-uses the project workspace (falling back to clone if it doesn't exist)"
+msgstr ""
+
msgid "Feb"
msgstr "Фев."
@@ -1948,18 +2318,12 @@ msgstr "Февраль"
msgid "Fields on this page are now uneditable, you can configure"
msgstr "ÐŸÐ¾Ð»Ñ Ð½Ð° Ñтой Ñтранице ÑÐµÐ¹Ñ‡Ð°Ñ Ð½ÐµÐ´Ð¾Ñтупны Ð´Ð»Ñ Ñ€ÐµÐ´Ð°ÐºÑ‚Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð¸Ñ, вы можете наÑтроить"
-msgid "File name"
-msgstr "Ð˜Ð¼Ñ Ñ„Ð°Ð¹Ð»Ð°"
-
msgid "Files"
msgstr "Файлы"
msgid "Files (%{human_size})"
msgstr "Файлов (%{human_size})"
-msgid "Fill in the fields below, turn on <strong>%{enable_label}</strong>, and press <strong>%{save_changes}</strong>"
-msgstr ""
-
msgid "Filter by commit message"
msgstr "Фильтр по комментариÑми к коммитам"
@@ -1970,7 +2334,7 @@ msgid "Find file"
msgstr "Ðайти файл"
msgid "Finished"
-msgstr ""
+msgstr "Завершено"
msgid "FirstPushedBy|First"
msgstr "Первый"
@@ -1978,10 +2342,13 @@ msgstr "Первый"
msgid "FirstPushedBy|pushed by"
msgstr "отправлено автором"
-msgid "Font Color"
+msgid "For internal projects, any logged in user can view pipelines and access job details (output logs and artifacts)"
msgstr ""
-msgid "Footer message"
+msgid "For private projects, any member (guest or higher) can view pipelines and access job details (output logs and artifacts)"
+msgstr ""
+
+msgid "For public projects, anyone can view pipelines and access job details (output logs and artifacts)"
msgstr ""
msgid "Fork"
@@ -2003,9 +2370,12 @@ msgstr "ВыполнÑетÑÑ Ð¾Ñ‚Ð²ÐµÑ‚Ð²Ð»ÐµÐ½Ð¸Ðµ"
msgid "Format"
msgstr "Формат"
-msgid "From %{provider_title}"
+msgid "Found errors in your .gitlab-ci.yml:"
msgstr ""
+msgid "From %{provider_title}"
+msgstr "Из %{provider_title}"
+
msgid "From issue creation until deploy to production"
msgstr "От ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ Ð¾Ð±ÑÑƒÐ¶Ð´ÐµÐ½Ð¸Ñ Ð´Ð¾ Ñ€Ð°Ð·Ð²ÐµÑ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ñ€ÐµÐ°Ð»Ð¸Ð·Ð°Ñ†Ð¸Ð¸ в рабочей Ñреде"
@@ -2018,167 +2388,14 @@ msgstr ""
msgid "GPG Keys"
msgstr "GPG Ключи"
-msgid "Generate a default set of labels"
-msgstr "Создать Ñтандартный набор меток"
-
-msgid "Geo Nodes"
-msgstr "ГеографичеÑкие Узлы"
-
-msgid "Geo allows you to replicate your GitLab instance to other geographical locations."
-msgstr ""
-
-msgid "GeoNodeSyncStatus|Node is failing or broken."
-msgstr "Ðа узле Ñбой или он не работает."
-
-msgid "GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage."
-msgstr "Узел функционирует медленно, перегужен или только что воÑÑтановлен поÑле ÑбоÑ."
-
-msgid "GeoNodes|Checksummed"
-msgstr ""
-
-msgid "GeoNodes|Database replication lag:"
-msgstr ""
-
-msgid "GeoNodes|Disabling a node stops the sync process. Are you sure?"
-msgstr ""
-
-msgid "GeoNodes|Does not match the primary storage configuration"
-msgstr ""
-
-msgid "GeoNodes|Failed"
-msgstr ""
-
-msgid "GeoNodes|Full"
-msgstr ""
-
-msgid "GeoNodes|GitLab version does not match the primary node version"
-msgstr ""
-
-msgid "GeoNodes|GitLab version:"
-msgstr ""
-
-msgid "GeoNodes|Health status:"
-msgstr ""
-
-msgid "GeoNodes|Last event ID processed by cursor:"
-msgstr ""
-
-msgid "GeoNodes|Last event ID seen from primary:"
-msgstr ""
-
-msgid "GeoNodes|Loading nodes"
-msgstr ""
-
-msgid "GeoNodes|Local Attachments:"
-msgstr ""
-
-msgid "GeoNodes|Local LFS objects:"
-msgstr ""
-
-msgid "GeoNodes|Local job artifacts:"
-msgstr ""
-
-msgid "GeoNodes|New node"
-msgstr ""
-
-msgid "GeoNodes|Node Authentication was successfully repaired."
-msgstr ""
-
-msgid "GeoNodes|Node was successfully removed."
-msgstr ""
-
-msgid "GeoNodes|Not checksummed"
-msgstr ""
-
-msgid "GeoNodes|Out of sync"
-msgstr ""
-
-msgid "GeoNodes|Removing a node stops the sync process. Are you sure?"
-msgstr ""
-
-msgid "GeoNodes|Replication slot WAL:"
-msgstr ""
-
-msgid "GeoNodes|Replication slots:"
-msgstr ""
-
-msgid "GeoNodes|Repositories checksummed:"
-msgstr ""
-
-msgid "GeoNodes|Repositories:"
-msgstr ""
-
-msgid "GeoNodes|Repository checksums verified:"
-msgstr ""
-
-msgid "GeoNodes|Selective"
-msgstr ""
-
-msgid "GeoNodes|Something went wrong while changing node status"
-msgstr ""
-
-msgid "GeoNodes|Something went wrong while removing node"
-msgstr ""
-
-msgid "GeoNodes|Something went wrong while repairing node"
-msgstr ""
-
-msgid "GeoNodes|Storage config:"
-msgstr ""
-
-msgid "GeoNodes|Sync settings:"
-msgstr ""
-
-msgid "GeoNodes|Synced"
-msgstr ""
-
-msgid "GeoNodes|Unused slots"
-msgstr ""
-
-msgid "GeoNodes|Unverified"
-msgstr ""
-
-msgid "GeoNodes|Used slots"
-msgstr ""
-
-msgid "GeoNodes|Verified"
-msgstr ""
-
-msgid "GeoNodes|Wiki checksums verified:"
-msgstr ""
-
-msgid "GeoNodes|Wikis checksummed:"
-msgstr ""
-
-msgid "GeoNodes|Wikis:"
-msgstr ""
-
-msgid "GeoNodes|You have configured Geo nodes using an insecure HTTP connection. We recommend the use of HTTPS."
-msgstr ""
-
-msgid "Geo|All projects"
-msgstr ""
-
-msgid "Geo|File sync capacity"
-msgstr "Объем хранилища Ð´Ð»Ñ Ñинхронизации файлов"
-
-msgid "Geo|Groups to synchronize"
-msgstr ""
-
-msgid "Geo|Projects in certain groups"
-msgstr ""
+msgid "General"
+msgstr "ОÑновныe"
-msgid "Geo|Projects in certain storage shards"
+msgid "General pipelines"
msgstr ""
-msgid "Geo|Repository sync capacity"
-msgstr "Объем хранилища Ð´Ð»Ñ Ñинхронизации репозиториÑ"
-
-msgid "Geo|Select groups to replicate."
-msgstr "Выберите группы Ð´Ð»Ñ Ñ€ÐµÐ¿Ð»Ð¸ÐºÐ°Ñ†Ð¸Ð¸."
-
-msgid "Geo|Shards to synchronize"
-msgstr ""
+msgid "Generate a default set of labels"
+msgstr "Создать Ñтандартный набор меток"
msgid "Git repository URL"
msgstr "URL-Ð°Ð´Ñ€ÐµÑ Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ð¸Ñ Git"
@@ -2189,30 +2406,36 @@ msgstr "Ð ÐµÐ²Ð¸Ð·Ð¸Ñ git"
msgid "Git storage health information has been reset"
msgstr "Ð˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð¾ ÑтабильноÑти Git хранилища была Ñброшена"
+msgid "Git strategy for pipelines"
+msgstr ""
+
msgid "Git version"
msgstr "ВерÑÐ¸Ñ Git"
msgid "GitHub import"
-msgstr ""
+msgstr "Импорт из GitHub"
msgid "GitLab CI Linter has been moved"
-msgstr ""
+msgstr "GitLab CI Linter перемещен"
-msgid "GitLab Geo"
-msgstr ""
+msgid "GitLab Group Runners can execute code for all the projects in this group."
+msgstr "Групповые Обработчики Заданий (GitLab Group Runners) могут выполнÑть код Ð´Ð»Ñ Ð²Ñех проектов в Ñтой группе."
msgid "GitLab Runner section"
msgstr "Ð¡ÐµÐºÑ†Ð¸Ñ Gitlab Runner"
-msgid "GitLab single sign on URL"
-msgstr ""
-
msgid "Gitaly"
-msgstr ""
+msgstr "Gitaly"
msgid "Gitaly Servers"
msgstr "Серверы Gitaly"
+msgid "Gitaly|Address"
+msgstr ""
+
+msgid "Go Back"
+msgstr ""
+
msgid "Go back"
msgstr "ВернутьÑÑ"
@@ -2228,22 +2451,19 @@ msgstr "ÐÑƒÑ‚ÐµÐ½Ñ‚Ð¸Ñ„Ð¸ÐºÐ°Ñ†Ð¸Ñ Google не %{link_to_documentation}. ПоÐ
msgid "Got it!"
msgstr "ПонÑтно!"
-msgid "GroupRoadmap|Epics let you manage your portfolio of projects more efficiently and with less effort"
+msgid "Graph"
msgstr ""
-msgid "GroupRoadmap|From %{dateWord}"
+msgid "Group CI/CD settings"
msgstr ""
-msgid "GroupRoadmap|Loading roadmap"
+msgid "Group ID"
msgstr ""
-msgid "GroupRoadmap|Something went wrong while fetching epics"
-msgstr ""
-
-msgid "GroupRoadmap|To view the roadmap, add a planned start or finish date to one of your epics in this group or its subgroups. Only epics in the past 3 months and the next 3 months are shown &ndash; from %{startDate} to %{endDate}."
-msgstr ""
+msgid "Group Runners"
+msgstr "Групповые Обработчики Заданий (GitLab Group Runner)"
-msgid "GroupRoadmap|Until %{dateWord}"
+msgid "Group maintainers can register group runners in the %{link}"
msgstr ""
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
@@ -2270,6 +2490,9 @@ msgstr "не может быть отменена до тех пор пока г
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr "удалить возможноÑть поделитьÑÑ Ñ Ð³Ñ€ÑƒÐ¿Ð¿Ð¾Ð²Ð¾Ð¹ блокировкой из %{ancestor_group_name}"
+msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
+msgstr ""
+
msgid "GroupsEmptyState|A group is a collection of several projects."
msgstr "Группа - Ñто набор из неÑкольких проектов."
@@ -2309,12 +2532,6 @@ msgstr "К Ñожалению, по вашему запроÑу групп не
msgid "GroupsTree|Sorry, no groups or projects matched your search"
msgstr "К Ñожалению, по вашему запроÑу групп или проектов не найдено"
-msgid "Have your users email"
-msgstr "Ð­Ð»ÐµÐºÑ‚Ñ€Ð¾Ð½Ð½Ð°Ñ Ð¿Ð¾Ñ‡Ñ‚Ð° Ð´Ð»Ñ Ð¾Ð±Ñ€Ð°Ñ‰ÐµÐ½Ð¸Ð¹ пользователей"
-
-msgid "Header message"
-msgstr ""
-
msgid "Health Check"
msgstr "Проверка работоÑпоÑобноÑти"
@@ -2334,20 +2551,23 @@ msgid "HealthCheck|Unhealthy"
msgstr "ÐеÑтабильный"
msgid "Help"
-msgstr ""
+msgstr "Помощь"
msgid "Help page"
-msgstr ""
+msgstr "Страница Ñправки"
msgid "Help page text and support page url."
-msgstr ""
+msgstr "ТекÑÑ‚ Ñтраницы Ñправки и Url-Ð°Ð´Ñ€ÐµÑ Ñтраницы поддержки."
msgid "Hide value"
msgid_plural "Hide values"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
+msgstr[0] "Скрыть значение"
+msgstr[1] "Скрыть значениÑ"
+msgstr[2] "Скрыть значениÑ"
+msgstr[3] "Скрыть значениÑ"
+
+msgid "Hide whitespace changes"
+msgstr ""
msgid "History"
msgstr "ИÑториÑ"
@@ -2355,47 +2575,77 @@ msgstr "ИÑториÑ"
msgid "Housekeeping successfully started"
msgstr "ОчиÑтка уÑпешно запущена"
-msgid "Identity provider single sign on URL"
+msgid "I accept the %{terms_link}"
msgstr ""
-msgid "If enabled, access to projects will be validated on an external service using their classification label."
+msgid "I accept the|Terms of Service and Privacy Policy"
msgstr ""
-msgid "If using GitHub, you’ll see pipeline statuses on GitHub for your commits and pull requests. %{more_info_link}"
+msgid "ID"
msgstr ""
-msgid "If you already have files you can push them using the %{link_to_cli} below."
+msgid "IDE|Commit"
+msgstr "Коммит"
+
+msgid "IDE|Edit"
+msgstr "Редактировать"
+
+msgid "IDE|Go back"
msgstr ""
+msgid "IDE|Open in file view"
+msgstr ""
+
+msgid "IDE|Review"
+msgstr "Обзор"
+
+msgid "Identifier"
+msgstr ""
+
+msgid "Identities"
+msgstr ""
+
+msgid "If disabled, the access level will depend on the user's permissions in the project."
+msgstr ""
+
+msgid "If enabled"
+msgstr ""
+
+msgid "If you already have files you can push them using the %{link_to_cli} below."
+msgstr "ЕÑли у Ð²Ð°Ñ ÑƒÐ¶Ðµ еÑть файлы иÑходных кодов вы можете отправить их Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ ÑÑылки %{link_to_cli} ниже."
+
msgid "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>."
msgstr ""
-msgid "Import"
+msgid "ImageDiffViewer|2-up"
msgstr ""
-msgid "Import all repositories"
+msgid "ImageDiffViewer|Onion skin"
+msgstr ""
+
+msgid "ImageDiffViewer|Swipe"
msgstr ""
+msgid "Import"
+msgstr "Импортировать"
+
+msgid "Import all repositories"
+msgstr "Импортировать вÑе репозитории"
+
msgid "Import in progress"
msgstr "ВыполнÑетÑÑ Ð¸Ð¼Ð¿Ð¾Ñ€Ñ‚"
msgid "Import repositories from GitHub"
-msgstr ""
+msgstr "Импорт репозиториев из GitHub"
msgid "Import repository"
msgstr "Импорт репозиториÑ"
-msgid "ImportButtons|Connect repositories from"
+msgid "Include a Terms of Service agreement and Privacy Policy that all users must accept."
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 "Inline"
+msgstr ""
msgid "Install Runner on Kubernetes"
msgstr "УÑтановить Runner на Kubernetes"
@@ -2403,21 +2653,17 @@ msgstr "УÑтановить Runner на Kubernetes"
msgid "Install a Runner compatible with GitLab CI"
msgstr "УÑтановите Gitlab Runner ÑовмеÑтимый Ñ Gitlab CI"
-msgid "Instance"
-msgid_plural "Instances"
-msgstr[0] "ЭкземплÑÑ€"
-msgstr[1] "ЭкземплÑра"
-msgstr[2] "ЭкземплÑров"
-msgstr[3] "ЭкземплÑры"
-
msgid "Instance does not support multiple Kubernetes clusters"
msgstr "ЭкземплÑÑ€ не поддерживает неÑколько клаÑтеров Kubernetes"
msgid "Integrations"
+msgstr "Интеграции"
+
+msgid "Integrations Settings"
msgstr ""
msgid "Interested parties can even contribute by pushing commits if they want to."
-msgstr ""
+msgstr "Желающие могут внеÑти Ñвой вклад, отправив коммит, еÑли захотÑÑ‚."
msgid "Internal - The group and any internal projects can be viewed by any logged in user."
msgstr "Внутренний - Группу и включённые в неё проекты может видеть любой зарегиÑтрированный пользователь."
@@ -2431,8 +2677,8 @@ msgstr "Шаблон интервала"
msgid "Introducing Cycle Analytics"
msgstr "Внедрение Цикла Ðналитик"
-msgid "Issue board focus mode"
-msgstr "Режим фокуÑировки над доÑкой обÑуждений"
+msgid "Issue Board"
+msgstr ""
msgid "Issue events"
msgstr "Ð¡Ð¾Ð±Ñ‹Ñ‚Ð¸Ñ Ð¾Ð±Ñуждений"
@@ -2440,14 +2686,11 @@ msgstr "Ð¡Ð¾Ð±Ñ‹Ñ‚Ð¸Ñ Ð¾Ð±Ñуждений"
msgid "IssueBoards|Board"
msgstr "ДоÑка"
-msgid "IssueBoards|Boards"
-msgstr "ДоÑки"
-
msgid "Issues"
msgstr "ОбÑуждениÑ"
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
-msgstr ""
+msgstr "ОбÑуждениÑми могут быть ошибки, задачи или идеи. Также, по обÑуждениÑм можно выполнÑть поиÑк и отбор."
msgid "Jan"
msgstr "Янв."
@@ -2455,6 +2698,12 @@ msgstr "Янв."
msgid "January"
msgstr "Январь"
+msgid "Job"
+msgstr ""
+
+msgid "Job has been erased"
+msgstr "Фоновое задание было удалено"
+
msgid "Jobs"
msgstr "ЗаданиÑ"
@@ -2471,7 +2720,7 @@ msgid "June"
msgstr "Июнь"
msgid "Koding"
-msgstr ""
+msgstr "Koding"
msgid "Kubernetes"
msgstr "Kubernetes"
@@ -2497,6 +2746,9 @@ msgstr "Kubernetes наÑтроен"
msgid "Kubernetes service integration has been deprecated. %{deprecated_message_content} your Kubernetes clusters using the new <a href=\"%{url}\"/>Kubernetes Clusters</a> page"
msgstr ""
+msgid "LFS"
+msgstr ""
+
msgid "LFSStatus|Disabled"
msgstr "Отключено"
@@ -2506,26 +2758,32 @@ msgstr "Включено"
msgid "Label"
msgstr "Метка"
-msgid "LabelSelect|%{firstLabelName} +%{remainingLabelCount} more"
+msgid "Label actions dropdown"
msgstr ""
+msgid "LabelSelect|%{firstLabelName} +%{remainingLabelCount} more"
+msgstr "%{firstLabelName} + ещё %{remainingLabelCount}"
+
msgid "LabelSelect|%{labelsString}, and %{remainingLabelCount} more"
-msgstr ""
+msgstr "%{labelsString} и еще %{remainingLabelCount} других"
msgid "Labels"
msgstr "Метки"
msgid "Labels can be applied to %{features}. Group labels are available for any project within the group."
-msgstr ""
+msgstr "Метки могут быть применены к %{features}. Групповые метки доÑтупны Ð´Ð»Ñ Ð»ÑŽÐ±Ð¾Ð³Ð¾ проекта внутри группы."
msgid "Labels can be applied to issues and merge requests to categorize them."
+msgstr "Метки могут применÑтьÑÑ Ðº обÑуждениÑм и запроÑам на ÑлиÑние Ð´Ð»Ñ Ð¸Ñ… категоризации."
+
+msgid "Labels can be applied to issues and merge requests."
msgstr ""
msgid "Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>"
-msgstr ""
+msgstr "<span>ПовыÑить метку</span> %{labelTitle} <span>до групповой метки?</span>"
msgid "Labels|Promote Label"
-msgstr ""
+msgstr "ПеренеÑти Метку"
msgid "Last %d day"
msgid_plural "Last %d days"
@@ -2558,11 +2816,14 @@ msgstr "Вы отправили в"
msgid "LastPushEvent|at"
msgstr "в"
+msgid "Latest changes"
+msgstr "ПоÑледние изменениÑ"
+
msgid "Learn more"
-msgstr ""
+msgstr "Подробнее"
msgid "Learn more about Kubernetes"
-msgstr ""
+msgstr "Подробнее о Kubernates"
msgid "Learn more about protected branches"
msgstr ""
@@ -2582,56 +2843,50 @@ msgstr "Покинуть группу"
msgid "Leave project"
msgstr "Покинуть проект"
-msgid "License"
-msgstr "ЛицензиÑ"
-
msgid "List"
msgstr "СпиÑок"
msgid "List your GitHub repositories"
-msgstr ""
+msgstr "СпиÑок ваших репозиториев на GitHub"
msgid "Loading the GitLab IDE..."
msgstr "Загрузка GitLab IDE..."
+msgid "Loading..."
+msgstr "Загрузка..."
+
msgid "Lock"
msgstr "Блокировка"
msgid "Lock %{issuableDisplayName}"
-msgstr ""
+msgstr "Заблокировать %{issuableDisplayName}"
msgid "Lock not found"
+msgstr "Блокировка не найдена"
+
+msgid "Lock to current projects"
msgstr ""
msgid "Locked"
msgstr "Заблокировано"
-msgid "Locked Files"
-msgstr "Заблокированные Файлы"
-
-msgid "Locks give the ability to lock specific file or folder."
+msgid "Locked to current projects"
msgstr ""
msgid "Login"
msgstr "Войти"
-msgid "Make everyone on your team more productive regardless of their location. GitLab Geo creates read-only mirrors of your GitLab instance so you can reduce the time it takes to clone and fetch large repos."
-msgstr ""
-
msgid "Manage all notifications"
-msgstr ""
+msgstr "Управление уведомлениÑми"
msgid "Manage group labels"
-msgstr ""
+msgstr "Управление метками группы"
msgid "Manage labels"
-msgstr ""
+msgstr "Управление метками"
msgid "Manage project labels"
-msgstr ""
-
-msgid "Manage your group’s membership while adding another level of security with SAML."
-msgstr ""
+msgstr "Управление метками проекта"
msgid "Mar"
msgstr "Мар."
@@ -2639,8 +2894,11 @@ msgstr "Мар."
msgid "March"
msgstr "Март"
-msgid "Mark done"
-msgstr ""
+msgid "Mark todo as done"
+msgstr "Отметить как Ñделанное"
+
+msgid "Markdown enabled"
+msgstr "Включен режим Markdown"
msgid "Maximum git storage failures"
msgstr "МакÑимальное количеÑтво Ñбоев хранилища git"
@@ -2654,8 +2912,8 @@ msgstr "Среднее"
msgid "Members"
msgstr "УчаÑтники"
-msgid "Members will be forwarded here when signing in to your group. Get this from your identity provider, where it can also be called \"SSO Service Location\", \"SAML Token Issuance Endpoint\", or \"SAML 2.0/W-Federation URL\"."
-msgstr ""
+msgid "Merge Request:"
+msgstr "Ð—Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние:"
msgid "Merge Requests"
msgstr "ЗапроÑÑ‹ на СлиÑние"
@@ -2666,145 +2924,115 @@ msgstr "Ð¡Ð¾Ð±Ñ‹Ñ‚Ð¸Ñ ÑлиÑний"
msgid "Merge request"
msgstr "Ð—Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние"
-msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
-msgstr ""
-
-msgid "Merged"
-msgstr ""
-
-msgid "Messages"
-msgstr "СообщениÑ"
-
-msgid "Metrics - Influx"
-msgstr ""
-
-msgid "Metrics - Prometheus"
-msgstr ""
+msgid "Merge requests"
+msgstr "ЗапроÑÑ‹ на ÑлиÑние"
-msgid "Metrics|Business"
-msgstr ""
-
-msgid "Metrics|Create metric"
-msgstr ""
-
-msgid "Metrics|Edit metric"
-msgstr ""
-
-msgid "Metrics|For grouping similar metrics"
-msgstr ""
-
-msgid "Metrics|Label of the chart's vertical axis. Usually the type of the unit being charted. The horizontal axis (X-axis) always represents time."
-msgstr ""
-
-msgid "Metrics|Legend label (optional)"
-msgstr ""
-
-msgid "Metrics|Must be a valid PromQL query."
-msgstr ""
-
-msgid "Metrics|Name"
-msgstr ""
-
-msgid "Metrics|New metric"
-msgstr ""
-
-msgid "Metrics|Prometheus Query Documentation"
-msgstr ""
-
-msgid "Metrics|Query"
-msgstr ""
-
-msgid "Metrics|Response"
-msgstr ""
-
-msgid "Metrics|System"
-msgstr ""
+msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
+msgstr "ЗапроÑÑ‹ на ÑлиÑние- Ñто меÑто, где можно предлагать Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð²Ð½Ð¾Ñимые в проект, и обÑуждать Ñти Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ñ Ð´Ñ€ÑƒÐ³Ð¸Ð¼Ð¸"
-msgid "Metrics|Type"
+msgid "MergeRequests|Resolve this discussion in a new issue"
msgstr ""
-msgid "Metrics|Unit label"
+msgid "MergeRequests|Saving the comment failed"
msgstr ""
-msgid "Metrics|Used as a title for the chart"
+msgid "MergeRequests|Toggle comments for this file"
msgstr ""
-msgid "Metrics|Used if the query returns a single series. If it returns multiple series, their legend labels will be picked up from the response."
+msgid "MergeRequests|Updating discussions failed"
msgstr ""
-msgid "Metrics|Y-axis label"
+msgid "MergeRequests|View file @ %{commitId}"
msgstr ""
-msgid "Metrics|e.g. HTTP requests"
+msgid "MergeRequests|View replaced file @ %{commitId}"
msgstr ""
-msgid "Metrics|e.g. Requests/second"
-msgstr ""
+msgid "Merged"
+msgstr "Слито"
-msgid "Metrics|e.g. Throughput"
-msgstr ""
+msgid "Messages"
+msgstr "СообщениÑ"
-msgid "Metrics|e.g. rate(http_requests_total[5m])"
-msgstr ""
+msgid "Metrics - Influx"
+msgstr "Метрики - Influx"
-msgid "Metrics|e.g. req/sec"
-msgstr ""
+msgid "Metrics - Prometheus"
+msgstr "Метрики - Prometheus"
msgid "Milestone"
-msgstr ""
+msgstr "Этап"
+
+msgid "Milestones"
+msgstr "Этапы"
msgid "Milestones|Delete milestone"
-msgstr ""
+msgstr "Удалить Ñтап"
msgid "Milestones|Delete milestone %{milestoneTitle}?"
-msgstr ""
+msgstr "Удалить Ñтап %{milestoneTitle}?"
msgid "Milestones|Failed to delete milestone %{milestoneTitle}"
-msgstr ""
+msgstr "Ðе удалоÑÑŒ удалить Ñтап %{milestoneTitle}"
msgid "Milestones|Milestone %{milestoneTitle} was not found"
-msgstr ""
+msgstr "Этап %{milestoneTitle} не найден"
msgid "Milestones|Promote %{milestoneTitle} to group milestone?"
-msgstr ""
+msgstr "ПовыÑить %{milestoneTitle} до группового Ñтапа?"
msgid "Milestones|Promote Milestone"
-msgstr ""
-
-msgid "Milestones|This action cannot be reversed."
-msgstr ""
+msgstr "ПовыÑить Ñтап"
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "добавить ключ SSH"
msgid "Modal|Cancel"
-msgstr ""
+msgstr "Отмена"
msgid "Modal|Close"
-msgstr ""
+msgstr "Закрыть"
msgid "Monitoring"
msgstr "Мониторинг"
-msgid "More info"
+msgid "More actions"
msgstr ""
msgid "More information"
-msgstr ""
+msgstr "Ð”Ð¾Ð¿Ð¾Ð»Ð½Ð¸Ñ‚ÐµÐ»ÑŒÐ½Ð°Ñ Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ"
msgid "More information is available|here"
msgstr "Больше информации доÑтупно|тут"
msgid "Move"
-msgstr ""
+msgstr "ПеремеÑтить"
msgid "Move issue"
-msgstr ""
+msgstr "ПеремеÑтить обÑуждение"
-msgid "Multiple issue boards"
-msgstr "Сводные доÑки обÑуждений"
+msgid "Name"
+msgstr "ИмÑ"
msgid "Name new label"
+msgstr "Ðазвать новую метку"
+
+msgid "Name your individual key via a title"
+msgstr ""
+
+msgid "Nav|Help"
+msgstr "Помощь"
+
+msgid "Nav|Home"
+msgstr "ГлавнаÑ"
+
+msgid "Nav|Sign In / Register"
+msgstr "Вход / РегиÑтрациÑ"
+
+msgid "Nav|Sign out and sign in with a different account"
+msgstr ""
+
+msgid "New Identity"
msgstr ""
msgid "New Issue"
@@ -2820,6 +3048,9 @@ msgstr ""
msgid "New Kubernetes cluster"
msgstr ""
+msgid "New Label"
+msgstr ""
+
msgid "New Pipeline Schedule"
msgstr "Ðовое РаÑпиÑание Сборочной Линии"
@@ -2832,24 +3063,27 @@ msgstr "ÐÐ¾Ð²Ð°Ñ Ð²ÐµÑ‚ÐºÐ° недоÑтупна"
msgid "New directory"
msgstr "Ðовый каталог"
-msgid "New epic"
-msgstr "Ðовый Ñпик"
-
msgid "New file"
msgstr "Ðовый файл"
msgid "New group"
msgstr "ÐÐ¾Ð²Ð°Ñ Ð³Ñ€ÑƒÐ¿Ð¿Ð°"
+msgid "New identity"
+msgstr ""
+
msgid "New issue"
msgstr "Ðовое обÑуждение"
msgid "New label"
-msgstr ""
+msgstr "ÐÐ¾Ð²Ð°Ñ Ð¼ÐµÑ‚ÐºÐ°"
msgid "New merge request"
msgstr "Ðовый Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние"
+msgid "New pipelines will cancel older, pending pipelines on the same branch"
+msgstr ""
+
msgid "New project"
msgstr "Ðовый проект"
@@ -2865,28 +3099,37 @@ msgstr "ÐÐ¾Ð²Ð°Ñ Ð¿Ð¾Ð´Ð³Ñ€ÑƒÐ¿Ð¿Ð°"
msgid "New tag"
msgstr "Ðовый тег"
-msgid "No Label"
-msgstr ""
+msgid "No"
+msgstr "Ðет"
msgid "No assignee"
-msgstr ""
+msgstr "Ðет ответÑтвенного"
msgid "No changes"
-msgstr ""
+msgstr "Ðет изменений"
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
msgid "No due date"
-msgstr ""
+msgstr "Плановый Ñрок не указан"
msgid "No estimate or time spent"
msgstr ""
msgid "No file chosen"
+msgstr "Файл не выбран"
+
+msgid "No files found"
msgstr ""
-msgid "No labels created yet."
+msgid "No files found."
+msgstr "Файлы не найдены."
+
+msgid "No merge requests found"
+msgstr ""
+
+msgid "No messages were logged"
msgstr ""
msgid "No repository"
@@ -2899,35 +3142,29 @@ msgid "None"
msgstr "ПуÑто"
msgid "Not allowed to merge"
-msgstr ""
+msgstr "СлиÑние не допуÑкаетÑÑ"
msgid "Not available"
msgstr "ÐедоÑтупно"
msgid "Not available for private projects"
-msgstr ""
+msgstr "ÐедоÑтупно Ð´Ð»Ñ Ð¿Ñ€Ð¸Ð²Ð°Ñ‚Ð½Ñ‹Ñ… проектов"
msgid "Not available for protected branches"
-msgstr ""
+msgstr "ÐедоÑтупно Ð´Ð»Ñ Ð·Ð°Ñ‰Ð¸Ñ‰ÐµÐ½Ð½Ñ‹Ñ… веток"
msgid "Not confidential"
-msgstr ""
+msgstr "Ðе конфиденциально"
msgid "Not enough data"
msgstr "ÐедоÑтаточно данных"
msgid "Note that the master branch is automatically protected. %{link_to_protected_branches}"
-msgstr ""
-
-msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow connecting repositories without generating a Personal Access Token."
-msgstr ""
+msgstr "Обратите внимание, что маÑтер ветка автоматичеÑки защищена. %{link_to_protected_branches}"
msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
msgstr ""
-msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow connecting repositories without generating a Personal Access Token."
-msgstr ""
-
msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
msgstr ""
@@ -2989,10 +3226,10 @@ msgid "Notifications"
msgstr "УведомлениÑ"
msgid "Notifications off"
-msgstr ""
+msgstr "Ð£Ð²ÐµÐ´Ð¾Ð¼Ð»ÐµÐ½Ð¸Ñ Ð¾Ñ‚ÐºÐ»ÑŽÑ‡ÐµÐ½Ñ‹"
msgid "Notifications on"
-msgstr ""
+msgstr "Ð£Ð²ÐµÐ´Ð¾Ð¼Ð»ÐµÐ½Ð¸Ñ Ð²ÐºÐ»ÑŽÑ‡ÐµÐ½Ñ‹"
msgid "Nov"
msgstr "ÐоÑб."
@@ -3003,9 +3240,6 @@ msgstr "ÐоÑбрь"
msgid "Number of access attempts"
msgstr "КоличеÑтво попыток доÑтупа"
-msgid "OK"
-msgstr ""
-
msgid "Oct"
msgstr "Окт."
@@ -3015,36 +3249,42 @@ msgstr "ОктÑбрь"
msgid "OfSearchInADropdown|Filter"
msgstr "Фильтр"
-msgid "Once imported, repositories can be mirrored over SSH. Read more %{ssh_link}"
+msgid "Online IDE integration settings."
msgstr ""
-msgid "Online IDE integration settings."
+msgid "Only comments from the following commit are shown below"
msgstr ""
msgid "Only project members can comment."
msgstr "Только учаÑтники проекта могут оÑтавлÑть комментарии."
-msgid "Open"
+msgid "Open in Xcode"
msgstr ""
-msgid "Opened"
-msgstr "Открыт"
-
msgid "OpenedNDaysAgo|Opened"
msgstr "Открыто"
msgid "Opens in a new window"
msgstr "ОткроетÑÑ Ð² новом окне"
+msgid "Operations"
+msgstr "Операции"
+
msgid "Options"
msgstr "ÐаÑтройки"
-msgid "Otherwise it is recommended you start with one of the options below."
+msgid "Or you can choose one of the suggested colors below"
msgstr ""
-msgid "Outbound requests"
+msgid "Other Labels"
msgstr ""
+msgid "Otherwise it is recommended you start with one of the options below."
+msgstr "Ð’ противном Ñлучае рекомендуетÑÑ Ð½Ð°Ñ‡Ð°Ñ‚ÑŒ Ñ Ð¾Ð´Ð½Ð¾Ð³Ð¾ из перечиÑленных ниже вариантов."
+
+msgid "Outbound requests"
+msgstr "ИÑходÑщие запроÑÑ‹"
+
msgid "Overview"
msgstr "Обзор"
@@ -3052,7 +3292,7 @@ msgid "Owner"
msgstr "Владелец"
msgid "Pages"
-msgstr ""
+msgstr "Страницы"
msgid "Pagination|Last »"
msgstr "ПоÑледнÑÑ Â»"
@@ -3067,19 +3307,34 @@ msgid "Pagination|« First"
msgstr "« ПерваÑ"
msgid "Part of merge request changes"
-msgstr ""
+msgstr "ЧаÑть изменений запроÑа на ÑлиÑние"
msgid "Password"
msgstr "Пароль"
+msgid "Paste your public SSH key, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'. Don't use your private SSH key."
+msgstr ""
+
+msgid "Pause"
+msgstr "ПриоÑтановить"
+
msgid "Pending"
+msgstr "В ожидании"
+
+msgid "Per job. If a job passes this threshold, it will be marked as failed"
msgstr ""
-msgid "Performance optimization"
+msgid "Perform advanced options such as changing path, transferring, or removing the group."
msgstr ""
+msgid "Performance optimization"
+msgstr "ÐžÐ¿Ñ‚Ð¸Ð¼Ð¸Ð·Ð°Ñ†Ð¸Ñ Ð¿Ñ€Ð¾Ð¸Ð·Ð²Ð¾Ð´Ð¸Ñ‚ÐµÐ»ÑŒÐ½Ð¾Ñти"
+
+msgid "Permissions"
+msgstr "Права доÑтупа"
+
msgid "Personal Access Token"
-msgstr ""
+msgstr "ПерÑональный Токен ДоÑтупа"
msgid "Pipeline"
msgstr "Ð¡Ð±Ð¾Ñ€Ð¾Ñ‡Ð½Ð°Ñ Ð»Ð¸Ð½Ð¸Ñ"
@@ -3093,8 +3348,8 @@ msgstr "РаÑпиÑание Сборочной Линии"
msgid "Pipeline Schedules"
msgstr "РаÑпиÑÐ°Ð½Ð¸Ñ Ð¡Ð±Ð¾Ñ€Ð¾Ñ‡Ð½Ñ‹Ñ… Линий"
-msgid "Pipeline quota"
-msgstr "Квота Ñборочной линии"
+msgid "Pipeline triggers"
+msgstr ""
msgid "PipelineCharts|Failed:"
msgstr "Ðеудача:"
@@ -3169,46 +3424,58 @@ msgid "Pipelines|Clear Runner Caches"
msgstr ""
msgid "Pipelines|Get started with Pipelines"
-msgstr ""
+msgstr "Ðачало работы Ñо Ñборочными линиÑми"
msgid "Pipelines|Loading Pipelines"
-msgstr ""
+msgstr "ЗагружаютÑÑ Ñборочные линии"
msgid "Pipelines|Project cache successfully reset."
-msgstr ""
+msgstr "КÑш проекта уÑпешно очищен."
msgid "Pipelines|Run Pipeline"
-msgstr ""
+msgstr "ЗапуÑтить Ñборочную линию"
msgid "Pipelines|Something went wrong while cleaning runners cache."
-msgstr ""
+msgstr "Что-то пошло не так, при очиÑтке кÑша обработчиков заданий."
msgid "Pipelines|There are currently no %{scope} pipelines."
-msgstr ""
+msgstr "Ð’ наÑтоÑщее Ð²Ñ€ÐµÐ¼Ñ Ð½ÐµÑ‚ %{scope} Ñборочных линий."
msgid "Pipelines|There are currently no pipelines."
-msgstr ""
+msgstr "Ð’ наÑтоÑщее Ð²Ñ€ÐµÐ¼Ñ Ð½ÐµÑ‚ Ñборочных линий."
msgid "Pipelines|This project is not currently set up to run pipelines."
+msgstr "Этот проект в наÑтоÑщее Ð²Ñ€ÐµÐ¼Ñ Ð½Ðµ наÑтроен Ð´Ð»Ñ Ð·Ð°Ð¿ÑƒÑка Ñборочных линий."
+
+msgid "Pipeline|Create for"
msgstr ""
-msgid "Pipeline|Retry pipeline"
+msgid "Pipeline|Create pipeline"
msgstr ""
-msgid "Pipeline|Retry pipeline #%{pipelineId}?"
+msgid "Pipeline|Existing branch name or tag"
msgstr ""
-msgid "Pipeline|Stop pipeline"
+msgid "Pipeline|Run Pipeline"
+msgstr "ЗапуÑтить Ñборочную линию"
+
+msgid "Pipeline|Search branches"
+msgstr "ПоиÑк веток"
+
+msgid "Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default."
msgstr ""
+msgid "Pipeline|Stop pipeline"
+msgstr "ОÑтановить Ñборочную линию"
+
msgid "Pipeline|Stop pipeline #%{pipelineId}?"
-msgstr ""
+msgstr "ОÑтановить Ñборочную линию #%{pipelineId}?"
-msgid "Pipeline|You’re about to retry pipeline %{pipelineId}."
+msgid "Pipeline|Variables"
msgstr ""
msgid "Pipeline|You’re about to stop pipeline %{pipelineId}."
-msgstr ""
+msgstr "Ð’Ñ‹ ÑобираетеÑÑŒ оÑтановить Ñборочную линию %{pipelineId}."
msgid "Pipeline|all"
msgstr "вÑе"
@@ -3222,28 +3489,46 @@ msgstr "Ñо Ñтадией"
msgid "Pipeline|with stages"
msgstr "Ñо ÑтадиÑми"
-msgid "PlantUML"
+msgid "Plain diff"
msgstr ""
+msgid "PlantUML"
+msgstr "PlantUML"
+
msgid "Play"
-msgstr ""
+msgstr "ЗапуÑк"
-msgid "Please <a href=%{link_to_billing} target=\"_blank\" rel=\"noopener noreferrer\">enable billing for one of your projects to be able to create a Kubernetes cluster</a>, then try again."
+msgid "Please accept the Terms of Service before continuing."
msgstr ""
+msgid "Please select at least one filter to see results"
+msgstr "ПожалуйÑта, выберите по крайней мере один фильтр, чтобы увидеть результаты"
+
msgid "Please solve the reCAPTCHA"
msgstr "ПожалуйÑта, решите reCAPTCHA"
-msgid "Please wait while we connect to your repository. Refresh at will."
+msgid "Please try again"
msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
-msgstr ""
+msgstr "ПожалуйÑта, подождите пока мы импортируем к ваш репозиторий. ОбновлÑйте Ñтраницу по желанию."
msgid "Preferences"
msgstr "ПредпочтениÑ"
-msgid "Primary"
+msgid "Preferences|Navigation theme"
+msgstr ""
+
+msgid "Prioritize"
+msgstr ""
+
+msgid "Prioritize label"
+msgstr ""
+
+msgid "Prioritized Labels"
+msgstr ""
+
+msgid "Prioritized label"
msgstr ""
msgid "Private - Project access must be granted explicitly to each user."
@@ -3253,7 +3538,7 @@ msgid "Private - The group and its projects can only be viewed by members."
msgstr "ÐŸÑ€Ð¸Ð²Ð°Ñ‚Ð½Ð°Ñ - Группу и включённые в неё проекты могут видеть только члены группы."
msgid "Private projects can be created in your personal namespace with:"
-msgstr ""
+msgstr "Личные проекты могут быть Ñозданы в вашем перÑональном проÑтранÑтве Ñ:"
msgid "Profile"
msgstr "Профиль"
@@ -3261,6 +3546,12 @@ msgstr "Профиль"
msgid "Profiles|Account scheduled for removal."
msgstr "Ð£Ñ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ запланирована к удалению."
+msgid "Profiles|Change username"
+msgstr "Изменить Ð¸Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ"
+
+msgid "Profiles|Current path: %{path}"
+msgstr "Текущий путь: %{path}"
+
msgid "Profiles|Delete Account"
msgstr "Удалить Учетную запиÑÑŒ"
@@ -3279,9 +3570,21 @@ msgstr "Ðеверный пароль"
msgid "Profiles|Invalid username"
msgstr "Ðеверное Ð¸Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ"
+msgid "Profiles|Path"
+msgstr "Путь"
+
msgid "Profiles|Type your %{confirmationValue} to confirm:"
msgstr "Введите значение %{confirmationValue} Ð´Ð»Ñ Ð¿Ð¾Ð´Ñ‚Ð²ÐµÑ€Ð¶Ð´ÐµÐ½Ð¸Ñ:"
+msgid "Profiles|Update username"
+msgstr "Обновить Ð¸Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ"
+
+msgid "Profiles|Username change failed - %{message}"
+msgstr "Ошибка Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð¸Ð¼ÐµÐ½Ð¸ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ - %{message}"
+
+msgid "Profiles|Username successfully changed"
+msgstr "Ð˜Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ ÑƒÑпешно изменено"
+
msgid "Profiles|You don't have access to delete this user."
msgstr "У Ð²Ð°Ñ Ð½ÐµÑ‚ прав на удаление Ñтого пользователÑ."
@@ -3295,11 +3598,17 @@ msgid "Profiles|your account"
msgstr "ваша ÑƒÑ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ"
msgid "Profiling - Performance bar"
-msgstr ""
+msgstr "Профилирование - панель производительноÑти"
msgid "Programming languages used in this repository"
+msgstr "Языки программированиÑ, иÑпользуемые в Ñтом репозитории"
+
+msgid "Progress"
msgstr ""
+msgid "Project"
+msgstr "Проект"
+
msgid "Project '%{project_name}' is in the process of being deleted."
msgstr "Проект '%{project_name}' находитÑÑ Ð² процеÑÑе удалениÑ."
@@ -3312,6 +3621,9 @@ msgstr "Проект '%{project_name}' уÑпешно Ñоздан."
msgid "Project '%{project_name}' was successfully updated."
msgstr "Проект '%{project_name}' уÑпешно обновлен."
+msgid "Project Badges"
+msgstr "Значки Проекта"
+
msgid "Project access must be granted explicitly to each user."
msgstr "ДоÑтуп к проекту должен предоÑтавлÑтьÑÑ Ñвно каждому пользователю."
@@ -3339,30 +3651,6 @@ msgstr "Ðачат ÑкÑпорт проекта. СÑылка Ð´Ð»Ñ Ñкачи
msgid "ProjectActivityRSS|Subscribe"
msgstr "ПодпиÑатьÑÑ"
-msgid "ProjectCreationLevel|Allowed to create projects"
-msgstr ""
-
-msgid "ProjectCreationLevel|Default project creation protection"
-msgstr ""
-
-msgid "ProjectCreationLevel|Developers + Masters"
-msgstr ""
-
-msgid "ProjectCreationLevel|Masters"
-msgstr ""
-
-msgid "ProjectCreationLevel|No one"
-msgstr ""
-
-msgid "ProjectFeature|Disabled"
-msgstr "Отключено"
-
-msgid "ProjectFeature|Everyone with access"
-msgstr "Ð’Ñе Ñ Ð´Ð¾Ñтупом"
-
-msgid "ProjectFeature|Only team members"
-msgstr "Только члены команды"
-
msgid "ProjectFileTree|Name"
msgstr "Ðаименование"
@@ -3372,27 +3660,6 @@ msgstr "Ðикогда"
msgid "ProjectLifecycle|Stage"
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 "Проекты"
@@ -3417,11 +3684,14 @@ msgstr "К Ñожалению, по вашему запроÑу проекты Ð
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr "Эта функциональноÑть требует поддержки localStorage в вашем браузере"
+msgid "PrometheusDashboard|Time"
+msgstr "ВремÑ"
+
msgid "PrometheusService|%{exporters} with %{metrics} were found"
-msgstr ""
+msgstr "Были найдены %{exporters} Ñ %{metrics}"
msgid "PrometheusService|<p class=\"text-tertiary\">No <a href=\"%{docsUrl}\">common metrics</a> were found</p>"
-msgstr ""
+msgstr "<p class=\"text-tertiary\">Ðи одной <a href=\"%{docsUrl}\">общей метрики</a> не найдено</p>"
msgid "PrometheusService|Active"
msgstr ""
@@ -3438,18 +3708,9 @@ msgstr "По умолчанию, Prometheus запуÑкаетÑÑ Ð¿Ð¾ адре
msgid "PrometheusService|Common metrics"
msgstr ""
-msgid "PrometheusService|Common metrics are automatically monitored based on a library of metrics from popular exporters."
-msgstr ""
-
-msgid "PrometheusService|Custom metrics"
-msgstr ""
-
msgid "PrometheusService|Finding and configuring metrics..."
msgstr "Определение и наÑтройка метрик..."
-msgid "PrometheusService|Finding custom metrics..."
-msgstr ""
-
msgid "PrometheusService|Install Prometheus on clusters"
msgstr ""
@@ -3462,26 +3723,23 @@ msgstr ""
msgid "PrometheusService|Metrics"
msgstr "Метрики"
+msgid "PrometheusService|Metrics are automatically configured and monitored based on a library of metrics from popular exporters."
+msgstr ""
+
msgid "PrometheusService|Missing environment variable"
msgstr "Пропущена Ð¿ÐµÑ€ÐµÐ¼ÐµÐ½Ð½Ð°Ñ Ð¾ÐºÑ€ÑƒÐ¶ÐµÐ½Ð¸Ñ"
msgid "PrometheusService|More information"
msgstr "Ð”Ð¾Ð¿Ð¾Ð»Ð½Ð¸Ñ‚ÐµÐ»ÑŒÐ½Ð°Ñ Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ"
-msgid "PrometheusService|New metric"
-msgstr ""
-
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr "Базовый Ð°Ð´Ñ€ÐµÑ Prometheus API, например http://prometheus.example.com/"
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
-msgstr ""
-
msgid "PrometheusService|Time-series monitoring service"
-msgstr ""
+msgstr "Служба мониторинга временных Ñ€Ñдов"
msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
msgstr ""
@@ -3495,13 +3753,19 @@ msgstr ""
msgid "Promote"
msgstr ""
-msgid "Promote to Group Label"
+msgid "Promote these project milestones into a group milestone."
msgstr ""
msgid "Promote to Group Milestone"
+msgstr "ПовыÑить до Группового Этапа"
+
+msgid "Promote to group label"
msgstr ""
msgid "Protip:"
+msgstr "ПодÑказка:"
+
+msgid "Provider"
msgstr ""
msgid "Public - The group and any public projects can be viewed without any authentication."
@@ -3510,8 +3774,8 @@ msgstr "Публичный - Группу и включённые в неё пр
msgid "Public - The project can be accessed without any authentication."
msgstr "Публичный - ДоÑтуп к проекту возможен без какой-либо проверки подлинноÑти."
-msgid "Push Rules"
-msgstr "Правила Отправки"
+msgid "Public pipelines"
+msgstr ""
msgid "Push events"
msgstr "Ð¡Ð¾Ð±Ñ‹Ñ‚Ð¸Ñ Ð¾Ñ‚Ð¿Ñ€Ð°Ð²ÐºÐ¸"
@@ -3522,10 +3786,10 @@ msgstr ""
msgid "Push to create a project"
msgstr ""
-msgid "PushRule|Committer restriction"
-msgstr "ÐžÐ³Ñ€Ð°Ð½Ð¸Ñ‡ÐµÐ½Ð¸Ñ Ð´Ð»Ñ ÐºÐ¾Ð¼Ð¼Ð¸Ñ‚ÐµÑ€Ð°"
-
msgid "Quick actions can be used in the issues description and comment boxes."
+msgstr "БыÑтрые дейÑÑ‚Ð²Ð¸Ñ Ð¼Ð¾Ð³ÑƒÑ‚ иÑпользоватьÑÑ Ð² опиÑании обÑуждений и комментариÑÑ…."
+
+msgid "Re-deploy"
msgstr ""
msgid "Read more"
@@ -3537,16 +3801,16 @@ msgstr "ИнÑтрукциÑ"
msgid "Real-time features"
msgstr ""
-msgid "RefSwitcher|Branches"
-msgstr "Ветки"
-
-msgid "RefSwitcher|Tags"
-msgstr "Теги"
-
msgid "Reference:"
-msgstr ""
+msgstr "СÑылка:"
msgid "Register / Sign In"
+msgstr "РегиÑÑ‚Ñ€Ð°Ñ†Ð¸Ñ / Вход"
+
+msgid "Register and see your runners for this group."
+msgstr "ЗарегиÑтрируйте и проÑмотрите ваши обработчики заданий Ð´Ð»Ñ Ñтой группы."
+
+msgid "Register and see your runners for this project."
msgstr ""
msgid "Registry"
@@ -3577,30 +3841,30 @@ msgid "Remind later"
msgstr "Ðапомнить позже"
msgid "Remove"
+msgstr "Удалить"
+
+msgid "Remove Runner"
msgstr ""
msgid "Remove avatar"
+msgstr "Удалить аватар"
+
+msgid "Remove priority"
msgstr ""
msgid "Remove project"
msgstr "Удалить проект"
-msgid "Repair authentication"
-msgstr ""
-
-msgid "Repo by URL"
-msgstr ""
-
msgid "Repository"
msgstr "Репозиторий"
-msgid "Repository has no locks."
+msgid "Repository Settings"
msgstr ""
msgid "Repository maintenance"
msgstr ""
-msgid "Repository mirror settings"
+msgid "Repository mirror"
msgstr ""
msgid "Repository storage"
@@ -3609,6 +3873,9 @@ msgstr ""
msgid "Request Access"
msgstr "Ð—Ð°Ð¿Ñ€Ð¾Ñ Ð´Ð¾Ñтупа"
+msgid "Require all users to accept Terms of Service and Privacy Policy when they access GitLab."
+msgstr ""
+
msgid "Reset git storage health information"
msgstr "СброÑить информацию о работоÑпоÑобноÑти Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ð¸Ñ git"
@@ -3616,20 +3883,35 @@ msgid "Reset health check access token"
msgstr "СброÑить ключ доÑтупа проверки работоÑпоÑобноÑти"
msgid "Reset runners registration token"
-msgstr "СброÑить ключ региÑтрации Gitlab Runners"
+msgstr "СброÑить ключ региÑтрации обработчиков заданий"
+
+msgid "Resolve all discussions in new issue"
+msgstr ""
+
+msgid "Resolve conflicts on source branch"
+msgstr ""
msgid "Resolve discussion"
msgstr "Закрыть диÑкуÑÑию"
-msgid "Response"
+msgid "Resume"
+msgstr ""
+
+msgid "Retry"
+msgstr "Повторить"
+
+msgid "Retry this job"
+msgstr "Повторить Ñто фоновое задание"
+
+msgid "Retry verification"
msgstr ""
msgid "Reveal value"
msgid_plural "Reveal values"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
+msgstr[0] "Показать значение"
+msgstr[1] "Показать значениÑ"
+msgstr[2] "Показать значениÑ"
+msgstr[3] "Показать значениÑ"
msgid "Revert this commit"
msgstr "Отменить Ñто коммит"
@@ -3637,7 +3919,7 @@ msgstr "Отменить Ñто коммит"
msgid "Revert this merge request"
msgstr "Отменить Ñтот Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние"
-msgid "Review the process for configuring service providers in your identity provider — in this case, GitLab is the \"service provider\" or \"relying party\"."
+msgid "Review"
msgstr ""
msgid "Reviewing"
@@ -3646,30 +3928,33 @@ msgstr ""
msgid "Reviewing (merge request !%{mergeRequestId})"
msgstr ""
-msgid "Roadmap"
+msgid "Rollback"
msgstr ""
-msgid "Run CI/CD pipelines for external repositories"
+msgid "Runner token"
msgstr ""
msgid "Runners"
-msgstr ""
+msgstr "Обработчики заданий"
+
+msgid "Runners API"
+msgstr "API обработчиков заданий"
+
+msgid "Runners can be placed on separate users, servers, and even on your local machine."
+msgstr "Обработчики заданий могут запуÑкатьÑÑ Ñƒ отдельных пользователей, Ñерверах и даже на вашей локальной машине."
msgid "Running"
-msgstr ""
+msgstr "ВыполнÑетÑÑ"
-msgid "SAML Single Sign On"
-msgstr ""
+msgid "SSH Keys"
+msgstr "SSH Ключи"
-msgid "SAML Single Sign On Settings"
+msgid "SSL Verification"
msgstr ""
-msgid "SHA1 fingerprint of the SAML token signing certificate. Get this from your identity provider, where it can also be called \"Thumbprint\"."
+msgid "Save"
msgstr ""
-msgid "SSH Keys"
-msgstr "SSH Ключи"
-
msgid "Save changes"
msgstr "Сохранить изменениÑ"
@@ -3677,13 +3962,13 @@ msgid "Save pipeline schedule"
msgstr "Сохранить раÑпиÑание Ñборочной лини"
msgid "Save variables"
-msgstr ""
+msgstr "Сохранить переменные"
msgid "Schedule a new pipeline"
msgstr "РаÑпиÑание новой Ñборочной линии"
msgid "Scheduled"
-msgstr ""
+msgstr "Запланировано"
msgid "Schedules"
msgstr "РаÑпиÑаниÑ"
@@ -3691,60 +3976,84 @@ msgstr "РаÑпиÑаниÑ"
msgid "Scheduling Pipelines"
msgstr "Планирование Сборочных Линий"
-msgid "Scoped issue boards"
-msgstr "ТематичеÑкие доÑки обÑуждений"
+msgid "Scroll to bottom"
+msgstr ""
+
+msgid "Scroll to top"
+msgstr ""
msgid "Search"
+msgstr "ПоиÑк"
+
+msgid "Search branches"
msgstr ""
msgid "Search branches and tags"
msgstr "Ðайти ветки и теги"
-msgid "Search milestones"
+msgid "Search files"
msgstr ""
-msgid "Search project"
+msgid "Search for projects, issues, etc."
msgstr ""
-msgid "Search users"
+msgid "Search merge requests"
msgstr ""
+msgid "Search milestones"
+msgstr "ПоиÑк Ñтапов"
+
+msgid "Search project"
+msgstr "ПоиÑк проекта"
+
+msgid "Search users"
+msgstr "ПоиÑк пользователей"
+
msgid "Seconds before reseting failure information"
msgstr "Секунд до очиÑтки информации о ÑбоÑÑ…"
msgid "Seconds to wait for a storage access attempt"
msgstr "Секунд задержки между попытками доÑтупа к хранилищу"
-msgid "Secret variables"
-msgstr ""
-
-msgid "Security report"
+msgid "Select"
msgstr ""
msgid "Select Archive Format"
msgstr "Выбрать формат архива"
+msgid "Select a namespace to fork the project"
+msgstr ""
+
msgid "Select a timezone"
msgstr "Выбор временной зоны"
msgid "Select an existing Kubernetes cluster or create a new one"
-msgstr ""
+msgstr "Выбрать ÑущеÑтвующий клаÑтер Kubernetes или Ñоздать новый"
msgid "Select assignee"
-msgstr ""
+msgstr "Выбрать ответÑтвенного"
msgid "Select branch/tag"
+msgstr "Выбрать ветку/тег"
+
+msgid "Select project"
msgstr ""
-msgid "Select target branch"
-msgstr "Выбор целевой ветки"
+msgid "Select project and zone to choose machine type"
+msgstr ""
-msgid "Selective synchronization"
+msgid "Select project to choose zone"
msgstr ""
-msgid "Send email"
+msgid "Select source branch"
msgstr ""
+msgid "Select target branch"
+msgstr "Выбор целевой ветки"
+
+msgid "Send email"
+msgstr "Отправить Ñлектронное пиÑьмо"
+
msgid "Sep"
msgstr "Сент."
@@ -3752,14 +4061,11 @@ msgid "September"
msgstr "СентÑбрь"
msgid "Server version"
-msgstr ""
+msgstr "ВерÑÐ¸Ñ Ñервера"
msgid "Service Templates"
msgstr "Шаблоны Служб"
-msgid "Service URL"
-msgstr ""
-
msgid "Session expiration, projects limit and attachment size."
msgstr ""
@@ -3770,23 +4076,20 @@ msgid "Set default and restrict visibility levels. Configure import sources and
msgstr ""
msgid "Set max session time for web terminal."
-msgstr ""
+msgstr "УÑтановить макÑимальное Ð²Ñ€ÐµÐ¼Ñ ÑеанÑа Ð´Ð»Ñ Ð²ÐµÐ±-терминала."
msgid "Set notification email for abuse reports."
-msgstr ""
+msgstr "ÐаÑтроить ÑƒÐ²ÐµÐ´Ð¾Ð¼Ð»ÐµÐ½Ð¸Ñ Ð¿Ð¾ Ñлектронной почте Ð´Ð»Ñ ÑÐ¾Ð¾Ð±Ñ‰ÐµÐ½Ð¸Ñ Ð¾ злоупотреблениÑÑ…."
msgid "Set requirements for a user to sign-in. Enable mandatory two-factor authentication."
msgstr ""
msgid "Set up CI/CD"
-msgstr ""
+msgstr "ÐаÑтройка CI/CD"
msgid "Set up Koding"
msgstr "ÐаÑтройка Koding"
-msgid "Set up assertions/attributes/claims (email, first_name, last_name) and NameID according to %{docsLinkStart}the documentation %{icon}%{docsLinkEnd}"
-msgstr ""
-
msgid "SetPasswordToCloneLink|set a password"
msgstr "уÑтановите пароль"
@@ -3794,21 +4097,24 @@ msgid "Settings"
msgstr "ÐаÑтройки"
msgid "Setup a specific Runner automatically"
-msgstr ""
+msgstr "ÐаÑтроить конкретный обработчик заданий автоматичеÑки"
-msgid "Share the <strong>%{sso_label}</strong> with members so they can sign in to your group through your identity provider"
-msgstr ""
+msgid "Share"
+msgstr "ПоделитьÑÑ"
-msgid "SharedRunnersMinutesSettings|By resetting the pipeline minutes for this namespace, the currently used minutes will be set to zero."
-msgstr ""
+msgid "Shared Runners"
+msgstr "Общие обработчики заданий"
+
+msgid "Show command"
+msgstr "Показать команду"
-msgid "SharedRunnersMinutesSettings|Reset pipeline minutes"
+msgid "Show complete raw log"
msgstr ""
-msgid "SharedRunnersMinutesSettings|Reset used pipeline minutes"
+msgid "Show latest version"
msgstr ""
-msgid "Show command"
+msgid "Show latest version of the diff"
msgstr ""
msgid "Show parent pages"
@@ -3817,6 +4123,9 @@ msgstr "Показать родительÑкие Ñтраницы"
msgid "Show parent subgroups"
msgstr "Показать родительÑкие подгруппы"
+msgid "Show whitespace changes"
+msgstr ""
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "Показано %d Ñобытие"
@@ -3824,46 +4133,40 @@ msgstr[1] "Показано %d ÑобытиÑ"
msgstr[2] "Показано %d Ñобытий"
msgstr[3] "Показано %d Ñобытий"
-msgid "Sidebar|Change weight"
-msgstr "Изменить веÑ"
-
-msgid "Sidebar|No"
-msgstr "Ðет"
-
-msgid "Sidebar|None"
-msgstr "ОтÑутÑтвует"
+msgid "Side-by-side"
+msgstr ""
-msgid "Sidebar|Weight"
-msgstr "ВеÑ"
+msgid "Sign out"
+msgstr "Выйти"
msgid "Sign-in restrictions"
-msgstr ""
+msgstr "ÐžÐ³Ñ€Ð°Ð½Ð¸Ñ‡ÐµÐ½Ð¸Ñ Ð²Ñ…Ð¾Ð´Ð°"
msgid "Sign-up restrictions"
-msgstr ""
+msgstr "ÐžÐ³Ñ€Ð°Ð½Ð¸Ñ‡ÐµÐ½Ð¸Ñ Ñ€ÐµÐ³Ð¸Ñтрации"
msgid "Size and domain settings for static websites"
-msgstr ""
+msgstr "ÐаÑтройки размера и доменных имён Ð´Ð»Ñ ÑтатичеÑких веб-Ñайтов"
-msgid "Slack application"
+msgid "Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job"
msgstr ""
msgid "Snippets"
msgstr "Сниппеты"
msgid "Something went wrong on our end"
-msgstr ""
+msgstr "Что-то пошло не так Ñ Ð½Ð°ÑˆÐµÐ¹ Ñтороны"
msgid "Something went wrong on our end."
-msgstr ""
+msgstr "Что-то пошло не так Ñ Ð½Ð°ÑˆÐµÐ¹ Ñтороны."
-msgid "Something went wrong when toggling the button"
+msgid "Something went wrong on our end. Please try again!"
msgstr ""
-msgid "Something went wrong while fetching Dependency Scanning."
-msgstr ""
+msgid "Something went wrong when toggling the button"
+msgstr "Что-то пошло не так при переключении кнопки"
-msgid "Something went wrong while fetching SAST."
+msgid "Something went wrong while closing the %{issuable}. Please try again later"
msgstr ""
msgid "Something went wrong while fetching the projects."
@@ -3872,9 +4175,15 @@ msgstr "Что-то пошло не так при получении проекÑ
msgid "Something went wrong while fetching the registry list."
msgstr "Что-то пошло не так при получении ÑпиÑка рееÑтров."
-msgid "Something went wrong. Please try again."
+msgid "Something went wrong while reopening the %{issuable}. Please try again later"
msgstr ""
+msgid "Something went wrong while resolving this discussion. Please try again."
+msgstr ""
+
+msgid "Something went wrong. Please try again."
+msgstr "Произошла ошибка. Попробуйте позже."
+
msgid "Sort by"
msgstr "Сортировать по"
@@ -3888,7 +4197,7 @@ msgid "SortOptions|Created date"
msgstr "Дата ÑозданиÑ"
msgid "SortOptions|Due date"
-msgstr "Срок"
+msgstr "Плановый Ñрок"
msgid "SortOptions|Due later"
msgstr "Срок позже"
@@ -3917,9 +4226,6 @@ msgstr "ПоÑледние обновлённые"
msgid "SortOptions|Least popular"
msgstr "Ðаименее популÑрный"
-msgid "SortOptions|Less weight"
-msgstr "Меньший веÑ"
-
msgid "SortOptions|Milestone"
msgstr "Веха"
@@ -3929,9 +4235,6 @@ msgstr "Веха, наÑÑ‚ÑƒÐ¿Ð°ÑŽÑ‰Ð°Ñ Ð¿Ð¾Ð·Ð´Ð½ÐµÐµ"
msgid "SortOptions|Milestone due soon"
msgstr "Веха, наÑÑ‚ÑƒÐ¿Ð°ÑŽÑ‰Ð°Ñ Ñ€Ð°Ð½ÑŒÑˆÐµ"
-msgid "SortOptions|More weight"
-msgstr "Больший веÑ"
-
msgid "SortOptions|Most popular"
msgstr "Ðаиболее популÑрный"
@@ -3971,14 +4274,11 @@ msgstr "Ðачатые позже"
msgid "SortOptions|Start soon"
msgstr "Ðачатые недавно"
-msgid "SortOptions|Weight"
-msgstr "ВеÑ"
-
msgid "Source"
msgstr "ИÑточник"
msgid "Source (branch or tag)"
-msgstr ""
+msgstr "ИÑточник (ветка или тег)"
msgid "Source code"
msgstr "ИÑходный код"
@@ -3990,19 +4290,46 @@ msgid "Spam Logs"
msgstr "Спам Логи"
msgid "Spam and Anti-bot Protection"
-msgstr ""
+msgstr "Защита от Ñпама и ботов"
+
+msgid "Specific Runners"
+msgstr "Конкретные обработчики заданий"
msgid "Specify the following URL during the Runner setup:"
msgstr "Укажите Ñледующий URL во Ð²Ñ€ÐµÐ¼Ñ Ð½Ð°Ñтройки Gitlab Runner:"
+msgid "Squash commits"
+msgstr ""
+
+msgid "Stage"
+msgstr ""
+
+msgid "Stage & Commit"
+msgstr ""
+
+msgid "Stage all changes"
+msgstr ""
+
+msgid "Stage changes"
+msgstr ""
+
+msgid "Staged"
+msgstr ""
+
+msgid "Staged %{type}"
+msgstr ""
+
+msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
+msgstr ""
+
msgid "StarProject|Star"
msgstr "Отметить"
msgid "Starred Projects"
-msgstr ""
+msgstr "Избранные проекты"
msgid "Starred Projects' Activity"
-msgstr ""
+msgstr "ÐктивноÑть в избранных проектах"
msgid "Starred projects"
msgstr "Отмеченные проекты"
@@ -4014,45 +4341,54 @@ msgid "Start the Runner!"
msgstr "ЗапуÑтить GitLab Runner!"
msgid "Started"
-msgstr ""
+msgstr "Запущен"
-msgid "State your message to activate"
+msgid "Starts at (UTC)"
msgstr ""
msgid "Status"
-msgstr ""
+msgstr "СтатуÑ"
+
+msgid "Stop this environment"
+msgstr "ОÑтановить Ñту Ñреду"
msgid "Stopped"
msgstr "ОÑтановлен"
msgid "Storage"
-msgstr ""
+msgstr "Хранилище"
msgid "Subgroups"
msgstr "Подгруппы"
-msgid "Switch branch/tag"
-msgstr "Переключить ветка/тег"
+msgid "Subscribe"
+msgstr ""
+
+msgid "Subscribe at group level"
+msgstr ""
-msgid "System"
+msgid "Subscribe at project level"
msgstr ""
+msgid "Switch branch/tag"
+msgstr "Переключить ветка/тег"
+
msgid "System Hooks"
msgstr "СиÑтемные Обработчики"
-msgid "System header and footer:"
-msgstr ""
-
msgid "Tag (%{tag_count})"
msgid_plural "Tags (%{tag_count})"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
+msgstr[0] "Тег (%{tag_count})"
+msgstr[1] "Тегов (%{tag_count})"
+msgstr[2] "Тегов (%{tag_count})"
+msgstr[3] "Тегов (%{tag_count})"
msgid "Tags"
msgstr "Теги"
+msgid "Tags:"
+msgstr "Теги:"
+
msgid "TagsPage|Browse commits"
msgstr "ПроÑмотреть коммиты"
@@ -4116,8 +4452,8 @@ msgstr "Этот тег не Ñодержит заметок к релизу."
msgid "TagsPage|Use git tag command to add a new one:"
msgstr "ИÑпользуйте команду git tag, чтобы добавить новый тег:"
-msgid "TagsPage|Write your release notes or drag files here..."
-msgstr "Ðапишите Ñвои заметки к релизу или перетащите файлы Ñюда..."
+msgid "TagsPage|Write your release notes or drag files here…"
+msgstr ""
msgid "TagsPage|protected"
msgstr "защищенный"
@@ -4126,24 +4462,24 @@ msgid "Target Branch"
msgstr "Ветка"
msgid "Target branch"
-msgstr ""
+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 "Terms of Service Agreement and Privacy Policy"
+msgstr ""
-msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project"
+msgid "Terms of Service and Privacy Policy"
msgstr ""
-msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
+msgid "Test coverage parsing"
msgstr ""
-msgid "The X509 Certificate to use when mutual TLS is required to communicate with the external authorization service. If left blank, the server certificate is still validated when accessing over HTTPS."
+msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project"
+msgstr "Трекер обÑуждений - Ñто меÑто, где можно добавить вещи, которые необходимо улучшить или решить в проекте"
+
+msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
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."
@@ -4152,20 +4488,17 @@ msgstr "Этап напиÑÐ°Ð½Ð¸Ñ ÐºÐ¾Ð´Ð° показывает Ð²Ñ€ÐµÐ¼Ñ Ñ
msgid "The collection of events added to the data gathered for that stage."
msgstr "ÐšÐ¾Ð»Ð»ÐµÐºÑ†Ð¸Ñ Ñобытий добавленных в данные Ñобранные Ð´Ð»Ñ Ñтого Ñтапа."
-msgid "The connection will time out after %{timeout}. For repositories that take longer, use a clone/push combination."
-msgstr ""
-
msgid "The fork relationship has been removed."
msgstr "СвÑзь Ñ Ð¾Ñ‚Ð²ÐµÑ‚Ð²Ð»ÐµÐ½Ð¸ÐµÐ¼ удалена."
msgid "The import will time out after %{timeout}. For repositories that take longer, use a clone/push combination."
-msgstr ""
+msgstr "Импорт будет отключен поÑле %{timeout}. Ð”Ð»Ñ Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ð¸ÐµÐ², которые импортируютÑÑ Ð·Ð° большее времÑ, иÑпользуйте комбинацию команд clone/push."
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 maximum file size allowed is 200KB."
-msgstr ""
+msgstr "МакÑимально допуÑтимый размер файла ÑоÑтавлÑет 200 Кб."
msgid "The number of attempts GitLab will make to access a storage."
msgstr "КоличеÑтво попыток, которые GitLab будет предпринимать Ð´Ð»Ñ Ð´Ð¾Ñтупа к хранилищу."
@@ -4173,7 +4506,7 @@ 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 passphrase required to decrypt the private key. This is optional and the value is encrypted at rest."
+msgid "The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>"
msgstr ""
msgid "The phase of the development lifecycle."
@@ -4182,9 +4515,6 @@ msgstr "Фаза жизненного цикла разработки."
msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit."
msgstr "Этап Ð¿Ð»Ð°Ð½Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð¸Ñ Ð¿Ð¾ÐºÐ°Ð·Ñ‹Ð²Ð°ÐµÑ‚ Ð²Ñ€ÐµÐ¼Ñ Ð¾Ñ‚ предыдущего шага до отправки первого коммита. ДобавлÑетÑÑ Ð°Ð²Ñ‚Ð¾Ð¼Ð°Ñ‚Ð¸Ñ‡ÐµÑки, как только отправите Ñвой первый коммит."
-msgid "The private key to use when a client certificate is provided. This value is encrypted at rest."
-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 "ПроизводÑтвенный Ñтап показывает общее Ð²Ñ€ÐµÐ¼Ñ Ð¼ÐµÐ¶Ð´Ñƒ Ñозданием обÑÑƒÐ¶Ð´ÐµÐ½Ð¸Ñ Ð¸ развертыванием кода в продуктивной Ñреде. Данные будут автоматичеÑки добавлены поÑле полного Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð¸Ñ Ð¸Ð´ÐµÐ¸."
@@ -4198,15 +4528,15 @@ msgid "The repository for this project does not exist."
msgstr "Репозиторий Ð´Ð»Ñ Ñтого проекта не ÑущеÑтвует."
msgid "The repository for this project is empty"
-msgstr ""
+msgstr "Репозиторий Ð´Ð»Ñ Ð´Ð°Ð½Ð½Ð¾Ð³Ð¾ проекта пуÑтой"
msgid "The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>."
-msgstr ""
+msgstr "Репозиторий должен быть доÑтупен через протоколы <code>http: //</code>, <code>https: //</code> или <code>git: //</code>."
msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request."
msgstr "Этап обзора показывает Ð²Ñ€ÐµÐ¼Ñ Ð¾Ñ‚ ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ Ð·Ð°Ð¿Ñ€Ð¾Ñа ÑлиÑÐ½Ð¸Ñ Ð´Ð¾ его выполнениÑ. Данные будут автоматичеÑки добавлены поÑле Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð¸Ñ Ð¿ÐµÑ€Ð²Ð¾Ð³Ð¾ запроÑа на ÑлиÑние."
-msgid "The roadmap shows the progress of your epics along a timeline"
+msgid "The secure token used by the Runner to checkout the project"
msgstr ""
msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time."
@@ -4231,6 +4561,9 @@ msgid "The value lying at the midpoint of a series of observed values. E.g., bet
msgstr "Среднее значение в Ñ€Ñду. Пример: между 3, 5, 9, Ñреднее 5, между 3, 5, 7, 8, Ñреднее (5+7)/2 = 6."
msgid "There are no issues to show"
+msgstr "Ðет обÑуждений, которые можно показать"
+
+msgid "There are no labels yet"
msgstr ""
msgid "There are no merge requests to show"
@@ -4239,33 +4572,45 @@ msgstr ""
msgid "There are problems accessing Git storage: "
msgstr "Проблемы Ñ Ð´Ð¾Ñтупом к Git хранилищу: "
-msgid "There was an error loading results"
+msgid "There was an error loading jobs"
+msgstr "Ошибка загрузки фоновых заданий"
+
+msgid "There was an error loading latest pipeline"
msgstr ""
msgid "There was an error loading users activity calendar."
msgstr ""
msgid "There was an error saving your notification settings."
-msgstr ""
+msgstr "Произошла ошибка при Ñохранении наÑтроек уведомлений."
msgid "There was an error subscribing to this label."
-msgstr ""
+msgstr "Произошла ошибка подпиÑки на Ñту метку."
msgid "There was an error when reseting email token."
msgstr ""
msgid "There was an error when subscribing to this label."
-msgstr ""
+msgstr "При подпиÑке на Ñту метку произошла ошибка."
msgid "There was an error when unsubscribing from this label."
msgstr ""
-msgid "This board\\'s scope is reduced"
-msgstr "УÑтановлен отбор"
+msgid "They can be managed using the %{link}."
+msgstr ""
-msgid "This directory"
+msgid "This GitLab instance does not provide any shared Runners yet. Instance administrators can register shared Runners in the admin area."
+msgstr "Этот ÑкземплÑÑ€ GitLab пока не предоÑтавлÑет никаких общих обработчиков заданий. ÐдминиÑтраторы ÑкземплÑров могут региÑтрировать общие обработчики заданий в Ñекции админиÑтрированиÑ."
+
+msgid "This diff is collapsed."
msgstr ""
+msgid "This directory"
+msgstr "Этот каталог"
+
+msgid "This group does not provide any group Runners yet."
+msgstr "Эта группа пока не Ñодержит групповых обработчики заданий."
+
msgid "This is a confidential issue."
msgstr "Это конфиденциальное обÑуждение."
@@ -4282,22 +4627,31 @@ msgid "This issue is locked."
msgstr "ОбÑуждение заблокировано."
msgid "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
-msgstr ""
+msgstr "Это задание завиÑит от того как пользователь инициирует Ñвой процеÑÑ Ñборки. Чаще вÑего оно иÑпользуетÑÑ Ð´Ð»Ñ Ñ€Ð°Ð·Ð²ÐµÑ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ ÐºÐ¾Ð´Ð° на продуктивных Ñредах"
msgid "This job depends on upstream jobs that need to succeed in order for this job to be triggered"
msgstr "Это задание завиÑит от рабочих заданий на верхнем уровне, которые должны завершитьÑÑ ÑƒÑпешно, чтобы Ñто задание было запущено"
+msgid "This job does not have a trace."
+msgstr "Это фоновое задание не Ñодержит траÑÑировку иÑполнениÑ."
+
+msgid "This job has been canceled"
+msgstr "Это фоновое задание отменено"
+
+msgid "This job has been skipped"
+msgstr "Это фоновое задание пропущено"
+
msgid "This job has not been triggered yet"
-msgstr ""
+msgstr "Это фоновое задание еще не запущено"
msgid "This job has not started yet"
-msgstr ""
+msgstr "Эта задача еще не запущена"
msgid "This job is in pending state and is waiting to be picked by a runner"
-msgstr ""
+msgstr "Это фоновое задание находитÑÑ Ð² ÑоÑтоÑнии Ð¾Ð¶Ð¸Ð´Ð°Ð½Ð¸Ñ Ð¸ ожидает запуÑка процеÑÑа иÑполнениÑ"
msgid "This job requires a manual action"
-msgstr ""
+msgstr "Это фоновое задание требует ручных дейÑтвий"
msgid "This means you can not push code until you create an empty repository or import existing one."
msgstr "Это означает, что вы не можете отправить код, пока не Ñоздадите пуÑтой репозиторий или не импортируете ÑущеÑтвующий."
@@ -4305,20 +4659,29 @@ msgstr "Это означает, что вы не можете отправитÑ
msgid "This merge request is locked."
msgstr "Ð—Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние заблокирован."
+msgid "This option is disabled while you still have unstaged changes"
+msgstr ""
+
msgid "This page is unavailable because you are not allowed to read information across multiple projects."
msgstr ""
-msgid "This project"
+msgid "This page will be removed in a future release."
msgstr ""
+msgid "This project"
+msgstr "Этот проект"
+
+msgid "This project does not belong to a group and can therefore not make use of group Runners."
+msgstr "Этот проект не отноÑитÑÑ Ð½Ð¸ к какой группе и поÑтому не может иÑпользовать групповые обработчики заданий."
+
msgid "This repository"
-msgstr ""
+msgstr "Этот репозиторий"
-msgid "This will delete the custom metric, Are you sure?"
+msgid "This source diff could not be displayed because it is too large."
msgstr ""
-msgid "Those emails automatically become issues (with the comments becoming the email conversation) listed here."
-msgstr "Эти Ñлектронные пиÑьма автоматичеÑки преобразуютÑÑ Ð² обÑÑƒÐ¶Ð´ÐµÐ½Ð¸Ñ (комментарии раÑÑылаютÑÑ ÐºÐ°Ðº ветвь обÑÑƒÐ¶Ð´ÐµÐ½Ð¸Ñ Ð² почте), перечиÑленные здеÑÑŒ."
+msgid "This user has no identities"
+msgstr ""
msgid "Time before an issue gets scheduled"
msgstr "Ð’Ñ€ÐµÐ¼Ñ Ð´Ð¾ начала Ð¿Ð¾Ð¿Ð°Ð´Ð°Ð½Ð¸Ñ Ð¾Ð±ÑÑƒÐ¶Ð´ÐµÐ½Ð¸Ñ Ð² планировщик"
@@ -4329,11 +4692,11 @@ msgstr "Ð’Ñ€ÐµÐ¼Ñ Ð´Ð¾ начала работы над обÑуждением"
msgid "Time between merge request creation and merge/close"
msgstr "Ð’Ñ€ÐµÐ¼Ñ Ð¼ÐµÐ¶Ð´Ñƒ Ñозданием запроÑа ÑлиÑÐ½Ð¸Ñ Ð¸ ÑлиÑнием / закрытием"
-msgid "Time between updates and capacity settings."
+msgid "Time remaining"
msgstr ""
-msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
-msgstr ""
+msgid "Time spent"
+msgstr "Времени затрачено"
msgid "Time tracking"
msgstr ""
@@ -4356,6 +4719,9 @@ msgstr "%s дней назад"
msgid "Timeago|%s days remaining"
msgstr "ОÑталоÑÑŒ %s дней"
+msgid "Timeago|%s hours ago"
+msgstr ""
+
msgid "Timeago|%s hours remaining"
msgstr "ОÑталоÑÑŒ %s чаÑов"
@@ -4371,6 +4737,9 @@ msgstr "%s меÑÑцев назад"
msgid "Timeago|%s months remaining"
msgstr "ОÑталоÑÑŒ %s меÑÑцев"
+msgid "Timeago|%s seconds ago"
+msgstr ""
+
msgid "Timeago|%s seconds remaining"
msgstr "ОÑталоÑÑŒ %s Ñекунд(Ñ‹)"
@@ -4386,48 +4755,45 @@ msgstr "%s лет назад"
msgid "Timeago|%s years remaining"
msgstr "ОÑталоÑÑŒ %s лет"
+msgid "Timeago|1 day ago"
+msgstr ""
+
msgid "Timeago|1 day remaining"
msgstr "ОÑталÑÑ Ð´ÐµÐ½ÑŒ"
+msgid "Timeago|1 hour ago"
+msgstr ""
+
msgid "Timeago|1 hour remaining"
msgstr "ОÑталÑÑ Ñ‡Ð°Ñ"
+msgid "Timeago|1 minute ago"
+msgstr ""
+
msgid "Timeago|1 minute remaining"
msgstr "ОÑталаÑÑŒ одна минута"
+msgid "Timeago|1 month ago"
+msgstr ""
+
msgid "Timeago|1 month remaining"
msgstr "ОÑталÑÑ Ð¼ÐµÑÑц"
+msgid "Timeago|1 week ago"
+msgstr ""
+
msgid "Timeago|1 week remaining"
msgstr "ОÑталаÑÑŒ неделÑ"
+msgid "Timeago|1 year ago"
+msgstr ""
+
msgid "Timeago|1 year remaining"
msgstr "ОÑталÑÑ Ð³Ð¾Ð´"
msgid "Timeago|Past due"
msgstr "ПроÑрочено"
-msgid "Timeago|a day ago"
-msgstr "день назад"
-
-msgid "Timeago|a month ago"
-msgstr "меÑÑц назад"
-
-msgid "Timeago|a week ago"
-msgstr "неделю назад"
-
-msgid "Timeago|a year ago"
-msgstr "год назад"
-
-msgid "Timeago|about %s hours ago"
-msgstr "около %s чаÑов назад"
-
-msgid "Timeago|about a minute ago"
-msgstr "около минуты назад"
-
-msgid "Timeago|about an hour ago"
-msgstr "около чаÑа назад"
-
msgid "Timeago|in %s days"
msgstr "Через %s дней"
@@ -4467,11 +4833,14 @@ msgstr "через неделю"
msgid "Timeago|in 1 year"
msgstr "через год"
-msgid "Timeago|in a while"
-msgstr "через некоторое времÑ"
+msgid "Timeago|just now"
+msgstr ""
+
+msgid "Timeago|right now"
+msgstr ""
-msgid "Timeago|less than a minute ago"
-msgstr "менее чем минуту назад"
+msgid "Timeout"
+msgstr ""
msgid "Time|hr"
msgid_plural "Time|hrs"
@@ -4491,21 +4860,12 @@ msgid "Time|s"
msgstr "Ñ"
msgid "Tip:"
-msgstr ""
-
-msgid "Title"
-msgstr "Заголовок"
+msgstr "Совет:"
msgid "To GitLab"
msgstr ""
-msgid "To connect GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to connect."
-msgstr ""
-
-msgid "To connect GitHub repositories, you first need to authorize GitLab to access the list of your GitHub repositories:"
-msgstr ""
-
-msgid "To connect an SVN repository, check out %{svn_link}."
+msgid "To add an SSH key you need to %{generate_link_start}generate one%{link_end} or use an %{existing_link_start}existing key%{link_end}."
msgstr ""
msgid "To import GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
@@ -4517,23 +4877,23 @@ msgstr ""
msgid "To import an SVN repository, check out %{svn_link}."
msgstr ""
-msgid "To only use CI/CD features for an external repository, choose <strong>CI/CD for external repo</strong>."
-msgstr ""
-
-msgid "To set up SAML authentication for your group through an identity provider like Azure, Okta, Onelogin, Ping Identity, or your custom SAML 2.0 provider:"
-msgstr ""
+msgid "To start serving your jobs you can add Runners to your group"
+msgstr "Чтобы начать выполнÑть Ñвои заданиÑ, вы можете добавить обработчик заданий в вашу группу"
msgid "To validate your GitLab CI configurations, go to 'CI/CD → Pipelines' inside your project, and click on the 'CI Lint' button."
msgstr ""
-msgid "To view the roadmap, add a planned start or finish date to one of your epics in this group or its subgroups. Only epics in the past 3 months and the next 3 months are shown."
+msgid "Todo"
+msgstr "Дела"
+
+msgid "Toggle Sidebar"
msgstr ""
-msgid "Todo"
+msgid "Toggle discussion"
msgstr ""
msgid "Toggle sidebar"
-msgstr ""
+msgstr "Переключить боковую панель"
msgid "ToggleButton|Toggle Status: OFF"
msgstr ""
@@ -4541,6 +4901,9 @@ msgstr ""
msgid "ToggleButton|Toggle Status: ON"
msgstr ""
+msgid "Too many changes to show."
+msgstr ""
+
msgid "Total Time"
msgstr "Общее времÑ"
@@ -4548,25 +4911,22 @@ msgid "Total test time for all commits/merges"
msgstr "Общее Ð²Ñ€ÐµÐ¼Ñ Ñ‚ÐµÑÑ‚Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð¸Ñ Ñ„Ð¸ÐºÑаций/ÑлиÑний"
msgid "Total: %{total}"
-msgstr ""
-
-msgid "Track activity with Contribution Analytics."
-msgstr "ОтÑлеживать активноÑть Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ Ðналитики УчаÑтников."
-
-msgid "Track groups of issues that share a theme, across projects and milestones"
-msgstr "Следите за обÑуждениÑми, Ñгруппированными по темам, Ñразу из неÑкольких проектов и Ñтапов"
+msgstr "Ð’Ñего: %{total}"
msgid "Track time with quick actions"
msgstr ""
msgid "Trigger this manual action"
+msgstr "ЗапуÑтить Ñто дейÑтвие вручную"
+
+msgid "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."
msgstr ""
-msgid "Turn on Service Desk"
-msgstr "Включить Службу Поддержки"
+msgid "Try again"
+msgstr "Попробовать Ñнова"
-msgid "Unknown"
-msgstr ""
+msgid "Unable to load the diff. %{button_try_again}"
+msgstr "Ðе удаетÑÑ Ð·Ð°Ð³Ñ€ÑƒÐ·Ð¸Ñ‚ÑŒ отличиÑ. %{button_try_again}"
msgid "Unlock"
msgstr "Разблокировать"
@@ -4577,26 +4937,48 @@ msgstr "Разблокировано"
msgid "Unresolve discussion"
msgstr "Переоткрыть диÑкуÑÑию"
+msgid "Unstage all changes"
+msgstr ""
+
+msgid "Unstage changes"
+msgstr ""
+
+msgid "Unstaged"
+msgstr ""
+
+msgid "Unstaged %{type}"
+msgstr ""
+
+msgid "Unstaged and staged %{type}"
+msgstr ""
+
msgid "Unstar"
msgstr "СнÑть отметку"
-msgid "Up to date"
+msgid "Unsubscribe"
msgstr ""
-msgid "Upgrade your plan to activate Advanced Global Search."
-msgstr "ПовыÑьте ваш тарифный план, чтобы активировать Улучшенный Глобальный ПоиÑк."
+msgid "Unsubscribe at group level"
+msgstr ""
-msgid "Upgrade your plan to activate Contribution Analytics."
-msgstr "ПовыÑьте ваш тарифный план, чтобы активировать Ðналитики УчаÑтников."
+msgid "Unsubscribe at project level"
+msgstr ""
-msgid "Upgrade your plan to activate Group Webhooks."
-msgstr "ПовыÑьте ваш тарифный план, чтобы активировать Групповые Веб-Обработчики."
+msgid "Unverified"
+msgstr "Ðеподтверждено"
-msgid "Upgrade your plan to activate Issue weight."
-msgstr "ПовыÑьте ваш тарифный план чтобы активировать Ð²ÐµÑ Ð¾Ð±Ñуждений."
+msgid "Up to date"
+msgstr "Ðктуальный"
-msgid "Upgrade your plan to improve Issue boards."
-msgstr "ПовыÑьте ваш тарифный план, чтобы улучшить доÑки обÑуждений."
+msgid "Update %{files}"
+msgid_plural "Update %{files} files"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "Update your group name, description, avatar, and other general settings."
+msgstr ""
msgid "Upload New File"
msgstr "Загрузить новый файл"
@@ -4605,19 +4987,19 @@ msgid "Upload file"
msgstr "Загрузить файл"
msgid "Upload new avatar"
-msgstr ""
+msgstr "Загрузить аватар"
msgid "UploadLink|click to upload"
msgstr "кликните Ð´Ð»Ñ Ð·Ð°Ð³Ñ€ÑƒÐ·ÐºÐ¸"
msgid "Upvotes"
-msgstr ""
+msgstr "ГолоÑа \"за\""
msgid "Usage statistics"
-msgstr ""
+msgstr "СтатиÑтика иÑпользованиÑ"
-msgid "Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab"
-msgstr "ИÑпользуйте Службу поддержки Ð´Ð»Ñ ÑвÑзи Ñ Ð²Ð°ÑˆÐ¸Ð¼Ð¸ пользователÑми (например, Ð´Ð»Ñ Ð¾ÑущеÑÑ‚Ð²Ð»ÐµÐ½Ð¸Ñ Ð¿Ð¾Ð´Ð´ÐµÑ€Ð¶ÐºÐ¸ клиентов) через Ñлектронную почту непоÑредÑтвенно в GitLab"
+msgid "Use group milestones to manage issues from multiple projects in the same milestone."
+msgstr ""
msgid "Use the following registration token during setup:"
msgstr "ИÑпользуйте Ñледующий токен региÑтрации в процеÑÑе уÑтановки:"
@@ -4625,44 +5007,50 @@ msgstr "ИÑпользуйте Ñледующий токен региÑтрацÐ
msgid "Use your global notification setting"
msgstr "ИÑпользуютÑÑ Ð³Ð»Ð¾Ð±Ð°Ð»ÑŒÐ½Ñ‹Ð¹ наÑтройки уведомлений"
-msgid "Used by members to sign in to your group in GitLab"
+msgid "User and IP Rate Limits"
msgstr ""
-msgid "User and IP Rate Limits"
+msgid "Users"
msgstr ""
-msgid "Variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. You can use variables for passwords, secret keys, or whatever you want."
+msgid "Variables"
msgstr ""
+msgid "Variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. You can use variables for passwords, secret keys, or whatever you want."
+msgstr "Переменные применÑÑŽÑ‚ÑÑ Ðº Ñреде через обработчики заданий. Их можно защитить, Ð´ÐµÐ»Ð°Ñ Ð¸Ñ… доÑтупными только защищенным ветвÑм или меткам. Ð’Ñ‹ можете иÑпользовать переменные Ð´Ð»Ñ Ð¿Ð°Ñ€Ð¾Ð»ÐµÐ¹, Ñекретных ключей и прочего."
+
msgid "Various container registry settings."
msgstr ""
msgid "Various email settings."
-msgstr ""
+msgstr "Различные наÑтройки Ñлектронной почты."
msgid "Various settings that affect GitLab performance."
-msgstr ""
-
-msgid "View and edit lines"
-msgstr ""
+msgstr "Различные наÑтройки, которые влиÑÑŽÑ‚ на производительноÑть GitLab."
-msgid "View epics list"
-msgstr ""
+msgid "Verified"
+msgstr "Проверено"
msgid "View file @ "
msgstr "ПроÑмотр файла @ "
msgid "View group labels"
+msgstr "ПроÑмотр меток группы"
+
+msgid "View jobs"
msgstr ""
msgid "View labels"
+msgstr "ПроÑмотр меток"
+
+msgid "View log"
msgstr ""
msgid "View open merge request"
msgstr "ПроÑмотреть открытый Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние"
msgid "View project labels"
-msgstr ""
+msgstr "ПроÑмотр меток проекта"
msgid "View replaced file @ "
msgstr "ПроÑмотр заменённого файла @ "
@@ -4685,9 +5073,6 @@ msgstr "Ðе определен"
msgid "Want to see the data? Please ask an administrator for access."
msgstr "Хотите увидеть данные? ОбратитеÑÑŒ к админиÑтратору за доÑтупом."
-msgid "We could not verify that one of your projects on GCP has billing enabled. Please try again."
-msgstr ""
-
msgid "We don't have enough data to show this stage."
msgstr "Ð˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð¿Ð¾ Ñтапу отÑутÑтвует."
@@ -4695,18 +5080,15 @@ msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr "Мы хотим быть уверены, что Ñто вы, пожалуйÑта, подтвердите, что вы не робот."
msgid "Web IDE"
-msgstr ""
+msgstr "Веб-Ð¸Ð½Ñ‚ÐµÑ€Ñ„ÐµÐ¹Ñ IDE"
msgid "Web terminal"
-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 еÑли, например, отправлен новый код или Ñоздано новое обÑуждение. Ð’Ñ‹ можете наÑтроить веб-обработчики так, чтобы они реагировали на определённые ÑобытиÑ, такие как отправки кода, обÑÑƒÐ¶Ð´ÐµÐ½Ð¸Ñ Ð¸Ð»Ð¸ запроÑÑ‹ на ÑлиÑние. Групповые веб-обработчики применÑÑŽÑ‚ÑÑ ÐºÐ¾ вÑем проектам в группе и позволÑÑŽÑ‚ вам Ñтандартизовать функциональноÑть веб-обработчиков Ð´Ð»Ñ Ð²Ñей вашей группы."
+msgstr "Web терминал"
-msgid "Weight"
-msgstr "ВеÑ"
+msgid "When a runner is locked, it cannot be assigned to other projects"
+msgstr "Когда процеÑÑ Runner заблокирован, он не может быть назначен другим проектам"
-msgid "When leaving the URL blank, classification labels can still be specified whitout disabling cross project features or performing external authorization checks."
+msgid "When enabled, users cannot use GitLab until the terms have been accepted."
msgstr ""
msgid "Wiki"
@@ -4733,8 +5115,32 @@ msgstr ""
msgid "WikiEdit|There is already a page with the same title in that path."
msgstr ""
-msgid "WikiEmptyPageError|You are not allowed to create wiki pages"
-msgstr "Ð’Ñ‹ не можете Ñоздавать вики-Ñтраницы"
+msgid "WikiEmptyIssueMessage|Suggest wiki improvement"
+msgstr ""
+
+msgid "WikiEmptyIssueMessage|You must be a project member in order to add wiki pages. If you have suggestions for how to improve the wiki for this project, consider opening an issue in the %{issues_link}."
+msgstr ""
+
+msgid "WikiEmptyIssueMessage|issue tracker"
+msgstr ""
+
+msgid "WikiEmpty|A wiki is where you can store all the details about your project. This can include why you've created it, its principles, how to use it, and so on."
+msgstr ""
+
+msgid "WikiEmpty|Create your first page"
+msgstr ""
+
+msgid "WikiEmpty|Suggest wiki improvement"
+msgstr ""
+
+msgid "WikiEmpty|The wiki lets you write documentation for your project"
+msgstr ""
+
+msgid "WikiEmpty|This project has no wiki pages"
+msgstr ""
+
+msgid "WikiEmpty|You must be a project member in order to add wiki pages."
+msgstr ""
msgid "WikiHistoricalPage|This is an old version of this page."
msgstr "Это уÑÑ‚Ð°Ñ€ÐµÐ²ÑˆÐ°Ñ Ð²ÐµÑ€ÑÐ¸Ñ Ñтой Ñтраницы."
@@ -4769,6 +5175,12 @@ msgstr "ÐÐ¾Ð²Ð°Ñ Ð’Ð¸ÐºÐ¸ Страница"
msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?"
msgstr "Ð’Ñ‹ уверены что хотите удалить Ñту Ñтраницу?"
+msgid "WikiPageConfirmDelete|Delete page"
+msgstr ""
+
+msgid "WikiPageConfirmDelete|Delete page %{pageTitle}?"
+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} и убедитеÑÑŒ, что внеÑенные Вами Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð½Ðµ затрут чужие."
@@ -4784,8 +5196,8 @@ msgstr "Обновить %{page_title}"
msgid "WikiPage|Page slug"
msgstr "СемантичеÑкий Url Ð´Ð»Ñ Ñтраницы"
-msgid "WikiPage|Write your content or drag files here..."
-msgstr "Ðапишите ваше Ñодержимое или перетащите Ñюда файлы..."
+msgid "WikiPage|Write your content or drag files here…"
+msgstr ""
msgid "Wiki|Create Page"
msgstr "Создать Страницу"
@@ -4796,9 +5208,6 @@ msgstr "Создать Ñтраницу"
msgid "Wiki|Edit Page"
msgstr "Редактировать Ñтраницу"
-msgid "Wiki|Empty page"
-msgstr "ПуÑÑ‚Ð°Ñ Ñтраница"
-
msgid "Wiki|More Pages"
msgstr "Ещё Ñтраницы"
@@ -4817,44 +5226,44 @@ 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 "Отменить Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð´Ð¾Ñтупа"
-msgid "Write a commit message..."
-msgstr ""
+msgid "Yes"
+msgstr "Да"
msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
msgstr "Ð’Ñ‹ ÑобираетеÑÑŒ удалить %{group_name}. Удаленные группы ÐЕ МОГУТ быть воÑÑтановлены! Ð’Ñ‹ ÐБСОЛЮТÐО уверены?"
msgid "You are going to remove %{project_full_name}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?"
-msgstr ""
+msgstr "Ð’Ñ‹ хотите удалить %{project_full_name}. Удаленный проект ÐЕ МОЖЕТ быть воÑÑтановлен! Ð’Ñ‹ ÐБСОЛЮТÐО уверены?"
msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?"
msgstr "Ð’Ñ‹ ÑобираетеÑÑŒ удалить ÑвÑзь Ð¾Ñ‚Ð²ÐµÑ‚Ð²Ð»ÐµÐ½Ð¸Ñ Ñ Ð¸Ñходным проектом %{forked_from_project}. Ð’Ñ‹ ÐБСОЛЮТÐО уверены?"
msgid "You are going to transfer %{project_full_name} to another owner. Are you ABSOLUTELY sure?"
-msgstr ""
+msgstr "Ð’Ñ‹ ÑобираетеÑÑŒ передать проект %{project_full_name} другому владельцу. Ð’Ñ‹ ÐБСОЛЮТÐО уверены?"
msgid "You are on a read-only GitLab instance."
-msgstr ""
+msgstr "Ð’Ñ‹ иÑпользуете GitLab в режиме только Ð´Ð»Ñ Ñ‡Ñ‚ÐµÐ½Ð¸Ñ."
-msgid "You are on a secondary (read-only) Geo node. If you want to make any changes, you must visit the %{primary_node}."
+msgid "You can %{linkStart}view the blob%{linkEnd} instead."
msgstr ""
msgid "You can also create a project from the command line."
-msgstr ""
+msgstr "Ð’Ñ‹ также можете Ñоздать проект из командной Ñтроки."
msgid "You can also star a label to make it a priority label."
msgstr ""
+msgid "You can also test your .gitlab-ci.yml in the %{linkStart}Lint%{linkEnd}"
+msgstr ""
+
msgid "You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}"
msgstr ""
msgid "You can move around the graph by using the arrow keys."
-msgstr ""
+msgstr "Ð’Ñ‹ можете перемещатьÑÑ Ð¿Ð¾ диаграмме Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ клавиш Ñо Ñтрелками."
msgid "You can only add files when you are on a branch"
msgstr "Ð’Ñ‹ можете добавлÑть только файлы, когда находитеÑÑŒ в ветке"
@@ -4862,30 +5271,33 @@ msgstr "Ð’Ñ‹ можете добавлÑть только файлы, когда
msgid "You can only edit 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 can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}"
+msgstr ""
msgid "You cannot write to this read-only GitLab instance."
msgstr "Ð’Ñ‹ не можете запиÑывать на Ñтот ÑкземплÑÑ€ \"только Ð´Ð»Ñ Ñ‡Ñ‚ÐµÐ½Ð¸Ñ\" клаÑтера GitLab."
-msgid "You do not have the correct permissions to override the settings from the LDAP group sync."
+msgid "You do not have any assigned merge requests"
msgstr ""
msgid "You have no permissions"
msgstr ""
+msgid "You have not created any merge requests"
+msgstr ""
+
msgid "You have reached your project limit"
msgstr "Ð’Ñ‹ доÑтигли Ð¾Ð³Ñ€Ð°Ð½Ð¸Ñ‡ÐµÐ½Ð¸Ñ Ð² вашем проекте"
-msgid "You must have master access to force delete a lock"
+msgid "You must accept our Terms of Service and privacy policy in order to register an account"
+msgstr ""
+
+msgid "You must have maintainer access to force delete a lock"
msgstr ""
msgid "You must sign in to star a project"
msgstr "Ðеобходимо войти, чтобы оценить проект"
-msgid "You need a different license to enable FileLocks feature"
-msgstr ""
-
msgid "You need permission."
msgstr "Вам нужно разрешение."
@@ -4914,31 +5326,31 @@ msgid "You won't be able to pull or push project code via SSH until you add an S
msgstr "Ð’Ñ‹ не Ñможете работать Ñ Ð¿Ñ€Ð¾ÐµÐºÑ‚Ð¾Ð¼ через SSH, пока не добавите в Ñвой профиль SSH ключ"
msgid "You'll need to use different branch names to get a valid comparison."
-msgstr ""
+msgstr "Ð”Ð»Ñ Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ ÐºÐ¾Ñ€Ñ€ÐµÐºÑ‚Ð½Ð¾Ð³Ð¾ ÑÑ€Ð°Ð²Ð½ÐµÐ½Ð¸Ñ Ð²Ð°Ð¼ нужно иÑпользовать разные имена веток."
msgid "You're receiving this email because of your account on %{host}. %{manage_notifications_link} &middot; %{help_link}"
-msgstr ""
+msgstr "Ð’Ñ‹ получили Ñто Ñлектронное пиÑьмо из Ñвоей учетной запиÑи %{host}. %{manage_notifications_link} &middot; %{help_link}"
msgid "Your Groups"
-msgstr ""
+msgstr "Ваши Группы"
msgid "Your Kubernetes cluster information on this page is still editable, but you are advised to disable and reconfigure"
-msgstr ""
+msgstr "Ð˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð¾ Вашем Kubernetes клаÑтере на Ñтой Ñтранице по-прежнему доÑтупна Ð´Ð»Ñ Ñ€ÐµÐ´Ð°ÐºÑ‚Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð¸Ñ, но мы рекомендуем вам отключить его перед переконфигурированием"
msgid "Your Projects (default)"
-msgstr ""
+msgstr "Ваши проекты (по умолчанию)"
msgid "Your Projects' Activity"
-msgstr ""
+msgstr "ÐктивноÑть в ваших проектов"
msgid "Your Todos"
-msgstr ""
+msgstr "Ваши дела"
msgid "Your changes can be committed to %{branch_name} because a merge request is open."
msgstr ""
msgid "Your changes have been committed. Commit %{commitId} %{commitStats}"
-msgstr ""
+msgstr "Ваши Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð±Ñ‹Ð»Ð¸ зафикÑированы. Коммит %{commitId} %{commitStats}"
msgid "Your comment will not be visible to the public."
msgstr "Ваш комментарий не будет виден вÑем."
@@ -4952,166 +5364,58 @@ msgstr "Ваше имÑ"
msgid "Your projects"
msgstr "Ваши проекты"
-msgid "among other things"
+msgid "ago"
msgstr ""
-msgid "and %d fixed vulnerability"
-msgid_plural "and %d fixed vulnerabilities"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
+msgid "among other things"
+msgstr ""
msgid "assign yourself"
-msgstr ""
+msgstr "назначить ÑебÑ"
msgid "branch name"
msgstr "Ð¸Ð¼Ñ Ð²ÐµÑ‚Ð²Ð¸"
-msgid "by"
-msgstr "по"
-
-msgid "ciReport|%{type} detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|%{type} detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Code quality"
-msgstr ""
-
-msgid "ciReport|DAST detected no alerts by analyzing the review app"
-msgstr ""
-
-msgid "ciReport|Dependency scanning"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Failed to load %{reportName} report"
-msgstr ""
-
-msgid "ciReport|Fixed:"
-msgstr ""
-
-msgid "ciReport|Instances"
-msgstr ""
-
-msgid "ciReport|Learn more about whitelisting"
-msgstr ""
-
-msgid "ciReport|Loading %{reportName} report"
-msgstr ""
-
-msgid "ciReport|No changes to code quality"
-msgstr ""
-
-msgid "ciReport|No changes to performance metrics"
-msgstr ""
-
-msgid "ciReport|Performance metrics"
-msgstr ""
-
-msgid "ciReport|SAST"
-msgstr ""
-
-msgid "ciReport|SAST detected"
-msgstr ""
-
-msgid "ciReport|SAST detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|SAST detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|SAST:container no vulnerabilities were found"
-msgstr ""
-
-msgid "ciReport|Security scanning"
-msgstr ""
-
-msgid "ciReport|Security scanning failed loading any results"
-msgstr ""
-
-msgid "ciReport|Show complete code vulnerabilities report"
-msgstr ""
-
-msgid "ciReport|Unapproved vulnerabilities (red) can be marked as approved. %{helpLink}"
-msgstr ""
-
-msgid "ciReport|no vulnerabilities"
-msgstr ""
-
msgid "command line instructions"
msgstr ""
msgid "connecting"
msgstr ""
-msgid "could not read private key, is the passphrase correct?"
-msgstr ""
-
msgid "day"
msgid_plural "days"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
-
-msgid "detected %d fixed vulnerability"
-msgid_plural "detected %d fixed vulnerabilities"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
-
-msgid "detected %d new vulnerability"
-msgid_plural "detected %d new vulnerabilities"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
-
-msgid "detected no vulnerabilities"
-msgstr ""
-
-msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
-msgstr ""
+msgstr[0] "день"
+msgstr[1] "дней"
+msgstr[2] "дней"
+msgstr[3] "дней"
-msgid "here"
+msgid "deploy token"
msgstr ""
-msgid "importing"
+msgid "disabled"
msgstr ""
-msgid "in progress"
+msgid "enabled"
msgstr ""
-msgid "is invalid because there is downstream lock"
+msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
msgstr ""
-msgid "is invalid because there is upstream lock"
+msgid "for this project"
msgstr ""
-msgid "is not a valid X509 certificate."
+msgid "importing"
msgstr ""
-msgid "locked by %{path_lock_user_name} %{created_at}"
+msgid "latest version"
msgstr ""
msgid "merge request"
msgid_plural "merge requests"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
+msgstr[0] "Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние"
+msgstr[1] "запроÑов на ÑлиÑние"
+msgstr[2] "запроÑов на ÑлиÑние"
+msgstr[3] "запроÑов на ÑлиÑние"
msgid "mrWidget| Please restore it or use a different %{missingBranchName} branch"
msgstr ""
@@ -5125,28 +5429,7 @@ msgstr ""
msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage is %{emphasisStart} unchanged %{emphasisEnd} at %{memoryFrom}MB"
msgstr ""
-msgid "mrWidget|Add approval"
-msgstr ""
-
-msgid "mrWidget|Allows edits from maintainers"
-msgstr ""
-
-msgid "mrWidget|An error occured while removing your approval."
-msgstr ""
-
-msgid "mrWidget|An error occured while retrieving approval data for this merge request."
-msgstr ""
-
-msgid "mrWidget|An error occured while submitting your approval."
-msgstr ""
-
-msgid "mrWidget|Approve"
-msgstr ""
-
-msgid "mrWidget|Approved"
-msgstr ""
-
-msgid "mrWidget|Approved by"
+msgid "mrWidget|Allows commits from members who can merge to the target branch"
msgstr ""
msgid "mrWidget|Cancel automatic merge"
@@ -5173,6 +5456,9 @@ msgstr ""
msgid "mrWidget|Closes"
msgstr ""
+msgid "mrWidget|Create an issue to resolve them later"
+msgstr "Создать обÑуждение Ð´Ð»Ñ ÐµÐ³Ð¾ поÑледующего решениÑ"
+
msgid "mrWidget|Deployment statistics are not available currently"
msgstr ""
@@ -5216,10 +5502,10 @@ msgid "mrWidget|Refresh"
msgstr ""
msgid "mrWidget|Refresh now"
-msgstr ""
+msgstr "Обновить ÑейчаÑ"
msgid "mrWidget|Refreshing now"
-msgstr ""
+msgstr "ОбновлÑетÑÑ ÑейчаÑ"
msgid "mrWidget|Remove Source Branch"
msgstr ""
@@ -5227,9 +5513,6 @@ msgstr ""
msgid "mrWidget|Remove source branch"
msgstr ""
-msgid "mrWidget|Remove your approval"
-msgstr ""
-
msgid "mrWidget|Request to merge"
msgstr ""
@@ -5269,6 +5552,9 @@ msgstr ""
msgid "mrWidget|There are merge conflicts"
msgstr ""
+msgid "mrWidget|There are unresolved discussions. Please resolve these discussions"
+msgstr ""
+
msgid "mrWidget|This merge request failed to be merged automatically"
msgstr ""
@@ -5291,7 +5577,7 @@ msgid "mrWidget|branch does not exist."
msgstr ""
msgid "mrWidget|command line"
-msgstr ""
+msgstr "ÐºÐ¾Ð¼Ð°Ð½Ð´Ð½Ð°Ñ Ñтрока"
msgid "mrWidget|into"
msgstr ""
@@ -5306,14 +5592,14 @@ msgid "notification emails"
msgstr "email Ð´Ð»Ñ ÑƒÐ²ÐµÐ´Ð¾Ð¼Ð»ÐµÐ½Ð¸Ð¹"
msgid "or"
-msgstr ""
+msgstr "или"
msgid "parent"
msgid_plural "parents"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
+msgstr[0] "владелец"
+msgstr[1] "владельцы"
+msgstr[2] "владельцы"
+msgstr[3] "владельцы"
msgid "password"
msgstr "пароль"
@@ -5321,11 +5607,11 @@ msgstr "пароль"
msgid "personal access token"
msgstr "токен Ð´Ð»Ñ Ð¿ÐµÑ€Ñонального доÑтупа"
-msgid "private key does not match certificate."
-msgstr ""
+msgid "remaining"
+msgstr "оÑталоÑÑŒ"
msgid "remove due date"
-msgstr ""
+msgstr "убрать плановый Ñрок"
msgid "source"
msgstr "иÑходный текÑÑ‚"
@@ -5334,10 +5620,7 @@ msgid "spendCommand|%{slash_command} will update the sum of the time spent."
msgstr ""
msgid "this document"
-msgstr ""
-
-msgid "to help your contributors communicate effectively!"
-msgstr "чтобы помочь учаÑтникам взаимодейÑтвовать Ñффективнее!"
+msgstr "Ñтот документ"
msgid "username"
msgstr "Ð¸Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ"
@@ -5348,3 +5631,10 @@ msgstr ""
msgid "with %{additions} additions, %{deletions} deletions."
msgstr ""
+msgid "within %d minute "
+msgid_plural "within %d minutes "
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
diff --git a/locale/tr_TR/gitlab.po b/locale/tr_TR/gitlab.po
index 6a025dfbae5..6cdfa33b8ae 100644
--- a/locale/tr_TR/gitlab.po
+++ b/locale/tr_TR/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2018-04-04 19:35+0200\n"
-"PO-Revision-Date: 2018-04-05 03:35-0400\n"
+"POT-Creation-Date: 2018-07-01 16:35+1000\n"
+"PO-Revision-Date: 2018-07-01 11:01\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Turkish\n"
"Language: tr_TR\n"
@@ -16,8 +16,10 @@ msgstr ""
"X-Crowdin-Language: tr\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-msgid " and"
-msgstr " ve"
+msgid "%d changed file"
+msgid_plural "%d changed files"
+msgstr[0] ""
+msgstr[1] ""
msgid "%d commit"
msgid_plural "%d commits"
@@ -54,6 +56,16 @@ msgid_plural "%d metrics"
msgstr[0] ""
msgstr[1] ""
+msgid "%d staged change"
+msgid_plural "%d staged changes"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "%d unstaged change"
+msgid_plural "%d unstaged changes"
+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] ""
@@ -70,12 +82,21 @@ msgid_plural "%{count} participants"
msgstr[0] "%{count} katılımcı"
msgstr[1] "%{count} katılımcı"
+msgid "%{filePath} deleted"
+msgstr ""
+
+msgid "%{group_docs_link_start}Groups%{group_docs_link_end} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects."
+msgstr ""
+
msgid "%{loadingIcon} Started"
msgstr ""
msgid "%{lock_path} is locked by GitLab User %{lock_user_id}"
msgstr ""
+msgid "%{nip_domain} can be used as an alternative to a custom domain."
+msgstr ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -88,6 +109,9 @@ msgstr ""
msgid "%{openOrClose} %{noteable}"
msgstr ""
+msgid "%{percent}%% complete"
+msgstr ""
+
msgid "%{storage_name}: failed storage access attempt on host:"
msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:"
msgstr[0] ""
@@ -96,15 +120,62 @@ msgstr[1] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{title} changes"
+msgstr ""
+
+msgid "%{unstaged} unstaged and %{staged} staged changes"
+msgstr ""
+
msgid "(checkout the %{link} for information on how to install it)."
msgstr ""
msgid "+ %{moreCount} more"
msgstr "+ %{moreCount} daha fazla"
+msgid "- Runner is active and can process any new jobs"
+msgstr ""
+
+msgid "- Runner is paused and will not receive any new jobs"
+msgstr ""
+
msgid "- show less"
msgstr "- daha az göster"
+msgid "1 %{type} addition"
+msgid_plural "%{count} %{type} additions"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 %{type} modification"
+msgid_plural "%{count} %{type} modifications"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 closed issue"
+msgid_plural "%d closed issues"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 closed merge request"
+msgid_plural "%d closed merge requests"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 merged merge request"
+msgid_plural "%d merged merge requests"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 open issue"
+msgid_plural "%d open issues"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 open merge request"
+msgid_plural "%d open merge requests"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] ""
@@ -116,9 +187,27 @@ msgstr "İlk katkı!"
msgid "2FA enabled"
msgstr ""
+msgid "403|Please contact your GitLab administrator to get the permission."
+msgstr ""
+
+msgid "403|You don't have the permission to access this page."
+msgstr ""
+
+msgid "404|Make sure the address is correct and the page hasn't moved."
+msgstr ""
+
+msgid "404|Page Not Found"
+msgstr ""
+
+msgid "404|Please contact your GitLab administrator if you think this is a mistake."
+msgstr ""
+
msgid "<strong>Removes</strong> source branch"
msgstr ""
+msgid "A 'Runner' is a process which runs a job. You can setup as many Runners as you need."
+msgstr ""
+
msgid "A collection of graphs regarding Continuous Integration"
msgstr ""
@@ -128,6 +217,9 @@ msgstr ""
msgid "A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}."
msgstr ""
+msgid "A regular expression that will be used to find the test coverage output in the job trace. Leave blank to disable"
+msgstr ""
+
msgid "A user with write access to the source branch selected this option"
msgstr ""
@@ -140,36 +232,39 @@ msgstr "Kötüye Kullanım Raporları"
msgid "Abuse reports"
msgstr ""
+msgid "Accept terms"
+msgstr ""
+
msgid "Access Tokens"
msgstr "Erişim anahtarları"
msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again."
msgstr ""
+msgid "Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report."
+msgstr ""
+
msgid "Account"
msgstr "Hesap"
-msgid "Account and limit settings"
+msgid "Account and limit"
msgstr ""
msgid "Active"
msgstr "Etkin"
+msgid "Active Sessions"
+msgstr ""
+
msgid "Activity"
msgstr "Etkinlik"
-msgid "Add"
-msgstr "Ekle"
-
msgid "Add Changelog"
msgstr "DeÄŸiÅŸiklik Bilgisi Ekle"
msgid "Add Contribution guide"
msgstr "Katkı kılavuzu ekle"
-msgid "Add Group Webhooks and GitLab Enterprise Edition."
-msgstr ""
-
msgid "Add Kubernetes cluster"
msgstr ""
@@ -182,6 +277,9 @@ msgstr ""
msgid "Add new directory"
msgstr "Yeni dizin ekle"
+msgid "Add reaction"
+msgstr ""
+
msgid "Add todo"
msgstr "Yapılacaklara Ekle"
@@ -237,7 +335,7 @@ msgid "AdminUsers|To confirm, type %{username}"
msgstr ""
msgid "Advanced"
-msgstr "GeliÅŸmiÅŸ"
+msgstr ""
msgid "Advanced settings"
msgstr "GeliÅŸmiÅŸ ayarlar"
@@ -251,7 +349,10 @@ msgstr ""
msgid "All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings."
msgstr ""
-msgid "Allow edits from maintainers."
+msgid "Allow commits from members who can merge to the target branch."
+msgstr ""
+
+msgid "Allow public access to pipelines and job details, including output logs and artifacts"
msgstr ""
msgid "Allow rendering of PlantUML diagrams in Asciidoc documents."
@@ -263,31 +364,37 @@ msgstr ""
msgid "Allows you to add and manage Kubernetes clusters."
msgstr "Kubernetes kümelerini eklemeye ve yönetmenize olanak tanır."
-msgid "Also called \"Issuer\" or \"Relying party trust identifier\""
+msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
msgstr ""
-msgid "Also called \"Relying party service URL\" or \"Reply URL\""
+msgid "An error occured creating the new branch."
msgstr ""
-msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to connect."
+msgid "An error occured whilst loading all the files."
msgstr ""
-msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
+msgid "An error occured whilst loading the file content."
msgstr ""
-msgid "An error occurred previewing the blob"
+msgid "An error occured whilst loading the file."
msgstr ""
-msgid "An error occurred when toggling the notification subscription"
+msgid "An error occured whilst loading the merge request changes."
msgstr ""
-msgid "An error occurred when updating the issue weight"
+msgid "An error occured whilst loading the merge request version data."
msgstr ""
-msgid "An error occurred while adding approver"
+msgid "An error occured whilst loading the merge request."
msgstr ""
-msgid "An error occurred while detecting host keys"
+msgid "An error occurred previewing the blob"
+msgstr ""
+
+msgid "An error occurred when toggling the notification subscription"
+msgstr ""
+
+msgid "An error occurred while dismissing the alert. Refresh the page and try again."
msgstr ""
msgid "An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again."
@@ -305,10 +412,7 @@ msgstr ""
msgid "An error occurred while getting projects"
msgstr "Projeler yüklenirken bir hata oluştu"
-msgid "An error occurred while importing project"
-msgstr ""
-
-msgid "An error occurred while initializing path locks"
+msgid "An error occurred while importing project: ${details}"
msgstr ""
msgid "An error occurred while loading commits"
@@ -326,9 +430,6 @@ msgstr ""
msgid "An error occurred while making the request."
msgstr ""
-msgid "An error occurred while removing approver"
-msgstr ""
-
msgid "An error occurred while rendering KaTeX"
msgstr "KaTeX'i iÅŸlerken bir hata oluÅŸtu"
@@ -341,9 +442,6 @@ msgstr ""
msgid "An error occurred while retrieving diff"
msgstr "Değişiklikler yüklenirken bir hata oluştu"
-msgid "An error occurred while saving LDAP override status. Please try again."
-msgstr ""
-
msgid "An error occurred while saving assignees"
msgstr ""
@@ -353,9 +451,6 @@ msgstr "Kullanıcı adı doğrulanırken bir hata oluştu"
msgid "An error occurred. Please try again."
msgstr "Bir hata oluştu. Lütfen tekrar deneyin."
-msgid "Any Label"
-msgstr ""
-
msgid "Appearance"
msgstr "Görünüm"
@@ -368,19 +463,19 @@ msgstr "Nis"
msgid "April"
msgstr "Nisan"
-msgid "Archived project! Repository is read-only"
-msgstr "Arşivlenmiş proje! Sadece okuma yapılabilir"
+msgid "Archived project! Repository and other project resources are read-only"
+msgstr ""
msgid "Are you sure you want to delete this pipeline schedule?"
msgstr ""
-msgid "Are you sure you want to reset registration token?"
+msgid "Are you sure you want to remove this identity?"
msgstr ""
-msgid "Are you sure you want to reset the health check token?"
+msgid "Are you sure you want to reset registration token?"
msgstr ""
-msgid "Are you sure you want to unlock %{path_lock_path}?"
+msgid "Are you sure you want to reset the health check token?"
msgstr ""
msgid "Are you sure?"
@@ -389,7 +484,7 @@ msgstr "Emin misiniz?"
msgid "Artifacts"
msgstr ""
-msgid "Assertion consumer service URL"
+msgid "Ask your group maintainer to setup a group Runner."
msgstr ""
msgid "Assign custom color like #FF0000"
@@ -413,9 +508,15 @@ msgstr ""
msgid "Assigned to :name"
msgstr ""
+msgid "Assigned to me"
+msgstr ""
+
msgid "Assignee"
msgstr ""
+msgid "Assignee(s)"
+msgstr ""
+
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "Sürükleyip bırakarak bir dosya ekle veya %{upload_link}"
@@ -449,7 +550,10 @@ msgstr ""
msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
msgstr ""
-msgid "AutoDevOps|Auto DevOps (Beta)"
+msgid "Auto-cancel redundant, pending pipelines"
+msgstr ""
+
+msgid "AutoDevOps|Auto DevOps"
msgstr ""
msgid "AutoDevOps|Auto DevOps documentation"
@@ -470,79 +574,109 @@ msgstr ""
msgid "AutoDevOps|add a Kubernetes cluster"
msgstr ""
-msgid "AutoDevOps|enable Auto DevOps (Beta)"
+msgid "AutoDevOps|enable Auto DevOps"
msgstr ""
msgid "Available"
msgstr "Kullanılabilir"
+msgid "Available group Runners : %{runners}"
+msgstr ""
+
+msgid "Available group Runners : %{runners}."
+msgstr ""
+
msgid "Avatar will be removed. Are you sure?"
msgstr "Avatar kaldırılacak. Emin misiniz?"
msgid "Average per day: %{average}"
msgstr ""
-msgid "Background Color"
+msgid "Background color"
msgstr ""
msgid "Background jobs"
msgstr ""
-msgid "Begin with the selected commit"
+msgid "Badges"
+msgstr ""
+
+msgid "Badges|A new badge was added."
+msgstr ""
+
+msgid "Badges|Add badge"
+msgstr ""
+
+msgid "Badges|Adding the badge failed, please check the entered URLs and try again."
+msgstr ""
+
+msgid "Badges|Badge image URL"
+msgstr ""
+
+msgid "Badges|Badge image preview"
+msgstr ""
+
+msgid "Badges|Delete badge"
+msgstr ""
+
+msgid "Badges|Delete badge?"
msgstr ""
-msgid "Billing"
+msgid "Badges|Deleting the badge failed, please try again."
msgstr ""
-msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan."
+msgid "Badges|Group Badge"
msgstr ""
-msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available."
+msgid "Badges|Link"
msgstr ""
-msgid "BillingPlans|Current plan"
+msgid "Badges|No badge image"
msgstr ""
-msgid "BillingPlans|Customer Support"
+msgid "Badges|No image to preview"
msgstr ""
-msgid "BillingPlans|Downgrade"
+msgid "Badges|Project Badge"
msgstr ""
-msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
+msgid "Badges|Reload badge image"
msgstr ""
-msgid "BillingPlans|Manage plan"
+msgid "Badges|Save changes"
msgstr ""
-msgid "BillingPlans|Please contact %{customer_support_link} in that case."
+msgid "Badges|Saving the badge failed, please check the entered URLs and try again."
msgstr ""
-msgid "BillingPlans|See all %{plan_name} features"
+msgid "Badges|The %{docsLinkStart}variables%{docsLinkEnd} GitLab supports: %{placeholders}"
msgstr ""
-msgid "BillingPlans|This group uses the plan associated with its parent group."
+msgid "Badges|The badge was deleted."
msgstr ""
-msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
+msgid "Badges|The badge was saved."
msgstr ""
-msgid "BillingPlans|Upgrade"
+msgid "Badges|This group has no badges"
msgstr ""
-msgid "BillingPlans|You are currently on the %{plan_link} plan."
+msgid "Badges|This project has no badges"
msgstr ""
-msgid "BillingPlans|frequently asked questions"
+msgid "Badges|Your badges"
msgstr ""
-msgid "BillingPlans|monthly"
+msgid "Begin with the selected commit"
msgstr ""
-msgid "BillingPlans|paid annually at %{price_per_year}"
+msgid "Below are examples of regex for existing tools:"
msgstr ""
-msgid "BillingPlans|per user"
+msgid "Boards"
+msgstr ""
+
+msgid "Branch %{branchName} was not found in this project's repository."
msgstr ""
msgid "Branch (%{branch_count})"
@@ -622,7 +756,7 @@ msgstr ""
msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
msgstr ""
-msgid "Branches|Only a project master or owner can delete a protected branch"
+msgid "Branches|Only a project maintainer or owner can delete a protected branch"
msgstr ""
msgid "Branches|Overview"
@@ -658,9 +792,6 @@ msgstr ""
msgid "Branches|Stale branches"
msgstr ""
-msgid "Branches|The branch could not be updated automatically because it has diverged from its upstream counterpart."
-msgstr ""
-
msgid "Branches|The default branch cannot be deleted"
msgstr ""
@@ -673,15 +804,9 @@ msgstr ""
msgid "Branches|To confirm, type %{branch_name_confirmation}:"
msgstr ""
-msgid "Branches|To discard the local changes and overwrite the branch with the upstream version, delete it here and choose 'Update Now' above."
-msgstr ""
-
msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
msgstr ""
-msgid "Branches|diverged from upstream"
-msgstr ""
-
msgid "Branches|merged"
msgstr ""
@@ -703,40 +828,79 @@ msgstr ""
msgid "Browse files"
msgstr ""
-msgid "Business"
-msgstr ""
-
msgid "ByAuthor|by"
msgstr ""
msgid "CI / CD"
msgstr ""
-msgid "CI/CD"
+msgid "CI / CD Settings"
msgstr ""
msgid "CI/CD configuration"
msgstr ""
-msgid "CI/CD for external repo"
+msgid "CI/CD settings"
+msgstr ""
+
+msgid "CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery."
+msgstr ""
+
+msgid "CICD|Auto DevOps"
+msgstr ""
+
+msgid "CICD|Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration."
+msgstr ""
+
+msgid "CICD|Automatic deployment to staging, manual deployment to production"
+msgstr ""
+
+msgid "CICD|Continuous deployment to production"
+msgstr ""
+
+msgid "CICD|Deployment strategy"
+msgstr ""
+
+msgid "CICD|Deployment strategy needs a domain name to work correctly."
+msgstr ""
+
+msgid "CICD|Disable Auto DevOps"
+msgstr ""
+
+msgid "CICD|Enable Auto DevOps"
+msgstr ""
+
+msgid "CICD|Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific %{ci_file}."
+msgstr ""
+
+msgid "CICD|Instance default (%{state})"
msgstr ""
msgid "CICD|Jobs"
msgstr ""
-msgid "Cancel"
+msgid "CICD|Learn more about Auto DevOps"
msgstr ""
-msgid "Cannot be merged automatically"
+msgid "CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project."
msgstr ""
-msgid "Cannot modify managed Kubernetes cluster"
+msgid "CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages."
msgstr ""
-msgid "Certificate fingerprint"
+msgid "Can't find HEAD commit for this branch"
msgstr ""
-msgid "Change Weight"
+msgid "Cancel"
+msgstr ""
+
+msgid "Cancel this job"
+msgstr ""
+
+msgid "Cannot be merged automatically"
+msgstr ""
+
+msgid "Cannot modify managed Kubernetes cluster"
msgstr ""
msgid "Change this value to influence how frequently the GitLab UI polls for updates."
@@ -790,21 +954,18 @@ msgstr ""
msgid "Choose a branch/tag (e.g. %{master}) or enter a commit (e.g. %{sha}) to see what's changed or to create a merge request."
msgstr ""
-msgid "Choose file..."
+msgid "Choose any color."
msgstr ""
-msgid "Choose which groups you wish to synchronize to this secondary node."
+msgid "Choose between <code>clone</code> or <code>fetch</code> to get the recent application code"
msgstr ""
-msgid "Choose which repositories you want to connect and run CI/CD pipelines."
+msgid "Choose file..."
msgstr ""
msgid "Choose which repositories you want to import."
msgstr ""
-msgid "Choose which shards you wish to synchronize to this secondary node."
-msgstr ""
-
msgid "CiStatusLabel|canceled"
msgstr ""
@@ -874,21 +1035,12 @@ msgstr ""
msgid "CiVariable|All environments"
msgstr ""
-msgid "CiVariable|Create wildcard"
-msgstr ""
-
msgid "CiVariable|Error occured while saving variables"
msgstr ""
-msgid "CiVariable|New environment"
-msgstr ""
-
msgid "CiVariable|Protected"
msgstr ""
-msgid "CiVariable|Search environments"
-msgstr ""
-
msgid "CiVariable|Toggle protected"
msgstr ""
@@ -898,28 +1050,28 @@ msgstr ""
msgid "CircuitBreakerApiLink|circuitbreaker api"
msgstr ""
-msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
+msgid "Clear search input"
msgstr ""
-msgid "Click to expand text"
+msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Client authentication certificate"
+msgid "Click the <strong>Promote</strong> button in the top right corner to promote it to a group milestone."
msgstr ""
-msgid "Client authentication key"
+msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
msgstr ""
-msgid "Client authentication key password"
+msgid "Click to expand it."
msgstr ""
-msgid "Clone repository"
+msgid "Click to expand text"
msgstr ""
-msgid "Close"
+msgid "Clone repository"
msgstr ""
-msgid "Closed"
+msgid "Close"
msgstr ""
msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster"
@@ -937,6 +1089,12 @@ msgstr ""
msgid "ClusterIntegration|Advanced options on this Kubernetes cluster's integration"
msgstr ""
+msgid "ClusterIntegration|An error occured while trying to fetch project zones: %{error}"
+msgstr ""
+
+msgid "ClusterIntegration|An error occured while trying to fetch your projects: %{error}"
+msgstr ""
+
msgid "ClusterIntegration|Applications"
msgstr ""
@@ -967,6 +1125,9 @@ msgstr ""
msgid "ClusterIntegration|Copy Ingress IP Address to clipboard"
msgstr ""
+msgid "ClusterIntegration|Copy Jupyter Hostname to clipboard"
+msgstr ""
+
msgid "ClusterIntegration|Copy Kubernetes cluster name"
msgstr ""
@@ -982,7 +1143,7 @@ msgstr ""
msgid "ClusterIntegration|Create a new Kubernetes cluster on Google Kubernetes Engine right from GitLab"
msgstr ""
-msgid "ClusterIntegration|Create on GKE"
+msgid "ClusterIntegration|Create on Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
@@ -994,13 +1155,25 @@ msgstr ""
msgid "ClusterIntegration|Environment scope"
msgstr ""
+msgid "ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab's Google Kubernetes Engine Integration."
+msgstr ""
+
+msgid "ClusterIntegration|Fetching machine types"
+msgstr ""
+
+msgid "ClusterIntegration|Fetching projects"
+msgstr ""
+
+msgid "ClusterIntegration|Fetching zones"
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
msgid "ClusterIntegration|GitLab Runner"
msgstr ""
-msgid "ClusterIntegration|Google Cloud Platform project ID"
+msgid "ClusterIntegration|Google Cloud Platform project"
msgstr ""
msgid "ClusterIntegration|Google Kubernetes Engine"
@@ -1012,9 +1185,6 @@ msgstr ""
msgid "ClusterIntegration|Helm Tiller"
msgstr ""
-msgid "ClusterIntegration|In order to show the health of the cluster, we'll need to provision your cluster with Prometheus to collect the required data."
-msgstr ""
-
msgid "ClusterIntegration|Ingress"
msgstr ""
@@ -1024,9 +1194,6 @@ msgstr ""
msgid "ClusterIntegration|Install"
msgstr ""
-msgid "ClusterIntegration|Install Prometheus"
-msgstr ""
-
msgid "ClusterIntegration|Installed"
msgstr ""
@@ -1039,13 +1206,16 @@ msgstr ""
msgid "ClusterIntegration|Integration status"
msgstr ""
-msgid "ClusterIntegration|Kubernetes cluster"
+msgid "ClusterIntegration|Jupyter Hostname"
msgstr ""
-msgid "ClusterIntegration|Kubernetes cluster details"
+msgid "ClusterIntegration|JupyterHub"
+msgstr ""
+
+msgid "ClusterIntegration|Kubernetes cluster"
msgstr ""
-msgid "ClusterIntegration|Kubernetes cluster health"
+msgid "ClusterIntegration|Kubernetes cluster details"
msgstr ""
msgid "ClusterIntegration|Kubernetes cluster integration"
@@ -1075,7 +1245,13 @@ msgstr ""
msgid "ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project"
msgstr ""
-msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
+msgid "ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}."
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{help_link_start}Kubernetes%{help_link_end}."
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}."
msgstr ""
msgid "ClusterIntegration|Learn more about environments"
@@ -1099,7 +1275,16 @@ msgstr ""
msgid "ClusterIntegration|More information"
msgstr ""
-msgid "ClusterIntegration|Multiple Kubernetes clusters are available in GitLab Enterprise Edition Premium and Ultimate"
+msgid "ClusterIntegration|No machine types matched your search"
+msgstr ""
+
+msgid "ClusterIntegration|No projects found"
+msgstr ""
+
+msgid "ClusterIntegration|No projects matched your search"
+msgstr ""
+
+msgid "ClusterIntegration|No zones matched your search"
msgstr ""
msgid "ClusterIntegration|Note:"
@@ -1114,9 +1299,6 @@ msgstr ""
msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
msgstr ""
-msgid "ClusterIntegration|Project ID"
-msgstr ""
-
msgid "ClusterIntegration|Project namespace"
msgstr ""
@@ -1129,6 +1311,9 @@ msgstr ""
msgid "ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration."
msgstr ""
+msgid "ClusterIntegration|Redeem up to $500 in free credit for Google Cloud Platform"
+msgstr ""
+
msgid "ClusterIntegration|Remove Kubernetes cluster integration"
msgstr ""
@@ -1144,19 +1329,37 @@ msgstr ""
msgid "ClusterIntegration|Save changes"
msgstr ""
+msgid "ClusterIntegration|Search machine types"
+msgstr ""
+
+msgid "ClusterIntegration|Search projects"
+msgstr ""
+
+msgid "ClusterIntegration|Search zones"
+msgstr ""
+
msgid "ClusterIntegration|Security"
msgstr ""
msgid "ClusterIntegration|See and edit the details for your Kubernetes cluster"
msgstr ""
-msgid "ClusterIntegration|See machine types"
+msgid "ClusterIntegration|Select machine type"
+msgstr ""
+
+msgid "ClusterIntegration|Select project"
msgstr ""
-msgid "ClusterIntegration|See your projects"
+msgid "ClusterIntegration|Select project and zone to choose machine type"
msgstr ""
-msgid "ClusterIntegration|See zones"
+msgid "ClusterIntegration|Select project to choose zone"
+msgstr ""
+
+msgid "ClusterIntegration|Select zone"
+msgstr ""
+
+msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
msgid "ClusterIntegration|Service token"
@@ -1189,6 +1392,9 @@ msgstr ""
msgid "ClusterIntegration|Token"
msgstr ""
+msgid "ClusterIntegration|Validating project billing status"
+msgstr ""
+
msgid "ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
@@ -1219,13 +1425,19 @@ msgstr ""
msgid "ClusterIntegration|properly configured"
msgstr ""
+msgid "ClusterIntegration|sign up"
+msgstr ""
+
msgid "Collapse"
msgstr ""
-msgid "Comment and resolve discussion"
+msgid "Collapse sidebar"
+msgstr ""
+
+msgid "Comment & resolve discussion"
msgstr ""
-msgid "Comment and unresolve discussion"
+msgid "Comment & unresolve discussion"
msgstr ""
msgid "Comments"
@@ -1292,6 +1504,9 @@ msgstr ""
msgid "Committed by"
msgstr ""
+msgid "Commit…"
+msgstr ""
+
msgid "Compare"
msgstr ""
@@ -1340,6 +1555,9 @@ msgstr ""
msgid "Configure limits for web and API requests."
msgstr ""
+msgid "Configure push mirrors."
+msgstr ""
+
msgid "Configure storage path and circuit breaker settings."
msgstr ""
@@ -1349,18 +1567,9 @@ msgstr ""
msgid "Connect"
msgstr ""
-msgid "Connect all repositories"
-msgstr ""
-
msgid "Connect repositories from GitHub"
msgstr ""
-msgid "Connect your external repositories, and CI/CD pipelines will run for new commits. A GitLab project will be created with only CI/CD features enabled."
-msgstr ""
-
-msgid "Connecting..."
-msgstr ""
-
msgid "Container Registry"
msgstr ""
@@ -1406,9 +1615,18 @@ 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 "ContainerRegistry|You can also use a %{deploy_token} for read-only access to the registry images."
+msgstr ""
+
+msgid "Continue"
+msgstr ""
+
msgid "Continuous Integration and Deployment"
msgstr ""
+msgid "Contribute to GitLab"
+msgstr ""
+
msgid "Contribution"
msgstr ""
@@ -1430,28 +1648,28 @@ msgstr ""
msgid "ContributorsPage|Please wait a moment, this page will automatically refresh when ready."
msgstr ""
-msgid "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
+msgid "Copy URL to clipboard"
msgstr ""
-msgid "Control the maximum concurrency of repository backfill for this secondary node"
+msgid "Copy branch name to clipboard"
msgstr ""
-msgid "Copy SSH public key to clipboard"
+msgid "Copy command to clipboard"
msgstr ""
-msgid "Copy URL to clipboard"
+msgid "Copy commit SHA to clipboard"
msgstr ""
-msgid "Copy branch name to clipboard"
+msgid "Copy file name to clipboard"
msgstr ""
-msgid "Copy command to clipboard"
+msgid "Copy file path to clipboard"
msgstr ""
-msgid "Copy commit SHA to clipboard"
+msgid "Copy reference to clipboard"
msgstr ""
-msgid "Copy reference to clipboard"
+msgid "Copy to clipboard"
msgstr ""
msgid "Create"
@@ -1472,13 +1690,13 @@ msgstr ""
msgid "Create branch"
msgstr ""
-msgid "Create directory"
+msgid "Create commit"
msgstr ""
-msgid "Create empty repository"
+msgid "Create directory"
msgstr ""
-msgid "Create epic"
+msgid "Create empty repository"
msgstr ""
msgid "Create file"
@@ -1523,13 +1741,10 @@ msgstr ""
msgid "CreateTokenToCloneLink|create a personal access token"
msgstr ""
-msgid "Creates a new branch from %{branchName}"
-msgstr ""
-
-msgid "Creates a new branch from %{branchName} and re-directs to create a new merge request"
+msgid "Created"
msgstr ""
-msgid "Creating epic"
+msgid "Created by me"
msgstr ""
msgid "Cron Timezone"
@@ -1538,16 +1753,19 @@ msgstr ""
msgid "Cron syntax"
msgstr ""
-msgid "Current node"
+msgid "CurrentUser|Profile"
msgstr ""
-msgid "Custom notification events"
+msgid "CurrentUser|Settings"
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}."
+msgid "Custom CI config path"
msgstr ""
-msgid "Customize colors"
+msgid "Custom notification events"
+msgstr ""
+
+msgid "Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notification_link}."
msgstr ""
msgid "Cycle Analytics"
@@ -1586,7 +1804,7 @@ msgstr ""
msgid "December"
msgstr ""
-msgid "Default classification label"
+msgid "Decline and sign out"
msgstr ""
msgid "Define a custom pattern with cron syntax"
@@ -1595,6 +1813,9 @@ msgstr ""
msgid "Delete"
msgstr ""
+msgid "Delete list"
+msgstr ""
+
msgid "Deploy"
msgid_plural "Deploys"
msgstr[0] ""
@@ -1603,549 +1824,525 @@ msgstr[1] ""
msgid "Deploy Keys"
msgstr ""
-msgid "Description"
+msgid "DeployKeys|+%{count} others"
msgstr ""
-msgid "Description templates allow you to define context-specific templates for issue and merge request description fields for your project."
+msgid "DeployKeys|Current project"
msgstr ""
-msgid "Details"
+msgid "DeployKeys|Deploy key"
msgstr ""
-msgid "Diffs|No file name available"
+msgid "DeployKeys|Enabled deploy keys"
msgstr ""
-msgid "Directory name"
+msgid "DeployKeys|Error enabling deploy key"
msgstr ""
-msgid "Disable"
+msgid "DeployKeys|Error getting deploy keys"
msgstr ""
-msgid "Discard draft"
+msgid "DeployKeys|Error removing deploy key"
msgstr ""
-msgid "Discover GitLab Geo."
+msgid "DeployKeys|Expand %{count} other projects"
msgstr ""
-msgid "Dismiss Cycle Analytics introduction box"
+msgid "DeployKeys|Loading deploy keys"
msgstr ""
-msgid "Dismiss Merge Request promotion"
+msgid "DeployKeys|No deploy keys found. Create one with the form above."
msgstr ""
-msgid "Documentation for popular identity providers"
+msgid "DeployKeys|Privately accessible deploy keys"
msgstr ""
-msgid "Don't show again"
+msgid "DeployKeys|Project usage"
msgstr ""
-msgid "Done"
+msgid "DeployKeys|Publicly accessible deploy keys"
msgstr ""
-msgid "Download"
+msgid "DeployKeys|Read access only"
msgstr ""
-msgid "Download tar"
+msgid "DeployKeys|Write access allowed"
msgstr ""
-msgid "Download tar.bz2"
+msgid "DeployKeys|You are going to remove this deploy key. Are you sure?"
msgstr ""
-msgid "Download tar.gz"
+msgid "DeployTokens|Active Deploy Tokens (%{active_tokens})"
msgstr ""
-msgid "Download zip"
+msgid "DeployTokens|Add a deploy token"
msgstr ""
-msgid "DownloadArtifacts|Download"
+msgid "DeployTokens|Allows read-only access to the registry images"
msgstr ""
-msgid "DownloadCommit|Email Patches"
+msgid "DeployTokens|Allows read-only access to the repository"
msgstr ""
-msgid "DownloadCommit|Plain Diff"
+msgid "DeployTokens|Copy deploy token to clipboard"
msgstr ""
-msgid "DownloadSource|Download"
+msgid "DeployTokens|Copy username to clipboard"
msgstr ""
-msgid "Downvotes"
+msgid "DeployTokens|Create deploy token"
msgstr ""
-msgid "Due date"
+msgid "DeployTokens|Created"
msgstr ""
-msgid "During this process, you’ll be asked for URLs from GitLab’s side. Use the URLs shown below."
+msgid "DeployTokens|Deploy Tokens"
msgstr ""
-msgid "Edit"
+msgid "DeployTokens|Deploy tokens allow read-only access to your repository and registry images."
msgstr ""
-msgid "Edit Pipeline Schedule %{id}"
+msgid "DeployTokens|Expires"
msgstr ""
-msgid "Edit files in the editor and commit changes here"
+msgid "DeployTokens|Name"
msgstr ""
-msgid "Editing"
+msgid "DeployTokens|Pick a name for the application, and we'll give you a unique deploy token."
msgstr ""
-msgid "Elasticsearch"
+msgid "DeployTokens|Revoke"
msgstr ""
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "DeployTokens|Revoke %{name}"
msgstr ""
-msgid "Email"
+msgid "DeployTokens|Scopes"
msgstr ""
-msgid "Emails"
+msgid "DeployTokens|This action cannot be undone."
msgstr ""
-msgid "Enable"
+msgid "DeployTokens|This project has no active Deploy Tokens."
msgstr ""
-msgid "Enable Auto DevOps"
+msgid "DeployTokens|Use this token as a password. Make sure you save it - you won't be able to access it again."
msgstr ""
-msgid "Enable SAML authentication for this group"
+msgid "DeployTokens|Use this username as a login."
msgstr ""
-msgid "Enable Sentry for error reporting and logging."
+msgid "DeployTokens|Username"
msgstr ""
-msgid "Enable and configure InfluxDB metrics."
-msgstr ""
-
-msgid "Enable and configure Prometheus metrics."
-msgstr ""
-
-msgid "Enable classification control using an external service"
-msgstr ""
-
-msgid "Enable or disable version check and usage ping."
-msgstr ""
-
-msgid "Enable reCAPTCHA or Akismet and set IP limits."
-msgstr ""
-
-msgid "Enable the Performance Bar for a given group."
-msgstr ""
-
-msgid "Enabled"
+msgid "DeployTokens|You are about to revoke"
msgstr ""
-msgid "Environments|An error occurred while fetching the environments."
+msgid "DeployTokens|Your New Deploy Token"
msgstr ""
-msgid "Environments|An error occurred while making the request."
+msgid "DeployTokens|Your new project deploy token has been created."
msgstr ""
-msgid "Environments|Commit"
+msgid "Deprioritize label"
msgstr ""
-msgid "Environments|Deployment"
+msgid "Description"
msgstr ""
-msgid "Environments|Environment"
+msgid "Details"
msgstr ""
-msgid "Environments|Environments"
+msgid "Diffs|No file name available"
msgstr ""
-msgid "Environments|Job"
+msgid "Diffs|Something went wrong while fetching diff lines."
msgstr ""
-msgid "Environments|New environment"
+msgid "Directory name"
msgstr ""
-msgid "Environments|No deployments yet"
+msgid "Disable"
msgstr ""
-msgid "Environments|Open"
+msgid "Disable for this project"
msgstr ""
-msgid "Environments|Re-deploy"
+msgid "Disable group Runners"
msgstr ""
-msgid "Environments|Read more about environments"
+msgid "Discard changes"
msgstr ""
-msgid "Environments|Rollback"
-msgstr ""
-
-msgid "Environments|Show all"
+msgid "Discard draft"
msgstr ""
-msgid "Environments|Updated"
+msgid "Dismiss Cycle Analytics introduction box"
msgstr ""
-msgid "Environments|You don't have any environments right now."
+msgid "Domain"
msgstr ""
-msgid "Epic will be removed! Are you sure?"
+msgid "Don't show again"
msgstr ""
-msgid "Epics"
+msgid "Done"
msgstr ""
-msgid "Epics Roadmap"
+msgid "Download"
msgstr ""
-msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
+msgid "Download tar"
msgstr ""
-msgid "Error Reporting and Logging"
+msgid "Download tar.bz2"
msgstr ""
-msgid "Error checking branch data. Please try again."
+msgid "Download tar.gz"
msgstr ""
-msgid "Error committing changes. Please try again."
+msgid "Download zip"
msgstr ""
-msgid "Error creating epic"
+msgid "DownloadArtifacts|Download"
msgstr ""
-msgid "Error fetching contributors data."
+msgid "DownloadCommit|Email Patches"
msgstr ""
-msgid "Error fetching labels."
+msgid "DownloadCommit|Plain Diff"
msgstr ""
-msgid "Error fetching network graph."
+msgid "DownloadSource|Download"
msgstr ""
-msgid "Error fetching refs"
+msgid "Downvotes"
msgstr ""
-msgid "Error fetching usage ping data."
+msgid "Due date"
msgstr ""
-msgid "Error occurred when toggling the notification subscription"
+msgid "Each Runner can be in one of the following states:"
msgstr ""
-msgid "Error saving label update."
+msgid "Edit"
msgstr ""
-msgid "Error updating status for all todos."
+msgid "Edit Label"
msgstr ""
-msgid "Error updating todo status."
+msgid "Edit Pipeline Schedule %{id}"
msgstr ""
-msgid "EventFilterBy|Filter by all"
+msgid "Edit files in the editor and commit changes here"
msgstr ""
-msgid "EventFilterBy|Filter by comments"
+msgid "Edit identity for %{user_name}"
msgstr ""
-msgid "EventFilterBy|Filter by issue events"
+msgid "Email"
msgstr ""
-msgid "EventFilterBy|Filter by merge events"
+msgid "Email patch"
msgstr ""
-msgid "EventFilterBy|Filter by push events"
+msgid "Emails"
msgstr ""
-msgid "EventFilterBy|Filter by team"
+msgid "Embed"
msgstr ""
-msgid "Every day (at 4:00am)"
+msgid "Enable"
msgstr ""
-msgid "Every month (on the 1st at 4:00am)"
+msgid "Enable Auto DevOps"
msgstr ""
-msgid "Every week (Sundays at 4:00am)"
+msgid "Enable Sentry for error reporting and logging."
msgstr ""
-msgid "Expand"
+msgid "Enable and configure InfluxDB metrics."
msgstr ""
-msgid "Explore projects"
+msgid "Enable and configure Prometheus metrics."
msgstr ""
-msgid "Explore public groups"
+msgid "Enable for this project"
msgstr ""
-msgid "External Classification Policy Authorization"
+msgid "Enable group Runners"
msgstr ""
-msgid "External authentication"
+msgid "Enable or disable certain group features and choose access levels."
msgstr ""
-msgid "External authorization denied access to this project"
+msgid "Enable or disable version check and usage ping."
msgstr ""
-msgid "External authorization request timeout"
+msgid "Enable reCAPTCHA or Akismet and set IP limits."
msgstr ""
-msgid "ExternalAuthorizationService|Classification Label"
+msgid "Enable the Performance Bar for a given group."
msgstr ""
-msgid "ExternalAuthorizationService|Classification label"
+msgid "Ends at (UTC)"
msgstr ""
-msgid "ExternalAuthorizationService|When no classification label is set the default label `%{default_label}` will be used."
+msgid "Environments"
msgstr ""
-msgid "Failed"
+msgid "Environments|An error occurred while fetching the environments."
msgstr ""
-msgid "Failed Jobs"
+msgid "Environments|An error occurred while making the request."
msgstr ""
-msgid "Failed to change the owner"
+msgid "Environments|Commit"
msgstr ""
-msgid "Failed to remove issue from board, please try again."
+msgid "Environments|Deployment"
msgstr ""
-msgid "Failed to remove the pipeline schedule"
+msgid "Environments|Environment"
msgstr ""
-msgid "Failed to update issues, please try again."
+msgid "Environments|Environments"
msgstr ""
-msgid "Feb"
+msgid "Environments|Job"
msgstr ""
-msgid "February"
+msgid "Environments|New environment"
msgstr ""
-msgid "Fields on this page are now uneditable, you can configure"
+msgid "Environments|No deployments yet"
msgstr ""
-msgid "File name"
+msgid "Environments|Open"
msgstr ""
-msgid "Files"
+msgid "Environments|Re-deploy"
msgstr ""
-msgid "Files (%{human_size})"
+msgid "Environments|Read more about environments"
msgstr ""
-msgid "Fill in the fields below, turn on <strong>%{enable_label}</strong>, and press <strong>%{save_changes}</strong>"
+msgid "Environments|Rollback"
msgstr ""
-msgid "Filter by commit message"
+msgid "Environments|Show all"
msgstr ""
-msgid "Find by path"
+msgid "Environments|Updated"
msgstr ""
-msgid "Find file"
+msgid "Environments|You don't have any environments right now."
msgstr ""
-msgid "Finished"
+msgid "Error Reporting and Logging"
msgstr ""
-msgid "FirstPushedBy|First"
+msgid "Error committing changes. Please try again."
msgstr ""
-msgid "FirstPushedBy|pushed by"
+msgid "Error fetching contributors data."
msgstr ""
-msgid "Font Color"
+msgid "Error fetching job trace"
msgstr ""
-msgid "Footer message"
+msgid "Error fetching labels."
msgstr ""
-msgid "Fork"
-msgid_plural "Forks"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "ForkedFromProjectPath|Forked from"
+msgid "Error fetching network graph."
msgstr ""
-msgid "ForkedFromProjectPath|Forked from %{project_name} (deleted)"
+msgid "Error fetching refs"
msgstr ""
-msgid "Forking in progress"
+msgid "Error fetching usage ping data."
msgstr ""
-msgid "Format"
+msgid "Error loading branch data. Please try again."
msgstr ""
-msgid "From %{provider_title}"
+msgid "Error loading last commit."
msgstr ""
-msgid "From issue creation until deploy to production"
+msgid "Error loading merge requests."
msgstr ""
-msgid "From merge request merge until deploy to production"
+msgid "Error loading project data. Please try again."
msgstr ""
-msgid "From the Kubernetes cluster details view, install Runner from the applications list"
+msgid "Error occurred when toggling the notification subscription"
msgstr ""
-msgid "GPG Keys"
+msgid "Error saving label update."
msgstr ""
-msgid "Generate a default set of labels"
+msgid "Error updating status for all todos."
msgstr ""
-msgid "Geo Nodes"
+msgid "Error updating todo status."
msgstr ""
-msgid "Geo allows you to replicate your GitLab instance to other geographical locations."
+msgid "Estimated"
msgstr ""
-msgid "GeoNodeSyncStatus|Node is failing or broken."
+msgid "EventFilterBy|Filter by all"
msgstr ""
-msgid "GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage."
+msgid "EventFilterBy|Filter by comments"
msgstr ""
-msgid "GeoNodes|Checksummed"
+msgid "EventFilterBy|Filter by issue events"
msgstr ""
-msgid "GeoNodes|Database replication lag:"
+msgid "EventFilterBy|Filter by merge events"
msgstr ""
-msgid "GeoNodes|Disabling a node stops the sync process. Are you sure?"
+msgid "EventFilterBy|Filter by push events"
msgstr ""
-msgid "GeoNodes|Does not match the primary storage configuration"
+msgid "EventFilterBy|Filter by team"
msgstr ""
-msgid "GeoNodes|Failed"
+msgid "Every day (at 4:00am)"
msgstr ""
-msgid "GeoNodes|Full"
+msgid "Every month (on the 1st at 4:00am)"
msgstr ""
-msgid "GeoNodes|GitLab version does not match the primary node version"
+msgid "Every week (Sundays at 4:00am)"
msgstr ""
-msgid "GeoNodes|GitLab version:"
+msgid "Expand"
msgstr ""
-msgid "GeoNodes|Health status:"
+msgid "Expand all"
msgstr ""
-msgid "GeoNodes|Last event ID processed by cursor:"
+msgid "Expand sidebar"
msgstr ""
-msgid "GeoNodes|Last event ID seen from primary:"
+msgid "Explore projects"
msgstr ""
-msgid "GeoNodes|Loading nodes"
+msgid "Explore public groups"
msgstr ""
-msgid "GeoNodes|Local Attachments:"
+msgid "Failed"
msgstr ""
-msgid "GeoNodes|Local LFS objects:"
+msgid "Failed Jobs"
msgstr ""
-msgid "GeoNodes|Local job artifacts:"
+msgid "Failed to change the owner"
msgstr ""
-msgid "GeoNodes|New node"
+msgid "Failed to check related branches."
msgstr ""
-msgid "GeoNodes|Node Authentication was successfully repaired."
+msgid "Failed to remove issue from board, please try again."
msgstr ""
-msgid "GeoNodes|Node was successfully removed."
+msgid "Failed to remove the pipeline schedule"
msgstr ""
-msgid "GeoNodes|Not checksummed"
+msgid "Failed to update issues, please try again."
msgstr ""
-msgid "GeoNodes|Out of sync"
+msgid "Failure"
msgstr ""
-msgid "GeoNodes|Removing a node stops the sync process. Are you sure?"
+msgid "Faster as it re-uses the project workspace (falling back to clone if it doesn't exist)"
msgstr ""
-msgid "GeoNodes|Replication slot WAL:"
+msgid "Feb"
msgstr ""
-msgid "GeoNodes|Replication slots:"
+msgid "February"
msgstr ""
-msgid "GeoNodes|Repositories checksummed:"
+msgid "Fields on this page are now uneditable, you can configure"
msgstr ""
-msgid "GeoNodes|Repositories:"
+msgid "Files"
msgstr ""
-msgid "GeoNodes|Repository checksums verified:"
+msgid "Files (%{human_size})"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "Filter by commit message"
msgstr ""
-msgid "GeoNodes|Something went wrong while changing node status"
+msgid "Find by path"
msgstr ""
-msgid "GeoNodes|Something went wrong while removing node"
+msgid "Find file"
msgstr ""
-msgid "GeoNodes|Something went wrong while repairing node"
+msgid "Finished"
msgstr ""
-msgid "GeoNodes|Storage config:"
+msgid "FirstPushedBy|First"
msgstr ""
-msgid "GeoNodes|Sync settings:"
+msgid "FirstPushedBy|pushed by"
msgstr ""
-msgid "GeoNodes|Synced"
+msgid "For internal projects, any logged in user can view pipelines and access job details (output logs and artifacts)"
msgstr ""
-msgid "GeoNodes|Unused slots"
+msgid "For private projects, any member (guest or higher) can view pipelines and access job details (output logs and artifacts)"
msgstr ""
-msgid "GeoNodes|Unverified"
+msgid "For public projects, anyone can view pipelines and access job details (output logs and artifacts)"
msgstr ""
-msgid "GeoNodes|Used slots"
-msgstr ""
+msgid "Fork"
+msgid_plural "Forks"
+msgstr[0] ""
+msgstr[1] ""
-msgid "GeoNodes|Verified"
+msgid "ForkedFromProjectPath|Forked from"
msgstr ""
-msgid "GeoNodes|Wiki checksums verified:"
+msgid "ForkedFromProjectPath|Forked from %{project_name} (deleted)"
msgstr ""
-msgid "GeoNodes|Wikis checksummed:"
+msgid "Forking in progress"
msgstr ""
-msgid "GeoNodes|Wikis:"
+msgid "Format"
msgstr ""
-msgid "GeoNodes|You have configured Geo nodes using an insecure HTTP connection. We recommend the use of HTTPS."
+msgid "Found errors in your .gitlab-ci.yml:"
msgstr ""
-msgid "Geo|All projects"
+msgid "From %{provider_title}"
msgstr ""
-msgid "Geo|File sync capacity"
+msgid "From issue creation until deploy to production"
msgstr ""
-msgid "Geo|Groups to synchronize"
+msgid "From merge request merge until deploy to production"
msgstr ""
-msgid "Geo|Projects in certain groups"
+msgid "From the Kubernetes cluster details view, install Runner from the applications list"
msgstr ""
-msgid "Geo|Projects in certain storage shards"
+msgid "GPG Keys"
msgstr ""
-msgid "Geo|Repository sync capacity"
+msgid "General"
msgstr ""
-msgid "Geo|Select groups to replicate."
+msgid "General pipelines"
msgstr ""
-msgid "Geo|Shards to synchronize"
+msgid "Generate a default set of labels"
msgstr ""
msgid "Git repository URL"
@@ -2157,6 +2354,9 @@ msgstr ""
msgid "Git storage health information has been reset"
msgstr ""
+msgid "Git strategy for pipelines"
+msgstr ""
+
msgid "Git version"
msgstr ""
@@ -2166,21 +2366,24 @@ msgstr ""
msgid "GitLab CI Linter has been moved"
msgstr ""
-msgid "GitLab Geo"
+msgid "GitLab Group Runners can execute code for all the projects in this group."
msgstr ""
msgid "GitLab Runner section"
msgstr ""
-msgid "GitLab single sign on URL"
-msgstr ""
-
msgid "Gitaly"
msgstr ""
msgid "Gitaly Servers"
msgstr ""
+msgid "Gitaly|Address"
+msgstr ""
+
+msgid "Go Back"
+msgstr ""
+
msgid "Go back"
msgstr ""
@@ -2196,22 +2399,19 @@ msgstr ""
msgid "Got it!"
msgstr ""
-msgid "GroupRoadmap|Epics let you manage your portfolio of projects more efficiently and with less effort"
-msgstr ""
-
-msgid "GroupRoadmap|From %{dateWord}"
+msgid "Graph"
msgstr ""
-msgid "GroupRoadmap|Loading roadmap"
+msgid "Group CI/CD settings"
msgstr ""
-msgid "GroupRoadmap|Something went wrong while fetching epics"
+msgid "Group ID"
msgstr ""
-msgid "GroupRoadmap|To view the roadmap, add a planned start or finish date to one of your epics in this group or its subgroups. Only epics in the past 3 months and the next 3 months are shown &ndash; from %{startDate} to %{endDate}."
+msgid "Group Runners"
msgstr ""
-msgid "GroupRoadmap|Until %{dateWord}"
+msgid "Group maintainers can register group runners in the %{link}"
msgstr ""
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
@@ -2238,6 +2438,9 @@ msgstr ""
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
+msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
+msgstr ""
+
msgid "GroupsEmptyState|A group is a collection of several projects."
msgstr ""
@@ -2277,12 +2480,6 @@ msgstr ""
msgid "GroupsTree|Sorry, no groups or projects matched your search"
msgstr ""
-msgid "Have your users email"
-msgstr ""
-
-msgid "Header message"
-msgstr ""
-
msgid "Health Check"
msgstr ""
@@ -2315,19 +2512,49 @@ msgid_plural "Hide values"
msgstr[0] ""
msgstr[1] ""
+msgid "Hide whitespace changes"
+msgstr ""
+
msgid "History"
msgstr ""
msgid "Housekeeping successfully started"
msgstr ""
-msgid "Identity provider single sign on URL"
+msgid "I accept the %{terms_link}"
+msgstr ""
+
+msgid "I accept the|Terms of Service and Privacy Policy"
+msgstr ""
+
+msgid "ID"
+msgstr ""
+
+msgid "IDE|Commit"
+msgstr ""
+
+msgid "IDE|Edit"
+msgstr ""
+
+msgid "IDE|Go back"
+msgstr ""
+
+msgid "IDE|Open in file view"
+msgstr ""
+
+msgid "IDE|Review"
+msgstr ""
+
+msgid "Identifier"
+msgstr ""
+
+msgid "Identities"
msgstr ""
-msgid "If enabled, access to projects will be validated on an external service using their classification label."
+msgid "If disabled, the access level will depend on the user's permissions in the project."
msgstr ""
-msgid "If using GitHub, you’ll see pipeline statuses on GitHub for your commits and pull requests. %{more_info_link}"
+msgid "If enabled"
msgstr ""
msgid "If you already have files you can push them using the %{link_to_cli} below."
@@ -2336,6 +2563,15 @@ msgstr ""
msgid "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>."
msgstr ""
+msgid "ImageDiffViewer|2-up"
+msgstr ""
+
+msgid "ImageDiffViewer|Onion skin"
+msgstr ""
+
+msgid "ImageDiffViewer|Swipe"
+msgstr ""
+
msgid "Import"
msgstr ""
@@ -2351,16 +2587,10 @@ msgstr ""
msgid "Import repository"
msgstr ""
-msgid "ImportButtons|Connect repositories from"
-msgstr ""
-
-msgid "Improve Issue boards with GitLab Enterprise Edition."
+msgid "Include a Terms of Service agreement and Privacy Policy that all users must accept."
msgstr ""
-msgid "Improve issues management with Issue weight and GitLab Enterprise Edition."
-msgstr ""
-
-msgid "Improve search with Advanced Global Search and GitLab Enterprise Edition."
+msgid "Inline"
msgstr ""
msgid "Install Runner on Kubernetes"
@@ -2369,17 +2599,15 @@ msgstr ""
msgid "Install a Runner compatible with GitLab CI"
msgstr ""
-msgid "Instance"
-msgid_plural "Instances"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "Instance does not support multiple Kubernetes clusters"
msgstr ""
msgid "Integrations"
msgstr ""
+msgid "Integrations Settings"
+msgstr ""
+
msgid "Interested parties can even contribute by pushing commits if they want to."
msgstr ""
@@ -2395,7 +2623,7 @@ msgstr ""
msgid "Introducing Cycle Analytics"
msgstr ""
-msgid "Issue board focus mode"
+msgid "Issue Board"
msgstr ""
msgid "Issue events"
@@ -2404,9 +2632,6 @@ msgstr ""
msgid "IssueBoards|Board"
msgstr ""
-msgid "IssueBoards|Boards"
-msgstr ""
-
msgid "Issues"
msgstr ""
@@ -2419,6 +2644,12 @@ msgstr ""
msgid "January"
msgstr ""
+msgid "Job"
+msgstr ""
+
+msgid "Job has been erased"
+msgstr ""
+
msgid "Jobs"
msgstr ""
@@ -2461,6 +2692,9 @@ msgstr ""
msgid "Kubernetes service integration has been deprecated. %{deprecated_message_content} your Kubernetes clusters using the new <a href=\"%{url}\"/>Kubernetes Clusters</a> page"
msgstr ""
+msgid "LFS"
+msgstr ""
+
msgid "LFSStatus|Disabled"
msgstr ""
@@ -2470,6 +2704,9 @@ msgstr ""
msgid "Label"
msgstr ""
+msgid "Label actions dropdown"
+msgstr ""
+
msgid "LabelSelect|%{firstLabelName} +%{remainingLabelCount} more"
msgstr ""
@@ -2485,6 +2722,9 @@ msgstr ""
msgid "Labels can be applied to issues and merge requests to categorize them."
msgstr ""
+msgid "Labels can be applied to issues and merge requests."
+msgstr ""
+
msgid "Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>"
msgstr ""
@@ -2520,6 +2760,9 @@ msgstr ""
msgid "LastPushEvent|at"
msgstr ""
+msgid "Latest changes"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -2544,9 +2787,6 @@ msgstr ""
msgid "Leave project"
msgstr ""
-msgid "License"
-msgstr ""
-
msgid "List"
msgstr ""
@@ -2556,6 +2796,9 @@ msgstr ""
msgid "Loading the GitLab IDE..."
msgstr ""
+msgid "Loading..."
+msgstr ""
+
msgid "Lock"
msgstr ""
@@ -2565,21 +2808,18 @@ msgstr ""
msgid "Lock not found"
msgstr ""
-msgid "Locked"
+msgid "Lock to current projects"
msgstr ""
-msgid "Locked Files"
+msgid "Locked"
msgstr ""
-msgid "Locks give the ability to lock specific file or folder."
+msgid "Locked to current projects"
msgstr ""
msgid "Login"
msgstr ""
-msgid "Make everyone on your team more productive regardless of their location. GitLab Geo creates read-only mirrors of your GitLab instance so you can reduce the time it takes to clone and fetch large repos."
-msgstr ""
-
msgid "Manage all notifications"
msgstr ""
@@ -2592,16 +2832,16 @@ msgstr ""
msgid "Manage project labels"
msgstr ""
-msgid "Manage your group’s membership while adding another level of security with SAML."
-msgstr ""
-
msgid "Mar"
msgstr ""
msgid "March"
msgstr ""
-msgid "Mark done"
+msgid "Mark todo as done"
+msgstr ""
+
+msgid "Markdown enabled"
msgstr ""
msgid "Maximum git storage failures"
@@ -2616,7 +2856,7 @@ msgstr ""
msgid "Members"
msgstr ""
-msgid "Members will be forwarded here when signing in to your group. Get this from your identity provider, where it can also be called \"SSO Service Location\", \"SAML Token Issuance Endpoint\", or \"SAML 2.0/W-Federation URL\"."
+msgid "Merge Request:"
msgstr ""
msgid "Merge Requests"
@@ -2628,91 +2868,46 @@ msgstr ""
msgid "Merge request"
msgstr ""
-msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
+msgid "Merge requests"
msgstr ""
-msgid "Merged"
-msgstr ""
-
-msgid "Messages"
-msgstr ""
-
-msgid "Metrics - Influx"
-msgstr ""
-
-msgid "Metrics - Prometheus"
-msgstr ""
-
-msgid "Metrics|Business"
-msgstr ""
-
-msgid "Metrics|Create metric"
-msgstr ""
-
-msgid "Metrics|Edit metric"
-msgstr ""
-
-msgid "Metrics|For grouping similar metrics"
-msgstr ""
-
-msgid "Metrics|Label of the chart's vertical axis. Usually the type of the unit being charted. The horizontal axis (X-axis) always represents time."
-msgstr ""
-
-msgid "Metrics|Legend label (optional)"
-msgstr ""
-
-msgid "Metrics|Must be a valid PromQL query."
-msgstr ""
-
-msgid "Metrics|Name"
-msgstr ""
-
-msgid "Metrics|New metric"
-msgstr ""
-
-msgid "Metrics|Prometheus Query Documentation"
-msgstr ""
-
-msgid "Metrics|Query"
-msgstr ""
-
-msgid "Metrics|Response"
+msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
msgstr ""
-msgid "Metrics|System"
+msgid "MergeRequests|Resolve this discussion in a new issue"
msgstr ""
-msgid "Metrics|Type"
+msgid "MergeRequests|Saving the comment failed"
msgstr ""
-msgid "Metrics|Unit label"
+msgid "MergeRequests|Toggle comments for this file"
msgstr ""
-msgid "Metrics|Used as a title for the chart"
+msgid "MergeRequests|Updating discussions failed"
msgstr ""
-msgid "Metrics|Used if the query returns a single series. If it returns multiple series, their legend labels will be picked up from the response."
+msgid "MergeRequests|View file @ %{commitId}"
msgstr ""
-msgid "Metrics|Y-axis label"
+msgid "MergeRequests|View replaced file @ %{commitId}"
msgstr ""
-msgid "Metrics|e.g. HTTP requests"
+msgid "Merged"
msgstr ""
-msgid "Metrics|e.g. Requests/second"
+msgid "Messages"
msgstr ""
-msgid "Metrics|e.g. Throughput"
+msgid "Metrics - Influx"
msgstr ""
-msgid "Metrics|e.g. rate(http_requests_total[5m])"
+msgid "Metrics - Prometheus"
msgstr ""
-msgid "Metrics|e.g. req/sec"
+msgid "Milestone"
msgstr ""
-msgid "Milestone"
+msgid "Milestones"
msgstr ""
msgid "Milestones|Delete milestone"
@@ -2733,9 +2928,6 @@ msgstr ""
msgid "Milestones|Promote Milestone"
msgstr ""
-msgid "Milestones|This action cannot be reversed."
-msgstr ""
-
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr ""
@@ -2748,7 +2940,7 @@ msgstr ""
msgid "Monitoring"
msgstr ""
-msgid "More info"
+msgid "More actions"
msgstr ""
msgid "More information"
@@ -2763,12 +2955,30 @@ msgstr ""
msgid "Move issue"
msgstr ""
-msgid "Multiple issue boards"
+msgid "Name"
msgstr ""
msgid "Name new label"
msgstr ""
+msgid "Name your individual key via a title"
+msgstr ""
+
+msgid "Nav|Help"
+msgstr ""
+
+msgid "Nav|Home"
+msgstr ""
+
+msgid "Nav|Sign In / Register"
+msgstr ""
+
+msgid "Nav|Sign out and sign in with a different account"
+msgstr ""
+
+msgid "New Identity"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] ""
@@ -2780,6 +2990,9 @@ msgstr ""
msgid "New Kubernetes cluster"
msgstr ""
+msgid "New Label"
+msgstr ""
+
msgid "New Pipeline Schedule"
msgstr ""
@@ -2792,15 +3005,15 @@ msgstr ""
msgid "New directory"
msgstr ""
-msgid "New epic"
-msgstr ""
-
msgid "New file"
msgstr ""
msgid "New group"
msgstr ""
+msgid "New identity"
+msgstr ""
+
msgid "New issue"
msgstr ""
@@ -2810,6 +3023,9 @@ msgstr ""
msgid "New merge request"
msgstr ""
+msgid "New pipelines will cancel older, pending pipelines on the same branch"
+msgstr ""
+
msgid "New project"
msgstr ""
@@ -2825,7 +3041,7 @@ msgstr ""
msgid "New tag"
msgstr ""
-msgid "No Label"
+msgid "No"
msgstr ""
msgid "No assignee"
@@ -2846,7 +3062,16 @@ msgstr ""
msgid "No file chosen"
msgstr ""
-msgid "No labels created yet."
+msgid "No files found"
+msgstr ""
+
+msgid "No files found."
+msgstr ""
+
+msgid "No merge requests found"
+msgstr ""
+
+msgid "No messages were logged"
msgstr ""
msgid "No repository"
@@ -2879,15 +3104,9 @@ msgstr ""
msgid "Note that the master branch is automatically protected. %{link_to_protected_branches}"
msgstr ""
-msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow connecting repositories without generating a Personal Access Token."
-msgstr ""
-
msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
msgstr ""
-msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow connecting repositories without generating a Personal Access Token."
-msgstr ""
-
msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
msgstr ""
@@ -2963,9 +3182,6 @@ msgstr ""
msgid "Number of access attempts"
msgstr ""
-msgid "OK"
-msgstr ""
-
msgid "Oct"
msgstr ""
@@ -2975,19 +3191,16 @@ msgstr ""
msgid "OfSearchInADropdown|Filter"
msgstr ""
-msgid "Once imported, repositories can be mirrored over SSH. Read more %{ssh_link}"
-msgstr ""
-
msgid "Online IDE integration settings."
msgstr ""
-msgid "Only project members can comment."
+msgid "Only comments from the following commit are shown below"
msgstr ""
-msgid "Open"
+msgid "Only project members can comment."
msgstr ""
-msgid "Opened"
+msgid "Open in Xcode"
msgstr ""
msgid "OpenedNDaysAgo|Opened"
@@ -2996,9 +3209,18 @@ msgstr ""
msgid "Opens in a new window"
msgstr ""
+msgid "Operations"
+msgstr ""
+
msgid "Options"
msgstr ""
+msgid "Or you can choose one of the suggested colors below"
+msgstr ""
+
+msgid "Other Labels"
+msgstr ""
+
msgid "Otherwise it is recommended you start with one of the options below."
msgstr ""
@@ -3032,12 +3254,27 @@ msgstr ""
msgid "Password"
msgstr ""
+msgid "Paste your public SSH key, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'. Don't use your private SSH key."
+msgstr ""
+
+msgid "Pause"
+msgstr ""
+
msgid "Pending"
msgstr ""
+msgid "Per job. If a job passes this threshold, it will be marked as failed"
+msgstr ""
+
+msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgstr ""
+
msgid "Performance optimization"
msgstr ""
+msgid "Permissions"
+msgstr ""
+
msgid "Personal Access Token"
msgstr ""
@@ -3053,7 +3290,7 @@ msgstr ""
msgid "Pipeline Schedules"
msgstr ""
-msgid "Pipeline quota"
+msgid "Pipeline triggers"
msgstr ""
msgid "PipelineCharts|Failed:"
@@ -3152,10 +3389,22 @@ msgstr ""
msgid "Pipelines|This project is not currently set up to run pipelines."
msgstr ""
-msgid "Pipeline|Retry pipeline"
+msgid "Pipeline|Create for"
+msgstr ""
+
+msgid "Pipeline|Create pipeline"
+msgstr ""
+
+msgid "Pipeline|Existing branch name or tag"
+msgstr ""
+
+msgid "Pipeline|Run Pipeline"
+msgstr ""
+
+msgid "Pipeline|Search branches"
msgstr ""
-msgid "Pipeline|Retry pipeline #%{pipelineId}?"
+msgid "Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default."
msgstr ""
msgid "Pipeline|Stop pipeline"
@@ -3164,7 +3413,7 @@ msgstr ""
msgid "Pipeline|Stop pipeline #%{pipelineId}?"
msgstr ""
-msgid "Pipeline|You’re about to retry pipeline %{pipelineId}."
+msgid "Pipeline|Variables"
msgstr ""
msgid "Pipeline|You’re about to stop pipeline %{pipelineId}."
@@ -3182,19 +3431,25 @@ msgstr ""
msgid "Pipeline|with stages"
msgstr ""
+msgid "Plain diff"
+msgstr ""
+
msgid "PlantUML"
msgstr ""
msgid "Play"
msgstr ""
-msgid "Please <a href=%{link_to_billing} target=\"_blank\" rel=\"noopener noreferrer\">enable billing for one of your projects to be able to create a Kubernetes cluster</a>, then try again."
+msgid "Please accept the Terms of Service before continuing."
+msgstr ""
+
+msgid "Please select at least one filter to see results"
msgstr ""
msgid "Please solve the reCAPTCHA"
msgstr ""
-msgid "Please wait while we connect to your repository. Refresh at will."
+msgid "Please try again"
msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
@@ -3203,7 +3458,19 @@ msgstr ""
msgid "Preferences"
msgstr ""
-msgid "Primary"
+msgid "Preferences|Navigation theme"
+msgstr ""
+
+msgid "Prioritize"
+msgstr ""
+
+msgid "Prioritize label"
+msgstr ""
+
+msgid "Prioritized Labels"
+msgstr ""
+
+msgid "Prioritized label"
msgstr ""
msgid "Private - Project access must be granted explicitly to each user."
@@ -3221,6 +3488,12 @@ msgstr ""
msgid "Profiles|Account scheduled for removal."
msgstr ""
+msgid "Profiles|Change username"
+msgstr ""
+
+msgid "Profiles|Current path: %{path}"
+msgstr ""
+
msgid "Profiles|Delete Account"
msgstr ""
@@ -3239,9 +3512,21 @@ msgstr ""
msgid "Profiles|Invalid username"
msgstr ""
+msgid "Profiles|Path"
+msgstr ""
+
msgid "Profiles|Type your %{confirmationValue} to confirm:"
msgstr ""
+msgid "Profiles|Update username"
+msgstr ""
+
+msgid "Profiles|Username change failed - %{message}"
+msgstr ""
+
+msgid "Profiles|Username successfully changed"
+msgstr ""
+
msgid "Profiles|You don't have access to delete this user."
msgstr ""
@@ -3260,6 +3545,12 @@ msgstr ""
msgid "Programming languages used in this repository"
msgstr ""
+msgid "Progress"
+msgstr ""
+
+msgid "Project"
+msgstr ""
+
msgid "Project '%{project_name}' is in the process of being deleted."
msgstr ""
@@ -3272,6 +3563,9 @@ msgstr ""
msgid "Project '%{project_name}' was successfully updated."
msgstr ""
+msgid "Project Badges"
+msgstr ""
+
msgid "Project access must be granted explicitly to each user."
msgstr ""
@@ -3299,30 +3593,6 @@ msgstr ""
msgid "ProjectActivityRSS|Subscribe"
msgstr ""
-msgid "ProjectCreationLevel|Allowed to create projects"
-msgstr ""
-
-msgid "ProjectCreationLevel|Default project creation protection"
-msgstr ""
-
-msgid "ProjectCreationLevel|Developers + Masters"
-msgstr ""
-
-msgid "ProjectCreationLevel|Masters"
-msgstr ""
-
-msgid "ProjectCreationLevel|No one"
-msgstr ""
-
-msgid "ProjectFeature|Disabled"
-msgstr ""
-
-msgid "ProjectFeature|Everyone with access"
-msgstr ""
-
-msgid "ProjectFeature|Only team members"
-msgstr ""
-
msgid "ProjectFileTree|Name"
msgstr ""
@@ -3332,27 +3602,6 @@ msgstr ""
msgid "ProjectLifecycle|Stage"
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 ""
@@ -3377,6 +3626,9 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr ""
+msgid "PrometheusDashboard|Time"
+msgstr ""
+
msgid "PrometheusService|%{exporters} with %{metrics} were found"
msgstr ""
@@ -3398,18 +3650,9 @@ msgstr ""
msgid "PrometheusService|Common metrics"
msgstr ""
-msgid "PrometheusService|Common metrics are automatically monitored based on a library of metrics from popular exporters."
-msgstr ""
-
-msgid "PrometheusService|Custom metrics"
-msgstr ""
-
msgid "PrometheusService|Finding and configuring metrics..."
msgstr ""
-msgid "PrometheusService|Finding custom metrics..."
-msgstr ""
-
msgid "PrometheusService|Install Prometheus on clusters"
msgstr ""
@@ -3422,13 +3665,13 @@ msgstr ""
msgid "PrometheusService|Metrics"
msgstr ""
-msgid "PrometheusService|Missing environment variable"
+msgid "PrometheusService|Metrics are automatically configured and monitored based on a library of metrics from popular exporters."
msgstr ""
-msgid "PrometheusService|More information"
+msgid "PrometheusService|Missing environment variable"
msgstr ""
-msgid "PrometheusService|New metric"
+msgid "PrometheusService|More information"
msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
@@ -3437,9 +3680,6 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
-msgstr ""
-
msgid "PrometheusService|Time-series monitoring service"
msgstr ""
@@ -3455,22 +3695,28 @@ msgstr ""
msgid "Promote"
msgstr ""
-msgid "Promote to Group Label"
+msgid "Promote these project milestones into a group milestone."
msgstr ""
msgid "Promote to Group Milestone"
msgstr ""
+msgid "Promote to group label"
+msgstr ""
+
msgid "Protip:"
msgstr ""
+msgid "Provider"
+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"
+msgid "Public pipelines"
msgstr ""
msgid "Push events"
@@ -3482,10 +3728,10 @@ msgstr ""
msgid "Push to create a project"
msgstr ""
-msgid "PushRule|Committer restriction"
+msgid "Quick actions can be used in the issues description and comment boxes."
msgstr ""
-msgid "Quick actions can be used in the issues description and comment boxes."
+msgid "Re-deploy"
msgstr ""
msgid "Read more"
@@ -3497,16 +3743,16 @@ msgstr ""
msgid "Real-time features"
msgstr ""
-msgid "RefSwitcher|Branches"
+msgid "Reference:"
msgstr ""
-msgid "RefSwitcher|Tags"
+msgid "Register / Sign In"
msgstr ""
-msgid "Reference:"
+msgid "Register and see your runners for this group."
msgstr ""
-msgid "Register / Sign In"
+msgid "Register and see your runners for this project."
msgstr ""
msgid "Registry"
@@ -3539,28 +3785,28 @@ msgstr ""
msgid "Remove"
msgstr ""
-msgid "Remove avatar"
+msgid "Remove Runner"
msgstr ""
-msgid "Remove project"
+msgid "Remove avatar"
msgstr ""
-msgid "Repair authentication"
+msgid "Remove priority"
msgstr ""
-msgid "Repo by URL"
+msgid "Remove project"
msgstr ""
msgid "Repository"
msgstr ""
-msgid "Repository has no locks."
+msgid "Repository Settings"
msgstr ""
msgid "Repository maintenance"
msgstr ""
-msgid "Repository mirror settings"
+msgid "Repository mirror"
msgstr ""
msgid "Repository storage"
@@ -3569,6 +3815,9 @@ msgstr ""
msgid "Request Access"
msgstr ""
+msgid "Require all users to accept Terms of Service and Privacy Policy when they access GitLab."
+msgstr ""
+
msgid "Reset git storage health information"
msgstr ""
@@ -3578,10 +3827,25 @@ msgstr ""
msgid "Reset runners registration token"
msgstr ""
+msgid "Resolve all discussions in new issue"
+msgstr ""
+
+msgid "Resolve conflicts on source branch"
+msgstr ""
+
msgid "Resolve discussion"
msgstr ""
-msgid "Response"
+msgid "Resume"
+msgstr ""
+
+msgid "Retry"
+msgstr ""
+
+msgid "Retry this job"
+msgstr ""
+
+msgid "Retry verification"
msgstr ""
msgid "Reveal value"
@@ -3595,7 +3859,7 @@ msgstr ""
msgid "Revert this merge request"
msgstr ""
-msgid "Review the process for configuring service providers in your identity provider — in this case, GitLab is the \"service provider\" or \"relying party\"."
+msgid "Review"
msgstr ""
msgid "Reviewing"
@@ -3604,28 +3868,31 @@ msgstr ""
msgid "Reviewing (merge request !%{mergeRequestId})"
msgstr ""
-msgid "Roadmap"
+msgid "Rollback"
msgstr ""
-msgid "Run CI/CD pipelines for external repositories"
+msgid "Runner token"
msgstr ""
msgid "Runners"
msgstr ""
-msgid "Running"
+msgid "Runners API"
+msgstr ""
+
+msgid "Runners can be placed on separate users, servers, and even on your local machine."
msgstr ""
-msgid "SAML Single Sign On"
+msgid "Running"
msgstr ""
-msgid "SAML Single Sign On Settings"
+msgid "SSH Keys"
msgstr ""
-msgid "SHA1 fingerprint of the SAML token signing certificate. Get this from your identity provider, where it can also be called \"Thumbprint\"."
+msgid "SSL Verification"
msgstr ""
-msgid "SSH Keys"
+msgid "Save"
msgstr ""
msgid "Save changes"
@@ -3649,15 +3916,30 @@ msgstr ""
msgid "Scheduling Pipelines"
msgstr ""
-msgid "Scoped issue boards"
+msgid "Scroll to bottom"
+msgstr ""
+
+msgid "Scroll to top"
msgstr ""
msgid "Search"
msgstr ""
+msgid "Search branches"
+msgstr ""
+
msgid "Search branches and tags"
msgstr ""
+msgid "Search files"
+msgstr ""
+
+msgid "Search for projects, issues, etc."
+msgstr ""
+
+msgid "Search merge requests"
+msgstr ""
+
msgid "Search milestones"
msgstr ""
@@ -3673,13 +3955,13 @@ msgstr ""
msgid "Seconds to wait for a storage access attempt"
msgstr ""
-msgid "Secret variables"
+msgid "Select"
msgstr ""
-msgid "Security report"
+msgid "Select Archive Format"
msgstr ""
-msgid "Select Archive Format"
+msgid "Select a namespace to fork the project"
msgstr ""
msgid "Select a timezone"
@@ -3694,10 +3976,19 @@ msgstr ""
msgid "Select branch/tag"
msgstr ""
-msgid "Select target branch"
+msgid "Select project"
msgstr ""
-msgid "Selective synchronization"
+msgid "Select project and zone to choose machine type"
+msgstr ""
+
+msgid "Select project to choose zone"
+msgstr ""
+
+msgid "Select source branch"
+msgstr ""
+
+msgid "Select target branch"
msgstr ""
msgid "Send email"
@@ -3715,9 +4006,6 @@ msgstr ""
msgid "Service Templates"
msgstr ""
-msgid "Service URL"
-msgstr ""
-
msgid "Session expiration, projects limit and attachment size."
msgstr ""
@@ -3742,9 +4030,6 @@ msgstr ""
msgid "Set up Koding"
msgstr ""
-msgid "Set up assertions/attributes/claims (email, first_name, last_name) and NameID according to %{docsLinkStart}the documentation %{icon}%{docsLinkEnd}"
-msgstr ""
-
msgid "SetPasswordToCloneLink|set a password"
msgstr ""
@@ -3754,19 +4039,22 @@ msgstr ""
msgid "Setup a specific Runner automatically"
msgstr ""
-msgid "Share the <strong>%{sso_label}</strong> with members so they can sign in to your group through your identity provider"
+msgid "Share"
msgstr ""
-msgid "SharedRunnersMinutesSettings|By resetting the pipeline minutes for this namespace, the currently used minutes will be set to zero."
+msgid "Shared Runners"
msgstr ""
-msgid "SharedRunnersMinutesSettings|Reset pipeline minutes"
+msgid "Show command"
msgstr ""
-msgid "SharedRunnersMinutesSettings|Reset used pipeline minutes"
+msgid "Show complete raw log"
msgstr ""
-msgid "Show command"
+msgid "Show latest version"
+msgstr ""
+
+msgid "Show latest version of the diff"
msgstr ""
msgid "Show parent pages"
@@ -3775,21 +4063,18 @@ msgstr ""
msgid "Show parent subgroups"
msgstr ""
+msgid "Show whitespace changes"
+msgstr ""
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] ""
msgstr[1] ""
-msgid "Sidebar|Change weight"
+msgid "Side-by-side"
msgstr ""
-msgid "Sidebar|No"
-msgstr ""
-
-msgid "Sidebar|None"
-msgstr ""
-
-msgid "Sidebar|Weight"
+msgid "Sign out"
msgstr ""
msgid "Sign-in restrictions"
@@ -3801,7 +4086,7 @@ msgstr ""
msgid "Size and domain settings for static websites"
msgstr ""
-msgid "Slack application"
+msgid "Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job"
msgstr ""
msgid "Snippets"
@@ -3813,13 +4098,13 @@ msgstr ""
msgid "Something went wrong on our end."
msgstr ""
-msgid "Something went wrong when toggling the button"
+msgid "Something went wrong on our end. Please try again!"
msgstr ""
-msgid "Something went wrong while fetching Dependency Scanning."
+msgid "Something went wrong when toggling the button"
msgstr ""
-msgid "Something went wrong while fetching SAST."
+msgid "Something went wrong while closing the %{issuable}. Please try again later"
msgstr ""
msgid "Something went wrong while fetching the projects."
@@ -3828,6 +4113,12 @@ msgstr ""
msgid "Something went wrong while fetching the registry list."
msgstr ""
+msgid "Something went wrong while reopening the %{issuable}. Please try again later"
+msgstr ""
+
+msgid "Something went wrong while resolving this discussion. Please try again."
+msgstr ""
+
msgid "Something went wrong. Please try again."
msgstr ""
@@ -3873,9 +4164,6 @@ msgstr ""
msgid "SortOptions|Least popular"
msgstr ""
-msgid "SortOptions|Less weight"
-msgstr ""
-
msgid "SortOptions|Milestone"
msgstr ""
@@ -3885,9 +4173,6 @@ msgstr ""
msgid "SortOptions|Milestone due soon"
msgstr ""
-msgid "SortOptions|More weight"
-msgstr ""
-
msgid "SortOptions|Most popular"
msgstr ""
@@ -3927,9 +4212,6 @@ msgstr ""
msgid "SortOptions|Start soon"
msgstr ""
-msgid "SortOptions|Weight"
-msgstr ""
-
msgid "Source"
msgstr ""
@@ -3948,9 +4230,36 @@ msgstr ""
msgid "Spam and Anti-bot Protection"
msgstr ""
+msgid "Specific Runners"
+msgstr ""
+
msgid "Specify the following URL during the Runner setup:"
msgstr ""
+msgid "Squash commits"
+msgstr ""
+
+msgid "Stage"
+msgstr ""
+
+msgid "Stage & Commit"
+msgstr ""
+
+msgid "Stage all changes"
+msgstr ""
+
+msgid "Stage changes"
+msgstr ""
+
+msgid "Staged"
+msgstr ""
+
+msgid "Staged %{type}"
+msgstr ""
+
+msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
+msgstr ""
+
msgid "StarProject|Star"
msgstr ""
@@ -3972,12 +4281,15 @@ msgstr ""
msgid "Started"
msgstr ""
-msgid "State your message to activate"
+msgid "Starts at (UTC)"
msgstr ""
msgid "Status"
msgstr ""
+msgid "Stop this environment"
+msgstr ""
+
msgid "Stopped"
msgstr ""
@@ -3987,16 +4299,19 @@ msgstr ""
msgid "Subgroups"
msgstr ""
-msgid "Switch branch/tag"
+msgid "Subscribe"
msgstr ""
-msgid "System"
+msgid "Subscribe at group level"
msgstr ""
-msgid "System Hooks"
+msgid "Subscribe at project level"
msgstr ""
-msgid "System header and footer:"
+msgid "Switch branch/tag"
+msgstr ""
+
+msgid "System Hooks"
msgstr ""
msgid "Tag (%{tag_count})"
@@ -4007,6 +4322,9 @@ msgstr[1] ""
msgid "Tags"
msgstr ""
+msgid "Tags:"
+msgstr ""
+
msgid "TagsPage|Browse commits"
msgstr ""
@@ -4070,7 +4388,7 @@ msgstr ""
msgid "TagsPage|Use git tag command to add a new one:"
msgstr ""
-msgid "TagsPage|Write your release notes or drag files here..."
+msgid "TagsPage|Write your release notes or drag files here…"
msgstr ""
msgid "TagsPage|protected"
@@ -4085,19 +4403,19 @@ msgstr ""
msgid "Team"
msgstr ""
-msgid "Thanks! Don't show me this again"
+msgid "Terms of Service Agreement and Privacy Policy"
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."
+msgid "Terms of Service and Privacy Policy"
msgstr ""
-msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project"
+msgid "Test coverage parsing"
msgstr ""
-msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
+msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project"
msgstr ""
-msgid "The X509 Certificate to use when mutual TLS is required to communicate with the external authorization service. If left blank, the server certificate is still validated when accessing over HTTPS."
+msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
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."
@@ -4106,9 +4424,6 @@ msgstr ""
msgid "The collection of events added to the data gathered for that stage."
msgstr ""
-msgid "The connection will time out after %{timeout}. For repositories that take longer, use a clone/push combination."
-msgstr ""
-
msgid "The fork relationship has been removed."
msgstr ""
@@ -4127,7 +4442,7 @@ 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 passphrase required to decrypt the private key. This is optional and the value is encrypted at rest."
+msgid "The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>"
msgstr ""
msgid "The phase of the development lifecycle."
@@ -4136,9 +4451,6 @@ msgstr ""
msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit."
msgstr ""
-msgid "The private key to use when a client certificate is provided. This value is encrypted at rest."
-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 ""
@@ -4160,7 +4472,7 @@ msgstr ""
msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request."
msgstr ""
-msgid "The roadmap shows the progress of your epics along a timeline"
+msgid "The secure token used by the Runner to checkout the project"
msgstr ""
msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time."
@@ -4187,13 +4499,19 @@ msgstr ""
msgid "There are no issues to show"
msgstr ""
+msgid "There are no labels yet"
+msgstr ""
+
msgid "There are no merge requests to show"
msgstr ""
msgid "There are problems accessing Git storage: "
msgstr ""
-msgid "There was an error loading results"
+msgid "There was an error loading jobs"
+msgstr ""
+
+msgid "There was an error loading latest pipeline"
msgstr ""
msgid "There was an error loading users activity calendar."
@@ -4214,12 +4532,21 @@ msgstr ""
msgid "There was an error when unsubscribing from this label."
msgstr ""
-msgid "This board\\'s scope is reduced"
+msgid "They can be managed using the %{link}."
+msgstr ""
+
+msgid "This GitLab instance does not provide any shared Runners yet. Instance administrators can register shared Runners in the admin area."
+msgstr ""
+
+msgid "This diff is collapsed."
msgstr ""
msgid "This directory"
msgstr ""
+msgid "This group does not provide any group Runners yet."
+msgstr ""
+
msgid "This is a confidential issue."
msgstr ""
@@ -4241,6 +4568,15 @@ msgstr ""
msgid "This job depends on upstream jobs that need to succeed in order for this job to be triggered"
msgstr ""
+msgid "This job does not have a trace."
+msgstr ""
+
+msgid "This job has been canceled"
+msgstr ""
+
+msgid "This job has been skipped"
+msgstr ""
+
msgid "This job has not been triggered yet"
msgstr ""
@@ -4259,19 +4595,28 @@ msgstr ""
msgid "This merge request is locked."
msgstr ""
+msgid "This option is disabled while you still have unstaged changes"
+msgstr ""
+
msgid "This page is unavailable because you are not allowed to read information across multiple projects."
msgstr ""
+msgid "This page will be removed in a future release."
+msgstr ""
+
msgid "This project"
msgstr ""
+msgid "This project does not belong to a group and can therefore not make use of group Runners."
+msgstr ""
+
msgid "This repository"
msgstr ""
-msgid "This will delete the custom metric, Are you sure?"
+msgid "This source diff could not be displayed because it is too large."
msgstr ""
-msgid "Those emails automatically become issues (with the comments becoming the email conversation) listed here."
+msgid "This user has no identities"
msgstr ""
msgid "Time before an issue gets scheduled"
@@ -4283,10 +4628,10 @@ msgstr ""
msgid "Time between merge request creation and merge/close"
msgstr ""
-msgid "Time between updates and capacity settings."
+msgid "Time remaining"
msgstr ""
-msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
+msgid "Time spent"
msgstr ""
msgid "Time tracking"
@@ -4310,6 +4655,9 @@ msgstr ""
msgid "Timeago|%s days remaining"
msgstr ""
+msgid "Timeago|%s hours ago"
+msgstr ""
+
msgid "Timeago|%s hours remaining"
msgstr ""
@@ -4325,6 +4673,9 @@ msgstr ""
msgid "Timeago|%s months remaining"
msgstr ""
+msgid "Timeago|%s seconds ago"
+msgstr ""
+
msgid "Timeago|%s seconds remaining"
msgstr ""
@@ -4340,46 +4691,43 @@ msgstr ""
msgid "Timeago|%s years remaining"
msgstr ""
-msgid "Timeago|1 day remaining"
-msgstr ""
-
-msgid "Timeago|1 hour remaining"
+msgid "Timeago|1 day ago"
msgstr ""
-msgid "Timeago|1 minute remaining"
+msgid "Timeago|1 day remaining"
msgstr ""
-msgid "Timeago|1 month remaining"
+msgid "Timeago|1 hour ago"
msgstr ""
-msgid "Timeago|1 week remaining"
+msgid "Timeago|1 hour remaining"
msgstr ""
-msgid "Timeago|1 year remaining"
+msgid "Timeago|1 minute ago"
msgstr ""
-msgid "Timeago|Past due"
+msgid "Timeago|1 minute remaining"
msgstr ""
-msgid "Timeago|a day ago"
+msgid "Timeago|1 month ago"
msgstr ""
-msgid "Timeago|a month ago"
+msgid "Timeago|1 month remaining"
msgstr ""
-msgid "Timeago|a week ago"
+msgid "Timeago|1 week ago"
msgstr ""
-msgid "Timeago|a year ago"
+msgid "Timeago|1 week remaining"
msgstr ""
-msgid "Timeago|about %s hours ago"
+msgid "Timeago|1 year ago"
msgstr ""
-msgid "Timeago|about a minute ago"
+msgid "Timeago|1 year remaining"
msgstr ""
-msgid "Timeago|about an hour ago"
+msgid "Timeago|Past due"
msgstr ""
msgid "Timeago|in %s days"
@@ -4421,10 +4769,13 @@ msgstr ""
msgid "Timeago|in 1 year"
msgstr ""
-msgid "Timeago|in a while"
+msgid "Timeago|just now"
msgstr ""
-msgid "Timeago|less than a minute ago"
+msgid "Timeago|right now"
+msgstr ""
+
+msgid "Timeout"
msgstr ""
msgid "Time|hr"
@@ -4443,19 +4794,10 @@ msgstr ""
msgid "Tip:"
msgstr ""
-msgid "Title"
-msgstr ""
-
msgid "To GitLab"
msgstr ""
-msgid "To connect GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to connect."
-msgstr ""
-
-msgid "To connect GitHub repositories, you first need to authorize GitLab to access the list of your GitHub repositories:"
-msgstr ""
-
-msgid "To connect an SVN repository, check out %{svn_link}."
+msgid "To add an SSH key you need to %{generate_link_start}generate one%{link_end} or use an %{existing_link_start}existing key%{link_end}."
msgstr ""
msgid "To import GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
@@ -4467,19 +4809,19 @@ msgstr ""
msgid "To import an SVN repository, check out %{svn_link}."
msgstr ""
-msgid "To only use CI/CD features for an external repository, choose <strong>CI/CD for external repo</strong>."
+msgid "To start serving your jobs you can add Runners to your group"
msgstr ""
-msgid "To set up SAML authentication for your group through an identity provider like Azure, Okta, Onelogin, Ping Identity, or your custom SAML 2.0 provider:"
+msgid "To validate your GitLab CI configurations, go to 'CI/CD → Pipelines' inside your project, and click on the 'CI Lint' button."
msgstr ""
-msgid "To validate your GitLab CI configurations, go to 'CI/CD → Pipelines' inside your project, and click on the 'CI Lint' button."
+msgid "Todo"
msgstr ""
-msgid "To view the roadmap, add a planned start or finish date to one of your epics in this group or its subgroups. Only epics in the past 3 months and the next 3 months are shown."
+msgid "Toggle Sidebar"
msgstr ""
-msgid "Todo"
+msgid "Toggle discussion"
msgstr ""
msgid "Toggle sidebar"
@@ -4491,6 +4833,9 @@ msgstr ""
msgid "ToggleButton|Toggle Status: ON"
msgstr ""
+msgid "Too many changes to show."
+msgstr ""
+
msgid "Total Time"
msgstr ""
@@ -4500,22 +4845,19 @@ msgstr ""
msgid "Total: %{total}"
msgstr ""
-msgid "Track activity with Contribution Analytics."
-msgstr ""
-
-msgid "Track groups of issues that share a theme, across projects and milestones"
-msgstr ""
-
msgid "Track time with quick actions"
msgstr ""
msgid "Trigger this manual action"
msgstr ""
-msgid "Turn on Service Desk"
+msgid "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."
msgstr ""
-msgid "Unknown"
+msgid "Try again"
+msgstr ""
+
+msgid "Unable to load the diff. %{button_try_again}"
msgstr ""
msgid "Unlock"
@@ -4527,25 +4869,45 @@ msgstr ""
msgid "Unresolve discussion"
msgstr ""
+msgid "Unstage all changes"
+msgstr ""
+
+msgid "Unstage changes"
+msgstr ""
+
+msgid "Unstaged"
+msgstr ""
+
+msgid "Unstaged %{type}"
+msgstr ""
+
+msgid "Unstaged and staged %{type}"
+msgstr ""
+
msgid "Unstar"
msgstr ""
-msgid "Up to date"
+msgid "Unsubscribe"
msgstr ""
-msgid "Upgrade your plan to activate Advanced Global Search."
+msgid "Unsubscribe at group level"
msgstr ""
-msgid "Upgrade your plan to activate Contribution Analytics."
+msgid "Unsubscribe at project level"
msgstr ""
-msgid "Upgrade your plan to activate Group Webhooks."
+msgid "Unverified"
msgstr ""
-msgid "Upgrade your plan to activate Issue weight."
+msgid "Up to date"
msgstr ""
-msgid "Upgrade your plan to improve Issue boards."
+msgid "Update %{files}"
+msgid_plural "Update %{files} files"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Update your group name, description, avatar, and other general settings."
msgstr ""
msgid "Upload New File"
@@ -4566,7 +4928,7 @@ msgstr ""
msgid "Usage statistics"
msgstr ""
-msgid "Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab"
+msgid "Use group milestones to manage issues from multiple projects in the same milestone."
msgstr ""
msgid "Use the following registration token during setup:"
@@ -4575,10 +4937,13 @@ msgstr ""
msgid "Use your global notification setting"
msgstr ""
-msgid "Used by members to sign in to your group in GitLab"
+msgid "User and IP Rate Limits"
msgstr ""
-msgid "User and IP Rate Limits"
+msgid "Users"
+msgstr ""
+
+msgid "Variables"
msgstr ""
msgid "Variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. You can use variables for passwords, secret keys, or whatever you want."
@@ -4593,10 +4958,7 @@ msgstr ""
msgid "Various settings that affect GitLab performance."
msgstr ""
-msgid "View and edit lines"
-msgstr ""
-
-msgid "View epics list"
+msgid "Verified"
msgstr ""
msgid "View file @ "
@@ -4605,9 +4967,15 @@ msgstr ""
msgid "View group labels"
msgstr ""
+msgid "View jobs"
+msgstr ""
+
msgid "View labels"
msgstr ""
+msgid "View log"
+msgstr ""
+
msgid "View open merge request"
msgstr ""
@@ -4635,9 +5003,6 @@ msgstr ""
msgid "Want to see the data? Please ask an administrator for access."
msgstr ""
-msgid "We could not verify that one of your projects on GCP has billing enabled. Please try again."
-msgstr ""
-
msgid "We don't have enough data to show this stage."
msgstr ""
@@ -4650,13 +5015,10 @@ msgstr ""
msgid "Web terminal"
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."
+msgid "When a runner is locked, it cannot be assigned to other projects"
msgstr ""
-msgid "Weight"
-msgstr ""
-
-msgid "When leaving the URL blank, classification labels can still be specified whitout disabling cross project features or performing external authorization checks."
+msgid "When enabled, users cannot use GitLab until the terms have been accepted."
msgstr ""
msgid "Wiki"
@@ -4683,7 +5045,31 @@ msgstr ""
msgid "WikiEdit|There is already a page with the same title in that path."
msgstr ""
-msgid "WikiEmptyPageError|You are not allowed to create wiki pages"
+msgid "WikiEmptyIssueMessage|Suggest wiki improvement"
+msgstr ""
+
+msgid "WikiEmptyIssueMessage|You must be a project member in order to add wiki pages. If you have suggestions for how to improve the wiki for this project, consider opening an issue in the %{issues_link}."
+msgstr ""
+
+msgid "WikiEmptyIssueMessage|issue tracker"
+msgstr ""
+
+msgid "WikiEmpty|A wiki is where you can store all the details about your project. This can include why you've created it, its principles, how to use it, and so on."
+msgstr ""
+
+msgid "WikiEmpty|Create your first page"
+msgstr ""
+
+msgid "WikiEmpty|Suggest wiki improvement"
+msgstr ""
+
+msgid "WikiEmpty|The wiki lets you write documentation for your project"
+msgstr ""
+
+msgid "WikiEmpty|This project has no wiki pages"
+msgstr ""
+
+msgid "WikiEmpty|You must be a project member in order to add wiki pages."
msgstr ""
msgid "WikiHistoricalPage|This is an old version of this page."
@@ -4719,6 +5105,12 @@ msgstr ""
msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?"
msgstr ""
+msgid "WikiPageConfirmDelete|Delete page"
+msgstr ""
+
+msgid "WikiPageConfirmDelete|Delete page %{pageTitle}?"
+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 ""
@@ -4734,7 +5126,7 @@ msgstr ""
msgid "WikiPage|Page slug"
msgstr ""
-msgid "WikiPage|Write your content or drag files here..."
+msgid "WikiPage|Write your content or drag files here…"
msgstr ""
msgid "Wiki|Create Page"
@@ -4746,9 +5138,6 @@ msgstr ""
msgid "Wiki|Edit Page"
msgstr ""
-msgid "Wiki|Empty page"
-msgstr ""
-
msgid "Wiki|More Pages"
msgstr ""
@@ -4767,13 +5156,10 @@ 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 ""
-msgid "Write a commit message..."
+msgid "Yes"
msgstr ""
msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
@@ -4791,7 +5177,7 @@ msgstr ""
msgid "You are on a read-only GitLab instance."
msgstr ""
-msgid "You are on a secondary (read-only) Geo node. If you want to make any changes, you must visit the %{primary_node}."
+msgid "You can %{linkStart}view the blob%{linkEnd} instead."
msgstr ""
msgid "You can also create a project from the command line."
@@ -4800,6 +5186,9 @@ msgstr ""
msgid "You can also star a label to make it a priority label."
msgstr ""
+msgid "You can also test your .gitlab-ci.yml in the %{linkStart}Lint%{linkEnd}"
+msgstr ""
+
msgid "You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}"
msgstr ""
@@ -4812,28 +5201,31 @@ msgstr ""
msgid "You can only edit 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."
+msgid "You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}"
msgstr ""
msgid "You cannot write to this read-only GitLab instance."
msgstr ""
-msgid "You do not have the correct permissions to override the settings from the LDAP group sync."
+msgid "You do not have any assigned merge requests"
msgstr ""
msgid "You have no permissions"
msgstr ""
+msgid "You have not created any merge requests"
+msgstr ""
+
msgid "You have reached your project limit"
msgstr ""
-msgid "You must have master access to force delete a lock"
+msgid "You must accept our Terms of Service and privacy policy in order to register an account"
msgstr ""
-msgid "You must sign in to star a project"
+msgid "You must have maintainer access to force delete a lock"
msgstr ""
-msgid "You need a different license to enable FileLocks feature"
+msgid "You must sign in to star a project"
msgstr ""
msgid "You need permission."
@@ -4902,13 +5294,11 @@ msgstr ""
msgid "Your projects"
msgstr ""
-msgid "among other things"
+msgid "ago"
msgstr ""
-msgid "and %d fixed vulnerability"
-msgid_plural "and %d fixed vulnerabilities"
-msgstr[0] ""
-msgstr[1] ""
+msgid "among other things"
+msgstr ""
msgid "assign yourself"
msgstr ""
@@ -4916,136 +5306,36 @@ msgstr ""
msgid "branch name"
msgstr ""
-msgid "by"
-msgstr ""
-
-msgid "ciReport|%{type} detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|%{type} detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Code quality"
-msgstr ""
-
-msgid "ciReport|DAST detected no alerts by analyzing the review app"
-msgstr ""
-
-msgid "ciReport|Dependency scanning"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Failed to load %{reportName} report"
-msgstr ""
-
-msgid "ciReport|Fixed:"
-msgstr ""
-
-msgid "ciReport|Instances"
-msgstr ""
-
-msgid "ciReport|Learn more about whitelisting"
-msgstr ""
-
-msgid "ciReport|Loading %{reportName} report"
-msgstr ""
-
-msgid "ciReport|No changes to code quality"
-msgstr ""
-
-msgid "ciReport|No changes to performance metrics"
-msgstr ""
-
-msgid "ciReport|Performance metrics"
-msgstr ""
-
-msgid "ciReport|SAST"
-msgstr ""
-
-msgid "ciReport|SAST detected"
-msgstr ""
-
-msgid "ciReport|SAST detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|SAST detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|SAST:container no vulnerabilities were found"
-msgstr ""
-
-msgid "ciReport|Security scanning"
-msgstr ""
-
-msgid "ciReport|Security scanning failed loading any results"
-msgstr ""
-
-msgid "ciReport|Show complete code vulnerabilities report"
-msgstr ""
-
-msgid "ciReport|Unapproved vulnerabilities (red) can be marked as approved. %{helpLink}"
-msgstr ""
-
-msgid "ciReport|no vulnerabilities"
-msgstr ""
-
msgid "command line instructions"
msgstr ""
msgid "connecting"
msgstr ""
-msgid "could not read private key, is the passphrase correct?"
-msgstr ""
-
msgid "day"
msgid_plural "days"
msgstr[0] ""
msgstr[1] ""
-msgid "detected %d fixed vulnerability"
-msgid_plural "detected %d fixed vulnerabilities"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "detected %d new vulnerability"
-msgid_plural "detected %d new vulnerabilities"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "detected no vulnerabilities"
+msgid "deploy token"
msgstr ""
-msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
+msgid "disabled"
msgstr ""
-msgid "here"
-msgstr ""
-
-msgid "importing"
+msgid "enabled"
msgstr ""
-msgid "in progress"
-msgstr ""
-
-msgid "is invalid because there is downstream lock"
+msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
msgstr ""
-msgid "is invalid because there is upstream lock"
+msgid "for this project"
msgstr ""
-msgid "is not a valid X509 certificate."
+msgid "importing"
msgstr ""
-msgid "locked by %{path_lock_user_name} %{created_at}"
+msgid "latest version"
msgstr ""
msgid "merge request"
@@ -5065,28 +5355,7 @@ msgstr ""
msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage is %{emphasisStart} unchanged %{emphasisEnd} at %{memoryFrom}MB"
msgstr ""
-msgid "mrWidget|Add approval"
-msgstr ""
-
-msgid "mrWidget|Allows edits from maintainers"
-msgstr ""
-
-msgid "mrWidget|An error occured while removing your approval."
-msgstr ""
-
-msgid "mrWidget|An error occured while retrieving approval data for this merge request."
-msgstr ""
-
-msgid "mrWidget|An error occured while submitting your approval."
-msgstr ""
-
-msgid "mrWidget|Approve"
-msgstr ""
-
-msgid "mrWidget|Approved"
-msgstr ""
-
-msgid "mrWidget|Approved by"
+msgid "mrWidget|Allows commits from members who can merge to the target branch"
msgstr ""
msgid "mrWidget|Cancel automatic merge"
@@ -5113,6 +5382,9 @@ msgstr ""
msgid "mrWidget|Closes"
msgstr ""
+msgid "mrWidget|Create an issue to resolve them later"
+msgstr ""
+
msgid "mrWidget|Deployment statistics are not available currently"
msgstr ""
@@ -5167,9 +5439,6 @@ msgstr ""
msgid "mrWidget|Remove source branch"
msgstr ""
-msgid "mrWidget|Remove your approval"
-msgstr ""
-
msgid "mrWidget|Request to merge"
msgstr ""
@@ -5209,6 +5478,9 @@ msgstr ""
msgid "mrWidget|There are merge conflicts"
msgstr ""
+msgid "mrWidget|There are unresolved discussions. Please resolve these discussions"
+msgstr ""
+
msgid "mrWidget|This merge request failed to be merged automatically"
msgstr ""
@@ -5259,7 +5531,7 @@ msgstr ""
msgid "personal access token"
msgstr ""
-msgid "private key does not match certificate."
+msgid "remaining"
msgstr ""
msgid "remove due date"
@@ -5274,9 +5546,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "to help your contributors communicate effectively!"
-msgstr ""
-
msgid "username"
msgstr ""
@@ -5286,3 +5555,8 @@ msgstr ""
msgid "with %{additions} additions, %{deletions} deletions."
msgstr ""
+msgid "within %d minute "
+msgid_plural "within %d minutes "
+msgstr[0] ""
+msgstr[1] ""
+
diff --git a/locale/uk/gitlab.po b/locale/uk/gitlab.po
index 2c7d3751531..087d5a7a764 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: 2018-04-04 19:35+0200\n"
-"PO-Revision-Date: 2018-04-05 03:35-0400\n"
+"POT-Creation-Date: 2018-07-01 16:35+1000\n"
+"PO-Revision-Date: 2018-07-01 18:01\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Ukrainian\n"
"Language: uk_UA\n"
@@ -16,8 +16,12 @@ msgstr ""
"X-Crowdin-Language: uk\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-msgid " and"
-msgstr " Ñ–"
+msgid "%d changed file"
+msgid_plural "%d changed files"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
msgid "%d commit"
msgid_plural "%d commits"
@@ -68,6 +72,20 @@ msgstr[1] "%d метрики"
msgstr[2] "%d метрик"
msgstr[3] "%d метрик"
+msgid "%d staged change"
+msgid_plural "%d staged changes"
+msgstr[0] "%d проіндекÑована зміна"
+msgstr[1] "%d проіндекÑовані зміни"
+msgstr[2] "%d проіндекÑованих змін"
+msgstr[3] "%d проіндекÑованих змін"
+
+msgid "%d unstaged change"
+msgid_plural "%d unstaged changes"
+msgstr[0] "%d неіндекÑована зміна"
+msgstr[1] "%d неіндекÑовані зміни"
+msgstr[2] "%d неіндекÑованих змін"
+msgstr[3] "%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 доданий коміт був виключений Ð´Ð»Ñ Ð·Ð°Ð¿Ð¾Ð±Ñ–Ð³Ð°Ð½Ð½Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼ із продуктивніÑтю."
@@ -88,12 +106,21 @@ msgstr[1] "%{count} учаÑтника"
msgstr[2] "%{count} учаÑтників"
msgstr[3] "%{count} учаÑтників"
+msgid "%{filePath} deleted"
+msgstr ""
+
+msgid "%{group_docs_link_start}Groups%{group_docs_link_end} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects."
+msgstr ""
+
msgid "%{loadingIcon} Started"
msgstr "%{loadingIcon} Початок"
msgid "%{lock_path} is locked by GitLab User %{lock_user_id}"
msgstr "%{lock_path} заблоковано кориÑтувачем GitLab %{lock_user_id}"
+msgid "%{nip_domain} can be used as an alternative to a custom domain."
+msgstr "%{nip_domain} може бути викориÑтана Ñк альтернатива влаÑному домену."
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr "на %{number_commits_behind} комітів позаду %{default_branch}, на %{number_commits_ahead} комітів попереду"
@@ -106,6 +133,9 @@ msgstr "%{number_of_failures} від %{maximum_failures} невдач. GitLab а
msgid "%{openOrClose} %{noteable}"
msgstr "%{openOrClose} %{noteable}"
+msgid "%{percent}%% complete"
+msgstr "%{percent}%% завершено"
+
msgid "%{storage_name}: failed storage access attempt on host:"
msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:"
msgstr[0] "%{storage_name}: Ñпроба невдалого доÑтупу до Ñховища на хоÑті:"
@@ -116,15 +146,76 @@ msgstr[3] "%{storage_name}: %{failed_attempts} невдалих Ñпроб доÑ
msgid "%{text} is available"
msgstr "%{text} доÑтупний"
+msgid "%{title} changes"
+msgstr "%{title} зміни"
+
+msgid "%{unstaged} unstaged and %{staged} staged changes"
+msgstr "%{unstaged} неіндекÑованих та %{staged} проіндекÑованих змін"
+
msgid "(checkout the %{link} for information on how to install it)."
msgstr "(перейдіть за поÑиланнÑм %{link} Ð´Ð»Ñ Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ð½Ð½Ñ Ñ–Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ñ–Ñ— ÑтоÑовно вÑтановленнÑ)."
msgid "+ %{moreCount} more"
msgstr "+ ще %{moreCount}"
+msgid "- Runner is active and can process any new jobs"
+msgstr "- Runner активний Ñ– може виконувати нові завданнÑ"
+
+msgid "- Runner is paused and will not receive any new jobs"
+msgstr "- Runner призупинено Ñ– він не зможе виконувати нові завданнÑ"
+
msgid "- show less"
msgstr "- показати менше"
+msgid "1 %{type} addition"
+msgid_plural "%{count} %{type} additions"
+msgstr[0] "1 %{type} доповненнÑ"
+msgstr[1] "%{count} %{type} доповненнÑ"
+msgstr[2] "%{count} %{type} доповнень"
+msgstr[3] "%{count} %{type} доповнень"
+
+msgid "1 %{type} modification"
+msgid_plural "%{count} %{type} modifications"
+msgstr[0] "1 %{type} зміна"
+msgstr[1] "%{count} %{type} зміни"
+msgstr[2] "%{count} %{type} змін"
+msgstr[3] "%{count} %{type} змін"
+
+msgid "1 closed issue"
+msgid_plural "%d closed issues"
+msgstr[0] "1 закрита проблема"
+msgstr[1] "%d закриті проблеми"
+msgstr[2] "%d закритих проблем"
+msgstr[3] "%d закритих проблем"
+
+msgid "1 closed merge request"
+msgid_plural "%d closed merge requests"
+msgstr[0] "1 закритий запит на злиттÑ"
+msgstr[1] "%d закритих запити на злиттÑ"
+msgstr[2] "%d закритих запитів на злиттÑ"
+msgstr[3] "%d закритих запитів на злиттÑ"
+
+msgid "1 merged merge request"
+msgid_plural "%d merged merge requests"
+msgstr[0] "1 заÑтоÑований запит на злиттÑ"
+msgstr[1] "%d заÑтоÑованих запити на злиттÑ"
+msgstr[2] "%d заÑтоÑованих запитів на злиттÑ"
+msgstr[3] "%d заÑтоÑованих запитів на злиттÑ"
+
+msgid "1 open issue"
+msgid_plural "%d open issues"
+msgstr[0] "1 відкрита проблема"
+msgstr[1] "%d відкриті проблеми"
+msgstr[2] "%d відкритих проблем"
+msgstr[3] "%d відкритих проблем"
+
+msgid "1 open merge request"
+msgid_plural "%d open merge requests"
+msgstr[0] "1 відкритий запит на злиттÑ"
+msgstr[1] "%d відкритих запити на злиттÑ"
+msgstr[2] "%d відкритих запитів на злиттÑ"
+msgstr[3] "%d відкритих запитів на злиттÑ"
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] "1 конвеєр"
@@ -138,9 +229,27 @@ msgstr "Перший внеÑок!"
msgid "2FA enabled"
msgstr "Двоетапна Ð°ÑƒÑ‚ÐµÐ½Ñ‚Ð¸Ñ„Ñ–ÐºÐ°Ñ†Ñ–Ñ ÑƒÐ²Ñ–Ð¼ÐºÐ½ÐµÐ½Ð°"
+msgid "403|Please contact your GitLab administrator to get the permission."
+msgstr "Будь лаÑка, звернітьÑÑ Ð´Ð¾ адмініÑтратора GitLab, щоб отримати дозвіл."
+
+msgid "403|You don't have the permission to access this page."
+msgstr "У Ð²Ð°Ñ Ð½ÐµÐ¼Ð°Ñ” доÑтупу до цієї Ñторінки."
+
+msgid "404|Make sure the address is correct and the page hasn't moved."
+msgstr "Перевірте, що адреÑа правильна Ñ– те, що Ñторінка не переміщувалаÑÑ."
+
+msgid "404|Page Not Found"
+msgstr "Сторінка не знайдена"
+
+msgid "404|Please contact your GitLab administrator if you think this is a mistake."
+msgstr "Будь лаÑка, звернітьÑÑ Ð´Ð¾ адмініÑтратора GitLab, Ñкщо ви вважаєте, що це помилка."
+
msgid "<strong>Removes</strong> source branch"
msgstr "<strong>ВидалÑÑ”</strong> гілку-джерело"
+msgid "A 'Runner' is a process which runs a job. You can setup as many Runners as you need."
+msgstr "'Runner' — це процеÑ, Ñкий виконує завданнÑ. Ви можете Ñтворити потрібну кількіÑть Runner'ів."
+
msgid "A collection of graphs regarding Continuous Integration"
msgstr "Ðабір графіків відноÑно безперервної інтеграції"
@@ -150,6 +259,9 @@ msgstr "У вашому форку буде Ñтворено нову гілку
msgid "A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}."
msgstr "Проект — це міÑце де ви можете розміщувати Ñвої файли (репозиторій), планувати роботу (проблеми) Ñ– публікувати документацію (wiki), %{among_other_things_link}."
+msgid "A regular expression that will be used to find the test coverage output in the job trace. Leave blank to disable"
+msgstr ""
+
msgid "A user with write access to the source branch selected this option"
msgstr "КориÑтувач із правом запиÑу в гілку-джерело вибрав цей варіант"
@@ -160,7 +272,10 @@ msgid "Abuse Reports"
msgstr "Звіти про зловживаннÑ"
msgid "Abuse reports"
-msgstr ""
+msgstr "Звіти про зловживаннÑ"
+
+msgid "Accept terms"
+msgstr "ПрийнÑти умови"
msgid "Access Tokens"
msgstr "Токени доÑтупу"
@@ -168,30 +283,30 @@ msgstr "Токени доÑтупу"
msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again."
msgstr "ДоÑтуп до Ñховищ, що вийшли з ладу, тимчаÑово прибраний Ð·Ð°Ð´Ð»Ñ Ð²Ñ–Ð´Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¼Ð¾Ð½Ñ‚ÑƒÐ²Ð°Ð½Ð½Ñ. ПіÑÐ»Ñ Ð²Ð¸Ñ€Ñ–ÑˆÐµÐ½Ð½Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼Ð¸ обнуліть інформацію Ñховища Ð´Ð»Ñ Ð²Ñ–Ð´Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð´Ð¾Ñтупу."
+msgid "Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report."
+msgstr ""
+
msgid "Account"
msgstr "Обліковий запиÑ"
-msgid "Account and limit settings"
-msgstr ""
+msgid "Account and limit"
+msgstr "Обліковий Ð·Ð°Ð¿Ð¸Ñ Ñ‚Ð° ліміт"
msgid "Active"
msgstr "Ðктивний"
+msgid "Active Sessions"
+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 "Додайте групові веб-гуки та GitLab Enterprise Edition."
-
msgid "Add Kubernetes cluster"
msgstr "Додати Kubernetes-клаÑтер"
@@ -204,6 +319,9 @@ msgstr "Додати інÑтрукцію"
msgid "Add new directory"
msgstr "Додати новий каталог"
+msgid "Add reaction"
+msgstr "Додати реакцію"
+
msgid "Add todo"
msgstr "Додати задачу"
@@ -273,29 +391,44 @@ msgstr "Ð’ÑÑ– зміни закомічені"
msgid "All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings."
msgstr "Ð’ÑÑ– функції Ð´Ð»Ñ Ð½Ð¾Ð²Ð¸Ñ… проектів берутьÑÑ Ñ–Ð· шаблонів або під Ñ‡Ð°Ñ Ñ–Ð¼Ð¿Ð¾Ñ€Ñ‚ÑƒÐ²Ð°Ð½Ð½Ñ, але ви можете вимикати Ñ—Ñ… пізніше в налаштуваннÑÑ… проекту."
-msgid "Allow edits from maintainers."
-msgstr "Дозволити Ñ€ÐµÐ´Ð°Ð³ÑƒÐ²Ð°Ð½Ð½Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´Ð¾ÑŽ проекту."
+msgid "Allow commits from members who can merge to the target branch."
+msgstr "Дозволити коміти від учаÑників, Ñкі можуть зливати в цільову гілку."
-msgid "Allow rendering of PlantUML diagrams in Asciidoc documents."
+msgid "Allow public access to pipelines and job details, including output logs and artifacts"
msgstr ""
+msgid "Allow rendering of PlantUML diagrams in Asciidoc documents."
+msgstr "Дозволити Ð²Ñ–Ð´Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð½Ñ Ð´Ñ–Ð°Ð³Ñ€Ð°Ð¼ PlantUML в документах Asciidoc."
+
msgid "Allow requests to the local network from hooks and services."
-msgstr ""
+msgstr "Дозволити запити до локальної мережі із гуків та ÑервіÑів."
msgid "Allows you to add and manage Kubernetes clusters."
msgstr "ДозволÑÑ” додавати та керувати клаÑтерами Kubernetes."
-msgid "Also called \"Issuer\" or \"Relying party trust identifier\""
+msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
+msgstr "Крім того, ви можете викориÑтовувати %{personal_access_token_link}. Коли ви Ñтворюватимете Ñвій перÑональний токен доÑтупу, вам потрібно буде вибрати облаÑть <code>repo</code>, щоб ми могли відобразити ÑпиÑок ваших публічних та приватних репозиторіїв, доÑтупних Ð´Ð»Ñ Ñ–Ð¼Ð¿Ð¾Ñ€Ñ‚Ñƒ."
+
+msgid "An error occured creating the new branch."
msgstr ""
-msgid "Also called \"Relying party service URL\" or \"Reply URL\""
+msgid "An error occured whilst loading all the files."
msgstr ""
-msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to connect."
+msgid "An error occured whilst loading the file content."
msgstr ""
-msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
-msgstr "Крім того, ви можете викориÑтовувати %{personal_access_token_link}. Коли ви Ñтворюватимете Ñвій перÑональний токен доÑтупу, вам потрібно буде вибрати облаÑть <code>репозиторіÑ</code>, щоб ми могли відобразити ÑпиÑок ваших публічних та приватних репозиторіїв, доÑтупних Ð´Ð»Ñ Ñ–Ð¼Ð¿Ð¾Ñ€Ñ‚Ñƒ."
+msgid "An error occured whilst loading the file."
+msgstr ""
+
+msgid "An error occured whilst loading the merge request changes."
+msgstr ""
+
+msgid "An error occured whilst loading the merge request version data."
+msgstr ""
+
+msgid "An error occured whilst loading the merge request."
+msgstr ""
msgid "An error occurred previewing the blob"
msgstr "СталаÑÑ Ð¿Ð¾Ð¼Ð¸Ð»ÐºÐ° під Ñ‡Ð°Ñ Ð¿Ð¾Ð¿ÐµÑ€ÐµÐ´Ð½ÑŒÐ¾Ð³Ð¾ переглÑду об'єкта"
@@ -303,17 +436,11 @@ msgstr "СталаÑÑ Ð¿Ð¾Ð¼Ð¸Ð»ÐºÐ° під Ñ‡Ð°Ñ Ð¿Ð¾Ð¿ÐµÑ€ÐµÐ´Ð½ÑŒÐ¾Ð³Ð¾ пÐ
msgid "An error occurred when toggling the notification subscription"
msgstr "Виникла помилка під Ñ‡Ð°Ñ Ð·Ð¼Ñ–Ð½Ð¸ підпиÑки на ÑповіщеннÑ"
-msgid "An error occurred when updating the issue weight"
-msgstr "Збій під Ñ‡Ð°Ñ Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð²Ð°Ð³Ð¸ проблеми"
-
-msgid "An error occurred while adding approver"
-msgstr "Помилка при додаванні учаÑника Ð´Ð»Ñ Ð·Ð°Ñ‚Ð²ÐµÑ€Ð´Ð¶ÐµÐ½Ð½Ñ"
-
-msgid "An error occurred while detecting host keys"
-msgstr "СталаÑÑ Ð¿Ð¾Ð¼Ð¸Ð»ÐºÐ° при виÑвленні ключів хоÑта"
+msgid "An error occurred while dismissing the alert. Refresh the page and try again."
+msgstr "Помилка при відхиленні попередженнÑ. Оновіть Ñторінку та Ñпробуйте знову."
msgid "An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again."
-msgstr "СталаÑÑ Ð¿Ð¾Ð¼Ð¸Ð»ÐºÐ° при вимкненні Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ð¿Ñ€Ð¾ функцію. Оновіть Ñторінку Ñ– Ñпробуйте знову."
+msgstr "СталаÑÑ Ð¿Ð¾Ð¼Ð¸Ð»ÐºÐ° при відхиленні Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ð¿Ñ€Ð¾ функцію. Оновіть Ñторінку Ñ– Ñпробуйте знову."
msgid "An error occurred while fetching markdown preview"
msgstr "СталаÑÑ Ð¿Ð¾Ð¼Ð¸Ð»ÐºÐ° при попередньому переглÑді markdown"
@@ -327,11 +454,8 @@ msgstr "Помилка при отриманні данних конвеєра."
msgid "An error occurred while getting projects"
msgstr "СталаÑÑ Ð¿Ð¾Ð¼Ð¸Ð»ÐºÐ° при отриманні проектів"
-msgid "An error occurred while importing project"
-msgstr "Помилка при імпорті проекту"
-
-msgid "An error occurred while initializing path locks"
-msgstr "Помилка при ініціалізації Ð±Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ñ„Ð°Ð¹Ð»Ð¾Ð²Ð¸Ñ… шлÑхів"
+msgid "An error occurred while importing project: ${details}"
+msgstr "При імпортуванні проекту ÑталаÑÑ Ð¿Ð¾Ð¼Ð¸Ð»ÐºÐ°: ${details}"
msgid "An error occurred while loading commits"
msgstr "Помилка при завантажені комітів"
@@ -348,9 +472,6 @@ msgstr "Помилка при завантаженні файлу"
msgid "An error occurred while making the request."
msgstr "Помилка при Ñтворенні запиту."
-msgid "An error occurred while removing approver"
-msgstr "Помилка при видаленні учаÑника Ð´Ð»Ñ Ð·Ð°Ñ‚Ð²ÐµÑ€Ð´Ð¶ÐµÐ½Ð½Ñ"
-
msgid "An error occurred while rendering KaTeX"
msgstr "СталаÑÑ Ð¿Ð¾Ð¼Ð¸Ð»ÐºÐ° при рендерингу KaTeX"
@@ -363,9 +484,6 @@ msgstr "СталаÑÑ Ð¿Ð¾Ð¼Ð¸Ð»ÐºÐ° при отриманні календар
msgid "An error occurred while retrieving diff"
msgstr "СталаÑÑ Ð¿Ð¾Ð¼Ð¸Ð»ÐºÐ° при отриманні різниці"
-msgid "An error occurred while saving LDAP override status. Please try again."
-msgstr "Помилка при збереженні ÑтатуÑу Ð¿ÐµÑ€ÐµÐ²Ð¸Ð·Ð½Ð°Ñ‡ÐµÐ½Ð½Ñ LDAP. Будь лаÑка, Ñпробуйте ще раз."
-
msgid "An error occurred while saving assignees"
msgstr "Помилка при збереженні виконавців"
@@ -375,9 +493,6 @@ msgstr "СталаÑÑ Ð¿Ð¾Ð¼Ð¸Ð»ÐºÐ° під Ñ‡Ð°Ñ Ð¿ÐµÑ€ÐµÐ²Ñ–Ñ€ÐºÐ¸ іменÑ
msgid "An error occurred. Please try again."
msgstr "СталаÑÑŒ помилка. Спробуйте ще раз."
-msgid "Any Label"
-msgstr "Будь-Ñка мітка"
-
msgid "Appearance"
msgstr "Зовнішній виглÑд"
@@ -390,29 +505,29 @@ msgstr "квіт."
msgid "April"
msgstr "квітень"
-msgid "Archived project! Repository is read-only"
-msgstr "Заархівований проект! Репозиторій доÑтупний лише Ð´Ð»Ñ Ñ‡Ð¸Ñ‚Ð°Ð½Ð½Ñ"
+msgid "Archived project! Repository and other project resources are read-only"
+msgstr "Ðрхівований проект! Репозиторій та інші реÑурÑи проекту доÑтупні лише Ð´Ð»Ñ Ñ‡Ð¸Ñ‚Ð°Ð½Ð½Ñ"
msgid "Are you sure you want to delete this pipeline schedule?"
msgstr "Ви впевнені, що хочете видалити цей розклад Ð´Ð»Ñ ÐºÐ¾Ð½Ð²ÐµÑ”Ñ€Ð°?"
+msgid "Are you sure you want to remove this identity?"
+msgstr ""
+
msgid "Are you sure you want to reset registration token?"
msgstr "Ви впевнені, що бажаєте перегенерувати реєÑтраційний токен?"
msgid "Are you sure you want to reset the health check token?"
msgstr "Ви впевнені, що Ви хочете перегенерувати цей ключ перевірки працездатноÑті?"
-msgid "Are you sure you want to unlock %{path_lock_path}?"
-msgstr "Ви впевнені, що хочете розблокувати %{path_lock_path}?"
-
msgid "Are you sure?"
msgstr "Ви впевнені?"
msgid "Artifacts"
msgstr "Ðртефакти"
-msgid "Assertion consumer service URL"
-msgstr ""
+msgid "Ask your group maintainer to setup a group Runner."
+msgstr "ПопроÑіть керівника групи, щоб налаштувати груповий Runner."
msgid "Assign custom color like #FF0000"
msgstr "Призначити влаÑний колір типу #FF0000"
@@ -427,17 +542,23 @@ msgid "Assign to"
msgstr "Призначити"
msgid "Assigned Issues"
-msgstr ""
+msgstr "Призначені проблеми"
msgid "Assigned Merge Requests"
-msgstr ""
+msgstr "Призначені запити на злиттÑ"
msgid "Assigned to :name"
-msgstr ""
+msgstr "Призначено :ім'Ñ"
+
+msgid "Assigned to me"
+msgstr "Призначено мені"
msgid "Assignee"
msgstr "Виконавець"
+msgid "Assignee(s)"
+msgstr "Виконавець(ці)"
+
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "Прикріпити файл за допомогою перетÑÐ³ÑƒÐ²Ð°Ð½Ð½Ñ Ð°Ð±Ð¾ %{upload_link}"
@@ -460,7 +581,7 @@ msgid "Auto DevOps enabled"
msgstr "Auto DevOps увімкнено"
msgid "Auto DevOps, runners and job artifacts"
-msgstr ""
+msgstr "Auto DevOps, runner'и і артефакти завдань"
msgid "Auto Review Apps and Auto Deploy need a %{kubernetes} to work correctly."
msgstr "Ð”Ð»Ñ ÐºÐ¾Ñ€ÐµÐºÑ‚Ð½Ð¾Ñ— роботи Auto Review Apps та Auto Deploy необхіден %{kubernetes}."
@@ -471,8 +592,11 @@ msgstr "Ð”Ð»Ñ ÐºÐ¾Ñ€ÐµÐºÑ‚Ð½Ð¾Ñ— роботи Auto Review Apps та Auto Deploy
msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
msgstr "Ð”Ð»Ñ ÐºÐ¾Ñ€ÐµÐºÑ‚Ð½Ð¾Ñ— роботи Auto Review Apps та Auto Deploy необхідно вказати доменне ім’Ñ."
-msgid "AutoDevOps|Auto DevOps (Beta)"
-msgstr "Auto DevOps (бета)"
+msgid "Auto-cancel redundant, pending pipelines"
+msgstr ""
+
+msgid "AutoDevOps|Auto DevOps"
+msgstr "Auto DevOps"
msgid "AutoDevOps|Auto DevOps documentation"
msgstr "Auto DevOps документації"
@@ -492,80 +616,110 @@ msgstr "Ви можете автоматично збирати й теÑтувÐ
msgid "AutoDevOps|add a Kubernetes cluster"
msgstr "додати Kubernetes-клаÑтер"
-msgid "AutoDevOps|enable Auto DevOps (Beta)"
-msgstr "Увімкнути Auto DevOps (Beta)"
+msgid "AutoDevOps|enable Auto DevOps"
+msgstr "Увімкнути Auto DevOps"
msgid "Available"
msgstr "ДоÑтупно"
+msgid "Available group Runners : %{runners}"
+msgstr "ДоÑтупні групові Runner'и: %{runners}"
+
+msgid "Available group Runners : %{runners}."
+msgstr "ДоÑтупні групові Runner'и: %{runners}."
+
msgid "Avatar will be removed. Are you sure?"
msgstr "Ðватар буде видалено. Ви впевнені?"
msgid "Average per day: %{average}"
msgstr "Ð’ Ñередньому за день: %{average}"
-msgid "Background Color"
-msgstr ""
+msgid "Background color"
+msgstr "Колір фону"
msgid "Background jobs"
-msgstr ""
+msgstr "Фонові завданнÑ"
-msgid "Begin with the selected commit"
-msgstr "Почати із виділеного коміту"
+msgid "Badges"
+msgstr "Значки"
+
+msgid "Badges|A new badge was added."
+msgstr "Ðовий значок був доданий."
+
+msgid "Badges|Add badge"
+msgstr "Додати значок"
+
+msgid "Badges|Adding the badge failed, please check the entered URLs and try again."
+msgstr "Ð”Ð¾Ð´Ð°Ð²Ð°Ð½Ð½Ñ Ð·Ð½Ð°Ñ‡ÐºÐ° не вдалоÑÑ, будь лаÑка перевірте введений URL Ñ– Ñпробуйте знову."
+
+msgid "Badges|Badge image URL"
+msgstr "URL-адреÑа Ð·Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð½Ñ Ð·Ð½Ð°Ñ‡ÐºÐ°"
+
+msgid "Badges|Badge image preview"
+msgstr "Попередній переглÑд значка"
-msgid "Billing"
-msgstr "Білінг"
+msgid "Badges|Delete badge"
+msgstr "Видалити значок"
-msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan."
-msgstr "%{group_name} зараз має план %{plan_link}."
+msgid "Badges|Delete badge?"
+msgstr "Видалити значок?"
-msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available."
-msgstr "Ðвтоматичні Ð¿Ð¾Ð½Ð¸Ð¶ÐµÐ½Ð½Ñ Ñ‚Ð° Ð¿Ñ–Ð´Ð²Ð¸Ñ‰ÐµÐ½Ð½Ñ Ð´Ð¾ деÑких планів зараз не доÑтупні."
+msgid "Badges|Deleting the badge failed, please try again."
+msgstr "Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð·Ð½Ð°Ñ‡ÐºÐ° не вдалоÑÑ, будь лаÑка Ñпробуйте ще раз."
-msgid "BillingPlans|Current plan"
-msgstr "Поточний план"
+msgid "Badges|Group Badge"
+msgstr "Значок групи"
-msgid "BillingPlans|Customer Support"
-msgstr "Служба підтримки"
+msgid "Badges|Link"
+msgstr "ПоÑиланнÑ"
-msgid "BillingPlans|Downgrade"
-msgstr "Понизити"
+msgid "Badges|No badge image"
+msgstr "Значок Ð·Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð½Ñ Ð²Ñ–Ð´Ñутній"
-msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
-msgstr "ДізнайтеÑÑ Ð±Ñ–Ð»ÑŒÑˆÐµ про кожен план, читаючи наш %{faq_link}."
+msgid "Badges|No image to preview"
+msgstr "Ðемає Ð·Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð½Ñ Ð´Ð»Ñ Ð¿Ð¾Ð¿ÐµÑ€ÐµÐ´Ð½ÑŒÐ¾Ð³Ð¾ переглÑду"
-msgid "BillingPlans|Manage plan"
-msgstr "Ð£Ð¿Ñ€Ð°Ð²Ð»Ñ–Ð½Ð½Ñ Ð¿Ð»Ð°Ð½Ð¾Ð¼"
+msgid "Badges|Project Badge"
+msgstr "Значок проекту"
-msgid "BillingPlans|Please contact %{customer_support_link} in that case."
-msgstr "Будь лаÑка, в цьому випадку зв'ÑжітьÑÑ Ð· %{customer_support_link}."
+msgid "Badges|Reload badge image"
+msgstr "Оновити Ð·Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð½Ñ Ð·Ð½Ð°Ñ‡ÐºÐ°"
-msgid "BillingPlans|See all %{plan_name} features"
-msgstr "ПодивітьÑÑ Ð²ÑÑ– можливоÑті %{plan_name}"
+msgid "Badges|Save changes"
+msgstr "Зберегти зміни"
+
+msgid "Badges|Saving the badge failed, please check the entered URLs and try again."
+msgstr "Ð—Ð±ÐµÑ€ÐµÐ¶ÐµÐ½Ð½Ñ Ð·Ð½Ð°Ñ‡ÐºÐ° не вдалоÑÑ, будь лаÑка перевірте введений URL Ñ– Ñпробуйте знову."
-msgid "BillingPlans|This group uses the plan associated with its parent group."
-msgstr "Ð¦Ñ Ð³Ñ€ÑƒÐ¿Ð° викориÑтовує план, пов'Ñзаний із батьківÑькою групою."
+msgid "Badges|The %{docsLinkStart}variables%{docsLinkEnd} GitLab supports: %{placeholders}"
+msgstr "%{docsLinkStart}Змінні%{docsLinkEnd}, Ñкі підтримує GitLab: %{placeholders}"
-msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
-msgstr "Щоб керувати планом цієї групи, відвідайте розділ білінгу на %{parent_billing_page_link}."
+msgid "Badges|The badge was deleted."
+msgstr "Значок був видалений."
-msgid "BillingPlans|Upgrade"
-msgstr "Підвищити"
+msgid "Badges|The badge was saved."
+msgstr "Значок було збережено."
-msgid "BillingPlans|You are currently on the %{plan_link} plan."
-msgstr "Зараз ви викориÑтовуєте план %{plan_link}."
+msgid "Badges|This group has no badges"
+msgstr "Ð¦Ñ Ð³Ñ€ÑƒÐ¿Ð° не має значків"
-msgid "BillingPlans|frequently asked questions"
-msgstr "ЧаÑті питаннÑ"
+msgid "Badges|This project has no badges"
+msgstr "У цього проекту немає значків"
-msgid "BillingPlans|monthly"
-msgstr "щоміÑÑцÑ"
+msgid "Badges|Your badges"
+msgstr "Ваші значки"
-msgid "BillingPlans|paid annually at %{price_per_year}"
-msgstr "ОплачуєтьÑÑ Ñ‰Ð¾Ñ€Ñ–Ñ‡Ð½Ð¾ %{price_per_year}"
+msgid "Begin with the selected commit"
+msgstr "Почати із виділеного коміту"
-msgid "BillingPlans|per user"
-msgstr "за кориÑтувача"
+msgid "Below are examples of regex for existing tools:"
+msgstr ""
+
+msgid "Boards"
+msgstr "Дошки"
+
+msgid "Branch %{branchName} was not found in this project's repository."
+msgstr ""
msgid "Branch (%{branch_count})"
msgid_plural "Branches (%{branch_count})"
@@ -646,7 +800,7 @@ msgstr "Ðемає гілок Ð´Ð»Ñ Ð²Ñ–Ð´Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð½Ñ"
msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
msgstr "Як тільки ви підтвердите Ñ– натиÑнете %{delete_protected_branch}, дані будуть втрачені, Ñ– Ñ—Ñ… не можливо буде відновити."
-msgid "Branches|Only a project master or owner can delete a protected branch"
+msgid "Branches|Only a project maintainer or owner can delete a protected branch"
msgstr "Тільки керівник або влаÑник проекту може видалити захищену гілку"
msgid "Branches|Overview"
@@ -682,9 +836,6 @@ msgstr "ЗаÑтарілі"
msgid "Branches|Stale branches"
msgstr "ЗаÑтарілі гілки"
-msgid "Branches|The branch could not be updated automatically because it has diverged from its upstream counterpart."
-msgstr "Гілка не може бути оновлена автоматично, тому що вона має розбіжноÑті із upstream."
-
msgid "Branches|The default branch cannot be deleted"
msgstr "Гілка \"за замовчуваннÑм\" не може бути видалена"
@@ -697,15 +848,9 @@ msgstr "Щоб уникнути втрати даних, розглÑньте м
msgid "Branches|To confirm, type %{branch_name_confirmation}:"
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 "Щоб відхилити локальні зміни Ñ– перезапиÑати гілку верÑією з upstream, видаліть Ñ—Ñ— тут Ñ– виберіть \"Оновити зараз\" вище."
-
msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
msgstr "Ви збираєтеÑÑ Ð²Ð¸Ð´Ð°Ð»Ð¸Ñ‚Ð¸ захищену гілку %{branch_name}."
-msgid "Branches|diverged from upstream"
-msgstr "розходитьÑÑ Ð· upstream"
-
msgid "Branches|merged"
msgstr "злита"
@@ -727,44 +872,83 @@ msgstr "ПереглÑд файлів"
msgid "Browse files"
msgstr "ПереглÑд файлів"
-msgid "Business"
-msgstr "БізнеÑ"
-
msgid "ByAuthor|by"
msgstr "від"
msgid "CI / CD"
msgstr "CI / CD"
-msgid "CI/CD"
-msgstr "CI/CD"
+msgid "CI / CD Settings"
+msgstr ""
msgid "CI/CD configuration"
msgstr "ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ CI/CD"
-msgid "CI/CD for external repo"
-msgstr "CI/CD Ð´Ð»Ñ Ð·Ð¾Ð²Ð½Ñ–ÑˆÐ½ÑŒÐ¾Ð³Ð¾ репозиторіÑ"
+msgid "CI/CD settings"
+msgstr "ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ CI/CD"
+
+msgid "CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery."
+msgstr "Ðеобхідно вказати %{ci_file} перед тим, Ñк ви зможете викориÑтовувати безперервну інтреграцію та розгортаннÑ."
+
+msgid "CICD|Auto DevOps"
+msgstr "Auto DevOps"
+
+msgid "CICD|Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration."
+msgstr "Auto DevOps буду автоматично збирати, теÑтувати та розгортати ваш заÑтоÑунок на оÑнові Ñтандартної конфігурації неперервної інтеграції та розгортаннÑ."
+
+msgid "CICD|Automatic deployment to staging, manual deployment to production"
+msgstr "Ðвтоматичне Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ð½Ð° staging, ручне Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ð½Ð° production"
+
+msgid "CICD|Continuous deployment to production"
+msgstr "Безперервне Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ð½Ð° production"
+
+msgid "CICD|Deployment strategy"
+msgstr "Ð¡Ñ‚Ñ€Ð°Ñ‚ÐµÐ³Ñ–Ñ Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ"
+
+msgid "CICD|Deployment strategy needs a domain name to work correctly."
+msgstr "Ðеобхідно вказати доменне Ñ–Ð¼â€™Ñ Ð´Ð»Ñ ÐºÐ¾Ñ€ÐµÐºÑ‚Ð½Ð¾Ñ— роботи Ñтратегії розгортаннÑ."
+
+msgid "CICD|Disable Auto DevOps"
+msgstr "Вимкнути Auto DevOps"
+
+msgid "CICD|Enable Auto DevOps"
+msgstr "Увімкнути Auto DevOps"
+
+msgid "CICD|Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific %{ci_file}."
+msgstr "ВикориÑтовувати Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ñ–Ð½ÑтанÑу за замовчаннÑм Ð´Ð»Ñ ÑƒÐ²Ñ–Ð¼ÐºÐµÐ½Ð½Ñ Ð°Ð±Ð¾ Ð²Ð¸Ð¼ÐºÐ½ÐµÐ½Ð½Ñ Auto DevOps, Ñкщо у проекті відÑутній %{ci_file}."
+
+msgid "CICD|Instance default (%{state})"
+msgstr "ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ñ–Ð½ÑтанÑу за замовчуваннÑм (%{state})"
msgid "CICD|Jobs"
msgstr "ЗавданнÑ"
+msgid "CICD|Learn more about Auto DevOps"
+msgstr "ДізнайтеÑÑ Ð±Ñ–Ð»ÑŒÑˆÐµ про Auto DevOps"
+
+msgid "CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project."
+msgstr "ÐšÐ¾Ð½Ñ„Ñ–Ð³ÑƒÑ€Ð°Ñ†Ñ–Ñ ÐºÐ¾Ð½Ð²ÐµÑ”Ñ€Ñƒ Auto DevOps буде заÑтоÑовуватиÑÑ, коли в проекті немає %{ci_file}."
+
+msgid "CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages."
+msgstr "Вам потрібно вказати домен, Ñкщо ви хочете викориÑтовувати Auto Review Apps та Auto Deploy."
+
+msgid "Can't find HEAD commit for this branch"
+msgstr ""
+
msgid "Cancel"
msgstr "СкаÑувати"
+msgid "Cancel this job"
+msgstr "СкаÑувати це завданнÑ"
+
msgid "Cannot be merged automatically"
-msgstr ""
+msgstr "Ðеможливо злити автоматично"
msgid "Cannot modify managed Kubernetes cluster"
msgstr "Ðеможливо змінити керований клаÑтер Kubernetes"
-msgid "Certificate fingerprint"
-msgstr ""
-
-msgid "Change Weight"
-msgstr "Вага зміни"
-
msgid "Change this value to influence how frequently the GitLab UI polls for updates."
-msgstr ""
+msgstr "Змініть це значеннÑ, щоб вплинути на чаÑтоту Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ñ–Ð½Ñ‚ÐµÑ€Ñ„ÐµÐ¹Ñу GitLab."
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "Вибрати в гілці"
@@ -814,21 +998,18 @@ msgstr "Виберіть файл ..."
msgid "Choose a branch/tag (e.g. %{master}) or enter a commit (e.g. %{sha}) to see what's changed or to create a merge request."
msgstr "Виберіть гілку чи тег (напр. %{master}) або введіть коміт (напр. %{sha}) Ð´Ð»Ñ Ð¿ÐµÑ€ÐµÐ³Ð»Ñду змін або Ð´Ð»Ñ ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ð·Ð°Ð¿Ð¸Ñ‚Ñƒ на злиттÑ."
-msgid "Choose file..."
-msgstr "Виберіть файл..."
+msgid "Choose any color."
+msgstr "Вибрати будь-Ñкий колір."
-msgid "Choose which groups you wish to synchronize to this secondary node."
-msgstr "Виберіть групи Ð´Ð»Ñ Ñинхронізації на цей вторинний вузол."
+msgid "Choose between <code>clone</code> or <code>fetch</code> to get the recent application code"
+msgstr ""
-msgid "Choose which repositories you want to connect and run CI/CD pipelines."
-msgstr "Виберіть, Ñкі репозиторії ви хочете підключити Ñ– запуÑтити конвеєри CI/CD."
+msgid "Choose file..."
+msgstr "Виберіть файл..."
msgid "Choose which repositories you want to import."
msgstr "Виберіть, Ñкі репозиторії ви хочете імпортувати."
-msgid "Choose which shards you wish to synchronize to this secondary node."
-msgstr "Виберіть Ñегменти Ð´Ð»Ñ Ñинхронізації на цей вторинний вузол."
-
msgid "CiStatusLabel|canceled"
msgstr "ÑкаÑовано"
@@ -898,21 +1079,12 @@ msgstr "* (Ð’ÑÑ– Ñередовища)"
msgid "CiVariable|All environments"
msgstr "Ð’ÑÑ– Ñередовища"
-msgid "CiVariable|Create wildcard"
-msgstr "Створити шаблон"
-
msgid "CiVariable|Error occured while saving variables"
msgstr "Помилка при збереженні змінних"
-msgid "CiVariable|New environment"
-msgstr "Ðове Ñередовище"
-
msgid "CiVariable|Protected"
msgstr "Захищений"
-msgid "CiVariable|Search environments"
-msgstr "Пошук Ñередовищ"
-
msgid "CiVariable|Toggle protected"
msgstr "Ввімкнути/вимкнути захиÑÑ‚"
@@ -922,30 +1094,30 @@ msgstr "Перевірка невдала"
msgid "CircuitBreakerApiLink|circuitbreaker api"
msgstr "circuitbreaker api"
-msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
-msgstr "ÐатиÑніть кнопку нижче, щоб розпочати Ð¿Ñ€Ð¾Ñ†ÐµÑ Ð²ÑтановленнÑ, перейшовши на Ñторінку Kubernetes"
+msgid "Clear search input"
+msgstr "ОчиÑтити поле вводу"
-msgid "Click to expand text"
-msgstr "ÐатиÑніть, щоб розгорнути текÑÑ‚"
+msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
+msgstr "Клікніть по будь-Ñкому <strong>імені проекту</strong> зі ÑпиÑку нижче Ð´Ð»Ñ Ñ‚Ð¾Ð³Ð¾, щоб перейти до етапу проекту."
-msgid "Client authentication certificate"
-msgstr ""
+msgid "Click the <strong>Promote</strong> button in the top right corner to promote it to a group milestone."
+msgstr "ÐатиÑніть кнопку <strong>ПеренеÑти</strong> у правому верхному куті щоб перенеÑти етап на рівень групи."
-msgid "Client authentication key"
-msgstr ""
+msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
+msgstr "ÐатиÑніть кнопку нижче, щоб розпочати Ð¿Ñ€Ð¾Ñ†ÐµÑ Ð²ÑтановленнÑ, перейшовши на Ñторінку Kubernetes"
-msgid "Client authentication key password"
+msgid "Click to expand it."
msgstr ""
+msgid "Click to expand text"
+msgstr "ÐатиÑніть, щоб розгорнути текÑÑ‚"
+
msgid "Clone repository"
msgstr "Клонувати репозиторій"
msgid "Close"
msgstr "Закрити"
-msgid "Closed"
-msgstr "Закрито"
-
msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster"
msgstr "%{appList} були уÑпішно вÑтановлені на ваш Kubernetes-клаÑтер"
@@ -961,6 +1133,12 @@ msgstr "Додати Ñ–Ñнуючий Kubernetes-клаÑтер"
msgid "ClusterIntegration|Advanced options on this Kubernetes cluster's integration"
msgstr "Детальні Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ñ–Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ñ–Ñ— із цим Kubernetes-клаÑтером"
+msgid "ClusterIntegration|An error occured while trying to fetch project zones: %{error}"
+msgstr "Помилка при отриманні зон проекту: %{error}"
+
+msgid "ClusterIntegration|An error occured while trying to fetch your projects: %{error}"
+msgstr "Помилка при отриманні ваших проектів: %{error}"
+
msgid "ClusterIntegration|Applications"
msgstr "ЗаÑтоÑунки"
@@ -991,6 +1169,9 @@ msgstr "Скопіювати Ñертифікат центру ÑертифікÐ
msgid "ClusterIntegration|Copy Ingress IP Address to clipboard"
msgstr "Копіювати IP-адреÑу Ingress в буфер обміну"
+msgid "ClusterIntegration|Copy Jupyter Hostname to clipboard"
+msgstr "Скопіювати Ñ–Ð¼â€™Ñ Ñ…Ð¾Ñта Jupyter в буфер обміну"
+
msgid "ClusterIntegration|Copy Kubernetes cluster name"
msgstr "Скопіювати Ñ–Ð¼â€™Ñ Kubernetes-клаÑтера"
@@ -1006,8 +1187,8 @@ msgstr "Створити Kubernetes-клаÑтер на Google Kubernetes Engine
msgid "ClusterIntegration|Create a new Kubernetes cluster on Google Kubernetes Engine right from GitLab"
msgstr "Створити Kubernetes-клаÑтер на Google Kubernetes Engine прÑмо із GitLab"
-msgid "ClusterIntegration|Create on GKE"
-msgstr "Створити в GKE"
+msgid "ClusterIntegration|Create on Google Kubernetes Engine"
+msgstr "Створити в Google Kubernetes Engine"
msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
msgstr "Вкажіть параметри Ñ–Ñнуючого клаÑтера Kubernetes"
@@ -1016,29 +1197,38 @@ msgid "ClusterIntegration|Enter the details for your Kubernetes cluster"
msgstr "Введіть параметри вашого Kubernetes-клаÑтера"
msgid "ClusterIntegration|Environment scope"
+msgstr "Межі Ñередовищ"
+
+msgid "ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab's Google Kubernetes Engine Integration."
msgstr ""
+msgid "ClusterIntegration|Fetching machine types"
+msgstr "ÐžÑ‚Ñ€Ð¸Ð¼Ð°Ð½Ð½Ñ Ñ‚Ð¸Ð¿Ñ–Ð² машин"
+
+msgid "ClusterIntegration|Fetching projects"
+msgstr "ÐžÑ‚Ñ€Ð¸Ð¼Ð°Ð½Ð½Ñ Ð¿Ñ€Ð¾ÐµÐºÑ‚Ñ–Ð²"
+
+msgid "ClusterIntegration|Fetching zones"
+msgstr "ÐžÑ‚Ñ€Ð¸Ð¼Ð°Ð½Ð½Ñ Ð·Ð¾Ð½"
+
msgid "ClusterIntegration|GitLab Integration"
msgstr "Ð†Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ñ–Ñ Ð· GitLab"
msgid "ClusterIntegration|GitLab Runner"
msgstr "GitLab Runner"
-msgid "ClusterIntegration|Google Cloud Platform project ID"
-msgstr "Ідентифікатор проекту в Google Cloud Platform"
+msgid "ClusterIntegration|Google Cloud Platform project"
+msgstr "Проект Google Cloud Platform"
msgid "ClusterIntegration|Google Kubernetes Engine"
msgstr "Google Kubernetes Engine"
msgid "ClusterIntegration|Google Kubernetes Engine project"
-msgstr "Проект Google Kubernetes Engine"
+msgstr "проекті Google Kubernetes Engine"
msgid "ClusterIntegration|Helm Tiller"
msgstr "Helm Tiller"
-msgid "ClusterIntegration|In order to show the health of the cluster, we'll need to provision your cluster with Prometheus to collect the required data."
-msgstr "Ð”Ð»Ñ Ñ‚Ð¾Ð³Ð¾, щоб показувати Ñтан клаÑтера, нам потрібно буде вÑтановити Prometheus на ваш клаÑтер Ð´Ð»Ñ Ð·Ð±Ð¾Ñ€Ñƒ необхідних даних."
-
msgid "ClusterIntegration|Ingress"
msgstr "Ingress"
@@ -1048,9 +1238,6 @@ msgstr "Ingress IP-адреÑа"
msgid "ClusterIntegration|Install"
msgstr "Ð’Ñтановити"
-msgid "ClusterIntegration|Install Prometheus"
-msgstr "Ð’Ñтановити Prometheus"
-
msgid "ClusterIntegration|Installed"
msgstr "Ð’Ñтановлений"
@@ -1063,15 +1250,18 @@ msgstr "Ð†Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ñ–Ñ ÐºÐ»Ð°Ñтерної автоматизації Kub
msgid "ClusterIntegration|Integration status"
msgstr "Ð¡Ñ‚Ð°Ñ‚ÑƒÑ Ñ–Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ñ–Ñ—"
+msgid "ClusterIntegration|Jupyter Hostname"
+msgstr "Ð†Ð¼â€™Ñ Ñ…Ð¾Ñта Jupyter"
+
+msgid "ClusterIntegration|JupyterHub"
+msgstr "JupyterHub"
+
msgid "ClusterIntegration|Kubernetes cluster"
msgstr "Kubernetes-клаÑтер"
msgid "ClusterIntegration|Kubernetes cluster details"
msgstr "Параметри Kubernetes-клаÑтера"
-msgid "ClusterIntegration|Kubernetes cluster health"
-msgstr "Стан Kubernetes-клаÑтера"
-
msgid "ClusterIntegration|Kubernetes cluster integration"
msgstr "Ð†Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ñ–Ñ Ñ–Ð· Kubernetes-клаÑтером"
@@ -1099,8 +1289,14 @@ msgstr "Kubernetes-клаÑтери дозволÑють вам викориÑÑ‚
msgid "ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project"
msgstr "Kubernetes-клаÑтери можуть бути викориÑтані Ð´Ð»Ñ Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ð·Ð°ÑтоÑунків Ñ– викориÑÑ‚Ð°Ð½Ð½Ñ Review Apps Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ проекту"
-msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
-msgstr "ДізнайтеÑÑ Ð±Ñ–Ð»ÑŒÑˆÐµ про %{link_to_documentation}"
+msgid "ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}."
+msgstr "ДізнайтеÑÑ Ð±Ñ–Ð»ÑŒÑˆÐµ про %{help_link_start_machine_type}типи машин%{help_link_end} та %{help_link_start_pricing}ціни%{help_link_end}."
+
+msgid "ClusterIntegration|Learn more about %{help_link_start}Kubernetes%{help_link_end}."
+msgstr "ДізнайтеÑÑ Ð±Ñ–Ð»ÑŒÑˆÐµ про %{help_link_start}Kubernetes%{help_link_end}."
+
+msgid "ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}."
+msgstr "ДізнайтеÑÑ Ð±Ñ–Ð»ÑŒÑˆÐµ про %{help_link_start}зони%{help_link_end}."
msgid "ClusterIntegration|Learn more about environments"
msgstr "ДізнайтеÑÑ Ð±Ñ–Ð»ÑŒÑˆÐµ про Ñередовища"
@@ -1112,7 +1308,7 @@ msgid "ClusterIntegration|Machine type"
msgstr "Тип машини"
msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create Kubernetes clusters"
-msgstr "ВпевнітьÑÑ, що ваш обліковий Ð·Ð°Ð¿Ð¸Ñ Ð·Ð°Ð´Ð¾Ð²Ñ–Ð»ÑŒÐ½ÑÑ” %{link_to_requirements} Ð´Ð»Ñ ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Kubernetes-клаÑтерів"
+msgstr "ВпевнітьÑÑ, що ваш обліковий Ð·Ð°Ð¿Ð¸Ñ %{link_to_requirements} Ð´Ð»Ñ ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Kubernetes-клаÑтерів"
msgid "ClusterIntegration|Manage"
msgstr "УправліннÑ"
@@ -1123,8 +1319,17 @@ msgstr "Керуйте вашим Kubernetes-клаÑтером за допомÐ
msgid "ClusterIntegration|More information"
msgstr "Додаткова інформаціÑ"
-msgid "ClusterIntegration|Multiple Kubernetes clusters are available in GitLab Enterprise Edition Premium and Ultimate"
-msgstr "Декілька Kubernetes-клаÑтерів доÑтупні в GitLab Enterprise Edition Premium та Ultimate"
+msgid "ClusterIntegration|No machine types matched your search"
+msgstr "Жоден тип машин не відповідає вашому пошуку"
+
+msgid "ClusterIntegration|No projects found"
+msgstr "Проектів не знайдено"
+
+msgid "ClusterIntegration|No projects matched your search"
+msgstr "Жоден проект не відповідає вашому пошуку"
+
+msgid "ClusterIntegration|No zones matched your search"
+msgstr "Жодна зона не відповідає вашому пошуку"
msgid "ClusterIntegration|Note:"
msgstr "Примітка:"
@@ -1138,9 +1343,6 @@ msgstr "Введіть інформацію про доÑтуп до Ñвого
msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
msgstr "Будь-лаÑка впевнітьÑÑ, що ваш обліковий Ð·Ð°Ð¿Ð¸Ñ Google задовольнÑÑ” наÑтупним вимогам:"
-msgid "ClusterIntegration|Project ID"
-msgstr "Ідентифікатор проекту"
-
msgid "ClusterIntegration|Project namespace"
msgstr "ПроÑтір імен проекту"
@@ -1153,6 +1355,9 @@ msgstr "Prometheus"
msgid "ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration."
msgstr "ПереглÑньте нашу %{link_to_help_page} про інтеграцію із Kubernetes."
+msgid "ClusterIntegration|Redeem up to $500 in free credit for Google Cloud Platform"
+msgstr "Отримайте до $500 на Ñвій рахунок Google Cloud Platform"
+
msgid "ClusterIntegration|Remove Kubernetes cluster integration"
msgstr "Відалити інтеграцію із Kubernetes-клаÑтером"
@@ -1168,20 +1373,38 @@ msgstr "Запит про початок вÑÑ‚Ð°Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð½Ðµ викоÐ
msgid "ClusterIntegration|Save changes"
msgstr "Зберегти зміни"
+msgid "ClusterIntegration|Search machine types"
+msgstr "Пошук по типам машин"
+
+msgid "ClusterIntegration|Search projects"
+msgstr "Пошук проектів"
+
+msgid "ClusterIntegration|Search zones"
+msgstr "Пошук зон"
+
msgid "ClusterIntegration|Security"
msgstr "Безпека"
msgid "ClusterIntegration|See and edit the details for your Kubernetes cluster"
msgstr "ПереглÑнути та редагувати параметри вашого Kubernetes-клаÑтера"
-msgid "ClusterIntegration|See machine types"
-msgstr "ПереглÑнути типи машин"
+msgid "ClusterIntegration|Select machine type"
+msgstr "Вибрати тип машин"
+
+msgid "ClusterIntegration|Select project"
+msgstr "Вибрати проект"
+
+msgid "ClusterIntegration|Select project and zone to choose machine type"
+msgstr "Виберіть проект та зону Ð´Ð»Ñ Ñ‚Ð¾Ð³Ð¾, що вибрати тип машини"
+
+msgid "ClusterIntegration|Select project to choose zone"
+msgstr "Виберіть проект, щоб вибрати зону"
-msgid "ClusterIntegration|See your projects"
-msgstr "ПереглÑнути ваші проекти"
+msgid "ClusterIntegration|Select zone"
+msgstr "Вибрати зону"
-msgid "ClusterIntegration|See zones"
-msgstr "ПереглÑнути зони"
+msgid "ClusterIntegration|Select zone to choose machine type"
+msgstr "Виберіть зону, щоб вибрати тип машин"
msgid "ClusterIntegration|Service token"
msgstr "Токен СервіÑа"
@@ -1213,6 +1436,9 @@ msgstr "Увімкнути/вимкнути Kubernetes-клаÑтер"
msgid "ClusterIntegration|Token"
msgstr "Токен"
+msgid "ClusterIntegration|Validating project billing status"
+msgstr "Перевірка Ñтану білінгу проекта"
+
msgid "ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr "За допомогою підключеного до цього проекту Kubernetes-клаÑтера, ви можете викориÑтовувати Review Apps, розгортати ваші проекти, запуÑкати конвеєри збірки тощо."
@@ -1243,14 +1469,20 @@ msgstr "задовольнÑÑ” вимогам"
msgid "ClusterIntegration|properly configured"
msgstr "правильно налаштований"
+msgid "ClusterIntegration|sign up"
+msgstr "зареєÑтрувати"
+
msgid "Collapse"
msgstr "Згорнути"
-msgid "Comment and resolve discussion"
-msgstr "Залишити коментар Ñ– завершити обговореннÑ"
+msgid "Collapse sidebar"
+msgstr "Згорнути бокову панель"
-msgid "Comment and unresolve discussion"
-msgstr "Залишити коментар Ñ– повторно відкрити обговореннÑ"
+msgid "Comment & resolve discussion"
+msgstr ""
+
+msgid "Comment & unresolve discussion"
+msgstr ""
msgid "Comments"
msgstr "Коментарі"
@@ -1320,6 +1552,9 @@ msgstr "Ðе знайдено пов'Ñзаних запитів на злитт
msgid "Committed by"
msgstr "Коміт від"
+msgid "Commit…"
+msgstr "Коміт…"
+
msgid "Compare"
msgstr "ПорівнÑти"
@@ -1330,10 +1565,10 @@ msgid "Compare Revisions"
msgstr "ПорівнÑÐ½Ð½Ñ Ñ€ÐµÐ´Ð°ÐºÑ†Ñ–Ð¹"
msgid "Compare changes with the last commit"
-msgstr ""
+msgstr "ПорівнÑти зміни з оÑтаннім комітом"
msgid "Compare changes with the merge request target branch"
-msgstr ""
+msgstr "ПорівнÑти зміни із ціловою гілкою запиту на злиттÑ"
msgid "CompareBranches|%{source_branch} and %{target_branch} are the same."
msgstr "%{source_branch} і %{target_branch} однакові."
@@ -1348,47 +1583,41 @@ msgid "CompareBranches|Target"
msgstr "Ціль"
msgid "CompareBranches|There isn't anything to compare."
-msgstr ""
+msgstr "Ðема чого порівнювати."
msgid "Confidential"
-msgstr ""
+msgstr "Конфіденційний"
msgid "Confidentiality"
msgstr "КонфіденційніÑть"
msgid "Configure Gitaly timeouts."
-msgstr ""
+msgstr "Ðалаштувати таймаути Gitaly."
msgid "Configure Sidekiq job throttling."
-msgstr ""
+msgstr "Ðалаштувати чаÑтоту завдань Sidekiq."
msgid "Configure automatic git checks and housekeeping on repositories."
-msgstr ""
+msgstr "Ðалаштувати автоматичні перевірки git Ñ– Ð¾Ñ‡Ð¸Ñ‰ÐµÐ½Ð½Ñ Ð² репозиторіÑÑ…."
msgid "Configure limits for web and API requests."
+msgstr "Ðалаштуйте Ð¾Ð±Ð¼ÐµÐ¶ÐµÐ½Ð½Ñ Ð´Ð»Ñ Ð²ÐµÐ± та API запитів."
+
+msgid "Configure push mirrors."
msgstr ""
msgid "Configure storage path and circuit breaker settings."
-msgstr ""
+msgstr "Ðалаштуйте шлÑÑ… до Ñховищ та circuit breaker."
msgid "Configure the way a user creates a new account."
-msgstr ""
+msgstr "Ðалаштувати ÑпоÑіб ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ ÐºÐ¾Ñ€Ð¸Ñтувачем нового облікового запиÑу."
msgid "Connect"
msgstr "Підключити"
-msgid "Connect all repositories"
-msgstr "Підключити вÑÑ– репозиторії"
-
msgid "Connect repositories from GitHub"
msgstr "Підключити репозиторії з GitHub"
-msgid "Connect your external repositories, and CI/CD pipelines will run for new commits. A GitLab project will be created with only CI/CD features enabled."
-msgstr "Підключіть ваші зовнішні репозиторії, Ñ– CI/CD конвеєри будуть запуÑкатиÑÑ Ð´Ð»Ñ Ð½Ð¾Ð²Ð¸Ñ… комітів. Створений GitLab-проект буде мати лише CI/CD фунції."
-
-msgid "Connecting..."
-msgstr "З'єднаннÑ..."
-
msgid "Container Registry"
msgstr "РеєÑтр Контейнерів"
@@ -1434,7 +1663,16 @@ 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 "ContainerRegistry|You can also use a %{deploy_token} for read-only access to the registry images."
+msgstr "Ви також можете викориÑтовувати %{deploy_token} Ð´Ð»Ñ Ð´Ð¾Ñтупу тільки Ð´Ð»Ñ Ñ‡Ð¸Ñ‚Ð°Ð½Ð½Ñ Ð´Ð¾ образів у реєÑтрі."
+
+msgid "Continue"
+msgstr "Продовжити"
+
msgid "Continuous Integration and Deployment"
+msgstr "Безперервна Ñ–Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ñ–Ñ Ñ‚Ð° розгортаннÑ"
+
+msgid "Contribute to GitLab"
msgstr ""
msgid "Contribution"
@@ -1458,15 +1696,6 @@ msgstr "Коміти в %{branch_name}, за винÑтком комітів зÐ
msgid "ContributorsPage|Please wait a moment, this page will automatically refresh when ready."
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 "Скопіюйте відкритий SSH-ключ в буфер обміну"
-
msgid "Copy URL to clipboard"
msgstr "Скопіювати URL в буфер обміну"
@@ -1479,9 +1708,18 @@ msgstr "Скопіювати команду в буфер обміну"
msgid "Copy commit SHA to clipboard"
msgstr "Скопіювати ідентифікатор в буфер обміну"
+msgid "Copy file name to clipboard"
+msgstr ""
+
+msgid "Copy file path to clipboard"
+msgstr ""
+
msgid "Copy reference to clipboard"
msgstr "Скопіювати поÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð² буфер обміну"
+msgid "Copy to clipboard"
+msgstr "Копіювати в буфер обміну"
+
msgid "Create"
msgstr "Створити"
@@ -1500,14 +1738,14 @@ msgstr "Створіть токен доÑтупу Ð´Ð»Ñ Ð²Ð°ÑˆÐ¾Ð³Ð¾ аккаÑ
msgid "Create branch"
msgstr "Створити гілку"
+msgid "Create commit"
+msgstr "Створити коміт"
+
msgid "Create directory"
msgstr "Створити каталог"
msgid "Create empty repository"
-msgstr ""
-
-msgid "Create epic"
-msgstr "Створити епік"
+msgstr "Створити порожній репозиторій"
msgid "Create file"
msgstr "Створити файл"
@@ -1516,7 +1754,7 @@ msgid "Create group label"
msgstr "Створити мітку групи"
msgid "Create lists from labels. Issues with that label appear in that list."
-msgstr "Створити ÑпиÑок на оÑтнові міток. Ð’ ньому будуть проблеми з такими мітками."
+msgstr "Створити ÑпиÑок на оÑнові міток. Ð’ ньому будуть проблеми з такими мітками."
msgid "Create merge request"
msgstr "Створити запит на злиттÑ"
@@ -1551,14 +1789,11 @@ msgstr "Тег"
msgid "CreateTokenToCloneLink|create a personal access token"
msgstr "Створити токен Ð´Ð»Ñ Ð¾ÑобиÑтого доÑтупу"
-msgid "Creates a new branch from %{branchName}"
-msgstr "Створює нову гілку із %{branchName}"
+msgid "Created"
+msgstr "Створено"
-msgid "Creates a new branch from %{branchName} and re-directs to create a new merge request"
-msgstr "Створює нову гілку із %{branchName} Ñ– перенаправлÑÑ” Ð´Ð»Ñ ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ð½Ð¾Ð²Ð¾Ð³Ð¾ запиту на злиттÑ"
-
-msgid "Creating epic"
-msgstr "Ð¡Ñ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ ÐµÐ¿Ñ–ÐºÑƒ"
+msgid "Created by me"
+msgstr "Створено мною"
msgid "Cron Timezone"
msgstr "ЧаÑовий поÑÑ Cron"
@@ -1566,8 +1801,14 @@ msgstr "ЧаÑовий поÑÑ Cron"
msgid "Cron syntax"
msgstr "СинтакÑÐ¸Ñ Cron"
-msgid "Current node"
-msgstr "Поточний вузол"
+msgid "CurrentUser|Profile"
+msgstr "Профіль"
+
+msgid "CurrentUser|Settings"
+msgstr "ÐалаштуваннÑ"
+
+msgid "Custom CI config path"
+msgstr ""
msgid "Custom notification events"
msgstr "КориÑтувацькі Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½ÑŒ про події"
@@ -1575,9 +1816,6 @@ 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 "Customize colors"
-msgstr ""
-
msgid "Cycle Analytics"
msgstr "Ðналіз циклу"
@@ -1614,8 +1852,8 @@ msgstr "груд."
msgid "December"
msgstr "грудень"
-msgid "Default classification label"
-msgstr "Мітка клаÑифікації за замовчуваннÑм"
+msgid "Decline and sign out"
+msgstr "Відхити та вийти"
msgid "Define a custom pattern with cron syntax"
msgstr "Визначте влаÑний шаблон за допомогою ÑинтакÑиÑу cron"
@@ -1623,6 +1861,9 @@ msgstr "Визначте влаÑний шаблон за допомогою ÑÐ
msgid "Delete"
msgstr "Видалити"
+msgid "Delete list"
+msgstr "Видалити ÑпиÑок"
+
msgid "Deploy"
msgid_plural "Deploys"
msgstr[0] "РозгортаннÑ"
@@ -1633,38 +1874,164 @@ msgstr[3] "Розгортань"
msgid "Deploy Keys"
msgstr "Ключі Ð´Ð»Ñ Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚ÑƒÐ²Ð°Ð½Ð½Ñ"
+msgid "DeployKeys|+%{count} others"
+msgstr "+%{count} інших"
+
+msgid "DeployKeys|Current project"
+msgstr "Поточний проект"
+
+msgid "DeployKeys|Deploy key"
+msgstr "Ключ Ð´Ð»Ñ Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚ÑƒÐ²Ð°Ð½Ð½Ñ"
+
+msgid "DeployKeys|Enabled deploy keys"
+msgstr "Увімкнені ключі розгортаннÑ"
+
+msgid "DeployKeys|Error enabling deploy key"
+msgstr "Помилка при увімкненні ключа розгортаннÑ"
+
+msgid "DeployKeys|Error getting deploy keys"
+msgstr "Помилка при отриманні колючів розгортаннÑ"
+
+msgid "DeployKeys|Error removing deploy key"
+msgstr "Помилка при видаленні ключа розгортаннÑ"
+
+msgid "DeployKeys|Expand %{count} other projects"
+msgstr "Розгорнути %{count} інших проектів"
+
+msgid "DeployKeys|Loading deploy keys"
+msgstr "Ð—Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ ÐºÐ¾Ð»ÑŽÑ‡Ñ–Ð² розгортаннÑ"
+
+msgid "DeployKeys|No deploy keys found. Create one with the form above."
+msgstr "Ðе знайдено ключів розгортаннÑ. Створюйте Ñ—Ñ… за допомогою форми вище."
+
+msgid "DeployKeys|Privately accessible deploy keys"
+msgstr "Приватні ключі розгортаннÑ"
+
+msgid "DeployKeys|Project usage"
+msgstr "ВикориÑÑ‚Ð°Ð½Ð½Ñ Ñƒ проектах"
+
+msgid "DeployKeys|Publicly accessible deploy keys"
+msgstr "Публічні колючі розгортаннÑ"
+
+msgid "DeployKeys|Read access only"
+msgstr "ДоÑтуп тільки Ð´Ð»Ñ Ñ‡Ð¸Ñ‚Ð°Ð½Ð½Ñ"
+
+msgid "DeployKeys|Write access allowed"
+msgstr "Дозволено доÑтуп Ð´Ð»Ñ Ð·Ð°Ð¿Ð¸Ñу"
+
+msgid "DeployKeys|You are going to remove this deploy key. Are you sure?"
+msgstr "Ви видалÑєте ключ розгортаннÑ. Ви впевнені?"
+
+msgid "DeployTokens|Active Deploy Tokens (%{active_tokens})"
+msgstr "Ðктивні токени Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ (%{active_tokens})"
+
+msgid "DeployTokens|Add a deploy token"
+msgstr "Додати токен розгортаннÑ"
+
+msgid "DeployTokens|Allows read-only access to the registry images"
+msgstr "ДозволÑÑ” доÑтуп лише Ð´Ð»Ñ Ñ‡Ð¸Ñ‚Ð°Ð½Ð½Ñ Ð´Ð¾ реєÑтру образів"
+
+msgid "DeployTokens|Allows read-only access to the repository"
+msgstr "ДозволÑÑ” доÑтуп лише Ð´Ð»Ñ Ñ‡Ð¸Ñ‚Ð°Ð½Ð½Ñ Ð´Ð¾ репозиторіÑ"
+
+msgid "DeployTokens|Copy deploy token to clipboard"
+msgstr "Скопіюйте токен Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ð² буфер обміну"
+
+msgid "DeployTokens|Copy username to clipboard"
+msgstr "Скопіюйте ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача в буфер обміну"
+
+msgid "DeployTokens|Create deploy token"
+msgstr "Створити токен розгортаннÑ"
+
+msgid "DeployTokens|Created"
+msgstr "Створено"
+
+msgid "DeployTokens|Deploy Tokens"
+msgstr "Токени РозгортаннÑ"
+
+msgid "DeployTokens|Deploy tokens allow read-only access to your repository and registry images."
+msgstr "Токени Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ð½Ð°Ð´Ð°ÑŽÑ‚ÑŒ доÑтуп лише Ð´Ð»Ñ Ñ‡Ð¸Ñ‚Ð°Ð½Ð½Ñ Ð´Ð¾ вашого репозиторію Ñ– образів у реєÑтрі."
+
+msgid "DeployTokens|Expires"
+msgstr "Термін дії"
+
+msgid "DeployTokens|Name"
+msgstr "Ðазва"
+
+msgid "DeployTokens|Pick a name for the application, and we'll give you a unique deploy token."
+msgstr "Виберіть назву заÑтоÑунку Ñ– ми надамо вам унікальний токен розгортаннÑ."
+
+msgid "DeployTokens|Revoke"
+msgstr "Відкликати"
+
+msgid "DeployTokens|Revoke %{name}"
+msgstr "Відкликати %{name}"
+
+msgid "DeployTokens|Scopes"
+msgstr "Межі"
+
+msgid "DeployTokens|This action cannot be undone."
+msgstr "Ð¦Ñ Ð´Ñ–Ñ Ð½Ðµ може бути ÑкаÑована."
+
+msgid "DeployTokens|This project has no active Deploy Tokens."
+msgstr "Цей проект не має активних токенів розгортаннÑ."
+
+msgid "DeployTokens|Use this token as a password. Make sure you save it - you won't be able to access it again."
+msgstr "ВикориÑтовуйте цей токен Ñк пароль. ПереконайтеÑÑ, що ви його зберегли: ви більше не зможете отримати доÑтуп до нього."
+
+msgid "DeployTokens|Use this username as a login."
+msgstr "ВикориÑтовувати це ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача Ñк логін."
+
+msgid "DeployTokens|Username"
+msgstr "Ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача"
+
+msgid "DeployTokens|You are about to revoke"
+msgstr "Зараз ви відкличете"
+
+msgid "DeployTokens|Your New Deploy Token"
+msgstr "Ваш новий токер розгортаннÑ"
+
+msgid "DeployTokens|Your new project deploy token has been created."
+msgstr "Створено ваш новий токен Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ð´Ð»Ñ Ð¿Ñ€Ð¾ÐµÐºÑ‚Ñƒ."
+
+msgid "Deprioritize label"
+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 "Diffs|No file name available"
msgstr "Ім'Ñ Ñ„Ð°Ð¹Ð»Ñƒ не доÑтупне"
+msgid "Diffs|Something went wrong while fetching diff lines."
+msgstr ""
+
msgid "Directory name"
msgstr "Ім'Ñ ÐºÐ°Ñ‚Ð°Ð»Ð¾Ð³Ñƒ"
msgid "Disable"
msgstr "Вимкнути"
+msgid "Disable for this project"
+msgstr "Вимкнути Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ проекту"
+
+msgid "Disable group Runners"
+msgstr "Вимкнути групові Runner'и"
+
+msgid "Discard changes"
+msgstr "Відхилити зміни"
+
msgid "Discard draft"
msgstr "Видалити чернетку"
-msgid "Discover GitLab Geo."
-msgstr "Відкрийте GitLab Geo."
-
msgid "Dismiss Cycle Analytics introduction box"
-msgstr "Відмінити блок вÑтупу до Ðналитики Циклу"
-
-msgid "Dismiss Merge Request promotion"
-msgstr "Вимкнути рекламу Ð´Ð»Ñ Ð—Ð°Ð¿Ð¸Ñ‚Ñ–Ð² на злиттÑ"
+msgstr "Відхилити блок вÑтупу до Ðналитики Циклу"
-msgid "Documentation for popular identity providers"
-msgstr ""
+msgid "Domain"
+msgstr "Домен"
msgid "Don't show again"
msgstr "Ðе показувати знову"
@@ -1700,70 +2067,79 @@ msgid "DownloadSource|Download"
msgstr "Завантажити"
msgid "Downvotes"
-msgstr ""
+msgstr "Дизлайки"
msgid "Due date"
msgstr "Запланована дата завершеннÑ"
-msgid "During this process, you’ll be asked for URLs from GitLab’s side. Use the URLs shown below."
-msgstr ""
+msgid "Each Runner can be in one of the following states:"
+msgstr "Кожен Runner може бути в одному із таких Ñтанів:"
msgid "Edit"
msgstr "Редагувати"
+msgid "Edit Label"
+msgstr "Редагувати мітку"
+
msgid "Edit Pipeline Schedule %{id}"
msgstr "Редагувати Розклад Конвеєра %{id}"
msgid "Edit files in the editor and commit changes here"
msgstr "Редагуйте файли в редакторі і закомітьте зміни тут"
-msgid "Editing"
-msgstr ""
-
-msgid "Elasticsearch"
-msgstr ""
-
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "Edit identity for %{user_name}"
msgstr ""
msgid "Email"
+msgstr "Електронна пошта"
+
+msgid "Email patch"
msgstr ""
msgid "Emails"
msgstr "ÐдреÑи електронної пошти"
+msgid "Embed"
+msgstr "Вбудувати"
+
msgid "Enable"
msgstr "Увімкнути"
msgid "Enable Auto DevOps"
msgstr "Увімкнути Auto DevOps"
-msgid "Enable SAML authentication for this group"
-msgstr ""
-
msgid "Enable Sentry for error reporting and logging."
-msgstr ""
+msgstr "Увімкнути Sentry Ð´Ð»Ñ Ð·Ð²Ñ–Ñ‚Ñ–Ð² про помилки та логуваннÑ."
msgid "Enable and configure InfluxDB metrics."
-msgstr ""
+msgstr "Включити і налаштувати метрики InfluxDB."
msgid "Enable and configure Prometheus metrics."
-msgstr ""
+msgstr "Включити і налаштувати метрики Prometheus."
-msgid "Enable classification control using an external service"
-msgstr ""
+msgid "Enable for this project"
+msgstr "Увімкнути Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ проекту"
+
+msgid "Enable group Runners"
+msgstr "Увімкнути групові Runner'и"
+
+msgid "Enable or disable certain group features and choose access levels."
+msgstr "Увімкніть або вимкніть певні функції групи та виберіть рівні доÑтупу."
msgid "Enable or disable version check and usage ping."
-msgstr ""
+msgstr "Увімкнути чи вимкнути перевірку верÑÑ–Ñ— та надÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð´Ð°Ð½Ð¸Ñ… про викориÑтаннÑ."
msgid "Enable reCAPTCHA or Akismet and set IP limits."
-msgstr ""
+msgstr "Увімкнути reCAPTCHA або Akismet та вÑтановити Ð¾Ð±Ð¼ÐµÐ¶ÐµÐ½Ð½Ñ Ð¿Ð¾ IP."
msgid "Enable the Performance Bar for a given group."
-msgstr ""
+msgstr "Увімкнути панель продуктивноÑті Ð´Ð»Ñ Ð´Ð°Ð½Ð¾Ñ— групи."
-msgid "Enabled"
-msgstr ""
+msgid "Ends at (UTC)"
+msgstr "ЗавершуєтьÑÑ Ð¾ (за Грінвічем)"
+
+msgid "Environments"
+msgstr "Середовища"
msgid "Environments|An error occurred while fetching the environments."
msgstr "Виникла помилка при завантаженні Ñередовищ."
@@ -1813,33 +2189,18 @@ msgstr "Оновлено"
msgid "Environments|You don't have any environments right now."
msgstr "Ви поки не налаштували жодного Ñередовища."
-msgid "Epic will be removed! Are you sure?"
-msgstr "Епік буде видалено! Ви впевнені?"
-
-msgid "Epics"
-msgstr "Епіки"
-
-msgid "Epics Roadmap"
-msgstr "План-графік епіків"
-
-msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
-msgstr "Епіки дозволÑють керувати вашим портфелем проектів ефективніше та з меншими зуÑиллÑми"
-
msgid "Error Reporting and Logging"
-msgstr ""
-
-msgid "Error checking branch data. Please try again."
-msgstr "Помилка при отриманні даних гілки. Будь лаÑка, Ñпробуйте пізніше."
+msgstr "Звіти про помилки та логуваннÑ"
msgid "Error committing changes. Please try again."
msgstr "Помилка при коміті змін. Будь лаÑка, Ñпробуйте пізніше."
-msgid "Error creating epic"
-msgstr "Помилка при Ñтворенні епіку"
-
msgid "Error fetching contributors data."
msgstr "Помилка Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ð½Ð½Ñ Ð´Ð°Ð½Ð¸Ñ… учаÑників."
+msgid "Error fetching job trace"
+msgstr "Помилка при отриманні журнала завданнÑ"
+
msgid "Error fetching labels."
msgstr "Помилка Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ Ð¼Ñ–Ñ‚Ð¾Ðº."
@@ -1850,7 +2211,19 @@ msgid "Error fetching refs"
msgstr "Помилка Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ refs"
msgid "Error fetching usage ping data."
-msgstr "Помилка при отриманні данних по перевірці з’єднаннÑ."
+msgstr "Помилка при отриманні данних про викориÑтаннÑ."
+
+msgid "Error loading branch data. Please try again."
+msgstr "Помилка при завантаженні даних про гілку. Будь лаÑка, Ñпробуйте знову."
+
+msgid "Error loading last commit."
+msgstr "Помилка при завантаженні оÑтаннього коміту."
+
+msgid "Error loading merge requests."
+msgstr "Помилка при завантаженні запитів на злиттÑ."
+
+msgid "Error loading project data. Please try again."
+msgstr "Помилка при завантаженні даних проекту. Будь лаÑка, Ñпробуйте знову."
msgid "Error occurred when toggling the notification subscription"
msgstr "СталаÑÑ Ð¿Ð¾Ð¼Ð¸Ð»ÐºÐ° під Ñ‡Ð°Ñ Ð¿Ñ–Ð´ÐºÐ»ÑŽÑ‡ÐµÐ½Ð½Ñ Ð¿Ñ–Ð´Ð¿Ð¸Ñки на ÑповіщеннÑ"
@@ -1864,6 +2237,9 @@ msgstr "Помилка Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ ÑтатуÑу Ð´Ð»Ñ Ð²ÑÑ–Ñ… задÐ
msgid "Error updating todo status."
msgstr "Помилка при оновленні ÑтатуÑу задачі."
+msgid "Estimated"
+msgstr "За оцінками"
+
msgid "EventFilterBy|Filter by all"
msgstr "Фільтрувати по вÑім"
@@ -1894,42 +2270,30 @@ msgstr "Ð©Ð¾Ñ‚Ð¸Ð¶Ð½Ñ (в неділю о 4:00 ранку)"
msgid "Expand"
msgstr "Розгорнути"
+msgid "Expand all"
+msgstr ""
+
+msgid "Expand sidebar"
+msgstr "Розгорніть бічну панель"
+
msgid "Explore projects"
msgstr "ОглÑд проектів"
msgid "Explore public groups"
msgstr "ПереглÑнути публічні групи"
-msgid "External Classification Policy Authorization"
-msgstr "ÐÐ²Ñ‚Ð¾Ñ€Ð¸Ð·Ð°Ñ†Ñ–Ñ Ð²Ñ–Ð´Ð¿Ð¾Ð²Ñ–Ð´Ð½Ð¾ до зовнішньої політики"
-
-msgid "External authentication"
-msgstr ""
-
-msgid "External authorization denied access to this project"
-msgstr "Ð—Ð¾Ð²Ð½Ñ–ÑˆÐ½Ñ Ð°Ð²Ñ‚Ð¾Ñ€Ð¸Ð·Ð°Ñ†Ñ–Ñ Ð·Ð°Ð±Ð¾Ñ€Ð¾Ð½Ð¸Ð»Ð° доÑтуп до цього проекту"
-
-msgid "External authorization request timeout"
-msgstr ""
-
-msgid "ExternalAuthorizationService|Classification Label"
-msgstr "Мітка клаÑифікації"
-
-msgid "ExternalAuthorizationService|Classification label"
-msgstr "Мітка клаÑифікації"
-
-msgid "ExternalAuthorizationService|When no classification label is set the default label `%{default_label}` will be used."
-msgstr "Якщо клаÑифікаційну мітку не вÑтановлено, викориÑтовуватиметьÑÑ Ñтандартна мітка `%{default_label}`."
-
msgid "Failed"
msgstr "Ðевдало"
msgid "Failed Jobs"
-msgstr ""
+msgstr "Провалені завданнÑ"
msgid "Failed to change the owner"
msgstr "Ðе вдалоÑÑ Ð·Ð¼Ñ–Ð½Ð¸Ñ‚Ð¸ влаÑника"
+msgid "Failed to check related branches."
+msgstr "Ðе вдалоÑÑ Ð¿ÐµÑ€ÐµÐ²Ñ–Ñ€Ð¸Ñ‚Ð¸ пов’Ñзані гілки."
+
msgid "Failed to remove issue from board, please try again."
msgstr "Ðе вдалоÑÑ Ð²Ð¸Ð´Ð°Ð»Ð¸Ñ‚Ð¸ проблему з дошки, будь лаÑка, Ñпробуйте ще раз."
@@ -1939,6 +2303,12 @@ msgstr "Ðе вдалоÑÑ Ð²Ð¸Ð´Ð°Ð»Ð¸Ñ‚Ð¸ розклад конвеєра"
msgid "Failed to update issues, please try again."
msgstr "Ðе вдалоÑÑ Ð¾Ð½Ð¾Ð²Ð¸Ñ‚Ð¸ проблеми. Будь лаÑка, Ñпробуйте ще раз."
+msgid "Failure"
+msgstr "Помилка"
+
+msgid "Faster as it re-uses the project workspace (falling back to clone if it doesn't exist)"
+msgstr ""
+
msgid "Feb"
msgstr "лют."
@@ -1948,18 +2318,12 @@ msgstr "лютий"
msgid "Fields on this page are now uneditable, you can configure"
msgstr "ÐŸÐ¾Ð»Ñ Ð½Ð° цій Ñторінці зараз недоÑтупні Ð´Ð»Ñ Ñ€ÐµÐ´Ð°Ð³ÑƒÐ²Ð°Ð½Ð½Ñ, ви можете налаштувати"
-msgid "File name"
-msgstr "Ім'Ñ Ñ„Ð°Ð¹Ð»Ñƒ"
-
msgid "Files"
msgstr "Файли"
msgid "Files (%{human_size})"
msgstr "Файли (%{human_size})"
-msgid "Fill in the fields below, turn on <strong>%{enable_label}</strong>, and press <strong>%{save_changes}</strong>"
-msgstr ""
-
msgid "Filter by commit message"
msgstr "Фільтрувати за коміт-повідомленнÑм"
@@ -1978,10 +2342,13 @@ msgstr "Перший"
msgid "FirstPushedBy|pushed by"
msgstr "відправлено"
-msgid "Font Color"
+msgid "For internal projects, any logged in user can view pipelines and access job details (output logs and artifacts)"
msgstr ""
-msgid "Footer message"
+msgid "For private projects, any member (guest or higher) can view pipelines and access job details (output logs and artifacts)"
+msgstr ""
+
+msgid "For public projects, anyone can view pipelines and access job details (output logs and artifacts)"
msgstr ""
msgid "Fork"
@@ -2003,6 +2370,9 @@ msgstr "ВідбуваєтьÑÑ ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ñ„Ð¾Ñ€ÐºÑƒ"
msgid "Format"
msgstr "Формат"
+msgid "Found errors in your .gitlab-ci.yml:"
+msgstr "Знайдено помилки у вашому .gitlab-ci.yml:"
+
msgid "From %{provider_title}"
msgstr "З %{provider_title}"
@@ -2018,167 +2388,14 @@ msgstr "Із Ñторінки деталей Kubernetes-клаÑтера, вÑÑ‚
msgid "GPG Keys"
msgstr "GPG ключі"
-msgid "Generate a default set of labels"
-msgstr "Створити Ñтандартний набір міткок"
-
-msgid "Geo Nodes"
-msgstr "Гео-Вузли"
+msgid "General"
+msgstr "Загальні"
-msgid "Geo allows you to replicate your GitLab instance to other geographical locations."
+msgid "General pipelines"
msgstr ""
-msgid "GeoNodeSyncStatus|Node is failing or broken."
-msgstr "Вузол не працює або зламаний."
-
-msgid "GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage."
-msgstr "Вузол працює повільно, перевантажений або тільки що відновивÑÑ Ð¿Ñ–ÑÐ»Ñ Ð·Ð±Ð¾ÑŽ."
-
-msgid "GeoNodes|Checksummed"
-msgstr "Із контрольною Ñумою"
-
-msgid "GeoNodes|Database replication lag:"
-msgstr "Затримка реплікації бази даних:"
-
-msgid "GeoNodes|Disabling a node stops the sync process. Are you sure?"
-msgstr "Ð’Ñ–Ð´ÐºÐ»ÑŽÑ‡ÐµÐ½Ð½Ñ Ð²ÑƒÐ·Ð»Ð° зупинÑÑ” Ð¿Ñ€Ð¾Ñ†ÐµÑ Ñинхронізації. Ви впевнені?"
-
-msgid "GeoNodes|Does not match the primary storage configuration"
-msgstr "Ðе відповідає оÑновній конфігурації Ñховища"
-
-msgid "GeoNodes|Failed"
-msgstr "Ðевдало"
-
-msgid "GeoNodes|Full"
-msgstr "Повний"
-
-msgid "GeoNodes|GitLab version does not match the primary node version"
-msgstr "ВерÑÑ–Ñ GitLab не відповідає верÑÑ–Ñ— оÑновного вузла"
-
-msgid "GeoNodes|GitLab version:"
-msgstr "ВерÑÑ–Ñ GitLab:"
-
-msgid "GeoNodes|Health status:"
-msgstr "Стан працездатноÑті:"
-
-msgid "GeoNodes|Last event ID processed by cursor:"
-msgstr "Ідентифікатор оÑтанньої події, обробленої курÑором:"
-
-msgid "GeoNodes|Last event ID seen from primary:"
-msgstr "Ідентифікатор оÑтанньої події видимої із оÑновного:"
-
-msgid "GeoNodes|Loading nodes"
-msgstr "Ð—Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ Ð²ÑƒÐ·Ð»Ñ–Ð²"
-
-msgid "GeoNodes|Local Attachments:"
-msgstr "Локальні Ð²ÐºÐ»Ð°Ð´ÐµÐ½Ð½Ñ (attachments):"
-
-msgid "GeoNodes|Local LFS objects:"
-msgstr "Локальні LFS-об’єкти:"
-
-msgid "GeoNodes|Local job artifacts:"
-msgstr "Ðртефакти локальних завдань:"
-
-msgid "GeoNodes|New node"
-msgstr "Ðовий вузол"
-
-msgid "GeoNodes|Node Authentication was successfully repaired."
-msgstr "Ðутентифікацію вузла уÑпішно полагоджено."
-
-msgid "GeoNodes|Node was successfully removed."
-msgstr "Вузол уÑпішно видалено."
-
-msgid "GeoNodes|Not checksummed"
-msgstr "Без контрольної Ñуми"
-
-msgid "GeoNodes|Out of sync"
-msgstr "РозÑинхронізовані"
-
-msgid "GeoNodes|Removing a node stops the sync process. Are you sure?"
-msgstr "Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð²ÑƒÐ·Ð»Ð° зупинÑÑ” Ð¿Ñ€Ð¾Ñ†ÐµÑ Ñинхронізації. Ви впевнені?"
-
-msgid "GeoNodes|Replication slot WAL:"
-msgstr "Слот реплікації WAL:"
-
-msgid "GeoNodes|Replication slots:"
-msgstr "Слоти реплікації:"
-
-msgid "GeoNodes|Repositories checksummed:"
-msgstr "Репозиторії із контрольними Ñумами:"
-
-msgid "GeoNodes|Repositories:"
-msgstr "Репозиторії:"
-
-msgid "GeoNodes|Repository checksums verified:"
-msgstr "Репозиторії із підтвердженою контрольною Ñумою:"
-
-msgid "GeoNodes|Selective"
-msgstr "Вибіркові"
-
-msgid "GeoNodes|Something went wrong while changing node status"
-msgstr "Проблема при зміні ÑтатуÑа вузла"
-
-msgid "GeoNodes|Something went wrong while removing node"
-msgstr "Проблема при видаленні вузла"
-
-msgid "GeoNodes|Something went wrong while repairing node"
-msgstr "Проблема при полагодженні вузла"
-
-msgid "GeoNodes|Storage config:"
-msgstr "ÐšÐ¾Ð½Ñ„Ñ–Ð³ÑƒÑ€Ð°Ñ†Ñ–Ñ Ñховища:"
-
-msgid "GeoNodes|Sync settings:"
-msgstr "ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ñинхронізації:"
-
-msgid "GeoNodes|Synced"
-msgstr "Синхронізовано"
-
-msgid "GeoNodes|Unused slots"
-msgstr "ÐевикориÑтані Ñлоти"
-
-msgid "GeoNodes|Unverified"
-msgstr "Ðепідтверджені"
-
-msgid "GeoNodes|Used slots"
-msgstr "ВикориÑтані Ñлоти"
-
-msgid "GeoNodes|Verified"
-msgstr "Підтверджені"
-
-msgid "GeoNodes|Wiki checksums verified:"
-msgstr "Wiki репозиторії із підтвердженою контрольною Ñумою:"
-
-msgid "GeoNodes|Wikis checksummed:"
-msgstr "Wiki із контрольною Ñумою:"
-
-msgid "GeoNodes|Wikis:"
-msgstr "Wiki:"
-
-msgid "GeoNodes|You have configured Geo nodes using an insecure HTTP connection. We recommend the use of HTTPS."
-msgstr "Ви налаштували Geo-вузли через незахищене HTTP-з’єднаннÑ. Ми рекомендуємо викориÑтовувати HTTPS."
-
-msgid "Geo|All projects"
-msgstr "Ð’ÑÑ– проекти"
-
-msgid "Geo|File sync capacity"
-msgstr "ПропуÑкна здатніÑть Ñинхронізації файлів"
-
-msgid "Geo|Groups to synchronize"
-msgstr "Групи Ð´Ð»Ñ Ñинхронізації"
-
-msgid "Geo|Projects in certain groups"
-msgstr "Проекти в певних групах"
-
-msgid "Geo|Projects in certain storage shards"
-msgstr "Проекти в певних Ñегментах Ñховищ"
-
-msgid "Geo|Repository sync capacity"
-msgstr "ПропуÑкна здатніÑть Ñинхронізації репозиторіїв"
-
-msgid "Geo|Select groups to replicate."
-msgstr "Виберіть групи Ð´Ð»Ñ Ñ€ÐµÐ¿Ð»Ñ–ÐºÐ°Ñ†Ñ–Ñ—."
-
-msgid "Geo|Shards to synchronize"
-msgstr "Сегменти Ð´Ð»Ñ Ñинхронізації"
+msgid "Generate a default set of labels"
+msgstr "Створити Ñтандартний набір міток"
msgid "Git repository URL"
msgstr "URL Git-репозиторіÑ"
@@ -2189,6 +2406,9 @@ msgstr "Git-редакціÑ"
msgid "Git storage health information has been reset"
msgstr "Ð†Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ñ–Ñ Ð¿Ñ€Ð¾ ÑÑ‚Ð°Ñ‚ÑƒÑ Ð·Ð±ÐµÑ€Ñ–Ð³Ð°Ð½Ð½Ñ Git була Ñкинута"
+msgid "Git strategy for pipelines"
+msgstr ""
+
msgid "Git version"
msgstr "Git-верÑÑ–Ñ"
@@ -2196,23 +2416,26 @@ msgid "GitHub import"
msgstr "GitHub-імпорт"
msgid "GitLab CI Linter has been moved"
-msgstr ""
+msgstr "GitLab CI Linter був перенеÑений"
-msgid "GitLab Geo"
-msgstr ""
+msgid "GitLab Group Runners can execute code for all the projects in this group."
+msgstr "Групові Runner'и Gitlab можуть виконувати код Ð´Ð»Ñ ÑƒÑÑ–Ñ… проектів у цій групі."
msgid "GitLab Runner section"
msgstr "Розділ GitLab Runner"
-msgid "GitLab single sign on URL"
-msgstr ""
-
msgid "Gitaly"
-msgstr ""
+msgstr "Gitaly"
msgid "Gitaly Servers"
msgstr "Сервери Gitaly"
+msgid "Gitaly|Address"
+msgstr "ÐдреÑа"
+
+msgid "Go Back"
+msgstr "ПовернутиÑÑ"
+
msgid "Go back"
msgstr "ПовернутиÑÑ"
@@ -2228,23 +2451,20 @@ msgstr "ÐÑƒÑ‚ÐµÐ½Ñ‚Ð¸Ñ„Ñ–ÐºÐ°Ñ†Ñ–Ñ Google не %{link_to_documentation}. ПоÐ
msgid "Got it!"
msgstr "Зрозуміло!"
-msgid "GroupRoadmap|Epics let you manage your portfolio of projects more efficiently and with less effort"
-msgstr "Епіки дозволють вам керувати портфоліо ваших проектів більш ефективно Ñ– з меншими зуÑиллÑми"
-
-msgid "GroupRoadmap|From %{dateWord}"
-msgstr "Від %{dateWord}"
+msgid "Graph"
+msgstr "Графік"
-msgid "GroupRoadmap|Loading roadmap"
-msgstr "Ð—Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ Ð¿Ð»Ð°Ð½Ñƒ-графіку"
+msgid "Group CI/CD settings"
+msgstr "ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ CI/CD групи"
-msgid "GroupRoadmap|Something went wrong while fetching epics"
-msgstr "Проблема при завантаженні епіків"
+msgid "Group ID"
+msgstr "Ідентифікатор групи"
-msgid "GroupRoadmap|To view the roadmap, add a planned start or finish date to one of your epics in this group or its subgroups. Only epics in the past 3 months and the next 3 months are shown &ndash; from %{startDate} to %{endDate}."
-msgstr "Ð”Ð»Ñ Ð¿ÐµÑ€ÐµÐ³Ð»Ñду плану-графіку додайте заплановані дати початку та Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð½Ñ Ð´Ð¾ одного з ваших епіків у цій групі або Ñ—Ñ— підгрупах. ВідображаютьÑÑ Ð»Ð¸ÑˆÐµ епіки за попередні та наÑтупні 3 міÑÑці: від %{startDate} до %{endDate}."
+msgid "Group Runners"
+msgstr "Групові Runner'и"
-msgid "GroupRoadmap|Until %{dateWord}"
-msgstr "До %{dateWord}"
+msgid "Group maintainers can register group runners in the %{link}"
+msgstr "Керівники групи можуть зареєÑтрувати групові runner'и через %{link}"
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
msgstr "Заборонити Ñпільний доÑтуп до проекту в рамках %{group} з іншими групами"
@@ -2270,6 +2490,9 @@ msgstr "не може бути ÑкаÑовано поки \"БлокуваннÑ
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr "Видалити Ð±Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ñпільного доÑтупу з іншими групами з %{ancestor_group_name}"
+msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
+msgstr ""
+
msgid "GroupsEmptyState|A group is a collection of several projects."
msgstr "Група — набір із декількох проектів."
@@ -2309,12 +2532,6 @@ msgstr "Ðа жаль жодна группа не задовольнÑÑ” пар
msgid "GroupsTree|Sorry, no groups or projects matched your search"
msgstr "Ðа жаль жодна группа чи проект не задовольнÑÑ” параметрам вашого запиту"
-msgid "Have your users email"
-msgstr "Електронна пошта Ð´Ð»Ñ Ð·Ð²ÐµÑ€Ñ‚Ð°Ð½ÑŒ кориÑтувачів"
-
-msgid "Header message"
-msgstr ""
-
msgid "Health Check"
msgstr "Перевірка ПрацездатноÑті"
@@ -2334,13 +2551,13 @@ msgid "HealthCheck|Unhealthy"
msgstr "Ðездоровий"
msgid "Help"
-msgstr ""
+msgstr "Допомога"
msgid "Help page"
-msgstr ""
+msgstr "Сторінка довідки"
msgid "Help page text and support page url."
-msgstr ""
+msgstr "ТекÑÑ‚ Ñторінки довідки та url-адреÑа Ñторінки підтримки."
msgid "Hide value"
msgid_plural "Hide values"
@@ -2349,20 +2566,50 @@ msgstr[1] "Сховати значеннÑ"
msgstr[2] "Сховати значень"
msgstr[3] "Сховати значень"
+msgid "Hide whitespace changes"
+msgstr ""
+
msgid "History"
msgstr "ІÑторіÑ"
msgid "Housekeeping successfully started"
msgstr "ÐžÑ‡Ð¸Ñ‰ÐµÐ½Ð½Ñ ÑƒÑпішно розпочато"
-msgid "Identity provider single sign on URL"
+msgid "I accept the %{terms_link}"
+msgstr "Я приймаю %{terms_link}"
+
+msgid "I accept the|Terms of Service and Privacy Policy"
+msgstr "Я погоджуюÑÑŒ з Правилами кориÑÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ ÑервіÑом Ñ– політикою конфіденційноÑті"
+
+msgid "ID"
+msgstr "ID"
+
+msgid "IDE|Commit"
+msgstr "Коміт"
+
+msgid "IDE|Edit"
+msgstr "Редагувати"
+
+msgid "IDE|Go back"
+msgstr "ПовернутиÑÑ Ð½Ð°Ð·Ð°Ð´"
+
+msgid "IDE|Open in file view"
+msgstr "Відкрити Ñк файл"
+
+msgid "IDE|Review"
+msgstr "ОглÑд"
+
+msgid "Identifier"
+msgstr "Ідентифікатор"
+
+msgid "Identities"
msgstr ""
-msgid "If enabled, access to projects will be validated on an external service using their classification label."
+msgid "If disabled, the access level will depend on the user's permissions in the project."
msgstr ""
-msgid "If using GitHub, you’ll see pipeline statuses on GitHub for your commits and pull requests. %{more_info_link}"
-msgstr "При викориÑтанні GitHub, ви побачите ÑтатуÑи конвеєрів Ð´Ð»Ñ ÐºÐ¾Ð¼Ñ–Ñ‚Ñ–Ð² Ñ– запитів на злиттÑ. %{more_info_link}"
+msgid "If enabled"
+msgstr ""
msgid "If you already have files you can push them using the %{link_to_cli} below."
msgstr "Якщо у Ð²Ð°Ñ ÑƒÐ¶Ðµ Ñ” файли, ви можете відправити Ñ—Ñ… за допомогою %{link_to_cli} нижче."
@@ -2370,6 +2617,15 @@ msgstr "Якщо у Ð²Ð°Ñ ÑƒÐ¶Ðµ Ñ” файли, ви можете відпраÐ
msgid "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>."
msgstr "Якщо ваш HTTP-репозиторій не Ñ” публічним, додайте дані Ð´Ð»Ñ Ð°ÑƒÑ‚ÐµÐ½Ñ‚Ð¸Ñ„Ñ–ÐºÐ°Ñ†Ñ–Ñ— до URL: <code>https://username:password@gitlab.company.com/group/project.git</code>."
+msgid "ImageDiffViewer|2-up"
+msgstr "2 поруч"
+
+msgid "ImageDiffViewer|Onion skin"
+msgstr "Ðакладені (прозоріÑть)"
+
+msgid "ImageDiffViewer|Swipe"
+msgstr "Ðакладені (проведеннÑ)"
+
msgid "Import"
msgstr "Імпорт"
@@ -2385,17 +2641,11 @@ msgstr "Імпорт репозиторіїв з GitHub"
msgid "Import repository"
msgstr "Імпорт репозиторію"
-msgid "ImportButtons|Connect repositories from"
-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 "Include a Terms of Service agreement and Privacy Policy that all users must accept."
+msgstr "Включити угоду про Ð½Ð°Ð´Ð°Ð½Ð½Ñ Ð¿Ð¾Ñлуг та правила конфіденційноÑті, Ñкі повинні прийнÑти вÑÑ– кориÑтувачі."
-msgid "Improve search with Advanced Global Search and GitLab Enterprise Edition."
-msgstr "Покращити пошук за допомогою розширеного глобального пошук в верÑÑ–Ñ— GitLab Enterprise Edition."
+msgid "Inline"
+msgstr ""
msgid "Install Runner on Kubernetes"
msgstr "Ð’Ñтановити Runner на Kubernetes"
@@ -2403,19 +2653,15 @@ msgstr "Ð’Ñтановити Runner на Kubernetes"
msgid "Install a Runner compatible with GitLab CI"
msgstr "Ð’Ñтановіть Runner, ÑуміÑний з GitLab CI"
-msgid "Instance"
-msgid_plural "Instances"
-msgstr[0] "ІнÑтанÑ"
-msgstr[1] "IнÑтанÑи"
-msgstr[2] "ІнÑтанÑів"
-msgstr[3] "ІнÑтанÑів"
-
msgid "Instance does not support multiple Kubernetes clusters"
msgstr "Цей інÑÑ‚Ð°Ð½Ñ Ð½Ðµ підтримує декілька Kubernetes-клаÑтерів"
msgid "Integrations"
msgstr "Інтеграції"
+msgid "Integrations Settings"
+msgstr ""
+
msgid "Interested parties can even contribute by pushing commits if they want to."
msgstr "Зацікавлені Ñторони за бажаннÑм можуть навіть робити внеÑки шлÑхом Ð²Ñ–Ð´Ð¿Ñ€Ð°Ð²Ð»ÐµÐ½Ð½Ñ ÐºÐ¾Ð¼Ñ–Ñ‚Ñ–Ð²."
@@ -2431,8 +2677,8 @@ msgstr "Шаблон інтервалу"
msgid "Introducing Cycle Analytics"
msgstr "ПредÑтавлÑємо аналітику циклу"
-msgid "Issue board focus mode"
-msgstr "Режим фокуÑÑƒÐ²Ð°Ð½Ð½Ñ Ð´Ð»Ñ Ð´Ð¾ÑˆÐºÐ¸ обговорень"
+msgid "Issue Board"
+msgstr ""
msgid "Issue events"
msgstr "Проблеми"
@@ -2440,9 +2686,6 @@ msgstr "Проблеми"
msgid "IssueBoards|Board"
msgstr "Дошка"
-msgid "IssueBoards|Boards"
-msgstr "Дошки"
-
msgid "Issues"
msgstr "Проблеми"
@@ -2455,6 +2698,12 @@ msgstr "Ñіч."
msgid "January"
msgstr "Ñічень"
+msgid "Job"
+msgstr ""
+
+msgid "Job has been erased"
+msgstr "Ð—Ð°Ð²Ð´Ð°Ð½Ð½Ñ Ð±ÑƒÐ»Ð¾ Ñтерте"
+
msgid "Jobs"
msgstr "ЗавданнÑ"
@@ -2471,7 +2720,7 @@ msgid "June"
msgstr "червень"
msgid "Koding"
-msgstr ""
+msgstr "Koding"
msgid "Kubernetes"
msgstr "Kubernetes"
@@ -2497,6 +2746,9 @@ msgstr "Kubernetes налаштовано"
msgid "Kubernetes service integration has been deprecated. %{deprecated_message_content} your Kubernetes clusters using the new <a href=\"%{url}\"/>Kubernetes Clusters</a> page"
msgstr "СпоÑіб Інтеграції Kubernetes Ñк ÑервіÑа заÑтарів. %{deprecated_message_content} ваші Kubernetes-клаÑтери за допомогою нової Ñторінки <a href=\"%{url}\"/>КлаÑтери Kubernetes</a>"
+msgid "LFS"
+msgstr "LFS"
+
msgid "LFSStatus|Disabled"
msgstr "Вимкнено"
@@ -2506,6 +2758,9 @@ msgstr "Увімкнено"
msgid "Label"
msgstr "Мітка"
+msgid "Label actions dropdown"
+msgstr "Випадаючий ÑпиÑок дій із мітками"
+
msgid "LabelSelect|%{firstLabelName} +%{remainingLabelCount} more"
msgstr "%{firstLabelName} + %{remainingLabelCount} ще"
@@ -2521,11 +2776,14 @@ msgstr "Мітки можуть бути заÑтоÑовані до %{features}
msgid "Labels can be applied to issues and merge requests to categorize them."
msgstr "Мітки можуть бути заÑтоÑовані до проблем та запитів на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð´Ð»Ñ Ñ—Ñ… категоризації."
+msgid "Labels can be applied to issues and merge requests."
+msgstr "Мітки можуть бути заÑтоÑовані до проблем та запитів на злиттÑ."
+
msgid "Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>"
-msgstr ""
+msgstr "<span>ПеренеÑти мітку</span> %{labelTitle} <span>на рівень групи?</span>"
msgid "Labels|Promote Label"
-msgstr "Підвищити мітку"
+msgstr "ПеренеÑти мітку"
msgid "Last %d day"
msgid_plural "Last %d days"
@@ -2558,6 +2816,9 @@ msgstr "Ви відправили зміни до"
msgid "LastPushEvent|at"
msgstr "в"
+msgid "Latest changes"
+msgstr "ОÑтанні зміни"
+
msgid "Learn more"
msgstr "ДізнатиÑÑ Ð±Ñ–Ð»ÑŒÑˆÐµ"
@@ -2582,9 +2843,6 @@ msgstr "Залишити групу"
msgid "Leave project"
msgstr "Залишити проект"
-msgid "License"
-msgstr "ЛіцензіÑ"
-
msgid "List"
msgstr "СпиÑок"
@@ -2594,6 +2852,9 @@ msgstr "СпиÑок ваших репозиторіїв GitHub"
msgid "Loading the GitLab IDE..."
msgstr "Ð—Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ IDE GitLab..."
+msgid "Loading..."
+msgstr "ЗавантаженнÑ..."
+
msgid "Lock"
msgstr "БлокуваннÑ"
@@ -2603,23 +2864,20 @@ msgstr "Заблокувати %{issuableDisplayName}"
msgid "Lock not found"
msgstr "Ð‘Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ð½Ðµ знайдено"
+msgid "Lock to current projects"
+msgstr "Закріпити за поточними проектами"
+
msgid "Locked"
msgstr "Заблоковано"
-msgid "Locked Files"
-msgstr "Заблоковані файли"
-
-msgid "Locks give the ability to lock specific file or folder."
-msgstr "Ð‘Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ð¼Ð¾Ð¶Ðµ бути заÑтоÑоване до конкретного файлу або директорії."
+msgid "Locked to current projects"
+msgstr "Закріплено за поточними проектами"
msgid "Login"
msgstr "Вхід"
-msgid "Make everyone on your team more productive regardless of their location. GitLab Geo creates read-only mirrors of your GitLab instance so you can reduce the time it takes to clone and fetch large repos."
-msgstr "Зробіть кожного учаÑника команди більш продуктивним незалежно від його міÑцезнаходженнÑ. GitLab Geo Ñтворює копії \"тільки Ð´Ð»Ñ Ñ‡Ð¸Ñ‚Ð°Ð½Ð½Ñ\" вашого GitLab Ñервера, щоб Ñкоротити Ñ‡Ð°Ñ Ð´Ð»Ñ ÐºÐ»Ð¾Ð½ÑƒÐ²Ð°Ð½Ð½Ñ Ñ– Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ð½Ð½Ñ ÐºÐ¾Ð´Ñƒ з великих репозиторіїв."
-
msgid "Manage all notifications"
-msgstr ""
+msgstr "Керувати вÑіма ÑповіщеннÑми"
msgid "Manage group labels"
msgstr "ÐšÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ Ð¼Ñ–Ñ‚ÐºÐ°Ð¼Ð¸ групи"
@@ -2630,17 +2888,17 @@ msgstr "Керувати мітками"
msgid "Manage project labels"
msgstr "ÐšÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ Ð¼Ñ–Ñ‚ÐºÐ°Ð¼Ð¸ проекту"
-msgid "Manage your group’s membership while adding another level of security with SAML."
-msgstr ""
-
msgid "Mar"
msgstr "бер."
msgid "March"
msgstr "березень"
-msgid "Mark done"
-msgstr "Позначити виконаним"
+msgid "Mark todo as done"
+msgstr "Відмітити Ð·Ð°Ð²Ð´Ð°Ð½Ð½Ñ Ð²Ð¸ÐºÐ¾Ð½Ð°Ð½Ð¸Ð¼"
+
+msgid "Markdown enabled"
+msgstr "Markdown увімкнено"
msgid "Maximum git storage failures"
msgstr "МакÑимальна кількіÑть невдач в Ñховищі даних git"
@@ -2654,8 +2912,8 @@ msgstr "Медіана"
msgid "Members"
msgstr "КориÑтувачі"
-msgid "Members will be forwarded here when signing in to your group. Get this from your identity provider, where it can also be called \"SSO Service Location\", \"SAML Token Issuance Endpoint\", or \"SAML 2.0/W-Federation URL\"."
-msgstr ""
+msgid "Merge Request:"
+msgstr "Запит на злиттÑ:"
msgid "Merge Requests"
msgstr "Запити на злиттÑ"
@@ -2666,93 +2924,48 @@ msgstr "Події злиттÑ"
msgid "Merge request"
msgstr "Запит на злиттÑ"
+msgid "Merge requests"
+msgstr "Запити на злиттÑ"
+
msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
msgstr "Запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ â€” це ÑпоÑіб запропонувати Ñвої зміни до проекту Ñ– обговорити Ñ—Ñ… із іншими"
-msgid "Merged"
-msgstr "Злито"
-
-msgid "Messages"
-msgstr "ПовідомленнÑ"
-
-msgid "Metrics - Influx"
+msgid "MergeRequests|Resolve this discussion in a new issue"
msgstr ""
-msgid "Metrics - Prometheus"
+msgid "MergeRequests|Saving the comment failed"
msgstr ""
-msgid "Metrics|Business"
-msgstr "БізнеÑ"
-
-msgid "Metrics|Create metric"
-msgstr "Створити метрику"
-
-msgid "Metrics|Edit metric"
-msgstr "Редагувати метрику"
-
-msgid "Metrics|For grouping similar metrics"
-msgstr "Ð”Ð»Ñ Ð³Ñ€ÑƒÐ¿ÑƒÐ²Ð°Ð½Ð½Ñ Ð¿Ð¾Ð´Ñ–Ð±Ð½Ð¸Ñ… метрик"
-
-msgid "Metrics|Label of the chart's vertical axis. Usually the type of the unit being charted. The horizontal axis (X-axis) always represents time."
-msgstr "Ðазва вертикальної оÑÑ– графіка. Зазвичай це — одиниці вимірюваннÑ. Горизонтальна віÑÑŒ (віÑÑŒ X) завжди відображає чаÑ."
-
-msgid "Metrics|Legend label (optional)"
-msgstr "Заголовок легенди (необов’Ñзковий)"
-
-msgid "Metrics|Must be a valid PromQL query."
-msgstr "Має бути коректним запитом PromQL."
-
-msgid "Metrics|Name"
-msgstr "Ім'Ñ"
-
-msgid "Metrics|New metric"
-msgstr "Ðова метрика"
-
-msgid "Metrics|Prometheus Query Documentation"
-msgstr "Ð”Ð¾ÐºÑƒÐ¼ÐµÐ½Ñ‚Ð°Ñ†Ñ–Ñ Ð¿Ð¾ запитам Prometheus"
-
-msgid "Metrics|Query"
-msgstr "Запит"
-
-msgid "Metrics|Response"
-msgstr "Відповідь"
-
-msgid "Metrics|System"
-msgstr "СиÑтема"
-
-msgid "Metrics|Type"
-msgstr "Тип"
-
-msgid "Metrics|Unit label"
-msgstr "Одиниці вимірюваннÑ"
-
-msgid "Metrics|Used as a title for the chart"
-msgstr "ВикориÑтовуєтьÑÑ Ñк заголовок графіка"
+msgid "MergeRequests|Toggle comments for this file"
+msgstr ""
-msgid "Metrics|Used if the query returns a single series. If it returns multiple series, their legend labels will be picked up from the response."
-msgstr "ВикориÑтовуєтьÑÑ, Ñкщо запит повертає єдину поÑлідовніÑть. Якщо ж він повертає декілька, Ñ—Ñ… назви берутьÑÑ Ñ–Ð· відповіді."
+msgid "MergeRequests|Updating discussions failed"
+msgstr ""
-msgid "Metrics|Y-axis label"
-msgstr "Ðазва оÑÑ– Y"
+msgid "MergeRequests|View file @ %{commitId}"
+msgstr ""
-msgid "Metrics|e.g. HTTP requests"
-msgstr "напр. HTTP-запити"
+msgid "MergeRequests|View replaced file @ %{commitId}"
+msgstr ""
-msgid "Metrics|e.g. Requests/second"
-msgstr "напр. запитів/Ñек"
+msgid "Merged"
+msgstr "Злито"
-msgid "Metrics|e.g. Throughput"
-msgstr "напр. пропуÑкна здатніÑть"
+msgid "Messages"
+msgstr "ПовідомленнÑ"
-msgid "Metrics|e.g. rate(http_requests_total[5m])"
-msgstr "напр. rate(http_requests_total[5m])"
+msgid "Metrics - Influx"
+msgstr "Метрики - Influx"
-msgid "Metrics|e.g. req/sec"
-msgstr "напр. зап/Ñек"
+msgid "Metrics - Prometheus"
+msgstr "Метрики - Prometheus"
msgid "Milestone"
msgstr "Етап"
+msgid "Milestones"
+msgstr "Етапи"
+
msgid "Milestones|Delete milestone"
msgstr "Видалити етап"
@@ -2766,13 +2979,10 @@ msgid "Milestones|Milestone %{milestoneTitle} was not found"
msgstr "Етап %{milestoneTitle} не знайдено"
msgid "Milestones|Promote %{milestoneTitle} to group milestone?"
-msgstr "Підвищити %{milestoneTitle} до групового етапу?"
+msgstr "ПеренеÑти %{milestoneTitle} на рівень групи?"
msgid "Milestones|Promote Milestone"
-msgstr "Підвищити Етап"
-
-msgid "Milestones|This action cannot be reversed."
-msgstr ""
+msgstr "ПеренеÑти етап"
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "не додаÑте SSH ключ"
@@ -2786,8 +2996,8 @@ msgstr "Закрити"
msgid "Monitoring"
msgstr "Моніторинг"
-msgid "More info"
-msgstr "Детальніше"
+msgid "More actions"
+msgstr "Додаткові дії"
msgid "More information"
msgstr "Детальніше"
@@ -2801,12 +3011,30 @@ msgstr "ПереміÑтити"
msgid "Move issue"
msgstr "ПереміÑтити проблему"
-msgid "Multiple issue boards"
-msgstr "Кілька дошок обговореннÑ"
+msgid "Name"
+msgstr "Ім’Ñ"
msgid "Name new label"
msgstr "Ðазвіть нову мітку"
+msgid "Name your individual key via a title"
+msgstr ""
+
+msgid "Nav|Help"
+msgstr "Допомога"
+
+msgid "Nav|Home"
+msgstr "Додому"
+
+msgid "Nav|Sign In / Register"
+msgstr "Увійти / зареєÑтруватиÑÑ"
+
+msgid "Nav|Sign out and sign in with a different account"
+msgstr "Вийти із зайти під інший обліковим запиÑом"
+
+msgid "New Identity"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "Ðова проблема"
@@ -2820,6 +3048,9 @@ msgstr "Ðовий Kubernetes-клаÑтер"
msgid "New Kubernetes cluster"
msgstr "Ðовий Kubernetes-клаÑтер"
+msgid "New Label"
+msgstr ""
+
msgid "New Pipeline Schedule"
msgstr "Ðовий розклад Конвеєра"
@@ -2832,15 +3063,15 @@ msgstr "Ðова гілка недоÑтупна"
msgid "New directory"
msgstr "Ðовий каталог"
-msgid "New epic"
-msgstr "Ðовий епік"
-
msgid "New file"
msgstr "Ðовий файл"
msgid "New group"
msgstr "Ðова група"
+msgid "New identity"
+msgstr ""
+
msgid "New issue"
msgstr "Ðова проблема"
@@ -2850,6 +3081,9 @@ msgstr "Ðова мітка"
msgid "New merge request"
msgstr "Ðовий запит на злиттÑ"
+msgid "New pipelines will cancel older, pending pipelines on the same branch"
+msgstr ""
+
msgid "New project"
msgstr "Ðовий проект"
@@ -2865,8 +3099,8 @@ msgstr "Ðова підгрупа"
msgid "New tag"
msgstr "Ðовий тег"
-msgid "No Label"
-msgstr "Без Мітки"
+msgid "No"
+msgstr "ÐÑ–"
msgid "No assignee"
msgstr "Ðемає виконавцÑ"
@@ -2886,14 +3120,23 @@ msgstr "Ðемає запланованого або витраченого ча
msgid "No file chosen"
msgstr "Файл не вибрано"
-msgid "No labels created yet."
-msgstr "Мітки ще не Ñтворені."
+msgid "No files found"
+msgstr ""
+
+msgid "No files found."
+msgstr "Ðе знайдено жодного файлу."
+
+msgid "No merge requests found"
+msgstr "Ðе знайдено жодного запиту на злиттÑ"
+
+msgid "No messages were logged"
+msgstr "Ðемає повідомлень у журналі"
msgid "No repository"
msgstr "Ðемає репозиторію"
msgid "No schedules"
-msgstr "немає Розкладів"
+msgstr "Ðемає розкладів"
msgid "None"
msgstr "Ðемає"
@@ -2919,15 +3162,9 @@ msgstr "ÐедоÑтатньо даних"
msgid "Note that the master branch is automatically protected. %{link_to_protected_branches}"
msgstr "Майте на увазі, що гілка master захищена автоматично. %{link_to_protected_branches}"
-msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow connecting repositories without generating a Personal Access Token."
-msgstr "Примітка: Ñк адмініÑтратор ви можете налаштувати %{github_integration_link}, що дозволить входити через GitHub Ñ– підключати репозиторії без ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ð¾ÑобиÑтого токену доÑтупу."
-
msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
msgstr "Примітка: Ñк адмініÑтратор ви можете налаштувати %{github_integration_link}, що дозволить входити через GitHub Ñ– імпортувати репозиторії без ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ð¾ÑобиÑтого токену доÑтупу."
-msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow connecting repositories without generating a Personal Access Token."
-msgstr "Примітка: звернітьÑÑ Ð´Ð¾ вашого адмініÑтратора GitLab, щоб налаштувати %{github_integration_link}, Ñкий дозволить входити за допомогою GitHub Ñ– приєднувати репозиторії без генерації перÑонального токена доÑтупу."
-
msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
msgstr "Примітка: звернітьÑÑ Ð´Ð¾ вашого адмініÑтратора GitLab, щоб налаштувати %{github_integration_link}, Ñкий дозволить входити за допомогою GitHub та імпортувати репозиторії без генерації перÑонального токена доÑтупу."
@@ -3003,9 +3240,6 @@ msgstr "лиÑтопад"
msgid "Number of access attempts"
msgstr "КількіÑть Ñпроб доÑтупу"
-msgid "OK"
-msgstr "OK"
-
msgid "Oct"
msgstr "жовт."
@@ -3015,20 +3249,17 @@ msgstr "жовтень"
msgid "OfSearchInADropdown|Filter"
msgstr "Фільтр"
-msgid "Once imported, repositories can be mirrored over SSH. Read more %{ssh_link}"
-msgstr "ПіÑÐ»Ñ Ñ–Ð¼Ð¿Ð¾Ñ€Ñ‚Ñƒ репозиторії можуть бути віддзеркалені через SSH. ДізнайтеÑÑ Ð±Ñ–Ð»ÑŒÑˆÐµ %{ssh_link}"
-
msgid "Online IDE integration settings."
+msgstr "ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ñ–Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ñ–Ñ— із онлайн IDE."
+
+msgid "Only comments from the following commit are shown below"
msgstr ""
msgid "Only project members can comment."
msgstr "Тільки учаÑники проекту можуть залишати коментарі."
-msgid "Open"
-msgstr "Відкрити"
-
-msgid "Opened"
-msgstr "Відкрито"
+msgid "Open in Xcode"
+msgstr "Відкрити в Xcode"
msgid "OpenedNDaysAgo|Opened"
msgstr "Відкрито"
@@ -3036,14 +3267,23 @@ msgstr "Відкрито"
msgid "Opens in a new window"
msgstr "ВідкриваєтьÑÑ Ñƒ новому вікні"
+msgid "Operations"
+msgstr "Операції"
+
msgid "Options"
msgstr "Параметри"
+msgid "Or you can choose one of the suggested colors below"
+msgstr ""
+
+msgid "Other Labels"
+msgstr "Інші мітки"
+
msgid "Otherwise it is recommended you start with one of the options below."
msgstr "Ð’ іншому разі рекомендуєтьÑÑ Ð¿Ð¾Ñ‡Ð°Ñ‚Ð¸ з одного з наведених нижче варіантів."
msgid "Outbound requests"
-msgstr ""
+msgstr "Вихідні запити"
msgid "Overview"
msgstr "ОглÑд"
@@ -3052,7 +3292,7 @@ msgid "Owner"
msgstr "ВлаÑник"
msgid "Pages"
-msgstr ""
+msgstr "Сторінки"
msgid "Pagination|Last »"
msgstr "ОÑÑ‚Ð°Ð½Ð½Ñ Â»"
@@ -3067,17 +3307,32 @@ msgid "Pagination|« First"
msgstr "« Перша"
msgid "Part of merge request changes"
-msgstr ""
+msgstr "ЧаÑтина змін у запиті на злиттÑ"
msgid "Password"
msgstr "Пароль"
+msgid "Paste your public SSH key, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'. Don't use your private SSH key."
+msgstr ""
+
+msgid "Pause"
+msgstr "Призупинити"
+
msgid "Pending"
msgstr "В очікуванні"
-msgid "Performance optimization"
+msgid "Per job. If a job passes this threshold, it will be marked as failed"
msgstr ""
+msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgstr "Виконуйте такі розширені операції, Ñк зміна шлÑху, перенеÑÐµÐ½Ð½Ñ Ñ‡Ð¸ Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð³Ñ€ÑƒÐ¿Ð¸."
+
+msgid "Performance optimization"
+msgstr "ÐžÐ¿Ñ‚Ð¸Ð¼Ñ–Ð·Ð°Ñ†Ñ–Ñ Ð¿Ñ€Ð¾Ð´ÑƒÐºÑ‚Ð¸Ð²Ð½Ð¾Ñті"
+
+msgid "Permissions"
+msgstr "Права доÑтупу"
+
msgid "Personal Access Token"
msgstr "Токену перÑонального доÑтупу"
@@ -3093,8 +3348,8 @@ msgstr "Розклад Конвеєра"
msgid "Pipeline Schedules"
msgstr "Розклади Конвеєрів"
-msgid "Pipeline quota"
-msgstr "Квота на конвеєри"
+msgid "Pipeline triggers"
+msgstr ""
msgid "PipelineCharts|Failed:"
msgstr "Ðевдалі:"
@@ -3192,11 +3447,23 @@ msgstr "Ð’ даний Ñ‡Ð°Ñ Ð½ÐµÐ¼Ð°Ñ” конвеєрів."
msgid "Pipelines|This project is not currently set up to run pipelines."
msgstr "Цей проект в даний Ñ‡Ð°Ñ Ð½Ðµ налаштований Ð´Ð»Ñ Ð·Ð°Ð¿ÑƒÑку конвеєрів."
-msgid "Pipeline|Retry pipeline"
-msgstr "ПерезапуÑтити конвеєр"
+msgid "Pipeline|Create for"
+msgstr "Створити длÑ"
+
+msgid "Pipeline|Create pipeline"
+msgstr "Створити конвеєр"
-msgid "Pipeline|Retry pipeline #%{pipelineId}?"
-msgstr "ПерезапуÑтити конвеєр #%{pipelineId}?"
+msgid "Pipeline|Existing branch name or tag"
+msgstr "ІÑнуюче Ñ–Ð¼â€™Ñ Ð³Ñ–Ð»ÐºÐ¸ або тег"
+
+msgid "Pipeline|Run Pipeline"
+msgstr "ЗапуÑтити Конвеєр"
+
+msgid "Pipeline|Search branches"
+msgstr "Пошук гілки"
+
+msgid "Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default."
+msgstr "Вкажіть Ð·Ð½Ð°Ñ‡ÐµÐ½Ð½Ñ Ð·Ð¼Ñ–Ð½Ð½Ð¸Ñ…, Ñкі будуть викориÑтані в цьому запуÑку. Інакше будуть викориÑтані значеннÑ, вказані в %{settings_link}."
msgid "Pipeline|Stop pipeline"
msgstr "Зупинити конвеєр"
@@ -3204,8 +3471,8 @@ msgstr "Зупинити конвеєр"
msgid "Pipeline|Stop pipeline #%{pipelineId}?"
msgstr "Зупинити конвеєр #%{pipelineId}?"
-msgid "Pipeline|You’re about to retry pipeline %{pipelineId}."
-msgstr "Зараз ви перезапуÑтите конвеєр %{pipelineId}."
+msgid "Pipeline|Variables"
+msgstr "Змінні"
msgid "Pipeline|You’re about to stop pipeline %{pipelineId}."
msgstr "Зараз ви зупинете конвеєр %{pipelineId}."
@@ -3222,20 +3489,26 @@ msgstr "зі Ñтадією"
msgid "Pipeline|with stages"
msgstr "зі ÑтадіÑми"
-msgid "PlantUML"
+msgid "Plain diff"
msgstr ""
+msgid "PlantUML"
+msgstr "PlantUML"
+
msgid "Play"
msgstr "Відтворити"
-msgid "Please <a href=%{link_to_billing} target=\"_blank\" rel=\"noopener noreferrer\">enable billing for one of your projects to be able to create a Kubernetes cluster</a>, then try again."
-msgstr "Будь лаÑка, <a href=%{link_to_billing} target=\"_blank\" rel=\"noopener noreferrer\">увімкніть білінга Ð´Ð»Ñ Ð¾Ð´Ð½Ð¾Ð³Ð¾ з ваших проектів, щоб мати можливіÑть Ñтворити Kubernetes-клаÑтер</a>, Ñ– повторіть Ñпробу."
+msgid "Please accept the Terms of Service before continuing."
+msgstr "Будь лаÑка, Ð´Ð»Ñ Ð¿Ñ€Ð¾Ð´Ð¾Ð²Ð¶ÐµÐ½Ð½Ñ Ð¿Ñ€Ð¸Ð¹Ð¼Ñ–Ñ‚ÑŒ умови Ð½Ð°Ð´Ð°Ð½Ð½Ñ Ð¿Ð¾Ñлуг."
+
+msgid "Please select at least one filter to see results"
+msgstr "Будь лаÑка виберіть хоча б один фільтр, щоб побачити результати"
msgid "Please solve the reCAPTCHA"
msgstr "Будь лаÑка, пройдіть reCAPTCHA"
-msgid "Please wait while we connect to your repository. Refresh at will."
-msgstr "Будь лаÑка, почекайте поки ми з’єднуємоÑÑ Ñ–Ð· вашим репозиторієм. Оновлюйте Ñторінку за бажаннÑм."
+msgid "Please try again"
+msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr "Будь лаÑка, почекайте поки ми імпортуємо ваш репозиторій. Оновлюйте Ñторінку за бажаннÑм."
@@ -3243,8 +3516,20 @@ msgstr "Будь лаÑка, почекайте поки ми імпортуєм
msgid "Preferences"
msgstr "ÐалаштуваннÑ"
-msgid "Primary"
-msgstr "Головний"
+msgid "Preferences|Navigation theme"
+msgstr "Тема навігації"
+
+msgid "Prioritize"
+msgstr "Пріоритизувати"
+
+msgid "Prioritize label"
+msgstr "Пріоритизувати мітку"
+
+msgid "Prioritized Labels"
+msgstr "Пріоритетні мітки"
+
+msgid "Prioritized label"
+msgstr "Пріоритетні мітки"
msgid "Private - Project access must be granted explicitly to each user."
msgstr "Приватний — доÑтуп до проекту повинен надаватиÑÑ ÐºÐ¾Ð¶Ð½Ð¾Ð¼Ñƒ кориÑтувачеві."
@@ -3261,6 +3546,12 @@ msgstr "Профіль"
msgid "Profiles|Account scheduled for removal."
msgstr "Обліковий Ð·Ð°Ð¿Ð¸Ñ Ð·Ð°Ð¿Ð»Ð°Ð½Ð¾Ð²Ð°Ð½Ð¸Ð¹ Ð´Ð»Ñ Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ."
+msgid "Profiles|Change username"
+msgstr "Змінити ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача"
+
+msgid "Profiles|Current path: %{path}"
+msgstr "Поточний шлÑÑ…: %{path}"
+
msgid "Profiles|Delete Account"
msgstr "Видалити обліковий запиÑ"
@@ -3279,9 +3570,21 @@ msgstr "Ðеправильний пароль"
msgid "Profiles|Invalid username"
msgstr "Ðеправильне ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача"
+msgid "Profiles|Path"
+msgstr "ШлÑÑ…"
+
msgid "Profiles|Type your %{confirmationValue} to confirm:"
msgstr "Введіть ваш %{confirmationValue} Ð´Ð»Ñ Ð¿Ñ–Ð´Ñ‚Ð²ÐµÑ€Ð´Ð¶ÐµÐ½Ð½Ñ:"
+msgid "Profiles|Update username"
+msgstr "Оновити ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача"
+
+msgid "Profiles|Username change failed - %{message}"
+msgstr "Помилка при збереженні імені кориÑтувача - %{message}"
+
+msgid "Profiles|Username successfully changed"
+msgstr "Ð†Ð¼â€™Ñ ÐºÐ¾Ñ€Ð¸Ñтувача уÑпішно збережено"
+
msgid "Profiles|You don't have access to delete this user."
msgstr "У Ð²Ð°Ñ Ð½ÐµÐ¼Ð°Ñ” доÑтупу Ð´Ð»Ñ Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ñ†ÑŒÐ¾Ð³Ð¾ кориÑтувача."
@@ -3295,11 +3598,17 @@ msgid "Profiles|your account"
msgstr "ваш обліковий запиÑ"
msgid "Profiling - Performance bar"
-msgstr ""
+msgstr "ÐŸÑ€Ð¾Ñ„Ñ–Ð»ÑŽÐ²Ð°Ð½Ð½Ñ - панель продуктивноÑті"
msgid "Programming languages used in this repository"
msgstr "Мови програмуваннÑ, що викориÑтовуєтьÑÑ Ð² цьому репозиторії"
+msgid "Progress"
+msgstr "ПрогреÑ"
+
+msgid "Project"
+msgstr "Проект"
+
msgid "Project '%{project_name}' is in the process of being deleted."
msgstr "Проект '%{project_name}' перебуває в процеÑÑ– видаленнÑ."
@@ -3312,6 +3621,9 @@ msgstr "Проект '%{project_name}' уÑпішно Ñтворений."
msgid "Project '%{project_name}' was successfully updated."
msgstr "Проект '%{project_name}' уÑпішно оновлено."
+msgid "Project Badges"
+msgstr "Значки проекту"
+
msgid "Project access must be granted explicitly to each user."
msgstr "ДоÑтуп до проекту повинен надаватиÑÑ ÐºÐ¾Ð¶Ð½Ð¾Ð¼Ñƒ кориÑтувачеві."
@@ -3339,30 +3651,6 @@ msgstr "Розпочато екÑпорт проекту. ПоÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð´Ð
msgid "ProjectActivityRSS|Subscribe"
msgstr "ПідпиÑатиÑÑ"
-msgid "ProjectCreationLevel|Allowed to create projects"
-msgstr "Дозволено Ñтворювати проекти"
-
-msgid "ProjectCreationLevel|Default project creation protection"
-msgstr ""
-
-msgid "ProjectCreationLevel|Developers + Masters"
-msgstr "Розробники + Керівники"
-
-msgid "ProjectCreationLevel|Masters"
-msgstr "Керівники"
-
-msgid "ProjectCreationLevel|No one"
-msgstr "Ðіхто"
-
-msgid "ProjectFeature|Disabled"
-msgstr "Вимкнено"
-
-msgid "ProjectFeature|Everyone with access"
-msgstr "Ð’ÑÑ– із доÑтупом"
-
-msgid "ProjectFeature|Only team members"
-msgstr "Тільки члени команди"
-
msgid "ProjectFileTree|Name"
msgstr "Ім'Ñ"
@@ -3372,27 +3660,6 @@ msgstr "Ðіколи"
msgid "ProjectLifecycle|Stage"
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 "Проекти"
@@ -3417,6 +3684,9 @@ msgstr "Ðа жаль, по вашоу запиту проектів не зна
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr "Ð¦Ñ Ñ„ÑƒÐ½ÐºÑ†Ñ–Ñ Ð¿Ð¾Ñ‚Ñ€ÐµÐ±ÑƒÑ” підтримки localStorage вашим браузером"
+msgid "PrometheusDashboard|Time"
+msgstr "ЧаÑ"
+
msgid "PrometheusService|%{exporters} with %{metrics} were found"
msgstr "було знайдено %{exporters} з %{metrics}"
@@ -3438,18 +3708,9 @@ msgstr "За замовчуваннÑм, Prometheus запуÑкаєтьÑÑ Ð·Ð
msgid "PrometheusService|Common metrics"
msgstr "Загальні метрики"
-msgid "PrometheusService|Common metrics are automatically monitored based on a library of metrics from popular exporters."
-msgstr "Загальні метрики автоматично збираютьÑÑ Ð½Ð° оÑнові набору метрик від популÑрних екÑпортерів."
-
-msgid "PrometheusService|Custom metrics"
-msgstr "ВлаÑні метрики"
-
msgid "PrometheusService|Finding and configuring metrics..."
msgstr "Пошук та Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð¼ÐµÑ‚Ñ€Ð¸Ðº..."
-msgid "PrometheusService|Finding custom metrics..."
-msgstr "Пошук влаÑних метрик..."
-
msgid "PrometheusService|Install Prometheus on clusters"
msgstr "Ð’Ñтановити Prometheus на клаÑтери"
@@ -3462,24 +3723,21 @@ msgstr "Ручні налаштуваннÑ"
msgid "PrometheusService|Metrics"
msgstr "Метрики"
+msgid "PrometheusService|Metrics are automatically configured and monitored based on a library of metrics from popular exporters."
+msgstr ""
+
msgid "PrometheusService|Missing environment variable"
msgstr "Пропущена змінна Ñередовища"
msgid "PrometheusService|More information"
msgstr "Додаткова інформаціÑ"
-msgid "PrometheusService|New metric"
-msgstr "Ðова метрика"
-
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr "Базова адреÑа Prometheus API, наприклад http://prometheus.example.com/"
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr "Prometheus автоматично налаштований на ваших клаÑтерах"
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
-msgstr "Ці метрики будуть збиратиÑÑ Ñ‚Ñ–Ð»ÑŒÐºÐ¸ піÑÐ»Ñ Ð¿ÐµÑ€ÑˆÐ¾Ð³Ð¾ Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ð² ÑкомуÑÑŒ Ñеридовищі"
-
msgid "PrometheusService|Time-series monitoring service"
msgstr "Ð¡ÐµÑ€Ð²Ñ–Ñ Ð¼Ð¾Ð½Ñ–Ñ‚Ð¾Ñ€Ð¸Ð½Ð³Ñƒ чаÑових Ñ€Ñдів"
@@ -3493,25 +3751,31 @@ msgid "PrometheusService|Waiting for your first deployment to an environment to
msgstr "ÐžÑ‡Ñ–ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ð¿ÐµÑ€ÑˆÐ¾Ð³Ð¾ Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ñƒ Ñеридовищі Ð´Ð»Ñ Ð·Ð±Ð¾Ñ€Ñƒ загальних метрик"
msgid "Promote"
-msgstr ""
+msgstr "ПеренеÑти"
-msgid "Promote to Group Label"
-msgstr "ПеренеÑти мітку на рівень групи"
+msgid "Promote these project milestones into a group milestone."
+msgstr "ПеренеÑти ці проектні етапи на рівень групи."
msgid "Promote to Group Milestone"
-msgstr ""
+msgstr "ПеренеÑти Етап на рівень групи"
+
+msgid "Promote to group label"
+msgstr "ПеренеÑти мітку на рівень групи"
msgid "Protip:"
msgstr "Підказка:"
+msgid "Provider"
+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 "Public pipelines"
+msgstr ""
msgid "Push events"
msgstr "Події Ð²Ñ–Ð´Ð¿Ñ€Ð°Ð²Ð»ÐµÐ½Ð½Ñ (push)"
@@ -3522,12 +3786,12 @@ msgstr "Виконати push проекту за допомогою команÐ
msgid "Push to create a project"
msgstr "ÐатиÑніть, щоб Ñтворити проект"
-msgid "PushRule|Committer restriction"
-msgstr "ÐžÐ±Ð¼ÐµÐ¶ÐµÐ½Ð½Ñ Ð´Ð»Ñ ÐºÐ¾Ð¼Ñ–Ñ‚Ñ‚ÐµÑ€Ð°"
-
msgid "Quick actions can be used in the issues description and comment boxes."
msgstr "Швидкі дії можна викориÑтовувати в опиÑах проблем Ñ– коментарÑÑ…."
+msgid "Re-deploy"
+msgstr ""
+
msgid "Read more"
msgstr "Докладніше"
@@ -3535,13 +3799,7 @@ msgid "Readme"
msgstr "ІнÑтрукціÑ"
msgid "Real-time features"
-msgstr ""
-
-msgid "RefSwitcher|Branches"
-msgstr "Гілки"
-
-msgid "RefSwitcher|Tags"
-msgstr "Теги"
+msgstr "Фунції реального чаÑу"
msgid "Reference:"
msgstr "ПоÑиланнÑ:"
@@ -3549,6 +3807,12 @@ msgstr "ПоÑиланнÑ:"
msgid "Register / Sign In"
msgstr "ЗареєÑтруватиÑÑ / Увійти"
+msgid "Register and see your runners for this group."
+msgstr "ЗареєÑтруйте Ñ– переглÑдайте ваші Runner’и Ð´Ð»Ñ Ñ†Ñ–Ñ”Ñ— групи."
+
+msgid "Register and see your runners for this project."
+msgstr ""
+
msgid "Registry"
msgstr "РеєÑтр"
@@ -3571,7 +3835,7 @@ msgid "Related Merged Requests"
msgstr "Пов'Ñзані виконані запити"
msgid "Related merge requests"
-msgstr ""
+msgstr "Пов'Ñзані запити на злиттÑ"
msgid "Remind later"
msgstr "Ðагадати пізніше"
@@ -3579,36 +3843,39 @@ msgstr "Ðагадати пізніше"
msgid "Remove"
msgstr "Видалити"
+msgid "Remove Runner"
+msgstr "Видалити Runner"
+
msgid "Remove avatar"
msgstr "Видалити аватар"
+msgid "Remove priority"
+msgstr "Видалити пріоритет"
+
msgid "Remove project"
msgstr "Видалити проект"
-msgid "Repair authentication"
-msgstr "Відновити аутентифікацію"
-
-msgid "Repo by URL"
-msgstr "Репозиторії по URL"
-
msgid "Repository"
msgstr "Репозиторій"
-msgid "Repository has no locks."
-msgstr "Репозиторій не має блокувань."
+msgid "Repository Settings"
+msgstr ""
msgid "Repository maintenance"
-msgstr ""
+msgstr "ОбÑÐ»ÑƒÐ³Ð¾Ð²ÑƒÐ²Ð°Ð½Ð½Ñ Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ñ–ÑŽ"
-msgid "Repository mirror settings"
-msgstr ""
+msgid "Repository mirror"
+msgstr "Дзеркало репозиторію"
msgid "Repository storage"
-msgstr ""
+msgstr "Сховище репозиторію"
msgid "Request Access"
msgstr "Запит доÑтупу"
+msgid "Require all users to accept Terms of Service and Privacy Policy when they access GitLab."
+msgstr "Вимагати від уÑÑ–Ñ… кориÑтувачів приймати умови Ð½Ð°Ð´Ð°Ð½Ð½Ñ Ð¿Ð¾Ñлуг та політику конфіденційноÑті, коли вони отримують доÑтуп до GitLab."
+
msgid "Reset git storage health information"
msgstr "Скиньте інформацію про працездатніÑть Ñховища git"
@@ -3618,11 +3885,26 @@ msgstr "Оновити токен доÑтупу Ð´Ð»Ñ Ð¿ÐµÑ€ÐµÐ²Ñ–Ñ€ÐºÐ¸ прÐ
msgid "Reset runners registration token"
msgstr "Перегенерувати реєÑтраційний токен runner-ів"
+msgid "Resolve all discussions in new issue"
+msgstr ""
+
+msgid "Resolve conflicts on source branch"
+msgstr "Вирішити конфлікти у гілці-джерелі"
+
msgid "Resolve discussion"
msgstr "Завершити обговореннÑ"
-msgid "Response"
-msgstr "Відповідь"
+msgid "Resume"
+msgstr "Продовжити"
+
+msgid "Retry"
+msgstr "Спробувати знову"
+
+msgid "Retry this job"
+msgstr "Повторити це завданнÑ"
+
+msgid "Retry verification"
+msgstr "Повторити перевірку"
msgid "Reveal value"
msgid_plural "Reveal values"
@@ -3637,38 +3919,41 @@ msgstr "Ðнулювати цей коміт"
msgid "Revert this merge request"
msgstr "Ðнулювати цей запит на злиттÑ"
-msgid "Review the process for configuring service providers in your identity provider — in this case, GitLab is the \"service provider\" or \"relying party\"."
-msgstr ""
+msgid "Review"
+msgstr "ОглÑд"
msgid "Reviewing"
-msgstr ""
+msgstr "ЗатвердженнÑ"
msgid "Reviewing (merge request !%{mergeRequestId})"
-msgstr ""
+msgstr "Ð—Ð°Ñ‚Ð²ÐµÑ€Ð´Ð¶ÐµÐ½Ð½Ñ (запиту на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ !%{mergeRequestId})"
-msgid "Roadmap"
-msgstr "План-графік"
+msgid "Rollback"
+msgstr ""
-msgid "Run CI/CD pipelines for external repositories"
-msgstr "ЗапуÑтити CI/CD конвеєри Ð´Ð»Ñ Ð·Ð¾Ð²Ð½Ñ–ÑˆÐ½Ñ–Ñ… репозиторіїв"
+msgid "Runner token"
+msgstr ""
msgid "Runners"
-msgstr ""
+msgstr "Runner'и"
+
+msgid "Runners API"
+msgstr "API Runner’ів"
+
+msgid "Runners can be placed on separate users, servers, and even on your local machine."
+msgstr "Runner’и можуть розміщуватиÑÑ Ñƒ різних кориÑтувачів, на Ñерверах Ñ– навіть на вашій локальній машині."
msgid "Running"
msgstr "ВиконуєтьÑÑ"
-msgid "SAML Single Sign On"
-msgstr ""
-
-msgid "SAML Single Sign On Settings"
-msgstr ""
+msgid "SSH Keys"
+msgstr "Ключі SSH"
-msgid "SHA1 fingerprint of the SAML token signing certificate. Get this from your identity provider, where it can also be called \"Thumbprint\"."
+msgid "SSL Verification"
msgstr ""
-msgid "SSH Keys"
-msgstr "Ключі SSH"
+msgid "Save"
+msgstr "Зберегти"
msgid "Save changes"
msgstr "Зберегти зміни"
@@ -3691,15 +3976,30 @@ msgstr "Розклади"
msgid "Scheduling Pipelines"
msgstr "ÐŸÐ»Ð°Ð½ÑƒÐ²Ð°Ð½Ð½Ñ ÐºÐ¾Ð½Ð²ÐµÑ”Ñ€Ñ–Ð²"
-msgid "Scoped issue boards"
-msgstr "Тематичні дошки проблем"
+msgid "Scroll to bottom"
+msgstr "Прокрутити вниз"
+
+msgid "Scroll to top"
+msgstr "Прокрутити вгору"
msgid "Search"
msgstr "Пошук"
+msgid "Search branches"
+msgstr "Пошук у гілках"
+
msgid "Search branches and tags"
msgstr "Пошук гілок та тегів"
+msgid "Search files"
+msgstr "Пошук файлів"
+
+msgid "Search for projects, issues, etc."
+msgstr "Пошук в проектах, проблемах і т. д."
+
+msgid "Search merge requests"
+msgstr "Пошук у запитах на злиттÑ"
+
msgid "Search milestones"
msgstr "Пошук етапів"
@@ -3715,15 +4015,15 @@ msgstr "КількіÑть Ñекунд до ÑÐºÐ¸Ð´Ð°Ð½Ð½Ñ Ñ–Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ñ–
msgid "Seconds to wait for a storage access attempt"
msgstr "КількіÑть Ñекунд Ð¾Ñ‡Ñ–ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ð¿ÐµÑ€ÐµÐ´ повторною Ñпробою доÑтупу до Ñховища даних"
-msgid "Secret variables"
-msgstr "Секретні змінні"
-
-msgid "Security report"
-msgstr "Звіт по безпеці"
+msgid "Select"
+msgstr "Вибрати"
msgid "Select Archive Format"
msgstr "Виберіть формат архіву"
+msgid "Select a namespace to fork the project"
+msgstr "Виберіть проÑтір імен Ð´Ð»Ñ Ñ„Ð¾Ñ€ÐºÑƒ проекту"
+
msgid "Select a timezone"
msgstr "Вибрати чаÑовий поÑÑ"
@@ -3736,12 +4036,21 @@ msgstr "Виберіть виконавцÑ"
msgid "Select branch/tag"
msgstr "Виберіть гілку або тег"
+msgid "Select project"
+msgstr "Вибрати проект"
+
+msgid "Select project and zone to choose machine type"
+msgstr "Вибрати проект та зону Ð´Ð»Ñ Ð²Ð¸Ð±Ð¾Ñ€Ñƒ типу машини"
+
+msgid "Select project to choose zone"
+msgstr "Вибрати проект Ð´Ð»Ñ Ð²Ð¸Ð±Ð¾Ñ€Ñƒ зони"
+
+msgid "Select source branch"
+msgstr "Виберіть гілку-джерело"
+
msgid "Select target branch"
msgstr "Вибір цільової гілки"
-msgid "Selective synchronization"
-msgstr "Вибіркова ÑинхронізаціÑ"
-
msgid "Send email"
msgstr "ÐадіÑлати лиÑта"
@@ -3757,26 +4066,23 @@ msgstr "ВерÑÑ–Ñ Ñервера"
msgid "Service Templates"
msgstr "Шаблони ÑервіÑів"
-msgid "Service URL"
-msgstr "Ð¡ÐµÑ€Ð²Ñ–Ñ URL"
-
msgid "Session expiration, projects limit and attachment size."
-msgstr ""
+msgstr "Термін дії ÑеÑÑ–Ñ—, проектні ліміти та розміри вкладень."
msgid "Set a password on your account to pull or push via %{protocol}."
msgstr "Ð’Ñтановіть пароль Ð´Ð»Ñ Ñвого облікового запиÑу, щоб мати можливіÑть відправлÑти та отримувати через %{protocol}."
msgid "Set default and restrict visibility levels. Configure import sources and git access protocol."
-msgstr ""
+msgstr "Ð’Ñтановіть Ð·Ð½Ð°Ñ‡ÐµÐ½Ð½Ñ Ð·Ð° замовчуваннÑм Ñ– обмежте рівні видимоÑті. Ðалаштуйте джерела імпорту Ñ– протокол доÑтупу git."
msgid "Set max session time for web terminal."
-msgstr ""
+msgstr "МакÑимальний термін дії ÑеÑÑ–Ñ— Ð´Ð»Ñ Ð²ÐµÐ±-терміналу."
msgid "Set notification email for abuse reports."
-msgstr ""
+msgstr "Ðалаштувати ÑÐ¿Ð¾Ð²Ñ–Ñ‰ÐµÐ½Ð½Ñ Ð¿Ð¾ електронній пошті Ð´Ð»Ñ Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½ÑŒ про зловживаннÑ."
msgid "Set requirements for a user to sign-in. Enable mandatory two-factor authentication."
-msgstr ""
+msgstr "Ð’Ñтановіть вимоги Ð´Ð»Ñ Ð²Ñ…Ð¾Ð´Ñƒ кориÑтувачів. Увімкніть обов’Ñзкову двофакторну автентифікацію."
msgid "Set up CI/CD"
msgstr "ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ CI/CD"
@@ -3784,9 +4090,6 @@ msgstr "ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ CI/CD"
msgid "Set up Koding"
msgstr "ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Koding"
-msgid "Set up assertions/attributes/claims (email, first_name, last_name) and NameID according to %{docsLinkStart}the documentation %{icon}%{docsLinkEnd}"
-msgstr ""
-
msgid "SetPasswordToCloneLink|set a password"
msgstr "вÑтановити пароль"
@@ -3794,29 +4097,35 @@ msgid "Settings"
msgstr "ÐалаштуваннÑ"
msgid "Setup a specific Runner automatically"
-msgstr ""
-
-msgid "Share the <strong>%{sso_label}</strong> with members so they can sign in to your group through your identity provider"
-msgstr ""
-
-msgid "SharedRunnersMinutesSettings|By resetting the pipeline minutes for this namespace, the currently used minutes will be set to zero."
-msgstr "При обнуленні хвилин конвеєрів Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ проÑтору імен, кількіÑть вже викориÑтаних хвилин буде дорівнювати 0."
+msgstr "Ðвтоматично налаштувати Ñпецифічний runner"
-msgid "SharedRunnersMinutesSettings|Reset pipeline minutes"
-msgstr "Обнулити хвилини в конвеєрі"
+msgid "Share"
+msgstr "ПоділитиÑÑ"
-msgid "SharedRunnersMinutesSettings|Reset used pipeline minutes"
-msgstr "Обнулити викориÑтані хвилини в конвеєрі"
+msgid "Shared Runners"
+msgstr "Загальні Runner'и"
msgid "Show command"
msgstr "Показати команду"
+msgid "Show complete raw log"
+msgstr "Показати повний неформатований журнал"
+
+msgid "Show latest version"
+msgstr ""
+
+msgid "Show latest version of the diff"
+msgstr ""
+
msgid "Show parent pages"
msgstr "Показати батьківÑькі Ñторінки"
msgid "Show parent subgroups"
msgstr "Показати батьківÑькі підгрупи"
+msgid "Show whitespace changes"
+msgstr ""
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "Показано %d подію"
@@ -3824,28 +4133,22 @@ msgstr[1] "Показано %d події"
msgstr[2] "Показано %d подій"
msgstr[3] "Показано %d подій"
-msgid "Sidebar|Change weight"
-msgstr "Змінити вагу"
-
-msgid "Sidebar|No"
-msgstr "ÐÑ–"
-
-msgid "Sidebar|None"
-msgstr "Ðемає"
+msgid "Side-by-side"
+msgstr ""
-msgid "Sidebar|Weight"
-msgstr "Вага"
+msgid "Sign out"
+msgstr "Вийти"
msgid "Sign-in restrictions"
-msgstr ""
+msgstr "ÐžÐ±Ð¼ÐµÐ¶ÐµÐ½Ð½Ñ Ð´Ð»Ñ Ð²Ñ…Ð¾Ð´Ñƒ"
msgid "Sign-up restrictions"
-msgstr ""
+msgstr "ÐžÐ±Ð¼ÐµÐ¶ÐµÐ½Ð½Ñ Ð´Ð»Ñ Ñ€ÐµÑ”Ñтрації"
msgid "Size and domain settings for static websites"
-msgstr ""
+msgstr "ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ñ€Ð¾Ð·Ð¼Ñ–Ñ€Ñƒ та домену Ð´Ð»Ñ Ñтатичних веб-Ñайтів"
-msgid "Slack application"
+msgid "Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job"
msgstr ""
msgid "Snippets"
@@ -3855,23 +4158,29 @@ msgid "Something went wrong on our end"
msgstr "ЩоÑÑŒ пішло не так з нашого боку"
msgid "Something went wrong on our end."
+msgstr "ЩоÑÑŒ пішло не так з нашого боку."
+
+msgid "Something went wrong on our end. Please try again!"
msgstr ""
msgid "Something went wrong when toggling the button"
msgstr "Помилка при перемиканні кнопки"
-msgid "Something went wrong while fetching Dependency Scanning."
+msgid "Something went wrong while closing the %{issuable}. Please try again later"
msgstr ""
-msgid "Something went wrong while fetching SAST."
-msgstr "Помилка при отриманні SAST."
-
msgid "Something went wrong while fetching the projects."
msgstr "ЩоÑÑŒ пішло не так під Ñ‡Ð°Ñ Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ð½Ð½Ñ Ð¿Ñ€Ð¾ÐµÐºÑ‚Ñ–Ð²"
msgid "Something went wrong while fetching the registry list."
msgstr "ЩоÑÑŒ пішло не так при отриманні ÑпиÑку із реєÑтру."
+msgid "Something went wrong while reopening the %{issuable}. Please try again later"
+msgstr ""
+
+msgid "Something went wrong while resolving this discussion. Please try again."
+msgstr ""
+
msgid "Something went wrong. Please try again."
msgstr "ЩоÑÑŒ пішло не так. Будь лаÑка Ñпробуйте ще раз."
@@ -3917,9 +4226,6 @@ msgstr "ОÑтанній оновлений"
msgid "SortOptions|Least popular"
msgstr "Ðайменш популÑрний"
-msgid "SortOptions|Less weight"
-msgstr "Менша вага"
-
msgid "SortOptions|Milestone"
msgstr "Етап"
@@ -3929,9 +4235,6 @@ msgstr "Етап запланований на пізніше"
msgid "SortOptions|Milestone due soon"
msgstr "Етап запланований незабаром"
-msgid "SortOptions|More weight"
-msgstr "Більша вага"
-
msgid "SortOptions|Most popular"
msgstr "Ðайбільш популÑрний"
@@ -3971,9 +4274,6 @@ msgstr "Розпочатий пізніше"
msgid "SortOptions|Start soon"
msgstr "Розпочатий нещодавно"
-msgid "SortOptions|Weight"
-msgstr "Вага"
-
msgid "Source"
msgstr "Джерело"
@@ -3990,19 +4290,46 @@ msgid "Spam Logs"
msgstr "Спам-журнал"
msgid "Spam and Anti-bot Protection"
-msgstr ""
+msgstr "ЗахиÑÑ‚ від Ñпаму Ñ– ботів"
+
+msgid "Specific Runners"
+msgstr "Спеціальні Runner’и"
msgid "Specify the following URL during the Runner setup:"
msgstr "Зазначте наÑтупний URL під Ñ‡Ð°Ñ Ð²ÑÑ‚Ð°Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Runner-а:"
+msgid "Squash commits"
+msgstr "Виконати об'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ (squash) комітів"
+
+msgid "Stage"
+msgstr "СтадіÑ"
+
+msgid "Stage & Commit"
+msgstr ""
+
+msgid "Stage all changes"
+msgstr "ПроіндекÑувати вÑÑ– зміни"
+
+msgid "Stage changes"
+msgstr "ПроіндекÑувати зміни"
+
+msgid "Staged"
+msgstr "ПроіндекÑовано"
+
+msgid "Staged %{type}"
+msgstr "ПроіндекÑовано %{type}"
+
+msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
+msgstr "Позначте мітку, щоб зробити Ñ—Ñ— пріоритетною. ПеретÑгуйте пріоритетні мітки Ð´Ð»Ñ Ð·Ð¼Ñ–Ð½Ð¸ Ñ—Ñ… відноÑного пріоритету."
+
msgid "StarProject|Star"
msgstr "В обрані"
msgid "Starred Projects"
-msgstr ""
+msgstr "Обрані Проекти"
msgid "Starred Projects' Activity"
-msgstr ""
+msgstr "ÐктивніÑть в обраних проектах"
msgid "Starred projects"
msgstr "Обрані проекти"
@@ -4016,12 +4343,15 @@ msgstr "ЗапуÑтіть Runner!"
msgid "Started"
msgstr "Запущений"
-msgid "State your message to activate"
-msgstr ""
+msgid "Starts at (UTC)"
+msgstr "ПочинаєтьÑÑ Ð¾ (за Грінвічем)"
msgid "Status"
msgstr "СтатуÑ"
+msgid "Stop this environment"
+msgstr "Зупинити це Ñередовище"
+
msgid "Stopped"
msgstr "Зупинено"
@@ -4031,18 +4361,21 @@ msgstr "Сховище"
msgid "Subgroups"
msgstr "Підгрупи"
+msgid "Subscribe"
+msgstr "ПідпиÑатиÑÑ"
+
+msgid "Subscribe at group level"
+msgstr "ПідпиÑатиÑÑ Ð½Ð° рівні групи"
+
+msgid "Subscribe at project level"
+msgstr "ПідпиÑатиÑÑ Ð½Ð° рівні проекту"
+
msgid "Switch branch/tag"
msgstr "Перейти в гілку/тег"
-msgid "System"
-msgstr "СиÑтема"
-
msgid "System Hooks"
msgstr "СиÑтемні гуки"
-msgid "System header and footer:"
-msgstr ""
-
msgid "Tag (%{tag_count})"
msgid_plural "Tags (%{tag_count})"
msgstr[0] "Тег (%{tag_count})"
@@ -4053,6 +4386,9 @@ msgstr[3] "Тегів (%{tag_count})"
msgid "Tags"
msgstr "Теги"
+msgid "Tags:"
+msgstr "Теги:"
+
msgid "TagsPage|Browse commits"
msgstr "ПереглÑнути коміти"
@@ -4116,8 +4452,8 @@ msgstr "Цей тег не міÑтить опиÑу релізу."
msgid "TagsPage|Use git tag command to add a new one:"
msgstr "ВикориÑтовуйте команду git tag, щоб додати новий тег:"
-msgid "TagsPage|Write your release notes or drag files here..."
-msgstr "Ðапишіть Ñвій Ð¾Ð¿Ð¸Ñ Ñ€ÐµÐ»Ñ–Ð·Ñƒ або перетÑгніть файли Ñюди..."
+msgid "TagsPage|Write your release notes or drag files here…"
+msgstr "Створіть Ð¾Ð¿Ð¸Ñ Ñ€ÐµÐ»Ñ–Ð·Ñƒ або перетÑгніть файли Ñюди…"
msgid "TagsPage|protected"
msgstr "захищений"
@@ -4126,25 +4462,25 @@ msgid "Target Branch"
msgstr "Цільова гілка"
msgid "Target branch"
-msgstr ""
+msgstr "Цільова гілка"
msgid "Team"
msgstr "Команда"
-msgid "Thanks! Don't show me this again"
-msgstr "ДÑкую! Більше не показувати це повідомленнÑ"
+msgid "Terms of Service Agreement and Privacy Policy"
+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 "Terms of Service and Privacy Policy"
+msgstr "Правилами кориÑÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ ÑервіÑом Ñ– політика конфіденційноÑті"
-msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project"
+msgid "Test coverage parsing"
msgstr ""
-msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
-msgstr ""
+msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project"
+msgstr "Трекер проблем — це міÑце, де можна додати речі, Ñкі потрібно покращити або розв’Ñзати в проекті"
-msgid "The X509 Certificate to use when mutual TLS is required to communicate with the external authorization service. If left blank, the server certificate is still validated when accessing over HTTPS."
-msgstr ""
+msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
+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 "Ð¡Ñ‚Ð°Ð´Ñ–Ñ ÐапиÑÐ°Ð½Ð½Ñ ÐºÐ¾Ð´Ñƒ показує Ñ‡Ð°Ñ Ð²Ñ–Ð´ першого коміту до ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ð·Ð°Ð¿Ð¸Ñ‚Ñƒ на злиттÑ. Дані будуть автоматично додані піÑÐ»Ñ ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ð²Ð°ÑˆÐ¾Ð³Ð¾ першого запиту на злиттÑ."
@@ -4152,9 +4488,6 @@ msgstr "Ð¡Ñ‚Ð°Ð´Ñ–Ñ ÐапиÑÐ°Ð½Ð½Ñ ÐºÐ¾Ð´Ñƒ показує Ñ‡Ð°Ñ Ð²Ñ–Ð´ п
msgid "The collection of events added to the data gathered for that stage."
msgstr "ÐšÐ¾Ð»ÐµÐºÑ†Ñ–Ñ Ð¿Ð¾Ð´Ñ–Ð¹ додана до даних, зібраних Ð´Ð»Ñ Ñ†Ñ–Ñ”Ñ— Ñтадії."
-msgid "The connection will time out after %{timeout}. For repositories that take longer, use a clone/push combination."
-msgstr "Ð—â€™Ñ”Ð´Ð½Ð°Ð½Ð½Ñ Ð±ÑƒÐ´Ðµ припинено піÑÐ»Ñ %{timeout}. Ð”Ð»Ñ Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ñ–Ñ—Ð², Ñким потрібно більше чаÑу, викориÑтовуйте комбінацію clone/push."
-
msgid "The fork relationship has been removed."
msgstr "Зв'Ñзок форку видалено."
@@ -4173,7 +4506,7 @@ 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 passphrase required to decrypt the private key. This is optional and the value is encrypted at rest."
+msgid "The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>"
msgstr ""
msgid "The phase of the development lifecycle."
@@ -4182,9 +4515,6 @@ msgstr "Фаза життєвого циклу розробки."
msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit."
msgstr "Ð¡Ñ‚Ð°Ð´Ñ–Ñ ÐŸÐ»Ð°Ð½ÑƒÐ²Ð°Ð½Ð½Ñ Ð²Ñ–Ð´Ð¾Ð±Ñ€Ð°Ð¶Ð°Ñ” Ñ‡Ð°Ñ Ð²Ñ–Ð´ попереднього кроку до першого коміту. ДодаєтьÑÑ Ð°Ð²Ñ‚Ð¾Ð¼Ð°Ñ‚Ð¸Ñ‡Ð½Ð¾, Ñк тільки відправитьÑÑ Ð¿ÐµÑ€ÑˆÐ¸Ð¹ коміт."
-msgid "The private key to use when a client certificate is provided. This value is encrypted at rest."
-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 "Ð¡Ñ‚Ð°Ð´Ñ–Ñ Production показує загальний Ñ‡Ð°Ñ Ð¼Ñ–Ð¶ ÑтвореннÑм проблеми та розгортаннÑм коду у production. Дані будуть автоматично додані піÑÐ»Ñ Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð½Ñ Ð¿Ð¾Ð²Ð½Ð¾Ñ— ідеї до production циклу."
@@ -4206,8 +4536,8 @@ msgstr "Репозиторій має бути доÑтупним через <co
msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request."
msgstr "Ð¡Ñ‚Ð°Ð´Ñ–Ñ Ð—Ð°Ñ‚Ð²ÐµÑ€Ð´Ð¶ÐµÐ½Ð½Ñ Ð¿Ð¾ÐºÐ°Ð·ÑƒÑ” Ñ‡Ð°Ñ Ð²Ñ–Ð´ ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ð·Ð°Ð¿Ð¸Ñ‚Ñƒ про об'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ Ð´Ð¾ його виконаннÑ. Дані будуть автоматично додані піÑÐ»Ñ Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð½Ñ Ð¿ÐµÑ€ÑˆÐ¾Ð³Ð¾ запиту на злиттÑ."
-msgid "The roadmap shows the progress of your epics along a timeline"
-msgstr "План-графік показує Ñтан ваших епіків у чаÑÑ–"
+msgid "The secure token used by the Runner to checkout the project"
+msgstr ""
msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time."
msgstr "Ð¡Ñ‚Ð°Ð´Ñ–Ñ Staging показує Ñ‡Ð°Ñ Ð¼Ñ–Ð¶ Ð²Ð¸ÐºÐ¾Ð½Ð°Ð½Ð½Ñ Ð·Ð°Ð¿Ð¸Ñ‚Ñƒ на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ñ‚Ð° розгортаннÑм коду у production. Дані автоматично додаютьÑÑ Ð¿Ñ–ÑÐ»Ñ Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ñƒ production вперше."
@@ -4233,14 +4563,20 @@ msgstr "Середнє Ð·Ð½Ð°Ñ‡ÐµÐ½Ð½Ñ Ð² Ñ€Ñдку. Приклад: між 3,
msgid "There are no issues to show"
msgstr "Ðемає проблем Ð´Ð»Ñ Ð²Ñ–Ð´Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð½Ñ"
+msgid "There are no labels yet"
+msgstr ""
+
msgid "There are no merge requests to show"
msgstr "Ðемає запитів на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð´Ð»Ñ Ð²Ñ–Ð´Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð½Ñ"
msgid "There are problems accessing Git storage: "
msgstr "Є проблеми з доÑтупом до Ñховища git: "
-msgid "There was an error loading results"
-msgstr ""
+msgid "There was an error loading jobs"
+msgstr "Помилка при завантаженні завдань"
+
+msgid "There was an error loading latest pipeline"
+msgstr "Помилка при завантаженні оÑтаннього конвеєру"
msgid "There was an error loading users activity calendar."
msgstr "Помилка при завантаженні ÐºÐ°Ð»ÐµÐ½Ð´Ð°Ñ€Ñ Ð°ÐºÑ‚Ð¸Ð²Ð½Ð¾Ñті кориÑтувачів."
@@ -4260,12 +4596,21 @@ msgstr "Помилка при підпиÑці на цю мітку."
msgid "There was an error when unsubscribing from this label."
msgstr "Помилка при відпиÑці від цієї мітки."
-msgid "This board\\'s scope is reduced"
-msgstr "ВидиміÑть цієї дошки обмежена"
+msgid "They can be managed using the %{link}."
+msgstr "Ðими можна керувати за допомогою %{link}."
+
+msgid "This GitLab instance does not provide any shared Runners yet. Instance administrators can register shared Runners in the admin area."
+msgstr "Цей інÑÑ‚Ð°Ð½Ñ GitLab ще немає загальних Runner'ів. ÐдмініÑтратори можуть Ñ—Ñ… зареєÑтрувати у Ñпеціальному розділі конфігурації."
+
+msgid "This diff is collapsed."
+msgstr ""
msgid "This directory"
msgstr "Цей каталог"
+msgid "This group does not provide any group Runners yet."
+msgstr "Ð¦Ñ Ð³Ñ€ÑƒÐ¿Ð° ще не має жодного групового Runner’а."
+
msgid "This is a confidential issue."
msgstr "Це конфіденційна проблема."
@@ -4287,6 +4632,15 @@ msgstr "Це Ð·Ð°Ð²Ð´Ð°Ð½Ð½Ñ Ð·Ð°Ð¿ÑƒÑкаєтьÑÑ ÐºÐ¾Ñ€Ð¸Ñтувачем.
msgid "This job depends on upstream jobs that need to succeed in order for this job to be triggered"
msgstr "Це Ð·Ð°Ð²Ð´Ð°Ð½Ð½Ñ Ð·Ð°Ð»ÐµÐ¶Ð¸Ñ‚ÑŒ від попередніх, Ñкі повинні завершитиÑÑ ÑƒÑпішно Ð´Ð»Ñ Ð¹Ð¾Ð³Ð¾ запуÑку"
+msgid "This job does not have a trace."
+msgstr "Лог Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ Ð·Ð°Ð²Ð´Ð°Ð½Ð½Ñ Ð²Ñ–Ð´Ñутній."
+
+msgid "This job has been canceled"
+msgstr "Це Ð·Ð°Ð²Ð´Ð°Ð½Ð½Ñ Ð±ÑƒÐ»Ð¾ відмінене"
+
+msgid "This job has been skipped"
+msgstr "Це Ð·Ð°Ð²Ð´Ð°Ð½Ð½Ñ Ð±ÑƒÐ»Ð¾ пропущене"
+
msgid "This job has not been triggered yet"
msgstr "Ð—Ð°Ð²Ð´Ð°Ð½Ð½Ñ Ñ‰Ðµ не було запущене"
@@ -4294,7 +4648,7 @@ msgid "This job has not started yet"
msgstr "Це Ð·Ð°Ð²Ð´Ð°Ð½Ð½Ñ Ñ‰Ðµ не запуÑтилоÑÑ"
msgid "This job is in pending state and is waiting to be picked by a runner"
-msgstr ""
+msgstr "Це Ð·Ð°Ð²Ð´Ð°Ð½Ð½Ñ Ð¿ÐµÑ€ÐµÐ±ÑƒÐ²Ð°Ñ” в Ñтані Ð¾Ñ‡Ñ–ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ñ– чекає на запуÑк Runner"
msgid "This job requires a manual action"
msgstr "Ð—Ð°Ð²Ð´Ð°Ð½Ð½Ñ Ð²Ð¸Ð¼Ð°Ð³Ð°Ñ” ручних дій"
@@ -4305,20 +4659,29 @@ msgstr "Це означає, що ви не можете відправлÑти
msgid "This merge request is locked."
msgstr "Цей запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð·Ð°Ð±Ð»Ð¾ÐºÐ¾Ð²Ð°Ð½Ð¾."
+msgid "This option is disabled while you still have unstaged changes"
+msgstr "Ð¦Ñ Ð¾Ð¿Ñ†Ñ–Ñ Ð½ÐµÐ´Ð¾Ñтупна, поки у Ð²Ð°Ñ Ñ” неіндекÑовані зміни"
+
msgid "This page is unavailable because you are not allowed to read information across multiple projects."
msgstr "Ð¦Ñ Ñторінка недоÑтупна, тому що ви не можете переглÑдати інформацію по кількох проектах."
+msgid "This page will be removed in a future release."
+msgstr "Цю Ñторінку буде видалено у майбутній верÑÑ–Ñ—."
+
msgid "This project"
msgstr "Цей проект"
+msgid "This project does not belong to a group and can therefore not make use of group Runners."
+msgstr "Цей проект не входить до жодної групи Ñ– тому не може викориÑтовувати групові Runner’и."
+
msgid "This repository"
msgstr "Цей репозиторій"
-msgid "This will delete the custom metric, Are you sure?"
-msgstr "Це призведе до Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð²Ð»Ð°Ñної метрики, ви впевнені?"
+msgid "This source diff could not be displayed because it is too large."
+msgstr ""
-msgid "Those emails automatically become issues (with the comments becoming the email conversation) listed here."
-msgstr "Ці Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ ÐµÐ»ÐµÐºÑ‚Ñ€Ð¾Ð½Ð½Ð¾Ñ— пошти автоматично Ñтануть обговореннÑми проблем, Ñкі відображатимутьÑÑ Ñ‚ÑƒÑ‚ (причому коментарі Ñтануть чаÑтиною перепиÑки)."
+msgid "This user has no identities"
+msgstr ""
msgid "Time before an issue gets scheduled"
msgstr "Ð§Ð°Ñ Ð´Ð¾ початку потраплÑÐ½Ð½Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼Ð¸ в планувальник"
@@ -4329,11 +4692,11 @@ msgstr "Ð§Ð°Ñ Ð´Ð¾ початку роботи над проблемою"
msgid "Time between merge request creation and merge/close"
msgstr "Ð§Ð°Ñ Ð¼Ñ–Ð¶ ÑтвореннÑм запиту Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ñ– його виконаннÑм або закриттÑм"
-msgid "Time between updates and capacity settings."
-msgstr ""
+msgid "Time remaining"
+msgstr "ЗалишилоÑÑ Ñ‡Ð°Ñу"
-msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
-msgstr ""
+msgid "Time spent"
+msgstr "Витрачено чаÑу"
msgid "Time tracking"
msgstr "ВідÑÑ‚ÐµÐ¶ÐµÐ½Ð½Ñ Ñ‡Ð°Ñу"
@@ -4356,6 +4719,9 @@ msgstr "%s днів тому"
msgid "Timeago|%s days remaining"
msgstr "залишилоÑÑ %s днів"
+msgid "Timeago|%s hours ago"
+msgstr "%s годин тому"
+
msgid "Timeago|%s hours remaining"
msgstr "залишилоÑÑ %s годин"
@@ -4371,6 +4737,9 @@ msgstr "%s міÑÑці(в) тому"
msgid "Timeago|%s months remaining"
msgstr "залишилоÑÑ %s міÑÑці(в)"
+msgid "Timeago|%s seconds ago"
+msgstr "%s Ñекунд тому"
+
msgid "Timeago|%s seconds remaining"
msgstr "%s Ñекунд, що залишаютьÑÑ"
@@ -4386,48 +4755,45 @@ msgstr "%s років тому"
msgid "Timeago|%s years remaining"
msgstr "залишилоÑÑ %s роки"
+msgid "Timeago|1 day ago"
+msgstr "1 день тому"
+
msgid "Timeago|1 day remaining"
msgstr "ЗалишивÑÑ 1 день"
+msgid "Timeago|1 hour ago"
+msgstr "1 година тому"
+
msgid "Timeago|1 hour remaining"
msgstr "ЗалишилаÑÑŒ 1 година"
+msgid "Timeago|1 minute ago"
+msgstr "1 хвилина тому"
+
msgid "Timeago|1 minute remaining"
msgstr "ЗалишилаÑÑŒ 1 хвилина"
+msgid "Timeago|1 month ago"
+msgstr "1 міÑÑць тому"
+
msgid "Timeago|1 month remaining"
msgstr "ЗалишивÑÑ 1 міÑÑць"
+msgid "Timeago|1 week ago"
+msgstr "1 тиждень тому"
+
msgid "Timeago|1 week remaining"
msgstr "ЗалишивÑÑ 1 тиждень"
+msgid "Timeago|1 year ago"
+msgstr "1 рік тому"
+
msgid "Timeago|1 year remaining"
msgstr "ЗалишивÑÑ 1 рік"
msgid "Timeago|Past due"
msgstr "ПроÑтрочені"
-msgid "Timeago|a day ago"
-msgstr "День тому"
-
-msgid "Timeago|a month ago"
-msgstr "міÑÑць тому"
-
-msgid "Timeago|a week ago"
-msgstr "тиждень тому"
-
-msgid "Timeago|a year ago"
-msgstr "рік тому"
-
-msgid "Timeago|about %s hours ago"
-msgstr "Близько %s годин(и) тому"
-
-msgid "Timeago|about a minute ago"
-msgstr "Близько хвилини тому"
-
-msgid "Timeago|about an hour ago"
-msgstr "Близько години тому"
-
msgid "Timeago|in %s days"
msgstr "через %s дні(в)"
@@ -4467,11 +4833,14 @@ msgstr "через тиждень"
msgid "Timeago|in 1 year"
msgstr "через рік"
-msgid "Timeago|in a while"
-msgstr "невдовзі"
+msgid "Timeago|just now"
+msgstr "щойно"
-msgid "Timeago|less than a minute ago"
-msgstr "менше хвилини тому"
+msgid "Timeago|right now"
+msgstr "зараз"
+
+msgid "Timeout"
+msgstr ""
msgid "Time|hr"
msgid_plural "Time|hrs"
@@ -4493,23 +4862,14 @@ msgstr "Ñекунд(а)"
msgid "Tip:"
msgstr "Порада:"
-msgid "Title"
-msgstr "Ðазва"
-
msgid "To GitLab"
msgstr "Ð’ GitLab"
-msgid "To connect GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to connect."
+msgid "To add an SSH key you need to %{generate_link_start}generate one%{link_end} or use an %{existing_link_start}existing key%{link_end}."
msgstr ""
-msgid "To connect GitHub repositories, you first need to authorize GitLab to access the list of your GitHub repositories:"
-msgstr "Ð”Ð»Ñ Ð¿Ñ–Ð´ÐºÐ»ÑŽÑ‡ÐµÐ½Ð½Ñ Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ñ–Ñ—Ð² з GitHub, ви Ñпочатку повинні дозволити Gitlab доÑтуп до ÑпиÑку ваших репозиторіїв на GitHub:"
-
-msgid "To connect an SVN repository, check out %{svn_link}."
-msgstr "Ð”Ð»Ñ Ð¿Ñ€Ð¸Ñ”Ð´Ð½Ð°Ð½Ð½Ñ SVN-репозиторію, переглÑньте %{svn_link}."
-
msgid "To import GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
-msgstr ""
+msgstr "Ð”Ð»Ñ Ñ–Ð¼Ð¿Ð¾Ñ€Ñ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ñ–Ñ—Ð² з GitHub, ви можете викориÑтовувати %{personal_access_token_link}. Коли ви Ñтворюватимете ваш перÑональний токен доÑтупу, вам потрібно буде вибрати облаÑть дії <code>repo</code>, щоб ми могли відобразити ÑпиÑок ваших публічних та приватних репозиторіїв, доÑтупних Ð´Ð»Ñ Ð¿Ñ–Ð´ÐºÐ»ÑŽÑ‡ÐµÐ½Ð½Ñ."
msgid "To import GitHub repositories, you first need to authorize GitLab to access the list of your GitHub repositories:"
msgstr "Ð”Ð»Ñ Ñ–Ð¼Ð¿Ð¾Ñ€Ñ‚Ñƒ репозиторіїв з GitHub, ви Ñпочатку повинні дозволити Gitlab доÑтуп до ÑпиÑку ваших репозиторіїв на GitHub:"
@@ -4517,21 +4877,21 @@ msgstr "Ð”Ð»Ñ Ñ–Ð¼Ð¿Ð¾Ñ€Ñ‚Ñƒ репозиторіїв з GitHub, ви ÑпочÐ
msgid "To import an SVN repository, check out %{svn_link}."
msgstr "Ð”Ð»Ñ Ñ–Ð¼Ð¿Ð¾Ñ€Ñ‚ÑƒÐ²Ð°Ð½Ð½Ñ SVN-репозиторію, переглÑньте %{svn_link}."
-msgid "To only use CI/CD features for an external repository, choose <strong>CI/CD for external repo</strong>."
-msgstr "Щоб викориÑтовувати лише функції CI/CD Ð´Ð»Ñ Ð·Ð¾Ð²Ð½Ñ–ÑˆÐ½ÑŒÐ¾Ð³Ð¾ репозиторію, виберіть <strong>CI/CD Ð´Ð»Ñ Ð·Ð¾Ð²Ð½Ñ–ÑˆÐ½ÑŒÐ¾Ð³Ð¾ репозиторію</strong>."
-
-msgid "To set up SAML authentication for your group through an identity provider like Azure, Okta, Onelogin, Ping Identity, or your custom SAML 2.0 provider:"
-msgstr ""
+msgid "To start serving your jobs you can add Runners to your group"
+msgstr "Ð”Ð»Ñ Ð²Ð¸ÐºÐ¾Ð½Ð°Ð½Ð½Ñ Ð²Ð°ÑˆÐ¸Ñ… завдань ви можете додати Runner’и до вашої групи"
msgid "To validate your GitLab CI configurations, go to 'CI/CD → Pipelines' inside your project, and click on the 'CI Lint' button."
-msgstr ""
-
-msgid "To view the roadmap, add a planned start or finish date to one of your epics in this group or its subgroups. Only epics in the past 3 months and the next 3 months are shown."
-msgstr "Ð”Ð»Ñ Ð¿ÐµÑ€ÐµÐ³Ð»Ñду плану-графіку додайте заплановані дати початку та Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð½Ñ Ð´Ð¾ одного з ваших епіків у цій групі або Ñ—Ñ— підгрупах. ВідображаютьÑÑ Ð»Ð¸ÑˆÐµ епіки за попередні та наÑтупні 3 міÑÑці."
+msgstr "Щоб перевірити Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ GitLab CI, перейдіть до \"CI / CD → Конвеєри\" у вашому проекті та натиÑніть кнопку \"Перевірка конфігурації (CI Lint)\"."
msgid "Todo"
msgstr "Задача"
+msgid "Toggle Sidebar"
+msgstr "Перемикач бічної панелі"
+
+msgid "Toggle discussion"
+msgstr "Перемикач диÑкуÑÑ–Ñ—"
+
msgid "Toggle sidebar"
msgstr "Перемикач бічної панелі"
@@ -4541,6 +4901,9 @@ msgstr "Ð¡Ñ‚Ð°Ñ‚ÑƒÑ Ð¿ÐµÑ€ÐµÐ¼Ð¸ÐºÐ°Ñ‡Ð°: ВИМКÐЕÐО"
msgid "ToggleButton|Toggle Status: ON"
msgstr "Ð¡Ñ‚Ð°Ñ‚ÑƒÑ Ð¿ÐµÑ€ÐµÐ¼Ð¸ÐºÐ°Ñ‡Ð°: УВІМКÐЕÐО"
+msgid "Too many changes to show."
+msgstr ""
+
msgid "Total Time"
msgstr "Загальний чаÑ"
@@ -4550,23 +4913,20 @@ msgstr "Загальний чаÑ, щоб перевірити вÑÑ– коміт
msgid "Total: %{total}"
msgstr "Ð’Ñього: %{total}"
-msgid "Track activity with Contribution Analytics."
-msgstr "ВідÑтежувати активніÑть за допомогою Ðналітики учаÑників."
-
-msgid "Track groups of issues that share a theme, across projects and milestones"
-msgstr "ВідÑтежуйте групи проблем зі Ñпільною темою з різних проектів та етапів"
-
msgid "Track time with quick actions"
msgstr "ВідÑтежуйте Ñ‡Ð°Ñ Ð·Ð° допомогою швидких дій"
msgid "Trigger this manual action"
msgstr "ЗапуÑтити цю ручну дію"
-msgid "Turn on Service Desk"
-msgstr "Ввімкнути Service Desk"
+msgid "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."
+msgstr ""
+
+msgid "Try again"
+msgstr "Спробуйте ще раз"
-msgid "Unknown"
-msgstr "Ðевідомо"
+msgid "Unable to load the diff. %{button_try_again}"
+msgstr "Ðеможливо завантажити порівнÑÐ½Ð½Ñ (diff). %{button_try_again}"
msgid "Unlock"
msgstr "Розблокувати"
@@ -4577,26 +4937,48 @@ msgstr "Розблоковано"
msgid "Unresolve discussion"
msgstr "Повторно відкрити обговореннÑ"
+msgid "Unstage all changes"
+msgstr "Ðе індекÑувати вÑÑ– зміни"
+
+msgid "Unstage changes"
+msgstr "Ðе індекÑувати зміни"
+
+msgid "Unstaged"
+msgstr "ÐеіндекÑовано"
+
+msgid "Unstaged %{type}"
+msgstr "ÐеіндекÑовано %{type}"
+
+msgid "Unstaged and staged %{type}"
+msgstr "ÐеіндекÑовано та проіндекÑовано %{type}"
+
msgid "Unstar"
msgstr "Видалити із обраних"
-msgid "Up to date"
-msgstr "Ðктуальний"
+msgid "Unsubscribe"
+msgstr "ВідпиÑатиÑÑ"
-msgid "Upgrade your plan to activate Advanced Global Search."
-msgstr "Перейдіть на вищий тарифний план щоб активувати Покращений Глобальний Пошук."
+msgid "Unsubscribe at group level"
+msgstr "ВідпиÑатиÑÑ Ð½Ð° рівні групи"
-msgid "Upgrade your plan to activate Contribution Analytics."
-msgstr "Перейдіть на вищий тарифний план Ð´Ð»Ñ Ð°ÐºÑ‚Ð¸Ð²Ð°Ñ†Ñ–Ñ— Ðналітики учаÑників."
+msgid "Unsubscribe at project level"
+msgstr "ВідпиÑатиÑÑ Ð½Ð° рівні проекту"
-msgid "Upgrade your plan to activate Group Webhooks."
-msgstr "Перейдіть на вищий тарифний план щоб активувати групові веб-гуки."
+msgid "Unverified"
+msgstr "Ðепідтверджено"
+
+msgid "Up to date"
+msgstr "Ðктуальний"
-msgid "Upgrade your plan to activate Issue weight."
-msgstr "Перейдіть на вищий тарифний план щоб активувати вагу обговорень проблем."
+msgid "Update %{files}"
+msgid_plural "Update %{files} files"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
-msgid "Upgrade your plan to improve Issue boards."
-msgstr "Перейдіть на вищий тарифний план щоб покращити дошки обговорень."
+msgid "Update your group name, description, avatar, and other general settings."
+msgstr "Оновіть Ñ–Ð¼â€™Ñ Ð³Ñ€ÑƒÐ¿Ð¸, опиÑ, аватар та інші загальні налаштуваннÑ."
msgid "Upload New File"
msgstr "Завантажити новий файл"
@@ -4611,13 +4993,13 @@ msgid "UploadLink|click to upload"
msgstr "ÐатиÑніть, щоб завантажити"
msgid "Upvotes"
-msgstr ""
+msgstr "Лайки"
msgid "Usage statistics"
-msgstr ""
+msgstr "СтатиÑтика викориÑтаннÑ"
-msgid "Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab"
-msgstr "ВикориÑтовуйте Service Desk Ð´Ð»Ñ Ð·Ð²â€™Ñзку з вашими кориÑтувачами (наприклад, щоб запропонувати клієнтÑьку підтримку) через електронну пошту безпоÑередньо із GitLab"
+msgid "Use group milestones to manage issues from multiple projects in the same milestone."
+msgstr "ВикориÑтовуйте групові етапи, щоб керувати у одному етапі проблеми з різних проектів."
msgid "Use the following registration token during setup:"
msgstr "ВикориÑтовувати токен під Ñ‡Ð°Ñ ÑƒÑтановки:"
@@ -4625,29 +5007,29 @@ msgstr "ВикориÑтовувати токен під Ñ‡Ð°Ñ ÑƒÑтановк
msgid "Use your global notification setting"
msgstr "ВикориÑтовуютьÑÑ Ð³Ð»Ð¾Ð±Ð°Ð»ÑŒÐ½Ñ– Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½ÑŒ"
-msgid "Used by members to sign in to your group in GitLab"
-msgstr ""
-
msgid "User and IP Rate Limits"
-msgstr ""
+msgstr "Ліміти чаÑтоти Ð´Ð»Ñ ÐºÐ¾Ñ€Ð¸Ñтувачів та IP"
+
+msgid "Users"
+msgstr "КориÑтувачі"
+
+msgid "Variables"
+msgstr "Змінні"
msgid "Variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. You can use variables for passwords, secret keys, or whatever you want."
msgstr "Змінні заÑтоÑовуютьÑÑ Ð´Ð¾ Ñередовищ через runner. Вони можуть бути захищені, в такому випадку вони доÑтупні тільки Ð´Ð»Ñ Ð·Ð°Ñ…Ð¸Ñ‰ÐµÐ½Ð¸Ñ… гілок та тегів. Ви можете викориÑтовувати змінні Ð´Ð»Ñ Ð¿Ð°Ñ€Ð¾Ð»Ñ–Ð², Ñекретний ключів тощо."
msgid "Various container registry settings."
-msgstr ""
+msgstr "Різноманітні Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ñ€ÐµÑ”Ñтру контейнерів."
msgid "Various email settings."
-msgstr ""
+msgstr "Різноманітні Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð¿Ð¾ÑˆÑ‚Ð¸."
msgid "Various settings that affect GitLab performance."
-msgstr ""
+msgstr "Різноманітні налаштуваннÑ, що впливають на продуктивніÑть GitLab."
-msgid "View and edit lines"
-msgstr "ПереглÑнути та відредагувати Ñ€Ñдки"
-
-msgid "View epics list"
-msgstr "ПереглÑнути ÑпиÑок епіків"
+msgid "Verified"
+msgstr "Підтверджено"
msgid "View file @ "
msgstr "ПереглÑд файла @ "
@@ -4655,9 +5037,15 @@ msgstr "ПереглÑд файла @ "
msgid "View group labels"
msgstr "ПереглÑнути мітки групи"
+msgid "View jobs"
+msgstr "ПереглÑнути завданнÑ"
+
msgid "View labels"
msgstr "ПереглÑнути мітки"
+msgid "View log"
+msgstr "ПереглÑнути журнал"
+
msgid "View open merge request"
msgstr "ПереглÑнути відкритий запит на злиттÑ"
@@ -4668,7 +5056,7 @@ msgid "View replaced file @ "
msgstr "ПереглÑд заміненого файлу @ "
msgid "Visibility and access controls"
-msgstr ""
+msgstr "ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð²Ð¸Ð´Ð¸Ð¼Ð¾Ñті та доÑтупу"
msgid "VisibilityLevel|Internal"
msgstr "Внутрішній"
@@ -4685,9 +5073,6 @@ msgstr "Ðевідомий"
msgid "Want to see the data? Please ask an administrator for access."
msgstr "Хочете побачити дані? Будь лаÑка, попроÑить у адмініÑтратора доÑтуп."
-msgid "We could not verify that one of your projects on GCP has billing enabled. Please try again."
-msgstr "Ми не змогли перевірити, що один із ваших проектів в GCP має ввімкнений білінг. Будь лаÑка, Ñпробуйте ще раз."
-
msgid "We don't have enough data to show this stage."
msgstr "Ми не маємо доÑтатньо даних Ð´Ð»Ñ Ð²Ñ–Ð´Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð½Ñ Ñ†Ñ–Ñ”Ñ— Ñтадії."
@@ -4698,16 +5083,13 @@ msgid "Web IDE"
msgstr "Веб-IDE"
msgid "Web terminal"
-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 Ñкщо, наприклад, був відправлений новий код або Ñтворено нову проблему. Ви можете налаштувати його так, щоб він реагував на певні події (відправки коду, проблеми або запити на злиттÑ). Групові веб-гуки заÑтоÑовуютьÑÑ Ð´Ð¾ вÑÑ–Ñ… проектів в групі Ñ– дозволÑють вам Ñтандартизувати Ñ—Ñ… Ð´Ð»Ñ Ð²Ñієї вашої групи."
+msgstr "Веб-термінал"
-msgid "Weight"
-msgstr "Вага"
+msgid "When a runner is locked, it cannot be assigned to other projects"
+msgstr "Коли runner закріплений (за проектами), його не можна викориÑтовувати в інших проектах"
-msgid "When leaving the URL blank, classification labels can still be specified whitout disabling cross project features or performing external authorization checks."
-msgstr ""
+msgid "When enabled, users cannot use GitLab until the terms have been accepted."
+msgstr "Якщо увімкнено, кориÑтувачі не можуть викориÑтовувати GitLab, поки вони не приймуть умови."
msgid "Wiki"
msgstr "Wiki"
@@ -4733,8 +5115,32 @@ msgstr "Порада: Ви можете переміÑтити цю Ñторін
msgid "WikiEdit|There is already a page with the same title in that path."
msgstr "Вже Ñ–Ñнує Ñторінка з таким шлÑхом Ñ– заголовком."
-msgid "WikiEmptyPageError|You are not allowed to create wiki pages"
-msgstr "Ви не можете Ñтворювати wiki-Ñторінки"
+msgid "WikiEmptyIssueMessage|Suggest wiki improvement"
+msgstr "Запропонувати Ð¿Ð¾ÐºÑ€Ð°Ñ‰ÐµÐ½Ð½Ñ wiki"
+
+msgid "WikiEmptyIssueMessage|You must be a project member in order to add wiki pages. If you have suggestions for how to improve the wiki for this project, consider opening an issue in the %{issues_link}."
+msgstr "Ви маєте бути учаÑником проекту Ð´Ð»Ñ Ñ‚Ð¾Ð³Ð¾, щоб додавати wiki Ñторінки. Якщо у Ð²Ð°Ñ Ñ” пропозиції ÑтоÑовно Ð¿Ð¾ÐºÑ€Ð°Ñ‰ÐµÐ½Ð½Ñ wiki цього проекту, відкрийте проблему в %{issues_link}."
+
+msgid "WikiEmptyIssueMessage|issue tracker"
+msgstr "РеєÑтр проблем"
+
+msgid "WikiEmpty|A wiki is where you can store all the details about your project. This can include why you've created it, its principles, how to use it, and so on."
+msgstr "Wiki дозволÑÑ” зберігати інформацію про ваш проект. Ðаприклад причину ÑтвореннÑ, принципи, Ñк його викориÑтовувати Ñ– Ñ‚. д."
+
+msgid "WikiEmpty|Create your first page"
+msgstr "Створіть вашу першу Ñторінку"
+
+msgid "WikiEmpty|Suggest wiki improvement"
+msgstr "Запропонувати Ð¿Ð¾ÐºÑ€Ð°Ñ‰ÐµÐ½Ð½Ñ wiki"
+
+msgid "WikiEmpty|The wiki lets you write documentation for your project"
+msgstr "Wiki дозволÑють пиÑати документацію Ð´Ð»Ñ Ð²Ð°ÑˆÐ¾Ð³Ð¾ проекту"
+
+msgid "WikiEmpty|This project has no wiki pages"
+msgstr "Цей проект не має wiki Ñторінок"
+
+msgid "WikiEmpty|You must be a project member in order to add wiki pages."
+msgstr "Ви повинні бути учаÑником проекту Ð´Ð»Ñ Ñ‚Ð¾Ð³Ð¾, щоб додавати wiki Ñторінки."
msgid "WikiHistoricalPage|This is an old version of this page."
msgstr "Це — Ñтара верÑÑ–Ñ Ñторінки."
@@ -4769,6 +5175,12 @@ msgstr "Ðова wiki-Ñторінка"
msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?"
msgstr "Ви дійÑно бажаєте видалити цю Ñторінку?"
+msgid "WikiPageConfirmDelete|Delete page"
+msgstr "Видалити Ñторінку"
+
+msgid "WikiPageConfirmDelete|Delete page %{pageTitle}?"
+msgstr "Видалити Ñторінку %{pageTitle}?"
+
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} Ñ– переконайтеÑÑ, ваші зміни не затруть зміни інших."
@@ -4784,8 +5196,8 @@ msgstr "Оновити %{page_title}"
msgid "WikiPage|Page slug"
msgstr "ШлÑÑ… Ñторінки"
-msgid "WikiPage|Write your content or drag files here..."
-msgstr "Ðапишіть текÑÑ‚ або перетÑгніть файли Ñюди..."
+msgid "WikiPage|Write your content or drag files here…"
+msgstr "Ðапишіть текÑÑ‚ або перетÑгніть файли Ñюди…"
msgid "Wiki|Create Page"
msgstr "Створити Ñторінку"
@@ -4796,9 +5208,6 @@ msgstr "Створити Ñторінку"
msgid "Wiki|Edit Page"
msgstr "Ð ÐµÐ´Ð°Ð³ÑƒÐ²Ð°Ð½Ð½Ñ cторінки"
-msgid "Wiki|Empty page"
-msgstr "ÐŸÐ¾Ñ€Ð¾Ð¶Ð½Ñ Ñторінка"
-
msgid "Wiki|More Pages"
msgstr "Більше Ñторінок"
@@ -4817,14 +5226,11 @@ 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 "СкаÑувати запит доÑтупу"
-msgid "Write a commit message..."
-msgstr "Ðапишіть коміт-повідомленнÑ..."
+msgid "Yes"
+msgstr "Так"
msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
msgstr "Ви хочете видалити %{group_name}. Видалені групи ÐЕ МОЖÐРбуду відновити! Ви ÐБСОЛЮТÐО впевнені?"
@@ -4839,9 +5245,9 @@ msgid "You are going to transfer %{project_full_name} to another owner. Are you
msgstr "Ви збираєтеÑÑ Ð¿ÐµÑ€ÐµÐ´Ð°Ñ‚Ð¸ проект %{project_full_name} іншому влаÑнику. Ви ÐБСОЛЮТÐО впевнені?"
msgid "You are on a read-only GitLab instance."
-msgstr ""
+msgstr "Ви знаходитеÑÑ Ð½Ð° інÑтанÑÑ– Gitlab \"тільки Ð´Ð»Ñ Ñ‡Ð¸Ñ‚Ð°Ð½Ð½Ñ\"."
-msgid "You are on a secondary (read-only) Geo node. If you want to make any changes, you must visit the %{primary_node}."
+msgid "You can %{linkStart}view the blob%{linkEnd} instead."
msgstr ""
msgid "You can also create a project from the command line."
@@ -4850,6 +5256,9 @@ msgstr "Ви також можете Ñтворити проект із кома
msgid "You can also star a label to make it a priority label."
msgstr "Ви можете додати мітку в обрані, щоб зробити її пріоритетною."
+msgid "You can also test your .gitlab-ci.yml in the %{linkStart}Lint%{linkEnd}"
+msgstr "Ви також можете перевірити Ñвій .gitlab-ci.yml за допомогою %{linkStart}Lint%{linkEnd}"
+
msgid "You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}"
msgstr "Ви можете легко вÑтановити Runner на клаÑтері Kubernetes. %{link_to_help_page}"
@@ -4862,30 +5271,33 @@ msgstr "Ви можете додавати файли тільки коли пе
msgid "You can only edit 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 can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}"
+msgstr "Ви можете розв’Ñзати цей конфлікт Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð·Ð° допомогою інтерактивного режиму (викориÑтовуючи кнопки %{use_ours} та %{use_theirs}), або безпоÑередньо редагуючи файли. Закомітити зміни у %{branch_name}"
msgid "You cannot write to this read-only GitLab instance."
msgstr "Ви не можете запиÑувати на цей \"тільки Ð´Ð»Ñ Ñ‡Ð¸Ñ‚Ð°Ð½Ð½Ñ\" інÑÑ‚Ð°Ð½Ñ GitLab."
-msgid "You do not have the correct permissions to override the settings from the LDAP group sync."
-msgstr "У Ð²Ð°Ñ Ð½ÐµÐ¼Ð°Ñ” необхідних прав доÑтупу, щоб перевизначити Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ñинхронізації LDAP-груп."
+msgid "You do not have any assigned merge requests"
+msgstr "У Ð²Ð°Ñ Ð½ÐµÐ¼Ð°Ñ” призначених запитів на злиттÑ"
msgid "You have no permissions"
msgstr "У Ð²Ð°Ñ Ð½ÐµÐ¼Ð°Ñ” прав доÑтупу"
+msgid "You have not created any merge requests"
+msgstr "Ви ще не Ñтворювали запитів на злиттÑ"
+
msgid "You have reached your project limit"
msgstr "Ви доÑÑгли Ð¾Ð±Ð¼ÐµÐ¶ÐµÐ½Ð½Ñ Ð² вашому проекті"
-msgid "You must have master access to force delete a lock"
-msgstr "У Ð²Ð°Ñ Ð¿Ð¾Ð²Ð¸Ð½ÐµÐ½ бути доÑтуп на рівні керівника, Ð´Ð»Ñ Ð¿Ñ€Ð¸Ð¼ÑƒÑового Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð±Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ"
+msgid "You must accept our Terms of Service and privacy policy in order to register an account"
+msgstr "Ви повинні прийнÑти правила кориÑÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ ÑервіÑом Ñ– політику конфіденційноÑті Ð´Ð»Ñ Ñ‚Ð¾Ð³Ð¾, щоб Ñтворити обліковий запиÑ"
+
+msgid "You must have maintainer access to force delete a lock"
+msgstr "Ви повинні мати доÑтуп керівника Ð´Ð»Ñ Ð¿Ñ€Ð¸Ð¼ÑƒÑового Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð±Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ "
msgid "You must sign in to star a project"
msgstr "Ðеобхідно увійти, щоб оцінити проект"
-msgid "You need a different license to enable FileLocks feature"
-msgstr "Ð”Ð»Ñ Ð°ÐºÑ‚Ð¸Ð²Ð°Ñ†Ñ–Ñ— функції Ð‘Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ð¤Ð°Ð¹Ð»Ñ–Ð² вам потрібна інша ліцензіÑ"
-
msgid "You need permission."
msgstr "Вам потрібен дозвіл"
@@ -4917,22 +5329,22 @@ msgid "You'll need to use different branch names to get a valid comparison."
msgstr "Вам необхідно викориÑтовувати різні імена гілок Ð´Ð»Ñ ÐºÐ¾Ñ€ÐµÐºÑ‚Ð½Ð¾Ð³Ð¾ порівнÑннÑ."
msgid "You're receiving this email because of your account on %{host}. %{manage_notifications_link} &middot; %{help_link}"
-msgstr ""
+msgstr "Ви отримали це Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ñ‡ÐµÑ€ÐµÐ· ваш обліковий Ð·Ð°Ð¿Ð¸Ñ Ð½Ð° %{host}. %{manage_notifications_link} &middot; %{help_link}"
msgid "Your Groups"
-msgstr ""
+msgstr "Ваші групи"
msgid "Your Kubernetes cluster information on this page is still editable, but you are advised to disable and reconfigure"
msgstr "Ð†Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ñ–Ñ Ð¿Ñ€Ð¾ ваш Kubernetes-клаÑтер вÑе ще доÑтупна Ð´Ð»Ñ Ñ€ÐµÐ´Ð°Ð³ÑƒÐ²Ð°Ð½Ð½Ñ Ð½Ð° цій Ñторінці, але ми радимо вимкнути Ñ– повторно налаштувати"
msgid "Your Projects (default)"
-msgstr ""
+msgstr "Ваші проекти (за замовчуваннÑм)"
msgid "Your Projects' Activity"
-msgstr ""
+msgstr "ÐктивніÑть ваших проектів"
msgid "Your Todos"
-msgstr ""
+msgstr "Ваші Задачі"
msgid "Your changes can be committed to %{branch_name} because a merge request is open."
msgstr "Ваші зміни можуть бути закомічені до %{branch_name}, оÑкільки запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð²Ñ–Ð´ÐºÑ€Ð¸Ñ‚Ð¸Ð¹."
@@ -4952,15 +5364,11 @@ msgstr "Ваше ім'Ñ"
msgid "Your projects"
msgstr "Ваші проекти"
-msgid "among other things"
-msgstr "між іншим"
+msgid "ago"
+msgstr "тому"
-msgid "and %d fixed vulnerability"
-msgid_plural "and %d fixed vulnerabilities"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
+msgid "among other things"
+msgstr "тощо"
msgid "assign yourself"
msgstr "призначити Ñебе"
@@ -4968,96 +5376,12 @@ msgstr "призначити Ñебе"
msgid "branch name"
msgstr "ім'Ñ Ð³Ñ–Ð»ÐºÐ¸"
-msgid "by"
-msgstr "від"
-
-msgid "ciReport|%{type} detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|%{type} detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Code quality"
-msgstr "ЯкіÑть коду"
-
-msgid "ciReport|DAST detected no alerts by analyzing the review app"
-msgstr "DAST не виÑвив попереджень при аналізі цього review app"
-
-msgid "ciReport|Dependency scanning"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Failed to load %{reportName} report"
-msgstr "Помилка при завантаженні звіту %{reportName}"
-
-msgid "ciReport|Fixed:"
-msgstr "Виправлено:"
-
-msgid "ciReport|Instances"
-msgstr "ІнÑтанÑи"
-
-msgid "ciReport|Learn more about whitelisting"
-msgstr ""
-
-msgid "ciReport|Loading %{reportName} report"
-msgstr "Ð—Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ Ð·Ð²Ñ–Ñ‚Ñƒ %{reportName}"
-
-msgid "ciReport|No changes to code quality"
-msgstr "Ðемає змін у ÑкоÑті коду"
-
-msgid "ciReport|No changes to performance metrics"
-msgstr "Ðемає змін у показниках продуктивноÑті"
-
-msgid "ciReport|Performance metrics"
-msgstr "Показники продуктивноÑті"
-
-msgid "ciReport|SAST"
-msgstr "SAST"
-
-msgid "ciReport|SAST detected"
-msgstr "SAST виÑвив"
-
-msgid "ciReport|SAST detected no new security vulnerabilities"
-msgstr "SAST не виÑвив нових вразливоÑтей"
-
-msgid "ciReport|SAST detected no security vulnerabilities"
-msgstr "SAST не виÑвив жодних вразливоÑтей"
-
-msgid "ciReport|SAST:container no vulnerabilities were found"
-msgstr ""
-
-msgid "ciReport|Security scanning"
-msgstr ""
-
-msgid "ciReport|Security scanning failed loading any results"
-msgstr ""
-
-msgid "ciReport|Show complete code vulnerabilities report"
-msgstr "Показати повний звіт про вразливоÑті в коді"
-
-msgid "ciReport|Unapproved vulnerabilities (red) can be marked as approved. %{helpLink}"
-msgstr "Ðезатверджені вразливоÑті (червоні) можуть бути відмічені Ñк затверджені. %{helpLink}"
-
-msgid "ciReport|no vulnerabilities"
-msgstr ""
-
msgid "command line instructions"
msgstr "інÑтрукції Ð´Ð»Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´Ð½Ð¾Ð³Ð¾ Ñ€Ñдка"
msgid "connecting"
msgstr "з'єднаннÑ"
-msgid "could not read private key, is the passphrase correct?"
-msgstr ""
-
msgid "day"
msgid_plural "days"
msgstr[0] "день"
@@ -5065,46 +5389,26 @@ msgstr[1] "дні"
msgstr[2] "днів"
msgstr[3] "днів"
-msgid "detected %d fixed vulnerability"
-msgid_plural "detected %d fixed vulnerabilities"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
+msgid "deploy token"
+msgstr "токен Ð´Ð»Ñ Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ"
-msgid "detected %d new vulnerability"
-msgid_plural "detected %d new vulnerabilities"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
+msgid "disabled"
+msgstr "вимкнено"
-msgid "detected no vulnerabilities"
-msgstr ""
+msgid "enabled"
+msgstr "увімкнено"
msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
msgstr "%{slash_command} перезезапиÑує запланований Ñ‡Ð°Ñ Ð¾Ñтаннім значеннÑм."
-msgid "here"
-msgstr "тут"
+msgid "for this project"
+msgstr "Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ проекту"
msgid "importing"
msgstr "імпорт"
-msgid "in progress"
-msgstr ""
-
-msgid "is invalid because there is downstream lock"
-msgstr "неправильний через наÑвніÑть блокувань на нижчих рівнÑÑ…"
-
-msgid "is invalid because there is upstream lock"
-msgstr "неправильний через наÑвніÑть блокувань на вищих рівнÑÑ…"
-
-msgid "is not a valid X509 certificate."
-msgstr ""
-
-msgid "locked by %{path_lock_user_name} %{created_at}"
-msgstr "заблоковано %{path_lock_user_name} %{created_at}"
+msgid "latest version"
+msgstr "оÑÑ‚Ð°Ð½Ð½Ñ Ð²ÐµÑ€ÑÑ–Ñ"
msgid "merge request"
msgid_plural "merge requests"
@@ -5117,37 +5421,16 @@ msgid "mrWidget| Please restore it or use a different %{missingBranchName} branc
msgstr "Будь лаÑка відновіть Ñ—Ñ— або викориÑтовуйте іншу %{missingBranchName} гілку"
msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} decreased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB"
-msgstr ""
+msgstr "ВикориÑÑ‚Ð°Ð½Ð½Ñ %{metricsLinkStart} пам’Ñті %{metricsLinkEnd} %{emphasisStart} впало %{emphasisEnd} з %{memoryFrom}Мб до %{memoryTo}Мб"
msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} increased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB"
-msgstr ""
+msgstr "ВикориÑÑ‚Ð°Ð½Ð½Ñ %{metricsLinkStart} пам’Ñті %{metricsLinkEnd} %{emphasisStart} зроÑло %{emphasisEnd} з %{memoryFrom}Мб до %{memoryTo}Мб"
msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage is %{emphasisStart} unchanged %{emphasisEnd} at %{memoryFrom}MB"
-msgstr ""
-
-msgid "mrWidget|Add approval"
-msgstr "Додати затвердженнÑ"
-
-msgid "mrWidget|Allows edits from maintainers"
-msgstr "Дозволити Ñ€ÐµÐ´Ð°Ð³ÑƒÐ²Ð°Ð½Ð½Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´Ð¾ÑŽ проекту"
-
-msgid "mrWidget|An error occured while removing your approval."
-msgstr "Під Ñ‡Ð°Ñ Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð²Ð°ÑˆÐ¾Ð³Ð¾ Ð·Ð°Ñ‚Ð²ÐµÑ€Ð´Ð¶ÐµÐ½Ð½Ñ ÑталаÑÑ Ð¿Ð¾Ð¼Ð¸Ð»ÐºÐ°."
-
-msgid "mrWidget|An error occured while retrieving approval data for this merge request."
-msgstr "Помилка при отриманні даних про Ð·Ð°Ñ‚Ð²ÐµÑ€Ð´Ð¶ÐµÐ½Ð½Ñ Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ запиту на злиттÑ."
-
-msgid "mrWidget|An error occured while submitting your approval."
-msgstr "Помилка при обробці вашого затвердженнÑ."
-
-msgid "mrWidget|Approve"
-msgstr "Затвердити"
+msgstr "ВикориÑÑ‚Ð°Ð½Ð½Ñ %{metricsLinkStart} пам’Ñті %{metricsLinkEnd} %{emphasisStart} не змінилоÑÑ %{emphasisEnd} %{memoryFrom}Мб"
-msgid "mrWidget|Approved"
-msgstr ""
-
-msgid "mrWidget|Approved by"
-msgstr ""
+msgid "mrWidget|Allows commits from members who can merge to the target branch"
+msgstr "ДозволÑÑ” коміти від учаÑників, Ñкі можуть зливати до цільової гілки"
msgid "mrWidget|Cancel automatic merge"
msgstr "СкаÑувати автоматичне злиттÑ"
@@ -5173,8 +5456,11 @@ msgstr "Закритий"
msgid "mrWidget|Closes"
msgstr "Закриває"
+msgid "mrWidget|Create an issue to resolve them later"
+msgstr "Створіть проблему, щоб вирішити їх пізніше"
+
msgid "mrWidget|Deployment statistics are not available currently"
-msgstr ""
+msgstr "СтатиÑтика Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ð½Ð°Ñ€Ð°Ð·Ñ– недоÑтупна"
msgid "mrWidget|Did not close"
msgstr "Ðе закрив"
@@ -5183,7 +5469,7 @@ msgid "mrWidget|Email patches"
msgstr "Email-патчі"
msgid "mrWidget|Failed to load deployment statistics"
-msgstr ""
+msgstr "Ðе вдалоÑÑ Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶Ð¸Ñ‚Ð¸ ÑтатиÑтику розгортаннÑ"
msgid "mrWidget|If the %{branch} branch exists in your local repository, you can merge this merge request manually using the"
msgstr "Якщо гілка %{branch} Ñ–Ñнує у вашому локальному репозиторії, то ви можете заÑтоÑувати цей запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð²Ñ€ÑƒÑ‡Ð½Ñƒ за допомогою"
@@ -5192,7 +5478,7 @@ msgid "mrWidget|If the %{missingBranchName} branch exists in your local reposito
msgstr "Якщо гілка %{missingBranchName} Ñ–Ñнує у вашому локальному репозиторії, то ви можете заÑтоÑувати цей запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð²Ñ€ÑƒÑ‡Ð½Ñƒ за допомогою командного Ñ€Ñдка"
msgid "mrWidget|Loading deployment statistics"
-msgstr ""
+msgstr "Ð—Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ ÑтатиÑтики розгортаннÑ"
msgid "mrWidget|Mentions"
msgstr "Згадки"
@@ -5227,9 +5513,6 @@ msgstr "Видалити гілку-джерело"
msgid "mrWidget|Remove source branch"
msgstr "Видалити гілку-джерело"
-msgid "mrWidget|Remove your approval"
-msgstr "Видалити ваше затвердженнÑ"
-
msgid "mrWidget|Request to merge"
msgstr "Запит на злиттÑ"
@@ -5269,6 +5552,9 @@ msgstr "Гілку-джерело не буде видалено"
msgid "mrWidget|There are merge conflicts"
msgstr "Ñ–Ñнують конфлікти при злитті"
+msgid "mrWidget|There are unresolved discussions. Please resolve these discussions"
+msgstr "ПриÑутні незавершені обговореннÑ. Будь лаÑка завершіть Ñ—Ñ…"
+
msgid "mrWidget|This merge request failed to be merged automatically"
msgstr "ВідбулаÑÑ Ð¿Ð¾Ð¼Ð¸Ð»ÐºÐ° при автоматичному злитті цього запиту"
@@ -5279,7 +5565,7 @@ msgid "mrWidget|This project is archived, write access has been disabled"
msgstr "Цей проект заархівований, доÑтуп до запиÑу було відключено"
msgid "mrWidget|Web IDE"
-msgstr ""
+msgstr "Веб-IDE"
msgid "mrWidget|You can merge this merge request manually using the"
msgstr "Ви можете прийнÑти цей запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð²Ñ€ÑƒÑ‡Ð½Ñƒ за допомогою"
@@ -5321,8 +5607,8 @@ msgstr "пароль"
msgid "personal access token"
msgstr "оÑобиÑтий токен доÑтупу"
-msgid "private key does not match certificate."
-msgstr ""
+msgid "remaining"
+msgstr "залишилоÑÑŒ"
msgid "remove due date"
msgstr "видалити заплановану дату завершеннÑ"
@@ -5336,9 +5622,6 @@ msgstr "%{slash_command} оновлює Ñуму витраченого чаÑу
msgid "this document"
msgstr "цей документ"
-msgid "to help your contributors communicate effectively!"
-msgstr "щоб допомогти учаÑникам ефективно ÑпілкуватиÑÑ!"
-
msgid "username"
msgstr "ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача"
@@ -5348,3 +5631,10 @@ msgstr "викориÑтовує клаÑтери Kubernetes Ð´Ð»Ñ Ñ€Ð¾Ð·Ð³Ð¾Ñ€
msgid "with %{additions} additions, %{deletions} deletions."
msgstr "з %{additions} додаваннÑми Ñ– %{deletions} видаленнÑми."
+msgid "within %d minute "
+msgid_plural "within %d minutes "
+msgstr[0] "протÑгом %d хвилини "
+msgstr[1] "протÑгом %d хвилин "
+msgstr[2] "протÑгом %d хвилин "
+msgstr[3] "протÑгом %d хвилин "
+
diff --git a/locale/zh_CN/gitlab.po b/locale/zh_CN/gitlab.po
index c25e892e568..cf9a8a37638 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: 2018-04-04 19:35+0200\n"
-"PO-Revision-Date: 2018-04-05 03:36-0400\n"
+"POT-Creation-Date: 2018-07-01 16:35+1000\n"
+"PO-Revision-Date: 2018-07-01 11:02\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Chinese Simplified\n"
"Language: zh_CN\n"
@@ -16,8 +16,9 @@ msgstr ""
"X-Crowdin-Language: zh-CN\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-msgid " and"
-msgstr " 和"
+msgid "%d changed file"
+msgid_plural "%d changed files"
+msgstr[0] ""
msgid "%d commit"
msgid_plural "%d commits"
@@ -47,6 +48,14 @@ msgid "%d metric"
msgid_plural "%d metrics"
msgstr[0] "%d 指标"
+msgid "%d staged change"
+msgid_plural "%d staged changes"
+msgstr[0] "%d个已暂存的修改"
+
+msgid "%d unstaged change"
+msgid_plural "%d unstaged changes"
+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 次æäº¤ã€‚"
@@ -61,12 +70,21 @@ msgid "%{count} participant"
msgid_plural "%{count} participants"
msgstr[0] "%{count} ä½å‚与者"
+msgid "%{filePath} deleted"
+msgstr ""
+
+msgid "%{group_docs_link_start}Groups%{group_docs_link_end} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects."
+msgstr ""
+
msgid "%{loadingIcon} Started"
msgstr "%{loadingIcon} 已开始"
msgid "%{lock_path} is locked by GitLab User %{lock_user_id}"
msgstr "%{lock_path} 被GitLab用户 %{lock_user_id} é”定"
+msgid "%{nip_domain} can be used as an alternative to a custom domain."
+msgstr "%{nip_domain} å¯ä»¥æ›¿ä»£è‡ªå®šä¹‰åŸŸä½¿ç”¨ã€‚"
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr "%{number_commits_behind} 个è½åŽ %{default_branch} 分支的æäº¤, %{number_commits_ahead} æ—©è¶…å‰çš„æäº¤"
@@ -74,11 +92,14 @@ msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will allow
msgstr "已失败 %{number_of_failures} 次/最多å…许失败失败 %{maximum_failures} 次,GitLab 将继续é‡è¯•。"
msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will not retry automatically. Reset storage information when the problem is resolved."
-msgstr "已失败 %{number_of_failures} 次/最多å…许失败 %{maximum_failures} 次,GitLab ä¸ä¼šç»§ç»­è‡ªåЍé‡è¯•。请在问题解决åŽé‡ç½®å­˜å‚¨å¥åº·ä¿¡æ¯ã€‚"
+msgstr "已失败 %{number_of_failures} 次/最多å…许失败 %{maximum_failures} 次,GitLab ä¸ä¼šç»§ç»­è‡ªåЍé‡è¯•。请在问题解决åŽé‡ç½®å­˜å‚¨è¿è¡ŒçŠ¶å†µä¿¡æ¯ã€‚"
msgid "%{openOrClose} %{noteable}"
msgstr "%{openOrClose} %{noteable}"
+msgid "%{percent}%% complete"
+msgstr "å·²å®Œæˆ %{percent}%%"
+
msgid "%{storage_name}: failed storage access attempt on host:"
msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:"
msgstr[0] "%{storage_name}:已 %{failed_attempts} 次å°è¯•访问存储失败:"
@@ -86,15 +107,55 @@ msgstr[0] "%{storage_name}:已 %{failed_attempts} 次å°è¯•访问存储失败ï
msgid "%{text} is available"
msgstr "%{text}å¯ç”¨"
+msgid "%{title} changes"
+msgstr "%{title}更改"
+
+msgid "%{unstaged} unstaged and %{staged} staged changes"
+msgstr "%{unstaged}个未暂存的更改åŠ%{staged}个已暂存的更改"
+
msgid "(checkout the %{link} for information on how to install it)."
msgstr "(如需了解更多的安装信æ¯ï¼Œè¯·æŸ¥çœ‹ %{link})"
msgid "+ %{moreCount} more"
-msgstr "+ 还有 %{moreCount} æ¡"
+msgstr "+ 其余 %{moreCount} 项"
+
+msgid "- Runner is active and can process any new jobs"
+msgstr "- Runnerå·²å¯ç”¨ï¼Œéšæ—¶å¯ä»¥å¤„ç†æ–°ä½œä¸š"
+
+msgid "- Runner is paused and will not receive any new jobs"
+msgstr "- Runner已暂åœï¼Œæš‚æ—¶ä¸ä¼šæŽ¥å—新的作业"
msgid "- show less"
msgstr "- 显示较少"
+msgid "1 %{type} addition"
+msgid_plural "%{count} %{type} additions"
+msgstr[0] "%{count} 个 %{type} 的添加"
+
+msgid "1 %{type} modification"
+msgid_plural "%{count} %{type} modifications"
+msgstr[0] "%{count} 个 %{type} 的修改"
+
+msgid "1 closed issue"
+msgid_plural "%d closed issues"
+msgstr[0] "%d 个已关闭的议题"
+
+msgid "1 closed merge request"
+msgid_plural "%d closed merge requests"
+msgstr[0] "%d 个已关闭的åˆå¹¶è¯·æ±‚"
+
+msgid "1 merged merge request"
+msgid_plural "%d merged merge requests"
+msgstr[0] "%d 个已åˆå¹¶çš„åˆå¹¶è¯·æ±‚"
+
+msgid "1 open issue"
+msgid_plural "%d open issues"
+msgstr[0] "%d 个开å¯ä¸­çš„议题"
+
+msgid "1 open merge request"
+msgid_plural "%d open merge requests"
+msgstr[0] "%d 个开å¯ä¸­çš„åˆå¹¶è¯·æ±‚"
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] "%d æ¡æµæ°´çº¿"
@@ -105,9 +166,27 @@ msgstr "最高贡献"
msgid "2FA enabled"
msgstr "å¯ç”¨ä¸¤æ­¥éªŒè¯"
+msgid "403|Please contact your GitLab administrator to get the permission."
+msgstr "请è”系您的 GitLab 管ç†å‘˜èŽ·å–访问æƒé™ã€‚"
+
+msgid "403|You don't have the permission to access this page."
+msgstr "您没有æƒé™è®¿é—®æ­¤é¡µé¢ã€‚"
+
+msgid "404|Make sure the address is correct and the page hasn't moved."
+msgstr "è¯·ç¡®è®¤æ‚¨è®¿é—®çš„åœ°å€æ­£ç¡®å¹¶ä¸”页颿œªè¢«ç§»åŠ¨ã€‚"
+
+msgid "404|Page Not Found"
+msgstr "页颿œªæ‰¾åˆ°"
+
+msgid "404|Please contact your GitLab administrator if you think this is a mistake."
+msgstr "如果您觉得这是一个错误的æç¤ºä¿¡æ¯ï¼Œè¯·è”系您的 GitLab 管ç†å‘˜ã€‚"
+
msgid "<strong>Removes</strong> source branch"
msgstr "<strong>删除</strong>æºåˆ†æ”¯"
+msgid "A 'Runner' is a process which runs a job. You can setup as many Runners as you need."
+msgstr "Runner是一个执行任务的进程。您å¯ä»¥æ ¹æ®éœ€è¦é…ç½®ä»»æ„æ•°é‡çš„Runner。"
+
msgid "A collection of graphs regarding Continuous Integration"
msgstr "æŒç»­é›†æˆæ•°æ®å›¾"
@@ -117,6 +196,9 @@ msgstr "将在派生(fork)项目中中创建一个新的分支, å¹¶å¼€å¯ä¸€ä¸ªæ
msgid "A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}."
msgstr "项目å¯ä»¥ç”¨äºŽå­˜æ”¾æ–‡ä»¶(仓库),安排计划(议题),并å‘布文档(wiki), %{among_other_things_link}。"
+msgid "A regular expression that will be used to find the test coverage output in the job trace. Leave blank to disable"
+msgstr ""
+
msgid "A user with write access to the source branch selected this option"
msgstr "具有对æºåˆ†æ”¯çš„写入æƒé™çš„用户选择了此选项"
@@ -127,38 +209,41 @@ msgid "Abuse Reports"
msgstr "滥用报告"
msgid "Abuse reports"
-msgstr ""
+msgstr "滥用报告"
+
+msgid "Accept terms"
+msgstr "æŽ¥å—æ¡æ¬¾"
msgid "Access Tokens"
msgstr "访问令牌"
msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again."
-msgstr "ä¸ºæ–¹ä¾¿ä¿®å¤æŒ‚载问题,访问故障存储已被暂时ç¦ç”¨ã€‚在问题解决åŽè¯·é‡ç½®å­˜å‚¨å¥åº·ä¿¡æ¯ï¼Œä»¥å…è®¸å†æ¬¡è®¿é—®ã€‚"
+msgstr "ä¸ºæ–¹ä¾¿ä¿®å¤æŒ‚载问题,访问故障存储已被暂时ç¦ç”¨ã€‚在问题解决åŽè¯·é‡ç½®å­˜å‚¨è¿è¡ŒçŠ¶å†µä¿¡æ¯ï¼Œä»¥å…è®¸å†æ¬¡è®¿é—®ã€‚"
+
+msgid "Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report."
+msgstr ""
msgid "Account"
msgstr "å¸å·"
-msgid "Account and limit settings"
-msgstr "叿ˆ·å’Œé™åˆ¶è®¾ç½®"
+msgid "Account and limit"
+msgstr "叿ˆ·å’Œé™åˆ¶"
msgid "Active"
msgstr "å¯ç”¨"
+msgid "Active Sessions"
+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 Kubernetes cluster"
msgstr "添加 Kubernetes 集群"
@@ -171,6 +256,9 @@ msgstr "添加自述文件"
msgid "Add new directory"
msgstr "添加目录"
+msgid "Add reaction"
+msgstr "添加回应"
+
msgid "Add todo"
msgstr "添加待办事项"
@@ -190,7 +278,7 @@ msgid "AdminArea|You’re about to stop all jobs.This will halt all current jobs
msgstr "您å³å°†åœæ­¢æ‰€æœ‰ä½œä¸šã€‚所有正在è¿è¡Œçš„éƒ½ä¼šè¢«åœæ­¢ã€‚"
msgid "AdminHealthPageLink|health page"
-msgstr "å¥åº·é¡µé¢"
+msgstr "è¿è¡Œçж况页é¢"
msgid "AdminProjects|Delete"
msgstr "删除"
@@ -240,29 +328,44 @@ msgstr "所有更改å‡å·²æäº¤"
msgid "All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings."
msgstr "ä»Žæ¨¡æ¿æˆ–导入时为空白项目将å¯ç”¨æ‰€æœ‰åŠŸèƒ½ï¼Œä½†å¯ä»¥åœ¨é¡¹ç›®è®¾ç½®ä¸­å°†å…¶ç¦ç”¨ã€‚"
-msgid "Allow edits from maintainers."
-msgstr "å…许上游项目维护人员进行编辑。"
+msgid "Allow commits from members who can merge to the target branch."
+msgstr "具有åˆå¹¶åˆ°ç›®æ ‡åˆ†æ”¯æƒé™çš„æˆå‘˜å…许æäº¤"
-msgid "Allow rendering of PlantUML diagrams in Asciidoc documents."
+msgid "Allow public access to pipelines and job details, including output logs and artifacts"
msgstr ""
+msgid "Allow rendering of PlantUML diagrams in Asciidoc documents."
+msgstr "å…许在Asciidoc文档中渲染PlantUML图。"
+
msgid "Allow requests to the local network from hooks and services."
-msgstr ""
+msgstr "å…许æ¥è‡ªé’©å­å’ŒæœåŠ¡çš„å¯¹æœ¬åœ°ç½‘ç»œçš„è¯·æ±‚ã€‚"
msgid "Allows you to add and manage Kubernetes clusters."
msgstr "这里å¯ä»¥æ·»åŠ å’Œç®¡ç† Kubernetes 集群。"
-msgid "Also called \"Issuer\" or \"Relying party trust identifier\""
+msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
+msgstr "此外,也å¯ä»¥ä½¿ç”¨ %{personal_access_token_link}。创建Personal Access Token时,在范围中需选择 <code>repo</code> ,以便显示å¯ä¾›å¯¼å…¥å…¬å¼€å’Œç§æœ‰çš„仓库列表"
+
+msgid "An error occured creating the new branch."
msgstr ""
-msgid "Also called \"Relying party service URL\" or \"Reply URL\""
+msgid "An error occured whilst loading all the files."
msgstr ""
-msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to connect."
-msgstr "此外,也å¯ä»¥ä½¿ç”¨ %{personal_access_token_link}。创建Personal Access Token时,在范围中需选择 <code>repo</code> ,以便显示å¯ä¾›è¿žæŽ¥çš„å…¬å¼€å’Œç§æœ‰çš„仓库列表。"
+msgid "An error occured whilst loading the file content."
+msgstr ""
-msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
-msgstr "此外,也å¯ä»¥ä½¿ç”¨ %{personal_access_token_link}。创建Personal Access Token时,在范围中需选择 <code>repo</code> ,以便显示å¯ä¾›å¯¼å…¥å…¬å¼€å’Œç§æœ‰çš„仓库列表"
+msgid "An error occured whilst loading the file."
+msgstr ""
+
+msgid "An error occured whilst loading the merge request changes."
+msgstr ""
+
+msgid "An error occured whilst loading the merge request version data."
+msgstr ""
+
+msgid "An error occured whilst loading the merge request."
+msgstr ""
msgid "An error occurred previewing the blob"
msgstr "预览 blob 时出错"
@@ -270,14 +373,8 @@ msgstr "预览 blob 时出错"
msgid "An error occurred when toggling the notification subscription"
msgstr "切æ¢é€šçŸ¥è®¢é˜…æ—¶å‘生错误"
-msgid "An error occurred when updating the issue weight"
-msgstr "更新议题æƒé‡æ—¶å‘生错误"
-
-msgid "An error occurred while adding approver"
-msgstr "添加批准人时å‘生错误"
-
-msgid "An error occurred while detecting host keys"
-msgstr "检测主机密钥时å‘生错误"
+msgid "An error occurred while dismissing the alert. Refresh the page and try again."
+msgstr "消除警告时å‘生错误。请刷新页é¢å¹¶å†æ¬¡å°è¯•。"
msgid "An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again."
msgstr "关闭功能çªå‡ºæ˜¾ç¤ºæ—¶å‘生错误。请刷新页é¢å¹¶å†æ¬¡å°è¯•。"
@@ -294,11 +391,8 @@ msgstr "èŽ·å–æµæ°´çº¿æ—¶å‡ºé”™"
msgid "An error occurred while getting projects"
msgstr "获å–项目时å‘生错误"
-msgid "An error occurred while importing project"
-msgstr "在导入项目时å‘生错误。"
-
-msgid "An error occurred while initializing path locks"
-msgstr "åˆå§‹åŒ–è·¯å¾„é”æ—¶å‘生错误"
+msgid "An error occurred while importing project: ${details}"
+msgstr "在导入项目时å‘生错误:${details}"
msgid "An error occurred while loading commits"
msgstr "加载æäº¤æ—¶å‘生错误"
@@ -315,9 +409,6 @@ msgstr "加载文件时å‘生错误"
msgid "An error occurred while making the request."
msgstr "å‘é€è¯·æ±‚æ—¶å‘生错误。"
-msgid "An error occurred while removing approver"
-msgstr "删除批准者时å‘生错误"
-
msgid "An error occurred while rendering KaTeX"
msgstr "渲染KaTeXæ—¶å‘生错误"
@@ -330,9 +421,6 @@ msgstr "èŽ·å–æ—¥åŽ†æ´»åŠ¨æ—¶å‘生错误"
msgid "An error occurred while retrieving diff"
msgstr "获å–差异时å‘生错误"
-msgid "An error occurred while saving LDAP override status. Please try again."
-msgstr "ä¿å­˜LDAPè¦†ç›–çŠ¶æ€æ—¶å‘生错误。请å†è¯•一次。"
-
msgid "An error occurred while saving assignees"
msgstr "ä¿å­˜è¢«æŒ‡æ´¾äººæ—¶å‡ºçŽ°é”™è¯¯ã€‚"
@@ -342,9 +430,6 @@ msgstr "验è¯ç”¨æˆ·åæ—¶å‘生错误"
msgid "An error occurred. Please try again."
msgstr "å‘生了错误,请å†è¯•一次。"
-msgid "Any Label"
-msgstr "任何标记"
-
msgid "Appearance"
msgstr "外观"
@@ -357,20 +442,20 @@ msgstr "å››"
msgid "April"
msgstr "四月"
-msgid "Archived project! Repository is read-only"
-msgstr "项目已归档ï¼ä»“库为åªè¯»çжæ€"
+msgid "Archived project! Repository and other project resources are read-only"
+msgstr "已归档项目ï¼ä»“库和其他项目资æºå‡ä¸ºåªè¯»"
msgid "Are you sure you want to delete this pipeline schedule?"
msgstr "确定è¦åˆ é™¤æ­¤æµæ°´çº¿è®¡åˆ’å—?"
+msgid "Are you sure you want to remove this identity?"
+msgstr ""
+
msgid "Are you sure you want to reset registration token?"
msgstr "确定è¦é‡ç½®æ³¨å†Œä»¤ç‰Œå—?"
msgid "Are you sure you want to reset the health check token?"
-msgstr "确定è¦é‡ç½®å¥åº·æ£€æŸ¥ä»¤ç‰Œå—?"
-
-msgid "Are you sure you want to unlock %{path_lock_path}?"
-msgstr "确定è¦è§£é” %{path_lock_path} å—?"
+msgstr "确定è¦é‡ç½®è¿è¡ŒçŠ¶å†µæ£€æŸ¥ä»¤ç‰Œå—?"
msgid "Are you sure?"
msgstr "确定å—?"
@@ -378,8 +463,8 @@ msgstr "确定å—?"
msgid "Artifacts"
msgstr "产物"
-msgid "Assertion consumer service URL"
-msgstr ""
+msgid "Ask your group maintainer to setup a group Runner."
+msgstr "请群组维护者é…置一个群组级 Runner。"
msgid "Assign custom color like #FF0000"
msgstr "分é…自定义颜色,如FF0000"
@@ -402,9 +487,15 @@ msgstr "已分é…åˆå¹¶è¯·æ±‚"
msgid "Assigned to :name"
msgstr "已分é…至 :name"
+msgid "Assigned to me"
+msgstr "已分派给我"
+
msgid "Assignee"
msgstr "指派人"
+msgid "Assignee(s)"
+msgstr "指派"
+
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "拖放文件到此处或者 %{upload_link}"
@@ -427,7 +518,7 @@ msgid "Auto DevOps enabled"
msgstr "å¯ç”¨Auto DevOps"
msgid "Auto DevOps, runners and job artifacts"
-msgstr ""
+msgstr "Auto DevOps, runnersåŠä½œä¸šäº§ç‰©"
msgid "Auto Review Apps and Auto Deploy need a %{kubernetes} to work correctly."
msgstr "自动审阅程åºå’Œè‡ªåŠ¨éƒ¨ç½²ç¨‹åºéœ€è¦ %{kubernetes} æ‰èƒ½æ­£å¸¸å·¥ä½œã€‚"
@@ -438,8 +529,11 @@ msgstr "自动审阅程åºå’Œè‡ªåŠ¨éƒ¨ç½²ç¨‹åºéœ€è¦ä¸€ä¸ªåŸŸåå’Œ %{kubernete
msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
msgstr "自动审阅程åºå’Œè‡ªåŠ¨éƒ¨ç½²ç¨‹åºéœ€è¦ä¸€ä¸ªåŸŸåæ‰èƒ½æ­£å¸¸å·¥ä½œã€‚"
-msgid "AutoDevOps|Auto DevOps (Beta)"
-msgstr "DevOps 自动化(测试版)"
+msgid "Auto-cancel redundant, pending pipelines"
+msgstr ""
+
+msgid "AutoDevOps|Auto DevOps"
+msgstr "Auto DevOps"
msgid "AutoDevOps|Auto DevOps documentation"
msgstr "DevOps 自动化文档"
@@ -457,82 +551,112 @@ msgid "AutoDevOps|You can automatically build and test your application if you %
msgstr "如果当å‰é¡¹ç›®%{link_to_auto_devops_settings}, å¯ä»¥è‡ªåŠ¨çš„æž„å»ºå’Œæµ‹è¯•åº”ç”¨ã€‚å¦‚æžœå·²%{link_to_add_kubernetes_cluster},则也å¯ä»¥å®žçŽ°è‡ªåŠ¨éƒ¨ç½²ã€‚"
msgid "AutoDevOps|add a Kubernetes cluster"
-msgstr "添加Kubernetes群集"
+msgstr "添加Kubernetes集群"
-msgid "AutoDevOps|enable Auto DevOps (Beta)"
-msgstr "å¯ç”¨Auto DevOps (Beta)"
+msgid "AutoDevOps|enable Auto DevOps"
+msgstr "å¯ç”¨Auto DevOps"
msgid "Available"
msgstr "å¯ç”¨çš„"
+msgid "Available group Runners : %{runners}"
+msgstr "å¯ç”¨çš„群组Runner: %{runners}"
+
+msgid "Available group Runners : %{runners}."
+msgstr "å¯ç”¨çš„群组Runner: %{runners}."
+
msgid "Avatar will be removed. Are you sure?"
msgstr "å³å°†åˆ é™¤å¤´åƒã€‚确定继续å—?"
msgid "Average per day: %{average}"
msgstr "平凿¯å¤©: %{average}"
-msgid "Background Color"
+msgid "Background color"
msgstr ""
msgid "Background jobs"
-msgstr ""
+msgstr "åŽå°ä½œä¸š"
-msgid "Begin with the selected commit"
-msgstr "从选定的æäº¤å¼€å§‹"
+msgid "Badges"
+msgstr "徽章"
-msgid "Billing"
-msgstr "è´¦å•"
+msgid "Badges|A new badge was added."
+msgstr "新徽章已添加。"
-msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan."
-msgstr "%{group_name}ç›®å‰ä½äºŽ%{plan_link}计划中。"
+msgid "Badges|Add badge"
+msgstr "添加徽章"
-msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available."
-msgstr "自动é™çº§ã€å‡çº§åˆ°æŸäº›è®¡åˆ’ç›®å‰ä¸å¯ç”¨ã€‚"
+msgid "Badges|Adding the badge failed, please check the entered URLs and try again."
+msgstr "添加徽章失败,请检查输入的网å€å¹¶é‡è¯•。"
-msgid "BillingPlans|Current plan"
-msgstr "当å‰è®¡åˆ’"
+msgid "Badges|Badge image URL"
+msgstr "徽章图åƒç½‘å€"
-msgid "BillingPlans|Customer Support"
-msgstr "客户支æŒ"
+msgid "Badges|Badge image preview"
+msgstr "徽章图åƒé¢„览"
-msgid "BillingPlans|Downgrade"
-msgstr "é™çº§"
+msgid "Badges|Delete badge"
+msgstr "删除徽章"
-msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
-msgstr "请查阅%{faq_link} 进一步了解æ¯ä¸ªè®¡åˆ’的相关信æ¯ã€‚"
+msgid "Badges|Delete badge?"
+msgstr "删除徽章�"
-msgid "BillingPlans|Manage plan"
-msgstr "管ç†è®¡åˆ’"
+msgid "Badges|Deleting the badge failed, please try again."
+msgstr "删除徽章失败,请é‡è¯•。"
-msgid "BillingPlans|Please contact %{customer_support_link} in that case."
-msgstr "åœ¨è¿™ç§æƒ…况下请è”ç³»%{customer_support_link}。"
+msgid "Badges|Group Badge"
+msgstr "群组徽章"
-msgid "BillingPlans|See all %{plan_name} features"
-msgstr "查看所有%{plan_name}功能"
+msgid "Badges|Link"
+msgstr "链接"
-msgid "BillingPlans|This group uses the plan associated with its parent group."
-msgstr "该群组使用其父群组相关è”的计划。"
+msgid "Badges|No badge image"
+msgstr "无徽章图åƒ"
-msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
-msgstr "è¦ç®¡ç†æ­¤ç¾¤ç»„的计划,请访问%{parent_billing_page_link}的账å•部分。"
+msgid "Badges|No image to preview"
+msgstr "无图åƒå¯é¢„览"
-msgid "BillingPlans|Upgrade"
-msgstr "å‡çº§"
+msgid "Badges|Project Badge"
+msgstr "项目徽章"
+
+msgid "Badges|Reload badge image"
+msgstr "釿–°åŠ è½½å¾½ç« å›¾åƒ"
+
+msgid "Badges|Save changes"
+msgstr "ä¿å­˜æ›´æ”¹"
+
+msgid "Badges|Saving the badge failed, please check the entered URLs and try again."
+msgstr "ä¿å­˜å¾½ç« å¤±è´¥ï¼Œè¯·æ£€æŸ¥è¾“入的网å€å¹¶é‡è¯•。"
-msgid "BillingPlans|You are currently on the %{plan_link} plan."
-msgstr "æ‚¨ç›®å‰æ­£åœ¨ä½¿ç”¨%{plan_link}计划。"
+msgid "Badges|The %{docsLinkStart}variables%{docsLinkEnd} GitLab supports: %{placeholders}"
+msgstr "%{docsLinkStart}å˜é‡%{docsLinkEnd} GitLab支æŒï¼š %{placeholders}"
-msgid "BillingPlans|frequently asked questions"
-msgstr "常问问题"
+msgid "Badges|The badge was deleted."
+msgstr "徽章已删除。"
-msgid "BillingPlans|monthly"
-msgstr "æ¯æœˆ"
+msgid "Badges|The badge was saved."
+msgstr "徽章已ä¿å­˜ã€‚"
-msgid "BillingPlans|paid annually at %{price_per_year}"
-msgstr "æ¯å¹´æ”¯ä»˜ %{price_per_year}"
+msgid "Badges|This group has no badges"
+msgstr "当å‰ç¾¤ç»„无徽章"
-msgid "BillingPlans|per user"
-msgstr "æ¯ç”¨æˆ·"
+msgid "Badges|This project has no badges"
+msgstr "当å‰é¡¹ç›®æ— å¾½ç« "
+
+msgid "Badges|Your badges"
+msgstr "您的徽章"
+
+msgid "Begin with the selected commit"
+msgstr "从选定的æäº¤å¼€å§‹"
+
+msgid "Below are examples of regex for existing tools:"
+msgstr ""
+
+msgid "Boards"
+msgstr ""
+
+msgid "Branch %{branchName} was not found in this project's repository."
+msgstr ""
msgid "Branch (%{branch_count})"
msgid_plural "Branches (%{branch_count})"
@@ -610,8 +734,8 @@ msgstr "找ä¸åˆ°åˆ†æ”¯"
msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
msgstr "确认执行 %{delete_protected_branch} åŽå°†æ— æ³•撤销或æ¢å¤ã€‚"
-msgid "Branches|Only a project master or owner can delete a protected branch"
-msgstr "åªæœ‰é¡¹ç›®ç®¡ç†è€…或所有者æ‰èƒ½åˆ é™¤å—ä¿æŠ¤çš„åˆ†æ”¯ï¼"
+msgid "Branches|Only a project maintainer or owner can delete a protected branch"
+msgstr "åªæœ‰é¡¹ç›®ç»´æŠ¤è€…或所有者æ‰èƒ½åˆ é™¤å—ä¿æŠ¤çš„åˆ†æ”¯"
msgid "Branches|Overview"
msgstr "概览"
@@ -646,9 +770,6 @@ msgstr "éžæ´»è·ƒ"
msgid "Branches|Stale branches"
msgstr "éžæ´»è·ƒåˆ†æ”¯"
-msgid "Branches|The branch could not be updated automatically because it has diverged from its upstream counterpart."
-msgstr "分支ä¸èƒ½è‡ªåŠ¨æ›´æ–°ï¼Œå› ä¸ºå®ƒä¸Žä¸Šæ¸¸åˆ†æ”¯ä¸ä¸€è‡´ã€‚"
-
msgid "Branches|The default branch cannot be deleted"
msgstr "无法删除默认分支"
@@ -661,15 +782,9 @@ msgstr "为é¿å…æ•°æ®ä¸¢å¤±ï¼Œè¯·åœ¨åˆ é™¤ä¹‹å‰åˆå¹¶æ­¤åˆ†æ”¯ã€‚"
msgid "Branches|To confirm, type %{branch_name_confirmation}:"
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 "è¦æ”¾å¼ƒæœ¬åœ°æ›´æ”¹å¹¶è¦†ç›–上游版本的分支,请在此处将其删除,然åŽé€‰æ‹©ä¸Šé¢çš„â€œç«‹å³æ›´æ–°â€ã€‚"
-
msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
msgstr "å°†è¦æ°¸ä¹…删除å—ä¿æŠ¤çš„ %{branch_name} 分支。"
-msgid "Branches|diverged from upstream"
-msgstr "上游分支"
-
msgid "Branches|merged"
msgstr "å·²åˆå¹¶"
@@ -691,44 +806,83 @@ msgstr "æµè§ˆæ–‡ä»¶"
msgid "Browse files"
msgstr "æµè§ˆæ–‡ä»¶"
-msgid "Business"
-msgstr "业务"
-
msgid "ByAuthor|by"
msgstr "作者:"
msgid "CI / CD"
msgstr "CI / CD"
-msgid "CI/CD"
-msgstr "CI/CD"
+msgid "CI / CD Settings"
+msgstr ""
msgid "CI/CD configuration"
msgstr "CI/CD é…ç½®"
-msgid "CI/CD for external repo"
-msgstr "外部仓库的 CI/CD"
+msgid "CI/CD settings"
+msgstr "CI/CD 设置"
+
+msgid "CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery."
+msgstr "在开始使用æŒç»­é›†æˆå’ŒæŒç»­äº¤ä»˜ä¹‹å‰ï¼Œéœ€è¦æ˜Žç¡®æŒ‡å®š %{ci_file}。"
+
+msgid "CICD|Auto DevOps"
+msgstr "Auto DevOps"
+
+msgid "CICD|Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration."
+msgstr "Auto DevOps 将根æ®é¢„定义的æŒç»­é›†æˆå’ŒæŒç»­äº¤ä»˜é…ç½®è‡ªåŠ¨åŒ–åœ°æž„å»ºã€æµ‹è¯•和部署应用程åºã€‚"
+
+msgid "CICD|Automatic deployment to staging, manual deployment to production"
+msgstr "自动部署到预å‘布环境,手动部署到生产环境"
+
+msgid "CICD|Continuous deployment to production"
+msgstr "æŒç»­éƒ¨ç½²åˆ°ç”Ÿäº§çŽ¯å¢ƒ"
+
+msgid "CICD|Deployment strategy"
+msgstr "部署策略"
+
+msgid "CICD|Deployment strategy needs a domain name to work correctly."
+msgstr "部署策略需è¦ä¸€ä¸ªåŸŸåæ‰èƒ½æ­£å¸¸å·¥ä½œã€‚"
+
+msgid "CICD|Disable Auto DevOps"
+msgstr "ç¦ç”¨ Auto DevOps"
+
+msgid "CICD|Enable Auto DevOps"
+msgstr "å¯ç”¨Auto DevOps"
+
+msgid "CICD|Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific %{ci_file}."
+msgstr "项目ä¸å­˜åœ¨ %{ci_file} 时,将会使用系统默认设置å¯ç”¨æˆ–ç¦ç”¨Auto DevOps。"
+
+msgid "CICD|Instance default (%{state})"
+msgstr "系统默认 (%{state})"
msgid "CICD|Jobs"
msgstr "作业"
+msgid "CICD|Learn more about Auto DevOps"
+msgstr "了解更多Auto DevOps的相关信æ¯"
+
+msgid "CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project."
+msgstr "当项目中没有 %{ci_file} 时,将使用 Auto DevOpsæµæ°´çº¿é…置。"
+
+msgid "CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages."
+msgstr "如需使用自动化应用程åºè¯„审和自动部署,请指定域å。"
+
+msgid "Can't find HEAD commit for this branch"
+msgstr ""
+
msgid "Cancel"
msgstr "å–æ¶ˆ"
+msgid "Cancel this job"
+msgstr "å–æ¶ˆæ­¤ä½œä¸š"
+
msgid "Cannot be merged automatically"
msgstr "无法自动åˆå¹¶"
msgid "Cannot modify managed Kubernetes cluster"
-msgstr "无法修改托管的 Kubernetes 群集"
-
-msgid "Certificate fingerprint"
-msgstr ""
-
-msgid "Change Weight"
-msgstr "æ”¹å˜æƒé‡"
+msgstr "无法修改托管的 Kubernetes 集群"
msgid "Change this value to influence how frequently the GitLab UI polls for updates."
-msgstr ""
+msgstr "更改此值以影å“GitLab UIæ‹‰å–æ›´æ–°çš„频率。"
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "选择分支"
@@ -778,21 +932,18 @@ msgstr "选择文件 ……"
msgid "Choose a branch/tag (e.g. %{master}) or enter a commit (e.g. %{sha}) to see what's changed or to create a merge request."
msgstr "选择分支/标签(例如%{master})或输入æäº¤(例如%{sha})以查看更改内容或创建åˆå¹¶è¯·æ±‚。"
-msgid "Choose file..."
-msgstr "选择文件..."
+msgid "Choose any color."
+msgstr ""
-msgid "Choose which groups you wish to synchronize to this secondary node."
-msgstr "选择è¦åŒæ­¥åˆ°æ­¤æ¬¡èŠ‚ç‚¹çš„ç¾¤ç»„ã€‚"
+msgid "Choose between <code>clone</code> or <code>fetch</code> to get the recent application code"
+msgstr ""
-msgid "Choose which repositories you want to connect and run CI/CD pipelines."
-msgstr "清选择è¦è¿žæŽ¥å¹¶è¿è¡Œ CI/CD æµæ°´çº¿çš„代ç ä»“库。"
+msgid "Choose file..."
+msgstr "选择文件..."
msgid "Choose which repositories you want to import."
msgstr "选择è¦å¯¼å…¥çš„仓库"
-msgid "Choose which shards you wish to synchronize to this secondary node."
-msgstr "选择è¦åŒæ­¥åˆ°æ­¤æ¬¡èŠ‚ç‚¹çš„åˆ‡ç‰‡ã€‚"
-
msgid "CiStatusLabel|canceled"
msgstr "已喿¶ˆ"
@@ -862,21 +1013,12 @@ msgstr "* (所有环境)"
msgid "CiVariable|All environments"
msgstr "所有环境"
-msgid "CiVariable|Create wildcard"
-msgstr "创建通é…符"
-
msgid "CiVariable|Error occured while saving variables"
msgstr "ä¿å­˜å˜é‡æ—¶å‘生错误"
-msgid "CiVariable|New environment"
-msgstr "新建环境"
-
msgid "CiVariable|Protected"
msgstr "å—ä¿æŠ¤"
-msgid "CiVariable|Search environments"
-msgstr "æœç´¢çŽ¯å¢ƒ"
-
msgid "CiVariable|Toggle protected"
msgstr "å¼€å…³ä¿æŠ¤çŠ¶æ€"
@@ -886,50 +1028,56 @@ msgstr "验è¯å¤±è´¥"
msgid "CircuitBreakerApiLink|circuitbreaker api"
msgstr "断路器 API"
+msgid "Clear search input"
+msgstr "清除æœç´¢è¾“å…¥"
+
+msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
+msgstr "å•击下é¢é¡¹ç›®åˆ—表中的任何 <strong>项目åç§°</strong> 跳转到项目里程碑。"
+
+msgid "Click the <strong>Promote</strong> button in the top right corner to promote it to a group milestone."
+msgstr "点击å³ä¸Šè§’çš„ <strong>å‡çº§</strong> 按钮以å‡çº§åˆ°åˆ°ç¾¤ç»„里程碑。"
+
msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
msgstr "点击下é¢çš„æŒ‰é’®è½¬åˆ°Kubernetes页é¢å¼€å§‹å®‰è£…过程"
+msgid "Click to expand it."
+msgstr ""
+
msgid "Click to expand text"
msgstr "点击展开文本"
-msgid "Client authentication certificate"
-msgstr "客户端认è¯è¯ä¹¦"
-
-msgid "Client authentication key"
-msgstr "客户端认è¯å¯†é’¥"
-
-msgid "Client authentication key password"
-msgstr "客户端认è¯å¯†é’¥å¯†ç "
-
msgid "Clone repository"
msgstr "克隆仓库"
msgid "Close"
msgstr "关闭"
-msgid "Closed"
-msgstr "已关闭"
-
msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster"
-msgstr "%{appList} å·²æˆåŠŸå®‰è£…åˆ°Kubernetes群集上"
+msgstr "%{appList} å·²æˆåŠŸå®‰è£…åˆ°Kubernetes集群上"
msgid "ClusterIntegration|API URL"
msgstr "API地å€"
msgid "ClusterIntegration|Add Kubernetes cluster"
-msgstr "添加 Kubernetes 群集"
+msgstr "添加 Kubernetes 集群"
msgid "ClusterIntegration|Add an existing Kubernetes cluster"
-msgstr "添加现有的Kubernetes群集"
+msgstr "添加现有的Kubernetes集群"
msgid "ClusterIntegration|Advanced options on this Kubernetes cluster's integration"
msgstr "Kubernetes集群集æˆçš„高级选项"
+msgid "ClusterIntegration|An error occured while trying to fetch project zones: %{error}"
+msgstr "å°è¯•获å–项目地域时å‘生错误:%{error}"
+
+msgid "ClusterIntegration|An error occured while trying to fetch your projects: %{error}"
+msgstr "å°è¯•èŽ·å–æ‚¨çš„项目时å‘生错误:%{error}"
+
msgid "ClusterIntegration|Applications"
msgstr "应用程åº"
msgid "ClusterIntegration|Are you sure you want to remove this Kubernetes cluster's integration? This will not delete your actual Kubernetes cluster."
-msgstr "确定è¦åˆ é™¤æ­¤Kubernetes集群的集æˆå—?注æ„这并ä¸ä¼šåˆ é™¤å®žé™…çš„Kubernetes群集本身。"
+msgstr "确定è¦åˆ é™¤æ­¤Kubernetes集群的集æˆå—?注æ„这并ä¸ä¼šåˆ é™¤å®žé™…çš„Kubernetes集群本身。"
msgid "ClusterIntegration|CA Certificate"
msgstr "CAè¯ä¹¦"
@@ -941,7 +1089,7 @@ msgid "ClusterIntegration|Choose how to set up Kubernetes cluster integration"
msgstr "选择如何设置Kubernetes集群集æˆ"
msgid "ClusterIntegration|Choose which of your project's environments will use this Kubernetes cluster."
-msgstr "请选择使用此Kubernetes群集的环境。"
+msgstr "请选择使用此Kubernetes集群的环境。"
msgid "ClusterIntegration|Control how your Kubernetes cluster integrates with GitLab"
msgstr "控制Kubernetes集群与GitLabé›†æˆæ–¹å¼"
@@ -955,6 +1103,9 @@ msgstr "å¤åˆ¶CAè¯ä¹¦"
msgid "ClusterIntegration|Copy Ingress IP Address to clipboard"
msgstr "å¤åˆ¶Ingress IP地å€åˆ°å‰ªè´´æ¿"
+msgid "ClusterIntegration|Copy Jupyter Hostname to clipboard"
+msgstr "å°†Jupyter主机åå¤åˆ¶åˆ°å‰ªè´´æ¿"
+
msgid "ClusterIntegration|Copy Kubernetes cluster name"
msgstr "å¤åˆ¶Kubernetes集群åç§°"
@@ -962,7 +1113,7 @@ msgid "ClusterIntegration|Copy Token"
msgstr "å¤åˆ¶ä»¤ç‰Œ"
msgid "ClusterIntegration|Create Kubernetes cluster"
-msgstr "创建Kubernetes群集"
+msgstr "创建Kubernetes集群"
msgid "ClusterIntegration|Create Kubernetes cluster on Google Kubernetes Engine"
msgstr "在Google Kubernetes引擎上创建Kubernetes集群"
@@ -970,26 +1121,38 @@ msgstr "在Google Kubernetes引擎上创建Kubernetes集群"
msgid "ClusterIntegration|Create a new Kubernetes cluster on Google Kubernetes Engine right from GitLab"
msgstr "通过GitLab在Google Kubernetes引擎上创建Kubernetes集群"
-msgid "ClusterIntegration|Create on GKE"
-msgstr "在GKE中创建"
+msgid "ClusterIntegration|Create on Google Kubernetes Engine"
+msgstr "在 Google Kubernetes Engine 上创建"
msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
msgstr "输入现有的 Kubernetes 集群详细信æ¯"
msgid "ClusterIntegration|Enter the details for your Kubernetes cluster"
-msgstr "输入Kubernetes群集的详细信æ¯"
+msgstr "输入Kubernetes集群的详细信æ¯"
msgid "ClusterIntegration|Environment scope"
msgstr "环境范围"
+msgid "ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab's Google Kubernetes Engine Integration."
+msgstr ""
+
+msgid "ClusterIntegration|Fetching machine types"
+msgstr "正在获å–实例类型"
+
+msgid "ClusterIntegration|Fetching projects"
+msgstr "正在获å–项目"
+
+msgid "ClusterIntegration|Fetching zones"
+msgstr "正在获å–地域"
+
msgid "ClusterIntegration|GitLab Integration"
msgstr "GitLab集æˆ"
msgid "ClusterIntegration|GitLab Runner"
msgstr "GitLab Runner"
-msgid "ClusterIntegration|Google Cloud Platform project ID"
-msgstr "Google 云平å°é¡¹ç›®ID"
+msgid "ClusterIntegration|Google Cloud Platform project"
+msgstr "Google 云平å°é¡¹ç›®"
msgid "ClusterIntegration|Google Kubernetes Engine"
msgstr "Google Kubernetes Engine"
@@ -1000,11 +1163,8 @@ msgstr "Google Kubernetes Engine 项目"
msgid "ClusterIntegration|Helm Tiller"
msgstr "Helm Tiller"
-msgid "ClusterIntegration|In order to show the health of the cluster, we'll need to provision your cluster with Prometheus to collect the required data."
-msgstr "为了显示集群的å¥åº·çŠ¶å†µï¼Œæ‚¨çš„é›†ç¾¤éœ€è¦é…ç½®Prometheus以收集所需的数æ®ã€‚"
-
msgid "ClusterIntegration|Ingress"
-msgstr "å…¥å£"
+msgstr "Ingress"
msgid "ClusterIntegration|Ingress IP Address"
msgstr "Ingress IP地å€"
@@ -1012,9 +1172,6 @@ msgstr "Ingress IP地å€"
msgid "ClusterIntegration|Install"
msgstr "安装"
-msgid "ClusterIntegration|Install Prometheus"
-msgstr "安装Prometheus"
-
msgid "ClusterIntegration|Installed"
msgstr "已安装"
@@ -1027,44 +1184,53 @@ msgstr "集æˆKubernetes集群自动化"
msgid "ClusterIntegration|Integration status"
msgstr "集æˆçжæ€"
+msgid "ClusterIntegration|Jupyter Hostname"
+msgstr "Jupyter主机å"
+
+msgid "ClusterIntegration|JupyterHub"
+msgstr "JupyterHub"
+
msgid "ClusterIntegration|Kubernetes cluster"
-msgstr "Kubernetes 群集"
+msgstr "Kubernetes 集群"
msgid "ClusterIntegration|Kubernetes cluster details"
-msgstr "Kubernetes群集详细信æ¯"
-
-msgid "ClusterIntegration|Kubernetes cluster health"
-msgstr "Kubernetes集群å¥åº·åº¦"
+msgstr "Kubernetes集群详细信æ¯"
msgid "ClusterIntegration|Kubernetes cluster integration"
msgstr "Kubernetes集群集æˆ"
msgid "ClusterIntegration|Kubernetes cluster integration is disabled for this project."
-msgstr "此项目已ç¦ç”¨ Kubernetes 群集集æˆã€‚"
+msgstr "此项目已ç¦ç”¨ Kubernetes 集群集æˆã€‚"
msgid "ClusterIntegration|Kubernetes cluster integration is enabled for this project."
-msgstr "此项目已å¯ç”¨ Kubernetes 群集集æˆã€‚"
+msgstr "此项目已å¯ç”¨ Kubernetes 集群集æˆã€‚"
msgid "ClusterIntegration|Kubernetes cluster integration is enabled for this project. Disabling this integration will not affect your Kubernetes cluster, it will only temporarily turn off GitLab's connection to it."
-msgstr "此项目已å¯ç”¨ Kubernetes 群集集æˆã€‚ç¦ç”¨æ­¤é›†æˆä¸ä¼šå½±å“ Kubernetes 群集本身, åªä¼šæš‚时关闭 GitLab 与其连接。"
+msgstr "此项目已å¯ç”¨ Kubernetes 集群集æˆã€‚ç¦ç”¨æ­¤é›†æˆä¸ä¼šå½±å“ Kubernetes 集群本身, åªä¼šæš‚时关闭 GitLab 与其连接。"
msgid "ClusterIntegration|Kubernetes cluster is being created on Google Kubernetes Engine..."
-msgstr "正在Google Kubernetes Engine上创建Kubernetes群集..."
+msgstr "正在Google Kubernetes Engine上创建Kubernetes集群..."
msgid "ClusterIntegration|Kubernetes cluster name"
-msgstr "Kubernetes 群集åç§°"
+msgstr "Kubernetes 集群åç§°"
msgid "ClusterIntegration|Kubernetes cluster was successfully created on Google Kubernetes Engine. Refresh the page to see Kubernetes cluster's details"
-msgstr "Kubernetes集群已在Google Kubernetes Engine上æˆåŠŸåˆ›å»ºã€‚åˆ·æ–°é¡µé¢ä»¥æŸ¥çœ‹Kubernetes群集的详细信æ¯"
+msgstr "Kubernetes集群已在Google Kubernetes Engine上æˆåŠŸåˆ›å»ºã€‚åˆ·æ–°é¡µé¢ä»¥æŸ¥çœ‹Kubernetes集群的详细信æ¯"
msgid "ClusterIntegration|Kubernetes clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way. %{link_to_help_page}"
-msgstr "通过Kubernetes 群集集æˆï¼Œå¯ä»¥æ–¹ä¾¿åœ°ä½¿ç”¨å®¡é˜…应用ã€éƒ¨ç½²åº”用程åºã€è¿è¡Œæµæ°´çº¿ç­‰ç­‰ã€‚%{link_to_help_page}"
+msgstr "通过Kubernetes 集群集æˆï¼Œå¯ä»¥æ–¹ä¾¿åœ°ä½¿ç”¨å®¡é˜…应用ã€éƒ¨ç½²åº”用程åºã€è¿è¡Œæµæ°´çº¿ç­‰ç­‰ã€‚%{link_to_help_page}"
msgid "ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project"
-msgstr "Kubernetes 群集å¯ç”¨äºŽéƒ¨ç½²åº”用程åºå’Œæä¾›æ­¤é¡¹ç›®çš„审阅应用"
+msgstr "Kubernetes 集群å¯ç”¨äºŽéƒ¨ç½²åº”用程åºå’Œæä¾›æ­¤é¡¹ç›®çš„审阅应用"
+
+msgid "ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}."
+msgstr "进一步了解 %{help_link_start_machine_type}实例类型%{help_link_end} å’Œ %{help_link_start_pricing}定价信æ¯%{help_link_end}。"
-msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
-msgstr "进一步了解%{link_to_documentation}"
+msgid "ClusterIntegration|Learn more about %{help_link_start}Kubernetes%{help_link_end}."
+msgstr "进一步了解 %{help_link_start}Kubernetes%{help_link_end}。"
+
+msgid "ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}."
+msgstr "进一步了解 %{help_link_start}地域%{help_link_end}。"
msgid "ClusterIntegration|Learn more about environments"
msgstr "进一步了解有关环境的信æ¯"
@@ -1076,19 +1242,28 @@ msgid "ClusterIntegration|Machine type"
msgstr "机器类型"
msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create Kubernetes clusters"
-msgstr "è¯·ç¡®ä¿æ‚¨çš„叿ˆ· %{link_to_requirements} å¯ä»¥åˆ›å»º Kubernetes 群集"
+msgstr "è¯·ç¡®ä¿æ‚¨çš„叿ˆ· %{link_to_requirements} å¯ä»¥åˆ›å»º Kubernetes 集群"
msgid "ClusterIntegration|Manage"
msgstr "管ç†"
msgid "ClusterIntegration|Manage your Kubernetes cluster by visiting %{link_gke}"
-msgstr "通过访问 %{link_gke} ç®¡ç† Kubernetes 群集"
+msgstr "通过访问 %{link_gke} ç®¡ç† Kubernetes 集群"
msgid "ClusterIntegration|More information"
msgstr "更多信æ¯"
-msgid "ClusterIntegration|Multiple Kubernetes clusters are available in GitLab Enterprise Edition Premium and Ultimate"
-msgstr "在GitLabä¼ä¸šé«˜çº§å’Œæ——舰版中å¯ä»¥ä½¿ç”¨å¤šä¸ªKubernetes集群"
+msgid "ClusterIntegration|No machine types matched your search"
+msgstr "未找到您æœç´¢çš„实例类型"
+
+msgid "ClusterIntegration|No projects found"
+msgstr "未找到项目"
+
+msgid "ClusterIntegration|No projects matched your search"
+msgstr "未找到您æœç´¢çš„项目"
+
+msgid "ClusterIntegration|No zones matched your search"
+msgstr "未找到您æœç´¢çš„地域"
msgid "ClusterIntegration|Note:"
msgstr "注æ„:"
@@ -1102,9 +1277,6 @@ msgstr "请输入Kubernetes集群的访问信æ¯ã€‚如需帮助,å¯ä»¥é˜…读Ku
msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
msgstr "è¯·ç¡®ä¿æ‚¨çš„ Google 叿ˆ·ç¬¦åˆä»¥ä¸‹è¦æ±‚:"
-msgid "ClusterIntegration|Project ID"
-msgstr "项目 ID"
-
msgid "ClusterIntegration|Project namespace"
msgstr "项目命å空间"
@@ -1117,6 +1289,9 @@ msgstr "Prometheus"
msgid "ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration."
msgstr "请阅读关于Kubernetes集群集æˆçš„%{link_to_help_page}。"
+msgid "ClusterIntegration|Redeem up to $500 in free credit for Google Cloud Platform"
+msgstr "从Google Cloud Platform å…‘æ¢æœ€é«˜$500çš„å…è´¹é¢åº¦"
+
msgid "ClusterIntegration|Remove Kubernetes cluster integration"
msgstr "删除Kubernetes集群集æˆ"
@@ -1124,7 +1299,7 @@ msgid "ClusterIntegration|Remove integration"
msgstr "删除集æˆ"
msgid "ClusterIntegration|Remove this Kubernetes cluster's configuration from this project. This will not delete your actual Kubernetes cluster."
-msgstr "从当å‰é¡¹ç›®ä¸­åˆ é™¤æ­¤Kubernetes集群的é…置。该æ“作并ä¸ä¼šåˆ é™¤å®žé™…Kubernetes群集。"
+msgstr "从当å‰é¡¹ç›®ä¸­åˆ é™¤æ­¤Kubernetes集群的é…置。该æ“作并ä¸ä¼šåˆ é™¤å®žé™…Kubernetes集群。"
msgid "ClusterIntegration|Request to begin installing failed"
msgstr "请求安装失败"
@@ -1132,20 +1307,38 @@ msgstr "请求安装失败"
msgid "ClusterIntegration|Save changes"
msgstr "ä¿å­˜æ›´æ”¹"
+msgid "ClusterIntegration|Search machine types"
+msgstr "æœç´¢å®žä¾‹ç±»åž‹"
+
+msgid "ClusterIntegration|Search projects"
+msgstr "æœç´¢é¡¹ç›®"
+
+msgid "ClusterIntegration|Search zones"
+msgstr "æœç´¢åœ°åŸŸ"
+
msgid "ClusterIntegration|Security"
msgstr "安全"
msgid "ClusterIntegration|See and edit the details for your Kubernetes cluster"
msgstr "查看并编辑Kubernetes集群的详细信æ¯"
-msgid "ClusterIntegration|See machine types"
-msgstr "å‚è§æœºå™¨ç±»åž‹"
+msgid "ClusterIntegration|Select machine type"
+msgstr "选择实例类型"
+
+msgid "ClusterIntegration|Select project"
+msgstr "选择项目"
+
+msgid "ClusterIntegration|Select project and zone to choose machine type"
+msgstr "按项目和地域选择实例类型"
+
+msgid "ClusterIntegration|Select project to choose zone"
+msgstr "按项目选择地域"
-msgid "ClusterIntegration|See your projects"
-msgstr "看到您的项目"
+msgid "ClusterIntegration|Select zone"
+msgstr "选择地域"
-msgid "ClusterIntegration|See zones"
-msgstr "查看区域"
+msgid "ClusterIntegration|Select zone to choose machine type"
+msgstr "按地域选择实例类型"
msgid "ClusterIntegration|Service token"
msgstr "æœåŠ¡ä»¤ç‰Œ"
@@ -1169,14 +1362,17 @@ msgid "ClusterIntegration|This account must have permissions to create a Kuberne
msgstr "è¯¥å¸æˆ·éœ€å…·å¤‡åœ¨ä¸‹é¢æŒ‡å®šçš„%{link_to_container_project}中创建 Kubernetes集群的æƒé™"
msgid "ClusterIntegration|Toggle Kubernetes Cluster"
-msgstr "开关Kubernetes 群集"
+msgstr "开关Kubernetes 集群"
msgid "ClusterIntegration|Toggle Kubernetes cluster"
-msgstr "开关Kubernetes 群集"
+msgstr "开关Kubernetes 集群"
msgid "ClusterIntegration|Token"
msgstr "令牌"
+msgid "ClusterIntegration|Validating project billing status"
+msgstr "验è¯é¡¹ç›®è´¦å•状æ€"
+
msgid "ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr "使用与此项目关è”çš„Kubernetes集群,å¯ä»¥æ–¹ä¾¿åœ°ä½¿ç”¨å®¡é˜…应用,部署应用程åºï¼Œè¿è¡Œæµæ°´çº¿ç­‰ç­‰ã€‚"
@@ -1184,7 +1380,7 @@ msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}"
msgstr "æ‚¨çš„å¸æˆ·å¿…须拥有%{link_to_kubernetes_engine}"
msgid "ClusterIntegration|Zone"
-msgstr "区域"
+msgstr "地域"
msgid "ClusterIntegration|access to Google Kubernetes Engine"
msgstr "访问 Google Kubernetes Engine"
@@ -1207,14 +1403,20 @@ msgstr "符åˆè¦æ±‚"
msgid "ClusterIntegration|properly configured"
msgstr "正确é…ç½®"
+msgid "ClusterIntegration|sign up"
+msgstr "注册"
+
msgid "Collapse"
msgstr "æ”¶èµ·"
-msgid "Comment and resolve discussion"
-msgstr "评论并解决讨论"
+msgid "Collapse sidebar"
+msgstr "折å ä¾§è¾¹æ "
+
+msgid "Comment & resolve discussion"
+msgstr ""
-msgid "Comment and unresolve discussion"
-msgstr "评论并将讨论å˜ä¸ºæœªå†³"
+msgid "Comment & unresolve discussion"
+msgstr ""
msgid "Comments"
msgstr "评论"
@@ -1278,6 +1480,9 @@ msgstr "无相关åˆå¹¶è¯·æ±‚"
msgid "Committed by"
msgstr "æäº¤è€…:"
+msgid "Commit…"
+msgstr ""
+
msgid "Compare"
msgstr "比较"
@@ -1291,7 +1496,7 @@ msgid "Compare changes with the last commit"
msgstr "与上个æäº¤æ¯”è¾ƒå˜æ›´å†…容"
msgid "Compare changes with the merge request target branch"
-msgstr ""
+msgstr "与åˆå¹¶è¯·æ±‚çš„ç›®æ ‡åˆ†æ”¯æ¯”è¾ƒå˜æ›´å†…容"
msgid "CompareBranches|%{source_branch} and %{target_branch} are the same."
msgstr "%{source_branch} å’Œ %{target_branch} 是相åŒçš„"
@@ -1315,19 +1520,22 @@ msgid "Confidentiality"
msgstr "ç§å¯†æ€§"
msgid "Configure Gitaly timeouts."
-msgstr ""
+msgstr "é…ç½®Gitaly超时时间。"
msgid "Configure Sidekiq job throttling."
-msgstr ""
+msgstr "é…ç½® Sidekiq 作业é™åˆ¶ã€‚"
msgid "Configure automatic git checks and housekeeping on repositories."
-msgstr ""
+msgstr "在仓库上é…置自动git检查和仓库整ç†ã€‚"
msgid "Configure limits for web and API requests."
+msgstr "é…ç½® web å’Œ API 请求é™åˆ¶ã€‚"
+
+msgid "Configure push mirrors."
msgstr ""
msgid "Configure storage path and circuit breaker settings."
-msgstr ""
+msgstr "é…ç½®å­˜å‚¨è·¯å¾„åŠæ–­è·¯å™¨è®¾ç½®ã€‚"
msgid "Configure the way a user creates a new account."
msgstr "é…ç½®ç”¨æˆ·åˆ›å»ºæ–°å¸æˆ·çš„æ–¹å¼ã€‚"
@@ -1335,20 +1543,11 @@ msgstr "é…ç½®ç”¨æˆ·åˆ›å»ºæ–°å¸æˆ·çš„æ–¹å¼ã€‚"
msgid "Connect"
msgstr "连接"
-msgid "Connect all repositories"
-msgstr "连接所有仓库"
-
msgid "Connect repositories from GitHub"
msgstr "从 Github 中导入代ç ä»“库"
-msgid "Connect your external repositories, and CI/CD pipelines will run for new commits. A GitLab project will be created with only CI/CD features enabled."
-msgstr "连接外部仓库åŽï¼Œæ–°æäº¤å°†ä¼šå¯åЍCI/CDæµæ°´çº¿ã€‚ä»…å¯ç”¨CI/CD功能的Gitlab项目将会被创建。"
-
-msgid "Connecting..."
-msgstr "正在连接..."
-
msgid "Container Registry"
-msgstr "容器注册"
+msgstr "容器注册表"
msgid "ContainerRegistry|Created"
msgstr "已创建"
@@ -1392,9 +1591,18 @@ 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 "ContainerRegistry|You can also use a %{deploy_token} for read-only access to the registry images."
+msgstr "您也å¯ä»¥ä½¿ç”¨ %{deploy_token} 以åªè¯»æ–¹å¼è®¿é—®é•œåƒåº“的镜åƒã€‚"
+
+msgid "Continue"
+msgstr "ç»§ç»­"
+
msgid "Continuous Integration and Deployment"
msgstr "æŒç»­é›†æˆå’Œéƒ¨ç½²"
+msgid "Contribute to GitLab"
+msgstr ""
+
msgid "Contribution"
msgstr "贡献"
@@ -1416,15 +1624,6 @@ msgstr "%{branch_name} 分支上的æäº¤ï¼Œä¸å«åˆå¹¶æäº¤ã€‚é™6000次。"
msgid "ContributorsPage|Please wait a moment, this page will automatically refresh when ready."
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 "å¤åˆ¶ SSH 公钥到剪贴æ¿"
-
msgid "Copy URL to clipboard"
msgstr "å¤åˆ¶ URL 到剪贴æ¿"
@@ -1437,9 +1636,18 @@ msgstr "将命令å¤åˆ¶åˆ°å‰ªè´´æ¿"
msgid "Copy commit SHA to clipboard"
msgstr "å¤åˆ¶æäº¤ SHA 的值到剪贴æ¿"
+msgid "Copy file name to clipboard"
+msgstr ""
+
+msgid "Copy file path to clipboard"
+msgstr ""
+
msgid "Copy reference to clipboard"
msgstr "将索引å¤åˆ¶åˆ°å‰ªè´´æ¿"
+msgid "Copy to clipboard"
+msgstr "å¤åˆ¶åˆ°å‰ªè´´æ¿"
+
msgid "Create"
msgstr "创建"
@@ -1458,15 +1666,15 @@ msgstr "åœ¨å¸æˆ·ä¸Šåˆ›å»ºä¸ªäººè®¿é—®ä»¤ç‰Œï¼Œä»¥é€šè¿‡ %{protocol} æ¥æ‹‰å–æˆ
msgid "Create branch"
msgstr "创建分支"
+msgid "Create commit"
+msgstr ""
+
msgid "Create directory"
msgstr "创建目录"
msgid "Create empty repository"
msgstr "创建空的仓库"
-msgid "Create epic"
-msgstr "创建å²è¯—故事"
-
msgid "Create file"
msgstr "创建文件"
@@ -1509,14 +1717,11 @@ msgstr "标签"
msgid "CreateTokenToCloneLink|create a personal access token"
msgstr "创建个人访问令牌"
-msgid "Creates a new branch from %{branchName}"
-msgstr "自%{branchName} 创建一个新分支"
-
-msgid "Creates a new branch from %{branchName} and re-directs to create a new merge request"
-msgstr "自%{branchName} 创建一个新分支,并转到创建一个新的åˆå¹¶è¯·æ±‚"
+msgid "Created"
+msgstr "已创建"
-msgid "Creating epic"
-msgstr "创建å²è¯—故事中"
+msgid "Created by me"
+msgstr "由我创建"
msgid "Cron Timezone"
msgstr "Cron 时区"
@@ -1524,8 +1729,14 @@ msgstr "Cron 时区"
msgid "Cron syntax"
msgstr "Cron 语法"
-msgid "Current node"
-msgstr "当å‰èŠ‚ç‚¹"
+msgid "CurrentUser|Profile"
+msgstr "用户资料"
+
+msgid "CurrentUser|Settings"
+msgstr "设置"
+
+msgid "Custom CI config path"
+msgstr ""
msgid "Custom notification events"
msgstr "自定义通知事件"
@@ -1533,9 +1744,6 @@ 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 "Customize colors"
-msgstr ""
-
msgid "Cycle Analytics"
msgstr "周期分æž"
@@ -1572,8 +1780,8 @@ msgstr "å二"
msgid "December"
msgstr "å二月"
-msgid "Default classification label"
-msgstr "默认分类标签"
+msgid "Decline and sign out"
+msgstr "æ‹’ç»å¹¶é€€å‡º"
msgid "Define a custom pattern with cron syntax"
msgstr "使用 Cron 语法定义自定义模å¼"
@@ -1581,6 +1789,9 @@ msgstr "使用 Cron 语法定义自定义模å¼"
msgid "Delete"
msgstr "删除"
+msgid "Delete list"
+msgstr ""
+
msgid "Deploy"
msgid_plural "Deploys"
msgstr[0] "部署"
@@ -1588,38 +1799,164 @@ msgstr[0] "部署"
msgid "Deploy Keys"
msgstr "部署密钥"
+msgid "DeployKeys|+%{count} others"
+msgstr "+%{count} å…¶ä»–"
+
+msgid "DeployKeys|Current project"
+msgstr "DeployKeys |当å‰é¡¹ç›®"
+
+msgid "DeployKeys|Deploy key"
+msgstr "部署密钥"
+
+msgid "DeployKeys|Enabled deploy keys"
+msgstr "å¯ç”¨éƒ¨ç½²å¯†é’¥"
+
+msgid "DeployKeys|Error enabling deploy key"
+msgstr "å¯ç”¨éƒ¨ç½²å¯†é’¥å‡ºé”™"
+
+msgid "DeployKeys|Error getting deploy keys"
+msgstr "获得部署密钥出错"
+
+msgid "DeployKeys|Error removing deploy key"
+msgstr "移除部署密钥出错"
+
+msgid "DeployKeys|Expand %{count} other projects"
+msgstr "展开 %{count} 个其他项目"
+
+msgid "DeployKeys|Loading deploy keys"
+msgstr "加载部署密钥"
+
+msgid "DeployKeys|No deploy keys found. Create one with the form above."
+msgstr "没有找到部署密钥。请使用上é¢çš„表å•创建部署密钥。"
+
+msgid "DeployKeys|Privately accessible deploy keys"
+msgstr "ç§äººè®¿é—®çš„部署密钥"
+
+msgid "DeployKeys|Project usage"
+msgstr "项目使用情况"
+
+msgid "DeployKeys|Publicly accessible deploy keys"
+msgstr "公开访问的部署密钥"
+
+msgid "DeployKeys|Read access only"
+msgstr "åªè¯»æƒé™"
+
+msgid "DeployKeys|Write access allowed"
+msgstr "写入æƒé™"
+
+msgid "DeployKeys|You are going to remove this deploy key. Are you sure?"
+msgstr "å³å°†å°†åˆ é™¤è¯¥éƒ¨ç½²å¯†é’¥ã€‚确定继续å—?"
+
+msgid "DeployTokens|Active Deploy Tokens (%{active_tokens})"
+msgstr "å¯ç”¨éƒ¨ç½²ä»¤ç‰Œï¼ˆ%{active_tokens})"
+
+msgid "DeployTokens|Add a deploy token"
+msgstr "添加一个部署令牌"
+
+msgid "DeployTokens|Allows read-only access to the registry images"
+msgstr "å…许对容器注册表镜åƒè¿›è¡Œåªè¯»è®¿é—®"
+
+msgid "DeployTokens|Allows read-only access to the repository"
+msgstr "å…许对仓库进行åªè¯»è®¿é—®"
+
+msgid "DeployTokens|Copy deploy token to clipboard"
+msgstr "将部署令牌å¤åˆ¶åˆ°å‰ªè´´æ¿"
+
+msgid "DeployTokens|Copy username to clipboard"
+msgstr "将用户åå¤åˆ¶åˆ°å‰ªè´´æ¿"
+
+msgid "DeployTokens|Create deploy token"
+msgstr "创建部署令牌"
+
+msgid "DeployTokens|Created"
+msgstr "已创建"
+
+msgid "DeployTokens|Deploy Tokens"
+msgstr "部署令牌"
+
+msgid "DeployTokens|Deploy tokens allow read-only access to your repository and registry images."
+msgstr "部署令牌å¯ä»¥å¯¹ä»“库和容器注册表中的镜åƒè¿›è¡Œåªè¯»è®¿é—®ã€‚"
+
+msgid "DeployTokens|Expires"
+msgstr "到期"
+
+msgid "DeployTokens|Name"
+msgstr "åç§°"
+
+msgid "DeployTokens|Pick a name for the application, and we'll give you a unique deploy token."
+msgstr "请为应用程åºé€‰æ‹©ä¸€ä¸ªå称,以便生æˆå”¯ä¸€çš„部署令牌。"
+
+msgid "DeployTokens|Revoke"
+msgstr "撤销"
+
+msgid "DeployTokens|Revoke %{name}"
+msgstr "撤销 %{name}"
+
+msgid "DeployTokens|Scopes"
+msgstr "有效范围"
+
+msgid "DeployTokens|This action cannot be undone."
+msgstr "æ­¤æ“作无法撤消。"
+
+msgid "DeployTokens|This project has no active Deploy Tokens."
+msgstr "该项目没有å¯ç”¨çš„部署令牌。"
+
+msgid "DeployTokens|Use this token as a password. Make sure you save it - you won't be able to access it again."
+msgstr "此令牌和密ç ä½œç”¨ä¸€æ ·ã€‚当å‰çª—å£å…³é—­åŽå°†æ— æ³•冿¬¡æŸ¥çœ‹ä»¤ç‰Œå†…容,请立å³å¦¥å–„ä¿å­˜ã€‚"
+
+msgid "DeployTokens|Use this username as a login."
+msgstr "将此用户å用作登录å。"
+
+msgid "DeployTokens|Username"
+msgstr "用户å"
+
+msgid "DeployTokens|You are about to revoke"
+msgstr "å³å°†æ’¤é”€"
+
+msgid "DeployTokens|Your New Deploy Token"
+msgstr "新部署令牌"
+
+msgid "DeployTokens|Your new project deploy token has been created."
+msgstr "新项目部署令牌已创建。"
+
+msgid "Deprioritize label"
+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 "Diffs|No file name available"
msgstr "没有å¯ç”¨çš„æ–‡ä»¶å"
+msgid "Diffs|Something went wrong while fetching diff lines."
+msgstr ""
+
msgid "Directory name"
msgstr "目录åç§°"
msgid "Disable"
msgstr "ç¦ç”¨"
-msgid "Discard draft"
-msgstr "èˆå¼ƒè‰ç¨¿"
+msgid "Disable for this project"
+msgstr "在此项目中ç¦ç”¨"
+
+msgid "Disable group Runners"
+msgstr "ç¦ç”¨ç¾¤ç»„Runner"
-msgid "Discover GitLab Geo."
-msgstr "å‘现GitLab Geo。"
+msgid "Discard changes"
+msgstr "放弃更改"
+
+msgid "Discard draft"
+msgstr "å–æ¶ˆ"
msgid "Dismiss Cycle Analytics introduction box"
msgstr "关闭循环分æžä»‹ç»æ¡†"
-msgid "Dismiss Merge Request promotion"
-msgstr "关闭åˆå¹¶è¯·æ±‚中的促销广告"
-
-msgid "Documentation for popular identity providers"
-msgstr ""
+msgid "Domain"
+msgstr "域å"
msgid "Don't show again"
msgstr "ä¸å†æ˜¾ç¤º"
@@ -1660,44 +1997,44 @@ msgstr "踩"
msgid "Due date"
msgstr "截止日期"
-msgid "During this process, you’ll be asked for URLs from GitLab’s side. Use the URLs shown below."
-msgstr ""
+msgid "Each Runner can be in one of the following states:"
+msgstr "æ¯ä¸ªRunnerå¯ä»¥å¤„于以下状æ€ä¸­çš„其中一ç§ï¼š"
msgid "Edit"
msgstr "编辑"
+msgid "Edit Label"
+msgstr ""
+
msgid "Edit Pipeline Schedule %{id}"
msgstr "编辑 %{id} æµæ°´çº¿è®¡åˆ’"
msgid "Edit files in the editor and commit changes here"
msgstr "在编辑器中编辑文件并在这里​​æäº¤å˜æ›´å†…容"
-msgid "Editing"
-msgstr "编辑"
-
-msgid "Elasticsearch"
-msgstr ""
-
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "Edit identity for %{user_name}"
msgstr ""
msgid "Email"
+msgstr "电å­é‚®ä»¶"
+
+msgid "Email patch"
msgstr ""
msgid "Emails"
msgstr "电å­é‚®ä»¶"
+msgid "Embed"
+msgstr "嵌入"
+
msgid "Enable"
msgstr "å¯ç”¨"
msgid "Enable Auto DevOps"
msgstr "å¯ç”¨Auto DevOps"
-msgid "Enable SAML authentication for this group"
-msgstr ""
-
msgid "Enable Sentry for error reporting and logging."
-msgstr ""
+msgstr "å¯ç”¨S​​entry进行错误报告和日志记录。"
msgid "Enable and configure InfluxDB metrics."
msgstr "å¯ç”¨å¹¶é…ç½®InfluxDB指标。"
@@ -1705,20 +2042,29 @@ msgstr "å¯ç”¨å¹¶é…ç½®InfluxDB指标。"
msgid "Enable and configure Prometheus metrics."
msgstr "å¯ç”¨å¹¶é…ç½®Prometheus指标。"
-msgid "Enable classification control using an external service"
-msgstr "使用外部æœåŠ¡å¯ç”¨åˆ†ç±»æŽ§åˆ¶"
+msgid "Enable for this project"
+msgstr "在此项目中å¯ç”¨"
+
+msgid "Enable group Runners"
+msgstr "å¯ç”¨ç¾¤ç»„Runner"
+
+msgid "Enable or disable certain group features and choose access levels."
+msgstr "å¯ç”¨æˆ–ç¦ç”¨éƒ¨åˆ†ç¾¤ç»„功能并选择访问等级。"
msgid "Enable or disable version check and usage ping."
-msgstr ""
+msgstr "å¯ç”¨æˆ–ç¦ç”¨ç‰ˆæœ¬æ£€æŸ¥åŠä½¿ç”¨ping。"
msgid "Enable reCAPTCHA or Akismet and set IP limits."
-msgstr ""
+msgstr "å¯ç”¨reCAPTCHA或Akismet并设置IPé™åˆ¶ã€‚"
msgid "Enable the Performance Bar for a given group."
-msgstr ""
+msgstr "对指定群组å¯ç”¨æ€§èƒ½æ ã€‚"
-msgid "Enabled"
-msgstr ""
+msgid "Ends at (UTC)"
+msgstr "结æŸäºŽ(UTC)"
+
+msgid "Environments"
+msgstr "环境"
msgid "Environments|An error occurred while fetching the environments."
msgstr "获å–环境时å‘生错误。"
@@ -1768,33 +2114,18 @@ msgstr "已更新"
msgid "Environments|You don't have any environments right now."
msgstr "当剿œªè®¾ç½®çŽ¯å¢ƒ"
-msgid "Epic will be removed! Are you sure?"
-msgstr "EPIC将被删除!是å¦ç¡®å®šï¼Ÿ"
-
-msgid "Epics"
-msgstr "EPIC"
-
-msgid "Epics Roadmap"
-msgstr "å²è¯—故事路线图"
-
-msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
-msgstr "利用å²è¯—故事(Epics),产å“线管ç†ä¼šå˜å¾—æ›´è½»æ¾ä¸”更高效"
-
msgid "Error Reporting and Logging"
-msgstr ""
-
-msgid "Error checking branch data. Please try again."
-msgstr "æ£€æŸ¥åˆ†æ”¯æ•°æ®æ—¶å‡ºé”™ã€‚请å†è¯•一次。"
+msgstr "错误报告和日志记录"
msgid "Error committing changes. Please try again."
msgstr "æäº¤æ›´æ”¹æ—¶å‡ºé”™ã€‚请å†è¯•一次。"
-msgid "Error creating epic"
-msgstr "创建å²è¯—故事时出错"
-
msgid "Error fetching contributors data."
msgstr "获å–è´¡çŒ®è€…æ•°æ®æ—¶å‡ºé”™ã€‚"
+msgid "Error fetching job trace"
+msgstr "获å–作业日志时出错"
+
msgid "Error fetching labels."
msgstr "èŽ·å–æ ‡è®°æ—¶å‡ºé”™ã€‚"
@@ -1807,6 +2138,18 @@ msgstr "获å–refs时出错。"
msgid "Error fetching usage ping data."
msgstr "获å–使用情况(usage ping) æ•°æ®æ—¶å‡ºé”™ã€‚"
+msgid "Error loading branch data. Please try again."
+msgstr "加载分支数æ®å¤±è´¥ï¼Œè¯·é‡è¯•。"
+
+msgid "Error loading last commit."
+msgstr "加载最åŽä¸€æ¬¡æäº¤å¤±è´¥ã€‚"
+
+msgid "Error loading merge requests."
+msgstr "加载åˆå¹¶è¯·æ±‚时出错。"
+
+msgid "Error loading project data. Please try again."
+msgstr "加载项目数æ®å¤±è´¥ï¼Œè¯·é‡è¯•。"
+
msgid "Error occurred when toggling the notification subscription"
msgstr "切æ¢é€šçŸ¥è®¢é˜…æ—¶å‘生错误"
@@ -1819,6 +2162,9 @@ msgstr "æ›´æ–°æ‰€æœ‰å¾…åŠžäº‹é¡¹çš„çŠ¶æ€æ—¶å‡ºé”™ã€‚"
msgid "Error updating todo status."
msgstr "æ›´æ–°å¾…åŠžäº‹é¡¹çŠ¶æ€æ—¶å‡ºé”™ã€‚"
+msgid "Estimated"
+msgstr "预计"
+
msgid "EventFilterBy|Filter by all"
msgstr "全部"
@@ -1849,33 +2195,18 @@ msgstr "æ¯å‘¨æ‰§è¡Œï¼ˆå‘¨æ—¥å‡Œæ™¨ 4 点)"
msgid "Expand"
msgstr "展开"
+msgid "Expand all"
+msgstr ""
+
+msgid "Expand sidebar"
+msgstr "展开侧边æ "
+
msgid "Explore projects"
msgstr "查看项目"
msgid "Explore public groups"
msgstr "æœç´¢å…¬å…±ç¾¤ç»„"
-msgid "External Classification Policy Authorization"
-msgstr "外部分类政策授æƒ"
-
-msgid "External authentication"
-msgstr ""
-
-msgid "External authorization denied access to this project"
-msgstr "å¤–éƒ¨æŽˆæƒæ‹’ç»è®¿é—®æ­¤é¡¹ç›®"
-
-msgid "External authorization request timeout"
-msgstr "外部授æƒè¯·æ±‚è¶…æ—¶"
-
-msgid "ExternalAuthorizationService|Classification Label"
-msgstr "分类标签"
-
-msgid "ExternalAuthorizationService|Classification label"
-msgstr "分类标签"
-
-msgid "ExternalAuthorizationService|When no classification label is set the default label `%{default_label}` will be used."
-msgstr "未设置分类标签时,将使用默认的分类标签`%{default_label}`。"
-
msgid "Failed"
msgstr "已失败"
@@ -1885,6 +2216,9 @@ msgstr "失败的作业"
msgid "Failed to change the owner"
msgstr "æ— æ³•å˜æ›´æ‰€æœ‰è€…"
+msgid "Failed to check related branches."
+msgstr "无法检查相关分支。"
+
msgid "Failed to remove issue from board, please try again."
msgstr "无法从看æ¿ç§»é™¤é—®é¢˜ï¼Œè¯·é‡è¯•。"
@@ -1894,6 +2228,12 @@ msgstr "æ— æ³•åˆ é™¤æµæ°´çº¿è®¡åˆ’"
msgid "Failed to update issues, please try again."
msgstr "更新议题失败, 请é‡è¯•"
+msgid "Failure"
+msgstr "失败"
+
+msgid "Faster as it re-uses the project workspace (falling back to clone if it doesn't exist)"
+msgstr ""
+
msgid "Feb"
msgstr "二"
@@ -1903,18 +2243,12 @@ msgstr "二月"
msgid "Fields on this page are now uneditable, you can configure"
msgstr "当å‰é¡µé¢ä¸Šçš„字段ä¸å¯ç¼–辑,å¯ä»¥é…ç½®"
-msgid "File name"
-msgstr "文件å"
-
msgid "Files"
msgstr "文件"
msgid "Files (%{human_size})"
msgstr "文件(%{human_size})"
-msgid "Fill in the fields below, turn on <strong>%{enable_label}</strong>, and press <strong>%{save_changes}</strong>"
-msgstr ""
-
msgid "Filter by commit message"
msgstr "按æäº¤æ¶ˆæ¯è¿‡æ»¤"
@@ -1933,10 +2267,13 @@ msgstr "首次推é€"
msgid "FirstPushedBy|pushed by"
msgstr "推é€è€…:"
-msgid "Font Color"
+msgid "For internal projects, any logged in user can view pipelines and access job details (output logs and artifacts)"
msgstr ""
-msgid "Footer message"
+msgid "For private projects, any member (guest or higher) can view pipelines and access job details (output logs and artifacts)"
+msgstr ""
+
+msgid "For public projects, anyone can view pipelines and access job details (output logs and artifacts)"
msgstr ""
msgid "Fork"
@@ -1947,7 +2284,7 @@ msgid "ForkedFromProjectPath|Forked from"
msgstr "派生自"
msgid "ForkedFromProjectPath|Forked from %{project_name} (deleted)"
-msgstr "派生自 %{project_name} (删除)"
+msgstr "派生自 %{project_name} (已删除)"
msgid "Forking in progress"
msgstr "派生(Fork)中"
@@ -1955,6 +2292,9 @@ msgstr "派生(Fork)中"
msgid "Format"
msgstr "æ ¼å¼"
+msgid "Found errors in your .gitlab-ci.yml:"
+msgstr "在.gitlab-ci.yml中å‘现错误:"
+
msgid "From %{provider_title}"
msgstr "æ¥è‡ª %{provider_title}"
@@ -1965,172 +2305,19 @@ msgid "From merge request merge until deploy to production"
msgstr "从åˆå¹¶è¯·æ±‚被åˆå¹¶åŽåˆ°éƒ¨ç½²è‡³ç”Ÿäº§çŽ¯å¢ƒ"
msgid "From the Kubernetes cluster details view, install Runner from the applications list"
-msgstr "在Kubernetes群集详细信æ¯è§†å›¾ä¸­ï¼Œä»Žåº”用程åºåˆ—表中安装Runner"
+msgstr "在Kubernetes集群详细信æ¯è§†å›¾ä¸­ï¼Œä»Žåº”用程åºåˆ—表中安装Runner"
msgid "GPG Keys"
msgstr "GPG 密钥"
-msgid "Generate a default set of labels"
-msgstr "生æˆä¸€ç»„默认的标记"
-
-msgid "Geo Nodes"
-msgstr "Geo 节点"
+msgid "General"
+msgstr "通用"
-msgid "Geo allows you to replicate your GitLab instance to other geographical locations."
+msgid "General pipelines"
msgstr ""
-msgid "GeoNodeSyncStatus|Node is failing or broken."
-msgstr "节点出现故障或æŸå。"
-
-msgid "GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage."
-msgstr "节点è¿è¡Œç¼“æ…¢ã€è¶…è½½, æˆ–è€…åœ¨åœæœºåŽåˆšåˆšæ¢å¤ã€‚"
-
-msgid "GeoNodes|Checksummed"
-msgstr "已校验"
-
-msgid "GeoNodes|Database replication lag:"
-msgstr "æ•°æ®åº“åŒæ­¥æ»žåŽ"
-
-msgid "GeoNodes|Disabling a node stops the sync process. Are you sure?"
-msgstr "ç¦ç”¨èŠ‚ç‚¹ä¼šä¸­æ­¢åŒæ­¥è¿‡ç¨‹ã€‚确定继续å—?"
-
-msgid "GeoNodes|Does not match the primary storage configuration"
-msgstr "与主存储é…ç½®ä¸ä¸€è‡´"
-
-msgid "GeoNodes|Failed"
-msgstr "失败"
-
-msgid "GeoNodes|Full"
-msgstr "全部"
-
-msgid "GeoNodes|GitLab version does not match the primary node version"
-msgstr "GitLab版本与主节点版本ä¸ä¸€è‡´"
-
-msgid "GeoNodes|GitLab version:"
-msgstr "GitLab版本:"
-
-msgid "GeoNodes|Health status:"
-msgstr "å¥åº·çŠ¶å†µï¼š"
-
-msgid "GeoNodes|Last event ID processed by cursor:"
-msgstr "游标处ç†çš„æœ€åŽäº‹ä»¶ID:"
-
-msgid "GeoNodes|Last event ID seen from primary:"
-msgstr "主节点中最åŽäº‹ä»¶ID:"
-
-msgid "GeoNodes|Loading nodes"
-msgstr "载入节点"
-
-msgid "GeoNodes|Local Attachments:"
-msgstr "本地附件:"
-
-msgid "GeoNodes|Local LFS objects:"
-msgstr "本地LFS对象:"
-
-msgid "GeoNodes|Local job artifacts:"
-msgstr "本地作业生æˆç‰©:"
-
-msgid "GeoNodes|New node"
-msgstr "新建节点"
-
-msgid "GeoNodes|Node Authentication was successfully repaired."
-msgstr "节点认è¯å·²æˆåŠŸä¿®å¤ã€‚"
-
-msgid "GeoNodes|Node was successfully removed."
-msgstr "节点已æˆåŠŸåˆ é™¤ã€‚"
-
-msgid "GeoNodes|Not checksummed"
-msgstr "未校验"
-
-msgid "GeoNodes|Out of sync"
-msgstr "ä¸åŒæ­¥"
-
-msgid "GeoNodes|Removing a node stops the sync process. Are you sure?"
-msgstr "åˆ é™¤èŠ‚ç‚¹ä¼šåœæ­¢åŒæ­¥ã€‚确定继续?"
-
-msgid "GeoNodes|Replication slot WAL:"
-msgstr "å¤åˆ¶æ§½WAL:"
-
-msgid "GeoNodes|Replication slots:"
-msgstr "å¤åˆ¶æ§½ï¼š"
-
-msgid "GeoNodes|Repositories checksummed:"
-msgstr "已校验仓库:"
-
-msgid "GeoNodes|Repositories:"
-msgstr "仓库:"
-
-msgid "GeoNodes|Repository checksums verified:"
-msgstr "仓库校验和已验è¯ï¼š"
-
-msgid "GeoNodes|Selective"
-msgstr "选择性"
-
-msgid "GeoNodes|Something went wrong while changing node status"
-msgstr "æ›´æ”¹èŠ‚ç‚¹çŠ¶æ€æ—¶å‘生错误"
-
-msgid "GeoNodes|Something went wrong while removing node"
-msgstr "删除节点时å‘生错误"
-
-msgid "GeoNodes|Something went wrong while repairing node"
-msgstr "ä¿®å¤èŠ‚ç‚¹æ—¶å‘生错误"
-
-msgid "GeoNodes|Storage config:"
-msgstr "存储设置:"
-
-msgid "GeoNodes|Sync settings:"
-msgstr "åŒæ­¥è®¾ç½®:"
-
-msgid "GeoNodes|Synced"
-msgstr "å·²åŒæ­¥"
-
-msgid "GeoNodes|Unused slots"
-msgstr "未使用的槽"
-
-msgid "GeoNodes|Unverified"
-msgstr "未验è¯"
-
-msgid "GeoNodes|Used slots"
-msgstr "已使用的槽"
-
-msgid "GeoNodes|Verified"
-msgstr "已验è¯"
-
-msgid "GeoNodes|Wiki checksums verified:"
-msgstr " Wiki校验已验è¯ï¼š"
-
-msgid "GeoNodes|Wikis checksummed:"
-msgstr "wiki已校验"
-
-msgid "GeoNodes|Wikis:"
-msgstr "Wiki:"
-
-msgid "GeoNodes|You have configured Geo nodes using an insecure HTTP connection. We recommend the use of HTTPS."
-msgstr "当å‰Geo节点é…置使用éžåŠ å¯†çš„HTTP连接, 建议使用HTTPS。"
-
-msgid "Geo|All projects"
-msgstr "所有项目"
-
-msgid "Geo|File sync capacity"
-msgstr "æ–‡ä»¶åŒæ­¥é‡"
-
-msgid "Geo|Groups to synchronize"
-msgstr "éœ€åŒæ­¥çš„群组"
-
-msgid "Geo|Projects in certain groups"
-msgstr "特定群组中的项目"
-
-msgid "Geo|Projects in certain storage shards"
-msgstr "特定存储片中的项目"
-
-msgid "Geo|Repository sync capacity"
-msgstr "ä»“åº“åŒæ­¥é‡"
-
-msgid "Geo|Select groups to replicate."
-msgstr "选择è¦å¤åˆ¶çš„群组。"
-
-msgid "Geo|Shards to synchronize"
-msgstr "éœ€åŒæ­¥çš„存储片"
+msgid "Generate a default set of labels"
+msgstr "生æˆä¸€ç»„默认的标记"
msgid "Git repository URL"
msgstr "Git仓库URL"
@@ -2139,7 +2326,10 @@ msgid "Git revision"
msgstr "Gitæäº¤ç‰ˆæœ¬"
msgid "Git storage health information has been reset"
-msgstr "Git 存储å¥åº·ä¿¡æ¯å·²é‡ç½®"
+msgstr "Git 存储è¿è¡ŒçŠ¶å†µä¿¡æ¯å·²é‡ç½®"
+
+msgid "Git strategy for pipelines"
+msgstr ""
msgid "Git version"
msgstr "Git 版本"
@@ -2150,21 +2340,24 @@ msgstr "GitHub导入"
msgid "GitLab CI Linter has been moved"
msgstr "GitLab CI Linter已被转移"
-msgid "GitLab Geo"
-msgstr ""
+msgid "GitLab Group Runners can execute code for all the projects in this group."
+msgstr "Gitlab群组Runnerå¯ä»¥ç”¨æ¥è¿è¡Œç¾¤ç»„内所有项目的代ç ã€‚"
msgid "GitLab Runner section"
msgstr "GitLab Runner"
-msgid "GitLab single sign on URL"
-msgstr ""
-
msgid "Gitaly"
-msgstr ""
+msgstr "Gitaly"
msgid "Gitaly Servers"
msgstr "GitalyæœåС噍"
+msgid "Gitaly|Address"
+msgstr "地å€"
+
+msgid "Go Back"
+msgstr "返回"
+
msgid "Go back"
msgstr "返回"
@@ -2180,23 +2373,20 @@ msgstr "Google 身份验è¯ä¸æ˜¯%{link_to_documentation}。如果您想使用æ­
msgid "Got it!"
msgstr "了解ï¼"
-msgid "GroupRoadmap|Epics let you manage your portfolio of projects more efficiently and with less effort"
-msgstr "利用å²è¯—故事(Epics),产å“线管ç†ä¼šå˜å¾—æ›´è½»æ¾ä¸”更高效"
-
-msgid "GroupRoadmap|From %{dateWord}"
-msgstr "从 %{dateWord}"
+msgid "Graph"
+msgstr "图表"
-msgid "GroupRoadmap|Loading roadmap"
-msgstr "载入路线图"
+msgid "Group CI/CD settings"
+msgstr "群组 CI/CD 设置"
-msgid "GroupRoadmap|Something went wrong while fetching epics"
-msgstr "读å–å²è¯—故事时出错"
+msgid "Group ID"
+msgstr "群组 ID"
-msgid "GroupRoadmap|To view the roadmap, add a planned start or finish date to one of your epics in this group or its subgroups. Only epics in the past 3 months and the next 3 months are shown &ndash; from %{startDate} to %{endDate}."
-msgstr "å¦‚éœ€æŸ¥çœ‹è·¯çº¿å›¾ï¼Œè¯·å°†è®¡åˆ’çš„å¼€å§‹æˆ–ç»“æŸæ—¥æœŸæ·»åŠ åˆ°å½“å‰ç¾¤ç»„或其å­ç»„中的æŸä¸ªå²è¯—æ•…äº‹ã€‚åªæ˜¾ç¤ºè¿‡åŽ»3个月和接下æ¥3个月的å²è¯—故事&ndash; 从 %{startDate} 至 %{endDate}."
+msgid "Group Runners"
+msgstr "群组Runner"
-msgid "GroupRoadmap|Until %{dateWord}"
-msgstr "直到 %{dateWord}"
+msgid "Group maintainers can register group runners in the %{link}"
+msgstr "群组维护者å¯ä»¥åœ¨é€šè¿‡ %{link} 注册群组级 Runner"
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
msgstr "ç¦æ­¢ä¸Žå…¶ä»–群组共享 %{group} 中的项目"
@@ -2222,6 +2412,9 @@ msgstr "无法ç¦ç”¨çˆ¶ç»„的“共享群组é”â€ï¼Œåªæœ‰çˆ¶ç¾¤ç»„的所有者
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr "从 %{ancestor_group_name} 中删除共享群组é”"
+msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
+msgstr ""
+
msgid "GroupsEmptyState|A group is a collection of several projects."
msgstr "群组是几个项目的集åˆã€‚"
@@ -2261,17 +2454,11 @@ msgstr "对ä¸èµ·ï¼Œæ²¡æœ‰æœç´¢åˆ°ä»»ä½•符åˆçš„群组"
msgid "GroupsTree|Sorry, no groups or projects matched your search"
msgstr "对ä¸èµ·ï¼Œæ²¡æœ‰ä»»ä½•ç¾¤ç»„æˆ–é¡¹ç›®ç¬¦åˆæ‚¨çš„æœç´¢"
-msgid "Have your users email"
-msgstr "有你的用户邮件"
-
-msgid "Header message"
-msgstr ""
-
msgid "Health Check"
-msgstr "å¥åº·æ£€æŸ¥"
+msgstr "è¿è¡ŒçŠ¶å†µæ£€æŸ¥"
msgid "Health information can be retrieved from the following endpoints. More information is available"
-msgstr "å¥åº·ä¿¡æ¯å¯ä»¥ä»Žä»¥ä¸‹API路径获å–。如需了解更多信æ¯ï¼Œè¯·æŸ¥çœ‹"
+msgstr "è¿è¡ŒçŠ¶å†µä¿¡æ¯å¯ä»¥ä»Žä»¥ä¸‹API路径获å–。如需了解更多信æ¯ï¼Œè¯·æŸ¥çœ‹"
msgid "HealthCheck|Access token is"
msgstr "访问令牌为"
@@ -2280,7 +2467,7 @@ msgid "HealthCheck|Healthy"
msgstr "å¥åº·"
msgid "HealthCheck|No Health Problems Detected"
-msgstr "没有检测到å¥åº·é—®é¢˜"
+msgstr "没有检测到è¿è¡ŒçŠ¶å†µé—®é¢˜"
msgid "HealthCheck|Unhealthy"
msgstr "éžå¥åº·"
@@ -2298,20 +2485,50 @@ msgid "Hide value"
msgid_plural "Hide values"
msgstr[0] "éšè—值"
+msgid "Hide whitespace changes"
+msgstr ""
+
msgid "History"
msgstr "历å²"
msgid "Housekeeping successfully started"
msgstr "已开始维护"
-msgid "Identity provider single sign on URL"
+msgid "I accept the %{terms_link}"
+msgstr "æˆ‘æŽ¥å— %{terms_link}"
+
+msgid "I accept the|Terms of Service and Privacy Policy"
+msgstr "æœåŠ¡æ¡æ¬¾å’Œéšç§æ”¿ç­–"
+
+msgid "ID"
+msgstr ""
+
+msgid "IDE|Commit"
+msgstr "æäº¤"
+
+msgid "IDE|Edit"
+msgstr "编辑"
+
+msgid "IDE|Go back"
+msgstr "返回"
+
+msgid "IDE|Open in file view"
+msgstr "在文件视图中打开"
+
+msgid "IDE|Review"
+msgstr "审阅"
+
+msgid "Identifier"
msgstr ""
-msgid "If enabled, access to projects will be validated on an external service using their classification label."
-msgstr "如果å¯ç”¨ï¼Œåˆ™ä½¿ç”¨å¤–部æœåŠ¡ä¸Šçš„åˆ†ç±»æ ‡ç­¾æ¥éªŒè¯å¯¹é¡¹ç›®çš„访问æƒé™ã€‚"
+msgid "Identities"
+msgstr ""
+
+msgid "If disabled, the access level will depend on the user's permissions in the project."
+msgstr ""
-msgid "If using GitHub, you’ll see pipeline statuses on GitHub for your commits and pull requests. %{more_info_link}"
-msgstr "如使用GitHub,GitHub上的æäº¤å’Œæ‹‰å–请求(pull request)å°†ä¼šæ˜¾ç¤ºæµæ°´çº¿çжæ€ã€‚ %{more_info_link}"
+msgid "If enabled"
+msgstr ""
msgid "If you already have files you can push them using the %{link_to_cli} below."
msgstr "如果文件已存在,å¯ä»¥ä½¿ç”¨ä¸‹é¢çš„ %{link_to_cli} 推é€å®ƒä»¬ã€‚"
@@ -2319,6 +2536,15 @@ msgstr "如果文件已存在,å¯ä»¥ä½¿ç”¨ä¸‹é¢çš„ %{link_to_cli} 推é€å®ƒä»
msgid "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>."
msgstr "如果HTTP仓库ä¸å¯å…¬å¼€è®¿é—®ï¼Œè¯·å°†èº«ä»½éªŒè¯ä¿¡æ¯æ·»åŠ åˆ°URL: <code>https://username:password@gitlab.company.com/group/project.git</code>."
+msgid "ImageDiffViewer|2-up"
+msgstr "并列(2-up)"
+
+msgid "ImageDiffViewer|Onion skin"
+msgstr "分帧(Onion skin)"
+
+msgid "ImageDiffViewer|Swipe"
+msgstr "滑动"
+
msgid "Import"
msgstr "导入"
@@ -2334,17 +2560,11 @@ msgstr "从 GitHub 导入仓库"
msgid "Import repository"
msgstr "导入仓库"
-msgid "ImportButtons|Connect repositories from"
-msgstr "用以下方å¼è¿žæŽ¥ä»“库"
+msgid "Include a Terms of Service agreement and Privacy Policy that all users must accept."
+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 "Inline"
+msgstr ""
msgid "Install Runner on Kubernetes"
msgstr "在Kubernetes上安装Runner"
@@ -2352,16 +2572,15 @@ msgstr "在Kubernetes上安装Runner"
msgid "Install a Runner compatible with GitLab CI"
msgstr "安装一个与 GitLab CI 兼容的 Runner"
-msgid "Instance"
-msgid_plural "Instances"
-msgstr[0] "实例"
-
msgid "Instance does not support multiple Kubernetes clusters"
-msgstr "å®žä¾‹ä¸æ”¯æŒå¤šä¸ªKubernetes群集"
+msgstr "å®žä¾‹ä¸æ”¯æŒå¤šä¸ªKubernetes集群"
msgid "Integrations"
msgstr "导入所有仓库"
+msgid "Integrations Settings"
+msgstr ""
+
msgid "Interested parties can even contribute by pushing commits if they want to."
msgstr "相关人员甚至å¯ä»¥é€šè¿‡æŽ¨é€æäº¤æ¥ä¸ºé¡¹ç›®ä½œå‡ºè´¡çŒ®ã€‚"
@@ -2377,8 +2596,8 @@ msgstr "循环周期"
msgid "Introducing Cycle Analytics"
msgstr "周期分æžç®€ä»‹"
-msgid "Issue board focus mode"
-msgstr "è®®é¢˜çœ‹æ¿æ¨¡å¼"
+msgid "Issue Board"
+msgstr ""
msgid "Issue events"
msgstr "议题事件"
@@ -2386,9 +2605,6 @@ msgstr "议题事件"
msgid "IssueBoards|Board"
msgstr "看æ¿"
-msgid "IssueBoards|Boards"
-msgstr "看æ¿"
-
msgid "Issues"
msgstr "议题"
@@ -2401,6 +2617,12 @@ msgstr "一"
msgid "January"
msgstr "一月"
+msgid "Job"
+msgstr ""
+
+msgid "Job has been erased"
+msgstr "作业已被删除"
+
msgid "Jobs"
msgstr "作业"
@@ -2417,7 +2639,7 @@ msgid "June"
msgstr "六月"
msgid "Koding"
-msgstr ""
+msgstr "Koding"
msgid "Kubernetes"
msgstr "Kubernetes"
@@ -2435,13 +2657,16 @@ msgid "Kubernetes cluster integration was successfully removed."
msgstr "Kubernetes集群集æˆå·²æˆåŠŸåˆ é™¤ã€‚"
msgid "Kubernetes cluster was successfully updated."
-msgstr "Kubernetes群集已æˆåŠŸæ›´æ–°ã€‚"
+msgstr "Kubernetes集群已æˆåŠŸæ›´æ–°ã€‚"
msgid "Kubernetes configured"
msgstr "Kuberneteså·²é…ç½®"
msgid "Kubernetes service integration has been deprecated. %{deprecated_message_content} your Kubernetes clusters using the new <a href=\"%{url}\"/>Kubernetes Clusters</a> page"
-msgstr "KubernetesæœåС集æˆå³å°†è¢«åœç”¨ã€‚ 请使用新的 <a href=\"%{url}\"/>Kubernetes群集</a> 页é¢%{deprecated_message_content} Kubernetes集群"
+msgstr "KubernetesæœåС集æˆå³å°†è¢«åœç”¨ã€‚ 请使用新的 <a href=\"%{url}\"/>Kubernetes集群</a> 页é¢%{deprecated_message_content} Kubernetes集群"
+
+msgid "LFS"
+msgstr ""
msgid "LFSStatus|Disabled"
msgstr "åœç”¨"
@@ -2452,6 +2677,9 @@ msgstr "å¯ç”¨"
msgid "Label"
msgstr "标记"
+msgid "Label actions dropdown"
+msgstr "标记æ“作下拉èœå•"
+
msgid "LabelSelect|%{firstLabelName} +%{remainingLabelCount} more"
msgstr "%{firstLabelName} +%{remainingLabelCount} 更多"
@@ -2467,6 +2695,9 @@ msgstr "标记å¯ä»¥åº”用于 %{features}。群组标记å¯ç”¨äºŽç¾¤ç»„中的所
msgid "Labels can be applied to issues and merge requests to categorize them."
msgstr "标记å¯ç”¨äºŽå¯¹è®®é¢˜å’Œåˆå¹¶è¯·æ±‚进行分类。"
+msgid "Labels can be applied to issues and merge requests."
+msgstr "标记å¯ç”¨äºŽè®®é¢˜å’Œåˆå¹¶è¯·æ±‚。"
+
msgid "Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>"
msgstr "<span>将标记</span> %{labelTitle} <span>å‡çº§ä¸ºç¾¤ç»„标记?</span>"
@@ -2501,6 +2732,9 @@ msgstr "您推é€äº†"
msgid "LastPushEvent|at"
msgstr "于"
+msgid "Latest changes"
+msgstr "最新更改"
+
msgid "Learn more"
msgstr "进一步了解"
@@ -2525,9 +2759,6 @@ msgstr "退出群组"
msgid "Leave project"
msgstr "退出项目"
-msgid "License"
-msgstr "许å¯åè®®"
-
msgid "List"
msgstr "列表"
@@ -2537,6 +2768,9 @@ msgstr "列出GitHub仓库"
msgid "Loading the GitLab IDE..."
msgstr "加载GitLab IDE..."
+msgid "Loading..."
+msgstr "正在加载..."
+
msgid "Lock"
msgstr "é”定"
@@ -2546,21 +2780,18 @@ msgstr "é”定 %{issuableDisplayName}"
msgid "Lock not found"
msgstr "未找到é”"
+msgid "Lock to current projects"
+msgstr "é”定到当å‰é¡¹ç›®"
+
msgid "Locked"
msgstr "å·²é”定"
-msgid "Locked Files"
-msgstr "å·²é”定文件"
-
-msgid "Locks give the ability to lock specific file or folder."
-msgstr "加é”å¯ä»¥é”定特定的文件或文件夹。"
+msgid "Locked to current projects"
+msgstr "å·²é”定到目å‰çš„项目"
msgid "Login"
msgstr "登录"
-msgid "Make everyone on your team more productive regardless of their location. GitLab Geo creates read-only mirrors of your GitLab instance so you can reduce the time it takes to clone and fetch large repos."
-msgstr "GitLab Geo å¯ä»¥åˆ›å»º GitLab 实例的åªè¯»é•œåƒ, 使得从远端克隆和拉å–大型代ç ä»“库的时间大大缩短,æé«˜å›¢é˜Ÿæˆå‘˜çš„工作效率。"
-
msgid "Manage all notifications"
msgstr "管ç†å…¨éƒ¨é€šçŸ¥"
@@ -2573,20 +2804,20 @@ msgstr "ç®¡ç†æ ‡è®°"
msgid "Manage project labels"
msgstr "管ç†é¡¹ç›®æ ‡è®°"
-msgid "Manage your group’s membership while adding another level of security with SAML."
-msgstr ""
-
msgid "Mar"
msgstr "三"
msgid "March"
msgstr "三月"
-msgid "Mark done"
+msgid "Mark todo as done"
msgstr "标记为已完æˆ"
+msgid "Markdown enabled"
+msgstr "支æŒMarkdownæ ¼å¼"
+
msgid "Maximum git storage failures"
-msgstr "最大 git 存储失败"
+msgstr "最大 git 存储失败次数"
msgid "May"
msgstr "五"
@@ -2597,8 +2828,8 @@ msgstr "䏭使•°"
msgid "Members"
msgstr "æˆå‘˜"
-msgid "Members will be forwarded here when signing in to your group. Get this from your identity provider, where it can also be called \"SSO Service Location\", \"SAML Token Issuance Endpoint\", or \"SAML 2.0/W-Federation URL\"."
-msgstr ""
+msgid "Merge Request:"
+msgstr "åˆå¹¶è¯·æ±‚:"
msgid "Merge Requests"
msgstr "åˆå¹¶è¯·æ±‚"
@@ -2609,9 +2840,30 @@ msgstr "åˆå¹¶äº‹ä»¶"
msgid "Merge request"
msgstr "åˆå¹¶è¯·æ±‚"
+msgid "Merge requests"
+msgstr "åˆå¹¶è¯·æ±‚"
+
msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
msgstr "åˆå¹¶è¯·æ±‚用于æå‡ºå¯¹é¡¹ç›®çš„æ›´æ”¹ä¸Žä»–人讨论"
+msgid "MergeRequests|Resolve this discussion in a new issue"
+msgstr ""
+
+msgid "MergeRequests|Saving the comment failed"
+msgstr ""
+
+msgid "MergeRequests|Toggle comments for this file"
+msgstr ""
+
+msgid "MergeRequests|Updating discussions failed"
+msgstr ""
+
+msgid "MergeRequests|View file @ %{commitId}"
+msgstr ""
+
+msgid "MergeRequests|View replaced file @ %{commitId}"
+msgstr ""
+
msgid "Merged"
msgstr "å·²åˆå¹¶"
@@ -2624,78 +2876,12 @@ msgstr "指标 - Influx"
msgid "Metrics - Prometheus"
msgstr "指标 - Prometheus"
-msgid "Metrics|Business"
-msgstr "业务"
-
-msgid "Metrics|Create metric"
-msgstr "创建指标"
-
-msgid "Metrics|Edit metric"
-msgstr "编辑指标"
-
-msgid "Metrics|For grouping similar metrics"
-msgstr "用于分组类似指标"
-
-msgid "Metrics|Label of the chart's vertical axis. Usually the type of the unit being charted. The horizontal axis (X-axis) always represents time."
-msgstr "图表纵轴的标签。通常表示绘制å•ä½ã€‚水平轴(X轴)一般表示时间。"
-
-msgid "Metrics|Legend label (optional)"
-msgstr "图例标签(å¯é€‰ï¼‰"
-
-msgid "Metrics|Must be a valid PromQL query."
-msgstr "必须是有效的 PromQL 查询。"
-
-msgid "Metrics|Name"
-msgstr "åç§°"
-
-msgid "Metrics|New metric"
-msgstr "创建指标"
-
-msgid "Metrics|Prometheus Query Documentation"
-msgstr "Prometheus查询文档"
-
-msgid "Metrics|Query"
-msgstr "查询"
-
-msgid "Metrics|Response"
-msgstr "å“应"
-
-msgid "Metrics|System"
-msgstr "系统"
-
-msgid "Metrics|Type"
-msgstr "类型"
-
-msgid "Metrics|Unit label"
-msgstr "å•使 ‡ç­¾"
-
-msgid "Metrics|Used as a title for the chart"
-msgstr "用作图表的标题"
-
-msgid "Metrics|Used if the query returns a single series. If it returns multiple series, their legend labels will be picked up from the response."
-msgstr "用于查询返回å•个系列时。如果返回多个系列,相应的图例标签将从返回数æ®ä¸­é€‰å–。"
-
-msgid "Metrics|Y-axis label"
-msgstr "Y轴标签"
-
-msgid "Metrics|e.g. HTTP requests"
-msgstr "例如HTTP请求"
-
-msgid "Metrics|e.g. Requests/second"
-msgstr "例如æ¯ç§’请求数"
-
-msgid "Metrics|e.g. Throughput"
-msgstr "例如åžåé‡"
-
-msgid "Metrics|e.g. rate(http_requests_total[5m])"
-msgstr "速率(5分钟内所有http请求)"
-
-msgid "Metrics|e.g. req/sec"
-msgstr "例如æ¯ç§’请求数"
-
msgid "Milestone"
msgstr "里程碑"
+msgid "Milestones"
+msgstr "里程碑"
+
msgid "Milestones|Delete milestone"
msgstr "删除里程碑"
@@ -2714,9 +2900,6 @@ msgstr "å°† %{milestoneTitle} å‡çº§ä¸ºç¾¤ç»„里程碑?"
msgid "Milestones|Promote Milestone"
msgstr "å‡çº§é‡Œç¨‹ç¢‘"
-msgid "Milestones|This action cannot be reversed."
-msgstr "该æ“作无法撤销。"
-
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "新建 SSH 公钥"
@@ -2729,8 +2912,8 @@ msgstr "关闭"
msgid "Monitoring"
msgstr "监控"
-msgid "More info"
-msgstr "更多信æ¯"
+msgid "More actions"
+msgstr "更多æ“作"
msgid "More information"
msgstr "更多信æ¯"
@@ -2744,12 +2927,30 @@ msgstr "移动"
msgid "Move issue"
msgstr "移动议题"
-msgid "Multiple issue boards"
-msgstr "多个议题看æ¿"
+msgid "Name"
+msgstr "å§“å"
msgid "Name new label"
msgstr "命忖°æ ‡è®°"
+msgid "Name your individual key via a title"
+msgstr ""
+
+msgid "Nav|Help"
+msgstr "帮助"
+
+msgid "Nav|Home"
+msgstr "首页"
+
+msgid "Nav|Sign In / Register"
+msgstr "注册/登录"
+
+msgid "Nav|Sign out and sign in with a different account"
+msgstr "退出并登录到其他账å·"
+
+msgid "New Identity"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "新建议题"
@@ -2760,6 +2961,9 @@ msgstr "新建Kubernetes集群"
msgid "New Kubernetes cluster"
msgstr "新建Kubernetes集群"
+msgid "New Label"
+msgstr ""
+
msgid "New Pipeline Schedule"
msgstr "åˆ›å»ºæµæ°´çº¿è®¡åˆ’"
@@ -2772,14 +2976,14 @@ msgstr "新分支ä¸å¯ç”¨"
msgid "New directory"
msgstr "新建目录"
-msgid "New epic"
-msgstr "新建å²è¯—故事"
-
msgid "New file"
msgstr "新建文件"
msgid "New group"
-msgstr "新群组"
+msgstr "新建群组"
+
+msgid "New identity"
+msgstr ""
msgid "New issue"
msgstr "新建议题"
@@ -2790,6 +2994,9 @@ msgstr "新建标记"
msgid "New merge request"
msgstr "新建åˆå¹¶è¯·æ±‚"
+msgid "New pipelines will cancel older, pending pipelines on the same branch"
+msgstr ""
+
msgid "New project"
msgstr "新建项目"
@@ -2805,8 +3012,8 @@ msgstr "新建å­ç¾¤ç»„"
msgid "New tag"
msgstr "新建标签"
-msgid "No Label"
-msgstr "无标记"
+msgid "No"
+msgstr "å¦"
msgid "No assignee"
msgstr "未指派"
@@ -2821,13 +3028,22 @@ msgid "No due date"
msgstr "无截止日期"
msgid "No estimate or time spent"
-msgstr "无估算或消耗的时间"
+msgstr "无预计或已用时间"
msgid "No file chosen"
msgstr "未选定任何文件"
-msgid "No labels created yet."
-msgstr "尚未创建标记"
+msgid "No files found"
+msgstr ""
+
+msgid "No files found."
+msgstr "没有找到文件。"
+
+msgid "No merge requests found"
+msgstr "找ä¸åˆ°åˆå¹¶è¯·æ±‚"
+
+msgid "No messages were logged"
+msgstr "未记录任何消æ¯"
msgid "No repository"
msgstr "没有仓库"
@@ -2859,15 +3075,9 @@ msgstr "æ•°æ®ä¸è¶³"
msgid "Note that the master branch is automatically protected. %{link_to_protected_branches}"
msgstr "请注æ„,master分支自动å—ä¿æŠ¤ã€‚%{link_to_protected_branches}"
-msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow connecting repositories without generating a Personal Access Token."
-msgstr "æç¤ºï¼šä½œä¸ºGitLab管ç†å‘˜ï¼Œå¯ä»¥é…ç½® %{github_integration_link},这将å…许通过GitHub登录并å…许连接Github代ç ä»“库而ä¸éœ€è¦ä¸ªäººè®¿é—®ä»¤ç‰Œã€‚"
-
msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
msgstr "æç¤ºï¼šä½œä¸ºGitLab管ç†å‘˜ï¼Œå¯ä»¥é…ç½® %{github_integration_link},这将å…许通过GitHub登录并å…许导入Github代ç ä»“库而ä¸éœ€è¦ä¸ªäººè®¿é—®ä»¤ç‰Œã€‚"
-msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow connecting repositories without generating a Personal Access Token."
-msgstr "æç¤ºï¼šå¦‚GitLab管ç†å‘˜é…ç½® %{github_integration_link},将å…许通过GitHub登录并å…许连接Github代ç ä»“库而ä¸éœ€è¦ä¸ªäººè®¿é—®ä»¤ç‰Œã€‚"
-
msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
msgstr "æç¤ºï¼šå¦‚GitLab管ç†å‘˜é…ç½® %{github_integration_link},将å…许通过GitHub登录并å…许导入Github代ç ä»“库而ä¸éœ€è¦ä¸ªäººè®¿é—®ä»¤ç‰Œã€‚"
@@ -2943,9 +3153,6 @@ msgstr "å一月"
msgid "Number of access attempts"
msgstr "å°è¯•访问次数"
-msgid "OK"
-msgstr "确定"
-
msgid "Oct"
msgstr "å"
@@ -2953,22 +3160,19 @@ msgid "October"
msgstr "åæœˆ"
msgid "OfSearchInADropdown|Filter"
-msgstr "筛选"
-
-msgid "Once imported, repositories can be mirrored over SSH. Read more %{ssh_link}"
-msgstr "仓库导入åŽï¼Œå¯ä»¥é€šè¿‡ SSH 拉å–镜åƒã€‚了解更多 %{ssh_link}"
+msgstr "过滤"
msgid "Online IDE integration settings."
+msgstr "在线IDE集æˆè®¾ç½®ã€‚"
+
+msgid "Only comments from the following commit are shown below"
msgstr ""
msgid "Only project members can comment."
msgstr "åªæœ‰é¡¹ç›®æˆå‘˜å¯ä»¥å‘表评论。"
-msgid "Open"
-msgstr "打开"
-
-msgid "Opened"
-msgstr "已打开"
+msgid "Open in Xcode"
+msgstr "用Xcode打开"
msgid "OpenedNDaysAgo|Opened"
msgstr "创建于"
@@ -2976,14 +3180,23 @@ msgstr "创建于"
msgid "Opens in a new window"
msgstr "打开一个新窗å£"
+msgid "Operations"
+msgstr "è¿ç»´"
+
msgid "Options"
msgstr "æ“作"
+msgid "Or you can choose one of the suggested colors below"
+msgstr ""
+
+msgid "Other Labels"
+msgstr "其他标记"
+
msgid "Otherwise it is recommended you start with one of the options below."
msgstr "å¦åˆ™ï¼Œå»ºè®®æ‚¨ä»Žä¸‹é¢çš„一个选项开始。"
msgid "Outbound requests"
-msgstr ""
+msgstr "外å‘请求"
msgid "Overview"
msgstr "概览"
@@ -3007,17 +3220,32 @@ msgid "Pagination|« First"
msgstr "« 首页"
msgid "Part of merge request changes"
-msgstr ""
+msgstr "包å«äºŽåˆå¹¶è¯·æ±‚å˜æ›´ä¸­"
msgid "Password"
msgstr "密ç "
+msgid "Paste your public SSH key, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'. Don't use your private SSH key."
+msgstr ""
+
+msgid "Pause"
+msgstr "æš‚åœ"
+
msgid "Pending"
-msgstr "等待处ç†"
+msgstr "等待中"
-msgid "Performance optimization"
+msgid "Per job. If a job passes this threshold, it will be marked as failed"
msgstr ""
+msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgstr "执行高级选项,例如更改路径,移动或删除群组。"
+
+msgid "Performance optimization"
+msgstr "性能优化"
+
+msgid "Permissions"
+msgstr "æƒé™"
+
msgid "Personal Access Token"
msgstr "个人访问凭è¯"
@@ -3025,7 +3253,7 @@ msgid "Pipeline"
msgstr "æµæ°´çº¿"
msgid "Pipeline Health"
-msgstr "æµæ°´çº¿å¥åº·æŒ‡æ ‡"
+msgstr "æµæ°´çº¿è¿è¡ŒçŠ¶å†µæŒ‡æ ‡"
msgid "Pipeline Schedule"
msgstr "æµæ°´çº¿è®¡åˆ’"
@@ -3033,8 +3261,8 @@ msgstr "æµæ°´çº¿è®¡åˆ’"
msgid "Pipeline Schedules"
msgstr "æµæ°´çº¿è®¡åˆ’"
-msgid "Pipeline quota"
-msgstr "æµæ°´çº¿é…é¢"
+msgid "Pipeline triggers"
+msgstr ""
msgid "PipelineCharts|Failed:"
msgstr "失败:"
@@ -3132,11 +3360,23 @@ msgstr "当剿²¡æœ‰æµæ°´çº¿ã€‚"
msgid "Pipelines|This project is not currently set up to run pipelines."
msgstr "æ­¤é¡¹ç›®å½“å‰æœªé…ç½®è¿è¡Œæµæ°´çº¿ã€‚"
-msgid "Pipeline|Retry pipeline"
-msgstr "é‡è¯•æµæ°´çº¿"
+msgid "Pipeline|Create for"
+msgstr "创建于"
+
+msgid "Pipeline|Create pipeline"
+msgstr "åˆ›å»ºæµæ°´çº¿"
-msgid "Pipeline|Retry pipeline #%{pipelineId}?"
-msgstr "é‡è¯•æµæ°´çº¿ï¼ƒ%{pipelineId}å—?"
+msgid "Pipeline|Existing branch name or tag"
+msgstr "现有分支å称或者标签"
+
+msgid "Pipeline|Run Pipeline"
+msgstr "è¿è¡Œæµæ°´çº¿"
+
+msgid "Pipeline|Search branches"
+msgstr "æœç´¢åˆ†æ”¯"
+
+msgid "Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default."
+msgstr "指定è¦åœ¨æ­¤æ¬¡è¿è¡Œä¸­ä½¿ç”¨çš„å˜é‡å€¼ã€‚默认情况下将使用 %{settings_link} 中指定的值。"
msgid "Pipeline|Stop pipeline"
msgstr "åœæ­¢æµæ°´çº¿"
@@ -3144,8 +3384,8 @@ msgstr "åœæ­¢æµæ°´çº¿"
msgid "Pipeline|Stop pipeline #%{pipelineId}?"
msgstr "åœæ­¢æµæ°´çº¿ï¼ƒ%{pipelineId}å—?"
-msgid "Pipeline|You’re about to retry pipeline %{pipelineId}."
-msgstr "å³å°†é‡è¯•æµæ°´çº¿ %{pipelineId}。"
+msgid "Pipeline|Variables"
+msgstr "å˜é‡"
msgid "Pipeline|You’re about to stop pipeline %{pipelineId}."
msgstr "å³å°†åœæ­¢æµæ°´çº¿ %{pipelineId}。"
@@ -3162,20 +3402,26 @@ msgstr "于阶段"
msgid "Pipeline|with stages"
msgstr "于阶段"
-msgid "PlantUML"
+msgid "Plain diff"
msgstr ""
+msgid "PlantUML"
+msgstr "PlantUML"
+
msgid "Play"
msgstr "è¿è¡Œ"
-msgid "Please <a href=%{link_to_billing} target=\"_blank\" rel=\"noopener noreferrer\">enable billing for one of your projects to be able to create a Kubernetes cluster</a>, then try again."
-msgstr "请 <a href=%{link_to_billing} target=\"_blank\" rel=\"noopener noreferrer\">为æŸä¸ªé¡¹ç›®å¯ç”¨è®¡è´¹åŠŸèƒ½ï¼Œä»¥ä¾¿èƒ½å¤Ÿåˆ›å»ºKubernetes群集</a>,然åŽé‡è¯•。"
+msgid "Please accept the Terms of Service before continuing."
+msgstr "è¯·æŽ¥å—æœåŠ¡æ¡æ¬¾ä»¥ç»§ç»­ã€‚"
+
+msgid "Please select at least one filter to see results"
+msgstr "è¯·è‡³å°‘é€‰æ‹©ä¸€ä¸ªè¿‡æ»¤å™¨æ¥æŸ¥çœ‹ç»“æžœ"
msgid "Please solve the reCAPTCHA"
msgstr "请填写验è¯ç ã€‚"
-msgid "Please wait while we connect to your repository. Refresh at will."
-msgstr "连接代ç ä»“库中,请ç¨å€™ã€‚å¯åœ¨ä»»æ„时刻刷新以获å–当å‰çжæ€ã€‚"
+msgid "Please try again"
+msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr "导入代ç ä»“库中,请ç¨å€™ã€‚å¯åœ¨ä»»æ„时刻刷新以获å–当å‰çжæ€ã€‚"
@@ -3183,8 +3429,20 @@ msgstr "导入代ç ä»“库中,请ç¨å€™ã€‚å¯åœ¨ä»»æ„时刻刷新以获å–当
msgid "Preferences"
msgstr "å好设置"
-msgid "Primary"
-msgstr "主è¦"
+msgid "Preferences|Navigation theme"
+msgstr "导航主题"
+
+msgid "Prioritize"
+msgstr "优先"
+
+msgid "Prioritize label"
+msgstr "优先标记"
+
+msgid "Prioritized Labels"
+msgstr "优先的标记"
+
+msgid "Prioritized label"
+msgstr "优先的标记"
msgid "Private - Project access must be granted explicitly to each user."
msgstr "ç§äºº - å¿…é¡»å‘æ¯ä¸ªç”¨æˆ·æ˜Žç¡®æŽˆäºˆé¡¹ç›®è®¿é—®æƒé™ã€‚"
@@ -3196,10 +3454,16 @@ msgid "Private projects can be created in your personal namespace with:"
msgstr "ç§æœ‰é¡¹ç›®å¯ä»¥åœ¨ä¸ªäººå称空间中创建:"
msgid "Profile"
-msgstr "用户信æ¯"
+msgstr "用户资料"
msgid "Profiles|Account scheduled for removal."
-msgstr "叿ˆ·åˆ é™¤è®¡åˆ’。"
+msgstr "叿ˆ·å·²å®‰æŽ’被删除。"
+
+msgid "Profiles|Change username"
+msgstr "更改用户å"
+
+msgid "Profiles|Current path: %{path}"
+msgstr "当å‰è·¯å¾„: %{path}"
msgid "Profiles|Delete Account"
msgstr "åˆ é™¤å¸æˆ·"
@@ -3219,9 +3483,21 @@ msgstr "å¯†ç æ— æ•ˆ"
msgid "Profiles|Invalid username"
msgstr "ç”¨æˆ·åæ— æ•ˆ"
+msgid "Profiles|Path"
+msgstr "路径"
+
msgid "Profiles|Type your %{confirmationValue} to confirm:"
msgstr "键入您的 %{confirmationValue} 以确认:"
+msgid "Profiles|Update username"
+msgstr "更新用户å"
+
+msgid "Profiles|Username change failed - %{message}"
+msgstr "ç”¨æˆ·åæ›´æ”¹å¤±è´¥ - %{message}"
+
+msgid "Profiles|Username successfully changed"
+msgstr "ç”¨æˆ·åæ›´æ”¹æˆåŠŸ"
+
msgid "Profiles|You don't have access to delete this user."
msgstr "您无æƒåˆ é™¤æ­¤ç”¨æˆ·ã€‚"
@@ -3235,11 +3511,17 @@ msgid "Profiles|your account"
msgstr "æ‚¨çš„å¸æˆ·"
msgid "Profiling - Performance bar"
-msgstr ""
+msgstr "åˆ†æž - 性能æ "
msgid "Programming languages used in this repository"
msgstr "当å‰ä»£ç ä»“库中使用的编程语言"
+msgid "Progress"
+msgstr "进度"
+
+msgid "Project"
+msgstr "项目"
+
msgid "Project '%{project_name}' is in the process of being deleted."
msgstr "项目 “%{project_name}†正在被删除。"
@@ -3252,6 +3534,9 @@ msgstr "项目 '%{project_name}' 已创建æˆåŠŸã€‚"
msgid "Project '%{project_name}' was successfully updated."
msgstr "项目 '%{project_name}' 已更新完æˆã€‚"
+msgid "Project Badges"
+msgstr "项目徽章"
+
msgid "Project access must be granted explicitly to each user."
msgstr "项目访问æƒé™å¿…须明确授æƒç»™æ¯ä¸ªç”¨æˆ·ã€‚"
@@ -3279,30 +3564,6 @@ msgstr "项目导出已开始。下载链接将通过电å­é‚®ä»¶å‘é€ã€‚"
msgid "ProjectActivityRSS|Subscribe"
msgstr "订阅"
-msgid "ProjectCreationLevel|Allowed to create projects"
-msgstr "å…许创建项目"
-
-msgid "ProjectCreationLevel|Default project creation protection"
-msgstr "é»˜è®¤é¡¹ç›®åˆ›å»ºä¿æŠ¤"
-
-msgid "ProjectCreationLevel|Developers + Masters"
-msgstr "Developers + Masters"
-
-msgid "ProjectCreationLevel|Masters"
-msgstr "Masters"
-
-msgid "ProjectCreationLevel|No one"
-msgstr "ç¦æ­¢"
-
-msgid "ProjectFeature|Disabled"
-msgstr "åœç”¨"
-
-msgid "ProjectFeature|Everyone with access"
-msgstr "任何对项目有访问æƒçš„人"
-
-msgid "ProjectFeature|Only team members"
-msgstr "åªé™å›¢é˜Ÿæˆå‘˜"
-
msgid "ProjectFileTree|Name"
msgstr "åç§°"
@@ -3312,27 +3573,6 @@ msgstr "从未"
msgid "ProjectLifecycle|Stage"
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 "项目"
@@ -3357,6 +3597,9 @@ msgstr "对ä¸èµ·ï¼Œæ²¡æœ‰æœç´¢åˆ°ç¬¦åˆæ¡ä»¶çš„项目"
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr "æ­¤åŠŸèƒ½éœ€è¦æµè§ˆå™¨æ”¯æŒ localStorage"
+msgid "PrometheusDashboard|Time"
+msgstr "æ—¶é—´"
+
msgid "PrometheusService|%{exporters} with %{metrics} were found"
msgstr "找到%{exporters} åŠ %{metrics}"
@@ -3378,18 +3621,9 @@ msgstr "默认情况下,Prometheus ä¾¦å¬ â€˜http://localhost:9090’。ä¸å»º
msgid "PrometheusService|Common metrics"
msgstr "常用指标"
-msgid "PrometheusService|Common metrics are automatically monitored based on a library of metrics from popular exporters."
-msgstr "常用指标会根æ®åº”用广泛的导出器指标库自动监控。"
-
-msgid "PrometheusService|Custom metrics"
-msgstr "自定义指标"
-
msgid "PrometheusService|Finding and configuring metrics..."
msgstr "查找和é…置指标..."
-msgid "PrometheusService|Finding custom metrics..."
-msgstr "查找自定义指标..."
-
msgid "PrometheusService|Install Prometheus on clusters"
msgstr "在群集上安装Prometheus"
@@ -3402,24 +3636,21 @@ msgstr "手动é…ç½®"
msgid "PrometheusService|Metrics"
msgstr "指标"
+msgid "PrometheusService|Metrics are automatically configured and monitored based on a library of metrics from popular exporters."
+msgstr ""
+
msgid "PrometheusService|Missing environment variable"
msgstr "没有环境å˜é‡"
msgid "PrometheusService|More information"
msgstr "更多的信æ¯"
-msgid "PrometheusService|New metric"
-msgstr "新建指标"
-
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr "Prometheus API 地å€ï¼Œä¾‹å¦‚ http://prometheus.example.com/"
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr "Prometheus正在被群集自动管ç†"
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
-msgstr "在首次部署到æŸçŽ¯å¢ƒä¹‹åŽ, 这些指标æ‰ä¼šè¢«ç›‘控"
-
msgid "PrometheusService|Time-series monitoring service"
msgstr "æ—¶é—´åºåˆ—监控æœåŠ¡"
@@ -3435,23 +3666,29 @@ msgstr "等待首次部署到环境以查找常用指标"
msgid "Promote"
msgstr "å‡çº§"
-msgid "Promote to Group Label"
-msgstr "å‡çº§åˆ°ç¾¤ç»„标记"
+msgid "Promote these project milestones into a group milestone."
+msgstr "将这些项目里程碑å‡çº§ä¸ºç¾¤ç»„里程碑。"
msgid "Promote to Group Milestone"
msgstr "å‡çº§åˆ°ç¾¤ç»„里程碑"
+msgid "Promote to group label"
+msgstr "å‡çº§åˆ°ç¾¤ç»„标记"
+
msgid "Protip:"
msgstr "专家æç¤ºï¼š"
+msgid "Provider"
+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 "Public pipelines"
+msgstr ""
msgid "Push events"
msgstr "推é€äº‹ä»¶"
@@ -3462,12 +3699,12 @@ msgstr "从命令行推é€é¡¹ç›®"
msgid "Push to create a project"
msgstr "通过推é€åˆ›å»ºé¡¹ç›®"
-msgid "PushRule|Committer restriction"
-msgstr "æäº¤é™åˆ¶"
-
msgid "Quick actions can be used in the issues description and comment boxes."
msgstr "快速æ“作å¯ç”¨äºŽè®®é¢˜æè¿°å’Œè¯„论框。"
+msgid "Re-deploy"
+msgstr ""
+
msgid "Read more"
msgstr "进一步了解"
@@ -3475,13 +3712,7 @@ msgid "Readme"
msgstr "自述文件"
msgid "Real-time features"
-msgstr ""
-
-msgid "RefSwitcher|Branches"
-msgstr "分支"
-
-msgid "RefSwitcher|Tags"
-msgstr "标签"
+msgstr "实时功能"
msgid "Reference:"
msgstr "引用:"
@@ -3489,6 +3720,12 @@ msgstr "引用:"
msgid "Register / Sign In"
msgstr "注册/登录"
+msgid "Register and see your runners for this group."
+msgstr "注册并查看当å‰ç¾¤ç»„çš„Runner。"
+
+msgid "Register and see your runners for this project."
+msgstr ""
+
msgid "Registry"
msgstr "注册表"
@@ -3519,50 +3756,68 @@ msgstr "ç¨å޿醒"
msgid "Remove"
msgstr "删除"
+msgid "Remove Runner"
+msgstr "移除Runner"
+
msgid "Remove avatar"
msgstr "删除头åƒ"
+msgid "Remove priority"
+msgstr "删除优先级"
+
msgid "Remove project"
msgstr "删除项目"
-msgid "Repair authentication"
-msgstr "ä¿®å¤è®¤è¯"
-
-msgid "Repo by URL"
-msgstr "从URL导入仓库"
-
msgid "Repository"
msgstr "仓库"
-msgid "Repository has no locks."
-msgstr "当å‰ä»“åº“æ— åŠ é”æ–‡ä»¶ã€‚"
+msgid "Repository Settings"
+msgstr ""
msgid "Repository maintenance"
-msgstr ""
+msgstr "仓库维护"
-msgid "Repository mirror settings"
-msgstr ""
+msgid "Repository mirror"
+msgstr "仓库镜åƒ"
msgid "Repository storage"
-msgstr ""
+msgstr "仓库存储"
msgid "Request Access"
msgstr "申请æƒé™"
+msgid "Require all users to accept Terms of Service and Privacy Policy when they access GitLab."
+msgstr "è¦æ±‚所有用户在访问GitLabæ—¶æŽ¥å—æœåŠ¡æ¡æ¬¾å’Œéšç§æ”¿ç­–。"
+
msgid "Reset git storage health information"
-msgstr "é‡ç½® Git 存储的å¥åº·ä¿¡æ¯"
+msgstr "é‡ç½® Git 存储的è¿è¡ŒçŠ¶å†µä¿¡æ¯"
msgid "Reset health check access token"
-msgstr "é‡ç½®å¥åº·æ£€æŸ¥è®¿é—®ä»¤ç‰Œ"
+msgstr "é‡ç½®è¿è¡ŒçŠ¶å†µæ£€æŸ¥è®¿é—®ä»¤ç‰Œ"
msgid "Reset runners registration token"
msgstr "é‡ç½® Runner 注册令牌"
+msgid "Resolve all discussions in new issue"
+msgstr ""
+
+msgid "Resolve conflicts on source branch"
+msgstr "在æºåˆ†æ”¯ä¸Šè§£å†³å†²çª"
+
msgid "Resolve discussion"
msgstr "解决讨论"
-msgid "Response"
-msgstr "å“应"
+msgid "Resume"
+msgstr "æ¢å¤"
+
+msgid "Retry"
+msgstr "é‡è¯•"
+
+msgid "Retry this job"
+msgstr "é‡è¯•当å‰ä½œä¸š"
+
+msgid "Retry verification"
+msgstr "é‡è¯•验è¯"
msgid "Reveal value"
msgid_plural "Reveal values"
@@ -3574,39 +3829,42 @@ msgstr "还原此æäº¤"
msgid "Revert this merge request"
msgstr "还原此åˆå¹¶è¯·æ±‚"
-msgid "Review the process for configuring service providers in your identity provider — in this case, GitLab is the \"service provider\" or \"relying party\"."
-msgstr ""
+msgid "Review"
+msgstr "审阅"
msgid "Reviewing"
-msgstr "检查"
+msgstr "审阅中"
msgid "Reviewing (merge request !%{mergeRequestId})"
-msgstr ""
+msgstr "审核(åˆå¹¶è¯·æ±‚ !%{mergeRequestId})"
-msgid "Roadmap"
-msgstr "路线图"
+msgid "Rollback"
+msgstr ""
-msgid "Run CI/CD pipelines for external repositories"
-msgstr "使用外部仓库的CI/CDæµæ°´çº¿"
+msgid "Runner token"
+msgstr ""
msgid "Runners"
msgstr "Runner"
+msgid "Runners API"
+msgstr "Runners API"
+
+msgid "Runners can be placed on separate users, servers, and even on your local machine."
+msgstr "Runnerå¯ä»¥æ”¾åœ¨ä¸åŒçš„ç”¨æˆ·ã€æœåŠ¡å™¨ï¼Œç”šè‡³æœ¬åœ°æœºå™¨ä¸Šã€‚"
+
msgid "Running"
msgstr "è¿è¡Œä¸­"
-msgid "SAML Single Sign On"
-msgstr ""
+msgid "SSH Keys"
+msgstr "SSH 密钥"
-msgid "SAML Single Sign On Settings"
+msgid "SSL Verification"
msgstr ""
-msgid "SHA1 fingerprint of the SAML token signing certificate. Get this from your identity provider, where it can also be called \"Thumbprint\"."
+msgid "Save"
msgstr ""
-msgid "SSH Keys"
-msgstr "SSH 密钥"
-
msgid "Save changes"
msgstr "ä¿å­˜ä¿®æ”¹"
@@ -3628,15 +3886,30 @@ msgstr "日程"
msgid "Scheduling Pipelines"
msgstr "æµæ°´çº¿è®¡åˆ’"
-msgid "Scoped issue boards"
-msgstr "议题看æ¿èŒƒå›´"
+msgid "Scroll to bottom"
+msgstr "滚动到底部"
+
+msgid "Scroll to top"
+msgstr "滚动到顶部"
msgid "Search"
msgstr "æœç´¢"
+msgid "Search branches"
+msgstr "æœç´¢åˆ†æ”¯"
+
msgid "Search branches and tags"
msgstr "æœç´¢åˆ†æ”¯å’Œæ ‡ç­¾"
+msgid "Search files"
+msgstr "æœç´¢æ–‡ä»¶"
+
+msgid "Search for projects, issues, etc."
+msgstr "æœç´¢é¡¹ç›®ã€è®®é¢˜ç­‰ç­‰â€¦"
+
+msgid "Search merge requests"
+msgstr "æœç´¢åˆå¹¶è¯·æ±‚"
+
msgid "Search milestones"
msgstr "æœç´¢é‡Œç¨‹ç¢‘"
@@ -3652,15 +3925,15 @@ msgstr "é‡ç½®å¤±è´¥ä¿¡æ¯ç­‰å¾…æ—¶é—´(ç§’)"
msgid "Seconds to wait for a storage access attempt"
msgstr "等待存储访问å°è¯•æ—¶é—´(ç§’)"
-msgid "Secret variables"
-msgstr "加密å˜é‡"
-
-msgid "Security report"
-msgstr "安全报告"
+msgid "Select"
+msgstr "选择"
msgid "Select Archive Format"
msgstr "选择下载格å¼"
+msgid "Select a namespace to fork the project"
+msgstr "选择一个命åç©ºé—´æ¥æ´¾ç”Ÿé¡¹ç›®"
+
msgid "Select a timezone"
msgstr "选择时区"
@@ -3673,12 +3946,21 @@ msgstr "选择指派人"
msgid "Select branch/tag"
msgstr "选择分支/标签"
+msgid "Select project"
+msgstr "选择项目"
+
+msgid "Select project and zone to choose machine type"
+msgstr "按项目和地域选择实例类型"
+
+msgid "Select project to choose zone"
+msgstr "按项目选择地域"
+
+msgid "Select source branch"
+msgstr "选择æºåˆ†æ”¯"
+
msgid "Select target branch"
msgstr "选择目标分支"
-msgid "Selective synchronization"
-msgstr "é€‰æ‹©æ€§åŒæ­¥"
-
msgid "Send email"
msgstr "å‘é€ç”µå­é‚®ä»¶"
@@ -3694,9 +3976,6 @@ msgstr "æœåŠ¡å™¨ç‰ˆæœ¬"
msgid "Service Templates"
msgstr "æœåŠ¡æ¨¡æ¿"
-msgid "Service URL"
-msgstr "æœåŠ¡URL"
-
msgid "Session expiration, projects limit and attachment size."
msgstr "ä¼šè¯æœ‰æ•ˆæœŸï¼Œé¡¹ç›®é™åˆ¶åŠé™„件大å°ã€‚"
@@ -3707,10 +3986,10 @@ msgid "Set default and restrict visibility levels. Configure import sources and
msgstr "设定缺çœåŠå—é™å¯è§æ€§çº§åˆ«ã€‚é…ç½®å¯¼å…¥æ¥æºåŠgit访问å议。"
msgid "Set max session time for web terminal."
-msgstr ""
+msgstr "为Webç»ˆç«¯è®¾ç½®æœ€é•¿ä¼šè¯æ—¶é—´ã€‚"
msgid "Set notification email for abuse reports."
-msgstr ""
+msgstr "为滥用报告设置通知电å­é‚®ä»¶ã€‚"
msgid "Set requirements for a user to sign-in. Enable mandatory two-factor authentication."
msgstr "设定用户登录的æ¡ä»¶ã€‚å¯ç”¨å¼ºåˆ¶åŒé‡è®¤è¯ã€‚"
@@ -3721,9 +4000,6 @@ msgstr "é…ç½® CI/CD"
msgid "Set up Koding"
msgstr "设置 Koding"
-msgid "Set up assertions/attributes/claims (email, first_name, last_name) and NameID according to %{docsLinkStart}the documentation %{icon}%{docsLinkEnd}"
-msgstr ""
-
msgid "SetPasswordToCloneLink|set a password"
msgstr "设置密ç "
@@ -3731,22 +4007,25 @@ msgid "Settings"
msgstr "设置"
msgid "Setup a specific Runner automatically"
-msgstr "自动创建独享Runner"
+msgstr "自动创建专用Runner"
-msgid "Share the <strong>%{sso_label}</strong> with members so they can sign in to your group through your identity provider"
-msgstr ""
+msgid "Share"
+msgstr "分享"
-msgid "SharedRunnersMinutesSettings|By resetting the pipeline minutes for this namespace, the currently used minutes will be set to zero."
-msgstr "通过é‡ç½®æ­¤å‘½åç©ºé—´çš„æµæ°´çº¿åˆ†é’Ÿæ•°ï¼Œå½“å‰ä½¿ç”¨çš„分钟数将被归零。"
+msgid "Shared Runners"
+msgstr "共享Runner"
-msgid "SharedRunnersMinutesSettings|Reset pipeline minutes"
-msgstr "é‡ç½®æµæ°´çº¿åˆ†é’Ÿæ•°"
+msgid "Show command"
+msgstr "显示相关命令"
-msgid "SharedRunnersMinutesSettings|Reset used pipeline minutes"
-msgstr "é‡ç½®å·²ç”¨æµæ°´çº¿åˆ†é’Ÿæ•°"
+msgid "Show complete raw log"
+msgstr "显示完整的原始日志"
-msgid "Show command"
-msgstr "显示命令"
+msgid "Show latest version"
+msgstr ""
+
+msgid "Show latest version of the diff"
+msgstr ""
msgid "Show parent pages"
msgstr "查看上级页é¢"
@@ -3754,21 +4033,18 @@ msgstr "查看上级页é¢"
msgid "Show parent subgroups"
msgstr "查看上级å­ç¾¤ç»„"
+msgid "Show whitespace changes"
+msgstr ""
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "显示 %d 个事件"
-msgid "Sidebar|Change weight"
-msgstr "编辑æƒé‡"
-
-msgid "Sidebar|No"
-msgstr "æ— "
-
-msgid "Sidebar|None"
-msgstr "æ— "
+msgid "Side-by-side"
+msgstr ""
-msgid "Sidebar|Weight"
-msgstr "æƒé‡"
+msgid "Sign out"
+msgstr "退出"
msgid "Sign-in restrictions"
msgstr "登录é™åˆ¶"
@@ -3779,7 +4055,7 @@ msgstr "注册é™åˆ¶"
msgid "Size and domain settings for static websites"
msgstr "陿€ç½‘站的大å°å’ŒåŸŸè®¾ç½®"
-msgid "Slack application"
+msgid "Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job"
msgstr ""
msgid "Snippets"
@@ -3791,14 +4067,14 @@ msgstr "出错了,抱歉。"
msgid "Something went wrong on our end."
msgstr "出错了,抱歉。"
+msgid "Something went wrong on our end. Please try again!"
+msgstr ""
+
msgid "Something went wrong when toggling the button"
msgstr "点击按钮时出错"
-msgid "Something went wrong while fetching Dependency Scanning."
-msgstr "读å–ä¾èµ–扫æç»“果时å‘生错误。"
-
-msgid "Something went wrong while fetching SAST."
-msgstr "读å–SAST 时出错。"
+msgid "Something went wrong while closing the %{issuable}. Please try again later"
+msgstr ""
msgid "Something went wrong while fetching the projects."
msgstr "拉å–项目时å‘生错误。"
@@ -3806,6 +4082,12 @@ msgstr "拉å–项目时å‘生错误。"
msgid "Something went wrong while fetching the registry list."
msgstr "æ‹‰å–æ³¨å†Œè¡¨åˆ—表时å‘生错误。"
+msgid "Something went wrong while reopening the %{issuable}. Please try again later"
+msgstr ""
+
+msgid "Something went wrong while resolving this discussion. Please try again."
+msgstr ""
+
msgid "Something went wrong. Please try again."
msgstr "出现错误。请é‡è¯•。"
@@ -3851,9 +4133,6 @@ msgstr "最近更新"
msgid "SortOptions|Least popular"
msgstr "最ä¸å—欢迎"
-msgid "SortOptions|Less weight"
-msgstr "最低æƒé‡"
-
msgid "SortOptions|Milestone"
msgstr "里程碑"
@@ -3863,9 +4142,6 @@ msgstr "里程碑截止日期"
msgid "SortOptions|Milestone due soon"
msgstr "å³å°†æˆªæ­¢çš„里程碑"
-msgid "SortOptions|More weight"
-msgstr "最高æƒé‡"
-
msgid "SortOptions|Most popular"
msgstr "æœ€å—æ¬¢è¿Ž"
@@ -3905,9 +4181,6 @@ msgstr "ç¨åŽå¼€å§‹"
msgid "SortOptions|Start soon"
msgstr "现在开始"
-msgid "SortOptions|Weight"
-msgstr "æƒé‡"
-
msgid "Source"
msgstr "æº"
@@ -3924,11 +4197,38 @@ msgid "Spam Logs"
msgstr "åžƒåœ¾ä¿¡æ¯æ—¥å¿—"
msgid "Spam and Anti-bot Protection"
-msgstr ""
+msgstr "垃圾邮件åŠé˜²æœºå™¨äººä¿æŠ¤"
+
+msgid "Specific Runners"
+msgstr "专用Runner"
msgid "Specify the following URL during the Runner setup:"
msgstr "在 Runner 设置时指定以下 URL:"
+msgid "Squash commits"
+msgstr "åˆå¹¶æäº¤"
+
+msgid "Stage"
+msgstr "阶段"
+
+msgid "Stage & Commit"
+msgstr ""
+
+msgid "Stage all changes"
+msgstr "暂存全部更改"
+
+msgid "Stage changes"
+msgstr "暂存更改"
+
+msgid "Staged"
+msgstr "已暂存"
+
+msgid "Staged %{type}"
+msgstr "已暂存的 %{type}"
+
+msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
+msgstr "把一个标记加上星标,å¯å°†å…¶å˜ä¸ºä¼˜å…ˆæ ‡è®°ã€‚通过拖放æ¥è°ƒæ•´ä¼˜å…ˆæ ‡è®°çš„顺åºï¼Œå¯ä»¥æ”¹å˜ä»–们的相对优先级。"
+
msgid "StarProject|Star"
msgstr "星标"
@@ -3950,12 +4250,15 @@ msgstr "å¯åЍ Runner!"
msgid "Started"
msgstr "å·²å¯åЍ"
-msgid "State your message to activate"
-msgstr ""
+msgid "Starts at (UTC)"
+msgstr "开始于(UTC)"
msgid "Status"
msgstr "状æ€"
+msgid "Stop this environment"
+msgstr "åœæ­¢å½“å‰çŽ¯å¢ƒ"
+
msgid "Stopped"
msgstr "å·²åœæ­¢"
@@ -3965,18 +4268,21 @@ msgstr "存储"
msgid "Subgroups"
msgstr "å­ç¾¤ç»„"
+msgid "Subscribe"
+msgstr "订阅"
+
+msgid "Subscribe at group level"
+msgstr "在群组级别订阅"
+
+msgid "Subscribe at project level"
+msgstr "在项目级别订阅"
+
msgid "Switch branch/tag"
msgstr "切æ¢åˆ†æ”¯/标签"
-msgid "System"
-msgstr "系统"
-
msgid "System Hooks"
msgstr "系统钩å­"
-msgid "System header and footer:"
-msgstr ""
-
msgid "Tag (%{tag_count})"
msgid_plural "Tags (%{tag_count})"
msgstr[0] "标签(%{tag_count})"
@@ -3984,6 +4290,9 @@ msgstr[0] "标签(%{tag_count})"
msgid "Tags"
msgstr "标签"
+msgid "Tags:"
+msgstr "标签:"
+
msgid "TagsPage|Browse commits"
msgstr "æµè§ˆæäº¤"
@@ -4047,8 +4356,8 @@ msgstr "此标签没有å‘行说明。"
msgid "TagsPage|Use git tag command to add a new one:"
msgstr "使用git tag命令添加一个:"
-msgid "TagsPage|Write your release notes or drag files here..."
-msgstr "撰写å‘行说明或拖放文件到这里..."
+msgid "TagsPage|Write your release notes or drag files here…"
+msgstr "编写å‘行说明(Release Notes)或将文件拖动到此处..."
msgid "TagsPage|protected"
msgstr "å·²ä¿æŠ¤"
@@ -4062,11 +4371,14 @@ msgstr "目标分支"
msgid "Team"
msgstr "团队"
-msgid "Thanks! Don't show me this again"
-msgstr "谢谢 ! 请ä¸è¦å†æ˜¾ç¤º"
+msgid "Terms of Service Agreement and Privacy Policy"
+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 "Terms of Service and Privacy Policy"
+msgstr "æœåŠ¡æ¡æ¬¾å’Œéšç§æ”¿ç­–"
+
+msgid "Test coverage parsing"
+msgstr ""
msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project"
msgstr "议题跟踪用于管ç†éœ€è¦æ”¹è¿›æˆ–者解决的问题"
@@ -4074,18 +4386,12 @@ msgstr "议题跟踪用于管ç†éœ€è¦æ”¹è¿›æˆ–者解决的问题"
msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
msgstr "议题跟踪用于管ç†éœ€è¦æ”¹è¿›æˆ–者解决的问题。请注册或登录åŽä¸ºå½“å‰é¡¹ç›®åˆ›å»ºè®®é¢˜ã€‚"
-msgid "The X509 Certificate to use when mutual TLS is required to communicate with the external authorization service. If left blank, the server certificate is still validated when accessing over HTTPS."
-msgstr "在需è¦ç›¸äº’ TLS ä¸Žå¤–éƒ¨æŽˆæƒæœåŠ¡é€šä¿¡æ—¶ä½¿ç”¨çš„ X509 è¯ä¹¦ã€‚如果ä¿ç•™ä¸ºç©º, 则在访问 HTTPS æ—¶ä»ç„¶éªŒè¯æœåС噍è¯ä¹¦ã€‚"
-
msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request."
msgstr "ç¼–ç é˜¶æ®µæ¦‚述了从第一次æäº¤åˆ°åˆ›å»ºåˆå¹¶è¯·æ±‚的时间。创建第一个åˆå¹¶è¯·æ±‚åŽï¼Œæ•°æ®å°†è‡ªåŠ¨æ·»åŠ åˆ°æ­¤å¤„ã€‚"
msgid "The collection of events added to the data gathered for that stage."
msgstr "与该阶段相关的事件集åˆã€‚"
-msgid "The connection will time out after %{timeout}. For repositories that take longer, use a clone/push combination."
-msgstr "该连接将在 %{timeout}åŽè¶…时。对于需è¦é•¿äºŽè¯¥æ—¶é—´æ‰èƒ½å¯¼å…¥çš„仓库,请使用克隆/推é€ç»„åˆã€‚"
-
msgid "The fork relationship has been removed."
msgstr "派生关系已被删除。"
@@ -4104,8 +4410,8 @@ 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 passphrase required to decrypt the private key. This is optional and the value is encrypted at rest."
-msgstr "解密ç§é’¥æ‰€éœ€çš„密ç çŸ­è¯­ã€‚该项为å¯é€‰é¡¹, 并且内容被加密存储。"
+msgid "The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>"
+msgstr ""
msgid "The phase of the development lifecycle."
msgstr "项目生命周期中的å„个阶段。"
@@ -4113,9 +4419,6 @@ msgstr "项目生命周期中的å„个阶段。"
msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit."
msgstr "计划阶段概述了从议题添加到日程到推é€é¦–次æäº¤çš„æ—¶é—´ã€‚å½“é¦–æ¬¡æŽ¨é€æäº¤åŽï¼Œæ•°æ®å°†è‡ªåŠ¨æ·»åŠ åˆ°æ­¤å¤„ã€‚"
-msgid "The private key to use when a client certificate is provided. This value is encrypted at rest."
-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 "生产阶段概述了从创建一个议题到将代ç éƒ¨ç½²åˆ°ç”Ÿäº§çŽ¯å¢ƒçš„æ€»æ—¶é—´ã€‚å½“å®Œæˆæƒ³æ³•到部署生产的循环,数æ®å°†è‡ªåŠ¨æ·»åŠ åˆ°æ­¤å¤„ã€‚"
@@ -4135,10 +4438,10 @@ msgid "The repository must be accessible over <code>http://</code>, <code>https:
msgstr "该仓库必须å¯é€šè¿‡<code>http://</code>, <code>https://</code> 或 <code>git://</code>进行访问。"
msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request."
-msgstr "评审阶段概述了从创建åˆå¹¶è¯·æ±‚到被åˆå¹¶çš„æ—¶é—´ã€‚当创建第一个åˆå¹¶è¯·æ±‚åŽï¼Œæ•°æ®å°†è‡ªåŠ¨æ·»åŠ åˆ°æ­¤å¤„ã€‚"
+msgstr "审阅阶段概述了从创建åˆå¹¶è¯·æ±‚到被åˆå¹¶çš„æ—¶é—´ã€‚当创建第一个åˆå¹¶è¯·æ±‚åŽï¼Œæ•°æ®å°†è‡ªåŠ¨æ·»åŠ åˆ°æ­¤å¤„ã€‚"
-msgid "The roadmap shows the progress of your epics along a timeline"
-msgstr "路线图显示了å²è¯—æ•…äº‹æ²¿ç€æ—¶é—´çº¿çš„进展情况"
+msgid "The secure token used by the Runner to checkout the project"
+msgstr ""
msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time."
msgstr "预å‘布阶段概述了从åˆå¹¶è¯·æ±‚被åˆå¹¶åˆ°éƒ¨ç½²è‡³ç”Ÿäº§çŽ¯å¢ƒçš„æ€»æ—¶é—´ã€‚é¦–æ¬¡éƒ¨ç½²åˆ°ç”Ÿäº§çŽ¯å¢ƒåŽï¼Œæ•°æ®å°†è‡ªåŠ¨æ·»åŠ åˆ°æ­¤å¤„ã€‚"
@@ -4164,14 +4467,20 @@ msgstr "䏭使•°æ˜¯ä¸€ä¸ªæ•°åˆ—中最中间的值。例如在 3ã€5ã€9 之间ï
msgid "There are no issues to show"
msgstr "当剿— è®®é¢˜"
+msgid "There are no labels yet"
+msgstr ""
+
msgid "There are no merge requests to show"
msgstr "当剿— åˆå¹¶è¯·æ±‚"
msgid "There are problems accessing Git storage: "
msgstr "访问 Git 存储时出现问题:"
-msgid "There was an error loading results"
-msgstr "加载结果时出错"
+msgid "There was an error loading jobs"
+msgstr "加载作业时å‘生错误"
+
+msgid "There was an error loading latest pipeline"
+msgstr "加载最åŽä¸€æ¡æµæ°´çº¿æ—¶å‘生错误"
msgid "There was an error loading users activity calendar."
msgstr "加载用户活动日历时出错。"
@@ -4191,12 +4500,21 @@ msgstr "订阅此标记时出错。"
msgid "There was an error when unsubscribing from this label."
msgstr "å–æ¶ˆè®¢é˜…此标记时出错。"
-msgid "This board\\'s scope is reduced"
-msgstr "这个看æ¿çš„范围缩å°äº†"
+msgid "They can be managed using the %{link}."
+msgstr "他们å¯ä»¥é€šè¿‡ %{link} 进行管ç†ã€‚"
+
+msgid "This GitLab instance does not provide any shared Runners yet. Instance administrators can register shared Runners in the admin area."
+msgstr "æ­¤GitLab实例尚未æä¾›ä»»ä½•共享Runner。管ç†å‘˜å¯ä»¥åœ¨ç®¡ç†åŒºåŸŸä¸­æ³¨å†Œå…±äº«Runner。"
+
+msgid "This diff is collapsed."
+msgstr ""
msgid "This directory"
msgstr "当å‰ç›®å½•"
+msgid "This group does not provide any group Runners yet."
+msgstr "该群组未æä¾›ä»»ä½•群组Runner。"
+
msgid "This is a confidential issue."
msgstr "这是一个机密议题。"
@@ -4218,6 +4536,15 @@ msgstr "当å‰ä½œä¸šéœ€è¦ç”¨æˆ·è§¦å‘其过程。此类作业通常用于将代
msgid "This job depends on upstream jobs that need to succeed in order for this job to be triggered"
msgstr "当å‰ä½œä¸šéœ€åœ¨ä¸Šçº§ä½œä¸šæˆåŠŸåŽæ‰å¯è¢«å¯åŠ¨ã€‚"
+msgid "This job does not have a trace."
+msgstr "此作业没有输出日志。"
+
+msgid "This job has been canceled"
+msgstr "æ­¤ä½œä¸šå·²å–æ¶ˆ"
+
+msgid "This job has been skipped"
+msgstr "此作业已被跳过"
+
msgid "This job has not been triggered yet"
msgstr "作业还未被触å‘"
@@ -4236,20 +4563,29 @@ msgstr "在创建一个空的仓库或导入现有仓库之å‰ï¼Œå°†æ— æ³•推é€
msgid "This merge request is locked."
msgstr "æ­¤åˆå¹¶è¯·æ±‚å·²é”定。"
+msgid "This option is disabled while you still have unstaged changes"
+msgstr "有未暂存更改时此选项会被ç¦ç”¨ã€‚"
+
msgid "This page is unavailable because you are not allowed to read information across multiple projects."
msgstr "此页é¢ä¸å¯ç”¨ï¼Œæ‚¨æ— æƒè·¨é¡¹ç›®é˜…读相关信æ¯ã€‚"
+msgid "This page will be removed in a future release."
+msgstr "此页é¢å°†åœ¨å°†æ¥çš„版本中删除。"
+
msgid "This project"
msgstr "当å‰é¡¹ç›®"
+msgid "This project does not belong to a group and can therefore not make use of group Runners."
+msgstr "该项目ä¸å±žäºŽä»»ä½•群组,因此ä¸èƒ½ä½¿ç”¨ç¾¤ç»„Runner。"
+
msgid "This repository"
msgstr "当å‰ä»“库"
-msgid "This will delete the custom metric, Are you sure?"
-msgstr "æ­¤æ“作将删除自定义指标,确定继续å—?"
+msgid "This source diff could not be displayed because it is too large."
+msgstr ""
-msgid "Those emails automatically become issues (with the comments becoming the email conversation) listed here."
-msgstr "这些电å­é‚®ä»¶è‡ªåŠ¨ç”Ÿæˆä¸ºé—®é¢˜(评论生æˆä¸ºç”µå­é‚®ä»¶å¯¹è¯)在这里列出。"
+msgid "This user has no identities"
+msgstr ""
msgid "Time before an issue gets scheduled"
msgstr "议题被列入日程表的时间"
@@ -4260,11 +4596,11 @@ msgstr "开始进行编ç å‰çš„æ—¶é—´"
msgid "Time between merge request creation and merge/close"
msgstr "从创建åˆå¹¶è¯·æ±‚到被åˆå¹¶æˆ–关闭的时间"
-msgid "Time between updates and capacity settings."
-msgstr ""
+msgid "Time remaining"
+msgstr "剩余时间:"
-msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
-msgstr "GitLab等待外部æœåŠ¡çš„å“应时间(秒)。当æœåŠ¡æ²¡æœ‰åŠæ—¶å“应时,访问将被拒ç»ã€‚"
+msgid "Time spent"
+msgstr "花费的时间"
msgid "Time tracking"
msgstr "工时统计"
@@ -4287,6 +4623,9 @@ msgstr " %s 天å‰"
msgid "Timeago|%s days remaining"
msgstr "剩余 %s 天"
+msgid "Timeago|%s hours ago"
+msgstr "%s å°æ—¶å‰"
+
msgid "Timeago|%s hours remaining"
msgstr "剩余 %s å°æ—¶"
@@ -4302,6 +4641,9 @@ msgstr " %s 个月å‰"
msgid "Timeago|%s months remaining"
msgstr "剩余 %s 月"
+msgid "Timeago|%s seconds ago"
+msgstr "%s ç§’å‰"
+
msgid "Timeago|%s seconds remaining"
msgstr "剩余 %s 秒"
@@ -4317,48 +4659,45 @@ msgstr " %s å¹´å‰"
msgid "Timeago|%s years remaining"
msgstr "剩余 %s 年"
+msgid "Timeago|1 day ago"
+msgstr "1天å‰"
+
msgid "Timeago|1 day remaining"
msgstr "剩余 1 天"
+msgid "Timeago|1 hour ago"
+msgstr "1å°æ—¶å‰"
+
msgid "Timeago|1 hour remaining"
msgstr "剩余 1 å°æ—¶"
+msgid "Timeago|1 minute ago"
+msgstr "1分钟å‰"
+
msgid "Timeago|1 minute remaining"
msgstr "剩余 1 分钟"
+msgid "Timeago|1 month ago"
+msgstr "1个月å‰"
+
msgid "Timeago|1 month remaining"
msgstr "剩余 1 个月"
+msgid "Timeago|1 week ago"
+msgstr "1星期å‰"
+
msgid "Timeago|1 week remaining"
msgstr "剩余 1 星期"
+msgid "Timeago|1 year ago"
+msgstr "1å¹´å‰"
+
msgid "Timeago|1 year remaining"
msgstr "剩余 1 年"
msgid "Timeago|Past due"
msgstr "逾期"
-msgid "Timeago|a day ago"
-msgstr " 1 天å‰"
-
-msgid "Timeago|a month ago"
-msgstr " 1 个月å‰"
-
-msgid "Timeago|a week ago"
-msgstr " 1 星期å‰"
-
-msgid "Timeago|a year ago"
-msgstr " 1 å¹´å‰"
-
-msgid "Timeago|about %s hours ago"
-msgstr "约 %s å°æ—¶å‰"
-
-msgid "Timeago|about a minute ago"
-msgstr "约 1 分钟å‰"
-
-msgid "Timeago|about an hour ago"
-msgstr "约 1 å°æ—¶å‰"
-
msgid "Timeago|in %s days"
msgstr " %s 天åŽ"
@@ -4398,11 +4737,14 @@ msgstr " 1 星期åŽ"
msgid "Timeago|in 1 year"
msgstr " 1 å¹´åŽ"
-msgid "Timeago|in a while"
+msgid "Timeago|just now"
msgstr "刚刚"
-msgid "Timeago|less than a minute ago"
-msgstr "ä¸åˆ° 1 分钟å‰"
+msgid "Timeago|right now"
+msgstr "ç«‹å³"
+
+msgid "Timeout"
+msgstr ""
msgid "Time|hr"
msgid_plural "Time|hrs"
@@ -4418,20 +4760,11 @@ msgstr "ç§’"
msgid "Tip:"
msgstr "æç¤ºï¼š"
-msgid "Title"
-msgstr "标题"
-
msgid "To GitLab"
msgstr "到GitLab"
-msgid "To connect GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to connect."
-msgstr "å¯ä»¥ä½¿ç”¨ %{personal_access_token_link}连接GitHub仓库。当创建个人访问令牌时,需è¦é€‰æ‹© <code>repo</code> 范围,以显示å¯ä¾›è¿žæŽ¥çš„å…¬å…±å’Œç§æœ‰çš„仓库列表。"
-
-msgid "To connect GitHub repositories, you first need to authorize GitLab to access the list of your GitHub repositories:"
-msgstr "è¦è¿žæŽ¥GitHubä»“åº“ï¼Œé¦–å…ˆéœ€è¦æŽˆæƒGitLab访问列表中的GitHub仓库:"
-
-msgid "To connect an SVN repository, check out %{svn_link}."
-msgstr "è¦è¿žæŽ¥SVN仓库,请查看 %{svn_link}。"
+msgid "To add an SSH key you need to %{generate_link_start}generate one%{link_end} or use an %{existing_link_start}existing key%{link_end}."
+msgstr ""
msgid "To import GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
msgstr "å¯ä»¥ä½¿ç”¨ %{personal_access_token_link}导入GitHub仓库。当创建个人访问令牌时,需è¦é€‰æ‹© <code>repo</code> 范围,以显示å¯å¯¼å…¥çš„å…¬å…±å’Œç§æœ‰çš„仓库列表。"
@@ -4442,21 +4775,21 @@ msgstr "è¦å¯¼å…¥GitHubä»“åº“ï¼Œé¦–å…ˆéœ€è¦æŽˆæƒGitLab访问列表中的GitHub
msgid "To import an SVN repository, check out %{svn_link}."
msgstr "è¦å¯¼å…¥SVN仓库,请查看 %{svn_link}。"
-msgid "To only use CI/CD features for an external repository, choose <strong>CI/CD for external repo</strong>."
-msgstr "è¦ä»…为外部仓库使用CI / CD功能时,请选择</strong>使用外部仓库è¿è¡ŒCI/CD<strong>。"
-
-msgid "To set up SAML authentication for your group through an identity provider like Azure, Okta, Onelogin, Ping Identity, or your custom SAML 2.0 provider:"
-msgstr ""
+msgid "To start serving your jobs you can add Runners to your group"
+msgstr "è¦å¼€å§‹æ‰§è¡Œä»»åŠ¡ï¼Œè¯·æŠŠRunner加到群组中"
msgid "To validate your GitLab CI configurations, go to 'CI/CD → Pipelines' inside your project, and click on the 'CI Lint' button."
msgstr "如需验è¯GitLab CI设置,请访问当å‰é¡¹ç›®çš„'CI/CD → æµæ°´çº¿',然åŽç‚¹å‡»'CI Lint'按钮。"
-msgid "To view the roadmap, add a planned start or finish date to one of your epics in this group or its subgroups. Only epics in the past 3 months and the next 3 months are shown."
-msgstr "å¦‚éœ€æŸ¥çœ‹è·¯çº¿å›¾ï¼Œè¯·å°†è®¡åˆ’çš„å¼€å§‹æˆ–ç»“æŸæ—¥æœŸæ·»åŠ åˆ°å½“å‰ç¾¤ç»„或其å­ç»„中的æŸä¸ªå²è¯—æ•…äº‹ã€‚åªæ˜¾ç¤ºè¿‡åŽ»3个月和接下æ¥3个月的å²è¯—故事。"
-
msgid "Todo"
msgstr "待办事项"
+msgid "Toggle Sidebar"
+msgstr "切æ¢ä¾§è¾¹æ "
+
+msgid "Toggle discussion"
+msgstr "开关讨论"
+
msgid "Toggle sidebar"
msgstr "切æ¢è¾¹æ "
@@ -4466,6 +4799,9 @@ msgstr "切æ¢çжæ€ï¼šå…³é—­"
msgid "ToggleButton|Toggle Status: ON"
msgstr "切æ¢çжæ€ï¼šå¼€å¯"
+msgid "Too many changes to show."
+msgstr ""
+
msgid "Total Time"
msgstr "总时间"
@@ -4475,23 +4811,20 @@ msgstr "所有æäº¤å’Œåˆå¹¶çš„æ€»æµ‹è¯•æ—¶é—´"
msgid "Total: %{total}"
msgstr "总计:%{total}"
-msgid "Track activity with Contribution Analytics."
-msgstr "跟踪活动与贡献的分æžã€‚"
-
-msgid "Track groups of issues that share a theme, across projects and milestones"
-msgstr "在ä¸åŒé¡¹ç›®å’Œé‡Œç¨‹ç¢‘中跟踪具有åŒä¸€ä¸»é¢˜çš„议题组"
-
msgid "Track time with quick actions"
msgstr "ä½¿ç”¨å¿«æ·æ“作æ¥ç»Ÿè®¡å·¥æ—¶"
msgid "Trigger this manual action"
msgstr "è§¦å‘æ­¤æ‰‹åЍæ“作"
-msgid "Turn on Service Desk"
-msgstr "打开æœåŠ¡å°"
+msgid "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."
+msgstr ""
+
+msgid "Try again"
+msgstr "请é‡è¯•"
-msgid "Unknown"
-msgstr "未知的"
+msgid "Unable to load the diff. %{button_try_again}"
+msgstr "无法加载差异。 %{button_try_again}"
msgid "Unlock"
msgstr "è§£é”"
@@ -4502,26 +4835,45 @@ msgstr "已解é”"
msgid "Unresolve discussion"
msgstr "待解决的讨论"
+msgid "Unstage all changes"
+msgstr "å–æ¶ˆå…¨éƒ¨æš‚存更改"
+
+msgid "Unstage changes"
+msgstr "å–æ¶ˆæš‚存更改"
+
+msgid "Unstaged"
+msgstr "未暂存"
+
+msgid "Unstaged %{type}"
+msgstr "未暂存的 %{type}"
+
+msgid "Unstaged and staged %{type}"
+msgstr "已暂存和未暂存的 %{type}"
+
msgid "Unstar"
msgstr "å–æ¶ˆæ˜Ÿæ ‡"
-msgid "Up to date"
-msgstr "已是最新"
+msgid "Unsubscribe"
+msgstr "å–æ¶ˆè®¢é˜…"
-msgid "Upgrade your plan to activate Advanced Global Search."
-msgstr "å‡çº§æ‚¨çš„æ–¹æ¡ˆä»¥å¯ç”¨é«˜çº§å…¨å±€æœç´¢ã€‚"
+msgid "Unsubscribe at group level"
+msgstr "åœ¨ç¾¤ç»„çº§åˆ«å–æ¶ˆè®¢é˜…"
-msgid "Upgrade your plan to activate Contribution Analytics."
-msgstr "å‡çº§æ‚¨çš„æ–¹æ¡ˆä»¥å¯ç”¨è´¡çŒ®åˆ†æžã€‚"
+msgid "Unsubscribe at project level"
+msgstr "åœ¨é¡¹ç›®çº§åˆ«å–æ¶ˆè®¢é˜…"
-msgid "Upgrade your plan to activate Group Webhooks."
-msgstr "å‡çº§æ‚¨çš„æ–¹æ¡ˆä»¥æ¿€æ´» Webhooks 。"
+msgid "Unverified"
+msgstr "未验è¯"
+
+msgid "Up to date"
+msgstr "已是最新"
-msgid "Upgrade your plan to activate Issue weight."
-msgstr "å‡çº§æ‚¨çš„æ–¹æ¡ˆä»¥æ¿€æ´»è®®é¢˜æƒé‡ã€‚"
+msgid "Update %{files}"
+msgid_plural "Update %{files} files"
+msgstr[0] ""
-msgid "Upgrade your plan to improve Issue boards."
-msgstr "å‡çº§æ‚¨çš„æ–¹æ¡ˆä»¥ä½¿ç”¨è®®é¢˜çœ‹æ¿ã€‚"
+msgid "Update your group name, description, avatar, and other general settings."
+msgstr "更新您的群组åç§°ã€è¯´æ˜Žã€å¤´åƒä»¥åŠå…¶å®ƒé€šç”¨è®¾ç½®ã€‚"
msgid "Upload New File"
msgstr "上传新文件"
@@ -4539,10 +4891,10 @@ msgid "Upvotes"
msgstr "é¡¶"
msgid "Usage statistics"
-msgstr ""
+msgstr "使用情况统计"
-msgid "Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab"
-msgstr "使用æœåŠ¡å°åœ¨GitLab内部通过电å­é‚®ä»¶ä¸Žç”¨æˆ·è”系(例如æä¾›å®¢æˆ·æ”¯æŒï¼‰"
+msgid "Use group milestones to manage issues from multiple projects in the same milestone."
+msgstr "使用群组里程碑å¯ä»¥ç»Ÿä¸€ç®¡ç†å¤šä¸ªé¡¹ç›®ä¸­åŒä¸€é‡Œç¨‹ç¢‘的议题。"
msgid "Use the following registration token during setup:"
msgstr "在安装过程中使用以下注册令牌:"
@@ -4550,29 +4902,29 @@ msgstr "在安装过程中使用以下注册令牌:"
msgid "Use your global notification setting"
msgstr "使用全局通知设置"
-msgid "Used by members to sign in to your group in GitLab"
-msgstr ""
-
msgid "User and IP Rate Limits"
+msgstr "用户和IP频率é™åˆ¶"
+
+msgid "Users"
msgstr ""
+msgid "Variables"
+msgstr "å˜é‡"
+
msgid "Variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. You can use variables for passwords, secret keys, or whatever you want."
msgstr "å˜é‡é€šè¿‡runner作用于环境中。å¯å°†å˜é‡é™åˆ¶ä¸ºä»…å—ä¿æŠ¤çš„åˆ†æ”¯æˆ–æ ‡ç­¾å¯ä»¥è®¿é—®ã€‚å¯ä»¥ä½¿ç”¨å˜é‡æ¥ä¿å­˜å¯†ç ã€å¯†é’¥æˆ–任何其他内容。"
msgid "Various container registry settings."
-msgstr ""
+msgstr "容器注册表相关设置。"
msgid "Various email settings."
-msgstr ""
+msgstr "电å­é‚®ä»¶ç›¸å…³è®¾ç½®"
msgid "Various settings that affect GitLab performance."
-msgstr ""
-
-msgid "View and edit lines"
-msgstr "查看和编辑行"
+msgstr "å½±å“GitLab性能相关设置。"
-msgid "View epics list"
-msgstr "查看å²è¯—故事列表"
+msgid "Verified"
+msgstr "已验è¯"
msgid "View file @ "
msgstr "æµè§ˆæ–‡ä»¶ @ "
@@ -4580,9 +4932,15 @@ msgstr "æµè§ˆæ–‡ä»¶ @ "
msgid "View group labels"
msgstr "查看群组标记"
+msgid "View jobs"
+msgstr "查看作业"
+
msgid "View labels"
msgstr "查看标记"
+msgid "View log"
+msgstr "查看日志"
+
msgid "View open merge request"
msgstr "查看待处ç†çš„åˆå¹¶è¯·æ±‚"
@@ -4610,9 +4968,6 @@ msgstr "未知"
msgid "Want to see the data? Please ask an administrator for access."
msgstr "æƒé™ä¸è¶³ã€‚如需查看相关数æ®ï¼Œè¯·å‘管ç†å‘˜ç”³è¯·æƒé™ã€‚"
-msgid "We could not verify that one of your projects on GCP has billing enabled. Please try again."
-msgstr "æ— æ³•éªŒè¯æ‚¨åœ¨ GCP 上的æŸä¸ªé¡¹ç›®æ˜¯å¦å¯ç”¨äº†è®¡è´¹ã€‚请é‡è¯•。"
-
msgid "We don't have enough data to show this stage."
msgstr "该阶段的数æ®ä¸è¶³ï¼Œæ— æ³•显示。"
@@ -4623,16 +4978,13 @@ msgid "Web IDE"
msgstr "Web IDE"
msgid "Web terminal"
-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 。"
+msgstr "Web终端"
-msgid "Weight"
-msgstr "æƒé‡"
+msgid "When a runner is locked, it cannot be assigned to other projects"
+msgstr "当Runner被é”定时,ä¸èƒ½å°†å…¶åˆ†é…给其他项目"
-msgid "When leaving the URL blank, classification labels can still be specified whitout disabling cross project features or performing external authorization checks."
-msgstr "å°†URLä¿ç•™ä¸ºç©ºç™½æ—¶ï¼Œä»å¯æŒ‡å®šåˆ†ç±»æ ‡ç­¾ï¼Œè€Œæ— éœ€ç¦ç”¨è·¨é¡¹ç›®åŠŸèƒ½æˆ–æ‰§è¡Œå¤–éƒ¨æŽˆæƒæ£€æŸ¥ã€‚"
+msgid "When enabled, users cannot use GitLab until the terms have been accepted."
+msgstr "该项å¯ç”¨åŽï¼Œç”¨æˆ·åœ¨æŽ¥å—æ¡æ¬¾è¢«å‰å°†ä¸èƒ½ä½¿ç”¨GitLab。"
msgid "Wiki"
msgstr "Wiki"
@@ -4658,8 +5010,32 @@ msgstr "æç¤ºï¼šå¯ä»¥é€šè¿‡å°†è·¯å¾„添加到标题开头æ¥ç§»åŠ¨æ­¤é¡µé¢ã€‚
msgid "WikiEdit|There is already a page with the same title in that path."
msgstr "åœ¨è¯¥è·¯å¾„ä¸­å·²ç»æœ‰ä¸€ä¸ªå…·æœ‰ç›¸åŒæ ‡é¢˜çš„页é¢ã€‚"
-msgid "WikiEmptyPageError|You are not allowed to create wiki pages"
-msgstr "您ä¸èƒ½åˆ›å»º wiki 页é¢"
+msgid "WikiEmptyIssueMessage|Suggest wiki improvement"
+msgstr "Wiki 改进建议"
+
+msgid "WikiEmptyIssueMessage|You must be a project member in order to add wiki pages. If you have suggestions for how to improve the wiki for this project, consider opening an issue in the %{issues_link}."
+msgstr "åªæœ‰è¯¥é¡¹ç›®çš„æˆå‘˜æ‰å¯ä»¥æ·»åŠ  Wiki 页é¢ï¼Œå¦‚果您有任何关于项目 Wiki 的改进建议,请通过 %{issues_link} æäº¤è®®é¢˜ã€‚"
+
+msgid "WikiEmptyIssueMessage|issue tracker"
+msgstr "议题跟踪"
+
+msgid "WikiEmpty|A wiki is where you can store all the details about your project. This can include why you've created it, its principles, how to use it, and so on."
+msgstr "您å¯ä»¥ä½¿ç”¨ Wiki æ¥ä¿å­˜é¡¹ç›®çš„详细信æ¯ï¼Œä¾‹å¦‚:创建该项目的原因ã€é¡¹ç›®çš„原则或者项目的使用说明等等。"
+
+msgid "WikiEmpty|Create your first page"
+msgstr "创建您的第一个页é¢"
+
+msgid "WikiEmpty|Suggest wiki improvement"
+msgstr "Wiki 改进建议"
+
+msgid "WikiEmpty|The wiki lets you write documentation for your project"
+msgstr "您å¯ä»¥ä½¿ç”¨ Wiki 为您的项目编写文档"
+
+msgid "WikiEmpty|This project has no wiki pages"
+msgstr "该项目没有任何 Wiki 页é¢"
+
+msgid "WikiEmpty|You must be a project member in order to add wiki pages."
+msgstr "åªæœ‰é¡¹ç›®æˆå‘˜æ‰å¯ä»¥æ·»åŠ  Wiki 页é¢ã€‚"
msgid "WikiHistoricalPage|This is an old version of this page."
msgstr "这是此页é¢çš„过期版本。"
@@ -4694,6 +5070,12 @@ msgstr "æ–° Wiki 页é¢"
msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?"
msgstr "确定è¦åˆ é™¤æ­¤é¡µé¢å—?"
+msgid "WikiPageConfirmDelete|Delete page"
+msgstr "删除页é¢"
+
+msgid "WikiPageConfirmDelete|Delete page %{pageTitle}?"
+msgstr "删除页é¢%{pageTitle}?"
+
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} å¹¶ç¡®ä¿æ‚¨çš„æ›´æ”¹ä¸ä¼šæ— æ„中删除。"
@@ -4709,8 +5091,8 @@ msgstr "æ›´æ–° %{page_title}"
msgid "WikiPage|Page slug"
msgstr "页é¢å—"
-msgid "WikiPage|Write your content or drag files here..."
-msgstr "在这里撰写您的内容或拖曳文件到此..."
+msgid "WikiPage|Write your content or drag files here…"
+msgstr "在这里编写您的内容或将文件拖动到此处..."
msgid "Wiki|Create Page"
msgstr "创建页é¢"
@@ -4721,9 +5103,6 @@ msgstr "创建页é¢"
msgid "Wiki|Edit Page"
msgstr "修改页é¢"
-msgid "Wiki|Empty page"
-msgstr "空页é¢"
-
msgid "Wiki|More Pages"
msgstr "更多页é¢"
@@ -4742,14 +5121,11 @@ 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 "å–æ¶ˆæƒé™ç”³è¯·"
-msgid "Write a commit message..."
-msgstr "填写æäº¤ä¿¡æ¯..."
+msgid "Yes"
+msgstr "是"
msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
msgstr "å³å°†åˆ é™¤ %{group_name}。已删除的群组无法æ¢å¤ï¼ç¡®å®šç»§ç»­å—?"
@@ -4766,17 +5142,20 @@ msgstr "å°†è¦æŠŠ %{project_full_name} 转移给å¦ä¸€ä¸ªæ‰€æœ‰è€…。确定执è¡
msgid "You are on a read-only GitLab instance."
msgstr "当剿­£åœ¨è®¿é—®åªè¯» GitLab 实例。"
-msgid "You are on a secondary (read-only) Geo node. If you want to make any changes, you must visit the %{primary_node}."
-msgstr "当剿­£åœ¨è®¿é—®Geo次(åªè¯»)节点。如需进行任何写入æ“作,必须访问%{primary_node}。"
+msgid "You can %{linkStart}view the blob%{linkEnd} instead."
+msgstr ""
msgid "You can also create a project from the command line."
-msgstr "å¯ä»¥ä½¿ç”¨å‘½ä»¤è¡Œæ¥åˆ›å»ºé¡¹ç›®ã€‚"
+msgstr "您也å¯ä»¥é€šè¿‡å‘½ä»¤è¡Œæ¥åˆ›å»ºæ–°é¡¹ç›®ã€‚"
msgid "You can also star a label to make it a priority label."
msgstr "å¯ä»¥é€šè¿‡ä¸ºæ ‡è®°è®¾ç½®æ˜Ÿæ ‡æ¥æé«˜å…¶ä¼˜å…ˆçº§ã€‚"
+msgid "You can also test your .gitlab-ci.yml in the %{linkStart}Lint%{linkEnd}"
+msgstr "您也å¯ä»¥é€šè¿‡%{linkStart}Lint%{linkEnd}测试.gitlab-ci.yml"
+
msgid "You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}"
-msgstr "å¯ä»¥è½»æ¾åœ°åœ¨Kubernetes群集上安装Runner。 %{link_to_help_page}"
+msgstr "å¯ä»¥è½»æ¾åœ°åœ¨Kubernetes集群上安装Runner。 %{link_to_help_page}"
msgid "You can move around the graph by using the arrow keys."
msgstr "å¯ä»¥ä½¿ç”¨æ–¹å‘键移动图形。"
@@ -4787,30 +5166,33 @@ msgstr "åªèƒ½åœ¨åˆ†æ”¯ä¸Šæ·»åŠ æ–‡ä»¶"
msgid "You can only edit 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 can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}"
+msgstr "您å¯ä»¥ä½¿ç”¨äº¤äº’模å¼ï¼Œé€šè¿‡é€‰æ‹© %{use_ours} 或 %{use_theirs} 按钮æ¥è§£å†³åˆå¹¶å†²çªã€‚也å¯ä»¥é€šè¿‡ç›´æŽ¥ç¼–辑文件æ¥è§£å†³åˆå¹¶å†²çªã€‚ç„¶åŽå°†è¿™äº›æ›´æ”¹æäº¤åˆ° %{branch_name}"
msgid "You cannot write to this read-only GitLab instance."
msgstr "您ä¸èƒ½å†™å…¥è¿™ä¸ªåªè¯»çš„ GitLab 实例。"
-msgid "You do not have the correct permissions to override the settings from the LDAP group sync."
-msgstr "没有足够æƒé™æ¥ä¿®æ”¹LDAPç»„åŒæ­¥ä¸­çš„设置。"
+msgid "You do not have any assigned merge requests"
+msgstr "没有任何指派给您的åˆå¹¶è¯·æ±‚"
msgid "You have no permissions"
msgstr "没有æƒé™"
+msgid "You have not created any merge requests"
+msgstr "您尚未创建任何åˆå¹¶è¯·æ±‚"
+
msgid "You have reached your project limit"
msgstr "您已达到项目数é‡é™åˆ¶"
-msgid "You must have master access to force delete a lock"
-msgstr "必须拥有 master æƒé™æ‰èƒ½å¼ºåˆ¶è§£é™¤é”定"
+msgid "You must accept our Terms of Service and privacy policy in order to register an account"
+msgstr "æ‚¨å¿…é¡»æŽ¥å—æˆ‘们的æœåŠ¡æ¡æ¬¾å’Œéšç§æ”¿ç­–æ‰èƒ½æ³¨å†Œå¸æˆ·"
+
+msgid "You must have maintainer access to force delete a lock"
+msgstr "必须拥有维护者æƒé™æ‰èƒ½å¼ºåˆ¶åˆ é™¤é”"
msgid "You must sign in to star a project"
msgstr "必须登录æ‰èƒ½å¯¹é¡¹ç›®åŠ æ˜Ÿæ ‡"
-msgid "You need a different license to enable FileLocks feature"
-msgstr "需è¦ä½¿ç”¨ä¸Žå½“å‰ä¸åŒçš„许å¯(license)æ‰èƒ½å¯ç”¨FileLocks功能"
-
msgid "You need permission."
msgstr "需è¦ç›¸å…³çš„æƒé™ã€‚"
@@ -4848,7 +5230,7 @@ msgid "Your Groups"
msgstr "您的群组"
msgid "Your Kubernetes cluster information on this page is still editable, but you are advised to disable and reconfigure"
-msgstr "在此页é¢ä¸Šçš„Kubernetes群集信æ¯ä»å¯ç¼–辑,但建议您ç¦ç”¨å¹¶é‡æ–°é…ç½®"
+msgstr "在此页é¢ä¸Šçš„Kubernetes集群信æ¯ä»å¯ç¼–辑,但建议您ç¦ç”¨å¹¶é‡æ–°é…ç½®"
msgid "Your Projects (default)"
msgstr "您的项目 (默认值)"
@@ -4877,12 +5259,11 @@ msgstr "您的åå­—"
msgid "Your projects"
msgstr "您的项目"
-msgid "among other things"
-msgstr "除其他事项外"
+msgid "ago"
+msgstr "å‰"
-msgid "and %d fixed vulnerability"
-msgid_plural "and %d fixed vulnerabilities"
-msgstr[0] "åŠ%d个修å¤çš„æ¼æ´ž"
+msgid "among other things"
+msgstr "åŠå…¶ä»–功能"
msgid "assign yourself"
msgstr "分é…给自己"
@@ -4890,134 +5271,36 @@ msgstr "分é…给自己"
msgid "branch name"
msgstr "分支åç§°"
-msgid "by"
-msgstr "æ¥è‡ª"
-
-msgid "ciReport|%{type} detected no new security vulnerabilities"
-msgstr "%{type} 未å‘çŽ°æ–°çš„å®‰å…¨æ¼æ´ž"
-
-msgid "ciReport|%{type} detected no security vulnerabilities"
-msgstr "%{type} 未å‘çŽ°å®‰å…¨æ¼æ´ž"
-
-msgid "ciReport|Code quality"
-msgstr "代ç è´¨é‡"
-
-msgid "ciReport|DAST detected no alerts by analyzing the review app"
-msgstr "DAST在审阅应用中未检测到告警"
-
-msgid "ciReport|Dependency scanning"
-msgstr "ä¾èµ–关系扫æ"
-
-msgid "ciReport|Dependency scanning detected"
-msgstr "ä¾èµ–å…³ç³»æ‰«ææ£€æµ‹åˆ°"
-
-msgid "ciReport|Dependency scanning detected no new security vulnerabilities"
-msgstr "ä¾èµ–å…³ç³»æ‰«ææœªæ£€æµ‹åˆ°æ–°çš„å®‰å…¨æ¼æ´ž"
-
-msgid "ciReport|Dependency scanning detected no security vulnerabilities"
-msgstr "ä¾èµ–å…³ç³»æ‰«ææœªæ£€æµ‹åˆ°å®‰å…¨æ¼æ´ž"
-
-msgid "ciReport|Failed to load %{reportName} report"
-msgstr "无法加载 %{reportName} 报告"
-
-msgid "ciReport|Fixed:"
-msgstr "失败:"
-
-msgid "ciReport|Instances"
-msgstr "实例"
-
-msgid "ciReport|Learn more about whitelisting"
-msgstr "进一步了解关于白åå•的信æ¯"
-
-msgid "ciReport|Loading %{reportName} report"
-msgstr "载入%{reportName} 报告"
-
-msgid "ciReport|No changes to code quality"
-msgstr "代ç è´¨é‡æ— å˜åŒ–"
-
-msgid "ciReport|No changes to performance metrics"
-msgstr "性能指标无å˜åŒ–"
-
-msgid "ciReport|Performance metrics"
-msgstr "性能指标"
-
-msgid "ciReport|SAST"
-msgstr "SAST"
-
-msgid "ciReport|SAST detected"
-msgstr "SAST检测到"
-
-msgid "ciReport|SAST detected no new security vulnerabilities"
-msgstr "SAST未å‘çŽ°æ–°çš„å®‰å…¨æ¼æ´ž"
-
-msgid "ciReport|SAST detected no security vulnerabilities"
-msgstr "SAST未å‘çŽ°å®‰å…¨æ¼æ´ž"
-
-msgid "ciReport|SAST:container no vulnerabilities were found"
-msgstr "SAST:container未å‘çŽ°æ¼æ´ž"
-
-msgid "ciReport|Security scanning"
-msgstr "安全扫æ"
-
-msgid "ciReport|Security scanning failed loading any results"
-msgstr "å®‰å…¨æ‰«ææ— æ³•加载任何结果"
-
-msgid "ciReport|Show complete code vulnerabilities report"
-msgstr "æ˜¾ç¤ºå®Œæ•´çš„ä»£ç æ¼æ´žæŠ¥å‘Š"
-
-msgid "ciReport|Unapproved vulnerabilities (red) can be marked as approved. %{helpLink}"
-msgstr "æœªæ‰¹å‡†çš„æ¼æ´ž (红色) å¯ä»¥æ ‡è®°ä¸ºå·²æ‰¹å‡†ã€‚ %{helpLink}"
-
-msgid "ciReport|no vulnerabilities"
-msgstr "æœªæ£€æµ‹åˆ°å®‰å…¨æ¼æ´ž"
-
msgid "command line instructions"
msgstr "命令行指å—"
msgid "connecting"
msgstr "连接中"
-msgid "could not read private key, is the passphrase correct?"
-msgstr "无法读å–ç§é’¥ï¼Œå¯†ç çŸ­è¯­æ˜¯å¦æ­£ç¡®ï¼Ÿ"
-
msgid "day"
msgid_plural "days"
msgstr[0] "天"
-msgid "detected %d fixed vulnerability"
-msgid_plural "detected %d fixed vulnerabilities"
-msgstr[0] "检测到%dä¸ªå®‰å…¨æ¼æ´žå·²ä¿®å¤"
+msgid "deploy token"
+msgstr "部署令牌"
-msgid "detected %d new vulnerability"
-msgid_plural "detected %d new vulnerabilities"
-msgstr[0] "检测到%dä¸ªæ–°çš„å®‰å…¨æ¼æ´ž"
+msgid "disabled"
+msgstr "å·²ç¦ç”¨"
-msgid "detected no vulnerabilities"
-msgstr "æœªæ£€æµ‹åˆ°å®‰å…¨æ¼æ´ž"
+msgid "enabled"
+msgstr "å·²å¯ç”¨"
msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
msgstr "最åŽä¸€æ¬¡%{slash_command} 命令将更新预计时间。"
-msgid "here"
-msgstr "此处"
+msgid "for this project"
+msgstr "对于这个项目"
msgid "importing"
msgstr "导入中"
-msgid "in progress"
-msgstr "进行中"
-
-msgid "is invalid because there is downstream lock"
-msgstr "因下游é”定而无效"
-
-msgid "is invalid because there is upstream lock"
-msgstr "因上游é”定而无效"
-
-msgid "is not a valid X509 certificate."
-msgstr "䏿˜¯æœ‰æ•ˆçš„X509è¯ä¹¦ã€‚"
-
-msgid "locked by %{path_lock_user_name} %{created_at}"
-msgstr "被 %{path_lock_user_name} 于 %{created_at} é”定"
+msgid "latest version"
+msgstr ""
msgid "merge request"
msgid_plural "merge requests"
@@ -5035,29 +5318,8 @@ msgstr "%{metricsLinkStart} 内存 %{metricsLinkEnd} å ç”¨ %{emphasisStart} 上
msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage is %{emphasisStart} unchanged %{emphasisEnd} at %{memoryFrom}MB"
msgstr "%{metricsLinkStart} 内存 %{metricsLinkEnd} å ç”¨ %{emphasisStart} æ— å˜åŒ– %{emphasisEnd}, ä¿æŒåœ¨ %{memoryFrom}MB"
-msgid "mrWidget|Add approval"
-msgstr "增加批准"
-
-msgid "mrWidget|Allows edits from maintainers"
-msgstr "å…许上游项目维护人员进行编辑。"
-
-msgid "mrWidget|An error occured while removing your approval."
-msgstr "删除批准时å‘生错误。"
-
-msgid "mrWidget|An error occured while retrieving approval data for this merge request."
-msgstr "è¯»å–æ­¤åˆå¹¶è¯·æ±‚çš„æ‰¹å‡†æ•°æ®æ—¶å‘生错误。"
-
-msgid "mrWidget|An error occured while submitting your approval."
-msgstr "æäº¤æ‰¹å‡†æ—¶å‘生错误。"
-
-msgid "mrWidget|Approve"
-msgstr "批准"
-
-msgid "mrWidget|Approved"
-msgstr ""
-
-msgid "mrWidget|Approved by"
-msgstr "批准人:"
+msgid "mrWidget|Allows commits from members who can merge to the target branch"
+msgstr "具有åˆå¹¶åˆ°ç›®æ ‡åˆ†æ”¯æƒé™çš„æˆå‘˜å…许æäº¤"
msgid "mrWidget|Cancel automatic merge"
msgstr "å–æ¶ˆè‡ªåЍåˆå¹¶"
@@ -5083,6 +5345,9 @@ msgstr "关闭:"
msgid "mrWidget|Closes"
msgstr "关闭"
+msgid "mrWidget|Create an issue to resolve them later"
+msgstr "创建议题以便åŽç»­å¤„ç†"
+
msgid "mrWidget|Deployment statistics are not available currently"
msgstr "部署统计信æ¯å½“å‰ä¸å¯ç”¨"
@@ -5117,7 +5382,7 @@ msgid "mrWidget|Merge locally"
msgstr "本地åˆå¹¶"
msgid "mrWidget|Merged by"
-msgstr "åˆå¹¶:"
+msgstr "åˆå¹¶è€…:"
msgid "mrWidget|Plain diff"
msgstr "文本差异"
@@ -5137,9 +5402,6 @@ msgstr "删除æºåˆ†æ”¯"
msgid "mrWidget|Remove source branch"
msgstr "删除æºåˆ†æ”¯"
-msgid "mrWidget|Remove your approval"
-msgstr "删除您的批准"
-
msgid "mrWidget|Request to merge"
msgstr "请求åˆå¹¶"
@@ -5179,6 +5441,9 @@ msgstr "æºåˆ†æ”¯ä¸ä¼šè¢«åˆ é™¤"
msgid "mrWidget|There are merge conflicts"
msgstr "存在åˆå¹¶å†²çª"
+msgid "mrWidget|There are unresolved discussions. Please resolve these discussions"
+msgstr "存在尚未解决的讨论。请先解决这些讨论。"
+
msgid "mrWidget|This merge request failed to be merged automatically"
msgstr "该åˆå¹¶è¯·æ±‚未能自动åˆå¹¶"
@@ -5189,7 +5454,7 @@ msgid "mrWidget|This project is archived, write access has been disabled"
msgstr "è¯¥é¡¹ç›®å·²å­˜æ¡£ï¼Œç¦æ­¢å†™å…¥"
msgid "mrWidget|Web IDE"
-msgstr ""
+msgstr "Web IDE"
msgid "mrWidget|You can merge this merge request manually using the"
msgstr "å¯ä»¥æ‰‹åЍåˆå¹¶æ­¤åˆå¹¶è¯·æ±‚,使用以下"
@@ -5228,8 +5493,8 @@ msgstr "密ç "
msgid "personal access token"
msgstr "个人访问令牌"
-msgid "private key does not match certificate."
-msgstr "ç§é’¥ä¸Žè¯ä¹¦ä¸åŒ¹é…。"
+msgid "remaining"
+msgstr "剩余"
msgid "remove due date"
msgstr "删除截止日期"
@@ -5243,9 +5508,6 @@ msgstr "%{slash_command} 将会更新消耗的总时长。"
msgid "this document"
msgstr "此文档"
-msgid "to help your contributors communicate effectively!"
-msgstr "帮助您的贡献者进行有效沟通ï¼"
-
msgid "username"
msgstr "用户å"
@@ -5255,3 +5517,7 @@ msgstr "使用 Kubernetes 集群æ¥éƒ¨ç½²ä»£ç ï¼"
msgid "with %{additions} additions, %{deletions} deletions."
msgstr "å…± %{additions} æ¡æ–°å¢ž, %{deletions} æ¡åˆ é™¤."
+msgid "within %d minute "
+msgid_plural "within %d minutes "
+msgstr[0] "在 %d 分钟内 "
+
diff --git a/locale/zh_HK/gitlab.po b/locale/zh_HK/gitlab.po
index 6bfcae6aa91..d8fb7353484 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: 2018-04-04 19:35+0200\n"
-"PO-Revision-Date: 2018-04-05 03:39-0400\n"
+"POT-Creation-Date: 2018-07-01 16:35+1000\n"
+"PO-Revision-Date: 2018-07-01 11:02\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Chinese Traditional, Hong Kong\n"
"Language: zh_HK\n"
@@ -16,8 +16,9 @@ msgstr ""
"X-Crowdin-Language: zh-HK\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-msgid " and"
-msgstr ""
+msgid "%d changed file"
+msgid_plural "%d changed files"
+msgstr[0] ""
msgid "%d commit"
msgid_plural "%d commits"
@@ -47,6 +48,14 @@ msgid "%d metric"
msgid_plural "%d metrics"
msgstr[0] ""
+msgid "%d staged change"
+msgid_plural "%d staged changes"
+msgstr[0] ""
+
+msgid "%d unstaged change"
+msgid_plural "%d unstaged changes"
+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 次æäº¤ã€‚"
@@ -61,12 +70,21 @@ msgid "%{count} participant"
msgid_plural "%{count} participants"
msgstr[0] ""
+msgid "%{filePath} deleted"
+msgstr ""
+
+msgid "%{group_docs_link_start}Groups%{group_docs_link_end} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects."
+msgstr ""
+
msgid "%{loadingIcon} Started"
msgstr ""
msgid "%{lock_path} is locked by GitLab User %{lock_user_id}"
msgstr ""
+msgid "%{nip_domain} can be used as an alternative to a custom domain."
+msgstr ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -79,6 +97,9 @@ msgstr "已失敗 %{number_of_failures} 次,最大失敗 %{maximum_failures} æ
msgid "%{openOrClose} %{noteable}"
msgstr ""
+msgid "%{percent}%% complete"
+msgstr ""
+
msgid "%{storage_name}: failed storage access attempt on host:"
msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:"
msgstr[0] "%{storage_name}ï¼šå·²è¨ªå•æ­¤ä¸»æ©Ÿå¤±æ•— %{failed_attempts} 次"
@@ -86,15 +107,55 @@ msgstr[0] "%{storage_name}ï¼šå·²è¨ªå•æ­¤ä¸»æ©Ÿå¤±æ•— %{failed_attempts} 次"
msgid "%{text} is available"
msgstr ""
+msgid "%{title} changes"
+msgstr ""
+
+msgid "%{unstaged} unstaged and %{staged} staged changes"
+msgstr ""
+
msgid "(checkout the %{link} for information on how to install it)."
msgstr "(想了解更多的安è£è¨Šæ¯è«‹æŸ¥çœ‹ %{link})"
msgid "+ %{moreCount} more"
msgstr ""
+msgid "- Runner is active and can process any new jobs"
+msgstr ""
+
+msgid "- Runner is paused and will not receive any new jobs"
+msgstr ""
+
msgid "- show less"
msgstr ""
+msgid "1 %{type} addition"
+msgid_plural "%{count} %{type} additions"
+msgstr[0] "%{count}%{type} 個附加"
+
+msgid "1 %{type} modification"
+msgid_plural "%{count} %{type} modifications"
+msgstr[0] "%{count}%{type} 個變更"
+
+msgid "1 closed issue"
+msgid_plural "%d closed issues"
+msgstr[0] ""
+
+msgid "1 closed merge request"
+msgid_plural "%d closed merge requests"
+msgstr[0] ""
+
+msgid "1 merged merge request"
+msgid_plural "%d merged merge requests"
+msgstr[0] ""
+
+msgid "1 open issue"
+msgid_plural "%d open issues"
+msgstr[0] ""
+
+msgid "1 open merge request"
+msgid_plural "%d open merge requests"
+msgstr[0] ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] "%d æ¢æµæ°´ç·š"
@@ -105,9 +166,27 @@ msgstr ""
msgid "2FA enabled"
msgstr ""
+msgid "403|Please contact your GitLab administrator to get the permission."
+msgstr ""
+
+msgid "403|You don't have the permission to access this page."
+msgstr ""
+
+msgid "404|Make sure the address is correct and the page hasn't moved."
+msgstr ""
+
+msgid "404|Page Not Found"
+msgstr ""
+
+msgid "404|Please contact your GitLab administrator if you think this is a mistake."
+msgstr ""
+
msgid "<strong>Removes</strong> source branch"
msgstr ""
+msgid "A 'Runner' is a process which runs a job. You can setup as many Runners as you need."
+msgstr ""
+
msgid "A collection of graphs regarding Continuous Integration"
msgstr "相關æŒçºŒé›†æˆçš„圖åƒé›†åˆ"
@@ -117,6 +196,9 @@ msgstr ""
msgid "A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}."
msgstr ""
+msgid "A regular expression that will be used to find the test coverage output in the job trace. Leave blank to disable"
+msgstr ""
+
msgid "A user with write access to the source branch selected this option"
msgstr ""
@@ -129,36 +211,39 @@ msgstr ""
msgid "Abuse reports"
msgstr ""
+msgid "Accept terms"
+msgstr ""
+
msgid "Access Tokens"
msgstr ""
msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again."
msgstr "å› æ¢å¾©å®‰è£ï¼Œè¨ªå•故障存儲已被暫時ç¦ç”¨ã€‚在å•題解決後將é‡ç½®å­˜å„²ä¿¡æ¯ï¼Œä»¥ä¾¿å†æ¬¡è¨ªå•。"
+msgid "Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report."
+msgstr ""
+
msgid "Account"
msgstr ""
-msgid "Account and limit settings"
+msgid "Account and limit"
msgstr ""
msgid "Active"
msgstr "啟用"
+msgid "Active Sessions"
+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 Kubernetes cluster"
msgstr ""
@@ -171,6 +256,9 @@ msgstr ""
msgid "Add new directory"
msgstr "添加新目錄"
+msgid "Add reaction"
+msgstr ""
+
msgid "Add todo"
msgstr ""
@@ -240,7 +328,10 @@ msgstr ""
msgid "All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings."
msgstr ""
-msgid "Allow edits from maintainers."
+msgid "Allow commits from members who can merge to the target branch."
+msgstr ""
+
+msgid "Allow public access to pipelines and job details, including output logs and artifacts"
msgstr ""
msgid "Allow rendering of PlantUML diagrams in Asciidoc documents."
@@ -252,31 +343,37 @@ msgstr ""
msgid "Allows you to add and manage Kubernetes clusters."
msgstr ""
-msgid "Also called \"Issuer\" or \"Relying party trust identifier\""
+msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
msgstr ""
-msgid "Also called \"Relying party service URL\" or \"Reply URL\""
+msgid "An error occured creating the new branch."
msgstr ""
-msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to connect."
+msgid "An error occured whilst loading all the files."
msgstr ""
-msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
+msgid "An error occured whilst loading the file content."
msgstr ""
-msgid "An error occurred previewing the blob"
+msgid "An error occured whilst loading the file."
msgstr ""
-msgid "An error occurred when toggling the notification subscription"
+msgid "An error occured whilst loading the merge request changes."
+msgstr ""
+
+msgid "An error occured whilst loading the merge request version data."
+msgstr ""
+
+msgid "An error occured whilst loading the merge request."
msgstr ""
-msgid "An error occurred when updating the issue weight"
+msgid "An error occurred previewing the blob"
msgstr ""
-msgid "An error occurred while adding approver"
+msgid "An error occurred when toggling the notification subscription"
msgstr ""
-msgid "An error occurred while detecting host keys"
+msgid "An error occurred while dismissing the alert. Refresh the page and try again."
msgstr ""
msgid "An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again."
@@ -294,10 +391,7 @@ msgstr ""
msgid "An error occurred while getting projects"
msgstr ""
-msgid "An error occurred while importing project"
-msgstr ""
-
-msgid "An error occurred while initializing path locks"
+msgid "An error occurred while importing project: ${details}"
msgstr ""
msgid "An error occurred while loading commits"
@@ -315,9 +409,6 @@ msgstr ""
msgid "An error occurred while making the request."
msgstr ""
-msgid "An error occurred while removing approver"
-msgstr ""
-
msgid "An error occurred while rendering KaTeX"
msgstr ""
@@ -330,9 +421,6 @@ msgstr ""
msgid "An error occurred while retrieving diff"
msgstr ""
-msgid "An error occurred while saving LDAP override status. Please try again."
-msgstr ""
-
msgid "An error occurred while saving assignees"
msgstr ""
@@ -342,9 +430,6 @@ msgstr ""
msgid "An error occurred. Please try again."
msgstr ""
-msgid "Any Label"
-msgstr ""
-
msgid "Appearance"
msgstr ""
@@ -357,28 +442,28 @@ msgstr ""
msgid "April"
msgstr ""
-msgid "Archived project! Repository is read-only"
-msgstr "歸檔項目ï¼å­˜å„²åº«ç‚ºåªè®€"
+msgid "Archived project! Repository and other project resources are read-only"
+msgstr ""
msgid "Are you sure you want to delete this pipeline schedule?"
msgstr "確定è¦åˆªé™¤æ­¤æµæ°´ç·šè¨ˆåŠƒå—Žï¼Ÿ"
+msgid "Are you sure you want to remove this identity?"
+msgstr ""
+
msgid "Are you sure you want to reset registration token?"
msgstr "確定è¦é‡ç½®è¨»å†Šä»¤ç‰Œå—Žï¼Ÿ"
msgid "Are you sure you want to reset the health check token?"
msgstr "確定è¦é‡ç½®å¥åº·æª¢æŸ¥ä»¤ç‰Œå—Žï¼Ÿ"
-msgid "Are you sure you want to unlock %{path_lock_path}?"
-msgstr ""
-
msgid "Are you sure?"
msgstr "確定嗎?"
msgid "Artifacts"
msgstr ""
-msgid "Assertion consumer service URL"
+msgid "Ask your group maintainer to setup a group Runner."
msgstr ""
msgid "Assign custom color like #FF0000"
@@ -402,9 +487,15 @@ msgstr ""
msgid "Assigned to :name"
msgstr ""
+msgid "Assigned to me"
+msgstr ""
+
msgid "Assignee"
msgstr ""
+msgid "Assignee(s)"
+msgstr ""
+
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "拖放文件到此處或者 %{upload_link}"
@@ -438,7 +529,10 @@ msgstr ""
msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
msgstr ""
-msgid "AutoDevOps|Auto DevOps (Beta)"
+msgid "Auto-cancel redundant, pending pipelines"
+msgstr ""
+
+msgid "AutoDevOps|Auto DevOps"
msgstr ""
msgid "AutoDevOps|Auto DevOps documentation"
@@ -459,79 +553,109 @@ msgstr ""
msgid "AutoDevOps|add a Kubernetes cluster"
msgstr ""
-msgid "AutoDevOps|enable Auto DevOps (Beta)"
+msgid "AutoDevOps|enable Auto DevOps"
msgstr ""
msgid "Available"
msgstr ""
+msgid "Available group Runners : %{runners}"
+msgstr ""
+
+msgid "Available group Runners : %{runners}."
+msgstr ""
+
msgid "Avatar will be removed. Are you sure?"
msgstr ""
msgid "Average per day: %{average}"
msgstr ""
-msgid "Background Color"
+msgid "Background color"
msgstr ""
msgid "Background jobs"
msgstr ""
-msgid "Begin with the selected commit"
+msgid "Badges"
+msgstr ""
+
+msgid "Badges|A new badge was added."
msgstr ""
-msgid "Billing"
+msgid "Badges|Add badge"
msgstr ""
-msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan."
+msgid "Badges|Adding the badge failed, please check the entered URLs and try again."
msgstr ""
-msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available."
+msgid "Badges|Badge image URL"
msgstr ""
-msgid "BillingPlans|Current plan"
+msgid "Badges|Badge image preview"
msgstr ""
-msgid "BillingPlans|Customer Support"
+msgid "Badges|Delete badge"
msgstr ""
-msgid "BillingPlans|Downgrade"
+msgid "Badges|Delete badge?"
msgstr ""
-msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
+msgid "Badges|Deleting the badge failed, please try again."
msgstr ""
-msgid "BillingPlans|Manage plan"
+msgid "Badges|Group Badge"
msgstr ""
-msgid "BillingPlans|Please contact %{customer_support_link} in that case."
+msgid "Badges|Link"
msgstr ""
-msgid "BillingPlans|See all %{plan_name} features"
+msgid "Badges|No badge image"
msgstr ""
-msgid "BillingPlans|This group uses the plan associated with its parent group."
+msgid "Badges|No image to preview"
msgstr ""
-msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
+msgid "Badges|Project Badge"
msgstr ""
-msgid "BillingPlans|Upgrade"
+msgid "Badges|Reload badge image"
msgstr ""
-msgid "BillingPlans|You are currently on the %{plan_link} plan."
+msgid "Badges|Save changes"
msgstr ""
-msgid "BillingPlans|frequently asked questions"
+msgid "Badges|Saving the badge failed, please check the entered URLs and try again."
msgstr ""
-msgid "BillingPlans|monthly"
+msgid "Badges|The %{docsLinkStart}variables%{docsLinkEnd} GitLab supports: %{placeholders}"
msgstr ""
-msgid "BillingPlans|paid annually at %{price_per_year}"
+msgid "Badges|The badge was deleted."
msgstr ""
-msgid "BillingPlans|per user"
+msgid "Badges|The badge was saved."
+msgstr ""
+
+msgid "Badges|This group has no badges"
+msgstr ""
+
+msgid "Badges|This project has no badges"
+msgstr ""
+
+msgid "Badges|Your badges"
+msgstr ""
+
+msgid "Begin with the selected commit"
+msgstr ""
+
+msgid "Below are examples of regex for existing tools:"
+msgstr ""
+
+msgid "Boards"
+msgstr ""
+
+msgid "Branch %{branchName} was not found in this project's repository."
msgstr ""
msgid "Branch (%{branch_count})"
@@ -610,7 +734,7 @@ msgstr ""
msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
msgstr ""
-msgid "Branches|Only a project master or owner can delete a protected branch"
+msgid "Branches|Only a project maintainer or owner can delete a protected branch"
msgstr ""
msgid "Branches|Overview"
@@ -646,9 +770,6 @@ msgstr ""
msgid "Branches|Stale branches"
msgstr ""
-msgid "Branches|The branch could not be updated automatically because it has diverged from its upstream counterpart."
-msgstr ""
-
msgid "Branches|The default branch cannot be deleted"
msgstr ""
@@ -661,15 +782,9 @@ msgstr ""
msgid "Branches|To confirm, type %{branch_name_confirmation}:"
msgstr ""
-msgid "Branches|To discard the local changes and overwrite the branch with the upstream version, delete it here and choose 'Update Now' above."
-msgstr ""
-
msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
msgstr ""
-msgid "Branches|diverged from upstream"
-msgstr ""
-
msgid "Branches|merged"
msgstr ""
@@ -691,40 +806,79 @@ msgstr "ç€è¦½æ–‡ä»¶"
msgid "Browse files"
msgstr "ç€è¦½æ–‡ä»¶"
-msgid "Business"
-msgstr ""
-
msgid "ByAuthor|by"
msgstr "作者:"
msgid "CI / CD"
msgstr ""
-msgid "CI/CD"
+msgid "CI / CD Settings"
msgstr ""
msgid "CI/CD configuration"
msgstr ""
-msgid "CI/CD for external repo"
+msgid "CI/CD settings"
+msgstr ""
+
+msgid "CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery."
+msgstr ""
+
+msgid "CICD|Auto DevOps"
+msgstr ""
+
+msgid "CICD|Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration."
+msgstr ""
+
+msgid "CICD|Automatic deployment to staging, manual deployment to production"
+msgstr ""
+
+msgid "CICD|Continuous deployment to production"
+msgstr ""
+
+msgid "CICD|Deployment strategy"
+msgstr ""
+
+msgid "CICD|Deployment strategy needs a domain name to work correctly."
+msgstr ""
+
+msgid "CICD|Disable Auto DevOps"
+msgstr ""
+
+msgid "CICD|Enable Auto DevOps"
+msgstr ""
+
+msgid "CICD|Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific %{ci_file}."
+msgstr ""
+
+msgid "CICD|Instance default (%{state})"
msgstr ""
msgid "CICD|Jobs"
msgstr ""
-msgid "Cancel"
-msgstr "å–æ¶ˆ"
+msgid "CICD|Learn more about Auto DevOps"
+msgstr ""
-msgid "Cannot be merged automatically"
+msgid "CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project."
msgstr ""
-msgid "Cannot modify managed Kubernetes cluster"
+msgid "CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages."
+msgstr ""
+
+msgid "Can't find HEAD commit for this branch"
+msgstr ""
+
+msgid "Cancel"
+msgstr "å–æ¶ˆ"
+
+msgid "Cancel this job"
msgstr ""
-msgid "Certificate fingerprint"
+msgid "Cannot be merged automatically"
msgstr ""
-msgid "Change Weight"
+msgid "Cannot modify managed Kubernetes cluster"
msgstr ""
msgid "Change this value to influence how frequently the GitLab UI polls for updates."
@@ -778,21 +932,18 @@ msgstr ""
msgid "Choose a branch/tag (e.g. %{master}) or enter a commit (e.g. %{sha}) to see what's changed or to create a merge request."
msgstr ""
-msgid "Choose file..."
+msgid "Choose any color."
msgstr ""
-msgid "Choose which groups you wish to synchronize to this secondary node."
+msgid "Choose between <code>clone</code> or <code>fetch</code> to get the recent application code"
msgstr ""
-msgid "Choose which repositories you want to connect and run CI/CD pipelines."
+msgid "Choose file..."
msgstr ""
msgid "Choose which repositories you want to import."
msgstr ""
-msgid "Choose which shards you wish to synchronize to this secondary node."
-msgstr ""
-
msgid "CiStatusLabel|canceled"
msgstr "已喿¶ˆ"
@@ -862,21 +1013,12 @@ msgstr ""
msgid "CiVariable|All environments"
msgstr ""
-msgid "CiVariable|Create wildcard"
-msgstr ""
-
msgid "CiVariable|Error occured while saving variables"
msgstr ""
-msgid "CiVariable|New environment"
-msgstr ""
-
msgid "CiVariable|Protected"
msgstr ""
-msgid "CiVariable|Search environments"
-msgstr ""
-
msgid "CiVariable|Toggle protected"
msgstr ""
@@ -886,28 +1028,28 @@ msgstr ""
msgid "CircuitBreakerApiLink|circuitbreaker api"
msgstr ""
-msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
+msgid "Clear search input"
msgstr ""
-msgid "Click to expand text"
+msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Client authentication certificate"
+msgid "Click the <strong>Promote</strong> button in the top right corner to promote it to a group milestone."
msgstr ""
-msgid "Client authentication key"
+msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
msgstr ""
-msgid "Client authentication key password"
+msgid "Click to expand it."
msgstr ""
-msgid "Clone repository"
+msgid "Click to expand text"
msgstr ""
-msgid "Close"
+msgid "Clone repository"
msgstr ""
-msgid "Closed"
+msgid "Close"
msgstr ""
msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster"
@@ -925,6 +1067,12 @@ msgstr ""
msgid "ClusterIntegration|Advanced options on this Kubernetes cluster's integration"
msgstr ""
+msgid "ClusterIntegration|An error occured while trying to fetch project zones: %{error}"
+msgstr ""
+
+msgid "ClusterIntegration|An error occured while trying to fetch your projects: %{error}"
+msgstr ""
+
msgid "ClusterIntegration|Applications"
msgstr ""
@@ -955,6 +1103,9 @@ msgstr ""
msgid "ClusterIntegration|Copy Ingress IP Address to clipboard"
msgstr ""
+msgid "ClusterIntegration|Copy Jupyter Hostname to clipboard"
+msgstr ""
+
msgid "ClusterIntegration|Copy Kubernetes cluster name"
msgstr ""
@@ -970,7 +1121,7 @@ msgstr ""
msgid "ClusterIntegration|Create a new Kubernetes cluster on Google Kubernetes Engine right from GitLab"
msgstr ""
-msgid "ClusterIntegration|Create on GKE"
+msgid "ClusterIntegration|Create on Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
@@ -982,13 +1133,25 @@ msgstr ""
msgid "ClusterIntegration|Environment scope"
msgstr ""
+msgid "ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab's Google Kubernetes Engine Integration."
+msgstr ""
+
+msgid "ClusterIntegration|Fetching machine types"
+msgstr ""
+
+msgid "ClusterIntegration|Fetching projects"
+msgstr ""
+
+msgid "ClusterIntegration|Fetching zones"
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
msgid "ClusterIntegration|GitLab Runner"
msgstr ""
-msgid "ClusterIntegration|Google Cloud Platform project ID"
+msgid "ClusterIntegration|Google Cloud Platform project"
msgstr ""
msgid "ClusterIntegration|Google Kubernetes Engine"
@@ -1000,9 +1163,6 @@ msgstr ""
msgid "ClusterIntegration|Helm Tiller"
msgstr ""
-msgid "ClusterIntegration|In order to show the health of the cluster, we'll need to provision your cluster with Prometheus to collect the required data."
-msgstr ""
-
msgid "ClusterIntegration|Ingress"
msgstr ""
@@ -1012,9 +1172,6 @@ msgstr ""
msgid "ClusterIntegration|Install"
msgstr ""
-msgid "ClusterIntegration|Install Prometheus"
-msgstr ""
-
msgid "ClusterIntegration|Installed"
msgstr ""
@@ -1027,13 +1184,16 @@ msgstr ""
msgid "ClusterIntegration|Integration status"
msgstr ""
-msgid "ClusterIntegration|Kubernetes cluster"
+msgid "ClusterIntegration|Jupyter Hostname"
msgstr ""
-msgid "ClusterIntegration|Kubernetes cluster details"
+msgid "ClusterIntegration|JupyterHub"
msgstr ""
-msgid "ClusterIntegration|Kubernetes cluster health"
+msgid "ClusterIntegration|Kubernetes cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Kubernetes cluster details"
msgstr ""
msgid "ClusterIntegration|Kubernetes cluster integration"
@@ -1063,7 +1223,13 @@ msgstr ""
msgid "ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project"
msgstr ""
-msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
+msgid "ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}."
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{help_link_start}Kubernetes%{help_link_end}."
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}."
msgstr ""
msgid "ClusterIntegration|Learn more about environments"
@@ -1087,7 +1253,16 @@ msgstr ""
msgid "ClusterIntegration|More information"
msgstr ""
-msgid "ClusterIntegration|Multiple Kubernetes clusters are available in GitLab Enterprise Edition Premium and Ultimate"
+msgid "ClusterIntegration|No machine types matched your search"
+msgstr ""
+
+msgid "ClusterIntegration|No projects found"
+msgstr ""
+
+msgid "ClusterIntegration|No projects matched your search"
+msgstr ""
+
+msgid "ClusterIntegration|No zones matched your search"
msgstr ""
msgid "ClusterIntegration|Note:"
@@ -1102,9 +1277,6 @@ msgstr ""
msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
msgstr ""
-msgid "ClusterIntegration|Project ID"
-msgstr ""
-
msgid "ClusterIntegration|Project namespace"
msgstr ""
@@ -1117,6 +1289,9 @@ msgstr ""
msgid "ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration."
msgstr ""
+msgid "ClusterIntegration|Redeem up to $500 in free credit for Google Cloud Platform"
+msgstr ""
+
msgid "ClusterIntegration|Remove Kubernetes cluster integration"
msgstr ""
@@ -1132,19 +1307,37 @@ msgstr ""
msgid "ClusterIntegration|Save changes"
msgstr ""
+msgid "ClusterIntegration|Search machine types"
+msgstr ""
+
+msgid "ClusterIntegration|Search projects"
+msgstr ""
+
+msgid "ClusterIntegration|Search zones"
+msgstr ""
+
msgid "ClusterIntegration|Security"
msgstr ""
msgid "ClusterIntegration|See and edit the details for your Kubernetes cluster"
msgstr ""
-msgid "ClusterIntegration|See machine types"
+msgid "ClusterIntegration|Select machine type"
+msgstr ""
+
+msgid "ClusterIntegration|Select project"
+msgstr ""
+
+msgid "ClusterIntegration|Select project and zone to choose machine type"
+msgstr ""
+
+msgid "ClusterIntegration|Select project to choose zone"
msgstr ""
-msgid "ClusterIntegration|See your projects"
+msgid "ClusterIntegration|Select zone"
msgstr ""
-msgid "ClusterIntegration|See zones"
+msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
msgid "ClusterIntegration|Service token"
@@ -1177,6 +1370,9 @@ msgstr ""
msgid "ClusterIntegration|Token"
msgstr ""
+msgid "ClusterIntegration|Validating project billing status"
+msgstr ""
+
msgid "ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
@@ -1207,13 +1403,19 @@ msgstr ""
msgid "ClusterIntegration|properly configured"
msgstr ""
+msgid "ClusterIntegration|sign up"
+msgstr ""
+
msgid "Collapse"
msgstr ""
-msgid "Comment and resolve discussion"
+msgid "Collapse sidebar"
+msgstr ""
+
+msgid "Comment & resolve discussion"
msgstr ""
-msgid "Comment and unresolve discussion"
+msgid "Comment & unresolve discussion"
msgstr ""
msgid "Comments"
@@ -1278,6 +1480,9 @@ msgstr ""
msgid "Committed by"
msgstr "æäº¤è€…:"
+msgid "Commit…"
+msgstr ""
+
msgid "Compare"
msgstr "比較"
@@ -1326,6 +1531,9 @@ msgstr ""
msgid "Configure limits for web and API requests."
msgstr ""
+msgid "Configure push mirrors."
+msgstr ""
+
msgid "Configure storage path and circuit breaker settings."
msgstr ""
@@ -1335,18 +1543,9 @@ msgstr ""
msgid "Connect"
msgstr ""
-msgid "Connect all repositories"
-msgstr ""
-
msgid "Connect repositories from GitHub"
msgstr ""
-msgid "Connect your external repositories, and CI/CD pipelines will run for new commits. A GitLab project will be created with only CI/CD features enabled."
-msgstr ""
-
-msgid "Connecting..."
-msgstr ""
-
msgid "Container Registry"
msgstr ""
@@ -1392,9 +1591,18 @@ 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 "ContainerRegistry|You can also use a %{deploy_token} for read-only access to the registry images."
+msgstr ""
+
+msgid "Continue"
+msgstr ""
+
msgid "Continuous Integration and Deployment"
msgstr ""
+msgid "Contribute to GitLab"
+msgstr ""
+
msgid "Contribution"
msgstr ""
@@ -1416,15 +1624,6 @@ msgstr ""
msgid "ContributorsPage|Please wait a moment, this page will automatically refresh when ready."
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 ""
-
msgid "Copy URL to clipboard"
msgstr "複製URL到剪貼æ¿"
@@ -1437,9 +1636,18 @@ msgstr ""
msgid "Copy commit SHA to clipboard"
msgstr "複製æäº¤ SHA 到剪貼æ¿"
+msgid "Copy file name to clipboard"
+msgstr ""
+
+msgid "Copy file path to clipboard"
+msgstr ""
+
msgid "Copy reference to clipboard"
msgstr ""
+msgid "Copy to clipboard"
+msgstr ""
+
msgid "Create"
msgstr ""
@@ -1458,15 +1666,15 @@ msgstr "在帳戶上創建個人訪å•ä»¤ç‰Œï¼Œä»¥é€šéŽ %{protocol} ä¾†æ‹‰å–æˆ
msgid "Create branch"
msgstr ""
+msgid "Create commit"
+msgstr ""
+
msgid "Create directory"
msgstr "創建目錄"
msgid "Create empty repository"
msgstr ""
-msgid "Create epic"
-msgstr ""
-
msgid "Create file"
msgstr ""
@@ -1509,13 +1717,10 @@ msgstr "標籤"
msgid "CreateTokenToCloneLink|create a personal access token"
msgstr "創建個人訪å•令牌"
-msgid "Creates a new branch from %{branchName}"
-msgstr ""
-
-msgid "Creates a new branch from %{branchName} and re-directs to create a new merge request"
+msgid "Created"
msgstr ""
-msgid "Creating epic"
+msgid "Created by me"
msgstr ""
msgid "Cron Timezone"
@@ -1524,7 +1729,13 @@ msgstr "Cron 時å€"
msgid "Cron syntax"
msgstr "Cron 語法"
-msgid "Current node"
+msgid "CurrentUser|Profile"
+msgstr ""
+
+msgid "CurrentUser|Settings"
+msgstr ""
+
+msgid "Custom CI config path"
msgstr ""
msgid "Custom notification events"
@@ -1533,9 +1744,6 @@ 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 "Customize colors"
-msgstr ""
-
msgid "Cycle Analytics"
msgstr "週期分æž"
@@ -1572,7 +1780,7 @@ msgstr ""
msgid "December"
msgstr ""
-msgid "Default classification label"
+msgid "Decline and sign out"
msgstr ""
msgid "Define a custom pattern with cron syntax"
@@ -1581,6 +1789,9 @@ msgstr "使用 Cron 語法定義自定義模å¼"
msgid "Delete"
msgstr "刪除"
+msgid "Delete list"
+msgstr ""
+
msgid "Deploy"
msgid_plural "Deploys"
msgstr[0] "部署"
@@ -1588,37 +1799,163 @@ msgstr[0] "部署"
msgid "Deploy Keys"
msgstr ""
-msgid "Description"
-msgstr "æè¿°"
+msgid "DeployKeys|+%{count} others"
+msgstr ""
+
+msgid "DeployKeys|Current project"
+msgstr ""
+
+msgid "DeployKeys|Deploy key"
+msgstr ""
+
+msgid "DeployKeys|Enabled deploy keys"
+msgstr ""
-msgid "Description templates allow you to define context-specific templates for issue and merge request description fields for your project."
+msgid "DeployKeys|Error enabling deploy key"
msgstr ""
+msgid "DeployKeys|Error getting deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Error removing deploy key"
+msgstr ""
+
+msgid "DeployKeys|Expand %{count} other projects"
+msgstr ""
+
+msgid "DeployKeys|Loading deploy keys"
+msgstr ""
+
+msgid "DeployKeys|No deploy keys found. Create one with the form above."
+msgstr ""
+
+msgid "DeployKeys|Privately accessible deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Project usage"
+msgstr ""
+
+msgid "DeployKeys|Publicly accessible deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Read access only"
+msgstr ""
+
+msgid "DeployKeys|Write access allowed"
+msgstr ""
+
+msgid "DeployKeys|You are going to remove this deploy key. Are you sure?"
+msgstr ""
+
+msgid "DeployTokens|Active Deploy Tokens (%{active_tokens})"
+msgstr ""
+
+msgid "DeployTokens|Add a deploy token"
+msgstr ""
+
+msgid "DeployTokens|Allows read-only access to the registry images"
+msgstr ""
+
+msgid "DeployTokens|Allows read-only access to the repository"
+msgstr ""
+
+msgid "DeployTokens|Copy deploy token to clipboard"
+msgstr ""
+
+msgid "DeployTokens|Copy username to clipboard"
+msgstr ""
+
+msgid "DeployTokens|Create deploy token"
+msgstr ""
+
+msgid "DeployTokens|Created"
+msgstr ""
+
+msgid "DeployTokens|Deploy Tokens"
+msgstr ""
+
+msgid "DeployTokens|Deploy tokens allow read-only access to your repository and registry images."
+msgstr ""
+
+msgid "DeployTokens|Expires"
+msgstr ""
+
+msgid "DeployTokens|Name"
+msgstr ""
+
+msgid "DeployTokens|Pick a name for the application, and we'll give you a unique deploy token."
+msgstr ""
+
+msgid "DeployTokens|Revoke"
+msgstr ""
+
+msgid "DeployTokens|Revoke %{name}"
+msgstr ""
+
+msgid "DeployTokens|Scopes"
+msgstr ""
+
+msgid "DeployTokens|This action cannot be undone."
+msgstr ""
+
+msgid "DeployTokens|This project has no active Deploy Tokens."
+msgstr ""
+
+msgid "DeployTokens|Use this token as a password. Make sure you save it - you won't be able to access it again."
+msgstr ""
+
+msgid "DeployTokens|Use this username as a login."
+msgstr ""
+
+msgid "DeployTokens|Username"
+msgstr ""
+
+msgid "DeployTokens|You are about to revoke"
+msgstr ""
+
+msgid "DeployTokens|Your New Deploy Token"
+msgstr ""
+
+msgid "DeployTokens|Your new project deploy token has been created."
+msgstr ""
+
+msgid "Deprioritize label"
+msgstr ""
+
+msgid "Description"
+msgstr "æè¿°"
+
msgid "Details"
msgstr "詳情"
msgid "Diffs|No file name available"
msgstr ""
+msgid "Diffs|Something went wrong while fetching diff lines."
+msgstr ""
+
msgid "Directory name"
msgstr "目錄å稱"
msgid "Disable"
msgstr ""
-msgid "Discard draft"
+msgid "Disable for this project"
msgstr ""
-msgid "Discover GitLab Geo."
+msgid "Disable group Runners"
msgstr ""
-msgid "Dismiss Cycle Analytics introduction box"
+msgid "Discard changes"
+msgstr ""
+
+msgid "Discard draft"
msgstr ""
-msgid "Dismiss Merge Request promotion"
+msgid "Dismiss Cycle Analytics introduction box"
msgstr ""
-msgid "Documentation for popular identity providers"
+msgid "Domain"
msgstr ""
msgid "Don't show again"
@@ -1660,31 +1997,34 @@ msgstr ""
msgid "Due date"
msgstr ""
-msgid "During this process, you’ll be asked for URLs from GitLab’s side. Use the URLs shown below."
+msgid "Each Runner can be in one of the following states:"
msgstr ""
msgid "Edit"
msgstr "編輯"
+msgid "Edit Label"
+msgstr ""
+
msgid "Edit Pipeline Schedule %{id}"
msgstr "編輯 %{id} æµæ°´ç·šè¨ˆåŠƒ"
msgid "Edit files in the editor and commit changes here"
msgstr ""
-msgid "Editing"
+msgid "Edit identity for %{user_name}"
msgstr ""
-msgid "Elasticsearch"
+msgid "Email"
msgstr ""
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "Email patch"
msgstr ""
-msgid "Email"
+msgid "Emails"
msgstr ""
-msgid "Emails"
+msgid "Embed"
msgstr ""
msgid "Enable"
@@ -1693,9 +2033,6 @@ msgstr ""
msgid "Enable Auto DevOps"
msgstr ""
-msgid "Enable SAML authentication for this group"
-msgstr ""
-
msgid "Enable Sentry for error reporting and logging."
msgstr ""
@@ -1705,7 +2042,13 @@ msgstr ""
msgid "Enable and configure Prometheus metrics."
msgstr ""
-msgid "Enable classification control using an external service"
+msgid "Enable for this project"
+msgstr ""
+
+msgid "Enable group Runners"
+msgstr ""
+
+msgid "Enable or disable certain group features and choose access levels."
msgstr ""
msgid "Enable or disable version check and usage ping."
@@ -1717,7 +2060,10 @@ msgstr ""
msgid "Enable the Performance Bar for a given group."
msgstr ""
-msgid "Enabled"
+msgid "Ends at (UTC)"
+msgstr ""
+
+msgid "Environments"
msgstr ""
msgid "Environments|An error occurred while fetching the environments."
@@ -1768,43 +2114,40 @@ msgstr ""
msgid "Environments|You don't have any environments right now."
msgstr ""
-msgid "Epic will be removed! Are you sure?"
-msgstr ""
-
-msgid "Epics"
+msgid "Error Reporting and Logging"
msgstr ""
-msgid "Epics Roadmap"
+msgid "Error committing changes. Please try again."
msgstr ""
-msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
+msgid "Error fetching contributors data."
msgstr ""
-msgid "Error Reporting and Logging"
+msgid "Error fetching job trace"
msgstr ""
-msgid "Error checking branch data. Please try again."
+msgid "Error fetching labels."
msgstr ""
-msgid "Error committing changes. Please try again."
+msgid "Error fetching network graph."
msgstr ""
-msgid "Error creating epic"
+msgid "Error fetching refs"
msgstr ""
-msgid "Error fetching contributors data."
+msgid "Error fetching usage ping data."
msgstr ""
-msgid "Error fetching labels."
+msgid "Error loading branch data. Please try again."
msgstr ""
-msgid "Error fetching network graph."
+msgid "Error loading last commit."
msgstr ""
-msgid "Error fetching refs"
+msgid "Error loading merge requests."
msgstr ""
-msgid "Error fetching usage ping data."
+msgid "Error loading project data. Please try again."
msgstr ""
msgid "Error occurred when toggling the notification subscription"
@@ -1819,6 +2162,9 @@ msgstr ""
msgid "Error updating todo status."
msgstr ""
+msgid "Estimated"
+msgstr ""
+
msgid "EventFilterBy|Filter by all"
msgstr "全部"
@@ -1849,31 +2195,16 @@ msgstr "æ¯é€±åŸ·è¡Œï¼ˆå‘¨æ—¥æ·©æ™¨ 4 點)"
msgid "Expand"
msgstr ""
-msgid "Explore projects"
-msgstr ""
-
-msgid "Explore public groups"
-msgstr ""
-
-msgid "External Classification Policy Authorization"
-msgstr ""
-
-msgid "External authentication"
-msgstr ""
-
-msgid "External authorization denied access to this project"
+msgid "Expand all"
msgstr ""
-msgid "External authorization request timeout"
+msgid "Expand sidebar"
msgstr ""
-msgid "ExternalAuthorizationService|Classification Label"
-msgstr ""
-
-msgid "ExternalAuthorizationService|Classification label"
+msgid "Explore projects"
msgstr ""
-msgid "ExternalAuthorizationService|When no classification label is set the default label `%{default_label}` will be used."
+msgid "Explore public groups"
msgstr ""
msgid "Failed"
@@ -1885,6 +2216,9 @@ msgstr ""
msgid "Failed to change the owner"
msgstr "無法變更所有者"
+msgid "Failed to check related branches."
+msgstr ""
+
msgid "Failed to remove issue from board, please try again."
msgstr ""
@@ -1894,6 +2228,12 @@ msgstr "ç„¡æ³•åˆªé™¤æµæ°´ç·šè¨ˆåŠƒ"
msgid "Failed to update issues, please try again."
msgstr ""
+msgid "Failure"
+msgstr ""
+
+msgid "Faster as it re-uses the project workspace (falling back to clone if it doesn't exist)"
+msgstr ""
+
msgid "Feb"
msgstr ""
@@ -1903,18 +2243,12 @@ msgstr ""
msgid "Fields on this page are now uneditable, you can configure"
msgstr ""
-msgid "File name"
-msgstr ""
-
msgid "Files"
msgstr "文件"
msgid "Files (%{human_size})"
msgstr ""
-msgid "Fill in the fields below, turn on <strong>%{enable_label}</strong>, and press <strong>%{save_changes}</strong>"
-msgstr ""
-
msgid "Filter by commit message"
msgstr "按æäº¤æ¶ˆæ¯éŽæ¿¾"
@@ -1933,10 +2267,13 @@ msgstr "首次推é€"
msgid "FirstPushedBy|pushed by"
msgstr "推é€è€…:"
-msgid "Font Color"
+msgid "For internal projects, any logged in user can view pipelines and access job details (output logs and artifacts)"
+msgstr ""
+
+msgid "For private projects, any member (guest or higher) can view pipelines and access job details (output logs and artifacts)"
msgstr ""
-msgid "Footer message"
+msgid "For public projects, anyone can view pipelines and access job details (output logs and artifacts)"
msgstr ""
msgid "Fork"
@@ -1955,6 +2292,9 @@ msgstr ""
msgid "Format"
msgstr ""
+msgid "Found errors in your .gitlab-ci.yml:"
+msgstr ""
+
msgid "From %{provider_title}"
msgstr ""
@@ -1970,166 +2310,13 @@ msgstr ""
msgid "GPG Keys"
msgstr ""
-msgid "Generate a default set of labels"
-msgstr ""
-
-msgid "Geo Nodes"
-msgstr ""
-
-msgid "Geo allows you to replicate your GitLab instance to other geographical locations."
-msgstr ""
-
-msgid "GeoNodeSyncStatus|Node is failing or broken."
-msgstr ""
-
-msgid "GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage."
-msgstr ""
-
-msgid "GeoNodes|Checksummed"
-msgstr ""
-
-msgid "GeoNodes|Database replication lag:"
-msgstr ""
-
-msgid "GeoNodes|Disabling a node stops the sync process. Are you sure?"
-msgstr ""
-
-msgid "GeoNodes|Does not match the primary storage configuration"
-msgstr ""
-
-msgid "GeoNodes|Failed"
-msgstr ""
-
-msgid "GeoNodes|Full"
-msgstr ""
-
-msgid "GeoNodes|GitLab version does not match the primary node version"
-msgstr ""
-
-msgid "GeoNodes|GitLab version:"
-msgstr ""
-
-msgid "GeoNodes|Health status:"
-msgstr ""
-
-msgid "GeoNodes|Last event ID processed by cursor:"
-msgstr ""
-
-msgid "GeoNodes|Last event ID seen from primary:"
-msgstr ""
-
-msgid "GeoNodes|Loading nodes"
-msgstr ""
-
-msgid "GeoNodes|Local Attachments:"
+msgid "General"
msgstr ""
-msgid "GeoNodes|Local LFS objects:"
+msgid "General pipelines"
msgstr ""
-msgid "GeoNodes|Local job artifacts:"
-msgstr ""
-
-msgid "GeoNodes|New node"
-msgstr ""
-
-msgid "GeoNodes|Node Authentication was successfully repaired."
-msgstr ""
-
-msgid "GeoNodes|Node was successfully removed."
-msgstr ""
-
-msgid "GeoNodes|Not checksummed"
-msgstr ""
-
-msgid "GeoNodes|Out of sync"
-msgstr ""
-
-msgid "GeoNodes|Removing a node stops the sync process. Are you sure?"
-msgstr ""
-
-msgid "GeoNodes|Replication slot WAL:"
-msgstr ""
-
-msgid "GeoNodes|Replication slots:"
-msgstr ""
-
-msgid "GeoNodes|Repositories checksummed:"
-msgstr ""
-
-msgid "GeoNodes|Repositories:"
-msgstr ""
-
-msgid "GeoNodes|Repository checksums verified:"
-msgstr ""
-
-msgid "GeoNodes|Selective"
-msgstr ""
-
-msgid "GeoNodes|Something went wrong while changing node status"
-msgstr ""
-
-msgid "GeoNodes|Something went wrong while removing node"
-msgstr ""
-
-msgid "GeoNodes|Something went wrong while repairing node"
-msgstr ""
-
-msgid "GeoNodes|Storage config:"
-msgstr ""
-
-msgid "GeoNodes|Sync settings:"
-msgstr ""
-
-msgid "GeoNodes|Synced"
-msgstr ""
-
-msgid "GeoNodes|Unused slots"
-msgstr ""
-
-msgid "GeoNodes|Unverified"
-msgstr ""
-
-msgid "GeoNodes|Used slots"
-msgstr ""
-
-msgid "GeoNodes|Verified"
-msgstr ""
-
-msgid "GeoNodes|Wiki checksums verified:"
-msgstr ""
-
-msgid "GeoNodes|Wikis checksummed:"
-msgstr ""
-
-msgid "GeoNodes|Wikis:"
-msgstr ""
-
-msgid "GeoNodes|You have configured Geo nodes using an insecure HTTP connection. We recommend the use of HTTPS."
-msgstr ""
-
-msgid "Geo|All projects"
-msgstr ""
-
-msgid "Geo|File sync capacity"
-msgstr ""
-
-msgid "Geo|Groups to synchronize"
-msgstr ""
-
-msgid "Geo|Projects in certain groups"
-msgstr ""
-
-msgid "Geo|Projects in certain storage shards"
-msgstr ""
-
-msgid "Geo|Repository sync capacity"
-msgstr ""
-
-msgid "Geo|Select groups to replicate."
-msgstr ""
-
-msgid "Geo|Shards to synchronize"
+msgid "Generate a default set of labels"
msgstr ""
msgid "Git repository URL"
@@ -2141,6 +2328,9 @@ msgstr ""
msgid "Git storage health information has been reset"
msgstr "Git 存儲å¥åº·ä¿¡æ¯å·²é‡ç½®"
+msgid "Git strategy for pipelines"
+msgstr ""
+
msgid "Git version"
msgstr ""
@@ -2150,21 +2340,24 @@ msgstr ""
msgid "GitLab CI Linter has been moved"
msgstr ""
-msgid "GitLab Geo"
+msgid "GitLab Group Runners can execute code for all the projects in this group."
msgstr ""
msgid "GitLab Runner section"
msgstr "GitLab Runner 介紹"
-msgid "GitLab single sign on URL"
-msgstr ""
-
msgid "Gitaly"
msgstr ""
msgid "Gitaly Servers"
msgstr ""
+msgid "Gitaly|Address"
+msgstr ""
+
+msgid "Go Back"
+msgstr ""
+
msgid "Go back"
msgstr ""
@@ -2180,22 +2373,19 @@ msgstr ""
msgid "Got it!"
msgstr ""
-msgid "GroupRoadmap|Epics let you manage your portfolio of projects more efficiently and with less effort"
-msgstr ""
-
-msgid "GroupRoadmap|From %{dateWord}"
+msgid "Graph"
msgstr ""
-msgid "GroupRoadmap|Loading roadmap"
+msgid "Group CI/CD settings"
msgstr ""
-msgid "GroupRoadmap|Something went wrong while fetching epics"
+msgid "Group ID"
msgstr ""
-msgid "GroupRoadmap|To view the roadmap, add a planned start or finish date to one of your epics in this group or its subgroups. Only epics in the past 3 months and the next 3 months are shown &ndash; from %{startDate} to %{endDate}."
+msgid "Group Runners"
msgstr ""
-msgid "GroupRoadmap|Until %{dateWord}"
+msgid "Group maintainers can register group runners in the %{link}"
msgstr ""
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
@@ -2222,6 +2412,9 @@ msgstr ""
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
+msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
+msgstr ""
+
msgid "GroupsEmptyState|A group is a collection of several projects."
msgstr ""
@@ -2261,12 +2454,6 @@ msgstr ""
msgid "GroupsTree|Sorry, no groups or projects matched your search"
msgstr ""
-msgid "Have your users email"
-msgstr ""
-
-msgid "Header message"
-msgstr ""
-
msgid "Health Check"
msgstr "å¥åº·æª¢æŸ¥ (Health Check)"
@@ -2298,19 +2485,49 @@ msgid "Hide value"
msgid_plural "Hide values"
msgstr[0] ""
+msgid "Hide whitespace changes"
+msgstr ""
+
msgid "History"
msgstr ""
msgid "Housekeeping successfully started"
msgstr "已開始維護"
-msgid "Identity provider single sign on URL"
+msgid "I accept the %{terms_link}"
+msgstr ""
+
+msgid "I accept the|Terms of Service and Privacy Policy"
+msgstr ""
+
+msgid "ID"
+msgstr ""
+
+msgid "IDE|Commit"
+msgstr ""
+
+msgid "IDE|Edit"
+msgstr ""
+
+msgid "IDE|Go back"
+msgstr ""
+
+msgid "IDE|Open in file view"
+msgstr ""
+
+msgid "IDE|Review"
+msgstr ""
+
+msgid "Identifier"
+msgstr ""
+
+msgid "Identities"
msgstr ""
-msgid "If enabled, access to projects will be validated on an external service using their classification label."
+msgid "If disabled, the access level will depend on the user's permissions in the project."
msgstr ""
-msgid "If using GitHub, you’ll see pipeline statuses on GitHub for your commits and pull requests. %{more_info_link}"
+msgid "If enabled"
msgstr ""
msgid "If you already have files you can push them using the %{link_to_cli} below."
@@ -2319,6 +2536,15 @@ msgstr ""
msgid "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>."
msgstr ""
+msgid "ImageDiffViewer|2-up"
+msgstr ""
+
+msgid "ImageDiffViewer|Onion skin"
+msgstr ""
+
+msgid "ImageDiffViewer|Swipe"
+msgstr ""
+
msgid "Import"
msgstr ""
@@ -2334,16 +2560,10 @@ msgstr ""
msgid "Import repository"
msgstr "導入存儲庫"
-msgid "ImportButtons|Connect repositories from"
-msgstr ""
-
-msgid "Improve Issue boards with GitLab Enterprise Edition."
-msgstr ""
-
-msgid "Improve issues management with Issue weight and GitLab Enterprise Edition."
+msgid "Include a Terms of Service agreement and Privacy Policy that all users must accept."
msgstr ""
-msgid "Improve search with Advanced Global Search and GitLab Enterprise Edition."
+msgid "Inline"
msgstr ""
msgid "Install Runner on Kubernetes"
@@ -2352,16 +2572,15 @@ msgstr ""
msgid "Install a Runner compatible with GitLab CI"
msgstr "安è£å£¹å€‹èˆ‡ GitLab CI 兼容的 Runner"
-msgid "Instance"
-msgid_plural "Instances"
-msgstr[0] ""
-
msgid "Instance does not support multiple Kubernetes clusters"
msgstr ""
msgid "Integrations"
msgstr ""
+msgid "Integrations Settings"
+msgstr ""
+
msgid "Interested parties can even contribute by pushing commits if they want to."
msgstr ""
@@ -2377,7 +2596,7 @@ msgstr "循環週期"
msgid "Introducing Cycle Analytics"
msgstr "週期分æžç°¡ä»‹"
-msgid "Issue board focus mode"
+msgid "Issue Board"
msgstr ""
msgid "Issue events"
@@ -2386,9 +2605,6 @@ msgstr "議題事件 (issue event)"
msgid "IssueBoards|Board"
msgstr ""
-msgid "IssueBoards|Boards"
-msgstr ""
-
msgid "Issues"
msgstr ""
@@ -2401,6 +2617,12 @@ msgstr ""
msgid "January"
msgstr ""
+msgid "Job"
+msgstr ""
+
+msgid "Job has been erased"
+msgstr ""
+
msgid "Jobs"
msgstr ""
@@ -2443,6 +2665,9 @@ msgstr ""
msgid "Kubernetes service integration has been deprecated. %{deprecated_message_content} your Kubernetes clusters using the new <a href=\"%{url}\"/>Kubernetes Clusters</a> page"
msgstr ""
+msgid "LFS"
+msgstr ""
+
msgid "LFSStatus|Disabled"
msgstr "åœç”¨"
@@ -2452,6 +2677,9 @@ msgstr "啟用"
msgid "Label"
msgstr ""
+msgid "Label actions dropdown"
+msgstr ""
+
msgid "LabelSelect|%{firstLabelName} +%{remainingLabelCount} more"
msgstr ""
@@ -2467,6 +2695,9 @@ msgstr ""
msgid "Labels can be applied to issues and merge requests to categorize them."
msgstr ""
+msgid "Labels can be applied to issues and merge requests."
+msgstr ""
+
msgid "Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>"
msgstr ""
@@ -2501,6 +2732,9 @@ msgstr "您推é€äº†"
msgid "LastPushEvent|at"
msgstr "在"
+msgid "Latest changes"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -2525,9 +2759,6 @@ msgstr "退出群組"
msgid "Leave project"
msgstr "退出項目"
-msgid "License"
-msgstr ""
-
msgid "List"
msgstr ""
@@ -2537,6 +2768,9 @@ msgstr ""
msgid "Loading the GitLab IDE..."
msgstr ""
+msgid "Loading..."
+msgstr ""
+
msgid "Lock"
msgstr ""
@@ -2546,21 +2780,18 @@ msgstr ""
msgid "Lock not found"
msgstr ""
-msgid "Locked"
+msgid "Lock to current projects"
msgstr ""
-msgid "Locked Files"
+msgid "Locked"
msgstr ""
-msgid "Locks give the ability to lock specific file or folder."
+msgid "Locked to current projects"
msgstr ""
msgid "Login"
msgstr ""
-msgid "Make everyone on your team more productive regardless of their location. GitLab Geo creates read-only mirrors of your GitLab instance so you can reduce the time it takes to clone and fetch large repos."
-msgstr ""
-
msgid "Manage all notifications"
msgstr ""
@@ -2573,16 +2804,16 @@ msgstr ""
msgid "Manage project labels"
msgstr ""
-msgid "Manage your group’s membership while adding another level of security with SAML."
-msgstr ""
-
msgid "Mar"
msgstr ""
msgid "March"
msgstr ""
-msgid "Mark done"
+msgid "Mark todo as done"
+msgstr ""
+
+msgid "Markdown enabled"
msgstr ""
msgid "Maximum git storage failures"
@@ -2597,7 +2828,7 @@ msgstr "䏭使•¸"
msgid "Members"
msgstr ""
-msgid "Members will be forwarded here when signing in to your group. Get this from your identity provider, where it can also be called \"SSO Service Location\", \"SAML Token Issuance Endpoint\", or \"SAML 2.0/W-Federation URL\"."
+msgid "Merge Request:"
msgstr ""
msgid "Merge Requests"
@@ -2609,91 +2840,46 @@ msgstr "åˆä½µäº‹ä»¶ (merge event)"
msgid "Merge request"
msgstr ""
-msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
-msgstr ""
-
-msgid "Merged"
-msgstr ""
-
-msgid "Messages"
-msgstr ""
-
-msgid "Metrics - Influx"
-msgstr ""
-
-msgid "Metrics - Prometheus"
-msgstr ""
-
-msgid "Metrics|Business"
-msgstr ""
-
-msgid "Metrics|Create metric"
-msgstr ""
-
-msgid "Metrics|Edit metric"
-msgstr ""
-
-msgid "Metrics|For grouping similar metrics"
-msgstr ""
-
-msgid "Metrics|Label of the chart's vertical axis. Usually the type of the unit being charted. The horizontal axis (X-axis) always represents time."
-msgstr ""
-
-msgid "Metrics|Legend label (optional)"
-msgstr ""
-
-msgid "Metrics|Must be a valid PromQL query."
-msgstr ""
-
-msgid "Metrics|Name"
-msgstr ""
-
-msgid "Metrics|New metric"
+msgid "Merge requests"
msgstr ""
-msgid "Metrics|Prometheus Query Documentation"
-msgstr ""
-
-msgid "Metrics|Query"
-msgstr ""
-
-msgid "Metrics|Response"
+msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
msgstr ""
-msgid "Metrics|System"
+msgid "MergeRequests|Resolve this discussion in a new issue"
msgstr ""
-msgid "Metrics|Type"
+msgid "MergeRequests|Saving the comment failed"
msgstr ""
-msgid "Metrics|Unit label"
+msgid "MergeRequests|Toggle comments for this file"
msgstr ""
-msgid "Metrics|Used as a title for the chart"
+msgid "MergeRequests|Updating discussions failed"
msgstr ""
-msgid "Metrics|Used if the query returns a single series. If it returns multiple series, their legend labels will be picked up from the response."
+msgid "MergeRequests|View file @ %{commitId}"
msgstr ""
-msgid "Metrics|Y-axis label"
+msgid "MergeRequests|View replaced file @ %{commitId}"
msgstr ""
-msgid "Metrics|e.g. HTTP requests"
+msgid "Merged"
msgstr ""
-msgid "Metrics|e.g. Requests/second"
+msgid "Messages"
msgstr ""
-msgid "Metrics|e.g. Throughput"
+msgid "Metrics - Influx"
msgstr ""
-msgid "Metrics|e.g. rate(http_requests_total[5m])"
+msgid "Metrics - Prometheus"
msgstr ""
-msgid "Metrics|e.g. req/sec"
+msgid "Milestone"
msgstr ""
-msgid "Milestone"
+msgid "Milestones"
msgstr ""
msgid "Milestones|Delete milestone"
@@ -2714,9 +2900,6 @@ msgstr ""
msgid "Milestones|Promote Milestone"
msgstr ""
-msgid "Milestones|This action cannot be reversed."
-msgstr ""
-
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "添加壹個 SSH 公鑰"
@@ -2729,7 +2912,7 @@ msgstr ""
msgid "Monitoring"
msgstr ""
-msgid "More info"
+msgid "More actions"
msgstr ""
msgid "More information"
@@ -2744,12 +2927,30 @@ msgstr ""
msgid "Move issue"
msgstr ""
-msgid "Multiple issue boards"
+msgid "Name"
msgstr ""
msgid "Name new label"
msgstr ""
+msgid "Name your individual key via a title"
+msgstr ""
+
+msgid "Nav|Help"
+msgstr ""
+
+msgid "Nav|Home"
+msgstr ""
+
+msgid "Nav|Sign In / Register"
+msgstr ""
+
+msgid "Nav|Sign out and sign in with a different account"
+msgstr ""
+
+msgid "New Identity"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "新建議題"
@@ -2760,6 +2961,9 @@ msgstr ""
msgid "New Kubernetes cluster"
msgstr ""
+msgid "New Label"
+msgstr ""
+
msgid "New Pipeline Schedule"
msgstr "å‰µå»ºæµæ°´ç·šè¨ˆåŠƒ"
@@ -2772,15 +2976,15 @@ msgstr ""
msgid "New directory"
msgstr "新增目錄"
-msgid "New epic"
-msgstr ""
-
msgid "New file"
msgstr "新增文件"
msgid "New group"
msgstr ""
+msgid "New identity"
+msgstr ""
+
msgid "New issue"
msgstr "新議題"
@@ -2790,6 +2994,9 @@ msgstr ""
msgid "New merge request"
msgstr "新增åˆä½µè«‹æ±‚"
+msgid "New pipelines will cancel older, pending pipelines on the same branch"
+msgstr ""
+
msgid "New project"
msgstr ""
@@ -2805,7 +3012,7 @@ msgstr ""
msgid "New tag"
msgstr "新增標籤"
-msgid "No Label"
+msgid "No"
msgstr ""
msgid "No assignee"
@@ -2826,7 +3033,16 @@ msgstr ""
msgid "No file chosen"
msgstr ""
-msgid "No labels created yet."
+msgid "No files found"
+msgstr ""
+
+msgid "No files found."
+msgstr ""
+
+msgid "No merge requests found"
+msgstr ""
+
+msgid "No messages were logged"
msgstr ""
msgid "No repository"
@@ -2859,15 +3075,9 @@ msgstr "數據ä¸è¶³"
msgid "Note that the master branch is automatically protected. %{link_to_protected_branches}"
msgstr ""
-msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow connecting repositories without generating a Personal Access Token."
-msgstr ""
-
msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
msgstr ""
-msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow connecting repositories without generating a Personal Access Token."
-msgstr ""
-
msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
msgstr ""
@@ -2943,9 +3153,6 @@ msgstr ""
msgid "Number of access attempts"
msgstr ""
-msgid "OK"
-msgstr ""
-
msgid "Oct"
msgstr ""
@@ -2955,19 +3162,16 @@ msgstr ""
msgid "OfSearchInADropdown|Filter"
msgstr "篩é¸"
-msgid "Once imported, repositories can be mirrored over SSH. Read more %{ssh_link}"
-msgstr ""
-
msgid "Online IDE integration settings."
msgstr ""
-msgid "Only project members can comment."
+msgid "Only comments from the following commit are shown below"
msgstr ""
-msgid "Open"
+msgid "Only project members can comment."
msgstr ""
-msgid "Opened"
+msgid "Open in Xcode"
msgstr ""
msgid "OpenedNDaysAgo|Opened"
@@ -2976,9 +3180,18 @@ msgstr "é–‹å§‹æ–¼"
msgid "Opens in a new window"
msgstr ""
+msgid "Operations"
+msgstr ""
+
msgid "Options"
msgstr "æ“作"
+msgid "Or you can choose one of the suggested colors below"
+msgstr ""
+
+msgid "Other Labels"
+msgstr ""
+
msgid "Otherwise it is recommended you start with one of the options below."
msgstr ""
@@ -3012,12 +3225,27 @@ msgstr ""
msgid "Password"
msgstr ""
+msgid "Paste your public SSH key, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'. Don't use your private SSH key."
+msgstr ""
+
+msgid "Pause"
+msgstr ""
+
msgid "Pending"
msgstr ""
+msgid "Per job. If a job passes this threshold, it will be marked as failed"
+msgstr ""
+
+msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgstr ""
+
msgid "Performance optimization"
msgstr ""
+msgid "Permissions"
+msgstr ""
+
msgid "Personal Access Token"
msgstr ""
@@ -3033,7 +3261,7 @@ msgstr "æµæ°´ç·šè¨ˆåŠƒ"
msgid "Pipeline Schedules"
msgstr "æµæ°´ç·šè¨ˆåŠƒ"
-msgid "Pipeline quota"
+msgid "Pipeline triggers"
msgstr ""
msgid "PipelineCharts|Failed:"
@@ -3132,10 +3360,22 @@ msgstr ""
msgid "Pipelines|This project is not currently set up to run pipelines."
msgstr ""
-msgid "Pipeline|Retry pipeline"
+msgid "Pipeline|Create for"
+msgstr ""
+
+msgid "Pipeline|Create pipeline"
+msgstr ""
+
+msgid "Pipeline|Existing branch name or tag"
+msgstr ""
+
+msgid "Pipeline|Run Pipeline"
msgstr ""
-msgid "Pipeline|Retry pipeline #%{pipelineId}?"
+msgid "Pipeline|Search branches"
+msgstr ""
+
+msgid "Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default."
msgstr ""
msgid "Pipeline|Stop pipeline"
@@ -3144,7 +3384,7 @@ msgstr ""
msgid "Pipeline|Stop pipeline #%{pipelineId}?"
msgstr ""
-msgid "Pipeline|You’re about to retry pipeline %{pipelineId}."
+msgid "Pipeline|Variables"
msgstr ""
msgid "Pipeline|You’re about to stop pipeline %{pipelineId}."
@@ -3162,19 +3402,25 @@ msgstr "於階段"
msgid "Pipeline|with stages"
msgstr "於階段"
+msgid "Plain diff"
+msgstr ""
+
msgid "PlantUML"
msgstr ""
msgid "Play"
msgstr ""
-msgid "Please <a href=%{link_to_billing} target=\"_blank\" rel=\"noopener noreferrer\">enable billing for one of your projects to be able to create a Kubernetes cluster</a>, then try again."
+msgid "Please accept the Terms of Service before continuing."
+msgstr ""
+
+msgid "Please select at least one filter to see results"
msgstr ""
msgid "Please solve the reCAPTCHA"
msgstr ""
-msgid "Please wait while we connect to your repository. Refresh at will."
+msgid "Please try again"
msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
@@ -3183,7 +3429,19 @@ msgstr ""
msgid "Preferences"
msgstr ""
-msgid "Primary"
+msgid "Preferences|Navigation theme"
+msgstr ""
+
+msgid "Prioritize"
+msgstr ""
+
+msgid "Prioritize label"
+msgstr ""
+
+msgid "Prioritized Labels"
+msgstr ""
+
+msgid "Prioritized label"
msgstr ""
msgid "Private - Project access must be granted explicitly to each user."
@@ -3201,6 +3459,12 @@ msgstr ""
msgid "Profiles|Account scheduled for removal."
msgstr ""
+msgid "Profiles|Change username"
+msgstr ""
+
+msgid "Profiles|Current path: %{path}"
+msgstr ""
+
msgid "Profiles|Delete Account"
msgstr ""
@@ -3219,9 +3483,21 @@ msgstr ""
msgid "Profiles|Invalid username"
msgstr ""
+msgid "Profiles|Path"
+msgstr ""
+
msgid "Profiles|Type your %{confirmationValue} to confirm:"
msgstr ""
+msgid "Profiles|Update username"
+msgstr ""
+
+msgid "Profiles|Username change failed - %{message}"
+msgstr ""
+
+msgid "Profiles|Username successfully changed"
+msgstr ""
+
msgid "Profiles|You don't have access to delete this user."
msgstr ""
@@ -3240,6 +3516,12 @@ msgstr ""
msgid "Programming languages used in this repository"
msgstr ""
+msgid "Progress"
+msgstr ""
+
+msgid "Project"
+msgstr ""
+
msgid "Project '%{project_name}' is in the process of being deleted."
msgstr ""
@@ -3252,6 +3534,9 @@ msgstr "é …ç›® '%{project_name}' 已創建æˆåŠŸã€‚"
msgid "Project '%{project_name}' was successfully updated."
msgstr "é …ç›® '%{project_name}' 已更新完æˆã€‚"
+msgid "Project Badges"
+msgstr ""
+
msgid "Project access must be granted explicitly to each user."
msgstr "é …ç›®è¨ªå•æ¬Šé™å¿…須明確授權給æ¯å€‹ç”¨æˆ¶ã€‚"
@@ -3279,30 +3564,6 @@ msgstr "é …ç›®å°Žå‡ºå·²é–‹å§‹ã€‚ä¸‹è¼‰éˆæŽ¥å°‡é€šéŽé›»å­éƒµä»¶ç™¼é€ã€‚"
msgid "ProjectActivityRSS|Subscribe"
msgstr "訂閱"
-msgid "ProjectCreationLevel|Allowed to create projects"
-msgstr ""
-
-msgid "ProjectCreationLevel|Default project creation protection"
-msgstr ""
-
-msgid "ProjectCreationLevel|Developers + Masters"
-msgstr ""
-
-msgid "ProjectCreationLevel|Masters"
-msgstr ""
-
-msgid "ProjectCreationLevel|No one"
-msgstr ""
-
-msgid "ProjectFeature|Disabled"
-msgstr "åœç”¨"
-
-msgid "ProjectFeature|Everyone with access"
-msgstr "任何人都å¯è¨ªå•"
-
-msgid "ProjectFeature|Only team members"
-msgstr "åªé™åœ˜éšŠæˆå“¡"
-
msgid "ProjectFileTree|Name"
msgstr "å稱"
@@ -3312,27 +3573,6 @@ msgstr "從未"
msgid "ProjectLifecycle|Stage"
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 ""
@@ -3357,6 +3597,9 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr ""
+msgid "PrometheusDashboard|Time"
+msgstr ""
+
msgid "PrometheusService|%{exporters} with %{metrics} were found"
msgstr ""
@@ -3378,18 +3621,9 @@ msgstr ""
msgid "PrometheusService|Common metrics"
msgstr ""
-msgid "PrometheusService|Common metrics are automatically monitored based on a library of metrics from popular exporters."
-msgstr ""
-
-msgid "PrometheusService|Custom metrics"
-msgstr ""
-
msgid "PrometheusService|Finding and configuring metrics..."
msgstr ""
-msgid "PrometheusService|Finding custom metrics..."
-msgstr ""
-
msgid "PrometheusService|Install Prometheus on clusters"
msgstr ""
@@ -3402,13 +3636,13 @@ msgstr ""
msgid "PrometheusService|Metrics"
msgstr ""
-msgid "PrometheusService|Missing environment variable"
+msgid "PrometheusService|Metrics are automatically configured and monitored based on a library of metrics from popular exporters."
msgstr ""
-msgid "PrometheusService|More information"
+msgid "PrometheusService|Missing environment variable"
msgstr ""
-msgid "PrometheusService|New metric"
+msgid "PrometheusService|More information"
msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
@@ -3417,9 +3651,6 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
-msgstr ""
-
msgid "PrometheusService|Time-series monitoring service"
msgstr ""
@@ -3435,22 +3666,28 @@ msgstr ""
msgid "Promote"
msgstr ""
-msgid "Promote to Group Label"
+msgid "Promote these project milestones into a group milestone."
msgstr ""
msgid "Promote to Group Milestone"
msgstr ""
+msgid "Promote to group label"
+msgstr ""
+
msgid "Protip:"
msgstr ""
+msgid "Provider"
+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"
+msgid "Public pipelines"
msgstr ""
msgid "Push events"
@@ -3462,10 +3699,10 @@ msgstr ""
msgid "Push to create a project"
msgstr ""
-msgid "PushRule|Committer restriction"
+msgid "Quick actions can be used in the issues description and comment boxes."
msgstr ""
-msgid "Quick actions can be used in the issues description and comment boxes."
+msgid "Re-deploy"
msgstr ""
msgid "Read more"
@@ -3477,18 +3714,18 @@ msgstr "自述文件"
msgid "Real-time features"
msgstr ""
-msgid "RefSwitcher|Branches"
-msgstr "分支"
-
-msgid "RefSwitcher|Tags"
-msgstr "標籤"
-
msgid "Reference:"
msgstr ""
msgid "Register / Sign In"
msgstr ""
+msgid "Register and see your runners for this group."
+msgstr ""
+
+msgid "Register and see your runners for this project."
+msgstr ""
+
msgid "Registry"
msgstr ""
@@ -3519,28 +3756,28 @@ msgstr "ç¨å¾Œæé†’"
msgid "Remove"
msgstr ""
-msgid "Remove avatar"
+msgid "Remove Runner"
msgstr ""
-msgid "Remove project"
-msgstr "刪除項目"
-
-msgid "Repair authentication"
+msgid "Remove avatar"
msgstr ""
-msgid "Repo by URL"
+msgid "Remove priority"
msgstr ""
+msgid "Remove project"
+msgstr "刪除項目"
+
msgid "Repository"
msgstr "存儲庫"
-msgid "Repository has no locks."
+msgid "Repository Settings"
msgstr ""
msgid "Repository maintenance"
msgstr ""
-msgid "Repository mirror settings"
+msgid "Repository mirror"
msgstr ""
msgid "Repository storage"
@@ -3549,6 +3786,9 @@ msgstr ""
msgid "Request Access"
msgstr "申請權é™"
+msgid "Require all users to accept Terms of Service and Privacy Policy when they access GitLab."
+msgstr ""
+
msgid "Reset git storage health information"
msgstr "é‡ç½® Git 存儲的å¥åº·ä¿¡æ¯"
@@ -3558,10 +3798,25 @@ msgstr "é‡ç½®å¥åº·æª¢æŸ¥è¨ªå•令牌"
msgid "Reset runners registration token"
msgstr "é‡ç½® Runner 註冊令牌"
+msgid "Resolve all discussions in new issue"
+msgstr ""
+
+msgid "Resolve conflicts on source branch"
+msgstr ""
+
msgid "Resolve discussion"
msgstr ""
-msgid "Response"
+msgid "Resume"
+msgstr ""
+
+msgid "Retry"
+msgstr ""
+
+msgid "Retry this job"
+msgstr ""
+
+msgid "Retry verification"
msgstr ""
msgid "Reveal value"
@@ -3574,7 +3829,7 @@ msgstr "還原此æäº¤"
msgid "Revert this merge request"
msgstr "還原此åˆä½µè«‹æ±‚"
-msgid "Review the process for configuring service providers in your identity provider — in this case, GitLab is the \"service provider\" or \"relying party\"."
+msgid "Review"
msgstr ""
msgid "Reviewing"
@@ -3583,28 +3838,31 @@ msgstr ""
msgid "Reviewing (merge request !%{mergeRequestId})"
msgstr ""
-msgid "Roadmap"
+msgid "Rollback"
msgstr ""
-msgid "Run CI/CD pipelines for external repositories"
+msgid "Runner token"
msgstr ""
msgid "Runners"
msgstr ""
-msgid "Running"
+msgid "Runners API"
msgstr ""
-msgid "SAML Single Sign On"
+msgid "Runners can be placed on separate users, servers, and even on your local machine."
msgstr ""
-msgid "SAML Single Sign On Settings"
+msgid "Running"
msgstr ""
-msgid "SHA1 fingerprint of the SAML token signing certificate. Get this from your identity provider, where it can also be called \"Thumbprint\"."
+msgid "SSH Keys"
msgstr ""
-msgid "SSH Keys"
+msgid "SSL Verification"
+msgstr ""
+
+msgid "Save"
msgstr ""
msgid "Save changes"
@@ -3628,15 +3886,30 @@ msgstr ""
msgid "Scheduling Pipelines"
msgstr "æµæ°´ç·šè¨ˆåŠƒ"
-msgid "Scoped issue boards"
+msgid "Scroll to bottom"
+msgstr ""
+
+msgid "Scroll to top"
msgstr ""
msgid "Search"
msgstr ""
+msgid "Search branches"
+msgstr ""
+
msgid "Search branches and tags"
msgstr "æœç´¢åˆ†æ”¯å’Œæ¨™ç±¤"
+msgid "Search files"
+msgstr ""
+
+msgid "Search for projects, issues, etc."
+msgstr ""
+
+msgid "Search merge requests"
+msgstr ""
+
msgid "Search milestones"
msgstr ""
@@ -3652,15 +3925,15 @@ msgstr ""
msgid "Seconds to wait for a storage access attempt"
msgstr ""
-msgid "Secret variables"
-msgstr ""
-
-msgid "Security report"
+msgid "Select"
msgstr ""
msgid "Select Archive Format"
msgstr "鏿“‡ä¸‹è¼‰æ ¼å¼"
+msgid "Select a namespace to fork the project"
+msgstr ""
+
msgid "Select a timezone"
msgstr "鏿“‡æ™‚å€"
@@ -3673,12 +3946,21 @@ msgstr ""
msgid "Select branch/tag"
msgstr ""
-msgid "Select target branch"
-msgstr "鏿“‡ç›®æ¨™åˆ†æ”¯"
+msgid "Select project"
+msgstr ""
+
+msgid "Select project and zone to choose machine type"
+msgstr ""
-msgid "Selective synchronization"
+msgid "Select project to choose zone"
msgstr ""
+msgid "Select source branch"
+msgstr ""
+
+msgid "Select target branch"
+msgstr "鏿“‡ç›®æ¨™åˆ†æ”¯"
+
msgid "Send email"
msgstr ""
@@ -3694,9 +3976,6 @@ msgstr ""
msgid "Service Templates"
msgstr ""
-msgid "Service URL"
-msgstr ""
-
msgid "Session expiration, projects limit and attachment size."
msgstr ""
@@ -3721,9 +4000,6 @@ msgstr ""
msgid "Set up Koding"
msgstr "設置 Koding"
-msgid "Set up assertions/attributes/claims (email, first_name, last_name) and NameID according to %{docsLinkStart}the documentation %{icon}%{docsLinkEnd}"
-msgstr ""
-
msgid "SetPasswordToCloneLink|set a password"
msgstr "設置密碼"
@@ -3733,19 +4009,22 @@ msgstr ""
msgid "Setup a specific Runner automatically"
msgstr ""
-msgid "Share the <strong>%{sso_label}</strong> with members so they can sign in to your group through your identity provider"
+msgid "Share"
msgstr ""
-msgid "SharedRunnersMinutesSettings|By resetting the pipeline minutes for this namespace, the currently used minutes will be set to zero."
+msgid "Shared Runners"
msgstr ""
-msgid "SharedRunnersMinutesSettings|Reset pipeline minutes"
+msgid "Show command"
msgstr ""
-msgid "SharedRunnersMinutesSettings|Reset used pipeline minutes"
+msgid "Show complete raw log"
msgstr ""
-msgid "Show command"
+msgid "Show latest version"
+msgstr ""
+
+msgid "Show latest version of the diff"
msgstr ""
msgid "Show parent pages"
@@ -3754,20 +4033,17 @@ msgstr ""
msgid "Show parent subgroups"
msgstr ""
+msgid "Show whitespace changes"
+msgstr ""
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "顯示 %d 個事件"
-msgid "Sidebar|Change weight"
-msgstr ""
-
-msgid "Sidebar|No"
-msgstr ""
-
-msgid "Sidebar|None"
+msgid "Side-by-side"
msgstr ""
-msgid "Sidebar|Weight"
+msgid "Sign out"
msgstr ""
msgid "Sign-in restrictions"
@@ -3779,7 +4055,7 @@ msgstr ""
msgid "Size and domain settings for static websites"
msgstr ""
-msgid "Slack application"
+msgid "Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job"
msgstr ""
msgid "Snippets"
@@ -3791,13 +4067,13 @@ msgstr ""
msgid "Something went wrong on our end."
msgstr ""
-msgid "Something went wrong when toggling the button"
+msgid "Something went wrong on our end. Please try again!"
msgstr ""
-msgid "Something went wrong while fetching Dependency Scanning."
+msgid "Something went wrong when toggling the button"
msgstr ""
-msgid "Something went wrong while fetching SAST."
+msgid "Something went wrong while closing the %{issuable}. Please try again later"
msgstr ""
msgid "Something went wrong while fetching the projects."
@@ -3806,6 +4082,12 @@ msgstr ""
msgid "Something went wrong while fetching the registry list."
msgstr ""
+msgid "Something went wrong while reopening the %{issuable}. Please try again later"
+msgstr ""
+
+msgid "Something went wrong while resolving this discussion. Please try again."
+msgstr ""
+
msgid "Something went wrong. Please try again."
msgstr ""
@@ -3851,9 +4133,6 @@ msgstr ""
msgid "SortOptions|Least popular"
msgstr ""
-msgid "SortOptions|Less weight"
-msgstr ""
-
msgid "SortOptions|Milestone"
msgstr ""
@@ -3863,9 +4142,6 @@ msgstr ""
msgid "SortOptions|Milestone due soon"
msgstr ""
-msgid "SortOptions|More weight"
-msgstr ""
-
msgid "SortOptions|Most popular"
msgstr ""
@@ -3905,9 +4181,6 @@ msgstr ""
msgid "SortOptions|Start soon"
msgstr ""
-msgid "SortOptions|Weight"
-msgstr ""
-
msgid "Source"
msgstr ""
@@ -3926,9 +4199,36 @@ msgstr ""
msgid "Spam and Anti-bot Protection"
msgstr ""
+msgid "Specific Runners"
+msgstr ""
+
msgid "Specify the following URL during the Runner setup:"
msgstr "在 Runner 設置時指定以下 URL:"
+msgid "Squash commits"
+msgstr ""
+
+msgid "Stage"
+msgstr ""
+
+msgid "Stage & Commit"
+msgstr ""
+
+msgid "Stage all changes"
+msgstr ""
+
+msgid "Stage changes"
+msgstr ""
+
+msgid "Staged"
+msgstr ""
+
+msgid "Staged %{type}"
+msgstr ""
+
+msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
+msgstr ""
+
msgid "StarProject|Star"
msgstr "星標"
@@ -3950,12 +4250,15 @@ msgstr "é‹ä½œ Runner!"
msgid "Started"
msgstr ""
-msgid "State your message to activate"
+msgid "Starts at (UTC)"
msgstr ""
msgid "Status"
msgstr ""
+msgid "Stop this environment"
+msgstr ""
+
msgid "Stopped"
msgstr ""
@@ -3965,16 +4268,19 @@ msgstr ""
msgid "Subgroups"
msgstr ""
-msgid "Switch branch/tag"
-msgstr "切æ›åˆ†æ”¯/標籤"
+msgid "Subscribe"
+msgstr ""
-msgid "System"
+msgid "Subscribe at group level"
msgstr ""
-msgid "System Hooks"
+msgid "Subscribe at project level"
msgstr ""
-msgid "System header and footer:"
+msgid "Switch branch/tag"
+msgstr "切æ›åˆ†æ”¯/標籤"
+
+msgid "System Hooks"
msgstr ""
msgid "Tag (%{tag_count})"
@@ -3984,6 +4290,9 @@ msgstr[0] ""
msgid "Tags"
msgstr "標籤"
+msgid "Tags:"
+msgstr ""
+
msgid "TagsPage|Browse commits"
msgstr ""
@@ -4047,7 +4356,7 @@ msgstr ""
msgid "TagsPage|Use git tag command to add a new one:"
msgstr ""
-msgid "TagsPage|Write your release notes or drag files here..."
+msgid "TagsPage|Write your release notes or drag files here…"
msgstr ""
msgid "TagsPage|protected"
@@ -4062,19 +4371,19 @@ msgstr ""
msgid "Team"
msgstr "團隊"
-msgid "Thanks! Don't show me this again"
+msgid "Terms of Service Agreement and Privacy Policy"
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."
+msgid "Terms of Service and Privacy Policy"
msgstr ""
-msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project"
+msgid "Test coverage parsing"
msgstr ""
-msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
+msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project"
msgstr ""
-msgid "The X509 Certificate to use when mutual TLS is required to communicate with the external authorization service. If left blank, the server certificate is still validated when accessing over HTTPS."
+msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
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."
@@ -4083,9 +4392,6 @@ msgstr "編碼階段概述了從第壹次æäº¤åˆ°å‰µå»ºåˆä½µè«‹æ±‚的時間。
msgid "The collection of events added to the data gathered for that stage."
msgstr "與該階段相關的事件。"
-msgid "The connection will time out after %{timeout}. For repositories that take longer, use a clone/push combination."
-msgstr ""
-
msgid "The fork relationship has been removed."
msgstr "派生關係已被刪除。"
@@ -4104,7 +4410,7 @@ 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 passphrase required to decrypt the private key. This is optional and the value is encrypted at rest."
+msgid "The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>"
msgstr ""
msgid "The phase of the development lifecycle."
@@ -4113,9 +4419,6 @@ msgstr "項目生命週期中的å„個階段。"
msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit."
msgstr "計劃階段概述了從議題添加到日程到推é€é¦–次æäº¤çš„æ™‚é–“ã€‚ç•¶é¦–æ¬¡æŽ¨é€æäº¤å¾Œï¼Œæ•¸æ“šå°‡è‡ªå‹•æ·»åŠ åˆ°æ­¤è™•ã€‚"
-msgid "The private key to use when a client certificate is provided. This value is encrypted at rest."
-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 "生產階段概述了從創建議題到將代碼部署到生產環境的時間。當完æˆå®Œæ•´çš„æƒ³æ³•到部署生產,數據將自動添加到此處。"
@@ -4137,7 +4440,7 @@ msgstr ""
msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request."
msgstr "評審階段概述了從創建åˆä½µè«‹æ±‚到åˆä½µçš„æ™‚間。當創建第壹個åˆä½µè«‹æ±‚後,數據將自動添加到此處。"
-msgid "The roadmap shows the progress of your epics along a timeline"
+msgid "The secure token used by the Runner to checkout the project"
msgstr ""
msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time."
@@ -4164,13 +4467,19 @@ msgstr "䏭使•¸æ˜¯å£¹å€‹æ•¸åˆ—中最中間的值。例如在 3ã€5ã€9 之間ï
msgid "There are no issues to show"
msgstr ""
+msgid "There are no labels yet"
+msgstr ""
+
msgid "There are no merge requests to show"
msgstr ""
msgid "There are problems accessing Git storage: "
msgstr "è¨ªå• Git 存儲時出ç¾å•題:"
-msgid "There was an error loading results"
+msgid "There was an error loading jobs"
+msgstr ""
+
+msgid "There was an error loading latest pipeline"
msgstr ""
msgid "There was an error loading users activity calendar."
@@ -4191,12 +4500,21 @@ msgstr ""
msgid "There was an error when unsubscribing from this label."
msgstr ""
-msgid "This board\\'s scope is reduced"
+msgid "They can be managed using the %{link}."
+msgstr ""
+
+msgid "This GitLab instance does not provide any shared Runners yet. Instance administrators can register shared Runners in the admin area."
+msgstr ""
+
+msgid "This diff is collapsed."
msgstr ""
msgid "This directory"
msgstr ""
+msgid "This group does not provide any group Runners yet."
+msgstr ""
+
msgid "This is a confidential issue."
msgstr ""
@@ -4218,6 +4536,15 @@ msgstr ""
msgid "This job depends on upstream jobs that need to succeed in order for this job to be triggered"
msgstr ""
+msgid "This job does not have a trace."
+msgstr ""
+
+msgid "This job has been canceled"
+msgstr ""
+
+msgid "This job has been skipped"
+msgstr ""
+
msgid "This job has not been triggered yet"
msgstr ""
@@ -4236,19 +4563,28 @@ msgstr "åœ¨å‰µå»ºå£¹å€‹ç©ºçš„å­˜å„²åº«æˆ–å°Žå…¥ç¾æœ‰å­˜å„²åº«ä¹‹å‰ï¼Œæ‚¨å°‡ç„¡
msgid "This merge request is locked."
msgstr ""
+msgid "This option is disabled while you still have unstaged changes"
+msgstr ""
+
msgid "This page is unavailable because you are not allowed to read information across multiple projects."
msgstr ""
+msgid "This page will be removed in a future release."
+msgstr ""
+
msgid "This project"
msgstr ""
+msgid "This project does not belong to a group and can therefore not make use of group Runners."
+msgstr ""
+
msgid "This repository"
msgstr ""
-msgid "This will delete the custom metric, Are you sure?"
+msgid "This source diff could not be displayed because it is too large."
msgstr ""
-msgid "Those emails automatically become issues (with the comments becoming the email conversation) listed here."
+msgid "This user has no identities"
msgstr ""
msgid "Time before an issue gets scheduled"
@@ -4260,10 +4596,10 @@ msgstr "開始進行編碼å‰çš„æ™‚é–“"
msgid "Time between merge request creation and merge/close"
msgstr "從創建åˆä½µè«‹æ±‚到被åˆä½µæˆ–關閉的時間"
-msgid "Time between updates and capacity settings."
+msgid "Time remaining"
msgstr ""
-msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
+msgid "Time spent"
msgstr ""
msgid "Time tracking"
@@ -4287,6 +4623,9 @@ msgstr " %s 天å‰"
msgid "Timeago|%s days remaining"
msgstr "剩餘 %s 天"
+msgid "Timeago|%s hours ago"
+msgstr ""
+
msgid "Timeago|%s hours remaining"
msgstr "剩餘 %s å°æ™‚"
@@ -4302,6 +4641,9 @@ msgstr " %s 個月å‰"
msgid "Timeago|%s months remaining"
msgstr "剩餘 %s 月"
+msgid "Timeago|%s seconds ago"
+msgstr ""
+
msgid "Timeago|%s seconds remaining"
msgstr "剩餘 %s 秒"
@@ -4317,48 +4659,45 @@ msgstr " %s å¹´å‰"
msgid "Timeago|%s years remaining"
msgstr "剩餘 %s 年"
+msgid "Timeago|1 day ago"
+msgstr ""
+
msgid "Timeago|1 day remaining"
msgstr "剩餘 1 天"
+msgid "Timeago|1 hour ago"
+msgstr ""
+
msgid "Timeago|1 hour remaining"
msgstr "剩餘 1 å°æ™‚"
+msgid "Timeago|1 minute ago"
+msgstr ""
+
msgid "Timeago|1 minute remaining"
msgstr "剩餘 1 分é˜"
+msgid "Timeago|1 month ago"
+msgstr ""
+
msgid "Timeago|1 month remaining"
msgstr "剩餘 1 個月"
+msgid "Timeago|1 week ago"
+msgstr ""
+
msgid "Timeago|1 week remaining"
msgstr "剩餘 1 星期"
+msgid "Timeago|1 year ago"
+msgstr ""
+
msgid "Timeago|1 year remaining"
msgstr "剩餘 1 年"
msgid "Timeago|Past due"
msgstr "逾期"
-msgid "Timeago|a day ago"
-msgstr " 1 天å‰"
-
-msgid "Timeago|a month ago"
-msgstr " 1 個月å‰"
-
-msgid "Timeago|a week ago"
-msgstr " 1 星期å‰"
-
-msgid "Timeago|a year ago"
-msgstr " 1 å¹´å‰"
-
-msgid "Timeago|about %s hours ago"
-msgstr "ç´„ %s å°æ™‚å‰"
-
-msgid "Timeago|about a minute ago"
-msgstr "ç´„ 1 分é˜å‰"
-
-msgid "Timeago|about an hour ago"
-msgstr "ç´„ 1 å°æ™‚å‰"
-
msgid "Timeago|in %s days"
msgstr " %s 天後"
@@ -4398,11 +4737,14 @@ msgstr " 1 星期後"
msgid "Timeago|in 1 year"
msgstr " 1 年後"
-msgid "Timeago|in a while"
+msgid "Timeago|just now"
+msgstr ""
+
+msgid "Timeago|right now"
msgstr ""
-msgid "Timeago|less than a minute ago"
-msgstr "ä¸åˆ° 1 分é˜å‰"
+msgid "Timeout"
+msgstr ""
msgid "Time|hr"
msgid_plural "Time|hrs"
@@ -4418,19 +4760,10 @@ msgstr "ç§’"
msgid "Tip:"
msgstr ""
-msgid "Title"
-msgstr ""
-
msgid "To GitLab"
msgstr ""
-msgid "To connect GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to connect."
-msgstr ""
-
-msgid "To connect GitHub repositories, you first need to authorize GitLab to access the list of your GitHub repositories:"
-msgstr ""
-
-msgid "To connect an SVN repository, check out %{svn_link}."
+msgid "To add an SSH key you need to %{generate_link_start}generate one%{link_end} or use an %{existing_link_start}existing key%{link_end}."
msgstr ""
msgid "To import GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
@@ -4442,19 +4775,19 @@ msgstr ""
msgid "To import an SVN repository, check out %{svn_link}."
msgstr ""
-msgid "To only use CI/CD features for an external repository, choose <strong>CI/CD for external repo</strong>."
+msgid "To start serving your jobs you can add Runners to your group"
msgstr ""
-msgid "To set up SAML authentication for your group through an identity provider like Azure, Okta, Onelogin, Ping Identity, or your custom SAML 2.0 provider:"
+msgid "To validate your GitLab CI configurations, go to 'CI/CD → Pipelines' inside your project, and click on the 'CI Lint' button."
msgstr ""
-msgid "To validate your GitLab CI configurations, go to 'CI/CD → Pipelines' inside your project, and click on the 'CI Lint' button."
+msgid "Todo"
msgstr ""
-msgid "To view the roadmap, add a planned start or finish date to one of your epics in this group or its subgroups. Only epics in the past 3 months and the next 3 months are shown."
+msgid "Toggle Sidebar"
msgstr ""
-msgid "Todo"
+msgid "Toggle discussion"
msgstr ""
msgid "Toggle sidebar"
@@ -4466,6 +4799,9 @@ msgstr ""
msgid "ToggleButton|Toggle Status: ON"
msgstr ""
+msgid "Too many changes to show."
+msgstr ""
+
msgid "Total Time"
msgstr "總時間"
@@ -4475,22 +4811,19 @@ msgstr "所有æäº¤å’Œåˆä½µçš„總測試時間"
msgid "Total: %{total}"
msgstr ""
-msgid "Track activity with Contribution Analytics."
-msgstr ""
-
-msgid "Track groups of issues that share a theme, across projects and milestones"
-msgstr ""
-
msgid "Track time with quick actions"
msgstr ""
msgid "Trigger this manual action"
msgstr ""
-msgid "Turn on Service Desk"
+msgid "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."
msgstr ""
-msgid "Unknown"
+msgid "Try again"
+msgstr ""
+
+msgid "Unable to load the diff. %{button_try_again}"
msgstr ""
msgid "Unlock"
@@ -4502,25 +4835,44 @@ msgstr ""
msgid "Unresolve discussion"
msgstr ""
+msgid "Unstage all changes"
+msgstr ""
+
+msgid "Unstage changes"
+msgstr ""
+
+msgid "Unstaged"
+msgstr ""
+
+msgid "Unstaged %{type}"
+msgstr ""
+
+msgid "Unstaged and staged %{type}"
+msgstr ""
+
msgid "Unstar"
msgstr "å–æ¶ˆæ˜Ÿæ¨™"
-msgid "Up to date"
+msgid "Unsubscribe"
msgstr ""
-msgid "Upgrade your plan to activate Advanced Global Search."
+msgid "Unsubscribe at group level"
msgstr ""
-msgid "Upgrade your plan to activate Contribution Analytics."
+msgid "Unsubscribe at project level"
msgstr ""
-msgid "Upgrade your plan to activate Group Webhooks."
+msgid "Unverified"
msgstr ""
-msgid "Upgrade your plan to activate Issue weight."
+msgid "Up to date"
msgstr ""
-msgid "Upgrade your plan to improve Issue boards."
+msgid "Update %{files}"
+msgid_plural "Update %{files} files"
+msgstr[0] ""
+
+msgid "Update your group name, description, avatar, and other general settings."
msgstr ""
msgid "Upload New File"
@@ -4541,7 +4893,7 @@ msgstr ""
msgid "Usage statistics"
msgstr ""
-msgid "Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab"
+msgid "Use group milestones to manage issues from multiple projects in the same milestone."
msgstr ""
msgid "Use the following registration token during setup:"
@@ -4550,10 +4902,13 @@ msgstr "在安è£éŽç¨‹ä¸­ä½¿ç”¨ä»¥ä¸‹è¨»å†Šä»¤ç‰Œï¼š"
msgid "Use your global notification setting"
msgstr "使用全局通知設置"
-msgid "Used by members to sign in to your group in GitLab"
+msgid "User and IP Rate Limits"
+msgstr ""
+
+msgid "Users"
msgstr ""
-msgid "User and IP Rate Limits"
+msgid "Variables"
msgstr ""
msgid "Variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. You can use variables for passwords, secret keys, or whatever you want."
@@ -4568,10 +4923,7 @@ msgstr ""
msgid "Various settings that affect GitLab performance."
msgstr ""
-msgid "View and edit lines"
-msgstr ""
-
-msgid "View epics list"
+msgid "Verified"
msgstr ""
msgid "View file @ "
@@ -4580,9 +4932,15 @@ msgstr ""
msgid "View group labels"
msgstr ""
+msgid "View jobs"
+msgstr ""
+
msgid "View labels"
msgstr ""
+msgid "View log"
+msgstr ""
+
msgid "View open merge request"
msgstr "查看開啟的åˆä¸¦è«‹æ±‚"
@@ -4610,9 +4968,6 @@ msgstr "未知"
msgid "Want to see the data? Please ask an administrator for access."
msgstr "權é™ä¸è¶³ã€‚如需查看相關數據,請å‘管ç†å“¡ç”³è«‹æ¬Šé™ã€‚"
-msgid "We could not verify that one of your projects on GCP has billing enabled. Please try again."
-msgstr ""
-
msgid "We don't have enough data to show this stage."
msgstr "該階段的數據ä¸è¶³ï¼Œç„¡æ³•顯示。"
@@ -4625,13 +4980,10 @@ msgstr ""
msgid "Web terminal"
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."
+msgid "When a runner is locked, it cannot be assigned to other projects"
msgstr ""
-msgid "Weight"
-msgstr ""
-
-msgid "When leaving the URL blank, classification labels can still be specified whitout disabling cross project features or performing external authorization checks."
+msgid "When enabled, users cannot use GitLab until the terms have been accepted."
msgstr ""
msgid "Wiki"
@@ -4658,7 +5010,31 @@ msgstr ""
msgid "WikiEdit|There is already a page with the same title in that path."
msgstr ""
-msgid "WikiEmptyPageError|You are not allowed to create wiki pages"
+msgid "WikiEmptyIssueMessage|Suggest wiki improvement"
+msgstr ""
+
+msgid "WikiEmptyIssueMessage|You must be a project member in order to add wiki pages. If you have suggestions for how to improve the wiki for this project, consider opening an issue in the %{issues_link}."
+msgstr ""
+
+msgid "WikiEmptyIssueMessage|issue tracker"
+msgstr ""
+
+msgid "WikiEmpty|A wiki is where you can store all the details about your project. This can include why you've created it, its principles, how to use it, and so on."
+msgstr ""
+
+msgid "WikiEmpty|Create your first page"
+msgstr ""
+
+msgid "WikiEmpty|Suggest wiki improvement"
+msgstr ""
+
+msgid "WikiEmpty|The wiki lets you write documentation for your project"
+msgstr ""
+
+msgid "WikiEmpty|This project has no wiki pages"
+msgstr ""
+
+msgid "WikiEmpty|You must be a project member in order to add wiki pages."
msgstr ""
msgid "WikiHistoricalPage|This is an old version of this page."
@@ -4694,6 +5070,12 @@ msgstr ""
msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?"
msgstr ""
+msgid "WikiPageConfirmDelete|Delete page"
+msgstr ""
+
+msgid "WikiPageConfirmDelete|Delete page %{pageTitle}?"
+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 ""
@@ -4709,7 +5091,7 @@ msgstr ""
msgid "WikiPage|Page slug"
msgstr ""
-msgid "WikiPage|Write your content or drag files here..."
+msgid "WikiPage|Write your content or drag files here…"
msgstr ""
msgid "Wiki|Create Page"
@@ -4721,9 +5103,6 @@ msgstr ""
msgid "Wiki|Edit Page"
msgstr ""
-msgid "Wiki|Empty page"
-msgstr ""
-
msgid "Wiki|More Pages"
msgstr ""
@@ -4742,13 +5121,10 @@ 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 "å–æ¶ˆæ¬Šé™ç”³è¯·"
-msgid "Write a commit message..."
+msgid "Yes"
msgstr ""
msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
@@ -4766,7 +5142,7 @@ msgstr ""
msgid "You are on a read-only GitLab instance."
msgstr ""
-msgid "You are on a secondary (read-only) Geo node. If you want to make any changes, you must visit the %{primary_node}."
+msgid "You can %{linkStart}view the blob%{linkEnd} instead."
msgstr ""
msgid "You can also create a project from the command line."
@@ -4775,6 +5151,9 @@ msgstr ""
msgid "You can also star a label to make it a priority label."
msgstr ""
+msgid "You can also test your .gitlab-ci.yml in the %{linkStart}Lint%{linkEnd}"
+msgstr ""
+
msgid "You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}"
msgstr ""
@@ -4787,30 +5166,33 @@ msgstr "åªèƒ½åœ¨åˆ†æ”¯ä¸Šæ·»åŠ æ–‡ä»¶"
msgid "You can only edit 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."
+msgid "You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}"
msgstr ""
msgid "You cannot write to this read-only GitLab instance."
msgstr ""
-msgid "You do not have the correct permissions to override the settings from the LDAP group sync."
+msgid "You do not have any assigned merge requests"
msgstr ""
msgid "You have no permissions"
msgstr ""
+msgid "You have not created any merge requests"
+msgstr ""
+
msgid "You have reached your project limit"
msgstr "您已é”到項目數é‡é™åˆ¶"
-msgid "You must have master access to force delete a lock"
+msgid "You must accept our Terms of Service and privacy policy in order to register an account"
+msgstr ""
+
+msgid "You must have maintainer access to force delete a lock"
msgstr ""
msgid "You must sign in to star a project"
msgstr "必須登錄æ‰èƒ½å°é …目加星標"
-msgid "You need a different license to enable FileLocks feature"
-msgstr ""
-
msgid "You need permission."
msgstr "需è¦ç›¸é—œçš„æ¬Šé™ã€‚"
@@ -4877,12 +5259,11 @@ msgstr "您的åå­—"
msgid "Your projects"
msgstr ""
-msgid "among other things"
+msgid "ago"
msgstr ""
-msgid "and %d fixed vulnerability"
-msgid_plural "and %d fixed vulnerabilities"
-msgstr[0] ""
+msgid "among other things"
+msgstr ""
msgid "assign yourself"
msgstr ""
@@ -4890,133 +5271,35 @@ msgstr ""
msgid "branch name"
msgstr ""
-msgid "by"
-msgstr ""
-
-msgid "ciReport|%{type} detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|%{type} detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Code quality"
-msgstr ""
-
-msgid "ciReport|DAST detected no alerts by analyzing the review app"
-msgstr ""
-
-msgid "ciReport|Dependency scanning"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Failed to load %{reportName} report"
-msgstr ""
-
-msgid "ciReport|Fixed:"
-msgstr ""
-
-msgid "ciReport|Instances"
-msgstr ""
-
-msgid "ciReport|Learn more about whitelisting"
-msgstr ""
-
-msgid "ciReport|Loading %{reportName} report"
-msgstr ""
-
-msgid "ciReport|No changes to code quality"
-msgstr ""
-
-msgid "ciReport|No changes to performance metrics"
-msgstr ""
-
-msgid "ciReport|Performance metrics"
-msgstr ""
-
-msgid "ciReport|SAST"
-msgstr ""
-
-msgid "ciReport|SAST detected"
-msgstr ""
-
-msgid "ciReport|SAST detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|SAST detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|SAST:container no vulnerabilities were found"
-msgstr ""
-
-msgid "ciReport|Security scanning"
-msgstr ""
-
-msgid "ciReport|Security scanning failed loading any results"
-msgstr ""
-
-msgid "ciReport|Show complete code vulnerabilities report"
-msgstr ""
-
-msgid "ciReport|Unapproved vulnerabilities (red) can be marked as approved. %{helpLink}"
-msgstr ""
-
-msgid "ciReport|no vulnerabilities"
-msgstr ""
-
msgid "command line instructions"
msgstr ""
msgid "connecting"
msgstr ""
-msgid "could not read private key, is the passphrase correct?"
-msgstr ""
-
msgid "day"
msgid_plural "days"
msgstr[0] "天"
-msgid "detected %d fixed vulnerability"
-msgid_plural "detected %d fixed vulnerabilities"
-msgstr[0] ""
-
-msgid "detected %d new vulnerability"
-msgid_plural "detected %d new vulnerabilities"
-msgstr[0] ""
-
-msgid "detected no vulnerabilities"
+msgid "deploy token"
msgstr ""
-msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
-msgstr ""
-
-msgid "here"
-msgstr ""
-
-msgid "importing"
+msgid "disabled"
msgstr ""
-msgid "in progress"
+msgid "enabled"
msgstr ""
-msgid "is invalid because there is downstream lock"
+msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
msgstr ""
-msgid "is invalid because there is upstream lock"
+msgid "for this project"
msgstr ""
-msgid "is not a valid X509 certificate."
+msgid "importing"
msgstr ""
-msgid "locked by %{path_lock_user_name} %{created_at}"
+msgid "latest version"
msgstr ""
msgid "merge request"
@@ -5035,28 +5318,7 @@ msgstr ""
msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage is %{emphasisStart} unchanged %{emphasisEnd} at %{memoryFrom}MB"
msgstr ""
-msgid "mrWidget|Add approval"
-msgstr ""
-
-msgid "mrWidget|Allows edits from maintainers"
-msgstr ""
-
-msgid "mrWidget|An error occured while removing your approval."
-msgstr ""
-
-msgid "mrWidget|An error occured while retrieving approval data for this merge request."
-msgstr ""
-
-msgid "mrWidget|An error occured while submitting your approval."
-msgstr ""
-
-msgid "mrWidget|Approve"
-msgstr ""
-
-msgid "mrWidget|Approved"
-msgstr ""
-
-msgid "mrWidget|Approved by"
+msgid "mrWidget|Allows commits from members who can merge to the target branch"
msgstr ""
msgid "mrWidget|Cancel automatic merge"
@@ -5083,6 +5345,9 @@ msgstr ""
msgid "mrWidget|Closes"
msgstr ""
+msgid "mrWidget|Create an issue to resolve them later"
+msgstr ""
+
msgid "mrWidget|Deployment statistics are not available currently"
msgstr ""
@@ -5137,9 +5402,6 @@ msgstr ""
msgid "mrWidget|Remove source branch"
msgstr ""
-msgid "mrWidget|Remove your approval"
-msgstr ""
-
msgid "mrWidget|Request to merge"
msgstr ""
@@ -5179,6 +5441,9 @@ msgstr ""
msgid "mrWidget|There are merge conflicts"
msgstr ""
+msgid "mrWidget|There are unresolved discussions. Please resolve these discussions"
+msgstr ""
+
msgid "mrWidget|This merge request failed to be merged automatically"
msgstr ""
@@ -5228,7 +5493,7 @@ msgstr ""
msgid "personal access token"
msgstr ""
-msgid "private key does not match certificate."
+msgid "remaining"
msgstr ""
msgid "remove due date"
@@ -5243,9 +5508,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "to help your contributors communicate effectively!"
-msgstr ""
-
msgid "username"
msgstr ""
@@ -5255,3 +5517,7 @@ msgstr ""
msgid "with %{additions} additions, %{deletions} deletions."
msgstr ""
+msgid "within %d minute "
+msgid_plural "within %d minutes "
+msgstr[0] ""
+
diff --git a/locale/zh_TW/gitlab.po b/locale/zh_TW/gitlab.po
index 553050d06a1..206342b160d 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: 2018-04-04 19:35+0200\n"
-"PO-Revision-Date: 2018-04-05 03:39-0400\n"
+"POT-Creation-Date: 2018-07-01 16:35+1000\n"
+"PO-Revision-Date: 2018-07-02 07:22\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Chinese Traditional\n"
"Language: zh_TW\n"
@@ -16,12 +16,13 @@ msgstr ""
"X-Crowdin-Language: zh-TW\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-msgid " and"
-msgstr " 和"
+msgid "%d changed file"
+msgid_plural "%d changed files"
+msgstr[0] "%d 個變更的檔案"
msgid "%d commit"
msgid_plural "%d commits"
-msgstr[0] "%d 個更動 (commit)"
+msgstr[0] "%d 個更動"
msgid "%d commit behind"
msgid_plural "%d commits behind"
@@ -47,12 +48,20 @@ msgid "%d metric"
msgid_plural "%d metrics"
msgstr[0] "%d 指標"
+msgid "%d staged change"
+msgid_plural "%d staged changes"
+msgstr[0] "%d 個暫存變更"
+
+msgid "%d unstaged change"
+msgid_plural "%d unstaged changes"
+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)。"
msgid "%{actionText} & %{openOrClose} %{noteable}"
-msgstr ""
+msgstr "%{actionText} 和 %{openOrClose} %{noteable}"
msgid "%{commit_author_link} authored %{commit_timeago}"
msgstr "ç”± %{commit_author_link} æäº¤æ–¼ %{commit_timeago}"
@@ -61,14 +70,23 @@ msgid "%{count} participant"
msgid_plural "%{count} participants"
msgstr[0] "%{count} åƒèˆ‡è€…"
+msgid "%{filePath} deleted"
+msgstr "已刪除 %{filePath}"
+
+msgid "%{group_docs_link_start}Groups%{group_docs_link_end} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects."
+msgstr "%{group_docs_link_start}群組%{group_docs_link_end} å…許您管ç†ã€å”作多個專案。群組的æˆå“¡å¯ä»¥è¨ªå•群組下的所有專案。"
+
msgid "%{loadingIcon} Started"
msgstr "%{loadingIcon} é–‹å§‹"
msgid "%{lock_path} is locked by GitLab User %{lock_user_id}"
msgstr "%{lock_path} 被使用者 %{lock_user_id} 鎖定"
+msgid "%{nip_domain} can be used as an alternative to a custom domain."
+msgstr "%{nip_domain} å¯ä»¥ä½¿ç”¨ç‚ºè‡ªè¨‚網域的替代方案。"
+
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} 次。GitLab å…許在 %{maximum_failures} 次之內å¯å†å˜—è©¦è®€å– ã€‚"
@@ -79,6 +97,9 @@ msgstr "已失敗 %{number_of_failures} / %{maximum_failures} 次,GitLab å°‡ä¸
msgid "%{openOrClose} %{noteable}"
msgstr "%{openOrClose} %{noteable}"
+msgid "%{percent}%% complete"
+msgstr "%{percent}%% 完æˆ"
+
msgid "%{storage_name}: failed storage access attempt on host:"
msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:"
msgstr[0] "%{storage_name}ï¼šå·²å­˜å–æ­¤ä¸»æ©Ÿå¤±æ•— %{failed_attempts} 次"
@@ -86,15 +107,55 @@ msgstr[0] "%{storage_name}ï¼šå·²å­˜å–æ­¤ä¸»æ©Ÿå¤±æ•— %{failed_attempts} 次"
msgid "%{text} is available"
msgstr "%{text} å¯ç”¨"
+msgid "%{title} changes"
+msgstr "%{title} 變更"
+
+msgid "%{unstaged} unstaged and %{staged} staged changes"
+msgstr "%{unstaged} 個未暫存和 %{staged} 個已暫存變更"
+
msgid "(checkout the %{link} for information on how to install it)."
msgstr "(如何安è£è«‹åƒé–± %{link})"
msgid "+ %{moreCount} more"
msgstr "+ %{moreCount} 更多"
+msgid "- Runner is active and can process any new jobs"
+msgstr "- Runner 為啟用狀態,並且å¯ä»¥è™•ç†ä»»ä½•新的工作。"
+
+msgid "- Runner is paused and will not receive any new jobs"
+msgstr "- Runner 為暫åœç‹€æ…‹ï¼Œä¸”將䏿œƒæŽ¥å—任何新的工作"
+
msgid "- show less"
msgstr "顯示較少"
+msgid "1 %{type} addition"
+msgid_plural "%{count} %{type} additions"
+msgstr[0] "%{count}%{type} 個附加"
+
+msgid "1 %{type} modification"
+msgid_plural "%{count} %{type} modifications"
+msgstr[0] "%{count}%{type} 個變更"
+
+msgid "1 closed issue"
+msgid_plural "%d closed issues"
+msgstr[0] "%d 個關閉的議題"
+
+msgid "1 closed merge request"
+msgid_plural "%d closed merge requests"
+msgstr[0] "%d 個關閉的åˆä½µè«‹æ±‚"
+
+msgid "1 merged merge request"
+msgid_plural "%d merged merge requests"
+msgstr[0] "%d 個已åˆä½µçš„åˆä½µè«‹æ±‚"
+
+msgid "1 open issue"
+msgid_plural "%d open issues"
+msgstr[0] "%d 個進行中的議題"
+
+msgid "1 open merge request"
+msgid_plural "%d open merge requests"
+msgstr[0] "%d 個進行中的åˆä½µè«‹æ±‚"
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] "%d æ¢æµæ°´ç·š"
@@ -105,11 +166,29 @@ msgstr "第一次å”作"
msgid "2FA enabled"
msgstr "已啟用雙é‡èªè­‰"
+msgid "403|Please contact your GitLab administrator to get the permission."
+msgstr "403 |è«‹è¯ç¹«æ‚¨çš„GitLab管ç†å“¡ä»¥ç²å–權é™ã€‚"
+
+msgid "403|You don't have the permission to access this page."
+msgstr "您無權使用這個é é¢ã€‚"
+
+msgid "404|Make sure the address is correct and the page hasn't moved."
+msgstr "404 |請確ä¿ç¶²å€æ­£ç¢ºä¸”ç¶²é ä½ç½®æ²’有被更改。"
+
+msgid "404|Page Not Found"
+msgstr "無法找到網é "
+
+msgid "404|Please contact your GitLab administrator if you think this is a mistake."
+msgstr "404 |如果您èªç‚ºé€™æ˜¯éŒ¯èª¤ï¼Œè«‹è¯ç¹«æ‚¨çš„GitLab管ç†å“¡ã€‚"
+
msgid "<strong>Removes</strong> source branch"
msgstr "<strong>刪除</strong>來æºåˆ†æ”¯"
+msgid "A 'Runner' is a process which runs a job. You can setup as many Runners as you need."
+msgstr "ä¸€å€‹ã€ŒåŸ·è¡Œå™¨ã€æ˜¯ä¸€å€‹åŸ·è¡Œå·¥ä½œçš„進程。你å¯ä»¥å®‰è£ä½ éœ€è¦çš„多個執行器。"
+
msgid "A collection of graphs regarding Continuous Integration"
-msgstr "æŒçºŒæ•´åˆ (CI) 相關的圖表"
+msgstr "æŒçºŒæ•´åˆç›¸é—œçš„圖表"
msgid "A new branch will be created in your fork and a new merge request will be started."
msgstr "將會å†å‰µå»ºä¸€å€‹æ–°çš„分支,並建立一個新的åˆä½µè«‹æ±‚。"
@@ -117,6 +196,9 @@ msgstr "將會å†å‰µå»ºä¸€å€‹æ–°çš„分支,並建立一個新的åˆä½µè«‹æ±‚。
msgid "A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}."
msgstr "一個專案æä¾›äº†ä»¥ä¸‹åŠŸèƒ½ï¼Œå­˜æ”¾ä½ çš„æ–‡ä»¶(存儲庫),計劃你的工作(議題),並發布你的文件(維基), %{among_other_things_link}。"
+msgid "A regular expression that will be used to find the test coverage output in the job trace. Leave blank to disable"
+msgstr "正則表é”å¼ï¼Œå°‡ç”¨æ–¼åœ¨ä½œæ¥­è·Ÿè¸ªä¸­æŸ¥æ‰¾æ¸¬è©¦è¦†è“‹çŽ‡çš„è¼¸å‡ºã€‚ç•™ç©ºæ™‚åœç”¨æ­¤åŠŸèƒ½"
+
msgid "A user with write access to the source branch selected this option"
msgstr "一個有存å–原始分支權é™çš„ä½¿ç”¨è€…ï¼Œé¸æ“‡äº†æ­¤é …ç›®"
@@ -127,38 +209,41 @@ msgid "Abuse Reports"
msgstr "濫用報告"
msgid "Abuse reports"
-msgstr ""
+msgstr "濫用報告"
+
+msgid "Accept terms"
+msgstr "æŽ¥å—æ¢æ¬¾"
msgid "Access Tokens"
-msgstr "å­˜å–æ†‘è­‰ (access token)"
+msgstr "å­˜å–æ†‘è­‰"
msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again."
msgstr "已暫時åœç”¨å¤±æ•—çš„ Git 儲存空間。當儲存空間æ¢å¾©æ­£å¸¸å¾Œï¼Œè«‹é‡ç½®å„²å­˜ç©ºé–“å¥åº·æŒ‡æ•¸ã€‚"
+msgid "Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report."
+msgstr "å­˜å–æ‚¨åŸ·è¡Œå™¨æ†‘è­‰ï¼Œè‡ªå®šç¾©æ‚¨çš„æµæ°´ç·šé…ç½®ï¼Œä¸¦æŸ¥çœ‹ä½ çš„æµæ°´ç¾ç‹€æ…‹åŠæ¸¬è©¦æ¶µè“‹çŽ‡å ±å‘Šã€‚"
+
msgid "Account"
msgstr "帳號"
-msgid "Account and limit settings"
-msgstr ""
+msgid "Account and limit"
+msgstr "帳戶和é™åˆ¶"
msgid "Active"
msgstr "啟用"
+msgid "Active Sessions"
+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 Kubernetes cluster"
msgstr "增加 Kubernetes å¢é›†"
@@ -171,6 +256,9 @@ msgstr "增加說明檔案"
msgid "Add new directory"
msgstr "新增目錄"
+msgid "Add reaction"
+msgstr "添加回應"
+
msgid "Add todo"
msgstr "增加待辦事項"
@@ -202,7 +290,7 @@ msgid "AdminProjects|Delete project"
msgstr "刪除專案"
msgid "AdminSettings|Specify a domain to use by default for every project's Auto Review Apps and Auto Deploy stages."
-msgstr ""
+msgstr "為æ¯å€‹å°ˆæ¡ˆçš„自動複閱應用åŠè‡ªå‹•部署指定一個é è¨­çš„網域"
msgid "AdminUsers|Block user"
msgstr "å°éŽ–ä½¿ç”¨è€…"
@@ -226,7 +314,7 @@ msgid "AdminUsers|To confirm, type %{username}"
msgstr "請輸入 %{username} 以進行確èª"
msgid "Advanced"
-msgstr "進階設置"
+msgstr "進階"
msgid "Advanced settings"
msgstr "進階設定"
@@ -240,29 +328,44 @@ msgstr "所有改變都已經æäº¤"
msgid "All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings."
msgstr "從模æ¿å»ºç«‹æˆ–導入專案時將啟用所有功能,您å¯ä»¥åœ¨å°ˆæ¡ˆè¨­ç½®ä¸­å°‡å…¶é—œé–‰ã€‚"
-msgid "Allow edits from maintainers."
-msgstr "å…許維護人員編輯。"
+msgid "Allow commits from members who can merge to the target branch."
+msgstr "å…許å¯ä»¥åˆä½µè‡³ç›®æ¨™åˆ†æ”¯çš„æˆå“¡æäº¤"
+
+msgid "Allow public access to pipelines and job details, including output logs and artifacts"
+msgstr "å…è¨±å…¬é–‹å­˜å–æµæ°´ç·šå’Œä»»å‹™è©³ç´°è³‡è¨Šï¼ŒåŒ…å«è¼¸å‡ºæ—¥èªŒå’Œç”¢ç‰©"
msgid "Allow rendering of PlantUML diagrams in Asciidoc documents."
-msgstr ""
+msgstr "å…許在 Asciidoc 文件中渲染 PlantUML64"
msgid "Allow requests to the local network from hooks and services."
-msgstr ""
+msgstr "å…許來自鉤å­å’Œæœå‹™çš„å°æœ¬åœ°ç¶²çµ¡çš„請求。"
msgid "Allows you to add and manage Kubernetes clusters."
msgstr "å…許您增加和管ç†Kuberneteså¢é›†ã€‚"
-msgid "Also called \"Issuer\" or \"Relying party trust identifier\""
-msgstr ""
+msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
+msgstr "或者,你å¯ä»¥ä½¿ç”¨ %{personal_access_token_link}ã€‚ç•¶ä½ å»ºç«‹ä½ çš„å€‹äººå­˜å–æ¬Šæ–時,你將需è¦é¸æ“‡<code>檔案庫</code>範åœï¼Œæ‰€ä»¥æˆ‘們將會顯示你公開åŠç§äººçš„æª”案庫清單進行匯入。"
-msgid "Also called \"Relying party service URL\" or \"Reply URL\""
-msgstr ""
+msgid "An error occured creating the new branch."
+msgstr "創建新分支時發生錯誤。"
-msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to connect."
-msgstr "或者,你å¯ä»¥ä½¿ç”¨ %{personal_access_token_link}ã€‚ç•¶ä½ å»ºç«‹ä½ çš„å€‹äººå­˜å–æ¬Šæ–時,你將需è¦é¸æ“‡<code>檔案庫</code>範åœï¼Œæ‰€ä»¥æˆ‘們將會顯示你公開åŠç§äººçš„æª”案庫清單進行連çµã€‚"
+msgid "An error occured whilst loading all the files."
+msgstr "è®€å–æª”案時發生錯誤。"
-msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
-msgstr ""
+msgid "An error occured whilst loading the file content."
+msgstr "è®€å–æª”案內容時發生錯誤。"
+
+msgid "An error occured whilst loading the file."
+msgstr "è®€å–æª”案時發生錯誤。"
+
+msgid "An error occured whilst loading the merge request changes."
+msgstr "讀å–åˆä½µè«‹æ±‚的更改時發生錯誤。"
+
+msgid "An error occured whilst loading the merge request version data."
+msgstr "讀å–åˆä½µè«‹æ±‚的版本資訊時發生錯誤。"
+
+msgid "An error occured whilst loading the merge request."
+msgstr "讀å–åˆä½µè«‹æ±‚時發生錯誤。"
msgid "An error occurred previewing the blob"
msgstr "é è¦½ blob 檔案時發生錯誤"
@@ -270,81 +373,63 @@ msgstr "é è¦½ blob 檔案時發生錯誤"
msgid "An error occurred when toggling the notification subscription"
msgstr "切æ›è¨‚閱通知時發生錯誤"
-msgid "An error occurred when updating the issue weight"
-msgstr ""
-
-msgid "An error occurred while adding approver"
-msgstr ""
-
-msgid "An error occurred while detecting host keys"
-msgstr ""
+msgid "An error occurred while dismissing the alert. Refresh the page and try again."
+msgstr "ç•¶è§£é™¤é€šçŸ¥æ™‚éŒ¯èª¤ç™¼ç”Ÿã€‚è«‹å˜—è©¦é‡æ–°æ•´ç†é é¢ä¸¦é‡è©¦ã€‚"
msgid "An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again."
msgstr "è§£é™¤äº®é«˜é¡¯ç¤ºæ™‚ç™¼ç”ŸéŒ¯èª¤ï¼Œè«‹é‡æ–°æ•´ç†é é¢å†æ¬¡å˜—試。"
msgid "An error occurred while fetching markdown preview"
-msgstr ""
+msgstr "è®€å– markdown é è¦½æ™‚發生錯誤"
msgid "An error occurred while fetching sidebar data"
-msgstr ""
+msgstr "讀å–å´é‚Šæ¬„資料時發生錯誤"
msgid "An error occurred while fetching the pipeline."
-msgstr ""
+msgstr "è®€å–æµæ°´ç·šæ™‚發生錯誤"
msgid "An error occurred while getting projects"
-msgstr ""
+msgstr "讀å–專案時發生錯誤"
-msgid "An error occurred while importing project"
-msgstr ""
-
-msgid "An error occurred while initializing path locks"
-msgstr ""
+msgid "An error occurred while importing project: ${details}"
+msgstr "匯入專案時發生錯誤: ${details}"
msgid "An error occurred while loading commits"
-msgstr ""
+msgstr "è®€å–æ›´å‹•記錄時發生錯誤"
msgid "An error occurred while loading diff"
-msgstr ""
+msgstr "讀å–差異檔時發生錯誤"
msgid "An error occurred while loading filenames"
-msgstr ""
+msgstr "è®€å–æª”案å稱時發生錯誤"
msgid "An error occurred while loading the file"
-msgstr ""
+msgstr "è®€å–æª”案時發生錯誤"
msgid "An error occurred while making the request."
-msgstr ""
-
-msgid "An error occurred while removing approver"
-msgstr ""
+msgstr "建立請求時發生錯誤"
msgid "An error occurred while rendering KaTeX"
-msgstr ""
+msgstr "渲染 KaTex 時發生錯誤"
msgid "An error occurred while rendering preview broadcast message"
-msgstr ""
+msgstr "åœ¨ç”¢ç”Ÿå»£æ’­æ¶ˆæ¯æ™‚發生錯誤"
msgid "An error occurred while retrieving calendar activity"
-msgstr ""
+msgstr "讀å–行事曆時發生錯誤"
msgid "An error occurred while retrieving diff"
-msgstr ""
-
-msgid "An error occurred while saving LDAP override status. Please try again."
-msgstr ""
+msgstr "讀å–差異檔時發生錯誤"
msgid "An error occurred while saving assignees"
msgstr "儲存被指派人時發生錯誤"
msgid "An error occurred while validating username"
-msgstr ""
+msgstr "驗證使用者å稱時發生錯誤"
msgid "An error occurred. Please try again."
msgstr "發生錯誤,請å†è©¦ä¸€æ¬¡ã€‚"
-msgid "Any Label"
-msgstr "ä»»æ„æ¨™ç±¤"
-
msgid "Appearance"
msgstr "外觀"
@@ -357,20 +442,20 @@ msgstr "四月"
msgid "April"
msgstr "四月"
-msgid "Archived project! Repository is read-only"
-msgstr "此專案已å°å­˜ï¼æª”案庫 (repository) 為唯讀狀態"
+msgid "Archived project! Repository and other project resources are read-only"
+msgstr "這是個已經å°å­˜çš„專案ï¼å…¶æª”案庫與其其他專案資æºç‚ºå”¯è®€ç‹€æ…‹ã€‚"
msgid "Are you sure you want to delete this pipeline schedule?"
-msgstr "確定è¦åˆªé™¤æ­¤æµæ°´ç·š (pipeline) 排程嗎?"
+msgstr "確定è¦åˆªé™¤æ­¤æµæ°´ç·šæŽ’程嗎?"
+
+msgid "Are you sure you want to remove this identity?"
+msgstr "您確定è¦åˆªé™¤æ­¤èº«ä»½å—Ž?"
msgid "Are you sure you want to reset registration token?"
-msgstr "確定è¦é‡ç½®è¨»å†Šæ†‘è­‰ (registration token) 嗎?"
+msgstr "確定è¦é‡ç½®è¨»å†Šæ†‘證嗎?"
msgid "Are you sure you want to reset the health check token?"
-msgstr "確定è¦é‡ç½®å¥åº·æª¢æŸ¥å­˜å–憑證 (access token) 嗎?"
-
-msgid "Are you sure you want to unlock %{path_lock_path}?"
-msgstr ""
+msgstr "確定è¦é‡ç½®å¥åº·æª¢æŸ¥å­˜å–憑證嗎?"
msgid "Are you sure?"
msgstr "確定嗎?"
@@ -378,8 +463,8 @@ msgstr "確定嗎?"
msgid "Artifacts"
msgstr "產物"
-msgid "Assertion consumer service URL"
-msgstr ""
+msgid "Ask your group maintainer to setup a group Runner."
+msgstr "è©¢å•æ‚¨çš„群組維護者以安è£ç¾¤çµ„執行器。"
msgid "Assign custom color like #FF0000"
msgstr "自定義é¡è‰²ï¼Œä¾‹å¦‚ #FF0000"
@@ -394,17 +479,23 @@ msgid "Assign to"
msgstr "指派給"
msgid "Assigned Issues"
-msgstr ""
+msgstr "已指派的議題"
msgid "Assigned Merge Requests"
-msgstr ""
+msgstr "已指派的åˆä½µè«‹æ±‚"
msgid "Assigned to :name"
-msgstr ""
+msgstr "指派給:人å"
+
+msgid "Assigned to me"
+msgstr "指派給我"
msgid "Assignee"
msgstr "指派人"
+msgid "Assignee(s)"
+msgstr "執行者"
+
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "拖放檔案到此處或者 %{upload_link}"
@@ -427,19 +518,22 @@ msgid "Auto DevOps enabled"
msgstr "啟動自動 DevOps"
msgid "Auto DevOps, runners and job artifacts"
-msgstr ""
+msgstr "自動 DevOpsã€åŸ·è¡Œå™¨èˆ‡å·¥ä½œç”¢å‡ºç‰©"
msgid "Auto Review Apps and Auto Deploy need a %{kubernetes} to work correctly."
-msgstr ""
+msgstr "自動複閱應用åŠè‡ªå‹•éƒ¨ç½²éœ€è¦ %{kubernetes} æ‰èƒ½æ­£å¸¸å·¥ä½œã€‚"
msgid "Auto Review Apps and Auto Deploy need a domain name and a %{kubernetes} to work correctly."
-msgstr ""
+msgstr "自動復閱åŠè‡ªå‹•部署需è¦ä¸€å€‹ç¶²åŸŸå’Œ %{kubernetes} æ‰èƒ½æ­£å¸¸å·¥ä½œ"
msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
-msgstr "自動複閱應用 (review apps) 與自動部署需è¦ç¶²åŸŸæ‰èƒ½é‹ä½œã€‚"
+msgstr "自動複閱應用與自動部署需è¦ç¶²åŸŸæ‰èƒ½é‹ä½œã€‚"
+
+msgid "Auto-cancel redundant, pending pipelines"
+msgstr "è‡ªå‹•å–æ¶ˆå¤šé¤˜ã€å¾…處ç†çš„æµæ°´ç·š"
-msgid "AutoDevOps|Auto DevOps (Beta)"
-msgstr "DevOps 自動化(beta)"
+msgid "AutoDevOps|Auto DevOps"
+msgstr "自動 DevOps"
msgid "AutoDevOps|Auto DevOps documentation"
msgstr "「DevOps 自動化〠文件"
@@ -454,116 +548,146 @@ msgid "AutoDevOps|Learn more in the %{link_to_documentation}"
msgstr "了解更多於 %{link_to_documentation}"
msgid "AutoDevOps|You can automatically build and test your application if you %{link_to_auto_devops_settings} for this project. You can automatically deploy it as well, if you %{link_to_add_kubernetes_cluster}."
-msgstr ""
+msgstr "如果 %{link_to_auto_devops_settings} 這個專案,您å¯ä»¥è‡ªå‹•å»ºç½®å’Œæ¸¬è©¦ä½ çš„æ‡‰ç”¨ç¨‹å¼ %{link_to_add_kubernetes_cluster} 則å¯ä»¥è®“你自動部署"
msgid "AutoDevOps|add a Kubernetes cluster"
-msgstr ""
+msgstr "添加 Kubernetes å¢é›†"
-msgid "AutoDevOps|enable Auto DevOps (Beta)"
-msgstr "啟用自動 DevOps (測試版)"
+msgid "AutoDevOps|enable Auto DevOps"
+msgstr "啟用自動 DevOps"
msgid "Available"
msgstr "能é‹åšçš„"
+msgid "Available group Runners : %{runners}"
+msgstr "å¯ä½¿ç”¨çš„群組執行器:%{runners}"
+
+msgid "Available group Runners : %{runners}."
+msgstr "å¯ä½¿ç”¨çš„群組執行器:%{runners}。"
+
msgid "Avatar will be removed. Are you sure?"
msgstr "大頭貼將被刪除。你確定嗎?"
msgid "Average per day: %{average}"
msgstr "平凿¯å¤©ï¼š%{average}"
-msgid "Background Color"
-msgstr ""
+msgid "Background color"
+msgstr "背景é¡è‰²"
msgid "Background jobs"
-msgstr ""
+msgstr "背景工作"
-msgid "Begin with the selected commit"
-msgstr "從é¸å®šçš„變更紀錄開始"
+msgid "Badges"
+msgstr "徽章"
-msgid "Billing"
-msgstr "帳單"
+msgid "Badges|A new badge was added."
+msgstr "添加了新的徽章"
-msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan."
-msgstr ""
+msgid "Badges|Add badge"
+msgstr "新增徽章"
-msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available."
-msgstr ""
+msgid "Badges|Adding the badge failed, please check the entered URLs and try again."
+msgstr "新增徽章失敗,請確èªè¼¸å…¥çš„ç¶²å€ï¼Œå†è©¦ä¸€æ¬¡ã€‚"
-msgid "BillingPlans|Current plan"
-msgstr ""
+msgid "Badges|Badge image URL"
+msgstr "徽章圖片網å€"
-msgid "BillingPlans|Customer Support"
-msgstr ""
+msgid "Badges|Badge image preview"
+msgstr "徽章圖片é è¦½"
-msgid "BillingPlans|Downgrade"
-msgstr ""
+msgid "Badges|Delete badge"
+msgstr "刪除徽章"
-msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
-msgstr ""
+msgid "Badges|Delete badge?"
+msgstr "你確定è¦åˆªé™¤å¾½ç« å—Žï¼Ÿ"
-msgid "BillingPlans|Manage plan"
-msgstr ""
+msgid "Badges|Deleting the badge failed, please try again."
+msgstr "刪除徽章失敗,請ç¨å€™é‡è©¦"
-msgid "BillingPlans|Please contact %{customer_support_link} in that case."
-msgstr ""
+msgid "Badges|Group Badge"
+msgstr "群組徽章"
-msgid "BillingPlans|See all %{plan_name} features"
-msgstr ""
+msgid "Badges|Link"
+msgstr "連çµ"
-msgid "BillingPlans|This group uses the plan associated with its parent group."
-msgstr ""
+msgid "Badges|No badge image"
+msgstr "沒有徽章圖片"
-msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
-msgstr ""
+msgid "Badges|No image to preview"
+msgstr "沒有圖片å¯ä»¥é è¦½"
-msgid "BillingPlans|Upgrade"
-msgstr ""
+msgid "Badges|Project Badge"
+msgstr "專案徽章"
-msgid "BillingPlans|You are currently on the %{plan_link} plan."
-msgstr ""
+msgid "Badges|Reload badge image"
+msgstr "釿–°è®€å–徽章圖片"
-msgid "BillingPlans|frequently asked questions"
-msgstr ""
+msgid "Badges|Save changes"
+msgstr "儲存變更"
-msgid "BillingPlans|monthly"
-msgstr ""
+msgid "Badges|Saving the badge failed, please check the entered URLs and try again."
+msgstr "儲存徽章失敗,請檢查輸入的連çµä¸¦é‡è©¦ã€‚"
-msgid "BillingPlans|paid annually at %{price_per_year}"
-msgstr ""
+msgid "Badges|The %{docsLinkStart}variables%{docsLinkEnd} GitLab supports: %{placeholders}"
+msgstr "%{docsLinkStart} 變數%{docsLinkEnd} GitLab 支æ´ï¼š%{placeholders}"
-msgid "BillingPlans|per user"
-msgstr ""
+msgid "Badges|The badge was deleted."
+msgstr "此徽章已移除"
+
+msgid "Badges|The badge was saved."
+msgstr "此徽章已儲存。"
+
+msgid "Badges|This group has no badges"
+msgstr "此群組無徽章。"
+
+msgid "Badges|This project has no badges"
+msgstr "此專案無徽章"
+
+msgid "Badges|Your badges"
+msgstr "您的徽章"
+
+msgid "Begin with the selected commit"
+msgstr "從é¸å®šçš„變更紀錄開始"
+
+msgid "Below are examples of regex for existing tools:"
+msgstr "ä»¥ä¸‹æ˜¯ç¾æœ‰å·¥å…·çš„æ­£è¦è¡¨é”å¼ç¤ºä¾‹ï¼š"
+
+msgid "Boards"
+msgstr "看æ¿"
+
+msgid "Branch %{branchName} was not found in this project's repository."
+msgstr "在這個專案的檔案庫中找ä¸åˆ° %{branchName} 分支。"
msgid "Branch (%{branch_count})"
msgid_plural "Branches (%{branch_count})"
-msgstr[0] ""
+msgstr[0] "分支 (%{branch_count})"
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}"
+msgstr "已建立分支 <strong>%{branch_name}</strong> 。如需設定自動部署, åœ¨é¸æ“‡åˆé©çš„ GitLab CI Yaml 模æ¿å¾Œï¼Œè«‹æäº¤æ‚¨çš„編輯內容。%{link_to_autodeploy_doc}"
msgid "Branch has changed"
-msgstr "分支(branch)已變更"
+msgstr "分支已變更"
msgid "Branch is already taken"
-msgstr ""
+msgstr "分支已經被更新éŽã€‚"
msgid "Branch name"
-msgstr ""
+msgstr "分支å稱"
msgid "BranchSwitcherPlaceholder|Search branches"
-msgstr "æœå°‹åˆ†æ”¯ (branches)"
+msgstr "æœå°‹åˆ†æ”¯"
msgid "BranchSwitcherTitle|Switch branch"
-msgstr "切æ›åˆ†æ”¯ (branch)"
+msgstr "切æ›åˆ†æ”¯"
msgid "Branches"
-msgstr "分支 (branch) "
+msgstr "分支"
msgid "Branches|Active"
-msgstr ""
+msgstr "æ´»èºçš„"
msgid "Branches|Active branches"
-msgstr ""
+msgstr "æ´»èºçš„分支"
msgid "Branches|All"
msgstr "全部"
@@ -610,44 +734,41 @@ msgstr "找ä¸åˆ°åˆ†æ”¯"
msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
msgstr "一旦你確èªä¸¦æŒ‰ä¸‹ %{delete_protected_branch} 之後,此動作將無法撤銷或還原。"
-msgid "Branches|Only a project master or owner can delete a protected branch"
-msgstr "åªæœ‰å°ˆæ¡ˆç®¡ç†è€…æˆ–æ“æœ‰è€…æ‰èƒ½åˆªé™¤è¢«ä¿è­·çš„分支。"
+msgid "Branches|Only a project maintainer or owner can delete a protected branch"
+msgstr "åªæœ‰å°ˆæ¡ˆç¶­è­·è€…æˆ–æ“æœ‰è€…å¯ä»¥ç§»é™¤å—ä¿è­·çš„分支。"
msgid "Branches|Overview"
-msgstr ""
+msgstr "概覽"
msgid "Branches|Protected branches can be managed in %{project_settings_link}."
-msgstr ""
+msgstr "å—ä¿è­·çš„分支å¯ä»¥åœ¨ %{project_settings_link} 進行管ç†ã€‚"
msgid "Branches|Show active branches"
-msgstr ""
+msgstr "顯示活èºçš„分支"
msgid "Branches|Show all branches"
-msgstr ""
+msgstr "顯示所有分支"
msgid "Branches|Show more active branches"
-msgstr ""
+msgstr "顯示更多活èºçš„分支"
msgid "Branches|Show more stale branches"
-msgstr ""
+msgstr "顯示更多沉éœçš„分支"
msgid "Branches|Show overview of the branches"
-msgstr ""
+msgstr "顯示分支概覽"
msgid "Branches|Show stale branches"
-msgstr ""
+msgstr "顯示沉éœçš„分支"
msgid "Branches|Sort by"
msgstr "排åºè‡ª"
msgid "Branches|Stale"
-msgstr ""
+msgstr "沉éœçš„"
msgid "Branches|Stale branches"
-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 "無法刪除é è¨­åˆ†æ”¯"
@@ -661,15 +782,9 @@ msgstr "為é¿å…資料éºå¤±ï¼Œè«‹åˆä½µè©²åˆ†æ”¯å¾Œå†å°‡å®ƒåˆªé™¤ã€‚"
msgid "Branches|To confirm, type %{branch_name_confirmation}:"
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 ""
-
msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
msgstr "你將永久刪除å—ä¿è­·çš„ %{branch_name} 分支。"
-msgid "Branches|diverged from upstream"
-msgstr ""
-
msgid "Branches|merged"
msgstr "å·²åˆä½µ"
@@ -691,50 +806,89 @@ msgstr "ç€è¦½æª”案"
msgid "Browse files"
msgstr "ç€è¦½æª”案"
-msgid "Business"
-msgstr ""
-
msgid "ByAuthor|by"
msgstr "作者:"
msgid "CI / CD"
msgstr "CI / CD"
-msgid "CI/CD"
-msgstr ""
+msgid "CI / CD Settings"
+msgstr "CI / CD 設定"
msgid "CI/CD configuration"
msgstr "CI/CD 設定"
-msgid "CI/CD for external repo"
-msgstr ""
+msgid "CI/CD settings"
+msgstr "CI / CD 設定"
+
+msgid "CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery."
+msgstr "明確的 %{ci_file} 需è¦åœ¨ä½ å¯ä»¥é–‹å§‹ä½¿ç”¨ã€ŒæŒçºŒæ•´åˆã€èˆ‡ã€Œå‚³éžã€ä¹‹å‰æŒ‡å®šã€‚"
+
+msgid "CICD|Auto DevOps"
+msgstr "自動 DevOps"
+
+msgid "CICD|Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration."
+msgstr "自動 DevOps å°‡æœƒè‡ªå‹•ç·¨è­¯ã€æ¸¬è©¦ã€ä¸¦åœ¨åŸºæ–¼é ç·¨è­¯ã€ŒæŒçºŒæ•´åˆã€å’Œã€Œå‚³éžã€è¨­å®šçš„環境部署您的應用程å¼"
+
+msgid "CICD|Automatic deployment to staging, manual deployment to production"
+msgstr "至動部屬到模擬環境,手動部屬到正å¼ç’°å¢ƒ"
+
+msgid "CICD|Continuous deployment to production"
+msgstr "æŒçºŒéƒ¨å±¬åˆ°æ­£å¼ç’°å¢ƒ"
+
+msgid "CICD|Deployment strategy"
+msgstr "部屬策略"
+
+msgid "CICD|Deployment strategy needs a domain name to work correctly."
+msgstr "部屬策略需è¦ç¶²åŸŸæ‰èƒ½æ­£å¸¸å·¥ä½œ"
+
+msgid "CICD|Disable Auto DevOps"
+msgstr "åœç”¨è‡ªå‹• DevOps"
+
+msgid "CICD|Enable Auto DevOps"
+msgstr "啟用自動 DevOps"
+
+msgid "CICD|Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific %{ci_file}."
+msgstr "跟隨著實例é è¨­å€¼ï¼Œä»¥åœ¨æ²’有專案指定 %{ci_file} 的情æ³ä¸‹å•Ÿç”¨æˆ–åœç”¨è‡ªå‹• DevOps 環境。"
+
+msgid "CICD|Instance default (%{state})"
+msgstr "é è¨­(%{state})"
msgid "CICD|Jobs"
msgstr "作業"
+msgid "CICD|Learn more about Auto DevOps"
+msgstr "學習更多關於 Auto DevOps 的相關訊æ¯"
+
+msgid "CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project."
+msgstr "當專案沒有 %{ci_file} 檔案,將啟用 Auto DevOps çš„æµæ°´ç·šè¨­ç½®ã€‚"
+
+msgid "CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages."
+msgstr "如果你想è¦ä½¿ç”¨è‡ªå‹•複閱程å¼åŠè‡ªå‹•部署,請指定網域。"
+
+msgid "Can't find HEAD commit for this branch"
+msgstr "找ä¸åˆ°æ­¤åˆ†æ”¯çš„ HEAD 更動。"
+
msgid "Cancel"
msgstr "å–æ¶ˆ"
+msgid "Cancel this job"
+msgstr "å–æ¶ˆæ­¤ä»»å‹™"
+
msgid "Cannot be merged automatically"
-msgstr ""
+msgstr "ä¸èƒ½è‡ªå‹•åˆä½µ"
msgid "Cannot modify managed Kubernetes cluster"
-msgstr ""
-
-msgid "Certificate fingerprint"
-msgstr ""
-
-msgid "Change Weight"
-msgstr ""
+msgstr "無法變更託管的 Kubernetes å¢é›†"
msgid "Change this value to influence how frequently the GitLab UI polls for updates."
-msgstr ""
+msgstr "更改此數值將影響到 GitLab UI æ‹‰å–æ›´æ–°çš„頻率。"
msgid "ChangeTypeActionLabel|Pick into branch"
-msgstr "挑é¸åˆ°åˆ†æ”¯ (branch) "
+msgstr "挑é¸åˆ°åˆ†æ”¯"
msgid "ChangeTypeActionLabel|Revert in branch"
-msgstr "還原分支 (branch) "
+msgstr "還原分支"
msgid "ChangeTypeAction|Cherry-pick"
msgstr "挑é¸"
@@ -743,13 +897,13 @@ msgid "ChangeTypeAction|Revert"
msgstr "還原"
msgid "ChangeTypeAction|This will create a new commit in order to revert the existing changes."
-msgstr ""
+msgstr "é€™å°‡æœƒå»ºç«‹æ–°çš„æ›´å‹•ç´€éŒ„ï¼Œä»¥é‚„åŽŸç¾æœ‰çš„æ›´æ”¹ã€‚"
msgid "Changelog"
msgstr "更新日誌"
msgid "Changes are shown as if the <b>source</b> revision was being merged into the <b>target</b> revision."
-msgstr ""
+msgstr "顯示更改,如果<b>來æº</b>修訂版正在åˆä½µåˆ°<b>目標</b>中修訂版本。"
msgid "Charts"
msgstr "統計圖"
@@ -758,40 +912,37 @@ msgid "Chat"
msgstr "峿™‚通訊"
msgid "Check interval"
-msgstr ""
+msgstr "檢查間隔"
msgid "Checking %{text} availability…"
-msgstr ""
+msgstr "正在檢查%{text} å¯ç”¨æ€§.."
msgid "Checking branch availability..."
-msgstr ""
+msgstr "正在檢查分支是å¦å·²ç¶“有人使用..."
msgid "Cherry-pick this commit"
-msgstr "æŒ‘é¸æ­¤æ›´å‹•記錄 (commit) "
+msgstr "æŒ‘é¸æ­¤æ›´å‹•記錄"
msgid "Cherry-pick this merge request"
-msgstr "æŒ‘é¸æ­¤åˆä½µè«‹æ±‚ (merge request) "
+msgstr "æŒ‘é¸æ­¤åˆä½µè«‹æ±‚"
msgid "Choose File ..."
msgstr "鏿“‡æª”案⋯"
msgid "Choose a branch/tag (e.g. %{master}) or enter a commit (e.g. %{sha}) to see what's changed or to create a merge request."
-msgstr ""
+msgstr "鏿“‡åˆ†æ”¯/標籤(例如:%{master})或者輸入更動紀錄(例如:%{sha})以查看更改內容或建立åˆä½µè«‹æ±‚。"
-msgid "Choose file..."
-msgstr "鏿“‡æª”案⋯"
+msgid "Choose any color."
+msgstr "é¸å–é¡è‰²ã€‚"
-msgid "Choose which groups you wish to synchronize to this secondary node."
-msgstr ""
+msgid "Choose between <code>clone</code> or <code>fetch</code> to get the recent application code"
+msgstr "鏿“‡ <code>複製</code> 或 <code>抓å–</code> 以ç²å–最新的應用程å¼åŽŸå§‹ç¢¼"
-msgid "Choose which repositories you want to connect and run CI/CD pipelines."
-msgstr ""
+msgid "Choose file..."
+msgstr "鏿“‡æª”案⋯"
msgid "Choose which repositories you want to import."
-msgstr ""
-
-msgid "Choose which shards you wish to synchronize to this secondary node."
-msgstr ""
+msgstr "鏿“‡ä½ æƒ³åŒ¯å…¥çš„æª”案庫。"
msgid "CiStatusLabel|canceled"
msgstr "已喿¶ˆ"
@@ -848,358 +999,403 @@ msgid "CiStatus|running"
msgstr "執行中"
msgid "CiVariables|Input variable key"
-msgstr ""
+msgstr "輸入變數å稱"
msgid "CiVariables|Input variable value"
-msgstr ""
+msgstr "輸入變數的值"
msgid "CiVariables|Remove variable row"
-msgstr ""
+msgstr "刪除變數"
msgid "CiVariable|* (All environments)"
-msgstr ""
+msgstr "* (所有環境)"
msgid "CiVariable|All environments"
-msgstr ""
-
-msgid "CiVariable|Create wildcard"
-msgstr ""
+msgstr "所有環境"
msgid "CiVariable|Error occured while saving variables"
-msgstr ""
-
-msgid "CiVariable|New environment"
-msgstr ""
+msgstr "儲存變數時發生錯誤"
msgid "CiVariable|Protected"
-msgstr ""
-
-msgid "CiVariable|Search environments"
-msgstr ""
+msgstr "å—ä¿è­·çš„"
msgid "CiVariable|Toggle protected"
-msgstr ""
+msgstr "切æ›ç‚ºä¿è­·ç‹€æ…‹"
msgid "CiVariable|Validation failed"
-msgstr ""
+msgstr "驗證失敗"
msgid "CircuitBreakerApiLink|circuitbreaker api"
-msgstr "斷路器 (circuitbreaker) API"
+msgstr "斷路器 Api"
-msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
-msgstr ""
+msgid "Clear search input"
+msgstr "清除æœå°‹ç´€éŒ„"
-msgid "Click to expand text"
-msgstr ""
+msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
+msgstr "在專案列表點擊任何<strong>專案å稱</strong>,將轉跳到專案的里程碑。"
-msgid "Client authentication certificate"
-msgstr ""
+msgid "Click the <strong>Promote</strong> button in the top right corner to promote it to a group milestone."
+msgstr "點擊左上角的<strong>å‡ç´š</strong>按鈕,將æå‡è‡³ç¾¤çµ„里程碑。"
-msgid "Client authentication key"
-msgstr ""
+msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
+msgstr "點擊下é¢çš„æŒ‰éˆ•,連çµåˆ° Kubernetes é é¢é–‹å§‹å®‰è£ã€‚"
-msgid "Client authentication key password"
-msgstr ""
+msgid "Click to expand it."
+msgstr "按一下以展開。"
+
+msgid "Click to expand text"
+msgstr "點擊以展開內容"
msgid "Clone repository"
-msgstr "複製(clone)檔案庫(repository)"
+msgstr "複製檔案庫"
msgid "Close"
-msgstr ""
-
-msgid "Closed"
-msgstr ""
+msgstr "關閉"
msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster"
-msgstr ""
+msgstr "%{appList} 已經æˆåŠŸå®‰è£åˆ°æ‚¨çš„ Kubernetes å¢é›†"
msgid "ClusterIntegration|API URL"
-msgstr ""
+msgstr "Api ç¶²å€"
msgid "ClusterIntegration|Add Kubernetes cluster"
-msgstr ""
+msgstr "增加 Kubernetes å¢é›†"
msgid "ClusterIntegration|Add an existing Kubernetes cluster"
-msgstr ""
+msgstr "å¢žåŠ ç¾æœ‰çš„ Kubernetes å¢é›†"
msgid "ClusterIntegration|Advanced options on this Kubernetes cluster's integration"
-msgstr ""
+msgstr "Kubernetes å¢é›†æ•´åˆçš„進階設定"
+
+msgid "ClusterIntegration|An error occured while trying to fetch project zones: %{error}"
+msgstr "當嘗試ç²å–專案å€åŸŸæ™‚錯誤發生:%{error}"
+
+msgid "ClusterIntegration|An error occured while trying to fetch your projects: %{error}"
+msgstr "當嘗試ç²å–您的專案時發生錯誤:%{error}"
msgid "ClusterIntegration|Applications"
-msgstr ""
+msgstr "應用程å¼"
msgid "ClusterIntegration|Are you sure you want to remove this Kubernetes cluster's integration? This will not delete your actual Kubernetes cluster."
-msgstr ""
+msgstr "您確定è¦åˆªé™¤æ­¤ Kubernetes å¢é›†çš„æ•´åˆè¨­å®šå—Žï¼Ÿ 這將會刪除您實際的 Kubernetes å¢é›†ã€‚"
msgid "ClusterIntegration|CA Certificate"
-msgstr ""
+msgstr "CAèªè­‰"
msgid "ClusterIntegration|Certificate Authority bundle (PEM format)"
-msgstr ""
+msgstr "憑證 (PEMæ ¼å¼)"
msgid "ClusterIntegration|Choose how to set up Kubernetes cluster integration"
-msgstr ""
+msgstr "鏿“‡å¦‚ä½•æ•´åˆ Kubernetes å¢é›†"
msgid "ClusterIntegration|Choose which of your project's environments will use this Kubernetes cluster."
-msgstr ""
+msgstr "鏿“‡æ‚¨è¦ä½¿ç”¨æ­¤ Kubernetes å¢é›†çš„專案環境"
msgid "ClusterIntegration|Control how your Kubernetes cluster integrates with GitLab"
-msgstr ""
+msgstr "控制 Kubernetes å¢é›†å¦‚何與 GitLab æ•´åˆ"
msgid "ClusterIntegration|Copy API URL"
-msgstr ""
+msgstr "複製 API 連çµ"
msgid "ClusterIntegration|Copy CA Certificate"
-msgstr ""
+msgstr "複製 CA 憑證"
msgid "ClusterIntegration|Copy Ingress IP Address to clipboard"
-msgstr ""
+msgstr "è¤‡è£½å…¥å£ IP ä½ç½®åˆ°å‰ªè²¼è¤²"
+
+msgid "ClusterIntegration|Copy Jupyter Hostname to clipboard"
+msgstr "複製 Jupyter 主機å稱至剪貼簿"
msgid "ClusterIntegration|Copy Kubernetes cluster name"
-msgstr ""
+msgstr "複製 Kubernetes å¢é›†å稱"
msgid "ClusterIntegration|Copy Token"
-msgstr ""
+msgstr "複製權æ–"
msgid "ClusterIntegration|Create Kubernetes cluster"
-msgstr ""
+msgstr "建立 Kubernetes å¢é›†"
msgid "ClusterIntegration|Create Kubernetes cluster on Google Kubernetes Engine"
-msgstr ""
+msgstr "在 Google Kubernetes Engine 上建立 Kubernetes å¢é›†"
msgid "ClusterIntegration|Create a new Kubernetes cluster on Google Kubernetes Engine right from GitLab"
-msgstr ""
+msgstr "é€éŽ GitLab 在 Google Kubernetes Engine 上建立 Kubernetes å¢é›†"
-msgid "ClusterIntegration|Create on GKE"
-msgstr ""
+msgid "ClusterIntegration|Create on Google Kubernetes Engine"
+msgstr "在 Google Kubernetes Engine 上建立"
msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
-msgstr ""
+msgstr "è¼¸å…¥ç¾æœ‰çš„ Kubernetes å¢é›†è©³ç´°è³‡è¨Š"
msgid "ClusterIntegration|Enter the details for your Kubernetes cluster"
-msgstr ""
+msgstr "輸入您的 Kubernetes å¢é›†è©³ç´°è³‡è¨Š"
msgid "ClusterIntegration|Environment scope"
-msgstr ""
+msgstr "環境範åœ"
+
+msgid "ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab's Google Kubernetes Engine Integration."
+msgstr "æ¯å€‹æ–°çš„ Google Cloud Platform (GCP) 帳號都會在 %{sign_up_link} 收到 300 元的優惠。因與 Google åˆä½œï¼ŒGitLab å°‡å¯ä»¥ç‚ºæ–°å»ºç«‹åŠç¾æœ‰çš„ GCP 帳號æä¾›é¡å¤–çš„ 200 元優惠,以嘗試 GitLab çš„ Google Kubernetes Engine æ•´åˆã€‚"
+
+msgid "ClusterIntegration|Fetching machine types"
+msgstr "正在ç²å–機器類型"
+
+msgid "ClusterIntegration|Fetching projects"
+msgstr "正在ç²å–專案"
+
+msgid "ClusterIntegration|Fetching zones"
+msgstr "正在ç²å–å€åŸŸ"
msgid "ClusterIntegration|GitLab Integration"
-msgstr ""
+msgstr "GitLab æ•´åˆ"
msgid "ClusterIntegration|GitLab Runner"
-msgstr ""
+msgstr "Gitlab Runner"
-msgid "ClusterIntegration|Google Cloud Platform project ID"
-msgstr "Google 雲端專案 ID"
+msgid "ClusterIntegration|Google Cloud Platform project"
+msgstr "Google 雲端專案"
msgid "ClusterIntegration|Google Kubernetes Engine"
-msgstr ""
+msgstr "Google Kubernetes Engine"
msgid "ClusterIntegration|Google Kubernetes Engine project"
-msgstr ""
+msgstr "Google Kubernetes Engine 專案"
msgid "ClusterIntegration|Helm Tiller"
-msgstr ""
-
-msgid "ClusterIntegration|In order to show the health of the cluster, we'll need to provision your cluster with Prometheus to collect the required data."
-msgstr ""
+msgstr "Helm Tiller"
msgid "ClusterIntegration|Ingress"
-msgstr ""
+msgstr "å…¥å£"
msgid "ClusterIntegration|Ingress IP Address"
-msgstr ""
+msgstr "å…¥å£ IP ä½ç½®"
msgid "ClusterIntegration|Install"
-msgstr ""
-
-msgid "ClusterIntegration|Install Prometheus"
-msgstr ""
+msgstr "安è£"
msgid "ClusterIntegration|Installed"
-msgstr ""
+msgstr "已安è£"
msgid "ClusterIntegration|Installing"
-msgstr ""
+msgstr "安è£ä¸­"
msgid "ClusterIntegration|Integrate Kubernetes cluster automation"
-msgstr ""
+msgstr "æ•´åˆ Kubernetes å¢é›†è‡ªå‹•化"
msgid "ClusterIntegration|Integration status"
-msgstr ""
+msgstr "æ•´åˆç‹€æ…‹"
+
+msgid "ClusterIntegration|Jupyter Hostname"
+msgstr "Jupyter 主機å稱"
+
+msgid "ClusterIntegration|JupyterHub"
+msgstr "JupyterHub"
msgid "ClusterIntegration|Kubernetes cluster"
-msgstr ""
+msgstr "Kubernetes å¢é›†"
msgid "ClusterIntegration|Kubernetes cluster details"
-msgstr ""
-
-msgid "ClusterIntegration|Kubernetes cluster health"
-msgstr ""
+msgstr "Kubernetes å¢é›†è©³ç´°è³‡è¨Š"
msgid "ClusterIntegration|Kubernetes cluster integration"
-msgstr ""
+msgstr "Kubernetes å¢é›†æ•´åˆ"
msgid "ClusterIntegration|Kubernetes cluster integration is disabled for this project."
-msgstr ""
+msgstr "此專案ç¦ç”¨ Kubernetes å¢é›†æ•´åˆ"
msgid "ClusterIntegration|Kubernetes cluster integration is enabled for this project."
-msgstr ""
+msgstr "此專案已啟用 Kubernetes å¢é›†æ•´åˆ"
msgid "ClusterIntegration|Kubernetes cluster integration is enabled for this project. Disabling this integration will not affect your Kubernetes cluster, it will only temporarily turn off GitLab's connection to it."
-msgstr ""
+msgstr "此專案已經啟用 Kubernetes å¢é›†æ•´åˆã€‚關閉此整åˆä¸¦ä¸æœƒå¼•響您的 Kubernetes å¢é›†ï¼Œä»–åªæœƒæš«æ™‚性的關閉 GitLab 與它的連çµã€‚"
msgid "ClusterIntegration|Kubernetes cluster is being created on Google Kubernetes Engine..."
-msgstr ""
+msgstr "Kubernetes å¢é›†å·²ç¶“在 Google Kubernetes Engine 上被建立"
msgid "ClusterIntegration|Kubernetes cluster name"
-msgstr ""
+msgstr "Kubernetes å¢é›†å稱"
msgid "ClusterIntegration|Kubernetes cluster was successfully created on Google Kubernetes Engine. Refresh the page to see Kubernetes cluster's details"
-msgstr ""
+msgstr "Kubernetes å¢é›†å·²ç¶“æˆåŠŸè¢«å»ºç«‹æ–¼ Google Kubernetes Engineï¼Œé‡æ–°æ•´ç†æ­¤é é¢æ‚¨å°‡æœƒçœ‹åˆ° Kubernetes å¢é›†çš„詳細資訊"
msgid "ClusterIntegration|Kubernetes clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way. %{link_to_help_page}"
-msgstr ""
+msgstr "å¢é›†è®“你用簡單的方å¼å¯©æŸ¥ã€éƒ¨ç½²æ‡‰ç”¨ç¨‹å¼ï¼Œé‹è¡Œæµæ°´ç·šç­‰åŠŸèƒ½ã€‚è©³æƒ…è«‹é–±ï¼š%{link_to_help_page}"
msgid "ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project"
-msgstr ""
+msgstr "Kubernetes å¢é›†å¯ä»¥ç”¨æ–¼éƒ¨ç½²æ‡‰ç”¨ç¨‹å¼ä»¥åŠç‚ºæ­¤å°ˆæ¡ˆæä¾›ä»£ç¢¼å¯©æŸ¥å·¥å…·"
+
+msgid "ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}."
+msgstr "瞭解有關 %{help_link_start_machine_type}機器類型%{help_link_end} 和 %{help_link_start_pricing}定價%{help_link_end} 的詳細資訊。"
+
+msgid "ClusterIntegration|Learn more about %{help_link_start}Kubernetes%{help_link_end}."
+msgstr "瞭解有關 %{help_link_start}Kubernetes%{help_link_end} 的詳細資訊。"
-msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
-msgstr "學習更多有關於%{link_to_documentation}"
+msgid "ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}."
+msgstr "瞭解有關 %{help_link_start}å€åŸŸ%{help_link_end} 的詳細資訊。"
msgid "ClusterIntegration|Learn more about environments"
-msgstr ""
+msgstr "了解關於環境的更多資訊"
msgid "ClusterIntegration|Learn more about security configuration"
-msgstr ""
+msgstr "了解更多安全性設定"
msgid "ClusterIntegration|Machine type"
msgstr "機器型別"
msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create Kubernetes clusters"
-msgstr ""
+msgstr "è«‹ç¢ºèªæ‚¨çš„帳戶中 %{link_to_requirements} 是å¦å¯ä»¥å»ºç«‹ Kubernetes å¢é›†"
msgid "ClusterIntegration|Manage"
-msgstr ""
+msgstr "管ç†"
msgid "ClusterIntegration|Manage your Kubernetes cluster by visiting %{link_gke}"
-msgstr ""
+msgstr "請至 %{link_gke} ç®¡ç†æ‚¨çš„ Kubernetes å¢é›†"
msgid "ClusterIntegration|More information"
-msgstr ""
+msgstr "詳細資料"
-msgid "ClusterIntegration|Multiple Kubernetes clusters are available in GitLab Enterprise Edition Premium and Ultimate"
-msgstr ""
+msgid "ClusterIntegration|No machine types matched your search"
+msgstr "æ²’æœ‰æ©Ÿå™¨é¡žåž‹ç¬¦åˆæ‚¨çš„æœå°‹"
+
+msgid "ClusterIntegration|No projects found"
+msgstr "找ä¸åˆ°ä»»ä½•專案"
+
+msgid "ClusterIntegration|No projects matched your search"
+msgstr "æ²’æœ‰ä»»ä½•å°ˆæ¡ˆç¬¦åˆæ‚¨çš„æœå°‹"
+
+msgid "ClusterIntegration|No zones matched your search"
+msgstr "沒有å€åŸŸç¬¦åˆæ‚¨çš„æœå°‹"
msgid "ClusterIntegration|Note:"
-msgstr ""
+msgstr "注æ„:"
msgid "ClusterIntegration|Number of nodes"
msgstr "所有的端點數é‡"
msgid "ClusterIntegration|Please enter access information for your Kubernetes cluster. If you need help, you can read our %{link_to_help_page} on Kubernetes"
-msgstr ""
+msgstr "請為你的 Kubernetes å¢é›†è¼¸å…¥å­˜å–訊æ¯ï¼Œå¦‚果你需è¦å¹«åŠ©ï¼Œå¯ä»¥é–±è®€æˆ‘們關於 Kubernetes å¢é›†çš„ %{link_to_help_page}"
msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
msgstr "請確èªä½ çš„ Google 帳號是å¦ç¬¦åˆé€™äº›æ¢ä»¶"
-msgid "ClusterIntegration|Project ID"
-msgstr ""
-
msgid "ClusterIntegration|Project namespace"
-msgstr ""
+msgstr "專案命å空間"
msgid "ClusterIntegration|Project namespace (optional, unique)"
msgstr "專案命å空間(é¸å¡«ï¼Œä¸å¯é‡è¤‡ï¼‰"
msgid "ClusterIntegration|Prometheus"
-msgstr ""
+msgstr "Prometheus"
msgid "ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration."
-msgstr ""
+msgstr "閱讀我們關於 Kubernetes å¢é›†æ•´åˆçš„ %{link_to_help_page}。"
+
+msgid "ClusterIntegration|Redeem up to $500 in free credit for Google Cloud Platform"
+msgstr "為 Google Cloud Platform å…Œæ›å…費的 500 å…ƒé¡åº¦"
msgid "ClusterIntegration|Remove Kubernetes cluster integration"
-msgstr ""
+msgstr "刪除 Kubernetes å¢é›†æ•´åˆ"
msgid "ClusterIntegration|Remove integration"
msgstr "刪除整åˆ"
msgid "ClusterIntegration|Remove this Kubernetes cluster's configuration from this project. This will not delete your actual Kubernetes cluster."
-msgstr ""
+msgstr "刪除此 Kubernetes å¢é›†è¨­å®šï¼Œé€™ä¸æœƒåˆªé™¤å¯¦éš›çš„ Kubernetes å¢é›†ã€‚"
msgid "ClusterIntegration|Request to begin installing failed"
-msgstr ""
+msgstr "請求安è£å¤±æ•—"
msgid "ClusterIntegration|Save changes"
-msgstr ""
+msgstr "儲存修改"
+
+msgid "ClusterIntegration|Search machine types"
+msgstr "æœå°‹æ©Ÿå™¨é¡žåž‹"
+
+msgid "ClusterIntegration|Search projects"
+msgstr "æœå°‹å°ˆæ¡ˆ"
+
+msgid "ClusterIntegration|Search zones"
+msgstr "æœå°‹å€åŸŸ"
msgid "ClusterIntegration|Security"
-msgstr ""
+msgstr "安全"
msgid "ClusterIntegration|See and edit the details for your Kubernetes cluster"
-msgstr ""
+msgstr "查看與編輯您的 Kubernetes å¢é›†è©³ç´°è³‡è¨Š"
+
+msgid "ClusterIntegration|Select machine type"
+msgstr "鏿“‡æ©Ÿå™¨é¡žåž‹"
+
+msgid "ClusterIntegration|Select project"
+msgstr "鏿“‡å°ˆæ¡ˆ"
-msgid "ClusterIntegration|See machine types"
-msgstr "查看機器型別"
+msgid "ClusterIntegration|Select project and zone to choose machine type"
+msgstr "鏿“‡å°ˆæ¡ˆèˆ‡ä½ç½®ä¾†é¸æ“‡æ©Ÿå™¨é¡žåž‹"
-msgid "ClusterIntegration|See your projects"
-msgstr "查看您的專案"
+msgid "ClusterIntegration|Select project to choose zone"
+msgstr "鏿“‡å°ˆæ¡ˆä¾†é¸æ“‡æ™‚å€"
-msgid "ClusterIntegration|See zones"
-msgstr "查看å€åŸŸ"
+msgid "ClusterIntegration|Select zone"
+msgstr "鏿“‡å€åŸŸ"
+
+msgid "ClusterIntegration|Select zone to choose machine type"
+msgstr "鏿“‡å€åŸŸä¾†é¸æ“‡æ©Ÿå™¨é¡žåž‹"
msgid "ClusterIntegration|Service token"
-msgstr ""
+msgstr "æœå‹™æ¬Šæ–"
msgid "ClusterIntegration|Show"
-msgstr ""
+msgstr "顯示"
msgid "ClusterIntegration|Something went wrong on our end."
msgstr "內部發生了錯誤"
msgid "ClusterIntegration|Something went wrong while creating your Kubernetes cluster on Google Kubernetes Engine"
-msgstr ""
+msgstr "在 Google Kubernetes Engine 上建立å¢é›†æ™‚發生錯誤"
msgid "ClusterIntegration|Something went wrong while installing %{title}"
-msgstr ""
+msgstr "å®‰è£ %{title} 時發生錯誤"
msgid "ClusterIntegration|The default cluster configuration grants access to a wide set of functionalities needed to successfully build and deploy a containerised application."
-msgstr ""
+msgstr "é è¨­çš„å¢é›†è¨­å®šï¼Œå…許存å–廣泛的功能,用以建置和部署å¯ä»¥ä½¿ç”¨å®¹å™¨æŠ€è¡“擴充的應用程å¼ã€‚"
msgid "ClusterIntegration|This account must have permissions to create a Kubernetes cluster in the %{link_to_container_project} specified below"
-msgstr ""
+msgstr "æ­¤å¸³æˆ¶å¿…é ˆæ“æœ‰åœ¨ %{link_to_container_project} 中建立 Kubernetes å¢é›†çš„æ¬Šé™"
msgid "ClusterIntegration|Toggle Kubernetes Cluster"
-msgstr ""
+msgstr "åˆ‡æ› Kubernetes å¢é›†"
msgid "ClusterIntegration|Toggle Kubernetes cluster"
-msgstr ""
+msgstr "åˆ‡æ› Kubernetes å¢é›†"
msgid "ClusterIntegration|Token"
-msgstr ""
+msgstr "權æ–"
+
+msgid "ClusterIntegration|Validating project billing status"
+msgstr "正在驗證è£ç½®è¨ˆè²»ç‹€æ…‹"
msgid "ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
-msgstr ""
+msgstr "ç•¶å¢é›†é€£çµåˆ°æ­¤å°ˆæ¡ˆï¼Œä½ å¯ä»¥ä½¿ç”¨è¤‡é–±æ‡‰ç”¨ï¼Œéƒ¨ç½²ä½ çš„æ‡‰ç”¨ç¨‹å¼ï¼ŒåŸ·è¡Œä½ çš„æµæ°´ç·šï¼Œé‚„æœ‰æ›´å¤šå®¹æ˜“ä¸Šæ‰‹çš„æ–¹å¼å¯ä»¥ä½¿ç”¨ã€‚"
msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}"
-msgstr ""
+msgstr "您的帳號必須有 %{link_to_kubernetes_engine}"
msgid "ClusterIntegration|Zone"
msgstr "å€åŸŸ"
msgid "ClusterIntegration|access to Google Kubernetes Engine"
-msgstr ""
+msgstr "å­˜å– Google Container Engine"
msgid "ClusterIntegration|check the pricing here"
-msgstr ""
+msgstr "在這裡檢查價格"
msgid "ClusterIntegration|documentation"
-msgstr ""
+msgstr "文件"
msgid "ClusterIntegration|help page"
msgstr "說明é é¢"
msgid "ClusterIntegration|installing applications"
-msgstr ""
+msgstr "å®‰è£æ‡‰ç”¨ç¨‹åº"
msgid "ClusterIntegration|meets the requirements"
msgstr "符åˆéœ€æ±‚"
@@ -1207,25 +1403,31 @@ msgstr "符åˆéœ€æ±‚"
msgid "ClusterIntegration|properly configured"
msgstr "設定正確"
+msgid "ClusterIntegration|sign up"
+msgstr "註冊"
+
msgid "Collapse"
-msgstr ""
+msgstr "æ”¶èµ·"
-msgid "Comment and resolve discussion"
-msgstr ""
+msgid "Collapse sidebar"
+msgstr "æ”¶èµ·å´é‚Šæ¬„"
-msgid "Comment and unresolve discussion"
-msgstr ""
+msgid "Comment & resolve discussion"
+msgstr "評論並關閉討論"
+
+msgid "Comment & unresolve discussion"
+msgstr "è©•è«–ä¸¦é‡æ–°è¨Žè«–"
msgid "Comments"
msgstr "留言"
msgid "Commit"
msgid_plural "Commits"
-msgstr[0] "更動記錄 (commit) "
+msgstr[0] "更動記錄"
msgid "Commit (%{commit_count})"
msgid_plural "Commits (%{commit_count})"
-msgstr[0] ""
+msgstr[0] "更動紀錄 (%{commit_count})"
msgid "Commit Message"
msgstr "更動訊æ¯"
@@ -1234,13 +1436,13 @@ msgid "Commit duration in minutes for last 30 commits"
msgstr "最近 30 次更動所花費的時間(分é˜ï¼‰"
msgid "Commit message"
-msgstr "更動說明 (commit) "
+msgstr "更動說明"
msgid "Commit statistics for %{ref} %{start_time} - %{end_time}"
-msgstr ""
+msgstr "更動紀錄統計 %{ref} %{start_time} - %{end_time}"
msgid "Commit to %{branchName} branch"
-msgstr ""
+msgstr "æäº¤è‡³ %{branchName} 分支"
msgid "CommitBoxTitle|Commit"
msgstr "é€äº¤"
@@ -1249,103 +1451,100 @@ msgid "CommitMessage|Add %{file_name}"
msgstr "建立 %{file_name}"
msgid "Commits"
-msgstr "更動記錄 (commit) "
+msgstr "更動記錄"
msgid "Commits feed"
-msgstr "æ›´å‹•æ‘˜è¦ (commit feed)"
+msgstr "更動摘è¦"
msgid "Commits per day hour (UTC)"
-msgstr ""
+msgstr "ä¾å°æ™‚統計æäº¤æ¬¡æ•¸(UTC)"
msgid "Commits per day of month"
-msgstr ""
+msgstr "便—¥æœŸçµ±è¨ˆæäº¤æ¬¡æ•¸"
msgid "Commits per weekday"
-msgstr ""
+msgstr "便˜ŸæœŸçµ±è¨ˆæäº¤æ¬¡æ•¸"
msgid "Commits|An error occurred while fetching merge requests data."
-msgstr ""
+msgstr "讀å–請求åˆä½µè³‡æ–™æ™‚發生錯誤"
msgid "Commits|Commit: %{commitText}"
-msgstr ""
+msgstr "更動紀錄: %{commitText}"
msgid "Commits|History"
-msgstr "更動紀錄 (commit)"
+msgstr "æ­·å²ç´€éŒ„"
msgid "Commits|No related merge requests found"
-msgstr ""
+msgstr "找ä¸åˆ°ç›¸é—œçš„åˆä½µè«‹æ±‚"
msgid "Committed by"
msgstr "é€äº¤è€…為 "
+msgid "Commit…"
+msgstr "æäº¤â€¦"
+
msgid "Compare"
msgstr "比較"
msgid "Compare Git revisions"
-msgstr ""
+msgstr "比較 Git 修訂版本"
msgid "Compare Revisions"
-msgstr ""
+msgstr "比較修訂版本"
msgid "Compare changes with the last commit"
-msgstr ""
+msgstr "與上一次的更動紀錄進行比較"
msgid "Compare changes with the merge request target branch"
-msgstr ""
+msgstr "將更改與åˆä½µè«‹æ±‚目標分支進行比較"
msgid "CompareBranches|%{source_branch} and %{target_branch} are the same."
-msgstr ""
+msgstr "%{source_branch} 和 %{target_branch} 是一樣的"
msgid "CompareBranches|Compare"
-msgstr ""
+msgstr "比較"
msgid "CompareBranches|Source"
-msgstr ""
+msgstr "來æº"
msgid "CompareBranches|Target"
-msgstr ""
+msgstr "目標"
msgid "CompareBranches|There isn't anything to compare."
-msgstr ""
+msgstr "沒有任何地方å¯ä»¥æ¯”較的"
msgid "Confidential"
-msgstr ""
+msgstr "機密"
msgid "Confidentiality"
-msgstr ""
+msgstr "機密的"
msgid "Configure Gitaly timeouts."
-msgstr ""
+msgstr "設定 Gitaly å»¶é²éŽä¹…。"
msgid "Configure Sidekiq job throttling."
-msgstr ""
+msgstr "設定 Sidekiq 工作é™åˆ¶ã€‚"
msgid "Configure automatic git checks and housekeeping on repositories."
-msgstr ""
+msgstr "於檔案庫上設定自動 Git 檢查與內務管ç†"
msgid "Configure limits for web and API requests."
-msgstr ""
+msgstr "為網路與 API 請求設定é™åˆ¶ã€‚"
+
+msgid "Configure push mirrors."
+msgstr "設定推é€çš„é¡åƒã€‚"
msgid "Configure storage path and circuit breaker settings."
-msgstr ""
+msgstr "設定儲存路徑與斷路器設定。"
msgid "Configure the way a user creates a new account."
-msgstr ""
+msgstr "設定使用者建立新帳號的方å¼ã€‚"
msgid "Connect"
-msgstr ""
-
-msgid "Connect all repositories"
-msgstr ""
+msgstr "連線"
msgid "Connect repositories from GitHub"
-msgstr ""
-
-msgid "Connect your external repositories, and CI/CD pipelines will run for new commits. A GitLab project will be created with only CI/CD features enabled."
-msgstr ""
-
-msgid "Connecting..."
-msgstr ""
+msgstr "é€£çµ GitHub 檔案庫"
msgid "Container Registry"
msgstr "Container Registry"
@@ -1369,16 +1568,16 @@ msgid "ContainerRegistry|No tags in Container Registry for this container image.
msgstr "在這個 Container Registry 中ä¸åŒ…å«ä»»ä½•æœ‰æ¨™ç±¤çš„å®¹å™¨æ˜ åƒæª”"
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} æŒ‡ä»¤ä¾†è‡ªç”±å»ºç«‹å’Œä¸Šå‚³å®¹å™¨æ˜ åƒæª” (container image)"
+msgstr "當您登入後,您å¯ä»¥é€éŽ %{build} å’Œ %{push} æŒ‡ä»¤ä¾†è‡ªç”±å»ºç«‹å’Œä¸Šå‚³å®¹å™¨æ˜ åƒæª”"
msgid "ContainerRegistry|Remove repository"
-msgstr "刪除檔案庫(repository)"
+msgstr "刪除檔案庫"
msgid "ContainerRegistry|Remove tag"
msgstr "刪除標籤"
msgid "ContainerRegistry|Size"
-msgstr ""
+msgstr "大å°"
msgid "ContainerRegistry|Tag"
msgstr "標籤"
@@ -1392,11 +1591,20 @@ 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 Container Registry æ•´åˆåˆ° GitLab 中後,æ¯å€‹å°ˆæ¡ˆéƒ½å¯ä»¥æœ‰è‡ªå·±çš„空間來儲存 Docker çš„æ˜ åƒæª”"
+msgid "ContainerRegistry|You can also use a %{deploy_token} for read-only access to the registry images."
+msgstr "您也å¯ä»¥ä½¿ç”¨ %{deploy_token} 為 Registry æ˜ åƒæª”進行唯讀存å–"
+
+msgid "Continue"
+msgstr "繼續"
+
msgid "Continuous Integration and Deployment"
-msgstr ""
+msgstr "æŒçºŒæ•´åˆèˆ‡éƒ¨å±¬"
+
+msgid "Contribute to GitLab"
+msgstr "è²¢ç»çµ¦GitLab"
msgid "Contribution"
-msgstr ""
+msgstr "è²¢ç»"
msgid "Contribution guide"
msgstr "å”作指å—"
@@ -1405,85 +1613,85 @@ msgid "Contributors"
msgstr "å”作者"
msgid "ContributorsPage|%{startDate} – %{endDate}"
-msgstr ""
+msgstr "%{startDate} - %{endDate}"
msgid "ContributorsPage|Building repository graph."
-msgstr ""
+msgstr "建立檔案庫的圖形"
msgid "ContributorsPage|Commits to %{branch_name}, excluding merge commits. Limited to 6,000 commits."
-msgstr ""
+msgstr "æäº¤åˆ°%{branch_name},ä¸åŒ…å«åˆä½µæäº¤ï¼Œé™åˆ¶æ–¼ 6,000 次更動。"
msgid "ContributorsPage|Please wait a moment, this page will automatically refresh when ready."
-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 ""
+msgstr "è«‹ç¨ç­‰ç‰‡åˆ»ï¼Œé€™å€‹é é¢æœƒæº–備好時自動刷新。"
msgid "Copy URL to clipboard"
msgstr "複製網å€åˆ°å‰ªè²¼ç°¿"
msgid "Copy branch name to clipboard"
-msgstr ""
+msgstr "複製分支å稱"
msgid "Copy command to clipboard"
-msgstr ""
+msgstr "複製指令"
msgid "Copy commit SHA to clipboard"
-msgstr "複製更動記錄 (commit) 的 SHA 值到剪貼簿"
+msgstr "複製更動記錄的 SHA 值到剪貼簿"
+
+msgid "Copy file name to clipboard"
+msgstr "將檔案å稱複製到剪貼簿"
+
+msgid "Copy file path to clipboard"
+msgstr "複製檔案路徑到剪貼簿"
msgid "Copy reference to clipboard"
-msgstr ""
+msgstr "複製連çµ"
+
+msgid "Copy to clipboard"
+msgstr "複製至剪貼簿"
msgid "Create"
-msgstr ""
+msgstr "建立"
msgid "Create New Directory"
msgstr "建立新目錄"
msgid "Create a new branch"
-msgstr ""
+msgstr "建立新分支"
msgid "Create a new branch and merge request"
-msgstr ""
+msgstr "建立新分支åŠåˆä½µè«‹æ±‚"
msgid "Create a personal access token on your account to pull or push via %{protocol}."
-msgstr "å»ºç«‹å€‹äººå­˜å–æ†‘è­‰ (access token) 以使用 %{protocol} 來上傳 (push) 或下載 (pull) 。"
+msgstr "å»ºç«‹å€‹äººå­˜å–æ†‘證以使用 %{protocol} 來上傳或下載。"
msgid "Create branch"
-msgstr ""
+msgstr "建立分支"
+
+msgid "Create commit"
+msgstr "建立æäº¤"
msgid "Create directory"
msgstr "建立目錄"
msgid "Create empty repository"
-msgstr ""
-
-msgid "Create epic"
-msgstr ""
+msgstr "建立空的檔案庫"
msgid "Create file"
msgstr "新增檔案"
msgid "Create group label"
-msgstr ""
+msgstr "建立群組標籤"
msgid "Create lists from labels. Issues with that label appear in that list."
-msgstr ""
+msgstr "å»ºç«‹æ¨™ç±¤åˆ—è¡¨ã€‚å«æœ‰æ­¤æ¨™ç±¤çš„議題將會顯示於清單中。"
msgid "Create merge request"
-msgstr "發出åˆä½µè«‹æ±‚ (merge request) "
+msgstr "發出åˆä½µè«‹æ±‚"
msgid "Create merge request and branch"
-msgstr ""
+msgstr "建立åˆä½µè«‹æ±‚åŠåˆ†æ”¯"
msgid "Create new branch"
-msgstr "新增分支(branch)"
+msgstr "新增分支"
msgid "Create new directory"
msgstr "新增資料夾"
@@ -1492,31 +1700,28 @@ msgid "Create new file"
msgstr "新增檔案"
msgid "Create new label"
-msgstr ""
+msgstr "建立新的標籤"
msgid "Create new..."
msgstr "建立..."
msgid "Create project label"
-msgstr ""
+msgstr "建立專案標籤"
msgid "CreateNewFork|Fork"
-msgstr "分支 (fork) "
+msgstr "分支"
msgid "CreateTag|Tag"
msgstr "建立標籤"
msgid "CreateTokenToCloneLink|create a personal access token"
-msgstr "å»ºç«‹å€‹äººå­˜å–æ†‘è­‰ (access token)"
-
-msgid "Creates a new branch from %{branchName}"
-msgstr ""
+msgstr "å»ºç«‹å€‹äººå­˜å–æ†‘è­‰"
-msgid "Creates a new branch from %{branchName} and re-directs to create a new merge request"
-msgstr ""
+msgid "Created"
+msgstr "已建立"
-msgid "Creating epic"
-msgstr ""
+msgid "Created by me"
+msgstr "由我創建"
msgid "Cron Timezone"
msgstr "Cron 時å€"
@@ -1524,8 +1729,14 @@ msgstr "Cron 時å€"
msgid "Cron syntax"
msgstr "Cron 語法"
-msgid "Current node"
-msgstr ""
+msgid "CurrentUser|Profile"
+msgstr "個人資料"
+
+msgid "CurrentUser|Settings"
+msgstr "設定"
+
+msgid "Custom CI config path"
+msgstr "自定義 CI é…置路徑"
msgid "Custom notification events"
msgstr "自訂事件通知"
@@ -1533,9 +1744,6 @@ 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 "Customize colors"
-msgstr ""
-
msgid "Cycle Analytics"
msgstr "週期分æž"
@@ -1543,7 +1751,7 @@ msgid "CycleAnalyticsStage|Code"
msgstr "程å¼é–‹ç™¼"
msgid "CycleAnalyticsStage|Issue"
-msgstr "議題 (issue) "
+msgstr "議題"
msgid "CycleAnalyticsStage|Plan"
msgstr "計劃"
@@ -1567,13 +1775,13 @@ msgid "DashboardProjects|Personal"
msgstr "個人"
msgid "Dec"
-msgstr ""
+msgstr "å二月"
msgid "December"
-msgstr ""
+msgstr "å二月"
-msgid "Default classification label"
-msgstr ""
+msgid "Decline and sign out"
+msgstr "拒絕並登出"
msgid "Define a custom pattern with cron syntax"
msgstr "使用 Cron 語法自訂排程"
@@ -1581,6 +1789,9 @@ msgstr "使用 Cron 語法自訂排程"
msgid "Delete"
msgstr "刪除"
+msgid "Delete list"
+msgstr "刪除清單"
+
msgid "Deploy"
msgid_plural "Deploys"
msgstr[0] "部署"
@@ -1588,44 +1799,170 @@ msgstr[0] "部署"
msgid "Deploy Keys"
msgstr "部署金鑰"
+msgid "DeployKeys|+%{count} others"
+msgstr "其他 +%{count} 個"
+
+msgid "DeployKeys|Current project"
+msgstr "ç›®å‰å°ˆæ¡ˆ"
+
+msgid "DeployKeys|Deploy key"
+msgstr "部屬金鑰"
+
+msgid "DeployKeys|Enabled deploy keys"
+msgstr "已啟用部署金鑰"
+
+msgid "DeployKeys|Error enabling deploy key"
+msgstr "啟用部署金鑰時失敗"
+
+msgid "DeployKeys|Error getting deploy keys"
+msgstr "å–得部署金鑰時失敗"
+
+msgid "DeployKeys|Error removing deploy key"
+msgstr "移除部屬金鑰時失敗"
+
+msgid "DeployKeys|Expand %{count} other projects"
+msgstr "展開其他 %{count} 個專案"
+
+msgid "DeployKeys|Loading deploy keys"
+msgstr "正在載入部署金鑰"
+
+msgid "DeployKeys|No deploy keys found. Create one with the form above."
+msgstr "找ä¸åˆ°ä»»ä½•部屬金鑰。請使用上é¢çš„表格建立一個。"
+
+msgid "DeployKeys|Privately accessible deploy keys"
+msgstr "å¯å­˜å–çš„ç§äººéƒ¨ç½²é‡‘é‘°"
+
+msgid "DeployKeys|Project usage"
+msgstr "專案用é‡"
+
+msgid "DeployKeys|Publicly accessible deploy keys"
+msgstr "å¯å­˜å–的公開部署金鑰"
+
+msgid "DeployKeys|Read access only"
+msgstr "åªè®€å­˜å–"
+
+msgid "DeployKeys|Write access allowed"
+msgstr "å·²å…許寫入權é™"
+
+msgid "DeployKeys|You are going to remove this deploy key. Are you sure?"
+msgstr "你準備è¦ç§»é™¤é€™å€‹éƒ¨ç½²é‡‘鑰,你確定嗎?"
+
+msgid "DeployTokens|Active Deploy Tokens (%{active_tokens})"
+msgstr "啟用部屬憑證(%{active_tokens})"
+
+msgid "DeployTokens|Add a deploy token"
+msgstr "建立一個部屬憑證"
+
+msgid "DeployTokens|Allows read-only access to the registry images"
+msgstr "å…許 Registry æ˜ åƒæª”çš„åªè®€å­˜å–"
+
+msgid "DeployTokens|Allows read-only access to the repository"
+msgstr "å…許此檔案庫的åªè®€å­˜å–"
+
+msgid "DeployTokens|Copy deploy token to clipboard"
+msgstr "複製部署憑證至剪貼簿"
+
+msgid "DeployTokens|Copy username to clipboard"
+msgstr "複製使用者å稱到剪貼簿"
+
+msgid "DeployTokens|Create deploy token"
+msgstr "建立部屬憑證"
+
+msgid "DeployTokens|Created"
+msgstr "已建立"
+
+msgid "DeployTokens|Deploy Tokens"
+msgstr "部屬憑證"
+
+msgid "DeployTokens|Deploy tokens allow read-only access to your repository and registry images."
+msgstr "部屬憑證å…許您檔案庫的åªè®€å­˜å–與 Registry æ˜ åƒæª”。"
+
+msgid "DeployTokens|Expires"
+msgstr "éŽæœŸçš„"
+
+msgid "DeployTokens|Name"
+msgstr "å稱"
+
+msgid "DeployTokens|Pick a name for the application, and we'll give you a unique deploy token."
+msgstr "為您的應用程å¼å–個å稱,然後我們將會給您一個ç¨ç‰¹çš„部屬憑證。"
+
+msgid "DeployTokens|Revoke"
+msgstr "撤回"
+
+msgid "DeployTokens|Revoke %{name}"
+msgstr "撤回 %{name}"
+
+msgid "DeployTokens|Scopes"
+msgstr "範åœ"
+
+msgid "DeployTokens|This action cannot be undone."
+msgstr "此動作將無法還原。"
+
+msgid "DeployTokens|This project has no active Deploy Tokens."
+msgstr "此專案沒有任何啟用的部屬憑證。"
+
+msgid "DeployTokens|Use this token as a password. Make sure you save it - you won't be able to access it again."
+msgstr "把這個憑證當åšå¯†ç¢¼ä½¿ç”¨ã€‚ä¿è­‰ä½ å„²å­˜äº†å®ƒ - å› ç‚ºä½ å°‡ç„¡æ³•å†æ¬¡å­˜å–它。"
+
+msgid "DeployTokens|Use this username as a login."
+msgstr "將使用者å稱當作登入å稱。"
+
+msgid "DeployTokens|Username"
+msgstr "使用者å稱"
+
+msgid "DeployTokens|You are about to revoke"
+msgstr "您正打算撤銷"
+
+msgid "DeployTokens|Your New Deploy Token"
+msgstr "您的新部屬憑證"
+
+msgid "DeployTokens|Your new project deploy token has been created."
+msgstr "您的新專案部屬憑證已經建立。"
+
+msgid "Deprioritize label"
+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 "Diffs|No file name available"
-msgstr ""
+msgstr "沒有å¯ç”¨çš„æª”案å稱"
+
+msgid "Diffs|Something went wrong while fetching diff lines."
+msgstr "讀å–差異檔時發生了一些錯誤."
msgid "Directory name"
msgstr "目錄å稱"
msgid "Disable"
-msgstr ""
+msgstr "åœç”¨"
-msgid "Discard draft"
-msgstr ""
+msgid "Disable for this project"
+msgstr "為此專案åœç”¨"
-msgid "Discover GitLab Geo."
-msgstr ""
+msgid "Disable group Runners"
+msgstr "åœç”¨ç¾¤çµ„執行器"
+
+msgid "Discard changes"
+msgstr "撤銷變更"
+
+msgid "Discard draft"
+msgstr "放棄è‰ç¨¿"
msgid "Dismiss Cycle Analytics introduction box"
msgstr "關閉循環分æžä»‹ç´¹è¦–窗"
-msgid "Dismiss Merge Request promotion"
-msgstr ""
-
-msgid "Documentation for popular identity providers"
-msgstr ""
+msgid "Domain"
+msgstr "網域"
msgid "Don't show again"
msgstr "ä¸å†é¡¯ç¤º"
msgid "Done"
-msgstr ""
+msgstr "完æˆ"
msgid "Download"
msgstr "下載"
@@ -1646,178 +1983,187 @@ msgid "DownloadArtifacts|Download"
msgstr "下載"
msgid "DownloadCommit|Email Patches"
-msgstr "é›»å­éƒµä»¶ä¿®è£œæª”案 (patch)"
+msgstr "é›»å­éƒµä»¶ä¿®è£œæª”案"
msgid "DownloadCommit|Plain Diff"
-msgstr "差異檔 (diff)"
+msgstr "差異檔"
msgid "DownloadSource|Download"
msgstr "下載原始碼"
msgid "Downvotes"
-msgstr ""
+msgstr "噓"
msgid "Due date"
-msgstr ""
+msgstr "截止日期"
-msgid "During this process, you’ll be asked for URLs from GitLab’s side. Use the URLs shown below."
-msgstr ""
+msgid "Each Runner can be in one of the following states:"
+msgstr "æ¯å€‹åŸ·è¡Œå™¨å¯ä»¥è™•於以下狀態之一:"
msgid "Edit"
msgstr "編輯"
+msgid "Edit Label"
+msgstr "編輯標籤"
+
msgid "Edit Pipeline Schedule %{id}"
-msgstr "編輯 %{id} æµæ°´ç·š (pipeline) 排程"
+msgstr "編輯 %{id} æµæ°´ç·šæŽ’程"
msgid "Edit files in the editor and commit changes here"
-msgstr ""
-
-msgid "Editing"
-msgstr ""
-
-msgid "Elasticsearch"
-msgstr ""
+msgstr "在編輯器中編輯æäº¤æ›´å‹•紀錄"
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
-msgstr ""
+msgid "Edit identity for %{user_name}"
+msgstr "編輯 %{user_name} 的身份"
msgid "Email"
-msgstr ""
+msgstr "é›»å­ä¿¡ç®±"
+
+msgid "Email patch"
+msgstr "é›»å­éƒµä»¶è£œä¸"
msgid "Emails"
msgstr "é›»å­éƒµä»¶"
+msgid "Embed"
+msgstr "內嵌"
+
msgid "Enable"
-msgstr ""
+msgstr "啟用"
msgid "Enable Auto DevOps"
-msgstr ""
-
-msgid "Enable SAML authentication for this group"
-msgstr ""
+msgstr "啟用自動 DevOps"
msgid "Enable Sentry for error reporting and logging."
-msgstr ""
+msgstr "為錯誤報告與日誌啟用 Sentry。"
msgid "Enable and configure InfluxDB metrics."
-msgstr ""
+msgstr "啟用與設定 InfluxDB 指標。"
msgid "Enable and configure Prometheus metrics."
-msgstr ""
+msgstr "啟用與設定 Prometheus 指標。"
-msgid "Enable classification control using an external service"
-msgstr ""
+msgid "Enable for this project"
+msgstr "為此專案啟用"
+
+msgid "Enable group Runners"
+msgstr "啟用群組執行器"
+
+msgid "Enable or disable certain group features and choose access levels."
+msgstr "啟用或åœç”¨æŸäº›ç¾¤çµ„åŠŸèƒ½ï¼Œä¸¦é¸æ“‡å­˜å–等級。"
msgid "Enable or disable version check and usage ping."
-msgstr ""
+msgstr "啟用或åœç”¨ç‰ˆæœ¬æª¢æŸ¥å’Œä½¿ç”¨ ping。"
msgid "Enable reCAPTCHA or Akismet and set IP limits."
-msgstr ""
+msgstr "啟用 reCAPTCHA 或 Akismet 並設定 IP 上é™ã€‚"
msgid "Enable the Performance Bar for a given group."
-msgstr ""
+msgstr "啟用æä¾›ç¾¤çµ„的效能桿。"
-msgid "Enabled"
-msgstr ""
+msgid "Ends at (UTC)"
+msgstr "æ–¼ (UTC) çµæŸ"
+
+msgid "Environments"
+msgstr "環境"
msgid "Environments|An error occurred while fetching the environments."
-msgstr ""
+msgstr "ç²å–環境時發生錯誤。"
msgid "Environments|An error occurred while making the request."
-msgstr ""
+msgstr "發é€è«‹æ±‚時發生錯誤"
msgid "Environments|Commit"
-msgstr ""
+msgstr "æ›´å‹•"
msgid "Environments|Deployment"
-msgstr ""
+msgstr "部署"
msgid "Environments|Environment"
-msgstr ""
+msgstr "環境"
msgid "Environments|Environments"
-msgstr ""
+msgstr "環境"
msgid "Environments|Job"
-msgstr ""
+msgstr "任務"
msgid "Environments|New environment"
-msgstr ""
+msgstr "新建環境"
msgid "Environments|No deployments yet"
-msgstr ""
+msgstr "尚未部署"
msgid "Environments|Open"
-msgstr ""
+msgstr "開啟"
msgid "Environments|Re-deploy"
-msgstr ""
+msgstr "釿–°éƒ¨ç½²"
msgid "Environments|Read more about environments"
-msgstr ""
+msgstr "了解有關環境的更多訊æ¯"
msgid "Environments|Rollback"
-msgstr ""
+msgstr "還原"
msgid "Environments|Show all"
-msgstr ""
+msgstr "顯示全部"
msgid "Environments|Updated"
-msgstr ""
+msgstr "已更新"
msgid "Environments|You don't have any environments right now."
-msgstr ""
-
-msgid "Epic will be removed! Are you sure?"
-msgstr ""
-
-msgid "Epics"
-msgstr ""
-
-msgid "Epics Roadmap"
-msgstr ""
-
-msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
-msgstr ""
+msgstr "你還沒有設置環境"
msgid "Error Reporting and Logging"
-msgstr ""
-
-msgid "Error checking branch data. Please try again."
-msgstr ""
+msgstr "錯誤報告與日誌"
msgid "Error committing changes. Please try again."
-msgstr ""
-
-msgid "Error creating epic"
-msgstr ""
+msgstr "æäº¤æ”¹è®Šæ™‚發生錯誤,請ç¨å¾Œå†è©¦ã€‚"
msgid "Error fetching contributors data."
-msgstr ""
+msgstr "讀å–è²¢ç»è€…資料時發生錯誤。"
+
+msgid "Error fetching job trace"
+msgstr "追蹤讀å–任務錯誤"
msgid "Error fetching labels."
-msgstr ""
+msgstr "è®€å–æ¨™ç±¤æ™‚發生錯誤。"
msgid "Error fetching network graph."
-msgstr ""
+msgstr "讀å–分支圖時發生錯誤。"
msgid "Error fetching refs"
-msgstr ""
+msgstr "讀å–分支資料時發生錯誤。"
msgid "Error fetching usage ping data."
-msgstr ""
+msgstr "讀å–ä½¿ç”¨æƒ…æ³ ping 資料時發生錯誤。"
+
+msgid "Error loading branch data. Please try again."
+msgstr "載入分支資料時錯誤,請é‡è©¦ã€‚"
+
+msgid "Error loading last commit."
+msgstr "載入最新更動紀錄時失敗。"
+
+msgid "Error loading merge requests."
+msgstr "載入åˆä½µè«‹æ±‚時錯誤。"
+
+msgid "Error loading project data. Please try again."
+msgstr "載入專案資料時錯誤,請é‡è©¦ã€‚"
msgid "Error occurred when toggling the notification subscription"
-msgstr ""
+msgstr "切æ›è¨‚閱通知時發生錯誤"
msgid "Error saving label update."
-msgstr ""
+msgstr "更新標籤時發生錯誤。"
msgid "Error updating status for all todos."
-msgstr ""
+msgstr "更新代辦事項清單時發生錯誤"
msgid "Error updating todo status."
-msgstr ""
+msgstr "更新代辦事項時發生錯誤。"
+
+msgid "Estimated"
+msgstr "é ä¼°"
msgid "EventFilterBy|Filter by all"
msgstr "顯示全部"
@@ -1826,13 +2172,13 @@ msgid "EventFilterBy|Filter by comments"
msgstr "以留言篩é¸"
msgid "EventFilterBy|Filter by issue events"
-msgstr "以議題 (issue) 事件篩é¸"
+msgstr "以議題事件篩é¸"
msgid "EventFilterBy|Filter by merge events"
-msgstr "以åˆä½µ (merge) 事件篩é¸"
+msgstr "以åˆä½µäº‹ä»¶ç¯©é¸"
msgid "EventFilterBy|Filter by push events"
-msgstr "ä»¥æŽ¨é€ (push) 事件篩é¸"
+msgstr "以推é€äº‹ä»¶ç¯©é¸"
msgid "EventFilterBy|Filter by team"
msgstr "以團隊篩é¸"
@@ -1847,7 +2193,13 @@ msgid "Every week (Sundays at 4:00am)"
msgstr "æ¯é€±åŸ·è¡Œï¼ˆé€±æ—¥æ·©æ™¨ 四點)"
msgid "Expand"
-msgstr ""
+msgstr "展開"
+
+msgid "Expand all"
+msgstr "展開全部"
+
+msgid "Expand sidebar"
+msgstr "展開å´é‚Šæ¬„"
msgid "Explore projects"
msgstr "ç€è¦½å°ˆæ¡ˆ"
@@ -1855,65 +2207,47 @@ msgstr "ç€è¦½å°ˆæ¡ˆ"
msgid "Explore public groups"
msgstr "æœå°‹å…¬é–‹çš„群組"
-msgid "External Classification Policy Authorization"
-msgstr ""
-
-msgid "External authentication"
-msgstr ""
-
-msgid "External authorization denied access to this project"
-msgstr ""
-
-msgid "External authorization request timeout"
-msgstr ""
-
-msgid "ExternalAuthorizationService|Classification Label"
-msgstr ""
-
-msgid "ExternalAuthorizationService|Classification label"
-msgstr ""
-
-msgid "ExternalAuthorizationService|When no classification label is set the default label `%{default_label}` will be used."
-msgstr ""
-
msgid "Failed"
-msgstr ""
+msgstr "失敗"
msgid "Failed Jobs"
-msgstr ""
+msgstr "失敗的任務"
msgid "Failed to change the owner"
msgstr "無法變更所有權"
+msgid "Failed to check related branches."
+msgstr "檢查相關分支失敗。"
+
msgid "Failed to remove issue from board, please try again."
-msgstr ""
+msgstr "從看æ¿åˆªé™¤è­°é¡Œæ™‚發生錯誤,請ç¨å€™é‡è©¦"
msgid "Failed to remove the pipeline schedule"
-msgstr "ç„¡æ³•åˆªé™¤æµæ°´ç·š (pipeline) 排程"
+msgstr "ç„¡æ³•åˆªé™¤æµæ°´ç·šæŽ’程"
msgid "Failed to update issues, please try again."
-msgstr ""
+msgstr "更新議題時發生錯誤,請ç¨å¾Œé‡è©¦ã€‚"
+
+msgid "Failure"
+msgstr "失敗"
+
+msgid "Faster as it re-uses the project workspace (falling back to clone if it doesn't exist)"
+msgstr "è¼ƒå¿«ï¼Œå› ç‚ºå®ƒå†æ¬¡ä½¿ç”¨å°ˆæ¡ˆå·¥ä½œç©ºé–“ (如果它ä¸å­˜åœ¨ï¼Œå‰‡è¿”回到複製)"
msgid "Feb"
-msgstr ""
+msgstr "二月"
msgid "February"
-msgstr ""
+msgstr "二月"
msgid "Fields on this page are now uneditable, you can configure"
-msgstr ""
-
-msgid "File name"
-msgstr "檔案å稱"
+msgstr "æ­¤é é¢æ¬„ä½ç¾åœ¨ç„¡æ³•編輯,你å¯ä»¥è¨­ç½®"
msgid "Files"
msgstr "檔案"
msgid "Files (%{human_size})"
-msgstr ""
-
-msgid "Fill in the fields below, turn on <strong>%{enable_label}</strong>, and press <strong>%{save_changes}</strong>"
-msgstr ""
+msgstr "檔案 (%{human_size})"
msgid "Filter by commit message"
msgstr "以更動說明篩é¸"
@@ -1925,284 +2259,140 @@ msgid "Find file"
msgstr "æœå°‹æª”案"
msgid "Finished"
-msgstr ""
+msgstr "已完æˆ"
msgid "FirstPushedBy|First"
-msgstr "é¦–æ¬¡æŽ¨é€ (push) "
+msgstr "首次"
msgid "FirstPushedBy|pushed by"
-msgstr "推é€è€… (push) :"
+msgstr "推é€è€…:"
-msgid "Font Color"
-msgstr ""
+msgid "For internal projects, any logged in user can view pipelines and access job details (output logs and artifacts)"
+msgstr "å°æ–¼å…§éƒ¨å°ˆæ¡ˆï¼Œä»»ä½•登錄的使用者都å¯ä»¥æŸ¥çœ‹æµæ°´ç·šä¸¦å­˜å–工作詳細資訊 (輸出日誌和產物)"
-msgid "Footer message"
-msgstr ""
+msgid "For private projects, any member (guest or higher) can view pipelines and access job details (output logs and artifacts)"
+msgstr "å°æ–¼ç§äººå°ˆæ¡ˆï¼Œä»»ä½•æˆå“¡ (訪客或以上) 都å¯ä»¥æŸ¥çœ‹æµæ°´ç·šä¸¦å­˜å–工作詳細資訊 (輸出日誌和產物)"
+
+msgid "For public projects, anyone can view pipelines and access job details (output logs and artifacts)"
+msgstr "å°æ–¼å…¬é–‹å°ˆæ¡ˆï¼Œä»»ä½•人都å¯ä»¥æŸ¥çœ‹æµæ°´ç·šä¸¦å­˜å–工作詳細資訊 (輸出日誌和產物)"
msgid "Fork"
msgid_plural "Forks"
-msgstr[0] "分支 (fork) "
+msgstr[0] "分支"
msgid "ForkedFromProjectPath|Forked from"
-msgstr "分支 (fork) 自"
+msgstr "分支自"
msgid "ForkedFromProjectPath|Forked from %{project_name} (deleted)"
-msgstr "從 %{project_name} Fork. (deleted)"
+msgstr "從 %{project_name} 分支. (已刪除)"
msgid "Forking in progress"
-msgstr ""
+msgstr "正在建立分å‰"
msgid "Format"
msgstr "æ ¼å¼"
+msgid "Found errors in your .gitlab-ci.yml:"
+msgstr "在您的 .gitlab-ci.yml 中找到錯誤:"
+
msgid "From %{provider_title}"
-msgstr ""
+msgstr "來自 %{provider_title}"
msgid "From issue creation until deploy to production"
-msgstr "從議題 (issue) 建立直到部署至營é‹ç’°å¢ƒ"
+msgstr "從議題建立直到部署至營é‹ç’°å¢ƒ"
msgid "From merge request merge until deploy to production"
-msgstr "從請求被åˆä½µå¾Œ (merge request merged) 直到部署至營é‹ç’°å¢ƒ"
+msgstr "從請求被åˆä½µå¾Œç›´åˆ°éƒ¨ç½²è‡³ç‡Ÿé‹ç’°å¢ƒ"
msgid "From the Kubernetes cluster details view, install Runner from the applications list"
-msgstr ""
+msgstr "從 Kubernetes å¢é›†è©³ç´°è³‡æ–™é ï¼Œå¾žæ‡‰ç”¨ç¨‹å¼åˆ—表中安è£é‹è¡Œå™¨"
msgid "GPG Keys"
msgstr "GPG 金鑰"
-msgid "Generate a default set of labels"
-msgstr ""
-
-msgid "Geo Nodes"
-msgstr ""
-
-msgid "Geo allows you to replicate your GitLab instance to other geographical locations."
-msgstr ""
-
-msgid "GeoNodeSyncStatus|Node is failing or broken."
-msgstr ""
-
-msgid "GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage."
-msgstr ""
+msgid "General"
+msgstr "一般"
-msgid "GeoNodes|Checksummed"
-msgstr ""
+msgid "General pipelines"
+msgstr "ä¸€èˆ¬æµæ°´ç·š"
-msgid "GeoNodes|Database replication lag:"
-msgstr ""
-
-msgid "GeoNodes|Disabling a node stops the sync process. Are you sure?"
-msgstr ""
-
-msgid "GeoNodes|Does not match the primary storage configuration"
-msgstr ""
-
-msgid "GeoNodes|Failed"
-msgstr ""
-
-msgid "GeoNodes|Full"
-msgstr ""
-
-msgid "GeoNodes|GitLab version does not match the primary node version"
-msgstr ""
-
-msgid "GeoNodes|GitLab version:"
-msgstr ""
-
-msgid "GeoNodes|Health status:"
-msgstr ""
-
-msgid "GeoNodes|Last event ID processed by cursor:"
-msgstr ""
-
-msgid "GeoNodes|Last event ID seen from primary:"
-msgstr ""
-
-msgid "GeoNodes|Loading nodes"
-msgstr ""
-
-msgid "GeoNodes|Local Attachments:"
-msgstr ""
-
-msgid "GeoNodes|Local LFS objects:"
-msgstr ""
-
-msgid "GeoNodes|Local job artifacts:"
-msgstr ""
-
-msgid "GeoNodes|New node"
-msgstr ""
-
-msgid "GeoNodes|Node Authentication was successfully repaired."
-msgstr ""
-
-msgid "GeoNodes|Node was successfully removed."
-msgstr ""
-
-msgid "GeoNodes|Not checksummed"
-msgstr ""
-
-msgid "GeoNodes|Out of sync"
-msgstr ""
-
-msgid "GeoNodes|Removing a node stops the sync process. Are you sure?"
-msgstr ""
-
-msgid "GeoNodes|Replication slot WAL:"
-msgstr ""
-
-msgid "GeoNodes|Replication slots:"
-msgstr ""
-
-msgid "GeoNodes|Repositories checksummed:"
-msgstr ""
-
-msgid "GeoNodes|Repositories:"
-msgstr ""
-
-msgid "GeoNodes|Repository checksums verified:"
-msgstr ""
-
-msgid "GeoNodes|Selective"
-msgstr ""
-
-msgid "GeoNodes|Something went wrong while changing node status"
-msgstr ""
-
-msgid "GeoNodes|Something went wrong while removing node"
-msgstr ""
-
-msgid "GeoNodes|Something went wrong while repairing node"
-msgstr ""
-
-msgid "GeoNodes|Storage config:"
-msgstr ""
-
-msgid "GeoNodes|Sync settings:"
-msgstr ""
-
-msgid "GeoNodes|Synced"
-msgstr ""
-
-msgid "GeoNodes|Unused slots"
-msgstr ""
-
-msgid "GeoNodes|Unverified"
-msgstr ""
-
-msgid "GeoNodes|Used slots"
-msgstr ""
-
-msgid "GeoNodes|Verified"
-msgstr ""
-
-msgid "GeoNodes|Wiki checksums verified:"
-msgstr ""
-
-msgid "GeoNodes|Wikis checksummed:"
-msgstr ""
-
-msgid "GeoNodes|Wikis:"
-msgstr ""
-
-msgid "GeoNodes|You have configured Geo nodes using an insecure HTTP connection. We recommend the use of HTTPS."
-msgstr ""
-
-msgid "Geo|All projects"
-msgstr ""
-
-msgid "Geo|File sync capacity"
-msgstr ""
-
-msgid "Geo|Groups to synchronize"
-msgstr ""
-
-msgid "Geo|Projects in certain groups"
-msgstr ""
-
-msgid "Geo|Projects in certain storage shards"
-msgstr ""
-
-msgid "Geo|Repository sync capacity"
-msgstr ""
-
-msgid "Geo|Select groups to replicate."
-msgstr ""
-
-msgid "Geo|Shards to synchronize"
-msgstr ""
+msgid "Generate a default set of labels"
+msgstr "產生é è¨­çš„æ¨™ç±¤"
msgid "Git repository URL"
-msgstr ""
+msgstr "Git 檔案庫網å€"
msgid "Git revision"
-msgstr ""
+msgstr "Git 修訂版本"
msgid "Git storage health information has been reset"
msgstr "Git 儲存空間å¥åº·æŒ‡æ•¸å·²é‡ç½®"
+msgid "Git strategy for pipelines"
+msgstr "Git æµæ°´ç·šç­–ç•¥"
+
msgid "Git version"
-msgstr ""
+msgstr "Git 版本"
msgid "GitHub import"
-msgstr ""
+msgstr "GitHub 匯入"
msgid "GitLab CI Linter has been moved"
-msgstr ""
+msgstr "GitLab CI Linter 已經æ¬ç§»"
-msgid "GitLab Geo"
-msgstr ""
+msgid "GitLab Group Runners can execute code for all the projects in this group."
+msgstr "GitLab 群組執行器å¯ä»¥ç‚ºæ­¤ç¾¤çµ„的所有專案執行程å¼ç¢¼ã€‚"
msgid "GitLab Runner section"
msgstr "GitLab Runner"
-msgid "GitLab single sign on URL"
-msgstr ""
-
msgid "Gitaly"
-msgstr ""
+msgstr "Gitaly"
msgid "Gitaly Servers"
-msgstr ""
+msgstr "Gitaly 伺æœå™¨"
+
+msgid "Gitaly|Address"
+msgstr "地å€"
+
+msgid "Go Back"
+msgstr "返回"
msgid "Go back"
-msgstr ""
+msgstr "上一é "
msgid "Go to your fork"
-msgstr "å‰å¾€æ‚¨çš„分支 (fork) "
+msgstr "å‰å¾€æ‚¨çš„分支"
msgid "GoToYourFork|Fork"
-msgstr "å‰å¾€æ‚¨çš„分支 (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}。如果您想使用此æœå‹™ï¼Œè«‹è«®è©¢ç®¡ç†å“¡ã€‚"
msgid "Got it!"
-msgstr ""
+msgstr "明白ï¼"
-msgid "GroupRoadmap|Epics let you manage your portfolio of projects more efficiently and with less effort"
-msgstr ""
+msgid "Graph"
+msgstr "圖表"
-msgid "GroupRoadmap|From %{dateWord}"
-msgstr ""
+msgid "Group CI/CD settings"
+msgstr "群組 CI / CD 設定"
-msgid "GroupRoadmap|Loading roadmap"
-msgstr ""
+msgid "Group ID"
+msgstr "群組 ID"
-msgid "GroupRoadmap|Something went wrong while fetching epics"
-msgstr ""
+msgid "Group Runners"
+msgstr "群組執行器"
-msgid "GroupRoadmap|To view the roadmap, add a planned start or finish date to one of your epics in this group or its subgroups. Only epics in the past 3 months and the next 3 months are shown &ndash; from %{startDate} to %{endDate}."
-msgstr ""
-
-msgid "GroupRoadmap|Until %{dateWord}"
-msgstr ""
+msgid "Group maintainers can register group runners in the %{link}"
+msgstr "群組維護者å¯ä»¥åœ¨ %{link} 註冊群組執行器"
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 "這個設定已經套用至 %{ancestor_group},並覆蓋了它的å­ç¾¤çµ„設定。"
@@ -2222,6 +2412,9 @@ msgstr "無法啟用上級群組的「共享群組鎖ã€ã€‚åªæœ‰ä¸Šç´šç¾¤çµ„çš„
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr "從 %{ancestor_group_name} 中移除共享群組鎖"
+msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
+msgstr "群組也å¯ä»¥å‰µå»º %{subgroup_docs_link_start}å­ç¾¤çµ„%{subgroup_docs_link_end}。"
+
msgid "GroupsEmptyState|A group is a collection of several projects."
msgstr "群組是數個專案的集åˆã€‚"
@@ -2261,12 +2454,6 @@ msgstr "ä¸å¥½æ„æ€ï¼Œæ²’有æœå°‹åˆ°ä»»ä½•ç¬¦åˆæ¢ä»¶çš„群組"
msgid "GroupsTree|Sorry, no groups or projects matched your search"
msgstr "ä¸å¥½æ„æ€ï¼Œæ²’有æœå°‹åˆ°ä»»ä½•ç¬¦åˆæ¢ä»¶çš„群組或專案"
-msgid "Have your users email"
-msgstr ""
-
-msgid "Header message"
-msgstr ""
-
msgid "Health Check"
msgstr "å¥åº·æª¢æŸ¥"
@@ -2274,7 +2461,7 @@ msgid "Health information can be retrieved from the following endpoints. More in
msgstr "å¥åº·è³‡è¨Šå¯å¾žä»¥ä¸‹é€£çµå–得。想了解更多請åƒé–±"
msgid "HealthCheck|Access token is"
-msgstr "å­˜å–æ†‘è­‰ (access token) 是"
+msgstr "å­˜å–æ†‘證是"
msgid "HealthCheck|Healthy"
msgstr "å¥åº·"
@@ -2286,17 +2473,20 @@ msgid "HealthCheck|Unhealthy"
msgstr "ä¸è‰¯"
msgid "Help"
-msgstr ""
+msgstr "説明"
msgid "Help page"
-msgstr ""
+msgstr "說明é é¢"
msgid "Help page text and support page url."
-msgstr ""
+msgstr "說明é é¢æ–‡å­—與支æ´é é¢é€£çµ"
msgid "Hide value"
msgid_plural "Hide values"
-msgstr[0] ""
+msgstr[0] "éš±è—資料"
+
+msgid "Hide whitespace changes"
+msgstr "éš±è—空白變化"
msgid "History"
msgstr "æ­·å²"
@@ -2304,66 +2494,95 @@ msgstr "æ­·å²"
msgid "Housekeeping successfully started"
msgstr "已開始維護"
-msgid "Identity provider single sign on URL"
-msgstr ""
+msgid "I accept the %{terms_link}"
+msgstr "æˆ‘æŽ¥å— %{terms_link}"
-msgid "If enabled, access to projects will be validated on an external service using their classification label."
-msgstr ""
+msgid "I accept the|Terms of Service and Privacy Policy"
+msgstr "æœå‹™æ¢æ¬¾å’Œéš±ç§æ”¿ç­–"
-msgid "If using GitHub, you’ll see pipeline statuses on GitHub for your commits and pull requests. %{more_info_link}"
-msgstr ""
+msgid "ID"
+msgstr "ID"
+
+msgid "IDE|Commit"
+msgstr "æäº¤"
+
+msgid "IDE|Edit"
+msgstr "編輯"
+
+msgid "IDE|Go back"
+msgstr "返回"
+
+msgid "IDE|Open in file view"
+msgstr "以檔案顯示開啟"
+
+msgid "IDE|Review"
+msgstr "審閱"
+
+msgid "Identifier"
+msgstr "識別碼"
+
+msgid "Identities"
+msgstr "身份"
+
+msgid "If disabled, the access level will depend on the user's permissions in the project."
+msgstr "如果ç¦ç”¨ï¼Œå­˜å–ç­‰ç´šå°‡å–æ±ºæ–¼ä½¿ç”¨è€…在專案中的權é™ã€‚"
+
+msgid "If enabled"
+msgstr "如果啟用"
msgid "If you already have files you can push them using the %{link_to_cli} below."
-msgstr ""
+msgstr "å¦‚æžœä½ å·²ç¶“æ“æœ‰æª”案,你å¯ä»¥ä½¿ç”¨ %{link_to_cli} 推é€ä»–們。"
msgid "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>."
-msgstr ""
+msgstr "如果你的 HTTP 檔案庫沒有公開存å–,請增加驗證欄ä½åˆ°ç¶²å€ä¸Šï¼š<code>https://username:password@gitlab.company.com/group/project.git</code>."
+
+msgid "ImageDiffViewer|2-up"
+msgstr "2-up"
+
+msgid "ImageDiffViewer|Onion skin"
+msgstr "Onion 主題"
+
+msgid "ImageDiffViewer|Swipe"
+msgstr "Swipe"
msgid "Import"
-msgstr ""
+msgstr "匯入"
msgid "Import all repositories"
-msgstr ""
+msgstr "匯入所有檔案庫"
msgid "Import in progress"
-msgstr ""
+msgstr "匯入中..."
msgid "Import repositories from GitHub"
-msgstr ""
+msgstr "從 GitHub 匯入檔案庫"
msgid "Import repository"
-msgstr "匯入檔案庫 (repository)"
+msgstr "匯入檔案庫"
-msgid "ImportButtons|Connect repositories from"
-msgstr ""
-
-msgid "Improve Issue boards with GitLab Enterprise Edition."
-msgstr ""
+msgid "Include a Terms of Service agreement and Privacy Policy that all users must accept."
+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 "Inline"
+msgstr "內嵌"
msgid "Install Runner on Kubernetes"
-msgstr ""
+msgstr "在 Kubernetes 上安è£é‹è¡Œå™¨"
msgid "Install a Runner compatible with GitLab CI"
msgstr "安è£èˆ‡ GitLab CI 相容的 Runner"
-msgid "Instance"
-msgid_plural "Instances"
-msgstr[0] ""
-
msgid "Instance does not support multiple Kubernetes clusters"
-msgstr ""
+msgstr "主機沒有支æ´å¤šå€‹ Kubernetes å¢é›†"
msgid "Integrations"
-msgstr ""
+msgstr "æ•´åˆ"
+
+msgid "Integrations Settings"
+msgstr "æ•´åˆè¨­å®š"
msgid "Interested parties can even contribute by pushing commits if they want to."
-msgstr ""
+msgstr "有興趣的人甚至å¯ä»¥é€šé޿ލ逿›´å‹•紀錄åšå‡ºè²¢ç»ï¼Œå¦‚果他們願æ„的話。"
msgid "Internal - The group and any internal projects can be viewed by any logged in user."
msgstr "內部 - 任何登入的使用者都å¯ä»¥æŸ¥çœ‹è©²ç¾¤çµ„åŠå…¶å°ˆæ¡ˆ"
@@ -2377,71 +2596,77 @@ msgstr "循環週期"
msgid "Introducing Cycle Analytics"
msgstr "週期分æžç°¡ä»‹"
-msgid "Issue board focus mode"
-msgstr ""
+msgid "Issue Board"
+msgstr "議題看æ¿"
msgid "Issue events"
-msgstr "議題 (issue) 事件"
+msgstr "議題事件"
msgid "IssueBoards|Board"
msgstr "看æ¿"
-msgid "IssueBoards|Boards"
-msgstr ""
-
msgid "Issues"
msgstr "議題"
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
-msgstr ""
+msgstr "議題å¯ä»¥æ˜¯bug,任務或想法來討論。此外,å•é¡Œæ˜¯å¯æœå°‹å’ŒéŽæ¿¾çš„。"
msgid "Jan"
-msgstr ""
+msgstr "一月"
msgid "January"
-msgstr ""
+msgstr "一月"
+
+msgid "Job"
+msgstr "任務"
+
+msgid "Job has been erased"
+msgstr "工作已被抹除"
msgid "Jobs"
-msgstr ""
+msgstr "任務"
msgid "Jul"
-msgstr ""
+msgstr "七月"
msgid "July"
-msgstr ""
+msgstr "七月"
msgid "Jun"
-msgstr ""
+msgstr "六月"
msgid "June"
-msgstr ""
+msgstr "六月"
msgid "Koding"
-msgstr ""
+msgstr "Koding"
msgid "Kubernetes"
-msgstr ""
+msgstr "Kubernetes"
msgid "Kubernetes Cluster"
-msgstr ""
+msgstr "Kuberneteså¢é›†"
msgid "Kubernetes cluster creation time exceeds timeout; %{timeout}"
-msgstr ""
+msgstr "Kuberneteså¢é›†å»ºç«‹è¶…時;%{timeout}"
msgid "Kubernetes cluster integration was not removed."
-msgstr ""
+msgstr "Kubernetes å¢é›†æ•´åˆæœªè¢«åˆªé™¤ã€‚"
msgid "Kubernetes cluster integration was successfully removed."
-msgstr ""
+msgstr "Kubernetes å¢é›†æ•´åˆå·²æˆåŠŸè¢«åˆªé™¤ã€‚"
msgid "Kubernetes cluster was successfully updated."
-msgstr ""
+msgstr "Kubernetes å¢é›†å·²æˆåŠŸæ›´æ–°ã€‚"
msgid "Kubernetes configured"
-msgstr ""
+msgstr "Kubernetes å·²é…ç½®"
msgid "Kubernetes service integration has been deprecated. %{deprecated_message_content} your Kubernetes clusters using the new <a href=\"%{url}\"/>Kubernetes Clusters</a> page"
-msgstr ""
+msgstr "Kubernetesæœå‹™æ•´åˆå·²è¢«æ£„用。 %{deprecated_message_content} 您的 Kubernetes å¢é›†ä½¿ç”¨æ–°çš„ <a href=\"%{url}\"/>Kubernetes å¢é›†</a> é é¢"
+
+msgid "LFS"
+msgstr "LFS"
msgid "LFSStatus|Disabled"
msgstr "åœç”¨"
@@ -2450,38 +2675,44 @@ msgid "LFSStatus|Enabled"
msgstr "啟用"
msgid "Label"
-msgstr ""
+msgstr "標籤"
+
+msgid "Label actions dropdown"
+msgstr "標籤æ“作下拉èœå–®"
msgid "LabelSelect|%{firstLabelName} +%{remainingLabelCount} more"
-msgstr ""
+msgstr "%{firstLabelName} +%{remainingLabelCount} 更多"
msgid "LabelSelect|%{labelsString}, and %{remainingLabelCount} more"
-msgstr ""
+msgstr "%{labelsString},和%{remainingLabelCount} 更多"
msgid "Labels"
msgstr "標籤"
msgid "Labels can be applied to %{features}. Group labels are available for any project within the group."
-msgstr ""
+msgstr "標籤å¯ä»¥æ‡‰ç”¨æ–¼ %{features}。群組標籤å¯ä»¥ç”¨æ–¼ä»»ä½•群組內的項目。"
msgid "Labels can be applied to issues and merge requests to categorize them."
-msgstr ""
+msgstr "標籤å¯ä»¥ç”¨æ–¼è­°é¡Œå’Œåˆä½µè«‹æ±‚以å°å®ƒå€‘進行分類。"
+
+msgid "Labels can be applied to issues and merge requests."
+msgstr "標籤å¯ä»¥ç”¨æ–¼è­°é¡Œå’Œåˆä½µè«‹æ±‚。"
msgid "Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>"
-msgstr ""
+msgstr "<span>è¦è®“標籤</span> %{labelTitle} <span>æå‡åˆ°ç¾¤çµ„標籤嗎?</span>"
msgid "Labels|Promote Label"
-msgstr ""
+msgstr "æå‡æ¨™ç±¤"
msgid "Last %d day"
msgid_plural "Last %d days"
msgstr[0] "最近 %d 天"
msgid "Last Pipeline"
-msgstr "æœ€æ–°æµæ°´ç·š (pipeline) "
+msgstr "æœ€æ–°æµæ°´ç·š"
msgid "Last commit"
-msgstr "最後更動記錄 (commit) "
+msgstr "最後更動記錄"
msgid "Last edited %{date}"
msgstr "最後編輯於 %{date}"
@@ -2496,25 +2727,28 @@ msgid "Last updated"
msgstr "上次更新"
msgid "LastPushEvent|You pushed to"
-msgstr "您上傳 (push) 了"
+msgstr "您上傳了"
msgid "LastPushEvent|at"
msgstr "æ–¼"
+msgid "Latest changes"
+msgstr "最新修改"
+
msgid "Learn more"
-msgstr ""
+msgstr "進一步了解"
msgid "Learn more about Kubernetes"
-msgstr ""
+msgstr "了解更多端於 Kubernetes 的資訊"
msgid "Learn more about protected branches"
-msgstr ""
+msgstr "進一步了解å—ä¿è­·çš„分支"
msgid "Learn more in the"
msgstr "了解更多"
msgid "Learn more in the|pipeline schedules documentation"
-msgstr "æµæ°´ç·š (pipeline) 排程說明文件"
+msgstr "æµæ°´ç·šæŽ’程說明文件"
msgid "Leave"
msgstr "離開"
@@ -2525,71 +2759,68 @@ msgstr "退出群組"
msgid "Leave project"
msgstr "退出專案"
-msgid "License"
-msgstr ""
-
msgid "List"
-msgstr ""
+msgstr "清單"
msgid "List your GitHub repositories"
-msgstr ""
+msgstr "列出您 GitHub 的檔案庫"
msgid "Loading the GitLab IDE..."
-msgstr ""
+msgstr "è®€å– GitLab IDE..."
+
+msgid "Loading..."
+msgstr "載入中…"
msgid "Lock"
msgstr "鎖定"
msgid "Lock %{issuableDisplayName}"
-msgstr ""
+msgstr "鎖定 %{issuableDisplayName}"
msgid "Lock not found"
-msgstr ""
+msgstr "找ä¸åˆ°éŽ–å®šçš„æª”æ¡ˆ"
+
+msgid "Lock to current projects"
+msgstr "鎖定目å‰å°ˆæ¡ˆ"
msgid "Locked"
msgstr "鎖定"
-msgid "Locked Files"
-msgstr ""
-
-msgid "Locks give the ability to lock specific file or folder."
-msgstr ""
+msgid "Locked to current projects"
+msgstr "已鎖定目å‰å°ˆæ¡ˆ"
msgid "Login"
msgstr "登入"
-msgid "Make everyone on your team more productive regardless of their location. GitLab Geo creates read-only mirrors of your GitLab instance so you can reduce the time it takes to clone and fetch large repos."
-msgstr ""
-
msgid "Manage all notifications"
-msgstr ""
+msgstr "ç®¡ç†æ‰€æœ‰é€šçŸ¥"
msgid "Manage group labels"
-msgstr ""
+msgstr "管ç†ç¾¤çµ„標籤"
msgid "Manage labels"
-msgstr ""
+msgstr "ç®¡ç†æ¨™ç±¤"
msgid "Manage project labels"
-msgstr ""
-
-msgid "Manage your group’s membership while adding another level of security with SAML."
-msgstr ""
+msgstr "管ç†å°ˆæ¡ˆæ¨™ç±¤"
msgid "Mar"
-msgstr ""
+msgstr "三月"
msgid "March"
-msgstr ""
+msgstr "三月"
-msgid "Mark done"
-msgstr ""
+msgid "Mark todo as done"
+msgstr "標記「å³å°‡å®Œæˆã€ç‚ºå®Œæˆã€‚"
+
+msgid "Markdown enabled"
+msgstr "已啟用 Markdown"
msgid "Maximum git storage failures"
msgstr "最大 git 儲存失敗"
msgid "May"
-msgstr ""
+msgstr "五月"
msgid "Median"
msgstr "䏭使•¸"
@@ -2597,198 +2828,174 @@ msgstr "䏭使•¸"
msgid "Members"
msgstr "æˆå“¡"
-msgid "Members will be forwarded here when signing in to your group. Get this from your identity provider, where it can also be called \"SSO Service Location\", \"SAML Token Issuance Endpoint\", or \"SAML 2.0/W-Federation URL\"."
-msgstr ""
+msgid "Merge Request:"
+msgstr "åˆä½µè«‹æ±‚:"
msgid "Merge Requests"
-msgstr "åˆä½µè«‹æ±‚ (merge request)"
+msgstr "åˆä½µè«‹æ±‚"
msgid "Merge events"
-msgstr "åˆä½µ (merge) 事件"
+msgstr "åˆä½µäº‹ä»¶"
msgid "Merge request"
msgstr "åˆä½µè«‹æ±‚"
-msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
-msgstr ""
-
-msgid "Merged"
-msgstr ""
-
-msgid "Messages"
-msgstr "公告"
-
-msgid "Metrics - Influx"
-msgstr ""
-
-msgid "Metrics - Prometheus"
-msgstr ""
-
-msgid "Metrics|Business"
-msgstr ""
-
-msgid "Metrics|Create metric"
-msgstr ""
-
-msgid "Metrics|Edit metric"
-msgstr ""
-
-msgid "Metrics|For grouping similar metrics"
-msgstr ""
-
-msgid "Metrics|Label of the chart's vertical axis. Usually the type of the unit being charted. The horizontal axis (X-axis) always represents time."
-msgstr ""
-
-msgid "Metrics|Legend label (optional)"
-msgstr ""
-
-msgid "Metrics|Must be a valid PromQL query."
-msgstr ""
-
-msgid "Metrics|Name"
-msgstr ""
-
-msgid "Metrics|New metric"
-msgstr ""
-
-msgid "Metrics|Prometheus Query Documentation"
-msgstr ""
-
-msgid "Metrics|Query"
-msgstr ""
-
-msgid "Metrics|Response"
-msgstr ""
+msgid "Merge requests"
+msgstr "åˆä½µè«‹æ±‚"
-msgid "Metrics|System"
-msgstr ""
+msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
+msgstr "åˆä½µè«‹æ±‚是一個讓其他人æå‡ºæ›´æ”¹å»ºè­°ä¸¦è¨Žè«–的地方"
-msgid "Metrics|Type"
-msgstr ""
+msgid "MergeRequests|Resolve this discussion in a new issue"
+msgstr "在新議題中解決此討論"
-msgid "Metrics|Unit label"
-msgstr ""
+msgid "MergeRequests|Saving the comment failed"
+msgstr "儲存評論失敗"
-msgid "Metrics|Used as a title for the chart"
-msgstr ""
+msgid "MergeRequests|Toggle comments for this file"
+msgstr "åˆ‡æ›æ­¤æ–‡ä»¶çš„註釋"
-msgid "Metrics|Used if the query returns a single series. If it returns multiple series, their legend labels will be picked up from the response."
-msgstr ""
+msgid "MergeRequests|Updating discussions failed"
+msgstr "更新討論失敗"
-msgid "Metrics|Y-axis label"
-msgstr ""
+msgid "MergeRequests|View file @ %{commitId}"
+msgstr "查看文件@ %{commitId}"
-msgid "Metrics|e.g. HTTP requests"
-msgstr ""
+msgid "MergeRequests|View replaced file @ %{commitId}"
+msgstr "æŸ¥çœ‹æ›¿æ›æ–‡ä»¶@ %{commitId}"
-msgid "Metrics|e.g. Requests/second"
-msgstr ""
+msgid "Merged"
+msgstr "å·²åˆä½µ"
-msgid "Metrics|e.g. Throughput"
-msgstr ""
+msgid "Messages"
+msgstr "公告"
-msgid "Metrics|e.g. rate(http_requests_total[5m])"
-msgstr ""
+msgid "Metrics - Influx"
+msgstr "指標 - Influx"
-msgid "Metrics|e.g. req/sec"
-msgstr ""
+msgid "Metrics - Prometheus"
+msgstr "指標 - Prometheus"
msgid "Milestone"
-msgstr ""
+msgstr "里程碑"
+
+msgid "Milestones"
+msgstr "里程碑"
msgid "Milestones|Delete milestone"
-msgstr ""
+msgstr "刪除里程碑"
msgid "Milestones|Delete milestone %{milestoneTitle}?"
-msgstr ""
+msgstr "刪除里程碑 %{milestoneTitle} ?"
msgid "Milestones|Failed to delete milestone %{milestoneTitle}"
-msgstr ""
+msgstr "刪除里程碑 %{milestoneTitle} 時發生錯誤"
msgid "Milestones|Milestone %{milestoneTitle} was not found"
-msgstr ""
+msgstr "找ä¸åˆ°é‡Œç¨‹ç¢‘ %{milestoneTitle}"
msgid "Milestones|Promote %{milestoneTitle} to group milestone?"
-msgstr ""
+msgstr "å°‡ %{milestoneTitle} æå‡æˆç¾¤çµ„里程碑?"
msgid "Milestones|Promote Milestone"
-msgstr ""
-
-msgid "Milestones|This action cannot be reversed."
-msgstr ""
+msgstr "推動里程碑"
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "新增 SSH 金鑰"
msgid "Modal|Cancel"
-msgstr ""
+msgstr "å–æ¶ˆ"
msgid "Modal|Close"
-msgstr ""
+msgstr "關閉"
msgid "Monitoring"
msgstr "監控"
-msgid "More info"
-msgstr ""
+msgid "More actions"
+msgstr "更多動作"
msgid "More information"
-msgstr ""
+msgstr "更多資訊"
msgid "More information is available|here"
msgstr "å¥åº·æª¢æŸ¥"
msgid "Move"
-msgstr ""
+msgstr "移動"
msgid "Move issue"
-msgstr ""
+msgstr "移動議題"
-msgid "Multiple issue boards"
-msgstr ""
+msgid "Name"
+msgstr "å稱"
msgid "Name new label"
-msgstr ""
+msgstr "命忖°æ¨™ç±¤"
+
+msgid "Name your individual key via a title"
+msgstr "é€éŽæ¨™é¡Œç‚ºæ‚¨çš„個人密鑰命å"
+
+msgid "Nav|Help"
+msgstr "幫助"
+
+msgid "Nav|Home"
+msgstr "首é "
+
+msgid "Nav|Sign In / Register"
+msgstr "註冊 / 登入"
+
+msgid "Nav|Sign out and sign in with a different account"
+msgstr "登出,並使用其他帳號登入"
+
+msgid "New Identity"
+msgstr "新增身份"
msgid "New Issue"
msgid_plural "New Issues"
-msgstr[0] "建立議題 (issue) "
+msgstr[0] "建立議題"
msgid "New Kubernetes Cluster"
-msgstr ""
+msgstr "建立 Kubernetes å¢é›†"
msgid "New Kubernetes cluster"
-msgstr ""
+msgstr "建立 Kubernetes å¢é›†"
+
+msgid "New Label"
+msgstr "新增標籤"
msgid "New Pipeline Schedule"
-msgstr "å»ºç«‹æµæ°´ç·š (pipeline) 排程"
+msgstr "å»ºç«‹æµæ°´ç·šæŽ’程"
msgid "New branch"
-msgstr "新分支 (branch) "
+msgstr "新分支"
msgid "New branch unavailable"
-msgstr ""
+msgstr "新的分支ä¸å¯ç”¨"
msgid "New directory"
msgstr "新增目錄"
-msgid "New epic"
-msgstr ""
-
msgid "New file"
msgstr "新增檔案"
msgid "New group"
msgstr "新群組"
+msgid "New identity"
+msgstr "新增身份"
+
msgid "New issue"
-msgstr "新增議題 (issue) "
+msgstr "新增議題"
msgid "New label"
-msgstr ""
+msgstr "建立標籤"
msgid "New merge request"
-msgstr "新增åˆä½µè«‹æ±‚ (merge request) "
+msgstr "新增åˆä½µè«‹æ±‚"
+
+msgid "New pipelines will cancel older, pending pipelines on the same branch"
+msgstr "æ–°çš„æµæ°´ç·šå°‡å–消åŒä¸€åˆ†æ”¯ä¸Šè¼ƒèˆŠã€æœªè§£æ±ºçš„æµæ°´ç·š"
msgid "New project"
msgstr "新專案"
@@ -2805,32 +3012,41 @@ msgstr "æ–°å­ç¾¤çµ„"
msgid "New tag"
msgstr "新增標籤"
-msgid "No Label"
-msgstr ""
+msgid "No"
+msgstr "å¦"
msgid "No assignee"
-msgstr ""
+msgstr "未指派"
msgid "No changes"
-msgstr ""
+msgstr "沒有改變"
msgid "No connection could be made to a Gitaly Server, please check your logs!"
-msgstr ""
+msgstr "無法連接到 Gitaly 伺æœå™¨ï¼Œè«‹æª¢æŸ¥æ‚¨çš„æ—¥èªŒï¼"
msgid "No due date"
-msgstr ""
+msgstr "沒有到期日"
msgid "No estimate or time spent"
-msgstr ""
+msgstr "沒有é ä¼°æˆ–花費時間"
msgid "No file chosen"
-msgstr ""
+msgstr "æœªé¸æ“‡ä»»ä½•檔案"
-msgid "No labels created yet."
-msgstr ""
+msgid "No files found"
+msgstr "找ä¸åˆ°ä»»ä½•檔案"
+
+msgid "No files found."
+msgstr "找ä¸åˆ°ä»»ä½•檔案"
+
+msgid "No merge requests found"
+msgstr "找ä¸åˆ°åˆä½µè«‹æ±‚"
+
+msgid "No messages were logged"
+msgstr "沒有消æ¯è¢«è¨˜éŒ„"
msgid "No repository"
-msgstr "找ä¸åˆ°æª”案庫 (repository)"
+msgstr "找ä¸åˆ°æª”案庫"
msgid "No schedules"
msgstr "沒有排程"
@@ -2839,73 +3055,67 @@ msgid "None"
msgstr "ç„¡"
msgid "Not allowed to merge"
-msgstr ""
+msgstr "ä¸å…許åˆä½µ"
msgid "Not available"
msgstr "無法使用"
msgid "Not available for private projects"
-msgstr ""
+msgstr "ä¸é©ç”¨æ–¼ç§äººå°ˆæ¡ˆ"
msgid "Not available for protected branches"
-msgstr ""
+msgstr "ä¸é©ç”¨æ–¼å—ä¿è­·çš„分支"
msgid "Not confidential"
-msgstr ""
+msgstr "éžæ©Ÿå¯†"
msgid "Not enough data"
msgstr "資料ä¸è¶³"
msgid "Note that the master branch is automatically protected. %{link_to_protected_branches}"
-msgstr ""
-
-msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow connecting repositories without generating a Personal Access Token."
-msgstr ""
+msgstr "請注æ„,master 分支é è¨­ç‚ºå—ä¿è­·çš„。 %{link_to_protected_branches}"
msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
-msgstr ""
-
-msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow connecting repositories without generating a Personal Access Token."
-msgstr ""
+msgstr "注æ„:作為管ç†å“¡ï¼Œæ‚¨å¯èƒ½å¸Œæœ›é…ç½® %{github_integration_link},這將å…è¨±é€šéŽ GitHub 登入並å…許匯入存儲庫而ä¸ç”Ÿæˆå€‹äººå­˜å–憑證。"
msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
-msgstr ""
+msgstr "注æ„:請考慮讓您的 GitLab 管ç†å“¡è¨­å®š %{github_integration_link},這將å…許使用者é€éŽ GitHub 登入並å…許匯入儲存庫而ä¸ç”Ÿæˆå€‹äººå­˜å–憑證。"
msgid "Notification events"
msgstr "事件通知"
msgid "NotificationEvent|Close issue"
-msgstr "關閉議題 (issue) "
+msgstr "關閉議題"
msgid "NotificationEvent|Close merge request"
-msgstr "關閉åˆä½µè«‹æ±‚ (merge request) "
+msgstr "關閉åˆä½µè«‹æ±‚"
msgid "NotificationEvent|Failed pipeline"
-msgstr "æµæ°´ç·š (pipeline) 失敗"
+msgstr "æµæ°´ç·šå¤±æ•—"
msgid "NotificationEvent|Merge merge request"
-msgstr "åˆä½µè«‹æ±‚ (merge request) 被åˆä½µ"
+msgstr "åˆä½µè«‹æ±‚被åˆä½µ"
msgid "NotificationEvent|New issue"
-msgstr "新增議題 (issue) "
+msgstr "新增議題"
msgid "NotificationEvent|New merge request"
-msgstr "新增åˆä½µè«‹æ±‚ (merge request) "
+msgstr "新增åˆä½µè«‹æ±‚"
msgid "NotificationEvent|New note"
msgstr "新增評論"
msgid "NotificationEvent|Reassign issue"
-msgstr "釿–°æŒ‡æ´¾è­°é¡Œ (issue) "
+msgstr "釿–°æŒ‡æ´¾è­°é¡Œ"
msgid "NotificationEvent|Reassign merge request"
-msgstr "釿–°æŒ‡æ´¾åˆä½µè«‹æ±‚ (merge request) "
+msgstr "釿–°æŒ‡æ´¾åˆä½µè«‹æ±‚"
msgid "NotificationEvent|Reopen issue"
-msgstr "é‡å•Ÿè­°é¡Œ (issue)"
+msgstr "é‡å•Ÿè­°é¡Œ"
msgid "NotificationEvent|Successful pipeline"
-msgstr "æµæ°´ç·š (pipeline) æˆåŠŸå®Œæˆ"
+msgstr "æµæ°´ç·šæˆåŠŸå®Œæˆ"
msgid "NotificationLevel|Custom"
msgstr "自訂"
@@ -2929,46 +3139,40 @@ msgid "Notifications"
msgstr "通知"
msgid "Notifications off"
-msgstr ""
+msgstr "關閉通知"
msgid "Notifications on"
-msgstr ""
+msgstr "開啟通知"
msgid "Nov"
-msgstr ""
+msgstr "å一月"
msgid "November"
-msgstr ""
+msgstr "å一月"
msgid "Number of access attempts"
msgstr "嘗試存å–的次數"
-msgid "OK"
-msgstr ""
-
msgid "Oct"
-msgstr ""
+msgstr "åæœˆ"
msgid "October"
-msgstr ""
+msgstr "åæœˆ"
msgid "OfSearchInADropdown|Filter"
msgstr "篩é¸"
-msgid "Once imported, repositories can be mirrored over SSH. Read more %{ssh_link}"
-msgstr ""
-
msgid "Online IDE integration settings."
-msgstr ""
+msgstr "線上 IDE æ•´åˆè¨­å®šã€‚"
+
+msgid "Only comments from the following commit are shown below"
+msgstr "下é¢åƒ…顯示來自以下æäº¤çš„註釋"
msgid "Only project members can comment."
msgstr "åªæœ‰ç¾¤çµ„æˆå“¡æ‰èƒ½ç•™è¨€ã€‚"
-msgid "Open"
-msgstr ""
-
-msgid "Opened"
-msgstr ""
+msgid "Open in Xcode"
+msgstr "在 Xcode 開啟"
msgid "OpenedNDaysAgo|Opened"
msgstr "é–‹å§‹æ–¼"
@@ -2976,14 +3180,23 @@ msgstr "é–‹å§‹æ–¼"
msgid "Opens in a new window"
msgstr "於新視窗開啟"
+msgid "Operations"
+msgstr "動作"
+
msgid "Options"
msgstr "é¸é …"
+msgid "Or you can choose one of the suggested colors below"
+msgstr "或者您å¯ä»¥åœ¨ä¸‹æ–¹å»ºè­°çš„é¡è‰²ä¸­é¸æ“‡ä¸€å€‹"
+
+msgid "Other Labels"
+msgstr "其他標籤"
+
msgid "Otherwise it is recommended you start with one of the options below."
-msgstr ""
+msgstr "此外,建議您從下é¢çš„一個é¸é …開始。"
msgid "Outbound requests"
-msgstr ""
+msgstr "Outbound 請求"
msgid "Overview"
msgstr "總覽"
@@ -2992,7 +3205,7 @@ msgid "Owner"
msgstr "所有權"
msgid "Pages"
-msgstr ""
+msgstr "é é¢"
msgid "Pagination|Last »"
msgstr "æœ€æœ«é  Â»"
@@ -3007,34 +3220,49 @@ msgid "Pagination|« First"
msgstr "« 第一é "
msgid "Part of merge request changes"
-msgstr ""
+msgstr "åˆä½µè«‹æ±‚更改的部分"
msgid "Password"
msgstr "密碼"
+msgid "Paste your public SSH key, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'. Don't use your private SSH key."
+msgstr "貼上您的SSH公鑰,通常放置在 '~/.ssh/id_rsa.pub',並以 'ssh-rsa' 開頭。ä¸è¦ä½¿ç”¨æ‚¨çš„SSHç§é‘°ã€‚"
+
+msgid "Pause"
+msgstr "æš«åœ"
+
msgid "Pending"
-msgstr ""
+msgstr "等待處ç†"
+
+msgid "Per job. If a job passes this threshold, it will be marked as failed"
+msgstr "æ¯ä»½ä»»å‹™ã€‚å¦‚æžœä»»å‹™é€šéŽæ­¤é–¾å€¼ï¼Œå®ƒå°‡è¢«æ¨™è¨˜ç‚ºå¤±æ•—"
+
+msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgstr "執行進階é¸é …,例如更改路徑ã€å‚³è¼¸æˆ–移除群組。"
msgid "Performance optimization"
-msgstr ""
+msgstr "效能最佳化"
+
+msgid "Permissions"
+msgstr "權é™"
msgid "Personal Access Token"
-msgstr ""
+msgstr "å€‹äººè¨ªå•æ†‘è­‰"
msgid "Pipeline"
-msgstr "æµæ°´ç·š (pipeline) "
+msgstr "æµæ°´ç·š"
msgid "Pipeline Health"
-msgstr "æµæ°´ç·š (pipeline) å¥åº·æŒ‡æ•¸"
+msgstr "æµæ°´ç·šå¥åº·æŒ‡æ•¸"
msgid "Pipeline Schedule"
-msgstr "æµæ°´ç·š (pipeline) 排程"
+msgstr "æµæ°´ç·šæŽ’程"
msgid "Pipeline Schedules"
-msgstr "æµæ°´ç·š (pipeline) 排程"
+msgstr "æµæ°´ç·šæŽ’程"
-msgid "Pipeline quota"
-msgstr ""
+msgid "Pipeline triggers"
+msgstr "æµæ°´ç·šè§¸ç™¼å™¨"
msgid "PipelineCharts|Failed:"
msgstr "失敗:"
@@ -3070,7 +3298,7 @@ msgid "PipelineSchedules|None"
msgstr "ç„¡"
msgid "PipelineSchedules|Provide a short description for this pipeline"
-msgstr "è«‹ç°¡å–®èªªæ˜Žæ­¤æµæ°´ç·š (pipeline) "
+msgstr "è«‹ç°¡å–®èªªæ˜Žæ­¤æµæ°´ç·š"
msgid "PipelineSchedules|Take ownership"
msgstr "å–得所有權"
@@ -3085,10 +3313,10 @@ msgid "PipelineSheduleIntervalPattern|Custom"
msgstr "自訂"
msgid "Pipelines"
-msgstr "æµæ°´ç·š (pipeline) "
+msgstr "æµæ°´ç·š"
msgid "Pipelines charts"
-msgstr "æµæ°´ç·š (pipeline) 圖表"
+msgstr "æµæ°´ç·šåœ–表"
msgid "Pipelines for last month"
msgstr "ä¸Šå€‹æœˆçš„æµæ°´ç·š"
@@ -3100,7 +3328,7 @@ msgid "Pipelines for last year"
msgstr "åŽ»å¹´çš„æµæ°´ç·š"
msgid "Pipelines|Build with confidence"
-msgstr ""
+msgstr "信心滿滿的建立"
msgid "Pipelines|CI Lint"
msgstr "CI Lint"
@@ -3109,46 +3337,58 @@ msgid "Pipelines|Clear Runner Caches"
msgstr "清除é‹è¡Œå™¨å¿«å–"
msgid "Pipelines|Get started with Pipelines"
-msgstr ""
+msgstr "é–‹å§‹"
msgid "Pipelines|Loading Pipelines"
-msgstr ""
+msgstr "è®€å–æµæ°´ç·š"
msgid "Pipelines|Project cache successfully reset."
-msgstr ""
+msgstr "專案快å–已經æˆåŠŸé‡ç½®ã€‚"
msgid "Pipelines|Run Pipeline"
-msgstr ""
+msgstr "åŸ·è¡Œæµæ°´ç·š"
msgid "Pipelines|Something went wrong while cleaning runners cache."
-msgstr ""
+msgstr "清除é‹è¡Œå™¨å¿«å–時發生錯誤。"
msgid "Pipelines|There are currently no %{scope} pipelines."
-msgstr ""
+msgstr "ç›®å‰æ²’有 %{scope} æµæ°´ç·šã€‚"
msgid "Pipelines|There are currently no pipelines."
-msgstr ""
+msgstr "ç›®å‰æ²’æœ‰æµæ°´ç·šã€‚"
msgid "Pipelines|This project is not currently set up to run pipelines."
-msgstr ""
+msgstr "這個專案目å‰é‚„æ²’è¨­å®šæµæ°´ç·šã€‚"
-msgid "Pipeline|Retry pipeline"
-msgstr ""
+msgid "Pipeline|Create for"
+msgstr "建立"
-msgid "Pipeline|Retry pipeline #%{pipelineId}?"
-msgstr ""
+msgid "Pipeline|Create pipeline"
+msgstr "å»ºç«‹æµæ°´ç·š"
+
+msgid "Pipeline|Existing branch name or tag"
+msgstr "存在的分支å稱或標籤"
+
+msgid "Pipeline|Run Pipeline"
+msgstr "åŸ·è¡Œæµæ°´ç·š"
+
+msgid "Pipeline|Search branches"
+msgstr "æœå°‹åˆ†æ”¯"
+
+msgid "Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default."
+msgstr "指定è¦ä½¿ç”¨åœ¨æ­¤æ¬¡åŸ·è¡Œçš„變數值。%{settings_link} 中指定的值將會使用為é è¨­å€¼ã€‚"
msgid "Pipeline|Stop pipeline"
-msgstr ""
+msgstr "åœæ­¢æµæ°´ç·š"
msgid "Pipeline|Stop pipeline #%{pipelineId}?"
-msgstr ""
+msgstr "åœæ­¢æµæ°´ç·š #%{pipelineId}?"
-msgid "Pipeline|You’re about to retry pipeline %{pipelineId}."
-msgstr ""
+msgid "Pipeline|Variables"
+msgstr "變數"
msgid "Pipeline|You’re about to stop pipeline %{pipelineId}."
-msgstr ""
+msgstr "ä½ å°‡åœæ­¢æµæ°´ç·š %{pipelineId}。"
msgid "Pipeline|all"
msgstr "所有"
@@ -3162,29 +3402,47 @@ msgstr "於階段"
msgid "Pipeline|with stages"
msgstr "於階段"
+msgid "Plain diff"
+msgstr "本文差異"
+
msgid "PlantUML"
-msgstr ""
+msgstr "PlantUML"
msgid "Play"
-msgstr ""
+msgstr "執行"
-msgid "Please <a href=%{link_to_billing} target=\"_blank\" rel=\"noopener noreferrer\">enable billing for one of your projects to be able to create a Kubernetes cluster</a>, then try again."
-msgstr ""
+msgid "Please accept the Terms of Service before continuing."
+msgstr "在繼續之å‰ï¼Œè«‹åŒæ„æœå‹™æ¢æ¬¾"
+
+msgid "Please select at least one filter to see results"
+msgstr "è«‹é¸æ“‡è‡³å°‘一個篩é¸å™¨ä»¥æª¢è¦–çµæžœ"
msgid "Please solve the reCAPTCHA"
-msgstr ""
+msgstr "請填寫驗證碼"
-msgid "Please wait while we connect to your repository. Refresh at will."
-msgstr ""
+msgid "Please try again"
+msgstr "è«‹å†è©¦ä¸€æ¬¡"
msgid "Please wait while we import the repository for you. Refresh at will."
-msgstr ""
+msgstr "è«‹ç¨å€™ï¼Œæˆ‘們正在匯入您的檔案庫,ç¨å¾Œè«‹é‡æ–°æ•´ç†ã€‚"
msgid "Preferences"
msgstr "å好設定"
-msgid "Primary"
-msgstr ""
+msgid "Preferences|Navigation theme"
+msgstr "導航主題"
+
+msgid "Prioritize"
+msgstr "優先"
+
+msgid "Prioritize label"
+msgstr "優先標籤"
+
+msgid "Prioritized Labels"
+msgstr "優先標籤"
+
+msgid "Prioritized label"
+msgstr "優先標籤"
msgid "Private - Project access must be granted explicitly to each user."
msgstr "ç§æœ‰ - 專案權é™å¿…須一一指派給æ¯å€‹ä½¿ç”¨è€…"
@@ -3193,7 +3451,7 @@ msgid "Private - The group and its projects can only be viewed by members."
msgstr "ç§æœ‰ - ç¾¤çµ„åŠæ——下專案åªèƒ½è¢«è©²ç¾¤çµ„æˆå“¡æŸ¥çœ‹"
msgid "Private projects can be created in your personal namespace with:"
-msgstr ""
+msgstr "å¯ä»¥åœ¨æ‚¨çš„個人命å空間中建立ç§äººå°ˆæ¡ˆ:"
msgid "Profile"
msgstr "個人資料"
@@ -3201,6 +3459,12 @@ msgstr "個人資料"
msgid "Profiles|Account scheduled for removal."
msgstr "帳號將會被刪除"
+msgid "Profiles|Change username"
+msgstr "變更使用者å稱"
+
+msgid "Profiles|Current path: %{path}"
+msgstr "ç›®å‰è·¯å¾‘:%{path}"
+
msgid "Profiles|Delete Account"
msgstr "刪除帳號"
@@ -3219,9 +3483,21 @@ msgstr "無效的密碼"
msgid "Profiles|Invalid username"
msgstr "無效的使用者å稱"
+msgid "Profiles|Path"
+msgstr "ä½ç½®"
+
msgid "Profiles|Type your %{confirmationValue} to confirm:"
msgstr "輸入您的 %{confirmationValue} 以確èªï¼š"
+msgid "Profiles|Update username"
+msgstr "更新使用者å稱"
+
+msgid "Profiles|Username change failed - %{message}"
+msgstr "使用者å稱更改失敗 - %{message}"
+
+msgid "Profiles|Username successfully changed"
+msgstr "使用者å稱順利變更"
+
msgid "Profiles|You don't have access to delete this user."
msgstr "您沒有權é™åˆªé™¤æ­¤å¸³è™Ÿ"
@@ -3235,10 +3511,16 @@ msgid "Profiles|your account"
msgstr "你的帳號"
msgid "Profiling - Performance bar"
-msgstr ""
+msgstr "效能欄"
msgid "Programming languages used in this repository"
-msgstr ""
+msgstr "在這個檔案庫中使用的程å¼èªžè¨€"
+
+msgid "Progress"
+msgstr "進度"
+
+msgid "Project"
+msgstr "專案"
msgid "Project '%{project_name}' is in the process of being deleted."
msgstr "專案 \"%{project_name}\" 正在被刪除。"
@@ -3252,14 +3534,17 @@ msgstr "專案 '%{project_name}' 建立完æˆã€‚"
msgid "Project '%{project_name}' was successfully updated."
msgstr "專案 '%{project_name}' 更新完æˆã€‚"
+msgid "Project Badges"
+msgstr "專案徽章"
+
msgid "Project access must be granted explicitly to each user."
msgstr "專案權é™å¿…須一一指派給æ¯å€‹ä½¿ç”¨è€…。"
msgid "Project avatar"
-msgstr ""
+msgstr "專案圖åƒ"
msgid "Project avatar in repository: %{link}"
-msgstr ""
+msgstr "專案圖åƒåœ¨æª”案庫中: %{link}"
msgid "Project details"
msgstr "專案細節"
@@ -3279,30 +3564,6 @@ msgstr "專案導出已開始。完æˆå¾Œä¸‹è¼‰é€£çµæœƒé€åˆ°æ‚¨çš„信箱。"
msgid "ProjectActivityRSS|Subscribe"
msgstr "訂閱"
-msgid "ProjectCreationLevel|Allowed to create projects"
-msgstr ""
-
-msgid "ProjectCreationLevel|Default project creation protection"
-msgstr ""
-
-msgid "ProjectCreationLevel|Developers + Masters"
-msgstr ""
-
-msgid "ProjectCreationLevel|Masters"
-msgstr ""
-
-msgid "ProjectCreationLevel|No one"
-msgstr ""
-
-msgid "ProjectFeature|Disabled"
-msgstr "åœç”¨"
-
-msgid "ProjectFeature|Everyone with access"
-msgstr "任何人都å¯å­˜å–"
-
-msgid "ProjectFeature|Only team members"
-msgstr "åªæœ‰åœ˜éšŠæˆå“¡å¯ä»¥å­˜å–"
-
msgid "ProjectFileTree|Name"
msgstr "å稱"
@@ -3312,27 +3573,6 @@ msgstr "從未"
msgid "ProjectLifecycle|Stage"
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 "專案"
@@ -3357,92 +3597,89 @@ msgstr "æŠ±æ­‰ï¼Œæ²’æœ‰ç¬¦åˆæœå°‹æ¢ä»¶çš„專案"
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr "此功能需è¦ç€è¦½å™¨æ”¯æ´ localStorage"
+msgid "PrometheusDashboard|Time"
+msgstr "時間"
+
msgid "PrometheusService|%{exporters} with %{metrics} were found"
-msgstr ""
+msgstr "ç™¼ç¾ %{exporters} åŠ %{metrics}"
msgid "PrometheusService|<p class=\"text-tertiary\">No <a href=\"%{docsUrl}\">common metrics</a> were found</p>"
-msgstr ""
+msgstr "<p class=\"text-tertiary\">沒有找到<a href=\"%{docsUrl}\">常見指標</a></p>"
msgid "PrometheusService|Active"
-msgstr ""
+msgstr "啟用"
msgid "PrometheusService|Auto configuration"
-msgstr ""
+msgstr "自動設定"
msgid "PrometheusService|Automatically deploy and configure Prometheus on your clusters to monitor your project’s environments"
-msgstr ""
+msgstr "在你的群集上自動部署和é…ç½® Prometheus 以監控您的專案環境"
msgid "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."
-msgstr ""
+msgstr "é è¨­ Prometheus ç›£è½ â€˜http://localhost:9090’。ä¸å»ºè­°æ›´æ”¹é è¨­çš„ç¶²å€å’Œç›£è½åŸ ï¼Œå› ç‚ºé€™å¯èƒ½æœƒå¼•響到 GitLab 伺æœå™¨ä¸Šé‹è¡Œçš„å…¶ä»–æœå‹™ã€‚"
msgid "PrometheusService|Common metrics"
-msgstr ""
-
-msgid "PrometheusService|Common metrics are automatically monitored based on a library of metrics from popular exporters."
-msgstr ""
-
-msgid "PrometheusService|Custom metrics"
-msgstr ""
+msgstr "常見指標"
msgid "PrometheusService|Finding and configuring metrics..."
-msgstr ""
-
-msgid "PrometheusService|Finding custom metrics..."
-msgstr ""
+msgstr "尋找和é…置指標⋯⋯"
msgid "PrometheusService|Install Prometheus on clusters"
-msgstr ""
+msgstr "在å¢é›†ä¸­å®‰è£ Prometheus"
msgid "PrometheusService|Manage clusters"
-msgstr ""
+msgstr "管ç†å¢é›†"
msgid "PrometheusService|Manual configuration"
-msgstr ""
+msgstr "手動設置"
msgid "PrometheusService|Metrics"
-msgstr ""
+msgstr "指標"
+
+msgid "PrometheusService|Metrics are automatically configured and monitored based on a library of metrics from popular exporters."
+msgstr "指標會根據一些å—大眾歡迎的指標,自動設置åŠç›£æŽ§ã€‚"
msgid "PrometheusService|Missing environment variable"
-msgstr ""
+msgstr "缺少環境變數"
msgid "PrometheusService|More information"
-msgstr ""
-
-msgid "PrometheusService|New metric"
-msgstr ""
+msgstr "更多訊æ¯"
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
-msgstr ""
+msgstr "Prometheus API 地å€ï¼Œä¾‹å¦‚ http://prometheus.example.com/"
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
-msgstr ""
-
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
-msgstr ""
+msgstr "Prometheus æ­£åœ¨è‡ªå‹•ç®¡ç†æ‚¨çš„å¢é›†"
msgid "PrometheusService|Time-series monitoring service"
-msgstr ""
+msgstr "時間監控æœå‹™"
msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
-msgstr ""
+msgstr "è¦èµ·ç”¨æ‰‹å‹•é…置,請從群集中å¸è¼‰ Prometheus"
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
-msgstr ""
+msgstr "如果å…è¨±å®‰è£ Prometheus åœ¨æ‚¨çš„ç¾¤é›†ä¸Šï¼Œè«‹å–æ¶ˆä¸‹é¢çš„æ‰‹å‹•é…ç½®"
msgid "PrometheusService|Waiting for your first deployment to an environment to find common metrics"
-msgstr ""
+msgstr "等您首次部署到環境後以查詢常用指標"
msgid "Promote"
-msgstr ""
+msgstr "å‡ç´š"
-msgid "Promote to Group Label"
-msgstr ""
+msgid "Promote these project milestones into a group milestone."
+msgstr "將這些項目里程碑åˆä½µåˆ°åˆ°ä¸€å€‹ç¾¤çµ„里程碑。"
msgid "Promote to Group Milestone"
-msgstr ""
+msgstr "æå‡è‡³ç¾¤çµ„里程碑"
+
+msgid "Promote to group label"
+msgstr "æå‡è‡³ç¾¤çµ„標籤"
msgid "Protip:"
-msgstr ""
+msgstr "æç¤º:"
+
+msgid "Provider"
+msgstr "æä¾›è€…"
msgid "Public - The group and any public projects can be viewed without any authentication."
msgstr "公開 - 未登入的情æ³ä¸‹ä¾ç„¶å¯ä»¥æŸ¥çœ‹ä»»ä½•公開專案"
@@ -3450,23 +3687,23 @@ msgstr "公開 - 未登入的情æ³ä¸‹ä¾ç„¶å¯ä»¥æŸ¥çœ‹ä»»ä½•公開專案"
msgid "Public - The project can be accessed without any authentication."
msgstr "公開 - 無須任何身份驗證å³å¯å­˜å–該專案"
-msgid "Push Rules"
-msgstr ""
+msgid "Public pipelines"
+msgstr "å…¬å…±æµæ°´ç·š"
msgid "Push events"
-msgstr "æŽ¨é€ (push) 事件"
+msgstr "推é€äº‹ä»¶"
msgid "Push project from command line"
-msgstr ""
+msgstr "é€éŽæŒ‡ä»¤æŽ¨é€å°ˆæ¡ˆ"
msgid "Push to create a project"
-msgstr ""
-
-msgid "PushRule|Committer restriction"
-msgstr ""
+msgstr "é€é޿ލé€å»ºç«‹å°ˆæ¡ˆ"
msgid "Quick actions can be used in the issues description and comment boxes."
-msgstr ""
+msgstr "快速æ“作å¯ä»¥ç”¨æ–¼å•題æè¿°å’Œè©•論框。"
+
+msgid "Re-deploy"
+msgstr "釿–°éƒ¨ç½²"
msgid "Read more"
msgstr "瞭解更多"
@@ -3475,176 +3712,212 @@ msgid "Readme"
msgstr "說明檔"
msgid "Real-time features"
-msgstr ""
-
-msgid "RefSwitcher|Branches"
-msgstr "分支 (branch) "
-
-msgid "RefSwitcher|Tags"
-msgstr "標籤"
+msgstr "峿™‚功能"
msgid "Reference:"
-msgstr ""
+msgstr "åƒè€ƒä¾†æº:"
msgid "Register / Sign In"
-msgstr ""
+msgstr "註冊 / 登入"
+
+msgid "Register and see your runners for this group."
+msgstr "註冊ã€ä¸¦è§€å¯Ÿæ‚¨åœ¨é€™å€‹ç¾¤çµ„的執行器。"
+
+msgid "Register and see your runners for this project."
+msgstr "註冊ã€ä¸¦è§€å¯Ÿæ‚¨åœ¨é€™å€‹å°ˆæ¡ˆçš„執行器。"
msgid "Registry"
-msgstr ""
+msgstr "註冊表"
msgid "Related Commits"
-msgstr "相關的更動記錄 (commit) "
+msgstr "相關的更動記錄"
msgid "Related Deployed Jobs"
msgstr "相關的部署作業"
msgid "Related Issues"
-msgstr "相關的議題 (issue) "
+msgstr "相關的議題"
msgid "Related Jobs"
msgstr "相關的作業"
msgid "Related Merge Requests"
-msgstr "相關的åˆä½µè«‹æ±‚ (merge request) "
+msgstr "相關的åˆä½µè«‹æ±‚"
msgid "Related Merged Requests"
msgstr "相關已åˆä½µçš„請求"
msgid "Related merge requests"
-msgstr ""
+msgstr "相關的åˆä½µè«‹æ±‚"
msgid "Remind later"
msgstr "ç¨å¾Œæé†’"
msgid "Remove"
-msgstr ""
+msgstr "刪除"
+
+msgid "Remove Runner"
+msgstr "移除執行器"
msgid "Remove avatar"
-msgstr ""
+msgstr "刪除大頭貼"
+
+msgid "Remove priority"
+msgstr "刪除優先權"
msgid "Remove project"
msgstr "刪除專案"
-msgid "Repair authentication"
-msgstr ""
-
-msgid "Repo by URL"
-msgstr ""
-
msgid "Repository"
-msgstr "檔案庫 (repository)"
+msgstr "檔案庫"
-msgid "Repository has no locks."
-msgstr ""
+msgid "Repository Settings"
+msgstr "檔案庫設置"
msgid "Repository maintenance"
-msgstr ""
+msgstr "檔案庫維護"
-msgid "Repository mirror settings"
-msgstr ""
+msgid "Repository mirror"
+msgstr "é¡åƒæª”案庫"
msgid "Repository storage"
-msgstr ""
+msgstr "檔案庫儲存空間"
msgid "Request Access"
msgstr "申請權é™"
+msgid "Require all users to accept Terms of Service and Privacy Policy when they access GitLab."
+msgstr "è¦æ±‚所有用戶在訪å•GitLabæ™‚æŽ¥å—æœå‹™æ¢æ¬¾å’Œéš±ç§æ”¿ç­–。"
+
msgid "Reset git storage health information"
msgstr "é‡ç½® Git 儲存空間å¥åº·æŒ‡æ•¸"
msgid "Reset health check access token"
-msgstr "é‡ç½®å¥åº·æª¢æŸ¥å­˜å–憑證 (access token)"
+msgstr "é‡ç½®å¥åº·æª¢æŸ¥å­˜å–憑證"
msgid "Reset runners registration token"
-msgstr "é‡ç½® Runner 註冊憑證 (registration token)"
+msgstr "é‡ç½® Runner 註冊憑證"
+
+msgid "Resolve all discussions in new issue"
+msgstr "建立新議題以解決所有討論"
+
+msgid "Resolve conflicts on source branch"
+msgstr "解決來æºåˆ†æ”¯ä¸Šçš„è¡çª"
msgid "Resolve discussion"
-msgstr ""
+msgstr "關閉討論"
-msgid "Response"
-msgstr ""
+msgid "Resume"
+msgstr "繼續"
+
+msgid "Retry"
+msgstr "é‡è©¦"
+
+msgid "Retry this job"
+msgstr "é‡è©¦æ­¤å·¥ä½œ"
+
+msgid "Retry verification"
+msgstr "é‡è©¦é©—è­‰"
msgid "Reveal value"
msgid_plural "Reveal values"
-msgstr[0] ""
+msgstr[0] "顯示隱è—的資料"
msgid "Revert this commit"
-msgstr "還原此更動記錄 (commit)"
+msgstr "還原此更動記錄"
msgid "Revert this merge request"
-msgstr "還原此åˆä½µè«‹æ±‚ (merge request) "
+msgstr "還原此åˆä½µ"
-msgid "Review the process for configuring service providers in your identity provider — in this case, GitLab is the \"service provider\" or \"relying party\"."
-msgstr ""
+msgid "Review"
+msgstr "審閱"
msgid "Reviewing"
-msgstr ""
+msgstr "審查"
msgid "Reviewing (merge request !%{mergeRequestId})"
-msgstr ""
+msgstr "正在審閱中(åˆä½µè«‹æ±‚ !%{mergeRequestId})"
-msgid "Roadmap"
-msgstr ""
+msgid "Rollback"
+msgstr "退回"
-msgid "Run CI/CD pipelines for external repositories"
-msgstr ""
+msgid "Runner token"
+msgstr "執行器憑證"
msgid "Runners"
-msgstr ""
-
-msgid "Running"
-msgstr ""
+msgstr "執行器"
-msgid "SAML Single Sign On"
-msgstr ""
+msgid "Runners API"
+msgstr "執行器 API"
-msgid "SAML Single Sign On Settings"
-msgstr ""
+msgid "Runners can be placed on separate users, servers, and even on your local machine."
+msgstr "åŸ·è¡Œå™¨å¯æ”¾ç½®æ–¼ä¸åŒçš„使用者ã€ä¼ºæœå™¨ï¼Œç”šè‡³åœ¨æ‚¨çš„æœ¬åœ°æ©Ÿå™¨ä¸Šã€‚"
-msgid "SHA1 fingerprint of the SAML token signing certificate. Get this from your identity provider, where it can also be called \"Thumbprint\"."
-msgstr ""
+msgid "Running"
+msgstr "執行中"
msgid "SSH Keys"
msgstr "SSH 金鑰"
+msgid "SSL Verification"
+msgstr "SSL é©—è­‰"
+
+msgid "Save"
+msgstr "儲存"
+
msgid "Save changes"
msgstr "儲存變更"
msgid "Save pipeline schedule"
-msgstr "å„²å­˜æµæ°´ç·š (pipeline) 排程"
+msgstr "å„²å­˜æµæ°´ç·šæŽ’程"
msgid "Save variables"
-msgstr ""
+msgstr "儲存變數"
msgid "Schedule a new pipeline"
-msgstr "å»ºç«‹æµæ°´ç·š (pipeline) 排程"
+msgstr "å»ºç«‹æµæ°´ç·šæŽ’程"
msgid "Scheduled"
-msgstr ""
+msgstr "已排程"
msgid "Schedules"
msgstr "排程"
msgid "Scheduling Pipelines"
-msgstr "æµæ°´ç·š (pipeline) 排程"
+msgstr "æµæ°´ç·šæŽ’程"
-msgid "Scoped issue boards"
-msgstr ""
+msgid "Scroll to bottom"
+msgstr "滾到底部"
+
+msgid "Scroll to top"
+msgstr "滾到頂部"
msgid "Search"
-msgstr ""
+msgstr "æœå°‹"
+
+msgid "Search branches"
+msgstr "æœå°‹åˆ†æ”¯"
msgid "Search branches and tags"
-msgstr "æœå°‹åˆ†æ”¯ (branch) 和標籤"
+msgstr "æœå°‹åˆ†æ”¯å’Œæ¨™ç±¤"
+
+msgid "Search files"
+msgstr "æœå°‹æª”案"
+
+msgid "Search for projects, issues, etc."
+msgstr "æœå°‹å°ˆæ¡ˆã€è­°é¡Œç­‰ç­‰"
+
+msgid "Search merge requests"
+msgstr "æœå°‹åˆä½µè«‹æ±‚"
msgid "Search milestones"
-msgstr ""
+msgstr "æœå°‹é‡Œç¨‹ç¢‘"
msgid "Search project"
-msgstr ""
+msgstr "æœå°‹å°ˆæ¡ˆ"
msgid "Search users"
-msgstr ""
+msgstr "æœå°‹ä½¿ç”¨è€…"
msgid "Seconds before reseting failure information"
msgstr "é‡ç½®å¤±æ•—訊æ¯ç­‰å¾…時間(秒)"
@@ -3652,78 +3925,81 @@ msgstr "é‡ç½®å¤±æ•—訊æ¯ç­‰å¾…時間(秒)"
msgid "Seconds to wait for a storage access attempt"
msgstr "等待存å–儲存空間的嘗試時間(秒)"
-msgid "Secret variables"
-msgstr ""
-
-msgid "Security report"
-msgstr ""
+msgid "Select"
+msgstr "鏿“‡"
msgid "Select Archive Format"
msgstr "鏿“‡ä¸‹è¼‰æ ¼å¼"
+msgid "Select a namespace to fork the project"
+msgstr "鏿“‡ä¸€å€‹å‘½å空間,以複製這個專案"
+
msgid "Select a timezone"
msgstr "鏿“‡æ™‚å€"
msgid "Select an existing Kubernetes cluster or create a new one"
-msgstr ""
+msgstr "鏿“‡ä¸€å€‹ç¾æœ‰çš„ Kubernetes å¢é›†æˆ–新增一個"
msgid "Select assignee"
-msgstr ""
+msgstr "鏿“‡æŒ‡æ´¾äºº"
msgid "Select branch/tag"
-msgstr ""
+msgstr "鏿“‡åˆ†æ”¯/標籤"
-msgid "Select target branch"
-msgstr "鏿“‡ç›®æ¨™åˆ†æ”¯ (branch) "
+msgid "Select project"
+msgstr "鏿“‡å°ˆæ¡ˆ"
-msgid "Selective synchronization"
-msgstr ""
+msgid "Select project and zone to choose machine type"
+msgstr "鏿“‡å°ˆæ¡ˆèˆ‡ä½ç½®ä¾†é¸æ“‡æ©Ÿå™¨é¡žåž‹"
+
+msgid "Select project to choose zone"
+msgstr "鏿“‡å°ˆæ¡ˆä»¥é¸æ“‡ä½ç½®"
+
+msgid "Select source branch"
+msgstr "鏿“‡ä¾†æºåˆ†æ”¯"
+
+msgid "Select target branch"
+msgstr "鏿“‡ç›®æ¨™åˆ†æ”¯"
msgid "Send email"
-msgstr ""
+msgstr "傳é€é›»å­éƒµä»¶"
msgid "Sep"
-msgstr ""
+msgstr "乿œˆ"
msgid "September"
-msgstr ""
+msgstr "乿œˆ"
msgid "Server version"
-msgstr ""
+msgstr "伺æœå™¨ç‰ˆæœ¬"
msgid "Service Templates"
msgstr "æœå‹™ç¯„本"
-msgid "Service URL"
-msgstr ""
-
msgid "Session expiration, projects limit and attachment size."
-msgstr ""
+msgstr "éŽæœŸå·¥ä½œéšŽæ®µã€å°ˆæ¡ˆä¸Šé™èˆ‡é™„件大å°ã€‚"
msgid "Set a password on your account to pull or push via %{protocol}."
-msgstr "請先設定密碼,æ‰èƒ½ä½¿ç”¨ %{protocol} 來上傳 (push) 或下載 (pull) 。"
+msgstr "請先設定密碼,æ‰èƒ½ä½¿ç”¨ %{protocol} 來上傳或下載。"
msgid "Set default and restrict visibility levels. Configure import sources and git access protocol."
-msgstr ""
+msgstr "設定為é è¨­å€¼ï¼Œä¸¦é™åˆ¶å¯è¦‹ç­‰ç´šã€‚設定匯入來æºå’Œ git å­˜å–連接å”定。"
msgid "Set max session time for web terminal."
-msgstr ""
+msgstr "為網é çµ‚端器設定最長工作階段時間"
msgid "Set notification email for abuse reports."
-msgstr ""
+msgstr "為濫用回報設定通知電å­ä¿¡ç®±ã€‚"
msgid "Set requirements for a user to sign-in. Enable mandatory two-factor authentication."
-msgstr ""
+msgstr "設定使用者登入的需求。啟用強制性的兩步驟驗證。"
msgid "Set up CI/CD"
-msgstr ""
+msgstr "設定 CI/CD"
msgid "Set up Koding"
msgstr "設定 Koding"
-msgid "Set up assertions/attributes/claims (email, first_name, last_name) and NameID according to %{docsLinkStart}the documentation %{icon}%{docsLinkEnd}"
-msgstr ""
-
msgid "SetPasswordToCloneLink|set a password"
msgstr "設定密碼"
@@ -3731,22 +4007,25 @@ msgid "Settings"
msgstr "設定"
msgid "Setup a specific Runner automatically"
-msgstr ""
+msgstr "自動設置特定的執行器"
-msgid "Share the <strong>%{sso_label}</strong> with members so they can sign in to your group through your identity provider"
-msgstr ""
+msgid "Share"
+msgstr "分享"
-msgid "SharedRunnersMinutesSettings|By resetting the pipeline minutes for this namespace, the currently used minutes will be set to zero."
-msgstr ""
+msgid "Shared Runners"
+msgstr "分享的執行器"
-msgid "SharedRunnersMinutesSettings|Reset pipeline minutes"
-msgstr ""
+msgid "Show command"
+msgstr "顯示指令"
-msgid "SharedRunnersMinutesSettings|Reset used pipeline minutes"
-msgstr ""
+msgid "Show complete raw log"
+msgstr "顯示完整的原始日誌"
-msgid "Show command"
-msgstr ""
+msgid "Show latest version"
+msgstr "顯示最新版本"
+
+msgid "Show latest version of the diff"
+msgstr "顯示與最新版本的差異"
msgid "Show parent pages"
msgstr "顯示上層é é¢"
@@ -3754,51 +4033,48 @@ msgstr "顯示上層é é¢"
msgid "Show parent subgroups"
msgstr "顯示群組中的å­ç¾¤çµ„"
+msgid "Show whitespace changes"
+msgstr "顯示空白變化"
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "顯示 %d 個事件"
-msgid "Sidebar|Change weight"
-msgstr ""
+msgid "Side-by-side"
+msgstr "並排"
-msgid "Sidebar|No"
-msgstr ""
-
-msgid "Sidebar|None"
-msgstr ""
-
-msgid "Sidebar|Weight"
-msgstr ""
+msgid "Sign out"
+msgstr "登出"
msgid "Sign-in restrictions"
-msgstr ""
+msgstr "登錄é™åˆ¶"
msgid "Sign-up restrictions"
-msgstr ""
+msgstr "註冊é™åˆ¶"
msgid "Size and domain settings for static websites"
-msgstr ""
+msgstr "ç‚ºéœæ…‹ç¶²ç«™çš„大å°èˆ‡ç¶²åŸŸè¨­å®š"
-msgid "Slack application"
-msgstr ""
+msgid "Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job"
+msgstr "速度較慢,但確ä¿é …目工作空間是原始的,因為它為æ¯é …工作從頭開始複製檔案庫"
msgid "Snippets"
-msgstr "文字片段"
+msgstr "程å¼ç¢¼ç‰‡æ®µ"
msgid "Something went wrong on our end"
-msgstr ""
+msgstr "發生了錯誤。"
msgid "Something went wrong on our end."
-msgstr ""
+msgstr "發生了錯誤。"
+
+msgid "Something went wrong on our end. Please try again!"
+msgstr "未知錯誤,請é‡è©¦!"
msgid "Something went wrong when toggling the button"
-msgstr ""
+msgstr "åˆ‡æ›æŒ‰éˆ•時發生未知的錯誤"
-msgid "Something went wrong while fetching Dependency Scanning."
-msgstr ""
-
-msgid "Something went wrong while fetching SAST."
-msgstr ""
+msgid "Something went wrong while closing the %{issuable}. Please try again later"
+msgstr "在關閉議題 %{issuable} 時出ç¾å•題,請ç¨å¾Œå†è©¦"
msgid "Something went wrong while fetching the projects."
msgstr "讀å–專案時發生錯誤。"
@@ -3806,8 +4082,14 @@ msgstr "讀å–專案時發生錯誤。"
msgid "Something went wrong while fetching the registry list."
msgstr "讀å–註冊列表時發生錯誤。"
+msgid "Something went wrong while reopening the %{issuable}. Please try again later"
+msgstr "釿–°é–‹å•Ÿ %{issuable} 議題時發生錯誤。請ç¨å¾Œå†è©¦"
+
+msgid "Something went wrong while resolving this discussion. Please try again."
+msgstr "解決此議題時出ç¾å•題。請å†è©¦ä¸€æ¬¡ã€‚"
+
msgid "Something went wrong. Please try again."
-msgstr ""
+msgstr "發生了未知的錯誤,請ç¨å¾Œå†è©¦"
msgid "Sort by"
msgstr "排åº"
@@ -3816,7 +4098,7 @@ msgid "SortOptions|Access level, ascending"
msgstr "å­˜å–層級,以å‡å†ªæŽ’列"
msgid "SortOptions|Access level, descending"
-msgstr ""
+msgstr "å­˜å–層級,以é™å†ªæŽ’列"
msgid "SortOptions|Created date"
msgstr "建立日期"
@@ -3837,7 +4119,7 @@ msgid "SortOptions|Largest group"
msgstr "最大群組"
msgid "SortOptions|Largest repository"
-msgstr "最大檔案庫(repository)"
+msgstr "最大儲存庫"
msgid "SortOptions|Last created"
msgstr "最近建立"
@@ -3851,9 +4133,6 @@ msgstr "最近更新"
msgid "SortOptions|Least popular"
msgstr "最ä¸å—歡迎"
-msgid "SortOptions|Less weight"
-msgstr ""
-
msgid "SortOptions|Milestone"
msgstr "里程碑"
@@ -3863,9 +4142,6 @@ msgstr "里程碑截止日期"
msgid "SortOptions|Milestone due soon"
msgstr "å³å°‡æˆªæ­¢çš„里程碑"
-msgid "SortOptions|More weight"
-msgstr ""
-
msgid "SortOptions|Most popular"
msgstr "æœ€å—æ­¡è¿Ž"
@@ -3905,38 +4181,62 @@ msgstr "ç¨å¾Œé–‹å§‹"
msgid "SortOptions|Start soon"
msgstr "ç¾åœ¨é–‹å§‹"
-msgid "SortOptions|Weight"
-msgstr ""
-
msgid "Source"
-msgstr ""
+msgstr "來æº"
msgid "Source (branch or tag)"
-msgstr ""
+msgstr "ä¾†æº (分支或標籤)"
msgid "Source code"
msgstr "原始碼"
msgid "Source is not available"
-msgstr ""
+msgstr "來æºä¸å¯ç”¨"
msgid "Spam Logs"
msgstr "垃圾訊æ¯è¨˜éŒ„"
msgid "Spam and Anti-bot Protection"
-msgstr ""
+msgstr "垃圾內容與防機器人ä¿è­·"
+
+msgid "Specific Runners"
+msgstr "指定執行器"
msgid "Specify the following URL during the Runner setup:"
msgstr "åœ¨å®‰è£ Runner 時指定以下 URL:"
+msgid "Squash commits"
+msgstr "åˆä½µæäº¤"
+
+msgid "Stage"
+msgstr "階段"
+
+msgid "Stage & Commit"
+msgstr "暫存 & æäº¤"
+
+msgid "Stage all changes"
+msgstr "暫存所有變更"
+
+msgid "Stage changes"
+msgstr "暫存變更"
+
+msgid "Staged"
+msgstr "已暫存"
+
+msgid "Staged %{type}"
+msgstr "已暫存的 %{type}"
+
+msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
+msgstr "為一個標籤標上星號,使其æˆç‚ºå„ªå…ˆæ¨™ç±¤ã€‚é€šéŽæ‹–動命令優先標籤更改其相å°å„ªå…ˆç´šã€‚"
+
msgid "StarProject|Star"
msgstr "æ”¶è—"
msgid "Starred Projects"
-msgstr ""
+msgstr "已星標的專案"
msgid "Starred Projects' Activity"
-msgstr ""
+msgstr "已星標的專案活動"
msgid "Starred projects"
msgstr "星標項目"
@@ -3948,155 +4248,161 @@ msgid "Start the Runner!"
msgstr "啟動 Runner!"
msgid "Started"
-msgstr ""
+msgstr "已開始"
-msgid "State your message to activate"
-msgstr ""
+msgid "Starts at (UTC)"
+msgstr "é–‹å§‹æ–¼ (UTC)"
msgid "Status"
-msgstr ""
+msgstr "狀態"
+
+msgid "Stop this environment"
+msgstr "åœæ­¢æ­¤ç’°å¢ƒ"
msgid "Stopped"
-msgstr ""
+msgstr "å·²åœæ­¢"
msgid "Storage"
-msgstr ""
+msgstr "儲存"
msgid "Subgroups"
msgstr "å­ç¾¤çµ„"
-msgid "Switch branch/tag"
-msgstr "切æ›åˆ†æ”¯ (branch) 或標籤"
+msgid "Subscribe"
+msgstr "訂閱"
-msgid "System"
-msgstr ""
+msgid "Subscribe at group level"
+msgstr "訂閱群組"
+
+msgid "Subscribe at project level"
+msgstr "訂閱專案"
+
+msgid "Switch branch/tag"
+msgstr "切æ›åˆ†æ”¯/標籤"
msgid "System Hooks"
msgstr "系統鉤å­"
-msgid "System header and footer:"
-msgstr ""
-
msgid "Tag (%{tag_count})"
msgid_plural "Tags (%{tag_count})"
-msgstr[0] ""
+msgstr[0] "標籤(%{tag_count})"
msgid "Tags"
msgstr "標籤"
+msgid "Tags:"
+msgstr "標籤:"
+
msgid "TagsPage|Browse commits"
-msgstr ""
+msgstr "ç€è¦½æ›´å‹•"
msgid "TagsPage|Browse files"
-msgstr ""
+msgstr "ç€è¦½æª”案"
msgid "TagsPage|Can't find HEAD commit for this tag"
-msgstr ""
+msgstr "無法找到此標籤的更動"
msgid "TagsPage|Cancel"
-msgstr ""
+msgstr "å–æ¶ˆ"
msgid "TagsPage|Create tag"
-msgstr ""
+msgstr "建立標籤"
msgid "TagsPage|Delete tag"
-msgstr ""
+msgstr "刪除標籤"
msgid "TagsPage|Deleting the %{tag_name} tag cannot be undone. Are you sure?"
-msgstr ""
+msgstr "刪除 %{tag_name} 後將無法æ¢å¾©ï¼Œæ‚¨ç¢ºå®šå—Žï¼Ÿ"
msgid "TagsPage|Edit release notes"
-msgstr ""
+msgstr "編輯發佈訊æ¯"
msgid "TagsPage|Existing branch name, tag, or commit SHA"
-msgstr ""
+msgstr "已存在分支åç¨±ã€æ¨™ç±¤æˆ–者更動SHA"
msgid "TagsPage|Filter by tag name"
-msgstr ""
+msgstr "根據標籤å稱塞é¸"
msgid "TagsPage|New Tag"
-msgstr ""
+msgstr "新標籤"
msgid "TagsPage|New tag"
-msgstr ""
+msgstr "新標籤"
msgid "TagsPage|Optionally, add a message to the tag."
-msgstr ""
+msgstr "增加標籤的說明(å¯é¸ï¼‰"
msgid "TagsPage|Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page."
-msgstr ""
+msgstr "增加標籤的發佈說明,這將會被儲存在Gitlab資料庫中,並顯示於標籤é ã€‚(å¯é¸ï¼‰"
msgid "TagsPage|Release notes"
-msgstr ""
+msgstr "發佈說明"
msgid "TagsPage|Repository has no tags yet."
-msgstr ""
+msgstr "檔案庫還沒有任何標籤。"
msgid "TagsPage|Sort by"
-msgstr ""
+msgstr "ä¾ç…§æŽ’åº"
msgid "TagsPage|Tags"
-msgstr ""
+msgstr "標籤"
msgid "TagsPage|Tags give the ability to mark specific points in history as being important"
-msgstr ""
+msgstr "æ¨™ç±¤å…·æœ‰å†æäº¤æ­·å²ä¸Šï¼Œæ¨™æ³¨ç‰¹å®šæäº¤çš„功能"
msgid "TagsPage|This tag has no release notes."
-msgstr ""
+msgstr "此標籤沒有發佈說明"
msgid "TagsPage|Use git tag command to add a new one:"
-msgstr ""
+msgstr "使用 git tag 指令增加一個新的標籤:"
-msgid "TagsPage|Write your release notes or drag files here..."
-msgstr ""
+msgid "TagsPage|Write your release notes or drag files here…"
+msgstr "撰寫發行說明或拖放文件到這裡⋯"
msgid "TagsPage|protected"
-msgstr ""
+msgstr "ä¿è­·"
msgid "Target Branch"
-msgstr "目標分支 (branch) "
+msgstr "目標分支"
msgid "Target branch"
-msgstr ""
+msgstr "目標分支"
msgid "Team"
msgstr "團隊"
-msgid "Thanks! Don't show me this again"
-msgstr ""
+msgid "Terms of Service Agreement and Privacy Policy"
+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 "Terms of Service and Privacy Policy"
+msgstr "æœå‹™æ¢æ¬¾å’Œéš±ç§æ”¿ç­–"
+
+msgid "Test coverage parsing"
+msgstr "測試覆蓋率分æž"
msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project"
-msgstr ""
+msgstr "è­°é¡Œè¿½è¹¤å™¨æ˜¯æ·»åŠ å°ˆæ¡ˆä¸­éœ€è¦æ”¹é€²æˆ–解決å•題的地方。"
msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
-msgstr ""
-
-msgid "The X509 Certificate to use when mutual TLS is required to communicate with the external authorization service. If left blank, the server certificate is still validated when accessing over HTTPS."
-msgstr ""
+msgstr "è­°é¡Œè¿½è¹¤å™¨æ˜¯æ·»åŠ å°ˆæ¡ˆä¸­éœ€è¦æ”¹é€²æˆ–解決å•題的地方。您å¯ä»¥è¨»å†Šæˆ–登入為專案創建議題。"
msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request."
-msgstr "程å¼é–‹ç™¼éšŽæ®µé¡¯ç¤ºå¾žç¬¬ä¸€æ¬¡æ›´å‹•記錄 (commit) 到建立åˆä½µè«‹æ±‚ (merge request) 的時間。建立第一個åˆä½µè«‹æ±‚後,資料將自動填入。"
+msgstr "程å¼é–‹ç™¼éšŽæ®µé¡¯ç¤ºå¾žç¬¬ä¸€æ¬¡æ›´å‹•記錄到建立åˆä½µè«‹æ±‚的時間。建立第一個åˆä½µè«‹æ±‚後,資料將自動填入。"
msgid "The collection of events added to the data gathered for that stage."
msgstr "該階段中的相關事件集åˆã€‚"
-msgid "The connection will time out after %{timeout}. For repositories that take longer, use a clone/push combination."
-msgstr ""
-
msgid "The fork relationship has been removed."
-msgstr "åˆ†æ”¯èˆ‡ä¸»å¹¹é–“çš„é—œè¯ (fork relationship) 已被刪除。"
+msgstr "分支與主幹間的關è¯å·²è¢«åˆªé™¤ã€‚"
msgid "The import will time out after %{timeout}. For repositories that take longer, use a clone/push combination."
-msgstr ""
+msgstr "匯入將在 %{timeout} ä¹‹å¾Œè¶…æ™‚ã€‚å°æ–¼éœ€è¦æ›´é•·æ™‚間的存儲庫,請使用 clone / push 組åˆã€‚"
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) 中所花的時間。建立第一個議題後,資料將自動填入。"
+msgstr "議題階段顯示從議題建立到設定里程碑所花的時間,或是議題被分類到議題看æ¿ä¸­æ‰€èŠ±çš„æ™‚é–“ã€‚å»ºç«‹ç¬¬ä¸€å€‹è­°é¡Œå¾Œï¼Œè³‡æ–™å°‡è‡ªå‹•å¡«å…¥ã€‚"
msgid "The maximum file size allowed is 200KB."
-msgstr ""
+msgstr "最大檔案大å°ç‚º 200KB"
msgid "The number of attempts GitLab will make to access a storage."
msgstr "GitLab å­˜å–儲存空間的嘗試次數。"
@@ -4104,20 +4410,17 @@ 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 passphrase required to decrypt the private key. This is optional and the value is encrypted at rest."
-msgstr ""
+msgid "The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>"
+msgstr "CI 設定檔的路徑。默èªç‚º <code>.gitlab-ci.yml</code>"
msgid "The phase of the development lifecycle."
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 "計劃階段顯示從更動記錄 (commit) 被排程至第一個推é€çš„æ™‚間。第一次推é€ä¹‹å¾Œï¼Œè³‡æ–™å°‡è‡ªå‹•填入。"
-
-msgid "The private key to use when a client certificate is provided. This value is encrypted at rest."
-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 "營é‹éšŽæ®µé¡¯ç¤ºå¾žå»ºç«‹è­°é¡Œ (issue) 到部署程å¼ä¸Šç·šæ‰€èŠ±çš„æ™‚é–“ã€‚å®Œæˆå¾žç™¼æƒ³åˆ°ä¸Šç·šçš„完整開發週期後,資料將自動填入。"
+msgstr "營é‹éšŽæ®µé¡¯ç¤ºå¾žå»ºç«‹è­°é¡Œåˆ°éƒ¨ç½²ç¨‹å¼ä¸Šç·šæ‰€èŠ±çš„æ™‚é–“ã€‚å®Œæˆå¾žç™¼æƒ³åˆ°ä¸Šç·šçš„完整開發週期後,資料將自動填入。"
msgid "The project can be accessed by any logged in user."
msgstr "本專案å¯è®“任何已登入的使用者存å–"
@@ -4126,34 +4429,34 @@ msgid "The project can be accessed without any authentication."
msgstr "本專案å¯è®“任何人存å–"
msgid "The repository for this project does not exist."
-msgstr "本專案沒有檔案庫 (repository) "
+msgstr "本專案沒有檔案庫"
msgid "The repository for this project is empty"
-msgstr ""
+msgstr "這個專案的檔案庫是空的"
msgid "The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>."
-msgstr ""
+msgstr "該存儲庫必須å¯é€šéŽ <code>http://</code>, <code>https://</code> 或 <code>git://</code>訪å•。"
msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request."
-msgstr "複閱階段顯示從åˆä½µè«‹æ±‚ (merge request) 建立後至被åˆä½µçš„æ™‚間。當建立第一個åˆä½µè«‹æ±‚ (merge request) 後,資料將自動填入。"
+msgstr "複閱階段顯示從åˆä½µè«‹æ±‚建立後至被åˆä½µçš„æ™‚間。當建立第一個åˆä½µè«‹æ±‚後,資料將自動填入。"
-msgid "The roadmap shows the progress of your epics along a timeline"
-msgstr ""
+msgid "The secure token used by the Runner to checkout the project"
+msgstr "此安全憑證使用於執行器簽出專案"
msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time."
-msgstr "è©¦ç‡Ÿé‹æ®µé¡¯ç¤ºå¾žåˆä½µè«‹æ±‚ (merge request) 被åˆä½µå¾Œè‡³éƒ¨ç½²ç‡Ÿé‹çš„æ™‚間。當第一次部署營é‹å¾Œï¼Œè³‡æ–™å°‡è‡ªå‹•å¡«å…¥"
+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 "測試階段顯示相關åˆä½µè«‹æ±‚ (merge request) çš„æµæ°´ç·š (pipeline) æ‰€èŠ±çš„æ™‚é–“ã€‚ç•¶ç¬¬ä¸€å€‹æµæ°´ç·š (pipeline) 執行完畢後,資料將自動填入。"
+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 "GitLab ä¿å­˜å¤±æ•—訊æ¯çš„æ™‚é–“(ç§’)。在此時間內若沒有發生錯誤,失敗訊æ¯å°‡æœƒè¢«é‡ç½®"
msgid "The time in seconds GitLab will try to access storage. After this time a timeout error will be raised."
-msgstr "GitLab å˜—è©¦å­˜å–æª”案庫 (repository) 的時間 (ç§’)ã€‚è¶…éŽæ­¤æ™‚間將會引發逾時錯誤。"
+msgstr "GitLab å˜—è©¦å­˜å–æª”案庫的時間 (ç§’)ã€‚è¶…éŽæ­¤æ™‚間將會引發逾時錯誤。"
msgid "The time in seconds between storage checks. When a previous check did complete yet, GitLab will skip a check."
-msgstr ""
+msgstr "存儲檢查之間的時間(秒)。當以å‰çš„æª¢æŸ¥å®Œæˆæ™‚,GitLabå°‡è·³éŽæª¢æŸ¥ã€‚"
msgid "The time taken by each data entry gathered by that stage."
msgstr "該階段中æ¯ä¸€å€‹è³‡æ–™é …目所花的時間。"
@@ -4162,40 +4465,55 @@ msgid "The value lying at the midpoint of a series of observed values. E.g., bet
msgstr "䏭使•¸æ˜¯ä¸€å€‹æ•¸åˆ—中最中間的值。例如在 3ã€5ã€9 ä¹‹é–“ï¼Œä¸­ä½æ•¸æ˜¯ 5。在 3ã€5ã€7ã€8 ä¹‹é–“ï¼Œä¸­ä½æ•¸æ˜¯ (5 + 7)/ 2 = 6。"
msgid "There are no issues to show"
-msgstr ""
+msgstr "沒有å¯ä»¥é¡¯ç¤ºçš„議題"
+
+msgid "There are no labels yet"
+msgstr "ç›®å‰é‚„沒有標籤"
msgid "There are no merge requests to show"
-msgstr ""
+msgstr "沒有任何的åˆä½µè«‹æ±‚"
msgid "There are problems accessing Git storage: "
msgstr "å­˜å– Git 儲存空間時出ç¾å•題:"
-msgid "There was an error loading results"
-msgstr ""
+msgid "There was an error loading jobs"
+msgstr "載入工作時發生錯誤"
+
+msgid "There was an error loading latest pipeline"
+msgstr "è¼‰å…¥æœ€æ–°æµæ°´ç·šæ™‚發生錯誤"
msgid "There was an error loading users activity calendar."
-msgstr ""
+msgstr "讀å–使用者行事曆活動時發生錯誤"
msgid "There was an error saving your notification settings."
-msgstr ""
+msgstr "ä¿å­˜æ‚¨çš„通知設置時發生錯誤。"
msgid "There was an error subscribing to this label."
-msgstr ""
+msgstr "訂閱此標籤時發生錯誤。"
msgid "There was an error when reseting email token."
-msgstr ""
+msgstr "é‡ç½®é›»å­éƒµä»¶æ†‘證時發生錯誤。"
msgid "There was an error when subscribing to this label."
-msgstr ""
+msgstr "訂閱此標籤時出ç¾éŒ¯èª¤ã€‚"
msgid "There was an error when unsubscribing from this label."
-msgstr ""
+msgstr "å–æ¶ˆè¨‚閱此標籤時出ç¾éŒ¯èª¤ã€‚"
-msgid "This board\\'s scope is reduced"
-msgstr ""
+msgid "They can be managed using the %{link}."
+msgstr "å…¶å¯ä»¥ä½¿ç”¨ %{link} 進行管ç†ã€‚"
+
+msgid "This GitLab instance does not provide any shared Runners yet. Instance administrators can register shared Runners in the admin area."
+msgstr "æ­¤ GitLab å¯¦ä¾‹ä¸æä¾›ä»»ä½•åˆ†äº«çš„åŸ·è¡Œå™¨ã€‚å¯¦ä¾‹ç®¡ç†å“¡å¯ä»¥åœ¨ç®¡ç†å“¡å€åŸŸè¨»å†Šåˆ†äº«çš„執行器。"
+
+msgid "This diff is collapsed."
+msgstr "此差異已折疊。"
msgid "This directory"
-msgstr ""
+msgstr "這個目錄"
+
+msgid "This group does not provide any group Runners yet."
+msgstr "這群組尚未æä¾›ä»»ä½•群組執行器。"
msgid "This is a confidential issue."
msgstr "這是個隱密å•題。"
@@ -4204,7 +4522,7 @@ msgid "This is the author's first Merge Request to this project."
msgstr "這是作者第一次åˆä½µè«‹æ±‚至本專案。"
msgid "This issue is confidential"
-msgstr ""
+msgstr "這個議題是ä¿å¯†çš„"
msgid "This issue is confidential and locked."
msgstr "這個å•題是ä¿å¯†ä¸”鎖定的。"
@@ -4213,73 +4531,91 @@ msgid "This issue is locked."
msgstr "這個å•題已被鎖定。"
msgid "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
-msgstr ""
+msgstr "這項任務將由使用者觸發,這通常用於部署應用程å¼åˆ°æ­£å¼ç’°å¢ƒ"
msgid "This job depends on upstream jobs that need to succeed in order for this job to be triggered"
-msgstr ""
+msgstr "這項工作需è¦ä¸Šä¸€å€‹å·¥ä½œæˆåŠŸæ™‚æ‰èƒ½è§¸ç™¼åŸ·è¡Œå·¥ä½œ"
+
+msgid "This job does not have a trace."
+msgstr "此工作沒有追蹤。"
+
+msgid "This job has been canceled"
+msgstr "æ­¤å·¥ä½œå·²ç¶“è¢«å–æ¶ˆ"
+
+msgid "This job has been skipped"
+msgstr "此工作已經被跳éŽ"
msgid "This job has not been triggered yet"
-msgstr ""
+msgstr "這份任務還沒被觸發"
msgid "This job has not started yet"
-msgstr ""
+msgstr "這份任務還沒有開始執行"
msgid "This job is in pending state and is waiting to be picked by a runner"
-msgstr ""
+msgstr "這份任務使–¼ç­‰å¾…狀態,等待 Runner 來執行"
msgid "This job requires a manual action"
-msgstr ""
+msgstr "é€™ä»½ä»»å‹™éœ€è¦æ‰‹å‹•執行"
msgid "This means you can not push code until you create an empty repository or import existing one."
-msgstr "這代表在您建立一個空的檔案庫 (repository) 或是匯入一個ç¾å­˜çš„æª”案庫之å‰ï¼Œæ‚¨å°‡ç„¡æ³•上傳更新 (push) 。"
+msgstr "這代表在您建立一個空的檔案庫或是匯入一個ç¾å­˜çš„æª”案庫之å‰ï¼Œæ‚¨å°‡ç„¡æ³•上傳更新。"
msgid "This merge request is locked."
msgstr "這個åˆä½µè«‹æ±‚已被鎖定。"
+msgid "This option is disabled while you still have unstaged changes"
+msgstr "ç•¶æ‚¨ä»æ“有未暫存的變更時,此é¸é …將會是åœç”¨çš„"
+
msgid "This page is unavailable because you are not allowed to read information across multiple projects."
-msgstr ""
+msgstr "æ­¤é é¢ä¸å¯ç”¨ï¼Œå› ç‚ºæ‚¨ä¸å…許跨多個專案閱讀信æ¯ã€‚"
+
+msgid "This page will be removed in a future release."
+msgstr "æ­¤é é¢å°‡æœƒåœ¨æœªä¾†ç‰ˆæœ¬ä¸­ç§»é™¤ã€‚"
msgid "This project"
-msgstr ""
+msgstr "這個專案"
+
+msgid "This project does not belong to a group and can therefore not make use of group Runners."
+msgstr "此專案ä¸å±¬æ–¼ä¸€å€‹ç¾¤çµ„,因此ä¸èƒ½ä½¿ç”¨ç¾¤çµ„執行器。"
msgid "This repository"
-msgstr ""
+msgstr "這個檔案庫"
-msgid "This will delete the custom metric, Are you sure?"
-msgstr ""
+msgid "This source diff could not be displayed because it is too large."
+msgstr "此原始碼差異無法顯示,因為它太大。"
-msgid "Those emails automatically become issues (with the comments becoming the email conversation) listed here."
-msgstr ""
+msgid "This user has no identities"
+msgstr "該使用者沒有身份"
msgid "Time before an issue gets scheduled"
-msgstr "議題 (issue) 被列入日程表的時間"
+msgstr "議題被列入日程表的時間"
msgid "Time before an issue starts implementation"
-msgstr "議題 (issue) 等待開始實作的時間"
+msgstr "議題等待開始實作的時間"
msgid "Time between merge request creation and merge/close"
-msgstr "åˆä½µè«‹æ±‚ (merge request) 從建立到被åˆä½µæˆ–是關閉的時間"
+msgstr "åˆä½µè«‹æ±‚從建立到被åˆä½µæˆ–是關閉的時間"
-msgid "Time between updates and capacity settings."
-msgstr ""
+msgid "Time remaining"
+msgstr "剩餘時間"
-msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
-msgstr "GitLab 等待外部æœå‹™çš„回應時間(秒)。當æœå‹™æ²’有在時間內回應時,存å–將被拒絕。"
+msgid "Time spent"
+msgstr "ç¶“éŽæ™‚é–“"
msgid "Time tracking"
-msgstr ""
+msgstr "時間追蹤"
msgid "Time until first merge request"
-msgstr "第一個åˆä½µè«‹æ±‚ (merge request) 被建立å‰çš„æ™‚é–“"
+msgstr "第一個åˆä½µè«‹æ±‚被建立å‰çš„æ™‚é–“"
msgid "TimeTrackingEstimated|Est"
-msgstr ""
+msgstr "é ä¼°"
msgid "TimeTracking|Estimated:"
-msgstr ""
+msgstr "é ä¼°ï¼š"
msgid "TimeTracking|Spent"
-msgstr ""
+msgstr "已花費:"
msgid "Timeago|%s days ago"
msgstr " %s 天å‰"
@@ -4287,6 +4623,9 @@ msgstr " %s 天å‰"
msgid "Timeago|%s days remaining"
msgstr "剩下 %s 天"
+msgid "Timeago|%s hours ago"
+msgstr "%s å°æ™‚å‰"
+
msgid "Timeago|%s hours remaining"
msgstr "剩下 %s å°æ™‚"
@@ -4302,6 +4641,9 @@ msgstr " %s 個月å‰"
msgid "Timeago|%s months remaining"
msgstr "剩下 %s 月"
+msgid "Timeago|%s seconds ago"
+msgstr "%s ç§’å‰"
+
msgid "Timeago|%s seconds remaining"
msgstr "剩下 %s 秒"
@@ -4317,48 +4659,45 @@ msgstr " %s å¹´å‰"
msgid "Timeago|%s years remaining"
msgstr "剩下 %s 年"
+msgid "Timeago|1 day ago"
+msgstr "1 天å‰"
+
msgid "Timeago|1 day remaining"
msgstr "剩下 1 天"
+msgid "Timeago|1 hour ago"
+msgstr "1 å°æ™‚å‰"
+
msgid "Timeago|1 hour remaining"
msgstr "剩下 1 å°æ™‚"
+msgid "Timeago|1 minute ago"
+msgstr "1 分é˜å‰"
+
msgid "Timeago|1 minute remaining"
msgstr "剩下 1 分é˜"
+msgid "Timeago|1 month ago"
+msgstr "1 個月å‰"
+
msgid "Timeago|1 month remaining"
msgstr "剩下 1 個月"
+msgid "Timeago|1 week ago"
+msgstr "1 週å‰"
+
msgid "Timeago|1 week remaining"
msgstr "剩下 1 週"
+msgid "Timeago|1 year ago"
+msgstr "1 å¹´å‰"
+
msgid "Timeago|1 year remaining"
msgstr "剩下 1 年"
msgid "Timeago|Past due"
msgstr "逾期"
-msgid "Timeago|a day ago"
-msgstr " 1 天å‰"
-
-msgid "Timeago|a month ago"
-msgstr " 1 個月å‰"
-
-msgid "Timeago|a week ago"
-msgstr " 1 週å‰"
-
-msgid "Timeago|a year ago"
-msgstr " 1 å¹´å‰"
-
-msgid "Timeago|about %s hours ago"
-msgstr "ç´„ %s å°æ™‚å‰"
-
-msgid "Timeago|about a minute ago"
-msgstr "ç´„ 1 分é˜å‰"
-
-msgid "Timeago|about an hour ago"
-msgstr "ç´„ 1 å°æ™‚å‰"
-
msgid "Timeago|in %s days"
msgstr " %s 天後"
@@ -4398,11 +4737,14 @@ msgstr " 1 週後"
msgid "Timeago|in 1 year"
msgstr " 1 年後"
-msgid "Timeago|in a while"
-msgstr "剛剛"
+msgid "Timeago|just now"
+msgstr "ç¾åœ¨"
+
+msgid "Timeago|right now"
+msgstr "立刻"
-msgid "Timeago|less than a minute ago"
-msgstr "ä¸åˆ° 1 分é˜å‰"
+msgid "Timeout"
+msgstr "逾時"
msgid "Time|hr"
msgid_plural "Time|hrs"
@@ -4416,82 +4758,73 @@ msgid "Time|s"
msgstr "ç§’"
msgid "Tip:"
-msgstr ""
-
-msgid "Title"
-msgstr ""
+msgstr "æç¤º:"
msgid "To GitLab"
-msgstr ""
+msgstr "到 GitLab"
-msgid "To connect GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to connect."
-msgstr ""
-
-msgid "To connect GitHub repositories, you first need to authorize GitLab to access the list of your GitHub repositories:"
-msgstr ""
-
-msgid "To connect an SVN repository, check out %{svn_link}."
-msgstr ""
+msgid "To add an SSH key you need to %{generate_link_start}generate one%{link_end} or use an %{existing_link_start}existing key%{link_end}."
+msgstr "è¦æ·»åŠ SSHå¯†é‘°ï¼Œä½ éœ€è¦ %{generate_link_start}產生一個%{link_end} 或使用 %{existing_link_start}ç¾æœ‰çš„密鑰%{link_end}。"
msgid "To import GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
-msgstr ""
+msgstr "è¦åŒ¯å…¥ GitHub 存儲庫,你å¯ä»¥ä½¿ç”¨ %{personal_access_token_link}ã€‚ç•¶ä½ å»ºç«‹ä½ çš„å€‹äººå­˜å–æ¬Šæ–時,你將需è¦é¸æ“‡<code>檔案庫</code>範åœï¼Œæ‰€ä»¥æˆ‘們將會顯示你公開åŠç§äººçš„æª”案庫清單進行匯入。"
msgid "To import GitHub repositories, you first need to authorize GitLab to access the list of your GitHub repositories:"
-msgstr ""
+msgstr "è¦åŒ¯å…¥ GitHub å­˜å„²åº«ï¼Œæ‚¨é¦–å…ˆéœ€è¦æŽˆæ¬Š GitLab è¨ªå•æ‚¨çš„ GitHub 存儲庫列表:"
msgid "To import an SVN repository, check out %{svn_link}."
-msgstr ""
-
-msgid "To only use CI/CD features for an external repository, choose <strong>CI/CD for external repo</strong>."
-msgstr ""
+msgstr "è¦åŒ¯å…¥SVN存儲庫,請查看 %{svn_link}。"
-msgid "To set up SAML authentication for your group through an identity provider like Azure, Okta, Onelogin, Ping Identity, or your custom SAML 2.0 provider:"
-msgstr ""
+msgid "To start serving your jobs you can add Runners to your group"
+msgstr "è‹¥è¦é–‹å§‹æä¾›æ‚¨çš„工作,您å¯ä»¥å¢žåŠ åŸ·è¡Œå™¨è‡³é€™å€‹ç¾¤çµ„"
msgid "To validate your GitLab CI configurations, go to 'CI/CD → Pipelines' inside your project, and click on the 'CI Lint' button."
-msgstr ""
-
-msgid "To view the roadmap, add a planned start or finish date to one of your epics in this group or its subgroups. Only epics in the past 3 months and the next 3 months are shown."
-msgstr ""
+msgstr "è‹¥è¦é©—證您的 GitLab CI 設定,請å‰å¾€æ‚¨å°ˆæ¡ˆå…§çš„「CI/CD → æµæ°´ç·šã€ï¼Œä¸¦æŒ‰ä¸‹ã€ŒCI Lintã€æŒ‰éˆ•。"
msgid "Todo"
-msgstr ""
+msgstr "待辦事項"
+
+msgid "Toggle Sidebar"
+msgstr "展開 / æ”¶èµ·å´é‚Šæ¬„"
+
+msgid "Toggle discussion"
+msgstr "切æ›è¨Žè«–"
msgid "Toggle sidebar"
-msgstr ""
+msgstr "切æ›å´é‚Šæ¬„"
msgid "ToggleButton|Toggle Status: OFF"
-msgstr ""
+msgstr "切æ›ç‹€æ…‹ï¼šé—œé–‰"
msgid "ToggleButton|Toggle Status: ON"
-msgstr ""
+msgstr "切æ›ç‹€æ…‹ï¼šé–‹å•Ÿ"
+
+msgid "Too many changes to show."
+msgstr "太多的更改。"
msgid "Total Time"
msgstr "總時間"
msgid "Total test time for all commits/merges"
-msgstr "åˆä½µ (merge) 與更動記錄 (commit) 的總測試時間"
+msgstr "åˆä½µ/更動記錄的總測試時間"
msgid "Total: %{total}"
-msgstr ""
-
-msgid "Track activity with Contribution Analytics."
-msgstr ""
-
-msgid "Track groups of issues that share a theme, across projects and milestones"
-msgstr ""
+msgstr "總計: %{total}"
msgid "Track time with quick actions"
-msgstr ""
+msgstr "快速使用時間追蹤工具"
msgid "Trigger this manual action"
-msgstr ""
+msgstr "啟動此任務"
-msgid "Turn on Service Desk"
-msgstr ""
+msgid "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."
+msgstr "觸發器å¯ä»¥å¼·åˆ¶ä½¿ç”¨API​​調用é‡å»ºç‰¹å®šçš„分支或標記。這些令牌將模仿他們的關è¯ç”¨æˆ¶ï¼ŒåŒ…括他們å°é …ç›®çš„è¨ªå•æ¬Šé™ã€‚"
-msgid "Unknown"
-msgstr ""
+msgid "Try again"
+msgstr "é‡è©¦"
+
+msgid "Unable to load the diff. %{button_try_again}"
+msgstr "無法載入差異。%{button_try_again}"
msgid "Unlock"
msgstr "解鎖"
@@ -4500,28 +4833,47 @@ msgid "Unlocked"
msgstr "已解鎖"
msgid "Unresolve discussion"
-msgstr ""
+msgstr "釿–°è¨Žè«–"
+
+msgid "Unstage all changes"
+msgstr "å–æ¶ˆæš«å­˜æ‰€æœ‰è®Šæ›´"
+
+msgid "Unstage changes"
+msgstr "未暫存的變更"
+
+msgid "Unstaged"
+msgstr "未暫存的"
+
+msgid "Unstaged %{type}"
+msgstr "未暫存的 %{type}"
+
+msgid "Unstaged and staged %{type}"
+msgstr "暫存與未暫存的 %{type}"
msgid "Unstar"
msgstr "å–æ¶ˆæ”¶è—"
-msgid "Up to date"
-msgstr ""
+msgid "Unsubscribe"
+msgstr "解除訂閱"
-msgid "Upgrade your plan to activate Advanced Global Search."
-msgstr ""
+msgid "Unsubscribe at group level"
+msgstr "解除群組訂閱"
-msgid "Upgrade your plan to activate Contribution Analytics."
-msgstr ""
+msgid "Unsubscribe at project level"
+msgstr "解除專案訂閱"
-msgid "Upgrade your plan to activate Group Webhooks."
-msgstr ""
+msgid "Unverified"
+msgstr "未驗證"
-msgid "Upgrade your plan to activate Issue weight."
-msgstr ""
+msgid "Up to date"
+msgstr "已經是最新"
-msgid "Upgrade your plan to improve Issue boards."
-msgstr ""
+msgid "Update %{files}"
+msgid_plural "Update %{files} files"
+msgstr[0] "更新 %{files} 檔案"
+
+msgid "Update your group name, description, avatar, and other general settings."
+msgstr "更新您的群組å稱ã€èªªæ˜Žã€é ­åƒèˆ‡å…¶ä»–一般設定。"
msgid "Upload New File"
msgstr "上傳新檔案"
@@ -4530,70 +4882,76 @@ msgid "Upload file"
msgstr "上傳檔案"
msgid "Upload new avatar"
-msgstr ""
+msgstr "上傳大頭貼"
msgid "UploadLink|click to upload"
msgstr "點擊上傳"
msgid "Upvotes"
-msgstr ""
+msgstr "讚"
msgid "Usage statistics"
-msgstr ""
+msgstr "使用情形統計資料"
-msgid "Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab"
-msgstr ""
+msgid "Use group milestones to manage issues from multiple projects in the same milestone."
+msgstr "使用群組里程碑從多個專案中於åŒä¸€å€‹é‡Œç¨‹ç¢‘管ç†è­°é¡Œã€‚"
msgid "Use the following registration token during setup:"
-msgstr "在安è£éŽç¨‹ä¸­ä½¿ç”¨æ­¤è¨»å†Šæ†‘è­‰ (registration token):"
+msgstr "在安è£éŽç¨‹ä¸­ä½¿ç”¨æ­¤è¨»å†Šæ†‘è­‰:"
msgid "Use your global notification setting"
msgstr "使用全域通知設定"
-msgid "Used by members to sign in to your group in GitLab"
-msgstr ""
-
msgid "User and IP Rate Limits"
-msgstr ""
+msgstr "使用者與 IP 速率é™åˆ¶"
+
+msgid "Users"
+msgstr "使用者"
+
+msgid "Variables"
+msgstr "變數"
msgid "Variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. You can use variables for passwords, secret keys, or whatever you want."
-msgstr ""
+msgstr "變數通éŽé‹è¡Œå™¨æ‡‰ç”¨æ–¼ç’°å¢ƒã€‚å—ä¿è­·çš„è®Šæ•¸å°‡åªæ‡‰ç”¨æ–¼å—ä¿è­·çš„分支或標籤。你å¯ä»¥ç”¨æ–¼å„²å­˜å¯†ç¢¼ï¼Œå¯†é‘°æˆ–任何你想è¦çš„變數。"
msgid "Various container registry settings."
-msgstr ""
+msgstr "å„種容器的登錄表設定。"
msgid "Various email settings."
-msgstr ""
+msgstr "å„種電å­ä¿¡ç®±è¨­å®šã€‚"
msgid "Various settings that affect GitLab performance."
-msgstr ""
-
-msgid "View and edit lines"
-msgstr ""
+msgstr "å„種會影響 GitLab 效能的設定。"
-msgid "View epics list"
-msgstr ""
+msgid "Verified"
+msgstr "已驗證"
msgid "View file @ "
msgstr "ç€è¦½æª”案 @ "
msgid "View group labels"
-msgstr ""
+msgstr "查看群組標籤"
+
+msgid "View jobs"
+msgstr "查看工作"
msgid "View labels"
-msgstr ""
+msgstr "顯示標籤"
+
+msgid "View log"
+msgstr "查看日誌"
msgid "View open merge request"
-msgstr "查看此分支的åˆä½µè«‹æ±‚ (merge request)"
+msgstr "查看此分支的åˆä½µè«‹æ±‚"
msgid "View project labels"
-msgstr ""
+msgstr "查看專案標籤"
msgid "View replaced file @ "
msgstr "ç€è¦½å·²æ›¿æ›æª”案 @ "
msgid "Visibility and access controls"
-msgstr ""
+msgstr "å¯è¦‹æ€§èˆ‡å­˜å–控制"
msgid "VisibilityLevel|Internal"
msgstr "內部"
@@ -4610,35 +4968,29 @@ msgstr "䏿˜Ž"
msgid "Want to see the data? Please ask an administrator for access."
msgstr "權é™ä¸è¶³ã€‚如需查看相關資料,請å‘管ç†å“¡ç”³è«‹æ¬Šé™ã€‚"
-msgid "We could not verify that one of your projects on GCP has billing enabled. Please try again."
-msgstr ""
-
msgid "We don't have enough data to show this stage."
msgstr "因該階段的資料ä¸è¶³è€Œç„¡æ³•顯示相關資訊"
msgid "We want to be sure it is you, please confirm you are not a robot."
-msgstr ""
+msgstr "我們è¦ç¢ºå®šä½ ä¸æ˜¯æ©Ÿå™¨äººã€‚"
msgid "Web IDE"
-msgstr ""
+msgstr "ç¶²é  IDE"
msgid "Web terminal"
-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 ""
+msgstr "ç¶²é çµ‚端器"
-msgid "Weight"
-msgstr ""
+msgid "When a runner is locked, it cannot be assigned to other projects"
+msgstr "當執行器被鎖定,其將ä¸èƒ½è¢«å…¶ä»–專案指定。"
-msgid "When leaving the URL blank, classification labels can still be specified whitout disabling cross project features or performing external authorization checks."
-msgstr ""
+msgid "When enabled, users cannot use GitLab until the terms have been accepted."
+msgstr "ç•¶å•Ÿç”¨ï¼Œä½¿ç”¨è€…åœ¨æŽ¥å—æ¢æ¬¾ä¹‹å‰éƒ½ä¸èƒ½ä½¿ç”¨ GitLab。"
msgid "Wiki"
msgstr "Wiki"
msgid "WikiClone|Clone your wiki"
-msgstr "複製(clone)您的 Wiki"
+msgstr "複製您的 Wiki"
msgid "WikiClone|Git Access"
msgstr "Git 讀å–"
@@ -4653,13 +5005,37 @@ msgid "WikiClone|Start Gollum and edit locally"
msgstr "開始你的 Gollum 並在本機編輯。"
msgid "WikiEditPageTip|Tip: You can move this page by adding the path to the beginning of the title."
-msgstr ""
+msgstr "æç¤ºï¼šä½ å¯ä»¥åœ¨æ¨™é¡Œé–‹é ­åŠ ä¸Šè·¯å¾‘ï¼Œé€™å°‡æœƒç§»å‹•é€™å€‹é é¢ã€‚"
msgid "WikiEdit|There is already a page with the same title in that path."
-msgstr ""
+msgstr "åœ¨è©²è·¯å¾‘ä¸­å·²ç¶“æœ‰ç›¸åŒæ¨™é¡Œçš„é é¢ã€‚"
+
+msgid "WikiEmptyIssueMessage|Suggest wiki improvement"
+msgstr "建議 Wiki 改善"
+
+msgid "WikiEmptyIssueMessage|You must be a project member in order to add wiki pages. If you have suggestions for how to improve the wiki for this project, consider opening an issue in the %{issues_link}."
+msgstr "您必須是專案æˆå“¡æ‰èƒ½å»ºç«‹ Wiki é é¢ã€‚如果你有為此專案改善 Wiki 的建議,請考慮在 %{issues_link} 開啟一個議題。"
+
+msgid "WikiEmptyIssueMessage|issue tracker"
+msgstr "議題追蹤器"
+
+msgid "WikiEmpty|A wiki is where you can store all the details about your project. This can include why you've created it, its principles, how to use it, and so on."
+msgstr "å¯ä»¥å„²å­˜æ‚¨å°ˆæ¡ˆçš„æ‰€æœ‰è©³æƒ…。這å¯ä»¥åŒ…嫿‚¨å»ºç«‹ä»–的原因ã€åŽŸç†ã€å¦‚何使用等等。"
+
+msgid "WikiEmpty|Create your first page"
+msgstr "建立您的第一個é é¢"
-msgid "WikiEmptyPageError|You are not allowed to create wiki pages"
-msgstr "你沒有權é™å»ºç«‹ Wiki é é¢"
+msgid "WikiEmpty|Suggest wiki improvement"
+msgstr "建議 Wiki 改善"
+
+msgid "WikiEmpty|The wiki lets you write documentation for your project"
+msgstr "Wiki å…許您為您的專案撰寫文件"
+
+msgid "WikiEmpty|This project has no wiki pages"
+msgstr "此專案沒有任何 Wiki é é¢"
+
+msgid "WikiEmpty|You must be a project member in order to add wiki pages."
+msgstr "您必須是專案æˆå“¡æ‰èƒ½å»ºç«‹ Wiki é é¢ã€‚"
msgid "WikiHistoricalPage|This is an old version of this page."
msgstr "這個é é¢è¼ƒèˆŠçš„版本。"
@@ -4694,6 +5070,12 @@ msgstr "新的維基é é¢"
msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?"
msgstr "你確定è¦åˆªé™¤é€™å€‹é é¢ï¼Ÿ"
+msgid "WikiPageConfirmDelete|Delete page"
+msgstr "刪除é é¢"
+
+msgid "WikiPageConfirmDelete|Delete page %{pageTitle}?"
+msgstr "刪除 %{pageTitle} é é¢ï¼Ÿ"
+
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} 並確定你的變更並沒有被他們無æ„中移除。"
@@ -4709,7 +5091,7 @@ msgstr "æ›´æ–° %{page_title}"
msgid "WikiPage|Page slug"
msgstr "é é¢ slug"
-msgid "WikiPage|Write your content or drag files here..."
+msgid "WikiPage|Write your content or drag files here…"
msgstr "填寫內容或拖曳檔案至此..."
msgid "Wiki|Create Page"
@@ -4721,9 +5103,6 @@ msgstr "建立é é¢"
msgid "Wiki|Edit Page"
msgstr "編輯é é¢"
-msgid "Wiki|Empty page"
-msgstr "空白é é¢"
-
msgid "Wiki|More Pages"
msgstr "更多é é¢"
@@ -4742,75 +5121,78 @@ 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 "å–æ¶ˆæ¬Šé™ç”³è«‹"
-msgid "Write a commit message..."
-msgstr ""
+msgid "Yes"
+msgstr "是"
msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
msgstr "å°‡è¦åˆªé™¤ %{group_name}。被刪除的群組無法復原ï¼çœŸçš„「確定ã€è¦é€™éº¼åšå—Žï¼Ÿ"
msgid "You are going to remove %{project_full_name}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?"
-msgstr ""
+msgstr "å°‡è¦åˆªé™¤ %{project_full_name}。被刪除的專案無法復原ï¼çœŸçš„「確定ã€è¦é€™éº¼åšå—Žï¼Ÿ"
msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?"
msgstr "å°‡è¦åˆªé™¤æœ¬åˆ†æ”¯å°ˆæ¡ˆèˆ‡ä¸»å¹¹ %{forked_from_project} 的所有關è¯ã€‚ 真的「確定ã€è¦é€™éº¼åšå—Žï¼Ÿ"
msgid "You are going to transfer %{project_full_name} to another owner. Are you ABSOLUTELY sure?"
-msgstr ""
+msgstr "å°‡è¦æŠŠ %{project_full_name} 的所有權轉移給å¦ä¸€å€‹äººã€‚真的「確定ã€è¦é€™éº¼åšå—Žï¼Ÿ"
msgid "You are on a read-only GitLab instance."
-msgstr ""
+msgstr "您在唯讀的 GitLab 主機上。"
-msgid "You are on a secondary (read-only) Geo node. If you want to make any changes, you must visit the %{primary_node}."
-msgstr ""
+msgid "You can %{linkStart}view the blob%{linkEnd} instead."
+msgstr "您å¯ä»¥ %{linkStart}查看BLOB%{linkEnd} 。"
msgid "You can also create a project from the command line."
-msgstr ""
+msgstr "您也å¯ä»¥å¾žæŒ‡ä»¤æ–°å¢žä¸€å€‹å°ˆæ¡ˆã€‚"
msgid "You can also star a label to make it a priority label."
-msgstr ""
+msgstr "您還å¯ä»¥ç‚ºæŸå€‹æ¨™ç±¤åŠ ä¸Šæ˜Ÿæ˜Ÿï¼Œä½¿å…¶æˆç‚ºå„ªå…ˆæ¨™ç±¤ã€‚"
+
+msgid "You can also test your .gitlab-ci.yml in the %{linkStart}Lint%{linkEnd}"
+msgstr "您也å¯ä»¥åœ¨ %{linkStart}Lint%{linkEnd} 測試您的 .gitlab-ci.yml"
msgid "You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}"
-msgstr ""
+msgstr "您å¯ä»¥è¼•鬆地在 Kubernetes å¢é›†ä¸Šå®‰è£é‹è¡Œå™¨ã€‚ %{link_to_help_page}"
msgid "You can move around the graph by using the arrow keys."
-msgstr ""
+msgstr "您å¯ä»¥ä½¿ç”¨ä¸Šä¸‹å·¦å³éµç§»å‹•圖形。"
msgid "You can only add files when you are on a branch"
-msgstr "åªèƒ½åœ¨åˆ†æ”¯ (branch) 上建立檔案"
+msgstr "åªèƒ½åœ¨åˆ†æ”¯ä¸Šå»ºç«‹æª”案"
msgid "You can only edit files when you are on a branch"
-msgstr ""
+msgstr "您åªèƒ½åœ¨åˆ†æ”¯ä¸Šç·¨è¼¯æ–‡ä»¶"
-msgid "You cannot write to a read-only secondary GitLab Geo instance. Please use %{link_to_primary_node} instead."
-msgstr ""
+msgid "You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}"
+msgstr "您å¯ä»¥é€éŽä½¿ç”¨äº’動模å¼é¸æ“‡ %{use_ours} 或 %{use_theirs} æŒ‰éˆ•ã€æˆ–者是直接編輯檔案來解決åˆä½µè¡çªï¼Œä¸¦å°‡é€™äº›è®Šæ›´æäº¤åˆ° %{branch_name}。"
msgid "You cannot write to this read-only GitLab instance."
msgstr "您ä¸èƒ½ä¿®æ”¹é€™å€‹å”¯è®€çš„ GitLab 主機。"
-msgid "You do not have the correct permissions to override the settings from the LDAP group sync."
-msgstr ""
+msgid "You do not have any assigned merge requests"
+msgstr "您沒有被分é…çš„åˆä½µè«‹æ±‚"
msgid "You have no permissions"
-msgstr ""
+msgstr "你沒有權é™"
+
+msgid "You have not created any merge requests"
+msgstr "您尚未創建任何åˆä½µè«‹æ±‚"
msgid "You have reached your project limit"
msgstr "您已é”到專案數é‡é™åˆ¶"
-msgid "You must have master access to force delete a lock"
-msgstr ""
+msgid "You must accept our Terms of Service and privacy policy in order to register an account"
+msgstr "æ‚¨å¿…é ˆæŽ¥å—æˆ‘們的æœå‹™æ¢æ¬¾å’Œéš±ç§æ”¿ç­–æ‰èƒ½è¨»å†Šå¸³æˆ¶"
+
+msgid "You must have maintainer access to force delete a lock"
+msgstr "æ‚¨å¿…é ˆæ“æœ‰ç¶­è­·è€…å­˜å–æ‰èƒ½å¼·åˆ¶ç§»é™¤éŽ–å®š"
msgid "You must sign in to star a project"
msgstr "必須登入æ‰èƒ½æ”¶è—專案"
-msgid "You need a different license to enable FileLocks feature"
-msgstr ""
-
msgid "You need permission."
msgstr "éœ€è¦æ¬Šé™æ‰èƒ½é€™éº¼åšã€‚"
@@ -4830,40 +5212,40 @@ 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 "在帳號上 %{set_password_link} 之å‰ï¼Œ 將無法使用 %{protocol} 上傳 (push) 或下載 (pull) 程å¼ç¢¼ã€‚"
+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) 程å¼ç¢¼ã€‚"
+msgstr "在個人帳號中 %{add_ssh_key_link} 之å‰ï¼Œ 將無法使用 SSH 上傳或下載程å¼ç¢¼ã€‚"
msgid "You won't be able to pull or push project code via SSH until you add an SSH key to your profile"
-msgstr ""
+msgstr "在您的個人資料中添加 SSH 密鑰之å‰ï¼Œä½ ä¸èƒ½é€éŽ SSH ä¾†æ‹‰å–æˆ–推é€å°ˆæ¡ˆç¨‹å¼ç¢¼ã€‚"
msgid "You'll need to use different branch names to get a valid comparison."
-msgstr ""
+msgstr "你需è¦é¸æ“‡å…©å€‹ä¸åŒçš„分支,æ‰èƒ½é€²è¡Œæ¯”較。"
msgid "You're receiving this email because of your account on %{host}. %{manage_notifications_link} &middot; %{help_link}"
-msgstr ""
+msgstr "你收到此電å­éƒµä»¶æ˜¯å› ç‚ºä½ çš„帳戶在 %{host}. %{manage_notifications_link} &middot; %{help_link}"
msgid "Your Groups"
-msgstr ""
+msgstr "你的群組"
msgid "Your Kubernetes cluster information on this page is still editable, but you are advised to disable and reconfigure"
-msgstr ""
+msgstr "您的Kuberneteså¢é›†è³‡è¨Šä»ç„¶å¯ä»¥ç·¨è¼¯ï¼Œä½†å»ºè­°æ‚¨ç¦ç”¨ä¸¦é‡æ–°è¨­ç½®ã€‚"
msgid "Your Projects (default)"
-msgstr ""
+msgstr "您的專案(é è¨­å€¼ï¼‰"
msgid "Your Projects' Activity"
-msgstr ""
+msgstr "您的專案活動"
msgid "Your Todos"
-msgstr ""
+msgstr "您的待辦事項"
msgid "Your changes can be committed to %{branch_name} because a merge request is open."
-msgstr ""
+msgstr "你的更改將å¯ä»¥æäº¤åˆ° %{branch_name} 因為åˆä½µè«‹æ±‚已打開。"
msgid "Your changes have been committed. Commit %{commitId} %{commitStats}"
-msgstr ""
+msgstr "您的更改已被æäº¤ã€‚更動紀錄 %{commitId} %{commitStats}"
msgid "Your comment will not be visible to the public."
msgstr "ä½ çš„ç•™è¨€å°‡ä¸æœƒè¢«å…¬é–‹ã€‚"
@@ -4877,337 +5259,220 @@ msgstr "您的åå­—"
msgid "Your projects"
msgstr "你的計劃"
-msgid "among other things"
-msgstr ""
+msgid "ago"
+msgstr "之å‰"
-msgid "and %d fixed vulnerability"
-msgid_plural "and %d fixed vulnerabilities"
-msgstr[0] ""
+msgid "among other things"
+msgstr "除了其他事情"
msgid "assign yourself"
-msgstr ""
+msgstr "指派給自己"
msgid "branch name"
-msgstr ""
-
-msgid "by"
-msgstr ""
-
-msgid "ciReport|%{type} detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|%{type} detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Code quality"
-msgstr ""
-
-msgid "ciReport|DAST detected no alerts by analyzing the review app"
-msgstr ""
-
-msgid "ciReport|Dependency scanning"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Dependency scanning detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|Failed to load %{reportName} report"
-msgstr ""
-
-msgid "ciReport|Fixed:"
-msgstr ""
-
-msgid "ciReport|Instances"
-msgstr ""
-
-msgid "ciReport|Learn more about whitelisting"
-msgstr ""
-
-msgid "ciReport|Loading %{reportName} report"
-msgstr ""
-
-msgid "ciReport|No changes to code quality"
-msgstr ""
-
-msgid "ciReport|No changes to performance metrics"
-msgstr ""
-
-msgid "ciReport|Performance metrics"
-msgstr ""
-
-msgid "ciReport|SAST"
-msgstr ""
-
-msgid "ciReport|SAST detected"
-msgstr ""
-
-msgid "ciReport|SAST detected no new security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|SAST detected no security vulnerabilities"
-msgstr ""
-
-msgid "ciReport|SAST:container no vulnerabilities were found"
-msgstr ""
-
-msgid "ciReport|Security scanning"
-msgstr ""
-
-msgid "ciReport|Security scanning failed loading any results"
-msgstr ""
-
-msgid "ciReport|Show complete code vulnerabilities report"
-msgstr ""
-
-msgid "ciReport|Unapproved vulnerabilities (red) can be marked as approved. %{helpLink}"
-msgstr ""
-
-msgid "ciReport|no vulnerabilities"
-msgstr ""
+msgstr "分支å稱"
msgid "command line instructions"
-msgstr ""
+msgstr "指令說明"
msgid "connecting"
-msgstr ""
-
-msgid "could not read private key, is the passphrase correct?"
-msgstr ""
+msgstr "連線中"
msgid "day"
msgid_plural "days"
msgstr[0] "天"
-msgid "detected %d fixed vulnerability"
-msgid_plural "detected %d fixed vulnerabilities"
-msgstr[0] ""
+msgid "deploy token"
+msgstr "部署憑證"
-msgid "detected %d new vulnerability"
-msgid_plural "detected %d new vulnerabilities"
-msgstr[0] ""
+msgid "disabled"
+msgstr "å·²åœç”¨"
-msgid "detected no vulnerabilities"
-msgstr ""
+msgid "enabled"
+msgstr "已啟用"
msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
-msgstr ""
+msgstr "%{slash_command} 將更新é ä¼°èŠ±è²»æ™‚é–“ã€‚"
-msgid "here"
-msgstr ""
+msgid "for this project"
+msgstr "為此專案"
msgid "importing"
-msgstr ""
-
-msgid "in progress"
-msgstr ""
-
-msgid "is invalid because there is downstream lock"
-msgstr ""
-
-msgid "is invalid because there is upstream lock"
-msgstr ""
-
-msgid "is not a valid X509 certificate."
-msgstr ""
+msgstr "輸入"
-msgid "locked by %{path_lock_user_name} %{created_at}"
-msgstr ""
+msgid "latest version"
+msgstr "最新版本"
msgid "merge request"
msgid_plural "merge requests"
-msgstr[0] ""
+msgstr[0] "åˆä½µè«‹æ±‚"
msgid "mrWidget| Please restore it or use a different %{missingBranchName} branch"
-msgstr ""
+msgstr "è«‹æ¢å¾©å®ƒæˆ–使用一個ä¸åŒ %{missingBranchName} 的分支"
msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} decreased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB"
-msgstr ""
+msgstr "%{metricsLinkStart} 記憶體 %{metricsLinkEnd} 使用 %{emphasisStart} ä¸‹é™ %{emphasisEnd} 從 %{memoryFrom} MB 到 %{memoryTo} MB"
msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} increased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB"
-msgstr ""
+msgstr "%{metricsLinkStart} 記憶體 %{metricsLinkEnd} 使用 %{emphasisStart} ä¸Šå‡ %{emphasisEnd} 從 %{memoryFrom} MB 到 %{memoryTo} MB"
msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage is %{emphasisStart} unchanged %{emphasisEnd} at %{memoryFrom}MB"
-msgstr ""
-
-msgid "mrWidget|Add approval"
-msgstr ""
-
-msgid "mrWidget|Allows edits from maintainers"
-msgstr ""
+msgstr "%{metricsLinkStart} 記憶體 %{metricsLinkEnd} 使用 %{emphasisStart} 沒有發生變化 %{emphasisEnd} 於 %{memoryFrom} MB"
-msgid "mrWidget|An error occured while removing your approval."
-msgstr ""
-
-msgid "mrWidget|An error occured while retrieving approval data for this merge request."
-msgstr ""
-
-msgid "mrWidget|An error occured while submitting your approval."
-msgstr ""
-
-msgid "mrWidget|Approve"
-msgstr ""
-
-msgid "mrWidget|Approved"
-msgstr ""
-
-msgid "mrWidget|Approved by"
-msgstr ""
+msgid "mrWidget|Allows commits from members who can merge to the target branch"
+msgstr "å…許å¯ä»¥åˆä½µåˆ°ç›®æ¨™åˆ†æ”¯çš„æˆå“¡æäº¤"
msgid "mrWidget|Cancel automatic merge"
-msgstr ""
+msgstr "å–æ¶ˆè‡ªå‹•åˆä½µ"
msgid "mrWidget|Check out branch"
-msgstr ""
+msgstr "é·å‡ºæ­¤åˆ†æ”¯"
msgid "mrWidget|Checking ability to merge automatically"
-msgstr ""
+msgstr "檢查是å¦å¯ä»¥è‡ªå‹•åˆä½µ"
msgid "mrWidget|Cherry-pick"
-msgstr ""
+msgstr "挑é¸"
msgid "mrWidget|Cherry-pick this merge request in a new merge request"
-msgstr ""
+msgstr "æŒ‘é¸æ­¤åˆä½µè«‹æ±‚的修訂版本以建立新的åˆä½µè«‹æ±‚"
msgid "mrWidget|Closed"
-msgstr ""
+msgstr "關閉"
msgid "mrWidget|Closed by"
-msgstr ""
+msgstr "關閉"
msgid "mrWidget|Closes"
-msgstr ""
+msgstr "關閉"
+
+msgid "mrWidget|Create an issue to resolve them later"
+msgstr "建立一個議題,以在ç¨å€™è§£æ±º"
msgid "mrWidget|Deployment statistics are not available currently"
-msgstr ""
+msgstr "ç›®å‰ç„¡æ³•使用部署的統計資料"
msgid "mrWidget|Did not close"
-msgstr ""
+msgstr "沒有關閉"
msgid "mrWidget|Email patches"
-msgstr ""
+msgstr "Email 補ä¸"
msgid "mrWidget|Failed to load deployment statistics"
-msgstr ""
+msgstr "讀å–部署統計資料時發生錯誤"
msgid "mrWidget|If the %{branch} branch exists in your local repository, you can merge this merge request manually using the"
-msgstr ""
+msgstr "如果 %{branch} 分支存在於您的本地檔案庫,你å¯ä»¥æ‰‹å‹•åˆä½µæ­¤åˆä½µè«‹æ±‚,藉由"
msgid "mrWidget|If the %{missingBranchName} branch exists in your local repository, you can merge this merge request manually using the command line"
-msgstr ""
+msgstr "如果 %{missingBranchName} 分支徂在於您的本地檔案庫,您å¯ä»¥æ‰‹å‹•åˆä½µæ­¤åˆä½µè«‹æ±‚藉由此指令"
msgid "mrWidget|Loading deployment statistics"
-msgstr ""
+msgstr "讀å–部署統計資料"
msgid "mrWidget|Mentions"
-msgstr ""
+msgstr "æåˆ°"
msgid "mrWidget|Merge"
-msgstr ""
+msgstr "åˆä½µ"
msgid "mrWidget|Merge failed."
-msgstr ""
+msgstr "åˆä½µå¤±æ•—"
msgid "mrWidget|Merge locally"
-msgstr ""
+msgstr "本地åˆä½µ"
msgid "mrWidget|Merged by"
-msgstr ""
+msgstr "åˆä½µè€…"
msgid "mrWidget|Plain diff"
-msgstr ""
+msgstr "本文差異"
msgid "mrWidget|Refresh"
-msgstr ""
+msgstr "釿–°æ•´ç†"
msgid "mrWidget|Refresh now"
-msgstr ""
+msgstr "ç«‹å³é‡æ–°æ•´ç†"
msgid "mrWidget|Refreshing now"
-msgstr ""
+msgstr "ç«‹å³é‡æ–°æ•´ç†"
msgid "mrWidget|Remove Source Branch"
-msgstr ""
+msgstr "刪除原始分支"
msgid "mrWidget|Remove source branch"
-msgstr ""
-
-msgid "mrWidget|Remove your approval"
-msgstr ""
+msgstr "刪除原始分支"
msgid "mrWidget|Request to merge"
-msgstr ""
+msgstr "請求åˆä½µ"
msgid "mrWidget|Resolve conflicts"
-msgstr ""
+msgstr "解決è¡çª"
msgid "mrWidget|Revert"
-msgstr ""
+msgstr "還原"
msgid "mrWidget|Revert this merge request in a new merge request"
-msgstr ""
+msgstr "é€éŽæ–°çš„åˆä½µè«‹æ±‚還原此åˆä½µè«‹æ±‚更動的項目"
msgid "mrWidget|Set by"
-msgstr ""
+msgstr "設置者"
msgid "mrWidget|The changes were merged into"
-msgstr ""
+msgstr "這些更改將會åˆä½µåˆ°"
msgid "mrWidget|The changes were not merged into"
-msgstr ""
+msgstr "é€™äº›æ›´æ”¹å°‡ä¸æœƒåˆä½µåˆ°"
msgid "mrWidget|The changes will be merged into"
-msgstr ""
+msgstr "這些更改將會åˆä½µåˆ°"
msgid "mrWidget|The source branch has been removed"
-msgstr ""
+msgstr "來æºåˆ†æ”¯å·²ç¶“被刪除"
msgid "mrWidget|The source branch is being removed"
-msgstr ""
+msgstr "來æºåˆ†æ”¯æ­£åœ¨è¢«åˆªé™¤"
msgid "mrWidget|The source branch will be removed"
-msgstr ""
+msgstr "來æºåˆ†æ”¯å°‡æœƒè¢«åˆªé™¤"
msgid "mrWidget|The source branch will not be removed"
-msgstr ""
+msgstr "來æºåˆ†æ”¯å°‡ä¸æœƒè¢«åˆªé™¤"
msgid "mrWidget|There are merge conflicts"
-msgstr ""
+msgstr "發生åˆä½µè¡çª"
+
+msgid "mrWidget|There are unresolved discussions. Please resolve these discussions"
+msgstr "有個未解決的討論,請解決這些討論。"
msgid "mrWidget|This merge request failed to be merged automatically"
-msgstr ""
+msgstr "æ­¤åˆä½µè«‹æ±‚未能自動åˆä½µ"
msgid "mrWidget|This merge request is in the process of being merged"
-msgstr ""
+msgstr "æ­¤åˆä½µè«‹æ±‚正在被åˆä½µ"
msgid "mrWidget|This project is archived, write access has been disabled"
-msgstr ""
+msgstr "這個專案已經被打包,無法寫入"
msgid "mrWidget|Web IDE"
-msgstr ""
+msgstr "ç¶²é  IDE"
msgid "mrWidget|You can merge this merge request manually using the"
-msgstr ""
+msgstr "ä½ å¯ä»¥æ‰‹å‹•åˆä½µé€™å€‹åˆä½µè«‹æ±‚,藉由"
msgid "mrWidget|You can remove source branch now"
-msgstr ""
+msgstr "ä½ ç¾åœ¨å¯ä»¥åˆªé™¤ä¾†æºåˆ†æ”¯"
msgid "mrWidget|branch does not exist."
-msgstr ""
+msgstr "分支並ä¸å­˜åœ¨"
msgid "mrWidget|command line"
-msgstr ""
+msgstr "指令"
msgid "mrWidget|into"
-msgstr ""
+msgstr "進入"
msgid "mrWidget|to be merged automatically when the pipeline succeeds"
-msgstr ""
+msgstr "ç•¶æµæ°´ç·šå®Œæˆæ™‚將會被自動åˆä½µ"
msgid "new merge request"
msgstr "建立åˆä½µè«‹æ±‚"
@@ -5216,7 +5481,7 @@ msgid "notification emails"
msgstr "通知信"
msgid "or"
-msgstr ""
+msgstr "或"
msgid "parent"
msgid_plural "parents"
@@ -5226,32 +5491,33 @@ msgid "password"
msgstr "密碼"
msgid "personal access token"
-msgstr "ç§äººå­˜å–憑證 (access token)"
+msgstr "ç§äººå­˜å–憑證"
-msgid "private key does not match certificate."
-msgstr ""
+msgid "remaining"
+msgstr "剩餘"
msgid "remove due date"
-msgstr ""
+msgstr "刪除截止日期"
msgid "source"
-msgstr ""
+msgstr "來æº"
msgid "spendCommand|%{slash_command} will update the sum of the time spent."
-msgstr ""
+msgstr "%{slash_command} 將會更新ã€åŠ ç¸½æ‰€èŠ±è²»çš„æ™‚é–“"
msgid "this document"
-msgstr ""
-
-msgid "to help your contributors communicate effectively!"
-msgstr ""
+msgstr "這份文件"
msgid "username"
msgstr "使用者å稱"
msgid "uses Kubernetes clusters to deploy your code!"
-msgstr ""
+msgstr "使用 Kubernetes å¢é›†éƒ¨ç½²æ‚¨çš„程å¼ç¢¼!"
msgid "with %{additions} additions, %{deletions} deletions."
-msgstr ""
+msgstr "共增加 %{additions} ,刪除 %{deletions}"
+
+msgid "within %d minute "
+msgid_plural "within %d minutes "
+msgstr[0] "在 %d 分é˜å…§ "
diff --git a/package.json b/package.json
index 1382afd41d6..26b87c70e98 100644
--- a/package.json
+++ b/package.json
@@ -1,13 +1,15 @@
{
"private": true,
"scripts": {
+ "clean": "rm -rf public/assets tmp/cache/*-loader",
"dev-server": "nodemon -w 'config/webpack.config.js' --exec 'webpack-dev-server --config config/webpack.config.js'",
- "eslint": "eslint --max-warnings 0 --ext .js,.vue .",
- "eslint-fix": "eslint --max-warnings 0 --ext .js,.vue --fix .",
- "eslint-report": "eslint --max-warnings 0 --ext .js,.vue --format html --output-file ./eslint-report.html .",
+ "eslint": "eslint --max-warnings 0 --report-unused-disable-directives --ext .js,.vue .",
+ "eslint-fix": "eslint --max-warnings 0 --report-unused-disable-directives --ext .js,.vue --fix .",
+ "eslint-report": "eslint --max-warnings 0 --ext .js,.vue --format html --output-file ./eslint-report.html --no-inline-config .",
"karma": "BABEL_ENV=${BABEL_ENV:=karma} karma start --single-run true config/karma.config.js",
"karma-coverage": "BABEL_ENV=coverage karma start --single-run true config/karma.config.js",
"karma-start": "BABEL_ENV=karma karma start config/karma.config.js",
+ "postinstall": "node ./scripts/frontend/postinstall.js",
"prettier-staged": "node ./scripts/frontend/prettier.js",
"prettier-staged-save": "node ./scripts/frontend/prettier.js save",
"prettier-all": "node ./scripts/frontend/prettier.js check-all",
@@ -16,7 +18,7 @@
"webpack-prod": "NODE_ENV=production webpack --config config/webpack.config.js"
},
"dependencies": {
- "@gitlab-org/gitlab-svgs": "^1.22.0",
+ "@gitlab-org/gitlab-svgs": "^1.25.0",
"autosize": "^4.0.0",
"axios": "^0.17.1",
"babel-core": "^6.26.3",
@@ -25,14 +27,13 @@
"babel-preset-latest": "^6.24.1",
"babel-preset-stage-2": "^6.24.1",
"blackst0ne-mermaid": "^7.1.0-fixed",
- "bootstrap-sass": "^3.3.6",
+ "bootstrap": "~4.1.1",
"brace-expansion": "^1.1.8",
"cache-loader": "^1.2.2",
"chart.js": "1.0.2",
"classlist-polyfill": "^1.2.0",
"clipboard": "^1.7.1",
"compression-webpack-plugin": "^1.1.11",
- "copy-webpack-plugin": "^4.5.1",
"core-js": "^2.4.1",
"cropper": "^2.3.0",
"css-loader": "^0.28.11",
@@ -44,6 +45,7 @@
"d3-shape": "^1.2.0",
"d3-time": "^1.0.8",
"d3-time-format": "^2.1.1",
+ "dateformat": "^3.0.3",
"deckar01-task_list": "^2.0.0",
"diff": "^3.4.0",
"document-register-element": "1.3.0",
@@ -63,9 +65,11 @@
"jszip-utils": "^0.0.2",
"katex": "^0.8.3",
"marked": "^0.3.12",
- "monaco-editor": "0.10.0",
+ "monaco-editor": "0.13.1",
+ "monaco-editor-webpack-plugin": "^1.4.0",
"mousetrap": "^1.4.6",
"pikaday": "^1.6.1",
+ "popper.js": "^1.14.3",
"prismjs": "^1.6.0",
"raphael": "^2.2.7",
"raven-js": "^3.22.1",
@@ -73,7 +77,9 @@
"sanitize-html": "^1.16.1",
"select2": "3.5.2-browserify",
"sha1": "^1.1.1",
+ "sortablejs": "^1.7.0",
"sql.js": "^0.4.0",
+ "stickyfilljs": "^2.0.5",
"style-loader": "^0.21.0",
"svg4everybody": "2.1.9",
"three": "^0.84.0",
@@ -90,30 +96,30 @@
"vue-template-compiler": "^2.5.16",
"vue-virtual-scroll-list": "^1.2.5",
"vuex": "^3.0.1",
- "webpack": "^4.7.0",
+ "webpack": "^4.11.1",
"webpack-bundle-analyzer": "^2.11.1",
- "webpack-cli": "^2.1.2",
+ "webpack-cli": "^3.0.2",
"webpack-stats-plugin": "^0.2.1",
- "worker-loader": "^1.1.1"
+ "worker-loader": "^2.0.0"
},
"devDependencies": {
"axios-mock-adapter": "^1.15.0",
- "babel-eslint": "^8.0.2",
+ "babel-eslint": "^8.2.3",
"babel-plugin-istanbul": "^4.1.6",
"babel-plugin-rewire": "^1.1.0",
"babel-template": "^6.26.0",
"babel-types": "^6.26.0",
"chalk": "^2.4.1",
"commander": "^2.15.1",
- "eslint": "^3.18.0",
- "eslint-config-airbnb-base": "^10.0.1",
- "eslint-import-resolver-webpack": "^0.8.3",
- "eslint-plugin-filenames": "^1.1.0",
- "eslint-plugin-html": "2.0.1",
- "eslint-plugin-import": "^2.2.0",
+ "eslint": "~4.12.1",
+ "eslint-config-airbnb-base": "^12.1.0",
+ "eslint-import-resolver-webpack": "^0.10.0",
+ "eslint-plugin-filenames": "^1.2.0",
+ "eslint-plugin-html": "4.0.3",
+ "eslint-plugin-import": "^2.12.0",
"eslint-plugin-jasmine": "^2.1.0",
- "eslint-plugin-promise": "^3.5.0",
- "eslint-plugin-vue": "^4.0.1",
+ "eslint-plugin-promise": "^3.8.0",
+ "eslint-plugin-vue": "^4.5.0",
"ignore": "^3.3.7",
"istanbul": "^0.4.5",
"jasmine-core": "^2.9.0",
@@ -126,7 +132,7 @@
"karma-sourcemap-loader": "^0.3.7",
"karma-webpack": "3.0.0",
"nodemon": "^1.17.3",
- "prettier": "1.11.1",
+ "prettier": "1.12.1",
"webpack-dev-server": "^3.1.4"
}
}
diff --git a/public/favicon.ico b/public/favicon.ico
deleted file mode 100644
index 3479cbbb46f..00000000000
--- a/public/favicon.ico
+++ /dev/null
Binary files differ
diff --git a/qa/Dockerfile b/qa/Dockerfile
index 77cee9c5461..abf2184e1e2 100644
--- a/qa/Dockerfile
+++ b/qa/Dockerfile
@@ -28,6 +28,15 @@ RUN apt-get update -q && apt-get install -y google-chrome-stable && apt-get clea
RUN wget -q https://chromedriver.storage.googleapis.com/$(wget -q -O - https://chromedriver.storage.googleapis.com/LATEST_RELEASE)/chromedriver_linux64.zip
RUN unzip chromedriver_linux64.zip -d /usr/local/bin
+##
+# Install gcloud and kubectl CLI used in Auto DevOps test to create K8s
+# clusters
+#
+RUN export CLOUD_SDK_REPO="cloud-sdk-$(lsb_release -c -s)" && \
+ echo "deb http://packages.cloud.google.com/apt $CLOUD_SDK_REPO main" | tee -a /etc/apt/sources.list.d/google-cloud-sdk.list && \
+ curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - && \
+ apt-get update -y && apt-get install google-cloud-sdk kubectl -y
+
WORKDIR /home/qa
COPY ./Gemfile* ./
RUN bundle install
diff --git a/qa/qa.rb b/qa/qa.rb
index 40e12c8b336..be7bcf7a2ad 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -12,7 +12,11 @@ module QA
autoload :Browser, 'qa/runtime/browser'
autoload :Env, 'qa/runtime/env'
autoload :Address, 'qa/runtime/address'
- autoload :API, 'qa/runtime/api'
+
+ module API
+ autoload :Client, 'qa/runtime/api/client'
+ autoload :Request, 'qa/runtime/api/request'
+ end
module Key
autoload :Base, 'qa/runtime/key/base'
@@ -36,15 +40,21 @@ module QA
autoload :Issue, 'qa/factory/resource/issue'
autoload :Project, 'qa/factory/resource/project'
autoload :MergeRequest, 'qa/factory/resource/merge_request'
+ autoload :ProjectImportedFromGithub, 'qa/factory/resource/project_imported_from_github'
autoload :DeployKey, 'qa/factory/resource/deploy_key'
autoload :Branch, 'qa/factory/resource/branch'
autoload :SecretVariable, 'qa/factory/resource/secret_variable'
autoload :Runner, 'qa/factory/resource/runner'
autoload :PersonalAccessToken, 'qa/factory/resource/personal_access_token'
+ autoload :KubernetesCluster, 'qa/factory/resource/kubernetes_cluster'
+ autoload :ProjectMilestone, 'qa/factory/resource/project_milestone'
+ autoload :Wiki, 'qa/factory/resource/wiki'
end
module Repository
autoload :Push, 'qa/factory/repository/push'
+ autoload :ProjectPush, 'qa/factory/repository/project_push'
+ autoload :WikiPush, 'qa/factory/repository/wiki_push'
end
module Settings
@@ -71,7 +81,9 @@ module QA
autoload :Instance, 'qa/scenario/test/instance'
module Integration
+ autoload :Github, 'qa/scenario/test/integration/github'
autoload :LDAP, 'qa/scenario/test/integration/ldap'
+ autoload :Kubernetes, 'qa/scenario/test/integration/kubernetes'
autoload :Mattermost, 'qa/scenario/test/integration/mattermost'
end
@@ -123,6 +135,10 @@ module QA
autoload :Show, 'qa/page/project/show'
autoload :Activity, 'qa/page/project/activity'
+ module Import
+ autoload :Github, 'qa/page/project/import/github'
+ end
+
module Pipeline
autoload :Index, 'qa/page/project/pipeline/index'
autoload :Show, 'qa/page/project/pipeline/show'
@@ -150,12 +166,40 @@ module QA
autoload :Show, 'qa/page/project/issue/show'
autoload :Index, 'qa/page/project/issue/index'
end
+
+ module Milestone
+ autoload :New, 'qa/page/project/milestone/new'
+ autoload :Index, 'qa/page/project/milestone/index'
+ end
+
+ module Operations
+ module Kubernetes
+ autoload :Index, 'qa/page/project/operations/kubernetes/index'
+ autoload :Add, 'qa/page/project/operations/kubernetes/add'
+ autoload :AddExisting, 'qa/page/project/operations/kubernetes/add_existing'
+ autoload :Show, 'qa/page/project/operations/kubernetes/show'
+ end
+ end
+
+ module Wiki
+ autoload :Edit, 'qa/page/project/wiki/edit'
+ autoload :New, 'qa/page/project/wiki/new'
+ autoload :Show, 'qa/page/project/wiki/show'
+ end
+ end
+
+ module Shared
+ autoload :ClonePanel, 'qa/page/shared/clone_panel'
end
module Profile
autoload :PersonalAccessTokens, 'qa/page/profile/personal_access_tokens'
end
+ module Issuable
+ autoload :Sidebar, 'qa/page/issuable/sidebar'
+ end
+
module MergeRequest
autoload :New, 'qa/page/merge_request/new'
autoload :Show, 'qa/page/merge_request/show'
@@ -178,6 +222,7 @@ module QA
#
module Component
autoload :Dropzone, 'qa/page/component/dropzone'
+ autoload :Select2, 'qa/page/component/select2'
end
end
@@ -195,6 +240,7 @@ module QA
#
module Service
autoload :Shellout, 'qa/service/shellout'
+ autoload :KubernetesCluster, 'qa/service/kubernetes_cluster'
autoload :Omnibus, 'qa/service/omnibus'
autoload :Runner, 'qa/service/runner'
end
diff --git a/qa/qa/factory/repository/project_push.rb b/qa/qa/factory/repository/project_push.rb
new file mode 100644
index 00000000000..48674c08a8d
--- /dev/null
+++ b/qa/qa/factory/repository/project_push.rb
@@ -0,0 +1,34 @@
+module QA
+ module Factory
+ module Repository
+ class ProjectPush < Factory::Repository::Push
+ dependency Factory::Resource::Project, as: :project do |project|
+ project.name = 'project-with-code'
+ project.description = 'Project with repository'
+ end
+
+ product :output do |factory|
+ factory.output
+ end
+
+ def initialize
+ @file_name = 'file.txt'
+ @file_content = '# This is test project'
+ @commit_message = "This is a test commit"
+ @branch_name = 'master'
+ @new_branch = true
+ end
+
+ def repository_uri
+ @repository_uri ||= begin
+ project.visit!
+ Page::Project::Show.act do
+ choose_repository_clone_http
+ repository_location.uri
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/factory/repository/push.rb b/qa/qa/factory/repository/push.rb
index 795f1f9cb1a..4f97e65b091 100644
--- a/qa/qa/factory/repository/push.rb
+++ b/qa/qa/factory/repository/push.rb
@@ -1,38 +1,36 @@
+require 'pathname'
+
module QA
module Factory
module Repository
class Push < Factory::Base
attr_accessor :file_name, :file_content, :commit_message,
- :branch_name, :new_branch
+ :branch_name, :new_branch, :output, :repository_uri
attr_writer :remote_branch
- dependency Factory::Resource::Project, as: :project do |project|
- project.name = 'project-with-code'
- project.description = 'Project with repository'
- end
-
def initialize
@file_name = 'file.txt'
- @file_content = '# This is test project'
- @commit_message = "Add #{@file_name}"
+ @file_content = '# This is test file'
+ @commit_message = "This is a test commit"
@branch_name = 'master'
@new_branch = true
+ @repository_uri = ""
end
def remote_branch
@remote_branch ||= branch_name
end
- def fabricate!
- project.visit!
+ def directory=(dir)
+ raise "Must set directory as a Pathname" unless dir.is_a?(Pathname)
- Git::Repository.perform do |repository|
- repository.uri = Page::Project::Show.act do
- choose_repository_clone_http
- repository_location.uri
- end
+ @directory = dir
+ end
+ def fabricate!
+ Git::Repository.perform do |repository|
+ repository.uri = repository_uri
repository.use_default_credentials
repository.clone
repository.configure_identity('GitLab QA', 'root@gitlab.com')
@@ -43,9 +41,16 @@ module QA
repository.checkout(branch_name)
end
- repository.add_file(file_name, file_content)
+ if @directory
+ @directory.each_child do |f|
+ repository.add_file(f.basename, f.read) if f.file?
+ end
+ else
+ repository.add_file(file_name, file_content)
+ end
+
repository.commit(commit_message)
- repository.push_changes("#{branch_name}:#{remote_branch}")
+ @output = repository.push_changes("#{branch_name}:#{remote_branch}")
end
end
end
diff --git a/qa/qa/factory/repository/wiki_push.rb b/qa/qa/factory/repository/wiki_push.rb
new file mode 100644
index 00000000000..fb7c2bb660d
--- /dev/null
+++ b/qa/qa/factory/repository/wiki_push.rb
@@ -0,0 +1,32 @@
+module QA
+ module Factory
+ module Repository
+ class WikiPush < Factory::Repository::Push
+ dependency Factory::Resource::Wiki, as: :wiki do |wiki|
+ wiki.title = 'Home'
+ wiki.content = '# My First Wiki Content'
+ wiki.message = 'Update home'
+ end
+
+ def initialize
+ @file_name = 'Home.md'
+ @file_content = '# Welcome to My Wiki'
+ @commit_message = 'Updating Home Page'
+ @branch_name = 'master'
+ @new_branch = false
+ end
+
+ def repository_uri
+ @repository_uri ||= begin
+ wiki.visit!
+ Page::Project::Wiki::Show.act do
+ go_to_clone_repository
+ choose_repository_clone_http
+ repository_location.uri
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/factory/resource/branch.rb b/qa/qa/factory/resource/branch.rb
index 1785441f5a8..7fb0633ec90 100644
--- a/qa/qa/factory/resource/branch.rb
+++ b/qa/qa/factory/resource/branch.rb
@@ -31,13 +31,13 @@ module QA
def fabricate!
project.visit!
- Factory::Repository::Push.fabricate! do |resource|
+ Factory::Repository::ProjectPush.fabricate! do |resource|
resource.project = project
resource.file_name = 'kick-off.txt'
resource.commit_message = 'First commit'
end
- branch = Factory::Repository::Push.fabricate! do |resource|
+ branch = Factory::Repository::ProjectPush.fabricate! do |resource|
resource.project = project
resource.file_name = 'README.md'
resource.commit_message = 'Add readme'
@@ -46,7 +46,9 @@ module QA
resource.remote_branch = @branch_name
end
- Page::Project::Show.act { wait_for_push }
+ Page::Project::Show.perform do |page|
+ page.wait { page.has_content?(branch_name) }
+ end
# The upcoming process will make it access the Protected Branches page,
# select the already created branch and protect it according
@@ -62,13 +64,13 @@ module QA
page.select_branch(branch_name)
if allow_to_push
- page.allow_devs_and_masters_to_push
+ page.allow_devs_and_maintainers_to_push
else
page.allow_no_one_to_push
end
if allow_to_merge
- page.allow_devs_and_masters_to_merge
+ page.allow_devs_and_maintainers_to_merge
else
page.allow_no_one_to_merge
end
@@ -79,9 +81,13 @@ module QA
page.protect_branch
- # Wait for page load, which resets the expanded sections
- page.wait(reload: false) do
- !page.has_content?('Collapse')
+ # Avoid Selenium::WebDriver::Error::StaleElementReferenceError
+ # without sleeping. I.e. this completes fast on fast machines.
+ page.refresh
+
+ # It is possible for the protected branch row to "disappear" at first
+ page.wait do
+ page.has_content?(branch_name)
end
end
end
diff --git a/qa/qa/factory/resource/group.rb b/qa/qa/factory/resource/group.rb
index 9f13e26f35c..531fccd2ad8 100644
--- a/qa/qa/factory/resource/group.rb
+++ b/qa/qa/factory/resource/group.rb
@@ -23,7 +23,7 @@ module QA
Page::Group::New.perform do |group|
group.set_path(@path)
group.set_description(@description)
- group.set_visibility('Private')
+ group.set_visibility('Public')
group.create
end
end
diff --git a/qa/qa/factory/resource/kubernetes_cluster.rb b/qa/qa/factory/resource/kubernetes_cluster.rb
new file mode 100644
index 00000000000..f32cf985e9d
--- /dev/null
+++ b/qa/qa/factory/resource/kubernetes_cluster.rb
@@ -0,0 +1,55 @@
+require 'securerandom'
+
+module QA
+ module Factory
+ module Resource
+ class KubernetesCluster < Factory::Base
+ attr_writer :project, :cluster,
+ :install_helm_tiller, :install_ingress, :install_prometheus, :install_runner
+
+ product :ingress_ip do
+ Page::Project::Operations::Kubernetes::Show.perform do |page|
+ page.ingress_ip
+ end
+ end
+
+ def fabricate!
+ @project.visit!
+
+ Page::Menu::Side.act { click_operations_kubernetes }
+
+ Page::Project::Operations::Kubernetes::Index.perform do |page|
+ page.add_kubernetes_cluster
+ end
+
+ Page::Project::Operations::Kubernetes::Add.perform do |page|
+ page.add_existing_cluster
+ end
+
+ Page::Project::Operations::Kubernetes::AddExisting.perform do |page|
+ page.set_cluster_name(@cluster.cluster_name)
+ page.set_api_url(@cluster.api_url)
+ page.set_ca_certificate(@cluster.ca_certificate)
+ page.set_token(@cluster.token)
+ page.add_cluster!
+ end
+
+ if @install_helm_tiller
+ Page::Project::Operations::Kubernetes::Show.perform do |page|
+ # Helm must be installed before everything else
+ page.install!(:helm)
+ page.await_installed(:helm)
+
+ page.install!(:ingress) if @install_ingress
+ page.await_installed(:ingress) if @install_ingress
+ page.install!(:prometheus) if @install_prometheus
+ page.await_installed(:prometheus) if @install_prometheus
+ page.install!(:runner) if @install_runner
+ page.await_installed(:runner) if @install_runner
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/factory/resource/merge_request.rb b/qa/qa/factory/resource/merge_request.rb
index 7588ac5735d..ddb62bd0a68 100644
--- a/qa/qa/factory/resource/merge_request.rb
+++ b/qa/qa/factory/resource/merge_request.rb
@@ -7,7 +7,10 @@ module QA
attr_accessor :title,
:description,
:source_branch,
- :target_branch
+ :target_branch,
+ :assignee,
+ :milestone,
+ :labels
product :project do |factory|
factory.project
@@ -21,14 +24,14 @@ module QA
project.name = 'project-with-merge-request'
end
- dependency Factory::Repository::Push, as: :target do |push, factory|
+ dependency Factory::Repository::ProjectPush, as: :target do |push, factory|
factory.project.visit!
push.project = factory.project
push.branch_name = 'master'
push.remote_branch = factory.target_branch
end
- dependency Factory::Repository::Push, as: :source do |push, factory|
+ dependency Factory::Repository::ProjectPush, as: :source do |push, factory|
push.project = factory.project
push.branch_name = factory.target_branch
push.remote_branch = factory.source_branch
@@ -41,16 +44,18 @@ module QA
@description = 'This is a test merge request'
@source_branch = "qa-test-feature-#{SecureRandom.hex(8)}"
@target_branch = "master"
+ @assignee = nil
+ @milestone = nil
+ @labels = []
end
def fabricate!
project.visit!
-
Page::Project::Show.act { new_merge_request }
-
Page::MergeRequest::New.perform do |page|
page.fill_title(@title)
page.fill_description(@description)
+ page.choose_milestone(@milestone) if @milestone
page.create_merge_request
end
end
diff --git a/qa/qa/factory/resource/project.rb b/qa/qa/factory/resource/project.rb
index cda1b35ba6a..7bc64c6ae5d 100644
--- a/qa/qa/factory/resource/project.rb
+++ b/qa/qa/factory/resource/project.rb
@@ -5,16 +5,12 @@ module QA
module Resource
class Project < Factory::Base
attr_writer :description
+ attr_reader :name
dependency Factory::Resource::Group, as: :group
- def name=(name)
- @name = "#{name}-#{SecureRandom.hex(8)}"
- @description = 'My awesome project'
- end
-
- product :name do
- Page::Project::Show.act { project_name }
+ product :name do |factory|
+ factory.name
end
product :repository_ssh_location do
@@ -24,6 +20,14 @@ module QA
end
end
+ def initialize
+ @description = 'My awesome project'
+ end
+
+ def name=(raw_name)
+ @name = "#{raw_name}-#{SecureRandom.hex(8)}"
+ end
+
def fabricate!
group.visit!
diff --git a/qa/qa/factory/resource/project_imported_from_github.rb b/qa/qa/factory/resource/project_imported_from_github.rb
new file mode 100644
index 00000000000..df2a3340d60
--- /dev/null
+++ b/qa/qa/factory/resource/project_imported_from_github.rb
@@ -0,0 +1,37 @@
+require 'securerandom'
+
+module QA
+ module Factory
+ module Resource
+ class ProjectImportedFromGithub < Resource::Project
+ attr_writer :personal_access_token, :github_repository_path
+
+ dependency Factory::Resource::Group, as: :group
+
+ product :name do |factory|
+ factory.name
+ end
+
+ def fabricate!
+ group.visit!
+
+ Page::Group::Show.act { go_to_new_project }
+
+ Page::Project::New.perform do |page|
+ page.go_to_import_project
+ end
+
+ Page::Project::New.perform do |page|
+ page.go_to_github_import
+ end
+
+ Page::Project::Import::Github.perform do |page|
+ page.add_personal_access_token(@personal_access_token)
+ page.list_repos
+ page.import!(@github_repository_path, @name)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/factory/resource/project_milestone.rb b/qa/qa/factory/resource/project_milestone.rb
new file mode 100644
index 00000000000..47a5e74204f
--- /dev/null
+++ b/qa/qa/factory/resource/project_milestone.rb
@@ -0,0 +1,36 @@
+module QA
+ module Factory
+ module Resource
+ class ProjectMilestone < Factory::Base
+ attr_accessor :description
+ attr_reader :title
+
+ dependency Factory::Resource::Project, as: :project
+
+ product(:title) { |factory| factory.title }
+
+ def title=(title)
+ @title = "#{title}-#{SecureRandom.hex(4)}"
+ @description = 'A milestone'
+ end
+
+ def fabricate!
+ project.visit!
+
+ Page::Menu::Side.act do
+ click_issues
+ click_milestones
+ end
+
+ Page::Project::Milestone::Index.act { click_new_milestone }
+
+ Page::Project::Milestone::New.perform do |milestone_new|
+ milestone_new.set_title(@title)
+ milestone_new.set_description(@description)
+ milestone_new.create_new_milestone
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/factory/resource/sandbox.rb b/qa/qa/factory/resource/sandbox.rb
index ad376988e82..4f6039f300f 100644
--- a/qa/qa/factory/resource/sandbox.rb
+++ b/qa/qa/factory/resource/sandbox.rb
@@ -21,8 +21,8 @@ module QA
Page::Group::New.perform do |group|
group.set_path(@name)
- group.set_description('GitLab QA Sandbox')
- group.set_visibility('Private')
+ group.set_description('GitLab QA Sandbox Group')
+ group.set_visibility('Public')
group.create
end
end
diff --git a/qa/qa/factory/resource/wiki.rb b/qa/qa/factory/resource/wiki.rb
new file mode 100644
index 00000000000..cc200a512d5
--- /dev/null
+++ b/qa/qa/factory/resource/wiki.rb
@@ -0,0 +1,25 @@
+module QA
+ module Factory
+ module Resource
+ class Wiki < Factory::Base
+ attr_accessor :title, :content, :message
+
+ dependency Factory::Resource::Project, as: :project do |project|
+ project.name = 'project-for-wikis'
+ project.description = 'project for adding wikis'
+ end
+
+ def fabricate!
+ Page::Menu::Side.act { click_wiki }
+ Page::Project::Wiki::New.perform do |page|
+ page.go_to_create_first_page
+ page.set_title(@title)
+ page.set_content(@content)
+ page.set_message(@message)
+ page.create_new_page
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/fixtures/auto_devops_rack/Gemfile b/qa/qa/fixtures/auto_devops_rack/Gemfile
new file mode 100644
index 00000000000..fc7514242d0
--- /dev/null
+++ b/qa/qa/fixtures/auto_devops_rack/Gemfile
@@ -0,0 +1,3 @@
+source 'https://rubygems.org'
+gem 'rack'
+gem 'rake'
diff --git a/qa/qa/fixtures/auto_devops_rack/Gemfile.lock b/qa/qa/fixtures/auto_devops_rack/Gemfile.lock
new file mode 100644
index 00000000000..09cf72c48ac
--- /dev/null
+++ b/qa/qa/fixtures/auto_devops_rack/Gemfile.lock
@@ -0,0 +1,15 @@
+GEM
+ remote: https://rubygems.org/
+ specs:
+ rack (2.0.4)
+ rake (12.3.0)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ rack
+ rake
+
+BUNDLED WITH
+ 1.16.1
diff --git a/qa/qa/fixtures/auto_devops_rack/Rakefile b/qa/qa/fixtures/auto_devops_rack/Rakefile
new file mode 100644
index 00000000000..c865c9aaac1
--- /dev/null
+++ b/qa/qa/fixtures/auto_devops_rack/Rakefile
@@ -0,0 +1,7 @@
+require 'rake/testtask'
+
+task default: %w[test]
+
+task :test do
+ puts "ok"
+end
diff --git a/qa/qa/fixtures/auto_devops_rack/config.ru b/qa/qa/fixtures/auto_devops_rack/config.ru
new file mode 100644
index 00000000000..bde8e15488a
--- /dev/null
+++ b/qa/qa/fixtures/auto_devops_rack/config.ru
@@ -0,0 +1 @@
+run lambda { |env| [200, { 'Content-Type' => 'text/plain' }, StringIO.new("Hello World!\n")] }
diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb
index 1367671e3ca..fc753554fc4 100644
--- a/qa/qa/git/repository.rb
+++ b/qa/qa/git/repository.rb
@@ -7,8 +7,6 @@ module QA
class Repository
include Scenario::Actable
- attr_reader :push_error
-
def self.perform(*args)
Dir.mktmpdir do |dir|
Dir.chdir(dir) { super }
@@ -35,7 +33,7 @@ module QA
end
def clone(opts = '')
- `git clone #{opts} #{@uri.to_s} ./ #{suppress_output}`
+ run_and_redact_credentials("git clone #{opts} #{@uri} ./")
end
def checkout(branch_name)
@@ -71,8 +69,9 @@ module QA
end
def push_changes(branch = 'master')
- # capture3 returns stdout, stderr and status.
- _, @push_error, _ = Open3.capture3("git push #{@uri} #{branch} #{suppress_output}")
+ output, _ = run_and_redact_credentials("git push #{@uri} #{branch}")
+
+ output
end
def commits
@@ -81,12 +80,10 @@ module QA
private
- def suppress_output
- # If we're running as the default user, it's probably a temporary
- # instance and output can be useful for debugging
- return if @username == Runtime::User.default_name
-
- "&> #{File::NULL}"
+ # Since the remote URL contains the credentials, and git occasionally
+ # outputs the URL. Note that stderr is redirected to stdout.
+ def run_and_redact_credentials(command)
+ Open3.capture2("#{command} 2>&1 | sed -E 's#://[^@]+@#://****@#g'")
end
end
end
diff --git a/qa/qa/page/admin/settings/main.rb b/qa/qa/page/admin/settings/main.rb
index e7c1220c967..db3387b4557 100644
--- a/qa/qa/page/admin/settings/main.rb
+++ b/qa/qa/page/admin/settings/main.rb
@@ -6,11 +6,11 @@ module QA
include QA::Page::Settings::Common
view 'app/views/admin/application_settings/show.html.haml' do
- element :advanced_settings_section, 'Repository storage'
+ element :repository_storage_settings
end
def expand_repository_storage(&block)
- expand_section('Repository storage') do
+ expand_section(:repository_storage_settings) do
RepositoryStorage.perform(&block)
end
end
diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb
index 0a69af88570..30e35bf7abb 100644
--- a/qa/qa/page/base.rb
+++ b/qa/qa/page/base.rb
@@ -13,7 +13,7 @@ module QA
visit current_url
end
- def wait(max: 60, time: 1, reload: true)
+ def wait(max: 60, time: 0.1, reload: true)
start = Time.now
while Time.now - start < max
diff --git a/qa/qa/page/component/select2.rb b/qa/qa/page/component/select2.rb
new file mode 100644
index 00000000000..30829eb0221
--- /dev/null
+++ b/qa/qa/page/component/select2.rb
@@ -0,0 +1,11 @@
+module QA
+ module Page
+ module Component
+ module Select2
+ def select_item(item_text)
+ find('ul.select2-result-sub > li', text: item_text).click
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/group/show.rb b/qa/qa/page/group/show.rb
index 89125bd2e59..3e0eaa392f5 100644
--- a/qa/qa/page/group/show.rb
+++ b/qa/qa/page/group/show.rb
@@ -28,11 +28,9 @@ module QA
def has_subgroup?(name)
filter_by_name(name)
- wait(reload: false) do
- break false if page.has_content?('Sorry, no groups or projects matched your search')
+ page.has_text?(/#{name}|Sorry, no groups or projects matched your search/, wait: 60)
- page.has_link?(name)
- end
+ page.has_text?(name, wait: 0)
end
def go_to_new_subgroup
diff --git a/qa/qa/page/issuable/sidebar.rb b/qa/qa/page/issuable/sidebar.rb
new file mode 100644
index 00000000000..dec2ce1eab3
--- /dev/null
+++ b/qa/qa/page/issuable/sidebar.rb
@@ -0,0 +1,17 @@
+module QA
+ module Page
+ module Issuable
+ class Sidebar < Page::Base
+ view 'app/views/shared/issuable/_sidebar.html.haml' do
+ element :labels_block, ".issuable-show-labels"
+ end
+
+ def has_label?(label)
+ page.within('.issuable-show-labels') do
+ !!find('span', text: label)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb
index 596205fe540..26c99efc53d 100644
--- a/qa/qa/page/main/login.rb
+++ b/qa/qa/page/main/login.rb
@@ -26,53 +26,58 @@ module QA
end
def initialize
+ # The login page is usually the entry point for all the scenarios so
+ # we need to wait for the instance to start. That said, in some cases
+ # we are already logged-in so we check both cases here.
wait(max: 500) do
- page.has_css?('.application')
+ page.has_css?('.login-page') ||
+ Page::Menu::Main.act { has_personal_area? }
end
end
- def set_initial_password_if_present
- 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'
+ def sign_in_using_credentials
+ # Don't try to log-in if we're already logged-in
+ return if Page::Menu::Main.act { has_personal_area? }
+
+ using_wait_time 0 do
+ set_initial_password_if_present
+
+ if Runtime::User.ldap_user?
+ sign_in_using_ldap_credentials
+ else
+ sign_in_using_gitlab_credentials
+ end
end
end
- def sign_in_using_credentials
- if Runtime::User.ldap_user?
- sign_in_using_ldap_credentials
- else
- sign_in_using_gitlab_credentials
- end
+ def self.path
+ '/users/sign_in'
end
- def sign_in_using_ldap_credentials
- using_wait_time 0 do
- set_initial_password_if_present
+ private
- click_link 'LDAP'
+ def sign_in_using_ldap_credentials
+ click_link 'LDAP'
- fill_in :username, with: Runtime::User.ldap_username
- fill_in :password, with: Runtime::User.ldap_password
- click_button 'Sign in'
- end
+ fill_in :username, with: Runtime::User.ldap_username
+ fill_in :password, with: Runtime::User.ldap_password
+ click_button 'Sign in'
end
def sign_in_using_gitlab_credentials
- using_wait_time 0 do
- set_initial_password_if_present
-
- click_link 'Standard' if page.has_content?('LDAP')
+ click_link 'Standard' if page.has_content?('LDAP')
- fill_in :user_login, with: Runtime::User.name
- fill_in :user_password, with: Runtime::User.password
- click_button 'Sign in'
- end
+ fill_in :user_login, with: Runtime::User.name
+ fill_in :user_password, with: Runtime::User.password
+ click_button 'Sign in'
end
- def self.path
- '/users/sign_in'
+ def set_initial_password_if_present
+ return unless 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
end
end
diff --git a/qa/qa/page/menu/main.rb b/qa/qa/page/menu/main.rb
index d3562effaab..aef5c9f9c82 100644
--- a/qa/qa/page/menu/main.rb
+++ b/qa/qa/page/menu/main.rb
@@ -10,13 +10,13 @@ module QA
view 'app/views/layouts/header/_default.html.haml' do
element :navbar
element :user_avatar
- element :user_menu, '.dropdown-menu-nav'
+ element :user_menu, '.dropdown-menu'
end
view 'app/views/layouts/nav/_dashboard.html.haml' do
element :admin_area_link
element :projects_dropdown
- element :groups_link
+ element :groups_dropdown
end
view 'app/views/layouts/nav/projects_dropdown/_show.html.haml' do
@@ -25,7 +25,13 @@ module QA
end
def go_to_groups
- within_top_menu { click_element :groups_link }
+ within_top_menu do
+ click_element :groups_dropdown
+ end
+
+ page.within('.qa-groups-dropdown-sidebar') do
+ click_element :your_groups_link
+ end
end
def go_to_projects
@@ -55,7 +61,8 @@ module QA
end
def has_personal_area?
- page.has_selector?('.qa-user-avatar')
+ # No need to wait, either we're logged-in, or not.
+ using_wait_time(0) { page.has_selector?('.qa-user-avatar') }
end
private
@@ -70,7 +77,7 @@ module QA
within_top_menu do
click_element :user_avatar
- page.within('.dropdown-menu-nav') do
+ page.within('.dropdown-menu') do
yield
end
end
diff --git a/qa/qa/page/menu/side.rb b/qa/qa/page/menu/side.rb
index 7e028add2ef..c14a835c2c9 100644
--- a/qa/qa/page/menu/side.rb
+++ b/qa/qa/page/menu/side.rb
@@ -7,10 +7,16 @@ module QA
element :settings_link, 'link_to edit_project_path'
element :repository_link, "title: 'Repository'"
element :pipelines_settings_link, "title: 'CI / CD'"
+ element :operations_kubernetes_link, "title: _('Kubernetes')"
element :issues_link, /link_to.*shortcuts-issues/
element :issues_link_text, "Issues"
+ element :merge_requests_link, /link_to.*shortcuts-merge_requests/
+ element :merge_requests_link_text, "Merge Requests"
element :top_level_items, '.sidebar-top-level-items'
+ element :operations_section, "class: 'shortcuts-operations'"
element :activity_link, "title: 'Activity'"
+ element :wiki_link_text, "Wiki"
+ element :milestones_link
end
view 'app/assets/javascripts/fly_out_nav.js' do
@@ -33,6 +39,14 @@ module QA
end
end
+ def click_operations_kubernetes
+ hover_operations do
+ within_submenu do
+ click_link('Kubernetes')
+ end
+ end
+ end
+
def click_ci_cd_pipelines
within_sidebar do
click_link('CI / CD')
@@ -51,6 +65,24 @@ module QA
end
end
+ def click_merge_requests
+ within_sidebar do
+ click_link('Merge Requests')
+ end
+ end
+
+ def click_milestones
+ within_sidebar do
+ click_element :milestones_link
+ end
+ end
+
+ def click_wiki
+ within_sidebar do
+ click_link('Wiki')
+ end
+ end
+
private
def hover_settings
@@ -61,6 +93,14 @@ module QA
end
end
+ def hover_operations
+ within_sidebar do
+ find('.shortcuts-operations').hover
+
+ yield
+ end
+ end
+
def within_sidebar
page.within('.sidebar-top-level-items') do
yield
diff --git a/qa/qa/page/merge_request/new.rb b/qa/qa/page/merge_request/new.rb
index ec94ff4ac98..83cc4bbbace 100644
--- a/qa/qa/page/merge_request/new.rb
+++ b/qa/qa/page/merge_request/new.rb
@@ -10,10 +10,18 @@ module QA
element :issuable_form_title
end
+ view 'app/views/shared/issuable/form/_metadata.html.haml' do
+ element :issuable_milestone_dropdown
+ end
+
view 'app/views/shared/form_elements/_description.html.haml' do
element :issuable_form_description
end
+ view 'app/views/shared/issuable/_milestone_dropdown.html.haml' do
+ element :issuable_dropdown_menu_milestone
+ end
+
def create_merge_request
click_element :issuable_create_button
end
@@ -25,6 +33,13 @@ module QA
def fill_description(description)
fill_element :issuable_form_description, description
end
+
+ def choose_milestone(milestone)
+ click_element :issuable_milestone_dropdown
+ within_element(:issuable_dropdown_menu_milestone) do
+ click_on milestone.title
+ end
+ end
end
end
end
diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb
index 166861e6c4a..f3200160a78 100644
--- a/qa/qa/page/merge_request/show.rb
+++ b/qa/qa/page/merge_request/show.rb
@@ -16,12 +16,8 @@ module QA
element :no_fast_forward_message, 'Fast-forward merge is not possible'
end
- def rebase!
- click_element :mr_rebase_button
-
- wait(reload: false) do
- has_text?('Fast-forward merge without a merge commit')
- end
+ view 'app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue' do
+ element :squash_checkbox
end
def fast_forward_possible?
@@ -34,13 +30,61 @@ module QA
has_selector?('.accept-merge-request')
end
+ def rebase!
+ # The rebase button is disabled on load
+ wait do
+ has_css?(element_selector_css(:mr_rebase_button))
+ end
+
+ # The rebase button is enabled via JS
+ wait(reload: false) do
+ !first(element_selector_css(:mr_rebase_button)).disabled?
+ end
+
+ click_element :mr_rebase_button
+
+ wait(reload: false) do
+ has_text?('Fast-forward merge without a merge commit')
+ end
+ end
+
def merge!
+ # The merge button is disabled on load
+ wait do
+ has_css?(element_selector_css(:merge_button))
+ end
+
+ # The merge button is enabled via JS
+ wait(reload: false) do
+ !first(element_selector_css(:merge_button)).disabled?
+ end
+
click_element :merge_button
wait(reload: false) do
has_text?('The changes were merged into')
end
end
+
+ def mark_to_squash
+ # The squash checkbox is disabled on load
+ wait do
+ has_css?(element_selector_css(:squash_checkbox))
+ end
+
+ # The squash checkbox is enabled via JS
+ wait(reload: false) do
+ !first(element_selector_css(:squash_checkbox)).disabled?
+ end
+
+ click_element :squash_checkbox
+ end
+
+ def has_milestone?(milestone_title)
+ page.within('.issuable-sidebar') do
+ !!find("[href*='/milestones/']", text: milestone_title, wait: 1)
+ end
+ end
end
end
end
diff --git a/qa/qa/page/project/import/github.rb b/qa/qa/page/project/import/github.rb
new file mode 100644
index 00000000000..36567927194
--- /dev/null
+++ b/qa/qa/page/project/import/github.rb
@@ -0,0 +1,66 @@
+module QA
+ module Page
+ module Project
+ module Import
+ class Github < Page::Base
+ include Page::Component::Select2
+
+ view 'app/views/import/github/new.html.haml' do
+ element :personal_access_token_field, 'text_field_tag :personal_access_token'
+ element :list_repos_button, "submit_tag _('List your GitHub repositories')"
+ end
+
+ view 'app/views/import/_githubish_status.html.haml' do
+ element :project_import_row, 'data: { qa: { repo_path: repo.full_name } }'
+ element :project_namespace_select
+ element :project_namespace_field, 'select_tag :namespace_id'
+ element :project_path_field, 'text_field_tag :path, repo.name'
+ element :import_button, "_('Import')"
+ end
+
+ def add_personal_access_token(personal_access_token)
+ fill_in 'personal_access_token', with: personal_access_token
+ end
+
+ def list_repos
+ click_button 'List your GitHub repositories'
+ end
+
+ def import!(full_path, name)
+ choose_test_namespace(full_path)
+ set_path(full_path, name)
+ import_project(full_path)
+ end
+
+ private
+
+ def within_repo_path(full_path)
+ page.within(%Q(tr[data-qa-repo-path="#{full_path}"])) do
+ yield
+ end
+ end
+
+ def choose_test_namespace(full_path)
+ within_repo_path(full_path) do
+ click_element :project_namespace_select
+ end
+
+ select_item(Runtime::Namespace.path)
+ end
+
+ def set_path(full_path, name)
+ within_repo_path(full_path) do
+ fill_in 'path', with: name
+ end
+ end
+
+ def import_project(full_path)
+ within_repo_path(full_path) do
+ click_button 'Import'
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/job/show.rb b/qa/qa/page/project/job/show.rb
index 83bb224b5c3..228ffd9d381 100644
--- a/qa/qa/page/project/job/show.rb
+++ b/qa/qa/page/project/job/show.rb
@@ -4,8 +4,9 @@ module QA::Page
COMPLETED_STATUSES = %w[passed failed canceled blocked skipped manual].freeze # excludes created, pending, running
PASSED_STATUS = 'passed'.freeze
- view 'app/views/projects/jobs/show.html.haml' do
+ view 'app/views/shared/builds/_build_output.html.haml' do
element :build_output, '.js-build-output'
+ element :loading_animation, '.js-build-refresh'
end
view 'app/assets/javascripts/vue_shared/components/ci_badge_link.vue' do
@@ -20,6 +21,10 @@ module QA::Page
find('.ci-status').text == PASSED_STATUS
end
+ def trace_loading?
+ has_css?('.js-build-refresh')
+ end
+
# Reminder: You may wish to wait for a particular job status before checking output
def output
find('.js-build-output').text
diff --git a/qa/qa/page/project/milestone/index.rb b/qa/qa/page/project/milestone/index.rb
new file mode 100644
index 00000000000..a1519c9ef1c
--- /dev/null
+++ b/qa/qa/page/project/milestone/index.rb
@@ -0,0 +1,17 @@
+module QA
+ module Page
+ module Project
+ module Milestone
+ class Index < Page::Base
+ view 'app/views/projects/milestones/index.html.haml' do
+ element :new_project_milestone
+ end
+
+ def click_new_milestone
+ click_element :new_project_milestone
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/milestone/new.rb b/qa/qa/page/project/milestone/new.rb
new file mode 100644
index 00000000000..992ef89004b
--- /dev/null
+++ b/qa/qa/page/project/milestone/new.rb
@@ -0,0 +1,27 @@
+module QA
+ module Page
+ module Project
+ module Milestone
+ class New < Page::Base
+ view 'app/views/projects/milestones/_form.html.haml' do
+ element :milestone_create_button
+ element :milestone_title
+ element :milestone_description
+ end
+
+ def set_title(title)
+ fill_element :milestone_title, title
+ end
+
+ def set_description(description)
+ fill_element :milestone_description, description
+ end
+
+ def create_new_milestone
+ click_element :milestone_create_button
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/new.rb b/qa/qa/page/project/new.rb
index 186a4724326..7976e96d43b 100644
--- a/qa/qa/page/project/new.rb
+++ b/qa/qa/page/project/new.rb
@@ -2,6 +2,12 @@ module QA
module Page
module Project
class New < Page::Base
+ include Page::Component::Select2
+
+ view 'app/views/projects/new.html.haml' do
+ element :import_project_tab, "Import project"
+ end
+
view 'app/views/projects/_new_project_fields.html.haml' do
element :project_namespace_select
element :project_namespace_field, /select :namespace_id.*class: 'select2/
@@ -10,10 +16,18 @@ module QA
element :project_create_button, "submit 'Create project'"
end
+ view 'app/views/projects/_import_project_pane.html.haml' do
+ element :import_github, "icon('github', text: 'GitHub')"
+ end
+
def choose_test_namespace
click_element :project_namespace_select
- find('ul.select2-result-sub > li', text: Runtime::Namespace.path).click
+ select_item(Runtime::Namespace.path)
+ end
+
+ def go_to_import_project
+ click_on 'Import project'
end
def choose_name(name)
@@ -27,6 +41,10 @@ module QA
def create_new_project
click_on 'Create project'
end
+
+ def go_to_github_import
+ click_link 'GitHub'
+ end
end
end
end
diff --git a/qa/qa/page/project/operations/kubernetes/add.rb b/qa/qa/page/project/operations/kubernetes/add.rb
new file mode 100644
index 00000000000..11ebe10fb18
--- /dev/null
+++ b/qa/qa/page/project/operations/kubernetes/add.rb
@@ -0,0 +1,19 @@
+module QA
+ module Page
+ module Project
+ module Operations
+ module Kubernetes
+ class Add < Page::Base
+ view 'app/views/projects/clusters/new.html.haml' do
+ element :add_existing_cluster_button, "Add existing cluster"
+ end
+
+ def add_existing_cluster
+ click_on 'Add existing cluster'
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/operations/kubernetes/add_existing.rb b/qa/qa/page/project/operations/kubernetes/add_existing.rb
new file mode 100644
index 00000000000..eef82b5f329
--- /dev/null
+++ b/qa/qa/page/project/operations/kubernetes/add_existing.rb
@@ -0,0 +1,39 @@
+module QA
+ module Page
+ module Project
+ module Operations
+ module Kubernetes
+ class AddExisting < Page::Base
+ view 'app/views/projects/clusters/user/_form.html.haml' do
+ element :cluster_name, 'text_field :name'
+ element :api_url, 'text_field :api_url'
+ element :ca_certificate, 'text_area :ca_cert'
+ element :token, 'text_field :token'
+ element :add_cluster_button, "submit s_('ClusterIntegration|Add Kubernetes cluster')"
+ end
+
+ def set_cluster_name(name)
+ fill_in 'cluster_name', with: name
+ end
+
+ def set_api_url(api_url)
+ fill_in 'cluster_platform_kubernetes_attributes_api_url', with: api_url
+ end
+
+ def set_ca_certificate(ca_certificate)
+ fill_in 'cluster_platform_kubernetes_attributes_ca_cert', with: ca_certificate
+ end
+
+ def set_token(token)
+ fill_in 'cluster_platform_kubernetes_attributes_token', with: token
+ end
+
+ def add_cluster!
+ click_on 'Add Kubernetes cluster'
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/operations/kubernetes/index.rb b/qa/qa/page/project/operations/kubernetes/index.rb
new file mode 100644
index 00000000000..7261b5645da
--- /dev/null
+++ b/qa/qa/page/project/operations/kubernetes/index.rb
@@ -0,0 +1,19 @@
+module QA
+ module Page
+ module Project
+ module Operations
+ module Kubernetes
+ class Index < Page::Base
+ view 'app/views/projects/clusters/_empty_state.html.haml' do
+ element :add_kubernetes_cluster_button, "link_to s_('ClusterIntegration|Add Kubernetes cluster')"
+ end
+
+ def add_kubernetes_cluster
+ click_on 'Add Kubernetes cluster'
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/operations/kubernetes/show.rb b/qa/qa/page/project/operations/kubernetes/show.rb
new file mode 100644
index 00000000000..4923304133e
--- /dev/null
+++ b/qa/qa/page/project/operations/kubernetes/show.rb
@@ -0,0 +1,39 @@
+module QA
+ module Page
+ module Project
+ module Operations
+ module Kubernetes
+ class Show < Page::Base
+ view 'app/assets/javascripts/clusters/components/application_row.vue' do
+ element :application_row, 'js-cluster-application-row-${this.id}'
+ element :install_button, "s__('ClusterIntegration|Install')"
+ element :installed_button, "s__('ClusterIntegration|Installed')"
+ end
+
+ view 'app/assets/javascripts/clusters/components/applications.vue' do
+ element :ingress_ip_address, 'id="ingress-ip-address"'
+ end
+
+ def install!(application_name)
+ within(".js-cluster-application-row-#{application_name}") do
+ click_on 'Install'
+ end
+ end
+
+ def await_installed(application_name)
+ within(".js-cluster-application-row-#{application_name}") do
+ page.has_text?('Installed', wait: 300)
+ end
+ end
+
+ def ingress_ip
+ # We need to wait longer since it can take some time before the
+ # ip address is assigned for the ingress controller
+ page.find('#ingress-ip-address', wait: 500).value
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/pipeline/show.rb b/qa/qa/page/project/pipeline/show.rb
index ec61c47b3bb..babc0079f3f 100644
--- a/qa/qa/page/project/pipeline/show.rb
+++ b/qa/qa/page/project/pipeline/show.rb
@@ -24,10 +24,10 @@ module QA::Page
end
end
- def has_build?(name, status: :success)
+ def has_build?(name, status: :success, wait: nil)
within('.pipeline-graph') do
within('.ci-job-component', text: name) do
- has_selector?(".ci-status-icon-#{status}")
+ has_selector?(".ci-status-icon-#{status}", { wait: wait }.compact)
end
end
end
diff --git a/qa/qa/page/project/settings/advanced.rb b/qa/qa/page/project/settings/advanced.rb
index 5ef00504fdf..d7b2b66b587 100644
--- a/qa/qa/page/project/settings/advanced.rb
+++ b/qa/qa/page/project/settings/advanced.rb
@@ -4,9 +4,9 @@ module QA
module Settings
class Advanced < Page::Base
view 'app/views/projects/edit.html.haml' do
- element :project_path_field, 'f.text_field :path'
- element :project_name_field, 'f.text_field :name'
- element :rename_project_button, "f.submit 'Rename project'"
+ element :project_path_field, 'text_field :path'
+ element :project_name_field, 'text_field :name'
+ element :rename_project_button, "submit 'Rename project'"
end
def rename_to(path)
diff --git a/qa/qa/page/project/settings/ci_cd.rb b/qa/qa/page/project/settings/ci_cd.rb
index 17a1bc904e4..0f739f61db9 100644
--- a/qa/qa/page/project/settings/ci_cd.rb
+++ b/qa/qa/page/project/settings/ci_cd.rb
@@ -6,21 +6,38 @@ module QA # rubocop:disable Naming/FileName
include Common
view 'app/views/projects/settings/ci_cd/show.html.haml' do
- element :runners_settings, 'Runners settings'
- element :secret_variables, 'Secret variables'
+ element :autodevops_settings
+ element :runners_settings
+ element :variables_settings
+ end
+
+ view 'app/views/projects/settings/ci_cd/_autodevops_form.html.haml' do
+ element :enable_auto_devops_field, 'radio_button :enabled'
+ element :domain_field, 'text_field :domain'
+ element :enable_auto_devops_button, "%strong= s_('CICD|Enable Auto DevOps')"
+ element :domain_input, "%strong= _('Domain')"
+ element :save_changes_button, "submit _('Save changes')"
end
def expand_runners_settings(&block)
- expand_section('Runners settings') do
+ expand_section(:runners_settings) do
Settings::Runners.perform(&block)
end
end
def expand_secret_variables(&block)
- expand_section('Secret variables') do
+ expand_section(:variables_settings) do
Settings::SecretVariables.perform(&block)
end
end
+
+ def enable_auto_devops_with_domain(domain)
+ expand_section(:autodevops_settings) do
+ choose 'Enable Auto DevOps'
+ fill_in 'Domain', with: domain
+ click_on 'Save changes'
+ end
+ end
end
end
end
diff --git a/qa/qa/page/project/settings/deploy_keys.rb b/qa/qa/page/project/settings/deploy_keys.rb
index 4428e263bbb..90a0e7092bd 100644
--- a/qa/qa/page/project/settings/deploy_keys.rb
+++ b/qa/qa/page/project/settings/deploy_keys.rb
@@ -57,6 +57,10 @@ module QA
private
def within_project_deploy_keys
+ wait(reload: false) do
+ has_css?(element_selector_css(:project_deploy_keys))
+ end
+
within_element(:project_deploy_keys) do
yield
end
diff --git a/qa/qa/page/project/settings/main.rb b/qa/qa/page/project/settings/main.rb
index 5d743f4c9c8..d8cf1d49dd2 100644
--- a/qa/qa/page/project/settings/main.rb
+++ b/qa/qa/page/project/settings/main.rb
@@ -6,11 +6,11 @@ module QA
include Common
view 'app/views/projects/edit.html.haml' do
- element :advanced_settings_section, 'Advanced settings'
+ element :advanced_settings
end
def expand_advanced_settings(&block)
- expand_section('Advanced settings') do
+ expand_section(:advanced_settings) do
Advanced.perform(&block)
end
end
diff --git a/qa/qa/page/project/settings/merge_request.rb b/qa/qa/page/project/settings/merge_request.rb
index b147c91b467..d044d3715a9 100644
--- a/qa/qa/page/project/settings/merge_request.rb
+++ b/qa/qa/page/project/settings/merge_request.rb
@@ -5,17 +5,17 @@ module QA
class MergeRequest < QA::Page::Base
include Common
- view 'app/views/projects/_merge_request_fast_forward_settings.html.haml' do
- element :radio_button_merge_ff
- end
-
view 'app/views/projects/edit.html.haml' do
- element :merge_request_settings, 'Merge request settings'
+ element :merge_request_settings
element :save_merge_request_changes
end
+ view 'app/views/projects/_merge_request_merge_method_settings.html.haml' do
+ element :radio_button_merge_ff
+ end
+
def enable_ff_only
- expand_section('Merge request settings') do
+ expand_section(:merge_request_settings) do
click_element :radio_button_merge_ff
click_element :save_merge_request_changes
end
diff --git a/qa/qa/page/project/settings/protected_branches.rb b/qa/qa/page/project/settings/protected_branches.rb
index 63bc3aaa2bc..e572ae12132 100644
--- a/qa/qa/page/project/settings/protected_branches.rb
+++ b/qa/qa/page/project/settings/protected_branches.rb
@@ -40,18 +40,24 @@ module QA
click_allow(:push, 'No one')
end
- def allow_devs_and_masters_to_push
- click_allow(:push, 'Developers + Masters')
+ def allow_devs_and_maintainers_to_push
+ click_allow(:push, 'Developers + Maintainers')
end
+ # @deprecated
+ alias_method :allow_devs_and_masters_to_push, :allow_devs_and_maintainers_to_push
+
def allow_no_one_to_merge
click_allow(:merge, 'No one')
end
- def allow_devs_and_masters_to_merge
- click_allow(:merge, 'Developers + Masters')
+ def allow_devs_and_maintainers_to_merge
+ click_allow(:merge, 'Developers + Maintainers')
end
+ # @deprecated
+ alias_method :allow_devs_and_masters_to_merge, :allow_devs_and_maintainers_to_merge
+
def protect_branch
click_on 'Protect'
end
@@ -75,10 +81,6 @@ module QA
within_element(:"allowed_to_#{action}_dropdown") do
click_on text
-
- wait(reload: false) do
- has_css?('.is-active')
- end
end
end
end
diff --git a/qa/qa/page/project/settings/repository.rb b/qa/qa/page/project/settings/repository.rb
index 30900e74e90..1ed5f455a85 100644
--- a/qa/qa/page/project/settings/repository.rb
+++ b/qa/qa/page/project/settings/repository.rb
@@ -6,17 +6,21 @@ module QA
include Common
view 'app/views/projects/deploy_keys/_index.html.haml' do
- element :deploy_keys_section, 'Deploy Keys'
+ element :deploy_keys_settings
+ end
+
+ view 'app/views/projects/protected_branches/shared/_index.html.haml' do
+ element :protected_branches_settings
end
def expand_deploy_keys(&block)
- expand_section('Deploy Keys') do
+ expand_section(:deploy_keys_settings) do
DeployKeys.perform(&block)
end
end
def expand_protected_branches(&block)
- expand_section('Protected Branches') do
+ expand_section(:protected_branches_settings) do
ProtectedBranches.perform(&block)
end
end
diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb
index 5bbef040330..1dcdb59490a 100644
--- a/qa/qa/page/project/show.rb
+++ b/qa/qa/page/project/show.rb
@@ -2,11 +2,7 @@ module QA
module Page
module Project
class Show < Page::Base
- view 'app/views/shared/_clone_panel.html.haml' do
- element :clone_dropdown
- element :clone_options_dropdown, '.clone-options-dropdown'
- element :project_repository_location, 'text_field_tag :project_clone'
- end
+ include Page::Shared::ClonePanel
view 'app/views/projects/_last_push.html.haml' do
element :create_merge_request
@@ -26,19 +22,8 @@ module QA
element :branches_dropdown
end
- def choose_repository_clone_http
- choose_repository_clone('HTTP', 'http')
- end
-
- def choose_repository_clone_ssh
- # It's not always beginning with ssh:// so detecting with @
- # would be more reliable because ssh would always contain it.
- # We can't use .git because HTTP also contain that part.
- choose_repository_clone('SSH', '@')
- end
-
- def repository_location
- Git::Location.new(find('#project_clone').value)
+ view 'app/views/projects/_files.html.haml' do
+ element :tree_holder, '.tree-holder'
end
def project_name
@@ -65,9 +50,10 @@ module QA
click_element :create_merge_request
end
- def wait_for_push
- sleep 5
- refresh
+ def wait_for_import
+ wait(reload: true) do
+ has_css?('.tree-holder')
+ end
end
def go_to_new_issue
@@ -75,21 +61,6 @@ module QA
click_link 'New issue'
end
-
- private
-
- def choose_repository_clone(kind, detect_text)
- wait(reload: false) do
- click_element :clone_dropdown
-
- page.within('.clone-options-dropdown') do
- click_link(kind)
- end
-
- # Ensure git clone textbox was updated
- repository_location.git_uri.include?(detect_text)
- end
- end
end
end
end
diff --git a/qa/qa/page/project/wiki/edit.rb b/qa/qa/page/project/wiki/edit.rb
new file mode 100644
index 00000000000..6fa45569cc0
--- /dev/null
+++ b/qa/qa/page/project/wiki/edit.rb
@@ -0,0 +1,27 @@
+module QA
+ module Page
+ module Project
+ module Wiki
+ class Edit < Page::Base
+ view 'app/views/projects/wikis/_main_links.html.haml' do
+ element :new_page_link, 'New page'
+ element :page_history_link, 'Page history'
+ element :edit_page_link, 'Edit'
+ end
+
+ def go_to_new_page
+ click_on 'New page'
+ end
+
+ def got_to_view_history_page
+ click_on 'Page history'
+ end
+
+ def go_to_edit_page
+ click_on 'Edit'
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/wiki/new.rb b/qa/qa/page/project/wiki/new.rb
new file mode 100644
index 00000000000..415b3835538
--- /dev/null
+++ b/qa/qa/page/project/wiki/new.rb
@@ -0,0 +1,45 @@
+module QA
+ module Page
+ module Project
+ module Wiki
+ class New < Page::Base
+ view 'app/views/projects/wikis/_form.html.haml' do
+ element :wiki_title_textbox, 'text_field :title'
+ element :wiki_content_textarea, "render 'projects/zen', f: f, attr: :content"
+ element :wiki_message_textbox, 'text_field :message'
+ element :save_changes_button, 'submit _("Save changes")'
+ element :create_page_button, 'submit s_("Wiki|Create page")'
+ end
+
+ view 'app/views/shared/empty_states/_wikis.html.haml' do
+ element :create_link, 'Create your first page'
+ end
+
+ def go_to_create_first_page
+ click_link 'Create your first page'
+ end
+
+ def set_title(title)
+ fill_in 'wiki_title', with: title
+ end
+
+ def set_content(content)
+ fill_in 'wiki_content', with: content
+ end
+
+ def set_message(message)
+ fill_in 'wiki_message', with: message
+ end
+
+ def save_changes
+ click_on 'Save changes'
+ end
+
+ def create_new_page
+ click_on 'Create page'
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/wiki/show.rb b/qa/qa/page/project/wiki/show.rb
new file mode 100644
index 00000000000..044e514bab3
--- /dev/null
+++ b/qa/qa/page/project/wiki/show.rb
@@ -0,0 +1,19 @@
+module QA
+ module Page
+ module Project
+ module Wiki
+ class Show < Page::Base
+ include Page::Shared::ClonePanel
+
+ view 'app/views/projects/wikis/pages.html.haml' do
+ element :clone_repository_link, 'Clone repository'
+ end
+
+ def go_to_clone_repository
+ click_on 'Clone repository'
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/settings/common.rb b/qa/qa/page/settings/common.rb
index a683a6829d5..f9f71aa4a72 100644
--- a/qa/qa/page/settings/common.rb
+++ b/qa/qa/page/settings/common.rb
@@ -4,19 +4,17 @@ module QA
module Common
# Click the Expand button present in the specified section
#
- # @param [String] name present in the container in the DOM
- def expand_section(name)
- page.within('#content-body') do
- page.within('section', text: name) do
- # Because it is possible to click the button before the JS toggle code is bound
- wait(reload: false) do
- click_button 'Expand' unless first('button', text: 'Collapse')
+ # @param [Symbol] and `element` name defined in a `view` block
+ def expand_section(element_name)
+ within_element(element_name) do
+ # Because it is possible to click the button before the JS toggle code is bound
+ wait(reload: false) do
+ click_button 'Expand' unless first('button', text: 'Collapse')
- page.has_content?('Collapse')
- end
-
- yield if block_given?
+ page.has_content?('Collapse')
end
+
+ yield if block_given?
end
end
end
diff --git a/qa/qa/page/shared/clone_panel.rb b/qa/qa/page/shared/clone_panel.rb
new file mode 100644
index 00000000000..73e3dff956d
--- /dev/null
+++ b/qa/qa/page/shared/clone_panel.rb
@@ -0,0 +1,50 @@
+module QA
+ module Page
+ module Shared
+ module ClonePanel
+ def self.included(base)
+ base.view 'app/views/shared/_clone_panel.html.haml' do
+ element :clone_dropdown
+ element :clone_options_dropdown, '.clone-options-dropdown'
+ element :project_repository_location, 'text_field_tag :project_clone'
+ end
+ end
+
+ def choose_repository_clone_http
+ choose_repository_clone('HTTP', 'http')
+ end
+
+ def choose_repository_clone_ssh
+ # It's not always beginning with ssh:// so detecting with @
+ # would be more reliable because ssh would always contain it.
+ # We can't use .git because HTTP also contain that part.
+ choose_repository_clone('SSH', '@')
+ end
+
+ def repository_location
+ Git::Location.new(find('#project_clone').value)
+ end
+
+ def wait_for_push
+ sleep 5
+ refresh
+ end
+
+ private
+
+ def choose_repository_clone(kind, detect_text)
+ wait(reload: false) do
+ click_element :clone_dropdown
+
+ page.within('.clone-options-dropdown') do
+ click_link(kind)
+ end
+
+ # Ensure git clone textbox was updated
+ repository_location.git_uri.include?(detect_text)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/runtime/api.rb b/qa/qa/runtime/api.rb
deleted file mode 100644
index e2a096b971d..00000000000
--- a/qa/qa/runtime/api.rb
+++ /dev/null
@@ -1,82 +0,0 @@
-require 'airborne'
-
-module QA
- module Runtime
- module API
- class Client
- attr_reader :address
-
- def initialize(address = :gitlab)
- @address = address
- end
-
- def personal_access_token
- @personal_access_token ||= get_personal_access_token
- end
-
- def get_personal_access_token
- # you can set the environment variable PERSONAL_ACCESS_TOKEN
- # to use a specific access token rather than create one from the UI
- if Runtime::Env.personal_access_token
- Runtime::Env.personal_access_token
- else
- create_personal_access_token
- end
- end
-
- private
-
- def create_personal_access_token
- Runtime::Browser.visit(@address, Page::Main::Login) do
- Page::Main::Login.act { sign_in_using_credentials }
- Factory::Resource::PersonalAccessToken.fabricate!.access_token
- end
- end
- end
-
- class Request
- API_VERSION = 'v4'.freeze
-
- def initialize(api_client, path, personal_access_token: nil)
- personal_access_token ||= api_client.personal_access_token
- request_path = request_path(path, personal_access_token: personal_access_token)
- @session_address = Runtime::Address.new(api_client.address, request_path)
- end
-
- def url
- @session_address.address
- end
-
- # Prepend a request path with the path to the API
- #
- # path - Path to append
- #
- # Examples
- #
- # >> request_path('/issues')
- # => "/api/v4/issues"
- #
- # >> request_path('/issues', personal_access_token: 'sometoken)
- # => "/api/v4/issues?private_token=..."
- #
- # Returns the relative path to the requested API resource
- def request_path(path, version: API_VERSION, personal_access_token: nil, oauth_access_token: nil)
- full_path = File.join('/api', version, path)
-
- if oauth_access_token
- query_string = "access_token=#{oauth_access_token}"
- elsif personal_access_token
- query_string = "private_token=#{personal_access_token}"
- end
-
- if query_string
- full_path << (path.include?('?') ? '&' : '?')
- full_path << query_string
- end
-
- full_path
- end
- end
- end
- end
-end
diff --git a/qa/qa/runtime/api/client.rb b/qa/qa/runtime/api/client.rb
new file mode 100644
index 00000000000..02015e23ad8
--- /dev/null
+++ b/qa/qa/runtime/api/client.rb
@@ -0,0 +1,39 @@
+require 'airborne'
+
+module QA
+ module Runtime
+ module API
+ class Client
+ attr_reader :address
+
+ def initialize(address = :gitlab, personal_access_token: nil)
+ @address = address
+ @personal_access_token = personal_access_token
+ end
+
+ def personal_access_token
+ @personal_access_token ||= get_personal_access_token
+ end
+
+ def get_personal_access_token
+ # you can set the environment variable PERSONAL_ACCESS_TOKEN
+ # to use a specific access token rather than create one from the UI
+ if Runtime::Env.personal_access_token
+ Runtime::Env.personal_access_token
+ else
+ create_personal_access_token
+ end
+ end
+
+ private
+
+ def create_personal_access_token
+ Runtime::Browser.visit(@address, Page::Main::Login) do
+ Page::Main::Login.act { sign_in_using_credentials }
+ Factory::Resource::PersonalAccessToken.fabricate!.access_token
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/runtime/api/request.rb b/qa/qa/runtime/api/request.rb
new file mode 100644
index 00000000000..c33ada0de7a
--- /dev/null
+++ b/qa/qa/runtime/api/request.rb
@@ -0,0 +1,43 @@
+module QA
+ module Runtime
+ module API
+ class Request
+ API_VERSION = 'v4'.freeze
+
+ def initialize(api_client, path, **query_string)
+ query_string[:private_token] ||= api_client.personal_access_token unless query_string[:oauth_access_token]
+ request_path = request_path(path, **query_string)
+ @session_address = Runtime::Address.new(api_client.address, request_path)
+ end
+
+ def url
+ @session_address.address
+ end
+
+ # Prepend a request path with the path to the API
+ #
+ # path - Path to append
+ #
+ # Examples
+ #
+ # >> request_path('/issues')
+ # => "/api/v4/issues"
+ #
+ # >> request_path('/issues', private_token: 'sometoken)
+ # => "/api/v4/issues?private_token=..."
+ #
+ # Returns the relative path to the requested API resource
+ def request_path(path, version: API_VERSION, **query_string)
+ full_path = File.join('/api', version, path)
+
+ if query_string.any?
+ full_path << (path.include?('?') ? '&' : '?')
+ full_path << query_string.map { |k, v| "#{k}=#{CGI.escape(v)}" }.join('&')
+ end
+
+ full_path
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/runtime/browser.rb b/qa/qa/runtime/browser.rb
index a12d95683af..877864fb40c 100644
--- a/qa/qa/runtime/browser.rb
+++ b/qa/qa/runtime/browser.rb
@@ -32,6 +32,12 @@ module QA
end
def self.configure!
+ RSpec.configure do |config|
+ config.define_derived_metadata(file_path: %r{/qa/specs/features/}) do |metadata|
+ metadata[:type] = :feature
+ end
+ end
+
return if Capybara.drivers.include?(:chrome)
Capybara.register_driver :chrome do |app|
@@ -79,6 +85,10 @@ module QA
driver.browser.save_screenshot(path)
end
+ Capybara::Screenshot.register_filename_prefix_formatter(:rspec) do |example|
+ File.join(QA::Runtime::Namespace.name, example.file_path.sub('./qa/specs/features/', ''))
+ end
+
Capybara.configure do |config|
config.default_driver = :chrome
config.javascript_driver = :chrome
@@ -102,19 +112,7 @@ module QA
def perform(&block)
visit(url)
- yield if block_given?
- rescue
- raise if block.nil?
-
- # RSpec examples will take care of screenshots on their own
- #
- unless block.binding.receiver.is_a?(RSpec::Core::ExampleGroup)
- screenshot_and_save_page
- end
-
- raise
- ensure
- clear! if block_given?
+ yield.tap { clear! } if block_given?
end
##
diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb
index fe432edfa2a..5dc194e0aef 100644
--- a/qa/qa/runtime/env.rb
+++ b/qa/qa/runtime/env.rb
@@ -3,6 +3,8 @@ module QA
module Env
extend self
+ attr_writer :user_type
+
# set to 'false' to have Chrome run visibly instead of headless
def chrome_headless?
(ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i) != 0
@@ -20,7 +22,9 @@ module QA
# By default, "standard" denotes a standard GitLab user login.
# Set this to "ldap" if the user should be logged in via LDAP.
def user_type
- (ENV['GITLAB_USER_TYPE'] || 'standard').tap do |type|
+ return @user_type if defined?(@user_type) # rubocop:disable Gitlab/ModuleWithInstanceVariables
+
+ ENV.fetch('GITLAB_USER_TYPE', 'standard').tap do |type|
unless %w(ldap standard).include?(type)
raise ArgumentError.new("Invalid user type '#{type}': must be 'ldap' or 'standard'")
end
@@ -46,6 +50,33 @@ module QA
def sandbox_name
ENV['GITLAB_SANDBOX_NAME']
end
+
+ def gcloud_account_key
+ ENV.fetch("GCLOUD_ACCOUNT_KEY")
+ end
+
+ def gcloud_account_email
+ ENV.fetch("GCLOUD_ACCOUNT_EMAIL")
+ end
+
+ def gcloud_zone
+ ENV.fetch('GCLOUD_ZONE')
+ end
+
+ def has_gcloud_credentials?
+ %w[GCLOUD_ACCOUNT_KEY GCLOUD_ACCOUNT_EMAIL].none? { |var| ENV[var].to_s.empty? }
+ end
+
+ # Specifies the token that can be used for the GitHub API
+ def github_access_token
+ ENV['GITHUB_ACCESS_TOKEN'].to_s.strip
+ end
+
+ def require_github_access_token!
+ return unless github_access_token.empty?
+
+ raise ArgumentError, "Please provide GITHUB_ACCESS_TOKEN"
+ end
end
end
end
diff --git a/qa/qa/runtime/namespace.rb b/qa/qa/runtime/namespace.rb
index 8d05b387416..28f17d1160b 100644
--- a/qa/qa/runtime/namespace.rb
+++ b/qa/qa/runtime/namespace.rb
@@ -8,7 +8,7 @@ module QA
end
def name
- 'qa-test-' + time.strftime('%d-%m-%Y-%H-%M-%S')
+ "qa-test-#{time.strftime('%Y-%m-%d-%Y-%H-%M-%S')}"
end
def path
@@ -16,7 +16,7 @@ module QA
end
def sandbox_name
- Runtime::Env.sandbox_name || 'gitlab-qa-sandbox'
+ Runtime::Env.sandbox_name || 'gitlab-qa-sandbox-group'
end
end
end
diff --git a/qa/qa/runtime/release.rb b/qa/qa/runtime/release.rb
index 12e56404cf6..4f83a773645 100644
--- a/qa/qa/runtime/release.rb
+++ b/qa/qa/runtime/release.rb
@@ -21,7 +21,7 @@ module QA
end
def self.method_missing(name, *args)
- self.new.strategy.public_send(name, *args) # rubocop:disable GitlabSecurity/PublicSend
+ self.new.strategy.public_send(name, *args)
end
end
end
diff --git a/qa/qa/scenario/test/integration/github.rb b/qa/qa/scenario/test/integration/github.rb
new file mode 100644
index 00000000000..1d22b532aa5
--- /dev/null
+++ b/qa/qa/scenario/test/integration/github.rb
@@ -0,0 +1,18 @@
+module QA
+ module Scenario
+ module Test
+ module Integration
+ class Github < Test::Instance
+ tags :github
+
+ def perform(address, *rspec_options)
+ # This test suite requires a GitHub personal access token
+ Runtime::Env.require_github_access_token!
+
+ super
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/scenario/test/integration/kubernetes.rb b/qa/qa/scenario/test/integration/kubernetes.rb
new file mode 100644
index 00000000000..7479073e979
--- /dev/null
+++ b/qa/qa/scenario/test/integration/kubernetes.rb
@@ -0,0 +1,11 @@
+module QA
+ module Scenario
+ module Test
+ module Integration
+ class Kubernetes < Test::Instance
+ tags :kubernetes
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/service/kubernetes_cluster.rb b/qa/qa/service/kubernetes_cluster.rb
new file mode 100644
index 00000000000..abd9d53554f
--- /dev/null
+++ b/qa/qa/service/kubernetes_cluster.rb
@@ -0,0 +1,77 @@
+require 'securerandom'
+require 'mkmf'
+
+module QA
+ module Service
+ class KubernetesCluster
+ include Service::Shellout
+
+ attr_reader :api_url, :ca_certificate, :token
+
+ def cluster_name
+ @cluster_name ||= "qa-cluster-#{SecureRandom.hex(4)}-#{Time.now.utc.strftime("%Y%m%d%H%M%S")}"
+ end
+
+ def create!
+ validate_dependencies
+ login_if_not_already_logged_in
+
+ shell <<~CMD.tr("\n", ' ')
+ gcloud container clusters
+ create #{cluster_name}
+ --enable-legacy-authorization
+ --zone #{Runtime::Env.gcloud_zone}
+ && gcloud container clusters
+ get-credentials
+ --zone #{Runtime::Env.gcloud_zone}
+ #{cluster_name}
+ CMD
+
+ @api_url = `kubectl config view --minify -o jsonpath='{.clusters[].cluster.server}'`
+ @ca_certificate = Base64.decode64(`kubectl get secrets -o jsonpath="{.items[0].data['ca\\.crt']}"`)
+ @token = Base64.decode64(`kubectl get secrets -o jsonpath='{.items[0].data.token}'`)
+ self
+ end
+
+ def remove!
+ shell <<~CMD.tr("\n", ' ')
+ gcloud container clusters delete
+ --zone #{Runtime::Env.gcloud_zone}
+ #{cluster_name}
+ --quiet --async
+ CMD
+ end
+
+ private
+
+ def validate_dependencies
+ find_executable('gcloud') || raise("You must first install `gcloud` executable to run these tests.")
+ find_executable('kubectl') || raise("You must first install `kubectl` executable to run these tests.")
+ end
+
+ def login_if_not_already_logged_in
+ if Runtime::Env.has_gcloud_credentials?
+ attempt_login_with_env_vars
+ else
+ account = `gcloud auth list --filter=status:ACTIVE --format="value(account)"`
+ if account.empty?
+ raise "Failed to login to gcloud. No credentials provided in environment and no credentials found locally."
+ else
+ puts "gcloud account found. Using: #{account} for creating K8s cluster."
+ end
+ end
+ end
+
+ def attempt_login_with_env_vars
+ puts "No gcloud account. Attempting to login from env vars GCLOUD_ACCOUNT_EMAIL and GCLOUD_ACCOUNT_KEY."
+ gcloud_account_key = Tempfile.new('gcloud-account-key')
+ gcloud_account_key.write(Runtime::Env.gcloud_account_key)
+ gcloud_account_key.close
+ gcloud_account_email = Runtime::Env.gcloud_account_email
+ shell("gcloud auth activate-service-account #{gcloud_account_email} --key-file #{gcloud_account_key.path}")
+ ensure
+ gcloud_account_key && gcloud_account_key.unlink
+ end
+ end
+ end
+end
diff --git a/qa/qa/service/runner.rb b/qa/qa/service/runner.rb
index c0352e0467a..9417c707105 100644
--- a/qa/qa/service/runner.rb
+++ b/qa/qa/service/runner.rb
@@ -3,7 +3,6 @@ require 'securerandom'
module QA
module Service
class Runner
- include Scenario::Actable
include Service::Shellout
attr_accessor :token, :address, :tags, :image
diff --git a/qa/qa/specs/features/api/basics_spec.rb b/qa/qa/specs/features/api/basics_spec.rb
new file mode 100644
index 00000000000..6563b56d1b4
--- /dev/null
+++ b/qa/qa/specs/features/api/basics_spec.rb
@@ -0,0 +1,61 @@
+require 'securerandom'
+
+module QA
+ describe 'API basics', :core do
+ before(:context) do
+ @api_client = Runtime::API::Client.new(:gitlab)
+ end
+
+ let(:project_name) { "api-basics-#{SecureRandom.hex(8)}" }
+ let(:sanitized_project_path) { CGI.escape("#{Runtime::User.name}/#{project_name}") }
+
+ it 'user creates a project with a file and deletes them afterwards' do
+ create_project_request = Runtime::API::Request.new(@api_client, '/projects')
+ post create_project_request.url, path: project_name, name: project_name
+
+ expect_status(201)
+ expect(json_body).to match(
+ a_hash_including(name: project_name, path: project_name)
+ )
+
+ create_file_request = Runtime::API::Request.new(@api_client, "/projects/#{sanitized_project_path}/repository/files/README.md")
+ post create_file_request.url, branch: 'master', content: 'Hello world', commit_message: 'Add README.md'
+
+ expect_status(201)
+ expect(json_body).to match(
+ a_hash_including(branch: 'master', file_path: 'README.md')
+ )
+
+ get_file_request = Runtime::API::Request.new(@api_client, "/projects/#{sanitized_project_path}/repository/files/README.md", ref: 'master')
+ get get_file_request.url
+
+ expect_status(200)
+ expect(json_body).to match(
+ a_hash_including(
+ ref: 'master',
+ file_path: 'README.md', file_name: 'README.md',
+ encoding: 'base64', content: 'SGVsbG8gd29ybGQ='
+ )
+ )
+
+ delete_file_request = Runtime::API::Request.new(@api_client, "/projects/#{sanitized_project_path}/repository/files/README.md", branch: 'master', commit_message: 'Remove README.md')
+ delete delete_file_request.url
+
+ expect_status(204)
+
+ get_tree_request = Runtime::API::Request.new(@api_client, "/projects/#{sanitized_project_path}/repository/tree")
+ get get_tree_request.url
+
+ expect_status(200)
+ expect(json_body).to eq([])
+
+ delete_project_request = Runtime::API::Request.new(@api_client, "/projects/#{sanitized_project_path}")
+ delete delete_project_request.url
+
+ expect_status(202)
+ expect(json_body).to match(
+ a_hash_including(message: '202 Accepted')
+ )
+ end
+ end
+end
diff --git a/qa/qa/specs/features/api/users_spec.rb b/qa/qa/specs/features/api/users_spec.rb
index d4ff4ebbc9a..8a63d8095c9 100644
--- a/qa/qa/specs/features/api/users_spec.rb
+++ b/qa/qa/specs/features/api/users_spec.rb
@@ -1,5 +1,5 @@
module QA
- feature 'API users', :core do
+ describe 'API users', :core do
before(:context) do
@api_client = Runtime::API::Client.new(:gitlab)
end
@@ -7,32 +7,31 @@ module QA
context 'when authenticated' do
let(:request) { Runtime::API::Request.new(@api_client, '/users') }
- scenario 'get list of users' do
+ it 'get list of users' do
get request.url
expect_status(200)
end
- scenario 'submit request with a valid user name' do
+ it 'submit request with a valid user name' do
get request.url, { params: { username: Runtime::User.name } }
expect_status(200)
- expect(json_body).to be_an Array
- expect(json_body.size).to eq(1)
- expect(json_body.first[:username]).to eq Runtime::User.name
+ expect(json_body).to contain_exactly(
+ a_hash_including(username: Runtime::User.name)
+ )
end
- scenario 'submit request with an invalid user name' do
+ it 'submit request with an invalid user name' do
get request.url, { params: { username: SecureRandom.hex(10) } }
expect_status(200)
- expect(json_body).to be_an Array
- expect(json_body.size).to eq(0)
+ expect(json_body).to eq([])
end
end
- scenario 'submit request with an invalid token' do
- request = Runtime::API::Request.new(@api_client, '/users', personal_access_token: 'invalid')
+ it 'submit request with an invalid token' do
+ request = Runtime::API::Request.new(@api_client, '/users', private_token: 'invalid')
get request.url
diff --git a/qa/qa/specs/features/login/ldap_spec.rb b/qa/qa/specs/features/login/ldap_spec.rb
index ac2bd5a3c39..b7a284c584b 100644
--- a/qa/qa/specs/features/login/ldap_spec.rb
+++ b/qa/qa/specs/features/login/ldap_spec.rb
@@ -1,8 +1,12 @@
module QA
- feature 'LDAP user login', :ldap do
- scenario 'user logs in using LDAP credentials' do
+ describe 'LDAP user login', :ldap do
+ before do
+ Runtime::Env.user_type = 'ldap'
+ end
+
+ it 'user logs in using LDAP credentials' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.act { sign_in_using_ldap_credentials }
+ 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/login/standard_spec.rb b/qa/qa/specs/features/login/standard_spec.rb
index 141ffa3cfb7..254f47cf217 100644
--- a/qa/qa/specs/features/login/standard_spec.rb
+++ b/qa/qa/specs/features/login/standard_spec.rb
@@ -1,6 +1,6 @@
module QA
- feature 'standard user login', :core do
- scenario 'user logs in using credentials' do
+ describe 'standard user login', :core do
+ it 'user logs in using credentials' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
diff --git a/qa/qa/specs/features/mattermost/group_create_spec.rb b/qa/qa/specs/features/mattermost/group_create_spec.rb
index 2e27a285223..a59761da341 100644
--- a/qa/qa/specs/features/mattermost/group_create_spec.rb
+++ b/qa/qa/specs/features/mattermost/group_create_spec.rb
@@ -1,6 +1,6 @@
module QA
- feature 'create a new group', :mattermost do
- scenario 'creating a group with a mattermost team' do
+ describe 'create a new group', :mattermost do
+ it 'creating a group with a mattermost team' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
Page::Menu::Main.act { go_to_groups }
diff --git a/qa/qa/specs/features/mattermost/login_spec.rb b/qa/qa/specs/features/mattermost/login_spec.rb
index 637bbdd643a..b140191e160 100644
--- a/qa/qa/specs/features/mattermost/login_spec.rb
+++ b/qa/qa/specs/features/mattermost/login_spec.rb
@@ -1,6 +1,6 @@
module QA
- feature 'logging in to Mattermost', :mattermost do
- scenario 'can use gitlab oauth' do
+ describe 'logging in to Mattermost', :mattermost do
+ it 'can use gitlab oauth' do
Runtime::Browser.visit(:gitlab, Page::Main::Login) do
Page::Main::Login.act { sign_in_using_credentials }
diff --git a/qa/qa/specs/features/merge_request/create_spec.rb b/qa/qa/specs/features/merge_request/create_spec.rb
index 0931e649e24..5807e539699 100644
--- a/qa/qa/specs/features/merge_request/create_spec.rb
+++ b/qa/qa/specs/features/merge_request/create_spec.rb
@@ -1,17 +1,31 @@
module QA
- feature 'creates a merge request', :core do
- scenario 'user creates a new merge request' do
+ describe 'creates a merge request', :core do
+ it 'user creates a new merge request' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
+ current_project = Factory::Resource::Project.fabricate! do |project|
+ project.name = 'project-with-merge-request-and-milestone'
+ end
+
+ current_milestone = Factory::Resource::ProjectMilestone.fabricate! do |milestone|
+ milestone.title = 'unique-milestone'
+ milestone.project = current_project
+ end
+
Factory::Resource::MergeRequest.fabricate! do |merge_request|
- merge_request.title = 'This is a merge request'
- merge_request.description = 'Great feature'
+ merge_request.title = 'This is a merge request with a milestone'
+ merge_request.description = 'Great feature with milestone'
+ merge_request.project = current_project
+ merge_request.milestone = current_milestone
end
- expect(page).to have_content('This is a merge request')
- expect(page).to have_content('Great feature')
- expect(page).to have_content(/Opened [\w\s]+ a minute ago/)
+ Page::MergeRequest::Show.perform do |merge_request|
+ expect(page).to have_content('This is a merge request with a milestone')
+ expect(page).to have_content('Great feature with milestone')
+ expect(page).to have_content(/Opened [\w\s]+ ago/)
+ expect(merge_request).to have_milestone(current_milestone.title)
+ end
end
end
end
diff --git a/qa/qa/specs/features/merge_request/rebase_spec.rb b/qa/qa/specs/features/merge_request/rebase_spec.rb
index 2a44d42af6f..163dcbe7963 100644
--- a/qa/qa/specs/features/merge_request/rebase_spec.rb
+++ b/qa/qa/specs/features/merge_request/rebase_spec.rb
@@ -1,6 +1,6 @@
module QA
- feature 'merge request rebase', :core do
- scenario 'rebases source branch of merge request' do
+ describe 'merge request rebase', :core do
+ it 'rebases source branch of merge request' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
@@ -16,7 +16,7 @@ module QA
merge_request.title = 'Needs rebasing'
end
- Factory::Repository::Push.fabricate! do |push|
+ Factory::Repository::ProjectPush.fabricate! do |push|
push.project = project
push.file_name = "other.txt"
push.file_content = "New file added!"
diff --git a/qa/qa/specs/features/merge_request/squash_spec.rb b/qa/qa/specs/features/merge_request/squash_spec.rb
new file mode 100644
index 00000000000..4856bbe1a69
--- /dev/null
+++ b/qa/qa/specs/features/merge_request/squash_spec.rb
@@ -0,0 +1,50 @@
+module QA
+ describe 'merge request squash commits', :core do
+ it 'when squash commits is marked before merge' do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.act { sign_in_using_credentials }
+
+ project = Factory::Resource::Project.fabricate! do |project|
+ project.name = "squash-before-merge"
+ end
+
+ merge_request = Factory::Resource::MergeRequest.fabricate! do |merge_request|
+ merge_request.project = project
+ merge_request.title = 'Squashing commits'
+ end
+
+ Factory::Repository::ProjectPush.fabricate! do |push|
+ push.project = project
+ push.commit_message = 'to be squashed'
+ push.branch_name = merge_request.source_branch
+ push.new_branch = false
+ push.file_name = 'other.txt'
+ push.file_content = "Test with unicode characters â¤âœ“€â„"
+ end
+
+ merge_request.visit!
+
+ expect(page).to have_text('to be squashed')
+
+ Page::MergeRequest::Show.perform do |merge_request_page|
+ merge_request_page.mark_to_squash
+ merge_request_page.merge!
+
+ merge_request.project.visit!
+
+ Git::Repository.perform do |repository|
+ repository.uri = Page::Project::Show.act do
+ choose_repository_clone_http
+ repository_location.uri
+ end
+
+ repository.use_default_credentials
+
+ repository.act { clone }
+
+ expect(repository.commits.size).to eq 3
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/project/activity_spec.rb b/qa/qa/specs/features/project/activity_spec.rb
index ba94ce8cf28..02074e386b6 100644
--- a/qa/qa/specs/features/project/activity_spec.rb
+++ b/qa/qa/specs/features/project/activity_spec.rb
@@ -1,10 +1,10 @@
module QA
- feature 'activity page', :core do
- scenario 'push creates an event in the activity page' do
+ describe 'activity page', :core do
+ it 'push creates an event in the activity page' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- Factory::Repository::Push.fabricate! do |push|
+ Factory::Repository::ProjectPush.fabricate! do |push|
push.file_name = 'README.md'
push.file_content = '# This is a test project'
push.commit_message = 'Add README.md'
diff --git a/qa/qa/specs/features/project/add_deploy_key_spec.rb b/qa/qa/specs/features/project/add_deploy_key_spec.rb
index de53613dee1..14642af97ad 100644
--- a/qa/qa/specs/features/project/add_deploy_key_spec.rb
+++ b/qa/qa/specs/features/project/add_deploy_key_spec.rb
@@ -1,6 +1,6 @@
module QA
- feature 'deploy keys support', :core do
- scenario 'user adds a deploy key' do
+ describe 'deploy keys support', :core do
+ it 'user adds a deploy key' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
diff --git a/qa/qa/specs/features/project/add_secret_variable_spec.rb b/qa/qa/specs/features/project/add_secret_variable_spec.rb
index d1bf7849bd0..32c91dd9d4d 100644
--- a/qa/qa/specs/features/project/add_secret_variable_spec.rb
+++ b/qa/qa/specs/features/project/add_secret_variable_spec.rb
@@ -1,6 +1,6 @@
module QA
- feature 'secret variables support', :core do
- scenario 'user adds a secret variable' do
+ describe 'secret variables support', :core do
+ it 'user adds a secret variable' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
diff --git a/qa/qa/specs/features/project/auto_devops_spec.rb b/qa/qa/specs/features/project/auto_devops_spec.rb
new file mode 100644
index 00000000000..bc713b46d81
--- /dev/null
+++ b/qa/qa/specs/features/project/auto_devops_spec.rb
@@ -0,0 +1,57 @@
+require 'pathname'
+
+module QA
+ describe 'Auto Devops', :kubernetes do
+ after do
+ @cluster&.remove!
+ end
+
+ it 'user creates a new project and runs auto devops' do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.act { sign_in_using_credentials }
+
+ project = Factory::Resource::Project.fabricate! do |p|
+ p.name = 'project-with-autodevops'
+ p.description = 'Project with Auto Devops'
+ end
+
+ # Create Auto Devops compatible repo
+ Factory::Repository::ProjectPush.fabricate! do |push|
+ push.project = project
+ push.directory = Pathname
+ .new(__dir__)
+ .join('../../../fixtures/auto_devops_rack')
+ push.commit_message = 'Create Auto DevOps compatible rack application'
+ end
+
+ Page::Project::Show.act { wait_for_push }
+
+ # Create and connect K8s cluster
+ @cluster = Service::KubernetesCluster.new.create!
+ kubernetes_cluster = Factory::Resource::KubernetesCluster.fabricate! do |cluster|
+ cluster.project = project
+ cluster.cluster = @cluster
+ cluster.install_helm_tiller = true
+ cluster.install_ingress = true
+ cluster.install_prometheus = true
+ cluster.install_runner = true
+ end
+
+ project.visit!
+ Page::Menu::Side.act { click_ci_cd_settings }
+ Page::Project::Settings::CICD.perform do |p|
+ p.enable_auto_devops_with_domain("#{kubernetes_cluster.ingress_ip}.nip.io")
+ end
+
+ project.visit!
+ Page::Menu::Side.act { click_ci_cd_pipelines }
+ Page::Project::Pipeline::Index.act { go_to_latest_pipeline }
+
+ Page::Project::Pipeline::Show.perform do |pipeline|
+ expect(pipeline).to have_build('build', status: :success, wait: 600)
+ expect(pipeline).to have_build('test', status: :success, wait: 600)
+ expect(pipeline).to have_build('production', status: :success, wait: 600)
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/project/create_issue_spec.rb b/qa/qa/specs/features/project/create_issue_spec.rb
index b73f108c2d9..ac2ed2ca2a1 100644
--- a/qa/qa/specs/features/project/create_issue_spec.rb
+++ b/qa/qa/specs/features/project/create_issue_spec.rb
@@ -1,8 +1,8 @@
module QA
- feature 'creates issue', :core do
+ describe 'creates issue', :core do
let(:issue_title) { 'issue title' }
- scenario 'user creates issue' do
+ it 'user creates issue' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
diff --git a/qa/qa/specs/features/project/create_spec.rb b/qa/qa/specs/features/project/create_spec.rb
index b1c07249892..14ecd464685 100644
--- a/qa/qa/specs/features/project/create_spec.rb
+++ b/qa/qa/specs/features/project/create_spec.rb
@@ -1,6 +1,6 @@
module QA
- feature 'create a new project', :core do
- scenario 'user creates a new project' do
+ describe 'create a new project', :core do
+ it 'user creates a new project' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
diff --git a/qa/qa/specs/features/project/deploy_key_clone_spec.rb b/qa/qa/specs/features/project/deploy_key_clone_spec.rb
index 442ac312b4d..054f49b408a 100644
--- a/qa/qa/specs/features/project/deploy_key_clone_spec.rb
+++ b/qa/qa/specs/features/project/deploy_key_clone_spec.rb
@@ -1,7 +1,7 @@
require 'digest/sha1'
module QA
- feature 'cloning code using a deploy key', :core, :docker do
+ describe 'cloning code using a deploy key', :core, :docker do
def login
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
@@ -39,7 +39,7 @@ module QA
]
keys.each do |(key_class, bits)|
- scenario "user sets up a deploy key with #{key_class}(#{bits}) to clone code using pipelines" do
+ it "user sets up a deploy key with #{key_class}(#{bits}) to clone code using pipelines" do
key = key_class.new(*bits)
login
@@ -75,7 +75,7 @@ module QA
- docker
YAML
- Factory::Repository::Push.fabricate! do |resource|
+ Factory::Repository::ProjectPush.fabricate! do |resource|
resource.project = @project
resource.file_name = '.gitlab-ci.yml'
resource.commit_message = 'Add .gitlab-ci.yml'
@@ -92,7 +92,9 @@ module QA
Page::Project::Pipeline::Show.act { go_to_first_job }
Page::Project::Job::Show.perform do |job|
- job.wait(reload: false) { job.completed? }
+ job.wait(reload: false) do
+ job.completed? && !job.trace_loading?
+ end
expect(job.passed?).to be_truthy, "Job status did not become \"passed\"."
expect(job.output).to include(sha1sum)
diff --git a/qa/qa/specs/features/project/import_from_github_spec.rb b/qa/qa/specs/features/project/import_from_github_spec.rb
new file mode 100644
index 00000000000..221b5c27fba
--- /dev/null
+++ b/qa/qa/specs/features/project/import_from_github_spec.rb
@@ -0,0 +1,106 @@
+module QA
+ describe 'user imports a GitHub repo', :core, :github do
+ let(:imported_project) do
+ Factory::Resource::ProjectImportedFromGithub.fabricate! do |project|
+ project.name = 'imported-project'
+ project.personal_access_token = Runtime::Env.github_access_token
+ project.github_repository_path = 'gitlab-qa/test-project'
+ end
+ end
+
+ after do
+ # We need to delete the imported project because it's impossible to import
+ # the same GitHub project twice for a given user.
+ api_client = Runtime::API::Client.new(:gitlab)
+ delete_project_request = Runtime::API::Request.new(api_client, "/projects/#{CGI.escape("#{Runtime::Namespace.path}/#{imported_project.name}")}")
+ delete delete_project_request.url
+
+ expect_status(202)
+ end
+
+ it 'user imports a GitHub repo' do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.act { sign_in_using_credentials }
+
+ imported_project # import the project
+
+ Page::Menu::Main.act { go_to_projects }
+ Page::Dashboard::Projects.perform do |dashboard|
+ dashboard.go_to_project(imported_project.name)
+ end
+
+ Page::Project::Show.act { wait_for_import }
+
+ verify_repository_import
+ verify_issues_import
+ verify_merge_requests_import
+ verify_labels_import
+ verify_milestones_import
+ verify_wiki_import
+ end
+
+ def verify_repository_import
+ expect(page).to have_content('This test project is used for automated GitHub import by GitLab QA.')
+ expect(page).to have_content(imported_project.name)
+ end
+
+ def verify_issues_import
+ Page::Menu::Side.act { click_issues }
+ expect(page).to have_content('This is a sample issue')
+
+ click_link 'This is a sample issue'
+
+ expect(page).to have_content('We should populate this project with issues, pull requests and wiki pages.')
+
+ # Comments
+ expect(page).to have_content('This is a comment from @rymai.')
+
+ Page::Issuable::Sidebar.perform do |issuable|
+ expect(issuable).to have_label('enhancement')
+ expect(issuable).to have_label('help wanted')
+ expect(issuable).to have_label('good first issue')
+ end
+ end
+
+ def verify_merge_requests_import
+ Page::Menu::Side.act { click_merge_requests }
+ expect(page).to have_content('Improve README.md')
+
+ click_link 'Improve README.md'
+
+ expect(page).to have_content('This improves the README file a bit.')
+
+ # Review comment are not supported yet
+ expect(page).not_to have_content('Really nice change.')
+
+ # Comments
+ expect(page).to have_content('Nice work! This is a comment from @rymai.')
+
+ # Diff comments
+ expect(page).to have_content('[Review comment] I like that!')
+ expect(page).to have_content('[Review comment] Nice blank line.')
+ expect(page).to have_content('[Single diff comment] Much better without this line!')
+
+ Page::Issuable::Sidebar.perform do |issuable|
+ expect(issuable).to have_label('bug')
+ expect(issuable).to have_label('enhancement')
+ end
+ end
+
+ def verify_labels_import
+ # TODO: Waiting on https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/19228
+ # to build upon it.
+ end
+
+ def verify_milestones_import
+ # TODO: Waiting on https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18727
+ # to build upon it.
+ end
+
+ def verify_wiki_import
+ Page::Menu::Side.act { click_wiki }
+
+ expect(page).to have_content('Welcome to the test-project wiki!')
+ end
+ end
+end
diff --git a/qa/qa/specs/features/project/pipelines_spec.rb b/qa/qa/specs/features/project/pipelines_spec.rb
index 74f6474443d..ddedde7a8bc 100644
--- a/qa/qa/specs/features/project/pipelines_spec.rb
+++ b/qa/qa/specs/features/project/pipelines_spec.rb
@@ -1,12 +1,12 @@
module QA
- feature 'CI/CD Pipelines', :core, :docker do
+ describe 'CI/CD Pipelines', :core, :docker do
let(:executor) { "qa-runner-#{Time.now.to_i}" }
after do
Service::Runner.new(executor).remove!
end
- scenario 'user registers a new specific runner' do
+ it 'user registers a new specific runner' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
@@ -25,7 +25,7 @@ module QA
end
end
- scenario 'users creates a new pipeline' do
+ it 'users creates a new pipeline' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
@@ -40,7 +40,7 @@ module QA
runner.tags = %w[qa test]
end
- Factory::Repository::Push.fabricate! do |push|
+ Factory::Repository::ProjectPush.fabricate! do |push|
push.project = project
push.file_name = '.gitlab-ci.yml'
push.commit_message = 'Add .gitlab-ci.yml'
diff --git a/qa/qa/specs/features/project/wikis_spec.rb b/qa/qa/specs/features/project/wikis_spec.rb
new file mode 100644
index 00000000000..59cc455fffc
--- /dev/null
+++ b/qa/qa/specs/features/project/wikis_spec.rb
@@ -0,0 +1,45 @@
+module QA
+ describe 'Wiki Functionality', :core do
+ def login
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.act { sign_in_using_credentials }
+ end
+
+ def validate_content(content)
+ expect(page).to have_content('Wiki was successfully updated')
+ expect(page).to have_content(/#{content}/)
+ end
+
+ before do
+ login
+ end
+
+ it 'User creates, edits, clones, and pushes to the wiki' do
+ wiki = Factory::Resource::Wiki.fabricate! do |resource|
+ resource.title = 'Home'
+ resource.content = '# My First Wiki Content'
+ resource.message = 'Update home'
+ end
+
+ validate_content('My First Wiki Content')
+
+ Page::Project::Wiki::Edit.act { go_to_edit_page }
+ Page::Project::Wiki::New.perform do |page|
+ page.set_content("My Second Wiki Content")
+ page.save_changes
+ end
+
+ validate_content('My Second Wiki Content')
+
+ Factory::Repository::WikiPush.fabricate! do |push|
+ push.wiki = wiki
+ push.file_name = 'Home.md'
+ push.file_content = '# My Third Wiki Content'
+ push.commit_message = 'Update Home.md'
+ end
+ Page::Menu::Side.act { click_wiki }
+
+ expect(page).to have_content('My Third Wiki Content')
+ end
+ end
+end
diff --git a/qa/qa/specs/features/repository/clone_spec.rb b/qa/qa/specs/features/repository/clone_spec.rb
index bc9eb57bdb4..a04ce4e44d9 100644
--- a/qa/qa/specs/features/repository/clone_spec.rb
+++ b/qa/qa/specs/features/repository/clone_spec.rb
@@ -1,7 +1,7 @@
module QA
- feature 'clone code from the repository', :core do
+ describe 'clone code from the repository', :core do
context 'with regular account over http' do
- given(:location) do
+ let(:location) do
Page::Project::Show.act do
choose_repository_clone_http
repository_location
@@ -31,7 +31,7 @@ module QA
end
end
- scenario 'user performs a deep clone' do
+ it 'user performs a deep clone' do
Git::Repository.perform do |repository|
repository.uri = location.uri
repository.use_default_credentials
@@ -42,7 +42,7 @@ module QA
end
end
- scenario 'user performs a shallow clone' do
+ it 'user performs a shallow clone' do
Git::Repository.perform do |repository|
repository.uri = location.uri
repository.use_default_credentials
diff --git a/qa/qa/specs/features/repository/protected_branches_spec.rb b/qa/qa/specs/features/repository/protected_branches_spec.rb
index 406b2772b64..29ea2e69ec7 100644
--- a/qa/qa/specs/features/repository/protected_branches_spec.rb
+++ b/qa/qa/specs/features/repository/protected_branches_spec.rb
@@ -1,69 +1,80 @@
module QA
- feature 'branch protection support', :core do
- given(:branch_name) { 'protected-branch' }
- given(:commit_message) { 'Protected push commit message' }
- given(:project) do
+ describe 'branch protection support', :core do
+ let(:branch_name) { 'protected-branch' }
+ let(:commit_message) { 'Protected push commit message' }
+ let(:project) do
Factory::Resource::Project.fabricate! do |resource|
resource.name = 'protected-branch-project'
end
end
- given(:location) do
- Page::Project::Show.act do
- choose_repository_clone_http
- repository_location
- end
- end
before do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
end
- after do
+ after do |example|
# We need to clear localStorage because we're using it for the dropdown,
# and capybara doesn't do this for us.
# https://github.com/teamcapybara/capybara/issues/1702
Capybara.execute_script 'localStorage.clear()'
+
+ # In order to help diagnose a false failure
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/48241
+ log_push_output if example.exception
end
- scenario 'user is able to protect a branch' do
- protected_branch = Factory::Resource::Branch.fabricate! do |resource|
- resource.branch_name = branch_name
- resource.project = project
- resource.allow_to_push = true
- resource.protected = true
+ context 'when developers and maintainers are allowed to push to a protected branch' do
+ let!(:protected_branch) { create_protected_branch(allow_to_push: true) }
+
+ it 'user with push rights successfully pushes to the protected branch' do
+ expect(protected_branch.name).to have_content(branch_name)
+ expect(protected_branch.push_allowance).to have_content('Developers + Maintainers')
+
+ @push = push_new_file(branch_name)
+
+ expect(@push.output).to match(/remote: To create a merge request for protected-branch, visit/)
end
+ end
+
+ context 'when developers and maintainers are not allowed to push to a protected branch' do
+ it 'user without push rights fails to push to the protected branch' do
+ create_protected_branch(allow_to_push: false)
+
+ @push = push_new_file(branch_name)
- expect(protected_branch.name).to have_content(branch_name)
- expect(protected_branch.push_allowance).to have_content('Developers + Masters')
+ expect(@push.output)
+ .to match(/remote\: GitLab\: You are not allowed to push code to protected branches on this project/)
+ expect(@push.output)
+ .to match(/\[remote rejected\] #{branch_name} -> #{branch_name} \(pre-receive hook declined\)/)
+ end
end
- scenario 'users without authorization cannot push to protected branch' do
+ def create_protected_branch(allow_to_push:)
Factory::Resource::Branch.fabricate! do |resource|
resource.branch_name = branch_name
resource.project = project
- resource.allow_to_push = false
+ resource.allow_to_push = allow_to_push
resource.protected = true
end
+ end
- project.visit!
-
- Git::Repository.perform do |repository|
- repository.uri = location.uri
- repository.use_default_credentials
-
- repository.act do
- clone
- configure_identity('GitLab QA', 'root@gitlab.com')
- checkout('protected-branch')
- commit_file('README.md', 'readme content', 'Add a readme')
- push_changes('protected-branch')
- end
+ def push_new_file(branch)
+ Factory::Repository::ProjectPush.fabricate! do |resource|
+ resource.project = project
+ resource.file_name = 'new_file.md'
+ resource.file_content = '# This is a new file'
+ resource.commit_message = 'Add new_file.md'
+ resource.branch_name = branch_name
+ resource.new_branch = false
+ end
+ end
- expect(repository.push_error)
- .to match(/remote\: GitLab\: You are not allowed to push code to protected branches on this project/)
- expect(repository.push_error)
- .to match(/\[remote rejected\] #{branch_name} -> #{branch_name} \(pre-receive hook declined\)/)
+ def log_push_output
+ if defined?(@push)
+ filename = File.join('tmp', "push-output-#{project.name}")
+ puts "Exception detected. Push output will be saved to #{filename}"
+ IO.binwrite(filename, @push.output)
end
end
end
diff --git a/qa/qa/specs/features/repository/push_spec.rb b/qa/qa/specs/features/repository/push_spec.rb
index 51d9c2c7fd2..fc40b60d915 100644
--- a/qa/qa/specs/features/repository/push_spec.rb
+++ b/qa/qa/specs/features/repository/push_spec.rb
@@ -1,11 +1,11 @@
module QA
- feature 'push code to repository', :core do
+ describe 'push code to repository', :core do
context 'with regular account over http' do
- scenario 'user pushes code to the repository' do
+ it 'user pushes code to the repository' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- Factory::Repository::Push.fabricate! do |push|
+ Factory::Repository::ProjectPush.fabricate! do |push|
push.file_name = 'README.md'
push.file_content = '# This is a test project'
push.commit_message = 'Add README.md'
diff --git a/qa/spec/git/repository_spec.rb b/qa/spec/git/repository_spec.rb
new file mode 100644
index 00000000000..ee1f08da238
--- /dev/null
+++ b/qa/spec/git/repository_spec.rb
@@ -0,0 +1,40 @@
+describe QA::Git::Repository do
+ let(:repository) { described_class.new }
+
+ before do
+ cd_empty_temp_directory
+ set_bad_uri
+ repository.use_default_credentials
+ end
+
+ describe '#clone' do
+ it 'redacts credentials from the URI in output' do
+ output, _ = repository.clone
+
+ expect(output).to include("fatal: unable to access 'http://****@foo/bar.git/'")
+ end
+ end
+
+ describe '#push_changes' do
+ before do
+ `git init` # need a repo to push from
+ end
+
+ it 'redacts credentials from the URI in output' do
+ output, _ = repository.push_changes
+
+ expect(output).to include("error: failed to push some refs to 'http://****@foo/bar.git'")
+ end
+ end
+
+ def cd_empty_temp_directory
+ tmp_dir = 'tmp/git-repository-spec/'
+ FileUtils.rm_r(tmp_dir) if File.exist?(tmp_dir)
+ FileUtils.mkdir_p tmp_dir
+ FileUtils.cd tmp_dir
+ end
+
+ def set_bad_uri
+ repository.uri = 'http://foo/bar.git'
+ end
+end
diff --git a/qa/spec/runtime/api_client_spec.rb b/qa/spec/runtime/api/client_spec.rb
index d497d8839b8..d497d8839b8 100644
--- a/qa/spec/runtime/api_client_spec.rb
+++ b/qa/spec/runtime/api/client_spec.rb
diff --git a/qa/spec/runtime/api/request_spec.rb b/qa/spec/runtime/api/request_spec.rb
new file mode 100644
index 00000000000..80e3149f32d
--- /dev/null
+++ b/qa/spec/runtime/api/request_spec.rb
@@ -0,0 +1,44 @@
+describe QA::Runtime::API::Request do
+ include Support::StubENV
+
+ before do
+ stub_env('PERSONAL_ACCESS_TOKEN', 'a_token')
+ end
+
+ let(:client) { QA::Runtime::API::Client.new('http://example.com') }
+ let(:request) { described_class.new(client, '/users') }
+
+ describe '#url' do
+ it 'returns the full api request url' do
+ expect(request.url).to eq 'http://example.com/api/v4/users?private_token=a_token'
+ end
+ end
+
+ describe '#request_path' do
+ it 'prepends the api path' do
+ expect(request.request_path('/users')).to eq '/api/v4/users'
+ end
+
+ it 'adds the personal access token' do
+ expect(request.request_path('/users', private_token: 'token'))
+ .to eq '/api/v4/users?private_token=token'
+ end
+
+ it 'adds the oauth access token' do
+ expect(request.request_path('/users', access_token: 'otoken'))
+ .to eq '/api/v4/users?access_token=otoken'
+ end
+
+ it 'respects query parameters' do
+ expect(request.request_path('/users?page=1')).to eq '/api/v4/users?page=1'
+ expect(request.request_path('/users', private_token: 'token', foo: 'bar/baz'))
+ .to eq '/api/v4/users?private_token=token&foo=bar%2Fbaz'
+ expect(request.request_path('/users?page=1', private_token: 'token', foo: 'bar/baz'))
+ .to eq '/api/v4/users?page=1&private_token=token&foo=bar%2Fbaz'
+ end
+
+ it 'uses a different api version' do
+ expect(request.request_path('/users', version: 'other_version')).to eq '/api/other_version/users'
+ end
+ end
+end
diff --git a/qa/spec/runtime/api_request_spec.rb b/qa/spec/runtime/api_request_spec.rb
index 9a1ed8a7a46..e69de29bb2d 100644
--- a/qa/spec/runtime/api_request_spec.rb
+++ b/qa/spec/runtime/api_request_spec.rb
@@ -1,42 +0,0 @@
-describe QA::Runtime::API::Request do
- include Support::StubENV
-
- before do
- stub_env('PERSONAL_ACCESS_TOKEN', 'a_token')
- end
-
- let(:client) { QA::Runtime::API::Client.new('http://example.com') }
- let(:request) { described_class.new(client, '/users') }
-
- describe '#url' do
- it 'returns the full api request url' do
- expect(request.url).to eq 'http://example.com/api/v4/users?private_token=a_token'
- end
- end
-
- describe '#request_path' do
- it 'prepends the api path' do
- expect(request.request_path('/users')).to eq '/api/v4/users'
- end
-
- it 'adds the personal access token' do
- expect(request.request_path('/users', personal_access_token: 'token'))
- .to eq '/api/v4/users?private_token=token'
- end
-
- it 'adds the oauth access token' do
- expect(request.request_path('/users', oauth_access_token: 'otoken'))
- .to eq '/api/v4/users?access_token=otoken'
- end
-
- it 'respects query parameters' do
- expect(request.request_path('/users?page=1')).to eq '/api/v4/users?page=1'
- expect(request.request_path('/users?page=1', personal_access_token: 'token'))
- .to eq '/api/v4/users?page=1&private_token=token'
- end
-
- it 'uses a different api version' do
- expect(request.request_path('/users', version: 'v3')).to eq '/api/v3/users'
- end
- end
-end
diff --git a/qa/spec/runtime/env_spec.rb b/qa/spec/runtime/env_spec.rb
index 2b6365dbc41..851026c71f0 100644
--- a/qa/spec/runtime/env_spec.rb
+++ b/qa/spec/runtime/env_spec.rb
@@ -76,4 +76,27 @@ describe QA::Runtime::Env do
expect { described_class.user_type }.to raise_error(ArgumentError)
end
end
+
+ describe '.github_access_token' do
+ it 'returns "" if GITHUB_ACCESS_TOKEN is not defined' do
+ expect(described_class.github_access_token).to eq('')
+ end
+
+ it 'returns stripped string if GITHUB_ACCESS_TOKEN is defined' do
+ stub_env('GITHUB_ACCESS_TOKEN', ' abc123 ')
+ expect(described_class.github_access_token).to eq('abc123')
+ end
+ end
+
+ describe '.require_github_access_token!' do
+ it 'raises ArgumentError if GITHUB_ACCESS_TOKEN is not defined' do
+ expect { described_class.require_github_access_token! }.to raise_error(ArgumentError)
+ end
+
+ it 'does not raise if GITHUB_ACCESS_TOKEN is defined' do
+ stub_env('GITHUB_ACCESS_TOKEN', ' abc123 ')
+
+ expect { described_class.require_github_access_token! }.not_to raise_error
+ end
+ end
end
diff --git a/qa/spec/support/stub_env.rb b/qa/spec/support/stub_env.rb
index bc8f3a5e22e..044804cd599 100644
--- a/qa/spec/support/stub_env.rb
+++ b/qa/spec/support/stub_env.rb
@@ -32,6 +32,8 @@ module Support
allow(ENV).to receive(:[]).and_call_original
allow(ENV).to receive(:key?).and_call_original
allow(ENV).to receive(:fetch).and_call_original
+ # Prevent secrets from leaking in CI
+ allow(ENV).to receive(:inspect).and_return([])
add_stubbed_value(STUBBED_KEY, true)
end
end
diff --git a/rubocop/cop/gitlab/finder_with_find_by.rb b/rubocop/cop/gitlab/finder_with_find_by.rb
new file mode 100644
index 00000000000..f45a37ddc06
--- /dev/null
+++ b/rubocop/cop/gitlab/finder_with_find_by.rb
@@ -0,0 +1,52 @@
+module RuboCop
+ module Cop
+ module Gitlab
+ class FinderWithFindBy < RuboCop::Cop::Cop
+ FIND_PATTERN = /\Afind(_by\!?)?\z/
+ ALLOWED_MODULES = ['FinderMethods'].freeze
+
+ def message(used_method)
+ <<~MSG
+ Don't chain finders `#execute` method with `##{used_method}`.
+ Instead include `FinderMethods` in the Finder and call `##{used_method}`
+ directly on the finder instance.
+
+ This will make sure all authorization checks are performed on the resource.
+ MSG
+ end
+
+ def on_send(node)
+ if find_on_execute?(node) && !allowed_module?(node)
+ add_offense(node, location: :selector, message: message(node.method_name))
+ end
+ end
+
+ def autocorrect(node)
+ lambda do |corrector|
+ upto_including_execute = node.descendants.first.source_range
+ before_execute = node.descendants[1].source_range
+ range_to_remove = node.source_range
+ .with(begin_pos: before_execute.end_pos,
+ end_pos: upto_including_execute.end_pos)
+
+ corrector.remove(range_to_remove)
+ end
+ end
+
+ def find_on_execute?(node)
+ chained_on_node = node.descendants.first
+ node.method_name.to_s =~ FIND_PATTERN &&
+ chained_on_node&.method_name == :execute
+ end
+
+ def allowed_module?(node)
+ ALLOWED_MODULES.include?(node.parent_module_name)
+ end
+
+ def method_name_for_node(node)
+ children[1].to_s
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/cop/line_break_around_conditional_block.rb b/rubocop/cop/line_break_around_conditional_block.rb
index 3e7021e724e..011f2bcf8bf 100644
--- a/rubocop/cop/line_break_around_conditional_block.rb
+++ b/rubocop/cop/line_break_around_conditional_block.rb
@@ -43,6 +43,8 @@ module RuboCop
# end
# end
class LineBreakAroundConditionalBlock < RuboCop::Cop::Cop
+ include RangeHelp
+
MSG = 'Add a line break around conditional blocks'
def on_if(node)
@@ -95,7 +97,7 @@ module RuboCop
end
def end_clause_line?(line)
- line =~ /^\s*(rescue|else|elsif|when)/
+ line =~ /^\s*(#|rescue|else|elsif|when)/
end
def begin_line?(line)
diff --git a/rubocop/cop/migration/update_large_table.rb b/rubocop/cop/migration/update_large_table.rb
index bb14d0f4f56..c15eec22d04 100644
--- a/rubocop/cop/migration/update_large_table.rb
+++ b/rubocop/cop/migration/update_large_table.rb
@@ -20,10 +20,14 @@ module RuboCop
'necessary'.freeze
LARGE_TABLES = %i[
- ci_pipelines
+ ci_build_trace_sections
ci_builds
+ ci_job_artifacts
+ ci_pipelines
+ ci_stages
events
issues
+ merge_request_diff_commits
merge_request_diff_files
merge_request_diffs
merge_requests
@@ -34,8 +38,15 @@ module RuboCop
users
].freeze
+ BATCH_UPDATE_METHODS = %w[
+ :add_column_with_default
+ :change_column_type_concurrently
+ :rename_column_concurrently
+ :update_column_in_batches
+ ].join(' ').freeze
+
def_node_matcher :batch_update?, <<~PATTERN
- (send nil? ${:add_column_with_default :update_column_in_batches} $(sym ...) ...)
+ (send nil? ${#{BATCH_UPDATE_METHODS}} $(sym ...) ...)
PATTERN
def on_send(node)
diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb
index f05990232ab..aa7ae601f75 100644
--- a/rubocop/rubocop.rb
+++ b/rubocop/rubocop.rb
@@ -2,6 +2,7 @@
require_relative 'cop/gitlab/module_with_instance_variables'
require_relative 'cop/gitlab/predicate_memoization'
require_relative 'cop/gitlab/httparty'
+require_relative 'cop/gitlab/finder_with_find_by'
require_relative 'cop/include_sidekiq_worker'
require_relative 'cop/avoid_return_from_blocks'
require_relative 'cop/avoid_break_from_strong_memoize'
diff --git a/scripts/frontend/postinstall.js b/scripts/frontend/postinstall.js
new file mode 100644
index 00000000000..682039a41b3
--- /dev/null
+++ b/scripts/frontend/postinstall.js
@@ -0,0 +1,22 @@
+const chalk = require('chalk');
+
+// check that fsevents is available if we're on macOS
+if (process.platform === 'darwin') {
+ try {
+ require.resolve('fsevents');
+ } catch (e) {
+ console.error(`${chalk.red('error')} Dependency postinstall check failed.`);
+ console.error(
+ chalk.red(`
+ The fsevents driver is not installed properly.
+ If you are running a new version of Node, please
+ ensure that it is supported by the fsevents library.
+
+ You can try installing again with \`${chalk.cyan('yarn install --force')}\`
+ `)
+ );
+ process.exit(1);
+ }
+}
+
+console.log(`${chalk.green('success')} Dependency postinstall check passed.`);
diff --git a/scripts/frontend/prettier.js b/scripts/frontend/prettier.js
index 39de77bc333..b66ba885701 100644
--- a/scripts/frontend/prettier.js
+++ b/scripts/frontend/prettier.js
@@ -9,6 +9,8 @@ const getStagedFiles = require('./frontend_script_utils').getStagedFiles;
const mode = process.argv[2] || 'check';
const shouldSave = mode === 'save' || mode === 'save-all';
const allFiles = mode === 'check-all' || mode === 'save-all';
+let dirPath = process.argv[3] || '';
+if (dirPath && dirPath.charAt(dirPath.length - 1) !== '/') dirPath += '/';
const config = {
patterns: ['**/*.js', '**/*.vue', '**/*.scss'],
@@ -39,14 +41,15 @@ prettierIgnore.add(
const availableExtensions = Object.keys(config.parsers);
-console.log(`Loading ${allFiles ? 'All' : 'Staged'} Files ...`);
+console.log(`Loading ${allFiles ? 'All' : 'Selected'} Files ...`);
-const stagedFiles = allFiles ? null : getStagedFiles(availableExtensions.map(ext => `*.${ext}`));
+const stagedFiles =
+ allFiles || dirPath ? null : getStagedFiles(availableExtensions.map(ext => `*.${ext}`));
if (stagedFiles) {
if (!stagedFiles.length || (stagedFiles.length === 1 && !stagedFiles[0])) {
console.log('No matching staged files.');
- return;
+ process.exit(1);
}
console.log(`Matching staged Files : ${stagedFiles.length}`);
}
@@ -60,6 +63,13 @@ if (allFiles) {
const patterns = config.patterns;
const globPattern = patterns.length > 1 ? `{${patterns.join(',')}}` : `${patterns.join(',')}`;
files = glob.sync(globPattern, { ignore }).filter(f => allFiles || stagedFiles.includes(f));
+} else if (dirPath) {
+ const ignore = config.ignore;
+ const patterns = config.patterns.map(item => {
+ return dirPath + item;
+ });
+ const globPattern = patterns.length > 1 ? `{${patterns.join(',')}}` : `${patterns.join(',')}`;
+ files = glob.sync(globPattern, { ignore });
} else {
files = stagedFiles.filter(f => availableExtensions.includes(f.split('.').pop()));
}
@@ -68,17 +78,16 @@ files = prettierIgnore.filter(files);
if (!files.length) {
console.log('No Files found to process with Prettier');
- return;
+ process.exit(1);
}
console.log(`${shouldSave ? 'Updating' : 'Checking'} ${files.length} file(s)`);
-prettier
- .resolveConfig('.')
- .then(options => {
- console.log('Found options : ', options);
- files.forEach(file => {
- try {
+files.forEach(file => {
+ try {
+ prettier
+ .resolveConfig(file)
+ .then(options => {
const fileExtension = file.split('.').pop();
Object.assign(options, {
parser: config.parsers[fileExtension],
@@ -101,17 +110,17 @@ prettier
}
console.log(`Prettify Manually : ${file}`);
}
- } catch (error) {
- didError = true;
- console.log(`\n\nError with ${file}: ${error.message}`);
- }
- });
-
- if (didWarn || didError) {
- process.exit(1);
- }
- })
- .catch(e => {
- console.log(`Error on loading the Config File: ${e.message}`);
- process.exit(1);
- });
+ })
+ .catch(e => {
+ console.log(`Error on loading the Config File: ${e.message}`);
+ process.exit(1);
+ });
+ } catch (error) {
+ didError = true;
+ console.log(`\n\nError with ${file}: ${error.message}`);
+ }
+});
+
+if (didWarn || didError) {
+ process.exit(1);
+}
diff --git a/scripts/prune-old-flaky-specs b/scripts/prune-old-flaky-specs
index f7451fbd428..a00a334fd6e 100755
--- a/scripts/prune-old-flaky-specs
+++ b/scripts/prune-old-flaky-specs
@@ -5,7 +5,11 @@
# gem manually on the CI
require 'rubygems'
-require_relative '../lib/rspec_flaky/report'
+# In newer Ruby, alias_method is not private then we don't need __send__
+singleton_class.__send__(:alias_method, :require_dependency, :require) # rubocop:disable GitlabSecurity/PublicSend
+$:.unshift(File.expand_path('../lib', __dir__))
+
+require 'rspec_flaky/report'
report_file = ARGV.shift
unless report_file
diff --git a/scripts/rails5-gemfile-lock-check b/scripts/rails5-gemfile-lock-check
new file mode 100755
index 00000000000..da6f1b7145e
--- /dev/null
+++ b/scripts/rails5-gemfile-lock-check
@@ -0,0 +1,19 @@
+#!/usr/bin/env bash
+
+echo -e "=> Checking if Gemfile.rails5.lock is up-to-date...\\n"
+
+cp Gemfile.rails5.lock Gemfile.rails5.lock.orig
+BUNDLE_GEMFILE=Gemfile.rails5 bundle install "$BUNDLE_INSTALL_FLAGS"
+diff -u Gemfile.rails5.lock.orig Gemfile.rails5.lock >/dev/null 2>&1
+
+if [ $? == 1 ]
+then
+ diff -u Gemfile.rails5.lock.orig Gemfile.rails5.lock
+
+ echo -e "\\n✖ ERROR: Gemfile.rails5.lock is not up-to-date!
+ Please run 'BUNDLE_GEMFILE=Gemfile.rails5 bundle install'\\n" >&2
+ exit 1
+fi
+
+echo "✔ Gemfile.rails5.lock is up-to-date"
+exit 0
diff --git a/scripts/trigger-build b/scripts/trigger-build
new file mode 100755
index 00000000000..798bf1e82b7
--- /dev/null
+++ b/scripts/trigger-build
@@ -0,0 +1,185 @@
+#!/usr/bin/env ruby
+
+require 'net/http'
+require 'json'
+require 'cgi'
+
+module Trigger
+ OMNIBUS_PROJECT_PATH = 'gitlab-org/omnibus-gitlab'.freeze
+ CNG_PROJECT_PATH = 'gitlab-org/build/CNG'.freeze
+ TOKEN = ENV['BUILD_TRIGGER_TOKEN']
+
+ def self.ee?
+ ENV['CI_PROJECT_NAME'] == 'gitlab-ee' || File.exist?('CHANGELOG-EE.md')
+ end
+
+ class Omnibus
+ def initialize
+ @uri = URI("https://gitlab.com/api/v4/projects/#{CGI.escape(Trigger::OMNIBUS_PROJECT_PATH)}/trigger/pipeline")
+ @params = env_params.merge(file_params).merge(token: Trigger::TOKEN)
+ end
+
+ def invoke!
+ res = Net::HTTP.post_form(@uri, @params)
+ id = JSON.parse(res.body)['id']
+ project = Trigger::OMNIBUS_PROJECT_PATH
+
+ if id
+ puts "Triggered https://gitlab.com/#{project}/pipelines/#{id}"
+ puts "Waiting for downstream pipeline status"
+ else
+ raise "Trigger failed! The response from the trigger is: #{res.body}"
+ end
+
+ Trigger::Pipeline.new(project, id)
+ end
+
+ private
+
+ def env_params
+ {
+ "ref" => ENV["OMNIBUS_BRANCH"] || "master",
+ "variables[GITLAB_VERSION]" => ENV["CI_COMMIT_SHA"],
+ "variables[ALTERNATIVE_SOURCES]" => true,
+ "variables[ee]" => Trigger.ee? ? 'true' : 'false',
+ "variables[TRIGGERED_USER]" => ENV["GITLAB_USER_NAME"],
+ "variables[TRIGGER_SOURCE]" => "https://gitlab.com/gitlab-org/#{ENV['CI_PROJECT_NAME']}/-/jobs/#{ENV['CI_JOB_ID']}"
+ }
+ 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
+
+ class CNG
+ def initialize
+ @uri = URI("https://gitlab.com/api/v4/projects/#{CGI.escape(Trigger::CNG_PROJECT_PATH)}/trigger/pipeline")
+ @ref_name = ENV['CI_COMMIT_REF_NAME']
+ @username = ENV['GITLAB_USER_NAME']
+ @project_name = ENV['CI_PROJECT_NAME']
+ @job_id = ENV['CI_JOB_ID']
+ @params = env_params.merge(file_params).merge(token: Trigger::TOKEN)
+ end
+
+ #
+ # Trigger a pipeline
+ #
+ def invoke!
+ res = Net::HTTP.post_form(@uri, @params)
+ id = JSON.parse(res.body)['id']
+ project = Trigger::CNG_PROJECT_PATH
+
+ if id
+ puts "Triggered https://gitlab.com/#{project}/pipelines/#{id}"
+ puts "Waiting for downstream pipeline status"
+ else
+ raise "Trigger failed! The response from the trigger is: #{res.body}"
+ end
+
+ Trigger::Pipeline.new(project, id)
+ end
+
+ private
+
+ def env_params
+ params = {
+ "ref" => ENV["CNG_BRANCH"] || "master",
+ "variables[TRIGGERED_USER]" => @username,
+ "variables[TRIGGER_SOURCE]" => "https://gitlab.com/gitlab-org/#{@project_name}/-/jobs/#{@job_id}"
+ }
+
+ if Trigger.ee?
+ params["variables[GITLAB_EE_VERSION]"] = @ref_name
+ params["variables[EE_PIPELINE]"] = 'true'
+ else
+ params["variables[GITLAB_CE_VERSION]"] = @ref_name
+ params["variables[CE_PIPELINE]"] = 'true'
+ end
+
+ params
+ end
+
+ # Read version files from all components
+ def file_params
+ Dir.glob("*_VERSION").each_with_object({}) do |version_file, params|
+ raw_version = File.read(version_file).strip
+ # if the version matches semver format, treat it as a tag and prepend `v`
+ version = if raw_version =~ Regexp.compile(/^\d+\.\d+\.\d+(-rc\d+)?(-ee)?$/)
+ "v#{raw_version}"
+ else
+ raw_version
+ end
+
+ params["variables[#{version_file}]"] = version
+ end
+ end
+ end
+
+ class Pipeline
+ INTERVAL = 60 # seconds
+ MAX_DURATION = 3600 * 3 # 3 hours
+
+ def initialize(project, id)
+ @start = Time.now.to_i
+ @uri = URI("https://gitlab.com/api/v4/projects/#{CGI.escape(project)}/pipelines/#{id}")
+ end
+
+ def wait!
+ loop do
+ raise "Pipeline timed out after waiting for #{duration} minutes!" if timeout?
+
+ case status
+ when :created, :pending, :running
+ print "."
+ sleep INTERVAL
+ when :success
+ puts "Pipeline succeeded in #{duration} minutes!"
+ break
+ else
+ raise "Pipeline did not succeed!"
+ end
+
+ STDOUT.flush
+ end
+ end
+
+ def timeout?
+ Time.now.to_i > (@start + MAX_DURATION)
+ end
+
+ def duration
+ (Time.now.to_i - @start) / 60
+ 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
+ rescue JSON::ParserError
+ # Ignore GitLab API hiccups. If GitLab is really down, we'll hit the job
+ # timeout anyway.
+ :running
+ end
+ end
+end
+
+case ARGV[0]
+when 'omnibus'
+ Trigger::Omnibus.new.invoke!.wait!
+when 'cng'
+ Trigger::CNG.new.invoke!.wait!
+else
+ puts "Please provide a valid option:
+ omnibus - Triggers a pipeline that builds the omnibus-gitlab package
+ cng - Triggers a pipeline that builds images used by the GitLab helm chart"
+end
diff --git a/scripts/trigger-build-cloud-native b/scripts/trigger-build-cloud-native
deleted file mode 100755
index b6ca75a588d..00000000000
--- a/scripts/trigger-build-cloud-native
+++ /dev/null
@@ -1,61 +0,0 @@
-#!/usr/bin/env ruby
-
-require 'gitlab'
-
-#
-# Configure credentials to be used with gitlab gem
-#
-Gitlab.configure do |config|
- config.endpoint = 'https://gitlab.com/api/v4'
-end
-
-#
-# The remote project
-#
-GITLAB_CNG_REPO = 'gitlab-org/build/CNG'.freeze
-
-def ee?
- ENV['CI_PROJECT_NAME'] == 'gitlab-ee' || File.exist?('CHANGELOG-EE.md')
-end
-
-def read_file_version(filename)
- raw_version = File.read(filename).strip
-
- # if the version matches semver format, treat it as a tag and prepend `v`
- if raw_version =~ Regexp.compile(/^\d+\.\d+\.\d+(-rc\d+)?(-ee)?$/)
- "v#{raw_version}"
- else
- raw_version
- end
-end
-
-def params
- params = {
- 'GITLAB_SHELL_VERSION' => read_file_version('GITLAB_SHELL_VERSION'),
- 'GITALY_VERSION' => read_file_version('GITALY_SERVER_VERSION'),
- 'TRIGGERED_USER' => ENV['GITLAB_USER_NAME'],
- 'TRIGGER_SOURCE' => "https://gitlab.com/gitlab-org/#{ENV['CI_PROJECT_NAME']}/-/jobs/#{ENV['CI_JOB_ID']}"
- }
-
- if ee?
- params['EE_PIPELINE'] = 'true'
- params['GITLAB_EE_VERSION'] = ENV['CI_COMMIT_REF_NAME']
- else
- params['CE_PIPELINE'] = 'true'
- params['GITLAB_CE_VERSION'] = ENV['CI_COMMIT_REF_NAME']
- end
-
- params
-end
-
-#
-# Trigger a pipeline
-#
-def trigger_pipeline
- # Create the cross project pipeline using CI_JOB_TOKEN
- pipeline = Gitlab.run_trigger(GITLAB_CNG_REPO, ENV['CI_JOB_TOKEN'], 'master', params)
-
- puts "Triggered https://gitlab.com/#{GITLAB_CNG_REPO}/pipelines/#{pipeline.id}"
-end
-
-trigger_pipeline
diff --git a/scripts/trigger-build-docs b/scripts/trigger-build-docs
index c9aaba91aa0..9ee35684509 100755
--- a/scripts/trigger-build-docs
+++ b/scripts/trigger-build-docs
@@ -16,18 +16,14 @@ end
GITLAB_DOCS_REPO = 'gitlab-com/gitlab-docs'.freeze
#
-# Truncate the remote docs branch name if it's more than 63 characters
-# otherwise we hit the filesystem limit and the directory name where
-# NGINX serves the site won't match the branch name.
+# Truncate the remote docs branch name otherwise we hit the filesystem
+# limit and the directory name where NGINX serves the site won't match
+# the branch name.
#
def docs_branch
# The maximum string length a file can have on a filesystem (ext4)
- # is 63 characters. Let's use something smaller to be 100% sure.
- max = 42
- # Prefix the remote branch with the slug of the project in order
- # to avoid name conflicts in the rare case the branch name already
- # exists in the docs repo and truncate to max length.
- "#{slug}-#{ENV["CI_COMMIT_REF_SLUG"]}"[0...max]
+ # is 63 characters. CI_ENVIRONMENT_SLUG is limited to 24 characters.
+ ENV["CI_ENVIRONMENT_SLUG"]
end
#
diff --git a/scripts/trigger-build-omnibus b/scripts/trigger-build-omnibus
deleted file mode 100755
index 95f35b44f5a..00000000000
--- a/scripts/trigger-build-omnibus
+++ /dev/null
@@ -1,108 +0,0 @@
-#!/usr/bin/env ruby
-
-require 'net/http'
-require 'json'
-require 'cgi'
-
-module Omnibus
- PROJECT_PATH = 'gitlab-org/omnibus-gitlab'.freeze
-
- class Trigger
- TOKEN = ENV['BUILD_TRIGGER_TOKEN']
- TRIGGERER = ENV['CI_PROJECT_NAME']
-
- 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}"
- puts "Waiting for downstream pipeline status"
- else
- raise "Trigger failed! The response from the trigger is: #{res.body}"
- end
-
- Omnibus::Pipeline.new(id)
- end
-
- private
-
- def ee?
- TRIGGERER == 'gitlab-ee' || File.exist?('CHANGELOG-EE.md')
- end
-
- def env_params
- {
- "ref" => ENV["OMNIBUS_BRANCH"] || "master",
- "variables[GITLAB_VERSION]" => ENV["CI_COMMIT_SHA"],
- "variables[ALTERNATIVE_SOURCES]" => true,
- "variables[ee]" => ee? ? 'true' : 'false',
- "variables[TRIGGERED_USER]" => ENV["GITLAB_USER_NAME"],
- "variables[TRIGGER_SOURCE]" => "https://gitlab.com/gitlab-org/#{ENV['CI_PROJECT_NAME']}/-/jobs/#{ENV['CI_JOB_ID']}"
- }
- 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
-
- class Pipeline
- INTERVAL = 60 # seconds
- MAX_DURATION = 3600 * 3 # 3 hours
-
- 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 timed out after waiting for #{duration} minutes!" if timeout?
-
- case status
- when :created, :pending, :running
- print "."
- sleep INTERVAL
- when :success
- puts "Omnibus pipeline succeeded in #{duration} minutes!"
- break
- else
- raise "Omnibus pipeline did not succeed!"
- end
-
- STDOUT.flush
- end
- end
-
- def timeout?
- Time.now.to_i > (@start + MAX_DURATION)
- end
-
- def duration
- (Time.now.to_i - @start) / 60
- 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 fc1bf67d7b9..9dc4edf97d1 100644
--- a/spec/bin/changelog_spec.rb
+++ b/spec/bin/changelog_spec.rb
@@ -3,6 +3,20 @@ require 'spec_helper'
load File.expand_path('../../bin/changelog', __dir__)
describe 'bin/changelog' do
+ let(:options) { OpenStruct.new(title: 'Test title', type: 'fixed', dry_run: true) }
+
+ describe ChangelogEntry do
+ it 'truncates the file path' do
+ entry = described_class.new(options)
+
+ allow(entry).to receive(:ee?).and_return(false)
+ allow(entry).to receive(:branch_name).and_return('long-branch-' * 100)
+
+ file_path = entry.send(:file_path)
+ expect(file_path.length).to eq(140)
+ end
+ end
+
describe ChangelogOptionParser do
describe '.parse' do
it 'parses --amend' do
@@ -56,11 +70,11 @@ describe 'bin/changelog' do
it 'parses -h' do
expect do
expect { described_class.parse(%w[foo -h bar]) }.to output.to_stdout
- end.to raise_error(SystemExit)
+ end.to raise_error(ChangelogHelpers::Done)
end
it 'assigns title' do
- options = described_class.parse(%W[foo -m 1 bar\n -u baz\r\n --amend])
+ options = described_class.parse(%W[foo -m 1 bar\n baz\r\n --amend])
expect(options.title).to eq 'foo bar baz'
end
@@ -82,9 +96,10 @@ describe 'bin/changelog' do
it 'shows error message and exits the program' do
allow($stdin).to receive(:getc).and_return(type)
expect do
- expect do
- expect { described_class.read_type }.to raise_error(SystemExit)
- end.to output("Invalid category index, please select an index between 1 and 8\n").to_stderr
+ expect { described_class.read_type }.to raise_error(
+ ChangelogHelpers::Abort,
+ 'Invalid category index, please select an index between 1 and 8'
+ )
end.to output.to_stdout
end
end
diff --git a/spec/controllers/admin/application_settings_controller_spec.rb b/spec/controllers/admin/application_settings_controller_spec.rb
index b4fc2aa326f..9d10d725ff3 100644
--- a/spec/controllers/admin/application_settings_controller_spec.rb
+++ b/spec/controllers/admin/application_settings_controller_spec.rb
@@ -73,7 +73,7 @@ describe Admin::ApplicationSettingsController do
end
it 'updates the restricted_visibility_levels when empty array is passed' do
- put :update, application_setting: { restricted_visibility_levels: [] }
+ put :update, application_setting: { restricted_visibility_levels: [""] }
expect(response).to redirect_to(admin_application_settings_path)
expect(ApplicationSetting.current.restricted_visibility_levels).to be_empty
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index f0caac40afd..74f362fd7fc 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -1,3 +1,4 @@
+# coding: utf-8
require 'spec_helper'
describe ApplicationController do
@@ -146,35 +147,43 @@ describe ApplicationController do
end
end
- describe '#authenticate_user_from_rss_token' do
- describe "authenticating a user from an RSS token" do
+ describe '#authenticate_sessionless_user!' do
+ describe 'authenticating a user from a feed token' do
controller(described_class) do
def index
render text: 'authenticated'
end
end
- context "when the 'rss_token' param is populated with the RSS token" do
+ context "when the 'feed_token' param is populated with the feed token" do
context 'when the request format is atom' do
it "logs the user in" do
- get :index, rss_token: user.rss_token, format: :atom
+ get :index, feed_token: user.feed_token, format: :atom
expect(response).to have_gitlab_http_status 200
expect(response.body).to eq 'authenticated'
end
end
- context 'when the request format is not atom' do
+ context 'when the request format is ics' do
+ it "logs the user in" do
+ get :index, feed_token: user.feed_token, format: :ics
+ expect(response).to have_gitlab_http_status 200
+ expect(response.body).to eq 'authenticated'
+ end
+ end
+
+ context 'when the request format is neither atom nor ics' do
it "doesn't log the user in" do
- get :index, rss_token: user.rss_token
+ get :index, feed_token: user.feed_token
expect(response.status).not_to have_gitlab_http_status 200
expect(response.body).not_to eq 'authenticated'
end
end
end
- context "when the 'rss_token' param is populated with an invalid RSS token" do
+ context "when the 'feed_token' param is populated with an invalid feed token" do
it "doesn't log the user" do
- get :index, rss_token: "token"
+ get :index, feed_token: 'token', format: :atom
expect(response.status).not_to eq 200
expect(response.body).not_to eq 'authenticated'
end
@@ -449,24 +458,115 @@ describe ApplicationController do
end
context 'for sessionless users' do
+ render_views
+
before do
sign_out user
end
it 'renders a 403 when the sessionless user did not accept the terms' do
- get :index, rss_token: user.rss_token, format: :atom
+ get :index, feed_token: user.feed_token, format: :atom
expect(response).to have_gitlab_http_status(403)
end
+ it 'renders the error message when the format was html' do
+ get :index,
+ private_token: create(:personal_access_token, user: user).token,
+ format: :html
+
+ expect(response.body).to have_content /accept the terms of service/i
+ end
+
it 'renders a 200 when the sessionless user accepted the terms' do
accept_terms(user)
- get :index, rss_token: user.rss_token, format: :atom
+ get :index, feed_token: user.feed_token, format: :atom
expect(response).to have_gitlab_http_status(200)
end
end
end
end
+
+ describe '#append_info_to_payload' do
+ controller(described_class) do
+ attr_reader :last_payload
+
+ def index
+ render text: 'authenticated'
+ end
+
+ def append_info_to_payload(payload)
+ super
+
+ @last_payload = payload
+ end
+ end
+
+ it 'does not log errors with a 200 response' do
+ get :index
+
+ expect(controller.last_payload.has_key?(:response)).to be_falsey
+ end
+
+ context '422 errors' do
+ it 'logs a response with a string' do
+ response = spy(ActionDispatch::Response, status: 422, body: 'Hello world', content_type: 'application/json', cookies: {})
+ allow(controller).to receive(:response).and_return(response)
+ get :index
+
+ expect(controller.last_payload[:response]).to eq('Hello world')
+ end
+
+ it 'logs a response with an array' do
+ body = ['I want', 'my hat back']
+ response = spy(ActionDispatch::Response, status: 422, body: body, content_type: 'application/json', cookies: {})
+ allow(controller).to receive(:response).and_return(response)
+ get :index
+
+ expect(controller.last_payload[:response]).to eq(body)
+ end
+
+ it 'does not log a string with an empty body' do
+ response = spy(ActionDispatch::Response, status: 422, body: nil, content_type: 'application/json', cookies: {})
+ allow(controller).to receive(:response).and_return(response)
+ get :index
+
+ expect(controller.last_payload.has_key?(:response)).to be_falsey
+ end
+
+ it 'does not log an HTML body' do
+ response = spy(ActionDispatch::Response, status: 422, body: 'This is a test', content_type: 'application/html', cookies: {})
+ allow(controller).to receive(:response).and_return(response)
+ get :index
+
+ expect(controller.last_payload.has_key?(:response)).to be_falsey
+ end
+ end
+ end
+
+ describe '#access_denied' do
+ controller(described_class) do
+ def index
+ access_denied!(params[:message])
+ end
+ end
+
+ before do
+ sign_in user
+ end
+
+ it 'renders a 404 without a message' do
+ get :index
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'renders a 403 when a message is passed to access denied' do
+ get :index, message: 'None shall pass'
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
end
diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb
index fb6d82d7de3..2c59d1929a1 100644
--- a/spec/controllers/autocomplete_controller_spec.rb
+++ b/spec/controllers/autocomplete_controller_spec.rb
@@ -228,12 +228,12 @@ describe AutocompleteController do
before do
sign_in(user)
- project.add_master(user)
+ project.add_maintainer(user)
end
context 'authorized projects' do
before do
- authorized_project.add_master(user)
+ authorized_project.add_maintainer(user)
end
describe 'GET #projects with project ID' do
@@ -253,8 +253,8 @@ describe AutocompleteController do
context 'authorized projects and search' do
before do
- authorized_project.add_master(user)
- authorized_search_project.add_master(user)
+ authorized_project.add_maintainer(user)
+ authorized_search_project.add_maintainer(user)
end
describe 'GET #projects with project ID and search' do
@@ -277,9 +277,9 @@ describe AutocompleteController do
authorized_project2 = create(:project)
authorized_project3 = create(:project)
- authorized_project.add_master(user)
- authorized_project2.add_master(user)
- authorized_project3.add_master(user)
+ authorized_project.add_maintainer(user)
+ authorized_project2.add_maintainer(user)
+ authorized_project3.add_maintainer(user)
stub_const 'MoveToProjectFinder::PAGE_SIZE', 2
end
@@ -301,9 +301,9 @@ describe AutocompleteController do
authorized_project2 = create(:project)
authorized_project3 = create(:project)
- authorized_project.add_master(user)
- authorized_project2.add_master(user)
- authorized_project3.add_master(user)
+ authorized_project.add_maintainer(user)
+ authorized_project2.add_maintainer(user)
+ authorized_project3.add_maintainer(user)
end
describe 'GET #projects with project ID and offset_id' do
diff --git a/spec/controllers/boards/issues_controller_spec.rb b/spec/controllers/boards/issues_controller_spec.rb
index 4770e187db6..ce7762691c9 100644
--- a/spec/controllers/boards/issues_controller_spec.rb
+++ b/spec/controllers/boards/issues_controller_spec.rb
@@ -13,12 +13,12 @@ describe Boards::IssuesController do
let!(:list2) { create(:list, board: board, label: development, position: 1) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
project.add_guest(guest)
end
- describe 'GET index' do
- let(:johndoe) { create(:user, avatar: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png'))) }
+ describe 'GET index', :request_store do
+ let(:johndoe) { create(:user, avatar: fixture_file_upload(File.join('spec/fixtures/dk.png'))) }
context 'with invalid board id' do
it 'returns a not found 404 response' do
diff --git a/spec/controllers/boards/lists_controller_spec.rb b/spec/controllers/boards/lists_controller_spec.rb
index 71d45a22d91..80631d2efb0 100644
--- a/spec/controllers/boards/lists_controller_spec.rb
+++ b/spec/controllers/boards/lists_controller_spec.rb
@@ -7,7 +7,7 @@ describe Boards::ListsController do
let(:guest) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
project.add_guest(guest)
end
@@ -156,12 +156,18 @@ describe Boards::ListsController do
def move(user:, board:, list:, position:)
sign_in(user)
- patch :update, namespace_id: project.namespace.to_param,
- project_id: project,
- board_id: board.to_param,
- id: list.to_param,
- list: { position: position },
- format: :json
+ params = { namespace_id: project.namespace.to_param,
+ project_id: project,
+ board_id: board.to_param,
+ id: list.to_param,
+ list: { position: position },
+ format: :json }
+
+ if Gitlab.rails5?
+ patch :update, params: params, as: :json
+ else
+ patch :update, params
+ end
end
end
diff --git a/spec/controllers/concerns/controller_with_cross_project_access_check_spec.rb b/spec/controllers/concerns/controller_with_cross_project_access_check_spec.rb
index 27f558e1b5d..d20471ef603 100644
--- a/spec/controllers/concerns/controller_with_cross_project_access_check_spec.rb
+++ b/spec/controllers/concerns/controller_with_cross_project_access_check_spec.rb
@@ -43,13 +43,13 @@ describe ControllerWithCrossProjectAccessCheck do
end
end
- it 'renders a 404 with trying to access a cross project page' do
+ it 'renders a 403 with trying to access a cross project page' do
message = "This page is unavailable because you are not allowed to read "\
"information across multiple projects."
get :index
- expect(response).to have_gitlab_http_status(404)
+ expect(response).to have_gitlab_http_status(403)
expect(response.body).to match(/#{message}/)
end
@@ -119,7 +119,7 @@ describe ControllerWithCrossProjectAccessCheck do
get :index
- expect(response).to have_gitlab_http_status(404)
+ expect(response).to have_gitlab_http_status(403)
end
it 'is executed when the `unless` condition returns true' do
@@ -127,19 +127,19 @@ describe ControllerWithCrossProjectAccessCheck do
get :index
- expect(response).to have_gitlab_http_status(404)
+ expect(response).to have_gitlab_http_status(403)
end
it 'does not skip the check on an action that is not skipped' do
get :show, id: 'hello'
- expect(response).to have_gitlab_http_status(404)
+ expect(response).to have_gitlab_http_status(403)
end
it 'does not skip the check on an action that was not defined to skip' do
get :edit, id: 'hello'
- expect(response).to have_gitlab_http_status(404)
+ expect(response).to have_gitlab_http_status(403)
end
end
end
diff --git a/spec/controllers/concerns/group_tree_spec.rb b/spec/controllers/concerns/group_tree_spec.rb
index ba84fbf8564..503eb416962 100644
--- a/spec/controllers/concerns/group_tree_spec.rb
+++ b/spec/controllers/concerns/group_tree_spec.rb
@@ -63,6 +63,17 @@ describe GroupTree do
expect(assigns(:groups)).to contain_exactly(parent, subgroup)
end
+
+ it 'preloads parents regardless of pagination' do
+ allow(Kaminari.config).to receive(:default_per_page).and_return(1)
+ group = create(:group, :public)
+ subgroup = create(:group, :public, parent: group)
+ search_result = create(:group, :public, name: 'result', parent: subgroup)
+
+ get :index, filter: 'resu', format: :json
+
+ expect(assigns(:groups)).to contain_exactly(group, subgroup, search_result)
+ end
end
context 'json content' do
diff --git a/spec/controllers/concerns/internal_redirect_spec.rb b/spec/controllers/concerns/internal_redirect_spec.rb
index a0ee13b2352..7e23b56356e 100644
--- a/spec/controllers/concerns/internal_redirect_spec.rb
+++ b/spec/controllers/concerns/internal_redirect_spec.rb
@@ -54,6 +54,31 @@ describe InternalRedirect do
end
end
+ describe '#sanitize_redirect' do
+ let(:valid_path) { '/hello/world?hello=world' }
+ let(:valid_url) { "http://test.host#{valid_path}" }
+
+ it 'returns `nil` for invalid paths' do
+ invalid_path = '//not/valid'
+
+ expect(controller.sanitize_redirect(invalid_path)).to eq nil
+ end
+
+ it 'returns `nil` for invalid urls' do
+ input = 'http://test.host:3000/invalid'
+
+ expect(controller.sanitize_redirect(input)).to eq nil
+ end
+
+ it 'returns input for valid paths' do
+ expect(controller.sanitize_redirect(valid_path)).to eq valid_path
+ end
+
+ it 'returns path for valid urls' do
+ expect(controller.sanitize_redirect(valid_url)).to eq valid_path
+ end
+ end
+
describe '#host_allowed?' do
it 'allows uris with the same host and port' do
expect(controller.host_allowed?(URI('http://test.host/test'))).to be(true)
diff --git a/spec/controllers/dashboard/groups_controller_spec.rb b/spec/controllers/dashboard/groups_controller_spec.rb
index 7f2eaf95165..9068c1a792e 100644
--- a/spec/controllers/dashboard/groups_controller_spec.rb
+++ b/spec/controllers/dashboard/groups_controller_spec.rb
@@ -28,8 +28,8 @@ describe Dashboard::GroupsController do
let!(:other_group) { create(:group, name: 'other') }
before do
- top_level_result.add_master(user)
- top_level_a.add_master(user)
+ top_level_result.add_maintainer(user)
+ top_level_a.add_maintainer(user)
end
it 'renders only groups the user is a member of when searching hierarchy correctly' do
diff --git a/spec/controllers/dashboard/milestones_controller_spec.rb b/spec/controllers/dashboard/milestones_controller_spec.rb
index 60547db82b6..ba2669a5ea7 100644
--- a/spec/controllers/dashboard/milestones_controller_spec.rb
+++ b/spec/controllers/dashboard/milestones_controller_spec.rb
@@ -17,7 +17,7 @@ describe Dashboard::MilestonesController do
before do
sign_in(user)
- project.add_master(user)
+ project.add_maintainer(user)
end
it_behaves_like 'milestone tabs'
diff --git a/spec/controllers/dashboard_controller_spec.rb b/spec/controllers/dashboard_controller_spec.rb
index 3458d679107..187542ba30c 100644
--- a/spec/controllers/dashboard_controller_spec.rb
+++ b/spec/controllers/dashboard_controller_spec.rb
@@ -5,7 +5,7 @@ describe DashboardController do
let(:project) { create(:project) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb
new file mode 100644
index 00000000000..1449036e148
--- /dev/null
+++ b/spec/controllers/graphql_controller_spec.rb
@@ -0,0 +1,69 @@
+require 'spec_helper'
+
+describe GraphqlController do
+ describe 'execute' do
+ let(:user) { nil }
+
+ before do
+ sign_in(user) if user
+
+ run_test_query!
+ end
+
+ subject { query_response }
+
+ context 'graphql is disabled by feature flag' do
+ let(:user) { nil }
+
+ before do
+ stub_feature_flags(graphql: false)
+ end
+
+ it 'returns 404' do
+ run_test_query!
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'signed out' do
+ let(:user) { nil }
+
+ it 'runs the query with current_user: nil' do
+ is_expected.to eq('echo' => 'nil says: test success')
+ end
+ end
+
+ context 'signed in' do
+ let(:user) { create(:user, username: 'Simon') }
+
+ it 'runs the query with current_user set' do
+ is_expected.to eq('echo' => '"Simon" says: test success')
+ end
+ end
+
+ context 'invalid variables' do
+ it 'returns an error' do
+ run_test_query!(variables: "This is not JSON")
+
+ expect(response).to have_gitlab_http_status(422)
+ expect(json_response['errors'].first['message']).not_to be_nil
+ end
+ end
+ end
+
+ # Chosen to exercise all the moving parts in GraphqlController#execute
+ def run_test_query!(variables: { 'text' => 'test success' })
+ query = <<~QUERY
+ query Echo($text: String) {
+ echo(text: $text)
+ }
+ QUERY
+
+ post :execute, query: query, operationName: 'Echo', variables: variables
+ end
+
+ def query_response
+ json_response['data']
+ end
+end
diff --git a/spec/controllers/groups/avatars_controller_spec.rb b/spec/controllers/groups/avatars_controller_spec.rb
index 506aeee7d2a..7feecd0c380 100644
--- a/spec/controllers/groups/avatars_controller_spec.rb
+++ b/spec/controllers/groups/avatars_controller_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Groups::AvatarsController do
let(:user) { create(:user) }
- let(:group) { create(:group, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
+ let(:group) { create(:group, avatar: fixture_file_upload("spec/fixtures/dk.png", "image/png")) }
before do
group.add_owner(user)
diff --git a/spec/controllers/groups/boards_controller_spec.rb b/spec/controllers/groups/boards_controller_spec.rb
index 0f5bde62006..bf41aa0706f 100644
--- a/spec/controllers/groups/boards_controller_spec.rb
+++ b/spec/controllers/groups/boards_controller_spec.rb
@@ -5,7 +5,7 @@ describe Groups::BoardsController do
let(:user) { create(:user) }
before do
- group.add_master(user)
+ group.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb
index 733386500ca..f7068546093 100644
--- a/spec/controllers/groups/milestones_controller_spec.rb
+++ b/spec/controllers/groups/milestones_controller_spec.rb
@@ -28,7 +28,7 @@ describe Groups::MilestonesController do
before do
sign_in(user)
group.add_owner(user)
- project.add_master(user)
+ project.add_maintainer(user)
end
describe '#index' do
diff --git a/spec/controllers/groups/runners_controller_spec.rb b/spec/controllers/groups/runners_controller_spec.rb
index 6d31b0ce959..598fb84552f 100644
--- a/spec/controllers/groups/runners_controller_spec.rb
+++ b/spec/controllers/groups/runners_controller_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Groups::RunnersController do
let(:user) { create(:user) }
let(:group) { create(:group) }
- let(:runner) { create(:ci_runner) }
+ let(:runner) { create(:ci_runner, :group, groups: [group]) }
let(:params) do
{
@@ -14,8 +14,7 @@ describe Groups::RunnersController do
before do
sign_in(user)
- group.add_master(user)
- group.runners << runner
+ group.add_maintainer(user)
end
describe '#update' do
diff --git a/spec/controllers/groups/settings/ci_cd_controller_spec.rb b/spec/controllers/groups/settings/ci_cd_controller_spec.rb
index e9f0924caba..ea18122e0c3 100644
--- a/spec/controllers/groups/settings/ci_cd_controller_spec.rb
+++ b/spec/controllers/groups/settings/ci_cd_controller_spec.rb
@@ -5,7 +5,7 @@ describe Groups::Settings::CiCdController do
let(:user) { create(:user) }
before do
- group.add_master(user)
+ group.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/controllers/groups/shared_projects_controller_spec.rb b/spec/controllers/groups/shared_projects_controller_spec.rb
new file mode 100644
index 00000000000..003c8c262e7
--- /dev/null
+++ b/spec/controllers/groups/shared_projects_controller_spec.rb
@@ -0,0 +1,68 @@
+require 'spec_helper'
+
+describe Groups::SharedProjectsController do
+ def get_shared_projects(params = {})
+ get :index, params.reverse_merge(format: :json, group_id: group.full_path)
+ end
+
+ def share_project(project)
+ Projects::GroupLinks::CreateService.new(
+ project,
+ user,
+ link_group_access: ProjectGroupLink::DEVELOPER
+ ).execute(group)
+ end
+
+ set(:group) { create(:group) }
+ set(:user) { create(:user) }
+ set(:shared_project) do
+ shared_project = create(:project, namespace: user.namespace)
+ share_project(shared_project)
+
+ shared_project
+ end
+
+ let(:json_project_ids) { json_response.map { |project_info| project_info['id'] } }
+
+ before do
+ sign_in(user)
+ end
+
+ describe 'GET #index' do
+ it 'returns only projects shared with the group' do
+ create(:project, namespace: group)
+
+ get_shared_projects
+
+ expect(json_project_ids).to contain_exactly(shared_project.id)
+ end
+
+ it 'allows filtering shared projects' do
+ project = create(:project, namespace: user.namespace, name: "Searching for")
+ share_project(project)
+
+ get_shared_projects(filter: 'search')
+
+ expect(json_project_ids).to contain_exactly(project.id)
+ end
+
+ it 'allows sorting projects' do
+ shared_project.update!(name: 'bbb')
+ second_project = create(:project, namespace: user.namespace, name: 'aaaa')
+ share_project(second_project)
+
+ get_shared_projects(sort: 'name_asc')
+
+ expect(json_project_ids).to eq([second_project.id, shared_project.id])
+ end
+
+ it 'does not include archived projects' do
+ archived_project = create(:project, :archived, namespace: user.namespace)
+ share_project(archived_project)
+
+ get_shared_projects
+
+ expect(json_project_ids).to contain_exactly(shared_project.id)
+ end
+ end
+end
diff --git a/spec/controllers/groups/uploads_controller_spec.rb b/spec/controllers/groups/uploads_controller_spec.rb
index 6a1869d1a48..5a7281ed704 100644
--- a/spec/controllers/groups/uploads_controller_spec.rb
+++ b/spec/controllers/groups/uploads_controller_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Groups::UploadsController do
+ include WorkhorseHelpers
+
let(:model) { create(:group, :public) }
let(:params) do
{ group_id: model }
@@ -9,4 +11,10 @@ describe Groups::UploadsController do
it_behaves_like 'handle uploads' do
let(:uploader_class) { NamespaceFileUploader }
end
+
+ def post_authorize(verified: true)
+ request.headers.merge!(workhorse_internal_api_request_header) if verified
+
+ post :authorize, group_id: model.full_path, format: :json
+ end
end
diff --git a/spec/controllers/groups/variables_controller_spec.rb b/spec/controllers/groups/variables_controller_spec.rb
index 39a36b92bb4..e5ac5634f95 100644
--- a/spec/controllers/groups/variables_controller_spec.rb
+++ b/spec/controllers/groups/variables_controller_spec.rb
@@ -6,7 +6,7 @@ describe Groups::VariablesController do
before do
sign_in(user)
- group.add_master(user)
+ group.add_maintainer(user)
end
describe 'GET #show' do
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index 8688fb33f0d..7a037828035 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -7,7 +7,7 @@ describe GroupsController do
let(:project) { create(:project, namespace: group) }
let!(:group_member) { create(:group_member, group: group, user: user) }
let!(:owner) { group.add_owner(create(:user)).user }
- let!(:master) { group.add_master(create(:user)).user }
+ let!(:maintainer) { group.add_maintainer(create(:user)).user }
let!(:developer) { group.add_developer(create(:user)).user }
let!(:guest) { group.add_guest(create(:user)).user }
@@ -62,7 +62,7 @@ describe GroupsController do
[true, false].each do |can_create_group_status|
context "and can_create_group is #{can_create_group_status}" do
before do
- User.where(id: [admin, owner, master, developer, guest]).update_all(can_create_group: can_create_group_status)
+ User.where(id: [admin, owner, maintainer, developer, guest]).update_all(can_create_group: can_create_group_status)
end
[:admin, :owner].each do |member_type|
@@ -73,7 +73,7 @@ describe GroupsController do
end
end
- [:guest, :developer, :master].each do |member_type|
+ [:guest, :developer, :maintainer].each do |member_type|
context "and logged in as #{member_type.capitalize}" do
it_behaves_like 'member without ability to create subgroups' do
let(:member) { send(member_type) }
diff --git a/spec/controllers/health_controller_spec.rb b/spec/controllers/health_controller_spec.rb
index 542eddc2d16..d800ad7c187 100644
--- a/spec/controllers/health_controller_spec.rb
+++ b/spec/controllers/health_controller_spec.rb
@@ -69,8 +69,7 @@ describe HealthController do
expect(json_response['cache_check']['status']).to eq('ok')
expect(json_response['queues_check']['status']).to eq('ok')
expect(json_response['shared_state_check']['status']).to eq('ok')
- expect(json_response['fs_shards_check']['status']).to eq('ok')
- expect(json_response['fs_shards_check']['labels']['shard']).to eq('default')
+ expect(json_response['gitaly_check']['status']).to eq('ok')
end
end
@@ -122,7 +121,6 @@ describe HealthController do
expect(json_response['cache_check']['status']).to eq('ok')
expect(json_response['queues_check']['status']).to eq('ok')
expect(json_response['shared_state_check']['status']).to eq('ok')
- expect(json_response['fs_shards_check']['status']).to eq('ok')
end
end
diff --git a/spec/controllers/import/gitlab_projects_controller_spec.rb b/spec/controllers/import/gitlab_projects_controller_spec.rb
index 8759d3c0b97..d624659bce9 100644
--- a/spec/controllers/import/gitlab_projects_controller_spec.rb
+++ b/spec/controllers/import/gitlab_projects_controller_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Import::GitlabProjectsController do
set(:namespace) { create(:namespace) }
set(:user) { namespace.owner }
- let(:file) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') }
+ let(:file) { fixture_file_upload('spec/fixtures/doc_sample.txt', 'text/plain') }
before do
sign_in(user)
diff --git a/spec/controllers/import/google_code_controller_spec.rb b/spec/controllers/import/google_code_controller_spec.rb
index 4241db6e771..0763492d88a 100644
--- a/spec/controllers/import/google_code_controller_spec.rb
+++ b/spec/controllers/import/google_code_controller_spec.rb
@@ -4,7 +4,7 @@ describe Import::GoogleCodeController do
include ImportSpecHelper
let(:user) { create(:user) }
- let(:dump_file) { fixture_file_upload(Rails.root + 'spec/fixtures/GoogleCodeProjectHosting.json', 'application/json') }
+ let(:dump_file) { fixture_file_upload('spec/fixtures/GoogleCodeProjectHosting.json', 'application/json') }
before do
sign_in(user)
diff --git a/spec/controllers/metrics_controller_spec.rb b/spec/controllers/metrics_controller_spec.rb
index 9e8a37171ec..7376841fac8 100644
--- a/spec/controllers/metrics_controller_spec.rb
+++ b/spec/controllers/metrics_controller_spec.rb
@@ -59,6 +59,13 @@ describe MetricsController do
expect(response.body).to match(/^redis_shared_state_ping_latency_seconds [0-9\.]+$/)
end
+ it 'returns Gitaly metrics' do
+ get :index
+
+ expect(response.body).to match(/^gitaly_health_check_success{shard="default"} 1$/)
+ expect(response.body).to match(/^gitaly_health_check_latency_seconds{shard="default"} [0-9\.]+$/)
+ end
+
context 'prometheus metrics are disabled' do
before do
allow(Gitlab::Metrics).to receive(:prometheus_metrics_enabled?).and_return(false)
diff --git a/spec/controllers/oauth/authorizations_controller_spec.rb b/spec/controllers/oauth/authorizations_controller_spec.rb
index 149b690ff70..8c10ea53a7a 100644
--- a/spec/controllers/oauth/authorizations_controller_spec.rb
+++ b/spec/controllers/oauth/authorizations_controller_spec.rb
@@ -2,19 +2,12 @@ require 'spec_helper'
describe Oauth::AuthorizationsController do
let(:user) { create(:user) }
-
- let(:doorkeeper) do
- Doorkeeper::Application.create(
- name: "MyApp",
- redirect_uri: 'http://example.com',
- scopes: "")
- end
-
+ let!(:application) { create(:oauth_application, scopes: 'api read_user', redirect_uri: 'http://example.com') }
let(:params) do
{
response_type: "code",
- client_id: doorkeeper.uid,
- redirect_uri: doorkeeper.redirect_uri,
+ client_id: application.uid,
+ redirect_uri: application.redirect_uri,
state: 'state'
}
end
@@ -44,7 +37,7 @@ describe Oauth::AuthorizationsController do
end
it 'deletes session.user_return_to and redirects when skip authorization' do
- doorkeeper.update(trusted: true)
+ application.update(trusted: true)
request.session['user_return_to'] = 'http://example.com'
get :new, params
@@ -52,6 +45,25 @@ describe Oauth::AuthorizationsController do
expect(request.session['user_return_to']).to be_nil
expect(response).to have_gitlab_http_status(302)
end
+
+ context 'when there is already an access token for the application' do
+ context 'when the request scope matches any of the created token scopes' do
+ before do
+ scopes = Doorkeeper::OAuth::Scopes.from_string('api')
+
+ allow(Doorkeeper.configuration).to receive(:scopes).and_return(scopes)
+
+ create :oauth_access_token, application: application, resource_owner_id: user.id, scopes: scopes
+ end
+
+ it 'authorizes the request and redirects' do
+ get :new, params
+
+ expect(request.session['user_return_to']).to be_nil
+ expect(response).to have_gitlab_http_status(302)
+ end
+ end
+ end
end
end
end
diff --git a/spec/controllers/omniauth_callbacks_controller_spec.rb b/spec/controllers/omniauth_callbacks_controller_spec.rb
index 5f0e8c5eca9..b23f183fec8 100644
--- a/spec/controllers/omniauth_callbacks_controller_spec.rb
+++ b/spec/controllers/omniauth_callbacks_controller_spec.rb
@@ -1,127 +1,162 @@
require 'spec_helper'
-describe OmniauthCallbacksController do
+describe OmniauthCallbacksController, type: :controller do
include LoginHelpers
- let(:user) { create(:omniauth_user, extern_uid: extern_uid, provider: provider) }
-
- before do
- mock_auth_hash(provider.to_s, extern_uid, user.email)
- stub_omniauth_provider(provider, context: request)
- end
-
- context 'when the user is on the last sign in attempt' do
- let(:extern_uid) { 'my-uid' }
+ describe 'omniauth' do
+ let(:user) { create(:omniauth_user, extern_uid: extern_uid, provider: provider) }
before do
- user.update(failed_attempts: User.maximum_attempts.pred)
- subject.response = ActionDispatch::Response.new
+ mock_auth_hash(provider.to_s, extern_uid, user.email)
+ stub_omniauth_provider(provider, context: request)
end
- context 'when using a form based provider' do
- let(:provider) { :ldap }
-
- it 'locks the user when sign in fails' do
- allow(subject).to receive(:params).and_return(ActionController::Parameters.new(username: user.username))
- request.env['omniauth.error.strategy'] = OmniAuth::Strategies::LDAP.new(nil)
-
- subject.send(:failure)
+ context 'when the user is on the last sign in attempt' do
+ let(:extern_uid) { 'my-uid' }
- expect(user.reload).to be_access_locked
+ before do
+ user.update(failed_attempts: User.maximum_attempts.pred)
+ subject.response = ActionDispatch::Response.new
end
- end
- context 'when using a button based provider' do
- let(:provider) { :github }
+ context 'when using a form based provider' do
+ let(:provider) { :ldap }
- it 'does not lock the user when sign in fails' do
- request.env['omniauth.error.strategy'] = OmniAuth::Strategies::GitHub.new(nil)
+ it 'locks the user when sign in fails' do
+ allow(subject).to receive(:params).and_return(ActionController::Parameters.new(username: user.username))
+ request.env['omniauth.error.strategy'] = OmniAuth::Strategies::LDAP.new(nil)
- subject.send(:failure)
+ subject.send(:failure)
- expect(user.reload).not_to be_access_locked
+ expect(user.reload).to be_access_locked
+ end
end
- end
- end
- context 'strategies' do
- context 'github' do
- let(:extern_uid) { 'my-uid' }
- let(:provider) { :github }
+ context 'when using a button based provider' do
+ let(:provider) { :github }
- it 'allows sign in' do
- post provider
+ it 'does not lock the user when sign in fails' do
+ request.env['omniauth.error.strategy'] = OmniAuth::Strategies::GitHub.new(nil)
- expect(request.env['warden']).to be_authenticated
- end
-
- shared_context 'sign_up' do
- let(:user) { double(email: 'new@example.com') }
+ subject.send(:failure)
- before do
- stub_omniauth_setting(block_auto_created_users: false)
+ expect(user.reload).not_to be_access_locked
end
end
+ end
- context 'sign up' do
- include_context 'sign_up'
+ context 'strategies' do
+ context 'github' do
+ let(:extern_uid) { 'my-uid' }
+ let(:provider) { :github }
- it 'is allowed' do
+ it 'allows sign in' do
post provider
expect(request.env['warden']).to be_authenticated
end
- end
-
- context 'when OAuth is disabled' do
- before do
- stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
- settings = Gitlab::CurrentSettings.current_application_settings
- settings.update(disabled_oauth_sign_in_sources: [provider.to_s])
- end
- it 'prevents login via POST' do
- post provider
+ shared_context 'sign_up' do
+ let(:user) { double(email: 'new@example.com') }
- expect(request.env['warden']).not_to be_authenticated
+ before do
+ stub_omniauth_setting(block_auto_created_users: false)
+ end
end
- it 'shows warning when attempting login' do
- post provider
-
- expect(response).to redirect_to new_user_session_path
- expect(flash[:alert]).to eq('Signing in using GitHub has been disabled')
- end
+ context 'sign up' do
+ include_context 'sign_up'
- it 'allows linking the disabled provider' do
- user.identities.destroy_all
- sign_in(user)
+ it 'is allowed' do
+ post provider
- expect { post provider }.to change { user.reload.identities.count }.by(1)
+ expect(request.env['warden']).to be_authenticated
+ end
end
- context 'sign up' do
- include_context 'sign_up'
+ context 'when OAuth is disabled' do
+ before do
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
+ settings = Gitlab::CurrentSettings.current_application_settings
+ settings.update(disabled_oauth_sign_in_sources: [provider.to_s])
+ end
- it 'is prevented' do
+ it 'prevents login via POST' do
post provider
expect(request.env['warden']).not_to be_authenticated
end
+
+ it 'shows warning when attempting login' do
+ post provider
+
+ expect(response).to redirect_to new_user_session_path
+ expect(flash[:alert]).to eq('Signing in using GitHub has been disabled')
+ end
+
+ it 'allows linking the disabled provider' do
+ user.identities.destroy_all
+ sign_in(user)
+
+ expect { post provider }.to change { user.reload.identities.count }.by(1)
+ end
+
+ context 'sign up' do
+ include_context 'sign_up'
+
+ it 'is prevented' do
+ post provider
+
+ expect(request.env['warden']).not_to be_authenticated
+ end
+ end
+ end
+ end
+
+ context 'auth0' do
+ let(:extern_uid) { '' }
+ let(:provider) { :auth0 }
+
+ it 'does not allow sign in without extern_uid' do
+ post 'auth0'
+
+ expect(request.env['warden']).not_to be_authenticated
+ expect(response.status).to eq(302)
+ expect(controller).to set_flash[:alert].to('Wrong extern UID provided. Make sure Auth0 is configured correctly.')
end
end
end
+ end
+
+ describe '#saml' do
+ let(:user) { create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: 'saml') }
+ let(:mock_saml_response) { File.read('spec/fixtures/authentication/saml_response.xml') }
+ let(:saml_config) { mock_saml_config_with_upstream_two_factor_authn_contexts }
+
+ before do
+ stub_omniauth_saml_config({ enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'],
+ providers: [saml_config] })
+ mock_auth_hash('saml', 'my-uid', user.email, mock_saml_response)
+ request.env["devise.mapping"] = Devise.mappings[:user]
+ request.env['omniauth.auth'] = Rails.application.env_config['omniauth.auth']
+ post :saml, params: { SAMLResponse: mock_saml_response }
+ end
- context 'auth0' do
- let(:extern_uid) { '' }
- let(:provider) { :auth0 }
+ context 'when worth two factors' do
+ let(:mock_saml_response) do
+ File.read('spec/fixtures/authentication/saml_response.xml')
+ .gsub('urn:oasis:names:tc:SAML:2.0:ac:classes:Password', 'urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorIGTOKEN')
+ end
- it 'does not allow sign in without extern_uid' do
- post 'auth0'
+ it 'expects user to be signed_in' do
+ expect(request.env['warden']).to be_authenticated
+ end
+ end
+ context 'when not worth two factors' do
+ it 'expects user to provide second factor' do
+ expect(response).to render_template('devise/sessions/two_factor')
expect(request.env['warden']).not_to be_authenticated
- expect(response.status).to eq(302)
- expect(controller).to set_flash[:alert].to('Wrong extern UID provided. Make sure Auth0 is configured correctly.')
end
end
end
diff --git a/spec/controllers/profiles/avatars_controller_spec.rb b/spec/controllers/profiles/avatars_controller_spec.rb
index 4fa0462ccdf..909709e1103 100644
--- a/spec/controllers/profiles/avatars_controller_spec.rb
+++ b/spec/controllers/profiles/avatars_controller_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Profiles::AvatarsController do
- let(:user) { create(:user, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png")) }
+ let(:user) { create(:user, avatar: fixture_file_upload("spec/fixtures/dk.png")) }
before do
sign_in(user)
diff --git a/spec/controllers/profiles_controller_spec.rb b/spec/controllers/profiles_controller_spec.rb
index c621eb69171..4530a301d4d 100644
--- a/spec/controllers/profiles_controller_spec.rb
+++ b/spec/controllers/profiles_controller_spec.rb
@@ -3,6 +3,19 @@ require('spec_helper')
describe ProfilesController, :request_store do
let(:user) { create(:user) }
+ describe 'POST update' do
+ it 'does not update password' do
+ sign_in(user)
+
+ expect do
+ post :update,
+ user: { password: 'hello12345', password_confirmation: 'hello12345' }
+ end.not_to change { user.reload.encrypted_password }
+
+ expect(response.status).to eq(302)
+ end
+ end
+
describe 'PUT update' do
it 'allows an email update from a user without an external email address' do
sign_in(user)
diff --git a/spec/controllers/projects/avatars_controller_spec.rb b/spec/controllers/projects/avatars_controller_spec.rb
index 6a41c4d23ea..17c9a61f339 100644
--- a/spec/controllers/projects/avatars_controller_spec.rb
+++ b/spec/controllers/projects/avatars_controller_spec.rb
@@ -1,12 +1,12 @@
require 'spec_helper'
describe Projects::AvatarsController do
- let(:project) { create(:project, :repository, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
+ let(:project) { create(:project, :repository, avatar: fixture_file_upload("spec/fixtures/dk.png", "image/png")) }
let(:user) { create(:user) }
before do
sign_in(user)
- project.add_master(user)
+ project.add_maintainer(user)
controller.instance_variable_set(:@project, project)
end
diff --git a/spec/controllers/projects/badges_controller_spec.rb b/spec/controllers/projects/badges_controller_spec.rb
index e7cddf8cfbf..dfe34171b55 100644
--- a/spec/controllers/projects/badges_controller_spec.rb
+++ b/spec/controllers/projects/badges_controller_spec.rb
@@ -6,7 +6,7 @@ describe Projects::BadgesController do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/controllers/projects/blame_controller_spec.rb b/spec/controllers/projects/blame_controller_spec.rb
index 88d4f4e9cd0..fe4c4863717 100644
--- a/spec/controllers/projects/blame_controller_spec.rb
+++ b/spec/controllers/projects/blame_controller_spec.rb
@@ -7,7 +7,7 @@ describe Projects::BlameController do
before do
sign_in(user)
- project.add_master(user)
+ project.add_maintainer(user)
controller.instance_variable_set(:@project, project)
end
diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb
index 00a7df6ccc8..32cd7c6e70a 100644
--- a/spec/controllers/projects/blob_controller_spec.rb
+++ b/spec/controllers/projects/blob_controller_spec.rb
@@ -55,6 +55,25 @@ describe Projects::BlobController do
expect(json_response).to have_key 'raw_path'
end
end
+
+ context "with viewer=none" do
+ let(:id) { 'master/README.md' }
+
+ before do
+ get(:show,
+ namespace_id: project.namespace,
+ project_id: project,
+ id: id,
+ format: :json,
+ viewer: 'none')
+ end
+
+ it do
+ expect(response).to be_ok
+ expect(json_response).not_to have_key 'html'
+ expect(json_response).to have_key 'raw_path'
+ end
+ end
end
context 'with tree path' do
@@ -89,7 +108,7 @@ describe Projects::BlobController do
end
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
@@ -103,10 +122,64 @@ describe Projects::BlobController do
end
context 'when essential params are present' do
- it 'renders the diff content' do
- do_get(since: 1, to: 5, offset: 10)
+ context 'when rendering for commit' do
+ it 'renders the diff content' do
+ do_get(since: 1, to: 5, offset: 10)
+
+ expect(response.body).to be_present
+ end
+ end
+
+ context 'when rendering for merge request' do
+ it 'renders diff context lines Gitlab::Diff::Line array' do
+ do_get(since: 1, to: 5, offset: 10, from_merge_request: true)
+
+ lines = JSON.parse(response.body)
+
+ expect(lines.first).to have_key('type')
+ expect(lines.first).to have_key('rich_text')
+ expect(lines.first).to have_key('rich_text')
+ end
+
+ context 'when rendering match lines' do
+ it 'adds top match line when "since" is less than 1' do
+ do_get(since: 5, to: 10, offset: 10, from_merge_request: true)
+
+ match_line = JSON.parse(response.body).first
+
+ expect(match_line['type']).to eq('match')
+ expect(match_line['meta_data']).to have_key('old_pos')
+ expect(match_line['meta_data']).to have_key('new_pos')
+ end
+
+ it 'does not add top match line when when "since" is equal 1' do
+ do_get(since: 1, to: 10, offset: 10, from_merge_request: true)
- expect(response.body).to be_present
+ match_line = JSON.parse(response.body).first
+
+ expect(match_line['type']).to eq('context')
+ end
+
+ it 'adds bottom match line when "t"o is less than blob size' do
+ do_get(since: 1, to: 5, offset: 10, from_merge_request: true, bottom: true)
+
+ match_line = JSON.parse(response.body).last
+
+ expect(match_line['type']).to eq('match')
+ expect(match_line['meta_data']).to have_key('old_pos')
+ expect(match_line['meta_data']).to have_key('new_pos')
+ end
+
+ it 'does not add bottom match line when "to" is less than blob size' do
+ commit_id = project.repository.commit('master').id
+ blob = project.repository.blob_at(commit_id, 'CHANGELOG')
+ do_get(since: 1, to: blob.lines.count, offset: 10, from_merge_request: true, bottom: true)
+
+ match_line = JSON.parse(response.body).last
+
+ expect(match_line['type']).to eq('context')
+ end
+ end
end
end
end
@@ -157,12 +230,12 @@ describe Projects::BlobController do
end
end
- context 'as master' do
- let(:master) { create(:user) }
+ context 'as maintainer' do
+ let(:maintainer) { create(:user) }
before do
- project.add_master(master)
- sign_in(master)
+ project.add_maintainer(maintainer)
+ sign_in(maintainer)
get :edit, default_params
end
@@ -190,7 +263,7 @@ describe Projects::BlobController do
end
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/controllers/projects/boards_controller_spec.rb b/spec/controllers/projects/boards_controller_spec.rb
index 4d765229bde..096efc1c7b2 100644
--- a/spec/controllers/projects/boards_controller_spec.rb
+++ b/spec/controllers/projects/boards_controller_spec.rb
@@ -5,7 +5,7 @@ describe Projects::BoardsController do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
@@ -27,6 +27,20 @@ describe Projects::BoardsController do
expect(response).to render_template :index
expect(response.content_type).to eq 'text/html'
end
+
+ context 'with unauthorized user' do
+ before do
+ allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true)
+ allow(Ability).to receive(:allowed?).with(user, :read_board, project).and_return(false)
+ end
+
+ it 'returns a not found 404 response' do
+ list_boards
+
+ expect(response).to have_gitlab_http_status(404)
+ expect(response.content_type).to eq 'text/html'
+ end
+ end
end
context 'when format is JSON' do
@@ -40,18 +54,19 @@ describe Projects::BoardsController do
expect(response).to match_response_schema('boards')
expect(parsed_response.length).to eq 2
end
- end
- context 'with unauthorized user' do
- before do
- allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true)
- allow(Ability).to receive(:allowed?).with(user, :read_board, project).and_return(false)
- end
+ context 'with unauthorized user' do
+ before do
+ allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true)
+ allow(Ability).to receive(:allowed?).with(user, :read_board, project).and_return(false)
+ end
- it 'returns a not found 404 response' do
- list_boards
+ it 'returns a not found 404 response' do
+ list_boards format: :json
- expect(response).to have_gitlab_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
+ expect(response.content_type).to eq 'application/json'
+ end
end
end
@@ -88,6 +103,20 @@ describe Projects::BoardsController do
expect(response).to render_template :show
expect(response.content_type).to eq 'text/html'
end
+
+ context 'with unauthorized user' do
+ before do
+ allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true)
+ allow(Ability).to receive(:allowed?).with(user, :read_board, project).and_return(false)
+ end
+
+ it 'returns a not found 404 response' do
+ read_board board: board
+
+ expect(response).to have_gitlab_http_status(404)
+ expect(response.content_type).to eq 'text/html'
+ end
+ end
end
context 'when format is JSON' do
@@ -96,18 +125,19 @@ describe Projects::BoardsController do
expect(response).to match_response_schema('board')
end
- end
- context 'with unauthorized user' do
- before do
- allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true)
- allow(Ability).to receive(:allowed?).with(user, :read_board, project).and_return(false)
- end
+ context 'with unauthorized user' do
+ before do
+ allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true)
+ allow(Ability).to receive(:allowed?).with(user, :read_board, project).and_return(false)
+ end
- it 'returns a not found 404 response' do
- read_board board: board
+ it 'returns a not found 404 response' do
+ read_board board: board, format: :json
- expect(response).to have_gitlab_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
+ expect(response.content_type).to eq 'application/json'
+ end
end
end
diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb
index 16fb377b002..31471cde420 100644
--- a/spec/controllers/projects/branches_controller_spec.rb
+++ b/spec/controllers/projects/branches_controller_spec.rb
@@ -6,7 +6,7 @@ describe Projects::BranchesController do
let(:developer) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
project.add_developer(user)
allow(project).to receive(:branches).and_return(['master', 'foo/bar/baz'])
@@ -146,6 +146,24 @@ describe Projects::BranchesController do
it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
+
+ it 'redirects to autodeploy setup page' do
+ result = { status: :success, branch: double(name: branch) }
+
+ create(:cluster, :provided_by_gcp, projects: [project])
+
+ 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
+
+ expect(response.location).to include(project_new_blob_path(project, branch))
+ expect(response).to have_gitlab_http_status(302)
+ end
end
context 'when create branch service fails' do
diff --git a/spec/controllers/projects/clusters/applications_controller_spec.rb b/spec/controllers/projects/clusters/applications_controller_spec.rb
index 99fdff5f846..9e17e392d3d 100644
--- a/spec/controllers/projects/clusters/applications_controller_spec.rb
+++ b/spec/controllers/projects/clusters/applications_controller_spec.rb
@@ -17,7 +17,7 @@ describe Projects::Clusters::ApplicationsController do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
@@ -70,7 +70,7 @@ describe Projects::Clusters::ApplicationsController 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_allowed_for(:maintainer).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) }
diff --git a/spec/controllers/projects/clusters/gcp_controller_spec.rb b/spec/controllers/projects/clusters/gcp_controller_spec.rb
deleted file mode 100644
index 715bb9f5e52..00000000000
--- a/spec/controllers/projects/clusters/gcp_controller_spec.rb
+++ /dev/null
@@ -1,202 +0,0 @@
-require 'spec_helper'
-
-describe Projects::Clusters::GcpController do
- include AccessMatchersForController
- include GoogleApi::CloudPlatformHelpers
-
- set(:project) { create(:project) }
-
- describe 'GET login' do
- describe 'functionality' do
- let(:user) { create(:user) }
-
- before do
- project.add_master(user)
- sign_in(user)
- end
-
- context 'when omniauth has been configured' do
- let(:key) { 'secret-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(gcp_new_project_clusters_path(project))
- end
- end
-
- context 'when omniauth has not configured' do
- before do
- stub_omniauth_setting(providers: [])
- end
-
- it 'does not have authorize_url' do
- go
-
- 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
-
- describe 'GET new' do
- describe 'functionality' do
- let(:user) { create(:user) }
-
- before do
- project.add_master(user)
- sign_in(user)
- end
-
- context 'when access token is valid' do
- before do
- stub_google_api_validate_token
- end
-
- it 'has new object' do
- expect(controller).to receive(:authorize_google_project_billing)
-
- 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(gcp_login_project_clusters_path(project)) }
- end
-
- context 'when access token is not stored in session' do
- it { expect(go).to redirect_to(gcp_login_project_clusters_path(project)) }
- 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 :new, namespace_id: project.namespace, project_id: project
- end
- end
-
- describe 'POST create' do
- let(:params) do
- {
- cluster: {
- name: 'new-cluster',
- provider_gcp_attributes: {
- gcp_project_id: '111'
- }
- }
- }
- end
-
- describe 'functionality' do
- let(:user) { create(:user) }
-
- before do
- project.add_master(user)
- sign_in(user)
- end
-
- context 'when access token is valid' do
- before do
- stub_google_api_validate_token
- allow_any_instance_of(described_class).to receive(:authorize_google_project_billing)
- end
-
- context 'when google project billing is enabled' do
- before do
- redis_double = double.as_null_object
- allow(Gitlab::Redis::SharedState).to receive(:with).and_yield(redis_double)
- allow(redis_double).to receive(:get).with(CheckGcpProjectBillingWorker.redis_shared_state_key_for('token')).and_return('true')
- end
-
- it 'creates a new cluster' do
- expect(ClusterProvisionWorker).to receive(:perform_async)
- expect { go }.to change { Clusters::Cluster.count }
- .and change { Clusters::Providers::Gcp.count }
- expect(response).to redirect_to(project_cluster_path(project, project.clusters.first))
- expect(project.clusters.first).to be_gcp
- expect(project.clusters.first).to be_kubernetes
- end
- end
-
- context 'when google project billing is not enabled' do
- it 'renders the cluster form with an error' do
- go
-
- expect(response).to set_flash.now[:alert]
- expect(response).to render_template('new')
- end
- end
- end
-
- context 'when access token is expired' do
- before do
- stub_google_api_expired_token
- end
-
- it 'redirects to login page' do
- expect(go).to redirect_to(gcp_login_project_clusters_path(project))
- end
- end
-
- context 'when access token is not stored in session' do
- it 'redirects to login page' do
- expect(go).to redirect_to(gcp_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
-end
diff --git a/spec/controllers/projects/clusters/user_controller_spec.rb b/spec/controllers/projects/clusters/user_controller_spec.rb
deleted file mode 100644
index 913976d187f..00000000000
--- a/spec/controllers/projects/clusters/user_controller_spec.rb
+++ /dev/null
@@ -1,89 +0,0 @@
-require 'spec_helper'
-
-describe Projects::Clusters::UserController do
- include AccessMatchersForController
-
- set(:project) { create(:project) }
-
- describe 'GET new' do
- describe 'functionality' do
- let(:user) { create(:user) }
-
- before do
- project.add_master(user)
- sign_in(user)
- end
-
- it 'has new object' do
- go
-
- expect(assigns(:cluster)).to be_an_instance_of(Clusters::Cluster)
- 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 :new, namespace_id: project.namespace, project_id: project
- end
- end
-
- describe 'POST create' do
- let(:params) do
- {
- cluster: {
- name: 'new-cluster',
- platform_kubernetes_attributes: {
- api_url: 'http://my-url',
- token: 'test',
- namespace: 'aaa'
- }
- }
- }
- end
-
- describe 'functionality' do
- let(:user) { create(:user) }
-
- before do
- project.add_master(user)
- sign_in(user)
- end
-
- context 'when creates a cluster' do
- it 'creates a new cluster' do
- expect(ClusterProvisionWorker).to receive(:perform_async)
- expect { go }.to change { Clusters::Cluster.count }
- .and change { Clusters::Platforms::Kubernetes.count }
- expect(response).to redirect_to(project_cluster_path(project, project.clusters.first))
- expect(project.clusters.first).to be_user
- expect(project.clusters.first).to be_kubernetes
- 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
-end
diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb
index 82b20e12850..42917d0d505 100644
--- a/spec/controllers/projects/clusters_controller_spec.rb
+++ b/spec/controllers/projects/clusters_controller_spec.rb
@@ -11,7 +11,7 @@ describe Projects::ClustersController do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
@@ -61,7 +61,7 @@ describe Projects::ClustersController 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_allowed_for(:maintainer).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) }
@@ -74,6 +74,231 @@ describe Projects::ClustersController do
end
end
+ describe 'GET new' do
+ describe 'functionality for new cluster' do
+ let(:user) { create(:user) }
+
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+ end
+
+ context 'when omniauth has been configured' do
+ let(:key) { 'secret-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(new_project_cluster_path(project))
+ end
+ end
+
+ context 'when omniauth has not configured' do
+ before do
+ stub_omniauth_setting(providers: [])
+ end
+
+ it 'does not have authorize_url' do
+ go
+
+ expect(assigns(:authorize_url)).to be_nil
+ end
+ end
+
+ context 'when access token is valid' do
+ before do
+ stub_google_api_validate_token
+ end
+
+ it 'has new object' do
+ go
+
+ expect(assigns(:gcp_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(@valid_gcp_token).to be_falsey }
+ end
+
+ context 'when access token is not stored in session' do
+ it { expect(@valid_gcp_token).to be_falsey }
+ end
+ end
+
+ describe 'functionality for existing cluster' do
+ let(:user) { create(:user) }
+
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+ end
+
+ it 'has new object' do
+ go
+
+ expect(assigns(:user_cluster)).to be_an_instance_of(Clusters::Cluster)
+ 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(:maintainer).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, namespace_id: project.namespace, project_id: project
+ end
+ end
+
+ describe 'POST create for new cluster' do
+ let(:params) do
+ {
+ cluster: {
+ name: 'new-cluster',
+ provider_gcp_attributes: {
+ gcp_project_id: 'gcp-project-12345'
+ }
+ }
+ }
+ end
+
+ describe 'functionality' do
+ let(:user) { create(:user) }
+
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+ end
+
+ context 'when access token is valid' do
+ before do
+ stub_google_api_validate_token
+ end
+
+ it 'creates a new cluster' do
+ expect(ClusterProvisionWorker).to receive(:perform_async)
+ expect { go }.to change { Clusters::Cluster.count }
+ .and change { Clusters::Providers::Gcp.count }
+ expect(response).to redirect_to(project_cluster_path(project, project.clusters.first))
+ expect(project.clusters.first).to be_gcp
+ expect(project.clusters.first).to be_kubernetes
+ end
+ end
+
+ context 'when access token is expired' do
+ before do
+ stub_google_api_expired_token
+ end
+
+ it { expect(@valid_gcp_token).to be_falsey }
+ end
+
+ context 'when access token is not stored in session' do
+ it { expect(@valid_gcp_token).to be_falsey }
+ end
+ end
+
+ describe 'security' do
+ before do
+ allow_any_instance_of(described_class)
+ .to receive(:token_in_session).and_return('token')
+ allow_any_instance_of(described_class)
+ .to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s)
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:projects_zones_clusters_create) do
+ OpenStruct.new(
+ self_link: 'projects/gcp-project-12345/zones/us-central1-a/operations/ope-123',
+ status: 'RUNNING'
+ )
+ end
+
+ allow(WaitForClusterCreationWorker).to receive(:perform_in).and_return(nil)
+ 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(:maintainer).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_gcp, params.merge(namespace_id: project.namespace, project_id: project)
+ end
+ end
+
+ describe 'POST create for existing cluster' do
+ let(:params) do
+ {
+ cluster: {
+ name: 'new-cluster',
+ platform_kubernetes_attributes: {
+ api_url: 'http://my-url',
+ token: 'test',
+ namespace: 'aaa'
+ }
+ }
+ }
+ end
+
+ describe 'functionality' do
+ let(:user) { create(:user) }
+
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+ end
+
+ context 'when creates a cluster' do
+ it 'creates a new cluster' do
+ expect(ClusterProvisionWorker).to receive(:perform_async)
+ expect { go }.to change { Clusters::Cluster.count }
+ .and change { Clusters::Platforms::Kubernetes.count }
+ expect(response).to redirect_to(project_cluster_path(project, project.clusters.first))
+ expect(project.clusters.first).to be_user
+ expect(project.clusters.first).to be_kubernetes
+ 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(:maintainer).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_user, params.merge(namespace_id: project.namespace, project_id: project)
+ end
+ end
+
describe 'GET status' do
let(:cluster) { create(:cluster, :providing_by_gcp, projects: [project]) }
@@ -81,7 +306,7 @@ describe Projects::ClustersController do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
@@ -102,7 +327,7 @@ describe Projects::ClustersController do
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_allowed_for(:maintainer).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) }
@@ -125,7 +350,7 @@ describe Projects::ClustersController do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
@@ -140,7 +365,7 @@ describe Projects::ClustersController do
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_allowed_for(:maintainer).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) }
@@ -161,7 +386,7 @@ describe Projects::ClustersController do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
@@ -212,7 +437,7 @@ describe Projects::ClustersController do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
@@ -300,7 +525,7 @@ describe Projects::ClustersController 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_allowed_for(:maintainer).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) }
@@ -327,13 +552,13 @@ describe Projects::ClustersController do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
context 'when cluster is provided by GCP' do
context 'when cluster is created' do
- let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
+ let!(:cluster) { create(:cluster, :provided_by_gcp, :production_environment, projects: [project]) }
it "destroys and redirects back to clusters list" do
expect { go }
@@ -347,7 +572,7 @@ describe Projects::ClustersController do
end
context 'when cluster is being created' do
- let!(:cluster) { create(:cluster, :providing_by_gcp, projects: [project]) }
+ let!(:cluster) { create(:cluster, :providing_by_gcp, :production_environment, projects: [project]) }
it "destroys and redirects back to clusters list" do
expect { go }
@@ -361,7 +586,7 @@ describe Projects::ClustersController do
end
context 'when cluster is provided by user' do
- let!(:cluster) { create(:cluster, :provided_by_user, projects: [project]) }
+ let!(:cluster) { create(:cluster, :provided_by_user, :production_environment, projects: [project]) }
it "destroys and redirects back to clusters list" do
expect { go }
@@ -376,11 +601,11 @@ describe Projects::ClustersController do
end
describe 'security' do
- set(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
+ set(:cluster) { create(:cluster, :provided_by_gcp, :production_environment, projects: [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_allowed_for(:maintainer).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) }
diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb
index 003fec8ac68..916a4be2567 100644
--- a/spec/controllers/projects/commit_controller_spec.rb
+++ b/spec/controllers/projects/commit_controller_spec.rb
@@ -9,7 +9,7 @@ describe Projects::CommitController do
before do
sign_in(user)
- project.add_master(user)
+ project.add_maintainer(user)
end
describe 'GET show' do
diff --git a/spec/controllers/projects/commits_controller_spec.rb b/spec/controllers/projects/commits_controller_spec.rb
index 55ed276f96b..d44048fdf55 100644
--- a/spec/controllers/projects/commits_controller_spec.rb
+++ b/spec/controllers/projects/commits_controller_spec.rb
@@ -6,7 +6,7 @@ describe Projects::CommitsController do
before do
sign_in(user)
- project.add_master(user)
+ project.add_maintainer(user)
end
describe "GET show" do
diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb
index b15cde4314e..8695aa826bb 100644
--- a/spec/controllers/projects/compare_controller_spec.rb
+++ b/spec/controllers/projects/compare_controller_spec.rb
@@ -6,7 +6,7 @@ describe Projects::CompareController do
before do
sign_in(user)
- project.add_master(user)
+ project.add_maintainer(user)
end
describe 'GET index' do
diff --git a/spec/controllers/projects/cycle_analytics_controller_spec.rb b/spec/controllers/projects/cycle_analytics_controller_spec.rb
index 5516c95d044..5c79269e8f1 100644
--- a/spec/controllers/projects/cycle_analytics_controller_spec.rb
+++ b/spec/controllers/projects/cycle_analytics_controller_spec.rb
@@ -6,7 +6,7 @@ describe Projects::CycleAnalyticsController do
before do
sign_in(user)
- project.add_master(user)
+ project.add_maintainer(user)
end
describe 'cycle analytics not set up flag' do
diff --git a/spec/controllers/projects/deploy_keys_controller_spec.rb b/spec/controllers/projects/deploy_keys_controller_spec.rb
index 97db69427e9..d2f133f972a 100644
--- a/spec/controllers/projects/deploy_keys_controller_spec.rb
+++ b/spec/controllers/projects/deploy_keys_controller_spec.rb
@@ -5,7 +5,7 @@ describe Projects::DeployKeysController do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/controllers/projects/deployments_controller_spec.rb b/spec/controllers/projects/deployments_controller_spec.rb
index 6c67dfde63a..d1c960e895d 100644
--- a/spec/controllers/projects/deployments_controller_spec.rb
+++ b/spec/controllers/projects/deployments_controller_spec.rb
@@ -8,7 +8,7 @@ describe Projects::DeploymentsController do
let(:environment) { create(:environment, name: 'production', project: project) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/controllers/projects/discussions_controller_spec.rb b/spec/controllers/projects/discussions_controller_spec.rb
index 53647749a60..4aa33dbbb01 100644
--- a/spec/controllers/projects/discussions_controller_spec.rb
+++ b/spec/controllers/projects/discussions_controller_spec.rb
@@ -110,7 +110,7 @@ describe Projects::DiscussionsController do
it "returns the name of the resolving user" do
post :resolve, request_params
- expect(JSON.parse(response.body)["resolved_by"]).to eq(user.name)
+ expect(JSON.parse(response.body)['resolved_by']['name']).to eq(user.name)
end
it "returns status 200" do
@@ -119,16 +119,21 @@ describe Projects::DiscussionsController do
expect(response).to have_gitlab_http_status(200)
end
- context "when vue_mr_discussions cookie is present" do
- before do
- allow(controller).to receive(:cookies).and_return(vue_mr_discussions: 'true')
- end
+ it "renders discussion with serializer" do
+ expect_any_instance_of(DiscussionSerializer).to receive(:represent)
+ .with(instance_of(Discussion), { context: instance_of(described_class), render_truncated_diff_lines: true })
- it "renders discussion with serializer" do
- expect_any_instance_of(DiscussionSerializer).to receive(:represent)
- .with(instance_of(Discussion), { context: instance_of(described_class) })
+ post :resolve, request_params
+ end
+ context 'diff discussion' do
+ let(:note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project) }
+ let(:discussion) { note.discussion }
+
+ it "returns truncated diff lines" do
post :resolve, request_params
+
+ expect(JSON.parse(response.body)['truncated_diff_lines']).to be_present
end
end
end
@@ -187,7 +192,7 @@ describe Projects::DiscussionsController do
it "renders discussion with serializer" do
expect_any_instance_of(DiscussionSerializer).to receive(:represent)
- .with(instance_of(Discussion), { context: instance_of(described_class) })
+ .with(instance_of(Discussion), { context: instance_of(described_class), render_truncated_diff_lines: true })
delete :unresolve, request_params
end
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index ff9ab53d8c3..b86029a4baf 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -9,7 +9,7 @@ describe Projects::EnvironmentsController do
end
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
@@ -21,6 +21,13 @@ describe Projects::EnvironmentsController do
expect(response).to have_gitlab_http_status(:ok)
end
+
+ it 'expires etag cache to force reload environments list' do
+ expect_any_instance_of(Gitlab::EtagCaching::Store)
+ .to receive(:touch).with(project_environments_path(project, format: :json))
+
+ get :index, environment_params
+ end
end
context 'when requesting JSON response for folders' do
@@ -270,6 +277,25 @@ describe Projects::EnvironmentsController do
end
end
+ describe 'GET #metrics_redirect' do
+ let(:project) { create(:project) }
+
+ it 'redirects to environment if it exists' do
+ environment = create(:environment, name: 'production', project: project)
+
+ get :metrics_redirect, namespace_id: project.namespace, project_id: project
+
+ expect(response).to redirect_to(environment_metrics_path(environment))
+ end
+
+ it 'redirects to empty page if no environment exists' do
+ get :metrics_redirect, namespace_id: project.namespace, project_id: project
+
+ expect(response).to be_ok
+ expect(response).to render_template 'empty'
+ end
+ end
+
describe 'GET #metrics' do
before do
allow(controller).to receive(:environment).and_return(environment)
diff --git a/spec/controllers/projects/find_file_controller_spec.rb b/spec/controllers/projects/find_file_controller_spec.rb
index 505fe82851a..66fe41108e2 100644
--- a/spec/controllers/projects/find_file_controller_spec.rb
+++ b/spec/controllers/projects/find_file_controller_spec.rb
@@ -7,7 +7,7 @@ describe Projects::FindFileController do
before do
sign_in(user)
- project.add_master(user)
+ project.add_maintainer(user)
controller.instance_variable_set(:@project, project)
end
diff --git a/spec/controllers/projects/forks_controller_spec.rb b/spec/controllers/projects/forks_controller_spec.rb
index e20623c0ac1..945b6142abf 100644
--- a/spec/controllers/projects/forks_controller_spec.rb
+++ b/spec/controllers/projects/forks_controller_spec.rb
@@ -31,7 +31,7 @@ describe Projects::ForksController do
context 'when fork is private' do
before do
- forked_project.update_attributes(visibility_level: Project::PRIVATE, group: group)
+ forked_project.update(visibility_level: Project::PRIVATE, group: group)
end
it 'is not be visible for non logged in users' do
diff --git a/spec/controllers/projects/graphs_controller_spec.rb b/spec/controllers/projects/graphs_controller_spec.rb
index c3605555fe7..da78592a6f6 100644
--- a/spec/controllers/projects/graphs_controller_spec.rb
+++ b/spec/controllers/projects/graphs_controller_spec.rb
@@ -6,7 +6,7 @@ describe Projects::GraphsController do
before do
sign_in(user)
- project.add_master(user)
+ project.add_maintainer(user)
end
describe 'GET languages' do
diff --git a/spec/controllers/projects/group_links_controller_spec.rb b/spec/controllers/projects/group_links_controller_spec.rb
index 5bfc3d31401..879aff26deb 100644
--- a/spec/controllers/projects/group_links_controller_spec.rb
+++ b/spec/controllers/projects/group_links_controller_spec.rb
@@ -7,7 +7,7 @@ describe Projects::GroupLinksController do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
@@ -21,6 +21,18 @@ describe Projects::GroupLinksController do
end
end
+ context 'when project is not allowed to be shared with a group' do
+ before do
+ group.update(share_with_group_lock: false)
+ end
+
+ include_context 'link project to group'
+
+ it 'responds with status 404' do
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
context 'when user has access to group he want to link project to' do
before do
group.add_developer(user)
diff --git a/spec/controllers/projects/hooks_controller_spec.rb b/spec/controllers/projects/hooks_controller_spec.rb
index 2d473d5bf52..0f3033b0933 100644
--- a/spec/controllers/projects/hooks_controller_spec.rb
+++ b/spec/controllers/projects/hooks_controller_spec.rb
@@ -5,7 +5,7 @@ describe Projects::HooksController do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/controllers/projects/imports_controller_spec.rb b/spec/controllers/projects/imports_controller_spec.rb
index 7fb4c1b7425..adf3c78ae51 100644
--- a/spec/controllers/projects/imports_controller_spec.rb
+++ b/spec/controllers/projects/imports_controller_spec.rb
@@ -2,16 +2,15 @@ require 'spec_helper'
describe Projects::ImportsController do
let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ before do
+ sign_in(user)
+ project.add_maintainer(user)
+ end
describe 'GET #show' do
context 'when repository does not exists' do
- let(:project) { create(:project) }
-
- before do
- sign_in(user)
- project.add_master(user)
- end
-
it 'renders template' do
get :show, namespace_id: project.namespace.to_param, project_id: project
@@ -28,14 +27,9 @@ describe Projects::ImportsController do
context 'when repository exists' do
let(:project) { create(:project_empty_repo, import_url: 'https://github.com/vim/vim.git') }
- before do
- sign_in(user)
- project.add_master(user)
- end
-
context 'when import is in progress' do
before do
- project.update_attribute(:import_status, :started)
+ project.update(import_status: :started)
end
it 'renders template' do
@@ -53,7 +47,7 @@ describe Projects::ImportsController do
context 'when import failed' do
before do
- project.update_attribute(:import_status, :failed)
+ project.update(import_status: :failed)
end
it 'redirects to new_namespace_project_import_path' do
@@ -65,7 +59,7 @@ describe Projects::ImportsController do
context 'when import finished' do
before do
- project.update_attribute(:import_status, :finished)
+ project.update(import_status: :finished)
end
context 'when project is a fork' do
@@ -114,7 +108,7 @@ describe Projects::ImportsController do
context 'when import never happened' do
before do
- project.update_attribute(:import_status, :none)
+ project.update(import_status: :none)
end
it 'redirects to namespace_project_path' do
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index ca86b0bc737..ff1835a34c2 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -1,4 +1,4 @@
-require('spec_helper')
+require 'spec_helper'
describe Projects::IssuesController do
let(:project) { create(:project) }
@@ -695,7 +695,7 @@ describe Projects::IssuesController do
let(:project) { merge_request.source_project }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in user
end
@@ -869,7 +869,7 @@ describe Projects::IssuesController do
def post_spam
admin = create(:admin)
create(:user_agent_detail, subject: issue)
- project.add_master(admin)
+ project.add_maintainer(admin)
sign_in(admin)
post :mark_as_spam, {
namespace_id: project.namespace,
@@ -990,7 +990,7 @@ describe Projects::IssuesController do
it 'returns discussion json' do
get :discussions, namespace_id: project.namespace, project_id: project, id: issue.iid
- expect(json_response.first.keys).to match_array(%w[id reply_id expanded notes diff_discussion individual_note resolvable resolved])
+ expect(json_response.first.keys).to match_array(%w[id reply_id expanded notes diff_discussion discussion_path individual_note resolvable resolved resolved_at resolved_by resolved_by_push commit_id for_commit project_id])
end
context 'with cross-reference system note', :request_store do
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index a08fcea27a5..1aca44c6e74 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -102,6 +102,8 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
describe 'GET show' do
let!(:job) { create(:ci_build, :failed, pipeline: pipeline) }
+ let!(:second_job) { create(:ci_build, :failed, pipeline: pipeline) }
+ let!(:third_job) { create(:ci_build, :failed) }
context 'when requesting HTML' do
context 'when job exists' do
@@ -113,6 +115,13 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:build).id).to eq(job.id)
end
+
+ it 'has the correct build collection' do
+ builds = assigns(:builds).map(&:id)
+
+ expect(builds).to include(job.id, second_job.id)
+ expect(builds).not_to include(third_job.id)
+ end
end
context 'when job does not exist' do
@@ -207,17 +216,19 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
end
context 'when trace artifact is in ObjectStorage' do
+ let(:url) { 'http://object-storage/trace' }
+ let(:file_path) { expand_fixture_path('trace/sample_trace') }
let!(:job) { create(:ci_build, :success, :trace_artifact, pipeline: pipeline) }
before do
allow_any_instance_of(JobArtifactUploader).to receive(:file_storage?) { false }
- allow_any_instance_of(JobArtifactUploader).to receive(:url) { remote_trace_url }
- allow_any_instance_of(JobArtifactUploader).to receive(:size) { remote_trace_size }
+ allow_any_instance_of(JobArtifactUploader).to receive(:url) { url }
+ allow_any_instance_of(JobArtifactUploader).to receive(:size) { File.size(file_path) }
end
context 'when there are no network issues' do
before do
- stub_remote_trace_206
+ stub_remote_url_206(url, file_path)
get_trace
end
@@ -232,11 +243,11 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
context 'when there is a network issue' do
before do
- stub_remote_trace_500
+ stub_remote_url_500(url)
end
it 'returns a trace' do
- expect { get_trace }.to raise_error(Gitlab::Ci::Trace::HttpIO::FailedToGetChunkError)
+ expect { get_trace }.to raise_error(Gitlab::HttpIO::FailedToGetChunkError)
end
end
end
@@ -265,7 +276,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
expect(json_response['text']).to eq status.text
expect(json_response['label']).to eq status.label
expect(json_response['icon']).to eq status.icon
- expect(json_response['favicon']).to match_asset_path "/assets/ci_favicons/#{status.favicon}.ico"
+ expect(json_response['favicon']).to match_asset_path "/assets/ci_favicons/#{status.favicon}.png"
end
end
@@ -420,7 +431,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
end
describe 'POST erase' do
- let(:role) { :master }
+ let(:role) { :maintainer }
before do
project.add_role(user, role)
@@ -553,4 +564,105 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
end
end
end
+
+ describe 'GET #terminal' do
+ before do
+ project.add_developer(user)
+ sign_in(user)
+ end
+
+ context 'when job exists' do
+ context 'and it has a terminal' do
+ let!(:job) { create(:ci_build, :running, :with_runner_session, pipeline: pipeline) }
+
+ it 'has a job' do
+ get_terminal(id: job.id)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(assigns(:build).id).to eq(job.id)
+ end
+ end
+
+ context 'and does not have a terminal' do
+ let!(:job) { create(:ci_build, :running, pipeline: pipeline) }
+
+ it 'returns not_found' do
+ get_terminal(id: job.id)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ context 'when job does not exist' do
+ it 'renders not_found' do
+ get_terminal(id: 1234)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ def get_terminal(**extra_params)
+ params = {
+ namespace_id: project.namespace.to_param,
+ project_id: project
+ }
+
+ get :terminal, params.merge(extra_params)
+ end
+ end
+
+ describe 'GET #terminal_websocket_authorize' do
+ let!(:job) { create(:ci_build, :running, :with_runner_session, pipeline: pipeline) }
+
+ before do
+ project.add_developer(user)
+ sign_in(user)
+ end
+
+ context 'with valid workhorse signature' do
+ before do
+ allow(Gitlab::Workhorse).to receive(:verify_api_request!).and_return(nil)
+ end
+
+ context 'and valid id' do
+ it 'returns the terminal for the job' do
+ expect(Gitlab::Workhorse)
+ .to receive(:terminal_websocket)
+ .and_return(workhorse: :response)
+
+ get_terminal_websocket(id: job.id)
+
+ 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
+ end
+
+ context 'and invalid id' do
+ it 'returns 404' do
+ get_terminal_websocket(id: 1234)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
+ context 'with invalid workhorse signature' do
+ it 'aborts with an exception' do
+ allow(Gitlab::Workhorse).to receive(:verify_api_request!).and_raise(JWT::DecodeError)
+
+ expect { get_terminal_websocket(id: job.id) }.to raise_error(JWT::DecodeError)
+ end
+ end
+
+ def get_terminal_websocket(**extra_params)
+ params = {
+ namespace_id: project.namespace.to_param,
+ project_id: project
+ }
+
+ get :terminal_websocket_authorize, params.merge(extra_params)
+ end
+ end
end
diff --git a/spec/controllers/projects/labels_controller_spec.rb b/spec/controllers/projects/labels_controller_spec.rb
index 452d7e23983..273702e6d21 100644
--- a/spec/controllers/projects/labels_controller_spec.rb
+++ b/spec/controllers/projects/labels_controller_spec.rb
@@ -6,7 +6,7 @@ describe Projects::LabelsController do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/controllers/projects/mattermosts_controller_spec.rb b/spec/controllers/projects/mattermosts_controller_spec.rb
index c5ac0be27bb..c2a334a849c 100644
--- a/spec/controllers/projects/mattermosts_controller_spec.rb
+++ b/spec/controllers/projects/mattermosts_controller_spec.rb
@@ -5,7 +5,7 @@ describe Projects::MattermostsController do
let!(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb b/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb
index 2d7647a6e12..397cc79bde4 100644
--- a/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb
@@ -5,7 +5,7 @@ describe Projects::MergeRequests::ConflictsController do
let(:user) { project.owner }
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
let(:merge_request_with_conflicts) do
- create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start', source_project: project) do |mr|
+ create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start', source_project: project, merge_status: :unchecked) do |mr|
mr.mark_as_unmergeable
end
end
diff --git a/spec/controllers/projects/merge_requests/creations_controller_spec.rb b/spec/controllers/projects/merge_requests/creations_controller_spec.rb
index 00d76f3c39a..d8995f98575 100644
--- a/spec/controllers/projects/merge_requests/creations_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/creations_controller_spec.rb
@@ -16,7 +16,7 @@ describe Projects::MergeRequests::CreationsController do
end
before do
- fork_project.add_master(user)
+ fork_project.add_maintainer(user)
Projects::ForkService.new(project, user).execute(fork_project)
sign_in(user)
end
@@ -94,7 +94,7 @@ describe Projects::MergeRequests::CreationsController do
let(:other_project) { create(:project, :repository) }
before do
- other_project.add_master(user)
+ other_project.add_maintainer(user)
end
context 'when the path exists in the diff' do
diff --git a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
index 5d297c654bf..9dc06436c72 100644
--- a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
@@ -26,12 +26,13 @@ describe Projects::MergeRequests::DiffsController do
context 'with default params' do
context 'for the same project' do
before do
- go
+ allow(controller).to receive(:rendered_for_merge_request?).and_return(true)
end
- it 'renders the diffs template to a string' do
- expect(response).to render_template('projects/merge_requests/diffs/_diffs')
- expect(json_response).to have_key('html')
+ it 'serializes merge request diff collection' do
+ expect_any_instance_of(DiffsSerializer).to receive(:represent).with(an_instance_of(Gitlab::Diff::FileCollection::MergeRequestDiff), an_instance_of(Hash))
+
+ go
end
end
@@ -56,17 +57,6 @@ describe Projects::MergeRequests::DiffsController do
end
end
- context 'with ignore_whitespace_change' do
- before do
- go(w: 1)
- end
-
- it 'renders the diffs template to a string' do
- expect(response).to render_template('projects/merge_requests/diffs/_diffs')
- expect(json_response).to have_key('html')
- end
- end
-
context 'with view' do
before do
go(view: 'parallel')
@@ -105,12 +95,11 @@ describe Projects::MergeRequests::DiffsController do
end
it 'only renders the diffs for the path given' do
- expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs|
- expect(diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path)
- meth.call(diffs)
- end
-
diff_for_path(old_path: existing_path, new_path: existing_path)
+
+ paths = JSON.parse(response.body)["diff_files"].map { |file| file['new_path'] }
+
+ expect(paths).to include(existing_path)
end
end
@@ -151,7 +140,7 @@ describe Projects::MergeRequests::DiffsController do
let(:other_project) { create(:project) }
before do
- other_project.add_master(user)
+ other_project.add_maintainer(user)
diff_for_path(old_path: existing_path, new_path: existing_path, project_id: other_project)
end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index c8cc6b374f6..444415011a9 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -7,7 +7,7 @@ describe Projects::MergeRequestsController do
let(:user) { project.owner }
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
let(:merge_request_with_conflicts) do
- create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start', source_project: project) do |mr|
+ create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start', source_project: project, merge_status: :unchecked) do |mr|
mr.mark_as_unmergeable
end
end
@@ -80,6 +80,16 @@ describe Projects::MergeRequestsController do
))
end
end
+
+ context "that is invalid" do
+ let(:merge_request) { create(:invalid_merge_request, target_project: project, source_project: project) }
+
+ it "renders merge request page" do
+ go(format: :html)
+
+ expect(response).to be_success
+ end
+ end
end
describe 'as json' do
@@ -106,6 +116,16 @@ describe Projects::MergeRequestsController do
expect(response).to match_response_schema('entities/merge_request_widget')
end
end
+
+ context "that is invalid" do
+ let(:merge_request) { create(:invalid_merge_request, target_project: project, source_project: project) }
+
+ it "renders merge request page" do
+ go(format: :json)
+
+ expect(response).to be_success
+ end
+ end
end
describe "as diff" do
@@ -214,7 +234,7 @@ describe Projects::MergeRequestsController do
body = JSON.parse(response.body)
expect(body['assignee'].keys)
- .to match_array(%w(name username avatar_url))
+ .to match_array(%w(name username avatar_url id state web_url))
end
end
@@ -275,6 +295,7 @@ describe Projects::MergeRequestsController do
namespace_id: project.namespace,
project_id: project,
id: merge_request.iid,
+ squash: false,
format: 'json'
}
end
@@ -294,7 +315,7 @@ describe Projects::MergeRequestsController do
context 'when the merge request is not mergeable' do
before do
- merge_request.update_attributes(title: "WIP: #{merge_request.title}")
+ merge_request.update(title: "WIP: #{merge_request.title}")
post :merge, base_params
end
@@ -315,8 +336,13 @@ describe Projects::MergeRequestsController do
end
context 'when the sha parameter matches the source SHA' do
- def merge_with_sha
- post :merge, base_params.merge(sha: merge_request.diff_head_sha)
+ def merge_with_sha(params = {})
+ post_params = base_params.merge(sha: merge_request.diff_head_sha).merge(params)
+ if Gitlab.rails5?
+ post :merge, params: post_params, as: :json
+ else
+ post :merge, post_params
+ end
end
it 'returns :success' do
@@ -325,12 +351,30 @@ describe Projects::MergeRequestsController do
expect(json_response).to eq('status' => 'success')
end
- it 'starts the merge immediately' do
- expect(MergeWorker).to receive(:perform_async).with(merge_request.id, anything, anything)
+ it 'starts the merge immediately with permitted params' do
+ expect(MergeWorker).to receive(:perform_async).with(merge_request.id, anything, { 'squash' => false })
merge_with_sha
end
+ context 'when squash is passed as 1' do
+ it 'updates the squash attribute on the MR to true' do
+ merge_request.update(squash: false)
+ merge_with_sha(squash: '1')
+
+ expect(merge_request.reload.squash).to be_truthy
+ end
+ end
+
+ context 'when squash is passed as 0' do
+ it 'updates the squash attribute on the MR to false' do
+ merge_request.update(squash: true)
+ merge_with_sha(squash: '0')
+
+ expect(merge_request.reload.squash).to be_falsey
+ end
+ end
+
context 'when the pipeline succeeds is passed' do
let!(:head_pipeline) do
create(:ci_empty_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch, head_pipeline_of: merge_request)
@@ -662,7 +706,7 @@ describe Projects::MergeRequestsController do
expect(json_response['text']).to eq status.text
expect(json_response['label']).to eq status.label
expect(json_response['icon']).to eq status.icon
- expect(json_response['favicon']).to match_asset_path "/assets/ci_favicons/#{status.favicon}.ico"
+ expect(json_response['favicon']).to match_asset_path "/assets/ci_favicons/#{status.favicon}.png"
end
end
diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb
index 548c5ef36e7..ea906cf7f32 100644
--- a/spec/controllers/projects/milestones_controller_spec.rb
+++ b/spec/controllers/projects/milestones_controller_spec.rb
@@ -11,7 +11,7 @@ describe Projects::MilestonesController do
before do
sign_in(user)
- project.add_master(user)
+ project.add_maintainer(user)
controller.instance_variable_set(:@project, project)
end
@@ -57,19 +57,36 @@ describe Projects::MilestonesController do
context "as json" do
let!(:group) { create(:group, :public) }
let!(:group_milestone) { create(:milestone, group: group) }
- let!(:group_member) { create(:group_member, group: group, user: user) }
- before do
- project.update(namespace: group)
- get :index, namespace_id: project.namespace.id, project_id: project.id, format: :json
+ context 'with a single group ancestor' do
+ before do
+ project.update(namespace: group)
+ get :index, namespace_id: project.namespace.id, project_id: project.id, format: :json
+ end
+
+ it "queries projects milestones and groups milestones" do
+ milestones = assigns(:milestones)
+
+ expect(milestones.count).to eq(2)
+ expect(milestones).to match_array([milestone, group_milestone])
+ end
end
- it "queries projects milestones and groups milestones" do
- milestones = assigns(:milestones)
+ context 'with nested groups', :nested_groups do
+ let!(:subgroup) { create(:group, :public, parent: group) }
+ let!(:subgroup_milestone) { create(:milestone, group: subgroup) }
+
+ before do
+ project.update(namespace: subgroup)
+ get :index, namespace_id: project.namespace.id, project_id: project.id, format: :json
+ end
+
+ it "queries projects milestones and all ancestors milestones" do
+ milestones = assigns(:milestones)
- expect(milestones.count).to eq(2)
- expect(milestones.where(project_id: nil).first).to eq(group_milestone)
- expect(milestones.where(group_id: nil).first).to eq(milestone)
+ expect(milestones.count).to eq(3)
+ expect(milestones).to match_array([milestone, group_milestone, subgroup_milestone])
+ end
end
end
end
@@ -107,7 +124,7 @@ describe Projects::MilestonesController do
it 'shows group milestone' do
post :promote, namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid
- expect(flash[:notice]).to eq("#{milestone.title} promoted to <a href=\"#{group_milestone_path(project.group, milestone.iid)}\">group milestone</a>.")
+ expect(flash[:notice]).to eq("#{milestone.title} promoted to <a href=\"#{group_milestone_path(project.group, milestone.iid)}\"><u>group milestone</u></a>.")
expect(response).to redirect_to(project_milestones_path(project))
end
end
diff --git a/spec/controllers/projects/mirrors_controller_spec.rb b/spec/controllers/projects/mirrors_controller_spec.rb
index 45c1218a39c..5d64f362252 100644
--- a/spec/controllers/projects/mirrors_controller_spec.rb
+++ b/spec/controllers/projects/mirrors_controller_spec.rb
@@ -54,7 +54,7 @@ describe Projects::MirrorsController do
do_put(project, remote_mirrors_attributes: remote_mirror_attributes)
expect(response).to redirect_to(project_settings_repository_path(project))
- expect(flash[:alert]).to match(/must be a valid URL/)
+ expect(flash[:alert]).to match(/Only allowed protocols are/)
end
it 'should not create a RemoteMirror object' do
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index de132dfaa21..1458113b90c 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -51,7 +51,7 @@ describe Projects::NotesController do
let(:project) { create(:project, :repository) }
let!(:note) { create(:discussion_note_on_merge_request, project: project) }
- let(:params) { request_params.merge(target_type: 'merge_request', target_id: note.noteable_id) }
+ let(:params) { request_params.merge(target_type: 'merge_request', target_id: note.noteable_id, html: true) }
it 'responds with the expected attributes' do
get :index, params
@@ -67,7 +67,7 @@ describe Projects::NotesController do
let(:project) { create(:project, :repository) }
let!(:note) { create(:diff_note_on_merge_request, project: project) }
- let(:params) { request_params.merge(target_type: 'merge_request', target_id: note.noteable_id) }
+ let(:params) { request_params.merge(target_type: 'merge_request', target_id: note.noteable_id, html: true) }
it 'responds with the expected attributes' do
get :index, params
@@ -86,7 +86,7 @@ describe Projects::NotesController do
context 'when displayed on a merge request' do
let(:merge_request) { create(:merge_request, source_project: project) }
- let(:params) { request_params.merge(target_type: 'merge_request', target_id: merge_request.id) }
+ let(:params) { request_params.merge(target_type: 'merge_request', target_id: merge_request.id, html: true) }
it 'responds with the expected attributes' do
get :index, params
@@ -99,7 +99,7 @@ describe Projects::NotesController do
end
context 'when displayed on the commit' do
- let(:params) { request_params.merge(target_type: 'commit', target_id: note.commit_id) }
+ let(:params) { request_params.merge(target_type: 'commit', target_id: note.commit_id, html: true) }
it 'responds with the expected attributes' do
get :index, params
@@ -128,7 +128,7 @@ describe Projects::NotesController do
context 'for a regular note' do
let!(:note) { create(:note_on_merge_request, project: project) }
- let(:params) { request_params.merge(target_type: 'merge_request', target_id: note.noteable_id) }
+ let(:params) { request_params.merge(target_type: 'merge_request', target_id: note.noteable_id, html: true) }
it 'responds with the expected attributes' do
get :index, params
@@ -293,7 +293,7 @@ describe Projects::NotesController do
context 'when a noteable is not found' do
it 'returns 404 status' do
- request_params[:note][:noteable_id] = 9999
+ request_params[:target_id] = 9999
post :create, request_params.merge(format: :json)
expect(response).to have_gitlab_http_status(404)
@@ -475,7 +475,7 @@ describe Projects::NotesController do
end
it "returns the name of the resolving user" do
- post :resolve, request_params
+ post :resolve, request_params.merge(html: true)
expect(JSON.parse(response.body)["resolved_by"]).to eq(user.name)
end
diff --git a/spec/controllers/projects/pages_controller_spec.rb b/spec/controllers/projects/pages_controller_spec.rb
index 11f54eef531..927b6e0c473 100644
--- a/spec/controllers/projects/pages_controller_spec.rb
+++ b/spec/controllers/projects/pages_controller_spec.rb
@@ -14,7 +14,7 @@ describe Projects::PagesController do
before do
allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
sign_in(user)
- project.add_master(user)
+ project.add_maintainer(user)
end
describe 'GET show' do
@@ -71,7 +71,7 @@ describe Projects::PagesController do
{
namespace_id: project.namespace,
project_id: project,
- project: { pages_https_only: false }
+ project: { pages_https_only: 'false' }
}
end
@@ -96,7 +96,7 @@ describe Projects::PagesController do
it 'calls the update service' do
expect(Projects::UpdateService)
.to receive(:new)
- .with(project, user, request_params[:project])
+ .with(project, user, ActionController::Parameters.new(request_params[:project]).permit!)
.and_return(update_service)
patch :update, request_params
diff --git a/spec/controllers/projects/pages_domains_controller_spec.rb b/spec/controllers/projects/pages_domains_controller_spec.rb
index d4058a5c515..75871eab1ab 100644
--- a/spec/controllers/projects/pages_domains_controller_spec.rb
+++ b/spec/controllers/projects/pages_domains_controller_spec.rb
@@ -19,7 +19,7 @@ describe Projects::PagesDomainsController do
before do
allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
sign_in(user)
- project.add_master(user)
+ project.add_maintainer(user)
end
describe 'GET show' do
diff --git a/spec/controllers/projects/pipeline_schedules_controller_spec.rb b/spec/controllers/projects/pipeline_schedules_controller_spec.rb
index 3506305f755..7179423dde2 100644
--- a/spec/controllers/projects/pipeline_schedules_controller_spec.rb
+++ b/spec/controllers/projects/pipeline_schedules_controller_spec.rb
@@ -121,7 +121,7 @@ describe Projects::PipelineSchedulesController 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_allowed_for(:maintainer).of(project) }
it { expect { go }.to be_allowed_for(:developer).of(project) }
it { expect { go }.to be_denied_for(:reporter).of(project) }
it { expect { go }.to be_denied_for(:guest).of(project) }
@@ -274,7 +274,7 @@ describe Projects::PipelineSchedulesController 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_allowed_for(:maintainer).of(project) }
it { expect { go }.to be_allowed_for(:developer).of(project).own(pipeline_schedule) }
it { expect { go }.to be_denied_for(:reporter).of(project) }
it { expect { go }.to be_denied_for(:guest).of(project) }
@@ -292,27 +292,37 @@ describe Projects::PipelineSchedulesController do
it { expect { go }.to be_allowed_for(developer_1) }
it { expect { go }.to be_denied_for(:developer).of(project) }
- it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_allowed_for(:maintainer).of(project) }
end
- context 'when a master created a pipeline schedule' do
- let(:master_1) { create(:user) }
- let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: master_1) }
+ context 'when a maintainer created a pipeline schedule' do
+ let(:maintainer_1) { create(:user) }
+ let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: maintainer_1) }
before do
- project.add_master(master_1)
+ project.add_maintainer(maintainer_1)
end
- it { expect { go }.to be_allowed_for(master_1) }
- it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_allowed_for(maintainer_1) }
+ it { expect { go }.to be_allowed_for(:maintainer).of(project) }
it { expect { go }.to be_denied_for(:developer).of(project) }
end
end
def go
- put :update, namespace_id: project.namespace.to_param,
- project_id: project, id: pipeline_schedule,
- schedule: schedule
+ if Gitlab.rails5?
+ put :update, params: { namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: pipeline_schedule,
+ schedule: schedule },
+ as: :html
+
+ else
+ put :update, namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: pipeline_schedule,
+ schedule: schedule
+ end
end
end
@@ -321,7 +331,7 @@ describe Projects::PipelineSchedulesController do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
@@ -336,7 +346,7 @@ describe Projects::PipelineSchedulesController do
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_allowed_for(:maintainer).of(project) }
it { expect { go }.to be_allowed_for(:developer).of(project).own(pipeline_schedule) }
it { expect { go }.to be_denied_for(:reporter).of(project) }
it { expect { go }.to be_denied_for(:guest).of(project) }
@@ -354,7 +364,7 @@ describe Projects::PipelineSchedulesController do
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_allowed_for(:maintainer).of(project) }
it { expect { go }.to be_allowed_for(:developer).of(project).own(pipeline_schedule) }
it { expect { go }.to be_denied_for(:reporter).of(project) }
it { expect { go }.to be_denied_for(:guest).of(project) }
@@ -443,9 +453,9 @@ describe Projects::PipelineSchedulesController do
end
end
- context 'when a master makes the request' do
+ context 'when a maintainer makes the request' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index 9e7bc20a6d1..290fcd4f8e6 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -4,7 +4,7 @@ describe Projects::PipelinesController do
include ApiHelpers
set(:user) { create(:user) }
- set(:project) { create(:project, :public, :repository) }
+ let(:project) { create(:project, :public, :repository) }
let(:feature) { ProjectFeature::DISABLED }
before do
@@ -17,44 +17,121 @@ describe Projects::PipelinesController do
describe 'GET index.json' do
before do
- %w(pending running created success).each_with_index do |status, index|
- sha = project.commit("HEAD~#{index}")
- create(:ci_empty_pipeline, status: status, project: project, sha: sha)
+ %w(pending running success failed canceled).each_with_index do |status, index|
+ create_pipeline(status, project.commit("HEAD~#{index}"))
end
end
- subject do
- get :index, namespace_id: project.namespace, project_id: project, format: :json
+ context 'when using persisted stages', :request_store do
+ before do
+ stub_feature_flags(ci_pipeline_persisted_stages: true)
+ end
+
+ it 'returns serialized pipelines', :request_store do
+ queries = ActiveRecord::QueryRecorder.new do
+ get_pipelines_index_json
+ end
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('pipeline')
+
+ expect(json_response).to include('pipelines')
+ expect(json_response['pipelines'].count).to eq 5
+ expect(json_response['count']['all']).to eq '5'
+ expect(json_response['count']['running']).to eq '1'
+ expect(json_response['count']['pending']).to eq '1'
+ expect(json_response['count']['finished']).to eq '3'
+
+ json_response.dig('pipelines', 0, 'details', 'stages').tap do |stages|
+ expect(stages.count).to eq 3
+ end
+
+ expect(queries.count).to be
+ end
end
- it 'returns JSON with serialized pipelines' do
- subject
+ context 'when using legacy stages', :request_store do
+ before do
+ stub_feature_flags(ci_pipeline_persisted_stages: false)
+ end
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('pipeline')
+ it 'returns JSON with serialized pipelines', :request_store do
+ queries = ActiveRecord::QueryRecorder.new do
+ get_pipelines_index_json
+ end
- expect(json_response).to include('pipelines')
- expect(json_response['pipelines'].count).to eq 4
- expect(json_response['count']['all']).to eq '4'
- expect(json_response['count']['running']).to eq '1'
- expect(json_response['count']['pending']).to eq '1'
- expect(json_response['count']['finished']).to eq '1'
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('pipeline')
+
+ expect(json_response).to include('pipelines')
+ expect(json_response['pipelines'].count).to eq 5
+ expect(json_response['count']['all']).to eq '5'
+ expect(json_response['count']['running']).to eq '1'
+ expect(json_response['count']['pending']).to eq '1'
+ expect(json_response['count']['finished']).to eq '3'
+
+ json_response.dig('pipelines', 0, 'details', 'stages').tap do |stages|
+ expect(stages.count).to eq 3
+ end
+
+ expect(queries.count).to be_within(5).of(30)
+ end
end
it 'does not include coverage data for the pipelines' do
- subject
+ get_pipelines_index_json
expect(json_response['pipelines'][0]).not_to include('coverage')
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(3)
+ expect { get_pipelines_index_json }
+ .to change { Gitlab::GitalyClient.get_request_count }.by(2)
+ end
+ end
+
+ context 'when the project is private' do
+ let(:project) { create(:project, :private, :repository) }
+
+ it 'returns `not_found` when the user does not have access' do
+ sign_in(create(:user))
+
+ get_pipelines_index_json
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ it 'returns the pipelines when the user has access' do
+ get_pipelines_index_json
+
+ expect(json_response['pipelines'].size).to eq(5)
end
end
+
+ def get_pipelines_index_json
+ get :index, namespace_id: project.namespace,
+ project_id: project,
+ format: :json
+ end
+
+ def create_pipeline(status, sha)
+ pipeline = create(:ci_empty_pipeline, status: status,
+ project: project,
+ sha: sha)
+
+ create_build(pipeline, 'build', 1, 'build')
+ create_build(pipeline, 'test', 2, 'test')
+ create_build(pipeline, 'deploy', 3, 'deploy')
+ end
+
+ def create_build(pipeline, stage, stage_idx, name)
+ status = %w[created running pending success failed canceled].sample
+ create(:ci_build, pipeline: pipeline, stage: stage, stage_idx: stage_idx, name: name, status: status)
+ end
end
- describe 'GET show JSON' do
+ describe 'GET show.json' do
let(:pipeline) { create(:ci_pipeline_with_one_job, project: project) }
it 'returns the pipeline' do
@@ -67,6 +144,14 @@ describe Projects::PipelinesController do
end
context 'when the pipeline has multiple stages and groups', :request_store do
+ let(:project) { create(:project, :repository) }
+
+ let(:pipeline) do
+ create(:ci_empty_pipeline, project: project,
+ user: user,
+ sha: project.commit.id)
+ end
+
before do
create_build('build', 0, 'build')
create_build('test', 1, 'rspec 0')
@@ -74,11 +159,6 @@ describe Projects::PipelinesController do
create_build('post deploy', 3, 'pages 0')
end
- let(:project) { create(:project, :repository) }
- let(:pipeline) do
- create(:ci_empty_pipeline, project: project, user: user, sha: project.commit.id)
- end
-
it 'does not perform N + 1 queries' do
control_count = ActiveRecord::QueryRecorder.new { get_pipeline_json }.count
@@ -90,6 +170,7 @@ describe Projects::PipelinesController do
create_build('post deploy', 3, 'pages 2')
new_count = ActiveRecord::QueryRecorder.new { get_pipeline_json }.count
+
expect(new_count).to be_within(12).of(control_count)
end
end
@@ -190,7 +271,7 @@ describe Projects::PipelinesController do
expect(json_response['text']).to eq status.text
expect(json_response['label']).to eq status.label
expect(json_response['icon']).to eq status.icon
- expect(json_response['favicon']).to match_asset_path("/assets/ci_favicons/#{status.favicon}.ico")
+ expect(json_response['favicon']).to match_asset_path("/assets/ci_favicons/#{status.favicon}.png")
end
end
diff --git a/spec/controllers/projects/pipelines_settings_controller_spec.rb b/spec/controllers/projects/pipelines_settings_controller_spec.rb
index 694896b6bcf..b1ba9f74e38 100644
--- a/spec/controllers/projects/pipelines_settings_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_settings_controller_spec.rb
@@ -6,7 +6,7 @@ describe Projects::PipelinesSettingsController do
let(:project) { project_auto_devops.project }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb
index 46b08a03b19..519af10d78c 100644
--- a/spec/controllers/projects/project_members_controller_spec.rb
+++ b/spec/controllers/projects/project_members_controller_spec.rb
@@ -37,7 +37,7 @@ describe Projects::ProjectMembersController do
context 'when user has enough rights' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
it 'adds user to members' do
@@ -70,7 +70,7 @@ describe Projects::ProjectMembersController do
let(:requester) { create(:project_member, :access_request, project: project) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
@@ -121,7 +121,7 @@ describe Projects::ProjectMembersController do
context 'when user has enough rights' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
it '[HTML] removes user from members' do
@@ -181,10 +181,10 @@ describe Projects::ProjectMembersController do
let(:project) { create(:project, namespace: user.namespace) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
- it 'cannot remove himself from the project' do
+ it 'cannot remove themselves from the project' do
delete :leave, namespace_id: project.namespace,
project_id: project
@@ -263,7 +263,7 @@ describe Projects::ProjectMembersController do
context 'when user has enough rights' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
it 'adds user to members' do
@@ -285,7 +285,7 @@ describe Projects::ProjectMembersController do
let(:member) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
another_project.add_guest(member)
sign_in(user)
end
@@ -332,7 +332,7 @@ describe Projects::ProjectMembersController do
context 'when creating owner' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
@@ -346,9 +346,9 @@ describe Projects::ProjectMembersController do
end
end
- context 'when create master' do
+ context 'when create maintainer' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
@@ -356,7 +356,7 @@ describe Projects::ProjectMembersController do
expect do
post :create, user_ids: stranger.id,
namespace_id: project.namespace,
- access_level: Member::MASTER,
+ access_level: Member::MAINTAINER,
project_id: project
end.to change { project.members.count }.by(1)
end
diff --git a/spec/controllers/projects/prometheus/metrics_controller_spec.rb b/spec/controllers/projects/prometheus/metrics_controller_spec.rb
index 871dcf5c796..5c56a712245 100644
--- a/spec/controllers/projects/prometheus/metrics_controller_spec.rb
+++ b/spec/controllers/projects/prometheus/metrics_controller_spec.rb
@@ -7,7 +7,7 @@ describe Projects::Prometheus::MetricsController do
let(:prometheus_adapter) { double('prometheus_adapter', can_query?: true) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/controllers/projects/protected_branches_controller_spec.rb b/spec/controllers/projects/protected_branches_controller_spec.rb
index 096e29bc39f..ac812707e74 100644
--- a/spec/controllers/projects/protected_branches_controller_spec.rb
+++ b/spec/controllers/projects/protected_branches_controller_spec.rb
@@ -8,7 +8,7 @@ describe Projects::ProtectedBranchesController do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
describe "GET #index" do
@@ -20,10 +20,10 @@ describe Projects::ProtectedBranchesController do
end
describe "POST #create" do
- let(:master_access_level) { [{ access_level: Gitlab::Access::MASTER }] }
+ let(:maintainer_access_level) { [{ access_level: Gitlab::Access::MAINTAINER }] }
let(:access_level_params) do
- { merge_access_levels_attributes: master_access_level,
- push_access_levels_attributes: master_access_level }
+ { merge_access_levels_attributes: maintainer_access_level,
+ push_access_levels_attributes: maintainer_access_level }
end
let(:create_params) { attributes_for(:protected_branch).merge(access_level_params) }
diff --git a/spec/controllers/projects/protected_tags_controller_spec.rb b/spec/controllers/projects/protected_tags_controller_spec.rb
index b6de90039f3..20440c5a5d5 100644
--- a/spec/controllers/projects/protected_tags_controller_spec.rb
+++ b/spec/controllers/projects/protected_tags_controller_spec.rb
@@ -15,7 +15,7 @@ describe Projects::ProtectedTagsController do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/controllers/projects/runners_controller_spec.rb b/spec/controllers/projects/runners_controller_spec.rb
index 89a13f3c976..b1e0b496ede 100644
--- a/spec/controllers/projects/runners_controller_spec.rb
+++ b/spec/controllers/projects/runners_controller_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Projects::RunnersController do
let(:user) { create(:user) }
let(:project) { create(:project) }
- let(:runner) { create(:ci_runner) }
+ let(:runner) { create(:ci_runner, :project, projects: [project]) }
let(:params) do
{
@@ -15,8 +15,7 @@ describe Projects::RunnersController do
before do
sign_in(user)
- project.add_master(user)
- project.runners << runner
+ project.add_maintainer(user)
end
describe '#update' do
diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb
index e4dc61b3a68..45cea8c1351 100644
--- a/spec/controllers/projects/services_controller_spec.rb
+++ b/spec/controllers/projects/services_controller_spec.rb
@@ -9,7 +9,7 @@ describe Projects::ServicesController do
before do
sign_in(user)
- project.add_master(user)
+ project.add_maintainer(user)
end
describe '#test' do
@@ -102,7 +102,7 @@ describe Projects::ServicesController do
expect(response.status).to eq(200)
expect(JSON.parse(response.body))
- .to eq('error' => true, 'message' => 'Test failed.', 'service_response' => 'Bad test')
+ .to eq('error' => true, 'message' => 'Test failed.', 'service_response' => 'Bad test', 'test_failed' => true)
end
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 f1810763d2d..1f14a0cc381 100644
--- a/spec/controllers/projects/settings/ci_cd_controller_spec.rb
+++ b/spec/controllers/projects/settings/ci_cd_controller_spec.rb
@@ -6,7 +6,7 @@ describe Projects::Settings::CiCdController do
let(:project) { project_auto_devops.project }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
@@ -19,15 +19,15 @@ describe Projects::Settings::CiCdController do
end
context 'with group runners' do
- let(:group_runner) { create(:ci_runner, runner_type: :group_type) }
let(:parent_group) { create(:group) }
- let(:group) { create(:group, runners: [group_runner], parent: parent_group) }
+ let(:group) { create(:group, parent: parent_group) }
+ let(:group_runner) { create(:ci_runner, :group, groups: [group]) }
let(:other_project) { create(:project, group: group) }
- let!(:project_runner) { create(:ci_runner, projects: [other_project], runner_type: :project_type) }
- let!(:shared_runner) { create(:ci_runner, :shared) }
+ let!(:project_runner) { create(:ci_runner, :project, projects: [other_project]) }
+ let!(:shared_runner) { create(:ci_runner, :instance) }
it 'sets assignable project runners only' do
- group.add_master(user)
+ group.add_maintainer(user)
get :show, namespace_id: project.namespace, project_id: project
@@ -40,7 +40,7 @@ describe Projects::Settings::CiCdController do
before do
sign_in(user)
- project.add_master(user)
+ project.add_maintainer(user)
allow(ResetProjectCacheService).to receive_message_chain(:new, :execute).and_return(true)
end
diff --git a/spec/controllers/projects/settings/integrations_controller_spec.rb b/spec/controllers/projects/settings/integrations_controller_spec.rb
index 77df9a6f567..a2484c04c7a 100644
--- a/spec/controllers/projects/settings/integrations_controller_spec.rb
+++ b/spec/controllers/projects/settings/integrations_controller_spec.rb
@@ -5,7 +5,7 @@ describe Projects::Settings::IntegrationsController do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/controllers/projects/settings/repository_controller_spec.rb b/spec/controllers/projects/settings/repository_controller_spec.rb
index 3a4014b7768..9cee40b7553 100644
--- a/spec/controllers/projects/settings/repository_controller_spec.rb
+++ b/spec/controllers/projects/settings/repository_controller_spec.rb
@@ -5,7 +5,7 @@ describe Projects::Settings::RepositoryController do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb
index e7c0b484ede..9c383bd7628 100644
--- a/spec/controllers/projects/snippets_controller_spec.rb
+++ b/spec/controllers/projects/snippets_controller_spec.rb
@@ -6,8 +6,8 @@ describe Projects::SnippetsController do
let(:user2) { create(:user) }
before do
- project.add_master(user)
- project.add_master(user2)
+ project.add_maintainer(user)
+ project.add_maintainer(user2)
end
describe 'GET #index' do
@@ -291,7 +291,7 @@ describe Projects::SnippetsController do
def mark_as_spam
admin = create(:admin)
create(:user_agent_detail, subject: snippet)
- project.add_master(admin)
+ project.add_maintainer(admin)
sign_in(admin)
post :mark_as_spam,
diff --git a/spec/controllers/projects/templates_controller_spec.rb b/spec/controllers/projects/templates_controller_spec.rb
index 8fcfa3c9ecd..d7f07aa2b01 100644
--- a/spec/controllers/projects/templates_controller_spec.rb
+++ b/spec/controllers/projects/templates_controller_spec.rb
@@ -13,7 +13,7 @@ describe Projects::TemplatesController do
end
before do
- project.add_user(user, Gitlab::Access::MASTER)
+ project.add_user(user, Gitlab::Access::MAINTAINER)
project.repository.create_file(user, file_path_1, 'something valid',
message: 'test 3', branch_name: 'master')
end
diff --git a/spec/controllers/projects/tree_controller_spec.rb b/spec/controllers/projects/tree_controller_spec.rb
index d3188f054cf..9982b49eebb 100644
--- a/spec/controllers/projects/tree_controller_spec.rb
+++ b/spec/controllers/projects/tree_controller_spec.rb
@@ -7,7 +7,7 @@ describe Projects::TreeController do
before do
sign_in(user)
- project.add_master(user)
+ project.add_maintainer(user)
controller.instance_variable_set(:@project, project)
end
diff --git a/spec/controllers/projects/uploads_controller_spec.rb b/spec/controllers/projects/uploads_controller_spec.rb
index eca9baed9c9..325ee53aafb 100644
--- a/spec/controllers/projects/uploads_controller_spec.rb
+++ b/spec/controllers/projects/uploads_controller_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Projects::UploadsController do
+ include WorkhorseHelpers
+
let(:model) { create(:project, :public) }
let(:params) do
{ namespace_id: model.namespace.to_param, project_id: model }
@@ -15,4 +17,10 @@ describe Projects::UploadsController do
expect(response).to redirect_to(new_user_session_path)
end
end
+
+ def post_authorize(verified: true)
+ request.headers.merge!(workhorse_internal_api_request_header) if verified
+
+ post :authorize, namespace_id: model.namespace, project_id: model.path, format: :json
+ end
end
diff --git a/spec/controllers/projects/variables_controller_spec.rb b/spec/controllers/projects/variables_controller_spec.rb
index 68019743be0..9afd1f751c6 100644
--- a/spec/controllers/projects/variables_controller_spec.rb
+++ b/spec/controllers/projects/variables_controller_spec.rb
@@ -6,7 +6,7 @@ describe Projects::VariablesController do
before do
sign_in(user)
- project.add_master(user)
+ project.add_maintainer(user)
end
describe 'GET #show' do
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 994da3cd159..94644b1f9fd 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -6,8 +6,8 @@ describe ProjectsController do
let(:project) { create(:project) }
let(:public_project) { create(:project, :public) }
let(:user) { create(:user) }
- let(:jpg) { fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg') }
- let(:txt) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') }
+ let(:jpg) { fixture_file_upload('spec/fixtures/rails_sample.jpg', 'image/jpg') }
+ let(:txt) { fixture_file_upload('spec/fixtures/doc_sample.txt', 'text/plain') }
describe 'GET new' do
context 'with an authenticated user' do
@@ -166,7 +166,7 @@ describe ProjectsController do
User.project_views.keys.each do |project_view|
context "with #{project_view} view set" do
before do
- user.update_attributes(project_view: project_view)
+ user.update(project_view: project_view)
get :show, namespace_id: empty_project.namespace, id: empty_project
end
@@ -188,7 +188,7 @@ describe ProjectsController do
User.project_views.keys.each do |project_view|
context "with #{project_view} view set" do
before do
- user.update_attributes(project_view: project_view)
+ user.update(project_view: project_view)
get :show, namespace_id: empty_project.namespace, id: empty_project
end
@@ -296,16 +296,22 @@ describe ProjectsController do
shared_examples_for 'updating a project' do
context 'when only renaming a project path' do
it "sets the repository to the right path after a rename" do
- original_repository_path = project.repository.path
+ original_repository_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ project.repository.path
+ end
expect { update_project path: 'renamed_path' }
.to change { project.reload.path }
expect(project.path).to include 'renamed_path'
+ assign_repository_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ assigns(:repository).path
+ end
+
if project.hashed_storage?(:repository)
- expect(assigns(:repository).path).to eq(original_repository_path)
+ expect(assign_repository_path).to eq(original_repository_path)
else
- expect(assigns(:repository).path).to include(project.path)
+ expect(assign_repository_path).to include(project.path)
end
expect(response).to have_gitlab_http_status(302)
@@ -323,7 +329,7 @@ describe ProjectsController do
expect { update_project path: 'renamed_path' }
.not_to change { project.reload.path }
- expect(controller).to set_flash[:alert].to(/container registry tags/)
+ expect(controller).to set_flash.now[:alert].to(/container registry tags/)
expect(response).to have_gitlab_http_status(200)
end
end
@@ -591,16 +597,59 @@ describe ProjectsController do
expect(parsed_body["Tags"]).to include("v1.0.0")
expect(parsed_body["Commits"]).to include("123456")
end
+
+ context "when preferred language is Japanese" do
+ before do
+ user.update!(preferred_language: 'ja')
+ sign_in(user)
+ end
+
+ it "gets a list of branches, tags and commits" do
+ get :refs, namespace_id: public_project.namespace, id: public_project, ref: "123456"
+
+ 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 include("123456")
+ end
+ end
end
describe 'POST #preview_markdown' do
- it 'renders json in a correct format' do
+ before do
sign_in(user)
+ end
+ it 'renders json in a correct format' do
post :preview_markdown, namespace_id: public_project.namespace, id: public_project, text: '*Markdown* text'
expect(JSON.parse(response.body).keys).to match_array(%w(body references))
end
+
+ context 'state filter on references' do
+ let(:issue) { create(:issue, :closed, project: public_project) }
+ let(:merge_request) { create(:merge_request, :closed, target_project: public_project) }
+
+ it 'renders JSON body with state filter for issues' do
+ post :preview_markdown, namespace_id: public_project.namespace,
+ id: public_project,
+ text: issue.to_reference
+
+ json_response = JSON.parse(response.body)
+
+ expect(json_response['body']).to match(/\##{issue.iid} \(closed\)/)
+ end
+
+ it 'renders JSON body with state filter for MRs' do
+ post :preview_markdown, namespace_id: public_project.namespace,
+ id: public_project,
+ text: merge_request.to_reference
+
+ json_response = JSON.parse(response.body)
+
+ expect(json_response['body']).to match(/\!#{merge_request.iid} \(closed\)/)
+ end
+ end
end
describe '#ensure_canonical_path' do
@@ -710,7 +759,7 @@ describe ProjectsController do
before do
sign_in(user)
- project.add_master(user)
+ project.add_maintainer(user)
end
context 'when project export is enabled' do
@@ -738,26 +787,58 @@ describe ProjectsController do
before do
sign_in(user)
- project.add_master(user)
+ project.add_maintainer(user)
end
- context 'when project export is enabled' do
- it 'returns 302' do
- get :download_export, namespace_id: project.namespace, id: project
+ context 'object storage disabled' do
+ before do
+ stub_feature_flags(import_export_object_storage: false)
+ end
- expect(response).to have_gitlab_http_status(302)
+ context 'when project export is enabled' do
+ it 'returns 302' do
+ get :download_export, namespace_id: project.namespace, id: project
+
+ expect(response).to have_gitlab_http_status(302)
+ end
+ end
+
+ context 'when project export is disabled' do
+ before do
+ stub_application_setting(project_export_enabled?: false)
+ end
+
+ it 'returns 404' do
+ get :download_export, namespace_id: project.namespace, id: project
+
+ expect(response).to have_gitlab_http_status(404)
+ end
end
end
- context 'when project export is disabled' do
+ context 'object storage enabled' do
before do
- stub_application_setting(project_export_enabled?: false)
+ stub_feature_flags(import_export_object_storage: true)
end
- it 'returns 404' do
- get :download_export, namespace_id: project.namespace, id: project
+ context 'when project export is enabled' do
+ it 'returns 302' do
+ get :download_export, namespace_id: project.namespace, id: project
- expect(response).to have_gitlab_http_status(404)
+ expect(response).to have_gitlab_http_status(302)
+ end
+ end
+
+ context 'when project export is disabled' do
+ before do
+ stub_application_setting(project_export_enabled?: false)
+ end
+
+ it 'returns 404' do
+ get :download_export, namespace_id: project.namespace, id: project
+
+ expect(response).to have_gitlab_http_status(404)
+ end
end
end
end
@@ -766,7 +847,7 @@ describe ProjectsController do
before do
sign_in(user)
- project.add_master(user)
+ project.add_maintainer(user)
end
context 'when project export is enabled' do
@@ -794,7 +875,7 @@ describe ProjectsController do
before do
sign_in(user)
- project.add_master(user)
+ project.add_maintainer(user)
end
context 'when project export is enabled' do
diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb
index 346944fd5b0..898f3863008 100644
--- a/spec/controllers/registrations_controller_spec.rb
+++ b/spec/controllers/registrations_controller_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe RegistrationsController do
+ include TermsHelper
+
describe '#create' do
let(:user_params) { { user: { name: 'new_user', username: 'new_username', email: 'new@user.com', password: 'Any_password' } } }
@@ -67,6 +69,25 @@ describe RegistrationsController do
expect(flash[:notice]).to include 'Welcome! You have signed up successfully.'
end
end
+
+ context 'when terms are enforced' do
+ before do
+ enforce_terms
+ end
+
+ it 'redirects back with a notice when the checkbox was not checked' do
+ post :create, user_params
+
+ expect(flash[:alert]).to match /you must accept our terms/i
+ end
+
+ it 'creates the user with agreement when terms are accepted' do
+ post :create, user_params.merge(terms_opt_in: '1')
+
+ expect(subject.current_user).to be_present
+ expect(subject.current_user.terms_accepted?).to be(true)
+ end
+ end
end
describe '#destroy' do
diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb
index 30c06ddf744..416a09e1684 100644
--- a/spec/controllers/search_controller_spec.rb
+++ b/spec/controllers/search_controller_spec.rb
@@ -32,7 +32,7 @@ describe SearchController do
it 'still blocks searches without a project_id' do
get :show, search: 'hello'
- expect(response).to have_gitlab_http_status(404)
+ expect(response).to have_gitlab_http_status(403)
end
it 'allows searches with a project_id' do
diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb
index 555b186fe31..7c00652317b 100644
--- a/spec/controllers/sessions_controller_spec.rb
+++ b/spec/controllers/sessions_controller_spec.rb
@@ -53,21 +53,22 @@ describe SessionsController do
include UserActivitiesHelpers
let(:user) { create(:user) }
+ let(:user_params) { { login: user.username, password: user.password } }
it 'authenticates user correctly' do
- post(:create, user: { login: user.username, password: user.password })
+ post(:create, user: user_params)
expect(subject.current_user). to eq user
end
it 'creates an audit log record' do
- expect { post(:create, user: { login: user.username, password: user.password }) }.to change { SecurityEvent.count }.by(1)
+ expect { post(:create, user: user_params) }.to change { SecurityEvent.count }.by(1)
expect(SecurityEvent.last.details[:with]).to eq('standard')
end
include_examples 'user login request with unique ip limit', 302 do
def request
- post(:create, user: { login: user.username, password: user.password })
+ post(:create, user: user_params)
expect(subject.current_user).to eq user
subject.sign_out user
end
@@ -75,10 +76,53 @@ describe SessionsController do
it 'updates the user activity' do
expect do
- post(:create, user: { login: user.username, password: user.password })
+ post(:create, user: user_params)
end.to change { user_activity(user) }
end
end
+
+ context 'when reCAPTCHA is enabled' do
+ let(:user) { create(:user) }
+ let(:user_params) { { login: user.username, password: user.password } }
+
+ before do
+ stub_application_setting(recaptcha_enabled: true)
+ request.headers[described_class::CAPTCHA_HEADER] = 1
+ end
+
+ it 'displays an error when the reCAPTCHA is not solved' do
+ # Without this, `verify_recaptcha` arbitraily returns true in test env
+ Recaptcha.configuration.skip_verify_env.delete('test')
+ counter = double(:counter)
+
+ expect(counter).to receive(:increment)
+ expect(Gitlab::Metrics).to receive(:counter)
+ .with(:failed_login_captcha_total, anything)
+ .and_return(counter)
+
+ post(:create, user: user_params)
+
+ expect(response).to render_template(:new)
+ expect(flash[:alert]).to include 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
+ expect(subject.current_user).to be_nil
+ end
+
+ it 'successfully logs in a user when reCAPTCHA is solved' do
+ # Avoid test ordering issue and ensure `verify_recaptcha` returns true
+ Recaptcha.configuration.skip_verify_env << 'test'
+ counter = double(:counter)
+
+ expect(counter).to receive(:increment)
+ expect(Gitlab::Metrics).to receive(:counter)
+ .with(:successful_login_captcha_total, anything)
+ .and_return(counter)
+ expect(Gitlab::Metrics).to receive(:counter).and_call_original
+
+ post(:create, user: user_params)
+
+ expect(subject.current_user).to eq user
+ end
+ end
end
context 'when using two-factor authentication via OTP' do
@@ -257,15 +301,15 @@ describe SessionsController do
end
end
- describe '#new' do
+ describe "#new" do
before do
set_devise_mapping(context: @request)
end
- it 'redirects correctly for referer on same host with params' do
- search_path = '/search?search=seed_project'
- allow(controller.request).to receive(:referer)
- .and_return('http://%{host}%{path}' % { host: 'test.host', path: search_path })
+ it "redirects correctly for referer on same host with params" do
+ host = "test.host"
+ search_path = "/search?search=seed_project"
+ request.headers[:HTTP_REFERER] = "http://#{host}#{search_path}"
get(:new, redirect_to_referer: :yes)
diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb
index 376b229ffc9..bcf289f36a9 100644
--- a/spec/controllers/uploads_controller_spec.rb
+++ b/spec/controllers/uploads_controller_spec.rb
@@ -6,13 +6,13 @@ shared_examples 'content not cached without revalidation' do
end
describe UploadsController do
- let!(:user) { create(:user, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
+ let!(:user) { create(:user, avatar: fixture_file_upload("spec/fixtures/dk.png", "image/png")) }
describe 'POST create' do
let(:model) { 'personal_snippet' }
let(:snippet) { create(:personal_snippet, :public) }
- let(:jpg) { fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg') }
- let(:txt) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') }
+ let(:jpg) { fixture_file_upload('spec/fixtures/rails_sample.jpg', 'image/jpg') }
+ let(:txt) { fixture_file_upload('spec/fixtures/doc_sample.txt', 'text/plain') }
context 'when a user does not have permissions to upload a file' do
it "returns 401 when the user is not logged in" do
@@ -136,7 +136,7 @@ describe UploadsController do
context 'for PNG files' do
it 'returns Content-Disposition: inline' do
note = create(:note, :with_attachment, project: project)
- get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.png'
+ get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'dk.png'
expect(response['Content-Disposition']).to start_with('inline;')
end
@@ -145,7 +145,7 @@ describe UploadsController do
context 'for SVG files' do
it 'returns Content-Disposition: attachment' do
note = create(:note, :with_svg_attachment, project: project)
- get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.svg'
+ get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'unsanitized.svg'
expect(response['Content-Disposition']).to start_with('attachment;')
end
@@ -164,7 +164,7 @@ describe UploadsController do
end
it "redirects to the sign in page" do
- get :show, model: "user", mounted_as: "avatar", id: user.id, filename: "image.png"
+ get :show, model: "user", mounted_as: "avatar", id: user.id, filename: "dk.png"
expect(response).to redirect_to(new_user_session_path)
end
@@ -172,14 +172,14 @@ describe UploadsController do
context "when the user isn't blocked" do
it "responds with status 200" do
- get :show, model: "user", mounted_as: "avatar", id: user.id, filename: "image.png"
+ get :show, model: "user", mounted_as: "avatar", id: user.id, filename: "dk.png"
expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'content not cached without revalidation' do
subject do
- get :show, model: 'user', mounted_as: 'avatar', id: user.id, filename: 'image.png'
+ get :show, model: 'user', mounted_as: 'avatar', id: user.id, filename: 'dk.png'
response
end
@@ -189,14 +189,14 @@ describe UploadsController do
context "when not signed in" do
it "responds with status 200" do
- get :show, model: "user", mounted_as: "avatar", id: user.id, filename: "image.png"
+ get :show, model: "user", mounted_as: "avatar", id: user.id, filename: "dk.png"
expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'content not cached without revalidation' do
subject do
- get :show, model: 'user', mounted_as: 'avatar', id: user.id, filename: 'image.png'
+ get :show, model: 'user', mounted_as: 'avatar', id: user.id, filename: 'dk.png'
response
end
@@ -205,7 +205,7 @@ describe UploadsController do
end
context "when viewing a project avatar" do
- let!(:project) { create(:project, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
+ let!(:project) { create(:project, avatar: fixture_file_upload("spec/fixtures/dk.png", "image/png")) }
context "when the project is public" do
before do
@@ -214,14 +214,14 @@ describe UploadsController do
context "when not signed in" do
it "responds with status 200" do
- get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png"
+ get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "dk.png"
expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'content not cached without revalidation' do
subject do
- get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'image.png'
+ get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'dk.png'
response
end
@@ -234,14 +234,14 @@ describe UploadsController do
end
it "responds with status 200" do
- get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png"
+ get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "dk.png"
expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'content not cached without revalidation' do
subject do
- get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'image.png'
+ get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'dk.png'
response
end
@@ -256,7 +256,7 @@ describe UploadsController do
context "when not signed in" do
it "redirects to the sign in page" do
- get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png"
+ get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "dk.png"
expect(response).to redirect_to(new_user_session_path)
end
@@ -269,17 +269,17 @@ describe UploadsController do
context "when the user has access to the project" do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
context "when the user is blocked" do
before do
user.block
- project.add_master(user)
+ project.add_maintainer(user)
end
it "redirects to the sign in page" do
- get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png"
+ get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "dk.png"
expect(response).to redirect_to(new_user_session_path)
end
@@ -287,14 +287,14 @@ describe UploadsController do
context "when the user isn't blocked" do
it "responds with status 200" do
- get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png"
+ get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "dk.png"
expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'content not cached without revalidation' do
subject do
- get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'image.png'
+ get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'dk.png'
response
end
@@ -304,7 +304,7 @@ describe UploadsController do
context "when the user doesn't have access to the project" do
it "responds with status 404" do
- get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png"
+ get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "dk.png"
expect(response).to have_gitlab_http_status(404)
end
@@ -314,19 +314,19 @@ describe UploadsController do
end
context "when viewing a group avatar" do
- let!(:group) { create(:group, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
+ let!(:group) { create(:group, avatar: fixture_file_upload("spec/fixtures/dk.png", "image/png")) }
context "when the group is public" do
context "when not signed in" do
it "responds with status 200" do
- get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png"
+ get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "dk.png"
expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'content not cached without revalidation' do
subject do
- get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'image.png'
+ get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'dk.png'
response
end
@@ -339,14 +339,14 @@ describe UploadsController do
end
it "responds with status 200" do
- get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png"
+ get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "dk.png"
expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'content not cached without revalidation' do
subject do
- get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'image.png'
+ get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'dk.png'
response
end
@@ -375,7 +375,7 @@ describe UploadsController do
end
it "redirects to the sign in page" do
- get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png"
+ get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "dk.png"
expect(response).to redirect_to(new_user_session_path)
end
@@ -383,14 +383,14 @@ describe UploadsController do
context "when the user isn't blocked" do
it "responds with status 200" do
- get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png"
+ get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "dk.png"
expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'content not cached without revalidation' do
subject do
- get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'image.png'
+ get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'dk.png'
response
end
@@ -400,7 +400,7 @@ describe UploadsController do
context "when the user doesn't have access to the project" do
it "responds with status 404" do
- get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png"
+ get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "dk.png"
expect(response).to have_gitlab_http_status(404)
end
@@ -420,14 +420,14 @@ describe UploadsController do
context "when not signed in" do
it "responds with status 200" do
- get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png"
+ get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "dk.png"
expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'content not cached without revalidation' do
subject do
- get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.png'
+ get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'dk.png'
response
end
@@ -440,14 +440,14 @@ describe UploadsController do
end
it "responds with status 200" do
- get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png"
+ get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "dk.png"
expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'content not cached without revalidation' do
subject do
- get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.png'
+ get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'dk.png'
response
end
@@ -462,7 +462,7 @@ describe UploadsController do
context "when not signed in" do
it "redirects to the sign in page" do
- get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png"
+ get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "dk.png"
expect(response).to redirect_to(new_user_session_path)
end
@@ -475,17 +475,17 @@ describe UploadsController do
context "when the user has access to the project" do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
context "when the user is blocked" do
before do
user.block
- project.add_master(user)
+ project.add_maintainer(user)
end
it "redirects to the sign in page" do
- get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png"
+ get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "dk.png"
expect(response).to redirect_to(new_user_session_path)
end
@@ -493,14 +493,14 @@ describe UploadsController do
context "when the user isn't blocked" do
it "responds with status 200" do
- get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png"
+ get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "dk.png"
expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'content not cached without revalidation' do
subject do
- get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.png'
+ get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'dk.png'
response
end
@@ -510,7 +510,7 @@ describe UploadsController do
context "when the user doesn't have access to the project" do
it "responds with status 404" do
- get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png"
+ get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "dk.png"
expect(response).to have_gitlab_http_status(404)
end
@@ -521,7 +521,7 @@ describe UploadsController do
context 'Appearance' do
context 'when viewing a custom header logo' do
- let!(:appearance) { create :appearance, header_logo: fixture_file_upload(Rails.root.join('spec/fixtures/dk.png'), 'image/png') }
+ let!(:appearance) { create :appearance, header_logo: fixture_file_upload('spec/fixtures/dk.png', 'image/png') }
context 'when not signed in' do
it 'responds with status 200' do
@@ -541,7 +541,7 @@ describe UploadsController do
end
context 'when viewing a custom logo' do
- let!(:appearance) { create :appearance, logo: fixture_file_upload(Rails.root.join('spec/fixtures/dk.png'), 'image/png') }
+ let!(:appearance) { create :appearance, logo: fixture_file_upload('spec/fixtures/dk.png', 'image/png') }
context 'when not signed in' do
it 'responds with status 200' do
@@ -560,5 +560,26 @@ describe UploadsController do
end
end
end
+
+ context 'original filename or a version filename must match' do
+ let!(:appearance) { create :appearance, favicon: fixture_file_upload('spec/fixtures/dk.png', 'image/png') }
+
+ context 'has a valid filename on the original file' do
+ it 'successfully returns the file' do
+ get :show, model: 'appearance', mounted_as: 'favicon', id: appearance.id, filename: 'dk.png'
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response.header['Content-Disposition']).to end_with 'filename="dk.png"'
+ end
+ end
+
+ context 'has an invalid filename on the original file' do
+ it 'returns a 404' do
+ get :show, model: 'appearance', mounted_as: 'favicon', id: appearance.id, filename: 'bogus.png'
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
end
end
diff --git a/spec/controllers/users/terms_controller_spec.rb b/spec/controllers/users/terms_controller_spec.rb
index a744463413c..0d77e91a67d 100644
--- a/spec/controllers/users/terms_controller_spec.rb
+++ b/spec/controllers/users/terms_controller_spec.rb
@@ -1,6 +1,7 @@
require 'spec_helper'
describe Users::TermsController do
+ include TermsHelper
let(:user) { create(:user) }
let(:term) { create(:term) }
@@ -15,10 +16,25 @@ describe Users::TermsController do
expect(response).to have_gitlab_http_status(:redirect)
end
- it 'shows terms when they exist' do
- term
+ context 'when terms exist' do
+ before do
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
+ term
+ end
+
+ it 'shows terms when they exist' do
+ get :index
+
+ expect(response).to have_gitlab_http_status(:success)
+ end
- expect(response).to have_gitlab_http_status(:success)
+ it 'shows a message when the user already accepted the terms' do
+ accept_terms(user)
+
+ get :index
+
+ expect(controller).to set_flash.now[:notice].to(/already accepted/)
+ end
end
end
diff --git a/spec/dependencies/omniauth_saml_spec.rb b/spec/dependencies/omniauth_saml_spec.rb
new file mode 100644
index 00000000000..ccc604dc230
--- /dev/null
+++ b/spec/dependencies/omniauth_saml_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+require 'omniauth/strategies/saml'
+
+describe 'processing of SAMLResponse in dependencies' do
+ let(:mock_saml_response) { File.read('spec/fixtures/authentication/saml_response.xml') }
+ let(:saml_strategy) { OmniAuth::Strategies::SAML.new({}) }
+ let(:session_mock) { {} }
+ let(:settings) { OpenStruct.new({ soft: false, idp_cert_fingerprint: 'something' }) }
+ let(:auth_hash) { Gitlab::Auth::Saml::AuthHash.new(saml_strategy) }
+
+ subject { auth_hash.authn_context }
+
+ before do
+ allow(saml_strategy).to receive(:session).and_return(session_mock)
+ allow_any_instance_of(OneLogin::RubySaml::Response).to receive(:is_valid?).and_return(true)
+ saml_strategy.send(:handle_response, mock_saml_response, {}, settings ) { }
+ end
+
+ it 'can extract AuthnContextClassRef from SAMLResponse param' do
+ is_expected.to eq 'urn:oasis:names:tc:SAML:2.0:ac:classes:Password'
+ end
+end
diff --git a/spec/factories/application_settings.rb b/spec/factories/application_settings.rb
index 3ecc90b6573..00c063c49f8 100644
--- a/spec/factories/application_settings.rb
+++ b/spec/factories/application_settings.rb
@@ -1,4 +1,5 @@
FactoryBot.define do
factory :application_setting do
+ default_projects_limit 42
end
end
diff --git a/spec/factories/ci/build_trace_chunks.rb b/spec/factories/ci/build_trace_chunks.rb
index c0b9a25bfe8..3e8e2736423 100644
--- a/spec/factories/ci/build_trace_chunks.rb
+++ b/spec/factories/ci/build_trace_chunks.rb
@@ -3,5 +3,53 @@ FactoryBot.define do
build factory: :ci_build
chunk_index 0
data_store :redis
+
+ trait :redis_with_data do
+ data_store :redis
+
+ transient do
+ initial_data 'test data'
+ end
+
+ after(:create) do |build_trace_chunk, evaluator|
+ Ci::BuildTraceChunks::Redis.new.set_data(build_trace_chunk, evaluator.initial_data)
+ end
+ end
+
+ trait :redis_without_data do
+ data_store :redis
+ end
+
+ trait :database_with_data do
+ data_store :database
+
+ transient do
+ initial_data 'test data'
+ end
+
+ after(:build) do |build_trace_chunk, evaluator|
+ Ci::BuildTraceChunks::Database.new.set_data(build_trace_chunk, evaluator.initial_data)
+ end
+ end
+
+ trait :database_without_data do
+ data_store :database
+ end
+
+ trait :fog_with_data do
+ data_store :fog
+
+ transient do
+ initial_data 'test data'
+ end
+
+ after(:create) do |build_trace_chunk, evaluator|
+ Ci::BuildTraceChunks::Fog.new.set_data(build_trace_chunk, evaluator.initial_data)
+ end
+ end
+
+ trait :fog_without_data do
+ data_store :fog
+ end
end
end
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index 4acc008ed38..83cb4750741 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -248,5 +248,11 @@ FactoryBot.define do
failed
failure_reason 2
end
+
+ trait :with_runner_session do
+ after(:build) do |build|
+ build.build_runner_session(url: 'ws://localhost')
+ end
+ end
end
end
diff --git a/spec/factories/ci/runner_projects.rb b/spec/factories/ci/runner_projects.rb
index f605e90ceed..ec15972c423 100644
--- a/spec/factories/ci/runner_projects.rb
+++ b/spec/factories/ci/runner_projects.rb
@@ -1,6 +1,6 @@
FactoryBot.define do
factory :ci_runner_project, class: Ci::RunnerProject do
- runner factory: :ci_runner
+ runner factory: [:ci_runner, :project]
project
end
end
diff --git a/spec/factories/ci/runners.rb b/spec/factories/ci/runners.rb
index cdc170b9ccb..347e4f433e2 100644
--- a/spec/factories/ci/runners.rb
+++ b/spec/factories/ci/runners.rb
@@ -3,22 +3,41 @@ FactoryBot.define do
sequence(:description) { |n| "My runner#{n}" }
platform "darwin"
- is_shared false
active true
access_level :not_protected
- runner_type :project_type
+
+ runner_type :instance_type
trait :online do
contacted_at Time.now
end
- trait :shared do
- is_shared true
+ trait :instance do
runner_type :instance_type
end
- trait :specific do
- is_shared false
+ trait :group do
+ runner_type :group_type
+
+ after(:build) do |runner, evaluator|
+ runner.groups << build(:group) if runner.groups.empty?
+ end
+ end
+
+ trait :project do
+ runner_type :project_type
+
+ after(:build) do |runner, evaluator|
+ runner.projects << build(:project) if runner.projects.empty?
+ end
+ end
+
+ trait :without_projects do
+ # we use that to create invalid runner:
+ # the one without projects
+ after(:create) do |runner, evaluator|
+ runner.runner_projects.delete_all
+ end
end
trait :inactive do
diff --git a/spec/factories/clusters/applications/helm.rb b/spec/factories/clusters/applications/helm.rb
index 3deca103578..3e4277e4ba6 100644
--- a/spec/factories/clusters/applications/helm.rb
+++ b/spec/factories/clusters/applications/helm.rb
@@ -35,5 +35,8 @@ FactoryBot.define do
factory :clusters_applications_ingress, class: Clusters::Applications::Ingress
factory :clusters_applications_prometheus, class: Clusters::Applications::Prometheus
factory :clusters_applications_runner, class: Clusters::Applications::Runner
+ factory :clusters_applications_jupyter, class: Clusters::Applications::Jupyter do
+ oauth_application factory: :oauth_application
+ end
end
end
diff --git a/spec/factories/group_members.rb b/spec/factories/group_members.rb
index 1c2214e9481..47036560b9d 100644
--- a/spec/factories/group_members.rb
+++ b/spec/factories/group_members.rb
@@ -7,7 +7,7 @@ FactoryBot.define do
trait(:guest) { access_level GroupMember::GUEST }
trait(:reporter) { access_level GroupMember::REPORTER }
trait(:developer) { access_level GroupMember::DEVELOPER }
- trait(:master) { access_level GroupMember::MASTER }
+ trait(:maintainer) { access_level GroupMember::MAINTAINER }
trait(:owner) { access_level GroupMember::OWNER }
trait(:access_request) { requested_at Time.now }
diff --git a/spec/factories/import_export_uploads.rb b/spec/factories/import_export_uploads.rb
new file mode 100644
index 00000000000..7750d49b1d0
--- /dev/null
+++ b/spec/factories/import_export_uploads.rb
@@ -0,0 +1,5 @@
+FactoryBot.define do
+ factory :import_export_upload do
+ project { create(:project) }
+ end
+end
diff --git a/spec/factories/issues.rb b/spec/factories/issues.rb
index 998080a3dd5..4d4f74e101e 100644
--- a/spec/factories/issues.rb
+++ b/spec/factories/issues.rb
@@ -3,6 +3,7 @@ FactoryBot.define do
title { generate(:title) }
project
author { project.creator }
+ updated_by { author }
trait :confidential do
confidential true
@@ -26,7 +27,7 @@ FactoryBot.define do
end
after(:create) do |issue, evaluator|
- issue.update_attributes(labels: evaluator.labels)
+ issue.update(labels: evaluator.labels)
end
end
end
diff --git a/spec/factories/lfs_objects.rb b/spec/factories/lfs_objects.rb
index eaf3a4ed497..c909471bb55 100644
--- a/spec/factories/lfs_objects.rb
+++ b/spec/factories/lfs_objects.rb
@@ -7,7 +7,7 @@ FactoryBot.define do
end
trait :with_file do
- file { fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "`/png") }
+ file { fixture_file_upload("spec/fixtures/dk.png", "`/png") }
end
# The uniqueness constraint means we can't use the correct OID for all LFS
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index fab0ec22450..f722bb9cb0d 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -54,6 +54,11 @@ FactoryBot.define do
state :opened
end
+ trait :invalid do
+ source_branch "feature_one"
+ target_branch "feature_two"
+ end
+
trait :locked do
state :locked
end
@@ -98,6 +103,7 @@ FactoryBot.define do
factory :merged_merge_request, traits: [:merged]
factory :closed_merge_request, traits: [:closed]
factory :reopened_merge_request, traits: [:opened]
+ factory :invalid_merge_request, traits: [:invalid]
factory :merge_request_with_diffs, traits: [:with_diffs]
factory :merge_request_with_diff_notes do
after(:create) do |mr|
@@ -111,7 +117,7 @@ FactoryBot.define do
end
after(:create) do |merge_request, evaluator|
- merge_request.update_attributes(labels: evaluator.labels)
+ merge_request.update(labels: evaluator.labels)
end
end
end
diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb
index 40f3fa7d69b..9fdc3e616a6 100644
--- a/spec/factories/notes.rb
+++ b/spec/factories/notes.rb
@@ -130,11 +130,11 @@ FactoryBot.define do
end
trait :with_attachment do
- attachment { fixture_file_upload(Rails.root.join( "spec/fixtures/dk.png"), "image/png") }
+ attachment { fixture_file_upload("spec/fixtures/dk.png", "image/png") }
end
trait :with_svg_attachment do
- attachment { fixture_file_upload(Rails.root.join("spec/fixtures/unsanitized.svg"), "image/svg+xml") }
+ attachment { fixture_file_upload("spec/fixtures/unsanitized.svg", "image/svg+xml") }
end
transient do
diff --git a/spec/factories/project_auto_devops.rb b/spec/factories/project_auto_devops.rb
index 5ce1988c76f..b77f702f9e1 100644
--- a/spec/factories/project_auto_devops.rb
+++ b/spec/factories/project_auto_devops.rb
@@ -3,5 +3,14 @@ FactoryBot.define do
project
enabled true
domain "example.com"
+ deploy_strategy :continuous
+
+ trait :manual do
+ deploy_strategy :manual
+ end
+
+ trait :disabled do
+ enabled false
+ end
end
end
diff --git a/spec/factories/project_members.rb b/spec/factories/project_members.rb
index 4260f52498d..22a8085ea45 100644
--- a/spec/factories/project_members.rb
+++ b/spec/factories/project_members.rb
@@ -2,12 +2,12 @@ FactoryBot.define do
factory :project_member do
user
project
- master
+ maintainer
trait(:guest) { access_level ProjectMember::GUEST }
trait(:reporter) { access_level ProjectMember::REPORTER }
trait(:developer) { access_level ProjectMember::DEVELOPER }
- trait(:master) { access_level ProjectMember::MASTER }
+ trait(:maintainer) { access_level ProjectMember::MAINTAINER }
trait(:access_request) { requested_at Time.now }
trait(:invited) do
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index 16e025618a6..fec1bea2751 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -47,7 +47,7 @@ FactoryBot.define do
# user have access to the project. Our specs don't use said service class,
# thus we must manually refresh things here.
unless project.group || project.pending_delete
- project.add_master(project.owner)
+ project.add_maintainer(project.owner)
end
project.group&.refresh_members_authorized_projects
@@ -103,6 +103,22 @@ FactoryBot.define do
end
trait :with_export do
+ before(:create) do |_project, _evaluator|
+ allow(Feature).to receive(:enabled?).with(:import_export_object_storage) { false }
+ allow(Feature).to receive(:enabled?).with('import_export_object_storage') { false }
+ end
+
+ after(:create) do |project, _evaluator|
+ ProjectExportWorker.new.perform(project.creator.id, project.id)
+ end
+ end
+
+ trait :with_object_export do
+ before(:create) do |_project, _evaluator|
+ allow(Feature).to receive(:enabled?).with(:import_export_object_storage) { true }
+ allow(Feature).to receive(:enabled?).with('import_export_object_storage') { true }
+ end
+
after(:create) do |project, evaluator|
ProjectExportWorker.new.perform(project.creator.id, project.id)
end
@@ -151,11 +167,6 @@ FactoryBot.define do
trait :empty_repo do
after(:create) do |project|
raise "Failed to create repository!" unless project.create_repository
-
- # We delete hooks so that gitlab-shell will not try to authenticate with
- # an API that isn't running
- project.gitlab_shell.rm_directory(project.repository_storage,
- File.join("#{project.disk_path}.git", 'hooks'))
end
end
@@ -180,13 +191,6 @@ FactoryBot.define do
trait :wiki_repo do
after(:create) do |project|
raise 'Failed to create wiki repository!' unless project.create_wiki
-
- # We delete hooks so that gitlab-shell will not try to authenticate with
- # an API that isn't running
- project.gitlab_shell.rm_directory(
- project.repository_storage,
- File.join("#{project.wiki.repository.disk_path}.git", "hooks")
- )
end
end
diff --git a/spec/factories/protected_branches.rb b/spec/factories/protected_branches.rb
index 60956511834..5457c0d2a8f 100644
--- a/spec/factories/protected_branches.rb
+++ b/spec/factories/protected_branches.rb
@@ -39,23 +39,23 @@ FactoryBot.define do
end
end
- trait :masters_can_push do
+ trait :maintainers_can_push do
transient do
default_push_level false
end
after(:build) do |protected_branch|
- protected_branch.push_access_levels.new(access_level: Gitlab::Access::MASTER)
+ protected_branch.push_access_levels.new(access_level: Gitlab::Access::MAINTAINER)
end
end
after(:build) do |protected_branch, evaluator|
if evaluator.default_access_level && evaluator.default_push_level
- protected_branch.push_access_levels.new(access_level: Gitlab::Access::MASTER)
+ protected_branch.push_access_levels.new(access_level: Gitlab::Access::MAINTAINER)
end
if evaluator.default_access_level && evaluator.default_merge_level
- protected_branch.merge_access_levels.new(access_level: Gitlab::Access::MASTER)
+ protected_branch.merge_access_levels.new(access_level: Gitlab::Access::MAINTAINER)
end
end
diff --git a/spec/factories/protected_tags.rb b/spec/factories/protected_tags.rb
index df9c8b3cb63..2b81d089549 100644
--- a/spec/factories/protected_tags.rb
+++ b/spec/factories/protected_tags.rb
@@ -27,19 +27,19 @@ FactoryBot.define do
end
end
- trait :masters_can_create do
+ trait :maintainers_can_create do
transient do
default_access_level false
end
after(:build) do |protected_tag|
- protected_tag.create_access_levels.new(access_level: Gitlab::Access::MASTER)
+ protected_tag.create_access_levels.new(access_level: Gitlab::Access::MAINTAINER)
end
end
after(:build) do |protected_tag, evaluator|
if evaluator.default_access_level
- protected_tag.create_access_levels.new(access_level: Gitlab::Access::MASTER)
+ protected_tag.create_access_levels.new(access_level: Gitlab::Access::MAINTAINER)
end
end
end
diff --git a/spec/factories/term_agreements.rb b/spec/factories/term_agreements.rb
index 557599e663d..3c4eebd0196 100644
--- a/spec/factories/term_agreements.rb
+++ b/spec/factories/term_agreements.rb
@@ -3,4 +3,12 @@ FactoryBot.define do
term
user
end
+
+ trait :declined do
+ accepted false
+ end
+
+ trait :accepted do
+ accepted true
+ end
end
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index 769fd656e7a..59db8cdc34b 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -12,10 +12,6 @@ FactoryBot.define do
user.notification_email = user.email
end
- before(:create) do |user|
- user.ensure_rss_token
- end
-
trait :admin do
admin true
end
diff --git a/spec/features/abuse_report_spec.rb b/spec/features/abuse_report_spec.rb
index 091fdcec3db..eb12eebe8e6 100644
--- a/spec/features/abuse_report_spec.rb
+++ b/spec/features/abuse_report_spec.rb
@@ -1,13 +1,13 @@
require 'spec_helper'
-feature 'Abuse reports' do
+describe 'Abuse reports' do
let(:another_user) { create(:user) }
before do
sign_in(create(:user))
end
- scenario 'Report abuse' do
+ it 'Report abuse' do
visit user_path(another_user)
click_link 'Report abuse'
diff --git a/spec/features/admin/admin_abuse_reports_spec.rb b/spec/features/admin/admin_abuse_reports_spec.rb
index 766cd4b0090..d8fcdebfc6d 100644
--- a/spec/features/admin/admin_abuse_reports_spec.rb
+++ b/spec/features/admin/admin_abuse_reports_spec.rb
@@ -45,7 +45,7 @@ describe "Admin::AbuseReports", :js do
visit admin_abuse_reports_path
expect(page).to have_selector('.pagination')
- expect(page).to have_selector('.pagination .page', count: (report_count.to_f / AbuseReport.default_per_page).ceil)
+ expect(page).to have_selector('.pagination .js-pagination-page', count: (report_count.to_f / AbuseReport.default_per_page).ceil)
end
end
end
diff --git a/spec/features/admin/admin_appearance_spec.rb b/spec/features/admin/admin_appearance_spec.rb
index d91dcf76191..3c2ae5b3c6a 100644
--- a/spec/features/admin/admin_appearance_spec.rb
+++ b/spec/features/admin/admin_appearance_spec.rb
@@ -1,9 +1,9 @@
require 'spec_helper'
-feature 'Admin Appearance' do
+describe 'Admin Appearance' do
let!(:appearance) { create(:appearance) }
- scenario 'Create new appearance' do
+ it 'Create new appearance' do
sign_in(create(:admin))
visit admin_appearances_path
@@ -21,7 +21,7 @@ feature 'Admin Appearance' do
expect(page).to have_content 'Last edit'
end
- scenario 'Preview sign-in page appearance' do
+ it 'Preview sign-in page appearance' do
sign_in(create(:admin))
visit admin_appearances_path
@@ -30,7 +30,7 @@ feature 'Admin Appearance' do
expect_custom_sign_in_appearance(appearance)
end
- scenario 'Preview new project page appearance' do
+ it 'Preview new project page appearance' do
sign_in(create(:admin))
visit admin_appearances_path
@@ -39,20 +39,20 @@ feature 'Admin Appearance' do
expect_custom_new_project_appearance(appearance)
end
- scenario 'Custom sign-in page' do
+ it 'Custom sign-in page' do
visit new_user_session_path
expect_custom_sign_in_appearance(appearance)
end
- scenario 'Custom new project page' do
+ it 'Custom new project page' do
sign_in create(:user)
visit new_project_path
expect_custom_new_project_appearance(appearance)
end
- scenario 'Appearance logo' do
+ it 'Appearance logo' do
sign_in(create(:admin))
visit admin_appearances_path
@@ -64,7 +64,7 @@ feature 'Admin Appearance' do
expect(page).not_to have_css(logo_selector)
end
- scenario 'Header logos' do
+ it 'Header logos' do
sign_in(create(:admin))
visit admin_appearances_path
@@ -76,6 +76,26 @@ feature 'Admin Appearance' do
expect(page).not_to have_css(header_logo_selector)
end
+ it 'Favicon' do
+ sign_in(create(:admin))
+ visit admin_appearances_path
+
+ attach_file(:appearance_favicon, logo_fixture)
+ click_button 'Save'
+
+ expect(page).to have_css('.appearance-light-logo-preview')
+
+ click_link 'Remove favicon'
+
+ expect(page).not_to have_css('.appearance-light-logo-preview')
+
+ # allowed file types
+ attach_file(:appearance_favicon, Rails.root.join('spec', 'fixtures', 'sanitized.svg'))
+ click_button 'Save'
+
+ expect(page).to have_content 'Favicon You are not allowed to upload "svg" files, allowed types: png, ico'
+ end
+
def expect_custom_sign_in_appearance(appearance)
expect(page).to have_content appearance.title
expect(page).to have_content appearance.description
diff --git a/spec/features/admin/admin_broadcast_messages_spec.rb b/spec/features/admin/admin_broadcast_messages_spec.rb
index 430a8d22b0f..f6dc499df29 100644
--- a/spec/features/admin/admin_broadcast_messages_spec.rb
+++ b/spec/features/admin/admin_broadcast_messages_spec.rb
@@ -1,17 +1,17 @@
require 'spec_helper'
-feature 'Admin Broadcast Messages' do
+describe 'Admin Broadcast Messages' do
before do
sign_in(create(:admin))
create(:broadcast_message, :expired, message: 'Migration to new server')
visit admin_broadcast_messages_path
end
- scenario 'See broadcast messages list' do
+ it 'See broadcast messages list' do
expect(page).to have_content 'Migration to new server'
end
- scenario 'Create a customized broadcast message' do
+ it 'Create a customized broadcast message' do
fill_in 'broadcast_message_message', with: 'Application update from **4:00 CST to 5:00 CST**'
fill_in 'broadcast_message_color', with: '#f2dede'
fill_in 'broadcast_message_font', with: '#b94a48'
@@ -24,7 +24,7 @@ feature 'Admin Broadcast Messages' do
expect(page).to have_selector %(div[style="background-color: #f2dede; color: #b94a48"])
end
- scenario 'Edit an existing broadcast message' do
+ it 'Edit an existing broadcast message' do
click_link 'Edit'
fill_in 'broadcast_message_message', with: 'Application update RIGHT NOW'
click_button 'Update broadcast message'
@@ -33,14 +33,14 @@ feature 'Admin Broadcast Messages' do
expect(page).to have_content 'Application update RIGHT NOW'
end
- scenario 'Remove an existing broadcast message' do
+ it 'Remove an existing broadcast message' do
click_link 'Remove'
expect(current_path).to eq admin_broadcast_messages_path
expect(page).not_to have_content 'Migration to new server'
end
- scenario 'Live preview a customized broadcast message', :js do
+ it '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_browse_spam_logs_spec.rb b/spec/features/admin/admin_browse_spam_logs_spec.rb
index 31d4142a8e9..4645fde7522 100644
--- a/spec/features/admin/admin_browse_spam_logs_spec.rb
+++ b/spec/features/admin/admin_browse_spam_logs_spec.rb
@@ -7,7 +7,7 @@ describe 'Admin browse spam logs' do
sign_in(create(:admin))
end
- scenario 'Browse spam logs' do
+ it 'Browse spam logs' do
visit admin_spam_logs_path
expect(page).to have_content('Spam Logs')
diff --git a/spec/features/admin/admin_cohorts_spec.rb b/spec/features/admin/admin_cohorts_spec.rb
index bca52bf674c..9dce9494b97 100644
--- a/spec/features/admin/admin_cohorts_spec.rb
+++ b/spec/features/admin/admin_cohorts_spec.rb
@@ -1,11 +1,11 @@
require 'rails_helper'
-feature 'Admin cohorts page' do
+describe 'Admin cohorts page' do
before do
sign_in(create(:admin))
end
- scenario 'See users count per month' do
+ it 'See users count per month' do
2.times { create(:user) }
visit admin_cohorts_path
diff --git a/spec/features/admin/admin_disables_git_access_protocol_spec.rb b/spec/features/admin/admin_disables_git_access_protocol_spec.rb
index 9946cc77d1d..91c22e7ad82 100644
--- a/spec/features/admin/admin_disables_git_access_protocol_spec.rb
+++ b/spec/features/admin/admin_disables_git_access_protocol_spec.rb
@@ -1,22 +1,22 @@
require 'rails_helper'
-feature 'Admin disables Git access protocol' do
+describe 'Admin disables Git access protocol' do
include StubENV
let(:project) { create(:project, :empty_repo) }
let(:admin) { create(:admin) }
- background do
+ before do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
sign_in(admin)
end
context 'with HTTP disabled' do
- background do
+ before do
disable_http_protocol
end
- scenario 'shows only SSH url' do
+ it 'shows only SSH url' do
visit_project
expect(page).to have_content("git clone #{project.ssh_url_to_repo}")
@@ -25,11 +25,11 @@ feature 'Admin disables Git access protocol' do
end
context 'with SSH disabled' do
- background do
+ before do
disable_ssh_protocol
end
- scenario 'shows only HTTP url' do
+ it 'shows only HTTP url' do
visit_project
expect(page).to have_content("git clone #{project.http_url_to_repo}")
@@ -38,11 +38,11 @@ feature 'Admin disables Git access protocol' do
end
context 'with nothing disabled' do
- background do
+ before do
create(:personal_key, user: admin)
end
- scenario 'shows default SSH url and protocol selection dropdown' do
+ it 'shows default SSH url and protocol selection dropdown' do
visit_project
expect(page).to have_content("git clone #{project.ssh_url_to_repo}")
diff --git a/spec/features/admin/admin_disables_two_factor_spec.rb b/spec/features/admin/admin_disables_two_factor_spec.rb
index 2abdd3c9ef2..e41835b4f24 100644
--- a/spec/features/admin/admin_disables_two_factor_spec.rb
+++ b/spec/features/admin/admin_disables_two_factor_spec.rb
@@ -1,7 +1,7 @@
require 'rails_helper'
-feature 'Admin disables 2FA for a user' do
- scenario 'successfully', :js do
+describe 'Admin disables 2FA for a user' do
+ it 'successfully', :js do
sign_in(create(:admin))
user = create(:user, :two_factor)
@@ -16,7 +16,7 @@ feature 'Admin disables 2FA for a user' do
end
end
- scenario 'for a user without 2FA enabled' do
+ it 'for a user without 2FA enabled' do
sign_in(create(:admin))
user = create(:user)
diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb
index d5e603baeae..96dfde2e08c 100644
--- a/spec/features/admin/admin_groups_spec.rb
+++ b/spec/features/admin/admin_groups_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Admin Groups' do
+describe 'Admin Groups' do
include Select2Helper
let(:internal) { Gitlab::VisibilityLevel::INTERNAL }
@@ -31,6 +31,7 @@ feature 'Admin Groups' do
path_component = 'gitlab'
group_name = 'GitLab group name'
group_description = 'Description of group for GitLab'
+
fill_in 'group_path', with: path_component
fill_in 'group_name', with: group_name
fill_in 'group_description', with: group_description
@@ -46,13 +47,13 @@ feature 'Admin Groups' do
expect(li_texts).to match group_description
end
- scenario 'shows the visibility level radio populated with the default value' do
+ it 'shows the visibility level radio populated with the default value' do
visit new_admin_group_path
expect_selected_visibility(internal)
end
- scenario 'when entered in group path, it auto filled the group name', :js do
+ it 'when entered in group path, it auto filled the group name', :js do
visit admin_groups_path
click_link "New group"
group_path = 'gitlab'
@@ -63,7 +64,7 @@ feature 'Admin Groups' do
end
describe 'show a group' do
- scenario 'shows the group' do
+ it 'shows the group' do
group = create(:group, :private)
visit admin_group_path(group)
@@ -73,7 +74,7 @@ feature 'Admin Groups' do
end
describe 'group edit' do
- scenario 'shows the visibility level radio populated with the group visibility_level value' do
+ it 'shows the visibility level radio populated with the group visibility_level value' do
group = create(:group, :private)
visit admin_group_edit_path(group)
@@ -81,7 +82,7 @@ feature 'Admin Groups' do
expect_selected_visibility(group.visibility_level)
end
- scenario 'edit group path does not change group name', :js do
+ it 'edit group path does not change group name', :js do
group = create(:group, :private)
visit admin_group_edit_path(group)
@@ -167,7 +168,7 @@ feature 'Admin Groups' do
it 'renders shared project' do
empty_project = create(:project)
empty_project.project_group_links.create!(
- group_access: Gitlab::Access::MASTER,
+ group_access: Gitlab::Access::MAINTAINER,
group: group
)
diff --git a/spec/features/admin/admin_health_check_spec.rb b/spec/features/admin/admin_health_check_spec.rb
index 3693e5882f9..aaa3e8dc821 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 do
+describe "Admin Health Check", :feature do
include StubENV
before do
diff --git a/spec/features/admin/admin_hook_logs_spec.rb b/spec/features/admin/admin_hook_logs_spec.rb
index 710822ac042..928f97b6d29 100644
--- a/spec/features/admin/admin_hook_logs_spec.rb
+++ b/spec/features/admin/admin_hook_logs_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Admin::HookLogs' do
+describe 'Admin::HookLogs' do
let(:project) { create(:project) }
let(:system_hook) { create(:system_hook) }
let(:hook_log) { create(:web_hook_log, web_hook: system_hook, internal_error_message: 'some error') }
@@ -9,7 +9,7 @@ feature 'Admin::HookLogs' do
sign_in(create(:admin))
end
- scenario 'show list of hook logs' do
+ it 'show list of hook logs' do
hook_log
visit edit_admin_hook_path(system_hook)
@@ -17,7 +17,7 @@ feature 'Admin::HookLogs' do
expect(page).to have_content(hook_log.url)
end
- scenario 'show hook log details' do
+ it 'show hook log details' do
hook_log
visit edit_admin_hook_path(system_hook)
click_link 'View details'
@@ -27,7 +27,7 @@ feature 'Admin::HookLogs' do
expect(page).to have_content('Resend Request')
end
- scenario 'retry hook log' do
+ it 'retry hook log' do
WebMock.stub_request(:post, system_hook.url)
hook_log
diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb
index 6d8350e99f1..d6ee256f5b5 100644
--- a/spec/features/admin/admin_projects_spec.rb
+++ b/spec/features/admin/admin_projects_spec.rb
@@ -34,7 +34,7 @@ describe "Admin::Projects" do
expect(page).to have_content(project.name)
expect(page).to have_content(archived_project.name)
- expect(page).to have_xpath("//span[@class='label label-warning']", text: 'archived')
+ expect(page).to have_xpath("//span[@class='badge badge-warning']", text: 'archived')
end
it 'renders only archived projects', :js do
@@ -88,7 +88,7 @@ describe "Admin::Projects" do
describe 'add admin himself to a project' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
it 'adds admin a to a project as developer', :js do
@@ -110,7 +110,7 @@ describe "Admin::Projects" do
describe 'admin remove himself from a project' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
project.add_developer(current_user)
end
diff --git a/spec/features/admin/admin_requests_profiles_spec.rb b/spec/features/admin/admin_requests_profiles_spec.rb
index 380cd5d7703..2503fc9067d 100644
--- a/spec/features/admin/admin_requests_profiles_spec.rb
+++ b/spec/features/admin/admin_requests_profiles_spec.rb
@@ -33,12 +33,12 @@ describe 'Admin::RequestsProfilesController' do
visit admin_requests_profiles_path
- within('.panel', text: '/gitlab-org/gitlab-ce') do
+ within('.card', text: '/gitlab-org/gitlab-ce') do
expect(page).to have_selector("a[href='#{admin_requests_profile_path(profile1)}']", text: time1.to_s(:long))
expect(page).to have_selector("a[href='#{admin_requests_profile_path(profile2)}']", text: time2.to_s(:long))
end
- within('.panel', text: '/gitlab-com/infrastructure') do
+ within('.card', text: '/gitlab-com/infrastructure') do
expect(page).to have_selector("a[href='#{admin_requests_profile_path(profile3)}']", text: time3.to_s(:long))
end
end
diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb
index 3465ccfc423..be8754a5315 100644
--- a/spec/features/admin/admin_runners_spec.rb
+++ b/spec/features/admin/admin_runners_spec.rb
@@ -62,13 +62,13 @@ describe "Admin Runners" do
context 'group runner' do
let(:group) { create(:group) }
- let!(:runner) { create(:ci_runner, groups: [group], runner_type: :group_type) }
+ let!(:runner) { create(:ci_runner, :group, groups: [group]) }
it 'shows the label and does not show the project count' do
visit admin_runners_path
within "#runner_#{runner.id}" do
- expect(page).to have_selector '.label', text: 'group'
+ expect(page).to have_selector '.badge', text: 'group'
expect(page).to have_text 'n/a'
end
end
@@ -76,12 +76,12 @@ describe "Admin Runners" do
context 'shared runner' do
it 'shows the label and does not show the project count' do
- runner = create :ci_runner, :shared
+ runner = create :ci_runner, :instance
visit admin_runners_path
within "#runner_#{runner.id}" do
- expect(page).to have_selector '.label', text: 'shared'
+ expect(page).to have_selector '.badge', text: 'shared'
expect(page).to have_text 'n/a'
end
end
@@ -90,12 +90,12 @@ describe "Admin Runners" do
context 'specific runner' do
it 'shows the label and the project count' do
project = create :project
- runner = create :ci_runner, projects: [project]
+ runner = create :ci_runner, :project, projects: [project]
visit admin_runners_path
within "#runner_#{runner.id}" do
- expect(page).to have_selector '.label', text: 'specific'
+ expect(page).to have_selector '.badge', text: 'specific'
expect(page).to have_text '1'
end
end
@@ -149,8 +149,9 @@ describe "Admin Runners" do
end
context 'with specific runner' do
+ let(:runner) { create(:ci_runner, :project, projects: [@project1]) }
+
before do
- @project1.runners << runner
visit admin_runner_path(runner)
end
@@ -158,9 +159,9 @@ describe "Admin Runners" do
end
context 'with locked runner' do
+ let(:runner) { create(:ci_runner, :project, projects: [@project1], locked: true) }
+
before do
- runner.update(locked: true)
- @project1.runners << runner
visit admin_runner_path(runner)
end
@@ -168,9 +169,10 @@ describe "Admin Runners" do
end
context 'with shared runner' do
+ let(:runner) { create(:ci_runner, :instance) }
+
before do
@project1.destroy
- runner.update(is_shared: true)
visit admin_runner_path(runner)
end
@@ -179,8 +181,9 @@ describe "Admin Runners" do
end
describe 'disable/destroy' do
+ let(:runner) { create(:ci_runner, :project, projects: [@project1]) }
+
before do
- @project1.runners << runner
visit admin_runner_path(runner)
end
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index dc025d82937..a852ca689e7 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Admin updates settings' do
+describe 'Admin updates settings' do
include StubENV
include TermsHelper
@@ -12,7 +12,7 @@ feature 'Admin updates settings' do
visit admin_application_settings_path
end
- scenario 'Change visibility settings' do
+ it 'Change visibility settings' do
page.within('.as-visibility-access') do
choose "application_setting_default_project_visibility_20"
click_button 'Save changes'
@@ -21,7 +21,7 @@ feature 'Admin updates settings' do
expect(page).to have_content "Application settings saved successfully"
end
- scenario 'Uncheck all restricted visibility levels' do
+ it 'Uncheck all restricted visibility levels' do
page.within('.as-visibility-access') do
find('#application_setting_visibility_level_0').set(false)
find('#application_setting_visibility_level_10').set(false)
@@ -35,7 +35,7 @@ feature 'Admin updates settings' do
expect(find('#application_setting_visibility_level_20')).not_to be_checked
end
- scenario 'Modify import sources' do
+ it 'Modify import sources' do
expect(Gitlab::CurrentSettings.import_sources).not_to be_empty
page.within('.as-visibility-access') do
@@ -58,7 +58,7 @@ feature 'Admin updates settings' do
expect(Gitlab::CurrentSettings.import_sources).to eq(['git'])
end
- scenario 'Change Visibility and Access Controls' do
+ it 'Change Visibility and Access Controls' do
page.within('.as-visibility-access') do
uncheck 'Project export enabled'
click_button 'Save changes'
@@ -68,7 +68,7 @@ feature 'Admin updates settings' do
expect(page).to have_content "Application settings saved successfully"
end
- scenario 'Change Account and Limit Settings' do
+ it 'Change Account and Limit Settings' do
page.within('.as-account-limit') do
uncheck 'Gravatar enabled'
click_button 'Save changes'
@@ -78,7 +78,7 @@ feature 'Admin updates settings' do
expect(page).to have_content "Application settings saved successfully"
end
- scenario 'Change Sign-in restrictions' do
+ it 'Change Sign-in restrictions' do
page.within('.as-signin') do
fill_in 'Home page URL', with: 'https://about.gitlab.com/'
click_button 'Save changes'
@@ -88,13 +88,13 @@ feature 'Admin updates settings' do
expect(page).to have_content "Application settings saved successfully"
end
- scenario 'Terms of Service' do
+ it 'Terms of Service' do
# Already have the admin accept terms, so they don't need to accept in this spec.
_existing_terms = create(:term)
accept_terms(admin)
page.within('.as-terms') do
- check 'Require all users to accept Terms of Service when they access GitLab.'
+ check 'Require all users to accept Terms of Service and Privacy Policy when they access GitLab.'
fill_in 'Terms of Service Agreement', with: 'Be nice!'
click_button 'Save changes'
end
@@ -104,7 +104,7 @@ feature 'Admin updates settings' do
expect(page).to have_content 'Application settings saved successfully'
end
- scenario 'Modify oauth providers' do
+ it 'Modify oauth providers' do
expect(Gitlab::CurrentSettings.disabled_oauth_sign_in_sources).to be_empty
page.within('.as-signin') do
@@ -124,7 +124,30 @@ feature 'Admin updates settings' do
expect(Gitlab::CurrentSettings.disabled_oauth_sign_in_sources).not_to include('google_oauth2')
end
- scenario 'Change Help page' do
+ it 'Oauth providers do not raise validation errors when saving unrelated changes' do
+ expect(Gitlab::CurrentSettings.disabled_oauth_sign_in_sources).to be_empty
+
+ page.within('.as-signin') do
+ uncheck 'Google'
+ click_button 'Save changes'
+ end
+
+ expect(page).to have_content "Application settings saved successfully"
+ expect(Gitlab::CurrentSettings.disabled_oauth_sign_in_sources).to include('google_oauth2')
+
+ # Remove google_oauth2 from the Omniauth strategies
+ allow(Devise).to receive(:omniauth_providers).and_return([])
+
+ # Save an unrelated setting
+ page.within('.as-ci-cd') do
+ click_button 'Save changes'
+ end
+
+ expect(page).to have_content "Application settings saved successfully"
+ expect(Gitlab::CurrentSettings.disabled_oauth_sign_in_sources).to include('google_oauth2')
+ end
+
+ it 'Change Help page' do
page.within('.as-help-page') do
fill_in 'Help page text', with: 'Example text'
check 'Hide marketing-related entries from help'
@@ -138,7 +161,7 @@ feature 'Admin updates settings' do
expect(page).to have_content "Application settings saved successfully"
end
- scenario 'Change Pages settings' do
+ it 'Change Pages settings' do
page.within('.as-pages') do
fill_in 'Maximum size of pages (MB)', with: 15
check 'Require users to prove ownership of custom domains'
@@ -150,7 +173,7 @@ feature 'Admin updates settings' do
expect(page).to have_content "Application settings saved successfully"
end
- scenario 'Change CI/CD settings' do
+ it 'Change CI/CD settings' do
page.within('.as-ci-cd') do
check 'Enabled Auto DevOps for projects by default'
fill_in 'Auto devops domain', with: 'domain.com'
@@ -162,7 +185,7 @@ feature 'Admin updates settings' do
expect(page).to have_content "Application settings saved successfully"
end
- scenario 'Change Influx settings' do
+ it 'Change Influx settings' do
page.within('.as-influx') do
check 'Enable InfluxDB Metrics'
click_button 'Save changes'
@@ -172,7 +195,7 @@ feature 'Admin updates settings' do
expect(page).to have_content "Application settings saved successfully"
end
- scenario 'Change Prometheus settings' do
+ it 'Change Prometheus settings' do
page.within('.as-prometheus') do
check 'Enable Prometheus Metrics'
click_button 'Save changes'
@@ -182,7 +205,7 @@ feature 'Admin updates settings' do
expect(page).to have_content "Application settings saved successfully"
end
- scenario 'Change Performance bar settings' do
+ it 'Change Performance bar settings' do
group = create(:group)
page.within('.as-performance-bar') do
@@ -205,7 +228,7 @@ feature 'Admin updates settings' do
expect(find_field('Allowed group').value).to be_nil
end
- scenario 'Change Background jobs settings' do
+ it 'Change Background jobs settings' do
page.within('.as-background') do
fill_in 'Throttling Factor', with: 1
click_button 'Save changes'
@@ -215,7 +238,7 @@ feature 'Admin updates settings' do
expect(page).to have_content "Application settings saved successfully"
end
- scenario 'Change Spam settings' do
+ it 'Change Spam settings' do
page.within('.as-spam') do
check 'Enable reCAPTCHA'
fill_in 'reCAPTCHA Site Key', with: 'key'
@@ -229,7 +252,7 @@ feature 'Admin updates settings' do
expect(Gitlab::CurrentSettings.unique_ips_limit_per_user).to eq(15)
end
- scenario 'Configure web terminal' do
+ it 'Configure web terminal' do
page.within('.as-terminal') do
fill_in 'Max session time', with: 15
click_button 'Save changes'
@@ -239,7 +262,7 @@ feature 'Admin updates settings' do
expect(Gitlab::CurrentSettings.terminal_max_session_time).to eq(15)
end
- scenario 'Enable outbound requests' do
+ it 'Enable outbound requests' do
page.within('.as-outbound') do
check 'Allow requests to the local network from hooks and services'
click_button 'Save changes'
@@ -249,7 +272,17 @@ feature 'Admin updates settings' do
expect(Gitlab::CurrentSettings.allow_local_requests_from_hooks_and_services).to be true
end
- scenario 'Change Slack Notifications Service template settings' do
+ it 'Enable hiding third party offers' do
+ page.within('.as-third-party-offers') do
+ check 'Do not display offers from third parties within GitLab'
+ click_button 'Save changes'
+ end
+
+ expect(page).to have_content "Application settings saved successfully"
+ expect(Gitlab::CurrentSettings.hide_third_party_offers).to be true
+ end
+
+ it 'Change Slack Notifications Service template settings' do
first(:link, 'Service Templates').click
click_link 'Slack notifications'
fill_in 'Webhook', with: 'http://localhost'
@@ -273,7 +306,7 @@ feature 'Admin updates settings' do
expect(find('#service_push_channel').value).to eq '#test_channel'
end
- scenario 'Change Keys settings' do
+ it 'Change Keys settings' do
page.within('.as-visibility-access') do
select 'Are forbidden', from: 'RSA SSH keys'
select 'Are allowed', from: 'DSA SSH keys'
diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb
index 8fc57f4b4c3..9e3221577c7 100644
--- a/spec/features/admin/admin_users_spec.rb
+++ b/spec/features/admin/admin_users_spec.rb
@@ -283,14 +283,14 @@ describe "Admin::Users" do
end
it "lists group projects" do
- within(:css, '.append-bottom-default + .panel') do
+ within(:css, '.append-bottom-default + .card') do
expect(page).to have_content 'Group projects'
expect(page).to have_link group.name, href: admin_group_path(group)
end
end
it 'allows navigation to the group details' do
- within(:css, '.append-bottom-default + .panel') do
+ within(:css, '.append-bottom-default + .card') do
click_link group.name
end
within(:css, 'h3.page-title') do
@@ -300,7 +300,7 @@ describe "Admin::Users" do
end
it 'shows the group access level' do
- within(:css, '.append-bottom-default + .panel') do
+ within(:css, '.append-bottom-default + .card') do
expect(page).to have_content 'Developer'
end
end
diff --git a/spec/features/admin/admin_uses_repository_checks_spec.rb b/spec/features/admin/admin_uses_repository_checks_spec.rb
index 90cf5a53787..e658f1b6738 100644
--- a/spec/features/admin/admin_uses_repository_checks_spec.rb
+++ b/spec/features/admin/admin_uses_repository_checks_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Admin uses repository checks' do
+describe 'Admin uses repository checks' do
include StubENV
before do
@@ -8,7 +8,7 @@ feature 'Admin uses repository checks' do
sign_in(create(:admin))
end
- scenario 'to trigger a single check' do
+ it 'to trigger a single check' do
project = create(:project)
visit_admin_project_page(project)
@@ -19,7 +19,7 @@ feature 'Admin uses repository checks' do
expect(page).to have_content('Repository check was triggered')
end
- scenario 'to see a single failed repository check', :js do
+ it 'to see a single failed repository check', :js do
project = create(:project)
project.update_columns(
last_repository_check_failed: true,
@@ -28,11 +28,11 @@ feature 'Admin uses repository checks' do
visit_admin_project_page(project)
page.within('.alert') do
- expect(page.text).to match(/Last repository check \(.* ago\) failed/)
+ expect(page.text).to match(/Last repository check \(just now\) failed/)
end
end
- scenario 'to clear all repository checks', :js do
+ it 'to clear all repository checks', :js do
visit admin_application_settings_path
expect(RepositoryCheck::ClearWorker).to receive(:perform_async)
diff --git a/spec/features/atom/dashboard_issues_spec.rb b/spec/features/atom/dashboard_issues_spec.rb
index fb6c71ce997..bd4c00d97b1 100644
--- a/spec/features/atom/dashboard_issues_spec.rb
+++ b/spec/features/atom/dashboard_issues_spec.rb
@@ -8,8 +8,8 @@ describe "Dashboard Issues Feed" do
let!(:project2) { create(:project) }
before do
- project1.add_master(user)
- project2.add_master(user)
+ project1.add_maintainer(user)
+ project2.add_maintainer(user)
end
describe "atom feed" do
@@ -31,20 +31,20 @@ describe "Dashboard Issues Feed" do
expect(body).to have_selector('title', text: "#{user.name} issues")
end
- it "renders atom feed via RSS token" do
- visit issues_dashboard_path(:atom, rss_token: user.rss_token, assignee_id: user.id)
+ it "renders atom feed via feed token" do
+ visit issues_dashboard_path(:atom, feed_token: user.feed_token, assignee_id: user.id)
expect(response_headers['Content-Type']).to have_content('application/atom+xml')
expect(body).to have_selector('title', text: "#{user.name} issues")
end
it "renders atom feed with url parameters" do
- visit issues_dashboard_path(:atom, rss_token: user.rss_token, state: 'opened', assignee_id: user.id)
+ visit issues_dashboard_path(:atom, feed_token: user.feed_token, state: 'opened', assignee_id: user.id)
link = find('link[type="application/atom+xml"]')
params = CGI.parse(URI.parse(link[:href]).query)
- expect(params).to include('rss_token' => [user.rss_token])
+ expect(params).to include('feed_token' => [user.feed_token])
expect(params).to include('state' => ['opened'])
expect(params).to include('assignee_id' => [user.id.to_s])
end
@@ -53,7 +53,7 @@ describe "Dashboard Issues Feed" do
let!(:issue2) { create(:issue, author: user, assignees: [assignee], project: project2, description: 'test desc') }
it "renders issue fields" do
- visit issues_dashboard_path(:atom, rss_token: user.rss_token, assignee_id: assignee.id)
+ visit issues_dashboard_path(:atom, feed_token: user.feed_token, assignee_id: assignee.id)
entry = find(:xpath, "//feed/entry[contains(summary/text(),'#{issue2.title}')]")
@@ -76,7 +76,7 @@ describe "Dashboard Issues Feed" do
end
it "renders issue label and milestone info" do
- visit issues_dashboard_path(:atom, rss_token: user.rss_token, assignee_id: assignee.id)
+ visit issues_dashboard_path(:atom, feed_token: user.feed_token, assignee_id: assignee.id)
entry = find(:xpath, "//feed/entry[contains(summary/text(),'#{issue1.title}')]")
diff --git a/spec/features/atom/dashboard_spec.rb b/spec/features/atom/dashboard_spec.rb
index c6683bb3bc9..86b3f88298f 100644
--- a/spec/features/atom/dashboard_spec.rb
+++ b/spec/features/atom/dashboard_spec.rb
@@ -13,9 +13,9 @@ describe "Dashboard Feed" do
end
end
- context "projects atom feed via RSS token" do
+ context "projects atom feed via feed token" do
it "renders projects atom feed" do
- visit dashboard_projects_path(:atom, rss_token: user.rss_token)
+ visit dashboard_projects_path(:atom, feed_token: user.feed_token)
expect(body).to have_selector('feed title')
end
end
@@ -26,10 +26,10 @@ describe "Dashboard Feed" do
let(:note) { create(:note, noteable: issue, author: user, note: 'Bug confirmed', project: project) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
issue_event(issue, user)
note_event(note, user)
- visit dashboard_projects_path(:atom, rss_token: user.rss_token)
+ visit dashboard_projects_path(:atom, feed_token: user.feed_token)
end
it "has issue opened event" do
diff --git a/spec/features/atom/issues_spec.rb b/spec/features/atom/issues_spec.rb
index 525ce23aa56..ee3570a5b2b 100644
--- a/spec/features/atom/issues_spec.rb
+++ b/spec/features/atom/issues_spec.rb
@@ -45,10 +45,10 @@ describe 'Issues Feed' do
end
end
- context 'when authenticated via RSS token' do
+ context 'when authenticated via feed token' do
it 'renders atom feed' do
visit project_issues_path(project, :atom,
- rss_token: user.rss_token)
+ feed_token: user.feed_token)
expect(response_headers['Content-Type'])
.to have_content('application/atom+xml')
@@ -61,24 +61,23 @@ describe 'Issues Feed' do
end
it "renders atom feed with url parameters for project issues" do
- visit project_issues_path(project,
- :atom, rss_token: user.rss_token, state: 'opened', assignee_id: user.id)
+ visit project_issues_path(project, :atom, feed_token: user.feed_token, state: 'opened', assignee_id: user.id)
link = find('link[type="application/atom+xml"]')
params = CGI.parse(URI.parse(link[:href]).query)
- expect(params).to include('rss_token' => [user.rss_token])
+ expect(params).to include('feed_token' => [user.feed_token])
expect(params).to include('state' => ['opened'])
expect(params).to include('assignee_id' => [user.id.to_s])
end
it "renders atom feed with url parameters for group issues" do
- visit issues_group_path(group, :atom, rss_token: user.rss_token, state: 'opened', assignee_id: user.id)
+ visit issues_group_path(group, :atom, feed_token: user.feed_token, state: 'opened', assignee_id: user.id)
link = find('link[type="application/atom+xml"]')
params = CGI.parse(URI.parse(link[:href]).query)
- expect(params).to include('rss_token' => [user.rss_token])
+ expect(params).to include('feed_token' => [user.feed_token])
expect(params).to include('state' => ['opened'])
expect(params).to include('assignee_id' => [user.id.to_s])
end
diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb
index 2d074c115dd..8d7df346abb 100644
--- a/spec/features/atom/users_spec.rb
+++ b/spec/features/atom/users_spec.rb
@@ -13,9 +13,9 @@ describe "User Feed" do
end
end
- context 'user atom feed via RSS token' do
+ context 'user atom feed via feed token' do
it "renders user atom feed" do
- visit user_path(user, :atom, rss_token: user.rss_token)
+ visit user_path(user, :atom, feed_token: user.feed_token)
expect(body).to have_selector('feed title')
end
end
@@ -47,11 +47,11 @@ describe "User Feed" do
let!(:push_event_payload) { create(:push_event_payload, event: push_event) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
issue_event(issue, user)
note_event(note, user)
merge_request_event(merge_request, user)
- visit user_path(user, :atom, rss_token: user.rss_token)
+ visit user_path(user, :atom, feed_token: user.feed_token)
end
it 'has issue opened event' do
diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb
index 18901a954cc..eebc987499d 100644
--- a/spec/features/boards/add_issues_modal_spec.rb
+++ b/spec/features/boards/add_issues_modal_spec.rb
@@ -12,7 +12,7 @@ describe 'Issue Boards add issue modal', :js do
let!(:issue2) { create(:issue, project: project, title: 'hij', description: 'klm') }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
@@ -81,7 +81,7 @@ describe 'Issue Boards add issue modal', :js do
expect(page).to have_content('2')
end
- expect(page).to have_selector('.card', count: 2)
+ expect(page).to have_selector('.board-card', count: 2)
end
end
@@ -89,7 +89,7 @@ describe 'Issue Boards add issue modal', :js do
page.within('.add-issues-modal') do
click_link 'Selected issues'
- expect(page).not_to have_selector('.card')
+ expect(page).not_to have_selector('.board-card')
end
end
@@ -122,7 +122,7 @@ describe 'Issue Boards add issue modal', :js do
wait_for_requests
- expect(page).to have_selector('.card', count: 1)
+ expect(page).to have_selector('.board-card', count: 1)
end
end
@@ -133,7 +133,7 @@ describe 'Issue Boards add issue modal', :js do
wait_for_requests
- expect(page).not_to have_selector('.card')
+ expect(page).not_to have_selector('.board-card')
expect(page).not_to have_content("You haven't added any issues to your project yet")
end
end
@@ -142,7 +142,7 @@ describe 'Issue Boards add issue modal', :js do
context 'selecing issues' do
it 'selects single issue' do
page.within('.add-issues-modal') do
- first('.card .card-number').click
+ first('.board-card .board-card-number').click
page.within('.nav-links') do
expect(page).to have_content('Selected issues 1')
@@ -152,7 +152,7 @@ describe 'Issue Boards add issue modal', :js do
it 'changes button text' do
page.within('.add-issues-modal') do
- first('.card .card-number').click
+ first('.board-card .board-card-number').click
expect(first('.add-issues-footer .btn')).to have_content('Add 1 issue')
end
@@ -160,7 +160,7 @@ describe 'Issue Boards add issue modal', :js do
it 'changes button text with plural' do
page.within('.add-issues-modal') do
- all('.card .card-number').each do |el|
+ all('.board-card .board-card-number').each do |el|
el.click
end
@@ -170,11 +170,11 @@ describe 'Issue Boards add issue modal', :js do
it 'shows only selected issues on selected tab' do
page.within('.add-issues-modal') do
- first('.card .card-number').click
+ first('.board-card .board-card-number').click
click_link 'Selected issues'
- expect(page).to have_selector('.card', count: 1)
+ expect(page).to have_selector('.board-card', count: 1)
end
end
@@ -200,7 +200,7 @@ describe 'Issue Boards add issue modal', :js do
it 'selects all that arent already selected' do
page.within('.add-issues-modal') do
- first('.card .card-number').click
+ first('.board-card .board-card-number').click
expect(page).to have_selector('.is-active', count: 1)
@@ -212,11 +212,11 @@ describe 'Issue Boards add issue modal', :js do
it 'unselects from selected tab' do
page.within('.add-issues-modal') do
- first('.card .card-number').click
+ first('.board-card .board-card-number').click
click_link 'Selected issues'
- first('.card .card-number').click
+ first('.board-card .board-card-number').click
expect(page).not_to have_selector('.is-active')
end
@@ -226,19 +226,19 @@ describe 'Issue Boards add issue modal', :js do
context 'adding issues' do
it 'adds to board' do
page.within('.add-issues-modal') do
- first('.card .card-number').click
+ first('.board-card .board-card-number').click
click_button 'Add 1 issue'
end
page.within(find('.board:nth-child(2)')) do
- expect(page).to have_selector('.card')
+ expect(page).to have_selector('.board-card')
end
end
it 'adds to second list' do
page.within('.add-issues-modal') do
- first('.card .card-number').click
+ first('.board-card .board-card-number').click
click_button planning.title
@@ -248,7 +248,7 @@ describe 'Issue Boards add issue modal', :js do
end
page.within(find('.board:nth-child(3)')) do
- expect(page).to have_selector('.card')
+ expect(page).to have_selector('.board-card')
end
end
end
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index 52ff47d57f9..a0af2dea3ad 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -11,8 +11,8 @@ describe 'Issue Boards', :js do
let!(:user2) { create(:user) }
before do
- project.add_master(user)
- project.add_master(user2)
+ project.add_maintainer(user)
+ project.add_maintainer(user2)
set_cookie('sidebar_collapsed', 'true')
@@ -92,9 +92,9 @@ describe 'Issue Boards', :js do
wait_for_requests
expect(page).to have_selector('.board', count: 4)
- expect(find('.board:nth-child(2)')).to have_selector('.card')
- expect(find('.board:nth-child(3)')).to have_selector('.card')
- expect(find('.board:nth-child(4)')).to have_selector('.card')
+ expect(find('.board:nth-child(2)')).to have_selector('.board-card')
+ expect(find('.board:nth-child(3)')).to have_selector('.board-card')
+ expect(find('.board:nth-child(4)')).to have_selector('.board-card')
end
it 'shows description tooltip on list title' do
@@ -120,9 +120,9 @@ describe 'Issue Boards', :js do
wait_for_requests
- expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0)
- expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0)
- expect(find('.board:nth-child(4)')).to have_selector('.card', count: 1)
+ expect(find('.board:nth-child(2)')).to have_selector('.board-card', count: 0)
+ expect(find('.board:nth-child(3)')).to have_selector('.board-card', count: 0)
+ expect(find('.board:nth-child(4)')).to have_selector('.board-card', count: 1)
end
it 'search list' do
@@ -131,9 +131,9 @@ describe 'Issue Boards', :js do
wait_for_requests
- expect(find('.board:nth-child(2)')).to have_selector('.card', count: 1)
- expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0)
- expect(find('.board:nth-child(4)')).to have_selector('.card', count: 0)
+ expect(find('.board:nth-child(2)')).to have_selector('.board-card', count: 1)
+ expect(find('.board:nth-child(3)')).to have_selector('.board-card', count: 0)
+ expect(find('.board:nth-child(4)')).to have_selector('.board-card', count: 0)
end
it 'allows user to delete board' do
@@ -150,7 +150,7 @@ describe 'Issue Boards', :js do
click_button 'Add list'
wait_for_requests
- find('.dropdown-menu-close').click
+ find('.js-new-board-list').click
page.within(find('.board:nth-child(2)')) do
accept_confirm { find('.board-delete').click }
@@ -171,21 +171,21 @@ describe 'Issue Boards', :js do
page.within(find('.board:nth-child(2)')) do
expect(page.find('.board-header')).to have_content('58')
- expect(page).to have_selector('.card', count: 20)
+ expect(page).to have_selector('.board-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_selector('.board-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
- expect(page).to have_selector('.card', count: 58)
+ expect(page).to have_selector('.board-card', count: 58)
expect(page).to have_content('Showing all issues')
end
end
@@ -204,7 +204,7 @@ describe 'Issue Boards', :js do
wait_for_board_cards(4, 2)
expect(find('.board:nth-child(2)')).not_to have_content(issue9.title)
- expect(find('.board:nth-child(4)')).to have_selector('.card', count: 2)
+ expect(find('.board:nth-child(4)')).to have_selector('.board-card', count: 2)
expect(find('.board:nth-child(4)')).to have_content(issue9.title)
expect(find('.board:nth-child(4)')).not_to have_content(planning.title)
end
@@ -242,7 +242,7 @@ describe 'Issue Boards', :js do
wait_for_board_cards(4, 1)
expect(find('.board:nth-child(3)')).to have_content(issue6.title)
- expect(find('.board:nth-child(3)').all('.card').last).to have_content(development.title)
+ expect(find('.board:nth-child(3)').all('.board-card').last).to have_content(development.title)
end
it 'issue moves between lists' do
@@ -253,7 +253,7 @@ describe 'Issue Boards', :js do
wait_for_board_cards(4, 1)
expect(find('.board:nth-child(2)')).to have_content(issue7.title)
- expect(find('.board:nth-child(2)').all('.card').first).to have_content(planning.title)
+ expect(find('.board:nth-child(2)').all('.board-card').first).to have_content(planning.title)
end
it 'issue moves from closed' do
@@ -335,7 +335,7 @@ describe 'Issue Boards', :js do
wait_for_requests
- expect(page).to have_css('#js-add-list.open')
+ expect(page).to have_css('#js-add-list.show')
end
it 'creates new list from a new label' do
@@ -425,12 +425,12 @@ describe 'Issue Boards', :js do
page.within(find('.board:nth-child(2)')) do
expect(page.find('.board-header')).to have_content('1')
- expect(page).to have_selector('.card', count: 1)
+ expect(page).to have_selector('.board-card', count: 1)
end
page.within(find('.board:nth-child(3)')) do
expect(page.find('.board-header')).to have_content('0')
- expect(page).to have_selector('.card', count: 0)
+ expect(page).to have_selector('.board-card', count: 0)
end
end
@@ -460,19 +460,19 @@ describe 'Issue Boards', :js do
page.within(find('.board:nth-child(2)')) do
expect(page.find('.board-header')).to have_content('51')
- expect(page).to have_selector('.card', count: 20)
+ expect(page).to have_selector('.board-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_selector('.board-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)
+ expect(page).to have_selector('.board-card', count: 51)
expect(page).to have_content('Showing all issues')
end
end
@@ -494,8 +494,8 @@ describe 'Issue Boards', :js do
it 'filters by clicking label button on issue' do
page.within(find('.board:nth-child(2)')) do
- expect(page).to have_selector('.card', count: 8)
- expect(find('.card', match: :first)).to have_content(bug.title)
+ expect(page).to have_selector('.board-card', count: 8)
+ expect(find('.board-card', match: :first)).to have_content(bug.title)
click_button(bug.title)
wait_for_requests
end
@@ -512,13 +512,13 @@ describe 'Issue Boards', :js do
it 'removes label filter by clicking label button on issue' do
page.within(find('.board:nth-child(2)')) do
- page.within(find('.card', match: :first)) do
+ page.within(find('.board-card', match: :first)) do
click_button(bug.title)
end
wait_for_requests
- expect(page).to have_selector('.card', count: 1)
+ expect(page).to have_selector('.board-card', count: 1)
end
wait_for_requests
@@ -589,7 +589,7 @@ describe 'Issue Boards', :js do
def wait_for_board_cards(board_number, expected_cards)
page.within(find(".board:nth-child(#{board_number})")) do
expect(page.find('.board-header')).to have_content(expected_cards.to_s)
- expect(page).to have_selector('.card', count: expected_cards)
+ expect(page).to have_selector('.board-card', count: expected_cards)
end
end
diff --git a/spec/features/boards/issue_ordering_spec.rb b/spec/features/boards/issue_ordering_spec.rb
index 5abd02dbb48..ec0ca21450a 100644
--- a/spec/features/boards/issue_ordering_spec.rb
+++ b/spec/features/boards/issue_ordering_spec.rb
@@ -13,7 +13,7 @@ describe 'Issue Boards', :js do
let!(:issue3) { create(:labeled_issue, project: project, title: 'testing 3', labels: [label], relative_position: 1) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
@@ -30,17 +30,17 @@ describe 'Issue Boards', :js do
it 'has un-ordered issue as last issue' do
page.within(find('.board:nth-child(2)')) do
- expect(all('.card').last).to have_content(issue4.title)
+ expect(all('.board-card').last).to have_content(issue4.title)
end
end
it 'moves un-ordered issue to top of list' do
- drag(from_index: 3, to_index: 0)
+ drag(from_index: 3, to_index: 0, duration: 1180)
wait_for_requests
page.within(find('.board:nth-child(2)')) do
- expect(first('.card')).to have_content(issue4.title)
+ expect(first('.board-card')).to have_content(issue4.title)
end
end
end
@@ -58,7 +58,7 @@ describe 'Issue Boards', :js do
wait_for_requests
- expect(first('.card')).to have_content(issue2.title)
+ expect(first('.board-card')).to have_content(issue2.title)
end
it 'moves from middle to bottom' do
@@ -66,7 +66,7 @@ describe 'Issue Boards', :js do
wait_for_requests
- expect(all('.card').last).to have_content(issue2.title)
+ expect(all('.board-card').last).to have_content(issue2.title)
end
it 'moves from top to bottom' do
@@ -74,7 +74,7 @@ describe 'Issue Boards', :js do
wait_for_requests
- expect(all('.card').last).to have_content(issue3.title)
+ expect(all('.board-card').last).to have_content(issue3.title)
end
it 'moves from bottom to top' do
@@ -82,7 +82,7 @@ describe 'Issue Boards', :js do
wait_for_requests
- expect(first('.card')).to have_content(issue1.title)
+ expect(first('.board-card')).to have_content(issue1.title)
end
it 'moves from top to middle' do
@@ -90,7 +90,7 @@ describe 'Issue Boards', :js do
wait_for_requests
- expect(first('.card')).to have_content(issue2.title)
+ expect(first('.board-card')).to have_content(issue2.title)
end
it 'moves from bottom to middle' do
@@ -98,7 +98,7 @@ describe 'Issue Boards', :js do
wait_for_requests
- expect(all('.card').last).to have_content(issue2.title)
+ expect(all('.board-card').last).to have_content(issue2.title)
end
end
@@ -121,11 +121,11 @@ describe 'Issue Boards', :js do
wait_for_requests
- expect(find('.board:nth-child(2)')).to have_selector('.card', count: 2)
- expect(all('.board')[2]).to have_selector('.card', count: 4)
+ expect(find('.board:nth-child(2)')).to have_selector('.board-card', count: 2)
+ expect(all('.board')[2]).to have_selector('.board-card', count: 4)
page.within(all('.board')[2]) do
- expect(first('.card')).to have_content(issue3.title)
+ expect(first('.board-card')).to have_content(issue3.title)
end
end
@@ -134,11 +134,11 @@ describe 'Issue Boards', :js do
wait_for_requests
- expect(find('.board:nth-child(2)')).to have_selector('.card', count: 2)
- expect(all('.board')[2]).to have_selector('.card', count: 4)
+ expect(find('.board:nth-child(2)')).to have_selector('.board-card', count: 2)
+ expect(all('.board')[2]).to have_selector('.board-card', count: 4)
page.within(all('.board')[2]) do
- expect(all('.card').last).to have_content(issue3.title)
+ expect(all('.board-card').last).to have_content(issue3.title)
end
end
@@ -147,21 +147,22 @@ describe 'Issue Boards', :js do
wait_for_requests
- expect(find('.board:nth-child(2)')).to have_selector('.card', count: 2)
- expect(all('.board')[2]).to have_selector('.card', count: 4)
+ expect(find('.board:nth-child(2)')).to have_selector('.board-card', count: 2)
+ expect(all('.board')[2]).to have_selector('.board-card', count: 4)
page.within(all('.board')[2]) do
- expect(all('.card')[1]).to have_content(issue3.title)
+ expect(all('.board-card')[1]).to have_content(issue3.title)
end
end
end
- def drag(selector: '.board-list', list_from_index: 1, from_index: 0, to_index: 0, list_to_index: 1)
+ def drag(selector: '.board-list', list_from_index: 1, from_index: 0, to_index: 0, list_to_index: 1, duration: 1000)
drag_to(selector: selector,
scrollable: '#board-app',
list_from_index: list_from_index,
from_index: from_index,
to_index: to_index,
- list_to_index: list_to_index)
+ list_to_index: list_to_index,
+ duration: duration)
end
end
diff --git a/spec/features/boards/modal_filter_spec.rb b/spec/features/boards/modal_filter_spec.rb
index 5907bb0840f..615223a2a88 100644
--- a/spec/features/boards/modal_filter_spec.rb
+++ b/spec/features/boards/modal_filter_spec.rb
@@ -10,7 +10,7 @@ describe 'Issue Boards add issue modal filtering', :js do
let!(:issue1) { create(:issue, project: project) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
@@ -38,7 +38,7 @@ describe 'Issue Boards add issue modal filtering', :js do
page.within('.add-issues-modal') do
wait_for_requests
- expect(page).to have_selector('.card', count: 0)
+ expect(page).to have_selector('.board-card', count: 0)
click_button 'Cancel'
end
@@ -48,7 +48,7 @@ describe 'Issue Boards add issue modal filtering', :js do
page.within('.add-issues-modal') do
wait_for_requests
- expect(page).to have_selector('.card', count: 1)
+ expect(page).to have_selector('.board-card', count: 1)
end
end
@@ -62,13 +62,13 @@ describe 'Issue Boards add issue modal filtering', :js do
page.within('.add-issues-modal') do
wait_for_requests
- expect(page).to have_selector('.card', count: 0)
+ expect(page).to have_selector('.board-card', count: 0)
find('.clear-search').click
wait_for_requests
- expect(page).to have_selector('.card', count: 1)
+ expect(page).to have_selector('.board-card', count: 1)
end
end
@@ -90,7 +90,7 @@ describe 'Issue Boards add issue modal filtering', :js do
wait_for_requests
expect(page).to have_selector('.js-visual-token', text: user2.name)
- expect(page).to have_selector('.card', count: 1)
+ expect(page).to have_selector('.board-card', count: 1)
end
end
end
@@ -113,7 +113,7 @@ describe 'Issue Boards add issue modal filtering', :js do
wait_for_requests
expect(page).to have_selector('.js-visual-token', text: 'none')
- expect(page).to have_selector('.card', count: 1)
+ expect(page).to have_selector('.board-card', count: 1)
end
end
@@ -126,7 +126,7 @@ describe 'Issue Boards add issue modal filtering', :js do
wait_for_requests
expect(page).to have_selector('.js-visual-token', text: user2.name)
- expect(page).to have_selector('.card', count: 1)
+ expect(page).to have_selector('.board-card', count: 1)
end
end
end
@@ -148,7 +148,7 @@ describe 'Issue Boards add issue modal filtering', :js do
wait_for_requests
expect(page).to have_selector('.js-visual-token', text: 'upcoming')
- expect(page).to have_selector('.card', count: 0)
+ expect(page).to have_selector('.board-card', count: 0)
end
end
@@ -161,7 +161,7 @@ describe 'Issue Boards add issue modal filtering', :js do
wait_for_requests
expect(page).to have_selector('.js-visual-token', text: milestone.name)
- expect(page).to have_selector('.card', count: 1)
+ expect(page).to have_selector('.board-card', count: 1)
end
end
end
@@ -183,7 +183,7 @@ describe 'Issue Boards add issue modal filtering', :js do
wait_for_requests
expect(page).to have_selector('.js-visual-token', text: 'none')
- expect(page).to have_selector('.card', count: 1)
+ expect(page).to have_selector('.board-card', count: 1)
end
end
@@ -196,7 +196,7 @@ describe 'Issue Boards add issue modal filtering', :js do
wait_for_requests
expect(page).to have_selector('.js-visual-token', text: label.title)
- expect(page).to have_selector('.card', count: 1)
+ expect(page).to have_selector('.board-card', count: 1)
end
end
end
diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb
index e880f0096c1..0bf1ecbc433 100644
--- a/spec/features/boards/new_issue_spec.rb
+++ b/spec/features/boards/new_issue_spec.rb
@@ -8,7 +8,7 @@ describe 'Issue Boards new issue', :js do
context 'authorized user' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
@@ -64,7 +64,7 @@ describe 'Issue Boards new issue', :js do
expect(page).to have_content('1')
end
- page.within(first('.card')) do
+ page.within(first('.board-card')) do
issue = project.issues.find_by_title('bug')
expect(page).to have_content(issue.to_reference)
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index 4d31123a699..ee38e756f9e 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -15,14 +15,14 @@ describe 'Issue Boards', :js do
let!(:issue2) { create(:labeled_issue, project: project, labels: [development, stretch], relative_position: 1) }
let(:board) { create(:board, project: project) }
let!(:list) { create(:list, board: board, label: development, position: 0) }
- let(:card) { find('.board:nth-child(2)').first('.card') }
+ let(:card) { find('.board:nth-child(2)').first('.board-card') }
around do |example|
Timecop.freeze { example.run }
end
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
@@ -75,7 +75,7 @@ describe 'Issue Boards', :js do
wait_for_requests
page.within(find('.board:nth-child(2)')) do
- expect(page).to have_selector('.card', count: 1)
+ expect(page).to have_selector('.board-card', count: 1)
end
end
@@ -86,11 +86,11 @@ describe 'Issue Boards', :js do
visit project_board_path(project, board)
wait_for_requests
- click_card(find('.board:nth-child(1)').first('.card'))
+ click_card(find('.board:nth-child(1)').first('.board-card'))
expect(find('.issue-boards-sidebar')).not_to have_button 'Remove from board'
- click_card(find('.board:nth-child(3)').first('.card'))
+ click_card(find('.board:nth-child(3)').first('.board-card'))
expect(find('.issue-boards-sidebar')).not_to have_button 'Remove from board'
end
@@ -117,7 +117,7 @@ describe 'Issue Boards', :js do
end
it 'removes the assignee' do
- card_two = find('.board:nth-child(2)').find('.card:nth-child(2)')
+ card_two = find('.board:nth-child(2)').find('.board-card:nth-child(2)')
click_card(card_two)
page.within('.assignee') do
@@ -171,7 +171,7 @@ describe 'Issue Boards', :js do
end
page.within(find('.board:nth-child(2)')) do
- find('.card:nth-child(2)').click
+ find('.board-card:nth-child(2)').click
end
page.within('.assignee') do
@@ -246,7 +246,7 @@ describe 'Issue Boards', :js do
wait_for_requests
page.within('.value') do
- expect(page).to have_selector('.label', count: 2)
+ expect(page).to have_selector('.badge', count: 2)
expect(page).to have_content(development.title)
expect(page).to have_content(stretch.title)
end
@@ -268,12 +268,12 @@ describe 'Issue Boards', :js do
find('.dropdown-menu-close-icon').click
page.within('.value') do
- expect(page).to have_selector('.label', count: 3)
+ expect(page).to have_selector('.badge', count: 3)
expect(page).to have_content(bug.title)
end
end
- expect(card).to have_selector('.label', count: 3)
+ expect(card).to have_selector('.badge', count: 3)
expect(card).to have_content(bug.title)
end
@@ -293,13 +293,13 @@ describe 'Issue Boards', :js do
find('.dropdown-menu-close-icon').click
page.within('.value') do
- expect(page).to have_selector('.label', count: 4)
+ expect(page).to have_selector('.badge', count: 4)
expect(page).to have_content(bug.title)
expect(page).to have_content(regression.title)
end
end
- expect(card).to have_selector('.label', count: 4)
+ expect(card).to have_selector('.badge', count: 4)
expect(card).to have_content(bug.title)
expect(card).to have_content(regression.title)
end
@@ -321,12 +321,12 @@ describe 'Issue Boards', :js do
find('.dropdown-menu-close-icon').click
page.within('.value') do
- expect(page).to have_selector('.label', count: 1)
+ expect(page).to have_selector('.badge', count: 1)
expect(page).not_to have_content(stretch.title)
end
end
- expect(card).to have_selector('.label', count: 1)
+ expect(card).to have_selector('.badge', count: 1)
expect(card).not_to have_content(stretch.title)
end
diff --git a/spec/features/boards/sub_group_project_spec.rb b/spec/features/boards/sub_group_project_spec.rb
index 5fdb8044db2..de2cb4c335e 100644
--- a/spec/features/boards/sub_group_project_spec.rb
+++ b/spec/features/boards/sub_group_project_spec.rb
@@ -11,7 +11,7 @@ describe 'Sub-group project issue boards', :js do
let!(:issue) { create(:labeled_issue, project: project, labels: [label]) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
@@ -20,7 +20,7 @@ describe 'Sub-group project issue boards', :js do
end
it 'creates new label from sidebar' do
- find('.card').click
+ find('.board-card').click
page.within '.labels' do
click_link 'Edit'
diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb
index 70faf28e09d..f08946b0593 100644
--- a/spec/features/calendar_spec.rb
+++ b/spec/features/calendar_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Contributions Calendar', :js do
+describe 'Contributions Calendar', :js do
let(:user) { create(:user) }
let(:contributed_project) { create(:project, :public, :repository) }
let(:issue_note) { create(:note, project: contributed_project) }
diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb
index 62a2ec55b00..8989b2051bb 100644
--- a/spec/features/commits_spec.rb
+++ b/spec/features/commits_spec.rb
@@ -47,7 +47,7 @@ describe 'Commits' do
context 'commit status is Ci Build' do
let!(:build) { create(:ci_build, pipeline: pipeline) }
- let(:artifacts_file) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') }
+ let(:artifacts_file) { fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif') }
context 'when logged as developer' do
before do
@@ -89,7 +89,7 @@ describe 'Commits' do
context 'Download artifacts' do
before do
- build.update_attributes(legacy_artifacts_file: artifacts_file)
+ build.update(legacy_artifacts_file: artifacts_file)
end
it do
@@ -146,7 +146,7 @@ describe 'Commits' do
context "when logged as reporter" do
before do
project.add_reporter(user)
- build.update_attributes(legacy_artifacts_file: artifacts_file)
+ build.update(legacy_artifacts_file: artifacts_file)
visit pipeline_path(pipeline)
end
@@ -168,7 +168,7 @@ describe 'Commits' do
project.update(
visibility_level: Gitlab::VisibilityLevel::INTERNAL,
public_builds: false)
- build.update_attributes(legacy_artifacts_file: artifacts_file)
+ build.update(legacy_artifacts_file: artifacts_file)
visit pipeline_path(pipeline)
end
@@ -188,7 +188,7 @@ describe 'Commits' do
let(:branch_name) { 'master' }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit project_commits_path(project, branch_name)
end
diff --git a/spec/features/container_registry_spec.rb b/spec/features/container_registry_spec.rb
index bef2aa9e0e5..9986206f619 100644
--- a/spec/features/container_registry_spec.rb
+++ b/spec/features/container_registry_spec.rb
@@ -16,7 +16,7 @@ describe "Container Registry", :js do
end
context 'when there are no image repositories' do
- scenario 'user visits container registry main page' do
+ it 'user visits container registry main page' do
visit_container_registry
expect(page).to have_content 'No container images'
@@ -29,13 +29,13 @@ describe "Container Registry", :js do
project.container_repositories << container_repository
end
- scenario 'user wants to see multi-level container repository' do
+ it 'user wants to see multi-level container repository' do
visit_container_registry
expect(page).to have_content('my/image')
end
- scenario 'user removes entire container repository' do
+ it 'user removes entire container repository' do
visit_container_registry
expect_any_instance_of(ContainerRepository)
@@ -44,7 +44,7 @@ describe "Container Registry", :js do
click_on(class: 'js-remove-repo')
end
- scenario 'user removes a specific tag from container repository' do
+ it 'user removes a specific tag from container repository' do
visit_container_registry
find('.js-toggle-repo').click
diff --git a/spec/features/cycle_analytics_spec.rb b/spec/features/cycle_analytics_spec.rb
index ef493db3f11..32c75cae0a1 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 do
+describe 'Cycle Analytics', :js do
let(:user) { create(:user) }
let(:guest) { create(:user) }
let(:project) { create(:project, :repository) }
@@ -12,7 +12,7 @@ feature 'Cycle Analytics', :js do
context 'as an allowed user' do
context 'when project is new' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
@@ -39,7 +39,7 @@ feature 'Cycle Analytics', :js do
context "when there's cycle analytics data" do
before do
allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue])
- project.add_master(user)
+ project.add_maintainer(user)
@build = create_cycle(user, project, issue, mr, milestone, pipeline)
deploy_master(user, project)
@@ -95,7 +95,7 @@ feature 'Cycle Analytics', :js do
before do
user.update_attribute(:preferred_language, 'es')
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit project_cycle_analytics_path(project)
wait_for_requests
diff --git a/spec/features/dashboard/activity_spec.rb b/spec/features/dashboard/activity_spec.rb
index a74a8aac2b2..bf91dc121d8 100644
--- a/spec/features/dashboard/activity_spec.rb
+++ b/spec/features/dashboard/activity_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Dashboard > Activity' do
+describe 'Dashboard > Activity' do
let(:user) { create(:user) }
before do
@@ -12,8 +12,8 @@ feature 'Dashboard > Activity' do
visit activity_dashboard_path
end
- it_behaves_like "it has an RSS button with current_user's RSS token"
- it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token"
+ it_behaves_like "it has an RSS button with current_user's feed token"
+ it_behaves_like "an autodiscoverable RSS feed with current_user's feed token"
end
context 'event filters', :js do
@@ -60,13 +60,13 @@ feature 'Dashboard > Activity' do
end
before do
- project.add_master(user)
+ project.add_maintainer(user)
visit activity_dashboard_path
wait_for_requests
end
- scenario 'user should see all events' do
+ it 'user should see all events' do
within '.content_list' do
expect(page).to have_content('pushed new branch')
expect(page).to have_content('joined')
@@ -77,7 +77,7 @@ feature 'Dashboard > Activity' do
end
end
- scenario 'user should see only pushed events' do
+ it 'user should see only pushed events' do
click_link('Push events')
wait_for_requests
@@ -90,7 +90,7 @@ feature 'Dashboard > Activity' do
end
end
- scenario 'user should see only merged events' do
+ it 'user should see only merged events' do
click_link('Merge events')
wait_for_requests
@@ -103,7 +103,7 @@ feature 'Dashboard > Activity' do
end
end
- scenario 'user should see only issues events' do
+ it 'user should see only issues events' do
click_link('Issue events')
wait_for_requests
@@ -117,7 +117,7 @@ feature 'Dashboard > Activity' do
end
end
- scenario 'user should see only comments events' do
+ it 'user should see only comments events' do
click_link('Comments')
wait_for_requests
@@ -130,7 +130,7 @@ feature 'Dashboard > Activity' do
end
end
- scenario 'user should see only joined events' do
+ it 'user should see only joined events' do
click_link('Team')
wait_for_requests
@@ -143,7 +143,7 @@ feature 'Dashboard > Activity' do
end
end
- scenario 'user see selected event after page reloading' do
+ it 'user see selected event after page reloading' do
click_link('Push events')
wait_for_requests
visit activity_dashboard_path
diff --git a/spec/features/dashboard/archived_projects_spec.rb b/spec/features/dashboard/archived_projects_spec.rb
index b36231fd78b..6a0cd848345 100644
--- a/spec/features/dashboard/archived_projects_spec.rb
+++ b/spec/features/dashboard/archived_projects_spec.rb
@@ -6,8 +6,8 @@ RSpec.describe 'Dashboard Archived Project' do
let(:archived_project) { create(:project, :archived) }
before do
- project.add_master(user)
- archived_project.add_master(user)
+ project.add_maintainer(user)
+ archived_project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/dashboard/datetime_on_tooltips_spec.rb b/spec/features/dashboard/datetime_on_tooltips_spec.rb
index 089c388636d..d7234158fa1 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 do
+describe '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 }
@@ -8,7 +8,7 @@ feature 'Tooltips on .timeago dates', :js do
context 'on the activity tab' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
Event.create( project: project, author_id: user.id, action: Event::JOINED,
updated_at: created_date, created_at: created_date)
@@ -27,7 +27,7 @@ feature 'Tooltips on .timeago dates', :js do
context 'on the snippets tab' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
create(:snippet, author: user, updated_at: created_date, created_at: created_date)
sign_in user
diff --git a/spec/features/dashboard/groups_list_spec.rb b/spec/features/dashboard/groups_list_spec.rb
index ed47f7ed390..eceb12e91cd 100644
--- a/spec/features/dashboard/groups_list_spec.rb
+++ b/spec/features/dashboard/groups_list_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Dashboard Groups page', :js do
+describe 'Dashboard Groups page', :js do
let(:user) { create :user }
let(:group) { create(:group) }
let(:nested_group) { create(:group, :nested) }
@@ -65,7 +65,11 @@ feature 'Dashboard Groups page', :js do
fill_in 'filter', with: group.name
wait_for_requests
+ expect(page).to have_content(group.name)
+ expect(page).not_to have_content(nested_group.parent.name)
+
fill_in 'filter', with: ''
+ page.find('[name="filter"]').send_keys(:enter)
wait_for_requests
expect(page).to have_content(group.name)
diff --git a/spec/features/dashboard/issues_filter_spec.rb b/spec/features/dashboard/issues_filter_spec.rb
index bab34ac9346..95e2610dd4a 100644
--- a/spec/features/dashboard/issues_filter_spec.rb
+++ b/spec/features/dashboard/issues_filter_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Dashboard Issues filtering', :js do
+describe 'Dashboard Issues filtering', :js do
include Spec::Support::Helpers::Features::SortingHelpers
let(:user) { create(:user) }
@@ -11,7 +11,7 @@ feature 'Dashboard Issues filtering', :js do
let!(:issue2) { create(:issue, project: project, author: user, assignees: [user], milestone: milestone) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit_issues
@@ -47,15 +47,15 @@ feature 'Dashboard Issues filtering', :js do
it 'updates atom feed link' do
visit_issues(milestone_title: '', assignee_id: user.id)
- link = find('.nav-controls a[title="Subscribe"]')
+ link = find('.nav-controls a[title="Subscribe to RSS feed"]')
params = CGI.parse(URI.parse(link[:href]).query)
auto_discovery_link = find('link[type="application/atom+xml"]', visible: false)
auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query)
- expect(params).to include('rss_token' => [user.rss_token])
+ expect(params).to include('feed_token' => [user.feed_token])
expect(params).to include('milestone_title' => [''])
expect(params).to include('assignee_id' => [user.id.to_s])
- expect(auto_discovery_params).to include('rss_token' => [user.rss_token])
+ expect(auto_discovery_params).to include('feed_token' => [user.feed_token])
expect(auto_discovery_params).to include('milestone_title' => [''])
expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s])
end
diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb
index e41a2e4ce09..4ae062f242a 100644
--- a/spec/features/dashboard/issues_spec.rb
+++ b/spec/features/dashboard/issues_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe 'Dashboard Issues' do
let!(:other_issue) { create :issue, project: project }
before do
- [project, project_with_issues_disabled].each { |project| project.add_master(current_user) }
+ [project, project_with_issues_disabled].each { |project| project.add_maintainer(current_user) }
sign_in(current_user)
visit issues_dashboard_path(assignee_id: current_user.id)
end
@@ -56,8 +56,8 @@ RSpec.describe 'Dashboard Issues' do
expect(page).to have_current_path(issues_dashboard_url(assignee_id: current_user.id, state: 'closed'), url: true)
end
- it_behaves_like "it has an RSS button with current_user's RSS token"
- it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token"
+ it_behaves_like "it has an RSS button with current_user's feed token"
+ it_behaves_like "an autodiscoverable RSS feed with current_user's feed token"
end
describe 'new issue dropdown' do
diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb
index 0965b745c03..f51142f5790 100644
--- a/spec/features/dashboard/merge_requests_spec.rb
+++ b/spec/features/dashboard/merge_requests_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Dashboard Merge Requests' do
+describe 'Dashboard Merge Requests' do
include Spec::Support::Helpers::Features::SortingHelpers
include FilterItemSelectHelper
include ProjectForksHelper
@@ -12,7 +12,7 @@ feature 'Dashboard Merge Requests' do
let(:forked_project) { fork_project(public_project, current_user, repository: true) }
before do
- project.add_master(current_user)
+ project.add_maintainer(current_user)
sign_in(current_user)
end
@@ -20,7 +20,7 @@ feature 'Dashboard Merge Requests' do
let(:project_with_disabled_merge_requests) { create(:project, :merge_requests_disabled) }
before do
- project_with_disabled_merge_requests.add_master(current_user)
+ project_with_disabled_merge_requests.add_maintainer(current_user)
visit merge_requests_dashboard_path
end
diff --git a/spec/features/dashboard/milestone_filter_spec.rb b/spec/features/dashboard/milestone_filter_spec.rb
index 8cd57f4f327..00373050aeb 100644
--- a/spec/features/dashboard/milestone_filter_spec.rb
+++ b/spec/features/dashboard/milestone_filter_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Dashboard > milestone filter', :js do
+describe 'Dashboard > milestone filter', :js do
include FilterItemSelectHelper
let(:user) { create(:user) }
diff --git a/spec/features/dashboard/milestone_tabs_spec.rb b/spec/features/dashboard/milestone_tabs_spec.rb
index 6fcde35f541..21de7c2f06f 100644
--- a/spec/features/dashboard/milestone_tabs_spec.rb
+++ b/spec/features/dashboard/milestone_tabs_spec.rb
@@ -14,7 +14,7 @@ describe 'Dashboard milestone tabs', :js do
let!(:merge_request) { create(:labeled_merge_request, source_project: project, target_project: project, milestone: project_milestone, labels: [label]) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit dashboard_milestone_path(milestone.safe_title, title: milestone.title)
diff --git a/spec/features/dashboard/milestones_spec.rb b/spec/features/dashboard/milestones_spec.rb
index 7787772a958..0db69432702 100644
--- a/spec/features/dashboard/milestones_spec.rb
+++ b/spec/features/dashboard/milestones_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Dashboard > Milestones' do
+describe 'Dashboard > Milestones' do
describe 'as anonymous user' do
before do
visit dashboard_milestones_path
@@ -16,7 +16,7 @@ feature 'Dashboard > Milestones' do
let(:project) { create(:project, namespace: user.namespace) }
let!(:milestone) { create(:milestone, project: project) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit dashboard_milestones_path
end
diff --git a/spec/features/dashboard/project_member_activity_index_spec.rb b/spec/features/dashboard/project_member_activity_index_spec.rb
index 6c3093607b0..498775acff3 100644
--- a/spec/features/dashboard/project_member_activity_index_spec.rb
+++ b/spec/features/dashboard/project_member_activity_index_spec.rb
@@ -1,11 +1,11 @@
require 'spec_helper'
-feature 'Project member activity', :js do
+describe 'Project member activity', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :public, name: 'x', namespace: user.namespace) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
def visit_activities_and_wait_with_event(event_type)
diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb
index 257a3822503..4daacc61d85 100644
--- a/spec/features/dashboard/projects_spec.rb
+++ b/spec/features/dashboard/projects_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Dashboard Projects' do
+describe 'Dashboard Projects' do
let(:user) { create(:user) }
let(:project) { create(:project, :repository, name: 'awesome stuff') }
let(:project2) { create(:project, :public, name: 'Community project') }
@@ -10,7 +10,7 @@ feature 'Dashboard Projects' do
sign_in(user)
end
- it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token" do
+ it_behaves_like "an autodiscoverable RSS feed with current_user's feed token" do
before do
visit dashboard_projects_path
end
@@ -29,9 +29,37 @@ feature 'Dashboard Projects' do
end
end
+ context 'when user has access to the project' do
+ it 'shows role badge' do
+ visit dashboard_projects_path
+
+ page.within '.user-access-role' do
+ expect(page).to have_content('Developer')
+ end
+ end
+
+ context 'when role changes', :use_clean_rails_memory_store_fragment_caching do
+ it 'displays the right role' do
+ visit dashboard_projects_path
+
+ page.within '.user-access-role' do
+ expect(page).to have_content('Developer')
+ end
+
+ project.members.last.update(access_level: 40)
+
+ visit dashboard_projects_path
+
+ page.within '.user-access-role' do
+ expect(page).to have_content('Maintainer')
+ end
+ end
+ end
+ end
+
context 'when last_repository_updated_at, last_activity_at and update_at are present' do
it 'shows the last_repository_updated_at attribute as the update date' do
- project.update_attributes!(last_repository_updated_at: Time.now, last_activity_at: 1.hour.ago)
+ project.update!(last_repository_updated_at: Time.now, last_activity_at: 1.hour.ago)
visit dashboard_projects_path
@@ -39,7 +67,7 @@ feature 'Dashboard Projects' do
end
it 'shows the last_activity_at attribute as the update date' do
- project.update_attributes!(last_repository_updated_at: 1.hour.ago, last_activity_at: Time.now)
+ project.update!(last_repository_updated_at: 1.hour.ago, last_activity_at: Time.now)
visit dashboard_projects_path
@@ -49,7 +77,7 @@ feature 'Dashboard Projects' do
context 'when last_repository_updated_at and last_activity_at are missing' do
it 'shows the updated_at attribute as the update date' do
- project.update_attributes!(last_repository_updated_at: nil, last_activity_at: nil)
+ project.update!(last_repository_updated_at: nil, last_activity_at: nil)
project.touch
visit dashboard_projects_path
@@ -121,7 +149,7 @@ feature 'Dashboard Projects' do
visit dashboard_projects_path
end
- scenario 'shows "Create merge request" button' do
+ it 'shows "Create merge request" button' do
expect(page).to have_content 'You pushed to feature'
within('#content-body') do
diff --git a/spec/features/dashboard/shortcuts_spec.rb b/spec/features/dashboard/shortcuts_spec.rb
index e41bd7a8419..e5c5ab9c039 100644
--- a/spec/features/dashboard/shortcuts_spec.rb
+++ b/spec/features/dashboard/shortcuts_spec.rb
@@ -1,13 +1,13 @@
require 'spec_helper'
-feature 'Dashboard shortcuts', :js do
+describe 'Dashboard shortcuts', :js do
context 'logged in' do
before do
sign_in(create(:user))
visit root_dashboard_path
end
- scenario 'Navigate to tabs' do
+ it 'Navigate to tabs' do
find('body').send_keys([:shift, 'I'])
check_page_title('Issues')
@@ -31,7 +31,7 @@ feature 'Dashboard shortcuts', :js do
visit explore_root_path
end
- scenario 'Navigate to tabs' do
+ it 'Navigate to tabs' do
find('body').send_keys([:shift, 'G'])
find('.nothing-here-block')
diff --git a/spec/features/dashboard/todos/target_state_spec.rb b/spec/features/dashboard/todos/target_state_spec.rb
index 030a86d1c01..d55c32b3082 100644
--- a/spec/features/dashboard/todos/target_state_spec.rb
+++ b/spec/features/dashboard/todos/target_state_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Dashboard > Todo target states' do
+describe 'Dashboard > Todo target states' do
let(:user) { create(:user) }
let(:author) { create(:user) }
let(:project) { create(:project, :public) }
@@ -9,7 +9,7 @@ feature 'Dashboard > Todo target states' do
sign_in(user)
end
- scenario 'on a closed issue todo has closed label' do
+ it 'on a closed issue todo has closed label' do
issue_closed = create(:issue, state: 'closed')
create_todo issue_closed
visit dashboard_todos_path
@@ -19,7 +19,7 @@ feature 'Dashboard > Todo target states' do
end
end
- scenario 'on an open issue todo does not have an open label' do
+ it 'on an open issue todo does not have an open label' do
issue_open = create(:issue)
create_todo issue_open
visit dashboard_todos_path
@@ -29,7 +29,7 @@ feature 'Dashboard > Todo target states' do
end
end
- scenario 'on a merged merge request todo has merged label' do
+ it 'on a merged merge request todo has merged label' do
mr_merged = create(:merge_request, :simple, :merged, author: user)
create_todo mr_merged
visit dashboard_todos_path
@@ -39,7 +39,7 @@ feature 'Dashboard > Todo target states' do
end
end
- scenario 'on a closed merge request todo has closed label' do
+ it 'on a closed merge request todo has closed label' do
mr_closed = create(:merge_request, :simple, :closed, author: user)
create_todo mr_closed
visit dashboard_todos_path
@@ -49,7 +49,7 @@ feature 'Dashboard > Todo target states' do
end
end
- scenario 'on an open merge request todo does not have an open label' do
+ it 'on an open merge request todo does not have an open label' do
mr_open = create(:merge_request, :simple, author: user)
create_todo mr_open
visit dashboard_todos_path
diff --git a/spec/features/dashboard/todos/todos_filtering_spec.rb b/spec/features/dashboard/todos/todos_filtering_spec.rb
index 7b359b0c651..85f865321cf 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 do
+describe '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_sorting_spec.rb b/spec/features/dashboard/todos/todos_sorting_spec.rb
index 10e3ad843fd..b87caaa1c07 100644
--- a/spec/features/dashboard/todos/todos_sorting_spec.rb
+++ b/spec/features/dashboard/todos/todos_sorting_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Dashboard > User sorts todos' do
+describe 'Dashboard > User sorts todos' do
let(:user) { create(:user) }
let(:project) { create(:project) }
diff --git a/spec/features/dashboard/todos/todos_spec.rb b/spec/features/dashboard/todos/todos_spec.rb
index 94133c62b5c..96b22a0f64b 100644
--- a/spec/features/dashboard/todos/todos_spec.rb
+++ b/spec/features/dashboard/todos/todos_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Dashboard Todos' do
+describe 'Dashboard Todos' do
let(:user) { create(:user) }
let(:author) { create(:user) }
let(:project) { create(:project, :public) }
@@ -246,7 +246,7 @@ feature 'Dashboard Todos' do
it 'is has the right number of pages' do
visit dashboard_todos_path
- expect(page).to have_selector('.gl-pagination .page', count: 2)
+ expect(page).to have_selector('.gl-pagination .js-pagination-page', count: 2)
end
describe 'mark all as done', :js do
diff --git a/spec/features/dashboard/user_filters_projects_spec.rb b/spec/features/dashboard/user_filters_projects_spec.rb
index 92f4d4b854c..3746d37b9cd 100644
--- a/spec/features/dashboard/user_filters_projects_spec.rb
+++ b/spec/features/dashboard/user_filters_projects_spec.rb
@@ -7,7 +7,7 @@ describe 'Dashboard > User filters projects' do
let(:project2) { create(:project, name: 'Treasure', namespace: user2.namespace) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/features/discussion_comments/commit_spec.rb b/spec/features/discussion_comments/commit_spec.rb
index 69d35cdbc72..7a3b1d7ed47 100644
--- a/spec/features/discussion_comments/commit_spec.rb
+++ b/spec/features/discussion_comments/commit_spec.rb
@@ -8,7 +8,7 @@ describe 'Discussion Comments Commit', :js do
let(:merge_request) { create(:merge_request, source_project: project) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit project_commit_path(project, sample_commit.id)
diff --git a/spec/features/discussion_comments/issue_spec.rb b/spec/features/discussion_comments/issue_spec.rb
index 9812eaf3420..5ec19460bbd 100644
--- a/spec/features/discussion_comments/issue_spec.rb
+++ b/spec/features/discussion_comments/issue_spec.rb
@@ -6,7 +6,7 @@ describe 'Discussion Comments Issue', :js do
let(:issue) { create(:issue, project: project) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit project_issue_path(project, issue)
diff --git a/spec/features/discussion_comments/merge_request_spec.rb b/spec/features/discussion_comments/merge_request_spec.rb
index b0019c32189..f940e973923 100644
--- a/spec/features/discussion_comments/merge_request_spec.rb
+++ b/spec/features/discussion_comments/merge_request_spec.rb
@@ -6,7 +6,7 @@ describe 'Discussion Comments Merge Request', :js do
let(:merge_request) { create(:merge_request, source_project: project) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit project_merge_request_path(project, merge_request)
diff --git a/spec/features/discussion_comments/snippets_spec.rb b/spec/features/discussion_comments/snippets_spec.rb
index 4a236c4639b..d330e89505e 100644
--- a/spec/features/discussion_comments/snippets_spec.rb
+++ b/spec/features/discussion_comments/snippets_spec.rb
@@ -6,7 +6,7 @@ describe 'Discussion Comments Snippet', :js do
let(:snippet) { create(:project_snippet, :private, project: project, author: user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit project_snippet_path(project, snippet)
diff --git a/spec/features/error_pages_spec.rb b/spec/features/error_pages_spec.rb
new file mode 100644
index 00000000000..cd7bcf29cc9
--- /dev/null
+++ b/spec/features/error_pages_spec.rb
@@ -0,0 +1,42 @@
+require 'spec_helper'
+
+describe 'Error Pages' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+
+ before do
+ sign_in(user)
+ end
+
+ shared_examples 'shows nav links' do
+ it 'shows nav links' do
+ expect(page).to have_link("Home", href: root_path)
+ expect(page).to have_link("Help", href: help_path)
+ expect(page).to have_link(nil, href: destroy_user_session_path)
+ end
+ end
+
+ describe '404' do
+ before do
+ visit '/not-a-real-page'
+ end
+
+ it 'allows user to search' do
+ fill_in 'search', with: 'something'
+ click_button 'Search'
+
+ expect(page).to have_current_path(%r{^/search\?.*search=something.*})
+ end
+
+ it_behaves_like 'shows nav links'
+ end
+
+ describe '403' do
+ before do
+ visit '/'
+ visit edit_project_path(project)
+ end
+
+ it_behaves_like 'shows nav links'
+ end
+end
diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb
index 31862b2e8f4..8d801161148 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 do
+describe '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 801a33979ff..ad02b454aee 100644
--- a/spec/features/explore/groups_list_spec.rb
+++ b/spec/features/explore/groups_list_spec.rb
@@ -35,7 +35,11 @@ describe 'Explore Groups page', :js do
fill_in 'filter', with: group.name
wait_for_requests
+ expect(page).to have_content(group.full_name)
+ expect(page).not_to have_content(public_group.full_name)
+
fill_in 'filter', with: ""
+ page.find('[name="filter"]').send_keys(:enter)
wait_for_requests
expect(page).to have_content(group.full_name)
diff --git a/spec/features/explore/new_menu_spec.rb b/spec/features/explore/new_menu_spec.rb
index 8d5233d0c0f..0a88988ea09 100644
--- a/spec/features/explore/new_menu_spec.rb
+++ b/spec/features/explore/new_menu_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Top Plus Menu', :js do
+describe 'Top Plus Menu', :js do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, :repository, creator: user, namespace: user.namespace) }
@@ -15,7 +15,7 @@ feature 'Top Plus Menu', :js do
sign_in(user)
end
- scenario 'click on New project shows new project page' do
+ it 'click on New project shows new project page' do
visit root_dashboard_path
click_topmenuitem("New project")
@@ -24,7 +24,7 @@ feature 'Top Plus Menu', :js do
expect(page).to have_content('Project name')
end
- scenario 'click on New group shows new group page' do
+ it 'click on New group shows new group page' do
visit root_dashboard_path
click_topmenuitem("New group")
@@ -33,7 +33,7 @@ feature 'Top Plus Menu', :js do
expect(page).to have_content('Group name')
end
- scenario 'click on New snippet shows new snippet page' do
+ it 'click on New snippet shows new snippet page' do
visit root_dashboard_path
click_topmenuitem("New snippet")
@@ -42,7 +42,7 @@ feature 'Top Plus Menu', :js do
expect(page).to have_content('Title')
end
- scenario 'click on New issue shows new issue page' do
+ it 'click on New issue shows new issue page' do
visit project_path(project)
click_topmenuitem("New issue")
@@ -51,7 +51,7 @@ feature 'Top Plus Menu', :js do
expect(page).to have_content('Title')
end
- scenario 'click on New merge request shows new merge request page' do
+ it 'click on New merge request shows new merge request page' do
visit project_path(project)
click_topmenuitem("New merge request")
@@ -61,12 +61,12 @@ feature 'Top Plus Menu', :js do
expect(page).to have_content('Target branch')
end
- scenario 'click on New project snippet shows new snippet page' do
+ it 'click on New project snippet shows new snippet page' do
visit project_path(project)
page.within '.header-content' do
find('.header-new-dropdown-toggle').click
- expect(page).to have_selector('.header-new.dropdown.open', count: 1)
+ expect(page).to have_selector('.header-new.dropdown.show', count: 1)
find('.header-new-project-snippet a').click
end
@@ -74,7 +74,7 @@ feature 'Top Plus Menu', :js do
expect(page).to have_content('Title')
end
- scenario 'Click on New subgroup shows new group page', :nested_groups do
+ it 'Click on New subgroup shows new group page', :nested_groups do
visit group_path(group)
click_topmenuitem("New subgroup")
@@ -83,12 +83,12 @@ feature 'Top Plus Menu', :js do
expect(page).to have_content('Group name')
end
- scenario 'Click on New project in group shows new project page' do
+ it 'Click on New project in group shows new project page' do
visit group_path(group)
page.within '.header-content' do
find('.header-new-dropdown-toggle').click
- expect(page).to have_selector('.header-new.dropdown.open', count: 1)
+ expect(page).to have_selector('.header-new.dropdown.show', count: 1)
find('.header-new-group-project a').click
end
@@ -107,7 +107,7 @@ feature 'Top Plus Menu', :js do
sign_in(guest_user)
end
- scenario 'click on New issue shows new issue page' do
+ it 'click on New issue shows new issue page' do
visit project_path(project)
click_topmenuitem("New issue")
@@ -116,37 +116,37 @@ feature 'Top Plus Menu', :js do
expect(page).to have_content('Title')
end
- scenario 'has no New merge request menu item' do
+ it 'has no New merge request menu item' do
visit project_path(project)
hasnot_topmenuitem("New merge request")
end
- scenario 'has no New project snippet menu item' do
+ it 'has no New project snippet menu item' do
visit project_path(project)
expect(find('.header-new.dropdown')).not_to have_selector('.header-new-project-snippet')
end
- scenario 'public project has no New merge request menu item' do
+ it 'public project has no New merge request menu item' do
visit project_path(public_project)
hasnot_topmenuitem("New merge request")
end
- scenario 'public project has no New project snippet menu item' do
+ it 'public project has no New project snippet menu item' do
visit project_path(public_project)
expect(find('.header-new.dropdown')).not_to have_selector('.header-new-project-snippet')
end
- scenario 'has no New subgroup menu item' do
+ it 'has no New subgroup menu item' do
visit group_path(group)
hasnot_topmenuitem("New subgroup")
end
- scenario 'has no New project for group menu item' do
+ it 'has no New project for group menu item' do
visit group_path(group)
expect(find('.header-new.dropdown')).not_to have_selector('.header-new-group-project')
@@ -156,7 +156,7 @@ feature 'Top Plus Menu', :js do
def click_topmenuitem(item_name)
page.within '.header-content' do
find('.header-new-dropdown-toggle').click
- expect(page).to have_selector('.header-new.dropdown.open', count: 1)
+ expect(page).to have_selector('.header-new.dropdown.show', count: 1)
click_link item_name
end
end
diff --git a/spec/features/global_search_spec.rb b/spec/features/global_search_spec.rb
index f8c4db1403c..d7692181453 100644
--- a/spec/features/global_search_spec.rb
+++ b/spec/features/global_search_spec.rb
@@ -1,11 +1,11 @@
require 'spec_helper'
-feature 'Global search' do
+describe 'Global search' do
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/features/group_variables_spec.rb b/spec/features/group_variables_spec.rb
index f7863807572..89e0cdd8ed7 100644
--- a/spec/features/group_variables_spec.rb
+++ b/spec/features/group_variables_spec.rb
@@ -1,13 +1,13 @@
require 'spec_helper'
-feature 'Group variables', :js do
+describe 'Group variables', :js do
let(:user) { create(:user) }
let(:group) { create(:group) }
let!(:variable) { create(:ci_group_variable, key: 'test_key', value: 'test value', group: group) }
let(:page_path) { group_settings_ci_cd_path(group) }
- background do
- group.add_master(user)
+ before do
+ group.add_maintainer(user)
gitlab_sign_in(user)
visit page_path
diff --git a/spec/features/groups/activity_spec.rb b/spec/features/groups/activity_spec.rb
index 7bc809b3104..88fc12ae1e4 100644
--- a/spec/features/groups/activity_spec.rb
+++ b/spec/features/groups/activity_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Group activity page' do
+describe 'Group activity page' do
let(:user) { create(:group_member, :developer, user: create(:user), group: group ).user }
let(:group) { create(:group) }
let(:path) { activity_group_path(group) }
@@ -15,15 +15,15 @@ feature 'Group activity page' do
visit path
end
- it_behaves_like "it has an RSS button with current_user's RSS token"
- it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token"
+ it_behaves_like "it has an RSS button with current_user's feed token"
+ it_behaves_like "an autodiscoverable RSS feed with current_user's feed token"
end
context 'when project is in the group', :js do
let(:project) { create(:project, :public, namespace: group) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
visit path
end
@@ -39,7 +39,7 @@ feature 'Group activity page' do
visit path
end
- it_behaves_like "it has an RSS button without an RSS token"
- it_behaves_like "an autodiscoverable RSS feed without an RSS token"
+ it_behaves_like "it has an RSS button without a feed token"
+ it_behaves_like "an autodiscoverable RSS feed without a feed token"
end
end
diff --git a/spec/features/groups/empty_states_spec.rb b/spec/features/groups/empty_states_spec.rb
index 04217fec06c..8f5ca781b2c 100644
--- a/spec/features/groups/empty_states_spec.rb
+++ b/spec/features/groups/empty_states_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Group empty states' do
+describe 'Group empty states' do
let(:group) { create(:group) }
let(:user) { create(:group_member, :developer, user: create(:user), group: group ).user }
@@ -19,7 +19,7 @@ feature 'Group empty states' do
let(:project) { create(:project, namespace: group) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
context "the project has #{issuable_name}s" do
@@ -59,6 +59,18 @@ feature 'Group empty states' do
end
end
+ shared_examples "no projects" do
+ it 'displays an empty state' do
+ expect(page).to have_selector('.empty-state')
+ end
+
+ it "does not show a new #{issuable_name} button" do
+ within '.empty-state' do
+ expect(page).not_to have_link("create #{issuable_name}")
+ end
+ end
+ end
+
context 'group without a project' do
context 'group has a subgroup', :nested_groups do
let(:subgroup) { create(:group, parent: group) }
@@ -92,16 +104,18 @@ feature 'Group empty states' do
visit path
end
- it 'displays an empty state' do
- expect(page).to have_selector('.empty-state')
- end
+ it_behaves_like "no projects"
+ end
+ end
- it "shows a new #{issuable_name} button" do
- within '.empty-state' do
- expect(page).not_to have_link("create #{issuable_name}")
- end
- end
+ context 'group has only a project with issues disabled' do
+ let(:project_with_issues_disabled) { create(:empty_project, :issues_disabled, group: group) }
+
+ before do
+ visit path
end
+
+ it_behaves_like "no projects"
end
end
end
diff --git a/spec/features/groups/group_settings_spec.rb b/spec/features/groups/group_settings_spec.rb
index 1ce30015e81..59254ecc982 100644
--- a/spec/features/groups/group_settings_spec.rb
+++ b/spec/features/groups/group_settings_spec.rb
@@ -1,10 +1,10 @@
require 'spec_helper'
-feature 'Edit group settings' do
- given(:user) { create(:user) }
- given(:group) { create(:group, path: 'foo') }
+describe 'Edit group settings' do
+ let(:user) { create(:user) }
+ let(:group) { create(:group, path: 'foo') }
- background do
+ before do
group.add_owner(user)
sign_in(user)
end
@@ -14,14 +14,14 @@ feature 'Edit group settings' do
let(:old_group_full_path) { "/#{group.path}" }
let(:new_group_full_path) { "/#{new_group_path}" }
- scenario 'the group is accessible via the new path' do
+ it 'the group is accessible via the new path' do
update_path(new_group_path)
visit new_group_full_path
expect(current_path).to eq(new_group_full_path)
expect(find('h1.group-title')).to have_content(group.name)
end
- scenario 'the old group path redirects to the new path' do
+ it 'the old group path redirects to the new path' do
update_path(new_group_path)
visit old_group_full_path
expect(current_path).to eq(new_group_full_path)
@@ -29,18 +29,18 @@ feature 'Edit group settings' do
end
context 'with a subgroup' do
- given!(:subgroup) { create(:group, parent: group, path: 'subgroup') }
- given(:old_subgroup_full_path) { "/#{group.path}/#{subgroup.path}" }
- given(:new_subgroup_full_path) { "/#{new_group_path}/#{subgroup.path}" }
+ let!(:subgroup) { create(:group, parent: group, path: 'subgroup') }
+ let(:old_subgroup_full_path) { "/#{group.path}/#{subgroup.path}" }
+ let(:new_subgroup_full_path) { "/#{new_group_path}/#{subgroup.path}" }
- scenario 'the subgroup is accessible via the new path' do
+ it 'the subgroup is accessible via the new path' do
update_path(new_group_path)
visit new_subgroup_full_path
expect(current_path).to eq(new_subgroup_full_path)
expect(find('h1.group-title')).to have_content(subgroup.name)
end
- scenario 'the old subgroup path redirects to the new path' do
+ it 'the old subgroup path redirects to the new path' do
update_path(new_group_path)
visit old_subgroup_full_path
expect(current_path).to eq(new_subgroup_full_path)
@@ -49,9 +49,9 @@ feature 'Edit group settings' do
end
context 'with a project' do
- given!(:project) { create(:project, group: group) }
- given(:old_project_full_path) { "/#{group.path}/#{project.path}" }
- given(:new_project_full_path) { "/#{new_group_path}/#{project.path}" }
+ let!(:project) { create(:project, group: group) }
+ let(:old_project_full_path) { "/#{group.path}/#{project.path}" }
+ let(:new_project_full_path) { "/#{new_group_path}/#{project.path}" }
before(:context) do
TestEnv.clean_test_path
@@ -61,14 +61,14 @@ feature 'Edit group settings' do
TestEnv.clean_test_path
end
- scenario 'the project is accessible via the new path' do
+ it 'the project is accessible via the new path' do
update_path(new_group_path)
visit new_project_full_path
expect(current_path).to eq(new_project_full_path)
expect(find('.breadcrumbs')).to have_content(project.path)
end
- scenario 'the old project path redirects to the new path' do
+ it 'the old project path redirects to the new path' do
update_path(new_group_path)
visit old_project_full_path
expect(current_path).to eq(new_project_full_path)
@@ -83,7 +83,7 @@ feature 'Edit group settings' do
attach_file(:group_avatar, Rails.root.join('spec', 'fixtures', 'banana_sample.gif'))
- expect { click_button 'Save group' }.to change { group.reload.avatar? }.to(true)
+ expect { save_group }.to change { group.reload.avatar? }.to(true)
end
it 'uploads new group avatar' do
@@ -97,10 +97,19 @@ feature 'Edit group settings' do
expect(page).not_to have_link('Remove avatar')
end
end
-end
-def update_path(new_group_path)
- visit edit_group_path(group)
- fill_in 'group_path', with: new_group_path
- click_button 'Save group'
+ def update_path(new_group_path)
+ visit edit_group_path(group)
+
+ page.within('.gs-advanced') do
+ fill_in 'group_path', with: new_group_path
+ click_button 'Change group path'
+ end
+ end
+
+ def save_group
+ page.within('.gs-general') do
+ click_button 'Save group'
+ end
+ end
end
diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb
index 90bf7ba49f6..97d8776b15a 100644
--- a/spec/features/groups/issues_spec.rb
+++ b/spec/features/groups/issues_spec.rb
@@ -1,10 +1,11 @@
require 'spec_helper'
-feature 'Group issues page' do
+describe 'Group issues page' do
include FilteredSearchHelpers
let(:group) { create(:group) }
let(:project) { create(:project, :public, group: group)}
+ let(:project_with_issues_disabled) { create(:project, :issues_disabled, group: group) }
let(:path) { issues_group_path(group) }
context 'with shared examples' do
@@ -16,17 +17,21 @@ feature 'Group issues page' do
let(:access_level) { ProjectFeature::ENABLED }
context 'when signed in' do
- let(:user) { user_in_group }
-
- it_behaves_like "it has an RSS button with current_user's RSS token"
- it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token"
+ let(:user) do
+ user_in_group.ensure_feed_token
+ user_in_group.save!
+ user_in_group
+ end
+
+ it_behaves_like "it has an RSS button with current_user's feed token"
+ it_behaves_like "an autodiscoverable RSS feed with current_user's feed token"
end
context 'when signed out' do
let(:user) { nil }
- it_behaves_like "it has an RSS button without an RSS token"
- it_behaves_like "an autodiscoverable RSS feed without an RSS token"
+ it_behaves_like "it has an RSS button without a feed token"
+ it_behaves_like "an autodiscoverable RSS feed without a feed token"
end
end
@@ -72,4 +77,25 @@ feature 'Group issues page' do
end
end
end
+
+ context 'projects with issues disabled' do
+ describe 'issue dropdown' do
+ let(:user_in_group) { create(:group_member, :maintainer, user: create(:user), group: group ).user }
+
+ before do
+ [project, project_with_issues_disabled].each { |project| project.add_maintainer(user_in_group) }
+ sign_in(user_in_group)
+ visit issues_group_path(group)
+ end
+
+ it 'shows projects only with issues feature enabled', :js do
+ find('.new-project-item-link').click
+
+ page.within('.select2-results') do
+ expect(page).to have_content(project.full_name)
+ expect(page).not_to have_content(project_with_issues_disabled.full_name)
+ end
+ end
+ end
+ end
end
diff --git a/spec/features/groups/labels/edit_spec.rb b/spec/features/groups/labels/edit_spec.rb
index fb338127861..7cfc27a8905 100644
--- a/spec/features/groups/labels/edit_spec.rb
+++ b/spec/features/groups/labels/edit_spec.rb
@@ -1,17 +1,17 @@
require 'spec_helper'
-feature 'Edit group label' do
- given(:user) { create(:user) }
- given(:group) { create(:group) }
- given(:label) { create(:group_label, group: group) }
+describe 'Edit group label' do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+ let(:label) { create(:group_label, group: group) }
- background do
+ before do
group.add_owner(user)
sign_in(user)
visit edit_group_label_path(group, label)
end
- scenario 'update label with new title' do
+ it 'update label with new title' do
fill_in 'label_title', with: 'new label name'
click_button 'Save changes'
diff --git a/spec/features/groups/labels/index_spec.rb b/spec/features/groups/labels/index_spec.rb
new file mode 100644
index 00000000000..0ce7dad4040
--- /dev/null
+++ b/spec/features/groups/labels/index_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe 'Group labels' do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+ let!(:label) { create(:group_label, group: group) }
+
+ before do
+ group.add_owner(user)
+ sign_in(user)
+ visit group_labels_path(group)
+ end
+
+ it 'label has edit button', :js do
+ expect(page).to have_selector('.label-action.edit')
+ end
+end
diff --git a/spec/features/groups/labels/subscription_spec.rb b/spec/features/groups/labels/subscription_spec.rb
index 2e06caf98f6..d9543bfa97f 100644
--- a/spec/features/groups/labels/subscription_spec.rb
+++ b/spec/features/groups/labels/subscription_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Labels subscription' do
+describe 'Labels subscription' do
let(:user) { create(:user) }
let(:group) { create(:group) }
let!(:feature) { create(:group_label, group: group, title: 'feature') }
@@ -11,7 +11,7 @@ feature 'Labels subscription' do
gitlab_sign_in user
end
- scenario 'users can subscribe/unsubscribe to group labels', :js do
+ it '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/labels/user_sees_links_to_issuables.rb b/spec/features/groups/labels/user_sees_links_to_issuables.rb
index 5d6290d2109..1fdba78fa6c 100644
--- a/spec/features/groups/labels/user_sees_links_to_issuables.rb
+++ b/spec/features/groups/labels/user_sees_links_to_issuables.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Groups > Labels > User sees links to issuables' do
+describe 'Groups > Labels > User sees links to issuables' do
set(:group) { create(:group, :public) }
before do
@@ -8,7 +8,7 @@ feature 'Groups > Labels > User sees links to issuables' do
visit group_labels_path(group)
end
- scenario 'shows links to MRs and issues' do
+ it 'shows links to MRs and issues' do
expect(page).to have_link('view merge requests')
expect(page).to have_link('view open issues')
end
diff --git a/spec/features/groups/members/filter_members_spec.rb b/spec/features/groups/members/filter_members_spec.rb
index 5ddb5894624..386d81546d7 100644
--- a/spec/features/groups/members/filter_members_spec.rb
+++ b/spec/features/groups/members/filter_members_spec.rb
@@ -1,18 +1,18 @@
require 'spec_helper'
-feature 'Groups > Members > Filter members' do
+describe 'Groups > Members > Filter members' do
let(:user) { create(:user) }
let(:user_with_2fa) { create(:user, :two_factor_via_otp) }
let(:group) { create(:group) }
- background do
+ before do
group.add_owner(user)
- group.add_master(user_with_2fa)
+ group.add_maintainer(user_with_2fa)
sign_in(user)
end
- scenario 'shows all members' do
+ it 'shows all members' do
visit_members_list
expect(first_member).to include(user.name)
@@ -20,7 +20,7 @@ feature 'Groups > Members > Filter members' do
expect(page).to have_css('.member-filter-2fa-dropdown .dropdown-toggle-text', text: '2FA: Everyone')
end
- scenario 'shows only 2FA members' do
+ it 'shows only 2FA members' do
visit_members_list(two_factor: 'enabled')
expect(first_member).to include(user_with_2fa.name)
@@ -28,7 +28,7 @@ feature 'Groups > Members > Filter members' do
expect(page).to have_css('.member-filter-2fa-dropdown .dropdown-toggle-text', text: '2FA: Enabled')
end
- scenario 'shows only non 2FA members' do
+ it 'shows only non 2FA members' do
visit_members_list(two_factor: 'disabled')
expect(first_member).to include(user.name)
diff --git a/spec/features/groups/members/leave_group_spec.rb b/spec/features/groups/members/leave_group_spec.rb
index 067a2dc850f..7a91c64d7db 100644
--- a/spec/features/groups/members/leave_group_spec.rb
+++ b/spec/features/groups/members/leave_group_spec.rb
@@ -1,15 +1,15 @@
require 'spec_helper'
-feature 'Groups > Members > Leave group' do
+describe 'Groups > Members > Leave group' do
let(:user) { create(:user) }
let(:other_user) { create(:user) }
let(:group) { create(:group) }
- background do
+ before do
gitlab_sign_in(user)
end
- scenario 'guest leaves the group' do
+ it 'guest leaves the group' do
group.add_guest(user)
group.add_owner(other_user)
@@ -21,7 +21,7 @@ feature 'Groups > Members > Leave group' do
expect(group.users).not_to include(user)
end
- scenario 'guest leaves the group as last member' do
+ it 'guest leaves the group as last member' do
group.add_guest(user)
visit group_path(group)
@@ -32,7 +32,7 @@ feature 'Groups > Members > Leave group' do
expect(group.users).not_to include(user)
end
- scenario 'owner leaves the group if they is not the last owner' do
+ it 'owner leaves the group if they is not the last owner' do
group.add_owner(user)
group.add_owner(other_user)
@@ -44,7 +44,7 @@ feature 'Groups > Members > Leave group' do
expect(group.users).not_to include(user)
end
- scenario 'owner can not leave the group if they is a last owner' do
+ it 'owner can not leave the group if they is a last owner' do
group.add_owner(user)
visit group_path(group)
diff --git a/spec/features/groups/members/list_members_spec.rb b/spec/features/groups/members/list_members_spec.rb
index 5c5d48c3623..33f93fcc470 100644
--- a/spec/features/groups/members/list_members_spec.rb
+++ b/spec/features/groups/members/list_members_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Groups > Members > List members' do
+describe 'Groups > Members > List members' do
include Select2Helper
let(:user1) { create(:user, name: 'John Doe') }
@@ -8,11 +8,11 @@ feature 'Groups > Members > List members' do
let(:group) { create(:group) }
let(:nested_group) { create(:group, parent: group) }
- background do
+ before do
gitlab_sign_in(user1)
end
- scenario 'show members from current group and parent', :nested_groups do
+ it 'show members from current group and parent', :nested_groups do
group.add_developer(user1)
nested_group.add_developer(user2)
@@ -22,7 +22,7 @@ feature 'Groups > Members > List members' do
expect(second_row.text).to include(user2.name)
end
- scenario 'show user once if member of both current group and parent', :nested_groups do
+ it 'show user once if member of both current group and parent', :nested_groups do
group.add_developer(user1)
nested_group.add_developer(user1)
diff --git a/spec/features/groups/members/manage_members_spec.rb b/spec/features/groups/members/manage_members_spec.rb
index 21f7b4999ad..0eda2c7f26d 100644
--- a/spec/features/groups/members/manage_members_spec.rb
+++ b/spec/features/groups/members/manage_members_spec.rb
@@ -1,17 +1,17 @@
require 'spec_helper'
-feature 'Groups > Members > Manage members' do
+describe 'Groups > Members > Manage members' do
include Select2Helper
let(:user1) { create(:user, name: 'John Doe') }
let(:user2) { create(:user, name: 'Mary Jane') }
let(:group) { create(:group) }
- background do
+ before do
sign_in(user1)
end
- scenario 'update user to owner level', :js do
+ it 'update user to owner level', :js do
group.add_owner(user1)
group.add_developer(user2)
@@ -25,7 +25,7 @@ feature 'Groups > Members > Manage members' do
end
end
- scenario 'add user to group', :js do
+ it 'add user to group', :js do
group.add_owner(user1)
visit group_group_members_path(group)
@@ -38,7 +38,7 @@ feature 'Groups > Members > Manage members' do
end
end
- scenario 'do not disclose email addresses', :js do
+ it 'do not disclose email addresses', :js do
group.add_owner(user1)
create(:user, email: 'undisclosed_email@gitlab.com', name: "Jane 'invisible' Doe")
@@ -59,7 +59,7 @@ feature 'Groups > Members > Manage members' do
expect(page).to have_content("Jane 'invisible' Doe")
end
- scenario 'remove user from group', :js do
+ it 'remove user from group', :js do
group.add_owner(user1)
group.add_developer(user2)
@@ -75,7 +75,7 @@ feature 'Groups > Members > Manage members' do
expect(group.users).not_to include(user2)
end
- scenario 'add yourself to group when already an owner', :js do
+ it 'add yourself to group when already an owner', :js do
group.add_owner(user1)
visit group_group_members_path(group)
@@ -88,7 +88,7 @@ feature 'Groups > Members > Manage members' do
end
end
- scenario 'invite user to group', :js do
+ it 'invite user to group', :js do
group.add_owner(user1)
visit group_group_members_path(group)
@@ -102,7 +102,7 @@ feature 'Groups > Members > Manage members' do
end
end
- scenario 'guest can not manage other users' do
+ it 'guest can not manage other users' do
group.add_guest(user1)
group.add_developer(user2)
diff --git a/spec/features/groups/members/master_manages_access_requests_spec.rb b/spec/features/groups/members/master_manages_access_requests_spec.rb
index 2fd6d1ec599..bd615c99412 100644
--- a/spec/features/groups/members/master_manages_access_requests_spec.rb
+++ b/spec/features/groups/members/master_manages_access_requests_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
-feature 'Groups > Members > Master manages access requests' do
- it_behaves_like 'Master manages access requests' do
+describe 'Groups > Members > Maintainer manages access requests' do
+ it_behaves_like 'Maintainer manages access requests' do
let(:entity) { create(:group, :public, :access_requestable) }
let(:members_page_path) { group_group_members_path(entity) }
end
diff --git a/spec/features/groups/members/request_access_spec.rb b/spec/features/groups/members/request_access_spec.rb
index 10389a74703..94510f917a3 100644
--- a/spec/features/groups/members/request_access_spec.rb
+++ b/spec/features/groups/members/request_access_spec.rb
@@ -1,25 +1,25 @@
require 'spec_helper'
-feature 'Groups > Members > Request access' do
+describe 'Groups > Members > Request access' do
let(:user) { create(:user) }
let(:owner) { create(:user) }
let(:group) { create(:group, :public, :access_requestable) }
let!(:project) { create(:project, :private, namespace: group) }
- background do
+ before do
group.add_owner(owner)
sign_in(user)
visit group_path(group)
end
- scenario 'request access feature is disabled' do
- group.update_attributes(request_access_enabled: false)
+ it 'request access feature is disabled' do
+ group.update(request_access_enabled: false)
visit group_path(group)
expect(page).not_to have_content 'Request Access'
end
- scenario 'user can request access to a group' do
+ it 'user can request access to a group' do
perform_enqueued_jobs { click_link 'Request Access' }
expect(ActionMailer::Base.deliveries.last.to).to eq [owner.notification_email]
@@ -32,13 +32,13 @@ feature 'Groups > Members > Request access' do
expect(page).not_to have_content 'Leave group'
end
- scenario 'user does not see private projects' do
+ it 'user does not see private projects' do
perform_enqueued_jobs { click_link 'Request Access' }
expect(page).not_to have_content project.name
end
- scenario 'user does not see group in the Dashboard > Groups page' do
+ it 'user does not see group in the Dashboard > Groups page' do
perform_enqueued_jobs { click_link 'Request Access' }
visit dashboard_groups_path
@@ -46,7 +46,7 @@ feature 'Groups > Members > Request access' do
expect(page).not_to have_content group.name
end
- scenario 'user is not listed in the group members page' do
+ it 'user is not listed in the group members page' do
click_link 'Request Access'
expect(group.requesters.exists?(user_id: user)).to be_truthy
@@ -58,7 +58,7 @@ feature 'Groups > Members > Request access' do
end
end
- scenario 'user can withdraw its request for access' do
+ it 'user can withdraw its request for access' do
click_link 'Request Access'
expect(group.requesters.exists?(user_id: user)).to be_truthy
@@ -69,7 +69,7 @@ feature 'Groups > Members > Request access' do
expect(page).to have_content 'Your access request to the group has been withdrawn.'
end
- scenario 'member does not see the request access button' do
+ it 'member does not see the request access button' do
group.add_owner(user)
visit group_path(group)
diff --git a/spec/features/groups/members/search_members_spec.rb b/spec/features/groups/members/search_members_spec.rb
index 31fbbcf562c..e7efdf7dfef 100644
--- a/spec/features/groups/members/search_members_spec.rb
+++ b/spec/features/groups/members/search_members_spec.rb
@@ -22,7 +22,7 @@ describe 'Search group member' do
find('.member-search-btn').click
end
- group_members_list = find(".panel .content-list")
+ group_members_list = find(".card .content-list")
expect(group_members_list).to have_content(member.name)
expect(group_members_list).not_to have_content(user.name)
end
diff --git a/spec/features/groups/members/sort_members_spec.rb b/spec/features/groups/members/sort_members_spec.rb
index e175ad04f86..ee32f6d77fe 100644
--- a/spec/features/groups/members/sort_members_spec.rb
+++ b/spec/features/groups/members/sort_members_spec.rb
@@ -1,18 +1,18 @@
require 'spec_helper'
-feature 'Groups > Members > Sort members' do
+describe 'Groups > Members > Sort members' do
let(:owner) { create(:user, name: 'John Doe') }
let(:developer) { create(:user, name: 'Mary Jane', last_sign_in_at: 5.days.ago) }
let(:group) { create(:group) }
- background do
+ before do
create(:group_member, :owner, user: owner, group: group, created_at: 5.days.ago)
create(:group_member, :developer, user: developer, group: group, created_at: 3.days.ago)
sign_in(owner)
end
- scenario 'sorts alphabetically by default' do
+ it 'sorts alphabetically by default' do
visit_members_list(sort: nil)
expect(first_member).to include(owner.name)
@@ -20,7 +20,7 @@ feature 'Groups > Members > Sort members' do
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending')
end
- scenario 'sorts by access level ascending' do
+ it 'sorts by access level ascending' do
visit_members_list(sort: :access_level_asc)
expect(first_member).to include(developer.name)
@@ -28,7 +28,7 @@ feature 'Groups > Members > Sort members' do
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Access level, ascending')
end
- scenario 'sorts by access level descending' do
+ it 'sorts by access level descending' do
visit_members_list(sort: :access_level_desc)
expect(first_member).to include(owner.name)
@@ -36,7 +36,7 @@ feature 'Groups > Members > Sort members' do
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Access level, descending')
end
- scenario 'sorts by last joined' do
+ it 'sorts by last joined' do
visit_members_list(sort: :last_joined)
expect(first_member).to include(developer.name)
@@ -44,7 +44,7 @@ feature 'Groups > Members > Sort members' do
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Last joined')
end
- scenario 'sorts by oldest joined' do
+ it 'sorts by oldest joined' do
visit_members_list(sort: :oldest_joined)
expect(first_member).to include(owner.name)
@@ -52,7 +52,7 @@ feature 'Groups > Members > Sort members' do
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Oldest joined')
end
- scenario 'sorts by name ascending' do
+ it 'sorts by name ascending' do
visit_members_list(sort: :name_asc)
expect(first_member).to include(owner.name)
@@ -60,7 +60,7 @@ feature 'Groups > Members > Sort members' do
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending')
end
- scenario 'sorts by name descending' do
+ it 'sorts by name descending' do
visit_members_list(sort: :name_desc)
expect(first_member).to include(developer.name)
@@ -68,7 +68,7 @@ feature 'Groups > Members > Sort members' do
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, descending')
end
- scenario 'sorts by recent sign in', :clean_gitlab_redis_shared_state do
+ it 'sorts by recent sign in', :clean_gitlab_redis_shared_state do
visit_members_list(sort: :recent_sign_in)
expect(first_member).to include(owner.name)
@@ -76,7 +76,7 @@ feature 'Groups > Members > Sort members' do
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Recent sign in')
end
- scenario 'sorts by oldest sign in', :clean_gitlab_redis_shared_state do
+ it 'sorts by oldest sign in', :clean_gitlab_redis_shared_state do
visit_members_list(sort: :oldest_sign_in)
expect(first_member).to include(developer.name)
diff --git a/spec/features/groups/merge_requests_spec.rb b/spec/features/groups/merge_requests_spec.rb
index 672ae785c2d..54a8016c157 100644
--- a/spec/features/groups/merge_requests_spec.rb
+++ b/spec/features/groups/merge_requests_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Group merge requests page' do
+describe 'Group merge requests page' do
include FilteredSearchHelpers
let(:path) { merge_requests_group_path(group) }
@@ -56,4 +56,21 @@ feature 'Group merge requests page' do
expect(find('#js-dropdown-assignee .filter-dropdown')).not_to have_content(user2.name)
end
end
+
+ describe 'new merge request dropdown' do
+ let(:project_with_merge_requests_disabled) { create(:project, :merge_requests_disabled, group: group) }
+
+ before do
+ visit path
+ end
+
+ it 'shows projects only with merge requests feature enabled', :js do
+ find('.new-project-item-link').click
+
+ page.within('.select2-results') do
+ expect(page).to have_content(project.name_with_namespace)
+ expect(page).not_to have_content(project_with_merge_requests_disabled.name_with_namespace)
+ end
+ end
+ end
end
diff --git a/spec/features/groups/milestone_spec.rb b/spec/features/groups/milestone_spec.rb
index 20337f1d3b0..80df0618a6a 100644
--- a/spec/features/groups/milestone_spec.rb
+++ b/spec/features/groups/milestone_spec.rb
@@ -1,9 +1,9 @@
require 'rails_helper'
-feature 'Group milestones' do
+describe 'Group milestones' do
let(:group) { create(:group) }
let!(:project) { create(:project_empty_repo, group: group) }
- let(:user) { create(:group_member, :master, user: create(:user), group: group ).user }
+ let(:user) { create(:group_member, :maintainer, user: create(:user), group: group ).user }
around do |example|
Timecop.freeze { example.run }
@@ -107,19 +107,6 @@ feature 'Group milestones' do
expect(page).to have_selector("#milestone_#{legacy_milestone.milestones.first.id}", count: 1)
end
- it 'updates milestone' do
- page.within(".milestones #milestone_#{active_group_milestone.id}") do
- click_link('Edit')
- end
-
- page.within('.milestone-form') do
- fill_in 'milestone_title', with: 'new title'
- click_button('Update milestone')
- end
-
- expect(find('#content-body h2')).to have_content('new title')
- end
-
it 'shows milestone detail and supports its edit' do
page.within(".milestones #milestone_#{active_group_milestone.id}") do
click_link(active_group_milestone.title)
diff --git a/spec/features/groups/milestones_sorting_spec.rb b/spec/features/groups/milestones_sorting_spec.rb
index a0fe40cf1d3..bc226ff41c1 100644
--- a/spec/features/groups/milestones_sorting_spec.rb
+++ b/spec/features/groups/milestones_sorting_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Milestones sorting', :js do
+describe '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) }
@@ -9,13 +9,13 @@ feature 'Milestones sorting', :js do
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 }
+ let(:user) { create(:group_member, :maintainer, user: create(:user), group: group ).user }
before do
sign_in(user)
end
- scenario 'visit group milestones and sort by due_date_asc' do
+ it 'visit group milestones and sort by due_date_asc' do
visit group_milestones_path(group)
expect(page).to have_button('Due soon')
diff --git a/spec/features/groups/settings/group_badges_spec.rb b/spec/features/groups/settings/group_badges_spec.rb
index 92217294446..070a4a31ffa 100644
--- a/spec/features/groups/settings/group_badges_spec.rb
+++ b/spec/features/groups/settings/group_badges_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Group Badges' do
+describe 'Group Badges' do
include WaitForRequests
let(:user) { create(:user) }
@@ -21,7 +21,7 @@ feature 'Group Badges' do
page.within '.badge-settings' do
wait_for_requests
- rows = all('.panel-body > div')
+ rows = all('.card-body > div')
expect(rows.length).to eq 2
expect(rows[0]).to have_content badge_1.link_url
expect(rows[1]).to have_content badge_2.link_url
@@ -48,7 +48,7 @@ feature 'Group Badges' do
click_button 'Add badge'
wait_for_requests
- within '.panel-body' do
+ within '.card-body' do
expect(find('a')[:href]).to eq badge_link_url
expect(find('a img')[:src]).to eq badge_image_url
end
@@ -60,7 +60,7 @@ feature 'Group Badges' do
it 'form is shown when clicking edit button in list' do
page.within '.badge-settings' do
wait_for_requests
- rows = all('.panel-body > div')
+ rows = all('.card-body > div')
expect(rows.length).to eq 2
rows[1].find('[aria-label="Edit"]').click
@@ -74,7 +74,7 @@ feature 'Group Badges' do
it 'updates a badge when submitting the edit form' do
page.within '.badge-settings' do
wait_for_requests
- rows = all('.panel-body > div')
+ rows = all('.card-body > div')
expect(rows.length).to eq 2
rows[1].find('[aria-label="Edit"]').click
within 'form' do
@@ -85,7 +85,7 @@ feature 'Group Badges' do
wait_for_requests
end
- rows = all('.panel-body > div')
+ rows = all('.card-body > div')
expect(rows.length).to eq 2
expect(rows[1]).to have_content badge_link_url
end
@@ -99,7 +99,7 @@ feature 'Group Badges' do
it 'shows a modal when deleting a badge' do
wait_for_requests
- rows = all('.panel-body > div')
+ rows = all('.card-body > div')
expect(rows.length).to eq 2
click_delete_button(rows[1])
@@ -109,14 +109,14 @@ feature 'Group Badges' do
it 'deletes a badge when confirming the modal' do
wait_for_requests
- rows = all('.panel-body > div')
+ rows = all('.card-body > div')
expect(rows.length).to eq 2
click_delete_button(rows[1])
find('.modal .btn-danger').click
wait_for_requests
- rows = all('.panel-body > div')
+ rows = all('.card-body > div')
expect(rows.length).to eq 1
expect(rows[0]).to have_content badge_1.link_url
end
diff --git a/spec/features/groups/share_lock_spec.rb b/spec/features/groups/share_lock_spec.rb
index 8842d1391aa..5bbe77019ca 100644
--- a/spec/features/groups/share_lock_spec.rb
+++ b/spec/features/groups/share_lock_spec.rb
@@ -1,62 +1,73 @@
require 'spec_helper'
-feature 'Group share with group lock' do
- given(:root_owner) { create(:user) }
- given(:root_group) { create(:group) }
+describe 'Group share with group lock' do
+ let(:root_owner) { create(:user) }
+ let(:root_group) { create(:group) }
- background do
+ before do
root_group.add_owner(root_owner)
sign_in(root_owner)
end
context 'with a subgroup', :nested_groups do
- given!(:subgroup) { create(:group, parent: root_group) }
+ let!(:subgroup) { create(:group, parent: root_group) }
context 'when enabling the parent group share with group lock' do
- scenario 'the subgroup share with group lock becomes enabled' do
+ it 'the subgroup share with group lock becomes enabled' do
visit edit_group_path(root_group)
- check 'group_share_with_group_lock'
- click_on 'Save group'
+ enable_group_lock
expect(subgroup.reload.share_with_group_lock?).to be_truthy
end
end
context 'when disabling the parent group share with group lock (which was already enabled)' do
- background do
+ before do
visit edit_group_path(root_group)
- check 'group_share_with_group_lock'
- click_on 'Save group'
+
+ enable_group_lock
end
context 'and the subgroup share with group lock is enabled' do
- scenario 'the subgroup share with group lock does not change' do
+ it 'the subgroup share with group lock does not change' do
visit edit_group_path(root_group)
- uncheck 'group_share_with_group_lock'
- click_on 'Save group'
+ disable_group_lock
expect(subgroup.reload.share_with_group_lock?).to be_truthy
end
end
context 'but the subgroup share with group lock is disabled' do
- background do
+ before do
visit edit_group_path(subgroup)
- uncheck 'group_share_with_group_lock'
- click_on 'Save group'
+
+ disable_group_lock
end
- scenario 'the subgroup share with group lock does not change' do
+ it 'the subgroup share with group lock does not change' do
visit edit_group_path(root_group)
- uncheck 'group_share_with_group_lock'
- click_on 'Save group'
+ disable_group_lock
expect(subgroup.reload.share_with_group_lock?).to be_falsey
end
end
end
end
+
+ def enable_group_lock
+ page.within('.gs-permissions') do
+ check 'group_share_with_group_lock'
+ click_on 'Save group'
+ end
+ end
+
+ def disable_group_lock
+ page.within('.gs-permissions') do
+ uncheck 'group_share_with_group_lock'
+ click_on 'Save group'
+ end
+ end
end
diff --git a/spec/features/groups/show_spec.rb b/spec/features/groups/show_spec.rb
index 3a0424d60f8..ac961e98a61 100644
--- a/spec/features/groups/show_spec.rb
+++ b/spec/features/groups/show_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Group show page' do
+describe 'Group show page' do
let(:group) { create(:group) }
let(:path) { group_path(group) }
@@ -14,7 +14,7 @@ feature 'Group show page' do
visit path
end
- it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token"
+ it_behaves_like "an autodiscoverable RSS feed with current_user's feed token"
context 'when group does not exist' do
let(:path) { group_path('not-exist') }
@@ -29,7 +29,7 @@ feature 'Group show page' do
visit path
end
- it_behaves_like "an autodiscoverable RSS feed without an RSS token"
+ it_behaves_like "an autodiscoverable RSS feed without a feed token"
end
context 'when group has a public project', :js do
diff --git a/spec/features/groups/user_browse_projects_group_page_spec.rb b/spec/features/groups/user_browse_projects_group_page_spec.rb
index e81c3180e78..916363c41dd 100644
--- a/spec/features/groups/user_browse_projects_group_page_spec.rb
+++ b/spec/features/groups/user_browse_projects_group_page_spec.rb
@@ -21,7 +21,7 @@ describe 'User browse group projects page' do
visit projects_group_path(group)
expect(page).to have_link project.name
- expect(page).to have_xpath("//span[@class='label label-warning']", text: 'archived')
+ expect(page).to have_xpath("//span[@class='badge badge-warning']", text: 'archived')
end
end
end
diff --git a/spec/features/groups/user_sees_users_dropdowns_in_issuables_list_spec.rb b/spec/features/groups/user_sees_users_dropdowns_in_issuables_list_spec.rb
index 5ed4f3ad2bc..6d6f206d761 100644
--- a/spec/features/groups/user_sees_users_dropdowns_in_issuables_list_spec.rb
+++ b/spec/features/groups/user_sees_users_dropdowns_in_issuables_list_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Groups > User sees users dropdowns in issuables list' do
+describe 'Groups > User sees users dropdowns in issuables list' do
let(:entity) { create(:group) }
let(:user_in_dropdown) { create(:user) }
let!(:user_not_in_dropdown) { create(:user) }
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index c1f3d94bc20..e62bd6f8187 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Group' do
+describe 'Group' do
before do
sign_in(create(:admin))
end
@@ -141,8 +141,10 @@ feature 'Group' do
end
it 'saves new settings' do
- fill_in 'group_name', with: new_name
- click_button 'Save group'
+ page.within('.gs-general') do
+ fill_in 'group_name', with: new_name
+ click_button 'Save group'
+ end
expect(page).to have_content 'successfully updated'
expect(find('#group_name').value).to eq(new_name)
@@ -152,6 +154,12 @@ feature 'Group' do
end
end
+ it 'focuses confirmation field on remove group' do
+ click_button('Remove group')
+
+ expect(page).to have_selector '#confirm_name_input:focus'
+ end
+
it 'removes group' do
expect { remove_with_confirm('Remove group', group.path) }.to change {Group.count}.by(-1)
expect(group.members.all.count).to be_zero
diff --git a/spec/features/ics/dashboard_issues_spec.rb b/spec/features/ics/dashboard_issues_spec.rb
new file mode 100644
index 00000000000..ea714934ae7
--- /dev/null
+++ b/spec/features/ics/dashboard_issues_spec.rb
@@ -0,0 +1,102 @@
+require 'spec_helper'
+
+describe 'Dashboard Issues Calendar Feed' do
+ describe 'GET /issues' do
+ let!(:user) { create(:user, email: 'private1@example.com', public_email: 'public1@example.com') }
+ let!(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') }
+ let!(:project) { create(:project) }
+ let(:milestone) { create(:milestone, project_id: project.id, title: 'v1.0') }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ context 'when authenticated' do
+ context 'with no referer' do
+ it 'renders calendar feed' do
+ sign_in user
+ visit issues_dashboard_path(:ics,
+ due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name,
+ sort: 'closest_future_date')
+
+ expect(response_headers['Content-Type']).to have_content('text/calendar')
+ expect(body).to have_text('BEGIN:VCALENDAR')
+ end
+ end
+
+ context 'with GitLab as the referer' do
+ it 'renders calendar feed as text/plain' do
+ sign_in user
+ page.driver.header('Referer', issues_dashboard_url(host: Settings.gitlab.base_url))
+ visit issues_dashboard_path(:ics,
+ due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name,
+ sort: 'closest_future_date')
+
+ expect(response_headers['Content-Type']).to have_content('text/plain')
+ expect(body).to have_text('BEGIN:VCALENDAR')
+ end
+ end
+
+ context 'when filtered by milestone' do
+ it 'renders calendar feed' do
+ sign_in user
+ visit issues_dashboard_path(:ics,
+ due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name,
+ sort: 'closest_future_date',
+ milestone_title: milestone.title)
+
+ expect(response_headers['Content-Type']).to have_content('text/calendar')
+ expect(body).to have_text('BEGIN:VCALENDAR')
+ end
+ end
+ end
+
+ context 'when authenticated via personal access token' do
+ it 'renders calendar feed' do
+ personal_access_token = create(:personal_access_token, user: user)
+
+ visit issues_dashboard_path(:ics,
+ due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name,
+ sort: 'closest_future_date',
+ private_token: personal_access_token.token)
+
+ expect(response_headers['Content-Type']).to have_content('text/calendar')
+ expect(body).to have_text('BEGIN:VCALENDAR')
+ end
+ end
+
+ context 'when authenticated via feed token' do
+ it 'renders calendar feed' do
+ visit issues_dashboard_path(:ics,
+ due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name,
+ sort: 'closest_future_date',
+ feed_token: user.feed_token)
+
+ expect(response_headers['Content-Type']).to have_content('text/calendar')
+ expect(body).to have_text('BEGIN:VCALENDAR')
+ end
+ end
+
+ context 'issue with due date' do
+ let!(:issue) do
+ create(:issue, author: user, assignees: [assignee], project: project, title: 'test title',
+ description: 'test desc', due_date: Date.tomorrow)
+ end
+
+ it 'renders issue fields' do
+ visit issues_dashboard_path(:ics,
+ due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name,
+ sort: 'closest_future_date',
+ feed_token: user.feed_token)
+
+ expect(body).to have_text("SUMMARY:test title (in #{project.full_path})")
+ # line length for ics is 75 chars
+ expected_description = "DESCRIPTION:Find out more at #{issue_url(issue)}".insert(75, "\r\n")
+ expect(body).to have_text(expected_description)
+ expect(body).to have_text("DTSTART;VALUE=DATE:#{Date.tomorrow.strftime('%Y%m%d')}")
+ expect(body).to have_text("URL:#{issue_url(issue)}")
+ expect(body).to have_text('TRANSP:TRANSPARENT')
+ end
+ end
+ end
+end
diff --git a/spec/features/ics/group_issues_spec.rb b/spec/features/ics/group_issues_spec.rb
new file mode 100644
index 00000000000..24de5b4b7c6
--- /dev/null
+++ b/spec/features/ics/group_issues_spec.rb
@@ -0,0 +1,77 @@
+require 'spec_helper'
+
+describe 'Group Issues Calendar Feed' do
+ describe 'GET /issues' do
+ let!(:user) { create(:user, email: 'private1@example.com', public_email: 'public1@example.com') }
+ let!(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') }
+ let!(:group) { create(:group) }
+ let!(:project) { create(:project, group: group) }
+
+ before do
+ project.add_developer(user)
+ group.add_developer(user)
+ end
+
+ context 'when authenticated' do
+ context 'with no referer' do
+ it 'renders calendar feed' do
+ sign_in user
+ visit issues_group_path(group, :ics)
+
+ expect(response_headers['Content-Type']).to have_content('text/calendar')
+ expect(body).to have_text('BEGIN:VCALENDAR')
+ end
+ end
+
+ context 'with GitLab as the referer' do
+ it 'renders calendar feed as text/plain' do
+ sign_in user
+ page.driver.header('Referer', issues_group_url(group, host: Settings.gitlab.base_url))
+ visit issues_group_path(group, :ics)
+
+ expect(response_headers['Content-Type']).to have_content('text/plain')
+ expect(body).to have_text('BEGIN:VCALENDAR')
+ end
+ end
+ end
+
+ context 'when authenticated via personal access token' do
+ it 'renders calendar feed' do
+ personal_access_token = create(:personal_access_token, user: user)
+
+ visit issues_group_path(group, :ics, private_token: personal_access_token.token)
+
+ expect(response_headers['Content-Type']).to have_content('text/calendar')
+ expect(body).to have_text('BEGIN:VCALENDAR')
+ end
+ end
+
+ context 'when authenticated via feed token' do
+ it 'renders calendar feed' do
+ visit issues_group_path(group, :ics, feed_token: user.feed_token)
+
+ expect(response_headers['Content-Type']).to have_content('text/calendar')
+ expect(body).to have_text('BEGIN:VCALENDAR')
+ end
+ end
+
+ context 'issue with due date' do
+ let!(:issue) do
+ create(:issue, author: user, assignees: [assignee], project: project, title: 'test title',
+ description: 'test desc', due_date: Date.tomorrow)
+ end
+
+ it 'renders issue fields' do
+ visit issues_group_path(group, :ics, feed_token: user.feed_token)
+
+ expect(body).to have_text("SUMMARY:test title (in #{project.full_path})")
+ # line length for ics is 75 chars
+ expected_description = "DESCRIPTION:Find out more at #{issue_url(issue)}".insert(75, "\r\n")
+ expect(body).to have_text(expected_description)
+ expect(body).to have_text("DTSTART;VALUE=DATE:#{Date.tomorrow.strftime('%Y%m%d')}")
+ expect(body).to have_text("URL:#{issue_url(issue)}")
+ expect(body).to have_text('TRANSP:TRANSPARENT')
+ end
+ end
+ end
+end
diff --git a/spec/features/ics/project_issues_spec.rb b/spec/features/ics/project_issues_spec.rb
new file mode 100644
index 00000000000..2ca3d52a5be
--- /dev/null
+++ b/spec/features/ics/project_issues_spec.rb
@@ -0,0 +1,76 @@
+require 'spec_helper'
+
+describe 'Project Issues Calendar Feed' do
+ describe 'GET /issues' do
+ let!(:user) { create(:user, email: 'private1@example.com', public_email: 'public1@example.com') }
+ let!(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') }
+ let!(:project) { create(:project) }
+ let!(:issue) { create(:issue, author: user, assignees: [assignee], project: project) }
+
+ before do
+ project.add_developer(user)
+ end
+
+ context 'when authenticated' do
+ context 'with no referer' do
+ it 'renders calendar feed' do
+ sign_in user
+ visit project_issues_path(project, :ics)
+
+ expect(response_headers['Content-Type']).to have_content('text/calendar')
+ expect(body).to have_text('BEGIN:VCALENDAR')
+ end
+ end
+
+ context 'with GitLab as the referer' do
+ it 'renders calendar feed as text/plain' do
+ sign_in user
+ page.driver.header('Referer', project_issues_url(project, host: Settings.gitlab.base_url))
+ visit project_issues_path(project, :ics)
+
+ expect(response_headers['Content-Type']).to have_content('text/plain')
+ expect(body).to have_text('BEGIN:VCALENDAR')
+ end
+ end
+ end
+
+ context 'when authenticated via personal access token' do
+ it 'renders calendar feed' do
+ personal_access_token = create(:personal_access_token, user: user)
+
+ visit project_issues_path(project, :ics, private_token: personal_access_token.token)
+
+ expect(response_headers['Content-Type']).to have_content('text/calendar')
+ expect(body).to have_text('BEGIN:VCALENDAR')
+ end
+ end
+
+ context 'when authenticated via feed token' do
+ it 'renders calendar feed' do
+ visit project_issues_path(project, :ics, feed_token: user.feed_token)
+
+ expect(response_headers['Content-Type']).to have_content('text/calendar')
+ expect(body).to have_text('BEGIN:VCALENDAR')
+ end
+ end
+
+ context 'issue with due date' do
+ let!(:issue) do
+ create(:issue, author: user, assignees: [assignee], project: project, title: 'test title',
+ description: 'test desc', due_date: Date.tomorrow)
+ end
+
+ it 'renders issue fields' do
+ visit project_issues_path(project, :ics, feed_token: user.feed_token)
+
+ expect(body).to have_text("SUMMARY:test title (in #{project.full_path})")
+ # line length for ics is 75 chars
+ expected_description = "DESCRIPTION:Find out more at #{issue_url(issue)}".insert(75, "\r\n")
+ expect(body).to have_text(expected_description)
+ expect(body).to have_text("DTSTART;VALUE=DATE:#{Date.tomorrow.strftime('%Y%m%d')}")
+ expect(body).to have_text("URL:#{issue_url(issue)}")
+ expect(body).to have_text('TRANSP:TRANSPARENT')
+ end
+ end
+ end
+end
diff --git a/spec/features/ide_spec.rb b/spec/features/ide_spec.rb
index b3f24c2966d..65989c36c1e 100644
--- a/spec/features/ide_spec.rb
+++ b/spec/features/ide_spec.rb
@@ -8,7 +8,7 @@ describe 'IDE', :js do
let(:subgroup_project) { create(:project, :repository, namespace: subgroup) }
before do
- subgroup_project.add_master(user)
+ subgroup_project.add_maintainer(user)
sign_in(user)
visit project_path(subgroup_project)
diff --git a/spec/features/invites_spec.rb b/spec/features/invites_spec.rb
index a986ddc4abc..9e1a12a9c2a 100644
--- a/spec/features/invites_spec.rb
+++ b/spec/features/invites_spec.rb
@@ -8,7 +8,7 @@ describe 'Invites' do
let(:group_invite) { group.group_members.invite.last }
before do
- project.add_master(owner)
+ project.add_maintainer(owner)
group.add_user(owner, Gitlab::Access::OWNER)
group.add_developer('user@example.com', owner)
group_invite.generate_invite_token!
diff --git a/spec/features/issuables/close_reopen_report_toggle_spec.rb b/spec/features/issuables/close_reopen_report_toggle_spec.rb
index 3df77a104e8..de6f5fe1560 100644
--- a/spec/features/issuables/close_reopen_report_toggle_spec.rb
+++ b/spec/features/issuables/close_reopen_report_toggle_spec.rb
@@ -46,7 +46,7 @@ describe 'Issuables Close/Reopen/Report toggle' do
let(:issuable) { create(:issue, project: project) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
login_as user
end
@@ -83,7 +83,7 @@ describe 'Issuables Close/Reopen/Report toggle' do
let(:issuable) { create(:merge_request, source_project: project) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
login_as user
end
diff --git a/spec/features/issuables/markdown_references/jira_spec.rb b/spec/features/issuables/markdown_references/jira_spec.rb
index fa0ab88624e..8eaccfc0949 100644
--- a/spec/features/issuables/markdown_references/jira_spec.rb
+++ b/spec/features/issuables/markdown_references/jira_spec.rb
@@ -163,7 +163,7 @@ describe "Jira", :js do
HEREDOC
page.within("#diff-notes-app") do
- fill_in("note_note", with: markdown)
+ fill_in("note-body", with: markdown)
end
end
diff --git a/spec/features/issuables/shortcuts_issuable_spec.rb b/spec/features/issuables/shortcuts_issuable_spec.rb
index e25fd1a6249..a0ae6720a9f 100644
--- a/spec/features/issuables/shortcuts_issuable_spec.rb
+++ b/spec/features/issuables/shortcuts_issuable_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Blob shortcuts', :js do
+describe 'Blob shortcuts', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let(:issue) { create(:issue, project: project, author: user) }
@@ -12,6 +12,15 @@ feature 'Blob shortcuts', :js do
sign_in(user)
end
+ shared_examples "quotes the selected text" do
+ 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 'pressing "r"' do
describe 'On an Issue' do
before do
@@ -20,12 +29,7 @@ feature 'Blob shortcuts', :js do
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
+ include_examples 'quotes the selected text'
end
describe 'On a Merge Request' do
@@ -35,12 +39,7 @@ feature 'Blob shortcuts', :js do
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
+ include_examples 'quotes the selected text'
end
end
end
diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb
index 1131e1711bf..bf60b18873c 100644
--- a/spec/features/issues/award_emoji_spec.rb
+++ b/spec/features/issues/award_emoji_spec.rb
@@ -11,7 +11,7 @@ describe 'Awards Emoji' do
context 'authorized user' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/features/issues/award_spec.rb b/spec/features/issues/award_spec.rb
index ddb69d414da..e53a4ce49c7 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 do
+describe '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 cf283119f36..06cb2e36334 100644
--- a/spec/features/issues/bulk_assignment_labels_spec.rb
+++ b/spec/features/issues/bulk_assignment_labels_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Issues > Labels bulk assignment' do
+describe 'Issues > Labels bulk assignment' do
let(:user) { create(:user) }
let!(:project) { create(:project) }
let!(:issue1) { create(:issue, project: project, title: "Issue 1") }
@@ -11,7 +11,7 @@ feature 'Issues > Labels bulk assignment' do
context 'as an allowed user', :js do
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in user
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 e0466aaf422..ada57285abf 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,21 +1,27 @@
require 'rails_helper'
-feature 'Resolving all open discussions in a merge request from an issue', :js do
+describe '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) }
let!(:discussion) { create(:diff_note_on_merge_request, noteable: merge_request, project: project).to_discussion }
+ def resolve_all_discussions_link_selector
+ text = "Resolve all discussions in new issue"
+ url = new_project_issue_path(project, merge_request_to_resolve_discussions_of: merge_request.iid)
+ %Q{a[data-original-title="#{text}"][href="#{url}"]}
+ end
+
describe 'as a user with access to the project' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in user
visit project_merge_request_path(project, merge_request)
end
it 'shows a button to resolve all discussions by creating a new issue' do
- within('#resolve-count-app') do
- expect(page).to have_link "Resolve all discussions in new issue", href: new_project_issue_path(project, merge_request_to_resolve_discussions_of: merge_request.iid)
+ within('.line-resolve-all-container') do
+ expect(page).to have_selector resolve_all_discussions_link_selector
end
end
@@ -25,13 +31,13 @@ feature 'Resolving all open discussions in a merge request from an issue', :js d
end
it 'hides the link for creating a new issue' do
- expect(page).not_to have_link "Resolve all discussions in new issue", href: new_project_issue_path(project, merge_request_to_resolve_discussions_of: merge_request.iid)
+ expect(page).not_to have_selector resolve_all_discussions_link_selector
end
end
context 'creating an issue for discussions' do
before do
- click_link "Resolve all discussions in new issue", href: new_project_issue_path(project, merge_request_to_resolve_discussions_of: merge_request.iid)
+ find(resolve_all_discussions_link_selector).click
end
it_behaves_like 'creating an issue for a discussion'
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 34beb282bad..b20730bdb22 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
@@ -1,14 +1,20 @@
require 'rails_helper'
-feature 'Resolve an open discussion in a merge request by creating an issue' do
+describe 'Resolve an open discussion in a merge request by creating an issue', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :repository, only_allow_merge_if_all_discussions_are_resolved: true) }
let(:merge_request) { create(:merge_request, source_project: project) }
let!(:discussion) { create(:diff_note_on_merge_request, noteable: merge_request, project: project).to_discussion }
+ def resolve_discussion_selector
+ title = 'Resolve this discussion in a new issue'
+ url = new_project_issue_path(project, discussion_to_resolve: discussion.id, merge_request_to_resolve_discussions_of: merge_request.iid)
+ "a[data-original-title=\"#{title}\"][href=\"#{url}\"]"
+ end
+
describe 'As a user with access to the project' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in user
visit project_merge_request_path(project, merge_request)
end
@@ -20,17 +26,17 @@ feature 'Resolve an open discussion in a merge request by creating an issue' do
end
it 'does not show a link to create a new issue' do
- expect(page).not_to have_link 'Resolve this discussion in a new issue'
+ expect(page).not_to have_css resolve_discussion_selector
end
end
- context 'resolving the discussion', :js do
+ context 'resolving the discussion' do
before do
click_button 'Resolve discussion'
end
it 'hides the link for creating a new issue' do
- expect(page).not_to have_link 'Resolve this discussion in a new issue'
+ expect(page).not_to have_css resolve_discussion_selector
end
it 'shows the link for creating a new issue when unresolving a discussion' do
@@ -38,19 +44,17 @@ feature 'Resolve an open discussion in a merge request by creating an issue' do
click_button 'Unresolve discussion'
end
- expect(page).to have_link 'Resolve this discussion in a new issue'
+ expect(page).to have_css resolve_discussion_selector
end
end
it 'has a link to create a new issue for a discussion' do
- new_issue_link = new_project_issue_path(project, discussion_to_resolve: discussion.id, merge_request_to_resolve_discussions_of: merge_request.iid)
-
- expect(page).to have_link 'Resolve this discussion in a new issue', href: new_issue_link
+ expect(page).to have_css resolve_discussion_selector
end
context 'creating the issue' do
before do
- click_link 'Resolve this discussion in a new issue', href: new_project_issue_path(project, discussion_to_resolve: discussion.id, merge_request_to_resolve_discussions_of: merge_request.iid)
+ find(resolve_discussion_selector).click
end
it 'has a hidden field for the discussion' do
diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
index cbd0949c192..d011d2545bb 100644
--- a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
@@ -20,9 +20,9 @@ describe 'Dropdown assignee', :js do
end
before do
- project.add_master(user)
- project.add_master(user_john)
- project.add_master(user_jacob)
+ project.add_maintainer(user)
+ project.add_maintainer(user_john)
+ project.add_maintainer(user_jacob)
sign_in(user)
create(:issue, project: project)
@@ -31,7 +31,7 @@ describe 'Dropdown assignee', :js do
describe 'behavior' do
it 'opens when the search bar has assignee:' do
- filtered_search.set('assignee:')
+ input_filtered_search('assignee:', submit: false, extra_space: false)
expect(page).to have_css(js_dropdown_assignee, visible: true)
end
@@ -44,6 +44,7 @@ describe 'Dropdown assignee', :js do
it 'should show loading indicator when opened' do
slow_requests do
+ # We aren't using `input_filtered_search` because we want to see the loading indicator
filtered_search.set('assignee:')
expect(page).to have_css('#js-dropdown-assignee .filter-dropdown-loading', visible: true)
@@ -51,19 +52,19 @@ describe 'Dropdown assignee', :js do
end
it 'should hide loading indicator when loaded' do
- filtered_search.set('assignee:')
+ input_filtered_search('assignee:', submit: false, extra_space: false)
expect(find(js_dropdown_assignee)).not_to have_css('.filter-dropdown-loading')
end
it 'should load all the assignees when opened' do
- filtered_search.set('assignee:')
+ input_filtered_search('assignee:', submit: false, extra_space: false)
expect(dropdown_assignee_size).to eq(4)
end
it 'shows current user at top of dropdown' do
- filtered_search.set('assignee:')
+ input_filtered_search('assignee:', submit: false, extra_space: false)
expect(filter_dropdown.first('.filter-dropdown-item')).to have_content(user.name)
end
@@ -71,7 +72,7 @@ describe 'Dropdown assignee', :js do
describe 'filtering' do
before do
- filtered_search.set('assignee:')
+ input_filtered_search('assignee:', submit: false, extra_space: false)
expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_john.name)
expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name)
@@ -79,23 +80,21 @@ describe 'Dropdown assignee', :js do
end
it 'filters by name' do
- filtered_search.send_keys('j')
+ input_filtered_search('jac', submit: false, extra_space: false)
- expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_john.name)
expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name)
expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_no_content(user.name)
end
it 'filters by case insensitive name' do
- filtered_search.send_keys('J')
+ input_filtered_search('JAC', submit: false, extra_space: false)
- expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_john.name)
expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name)
expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_no_content(user.name)
end
it 'filters by username with symbol' do
- filtered_search.send_keys('@ot')
+ input_filtered_search('@ott', submit: false, extra_space: false)
expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name)
expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user.name)
@@ -103,7 +102,7 @@ describe 'Dropdown assignee', :js do
end
it 'filters by case insensitive username with symbol' do
- filtered_search.send_keys('@OT')
+ input_filtered_search('@OTT', submit: false, extra_space: false)
expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name)
expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user.name)
@@ -111,7 +110,9 @@ describe 'Dropdown assignee', :js do
end
it 'filters by username without symbol' do
- filtered_search.send_keys('ot')
+ input_filtered_search('ott', submit: false, extra_space: false)
+
+ wait_for_requests
expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name)
expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user.name)
@@ -119,7 +120,9 @@ describe 'Dropdown assignee', :js do
end
it 'filters by case insensitive username without symbol' do
- filtered_search.send_keys('OT')
+ input_filtered_search('OTT', submit: false, extra_space: false)
+
+ wait_for_requests
expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name)
expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user.name)
@@ -129,7 +132,7 @@ describe 'Dropdown assignee', :js do
describe 'selecting from dropdown' do
before do
- filtered_search.set('assignee:')
+ input_filtered_search('assignee:', submit: false, extra_space: false)
end
it 'fills in the assignee username when the assignee has not been filtered' do
@@ -143,7 +146,7 @@ describe 'Dropdown assignee', :js do
end
it 'fills in the assignee username when the assignee has been filtered' do
- filtered_search.send_keys('roo')
+ input_filtered_search('roo', submit: false, extra_space: false)
click_assignee(user.name)
wait_for_requests
@@ -165,7 +168,7 @@ describe 'Dropdown assignee', :js do
describe 'selecting from dropdown without Ajax call' do
before do
Gitlab::Testing::RequestBlockerMiddleware.block_requests!
- filtered_search.set('assignee:')
+ input_filtered_search('assignee:', submit: false, extra_space: false)
end
after do
@@ -183,31 +186,31 @@ describe 'Dropdown assignee', :js do
describe 'input has existing content' do
it 'opens assignee dropdown with existing search term' do
- filtered_search.set('searchTerm assignee:')
+ input_filtered_search('searchTerm assignee:', submit: false, extra_space: false)
expect(page).to have_css(js_dropdown_assignee, visible: true)
end
it 'opens assignee dropdown with existing author' do
- filtered_search.set('author:@user assignee:')
+ input_filtered_search('author:@user assignee:', submit: false, extra_space: false)
expect(page).to have_css(js_dropdown_assignee, visible: true)
end
it 'opens assignee dropdown with existing label' do
- filtered_search.set('label:~bug assignee:')
+ input_filtered_search('label:~bug assignee:', submit: false, extra_space: false)
expect(page).to have_css(js_dropdown_assignee, visible: true)
end
it 'opens assignee dropdown with existing milestone' do
- filtered_search.set('milestone:%v1.0 assignee:')
+ input_filtered_search('milestone:%v1.0 assignee:', submit: false, extra_space: false)
expect(page).to have_css(js_dropdown_assignee, visible: true)
end
it 'opens assignee dropdown with existing my-reaction' do
- filtered_search.set('my-reaction:star assignee:')
+ input_filtered_search('my-reaction:star assignee:', submit: false, extra_space: false)
expect(page).to have_css(js_dropdown_assignee, visible: true)
end
@@ -215,17 +218,15 @@ describe 'Dropdown assignee', :js do
describe 'caching requests' do
it 'caches requests after the first load' do
- filtered_search.set('assignee')
- filtered_search.send_keys(':')
+ input_filtered_search('assignee:', submit: false, extra_space: false)
initial_size = dropdown_assignee_size
expect(initial_size).to be > 0
new_user = create(:user)
- project.add_master(new_user)
+ project.add_maintainer(new_user)
find('.filtered-search-box .clear-search').click
- filtered_search.set('assignee')
- filtered_search.send_keys(':')
+ input_filtered_search('assignee:', submit: false, extra_space: false)
expect(dropdown_assignee_size).to eq(initial_size)
end
diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb
index 70b4f11410d..50d819a6161 100644
--- a/spec/features/issues/filtered_search/dropdown_author_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb
@@ -28,9 +28,9 @@ describe 'Dropdown author', :js do
end
before do
- project.add_master(user)
- project.add_master(user_john)
- project.add_master(user_jacob)
+ project.add_maintainer(user)
+ project.add_maintainer(user_john)
+ project.add_maintainer(user_jacob)
sign_in(user)
create(:issue, project: project)
@@ -195,7 +195,7 @@ describe 'Dropdown author', :js do
expect(initial_size).to be > 0
new_user = create(:user)
- project.add_master(new_user)
+ project.add_maintainer(new_user)
find('.filtered-search-box .clear-search').click
filtered_search.set('author')
send_keys_to_filtered_search(':')
diff --git a/spec/features/issues/filtered_search/dropdown_emoji_spec.rb b/spec/features/issues/filtered_search/dropdown_emoji_spec.rb
index 436625a6f7b..be229e8aa7d 100644
--- a/spec/features/issues/filtered_search/dropdown_emoji_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_emoji_spec.rb
@@ -28,7 +28,7 @@ describe 'Dropdown emoji', :js do
end
before do
- project.add_master(user)
+ project.add_maintainer(user)
create_list(:award_emoji, 2, user: user, name: 'thumbsup')
create_list(:award_emoji, 1, user: user, name: 'thumbsdown')
create_list(:award_emoji, 3, user: user, name: 'star')
diff --git a/spec/features/issues/filtered_search/dropdown_hint_spec.rb b/spec/features/issues/filtered_search/dropdown_hint_spec.rb
index ef40dddfd3a..b99c5a7f4e3 100644
--- a/spec/features/issues/filtered_search/dropdown_hint_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_hint_spec.rb
@@ -13,7 +13,7 @@ describe 'Dropdown hint', :js do
end
before do
- project.add_master(user)
+ project.add_maintainer(user)
create(:issue, project: project)
end
diff --git a/spec/features/issues/filtered_search/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb
index 18cdb199c70..ca5d506ab04 100644
--- a/spec/features/issues/filtered_search/dropdown_label_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb
@@ -33,7 +33,7 @@ describe 'Dropdown label', :js do
end
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
create(:issue, project: project)
diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
index 94710c2f71f..f76d30056da 100644
--- a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
@@ -29,7 +29,7 @@ describe 'Dropdown milestone', :js do
end
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
create(:issue, project: project)
diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb
index 08ba91a2682..d4949de3f27 100644
--- a/spec/features/issues/filtered_search/filter_issues_spec.rb
+++ b/spec/features/issues/filtered_search/filter_issues_spec.rb
@@ -10,6 +10,7 @@ describe 'Filter issues', :js do
# When the name is longer, the filtered search input can end up scrolling
# horizontally, and PhantomJS can't handle it.
let(:user) { create(:user, name: 'Ann') }
+ let(:user2) { create(:user, name: 'jane') }
let!(:bug_label) { create(:label, project: project, title: 'bug') }
let!(:caps_sensitive_label) { create(:label, project: project, title: 'CaPs') }
@@ -23,9 +24,7 @@ describe 'Filter issues', :js do
end
before do
- project.add_master(user)
-
- user2 = create(:user)
+ project.add_maintainer(user)
create(:issue, project: project, author: user2, title: "Bug report 1")
create(:issue, project: project, author: user2, title: "Bug report 2")
@@ -113,6 +112,24 @@ describe 'Filter issues', :js do
expect_issues_list_count(3)
expect_filtered_search_input_empty
end
+
+ it 'filters issues by invalid assignee' do
+ skip('to be tested, issue #26546')
+ end
+
+ it 'filters issues by multiple assignees' do
+ create(:issue, project: project, author: user, assignees: [user2, user])
+
+ input_filtered_search("assignee:@#{user.username} assignee:@#{user2.username}")
+
+ expect_tokens([
+ assignee_token(user.name),
+ assignee_token(user2.name)
+ ])
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input_empty
+ end
end
end
@@ -248,7 +265,7 @@ describe 'Filter issues', :js do
context 'issue label clicked' do
it 'filters and displays in search bar' do
- find('.issues-list .issue .issue-main-info .issuable-info a .label', text: multiple_words_label.title).click
+ find('.issues-list .issue .issue-main-info .issuable-info a .badge', text: multiple_words_label.title).click
expect_issues_list_count(1)
expect_tokens([label_token("\"#{multiple_words_label.title}\"")])
@@ -468,13 +485,13 @@ describe 'Filter issues', :js do
it "for #{type}" do
visit path
- link = find_link('Subscribe')
+ link = find_link('Subscribe to RSS feed')
params = CGI.parse(URI.parse(link[:href]).query)
auto_discovery_link = find('link[type="application/atom+xml"]', visible: false)
auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query)
expected = {
- 'rss_token' => [user.rss_token],
+ 'feed_token' => [user.feed_token],
'milestone_title' => [milestone.title],
'assignee_id' => [user.id.to_s]
}
@@ -491,6 +508,21 @@ describe 'Filter issues', :js do
it_behaves_like 'updates atom feed link', :group do
let(:path) { issues_group_path(group, milestone_title: milestone.title, assignee_id: user.id) }
end
+
+ it 'updates atom feed link for group issues' do
+ visit issues_group_path(group, milestone_title: milestone.title, assignee_id: user.id)
+ link = find('.nav-controls a[title="Subscribe to RSS feed"]', visible: false)
+ params = CGI.parse(URI.parse(link[:href]).query)
+ auto_discovery_link = find('link[type="application/atom+xml"]', visible: false)
+ auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query)
+
+ expect(params).to include('feed_token' => [user.feed_token])
+ expect(params).to include('milestone_title' => [milestone.title])
+ expect(params).to include('assignee_id' => [user.id.to_s])
+ expect(auto_discovery_params).to include('feed_token' => [user.feed_token])
+ expect(auto_discovery_params).to include('milestone_title' => [milestone.title])
+ expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s])
+ end
end
context 'URL has a trailing slash' do
diff --git a/spec/features/issues/filtered_search/search_bar_spec.rb b/spec/features/issues/filtered_search/search_bar_spec.rb
index 268590da599..8abab3f35d6 100644
--- a/spec/features/issues/filtered_search/search_bar_spec.rb
+++ b/spec/features/issues/filtered_search/search_bar_spec.rb
@@ -8,7 +8,7 @@ describe 'Search bar', :js do
let(:filtered_search) { find('.filtered-search') }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
create(:issue, project: project)
diff --git a/spec/features/issues/filtered_search/visual_tokens_spec.rb b/spec/features/issues/filtered_search/visual_tokens_spec.rb
index 0ae70c855db..6ac7ccd00f7 100644
--- a/spec/features/issues/filtered_search/visual_tokens_spec.rb
+++ b/spec/features/issues/filtered_search/visual_tokens_spec.rb
@@ -22,8 +22,8 @@ describe 'Visual tokens', :js do
end
before do
- project.add_user(user, :master)
- project.add_user(user_rock, :master)
+ project.add_user(user, :maintainer)
+ project.add_user(user_rock, :maintainer)
sign_in(user)
create(:issue, project: project)
diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb
index 4625a50b8d9..1456a2f0375 100644
--- a/spec/features/issues/form_spec.rb
+++ b/spec/features/issues/form_spec.rb
@@ -13,8 +13,8 @@ describe 'New/edit issue', :js do
let!(:issue) { create(:issue, project: project, assignees: [user], milestone: milestone) }
before do
- project.add_master(user)
- project.add_master(user2)
+ project.add_maintainer(user)
+ project.add_maintainer(user2)
sign_in(user)
end
@@ -143,6 +143,9 @@ describe 'New/edit issue', :js do
click_link label.title
click_link label2.title
end
+
+ find('.js-issuable-form-dropdown.js-label-select').click
+
page.within '.js-label-select' do
expect(page).to have_content label.title
end
@@ -318,7 +321,7 @@ describe 'New/edit issue', :js do
let(:sub_group_project) { create(:project, group: nested_group_1) }
before do
- sub_group_project.add_master(user)
+ sub_group_project.add_maintainer(user)
visit new_project_issue_path(sub_group_project)
end
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index f2624f55c86..98e37d8011a 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -1,13 +1,13 @@
require 'rails_helper'
-feature 'GFM autocomplete', :js do
+describe '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+') }
let(:issue) { create(:issue, project: project) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit project_issue_path(project, issue)
diff --git a/spec/features/issues/issue_detail_spec.rb b/spec/features/issues/issue_detail_spec.rb
index babb0285590..088ab114df3 100644
--- a/spec/features/issues/issue_detail_spec.rb
+++ b/spec/features/issues/issue_detail_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Issue Detail', :js do
+describe 'Issue Detail', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:issue) { create(:issue, project: project, author: user) }
diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb
index 830c794376d..3050f23c130 100644
--- a/spec/features/issues/issue_sidebar_spec.rb
+++ b/spec/features/issues/issue_sidebar_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Issue Sidebar' do
+describe 'Issue Sidebar' do
include MobileHelpers
let(:group) { create(:group, :nested) }
@@ -112,7 +112,7 @@ feature 'Issue Sidebar' do
context 'editing issue labels', :js do
before do
- issue.update_attributes(labels: [label])
+ issue.update(labels: [label])
page.within('.block.labels') do
find('.edit-link').click
end
diff --git a/spec/features/issues/markdown_toolbar_spec.rb b/spec/features/issues/markdown_toolbar_spec.rb
index fee8fd9b365..042ecdb172a 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 do
+describe 'Issue markdown toolbar', :js do
let(:project) { create(:project, :public) }
let(:issue) { create(:issue, project: project) }
let(:user) { create(:user) }
diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb
index 3c01ff345fc..2abc50b04e4 100644
--- a/spec/features/issues/move_spec.rb
+++ b/spec/features/issues/move_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'issue move to another project' do
+describe 'issue move to another project' do
let(:user) { create(:user) }
let(:old_project) { create(:project, :repository) }
let(:text) { 'Some issue description' }
@@ -9,16 +9,18 @@ feature 'issue move to another project' do
create(:issue, description: text, project: old_project, author: user)
end
- background { sign_in(user) }
+ before do
+ sign_in(user)
+ end
context 'user does not have permission to move issue' do
- background do
+ before do
old_project.add_guest(user)
visit issue_path(issue)
end
- scenario 'moving issue to another project not allowed' do
+ it 'moving issue to another project not allowed' do
expect(page).to have_no_selector('.js-sidebar-move-issue-block')
end
end
@@ -30,14 +32,14 @@ feature 'issue move to another project' do
let(:text) { "Text with #{mr.to_reference}" }
let(:cross_reference) { old_project.to_reference(new_project) }
- background do
+ before do
old_project.add_reporter(user)
new_project.add_reporter(user)
visit issue_path(issue)
end
- scenario 'moving issue to another project', :js do
+ it 'moving issue to another project', :js do
find('.js-move-issue').click
wait_for_requests
all('.js-move-issue-dropdown-item')[0].click
@@ -49,7 +51,7 @@ feature 'issue move to another project' do
expect(page.current_path).to include project_path(new_project)
end
- scenario 'searching project dropdown', :js do
+ it 'searching project dropdown', :js do
new_project_search.add_reporter(user)
find('.js-move-issue').click
@@ -66,9 +68,11 @@ feature 'issue move to another project' 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.add_guest(user) }
+ before do
+ another_project.add_guest(user)
+ end
- scenario 'browsing projects in projects select' do
+ it 'browsing projects in projects select' do
find('.js-move-issue').click
wait_for_requests
@@ -84,7 +88,7 @@ feature 'issue move to another project' do
create(:issue, project: old_project, author: user, moved_to: new_issue)
end
- scenario 'user wants to move issue that has already been moved' do
+ it 'user wants to move issue that has already been moved' do
expect(page).to have_no_selector('#move_to_project_id')
end
end
diff --git a/spec/features/issues/note_polling_spec.rb b/spec/features/issues/note_polling_spec.rb
index 793572851da..3cd7ce6dada 100644
--- a/spec/features/issues/note_polling_spec.rb
+++ b/spec/features/issues/note_polling_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Issue notes polling', :js do
+describe 'Issue notes polling', :js do
include NoteInteractionHelpers
let(:project) { create(:project, :public) }
diff --git a/spec/features/issues/spam_issues_spec.rb b/spec/features/issues/spam_issues_spec.rb
index 73022afbda2..7cce45ff206 100644
--- a/spec/features/issues/spam_issues_spec.rb
+++ b/spec/features/issues/spam_issues_spec.rb
@@ -17,7 +17,7 @@ describe 'New issue', :js do
recaptcha_private_key: 'test private key'
)
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/features/issues/todo_spec.rb b/spec/features/issues/todo_spec.rb
index 4a44ec302fc..0114178b9be 100644
--- a/spec/features/issues/todo_spec.rb
+++ b/spec/features/issues/todo_spec.rb
@@ -1,12 +1,12 @@
require 'rails_helper'
-feature 'Manually create a todo item from issue', :js do
+describe 'Manually create a todo item from issue', :js do
let!(:project) { create(:project) }
let!(:issue) { create(:issue, project: project) }
let!(:user) { create(:user)}
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit project_issue_path(project, issue)
end
diff --git a/spec/features/issues/update_issues_spec.rb b/spec/features/issues/update_issues_spec.rb
index 7d6edc171f8..845a7c5fc42 100644
--- a/spec/features/issues/update_issues_spec.rb
+++ b/spec/features/issues/update_issues_spec.rb
@@ -1,12 +1,12 @@
require 'rails_helper'
-feature 'Multiple issue updating from issues#index', :js do
+describe 'Multiple issue updating from issues#index', :js do
let!(:project) { create(:project) }
let!(:issue) { create(:issue, project: project) }
let!(:user) { create(:user)}
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
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
index 539d7e9ff01..3dfcbc2fcb8 100644
--- a/spec/features/issues/user_creates_branch_and_merge_request_spec.rb
+++ b/spec/features/issues/user_creates_branch_and_merge_request_spec.rb
@@ -139,8 +139,8 @@ describe 'User creates branch and merge request on issue page', :js do
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_css('.create-mr-dropdown-wrap .unavailable:not(.hidden)')
+ expect(page).to have_css('.create-mr-dropdown-wrap .available.hidden', visible: false)
expect(page).to have_content /1 Related Merge Request/
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 fd0aa6cf3a3..5926e442f24 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 do
+describe 'Issues > User uses quick actions', :js do
include Spec::Support::Helpers::Features::NotesHelpers
it_behaves_like 'issuable record that supports quick actions in its description and notes', :issue do
@@ -12,7 +12,7 @@ feature 'Issues > User uses quick actions', :js do
let(:project) { create(:project, :public) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit project_issue_path(project, issue)
end
@@ -153,6 +153,42 @@ feature 'Issues > User uses quick actions', :js do
end
end
+ describe 'make issue confidential' do
+ let(:issue) { create(:issue, project: project) }
+ let(:original_issue) { create(:issue, project: project) }
+
+ context 'when the current user can update issues' do
+ it 'does not create a note, and marks the issue as confidential' do
+ add_note("/confidential")
+
+ expect(page).not_to have_content "/confidential"
+ expect(page).to have_content 'Commands applied'
+ expect(page).to have_content "made the issue confidential"
+
+ expect(issue.reload).to be_confidential
+ end
+ end
+
+ context 'when the current user cannot update the issue' do
+ let(:guest) { create(:user) }
+ before do
+ project.add_guest(guest)
+ gitlab_sign_out
+ sign_in(guest)
+ visit project_issue_path(project, issue)
+ end
+
+ it 'does not create a note, and does not mark the issue as confidential' do
+ add_note("/confidential")
+
+ expect(page).not_to have_content 'Commands applied'
+ expect(page).not_to have_content "made the issue confidential"
+
+ expect(issue.reload).not_to be_confidential
+ end
+ end
+ end
+
describe 'move the issue to another project' do
let(:issue) { create(:issue, project: project) }
@@ -160,7 +196,7 @@ feature 'Issues > User uses quick actions', :js do
let(:target_project) { create(:project, :public) }
before do
- target_project.add_master(user)
+ target_project.add_maintainer(user)
gitlab_sign_out
sign_in(user)
visit project_issue_path(project, issue)
@@ -190,7 +226,9 @@ feature 'Issues > User uses quick actions', :js do
it 'does not move the issue' do
add_note("/move #{project_unauthorized.full_path}")
- expect(page).not_to have_content 'Commands applied'
+ wait_for_requests
+
+ expect(page).to have_content 'Commands applied'
expect(issue.reload).to be_open
end
end
@@ -220,7 +258,7 @@ feature 'Issues > User uses quick actions', :js do
let(:wontfix_target) { create(:label, project: target_project, title: 'wontfix') }
before do
- target_project.add_master(user)
+ target_project.add_maintainer(user)
gitlab_sign_out
sign_in(user)
visit project_issue_path(project, issue)
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index 314bd19f586..4d9b8a10e04 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -340,6 +340,20 @@ describe 'Issues' do
expect(page).to have_content('baz')
end
end
+
+ it 'filters by due next month and previous two weeks' do
+ foo.update(due_date: Date.today - 4.weeks)
+ bar.update(due_date: (Date.today + 2.months).beginning_of_month)
+ baz.update(due_date: Date.yesterday)
+
+ visit project_issues_path(project, due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name)
+
+ page.within '.issues-holder' do
+ expect(page).not_to have_content('foo')
+ expect(page).not_to have_content('bar')
+ expect(page).to have_content('baz')
+ end
+ end
end
describe 'sorting by milestone' do
@@ -384,7 +398,7 @@ describe 'Issues' do
before do
stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab")
- project1.add_master(user)
+ project1.add_maintainer(user)
visit namespace_project_issues_path(user.namespace, project1)
end
@@ -591,6 +605,20 @@ describe 'Issues' do
end
end
+ it 'clears local storage after creating a new issue', :js do
+ 2.times do
+ visit new_project_issue_path(project)
+ wait_for_requests
+
+ expect(page).to have_field('Title', with: '')
+
+ fill_in 'issue_title', with: 'bug 345'
+ fill_in 'issue_description', with: 'bug description'
+
+ click_button 'Submit issue'
+ end
+ end
+
context 'dropzone upload file', :js do
before do
visit new_project_issue_path(project)
diff --git a/spec/features/labels_hierarchy_spec.rb b/spec/features/labels_hierarchy_spec.rb
index ae41f611ddc..6f917f522bc 100644
--- a/spec/features/labels_hierarchy_spec.rb
+++ b/spec/features/labels_hierarchy_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Labels Hierarchy', :js, :nested_groups do
+describe 'Labels Hierarchy', :js, :nested_groups do
include FilteredSearchHelpers
let!(:user) { create(:user) }
@@ -34,7 +34,7 @@ feature 'Labels Hierarchy', :js, :nested_groups do
wait_for_requests
- expect(page).to have_selector('span.label', text: label.title)
+ expect(page).to have_selector('.badge', text: label.title)
end
end
@@ -45,7 +45,7 @@ feature 'Labels Hierarchy', :js, :nested_groups do
wait_for_requests
- expect(page).not_to have_selector('span.label', text: child_group_label.title)
+ expect(page).not_to have_selector('.badge', text: child_group_label.title)
end
end
@@ -57,7 +57,7 @@ feature 'Labels Hierarchy', :js, :nested_groups do
wait_for_requests
if board
- expect(page).to have_selector('.card-title') do |card|
+ expect(page).to have_selector('.board-card-title') do |card|
expect(card).to have_selector('a', text: labeled_issue.title)
end
else
@@ -96,11 +96,11 @@ feature 'Labels Hierarchy', :js, :nested_groups do
wait_for_requests
if board
- expect(page).to have_selector('.card-title') do |card|
+ expect(page).to have_selector('.board-card-title') do |card|
expect(card).to have_selector('a', text: labeled_issue.title)
end
- expect(page).to have_selector('.card-title') do |card|
+ expect(page).to have_selector('.board-card-title') do |card|
expect(card).to have_selector('a', text: labeled_issue_2.title)
end
else
@@ -118,11 +118,11 @@ feature 'Labels Hierarchy', :js, :nested_groups do
select_label_on_dropdown(group_label_3.title)
if board
- expect(page).to have_selector('.card-title') do |card|
+ expect(page).to have_selector('.board-card-title') do |card|
expect(card).not_to have_selector('a', text: labeled_issue_2.title)
end
- expect(page).to have_selector('.card-title') do |card|
+ expect(page).to have_selector('.board-card-title') do |card|
expect(card).to have_selector('a', text: labeled_issue_3.title)
end
else
@@ -159,9 +159,9 @@ feature 'Labels Hierarchy', :js, :nested_groups do
find('.btn-create').click
expect(page.find('.issue-details h2.title')).to have_content('new created issue')
- expect(page).to have_selector('span.label', text: grandparent_group_label.title)
- expect(page).to have_selector('span.label', text: parent_group_label.title)
- expect(page).to have_selector('span.label', text: project_label_1.title)
+ expect(page).to have_selector('span.badge', text: grandparent_group_label.title)
+ expect(page).to have_selector('span.badge', text: parent_group_label.title)
+ expect(page).to have_selector('span.badge', text: project_label_1.title)
end
end
@@ -188,7 +188,7 @@ feature 'Labels Hierarchy', :js, :nested_groups do
wait_for_requests
- find('.card').click
+ find('.board-card').click
end
it_behaves_like 'assigning labels from sidebar'
@@ -204,7 +204,7 @@ feature 'Labels Hierarchy', :js, :nested_groups do
wait_for_requests
- find('.card').click
+ find('.board-card').click
end
it_behaves_like 'assigning labels from sidebar'
diff --git a/spec/features/markdown/copy_as_gfm_spec.rb b/spec/features/markdown/copy_as_gfm_spec.rb
index 4d897f09b57..05228e27963 100644
--- a/spec/features/markdown/copy_as_gfm_spec.rb
+++ b/spec/features/markdown/copy_as_gfm_spec.rb
@@ -502,6 +502,13 @@ describe 'Copy as GFM', :js do
1. Numbered lists
GFM
+ # list item followed by an HR
+ <<-GFM.strip_heredoc,
+ - list item
+
+ -----
+ GFM
+
'# Heading',
'## Heading',
'### Heading',
@@ -515,8 +522,6 @@ describe 'Copy as GFM', :js do
'~~Strikethrough~~',
- '2^2',
-
'-----',
# table
diff --git a/spec/features/markdown/gitlab_flavored_markdown_spec.rb b/spec/features/markdown/gitlab_flavored_markdown_spec.rb
index 3c2186b3598..6997ca48427 100644
--- a/spec/features/markdown/gitlab_flavored_markdown_spec.rb
+++ b/spec/features/markdown/gitlab_flavored_markdown_spec.rb
@@ -6,7 +6,7 @@ describe "GitLab Flavored Markdown" do
let(:issue) { create(:issue, project: project) }
let(:fred) do
create(:user, name: 'fred') do |user|
- project.add_master(user)
+ project.add_maintainer(user)
end
end
diff --git a/spec/features/markdown/markdown_spec.rb b/spec/features/markdown/markdown_spec.rb
index f13d78d24e3..cac8a5068ec 100644
--- a/spec/features/markdown/markdown_spec.rb
+++ b/spec/features/markdown/markdown_spec.rb
@@ -24,7 +24,7 @@ require 'erb'
#
# See the MarkdownFeature class for setup details.
-describe 'GitLab Markdown' do
+describe 'GitLab Markdown', :aggregate_failures do
include Capybara::Node::Matchers
include MarkupHelper
include MarkdownMatchers
@@ -44,112 +44,106 @@ describe 'GitLab Markdown' do
# Shared behavior that all pipelines should exhibit
shared_examples 'all pipelines' do
- describe 'Redcarpet extensions' do
- it 'does not parse emphasis inside of words' do
+ it 'includes extensions' do
+ aggregate_failures 'does not parse emphasis inside of words' do
expect(doc.to_html).not_to match('foo<em>bar</em>baz')
end
- it 'parses table Markdown' do
- aggregate_failures do
- expect(doc).to have_selector('th:contains("Header")')
- expect(doc).to have_selector('th:contains("Row")')
- expect(doc).to have_selector('th:contains("Example")')
- end
+ aggregate_failures 'parses table Markdown' do
+ expect(doc).to have_selector('th:contains("Header")')
+ expect(doc).to have_selector('th:contains("Row")')
+ expect(doc).to have_selector('th:contains("Example")')
end
- it 'allows Markdown in tables' do
+ aggregate_failures 'allows Markdown in tables' do
expect(doc.at_css('td:contains("Baz")').children.to_html)
.to eq '<strong>Baz</strong>'
end
- it 'parses fenced code blocks' do
- aggregate_failures do
- expect(doc).to have_selector('pre.code.highlight.js-syntax-highlight.c')
- expect(doc).to have_selector('pre.code.highlight.js-syntax-highlight.python')
- end
+ aggregate_failures 'parses fenced code blocks' do
+ expect(doc).to have_selector('pre.code.highlight.js-syntax-highlight.c')
+ expect(doc).to have_selector('pre.code.highlight.js-syntax-highlight.python')
end
- it 'parses mermaid code block' do
- aggregate_failures do
- expect(doc).to have_selector('pre[lang=mermaid] > code.js-render-mermaid')
- end
+ aggregate_failures 'parses mermaid code block' do
+ expect(doc).to have_selector('pre[lang=mermaid] > code.js-render-mermaid')
end
- it 'parses strikethroughs' do
+ aggregate_failures 'parses strikethroughs' do
expect(doc).to have_selector(%{del:contains("and this text doesn't")})
end
-
- it 'parses superscript' do
- expect(doc).to have_selector('sup', count: 2)
- end
end
- describe 'SanitizationFilter' do
- it 'permits b elements' do
+ it 'includes SanitizationFilter' do
+ aggregate_failures 'permits b elements' do
expect(doc).to have_selector('b:contains("b tag")')
end
- it 'permits em elements' do
+ aggregate_failures 'permits em elements' do
expect(doc).to have_selector('em:contains("em tag")')
end
- it 'permits code elements' do
+ aggregate_failures 'permits code elements' do
expect(doc).to have_selector('code:contains("code tag")')
end
- it 'permits kbd elements' do
+ aggregate_failures 'permits kbd elements' do
expect(doc).to have_selector('kbd:contains("s")')
end
- it 'permits strike elements' do
+ aggregate_failures 'permits strike elements' do
expect(doc).to have_selector('strike:contains(Emoji)')
end
- it 'permits img elements' do
+ aggregate_failures 'permits img elements' do
expect(doc).to have_selector('img[data-src*="smile.png"]')
end
- it 'permits br elements' do
+ aggregate_failures 'permits br elements' do
expect(doc).to have_selector('br')
end
- it 'permits hr elements' do
+ aggregate_failures 'permits hr elements' do
expect(doc).to have_selector('hr')
end
- it 'permits span elements' do
+ aggregate_failures 'permits span elements' do
expect(doc).to have_selector('span:contains("span tag")')
end
- it 'permits details elements' do
+ aggregate_failures 'permits details elements' do
expect(doc).to have_selector('details:contains("Hiding the details")')
end
- it 'permits summary elements' do
+ aggregate_failures 'permits summary elements' do
expect(doc).to have_selector('details summary:contains("collapsible")')
end
- it 'permits style attribute in th elements' do
- aggregate_failures do
- expect(doc.at_css('th:contains("Header")')['style']).to eq 'text-align: center'
- expect(doc.at_css('th:contains("Row")')['style']).to eq 'text-align: right'
- expect(doc.at_css('th:contains("Example")')['style']).to eq 'text-align: left'
- end
+ aggregate_failures 'permits align attribute in th elements' do
+ expect(doc.at_css('th:contains("Header")')['align']).to eq 'center'
+ expect(doc.at_css('th:contains("Row")')['align']).to eq 'right'
+ expect(doc.at_css('th:contains("Example")')['align']).to eq 'left'
end
- it 'permits style attribute in td elements' do
- aggregate_failures do
- expect(doc.at_css('td:contains("Foo")')['style']).to eq 'text-align: center'
- expect(doc.at_css('td:contains("Bar")')['style']).to eq 'text-align: right'
- expect(doc.at_css('td:contains("Baz")')['style']).to eq 'text-align: left'
- end
+ aggregate_failures 'permits align attribute in td elements' do
+ expect(doc.at_css('td:contains("Foo")')['align']).to eq 'center'
+ expect(doc.at_css('td:contains("Bar")')['align']).to eq 'right'
+ expect(doc.at_css('td:contains("Baz")')['align']).to eq 'left'
+ end
+
+ aggregate_failures 'permits superscript elements' do
+ expect(doc).to have_selector('sup', count: 2)
+ end
+
+ aggregate_failures 'permits subscript elements' do
+ expect(doc).to have_selector('sub', count: 3)
end
- it 'removes `rel` attribute from links' do
+ aggregate_failures 'removes `rel` attribute from links' do
expect(doc).not_to have_selector('a[rel="bookmark"]')
end
- it "removes `href` from `a` elements if it's fishy" do
+ aggregate_failures "removes `href` from `a` elements if it's fishy" do
expect(doc).not_to have_selector('a[href*="javascript"]')
end
end
@@ -176,26 +170,26 @@ describe 'GitLab Markdown' do
end
end
- describe 'ExternalLinkFilter' do
- it 'adds nofollow to external link' do
+ it 'includes ExternalLinkFilter' do
+ aggregate_failures 'adds nofollow to external link' do
link = doc.at_css('a:contains("Google")')
expect(link.attr('rel')).to include('nofollow')
end
- it 'adds noreferrer to external link' do
+ aggregate_failures 'adds noreferrer to external link' do
link = doc.at_css('a:contains("Google")')
expect(link.attr('rel')).to include('noreferrer')
end
- it 'adds _blank to target attribute for external links' do
+ aggregate_failures 'adds _blank to target attribute for external links' do
link = doc.at_css('a:contains("Google")')
expect(link.attr('target')).to match('_blank')
end
- it 'ignores internal link' do
+ aggregate_failures 'ignores internal link' do
link = doc.at_css('a:contains("GitLab Root")')
expect(link.attr('rel')).not_to match 'nofollow'
@@ -219,24 +213,24 @@ describe 'GitLab Markdown' do
it_behaves_like 'all pipelines'
- it 'includes RelativeLinkFilter' do
- expect(doc).to parse_relative_links
- end
+ it 'includes custom filters' do
+ aggregate_failures 'RelativeLinkFilter' do
+ expect(doc).to parse_relative_links
+ end
- it 'includes EmojiFilter' do
- expect(doc).to parse_emoji
- end
+ aggregate_failures 'EmojiFilter' do
+ expect(doc).to parse_emoji
+ end
- it 'includes TableOfContentsFilter' do
- expect(doc).to create_header_links
- end
+ aggregate_failures 'TableOfContentsFilter' do
+ expect(doc).to create_header_links
+ end
- it 'includes AutolinkFilter' do
- expect(doc).to create_autolinks
- end
+ aggregate_failures 'AutolinkFilter' do
+ expect(doc).to create_autolinks
+ end
- it 'includes all reference filters' do
- aggregate_failures do
+ aggregate_failures 'all reference filters' do
expect(doc).to reference_users
expect(doc).to reference_issues
expect(doc).to reference_merge_requests
@@ -246,22 +240,22 @@ describe 'GitLab Markdown' do
expect(doc).to reference_labels
expect(doc).to reference_milestones
end
- end
- it 'includes TaskListFilter' do
- expect(doc).to parse_task_lists
- end
+ aggregate_failures 'TaskListFilter' do
+ expect(doc).to parse_task_lists
+ end
- it 'includes InlineDiffFilter' do
- expect(doc).to parse_inline_diffs
- end
+ aggregate_failures 'InlineDiffFilter' do
+ expect(doc).to parse_inline_diffs
+ end
- it 'includes VideoLinkFilter' do
- expect(doc).to parse_video_links
- end
+ aggregate_failures 'VideoLinkFilter' do
+ expect(doc).to parse_video_links
+ end
- it 'includes ColorFilter' do
- expect(doc).to parse_colors
+ aggregate_failures 'ColorFilter' do
+ expect(doc).to parse_colors
+ end
end
end
@@ -280,24 +274,24 @@ describe 'GitLab Markdown' do
it_behaves_like 'all pipelines'
- it 'includes RelativeLinkFilter' do
- expect(doc).not_to parse_relative_links
- end
+ it 'includes custom filters' do
+ aggregate_failures 'RelativeLinkFilter' do
+ expect(doc).not_to parse_relative_links
+ end
- it 'includes EmojiFilter' do
- expect(doc).to parse_emoji
- end
+ aggregate_failures 'EmojiFilter' do
+ expect(doc).to parse_emoji
+ end
- it 'includes TableOfContentsFilter' do
- expect(doc).to create_header_links
- end
+ aggregate_failures 'TableOfContentsFilter' do
+ expect(doc).to create_header_links
+ end
- it 'includes AutolinkFilter' do
- expect(doc).to create_autolinks
- end
+ aggregate_failures 'AutolinkFilter' do
+ expect(doc).to create_autolinks
+ end
- it 'includes all reference filters' do
- aggregate_failures do
+ aggregate_failures 'all reference filters' do
expect(doc).to reference_users
expect(doc).to reference_issues
expect(doc).to reference_merge_requests
@@ -307,26 +301,51 @@ describe 'GitLab Markdown' do
expect(doc).to reference_labels
expect(doc).to reference_milestones
end
- end
- it 'includes TaskListFilter' do
- expect(doc).to parse_task_lists
- end
+ aggregate_failures 'TaskListFilter' do
+ expect(doc).to parse_task_lists
+ end
- it 'includes GollumTagsFilter' do
- expect(doc).to parse_gollum_tags
- end
+ aggregate_failures 'GollumTagsFilter' do
+ expect(doc).to parse_gollum_tags
+ end
+
+ aggregate_failures 'InlineDiffFilter' do
+ expect(doc).to parse_inline_diffs
+ end
- it 'includes InlineDiffFilter' do
- expect(doc).to parse_inline_diffs
+ aggregate_failures 'VideoLinkFilter' do
+ expect(doc).to parse_video_links
+ end
+
+ aggregate_failures 'ColorFilter' do
+ expect(doc).to parse_colors
+ end
end
+ end
- it 'includes VideoLinkFilter' do
- expect(doc).to parse_video_links
+ context 'Redcarpet documents' do
+ before do
+ allow_any_instance_of(Banzai::Filter::MarkdownFilter).to receive(:engine).and_return('Redcarpet')
+ @html = markdown(@feat.raw_markdown)
end
- it 'includes ColorFilter' do
- expect(doc).to parse_colors
+ it 'processes certain elements differently' do
+ aggregate_failures 'parses superscript' do
+ expect(doc).to have_selector('sup', count: 3)
+ end
+
+ aggregate_failures 'permits style attribute in th elements' do
+ expect(doc.at_css('th:contains("Header")')['style']).to eq 'text-align: center'
+ expect(doc.at_css('th:contains("Row")')['style']).to eq 'text-align: right'
+ expect(doc.at_css('th:contains("Example")')['style']).to eq 'text-align: left'
+ end
+
+ aggregate_failures 'permits style attribute in td elements' do
+ expect(doc.at_css('td:contains("Foo")')['style']).to eq 'text-align: center'
+ expect(doc.at_css('td:contains("Bar")')['style']).to eq 'text-align: right'
+ expect(doc.at_css('td:contains("Baz")')['style']).to eq 'text-align: left'
+ end
end
end
diff --git a/spec/features/merge_request/maintainer_edits_fork_spec.rb b/spec/features/merge_request/maintainer_edits_fork_spec.rb
index a3323da1b1f..7839b97122c 100644
--- a/spec/features/merge_request/maintainer_edits_fork_spec.rb
+++ b/spec/features/merge_request/maintainer_edits_fork_spec.rb
@@ -14,11 +14,11 @@ describe 'a maintainer edits files on a source-branch of an MR from a fork', :js
source_branch: 'fix',
target_branch: 'master',
author: author,
- allow_maintainer_to_push: true)
+ allow_collaboration: true)
end
before do
- target_project.add_master(user)
+ target_project.add_maintainer(user)
sign_in(user)
visit project_merge_request_path(target_project, merge_request)
diff --git a/spec/features/merge_request/user_allows_a_maintainer_to_push_spec.rb b/spec/features/merge_request/user_allows_a_maintainer_to_push_spec.rb
deleted file mode 100644
index eb41d7de8ed..00000000000
--- a/spec/features/merge_request/user_allows_a_maintainer_to_push_spec.rb
+++ /dev/null
@@ -1,85 +0,0 @@
-require 'spec_helper'
-
-describe 'create a merge request that allows maintainers to push', :js do
- include ProjectForksHelper
- let(:user) { create(:user) }
- let(:target_project) { create(:project, :public, :repository) }
- let(:source_project) { fork_project(target_project, user, repository: true, namespace: user.namespace) }
-
- def visit_new_merge_request
- visit project_new_merge_request_path(
- source_project,
- merge_request: {
- source_project_id: source_project.id,
- target_project_id: target_project.id,
- source_branch: 'fix',
- target_branch: 'master'
- })
- end
-
- before do
- sign_in(user)
- end
-
- it 'allows setting maintainer push possible' do
- visit_new_merge_request
-
- check 'Allow edits from maintainers'
-
- click_button 'Submit merge request'
-
- wait_for_requests
-
- expect(page).to have_content('Allows edits from maintainers')
- end
-
- it 'shows a message when one of the projects is private' do
- source_project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
-
- visit_new_merge_request
-
- expect(page).to have_content('Not available for private projects')
- end
-
- it 'shows a message when the source branch is protected' do
- create(:protected_branch, project: source_project, name: 'fix')
-
- visit_new_merge_request
-
- expect(page).to have_content('Not available for protected branches')
- end
-
- context 'when the merge request is being created within the same project' do
- let(:source_project) { target_project }
-
- it 'hides the checkbox if the merge request is being created within the same project' do
- target_project.add_developer(user)
-
- visit_new_merge_request
-
- expect(page).not_to have_content('Allows edits from maintainers')
- end
- end
-
- context 'when a maintainer tries to edit the option' do
- let(:maintainer) { create(:user) }
- let(:merge_request) do
- create(:merge_request,
- source_project: source_project,
- target_project: target_project,
- source_branch: 'fixes')
- end
-
- before do
- target_project.add_master(maintainer)
-
- sign_in(maintainer)
- end
-
- it 'it hides the option from maintainers' do
- visit edit_project_merge_request_path(target_project, merge_request)
-
- expect(page).not_to have_content('Allows edits from maintainers')
- end
- end
-end
diff --git a/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb b/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb
new file mode 100644
index 00000000000..0ccab5b2fac
--- /dev/null
+++ b/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb
@@ -0,0 +1,85 @@
+require 'spec_helper'
+
+describe 'create a merge request, allowing commits from members who can merge to the target branch', :js do
+ include ProjectForksHelper
+ let(:user) { create(:user) }
+ let(:target_project) { create(:project, :public, :repository) }
+ let(:source_project) { fork_project(target_project, user, repository: true, namespace: user.namespace) }
+
+ def visit_new_merge_request
+ visit project_new_merge_request_path(
+ source_project,
+ merge_request: {
+ source_project_id: source_project.id,
+ target_project_id: target_project.id,
+ source_branch: 'fix',
+ target_branch: 'master'
+ })
+ end
+
+ before do
+ sign_in(user)
+ end
+
+ it 'allows setting possible' do
+ visit_new_merge_request
+
+ check 'Allow commits from members who can merge to the target branch'
+
+ click_button 'Submit merge request'
+
+ wait_for_requests
+
+ expect(page).to have_content('Allows commits from members who can merge to the target branch')
+ end
+
+ it 'shows a message when one of the projects is private' do
+ source_project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+
+ visit_new_merge_request
+
+ expect(page).to have_content('Not available for private projects')
+ end
+
+ it 'shows a message when the source branch is protected' do
+ create(:protected_branch, project: source_project, name: 'fix')
+
+ visit_new_merge_request
+
+ expect(page).to have_content('Not available for protected branches')
+ end
+
+ context 'when the merge request is being created within the same project' do
+ let(:source_project) { target_project }
+
+ it 'hides the checkbox if the merge request is being created within the same project' do
+ target_project.add_developer(user)
+
+ visit_new_merge_request
+
+ expect(page).not_to have_content('Allows commits from members who can merge to the target branch')
+ end
+ end
+
+ context 'when a member who can merge tries to edit the option' do
+ let(:member) { create(:user) }
+ let(:merge_request) do
+ create(:merge_request,
+ source_project: source_project,
+ target_project: target_project,
+ source_branch: 'fixes')
+ end
+
+ before do
+ target_project.add_maintainer(member)
+
+ sign_in(member)
+ end
+
+ it 'it hides the option from members' do
+ visit edit_project_merge_request_path(target_project, merge_request)
+
+ expect(page).not_to have_content('Allows commits from members who can merge to the target branch')
+ end
+ end
+end
diff --git a/spec/features/merge_request/user_cherry_picks_spec.rb b/spec/features/merge_request/user_cherry_picks_spec.rb
index 61d1bdaa95a..c6ec3f08cc5 100644
--- a/spec/features/merge_request/user_cherry_picks_spec.rb
+++ b/spec/features/merge_request/user_cherry_picks_spec.rb
@@ -7,7 +7,7 @@ describe 'Merge request > User cherry-picks', :js do
let(:merge_request) { create(:merge_request_with_diffs, source_project: project, author: user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/features/merge_request/user_creates_image_diff_notes_spec.rb b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb
index 7c4fd25bb39..f0d38dc6a0c 100644
--- a/spec/features/merge_request/user_creates_image_diff_notes_spec.rb
+++ b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Merge request > User creates image diff notes', :js do
+describe 'Merge request > User creates image diff notes', :js do
include NoteInteractionHelpers
let(:project) { create(:project, :public, :repository) }
@@ -12,7 +12,7 @@ feature 'Merge request > User creates image diff notes', :js do
# Stub helper to return any blob file as image from public app folder.
# This is necessary to run this specs since we don't display repo images in capybara.
allow_any_instance_of(DiffHelper).to receive(:diff_file_blob_raw_url).and_return('/apple-touch-icon.png')
- allow_any_instance_of(DiffHelper).to receive(:diff_file_old_blob_raw_url).and_return('/favicon.ico')
+ allow_any_instance_of(DiffHelper).to receive(:diff_file_old_blob_raw_url).and_return('/favicon.png')
end
context 'create commit diff notes' do
@@ -114,7 +114,8 @@ feature 'Merge request > User creates image diff notes', :js do
create_image_diff_note
end
- it 'shows indicator and avatar badges, and allows collapsing/expanding the discussion notes' do
+ # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
+ xit 'shows indicator and avatar badges, and allows collapsing/expanding the discussion notes' do
indicator = find('.js-image-badge', match: :first)
badge = find('.image-diff-avatar-link .badge', match: :first)
@@ -156,7 +157,8 @@ feature 'Merge request > User creates image diff notes', :js do
visit project_merge_request_path(project, merge_request)
end
- it 'render diff indicators within the image frame' do
+ # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
+ xit 'render diff indicators within the image frame' do
diff_note = create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: position)
wait_for_requests
diff --git a/spec/features/merge_request/user_customizes_merge_commit_message_spec.rb b/spec/features/merge_request/user_customizes_merge_commit_message_spec.rb
index e1e70b6d260..8d2d4279d3c 100644
--- a/spec/features/merge_request/user_customizes_merge_commit_message_spec.rb
+++ b/spec/features/merge_request/user_customizes_merge_commit_message_spec.rb
@@ -32,7 +32,7 @@ describe 'Merge request < User customizes merge commit message', :js do
end
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit project_merge_request_path(project, merge_request)
end
diff --git a/spec/features/merge_request/user_locks_discussion_spec.rb b/spec/features/merge_request/user_locks_discussion_spec.rb
index a68df872334..76c759ab8d3 100644
--- a/spec/features/merge_request/user_locks_discussion_spec.rb
+++ b/spec/features/merge_request/user_locks_discussion_spec.rb
@@ -38,9 +38,9 @@ describe 'Merge request > User locks discussion', :js do
end
it 'the user can not create a comment' do
- page.within('.issuable-discussion #notes') do
+ page.within('.js-vue-notes-event') do
expect(page).not_to have_selector('js-main-target-form')
- expect(page.find('.disabled-comment'))
+ expect(page.find('.issuable-note-warning'))
.to have_content('This merge request is locked. Only project members can comment.')
end
end
diff --git a/spec/features/merge_request/user_merges_immediately_spec.rb b/spec/features/merge_request/user_merges_immediately_spec.rb
index b16fc9bfc89..ea61f9675bc 100644
--- a/spec/features/merge_request/user_merges_immediately_spec.rb
+++ b/spec/features/merge_request/user_merges_immediately_spec.rb
@@ -19,7 +19,7 @@ describe 'Merge requests > User merges immediately', :js do
context 'when there is active pipeline for merge request' do
before do
create(:ci_build, pipeline: pipeline)
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit project_merge_request_path(project, merge_request)
end
diff --git a/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb b/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb
index a045791f6b4..8372b61f872 100644
--- a/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb
+++ b/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb
@@ -5,7 +5,7 @@ describe 'Merge request > User merges only if pipeline succeeds', :js do
let(:project) { merge_request.target_project }
before do
- project.add_master(merge_request.author)
+ project.add_maintainer(merge_request.author)
sign_in(merge_request.author)
end
diff --git a/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb b/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb
index db92a3504f3..53ed5d78598 100644
--- a/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb
+++ b/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb
@@ -17,7 +17,7 @@ describe 'Merge request > User merges when pipeline succeeds', :js do
end
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
context 'when there is active pipeline for merge request' do
diff --git a/spec/features/merge_request/user_posts_diff_notes_spec.rb b/spec/features/merge_request/user_posts_diff_notes_spec.rb
index 2b4623d6dc9..02d19db3828 100644
--- a/spec/features/merge_request/user_posts_diff_notes_spec.rb
+++ b/spec/features/merge_request/user_posts_diff_notes_spec.rb
@@ -65,11 +65,13 @@ describe 'Merge request > User posts diff notes', :js do
context 'with a match line' do
it 'does not allow commenting on the left side' do
- should_not_allow_commenting(find('.match', match: :first).find(:xpath, '..'), 'left')
+ line_holder = find('.match', match: :first).find(:xpath, '..')
+ should_not_allow_commenting(line_holder, 'left')
end
it 'does not allow commenting on the right side' do
- should_not_allow_commenting(find('.match', match: :first).find(:xpath, '..'), 'right')
+ line_holder = find('.match', match: :first).find(:xpath, '..')
+ should_not_allow_commenting(line_holder, 'right')
end
end
@@ -81,7 +83,7 @@ describe 'Merge request > User posts diff notes', :js do
# The first `.js-unfold` unfolds upwards, therefore the first
# `.line_holder` will be an unfolded line.
- let(:line_holder) { first('.line_holder[id="1"]') }
+ let(:line_holder) { first('#a5cc2925ca8258af241be7e5b0381edf30266302 .line_holder') }
it 'does not allow commenting on the left side' do
should_not_allow_commenting(line_holder, 'left')
@@ -143,7 +145,7 @@ describe 'Merge request > User posts diff notes', :js do
# The first `.js-unfold` unfolds upwards, therefore the first
# `.line_holder` will be an unfolded line.
- let(:line_holder) { first('.line_holder[id="1"]') }
+ let(:line_holder) { first('.line_holder[id="a5cc2925ca8258af241be7e5b0381edf30266302_1_1"]') }
it 'does not allow commenting' do
should_not_allow_commenting line_holder
@@ -183,7 +185,7 @@ describe 'Merge request > User posts diff notes', :js do
end
describe 'posting a note' do
- it 'adds as discussion' do
+ xit 'adds as discussion' do
expect(page).to have_css('.js-temp-notes-holder', count: 2)
should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]'), asset_form_reset: false)
@@ -196,25 +198,28 @@ describe 'Merge request > User posts diff notes', :js do
context 'when the MR only supports legacy diff notes' do
before do
- merge_request.merge_request_diff.update_attributes(start_commit_sha: nil)
+ merge_request.merge_request_diff.update(start_commit_sha: nil)
visit diffs_project_merge_request_path(project, merge_request, view: 'inline')
end
context 'with a new line' do
- it 'allows commenting' do
- should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
+ # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
+ xit 'allows commenting' do
+ should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]').find(:xpath, '..'))
end
end
context 'with an old line' do
- it 'allows commenting' do
- should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]'))
+ # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
+ xit 'allows commenting' do
+ should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]').find(:xpath, '..'))
end
end
context 'with an unchanged line' do
- it 'allows commenting' do
- should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]'))
+ # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
+ xit 'allows commenting' do
+ should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]').find(:xpath, '..'))
end
end
diff --git a/spec/features/merge_request/user_posts_notes_spec.rb b/spec/features/merge_request/user_posts_notes_spec.rb
index b54addce993..260c5f9c28b 100644
--- a/spec/features/merge_request/user_posts_notes_spec.rb
+++ b/spec/features/merge_request/user_posts_notes_spec.rb
@@ -14,7 +14,7 @@ describe 'Merge request > User posts notes', :js do
end
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit project_merge_request_path(project, merge_request)
end
@@ -24,10 +24,9 @@ describe 'Merge request > User posts notes', :js do
describe 'the note form' do
it 'is valid' do
is_expected.to have_css('.js-main-target-form', visible: true, count: 1)
- expect(find('.js-main-target-form .js-comment-button').value)
- .to eq('Comment')
+ expect(find('.js-main-target-form')).to have_selector('button', text: 'Comment')
page.within('.js-main-target-form') do
- expect(page).not_to have_link('Cancel')
+ expect(page).not_to have_button('Cancel')
end
end
@@ -60,8 +59,9 @@ describe 'Merge request > User posts notes', :js do
is_expected.to have_content('This is awesome!')
page.within('.js-main-target-form') do
expect(page).to have_no_field('note[note]', with: 'This is awesome!')
- expect(page).to have_css('.js-md-preview', visible: :hidden)
+ expect(page).to have_css('.js-vue-md-preview', visible: :hidden)
end
+ wait_for_requests
page.within('.js-main-target-form') do
is_expected.to have_css('.js-note-text', visible: true)
end
@@ -76,6 +76,7 @@ describe 'Merge request > User posts notes', :js do
end
it 'hides the toolbar buttons when previewing a note' do
+ wait_for_requests
find('.js-md-preview-button').click
page.within('.js-main-target-form') do
expect(page).not_to have_css('.md-header-toolbar.active')
@@ -84,11 +85,6 @@ describe 'Merge request > User posts notes', :js do
end
describe 'when editing a note' do
- it 'there should be a hidden edit form' do
- is_expected.to have_css('.note-edit-form:not(.mr-note-edit-form)', visible: false, count: 1)
- is_expected.to have_css('.note-edit-form.mr-note-edit-form', visible: false, count: 1)
- end
-
describe 'editing the note' do
before do
find('.note').hover
@@ -108,8 +104,8 @@ describe 'Merge request > User posts notes', :js do
within('.current-note-edit-form') do
fill_in 'note[note]', with: 'Some new content'
find('.btn-cancel').click
- expect(find('.js-note-text', visible: false).text).to eq ''
end
+ expect(find('.js-note-text').text).to eq ''
end
it 'allows using markdown buttons after saving a note and then trying to edit it again' do
@@ -118,8 +114,8 @@ describe 'Merge request > User posts notes', :js do
find('.btn-save').click
end
- wait_for_requests
find('.note').hover
+ wait_for_requests
find('.js-note-edit').click
@@ -139,7 +135,7 @@ describe 'Merge request > User posts notes', :js do
page.within("#note_#{note.id}") do
is_expected.to have_css('.note_edited_ago')
expect(find('.note_edited_ago').text)
- .to match(/less than a minute ago/)
+ .to match(/just now/)
end
end
end
@@ -151,13 +147,15 @@ describe 'Merge request > User posts notes', :js do
find('.js-note-edit').click
end
- it 'shows the delete link' do
+ # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
+ xit 'shows the delete link' do
page.within('.note-attachment') do
is_expected.to have_css('.js-note-attachment-delete')
end
end
- it 'removes the attachment div and resets the edit form' do
+ # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
+ xit 'removes the attachment div and resets the edit form' do
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')
diff --git a/spec/features/merge_request/user_resolves_conflicts_spec.rb b/spec/features/merge_request/user_resolves_conflicts_spec.rb
index 25a57f7d379..629052442b4 100644
--- a/spec/features/merge_request/user_resolves_conflicts_spec.rb
+++ b/spec/features/merge_request/user_resolves_conflicts_spec.rb
@@ -10,7 +10,7 @@ describe 'Merge request > User resolves conflicts', :js do
end
def create_merge_request(source_branch)
- create(:merge_request, source_branch: source_branch, target_branch: 'conflict-start', source_project: project) do |mr|
+ create(:merge_request, source_branch: source_branch, target_branch: 'conflict-start', source_project: project, merge_status: :unchecked) do |mr|
mr.mark_as_unmergeable
end
end
@@ -44,7 +44,9 @@ describe 'Merge request > User resolves conflicts', :js do
within find('.diff-file', text: 'files/ruby/regex.rb') do
expect(page).to have_selector('.line_content.new', text: "def username_regexp")
+ expect(page).not_to have_selector('.line_content.new', text: "def username_regex")
expect(page).to have_selector('.line_content.new', text: "def project_name_regexp")
+ expect(page).not_to have_selector('.line_content.new', text: "def project_name_regex")
expect(page).to have_selector('.line_content.new', text: "def path_regexp")
expect(page).to have_selector('.line_content.new', text: "def archive_formats_regexp")
expect(page).to have_selector('.line_content.new', text: "def git_reference_regexp")
@@ -108,8 +110,12 @@ describe 'Merge request > User resolves conflicts', :js do
click_link('conflicts', href: %r{/conflicts\Z})
end
- include_examples "conflicts are resolved in Interactive mode"
- include_examples "conflicts are resolved in Edit inline mode"
+ # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
+ # include_examples "conflicts are resolved in Interactive mode"
+ # include_examples "conflicts are resolved in Edit inline mode"
+
+ it 'prevents RSpec/EmptyExampleGroup' do
+ end
end
context 'in Parallel view mode' do
@@ -118,8 +124,12 @@ describe 'Merge request > User resolves conflicts', :js do
click_button 'Side-by-side'
end
- include_examples "conflicts are resolved in Interactive mode"
- include_examples "conflicts are resolved in Edit inline mode"
+ # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
+ # include_examples "conflicts are resolved in Interactive mode"
+ # include_examples "conflicts are resolved in Edit inline mode"
+
+ it 'prevents RSpec/EmptyExampleGroup' do
+ end
end
end
@@ -138,7 +148,8 @@ describe 'Merge request > User resolves conflicts', :js do
end
end
- it 'conflicts are resolved in Edit inline mode' do
+ # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
+ xit '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')
diff --git a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb
index 0fd2840c426..bf4d5396df9 100644
--- a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb
+++ b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb
@@ -19,7 +19,7 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
context 'no discussions' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
note.destroy
visit_merge_request
@@ -33,7 +33,7 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
context 'as authorized user' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit_merge_request
end
@@ -102,7 +102,8 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
describe 'timeline view' do
it 'hides when resolve discussion is clicked' do
- expect(page).to have_selector('.discussion-body', visible: false)
+ expect(page).to have_selector('.discussion-header')
+ expect(page).not_to have_selector('.discussion-body')
end
it 'shows resolved discussion when toggled' do
@@ -129,7 +130,7 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
end
it 'hides when resolve discussion is clicked' do
- expect(page).to have_selector('.diffs .diff-file .notes_holder', visible: false)
+ expect(page).not_to have_selector('.diffs .diff-file .notes_holder')
end
it 'shows resolved discussion when toggled' do
@@ -218,10 +219,13 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
it 'updates updated text after resolving note' do
page.within '.diff-content .note' do
- find('.line-resolve-btn').click
- end
+ resolve_button = find('.line-resolve-btn')
+
+ resolve_button.click
+ wait_for_requests
- expect(page).to have_content("Resolved by #{user.name}")
+ expect(resolve_button['data-original-title']).to eq("Resolved by #{user.name}")
+ end
end
it 'hides jump to next discussion button' do
@@ -254,11 +258,16 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
end
it 'resolves discussion' do
- page.all('.note .line-resolve-btn').each do |button|
+ resolve_buttons = page.all('.note .line-resolve-btn', count: 2)
+ resolve_buttons.each do |button|
button.click
end
- expect(page).to have_content('Resolved by')
+ wait_for_requests
+
+ resolve_buttons.each do |button|
+ expect(button['data-original-title']).to eq("Resolved by #{user.name}")
+ end
page.within '.line-resolve-all-container' do
expect(page).to have_content('1/1 discussion resolved')
@@ -287,7 +296,7 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
end
it 'allows user to mark all notes as resolved' do
- page.all('.line-resolve-btn').each do |btn|
+ page.all('.note .line-resolve-btn', count: 2).each do |btn|
btn.click
end
@@ -298,7 +307,7 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
end
it 'allows user user to mark all discussions as resolved' do
- page.all('.discussion-reply-holder').each do |reply_holder|
+ page.all('.discussion-reply-holder', count: 2).each do |reply_holder|
page.within reply_holder do
click_button 'Resolve discussion'
end
@@ -311,7 +320,7 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
end
it 'allows user to quickly scroll to next unresolved discussion' do
- page.within first('.discussion-reply-holder') do
+ page.within('.discussion-reply-holder', match: :first) do
click_button 'Resolve discussion'
end
@@ -323,19 +332,22 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
end
it 'updates updated text after resolving note' do
- page.within first('.diff-content .note') do
- find('.line-resolve-btn').click
- end
+ page.within('.diff-content .note', match: :first) do
+ resolve_button = find('.line-resolve-btn')
- expect(page).to have_content("Resolved by #{user.name}")
+ resolve_button.click
+ wait_for_requests
+
+ expect(resolve_button['data-original-title']).to eq("Resolved by #{user.name}")
+ end
end
it 'shows jump to next discussion button' do
- expect(page.all('.discussion-reply-holder')).to all(have_selector('.discussion-next-btn'))
+ expect(page.all('.discussion-reply-holder', count: 2)).to all(have_selector('.discussion-next-btn'))
end
it 'displays next discussion even if hidden' do
- page.all('.note-discussion').each do |discussion|
+ page.all('.note-discussion', count: 2).each do |discussion|
page.within discussion do
click_button 'Toggle discussion'
end
diff --git a/spec/features/merge_request/user_resolves_outdated_diff_discussions_spec.rb b/spec/features/merge_request/user_resolves_outdated_diff_discussions_spec.rb
index 9ba9e8b9585..777464ef841 100644
--- a/spec/features/merge_request/user_resolves_outdated_diff_discussions_spec.rb
+++ b/spec/features/merge_request/user_resolves_outdated_diff_discussions_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Merge request > User resolves outdated diff discussions', :js do
+describe 'Merge request > User resolves outdated diff discussions', :js do
let(:project) { create(:project, :repository, :public) }
let(:merge_request) do
@@ -63,7 +63,7 @@ feature 'Merge request > User resolves outdated diff discussions', :js do
it 'shows that as automatically resolved' do
within(".discussion[data-discussion-id='#{outdated_discussion.id}']") do
- expect(page).to have_css('.discussion-body', visible: false)
+ expect(page).not_to have_css('.discussion-body')
expect(page).to have_content('Automatically resolved')
end
end
diff --git a/spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb b/spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb
index 3b6fffb7abd..8c2599615cb 100644
--- a/spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb
+++ b/spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb
@@ -17,11 +17,12 @@ describe 'Merge request > User scrolls to note on load', :js do
it 'scrolls note into view' do
visit "#{project_merge_request_path(project, merge_request)}#{fragment_id}"
+ wait_for_requests
+
page_height = page.current_window.size[1]
page_scroll_y = page.evaluate_script("window.scrollY")
fragment_position_top = page.evaluate_script("Math.round($('#{fragment_id}').offset().top)")
- expect(find('.js-toggle-content').visible?).to eq true
expect(find(fragment_id).visible?).to eq true
expect(fragment_position_top).to be >= page_scroll_y
expect(fragment_position_top).to be < (page_scroll_y + page_height)
@@ -35,7 +36,7 @@ describe 'Merge request > User scrolls to note on load', :js do
page.execute_script "window.scrollTo(0,0)"
note_element = find(fragment_id)
- note_container = note_element.ancestor('.js-toggle-container')
+ note_container = note_element.ancestor('.js-discussion-container')
expect(note_element.visible?).to eq true
@@ -44,10 +45,11 @@ describe 'Merge request > User scrolls to note on load', :js do
end
end
- it 'expands collapsed notes' do
+ # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
+ xit 'expands collapsed notes' do
visit "#{project_merge_request_path(project, merge_request)}#{collapsed_fragment_id}"
note_element = find(collapsed_fragment_id)
- note_container = note_element.ancestor('.js-toggle-container')
+ note_container = note_element.ancestor('.timeline-content')
expect(note_element.visible?).to eq true
expect(note_container.find('.line_content.noteable_line.old', match: :first).visible?).to eq true
diff --git a/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb b/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb
index 9c0a04405a6..428eb414274 100644
--- a/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb
+++ b/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb
@@ -19,7 +19,7 @@ describe 'Merge request > User sees avatars on diff notes', :js do
let!(:note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: position) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in user
set_cookie('sidebar_collapsed', 'true')
@@ -35,7 +35,7 @@ describe 'Merge request > User sees avatars on diff notes', :js do
expect(page).not_to have_selector('.diff-comment-avatar-holders')
end
- it 'does not render avatars after commening on discussion tab' do
+ it 'does not render avatars after commenting on discussion tab' do
click_button 'Reply...'
page.within('.js-discussion-note-form') do
@@ -75,7 +75,7 @@ describe 'Merge request > User sees avatars on diff notes', :js do
end
end
- %w(inline parallel).each do |view|
+ %w(parallel).each do |view|
context "#{view} view" do
before do
visit diffs_project_merge_request_path(project, merge_request, view: view)
@@ -104,7 +104,7 @@ describe 'Merge request > User sees avatars on diff notes', :js do
find('.diff-notes-collapse').send_keys(:return)
end
- expect(page).to have_selector('.notes_holder', visible: false)
+ expect(page).not_to have_selector('.notes_holder')
page.within find_line(position.line_code(project.repository)) do
first('img.js-diff-comment-avatar').click
diff --git a/spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb b/spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb
new file mode 100644
index 00000000000..c40c720d168
--- /dev/null
+++ b/spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb
@@ -0,0 +1,24 @@
+require 'rails_helper'
+
+describe 'Merge request > User sees Check out branch modal', :js do
+ let(:project) { create(:project, :public, :repository) }
+ let(:user) { project.creator }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ before do
+ sign_in(user)
+ visit project_merge_request_path(project, merge_request)
+ wait_for_requests
+ click_button('Check out branch')
+ end
+
+ it 'shows the check out branch modal' do
+ expect(page).to have_content('Check out, review, and merge locally')
+ end
+
+ it 'closes the check out branch model with Escape keypress' do
+ find('#modal_merge_info').send_keys(:escape)
+
+ expect(page).not_to have_content('Check out, review, and merge locally')
+ end
+end
diff --git a/spec/features/merge_request/user_sees_closing_issues_message_spec.rb b/spec/features/merge_request/user_sees_closing_issues_message_spec.rb
index 726f35557a7..d7c784b14c5 100644
--- a/spec/features/merge_request/user_sees_closing_issues_message_spec.rb
+++ b/spec/features/merge_request/user_sees_closing_issues_message_spec.rb
@@ -18,7 +18,7 @@ describe 'Merge request > User sees closing issues message', :js do
let(:merge_request_title) { 'Merge Request Title' }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit project_merge_request_path(project, merge_request)
wait_for_requests
diff --git a/spec/features/merge_request/user_sees_deleted_target_branch_spec.rb b/spec/features/merge_request/user_sees_deleted_target_branch_spec.rb
index 01115318370..46c21a2b155 100644
--- a/spec/features/merge_request/user_sees_deleted_target_branch_spec.rb
+++ b/spec/features/merge_request/user_sees_deleted_target_branch_spec.rb
@@ -6,7 +6,7 @@ describe 'Merge request > User sees deleted target branch', :js do
let(:user) { project.creator }
before do
- project.add_master(user)
+ project.add_maintainer(user)
DeleteBranchService.new(project, user).execute('feature')
sign_in(user)
visit project_merge_request_path(project, merge_request)
diff --git a/spec/features/merge_request/user_sees_diff_spec.rb b/spec/features/merge_request/user_sees_diff_spec.rb
index a9063f2bcb3..d6e7ff33d5d 100644
--- a/spec/features/merge_request/user_sees_diff_spec.rb
+++ b/spec/features/merge_request/user_sees_diff_spec.rb
@@ -6,20 +6,6 @@ describe 'Merge request > User sees diff', :js do
let(:project) { create(:project, :public, :repository) }
let(:merge_request) { create(:merge_request, source_project: project) }
- context 'when visit with */* as accept header' do
- it 'renders the notes' do
- create :note_on_merge_request, project: project, noteable: merge_request, note: 'Rebasing with master'
-
- 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')
- expect(page).to have_css('.diffs.tab-pane.active')
- end
- end
-
context 'when linking to note' do
describe 'with unresolved note' do
let(:note) { create :diff_note_on_merge_request, project: project, noteable: merge_request }
@@ -51,6 +37,7 @@ describe 'Merge request > User sees diff', :js do
context 'when merge request has overflow' do
it 'displays warning' do
allow(Commit).to receive(:max_diff_options).and_return(max_files: 3)
+ allow_any_instance_of(DiffHelper).to receive(:render_overflow_warning?).and_return(true)
visit diffs_project_merge_request_path(project, merge_request)
diff --git a/spec/features/merge_request/user_sees_discussions_spec.rb b/spec/features/merge_request/user_sees_discussions_spec.rb
index d6e8c8e86ba..7b8c3bacfe2 100644
--- a/spec/features/merge_request/user_sees_discussions_spec.rb
+++ b/spec/features/merge_request/user_sees_discussions_spec.rb
@@ -1,12 +1,12 @@
require 'rails_helper'
-describe 'Merge request > User sees discussions' do
+describe 'Merge request > User sees discussions', :js do
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
let(:merge_request) { create(:merge_request, source_project: project) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
@@ -53,11 +53,13 @@ describe 'Merge request > User sees discussions' do
shared_examples 'a functional discussion' do
let(:discussion_id) { note.discussion_id(merge_request) }
- it 'is displayed' do
+ # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
+ xit 'is displayed' do
expect(page).to have_css(".discussion[data-discussion-id='#{discussion_id}']")
end
- it 'can be replied to' do
+ # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
+ xit 'can be replied to' do
within(".discussion[data-discussion-id='#{discussion_id}']") do
click_button 'Reply...'
fill_in 'note[note]', with: 'Test!'
diff --git a/spec/features/merge_request/user_sees_empty_state_spec.rb b/spec/features/merge_request/user_sees_empty_state_spec.rb
index a939c7e9001..482f31b02d4 100644
--- a/spec/features/merge_request/user_sees_empty_state_spec.rb
+++ b/spec/features/merge_request/user_sees_empty_state_spec.rb
@@ -5,7 +5,7 @@ describe 'Merge request > User sees empty state' do
let(:user) { project.creator }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/features/merge_request/user_sees_merge_button_depending_on_unresolved_discussions_spec.rb b/spec/features/merge_request/user_sees_merge_button_depending_on_unresolved_discussions_spec.rb
index 85df43df38e..f6b771facf8 100644
--- a/spec/features/merge_request/user_sees_merge_button_depending_on_unresolved_discussions_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_button_depending_on_unresolved_discussions_spec.rb
@@ -6,7 +6,7 @@ describe 'Merge request > User sees merge button depending on unresolved discuss
let!(:merge_request) { create(:merge_request_with_diff_notes, source_project: project, author: user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb
index 51a65407aec..b6b3844f2ae 100644
--- a/spec/features/merge_request/user_sees_merge_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb
@@ -10,8 +10,8 @@ describe 'Merge request > User sees merge widget', :js do
let(:merge_request_in_only_mwps_project) { create(:merge_request, source_project: project_only_mwps) }
before do
- project.add_master(user)
- project_only_mwps.add_master(user)
+ project.add_maintainer(user)
+ project_only_mwps.add_maintainer(user)
sign_in(user)
end
@@ -275,7 +275,7 @@ describe 'Merge request > User sees merge widget', :js do
let(:user2) { create(:user) }
before do
- project.add_master(user2)
+ project.add_maintainer(user2)
sign_out(:user)
sign_in(user2)
merge_request.update(target_project: fork_project)
diff --git a/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb b/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb
index fd1629746ef..0272d300e06 100644
--- a/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb
+++ b/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb
@@ -23,15 +23,16 @@ describe 'Merge request < User sees mini pipeline graph', :js do
end
context 'as json' do
- let(:artifacts_file1) { fixture_file_upload(Rails.root.join('spec/fixtures/banana_sample.gif'), 'image/gif') }
- let(:artifacts_file2) { fixture_file_upload(Rails.root.join('spec/fixtures/dk.png'), 'image/png') }
+ let(:artifacts_file1) { fixture_file_upload(File.join('spec/fixtures/banana_sample.gif'), 'image/gif') }
+ let(:artifacts_file2) { fixture_file_upload(File.join('spec/fixtures/dk.png'), 'image/png') }
before do
create(:ci_build, :success, :trace_artifact, pipeline: pipeline, legacy_artifacts_file: artifacts_file1)
create(:ci_build, :manual, pipeline: pipeline, when: 'manual')
end
- it 'avoids repeated database queries' do
+ # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
+ xit 'avoids repeated database queries' do
before = ActiveRecord::QueryRecorder.new { visit_merge_request(format: :json, serializer: 'widget') }
create(:ci_build, :success, :trace_artifact, pipeline: pipeline, legacy_artifacts_file: artifacts_file2)
diff --git a/spec/features/merge_request/user_sees_notes_from_forked_project_spec.rb b/spec/features/merge_request/user_sees_notes_from_forked_project_spec.rb
index b4cda269852..d4ad0b0a377 100644
--- a/spec/features/merge_request/user_sees_notes_from_forked_project_spec.rb
+++ b/spec/features/merge_request/user_sees_notes_from_forked_project_spec.rb
@@ -25,7 +25,7 @@ describe 'Merge request > User sees notes from forked project', :js do
page.within('.discussion-notes') do
find('.btn-text-field').click
find('#note_note').send_keys('A reply comment')
- find('.comment-btn').click
+ find('.js-comment-button').click
end
wait_for_requests
diff --git a/spec/features/merge_request/user_sees_pipelines_spec.rb b/spec/features/merge_request/user_sees_pipelines_spec.rb
index a42c016392b..45cccbee63e 100644
--- a/spec/features/merge_request/user_sees_pipelines_spec.rb
+++ b/spec/features/merge_request/user_sees_pipelines_spec.rb
@@ -7,7 +7,7 @@ describe 'Merge request > User sees pipelines', :js do
let(:user) { project.creator }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
@@ -70,7 +70,7 @@ describe 'Merge request > User sees pipelines', :js do
end
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in user
end
diff --git a/spec/features/merge_request/user_sees_system_notes_spec.rb b/spec/features/merge_request/user_sees_system_notes_spec.rb
index a00a682757d..c6811d4161a 100644
--- a/spec/features/merge_request/user_sees_system_notes_spec.rb
+++ b/spec/features/merge_request/user_sees_system_notes_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-describe 'Merge request > User sees system notes' do
+describe 'Merge request > User sees system notes', :js do
let(:public_project) { create(:project, :public, :repository) }
let(:private_project) { create(:project, :private, :repository) }
let(:user) { private_project.creator }
diff --git a/spec/features/merge_request/user_sees_versions_spec.rb b/spec/features/merge_request/user_sees_versions_spec.rb
index 3a15d70979a..f42b4dcbb47 100644
--- a/spec/features/merge_request/user_sees_versions_spec.rb
+++ b/spec/features/merge_request/user_sees_versions_spec.rb
@@ -10,7 +10,7 @@ describe 'Merge request > User sees versions', :js do
let!(:params) { {} }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit diffs_project_merge_request_path(project, merge_request, params)
end
@@ -143,9 +143,9 @@ describe 'Merge request > User sees versions', :js do
end
it_behaves_like 'allows commenting',
- file_id: '7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44',
- line_code: '4_4',
- comment: 'Typo, please fix.'
+ file_id: '7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44',
+ line_code: '4_4',
+ comment: 'Typo, please fix.'
end
describe 'compare with same version' do
diff --git a/spec/features/merge_request/user_sees_wip_help_message_spec.rb b/spec/features/merge_request/user_sees_wip_help_message_spec.rb
index bc25243244e..92cc73ddf1f 100644
--- a/spec/features/merge_request/user_sees_wip_help_message_spec.rb
+++ b/spec/features/merge_request/user_sees_wip_help_message_spec.rb
@@ -5,7 +5,7 @@ describe 'Merge request > User sees WIP help message' do
let(:user) { project.creator }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
index 42c279af117..ae41cf90576 100644
--- a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
+++ b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
@@ -4,8 +4,14 @@ describe 'Merge request > User selects branches for new MR', :js do
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
+ def select_source_branch(branch_name)
+ find('.js-source-branch', match: :first).click
+ find('.js-source-branch-dropdown .dropdown-input-field').native.send_keys branch_name
+ find('.js-source-branch-dropdown .dropdown-content a', text: branch_name, match: :first).click
+ end
+
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
@@ -43,8 +49,7 @@ describe 'Merge request > User selects branches for new MR', :js do
it 'generates a diff for an orphaned branch' do
visit project_new_merge_request_path(project)
- find('.js-source-branch', match: :first).click
- find('.js-source-branch-dropdown .dropdown-content a', text: 'orphaned-branch', match: :first).click
+ select_source_branch('orphaned-branch')
click_button "Compare branches"
click_link "Changes"
@@ -169,4 +174,31 @@ describe 'Merge request > User selects branches for new MR', :js do
end
end
end
+
+ context 'with special characters in branch names' do
+ it 'escapes quotes in branch names' do
+ special_branch_name = '"with-quotes"'
+ CreateBranchService.new(project, user)
+ .execute(special_branch_name, 'add-pdf-file')
+
+ visit project_new_merge_request_path(project)
+ select_source_branch(special_branch_name)
+
+ source_branch_input = find('[name="merge_request[source_branch]"]', visible: false)
+ expect(source_branch_input.value).to eq special_branch_name
+ end
+
+ it 'does not escape unicode in branch names' do
+ special_branch_name = 'ʕ•ᴥ•ʔ'
+ CreateBranchService.new(project, user)
+ .execute(special_branch_name, 'add-pdf-file')
+
+ visit project_new_merge_request_path(project)
+ select_source_branch(special_branch_name)
+
+ click_button "Compare branches"
+
+ expect(page).to have_button("Submit merge request")
+ end
+ end
end
diff --git a/spec/features/merge_request/user_toggles_whitespace_changes_spec.rb b/spec/features/merge_request/user_toggles_whitespace_changes_spec.rb
index 2e95a628013..dd860382daa 100644
--- a/spec/features/merge_request/user_toggles_whitespace_changes_spec.rb
+++ b/spec/features/merge_request/user_toggles_whitespace_changes_spec.rb
@@ -6,7 +6,7 @@ describe 'Merge request > User toggles whitespace changes', :js do
let(:user) { project.creator }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit diffs_project_merge_request_path(project, merge_request)
end
diff --git a/spec/features/merge_request/user_uses_slash_commands_spec.rb b/spec/features/merge_request/user_uses_slash_commands_spec.rb
index 7f261b580f7..b81478a481f 100644
--- a/spec/features/merge_request/user_uses_slash_commands_spec.rb
+++ b/spec/features/merge_request/user_uses_slash_commands_spec.rb
@@ -21,17 +21,25 @@ describe 'Merge request > User uses quick actions', :js do
let!(:milestone) { create(:milestone, project: project, title: 'ASAP') }
before do
- project.add_master(user)
- sign_in(user)
- visit project_merge_request_path(project, merge_request)
+ project.add_maintainer(user)
end
describe 'time tracking' do
+ before do
+ sign_in(user)
+ visit project_merge_request_path(project, merge_request)
+ end
+
it_behaves_like 'issuable time tracker'
end
describe 'toggling the WIP prefix in the title from note' do
context 'when the current user can toggle the WIP prefix' do
+ before do
+ sign_in(user)
+ visit project_merge_request_path(project, merge_request)
+ end
+
it 'adds the WIP: prefix to the title' do
add_note("/wip")
@@ -56,7 +64,6 @@ describe 'Merge request > User uses quick actions', :js do
context 'when the current user cannot toggle the WIP prefix' do
before do
project.add_guest(guest)
- sign_out(:user)
sign_in(guest)
visit project_merge_request_path(project, merge_request)
end
@@ -74,6 +81,11 @@ describe 'Merge request > User uses quick actions', :js do
describe 'merging the MR from the note' do
context 'when the current user can merge the MR' do
+ before do
+ sign_in(user)
+ visit project_merge_request_path(project, merge_request)
+ end
+
it 'merges the MR' do
add_note("/merge")
@@ -87,6 +99,8 @@ describe 'Merge request > User uses quick actions', :js do
before do
merge_request.source_branch = 'another_branch'
merge_request.save
+ sign_in(user)
+ visit project_merge_request_path(project, merge_request)
end
it 'does not merge the MR' do
@@ -101,7 +115,6 @@ describe 'Merge request > User uses quick actions', :js do
context 'when the current user cannot merge the MR' do
before do
project.add_guest(guest)
- sign_out(:user)
sign_in(guest)
visit project_merge_request_path(project, merge_request)
end
@@ -117,6 +130,11 @@ describe 'Merge request > User uses quick actions', :js do
end
describe 'adding a due date from note' do
+ before do
+ sign_in(user)
+ visit project_merge_request_path(project, merge_request)
+ end
+
it 'does not recognize the command nor create a note' do
add_note('/due 2016-08-28')
@@ -129,8 +147,7 @@ describe 'Merge request > User uses quick actions', :js do
let(:new_url_opts) { { merge_request: { source_branch: 'feature' } } }
before do
- sign_out(:user)
- another_project.add_master(user)
+ another_project.add_maintainer(user)
sign_in(user)
end
@@ -161,6 +178,11 @@ describe 'Merge request > User uses quick actions', :js do
describe '/target_branch command from note' do
context 'when the current user can change target branch' do
+ before do
+ sign_in(user)
+ visit project_merge_request_path(project, merge_request)
+ end
+
it 'changes target branch from a note' do
add_note("message start \n/target_branch merge-test\n message end.")
@@ -184,7 +206,6 @@ describe 'Merge request > User uses quick actions', :js do
context 'when current user can not change target branch' do
before do
project.add_guest(guest)
- sign_out(:user)
sign_in(guest)
visit project_merge_request_path(project, merge_request)
end
diff --git a/spec/features/merge_requests/user_mass_updates_spec.rb b/spec/features/merge_requests/user_mass_updates_spec.rb
index 199ba7e87ad..bb327159cb0 100644
--- a/spec/features/merge_requests/user_mass_updates_spec.rb
+++ b/spec/features/merge_requests/user_mass_updates_spec.rb
@@ -6,7 +6,7 @@ describe 'Merge requests > User mass updates', :js do
let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/features/merge_requests/user_squashes_merge_request_spec.rb b/spec/features/merge_requests/user_squashes_merge_request_spec.rb
new file mode 100644
index 00000000000..ec1153b7f7f
--- /dev/null
+++ b/spec/features/merge_requests/user_squashes_merge_request_spec.rb
@@ -0,0 +1,124 @@
+require 'spec_helper'
+
+describe 'User squashes a merge request', :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+ let(:source_branch) { 'csv' }
+
+ let!(:original_head) { project.repository.commit('master') }
+
+ shared_examples 'squash' do
+ it 'squashes the commits into a single commit, and adds a merge commit' do
+ expect(page).to have_content('Merged')
+
+ latest_master_commits = project.repository.commits_between(original_head.sha, 'master').map(&:raw)
+
+ squash_commit = an_object_having_attributes(sha: a_string_matching(/\h{40}/),
+ message: "Csv\n",
+ author_name: user.name,
+ committer_name: user.name)
+
+ merge_commit = an_object_having_attributes(sha: a_string_matching(/\h{40}/),
+ message: a_string_starting_with("Merge branch 'csv' into 'master'"),
+ author_name: user.name,
+ committer_name: user.name)
+
+ expect(project.repository).not_to be_merged_to_root_ref(source_branch)
+ expect(latest_master_commits).to match([squash_commit, merge_commit])
+ end
+ end
+
+ shared_examples 'no squash' do
+ it 'accepts the merge request without squashing' do
+ expect(page).to have_content('Merged')
+ expect(project.repository).to be_merged_to_root_ref(source_branch)
+ end
+ end
+
+ def accept_mr
+ expect(page).to have_button('Merge')
+
+ uncheck 'Remove source branch'
+ click_on 'Merge'
+ end
+
+ before do
+ # Prevent source branch from being removed so we can use be_merged_to_root_ref
+ # method to check if squash was performed or not
+ allow_any_instance_of(MergeRequest).to receive(:force_remove_source_branch?).and_return(false)
+ project.add_maintainer(user)
+
+ sign_in user
+ end
+
+ context 'when the MR has only one commit' do
+ before do
+ merge_request = create(:merge_request, source_project: project, target_project: project, source_branch: 'master', target_branch: 'branch-merged')
+
+ visit project_merge_request_path(project, merge_request)
+ end
+
+ it 'does not show the squash checkbox' do
+ expect(page).not_to have_field('squash')
+ end
+ end
+
+ context 'when squash is enabled on merge request creation' do
+ before do
+ visit project_new_merge_request_path(project, merge_request: { target_branch: 'master', source_branch: source_branch })
+ check 'merge_request[squash]'
+ click_on 'Submit merge request'
+ wait_for_requests
+ end
+
+ it 'shows the squash checkbox as checked' do
+ expect(page).to have_checked_field('squash')
+ end
+
+ context 'when accepting with squash checked' do
+ before do
+ accept_mr
+ end
+
+ include_examples 'squash'
+ end
+
+ context 'when accepting and unchecking squash' do
+ before do
+ uncheck 'squash'
+ accept_mr
+ end
+
+ include_examples 'no squash'
+ end
+ end
+
+ context 'when squash is not enabled on merge request creation' do
+ before do
+ visit project_new_merge_request_path(project, merge_request: { target_branch: 'master', source_branch: source_branch })
+ click_on 'Submit merge request'
+ wait_for_requests
+ end
+
+ it 'shows the squash checkbox as unchecked' do
+ expect(page).to have_unchecked_field('squash')
+ end
+
+ context 'when accepting and checking squash' do
+ before do
+ check 'squash'
+ accept_mr
+ end
+
+ include_examples 'squash'
+ end
+
+ context 'when accepting with squash unchecked' do
+ before do
+ accept_mr
+ end
+
+ include_examples 'no squash'
+ end
+ end
+end
diff --git a/spec/features/milestone_spec.rb b/spec/features/milestone_spec.rb
index 6c51e4bbe26..a0673b12738 100644
--- a/spec/features/milestone_spec.rb
+++ b/spec/features/milestone_spec.rb
@@ -1,18 +1,18 @@
require 'rails_helper'
-feature 'Milestone' do
+describe 'Milestone' do
let(:group) { create(:group, :public) }
let(:project) { create(:project, :public, namespace: group) }
let(:user) { create(:user) }
before do
create(:group_member, group: group, user: user)
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
- feature 'Create a milestone' do
- scenario 'shows an informative message for a new milestone' do
+ describe 'Create a milestone' do
+ it 'shows an informative message for a new milestone' do
visit new_project_milestone_path(project)
page.within '.milestone-form' do
@@ -28,8 +28,8 @@ feature 'Milestone' do
end
end
- feature 'Open a milestone with closed issues' do
- scenario 'shows an informative message' do
+ describe 'Open a milestone with closed issues' do
+ it 'shows an informative message' do
milestone = create(:milestone, project: project, title: 8.7)
create(:issue, title: "Bugfix1", project: project, milestone: milestone, state: "closed")
@@ -39,8 +39,8 @@ feature 'Milestone' do
end
end
- feature 'Open a project milestone with an existing title' do
- scenario 'displays validation message when there is a project milestone with same title' do
+ describe 'Open a project milestone with an existing title' do
+ it 'displays validation message when there is a project milestone with same title' do
milestone = create(:milestone, project: project, title: 8.7)
visit new_project_milestone_path(project)
@@ -52,7 +52,7 @@ feature 'Milestone' do
expect(find('.alert-danger')).to have_content('already being used for another group or project milestone.')
end
- scenario 'displays validation message when there is a group milestone with same title' do
+ it 'displays validation message when there is a group milestone with same title' do
milestone = create(:milestone, project_id: nil, group: project.group, title: 8.7)
visit new_group_milestone_path(project.group)
@@ -66,8 +66,8 @@ feature 'Milestone' do
end
end
- feature 'Open a milestone', :js do
- scenario 'shows total issue time spent correctly when no time has been logged' do
+ describe 'Open a milestone', :js do
+ it '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)
@@ -79,7 +79,7 @@ feature 'Milestone' do
end
end
- scenario 'shows total issue time spent' do
+ it '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)
@@ -98,8 +98,8 @@ feature 'Milestone' do
end
end
- feature 'Deleting a milestone' do
- scenario "The delete milestone button does not show for unauthorized users" do
+ describe 'Deleting a milestone' do
+ it "The delete milestone button does not show for unauthorized users" do
create(:milestone, project: project, title: 8.7)
sign_out(user)
@@ -109,7 +109,7 @@ feature 'Milestone' do
end
end
- feature 'deprecation popover', :js do
+ describe 'deprecation popover', :js do
it 'opens deprecation popover' do
milestone = create(:milestone, project: project)
@@ -119,7 +119,7 @@ feature 'Milestone' do
find('.milestone-deprecation-message .js-popover-link').click
- expect(page).to have_selector('.milestone-deprecation-message .popover')
+ expect(page).to have_selector('.popover')
end
end
end
diff --git a/spec/features/milestones/user_deletes_milestone_spec.rb b/spec/features/milestones/user_deletes_milestone_spec.rb
index 414702daba4..9d4a68239d3 100644
--- a/spec/features/milestones/user_deletes_milestone_spec.rb
+++ b/spec/features/milestones/user_deletes_milestone_spec.rb
@@ -13,6 +13,7 @@ describe "User deletes milestone", :js do
end
it "deletes milestone" do
+ click_link(milestone.title)
click_button("Delete")
click_button("Delete milestone")
diff --git a/spec/features/milestones/user_edits_milestone_spec.rb b/spec/features/milestones/user_edits_milestone_spec.rb
new file mode 100644
index 00000000000..077295f1cc0
--- /dev/null
+++ b/spec/features/milestones/user_edits_milestone_spec.rb
@@ -0,0 +1,22 @@
+require "rails_helper"
+
+describe "User edits milestone", :js do
+ set(:user) { create(:user) }
+ set(:project) { create(:project) }
+ set(:milestone) { create(:milestone, project: project, start_date: Date.today, due_date: 5.days.from_now) }
+
+ before do
+ project.add_developer(user)
+ sign_in(user)
+
+ visit(edit_project_milestone_path(project, milestone))
+ end
+
+ it "shows the right start date and due date" do
+ start_date = milestone.start_date.strftime("%F")
+ due_date = milestone.due_date.strftime("%F")
+
+ expect(page).to have_field(with: start_date)
+ expect(page).to have_field(with: due_date)
+ end
+end
diff --git a/spec/features/oauth_login_spec.rb b/spec/features/oauth_login_spec.rb
index 013cdaa6479..f4105730402 100644
--- a/spec/features/oauth_login_spec.rb
+++ b/spec/features/oauth_login_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'OAuth Login', :js, :allow_forgery_protection do
+describe 'OAuth Login', :js, :allow_forgery_protection do
include DeviseHelpers
def enter_code(code)
diff --git a/spec/features/participants_autocomplete_spec.rb b/spec/features/participants_autocomplete_spec.rb
index 96f6df587e1..134731a4639 100644
--- a/spec/features/participants_autocomplete_spec.rb
+++ b/spec/features/participants_autocomplete_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Member autocomplete', :js do
+describe 'Member autocomplete', :js do
let(:project) { create(:project, :public) }
let(:user) { create(:user) }
let(:author) { create(:user) }
@@ -14,10 +14,10 @@ feature 'Member autocomplete', :js do
shared_examples "open suggestions when typing @" do |resource_name|
before do
page.within('.new-note') do
- if resource_name == 'issue'
- find('#note-body').send_keys('@')
- else
+ if resource_name == 'commit'
find('#note_note').send_keys('@')
+ else
+ find('#note-body').send_keys('@')
end
end
end
diff --git a/spec/features/password_reset_spec.rb b/spec/features/password_reset_spec.rb
index 73a526c3d8a..dcc63dff9f5 100644
--- a/spec/features/password_reset_spec.rb
+++ b/spec/features/password_reset_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Password reset' do
+describe 'Password reset' do
describe 'throttling' do
it 'sends reset instructions when not previously sent' do
user = create(:user)
diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb
index 15dcb30cbdd..2e0753c3bfb 100644
--- a/spec/features/profile_spec.rb
+++ b/spec/features/profile_spec.rb
@@ -56,21 +56,21 @@ describe 'Profile account page', :js do
end
end
- describe 'when I reset RSS token' do
+ describe 'when I reset feed token' do
before do
visit profile_personal_access_tokens_path
end
- it 'resets RSS token' do
- within('.rss-token-reset') do
- previous_token = find("#rss_token").value
+ it 'resets feed token' do
+ within('.feed-token-reset') do
+ previous_token = find("#feed_token").value
accept_confirm { click_link('reset it') }
- expect(find('#rss_token').value).not_to eq(previous_token)
+ expect(find('#feed_token').value).not_to eq(previous_token)
end
- expect(page).to have_content 'RSS token was successfully reset'
+ expect(page).to have_content 'Feed token was successfully reset'
end
end
diff --git a/spec/features/profiles/account_spec.rb b/spec/features/profiles/account_spec.rb
index 215b658eb7b..90d0e9bb77c 100644
--- a/spec/features/profiles/account_spec.rb
+++ b/spec/features/profiles/account_spec.rb
@@ -1,25 +1,25 @@
require 'rails_helper'
-feature 'Profile > Account', :js do
- given(:user) { create(:user, username: 'foo') }
+describe 'Profile > Account', :js do
+ let(:user) { create(:user, username: 'foo') }
before do
sign_in(user)
end
describe 'Change username' do
- given(:new_username) { 'bar' }
- given(:new_user_path) { "/#{new_username}" }
- given(:old_user_path) { "/#{user.username}" }
+ let(:new_username) { 'bar' }
+ let(:new_user_path) { "/#{new_username}" }
+ let(:old_user_path) { "/#{user.username}" }
- scenario 'the user is accessible via the new path' do
+ it 'the user is accessible via the new path' do
update_username(new_username)
visit new_user_path
expect(current_path).to eq(new_user_path)
expect(find('.user-info')).to have_content(new_username)
end
- scenario 'the old user path redirects to the new path' do
+ it 'the old user path redirects to the new path' do
update_username(new_username)
visit old_user_path
expect(current_path).to eq(new_user_path)
@@ -27,9 +27,9 @@ feature 'Profile > Account', :js do
end
context 'with a project' do
- given!(:project) { create(:project, namespace: user.namespace) }
- given(:new_project_path) { "/#{new_username}/#{project.path}" }
- given(:old_project_path) { "/#{user.username}/#{project.path}" }
+ let!(:project) { create(:project, namespace: user.namespace) }
+ let(:new_project_path) { "/#{new_username}/#{project.path}" }
+ let(:old_project_path) { "/#{user.username}/#{project.path}" }
before(:context) do
TestEnv.clean_test_path
@@ -39,14 +39,14 @@ feature 'Profile > Account', :js do
TestEnv.clean_test_path
end
- scenario 'the project is accessible via the new path' do
+ it 'the project is accessible via the new path' do
update_username(new_username)
visit new_project_path
expect(current_path).to eq(new_project_path)
expect(find('.breadcrumbs-sub-title')).to have_content('Details')
end
- scenario 'the old project path redirects to the new path' do
+ it 'the old project path redirects to the new path' do
update_username(new_username)
visit old_project_path
expect(current_path).to eq(new_project_path)
@@ -67,4 +67,6 @@ def update_username(new_username)
page.within('.modal') do
find('.js-modal-primary-action').click
end
+
+ wait_for_requests
end
diff --git a/spec/features/profiles/active_sessions_spec.rb b/spec/features/profiles/active_sessions_spec.rb
index 4045cfd21c4..d3050760c06 100644
--- a/spec/features/profiles/active_sessions_spec.rb
+++ b/spec/features/profiles/active_sessions_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Profile > Active Sessions', :clean_gitlab_redis_shared_state do
+describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state do
let(:user) do
create(:user).tap do |user|
user.current_sign_in_at = Time.current
@@ -13,7 +13,7 @@ feature 'Profile > Active Sessions', :clean_gitlab_redis_shared_state do
end
end
- scenario 'User sees their active sessions' do
+ it 'User sees their active sessions' do
Capybara::Session.new(:session1)
Capybara::Session.new(:session2)
@@ -60,7 +60,7 @@ feature 'Profile > Active Sessions', :clean_gitlab_redis_shared_state do
end
end
- scenario 'User can revoke a session', :js, :redis_session_store do
+ it 'User can revoke a session', :js, :redis_session_store do
Capybara::Session.new(:session1)
Capybara::Session.new(:session2)
diff --git a/spec/features/profiles/chat_names_spec.rb b/spec/features/profiles/chat_names_spec.rb
index 5c959acbbc9..c72069f6262 100644
--- a/spec/features/profiles/chat_names_spec.rb
+++ b/spec/features/profiles/chat_names_spec.rb
@@ -1,19 +1,19 @@
require 'rails_helper'
-feature 'Profile > Chat' do
- given(:user) { create(:user) }
- given(:service) { create(:service) }
+describe 'Profile > Chat' do
+ let(:user) { create(:user) }
+ let(:service) { create(:service) }
before do
sign_in(user)
end
describe 'uses authorization link' do
- given(:params) do
+ let(:params) do
{ team_id: 'T00', team_domain: 'my_chat_team', user_id: 'U01', user_name: 'my_chat_user' }
end
- given!(:authorize_url) { ChatNames::AuthorizeUserService.new(service, params).execute }
- given(:authorize_path) { URI.parse(authorize_url).request_uri }
+ let!(:authorize_url) { ChatNames::AuthorizeUserService.new(service, params).execute }
+ let(:authorize_path) { URI.parse(authorize_url).request_uri }
before do
visit authorize_path
@@ -24,13 +24,13 @@ feature 'Profile > Chat' do
click_button 'Authorize'
end
- scenario 'goes to list of chat names and see chat account' do
+ it 'goes to list of chat names and see chat account' do
expect(page.current_path).to eq(profile_chat_names_path)
expect(page).to have_content('my_chat_team')
expect(page).to have_content('my_chat_user')
end
- scenario 'second use of link is denied' do
+ it 'second use of link is denied' do
visit authorize_path
expect(page).to have_gitlab_http_status(:not_found)
@@ -42,13 +42,13 @@ feature 'Profile > Chat' do
click_button 'Deny'
end
- scenario 'goes to list of chat names and do not see chat account' do
+ it 'goes to list of chat names and do not see chat account' do
expect(page.current_path).to eq(profile_chat_names_path)
expect(page).not_to have_content('my_chat_team')
expect(page).not_to have_content('my_chat_user')
end
- scenario 'second use of link is denied' do
+ it 'second use of link is denied' do
visit authorize_path
expect(page).to have_gitlab_http_status(:not_found)
@@ -57,18 +57,18 @@ feature 'Profile > Chat' do
end
describe 'visits chat accounts' do
- given!(:chat_name) { create(:chat_name, user: user, service: service) }
+ let!(:chat_name) { create(:chat_name, user: user, service: service) }
before do
visit profile_chat_names_path
end
- scenario 'sees chat user' do
+ it 'sees chat user' do
expect(page).to have_content(chat_name.team_domain)
expect(page).to have_content(chat_name.chat_name)
end
- scenario 'removes chat account' do
+ it 'removes chat account' do
click_link 'Remove'
expect(page).to have_content("You don't have any active chat names.")
diff --git a/spec/features/profiles/emails_spec.rb b/spec/features/profiles/emails_spec.rb
index 11cc8aae6f3..bc6d54b5ed7 100644
--- a/spec/features/profiles/emails_spec.rb
+++ b/spec/features/profiles/emails_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Profile > Emails' do
+describe 'Profile > Emails' do
let(:user) { create(:user) }
before do
@@ -12,7 +12,7 @@ feature 'Profile > Emails' do
visit profile_emails_path
end
- scenario 'saves the new email' do
+ it 'saves the new email' do
fill_in('Email', with: 'my@email.com')
click_button('Add email address')
@@ -21,7 +21,7 @@ feature 'Profile > Emails' do
expect(page).to have_content('Resend confirmation email')
end
- scenario 'does not add a duplicate email' do
+ it 'does not add a duplicate email' do
fill_in('Email', with: user.email)
click_button('Add email address')
@@ -31,7 +31,7 @@ feature 'Profile > Emails' do
end
end
- scenario 'User removes email' do
+ it 'User removes email' do
user.emails.create(email: 'my@email.com')
visit profile_emails_path
expect(page).to have_content("my@email.com")
@@ -40,7 +40,7 @@ feature 'Profile > Emails' do
expect(page).not_to have_content("my@email.com")
end
- scenario 'User confirms email' do
+ it 'User confirms email' do
email = user.emails.create(email: 'my@email.com')
visit profile_emails_path
expect(page).to have_content("#{email.email} Unverified")
@@ -52,7 +52,7 @@ feature 'Profile > Emails' do
expect(page).to have_content("#{email.email} Verified")
end
- scenario 'User re-sends confirmation email' do
+ it 'User re-sends confirmation email' do
email = user.emails.create(email: 'my@email.com')
visit profile_emails_path
@@ -60,7 +60,7 @@ feature 'Profile > Emails' do
expect(page).to have_content("Confirmation email sent to #{email.email}")
end
- scenario 'old unconfirmed emails show Send Confirmation button' do
+ it 'old unconfirmed emails show Send Confirmation button' do
email = user.emails.create(email: 'my@email.com')
email.update_attribute(:confirmation_sent_at, nil)
visit profile_emails_path
diff --git a/spec/features/profiles/gpg_keys_spec.rb b/spec/features/profiles/gpg_keys_spec.rb
index 59233e92f93..ec3ec795b63 100644
--- a/spec/features/profiles/gpg_keys_spec.rb
+++ b/spec/features/profiles/gpg_keys_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Profile > GPG Keys' do
+describe 'Profile > GPG Keys' do
let(:user) { create(:user, email: GpgHelpers::User2.emails.first) }
before do
@@ -12,7 +12,7 @@ feature 'Profile > GPG Keys' do
visit profile_gpg_keys_path
end
- scenario 'saves the new key' do
+ it 'saves the new key' do
fill_in('Key', with: GpgHelpers::User2.public_key)
click_button('Add key')
@@ -21,7 +21,7 @@ feature 'Profile > GPG Keys' do
expect(page).to have_content(GpgHelpers::User2.fingerprint)
end
- scenario 'with multiple subkeys' do
+ it 'with multiple subkeys' do
fill_in('Key', with: GpgHelpers::User3.public_key)
click_button('Add key')
@@ -34,7 +34,7 @@ feature 'Profile > GPG Keys' do
end
end
- scenario 'User sees their key' do
+ it 'User sees their key' do
create(:gpg_key, user: user, key: GpgHelpers::User2.public_key)
visit profile_gpg_keys_path
@@ -43,7 +43,7 @@ feature 'Profile > GPG Keys' do
expect(page).to have_content(GpgHelpers::User2.fingerprint)
end
- scenario 'User removes a key via the key index' do
+ it 'User removes a key via the key index' do
create(:gpg_key, user: user, key: GpgHelpers::User2.public_key)
visit profile_gpg_keys_path
@@ -52,7 +52,7 @@ feature 'Profile > GPG Keys' do
expect(page).to have_content('Your GPG keys (0)')
end
- scenario 'User revokes a key via the key index' do
+ it 'User revokes a key via the key index' do
gpg_key = create :gpg_key, user: user, key: GpgHelpers::User2.public_key
gpg_signature = create :gpg_signature, gpg_key: gpg_key, verification_status: :verified
diff --git a/spec/features/profiles/keys_spec.rb b/spec/features/profiles/keys_spec.rb
index b04a5422fed..e6586fc8a0a 100644
--- a/spec/features/profiles/keys_spec.rb
+++ b/spec/features/profiles/keys_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Profile > SSH Keys' do
+describe 'Profile > SSH Keys' do
let(:user) { create(:user) }
before do
@@ -12,13 +12,13 @@ feature 'Profile > SSH Keys' do
visit profile_keys_path
end
- scenario 'auto-populates the title', :js do
+ it '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")
end
- scenario 'saves the new key' do
+ it 'saves the new key' do
attrs = attributes_for(:key)
fill_in('Key', with: attrs[:key])
@@ -30,13 +30,27 @@ feature 'Profile > SSH Keys' do
expect(find('.breadcrumbs-sub-title')).to have_link(attrs[:title])
end
+ it 'shows a confirmable warning if the key does not start with ssh-' do
+ attrs = attributes_for(:key)
+
+ fill_in('Key', with: 'invalid-key')
+ fill_in('Title', with: attrs[:title])
+ click_button('Add key')
+
+ expect(page).to have_selector('.js-add-ssh-key-validation-warning')
+
+ find('.js-add-ssh-key-validation-confirm-submit').click
+
+ expect(page).to have_content('Key is invalid')
+ end
+
context 'when only DSA and ECDSA keys are allowed' do
before do
forbidden = ApplicationSetting::FORBIDDEN_KEY_VALUE
stub_application_setting(rsa_key_restriction: forbidden, ed25519_key_restriction: forbidden)
end
- scenario 'shows a validation error' do
+ it 'shows a validation error' do
attrs = attributes_for(:key)
fill_in('Key', with: attrs[:key])
@@ -48,14 +62,14 @@ feature 'Profile > SSH Keys' do
end
end
- scenario 'User sees their keys' do
+ it 'User sees their keys' do
key = create(:key, user: user)
visit profile_keys_path
expect(page).to have_content(key.title)
end
- scenario 'User removes a key via the key index' do
+ it 'User removes a key via the key index' do
create(:key, user: user)
visit profile_keys_path
@@ -64,7 +78,7 @@ feature 'Profile > SSH Keys' do
expect(page).to have_content('Your SSH keys (0)')
end
- scenario 'User removes a key via its details page' do
+ it 'User removes a key via its details page' do
key = create(:key, user: user)
visit profile_key_path(key)
diff --git a/spec/features/profiles/password_spec.rb b/spec/features/profiles/password_spec.rb
index f9c6ff90ca1..5e3569e4beb 100644
--- a/spec/features/profiles/password_spec.rb
+++ b/spec/features/profiles/password_spec.rb
@@ -117,7 +117,7 @@ describe 'Profile > Password' do
before do
sign_in(user)
- user.update_attributes(password_expires_at: 1.hour.ago)
+ user.update(password_expires_at: 1.hour.ago)
user.identities.delete
expect(user.ldap_user?).to eq false
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 d5fe5bdffc5..f618bc330ea 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,13 +1,13 @@
require 'spec_helper'
-feature 'Profile > Notifications > User changes notified_of_own_activity setting', :js do
+describe 'Profile > Notifications > User changes notified_of_own_activity setting', :js do
let(:user) { create(:user) }
before do
sign_in(user)
end
- scenario 'User opts into receiving notifications about their own activity' do
+ it 'User opts into receiving notifications about their own activity' do
visit profile_notifications_path
expect(page).not_to have_checked_field('user[notified_of_own_activity]')
@@ -18,7 +18,7 @@ feature 'Profile > Notifications > User changes notified_of_own_activity setting
expect(page).to have_checked_field('user[notified_of_own_activity]')
end
- scenario 'User opts out of receiving notifications about their own activity' do
+ it 'User opts out of receiving notifications about their own activity' do
user.update!(notified_of_own_activity: true)
visit profile_notifications_path
diff --git a/spec/features/profiles/user_visits_notifications_tab_spec.rb b/spec/features/profiles/user_visits_notifications_tab_spec.rb
index 95953fbcfac..db797bb586f 100644
--- a/spec/features/profiles/user_visits_notifications_tab_spec.rb
+++ b/spec/features/profiles/user_visits_notifications_tab_spec.rb
@@ -1,11 +1,11 @@
require 'spec_helper'
-feature 'User visits the notifications tab', :js do
+describe 'User visits the notifications tab', :js do
let(:project) { create(:project) }
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit(profile_notifications_path)
end
diff --git a/spec/features/profiles/user_visits_profile_spec.rb b/spec/features/profiles/user_visits_profile_spec.rb
index 713112477c8..2dc4547b2d8 100644
--- a/spec/features/profiles/user_visits_profile_spec.rb
+++ b/spec/features/profiles/user_visits_profile_spec.rb
@@ -29,7 +29,7 @@ describe 'User visits their profile' do
let!(:project) do
create(:project, :repository, namespace: group) do |project|
create(:closed_issue_event, project: project)
- project.add_master(user)
+ project.add_maintainer(user)
end
end
diff --git a/spec/features/project_variables_spec.rb b/spec/features/project_variables_spec.rb
index 0ba2224359a..a93df3696d2 100644
--- a/spec/features/project_variables_spec.rb
+++ b/spec/features/project_variables_spec.rb
@@ -8,7 +8,7 @@ describe 'Project variables', :js do
before do
sign_in(user)
- project.add_master(user)
+ project.add_maintainer(user)
project.variables << variable
visit page_path
diff --git a/spec/features/projects/activity/rss_spec.rb b/spec/features/projects/activity/rss_spec.rb
index cd1cfe07998..411134e7b8e 100644
--- a/spec/features/projects/activity/rss_spec.rb
+++ b/spec/features/projects/activity/rss_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Project Activity RSS' do
+describe 'Project Activity RSS' do
let(:project) { create(:project, :public) }
let(:user) { project.owner }
let(:path) { activity_project_path(project) }
@@ -15,7 +15,7 @@ feature 'Project Activity RSS' do
visit path
end
- it_behaves_like "it has an RSS button with current_user's RSS token"
+ it_behaves_like "it has an RSS button with current_user's feed token"
end
context 'when signed out' do
@@ -23,6 +23,6 @@ feature 'Project Activity RSS' do
visit path
end
- it_behaves_like "it has an RSS button without an RSS token"
+ it_behaves_like "it has an RSS button without a feed token"
end
end
diff --git a/spec/features/projects/activity/user_sees_activity_spec.rb b/spec/features/projects/activity/user_sees_activity_spec.rb
index 644a837dc14..e0248911b5f 100644
--- a/spec/features/projects/activity/user_sees_activity_spec.rb
+++ b/spec/features/projects/activity/user_sees_activity_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Projects > Activity > User sees activity' do
+describe 'Projects > Activity > User sees activity' do
let(:project) { create(:project, :repository, :public) }
let(:user) { project.creator }
diff --git a/spec/features/projects/actve_tabs_spec.rb b/spec/features/projects/actve_tabs_spec.rb
index ce5606b63ae..7c6110c533b 100644
--- a/spec/features/projects/actve_tabs_spec.rb
+++ b/spec/features/projects/actve_tabs_spec.rb
@@ -5,7 +5,7 @@ describe 'Project active tab' do
let(:project) { create(:project, :repository) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/features/projects/artifacts/file_spec.rb b/spec/features/projects/artifacts/file_spec.rb
index df1d17bdcb7..993d0040434 100644
--- a/spec/features/projects/artifacts/file_spec.rb
+++ b/spec/features/projects/artifacts/file_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Artifact file', :js do
+describe 'Artifact file', :js do
let(:project) { create(:project, :public) }
let(:pipeline) { create(:ci_empty_pipeline, project: project) }
let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
diff --git a/spec/features/projects/artifacts/raw_spec.rb b/spec/features/projects/artifacts/raw_spec.rb
index 0bec6e9ad31..d8ee9adda6b 100644
--- a/spec/features/projects/artifacts/raw_spec.rb
+++ b/spec/features/projects/artifacts/raw_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Raw artifact', :js do
+describe 'Raw artifact', :js do
let(:project) { create(:project, :public) }
let(:pipeline) { create(:ci_empty_pipeline, project: project) }
let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) }
diff --git a/spec/features/projects/awards/user_interacts_with_awards_in_issue_spec.rb b/spec/features/projects/awards/user_interacts_with_awards_in_issue_spec.rb
index 12e07647ecd..4d860893abe 100644
--- a/spec/features/projects/awards/user_interacts_with_awards_in_issue_spec.rb
+++ b/spec/features/projects/awards/user_interacts_with_awards_in_issue_spec.rb
@@ -6,7 +6,7 @@ describe 'User interacts with awards in an issue', :js do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit(project_issue_path(project, issue))
diff --git a/spec/features/projects/badges/coverage_spec.rb b/spec/features/projects/badges/coverage_spec.rb
index f51001edcd7..8522ea747fa 100644
--- a/spec/features/projects/badges/coverage_spec.rb
+++ b/spec/features/projects/badges/coverage_spec.rb
@@ -1,16 +1,16 @@
require 'spec_helper'
-feature 'test coverage badge' do
- given!(:user) { create(:user) }
- given!(:project) { create(:project, :private) }
+describe 'test coverage badge' do
+ let!(:user) { create(:user) }
+ let!(:project) { create(:project, :private) }
context 'when user has access to view badge' do
- background do
+ before do
project.add_developer(user)
sign_in(user)
end
- scenario 'user requests coverage badge image for pipeline' do
+ it 'user requests coverage badge image for pipeline' do
create_pipeline do |pipeline|
create_build(pipeline, coverage: 100, name: 'test:1')
create_build(pipeline, coverage: 90, name: 'test:2')
@@ -21,7 +21,7 @@ feature 'test coverage badge' do
expect_coverage_badge('95.00%')
end
- scenario 'user requests coverage badge for specific job' do
+ it 'user requests coverage badge for specific job' do
create_pipeline do |pipeline|
create_build(pipeline, coverage: 50, name: 'test:1')
create_build(pipeline, coverage: 50, name: 'test:2')
@@ -33,7 +33,7 @@ feature 'test coverage badge' do
expect_coverage_badge('85.00%')
end
- scenario 'user requests coverage badge for pipeline without coverage' do
+ it 'user requests coverage badge for pipeline without coverage' do
create_pipeline do |pipeline|
create_build(pipeline, coverage: nil, name: 'test')
end
@@ -45,9 +45,11 @@ feature 'test coverage badge' do
end
context 'when user does not have access to view badge' do
- background { sign_in(user) }
+ before do
+ sign_in(user)
+ end
- scenario 'user requests test coverage badge image' do
+ it 'user requests test coverage badge image' do
show_test_coverage_badge
expect(page).to have_gitlab_http_status(404)
diff --git a/spec/features/projects/badges/list_spec.rb b/spec/features/projects/badges/list_spec.rb
index 0abef4bc447..e30b908c60d 100644
--- a/spec/features/projects/badges/list_spec.rb
+++ b/spec/features/projects/badges/list_spec.rb
@@ -1,15 +1,15 @@
require 'spec_helper'
-feature 'list of badges' do
- background do
+describe 'list of badges' do
+ before do
user = create(:user)
project = create(:project, :repository)
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit project_settings_ci_cd_path(project)
end
- scenario 'user wants to see build status badge' do
+ it 'user wants to see build status badge' do
page.within('.pipeline-status') do
expect(page).to have_content 'pipeline status'
expect(page).to have_content 'Markdown'
@@ -24,7 +24,7 @@ feature 'list of badges' do
end
end
- scenario 'user wants to see coverage report badge' do
+ it 'user wants to see coverage report badge' do
page.within('.coverage-report') do
expect(page).to have_content 'coverage report'
expect(page).to have_content 'Markdown'
@@ -39,7 +39,7 @@ feature 'list of badges' do
end
end
- scenario 'user changes current ref of build status badge', :js do
+ it '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/badges/pipeline_badge_spec.rb b/spec/features/projects/badges/pipeline_badge_spec.rb
index b83ea8f4eaa..8c4488b2ca6 100644
--- a/spec/features/projects/badges/pipeline_badge_spec.rb
+++ b/spec/features/projects/badges/pipeline_badge_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Pipeline Badge' do
+describe 'Pipeline Badge' do
set(:project) { create(:project, :repository, :public) }
let(:ref) { project.default_branch }
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 c12e56d2c3f..96f514f4f04 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 do
+describe '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 e7b305925f7..27589428896 100644
--- a/spec/features/projects/blobs/blob_show_spec.rb
+++ b/spec/features/projects/blobs/blob_show_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'File blob', :js do
+describe 'File blob', :js do
include MobileHelpers
let(:project) { create(:project, :public, :repository) }
@@ -144,7 +144,7 @@ feature 'File blob', :js do
context 'Markdown file (stored in LFS)' do
before do
- project.add_master(project.creator)
+ project.add_maintainer(project.creator)
Files::CreateService.new(
project,
@@ -237,7 +237,7 @@ feature 'File blob', :js do
context 'PDF file' do
before do
- project.add_master(project.creator)
+ project.add_maintainer(project.creator)
Files::CreateService.new(
project,
@@ -350,7 +350,7 @@ feature 'File blob', :js do
context 'empty file' do
before do
- project.add_master(project.creator)
+ project.add_maintainer(project.creator)
Files::CreateService.new(
project,
@@ -418,7 +418,7 @@ feature 'File blob', :js do
context '.gitlab-ci.yml' do
before do
- project.add_master(project.creator)
+ project.add_maintainer(project.creator)
Files::CreateService.new(
project,
@@ -446,7 +446,7 @@ feature 'File blob', :js do
context '.gitlab/route-map.yml' do
before do
- project.add_master(project.creator)
+ project.add_maintainer(project.creator)
Files::CreateService.new(
project,
@@ -494,7 +494,7 @@ feature 'File blob', :js do
context '*.gemspec' do
before do
- project.add_master(project.creator)
+ project.add_maintainer(project.creator)
Files::CreateService.new(
project,
diff --git a/spec/features/projects/blobs/edit_spec.rb b/spec/features/projects/blobs/edit_spec.rb
index 89d3bd24b89..0e036b4ea68 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 do
+describe 'Editing file blob', :js do
include TreeHelper
let(:project) { create(:project, :public, :repository) }
@@ -134,11 +134,11 @@ feature 'Editing file blob', :js do
end
end
- context 'as master' do
+ context 'as maintainer' do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit project_edit_blob_path(project, tree_join(branch, file_path))
end
diff --git a/spec/features/projects/blobs/shortcuts_blob_spec.rb b/spec/features/projects/blobs/shortcuts_blob_spec.rb
index 9f1fef80ab5..aeed38aeb76 100644
--- a/spec/features/projects/blobs/shortcuts_blob_spec.rb
+++ b/spec/features/projects/blobs/shortcuts_blob_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Blob shortcuts' do
+describe 'Blob shortcuts' do
include TreeHelper
let(:project) { create(:project, :public, :repository) }
let(:path) { project.repository.ls_files(project.repository.root_ref)[0] }
diff --git a/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb b/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb
index b7d063596c1..8a0b92190dd 100644
--- a/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb
+++ b/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'User creates blob in new project', :js do
+describe 'User creates blob in new project', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :empty_repo) }
@@ -24,9 +24,9 @@ feature 'User creates blob in new project', :js do
end
end
- describe 'as a master' do
+ describe 'as a maintainer' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
it_behaves_like 'creating a file'
diff --git a/spec/features/projects/branches/download_buttons_spec.rb b/spec/features/projects/branches/download_buttons_spec.rb
index 605298ba8ab..c8dc72a34ec 100644
--- a/spec/features/projects/branches/download_buttons_spec.rb
+++ b/spec/features/projects/branches/download_buttons_spec.rb
@@ -1,12 +1,12 @@
require 'spec_helper'
-feature 'Download buttons in branches page' do
- given(:user) { create(:user) }
- given(:role) { :developer }
- given(:status) { 'success' }
- given(:project) { create(:project, :repository) }
+describe 'Download buttons in branches page' do
+ let(:user) { create(:user) }
+ let(:role) { :developer }
+ let(:status) { 'success' }
+ let(:project) { create(:project, :repository) }
- given(:pipeline) do
+ let(:pipeline) do
create(:ci_pipeline,
project: project,
sha: project.commit('binary-encoding').sha,
@@ -14,14 +14,14 @@ feature 'Download buttons in branches page' do
status: status)
end
- given!(:build) do
+ let!(:build) do
create(:ci_build, :success, :artifacts,
pipeline: pipeline,
status: pipeline.status,
name: 'build')
end
- background do
+ before do
sign_in(user)
project.add_role(user, role)
end
@@ -32,7 +32,7 @@ feature 'Download buttons in branches page' do
visit project_branches_filtered_path(project, state: 'all', search: 'binary-encoding')
end
- scenario 'shows download artifacts button' do
+ it 'shows download artifacts button' do
href = latest_succeeded_project_artifacts_path(project, 'binary-encoding/download', job: 'build')
expect(page).to have_link "Download '#{build.name}'", href: href
diff --git a/spec/features/projects/branches/new_branch_ref_dropdown_spec.rb b/spec/features/projects/branches/new_branch_ref_dropdown_spec.rb
index 0be434a567b..0faf73db7da 100644
--- a/spec/features/projects/branches/new_branch_ref_dropdown_spec.rb
+++ b/spec/features/projects/branches/new_branch_ref_dropdown_spec.rb
@@ -6,7 +6,7 @@ describe 'New Branch Ref Dropdown', :js do
let(:toggle) { find('.create-from .dropdown-menu-toggle') }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit new_project_branch_path(project)
diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb
index b7ce1b9993a..97757e8da92 100644
--- a/spec/features/projects/branches_spec.rb
+++ b/spec/features/projects/branches_spec.rb
@@ -182,10 +182,10 @@ describe 'Branches' do
end
end
- context 'logged in as master' do
+ context 'logged in as maintainer' do
before do
sign_in(user)
- project.add_master(user)
+ project.add_maintainer(user)
end
describe 'Initial branches page' do
diff --git a/spec/features/projects/clusters/applications_spec.rb b/spec/features/projects/clusters/applications_spec.rb
index 7b2c57aa652..a65ca662350 100644
--- a/spec/features/projects/clusters/applications_spec.rb
+++ b/spec/features/projects/clusters/applications_spec.rb
@@ -1,13 +1,13 @@
require 'spec_helper'
-feature 'Clusters Applications', :js do
+describe 'Clusters Applications', :js do
include GoogleApi::CloudPlatformHelpers
let(:project) { create(:project) }
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
@@ -19,7 +19,7 @@ feature 'Clusters Applications', :js do
context 'when cluster is being created' do
let(:cluster) { create(:cluster, :providing_by_gcp, projects: [project])}
- scenario 'user is unable to install applications' do
+ it 'user is unable to install applications' do
page.within('.js-cluster-application-row-helm') do
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Install')
@@ -30,7 +30,7 @@ feature 'Clusters Applications', :js do
context 'when cluster is created' do
let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project])}
- scenario 'user can install applications' do
+ it 'user can install applications' do
page.within('.js-cluster-application-row-helm') do
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to be_nil
expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Install')
diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb
index a8a627d8806..31e3ebf675d 100644
--- a/spec/features/projects/clusters/gcp_spec.rb
+++ b/spec/features/projects/clusters/gcp_spec.rb
@@ -1,13 +1,13 @@
require 'spec_helper'
-feature 'Gcp Cluster', :js do
+describe 'Gcp Cluster', :js do
include GoogleApi::CloudPlatformHelpers
let(:project) { create(:project) }
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
gitlab_sign_in(user)
allow(Projects::ClustersController).to receive(:STATUS_POLLING_INTERVAL) { 100 }
end
@@ -16,158 +16,129 @@ feature 'Gcp Cluster', :js do
let(:project_id) { 'test-project-1234' }
before do
- allow_any_instance_of(Projects::Clusters::GcpController)
+ allow_any_instance_of(Projects::ClustersController)
.to receive(:token_in_session).and_return('token')
- allow_any_instance_of(Projects::Clusters::GcpController)
+ 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 has a GCP project with billing enabled' do
+ context 'when user does not have a cluster and visits cluster index page' do
before do
- allow_any_instance_of(Projects::Clusters::GcpController).to receive(:authorize_google_project_billing)
- allow_any_instance_of(Projects::Clusters::GcpController).to receive(:google_project_billing_status).and_return(true)
- end
-
- context 'when user does not have a cluster and visits cluster index page' do
- before do
- visit project_clusters_path(project)
-
- click_link 'Add Kubernetes cluster'
- click_link 'Create on Google Kubernetes Engine'
- end
-
- context 'when user filled form with valid parameters' do
- before do
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:projects_zones_clusters_create) do
- OpenStruct.new(
- self_link: 'projects/gcp-project-12345/zones/us-central1-a/operations/ope-123',
- status: 'RUNNING'
- )
- end
-
- allow(WaitForClusterCreationWorker).to receive(:perform_in).and_return(nil)
-
- fill_in 'cluster_provider_gcp_attributes_gcp_project_id', with: 'gcp-project-123'
- fill_in 'cluster_name', with: 'dev-cluster'
- click_button 'Create Kubernetes cluster'
- end
+ visit project_clusters_path(project)
- it 'user sees a cluster details page and creation status' do
- expect(page).to have_content('Kubernetes cluster is being created on Google Kubernetes Engine...')
+ click_link 'Add Kubernetes cluster'
+ click_link 'Create new Cluster on GKE'
+ end
- Clusters::Cluster.last.provider.make_created!
+ context 'when user filled form with valid parameters' do
+ subject { click_button 'Create Kubernetes cluster' }
- expect(page).to have_content('Kubernetes cluster was successfully created on Google Kubernetes Engine')
+ before do
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:projects_zones_clusters_create) do
+ OpenStruct.new(
+ self_link: 'projects/gcp-project-12345/zones/us-central1-a/operations/ope-123',
+ status: 'RUNNING'
+ )
end
- it 'user sees a error if something worng during creation' do
- expect(page).to have_content('Kubernetes cluster is being created on Google Kubernetes Engine...')
+ allow(WaitForClusterCreationWorker).to receive(:perform_in).and_return(nil)
- Clusters::Cluster.last.provider.make_errored!('Something wrong!')
+ execute_script('document.querySelector(".js-gke-cluster-creation-submit").removeAttribute("disabled")')
+ sleep 2 # wait for ajax
+ execute_script('document.querySelector(".js-gcp-project-id-dropdown input").setAttribute("type", "text")')
+ execute_script('document.querySelector(".js-gcp-zone-dropdown input").setAttribute("type", "text")')
+ execute_script('document.querySelector(".js-gcp-machine-type-dropdown input").setAttribute("type", "text")')
- expect(page).to have_content('Something wrong!')
- end
+ fill_in 'cluster[name]', with: 'dev-cluster'
+ fill_in 'cluster[provider_gcp_attributes][gcp_project_id]', with: 'gcp-project-123'
+ fill_in 'cluster[provider_gcp_attributes][zone]', with: 'us-central1-a'
+ fill_in 'cluster[provider_gcp_attributes][machine_type]', with: 'n1-standard-2'
end
- context 'when user filled form with invalid parameters' do
- before do
- click_button 'Create Kubernetes cluster'
- end
-
- it 'user sees a validation error' do
- expect(page).to have_css('#error_explanation')
- end
+ it 'users sees a form with the GCP token' do
+ expect(page).to have_selector(:css, 'form[data-token="token"]')
end
- end
- context 'when user does have a cluster and visits cluster page' do
- let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
+ it 'user sees a cluster details page and creation status' do
+ subject
- before do
- visit project_cluster_path(project, cluster)
- end
+ expect(page).to have_content('Kubernetes cluster is being created on Google Kubernetes Engine...')
+
+ Clusters::Cluster.last.provider.make_created!
- it 'user sees a cluster details page' do
- expect(page).to have_button('Save changes')
- expect(page.find(:css, '.cluster-name').value).to eq(cluster.name)
+ expect(page).to have_content('Kubernetes cluster was successfully created on Google Kubernetes Engine')
end
- context 'when user disables the cluster' do
- before do
- page.find(:css, '.js-cluster-enable-toggle-area .js-project-feature-toggle').click
- page.within('#cluster-integration') { click_button 'Save changes' }
- end
+ it 'user sees a error if something wrong during creation' do
+ subject
- it 'user sees the successful message' do
- expect(page).to have_content('Kubernetes cluster was successfully updated.')
- end
- end
+ expect(page).to have_content('Kubernetes cluster is being created on Google Kubernetes Engine...')
- context 'when user changes cluster parameters' do
- before do
- fill_in 'cluster_platform_kubernetes_attributes_namespace', with: 'my-namespace'
- page.within('#js-cluster-details') { click_button 'Save changes' }
- end
+ Clusters::Cluster.last.provider.make_errored!('Something wrong!')
- it 'user sees the successful message' do
- expect(page).to have_content('Kubernetes cluster was successfully updated.')
- expect(cluster.reload.platform_kubernetes.namespace).to eq('my-namespace')
- end
+ expect(page).to have_content('Something wrong!')
end
+ end
- context 'when user destroy the cluster' do
- before do
- page.accept_confirm do
- click_link 'Remove integration'
- end
- end
+ context 'when user filled form with invalid parameters' do
+ before do
+ execute_script('document.querySelector(".js-gke-cluster-creation-submit").removeAttribute("disabled")')
+ click_button 'Create Kubernetes cluster'
+ end
- it 'user sees creation form with the successful message' do
- expect(page).to have_content('Kubernetes cluster integration was successfully removed.')
- expect(page).to have_link('Add Kubernetes cluster')
- end
+ it 'user sees a validation error' do
+ expect(page).to have_css('#error_explanation')
end
end
end
- context 'when user does not have a GCP project with billing enabled' do
- before do
- allow_any_instance_of(Projects::Clusters::GcpController).to receive(:authorize_google_project_billing)
- allow_any_instance_of(Projects::Clusters::GcpController).to receive(:google_project_billing_status).and_return(false)
-
- visit project_clusters_path(project)
-
- click_link 'Add Kubernetes cluster'
- click_link 'Create on Google Kubernetes Engine'
+ context 'when user does have a cluster and visits cluster page' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
- fill_in 'cluster_provider_gcp_attributes_gcp_project_id', with: 'gcp-project-123'
- fill_in 'cluster_name', with: 'dev-cluster'
- click_button 'Create Kubernetes cluster'
+ before do
+ visit project_cluster_path(project, cluster)
end
- it 'user sees form with error' do
- expect(page).to have_content('Please enable billing for one of your projects to be able to create a Kubernetes cluster, then try again.')
+ it 'user sees a cluster details page' do
+ expect(page).to have_button('Save changes')
+ expect(page.find(:css, '.cluster-name').value).to eq(cluster.name)
end
- end
- context 'when gcp billing status is not in redis' do
- before do
- allow_any_instance_of(Projects::Clusters::GcpController).to receive(:authorize_google_project_billing)
- allow_any_instance_of(Projects::Clusters::GcpController).to receive(:google_project_billing_status).and_return(nil)
+ context 'when user disables the cluster' do
+ before do
+ page.find(:css, '.js-cluster-enable-toggle-area .js-project-feature-toggle').click
+ page.within('#cluster-integration') { click_button 'Save changes' }
+ end
- visit project_clusters_path(project)
+ it 'user sees the successful message' do
+ expect(page).to have_content('Kubernetes cluster was successfully updated.')
+ end
+ end
- click_link 'Add Kubernetes cluster'
- click_link 'Create on Google Kubernetes Engine'
+ context 'when user changes cluster parameters' do
+ before do
+ fill_in 'cluster_platform_kubernetes_attributes_namespace', with: 'my-namespace'
+ page.within('#js-cluster-details') { click_button 'Save changes' }
+ end
- fill_in 'cluster_provider_gcp_attributes_gcp_project_id', with: 'gcp-project-123'
- fill_in 'cluster_name', with: 'dev-cluster'
- click_button 'Create Kubernetes cluster'
+ it 'user sees the successful message' do
+ expect(page).to have_content('Kubernetes cluster was successfully updated.')
+ expect(cluster.reload.platform_kubernetes.namespace).to eq('my-namespace')
+ end
end
- it 'user sees form with error' do
- expect(page).to have_content('We could not verify that one of your projects on GCP has billing enabled. Please try again.')
+ context 'when user destroy the cluster' do
+ before do
+ page.accept_confirm do
+ click_link 'Remove integration'
+ end
+ end
+
+ it 'user sees creation form with the successful message' do
+ expect(page).to have_content('Kubernetes cluster integration was successfully removed.')
+ expect(page).to have_link('Add Kubernetes cluster')
+ end
end
end
end
@@ -177,7 +148,7 @@ feature 'Gcp Cluster', :js do
visit project_clusters_path(project)
click_link 'Add Kubernetes cluster'
- click_link 'Create on Google Kubernetes Engine'
+ click_link 'Create new Cluster on GKE'
end
it 'user sees a login page' do
@@ -186,6 +157,19 @@ feature 'Gcp Cluster', :js do
end
end
+ context 'when a user cannot edit the environment scope' do
+ before do
+ visit project_clusters_path(project)
+
+ click_link 'Add Kubernetes cluster'
+ click_link 'Add existing cluster'
+ end
+
+ it 'user does not see the "Environment scope" field' do
+ expect(page).not_to have_css('#cluster_environment_scope')
+ end
+ end
+
context 'when user has not dismissed GCP signup offer' do
before do
visit project_clusters_path(project)
@@ -203,7 +187,7 @@ feature 'Gcp Cluster', :js do
it 'user sees offer on cluster GCP login page' do
click_link 'Add Kubernetes cluster'
- click_link 'Create on Google Kubernetes Engine'
+ click_link 'Create new Cluster on GKE'
expect(page).to have_css('.gcp-signup-offer')
end
diff --git a/spec/features/projects/clusters/interchangeability_spec.rb b/spec/features/projects/clusters/interchangeability_spec.rb
index 3ddb35c755c..0033e12b6b1 100644
--- a/spec/features/projects/clusters/interchangeability_spec.rb
+++ b/spec/features/projects/clusters/interchangeability_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Interchangeability between KubernetesService and Platform::Kubernetes' do
+describe 'Interchangeability between KubernetesService and Platform::Kubernetes' do
EXCEPT_METHODS = %i[test title description help fields initialize_properties namespace namespace= api_url api_url= deprecated? deprecation_message].freeze
EXCEPT_METHODS_GREP_V = %w[_touched? _changed? _was].freeze
diff --git a/spec/features/projects/clusters/user_spec.rb b/spec/features/projects/clusters/user_spec.rb
index 698b64a659c..babf47cc341 100644
--- a/spec/features/projects/clusters/user_spec.rb
+++ b/spec/features/projects/clusters/user_spec.rb
@@ -1,13 +1,13 @@
require 'spec_helper'
-feature 'User Cluster', :js do
+describe 'User Cluster', :js do
include GoogleApi::CloudPlatformHelpers
let(:project) { create(:project) }
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
gitlab_sign_in(user)
allow(Projects::ClustersController).to receive(:STATUS_POLLING_INTERVAL) { 100 }
end
@@ -17,7 +17,7 @@ feature 'User Cluster', :js do
visit project_clusters_path(project)
click_link 'Add Kubernetes cluster'
- click_link 'Add an existing Kubernetes cluster'
+ click_link 'Add existing cluster'
end
context 'when user filled form with valid parameters' do
diff --git a/spec/features/projects/clusters_spec.rb b/spec/features/projects/clusters_spec.rb
index a251a2f4e52..91eac9c8278 100644
--- a/spec/features/projects/clusters_spec.rb
+++ b/spec/features/projects/clusters_spec.rb
@@ -1,13 +1,13 @@
require 'spec_helper'
-feature 'Clusters', :js do
+describe 'Clusters', :js do
include GoogleApi::CloudPlatformHelpers
let(:project) { create(:project) }
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
gitlab_sign_in(user)
end
@@ -83,7 +83,7 @@ feature 'Clusters', :js do
visit project_clusters_path(project)
click_link 'Add Kubernetes cluster'
- click_link 'Create on Google Kubernetes Engine'
+ click_link 'Create new Cluster 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 36a746ac83d..bd254caddfb 100644
--- a/spec/features/projects/commit/builds_spec.rb
+++ b/spec/features/projects/commit/builds_spec.rb
@@ -1,22 +1,22 @@
require 'spec_helper'
-feature 'project commit pipelines', :js do
- given(:project) { create(:project, :repository) }
+describe 'project commit pipelines', :js do
+ let(:project) { create(:project, :repository) }
- background do
+ before do
user = create(:user)
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
context 'when no builds triggered yet' do
- background do
+ before do
create(:ci_pipeline, project: project,
sha: project.commit.sha,
ref: 'master')
end
- scenario 'user views commit pipelines page' do
+ it 'user views commit pipelines page' do
visit pipelines_project_commit_path(project, project.commit.sha)
page.within('.table-holder') do
diff --git a/spec/features/projects/commit/cherry_pick_spec.rb b/spec/features/projects/commit/cherry_pick_spec.rb
index 1df45865d6f..bc3c00dafe2 100644
--- a/spec/features/projects/commit/cherry_pick_spec.rb
+++ b/spec/features/projects/commit/cherry_pick_spec.rb
@@ -9,7 +9,7 @@ describe 'Cherry-pick Commits' do
before do
sign_in(user)
- project.add_master(user)
+ project.add_maintainer(user)
visit project_commit_path(project, master_pickable_commit.id)
end
diff --git a/spec/features/projects/commit/diff_notes_spec.rb b/spec/features/projects/commit/diff_notes_spec.rb
index 4dbfc6f6edf..e2aefa35fad 100644
--- a/spec/features/projects/commit/diff_notes_spec.rb
+++ b/spec/features/projects/commit/diff_notes_spec.rb
@@ -1,13 +1,13 @@
require 'spec_helper'
-feature 'Commit diff', :js do
+describe 'Commit diff', :js do
include RepoHelpers
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in user
end
diff --git a/spec/features/projects/commit/mini_pipeline_graph_spec.rb b/spec/features/projects/commit/mini_pipeline_graph_spec.rb
index 91282063a8d..19f6ebf2c1a 100644
--- a/spec/features/projects/commit/mini_pipeline_graph_spec.rb
+++ b/spec/features/projects/commit/mini_pipeline_graph_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Mini Pipeline Graph in Commit View', :js do
+describe 'Mini Pipeline Graph in Commit View', :js do
let(:project) { create(:project, :public, :repository) }
context 'when commit has pipelines' do
diff --git a/spec/features/projects/commit/user_comments_on_commit_spec.rb b/spec/features/projects/commit/user_comments_on_commit_spec.rb
new file mode 100644
index 00000000000..6397a8ad845
--- /dev/null
+++ b/spec/features/projects/commit/user_comments_on_commit_spec.rb
@@ -0,0 +1,109 @@
+require "spec_helper"
+
+describe "User comments on commit", :js do
+ include Spec::Support::Helpers::Features::NotesHelpers
+ include RepoHelpers
+
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+ let(:comment_text) { "XML attached" }
+
+ before do
+ sign_in(user)
+ project.add_developer(user)
+
+ visit(project_commit_path(project, sample_commit.id))
+ end
+
+ context "when adding new comment" do
+ it "adds comment" do
+ emoji_code = ":+1:"
+
+ page.within(".js-main-target-form") do
+ expect(page).not_to have_link("Cancel")
+
+ fill_in("note[note]", with: "#{comment_text} #{emoji_code}")
+
+ # Check on `Preview` tab
+ click_link("Preview")
+
+ expect(find(".js-md-preview")).to have_content(comment_text).and have_css("gl-emoji")
+ expect(page).not_to have_css(".js-note-text")
+
+ # Check on `Write` tab
+ click_link("Write")
+
+ expect(page).to have_field("note[note]", with: "#{comment_text} #{emoji_code}")
+
+ # Submit comment from the `Preview` tab to get rid of a separate `it` block
+ # which would specially tests if everything gets cleared from the note form.
+ click_link("Preview")
+ click_button("Comment")
+ end
+
+ wait_for_requests
+
+ page.within(".note") do
+ expect(page).to have_content(comment_text).and have_css("gl-emoji")
+ end
+
+ page.within(".js-main-target-form") do
+ expect(page).to have_field("note[note]", with: "").and have_no_css(".js-md-preview")
+ end
+ end
+ end
+
+ context "when editing comment" do
+ before do
+ add_note(comment_text)
+ end
+
+ it "edits comment" do
+ new_comment_text = "+1 Awesome!"
+
+ page.within(".main-notes-list") do
+ note = find(".note")
+ note.hover
+
+ note.find(".js-note-edit").click
+ end
+
+ page.find(".current-note-edit-form textarea")
+
+ page.within(".current-note-edit-form") do
+ fill_in("note[note]", with: new_comment_text)
+ click_button("Save comment")
+ end
+
+ wait_for_requests
+
+ page.within(".note") do
+ expect(page).to have_content(new_comment_text)
+ end
+ end
+ end
+
+ context "when deleting comment" do
+ before do
+ add_note(comment_text)
+ end
+
+ it "deletes comment" do
+ page.within(".note") do
+ expect(page).to have_content(comment_text)
+ end
+
+ page.within(".main-notes-list") do
+ note = find(".note")
+ note.hover
+
+ find(".more-actions").click
+ find(".more-actions .dropdown-menu li", match: :first)
+
+ accept_confirm { find(".js-note-delete").click }
+ end
+
+ expect(page).not_to have_css(".note")
+ end
+ end
+end
diff --git a/spec/features/projects/commits/rss_spec.rb b/spec/features/projects/commits/rss_spec.rb
index 0d9c7355ddd..cfc2637f1b2 100644
--- a/spec/features/projects/commits/rss_spec.rb
+++ b/spec/features/projects/commits/rss_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Project Commits RSS' do
+describe 'Project Commits RSS' do
let(:user) { create(:user) }
let(:project) { create(:project, :repository, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
let(:path) { project_commits_path(project, :master) }
@@ -12,8 +12,8 @@ feature 'Project Commits RSS' do
visit path
end
- it_behaves_like "it has an RSS button with current_user's RSS token"
- it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token"
+ it_behaves_like "it has an RSS button with current_user's feed token"
+ it_behaves_like "an autodiscoverable RSS feed with current_user's feed token"
end
context 'when signed out' do
@@ -21,7 +21,7 @@ feature 'Project Commits RSS' do
visit path
end
- it_behaves_like "it has an RSS button without an RSS token"
- it_behaves_like "an autodiscoverable RSS feed without an RSS token"
+ it_behaves_like "it has an RSS button without a feed token"
+ it_behaves_like "an autodiscoverable RSS feed without a feed token"
end
end
diff --git a/spec/features/projects/commits/user_browses_commits_spec.rb b/spec/features/projects/commits/user_browses_commits_spec.rb
index 35ed6620548..23d8d606790 100644
--- a/spec/features/projects/commits/user_browses_commits_spec.rb
+++ b/spec/features/projects/commits/user_browses_commits_spec.rb
@@ -7,7 +7,7 @@ describe 'User browses commits' do
let(:project) { create(:project, :repository, namespace: user.namespace) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/features/projects/compare_spec.rb b/spec/features/projects/compare_spec.rb
index 7e863d9df32..69600884909 100644
--- a/spec/features/projects/compare_spec.rb
+++ b/spec/features/projects/compare_spec.rb
@@ -5,7 +5,7 @@ describe "Compare", :js do
let(:project) { create(:project, :repository) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in user
end
diff --git a/spec/features/projects/deploy_keys_spec.rb b/spec/features/projects/deploy_keys_spec.rb
index 43a23c42f83..e12532e97fa 100644
--- a/spec/features/projects/deploy_keys_spec.rb
+++ b/spec/features/projects/deploy_keys_spec.rb
@@ -5,7 +5,7 @@ describe 'Project deploy keys', :js do
let(:project) { create(:project_empty_repo) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
@@ -22,7 +22,8 @@ describe 'Project deploy keys', :js do
accept_confirm { find('.ic-remove').click() }
- expect(page).not_to have_selector('.fa-spinner', count: 0)
+ wait_for_requests
+
expect(page).to have_selector('.deploy-key', count: 0)
end
end
diff --git a/spec/features/projects/diffs/diff_show_spec.rb b/spec/features/projects/diffs/diff_show_spec.rb
index c1307ab640f..df05625d105 100644
--- a/spec/features/projects/diffs/diff_show_spec.rb
+++ b/spec/features/projects/diffs/diff_show_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Diff file viewer', :js do
+describe 'Diff file viewer', :js do
let(:project) { create(:project, :public, :repository) }
def visit_commit(sha, anchor: nil)
@@ -24,7 +24,7 @@ feature 'Diff file viewer', :js do
context 'Ruby file (stored in LFS)' do
before do
- project.add_master(project.creator)
+ project.add_maintainer(project.creator)
@commit_id = Files::CreateService.new(
project,
@@ -166,8 +166,7 @@ feature 'Diff file viewer', :js do
context 'expanding the diff' do
before do
- # We can't use `click_link` because the "link" doesn't have an `href`.
- find('a.click-to-expand').click
+ click_button 'Click to expand it.'
wait_for_requests
end
diff --git a/spec/features/projects/environments/environment_metrics_spec.rb b/spec/features/projects/environments/environment_metrics_spec.rb
index 82a722c5960..edbab14f7c1 100644
--- a/spec/features/projects/environments/environment_metrics_spec.rb
+++ b/spec/features/projects/environments/environment_metrics_spec.rb
@@ -1,16 +1,16 @@
require 'spec_helper'
-feature 'Environment > Metrics' do
+describe 'Environment > Metrics' do
include PrometheusHelpers
- given(:user) { create(:user) }
- given(:project) { create(:prometheus_project) }
- given(:pipeline) { create(:ci_pipeline, project: project) }
- given(:build) { create(:ci_build, pipeline: pipeline) }
- given(:environment) { create(:environment, project: project) }
- given(:current_time) { Time.now.utc }
+ let(:user) { create(:user) }
+ let(:project) { create(:prometheus_project) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+ let(:environment) { create(:environment, project: project) }
+ let(:current_time) { Time.now.utc }
- background do
+ before do
project.add_developer(user)
create(:deployment, environment: environment, deployable: build)
stub_all_prometheus_requests(environment.slug)
@@ -24,7 +24,7 @@ feature 'Environment > Metrics' do
end
context 'with deployments and related deployable present' do
- scenario 'shows metrics' do
+ it 'shows metrics' do
click_link('See metrics')
expect(page).to have_css('div#prometheus-graphs')
diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb
index b233af83eec..4c5dda29fee 100644
--- a/spec/features/projects/environments/environment_spec.rb
+++ b/spec/features/projects/environments/environment_spec.rb
@@ -1,42 +1,42 @@
require 'spec_helper'
-feature 'Environment' do
- given(:project) { create(:project) }
- given(:user) { create(:user) }
- given(:role) { :developer }
+describe 'Environment' do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:role) { :developer }
- background do
+ before do
sign_in(user)
project.add_role(user, role)
end
- feature 'environment details page' do
- given!(:environment) { create(:environment, project: project) }
- given!(:permissions) { }
- given!(:deployment) { }
- given!(:action) { }
+ describe 'environment details page' do
+ let!(:environment) { create(:environment, project: project) }
+ let!(:permissions) { }
+ let!(:deployment) { }
+ let!(:action) { }
before do
visit_environment(environment)
end
- scenario 'shows environment name' do
+ it 'shows environment name' do
expect(page).to have_content(environment.name)
end
context 'without deployments' do
- scenario 'does show no deployments' do
+ it 'does show no deployments' do
expect(page).to have_content('You don\'t have any deployments right now.')
end
end
context 'with deployments' do
context 'when there is no related deployable' do
- given(:deployment) do
+ let(:deployment) do
create(:deployment, environment: environment, deployable: nil)
end
- scenario 'does show deployment SHA' do
+ it 'does show deployment SHA' do
expect(page).to have_link(deployment.short_sha)
expect(page).not_to have_link('Re-deploy')
expect(page).not_to have_terminal_button
@@ -44,27 +44,27 @@ feature 'Environment' do
end
context 'with related deployable present' do
- given(:pipeline) { create(:ci_pipeline, project: project) }
- given(:build) { create(:ci_build, pipeline: pipeline) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
- given(:deployment) do
+ let(:deployment) do
create(:deployment, environment: environment, deployable: build)
end
- scenario 'does show build name' do
+ it 'does show build name' do
expect(page).to have_link("#{build.name} (##{build.id})")
expect(page).to have_link('Re-deploy')
expect(page).not_to have_terminal_button
end
context 'with manual action' do
- given(:action) do
+ let(:action) do
create(:ci_build, :manual, pipeline: pipeline,
name: 'deploy to production')
end
context 'when user has ability to trigger deployment' do
- given(:permissions) do
+ let(:permissions) do
create(:protected_branch, :developers_can_merge,
name: action.ref, project: project)
end
@@ -91,21 +91,21 @@ feature 'Environment' do
end
context 'with external_url' do
- given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') }
- given(:build) { create(:ci_build, pipeline: pipeline) }
- given(:deployment) { create(:deployment, environment: environment, deployable: build) }
+ let(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+ let(:deployment) { create(:deployment, environment: environment, deployable: build) }
- scenario 'does show an external link button' do
+ it 'does show an external link button' do
expect(page).to have_link(nil, href: environment.external_url)
end
end
context 'with terminal' do
shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
- context 'for project master' do
- let(:role) { :master }
+ context 'for project maintainer' do
+ let(:role) { :maintainer }
- scenario 'it shows the terminal button' do
+ it 'it shows the terminal button' do
expect(page).to have_terminal_button
end
@@ -126,7 +126,7 @@ feature 'Environment' do
context 'for developer' do
let(:role) { :developer }
- scenario 'does not show terminal button' do
+ it 'does not show terminal button' do
expect(page).not_to have_terminal_button
end
end
@@ -148,25 +148,26 @@ feature 'Environment' do
context 'when environment is available' do
context 'with stop action' do
- given(:action) do
+ let(:action) do
create(:ci_build, :manual, pipeline: pipeline,
name: 'close_app')
end
- given(:deployment) do
+ let(:deployment) do
create(:deployment, environment: environment,
deployable: build,
on_stop: 'close_app')
end
context 'when user has ability to stop environment' do
- given(:permissions) do
+ let(:permissions) do
create(:protected_branch, :developers_can_merge,
name: action.ref, project: project)
end
it 'allows to stop environment' do
- click_link('Stop')
+ click_button('Stop')
+ click_button('Stop environment') # confirm modal
expect(page).to have_content('close_app')
end
@@ -174,25 +175,25 @@ feature 'Environment' do
context 'when user has no ability to stop environment' do
it 'does not allow to stop environment' do
- expect(page).to have_no_link('Stop')
+ expect(page).not_to have_button('Stop')
end
end
context 'for reporter' do
let(:role) { :reporter }
- scenario 'does not show stop button' do
- expect(page).not_to have_link('Stop')
+ it 'does not show stop button' do
+ expect(page).not_to have_button('Stop')
end
end
end
end
context 'when environment is stopped' do
- given(:environment) { create(:environment, project: project, state: :stopped) }
+ let(:environment) { create(:environment, project: project, state: :stopped) }
- scenario 'does not show stop button' do
- expect(page).not_to have_link('Stop')
+ it 'does not show stop button' do
+ expect(page).not_to have_button('Stop')
end
end
end
@@ -200,7 +201,7 @@ feature 'Environment' do
end
end
- feature 'environment folders', :js do
+ describe 'environment folders', :js do
context 'when folder name contains special charaters' do
before do
create(:environment, project: project,
@@ -219,21 +220,21 @@ feature 'Environment' do
end
end
- feature 'auto-close environment when branch is deleted' do
- given(:project) { create(:project, :repository) }
+ describe 'auto-close environment when branch is deleted' do
+ let(:project) { create(:project, :repository) }
- given!(:environment) do
+ let!(:environment) do
create(:environment, :with_review_app, project: project,
ref: 'feature')
end
- scenario 'user visits environment page' do
+ it 'user visits environment page' do
visit_environment(environment)
- expect(page).to have_link('Stop')
+ expect(page).to have_button('Stop')
end
- scenario 'user deletes the branch with running environment' do
+ it 'user deletes the branch with running environment' do
visit project_branches_filtered_path(project, state: 'all', search: 'feature')
remove_branch_with_hooks(project, user, 'feature') do
@@ -242,7 +243,7 @@ feature 'Environment' do
visit_environment(environment)
- expect(page).to have_no_link('Stop')
+ expect(page).not_to have_button('Stop')
end
##
diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb
index f9defa22d35..f0890018286 100644
--- a/spec/features/projects/environments/environments_spec.rb
+++ b/spec/features/projects/environments/environments_spec.rb
@@ -1,15 +1,19 @@
require 'spec_helper'
-feature 'Environments page', :js do
- given(:project) { create(:project) }
- given(:user) { create(:user) }
- given(:role) { :developer }
+describe 'Environments page', :js do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:role) { :developer }
- background do
+ before do
project.add_role(user, role)
sign_in(user)
end
+ def stop_button_selector
+ %q{button[data-original-title="Stop environment"]}
+ end
+
describe 'page tabs' do
it 'shows "Available" and "Stopped" tab with links' do
visit_environments(project)
@@ -99,7 +103,7 @@ feature 'Environments page', :js do
end
describe 'environments table' do
- given!(:environment) do
+ let!(:environment) do
create(:environment, project: project, state: :available)
end
@@ -120,14 +124,14 @@ feature 'Environments page', :js do
end
it 'does not show stip button when environment is not stoppable' do
- expect(page).not_to have_selector('.stop-env-link')
+ expect(page).not_to have_selector(stop_button_selector)
end
end
context 'when there are deployments' do
- given(:project) { create(:project, :repository) }
+ let(:project) { create(:project, :repository) }
- given!(:deployment) do
+ let!(:deployment) do
create(:deployment, environment: environment,
sha: project.commit.id)
end
@@ -140,14 +144,14 @@ feature 'Environments page', :js do
end
context 'when builds and manual actions are present' do
- given!(:pipeline) { create(:ci_pipeline, project: project) }
- given!(:build) { create(:ci_build, pipeline: pipeline) }
+ let!(:pipeline) { create(:ci_pipeline, project: project) }
+ let!(:build) { create(:ci_build, pipeline: pipeline) }
- given!(:action) do
+ let!(:action) do
create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production')
end
- given!(:deployment) do
+ let!(:deployment) do
create(:deployment, environment: environment,
deployable: build,
sha: project.commit.id)
@@ -178,7 +182,7 @@ feature 'Environments page', :js do
end
it 'shows a stop button' do
- expect(page).not_to have_selector('.stop-env-link')
+ expect(page).not_to have_selector(stop_button_selector)
end
it 'does not show external link button' do
@@ -190,9 +194,9 @@ feature 'Environments page', :js do
end
context 'with external_url' do
- given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') }
- given(:build) { create(:ci_build, pipeline: pipeline) }
- given(:deployment) { create(:deployment, environment: environment, deployable: build) }
+ let(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+ let(:deployment) { create(:deployment, environment: environment, deployable: build) }
it 'shows an external link button' do
expect(page).to have_link(nil, href: environment.external_url)
@@ -200,33 +204,33 @@ feature 'Environments page', :js do
end
context 'with stop action' do
- given(:action) do
+ let(:action) do
create(:ci_build, :manual, pipeline: pipeline, name: 'close_app')
end
- given(:deployment) do
+ let(:deployment) do
create(:deployment, environment: environment,
deployable: build,
on_stop: 'close_app')
end
it 'shows a stop button' do
- expect(page).to have_selector('.stop-env-link')
+ expect(page).to have_selector(stop_button_selector)
end
context 'when user is a reporter' do
let(:role) { :reporter }
it 'does not show stop button' do
- expect(page).not_to have_selector('.stop-env-link')
+ expect(page).not_to have_selector(stop_button_selector)
end
end
end
context 'when kubernetes terminal is available' do
shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
- context 'for project master' do
- let(:role) { :master }
+ context 'for project maintainer' do
+ let(:role) { :maintainer }
it 'shows the terminal button' do
expect(page).to have_terminal_button
@@ -271,9 +275,9 @@ feature 'Environments page', :js do
end
context 'user is a developer' do
- given(:role) { :developer }
+ let(:role) { :developer }
- scenario 'developer creates a new environment with a valid name' do
+ it 'developer creates a new environment with a valid name' do
within(".top-area") { click_link 'New environment' }
fill_in('Name', with: 'production')
click_on 'Save'
@@ -281,7 +285,7 @@ feature 'Environments page', :js do
expect(page).to have_content('production')
end
- scenario 'developer creates a new environmetn with invalid name' do
+ it 'developer creates a new environmetn with invalid name' do
within(".top-area") { click_link 'New environment' }
fill_in('Name', with: 'name,with,commas')
click_on 'Save'
@@ -291,9 +295,9 @@ feature 'Environments page', :js do
end
context 'user is a reporter' do
- given(:role) { :reporter }
+ let(:role) { :reporter }
- scenario 'reporters tries to create a new environment' do
+ it 'reporters tries to create a new environment' do
expect(page).not_to have_link('New environment')
end
end
@@ -309,7 +313,7 @@ feature 'Environments page', :js do
state: :available)
end
- scenario 'users unfurls an environment folder' do
+ it 'users unfurls an environment folder' do
visit_environments(project)
expect(page).not_to have_content 'review-1'
@@ -335,7 +339,7 @@ feature 'Environments page', :js do
state: :available)
end
- scenario 'user opens folder view' do
+ it 'user opens folder view' do
visit folder_project_environments_path(project, 'staging.review')
wait_for_requests
diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb
index b0eb7c5b42a..ab16fdee883 100644
--- a/spec/features/projects/features_visibility_spec.rb
+++ b/spec/features/projects/features_visibility_spec.rb
@@ -8,7 +8,7 @@ describe 'Edit Project Settings' do
describe 'project features visibility selectors', :js do
before do
- project.add_master(member)
+ project.add_maintainer(member)
sign_in(member)
end
@@ -165,7 +165,7 @@ describe 'Edit Project Settings' do
describe 'repository visibility', :js do
before do
- project.add_master(member)
+ project.add_maintainer(member)
sign_in(member)
visit edit_project_path(project)
end
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 b410199fd1f..ac6c8c337fa 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
@@ -2,16 +2,16 @@ require 'spec_helper'
describe 'Projects > Files > Project owner creates a license file', :js do
let(:project) { create(:project, :repository) }
- let(:project_master) { project.owner }
+ let(:project_maintainer) { project.owner }
before do
- project.repository.delete_file(project_master, 'LICENSE',
+ project.repository.delete_file(project_maintainer, 'LICENSE',
message: 'Remove LICENSE', branch_name: 'master')
- sign_in(project_master)
+ sign_in(project_maintainer)
visit project_path(project)
end
- it 'project master creates a license file manually from a template' do
+ it 'project maintainer creates a license file manually from a template' do
visit project_tree_path(project, project.repository.root_ref)
find('.add-to-tree').click
click_link 'New file'
@@ -35,7 +35,7 @@ describe 'Projects > Files > Project owner creates a license file', :js do
expect(page).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
end
- it 'project master creates a license file from the "Add license" link' do
+ it 'project maintainer creates a license file from the "Add license" link' do
click_link 'Add License'
expect(page).to have_content('New file')
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 53d8ace7c94..801291c1f77 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
@@ -2,13 +2,13 @@ require 'spec_helper'
describe 'Projects > Files > Project owner sees a link to create a license file in empty project', :js do
let(:project) { create(:project_empty_repo) }
- let(:project_master) { project.owner }
+ let(:project_maintainer) { project.owner }
before do
- sign_in(project_master)
+ sign_in(project_maintainer)
end
- it 'project master creates a license file from a template' do
+ it 'project maintainer creates a license file from a template' do
visit project_path(project)
click_on 'Add License'
expect(page).to have_content('New file')
diff --git a/spec/features/projects/files/template_selector_menu_spec.rb b/spec/features/projects/files/template_selector_menu_spec.rb
index b549a69ddf3..6b313824acd 100644
--- a/spec/features/projects/files/template_selector_menu_spec.rb
+++ b/spec/features/projects/files/template_selector_menu_spec.rb
@@ -1,11 +1,11 @@
require 'spec_helper'
-feature 'Template selector menu', :js do
+describe 'Template selector menu', :js do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in user
end
@@ -14,7 +14,7 @@ feature 'Template selector menu', :js do
create_and_edit_file('README.md')
end
- scenario 'is not displayed' do
+ it 'is not displayed' do
check_template_selector_menu_display(false)
end
@@ -23,7 +23,7 @@ feature 'Template selector menu', :js do
click_link 'Preview'
end
- scenario 'template selector menu is not displayed' do
+ it 'template selector menu is not displayed' do
check_template_selector_menu_display(false)
click_link 'Write'
check_template_selector_menu_display(false)
@@ -36,7 +36,7 @@ feature 'Template selector menu', :js do
visit project_edit_blob_path(project, File.join(project.default_branch, 'LICENSE'))
end
- scenario 'is displayed' do
+ it 'is displayed' do
check_template_selector_menu_display(true)
end
@@ -45,7 +45,7 @@ feature 'Template selector menu', :js do
click_link 'Preview'
end
- scenario 'template selector menu is hidden and shown correctly' do
+ it 'template selector menu is hidden and shown correctly' do
check_template_selector_menu_display(false)
click_link 'Write'
check_template_selector_menu_display(true)
diff --git a/spec/features/projects/files/user_browses_files_spec.rb b/spec/features/projects/files/user_browses_files_spec.rb
index 41f6c52fb8a..f56174fc85c 100644
--- a/spec/features/projects/files/user_browses_files_spec.rb
+++ b/spec/features/projects/files/user_browses_files_spec.rb
@@ -147,11 +147,8 @@ describe "User browses files" do
page.within(".tree-table") do
click_link("README.md")
end
-
- # rubocop:disable Lint/Void
# Test the full URLs of links instead of relative paths by `have_link(text: "...", href: "...")`.
find("a", text: /^empty$/)["href"] == project_blob_url(project, "markdown/d/README.md")
- # rubocop:enable Lint/Void
end
it "shows correct content of directory" do
diff --git a/spec/features/projects/files/user_creates_files_spec.rb b/spec/features/projects/files/user_creates_files_spec.rb
index 208cc8d81f7..d4dda43c823 100644
--- a/spec/features/projects/files/user_creates_files_spec.rb
+++ b/spec/features/projects/files/user_creates_files_spec.rb
@@ -12,7 +12,7 @@ describe 'Projects > Files > User creates files' do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/features/projects/files/user_deletes_files_spec.rb b/spec/features/projects/files/user_deletes_files_spec.rb
index 36d3e001a64..0e9f83a16ce 100644
--- a/spec/features/projects/files/user_deletes_files_spec.rb
+++ b/spec/features/projects/files/user_deletes_files_spec.rb
@@ -17,7 +17,7 @@ describe 'Projects > Files > User deletes files' do
context 'when an user has write access' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
visit(project_tree_path_root_ref)
end
diff --git a/spec/features/projects/files/user_edits_files_spec.rb b/spec/features/projects/files/user_edits_files_spec.rb
index dc6e4fd27cb..ccc1bc1bc10 100644
--- a/spec/features/projects/files/user_edits_files_spec.rb
+++ b/spec/features/projects/files/user_edits_files_spec.rb
@@ -31,7 +31,7 @@ describe 'Projects > Files > User edits files' do
context 'when an user has write access' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
visit(project_tree_path_root_ref)
end
diff --git a/spec/features/projects/files/user_find_file_spec.rb b/spec/features/projects/files/user_find_file_spec.rb
index df405e70dd4..e2d881b34d2 100644
--- a/spec/features/projects/files/user_find_file_spec.rb
+++ b/spec/features/projects/files/user_find_file_spec.rb
@@ -6,7 +6,7 @@ describe 'User find project file' do
before do
sign_in(user)
- project.add_master(user)
+ project.add_maintainer(user)
visit project_tree_path(project, project.repository.root_ref)
end
diff --git a/spec/features/projects/files/user_reads_pipeline_status_spec.rb b/spec/features/projects/files/user_reads_pipeline_status_spec.rb
index 2fb9da2f0a2..ff0aa933a3e 100644
--- a/spec/features/projects/files/user_reads_pipeline_status_spec.rb
+++ b/spec/features/projects/files/user_reads_pipeline_status_spec.rb
@@ -7,7 +7,7 @@ describe 'user reads pipeline status', :js do
let(:x110_pipeline) { create_pipeline('x1.1.0', 'failed') }
before do
- project.add_master(user)
+ project.add_maintainer(user)
project.repository.add_tag(user, 'x1.1.0', 'v1.1.0')
v110_pipeline
@@ -17,7 +17,7 @@ describe 'user reads pipeline status', :js do
end
shared_examples 'visiting project tree' do
- scenario 'sees the correct pipeline status' do
+ it 'sees the correct pipeline status' do
visit project_tree_path(project, expected_pipeline.ref)
wait_for_requests
diff --git a/spec/features/projects/files/user_replaces_files_spec.rb b/spec/features/projects/files/user_replaces_files_spec.rb
index 9ac3417b671..3a81e77c4ba 100644
--- a/spec/features/projects/files/user_replaces_files_spec.rb
+++ b/spec/features/projects/files/user_replaces_files_spec.rb
@@ -19,7 +19,7 @@ describe 'Projects > Files > User replaces files' do
context 'when an user has write access' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
visit(project_tree_path_root_ref)
end
diff --git a/spec/features/projects/files/user_uploads_files_spec.rb b/spec/features/projects/files/user_uploads_files_spec.rb
index 8b212faa29d..af3fc528a20 100644
--- a/spec/features/projects/files/user_uploads_files_spec.rb
+++ b/spec/features/projects/files/user_uploads_files_spec.rb
@@ -14,7 +14,7 @@ describe 'Projects > Files > User uploads files' do
let(:project2_tree_path_root_ref) { project_tree_path(project2, project2.repository.root_ref) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/features/projects/fork_spec.rb b/spec/features/projects/fork_spec.rb
index 1743b1e083f..cd5fef8238e 100644
--- a/spec/features/projects/fork_spec.rb
+++ b/spec/features/projects/fork_spec.rb
@@ -129,11 +129,11 @@ describe 'Project fork' do
end
end
- context 'master in group' do
+ context 'maintainer in group' do
let(:group) { create(:group) }
before do
- group.add_master(user)
+ group.add_maintainer(user)
end
it 'allows user to fork project to group or to user namespace' do
diff --git a/spec/features/projects/graph_spec.rb b/spec/features/projects/graph_spec.rb
index 57172610aed..9665f1755d6 100644
--- a/spec/features/projects/graph_spec.rb
+++ b/spec/features/projects/graph_spec.rb
@@ -3,16 +3,17 @@ require 'spec_helper'
describe 'Project Graph', :js do
let(:user) { create :user }
let(:project) { create(:project, :repository, namespace: user.namespace) }
+ let(:branch_name) { 'master' }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
shared_examples 'page should have commits graphs' do
it 'renders commits' do
- expect(page).to have_content('Commit statistics for master')
+ expect(page).to have_content("Commit statistics for #{branch_name}")
expect(page).to have_content('Commits per day of month')
end
end
@@ -57,6 +58,23 @@ describe 'Project Graph', :js do
it_behaves_like 'page should have languages graphs'
end
+ context 'chart graph with HTML escaped branch name' do
+ let(:branch_name) { '<h1>evil</h1>' }
+
+ before do
+ project.repository.create_branch(branch_name, 'master')
+
+ visit charts_project_graph_path(project, branch_name)
+ end
+
+ it_behaves_like 'page should have commits graphs'
+
+ it 'HTML escapes branch name' do
+ expect(page.body).to include("Commit statistics for <strong>#{ERB::Util.html_escape(branch_name)}</strong>")
+ expect(page.body).not_to include(branch_name)
+ end
+ end
+
context 'when CI enabled' do
before do
project.enable_ci
diff --git a/spec/features/projects/hook_logs/user_reads_log_spec.rb b/spec/features/projects/hook_logs/user_reads_log_spec.rb
index 18e975fa653..086cd4b9f03 100644
--- a/spec/features/projects/hook_logs/user_reads_log_spec.rb
+++ b/spec/features/projects/hook_logs/user_reads_log_spec.rb
@@ -1,17 +1,17 @@
require 'spec_helper'
-feature 'Hook logs' do
- given(:web_hook_log) { create(:web_hook_log, response_body: '<script>') }
- given(:project) { web_hook_log.web_hook.project }
- given(:user) { create(:user) }
+describe 'Hook logs' do
+ let(:web_hook_log) { create(:web_hook_log, response_body: '<script>') }
+ let(:project) { web_hook_log.web_hook.project }
+ let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
- scenario 'user reads log without getting XSS' do
+ it 'user reads log without getting XSS' do
visit(
project_hook_hook_log_path(
project, web_hook_log.web_hook, web_hook_log))
diff --git a/spec/features/projects/import_export/export_file_spec.rb b/spec/features/projects/import_export/export_file_spec.rb
index 6732cf61767..eb281cd2122 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'll 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 do
+describe 'Import/Export - project export integration test', :js do
include Select2Helper
include ExportFileHelper
@@ -23,8 +23,9 @@ feature 'Import/Export - project export integration test', :js do
let(:project) { setup_project }
- background do
+ before do
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
+ stub_feature_flags(import_export_object_storage: false)
end
after do
@@ -36,7 +37,7 @@ feature 'Import/Export - project export integration test', :js do
sign_in(user)
end
- scenario 'exports a project successfully' do
+ it 'exports a project successfully' do
visit edit_project_path(project)
expect(page).to have_content('Export project')
diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb
index 60fe30bd898..9cbfb62d872 100644
--- a/spec/features/projects/import_export/import_file_spec.rb
+++ b/spec/features/projects/import_export/import_file_spec.rb
@@ -1,13 +1,13 @@
require 'spec_helper'
-feature 'Import/Export - project import integration test', :js do
+describe 'Import/Export - project import integration test', :js do
include Select2Helper
let(:user) { create(:user) }
let(:file) { File.join(Rails.root, 'spec', 'features', 'projects', 'import_export', 'test_project_export.tar.gz') }
let(:export_path) { "#{Dir.tmpdir}/import_file_spec" }
- background do
+ before do
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
gitlab_sign_in(user)
end
@@ -22,7 +22,7 @@ feature 'Import/Export - project import integration test', :js do
let(:project_path) { 'test-project-path' + SecureRandom.hex }
context 'prefilled the path' do
- scenario 'user imports an exported project successfully' do
+ it 'user imports an exported project successfully' do
visit new_project_path
select2(namespace.id, from: '#project_namespace_id')
@@ -51,7 +51,7 @@ feature 'Import/Export - project import integration test', :js do
end
context 'path is not prefilled' do
- scenario 'user imports an exported project successfully' do
+ it 'user imports an exported project successfully' do
visit new_project_path
click_import_project_tab
click_link 'GitLab export'
@@ -68,7 +68,7 @@ feature 'Import/Export - project import integration test', :js do
end
end
- scenario 'invalid project' do
+ it 'invalid project' do
project = create(:project, namespace: user.namespace)
visit new_project_path
@@ -87,11 +87,13 @@ feature 'Import/Export - project import integration test', :js do
def wiki_exists?(project)
wiki = ProjectWiki.new(project)
- File.exist?(wiki.repository.path_to_repo) && !wiki.repository.empty?
+ wiki.repository.exists? && !wiki.repository.empty?
end
def project_hook_exists?(project)
- Gitlab::Git::Hook.new('post-receive', project.repository.raw_repository).exists?
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ Gitlab::Git::Hook.new('post-receive', project.repository.raw_repository).exists?
+ end
end
def click_import_project_tab
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 7d056b0c140..9bb8a2063b5 100644
--- a/spec/features/projects/import_export/namespace_export_file_spec.rb
+++ b/spec/features/projects/import_export/namespace_export_file_spec.rb
@@ -5,6 +5,7 @@ describe 'Import/Export - Namespace export file cleanup', :js do
before do
allow(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
+ stub_feature_flags(import_export_object_storage: false)
end
after do
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 72ab2d71f35..3b5df47e0b6 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 e26caf1f456..a57edc394f9 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 do
+describe 'issuable templates', :js do
include ProjectForksHelper
let(:user) { create(:user) }
@@ -8,7 +8,7 @@ feature 'issuable templates', :js do
let(:issue_form_location) { '#content-body .issuable-details .detail-page-description' }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in user
end
@@ -18,7 +18,7 @@ feature 'issuable templates', :js do
let(:issue) { create(:issue, author: user, assignees: [user], project: project) }
let(:description_addition) { ' appending to description' }
- background do
+ before do
project.repository.create_file(
user,
'.gitlab/issue_templates/bug.md',
@@ -36,14 +36,14 @@ feature 'issuable templates', :js do
fill_in :'issuable-title', with: 'test issue title'
end
- scenario 'user selects "bug" template' do
+ it 'user selects "bug" template' do
select_template 'bug'
wait_for_requests
assert_template(page_part: issue_form_location)
save_changes
end
- scenario 'user selects "bug" template and then "no template"' do
+ it 'user selects "bug" template and then "no template"' do
select_template 'bug'
wait_for_requests
select_option 'No template'
@@ -51,7 +51,7 @@ feature 'issuable templates', :js do
save_changes('')
end
- scenario 'user selects "bug" template, edits description and then selects "reset template"' do
+ it 'user selects "bug" template, edits description and then selects "reset template"' do
select_template 'bug'
wait_for_requests
find_field('issue-description').send_keys(description_addition)
@@ -67,7 +67,7 @@ feature 'issuable templates', :js do
let(:template_content) { 'this is a test "bug" template' }
let(:issue) { create(:issue, author: user, assignees: [user], project: project) }
- background do
+ before do
project.repository.create_file(
user,
'.gitlab/issue_templates/bug.md',
@@ -80,7 +80,7 @@ feature 'issuable templates', :js do
fill_in :'issue-description', with: prior_description
end
- scenario 'user selects "bug" template' do
+ it 'user selects "bug" template' do
select_template 'bug'
wait_for_requests
assert_template(page_part: issue_form_location)
@@ -92,7 +92,7 @@ feature 'issuable templates', :js do
let(:template_content) { 'this is a test "feature-proposal" template' }
let(:merge_request) { create(:merge_request, :with_diffs, source_project: project) }
- background do
+ before do
project.repository.create_file(
user,
'.gitlab/merge_request_templates/feature-proposal.md',
@@ -103,7 +103,7 @@ feature 'issuable templates', :js do
fill_in :'merge_request[title]', with: 'test merge request title'
end
- scenario 'user selects "feature-proposal" template' do
+ it 'user selects "feature-proposal" template' do
select_template 'feature-proposal'
wait_for_requests
assert_template
@@ -117,7 +117,7 @@ feature 'issuable templates', :js do
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
+ before do
sign_out(:user)
project.add_developer(fork_user)
@@ -136,7 +136,7 @@ feature 'issuable templates', :js do
context 'feature proposal template' do
context 'template exists in target project' do
- scenario 'user selects template' do
+ it 'user selects template' do
select_template 'feature-proposal'
wait_for_requests
assert_template
diff --git a/spec/features/projects/issues/rss_spec.rb b/spec/features/projects/issues/rss_spec.rb
index ff91aabc311..0e1383cd607 100644
--- a/spec/features/projects/issues/rss_spec.rb
+++ b/spec/features/projects/issues/rss_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Project Issues RSS' do
+describe 'Project Issues RSS' do
let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
let(:path) { project_issues_path(project) }
@@ -17,8 +17,8 @@ feature 'Project Issues RSS' do
visit path
end
- it_behaves_like "it has an RSS button with current_user's RSS token"
- it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token"
+ it_behaves_like "it has an RSS button with current_user's feed token"
+ it_behaves_like "an autodiscoverable RSS feed with current_user's feed token"
end
context 'when signed out' do
@@ -26,7 +26,7 @@ feature 'Project Issues RSS' do
visit path
end
- it_behaves_like "it has an RSS button without an RSS token"
- it_behaves_like "an autodiscoverable RSS feed without an RSS token"
+ it_behaves_like "it has an RSS button without a feed token"
+ it_behaves_like "an autodiscoverable RSS feed without a feed token"
end
end
diff --git a/spec/features/projects/issues/user_comments_on_issue_spec.rb b/spec/features/projects/issues/user_comments_on_issue_spec.rb
index c45fdc7642f..ba5b80ed04b 100644
--- a/spec/features/projects/issues/user_comments_on_issue_spec.rb
+++ b/spec/features/projects/issues/user_comments_on_issue_spec.rb
@@ -31,11 +31,14 @@ describe "User comments on issue", :js do
end
it "adds comment with code block" do
- comment = "```\nCommand [1]: /usr/local/bin/git , see [text](doc/text)\n```"
+ code_block_content = "Command [1]: /usr/local/bin/git , see [text](doc/text)"
+ comment = "```\n#{code_block_content}\n```"
add_note(comment)
- expect(page).to have_content(comment)
+ wait_for_requests
+
+ expect(page.find('pre code').text).to eq code_block_content
end
end
@@ -60,6 +63,14 @@ describe "User comments on issue", :js do
page.within(".current-note-edit-form") do
fill_in("note[note]", with: comment)
+ find('textarea').send_keys [:control, :shift, 'p']
+ expect(page).to have_selector('.current-note-edit-form .md-preview-holder')
+ expect(page.find('.current-note-edit-form .md-preview-holder p')).to have_content(comment)
+ end
+
+ expect(page).to have_selector('.new-note .note-textarea')
+
+ page.within(".current-note-edit-form") do
click_button("Save comment")
end
diff --git a/spec/features/projects/issues/user_creates_issue_spec.rb b/spec/features/projects/issues/user_creates_issue_spec.rb
index e76f7c5589d..5e8662100c5 100644
--- a/spec/features/projects/issues/user_creates_issue_spec.rb
+++ b/spec/features/projects/issues/user_creates_issue_spec.rb
@@ -17,6 +17,9 @@ describe "User creates issue" do
expect(page).to have_no_content("Assign to")
.and have_no_content("Labels")
.and have_no_content("Milestone")
+
+ expect(page.find('#issue_title')['placeholder']).to eq 'Title'
+ expect(page.find('#issue_description')['placeholder']).to eq 'Write a comment or drag your files here…'
end
issue_title = "500 error on profile"
diff --git a/spec/features/projects/issues/user_sorts_issues_spec.rb b/spec/features/projects/issues/user_sorts_issues_spec.rb
index c3d63000dac..db5936a30cb 100644
--- a/spec/features/projects/issues/user_sorts_issues_spec.rb
+++ b/spec/features/projects/issues/user_sorts_issues_spec.rb
@@ -18,7 +18,7 @@ describe "User sorts issues" do
it "sorts by popularity" do
find("button.dropdown-toggle").click
- page.within(".content ul.dropdown-menu.dropdown-menu-align-right li") do
+ page.within(".content ul.dropdown-menu.dropdown-menu-right li") do
click_link("Popularity")
end
diff --git a/spec/features/projects/jobs/permissions_spec.rb b/spec/features/projects/jobs/permissions_spec.rb
index 31abadf9bd6..e639f0cf82e 100644
--- a/spec/features/projects/jobs/permissions_spec.rb
+++ b/spec/features/projects/jobs/permissions_spec.rb
@@ -88,10 +88,9 @@ describe 'Project Jobs Permissions' do
describe 'artifacts page' do
context 'when recent job has artifacts available' do
before do
- artifacts = Rails.root.join('spec/fixtures/ci_build_artifacts.zip')
- archive = fixture_file_upload(artifacts, 'application/zip')
+ archive = fixture_file_upload('spec/fixtures/ci_build_artifacts.zip')
- job.update_attributes(legacy_artifacts_file: archive)
+ job.update(legacy_artifacts_file: archive)
end
context 'when public access for jobs is disabled' do
diff --git a/spec/features/projects/jobs/user_browses_job_spec.rb b/spec/features/projects/jobs/user_browses_job_spec.rb
index bff5bbe99af..50e957bf12b 100644
--- a/spec/features/projects/jobs/user_browses_job_spec.rb
+++ b/spec/features/projects/jobs/user_browses_job_spec.rb
@@ -8,7 +8,7 @@ describe 'User browses a job', :js do
let!(:build) { create(:ci_build, :success, :trace_artifact, :coverage, pipeline: pipeline) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
project.enable_ci
sign_in(user)
@@ -32,8 +32,6 @@ describe 'User browses a job', :js do
page.within('.erased') do
expect(page).to have_content('Job has been erased')
end
-
- expect(build.project.running_or_pending_build_count).to eq(build.project.builds.running_or_pending.count(:all))
end
context 'with a failed job' do
diff --git a/spec/features/projects/jobs/user_browses_jobs_spec.rb b/spec/features/projects/jobs/user_browses_jobs_spec.rb
index 786ec327b92..08786fe1630 100644
--- a/spec/features/projects/jobs/user_browses_jobs_spec.rb
+++ b/spec/features/projects/jobs/user_browses_jobs_spec.rb
@@ -7,7 +7,7 @@ describe 'User browses jobs' do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
project.enable_ci
project.update_attribute(:build_coverage_regex, /Coverage (\d+)%/)
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index 9d1c4cbad8b..35b1c46ecf6 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
require 'tempfile'
-feature 'Jobs', :clean_gitlab_redis_shared_state do
+describe 'Jobs', :clean_gitlab_redis_shared_state do
let(:user) { create(:user) }
let(:user_access_level) { :developer }
let(:project) { create(:project, :repository) }
@@ -11,7 +11,7 @@ feature 'Jobs', :clean_gitlab_redis_shared_state do
let(:job2) { create(:ci_build) }
let(:artifacts_file) do
- fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif')
+ fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif')
end
before do
@@ -165,7 +165,7 @@ feature 'Jobs', :clean_gitlab_redis_shared_state do
it 'links to issues/new with the title and description filled in' do
button_title = "Job Failed ##{job.id}"
- job_url = project_job_path(project, job)
+ job_url = project_job_url(project, job, host: page.server.host, port: page.server.port)
options = { issue: { title: button_title, description: "Job [##{job.id}](#{job_url}) failed for #{job.sha}:\n" } }
href = new_project_issue_path(project, options)
@@ -187,7 +187,7 @@ feature 'Jobs', :clean_gitlab_redis_shared_state do
context "Download artifacts" do
before do
- job.update_attributes(legacy_artifacts_file: artifacts_file)
+ job.update(legacy_artifacts_file: artifacts_file)
visit project_job_path(project, job)
end
@@ -198,8 +198,8 @@ feature 'Jobs', :clean_gitlab_redis_shared_state do
context 'Artifacts expire date' do
before do
- job.update_attributes(legacy_artifacts_file: artifacts_file,
- artifacts_expire_at: expire_at)
+ job.update(legacy_artifacts_file: artifacts_file,
+ artifacts_expire_at: expire_at)
visit project_job_path(project, job)
end
@@ -259,7 +259,7 @@ feature 'Jobs', :clean_gitlab_redis_shared_state do
end
end
- feature 'Raw trace' do
+ describe 'Raw trace' do
before do
job.run!
@@ -271,7 +271,7 @@ feature 'Jobs', :clean_gitlab_redis_shared_state do
end
end
- feature 'HTML trace', :js do
+ describe 'HTML trace', :js do
before do
job.run!
@@ -291,7 +291,7 @@ feature 'Jobs', :clean_gitlab_redis_shared_state do
end
end
- feature 'Variables' do
+ describe 'Variables' do
let(:trigger_request) { create(:ci_trigger_request) }
let(:job) do
@@ -530,14 +530,14 @@ feature 'Jobs', :clean_gitlab_redis_shared_state do
describe "GET /:project/jobs/:id/download" do
before do
- job.update_attributes(legacy_artifacts_file: artifacts_file)
+ job.update(legacy_artifacts_file: artifacts_file)
visit project_job_path(project, job)
click_link 'Download'
end
context "Build from other project" do
before do
- job2.update_attributes(legacy_artifacts_file: artifacts_file)
+ job2.update(legacy_artifacts_file: artifacts_file)
visit download_project_job_artifacts_path(project, job2)
end
diff --git a/spec/features/projects/labels/issues_sorted_by_priority_spec.rb b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb
index 0292a3192d8..6178f11ded7 100644
--- a/spec/features/projects/labels/issues_sorted_by_priority_spec.rb
+++ b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Issue prioritization' do
+describe 'Issue prioritization' do
let(:user) { create(:user) }
let(:project) { create(:project, name: 'test', namespace: user.namespace) }
@@ -13,7 +13,7 @@ feature 'Issue prioritization' do
# According to https://gitlab.com/gitlab-org/gitlab-ce/issues/14189#note_4360653
context 'when issues have one label' do
- scenario 'Are sorted properly' do
+ it 'Are sorted properly' do
# Issues
issue_1 = create(:issue, title: 'issue_1', project: project)
issue_2 = create(:issue, title: 'issue_2', project: project)
@@ -43,7 +43,7 @@ feature 'Issue prioritization' do
end
context 'when issues have multiple labels' do
- scenario 'Are sorted properly' do
+ it 'Are sorted properly' do
# Issues
issue_1 = create(:issue, title: 'issue_1', project: project)
issue_2 = create(:issue, title: 'issue_2', project: project)
diff --git a/spec/features/projects/labels/subscription_spec.rb b/spec/features/projects/labels/subscription_spec.rb
index 70e8d436dcb..49227eebf3d 100644
--- a/spec/features/projects/labels/subscription_spec.rb
+++ b/spec/features/projects/labels/subscription_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Labels subscription' do
+describe 'Labels subscription' do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, :public, namespace: group) }
@@ -13,7 +13,7 @@ feature 'Labels subscription' do
sign_in user
end
- scenario 'users can subscribe/unsubscribe to labels', :js do
+ it 'users can subscribe/unsubscribe to labels', :js do
visit project_labels_path(project)
expect(page).to have_content('bug')
@@ -36,7 +36,7 @@ feature 'Labels subscription' do
within "#group_label_#{feature.id}" do
expect(page).not_to have_button 'Unsubscribe'
- click_link_on_dropdown('Group level')
+ click_link_on_dropdown('Subscribe at group level')
expect(page).not_to have_selector('.dropdown-group-label')
expect(page).to have_button 'Unsubscribe'
@@ -45,7 +45,7 @@ feature 'Labels subscription' do
expect(page).to have_selector('.dropdown-group-label')
- click_link_on_dropdown('Project level')
+ click_link_on_dropdown('Subscribe at project level')
expect(page).not_to have_selector('.dropdown-group-label')
expect(page).to have_button 'Unsubscribe'
@@ -68,7 +68,7 @@ feature 'Labels subscription' do
find('.dropdown-group-label').click
page.within('.dropdown-group-label') do
- find('a.js-subscribe-button', text: text).click
+ find('.js-subscribe-button', text: text).click
end
end
end
diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb
index ae8b1364ec7..996040fde02 100644
--- a/spec/features/projects/labels/update_prioritization_spec.rb
+++ b/spec/features/projects/labels/update_prioritization_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Prioritize labels' do
+describe 'Prioritize labels' do
include DragTo
let(:user) { create(:user) }
@@ -17,7 +17,7 @@ feature 'Prioritize labels' do
sign_in user
end
- scenario 'user can prioritize a group label', :js do
+ it '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 do
+ it '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 do
+ it '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 do
+ it '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 do
+ it '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)
@@ -102,16 +102,16 @@ feature 'Prioritize labels' do
drag_to(selector: '.label-list-item', from_index: 1, to_index: 2)
page.within('.prioritized-labels') do
- expect(first('li')).to have_content('feature')
- expect(page.all('li').last).to have_content('bug')
+ expect(first('.label-list-item')).to have_content('feature')
+ expect(page.all('.label-list-item').last).to have_content('bug')
end
refresh
wait_for_requests
page.within('.prioritized-labels') do
- expect(first('li')).to have_content('feature')
- expect(page.all('li').last).to have_content('bug')
+ expect(first('.label-list-item')).to have_content('feature')
+ expect(page.all('.label-list-item').last).to have_content('bug')
end
end
diff --git a/spec/features/projects/labels/user_creates_labels_spec.rb b/spec/features/projects/labels/user_creates_labels_spec.rb
index 9fd7f3ee775..c71b04fea09 100644
--- a/spec/features/projects/labels/user_creates_labels_spec.rb
+++ b/spec/features/projects/labels/user_creates_labels_spec.rb
@@ -18,7 +18,7 @@ describe "User creates labels" do
context "in project" do
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit(new_project_label_path(project))
@@ -69,7 +69,7 @@ describe "User creates labels" do
before do
create(:label, project: project, title: "bug") # Create label for `project` (not `another_project`) project.
- another_project.add_master(user)
+ another_project.add_maintainer(user)
sign_in(user)
visit(new_project_label_path(another_project))
diff --git a/spec/features/projects/labels/user_edits_labels_spec.rb b/spec/features/projects/labels/user_edits_labels_spec.rb
index d1041ff5c1e..0708bbd40ce 100644
--- a/spec/features/projects/labels/user_edits_labels_spec.rb
+++ b/spec/features/projects/labels/user_edits_labels_spec.rb
@@ -6,7 +6,7 @@ describe "User edits labels" do
set(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit(edit_project_label_path(project, label))
diff --git a/spec/features/projects/labels/user_removes_labels_spec.rb b/spec/features/projects/labels/user_removes_labels_spec.rb
index f4fda6de465..b0ce03a1c31 100644
--- a/spec/features/projects/labels/user_removes_labels_spec.rb
+++ b/spec/features/projects/labels/user_removes_labels_spec.rb
@@ -5,7 +5,7 @@ describe "User removes labels" do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
@@ -17,8 +17,9 @@ describe "User removes labels" do
end
it "removes label" do
- page.within(".labels") do
+ page.within(".other-labels") do
page.first(".label-list-item") do
+ first('.js-label-options-dropdown').click
first(".remove-row").click
first(:link, "Delete label").click
end
@@ -36,17 +37,16 @@ describe "User removes labels" do
end
it "removes all labels" do
- page.within(".labels") do
- loop do
- li = page.first(".label-list-item")
- break unless li
+ loop do
+ li = page.first(".label-list-item")
+ break unless li
- li.click_link("Delete")
- click_link("Delete label")
- end
-
- expect(page).to have_content("Generate a default set of labels").and have_content("New label")
+ li.find('.js-label-options-dropdown').click
+ li.click_button("Delete")
+ click_link("Delete label")
end
+
+ expect(page).to have_content("Generate a default set of labels").and have_content("New label")
end
end
end
diff --git a/spec/features/projects/labels/user_sees_links_to_issuables.rb b/spec/features/projects/labels/user_sees_links_to_issuables.rb
index aa56fd7f74e..c404fc8d66f 100644
--- a/spec/features/projects/labels/user_sees_links_to_issuables.rb
+++ b/spec/features/projects/labels/user_sees_links_to_issuables.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Projects > Labels > User sees links to issuables' do
+describe 'Projects > Labels > User sees links to issuables' do
set(:user) { create(:user) }
before do
@@ -16,7 +16,7 @@ feature 'Projects > Labels > User sees links to issuables' do
context 'when merge requests and issues are enabled for the project' do
let(:project) { create(:project, :public) }
- scenario 'shows links to MRs and issues' do
+ it 'shows links to MRs and issues' do
expect(page).to have_link('view merge requests')
expect(page).to have_link('view open issues')
end
@@ -25,7 +25,7 @@ feature 'Projects > Labels > User sees links to issuables' do
context 'when issues are disabled for the project' do
let(:project) { create(:project, :public, issues_access_level: ProjectFeature::DISABLED) }
- scenario 'shows links to MRs but not to issues' do
+ it 'shows links to MRs but not to issues' do
expect(page).to have_link('view merge requests')
expect(page).not_to have_link('view open issues')
end
@@ -34,7 +34,7 @@ feature 'Projects > Labels > User sees links to issuables' do
context 'when merge requests are disabled for the project' do
let(:project) { create(:project, :public, merge_requests_access_level: ProjectFeature::DISABLED) }
- scenario 'shows links to issues but not to MRs' do
+ it 'shows links to issues but not to MRs' do
expect(page).not_to have_link('view merge requests')
expect(page).to have_link('view open issues')
end
@@ -48,7 +48,7 @@ feature 'Projects > Labels > User sees links to issuables' do
context 'when merge requests and issues are enabled for the project' do
let(:project) { create(:project, :public, namespace: group) }
- scenario 'shows links to MRs and issues' do
+ it 'shows links to MRs and issues' do
expect(page).to have_link('view merge requests')
expect(page).to have_link('view open issues')
end
@@ -57,7 +57,7 @@ feature 'Projects > Labels > User sees links to issuables' do
context 'when issues are disabled for the project' do
let(:project) { create(:project, :public, namespace: group, issues_access_level: ProjectFeature::DISABLED) }
- scenario 'shows links to MRs and issues' do
+ it 'shows links to MRs and issues' do
expect(page).to have_link('view merge requests')
expect(page).to have_link('view open issues')
end
@@ -66,7 +66,7 @@ feature 'Projects > Labels > User sees links to issuables' do
context 'when merge requests are disabled for the project' do
let(:project) { create(:project, :public, namespace: group, merge_requests_access_level: ProjectFeature::DISABLED) }
- scenario 'shows links to MRs and issues' do
+ it 'shows links to MRs and issues' do
expect(page).to have_link('view merge requests')
expect(page).to have_link('view open issues')
end
diff --git a/spec/features/projects/members/anonymous_user_sees_members_spec.rb b/spec/features/projects/members/anonymous_user_sees_members_spec.rb
index e2a48bfd1d4..b3ed725f602 100644
--- a/spec/features/projects/members/anonymous_user_sees_members_spec.rb
+++ b/spec/features/projects/members/anonymous_user_sees_members_spec.rb
@@ -1,16 +1,16 @@
require 'spec_helper'
-feature 'Projects > Members > Anonymous user sees members' do
+describe 'Projects > Members > Anonymous user sees members' do
let(:user) { create(:user) }
let(:group) { create(:group, :public) }
let(:project) { create(:project, :public) }
- background do
- project.add_master(user)
+ before do
+ project.add_maintainer(user)
create(:project_group_link, project: project, group: group)
end
- scenario "anonymous user visits the project's members page and sees the list of members" do
+ it "anonymous user visits the project's members page and sees the list of members" do
visit project_project_members_path(project)
expect(current_path).to eq(
diff --git a/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb b/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb
index 6b450fa4e45..0ab29660189 100644
--- a/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb
+++ b/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb
@@ -1,17 +1,17 @@
require 'spec_helper'
-feature 'Projects > Members > Group member cannot leave group project' do
+describe 'Projects > Members > Group member cannot leave group project' do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, namespace: group) }
- background do
+ before do
group.add_developer(user)
sign_in(user)
visit project_path(project)
end
- scenario 'user does not see a "Leave project" link' do
+ it 'user does not see a "Leave project" link' do
expect(page).not_to have_content 'Leave project'
end
end
diff --git a/spec/features/projects/members/group_member_cannot_request_access_to_his_group_project_spec.rb b/spec/features/projects/members/group_member_cannot_request_access_to_his_group_project_spec.rb
index 296a80a3c60..bb475ea95e5 100644
--- a/spec/features/projects/members/group_member_cannot_request_access_to_his_group_project_spec.rb
+++ b/spec/features/projects/members/group_member_cannot_request_access_to_his_group_project_spec.rb
@@ -1,39 +1,39 @@
require 'spec_helper'
-feature 'Projects > Members > Group member cannot request access to his group project' do
+describe 'Projects > Members > Group member cannot request access to his group project' do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, namespace: group) }
- scenario 'owner does not see the request access button' do
+ it 'owner does not see the request access button' do
group.add_owner(user)
login_and_visit_project_page(user)
expect(page).not_to have_content 'Request Access'
end
- scenario 'master does not see the request access button' do
- group.add_master(user)
+ it 'maintainer does not see the request access button' do
+ group.add_maintainer(user)
login_and_visit_project_page(user)
expect(page).not_to have_content 'Request Access'
end
- scenario 'developer does not see the request access button' do
+ it 'developer does not see the request access button' do
group.add_developer(user)
login_and_visit_project_page(user)
expect(page).not_to have_content 'Request Access'
end
- scenario 'reporter does not see the request access button' do
+ it 'reporter does not see the request access button' do
group.add_reporter(user)
login_and_visit_project_page(user)
expect(page).not_to have_content 'Request Access'
end
- scenario 'guest does not see the request access button' do
+ it 'guest does not see the request access button' do
group.add_guest(user)
login_and_visit_project_page(user)
diff --git a/spec/features/projects/members/group_members_spec.rb b/spec/features/projects/members/group_members_spec.rb
index e22b6fa6c43..41b2beb40b9 100644
--- a/spec/features/projects/members/group_members_spec.rb
+++ b/spec/features/projects/members/group_members_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Projects members' do
+describe 'Projects members' do
let(:user) { create(:user) }
let(:developer) { create(:user) }
let(:group) { create(:group, :public, :access_requestable) }
@@ -10,7 +10,7 @@ feature 'Projects members' do
let(:project_requester) { create(:user) }
let(:group_requester) { create(:user) }
- background do
+ before do
project.add_developer(developer)
group.add_owner(user)
sign_in(user)
@@ -22,7 +22,7 @@ feature 'Projects members' do
visit project_settings_members_path(project)
end
- scenario 'does not appear in the project members page' do
+ it 'does not appear in the project members page' do
page.within first('.content-list') do
expect(page).not_to have_content('test2@abc.com')
end
@@ -36,7 +36,7 @@ feature 'Projects members' do
visit project_settings_members_path(project)
end
- scenario 'shows the project invitee, the project developer, and the group owner' do
+ it 'shows the project invitee, the project developer, and the group owner' do
page.within first('.content-list') do
expect(page).to have_content('test1@abc.com')
expect(page).not_to have_content('test2@abc.com')
@@ -57,7 +57,7 @@ feature 'Projects members' do
visit project_settings_members_path(project)
end
- scenario 'does not appear in the project members page' do
+ it 'does not appear in the project members page' do
page.within first('.content-list') do
expect(page).not_to have_content(group_requester.name)
end
@@ -71,7 +71,7 @@ feature 'Projects members' do
visit project_settings_members_path(project)
end
- scenario 'shows the project requester, the project developer, and the group owner' do
+ it 'shows the project requester, the project developer, and the group owner' do
page.within first('.content-list') do
expect(page).to have_content(project_requester.name)
expect(page).not_to have_content(group_requester.name)
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 6d729f2f85f..ea3894c92bd 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,12 +1,12 @@
require 'spec_helper'
-feature 'Projects > Members > Group requester cannot request access to project', :js do
+describe '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) }
let(:project) { create(:project, :public, :access_requestable, namespace: group) }
- background do
+ before do
group.add_owner(owner)
sign_in(user)
visit group_path(group)
@@ -14,7 +14,7 @@ feature 'Projects > Members > Group requester cannot request access to project',
visit project_path(project)
end
- scenario 'group requester does not see the request access / withdraw access request button' do
+ it 'group requester does not see the request access / withdraw access request button' do
expect(page).not_to have_content 'Request Access'
expect(page).not_to have_content 'Withdraw Access Request'
end
diff --git a/spec/features/projects/members/groups_with_access_list_spec.rb b/spec/features/projects/members/groups_with_access_list_spec.rb
index e6d0c6e00f8..c0b5d943e96 100644
--- a/spec/features/projects/members/groups_with_access_list_spec.rb
+++ b/spec/features/projects/members/groups_with_access_list_spec.rb
@@ -1,19 +1,19 @@
require 'spec_helper'
-feature 'Projects > Members > Groups with access list', :js do
+describe 'Projects > Members > Groups with access list', :js do
let(:user) { create(:user) }
let(:group) { create(:group, :public) }
let(:project) { create(:project, :public) }
- background do
- project.add_master(user)
+ before do
+ project.add_maintainer(user)
@group_link = create(:project_group_link, project: project, group: group)
sign_in(user)
visit project_settings_members_path(project)
end
- scenario 'updates group access level' do
+ it 'updates group access level' do
click_button @group_link.human_access
page.within '.dropdown-menu' do
@@ -27,7 +27,7 @@ feature 'Projects > Members > Groups with access list', :js do
expect(first('.group_member')).to have_content('Guest')
end
- scenario 'updates expiry date' do
+ it 'updates expiry date' do
tomorrow = Date.today + 3
fill_in "member_expires_at_#{group.id}", with: tomorrow.strftime("%F")
@@ -39,7 +39,7 @@ feature 'Projects > Members > Groups with access list', :js do
end
end
- scenario 'deletes group link' do
+ it 'deletes group link' do
page.within(first('.group_member')) do
accept_confirm { find('.btn-remove').click }
end
@@ -49,7 +49,7 @@ feature 'Projects > Members > Groups with access list', :js do
end
context 'search in existing members (yes, this filters the groups list as well)' do
- scenario 'finds no results' do
+ it 'finds no results' do
page.within '.member-search-form' do
fill_in 'search', with: 'testing 123'
find('.member-search-btn').click
@@ -58,7 +58,7 @@ feature 'Projects > Members > Groups with access list', :js do
expect(page).not_to have_selector('.group_member')
end
- scenario 'finds results' do
+ it 'finds results' do
page.within '.member-search-form' do
fill_in 'search', with: group.name
find('.member-search-btn').click
diff --git a/spec/features/projects/members/list_spec.rb b/spec/features/projects/members/list_spec.rb
index 65b11a1d9e7..c2e980e75b8 100644
--- a/spec/features/projects/members/list_spec.rb
+++ b/spec/features/projects/members/list_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Project members list' do
+describe 'Project members list' do
include Select2Helper
let(:user1) { create(:user, name: 'John Doe') }
@@ -8,12 +8,12 @@ feature 'Project members list' do
let(:group) { create(:group) }
let(:project) { create(:project, namespace: group) }
- background do
+ before do
sign_in(user1)
group.add_owner(user1)
end
- scenario 'show members from project and group' do
+ it 'show members from project and group' do
project.add_developer(user2)
visit_members_page
@@ -22,7 +22,7 @@ feature 'Project members list' do
expect(second_row.text).to include(user2.name)
end
- scenario 'show user once if member of both group and project' do
+ it 'show user once if member of both group and project' do
project.add_developer(user1)
visit_members_page
@@ -31,7 +31,7 @@ feature 'Project members list' do
expect(second_row).to be_blank
end
- scenario 'update user acess level', :js do
+ it 'update user acess level', :js do
project.add_developer(user2)
visit_members_page
@@ -44,7 +44,7 @@ feature 'Project members list' do
end
end
- scenario 'add user to project', :js do
+ it 'add user to project', :js do
visit_members_page
add_user(user2.id, 'Reporter')
@@ -55,7 +55,7 @@ feature 'Project members list' do
end
end
- scenario 'remove user from project', :js do
+ it 'remove user from project', :js do
other_user = create(:user)
project.add_developer(other_user)
@@ -71,7 +71,7 @@ feature 'Project members list' do
expect(project.users).not_to include(other_user)
end
- scenario 'invite user to project', :js do
+ it 'invite user to project', :js do
visit_members_page
add_user('test@example.com', 'Reporter')
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 8fe340d3bae..26de6fb33fd 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,19 +1,19 @@
require 'spec_helper'
-feature 'Projects > Members > Master adds member with expiration date', :js do
+describe 'Projects > Members > Maintainer adds member with expiration date', :js do
include Select2Helper
include ActiveSupport::Testing::TimeHelpers
- let(:master) { create(:user) }
+ let(:maintainer) { create(:user) }
let(:project) { create(:project) }
let!(:new_member) { create(:user) }
- background do
- project.add_master(master)
- sign_in(master)
+ before do
+ project.add_maintainer(maintainer)
+ sign_in(maintainer)
end
- scenario 'expiration date is displayed in the members list' do
+ it 'expiration date is displayed in the members list' do
travel_to Time.zone.parse('2016-08-06 08:00') do
date = 4.days.from_now
visit project_project_members_path(project)
@@ -30,7 +30,7 @@ feature 'Projects > Members > Master adds member with expiration date', :js do
end
end
- scenario 'change expiration date' do
+ it 'change expiration date' do
travel_to Time.zone.parse('2016-08-06 08:00') do
date = 3.days.from_now
project.team.add_users([new_member.id], :developer, expires_at: Date.today.to_s(:medium))
diff --git a/spec/features/projects/members/master_manages_access_requests_spec.rb b/spec/features/projects/members/master_manages_access_requests_spec.rb
index 3ac6ca4fc86..adc8202cde7 100644
--- a/spec/features/projects/members/master_manages_access_requests_spec.rb
+++ b/spec/features/projects/members/master_manages_access_requests_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
-feature 'Projects > Members > Master manages access requests' do
- it_behaves_like 'Master manages access requests' do
+describe 'Projects > Members > Maintainer manages access requests' do
+ it_behaves_like 'Maintainer manages access requests' do
let(:entity) { create(:project, :public, :access_requestable) }
let(:members_page_path) { project_project_members_path(entity) }
end
diff --git a/spec/features/projects/members/member_cannot_request_access_to_his_project_spec.rb b/spec/features/projects/members/member_cannot_request_access_to_his_project_spec.rb
index 47911c32a72..f612ad8d551 100644
--- a/spec/features/projects/members/member_cannot_request_access_to_his_project_spec.rb
+++ b/spec/features/projects/members/member_cannot_request_access_to_his_project_spec.rb
@@ -1,16 +1,16 @@
require 'spec_helper'
-feature 'Projects > Members > Member cannot request access to his project' do
+describe 'Projects > Members > Member cannot request access to his project' do
let(:member) { create(:user) }
let(:project) { create(:project) }
- background do
+ before do
project.add_developer(member)
sign_in(member)
visit project_path(project)
end
- scenario 'member does not see the request access button' do
+ it 'member does not see the request access button' do
expect(page).not_to have_content 'Request Access'
end
end
diff --git a/spec/features/projects/members/member_leaves_project_spec.rb b/spec/features/projects/members/member_leaves_project_spec.rb
index e54c2c76975..94b29de4686 100644
--- a/spec/features/projects/members/member_leaves_project_spec.rb
+++ b/spec/features/projects/members/member_leaves_project_spec.rb
@@ -1,16 +1,16 @@
require 'spec_helper'
-feature 'Projects > Members > Member leaves project' do
+describe 'Projects > Members > Member leaves project' do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
- background do
+ before do
project.add_developer(user)
sign_in(user)
visit project_path(project)
end
- scenario 'user leaves project' do
+ it 'user leaves project' do
click_link 'Leave project'
expect(current_path).to eq(dashboard_projects_path)
diff --git a/spec/features/projects/members/owner_cannot_leave_project_spec.rb b/spec/features/projects/members/owner_cannot_leave_project_spec.rb
index 15162d01c44..0aa005adb4d 100644
--- a/spec/features/projects/members/owner_cannot_leave_project_spec.rb
+++ b/spec/features/projects/members/owner_cannot_leave_project_spec.rb
@@ -1,14 +1,14 @@
require 'spec_helper'
-feature 'Projects > Members > Owner cannot leave project' do
+describe 'Projects > Members > Owner cannot leave project' do
let(:project) { create(:project) }
- background do
+ before do
sign_in(project.owner)
visit project_path(project)
end
- scenario 'user does not see a "Leave project" link' do
+ it 'user does not see a "Leave project" link' do
expect(page).not_to have_content 'Leave project'
end
end
diff --git a/spec/features/projects/members/owner_cannot_request_access_to_his_project_spec.rb b/spec/features/projects/members/owner_cannot_request_access_to_his_project_spec.rb
index c27925c8dc4..eb1b720af05 100644
--- a/spec/features/projects/members/owner_cannot_request_access_to_his_project_spec.rb
+++ b/spec/features/projects/members/owner_cannot_request_access_to_his_project_spec.rb
@@ -1,14 +1,14 @@
require 'spec_helper'
-feature 'Projects > Members > Owner cannot request access to his project' do
+describe 'Projects > Members > Owner cannot request access to his project' do
let(:project) { create(:project) }
- background do
+ before do
sign_in(project.owner)
visit project_path(project)
end
- scenario 'owner does not see the request access button' do
+ it 'owner does not see the request access button' do
expect(page).not_to have_content 'Request Access'
end
end
diff --git a/spec/features/projects/members/share_with_group_spec.rb b/spec/features/projects/members/share_with_group_spec.rb
index 134c8b8bc39..c6d85e5d22f 100644
--- a/spec/features/projects/members/share_with_group_spec.rb
+++ b/spec/features/projects/members/share_with_group_spec.rb
@@ -1,21 +1,21 @@
require 'spec_helper'
-feature 'Project > Members > Share with Group', :js do
+describe 'Project > Members > Share with Group', :js do
include Select2Helper
include ActionView::Helpers::DateHelper
- let(:master) { create(:user) }
+ let(:maintainer) { create(:user) }
describe 'Share with group lock' do
shared_examples 'the project can be shared with groups' do
- scenario 'the "Share with group" tab exists' do
+ it 'the "Share with group" tab exists' do
visit project_settings_members_path(project)
expect(page).to have_selector('#share-with-group-tab')
end
end
shared_examples 'the project cannot be shared with groups' do
- scenario 'the "Share with group" tab does not exist' do
+ it 'the "Share with group" tab does not exist' do
visit project_settings_members_path(project)
expect(page).to have_selector('#add-member-tab')
expect(page).not_to have_selector('#share-with-group-tab')
@@ -26,15 +26,15 @@ feature 'Project > Members > Share with Group', :js do
let!(:group_to_share_with) { create(:group) }
let(:project) { create(:project, namespace: create(:group)) }
- background do
- project.add_master(master)
- sign_in(master)
+ before do
+ project.add_maintainer(maintainer)
+ sign_in(maintainer)
end
context 'when the group has "Share with group lock" disabled' do
it_behaves_like 'the project can be shared with groups'
- scenario 'the project can be shared with another group' do
+ it 'the project can be shared with another group' do
visit project_settings_members_path(project)
click_on 'share-with-group-tab'
@@ -64,9 +64,9 @@ feature 'Project > Members > Share with Group', :js do
let(:subgroup) { create(:group, parent: root_group) }
let(:project) { create(:project, namespace: subgroup) }
- background do
- project.add_master(master)
- sign_in(master)
+ before do
+ project.add_maintainer(maintainer)
+ sign_in(maintainer)
end
context 'when the root_group has "Share with group lock" disabled' do
@@ -112,8 +112,8 @@ feature 'Project > Members > Share with Group', :js do
end
before do
- project.add_master(master)
- sign_in(master)
+ project.add_maintainer(maintainer)
+ sign_in(maintainer)
visit project_settings_members_path(project)
@@ -126,7 +126,7 @@ feature 'Project > Members > Share with Group', :js do
find('.btn-create').click
end
- scenario 'the group link shows the expiration time with a warning class' do
+ it 'the group link shows the expiration time with a warning class' do
page.within('.project-members-groups') do
# Using distance_of_time_in_words_to_now because it is not the same as
# subtraction, and this way avoids time zone issues as well
@@ -141,12 +141,12 @@ feature 'Project > Members > Share with Group', :js do
context 'with multiple groups to choose from' do
let(:project) { create(:project) }
- background do
- project.add_master(master)
- sign_in(master)
+ before do
+ project.add_maintainer(maintainer)
+ sign_in(maintainer)
- create(:group).add_owner(master)
- create(:group).add_owner(master)
+ create(:group).add_owner(maintainer)
+ create(:group).add_owner(maintainer)
visit project_settings_members_path(project)
@@ -173,14 +173,14 @@ feature 'Project > Members > Share with Group', :js do
let!(:group_to_share_with) { create(:group) }
let!(:project) { create(:project, namespace: nested_group) }
- background do
- project.add_master(master)
- sign_in(master)
- group.add_master(master)
- group_to_share_with.add_master(master)
+ before do
+ project.add_maintainer(maintainer)
+ sign_in(maintainer)
+ group.add_maintainer(maintainer)
+ group_to_share_with.add_maintainer(maintainer)
end
- scenario 'the groups dropdown does not show ancestors', :nested_groups do
+ it 'the groups dropdown does not show ancestors', :nested_groups do
visit project_settings_members_path(project)
click_on 'share-with-group-tab'
diff --git a/spec/features/projects/members/sorting_spec.rb b/spec/features/projects/members/sorting_spec.rb
index afa173c59e5..220775b514d 100644
--- a/spec/features/projects/members/sorting_spec.rb
+++ b/spec/features/projects/members/sorting_spec.rb
@@ -1,85 +1,85 @@
require 'spec_helper'
-feature 'Projects > Members > Sorting' do
- let(:master) { create(:user, name: 'John Doe') }
+describe 'Projects > Members > Sorting' do
+ let(:maintainer) { create(:user, name: 'John Doe') }
let(:developer) { create(:user, name: 'Mary Jane', last_sign_in_at: 5.days.ago) }
- let(:project) { create(:project, namespace: master.namespace, creator: master) }
+ let(:project) { create(:project, namespace: maintainer.namespace, creator: maintainer) }
- background do
+ before do
create(:project_member, :developer, user: developer, project: project, created_at: 3.days.ago)
- sign_in(master)
+ sign_in(maintainer)
end
- scenario 'sorts alphabetically by default' do
+ it 'sorts alphabetically by default' do
visit_members_list(sort: nil)
- expect(first_member).to include(master.name)
+ expect(first_member).to include(maintainer.name)
expect(second_member).to include(developer.name)
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending')
end
- scenario 'sorts by access level ascending' do
+ it 'sorts by access level ascending' do
visit_members_list(sort: :access_level_asc)
expect(first_member).to include(developer.name)
- expect(second_member).to include(master.name)
+ expect(second_member).to include(maintainer.name)
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Access level, ascending')
end
- scenario 'sorts by access level descending' do
+ it 'sorts by access level descending' do
visit_members_list(sort: :access_level_desc)
- expect(first_member).to include(master.name)
+ expect(first_member).to include(maintainer.name)
expect(second_member).to include(developer.name)
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Access level, descending')
end
- scenario 'sorts by last joined' do
+ it 'sorts by last joined' do
visit_members_list(sort: :last_joined)
- expect(first_member).to include(master.name)
+ expect(first_member).to include(maintainer.name)
expect(second_member).to include(developer.name)
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Last joined')
end
- scenario 'sorts by oldest joined' do
+ it 'sorts by oldest joined' do
visit_members_list(sort: :oldest_joined)
expect(first_member).to include(developer.name)
- expect(second_member).to include(master.name)
+ expect(second_member).to include(maintainer.name)
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Oldest joined')
end
- scenario 'sorts by name ascending' do
+ it 'sorts by name ascending' do
visit_members_list(sort: :name_asc)
- expect(first_member).to include(master.name)
+ expect(first_member).to include(maintainer.name)
expect(second_member).to include(developer.name)
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending')
end
- scenario 'sorts by name descending' do
+ it 'sorts by name descending' do
visit_members_list(sort: :name_desc)
expect(first_member).to include(developer.name)
- expect(second_member).to include(master.name)
+ expect(second_member).to include(maintainer.name)
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, descending')
end
- scenario 'sorts by recent sign in', :clean_gitlab_redis_shared_state do
+ it 'sorts by recent sign in', :clean_gitlab_redis_shared_state do
visit_members_list(sort: :recent_sign_in)
- expect(first_member).to include(master.name)
+ expect(first_member).to include(maintainer.name)
expect(second_member).to include(developer.name)
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Recent sign in')
end
- scenario 'sorts by oldest sign in', :clean_gitlab_redis_shared_state do
+ it 'sorts by oldest sign in', :clean_gitlab_redis_shared_state do
visit_members_list(sort: :oldest_sign_in)
expect(first_member).to include(developer.name)
- expect(second_member).to include(master.name)
+ expect(second_member).to include(maintainer.name)
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Oldest sign in')
end
diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb
index 672d5daa3d8..50ba67f0ffc 100644
--- a/spec/features/projects/members/user_requests_access_spec.rb
+++ b/spec/features/projects/members/user_requests_access_spec.rb
@@ -1,26 +1,26 @@
require 'spec_helper'
-feature 'Projects > Members > User requests access', :js do
+describe 'Projects > Members > User requests access', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :public, :access_requestable, :repository) }
- let(:master) { project.owner }
+ let(:maintainer) { project.owner }
- background do
+ before do
sign_in(user)
visit project_path(project)
end
- scenario 'request access feature is disabled' do
- project.update_attributes(request_access_enabled: false)
+ it 'request access feature is disabled' do
+ project.update(request_access_enabled: false)
visit project_path(project)
expect(page).not_to have_content 'Request Access'
end
- scenario 'user can request access to a project' do
+ it 'user can request access to a project' do
perform_enqueued_jobs { click_link 'Request Access' }
- expect(ActionMailer::Base.deliveries.last.to).to eq [master.notification_email]
+ expect(ActionMailer::Base.deliveries.last.to).to eq [maintainer.notification_email]
expect(ActionMailer::Base.deliveries.last.subject).to eq "Request to join the #{project.full_name} project"
expect(project.requesters.exists?(user_id: user)).to be_truthy
@@ -31,7 +31,7 @@ feature 'Projects > Members > User requests access', :js do
end
context 'code access is restricted' do
- scenario 'user can request access' do
+ it 'user can request access' do
project.project_feature.update!(repository_access_level: ProjectFeature::PRIVATE,
builds_access_level: ProjectFeature::PRIVATE,
merge_requests_access_level: ProjectFeature::PRIVATE)
@@ -41,7 +41,7 @@ feature 'Projects > Members > User requests access', :js do
end
end
- scenario 'user is not listed in the project members page' do
+ it 'user is not listed in the project members page' do
click_link 'Request Access'
expect(project.requesters.exists?(user_id: user)).to be_truthy
@@ -55,7 +55,7 @@ feature 'Projects > Members > User requests access', :js do
end
end
- scenario 'user can withdraw its request for access' do
+ it 'user can withdraw its request for access' do
click_link 'Request Access'
expect(project.requesters.exists?(user_id: user)).to be_truthy
diff --git a/spec/features/projects/merge_request_button_spec.rb b/spec/features/projects/merge_request_button_spec.rb
index b571d5a0e26..69561b4d733 100644
--- a/spec/features/projects/merge_request_button_spec.rb
+++ b/spec/features/projects/merge_request_button_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Merge Request button' do
+describe 'Merge Request button' do
shared_examples 'Merge request button only shown when allowed' do
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
diff --git a/spec/features/projects/merge_requests/user_closes_merge_request_spec.rb b/spec/features/projects/merge_requests/user_closes_merge_request_spec.rb
index b257f447439..2d12d690151 100644
--- a/spec/features/projects/merge_requests/user_closes_merge_request_spec.rb
+++ b/spec/features/projects/merge_requests/user_closes_merge_request_spec.rb
@@ -6,7 +6,7 @@ describe 'User closes a merge requests', :js do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit(merge_request_path(merge_request))
diff --git a/spec/features/projects/merge_requests/user_comments_on_commit_spec.rb b/spec/features/projects/merge_requests/user_comments_on_commit_spec.rb
index 0a952cfc2a9..8ea358bcc70 100644
--- a/spec/features/projects/merge_requests/user_comments_on_commit_spec.rb
+++ b/spec/features/projects/merge_requests/user_comments_on_commit_spec.rb
@@ -9,7 +9,7 @@ describe 'User comments on a commit', :js do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit(project_commit_path(project, sample_commit.id))
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 e3f90a78cb5..441b080bee5 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
@@ -11,7 +11,7 @@ describe 'User comments on a diff', :js do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit(diffs_project_merge_request_path(project, merge_request))
@@ -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').click
+ find('.js-btn-vue-toggle-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').click
+ find('.js-btn-vue-toggle-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').click
+ find('.js-btn-vue-toggle-comments').click
end
# Now both the comments should be shown.
@@ -91,7 +91,7 @@ describe 'User comments on a diff', :js do
# Check the same comments in the side-by-side view.
execute_script("window.scrollTo(0,0);")
- click_link('Side-by-side')
+ click_button 'Side-by-side'
wait_for_requests
@@ -120,7 +120,7 @@ describe 'User comments on a diff', :js do
click_button('Comment')
end
- page.within('.diff-file:nth-of-type(5) .note') do
+ page.within('.diff-file:nth-of-type(5) .discussion .note') do
find('.js-note-edit').click
page.within('.current-note-edit-form') do
@@ -131,7 +131,7 @@ describe 'User comments on a diff', :js do
expect(page).not_to have_button('Save comment', disabled: true)
end
- page.within('.diff-file:nth-of-type(5) .note') do
+ page.within('.diff-file:nth-of-type(5) .discussion .note') do
expect(page).to have_content('Typo, please fix').and have_no_content('Line is wrong')
end
end
@@ -150,7 +150,7 @@ describe 'User comments on a diff', :js do
expect(page).to have_content('1')
end
- page.within('.diff-file:nth-of-type(5) .note') do
+ page.within('.diff-file:nth-of-type(5) .discussion .note') do
find('.more-actions').click
find('.more-actions .dropdown-menu li', match: :first)
diff --git a/spec/features/projects/merge_requests/user_comments_on_merge_request_spec.rb b/spec/features/projects/merge_requests/user_comments_on_merge_request_spec.rb
index 2eb652147ce..69bdab85d81 100644
--- a/spec/features/projects/merge_requests/user_comments_on_merge_request_spec.rb
+++ b/spec/features/projects/merge_requests/user_comments_on_merge_request_spec.rb
@@ -8,7 +8,7 @@ describe 'User comments on a merge request', :js do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit(merge_request_path(merge_request))
@@ -16,7 +16,7 @@ describe 'User comments on a merge request', :js do
it 'adds a comment' do
page.within('.js-main-target-form') do
- fill_in(:note_note, with: '# Comment with a header')
+ fill_in('note[note]', with: '# Comment with a header')
click_button('Comment')
end
@@ -32,7 +32,6 @@ describe 'User comments on a merge request', :js do
# Add new comment in background in order to check
# if it's going to be loaded automatically for current user.
create(:diff_note_on_merge_request, project: project, noteable: merge_request, author: user, note: 'Line is wrong')
-
# Trigger a refresh of notes.
execute_script("$(document).trigger('visibilitychange');")
wait_for_requests
diff --git a/spec/features/projects/merge_requests/user_creates_merge_request_spec.rb b/spec/features/projects/merge_requests/user_creates_merge_request_spec.rb
index 1f21ef7b382..38b4e4a6d1b 100644
--- a/spec/features/projects/merge_requests/user_creates_merge_request_spec.rb
+++ b/spec/features/projects/merge_requests/user_creates_merge_request_spec.rb
@@ -8,7 +8,7 @@ describe "User creates a merge request", :js do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/features/projects/merge_requests/user_edits_merge_request_spec.rb b/spec/features/projects/merge_requests/user_edits_merge_request_spec.rb
index 3d19a2923b9..7de0f9daac6 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
@@ -8,7 +8,7 @@ describe 'User edits a merge request', :js do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit(edit_project_merge_request_path(project, merge_request))
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 f55eb5c6664..68a835e7f77 100644
--- a/spec/features/projects/merge_requests/user_manages_subscription_spec.rb
+++ b/spec/features/projects/merge_requests/user_manages_subscription_spec.rb
@@ -6,7 +6,7 @@ describe 'User manages subscription', :js do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit(merge_request_path(merge_request))
diff --git a/spec/features/projects/merge_requests/user_reopens_merge_request_spec.rb b/spec/features/projects/merge_requests/user_reopens_merge_request_spec.rb
index ba3c9789da1..745b4537e72 100644
--- a/spec/features/projects/merge_requests/user_reopens_merge_request_spec.rb
+++ b/spec/features/projects/merge_requests/user_reopens_merge_request_spec.rb
@@ -6,7 +6,7 @@ describe 'User reopens a merge requests', :js do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit(merge_request_path(merge_request))
diff --git a/spec/features/projects/merge_requests/user_reverts_merge_request_spec.rb b/spec/features/projects/merge_requests/user_reverts_merge_request_spec.rb
index f3e97bc9eb2..67b6aefb2d8 100644
--- a/spec/features/projects/merge_requests/user_reverts_merge_request_spec.rb
+++ b/spec/features/projects/merge_requests/user_reverts_merge_request_spec.rb
@@ -13,6 +13,8 @@ describe 'User reverts a merge request', :js do
click_button('Merge')
+ wait_for_requests
+
visit(merge_request_path(merge_request))
end
diff --git a/spec/features/projects/merge_requests/user_sorts_merge_requests_spec.rb b/spec/features/projects/merge_requests/user_sorts_merge_requests_spec.rb
index d8d9f7e2a8c..e401933aed2 100644
--- a/spec/features/projects/merge_requests/user_sorts_merge_requests_spec.rb
+++ b/spec/features/projects/merge_requests/user_sorts_merge_requests_spec.rb
@@ -9,7 +9,7 @@ describe 'User sorts merge requests' do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit(project_merge_requests_path(project))
@@ -18,7 +18,7 @@ describe 'User sorts merge requests' do
it 'keeps the sort option' do
find('button.dropdown-toggle').click
- page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do
+ page.within('.content ul.dropdown-menu.dropdown-menu-right li') do
click_link('Last updated')
end
@@ -43,7 +43,7 @@ describe 'User sorts merge requests' do
it 'sorts by popularity' do
find('button.dropdown-toggle').click
- page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do
+ page.within('.content ul.dropdown-menu.dropdown-menu-right li') do
click_link('Popularity')
end
diff --git a/spec/features/projects/merge_requests/user_views_diffs_spec.rb b/spec/features/projects/merge_requests/user_views_diffs_spec.rb
index 295eb02b625..b1bfe9e5de3 100644
--- a/spec/features/projects/merge_requests/user_views_diffs_spec.rb
+++ b/spec/features/projects/merge_requests/user_views_diffs_spec.rb
@@ -16,7 +16,7 @@ describe 'User views diffs', :js do
it 'unfolds diffs' do
first('.js-unfold').click
- expect(first('.text-file')).to have_content('.bundle')
+ expect(find('.file-holder[id="a5cc2925ca8258af241be7e5b0381edf30266302"] .text-file')).to have_content('.bundle')
end
end
@@ -26,13 +26,17 @@ describe 'User views diffs', :js do
expect(page).to have_css('#inline-diff-btn', count: 1)
end
+ it 'hides loading spinner after load' do
+ expect(page).not_to have_selector('.mr-loading-status .loading', visible: true)
+ end
+
context 'when in the inline view' do
include_examples 'unfold diffs'
end
context 'when in the side-by-side view' do
before do
- click_link('Side-by-side')
+ click_button 'Side-by-side'
wait_for_requests
end
@@ -41,6 +45,14 @@ describe 'User views diffs', :js do
expect(page).to have_css('.parallel')
end
+ it 'toggles container class' do
+ expect(page).not_to have_css('.content-wrapper > .container-fluid.container-limited')
+
+ click_link 'Commits'
+
+ expect(page).to have_css('.content-wrapper > .container-fluid.container-limited')
+ end
+
include_examples 'unfold diffs'
end
end
diff --git a/spec/features/projects/merge_requests/user_views_open_merge_request_spec.rb b/spec/features/projects/merge_requests/user_views_open_merge_request_spec.rb
index 3aac93eaf7c..6ac495aa03d 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
@@ -31,7 +31,7 @@ describe 'User views an open merge request' do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit(edit_project_merge_request_path(project, merge_request))
diff --git a/spec/features/projects/milestones/milestone_spec.rb b/spec/features/projects/milestones/milestone_spec.rb
index 30de3e83fbb..ff31092b910 100644
--- a/spec/features/projects/milestones/milestone_spec.rb
+++ b/spec/features/projects/milestones/milestone_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Project milestone' do
+describe 'Project milestone' do
let(:user) { create(:user) }
let(:project) { create(:project, name: 'test', namespace: user.namespace) }
let(:milestone) { create(:milestone, project: project) }
@@ -17,8 +17,8 @@ feature 'Project milestone' do
it 'shows issues tab' do
within('#content-body') do
expect(page).to have_link 'Issues', href: '#tab-issues'
- expect(page).to have_selector '.nav-links li.active', count: 1
- expect(find('.nav-links li.active')).to have_content 'Issues'
+ expect(page).to have_selector '.nav-links li a.active', count: 1
+ expect(find('.nav-links li a.active')).to have_content 'Issues'
end
end
@@ -44,8 +44,8 @@ feature 'Project milestone' do
it 'hides issues tab' do
within('#content-body') do
expect(page).not_to have_link 'Issues', href: '#tab-issues'
- expect(page).to have_selector '.nav-links li.active', count: 1
- expect(find('.nav-links li.active')).to have_content 'Merge Requests'
+ expect(page).to have_selector '.nav-links li a.active', count: 1
+ expect(find('.nav-links li a.active')).to have_content 'Merge Requests'
end
end
diff --git a/spec/features/projects/milestones/milestones_sorting_spec.rb b/spec/features/projects/milestones/milestones_sorting_spec.rb
index b64786d4eec..dc711377e6e 100644
--- a/spec/features/projects/milestones/milestones_sorting_spec.rb
+++ b/spec/features/projects/milestones/milestones_sorting_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Milestones sorting', :js do
+describe 'Milestones sorting', :js do
let(:user) { create(:user) }
let(:project) { create(:project, name: 'test', namespace: user.namespace) }
@@ -17,7 +17,7 @@ feature 'Milestones sorting', :js do
sign_in(user)
end
- scenario 'visit project milestones and sort by due_date_asc' do
+ it 'visit project milestones and sort by due_date_asc' do
visit project_milestones_path(project)
expect(page).to have_button('Due soon')
diff --git a/spec/features/projects/milestones/new_spec.rb b/spec/features/projects/milestones/new_spec.rb
index f7900210fe6..0b5ab547dce 100644
--- a/spec/features/projects/milestones/new_spec.rb
+++ b/spec/features/projects/milestones/new_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Creating a new project milestone', :js do
+describe 'Creating a new project milestone', :js do
let(:user) { create(:user) }
let(:project) { create(:project, name: 'test', namespace: user.namespace) }
@@ -9,9 +9,9 @@ feature 'Creating a new project milestone', :js do
visit new_project_milestone_path(project)
end
- it 'description has autocomplete' do
+ it 'description has emoji autocomplete' do
find('#milestone_description').native.send_keys('')
- fill_in 'milestone_description', with: '@'
+ fill_in 'milestone_description', with: ':'
expect(page).to have_selector('.atwho-view')
end
diff --git a/spec/features/projects/milestones/user_interacts_with_labels_spec.rb b/spec/features/projects/milestones/user_interacts_with_labels_spec.rb
index f6a82f80d65..a6d58be7b13 100644
--- a/spec/features/projects/milestones/user_interacts_with_labels_spec.rb
+++ b/spec/features/projects/milestones/user_interacts_with_labels_spec.rb
@@ -11,7 +11,7 @@ describe 'User interacts with labels' do
let(:label_enhancement) { create(:label, project: project, title: 'enhancement') }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
issue1.labels << [label_bug, label_feature]
diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb
index fee6287558e..df8528e79dd 100644
--- a/spec/features/projects/new_project_spec.rb
+++ b/spec/features/projects/new_project_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'New project' do
+describe 'New project' do
include Select2Helper
let(:user) { create(:admin) }
@@ -48,6 +48,15 @@ feature 'New project' do
end
end
+ context 'Readme selector' do
+ it 'shows the initialize with Readme checkbox' do
+ visit new_project_path
+
+ expect(page).to have_css('input#project_initialize_with_readme')
+ expect(page).to have_content('Initialize repository with a README')
+ end
+ end
+
context 'Namespace selector' do
context 'with user namespace' do
before do
@@ -85,7 +94,7 @@ feature 'New project' do
let(:subgroup) { create(:group, parent: group) }
before do
- group.add_master(user)
+ group.add_maintainer(user)
visit new_project_path(namespace_id: subgroup.id)
end
diff --git a/spec/features/projects/pages_spec.rb b/spec/features/projects/pages_spec.rb
index bdd49f731c7..831f22a0e69 100644
--- a/spec/features/projects/pages_spec.rb
+++ b/spec/features/projects/pages_spec.rb
@@ -1,11 +1,11 @@
require 'spec_helper'
-feature 'Pages' do
- given(:project) { create(:project) }
- given(:user) { create(:user) }
- given(:role) { :master }
+describe 'Pages' do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:role) { :maintainer }
- background do
+ before do
allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
project.add_role(user, role)
@@ -14,7 +14,7 @@ feature 'Pages' do
end
shared_examples 'no pages deployed' do
- scenario 'does not see anything to destroy' do
+ it 'does not see anything to destroy' do
visit project_pages_path(project)
expect(page).to have_content('Configure pages')
@@ -24,16 +24,16 @@ feature 'Pages' do
end
context 'when user is the owner' do
- background do
+ before do
project.namespace.update(owner: user)
end
context 'when pages deployed' do
- background do
+ before do
allow_any_instance_of(Project).to receive(:pages_deployed?) { true }
end
- scenario 'renders Access pages' do
+ it 'renders Access pages' do
visit project_pages_path(project)
expect(page).to have_content('Access pages')
@@ -48,7 +48,7 @@ feature 'Pages' do
end
context 'when pages are exposed on external HTTP address', :http_pages_enabled do
- given(:project) { create(:project, pages_https_only: false) }
+ let(:project) { create(:project, pages_https_only: false) }
shared_examples 'adds new domain' do
it 'adds new domain' do
@@ -210,11 +210,11 @@ feature 'Pages' do
context 'when the user is not the owner' do
context 'when pages deployed' do
- background do
+ before do
allow_any_instance_of(Project).to receive(:pages_deployed?) { true }
end
- scenario 'sees "Only the project owner can remove pages" text' do
+ it 'sees "Only the project owner can remove pages" text' do
visit project_pages_path(project)
expect(page).to have_text('Only the project owner can remove pages')
@@ -225,13 +225,13 @@ feature 'Pages' do
end
describe 'HTTPS settings', :js, :https_pages_enabled do
- background do
+ before do
project.namespace.update(owner: user)
allow_any_instance_of(Project).to receive(:pages_deployed?) { true }
end
- scenario 'tries to change the setting' do
+ it 'tries to change the setting' do
visit project_pages_path(project)
expect(page).to have_content("Force domains with SSL certificates to use HTTPS")
@@ -251,7 +251,7 @@ feature 'Pages' do
allow(service).to receive(:execute).and_return(status: :error)
end
- scenario 'tries to change the setting' do
+ it 'tries to change the setting' do
visit project_pages_path(project)
uncheck :project_pages_https_only
@@ -263,13 +263,13 @@ feature 'Pages' do
end
context 'non-HTTPS domain exists' do
- given(:project) { create(:project, pages_https_only: false) }
+ let(:project) { create(:project, pages_https_only: false) }
before do
create(:pages_domain, :without_key, :without_certificate, project: project)
end
- scenario 'the setting is disabled' do
+ it 'the setting is disabled' do
visit project_pages_path(project)
expect(page).to have_field(:project_pages_https_only, disabled: true)
@@ -278,7 +278,7 @@ feature 'Pages' do
end
context 'HTTPS pages are disabled', :https_pages_disabled do
- scenario 'the setting is unavailable' do
+ it 'the setting is unavailable' do
visit project_pages_path(project)
expect(page).not_to have_field(:project_pages_https_only)
@@ -314,8 +314,8 @@ feature 'Pages' do
project: project,
pipeline: pipeline,
ref: 'HEAD',
- legacy_artifacts_file: fixture_file_upload(Rails.root.join('spec/fixtures/pages.zip')),
- legacy_artifacts_metadata: fixture_file_upload(Rails.root.join('spec/fixtures/pages.zip.meta'))
+ legacy_artifacts_file: fixture_file_upload(File.join('spec/fixtures/pages.zip')),
+ legacy_artifacts_metadata: fixture_file_upload(File.join('spec/fixtures/pages.zip.meta'))
)
end
diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb
index 065d00d51d4..ee6b67b2188 100644
--- a/spec/features/projects/pipeline_schedules_spec.rb
+++ b/spec/features/projects/pipeline_schedules_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Pipeline Schedules', :js do
+describe 'Pipeline Schedules', :js do
include PipelineSchedulesHelper
let!(:project) { create(:project, :repository) }
@@ -9,9 +9,9 @@ feature 'Pipeline Schedules', :js do
let(:scope) { nil }
let!(:user) { create(:user) }
- context 'logged in as master' do
+ context 'logged in as maintainer' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
gitlab_sign_in(user)
end
@@ -155,7 +155,7 @@ feature 'Pipeline Schedules', :js do
end
context 'when user creates a new pipeline schedule with variables' do
- background do
+ before do
visit_pipelines_schedules
click_link 'New schedule'
fill_in_schedule_form
@@ -166,7 +166,7 @@ feature 'Pipeline Schedules', :js do
save_pipeline_schedule
end
- scenario 'user sees the new variable in edit window' do
+ it 'user sees the new variable in edit window' do
find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
page.within('.ci-variable-list') do
expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-key").value).to eq('AAA')
@@ -178,7 +178,7 @@ feature 'Pipeline Schedules', :js do
end
context 'when user edits a variable of a pipeline schedule' do
- background do
+ before do
create(:ci_pipeline_schedule, project: project, owner: user).tap do |pipeline_schedule|
create(:ci_pipeline_schedule_variable, key: 'AAA', value: 'AAA123', pipeline_schedule: pipeline_schedule)
end
@@ -192,7 +192,7 @@ feature 'Pipeline Schedules', :js do
click_button 'Save pipeline schedule'
end
- scenario 'user sees the updated variable in edit window' do
+ it 'user sees the updated variable in edit window' do
find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
page.within('.ci-variable-list') do
expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-key").value).to eq('foo')
@@ -202,7 +202,7 @@ feature 'Pipeline Schedules', :js do
end
context 'when user removes a variable of a pipeline schedule' do
- background do
+ before do
create(:ci_pipeline_schedule, project: project, owner: user).tap do |pipeline_schedule|
create(:ci_pipeline_schedule_variable, key: 'AAA', value: 'AAA123', pipeline_schedule: pipeline_schedule)
end
@@ -213,7 +213,7 @@ feature 'Pipeline Schedules', :js do
click_button 'Save pipeline schedule'
end
- scenario 'user does not see the removed variable in edit window' do
+ it 'user does not see the removed variable in edit window' do
find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
page.within('.ci-variable-list') do
expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-key").value).to eq('')
@@ -223,13 +223,13 @@ feature 'Pipeline Schedules', :js do
end
context 'when active is true and next_run_at is NULL' do
- background do
+ before do
create(:ci_pipeline_schedule, project: project, owner: user).tap do |pipeline_schedule|
pipeline_schedule.update_attribute(:cron, nil) # Consequently next_run_at will be nil
end
end
- scenario 'user edit and recover the problematic pipeline schedule' do
+ it 'user edit and recover the problematic pipeline schedule' do
visit_pipelines_schedules
find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
fill_in 'schedule_cron', with: '* 1 2 3 4'
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index a29c21f6fef..ecc7cf84138 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -165,7 +165,7 @@ describe 'Pipeline', :js do
end
it 'shows Pipeline tab as active' do
- expect(page).to have_css('.js-pipeline-tab-link.active')
+ expect(page).to have_css('.js-pipeline-tab-link .active')
end
context 'without permission to access builds' do
@@ -271,7 +271,7 @@ describe 'Pipeline', :js do
end
it 'shows Jobs tab as active' do
- expect(page).to have_css('li.js-builds-tab-link.active')
+ expect(page).to have_css('li.js-builds-tab-link .active')
end
end
@@ -344,6 +344,16 @@ describe 'Pipeline', :js do
it 'shows build failure logs' do
expect(page).to have_content('4 examples, 1 failure')
end
+
+ it 'shows the failure reason' do
+ expect(page).to have_content('There is an unknown failure, please try again')
+ end
+
+ it 'shows retry button for failed build' do
+ page.within(find('.build-failures', match: :first)) do
+ expect(page).to have_link('Retry')
+ end
+ end
end
context 'when missing build logs' do
@@ -379,7 +389,7 @@ describe 'Pipeline', :js do
end
it 'fails to access the page' do
- expect(page).to have_content('Access Denied')
+ expect(page).to have_title('Access Denied')
end
end
end
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index 9c165b17704..4a83bcc3efb 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -595,7 +595,7 @@ describe 'Pipelines', :js do
before do
create(:ci_empty_pipeline, status: 'success', project: project, sha: project.commit.id, ref: 'master')
- project.add_master(user)
+ project.add_maintainer(user)
visit project_pipelines_path(project)
end
@@ -606,7 +606,7 @@ describe 'Pipelines', :js do
describe 'user clicks the button' do
context 'when project already has jobs_cache_index' do
before do
- project.update_attributes(jobs_cache_index: 1)
+ project.update(jobs_cache_index: 1)
end
it 'increments jobs_cache_index' do
diff --git a/spec/features/projects/remote_mirror_spec.rb b/spec/features/projects/remote_mirror_spec.rb
index 81a6b613cc8..5259a8942dc 100644
--- a/spec/features/projects/remote_mirror_spec.rb
+++ b/spec/features/projects/remote_mirror_spec.rb
@@ -1,19 +1,19 @@
require 'spec_helper'
-feature 'Project remote mirror', :feature do
+describe 'Project remote mirror', :feature do
let(:project) { create(:project, :repository, :remote_mirror) }
let(:remote_mirror) { project.remote_mirrors.first }
let(:user) { create(:user) }
describe 'On a project', :js do
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in user
end
context 'when last_error is present but last_update_at is not' do
it 'renders error message without timstamp' do
- remote_mirror.update_attributes(last_error: 'Some new error', last_update_at: nil)
+ remote_mirror.update(last_error: 'Some new error', last_update_at: nil)
visit project_mirror_path(project)
@@ -23,7 +23,7 @@ feature 'Project remote mirror', :feature do
context 'when last_error and last_update_at are present' do
it 'renders error message with timestamp' do
- remote_mirror.update_attributes(last_error: 'Some new error', last_update_at: Time.now - 5.minutes)
+ remote_mirror.update(last_error: 'Some new error', last_update_at: Time.now - 5.minutes)
visit project_mirror_path(project)
diff --git a/spec/features/projects/services/user_activates_asana_spec.rb b/spec/features/projects/services/user_activates_asana_spec.rb
index db836d2985c..c44e07dd3b4 100644
--- a/spec/features/projects/services/user_activates_asana_spec.rb
+++ b/spec/features/projects/services/user_activates_asana_spec.rb
@@ -5,7 +5,7 @@ describe 'User activates Asana' do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit(project_settings_integrations_path(project))
diff --git a/spec/features/projects/services/user_activates_assembla_spec.rb b/spec/features/projects/services/user_activates_assembla_spec.rb
index f099b332785..9c3884a7c74 100644
--- a/spec/features/projects/services/user_activates_assembla_spec.rb
+++ b/spec/features/projects/services/user_activates_assembla_spec.rb
@@ -5,7 +5,7 @@ describe 'User activates Assembla' do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit(project_settings_integrations_path(project))
diff --git a/spec/features/projects/services/user_activates_atlassian_bamboo_ci_spec.rb b/spec/features/projects/services/user_activates_atlassian_bamboo_ci_spec.rb
index a00c2e0ad99..19573565265 100644
--- a/spec/features/projects/services/user_activates_atlassian_bamboo_ci_spec.rb
+++ b/spec/features/projects/services/user_activates_atlassian_bamboo_ci_spec.rb
@@ -5,7 +5,7 @@ describe 'User activates Atlassian Bamboo CI' do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit(project_settings_integrations_path(project))
diff --git a/spec/features/projects/services/user_activates_emails_on_push_spec.rb b/spec/features/projects/services/user_activates_emails_on_push_spec.rb
index 3769875b29c..cc55f7b2060 100644
--- a/spec/features/projects/services/user_activates_emails_on_push_spec.rb
+++ b/spec/features/projects/services/user_activates_emails_on_push_spec.rb
@@ -5,7 +5,7 @@ describe 'User activates Emails on push' do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit(project_settings_integrations_path(project))
diff --git a/spec/features/projects/services/user_activates_flowdock_spec.rb b/spec/features/projects/services/user_activates_flowdock_spec.rb
index 5298d8acaf5..f981b7e9da9 100644
--- a/spec/features/projects/services/user_activates_flowdock_spec.rb
+++ b/spec/features/projects/services/user_activates_flowdock_spec.rb
@@ -5,7 +5,7 @@ describe 'User activates Flowdock' do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit(project_settings_integrations_path(project))
diff --git a/spec/features/projects/services/user_activates_hipchat_spec.rb b/spec/features/projects/services/user_activates_hipchat_spec.rb
index a9bf16642c7..2f5313c91f9 100644
--- a/spec/features/projects/services/user_activates_hipchat_spec.rb
+++ b/spec/features/projects/services/user_activates_hipchat_spec.rb
@@ -5,7 +5,7 @@ describe 'User activates HipChat' do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit(project_settings_integrations_path(project))
diff --git a/spec/features/projects/services/user_activates_irker_spec.rb b/spec/features/projects/services/user_activates_irker_spec.rb
index 435663c818f..4c8e321b411 100644
--- a/spec/features/projects/services/user_activates_irker_spec.rb
+++ b/spec/features/projects/services/user_activates_irker_spec.rb
@@ -5,7 +5,7 @@ describe 'User activates Irker (IRC gateway)' do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit(project_settings_integrations_path(project))
diff --git a/spec/features/projects/services/user_activates_issue_tracker_spec.rb b/spec/features/projects/services/user_activates_issue_tracker_spec.rb
index e9502178bd7..7cd5b12802b 100644
--- a/spec/features/projects/services/user_activates_issue_tracker_spec.rb
+++ b/spec/features/projects/services/user_activates_issue_tracker_spec.rb
@@ -15,7 +15,7 @@ describe 'User activates issue tracker', :js do
end
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit project_settings_integrations_path(project)
diff --git a/spec/features/projects/services/user_activates_jetbrains_teamcity_ci_spec.rb b/spec/features/projects/services/user_activates_jetbrains_teamcity_ci_spec.rb
index 1048803fde8..28d83a8b961 100644
--- a/spec/features/projects/services/user_activates_jetbrains_teamcity_ci_spec.rb
+++ b/spec/features/projects/services/user_activates_jetbrains_teamcity_ci_spec.rb
@@ -5,7 +5,7 @@ describe 'User activates JetBrains TeamCity CI' do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit(project_settings_integrations_path(project))
diff --git a/spec/features/projects/services/user_activates_jira_spec.rb b/spec/features/projects/services/user_activates_jira_spec.rb
index 429128ec096..08e1855d034 100644
--- a/spec/features/projects/services/user_activates_jira_spec.rb
+++ b/spec/features/projects/services/user_activates_jira_spec.rb
@@ -17,7 +17,7 @@ describe 'User activates Jira', :js do
end
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit project_settings_integrations_path(project)
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 b2906e315f7..25b74cc481d 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
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Setup Mattermost slash commands', :js do
+describe 'Setup Mattermost slash commands', :js do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:service) { project.create_mattermost_slash_commands_service }
@@ -8,7 +8,7 @@ feature 'Setup Mattermost slash commands', :js do
before do
stub_mattermost_setting(enabled: mattermost_enabled)
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit edit_project_service_path(project, service)
end
@@ -64,7 +64,7 @@ feature 'Setup Mattermost slash commands', :js do
click_link 'Add to Mattermost'
expect(page).to have_content('The team where the slash commands will be used in')
- expect(page).to have_content('This is the only available team.')
+ expect(page).to have_content('This is the only available team that you are a member of.')
end
it 'shows a disabled prefilled select if user is a member of 1 team' do
@@ -94,7 +94,7 @@ feature 'Setup Mattermost slash commands', :js do
click_link 'Add to Mattermost'
expect(page).to have_content('Select the team where the slash commands will be used in')
- expect(page).to have_content('The list shows all available teams.')
+ expect(page).to have_content('The list shows all available teams that you are a member of.')
end
it 'shows a select with team options user is a member of multiple teams' do
diff --git a/spec/features/projects/services/user_activates_packagist_spec.rb b/spec/features/projects/services/user_activates_packagist_spec.rb
index b0cc818f093..756e9b33c07 100644
--- a/spec/features/projects/services/user_activates_packagist_spec.rb
+++ b/spec/features/projects/services/user_activates_packagist_spec.rb
@@ -5,7 +5,7 @@ describe 'User activates Packagist' do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit(project_settings_integrations_path(project))
diff --git a/spec/features/projects/services/user_activates_pivotaltracker_spec.rb b/spec/features/projects/services/user_activates_pivotaltracker_spec.rb
index d5d109ba48b..1d6b19e0b0c 100644
--- a/spec/features/projects/services/user_activates_pivotaltracker_spec.rb
+++ b/spec/features/projects/services/user_activates_pivotaltracker_spec.rb
@@ -5,7 +5,7 @@ describe 'User activates PivotalTracker' do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit(project_settings_integrations_path(project))
diff --git a/spec/features/projects/services/user_activates_prometheus_spec.rb b/spec/features/projects/services/user_activates_prometheus_spec.rb
index 33f884eb148..61361c8a2e3 100644
--- a/spec/features/projects/services/user_activates_prometheus_spec.rb
+++ b/spec/features/projects/services/user_activates_prometheus_spec.rb
@@ -5,7 +5,7 @@ describe 'User activates Prometheus' do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit(project_settings_integrations_path(project))
diff --git a/spec/features/projects/services/user_activates_pushover_spec.rb b/spec/features/projects/services/user_activates_pushover_spec.rb
index 9b7e8d62792..24612ee1457 100644
--- a/spec/features/projects/services/user_activates_pushover_spec.rb
+++ b/spec/features/projects/services/user_activates_pushover_spec.rb
@@ -5,7 +5,7 @@ describe 'User activates Pushover' do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit(project_settings_integrations_path(project))
diff --git a/spec/features/projects/services/user_activates_slack_notifications_spec.rb b/spec/features/projects/services/user_activates_slack_notifications_spec.rb
index fae9ebd1bd6..24b5d5259db 100644
--- a/spec/features/projects/services/user_activates_slack_notifications_spec.rb
+++ b/spec/features/projects/services/user_activates_slack_notifications_spec.rb
@@ -6,7 +6,7 @@ describe 'User activates Slack notifications' do
let(:project) { create(:project, slack_service: service) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
@@ -29,7 +29,7 @@ describe 'User activates Slack notifications' do
context 'when service is already configured' do
before do
service.fields
- service.update_attributes(
+ service.update(
push_channel: 1,
issue_channel: 2,
merge_request_channel: 3,
diff --git a/spec/features/projects/services/user_activates_slack_slash_command_spec.rb b/spec/features/projects/services/user_activates_slack_slash_command_spec.rb
index 4a88654210c..08cfddf7993 100644
--- a/spec/features/projects/services/user_activates_slack_slash_command_spec.rb
+++ b/spec/features/projects/services/user_activates_slack_slash_command_spec.rb
@@ -1,12 +1,12 @@
require 'spec_helper'
-feature 'Slack slash commands' do
- given(:user) { create(:user) }
- given(:project) { create(:project) }
- given(:service) { project.create_slack_slash_commands_service }
+describe 'Slack slash commands' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:service) { project.create_slack_slash_commands_service }
- background do
- project.add_master(user)
+ before do
+ project.add_maintainer(user)
sign_in(user)
visit edit_project_service_path(project, service)
end
diff --git a/spec/features/projects/services/user_views_services_spec.rb b/spec/features/projects/services/user_views_services_spec.rb
index 5c5e8b66642..e9c8cf0fe34 100644
--- a/spec/features/projects/services/user_views_services_spec.rb
+++ b/spec/features/projects/services/user_views_services_spec.rb
@@ -5,7 +5,7 @@ describe 'User views services' do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit(project_settings_integrations_path(project))
diff --git a/spec/features/projects/settings/forked_project_settings_spec.rb b/spec/features/projects/settings/forked_project_settings_spec.rb
index a4d1b78b83b..df33d215602 100644
--- a/spec/features/projects/settings/forked_project_settings_spec.rb
+++ b/spec/features/projects/settings/forked_project_settings_spec.rb
@@ -7,8 +7,8 @@ describe 'Projects > Settings > For a forked project', :js do
let(:forked_project) { fork_project(original_project, user) }
before do
- original_project.add_master(user)
- forked_project.add_master(user)
+ original_project.add_maintainer(user)
+ forked_project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/features/projects/settings/integration_settings_spec.rb b/spec/features/projects/settings/integration_settings_spec.rb
index 5178d63050e..8745ff72df0 100644
--- a/spec/features/projects/settings/integration_settings_spec.rb
+++ b/spec/features/projects/settings/integration_settings_spec.rb
@@ -21,8 +21,8 @@ describe 'Projects > Settings > Integration settings' do
end
end
- context 'for master' do
- let(:role) { :master }
+ context 'for maintainer' do
+ let(:role) { :maintainer }
context 'Webhooks' do
let(:hook) { create(:project_hook, :all_events_enabled, enable_ssl_verification: true, project: project) }
diff --git a/spec/features/projects/settings/lfs_settings_spec.rb b/spec/features/projects/settings/lfs_settings_spec.rb
index 342be1d2a9d..befb306b48d 100644
--- a/spec/features/projects/settings/lfs_settings_spec.rb
+++ b/spec/features/projects/settings/lfs_settings_spec.rb
@@ -3,7 +3,7 @@ require 'rails_helper'
describe 'Projects > Settings > LFS settings' do
let(:project) { create(:project) }
let(:user) { create(:user) }
- let(:role) { :master }
+ let(:role) { :maintainer }
context 'LFS enabled setting' do
before do
@@ -13,8 +13,8 @@ describe 'Projects > Settings > LFS settings' do
project.add_role(user, role)
end
- context 'for master' do
- let(:role) { :master }
+ context 'for maintainer' do
+ let(:role) { :maintainer }
it 'displays the correct elements', :js do
visit edit_project_path(project)
diff --git a/spec/features/projects/settings/pipelines_settings_spec.rb b/spec/features/projects/settings/pipelines_settings_spec.rb
index cfdae246c09..742ecf82c38 100644
--- a/spec/features/projects/settings/pipelines_settings_spec.rb
+++ b/spec/features/projects/settings/pipelines_settings_spec.rb
@@ -21,8 +21,8 @@ describe "Projects > Settings > Pipelines settings" do
end
end
- context 'for master' do
- let(:role) { :master }
+ context 'for maintainer' do
+ let(:role) { :maintainer }
it 'be allowed to change' do
visit project_settings_ci_cd_path(project)
diff --git a/spec/features/projects/settings/project_badges_spec.rb b/spec/features/projects/settings/project_badges_spec.rb
index cc3551a4c21..2ec94274f80 100644
--- a/spec/features/projects/settings/project_badges_spec.rb
+++ b/spec/features/projects/settings/project_badges_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Project Badges' do
+describe 'Project Badges' do
include WaitForRequests
let(:user) { create(:user) }
@@ -12,7 +12,7 @@ feature 'Project Badges' do
let!(:group_badge) { create(:group_badge, group: group) }
before do
- group.add_master(user)
+ group.add_maintainer(user)
sign_in(user)
visit(project_settings_badges_path(project))
@@ -22,7 +22,7 @@ feature 'Project Badges' do
page.within '.badge-settings' do
wait_for_requests
- rows = all('.panel-body > div')
+ rows = all('.card-body > div')
expect(rows.length).to eq 2
expect(rows[0]).to have_content group_badge.link_url
expect(rows[1]).to have_content project_badge.link_url
@@ -49,7 +49,7 @@ feature 'Project Badges' do
click_button 'Add badge'
wait_for_requests
- within '.panel-body' do
+ within '.card-body' do
expect(find('a')[:href]).to eq badge_link_url
expect(find('a img')[:src]).to eq badge_image_url
end
@@ -61,7 +61,7 @@ feature 'Project Badges' do
it 'form is shown when clicking edit button in list' do
page.within '.badge-settings' do
wait_for_requests
- rows = all('.panel-body > div')
+ rows = all('.card-body > div')
expect(rows.length).to eq 2
rows[1].find('[aria-label="Edit"]').click
@@ -75,7 +75,7 @@ feature 'Project Badges' do
it 'updates a badge when submitting the edit form' do
page.within '.badge-settings' do
wait_for_requests
- rows = all('.panel-body > div')
+ rows = all('.card-body > div')
expect(rows.length).to eq 2
rows[1].find('[aria-label="Edit"]').click
within 'form' do
@@ -86,7 +86,7 @@ feature 'Project Badges' do
wait_for_requests
end
- rows = all('.panel-body > div')
+ rows = all('.card-body > div')
expect(rows.length).to eq 2
expect(rows[1]).to have_content badge_link_url
end
@@ -100,7 +100,7 @@ feature 'Project Badges' do
it 'shows a modal when deleting a badge' do
wait_for_requests
- rows = all('.panel-body > div')
+ rows = all('.card-body > div')
expect(rows.length).to eq 2
click_delete_button(rows[1])
@@ -110,14 +110,14 @@ feature 'Project Badges' do
it 'deletes a badge when confirming the modal' do
wait_for_requests
- rows = all('.panel-body > div')
+ rows = all('.card-body > div')
expect(rows.length).to eq 2
click_delete_button(rows[1])
find('.modal .btn-danger').click
wait_for_requests
- rows = all('.panel-body > div')
+ rows = all('.card-body > div')
expect(rows.length).to eq 1
expect(rows[0]).to have_content group_badge.link_url
end
diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb
index 08b40653764..a0f5b234ebc 100644
--- a/spec/features/projects/settings/repository_settings_spec.rb
+++ b/spec/features/projects/settings/repository_settings_spec.rb
@@ -20,8 +20,8 @@ describe 'Projects > Settings > Repository settings' do
end
end
- context 'for master' do
- let(:role) { :master }
+ context 'for maintainer' do
+ let(:role) { :maintainer }
context 'Deploy Keys', :js do
let(:private_deploy_key) { create(:deploy_key, title: 'private_deploy_key', public: false) }
@@ -101,7 +101,7 @@ describe 'Projects > Settings > Repository settings' do
visit project_settings_repository_path(project)
end
- scenario 'view deploy tokens' do
+ it 'view deploy tokens' do
within('.deploy-tokens') do
expect(page).to have_content(deploy_token.name)
expect(page).to have_content('read_repository')
@@ -109,7 +109,7 @@ describe 'Projects > Settings > Repository settings' do
end
end
- scenario 'add a new deploy token' do
+ it 'add a new deploy token' do
fill_in 'deploy_token_name', with: 'new_deploy_key'
fill_in 'deploy_token_expires_at', with: (Date.today + 1.month).to_s
check 'deploy_token_read_repository'
@@ -124,7 +124,7 @@ describe 'Projects > Settings > Repository settings' do
let(:user2) { create(:user) }
before do
- project.add_master(user2)
+ project.add_maintainer(user2)
visit project_settings_repository_path(project)
end
diff --git a/spec/features/projects/settings/user_archives_project_spec.rb b/spec/features/projects/settings/user_archives_project_spec.rb
index 38c8a8c2468..5008eab4d39 100644
--- a/spec/features/projects/settings/user_archives_project_spec.rb
+++ b/spec/features/projects/settings/user_archives_project_spec.rb
@@ -4,7 +4,7 @@ describe 'Projects > Settings > User archives a project' do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/projects/settings/user_changes_avatar_spec.rb b/spec/features/projects/settings/user_changes_avatar_spec.rb
index 2dcc79d8a12..64335163016 100644
--- a/spec/features/projects/settings/user_changes_avatar_spec.rb
+++ b/spec/features/projects/settings/user_changes_avatar_spec.rb
@@ -5,7 +5,7 @@ describe 'Projects > Settings > User changes avatar' do
let(:user) { project.creator }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/features/projects/settings/user_interacts_with_deploy_keys_spec.rb b/spec/features/projects/settings/user_interacts_with_deploy_keys_spec.rb
index 71a077039b7..ecfb49b9efe 100644
--- a/spec/features/projects/settings/user_interacts_with_deploy_keys_spec.rb
+++ b/spec/features/projects/settings/user_interacts_with_deploy_keys_spec.rb
@@ -50,7 +50,7 @@ describe "User interacts with deploy keys", :js do
before do
create(:deploy_keys_project, project: another_project, deploy_key: deploy_key)
- another_project.add_master(user)
+ another_project.add_maintainer(user)
end
it "shows deploy keys" do
@@ -110,7 +110,7 @@ describe "User interacts with deploy keys", :js do
before do
create(:deploy_keys_project, project: another_project, deploy_key: deploy_key)
- another_project.add_master(user)
+ another_project.add_maintainer(user)
end
it_behaves_like "attaches a key"
diff --git a/spec/features/projects/settings/user_manages_group_links_spec.rb b/spec/features/projects/settings/user_manages_group_links_spec.rb
index fdf42797091..2f1824d7849 100644
--- a/spec/features/projects/settings/user_manages_group_links_spec.rb
+++ b/spec/features/projects/settings/user_manages_group_links_spec.rb
@@ -9,10 +9,10 @@ describe 'Projects > Settings > User manages group links' do
let(:group_market) { create(:group, name: 'Market', path: 'market') }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
- share_link = project.project_group_links.new(group_access: Gitlab::Access::MASTER)
+ share_link = project.project_group_links.new(group_access: Gitlab::Access::MAINTAINER)
share_link.group_id = group_ops.id
share_link.save!
@@ -30,7 +30,7 @@ describe 'Projects > Settings > User manages group links' do
click_link('Share with group')
select2(group_market.id, from: '#link_group_id')
- select('Master', from: 'link_group_access')
+ select('Maintainer', from: 'link_group_access')
click_button('Share')
diff --git a/spec/features/projects/settings/user_manages_project_members_spec.rb b/spec/features/projects/settings/user_manages_project_members_spec.rb
index 8af95522165..b8ca11d53f0 100644
--- a/spec/features/projects/settings/user_manages_project_members_spec.rb
+++ b/spec/features/projects/settings/user_manages_project_members_spec.rb
@@ -9,7 +9,7 @@ describe 'Projects > Settings > User manages project members' do
let(:user_mike) { create(:user, name: 'Mike') }
before do
- project.add_master(user)
+ project.add_maintainer(user)
project.add_developer(user_dmitriy)
sign_in(user)
end
@@ -30,7 +30,7 @@ describe 'Projects > Settings > User manages project members' do
end
it 'imports a team from another project' do
- project2.add_master(user)
+ project2.add_maintainer(user)
project2.add_reporter(user_mike)
visit(project_project_members_path(project))
@@ -54,7 +54,7 @@ describe 'Projects > Settings > User manages project members' do
group.add_owner(user)
group.add_developer(user_dmitriy)
- share_link = project.project_group_links.new(group_access: Gitlab::Access::MASTER)
+ share_link = project.project_group_links.new(group_access: Gitlab::Access::MAINTAINER)
share_link.group_id = group.id
share_link.save!
@@ -62,7 +62,7 @@ describe 'Projects > Settings > User manages project members' do
page.within('.project-members-groups') do
expect(page).to have_content('OpenSource')
- expect(first('.group_member')).to have_content('Master')
+ expect(first('.group_member')).to have_content('Maintainer')
end
end
end
diff --git a/spec/features/projects/settings/user_sees_revoke_deploy_token_modal_spec.rb b/spec/features/projects/settings/user_sees_revoke_deploy_token_modal_spec.rb
new file mode 100644
index 00000000000..069704a1305
--- /dev/null
+++ b/spec/features/projects/settings/user_sees_revoke_deploy_token_modal_spec.rb
@@ -0,0 +1,25 @@
+require 'rails_helper'
+
+describe 'Repository Settings > User sees revoke deploy token modal', :js do
+ let(:project) { create(:project, :public, :repository) }
+ let(:user) { project.creator }
+ let(:role) { :developer }
+ let!(:deploy_token) { create(:deploy_token, :gitlab_deploy_token, projects: [project]) }
+
+ before do
+ project.add_role(user, role)
+ sign_in(user)
+ visit(project_settings_repository_path(project))
+ click_link('Revoke')
+ end
+
+ it 'shows the revoke deploy token modal' do
+ expect(page).to have_content('You are about to revoke')
+ end
+
+ it 'closes the revoke deploy token modal with escape keypress' do
+ find('.modal.show').send_keys(:escape)
+
+ expect(page).not_to have_content('You are about to revoke')
+ end
+end
diff --git a/spec/features/projects/settings/user_transfers_a_project_spec.rb b/spec/features/projects/settings/user_transfers_a_project_spec.rb
index 96b7cf1f93b..2fdbc04fa62 100644
--- a/spec/features/projects/settings/user_transfers_a_project_spec.rb
+++ b/spec/features/projects/settings/user_transfers_a_project_spec.rb
@@ -10,7 +10,7 @@ describe 'Projects > Settings > User transfers a project', :js do
sign_in(user)
end
- def transfer_project(project, group)
+ def transfer_project(project, group, confirm: true)
visit edit_project_path(project)
page.within('.js-project-transfer-form') do
@@ -21,6 +21,8 @@ describe 'Projects > Settings > User transfers a project', :js do
click_button('Transfer project')
+ return unless confirm
+
fill_in 'confirm_name_input', with: project.name
click_button 'Confirm'
@@ -28,6 +30,11 @@ describe 'Projects > Settings > User transfers a project', :js do
wait_for_requests
end
+ it 'focuses on the confirmation field' do
+ transfer_project(project, group, confirm: false)
+ expect(page).to have_selector '#confirm_name_input:focus'
+ end
+
it 'allows transferring a project to a group' do
old_path = project_path(project)
transfer_project(project, group)
diff --git a/spec/features/projects/settings/visibility_settings_spec.rb b/spec/features/projects/settings/visibility_settings_spec.rb
index 2ec6990313f..1fbc108697f 100644
--- a/spec/features/projects/settings/visibility_settings_spec.rb
+++ b/spec/features/projects/settings/visibility_settings_spec.rb
@@ -59,12 +59,12 @@ describe 'Projects > Settings > Visibility settings', :js do
end
end
- context 'as master' do
- let(:master_user) { create(:user) }
+ context 'as maintainer' do
+ let(:maintainer_user) { create(:user) }
before do
- project.add_master(master_user)
- sign_in(master_user)
+ project.add_maintainer(maintainer_user)
+ sign_in(maintainer_user)
visit edit_project_path(project)
end
diff --git a/spec/features/projects/show/developer_views_empty_project_instructions_spec.rb b/spec/features/projects/show/developer_views_empty_project_instructions_spec.rb
index 8803b5222be..227bdf524fe 100644
--- a/spec/features/projects/show/developer_views_empty_project_instructions_spec.rb
+++ b/spec/features/projects/show/developer_views_empty_project_instructions_spec.rb
@@ -1,23 +1,23 @@
require 'rails_helper'
-feature 'Projects > Show > Developer views empty project instructions' do
+describe 'Projects > Show > Developer views empty project instructions' do
let(:project) { create(:project, :empty_repo) }
let(:developer) { create(:user) }
- background do
+ before do
project.add_developer(developer)
sign_in(developer)
end
context 'without an SSH key' do
- scenario 'defaults to HTTP' do
+ it 'defaults to HTTP' do
visit_project
expect_instructions_for('http')
end
- scenario 'switches to SSH', :js do
+ it 'switches to SSH', :js do
visit_project
select_protocol('SSH')
@@ -27,17 +27,17 @@ feature 'Projects > Show > Developer views empty project instructions' do
end
context 'with an SSH key' do
- background do
+ before do
create(:personal_key, user: developer)
end
- scenario 'defaults to SSH' do
+ it 'defaults to SSH' do
visit_project
expect_instructions_for('ssh')
end
- scenario 'switches to HTTP', :js do
+ it 'switches to HTTP', :js do
visit_project
select_protocol('HTTP')
diff --git a/spec/features/projects/show/download_buttons_spec.rb b/spec/features/projects/show/download_buttons_spec.rb
index 254affd4a94..3a2dcc5aa55 100644
--- a/spec/features/projects/show/download_buttons_spec.rb
+++ b/spec/features/projects/show/download_buttons_spec.rb
@@ -1,12 +1,12 @@
require 'spec_helper'
-feature 'Projects > Show > Download buttons' do
- given(:user) { create(:user) }
- given(:role) { :developer }
- given(:status) { 'success' }
- given(:project) { create(:project, :repository) }
+describe 'Projects > Show > Download buttons' do
+ let(:user) { create(:user) }
+ let(:role) { :developer }
+ let(:status) { 'success' }
+ let(:project) { create(:project, :repository) }
- given(:pipeline) do
+ let(:pipeline) do
create(:ci_pipeline,
project: project,
sha: project.commit.sha,
@@ -14,14 +14,14 @@ feature 'Projects > Show > Download buttons' do
status: status)
end
- given!(:build) do
+ let!(:build) do
create(:ci_build, :success, :artifacts,
pipeline: pipeline,
status: pipeline.status,
name: 'build')
end
- background do
+ before do
sign_in(user)
project.add_role(user, role)
end
@@ -32,13 +32,13 @@ feature 'Projects > Show > Download buttons' do
visit project_path(project)
end
- scenario 'shows download artifacts button' do
+ it 'shows download artifacts button' do
href = latest_succeeded_project_artifacts_path(project, "#{project.default_branch}/download", job: 'build')
expect(page).to have_link "Download '#{build.name}'", href: href
end
- scenario 'download links have download attribute' do
+ it 'download links have download attribute' do
expect(page).to have_selector('a', text: 'Download')
page.all('a', text: 'Download').each do |link|
expect(link[:download]).to eq ''
diff --git a/spec/features/projects/show/no_password_spec.rb b/spec/features/projects/show/no_password_spec.rb
index b3b3212556c..8259d482fd9 100644
--- a/spec/features/projects/show/no_password_spec.rb
+++ b/spec/features/projects/show/no_password_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'No Password Alert' do
+describe 'No Password Alert' do
let(:project) { create(:project, :repository, namespace: user.namespace) }
context 'with internal auth enabled' do
diff --git a/spec/features/projects/show/rss_spec.rb b/spec/features/projects/show/rss_spec.rb
index d02eaf34533..4d9135b9677 100644
--- a/spec/features/projects/show/rss_spec.rb
+++ b/spec/features/projects/show/rss_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Projects > Show > RSS' do
+describe 'Projects > Show > RSS' do
let(:user) { create(:user) }
let(:project) { create(:project, :repository, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
let(:path) { project_path(project) }
@@ -12,7 +12,7 @@ feature 'Projects > Show > RSS' do
visit path
end
- it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token"
+ it_behaves_like "an autodiscoverable RSS feed with current_user's feed token"
end
context 'when signed out' do
@@ -20,6 +20,6 @@ feature 'Projects > Show > RSS' do
visit path
end
- it_behaves_like "an autodiscoverable RSS feed without an RSS token"
+ it_behaves_like "an autodiscoverable RSS feed without a feed token"
end
end
diff --git a/spec/features/projects/show/user_sees_deletion_failure_message_spec.rb b/spec/features/projects/show/user_sees_deletion_failure_message_spec.rb
index aa23bef6fd8..d9d57298929 100644
--- a/spec/features/projects/show/user_sees_deletion_failure_message_spec.rb
+++ b/spec/features/projects/show/user_sees_deletion_failure_message_spec.rb
@@ -8,7 +8,7 @@ describe 'Projects > Show > User sees a deletion failure message' do
end
it 'shows error message if deletion for project fails' do
- project.update_attributes(delete_error: "Something went wrong", pending_delete: false)
+ project.update(delete_error: "Something went wrong", pending_delete: false)
visit project_path(project)
diff --git a/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb b/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
index e44361fbe26..0405e21a0d7 100644
--- a/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
+++ b/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
@@ -5,6 +5,8 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
# see spec/features/projects/files/project_owner_creates_license_file_spec.rb
# see spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
+ include FakeBlobHelpers
+
let(:user) { create(:user) }
describe 'empty project' do
@@ -36,9 +38,9 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
end
end
- describe 'as a master' do
+ describe 'as a maintainer' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit project_path(project)
@@ -136,16 +138,62 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
end
end
- describe 'as a master' do
+ describe 'as a maintainer' do
before do
allow_any_instance_of(AutoDevopsHelper).to receive(:show_auto_devops_callout?).and_return(false)
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
+ end
- visit project_path(project)
+ context 'Readme button' do
+ before do
+ allow(Project).to receive(:find_by_full_path)
+ .with(project.full_path, follow_redirects: true)
+ .and_return(project)
+ end
+
+ context 'when the project has a populated Readme' do
+ it 'show the "Readme" anchor' do
+ visit project_path(project)
+
+ expect(project.repository.readme).not_to be_nil
+
+ page.within('.project-stats') do
+ expect(page).not_to have_link('Add Readme', href: presenter.add_readme_path)
+ expect(page).to have_link('Readme', href: presenter.readme_path)
+ end
+ end
+
+ context 'when the project has an empty Readme' do
+ it 'show the "Readme" anchor' do
+ allow(project.repository).to receive(:readme).and_return(fake_blob(path: 'README.md', data: '', size: 0))
+
+ visit project_path(project)
+
+ page.within('.project-stats') do
+ expect(page).not_to have_link('Add Readme', href: presenter.add_readme_path)
+ expect(page).to have_link('Readme', href: presenter.readme_path)
+ end
+ end
+ end
+ end
+
+ context 'when the project does not have a Readme' do
+ it 'shows the "Add Readme" button' do
+ allow(project.repository).to receive(:readme).and_return(nil)
+
+ visit project_path(project)
+
+ page.within('.project-stats') do
+ expect(page).to have_link('Add Readme', href: presenter.add_readme_path)
+ end
+ end
+ end
end
it 'no "Add Changelog" button if the project already has a changelog' do
+ visit project_path(project)
+
expect(project.repository.changelog).not_to be_nil
page.within('.project-stats') do
@@ -154,6 +202,8 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
end
it 'no "Add License" button if the project already has a license' do
+ visit project_path(project)
+
expect(project.repository.license_blob).not_to be_nil
page.within('.project-stats') do
@@ -162,6 +212,8 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
end
it 'no "Add Contribution guide" button if the project already has a contribution guide' do
+ visit project_path(project)
+
expect(project.repository.contribution_guide).not_to be_nil
page.within('.project-stats') do
@@ -171,6 +223,8 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
describe 'GitLab CI configuration button' do
it '"Set up CI/CD" button linked to new file populated for a .gitlab-ci.yml' do
+ visit project_path(project)
+
expect(project.repository.gitlab_ci_yml).to be_nil
page.within('.project-stats') do
@@ -211,6 +265,8 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
describe 'Auto DevOps button' do
it '"Enable Auto DevOps" button linked to settings page' do
+ visit project_path(project)
+
page.within('.project-stats') do
expect(page).to have_link('Enable Auto DevOps', href: project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))
end
@@ -263,6 +319,8 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
describe 'Kubernetes cluster button' do
it '"Add Kubernetes cluster" button linked to clusters page' do
+ visit project_path(project)
+
page.within('.project-stats') do
expect(page).to have_link('Add Kubernetes cluster', href: new_project_cluster_path(project))
end
diff --git a/spec/features/projects/snippets/create_snippet_spec.rb b/spec/features/projects/snippets/create_snippet_spec.rb
index 2388feeb980..6d8a72dd6a3 100644
--- a/spec/features/projects/snippets/create_snippet_spec.rb
+++ b/spec/features/projects/snippets/create_snippet_spec.rb
@@ -16,7 +16,7 @@ describe 'Projects > Snippets > Create Snippet', :js do
context 'when a user is authenticated' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit project_snippets_path(project)
diff --git a/spec/features/projects/snippets/show_spec.rb b/spec/features/projects/snippets/show_spec.rb
index 004ac55b656..3cc797277dd 100644
--- a/spec/features/projects/snippets/show_spec.rb
+++ b/spec/features/projects/snippets/show_spec.rb
@@ -6,7 +6,7 @@ describe 'Projects > Snippets > Project snippet', :js do
let(:snippet) { create(:project_snippet, project: project, file_name: file_name, content: content) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/features/projects/snippets/user_comments_on_snippet_spec.rb b/spec/features/projects/snippets/user_comments_on_snippet_spec.rb
index 01cf9740d1f..d82e350e0f7 100644
--- a/spec/features/projects/snippets/user_comments_on_snippet_spec.rb
+++ b/spec/features/projects/snippets/user_comments_on_snippet_spec.rb
@@ -6,7 +6,7 @@ describe 'Projects > Snippets > User comments on a snippet', :js do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit(project_snippet_path(project, snippet))
diff --git a/spec/features/projects/snippets/user_deletes_snippet_spec.rb b/spec/features/projects/snippets/user_deletes_snippet_spec.rb
index e64837ad59e..2bd8bb9d551 100644
--- a/spec/features/projects/snippets/user_deletes_snippet_spec.rb
+++ b/spec/features/projects/snippets/user_deletes_snippet_spec.rb
@@ -6,7 +6,7 @@ describe 'Projects > Snippets > User deletes a snippet' do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit(project_snippet_path(project, snippet))
diff --git a/spec/features/projects/snippets/user_updates_snippet_spec.rb b/spec/features/projects/snippets/user_updates_snippet_spec.rb
index eaedbbf32b6..33f77d55f89 100644
--- a/spec/features/projects/snippets/user_updates_snippet_spec.rb
+++ b/spec/features/projects/snippets/user_updates_snippet_spec.rb
@@ -6,7 +6,7 @@ describe 'Projects > Snippets > User updates a snippet' do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit(project_snippet_path(project, snippet))
diff --git a/spec/features/projects/snippets/user_views_snippets_spec.rb b/spec/features/projects/snippets/user_views_snippets_spec.rb
index 376b76e0001..1243db9d9f7 100644
--- a/spec/features/projects/snippets/user_views_snippets_spec.rb
+++ b/spec/features/projects/snippets/user_views_snippets_spec.rb
@@ -8,7 +8,7 @@ describe 'Projects > Snippets > User views snippets' do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit(project_snippets_path(project))
diff --git a/spec/features/projects/sub_group_issuables_spec.rb b/spec/features/projects/sub_group_issuables_spec.rb
index eb2d3ff50a0..50e7e934cf6 100644
--- a/spec/features/projects/sub_group_issuables_spec.rb
+++ b/spec/features/projects/sub_group_issuables_spec.rb
@@ -7,7 +7,7 @@ describe 'Subgroup Issuables', :js, :nested_groups do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in user
end
diff --git a/spec/features/projects/tags/download_buttons_spec.rb b/spec/features/projects/tags/download_buttons_spec.rb
index b62498194c4..fbfd8cee7aa 100644
--- a/spec/features/projects/tags/download_buttons_spec.rb
+++ b/spec/features/projects/tags/download_buttons_spec.rb
@@ -1,13 +1,13 @@
require 'spec_helper'
-feature 'Download buttons in tags page' do
- given(:user) { create(:user) }
- given(:role) { :developer }
- given(:status) { 'success' }
- given(:tag) { 'v1.0.0' }
- given(:project) { create(:project, :repository) }
+describe 'Download buttons in tags page' do
+ let(:user) { create(:user) }
+ let(:role) { :developer }
+ let(:status) { 'success' }
+ let(:tag) { 'v1.0.0' }
+ let(:project) { create(:project, :repository) }
- given(:pipeline) do
+ let(:pipeline) do
create(:ci_pipeline,
project: project,
sha: project.commit(tag).sha,
@@ -15,14 +15,14 @@ feature 'Download buttons in tags page' do
status: status)
end
- given!(:build) do
+ let!(:build) do
create(:ci_build, :success, :artifacts,
pipeline: pipeline,
status: pipeline.status,
name: 'build')
end
- background do
+ before do
sign_in(user)
project.add_role(user, role)
end
@@ -33,7 +33,7 @@ feature 'Download buttons in tags page' do
visit project_tags_path(project)
end
- scenario 'shows download artifacts button' do
+ it 'shows download artifacts button' do
href = latest_succeeded_project_artifacts_path(project, "#{tag}/download", job: 'build')
expect(page).to have_link "Download '#{build.name}'", href: href
diff --git a/spec/features/projects/tree/create_directory_spec.rb b/spec/features/projects/tree/create_directory_spec.rb
index 3017048e506..057b49cc68c 100644
--- a/spec/features/projects/tree/create_directory_spec.rb
+++ b/spec/features/projects/tree/create_directory_spec.rb
@@ -1,11 +1,11 @@
require 'spec_helper'
-feature 'Multi-file editor new directory', :js do
+describe 'Multi-file editor new directory', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit project_tree_path(project, :master)
diff --git a/spec/features/projects/tree/create_file_spec.rb b/spec/features/projects/tree/create_file_spec.rb
index 56471c8e7aa..b324ab01383 100644
--- a/spec/features/projects/tree/create_file_spec.rb
+++ b/spec/features/projects/tree/create_file_spec.rb
@@ -1,11 +1,11 @@
require 'spec_helper'
-feature 'Multi-file editor new file', :js do
+describe 'Multi-file editor new file', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit project_path(project)
diff --git a/spec/features/projects/tree/rss_spec.rb b/spec/features/projects/tree/rss_spec.rb
index 6407370ac0d..022167d9c5f 100644
--- a/spec/features/projects/tree/rss_spec.rb
+++ b/spec/features/projects/tree/rss_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Project Tree RSS' do
+describe 'Project Tree RSS' do
let(:user) { create(:user) }
let(:project) { create(:project, :repository, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
let(:path) { project_tree_path(project, :master) }
@@ -12,7 +12,7 @@ feature 'Project Tree RSS' do
visit path
end
- it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token"
+ it_behaves_like "an autodiscoverable RSS feed with current_user's feed token"
end
context 'when signed out' do
@@ -20,6 +20,6 @@ feature 'Project Tree RSS' do
visit path
end
- it_behaves_like "an autodiscoverable RSS feed without an RSS token"
+ it_behaves_like "an autodiscoverable RSS feed without a feed token"
end
end
diff --git a/spec/features/projects/tree/tree_show_spec.rb b/spec/features/projects/tree/tree_show_spec.rb
index c4b3fb9d171..9e15163fd72 100644
--- a/spec/features/projects/tree/tree_show_spec.rb
+++ b/spec/features/projects/tree/tree_show_spec.rb
@@ -1,11 +1,11 @@
require 'spec_helper'
-feature 'Projects tree' do
+describe 'Projects tree' do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit project_tree_path(project, 'master')
diff --git a/spec/features/projects/tree/upload_file_spec.rb b/spec/features/projects/tree/upload_file_spec.rb
index 4dfc325b37e..28da0a87f22 100644
--- a/spec/features/projects/tree/upload_file_spec.rb
+++ b/spec/features/projects/tree/upload_file_spec.rb
@@ -1,13 +1,13 @@
require 'spec_helper'
-feature 'Multi-file editor upload file', :js do
+describe '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)
+ project.add_maintainer(user)
sign_in(user)
visit project_tree_path(project, :master)
diff --git a/spec/features/projects/user_creates_project_spec.rb b/spec/features/projects/user_creates_project_spec.rb
index f95469ad070..83d18996f4e 100644
--- a/spec/features/projects/user_creates_project_spec.rb
+++ b/spec/features/projects/user_creates_project_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'User creates a project', :js do
+describe 'User creates a project', :js do
let(:user) { create(:user) }
before do
diff --git a/spec/features/projects/user_sees_sidebar_spec.rb b/spec/features/projects/user_sees_sidebar_spec.rb
index 8a9255db9e8..ee5734a9bf1 100644
--- a/spec/features/projects/user_sees_sidebar_spec.rb
+++ b/spec/features/projects/user_sees_sidebar_spec.rb
@@ -44,6 +44,18 @@ describe 'Projects > User sees sidebar' do
expect(page).not_to have_content 'Repository'
expect(page).not_to have_content 'CI / CD'
expect(page).not_to have_content 'Merge Requests'
+ expect(page).not_to have_content 'Operations'
+ end
+ end
+
+ it 'shows build tab if builds are public' do
+ project.public_builds = true
+ project.save
+
+ visit project_path(project)
+
+ within('.nav-sidebar') do
+ expect(page).to have_content 'CI / CD'
end
end
diff --git a/spec/features/projects/user_uses_shortcuts_spec.rb b/spec/features/projects/user_uses_shortcuts_spec.rb
index 495a010b32c..df9ee69aadb 100644
--- a/spec/features/projects/user_uses_shortcuts_spec.rb
+++ b/spec/features/projects/user_uses_shortcuts_spec.rb
@@ -5,7 +5,7 @@ describe 'User uses shortcuts', :js do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit(project_path(project))
@@ -110,6 +110,14 @@ describe 'User uses shortcuts', :js do
end
context 'when navigating to the Operations pages' do
+ it 'redirects to the Metrics page' do
+ find('body').native.send_key('g')
+ find('body').native.send_key('l')
+
+ expect(page).to have_active_navigation('Operations')
+ expect(page).to have_active_sub_navigation('Metrics')
+ end
+
it 'redirects to the Environments page' do
find('body').native.send_key('g')
find('body').native.send_key('e')
diff --git a/spec/features/projects/user_views_empty_project_spec.rb b/spec/features/projects/user_views_empty_project_spec.rb
index 7b982301ffc..b7c0834d33a 100644
--- a/spec/features/projects/user_views_empty_project_spec.rb
+++ b/spec/features/projects/user_views_empty_project_spec.rb
@@ -15,9 +15,9 @@ describe 'User views an empty project' do
end
end
- describe 'as a master' do
+ describe 'as a maintainer' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
it_behaves_like 'allowing push to default branch'
diff --git a/spec/features/projects/view_on_env_spec.rb b/spec/features/projects/view_on_env_spec.rb
index 7f547a4ca1f..a48ad94e9fa 100644
--- a/spec/features/projects/view_on_env_spec.rb
+++ b/spec/features/projects/view_on_env_spec.rb
@@ -7,7 +7,7 @@ describe 'View on environment', :js do
let(:user) { project.creator }
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
context 'when the branch has a route map' do
@@ -59,7 +59,9 @@ describe 'View on environment', :js do
it 'has a "View on env" button' do
within '.diffs' do
- expect(page).to have_link('View on feature.review.example.com', href: 'http://feature.review.example.com/ruby/feature')
+ text = 'View on feature.review.example.com'
+ url = 'http://feature.review.example.com/ruby/feature'
+ expect(page).to have_selector("a[data-original-title='#{text}'][href='#{url}']")
end
end
end
diff --git a/spec/features/projects/wiki/markdown_preview_spec.rb b/spec/features/projects/wiki/markdown_preview_spec.rb
index e473739a6aa..ed5f8105487 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 do
+describe 'Projects > Wiki > User previews markdown changes', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
let(:wiki_content) do
@@ -9,16 +9,18 @@ feature 'Projects > Wiki > User previews markdown changes', :js do
[relative link 1](../relative)
[relative link 2](./relative)
[relative link 3](./e/f/relative)
+[spaced link](title with spaces)
HEREDOC
end
- background do
- project.add_master(user)
+ before do
+ project.add_maintainer(user)
sign_in(user)
visit project_path(project)
find('.shortcuts-wiki').click
+ click_link "Create your first page"
end
context "while creating a new wiki page" do
@@ -41,6 +43,7 @@ feature 'Projects > Wiki > User previews markdown changes', :js do
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a/b/relative\">relative link 1</a>")
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a/b/c/relative\">relative link 2</a>")
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a/b/c/e/f/relative\">relative link 3</a>")
+ expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/title%20with%20spaces\">spaced link</a>")
end
end
@@ -63,6 +66,7 @@ feature 'Projects > Wiki > User previews markdown changes', :js do
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/relative\">relative link 1</a>")
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/relative\">relative link 2</a>")
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>")
+ expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/title%20with%20spaces\">spaced link</a>")
end
end
@@ -85,6 +89,7 @@ feature 'Projects > Wiki > User previews markdown changes', :js do
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/relative\">relative link 1</a>")
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/relative\">relative link 2</a>")
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>")
+ expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/title%20with%20spaces\">spaced link</a>")
end
end
end
@@ -118,6 +123,7 @@ feature 'Projects > Wiki > User previews markdown changes', :js do
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a/b/relative\">relative link 1</a>")
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a/b/c/relative\">relative link 2</a>")
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a/b/c/e/f/relative\">relative link 3</a>")
+ expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/title%20with%20spaces\">spaced link</a>")
end
end
@@ -135,6 +141,7 @@ feature 'Projects > Wiki > User previews markdown changes', :js do
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/relative\">relative link 1</a>")
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/relative\">relative link 2</a>")
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>")
+ expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/title%20with%20spaces\">spaced link</a>")
end
end
@@ -152,6 +159,7 @@ feature 'Projects > Wiki > User previews markdown changes', :js do
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/relative\">relative link 1</a>")
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/relative\">relative link 2</a>")
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>")
+ expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/title%20with%20spaces\">spaced link</a>")
end
end
end
diff --git a/spec/features/projects/wiki/shortcuts_spec.rb b/spec/features/projects/wiki/shortcuts_spec.rb
index 6178361082e..c01be1f14ed 100644
--- a/spec/features/projects/wiki/shortcuts_spec.rb
+++ b/spec/features/projects/wiki/shortcuts_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Wiki shortcuts', :js do
+describe 'Wiki shortcuts', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
let(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: { title: 'home', content: 'Home page' }) }
@@ -10,7 +10,7 @@ feature 'Wiki shortcuts', :js do
visit project_wiki_path(project, wiki_page)
end
- scenario 'Visit edit wiki page using "e" keyboard shortcut' do
+ it 'Visit edit wiki page using "e" keyboard shortcut' do
find('body').native.send_key('e')
expect(find('.wiki-page-title')).to have_content('Edit Page')
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 9989e1ffda7..830565620d6 100644
--- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
@@ -4,10 +4,11 @@ describe "User creates wiki page" do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit(project_wikis_path(project))
+ click_link "Create your first page"
end
context "when wiki is empty" do
@@ -241,7 +242,7 @@ describe "User creates wiki page" do
end
end
- it "shows the autocompletion dropdown" do
+ it "shows the emoji autocompletion dropdown" do
click_link("New page")
page.within("#modal-new-wiki") do
@@ -253,7 +254,7 @@ describe "User creates wiki page" do
page.within(".wiki-form") do
find("#wiki_content").native.send_keys("")
- fill_in(:wiki_content, with: "@")
+ fill_in(:wiki_content, with: ":")
end
expect(page).to have_selector(".atwho-view")
diff --git a/spec/features/projects/wiki/user_deletes_wiki_page_spec.rb b/spec/features/projects/wiki/user_deletes_wiki_page_spec.rb
index 2c67cec6b67..5007794cd77 100644
--- a/spec/features/projects/wiki/user_deletes_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_deletes_wiki_page_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'User deletes wiki page', :js do
+describe 'User deletes wiki page', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
let(:wiki_page) { create(:wiki_page, wiki: project.wiki) }
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 823399ac3c3..db97d59e918 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
@@ -9,7 +9,7 @@ describe 'Projects > Wiki > User views Git access wiki page' do
sign_in(user)
end
- scenario 'Visit Wiki Page Current Commit' do
+ it 'Visit Wiki Page Current Commit' do
visit project_wiki_path(project, wiki_page)
click_link 'Clone repository'
diff --git a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
index e019e3ce5a5..2840d28cf30 100644
--- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
@@ -4,13 +4,14 @@ describe 'User updates wiki page' do
shared_examples 'wiki page user update' do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
context 'when wiki is empty' do
before do
visit(project_wikis_path(project))
+ click_link "Create your first page"
end
context 'in a user namespace' do
@@ -95,11 +96,11 @@ describe 'User updates wiki page' do
expect(find('textarea#wiki_content').value).to eq('')
end
- it 'shows the autocompletion dropdown', :js do
+ it 'shows the emoji autocompletion dropdown', :js do
click_link('Edit')
find('#wiki_content').native.send_keys('')
- fill_in(:wiki_content, with: '@')
+ fill_in(:wiki_content, with: ':')
expect(page).to have_selector('.atwho-view')
end
diff --git a/spec/features/projects/wiki/user_views_wiki_empty_spec.rb b/spec/features/projects/wiki/user_views_wiki_empty_spec.rb
new file mode 100644
index 00000000000..83ffbb4a94e
--- /dev/null
+++ b/spec/features/projects/wiki/user_views_wiki_empty_spec.rb
@@ -0,0 +1,75 @@
+require 'spec_helper'
+
+describe 'User views empty wiki' do
+ let(:user) { create(:user) }
+
+ shared_examples 'empty wiki and accessible issues' do
+ it 'show "issue tracker" message' do
+ visit(project_wikis_path(project))
+
+ element = page.find('.row.empty-state')
+
+ expect(element).to have_content('This project has no wiki pages')
+ expect(element).to have_link("issue tracker", href: project_issues_path(project))
+ expect(element).to have_link("Suggest wiki improvement", href: new_project_issue_path(project))
+ end
+ end
+
+ shared_examples 'empty wiki and non-accessible issues' do
+ it 'does not show "issue tracker" message' do
+ visit(project_wikis_path(project))
+
+ element = page.find('.row.empty-state')
+
+ expect(element).to have_content('This project has no wiki pages')
+ expect(element).to have_no_link('Suggest wiki improvement')
+ end
+ end
+
+ context 'when user is logged out and issue tracker is public' do
+ let(:project) { create(:project, :public, :wiki_repo) }
+
+ it_behaves_like 'empty wiki and accessible issues'
+ end
+
+ context 'when user is logged in and not a member' do
+ let(:project) { create(:project, :public, :wiki_repo) }
+
+ before do
+ sign_in(user)
+ end
+
+ it_behaves_like 'empty wiki and accessible issues'
+ end
+
+ context 'when issue tracker is private' do
+ let(:project) { create(:project, :public, :wiki_repo, :issues_private) }
+
+ it_behaves_like 'empty wiki and non-accessible issues'
+ end
+
+ context 'when issue tracker is disabled' do
+ let(:project) { create(:project, :public, :wiki_repo, :issues_disabled) }
+
+ it_behaves_like 'empty wiki and non-accessible issues'
+ end
+
+ context 'when user is logged in and a memeber' do
+ let(:project) { create(:project, :public, :wiki_repo) }
+
+ before do
+ sign_in(user)
+ project.add_developer(user)
+ end
+
+ it 'show "create first page" message' do
+ visit(project_wikis_path(project))
+
+ element = page.find('.row.empty-state')
+
+ element.click_link 'Create your first page'
+
+ expect(page).to have_button('Create page')
+ end
+ end
+end
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 92b50169476..fb0ebe22bf7 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
@@ -4,7 +4,7 @@ describe 'Projects > Wiki > User views wiki in project page' do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/features/projects/wiki/user_views_wiki_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_page_spec.rb
index 6661714222a..0ef7f35f64a 100644
--- a/spec/features/projects/wiki/user_views_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_views_wiki_page_spec.rb
@@ -11,13 +11,14 @@ describe 'User views a wiki page' do
end
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
context 'when wiki is empty' do
before do
visit(project_wikis_path(project))
+ click_link "Create your first page"
click_on('New page')
@@ -140,6 +141,7 @@ describe 'User views a wiki page' do
visit(project_path(project))
find('.shortcuts-wiki').click
+ click_link "Create your first page"
expect(page).to have_content('Home · Create Page')
end
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index cfe979a8647..39b47d99040 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Project' do
+describe 'Project' do
include ProjectForksHelper
describe 'creating from template' do
@@ -151,10 +151,16 @@ feature 'Project' do
before do
sign_in(user)
- project.add_master(user)
+ project.add_maintainer(user)
visit edit_project_path(project)
end
+ it 'focuses on the confirmation field' do
+ click_button 'Remove project'
+
+ expect(page).to have_selector '#confirm_name_input:focus'
+ end
+
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 '#{project.full_name}' is in the process of being deleted."
@@ -169,7 +175,7 @@ feature 'Project' do
let(:project) { create(:forked_project_with_submodules) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in user
visit project_path(project)
end
@@ -198,7 +204,7 @@ feature 'Project' do
let(:project) { create(:project, :repository) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in user
visit project_path(project)
end
diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb
index 43cabd3b9f2..63c38a25f4b 100644
--- a/spec/features/protected_branches_spec.rb
+++ b/spec/features/protected_branches_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Protected Branches', :js do
+describe 'Protected Branches', :js do
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let(:project) { create(:project, :repository) }
@@ -28,9 +28,9 @@ feature 'Protected Branches', :js do
end
end
- context 'logged in as master' do
+ context 'logged in as maintainer' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
@@ -60,31 +60,6 @@ feature 'Protected Branches', :js do
expect(page).to have_content('No branches to show')
end
end
-
- describe "Saved defaults" do
- it "keeps the allowed to merge and push dropdowns defaults based on the previous selection" do
- visit project_protected_branches_path(project)
- form = '.js-new-protected-branch'
-
- within form do
- find(".js-allowed-to-merge").click
- click_link 'No one'
- find(".js-allowed-to-push").click
- click_link 'Developers + Masters'
- end
-
- visit project_protected_branches_path(project)
-
- within form do
- page.within(".js-allowed-to-merge") do
- expect(page.find(".dropdown-toggle-text")).to have_content("No one")
- end
- page.within(".js-allowed-to-push") do
- expect(page.find(".dropdown-toggle-text")).to have_content("Developers + Masters")
- end
- end
- end
- end
end
context 'logged in as admin' do
@@ -95,6 +70,7 @@ feature 'Protected Branches', :js do
describe "explicit protected branches" do
it "allows creating explicit protected branches" do
visit project_protected_branches_path(project)
+ set_defaults
set_protected_branch_name('some-branch')
click_on "Protect"
@@ -108,6 +84,7 @@ feature 'Protected Branches', :js do
project.repository.add_branch(admin, 'some-branch', commit.id)
visit project_protected_branches_path(project)
+ set_defaults
set_protected_branch_name('some-branch')
click_on "Protect"
@@ -116,6 +93,7 @@ feature 'Protected Branches', :js do
it "displays an error message if the named branch does not exist" do
visit project_protected_branches_path(project)
+ set_defaults
set_protected_branch_name('some-branch')
click_on "Protect"
@@ -126,6 +104,7 @@ feature 'Protected Branches', :js do
describe "wildcard protected branches" do
it "allows creating protected branches with a wildcard" do
visit project_protected_branches_path(project)
+ set_defaults
set_protected_branch_name('*-stable')
click_on "Protect"
@@ -139,6 +118,7 @@ feature 'Protected Branches', :js do
project.repository.add_branch(admin, 'staging-stable', 'master')
visit project_protected_branches_path(project)
+ set_defaults
set_protected_branch_name('*-stable')
click_on "Protect"
@@ -155,6 +135,7 @@ feature 'Protected Branches', :js do
visit project_protected_branches_path(project)
set_protected_branch_name('*-stable')
+ set_defaults
click_on "Protect"
visit project_protected_branches_path(project)
@@ -178,4 +159,18 @@ feature 'Protected Branches', :js do
find(".dropdown-input-field").set(branch_name)
click_on("Create wildcard #{branch_name}")
end
+
+ def set_defaults
+ find(".js-allowed-to-merge").click
+ within('.qa-allowed-to-merge-dropdown') do
+ expect(first("li")).to have_content("Roles")
+ find(:link, 'No one').click
+ end
+
+ find(".js-allowed-to-push").click
+ within('.qa-allowed-to-push-dropdown') do
+ expect(first("li")).to have_content("Roles")
+ find(:link, 'No one').click
+ end
+ end
end
diff --git a/spec/features/protected_tags_spec.rb b/spec/features/protected_tags_spec.rb
index efccaeaff6c..c8e92cd1c07 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 do
+describe '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 a9e815eaf4f..b0923b451ee 100644
--- a/spec/features/raven_js_spec.rb
+++ b/spec/features/raven_js_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'RavenJS' do
+describe 'RavenJS' do
let(:raven_path) { '/raven.chunk.js' }
it 'should not load raven if sentry is disabled' do
diff --git a/spec/features/reportable_note/commit_spec.rb b/spec/features/reportable_note/commit_spec.rb
index 9b6864eb90f..54ebda9dcab 100644
--- a/spec/features/reportable_note/commit_spec.rb
+++ b/spec/features/reportable_note/commit_spec.rb
@@ -7,7 +7,7 @@ describe 'Reportable note on commit', :js do
let(:project) { create(:project, :repository) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/features/reportable_note/issue_spec.rb b/spec/features/reportable_note/issue_spec.rb
index f5a1950e48e..bce1f7a3780 100644
--- a/spec/features/reportable_note/issue_spec.rb
+++ b/spec/features/reportable_note/issue_spec.rb
@@ -7,7 +7,7 @@ describe 'Reportable note on issue', :js do
let!(:note) { create(:note_on_issue, noteable: issue, project: project) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit project_issue_path(project, issue)
diff --git a/spec/features/reportable_note/merge_request_spec.rb b/spec/features/reportable_note/merge_request_spec.rb
index 1f69257f7ed..d00324156c4 100644
--- a/spec/features/reportable_note/merge_request_spec.rb
+++ b/spec/features/reportable_note/merge_request_spec.rb
@@ -6,7 +6,7 @@ describe 'Reportable note on merge request', :js do
let(:merge_request) { create(:merge_request, source_project: project) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit project_merge_request_path(project, merge_request)
diff --git a/spec/features/reportable_note/snippets_spec.rb b/spec/features/reportable_note/snippets_spec.rb
index 98ef50b78de..06218d9b286 100644
--- a/spec/features/reportable_note/snippets_spec.rb
+++ b/spec/features/reportable_note/snippets_spec.rb
@@ -5,7 +5,7 @@ describe 'Reportable note on snippets', :js do
let(:project) { create(:project) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb
index e0cd963fe39..0c6cf3dc477 100644
--- a/spec/features/runners_spec.rb
+++ b/spec/features/runners_spec.rb
@@ -1,20 +1,20 @@
require 'spec_helper'
-feature 'Runners' do
- given(:user) { create(:user) }
+describe 'Runners' do
+ let(:user) { create(:user) }
- background do
+ before do
sign_in(user)
end
context 'when user opens runners page' do
- given(:project) { create(:project) }
+ let(:project) { create(:project) }
- background do
- project.add_master(user)
+ before do
+ project.add_maintainer(user)
end
- scenario 'user can see a button to install runners on kubernetes clusters' do
+ it 'user can see a button to install runners on kubernetes clusters' do
visit project_runners_path(project)
expect(page).to have_link('Install Runner on Kubernetes', href: project_clusters_path(project))
@@ -22,20 +22,16 @@ feature 'Runners' do
end
context 'when a project has enabled shared_runners' do
- given(:project) { create(:project) }
+ let(:project) { create(:project) }
- background do
- project.add_master(user)
+ before do
+ project.add_maintainer(user)
end
- context 'when a specific runner is activated on the project' do
- given(:specific_runner) { create(:ci_runner, :specific) }
+ context 'when a project_type runner is activated on the project' do
+ let!(:specific_runner) { create(:ci_runner, :project, projects: [project]) }
- background do
- project.runners << specific_runner
- end
-
- scenario 'user sees the specific runner' do
+ it 'user sees the specific runner' do
visit project_runners_path(project)
within '.activated-specific-runners' do
@@ -47,7 +43,7 @@ feature 'Runners' do
expect(page).to have_content(specific_runner.platform)
end
- scenario 'user can pause and resume the specific runner' do
+ it 'user can pause and resume the specific runner' do
visit project_runners_path(project)
within '.activated-specific-runners' do
@@ -67,7 +63,7 @@ feature 'Runners' do
end
end
- scenario 'user removes an activated specific runner if this is last project for that runners' do
+ it 'user removes an activated specific runner if this is last project for that runners' do
visit project_runners_path(project)
within '.activated-specific-runners' do
@@ -77,7 +73,7 @@ feature 'Runners' do
expect(page).not_to have_content(specific_runner.display_name)
end
- scenario 'user edits the runner to be protected' do
+ it 'user edits the runner to be protected' do
visit project_runners_path(project)
within '.activated-specific-runners' do
@@ -93,11 +89,11 @@ feature 'Runners' do
end
context 'when a runner has a tag' do
- background do
+ before do
specific_runner.update(tag_list: ['tag'])
end
- scenario 'user edits runner not to run untagged jobs' do
+ it 'user edits runner not to run untagged jobs' do
visit project_runners_path(project)
within '.activated-specific-runners' do
@@ -114,9 +110,9 @@ feature 'Runners' do
end
context 'when a shared runner is activated on the project' do
- given!(:shared_runner) { create(:ci_runner, :shared) }
+ let!(:shared_runner) { create(:ci_runner, :instance) }
- scenario 'user sees CI/CD setting page' do
+ it 'user sees CI/CD setting page' do
visit project_runners_path(project)
expect(page.find('.available-shared-runners')).to have_content(shared_runner.display_name)
@@ -125,15 +121,14 @@ feature 'Runners' do
end
context 'when a specific runner exists in another project' do
- given(:another_project) { create(:project) }
- given(:specific_runner) { create(:ci_runner, :specific) }
+ let(:another_project) { create(:project) }
+ let!(:specific_runner) { create(:ci_runner, :project, projects: [another_project]) }
- background do
- another_project.add_master(user)
- another_project.runners << specific_runner
+ before do
+ another_project.add_maintainer(user)
end
- scenario 'user enables and disables a specific runner' do
+ it 'user enables and disables a specific runner' do
visit project_runners_path(project)
within '.available-specific-runners' do
@@ -151,14 +146,14 @@ feature 'Runners' do
end
context 'when application settings have shared_runners_text' do
- given(:shared_runners_text) { 'custom **shared** runners description' }
- given(:shared_runners_html) { 'custom shared runners description' }
+ let(:shared_runners_text) { 'custom **shared** runners description' }
+ let(:shared_runners_html) { 'custom shared runners description' }
- background do
+ before do
stub_application_setting(shared_runners_text: shared_runners_text)
end
- scenario 'user sees shared runners description' do
+ it 'user sees shared runners description' do
visit project_runners_path(project)
expect(page.find('.shared-runners-description')).to have_content(shared_runners_html)
@@ -167,13 +162,13 @@ feature 'Runners' do
end
context 'when a project has disabled shared_runners' do
- given(:project) { create(:project, shared_runners_enabled: false) }
+ let(:project) { create(:project, shared_runners_enabled: false) }
- background do
- project.add_master(user)
+ before do
+ project.add_maintainer(user)
end
- scenario 'user enables shared runners' do
+ it 'user enables shared runners' do
visit project_runners_path(project)
click_on 'Enable shared Runners'
@@ -183,36 +178,36 @@ feature 'Runners' do
end
context 'group runners in project settings' do
- background do
- project.add_master(user)
+ before do
+ project.add_maintainer(user)
end
- given(:group) { create :group }
+ let(:group) { create :group }
- context 'as project and group master' do
- background do
- group.add_master(user)
+ context 'as project and group maintainer' do
+ before do
+ group.add_maintainer(user)
end
context 'project with a group but no group runner' do
- given(:project) { create :project, group: group }
+ let(:project) { create :project, group: group }
- scenario 'group runners are not available' do
+ it 'group runners are not available' do
visit project_runners_path(project)
expect(page).to have_content 'This group does not provide any group Runners yet'
- expect(page).to have_content 'Group masters can register group runners in the Group CI/CD settings'
- expect(page).not_to have_content 'Ask your group master to setup a group Runner'
+ expect(page).to have_content 'Group maintainers can register group runners in the Group CI/CD settings'
+ expect(page).not_to have_content 'Ask your group maintainer to setup a group Runner'
end
end
end
- context 'as project master' do
+ context 'as project maintainer' do
context 'project without a group' do
- given(:project) { create :project }
+ let(:project) { create :project }
- scenario 'group runners are not available' do
+ it 'group runners are not available' do
visit project_runners_path(project)
expect(page).to have_content 'This project does not belong to a group and can therefore not make use of group Runners.'
@@ -220,32 +215,32 @@ feature 'Runners' do
end
context 'project with a group but no group runner' do
- given(:group) { create :group }
- given(:project) { create :project, group: group }
+ let(:group) { create(:group) }
+ let(:project) { create(:project, group: group) }
- scenario 'group runners are not available' do
+ it 'group runners are not available' do
visit project_runners_path(project)
expect(page).to have_content 'This group does not provide any group Runners yet.'
- expect(page).not_to have_content 'Group masters can register group runners in the Group CI/CD settings'
- expect(page).to have_content 'Ask your group master to setup a group Runner.'
+ expect(page).not_to have_content 'Group maintainers can register group runners in the Group CI/CD settings'
+ expect(page).to have_content 'Ask your group maintainer to setup a group Runner.'
end
end
context 'project with a group and a group runner' do
- given(:group) { create :group }
- given(:project) { create :project, group: group }
- given!(:ci_runner) { create :ci_runner, groups: [group], description: 'group-runner' }
+ let(:group) { create(:group) }
+ let(:project) { create(:project, group: group) }
+ let!(:ci_runner) { create(:ci_runner, :group, groups: [group], description: 'group-runner') }
- scenario 'group runners are available' do
+ it 'group runners are available' do
visit project_runners_path(project)
expect(page).to have_content 'Available group Runners : 1'
expect(page).to have_content 'group-runner'
end
- scenario 'group runners may be disabled for a project' do
+ it 'group runners may be disabled for a project' do
visit project_runners_path(project)
click_on 'Disable group Runners'
@@ -263,13 +258,13 @@ feature 'Runners' do
end
context 'group runners in group settings' do
- given(:group) { create :group }
- background do
- group.add_master(user)
+ let(:group) { create(:group) }
+ before do
+ group.add_maintainer(user)
end
context 'group with no runners' do
- scenario 'there are no runners displayed' do
+ it 'there are no runners displayed' do
visit group_settings_ci_cd_path(group)
expect(page).to have_content 'This group does not provide any group Runners yet'
@@ -277,9 +272,9 @@ feature 'Runners' do
end
context 'group with a runner' do
- let!(:runner) { create :ci_runner, groups: [group], description: 'group-runner' }
+ let!(:runner) { create(:ci_runner, :group, groups: [group], description: 'group-runner') }
- scenario 'the runner is visible' do
+ it 'the runner is visible' do
visit group_settings_ci_cd_path(group)
expect(page).not_to have_content 'This group does not provide any group Runners yet'
@@ -287,7 +282,7 @@ feature 'Runners' do
expect(page).to have_content 'group-runner'
end
- scenario 'user can pause and resume the group runner' do
+ it 'user can pause and resume the group runner' do
visit group_settings_ci_cd_path(group)
expect(page).to have_content('Pause')
@@ -304,7 +299,7 @@ feature 'Runners' do
expect(page).not_to have_content('Resume')
end
- scenario 'user can view runner details' do
+ it 'user can view runner details' do
visit group_settings_ci_cd_path(group)
expect(page).to have_content(runner.display_name)
@@ -314,7 +309,7 @@ feature 'Runners' do
expect(page).to have_content(runner.platform)
end
- scenario 'user can remove a group runner' do
+ it 'user can remove a group runner' do
visit group_settings_ci_cd_path(group)
click_on 'Remove Runner'
@@ -322,7 +317,7 @@ feature 'Runners' do
expect(page).not_to have_content(runner.display_name)
end
- scenario 'user edits the runner to be protected' do
+ it 'user edits the runner to be protected' do
visit group_settings_ci_cd_path(group)
first('.edit-runner > a').click
@@ -336,11 +331,11 @@ feature 'Runners' do
end
context 'when a runner has a tag' do
- background do
+ before do
runner.update(tag_list: ['tag'])
end
- scenario 'user edits runner not to run untagged jobs' do
+ it 'user edits runner not to run untagged jobs' do
visit group_settings_ci_cd_path(group)
first('.edit-runner > a').click
diff --git a/spec/features/search/user_searches_for_code_spec.rb b/spec/features/search/user_searches_for_code_spec.rb
index 9e089c5a6cb..ecec2f3e043 100644
--- a/spec/features/search/user_searches_for_code_spec.rb
+++ b/spec/features/search/user_searches_for_code_spec.rb
@@ -6,7 +6,7 @@ describe 'User searches for code' do
context 'when signed in' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/features/search/user_searches_for_issues_spec.rb b/spec/features/search/user_searches_for_issues_spec.rb
index d6120ff8517..4bff269f89e 100644
--- a/spec/features/search/user_searches_for_issues_spec.rb
+++ b/spec/features/search/user_searches_for_issues_spec.rb
@@ -8,7 +8,7 @@ describe 'User searches for issues', :js do
context 'when signed in' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit(search_path)
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 68e2f7a857d..75d44e413cb 100644
--- a/spec/features/search/user_searches_for_merge_requests_spec.rb
+++ b/spec/features/search/user_searches_for_merge_requests_spec.rb
@@ -7,7 +7,7 @@ describe 'User searches for merge requests', :js do
let!(:merge_request2) { create(:merge_request, :simple, title: 'Bar', source_project: project, target_project: project) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit(search_path)
diff --git a/spec/features/search/user_searches_for_milestones_spec.rb b/spec/features/search/user_searches_for_milestones_spec.rb
index fc6cd81eb68..7d52c4c8bcc 100644
--- a/spec/features/search/user_searches_for_milestones_spec.rb
+++ b/spec/features/search/user_searches_for_milestones_spec.rb
@@ -7,7 +7,7 @@ describe 'User searches for milestones', :js do
let!(:milestone2) { create(:milestone, title: 'Bar', project: project) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit(search_path)
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 5098fb49ee1..3ee753b7d23 100644
--- a/spec/features/search/user_searches_for_wiki_pages_spec.rb
+++ b/spec/features/search/user_searches_for_wiki_pages_spec.rb
@@ -6,7 +6,7 @@ describe 'User searches for wiki pages', :js do
let!(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: { title: 'test_wiki', content: 'Some Wiki content' }) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit(search_path)
diff --git a/spec/features/security/group/internal_access_spec.rb b/spec/features/security/group/internal_access_spec.rb
index 5067f0b0a49..51b32ba6c03 100644
--- a/spec/features/security/group/internal_access_spec.rb
+++ b/spec/features/security/group/internal_access_spec.rb
@@ -23,7 +23,7 @@ describe 'Internal Group access' do
it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(group) }
- it { is_expected.to be_allowed_for(:master).of(group) }
+ it { is_expected.to be_allowed_for(:maintainer).of(group) }
it { is_expected.to be_allowed_for(:developer).of(group) }
it { is_expected.to be_allowed_for(:reporter).of(group) }
it { is_expected.to be_allowed_for(:guest).of(group) }
@@ -38,7 +38,7 @@ describe 'Internal Group access' do
it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(group) }
- it { is_expected.to be_allowed_for(:master).of(group) }
+ it { is_expected.to be_allowed_for(:maintainer).of(group) }
it { is_expected.to be_allowed_for(:developer).of(group) }
it { is_expected.to be_allowed_for(:reporter).of(group) }
it { is_expected.to be_allowed_for(:guest).of(group) }
@@ -54,7 +54,7 @@ describe 'Internal Group access' do
it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(group) }
- it { is_expected.to be_allowed_for(:master).of(group) }
+ it { is_expected.to be_allowed_for(:maintainer).of(group) }
it { is_expected.to be_allowed_for(:developer).of(group) }
it { is_expected.to be_allowed_for(:reporter).of(group) }
it { is_expected.to be_allowed_for(:guest).of(group) }
@@ -69,7 +69,7 @@ describe 'Internal Group access' do
it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(group) }
- it { is_expected.to be_allowed_for(:master).of(group) }
+ it { is_expected.to be_allowed_for(:maintainer).of(group) }
it { is_expected.to be_allowed_for(:developer).of(group) }
it { is_expected.to be_allowed_for(:reporter).of(group) }
it { is_expected.to be_allowed_for(:guest).of(group) }
@@ -84,7 +84,7 @@ describe 'Internal Group access' do
it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(group) }
- it { is_expected.to be_denied_for(:master).of(group) }
+ it { is_expected.to be_denied_for(:maintainer).of(group) }
it { is_expected.to be_denied_for(:developer).of(group) }
it { is_expected.to be_denied_for(:reporter).of(group) }
it { is_expected.to be_denied_for(:guest).of(group) }
diff --git a/spec/features/security/group/private_access_spec.rb b/spec/features/security/group/private_access_spec.rb
index ff32413dc7e..4705cd12d23 100644
--- a/spec/features/security/group/private_access_spec.rb
+++ b/spec/features/security/group/private_access_spec.rb
@@ -23,7 +23,7 @@ describe 'Private Group access' do
it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(group) }
- it { is_expected.to be_allowed_for(:master).of(group) }
+ it { is_expected.to be_allowed_for(:maintainer).of(group) }
it { is_expected.to be_allowed_for(:developer).of(group) }
it { is_expected.to be_allowed_for(:reporter).of(group) }
it { is_expected.to be_allowed_for(:guest).of(group) }
@@ -38,7 +38,7 @@ describe 'Private Group access' do
it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(group) }
- it { is_expected.to be_allowed_for(:master).of(group) }
+ it { is_expected.to be_allowed_for(:maintainer).of(group) }
it { is_expected.to be_allowed_for(:developer).of(group) }
it { is_expected.to be_allowed_for(:reporter).of(group) }
it { is_expected.to be_allowed_for(:guest).of(group) }
@@ -54,7 +54,7 @@ describe 'Private Group access' do
it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(group) }
- it { is_expected.to be_allowed_for(:master).of(group) }
+ it { is_expected.to be_allowed_for(:maintainer).of(group) }
it { is_expected.to be_allowed_for(:developer).of(group) }
it { is_expected.to be_allowed_for(:reporter).of(group) }
it { is_expected.to be_allowed_for(:guest).of(group) }
@@ -69,7 +69,7 @@ describe 'Private Group access' do
it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(group) }
- it { is_expected.to be_allowed_for(:master).of(group) }
+ it { is_expected.to be_allowed_for(:maintainer).of(group) }
it { is_expected.to be_allowed_for(:developer).of(group) }
it { is_expected.to be_allowed_for(:reporter).of(group) }
it { is_expected.to be_allowed_for(:guest).of(group) }
@@ -84,7 +84,7 @@ describe 'Private Group access' do
it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(group) }
- it { is_expected.to be_denied_for(:master).of(group) }
+ it { is_expected.to be_denied_for(:maintainer).of(group) }
it { is_expected.to be_denied_for(:developer).of(group) }
it { is_expected.to be_denied_for(:reporter).of(group) }
it { is_expected.to be_denied_for(:guest).of(group) }
diff --git a/spec/features/security/group/public_access_spec.rb b/spec/features/security/group/public_access_spec.rb
index 16d114fb3f7..3a53c3c2bc7 100644
--- a/spec/features/security/group/public_access_spec.rb
+++ b/spec/features/security/group/public_access_spec.rb
@@ -23,7 +23,7 @@ describe 'Public Group access' do
it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(group) }
- it { is_expected.to be_allowed_for(:master).of(group) }
+ it { is_expected.to be_allowed_for(:maintainer).of(group) }
it { is_expected.to be_allowed_for(:developer).of(group) }
it { is_expected.to be_allowed_for(:reporter).of(group) }
it { is_expected.to be_allowed_for(:guest).of(group) }
@@ -38,7 +38,7 @@ describe 'Public Group access' do
it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(group) }
- it { is_expected.to be_allowed_for(:master).of(group) }
+ it { is_expected.to be_allowed_for(:maintainer).of(group) }
it { is_expected.to be_allowed_for(:developer).of(group) }
it { is_expected.to be_allowed_for(:reporter).of(group) }
it { is_expected.to be_allowed_for(:guest).of(group) }
@@ -54,7 +54,7 @@ describe 'Public Group access' do
it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(group) }
- it { is_expected.to be_allowed_for(:master).of(group) }
+ it { is_expected.to be_allowed_for(:maintainer).of(group) }
it { is_expected.to be_allowed_for(:developer).of(group) }
it { is_expected.to be_allowed_for(:reporter).of(group) }
it { is_expected.to be_allowed_for(:guest).of(group) }
@@ -69,7 +69,7 @@ describe 'Public Group access' do
it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(group) }
- it { is_expected.to be_allowed_for(:master).of(group) }
+ it { is_expected.to be_allowed_for(:maintainer).of(group) }
it { is_expected.to be_allowed_for(:developer).of(group) }
it { is_expected.to be_allowed_for(:reporter).of(group) }
it { is_expected.to be_allowed_for(:guest).of(group) }
@@ -84,7 +84,7 @@ describe 'Public Group access' do
it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(group) }
- it { is_expected.to be_denied_for(:master).of(group) }
+ it { is_expected.to be_denied_for(:maintainer).of(group) }
it { is_expected.to be_denied_for(:developer).of(group) }
it { is_expected.to be_denied_for(:reporter).of(group) }
it { is_expected.to be_denied_for(:guest).of(group) }
diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb
index a7928857b7d..001e6c10eb2 100644
--- a/spec/features/security/project/internal_access_spec.rb
+++ b/spec/features/security/project/internal_access_spec.rb
@@ -17,7 +17,7 @@ describe "Internal Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -31,7 +31,7 @@ describe "Internal Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -45,7 +45,7 @@ describe "Internal Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -59,7 +59,7 @@ describe "Internal Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -73,7 +73,7 @@ describe "Internal Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -87,7 +87,7 @@ describe "Internal Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -101,7 +101,7 @@ describe "Internal Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_denied_for(:developer).of(project) }
it { is_expected.to be_denied_for(:reporter).of(project) }
it { is_expected.to be_denied_for(:guest).of(project) }
@@ -115,7 +115,7 @@ describe "Internal Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_denied_for(:developer).of(project) }
it { is_expected.to be_denied_for(:reporter).of(project) }
it { is_expected.to be_denied_for(:guest).of(project) }
@@ -130,7 +130,7 @@ describe "Internal Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -144,7 +144,7 @@ describe "Internal Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_denied_for(:developer).of(project) }
it { is_expected.to be_denied_for(:reporter).of(project) }
it { is_expected.to be_denied_for(:guest).of(project) }
@@ -158,7 +158,7 @@ describe "Internal Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_denied_for(:developer).of(project) }
it { is_expected.to be_denied_for(:reporter).of(project) }
it { is_expected.to be_denied_for(:guest).of(project) }
@@ -172,7 +172,7 @@ describe "Internal Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -187,7 +187,7 @@ describe "Internal Project Access" do
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(:maintainer).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) }
@@ -201,7 +201,7 @@ describe "Internal Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -215,7 +215,7 @@ describe "Internal Project Access" do
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(:maintainer).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) }
@@ -229,7 +229,7 @@ describe "Internal Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -243,7 +243,7 @@ describe "Internal Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_denied_for(:reporter).of(project) }
it { is_expected.to be_denied_for(:guest).of(project) }
@@ -262,7 +262,7 @@ describe "Internal Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -281,7 +281,7 @@ describe "Internal Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -295,7 +295,7 @@ describe "Internal Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_denied_for(:developer).of(project) }
it { is_expected.to be_denied_for(:reporter).of(project) }
it { is_expected.to be_denied_for(:guest).of(project) }
@@ -309,7 +309,7 @@ describe "Internal Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -324,7 +324,7 @@ describe "Internal Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -343,7 +343,7 @@ describe "Internal Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -359,7 +359,7 @@ describe "Internal Project Access" do
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(:maintainer).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) }
@@ -381,7 +381,7 @@ describe "Internal Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -397,7 +397,7 @@ describe "Internal Project Access" do
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(:maintainer).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) }
@@ -419,7 +419,7 @@ describe "Internal Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -435,7 +435,7 @@ describe "Internal Project Access" do
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(:maintainer).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) }
@@ -450,7 +450,7 @@ describe "Internal Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -464,7 +464,7 @@ describe "Internal Project Access" do
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(:maintainer).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) }
@@ -479,7 +479,7 @@ describe "Internal Project Access" do
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(:maintainer).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) }
@@ -494,7 +494,7 @@ describe "Internal Project Access" do
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(:maintainer).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) }
@@ -508,7 +508,7 @@ describe "Internal Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_denied_for(:reporter).of(project) }
it { is_expected.to be_denied_for(:guest).of(project) }
@@ -530,7 +530,7 @@ describe "Internal Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb
index a4396b20afd..c6618355eea 100644
--- a/spec/features/security/project/private_access_spec.rb
+++ b/spec/features/security/project/private_access_spec.rb
@@ -17,7 +17,7 @@ describe "Private Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -31,7 +31,7 @@ describe "Private Project Access" do
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(:maintainer).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) }
@@ -45,7 +45,7 @@ describe "Private Project Access" do
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(:maintainer).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) }
@@ -59,7 +59,7 @@ describe "Private Project Access" do
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(:maintainer).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) }
@@ -73,7 +73,7 @@ describe "Private Project Access" do
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(:maintainer).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) }
@@ -87,7 +87,7 @@ describe "Private Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -101,7 +101,7 @@ describe "Private Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_denied_for(:developer).of(project) }
it { is_expected.to be_denied_for(:reporter).of(project) }
it { is_expected.to be_denied_for(:guest).of(project) }
@@ -115,7 +115,7 @@ describe "Private Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_denied_for(:developer).of(project) }
it { is_expected.to be_denied_for(:reporter).of(project) }
it { is_expected.to be_denied_for(:guest).of(project) }
@@ -130,7 +130,7 @@ describe "Private Project Access" do
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(:maintainer).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) }
@@ -144,7 +144,7 @@ describe "Private Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_denied_for(:developer).of(project) }
it { is_expected.to be_denied_for(:reporter).of(project) }
it { is_expected.to be_denied_for(:guest).of(project) }
@@ -158,7 +158,7 @@ describe "Private Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_denied_for(:developer).of(project) }
it { is_expected.to be_denied_for(:reporter).of(project) }
it { is_expected.to be_denied_for(:guest).of(project) }
@@ -172,7 +172,7 @@ describe "Private Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -187,7 +187,7 @@ describe "Private Project Access" do
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(:maintainer).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) }
@@ -201,7 +201,7 @@ describe "Private Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -215,7 +215,7 @@ describe "Private Project Access" do
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(:maintainer).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) }
@@ -234,7 +234,7 @@ describe "Private Project Access" do
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(:maintainer).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) }
@@ -253,7 +253,7 @@ describe "Private Project Access" do
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(:maintainer).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) }
@@ -267,7 +267,7 @@ describe "Private Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_denied_for(:developer).of(project) }
it { is_expected.to be_denied_for(:reporter).of(project) }
it { is_expected.to be_denied_for(:guest).of(project) }
@@ -281,7 +281,7 @@ describe "Private Project Access" do
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(:maintainer).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) }
@@ -308,7 +308,7 @@ describe "Private Project Access" do
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(:maintainer).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) }
@@ -334,7 +334,7 @@ describe "Private Project Access" do
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(:maintainer).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) }
@@ -362,7 +362,7 @@ describe "Private Project Access" do
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(:maintainer).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) }
@@ -395,7 +395,7 @@ describe "Private Project Access" do
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(:maintainer).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) }
@@ -425,7 +425,7 @@ describe "Private Project Access" do
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(:maintainer).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) }
@@ -440,7 +440,7 @@ describe "Private Project Access" do
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(:maintainer).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) }
@@ -455,7 +455,7 @@ describe "Private Project Access" do
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(:maintainer).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) }
@@ -469,7 +469,7 @@ describe "Private Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_denied_for(:reporter).of(project) }
it { is_expected.to be_denied_for(:guest).of(project) }
@@ -483,7 +483,7 @@ describe "Private Project Access" do
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(:maintainer).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) }
@@ -497,7 +497,7 @@ describe "Private Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_denied_for(:reporter).of(project) }
it { is_expected.to be_denied_for(:guest).of(project) }
@@ -511,7 +511,7 @@ describe "Private Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_denied_for(:reporter).of(project) }
it { is_expected.to be_denied_for(:guest).of(project) }
@@ -533,7 +533,7 @@ describe "Private Project Access" do
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(:maintainer).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) }
diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb
index fccdeb0e5b7..3717dc13f1e 100644
--- a/spec/features/security/project/public_access_spec.rb
+++ b/spec/features/security/project/public_access_spec.rb
@@ -17,7 +17,7 @@ describe "Public Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -31,7 +31,7 @@ describe "Public Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -45,7 +45,7 @@ describe "Public Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -59,7 +59,7 @@ describe "Public Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -73,7 +73,7 @@ describe "Public Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -87,7 +87,7 @@ describe "Public Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -101,7 +101,7 @@ describe "Public Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_denied_for(:developer).of(project) }
it { is_expected.to be_denied_for(:reporter).of(project) }
it { is_expected.to be_denied_for(:guest).of(project) }
@@ -115,7 +115,7 @@ describe "Public Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_denied_for(:developer).of(project) }
it { is_expected.to be_denied_for(:reporter).of(project) }
it { is_expected.to be_denied_for(:guest).of(project) }
@@ -129,7 +129,7 @@ describe "Public Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -144,7 +144,7 @@ describe "Public Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -163,7 +163,7 @@ describe "Public Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -179,7 +179,7 @@ describe "Public Project Access" do
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(:maintainer).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) }
@@ -201,7 +201,7 @@ describe "Public Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -217,7 +217,7 @@ describe "Public Project Access" do
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(:maintainer).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) }
@@ -239,7 +239,7 @@ describe "Public Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -255,7 +255,7 @@ describe "Public Project Access" do
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(:maintainer).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) }
@@ -270,7 +270,7 @@ describe "Public Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -284,7 +284,7 @@ describe "Public Project Access" do
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(:maintainer).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) }
@@ -299,7 +299,7 @@ describe "Public Project Access" do
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(:maintainer).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) }
@@ -314,7 +314,7 @@ describe "Public Project Access" do
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(:maintainer).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) }
@@ -328,7 +328,7 @@ describe "Public Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_denied_for(:reporter).of(project) }
it { is_expected.to be_denied_for(:guest).of(project) }
@@ -344,7 +344,7 @@ describe "Public Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -357,7 +357,7 @@ describe "Public Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_denied_for(:developer).of(project) }
it { is_expected.to be_denied_for(:reporter).of(project) }
it { is_expected.to be_denied_for(:guest).of(project) }
@@ -371,7 +371,7 @@ describe "Public Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_denied_for(:developer).of(project) }
it { is_expected.to be_denied_for(:reporter).of(project) }
it { is_expected.to be_denied_for(:guest).of(project) }
@@ -385,7 +385,7 @@ describe "Public Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -400,7 +400,7 @@ describe "Public Project Access" do
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(:maintainer).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) }
@@ -414,7 +414,7 @@ describe "Public Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -428,7 +428,7 @@ describe "Public Project Access" do
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(:maintainer).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) }
@@ -442,7 +442,7 @@ describe "Public Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -456,7 +456,7 @@ describe "Public Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_denied_for(:reporter).of(project) }
it { is_expected.to be_denied_for(:guest).of(project) }
@@ -475,7 +475,7 @@ describe "Public Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -494,7 +494,7 @@ describe "Public Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -508,7 +508,7 @@ describe "Public Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_denied_for(:developer).of(project) }
it { is_expected.to be_denied_for(:reporter).of(project) }
it { is_expected.to be_denied_for(:guest).of(project) }
@@ -530,7 +530,7 @@ describe "Public Project Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
diff --git a/spec/features/security/project/snippet/internal_access_spec.rb b/spec/features/security/project/snippet/internal_access_spec.rb
index d7dc99c0a57..b87eb86b88b 100644
--- a/spec/features/security/project/snippet/internal_access_spec.rb
+++ b/spec/features/security/project/snippet/internal_access_spec.rb
@@ -13,7 +13,7 @@ describe "Internal Project Snippets Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -27,7 +27,7 @@ describe "Internal Project Snippets Access" do
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(:maintainer).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) }
@@ -42,7 +42,7 @@ describe "Internal Project Snippets Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -56,7 +56,7 @@ describe "Internal Project Snippets Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -72,7 +72,7 @@ describe "Internal Project Snippets Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -86,7 +86,7 @@ describe "Internal Project Snippets Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
diff --git a/spec/features/security/project/snippet/private_access_spec.rb b/spec/features/security/project/snippet/private_access_spec.rb
index 3ec1a388185..ead91d9a5fa 100644
--- a/spec/features/security/project/snippet/private_access_spec.rb
+++ b/spec/features/security/project/snippet/private_access_spec.rb
@@ -12,7 +12,7 @@ describe "Private Project Snippets Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -26,7 +26,7 @@ describe "Private Project Snippets Access" do
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(:maintainer).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) }
@@ -40,7 +40,7 @@ describe "Private Project Snippets Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -54,7 +54,7 @@ describe "Private Project Snippets Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
diff --git a/spec/features/security/project/snippet/public_access_spec.rb b/spec/features/security/project/snippet/public_access_spec.rb
index 39b104bfe27..9bab3a474b8 100644
--- a/spec/features/security/project/snippet/public_access_spec.rb
+++ b/spec/features/security/project/snippet/public_access_spec.rb
@@ -14,7 +14,7 @@ describe "Public Project Snippets Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -28,7 +28,7 @@ describe "Public Project Snippets Access" do
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(:maintainer).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) }
@@ -43,7 +43,7 @@ describe "Public Project Snippets Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -57,7 +57,7 @@ describe "Public Project Snippets Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -71,7 +71,7 @@ describe "Public Project Snippets Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -87,7 +87,7 @@ describe "Public Project Snippets Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -101,7 +101,7 @@ describe "Public Project Snippets Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
@@ -115,7 +115,7 @@ describe "Public Project Snippets Access" do
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(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
diff --git a/spec/features/signed_commits_spec.rb b/spec/features/signed_commits_spec.rb
index 6088b831c14..3d05474dca2 100644
--- a/spec/features/signed_commits_spec.rb
+++ b/spec/features/signed_commits_spec.rb
@@ -5,7 +5,7 @@ describe 'GPG signed commits', :js do
it 'changes from unverified to verified when the user changes his email to match the gpg key' do
user = create :user, email: 'unrelated.user@example.org'
- project.add_master(user)
+ project.add_maintainer(user)
Sidekiq::Testing.inline! do
create :gpg_key, key: GpgHelpers::User1.public_key, user: user
@@ -23,7 +23,7 @@ describe 'GPG signed commits', :js do
# user changes his email which makes the gpg key verified
Sidekiq::Testing.inline! do
user.skip_reconfirmation!
- user.update_attributes!(email: GpgHelpers::User1.emails.first)
+ user.update!(email: GpgHelpers::User1.emails.first)
end
visit project_commits_path(project, :'signed-commits')
@@ -36,7 +36,7 @@ describe 'GPG signed commits', :js do
it 'changes from unverified to verified when the user adds the missing gpg key' do
user = create :user, email: GpgHelpers::User1.emails.first
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
@@ -86,7 +86,7 @@ describe 'GPG signed commits', :js do
before do
user = create :user
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
@@ -96,10 +96,11 @@ describe 'GPG signed commits', :js do
within(find('.commit', text: 'signed commit by bette cartwright')) do
click_on 'Unverified'
- within '.popover' do
- expect(page).to have_content 'This commit was signed with an unverified signature.'
- expect(page).to have_content "GPG Key ID: #{GpgHelpers::User2.primary_keyid}"
- end
+ end
+
+ within '.popover' do
+ expect(page).to have_content 'This commit was signed with an unverified signature.'
+ expect(page).to have_content "GPG Key ID: #{GpgHelpers::User2.primary_keyid}"
end
end
@@ -110,12 +111,13 @@ describe 'GPG signed commits', :js do
within(find('.commit', text: 'signed and authored commit by bette cartwright, different email')) do
click_on 'Unverified'
- within '.popover' do
- expect(page).to have_content 'This commit was signed with a verified signature, but the committer email is not verified to belong to the same user.'
- expect(page).to have_content 'Bette Cartwright'
- expect(page).to have_content '@bette.cartwright'
- expect(page).to have_content "GPG Key ID: #{GpgHelpers::User2.primary_keyid}"
- end
+ end
+
+ within '.popover' do
+ expect(page).to have_content 'This commit was signed with a verified signature, but the committer email is not verified to belong to the same user.'
+ expect(page).to have_content 'Bette Cartwright'
+ expect(page).to have_content '@bette.cartwright'
+ expect(page).to have_content "GPG Key ID: #{GpgHelpers::User2.primary_keyid}"
end
end
@@ -126,12 +128,13 @@ describe 'GPG signed commits', :js do
within(find('.commit', text: 'signed commit by bette cartwright')) do
click_on 'Unverified'
- within '.popover' do
- expect(page).to have_content "This commit was signed with a different user's verified signature."
- expect(page).to have_content 'Bette Cartwright'
- expect(page).to have_content '@bette.cartwright'
- expect(page).to have_content "GPG Key ID: #{GpgHelpers::User2.primary_keyid}"
- end
+ end
+
+ within '.popover' do
+ expect(page).to have_content "This commit was signed with a different user's verified signature."
+ expect(page).to have_content 'Bette Cartwright'
+ expect(page).to have_content '@bette.cartwright'
+ expect(page).to have_content "GPG Key ID: #{GpgHelpers::User2.primary_keyid}"
end
end
@@ -142,12 +145,13 @@ describe 'GPG signed commits', :js do
within(find('.commit', text: 'signed and authored commit by nannie bernhard')) do
click_on 'Verified'
- within '.popover' do
- expect(page).to have_content 'This commit was signed with a verified signature and the committer email is verified to belong to the same user.'
- expect(page).to have_content 'Nannie Bernhard'
- expect(page).to have_content '@nannie.bernhard'
- expect(page).to have_content "GPG Key ID: #{GpgHelpers::User1.primary_keyid}"
- end
+ end
+
+ within '.popover' do
+ expect(page).to have_content 'This commit was signed with a verified signature and the committer email is verified to belong to the same user.'
+ expect(page).to have_content 'Nannie Bernhard'
+ expect(page).to have_content '@nannie.bernhard'
+ expect(page).to have_content "GPG Key ID: #{GpgHelpers::User1.primary_keyid}"
end
end
@@ -167,12 +171,13 @@ describe 'GPG signed commits', :js do
within(find('.commit', text: 'signed and authored commit by nannie bernhard')) do
click_on 'Verified'
- within '.popover' do
- expect(page).to have_content 'This commit was signed with a verified signature and the committer email is verified to belong to the same user.'
- expect(page).to have_content 'Nannie Bernhard'
- expect(page).to have_content 'nannie.bernhard@example.com'
- expect(page).to have_content "GPG Key ID: #{GpgHelpers::User1.primary_keyid}"
- end
+ end
+
+ within '.popover' do
+ expect(page).to have_content 'This commit was signed with a verified signature and the committer email is verified to belong to the same user.'
+ expect(page).to have_content 'Nannie Bernhard'
+ expect(page).to have_content 'nannie.bernhard@example.com'
+ expect(page).to have_content "GPG Key ID: #{GpgHelpers::User1.primary_keyid}"
end
end
end
diff --git a/spec/features/snippets/explore_spec.rb b/spec/features/snippets/explore_spec.rb
index 835fd90adc8..c08f25875f8 100644
--- a/spec/features/snippets/explore_spec.rb
+++ b/spec/features/snippets/explore_spec.rb
@@ -1,11 +1,11 @@
require 'rails_helper'
-feature 'Explore Snippets' do
+describe 'Explore Snippets' do
let!(:public_snippet) { create(:personal_snippet, :public) }
let!(:internal_snippet) { create(:personal_snippet, :internal) }
let!(:private_snippet) { create(:personal_snippet, :private) }
- scenario 'User should see snippets that are not private' do
+ it 'User should see snippets that are not private' do
sign_in create(:user)
visit explore_snippets_path
@@ -14,7 +14,7 @@ feature 'Explore Snippets' do
expect(page).not_to have_content(private_snippet.title)
end
- scenario 'External user should see only public snippets' do
+ it 'External user should see only public snippets' do
sign_in create(:user, :external)
visit explore_snippets_path
@@ -23,7 +23,7 @@ feature 'Explore Snippets' do
expect(page).not_to have_content(private_snippet.title)
end
- scenario 'Not authenticated user should see only public snippets' do
+ it 'Not authenticated user should see only public snippets' do
visit explore_snippets_path
expect(page).to have_content(public_snippet.title)
diff --git a/spec/features/snippets/internal_snippet_spec.rb b/spec/features/snippets/internal_snippet_spec.rb
index 3a2768c424f..f6215b481dc 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 do
+describe 'Internal Snippets', :js do
let(:internal_snippet) { create(:personal_snippet, :internal) }
describe 'normal user' do
@@ -8,13 +8,13 @@ feature 'Internal Snippets', :js do
sign_in(create(:user))
end
- scenario 'sees internal snippets' do
+ it 'sees internal snippets' do
visit snippet_path(internal_snippet)
expect(page).to have_content(internal_snippet.content)
end
- scenario 'sees raw internal snippets' do
+ it 'sees raw internal snippets' do
visit raw_snippet_path(internal_snippet)
expect(page).to have_content(internal_snippet.content)
diff --git a/spec/features/snippets/public_snippets_spec.rb b/spec/features/snippets/public_snippets_spec.rb
index bdeeca7187e..71c72b98fad 100644
--- a/spec/features/snippets/public_snippets_spec.rb
+++ b/spec/features/snippets/public_snippets_spec.rb
@@ -1,7 +1,7 @@
require 'rails_helper'
-feature 'Public Snippets', :js do
- scenario 'Unauthenticated user should see public snippets' do
+describe 'Public Snippets', :js do
+ it 'Unauthenticated user should see public snippets' do
public_snippet = create(:personal_snippet, :public)
visit snippet_path(public_snippet)
@@ -10,7 +10,7 @@ feature 'Public Snippets', :js do
expect(page).to have_content(public_snippet.content)
end
- scenario 'Unauthenticated user should see raw public snippets' do
+ it 'Unauthenticated user should see raw public snippets' do
public_snippet = create(:personal_snippet, :public)
visit raw_snippet_path(public_snippet)
diff --git a/spec/features/snippets/search_snippets_spec.rb b/spec/features/snippets/search_snippets_spec.rb
index cd66a2cb20c..c137b0bcd96 100644
--- a/spec/features/snippets/search_snippets_spec.rb
+++ b/spec/features/snippets/search_snippets_spec.rb
@@ -1,7 +1,7 @@
require 'rails_helper'
-feature 'Search Snippets' do
- scenario 'User searches for snippets by title' do
+describe 'Search Snippets' do
+ it 'User searches for snippets by title' do
public_snippet = create(:personal_snippet, :public, title: 'Beginning and Middle')
private_snippet = create(:personal_snippet, :private, title: 'Middle and End')
@@ -19,7 +19,7 @@ feature 'Search Snippets' do
expect(page).to have_link(private_snippet.title)
end
- scenario 'User searches for snippet contents' do
+ it 'User searches for snippet contents' do
create(:personal_snippet,
:public,
title: 'Many lined snippet',
diff --git a/spec/features/snippets/show_spec.rb b/spec/features/snippets/show_spec.rb
index 5a48f5774ca..f31457db92f 100644
--- a/spec/features/snippets/show_spec.rb
+++ b/spec/features/snippets/show_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Snippet', :js do
+describe 'Snippet', :js do
let(:project) { create(:project, :repository) }
let(:snippet) { create(:personal_snippet, :public, file_name: file_name, content: content) }
@@ -68,6 +68,26 @@ feature 'Snippet', :js do
end
end
+ context 'with cached Redcarpet html' do
+ let(:snippet) { create(:personal_snippet, :public, file_name: file_name, content: content, cached_markdown_version: CacheMarkdownField::CACHE_REDCARPET_VERSION) }
+ let(:file_name) { 'test.md' }
+ let(:content) { "1. one\n - sublist\n" }
+
+ it 'renders correctly' do
+ expect(page).to have_xpath("//ol//li//ul")
+ end
+ end
+
+ context 'with cached CommonMark html' do
+ let(:snippet) { create(:personal_snippet, :public, file_name: file_name, content: content, cached_markdown_version: CacheMarkdownField::CACHE_COMMONMARK_VERSION) }
+ let(:file_name) { 'test.md' }
+ let(:content) { "1. one\n - sublist\n" }
+
+ it 'renders correctly' do
+ expect(page).not_to have_xpath("//ol//li//ul")
+ end
+ end
+
context 'switching to the simple viewer' do
before do
find('.js-blob-viewer-switch-btn[data-viewer=simple]').click
diff --git a/spec/features/snippets/user_creates_snippet_spec.rb b/spec/features/snippets/user_creates_snippet_spec.rb
index 941765b7578..879c46d7c4e 100644
--- a/spec/features/snippets/user_creates_snippet_spec.rb
+++ b/spec/features/snippets/user_creates_snippet_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'User creates snippet', :js do
+describe 'User creates snippet', :js do
include DropzoneHelper
let(:user) { create(:user) }
@@ -18,7 +18,7 @@ feature 'User creates snippet', :js do
end
end
- scenario 'Authenticated user creates a snippet' do
+ it 'Authenticated user creates a snippet' do
fill_form
click_button('Create snippet')
@@ -32,7 +32,7 @@ feature 'User creates snippet', :js do
expect(page).to have_content('Hello World!')
end
- scenario 'previews a snippet with file' do
+ it 'previews a snippet with file' do
fill_in 'personal_snippet_description', with: 'My Snippet'
dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
find('.js-md-preview-button').click
@@ -48,7 +48,7 @@ feature 'User creates snippet', :js do
end
end
- scenario 'uploads a file when dragging into textarea' do
+ it 'uploads a file when dragging into textarea' do
fill_form
dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
@@ -65,7 +65,7 @@ feature 'User creates snippet', :js do
expect(reqs.first.status_code).to eq(200)
end
- scenario 'validation fails for the first time' do
+ it 'validation fails for the first time' do
fill_in 'personal_snippet_title', with: 'My Snippet Title'
click_button('Create snippet')
@@ -90,7 +90,7 @@ feature 'User creates snippet', :js do
expect(reqs.first.status_code).to eq(200)
end
- scenario 'Authenticated user creates a snippet with + in filename' do
+ it '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'
diff --git a/spec/features/snippets/user_deletes_snippet_spec.rb b/spec/features/snippets/user_deletes_snippet_spec.rb
index ae5b883c477..7bdccacb0fa 100644
--- a/spec/features/snippets/user_deletes_snippet_spec.rb
+++ b/spec/features/snippets/user_deletes_snippet_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'User deletes snippet' do
+describe 'User deletes snippet' do
let(:user) { create(:user) }
let(:content) { 'puts "test"' }
let(:snippet) { create(:personal_snippet, :public, content: content, author: user) }
diff --git a/spec/features/snippets/user_edits_snippet_spec.rb b/spec/features/snippets/user_edits_snippet_spec.rb
index 71de6b6bd1c..77f62990158 100644
--- a/spec/features/snippets/user_edits_snippet_spec.rb
+++ b/spec/features/snippets/user_edits_snippet_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'User edits snippet', :js do
+describe 'User edits snippet', :js do
include DropzoneHelper
let(:file_name) { 'test.rb' }
diff --git a/spec/features/snippets/user_snippets_spec.rb b/spec/features/snippets/user_snippets_spec.rb
index 7bc27486787..e3065a899cc 100644
--- a/spec/features/snippets/user_snippets_spec.rb
+++ b/spec/features/snippets/user_snippets_spec.rb
@@ -1,23 +1,23 @@
require 'rails_helper'
-feature 'User Snippets' do
+describe 'User Snippets' do
let(:author) { create(:user) }
let!(:public_snippet) { create(:personal_snippet, :public, author: author, title: "This is a public snippet") }
let!(:internal_snippet) { create(:personal_snippet, :internal, author: author, title: "This is an internal snippet") }
let!(:private_snippet) { create(:personal_snippet, :private, author: author, title: "This is a private snippet") }
- background do
+ before do
sign_in author
visit dashboard_snippets_path
end
- scenario 'View all of my snippets' do
+ it 'View all of my snippets' do
expect(page).to have_content(public_snippet.title)
expect(page).to have_content(internal_snippet.title)
expect(page).to have_content(private_snippet.title)
end
- scenario 'View my public snippets' do
+ it 'View my public snippets' do
page.within('.snippet-scope-menu') do
click_link "Public"
end
@@ -27,7 +27,7 @@ feature 'User Snippets' do
expect(page).not_to have_content(private_snippet.title)
end
- scenario 'View my internal snippets' do
+ it 'View my internal snippets' do
page.within('.snippet-scope-menu') do
click_link "Internal"
end
@@ -37,7 +37,7 @@ feature 'User Snippets' do
expect(page).not_to have_content(private_snippet.title)
end
- scenario 'View my private snippets' do
+ it 'View my private snippets' do
page.within('.snippet-scope-menu') do
click_link "Private"
end
diff --git a/spec/features/tags/master_creates_tag_spec.rb b/spec/features/tags/master_creates_tag_spec.rb
index 8a8f6933fa5..db2970f3340 100644
--- a/spec/features/tags/master_creates_tag_spec.rb
+++ b/spec/features/tags/master_creates_tag_spec.rb
@@ -1,11 +1,11 @@
require 'spec_helper'
-feature 'Master creates tag' do
+describe 'Maintainer creates tag' do
let(:user) { create(:user) }
let(:project) { create(:project, :repository, namespace: user.namespace) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
@@ -14,25 +14,25 @@ feature 'Master creates tag' do
visit project_tags_path(project)
end
- scenario 'with an invalid name displays an error' do
+ it 'with an invalid name displays an error' do
create_tag_in_form(tag: 'v 1.0', ref: 'master')
expect(page).to have_content 'Tag name invalid'
end
- scenario 'with an invalid reference displays an error' do
+ it 'with an invalid reference displays an error' do
create_tag_in_form(tag: 'v2.0', ref: 'foo')
expect(page).to have_content 'Target foo is invalid'
end
- scenario 'that already exists displays an error' do
+ it 'that already exists displays an error' do
create_tag_in_form(tag: 'v1.1.0', ref: 'master')
expect(page).to have_content 'Tag v1.1.0 already exists'
end
- scenario 'with multiline message displays the message in a <pre> block' do
+ it 'with multiline message displays the message in a <pre> block' do
create_tag_in_form(tag: 'v3.0', ref: 'master', message: "Awesome tag message\n\n- hello\n- world")
expect(current_path).to eq(
@@ -43,7 +43,7 @@ feature 'Master creates tag' do
end
end
- scenario 'with multiline release notes parses the release note as Markdown' do
+ it 'with multiline release notes parses the release note as Markdown' do
create_tag_in_form(tag: 'v4.0', ref: 'master', desc: "Awesome release notes\n\n- hello\n- world")
expect(current_path).to eq(
@@ -55,7 +55,7 @@ feature 'Master creates tag' do
end
end
- scenario 'opens dropdown for ref', :js do
+ it '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
@@ -75,9 +75,9 @@ feature 'Master creates tag' do
visit new_project_tag_path(project)
end
- it 'description has autocomplete', :js do
+ it 'description has emoji autocomplete', :js do
find('#release_description').native.send_keys('')
- fill_in 'release_description', with: '@'
+ fill_in 'release_description', with: ':'
expect(page).to have_selector('.atwho-view')
end
diff --git a/spec/features/tags/master_deletes_tag_spec.rb b/spec/features/tags/master_deletes_tag_spec.rb
index c0b4fa52526..8d567e925ef 100644
--- a/spec/features/tags/master_deletes_tag_spec.rb
+++ b/spec/features/tags/master_deletes_tag_spec.rb
@@ -1,17 +1,17 @@
require 'spec_helper'
-feature 'Master deletes tag' do
+describe 'Maintainer deletes tag' do
let(:user) { create(:user) }
let(:project) { create(:project, :repository, namespace: user.namespace) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit project_tags_path(project)
end
context 'from the tags list page', :js do
- scenario 'deletes the tag' do
+ it 'deletes the tag' do
expect(page).to have_content 'v1.1.0'
delete_first_tag
@@ -21,7 +21,7 @@ feature 'Master deletes tag' do
end
context 'from a specific tag page' do
- scenario 'deletes the tag' do
+ it 'deletes the tag' do
click_on 'v1.0.0'
expect(current_path).to eq(
project_tag_path(project, 'v1.0.0'))
@@ -35,30 +35,15 @@ feature 'Master deletes tag' do
end
context 'when pre-receive hook fails', :js do
- context 'when Gitaly operation_user_delete_tag feature is enabled' do
- before do
- allow_any_instance_of(Gitlab::GitalyClient::OperationService).to receive(:rm_tag)
- .and_raise(Gitlab::Git::HooksService::PreReceiveError, 'Do not delete tags')
- end
-
- scenario 'shows the error message' do
- delete_first_tag
-
- expect(page).to have_content('Do not delete tags')
- end
+ before do
+ allow_any_instance_of(Gitlab::GitalyClient::OperationService).to receive(:rm_tag)
+ .and_raise(Gitlab::Git::PreReceiveError, 'Do not delete tags')
end
- context 'when Gitaly operation_user_delete_tag feature is disabled', :skip_gitaly_mock do
- before do
- allow_any_instance_of(Gitlab::Git::HooksService).to receive(:execute)
- .and_raise(Gitlab::Git::HooksService::PreReceiveError, 'Do not delete tags')
- end
-
- scenario 'shows the error message' do
- delete_first_tag
+ it 'shows the error message' do
+ delete_first_tag
- expect(page).to have_content('Do not delete tags')
- end
+ expect(page).to have_content('Do not delete tags')
end
end
diff --git a/spec/features/tags/master_updates_tag_spec.rb b/spec/features/tags/master_updates_tag_spec.rb
index 1c370a99b13..d8b5b3c4cc4 100644
--- a/spec/features/tags/master_updates_tag_spec.rb
+++ b/spec/features/tags/master_updates_tag_spec.rb
@@ -1,17 +1,17 @@
require 'spec_helper'
-feature 'Master updates tag' do
+describe 'Maintainer updates tag' do
let(:user) { create(:user) }
let(:project) { create(:project, :repository, namespace: user.namespace) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
visit project_tags_path(project)
end
context 'from the tags list page' do
- scenario 'updates the release notes' do
+ it 'updates the release notes' do
page.within(first('.content-list .controls')) do
click_link 'Edit release notes'
end
@@ -25,20 +25,20 @@ feature 'Master updates tag' do
expect(page).to have_content 'Awesome release notes'
end
- scenario 'description has autocomplete', :js do
+ it 'description has emoji autocomplete', :js do
page.within(first('.content-list .controls')) do
click_link 'Edit release notes'
end
find('#release_description').native.send_keys('')
- fill_in 'release_description', with: '@'
+ fill_in 'release_description', with: ':'
expect(page).to have_selector('.atwho-view')
end
end
context 'from a specific tag page' do
- scenario 'updates the release notes' do
+ it 'updates the release notes' do
click_on 'v1.1.0'
click_link 'Edit release notes'
fill_in 'release_description', with: 'Awesome release notes'
diff --git a/spec/features/tags/master_views_tags_spec.rb b/spec/features/tags/master_views_tags_spec.rb
index b625e7065cc..3f4fe549f3e 100644
--- a/spec/features/tags/master_views_tags_spec.rb
+++ b/spec/features/tags/master_views_tags_spec.rb
@@ -1,10 +1,10 @@
require 'spec_helper'
-feature 'Master views tags' do
+describe 'Maintainer views tags' do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
sign_in(user)
end
@@ -19,7 +19,7 @@ feature 'Master views tags' do
visit project_tags_path(project)
end
- scenario 'displays a specific message' do
+ it 'displays a specific message' do
expect(page).to have_content 'Repository has no tags yet.'
end
end
@@ -32,7 +32,7 @@ feature 'Master views tags' do
visit project_tags_path(project)
end
- scenario 'avoids a N+1 query in branches index' do
+ it 'avoids a N+1 query in branches index' do
control_count = ActiveRecord::QueryRecorder.new { visit project_tags_path(project) }.count
%w(one two three four five).each { |tag| repository.add_tag(user, tag, 'master', 'foo') }
@@ -40,11 +40,11 @@ feature 'Master views tags' do
expect { visit project_tags_path(project) }.not_to exceed_query_limit(control_count)
end
- scenario 'views the tags list page' do
+ it 'views the tags list page' do
expect(page).to have_content 'v1.0.0'
end
- scenario 'views a specific tag page' do
+ it 'views a specific tag page' do
click_on 'v1.0.0'
expect(current_path).to eq(
@@ -54,7 +54,7 @@ feature 'Master views tags' do
end
describe 'links on the tag page' do
- scenario 'has a button to browse files' do
+ it 'has a button to browse files' do
click_on 'v1.0.0'
expect(current_path).to eq(
@@ -66,7 +66,7 @@ feature 'Master views tags' do
project_tree_path(project, 'v1.0.0'))
end
- scenario 'has a button to browse commits' do
+ it 'has a button to browse commits' do
click_on 'v1.0.0'
expect(current_path).to eq(
diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb
index 2dc3c5e3927..9c9127980a1 100644
--- a/spec/features/task_lists_spec.rb
+++ b/spec/features/task_lists_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Task Lists' do
+describe 'Task Lists' do
include Warden::Test::Helpers
let(:project) { create(:project, :repository) }
@@ -36,7 +36,7 @@ feature 'Task Lists' do
MARKDOWN
end
- let(:nested_tasks_markdown) do
+ let(:nested_tasks_markdown_redcarpet) do
<<-EOT.strip_heredoc
- [ ] Task a
- [x] Task a.1
@@ -49,10 +49,23 @@ feature 'Task Lists' do
EOT
end
+ let(:nested_tasks_markdown) do
+ <<-EOT.strip_heredoc
+ - [ ] Task a
+ - [x] Task a.1
+ - [ ] Task a.2
+ - [ ] Task b
+
+ 1. [ ] Task 1
+ 1. [ ] Task 1.1
+ 1. [x] Task 1.2
+ EOT
+ end
+
before do
Warden.test_mode!
- project.add_master(user)
+ project.add_maintainer(user)
project.add_guest(user2)
login_as(user)
@@ -141,13 +154,11 @@ feature 'Task Lists' do
end
end
- describe 'nested tasks', :js do
- let(:issue) { create(:issue, description: nested_tasks_markdown, author: user, project: project) }
-
+ shared_examples 'shared nested tasks' do
before do
+ allow(Banzai::Filter::MarkdownFilter).to receive(:engine).and_return('Redcarpet')
visit_issue(project, issue)
end
-
it 'renders' do
expect(page).to have_selector('ul.task-list', count: 2)
expect(page).to have_selector('li.task-list-item', count: 7)
@@ -171,6 +182,30 @@ feature 'Task Lists' do
expect(page).to have_content('marked the task Task 1.1 as complete')
end
end
+
+ describe 'nested tasks', :js do
+ context 'with Redcarpet' do
+ let(:issue) { create(:issue, description: nested_tasks_markdown_redcarpet, author: user, project: project) }
+
+ before do
+ allow_any_instance_of(Banzai::Filter::MarkdownFilter).to receive(:engine).and_return('Redcarpet')
+ visit_issue(project, issue)
+ end
+
+ it_behaves_like 'shared nested tasks'
+ end
+
+ context 'with CommonMark' do
+ let(:issue) { create(:issue, description: nested_tasks_markdown, author: user, project: project) }
+
+ before do
+ allow_any_instance_of(Banzai::Filter::MarkdownFilter).to receive(:engine).and_return('CommonMark')
+ visit_issue(project, issue)
+ end
+
+ it_behaves_like 'shared nested tasks'
+ end
+ end
end
describe 'for Notes' do
diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb
index 19784120108..919859c145a 100644
--- a/spec/features/triggers_spec.rb
+++ b/spec/features/triggers_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Triggers', :js do
+describe 'Triggers', :js do
let(:trigger_title) { 'trigger desc' }
let(:user) { create(:user) }
let(:user2) { create(:user) }
@@ -10,23 +10,23 @@ feature 'Triggers', :js do
sign_in(user)
@project = create(:project)
- @project.add_master(user)
- @project.add_master(user2)
+ @project.add_maintainer(user)
+ @project.add_maintainer(user2)
@project.add_guest(guest_user)
visit project_settings_ci_cd_path(@project)
end
describe 'create trigger workflow' do
- scenario 'prevents adding new trigger with no description' do
+ it 'prevents adding new trigger with no description' do
fill_in 'trigger_description', with: ''
click_button 'Add trigger'
# See if input has error due to empty value
- expect(page.find('form.gl-show-field-errors .gl-field-error')['style']).to eq 'display: block;'
+ expect(page.find('form.gl-show-field-errors .gl-field-error')).to be_visible
end
- scenario 'adds new trigger with description' do
+ it 'adds new trigger with description' do
fill_in 'trigger_description', with: 'trigger desc'
click_button 'Add trigger'
@@ -40,7 +40,7 @@ feature 'Triggers', :js do
describe 'edit trigger workflow' do
let(:new_trigger_title) { 'new trigger' }
- scenario 'click on edit trigger opens edit trigger page' do
+ it 'click on edit trigger opens edit trigger page' do
create(:ci_trigger, owner: user, project: @project, description: trigger_title)
visit project_settings_ci_cd_path(@project)
@@ -49,7 +49,7 @@ feature 'Triggers', :js do
expect(page.find('#trigger_description').value).to have_content 'trigger desc'
end
- scenario 'edit trigger and save' do
+ it 'edit trigger and save' do
create(:ci_trigger, owner: user, project: @project, description: trigger_title)
visit project_settings_ci_cd_path(@project)
@@ -64,7 +64,7 @@ feature 'Triggers', :js do
expect(page.find('.triggers-list .trigger-owner')).to have_content user.name
end
- scenario 'edit "legacy" trigger and save' do
+ it 'edit "legacy" trigger and save' do
# Create new trigger without owner association, i.e. Legacy trigger
create(:ci_trigger, owner: nil, project: @project)
visit project_settings_ci_cd_path(@project)
@@ -87,12 +87,12 @@ feature 'Triggers', :js do
visit project_settings_ci_cd_path(@project)
end
- scenario 'button "Take ownership" has correct alert' do
+ it 'button "Take ownership" has correct alert' do
expected_alert = 'By taking ownership you will bind this trigger to your user account. With this the trigger will have access to all your projects as if it was you. Are you sure?'
expect(page.find('a.btn-trigger-take-ownership')['data-confirm']).to eq expected_alert
end
- scenario 'take trigger ownership' do
+ it 'take trigger ownership' do
# See if "Take ownership" on trigger works post trigger creation
page.accept_confirm do
first(:link, "Take ownership").send_keys(:return)
@@ -110,12 +110,12 @@ feature 'Triggers', :js do
visit project_settings_ci_cd_path(@project)
end
- scenario 'button "Revoke" has correct alert' do
+ it 'button "Revoke" has correct alert' do
expected_alert = 'By revoking a trigger you will break any processes making use of it. Are you sure?'
expect(page.find('a.btn-trigger-revoke')['data-confirm']).to eq expected_alert
end
- scenario 'revoke trigger' do
+ it 'revoke trigger' do
# See if "Revoke" on trigger works post trigger creation
page.accept_confirm do
find('a.btn-trigger-revoke').send_keys(:return)
@@ -127,11 +127,11 @@ feature 'Triggers', :js do
end
describe 'show triggers workflow' do
- scenario 'contains trigger description placeholder' do
+ it 'contains trigger description placeholder' do
expect(page.find('#trigger_description')['placeholder']).to eq 'Trigger description'
end
- scenario 'show "legacy" badge for legacy trigger' do
+ it 'show "legacy" badge for legacy trigger' do
create(:ci_trigger, owner: nil, project: @project)
visit project_settings_ci_cd_path(@project)
@@ -140,7 +140,7 @@ feature 'Triggers', :js do
expect(page.find('.triggers-list')).to have_selector('a[title="Edit"]')
end
- scenario 'show "invalid" badge for trigger with owner having insufficient permissions' do
+ it 'show "invalid" badge for trigger with owner having insufficient permissions' do
create(:ci_trigger, owner: guest_user, project: @project, description: trigger_title)
visit project_settings_ci_cd_path(@project)
@@ -149,7 +149,7 @@ feature 'Triggers', :js do
expect(page.find('.triggers-list')).not_to have_selector('a[title="Edit"]')
end
- scenario 'do not show "Edit" or full token for not owned trigger' do
+ it 'do not show "Edit" or full token for not owned trigger' do
# Create trigger with user different from current_user
create(:ci_trigger, owner: user2, project: @project, description: trigger_title)
visit project_settings_ci_cd_path(@project)
@@ -163,7 +163,7 @@ feature 'Triggers', :js do
expect(page.find('.triggers-list')).not_to have_selector('a[title="Edit"]')
end
- scenario 'show "Edit" and full token for owned trigger' do
+ it 'show "Edit" and full token for owned trigger' do
create(:ci_trigger, owner: user, project: @project, description: trigger_title)
visit project_settings_ci_cd_path(@project)
diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb
index fb65b570dd6..f245c1ebbd9 100644
--- a/spec/features/u2f_spec.rb
+++ b/spec/features/u2f_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do
+describe 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do
def manage_two_factor_authentication
click_on 'Manage two-factor authentication'
expect(page).to have_content("Setup new U2F device")
diff --git a/spec/features/uploads/user_uploads_avatar_to_group_spec.rb b/spec/features/uploads/user_uploads_avatar_to_group_spec.rb
index e8884bc1a00..a07edc42eae 100644
--- a/spec/features/uploads/user_uploads_avatar_to_group_spec.rb
+++ b/spec/features/uploads/user_uploads_avatar_to_group_spec.rb
@@ -1,7 +1,7 @@
require 'rails_helper'
-feature 'User uploads avatar to group' do
- scenario 'they see the new avatar' do
+describe 'User uploads avatar to group' do
+ it 'they see the new avatar' do
user = create(:user)
group = create(:group)
group.add_owner(user)
@@ -14,7 +14,9 @@ feature 'User uploads avatar to group' do
visible: false
)
- click_button 'Save group'
+ page.within('.gs-general') do
+ click_button 'Save group'
+ end
visit group_path(group)
diff --git a/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb
index 52003bb0859..4db73fccfb6 100644
--- a/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb
+++ b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb
@@ -1,17 +1,16 @@
require 'rails_helper'
-feature 'User uploads avatar to profile' do
- scenario 'they see their new avatar' do
- user = create(:user)
- sign_in(user)
+describe 'User uploads avatar to profile' do
+ let!(:user) { create(:user) }
+ let(:avatar_file_path) { Rails.root.join('spec', 'fixtures', 'dk.png') }
+ before do
+ sign_in user
visit profile_path
- attach_file(
- 'user_avatar',
- Rails.root.join('spec', 'fixtures', 'dk.png'),
- visible: false
- )
+ end
+ it 'they see their new avatar on their profile' do
+ attach_file('user_avatar', avatar_file_path, visible: false)
click_button 'Update profile settings'
visit user_path(user)
@@ -21,4 +20,16 @@ feature 'User uploads avatar to profile' do
# Cheating here to verify something that isn't user-facing, but is important
expect(user.reload.avatar.file).to exist
end
+
+ it 'their new avatar is immediately visible in the header', :js do
+ find('.js-user-avatar-input', visible: false).set(avatar_file_path)
+
+ click_button 'Set new profile picture'
+ click_button 'Update profile settings'
+
+ wait_for_all_requests
+
+ data_uri = find('.avatar-image .avatar')['src']
+ expect(page.find('.header-user-avatar')['src']).to eq data_uri
+ 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 972c10aaf23..24a00c86b0a 100644
--- a/spec/features/uploads/user_uploads_file_to_note_spec.rb
+++ b/spec/features/uploads/user_uploads_file_to_note_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'User uploads file to note' do
+describe 'User uploads file to note' do
include DropzoneHelper
let(:user) { create(:user) }
@@ -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 do
+ it '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/active_sessions_spec.rb b/spec/features/users/active_sessions_spec.rb
index 631d7e3bced..25349b5d036 100644
--- a/spec/features/users/active_sessions_spec.rb
+++ b/spec/features/users/active_sessions_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
-feature 'Active user sessions', :clean_gitlab_redis_shared_state do
- scenario 'Successful login adds a new active user login' do
+describe 'Active user sessions', :clean_gitlab_redis_shared_state do
+ it 'Successful login adds a new active user login' do
now = Time.zone.parse('2018-03-12 09:06')
Timecop.freeze(now) do
user = create(:user)
@@ -24,7 +24,7 @@ feature 'Active user sessions', :clean_gitlab_redis_shared_state do
end
end
- scenario 'Successful login cleans up obsolete entries' do
+ it 'Successful login cleans up obsolete entries' do
user = create(:user)
Gitlab::Redis::SharedState.with do |redis|
@@ -38,7 +38,7 @@ feature 'Active user sessions', :clean_gitlab_redis_shared_state do
end
end
- scenario 'Sessionless login does not clean up obsolete entries' do
+ it 'Sessionless login does not clean up obsolete entries' do
user = create(:user)
personal_access_token = create(:personal_access_token, user: user)
@@ -54,7 +54,7 @@ feature 'Active user sessions', :clean_gitlab_redis_shared_state do
end
end
- scenario 'Logout deletes the active user login' do
+ it 'Logout deletes the active user login' do
user = create(:user)
gitlab_sign_in(user)
expect(current_path).to eq root_path
diff --git a/spec/features/users/login_spec.rb b/spec/features/users/login_spec.rb
index 6f968a2c590..21891b9ccda 100644
--- a/spec/features/users/login_spec.rb
+++ b/spec/features/users/login_spec.rb
@@ -1,9 +1,9 @@
require 'spec_helper'
-feature 'Login' do
+describe 'Login' do
include TermsHelper
- scenario 'Successful user signin invalidates password reset token' do
+ it 'Successful user signin invalidates password reset token' do
user = create(:user)
expect(user.reset_password_token).to be_nil
@@ -177,14 +177,35 @@ feature 'Login' do
end
context 'logging in via OAuth' do
- it 'shows 2FA prompt after OAuth login' do
- stub_omniauth_saml_config(enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'], providers: [mock_saml_config])
- user = create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: 'saml')
- gitlab_sign_in_via('saml', user, 'my-uid')
+ let(:user) { create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: 'saml')}
+ let(:mock_saml_response) do
+ File.read('spec/fixtures/authentication/saml_response.xml')
+ end
- expect(page).to have_content('Two-Factor Authentication')
- enter_code(user.current_otp)
- expect(current_path).to eq root_path
+ before do
+ stub_omniauth_saml_config(enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'],
+ providers: [mock_saml_config_with_upstream_two_factor_authn_contexts])
+ gitlab_sign_in_via('saml', user, 'my-uid', mock_saml_response)
+ end
+
+ context 'when authn_context is worth two factors' do
+ let(:mock_saml_response) do
+ File.read('spec/fixtures/authentication/saml_response.xml')
+ .gsub('urn:oasis:names:tc:SAML:2.0:ac:classes:Password', 'urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorOTPSMS')
+ end
+
+ it 'signs user in without prompting for second factor' do
+ expect(page).not_to have_content('Two-Factor Authentication')
+ expect(current_path).to eq root_path
+ end
+ end
+
+ context 'when authn_context is not worth two factors' do
+ it 'shows 2FA prompt after OAuth login' do
+ expect(page).to have_content('Two-Factor Authentication')
+ enter_code(user.current_otp)
+ expect(current_path).to eq root_path
+ end
end
end
end
@@ -394,7 +415,7 @@ feature 'Login' do
end
def ensure_one_active_tab
- expect(page).to have_selector('ul.new-session-tabs > li.active', count: 1)
+ expect(page).to have_selector('ul.new-session-tabs > li > a.active', count: 1)
end
def ensure_one_active_pane
diff --git a/spec/features/users/rss_spec.rb b/spec/features/users/rss_spec.rb
index 7c5abe54d56..9af4447056e 100644
--- a/spec/features/users/rss_spec.rb
+++ b/spec/features/users/rss_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'User RSS' do
+describe 'User RSS' do
let(:user) { create(:user) }
let(:path) { user_path(create(:user)) }
@@ -10,7 +10,7 @@ feature 'User RSS' do
visit path
end
- it_behaves_like "it has an RSS button with current_user's RSS token"
+ it_behaves_like "it has an RSS button with current_user's feed token"
end
context 'when signed out' do
@@ -18,6 +18,6 @@ feature 'User RSS' do
visit path
end
- it_behaves_like "it has an RSS button without an RSS token"
+ it_behaves_like "it has an RSS button without a feed token"
end
end
diff --git a/spec/features/users/show_spec.rb b/spec/features/users/show_spec.rb
index b5bbb2c0ea5..3e2fb704bc6 100644
--- a/spec/features/users/show_spec.rb
+++ b/spec/features/users/show_spec.rb
@@ -14,4 +14,28 @@ describe 'User page' do
expect(page).to have_link('Snippets')
end
end
+
+ context 'signup disabled' do
+ it 'shows the sign in link' do
+ stub_application_setting(signup_enabled: false)
+
+ visit(user_path(user))
+
+ page.within '.navbar-nav' do
+ expect(page).to have_link('Sign in')
+ end
+ end
+ end
+
+ context 'signup enabled' do
+ it 'shows the sign in and register link' do
+ stub_application_setting(signup_enabled: true)
+
+ visit(user_path(user))
+
+ page.within '.navbar-nav' do
+ expect(page).to have_link('Sign in / Register')
+ end
+ end
+ end
end
diff --git a/spec/features/users/signup_spec.rb b/spec/features/users/signup_spec.rb
index b5bd5c505f2..bfe11ddf673 100644
--- a/spec/features/users/signup_spec.rb
+++ b/spec/features/users/signup_spec.rb
@@ -40,6 +40,15 @@ describe 'Signup' do
expect(find('.username')).to have_css '.gl-field-error-outline'
end
+
+ it 'shows an error message on submit if the username contains special characters' do
+ fill_in 'new_user_username', with: 'new$user!username'
+ wait_for_requests
+
+ click_button "Register"
+
+ expect(page).to have_content("Please create a username with only alphanumeric characters.")
+ end
end
context 'with no errors' do
@@ -140,7 +149,7 @@ describe 'Signup' do
enforce_terms
end
- it 'asks the user to accept terms before going to the dashboard' do
+ it 'requires the user to check the checkbox' do
visit root_path
fill_in 'new_user_name', with: new_user.name
@@ -148,11 +157,24 @@ describe 'Signup' do
fill_in 'new_user_email', with: new_user.email
fill_in 'new_user_email_confirmation', with: new_user.email
fill_in 'new_user_password', with: new_user.password
- click_button "Register"
- expect_to_be_on_terms_page
+ click_button 'Register'
+
+ expect(current_path).to eq new_user_session_path
+ expect(page).to have_content(/you must accept our terms of service/i)
+ end
- click_button 'Accept terms'
+ it 'asks the user to accept terms before going to the dashboard' do
+ visit root_path
+
+ fill_in 'new_user_name', with: new_user.name
+ fill_in 'new_user_username', with: new_user.username
+ fill_in 'new_user_email', with: new_user.email
+ fill_in 'new_user_email_confirmation', with: new_user.email
+ fill_in 'new_user_password', with: new_user.password
+ check :terms_opt_in
+
+ click_button "Register"
expect(current_path).to eq dashboard_projects_path
end
diff --git a/spec/features/users/terms_spec.rb b/spec/features/users/terms_spec.rb
index f9469adbfe3..5b2e7605c4d 100644
--- a/spec/features/users/terms_spec.rb
+++ b/spec/features/users/terms_spec.rb
@@ -3,12 +3,10 @@ require 'spec_helper'
describe 'Users > Terms' do
include TermsHelper
- let(:user) { create(:user) }
let!(:term) { create(:term, terms: 'By accepting, you promise to be nice!') }
before do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
- sign_in(user)
end
it 'shows the terms' do
@@ -17,85 +15,119 @@ describe 'Users > Terms' do
expect(page).to have_content('By accepting, you promise to be nice!')
end
- context 'declining the terms' do
- it 'returns the user to the app' do
- visit terms_path
+ it 'does not show buttons to accept, decline or sign out', :aggregate_failures do
+ visit terms_path
+
+ expect(page).not_to have_css('.footer-block')
+ expect(page).not_to have_content('Accept terms')
+ expect(page).not_to have_content('Decline and sign out')
+ expect(page).not_to have_content('Continue')
+ end
- click_button 'Decline and sign out'
+ context 'when signed in' do
+ let(:user) { create(:user) }
- expect(page).not_to have_content(term.terms)
- expect(user.reload.terms_accepted?).to be(false)
+ before do
+ sign_in(user)
end
- end
- context 'accepting the terms' do
- it 'returns the user to the app' do
- visit terms_path
+ context 'declining the terms' do
+ it 'returns the user to the app' do
+ visit terms_path
- click_button 'Accept terms'
+ click_button 'Decline and sign out'
- expect(page).not_to have_content(term.terms)
- expect(user.reload.terms_accepted?).to be(true)
+ expect(page).not_to have_content(term.terms)
+ expect(user.reload.terms_accepted?).to be(false)
+ end
end
- end
- context 'terms were enforced while session is active', :js do
- let(:project) { create(:project) }
+ context 'accepting the terms' do
+ it 'returns the user to the app' do
+ visit terms_path
- before do
- project.add_developer(user)
+ click_button 'Accept terms'
+
+ expect(page).not_to have_content(term.terms)
+ expect(user.reload.terms_accepted?).to be(true)
+ end
end
- it 'redirects to terms and back to where the user was going' do
- visit project_path(project)
+ context 'when the user has already accepted the terms' do
+ before do
+ accept_terms(user)
+ end
+
+ it 'allows the user to continue to the app' do
+ visit terms_path
+
+ expect(page).to have_content "You have already accepted the Terms of Service as #{user.to_reference}"
- enforce_terms
+ click_link 'Continue'
- within('.nav-sidebar') do
- click_link 'Issues'
+ expect(current_path).to eq(root_path)
end
+ end
+
+ context 'terms were enforced while session is active', :js do
+ let(:project) { create(:project) }
- expect_to_be_on_terms_page
+ before do
+ project.add_developer(user)
+ end
- click_button('Accept terms')
+ it 'redirects to terms and back to where the user was going' do
+ visit project_path(project)
- expect(current_path).to eq(project_issues_path(project))
- end
+ enforce_terms
- it 'redirects back to the page the user was trying to save' do
- visit new_project_issue_path(project)
+ within('.nav-sidebar') do
+ click_link 'Issues'
+ end
- fill_in :issue_title, with: 'Hello world, a new issue'
- fill_in :issue_description, with: "We don't want to lose what the user typed"
+ expect_to_be_on_terms_page
- enforce_terms
+ click_button('Accept terms')
- click_button 'Submit issue'
+ expect(current_path).to eq(project_issues_path(project))
+ end
- expect(current_path).to eq(terms_path)
+ # Disabled until https://gitlab.com/gitlab-org/gitlab-ce/issues/37162 is solved properly
+ xit 'redirects back to the page the user was trying to save' do
+ visit new_project_issue_path(project)
- click_button('Accept terms')
+ fill_in :issue_title, with: 'Hello world, a new issue'
+ fill_in :issue_description, with: "We don't want to lose what the user typed"
- expect(current_path).to eq(new_project_issue_path(project))
- expect(find_field('issue_title').value).to eq('Hello world, a new issue')
- expect(find_field('issue_description').value).to eq("We don't want to lose what the user typed")
- end
- end
+ enforce_terms
- context 'when the terms are enforced' do
- before do
- enforce_terms
+ click_button 'Submit issue'
+
+ expect(current_path).to eq(terms_path)
+
+ click_button('Accept terms')
+
+ expect(current_path).to eq(new_project_issue_path(project))
+ expect(find_field('issue_title').value).to eq('Hello world, a new issue')
+ expect(find_field('issue_description').value).to eq("We don't want to lose what the user typed")
+ end
end
- context 'signing out', :js do
- it 'allows the user to sign out without a response' do
- visit terms_path
+ context 'when the terms are enforced' do
+ before do
+ enforce_terms
+ end
+
+ context 'signing out', :js do
+ it 'allows the user to sign out without a response' do
+ visit terms_path
- find('.header-user-dropdown-toggle').click
- click_link('Sign out')
+ find('.header-user-dropdown-toggle').click
+ click_link('Sign out')
- expect(page).to have_content('Sign in')
- expect(page).to have_content('Register')
+ expect(page).to have_content('Sign in')
+ expect(page).to have_content('Register')
+ end
end
end
end
diff --git a/spec/features/users/user_browses_projects_on_user_page_spec.rb b/spec/features/users/user_browses_projects_on_user_page_spec.rb
index 7bede0b0d48..6a9b281fb4c 100644
--- a/spec/features/users/user_browses_projects_on_user_page_spec.rb
+++ b/spec/features/users/user_browses_projects_on_user_page_spec.rb
@@ -4,19 +4,19 @@ describe 'Users > User browses projects on user page', :js do
let!(:user) { create :user }
let!(:private_project) do
create :project, :private, name: 'private', namespace: user.namespace do |project|
- project.add_master(user)
+ project.add_maintainer(user)
end
end
let!(:internal_project) do
create :project, :internal, name: 'internal', namespace: user.namespace do |project|
- project.add_master(user)
+ project.add_maintainer(user)
end
end
let!(:public_project) do
create :project, :public, name: 'public', namespace: user.namespace do |project|
- project.add_master(user)
+ project.add_maintainer(user)
end
end
@@ -26,18 +26,23 @@ describe 'Users > User browses projects on user page', :js do
end
end
+ it 'hides loading spinner after load', :js do
+ visit user_path(user)
+ click_nav_link('Personal projects')
+
+ wait_for_requests
+
+ expect(page).not_to have_selector('.loading-status .loading', visible: true)
+ end
+
it 'paginates projects', :js do
project = create(:project, namespace: user.namespace, updated_at: 2.minutes.since)
project2 = create(:project, namespace: user.namespace, updated_at: 1.minute.since)
allow(Project).to receive(:default_per_page).and_return(1)
sign_in(user)
-
visit user_path(user)
-
- page.within('.user-profile-nav') do
- click_link('Personal projects')
- end
+ click_nav_link('Personal projects')
wait_for_requests
@@ -92,7 +97,6 @@ describe 'Users > User browses projects on user page', :js do
click_nav_link('Personal projects')
expect(title).to start_with(user.name)
-
expect(page).to have_content(private_project.name)
expect(page).to have_content(public_project.name)
expect(page).to have_content(internal_project.name)
diff --git a/spec/finders/access_requests_finder_spec.rb b/spec/finders/access_requests_finder_spec.rb
index 650f7229647..605777462bb 100644
--- a/spec/finders/access_requests_finder_spec.rb
+++ b/spec/finders/access_requests_finder_spec.rb
@@ -51,7 +51,7 @@ describe AccessRequestsFinder do
context 'when current user can see access requests' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
group.add_owner(user)
end
@@ -78,7 +78,7 @@ describe AccessRequestsFinder do
context 'when current user can see access requests' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
group.add_owner(user)
end
diff --git a/spec/finders/admin/projects_finder_spec.rb b/spec/finders/admin/projects_finder_spec.rb
index 7901d5fee28..44cc8debd04 100644
--- a/spec/finders/admin/projects_finder_spec.rb
+++ b/spec/finders/admin/projects_finder_spec.rb
@@ -54,7 +54,7 @@ describe Admin::ProjectsFinder do
context 'filter by visibility_level' do
before do
- private_project.add_master(user)
+ private_project.add_maintainer(user)
end
context 'private' do
diff --git a/spec/finders/concerns/finder_with_cross_project_access_spec.rb b/spec/finders/concerns/finder_with_cross_project_access_spec.rb
index c784fb87972..1ff65a8101b 100644
--- a/spec/finders/concerns/finder_with_cross_project_access_spec.rb
+++ b/spec/finders/concerns/finder_with_cross_project_access_spec.rb
@@ -25,7 +25,7 @@ describe FinderWithCrossProjectAccess do
let!(:result) { create(:issue) }
before do
- result.project.add_master(user)
+ result.project.add_maintainer(user)
end
def expect_access_check_on_result
diff --git a/spec/finders/contributed_projects_finder_spec.rb b/spec/finders/contributed_projects_finder_spec.rb
index 60ea98e61c7..9155a8d6fe9 100644
--- a/spec/finders/contributed_projects_finder_spec.rb
+++ b/spec/finders/contributed_projects_finder_spec.rb
@@ -10,9 +10,9 @@ describe ContributedProjectsFinder do
let!(:private_project) { create(:project, :private) }
before do
- private_project.add_master(source_user)
+ private_project.add_maintainer(source_user)
private_project.add_developer(current_user)
- public_project.add_master(source_user)
+ public_project.add_maintainer(source_user)
create(:push_event, project: public_project, author: source_user)
create(:push_event, project: private_project, author: source_user)
diff --git a/spec/finders/environments_finder_spec.rb b/spec/finders/environments_finder_spec.rb
index 3a8a1e7de74..3cd421f22eb 100644
--- a/spec/finders/environments_finder_spec.rb
+++ b/spec/finders/environments_finder_spec.rb
@@ -7,7 +7,7 @@ describe EnvironmentsFinder do
let(:environment) { create(:environment, project: project) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
context 'tagged deployment' do
diff --git a/spec/finders/group_members_finder_spec.rb b/spec/finders/group_members_finder_spec.rb
index 9f285e28535..f545da3aee4 100644
--- a/spec/finders/group_members_finder_spec.rb
+++ b/spec/finders/group_members_finder_spec.rb
@@ -9,9 +9,9 @@ describe GroupMembersFinder, '#execute' do
let(:user4) { create(:user) }
it 'returns members for top-level group' do
- member1 = group.add_master(user1)
- member2 = group.add_master(user2)
- member3 = group.add_master(user3)
+ member1 = group.add_maintainer(user1)
+ member2 = group.add_maintainer(user2)
+ member3 = group.add_maintainer(user3)
result = described_class.new(group).execute
@@ -19,14 +19,26 @@ describe GroupMembersFinder, '#execute' do
end
it 'returns members for nested group', :nested_groups do
- group.add_master(user2)
+ group.add_maintainer(user2)
nested_group.request_access(user4)
- member1 = group.add_master(user1)
- member3 = nested_group.add_master(user2)
- member4 = nested_group.add_master(user3)
+ member1 = group.add_maintainer(user1)
+ member3 = nested_group.add_maintainer(user2)
+ member4 = nested_group.add_maintainer(user3)
result = described_class.new(nested_group).execute
expect(result.to_a).to match_array([member1, member3, member4])
end
+
+ it 'returns members for descendant groups if requested', :nested_groups do
+ member1 = group.add_maintainer(user2)
+ member2 = group.add_maintainer(user1)
+ nested_group.add_maintainer(user2)
+ member3 = nested_group.add_maintainer(user3)
+ member4 = nested_group.add_maintainer(user4)
+
+ result = described_class.new(group).execute(include_descendants: true)
+
+ expect(result.to_a).to match_array([member1, member2, member3, member4])
+ end
end
diff --git a/spec/finders/group_projects_finder_spec.rb b/spec/finders/group_projects_finder_spec.rb
index be80ee7d767..d6d95906f5e 100644
--- a/spec/finders/group_projects_finder_spec.rb
+++ b/spec/finders/group_projects_finder_spec.rb
@@ -17,16 +17,16 @@ describe GroupProjectsFinder do
let!(:subgroup_private_project) { create(:project, :private, path: '7', group: subgroup) }
before do
- shared_project_1.project_group_links.create(group_access: Gitlab::Access::MASTER, group: group)
- shared_project_2.project_group_links.create(group_access: Gitlab::Access::MASTER, group: group)
- shared_project_3.project_group_links.create(group_access: Gitlab::Access::MASTER, group: group)
+ shared_project_1.project_group_links.create(group_access: Gitlab::Access::MAINTAINER, group: group)
+ shared_project_2.project_group_links.create(group_access: Gitlab::Access::MAINTAINER, group: group)
+ shared_project_3.project_group_links.create(group_access: Gitlab::Access::MAINTAINER, group: group)
end
subject { finder.execute }
describe 'with a group member current user' do
before do
- group.add_master(current_user)
+ group.add_maintainer(current_user)
end
context "only shared" do
@@ -68,7 +68,7 @@ describe GroupProjectsFinder do
describe 'without group member current_user' do
before do
- shared_project_2.add_master(current_user)
+ shared_project_2.add_maintainer(current_user)
current_user.reload
end
@@ -81,7 +81,7 @@ describe GroupProjectsFinder do
context "with external user" do
before do
- current_user.update_attributes(external: true)
+ current_user.update(external: true)
end
it { is_expected.to match_array([shared_project_2, shared_project_1]) }
@@ -93,8 +93,8 @@ describe GroupProjectsFinder do
context "without external user" do
before do
- private_project.add_master(current_user)
- subgroup_private_project.add_master(current_user)
+ private_project.add_maintainer(current_user)
+ subgroup_private_project.add_maintainer(current_user)
end
context 'with subgroups projects', :nested_groups do
@@ -112,7 +112,7 @@ describe GroupProjectsFinder do
context "with external user" do
before do
- current_user.update_attributes(external: true)
+ current_user.update(external: true)
end
context 'with subgroups projects', :nested_groups do
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index 74e91b02f0f..07a2fa86dd7 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -26,7 +26,7 @@ describe IssuesFinder do
let(:issues) { described_class.new(search_user, params.reverse_merge(scope: scope, state: 'opened')).execute }
before(:context) do
- project1.add_master(user)
+ project1.add_maintainer(user)
project2.add_developer(user)
project2.add_developer(user2)
project3.add_developer(user)
diff --git a/spec/finders/joined_groups_finder_spec.rb b/spec/finders/joined_groups_finder_spec.rb
index 29a47e005a6..ae3e55f90f1 100644
--- a/spec/finders/joined_groups_finder_spec.rb
+++ b/spec/finders/joined_groups_finder_spec.rb
@@ -15,7 +15,7 @@ describe JoinedGroupsFinder do
context 'without a user' do
before do
- public_group.add_master(profile_owner)
+ public_group.add_maintainer(profile_owner)
end
it 'only shows public groups from profile owner' do
@@ -25,9 +25,9 @@ describe JoinedGroupsFinder do
context "with a user" do
before do
- private_group.add_master(profile_owner)
- internal_group.add_master(profile_owner)
- public_group.add_master(profile_owner)
+ private_group.add_maintainer(profile_owner)
+ internal_group.add_maintainer(profile_owner)
+ public_group.add_maintainer(profile_owner)
end
context "when the profile visitor is in the private group" do
@@ -53,7 +53,7 @@ describe JoinedGroupsFinder do
context 'external users' do
before do
- profile_visitor.update_attributes(external: true)
+ profile_visitor.update(external: true)
end
context 'if not a member' do
@@ -64,7 +64,7 @@ describe JoinedGroupsFinder do
context "if authorized" do
before do
- internal_group.add_master(profile_visitor)
+ internal_group.add_maintainer(profile_visitor)
end
it "shows internal groups if authorized" do
diff --git a/spec/finders/members_finder_spec.rb b/spec/finders/members_finder_spec.rb
index 7bb1f45322e..db48f00cd74 100644
--- a/spec/finders/members_finder_spec.rb
+++ b/spec/finders/members_finder_spec.rb
@@ -11,12 +11,24 @@ describe MembersFinder, '#execute' do
it 'returns members for project and parent groups', :nested_groups do
nested_group.request_access(user1)
- member1 = group.add_master(user2)
- member2 = nested_group.add_master(user3)
- member3 = project.add_master(user4)
+ member1 = group.add_maintainer(user2)
+ member2 = nested_group.add_maintainer(user3)
+ member3 = project.add_maintainer(user4)
result = described_class.new(project, user2).execute
expect(result.to_a).to match_array([member1, member2, member3])
end
+
+ it 'includes nested group members if asked', :nested_groups do
+ project = create(:project, namespace: group)
+ nested_group.request_access(user1)
+ member1 = group.add_maintainer(user2)
+ member2 = nested_group.add_maintainer(user3)
+ member3 = project.add_maintainer(user4)
+
+ result = described_class.new(project, user2).execute(include_descendants: true)
+
+ expect(result.to_a).to match_array([member1, member2, member3])
+ end
end
diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb
index c8a43ddf410..35d0eeda8f6 100644
--- a/spec/finders/merge_requests_finder_spec.rb
+++ b/spec/finders/merge_requests_finder_spec.rb
@@ -19,12 +19,12 @@ describe MergeRequestsFinder do
let!(:merge_request1) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1) }
let!(:merge_request2) { create(:merge_request, :conflict, author: user, source_project: project2, target_project: project1, state: 'closed') }
- let!(:merge_request3) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project2) }
+ let!(:merge_request3) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project2, state: 'locked') }
let!(:merge_request4) { create(:merge_request, :simple, author: user, source_project: project3, target_project: project3) }
let!(:merge_request5) { create(:merge_request, :simple, author: user, source_project: project4, target_project: project4) }
before do
- project1.add_master(user)
+ project1.add_maintainer(user)
project2.add_developer(user)
project3.add_developer(user)
project2.add_developer(user2)
@@ -35,7 +35,7 @@ describe MergeRequestsFinder do
it 'filters by scope' do
params = { scope: 'authored', state: 'opened' }
merge_requests = described_class.new(user, params).execute
- expect(merge_requests.size).to eq(4)
+ expect(merge_requests.size).to eq(3)
end
it 'filters by project' do
@@ -90,6 +90,14 @@ describe MergeRequestsFinder do
expect(merge_requests).to contain_exactly(merge_request2)
end
+ it 'filters by state' do
+ params = { state: 'locked' }
+
+ merge_requests = described_class.new(user, params).execute
+
+ expect(merge_requests).to contain_exactly(merge_request3)
+ end
+
context 'filtering by group milestone' do
let!(:group) { create(:group, :public) }
let(:group_milestone) { create(:milestone, group: group) }
@@ -134,7 +142,7 @@ describe MergeRequestsFinder do
end
before do
- new_project.add_master(user)
+ new_project.add_maintainer(user)
end
it 'filters by created_after' do
@@ -199,7 +207,7 @@ describe MergeRequestsFinder do
it 'returns the number of rows for the default state' do
finder = described_class.new(user)
- expect(finder.row_count).to eq(4)
+ expect(finder.row_count).to eq(3)
end
it 'returns the number of rows for a given state' do
diff --git a/spec/finders/move_to_project_finder_spec.rb b/spec/finders/move_to_project_finder_spec.rb
index 74639d4147f..1511cb0e04c 100644
--- a/spec/finders/move_to_project_finder_spec.rb
+++ b/spec/finders/move_to_project_finder_spec.rb
@@ -8,7 +8,7 @@ describe MoveToProjectFinder do
let(:guest_project) { create(:project) }
let(:reporter_project) { create(:project) }
let(:developer_project) { create(:project) }
- let(:master_project) { create(:project) }
+ let(:maintainer_project) { create(:project) }
subject { described_class.new(user) }
@@ -23,9 +23,9 @@ describe MoveToProjectFinder do
it 'returns projects equal or above Gitlab::Access::REPORTER ordered by id in descending order' do
reporter_project.add_reporter(user)
developer_project.add_developer(user)
- master_project.add_master(user)
+ maintainer_project.add_maintainer(user)
- expect(subject.execute(project).to_a).to eq([master_project, developer_project, reporter_project])
+ expect(subject.execute(project).to_a).to eq([maintainer_project, developer_project, reporter_project])
end
it 'does not include the source project' do
@@ -45,7 +45,7 @@ describe MoveToProjectFinder do
it 'does not return projects for which issues are disabled' do
reporter_project.add_reporter(user)
- reporter_project.update_attributes(issues_enabled: false)
+ reporter_project.update(issues_enabled: false)
other_reporter_project = create(:project)
other_reporter_project.add_reporter(user)
@@ -57,9 +57,9 @@ describe MoveToProjectFinder do
reporter_project.add_reporter(user)
developer_project.add_developer(user)
- master_project.add_master(user)
+ maintainer_project.add_maintainer(user)
- expect(subject.execute(project).to_a).to eq([master_project, developer_project])
+ expect(subject.execute(project).to_a).to eq([maintainer_project, developer_project])
end
it 'returns projects after the given offset id' do
@@ -67,9 +67,9 @@ describe MoveToProjectFinder do
reporter_project.add_reporter(user)
developer_project.add_developer(user)
- master_project.add_master(user)
+ maintainer_project.add_maintainer(user)
- expect(subject.execute(project, search: nil, offset_id: master_project.id).to_a).to eq([developer_project, reporter_project])
+ expect(subject.execute(project, search: nil, offset_id: maintainer_project.id).to_a).to eq([developer_project, reporter_project])
expect(subject.execute(project, search: nil, offset_id: developer_project.id).to_a).to eq([reporter_project])
expect(subject.execute(project, search: nil, offset_id: reporter_project.id).to_a).to be_empty
end
@@ -84,10 +84,10 @@ describe MoveToProjectFinder do
it 'returns projects matching a search query' do
foo_project = create(:project)
- foo_project.add_master(user)
+ foo_project.add_maintainer(user)
wadus_project = create(:project, name: 'wadus')
- wadus_project.add_master(user)
+ wadus_project.add_maintainer(user)
expect(subject.execute(project).to_a).to eq([wadus_project, foo_project])
expect(subject.execute(project, search: 'wadus').to_a).to eq([wadus_project])
diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb
index f1ae2c7ab65..b776e9d856a 100644
--- a/spec/finders/notes_finder_spec.rb
+++ b/spec/finders/notes_finder_spec.rb
@@ -5,7 +5,7 @@ describe NotesFinder do
let(:project) { create(:project) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
describe '#execute' do
@@ -133,7 +133,7 @@ describe NotesFinder do
it 'raises an exception for an invalid target_type' do
params[:target_type] = 'invalid'
- expect { described_class.new(project, user, params).execute }.to raise_error('invalid target_type')
+ expect { described_class.new(project, user, params).execute }.to raise_error("invalid target_type '#{params[:target_type]}'")
end
it 'filters out old notes' do
diff --git a/spec/finders/personal_projects_finder_spec.rb b/spec/finders/personal_projects_finder_spec.rb
index 00c551a1f65..ef7dd0cd4a8 100644
--- a/spec/finders/personal_projects_finder_spec.rb
+++ b/spec/finders/personal_projects_finder_spec.rb
@@ -35,7 +35,7 @@ describe PersonalProjectsFinder do
context 'external' do
before do
- current_user.update_attributes(external: true)
+ current_user.update(external: true)
end
it { is_expected.to eq([public_project, private_project]) }
diff --git a/spec/finders/pipelines_finder_spec.rb b/spec/finders/pipelines_finder_spec.rb
index d6253b605b9..c6e832ad69b 100644
--- a/spec/finders/pipelines_finder_spec.rb
+++ b/spec/finders/pipelines_finder_spec.rb
@@ -1,9 +1,10 @@
require 'spec_helper'
describe PipelinesFinder do
- let(:project) { create(:project, :repository) }
-
- subject { described_class.new(project, params).execute }
+ let(:project) { create(:project, :public, :repository) }
+ let(:current_user) { nil }
+ let(:params) { {} }
+ subject { described_class.new(project, current_user, params).execute }
describe "#execute" do
context 'when params is empty' do
@@ -223,5 +224,27 @@ describe PipelinesFinder do
end
end
end
+
+ context 'when the project has limited access to piplines' do
+ let(:project) { create(:project, :private, :repository) }
+ let(:current_user) { create(:user) }
+ let!(:pipelines) { create_list(:ci_pipeline, 2, project: project) }
+
+ context 'when the user has access' do
+ before do
+ project.add_developer(current_user)
+ end
+
+ it 'is expected to return pipelines' do
+ is_expected.to contain_exactly(*pipelines)
+ end
+ end
+
+ context 'the user is not allowed to read pipelines' do
+ it 'returns empty' do
+ is_expected.to be_empty
+ end
+ end
+ end
end
end
diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb
index 0dfe6ba9c32..7931ad9b9f0 100644
--- a/spec/finders/projects_finder_spec.rb
+++ b/spec/finders/projects_finder_spec.rb
@@ -41,7 +41,7 @@ describe ProjectsFinder do
describe 'with private projects' do
before do
- private_project.add_master(user)
+ private_project.add_maintainer(user)
end
it { is_expected.to match_array([public_project, internal_project, private_project]) }
@@ -56,7 +56,7 @@ describe ProjectsFinder do
describe 'filter by visibility_level' do
before do
- private_project.add_master(user)
+ private_project.add_maintainer(user)
end
context 'private' do
diff --git a/spec/finders/runner_jobs_finder_spec.rb b/spec/finders/runner_jobs_finder_spec.rb
index 4275b1a7ff1..97304170c4e 100644
--- a/spec/finders/runner_jobs_finder_spec.rb
+++ b/spec/finders/runner_jobs_finder_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe RunnerJobsFinder do
let(:project) { create(:project) }
- let(:runner) { create(:ci_runner, :shared) }
+ let(:runner) { create(:ci_runner, :instance) }
subject { described_class.new(runner, params).execute }
diff --git a/spec/finders/user_recent_events_finder_spec.rb b/spec/finders/user_recent_events_finder_spec.rb
index 3ca0f7c3c89..da043f94021 100644
--- a/spec/finders/user_recent_events_finder_spec.rb
+++ b/spec/finders/user_recent_events_finder_spec.rb
@@ -1,31 +1,50 @@
require 'spec_helper'
describe UserRecentEventsFinder do
- let(:user) { create(:user) }
- let(:project) { create(:project) }
- let(:project_owner) { project.creator }
- let!(:event) { create(:event, project: project, author: project_owner) }
+ let(:current_user) { create(:user) }
+ let(:project_owner) { create(:user) }
+ let(:private_project) { create(:project, :private, creator: project_owner) }
+ let(:internal_project) { create(:project, :internal, creator: project_owner) }
+ let(:public_project) { create(:project, :public, creator: project_owner) }
+ let!(:private_event) { create(:event, project: private_project, author: project_owner) }
+ let!(:internal_event) { create(:event, project: internal_project, author: project_owner) }
+ let!(:public_event) { create(:event, project: public_project, author: project_owner) }
- subject(:finder) { described_class.new(user, project_owner) }
+ subject(:finder) { described_class.new(current_user, project_owner) }
describe '#execute' do
- it 'does not include the event when a user does not have access to the project' do
- expect(finder.execute).to be_empty
+ context 'current user does not have access to projects' do
+ it 'returns public and internal events' do
+ records = finder.execute
+
+ expect(records).to include(public_event, internal_event)
+ expect(records).not_to include(private_event)
+ end
end
- context 'when the user has access to a project' do
+ context 'when current user has access to the projects' do
before do
- project.add_developer(user)
+ private_project.add_developer(current_user)
+ internal_project.add_developer(current_user)
+ public_project.add_developer(current_user)
end
- it 'includes the event' do
- expect(finder.execute).to include(event)
+ it 'returns all the events' do
+ expect(finder.execute).to include(private_event, internal_event, public_event)
end
- it 'does not include the event if the user cannot read cross project' do
- expect(Ability).to receive(:allowed?).with(user, :read_cross_project) { false }
+ it 'does not include the events if the user cannot read cross project' do
+ expect(Ability).to receive(:allowed?).with(current_user, :read_cross_project) { false }
expect(finder.execute).to be_empty
end
end
+
+ context 'when current user is anonymous' do
+ let(:current_user) { nil }
+
+ it 'returns public events only' do
+ expect(finder.execute).to eq([public_event])
+ end
+ end
end
end
diff --git a/spec/fixtures/api/schemas/cluster_status.json b/spec/fixtures/api/schemas/cluster_status.json
index d27c12e43f2..ccef17a6615 100644
--- a/spec/fixtures/api/schemas/cluster_status.json
+++ b/spec/fixtures/api/schemas/cluster_status.json
@@ -31,7 +31,8 @@
}
},
"status_reason": { "type": ["string", "null"] },
- "external_ip": { "type": ["string", "null"] }
+ "external_ip": { "type": ["string", "null"] },
+ "hostname": { "type": ["string", "null"] }
},
"required" : [ "name", "status" ]
}
diff --git a/spec/fixtures/api/schemas/entities/issue.json b/spec/fixtures/api/schemas/entities/issue.json
index 38467b4ca20..00abe73ec8a 100644
--- a/spec/fixtures/api/schemas/entities/issue.json
+++ b/spec/fixtures/api/schemas/entities/issue.json
@@ -27,7 +27,7 @@
"due_date": { "type": "date" },
"confidential": { "type": "boolean" },
"discussion_locked": { "type": ["boolean", "null"] },
- "updated_by_id": { "type": ["string", "null"] },
+ "updated_by_id": { "type": ["integer", "null"] },
"time_estimate": { "type": "integer" },
"total_time_spent": { "type": "integer" },
"human_time_estimate": { "type": ["integer", "null"] },
diff --git a/spec/fixtures/api/schemas/entities/merge_request_basic.json b/spec/fixtures/api/schemas/entities/merge_request_basic.json
index 46031961cca..cf257ac00de 100644
--- a/spec/fixtures/api/schemas/entities/merge_request_basic.json
+++ b/spec/fixtures/api/schemas/entities/merge_request_basic.json
@@ -13,7 +13,22 @@
"assignee_id": { "type": ["integer", "null"] },
"subscribed": { "type": ["boolean", "null"] },
"participants": { "type": "array" },
- "allow_maintainer_to_push": { "type": "boolean"}
+ "allow_collaboration": { "type": "boolean"},
+ "allow_maintainer_to_push": { "type": "boolean"},
+ "assignee": {
+ "oneOf": [
+ { "type": "null" },
+ { "$ref": "user.json" }
+ ]
+ },
+ "milestone": {
+ "type": [ "object", "null" ]
+ },
+ "labels": {
+ "type": [ "array", "null" ]
+ },
+ "task_status": { "type": "string" },
+ "task_status_short": { "type": "string" }
},
"additionalProperties": false
}
diff --git a/spec/fixtures/api/schemas/entities/merge_request_widget.json b/spec/fixtures/api/schemas/entities/merge_request_widget.json
index 233102c4314..38ce92a5dc7 100644
--- a/spec/fixtures/api/schemas/entities/merge_request_widget.json
+++ b/spec/fixtures/api/schemas/entities/merge_request_widget.json
@@ -31,7 +31,7 @@
"source_project_id": { "type": "integer" },
"target_branch": { "type": "string" },
"target_project_id": { "type": "integer" },
- "allow_maintainer_to_push": { "type": "boolean"},
+ "allow_collaboration": { "type": "boolean"},
"metrics": {
"oneOf": [
{ "type": "null" },
@@ -109,10 +109,12 @@
"ff_only_enabled": { "type": ["boolean", false] },
"should_be_rebased": { "type": "boolean" },
"create_note_path": { "type": ["string", "null"] },
+ "preview_note_path": { "type": ["string", "null"] },
"rebase_commit_sha": { "type": ["string", "null"] },
"rebase_in_progress": { "type": "boolean" },
"can_push_to_source_branch": { "type": "boolean" },
- "rebase_path": { "type": ["string", "null"] }
+ "rebase_path": { "type": ["string", "null"] },
+ "squash": { "type": "boolean" }
},
"additionalProperties": false
}
diff --git a/spec/fixtures/api/schemas/list.json b/spec/fixtures/api/schemas/list.json
index 05922df6b81..b76ec115293 100644
--- a/spec/fixtures/api/schemas/list.json
+++ b/spec/fixtures/api/schemas/list.json
@@ -37,5 +37,5 @@
"title": { "type": "string" },
"position": { "type": ["integer", "null"] }
},
- "additionalProperties": false
+ "additionalProperties": true
}
diff --git a/spec/fixtures/api/schemas/public_api/v3/issues.json b/spec/fixtures/api/schemas/public_api/v3/issues.json
deleted file mode 100644
index 51b0822bc66..00000000000
--- a/spec/fixtures/api/schemas/public_api/v3/issues.json
+++ /dev/null
@@ -1,78 +0,0 @@
-{
- "type": "array",
- "items": {
- "type": "object",
- "properties" : {
- "id": { "type": "integer" },
- "iid": { "type": "integer" },
- "project_id": { "type": "integer" },
- "title": { "type": "string" },
- "description": { "type": ["string", "null"] },
- "state": { "type": "string" },
- "created_at": { "type": "date" },
- "updated_at": { "type": "date" },
- "labels": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "milestone": {
- "type": "object",
- "properties": {
- "id": { "type": "integer" },
- "iid": { "type": "integer" },
- "project_id": { "type": ["integer", "null"] },
- "group_id": { "type": ["integer", "null"] },
- "title": { "type": "string" },
- "description": { "type": ["string", "null"] },
- "state": { "type": "string" },
- "created_at": { "type": "date" },
- "updated_at": { "type": "date" },
- "due_date": { "type": "date" },
- "start_date": { "type": "date" }
- },
- "additionalProperties": false
- },
- "assignee": {
- "type": ["object", "null"],
- "properties": {
- "name": { "type": "string" },
- "username": { "type": "string" },
- "id": { "type": "integer" },
- "state": { "type": "string" },
- "avatar_url": { "type": "uri" },
- "web_url": { "type": "uri" }
- },
- "additionalProperties": false
- },
- "author": {
- "type": "object",
- "properties": {
- "name": { "type": "string" },
- "username": { "type": "string" },
- "id": { "type": "integer" },
- "state": { "type": "string" },
- "avatar_url": { "type": "uri" },
- "web_url": { "type": "uri" }
- },
- "additionalProperties": false
- },
- "user_notes_count": { "type": "integer" },
- "upvotes": { "type": "integer" },
- "downvotes": { "type": "integer" },
- "due_date": { "type": ["date", "null"] },
- "confidential": { "type": "boolean" },
- "web_url": { "type": "uri" },
- "subscribed": { "type": ["boolean"] }
- },
- "required": [
- "id", "iid", "project_id", "title", "description",
- "state", "created_at", "updated_at", "labels",
- "milestone", "assignee", "author", "user_notes_count",
- "upvotes", "downvotes", "due_date", "confidential",
- "web_url", "subscribed"
- ],
- "additionalProperties": false
- }
-}
diff --git a/spec/fixtures/api/schemas/public_api/v3/merge_requests.json b/spec/fixtures/api/schemas/public_api/v3/merge_requests.json
deleted file mode 100644
index b5c74bcc26e..00000000000
--- a/spec/fixtures/api/schemas/public_api/v3/merge_requests.json
+++ /dev/null
@@ -1,90 +0,0 @@
-{
- "type": "array",
- "items": {
- "type": "object",
- "properties" : {
- "id": { "type": "integer" },
- "iid": { "type": "integer" },
- "project_id": { "type": "integer" },
- "title": { "type": "string" },
- "description": { "type": ["string", "null"] },
- "state": { "type": "string" },
- "created_at": { "type": "date" },
- "updated_at": { "type": "date" },
- "target_branch": { "type": "string" },
- "source_branch": { "type": "string" },
- "upvotes": { "type": "integer" },
- "downvotes": { "type": "integer" },
- "author": {
- "type": "object",
- "properties": {
- "name": { "type": "string" },
- "username": { "type": "string" },
- "id": { "type": "integer" },
- "state": { "type": "string" },
- "avatar_url": { "type": "uri" },
- "web_url": { "type": "uri" }
- },
- "additionalProperties": false
- },
- "assignee": {
- "type": "object",
- "properties": {
- "name": { "type": "string" },
- "username": { "type": "string" },
- "id": { "type": "integer" },
- "state": { "type": "string" },
- "avatar_url": { "type": "uri" },
- "web_url": { "type": "uri" }
- },
- "additionalProperties": false
- },
- "source_project_id": { "type": "integer" },
- "target_project_id": { "type": "integer" },
- "labels": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "work_in_progress": { "type": "boolean" },
- "milestone": {
- "type": ["object", "null"],
- "properties": {
- "id": { "type": "integer" },
- "iid": { "type": "integer" },
- "project_id": { "type": ["integer", "null"] },
- "group_id": { "type": ["integer", "null"] },
- "title": { "type": "string" },
- "description": { "type": ["string", "null"] },
- "state": { "type": "string" },
- "created_at": { "type": "date" },
- "updated_at": { "type": "date" },
- "due_date": { "type": "date" },
- "start_date": { "type": "date" }
- },
- "additionalProperties": false
- },
- "merge_when_build_succeeds": { "type": "boolean" },
- "merge_status": { "type": "string" },
- "sha": { "type": "string" },
- "merge_commit_sha": { "type": ["string", "null"] },
- "user_notes_count": { "type": "integer" },
- "should_remove_source_branch": { "type": ["boolean", "null"] },
- "force_remove_source_branch": { "type": ["boolean", "null"] },
- "web_url": { "type": "uri" },
- "subscribed": { "type": ["boolean"] }
- },
- "required": [
- "id", "iid", "project_id", "title", "description",
- "state", "created_at", "updated_at", "target_branch",
- "source_branch", "upvotes", "downvotes", "author",
- "assignee", "source_project_id", "target_project_id",
- "labels", "work_in_progress", "milestone", "merge_when_build_succeeds",
- "merge_status", "sha", "merge_commit_sha", "user_notes_count",
- "should_remove_source_branch", "force_remove_source_branch",
- "web_url", "subscribed"
- ],
- "additionalProperties": false
- }
-}
diff --git a/spec/fixtures/api/schemas/public_api/v4/branch.json b/spec/fixtures/api/schemas/public_api/v4/branch.json
index a3581178974..a8891680d06 100644
--- a/spec/fixtures/api/schemas/public_api/v4/branch.json
+++ b/spec/fixtures/api/schemas/public_api/v4/branch.json
@@ -14,7 +14,8 @@
"merged": { "type": "boolean" },
"protected": { "type": "boolean" },
"developers_can_push": { "type": "boolean" },
- "developers_can_merge": { "type": "boolean" }
+ "developers_can_merge": { "type": "boolean" },
+ "can_push": { "type": "boolean" }
},
"additionalProperties": false
}
diff --git a/spec/fixtures/api/schemas/public_api/v4/commit/with_stats.json b/spec/fixtures/api/schemas/public_api/v4/commit/with_stats.json
new file mode 100644
index 00000000000..3b5dd547e69
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/commit/with_stats.json
@@ -0,0 +1,14 @@
+{
+ "type": "object",
+ "allOf": [
+ { "$ref": "basic.json" },
+ {
+ "required" : [
+ "stats"
+ ],
+ "properties": {
+ "stats": { "$ref": "../commit_stats.json" }
+ }
+ }
+ ]
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/commits_with_stats.json b/spec/fixtures/api/schemas/public_api/v4/commits_with_stats.json
new file mode 100644
index 00000000000..23511123ce4
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/commits_with_stats.json
@@ -0,0 +1,4 @@
+{
+ "type": "array",
+ "items": { "$ref": "commit/with_stats.json" }
+}
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 0dc2eabec5d..f7adc4e0b91 100644
--- a/spec/fixtures/api/schemas/public_api/v4/merge_requests.json
+++ b/spec/fixtures/api/schemas/public_api/v4/merge_requests.json
@@ -75,12 +75,14 @@
"force_remove_source_branch": { "type": ["boolean", "null"] },
"discussion_locked": { "type": ["boolean", "null"] },
"web_url": { "type": "uri" },
+ "squash": { "type": "boolean" },
"time_stats": {
"time_estimate": { "type": "integer" },
"total_time_spent": { "type": "integer" },
"human_time_estimate": { "type": ["string", "null"] },
"human_total_time_spent": { "type": ["string", "null"] }
},
+ "allow_collaboration": { "type": ["boolean", "null"] },
"allow_maintainer_to_push": { "type": ["boolean", "null"] }
},
"required": [
@@ -91,7 +93,7 @@
"labels", "work_in_progress", "milestone", "merge_when_pipeline_succeeds",
"merge_status", "sha", "merge_commit_sha", "user_notes_count",
"should_remove_source_branch", "force_remove_source_branch",
- "web_url"
+ "web_url", "squash"
],
"additionalProperties": false
}
diff --git a/spec/fixtures/api/schemas/public_api/v4/milestones.json b/spec/fixtures/api/schemas/public_api/v4/milestones.json
index c3c42b6ee60..448e97d6c85 100644
--- a/spec/fixtures/api/schemas/public_api/v4/milestones.json
+++ b/spec/fixtures/api/schemas/public_api/v4/milestones.json
@@ -13,7 +13,8 @@
"created_at": { "type": "date" },
"updated_at": { "type": "date" },
"start_date": { "type": "date" },
- "due_date": { "type": "date" }
+ "due_date": { "type": "date" },
+ "web_url": { "type": "string" }
},
"required": [
"id", "iid", "title", "description", "state",
diff --git a/spec/fixtures/api/schemas/public_api/v4/snippets.json b/spec/fixtures/api/schemas/public_api/v4/snippets.json
index e37e9704649..d13d703e063 100644
--- a/spec/fixtures/api/schemas/public_api/v4/snippets.json
+++ b/spec/fixtures/api/schemas/public_api/v4/snippets.json
@@ -8,6 +8,7 @@
"title": { "type": "string" },
"file_name": { "type": ["string", "null"] },
"description": { "type": ["string", "null"] },
+ "visibility": { "type": "string" },
"web_url": { "type": "string" },
"created_at": { "type": "date" },
"updated_at": { "type": "date" },
diff --git a/spec/fixtures/authentication/saml_response.xml b/spec/fixtures/authentication/saml_response.xml
new file mode 100644
index 00000000000..ac7b662be22
--- /dev/null
+++ b/spec/fixtures/authentication/saml_response.xml
@@ -0,0 +1,42 @@
+<?xml version='1.0'?>
+<samlp:Response xmlns:samlp='urn:oasis:names:tc:SAML:2.0:protocol' xmlns:saml='urn:oasis:names:tc:SAML:2.0:assertion' ID='pfxb9b71715-2202-9a51-8ae5-689d5b9dd25a' Version='2.0' IssueInstant='2014-07-17T01:01:48Z' Destination='http://sp.example.com/demo1/index.php?acs' InResponseTo='ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685'>
+ <saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer><ds:Signature xmlns:ds='http://www.w3.org/2000/09/xmldsig#'>
+ <ds:SignedInfo><ds:CanonicalizationMethod Algorithm='http://www.w3.org/2001/10/xml-exc-c14n#'/>
+ <ds:SignatureMethod Algorithm='http://www.w3.org/2000/09/xmldsig#rsa-sha1'/>
+ <ds:Reference URI='#pfxb9b71715-2202-9a51-8ae5-689d5b9dd25a'><ds:Transforms><ds:Transform Algorithm='http://www.w3.org/2000/09/xmldsig#enveloped-signature'/><ds:Transform Algorithm='http://www.w3.org/2001/10/xml-exc-c14n#'/></ds:Transforms><ds:DigestMethod Algorithm='http://www.w3.org/2000/09/xmldsig#sha1'/><ds:DigestValue>z0Y25hsUHVJJnYhgB5LzPVjqbgM=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>NSdsZopzNX4kJETipLNbU+7dG4GPTj5e40iSBaUeUMc1UUSX4UCe9Qx6R9ADEkEQgNekgYaCFOuY90kLNh9Ky0Czq8gd4w7ykQJEVJ7VF7LakmG8dPedHAKyAMAuZ8y3mNGye31vtR9frYaznCVoxB3eAi9rbVOXkQtdOTRMHec=</ds:SignatureValue>
+ <ds:KeyInfo><ds:X509Data><ds:X509Certificate>MIICajCCAdOgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBSMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRcwFQYDVQQDDA5zcC5leGFtcGxlLmNvbTAeFw0xNDA3MTcxNDEyNTZaFw0xNTA3MTcxNDEyNTZaMFIxCzAJBgNVBAYTAnVzMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQKDAxPbmVsb2dpbiBJbmMxFzAVBgNVBAMMDnNwLmV4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZx+ON4IUoIWxgukTb1tOiX3bMYzYQiwWPUNMp+Fq82xoNogso2bykZG0yiJm5o8zv/sd6pGouayMgkx/2FSOdc36T0jGbCHuRSbtia0PEzNIRtmViMrt3AeoWBidRXmZsxCNLwgIV6dn2WpuE5Az0bHgpZnQxTKFek0BMKU/d8wIDAQABo1AwTjAdBgNVHQ4EFgQUGHxYqZYyX7cTxKVODVgZwSTdCnwwHwYDVR0jBBgwFoAUGHxYqZYyX7cTxKVODVgZwSTdCnwwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQByFOl+hMFICbd3DJfnp2Rgd/dqttsZG/tyhILWvErbio/DEe98mXpowhTkC04ENprOyXi7ZbUqiicF89uAGyt1oqgTUCD1VsLahqIcmrzgumNyTwLGWo17WDAa1/usDhetWAMhgzF/Cnf5ek0nK00m0YZGyc4LzgD0CROMASTWNg==</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature>
+ <samlp:Status>
+ <samlp:StatusCode Value='urn:oasis:names:tc:SAML:2.0:status:Success'/>
+ </samlp:Status>
+ <saml:Assertion xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:xs='http://www.w3.org/2001/XMLSchema' ID='_d71a3a8e9fcc45c9e9d248ef7049393fc8f04e5f75' Version='2.0' IssueInstant='2014-07-17T01:01:48Z'>
+ <saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer>
+ <saml:Subject>
+ <saml:NameID SPNameQualifier='http://sp.example.com/demo1/metadata.php' Format='urn:oasis:names:tc:SAML:2.0:nameid-format:transient'>_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7</saml:NameID>
+ <saml:SubjectConfirmation Method='urn:oasis:names:tc:SAML:2.0:cm:bearer'>
+ <saml:SubjectConfirmationData NotOnOrAfter='2024-01-18T06:21:48Z' Recipient='http://sp.example.com/demo1/index.php?acs' InResponseTo='ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685'/>
+ </saml:SubjectConfirmation>
+ </saml:Subject>
+ <saml:Conditions NotBefore='2014-07-17T01:01:18Z' NotOnOrAfter='2024-01-18T06:21:48Z'>
+ <saml:AudienceRestriction>
+ <saml:Audience>http://sp.example.com/demo1/metadata.php</saml:Audience>
+ </saml:AudienceRestriction>
+ </saml:Conditions>
+ <saml:AuthnStatement AuthnInstant='2014-07-17T01:01:48Z' SessionNotOnOrAfter='2024-07-17T09:01:48Z' SessionIndex='_be9967abd904ddcae3c0eb4189adbe3f71e327cf93'>
+ <saml:AuthnContext>
+ <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
+ </saml:AuthnContext>
+ </saml:AuthnStatement>
+ <saml:AttributeStatement>
+ <saml:Attribute Name='uid' NameFormat='urn:oasis:names:tc:SAML:2.0:attrname-format:basic'>
+ <saml:AttributeValue xsi:type='xs:string'>test</saml:AttributeValue>
+ </saml:Attribute>
+ <saml:Attribute Name='mail' NameFormat='urn:oasis:names:tc:SAML:2.0:attrname-format:basic'>
+ <saml:AttributeValue xsi:type='xs:string'>test@example.com</saml:AttributeValue>
+ </saml:Attribute>
+ <saml:Attribute Name='eduPersonAffiliation' NameFormat='urn:oasis:names:tc:SAML:2.0:attrname-format:basic'>
+ <saml:AttributeValue xsi:type='xs:string'>users</saml:AttributeValue>
+ <saml:AttributeValue xsi:type='xs:string'>examplerole1</saml:AttributeValue>
+ </saml:Attribute>
+ </saml:AttributeStatement>
+ </saml:Assertion>
+</samlp:Response>
diff --git a/spec/fixtures/exported-project.gz b/spec/fixtures/exported-project.gz
deleted file mode 100644
index bef7e2ff8ee..00000000000
--- a/spec/fixtures/exported-project.gz
+++ /dev/null
Binary files differ
diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb
index da32a46675f..e5d01c3bd03 100644
--- a/spec/fixtures/markdown.md.erb
+++ b/spec/fixtures/markdown.md.erb
@@ -43,8 +43,14 @@ This text says this, ~~and this text doesn't~~.
### Superscript
-This is my 1^(st) time using superscript in Markdown. Now this is my
-2^(nd).
+This is my 1<sup>(st)</sup> time using superscript in Markdown. Now this is my
+2<sup>(nd)</sup>.
+
+Redcarpet supports this superscript syntax ( x^2 ).
+
+### Subscript
+
+This (C<sub>6</sub>H<sub>12</sub>O<sub>6</sub>) is an example of subscripts in Markdown.
### Next step
diff --git a/spec/fixtures/project_export.tar.gz b/spec/fixtures/project_export.tar.gz
new file mode 100644
index 00000000000..72ab2d71f35
--- /dev/null
+++ b/spec/fixtures/project_export.tar.gz
Binary files differ
diff --git a/spec/fixtures/trace/sample_trace b/spec/fixtures/trace/sample_trace
index c65cf05d5ca..7bfe3f83b7b 100644
--- a/spec/fixtures/trace/sample_trace
+++ b/spec/fixtures/trace/sample_trace
@@ -41,7 +41,7 @@ From https://gitlab.com/gitlab-org/gitlab-ce
section_end:1522927113:get_sources
section_start:1522927113:restore_cache
Checking cache for ruby-2.3.6-with-yarn...
-Downloading cache.zip from http://runners-cache-5-internal.gitlab.com:444/runner/project/13083/ruby-2.3.6-with-yarn
+Downloading cache.zip from http://runners-cache-5-internal.gitlab.com:444/runner/project/13083/ruby-2.3.6-with-yarn
Successfully extracted cache
section_end:1522927128:restore_cache
section_start:1522927128:download_artifacts
@@ -51,7 +51,7 @@ Downloading artifacts from coordinator... ok  id=61303215 respon
Downloading artifacts from coordinator... ok  id=61303216 responseStatus=200 OK token=iy2yYbq8
Downloading artifacts for setup-test-env (61303217)...
Downloading artifacts from coordinator... ok  id=61303217 responseStatus=200 OK token=ur1g79-4
-WARNING: tmp/tests/gitlab-shell/.gitlab_shell_secret: chmod tmp/tests/gitlab-shell/.gitlab_shell_secret: no such file or directory (suppressing repeats)
+WARNING: tmp/tests/gitlab-shell/.gitlab_shell_secret: chmod tmp/tests/gitlab-shell/.gitlab_shell_secret: no such file or directory (suppressing repeats)
section_end:1522927141:download_artifacts
section_start:1522927141:build_script
$ bundle --version
@@ -1486,7 +1486,7 @@ Gitlab::ImportExport::ProjectTreeSaver
overrides the project description
group members
does not export group members if it has no permission
- does not export group members as master
+ does not export group members as maintainer
exports group members as group owner
as admin
exports group members as admin
@@ -1690,7 +1690,7 @@ GroupsController
and logged in as Developer
behaves like member without ability to create subgroups
renders the 404 page (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
- and logged in as Master
+ and logged in as Maintainer
behaves like member without ability to create subgroups
renders the 404 page (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
and can_create_group is false
@@ -1706,7 +1706,7 @@ GroupsController
and logged in as Developer
behaves like member without ability to create subgroups
renders the 404 page (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
- and logged in as Master
+ and logged in as Maintainer
behaves like member without ability to create subgroups
renders the 404 page (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
GET #activity
@@ -2324,7 +2324,7 @@ Editing file blob
shows blob editor with same branch
with protected branch
shows blob editor with patch branch
- as master
+ as maintainer
shows blob editor with same branch
Boards::Lists::MoveService
@@ -2880,7 +2880,7 @@ API::V3::Environments
won't update the external_url if only the name is passed
returns a 404 if the environment does not exist
DELETE /projects/:id/environments/:environment_id
- as a master
+ as a maintainer
returns a 200 for an existing environment
returns a 404 for non existing id
a non member
@@ -3001,11 +3001,11 @@ LfsFileLock
#can_be_unlocked_by?
when it's forced
can be unlocked by the author
- can be unlocked by a master
+ can be unlocked by a maintainer
can't be unlocked by other user
when it isn't forced
can be unlocked by the author
- can't be unlocked by a master
+ can't be unlocked by a maintainer
can't be unlocked by other user
Gitlab::Ci::Config::Entry::Boolean
@@ -3069,7 +3069,7 @@ Pending: (Failures listed here are expected and do not affect your suite's statu
# around hook at ./spec/spec_helper.rb:186 did not execute the example
# ./spec/controllers/groups_controller_spec.rb:25
- 5) GroupsController GET #new when creating subgroups and can_create_group is true and logged in as Master behaves like member without ability to create subgroups renders the 404 page
+ 5) GroupsController GET #new when creating subgroups and can_create_group is true and logged in as Maintainer behaves like member without ability to create subgroups renders the 404 page
# around hook at ./spec/spec_helper.rb:186 did not execute the example
# ./spec/controllers/groups_controller_spec.rb:25
@@ -3089,7 +3089,7 @@ Pending: (Failures listed here are expected and do not affect your suite's statu
# around hook at ./spec/spec_helper.rb:186 did not execute the example
# ./spec/controllers/groups_controller_spec.rb:25
- 10) GroupsController GET #new when creating subgroups and can_create_group is false and logged in as Master behaves like member without ability to create subgroups renders the 404 page
+ 10) GroupsController GET #new when creating subgroups and can_create_group is false and logged in as Maintainer behaves like member without ability to create subgroups renders the 404 page
# around hook at ./spec/spec_helper.rb:186 did not execute the example
# ./spec/controllers/groups_controller_spec.rb:25
@@ -3237,7 +3237,7 @@ Pending: (Failures listed here are expected and do not affect your suite's statu
# around hook at ./spec/spec_helper.rb:190 did not execute the example
# ./spec/services/groups/transfer_service_spec.rb:212
- 47) Groups::TransferService#execute when transferring a subgroup into another group when the group is allowed to be transferred should update parent group to the new parent
+ 47) Groups::TransferService#execute when transferring a subgroup into another group when the group is allowed to be transferred should update parent group to the new parent
# around hook at ./spec/spec_helper.rb:190 did not execute the example
# ./spec/services/groups/transfer_service_spec.rb:216
@@ -3435,10 +3435,10 @@ section_end:1522927515:after_script
section_end:1522927516:archive_cache
section_start:1522927516:upload_artifacts
Uploading artifacts...
-coverage/: found 5 matching files 
-knapsack/: found 5 matching files 
-rspec_flaky/: found 4 matching files 
-WARNING: tmp/capybara/: no matching files 
+coverage/: found 5 matching files 
+knapsack/: found 5 matching files 
+rspec_flaky/: found 4 matching files 
+WARNING: tmp/capybara/: no matching files 
Uploading artifacts to coordinator... ok  id=61303283 responseStatus=201 Created token=rusBKvxM
section_end:1522927520:upload_artifacts
Job succeeded
diff --git a/spec/graphql/gitlab_schema_spec.rb b/spec/graphql/gitlab_schema_spec.rb
new file mode 100644
index 00000000000..515bbe78cb7
--- /dev/null
+++ b/spec/graphql/gitlab_schema_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+describe GitlabSchema do
+ it 'uses batch loading' do
+ expect(field_instrumenters).to include(BatchLoader::GraphQL)
+ end
+
+ it 'enables the preload instrumenter' do
+ expect(field_instrumenters).to include(BatchLoader::GraphQL)
+ end
+
+ it 'enables the authorization instrumenter' do
+ expect(field_instrumenters).to include(instance_of(::Gitlab::Graphql::Authorize::Instrumentation))
+ end
+
+ it 'enables using presenters' do
+ expect(field_instrumenters).to include(instance_of(::Gitlab::Graphql::Present::Instrumentation))
+ end
+
+ it 'has the base mutation' do
+ pending('Adding an empty mutation breaks the documentation explorer')
+
+ expect(described_class.mutation).to eq(::Types::MutationType.to_graphql)
+ end
+
+ it 'has the base query' do
+ expect(described_class.query).to eq(::Types::QueryType.to_graphql)
+ end
+
+ it 'paginates active record relations using `Gitlab::Graphql::Connections::KeysetConnection`' do
+ connection = GraphQL::Relay::BaseConnection::CONNECTION_IMPLEMENTATIONS[ActiveRecord::Relation.name]
+
+ expect(connection).to eq(Gitlab::Graphql::Connections::KeysetConnection)
+ end
+
+ def field_instrumenters
+ described_class.instrumenters[:field]
+ end
+end
diff --git a/spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb b/spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb
new file mode 100644
index 00000000000..ea7159eacf9
--- /dev/null
+++ b/spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+describe ResolvesPipelines do
+ include GraphqlHelpers
+
+ subject(:resolver) do
+ Class.new(Resolvers::BaseResolver) do
+ include ResolvesPipelines
+
+ def resolve(**args)
+ resolve_pipelines(object, args)
+ end
+ end
+ end
+
+ let(:current_user) { create(:user) }
+ set(:project) { create(:project, :private) }
+ set(:pipeline) { create(:ci_pipeline, project: project) }
+ set(:failed_pipeline) { create(:ci_pipeline, :failed, project: project) }
+ set(:ref_pipeline) { create(:ci_pipeline, project: project, ref: 'awesome-feature') }
+ set(:sha_pipeline) { create(:ci_pipeline, project: project, sha: 'deadbeef') }
+
+ before do
+ project.add_developer(current_user)
+ end
+
+ it { is_expected.to have_graphql_arguments(:status, :ref, :sha) }
+
+ it 'finds all pipelines' do
+ expect(resolve_pipelines).to contain_exactly(pipeline, failed_pipeline, ref_pipeline, sha_pipeline)
+ end
+
+ it 'allows filtering by status' do
+ expect(resolve_pipelines(status: 'failed')).to contain_exactly(failed_pipeline)
+ end
+
+ it 'allows filtering by ref' do
+ expect(resolve_pipelines(ref: 'awesome-feature')).to contain_exactly(ref_pipeline)
+ end
+
+ it 'allows filtering by sha' do
+ expect(resolve_pipelines(sha: 'deadbeef')).to contain_exactly(sha_pipeline)
+ end
+
+ it 'does not return any pipelines if the user does not have access' do
+ expect(resolve_pipelines({}, {})).to be_empty
+ end
+
+ def resolve_pipelines(args = {}, context = { current_user: current_user })
+ resolve(resolver, obj: project, args: args, ctx: context)
+ end
+end
diff --git a/spec/graphql/resolvers/merge_request_pipelines_resolver_spec.rb b/spec/graphql/resolvers/merge_request_pipelines_resolver_spec.rb
new file mode 100644
index 00000000000..09b17bf6fc9
--- /dev/null
+++ b/spec/graphql/resolvers/merge_request_pipelines_resolver_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+describe Resolvers::MergeRequestPipelinesResolver do
+ include GraphqlHelpers
+
+ set(:merge_request) { create(:merge_request) }
+ set(:pipeline) do
+ create(
+ :ci_pipeline,
+ project: merge_request.source_project,
+ ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha
+ )
+ end
+ set(:other_project_pipeline) { create(:ci_pipeline, project: merge_request.source_project) }
+ set(:other_pipeline) { create(:ci_pipeline) }
+ let(:current_user) { create(:user) }
+
+ before do
+ merge_request.project.add_developer(current_user)
+ end
+
+ def resolve_pipelines
+ resolve(described_class, obj: merge_request, ctx: { current_user: current_user })
+ end
+
+ it 'resolves only MRs for the passed merge request' do
+ expect(resolve_pipelines).to contain_exactly(pipeline)
+ end
+end
diff --git a/spec/graphql/resolvers/merge_request_resolver_spec.rb b/spec/graphql/resolvers/merge_request_resolver_spec.rb
new file mode 100644
index 00000000000..73993b3a039
--- /dev/null
+++ b/spec/graphql/resolvers/merge_request_resolver_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+describe Resolvers::MergeRequestResolver do
+ include GraphqlHelpers
+
+ set(:project) { create(:project, :repository) }
+ set(:merge_request_1) { create(:merge_request, :simple, source_project: project, target_project: project) }
+ set(:merge_request_2) { create(:merge_request, :rebased, source_project: project, target_project: project) }
+
+ set(:other_project) { create(:project, :repository) }
+ set(:other_merge_request) { create(:merge_request, source_project: other_project, target_project: other_project) }
+
+ let(:iid_1) { merge_request_1.iid }
+ let(:iid_2) { merge_request_2.iid }
+
+ let(:other_iid) { other_merge_request.iid }
+
+ describe '#resolve' do
+ it 'batch-resolves merge requests by target project full path and IID' do
+ result = batch(max_queries: 2) do
+ [resolve_mr(project, iid_1), resolve_mr(project, iid_2)]
+ end
+
+ expect(result).to contain_exactly(merge_request_1, merge_request_2)
+ end
+
+ it 'can batch-resolve merge requests from different projects' do
+ result = batch(max_queries: 3) do
+ [resolve_mr(project, iid_1), resolve_mr(project, iid_2), resolve_mr(other_project, other_iid)]
+ end
+
+ expect(result).to contain_exactly(merge_request_1, merge_request_2, other_merge_request)
+ end
+
+ it 'resolves an unknown iid to nil' do
+ result = batch { resolve_mr(project, -1) }
+
+ expect(result).to be_nil
+ end
+ end
+
+ def resolve_mr(project, iid)
+ resolve(described_class, obj: project, args: { iid: iid })
+ end
+end
diff --git a/spec/graphql/resolvers/project_pipelines_resolver_spec.rb b/spec/graphql/resolvers/project_pipelines_resolver_spec.rb
new file mode 100644
index 00000000000..407ca2f9d78
--- /dev/null
+++ b/spec/graphql/resolvers/project_pipelines_resolver_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe Resolvers::ProjectPipelinesResolver do
+ include GraphqlHelpers
+
+ set(:project) { create(:project) }
+ set(:pipeline) { create(:ci_pipeline, project: project) }
+ set(:other_pipeline) { create(:ci_pipeline) }
+ let(:current_user) { create(:user) }
+
+ before do
+ project.add_developer(current_user)
+ end
+
+ def resolve_pipelines
+ resolve(described_class, obj: project, ctx: { current_user: current_user })
+ end
+
+ it 'resolves only MRs for the passed merge request' do
+ expect(resolve_pipelines).to contain_exactly(pipeline)
+ end
+end
diff --git a/spec/graphql/resolvers/project_resolver_spec.rb b/spec/graphql/resolvers/project_resolver_spec.rb
new file mode 100644
index 00000000000..d4990c6492c
--- /dev/null
+++ b/spec/graphql/resolvers/project_resolver_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe Resolvers::ProjectResolver do
+ include GraphqlHelpers
+
+ set(:project1) { create(:project) }
+ set(:project2) { create(:project) }
+
+ set(:other_project) { create(:project) }
+
+ describe '#resolve' do
+ it 'batch-resolves projects by full path' do
+ paths = [project1.full_path, project2.full_path]
+
+ result = batch(max_queries: 1) do
+ paths.map { |path| resolve_project(path) }
+ end
+
+ expect(result).to contain_exactly(project1, project2)
+ end
+
+ it 'resolves an unknown full_path to nil' do
+ result = batch { resolve_project('unknown/project') }
+
+ expect(result).to be_nil
+ end
+ end
+
+ def resolve_project(full_path)
+ resolve(described_class, args: { full_path: full_path })
+ end
+end
diff --git a/spec/graphql/types/ci/pipeline_type_spec.rb b/spec/graphql/types/ci/pipeline_type_spec.rb
new file mode 100644
index 00000000000..ec1c689a4be
--- /dev/null
+++ b/spec/graphql/types/ci/pipeline_type_spec.rb
@@ -0,0 +1,7 @@
+require 'spec_helper'
+
+describe Types::Ci::PipelineType do
+ it { expect(described_class.graphql_name).to eq('Pipeline') }
+
+ it { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Ci::Pipeline) }
+end
diff --git a/spec/graphql/types/merge_request_type_spec.rb b/spec/graphql/types/merge_request_type_spec.rb
new file mode 100644
index 00000000000..c369953e3ea
--- /dev/null
+++ b/spec/graphql/types/merge_request_type_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe GitlabSchema.types['MergeRequest'] do
+ it { expect(described_class).to expose_permissions_using(Types::PermissionTypes::MergeRequest) }
+
+ describe 'head pipeline' do
+ it 'has a head pipeline field' do
+ expect(described_class).to have_graphql_field(:head_pipeline)
+ end
+
+ it 'authorizes the field' do
+ expect(described_class.fields['headPipeline'])
+ .to require_graphql_authorizations(:read_pipeline)
+ end
+ end
+end
diff --git a/spec/graphql/types/permission_types/base_permission_type_spec.rb b/spec/graphql/types/permission_types/base_permission_type_spec.rb
new file mode 100644
index 00000000000..a7e51797047
--- /dev/null
+++ b/spec/graphql/types/permission_types/base_permission_type_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe Types::PermissionTypes::BasePermissionType do
+ let(:permitable) { double('permittable') }
+ let(:current_user) { build(:user) }
+ let(:context) { { current_user: current_user } }
+ subject(:test_type) do
+ Class.new(described_class) do
+ graphql_name 'TestClass'
+
+ permission_field :do_stuff, resolve: -> (_, _, _) { true }
+ ability_field(:read_issue)
+ abilities :admin_issue
+ end
+ end
+
+ describe '.permission_field' do
+ it 'adds a field for the required permission' do
+ is_expected.to have_graphql_field(:do_stuff)
+ end
+ end
+
+ describe '.ability_field' do
+ it 'adds a field for the required permission' do
+ is_expected.to have_graphql_field(:read_issue)
+ end
+
+ it 'does not add a resolver block if another resolving param is passed' do
+ expected_keywords = {
+ name: :resolve_using_hash,
+ hash_key: :the_key,
+ type: GraphQL::BOOLEAN_TYPE,
+ description: "custom description",
+ null: false
+ }
+ expect(test_type).to receive(:field).with(expected_keywords)
+
+ test_type.ability_field :resolve_using_hash, hash_key: :the_key, description: "custom description"
+ end
+ end
+
+ describe '.abilities' do
+ it 'adds a field for the passed permissions' do
+ is_expected.to have_graphql_field(:admin_issue)
+ end
+ end
+end
diff --git a/spec/graphql/types/permission_types/merge_request_spec.rb b/spec/graphql/types/permission_types/merge_request_spec.rb
new file mode 100644
index 00000000000..e1026b01a74
--- /dev/null
+++ b/spec/graphql/types/permission_types/merge_request_spec.rb
@@ -0,0 +1,13 @@
+require 'spec_helper'
+
+describe Types::PermissionTypes::MergeRequest do
+ it do
+ expected_permissions = [
+ :read_merge_request, :admin_merge_request, :update_merge_request,
+ :create_note, :push_to_source_branch, :remove_source_branch,
+ :cherry_pick_on_current_merge_request, :revert_on_current_merge_request
+ ]
+
+ expect(described_class).to have_graphql_fields(expected_permissions)
+ end
+end
diff --git a/spec/graphql/types/permission_types/merge_request_type_spec.rb b/spec/graphql/types/permission_types/merge_request_type_spec.rb
new file mode 100644
index 00000000000..6e57122867a
--- /dev/null
+++ b/spec/graphql/types/permission_types/merge_request_type_spec.rb
@@ -0,0 +1,5 @@
+require 'spec_helper'
+
+describe Types::MergeRequestType do
+ it { expect(described_class).to expose_permissions_using(Types::PermissionTypes::MergeRequest) }
+end
diff --git a/spec/graphql/types/permission_types/project_spec.rb b/spec/graphql/types/permission_types/project_spec.rb
new file mode 100644
index 00000000000..89eecef096e
--- /dev/null
+++ b/spec/graphql/types/permission_types/project_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe Types::PermissionTypes::Project do
+ it do
+ expected_permissions = [
+ :change_namespace, :change_visibility_level, :rename_project, :remove_project, :archive_project,
+ :remove_fork_project, :remove_pages, :read_project, :create_merge_request_in,
+ :read_wiki, :read_project_member, :create_issue, :upload_file, :read_cycle_analytics,
+ :download_code, :download_wiki_code, :fork_project, :create_project_snippet,
+ :read_commit_status, :request_access, :create_pipeline, :create_pipeline_schedule,
+ :create_merge_request_from, :create_wiki, :push_code, :create_deployment, :push_to_delete_protected_branch,
+ :admin_wiki, :admin_project, :update_pages, :admin_remote_mirror, :create_label,
+ :update_wiki, :destroy_wiki, :create_pages, :destroy_pages
+ ]
+
+ expect(described_class).to have_graphql_fields(expected_permissions)
+ end
+end
diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb
new file mode 100644
index 00000000000..49606c397b9
--- /dev/null
+++ b/spec/graphql/types/project_type_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe GitlabSchema.types['Project'] do
+ it { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Project) }
+
+ it { expect(described_class.graphql_name).to eq('Project') }
+
+ describe 'nested merge request' do
+ it { expect(described_class).to have_graphql_field(:merge_request) }
+
+ it 'authorizes the merge request' do
+ expect(described_class.fields['mergeRequest'])
+ .to require_graphql_authorizations(:read_merge_request)
+ end
+ end
+
+ it { is_expected.to have_graphql_field(:pipelines) }
+end
diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb
new file mode 100644
index 00000000000..e1df6f9811d
--- /dev/null
+++ b/spec/graphql/types/query_type_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe GitlabSchema.types['Query'] do
+ it 'is called Query' do
+ expect(described_class.graphql_name).to eq('Query')
+ end
+
+ it { is_expected.to have_graphql_fields(:project, :echo) }
+
+ describe 'project field' do
+ subject { described_class.fields['project'] }
+
+ it 'finds projects by full path' do
+ is_expected.to have_graphql_arguments(:full_path)
+ is_expected.to have_graphql_type(Types::ProjectType)
+ is_expected.to have_graphql_resolver(Resolvers::ProjectResolver)
+ end
+
+ it 'authorizes with read_project' do
+ is_expected.to require_graphql_authorizations(:read_project)
+ end
+ end
+end
diff --git a/spec/graphql/types/time_type_spec.rb b/spec/graphql/types/time_type_spec.rb
new file mode 100644
index 00000000000..4196d9d27d4
--- /dev/null
+++ b/spec/graphql/types/time_type_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe GitlabSchema.types['Time'] do
+ let(:iso) { "2018-06-04T15:23:50+02:00" }
+ let(:time) { Time.parse(iso) }
+
+ it { expect(described_class.graphql_name).to eq('Time') }
+
+ it 'coerces Time object into ISO 8601' do
+ expect(described_class.coerce_isolated_result(time)).to eq(iso)
+ end
+
+ it 'coerces an ISO-time into Time object' do
+ expect(described_class.coerce_isolated_input(iso)).to eq(time)
+ end
+end
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 593b2ca1825..14297a1a544 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -157,7 +157,7 @@ describe ApplicationHelper do
let(:noteable_type) { Issue }
it 'returns paths for autocomplete_sources_controller' do
sources = helper.autocomplete_data_sources(project, noteable_type)
- expect(sources.keys).to match_array([:members, :issues, :merge_requests, :labels, :milestones, :commands])
+ expect(sources.keys).to match_array([:members, :issues, :mergeRequests, :labels, :milestones, :commands])
sources.keys.each do |key|
expect(sources[key]).not_to be_nil
end
diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb
index a3e010c3206..1c216b3fe97 100644
--- a/spec/helpers/blob_helper_spec.rb
+++ b/spec/helpers/blob_helper_spec.rb
@@ -65,9 +65,9 @@ describe BlobHelper do
describe "#sanitize_svg_data" do
let(:input_svg_path) { File.join(Rails.root, 'spec', 'fixtures', 'unsanitized.svg') }
- let(:data) { open(input_svg_path).read }
+ let(:data) { File.read(input_svg_path) }
let(:expected_svg_path) { File.join(Rails.root, 'spec', 'fixtures', 'sanitized.svg') }
- let(:expected) { open(expected_svg_path).read }
+ let(:expected) { File.read(expected_svg_path) }
it 'retains essential elements' do
expect(sanitize_svg_data(data)).to eq(expected)
diff --git a/spec/helpers/calendar_helper_spec.rb b/spec/helpers/calendar_helper_spec.rb
new file mode 100644
index 00000000000..828a9d9fea0
--- /dev/null
+++ b/spec/helpers/calendar_helper_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe CalendarHelper do
+ describe '#calendar_url_options' do
+ context 'when signed in' do
+ it "includes the current_user's feed_token" do
+ current_user = create(:user)
+ allow(helper).to receive(:current_user).and_return(current_user)
+ expect(helper.calendar_url_options).to include feed_token: current_user.feed_token
+ end
+ end
+
+ context 'when signed out' do
+ it "does not have a feed_token" do
+ allow(helper).to receive(:current_user).and_return(nil)
+ expect(helper.calendar_url_options[:feed_token]).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/helpers/emails_helper_spec.rb b/spec/helpers/emails_helper_spec.rb
index 2390c1f3e5d..139387e0b24 100644
--- a/spec/helpers/emails_helper_spec.rb
+++ b/spec/helpers/emails_helper_spec.rb
@@ -47,9 +47,7 @@ describe EmailsHelper do
describe '#header_logo' do
context 'there is a brand item with a logo' do
it 'returns the brand header logo' do
- appearance = create :appearance, header_logo: fixture_file_upload(
- Rails.root.join('spec/fixtures/dk.png')
- )
+ appearance = create :appearance, header_logo: fixture_file_upload('spec/fixtures/dk.png')
expect(header_logo).to eq(
%{<img style="height: 50px" src="/uploads/-/system/appearance/header_logo/#{appearance.id}/dk.png" alt="Dk" />}
diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb
index b48c252acd3..115807f954b 100644
--- a/spec/helpers/groups_helper_spec.rb
+++ b/spec/helpers/groups_helper_spec.rb
@@ -4,9 +4,9 @@ describe GroupsHelper do
include ApplicationHelper
describe 'group_icon' do
- avatar_file_path = File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif')
-
it 'returns an url for the avatar' do
+ avatar_file_path = File.join('spec', 'fixtures', 'banana_sample.gif')
+
group = create(:group)
group.avatar = fixture_file_upload(avatar_file_path)
group.save!
@@ -17,9 +17,9 @@ describe GroupsHelper do
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
+ avatar_file_path = File.join('spec', 'fixtures', 'banana_sample.gif')
+
group = create(:group)
group.avatar = fixture_file_upload(avatar_file_path)
group.save!
@@ -206,8 +206,9 @@ describe GroupsHelper do
let(:group) { create(:group, :public) }
let(:user) { create(:user) }
before do
+ group.add_owner(user)
allow(helper).to receive(:current_user) { user }
- allow(helper).to receive(:can?) { true }
+ allow(helper).to receive(:can?) { |*args| Ability.allowed?(*args) }
helper.instance_variable_set(:@group, group)
end
@@ -231,7 +232,10 @@ describe GroupsHelper do
cross_project_features = [:activity, :issues, :labels, :milestones,
:merge_requests]
- expect(helper).to receive(:can?).with(user, :read_cross_project) { false }
+ allow(Ability).to receive(:allowed?).and_call_original
+ cross_project_features.each do |feature|
+ expect(Ability).to receive(:allowed?).with(user, "read_group_#{feature}".to_sym, group) { false }
+ end
expect(helper.group_sidebar_links).not_to include(*cross_project_features)
end
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
index 7b59fde999d..77410e0070c 100644
--- a/spec/helpers/issuables_helper_spec.rb
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -45,22 +45,22 @@ describe IssuablesHelper do
it 'returns "Open" when state is :opened' do
expect(helper.issuables_state_counter_text(:issues, :opened, true))
- .to eq('<span>Open</span> <span class="badge">42</span>')
+ .to eq('<span>Open</span> <span class="badge badge-pill">42</span>')
end
it 'returns "Closed" when state is :closed' do
expect(helper.issuables_state_counter_text(:issues, :closed, true))
- .to eq('<span>Closed</span> <span class="badge">42</span>')
+ .to eq('<span>Closed</span> <span class="badge badge-pill">42</span>')
end
it 'returns "Merged" when state is :merged' do
expect(helper.issuables_state_counter_text(:merge_requests, :merged, true))
- .to eq('<span>Merged</span> <span class="badge">42</span>')
+ .to eq('<span>Merged</span> <span class="badge badge-pill">42</span>')
end
it 'returns "All" when state is :all' do
expect(helper.issuables_state_counter_text(:merge_requests, :all, true))
- .to eq('<span>All</span> <span class="badge">42</span>')
+ .to eq('<span>All</span> <span class="badge badge-pill">42</span>')
end
end
end
@@ -163,6 +163,7 @@ describe IssuablesHelper do
issuableRef: "##{issue.iid}",
markdownPreviewPath: "/#{@project.full_path}/preview_markdown",
markdownDocsPath: '/help/user/markdown',
+ markdownVersion: 11,
issuableTemplates: [],
projectPath: @project.path,
projectNamespace: @project.namespace.path,
diff --git a/spec/helpers/markup_helper_spec.rb b/spec/helpers/markup_helper_spec.rb
index c0dc9293397..597648b064d 100644
--- a/spec/helpers/markup_helper_spec.rb
+++ b/spec/helpers/markup_helper_spec.rb
@@ -11,7 +11,7 @@ describe MarkupHelper do
before do
# Ensure the generated reference links aren't redacted
- project.add_master(user)
+ project.add_maintainer(user)
# Helper expects a @project instance variable
helper.instance_variable_set(:@project, project)
@@ -205,7 +205,9 @@ describe MarkupHelper do
it "uses Wiki pipeline for markdown files" do
allow(@wiki).to receive(:format).and_return(:markdown)
- expect(helper).to receive(:markdown_unsafe).with('wiki content', pipeline: :wiki, project: project, project_wiki: @wiki, page_slug: "nested/page", issuable_state_filter_enabled: true)
+ expect(helper).to receive(:markdown_unsafe).with('wiki content',
+ pipeline: :wiki, project: project, project_wiki: @wiki, page_slug: "nested/page",
+ issuable_state_filter_enabled: true, markdown_engine: :redcarpet)
helper.render_wiki_content(@wiki)
end
@@ -236,19 +238,32 @@ describe MarkupHelper do
expect(helper.markup('foo.rst', content).encoding.name).to eq('UTF-8')
end
- it "delegates to #markdown_unsafe when file name corresponds to Markdown" do
+ it 'delegates to #markdown_unsafe when file name corresponds to Markdown' do
expect(helper).to receive(:gitlab_markdown?).with('foo.md').and_return(true)
expect(helper).to receive(:markdown_unsafe).and_return('NOEL')
expect(helper.markup('foo.md', content)).to eq('NOEL')
end
- it "delegates to #asciidoc_unsafe when file name corresponds to AsciiDoc" do
+ it 'delegates to #asciidoc_unsafe when file name corresponds to AsciiDoc' do
expect(helper).to receive(:asciidoc?).with('foo.adoc').and_return(true)
expect(helper).to receive(:asciidoc_unsafe).and_return('NOEL')
expect(helper.markup('foo.adoc', content)).to eq('NOEL')
end
+
+ it 'uses passed in rendered content' do
+ expect(helper).not_to receive(:gitlab_markdown?)
+ expect(helper).not_to receive(:markdown_unsafe)
+
+ expect(helper.markup('foo.md', content, rendered: '<p>NOEL</p>')).to eq('<p>NOEL</p>')
+ end
+
+ it 'defaults to Redcarpet' do
+ expect(helper).to receive(:markdown_unsafe).with(content, hash_including(markdown_engine: :redcarpet)).and_return('NOEL')
+
+ expect(helper.markup('foo.md', content)).to eq('NOEL')
+ end
end
describe '#first_line_in_markdown' do
@@ -298,7 +313,7 @@ describe MarkupHelper do
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\">" \
+ expected = "<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>"
diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb
index 3008528e60c..885204062fe 100644
--- a/spec/helpers/merge_requests_helper_spec.rb
+++ b/spec/helpers/merge_requests_helper_spec.rb
@@ -54,7 +54,7 @@ describe MergeRequestsHelper do
let(:options) { { force_link: true } }
it 'removes the data-toggle attributes' do
- is_expected.not_to match(/data-toggle="tab"/)
+ is_expected.not_to match(/data-toggle="tabvue"/)
end
end
end
diff --git a/spec/helpers/notes_helper_spec.rb b/spec/helpers/notes_helper_spec.rb
index b992bdb4a5e..21461e46cf4 100644
--- a/spec/helpers/notes_helper_spec.rb
+++ b/spec/helpers/notes_helper_spec.rb
@@ -6,18 +6,18 @@ describe NotesHelper do
let(:owner) { create(:owner) }
let(:group) { create(:group) }
let(:project) { create(:project, namespace: group) }
- let(:master) { create(:user) }
+ let(:maintainer) { create(:user) }
let(:reporter) { create(:user) }
let(:guest) { create(:user) }
let(:owner_note) { create(:note, author: owner, project: project) }
- let(:master_note) { create(:note, author: master, project: project) }
+ let(:maintainer_note) { create(:note, author: maintainer, project: project) }
let(:reporter_note) { create(:note, author: reporter, project: project) }
- let!(:notes) { [owner_note, master_note, reporter_note] }
+ let!(:notes) { [owner_note, maintainer_note, reporter_note] }
before do
group.add_owner(owner)
- project.add_master(master)
+ project.add_maintainer(maintainer)
project.add_reporter(reporter)
project.add_guest(guest)
end
@@ -25,16 +25,16 @@ describe NotesHelper do
describe "#notes_max_access_for_users" do
it 'returns access levels' do
expect(helper.note_max_access_for_user(owner_note)).to eq(Gitlab::Access::OWNER)
- expect(helper.note_max_access_for_user(master_note)).to eq(Gitlab::Access::MASTER)
+ expect(helper.note_max_access_for_user(maintainer_note)).to eq(Gitlab::Access::MAINTAINER)
expect(helper.note_max_access_for_user(reporter_note)).to eq(Gitlab::Access::REPORTER)
end
it 'handles access in different projects' do
second_project = create(:project)
- second_project.add_reporter(master)
- other_note = create(:note, author: master, project: second_project)
+ second_project.add_reporter(maintainer)
+ other_note = create(:note, author: maintainer, project: second_project)
- expect(helper.note_max_access_for_user(master_note)).to eq(Gitlab::Access::MASTER)
+ expect(helper.note_max_access_for_user(maintainer_note)).to eq(Gitlab::Access::MAINTAINER)
expect(helper.note_max_access_for_user(other_note)).to eq(Gitlab::Access::REPORTER)
end
end
diff --git a/spec/helpers/page_layout_helper_spec.rb b/spec/helpers/page_layout_helper_spec.rb
index b77114a8152..cf98eed27f1 100644
--- a/spec/helpers/page_layout_helper_spec.rb
+++ b/spec/helpers/page_layout_helper_spec.rb
@@ -40,23 +40,6 @@ describe PageLayoutHelper do
end
end
- describe 'favicon' do
- it 'defaults to favicon.ico' do
- allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('production'))
- expect(helper.favicon).to eq 'favicon.ico'
- end
-
- it 'has blue favicon for development' do
- allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('development'))
- expect(helper.favicon).to eq 'favicon-blue.ico'
- end
-
- it 'has yellow favicon for canary' do
- stub_env('CANARY', 'true')
- expect(helper.favicon).to eq 'favicon-yellow.ico'
- end
- end
-
describe 'page_image' do
it 'defaults to the GitLab logo' do
expect(helper.page_image).to match_asset_path 'assets/gitlab_logo.png'
diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb
index c9d2ec8a4ae..363ebc88afd 100644
--- a/spec/helpers/preferences_helper_spec.rb
+++ b/spec/helpers/preferences_helper_spec.rb
@@ -33,7 +33,7 @@ describe PreferencesHelper do
it "returns user's theme's css_class" do
stub_user(theme_id: 3)
- expect(helper.user_application_theme).to eq 'ui_light'
+ expect(helper.user_application_theme).to eq 'ui-light'
end
it 'returns the default when id is invalid' do
@@ -41,7 +41,7 @@ describe PreferencesHelper do
allow(Gitlab.config.gitlab).to receive(:default_theme).and_return(1)
- expect(helper.user_application_theme).to eq 'ui_indigo'
+ expect(helper.user_application_theme).to eq 'ui-indigo'
end
end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index f8877b6d1aa..beb6e8ea273 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -5,9 +5,9 @@ describe ProjectsHelper do
describe "#project_status_css_class" do
it "returns appropriate class" do
- expect(project_status_css_class("started")).to eq("active")
- expect(project_status_css_class("failed")).to eq("danger")
- expect(project_status_css_class("finished")).to eq("success")
+ expect(project_status_css_class("started")).to eq("table-active")
+ expect(project_status_css_class("failed")).to eq("table-danger")
+ expect(project_status_css_class("finished")).to eq("table-success")
end
end
@@ -80,6 +80,7 @@ describe ProjectsHelper do
before do
allow(helper).to receive(:current_user).and_return(user)
allow(helper).to receive(:can?).with(user, :read_cross_project) { true }
+ allow(user).to receive(:max_member_access_for_project).and_return(40)
end
it "includes the route" do
@@ -90,6 +91,10 @@ describe ProjectsHelper do
expect(helper.project_list_cache_key(project)).to include(project.cache_key)
end
+ it "includes the last activity date" do
+ expect(helper.project_list_cache_key(project)).to include(project.last_activity_date)
+ end
+
it "includes the controller name" do
expect(helper.controller).to receive(:controller_name).and_return("testcontroller")
@@ -121,6 +126,10 @@ describe ProjectsHelper do
expect(helper.project_list_cache_key(project)).to include("pipeline-status/#{project.commit.sha}-success")
end
+
+ it "includes the user max member access" do
+ expect(helper.project_list_cache_key(project)).to include('access:40')
+ end
end
describe '#load_pipeline_status' do
@@ -244,7 +253,7 @@ describe ProjectsHelper do
describe '#link_to_member' do
let(:group) { build_stubbed(:group) }
let(:project) { build_stubbed(:project, group: group) }
- let(:user) { build_stubbed(:user) }
+ let(:user) { build_stubbed(:user, name: '<h1>Administrator</h1>') }
describe 'using the default options' do
it 'returns an HTML link to the user' do
@@ -252,6 +261,13 @@ describe ProjectsHelper do
expect(link).to match(%r{/#{user.username}})
end
+
+ it 'HTML escapes the name of the user' do
+ link = helper.link_to_member(project, user)
+
+ expect(link).to include(ERB::Util.html_escape(user.name))
+ expect(link).not_to include(user.name)
+ end
end
end
@@ -276,7 +292,11 @@ describe ProjectsHelper do
describe '#sanitizerepo_repo_path' do
let(:project) { create(:project, :repository) }
- let(:storage_path) { Gitlab.config.repositories.storages.default.legacy_disk_path }
+ let(:storage_path) do
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ Gitlab.config.repositories.storages.default.legacy_disk_path
+ end
+ end
before do
allow(Settings.shared).to receive(:[]).with('path').and_return('/base/repo/export/path')
@@ -435,4 +455,46 @@ describe ProjectsHelper do
expect(helper.send(:git_user_name)).to eq('John \"A\" Doe53')
end
end
+
+ describe 'show_xcode_link' do
+ let!(:project) { create(:project) }
+ let(:mac_ua) { 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36' }
+ let(:ios_ua) { 'Mozilla/5.0 (iPad; CPU OS 5_1_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9B206 Safari/7534.48.3' }
+
+ context 'when the repository is xcode compatible' do
+ before do
+ allow(project.repository).to receive(:xcode_project?).and_return(true)
+ end
+
+ it 'returns false if the visitor is not using macos' do
+ allow(helper).to receive(:browser).and_return(Browser.new(ios_ua))
+
+ expect(helper.show_xcode_link?(project)).to eq(false)
+ end
+
+ it 'returns true if the visitor is using macos' do
+ allow(helper).to receive(:browser).and_return(Browser.new(mac_ua))
+
+ expect(helper.show_xcode_link?(project)).to eq(true)
+ end
+ end
+
+ context 'when the repository is not xcode compatible' do
+ before do
+ allow(project.repository).to receive(:xcode_project?).and_return(false)
+ end
+
+ it 'returns false if the visitor is not using macos' do
+ allow(helper).to receive(:browser).and_return(Browser.new(ios_ua))
+
+ expect(helper.show_xcode_link?(project)).to eq(false)
+ end
+
+ it 'returns false if the visitor is using macos' do
+ allow(helper).to receive(:browser).and_return(Browser.new(mac_ua))
+
+ expect(helper.show_xcode_link?(project)).to eq(false)
+ end
+ end
+ end
end
diff --git a/spec/helpers/rss_helper_spec.rb b/spec/helpers/rss_helper_spec.rb
index 269e1057e8d..a7f9bdf07e4 100644
--- a/spec/helpers/rss_helper_spec.rb
+++ b/spec/helpers/rss_helper_spec.rb
@@ -3,17 +3,17 @@ require 'spec_helper'
describe RssHelper do
describe '#rss_url_options' do
context 'when signed in' do
- it "includes the current_user's rss_token" do
+ it "includes the current_user's feed_token" do
current_user = create(:user)
allow(helper).to receive(:current_user).and_return(current_user)
- expect(helper.rss_url_options).to include rss_token: current_user.rss_token
+ expect(helper.rss_url_options).to include feed_token: current_user.feed_token
end
end
context 'when signed out' do
- it "does not have an rss_token" do
+ it "does not have a feed_token" do
allow(helper).to receive(:current_user).and_return(nil)
- expect(helper.rss_url_options[:rss_token]).to be_nil
+ expect(helper.rss_url_options[:feed_token]).to be_nil
end
end
end
diff --git a/spec/helpers/storage_helper_spec.rb b/spec/helpers/storage_helper_spec.rb
index 4627a1e1872..c580b78c908 100644
--- a/spec/helpers/storage_helper_spec.rb
+++ b/spec/helpers/storage_helper_spec.rb
@@ -1,21 +1,25 @@
-require 'spec_helper'
+require "spec_helper"
describe StorageHelper do
- describe '#storage_counter' do
- it 'formats bytes to one decimal place' do
- expect(helper.storage_counter(1.23.megabytes)).to eq '1.2 MB'
+ describe "#storage_counter" do
+ it "formats bytes to one decimal place" do
+ expect(helper.storage_counter(1.23.megabytes)).to eq("1.2 MB")
end
- it 'does not add decimals for sizes < 1 MB' do
- expect(helper.storage_counter(23.5.kilobytes)).to eq '24 KB'
+ it "does not add decimals for sizes < 1 MB" do
+ expect(helper.storage_counter(23.5.kilobytes)).to eq("24 KB")
end
- it 'does not add decimals for zeroes' do
- expect(helper.storage_counter(2.megabytes)).to eq '2 MB'
+ it "does not add decimals for zeroes" do
+ expect(helper.storage_counter(2.megabytes)).to eq("2 MB")
end
- it 'uses commas as thousands separator' do
- expect(helper.storage_counter(100_000_000_000_000_000)).to eq '90,949.5 TB'
+ it "uses commas as thousands separator" do
+ if Gitlab.rails5?
+ expect(helper.storage_counter(100_000_000_000_000_000_000_000)).to eq("86,736.2 EB")
+ else
+ expect(helper.storage_counter(100_000_000_000_000_000)).to eq("90,949.5 TB")
+ end
end
end
end
diff --git a/spec/helpers/time_helper_spec.rb b/spec/helpers/time_helper_spec.rb
index 21f35585367..0b371d69ecf 100644
--- a/spec/helpers/time_helper_spec.rb
+++ b/spec/helpers/time_helper_spec.rb
@@ -4,10 +4,12 @@ describe TimeHelper do
describe "#time_interval_in_words" do
it "returns minutes and seconds" do
intervals_in_words = {
- 100 => "1 minute 40 seconds",
- 100.32 => "1 minute 40 seconds",
- 121 => "2 minutes 1 second",
- 3721 => "62 minutes 1 second",
+ 60 => "1 minute",
+ 100 => "1 minute and 40 seconds",
+ 100.32 => "1 minute and 40 seconds",
+ 120 => "2 minutes",
+ 121 => "2 minutes and 1 second",
+ 3721 => "62 minutes and 1 second",
0 => "0 seconds"
}
diff --git a/spec/initializers/6_validations_spec.rb b/spec/initializers/6_validations_spec.rb
index 8d9dc092547..f96e5a2133f 100644
--- a/spec/initializers/6_validations_spec.rb
+++ b/spec/initializers/6_validations_spec.rb
@@ -44,49 +44,6 @@ describe '6_validations' do
end
end
- describe 'validate_storages_paths' do
- context 'with correct settings' do
- before do
- mock_storages('foo' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/paths/a/b/c'), 'bar' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/paths/a/b/d'))
- end
-
- it 'passes through' do
- expect { validate_storages_paths }.not_to raise_error
- end
- end
-
- context 'with nested storage paths' do
- before do
- mock_storages('foo' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/paths/a/b/c'), 'bar' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/paths/a/b/c/d'))
- end
-
- it 'throws an error' do
- expect { validate_storages_paths }.to raise_error('bar is a nested path of foo. Nested paths are not supported for repository storages. Please fix this in your gitlab.yml before starting GitLab.')
- end
- end
-
- context 'with similar but un-nested storage paths' do
- before do
- mock_storages('foo' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/paths/a/b/c'), 'bar' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/paths/a/b/c2'))
- end
-
- it 'passes through' do
- expect { validate_storages_paths }.not_to raise_error
- end
- end
-
- describe 'inaccessible storage' do
- before do
- mock_storages('foo' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/a/path/that/does/not/exist'))
- end
-
- it 'passes through with a warning' do
- expect(Rails.logger).to receive(:error)
- expect { validate_storages_paths }.not_to raise_error
- end
- end
- end
-
def mock_storages(storages)
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
end
diff --git a/spec/initializers/artifacts_direct_upload_support_spec.rb b/spec/initializers/artifacts_direct_upload_support_spec.rb
deleted file mode 100644
index bfb71da3388..00000000000
--- a/spec/initializers/artifacts_direct_upload_support_spec.rb
+++ /dev/null
@@ -1,71 +0,0 @@
-require 'spec_helper'
-
-describe 'Artifacts direct upload support' do
- subject do
- load Rails.root.join('config/initializers/artifacts_direct_upload_support.rb')
- end
-
- let(:connection) do
- { provider: provider }
- end
-
- before do
- stub_artifacts_setting(
- object_store: {
- enabled: enabled,
- direct_upload: direct_upload,
- connection: connection
- })
- end
-
- context 'when object storage is enabled' do
- let(:enabled) { true }
-
- context 'when direct upload is enabled' do
- let(:direct_upload) { true }
-
- context 'when provider is Google' do
- let(:provider) { 'Google' }
-
- it 'succeeds' do
- expect { subject }.not_to raise_error
- end
- end
-
- context 'when connection is empty' do
- let(:connection) { nil }
-
- it 'raises an error' do
- expect { subject }.to raise_error /object storage provider when 'direct_upload' of artifacts is used/
- end
- end
-
- context 'when other provider is used' do
- let(:provider) { 'AWS' }
-
- it 'raises an error' do
- expect { subject }.to raise_error /object storage provider when 'direct_upload' of artifacts is used/
- end
- end
- end
-
- context 'when direct upload is disabled' do
- let(:direct_upload) { false }
- let(:provider) { 'AWS' }
-
- it 'succeeds' do
- expect { subject }.not_to raise_error
- end
- end
- end
-
- context 'when object storage is disabled' do
- let(:enabled) { false }
- let(:direct_upload) { false }
- let(:provider) { 'AWS' }
-
- it 'succeeds' do
- expect { subject }.not_to raise_error
- end
- end
-end
diff --git a/spec/initializers/direct_upload_support_spec.rb b/spec/initializers/direct_upload_support_spec.rb
new file mode 100644
index 00000000000..e51d404e030
--- /dev/null
+++ b/spec/initializers/direct_upload_support_spec.rb
@@ -0,0 +1,90 @@
+require 'spec_helper'
+
+describe 'Direct upload support' do
+ subject do
+ load Rails.root.join('config/initializers/direct_upload_support.rb')
+ end
+
+ where(:config_name) do
+ %w(lfs artifacts uploads)
+ end
+
+ with_them do
+ let(:connection) do
+ { provider: provider }
+ end
+
+ let(:object_store) do
+ {
+ enabled: enabled,
+ direct_upload: direct_upload,
+ connection: connection
+ }
+ end
+
+ before do
+ allow(Gitlab.config).to receive_messages(to_settings(config_name => {
+ object_store: object_store
+ }))
+ end
+
+ context 'when object storage is enabled' do
+ let(:enabled) { true }
+
+ context 'when direct upload is enabled' do
+ let(:direct_upload) { true }
+
+ context 'when provider is AWS' do
+ let(:provider) { 'AWS' }
+
+ it 'succeeds' do
+ expect { subject }.not_to raise_error
+ end
+ end
+
+ context 'when provider is Google' do
+ let(:provider) { 'Google' }
+
+ it 'succeeds' do
+ expect { subject }.not_to raise_error
+ end
+ end
+
+ context 'when connection is empty' do
+ let(:connection) { nil }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error /are supported as a object storage provider when 'direct_upload' is used/
+ end
+ end
+
+ context 'when other provider is used' do
+ let(:provider) { 'Rackspace' }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error /are supported as a object storage provider when 'direct_upload' is used/
+ end
+ end
+ end
+
+ context 'when direct upload is disabled' do
+ let(:direct_upload) { false }
+ let(:provider) { 'AWS' }
+
+ it 'succeeds' do
+ expect { subject }.not_to raise_error
+ end
+ end
+ end
+
+ context 'when object storage is disabled' do
+ let(:enabled) { false }
+ let(:direct_upload) { false }
+ let(:provider) { 'Rackspace' }
+
+ it 'succeeds' do
+ expect { subject }.not_to raise_error
+ end
+ end
+ end
+end
diff --git a/spec/initializers/fog_google_https_private_urls_spec.rb b/spec/initializers/fog_google_https_private_urls_spec.rb
index de3c157ab7b..08346b71fee 100644
--- a/spec/initializers/fog_google_https_private_urls_spec.rb
+++ b/spec/initializers/fog_google_https_private_urls_spec.rb
@@ -1,13 +1,13 @@
require 'spec_helper'
-describe 'Fog::Storage::GoogleXML::File' do
+describe 'Fog::Storage::GoogleXML::File', :fog_requests do
let(:storage) do
Fog.mock!
- Fog::Storage.new({
- google_storage_access_key_id: "asdf",
- google_storage_secret_access_key: "asdf",
- provider: "Google"
- })
+ Fog::Storage.new(
+ google_storage_access_key_id: "asdf",
+ google_storage_secret_access_key: "asdf",
+ provider: "Google"
+ )
end
let(:file) do
diff --git a/spec/initializers/grape_route_helpers_fix_spec.rb b/spec/initializers/grape_route_helpers_fix_spec.rb
deleted file mode 100644
index 2cf5924128f..00000000000
--- a/spec/initializers/grape_route_helpers_fix_spec.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-require 'spec_helper'
-require_relative '../../config/initializers/grape_route_helpers_fix'
-
-describe 'route shadowing' do
- include GrapeRouteHelpers::NamedRouteMatcher
-
- it 'does not occur' do
- path = api_v4_projects_merge_requests_path(id: 1)
- expect(path).to eq('/api/v4/projects/1/merge_requests')
-
- path = api_v4_projects_merge_requests_path(id: 1, merge_request_iid: 3)
- expect(path).to eq('/api/v4/projects/1/merge_requests/3')
- end
-end
diff --git a/spec/initializers/secret_token_spec.rb b/spec/initializers/secret_token_spec.rb
index d56e14e0e0b..c3dfd7bedbe 100644
--- a/spec/initializers/secret_token_spec.rb
+++ b/spec/initializers/secret_token_spec.rb
@@ -1,5 +1,5 @@
require 'spec_helper'
-require_relative '../../config/initializers/secret_token'
+require_relative '../../config/initializers/01_secret_token'
describe 'create_tokens' do
include StubENV
diff --git a/spec/javascripts/.eslintrc b/spec/javascripts/.eslintrc
deleted file mode 100644
index 9eb0e732572..00000000000
--- a/spec/javascripts/.eslintrc
+++ /dev/null
@@ -1,33 +0,0 @@
-{
- "env": {
- "jasmine": true
- },
- "extends": "plugin:jasmine/recommended",
- "globals": {
- "appendLoadFixtures": false,
- "appendLoadStyleFixtures": false,
- "appendSetFixtures": false,
- "appendSetStyleFixtures": false,
- "getJSONFixture": false,
- "loadFixtures": false,
- "loadJSONFixtures": false,
- "loadStyleFixtures": false,
- "preloadFixtures": false,
- "preloadStyleFixtures": false,
- "readFixtures": false,
- "sandbox": false,
- "setFixtures": false,
- "setStyleFixtures": false,
- "spyOnDependency": false,
- "spyOnEvent": false,
- "ClassSpecHelper": false
- },
- "plugins": ["jasmine"],
- "rules": {
- "func-names": 0,
- "jasmine/no-suite-dupes": [1, "branch"],
- "jasmine/no-spec-dupes": [1, "branch"],
- "no-console": 0,
- "prefer-arrow-callback": 0
- }
-}
diff --git a/spec/javascripts/.eslintrc.yml b/spec/javascripts/.eslintrc.yml
new file mode 100644
index 00000000000..78e2f3b521f
--- /dev/null
+++ b/spec/javascripts/.eslintrc.yml
@@ -0,0 +1,38 @@
+---
+env:
+ jasmine: true
+extends: plugin:jasmine/recommended
+globals:
+ appendLoadFixtures: false
+ appendLoadStyleFixtures: false
+ appendSetFixtures: false
+ appendSetStyleFixtures: false
+ getJSONFixture: false
+ loadFixtures: false
+ loadJSONFixtures: false
+ loadStyleFixtures: false
+ preloadFixtures: false
+ preloadStyleFixtures: false
+ readFixtures: false
+ sandbox: false
+ setFixtures: false
+ setStyleFixtures: false
+ spyOnDependency: false
+ spyOnEvent: false
+ ClassSpecHelper: false
+plugins:
+ - jasmine
+rules:
+ func-names: off
+ jasmine/no-suite-dupes:
+ - warn
+ - branch
+ jasmine/no-spec-dupes:
+ - warn
+ - branch
+ no-console: off
+ prefer-arrow-callback: off
+ import/no-unresolved:
+ - error
+ - ignore:
+ - 'fixtures/blob'
diff --git a/spec/javascripts/activities_spec.js b/spec/javascripts/activities_spec.js
index 5dbdcd24296..068b8eb65bc 100644
--- a/spec/javascripts/activities_spec.js
+++ b/spec/javascripts/activities_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable no-unused-expressions, no-prototype-builtins, no-new, no-shadow, max-len */
+/* eslint-disable no-unused-expressions, no-prototype-builtins, no-new, no-shadow */
import $ from 'jquery';
import 'vendor/jquery.endless-scroll';
diff --git a/spec/javascripts/api_spec.js b/spec/javascripts/api_spec.js
index 3d7ccf432be..54cb6d84109 100644
--- a/spec/javascripts/api_spec.js
+++ b/spec/javascripts/api_spec.js
@@ -242,7 +242,7 @@ describe('Api', () => {
},
]);
- Api.groupProjects(groupId, query, response => {
+ Api.groupProjects(groupId, query, {}, response => {
expect(response.length).toBe(1);
expect(response[0].name).toBe('test');
done();
@@ -341,4 +341,50 @@ describe('Api', () => {
.catch(done.fail);
});
});
+
+ describe('commitPipelines', () => {
+ it('fetches pipelines for a given commit', done => {
+ const projectId = 'example/foobar';
+ const commitSha = 'abc123def';
+ const expectedUrl = `${dummyUrlRoot}/${projectId}/commit/${commitSha}/pipelines`;
+ mock.onGet(expectedUrl).reply(200, [
+ {
+ name: 'test',
+ },
+ ]);
+
+ Api.commitPipelines(projectId, commitSha)
+ .then(({ data }) => {
+ expect(data.length).toBe(1);
+ expect(data[0].name).toBe('test');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('createBranch', () => {
+ it('creates new branch', done => {
+ const ref = 'master';
+ const branch = 'new-branch-name';
+ const dummyProjectPath = 'gitlab-org/gitlab-ce';
+ const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${encodeURIComponent(
+ dummyProjectPath,
+ )}/repository/branches`;
+
+ spyOn(axios, 'post').and.callThrough();
+
+ mock.onPost(expectedUrl).replyOnce(200, {
+ name: branch,
+ });
+
+ Api.createBranch(dummyProjectPath, { ref, branch })
+ .then(({ data }) => {
+ expect(data.name).toBe(branch);
+ expect(axios.post).toHaveBeenCalledWith(expectedUrl, { ref, branch });
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
});
diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js
index e81055bc08f..ada26b37f4a 100644
--- a/spec/javascripts/awards_handler_spec.js
+++ b/spec/javascripts/awards_handler_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, new-parens, no-unused-vars, quotes, jasmine/no-spec-dupes, prefer-template, max-len */
+/* eslint-disable no-var, one-var, one-var-declaration-per-line, no-unused-expressions, no-unused-vars, prefer-template, max-len */
import $ from 'jquery';
import Cookies from 'js-cookie';
@@ -21,20 +21,21 @@ import '~/lib/utils/common_utils';
return setTimeout(function() {
assertFn();
return done();
- // Maybe jasmine.clock here?
+ // Maybe jasmine.clock here?
}, 333);
};
describe('AwardsHandler', function() {
- preloadFixtures('merge_requests/diff_comment.html.raw');
+ preloadFixtures('snippets/show.html.raw');
beforeEach(function(done) {
- loadFixtures('merge_requests/diff_comment.html.raw');
- $('body').attr('data-page', 'projects:merge_requests:show');
- loadAwardsHandler(true).then((obj) => {
- awardsHandler = obj;
- spyOn(awardsHandler, 'postEmoji').and.callFake((button, url, emoji, cb) => cb());
- done();
- }).catch(fail);
+ loadFixtures('snippets/show.html.raw');
+ loadAwardsHandler(true)
+ .then(obj => {
+ awardsHandler = obj;
+ spyOn(awardsHandler, 'postEmoji').and.callFake((button, url, emoji, cb) => cb());
+ done();
+ })
+ .catch(fail);
let isEmojiMenuBuilt = false;
openAndWaitForEmojiMenu = function() {
@@ -42,7 +43,9 @@ import '~/lib/utils/common_utils';
if (isEmojiMenuBuilt) {
resolve();
} else {
- $('.js-add-award').eq(0).click();
+ $('.js-add-award')
+ .eq(0)
+ .click();
const $menu = $('.emoji-menu');
$menu.one('build-emoji-menu-finish', () => {
isEmojiMenuBuilt = true;
@@ -63,7 +66,9 @@ import '~/lib/utils/common_utils';
});
describe('::showEmojiMenu', function() {
it('should show emoji menu when Add emoji button clicked', function(done) {
- $('.js-add-award').eq(0).click();
+ $('.js-add-award')
+ .eq(0)
+ .click();
return lazyAssert(done, function() {
var $emojiMenu;
$emojiMenu = $('.emoji-menu');
@@ -81,7 +86,9 @@ import '~/lib/utils/common_utils';
});
});
it('should remove emoji menu when body is clicked', function(done) {
- $('.js-add-award').eq(0).click();
+ $('.js-add-award')
+ .eq(0)
+ .click();
return lazyAssert(done, function() {
var $emojiMenu;
$emojiMenu = $('.emoji-menu');
@@ -92,7 +99,9 @@ import '~/lib/utils/common_utils';
});
});
it('should not remove emoji menu when search is clicked', function(done) {
- $('.js-add-award').eq(0).click();
+ $('.js-add-award')
+ .eq(0)
+ .click();
return lazyAssert(done, function() {
var $emojiMenu;
$emojiMenu = $('.emoji-menu');
@@ -103,6 +112,7 @@ import '~/lib/utils/common_utils';
});
});
});
+
describe('::addAwardToEmojiBar', function() {
it('should add emoji to votes block', function() {
var $emojiButton, $votesBlock;
@@ -139,7 +149,9 @@ import '~/lib/utils/common_utils';
$thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent();
$thumbsUpEmoji.attr('data-title', 'sam');
awardsHandler.userAuthored($thumbsUpEmoji);
- return expect($thumbsUpEmoji.data("originalTitle")).toBe("You cannot vote on your own issue, MR and note");
+ return expect($thumbsUpEmoji.data('originalTitle')).toBe(
+ 'You cannot vote on your own issue, MR and note',
+ );
});
it('should restore tooltip back to initial vote list', function() {
var $thumbsUpEmoji, $votesBlock;
@@ -150,12 +162,14 @@ import '~/lib/utils/common_utils';
awardsHandler.userAuthored($thumbsUpEmoji);
jasmine.clock().tick(2801);
jasmine.clock().uninstall();
- return expect($thumbsUpEmoji.data("originalTitle")).toBe("sam");
+ return expect($thumbsUpEmoji.data('originalTitle')).toBe('sam');
});
});
describe('::getAwardUrl', function() {
return it('returns the url for request', function() {
- return expect(awardsHandler.getAwardUrl()).toBe('http://test.host/frontend-fixtures/merge-requests-project/merge_requests/1/toggle_award_emoji');
+ return expect(awardsHandler.getAwardUrl()).toBe(
+ 'http://test.host/snippets/1/toggle_award_emoji',
+ );
});
});
describe('::addAward and ::checkMutuality', function() {
@@ -195,7 +209,7 @@ import '~/lib/utils/common_utils';
$thumbsUpEmoji.attr('data-title', 'sam, jerry, max, and andy');
awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
$thumbsUpEmoji.tooltip();
- return expect($thumbsUpEmoji.data("originalTitle")).toBe('You, sam, jerry, max, and andy');
+ return expect($thumbsUpEmoji.data('originalTitle')).toBe('You, sam, jerry, max, and andy');
});
return it('handles the special case where "You" is not cleanly comma seperated', function() {
var $thumbsUpEmoji, $votesBlock, awardUrl;
@@ -205,7 +219,7 @@ import '~/lib/utils/common_utils';
$thumbsUpEmoji.attr('data-title', 'sam');
awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
$thumbsUpEmoji.tooltip();
- return expect($thumbsUpEmoji.data("originalTitle")).toBe('You and sam');
+ return expect($thumbsUpEmoji.data('originalTitle')).toBe('You and sam');
});
});
describe('::removeYouToUserList', function() {
@@ -218,7 +232,7 @@ import '~/lib/utils/common_utils';
$thumbsUpEmoji.addClass('active');
awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
$thumbsUpEmoji.tooltip();
- return expect($thumbsUpEmoji.data("originalTitle")).toBe('sam, jerry, max, and andy');
+ return expect($thumbsUpEmoji.data('originalTitle')).toBe('sam, jerry, max, and andy');
});
return it('handles the special case where "You" is not cleanly comma seperated', function() {
var $thumbsUpEmoji, $votesBlock, awardUrl;
@@ -229,7 +243,7 @@ import '~/lib/utils/common_utils';
$thumbsUpEmoji.addClass('active');
awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
$thumbsUpEmoji.tooltip();
- return expect($thumbsUpEmoji.data("originalTitle")).toBe('sam');
+ return expect($thumbsUpEmoji.data('originalTitle')).toBe('sam');
});
});
describe('::searchEmojis', () => {
@@ -245,7 +259,7 @@ import '~/lib/utils/common_utils';
expect($('.js-emoji-menu-search').val()).toBe('ali');
})
.then(done)
- .catch((err) => {
+ .catch(err => {
done.fail(`Failed to open and build emoji menu: ${err.message}`);
});
});
@@ -263,7 +277,7 @@ import '~/lib/utils/common_utils';
expect($('.js-emoji-menu-search').val()).toBe('');
})
.then(done)
- .catch((err) => {
+ .catch(err => {
done.fail(`Failed to open and build emoji menu: ${err.message}`);
});
});
@@ -272,37 +286,40 @@ import '~/lib/utils/common_utils';
describe('emoji menu', function() {
const emojiSelector = '[data-name="sunglasses"]';
const openEmojiMenuAndAddEmoji = function() {
- return openAndWaitForEmojiMenu()
- .then(() => {
- const $menu = $('.emoji-menu');
- const $block = $('.js-awards-block');
- const $emoji = $menu.find('.emoji-menu-list:not(.frequent-emojis) ' + emojiSelector);
+ return openAndWaitForEmojiMenu().then(() => {
+ const $menu = $('.emoji-menu');
+ const $block = $('.js-awards-block');
+ const $emoji = $menu.find('.emoji-menu-list:not(.frequent-emojis) ' + emojiSelector);
- expect($emoji.length).toBe(1);
- expect($block.find(emojiSelector).length).toBe(0);
- $emoji.click();
- expect($menu.hasClass('.is-visible')).toBe(false);
- expect($block.find(emojiSelector).length).toBe(1);
- });
+ expect($emoji.length).toBe(1);
+ expect($block.find(emojiSelector).length).toBe(0);
+ $emoji.click();
+ expect($menu.hasClass('.is-visible')).toBe(false);
+ expect($block.find(emojiSelector).length).toBe(1);
+ });
};
it('should add selected emoji to awards block', function(done) {
return openEmojiMenuAndAddEmoji()
.then(done)
- .catch((err) => {
+ .catch(err => {
done.fail(`Failed to open and build emoji menu: ${err.message}`);
});
});
it('should remove already selected emoji', function(done) {
return openEmojiMenuAndAddEmoji()
.then(() => {
- $('.js-add-award').eq(0).click();
+ $('.js-add-award')
+ .eq(0)
+ .click();
const $block = $('.js-awards-block');
- const $emoji = $('.emoji-menu').find(`.emoji-menu-list:not(.frequent-emojis) ${emojiSelector}`);
+ const $emoji = $('.emoji-menu').find(
+ `.emoji-menu-list:not(.frequent-emojis) ${emojiSelector}`,
+ );
$emoji.click();
expect($block.find(emojiSelector).length).toBe(0);
})
.then(done)
- .catch((err) => {
+ .catch(err => {
done.fail(`Failed to open and build emoji menu: ${err.message}`);
});
});
@@ -318,12 +335,12 @@ import '~/lib/utils/common_utils';
return openAndWaitForEmojiMenu()
.then(() => {
const emojiMenu = document.querySelector('.emoji-menu');
- Array.prototype.forEach.call(emojiMenu.querySelectorAll('.emoji-menu-title'), (title) => {
+ Array.prototype.forEach.call(emojiMenu.querySelectorAll('.emoji-menu-title'), title => {
expect(title.textContent.trim().toLowerCase()).not.toBe('frequently used');
});
})
.then(done)
- .catch((err) => {
+ .catch(err => {
done.fail(`Failed to open and build emoji menu: ${err.message}`);
});
});
@@ -334,14 +351,15 @@ import '~/lib/utils/common_utils';
return openAndWaitForEmojiMenu()
.then(() => {
const emojiMenu = document.querySelector('.emoji-menu');
- const hasFrequentlyUsedHeading = Array.prototype.some.call(emojiMenu.querySelectorAll('.emoji-menu-title'), title =>
- title.textContent.trim().toLowerCase() === 'frequently used'
+ const hasFrequentlyUsedHeading = Array.prototype.some.call(
+ emojiMenu.querySelectorAll('.emoji-menu-title'),
+ title => title.textContent.trim().toLowerCase() === 'frequently used',
);
expect(hasFrequentlyUsedHeading).toBe(true);
})
.then(done)
- .catch((err) => {
+ .catch(err => {
done.fail(`Failed to open and build emoji menu: ${err.message}`);
});
});
@@ -361,4 +379,4 @@ import '~/lib/utils/common_utils';
});
});
});
-}).call(window);
+}.call(window));
diff --git a/spec/javascripts/badges/components/badge_list_spec.js b/spec/javascripts/badges/components/badge_list_spec.js
index 9439c578973..02e59ae0843 100644
--- a/spec/javascripts/badges/components/badge_list_spec.js
+++ b/spec/javascripts/badges/components/badge_list_spec.js
@@ -33,7 +33,7 @@ describe('BadgeList component', () => {
});
it('renders a header with the badge count', () => {
- const header = vm.$el.querySelector('.panel-heading');
+ const header = vm.$el.querySelector('.card-header');
expect(header).toHaveText(new RegExp(`Your badges\\s+${numberOfDummyBadges}`));
});
diff --git a/spec/javascripts/badges/components/badge_settings_spec.js b/spec/javascripts/badges/components/badge_settings_spec.js
index 3db02982ad4..59367c85125 100644
--- a/spec/javascripts/badges/components/badge_settings_spec.js
+++ b/spec/javascripts/badges/components/badge_settings_spec.js
@@ -60,7 +60,7 @@ describe('BadgeSettings component', () => {
});
it('displays badge list', () => {
- const badgeListElement = vm.$el.querySelector('.panel');
+ const badgeListElement = vm.$el.querySelector('.card');
expect(badgeListElement).not.toBe(null);
expect(badgeListElement).toBeVisible();
expect(badgeListElement).toContainText('Your badges');
@@ -87,7 +87,7 @@ describe('BadgeSettings component', () => {
});
it('displays no badge list', () => {
- const badgeListElement = vm.$el.querySelector('.panel');
+ const badgeListElement = vm.$el.querySelector('.card');
expect(badgeListElement).toBeHidden();
});
});
diff --git a/spec/javascripts/behaviors/copy_as_gfm_spec.js b/spec/javascripts/behaviors/copy_as_gfm_spec.js
index efbe09a10a2..c2db81c6ce4 100644
--- a/spec/javascripts/behaviors/copy_as_gfm_spec.js
+++ b/spec/javascripts/behaviors/copy_as_gfm_spec.js
@@ -44,4 +44,59 @@ describe('CopyAsGFM', () => {
callPasteGFM();
});
});
+
+ describe('CopyAsGFM.copyGFM', () => {
+ // Stub getSelection to return a purpose-built object.
+ const stubSelection = (html, parentNode) => ({
+ getRangeAt: () => ({
+ commonAncestorContainer: { tagName: parentNode },
+ cloneContents: () => {
+ const fragment = document.createDocumentFragment();
+ const node = document.createElement('div');
+ node.innerHTML = html;
+ Array.from(node.childNodes).forEach((item) => fragment.appendChild(item));
+ return fragment;
+ },
+ }),
+ rangeCount: 1,
+ });
+
+ const clipboardData = {
+ setData() {},
+ };
+
+ const simulateCopy = () => {
+ const e = {
+ originalEvent: {
+ clipboardData,
+ },
+ preventDefault() {},
+ stopPropagation() {},
+ };
+ CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformGFMSelection);
+ return clipboardData;
+ };
+
+ beforeEach(() => spyOn(clipboardData, 'setData'));
+
+ describe('list handling', () => {
+ it('uses correct gfm for unordered lists', () => {
+ const selection = stubSelection('<li>List Item1</li><li>List Item2</li>\n', 'UL');
+ spyOn(window, 'getSelection').and.returnValue(selection);
+ simulateCopy();
+
+ const expectedGFM = '- List Item1\n- List Item2';
+ expect(clipboardData.setData).toHaveBeenCalledWith('text/x-gfm', expectedGFM);
+ });
+
+ it('uses correct gfm for ordered lists', () => {
+ const selection = stubSelection('<li>List Item1</li><li>List Item2</li>\n', 'OL');
+ spyOn(window, 'getSelection').and.returnValue(selection);
+ simulateCopy();
+
+ const expectedGFM = '1. List Item1\n1. List Item2';
+ expect(clipboardData.setData).toHaveBeenCalledWith('text/x-gfm', expectedGFM);
+ });
+ });
+ });
});
diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js
index d03836d10f9..d8aa5c636da 100644
--- a/spec/javascripts/behaviors/quick_submit_spec.js
+++ b/spec/javascripts/behaviors/quick_submit_spec.js
@@ -4,12 +4,11 @@ import '~/behaviors/quick_submit';
describe('Quick Submit behavior', function () {
const keydownEvent = (options = { keyCode: 13, metaKey: true }) => $.Event('keydown', options);
- preloadFixtures('merge_requests/merge_request_with_task_list.html.raw');
+ preloadFixtures('snippets/show.html.raw');
beforeEach(() => {
- loadFixtures('merge_requests/merge_request_with_task_list.html.raw');
- $('body').attr('data-page', 'projects:merge_requests:show');
- $('form').submit((e) => {
+ loadFixtures('snippets/show.html.raw');
+ $('form').submit(e => {
// Prevent a form submit from moving us off the testing page
e.preventDefault();
});
@@ -26,24 +25,30 @@ describe('Quick Submit behavior', function () {
});
it('does not respond to other keyCodes', () => {
- this.textarea.trigger(keydownEvent({
- keyCode: 32,
- }));
+ this.textarea.trigger(
+ keydownEvent({
+ keyCode: 32,
+ }),
+ );
expect(this.spies.submit).not.toHaveBeenTriggered();
});
it('does not respond to Enter alone', () => {
- this.textarea.trigger(keydownEvent({
- ctrlKey: false,
- metaKey: false,
- }));
+ this.textarea.trigger(
+ keydownEvent({
+ ctrlKey: false,
+ metaKey: false,
+ }),
+ );
expect(this.spies.submit).not.toHaveBeenTriggered();
});
it('does not respond to repeated events', () => {
- this.textarea.trigger(keydownEvent({
- repeat: true,
- }));
+ this.textarea.trigger(
+ keydownEvent({
+ repeat: true,
+ }),
+ );
expect(this.spies.submit).not.toHaveBeenTriggered();
});
@@ -83,15 +88,21 @@ describe('Quick Submit behavior', function () {
});
it('excludes other modifier keys', () => {
- this.textarea.trigger(keydownEvent({
- altKey: true,
- }));
- this.textarea.trigger(keydownEvent({
- ctrlKey: true,
- }));
- this.textarea.trigger(keydownEvent({
- shiftKey: true,
- }));
+ this.textarea.trigger(
+ keydownEvent({
+ altKey: true,
+ }),
+ );
+ this.textarea.trigger(
+ keydownEvent({
+ ctrlKey: true,
+ }),
+ );
+ this.textarea.trigger(
+ keydownEvent({
+ shiftKey: true,
+ }),
+ );
return expect(this.spies.submit).not.toHaveBeenTriggered();
});
});
@@ -102,15 +113,21 @@ describe('Quick Submit behavior', function () {
});
it('excludes other modifier keys', () => {
- this.textarea.trigger(keydownEvent({
- altKey: true,
- }));
- this.textarea.trigger(keydownEvent({
- metaKey: true,
- }));
- this.textarea.trigger(keydownEvent({
- shiftKey: true,
- }));
+ this.textarea.trigger(
+ keydownEvent({
+ altKey: true,
+ }),
+ );
+ this.textarea.trigger(
+ keydownEvent({
+ metaKey: true,
+ }),
+ );
+ this.textarea.trigger(
+ keydownEvent({
+ shiftKey: true,
+ }),
+ );
return expect(this.spies.submit).not.toHaveBeenTriggered();
});
}
diff --git a/spec/javascripts/behaviors/secret_values_spec.js b/spec/javascripts/behaviors/secret_values_spec.js
index 38d9bba6868..95122fcf30f 100644
--- a/spec/javascripts/behaviors/secret_values_spec.js
+++ b/spec/javascripts/behaviors/secret_values_spec.js
@@ -9,7 +9,7 @@ function generateValueMarkup(
<div class="${placeholderClass}">
***
</div>
- <div class="hide ${valueClass}">
+ <div class="hidden ${valueClass}">
${secret}
</div>
`;
diff --git a/spec/javascripts/blob/3d_viewer/mesh_object_spec.js b/spec/javascripts/blob/3d_viewer/mesh_object_spec.js
index d1ebae33dab..7651792be2e 100644
--- a/spec/javascripts/blob/3d_viewer/mesh_object_spec.js
+++ b/spec/javascripts/blob/3d_viewer/mesh_object_spec.js
@@ -26,7 +26,7 @@ describe('Mesh object', () => {
const object = new MeshObject(
new BoxGeometry(10, 10, 10),
);
- const radius = object.geometry.boundingSphere.radius;
+ const { radius } = object.geometry.boundingSphere;
expect(radius).not.toBeGreaterThan(4);
});
@@ -35,7 +35,7 @@ describe('Mesh object', () => {
const object = new MeshObject(
new BoxGeometry(1, 1, 1),
);
- const radius = object.geometry.boundingSphere.radius;
+ const { radius } = object.geometry.boundingSphere;
expect(radius).toBeLessThan(1);
});
diff --git a/spec/javascripts/blob/balsamiq/balsamiq_viewer_integration_spec.js b/spec/javascripts/blob/balsamiq/balsamiq_viewer_integration_spec.js
index acd0aaf2a86..c726fa8e428 100644
--- a/spec/javascripts/blob/balsamiq/balsamiq_viewer_integration_spec.js
+++ b/spec/javascripts/blob/balsamiq/balsamiq_viewer_integration_spec.js
@@ -1,5 +1,3 @@
-/* eslint-disable import/no-unresolved */
-
import BalsamiqViewer from '~/blob/balsamiq/balsamiq_viewer';
import bmprPath from '../../fixtures/blob/balsamiq/test.bmpr';
diff --git a/spec/javascripts/blob/balsamiq/balsamiq_viewer_spec.js b/spec/javascripts/blob/balsamiq/balsamiq_viewer_spec.js
index aa87956109f..cb0f2ba686d 100644
--- a/spec/javascripts/blob/balsamiq/balsamiq_viewer_spec.js
+++ b/spec/javascripts/blob/balsamiq/balsamiq_viewer_spec.js
@@ -257,9 +257,9 @@ describe('BalsamiqViewer', () => {
name = 'name';
resource = 'resource';
template = `
- <div class="panel panel-default">
- <div class="panel-heading">name</div>
- <div class="panel-body">
+ <div class="card">
+ <div class="card-header">name</div>
+ <div class="card-body">
<img class="img-thumbnail" src="data:image/png;base64,image"/>
</div>
</div>
diff --git a/spec/javascripts/blob/notebook/index_spec.js b/spec/javascripts/blob/notebook/index_spec.js
index a143fc827d5..80c09a544d6 100644
--- a/spec/javascripts/blob/notebook/index_spec.js
+++ b/spec/javascripts/blob/notebook/index_spec.js
@@ -84,9 +84,14 @@ describe('iPython notebook renderer', () => {
describe('error in JSON response', () => {
let mock;
- beforeEach((done) => {
+ beforeEach(done => {
mock = new MockAdapter(axios);
- mock.onGet('/test').reply(() => Promise.reject({ status: 200, data: '{ "cells": [{"cell_type": "markdown"} }' }));
+ mock
+ .onGet('/test')
+ .reply(() =>
+ // eslint-disable-next-line prefer-promise-reject-errors
+ Promise.reject({ status: 200, data: '{ "cells": [{"cell_type": "markdown"} }' }),
+ );
renderNotebook();
diff --git a/spec/javascripts/blob/pdf/index_spec.js b/spec/javascripts/blob/pdf/index_spec.js
index 51bf3086627..bbe2500f8e3 100644
--- a/spec/javascripts/blob/pdf/index_spec.js
+++ b/spec/javascripts/blob/pdf/index_spec.js
@@ -1,5 +1,3 @@
-/* eslint-disable import/no-unresolved */
-
import renderPDF from '~/blob/pdf';
import testPDF from '../../fixtures/blob/pdf/test.pdf';
diff --git a/spec/javascripts/blob/sketch/index_spec.js b/spec/javascripts/blob/sketch/index_spec.js
index 79f40559817..e062a068a92 100644
--- a/spec/javascripts/blob/sketch/index_spec.js
+++ b/spec/javascripts/blob/sketch/index_spec.js
@@ -82,7 +82,7 @@ describe('Sketch viewer', () => {
const img = document.querySelector('#js-sketch-viewer img');
expect(img).not.toBeNull();
- expect(img.classList.contains('img-responsive')).toBeTruthy();
+ expect(img.classList.contains('img-fluid')).toBeTruthy();
});
it('renders link to image', () => {
diff --git a/spec/javascripts/blob/viewer/index_spec.js b/spec/javascripts/blob/viewer/index_spec.js
index f920c4ca945..8b79624d9f4 100644
--- a/spec/javascripts/blob/viewer/index_spec.js
+++ b/spec/javascripts/blob/viewer/index_spec.js
@@ -32,7 +32,7 @@ describe('Blob viewer', () => {
afterEach(() => {
mock.restore();
- location.hash = '';
+ window.location.hash = '';
});
it('loads source file after switching views', (done) => {
@@ -49,7 +49,7 @@ describe('Blob viewer', () => {
});
it('loads source file when line number is in hash', (done) => {
- location.hash = '#L1';
+ window.location.hash = '#L1';
new BlobViewer();
diff --git a/spec/javascripts/boards/board_blank_state_spec.js b/spec/javascripts/boards/board_blank_state_spec.js
index 664ea202e93..89a4fae4b59 100644
--- a/spec/javascripts/boards/board_blank_state_spec.js
+++ b/spec/javascripts/boards/board_blank_state_spec.js
@@ -1,4 +1,3 @@
-/* global BoardService */
import Vue from 'vue';
import '~/boards/stores/boards_store';
import BoardBlankState from '~/boards/components/board_blank_state.vue';
diff --git a/spec/javascripts/boards/board_card_spec.js b/spec/javascripts/boards/board_card_spec.js
index 13d607a06d2..ad263791cd4 100644
--- a/spec/javascripts/boards/board_card_spec.js
+++ b/spec/javascripts/boards/board_card_spec.js
@@ -1,15 +1,14 @@
/* global List */
/* global ListAssignee */
/* global ListLabel */
-/* global BoardService */
import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
-import '~/boards/models/assignee';
import eventHub from '~/boards/eventhub';
import '~/vue_shared/models/label';
+import '~/vue_shared/models/assignee';
import '~/boards/models/list';
import '~/boards/stores/boards_store';
import boardCard from '~/boards/components/board_card.vue';
diff --git a/spec/javascripts/boards/board_list_spec.js b/spec/javascripts/boards/board_list_spec.js
index 03df6c06691..de261d36c61 100644
--- a/spec/javascripts/boards/board_list_spec.js
+++ b/spec/javascripts/boards/board_list_spec.js
@@ -1,10 +1,9 @@
-/* global BoardService */
/* global List */
/* global ListIssue */
import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
-import Sortable from 'vendor/Sortable';
+import Sortable from 'sortablejs';
import BoardList from '~/boards/components/board_list.vue';
import eventHub from '~/boards/eventhub';
import '~/boards/mixins/sortable_default_options';
@@ -83,13 +82,13 @@ describe('Board list component', () => {
it('renders issues', () => {
expect(
- component.$el.querySelectorAll('.card').length,
+ component.$el.querySelectorAll('.board-card').length,
).toBe(1);
});
it('sets data attribute with issue id', () => {
expect(
- component.$el.querySelector('.card').getAttribute('data-issue-id'),
+ component.$el.querySelector('.board-card').getAttribute('data-issue-id'),
).toBe('1');
});
diff --git a/spec/javascripts/boards/board_new_issue_spec.js b/spec/javascripts/boards/board_new_issue_spec.js
index d5fbfdeaa91..ee37821ad08 100644
--- a/spec/javascripts/boards/board_new_issue_spec.js
+++ b/spec/javascripts/boards/board_new_issue_spec.js
@@ -1,4 +1,3 @@
-/* global BoardService */
/* global List */
import Vue from 'vue';
diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js
index 0cf9e4c9ba1..f7af099b3bf 100644
--- a/spec/javascripts/boards/boards_store_spec.js
+++ b/spec/javascripts/boards/boards_store_spec.js
@@ -1,5 +1,4 @@
-/* eslint-disable comma-dangle, one-var, no-unused-vars */
-/* global BoardService */
+/* eslint-disable comma-dangle, no-unused-vars */
/* global ListIssue */
import Vue from 'vue';
@@ -8,9 +7,9 @@ import axios from '~/lib/utils/axios_utils';
import Cookies from 'js-cookie';
import '~/vue_shared/models/label';
+import '~/vue_shared/models/assignee';
import '~/boards/models/issue';
import '~/boards/models/list';
-import '~/boards/models/assignee';
import '~/boards/services/board_service';
import '~/boards/stores/boards_store';
import { listObj, listObjDuplicate, boardsMockInterceptor, mockBoardService } from './mock_data';
diff --git a/spec/javascripts/boards/issue_card_spec.js b/spec/javascripts/boards/issue_card_spec.js
index be1ea0b57b4..7a32e84bced 100644
--- a/spec/javascripts/boards/issue_card_spec.js
+++ b/spec/javascripts/boards/issue_card_spec.js
@@ -5,11 +5,11 @@
import Vue from 'vue';
import '~/vue_shared/models/label';
+import '~/vue_shared/models/assignee';
import '~/boards/models/issue';
import '~/boards/models/list';
-import '~/boards/models/assignee';
import '~/boards/stores/boards_store';
-import '~/boards/components/issue_card_inner';
+import IssueCardInner from '~/boards/components/issue_card_inner.vue';
import { listObj } from './mock_data';
describe('Issue card component', () => {
@@ -48,7 +48,7 @@ describe('Issue card component', () => {
component = new Vue({
el: document.querySelector('.test-container'),
components: {
- 'issue-card': gl.issueBoards.IssueCardInner,
+ 'issue-card': IssueCardInner,
},
data() {
return {
@@ -70,19 +70,19 @@ describe('Issue card component', () => {
it('renders issue title', () => {
expect(
- component.$el.querySelector('.card-title').textContent,
+ component.$el.querySelector('.board-card-title').textContent,
).toContain(issue.title);
});
it('includes issue base in link', () => {
expect(
- component.$el.querySelector('.card-title a').getAttribute('href'),
+ component.$el.querySelector('.board-card-title a').getAttribute('href'),
).toContain('/test');
});
it('includes issue title on link', () => {
expect(
- component.$el.querySelector('.card-title a').getAttribute('title'),
+ component.$el.querySelector('.board-card-title a').getAttribute('title'),
).toBe(issue.title);
});
@@ -105,14 +105,14 @@ describe('Issue card component', () => {
it('renders issue ID with #', () => {
expect(
- component.$el.querySelector('.card-number').textContent,
+ component.$el.querySelector('.board-card-number').textContent,
).toContain(`#${issue.id}`);
});
describe('assignee', () => {
it('does not render assignee', () => {
expect(
- component.$el.querySelector('.card-assignee .avatar'),
+ component.$el.querySelector('.board-card-assignee .avatar'),
).toBeNull();
});
@@ -125,25 +125,25 @@ describe('Issue card component', () => {
it('renders assignee', () => {
expect(
- component.$el.querySelector('.card-assignee .avatar'),
+ component.$el.querySelector('.board-card-assignee .avatar'),
).not.toBeNull();
});
it('sets title', () => {
expect(
- component.$el.querySelector('.card-assignee img').getAttribute('data-original-title'),
+ component.$el.querySelector('.board-card-assignee img').getAttribute('data-original-title'),
).toContain(`Assigned to ${user.name}`);
});
it('sets users path', () => {
expect(
- component.$el.querySelector('.card-assignee a').getAttribute('href'),
+ component.$el.querySelector('.board-card-assignee a').getAttribute('href'),
).toBe('/test');
});
it('renders avatar', () => {
expect(
- component.$el.querySelector('.card-assignee img'),
+ component.$el.querySelector('.board-card-assignee img'),
).not.toBeNull();
});
});
@@ -161,10 +161,10 @@ describe('Issue card component', () => {
it('displays defaults avatar if users avatar is null', () => {
expect(
- component.$el.querySelector('.card-assignee img'),
+ component.$el.querySelector('.board-card-assignee img'),
).not.toBeNull();
expect(
- component.$el.querySelector('.card-assignee img').getAttribute('src'),
+ component.$el.querySelector('.board-card-assignee img').getAttribute('src'),
).toBe('default_avatar');
});
});
@@ -197,7 +197,7 @@ describe('Issue card component', () => {
});
it('renders all four assignees', () => {
- expect(component.$el.querySelectorAll('.card-assignee .avatar').length).toEqual(4);
+ expect(component.$el.querySelectorAll('.board-card-assignee .avatar').length).toEqual(4);
});
describe('more than four assignees', () => {
@@ -213,11 +213,11 @@ describe('Issue card component', () => {
});
it('renders more avatar counter', () => {
- expect(component.$el.querySelector('.card-assignee .avatar-counter').innerText).toEqual('+2');
+ expect(component.$el.querySelector('.board-card-assignee .avatar-counter').innerText).toEqual('+2');
});
it('renders three assignees', () => {
- expect(component.$el.querySelectorAll('.card-assignee .avatar').length).toEqual(3);
+ expect(component.$el.querySelectorAll('.board-card-assignee .avatar').length).toEqual(3);
});
it('renders 99+ avatar counter', (done) => {
@@ -232,7 +232,7 @@ describe('Issue card component', () => {
}
Vue.nextTick(() => {
- expect(component.$el.querySelector('.card-assignee .avatar-counter').innerText).toEqual('99+');
+ expect(component.$el.querySelector('.board-card-assignee .avatar-counter').innerText).toEqual('99+');
done();
});
});
@@ -248,14 +248,14 @@ describe('Issue card component', () => {
it('renders list label', () => {
expect(
- component.$el.querySelectorAll('.label').length,
+ component.$el.querySelectorAll('.badge').length,
).toBe(2);
});
it('renders label', () => {
const nodes = [];
- component.$el.querySelectorAll('.label').forEach((label) => {
- nodes.push(label.title);
+ component.$el.querySelectorAll('.badge').forEach((label) => {
+ nodes.push(label.getAttribute('data-original-title'));
});
expect(
@@ -265,13 +265,13 @@ describe('Issue card component', () => {
it('sets label description as title', () => {
expect(
- component.$el.querySelector('.label').getAttribute('title'),
+ component.$el.querySelector('.badge').getAttribute('data-original-title'),
).toContain(label1.description);
});
it('sets background color of button', () => {
const nodes = [];
- component.$el.querySelectorAll('.label').forEach((label) => {
+ component.$el.querySelectorAll('.badge').forEach((label) => {
nodes.push(label.style.backgroundColor);
});
@@ -288,7 +288,7 @@ describe('Issue card component', () => {
Vue.nextTick()
.then(() => {
expect(
- component.$el.querySelectorAll('.label').length,
+ component.$el.querySelectorAll('.badge').length,
).toBe(2);
expect(
component.$el.textContent,
diff --git a/spec/javascripts/boards/issue_spec.js b/spec/javascripts/boards/issue_spec.js
index 4a11131b55c..db68096e3bd 100644
--- a/spec/javascripts/boards/issue_spec.js
+++ b/spec/javascripts/boards/issue_spec.js
@@ -1,12 +1,11 @@
/* eslint-disable comma-dangle */
-/* global BoardService */
/* global ListIssue */
import Vue from 'vue';
import '~/vue_shared/models/label';
+import '~/vue_shared/models/assignee';
import '~/boards/models/issue';
import '~/boards/models/list';
-import '~/boards/models/assignee';
import '~/boards/services/board_service';
import '~/boards/stores/boards_store';
import { mockBoardService } from './mock_data';
diff --git a/spec/javascripts/boards/list_spec.js b/spec/javascripts/boards/list_spec.js
index d9a1d692949..ac8bbb8f2a8 100644
--- a/spec/javascripts/boards/list_spec.js
+++ b/spec/javascripts/boards/list_spec.js
@@ -1,5 +1,4 @@
/* eslint-disable comma-dangle */
-/* global BoardService */
/* global List */
/* global ListIssue */
@@ -7,9 +6,9 @@ import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import _ from 'underscore';
import '~/vue_shared/models/label';
+import '~/vue_shared/models/assignee';
import '~/boards/models/issue';
import '~/boards/models/list';
-import '~/boards/models/assignee';
import '~/boards/services/board_service';
import '~/boards/stores/boards_store';
import { listObj, listObjDuplicate, boardsMockInterceptor, mockBoardService } from './mock_data';
diff --git a/spec/javascripts/boards/modal_store_spec.js b/spec/javascripts/boards/modal_store_spec.js
index 797693a21aa..a234c81fadf 100644
--- a/spec/javascripts/boards/modal_store_spec.js
+++ b/spec/javascripts/boards/modal_store_spec.js
@@ -1,9 +1,9 @@
/* global ListIssue */
import '~/vue_shared/models/label';
+import '~/vue_shared/models/assignee';
import '~/boards/models/issue';
import '~/boards/models/list';
-import '~/boards/models/assignee';
import Store from '~/boards/stores/modal_store';
describe('Modal store', () => {
diff --git a/spec/javascripts/bootstrap_jquery_spec.js b/spec/javascripts/bootstrap_jquery_spec.js
index 0fd6f9dc810..052465d8d88 100644
--- a/spec/javascripts/bootstrap_jquery_spec.js
+++ b/spec/javascripts/bootstrap_jquery_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable space-before-function-paren, no-var */
+/* eslint-disable no-var */
import $ from 'jquery';
import '~/commons/bootstrap';
diff --git a/spec/javascripts/bootstrap_linked_tabs_spec.js b/spec/javascripts/bootstrap_linked_tabs_spec.js
index 93dc60d59fe..6f679369289 100644
--- a/spec/javascripts/bootstrap_linked_tabs_spec.js
+++ b/spec/javascripts/bootstrap_linked_tabs_spec.js
@@ -36,7 +36,7 @@ import LinkedTabs from '~/lib/utils/bootstrap_linked_tabs';
describe('on click', () => {
it('should change the url according to the clicked tab', () => {
- const historySpy = spyOn(history, 'replaceState').and.callFake(() => {});
+ const historySpy = spyOn(window.history, 'replaceState').and.callFake(() => {});
const linkedTabs = new LinkedTabs({
action: 'show',
diff --git a/spec/javascripts/clusters/clusters_bundle_spec.js b/spec/javascripts/clusters/clusters_bundle_spec.js
index a5cd247b689..abe2954d506 100644
--- a/spec/javascripts/clusters/clusters_bundle_spec.js
+++ b/spec/javascripts/clusters/clusters_bundle_spec.js
@@ -207,11 +207,11 @@ describe('Clusters', () => {
spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve());
expect(cluster.store.state.applications.helm.requestStatus).toEqual(null);
- cluster.installApplication('helm');
+ cluster.installApplication({ id: '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');
+ expect(cluster.service.installApplication).toHaveBeenCalledWith('helm', undefined);
getSetTimeoutPromise()
.then(() => {
@@ -226,11 +226,11 @@ describe('Clusters', () => {
spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve());
expect(cluster.store.state.applications.ingress.requestStatus).toEqual(null);
- cluster.installApplication('ingress');
+ cluster.installApplication({ id: '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');
+ expect(cluster.service.installApplication).toHaveBeenCalledWith('ingress', undefined);
getSetTimeoutPromise()
.then(() => {
@@ -245,11 +245,11 @@ describe('Clusters', () => {
spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve());
expect(cluster.store.state.applications.runner.requestStatus).toEqual(null);
- cluster.installApplication('runner');
+ cluster.installApplication({ id: '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');
+ expect(cluster.service.installApplication).toHaveBeenCalledWith('runner', undefined);
getSetTimeoutPromise()
.then(() => {
@@ -260,11 +260,29 @@ describe('Clusters', () => {
.catch(done.fail);
});
+ it('tries to install jupyter', (done) => {
+ spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve());
+ expect(cluster.store.state.applications.jupyter.requestStatus).toEqual(null);
+ cluster.installApplication({ id: 'jupyter', params: { hostname: cluster.store.state.applications.jupyter.hostname } });
+
+ expect(cluster.store.state.applications.jupyter.requestStatus).toEqual(REQUEST_LOADING);
+ expect(cluster.store.state.applications.jupyter.requestReason).toEqual(null);
+ expect(cluster.service.installApplication).toHaveBeenCalledWith('jupyter', { hostname: cluster.store.state.applications.jupyter.hostname });
+
+ getSetTimeoutPromise()
+ .then(() => {
+ expect(cluster.store.state.applications.jupyter.requestStatus).toEqual(REQUEST_SUCCESS);
+ expect(cluster.store.state.applications.jupyter.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');
+ cluster.installApplication({ id: 'helm' });
expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_LOADING);
expect(cluster.store.state.applications.helm.requestReason).toEqual(null);
diff --git a/spec/javascripts/clusters/components/application_row_spec.js b/spec/javascripts/clusters/components/application_row_spec.js
index 2c4707bb856..c83cbe90a57 100644
--- a/spec/javascripts/clusters/components/application_row_spec.js
+++ b/spec/javascripts/clusters/components/application_row_spec.js
@@ -174,7 +174,27 @@ describe('Application Row', () => {
installButton.click();
- expect(eventHub.$emit).toHaveBeenCalledWith('installApplication', DEFAULT_APPLICATION_STATE.id);
+ expect(eventHub.$emit).toHaveBeenCalledWith('installApplication', {
+ id: DEFAULT_APPLICATION_STATE.id,
+ params: {},
+ });
+ });
+
+ it('clicking install button when installApplicationRequestParams are provided emits event', () => {
+ spyOn(eventHub, '$emit');
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ status: APPLICATION_INSTALLABLE,
+ installApplicationRequestParams: { hostname: 'jupyter' },
+ });
+ const installButton = vm.$el.querySelector('.js-cluster-application-install-button');
+
+ installButton.click();
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('installApplication', {
+ id: DEFAULT_APPLICATION_STATE.id,
+ params: { hostname: 'jupyter' },
+ });
});
it('clicking disabled install button emits nothing', () => {
diff --git a/spec/javascripts/clusters/components/applications_spec.js b/spec/javascripts/clusters/components/applications_spec.js
index d546543d273..a70138c7eee 100644
--- a/spec/javascripts/clusters/components/applications_spec.js
+++ b/spec/javascripts/clusters/components/applications_spec.js
@@ -22,6 +22,7 @@ describe('Applications', () => {
ingress: { title: 'Ingress' },
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
+ jupyter: { title: 'JupyterHub' },
},
});
});
@@ -41,6 +42,10 @@ describe('Applications', () => {
it('renders a row for GitLab Runner', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-runner')).toBeDefined();
});
+
+ it('renders a row for Jupyter', () => {
+ expect(vm.$el.querySelector('.js-cluster-application-row-jupyter')).not.toBe(null);
+ });
});
describe('Ingress application', () => {
@@ -57,12 +62,11 @@ describe('Applications', () => {
helm: { title: 'Helm Tiller' },
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
+ jupyter: { title: 'JupyterHub', hostname: '' },
},
});
- expect(
- vm.$el.querySelector('.js-ip-address').value,
- ).toEqual('0.0.0.0');
+ expect(vm.$el.querySelector('.js-ip-address').value).toEqual('0.0.0.0');
expect(
vm.$el.querySelector('.js-clipboard-btn').getAttribute('data-clipboard-text'),
@@ -81,12 +85,11 @@ describe('Applications', () => {
helm: { title: 'Helm Tiller' },
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
+ jupyter: { title: 'JupyterHub', hostname: '' },
},
});
- expect(
- vm.$el.querySelector('.js-ip-address').value,
- ).toEqual('?');
+ expect(vm.$el.querySelector('.js-ip-address').value).toEqual('?');
expect(vm.$el.querySelector('.js-no-ip-message')).not.toBe(null);
});
@@ -101,6 +104,7 @@ describe('Applications', () => {
ingress: { title: 'Ingress' },
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
+ jupyter: { title: 'JupyterHub', hostname: '' },
},
});
@@ -108,5 +112,83 @@ describe('Applications', () => {
expect(vm.$el.querySelector('.js-ip-address')).toBe(null);
});
});
+
+ describe('Jupyter application', () => {
+ describe('with ingress installed with ip & jupyter installable', () => {
+ it('renders hostname active input', () => {
+ vm = mountComponent(Applications, {
+ applications: {
+ helm: { title: 'Helm Tiller', status: 'installed' },
+ ingress: { title: 'Ingress', status: 'installed', externalIp: '1.1.1.1' },
+ runner: { title: 'GitLab Runner' },
+ prometheus: { title: 'Prometheus' },
+ jupyter: { title: 'JupyterHub', hostname: '', status: 'installable' },
+ },
+ });
+
+ expect(vm.$el.querySelector('.js-hostname').getAttribute('readonly')).toEqual(null);
+ });
+ });
+
+ describe('with ingress installed without external ip', () => {
+ it('does not render hostname input', () => {
+ vm = mountComponent(Applications, {
+ applications: {
+ helm: { title: 'Helm Tiller', status: 'installed' },
+ ingress: { title: 'Ingress', status: 'installed' },
+ runner: { title: 'GitLab Runner' },
+ prometheus: { title: 'Prometheus' },
+ jupyter: { title: 'JupyterHub', hostname: '', status: 'installable' },
+ },
+ });
+
+ expect(vm.$el.querySelector('.js-hostname')).toBe(null);
+ });
+ });
+
+ describe('with ingress & jupyter installed', () => {
+ it('renders readonly input', () => {
+ vm = mountComponent(Applications, {
+ applications: {
+ helm: { title: 'Helm Tiller', status: 'installed' },
+ ingress: { title: 'Ingress', status: 'installed', externalIp: '1.1.1.1' },
+ runner: { title: 'GitLab Runner' },
+ prometheus: { title: 'Prometheus' },
+ jupyter: { title: 'JupyterHub', status: 'installed', hostname: '' },
+ },
+ });
+
+ expect(vm.$el.querySelector('.js-hostname').getAttribute('readonly')).toEqual('readonly');
+ });
+ });
+
+ describe('without ingress installed', () => {
+ beforeEach(() => {
+ vm = mountComponent(Applications, {
+ applications: {
+ helm: { title: 'Helm Tiller' },
+ ingress: { title: 'Ingress' },
+ runner: { title: 'GitLab Runner' },
+ prometheus: { title: 'Prometheus' },
+ jupyter: { title: 'JupyterHub', status: 'not_installable' },
+ },
+ });
+ });
+
+ it('does not render input', () => {
+ expect(vm.$el.querySelector('.js-hostname')).toBe(null);
+ });
+
+ it('renders disabled install button', () => {
+ expect(
+ vm.$el
+ .querySelector(
+ '.js-cluster-application-row-jupyter .js-cluster-application-install-button',
+ )
+ .getAttribute('disabled'),
+ ).toEqual('disabled');
+ });
+ });
+ });
});
});
diff --git a/spec/javascripts/clusters/services/mock_data.js b/spec/javascripts/clusters/services/mock_data.js
index 6ae7a792329..b2b0ebf840b 100644
--- a/spec/javascripts/clusters/services/mock_data.js
+++ b/spec/javascripts/clusters/services/mock_data.js
@@ -1,4 +1,5 @@
import {
+ APPLICATION_INSTALLED,
APPLICATION_INSTALLABLE,
APPLICATION_INSTALLING,
APPLICATION_ERROR,
@@ -28,6 +29,39 @@ const CLUSTERS_MOCK_DATA = {
name: 'prometheus',
status: APPLICATION_ERROR,
status_reason: 'Cannot connect',
+ }, {
+ name: 'jupyter',
+ status: APPLICATION_INSTALLING,
+ status_reason: 'Cannot connect',
+ }],
+ },
+ },
+ '/gitlab-org/gitlab-shell/clusters/2/status.json': {
+ data: {
+ status: 'errored',
+ status_reason: 'Failed to request to CloudPlatform.',
+ applications: [{
+ name: 'helm',
+ status: APPLICATION_INSTALLED,
+ status_reason: null,
+ }, {
+ name: 'ingress',
+ status: APPLICATION_INSTALLED,
+ status_reason: 'Cannot connect',
+ external_ip: '1.1.1.1',
+ }, {
+ name: 'runner',
+ status: APPLICATION_INSTALLING,
+ status_reason: null,
+ },
+ {
+ name: 'prometheus',
+ status: APPLICATION_ERROR,
+ status_reason: 'Cannot connect',
+ }, {
+ name: 'jupyter',
+ status: APPLICATION_INSTALLABLE,
+ status_reason: 'Cannot connect',
}],
},
},
@@ -37,6 +71,7 @@ const CLUSTERS_MOCK_DATA = {
'/gitlab-org/gitlab-shell/clusters/1/applications/ingress': { },
'/gitlab-org/gitlab-shell/clusters/1/applications/runner': { },
'/gitlab-org/gitlab-shell/clusters/1/applications/prometheus': { },
+ '/gitlab-org/gitlab-shell/clusters/1/applications/jupyter': { },
},
};
diff --git a/spec/javascripts/clusters/stores/clusters_store_spec.js b/spec/javascripts/clusters/stores/clusters_store_spec.js
index 8028faf2f02..9e43552f740 100644
--- a/spec/javascripts/clusters/stores/clusters_store_spec.js
+++ b/spec/javascripts/clusters/stores/clusters_store_spec.js
@@ -91,8 +91,26 @@ describe('Clusters Store', () => {
requestStatus: null,
requestReason: null,
},
+ jupyter: {
+ title: 'JupyterHub',
+ status: mockResponseData.applications[4].status,
+ statusReason: mockResponseData.applications[4].status_reason,
+ requestStatus: null,
+ requestReason: null,
+ hostname: '',
+ },
},
});
});
+
+ it('sets default hostname for jupyter when ingress has a ip address', () => {
+ const mockResponseData = CLUSTERS_MOCK_DATA.GET['/gitlab-org/gitlab-shell/clusters/2/status.json'].data;
+
+ store.updateStateFromServer(mockResponseData);
+
+ expect(
+ store.state.applications.jupyter.hostname,
+ ).toEqual(`jupyter.${store.state.applications.ingress.externalIp}.nip.io`);
+ });
});
});
diff --git a/spec/javascripts/commit/commit_pipeline_status_component_spec.js b/spec/javascripts/commit/commit_pipeline_status_component_spec.js
index 421fe62a1e7..d3776d0c3cf 100644
--- a/spec/javascripts/commit/commit_pipeline_status_component_spec.js
+++ b/spec/javascripts/commit/commit_pipeline_status_component_spec.js
@@ -75,10 +75,7 @@ describe('Commit pipeline status component', () => {
describe('When polling data was not succesful', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet('/dummy/endpoint').reply(() => {
- const res = Promise.reject([502, { }]);
- return res;
- });
+ mock.onGet('/dummy/endpoint').reply(502, {});
vm = new Component({
props: {
endpoint: '/dummy/endpoint',
diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js
index 819ed7896ca..a18e09da50a 100644
--- a/spec/javascripts/commit/pipelines/pipelines_spec.js
+++ b/spec/javascripts/commit/pipelines/pipelines_spec.js
@@ -16,7 +16,7 @@ describe('Pipelines table in Commits and Merge requests', function () {
beforeEach(() => {
mock = new MockAdapter(axios);
- const pipelines = getJSONFixture(jsonFixtureName).pipelines;
+ const { pipelines } = getJSONFixture(jsonFixtureName);
PipelinesTable = Vue.extend(pipelinesTable);
pipeline = pipelines.find(p => p.user !== null && p.commit !== null);
diff --git a/spec/javascripts/commits_spec.js b/spec/javascripts/commits_spec.js
index 60d100e8544..28b89157bd3 100644
--- a/spec/javascripts/commits_spec.js
+++ b/spec/javascripts/commits_spec.js
@@ -56,7 +56,7 @@ describe('Commits List', () => {
beforeEach(() => {
commitsList.searchField.val('');
- spyOn(history, 'replaceState').and.stub();
+ spyOn(window.history, 'replaceState').and.stub();
mock = new MockAdapter(axios);
mock.onGet('/h5bp/html5-boilerplate/commits/master').reply(200, {
diff --git a/spec/javascripts/create_merge_request_dropdown_spec.js b/spec/javascripts/create_merge_request_dropdown_spec.js
new file mode 100644
index 00000000000..b229765a8c5
--- /dev/null
+++ b/spec/javascripts/create_merge_request_dropdown_spec.js
@@ -0,0 +1,67 @@
+import axios from '~/lib/utils/axios_utils';
+import MockAdapter from 'axios-mock-adapter';
+import CreateMergeRequestDropdown from '~/create_merge_request_dropdown';
+import { TEST_HOST } from 'spec/test_constants';
+
+describe('CreateMergeRequestDropdown', () => {
+ let axiosMock;
+ let dropdown;
+
+ beforeEach(() => {
+ axiosMock = new MockAdapter(axios);
+
+ setFixtures(`
+ <div id="dummy-wrapper-element">
+ <div class="available"></div>
+ <div class="unavailable">
+ <div class="fa"></div>
+ <div class="text"></div>
+ </div>
+ <div class="js-ref"></div>
+ <div class="js-create-merge-request"></div>
+ <div class="js-create-target"></div>
+ <div class="js-dropdown-toggle"></div>
+ </div>
+ `);
+
+ const dummyElement = document.getElementById('dummy-wrapper-element');
+ dropdown = new CreateMergeRequestDropdown(dummyElement);
+ dropdown.refsPath = `${TEST_HOST}/dummy/refs?search=`;
+ });
+
+ afterEach(() => {
+ axiosMock.restore();
+ });
+
+ describe('getRef', () => {
+ it('escapes branch names correctly', done => {
+ const endpoint = `${dropdown.refsPath}contains%23hash`;
+ spyOn(axios, 'get').and.callThrough();
+ axiosMock.onGet(endpoint).replyOnce({});
+
+ dropdown
+ .getRef('contains#hash')
+ .then(() => {
+ expect(axios.get).toHaveBeenCalledWith(endpoint);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('updateCreatePaths', () => {
+ it('escapes branch names correctly', () => {
+ dropdown.createBranchPath = `${TEST_HOST}/branches?branch_name=some-branch&issue=42`;
+ dropdown.createMrPath = `${TEST_HOST}/create_merge_request?branch_name=some-branch&ref=master`;
+
+ dropdown.updateCreatePaths('branch', 'contains#hash');
+
+ expect(dropdown.createBranchPath).toBe(
+ `${TEST_HOST}/branches?branch_name=contains%23hash&issue=42`,
+ );
+ expect(dropdown.createMrPath).toBe(
+ `${TEST_HOST}/create_merge_request?branch_name=contains%23hash&ref=master`,
+ );
+ });
+ });
+});
diff --git a/spec/javascripts/datetime_utility_spec.js b/spec/javascripts/datetime_utility_spec.js
index a8d09202154..e224ed46d18 100644
--- a/spec/javascripts/datetime_utility_spec.js
+++ b/spec/javascripts/datetime_utility_spec.js
@@ -149,23 +149,22 @@ describe('getSundays', () => {
});
});
-describe('getTimeframeWindow', () => {
- it('returns array of dates representing a timeframe based on provided length and date', () => {
- const date = new Date(2018, 0, 1);
+describe('getTimeframeWindowFrom', () => {
+ it('returns array of date objects upto provided length start with provided startDate', () => {
+ const startDate = new Date(2018, 0, 1);
const mockTimeframe = [
- new Date(2017, 9, 1),
- new Date(2017, 10, 1),
- new Date(2017, 11, 1),
new Date(2018, 0, 1),
new Date(2018, 1, 1),
- new Date(2018, 2, 31),
+ new Date(2018, 2, 1),
+ new Date(2018, 3, 1),
+ new Date(2018, 4, 31),
];
- const timeframe = datetimeUtility.getTimeframeWindow(6, date);
-
- expect(timeframe.length).toBe(6);
+ const timeframe = datetimeUtility.getTimeframeWindowFrom(startDate, 5);
+ expect(timeframe.length).toBe(5);
timeframe.forEach((timeframeItem, index) => {
- expect(timeframeItem.getFullYear() === mockTimeframe[index].getFullYear()).toBeTruthy();
- expect(timeframeItem.getMonth() === mockTimeframe[index].getMonth()).toBeTruthy();
+ console.log(timeframeItem);
+ expect(timeframeItem.getFullYear() === mockTimeframe[index].getFullYear()).toBe(true);
+ expect(timeframeItem.getMonth() === mockTimeframe[index].getMonth()).toBe(true);
expect(timeframeItem.getDate() === mockTimeframe[index].getDate()).toBeTruthy();
});
});
diff --git a/spec/javascripts/deploy_keys/components/key_spec.js b/spec/javascripts/deploy_keys/components/key_spec.js
index 4279add21d1..d1de9d132b8 100644
--- a/spec/javascripts/deploy_keys/components/key_spec.js
+++ b/spec/javascripts/deploy_keys/components/key_spec.js
@@ -88,7 +88,7 @@ describe('Deploy keys key', () => {
});
it('expands all project labels after click', done => {
- const length = vm.deployKey.deploy_keys_projects.length;
+ const { length } = vm.deployKey.deploy_keys_projects;
vm.$el.querySelectorAll('.deploy-project-label')[1].click();
Vue.nextTick(() => {
diff --git a/spec/javascripts/diffs/components/app_spec.js b/spec/javascripts/diffs/components/app_spec.js
new file mode 100644
index 00000000000..7237274eb43
--- /dev/null
+++ b/spec/javascripts/diffs/components/app_spec.js
@@ -0,0 +1 @@
+// TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
diff --git a/spec/javascripts/diffs/components/changed_files_dropdown_spec.js b/spec/javascripts/diffs/components/changed_files_dropdown_spec.js
new file mode 100644
index 00000000000..7237274eb43
--- /dev/null
+++ b/spec/javascripts/diffs/components/changed_files_dropdown_spec.js
@@ -0,0 +1 @@
+// TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
diff --git a/spec/javascripts/diffs/components/changed_files_spec.js b/spec/javascripts/diffs/components/changed_files_spec.js
new file mode 100644
index 00000000000..f737e8fa38e
--- /dev/null
+++ b/spec/javascripts/diffs/components/changed_files_spec.js
@@ -0,0 +1,105 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import { mountComponentWithStore } from 'spec/helpers';
+import diffsModule from '~/diffs/store/modules';
+import changedFiles from '~/diffs/components/changed_files.vue';
+
+describe('ChangedFiles', () => {
+ const Component = Vue.extend(changedFiles);
+ const store = new Vuex.Store({
+ modules: {
+ diffs: diffsModule,
+ },
+ });
+
+ let vm;
+
+ beforeEach(() => {
+ setFixtures(`
+ <div id="dummy-element"></div>
+ <div class="js-tabs-affix"></div>
+ `);
+
+ const props = {
+ diffFiles: [
+ {
+ addedLines: 10,
+ removedLines: 20,
+ blob: {
+ path: 'some/code.txt',
+ },
+ filePath: 'some/code.txt',
+ },
+ ],
+ };
+
+ vm = mountComponentWithStore(Component, { props, store });
+ });
+
+ describe('with single file added', () => {
+ it('shows files changes', () => {
+ expect(vm.$el).toContainText('1 changed file');
+ });
+
+ it('shows file additions and deletions', () => {
+ expect(vm.$el).toContainText('10 additions');
+ expect(vm.$el).toContainText('20 deletions');
+ });
+ });
+
+ describe('diff view mode buttons', () => {
+ let inlineButton;
+ let parallelButton;
+
+ beforeEach(() => {
+ inlineButton = vm.$el.querySelector('.js-inline-diff-button');
+ parallelButton = vm.$el.querySelector('.js-parallel-diff-button');
+ });
+
+ it('should have Inline and Side-by-side buttons', () => {
+ expect(inlineButton).toBeDefined();
+ expect(parallelButton).toBeDefined();
+ });
+
+ it('should add active class to Inline button', done => {
+ vm.$store.state.diffs.diffViewType = 'inline';
+
+ vm.$nextTick(() => {
+ expect(inlineButton.classList.contains('active')).toEqual(true);
+ expect(parallelButton.classList.contains('active')).toEqual(false);
+
+ done();
+ });
+ });
+
+ it('should toggle active state of buttons when diff view type changed', done => {
+ vm.$store.state.diffs.diffViewType = 'parallel';
+
+ vm.$nextTick(() => {
+ expect(inlineButton.classList.contains('active')).toEqual(false);
+ expect(parallelButton.classList.contains('active')).toEqual(true);
+
+ done();
+ });
+ });
+
+ describe('clicking them', () => {
+ it('should toggle the diff view type', done => {
+ parallelButton.click();
+
+ vm.$nextTick(() => {
+ expect(inlineButton.classList.contains('active')).toEqual(false);
+ expect(parallelButton.classList.contains('active')).toEqual(true);
+
+ inlineButton.click();
+
+ vm.$nextTick(() => {
+ expect(inlineButton.classList.contains('active')).toEqual(true);
+ expect(parallelButton.classList.contains('active')).toEqual(false);
+ done();
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/diffs/components/compare_versions_dropdown_spec.js b/spec/javascripts/diffs/components/compare_versions_dropdown_spec.js
new file mode 100644
index 00000000000..7237274eb43
--- /dev/null
+++ b/spec/javascripts/diffs/components/compare_versions_dropdown_spec.js
@@ -0,0 +1 @@
+// TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
diff --git a/spec/javascripts/diffs/components/compare_versions_spec.js b/spec/javascripts/diffs/components/compare_versions_spec.js
new file mode 100644
index 00000000000..7237274eb43
--- /dev/null
+++ b/spec/javascripts/diffs/components/compare_versions_spec.js
@@ -0,0 +1 @@
+// TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
diff --git a/spec/javascripts/diffs/components/diff_content_spec.js b/spec/javascripts/diffs/components/diff_content_spec.js
new file mode 100644
index 00000000000..dea600a783a
--- /dev/null
+++ b/spec/javascripts/diffs/components/diff_content_spec.js
@@ -0,0 +1,95 @@
+import Vue from 'vue';
+import DiffContentComponent from '~/diffs/components/diff_content.vue';
+import store from '~/mr_notes/stores';
+import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { GREEN_BOX_IMAGE_URL, RED_BOX_IMAGE_URL } from 'spec/test_constants';
+import diffFileMockData from '../mock_data/diff_file';
+
+describe('DiffContent', () => {
+ const Component = Vue.extend(DiffContentComponent);
+ let vm;
+ const getDiffFileMock = () => Object.assign({}, diffFileMockData);
+
+ beforeEach(() => {
+ vm = mountComponentWithStore(Component, {
+ store,
+ props: {
+ diffFile: getDiffFileMock(),
+ },
+ });
+ });
+
+ describe('text based files', () => {
+ it('should render diff inline view', done => {
+ vm.$store.state.diffs.diffViewType = 'inline';
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelectorAll('.js-diff-inline-view').length).toEqual(1);
+
+ done();
+ });
+ });
+
+ it('should render diff parallel view', done => {
+ vm.$store.state.diffs.diffViewType = 'parallel';
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelectorAll('.parallel').length).toEqual(18);
+
+ done();
+ });
+ });
+ });
+
+ describe('Non-Text diffs', () => {
+ beforeEach(() => {
+ vm.diffFile.text = false;
+ });
+
+ describe('image diff', () => {
+ beforeEach(() => {
+ vm.diffFile.newPath = GREEN_BOX_IMAGE_URL;
+ vm.diffFile.newSha = 'DEF';
+ vm.diffFile.oldPath = RED_BOX_IMAGE_URL;
+ vm.diffFile.oldSha = 'ABC';
+ vm.diffFile.viewPath = '';
+ });
+
+ it('should have image diff view in place', done => {
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelectorAll('.js-diff-inline-view').length).toEqual(0);
+
+ expect(vm.$el.querySelectorAll('.diff-viewer .image').length).toEqual(1);
+
+ done();
+ });
+ });
+ });
+
+ describe('file diff', () => {
+ it('should have download buttons in place', done => {
+ const el = vm.$el;
+ vm.diffFile.newPath = 'test.abc';
+ vm.diffFile.newSha = 'DEF';
+ vm.diffFile.oldPath = 'test.abc';
+ vm.diffFile.oldSha = 'ABC';
+
+ vm.$nextTick(() => {
+ expect(el.querySelectorAll('.js-diff-inline-view').length).toEqual(0);
+
+ expect(el.querySelector('.deleted .file-info').textContent.trim()).toContain('test.abc');
+ expect(el.querySelector('.deleted .btn.btn-default').textContent.trim()).toContain(
+ 'Download',
+ );
+
+ expect(el.querySelector('.added .file-info').textContent.trim()).toContain('test.abc');
+ expect(el.querySelector('.added .btn.btn-default').textContent.trim()).toContain(
+ 'Download',
+ );
+
+ done();
+ });
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/diffs/components/diff_discussions_spec.js b/spec/javascripts/diffs/components/diff_discussions_spec.js
new file mode 100644
index 00000000000..270f363825f
--- /dev/null
+++ b/spec/javascripts/diffs/components/diff_discussions_spec.js
@@ -0,0 +1,24 @@
+import Vue from 'vue';
+import DiffDiscussions from '~/diffs/components/diff_discussions.vue';
+import store from '~/mr_notes/stores';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import discussionsMockData from '../mock_data/diff_discussions';
+
+describe('DiffDiscussions', () => {
+ let component;
+ const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)];
+
+ beforeEach(() => {
+ component = createComponentWithStore(Vue.extend(DiffDiscussions), store, {
+ discussions: getDiscussionsMockData(),
+ }).$mount();
+ });
+
+ describe('template', () => {
+ it('should have notes list', () => {
+ const { $el } = component;
+
+ expect($el.querySelectorAll('.discussion .note.timeline-entry').length).toEqual(5);
+ });
+ });
+});
diff --git a/spec/javascripts/diffs/components/diff_file_header_spec.js b/spec/javascripts/diffs/components/diff_file_header_spec.js
new file mode 100644
index 00000000000..0f3a95da5bf
--- /dev/null
+++ b/spec/javascripts/diffs/components/diff_file_header_spec.js
@@ -0,0 +1,439 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import diffsModule from '~/diffs/store/modules';
+import notesModule from '~/notes/stores/modules';
+import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+
+const discussionFixture = 'merge_requests/diff_discussion.json';
+
+describe('diff_file_header', () => {
+ let vm;
+ let props;
+ const Component = Vue.extend(DiffFileHeader);
+ const store = new Vuex.Store({
+ modules: {
+ diffs: diffsModule,
+ notes: notesModule,
+ },
+ });
+
+ beforeEach(() => {
+ const diffDiscussionMock = getJSONFixture(discussionFixture)[0];
+ const diffFile = convertObjectPropsToCamelCase(diffDiscussionMock.diff_file, { deep: true });
+ props = {
+ diffFile,
+ currentUser: {},
+ };
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('computed', () => {
+ describe('icon', () => {
+ beforeEach(() => {
+ props.diffFile.blob.icon = 'file-text-o';
+ });
+
+ it('returns the blob icon for files', () => {
+ props.diffFile.submodule = false;
+
+ vm = mountComponentWithStore(Component, { props, store });
+
+ expect(vm.icon).toBe(props.diffFile.blob.icon);
+ });
+
+ it('returns the archive icon for submodules', () => {
+ props.diffFile.submodule = true;
+
+ vm = mountComponentWithStore(Component, { props, store });
+
+ expect(vm.icon).toBe('archive');
+ });
+ });
+
+ describe('titleLink', () => {
+ beforeEach(() => {
+ Object.assign(props.diffFile, {
+ fileHash: 'badc0ffee',
+ submoduleLink: 'link://to/submodule',
+ submoduleTreeUrl: 'some://tree/url',
+ });
+ });
+
+ it('returns the fileHash for files', () => {
+ props.diffFile.submodule = false;
+
+ vm = mountComponentWithStore(Component, { props, store });
+
+ expect(vm.titleLink).toBe(`#${props.diffFile.fileHash}`);
+ });
+
+ it('returns the submoduleTreeUrl for submodules', () => {
+ props.diffFile.submodule = true;
+
+ vm = mountComponentWithStore(Component, { props, store });
+
+ expect(vm.titleLink).toBe(props.diffFile.submoduleTreeUrl);
+ });
+
+ it('returns the submoduleLink for submodules without submoduleTreeUrl', () => {
+ Object.assign(props.diffFile, {
+ submodule: true,
+ submoduleTreeUrl: null,
+ });
+
+ vm = mountComponentWithStore(Component, { props, store });
+
+ expect(vm.titleLink).toBe(props.diffFile.submoduleLink);
+ });
+ });
+
+ describe('filePath', () => {
+ beforeEach(() => {
+ Object.assign(props.diffFile, {
+ blob: { id: 'b10b1db10b1d' },
+ filePath: 'path/to/file',
+ });
+ });
+
+ it('returns the filePath for files', () => {
+ props.diffFile.submodule = false;
+
+ vm = mountComponentWithStore(Component, { props, store });
+
+ expect(vm.filePath).toBe(props.diffFile.filePath);
+ });
+
+ it('appends the truncated blob id for submodules', () => {
+ props.diffFile.submodule = true;
+
+ vm = mountComponentWithStore(Component, { props, store });
+
+ expect(vm.filePath).toBe(
+ `${props.diffFile.filePath} @ ${props.diffFile.blob.id.substr(0, 8)}`,
+ );
+ });
+ });
+
+ describe('titleTag', () => {
+ it('returns a link tag if fileHash is set', () => {
+ props.diffFile.fileHash = 'some hash';
+
+ vm = mountComponentWithStore(Component, { props, store });
+
+ expect(vm.titleTag).toBe('a');
+ });
+
+ it('returns a span tag if fileHash is not set', () => {
+ props.diffFile.fileHash = null;
+
+ vm = mountComponentWithStore(Component, { props, store });
+
+ expect(vm.titleTag).toBe('span');
+ });
+ });
+
+ describe('isUsingLfs', () => {
+ beforeEach(() => {
+ Object.assign(props.diffFile, {
+ storedExternally: true,
+ externalStorage: 'lfs',
+ });
+ });
+
+ it('returns true if file is stored in LFS', () => {
+ vm = mountComponentWithStore(Component, { props, store });
+
+ expect(vm.isUsingLfs).toBe(true);
+ });
+
+ it('returns false if file is not stored externally', () => {
+ props.diffFile.storedExternally = false;
+
+ vm = mountComponentWithStore(Component, { props, store });
+
+ expect(vm.isUsingLfs).toBe(false);
+ });
+
+ it('returns false if file is not stored in LFS', () => {
+ props.diffFile.externalStorage = 'not lfs';
+
+ vm = mountComponentWithStore(Component, { props, store });
+
+ expect(vm.isUsingLfs).toBe(false);
+ });
+ });
+
+ describe('collapseIcon', () => {
+ it('returns chevron-down if the diff is expanded', () => {
+ props.expanded = true;
+
+ vm = mountComponentWithStore(Component, { props, store });
+
+ expect(vm.collapseIcon).toBe('chevron-down');
+ });
+
+ it('returns chevron-right if the diff is collapsed', () => {
+ props.expanded = false;
+
+ vm = mountComponentWithStore(Component, { props, store });
+
+ expect(vm.collapseIcon).toBe('chevron-right');
+ });
+ });
+
+ describe('viewFileButtonText', () => {
+ it('contains the truncated content SHA', () => {
+ const dummySha = 'deebd00f is no SHA';
+ props.diffFile.contentSha = dummySha;
+
+ vm = mountComponentWithStore(Component, { props, store });
+
+ expect(vm.viewFileButtonText).not.toContain(dummySha);
+ expect(vm.viewFileButtonText).toContain(dummySha.substr(0, 8));
+ });
+ });
+
+ describe('viewReplacedFileButtonText', () => {
+ it('contains the truncated base SHA', () => {
+ const dummySha = 'deadabba sings no more';
+ props.diffFile.diffRefs.baseSha = dummySha;
+
+ vm = mountComponentWithStore(Component, { props, store });
+
+ expect(vm.viewReplacedFileButtonText).not.toContain(dummySha);
+ expect(vm.viewReplacedFileButtonText).toContain(dummySha.substr(0, 8));
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('handleToggleFile', () => {
+ beforeEach(() => {
+ spyOn(vm, '$emit').and.stub();
+ });
+
+ it('emits toggleFile if checkTarget is false', () => {
+ vm.handleToggleFile(null, false);
+
+ expect(vm.$emit).toHaveBeenCalledWith('toggleFile');
+ });
+
+ it('emits toggleFile if checkTarget is true and event target is header', () => {
+ vm.handleToggleFile({ target: vm.$refs.header }, true);
+
+ expect(vm.$emit).toHaveBeenCalledWith('toggleFile');
+ });
+
+ it('does not emit toggleFile if checkTarget is true and event target is not header', () => {
+ vm.handleToggleFile({ target: 'not header' }, true);
+
+ expect(vm.$emit).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('template', () => {
+ describe('collapse toggle', () => {
+ const collapseToggle = () => vm.$el.querySelector('.diff-toggle-caret');
+
+ it('is visible if collapsible is true', () => {
+ props.collapsible = true;
+
+ vm = mountComponentWithStore(Component, { props, store });
+
+ expect(collapseToggle()).not.toBe(null);
+ });
+
+ it('is hidden if collapsible is false', () => {
+ props.collapsible = false;
+
+ vm = mountComponentWithStore(Component, { props, store });
+
+ expect(collapseToggle()).toBe(null);
+ });
+ });
+
+ it('displays an file icon in the title', () => {
+ vm = mountComponentWithStore(Component, { props, store });
+ expect(vm.$el.querySelector('svg.js-file-icon use').getAttribute('xlink:href')).toContain(
+ 'ruby',
+ );
+ });
+
+ describe('file paths', () => {
+ const filePaths = () => vm.$el.querySelectorAll('.file-title-name');
+
+ it('displays the path of a added file', () => {
+ props.diffFile.renamedFile = false;
+
+ vm = mountComponentWithStore(Component, { props, store });
+
+ expect(filePaths()).toHaveLength(1);
+ expect(filePaths()[0]).toHaveText(props.diffFile.filePath);
+ });
+
+ it('displays path for deleted file', () => {
+ props.diffFile.renamedFile = false;
+ props.diffFile.deletedFile = true;
+
+ vm = mountComponentWithStore(Component, { props, store });
+
+ expect(filePaths()).toHaveLength(1);
+ expect(filePaths()[0]).toHaveText(`${props.diffFile.filePath} deleted`);
+ });
+
+ it('displays old and new path if the file was renamed', () => {
+ props.diffFile.renamedFile = true;
+
+ vm = mountComponentWithStore(Component, { props, store });
+
+ expect(filePaths()).toHaveLength(2);
+ expect(filePaths()[0]).toHaveText(props.diffFile.oldPath);
+ expect(filePaths()[1]).toHaveText(props.diffFile.newPath);
+ });
+ });
+
+ it('displays a copy to clipboard button', () => {
+ vm = mountComponentWithStore(Component, { props, store });
+
+ const button = vm.$el.querySelector('.btn-clipboard');
+ expect(button).not.toBe(null);
+ expect(button.dataset.clipboardText).toBe(props.diffFile.filePath);
+ });
+
+ describe('file mode', () => {
+ it('it displays old and new file mode if it changed', () => {
+ props.diffFile.modeChanged = true;
+
+ vm = mountComponentWithStore(Component, { props, store });
+
+ const { fileMode } = vm.$refs;
+ expect(fileMode).not.toBe(undefined);
+ expect(fileMode).toContainText(props.diffFile.aMode);
+ expect(fileMode).toContainText(props.diffFile.bMode);
+ });
+
+ it('does not display the file mode if it has not changed', () => {
+ props.diffFile.modeChanged = false;
+
+ vm = mountComponentWithStore(Component, { props, store });
+
+ const { fileMode } = vm.$refs;
+ expect(fileMode).toBe(undefined);
+ });
+ });
+
+ describe('LFS label', () => {
+ const lfsLabel = () => vm.$el.querySelector('.label-lfs');
+
+ it('displays the LFS label for files stored in LFS', () => {
+ Object.assign(props.diffFile, {
+ storedExternally: true,
+ externalStorage: 'lfs',
+ });
+
+ vm = mountComponentWithStore(Component, { props, store });
+
+ expect(lfsLabel()).not.toBe(null);
+ expect(lfsLabel()).toHaveText('LFS');
+ });
+
+ it('does not display the LFS label for files stored in repository', () => {
+ props.diffFile.storedExternally = false;
+
+ vm = mountComponentWithStore(Component, { props, store });
+
+ expect(lfsLabel()).toBe(null);
+ });
+ });
+
+ describe('edit button', () => {
+ it('should not render edit button if addMergeRequestButtons is not true', () => {
+ vm = mountComponentWithStore(Component, { props, store });
+
+ expect(vm.$el.querySelector('.js-edit-blob')).toEqual(null);
+ });
+
+ it('should show edit button when file is editable', () => {
+ props.addMergeRequestButtons = true;
+ props.diffFile.editPath = '/';
+ vm = mountComponentWithStore(Component, { props, store });
+
+ expect(vm.$el.querySelector('.js-edit-blob')).toContainText('Edit');
+ });
+
+ it('should not show edit button when file is deleted', () => {
+ props.addMergeRequestButtons = true;
+ props.diffFile.deletedFile = true;
+ props.diffFile.editPath = '/';
+ vm = mountComponentWithStore(Component, { props, store });
+
+ expect(vm.$el.querySelector('.js-edit-blob')).toEqual(null);
+ });
+ });
+
+ describe('addMergeRequestButtons', () => {
+ beforeEach(() => {
+ props.addMergeRequestButtons = true;
+ props.diffFile.editPath = '';
+ });
+
+ describe('view on environment button', () => {
+ const url = 'some.external.url/';
+ const title = 'url.title';
+
+ it('displays link to external url', () => {
+ props.diffFile.externalUrl = url;
+ props.diffFile.formattedExternalUrl = title;
+
+ vm = mountComponentWithStore(Component, { props, store });
+
+ expect(vm.$el.querySelector(`a[href="${url}"]`)).not.toBe(null);
+ expect(vm.$el.querySelector(`a[data-original-title="View on ${title}"]`)).not.toBe(null);
+ });
+
+ it('hides link if no external url', () => {
+ props.diffFile.externalUrl = '';
+ props.diffFile.formattedExternalUrl = title;
+
+ vm = mountComponentWithStore(Component, { props, store });
+
+ expect(vm.$el.querySelector(`a[data-original-title="View on ${title}"]`)).toBe(null);
+ });
+ });
+ });
+
+ describe('handles toggle discussions', () => {
+ it('dispatches toggleFileDiscussions when user clicks on toggle discussions button', () => {
+ const propsCopy = Object.assign({}, props);
+ propsCopy.diffFile.submodule = false;
+ propsCopy.diffFile.blob = {
+ id: '848ed9407c6730ff16edb3dd24485a0eea24292a',
+ path: 'lib/base.js',
+ name: 'base.js',
+ mode: '100644',
+ readableText: true,
+ icon: 'file-text-o',
+ };
+ propsCopy.addMergeRequestButtons = true;
+ propsCopy.diffFile.deletedFile = true;
+
+ vm = mountComponentWithStore(Component, {
+ props: propsCopy,
+ store,
+ });
+
+ spyOn(vm, 'toggleFileDiscussions');
+
+ vm.$el.querySelector('.js-btn-vue-toggle-comments').click();
+
+ expect(vm.toggleFileDiscussions).toHaveBeenCalled();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/diffs/components/diff_file_spec.js b/spec/javascripts/diffs/components/diff_file_spec.js
new file mode 100644
index 00000000000..9b994543e19
--- /dev/null
+++ b/spec/javascripts/diffs/components/diff_file_spec.js
@@ -0,0 +1,88 @@
+import Vue from 'vue';
+import DiffFileComponent from '~/diffs/components/diff_file.vue';
+import store from '~/mr_notes/stores';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import diffFileMockData from '../mock_data/diff_file';
+
+describe('DiffFile', () => {
+ let vm;
+ const getDiffFileMock = () => Object.assign({}, diffFileMockData);
+
+ beforeEach(() => {
+ vm = createComponentWithStore(Vue.extend(DiffFileComponent), store, {
+ file: getDiffFileMock(),
+ currentUser: {},
+ }).$mount();
+ });
+
+ describe('template', () => {
+ it('should render component with file header, file content components', () => {
+ const el = vm.$el;
+ const { fileHash, filePath } = diffFileMockData;
+
+ expect(el.id).toEqual(fileHash);
+ expect(el.classList.contains('diff-file')).toEqual(true);
+ expect(el.querySelectorAll('.diff-content.hidden').length).toEqual(0);
+ expect(el.querySelector('.js-file-title')).toBeDefined();
+ expect(el.querySelector('.file-title-name').innerText.indexOf(filePath) > -1).toEqual(true);
+ expect(el.querySelector('.js-syntax-highlight')).toBeDefined();
+ expect(el.querySelectorAll('.line_content').length > 5).toEqual(true);
+ });
+
+ describe('collapsed', () => {
+ it('should not have file content', done => {
+ expect(vm.$el.querySelectorAll('.diff-content').length).toEqual(1);
+ expect(vm.file.collapsed).toEqual(false);
+ vm.file.collapsed = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelectorAll('.diff-content').length).toEqual(0);
+
+ done();
+ });
+ });
+
+ it('should have collapsed text and link', done => {
+ vm.file.collapsed = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.innerText).toContain('This diff is collapsed');
+ expect(vm.$el.querySelectorAll('.js-click-to-expand').length).toEqual(1);
+
+ done();
+ });
+ });
+
+ it('should have loading icon while loading a collapsed diffs', done => {
+ vm.file.collapsed = true;
+ vm.isLoadingCollapsedDiff = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelectorAll('.diff-content.loading').length).toEqual(1);
+
+ done();
+ });
+ });
+ });
+ });
+
+ describe('too large diff', () => {
+ it('should have too large warning and blob link', done => {
+ const BLOB_LINK = '/file/view/path';
+ vm.file.tooLarge = true;
+ vm.file.viewPath = BLOB_LINK;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.innerText).toContain(
+ 'This source diff could not be displayed because it is too large',
+ );
+ expect(vm.$el.querySelector('.js-too-large-diff')).toBeDefined();
+ expect(vm.$el.querySelector('.js-too-large-diff a').href.indexOf(BLOB_LINK) > -1).toEqual(
+ true,
+ );
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/diffs/components/diff_gutter_avatars_spec.js b/spec/javascripts/diffs/components/diff_gutter_avatars_spec.js
new file mode 100644
index 00000000000..0085a16815a
--- /dev/null
+++ b/spec/javascripts/diffs/components/diff_gutter_avatars_spec.js
@@ -0,0 +1,115 @@
+import Vue from 'vue';
+import DiffGutterAvatarsComponent from '~/diffs/components/diff_gutter_avatars.vue';
+import { COUNT_OF_AVATARS_IN_GUTTER } from '~/diffs/constants';
+import store from '~/mr_notes/stores';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import discussionsMockData from '../mock_data/diff_discussions';
+
+describe('DiffGutterAvatars', () => {
+ let component;
+ const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)];
+
+ beforeEach(() => {
+ component = createComponentWithStore(Vue.extend(DiffGutterAvatarsComponent), store, {
+ discussions: getDiscussionsMockData(),
+ }).$mount();
+ });
+
+ describe('computed', () => {
+ describe('discussionsExpanded', () => {
+ it('should return true when all discussions are expanded', () => {
+ expect(component.discussionsExpanded).toEqual(true);
+ });
+
+ it('should return false when all discussions are not expanded', () => {
+ component.discussions[0].expanded = false;
+ expect(component.discussionsExpanded).toEqual(false);
+ });
+ });
+
+ describe('allDiscussions', () => {
+ it('should return an array of notes', () => {
+ expect(component.allDiscussions).toEqual([...component.discussions[0].notes]);
+ });
+ });
+
+ describe('notesInGutter', () => {
+ it('should return a subset of discussions to show in gutter', () => {
+ expect(component.notesInGutter.length).toEqual(COUNT_OF_AVATARS_IN_GUTTER);
+ expect(component.notesInGutter[0]).toEqual({
+ note: component.discussions[0].notes[0].note,
+ author: component.discussions[0].notes[0].author,
+ });
+ });
+ });
+
+ describe('moreCount', () => {
+ it('should return count of remaining discussions from gutter', () => {
+ expect(component.moreCount).toEqual(2);
+ });
+ });
+
+ describe('moreText', () => {
+ it('should return proper text if moreCount > 0', () => {
+ expect(component.moreText).toEqual('2 more comments');
+ });
+
+ it('should return empty string if there is no discussion', () => {
+ component.discussions = [];
+ expect(component.moreText).toEqual('');
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('getTooltipText', () => {
+ it('should return original comment if it is shorter than max length', () => {
+ const note = component.discussions[0].notes[0];
+
+ expect(component.getTooltipText(note)).toEqual('Administrator: comment 1');
+ });
+
+ it('should return truncated version of comment', () => {
+ const note = component.discussions[0].notes[1];
+
+ expect(component.getTooltipText(note)).toEqual('Fatih Acet: comment 2 is r...');
+ });
+ });
+
+ describe('toggleDiscussions', () => {
+ it('should toggle all discussions', () => {
+ expect(component.discussions[0].expanded).toEqual(true);
+
+ component.$store.dispatch('setInitialNotes', getDiscussionsMockData());
+ component.discussions = component.$store.state.notes.discussions;
+ component.toggleDiscussions();
+
+ expect(component.discussions[0].expanded).toEqual(false);
+ component.$store.dispatch('setInitialNotes', []);
+ });
+ });
+ });
+
+ describe('template', () => {
+ const buttonSelector = '.js-diff-comment-button';
+ const svgSelector = `${buttonSelector} svg`;
+ const avatarSelector = '.js-diff-comment-avatar';
+ const plusCountSelector = '.js-diff-comment-plus';
+
+ it('should have button to collapse discussions when the discussions expanded', () => {
+ expect(component.$el.querySelector(buttonSelector)).toBeDefined();
+ expect(component.$el.querySelector(svgSelector)).toBeDefined();
+ });
+
+ it('should have user avatars when discussions collapsed', () => {
+ component.discussions[0].expanded = false;
+
+ Vue.nextTick(() => {
+ expect(component.$el.querySelector(buttonSelector)).toBeNull();
+ expect(component.$el.querySelectorAll(avatarSelector).length).toEqual(4);
+ expect(component.$el.querySelector(plusCountSelector)).toBeDefined();
+ expect(component.$el.querySelector(plusCountSelector).textContent).toEqual('+2');
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/diffs/components/diff_line_gutter_content_spec.js b/spec/javascripts/diffs/components/diff_line_gutter_content_spec.js
new file mode 100644
index 00000000000..2d136a63c52
--- /dev/null
+++ b/spec/javascripts/diffs/components/diff_line_gutter_content_spec.js
@@ -0,0 +1,108 @@
+import Vue from 'vue';
+import DiffLineGutterContent from '~/diffs/components/diff_line_gutter_content.vue';
+import store from '~/mr_notes/stores';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import discussionsMockData from '../mock_data/diff_discussions';
+import diffFileMockData from '../mock_data/diff_file';
+
+describe('DiffLineGutterContent', () => {
+ const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)];
+ const getDiffFileMock = () => Object.assign({}, diffFileMockData);
+ const createComponent = (options = {}) => {
+ const cmp = Vue.extend(DiffLineGutterContent);
+ const props = Object.assign({}, options);
+ props.fileHash = getDiffFileMock().fileHash;
+ props.contextLinesPath = '/context/lines/path';
+
+ return createComponentWithStore(cmp, store, props).$mount();
+ };
+ const setDiscussions = component => {
+ component.$store.dispatch('setInitialNotes', getDiscussionsMockData());
+ };
+
+ const resetDiscussions = component => {
+ component.$store.dispatch('setInitialNotes', []);
+ };
+
+ describe('computed', () => {
+ describe('lineHref', () => {
+ it('should prepend # to lineCode', () => {
+ const lineCode = 'LC_42';
+ const component = createComponent({ lineCode });
+ expect(component.lineHref).toEqual(`#${lineCode}`);
+ });
+
+ it('should return # if there is no lineCode', () => {
+ const component = createComponent({ lineCode: null });
+ expect(component.lineHref).toEqual('#');
+ });
+ });
+
+ describe('discussions, hasDiscussions, shouldShowAvatarsOnGutter', () => {
+ it('should return empty array when there is no discussion', () => {
+ const component = createComponent({ lineCode: 'LC_42' });
+ expect(component.discussions).toEqual([]);
+ expect(component.hasDiscussions).toEqual(false);
+ expect(component.shouldShowAvatarsOnGutter).toEqual(false);
+ });
+
+ it('should return discussions for the given lineCode', () => {
+ const { lineCode } = getDiffFileMock().highlightedDiffLines[1];
+ const component = createComponent({ lineCode, showCommentButton: true });
+
+ setDiscussions(component);
+
+ expect(component.discussions).toEqual(getDiscussionsMockData());
+ expect(component.hasDiscussions).toEqual(true);
+ expect(component.shouldShowAvatarsOnGutter).toEqual(true);
+
+ resetDiscussions(component);
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('should render three dots for context lines', () => {
+ const component = createComponent({
+ isMatchLine: true,
+ });
+
+ expect(component.$el.querySelector('span').classList.contains('context-cell')).toEqual(true);
+ expect(component.$el.innerText).toEqual('...');
+ });
+
+ it('should render comment button', () => {
+ const component = createComponent({
+ showCommentButton: true,
+ });
+ Object.defineProperty(component, 'isLoggedIn', {
+ get() {
+ return true;
+ },
+ });
+
+ expect(component.$el.querySelector('.js-add-diff-note-button')).toBeDefined();
+ });
+
+ it('should render line link', () => {
+ const lineNumber = 42;
+ const lineCode = `LC_${lineNumber}`;
+ const component = createComponent({ lineNumber, lineCode });
+ const link = component.$el.querySelector('a');
+
+ expect(link.href.indexOf(`#${lineCode}`) > -1).toEqual(true);
+ expect(link.dataset.linenumber).toEqual(lineNumber.toString());
+ });
+
+ it('should render user avatars', () => {
+ const component = createComponent({
+ showCommentButton: true,
+ lineCode: getDiffFileMock().highlightedDiffLines[1].lineCode,
+ });
+
+ setDiscussions(component);
+ expect(component.$el.querySelector('.diff-comment-avatar-holders')).toBeDefined();
+ resetDiscussions(component);
+ });
+ });
+});
diff --git a/spec/javascripts/diffs/components/diff_line_note_form_spec.js b/spec/javascripts/diffs/components/diff_line_note_form_spec.js
new file mode 100644
index 00000000000..81cd4f9769a
--- /dev/null
+++ b/spec/javascripts/diffs/components/diff_line_note_form_spec.js
@@ -0,0 +1,85 @@
+import Vue from 'vue';
+import DiffLineNoteForm from '~/diffs/components/diff_line_note_form.vue';
+import store from '~/mr_notes/stores';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import diffFileMockData from '../mock_data/diff_file';
+
+describe('DiffLineNoteForm', () => {
+ let component;
+ let diffFile;
+ let diffLines;
+ const getDiffFileMock = () => Object.assign({}, diffFileMockData);
+
+ beforeEach(() => {
+ diffFile = getDiffFileMock();
+ diffLines = diffFile.highlightedDiffLines;
+
+ component = createComponentWithStore(Vue.extend(DiffLineNoteForm), store, {
+ diffFile,
+ diffLines,
+ line: diffLines[0],
+ noteTargetLine: diffLines[0],
+ });
+
+ Object.defineProperty(component, 'isLoggedIn', {
+ get() {
+ return true;
+ },
+ });
+
+ component.$mount();
+ });
+
+ describe('methods', () => {
+ describe('handleCancelCommentForm', () => {
+ it('should call cancelCommentForm with lineCode', () => {
+ spyOn(component, 'cancelCommentForm');
+ component.handleCancelCommentForm();
+
+ expect(component.cancelCommentForm).toHaveBeenCalledWith({
+ lineCode: diffLines[0].lineCode,
+ });
+ });
+ });
+
+ describe('saveNoteForm', () => {
+ it('should call saveNote action with proper params', done => {
+ let isPromiseCalled = false;
+ const formDataSpy = spyOnDependency(DiffLineNoteForm, 'getNoteFormData').and.returnValue({
+ postData: 1,
+ });
+ const saveNoteSpy = spyOn(component, 'saveNote').and.returnValue(
+ new Promise(() => {
+ isPromiseCalled = true;
+ done();
+ }),
+ );
+
+ component.handleSaveNote('note body');
+
+ expect(formDataSpy).toHaveBeenCalled();
+ expect(saveNoteSpy).toHaveBeenCalled();
+ expect(isPromiseCalled).toEqual(true);
+ });
+ });
+ });
+
+ describe('mounted', () => {
+ it('should init autosave', () => {
+ const key = 'autosave/Note/issue///DiffNote//1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_1';
+
+ expect(component.autosave).toBeDefined();
+ expect(component.autosave.key).toEqual(key);
+ });
+ });
+
+ describe('template', () => {
+ it('should have note form', () => {
+ const { $el } = component;
+
+ expect($el.querySelector('.js-vue-textarea')).toBeDefined();
+ expect($el.querySelector('.js-vue-issue-save')).toBeDefined();
+ expect($el.querySelector('.js-vue-markdown-field')).toBeDefined();
+ });
+ });
+});
diff --git a/spec/javascripts/diffs/components/edit_button_spec.js b/spec/javascripts/diffs/components/edit_button_spec.js
new file mode 100644
index 00000000000..7237274eb43
--- /dev/null
+++ b/spec/javascripts/diffs/components/edit_button_spec.js
@@ -0,0 +1 @@
+// TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
diff --git a/spec/javascripts/diffs/components/hidden_files_warning_spec.js b/spec/javascripts/diffs/components/hidden_files_warning_spec.js
new file mode 100644
index 00000000000..7237274eb43
--- /dev/null
+++ b/spec/javascripts/diffs/components/hidden_files_warning_spec.js
@@ -0,0 +1 @@
+// TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
diff --git a/spec/javascripts/diffs/components/inline_diff_view_spec.js b/spec/javascripts/diffs/components/inline_diff_view_spec.js
new file mode 100644
index 00000000000..b02328dd359
--- /dev/null
+++ b/spec/javascripts/diffs/components/inline_diff_view_spec.js
@@ -0,0 +1,47 @@
+import Vue from 'vue';
+import InlineDiffView from '~/diffs/components/inline_diff_view.vue';
+import store from '~/mr_notes/stores';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import diffFileMockData from '../mock_data/diff_file';
+import discussionsMockData from '../mock_data/diff_discussions';
+
+describe('InlineDiffView', () => {
+ let component;
+ const getDiffFileMock = () => Object.assign({}, diffFileMockData);
+ const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)];
+
+ beforeEach(() => {
+ const diffFile = getDiffFileMock();
+
+ store.dispatch('diffs/setInlineDiffViewType');
+ component = createComponentWithStore(Vue.extend(InlineDiffView), store, {
+ diffFile,
+ diffLines: diffFile.highlightedDiffLines,
+ }).$mount();
+ });
+
+ describe('template', () => {
+ it('should have rendered diff lines', () => {
+ const el = component.$el;
+
+ expect(el.querySelectorAll('tr.line_holder').length).toEqual(6);
+ expect(el.querySelectorAll('tr.line_holder.new').length).toEqual(2);
+ expect(el.querySelectorAll('tr.line_holder.match').length).toEqual(1);
+ expect(el.textContent.indexOf('Bad dates') > -1).toEqual(true);
+ });
+
+ it('should render discussions', done => {
+ const el = component.$el;
+ component.$store.dispatch('setInitialNotes', getDiscussionsMockData());
+
+ Vue.nextTick(() => {
+ expect(el.querySelectorAll('.notes_holder').length).toEqual(1);
+ expect(el.querySelectorAll('.notes_holder .note-discussion li').length).toEqual(5);
+ expect(el.innerText.indexOf('comment 5') > -1).toEqual(true);
+ component.$store.dispatch('setInitialNotes', []);
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/diffs/components/no_changes_spec.js b/spec/javascripts/diffs/components/no_changes_spec.js
new file mode 100644
index 00000000000..7237274eb43
--- /dev/null
+++ b/spec/javascripts/diffs/components/no_changes_spec.js
@@ -0,0 +1 @@
+// TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
diff --git a/spec/javascripts/diffs/components/parallel_diff_view_spec.js b/spec/javascripts/diffs/components/parallel_diff_view_spec.js
new file mode 100644
index 00000000000..165e4b69b6c
--- /dev/null
+++ b/spec/javascripts/diffs/components/parallel_diff_view_spec.js
@@ -0,0 +1,29 @@
+import Vue from 'vue';
+import ParallelDiffView from '~/diffs/components/parallel_diff_view.vue';
+import store from '~/mr_notes/stores';
+import * as constants from '~/diffs/constants';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import diffFileMockData from '../mock_data/diff_file';
+
+describe('ParallelDiffView', () => {
+ let component;
+ const getDiffFileMock = () => Object.assign({}, diffFileMockData);
+
+ beforeEach(() => {
+ const diffFile = getDiffFileMock();
+
+ component = createComponentWithStore(Vue.extend(ParallelDiffView), store, {
+ diffFile,
+ diffLines: diffFile.parallelDiffLines,
+ }).$mount();
+ });
+
+ describe('computed', () => {
+ describe('parallelDiffLines', () => {
+ it('should normalize lines for empty cells', () => {
+ expect(component.parallelDiffLines[0].left.type).toEqual(constants.EMPTY_CELL_TYPE);
+ expect(component.parallelDiffLines[1].left.type).toEqual(constants.EMPTY_CELL_TYPE);
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/diffs/mock_data/diff_discussions.js b/spec/javascripts/diffs/mock_data/diff_discussions.js
new file mode 100644
index 00000000000..41d0dfd8939
--- /dev/null
+++ b/spec/javascripts/diffs/mock_data/diff_discussions.js
@@ -0,0 +1,496 @@
+export default {
+ id: '6b232e05bea388c6b043ccc243ba505faac04ea8',
+ reply_id: '6b232e05bea388c6b043ccc243ba505faac04ea8',
+ position: {
+ formatter: {
+ old_line: null,
+ new_line: 2,
+ old_path: 'CHANGELOG',
+ new_path: 'CHANGELOG',
+ base_sha: 'e63f41fe459e62e1228fcef60d7189127aeba95a',
+ start_sha: 'd9eaefe5a676b820c57ff18cf5b68316025f7962',
+ head_sha: 'c48ee0d1bf3b30453f5b32250ce03134beaa6d13',
+ },
+ },
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2',
+ expanded: true,
+ notes: [
+ {
+ id: 1749,
+ type: 'DiffNote',
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ created_at: '2018-04-03T21:06:21.521Z',
+ updated_at: '2018-04-08T08:50:41.762Z',
+ system: false,
+ noteable_id: 51,
+ noteable_type: 'MergeRequest',
+ noteable_iid: 20,
+ human_access: 'Owner',
+ note: 'comment 1',
+ note_html: '<p dir="auto">comment 1</p>',
+ last_edited_at: '2018-04-08T08:50:41.762Z',
+ last_edited_by: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ current_user: {
+ can_edit: true,
+ can_award_emoji: true,
+ },
+ resolved: false,
+ resolvable: true,
+ resolved_by: null,
+ discussion_id: '6b232e05bea388c6b043ccc243ba505faac04ea8',
+ emoji_awardable: true,
+ award_emoji: [],
+ toggle_award_path: '/gitlab-org/gitlab-test/notes/1749/toggle_award_emoji',
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-test%2Fmerge_requests%2F20%23note_1749&user_id=1',
+ path: '/gitlab-org/gitlab-test/notes/1749',
+ noteable_note_url: 'http://localhost:3000/gitlab-org/gitlab-test/merge_requests/20#note_1749',
+ resolve_path:
+ '/gitlab-org/gitlab-test/merge_requests/20/discussions/6b232e05bea388c6b043ccc243ba505faac04ea8/resolve',
+ resolve_with_issue_path:
+ '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20',
+ },
+ {
+ id: 1753,
+ type: 'DiffNote',
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Fatih Acet',
+ username: 'fatihacet',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/fatihacevt',
+ },
+ created_at: '2018-04-08T08:49:35.804Z',
+ updated_at: '2018-04-08T08:50:45.915Z',
+ system: false,
+ noteable_id: 51,
+ noteable_type: 'MergeRequest',
+ noteable_iid: 20,
+ human_access: 'Owner',
+ note: 'comment 2 is really long one',
+ note_html: '<p dir="auto">comment 2 is really long one</p>',
+ last_edited_at: '2018-04-08T08:50:45.915Z',
+ last_edited_by: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ current_user: {
+ can_edit: true,
+ can_award_emoji: true,
+ },
+ resolved: false,
+ resolvable: true,
+ resolved_by: null,
+ discussion_id: '6b232e05bea388c6b043ccc243ba505faac04ea8',
+ emoji_awardable: true,
+ award_emoji: [],
+ toggle_award_path: '/gitlab-org/gitlab-test/notes/1753/toggle_award_emoji',
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-test%2Fmerge_requests%2F20%23note_1753&user_id=1',
+ path: '/gitlab-org/gitlab-test/notes/1753',
+ noteable_note_url: 'http://localhost:3000/gitlab-org/gitlab-test/merge_requests/20#note_1753',
+ resolve_path:
+ '/gitlab-org/gitlab-test/merge_requests/20/discussions/6b232e05bea388c6b043ccc243ba505faac04ea8/resolve',
+ resolve_with_issue_path:
+ '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20',
+ },
+ {
+ id: 1754,
+ type: 'DiffNote',
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ created_at: '2018-04-08T08:50:48.294Z',
+ updated_at: '2018-04-08T08:50:48.294Z',
+ system: false,
+ noteable_id: 51,
+ noteable_type: 'MergeRequest',
+ noteable_iid: 20,
+ human_access: 'Owner',
+ note: 'comment 3',
+ note_html: '<p dir="auto">comment 3</p>',
+ current_user: {
+ can_edit: true,
+ can_award_emoji: true,
+ },
+ resolved: false,
+ resolvable: true,
+ resolved_by: null,
+ discussion_id: '6b232e05bea388c6b043ccc243ba505faac04ea8',
+ emoji_awardable: true,
+ award_emoji: [],
+ toggle_award_path: '/gitlab-org/gitlab-test/notes/1754/toggle_award_emoji',
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-test%2Fmerge_requests%2F20%23note_1754&user_id=1',
+ path: '/gitlab-org/gitlab-test/notes/1754',
+ noteable_note_url: 'http://localhost:3000/gitlab-org/gitlab-test/merge_requests/20#note_1754',
+ resolve_path:
+ '/gitlab-org/gitlab-test/merge_requests/20/discussions/6b232e05bea388c6b043ccc243ba505faac04ea8/resolve',
+ resolve_with_issue_path:
+ '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20',
+ },
+ {
+ id: 1755,
+ type: 'DiffNote',
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ created_at: '2018-04-08T08:50:50.911Z',
+ updated_at: '2018-04-08T08:50:50.911Z',
+ system: false,
+ noteable_id: 51,
+ noteable_type: 'MergeRequest',
+ noteable_iid: 20,
+ human_access: 'Owner',
+ note: 'comment 4',
+ note_html: '<p dir="auto">comment 4</p>',
+ current_user: {
+ can_edit: true,
+ can_award_emoji: true,
+ },
+ resolved: false,
+ resolvable: true,
+ resolved_by: null,
+ discussion_id: '6b232e05bea388c6b043ccc243ba505faac04ea8',
+ emoji_awardable: true,
+ award_emoji: [],
+ toggle_award_path: '/gitlab-org/gitlab-test/notes/1755/toggle_award_emoji',
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-test%2Fmerge_requests%2F20%23note_1755&user_id=1',
+ path: '/gitlab-org/gitlab-test/notes/1755',
+ noteable_note_url: 'http://localhost:3000/gitlab-org/gitlab-test/merge_requests/20#note_1755',
+ resolve_path:
+ '/gitlab-org/gitlab-test/merge_requests/20/discussions/6b232e05bea388c6b043ccc243ba505faac04ea8/resolve',
+ resolve_with_issue_path:
+ '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20',
+ },
+ {
+ id: 1756,
+ type: 'DiffNote',
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ created_at: '2018-04-08T08:50:53.895Z',
+ updated_at: '2018-04-08T08:50:53.895Z',
+ system: false,
+ noteable_id: 51,
+ noteable_type: 'MergeRequest',
+ noteable_iid: 20,
+ human_access: 'Owner',
+ note: 'comment 5',
+ note_html: '<p dir="auto">comment 5</p>',
+ current_user: {
+ can_edit: true,
+ can_award_emoji: true,
+ },
+ resolved: false,
+ resolvable: true,
+ resolved_by: null,
+ discussion_id: '6b232e05bea388c6b043ccc243ba505faac04ea8',
+ emoji_awardable: true,
+ award_emoji: [],
+ toggle_award_path: '/gitlab-org/gitlab-test/notes/1756/toggle_award_emoji',
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-test%2Fmerge_requests%2F20%23note_1756&user_id=1',
+ path: '/gitlab-org/gitlab-test/notes/1756',
+ noteable_note_url: 'http://localhost:3000/gitlab-org/gitlab-test/merge_requests/20#note_1756',
+ resolve_path:
+ '/gitlab-org/gitlab-test/merge_requests/20/discussions/6b232e05bea388c6b043ccc243ba505faac04ea8/resolve',
+ resolve_with_issue_path:
+ '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20',
+ },
+ ],
+ individual_note: false,
+ resolvable: true,
+ resolved: false,
+ resolve_path:
+ '/gitlab-org/gitlab-test/merge_requests/20/discussions/6b232e05bea388c6b043ccc243ba505faac04ea8/resolve',
+ resolve_with_issue_path:
+ '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20',
+ diff_file: {
+ submodule: false,
+ submodule_link: null,
+ blob: {
+ id: '9e10516ca50788acf18c518a231914a21e5f16f7',
+ path: 'CHANGELOG',
+ name: 'CHANGELOG',
+ mode: '100644',
+ readable_text: true,
+ icon: 'file-text-o',
+ },
+ blob_path: 'CHANGELOG',
+ blob_name: 'CHANGELOG',
+ blob_icon: '<i aria-hidden="true" data-hidden="true" class="fa fa-file-text-o fa-fw"></i>',
+ file_hash: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a',
+ file_path: 'CHANGELOG',
+ new_file: false,
+ deleted_file: false,
+ renamed_file: false,
+ old_path: 'CHANGELOG',
+ new_path: 'CHANGELOG',
+ mode_changed: false,
+ a_mode: '100644',
+ b_mode: '100644',
+ text: true,
+ added_lines: 2,
+ removed_lines: 0,
+ diff_refs: {
+ base_sha: 'e63f41fe459e62e1228fcef60d7189127aeba95a',
+ start_sha: 'd9eaefe5a676b820c57ff18cf5b68316025f7962',
+ head_sha: 'c48ee0d1bf3b30453f5b32250ce03134beaa6d13',
+ },
+ content_sha: 'c48ee0d1bf3b30453f5b32250ce03134beaa6d13',
+ stored_externally: null,
+ external_storage: null,
+ old_path_html: ['CHANGELOG', 'CHANGELOG'],
+ new_path_html: 'CHANGELOG',
+ context_lines_path:
+ '/gitlab-org/gitlab-test/blob/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG/diff',
+ highlighted_diff_lines: [
+ {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_1',
+ type: 'new',
+ old_line: null,
+ new_line: 1,
+ text: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
+ rich_text: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
+ meta_data: null,
+ },
+ {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2',
+ type: 'new',
+ old_line: null,
+ new_line: 2,
+ text: '<span id="LC2" class="line" lang="plaintext"></span>\n',
+ rich_text: '<span id="LC2" class="line" lang="plaintext"></span>\n',
+ meta_data: null,
+ },
+ {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3',
+ type: null,
+ old_line: 1,
+ new_line: 3,
+ text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ rich_text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ meta_data: null,
+ },
+ {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4',
+ type: null,
+ old_line: 2,
+ new_line: 4,
+ text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
+ rich_text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
+ meta_data: null,
+ },
+ {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5',
+ type: null,
+ old_line: 3,
+ new_line: 5,
+ text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ rich_text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ meta_data: null,
+ },
+ {
+ line_code: null,
+ type: 'match',
+ old_line: null,
+ new_line: null,
+ text: '',
+ rich_text: '',
+ meta_data: {
+ old_pos: 3,
+ new_pos: 5,
+ },
+ },
+ {
+ line_code: null,
+ type: 'match',
+ old_line: null,
+ new_line: null,
+ text: '',
+ rich_text: '',
+ meta_data: {
+ old_pos: 3,
+ new_pos: 5,
+ },
+ },
+ {
+ line_code: null,
+ type: 'match',
+ old_line: null,
+ new_line: null,
+ text: '',
+ rich_text: '',
+ meta_data: {
+ old_pos: 3,
+ new_pos: 5,
+ },
+ },
+ ],
+ parallel_diff_lines: [
+ {
+ left: null,
+ right: {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_1',
+ type: 'new',
+ old_line: null,
+ new_line: 1,
+ text: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
+ rich_text: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
+ meta_data: null,
+ },
+ },
+ {
+ left: null,
+ right: {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2',
+ type: 'new',
+ old_line: null,
+ new_line: 2,
+ text: '<span id="LC2" class="line" lang="plaintext"></span>\n',
+ rich_text: '<span id="LC2" class="line" lang="plaintext"></span>\n',
+ meta_data: null,
+ },
+ },
+ {
+ left: {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3',
+ type: null,
+ old_line: 1,
+ new_line: 3,
+ text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ rich_text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ meta_data: null,
+ },
+ right: {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3',
+ type: null,
+ old_line: 1,
+ new_line: 3,
+ text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ rich_text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ meta_data: null,
+ },
+ },
+ {
+ left: {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4',
+ type: null,
+ old_line: 2,
+ new_line: 4,
+ text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
+ rich_text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
+ meta_data: null,
+ },
+ right: {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4',
+ type: null,
+ old_line: 2,
+ new_line: 4,
+ text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
+ rich_text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
+ meta_data: null,
+ },
+ },
+ {
+ left: {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5',
+ type: null,
+ old_line: 3,
+ new_line: 5,
+ text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ rich_text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ meta_data: null,
+ },
+ right: {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5',
+ type: null,
+ old_line: 3,
+ new_line: 5,
+ text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ rich_text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ meta_data: null,
+ },
+ },
+ {
+ left: {
+ line_code: null,
+ type: 'match',
+ old_line: null,
+ new_line: null,
+ text: '',
+ rich_text: '',
+ meta_data: {
+ old_pos: 3,
+ new_pos: 5,
+ },
+ },
+ right: {
+ line_code: null,
+ type: 'match',
+ old_line: null,
+ new_line: null,
+ text: '',
+ rich_text: '',
+ meta_data: {
+ old_pos: 3,
+ new_pos: 5,
+ },
+ },
+ },
+ ],
+ },
+ diff_discussion: true,
+ truncated_diff_lines:
+ '<tr class="line_holder new" id="">\n<td class="diff-line-num new old_line" data-linenumber="1">\n \n</td>\n<td class="diff-line-num new new_line" data-linenumber="1">\n1\n</td>\n<td class="line_content new noteable_line"><span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n</td>\n</tr>\n<tr class="line_holder new" id="">\n<td class="diff-line-num new old_line" data-linenumber="1">\n \n</td>\n<td class="diff-line-num new new_line" data-linenumber="2">\n2\n</td>\n<td class="line_content new noteable_line"><span id="LC2" class="line" lang="plaintext"></span>\n</td>\n</tr>\n',
+ image_diff_html:
+ '<div class="image js-replaced-image" data="">\n<div class="two-up view">\n<div class="wrap">\n<div class="frame deleted">\n<img alt="CHANGELOG" src="http://localhost:3000/gitlab-org/gitlab-test/raw/e63f41fe459e62e1228fcef60d7189127aeba95a/CHANGELOG" />\n</div>\n<p class="image-info hide">\n<span class="meta-filesize">22.3 KB</span>\n|\n<strong>W:</strong>\n<span class="meta-width"></span>\n|\n<strong>H:</strong>\n<span class="meta-height"></span>\n</p>\n</div>\n<div class="wrap">\n<div class="added frame js-image-frame" data-note-type="DiffNote" data-position="{&quot;base_sha&quot;:&quot;e63f41fe459e62e1228fcef60d7189127aeba95a&quot;,&quot;start_sha&quot;:&quot;d9eaefe5a676b820c57ff18cf5b68316025f7962&quot;,&quot;head_sha&quot;:&quot;c48ee0d1bf3b30453f5b32250ce03134beaa6d13&quot;,&quot;old_path&quot;:&quot;CHANGELOG&quot;,&quot;new_path&quot;:&quot;CHANGELOG&quot;,&quot;position_type&quot;:&quot;text&quot;,&quot;old_line&quot;:null,&quot;new_line&quot;:2}">\n<img alt="CHANGELOG" draggable="false" src="http://localhost:3000/gitlab-org/gitlab-test/raw/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG" />\n</div>\n\n<p class="image-info hide">\n<span class="meta-filesize">22.3 KB</span>\n|\n<strong>W:</strong>\n<span class="meta-width"></span>\n|\n<strong>H:</strong>\n<span class="meta-height"></span>\n</p>\n</div>\n</div>\n<div class="swipe view hide">\n<div class="swipe-frame">\n<div class="frame deleted">\n<img alt="CHANGELOG" src="http://localhost:3000/gitlab-org/gitlab-test/raw/e63f41fe459e62e1228fcef60d7189127aeba95a/CHANGELOG" />\n</div>\n<div class="swipe-wrap">\n<div class="added frame js-image-frame" data-note-type="DiffNote" data-position="{&quot;base_sha&quot;:&quot;e63f41fe459e62e1228fcef60d7189127aeba95a&quot;,&quot;start_sha&quot;:&quot;d9eaefe5a676b820c57ff18cf5b68316025f7962&quot;,&quot;head_sha&quot;:&quot;c48ee0d1bf3b30453f5b32250ce03134beaa6d13&quot;,&quot;old_path&quot;:&quot;CHANGELOG&quot;,&quot;new_path&quot;:&quot;CHANGELOG&quot;,&quot;position_type&quot;:&quot;text&quot;,&quot;old_line&quot;:null,&quot;new_line&quot;:2}">\n<img alt="CHANGELOG" draggable="false" src="http://localhost:3000/gitlab-org/gitlab-test/raw/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG" />\n</div>\n\n</div>\n<span class="swipe-bar">\n<span class="top-handle"></span>\n<span class="bottom-handle"></span>\n</span>\n</div>\n</div>\n<div class="onion-skin view hide">\n<div class="onion-skin-frame">\n<div class="frame deleted">\n<img alt="CHANGELOG" src="http://localhost:3000/gitlab-org/gitlab-test/raw/e63f41fe459e62e1228fcef60d7189127aeba95a/CHANGELOG" />\n</div>\n<div class="added frame js-image-frame" data-note-type="DiffNote" data-position="{&quot;base_sha&quot;:&quot;e63f41fe459e62e1228fcef60d7189127aeba95a&quot;,&quot;start_sha&quot;:&quot;d9eaefe5a676b820c57ff18cf5b68316025f7962&quot;,&quot;head_sha&quot;:&quot;c48ee0d1bf3b30453f5b32250ce03134beaa6d13&quot;,&quot;old_path&quot;:&quot;CHANGELOG&quot;,&quot;new_path&quot;:&quot;CHANGELOG&quot;,&quot;position_type&quot;:&quot;text&quot;,&quot;old_line&quot;:null,&quot;new_line&quot;:2}">\n<img alt="CHANGELOG" draggable="false" src="http://localhost:3000/gitlab-org/gitlab-test/raw/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG" />\n</div>\n\n<div class="controls">\n<div class="transparent"></div>\n<div class="drag-track">\n<div class="dragger" style="left: 0px;"></div>\n</div>\n<div class="opaque"></div>\n</div>\n</div>\n</div>\n</div>\n<div class="view-modes hide">\n<ul class="view-modes-menu">\n<li class="two-up" data-mode="two-up">2-up</li>\n<li class="swipe" data-mode="swipe">Swipe</li>\n<li class="onion-skin" data-mode="onion-skin">Onion skin</li>\n</ul>\n</div>\n',
+};
diff --git a/spec/javascripts/diffs/mock_data/diff_file.js b/spec/javascripts/diffs/mock_data/diff_file.js
new file mode 100644
index 00000000000..d3bf9525924
--- /dev/null
+++ b/spec/javascripts/diffs/mock_data/diff_file.js
@@ -0,0 +1,220 @@
+export default {
+ submodule: false,
+ submoduleLink: null,
+ blob: {
+ id: '9e10516ca50788acf18c518a231914a21e5f16f7',
+ path: 'CHANGELOG',
+ name: 'CHANGELOG',
+ mode: '100644',
+ readableText: true,
+ icon: 'file-text-o',
+ },
+ blobPath: 'CHANGELOG',
+ blobName: 'CHANGELOG',
+ blobIcon: '<i aria-hidden="true" data-hidden="true" class="fa fa-file-text-o fa-fw"></i>',
+ fileHash: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a',
+ filePath: 'CHANGELOG',
+ newFile: false,
+ deletedFile: false,
+ renamedFile: false,
+ oldPath: 'CHANGELOG',
+ newPath: 'CHANGELOG',
+ modeChanged: false,
+ aMode: '100644',
+ bMode: '100644',
+ text: true,
+ addedLines: 2,
+ removedLines: 0,
+ diffRefs: {
+ baseSha: 'e63f41fe459e62e1228fcef60d7189127aeba95a',
+ startSha: 'd9eaefe5a676b820c57ff18cf5b68316025f7962',
+ headSha: 'c48ee0d1bf3b30453f5b32250ce03134beaa6d13',
+ },
+ contentSha: 'c48ee0d1bf3b30453f5b32250ce03134beaa6d13',
+ storedExternally: null,
+ externalStorage: null,
+ oldPathHtml: ['CHANGELOG', 'CHANGELOG'],
+ newPathHtml: 'CHANGELOG',
+ editPath: '/gitlab-org/gitlab-test/edit/spooky-stuff/CHANGELOG',
+ viewPath: '/gitlab-org/gitlab-test/blob/spooky-stuff/CHANGELOG',
+ replacedViewPath: null,
+ collapsed: false,
+ tooLarge: false,
+ contextLinesPath:
+ '/gitlab-org/gitlab-test/blob/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG/diff',
+ highlightedDiffLines: [
+ {
+ lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_1',
+ type: 'new',
+ oldLine: null,
+ newLine: 1,
+ text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
+ richText: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
+ metaData: null,
+ },
+ {
+ lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2',
+ type: 'new',
+ oldLine: null,
+ newLine: 2,
+ text: '+<span id="LC2" class="line" lang="plaintext"></span>\n',
+ richText: '+<span id="LC2" class="line" lang="plaintext"></span>\n',
+ metaData: null,
+ },
+ {
+ lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3',
+ type: null,
+ oldLine: 1,
+ newLine: 3,
+ text: ' <span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ richText: ' <span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ metaData: null,
+ },
+ {
+ lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4',
+ type: null,
+ oldLine: 2,
+ newLine: 4,
+ text: ' <span id="LC4" class="line" lang="plaintext"></span>\n',
+ richText: ' <span id="LC4" class="line" lang="plaintext"></span>\n',
+ metaData: null,
+ },
+ {
+ lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5',
+ type: null,
+ oldLine: 3,
+ newLine: 5,
+ text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ richText: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ metaData: null,
+ },
+ {
+ lineCode: null,
+ type: 'match',
+ oldLine: null,
+ newLine: null,
+ text: '',
+ richText: '',
+ metaData: {
+ oldPos: 3,
+ newPos: 5,
+ },
+ },
+ ],
+ parallelDiffLines: [
+ {
+ left: {
+ type: 'empty-cell',
+ },
+ right: {
+ lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_1',
+ type: 'new',
+ oldLine: null,
+ newLine: 1,
+ text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
+ richText: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
+ metaData: null,
+ },
+ },
+ {
+ left: {
+ type: 'empty-cell',
+ },
+ right: {
+ lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2',
+ type: 'new',
+ oldLine: null,
+ newLine: 2,
+ text: '+<span id="LC2" class="line" lang="plaintext"></span>\n',
+ richText: '<span id="LC2" class="line" lang="plaintext"></span>\n',
+ metaData: null,
+ },
+ },
+ {
+ left: {
+ lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3',
+ type: null,
+ oldLine: 1,
+ newLine: 3,
+ text: ' <span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ richText: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ metaData: null,
+ },
+ right: {
+ lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3',
+ type: null,
+ oldLine: 1,
+ newLine: 3,
+ text: ' <span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ richText: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ metaData: null,
+ },
+ },
+ {
+ left: {
+ lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4',
+ type: null,
+ oldLine: 2,
+ newLine: 4,
+ text: ' <span id="LC4" class="line" lang="plaintext"></span>\n',
+ richText: '<span id="LC4" class="line" lang="plaintext"></span>\n',
+ metaData: null,
+ },
+ right: {
+ lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4',
+ type: null,
+ oldLine: 2,
+ newLine: 4,
+ text: ' <span id="LC4" class="line" lang="plaintext"></span>\n',
+ richText: '<span id="LC4" class="line" lang="plaintext"></span>\n',
+ metaData: null,
+ },
+ },
+ {
+ left: {
+ lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5',
+ type: null,
+ oldLine: 3,
+ newLine: 5,
+ text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ richText: '<span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ metaData: null,
+ },
+ right: {
+ lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5',
+ type: null,
+ oldLine: 3,
+ newLine: 5,
+ text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ richText: '<span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ metaData: null,
+ },
+ },
+ {
+ left: {
+ lineCode: null,
+ type: 'match',
+ oldLine: null,
+ newLine: null,
+ text: '',
+ richText: '',
+ metaData: {
+ oldPos: 3,
+ newPos: 5,
+ },
+ },
+ right: {
+ lineCode: null,
+ type: 'match',
+ oldLine: null,
+ newLine: null,
+ text: '',
+ richText: '',
+ metaData: {
+ oldPos: 3,
+ newPos: 5,
+ },
+ },
+ },
+ ],
+};
diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js
new file mode 100644
index 00000000000..c1560dac1a0
--- /dev/null
+++ b/spec/javascripts/diffs/store/actions_spec.js
@@ -0,0 +1,238 @@
+import MockAdapter from 'axios-mock-adapter';
+import Cookies from 'js-cookie';
+import {
+ DIFF_VIEW_COOKIE_NAME,
+ INLINE_DIFF_VIEW_TYPE,
+ PARALLEL_DIFF_VIEW_TYPE,
+} from '~/diffs/constants';
+import * as actions from '~/diffs/store/actions';
+import * as types from '~/diffs/store/mutation_types';
+import axios from '~/lib/utils/axios_utils';
+import testAction from '../../helpers/vuex_action_helper';
+
+describe('DiffsStoreActions', () => {
+ describe('setBaseConfig', () => {
+ it('should set given endpoint and project path', done => {
+ const endpoint = '/diffs/set/endpoint';
+ const projectPath = '/root/project';
+
+ testAction(
+ actions.setBaseConfig,
+ { endpoint, projectPath },
+ { endpoint: '', projectPath: '' },
+ [{ type: types.SET_BASE_CONFIG, payload: { endpoint, projectPath } }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('fetchDiffFiles', () => {
+ it('should fetch diff files', done => {
+ const endpoint = '/fetch/diff/files';
+ const mock = new MockAdapter(axios);
+ const res = { diff_files: 1, merge_request_diffs: [] };
+ mock.onGet(endpoint).reply(200, res);
+
+ testAction(
+ actions.fetchDiffFiles,
+ {},
+ { endpoint },
+ [
+ { type: types.SET_LOADING, payload: true },
+ { type: types.SET_LOADING, payload: false },
+ { type: types.SET_MERGE_REQUEST_DIFFS, payload: res.merge_request_diffs },
+ { type: types.SET_DIFF_DATA, payload: res },
+ ],
+ [],
+ () => {
+ mock.restore();
+ done();
+ },
+ );
+ });
+ });
+
+ describe('setInlineDiffViewType', () => {
+ it('should set diff view type to inline and also set the cookie properly', done => {
+ testAction(
+ actions.setInlineDiffViewType,
+ null,
+ {},
+ [{ type: types.SET_DIFF_VIEW_TYPE, payload: INLINE_DIFF_VIEW_TYPE }],
+ [],
+ () => {
+ setTimeout(() => {
+ expect(Cookies.get('diff_view')).toEqual(INLINE_DIFF_VIEW_TYPE);
+ done();
+ }, 0);
+ },
+ );
+ });
+ });
+
+ describe('setParallelDiffViewType', () => {
+ it('should set diff view type to parallel and also set the cookie properly', done => {
+ testAction(
+ actions.setParallelDiffViewType,
+ null,
+ {},
+ [{ type: types.SET_DIFF_VIEW_TYPE, payload: PARALLEL_DIFF_VIEW_TYPE }],
+ [],
+ () => {
+ setTimeout(() => {
+ expect(Cookies.get(DIFF_VIEW_COOKIE_NAME)).toEqual(PARALLEL_DIFF_VIEW_TYPE);
+ done();
+ }, 0);
+ },
+ );
+ });
+ });
+
+ describe('showCommentForm', () => {
+ it('should call mutation to show comment form', done => {
+ const payload = { lineCode: 'lineCode' };
+
+ testAction(
+ actions.showCommentForm,
+ payload,
+ {},
+ [{ type: types.ADD_COMMENT_FORM_LINE, payload }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('cancelCommentForm', () => {
+ it('should call mutation to cancel comment form', done => {
+ const payload = { lineCode: 'lineCode' };
+
+ testAction(
+ actions.cancelCommentForm,
+ payload,
+ {},
+ [{ type: types.REMOVE_COMMENT_FORM_LINE, payload }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('loadMoreLines', () => {
+ it('should call mutation to show comment form', done => {
+ const endpoint = '/diffs/load/more/lines';
+ const params = { since: 6, to: 26 };
+ const lineNumbers = { oldLineNumber: 3, newLineNumber: 5 };
+ const fileHash = 'ff9200';
+ const options = { endpoint, params, lineNumbers, fileHash };
+ const mock = new MockAdapter(axios);
+ const contextLines = { contextLines: [{ lineCode: 6 }] };
+ mock.onGet(endpoint).reply(200, contextLines);
+
+ testAction(
+ actions.loadMoreLines,
+ options,
+ {},
+ [
+ {
+ type: types.ADD_CONTEXT_LINES,
+ payload: { lineNumbers, contextLines, params, fileHash },
+ },
+ ],
+ [],
+ () => {
+ mock.restore();
+ done();
+ },
+ );
+ });
+ });
+
+ describe('loadCollapsedDiff', () => {
+ it('should fetch data and call mutation with response and the give parameter', done => {
+ const file = { hash: 123, loadCollapsedDiffUrl: '/load/collapsed/diff/url' };
+ const data = { hash: 123, parallelDiffLines: [{ lineCode: 1 }] };
+ const mock = new MockAdapter(axios);
+ mock.onGet(file.loadCollapsedDiffUrl).reply(200, data);
+
+ testAction(
+ actions.loadCollapsedDiff,
+ file,
+ {},
+ [
+ {
+ type: types.ADD_COLLAPSED_DIFFS,
+ payload: { file, data },
+ },
+ ],
+ [],
+ () => {
+ mock.restore();
+ done();
+ },
+ );
+ });
+ });
+
+ describe('expandAllFiles', () => {
+ it('should change the collapsed prop from the diffFiles', done => {
+ testAction(
+ actions.expandAllFiles,
+ null,
+ {},
+ [
+ {
+ type: types.EXPAND_ALL_FILES,
+ },
+ ],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('toggleFileDiscussions', () => {
+ it('should dispatch collapseDiscussion when all discussions are expanded', () => {
+ const getters = {
+ getDiffFileDiscussions: jasmine.createSpy().and.returnValue([{ id: 1 }]),
+ diffHasAllExpandedDiscussions: jasmine.createSpy().and.returnValue(true),
+ diffHasAllCollpasedDiscussions: jasmine.createSpy().and.returnValue(false),
+ };
+
+ const dispatch = jasmine.createSpy('dispatch');
+
+ actions.toggleFileDiscussions({ getters, dispatch });
+
+ expect(dispatch).toHaveBeenCalledWith('collapseDiscussion', { discussionId: 1 }, { root: true });
+ });
+
+ it('should dispatch expandDiscussion when all discussions are collapsed', () => {
+ const getters = {
+ getDiffFileDiscussions: jasmine.createSpy().and.returnValue([{ id: 1 }]),
+ diffHasAllExpandedDiscussions: jasmine.createSpy().and.returnValue(false),
+ diffHasAllCollpasedDiscussions: jasmine.createSpy().and.returnValue(true),
+ };
+
+ const dispatch = jasmine.createSpy();
+
+ actions.toggleFileDiscussions({ getters, dispatch });
+
+ expect(dispatch).toHaveBeenCalledWith('expandDiscussion', { discussionId: 1 }, { root: true });
+ });
+
+ it('should dispatch expandDiscussion when some discussions are collapsed and others are expanded for the collapsed discussion', () => {
+ const getters = {
+ getDiffFileDiscussions: jasmine.createSpy().and.returnValue([{ expanded: false, id: 1 }]),
+ diffHasAllExpandedDiscussions: jasmine.createSpy().and.returnValue(false),
+ diffHasAllCollpasedDiscussions: jasmine.createSpy().and.returnValue(false),
+ };
+
+ const dispatch = jasmine.createSpy();
+
+ actions.toggleFileDiscussions({ getters, dispatch });
+
+ expect(dispatch).toHaveBeenCalledWith('expandDiscussion', { discussionId: 1 }, { root: true });
+ });
+ });
+});
diff --git a/spec/javascripts/diffs/store/getters_spec.js b/spec/javascripts/diffs/store/getters_spec.js
new file mode 100644
index 00000000000..919b612bb6a
--- /dev/null
+++ b/spec/javascripts/diffs/store/getters_spec.js
@@ -0,0 +1,187 @@
+import * as getters from '~/diffs/store/getters';
+import state from '~/diffs/store/modules/diff_state';
+import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '~/diffs/constants';
+import discussion from '../mock_data/diff_discussions';
+
+describe('Diffs Module Getters', () => {
+ let localState;
+ let discussionMock;
+ let discussionMock1;
+
+ const diffFileMock = {
+ fileHash: '9732849daca6ae818696d9575f5d1207d1a7f8bb',
+ };
+
+ beforeEach(() => {
+ localState = state();
+ discussionMock = Object.assign({}, discussion);
+ discussionMock.diff_file.file_hash = diffFileMock.fileHash;
+
+ discussionMock1 = Object.assign({}, discussion);
+ discussionMock1.diff_file.file_hash = diffFileMock.fileHash;
+ });
+
+ describe('isParallelView', () => {
+ it('should return true if view set to parallel view', () => {
+ localState.diffViewType = PARALLEL_DIFF_VIEW_TYPE;
+
+ expect(getters.isParallelView(localState)).toEqual(true);
+ });
+
+ it('should return false if view not to parallel view', () => {
+ localState.diffViewType = INLINE_DIFF_VIEW_TYPE;
+
+ expect(getters.isParallelView(localState)).toEqual(false);
+ });
+ });
+
+ describe('isInlineView', () => {
+ it('should return true if view set to inline view', () => {
+ localState.diffViewType = INLINE_DIFF_VIEW_TYPE;
+
+ expect(getters.isInlineView(localState)).toEqual(true);
+ });
+
+ it('should return false if view not to inline view', () => {
+ localState.diffViewType = PARALLEL_DIFF_VIEW_TYPE;
+
+ expect(getters.isInlineView(localState)).toEqual(false);
+ });
+ });
+
+ describe('areAllFilesCollapsed', () => {
+ it('returns true when all files are collapsed', () => {
+ localState.diffFiles = [{ collapsed: true }, { collapsed: true }];
+ expect(getters.areAllFilesCollapsed(localState)).toEqual(true);
+ });
+
+ it('returns false when at least one file is not collapsed', () => {
+ localState.diffFiles = [{ collapsed: false }, { collapsed: true }];
+ expect(getters.areAllFilesCollapsed(localState)).toEqual(false);
+ });
+ });
+
+ describe('commitId', () => {
+ it('returns commit id when is set', () => {
+ const commitID = '800f7a91';
+ localState.commit = {
+ id: commitID,
+ };
+
+ expect(getters.commitId(localState)).toEqual(commitID);
+ });
+
+ it('returns null when no commit is set', () => {
+ expect(getters.commitId(localState)).toEqual(null);
+ });
+ });
+
+ describe('diffHasAllExpandedDiscussions', () => {
+ it('returns true when all discussions are expanded', () => {
+ expect(
+ getters.diffHasAllExpandedDiscussions(localState, {
+ getDiffFileDiscussions: () => [discussionMock, discussionMock],
+ })(diffFileMock),
+ ).toEqual(true);
+ });
+
+ it('returns false when there are no discussions', () => {
+ expect(
+ getters.diffHasAllExpandedDiscussions(localState, {
+ getDiffFileDiscussions: () => [],
+ })(diffFileMock),
+ ).toEqual(false);
+ });
+
+ it('returns false when one discussions is collapsed', () => {
+ discussionMock1.expanded = false;
+
+ expect(
+ getters.diffHasAllExpandedDiscussions(localState, {
+ getDiffFileDiscussions: () => [discussionMock, discussionMock1],
+ })(diffFileMock),
+ ).toEqual(false);
+ });
+ });
+
+ describe('diffHasAllCollpasedDiscussions', () => {
+ it('returns true when all discussions are collapsed', () => {
+ discussionMock.diff_file.file_hash = diffFileMock.fileHash;
+ discussionMock.expanded = false;
+
+ expect(
+ getters.diffHasAllCollpasedDiscussions(localState, {
+ getDiffFileDiscussions: () => [discussionMock],
+ })(diffFileMock),
+ ).toEqual(true);
+ });
+
+ it('returns false when there are no discussions', () => {
+ expect(
+ getters.diffHasAllCollpasedDiscussions(localState, {
+ getDiffFileDiscussions: () => [],
+ })(diffFileMock),
+ ).toEqual(false);
+ });
+
+ it('returns false when one discussions is expanded', () => {
+ discussionMock1.expanded = false;
+
+ expect(
+ getters.diffHasAllCollpasedDiscussions(localState, {
+ getDiffFileDiscussions: () => [discussionMock, discussionMock1],
+ })(diffFileMock),
+ ).toEqual(false);
+ });
+ });
+
+ describe('diffHasExpandedDiscussions', () => {
+ it('returns true when one of the discussions is expanded', () => {
+ discussionMock1.expanded = false;
+
+ expect(
+ getters.diffHasExpandedDiscussions(localState, {
+ getDiffFileDiscussions: () => [discussionMock, discussionMock],
+ })(diffFileMock),
+ ).toEqual(true);
+ });
+
+ it('returns false when there are no discussions', () => {
+ expect(
+ getters.diffHasExpandedDiscussions(localState, { getDiffFileDiscussions: () => [] })(
+ diffFileMock,
+ ),
+ ).toEqual(false);
+ });
+
+ it('returns false when no discussion is expanded', () => {
+ discussionMock.expanded = false;
+ discussionMock1.expanded = false;
+
+ expect(
+ getters.diffHasExpandedDiscussions(localState, {
+ getDiffFileDiscussions: () => [discussionMock, discussionMock1],
+ })(diffFileMock),
+ ).toEqual(false);
+ });
+ });
+
+ describe('getDiffFileDiscussions', () => {
+ it('returns an array with discussions when fileHash matches and the discussion belongs to a diff', () => {
+ discussionMock.diff_file.file_hash = diffFileMock.fileHash;
+
+ expect(
+ getters.getDiffFileDiscussions(localState, {}, {}, { discussions: [discussionMock] })(
+ diffFileMock,
+ ).length,
+ ).toEqual(1);
+ });
+
+ it('returns an empty array when no discussions are found in the given diff', () => {
+ expect(
+ getters.getDiffFileDiscussions(localState, {}, {}, { discussions: [] })(diffFileMock)
+ .length,
+ ).toEqual(0);
+ });
+ });
+});
diff --git a/spec/javascripts/diffs/store/mutations_spec.js b/spec/javascripts/diffs/store/mutations_spec.js
new file mode 100644
index 00000000000..1af49f4985c
--- /dev/null
+++ b/spec/javascripts/diffs/store/mutations_spec.js
@@ -0,0 +1,134 @@
+import mutations from '~/diffs/store/mutations';
+import * as types from '~/diffs/store/mutation_types';
+import { INLINE_DIFF_VIEW_TYPE } from '~/diffs/constants';
+
+describe('DiffsStoreMutations', () => {
+ describe('SET_BASE_CONFIG', () => {
+ it('should set endpoint and project path', () => {
+ const state = {};
+ const endpoint = '/diffs/endpoint';
+ const projectPath = '/root/project';
+
+ mutations[types.SET_BASE_CONFIG](state, { endpoint, projectPath });
+ expect(state.endpoint).toEqual(endpoint);
+ expect(state.projectPath).toEqual(projectPath);
+ });
+ });
+
+ describe('SET_LOADING', () => {
+ it('should set loading state', () => {
+ const state = {};
+
+ mutations[types.SET_LOADING](state, false);
+ expect(state.isLoading).toEqual(false);
+ });
+ });
+
+ describe('SET_DIFF_VIEW_TYPE', () => {
+ it('should set diff view type properly', () => {
+ const state = {};
+
+ mutations[types.SET_DIFF_VIEW_TYPE](state, INLINE_DIFF_VIEW_TYPE);
+ expect(state.diffViewType).toEqual(INLINE_DIFF_VIEW_TYPE);
+ });
+ });
+
+ describe('ADD_COMMENT_FORM_LINE', () => {
+ it('should set a truthy reference for the given line code in diffLineCommentForms', () => {
+ const state = { diffLineCommentForms: {} };
+ const lineCode = 'FDE';
+
+ mutations[types.ADD_COMMENT_FORM_LINE](state, { lineCode });
+ expect(state.diffLineCommentForms[lineCode]).toBeTruthy();
+ });
+ });
+
+ describe('REMOVE_COMMENT_FORM_LINE', () => {
+ it('should remove given reference from diffLineCommentForms', () => {
+ const state = { diffLineCommentForms: {} };
+ const lineCode = 'FDE';
+
+ mutations[types.ADD_COMMENT_FORM_LINE](state, { lineCode });
+ expect(state.diffLineCommentForms[lineCode]).toBeTruthy();
+
+ mutations[types.REMOVE_COMMENT_FORM_LINE](state, { lineCode });
+ expect(state.diffLineCommentForms[lineCode]).toBeUndefined();
+ });
+ });
+
+ describe('EXPAND_ALL_FILES', () => {
+ it('should change the collapsed prop from diffFiles', () => {
+ const diffFile = {
+ collapsed: true,
+ };
+ const state = { expandAllFiles: true, diffFiles: [diffFile] };
+
+ mutations[types.EXPAND_ALL_FILES](state);
+ expect(state.diffFiles[0].collapsed).toEqual(false);
+ });
+ });
+
+ describe('ADD_CONTEXT_LINES', () => {
+ it('should call utils.addContextLines with proper params', () => {
+ const options = {
+ lineNumbers: { oldLineNumber: 1, newLineNumber: 2 },
+ contextLines: [{ oldLine: 1 }],
+ fileHash: 'ff9200',
+ params: {
+ bottom: true,
+ },
+ };
+ const diffFile = {
+ fileHash: options.fileHash,
+ highlightedDiffLines: [],
+ parallelDiffLines: [],
+ };
+ const state = { diffFiles: [diffFile] };
+ const lines = [{ oldLine: 1 }];
+
+ const findDiffFileSpy = spyOnDependency(mutations, 'findDiffFile').and.returnValue(diffFile);
+ const removeMatchLineSpy = spyOnDependency(mutations, 'removeMatchLine');
+ const lineRefSpy = spyOnDependency(mutations, 'addLineReferences').and.returnValue(lines);
+ const addContextLinesSpy = spyOnDependency(mutations, 'addContextLines');
+
+ mutations[types.ADD_CONTEXT_LINES](state, options);
+
+ expect(findDiffFileSpy).toHaveBeenCalledWith(state.diffFiles, options.fileHash);
+ expect(removeMatchLineSpy).toHaveBeenCalledWith(
+ diffFile,
+ options.lineNumbers,
+ options.params.bottom,
+ );
+ expect(lineRefSpy).toHaveBeenCalledWith(
+ options.contextLines,
+ options.lineNumbers,
+ options.params.bottom,
+ );
+ expect(addContextLinesSpy).toHaveBeenCalledWith({
+ inlineLines: diffFile.highlightedDiffLines,
+ parallelLines: diffFile.parallelDiffLines,
+ contextLines: options.contextLines,
+ bottom: options.params.bottom,
+ lineNumbers: options.lineNumbers,
+ });
+ });
+ });
+
+ describe('ADD_COLLAPSED_DIFFS', () => {
+ it('should update the state with the given data for the given file hash', () => {
+ const spy = spyOnDependency(mutations, 'convertObjectPropsToCamelCase').and.callThrough();
+
+ const fileHash = 123;
+ const state = { diffFiles: [{}, { fileHash, existingField: 0 }] };
+ const file = { fileHash };
+ const data = { diff_files: [{ file_hash: fileHash, extra_field: 1, existingField: 1 }] };
+
+ mutations[types.ADD_COLLAPSED_DIFFS](state, { file, data });
+ expect(spy).toHaveBeenCalledWith(data, { deep: true });
+
+ expect(state.diffFiles[1].fileHash).toEqual(fileHash);
+ expect(state.diffFiles[1].existingField).toEqual(1);
+ expect(state.diffFiles[1].extraField).toEqual(1);
+ });
+ });
+});
diff --git a/spec/javascripts/diffs/store/utils_spec.js b/spec/javascripts/diffs/store/utils_spec.js
new file mode 100644
index 00000000000..32136d9ebff
--- /dev/null
+++ b/spec/javascripts/diffs/store/utils_spec.js
@@ -0,0 +1,210 @@
+import * as utils from '~/diffs/store/utils';
+import {
+ LINE_POSITION_LEFT,
+ LINE_POSITION_RIGHT,
+ TEXT_DIFF_POSITION_TYPE,
+ DIFF_NOTE_TYPE,
+ NEW_LINE_TYPE,
+ OLD_LINE_TYPE,
+ MATCH_LINE_TYPE,
+ PARALLEL_DIFF_VIEW_TYPE,
+} from '~/diffs/constants';
+import { MERGE_REQUEST_NOTEABLE_TYPE } from '~/notes/constants';
+import diffFileMockData from '../mock_data/diff_file';
+import { noteableDataMock } from '../../notes/mock_data';
+
+const getDiffFileMock = () => Object.assign({}, diffFileMockData);
+
+describe('DiffsStoreUtils', () => {
+ describe('findDiffFile', () => {
+ const files = [{ fileHash: 1, name: 'one' }];
+
+ it('should return correct file', () => {
+ expect(utils.findDiffFile(files, 1).name).toEqual('one');
+ expect(utils.findDiffFile(files, 2)).toBeUndefined();
+ });
+ });
+
+ describe('getReversePosition', () => {
+ it('should return correct line position name', () => {
+ expect(utils.getReversePosition(LINE_POSITION_RIGHT)).toEqual(LINE_POSITION_LEFT);
+ expect(utils.getReversePosition(LINE_POSITION_LEFT)).toEqual(LINE_POSITION_RIGHT);
+ });
+ });
+
+ describe('findIndexInInlineLines and findIndexInParallelLines', () => {
+ const expectSet = (method, lines, invalidLines) => {
+ expect(method(lines, { oldLineNumber: 3, newLineNumber: 5 })).toEqual(4);
+ expect(method(invalidLines || lines, { oldLineNumber: 32, newLineNumber: 53 })).toEqual(-1);
+ };
+
+ describe('findIndexInInlineLines', () => {
+ it('should return correct index for given line numbers', () => {
+ expectSet(utils.findIndexInInlineLines, getDiffFileMock().highlightedDiffLines);
+ });
+ });
+
+ describe('findIndexInParallelLines', () => {
+ it('should return correct index for given line numbers', () => {
+ expectSet(utils.findIndexInParallelLines, getDiffFileMock().parallelDiffLines, {});
+ });
+ });
+ });
+
+ describe('removeMatchLine', () => {
+ it('should remove match line properly by regarding the bottom parameter', () => {
+ const diffFile = getDiffFileMock();
+ const lineNumbers = { oldLineNumber: 3, newLineNumber: 5 };
+ const inlineIndex = utils.findIndexInInlineLines(diffFile.highlightedDiffLines, lineNumbers);
+ const parallelIndex = utils.findIndexInParallelLines(diffFile.parallelDiffLines, lineNumbers);
+ const atInlineIndex = diffFile.highlightedDiffLines[inlineIndex];
+ const atParallelIndex = diffFile.parallelDiffLines[parallelIndex];
+
+ utils.removeMatchLine(diffFile, lineNumbers, false);
+ expect(diffFile.highlightedDiffLines[inlineIndex]).not.toEqual(atInlineIndex);
+ expect(diffFile.parallelDiffLines[parallelIndex]).not.toEqual(atParallelIndex);
+
+ utils.removeMatchLine(diffFile, lineNumbers, true);
+ expect(diffFile.highlightedDiffLines[inlineIndex + 1]).not.toEqual(atInlineIndex);
+ expect(diffFile.parallelDiffLines[parallelIndex + 1]).not.toEqual(atParallelIndex);
+ });
+ });
+
+ describe('addContextLines', () => {
+ it('should add context lines properly with bottom parameter', () => {
+ const diffFile = getDiffFileMock();
+ const inlineLines = diffFile.highlightedDiffLines;
+ const parallelLines = diffFile.parallelDiffLines;
+ const lineNumbers = { oldLineNumber: 3, newLineNumber: 5 };
+ const contextLines = [{ lineNumber: 42 }];
+ const options = { inlineLines, parallelLines, contextLines, lineNumbers, bottom: true };
+ const inlineIndex = utils.findIndexInInlineLines(diffFile.highlightedDiffLines, lineNumbers);
+ const parallelIndex = utils.findIndexInParallelLines(diffFile.parallelDiffLines, lineNumbers);
+ const normalizedParallelLine = {
+ left: options.contextLines[0],
+ right: options.contextLines[0],
+ };
+
+ utils.addContextLines(options);
+ expect(inlineLines[inlineLines.length - 1]).toEqual(contextLines[0]);
+ expect(parallelLines[parallelLines.length - 1]).toEqual(normalizedParallelLine);
+
+ delete options.bottom;
+ utils.addContextLines(options);
+ expect(inlineLines[inlineIndex]).toEqual(contextLines[0]);
+ expect(parallelLines[parallelIndex]).toEqual(normalizedParallelLine);
+ });
+ });
+
+ describe('getNoteFormData', () => {
+ it('should properly create note form data', () => {
+ const diffFile = getDiffFileMock();
+ noteableDataMock.targetType = MERGE_REQUEST_NOTEABLE_TYPE;
+
+ const options = {
+ note: 'Hello world!',
+ noteableData: noteableDataMock,
+ noteableType: MERGE_REQUEST_NOTEABLE_TYPE,
+ diffFile,
+ noteTargetLine: {
+ lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3',
+ metaData: null,
+ newLine: 3,
+ oldLine: 1,
+ },
+ diffViewType: PARALLEL_DIFF_VIEW_TYPE,
+ linePosition: LINE_POSITION_LEFT,
+ };
+
+ const position = JSON.stringify({
+ base_sha: diffFile.diffRefs.baseSha,
+ start_sha: diffFile.diffRefs.startSha,
+ head_sha: diffFile.diffRefs.headSha,
+ old_path: diffFile.oldPath,
+ new_path: diffFile.newPath,
+ position_type: TEXT_DIFF_POSITION_TYPE,
+ old_line: options.noteTargetLine.oldLine,
+ new_line: options.noteTargetLine.newLine,
+ });
+
+ const postData = {
+ view: options.diffViewType,
+ line_type: options.linePosition === LINE_POSITION_RIGHT ? NEW_LINE_TYPE : OLD_LINE_TYPE,
+ merge_request_diff_head_sha: diffFile.diffRefs.headSha,
+ in_reply_to_discussion_id: '',
+ note_project_id: '',
+ target_type: options.noteableType,
+ target_id: options.noteableData.id,
+ note: {
+ noteable_type: options.noteableType,
+ noteable_id: options.noteableData.id,
+ commit_id: '',
+ type: DIFF_NOTE_TYPE,
+ line_code: options.noteTargetLine.lineCode,
+ note: options.note,
+ position,
+ },
+ };
+
+ expect(utils.getNoteFormData(options)).toEqual({
+ endpoint: options.noteableData.create_note_path,
+ data: postData,
+ });
+ });
+ });
+
+ describe('addLineReferences', () => {
+ const lineNumbers = { oldLineNumber: 3, newLineNumber: 4 };
+
+ it('should add correct line references when bottom set to true', () => {
+ const lines = [{ type: null }, { type: MATCH_LINE_TYPE }];
+ const linesWithReferences = utils.addLineReferences(lines, lineNumbers, true);
+
+ expect(linesWithReferences[0].oldLine).toEqual(lineNumbers.oldLineNumber + 1);
+ expect(linesWithReferences[0].newLine).toEqual(lineNumbers.newLineNumber + 1);
+ expect(linesWithReferences[1].metaData.oldPos).toEqual(4);
+ expect(linesWithReferences[1].metaData.newPos).toEqual(5);
+ });
+
+ it('should add correct line references when bottom falsy', () => {
+ const lines = [{ type: null }, { type: MATCH_LINE_TYPE }, { type: null }];
+ const linesWithReferences = utils.addLineReferences(lines, lineNumbers);
+
+ expect(linesWithReferences[0].oldLine).toEqual(0);
+ expect(linesWithReferences[0].newLine).toEqual(1);
+ expect(linesWithReferences[1].metaData.oldPos).toEqual(2);
+ expect(linesWithReferences[1].metaData.newPos).toEqual(3);
+ });
+ });
+
+ describe('trimFirstCharOfLineContent', () => {
+ it('trims the line when it starts with a space', () => {
+ expect(utils.trimFirstCharOfLineContent({ richText: ' diff' })).toEqual({ richText: 'diff' });
+ });
+
+ it('trims the line when it starts with a +', () => {
+ expect(utils.trimFirstCharOfLineContent({ richText: '+diff' })).toEqual({ richText: 'diff' });
+ });
+
+ it('trims the line when it starts with a -', () => {
+ expect(utils.trimFirstCharOfLineContent({ richText: '-diff' })).toEqual({ richText: 'diff' });
+ });
+
+ it('does not trims the line when it starts with a letter', () => {
+ expect(utils.trimFirstCharOfLineContent({ richText: 'diff' })).toEqual({ richText: 'diff' });
+ });
+
+ it('does not modify the provided object', () => {
+ const lineObj = {
+ richText: ' diff',
+ };
+
+ utils.trimFirstCharOfLineContent(lineObj);
+ expect(lineObj).toEqual({ richText: ' diff' });
+ });
+
+ it('handles a undefined or null parameter', () => {
+ expect(utils.trimFirstCharOfLineContent()).toEqual({});
+ });
+ });
+});
diff --git a/spec/javascripts/environments/environment_rollback_spec.js b/spec/javascripts/environments/environment_rollback_spec.js
index eb8e49d81fe..79f33c5bc8a 100644
--- a/spec/javascripts/environments/environment_rollback_spec.js
+++ b/spec/javascripts/environments/environment_rollback_spec.js
@@ -18,7 +18,7 @@ describe('Rollback Component', () => {
},
}).$mount();
- expect(component.$el.querySelector('span').textContent).toContain('Re-deploy');
+ expect(component.$el).toHaveSpriteIcon('repeat');
});
it('Should render Rollback label when isLastDeployment is false', () => {
@@ -30,6 +30,6 @@ describe('Rollback Component', () => {
},
}).$mount();
- expect(component.$el.querySelector('span').textContent).toContain('Rollback');
+ expect(component.$el).toHaveSpriteIcon('redo');
});
});
diff --git a/spec/javascripts/environments/environment_stop_spec.js b/spec/javascripts/environments/environment_stop_spec.js
index 3f95faf466a..4d9caa57566 100644
--- a/spec/javascripts/environments/environment_stop_spec.js
+++ b/spec/javascripts/environments/environment_stop_spec.js
@@ -4,7 +4,6 @@ import stopComp from '~/environments/components/environment_stop.vue';
describe('Stop Component', () => {
let StopComponent;
let component;
- const stopURL = '/stop';
beforeEach(() => {
StopComponent = Vue.extend(stopComp);
@@ -12,20 +11,13 @@ describe('Stop Component', () => {
component = new StopComponent({
propsData: {
- stopUrl: stopURL,
+ environment: {},
},
}).$mount();
});
- describe('computed', () => {
- it('title', () => {
- expect(component.title).toEqual('Stop');
- });
- });
-
it('should render a button to stop the environment', () => {
expect(component.$el.tagName).toEqual('BUTTON');
- expect(component.$el.getAttribute('data-original-title')).toEqual('Stop');
- expect(component.$el.getAttribute('aria-label')).toEqual('Stop');
+ expect(component.$el.getAttribute('data-original-title')).toEqual('Stop environment');
});
});
diff --git a/spec/javascripts/environments/environments_app_spec.js b/spec/javascripts/environments/environments_app_spec.js
index 615168645b4..6968fbc7ce7 100644
--- a/spec/javascripts/environments/environments_app_spec.js
+++ b/spec/javascripts/environments/environments_app_spec.js
@@ -220,7 +220,7 @@ describe('Environment', () => {
);
component = mountComponent(EnvironmentsComponent, mockData);
- spyOn(history, 'pushState').and.stub();
+ spyOn(window.history, 'pushState').and.stub();
});
describe('updateContent', () => {
diff --git a/spec/javascripts/environments/folder/environments_folder_view_spec.js b/spec/javascripts/environments/folder/environments_folder_view_spec.js
index f5ce4df0bfe..51d4213c38f 100644
--- a/spec/javascripts/environments/folder/environments_folder_view_spec.js
+++ b/spec/javascripts/environments/folder/environments_folder_view_spec.js
@@ -177,7 +177,7 @@ describe('Environments Folder View', () => {
});
component = mountComponent(Component, mockData);
- spyOn(history, 'pushState').and.stub();
+ spyOn(window.history, 'pushState').and.stub();
});
describe('updateContent', () => {
diff --git a/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js b/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js
index 59bd2650081..d926663fac0 100644
--- a/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js
+++ b/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js
@@ -103,7 +103,7 @@ describe('RecentSearchesDropdownContent', () => {
describe('processedItems', () => {
it('with items', () => {
vm = createComponent(propsDataWithItems);
- const processedItems = vm.processedItems;
+ const { processedItems } = vm;
expect(processedItems.length).toEqual(2);
@@ -122,7 +122,7 @@ describe('RecentSearchesDropdownContent', () => {
it('with no items', () => {
vm = createComponent(propsDataWithoutItems);
- const processedItems = vm.processedItems;
+ const { processedItems } = vm;
expect(processedItems.length).toEqual(0);
});
@@ -131,13 +131,13 @@ describe('RecentSearchesDropdownContent', () => {
describe('hasItems', () => {
it('with items', () => {
vm = createComponent(propsDataWithItems);
- const hasItems = vm.hasItems;
+ const { hasItems } = vm;
expect(hasItems).toEqual(true);
});
it('with no items', () => {
vm = createComponent(propsDataWithoutItems);
- const hasItems = vm.hasItems;
+ const { hasItems } = vm;
expect(hasItems).toEqual(false);
});
});
diff --git a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js
index fbc3926d332..68158cf52e4 100644
--- a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js
+++ b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js
@@ -17,6 +17,17 @@ describe('Filtered Search Token Keys', () => {
});
});
+ describe('getKeys', () => {
+ it('should return keys', () => {
+ const getKeys = FilteredSearchTokenKeys.getKeys();
+ const keys = FilteredSearchTokenKeys.get().map(i => i.key);
+
+ keys.forEach((key, i) => {
+ expect(key).toEqual(getKeys[i]);
+ });
+ });
+ });
+
describe('getConditions', () => {
let conditions;
diff --git a/spec/javascripts/filtered_search/recent_searches_root_spec.js b/spec/javascripts/filtered_search/recent_searches_root_spec.js
index 1e6272bad0b..d063fcf4f2d 100644
--- a/spec/javascripts/filtered_search/recent_searches_root_spec.js
+++ b/spec/javascripts/filtered_search/recent_searches_root_spec.js
@@ -15,8 +15,7 @@ describe('RecentSearchesRoot', () => {
};
VueSpy = spyOnDependency(RecentSearchesRoot, 'Vue').and.callFake((options) => {
- data = options.data;
- template = options.template;
+ ({ data, template } = options);
});
RecentSearchesRoot.prototype.render.call(recentSearchesRoot);
diff --git a/spec/javascripts/fixtures/commit.rb b/spec/javascripts/fixtures/commit.rb
new file mode 100644
index 00000000000..24ab8159a18
--- /dev/null
+++ b/spec/javascripts/fixtures/commit.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe Projects::CommitController, '(JavaScript fixtures)', type: :controller do
+ include JavaScriptFixturesHelpers
+
+ set(:project) { create(:project, :repository) }
+ set(:user) { create(:user) }
+ let(:commit) { project.commit("master") }
+
+ render_views
+
+ before(:all) do
+ clean_frontend_fixtures('commit/')
+ end
+
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+ end
+
+ it 'commit/show.html.raw' do |example|
+ params = {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: commit.id
+ }
+
+ get :show, params
+
+ expect(response).to be_success
+ store_frontend_fixture(response, example.description)
+ end
+end
diff --git a/spec/javascripts/fixtures/event_filter.html.haml b/spec/javascripts/fixtures/event_filter.html.haml
index 5477c6075f0..aa7af61c7eb 100644
--- a/spec/javascripts/fixtures/event_filter.html.haml
+++ b/spec/javascripts/fixtures/event_filter.html.haml
@@ -1,4 +1,4 @@
-%ul.nav-links.event-filter.scrolling-tabs
+%ul.nav-links.event-filter.scrolling-tabs.nav.nav-tabs
%li.active
%a.event-filter-link{ id: "all_event_filter", title: "Filter by all", href: "/dashboard/activity"}
%span
diff --git a/spec/javascripts/fixtures/groups.rb b/spec/javascripts/fixtures/groups.rb
index 35be52fbf97..a2035ceae15 100644
--- a/spec/javascripts/fixtures/groups.rb
+++ b/spec/javascripts/fixtures/groups.rb
@@ -13,7 +13,7 @@ describe 'Groups (JavaScript fixtures)', type: :controller do
end
before do
- group.add_master(admin)
+ group.add_maintainer(admin)
sign_in(admin)
end
diff --git a/spec/javascripts/fixtures/images/green_box.png b/spec/javascripts/fixtures/images/green_box.png
new file mode 100644
index 00000000000..cd1ff9f9ade
--- /dev/null
+++ b/spec/javascripts/fixtures/images/green_box.png
Binary files differ
diff --git a/spec/javascripts/fixtures/images/red_box.png b/spec/javascripts/fixtures/images/red_box.png
new file mode 100644
index 00000000000..73b2927da0f
--- /dev/null
+++ b/spec/javascripts/fixtures/images/red_box.png
Binary files differ
diff --git a/spec/javascripts/fixtures/issue_sidebar_label.html.haml b/spec/javascripts/fixtures/issue_sidebar_label.html.haml
index 397bdc85c67..06ce248dc9c 100644
--- a/spec/javascripts/fixtures/issue_sidebar_label.html.haml
+++ b/spec/javascripts/fixtures/issue_sidebar_label.html.haml
@@ -1,7 +1,7 @@
.block.labels
.sidebar-collapsed-icon.js-sidebar-labels-tooltip
.title.hide-collapsed
- %a.edit-link.pull-right{ href: "#" }
+ %a.edit-link.float-right{ href: "#" }
Edit
.selectbox.hide-collapsed{ style: "display: none;" }
.dropdown
diff --git a/spec/javascripts/fixtures/linked_tabs.html.haml b/spec/javascripts/fixtures/linked_tabs.html.haml
index c38fe8b1f25..632606e0536 100644
--- a/spec/javascripts/fixtures/linked_tabs.html.haml
+++ b/spec/javascripts/fixtures/linked_tabs.html.haml
@@ -1,9 +1,9 @@
-%ul.nav-links.new-session-tabs.linked-tabs
- %li
- %a{ href: 'foo/bar/1', data: { target: 'div#tab1', action: 'tab1', toggle: 'tab' } }
+%ul.nav.nav-tabs.new-session-tabs.linked-tabs
+ %li.nav-item
+ %a.nav-link{ href: 'foo/bar/1', data: { target: 'div#tab1', action: 'tab1', toggle: 'tab' } }
Tab 1
- %li
- %a{ href: 'foo/bar/1/context', data: { target: 'div#tab2', action: 'tab2', toggle: 'tab' } }
+ %li.nav-item
+ %a.nav-link{ href: 'foo/bar/1/context', data: { target: 'div#tab2', action: 'tab2', toggle: 'tab' } }
Tab 2
.tab-content
diff --git a/spec/javascripts/fixtures/merge_requests.rb b/spec/javascripts/fixtures/merge_requests.rb
index ee60489eb7c..7257d0c8556 100644
--- a/spec/javascripts/fixtures/merge_requests.rb
+++ b/spec/javascripts/fixtures/merge_requests.rb
@@ -80,6 +80,13 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont
render_discussions_json(merge_request, example.description)
end
+ it 'merge_requests/resolved_diff_discussion.json' do |example|
+ note = create(:discussion_note_on_merge_request, :resolved, project: project, author: admin, position: position, noteable: merge_request)
+ create(:system_note, project: project, author: admin, noteable: merge_request, discussion_id: note.discussion.id)
+
+ render_discussions_json(merge_request, example.description)
+ end
+
context 'with image diff' do
let(:merge_request2) { create(:merge_request_with_diffs, :with_image_diffs, source_project: project, title: "Added images") }
let(:image_path) { "files/images/ee_repo_logo.png" }
diff --git a/spec/javascripts/fixtures/projects.rb b/spec/javascripts/fixtures/projects.rb
index e8865b04874..57c78182abc 100644
--- a/spec/javascripts/fixtures/projects.rb
+++ b/spec/javascripts/fixtures/projects.rb
@@ -17,7 +17,7 @@ describe 'Projects (JavaScript fixtures)', type: :controller do
end
before do
- project.add_master(admin)
+ project.add_maintainer(admin)
sign_in(admin)
end
diff --git a/spec/javascripts/fixtures/snippet.rb b/spec/javascripts/fixtures/snippet.rb
index fa97f352e31..38fc963caf7 100644
--- a/spec/javascripts/fixtures/snippet.rb
+++ b/spec/javascripts/fixtures/snippet.rb
@@ -7,6 +7,7 @@ describe SnippetsController, '(JavaScript fixtures)', type: :controller do
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
let(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') }
let(:snippet) { create(:personal_snippet, title: 'snippet.md', content: '# snippet', file_name: 'snippet.md', author: admin) }
+ let!(:snippet_note) { create(:discussion_note_on_snippet, noteable: snippet, project: project, author: admin, note: '- [ ] Task List Item') }
render_views
diff --git a/spec/javascripts/frequent_items/components/app_spec.js b/spec/javascripts/frequent_items/components/app_spec.js
new file mode 100644
index 00000000000..834f919524d
--- /dev/null
+++ b/spec/javascripts/frequent_items/components/app_spec.js
@@ -0,0 +1,251 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import Vue from 'vue';
+import appComponent from '~/frequent_items/components/app.vue';
+import eventHub from '~/frequent_items/event_hub';
+import store from '~/frequent_items/store';
+import { FREQUENT_ITEMS, HOUR_IN_MS } from '~/frequent_items/constants';
+import { getTopFrequentItems } from '~/frequent_items/utils';
+import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { currentSession, mockFrequentProjects, mockSearchedProjects } from '../mock_data';
+
+let session;
+const createComponentWithStore = (namespace = 'projects') => {
+ session = currentSession[namespace];
+ gon.api_version = session.apiVersion;
+ const Component = Vue.extend(appComponent);
+
+ return mountComponentWithStore(Component, {
+ store,
+ props: {
+ namespace,
+ currentUserName: session.username,
+ currentItem: session.project || session.group,
+ },
+ });
+};
+
+describe('Frequent Items App Component', () => {
+ let vm;
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ vm = createComponentWithStore();
+ });
+
+ afterEach(() => {
+ mock.restore();
+ vm.$destroy();
+ });
+
+ describe('methods', () => {
+ describe('dropdownOpenHandler', () => {
+ it('should fetch frequent items when no search has been previously made on desktop', () => {
+ spyOn(vm, 'fetchFrequentItems');
+
+ vm.dropdownOpenHandler();
+
+ expect(vm.fetchFrequentItems).toHaveBeenCalledWith();
+ });
+ });
+
+ describe('logItemAccess', () => {
+ let storage;
+
+ beforeEach(() => {
+ storage = {};
+
+ spyOn(window.localStorage, 'setItem').and.callFake((storageKey, value) => {
+ storage[storageKey] = value;
+ });
+
+ spyOn(window.localStorage, 'getItem').and.callFake(storageKey => {
+ if (storage[storageKey]) {
+ return storage[storageKey];
+ }
+
+ return null;
+ });
+ });
+
+ it('should create a project store if it does not exist and adds a project', () => {
+ vm.logItemAccess(session.storageKey, session.project);
+
+ const projects = JSON.parse(storage[session.storageKey]);
+
+ expect(projects.length).toBe(1);
+ expect(projects[0].frequency).toBe(1);
+ expect(projects[0].lastAccessedOn).toBeDefined();
+ });
+
+ it('should prevent inserting same report multiple times into store', () => {
+ vm.logItemAccess(session.storageKey, session.project);
+ vm.logItemAccess(session.storageKey, session.project);
+
+ const projects = JSON.parse(storage[session.storageKey]);
+
+ expect(projects.length).toBe(1);
+ });
+
+ it('should increase frequency of report if it was logged multiple times over the course of an hour', () => {
+ let projects;
+ const newTimestamp = Date.now() + HOUR_IN_MS + 1;
+
+ vm.logItemAccess(session.storageKey, session.project);
+ projects = JSON.parse(storage[session.storageKey]);
+
+ expect(projects[0].frequency).toBe(1);
+
+ vm.logItemAccess(session.storageKey, {
+ ...session.project,
+ lastAccessedOn: newTimestamp,
+ });
+ projects = JSON.parse(storage[session.storageKey]);
+
+ expect(projects[0].frequency).toBe(2);
+ expect(projects[0].lastAccessedOn).not.toBe(session.project.lastAccessedOn);
+ });
+
+ it('should always update project metadata', () => {
+ let projects;
+ const oldProject = {
+ ...session.project,
+ };
+
+ const newProject = {
+ ...session.project,
+ name: 'New Name',
+ avatarUrl: 'new/avatar.png',
+ namespace: 'New / Namespace',
+ webUrl: 'http://localhost/new/web/url',
+ };
+
+ vm.logItemAccess(session.storageKey, oldProject);
+ projects = JSON.parse(storage[session.storageKey]);
+
+ expect(projects[0].name).toBe(oldProject.name);
+ expect(projects[0].avatarUrl).toBe(oldProject.avatarUrl);
+ expect(projects[0].namespace).toBe(oldProject.namespace);
+ expect(projects[0].webUrl).toBe(oldProject.webUrl);
+
+ vm.logItemAccess(session.storageKey, newProject);
+ projects = JSON.parse(storage[session.storageKey]);
+
+ expect(projects[0].name).toBe(newProject.name);
+ expect(projects[0].avatarUrl).toBe(newProject.avatarUrl);
+ expect(projects[0].namespace).toBe(newProject.namespace);
+ expect(projects[0].webUrl).toBe(newProject.webUrl);
+ });
+
+ it('should not add more than 20 projects in store', () => {
+ for (let id = 0; id < FREQUENT_ITEMS.MAX_COUNT; id += 1) {
+ const project = {
+ ...session.project,
+ id,
+ };
+ vm.logItemAccess(session.storageKey, project);
+ }
+
+ const projects = JSON.parse(storage[session.storageKey]);
+
+ expect(projects.length).toBe(FREQUENT_ITEMS.MAX_COUNT);
+ });
+ });
+ });
+
+ describe('created', () => {
+ it('should bind event listeners on eventHub', done => {
+ spyOn(eventHub, '$on');
+
+ createComponentWithStore().$mount();
+
+ Vue.nextTick(() => {
+ expect(eventHub.$on).toHaveBeenCalledWith('projects-dropdownOpen', jasmine.any(Function));
+ done();
+ });
+ });
+ });
+
+ describe('beforeDestroy', () => {
+ it('should unbind event listeners on eventHub', done => {
+ spyOn(eventHub, '$off');
+
+ vm.$mount();
+ vm.$destroy();
+
+ Vue.nextTick(() => {
+ expect(eventHub.$off).toHaveBeenCalledWith('projects-dropdownOpen', jasmine.any(Function));
+ done();
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('should render search input', () => {
+ expect(vm.$el.querySelector('.search-input-container')).toBeDefined();
+ });
+
+ it('should render loading animation', done => {
+ vm.$store.dispatch('fetchSearchedItems');
+
+ Vue.nextTick(() => {
+ const loadingEl = vm.$el.querySelector('.loading-animation');
+
+ expect(loadingEl).toBeDefined();
+ expect(loadingEl.classList.contains('prepend-top-20')).toBe(true);
+ expect(loadingEl.querySelector('i').getAttribute('aria-label')).toBe('Loading projects');
+ done();
+ });
+ });
+
+ it('should render frequent projects list header', done => {
+ Vue.nextTick(() => {
+ const sectionHeaderEl = vm.$el.querySelector('.section-header');
+
+ expect(sectionHeaderEl).toBeDefined();
+ expect(sectionHeaderEl.innerText.trim()).toBe('Frequently visited');
+ done();
+ });
+ });
+
+ it('should render frequent projects list', done => {
+ const expectedResult = getTopFrequentItems(mockFrequentProjects);
+ spyOn(window.localStorage, 'getItem').and.callFake(() =>
+ JSON.stringify(mockFrequentProjects),
+ );
+
+ expect(vm.$el.querySelectorAll('.frequent-items-list-container li').length).toBe(1);
+
+ vm.fetchFrequentItems();
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelectorAll('.frequent-items-list-container li').length).toBe(
+ expectedResult.length,
+ );
+ done();
+ });
+ });
+
+ it('should render searched projects list', done => {
+ mock.onGet(/\/api\/v4\/projects.json(.*)$/).replyOnce(200, mockSearchedProjects);
+
+ expect(vm.$el.querySelectorAll('.frequent-items-list-container li').length).toBe(1);
+
+ vm.$store.dispatch('setSearchQuery', 'gitlab');
+ vm
+ .$nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.loading-animation')).toBeDefined();
+ })
+ .then(vm.$nextTick)
+ .then(vm.$nextTick)
+ .then(() => {
+ expect(vm.$el.querySelectorAll('.frequent-items-list-container li').length).toBe(
+ mockSearchedProjects.length,
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/frequent_items/components/frequent_items_list_item_spec.js b/spec/javascripts/frequent_items/components/frequent_items_list_item_spec.js
new file mode 100644
index 00000000000..201aca77b10
--- /dev/null
+++ b/spec/javascripts/frequent_items/components/frequent_items_list_item_spec.js
@@ -0,0 +1,75 @@
+import Vue from 'vue';
+import frequentItemsListItemComponent from '~/frequent_items/components/frequent_items_list_item.vue';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import { mockProject } from '../mock_data'; // can also use 'mockGroup', but not useful to test here
+
+const createComponent = () => {
+ const Component = Vue.extend(frequentItemsListItemComponent);
+
+ return mountComponent(Component, {
+ itemId: mockProject.id,
+ itemName: mockProject.name,
+ namespace: mockProject.namespace,
+ webUrl: mockProject.webUrl,
+ avatarUrl: mockProject.avatarUrl,
+ });
+};
+
+describe('FrequentItemsListItemComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('computed', () => {
+ describe('hasAvatar', () => {
+ it('should return `true` or `false` if whether avatar is present or not', () => {
+ vm.avatarUrl = 'path/to/avatar.png';
+ expect(vm.hasAvatar).toBe(true);
+
+ vm.avatarUrl = null;
+ expect(vm.hasAvatar).toBe(false);
+ });
+ });
+
+ describe('highlightedItemName', () => {
+ it('should enclose part of project name in <b> & </b> which matches with `matcher` prop', () => {
+ vm.matcher = 'lab';
+ expect(vm.highlightedItemName).toContain('<b>Lab</b>');
+ });
+
+ it('should return project name as it is if `matcher` is not available', () => {
+ vm.matcher = null;
+ expect(vm.highlightedItemName).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', () => {
+ it('should render component element', () => {
+ expect(vm.$el.classList.contains('frequent-items-list-item-container')).toBeTruthy();
+ expect(vm.$el.querySelectorAll('a').length).toBe(1);
+ expect(vm.$el.querySelectorAll('.frequent-items-item-avatar-container').length).toBe(1);
+ expect(vm.$el.querySelectorAll('.frequent-items-item-metadata-container').length).toBe(1);
+ expect(vm.$el.querySelectorAll('.frequent-items-item-title').length).toBe(1);
+ expect(vm.$el.querySelectorAll('.frequent-items-item-namespace').length).toBe(1);
+ });
+ });
+});
diff --git a/spec/javascripts/frequent_items/components/frequent_items_list_spec.js b/spec/javascripts/frequent_items/components/frequent_items_list_spec.js
new file mode 100644
index 00000000000..3003b7ee000
--- /dev/null
+++ b/spec/javascripts/frequent_items/components/frequent_items_list_spec.js
@@ -0,0 +1,84 @@
+import Vue from 'vue';
+import frequentItemsListComponent from '~/frequent_items/components/frequent_items_list.vue';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import { mockFrequentProjects } from '../mock_data';
+
+const createComponent = (namespace = 'projects') => {
+ const Component = Vue.extend(frequentItemsListComponent);
+
+ return mountComponent(Component, {
+ namespace,
+ items: mockFrequentProjects,
+ isFetchFailed: false,
+ hasSearchQuery: false,
+ matcher: 'lab',
+ });
+};
+
+describe('FrequentItemsListComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('computed', () => {
+ describe('isListEmpty', () => {
+ it('should return `true` or `false` representing whether if `items` is empty or not with projects', () => {
+ vm.items = [];
+ expect(vm.isListEmpty).toBe(true);
+
+ vm.items = mockFrequentProjects;
+ expect(vm.isListEmpty).toBe(false);
+ });
+ });
+
+ describe('fetched item messages', () => {
+ it('should return appropriate empty list message based on value of `localStorageFailed` prop with projects', () => {
+ vm.isFetchFailed = true;
+ expect(vm.listEmptyMessage).toBe('This feature requires browser localStorage support');
+
+ vm.isFetchFailed = false;
+ expect(vm.listEmptyMessage).toBe('Projects you visit often will appear here');
+ });
+ });
+
+ describe('searched item messages', () => {
+ it('should return appropriate empty list message based on value of `searchFailed` prop with projects', () => {
+ vm.hasSearchQuery = true;
+ vm.isFetchFailed = true;
+ expect(vm.listEmptyMessage).toBe('Something went wrong on our end.');
+
+ vm.isFetchFailed = false;
+ expect(vm.listEmptyMessage).toBe('Sorry, no projects matched your search');
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('should render component element with list of projects', done => {
+ vm.items = mockFrequentProjects;
+
+ Vue.nextTick(() => {
+ expect(vm.$el.classList.contains('frequent-items-list-container')).toBe(true);
+ expect(vm.$el.querySelectorAll('ul.list-unstyled').length).toBe(1);
+ expect(vm.$el.querySelectorAll('li.frequent-items-list-item-container').length).toBe(5);
+ done();
+ });
+ });
+
+ it('should render component element with empty message', done => {
+ vm.items = [];
+
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelectorAll('li.section-empty').length).toBe(1);
+ expect(vm.$el.querySelectorAll('li.frequent-items-list-item-container').length).toBe(0);
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/frequent_items/components/frequent_items_search_input_spec.js b/spec/javascripts/frequent_items/components/frequent_items_search_input_spec.js
new file mode 100644
index 00000000000..6a11038e70a
--- /dev/null
+++ b/spec/javascripts/frequent_items/components/frequent_items_search_input_spec.js
@@ -0,0 +1,77 @@
+import Vue from 'vue';
+import searchComponent from '~/frequent_items/components/frequent_items_search_input.vue';
+import eventHub from '~/frequent_items/event_hub';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+
+const createComponent = (namespace = 'projects') => {
+ const Component = Vue.extend(searchComponent);
+
+ return mountComponent(Component, { namespace });
+};
+
+describe('FrequentItemsSearchInputComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('methods', () => {
+ describe('setFocus', () => {
+ it('should set focus to search input', () => {
+ spyOn(vm.$refs.search, 'focus');
+
+ vm.setFocus();
+ expect(vm.$refs.search.focus).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('mounted', () => {
+ it('should listen `dropdownOpen` event', done => {
+ spyOn(eventHub, '$on');
+ const vmX = createComponent();
+
+ Vue.nextTick(() => {
+ expect(eventHub.$on).toHaveBeenCalledWith(
+ `${vmX.namespace}-dropdownOpen`,
+ jasmine.any(Function),
+ );
+ done();
+ });
+ });
+ });
+
+ describe('beforeDestroy', () => {
+ it('should unbind event listeners on eventHub', done => {
+ const vmX = createComponent();
+ spyOn(eventHub, '$off');
+
+ vmX.$mount();
+ vmX.$destroy();
+
+ Vue.nextTick(() => {
+ expect(eventHub.$off).toHaveBeenCalledWith(
+ `${vmX.namespace}-dropdownOpen`,
+ jasmine.any(Function),
+ );
+ done();
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('should render component element', () => {
+ const inputEl = vm.$el.querySelector('input.form-control');
+
+ expect(vm.$el.classList.contains('search-input-container')).toBeTruthy();
+ expect(inputEl).not.toBe(null);
+ expect(inputEl.getAttribute('placeholder')).toBe('Search your projects');
+ expect(vm.$el.querySelector('.search-icon')).toBeDefined();
+ });
+ });
+});
diff --git a/spec/javascripts/frequent_items/mock_data.js b/spec/javascripts/frequent_items/mock_data.js
new file mode 100644
index 00000000000..cf3602f42d6
--- /dev/null
+++ b/spec/javascripts/frequent_items/mock_data.js
@@ -0,0 +1,168 @@
+export const currentSession = {
+ groups: {
+ username: 'root',
+ storageKey: 'root/frequent-groups',
+ apiVersion: 'v4',
+ group: {
+ id: 1,
+ name: 'dummy-group',
+ full_name: 'dummy-parent-group',
+ webUrl: `${gl.TEST_HOST}/dummy-group`,
+ avatarUrl: null,
+ lastAccessedOn: Date.now(),
+ },
+ },
+ projects: {
+ username: 'root',
+ storageKey: 'root/frequent-projects',
+ apiVersion: 'v4',
+ project: {
+ id: 1,
+ name: 'dummy-project',
+ namespace: 'SampleGroup / Dummy-Project',
+ webUrl: `${gl.TEST_HOST}/samplegroup/dummy-project`,
+ avatarUrl: null,
+ lastAccessedOn: Date.now(),
+ },
+ },
+};
+
+export const mockNamespace = 'projects';
+
+export const mockStorageKey = 'test-user/frequent-projects';
+
+export const mockGroup = {
+ id: 1,
+ name: 'Sub451',
+ namespace: 'Commit451 / Sub451',
+ webUrl: `${gl.TEST_HOST}/Commit451/Sub451`,
+ avatarUrl: null,
+};
+
+export const mockRawGroup = {
+ id: 1,
+ name: 'Sub451',
+ full_name: 'Commit451 / Sub451',
+ web_url: `${gl.TEST_HOST}/Commit451/Sub451`,
+ avatar_url: null,
+};
+
+export const mockFrequentGroups = [
+ {
+ id: 3,
+ name: 'Subgroup451',
+ full_name: 'Commit451 / Subgroup451',
+ webUrl: '/Commit451/Subgroup451',
+ avatarUrl: null,
+ frequency: 7,
+ lastAccessedOn: 1497979281815,
+ },
+ {
+ id: 1,
+ name: 'Commit451',
+ full_name: 'Commit451',
+ webUrl: '/Commit451',
+ avatarUrl: null,
+ frequency: 3,
+ lastAccessedOn: 1497979281815,
+ },
+];
+
+export const mockSearchedGroups = [mockRawGroup];
+export const mockProcessedSearchedGroups = [mockGroup];
+
+export const mockProject = {
+ id: 1,
+ name: 'GitLab Community Edition',
+ namespace: 'gitlab-org / gitlab-ce',
+ webUrl: `${gl.TEST_HOST}/gitlab-org/gitlab-ce`,
+ avatarUrl: null,
+};
+
+export const mockRawProject = {
+ id: 1,
+ name: 'GitLab Community Edition',
+ name_with_namespace: 'gitlab-org / gitlab-ce',
+ web_url: `${gl.TEST_HOST}/gitlab-org/gitlab-ce`,
+ avatar_url: null,
+};
+
+export const mockFrequentProjects = [
+ {
+ id: 1,
+ name: 'GitLab Community Edition',
+ namespace: 'gitlab-org / gitlab-ce',
+ webUrl: `${gl.TEST_HOST}/gitlab-org/gitlab-ce`,
+ avatarUrl: null,
+ frequency: 1,
+ lastAccessedOn: Date.now(),
+ },
+ {
+ id: 2,
+ name: 'GitLab CI',
+ namespace: 'gitlab-org / gitlab-ci',
+ webUrl: `${gl.TEST_HOST}/gitlab-org/gitlab-ci`,
+ avatarUrl: null,
+ frequency: 9,
+ lastAccessedOn: Date.now(),
+ },
+ {
+ id: 3,
+ name: 'Typeahead.Js',
+ namespace: 'twitter / typeahead-js',
+ webUrl: `${gl.TEST_HOST}/twitter/typeahead-js`,
+ avatarUrl: '/uploads/-/system/project/avatar/7/TWBS.png',
+ frequency: 2,
+ lastAccessedOn: Date.now(),
+ },
+ {
+ id: 4,
+ name: 'Intel',
+ namespace: 'platform / hardware / bsp / intel',
+ webUrl: `${gl.TEST_HOST}/platform/hardware/bsp/intel`,
+ avatarUrl: null,
+ frequency: 3,
+ lastAccessedOn: Date.now(),
+ },
+ {
+ id: 5,
+ name: 'v4.4',
+ namespace: 'platform / hardware / bsp / kernel / common / v4.4',
+ webUrl: `${gl.TEST_HOST}/platform/hardware/bsp/kernel/common/v4.4`,
+ avatarUrl: null,
+ frequency: 8,
+ lastAccessedOn: Date.now(),
+ },
+];
+
+export const mockSearchedProjects = [mockRawProject];
+export const mockProcessedSearchedProjects = [mockProject];
+
+export const unsortedFrequentItems = [
+ { id: 1, frequency: 12, lastAccessedOn: 1491400843391 },
+ { id: 2, frequency: 14, lastAccessedOn: 1488240890738 },
+ { id: 3, frequency: 44, lastAccessedOn: 1497675908472 },
+ { id: 4, frequency: 8, lastAccessedOn: 1497979281815 },
+ { id: 5, frequency: 34, lastAccessedOn: 1488089211943 },
+ { id: 6, frequency: 14, lastAccessedOn: 1493517292488 },
+ { id: 7, frequency: 42, lastAccessedOn: 1486815299875 },
+ { id: 8, frequency: 33, lastAccessedOn: 1500762279114 },
+ { id: 10, frequency: 46, lastAccessedOn: 1483251641543 },
+];
+
+/**
+ * This const has a specific order which tests authenticity
+ * of `getTopFrequentItems` method so
+ * DO NOT change order of items in this const.
+ */
+export const sortedFrequentItems = [
+ { id: 10, frequency: 46, lastAccessedOn: 1483251641543 },
+ { id: 3, frequency: 44, lastAccessedOn: 1497675908472 },
+ { id: 7, frequency: 42, lastAccessedOn: 1486815299875 },
+ { id: 5, frequency: 34, lastAccessedOn: 1488089211943 },
+ { id: 8, frequency: 33, lastAccessedOn: 1500762279114 },
+ { id: 6, frequency: 14, lastAccessedOn: 1493517292488 },
+ { id: 2, frequency: 14, lastAccessedOn: 1488240890738 },
+ { id: 1, frequency: 12, lastAccessedOn: 1491400843391 },
+ { id: 4, frequency: 8, lastAccessedOn: 1497979281815 },
+];
diff --git a/spec/javascripts/frequent_items/store/actions_spec.js b/spec/javascripts/frequent_items/store/actions_spec.js
new file mode 100644
index 00000000000..0cdd033d38f
--- /dev/null
+++ b/spec/javascripts/frequent_items/store/actions_spec.js
@@ -0,0 +1,225 @@
+import testAction from 'spec/helpers/vuex_action_helper';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import AccessorUtilities from '~/lib/utils/accessor';
+import * as actions from '~/frequent_items/store/actions';
+import * as types from '~/frequent_items/store/mutation_types';
+import state from '~/frequent_items/store/state';
+import {
+ mockNamespace,
+ mockStorageKey,
+ mockFrequentProjects,
+ mockSearchedProjects,
+} from '../mock_data';
+
+describe('Frequent Items Dropdown Store Actions', () => {
+ let mockedState;
+ let mock;
+
+ beforeEach(() => {
+ mockedState = state();
+ mock = new MockAdapter(axios);
+
+ mockedState.namespace = mockNamespace;
+ mockedState.storageKey = mockStorageKey;
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('setNamespace', () => {
+ it('should set namespace', done => {
+ testAction(
+ actions.setNamespace,
+ mockNamespace,
+ mockedState,
+ [{ type: types.SET_NAMESPACE, payload: mockNamespace }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('setStorageKey', () => {
+ it('should set storage key', done => {
+ testAction(
+ actions.setStorageKey,
+ mockStorageKey,
+ mockedState,
+ [{ type: types.SET_STORAGE_KEY, payload: mockStorageKey }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('requestFrequentItems', () => {
+ it('should request frequent items', done => {
+ testAction(
+ actions.requestFrequentItems,
+ null,
+ mockedState,
+ [{ type: types.REQUEST_FREQUENT_ITEMS }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveFrequentItemsSuccess', () => {
+ it('should set frequent items', done => {
+ testAction(
+ actions.receiveFrequentItemsSuccess,
+ mockFrequentProjects,
+ mockedState,
+ [{ type: types.RECEIVE_FREQUENT_ITEMS_SUCCESS, payload: mockFrequentProjects }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveFrequentItemsError', () => {
+ it('should set frequent items error state', done => {
+ testAction(
+ actions.receiveFrequentItemsError,
+ null,
+ mockedState,
+ [{ type: types.RECEIVE_FREQUENT_ITEMS_ERROR }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('fetchFrequentItems', () => {
+ it('should dispatch `receiveFrequentItemsSuccess`', done => {
+ mockedState.namespace = mockNamespace;
+ mockedState.storageKey = mockStorageKey;
+
+ testAction(
+ actions.fetchFrequentItems,
+ null,
+ mockedState,
+ [],
+ [{ type: 'requestFrequentItems' }, { type: 'receiveFrequentItemsSuccess', payload: [] }],
+ done,
+ );
+ });
+
+ it('should dispatch `receiveFrequentItemsError`', done => {
+ spyOn(AccessorUtilities, 'isLocalStorageAccessSafe').and.returnValue(false);
+ mockedState.namespace = mockNamespace;
+ mockedState.storageKey = mockStorageKey;
+
+ testAction(
+ actions.fetchFrequentItems,
+ null,
+ mockedState,
+ [],
+ [{ type: 'requestFrequentItems' }, { type: 'receiveFrequentItemsError' }],
+ done,
+ );
+ });
+ });
+
+ describe('requestSearchedItems', () => {
+ it('should request searched items', done => {
+ testAction(
+ actions.requestSearchedItems,
+ null,
+ mockedState,
+ [{ type: types.REQUEST_SEARCHED_ITEMS }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveSearchedItemsSuccess', () => {
+ it('should set searched items', done => {
+ testAction(
+ actions.receiveSearchedItemsSuccess,
+ mockSearchedProjects,
+ mockedState,
+ [{ type: types.RECEIVE_SEARCHED_ITEMS_SUCCESS, payload: mockSearchedProjects }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveSearchedItemsError', () => {
+ it('should set searched items error state', done => {
+ testAction(
+ actions.receiveSearchedItemsError,
+ null,
+ mockedState,
+ [{ type: types.RECEIVE_SEARCHED_ITEMS_ERROR }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('fetchSearchedItems', () => {
+ beforeEach(() => {
+ gon.api_version = 'v4';
+ });
+
+ it('should dispatch `receiveSearchedItemsSuccess`', done => {
+ mock.onGet(/\/api\/v4\/projects.json(.*)$/).replyOnce(200, mockSearchedProjects);
+
+ testAction(
+ actions.fetchSearchedItems,
+ null,
+ mockedState,
+ [],
+ [
+ { type: 'requestSearchedItems' },
+ { type: 'receiveSearchedItemsSuccess', payload: mockSearchedProjects },
+ ],
+ done,
+ );
+ });
+
+ it('should dispatch `receiveSearchedItemsError`', done => {
+ gon.api_version = 'v4';
+ mock.onGet(/\/api\/v4\/projects.json(.*)$/).replyOnce(500);
+
+ testAction(
+ actions.fetchSearchedItems,
+ null,
+ mockedState,
+ [],
+ [{ type: 'requestSearchedItems' }, { type: 'receiveSearchedItemsError' }],
+ done,
+ );
+ });
+ });
+
+ describe('setSearchQuery', () => {
+ it('should commit query and dispatch `fetchSearchedItems` when query is present', done => {
+ testAction(
+ actions.setSearchQuery,
+ { query: 'test' },
+ mockedState,
+ [{ type: types.SET_SEARCH_QUERY }],
+ [{ type: 'fetchSearchedItems', payload: { query: 'test' } }],
+ done,
+ );
+ });
+
+ it('should commit query and dispatch `fetchFrequentItems` when query is empty', done => {
+ testAction(
+ actions.setSearchQuery,
+ null,
+ mockedState,
+ [{ type: types.SET_SEARCH_QUERY }],
+ [{ type: 'fetchFrequentItems' }],
+ done,
+ );
+ });
+ });
+});
diff --git a/spec/javascripts/frequent_items/store/getters_spec.js b/spec/javascripts/frequent_items/store/getters_spec.js
new file mode 100644
index 00000000000..1cd12eb6832
--- /dev/null
+++ b/spec/javascripts/frequent_items/store/getters_spec.js
@@ -0,0 +1,24 @@
+import state from '~/frequent_items/store/state';
+import * as getters from '~/frequent_items/store/getters';
+
+describe('Frequent Items Dropdown Store Getters', () => {
+ let mockedState;
+
+ beforeEach(() => {
+ mockedState = state();
+ });
+
+ describe('hasSearchQuery', () => {
+ it('should return `true` when search query is present', () => {
+ mockedState.searchQuery = 'test';
+
+ expect(getters.hasSearchQuery(mockedState)).toBe(true);
+ });
+
+ it('should return `false` when search query is empty', () => {
+ mockedState.searchQuery = '';
+
+ expect(getters.hasSearchQuery(mockedState)).toBe(false);
+ });
+ });
+});
diff --git a/spec/javascripts/frequent_items/store/mutations_spec.js b/spec/javascripts/frequent_items/store/mutations_spec.js
new file mode 100644
index 00000000000..d36964b2600
--- /dev/null
+++ b/spec/javascripts/frequent_items/store/mutations_spec.js
@@ -0,0 +1,117 @@
+import state from '~/frequent_items/store/state';
+import mutations from '~/frequent_items/store/mutations';
+import * as types from '~/frequent_items/store/mutation_types';
+import {
+ mockNamespace,
+ mockStorageKey,
+ mockFrequentProjects,
+ mockSearchedProjects,
+ mockProcessedSearchedProjects,
+ mockSearchedGroups,
+ mockProcessedSearchedGroups,
+} from '../mock_data';
+
+describe('Frequent Items dropdown mutations', () => {
+ let stateCopy;
+
+ beforeEach(() => {
+ stateCopy = state();
+ });
+
+ describe('SET_NAMESPACE', () => {
+ it('should set namespace', () => {
+ mutations[types.SET_NAMESPACE](stateCopy, mockNamespace);
+
+ expect(stateCopy.namespace).toEqual(mockNamespace);
+ });
+ });
+
+ describe('SET_STORAGE_KEY', () => {
+ it('should set storage key', () => {
+ mutations[types.SET_STORAGE_KEY](stateCopy, mockStorageKey);
+
+ expect(stateCopy.storageKey).toEqual(mockStorageKey);
+ });
+ });
+
+ describe('SET_SEARCH_QUERY', () => {
+ it('should set search query', () => {
+ const searchQuery = 'gitlab-ce';
+
+ mutations[types.SET_SEARCH_QUERY](stateCopy, searchQuery);
+
+ expect(stateCopy.searchQuery).toEqual(searchQuery);
+ });
+ });
+
+ describe('REQUEST_FREQUENT_ITEMS', () => {
+ it('should set view states when requesting frequent items', () => {
+ mutations[types.REQUEST_FREQUENT_ITEMS](stateCopy);
+
+ expect(stateCopy.isLoadingItems).toEqual(true);
+ expect(stateCopy.hasSearchQuery).toEqual(false);
+ });
+ });
+
+ describe('RECEIVE_FREQUENT_ITEMS_SUCCESS', () => {
+ it('should set view states when receiving frequent items', () => {
+ mutations[types.RECEIVE_FREQUENT_ITEMS_SUCCESS](stateCopy, mockFrequentProjects);
+
+ expect(stateCopy.items).toEqual(mockFrequentProjects);
+ expect(stateCopy.isLoadingItems).toEqual(false);
+ expect(stateCopy.hasSearchQuery).toEqual(false);
+ expect(stateCopy.isFetchFailed).toEqual(false);
+ });
+ });
+
+ describe('RECEIVE_FREQUENT_ITEMS_ERROR', () => {
+ it('should set items and view states when error occurs retrieving frequent items', () => {
+ mutations[types.RECEIVE_FREQUENT_ITEMS_ERROR](stateCopy);
+
+ expect(stateCopy.items).toEqual([]);
+ expect(stateCopy.isLoadingItems).toEqual(false);
+ expect(stateCopy.hasSearchQuery).toEqual(false);
+ expect(stateCopy.isFetchFailed).toEqual(true);
+ });
+ });
+
+ describe('REQUEST_SEARCHED_ITEMS', () => {
+ it('should set view states when requesting searched items', () => {
+ mutations[types.REQUEST_SEARCHED_ITEMS](stateCopy);
+
+ expect(stateCopy.isLoadingItems).toEqual(true);
+ expect(stateCopy.hasSearchQuery).toEqual(true);
+ });
+ });
+
+ describe('RECEIVE_SEARCHED_ITEMS_SUCCESS', () => {
+ it('should set items and view states when receiving searched items', () => {
+ mutations[types.RECEIVE_SEARCHED_ITEMS_SUCCESS](stateCopy, mockSearchedProjects);
+
+ expect(stateCopy.items).toEqual(mockProcessedSearchedProjects);
+ expect(stateCopy.isLoadingItems).toEqual(false);
+ expect(stateCopy.hasSearchQuery).toEqual(true);
+ expect(stateCopy.isFetchFailed).toEqual(false);
+ });
+
+ it('should also handle the different `full_name` key for namespace in groups payload', () => {
+ mutations[types.RECEIVE_SEARCHED_ITEMS_SUCCESS](stateCopy, mockSearchedGroups);
+
+ expect(stateCopy.items).toEqual(mockProcessedSearchedGroups);
+ expect(stateCopy.isLoadingItems).toEqual(false);
+ expect(stateCopy.hasSearchQuery).toEqual(true);
+ expect(stateCopy.isFetchFailed).toEqual(false);
+ });
+ });
+
+ describe('RECEIVE_SEARCHED_ITEMS_ERROR', () => {
+ it('should set view states when error occurs retrieving searched items', () => {
+ mutations[types.RECEIVE_SEARCHED_ITEMS_ERROR](stateCopy);
+
+ expect(stateCopy.items).toEqual([]);
+ expect(stateCopy.isLoadingItems).toEqual(false);
+ expect(stateCopy.hasSearchQuery).toEqual(true);
+ expect(stateCopy.isFetchFailed).toEqual(true);
+ });
+ });
+});
diff --git a/spec/javascripts/frequent_items/utils_spec.js b/spec/javascripts/frequent_items/utils_spec.js
new file mode 100644
index 00000000000..cd27d79b29a
--- /dev/null
+++ b/spec/javascripts/frequent_items/utils_spec.js
@@ -0,0 +1,89 @@
+import bp from '~/breakpoints';
+import { isMobile, getTopFrequentItems, updateExistingFrequentItem } from '~/frequent_items/utils';
+import { HOUR_IN_MS, FREQUENT_ITEMS } from '~/frequent_items/constants';
+import { mockProject, unsortedFrequentItems, sortedFrequentItems } from './mock_data';
+
+describe('Frequent Items utils spec', () => {
+ describe('isMobile', () => {
+ it('returns true when the screen is small ', () => {
+ spyOn(bp, 'getBreakpointSize').and.returnValue('sm');
+
+ expect(isMobile()).toBe(true);
+ });
+
+ it('returns true when the screen is extra-small ', () => {
+ spyOn(bp, 'getBreakpointSize').and.returnValue('xs');
+
+ expect(isMobile()).toBe(true);
+ });
+
+ it('returns false when the screen is larger than small ', () => {
+ spyOn(bp, 'getBreakpointSize').and.returnValue('md');
+
+ expect(isMobile()).toBe(false);
+ });
+ });
+
+ describe('getTopFrequentItems', () => {
+ it('returns empty array if no items provided', () => {
+ const result = getTopFrequentItems();
+
+ expect(result.length).toBe(0);
+ });
+
+ it('returns correct amount of items for mobile', () => {
+ spyOn(bp, 'getBreakpointSize').and.returnValue('sm');
+ const result = getTopFrequentItems(unsortedFrequentItems);
+
+ expect(result.length).toBe(FREQUENT_ITEMS.LIST_COUNT_MOBILE);
+ });
+
+ it('returns correct amount of items for desktop', () => {
+ spyOn(bp, 'getBreakpointSize').and.returnValue('lg');
+ const result = getTopFrequentItems(unsortedFrequentItems);
+
+ expect(result.length).toBe(FREQUENT_ITEMS.LIST_COUNT_DESKTOP);
+ });
+
+ it('sorts frequent items in order of frequency and lastAccessedOn', () => {
+ spyOn(bp, 'getBreakpointSize').and.returnValue('lg');
+ const result = getTopFrequentItems(unsortedFrequentItems);
+ const expectedResult = sortedFrequentItems.slice(0, FREQUENT_ITEMS.LIST_COUNT_DESKTOP);
+
+ expect(result).toEqual(expectedResult);
+ });
+ });
+
+ describe('updateExistingFrequentItem', () => {
+ let mockedProject;
+
+ beforeEach(() => {
+ mockedProject = {
+ ...mockProject,
+ frequency: 1,
+ lastAccessedOn: 1497979281815,
+ };
+ });
+
+ it('updates item if accessed over an hour ago', () => {
+ const newTimestamp = Date.now() + HOUR_IN_MS + 1;
+ const newItem = {
+ ...mockedProject,
+ lastAccessedOn: newTimestamp,
+ };
+ const result = updateExistingFrequentItem(mockedProject, newItem);
+
+ expect(result.frequency).toBe(mockedProject.frequency + 1);
+ });
+
+ it('does not update item if accessed within the hour', () => {
+ const newItem = {
+ ...mockedProject,
+ lastAccessedOn: mockedProject.lastAccessedOn + HOUR_IN_MS,
+ };
+ const result = updateExistingFrequentItem(mockedProject, newItem);
+
+ expect(result.frequency).toBe(mockedProject.frequency);
+ });
+ });
+});
diff --git a/spec/javascripts/gl_dropdown_spec.js b/spec/javascripts/gl_dropdown_spec.js
index 7f9c4811fba..af58dff7da7 100644
--- a/spec/javascripts/gl_dropdown_spec.js
+++ b/spec/javascripts/gl_dropdown_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable comma-dangle, no-param-reassign, no-unused-expressions, max-len */
+/* eslint-disable comma-dangle, no-param-reassign */
import $ from 'jquery';
import GLDropdown from '~/gl_dropdown';
@@ -70,9 +70,9 @@ describe('glDropdown', function describeDropdown() {
it('should open on click', () => {
initDropDown.call(this, false);
- expect(this.dropdownContainerElement).not.toHaveClass('open');
+ expect(this.dropdownContainerElement).not.toHaveClass('show');
this.dropdownButtonElement.click();
- expect(this.dropdownContainerElement).toHaveClass('open');
+ expect(this.dropdownContainerElement).toHaveClass('show');
});
it('escapes HTML as text', () => {
@@ -134,12 +134,12 @@ describe('glDropdown', function describeDropdown() {
});
it('should click the selected item on ENTER keypress', () => {
- expect(this.dropdownContainerElement).toHaveClass('open');
+ expect(this.dropdownContainerElement).toHaveClass('show');
const randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0;
navigateWithKeys('down', randomIndex, () => {
const visitUrl = spyOnDependency(GLDropdown, 'visitUrl').and.stub();
navigateWithKeys('enter', null, () => {
- expect(this.dropdownContainerElement).not.toHaveClass('open');
+ expect(this.dropdownContainerElement).not.toHaveClass('show');
const link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement);
expect(link).toHaveClass('is-active');
const linkedLocation = link.attr('href');
@@ -149,13 +149,13 @@ describe('glDropdown', function describeDropdown() {
});
it('should close on ESC keypress', () => {
- expect(this.dropdownContainerElement).toHaveClass('open');
+ expect(this.dropdownContainerElement).toHaveClass('show');
this.dropdownContainerElement.trigger({
type: 'keyup',
which: ARROW_KEYS.ESC,
keyCode: ARROW_KEYS.ESC
});
- expect(this.dropdownContainerElement).not.toHaveClass('open');
+ expect(this.dropdownContainerElement).not.toHaveClass('show');
});
});
diff --git a/spec/javascripts/gl_field_errors_spec.js b/spec/javascripts/gl_field_errors_spec.js
index 4e93fd91751..21c462cd040 100644
--- a/spec/javascripts/gl_field_errors_spec.js
+++ b/spec/javascripts/gl_field_errors_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable space-before-function-paren, arrow-body-style */
+/* eslint-disable arrow-body-style */
import $ from 'jquery';
import GlFieldErrors from '~/gl_field_errors';
@@ -8,7 +8,9 @@ describe('GL Style Field Errors', function() {
beforeEach(function() {
loadFixtures('static/gl_field_errors.html.raw');
- const $form = this.$form = $('form.gl-show-field-errors');
+ const $form = $('form.gl-show-field-errors');
+
+ this.$form = $form;
this.fieldErrors = new GlFieldErrors($form);
});
@@ -16,7 +18,7 @@ describe('GL Style Field Errors', function() {
expect(this.$form).toBeDefined();
expect(this.$form.length).toBe(1);
expect(this.fieldErrors).toBeDefined();
- const inputs = this.fieldErrors.state.inputs;
+ const { inputs } = this.fieldErrors.state;
expect(inputs.length).toBe(4);
});
diff --git a/spec/javascripts/groups/components/app_spec.js b/spec/javascripts/groups/components/app_spec.js
index 2b92c485f41..03d4b472b87 100644
--- a/spec/javascripts/groups/components/app_spec.js
+++ b/spec/javascripts/groups/components/app_spec.js
@@ -67,7 +67,7 @@ describe('AppComponent', () => {
it('should return list of groups from store', () => {
spyOn(vm.store, 'getGroups');
- const groups = vm.groups;
+ const { groups } = vm;
expect(vm.store.getGroups).toHaveBeenCalled();
expect(groups).not.toBeDefined();
});
@@ -77,7 +77,7 @@ describe('AppComponent', () => {
it('should return pagination info from store', () => {
spyOn(vm.store, 'getPaginationInfo');
- const pageInfo = vm.pageInfo;
+ const { pageInfo } = vm;
expect(vm.store.getPaginationInfo).toHaveBeenCalled();
expect(pageInfo).not.toBeDefined();
});
@@ -293,7 +293,7 @@ describe('AppComponent', () => {
beforeEach(() => {
groupItem = Object.assign({}, mockParentGroupItem);
groupItem.children = mockChildren;
- childGroupItem = groupItem.children[0];
+ [childGroupItem] = groupItem.children;
groupItem.isChildrenLoading = false;
vm.targetGroup = childGroupItem;
vm.targetParentGroup = groupItem;
diff --git a/spec/javascripts/groups/components/group_item_spec.js b/spec/javascripts/groups/components/group_item_spec.js
index 49a139855c8..d0cac5efc40 100644
--- a/spec/javascripts/groups/components/group_item_spec.js
+++ b/spec/javascripts/groups/components/group_item_spec.js
@@ -41,7 +41,7 @@ describe('GroupItemComponent', () => {
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;
+ const { rowClass } = vm;
expect(Object.keys(rowClass).length).toBe(classes.length);
Object.keys(rowClass).forEach((className) => {
diff --git a/spec/javascripts/helpers/index.js b/spec/javascripts/helpers/index.js
new file mode 100644
index 00000000000..d2c5caf0bdb
--- /dev/null
+++ b/spec/javascripts/helpers/index.js
@@ -0,0 +1,3 @@
+import mountComponent, { mountComponentWithStore } from './vue_mount_component_helper';
+
+export { mountComponent, mountComponentWithStore };
diff --git a/spec/javascripts/helpers/init_vue_mr_page_helper.js b/spec/javascripts/helpers/init_vue_mr_page_helper.js
new file mode 100644
index 00000000000..fc4288eb15b
--- /dev/null
+++ b/spec/javascripts/helpers/init_vue_mr_page_helper.js
@@ -0,0 +1,46 @@
+import initMRPage from '~/mr_notes/index';
+import axios from '~/lib/utils/axios_utils';
+import MockAdapter from 'axios-mock-adapter';
+import { userDataMock, notesDataMock, noteableDataMock } from '../notes/mock_data';
+import diffFileMockData from '../diffs/mock_data/diff_file';
+
+export default function initVueMRPage() {
+ const mrTestEl = document.createElement('div');
+ mrTestEl.className = 'js-merge-request-test';
+ document.body.appendChild(mrTestEl);
+
+ const diffsAppEndpoint = '/diffs/app/endpoint';
+ const diffsAppProjectPath = 'testproject';
+ const mrEl = document.createElement('div');
+ mrEl.className = 'merge-request fixture-mr';
+ mrEl.setAttribute('data-mr-action', 'diffs');
+ mrTestEl.appendChild(mrEl);
+
+ const mrDiscussionsEl = document.createElement('div');
+ mrDiscussionsEl.id = 'js-vue-mr-discussions';
+ mrDiscussionsEl.setAttribute('data-current-user-data', JSON.stringify(userDataMock));
+ mrDiscussionsEl.setAttribute('data-noteable-data', JSON.stringify(noteableDataMock));
+ mrDiscussionsEl.setAttribute('data-notes-data', JSON.stringify(notesDataMock));
+ mrDiscussionsEl.setAttribute('data-noteable-type', 'merge-request');
+ mrTestEl.appendChild(mrDiscussionsEl);
+
+ const discussionCounterEl = document.createElement('div');
+ discussionCounterEl.id = 'js-vue-discussion-counter';
+ mrTestEl.appendChild(discussionCounterEl);
+
+ const diffsAppEl = document.createElement('div');
+ diffsAppEl.id = 'js-diffs-app';
+ diffsAppEl.setAttribute('data-endpoint', diffsAppEndpoint);
+ diffsAppEl.setAttribute('data-project-path', diffsAppProjectPath);
+ diffsAppEl.setAttribute('data-current-user-data', JSON.stringify(userDataMock));
+ mrTestEl.appendChild(diffsAppEl);
+
+ const mock = new MockAdapter(axios);
+ mock.onGet(diffsAppEndpoint).reply(200, {
+ branch_name: 'foo',
+ diff_files: [diffFileMockData],
+ });
+
+ initMRPage();
+ return mock;
+}
diff --git a/spec/javascripts/helpers/user_mock_data_helper.js b/spec/javascripts/helpers/user_mock_data_helper.js
index 323fee3767e..f6c3ce5aecc 100644
--- a/spec/javascripts/helpers/user_mock_data_helper.js
+++ b/spec/javascripts/helpers/user_mock_data_helper.js
@@ -1,7 +1,7 @@
export default {
createNumberRandomUsers(numberUsers) {
const users = [];
- for (let i = 0; i < numberUsers; i = i += 1) {
+ for (let i = 0; i < numberUsers; i += 1) {
users.push(
{
avatar: 'https://gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
diff --git a/spec/javascripts/helpers/vue_mount_component_helper.js b/spec/javascripts/helpers/vue_mount_component_helper.js
index a34a1add4e0..5ba17ecf5b5 100644
--- a/spec/javascripts/helpers/vue_mount_component_helper.js
+++ b/spec/javascripts/helpers/vue_mount_component_helper.js
@@ -1,30 +1,18 @@
-import Vue from 'vue';
-
-const mountComponent = (Component, props = {}, el = null) => new Component({
- propsData: props,
-}).$mount(el);
-
-export const createComponentWithStore = (Component, store, propsData = {}) => new Component({
- store,
- propsData,
-});
+const mountComponent = (Component, props = {}, el = null) =>
+ new Component({
+ propsData: props,
+ }).$mount(el);
-export const createComponentWithMixin = (mixins = [], state = {}, props = {}, template = '<div></div>') => {
- const Component = Vue.extend({
- template,
- mixins,
- data() {
- return props;
- },
+export const createComponentWithStore = (Component, store, propsData = {}) =>
+ new Component({
+ store,
+ propsData,
});
- return mountComponent(Component, props);
-};
-
export const mountComponentWithStore = (Component, { el, props, store }) =>
new Component({
store,
- propsData: props || { },
+ propsData: props || {},
}).$mount(el);
export default mountComponent;
diff --git a/spec/javascripts/helpers/vue_resource_helper.js b/spec/javascripts/helpers/vue_resource_helper.js
index 0d1bf5e2e80..70b7ec4e574 100644
--- a/spec/javascripts/helpers/vue_resource_helper.js
+++ b/spec/javascripts/helpers/vue_resource_helper.js
@@ -5,7 +5,6 @@ export const headersInterceptor = (request, next) => {
response.headers.forEach((value, key) => {
headers[key] = value;
});
- // eslint-disable-next-line no-param-reassign
response.headers = headers;
});
};
diff --git a/spec/javascripts/helpers/wait_for_promises.js b/spec/javascripts/helpers/wait_for_promises.js
new file mode 100644
index 00000000000..1d2b53fc770
--- /dev/null
+++ b/spec/javascripts/helpers/wait_for_promises.js
@@ -0,0 +1 @@
+export default () => new Promise(resolve => requestAnimationFrame(resolve));
diff --git a/spec/javascripts/ide/components/commit_sidebar/form_spec.js b/spec/javascripts/ide/components/commit_sidebar/form_spec.js
index 8b47a365582..b7a7afe4db4 100644
--- a/spec/javascripts/ide/components/commit_sidebar/form_spec.js
+++ b/spec/javascripts/ide/components/commit_sidebar/form_spec.js
@@ -16,6 +16,7 @@ describe('IDE commit form', () => {
store.state.changedFiles.push('test');
store.state.currentProjectId = 'abcproject';
+ store.state.currentBranchId = 'master';
Vue.set(store.state.projects, 'abcproject', { ...projectData });
vm = createComponentWithStore(Component, store).$mount();
@@ -146,4 +147,16 @@ describe('IDE commit form', () => {
});
});
});
+
+ describe('commitButtonText', () => {
+ it('returns commit text when staged files exist', () => {
+ vm.$store.state.stagedFiles.push('testing');
+
+ expect(vm.commitButtonText).toBe('Commit');
+ });
+
+ it('returns stage & commit text when staged files do not exist', () => {
+ expect(vm.commitButtonText).toBe('Stage & Commit');
+ });
+ });
});
diff --git a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js
index cc7e0a3f26d..bf96170f703 100644
--- a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js
+++ b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js
@@ -19,6 +19,7 @@ describe('Multi-file editor commit sidebar list item', () => {
vm = createComponentWithStore(Component, store, {
file: f,
actionComponent: 'stage-button',
+ activeFileKey: `staged-${f.key}`,
}).$mount();
});
@@ -89,4 +90,20 @@ describe('Multi-file editor commit sidebar list item', () => {
});
});
});
+
+ describe('is active', () => {
+ it('does not add active class when dont keys match', () => {
+ expect(vm.$el.querySelector('.is-active')).toBe(null);
+ });
+
+ it('adds active class when keys match', done => {
+ vm.keyPrefix = 'staged';
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.is-active')).not.toBe(null);
+
+ done();
+ });
+ });
+ });
});
diff --git a/spec/javascripts/ide/components/commit_sidebar/list_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_spec.js
index 54625ef90f8..b786be55019 100644
--- a/spec/javascripts/ide/components/commit_sidebar/list_spec.js
+++ b/spec/javascripts/ide/components/commit_sidebar/list_spec.js
@@ -16,7 +16,10 @@ describe('Multi-file editor commit sidebar list', () => {
iconName: 'staged',
action: 'stageAllChanges',
actionBtnText: 'stage all',
+ actionBtnIcon: 'history',
itemActionComponent: 'stage-button',
+ activeFileKey: 'staged-testing',
+ keyPrefix: 'staged',
});
vm.$store.state.rightPanelCollapsed = false;
@@ -40,7 +43,7 @@ describe('Multi-file editor commit sidebar list', () => {
});
it('renders list', () => {
- expect(vm.$el.querySelectorAll('li').length).toBe(1);
+ expect(vm.$el.querySelectorAll('.multi-file-commit-list > li').length).toBe(1);
});
});
diff --git a/spec/javascripts/ide/components/commit_sidebar/message_field_spec.js b/spec/javascripts/ide/components/commit_sidebar/message_field_spec.js
index d62d58101d6..942cc19f46d 100644
--- a/spec/javascripts/ide/components/commit_sidebar/message_field_spec.js
+++ b/spec/javascripts/ide/components/commit_sidebar/message_field_spec.js
@@ -13,6 +13,7 @@ describe('IDE commit message field', () => {
Component,
{
text: '',
+ placeholder: 'testing',
},
'#app',
);
diff --git a/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js b/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js
index 21bfe4be52f..ffc2a4c9ddb 100644
--- a/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js
+++ b/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js
@@ -114,4 +114,19 @@ describe('IDE commit sidebar radio group', () => {
});
});
});
+
+ describe('tooltipTitle', () => {
+ it('returns title when disabled', () => {
+ vm.title = 'test title';
+ vm.disabled = true;
+
+ expect(vm.tooltipTitle).toBe('test title');
+ });
+
+ it('returns blank when not disabled', () => {
+ vm.title = 'test title';
+
+ expect(vm.tooltipTitle).not.toBe('test title');
+ });
+ });
});
diff --git a/spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js b/spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js
index 6bf8710bda7..a5b906da8a1 100644
--- a/spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js
+++ b/spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js
@@ -39,7 +39,7 @@ describe('IDE stage file button', () => {
});
it('calls store with discard button', () => {
- vm.$el.querySelectorAll('.btn')[1].click();
+ vm.$el.querySelector('.dropdown-menu button').click();
expect(vm.discardFileChanges).toHaveBeenCalledWith(f.path);
});
diff --git a/spec/javascripts/ide/components/error_message_spec.js b/spec/javascripts/ide/components/error_message_spec.js
new file mode 100644
index 00000000000..430e8e2baa3
--- /dev/null
+++ b/spec/javascripts/ide/components/error_message_spec.js
@@ -0,0 +1,106 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import ErrorMessage from '~/ide/components/error_message.vue';
+import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
+import { resetStore } from '../helpers';
+
+describe('IDE error message component', () => {
+ const Component = Vue.extend(ErrorMessage);
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponentWithStore(Component, store, {
+ message: {
+ text: 'error message',
+ action: null,
+ actionText: null,
+ },
+ }).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ resetStore(vm.$store);
+ });
+
+ it('renders error message', () => {
+ expect(vm.$el.textContent).toContain('error message');
+ });
+
+ it('clears error message on click', () => {
+ spyOn(vm, 'setErrorMessage');
+
+ vm.$el.click();
+
+ expect(vm.setErrorMessage).toHaveBeenCalledWith(null);
+ });
+
+ describe('with action', () => {
+ let actionSpy;
+
+ beforeEach(done => {
+ actionSpy = jasmine.createSpy('action').and.returnValue(Promise.resolve());
+
+ vm.message.action = actionSpy;
+ vm.message.actionText = 'test action';
+ vm.message.actionPayload = 'testActionPayload';
+
+ vm.$nextTick(done);
+ });
+
+ it('renders action button', () => {
+ expect(vm.$el.querySelector('.flash-action')).not.toBe(null);
+ expect(vm.$el.textContent).toContain('test action');
+ });
+
+ it('does not clear error message on click', () => {
+ spyOn(vm, 'setErrorMessage');
+
+ vm.$el.click();
+
+ expect(vm.setErrorMessage).not.toHaveBeenCalled();
+ });
+
+ it('dispatches action', done => {
+ vm.$el.querySelector('.flash-action').click();
+
+ vm.$nextTick(() => {
+ expect(actionSpy).toHaveBeenCalledWith('testActionPayload');
+
+ done();
+ });
+ });
+
+ it('does not dispatch action when already loading', () => {
+ vm.isLoading = true;
+
+ vm.$el.querySelector('.flash-action').click();
+
+ expect(actionSpy).not.toHaveBeenCalledWith();
+ });
+
+ it('resets isLoading after click', done => {
+ vm.$el.querySelector('.flash-action').click();
+
+ expect(vm.isLoading).toBe(true);
+
+ vm.$nextTick(() => {
+ expect(vm.isLoading).toBe(false);
+
+ done();
+ });
+ });
+
+ it('shows loading icon when isLoading is true', done => {
+ expect(vm.$el.querySelector('.loading-container').style.display).not.toBe('');
+
+ vm.isLoading = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.loading-container').style.display).toBe('');
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/external_link_spec.js b/spec/javascripts/ide/components/external_link_spec.js
new file mode 100644
index 00000000000..b3d94c041fa
--- /dev/null
+++ b/spec/javascripts/ide/components/external_link_spec.js
@@ -0,0 +1,35 @@
+import Vue from 'vue';
+import externalLink from '~/ide/components/external_link.vue';
+import createVueComponent from '../../helpers/vue_mount_component_helper';
+import { file } from '../helpers';
+
+describe('ExternalLink', () => {
+ const activeFile = file();
+ let vm;
+
+ function createComponent() {
+ const ExternalLink = Vue.extend(externalLink);
+
+ activeFile.permalink = 'test';
+
+ return createVueComponent(ExternalLink, {
+ file: activeFile,
+ });
+ }
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders the external link with the correct href', done => {
+ activeFile.binary = true;
+ vm = createComponent();
+
+ vm.$nextTick(() => {
+ const openLink = vm.$el.querySelector('a');
+
+ expect(openLink.href).toMatch(`/${activeFile.permalink}`);
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/ide_file_buttons_spec.js b/spec/javascripts/ide/components/ide_file_buttons_spec.js
deleted file mode 100644
index 8ac8d1b2acf..00000000000
--- a/spec/javascripts/ide/components/ide_file_buttons_spec.js
+++ /dev/null
@@ -1,61 +0,0 @@
-import Vue from 'vue';
-import repoFileButtons from '~/ide/components/ide_file_buttons.vue';
-import createVueComponent from '../../helpers/vue_mount_component_helper';
-import { file } from '../helpers';
-
-describe('RepoFileButtons', () => {
- const activeFile = file();
- let vm;
-
- function createComponent() {
- const RepoFileButtons = Vue.extend(repoFileButtons);
-
- activeFile.rawPath = 'test';
- activeFile.blamePath = 'test';
- activeFile.commitsPath = 'test';
-
- return createVueComponent(RepoFileButtons, {
- file: activeFile,
- });
- }
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('renders Raw, Blame, History and Permalink', done => {
- vm = createComponent();
-
- vm.$nextTick(() => {
- const raw = vm.$el.querySelector('.raw');
- const blame = vm.$el.querySelector('.blame');
- const history = vm.$el.querySelector('.history');
-
- expect(raw.href).toMatch(`/${activeFile.rawPath}`);
- expect(raw.getAttribute('data-original-title')).toEqual('Raw');
- expect(blame.href).toMatch(`/${activeFile.blamePath}`);
- expect(blame.getAttribute('data-original-title')).toEqual('Blame');
- expect(history.href).toMatch(`/${activeFile.commitsPath}`);
- expect(history.getAttribute('data-original-title')).toEqual('History');
- expect(vm.$el.querySelector('.permalink').getAttribute('data-original-title')).toEqual(
- 'Permalink',
- );
-
- done();
- });
- });
-
- it('renders Download', done => {
- activeFile.binary = true;
- vm = createComponent();
-
- vm.$nextTick(() => {
- const raw = vm.$el.querySelector('.raw');
-
- expect(raw.href).toMatch(`/${activeFile.rawPath}`);
- expect(raw.getAttribute('data-original-title')).toEqual('Download');
-
- done();
- });
- });
-});
diff --git a/spec/javascripts/ide/components/ide_spec.js b/spec/javascripts/ide/components/ide_spec.js
index 045a60e56a0..708c9fe69af 100644
--- a/spec/javascripts/ide/components/ide_spec.js
+++ b/spec/javascripts/ide/components/ide_spec.js
@@ -114,4 +114,18 @@ describe('ide component', () => {
expect(vm.mousetrapStopCallback(null, document.querySelector('.inputarea'), 't')).toBe(true);
});
});
+
+ it('shows error message when set', done => {
+ expect(vm.$el.querySelector('.flash-container')).toBe(null);
+
+ vm.$store.state.errorMessage = {
+ text: 'error',
+ };
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.flash-container')).not.toBe(null);
+
+ done();
+ });
+ });
});
diff --git a/spec/javascripts/ide/components/jobs/detail/description_spec.js b/spec/javascripts/ide/components/jobs/detail/description_spec.js
new file mode 100644
index 00000000000..9b715a41499
--- /dev/null
+++ b/spec/javascripts/ide/components/jobs/detail/description_spec.js
@@ -0,0 +1,28 @@
+import Vue from 'vue';
+import Description from '~/ide/components/jobs/detail/description.vue';
+import mountComponent from '../../../../helpers/vue_mount_component_helper';
+import { jobs } from '../../../mock_data';
+
+describe('IDE job description', () => {
+ const Component = Vue.extend(Description);
+ let vm;
+
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ job: jobs[0],
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders job details', () => {
+ expect(vm.$el.textContent).toContain('#1');
+ expect(vm.$el.textContent).toContain('test');
+ });
+
+ it('renders CI icon', () => {
+ expect(vm.$el.querySelector('.ci-status-icon .ic-status_passed_borderless')).not.toBe(null);
+ });
+});
diff --git a/spec/javascripts/ide/components/jobs/detail/scroll_button_spec.js b/spec/javascripts/ide/components/jobs/detail/scroll_button_spec.js
new file mode 100644
index 00000000000..fff382a107f
--- /dev/null
+++ b/spec/javascripts/ide/components/jobs/detail/scroll_button_spec.js
@@ -0,0 +1,59 @@
+import Vue from 'vue';
+import ScrollButton from '~/ide/components/jobs/detail/scroll_button.vue';
+import mountComponent from '../../../../helpers/vue_mount_component_helper';
+
+describe('IDE job log scroll button', () => {
+ const Component = Vue.extend(ScrollButton);
+ let vm;
+
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ direction: 'up',
+ disabled: false,
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('iconName', () => {
+ ['up', 'down'].forEach(direction => {
+ it(`returns icon name for ${direction}`, () => {
+ vm.direction = direction;
+
+ expect(vm.iconName).toBe(`scroll_${direction}`);
+ });
+ });
+ });
+
+ describe('tooltipTitle', () => {
+ it('returns title for up', () => {
+ expect(vm.tooltipTitle).toBe('Scroll to top');
+ });
+
+ it('returns title for down', () => {
+ vm.direction = 'down';
+
+ expect(vm.tooltipTitle).toBe('Scroll to bottom');
+ });
+ });
+
+ it('emits click event on click', () => {
+ spyOn(vm, '$emit');
+
+ vm.$el.querySelector('.btn-scroll').click();
+
+ expect(vm.$emit).toHaveBeenCalledWith('click');
+ });
+
+ it('disables button when disabled is true', done => {
+ vm.disabled = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.btn-scroll').hasAttribute('disabled')).toBe(true);
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/jobs/detail_spec.js b/spec/javascripts/ide/components/jobs/detail_spec.js
new file mode 100644
index 00000000000..8f8d4b9709e
--- /dev/null
+++ b/spec/javascripts/ide/components/jobs/detail_spec.js
@@ -0,0 +1,185 @@
+import Vue from 'vue';
+import JobDetail from '~/ide/components/jobs/detail.vue';
+import { createStore } from '~/ide/stores';
+import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
+import { jobs } from '../../mock_data';
+
+describe('IDE jobs detail view', () => {
+ const Component = Vue.extend(JobDetail);
+ let vm;
+
+ beforeEach(() => {
+ const store = createStore();
+
+ store.state.pipelines.detailJob = {
+ ...jobs[0],
+ isLoading: true,
+ output: 'testing',
+ rawPath: `${gl.TEST_HOST}/raw`,
+ };
+
+ vm = createComponentWithStore(Component, store);
+
+ spyOn(vm, 'fetchJobTrace').and.returnValue(Promise.resolve());
+
+ vm = vm.$mount();
+
+ spyOn(vm.$refs.buildTrace, 'scrollTo');
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('calls fetchJobTrace on mount', () => {
+ expect(vm.fetchJobTrace).toHaveBeenCalled();
+ });
+
+ it('scrolls to bottom on mount', done => {
+ setTimeout(() => {
+ expect(vm.$refs.buildTrace.scrollTo).toHaveBeenCalled();
+
+ done();
+ });
+ });
+
+ it('renders job output', () => {
+ expect(vm.$el.querySelector('.bash').textContent).toContain('testing');
+ });
+
+ it('renders empty message output', done => {
+ vm.$store.state.pipelines.detailJob.output = '';
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.bash').textContent).toContain('No messages were logged');
+
+ done();
+ });
+ });
+
+ it('renders loading icon', () => {
+ expect(vm.$el.querySelector('.build-loader-animation')).not.toBe(null);
+ expect(vm.$el.querySelector('.build-loader-animation').style.display).toBe('');
+ });
+
+ it('hides output when loading', () => {
+ expect(vm.$el.querySelector('.bash')).not.toBe(null);
+ expect(vm.$el.querySelector('.bash').style.display).toBe('none');
+ });
+
+ it('hide loading icon when isLoading is false', done => {
+ vm.$store.state.pipelines.detailJob.isLoading = false;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.build-loader-animation').style.display).toBe('none');
+
+ done();
+ });
+ });
+
+ it('resets detailJob when clicking header button', () => {
+ spyOn(vm, 'setDetailJob');
+
+ vm.$el.querySelector('.btn').click();
+
+ expect(vm.setDetailJob).toHaveBeenCalledWith(null);
+ });
+
+ it('renders raw path link', () => {
+ expect(vm.$el.querySelector('.controllers-buttons').getAttribute('href')).toBe(
+ `${gl.TEST_HOST}/raw`,
+ );
+ });
+
+ describe('scroll buttons', () => {
+ it('triggers scrollDown when clicking down button', done => {
+ spyOn(vm, 'scrollDown');
+
+ vm.$el.querySelectorAll('.btn-scroll')[1].click();
+
+ vm.$nextTick(() => {
+ expect(vm.scrollDown).toHaveBeenCalled();
+
+ done();
+ });
+ });
+
+ it('triggers scrollUp when clicking up button', done => {
+ spyOn(vm, 'scrollUp');
+
+ vm.scrollPos = 1;
+
+ vm
+ .$nextTick()
+ .then(() => vm.$el.querySelector('.btn-scroll').click())
+ .then(() => vm.$nextTick())
+ .then(() => {
+ expect(vm.scrollUp).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('scrollDown', () => {
+ it('scrolls build trace to bottom', () => {
+ spyOnProperty(vm.$refs.buildTrace, 'scrollHeight').and.returnValue(1000);
+
+ vm.scrollDown();
+
+ expect(vm.$refs.buildTrace.scrollTo).toHaveBeenCalledWith(0, 1000);
+ });
+ });
+
+ describe('scrollUp', () => {
+ it('scrolls build trace to top', () => {
+ vm.scrollUp();
+
+ expect(vm.$refs.buildTrace.scrollTo).toHaveBeenCalledWith(0, 0);
+ });
+ });
+
+ describe('scrollBuildLog', () => {
+ beforeEach(() => {
+ spyOnProperty(vm.$refs.buildTrace, 'offsetHeight').and.returnValue(100);
+ spyOnProperty(vm.$refs.buildTrace, 'scrollHeight').and.returnValue(200);
+ });
+
+ it('sets scrollPos to bottom when at the bottom', done => {
+ spyOnProperty(vm.$refs.buildTrace, 'scrollTop').and.returnValue(100);
+
+ vm.scrollBuildLog();
+
+ setTimeout(() => {
+ expect(vm.scrollPos).toBe(1);
+
+ done();
+ });
+ });
+
+ it('sets scrollPos to top when at the top', done => {
+ spyOnProperty(vm.$refs.buildTrace, 'scrollTop').and.returnValue(0);
+ vm.scrollPos = 1;
+
+ vm.scrollBuildLog();
+
+ setTimeout(() => {
+ expect(vm.scrollPos).toBe(0);
+
+ done();
+ });
+ });
+
+ it('resets scrollPos when not at top or bottom', done => {
+ spyOnProperty(vm.$refs.buildTrace, 'scrollTop').and.returnValue(10);
+
+ vm.scrollBuildLog();
+
+ setTimeout(() => {
+ expect(vm.scrollPos).toBe('');
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/jobs/item_spec.js b/spec/javascripts/ide/components/jobs/item_spec.js
new file mode 100644
index 00000000000..79e07f00e7b
--- /dev/null
+++ b/spec/javascripts/ide/components/jobs/item_spec.js
@@ -0,0 +1,39 @@
+import Vue from 'vue';
+import JobItem from '~/ide/components/jobs/item.vue';
+import mountComponent from '../../../helpers/vue_mount_component_helper';
+import { jobs } from '../../mock_data';
+
+describe('IDE jobs item', () => {
+ const Component = Vue.extend(JobItem);
+ const job = jobs[0];
+ let vm;
+
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ job,
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders job details', () => {
+ expect(vm.$el.textContent).toContain(job.name);
+ expect(vm.$el.textContent).toContain(`#${job.id}`);
+ });
+
+ it('renders CI icon', () => {
+ expect(vm.$el.querySelector('.ic-status_passed_borderless')).not.toBe(null);
+ });
+
+ it('does not render view logs button if not started', done => {
+ vm.job.started = false;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.btn')).toBe(null);
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/jobs/list_spec.js b/spec/javascripts/ide/components/jobs/list_spec.js
new file mode 100644
index 00000000000..b24853c56fa
--- /dev/null
+++ b/spec/javascripts/ide/components/jobs/list_spec.js
@@ -0,0 +1,67 @@
+import Vue from 'vue';
+import StageList from '~/ide/components/jobs/list.vue';
+import { createStore } from '~/ide/stores';
+import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
+import { stages, jobs } from '../../mock_data';
+
+describe('IDE stages list', () => {
+ const Component = Vue.extend(StageList);
+ let vm;
+
+ beforeEach(() => {
+ const store = createStore();
+
+ vm = createComponentWithStore(Component, store, {
+ stages: stages.map((mappedState, i) => ({
+ ...mappedState,
+ id: i,
+ dropdownPath: mappedState.dropdown_path,
+ jobs: [...jobs],
+ isLoading: false,
+ isCollapsed: false,
+ })),
+ loading: false,
+ });
+
+ spyOn(vm, 'fetchJobs');
+ spyOn(vm, 'toggleStageCollapsed');
+
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders list of stages', () => {
+ expect(vm.$el.querySelectorAll('.card').length).toBe(2);
+ });
+
+ it('renders loading icon when no stages & is loading', done => {
+ vm.stages = [];
+ vm.loading = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.loading-container')).not.toBe(null);
+
+ done();
+ });
+ });
+
+ it('calls toggleStageCollapsed when clicking stage header', done => {
+ vm.$el.querySelector('.card-header').click();
+
+ vm.$nextTick(() => {
+ expect(vm.toggleStageCollapsed).toHaveBeenCalledWith(0);
+
+ done();
+ });
+ });
+
+ it('calls fetchJobs when stage is mounted', () => {
+ expect(vm.fetchJobs.calls.count()).toBe(stages.length);
+
+ expect(vm.fetchJobs.calls.argsFor(0)).toEqual([vm.stages[0]]);
+ expect(vm.fetchJobs.calls.argsFor(1)).toEqual([vm.stages[1]]);
+ });
+});
diff --git a/spec/javascripts/ide/components/jobs/stage_spec.js b/spec/javascripts/ide/components/jobs/stage_spec.js
new file mode 100644
index 00000000000..fc3831f2d05
--- /dev/null
+++ b/spec/javascripts/ide/components/jobs/stage_spec.js
@@ -0,0 +1,95 @@
+import Vue from 'vue';
+import Stage from '~/ide/components/jobs/stage.vue';
+import { stages, jobs } from '../../mock_data';
+
+describe('IDE pipeline stage', () => {
+ const Component = Vue.extend(Stage);
+ let vm;
+ let stage;
+
+ beforeEach(() => {
+ stage = {
+ ...stages[0],
+ id: 0,
+ dropdownPath: stages[0].dropdown_path,
+ jobs: [...jobs],
+ isLoading: false,
+ isCollapsed: false,
+ };
+
+ vm = new Component({
+ propsData: { stage },
+ });
+
+ spyOn(vm, '$emit');
+
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('emits fetch event when mounted', () => {
+ expect(vm.$emit).toHaveBeenCalledWith('fetch', vm.stage);
+ });
+
+ it('renders stages details', () => {
+ expect(vm.$el.textContent).toContain(vm.stage.name);
+ });
+
+ it('renders CI icon', () => {
+ expect(vm.$el.querySelector('.ic-status_failed')).not.toBe(null);
+ });
+
+ describe('collapsed', () => {
+ it('emits event when clicking header', done => {
+ vm.$el.querySelector('.card-header').click();
+
+ vm.$nextTick(() => {
+ expect(vm.$emit).toHaveBeenCalledWith('toggleCollapsed', vm.stage.id);
+
+ done();
+ });
+ });
+
+ it('toggles collapse status when collapsed', done => {
+ vm.stage.isCollapsed = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.card-body').style.display).toBe('none');
+
+ done();
+ });
+ });
+
+ it('sets border bottom class when collapsed', done => {
+ vm.stage.isCollapsed = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.card-header').classList).toContain('border-bottom-0');
+
+ done();
+ });
+ });
+ });
+
+ it('renders jobs count', () => {
+ expect(vm.$el.querySelector('.badge').textContent).toContain('4');
+ });
+
+ it('renders loading icon when no jobs and isLoading is true', done => {
+ vm.stage.isLoading = true;
+ vm.stage.jobs = [];
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.loading-container')).not.toBe(null);
+
+ done();
+ });
+ });
+
+ it('renders list of jobs', () => {
+ expect(vm.$el.querySelectorAll('.ide-job-item').length).toBe(4);
+ });
+});
diff --git a/spec/javascripts/ide/components/merge_requests/dropdown_spec.js b/spec/javascripts/ide/components/merge_requests/dropdown_spec.js
new file mode 100644
index 00000000000..74884c9a362
--- /dev/null
+++ b/spec/javascripts/ide/components/merge_requests/dropdown_spec.js
@@ -0,0 +1,47 @@
+import Vue from 'vue';
+import { createStore } from '~/ide/stores';
+import Dropdown from '~/ide/components/merge_requests/dropdown.vue';
+import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
+import { mergeRequests } from '../../mock_data';
+
+describe('IDE merge requests dropdown', () => {
+ const Component = Vue.extend(Dropdown);
+ let vm;
+
+ beforeEach(() => {
+ const store = createStore();
+
+ vm = createComponentWithStore(Component, store, { show: false }).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('does not render tabs when show is false', () => {
+ expect(vm.$el.querySelector('.nav-links')).toBe(null);
+ });
+
+ describe('when show is true', () => {
+ beforeEach(done => {
+ vm.show = true;
+ vm.$store.state.mergeRequests.assigned.mergeRequests.push(mergeRequests[0]);
+
+ vm.$nextTick(done);
+ });
+
+ it('renders tabs', () => {
+ expect(vm.$el.querySelector('.nav-links')).not.toBe(null);
+ });
+
+ it('renders count for assigned & created data', () => {
+ expect(vm.$el.querySelector('.nav-links a').textContent).toContain('Created by me');
+ expect(vm.$el.querySelector('.nav-links a .badge').textContent).toContain('0');
+
+ expect(vm.$el.querySelectorAll('.nav-links a')[1].textContent).toContain('Assigned to me');
+ expect(
+ vm.$el.querySelectorAll('.nav-links a')[1].querySelector('.badge').textContent,
+ ).toContain('1');
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/merge_requests/info_spec.js b/spec/javascripts/ide/components/merge_requests/info_spec.js
new file mode 100644
index 00000000000..98a29e5128b
--- /dev/null
+++ b/spec/javascripts/ide/components/merge_requests/info_spec.js
@@ -0,0 +1,51 @@
+import Vue from 'vue';
+import '~/behaviors/markdown/render_gfm';
+import { createStore } from '~/ide/stores';
+import Info from '~/ide/components/merge_requests/info.vue';
+import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
+
+describe('IDE merge request details', () => {
+ let Component;
+ let vm;
+
+ beforeAll(() => {
+ Component = Vue.extend(Info);
+ });
+
+ beforeEach(() => {
+ const store = createStore();
+ store.state.currentProjectId = 'gitlab-ce';
+ store.state.currentMergeRequestId = 1;
+ store.state.projects['gitlab-ce'] = {
+ mergeRequests: {
+ 1: {
+ iid: 1,
+ title: 'Testing',
+ title_html: '<span class="title-html">Testing</span>',
+ description: 'Description',
+ description_html: '<p class="description-html">Description HTML</p>',
+ },
+ },
+ };
+
+ vm = createComponentWithStore(Component, store).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders merge request IID', () => {
+ expect(vm.$el.querySelector('.detail-page-header').textContent).toContain('!1');
+ });
+
+ it('renders title as HTML', () => {
+ expect(vm.$el.querySelector('.title-html')).not.toBe(null);
+ expect(vm.$el.querySelector('.title').textContent).toContain('Testing');
+ });
+
+ it('renders description as HTML', () => {
+ expect(vm.$el.querySelector('.description-html')).not.toBe(null);
+ expect(vm.$el.querySelector('.description').textContent).toContain('Description HTML');
+ });
+});
diff --git a/spec/javascripts/ide/components/merge_requests/item_spec.js b/spec/javascripts/ide/components/merge_requests/item_spec.js
new file mode 100644
index 00000000000..51c4cddef2f
--- /dev/null
+++ b/spec/javascripts/ide/components/merge_requests/item_spec.js
@@ -0,0 +1,61 @@
+import Vue from 'vue';
+import Item from '~/ide/components/merge_requests/item.vue';
+import mountCompontent from '../../../helpers/vue_mount_component_helper';
+
+describe('IDE merge request item', () => {
+ const Component = Vue.extend(Item);
+ let vm;
+
+ beforeEach(() => {
+ vm = mountCompontent(Component, {
+ item: {
+ iid: 1,
+ projectPathWithNamespace: 'gitlab-org/gitlab-ce',
+ title: 'Merge request title',
+ },
+ currentId: '1',
+ currentProjectId: 'gitlab-org/gitlab-ce',
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders merge requests data', () => {
+ expect(vm.$el.textContent).toContain('Merge request title');
+ expect(vm.$el.textContent).toContain('gitlab-org/gitlab-ce!1');
+ });
+
+ it('renders icon if ID matches currentId', () => {
+ expect(vm.$el.querySelector('.ic-mobile-issue-close')).not.toBe(null);
+ });
+
+ it('does not render icon if ID does not match currentId', done => {
+ vm.currentId = '2';
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.ic-mobile-issue-close')).toBe(null);
+
+ done();
+ });
+ });
+
+ it('does not render icon if project ID does not match', done => {
+ vm.currentProjectId = 'test/test';
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.ic-mobile-issue-close')).toBe(null);
+
+ done();
+ });
+ });
+
+ it('emits click event on click', () => {
+ spyOn(vm, '$emit');
+
+ vm.$el.click();
+
+ expect(vm.$emit).toHaveBeenCalledWith('click', vm.item);
+ });
+});
diff --git a/spec/javascripts/ide/components/merge_requests/list_spec.js b/spec/javascripts/ide/components/merge_requests/list_spec.js
new file mode 100644
index 00000000000..f4b393778dc
--- /dev/null
+++ b/spec/javascripts/ide/components/merge_requests/list_spec.js
@@ -0,0 +1,126 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import List from '~/ide/components/merge_requests/list.vue';
+import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
+import { mergeRequests } from '../../mock_data';
+import { resetStore } from '../../helpers';
+
+describe('IDE merge requests list', () => {
+ const Component = Vue.extend(List);
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponentWithStore(Component, store, {
+ type: 'created',
+ emptyText: 'empty text',
+ });
+
+ spyOn(vm, 'fetchMergeRequests');
+
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('calls fetch on mounted', () => {
+ expect(vm.fetchMergeRequests).toHaveBeenCalledWith({
+ type: 'created',
+ search: '',
+ });
+ });
+
+ it('renders loading icon', done => {
+ vm.$store.state.mergeRequests.created.isLoading = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.loading-container')).not.toBe(null);
+
+ done();
+ });
+ });
+
+ it('renders empty text when no merge requests exist', () => {
+ expect(vm.$el.textContent).toContain('empty text');
+ });
+
+ it('renders no search results text when search is not empty', done => {
+ vm.search = 'testing';
+
+ vm.$nextTick(() => {
+ expect(vm.$el.textContent).toContain('No merge requests found');
+
+ done();
+ });
+ });
+
+ describe('with merge requests', () => {
+ beforeEach(done => {
+ vm.$store.state.mergeRequests.created.mergeRequests.push({
+ ...mergeRequests[0],
+ projectPathWithNamespace: 'gitlab-org/gitlab-ce',
+ });
+
+ vm.$nextTick(done);
+ });
+
+ it('renders list', () => {
+ expect(vm.$el.querySelectorAll('li').length).toBe(1);
+ expect(vm.$el.querySelector('li').textContent).toContain(mergeRequests[0].title);
+ });
+
+ it('calls openMergeRequest when clicking merge request', done => {
+ spyOn(vm, 'openMergeRequest');
+ vm.$el.querySelector('li button').click();
+
+ vm.$nextTick(() => {
+ expect(vm.openMergeRequest).toHaveBeenCalledWith({
+ projectPath: 'gitlab-org/gitlab-ce',
+ id: 1,
+ });
+
+ done();
+ });
+ });
+ });
+
+ describe('focusSearch', () => {
+ it('focuses search input when loading is false', done => {
+ spyOn(vm.$refs.searchInput, 'focus');
+
+ vm.$store.state.mergeRequests.created.isLoading = false;
+ vm.focusSearch();
+
+ vm.$nextTick(() => {
+ expect(vm.$refs.searchInput.focus).toHaveBeenCalled();
+
+ done();
+ });
+ });
+ });
+
+ describe('searchMergeRequests', () => {
+ beforeEach(() => {
+ spyOn(vm, 'loadMergeRequests');
+
+ jasmine.clock().install();
+ });
+
+ afterEach(() => {
+ jasmine.clock().uninstall();
+ });
+
+ it('calls loadMergeRequests on input in search field', () => {
+ const event = new Event('input');
+
+ vm.$el.querySelector('input').dispatchEvent(event);
+
+ jasmine.clock().tick(300);
+
+ expect(vm.loadMergeRequests).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/panes/right_spec.js b/spec/javascripts/ide/components/panes/right_spec.js
new file mode 100644
index 00000000000..99879fb0930
--- /dev/null
+++ b/spec/javascripts/ide/components/panes/right_spec.js
@@ -0,0 +1,72 @@
+import Vue from 'vue';
+import '~/behaviors/markdown/render_gfm';
+import { createStore } from '~/ide/stores';
+import RightPane from '~/ide/components/panes/right.vue';
+import { rightSidebarViews } from '~/ide/constants';
+import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
+
+describe('IDE right pane', () => {
+ let Component;
+ let vm;
+
+ beforeAll(() => {
+ Component = Vue.extend(RightPane);
+ });
+
+ beforeEach(() => {
+ const store = createStore();
+
+ vm = createComponentWithStore(Component, store).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('active', () => {
+ it('renders merge request button as active', done => {
+ vm.$store.state.rightPane = rightSidebarViews.mergeRequestInfo;
+ vm.$store.state.currentMergeRequestId = '123';
+ vm.$store.state.currentProjectId = 'gitlab-ce';
+ vm.$store.state.currentMergeRequestId = 1;
+ vm.$store.state.projects['gitlab-ce'] = {
+ mergeRequests: {
+ 1: {
+ iid: 1,
+ title: 'Testing',
+ title_html: '<span class="title-html">Testing</span>',
+ description: 'Description',
+ description_html: '<p class="description-html">Description HTML</p>',
+ },
+ },
+ };
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.ide-sidebar-link.active')).not.toBe(null);
+ expect(
+ vm.$el.querySelector('.ide-sidebar-link.active').getAttribute('data-original-title'),
+ ).toBe('Merge Request');
+
+ done();
+ });
+ });
+ });
+
+ describe('click', () => {
+ beforeEach(() => {
+ spyOn(vm, 'setRightPane');
+ });
+
+ it('sets view to merge request', done => {
+ vm.$store.state.currentMergeRequestId = '123';
+
+ vm.$nextTick(() => {
+ vm.$el.querySelector('.ide-sidebar-link').click();
+
+ expect(vm.setRightPane).toHaveBeenCalledWith(rightSidebarViews.mergeRequestInfo);
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/pipelines/list_spec.js b/spec/javascripts/ide/components/pipelines/list_spec.js
new file mode 100644
index 00000000000..68487733cb9
--- /dev/null
+++ b/spec/javascripts/ide/components/pipelines/list_spec.js
@@ -0,0 +1,120 @@
+import Vue from 'vue';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import { createStore } from '~/ide/stores';
+import List from '~/ide/components/pipelines/list.vue';
+import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
+import { pipelines, projectData, stages, jobs } from '../../mock_data';
+
+describe('IDE pipelines list', () => {
+ const Component = Vue.extend(List);
+ let vm;
+ let mock;
+
+ beforeEach(done => {
+ const store = createStore();
+
+ mock = new MockAdapter(axios);
+
+ store.state.currentProjectId = 'abc/def';
+ store.state.currentBranchId = 'master';
+ store.state.projects['abc/def'] = {
+ ...projectData,
+ path_with_namespace: 'abc/def',
+ branches: {
+ master: { commit: { id: '123' } },
+ },
+ };
+ store.state.links = { ciHelpPagePath: gl.TEST_HOST };
+ store.state.pipelinesEmptyStateSvgPath = gl.TEST_HOST;
+ store.state.pipelines.stages = stages.map((mappedState, i) => ({
+ ...mappedState,
+ id: i,
+ dropdownPath: mappedState.dropdown_path,
+ jobs: [...jobs],
+ isLoading: false,
+ isCollapsed: false,
+ }));
+
+ mock
+ .onGet('/abc/def/commit/123/pipelines')
+ .replyOnce(200, { pipelines: [...pipelines] }, { 'poll-interval': '-1' });
+
+ vm = createComponentWithStore(Component, store).$mount();
+
+ setTimeout(done);
+ });
+
+ afterEach(done => {
+ vm.$destroy();
+ mock.restore();
+
+ vm.$store
+ .dispatch('pipelines/stopPipelinePolling')
+ .then(() => vm.$store.dispatch('pipelines/clearEtagPoll'))
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('renders pipeline data', () => {
+ expect(vm.$el.textContent).toContain('#1');
+ });
+
+ it('renders CI icon', () => {
+ expect(vm.$el.querySelector('.ci-status-icon-failed')).not.toBe(null);
+ });
+
+ it('renders list of jobs', () => {
+ expect(vm.$el.querySelectorAll('.tab-pane:first-child .ide-job-item').length).toBe(
+ jobs.length * stages.length,
+ );
+ });
+
+ it('renders list of failed jobs on failed jobs tab', done => {
+ vm.$el.querySelectorAll('.tab-links a')[1].click();
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelectorAll('.tab-pane.active .ide-job-item').length).toBe(2);
+
+ done();
+ });
+ });
+
+ describe('YAML error', () => {
+ it('renders YAML error', done => {
+ vm.$store.state.pipelines.latestPipeline.yamlError = 'test yaml error';
+
+ vm.$nextTick(() => {
+ expect(vm.$el.textContent).toContain('Found errors in your .gitlab-ci.yml:');
+ expect(vm.$el.textContent).toContain('test yaml error');
+
+ done();
+ });
+ });
+ });
+
+ describe('empty state', () => {
+ it('renders pipelines empty state', done => {
+ vm.$store.state.pipelines.latestPipeline = false;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.empty-state')).not.toBe(null);
+
+ done();
+ });
+ });
+ });
+
+ describe('loading state', () => {
+ it('renders loading state when there is no latest pipeline', done => {
+ vm.$store.state.pipelines.latestPipeline = null;
+ vm.$store.state.pipelines.isLoadingPipeline = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.loading-container')).not.toBe(null);
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/repo_commit_section_spec.js b/spec/javascripts/ide/components/repo_commit_section_spec.js
index 5e3e00a180b..30cd92b2ca4 100644
--- a/spec/javascripts/ide/components/repo_commit_section_spec.js
+++ b/spec/javascripts/ide/components/repo_commit_section_spec.js
@@ -1,6 +1,5 @@
import Vue from 'vue';
import store from '~/ide/stores';
-import service from '~/ide/services';
import router from '~/ide/ide_router';
import repoCommitSection from '~/ide/components/repo_commit_section.vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
@@ -56,7 +55,7 @@ describe('RepoCommitSection', () => {
vm.$store.state.entries[f.path] = f;
});
- return vm.$mount();
+ return vm;
}
beforeEach(done => {
@@ -64,22 +63,9 @@ describe('RepoCommitSection', () => {
vm = createComponent();
- 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(vm, 'openPendingTab').and.callThrough();
+
+ vm.$mount();
Vue.nextTick(done);
});
@@ -98,6 +84,7 @@ describe('RepoCommitSection', () => {
store.state.noChangesStateSvgPath = 'nochangessvg';
store.state.committedStateSvgPath = 'svg';
+ vm.$destroy();
vm = createComponentWithStore(Component, store).$mount();
expect(vm.$el.querySelector('.js-empty-state').textContent.trim()).toContain('No changes');
@@ -106,7 +93,7 @@ describe('RepoCommitSection', () => {
});
it('renders a commit section', () => {
- const changedFileElements = [...vm.$el.querySelectorAll('.multi-file-commit-list li')];
+ const changedFileElements = [...vm.$el.querySelectorAll('.multi-file-commit-list > li')];
const allFiles = vm.$store.state.changedFiles.concat(vm.$store.state.stagedFiles);
expect(changedFileElements.length).toEqual(4);
@@ -135,22 +122,26 @@ describe('RepoCommitSection', () => {
vm.$el.querySelector('.multi-file-discard-btn .btn').click();
Vue.nextTick(() => {
- expect(vm.$el.querySelector('.ide-commit-list-container').querySelectorAll('li').length).toBe(
- 1,
- );
+ expect(
+ vm.$el
+ .querySelector('.ide-commit-list-container')
+ .querySelectorAll('.multi-file-commit-list > li').length,
+ ).toBe(1);
done();
});
});
it('discards a single file', done => {
- vm.$el.querySelectorAll('.multi-file-discard-btn .btn')[1].click();
+ vm.$el.querySelector('.multi-file-discard-btn .dropdown-menu button').click();
Vue.nextTick(() => {
expect(vm.$el.querySelector('.ide-commit-list-container').textContent).not.toContain('file1');
- expect(vm.$el.querySelector('.ide-commit-list-container').querySelectorAll('li').length).toBe(
- 1,
- );
+ expect(
+ vm.$el
+ .querySelector('.ide-commit-list-container')
+ .querySelectorAll('.multi-file-commit-list > li').length,
+ ).toBe(1);
done();
});
@@ -176,5 +167,12 @@ describe('RepoCommitSection', () => {
expect(store.state.openFiles.length).toBe(1);
expect(store.state.openFiles[0].pending).toBe(true);
});
+
+ it('calls openPendingTab', () => {
+ expect(vm.openPendingTab).toHaveBeenCalledWith({
+ file: vm.lastOpenedFile,
+ keyPrefix: 'unstaged',
+ });
+ });
});
});
diff --git a/spec/javascripts/ide/components/repo_editor_spec.js b/spec/javascripts/ide/components/repo_editor_spec.js
index d3f80e6f9c0..2256deb7dac 100644
--- a/spec/javascripts/ide/components/repo_editor_spec.js
+++ b/spec/javascripts/ide/components/repo_editor_spec.js
@@ -3,7 +3,6 @@ import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import store from '~/ide/stores';
import repoEditor from '~/ide/components/repo_editor.vue';
-import monacoLoader from '~/ide/monaco_loader';
import Editor from '~/ide/lib/editor';
import { activityBarViews } from '~/ide/constants';
import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
@@ -25,13 +24,10 @@ describe('RepoEditor', () => {
f.tempFile = true;
vm.$store.state.openFiles.push(f);
Vue.set(vm.$store.state.entries, f.path, f);
- vm.monaco = true;
vm.$mount();
- monacoLoader(['vs/editor/editor.main'], () => {
- setTimeout(done, 0);
- });
+ Vue.nextTick(() => setTimeout(done));
});
afterEach(() => {
@@ -319,6 +315,17 @@ describe('RepoEditor', () => {
done();
});
});
+
+ it('calls updateDimensions when rightPane is updated', done => {
+ vm.$store.state.rightPane = 'testing';
+
+ vm.$nextTick(() => {
+ expect(vm.editor.updateDimensions).toHaveBeenCalled();
+ expect(vm.editor.updateDiffView).toHaveBeenCalled();
+
+ done();
+ });
+ });
});
describe('show tabs', () => {
diff --git a/spec/javascripts/ide/components/repo_tab_spec.js b/spec/javascripts/ide/components/repo_tab_spec.js
index 8cabc6e8935..fc0695a4263 100644
--- a/spec/javascripts/ide/components/repo_tab_spec.js
+++ b/spec/javascripts/ide/components/repo_tab_spec.js
@@ -38,6 +38,26 @@ describe('RepoTab', () => {
expect(name.textContent.trim()).toEqual(vm.tab.name);
});
+ it('does not call openPendingTab when tab is active', done => {
+ vm = createComponent({
+ tab: {
+ ...file(),
+ pending: true,
+ active: true,
+ },
+ });
+
+ spyOn(vm, 'openPendingTab');
+
+ vm.$el.click();
+
+ vm.$nextTick(() => {
+ expect(vm.openPendingTab).not.toHaveBeenCalled();
+
+ done();
+ });
+ });
+
it('fires clickFile when the link is clicked', () => {
vm = createComponent({
tab: file(),
@@ -112,9 +132,9 @@ describe('RepoTab', () => {
});
it('renders a tooltip', () => {
- expect(
- vm.$el.querySelector('span:nth-child(2)').dataset.originalTitle,
- ).toContain('Locked by testuser');
+ expect(vm.$el.querySelector('span:nth-child(2)').dataset.originalTitle).toContain(
+ 'Locked by testuser',
+ );
});
});
diff --git a/spec/javascripts/ide/helpers.js b/spec/javascripts/ide/helpers.js
index 98db6defc7a..569fa5c7aae 100644
--- a/spec/javascripts/ide/helpers.js
+++ b/spec/javascripts/ide/helpers.js
@@ -1,22 +1,48 @@
+import * as pathUtils from 'path';
import { decorateData } from '~/ide/stores/utils';
import state from '~/ide/stores/state';
import commitState from '~/ide/stores/modules/commit/state';
+import mergeRequestsState from '~/ide/stores/modules/merge_requests/state';
+import pipelinesState from '~/ide/stores/modules/pipelines/state';
export const resetStore = store => {
const newState = {
...state(),
commit: commitState(),
+ mergeRequests: mergeRequestsState(),
+ pipelines: pipelinesState(),
};
store.replaceState(newState);
};
-export const file = (name = 'name', id = name, type = '') =>
+export const file = (name = 'name', id = name, type = '', parent = null) =>
decorateData({
id,
type,
icon: 'icon',
url: 'url',
name,
- path: name,
+ path: parent ? `${parent.path}/${name}` : name,
+ parentPath: parent ? parent.path : '',
lastCommit: {},
});
+
+export const createEntriesFromPaths = paths =>
+ paths
+ .map(path => ({
+ name: pathUtils.basename(path),
+ dir: pathUtils.dirname(path),
+ ext: pathUtils.extname(path),
+ }))
+ .reduce((entries, path, idx) => {
+ const { name } = path;
+ const parent = path.dir ? entries[path.dir] : null;
+ const type = path.ext ? 'blob' : 'tree';
+
+ const entry = file(name, (idx + 1).toString(), type, parent);
+
+ return {
+ [entry.path]: entry,
+ ...entries,
+ };
+ }, {});
diff --git a/spec/javascripts/ide/lib/common/model_manager_spec.js b/spec/javascripts/ide/lib/common/model_manager_spec.js
index c00d590c580..38ffa317e8e 100644
--- a/spec/javascripts/ide/lib/common/model_manager_spec.js
+++ b/spec/javascripts/ide/lib/common/model_manager_spec.js
@@ -1,18 +1,12 @@
-/* global monaco */
import eventHub from '~/ide/eventhub';
-import monacoLoader from '~/ide/monaco_loader';
import ModelManager from '~/ide/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();
- });
+ beforeEach(() => {
+ instance = new ModelManager();
});
afterEach(() => {
diff --git a/spec/javascripts/ide/lib/common/model_spec.js b/spec/javascripts/ide/lib/common/model_spec.js
index c278bf92b08..f096e06f43c 100644
--- a/spec/javascripts/ide/lib/common/model_spec.js
+++ b/spec/javascripts/ide/lib/common/model_spec.js
@@ -1,23 +1,17 @@
-/* global monaco */
import eventHub from '~/ide/eventhub';
-import monacoLoader from '~/ide/monaco_loader';
import Model from '~/ide/lib/common/model';
import { file } from '../../helpers';
describe('Multi-file editor library model', () => {
let model;
- beforeEach(done => {
+ beforeEach(() => {
spyOn(eventHub, '$on').and.callThrough();
- monacoLoader(['vs/editor/editor.main'], () => {
- const f = file('path');
- f.mrChange = { diff: 'ABC' };
- f.baseRaw = 'test';
- model = new Model(monaco, f);
-
- done();
- });
+ const f = file('path');
+ f.mrChange = { diff: 'ABC' };
+ f.baseRaw = 'test';
+ model = new Model(f);
});
afterEach(() => {
@@ -38,7 +32,7 @@ describe('Multi-file editor library model', () => {
const f = file('path');
model.dispose();
- model = new Model(monaco, f, {
+ model = new Model(f, {
...f,
content: '123 testing',
});
diff --git a/spec/javascripts/ide/lib/decorations/controller_spec.js b/spec/javascripts/ide/lib/decorations/controller_spec.js
index e1c4ca570b6..a112361e0d1 100644
--- a/spec/javascripts/ide/lib/decorations/controller_spec.js
+++ b/spec/javascripts/ide/lib/decorations/controller_spec.js
@@ -1,6 +1,4 @@
-/* global monaco */
-import monacoLoader from '~/ide/monaco_loader';
-import editor from '~/ide/lib/editor';
+import Editor from '~/ide/lib/editor';
import DecorationsController from '~/ide/lib/decorations/controller';
import Model from '~/ide/lib/common/model';
import { file } from '../../helpers';
@@ -10,16 +8,12 @@ describe('Multi-file editor library decorations controller', () => {
let controller;
let model;
- beforeEach(done => {
- monacoLoader(['vs/editor/editor.main'], () => {
- editorInstance = editor.create(monaco);
- editorInstance.createInstance(document.createElement('div'));
+ beforeEach(() => {
+ editorInstance = Editor.create();
+ editorInstance.createInstance(document.createElement('div'));
- controller = new DecorationsController(editorInstance);
- model = new Model(monaco, file('path'));
-
- done();
- });
+ controller = new DecorationsController(editorInstance);
+ model = new Model(file('path'));
});
afterEach(() => {
diff --git a/spec/javascripts/ide/lib/diff/controller_spec.js b/spec/javascripts/ide/lib/diff/controller_spec.js
index fd8ab3b4f1d..90ebb95b687 100644
--- a/spec/javascripts/ide/lib/diff/controller_spec.js
+++ b/spec/javascripts/ide/lib/diff/controller_spec.js
@@ -1,6 +1,5 @@
-/* global monaco */
-import monacoLoader from '~/ide/monaco_loader';
-import editor from '~/ide/lib/editor';
+import { Range } from 'monaco-editor';
+import Editor from '~/ide/lib/editor';
import ModelManager from '~/ide/lib/common/model_manager';
import DecorationsController from '~/ide/lib/decorations/controller';
import DirtyDiffController, { getDiffChangeType, getDecorator } from '~/ide/lib/diff/controller';
@@ -14,20 +13,16 @@ describe('Multi-file editor library dirty diff controller', () => {
let decorationsController;
let model;
- beforeEach(done => {
- monacoLoader(['vs/editor/editor.main'], () => {
- editorInstance = editor.create(monaco);
- editorInstance.createInstance(document.createElement('div'));
+ beforeEach(() => {
+ editorInstance = Editor.create();
+ editorInstance.createInstance(document.createElement('div'));
- modelManager = new ModelManager(monaco);
- decorationsController = new DecorationsController(editorInstance);
+ modelManager = new ModelManager();
+ decorationsController = new DecorationsController(editorInstance);
- model = modelManager.addModel(file('path'));
+ model = modelManager.addModel(file('path'));
- controller = new DirtyDiffController(modelManager, decorationsController);
-
- done();
- });
+ controller = new DirtyDiffController(modelManager, decorationsController);
});
afterEach(() => {
@@ -68,7 +63,7 @@ describe('Multi-file editor library dirty diff controller', () => {
[type]: true,
};
- const range = getDecorator(change).range;
+ const { range } = getDecorator(change);
expect(range.startLineNumber).toBe(1);
expect(range.endLineNumber).toBe(2);
@@ -170,7 +165,7 @@ describe('Multi-file editor library dirty diff controller', () => {
[],
[
{
- range: new monaco.Range(1, 1, 1, 1),
+ range: new Range(1, 1, 1, 1),
options: {
isWholeLine: true,
linesDecorationsClassName: 'dirty-diff dirty-diff-modified',
diff --git a/spec/javascripts/ide/lib/editor_spec.js b/spec/javascripts/ide/lib/editor_spec.js
index b88a12264ca..c2cb964ea87 100644
--- a/spec/javascripts/ide/lib/editor_spec.js
+++ b/spec/javascripts/ide/lib/editor_spec.js
@@ -1,6 +1,5 @@
-/* global monaco */
-import monacoLoader from '~/ide/monaco_loader';
-import editor from '~/ide/lib/editor';
+import { editor as monacoEditor } from 'monaco-editor';
+import Editor from '~/ide/lib/editor';
import { file } from '../helpers';
describe('Multi-file editor library', () => {
@@ -8,18 +7,14 @@ describe('Multi-file editor library', () => {
let el;
let holder;
- beforeEach(done => {
+ beforeEach(() => {
el = document.createElement('div');
holder = document.createElement('div');
el.appendChild(holder);
document.body.appendChild(el);
- monacoLoader(['vs/editor/editor.main'], () => {
- instance = editor.create(monaco);
-
- done();
- });
+ instance = Editor.create();
});
afterEach(() => {
@@ -29,20 +24,20 @@ describe('Multi-file editor library', () => {
});
it('creates instance of editor', () => {
- expect(editor.editorInstance).not.toBeNull();
+ expect(Editor.editorInstance).not.toBeNull();
});
it('creates instance returns cached instance', () => {
- expect(editor.create(monaco)).toEqual(instance);
+ expect(Editor.create()).toEqual(instance);
});
describe('createInstance', () => {
it('creates editor instance', () => {
- spyOn(instance.monaco.editor, 'create').and.callThrough();
+ spyOn(monacoEditor, 'create').and.callThrough();
instance.createInstance(holder);
- expect(instance.monaco.editor.create).toHaveBeenCalled();
+ expect(monacoEditor.create).toHaveBeenCalled();
});
it('creates dirty diff controller', () => {
@@ -60,11 +55,11 @@ describe('Multi-file editor library', () => {
describe('createDiffInstance', () => {
it('creates editor instance', () => {
- spyOn(instance.monaco.editor, 'createDiffEditor').and.callThrough();
+ spyOn(monacoEditor, 'createDiffEditor').and.callThrough();
instance.createDiffInstance(holder);
- expect(instance.monaco.editor.createDiffEditor).toHaveBeenCalledWith(holder, {
+ expect(monacoEditor.createDiffEditor).toHaveBeenCalledWith(holder, {
model: null,
contextmenu: true,
minimap: {
@@ -268,4 +263,23 @@ describe('Multi-file editor library', () => {
expect(instance.isDiffEditorType).toBe(false);
});
});
+
+ it('sets quickSuggestions to false when language is markdown', () => {
+ instance.createInstance(holder);
+
+ spyOn(instance.instance, 'updateOptions').and.callThrough();
+
+ const model = instance.createModel({
+ ...file(),
+ key: 'index.md',
+ path: 'index.md',
+ });
+
+ instance.attachModel(model);
+
+ expect(instance.instance.updateOptions).toHaveBeenCalledWith({
+ readOnly: false,
+ quickSuggestions: false,
+ });
+ });
});
diff --git a/spec/javascripts/ide/mock_data.js b/spec/javascripts/ide/mock_data.js
index 7e641c7984b..80bf664d491 100644
--- a/spec/javascripts/ide/mock_data.js
+++ b/spec/javascripts/ide/mock_data.js
@@ -8,6 +8,7 @@ export const projectData = {
branches: {
master: {
treeId: 'abcproject/master',
+ can_push: true,
},
},
mergeRequests: {},
@@ -19,13 +20,48 @@ export const pipelines = [
id: 1,
ref: 'master',
sha: '123',
- status: 'failed',
+ details: {
+ status: {
+ icon: 'status_failed',
+ group: 'failed',
+ text: 'Failed',
+ },
+ },
+ commit: { id: '123' },
},
{
id: 2,
ref: 'master',
sha: '213',
- status: 'success',
+ details: {
+ status: {
+ icon: 'status_failed',
+ group: 'failed',
+ text: 'Failed',
+ },
+ },
+ commit: { id: '213' },
+ },
+];
+
+export const stages = [
+ {
+ dropdown_path: `${gl.TEST_HOST}/testing`,
+ name: 'build',
+ status: {
+ icon: 'status_failed',
+ group: 'failed',
+ text: 'failed',
+ },
+ },
+ {
+ dropdown_path: 'testing',
+ name: 'test',
+ status: {
+ icon: 'status_failed',
+ group: 'failed',
+ text: 'failed',
+ },
},
];
@@ -33,29 +69,96 @@ export const jobs = [
{
id: 1,
name: 'test',
- status: 'failed',
+ path: 'testing',
+ status: {
+ icon: 'status_passed',
+ text: 'passed',
+ },
stage: 'test',
duration: 1,
+ started: new Date(),
},
{
id: 2,
name: 'test 2',
- status: 'failed',
+ path: 'testing2',
+ status: {
+ icon: 'status_passed',
+ text: 'passed',
+ },
stage: 'test',
duration: 1,
+ started: new Date(),
},
{
id: 3,
name: 'test 3',
- status: 'failed',
+ path: 'testing3',
+ status: {
+ icon: 'status_passed',
+ text: 'passed',
+ },
stage: 'test',
duration: 1,
+ started: new Date(),
},
{
id: 4,
- name: 'test 3',
- status: 'failed',
+ name: 'test 4',
+ path: 'testing4',
+ status: {
+ icon: 'status_failed',
+ text: 'failed',
+ },
stage: 'build',
duration: 1,
+ started: new Date(),
+ },
+];
+
+export const fullPipelinesResponse = {
+ data: {
+ count: {
+ all: 2,
+ },
+ pipelines: [
+ {
+ id: '51',
+ path: 'test',
+ commit: {
+ id: '123',
+ },
+ details: {
+ status: {
+ icon: 'status_failed',
+ text: 'failed',
+ },
+ stages: [...stages],
+ },
+ },
+ {
+ id: '50',
+ commit: {
+ id: 'abc123def456ghi789jkl',
+ },
+ details: {
+ status: {
+ icon: 'status_passed',
+ text: 'passed',
+ },
+ stages: [...stages],
+ },
+ },
+ ],
+ },
+};
+
+export const mergeRequests = [
+ {
+ id: 1,
+ iid: 1,
+ title: 'Test merge request',
+ project_id: 1,
+ web_url: `${gl.TEST_HOST}/namespace/project-path/merge_requests/1`,
},
];
diff --git a/spec/javascripts/ide/monaco_loader_spec.js b/spec/javascripts/ide/monaco_loader_spec.js
deleted file mode 100644
index 7ab315aa8c8..00000000000
--- a/spec/javascripts/ide/monaco_loader_spec.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import monacoContext from 'monaco-editor/dev/vs/loader';
-import monacoLoader from '~/ide/monaco_loader';
-
-describe('MonacoLoader', () => {
- it('calls require.config and exports require', () => {
- expect(monacoContext.require.getConfig()).toEqual(
- jasmine.objectContaining({
- paths: {
- vs: `${__webpack_public_path__}monaco-editor/vs`, // eslint-disable-line camelcase
- },
- }),
- );
- expect(monacoLoader).toBe(monacoContext.require);
- });
-});
diff --git a/spec/javascripts/ide/stores/actions/file_spec.js b/spec/javascripts/ide/stores/actions/file_spec.js
index 7bebc2288e3..58d3ffc6d94 100644
--- a/spec/javascripts/ide/stores/actions/file_spec.js
+++ b/spec/javascripts/ide/stores/actions/file_spec.js
@@ -1,4 +1,6 @@
import Vue from 'vue';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import store from '~/ide/stores';
import * as actions from '~/ide/stores/actions/file';
import * as types from '~/ide/stores/mutation_types';
@@ -9,11 +11,16 @@ import { file, resetStore } from '../../helpers';
import testAction from '../../../helpers/vuex_action_helper';
describe('IDE store file actions', () => {
+ let mock;
+
beforeEach(() => {
+ mock = new MockAdapter(axios);
+
spyOn(router, 'push');
});
afterEach(() => {
+ mock.restore();
resetStore(store);
});
@@ -166,12 +173,12 @@ describe('IDE store file actions', () => {
});
it('resets location.hash for line highlighting', done => {
- location.hash = 'test';
+ window.location.hash = 'test';
store
.dispatch('setFileActive', localFile.path)
.then(() => {
- expect(location.hash).not.toBe('test');
+ expect(window.location.hash).not.toBe('test');
done();
})
@@ -183,94 +190,125 @@ describe('IDE store file actions', () => {
let localFile;
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: '',
- }),
- }),
- );
+ spyOn(service, 'getFileData').and.callThrough();
localFile = file(`newCreate-${Math.random()}`);
- localFile.url = 'getFileDataURL';
+ localFile.url = `${gl.TEST_HOST}/getFileDataURL`;
store.state.entries[localFile.path] = localFile;
});
- it('calls the service', done => {
- store
- .dispatch('getFileData', { path: localFile.path })
- .then(() => {
- expect(service.getFileData).toHaveBeenCalledWith('getFileDataURL');
+ describe('success', () => {
+ beforeEach(() => {
+ mock.onGet(`${gl.TEST_HOST}/getFileDataURL`).replyOnce(
+ 200,
+ {
+ blame_path: 'blame_path',
+ commits_path: 'commits_path',
+ permalink: 'permalink',
+ raw_path: 'raw_path',
+ binary: false,
+ html: '123',
+ render_error: '',
+ },
+ {
+ 'page-title': 'testing getFileData',
+ },
+ );
+ });
- done();
- })
- .catch(done.fail);
- });
+ it('calls the service', done => {
+ store
+ .dispatch('getFileData', { path: localFile.path })
+ .then(() => {
+ expect(service.getFileData).toHaveBeenCalledWith(`${gl.TEST_HOST}/getFileDataURL`);
- it('sets the file data', done => {
- store
- .dispatch('getFileData', { path: localFile.path })
- .then(() => {
- expect(localFile.blamePath).toBe('blame_path');
+ done();
+ })
+ .catch(done.fail);
+ });
- done();
- })
- .catch(done.fail);
- });
+ it('sets the file data', done => {
+ store
+ .dispatch('getFileData', { path: localFile.path })
+ .then(() => {
+ expect(localFile.blamePath).toBe('blame_path');
- it('sets document title', done => {
- store
- .dispatch('getFileData', { path: localFile.path })
- .then(() => {
- expect(document.title).toBe('testing getFileData');
+ done();
+ })
+ .catch(done.fail);
+ });
- done();
- })
- .catch(done.fail);
- });
+ it('sets document title', done => {
+ store
+ .dispatch('getFileData', { path: localFile.path })
+ .then(() => {
+ expect(document.title).toBe('testing getFileData');
- it('sets the file as active', done => {
- store
- .dispatch('getFileData', { path: localFile.path })
- .then(() => {
- expect(localFile.active).toBeTruthy();
+ done();
+ })
+ .catch(done.fail);
+ });
- done();
- })
- .catch(done.fail);
- });
+ it('sets the file as active', done => {
+ store
+ .dispatch('getFileData', { path: localFile.path })
+ .then(() => {
+ expect(localFile.active).toBeTruthy();
- it('sets the file not as active if we pass makeFileActive false', done => {
- store
- .dispatch('getFileData', { path: localFile.path, makeFileActive: false })
- .then(() => {
- expect(localFile.active).toBeFalsy();
+ done();
+ })
+ .catch(done.fail);
+ });
- done();
- })
- .catch(done.fail);
+ it('sets the file not as active if we pass makeFileActive false', done => {
+ store
+ .dispatch('getFileData', { path: localFile.path, makeFileActive: false })
+ .then(() => {
+ expect(localFile.active).toBeFalsy();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('adds the file to open files', done => {
+ store
+ .dispatch('getFileData', { path: localFile.path })
+ .then(() => {
+ expect(store.state.openFiles.length).toBe(1);
+ expect(store.state.openFiles[0].name).toBe(localFile.name);
+
+ done();
+ })
+ .catch(done.fail);
+ });
});
- it('adds the file to open files', done => {
- store
- .dispatch('getFileData', { path: localFile.path })
- .then(() => {
- expect(store.state.openFiles.length).toBe(1);
- expect(store.state.openFiles[0].name).toBe(localFile.name);
+ describe('error', () => {
+ beforeEach(() => {
+ mock.onGet(`${gl.TEST_HOST}/getFileDataURL`).networkError();
+ });
- done();
- })
- .catch(done.fail);
+ it('dispatches error action', done => {
+ const dispatch = jasmine.createSpy('dispatch');
+
+ actions
+ .getFileData({ state: store.state, commit() {}, dispatch }, { path: localFile.path })
+ .then(() => {
+ expect(dispatch).toHaveBeenCalledWith('setErrorMessage', {
+ text: 'An error occured whilst loading the file.',
+ action: jasmine.any(Function),
+ actionText: 'Please try again',
+ actionPayload: {
+ path: localFile.path,
+ makeFileActive: true,
+ },
+ });
+
+ done();
+ })
+ .catch(done.fail);
+ });
});
});
@@ -278,48 +316,84 @@ describe('IDE store file actions', () => {
let tmpFile;
beforeEach(() => {
- spyOn(service, 'getRawFileData').and.returnValue(Promise.resolve('raw'));
+ spyOn(service, 'getRawFileData').and.callThrough();
tmpFile = file('tmpFile');
store.state.entries[tmpFile.path] = tmpFile;
});
- it('calls getRawFileData service method', done => {
- store
- .dispatch('getRawFileData', { path: tmpFile.path })
- .then(() => {
- expect(service.getRawFileData).toHaveBeenCalledWith(tmpFile);
+ describe('success', () => {
+ beforeEach(() => {
+ mock.onGet(/(.*)/).replyOnce(200, 'raw');
+ });
- done();
- })
- .catch(done.fail);
- });
+ it('calls getRawFileData service method', done => {
+ store
+ .dispatch('getRawFileData', { path: tmpFile.path })
+ .then(() => {
+ expect(service.getRawFileData).toHaveBeenCalledWith(tmpFile);
- it('updates file raw data', done => {
- store
- .dispatch('getRawFileData', { path: tmpFile.path })
- .then(() => {
- expect(tmpFile.raw).toBe('raw');
+ done();
+ })
+ .catch(done.fail);
+ });
- done();
- })
- .catch(done.fail);
- });
+ it('updates file raw data', done => {
+ store
+ .dispatch('getRawFileData', { path: tmpFile.path })
+ .then(() => {
+ expect(tmpFile.raw).toBe('raw');
- it('calls also getBaseRawFileData service method', done => {
- spyOn(service, 'getBaseRawFileData').and.returnValue(Promise.resolve('baseraw'));
+ done();
+ })
+ .catch(done.fail);
+ });
- tmpFile.mrChange = { new_file: false };
+ it('calls also getBaseRawFileData service method', done => {
+ spyOn(service, 'getBaseRawFileData').and.returnValue(Promise.resolve('baseraw'));
- store
- .dispatch('getRawFileData', { path: tmpFile.path, baseSha: 'SHA' })
- .then(() => {
- expect(service.getBaseRawFileData).toHaveBeenCalledWith(tmpFile, 'SHA');
- expect(tmpFile.baseRaw).toBe('baseraw');
+ tmpFile.mrChange = { new_file: false };
- done();
- })
- .catch(done.fail);
+ store
+ .dispatch('getRawFileData', { path: tmpFile.path, baseSha: 'SHA' })
+ .then(() => {
+ expect(service.getBaseRawFileData).toHaveBeenCalledWith(tmpFile, 'SHA');
+ expect(tmpFile.baseRaw).toBe('baseraw');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('error', () => {
+ beforeEach(() => {
+ mock.onGet(/(.*)/).networkError();
+ });
+
+ it('dispatches error action', done => {
+ const dispatch = jasmine.createSpy('dispatch');
+
+ actions
+ .getRawFileData(
+ { state: store.state, commit() {}, dispatch },
+ { path: tmpFile.path, baseSha: tmpFile.baseSha },
+ )
+ .then(done.fail)
+ .catch(() => {
+ expect(dispatch).toHaveBeenCalledWith('setErrorMessage', {
+ text: 'An error occured whilst loading the file content.',
+ action: jasmine.any(Function),
+ actionText: 'Please try again',
+ actionPayload: {
+ path: tmpFile.path,
+ baseSha: tmpFile.baseSha,
+ },
+ });
+
+ done();
+ });
+ });
});
});
diff --git a/spec/javascripts/ide/stores/actions/merge_request_spec.js b/spec/javascripts/ide/stores/actions/merge_request_spec.js
index b4ec4a0b173..90c28c769f7 100644
--- a/spec/javascripts/ide/stores/actions/merge_request_spec.js
+++ b/spec/javascripts/ide/stores/actions/merge_request_spec.js
@@ -1,110 +1,241 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import store from '~/ide/stores';
+import {
+ getMergeRequestData,
+ getMergeRequestChanges,
+ getMergeRequestVersions,
+} from '~/ide/stores/actions/merge_request';
import service from '~/ide/services';
import { resetStore } from '../../helpers';
describe('IDE store merge request actions', () => {
+ let mock;
+
beforeEach(() => {
+ mock = new MockAdapter(axios);
+
store.state.projects.abcproject = {
mergeRequests: {},
};
});
afterEach(() => {
+ mock.restore();
resetStore(store);
});
describe('getMergeRequestData', () => {
- beforeEach(() => {
- spyOn(service, 'getProjectMergeRequestData').and.returnValue(
- Promise.resolve({ data: { title: 'mergerequest' } }),
- );
+ describe('success', () => {
+ beforeEach(() => {
+ spyOn(service, 'getProjectMergeRequestData').and.callThrough();
+
+ mock
+ .onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1/)
+ .reply(200, { title: 'mergerequest' });
+ });
+
+ it('calls getProjectMergeRequestData service method', done => {
+ store
+ .dispatch('getMergeRequestData', { projectId: 'abcproject', mergeRequestId: 1 })
+ .then(() => {
+ expect(service.getProjectMergeRequestData).toHaveBeenCalledWith('abcproject', 1, {
+ render_html: true,
+ });
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('sets the Merge Request Object', done => {
+ store
+ .dispatch('getMergeRequestData', { projectId: 'abcproject', mergeRequestId: 1 })
+ .then(() => {
+ expect(store.state.projects.abcproject.mergeRequests['1'].title).toBe('mergerequest');
+ expect(store.state.currentMergeRequestId).toBe(1);
+
+ done();
+ })
+ .catch(done.fail);
+ });
});
- it('calls getProjectMergeRequestData service method', done => {
- store
- .dispatch('getMergeRequestData', { projectId: 'abcproject', mergeRequestId: 1 })
- .then(() => {
- expect(service.getProjectMergeRequestData).toHaveBeenCalledWith('abcproject', 1);
-
- done();
- })
- .catch(done.fail);
- });
-
- it('sets the Merge Request Object', done => {
- store
- .dispatch('getMergeRequestData', { projectId: 'abcproject', mergeRequestId: 1 })
- .then(() => {
- expect(store.state.projects.abcproject.mergeRequests['1'].title).toBe('mergerequest');
- expect(store.state.currentMergeRequestId).toBe(1);
-
- done();
- })
- .catch(done.fail);
+ describe('error', () => {
+ beforeEach(() => {
+ mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1/).networkError();
+ });
+
+ it('dispatches error action', done => {
+ const dispatch = jasmine.createSpy('dispatch');
+
+ getMergeRequestData(
+ {
+ commit() {},
+ dispatch,
+ state: store.state,
+ },
+ { projectId: 'abcproject', mergeRequestId: 1 },
+ )
+ .then(done.fail)
+ .catch(() => {
+ expect(dispatch).toHaveBeenCalledWith('setErrorMessage', {
+ text: 'An error occured whilst loading the merge request.',
+ action: jasmine.any(Function),
+ actionText: 'Please try again',
+ actionPayload: {
+ projectId: 'abcproject',
+ mergeRequestId: 1,
+ force: false,
+ },
+ });
+
+ done();
+ });
+ });
});
});
describe('getMergeRequestChanges', () => {
beforeEach(() => {
- spyOn(service, 'getProjectMergeRequestChanges').and.returnValue(
- Promise.resolve({ data: { title: 'mergerequest' } }),
- );
-
store.state.projects.abcproject.mergeRequests['1'] = { changes: [] };
});
- it('calls getProjectMergeRequestChanges service method', done => {
- store
- .dispatch('getMergeRequestChanges', { projectId: 'abcproject', mergeRequestId: 1 })
- .then(() => {
- expect(service.getProjectMergeRequestChanges).toHaveBeenCalledWith('abcproject', 1);
-
- done();
- })
- .catch(done.fail);
+ describe('success', () => {
+ beforeEach(() => {
+ spyOn(service, 'getProjectMergeRequestChanges').and.callThrough();
+
+ mock
+ .onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1\/changes/)
+ .reply(200, { title: 'mergerequest' });
+ });
+
+ it('calls getProjectMergeRequestChanges service method', done => {
+ store
+ .dispatch('getMergeRequestChanges', { projectId: 'abcproject', mergeRequestId: 1 })
+ .then(() => {
+ expect(service.getProjectMergeRequestChanges).toHaveBeenCalledWith('abcproject', 1);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('sets the Merge Request Changes Object', done => {
+ store
+ .dispatch('getMergeRequestChanges', { projectId: 'abcproject', mergeRequestId: 1 })
+ .then(() => {
+ expect(store.state.projects.abcproject.mergeRequests['1'].changes.title).toBe(
+ 'mergerequest',
+ );
+ done();
+ })
+ .catch(done.fail);
+ });
});
- it('sets the Merge Request Changes Object', done => {
- store
- .dispatch('getMergeRequestChanges', { projectId: 'abcproject', mergeRequestId: 1 })
- .then(() => {
- expect(store.state.projects.abcproject.mergeRequests['1'].changes.title).toBe(
- 'mergerequest',
- );
- done();
- })
- .catch(done.fail);
+ describe('error', () => {
+ beforeEach(() => {
+ mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1\/changes/).networkError();
+ });
+
+ it('dispatches error action', done => {
+ const dispatch = jasmine.createSpy('dispatch');
+
+ getMergeRequestChanges(
+ {
+ commit() {},
+ dispatch,
+ state: store.state,
+ },
+ { projectId: 'abcproject', mergeRequestId: 1 },
+ )
+ .then(done.fail)
+ .catch(() => {
+ expect(dispatch).toHaveBeenCalledWith('setErrorMessage', {
+ text: 'An error occured whilst loading the merge request changes.',
+ action: jasmine.any(Function),
+ actionText: 'Please try again',
+ actionPayload: {
+ projectId: 'abcproject',
+ mergeRequestId: 1,
+ force: false,
+ },
+ });
+
+ done();
+ });
+ });
});
});
describe('getMergeRequestVersions', () => {
beforeEach(() => {
- spyOn(service, 'getProjectMergeRequestVersions').and.returnValue(
- Promise.resolve({ data: [{ id: 789 }] }),
- );
-
store.state.projects.abcproject.mergeRequests['1'] = { versions: [] };
});
- it('calls getProjectMergeRequestVersions service method', done => {
- store
- .dispatch('getMergeRequestVersions', { projectId: 'abcproject', mergeRequestId: 1 })
- .then(() => {
- expect(service.getProjectMergeRequestVersions).toHaveBeenCalledWith('abcproject', 1);
-
- done();
- })
- .catch(done.fail);
+ describe('success', () => {
+ beforeEach(() => {
+ mock
+ .onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1\/versions/)
+ .reply(200, [{ id: 789 }]);
+ spyOn(service, 'getProjectMergeRequestVersions').and.callThrough();
+ });
+
+ it('calls getProjectMergeRequestVersions service method', done => {
+ store
+ .dispatch('getMergeRequestVersions', { projectId: 'abcproject', mergeRequestId: 1 })
+ .then(() => {
+ expect(service.getProjectMergeRequestVersions).toHaveBeenCalledWith('abcproject', 1);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('sets the Merge Request Versions Object', done => {
+ store
+ .dispatch('getMergeRequestVersions', { projectId: 'abcproject', mergeRequestId: 1 })
+ .then(() => {
+ expect(store.state.projects.abcproject.mergeRequests['1'].versions.length).toBe(1);
+ done();
+ })
+ .catch(done.fail);
+ });
});
- it('sets the Merge Request Versions Object', done => {
- store
- .dispatch('getMergeRequestVersions', { projectId: 'abcproject', mergeRequestId: 1 })
- .then(() => {
- expect(store.state.projects.abcproject.mergeRequests['1'].versions.length).toBe(1);
- done();
- })
- .catch(done.fail);
+ describe('error', () => {
+ beforeEach(() => {
+ mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1\/versions/).networkError();
+ });
+
+ it('dispatches error action', done => {
+ const dispatch = jasmine.createSpy('dispatch');
+
+ getMergeRequestVersions(
+ {
+ commit() {},
+ dispatch,
+ state: store.state,
+ },
+ { projectId: 'abcproject', mergeRequestId: 1 },
+ )
+ .then(done.fail)
+ .catch(() => {
+ expect(dispatch).toHaveBeenCalledWith('setErrorMessage', {
+ text: 'An error occured whilst loading the merge request version data.',
+ action: jasmine.any(Function),
+ actionText: 'Please try again',
+ actionPayload: {
+ projectId: 'abcproject',
+ mergeRequestId: 1,
+ force: false,
+ },
+ });
+
+ done();
+ });
+ });
});
});
});
diff --git a/spec/javascripts/ide/stores/actions/project_spec.js b/spec/javascripts/ide/stores/actions/project_spec.js
index ebd08d95810..ca79edafb7e 100644
--- a/spec/javascripts/ide/stores/actions/project_spec.js
+++ b/spec/javascripts/ide/stores/actions/project_spec.js
@@ -1,34 +1,47 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import {
refreshLastCommitData,
+ showBranchNotFoundError,
+ createNewBranchFromDefault,
+ getBranchData,
} from '~/ide/stores/actions';
import store from '~/ide/stores';
import service from '~/ide/services';
+import api from '~/api';
+import router from '~/ide/ide_router';
import { resetStore } from '../../helpers';
import testAction from '../../../helpers/vuex_action_helper';
describe('IDE store project actions', () => {
+ let mock;
+
beforeEach(() => {
- store.state.projects.abcproject = {};
+ mock = new MockAdapter(axios);
+
+ store.state.projects['abc/def'] = {
+ branches: {},
+ };
});
afterEach(() => {
+ mock.restore();
+
resetStore(store);
});
describe('refreshLastCommitData', () => {
beforeEach(() => {
- store.state.currentProjectId = 'abcproject';
+ store.state.currentProjectId = 'abc/def';
store.state.currentBranchId = 'master';
- store.state.projects.abcproject = {
+ store.state.projects['abc/def'] = {
+ id: 4,
branches: {
master: {
commit: null,
},
},
};
- });
-
- it('calls the service', done => {
spyOn(service, 'getBranchData').and.returnValue(
Promise.resolve({
data: {
@@ -36,14 +49,16 @@ describe('IDE store project actions', () => {
},
}),
);
+ });
+ it('calls the service', done => {
store
.dispatch('refreshLastCommitData', {
projectId: store.state.currentProjectId,
branchId: store.state.currentBranchId,
})
.then(() => {
- expect(service.getBranchData).toHaveBeenCalledWith('abcproject', 'master');
+ expect(service.getBranchData).toHaveBeenCalledWith('abc/def', 'master');
done();
})
@@ -53,19 +68,167 @@ describe('IDE store project actions', () => {
it('commits getBranchData', done => {
testAction(
refreshLastCommitData,
- {},
- {},
- [{
- type: 'SET_BRANCH_COMMIT',
- payload: {
- projectId: 'abcproject',
- branchId: 'master',
- commit: { id: '123' },
+ {
+ projectId: store.state.currentProjectId,
+ branchId: store.state.currentBranchId,
+ },
+ store.state,
+ [
+ {
+ type: 'SET_BRANCH_COMMIT',
+ payload: {
+ projectId: 'abc/def',
+ branchId: 'master',
+ commit: { id: '123' },
+ },
+ },
+ ], // mutations
+ [
+ {
+ type: 'getLastCommitPipeline',
+ payload: {
+ projectId: 'abc/def',
+ projectIdNumber: store.state.projects['abc/def'].id,
+ branchId: 'master',
+ },
+ },
+ ], // action
+ done,
+ );
+ });
+ });
+
+ describe('showBranchNotFoundError', () => {
+ it('dispatches setErrorMessage', done => {
+ testAction(
+ showBranchNotFoundError,
+ 'master',
+ null,
+ [],
+ [
+ {
+ type: 'setErrorMessage',
+ payload: {
+ text: "Branch <strong>master</strong> was not found in this project's repository.",
+ action: jasmine.any(Function),
+ actionText: 'Create branch',
+ actionPayload: 'master',
+ },
},
- }], // mutations
- [], // action
+ ],
done,
);
});
});
+
+ describe('createNewBranchFromDefault', () => {
+ it('calls API', done => {
+ spyOn(api, 'createBranch').and.returnValue(Promise.resolve());
+ spyOn(router, 'push');
+
+ createNewBranchFromDefault(
+ {
+ state: {
+ currentProjectId: 'project-path',
+ },
+ getters: {
+ currentProject: {
+ default_branch: 'master',
+ },
+ },
+ dispatch() {},
+ },
+ 'new-branch-name',
+ )
+ .then(() => {
+ expect(api.createBranch).toHaveBeenCalledWith('project-path', {
+ ref: 'master',
+ branch: 'new-branch-name',
+ });
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('clears error message', done => {
+ const dispatchSpy = jasmine.createSpy('dispatch');
+ spyOn(api, 'createBranch').and.returnValue(Promise.resolve());
+ spyOn(router, 'push');
+
+ createNewBranchFromDefault(
+ {
+ state: {
+ currentProjectId: 'project-path',
+ },
+ getters: {
+ currentProject: {
+ default_branch: 'master',
+ },
+ },
+ dispatch: dispatchSpy,
+ },
+ 'new-branch-name',
+ )
+ .then(() => {
+ expect(dispatchSpy).toHaveBeenCalledWith('setErrorMessage', null);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('reloads window', done => {
+ spyOn(api, 'createBranch').and.returnValue(Promise.resolve());
+ spyOn(router, 'push');
+
+ createNewBranchFromDefault(
+ {
+ state: {
+ currentProjectId: 'project-path',
+ },
+ getters: {
+ currentProject: {
+ default_branch: 'master',
+ },
+ },
+ dispatch() {},
+ },
+ 'new-branch-name',
+ )
+ .then(() => {
+ expect(router.push).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('getBranchData', () => {
+ describe('error', () => {
+ it('dispatches branch not found action when response is 404', done => {
+ const dispatch = jasmine.createSpy('dispatchSpy');
+
+ mock.onGet(/(.*)/).replyOnce(404);
+
+ getBranchData(
+ {
+ commit() {},
+ dispatch,
+ state: store.state,
+ },
+ {
+ projectId: 'abc/def',
+ branchId: 'master-testing',
+ },
+ )
+ .then(done.fail)
+ .catch(() => {
+ expect(dispatch.calls.argsFor(0)).toEqual([
+ 'showBranchNotFoundError',
+ 'master-testing',
+ ]);
+ done();
+ });
+ });
+ });
+ });
});
diff --git a/spec/javascripts/ide/stores/actions/tree_spec.js b/spec/javascripts/ide/stores/actions/tree_spec.js
index e0ef57a3966..6860e6cdb91 100644
--- a/spec/javascripts/ide/stores/actions/tree_spec.js
+++ b/spec/javascripts/ide/stores/actions/tree_spec.js
@@ -1,11 +1,16 @@
-import Vue from 'vue';
+import MockAdapter from 'axios-mock-adapter';
+import testAction from 'spec/helpers/vuex_action_helper';
+import { showTreeEntry, getFiles } from '~/ide/stores/actions/tree';
+import * as types from '~/ide/stores/mutation_types';
+import axios from '~/lib/utils/axios_utils';
import store from '~/ide/stores';
import service from '~/ide/services';
import router from '~/ide/ide_router';
-import { file, resetStore } from '../../helpers';
+import { file, resetStore, createEntriesFromPaths } from '../../helpers';
describe('Multi-file store tree actions', () => {
let projectTree;
+ let mock;
const basicCallParameters = {
endpoint: 'rootEndpoint',
@@ -17,6 +22,8 @@ describe('Multi-file store tree actions', () => {
beforeEach(() => {
spyOn(router, 'push');
+ mock = new MockAdapter(axios);
+
store.state.currentProjectId = 'abcproject';
store.state.currentBranchId = 'master';
store.state.projects.abcproject = {
@@ -30,49 +37,119 @@ describe('Multi-file store tree actions', () => {
});
afterEach(() => {
+ mock.restore();
resetStore(store);
});
describe('getFiles', () => {
- beforeEach(() => {
- spyOn(service, 'getFiles').and.returnValue(
- Promise.resolve({
- json: () =>
- Promise.resolve([
- 'file.txt',
- 'folder/fileinfolder.js',
- 'folder/subfolder/fileinsubfolder.js',
- ]),
- }),
- );
+ describe('success', () => {
+ beforeEach(() => {
+ spyOn(service, 'getFiles').and.callThrough();
+
+ mock
+ .onGet(/(.*)/)
+ .replyOnce(200, [
+ 'file.txt',
+ 'folder/fileinfolder.js',
+ 'folder/subfolder/fileinsubfolder.js',
+ ]);
+ });
+
+ it('calls service getFiles', done => {
+ store
+ .dispatch('getFiles', basicCallParameters)
+ .then(() => {
+ expect(service.getFiles).toHaveBeenCalledWith('', 'master');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('adds data into tree', done => {
+ store
+ .dispatch('getFiles', basicCallParameters)
+ .then(() => {
+ projectTree = store.state.trees['abcproject/master'];
+ expect(projectTree.tree.length).toBe(2);
+ expect(projectTree.tree[0].type).toBe('tree');
+ expect(projectTree.tree[0].tree[1].name).toBe('fileinfolder.js');
+ expect(projectTree.tree[1].type).toBe('blob');
+ expect(projectTree.tree[0].tree[0].tree[0].type).toBe('blob');
+ expect(projectTree.tree[0].tree[0].tree[0].name).toBe('fileinsubfolder.js');
+
+ done();
+ })
+ .catch(done.fail);
+ });
});
- it('calls service getFiles', done => {
- store
- .dispatch('getFiles', basicCallParameters)
- .then(() => {
- expect(service.getFiles).toHaveBeenCalledWith('', 'master');
+ describe('error', () => {
+ it('dispatches branch not found actions when response is 404', done => {
+ const dispatch = jasmine.createSpy('dispatchSpy');
- done();
- })
- .catch(done.fail);
- });
+ store.state.projects = {
+ 'abc/def': {
+ web_url: `${gl.TEST_HOST}/files`,
+ },
+ };
- it('adds data into tree', done => {
- store
- .dispatch('getFiles', basicCallParameters)
- .then(() => {
- projectTree = store.state.trees['abcproject/master'];
- expect(projectTree.tree.length).toBe(2);
- expect(projectTree.tree[0].type).toBe('tree');
- expect(projectTree.tree[0].tree[1].name).toBe('fileinfolder.js');
- expect(projectTree.tree[1].type).toBe('blob');
- expect(projectTree.tree[0].tree[0].tree[0].type).toBe('blob');
- expect(projectTree.tree[0].tree[0].tree[0].name).toBe('fileinsubfolder.js');
+ mock.onGet(/(.*)/).replyOnce(404);
- done();
- })
- .catch(done.fail);
+ getFiles(
+ {
+ commit() {},
+ dispatch,
+ state: store.state,
+ },
+ {
+ projectId: 'abc/def',
+ branchId: 'master-testing',
+ },
+ )
+ .then(done.fail)
+ .catch(() => {
+ expect(dispatch.calls.argsFor(0)).toEqual([
+ 'showBranchNotFoundError',
+ 'master-testing',
+ ]);
+ done();
+ });
+ });
+
+ it('dispatches error action', done => {
+ const dispatch = jasmine.createSpy('dispatchSpy');
+
+ store.state.projects = {
+ 'abc/def': {
+ web_url: `${gl.TEST_HOST}/files`,
+ },
+ };
+
+ mock.onGet(/(.*)/).replyOnce(500);
+
+ getFiles(
+ {
+ commit() {},
+ dispatch,
+ state: store.state,
+ },
+ {
+ projectId: 'abc/def',
+ branchId: 'master-testing',
+ },
+ )
+ .then(done.fail)
+ .catch(() => {
+ expect(dispatch).toHaveBeenCalledWith('setErrorMessage', {
+ text: 'An error occured whilst loading all the files.',
+ action: jasmine.any(Function),
+ actionText: 'Please try again',
+ actionPayload: { projectId: 'abc/def', branchId: 'master-testing' },
+ });
+ done();
+ });
+ });
});
});
@@ -96,71 +173,32 @@ describe('Multi-file store tree actions', () => {
});
});
- describe('getLastCommitData', () => {
+ describe('showTreeEntry', () => {
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.trees['abcproject/mybranch'] = {
- tree: [],
- };
-
- projectTree = store.state.trees['abcproject/mybranch'];
- projectTree.tree.push(file('testing', '1', 'tree'));
- projectTree.lastCommitPath = 'lastcommitpath';
+ const paths = [
+ 'grandparent',
+ 'ancestor',
+ 'grandparent/parent',
+ 'grandparent/aunt',
+ 'grandparent/parent/child.txt',
+ 'grandparent/aunt/cousing.txt',
+ ];
+
+ Object.assign(store.state.entries, createEntriesFromPaths(paths));
});
- it('calls service with lastCommitPath', done => {
- store
- .dispatch('getLastCommitData', projectTree)
- .then(() => {
- expect(service.getTreeLastCommit).toHaveBeenCalledWith('lastcommitpath');
-
- done();
- })
- .catch(done.fail);
- });
-
- it('updates trees last commit data', done => {
- store
- .dispatch('getLastCommitData', projectTree)
- .then(Vue.nextTick)
- .then(() => {
- expect(projectTree.tree[0].lastCommit.message).toBe('commit message');
-
- done();
- })
- .catch(done.fail);
- });
-
- it('does not update entry if not found', done => {
- projectTree.tree[0].name = 'a';
-
- store
- .dispatch('getLastCommitData', projectTree)
- .then(Vue.nextTick)
- .then(() => {
- expect(projectTree.tree[0].lastCommit.message).not.toBe('commit message');
-
- done();
- })
- .catch(done.fail);
+ it('opens the parents', done => {
+ testAction(
+ showTreeEntry,
+ 'grandparent/parent/child.txt',
+ store.state,
+ [
+ { type: types.SET_TREE_OPEN, payload: 'grandparent/parent' },
+ { type: types.SET_TREE_OPEN, payload: 'grandparent' },
+ ],
+ [{ type: 'showTreeEntry' }],
+ done,
+ );
});
});
});
diff --git a/spec/javascripts/ide/stores/actions_spec.js b/spec/javascripts/ide/stores/actions_spec.js
index 062c3497623..8b665a6d79e 100644
--- a/spec/javascripts/ide/stores/actions_spec.js
+++ b/spec/javascripts/ide/stores/actions_spec.js
@@ -6,6 +6,7 @@ import actions, {
setEmptyStateSvgs,
updateActivityBarView,
updateTempFlagForEntry,
+ setErrorMessage,
} from '~/ide/stores/actions';
import store from '~/ide/stores';
import * as types from '~/ide/stores/mutation_types';
@@ -443,4 +444,17 @@ describe('Multi-file store actions', () => {
);
});
});
+
+ describe('setErrorMessage', () => {
+ it('commis error messsage', done => {
+ testAction(
+ setErrorMessage,
+ 'error',
+ null,
+ [{ type: types.SET_ERROR_MESSAGE, payload: 'error' }],
+ [],
+ done,
+ );
+ });
+ });
});
diff --git a/spec/javascripts/ide/stores/getters_spec.js b/spec/javascripts/ide/stores/getters_spec.js
index 4833ba3edfd..70883e16b0d 100644
--- a/spec/javascripts/ide/stores/getters_spec.js
+++ b/spec/javascripts/ide/stores/getters_spec.js
@@ -147,12 +147,11 @@ describe('IDE store getters', () => {
const commitTitle = 'Example commit title';
const localGetters = {
currentProject: {
- branches: {
- 'example-branch': {
- commit: {
- title: commitTitle,
- },
- },
+ name: 'test-project',
+ },
+ currentBranch: {
+ commit: {
+ title: commitTitle,
},
},
};
@@ -161,4 +160,23 @@ describe('IDE store getters', () => {
expect(getters.lastCommit(localState, localGetters).title).toBe(commitTitle);
});
});
+
+ describe('currentBranch', () => {
+ it('returns current projects branch', () => {
+ const localGetters = {
+ currentProject: {
+ branches: {
+ master: {
+ name: 'master',
+ },
+ },
+ },
+ };
+ localState.currentBranchId = 'master';
+
+ expect(getters.currentBranch(localState, localGetters)).toEqual({
+ name: 'master',
+ });
+ });
+ });
});
diff --git a/spec/javascripts/ide/stores/modules/commit/actions_spec.js b/spec/javascripts/ide/stores/modules/commit/actions_spec.js
index a2869ff378b..133ad627f34 100644
--- a/spec/javascripts/ide/stores/modules/commit/actions_spec.js
+++ b/spec/javascripts/ide/stores/modules/commit/actions_spec.js
@@ -108,77 +108,6 @@ describe('IDE commit module actions', () => {
});
});
- describe('checkCommitStatus', () => {
- beforeEach(() => {
- store.state.currentProjectId = 'abcproject';
- store.state.currentBranchId = 'master';
- store.state.projects.abcproject = {
- branches: {
- master: {
- workingReference: '1',
- },
- },
- };
- });
-
- it('calls service', done => {
- spyOn(service, 'getBranchData').and.returnValue(
- Promise.resolve({
- data: {
- commit: { id: '123' },
- },
- }),
- );
-
- store
- .dispatch('commit/checkCommitStatus')
- .then(() => {
- expect(service.getBranchData).toHaveBeenCalledWith('abcproject', 'master');
-
- done();
- })
- .catch(done.fail);
- });
-
- it('returns true if current ref does not equal returned ID', done => {
- spyOn(service, 'getBranchData').and.returnValue(
- Promise.resolve({
- data: {
- commit: { id: '123' },
- },
- }),
- );
-
- store
- .dispatch('commit/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({
- data: {
- commit: { id: '1' },
- },
- }),
- );
-
- store
- .dispatch('commit/checkCommitStatus')
- .then(val => {
- expect(val).toBeFalsy();
-
- done();
- })
- .catch(done.fail);
- });
- });
-
describe('updateFilesAfterCommit', () => {
const data = {
id: '123',
@@ -314,6 +243,7 @@ describe('IDE commit module actions', () => {
...file('changed'),
type: 'blob',
active: true,
+ lastCommitSha: '123456789',
};
store.state.stagedFiles.push(f);
store.state.changedFiles = [
@@ -366,6 +296,7 @@ describe('IDE commit module actions', () => {
file_path: jasmine.anything(),
content: jasmine.anything(),
encoding: jasmine.anything(),
+ last_commit_id: undefined,
},
],
start_branch: 'master',
@@ -376,6 +307,32 @@ describe('IDE commit module actions', () => {
.catch(done.fail);
});
+ it('sends lastCommit ID when not creating new branch', done => {
+ store.state.commit.commitAction = '1';
+
+ store
+ .dispatch('commit/commitChanges')
+ .then(() => {
+ expect(service.commit).toHaveBeenCalledWith('abcproject', {
+ branch: jasmine.anything(),
+ commit_message: 'testing 123',
+ actions: [
+ {
+ action: 'update',
+ file_path: jasmine.anything(),
+ content: jasmine.anything(),
+ encoding: jasmine.anything(),
+ last_commit_id: '123456789',
+ },
+ ],
+ start_branch: undefined,
+ });
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
it('sets last Commit Msg', done => {
store
.dispatch('commit/commitChanges')
diff --git a/spec/javascripts/ide/stores/modules/commit/getters_spec.js b/spec/javascripts/ide/stores/modules/commit/getters_spec.js
index 55580f046ad..44c941d6dbb 100644
--- a/spec/javascripts/ide/stores/modules/commit/getters_spec.js
+++ b/spec/javascripts/ide/stores/modules/commit/getters_spec.js
@@ -29,46 +29,6 @@ describe('IDE commit module getters', () => {
});
});
- describe('commitButtonDisabled', () => {
- const localGetters = {
- discardDraftButtonDisabled: false,
- };
- const rootState = {
- stagedFiles: ['a'],
- };
-
- it('returns false when discardDraftButtonDisabled is false & stagedFiles is not empty', () => {
- expect(
- getters.commitButtonDisabled(state, localGetters, rootState),
- ).toBeFalsy();
- });
-
- it('returns true when discardDraftButtonDisabled is false & stagedFiles is empty', () => {
- rootState.stagedFiles.length = 0;
-
- expect(
- getters.commitButtonDisabled(state, localGetters, rootState),
- ).toBeTruthy();
- });
-
- it('returns true when discardDraftButtonDisabled is true', () => {
- localGetters.discardDraftButtonDisabled = true;
-
- expect(
- getters.commitButtonDisabled(state, localGetters, rootState),
- ).toBeTruthy();
- });
-
- it('returns true when discardDraftButtonDisabled is false & changedFiles is not empty', () => {
- localGetters.discardDraftButtonDisabled = false;
- rootState.stagedFiles.length = 0;
-
- expect(
- getters.commitButtonDisabled(state, localGetters, rootState),
- ).toBeTruthy();
- });
- });
-
describe('newBranchName', () => {
it('includes username, currentBranchId, patch & random number', () => {
gon.current_username = 'username';
@@ -108,9 +68,7 @@ describe('IDE commit module getters', () => {
});
it('uses newBranchName when not empty', () => {
- expect(getters.branchName(state, localGetters, rootState)).toBe(
- 'state-newBranchName',
- );
+ expect(getters.branchName(state, localGetters, rootState)).toBe('state-newBranchName');
});
it('uses getters newBranchName when state newBranchName is empty', () => {
@@ -118,11 +76,53 @@ describe('IDE commit module getters', () => {
newBranchName: '',
});
- expect(getters.branchName(state, localGetters, rootState)).toBe(
- 'newBranchName',
- );
+ expect(getters.branchName(state, localGetters, rootState)).toBe('newBranchName');
});
});
});
});
+
+ describe('preBuiltCommitMessage', () => {
+ let rootState = {};
+
+ beforeEach(() => {
+ rootState.changedFiles = [];
+ rootState.stagedFiles = [];
+ });
+
+ afterEach(() => {
+ rootState = {};
+ });
+
+ it('returns commitMessage when set', () => {
+ state.commitMessage = 'test commit message';
+
+ expect(getters.preBuiltCommitMessage(state, null, rootState)).toBe('test commit message');
+ });
+
+ ['changedFiles', 'stagedFiles'].forEach(key => {
+ it('returns commitMessage with updated file', () => {
+ rootState[key].push({
+ path: 'test-file',
+ });
+
+ expect(getters.preBuiltCommitMessage(state, null, rootState)).toBe('Update test-file');
+ });
+
+ it('returns commitMessage with updated files', () => {
+ rootState[key].push(
+ {
+ path: 'test-file',
+ },
+ {
+ path: 'index.js',
+ },
+ );
+
+ expect(getters.preBuiltCommitMessage(state, null, rootState)).toBe(
+ 'Update test-file, index.js files',
+ );
+ });
+ });
+ });
});
diff --git a/spec/javascripts/ide/stores/modules/merge_requests/actions_spec.js b/spec/javascripts/ide/stores/modules/merge_requests/actions_spec.js
new file mode 100644
index 00000000000..d21f33eaf6d
--- /dev/null
+++ b/spec/javascripts/ide/stores/modules/merge_requests/actions_spec.js
@@ -0,0 +1,240 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import state from '~/ide/stores/modules/merge_requests/state';
+import * as types from '~/ide/stores/modules/merge_requests/mutation_types';
+import {
+ requestMergeRequests,
+ receiveMergeRequestsError,
+ receiveMergeRequestsSuccess,
+ fetchMergeRequests,
+ resetMergeRequests,
+ openMergeRequest,
+} from '~/ide/stores/modules/merge_requests/actions';
+import router from '~/ide/ide_router';
+import { mergeRequests } from '../../../mock_data';
+import testAction from '../../../../helpers/vuex_action_helper';
+
+describe('IDE merge requests actions', () => {
+ let mockedState;
+ let mock;
+
+ beforeEach(() => {
+ mockedState = state();
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('requestMergeRequests', () => {
+ it('should should commit request', done => {
+ testAction(
+ requestMergeRequests,
+ 'created',
+ mockedState,
+ [{ type: types.REQUEST_MERGE_REQUESTS, payload: 'created' }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveMergeRequestsError', () => {
+ it('should should commit error', done => {
+ testAction(
+ receiveMergeRequestsError,
+ { type: 'created', search: '' },
+ mockedState,
+ [{ type: types.RECEIVE_MERGE_REQUESTS_ERROR, payload: 'created' }],
+ [
+ {
+ type: 'setErrorMessage',
+ payload: {
+ text: 'Error loading merge requests.',
+ action: jasmine.any(Function),
+ actionText: 'Please try again',
+ actionPayload: { type: 'created', search: '' },
+ },
+ },
+ ],
+ done,
+ );
+ });
+ });
+
+ describe('receiveMergeRequestsSuccess', () => {
+ it('should commit received data', done => {
+ testAction(
+ receiveMergeRequestsSuccess,
+ { type: 'created', data: 'data' },
+ mockedState,
+ [
+ {
+ type: types.RECEIVE_MERGE_REQUESTS_SUCCESS,
+ payload: { type: 'created', data: 'data' },
+ },
+ ],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('fetchMergeRequests', () => {
+ beforeEach(() => {
+ gon.api_version = 'v4';
+ });
+
+ describe('success', () => {
+ beforeEach(() => {
+ mock.onGet(/\/api\/v4\/merge_requests(.*)$/).replyOnce(200, mergeRequests);
+ });
+
+ it('calls API with params', () => {
+ const apiSpy = spyOn(axios, 'get').and.callThrough();
+
+ fetchMergeRequests({ dispatch() {}, state: mockedState }, { type: 'created' });
+
+ expect(apiSpy).toHaveBeenCalledWith(jasmine.anything(), {
+ params: {
+ scope: 'created-by-me',
+ state: 'opened',
+ search: '',
+ },
+ });
+ });
+
+ it('calls API with search', () => {
+ const apiSpy = spyOn(axios, 'get').and.callThrough();
+
+ fetchMergeRequests(
+ { dispatch() {}, state: mockedState },
+ { type: 'created', search: 'testing search' },
+ );
+
+ expect(apiSpy).toHaveBeenCalledWith(jasmine.anything(), {
+ params: {
+ scope: 'created-by-me',
+ state: 'opened',
+ search: 'testing search',
+ },
+ });
+ });
+
+ it('dispatches request', done => {
+ testAction(
+ fetchMergeRequests,
+ { type: 'created' },
+ mockedState,
+ [],
+ [
+ { type: 'requestMergeRequests' },
+ { type: 'resetMergeRequests' },
+ { type: 'receiveMergeRequestsSuccess' },
+ ],
+ done,
+ );
+ });
+
+ it('dispatches success with received data', done => {
+ testAction(
+ fetchMergeRequests,
+ { type: 'created' },
+ mockedState,
+ [],
+ [
+ { type: 'requestMergeRequests' },
+ { type: 'resetMergeRequests' },
+ {
+ type: 'receiveMergeRequestsSuccess',
+ payload: { type: 'created', data: mergeRequests },
+ },
+ ],
+ done,
+ );
+ });
+ });
+
+ describe('error', () => {
+ beforeEach(() => {
+ mock.onGet(/\/api\/v4\/merge_requests(.*)$/).replyOnce(500);
+ });
+
+ it('dispatches error', done => {
+ testAction(
+ fetchMergeRequests,
+ { type: 'created' },
+ mockedState,
+ [],
+ [
+ { type: 'requestMergeRequests' },
+ { type: 'resetMergeRequests' },
+ { type: 'receiveMergeRequestsError' },
+ ],
+ done,
+ );
+ });
+ });
+ });
+
+ describe('resetMergeRequests', () => {
+ it('commits reset', done => {
+ testAction(
+ resetMergeRequests,
+ 'created',
+ mockedState,
+ [{ type: types.RESET_MERGE_REQUESTS, payload: 'created' }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('openMergeRequest', () => {
+ beforeEach(() => {
+ spyOn(router, 'push');
+ });
+
+ it('commits reset mutations and actions', done => {
+ const commit = jasmine.createSpy();
+ const dispatch = jasmine.createSpy().and.returnValue(Promise.resolve());
+ openMergeRequest({ commit, dispatch }, { projectPath: 'gitlab-org/gitlab-ce', id: '1' });
+
+ setTimeout(() => {
+ expect(commit.calls.argsFor(0)).toEqual(['CLEAR_PROJECTS', null, { root: true }]);
+ expect(commit.calls.argsFor(1)).toEqual(['SET_CURRENT_MERGE_REQUEST', '1', { root: true }]);
+ expect(commit.calls.argsFor(2)).toEqual(['RESET_OPEN_FILES', null, { root: true }]);
+
+ expect(dispatch.calls.argsFor(0)).toEqual(['setCurrentBranchId', '', { root: true }]);
+ expect(dispatch.calls.argsFor(1)).toEqual([
+ 'pipelines/stopPipelinePolling',
+ null,
+ { root: true },
+ ]);
+ expect(dispatch.calls.argsFor(2)).toEqual(['setRightPane', null, { root: true }]);
+ expect(dispatch.calls.argsFor(3)).toEqual([
+ 'pipelines/resetLatestPipeline',
+ null,
+ { root: true },
+ ]);
+ expect(dispatch.calls.argsFor(4)).toEqual([
+ 'pipelines/clearEtagPoll',
+ null,
+ { root: true },
+ ]);
+
+ done();
+ });
+ });
+
+ it('pushes new route', () => {
+ openMergeRequest(
+ { commit() {}, dispatch: () => Promise.resolve() },
+ { projectPath: 'gitlab-org/gitlab-ce', id: '1' },
+ );
+
+ expect(router.push).toHaveBeenCalledWith('/project/gitlab-org/gitlab-ce/merge_requests/1');
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/modules/merge_requests/mutations_spec.js b/spec/javascripts/ide/stores/modules/merge_requests/mutations_spec.js
new file mode 100644
index 00000000000..ea03131d90d
--- /dev/null
+++ b/spec/javascripts/ide/stores/modules/merge_requests/mutations_spec.js
@@ -0,0 +1,58 @@
+import state from '~/ide/stores/modules/merge_requests/state';
+import mutations from '~/ide/stores/modules/merge_requests/mutations';
+import * as types from '~/ide/stores/modules/merge_requests/mutation_types';
+import { mergeRequests } from '../../../mock_data';
+
+describe('IDE merge requests mutations', () => {
+ let mockedState;
+
+ beforeEach(() => {
+ mockedState = state();
+ });
+
+ describe(types.REQUEST_MERGE_REQUESTS, () => {
+ it('sets loading to true', () => {
+ mutations[types.REQUEST_MERGE_REQUESTS](mockedState, 'created');
+
+ expect(mockedState.created.isLoading).toBe(true);
+ });
+ });
+
+ describe(types.RECEIVE_MERGE_REQUESTS_ERROR, () => {
+ it('sets loading to false', () => {
+ mutations[types.RECEIVE_MERGE_REQUESTS_ERROR](mockedState, 'created');
+
+ expect(mockedState.created.isLoading).toBe(false);
+ });
+ });
+
+ describe(types.RECEIVE_MERGE_REQUESTS_SUCCESS, () => {
+ it('sets merge requests', () => {
+ gon.gitlab_url = gl.TEST_HOST;
+ mutations[types.RECEIVE_MERGE_REQUESTS_SUCCESS](mockedState, {
+ type: 'created',
+ data: mergeRequests,
+ });
+
+ expect(mockedState.created.mergeRequests).toEqual([
+ {
+ id: 1,
+ iid: 1,
+ title: 'Test merge request',
+ projectId: 1,
+ projectPathWithNamespace: 'namespace/project-path',
+ },
+ ]);
+ });
+ });
+
+ describe(types.RESET_MERGE_REQUESTS, () => {
+ it('clears merge request array', () => {
+ mockedState.mergeRequests = ['test'];
+
+ mutations[types.RESET_MERGE_REQUESTS](mockedState, 'created');
+
+ expect(mockedState.created.mergeRequests).toEqual([]);
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js b/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js
index 85fbcf8084b..836ba72b5d8 100644
--- a/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js
+++ b/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js
@@ -1,17 +1,28 @@
+import Visibility from 'visibilityjs';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
-import actions, {
+import {
requestLatestPipeline,
receiveLatestPipelineError,
receiveLatestPipelineSuccess,
fetchLatestPipeline,
+ stopPipelinePolling,
+ clearEtagPoll,
requestJobs,
receiveJobsError,
receiveJobsSuccess,
fetchJobs,
+ toggleStageCollapsed,
+ setDetailJob,
+ requestJobTrace,
+ receiveJobTraceError,
+ receiveJobTraceSuccess,
+ fetchJobTrace,
+ resetLatestPipeline,
} from '~/ide/stores/modules/pipelines/actions';
import state from '~/ide/stores/modules/pipelines/state';
import * as types from '~/ide/stores/modules/pipelines/mutation_types';
+import { rightSidebarViews } from '~/ide/constants';
import testAction from '../../../../helpers/vuex_action_helper';
import { pipelines, jobs } from '../../../mock_data';
@@ -48,102 +59,156 @@ describe('IDE pipelines actions', () => {
it('commits error', done => {
testAction(
receiveLatestPipelineError,
- null,
+ { status: 404 },
mockedState,
[{ type: types.RECEIVE_LASTEST_PIPELINE_ERROR }],
- [],
+ [{ type: 'stopPipelinePolling' }],
done,
);
});
- it('creates flash message', () => {
- const flashSpy = spyOnDependency(actions, 'flash');
-
- receiveLatestPipelineError({ commit() {} });
-
- expect(flashSpy).toHaveBeenCalled();
+ it('dispatches setErrorMessage is not 404', done => {
+ testAction(
+ receiveLatestPipelineError,
+ { status: 500 },
+ mockedState,
+ [{ type: types.RECEIVE_LASTEST_PIPELINE_ERROR }],
+ [
+ {
+ type: 'setErrorMessage',
+ payload: {
+ text: 'An error occured whilst fetching the latest pipline.',
+ action: jasmine.any(Function),
+ actionText: 'Please try again',
+ actionPayload: null,
+ },
+ },
+ { type: 'stopPipelinePolling' },
+ ],
+ done,
+ );
});
});
describe('receiveLatestPipelineSuccess', () => {
- it('commits pipeline', done => {
- testAction(
- receiveLatestPipelineSuccess,
+ const rootGetters = {
+ lastCommit: { id: '123' },
+ };
+ let commit;
+
+ beforeEach(() => {
+ commit = jasmine.createSpy('commit');
+ });
+
+ it('commits pipeline', () => {
+ receiveLatestPipelineSuccess({ rootGetters, commit }, { pipelines });
+
+ expect(commit.calls.argsFor(0)).toEqual([
+ types.RECEIVE_LASTEST_PIPELINE_SUCCESS,
pipelines[0],
- mockedState,
- [{ type: types.RECEIVE_LASTEST_PIPELINE_SUCCESS, payload: pipelines[0] }],
- [],
- done,
- );
+ ]);
+ });
+
+ it('commits false when there are no pipelines', () => {
+ receiveLatestPipelineSuccess({ rootGetters, commit }, { pipelines: [] });
+
+ expect(commit.calls.argsFor(0)).toEqual([types.RECEIVE_LASTEST_PIPELINE_SUCCESS, false]);
});
});
describe('fetchLatestPipeline', () => {
+ beforeEach(() => {
+ jasmine.clock().install();
+ });
+
+ afterEach(() => {
+ jasmine.clock().uninstall();
+ stopPipelinePolling();
+ clearEtagPoll();
+ });
+
describe('success', () => {
beforeEach(() => {
- mock.onGet(/\/api\/v4\/projects\/(.*)\/pipelines(.*)/).replyOnce(200, pipelines);
+ mock
+ .onGet('/abc/def/commit/abc123def456ghi789jkl/pipelines')
+ .reply(200, { data: { foo: 'bar' } }, { 'poll-interval': '10000' });
});
it('dispatches request', done => {
- testAction(
- fetchLatestPipeline,
- '123',
- mockedState,
- [],
- [{ type: 'requestLatestPipeline' }, { type: 'receiveLatestPipelineSuccess' }],
- done,
- );
- });
-
- it('dispatches success with latest pipeline', done => {
- testAction(
- fetchLatestPipeline,
- '123',
- mockedState,
- [],
- [
- { type: 'requestLatestPipeline' },
- { type: 'receiveLatestPipelineSuccess', payload: pipelines[0] },
- ],
- done,
- );
- });
-
- it('calls axios with correct params', () => {
- const apiSpy = spyOn(axios, 'get').and.callThrough();
-
- fetchLatestPipeline({ dispatch() {}, rootState: state }, '123');
-
- expect(apiSpy).toHaveBeenCalledWith(jasmine.anything(), {
- params: {
- sha: '123',
- per_page: '1',
- },
- });
+ spyOn(axios, 'get').and.callThrough();
+ spyOn(Visibility, 'hidden').and.returnValue(false);
+
+ const dispatch = jasmine.createSpy('dispatch');
+ const rootGetters = {
+ lastCommit: { id: 'abc123def456ghi789jkl' },
+ currentProject: { path_with_namespace: 'abc/def' },
+ };
+
+ fetchLatestPipeline({ dispatch, rootGetters });
+
+ expect(dispatch.calls.argsFor(0)).toEqual(['requestLatestPipeline']);
+
+ jasmine.clock().tick(1000);
+
+ new Promise(resolve => requestAnimationFrame(resolve))
+ .then(() => {
+ expect(axios.get).toHaveBeenCalled();
+ expect(axios.get.calls.count()).toBe(1);
+
+ expect(dispatch.calls.argsFor(1)).toEqual([
+ 'receiveLatestPipelineSuccess',
+ jasmine.anything(),
+ ]);
+
+ jasmine.clock().tick(10000);
+ })
+ .then(() => new Promise(resolve => requestAnimationFrame(resolve)))
+ .then(() => {
+ expect(axios.get).toHaveBeenCalled();
+ expect(axios.get.calls.count()).toBe(2);
+
+ expect(dispatch.calls.argsFor(2)).toEqual([
+ 'receiveLatestPipelineSuccess',
+ jasmine.anything(),
+ ]);
+ })
+ .then(done)
+ .catch(done.fail);
});
});
describe('error', () => {
beforeEach(() => {
- mock.onGet(/\/api\/v4\/projects\/(.*)\/pipelines(.*)/).replyOnce(500);
+ mock.onGet('/abc/def/commit/abc123def456ghi789jkl/pipelines').reply(500);
});
it('dispatches error', done => {
- testAction(
- fetchLatestPipeline,
- '123',
- mockedState,
- [],
- [{ type: 'requestLatestPipeline' }, { type: 'receiveLatestPipelineError' }],
- done,
- );
+ const dispatch = jasmine.createSpy('dispatch');
+ const rootGetters = {
+ lastCommit: { id: 'abc123def456ghi789jkl' },
+ currentProject: { path_with_namespace: 'abc/def' },
+ };
+
+ fetchLatestPipeline({ dispatch, rootGetters });
+
+ jasmine.clock().tick(1500);
+
+ new Promise(resolve => requestAnimationFrame(resolve))
+ .then(() => {
+ expect(dispatch.calls.argsFor(1)).toEqual([
+ 'receiveLatestPipelineError',
+ jasmine.anything(),
+ ]);
+ })
+ .then(done)
+ .catch(done.fail);
});
});
});
describe('requestJobs', () => {
it('commits request', done => {
- testAction(requestJobs, null, mockedState, [{ type: types.REQUEST_JOBS }], [], done);
+ testAction(requestJobs, 1, mockedState, [{ type: types.REQUEST_JOBS, payload: 1 }], [], done);
});
});
@@ -151,30 +216,32 @@ describe('IDE pipelines actions', () => {
it('commits error', done => {
testAction(
receiveJobsError,
- null,
+ { id: 1 },
mockedState,
- [{ type: types.RECEIVE_JOBS_ERROR }],
- [],
+ [{ type: types.RECEIVE_JOBS_ERROR, payload: 1 }],
+ [
+ {
+ type: 'setErrorMessage',
+ payload: {
+ text: 'An error occured whilst loading the pipelines jobs.',
+ action: jasmine.anything(),
+ actionText: 'Please try again',
+ actionPayload: { id: 1 },
+ },
+ },
+ ],
done,
);
});
-
- it('creates flash message', () => {
- const flashSpy = spyOnDependency(actions, 'flash');
-
- receiveJobsError({ commit() {} });
-
- expect(flashSpy).toHaveBeenCalled();
- });
});
describe('receiveJobsSuccess', () => {
- it('commits jobs', done => {
+ it('commits data', done => {
testAction(
receiveJobsSuccess,
- jobs,
+ { id: 1, data: jobs },
mockedState,
- [{ type: types.RECEIVE_JOBS_SUCCESS, payload: jobs }],
+ [{ type: types.RECEIVE_JOBS_SUCCESS, payload: { id: 1, data: jobs } }],
[],
done,
);
@@ -182,108 +249,209 @@ describe('IDE pipelines actions', () => {
});
describe('fetchJobs', () => {
- let page = '';
-
- beforeEach(() => {
- mockedState.latestPipeline = pipelines[0];
- });
+ const stage = {
+ id: 1,
+ dropdownPath: `${gl.TEST_HOST}/jobs`,
+ };
describe('success', () => {
beforeEach(() => {
- mock.onGet(/\/api\/v4\/projects\/(.*)\/pipelines\/(.*)\/jobs/).replyOnce(() => [
- 200,
- jobs,
- {
- 'x-next-page': page,
- },
- ]);
+ mock.onGet(stage.dropdownPath).replyOnce(200, jobs);
});
it('dispatches request', done => {
testAction(
fetchJobs,
- null,
+ stage,
mockedState,
[],
- [{ type: 'requestJobs' }, { type: 'receiveJobsSuccess' }],
+ [
+ { type: 'requestJobs', payload: stage.id },
+ { type: 'receiveJobsSuccess', payload: { id: stage.id, data: jobs } },
+ ],
done,
);
});
+ });
- it('dispatches success with latest pipeline', done => {
- testAction(
- fetchJobs,
- null,
- mockedState,
- [],
- [{ type: 'requestJobs' }, { type: 'receiveJobsSuccess', payload: jobs }],
- done,
- );
+ describe('error', () => {
+ beforeEach(() => {
+ mock.onGet(stage.dropdownPath).replyOnce(500);
});
- it('dispatches twice for both pages', done => {
- page = '2';
-
+ it('dispatches error', done => {
testAction(
fetchJobs,
- null,
+ stage,
mockedState,
[],
[
- { type: 'requestJobs' },
- { type: 'receiveJobsSuccess', payload: jobs },
- { type: 'fetchJobs', payload: '2' },
- { type: 'requestJobs' },
- { type: 'receiveJobsSuccess', payload: jobs },
+ { type: 'requestJobs', payload: stage.id },
+ { type: 'receiveJobsError', payload: stage },
],
done,
);
});
+ });
+ });
- it('calls axios with correct URL', () => {
- const apiSpy = spyOn(axios, 'get').and.callThrough();
+ describe('toggleStageCollapsed', () => {
+ it('commits collapse', done => {
+ testAction(
+ toggleStageCollapsed,
+ 1,
+ mockedState,
+ [{ type: types.TOGGLE_STAGE_COLLAPSE, payload: 1 }],
+ [],
+ done,
+ );
+ });
+ });
- fetchJobs({ dispatch() {}, state: mockedState, rootState: mockedState });
+ describe('setDetailJob', () => {
+ it('commits job', done => {
+ testAction(
+ setDetailJob,
+ 'job',
+ mockedState,
+ [{ type: types.SET_DETAIL_JOB, payload: 'job' }],
+ [{ type: 'setRightPane' }],
+ done,
+ );
+ });
- expect(apiSpy).toHaveBeenCalledWith('/api/v4/projects/test%2Fproject/pipelines/1/jobs', {
- params: { page: '1' },
- });
- });
+ it('dispatches setRightPane as pipeline when job is null', done => {
+ testAction(
+ setDetailJob,
+ null,
+ mockedState,
+ [{ type: types.SET_DETAIL_JOB }],
+ [{ type: 'setRightPane', payload: rightSidebarViews.pipelines }],
+ done,
+ );
+ });
- it('calls axios with page next page', () => {
- const apiSpy = spyOn(axios, 'get').and.callThrough();
+ it('dispatches setRightPane as job', done => {
+ testAction(
+ setDetailJob,
+ 'job',
+ mockedState,
+ [{ type: types.SET_DETAIL_JOB }],
+ [{ type: 'setRightPane', payload: rightSidebarViews.jobsDetail }],
+ done,
+ );
+ });
+ });
- fetchJobs({ dispatch() {}, state: mockedState, rootState: mockedState });
+ describe('requestJobTrace', () => {
+ it('commits request', done => {
+ testAction(requestJobTrace, null, mockedState, [{ type: types.REQUEST_JOB_TRACE }], [], done);
+ });
+ });
- expect(apiSpy).toHaveBeenCalledWith('/api/v4/projects/test%2Fproject/pipelines/1/jobs', {
- params: { page: '1' },
- });
+ describe('receiveJobTraceError', () => {
+ it('commits error', done => {
+ testAction(
+ receiveJobTraceError,
+ null,
+ mockedState,
+ [{ type: types.RECEIVE_JOB_TRACE_ERROR }],
+ [
+ {
+ type: 'setErrorMessage',
+ payload: {
+ text: 'An error occured whilst fetching the job trace.',
+ action: jasmine.any(Function),
+ actionText: 'Please try again',
+ actionPayload: null,
+ },
+ },
+ ],
+ done,
+ );
+ });
+ });
+
+ describe('receiveJobTraceSuccess', () => {
+ it('commits data', done => {
+ testAction(
+ receiveJobTraceSuccess,
+ 'data',
+ mockedState,
+ [{ type: types.RECEIVE_JOB_TRACE_SUCCESS, payload: 'data' }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('fetchJobTrace', () => {
+ beforeEach(() => {
+ mockedState.detailJob = {
+ path: `${gl.TEST_HOST}/project/builds`,
+ };
+ });
+
+ describe('success', () => {
+ beforeEach(() => {
+ spyOn(axios, 'get').and.callThrough();
+ mock.onGet(`${gl.TEST_HOST}/project/builds/trace`).replyOnce(200, { html: 'html' });
+ });
- page = '2';
+ it('dispatches request', done => {
+ testAction(
+ fetchJobTrace,
+ null,
+ mockedState,
+ [],
+ [
+ { type: 'requestJobTrace' },
+ { type: 'receiveJobTraceSuccess', payload: { html: 'html' } },
+ ],
+ done,
+ );
+ });
- fetchJobs({ dispatch() {}, state: mockedState, rootState: mockedState }, page);
+ it('sends get request to correct URL', () => {
+ fetchJobTrace({ state: mockedState, dispatch() {} });
- expect(apiSpy).toHaveBeenCalledWith('/api/v4/projects/test%2Fproject/pipelines/1/jobs', {
- params: { page: '2' },
+ expect(axios.get).toHaveBeenCalledWith(`${gl.TEST_HOST}/project/builds/trace`, {
+ params: { format: 'json' },
});
});
});
describe('error', () => {
beforeEach(() => {
- mock.onGet(/\/api\/v4\/projects\/(.*)\/pipelines(.*)/).replyOnce(500);
+ mock.onGet(`${gl.TEST_HOST}/project/builds/trace`).replyOnce(500);
});
it('dispatches error', done => {
testAction(
- fetchJobs,
+ fetchJobTrace,
null,
mockedState,
[],
- [{ type: 'requestJobs' }, { type: 'receiveJobsError' }],
+ [{ type: 'requestJobTrace' }, { type: 'receiveJobTraceError' }],
done,
);
});
});
});
+
+ describe('resetLatestPipeline', () => {
+ it('commits reset mutations', done => {
+ testAction(
+ resetLatestPipeline,
+ null,
+ mockedState,
+ [
+ { type: types.RECEIVE_LASTEST_PIPELINE_SUCCESS, payload: null },
+ { type: types.SET_DETAIL_JOB, payload: null },
+ ],
+ [],
+ done,
+ );
+ });
+ });
});
diff --git a/spec/javascripts/ide/stores/modules/pipelines/getters_spec.js b/spec/javascripts/ide/stores/modules/pipelines/getters_spec.js
index b2a7e8a9025..4514896b5ea 100644
--- a/spec/javascripts/ide/stores/modules/pipelines/getters_spec.js
+++ b/spec/javascripts/ide/stores/modules/pipelines/getters_spec.js
@@ -37,35 +37,4 @@ describe('IDE pipeline getters', () => {
expect(getters.hasLatestPipeline(mockedState)).toBe(true);
});
});
-
- describe('failedJobs', () => {
- it('returns array of failed jobs', () => {
- mockedState.stages = [
- {
- title: 'test',
- jobs: [{ id: 1, status: 'failed' }, { id: 2, status: 'success' }],
- },
- {
- title: 'build',
- jobs: [{ id: 3, status: 'failed' }, { id: 4, status: 'failed' }],
- },
- ];
-
- expect(getters.failedJobs(mockedState).length).toBe(3);
- expect(getters.failedJobs(mockedState)).toEqual([
- {
- id: 1,
- status: jasmine.anything(),
- },
- {
- id: 3,
- status: jasmine.anything(),
- },
- {
- id: 4,
- status: jasmine.anything(),
- },
- ]);
- });
- });
});
diff --git a/spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js b/spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js
index 8262e916243..eb7346bd5fc 100644
--- a/spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js
+++ b/spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js
@@ -1,7 +1,7 @@
import mutations from '~/ide/stores/modules/pipelines/mutations';
import state from '~/ide/stores/modules/pipelines/state';
import * as types from '~/ide/stores/modules/pipelines/mutation_types';
-import { pipelines, jobs } from '../../../mock_data';
+import { fullPipelinesResponse, stages, jobs } from '../../../mock_data';
describe('IDE pipelines mutations', () => {
let mockedState;
@@ -28,93 +28,196 @@ describe('IDE pipelines mutations', () => {
describe(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, () => {
it('sets loading to false on success', () => {
- mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](mockedState, pipelines[0]);
+ mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](
+ mockedState,
+ fullPipelinesResponse.data.pipelines[0],
+ );
expect(mockedState.isLoadingPipeline).toBe(false);
});
it('sets latestPipeline', () => {
- mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](mockedState, pipelines[0]);
+ mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](
+ mockedState,
+ fullPipelinesResponse.data.pipelines[0],
+ );
expect(mockedState.latestPipeline).toEqual({
- id: pipelines[0].id,
- status: pipelines[0].status,
+ id: '51',
+ path: 'test',
+ commit: { id: '123' },
+ details: { status: jasmine.any(Object) },
+ yamlError: undefined,
});
});
it('does not set latest pipeline if pipeline is null', () => {
mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](mockedState, null);
- expect(mockedState.latestPipeline).toEqual(null);
+ expect(mockedState.latestPipeline).toEqual(false);
+ });
+
+ it('sets stages', () => {
+ mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](
+ mockedState,
+ fullPipelinesResponse.data.pipelines[0],
+ );
+
+ expect(mockedState.stages.length).toBe(2);
+ expect(mockedState.stages).toEqual([
+ {
+ id: 0,
+ dropdownPath: stages[0].dropdown_path,
+ name: stages[0].name,
+ status: stages[0].status,
+ isCollapsed: false,
+ isLoading: false,
+ jobs: [],
+ },
+ {
+ id: 1,
+ dropdownPath: stages[1].dropdown_path,
+ name: stages[1].name,
+ status: stages[1].status,
+ isCollapsed: false,
+ isLoading: false,
+ jobs: [],
+ },
+ ]);
});
});
describe(types.REQUEST_JOBS, () => {
- it('sets jobs loading to true', () => {
- mutations[types.REQUEST_JOBS](mockedState);
+ beforeEach(() => {
+ mockedState.stages = stages.map((stage, i) => ({
+ ...stage,
+ id: i,
+ }));
+ });
- expect(mockedState.isLoadingJobs).toBe(true);
+ it('sets isLoading on stage', () => {
+ mutations[types.REQUEST_JOBS](mockedState, mockedState.stages[0].id);
+
+ expect(mockedState.stages[0].isLoading).toBe(true);
});
});
describe(types.RECEIVE_JOBS_ERROR, () => {
- it('sets jobs loading to false', () => {
- mutations[types.RECEIVE_JOBS_ERROR](mockedState);
+ beforeEach(() => {
+ mockedState.stages = stages.map((stage, i) => ({
+ ...stage,
+ id: i,
+ }));
+ });
+
+ it('sets isLoading on stage after error', () => {
+ mutations[types.RECEIVE_JOBS_ERROR](mockedState, mockedState.stages[0].id);
- expect(mockedState.isLoadingJobs).toBe(false);
+ expect(mockedState.stages[0].isLoading).toBe(false);
});
});
describe(types.RECEIVE_JOBS_SUCCESS, () => {
- it('sets jobs loading to false on success', () => {
- mutations[types.RECEIVE_JOBS_SUCCESS](mockedState, jobs);
+ let data;
- expect(mockedState.isLoadingJobs).toBe(false);
+ beforeEach(() => {
+ mockedState.stages = stages.map((stage, i) => ({
+ ...stage,
+ id: i,
+ }));
+
+ data = {
+ latest_statuses: [...jobs],
+ };
});
- it('sets stages', () => {
- mutations[types.RECEIVE_JOBS_SUCCESS](mockedState, jobs);
+ it('updates loading', () => {
+ mutations[types.RECEIVE_JOBS_SUCCESS](mockedState, { id: mockedState.stages[0].id, data });
- expect(mockedState.stages.length).toBe(2);
- expect(mockedState.stages).toEqual([
- {
- title: 'test',
- jobs: jasmine.anything(),
- },
- {
- title: 'build',
- jobs: jasmine.anything(),
- },
- ]);
+ expect(mockedState.stages[0].isLoading).toBe(false);
});
- it('sets jobs in stages', () => {
- mutations[types.RECEIVE_JOBS_SUCCESS](mockedState, jobs);
+ it('sets jobs on stage', () => {
+ mutations[types.RECEIVE_JOBS_SUCCESS](mockedState, { id: mockedState.stages[0].id, data });
+
+ expect(mockedState.stages[0].jobs.length).toBe(jobs.length);
+ expect(mockedState.stages[0].jobs).toEqual(
+ jobs.map(job => ({
+ id: job.id,
+ name: job.name,
+ status: job.status,
+ path: job.build_path,
+ rawPath: `${job.build_path}/raw`,
+ started: job.started,
+ isLoading: false,
+ output: '',
+ })),
+ );
+ });
+ });
- expect(mockedState.stages[0].jobs.length).toBe(3);
- expect(mockedState.stages[1].jobs.length).toBe(1);
- expect(mockedState.stages).toEqual([
- {
- title: jasmine.anything(),
- jobs: jobs.filter(job => job.stage === 'test').map(job => ({
- id: job.id,
- name: job.name,
- status: job.status,
- stage: job.stage,
- duration: job.duration,
- })),
- },
- {
- title: jasmine.anything(),
- jobs: jobs.filter(job => job.stage === 'build').map(job => ({
- id: job.id,
- name: job.name,
- status: job.status,
- stage: job.stage,
- duration: job.duration,
- })),
- },
- ]);
+ describe(types.TOGGLE_STAGE_COLLAPSE, () => {
+ beforeEach(() => {
+ mockedState.stages = stages.map((stage, i) => ({
+ ...stage,
+ id: i,
+ isCollapsed: false,
+ }));
+ });
+
+ it('toggles collapsed state', () => {
+ mutations[types.TOGGLE_STAGE_COLLAPSE](mockedState, mockedState.stages[0].id);
+
+ expect(mockedState.stages[0].isCollapsed).toBe(true);
+
+ mutations[types.TOGGLE_STAGE_COLLAPSE](mockedState, mockedState.stages[0].id);
+
+ expect(mockedState.stages[0].isCollapsed).toBe(false);
+ });
+ });
+
+ describe(types.SET_DETAIL_JOB, () => {
+ it('sets detail job', () => {
+ mutations[types.SET_DETAIL_JOB](mockedState, jobs[0]);
+
+ expect(mockedState.detailJob).toEqual(jobs[0]);
+ });
+ });
+
+ describe(types.REQUEST_JOB_TRACE, () => {
+ beforeEach(() => {
+ mockedState.detailJob = { ...jobs[0] };
+ });
+
+ it('sets loading on detail job', () => {
+ mutations[types.REQUEST_JOB_TRACE](mockedState);
+
+ expect(mockedState.detailJob.isLoading).toBe(true);
+ });
+ });
+
+ describe(types.RECEIVE_JOB_TRACE_ERROR, () => {
+ beforeEach(() => {
+ mockedState.detailJob = { ...jobs[0], isLoading: true };
+ });
+
+ it('sets loading to false on detail job', () => {
+ mutations[types.RECEIVE_JOB_TRACE_ERROR](mockedState);
+
+ expect(mockedState.detailJob.isLoading).toBe(false);
+ });
+ });
+
+ describe(types.RECEIVE_JOB_TRACE_SUCCESS, () => {
+ beforeEach(() => {
+ mockedState.detailJob = { ...jobs[0], isLoading: true };
+ });
+
+ it('sets output on detail job', () => {
+ mutations[types.RECEIVE_JOB_TRACE_SUCCESS](mockedState, { html: 'html' });
+
+ expect(mockedState.detailJob.output).toBe('html');
+ expect(mockedState.detailJob.isLoading).toBe(false);
});
});
});
diff --git a/spec/javascripts/ide/stores/mutations/file_spec.js b/spec/javascripts/ide/stores/mutations/file_spec.js
index e83961fcedc..52f83be8e8c 100644
--- a/spec/javascripts/ide/stores/mutations/file_spec.js
+++ b/spec/javascripts/ide/stores/mutations/file_spec.js
@@ -152,6 +152,53 @@ describe('IDE store file mutations', () => {
expect(localFile.mrChange.diff).toBe('ABC');
});
+
+ it('has diffMode replaced by default', () => {
+ mutations.SET_FILE_MERGE_REQUEST_CHANGE(localState, {
+ file: localFile,
+ mrChange: {
+ diff: 'ABC',
+ },
+ });
+
+ expect(localFile.mrChange.diffMode).toBe('replaced');
+ });
+
+ it('has diffMode new', () => {
+ mutations.SET_FILE_MERGE_REQUEST_CHANGE(localState, {
+ file: localFile,
+ mrChange: {
+ diff: 'ABC',
+ new_file: true,
+ },
+ });
+
+ expect(localFile.mrChange.diffMode).toBe('new');
+ });
+
+ it('has diffMode deleted', () => {
+ mutations.SET_FILE_MERGE_REQUEST_CHANGE(localState, {
+ file: localFile,
+ mrChange: {
+ diff: 'ABC',
+ deleted_file: true,
+ },
+ });
+
+ expect(localFile.mrChange.diffMode).toBe('deleted');
+ });
+
+ it('has diffMode renamed', () => {
+ mutations.SET_FILE_MERGE_REQUEST_CHANGE(localState, {
+ file: localFile,
+ mrChange: {
+ diff: 'ABC',
+ renamed_file: true,
+ },
+ });
+
+ expect(localFile.mrChange.diffMode).toBe('renamed');
+ });
});
describe('DISCARD_FILE_CHANGES', () => {
diff --git a/spec/javascripts/ide/stores/mutations_spec.js b/spec/javascripts/ide/stores/mutations_spec.js
index 972713c5ad2..98016f593aa 100644
--- a/spec/javascripts/ide/stores/mutations_spec.js
+++ b/spec/javascripts/ide/stores/mutations_spec.js
@@ -148,4 +148,12 @@ describe('Multi-file store mutations', () => {
expect(localState.unusedSeal).toBe(false);
});
});
+
+ describe('SET_ERROR_MESSAGE', () => {
+ it('updates error message', () => {
+ mutations.SET_ERROR_MESSAGE(localState, 'error');
+
+ expect(localState.errorMessage).toBe('error');
+ });
+ });
});
diff --git a/spec/javascripts/ide/stores/utils_spec.js b/spec/javascripts/ide/stores/utils_spec.js
index f38ac6dd82f..6c5980cfae4 100644
--- a/spec/javascripts/ide/stores/utils_spec.js
+++ b/spec/javascripts/ide/stores/utils_spec.js
@@ -1,4 +1,5 @@
import * as utils from '~/ide/stores/utils';
+import { file } from '../helpers';
describe('Multi-file store utils', () => {
describe('setPageTitle', () => {
@@ -63,4 +64,113 @@ describe('Multi-file store utils', () => {
expect(foundEntry).toBeUndefined();
});
});
+
+ describe('createCommitPayload', () => {
+ it('returns API payload', () => {
+ const state = {
+ commitMessage: 'commit message',
+ };
+ const rootState = {
+ stagedFiles: [
+ {
+ ...file('staged'),
+ path: 'staged',
+ content: 'updated file content',
+ lastCommitSha: '123456789',
+ },
+ {
+ ...file('newFile'),
+ path: 'added',
+ tempFile: true,
+ content: 'new file content',
+ base64: true,
+ lastCommitSha: '123456789',
+ },
+ ],
+ currentBranchId: 'master',
+ };
+ const payload = utils.createCommitPayload({
+ branch: 'master',
+ newBranch: false,
+ state,
+ rootState,
+ getters: {},
+ });
+
+ expect(payload).toEqual({
+ branch: 'master',
+ commit_message: 'commit message',
+ actions: [
+ {
+ action: 'update',
+ file_path: 'staged',
+ content: 'updated file content',
+ encoding: 'text',
+ last_commit_id: '123456789',
+ },
+ {
+ action: 'create',
+ file_path: 'added',
+ content: 'new file content',
+ encoding: 'base64',
+ last_commit_id: '123456789',
+ },
+ ],
+ start_branch: undefined,
+ });
+ });
+
+ it('uses prebuilt commit message when commit message is empty', () => {
+ const rootState = {
+ stagedFiles: [
+ {
+ ...file('staged'),
+ path: 'staged',
+ content: 'updated file content',
+ lastCommitSha: '123456789',
+ },
+ {
+ ...file('newFile'),
+ path: 'added',
+ tempFile: true,
+ content: 'new file content',
+ base64: true,
+ lastCommitSha: '123456789',
+ },
+ ],
+ currentBranchId: 'master',
+ };
+ const payload = utils.createCommitPayload({
+ branch: 'master',
+ newBranch: false,
+ state: {},
+ rootState,
+ getters: {
+ preBuiltCommitMessage: 'prebuilt test commit message',
+ },
+ });
+
+ expect(payload).toEqual({
+ branch: 'master',
+ commit_message: 'prebuilt test commit message',
+ actions: [
+ {
+ action: 'update',
+ file_path: 'staged',
+ content: 'updated file content',
+ encoding: 'text',
+ last_commit_id: '123456789',
+ },
+ {
+ action: 'create',
+ file_path: 'added',
+ content: 'new file content',
+ encoding: 'base64',
+ last_commit_id: '123456789',
+ },
+ ],
+ start_branch: undefined,
+ });
+ });
+ });
});
diff --git a/spec/javascripts/importer_status_spec.js b/spec/javascripts/importer_status_spec.js
index 0575d02886d..63cdb3d5114 100644
--- a/spec/javascripts/importer_status_spec.js
+++ b/spec/javascripts/importer_status_spec.js
@@ -45,7 +45,25 @@ describe('Importer Status', () => {
currentTarget: document.querySelector('.js-add-to-import'),
})
.then(() => {
- expect(document.querySelector('tr').classList.contains('active')).toEqual(true);
+ expect(document.querySelector('tr').classList.contains('table-active')).toEqual(true);
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('shows error message after failed POST request', (done) => {
+ appendSetFixtures('<div class="flash-container"></div>');
+
+ mock.onPost(importUrl).reply(422, {
+ errors: 'You forgot your lunch',
+ });
+
+ instance.addToImport({
+ currentTarget: document.querySelector('.js-add-to-import'),
+ })
+ .then(() => {
+ const flashMessage = document.querySelector('.flash-text');
+ expect(flashMessage.textContent.trim()).toEqual('An error occurred while importing project: You forgot your lunch');
done();
})
.catch(done.fail);
diff --git a/spec/javascripts/integrations/integration_settings_form_spec.js b/spec/javascripts/integrations/integration_settings_form_spec.js
index 050b1f2074e..e07343810d2 100644
--- a/spec/javascripts/integrations/integration_settings_form_spec.js
+++ b/spec/javascripts/integrations/integration_settings_form_spec.js
@@ -143,6 +143,7 @@ describe('IntegrationSettingsForm', () => {
error: true,
message: errorMessage,
service_response: 'some error',
+ test_failed: true,
});
integrationSettingsForm.testSettings(formData)
@@ -157,6 +158,27 @@ describe('IntegrationSettingsForm', () => {
.catch(done.fail);
});
+ it('should not show error Flash with `Save anyway` action if ajax request responds with error in validation', (done) => {
+ const errorMessage = 'Validations failed.';
+ mock.onPut(integrationSettingsForm.testEndPoint).reply(200, {
+ error: true,
+ message: errorMessage,
+ service_response: 'some error',
+ test_failed: false,
+ });
+
+ integrationSettingsForm.testSettings(formData)
+ .then(() => {
+ const $flashContainer = $('.flash-container');
+ expect($flashContainer.find('.flash-text').text().trim()).toEqual('Validations failed. some error');
+ expect($flashContainer.find('.flash-action')).toBeDefined();
+ expect($flashContainer.find('.flash-action').text().trim()).toEqual('');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
it('should submit form if ajax request responds without any error in test', (done) => {
spyOn(integrationSettingsForm.$form, 'submit');
@@ -180,6 +202,7 @@ describe('IntegrationSettingsForm', () => {
mock.onPut(integrationSettingsForm.testEndPoint).reply(200, {
error: true,
message: errorMessage,
+ test_failed: true,
});
integrationSettingsForm.testSettings(formData)
diff --git a/spec/javascripts/issuable_time_tracker_spec.js b/spec/javascripts/issuable_time_tracker_spec.js
index ba9040524b1..5add150f874 100644
--- a/spec/javascripts/issuable_time_tracker_spec.js
+++ b/spec/javascripts/issuable_time_tracker_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable no-unused-vars, space-before-function-paren, func-call-spacing, no-spaced-func, semi, max-len, quotes, space-infix-ops, padded-blocks */
+/* eslint-disable no-unused-vars, func-call-spacing, no-spaced-func, semi, quotes, space-infix-ops, max-len */
import $ from 'jquery';
import Vue from 'vue';
diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js
index bf1f0c822fe..eb5e0bddb74 100644
--- a/spec/javascripts/issue_show/components/app_spec.js
+++ b/spec/javascripts/issue_show/components/app_spec.js
@@ -145,7 +145,7 @@ describe('Issuable output', () => {
resolve({
data: {
confidential: false,
- web_url: location.pathname,
+ web_url: window.location.pathname,
},
});
}));
@@ -177,7 +177,7 @@ describe('Issuable output', () => {
spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => {
resolve({
data: {
- web_url: location.pathname,
+ web_url: window.location.pathname,
confidential: vm.isConfidential,
},
});
diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js
index 047ecab27db..e12419b835d 100644
--- a/spec/javascripts/issue_spec.js
+++ b/spec/javascripts/issue_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable space-before-function-paren, one-var, one-var-declaration-per-line, no-use-before-define, comma-dangle, max-len */
+/* eslint-disable one-var, one-var-declaration-per-line, no-use-before-define, comma-dangle */
import $ from 'jquery';
import MockAdapter from 'axios-mock-adapter';
diff --git a/spec/javascripts/job_spec.js b/spec/javascripts/job_spec.js
index da00b615c9b..2fcb5566ebc 100644
--- a/spec/javascripts/job_spec.js
+++ b/spec/javascripts/job_spec.js
@@ -5,6 +5,7 @@ import { numberToHumanSize } from '~/lib/utils/number_utils';
import '~/lib/utils/datetime_utility';
import Job from '~/job';
import '~/breakpoints';
+import waitForPromises from 'spec/helpers/wait_for_promises';
describe('Job', () => {
const JOB_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1`;
@@ -12,10 +13,6 @@ describe('Job', () => {
let response;
let job;
- function waitForPromise() {
- return new Promise(resolve => requestAnimationFrame(resolve));
- }
-
preloadFixtures('builds/build-with-artifacts.html.raw');
beforeEach(() => {
@@ -49,7 +46,7 @@ describe('Job', () => {
beforeEach(function (done) {
job = new Job();
- waitForPromise()
+ waitForPromises()
.then(done)
.catch(done.fail);
});
@@ -93,7 +90,7 @@ describe('Job', () => {
job = new Job();
- waitForPromise()
+ waitForPromises()
.then(() => {
expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
expect(job.state).toBe('newstate');
@@ -107,7 +104,7 @@ describe('Job', () => {
};
})
.then(() => jasmine.clock().tick(4001))
- .then(waitForPromise)
+ .then(waitForPromises)
.then(() => {
expect($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/);
expect(job.state).toBe('finalstate');
@@ -126,7 +123,7 @@ describe('Job', () => {
job = new Job();
- waitForPromise()
+ waitForPromises()
.then(() => {
expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
@@ -137,7 +134,7 @@ describe('Job', () => {
};
})
.then(() => jasmine.clock().tick(4001))
- .then(waitForPromise)
+ .then(waitForPromises)
.then(() => {
expect($('#build-trace .js-build-output').text()).not.toMatch(/Update/);
expect($('#build-trace .js-build-output').text()).toMatch(/Different/);
@@ -160,7 +157,7 @@ describe('Job', () => {
job = new Job();
- waitForPromise()
+ waitForPromises()
.then(() => {
expect(document.querySelector('.js-truncated-info').classList).not.toContain('hidden');
})
@@ -181,7 +178,7 @@ describe('Job', () => {
job = new Job();
- waitForPromise()
+ waitForPromises()
.then(() => {
expect(
document.querySelector('.js-truncated-info-size').textContent.trim(),
@@ -203,7 +200,7 @@ describe('Job', () => {
job = new Job();
- waitForPromise()
+ waitForPromises()
.then(() => {
expect(
document.querySelector('.js-truncated-info-size').textContent.trim(),
@@ -219,7 +216,7 @@ describe('Job', () => {
};
})
.then(() => jasmine.clock().tick(4001))
- .then(waitForPromise)
+ .then(waitForPromises)
.then(() => {
expect(
document.querySelector('.js-truncated-info-size').textContent.trim(),
@@ -258,7 +255,7 @@ describe('Job', () => {
job = new Job();
- waitForPromise()
+ waitForPromises()
.then(() => {
expect(document.querySelector('.js-truncated-info').classList).toContain('hidden');
})
@@ -280,7 +277,7 @@ describe('Job', () => {
job = new Job();
- waitForPromise()
+ waitForPromises()
.then(done)
.catch(done.fail);
});
@@ -304,7 +301,6 @@ describe('Job', () => {
describe('getBuildTrace', () => {
it('should request build trace with state parameter', (done) => {
spyOn(axios, 'get').and.callThrough();
- // eslint-disable-next-line no-new
job = new Job();
setTimeout(() => {
diff --git a/spec/javascripts/jobs/header_spec.js b/spec/javascripts/jobs/header_spec.js
index 4f861c39d3f..cef30a007db 100644
--- a/spec/javascripts/jobs/header_spec.js
+++ b/spec/javascripts/jobs/header_spec.js
@@ -13,6 +13,9 @@ describe('Job details header', () => {
const threeWeeksAgo = new Date();
threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21);
+ const twoDaysAgo = new Date();
+ twoDaysAgo.setDate(twoDaysAgo.getDate() - 2);
+
props = {
job: {
status: {
@@ -31,7 +34,7 @@ describe('Job details header', () => {
email: 'foo@bar.com',
avatar_url: 'link',
},
- started: '2018-01-08T09:48:27.319Z',
+ started: twoDaysAgo.toISOString(),
new_issue_path: 'path',
},
isLoading: false,
@@ -69,7 +72,7 @@ describe('Job details header', () => {
.querySelector('.header-main-content')
.textContent.replace(/\s+/g, ' ')
.trim(),
- ).toEqual('failed Job #123 triggered 3 weeks ago by Foo');
+ ).toEqual('failed Job #123 triggered 2 days ago by Foo');
});
it('should render new issue link', () => {
diff --git a/spec/javascripts/jobs/mock_data.js b/spec/javascripts/jobs/mock_data.js
index 25ca8eb6c0b..dd025255bd1 100644
--- a/spec/javascripts/jobs/mock_data.js
+++ b/spec/javascripts/jobs/mock_data.js
@@ -20,7 +20,7 @@ export default {
group: 'success',
has_details: true,
details_path: '/root/ci-mock/-/jobs/4757',
- favicon: '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
+ favicon: '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
action: {
icon: 'retry',
title: 'Retry',
@@ -78,7 +78,7 @@ export default {
group: 'success',
has_details: true,
details_path: '/root/ci-mock/pipelines/140',
- favicon: '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
+ favicon: '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
},
duration: 6,
finished_at: '2017-06-01T17:32:00.042Z',
diff --git a/spec/javascripts/labels_select_spec.js b/spec/javascripts/labels_select_spec.js
index a2b89c0aef5..386e00bfd0c 100644
--- a/spec/javascripts/labels_select_spec.js
+++ b/spec/javascripts/labels_select_spec.js
@@ -40,5 +40,9 @@ describe('LabelsSelect', () => {
it('generated label item template has correct label styles', () => {
expect($labelEl.find('span.label').attr('style')).toBe(`background-color: ${label.color}; color: ${label.text_color};`);
});
+
+ it('generated label item has a badge class', () => {
+ expect($labelEl.find('span').hasClass('badge')).toEqual(true);
+ });
});
});
diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js
index 27f06573432..41ff59949e5 100644
--- a/spec/javascripts/lib/utils/common_utils_spec.js
+++ b/spec/javascripts/lib/utils/common_utils_spec.js
@@ -2,6 +2,7 @@
import axios from '~/lib/utils/axios_utils';
import * as commonUtils from '~/lib/utils/common_utils';
import MockAdapter from 'axios-mock-adapter';
+import { faviconDataUrl, overlayDataUrl, faviconWithOverlayDataUrl } from './mock_data';
describe('common_utils', () => {
describe('parseUrl', () => {
@@ -39,13 +40,13 @@ describe('common_utils', () => {
});
it('should decode params', () => {
- history.pushState('', '', '?label_name%5B%5D=test');
+ window.history.pushState('', '', '?label_name%5B%5D=test');
expect(
commonUtils.getUrlParamsArray()[0],
).toBe('label_name[]=test');
- history.pushState('', '', '?');
+ window.history.pushState('', '', '?');
});
});
@@ -395,6 +396,7 @@ describe('common_utils', () => {
const favicon = document.createElement('link');
favicon.setAttribute('id', 'favicon');
favicon.setAttribute('href', 'default/favicon');
+ favicon.setAttribute('data-default-href', 'default/favicon');
document.body.appendChild(favicon);
});
@@ -413,7 +415,7 @@ describe('common_utils', () => {
beforeEach(() => {
const favicon = document.createElement('link');
favicon.setAttribute('id', 'favicon');
- favicon.setAttribute('href', 'default/favicon');
+ favicon.setAttribute('data-original-href', 'default/favicon');
document.body.appendChild(favicon);
});
@@ -421,12 +423,43 @@ describe('common_utils', () => {
document.body.removeChild(document.getElementById('favicon'));
});
- it('should reset page favicon to tanuki', () => {
+ it('should reset page favicon to the default icon', () => {
+ const favicon = document.getElementById('favicon');
+ favicon.setAttribute('href', 'new/favicon');
commonUtils.resetFavicon();
expect(document.getElementById('favicon').getAttribute('href')).toEqual('default/favicon');
});
});
+ describe('createOverlayIcon', () => {
+ it('should return the favicon with the overlay', (done) => {
+ commonUtils.createOverlayIcon(faviconDataUrl, overlayDataUrl).then((url) => {
+ expect(url).toEqual(faviconWithOverlayDataUrl);
+ done();
+ });
+ });
+ });
+
+ describe('setFaviconOverlay', () => {
+ beforeEach(() => {
+ const favicon = document.createElement('link');
+ favicon.setAttribute('id', 'favicon');
+ favicon.setAttribute('data-original-href', faviconDataUrl);
+ document.body.appendChild(favicon);
+ });
+
+ afterEach(() => {
+ document.body.removeChild(document.getElementById('favicon'));
+ });
+
+ it('should set page favicon to provided favicon overlay', (done) => {
+ commonUtils.setFaviconOverlay(overlayDataUrl).then(() => {
+ expect(document.getElementById('favicon').getAttribute('href')).toEqual(faviconWithOverlayDataUrl);
+ done();
+ });
+ });
+ });
+
describe('setCiStatusFavicon', () => {
const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1/status.json`;
let mock;
@@ -434,6 +467,8 @@ describe('common_utils', () => {
beforeEach(() => {
const favicon = document.createElement('link');
favicon.setAttribute('id', 'favicon');
+ favicon.setAttribute('href', 'null');
+ favicon.setAttribute('data-original-href', faviconDataUrl);
document.body.appendChild(favicon);
mock = new MockAdapter(axios);
});
@@ -449,7 +484,7 @@ describe('common_utils', () => {
commonUtils.setCiStatusFavicon(BUILD_URL)
.then(() => {
const favicon = document.getElementById('favicon');
- expect(favicon.getAttribute('href')).toEqual('null');
+ expect(favicon.getAttribute('href')).toEqual(faviconDataUrl);
done();
})
// Error is already caught in catch() block of setCiStatusFavicon,
@@ -458,16 +493,14 @@ describe('common_utils', () => {
});
it('should set page favicon to CI status favicon based on provided status', (done) => {
- const FAVICON_PATH = '//icon_status_success';
-
mock.onGet(BUILD_URL).reply(200, {
- favicon: FAVICON_PATH,
+ favicon: overlayDataUrl,
});
commonUtils.setCiStatusFavicon(BUILD_URL)
.then(() => {
const favicon = document.getElementById('favicon');
- expect(favicon.getAttribute('href')).toEqual(FAVICON_PATH);
+ expect(favicon.getAttribute('href')).toEqual(faviconWithOverlayDataUrl);
done();
})
.catch(done.fail);
@@ -523,5 +556,75 @@ describe('common_utils', () => {
expect(Object.keys(commonUtils.convertObjectPropsToCamelCase()).length).toBe(0);
expect(Object.keys(commonUtils.convertObjectPropsToCamelCase({})).length).toBe(0);
});
+
+ it('does not deep-convert by default', () => {
+ const obj = {
+ snake_key: {
+ child_snake_key: 'value',
+ },
+ };
+
+ expect(
+ commonUtils.convertObjectPropsToCamelCase(obj),
+ ).toEqual({
+ snakeKey: {
+ child_snake_key: 'value',
+ },
+ });
+ });
+
+ describe('deep: true', () => {
+ it('converts object with child objects', () => {
+ const obj = {
+ snake_key: {
+ child_snake_key: 'value',
+ },
+ };
+
+ expect(
+ commonUtils.convertObjectPropsToCamelCase(obj, { deep: true }),
+ ).toEqual({
+ snakeKey: {
+ childSnakeKey: 'value',
+ },
+ });
+ });
+
+ it('converts array with child objects', () => {
+ const arr = [
+ {
+ child_snake_key: 'value',
+ },
+ ];
+
+ expect(
+ commonUtils.convertObjectPropsToCamelCase(arr, { deep: true }),
+ ).toEqual([
+ {
+ childSnakeKey: 'value',
+ },
+ ]);
+ });
+
+ it('converts array with child arrays', () => {
+ const arr = [
+ [
+ {
+ child_snake_key: 'value',
+ },
+ ],
+ ];
+
+ expect(
+ commonUtils.convertObjectPropsToCamelCase(arr, { deep: true }),
+ ).toEqual([
+ [
+ {
+ childSnakeKey: 'value',
+ },
+ ],
+ ]);
+ });
+ });
});
});
diff --git a/spec/javascripts/lib/utils/mock_data.js b/spec/javascripts/lib/utils/mock_data.js
new file mode 100644
index 00000000000..fd0d62b751f
--- /dev/null
+++ b/spec/javascripts/lib/utils/mock_data.js
@@ -0,0 +1,5 @@
+export const faviconDataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAACcFBMVEX////iQyniQyniQyniQyniQyniQyniQyniQynhRiriQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniRCniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQynhQiniQiniQiniQinhQinpUSjqUSjqTyjqTyjqTyjlSCniRCniQynjRCjqTyjsZSjrWyj8oib9kSb8pyb9pib8oyb8fyb3ZSb4Zib8fCb8oyb8oyb8oyb8pCb8cSbiQyn7bCb8cib8oyb8oSb8bSbtVSjpTij8nyb8oyb8oyb8lCb2Yyf3ZCf8mCb8oyb8oyb8oyb8iib8bSbiRCn8gyb8oyb8eCbpTinrUSj8oyb8oyb8oyb8pSb8bib4Zif0YCf8byb8oyb8oyb8oyb7oib8oyb8nCbjRSn9bib8ayb8nib8oyb8oyb8oyb8kSbpTyjpTyj8jib8oyb8oyb8oyb8fib0Xyf2ZSb8gCb8oyb6pSb8oyb8dib+cCbgQCnjRSn8cCb8oib8oyb8oyb8oybqUCjnSyn8bCb8oyb8oyb8oyb8myb2YyfyXyf8oyb8oyb8hibhQSn+bib8iSb8oyb8qCb+fSbmSSnqTyj8oib9pCb1YifxXyf7pSb8oCb8pCb+mCb0fCf8pSb7hSXvcSjiQyniQinqTyj9kCb9bib9byb+cCbqUSjiRCnsVCj+cSb8pib8bCb8bSbgQCn7bCb8bibjRSn8oyb8ayb8oib8aib8pCbjRCn8pybhQinhQSn8pSb7ayb7aSb6aib8eib///8IbM+7AAAAr3RSTlMBA3NtX2vT698HGQcRLwWLiXnv++3V+eEd/R8HE2V/Y5HjyefdFw99YWfJ+/3nwQP78/HvX1VTQ/kdA2HzbQXj9fX79/3DGf379/33T/v99/f7ba33+/f1+9/18/v59V339flzF/H9+fX3/fMhBwOh9/v5/fmvBV/z+fP3Awnp9/f38+UFgff7+/37+4c77/f7/flFz/f59dFr7/v98Wnr+/f3I5/197EDBU1ZAwUD8/kLUwAAAAFiS0dEAIgFHUgAAAAHdElNRQfhBQoLHiBV6/1lAAACHUlEQVQ4y41TZXsTQRCe4FAIUigN7m7FXY+iLRQKBG2x4g7BjhZ3Le7uMoEkFJprwyQk0CC/iZnNhUZaHt4vt6/szO7cHcD/wFKjZrJWq3YMq1M3eVc9rFzXR2yQkuA3RGxkjZLGiEk9miA2tURJs1RsnhhokYYtzaU13WZDbBVnW1sjo43J2vI6tZ0lLtFeAh1M0lECneI7dGYtrUtk3RUVIKaEJR25qw27yT0s3W0qEHuPlB4RradivXo7GX36xnbo51SQ+fWHARmCgYMGDxkaxbD3SssYPmIkwKgPLrfA87EETTg/fVaSa/SYsQDjSsd7DcGEsr+BieVKmaRNBsjUtClTfUI900y/5Mt05c8oJQKYSURZ2UqYFa0w283M588JEM2BuRwI5EqT8nmmXzZf4l8XsGNfCIv4QcHFklhiBpaqAsuC4tghj+ySyOdjeJYrP7RCCuR/E5tWAqxaLcmCNSyujdxjHZdbn8UHoA0bN/GoNm8hjQJb/ZzYpo6w3TB27JRduxxqrA7YzbWCezixN8RD2Oc2/Ptlfx7o5uT1A4XMiwzj4HfEikNe7+Ew0ZGjeuW70eEYaeHjxomTiKd++E4XnKGz8d+HDufOB3Ky3RcwdNF1qZiKLyf/B44r2tWf15wV143cwI2qfi8dbtKtX6Hbd+6G74EDqkTm/QcPH/0ufFyNLXjy9NnzF9Xb8BJevYY38C+8fZcg/AF3QTYemVkCwwAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNy0wNS0xMFQxMTozMDozMiswMjowMMzup8UAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTctMDUtMTBUMTE6MzA6MzIrMDI6MDC9sx95AAAAAElFTkSuQmCC';
+
+export const overlayDataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAA85JREFUWAntVllIVGEUPv/9b46O41KplYN7PeRkti8TjQlhCUGh3MmeQugpIsGKAi2soIcIooiohxYKK2daqDAlIpIiWwxtQaJcaHE0d5tMrbn37z9XRqfR0TvVW56Hudf//uec72zfEWBCJjIwkYGJDPzvGSD/KgExN3Oi2Q+2DJgSDYQEMwItVGH1iZGmJw/Si1y+/PwVAMYYib22MYc/8hVQFgKDEfYoId0KYzagAQebsos/ewMZoeB9wdffcTYpQSaCTWHKoqSQaDk7zkIt0+aCUR8BelEHrf3dUNv9AcqbnsHtT5UKB/hTASh0SLYjnjb/CIDRJi0XiFAaJOpCD8zLpdb4NB66b1OfelthX815dtdRRfiti2aAXLvVLiMQ6olGyztGDkSo4JGGXk8/QFdGpYzpHG2GBQTDhtgVhPEaVbbVpvI6GJz22rv4TcAfrYI1x7Rj5MWWAppomKFVVb2302SFzUkZHAbkG+0b1+Gh77yNYjrmqnWTrLBLRxdvBWv8qlFujH/kYjJYyvLkj71t78zAUvzMAMnHhpN4zf9UREJhd8omyssxu1IgazQDwDnHUcNuH6vhPIE1fmuBzHt74Hn7W89jWGtcAjoaIDOFrdcMYJBkgOCoaRF0Lj0oglddDbCj6tRvKjphEpgjkzEQs2YAKsNxMzjn3nKurhzK+Ly7xe28ua8TwgMMcHJZnvvT0BPtEEKM4tDJ+C8GvIIk4ylINIXVZ0EUKJxYuh3mhCeokbudl6TtVc88dfBdLwbyaWB6zQCYQJpBYSrDGQxBQ/ZWRM2B+VNmQnVnHWx7elyNuL2/R336co7KyJR8CL9oLgEuFlREevWUkEl6uGwpVEG4FBm0OEf9N10NMgPlvWYAuNVwsWDKvcUNYsHUWTCZ13ysyFEXe6TO6aC8CUr9IiK+A05TQrc8yjwmxARHeeMAPlfQJw+AQRwu0YhL/GDXi9NwufG+S8dYkuYMqIb4SsWthotlNMOUCOM6r+G9cqXxPmd1dqrBav/o1zJy2l5/NUjJA/VORwYuFnOUaTQcPs9wMqwV++Xv8oADxKAcZ8nLPr8AoGW+xR6HSqYk3GodAz2QNj0V+Gr26dT9ASNH5239Pf0gktVNWZca8ZvfAFBprWS6hSu1pqt++Y0PD+WIwDAhIWQGtzvSHDbcodfFUFB9hg1Gjs5LXqIdFL+acFBl+FddqYwdxsWC3I70OvgfUaA65zhq2O2c8VxYcyIGFTVlXegYtvCXANCQZJMobjVcLMjtSK/IcEgyOOe8Ve5w7ryKDefp2P3+C/5ohv8HZmVLAAAAAElFTkSuQmCC';
+
+export const faviconWithOverlayDataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAGt0lEQVRYR8WWf3DT5R3H35/vN0lDiCztSuiPAEnTFhSOUamIzFGokwMH55g0E845yjbP6+4qIoiHY6JjnHLeRI6h9jgQpQcD3ImH2u00eHBwjGthKLI1TSm26S8oKYVS0vT7/X52z7ckTUhqC/yx55/kks/zeb+ez6/nIWYm/B8XJQB4SGq6lL+CJA47vvRtvWs2D0nNl/Kf0qBZxx6u23arv0QAAIHivK8BynB4ffa7BgDQXJx/ngGnw+uThgTwP5ZnMocoJAxVxZDVZ+0L5n5WF75TkMafjLdJxpSg2E+gqW1X7zk3rbpaifhLiEBgTv4mEFbpBoTyu01DoDj/dQAv9rvjtdnp/k3Yx9rgAMV5QYCsAAwAgg6vL/1OTy/2BYrzzwLIBWACuNHhrXPG+otGoKaw0JA58kqGJtOFfgPS8yWT4sz88nzj7UIIfz+wd0mRdEZPLMnp2V/8R0+JrhLbBYFHJvwWzBUxYgqYNzhG+zfEhm24MIE5ectBtP0W+y0Or29FcoDifHFSRxwAcMrh9c0YrmisXaA4r0V0U8xvopgDDq9PpCQ+Ag0/zbEbNUNbMiG9fTwkDTsKHpJa2t1Zmiw1AqLg+tMZ+R6WVVtnZ2qP6Ib+FIjh05G3lsDrB4xjUIbRDeM+WZLJYZ4B1rKMzKPm/fdyzs9qg6WT225IMnPcuYjxbPZhn57qaA00zc4/QYT7b1b/wAZmDYSLjsN1WcmiM+6jXz7JTCs1aNPASBjrtrCGOXVBLK9ph72772bc0REZcsQlkEVoOxblhaFBH0Bxi6GBWFNC8gpV0XqYSe/hI85R9o1zxr/QaZbdbmuW9oRzljRrzBRkW9JhMaTgYugKzl35DlXNJ/Fp43FImoZnz7T0ln7bLihM0g85N627vkWPgLrbvYyCvAP1+rRIWETA5QsyQlcJYOCbMRasWpALtljwSsFyeJxFYsoNWqdN1y/ildM78Y/WGjxx8TL+ol3oluy8VupKe7cfoNLdCJkdqEUPOmBJ5ksJoae91mBps5lQ6pkIm20MPiz6A3KsmcNukDe/3Ye3zh3A77Q2XqcGjslLz88i/nB8pkpSoL8nAFSTBpUN4qSxS5KB5jOGUOniCebmzFQcevSN2xKP+Fp7ajt21f8TOxU/5i45JZFS6XwcTB9HxZgUnGTRNgk31x5jet+aGU7jWw+UweOcPeyTxxoqrGL25+UwdjehSvnmOVIqcz4C8y8GAABcQwjnYI5NheikhQWT+EZmDh2ev/l7cz4U2cGmYyg78TYqVH87Kbtd1wFY4hsVQAt14zu2RiDaTUZMf/BHWD35STx37wDv94k1dLeh7MRmvDZ1GR5Inxg17dX6MPnjZfh5X6tGSqXrV2B8ACIx98UNGOlV4CxCuA6zqIeq9FQ8c68bhx7ZiIK06CQdVF+Il3y1Hq03gnDfk4Uj8zbH2T51dCPOtlW39Q+iPTl2VSMfwKPiKw8aTuhgpl1Zdqxzj8PphRWwm21xZjv9VcgYkYb52dP132PFbSYr/la0DpNtrrg9a2oqsKfB2zlwG+4nSe1z7QDjaQBi2Eh6J4QRwimYt43LwOsuB2oX7YLVMCLqTAya3xx/EwZJxtYHy3WhyMkHExebXz3zAbbXfdo7AFBRaMAz1Ypa6XoaoPejKRGteZm6D3SlWVdOcOHo/Lfj2u9aXw+WHNmA00G/DiFEO0Jd+meyk0fIf/+vLfik6Xhj4qN0v7i5HCY1bBQPk+ij9GSzNbzYNdH03kMrscARfzvHQgiBocSFTVHVCrW+u+WrpK9iCIgS1rRK93oG/1GkRJVIup8KMNs1Sw/1rUtALD36ZzRca8XeJDmPtRc18vDn5SCJViYHENY3IZTK3JkE7RAYtpdkp3bAaJeOzN+CsSMTX+wqa7ih9sbVSLI2WV3znihAJYXZPThA7M6KQoM2MniyhUxTioxTpKLMadjx8Jqh5k3S//8d9GOh92XWmP/aXLKvfHgA0ZTklL0jj9m6UR6L5+9bjFWTPLcFIWbCY1+8pHb0drWybJ4aWLQrODyAWJndzoyylNyGg0hL+bV7Ll4rKIWB5CFBxMlLj21SL4W6QjDQjwOL9n4tNt0+AADPfo+UqgXPHJLSJrkso7F6ylLMy56OFMmYACIKblvtQext8Iqp0swyLYiI3zEAbs6Ml3cXv/p3Y+ryq5KcnSKb1Jmj75P7X0Rm/UV0tvO86r/WIhORwszvkmHEehH2WMo7ikDUQUWhoaIG+NNc96Os8eMEmklE2Qy2ANTO0OrA+CwFOFBfsq8pWZ7+B25aDBxvPp+QAAAAAElFTkSuQmCC';
diff --git a/spec/javascripts/lib/utils/text_utility_spec.js b/spec/javascripts/lib/utils/text_utility_spec.js
index eab5c24406a..33987574f00 100644
--- a/spec/javascripts/lib/utils/text_utility_spec.js
+++ b/spec/javascripts/lib/utils/text_utility_spec.js
@@ -96,4 +96,20 @@ describe('text_utility', () => {
expect(textUtils.convertToSentenceCase('Hello World')).toBe('Hello world');
});
});
+
+ describe('truncateSha', () => {
+ it('shortens SHAs to 8 characters', () => {
+ expect(textUtils.truncateSha('verylongsha')).toBe('verylong');
+ });
+
+ it('leaves short SHAs as is', () => {
+ expect(textUtils.truncateSha('shortsha')).toBe('shortsha');
+ });
+ });
+
+ describe('splitCamelCase', () => {
+ it('separates a PascalCase word to two', () => {
+ expect(textUtils.splitCamelCase('HelloWorld')).toBe('Hello World');
+ });
+ });
});
diff --git a/spec/javascripts/lib/utils/url_utility_spec.js b/spec/javascripts/lib/utils/url_utility_spec.js
new file mode 100644
index 00000000000..c7f4092911c
--- /dev/null
+++ b/spec/javascripts/lib/utils/url_utility_spec.js
@@ -0,0 +1,29 @@
+import { webIDEUrl } from '~/lib/utils/url_utility';
+
+describe('URL utility', () => {
+ describe('webIDEUrl', () => {
+ afterEach(() => {
+ gon.relative_url_root = '';
+ });
+
+ describe('without relative_url_root', () => {
+ it('returns IDE path with route', () => {
+ expect(webIDEUrl('/gitlab-org/gitlab-ce/merge_requests/1')).toBe(
+ '/-/ide/project/gitlab-org/gitlab-ce/merge_requests/1',
+ );
+ });
+ });
+
+ describe('with relative_url_root', () => {
+ beforeEach(() => {
+ gon.relative_url_root = '/gitlab';
+ });
+
+ it('returns IDE path with route', () => {
+ expect(webIDEUrl('/gitlab/gitlab-org/gitlab-ce/merge_requests/1')).toBe(
+ '/gitlab/-/ide/project/gitlab-org/gitlab-ce/merge_requests/1',
+ );
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js
index d2bdc9e160c..8cf0017f4d8 100644
--- a/spec/javascripts/line_highlighter_spec.js
+++ b/spec/javascripts/line_highlighter_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable space-before-function-paren, no-var, no-param-reassign, quotes, prefer-template, no-else-return, new-cap, dot-notation, no-return-assign, comma-dangle, no-new, one-var, one-var-declaration-per-line, jasmine/no-spec-dupes, no-underscore-dangle, max-len */
+/* eslint-disable no-var, quotes, prefer-template, no-else-return, dot-notation, no-return-assign, comma-dangle, no-new, one-var, one-var-declaration-per-line, no-underscore-dangle, max-len */
import $ from 'jquery';
import LineHighlighter from '~/line_highlighter';
diff --git a/spec/javascripts/matchers.js b/spec/javascripts/matchers.js
index 7cc5e753c22..0d465510fd3 100644
--- a/spec/javascripts/matchers.js
+++ b/spec/javascripts/matchers.js
@@ -1,4 +1,16 @@
export default {
+ toContainText: () => ({
+ compare(vm, text) {
+ if (!(vm.$el instanceof HTMLElement)) {
+ throw new Error('vm.$el is not a DOM element!');
+ }
+
+ const result = {
+ pass: vm.$el.innerText.includes(text),
+ };
+ return result;
+ },
+ }),
toHaveSpriteIcon: () => ({
compare(element, iconName) {
if (!iconName) {
@@ -10,7 +22,9 @@ export default {
}
const iconReferences = [].slice.apply(element.querySelectorAll('svg use'));
- const matchingIcon = iconReferences.find(reference => reference.getAttribute('xlink:href').endsWith(`#${iconName}`));
+ const matchingIcon = iconReferences.find(reference =>
+ reference.getAttribute('xlink:href').endsWith(`#${iconName}`),
+ );
const result = {
pass: !!matchingIcon,
};
@@ -20,7 +34,7 @@ export default {
} else {
result.message = `${element.outerHTML} does not contain the sprite icon "${iconName}"!`;
- const existingIcons = iconReferences.map((reference) => {
+ const existingIcons = iconReferences.map(reference => {
const iconUrl = reference.getAttribute('xlink:href');
return `"${iconUrl.replace(/^.+#/, '')}"`;
});
@@ -32,4 +46,12 @@ export default {
return result;
},
}),
+ toRender: () => ({
+ compare(vm) {
+ const result = {
+ pass: vm.$el.nodeType !== Node.COMMENT_NODE,
+ };
+ return result;
+ },
+ }),
};
diff --git a/spec/javascripts/merge_request_notes_spec.js b/spec/javascripts/merge_request_notes_spec.js
deleted file mode 100644
index dc9dc4d4249..00000000000
--- a/spec/javascripts/merge_request_notes_spec.js
+++ /dev/null
@@ -1,108 +0,0 @@
-import $ from 'jquery';
-import _ from 'underscore';
-import 'autosize';
-import '~/gl_form';
-import '~/lib/utils/text_utility';
-import '~/behaviors/markdown/render_gfm';
-import Notes from '~/notes';
-
-const upArrowKeyCode = 38;
-
-describe('Merge request notes', () => {
- window.gon = window.gon || {};
- window.gl = window.gl || {};
- gl.utils = gl.utils || {};
-
- const discussionTabFixture = 'merge_requests/diff_comment.html.raw';
- const changesTabJsonFixture = 'merge_request_diffs/inline_changes_tab_with_comments.json';
- preloadFixtures(discussionTabFixture, changesTabJsonFixture);
-
- describe('Discussion tab with diff comments', () => {
- beforeEach(() => {
- loadFixtures(discussionTabFixture);
- gl.utils.disableButtonIfEmptyField = _.noop;
- window.project_uploads_path = 'http://test.host/uploads';
- $('body').attr('data-page', 'projects:merge_requests:show');
- window.gon.current_user_id = $('.note:last').data('authorId');
-
- 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');
- upArrowEvent.which = upArrowKeyCode;
-
- spyOnEvent('.note:last .js-note-edit', 'click');
-
- $('.js-note-text').trigger(upArrowEvent);
-
- expect('click').toHaveBeenTriggeredOn('.note:last .js-note-edit');
- });
-
- it('edits last comment in discussion when triggered in discussion form', (done) => {
- const upArrowEvent = $.Event('keydown');
- upArrowEvent.which = upArrowKeyCode;
-
- spyOnEvent('.note-discussion .js-note-edit', 'click');
-
- $('.js-discussion-reply-button').click();
-
- setTimeout(() => {
- expect(
- $('.note-discussion .js-note-text'),
- ).toExist();
-
- $('.note-discussion .js-note-text').trigger(upArrowEvent);
-
- expect('click').toHaveBeenTriggeredOn('.note-discussion .js-note-edit');
-
- done();
- });
- });
- });
- });
-
- describe('Changes tab with diff comments', () => {
- beforeEach(() => {
- const diffsResponse = getJSONFixture(changesTabJsonFixture);
- const noteFormHtml = `<form class="js-new-note-form">
- <textarea class="js-note-text"></textarea>
- </form>`;
- setFixtures(diffsResponse.html + noteFormHtml);
- $('body').attr('data-page', 'projects:merge_requests:show');
- window.gon.current_user_id = $('.note:last').data('authorId');
-
- 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');
- upArrowEvent.which = upArrowKeyCode;
-
- spyOnEvent('.note:last .js-note-edit', 'click');
-
- $('.js-discussion-reply-button').trigger('click');
-
- setTimeout(() => {
- $('.js-note-text').trigger(upArrowEvent);
-
- expect('click').toHaveBeenTriggeredOn('.note:last .js-note-edit');
-
- done();
- });
- });
- });
- });
-});
diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js
index 74ceff76d37..7502f1fa2e1 100644
--- a/spec/javascripts/merge_request_spec.js
+++ b/spec/javascripts/merge_request_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable space-before-function-paren, no-return-assign */
+/* eslint-disable no-return-assign */
import $ from 'jquery';
import MockAdapter from 'axios-mock-adapter';
@@ -19,9 +19,11 @@ import IssuablesHelper from '~/helpers/issuables_helper';
spyOn(axios, 'patch').and.callThrough();
mock = new MockAdapter(axios);
- mock.onPatch(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`).reply(200, {});
+ mock
+ .onPatch(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`)
+ .reply(200, {});
- return this.merge = new MergeRequest();
+ return (this.merge = new MergeRequest());
});
afterEach(() => {
@@ -32,17 +34,22 @@ import IssuablesHelper from '~/helpers/issuables_helper';
spyOn($, 'ajax').and.stub();
const changeEvent = document.createEvent('HTMLEvents');
changeEvent.initEvent('change', true, true);
- $('input[type=checkbox]').attr('checked', true)[0].dispatchEvent(changeEvent);
+ $('input[type=checkbox]')
+ .attr('checked', true)[0]
+ .dispatchEvent(changeEvent);
return expect($('.js-task-list-field').val()).toBe('- [x] Task List Item');
});
- it('submits an ajax request on tasklist:changed', (done) => {
+ it('submits an ajax request on tasklist:changed', done => {
$('.js-task-list-field').trigger('tasklist:changed');
setTimeout(() => {
- expect(axios.patch).toHaveBeenCalledWith(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`, {
- merge_request: { description: '- [ ] Task List Item' },
- });
+ expect(axios.patch).toHaveBeenCalledWith(
+ `${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`,
+ {
+ merge_request: { description: '- [ ] Task List Item' },
+ },
+ );
done();
});
});
@@ -119,4 +126,4 @@ import IssuablesHelper from '~/helpers/issuables_helper';
});
});
});
-}).call(window);
+}.call(window));
diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js
index 3dbd9756cd2..7251ce19a90 100644
--- a/spec/javascripts/merge_request_tabs_spec.js
+++ b/spec/javascripts/merge_request_tabs_spec.js
@@ -1,5 +1,4 @@
-/* eslint-disable no-var, comma-dangle, object-shorthand */
-
+/* eslint-disable no-var, object-shorthand */
import $ from 'jquery';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
@@ -7,480 +6,229 @@ import MergeRequestTabs from '~/merge_request_tabs';
import '~/commit/pipelines/pipelines_bundle';
import '~/breakpoints';
import '~/lib/utils/common_utils';
-import Diff from '~/diff';
-import Notes from '~/notes';
import 'vendor/jquery.scrollTo';
-
-(function () {
- describe('MergeRequestTabs', function () {
- var stubLocation = {};
- var setLocation = function (stubs) {
- var defaults = {
- pathname: '',
- search: '',
- hash: ''
- };
- $.extend(stubLocation, defaults, stubs || {});
+import initMrPage from './helpers/init_vue_mr_page_helper';
+
+describe('MergeRequestTabs', function() {
+ let mrPageMock;
+ var stubLocation = {};
+ var setLocation = function(stubs) {
+ var defaults = {
+ pathname: '',
+ search: '',
+ hash: '',
};
+ $.extend(stubLocation, defaults, stubs || {});
+ };
- const inlineChangesTabJsonFixture = 'merge_request_diffs/inline_changes_tab_with_comments.json';
- const parallelChangesTabJsonFixture = 'merge_request_diffs/parallel_changes_tab_with_comments.json';
- preloadFixtures(
- 'merge_requests/merge_request_with_task_list.html.raw',
- 'merge_requests/diff_comment.html.raw',
- inlineChangesTabJsonFixture,
- parallelChangesTabJsonFixture
- );
-
- beforeEach(function () {
- this.class = new MergeRequestTabs({ stubLocation: stubLocation });
- setLocation();
-
- this.spies = {
- history: spyOn(window.history, 'replaceState').and.callFake(function () {})
- };
- });
-
- afterEach(function () {
- this.class.unbindEvents();
- this.class.destroyPipelinesView();
- });
-
- describe('activateTab', function () {
- beforeEach(function () {
- spyOn(axios, 'get').and.returnValue(Promise.resolve({ data: {} }));
- loadFixtures('merge_requests/merge_request_with_task_list.html.raw');
- this.subject = this.class.activateTab;
- });
- it('shows the notes tab when action is show', function () {
- this.subject('show');
- expect($('#notes')).toHaveClass('active');
- });
- it('shows the commits tab when action is commits', function () {
- this.subject('commits');
- expect($('#commits')).toHaveClass('active');
- });
- it('shows the diffs tab when action is diffs', function () {
- this.subject('diffs');
- expect($('#diffs')).toHaveClass('active');
- });
- });
-
- describe('opensInNewTab', function () {
- var tabUrl;
- var windowTarget = '_blank';
-
- beforeEach(function () {
- loadFixtures('merge_requests/merge_request_with_task_list.html.raw');
-
- tabUrl = $('.commits-tab a').attr('href');
+ preloadFixtures(
+ 'merge_requests/merge_request_with_task_list.html.raw',
+ 'merge_requests/diff_comment.html.raw',
+ );
- spyOn($.fn, 'attr').and.returnValue(tabUrl);
- });
-
- describe('meta click', () => {
- let metakeyEvent;
- beforeEach(function () {
- metakeyEvent = $.Event('click', { keyCode: 91, ctrlKey: true });
- });
+ beforeEach(function() {
+ mrPageMock = initMrPage();
+ this.class = new MergeRequestTabs({ stubLocation: stubLocation });
+ setLocation();
- it('opens page when commits link is clicked', function () {
- spyOn(window, 'open').and.callFake(function (url, name) {
- expect(url).toEqual(tabUrl);
- expect(name).toEqual(windowTarget);
- });
+ this.spies = {
+ history: spyOn(window.history, 'replaceState').and.callFake(function() {}),
+ };
+ });
- this.class.bindEvents();
- $('.merge-request-tabs .commits-tab a').trigger(metakeyEvent);
- });
+ afterEach(function() {
+ this.class.unbindEvents();
+ this.class.destroyPipelinesView();
+ mrPageMock.restore();
+ $('.js-merge-request-test').remove();
+ });
- it('opens page when commits badge is clicked', function () {
- spyOn(window, 'open').and.callFake(function (url, name) {
- expect(url).toEqual(tabUrl);
- expect(name).toEqual(windowTarget);
- });
+ describe('opensInNewTab', function() {
+ var tabUrl;
+ var windowTarget = '_blank';
- this.class.bindEvents();
- $('.merge-request-tabs .commits-tab a .badge').trigger(metakeyEvent);
- });
- });
+ beforeEach(function() {
+ loadFixtures('merge_requests/merge_request_with_task_list.html.raw');
- it('opens page tab in a new browser tab with Ctrl+Click - Windows/Linux', function () {
- spyOn(window, 'open').and.callFake(function (url, name) {
- expect(url).toEqual(tabUrl);
- expect(name).toEqual(windowTarget);
- });
+ tabUrl = $('.commits-tab a').attr('href');
+ });
- this.class.clickTab({
- metaKey: false,
- ctrlKey: true,
- which: 1,
- stopImmediatePropagation: function () {}
- });
+ describe('meta click', () => {
+ let metakeyEvent;
+ beforeEach(function() {
+ metakeyEvent = $.Event('click', { keyCode: 91, ctrlKey: true });
});
- it('opens page tab in a new browser tab with Cmd+Click - Mac', function () {
- spyOn(window, 'open').and.callFake(function (url, name) {
+ it('opens page when commits link is clicked', function() {
+ spyOn(window, 'open').and.callFake(function(url, name) {
expect(url).toEqual(tabUrl);
expect(name).toEqual(windowTarget);
});
- this.class.clickTab({
- metaKey: true,
- ctrlKey: false,
- which: 1,
- stopImmediatePropagation: function () {}
- });
+ this.class.bindEvents();
+ $('.merge-request-tabs .commits-tab a').trigger(metakeyEvent);
});
- it('opens page tab in a new browser tab with Middle-click - Mac/PC', function () {
- spyOn(window, 'open').and.callFake(function (url, name) {
+ it('opens page when commits badge is clicked', function() {
+ spyOn(window, 'open').and.callFake(function(url, name) {
expect(url).toEqual(tabUrl);
expect(name).toEqual(windowTarget);
});
- this.class.clickTab({
- metaKey: false,
- ctrlKey: false,
- which: 2,
- stopImmediatePropagation: function () {}
- });
+ this.class.bindEvents();
+ $('.merge-request-tabs .commits-tab a .badge').trigger(metakeyEvent);
});
});
- describe('setCurrentAction', function () {
- beforeEach(function () {
- spyOn(axios, 'get').and.returnValue(Promise.resolve({ data: {} }));
- this.subject = this.class.setCurrentAction;
- });
-
- it('changes from commits', function () {
- setLocation({
- pathname: '/foo/bar/merge_requests/1/commits'
- });
- expect(this.subject('show')).toBe('/foo/bar/merge_requests/1');
- expect(this.subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs');
- });
-
- it('changes from diffs', function () {
- setLocation({
- pathname: '/foo/bar/merge_requests/1/diffs'
- });
-
- expect(this.subject('show')).toBe('/foo/bar/merge_requests/1');
- expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits');
+ it('opens page tab in a new browser tab with Ctrl+Click - Windows/Linux', function() {
+ spyOn(window, 'open').and.callFake(function(url, name) {
+ expect(url).toEqual(tabUrl);
+ expect(name).toEqual(windowTarget);
});
- it('changes from diffs.html', function () {
- setLocation({
- pathname: '/foo/bar/merge_requests/1/diffs.html'
- });
- expect(this.subject('show')).toBe('/foo/bar/merge_requests/1');
- expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits');
+ this.class.clickTab({
+ metaKey: false,
+ ctrlKey: true,
+ which: 1,
+ stopImmediatePropagation: function() {},
});
+ });
- it('changes from notes', function () {
- setLocation({
- pathname: '/foo/bar/merge_requests/1'
- });
- expect(this.subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs');
- expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits');
+ it('opens page tab in a new browser tab with Cmd+Click - Mac', function() {
+ spyOn(window, 'open').and.callFake(function(url, name) {
+ expect(url).toEqual(tabUrl);
+ expect(name).toEqual(windowTarget);
});
- it('includes search parameters and hash string', function () {
- setLocation({
- pathname: '/foo/bar/merge_requests/1/diffs',
- search: '?view=parallel',
- hash: '#L15-35'
- });
- expect(this.subject('show')).toBe('/foo/bar/merge_requests/1?view=parallel#L15-35');
+ this.class.clickTab({
+ metaKey: true,
+ ctrlKey: false,
+ which: 1,
+ stopImmediatePropagation: function() {},
});
+ });
- it('replaces the current history state', function () {
- var newState;
- setLocation({
- pathname: '/foo/bar/merge_requests/1'
- });
- newState = this.subject('commits');
- expect(this.spies.history).toHaveBeenCalledWith({
- url: newState
- }, document.title, newState);
+ it('opens page tab in a new browser tab with Middle-click - Mac/PC', function() {
+ spyOn(window, 'open').and.callFake(function(url, name) {
+ expect(url).toEqual(tabUrl);
+ expect(name).toEqual(windowTarget);
});
- it('treats "show" like "notes"', function () {
- setLocation({
- pathname: '/foo/bar/merge_requests/1/commits'
- });
- expect(this.subject('show')).toBe('/foo/bar/merge_requests/1');
+ this.class.clickTab({
+ metaKey: false,
+ ctrlKey: false,
+ which: 2,
+ stopImmediatePropagation: function() {},
});
});
+ });
- describe('tabShown', () => {
- let mock;
+ describe('setCurrentAction', function() {
+ let mock;
- beforeEach(function () {
- mock = new MockAdapter(axios);
- mock.onGet(/(.*)\/diffs\.json/).reply(200, {
- data: { html: '' },
- });
+ beforeEach(function() {
+ mock = new MockAdapter(axios);
+ mock.onAny().reply({ data: {} });
+ this.subject = this.class.setCurrentAction;
+ });
- loadFixtures('merge_requests/merge_request_with_task_list.html.raw');
- });
+ afterEach(() => {
+ mock.restore();
+ });
- afterEach(() => {
- mock.restore();
+ it('changes from commits', function() {
+ setLocation({
+ pathname: '/foo/bar/merge_requests/1/commits',
});
- describe('with "Side-by-side"/parallel diff view', () => {
- beforeEach(function () {
- this.class.diffViewType = () => 'parallel';
- Diff.prototype.diffViewType = () => 'parallel';
- });
-
- it('maintains `container-limited` for pipelines tab', function (done) {
- const asyncClick = function (selector) {
- return new Promise((resolve) => {
- setTimeout(() => {
- document.querySelector(selector).click();
- resolve();
- });
- });
- };
- asyncClick('.merge-request-tabs .pipelines-tab a')
- .then(() => asyncClick('.merge-request-tabs .diffs-tab a'))
- .then(() => asyncClick('.merge-request-tabs .pipelines-tab a'))
- .then(() => {
- const hasContainerLimitedClass = document.querySelector('.content-wrapper .container-fluid').classList.contains('container-limited');
- expect(hasContainerLimitedClass).toBe(true);
- })
- .then(done)
- .catch((err) => {
- done.fail(`Something went wrong clicking MR tabs: ${err.message}\n${err.stack}`);
- });
- });
-
- it('maintains `container-limited` when switching from "Changes" tab before it loads', function (done) {
- const asyncClick = function (selector) {
- return new Promise((resolve) => {
- setTimeout(() => {
- document.querySelector(selector).click();
- resolve();
- });
- });
- };
-
- asyncClick('.merge-request-tabs .diffs-tab a')
- .then(() => asyncClick('.merge-request-tabs .notes-tab a'))
- .then(() => {
- const hasContainerLimitedClass = document.querySelector('.content-wrapper .container-fluid').classList.contains('container-limited');
- expect(hasContainerLimitedClass).toBe(true);
- })
- .then(done)
- .catch((err) => {
- done.fail(`Something went wrong clicking MR tabs: ${err.message}\n${err.stack}`);
- });
- });
- });
+ expect(this.subject('show')).toBe('/foo/bar/merge_requests/1');
+ expect(this.subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs');
});
- describe('loadDiff', function () {
- beforeEach(() => {
- loadFixtures('merge_requests/diff_comment.html.raw');
- $('body').attr('data-page', 'projects:merge_requests:show');
- window.gl.ImageFile = () => {};
- Notes.initialize('', []);
- spyOn(Notes.instance, 'toggleDiffNote').and.callThrough();
+ it('changes from diffs', function() {
+ setLocation({
+ pathname: '/foo/bar/merge_requests/1/diffs',
});
- afterEach(() => {
- delete window.gl.ImageFile;
- delete window.notes;
+ expect(this.subject('show')).toBe('/foo/bar/merge_requests/1');
+ expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits');
+ });
- // Undo what we did to the shared <body>
- $('body').removeAttr('data-page');
+ it('changes from diffs.html', function() {
+ setLocation({
+ pathname: '/foo/bar/merge_requests/1/diffs.html',
});
- it('triggers Ajax request to JSON endpoint', function (done) {
- const url = '/foo/bar/merge_requests/1/diffs';
-
- spyOn(axios, 'get').and.callFake((reqUrl) => {
- expect(reqUrl).toBe(`${url}.json`);
-
- done();
-
- return Promise.resolve({ data: {} });
- });
+ expect(this.subject('show')).toBe('/foo/bar/merge_requests/1');
+ expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits');
+ });
- this.class.loadDiff(url);
+ it('changes from notes', function() {
+ setLocation({
+ pathname: '/foo/bar/merge_requests/1',
});
- it('triggers scroll event when diff already loaded', function (done) {
- spyOn(axios, 'get').and.callFake(done.fail);
- spyOn(document, 'dispatchEvent');
-
- this.class.diffsLoaded = true;
- this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
+ expect(this.subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs');
+ expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits');
+ });
- expect(
- document.dispatchEvent,
- ).toHaveBeenCalledWith(new CustomEvent('scroll'));
- done();
+ it('includes search parameters and hash string', function() {
+ setLocation({
+ pathname: '/foo/bar/merge_requests/1/diffs',
+ search: '?view=parallel',
+ hash: '#L15-35',
});
- describe('with inline diff', () => {
- let noteId;
- let noteLineNumId;
- let mock;
-
- beforeEach(() => {
- const diffsResponse = getJSONFixture(inlineChangesTabJsonFixture);
-
- const $html = $(diffsResponse.html);
- noteId = $html.find('.note').attr('id');
- noteLineNumId = $html
- .find('.note')
- .closest('.notes_holder')
- .prev('.line_holder')
- .find('a[data-linenumber]')
- .attr('href')
- .replace('#', '');
-
- mock = new MockAdapter(axios);
- mock.onGet(/(.*)\/diffs\.json/).reply(200, diffsResponse);
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- describe('with note fragment hash', () => {
- it('should expand and scroll to linked fragment hash #note_xxx', function (done) {
- spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue(noteId);
- this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
-
- setTimeout(() => {
- expect(noteId.length).toBeGreaterThan(0);
- expect(Notes.instance.toggleDiffNote).toHaveBeenCalledWith({
- target: jasmine.any(Object),
- lineType: 'old',
- forceShow: true,
- });
-
- done();
- });
- });
-
- it('should gracefully ignore non-existant fragment hash', function (done) {
- spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue('note_something-that-does-not-exist');
- this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
-
- setTimeout(() => {
- expect(Notes.instance.toggleDiffNote).not.toHaveBeenCalled();
-
- done();
- });
- });
- });
-
- describe('with line number fragment hash', () => {
- it('should gracefully ignore line number fragment hash', function () {
- spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue(noteLineNumId);
- this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
+ expect(this.subject('show')).toBe('/foo/bar/merge_requests/1?view=parallel#L15-35');
+ });
- expect(noteLineNumId.length).toBeGreaterThan(0);
- expect(Notes.instance.toggleDiffNote).not.toHaveBeenCalled();
- });
- });
+ it('replaces the current history state', function() {
+ var newState;
+ setLocation({
+ pathname: '/foo/bar/merge_requests/1',
});
+ newState = this.subject('commits');
- describe('with parallel diff', () => {
- let noteId;
- let noteLineNumId;
- let mock;
-
- beforeEach(() => {
- const diffsResponse = getJSONFixture(parallelChangesTabJsonFixture);
-
- const $html = $(diffsResponse.html);
- noteId = $html.find('.note').attr('id');
- noteLineNumId = $html
- .find('.note')
- .closest('.notes_holder')
- .prev('.line_holder')
- .find('a[data-linenumber]')
- .attr('href')
- .replace('#', '');
-
- mock = new MockAdapter(axios);
- mock.onGet(/(.*)\/diffs\.json/).reply(200, diffsResponse);
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- describe('with note fragment hash', () => {
- it('should expand and scroll to linked fragment hash #note_xxx', function (done) {
- spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue(noteId);
-
- this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
-
- setTimeout(() => {
- expect(noteId.length).toBeGreaterThan(0);
- expect(Notes.instance.toggleDiffNote).toHaveBeenCalledWith({
- target: jasmine.any(Object),
- lineType: 'new',
- forceShow: true,
- });
-
- done();
- });
- });
-
- it('should gracefully ignore non-existant fragment hash', function (done) {
- spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue('note_something-that-does-not-exist');
- this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
-
- setTimeout(() => {
- expect(Notes.instance.toggleDiffNote).not.toHaveBeenCalled();
- done();
- });
- });
- });
-
- describe('with line number fragment hash', () => {
- it('should gracefully ignore line number fragment hash', function () {
- spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue(noteLineNumId);
- this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
+ expect(this.spies.history).toHaveBeenCalledWith(
+ {
+ url: newState,
+ },
+ document.title,
+ newState,
+ );
+ });
- expect(noteLineNumId.length).toBeGreaterThan(0);
- expect(Notes.instance.toggleDiffNote).not.toHaveBeenCalled();
- });
- });
+ it('treats "show" like "notes"', function() {
+ setLocation({
+ pathname: '/foo/bar/merge_requests/1/commits',
});
+
+ expect(this.subject('show')).toBe('/foo/bar/merge_requests/1');
});
+ });
- describe('expandViewContainer', function () {
- beforeEach(() => {
- $('body').append('<div class="content-wrapper"><div class="container-fluid container-limited"></div></div>');
- });
+ describe('expandViewContainer', function() {
+ beforeEach(() => {
+ $('body').append(
+ '<div class="content-wrapper"><div class="container-fluid container-limited"></div></div>',
+ );
+ });
- afterEach(() => {
- $('.content-wrapper').remove();
- });
+ afterEach(() => {
+ $('.content-wrapper').remove();
+ });
- it('removes container-limited from containers', function () {
- this.class.expandViewContainer();
+ it('removes container-limited from containers', function() {
+ this.class.expandViewContainer();
- expect($('.content-wrapper')).not.toContainElement('.container-limited');
- });
+ expect($('.content-wrapper')).not.toContainElement('.container-limited');
+ });
- it('does remove container-limited from breadcrumbs', function () {
- $('.container-limited').addClass('breadcrumbs');
- this.class.expandViewContainer();
+ it('does remove container-limited from breadcrumbs', function() {
+ $('.container-limited').addClass('breadcrumbs');
+ this.class.expandViewContainer();
- expect($('.content-wrapper')).toContainElement('.container-limited');
- });
+ expect($('.content-wrapper')).toContainElement('.container-limited');
});
});
-}).call(window);
+});
diff --git a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js
index 009b3fd75b7..1879424c629 100644
--- a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js
+++ b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js
@@ -1,5 +1,3 @@
-/* eslint-disable no-new */
-
import $ from 'jquery';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
diff --git a/spec/javascripts/monitoring/dashboard_spec.js b/spec/javascripts/monitoring/dashboard_spec.js
index eba6dcf47c5..997163c7602 100644
--- a/spec/javascripts/monitoring/dashboard_spec.js
+++ b/spec/javascripts/monitoring/dashboard_spec.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import Dashboard from '~/monitoring/components/dashboard.vue';
import axios from '~/lib/utils/axios_utils';
-import { metricsGroupsAPIResponse, mockApiEndpoint } from './mock_data';
+import { metricsGroupsAPIResponse, mockApiEndpoint, environmentData } from './mock_data';
describe('Dashboard', () => {
let DashboardComponent;
@@ -20,6 +20,8 @@ describe('Dashboard', () => {
emptyLoadingSvgPath: '/path/to/loading.svg',
emptyNoDataSvgPath: '/path/to/no-data.svg',
emptyUnableToConnectSvgPath: '/path/to/unable-to-connect.svg',
+ environmentsEndpoint: '/root/hello-prometheus/environments/35',
+ currentEnvironmentName: 'production',
};
beforeEach(() => {
@@ -50,7 +52,7 @@ describe('Dashboard', () => {
mock.restore();
});
- it('shows up a loading state', (done) => {
+ it('shows up a loading state', done => {
const component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
propsData: { ...propsData, hasMetrics: true },
@@ -62,7 +64,7 @@ describe('Dashboard', () => {
});
});
- it('hides the legend when showLegend is false', (done) => {
+ it('hides the legend when showLegend is false', done => {
const component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
propsData: { ...propsData, hasMetrics: true, showLegend: false },
@@ -76,7 +78,7 @@ describe('Dashboard', () => {
});
});
- it('hides the group panels when showPanels is false', (done) => {
+ it('hides the group panels when showPanels is false', done => {
const component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
propsData: { ...propsData, hasMetrics: true, showPanels: false },
@@ -89,5 +91,40 @@ describe('Dashboard', () => {
done();
});
});
+
+ it('renders the dropdown with a number of environments', done => {
+ const component = new DashboardComponent({
+ el: document.querySelector('.prometheus-graphs'),
+ propsData: { ...propsData, hasMetrics: true, showPanels: false },
+ });
+
+ component.store.storeEnvironmentsData(environmentData);
+
+ setTimeout(() => {
+ const dropdownMenuEnvironments = component.$el.querySelectorAll('.dropdown-menu ul li a');
+ expect(dropdownMenuEnvironments.length).toEqual(component.store.environmentsData.length);
+ done();
+ });
+ });
+
+ it('renders the dropdown with a single is-active element', done => {
+ const component = new DashboardComponent({
+ el: document.querySelector('.prometheus-graphs'),
+ propsData: { ...propsData, hasMetrics: true, showPanels: false },
+ });
+
+ component.store.storeEnvironmentsData(environmentData);
+
+ setTimeout(() => {
+ const dropdownIsActiveElement = component.$el.querySelectorAll(
+ '.dropdown-menu ul li a.is-active',
+ );
+ expect(dropdownIsActiveElement.length).toEqual(1);
+ expect(dropdownIsActiveElement[0].textContent.trim()).toEqual(
+ component.currentEnvironmentName,
+ );
+ done();
+ });
+ });
});
});
diff --git a/spec/javascripts/monitoring/graph_spec.js b/spec/javascripts/monitoring/graph_spec.js
index 220228e5c08..a46a387a534 100644
--- a/spec/javascripts/monitoring/graph_spec.js
+++ b/spec/javascripts/monitoring/graph_spec.js
@@ -18,9 +18,7 @@ const createComponent = propsData => {
}).$mount();
};
-const convertedMetrics = convertDatesMultipleSeries(
- singleRowMetricsMultipleSeries,
-);
+const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries);
describe('Graph', () => {
beforeEach(() => {
@@ -36,7 +34,7 @@ describe('Graph', () => {
projectPath,
});
- expect(component.$el.querySelector('.text-center').innerText.trim()).toBe(
+ expect(component.$el.querySelector('.prometheus-graph-title').innerText.trim()).toBe(
component.graphData.title,
);
});
@@ -52,9 +50,7 @@ describe('Graph', () => {
});
const transformedHeight = `${component.graphHeight - 100}`;
- expect(component.axisTransform.indexOf(transformedHeight)).not.toEqual(
- -1,
- );
+ expect(component.axisTransform.indexOf(transformedHeight)).not.toEqual(-1);
});
it('outerViewBox gets a width and height property based on the DOM size of the element', () => {
diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js
index 50da6da2e07..10808315372 100644
--- a/spec/javascripts/monitoring/mock_data.js
+++ b/spec/javascripts/monitoring/mock_data.js
@@ -1,5 +1,3 @@
-/* eslint-disable quote-props, indent, comma-dangle */
-
export const mockApiEndpoint = `${gl.TEST_HOST}/monitoring/mock`;
export const metricsGroupsAPIResponse = {
@@ -6544,3 +6542,44 @@ export function convertDatesMultipleSeries(multipleSeries) {
});
return convertedMultiple;
}
+
+export const environmentData = [
+ {
+ name: 'production',
+ size: 1,
+ latest: {
+ id: 34,
+ name: 'production',
+ state: 'available',
+ external_url: 'http://root-autodevops-deploy.my-fake-domain.com',
+ environment_type: null,
+ stop_action: false,
+ metrics_path: '/root/hello-prometheus/environments/34/metrics',
+ environment_path: '/root/hello-prometheus/environments/34',
+ stop_path: '/root/hello-prometheus/environments/34/stop',
+ terminal_path: '/root/hello-prometheus/environments/34/terminal',
+ folder_path: '/root/hello-prometheus/environments/folders/production',
+ created_at: '2018-06-29T16:53:38.301Z',
+ updated_at: '2018-06-29T16:57:09.825Z',
+ },
+ },
+ {
+ name: 'review',
+ size: 1,
+ latest: {
+ id: 35,
+ name: 'review/noop-branch',
+ state: 'available',
+ external_url: 'http://root-autodevops-deploy-review-noop-branc-die93w.my-fake-domain.com',
+ environment_type: 'review',
+ stop_action: true,
+ metrics_path: '/root/hello-prometheus/environments/35/metrics',
+ environment_path: '/root/hello-prometheus/environments/35',
+ stop_path: '/root/hello-prometheus/environments/35/stop',
+ terminal_path: '/root/hello-prometheus/environments/35/terminal',
+ folder_path: '/root/hello-prometheus/environments/folders/review',
+ created_at: '2018-07-03T18:39:41.702Z',
+ updated_at: '2018-07-03T18:44:54.010Z',
+ },
+ },
+];
diff --git a/spec/javascripts/namespace_select_spec.js b/spec/javascripts/namespace_select_spec.js
index 3b2641f7646..07b82ce721e 100644
--- a/spec/javascripts/namespace_select_spec.js
+++ b/spec/javascripts/namespace_select_spec.js
@@ -22,7 +22,7 @@ describe('NamespaceSelect', () => {
const dropdown = document.createElement('div');
// eslint-disable-next-line no-new
new NamespaceSelect({ dropdown });
- glDropdownOptions = $.fn.glDropdown.calls.argsFor(0)[0];
+ [glDropdownOptions] = $.fn.glDropdown.calls.argsFor(0);
});
it('prevents click events', () => {
@@ -43,7 +43,7 @@ describe('NamespaceSelect', () => {
dropdown.dataset.isFilter = 'true';
// eslint-disable-next-line no-new
new NamespaceSelect({ dropdown });
- glDropdownOptions = $.fn.glDropdown.calls.argsFor(0)[0];
+ [glDropdownOptions] = $.fn.glDropdown.calls.argsFor(0);
});
it('does not prevent click events', () => {
diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js
index 5e5d8f8f34f..122e5bc58b2 100644
--- a/spec/javascripts/new_branch_spec.js
+++ b/spec/javascripts/new_branch_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-return-assign, quotes, max-len */
+/* eslint-disable one-var, no-var, one-var-declaration-per-line, no-return-assign, quotes, max-len */
import $ from 'jquery';
import NewBranchForm from '~/new_branch_form';
diff --git a/spec/javascripts/notebook/cells/markdown_spec.js b/spec/javascripts/notebook/cells/markdown_spec.js
index 8f8ba231ae8..0b1b11de1fd 100644
--- a/spec/javascripts/notebook/cells/markdown_spec.js
+++ b/spec/javascripts/notebook/cells/markdown_spec.js
@@ -14,6 +14,7 @@ describe('Markdown component', () => {
beforeEach((done) => {
json = getJSONFixture('blob/notebook/basic.json');
+ // eslint-disable-next-line prefer-destructuring
cell = json.cells[1];
vm = new Component({
diff --git a/spec/javascripts/notes/components/comment_form_spec.js b/spec/javascripts/notes/components/comment_form_spec.js
index 224debbeff6..155c91dcc46 100644
--- a/spec/javascripts/notes/components/comment_form_spec.js
+++ b/spec/javascripts/notes/components/comment_form_spec.js
@@ -1,23 +1,27 @@
import $ from 'jquery';
import Vue from 'vue';
import Autosize from 'autosize';
-import store from '~/notes/stores';
+import createStore from '~/notes/stores';
import CommentForm from '~/notes/components/comment_form.vue';
+import * as constants from '~/notes/constants';
import { loggedOutnoteableData, notesDataMock, userDataMock, noteableDataMock } from '../mock_data';
import { keyboardDownEvent } from '../../issue_show/helpers';
describe('issue_comment_form component', () => {
+ let store;
let vm;
const Component = Vue.extend(CommentForm);
let mountComponent;
beforeEach(() => {
- mountComponent = (noteableType = 'issue') => new Component({
- propsData: {
- noteableType,
- },
- store,
- }).$mount();
+ store = createStore();
+ mountComponent = (noteableType = 'issue') =>
+ new Component({
+ propsData: {
+ noteableType,
+ },
+ store,
+ }).$mount();
});
afterEach(() => {
@@ -34,7 +38,9 @@ describe('issue_comment_form component', () => {
});
it('should render user avatar with link', () => {
- expect(vm.$el.querySelector('.timeline-icon .user-avatar-link').getAttribute('href')).toEqual(userDataMock.path);
+ expect(vm.$el.querySelector('.timeline-icon .user-avatar-link').getAttribute('href')).toEqual(
+ userDataMock.path,
+ );
});
describe('handleSave', () => {
@@ -60,7 +66,7 @@ describe('issue_comment_form component', () => {
expect(vm.toggleIssueState).toHaveBeenCalled();
});
- it('should disable action button whilst submitting', (done) => {
+ it('should disable action button whilst submitting', done => {
const saveNotePromise = Promise.resolve();
vm.note = 'hello world';
spyOn(vm, 'saveNote').and.returnValue(saveNotePromise);
@@ -84,19 +90,21 @@ describe('issue_comment_form component', () => {
it('should render textarea with placeholder', () => {
expect(
vm.$el.querySelector('.js-main-target-form textarea').getAttribute('placeholder'),
- ).toEqual('Write a comment or drag your files here...');
+ ).toEqual('Write a comment or drag your files here…');
});
- it('should make textarea disabled while requesting', (done) => {
+ it('should make textarea disabled while requesting', done => {
const $submitButton = $(vm.$el.querySelector('.js-comment-submit-button'));
vm.note = 'hello world';
spyOn(vm, 'stopPolling');
spyOn(vm, 'saveNote').and.returnValue(new Promise(() => {}));
- vm.$nextTick(() => { // Wait for vm.note change triggered. It should enable $submitButton.
+ vm.$nextTick(() => {
+ // Wait for vm.note change triggered. It should enable $submitButton.
$submitButton.trigger('click');
- vm.$nextTick(() => { // Wait for vm.isSubmitting triggered. It should disable textarea.
+ vm.$nextTick(() => {
+ // Wait for vm.isSubmitting triggered. It should disable textarea.
expect(vm.$el.querySelector('.js-main-target-form textarea').disabled).toBeTruthy();
done();
});
@@ -105,21 +113,27 @@ describe('issue_comment_form component', () => {
it('should support quick actions', () => {
expect(
- vm.$el.querySelector('.js-main-target-form textarea').getAttribute('data-supports-quick-actions'),
+ vm.$el
+ .querySelector('.js-main-target-form textarea')
+ .getAttribute('data-supports-quick-actions'),
).toEqual('true');
});
it('should link to markdown docs', () => {
const { markdownDocsPath } = notesDataMock;
- expect(vm.$el.querySelector(`a[href="${markdownDocsPath}"]`).textContent.trim()).toEqual('Markdown');
+ expect(vm.$el.querySelector(`a[href="${markdownDocsPath}"]`).textContent.trim()).toEqual(
+ 'Markdown',
+ );
});
it('should link to quick actions docs', () => {
const { quickActionsDocsPath } = notesDataMock;
- expect(vm.$el.querySelector(`a[href="${quickActionsDocsPath}"]`).textContent.trim()).toEqual('quick actions');
+ expect(
+ vm.$el.querySelector(`a[href="${quickActionsDocsPath}"]`).textContent.trim(),
+ ).toEqual('quick actions');
});
- it('should resize textarea after note discarded', (done) => {
+ it('should resize textarea after note discarded', done => {
spyOn(Autosize, 'update');
spyOn(vm, 'discard').and.callThrough();
@@ -136,7 +150,9 @@ describe('issue_comment_form component', () => {
it('should enter edit mode when arrow up is pressed', () => {
spyOn(vm, 'editCurrentUserLastNote').and.callThrough();
vm.$el.querySelector('.js-main-target-form textarea').value = 'Foo';
- vm.$el.querySelector('.js-main-target-form textarea').dispatchEvent(keyboardDownEvent(38, true));
+ vm.$el
+ .querySelector('.js-main-target-form textarea')
+ .dispatchEvent(keyboardDownEvent(38, true));
expect(vm.editCurrentUserLastNote).toHaveBeenCalled();
});
@@ -151,7 +167,9 @@ describe('issue_comment_form component', () => {
it('should save note when cmd+enter is pressed', () => {
spyOn(vm, 'handleSave').and.callThrough();
vm.$el.querySelector('.js-main-target-form textarea').value = 'Foo';
- vm.$el.querySelector('.js-main-target-form textarea').dispatchEvent(keyboardDownEvent(13, true));
+ vm.$el
+ .querySelector('.js-main-target-form textarea')
+ .dispatchEvent(keyboardDownEvent(13, true));
expect(vm.handleSave).toHaveBeenCalled();
});
@@ -159,7 +177,9 @@ describe('issue_comment_form component', () => {
it('should save note when ctrl+enter is pressed', () => {
spyOn(vm, 'handleSave').and.callThrough();
vm.$el.querySelector('.js-main-target-form textarea').value = 'Foo';
- vm.$el.querySelector('.js-main-target-form textarea').dispatchEvent(keyboardDownEvent(13, false, true));
+ vm.$el
+ .querySelector('.js-main-target-form textarea')
+ .dispatchEvent(keyboardDownEvent(13, false, true));
expect(vm.handleSave).toHaveBeenCalled();
});
@@ -168,41 +188,51 @@ describe('issue_comment_form component', () => {
describe('actions', () => {
it('should be possible to close the issue', () => {
- expect(vm.$el.querySelector('.btn-comment-and-close').textContent.trim()).toEqual('Close issue');
+ expect(vm.$el.querySelector('.btn-comment-and-close').textContent.trim()).toEqual(
+ 'Close issue',
+ );
});
it('should render comment button as disabled', () => {
- expect(vm.$el.querySelector('.js-comment-submit-button').getAttribute('disabled')).toEqual('disabled');
+ expect(vm.$el.querySelector('.js-comment-submit-button').getAttribute('disabled')).toEqual(
+ 'disabled',
+ );
});
- it('should enable comment button if it has note', (done) => {
+ it('should enable comment button if it has note', done => {
vm.note = 'Foo';
Vue.nextTick(() => {
- expect(vm.$el.querySelector('.js-comment-submit-button').getAttribute('disabled')).toEqual(null);
+ expect(
+ vm.$el.querySelector('.js-comment-submit-button').getAttribute('disabled'),
+ ).toEqual(null);
done();
});
});
- it('should update buttons texts when it has note', (done) => {
+ it('should update buttons texts when it has note', done => {
vm.note = 'Foo';
Vue.nextTick(() => {
- expect(vm.$el.querySelector('.btn-comment-and-close').textContent.trim()).toEqual('Comment & close issue');
+ expect(vm.$el.querySelector('.btn-comment-and-close').textContent.trim()).toEqual(
+ 'Comment & close issue',
+ );
expect(vm.$el.querySelector('.js-note-discard')).toBeDefined();
done();
});
});
- it('updates button text with noteable type', (done) => {
- vm.noteableType = 'merge_request';
+ it('updates button text with noteable type', done => {
+ vm.noteableType = constants.MERGE_REQUEST_NOTEABLE_TYPE;
Vue.nextTick(() => {
- expect(vm.$el.querySelector('.btn-comment-and-close').textContent.trim()).toEqual('Close merge request');
+ expect(vm.$el.querySelector('.btn-comment-and-close').textContent.trim()).toEqual(
+ 'Close merge request',
+ );
done();
});
});
describe('when clicking close/reopen button', () => {
- it('should disable button and show a loading spinner', (done) => {
+ it('should disable button and show a loading spinner', done => {
const toggleStateButton = vm.$el.querySelector('.js-action-button');
toggleStateButton.click();
@@ -217,7 +247,7 @@ describe('issue_comment_form component', () => {
});
describe('issue is confidential', () => {
- it('shows information warning', (done) => {
+ it('shows information warning', done => {
store.dispatch('setNoteableData', Object.assign(noteableDataMock, { confidential: true }));
Vue.nextTick(() => {
expect(vm.$el.querySelector('.confidential-issue-warning')).toBeDefined();
@@ -237,7 +267,9 @@ describe('issue_comment_form component', () => {
});
it('should render signed out widget', () => {
- expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toEqual('Please register or sign in to reply');
+ expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toEqual(
+ 'Please register or sign in to reply',
+ );
});
it('should not render submission form', () => {
diff --git a/spec/javascripts/notes/components/diff_file_header_spec.js b/spec/javascripts/notes/components/diff_file_header_spec.js
deleted file mode 100644
index ef6d513444a..00000000000
--- a/spec/javascripts/notes/components/diff_file_header_spec.js
+++ /dev/null
@@ -1,93 +0,0 @@
-import Vue from 'vue';
-import DiffFileHeader from '~/notes/components/diff_file_header.vue';
-import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-const discussionFixture = 'merge_requests/diff_discussion.json';
-
-describe('diff_file_header', () => {
- let vm;
- const diffDiscussionMock = getJSONFixture(discussionFixture)[0];
- const diffFile = convertObjectPropsToCamelCase(diffDiscussionMock.diff_file);
- const props = {
- diffFile,
- };
- const Component = Vue.extend(DiffFileHeader);
- const selectors = {
- get copyButton() {
- return vm.$el.querySelector('button[data-original-title="Copy file path to clipboard"]');
- },
- get fileName() {
- return vm.$el.querySelector('.file-title-name');
- },
- get titleWrapper() {
- return vm.$refs.titleWrapper;
- },
- };
-
- describe('submodule', () => {
- beforeEach(() => {
- props.diffFile.submodule = true;
- props.diffFile.submoduleLink = '<a href="/bha">Submodule</a>';
-
- vm = mountComponent(Component, props);
- });
-
- it('shows submoduleLink', () => {
- expect(selectors.fileName.innerHTML).toBe(props.diffFile.submoduleLink);
- });
-
- it('has button to copy blob path', () => {
- expect(selectors.copyButton).toExist();
- expect(selectors.copyButton.getAttribute('data-clipboard-text')).toBe(props.diffFile.submoduleLink);
- });
- });
-
- describe('changed file', () => {
- beforeEach(() => {
- props.diffFile.submodule = false;
- props.diffFile.discussionPath = 'some/discussion/id';
-
- vm = mountComponent(Component, props);
- });
-
- it('shows file type icon', () => {
- expect(vm.$el.innerHTML).toContain('fa-file-text-o');
- });
-
- it('links to discussion path', () => {
- expect(selectors.titleWrapper).toExist();
- expect(selectors.titleWrapper.tagName).toBe('A');
- expect(selectors.titleWrapper.getAttribute('href')).toBe(props.diffFile.discussionPath);
- });
-
- it('shows plain title if no link given', () => {
- props.diffFile.discussionPath = undefined;
- vm = mountComponent(Component, props);
-
- expect(selectors.titleWrapper.tagName).not.toBe('A');
- expect(selectors.titleWrapper.href).toBeFalsy();
- });
-
- it('has button to copy file path', () => {
- expect(selectors.copyButton).toExist();
- expect(selectors.copyButton.getAttribute('data-clipboard-text')).toBe(props.diffFile.filePath);
- });
-
- it('shows file mode change', (done) => {
- vm.diffFile = {
- ...props.diffFile,
- modeChanged: true,
- aMode: '100755',
- bMode: '100644',
- };
-
- Vue.nextTick(() => {
- expect(
- vm.$refs.fileMode.textContent.trim(),
- ).toBe('100755 → 100644');
- done();
- });
- });
- });
-});
diff --git a/spec/javascripts/notes/components/diff_with_note_spec.js b/spec/javascripts/notes/components/diff_with_note_spec.js
index f4ec7132dbd..239d7950907 100644
--- a/spec/javascripts/notes/components/diff_with_note_spec.js
+++ b/spec/javascripts/notes/components/diff_with_note_spec.js
@@ -1,12 +1,14 @@
import Vue from 'vue';
import DiffWithNote from '~/notes/components/diff_with_note.vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import createStore from '~/notes/stores';
+import { mountComponentWithStore } from 'spec/helpers';
const discussionFixture = 'merge_requests/diff_discussion.json';
const imageDiscussionFixture = 'merge_requests/image_diff_discussion.json';
describe('diff_with_note', () => {
+ let store;
let vm;
const diffDiscussionMock = getJSONFixture(discussionFixture)[0];
const diffDiscussion = convertObjectPropsToCamelCase(diffDiscussionMock);
@@ -29,9 +31,21 @@ describe('diff_with_note', () => {
},
};
+ beforeEach(() => {
+ store = createStore();
+ store.replaceState({
+ ...store.state,
+ notes: {
+ noteableData: {
+ current_user: {},
+ },
+ },
+ });
+ });
+
describe('text diff', () => {
beforeEach(() => {
- vm = mountComponent(Component, props);
+ vm = mountComponentWithStore(Component, { props, store });
});
it('shows text diff', () => {
@@ -55,7 +69,7 @@ describe('diff_with_note', () => {
});
it('shows image diff', () => {
- vm = mountComponent(Component, props);
+ vm = mountComponentWithStore(Component, { props, store });
expect(selectors.container).toHaveClass('js-image-file');
expect(selectors.diffTable).not.toExist();
diff --git a/spec/javascripts/notes/components/discussion_counter_spec.js b/spec/javascripts/notes/components/discussion_counter_spec.js
new file mode 100644
index 00000000000..a3869cc6498
--- /dev/null
+++ b/spec/javascripts/notes/components/discussion_counter_spec.js
@@ -0,0 +1,58 @@
+import Vue from 'vue';
+import createStore from '~/notes/stores';
+import DiscussionCounter from '~/notes/components/discussion_counter.vue';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { noteableDataMock, discussionMock, notesDataMock } from '../mock_data';
+
+describe('DiscussionCounter component', () => {
+ let store;
+ let vm;
+
+ beforeEach(() => {
+ window.mrTabs = {};
+
+ const Component = Vue.extend(DiscussionCounter);
+
+ store = createStore();
+ store.dispatch('setNoteableData', noteableDataMock);
+ store.dispatch('setNotesData', notesDataMock);
+
+ vm = createComponentWithStore(Component, store);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('methods', () => {
+ describe('jumpToFirstUnresolvedDiscussion', () => {
+ it('expands unresolved discussion', () => {
+ spyOn(vm, 'expandDiscussion').and.stub();
+ const discussions = [
+ {
+ ...discussionMock,
+ id: discussionMock.id,
+ notes: [{ ...discussionMock.notes[0], resolvable: true, resolved: true }],
+ },
+ {
+ ...discussionMock,
+ id: discussionMock.id + 1,
+ notes: [{ ...discussionMock.notes[0], resolvable: true, resolved: false }],
+ },
+ ];
+ const firstDiscussionId = discussionMock.id + 1;
+ store.replaceState({
+ ...store.state,
+ discussions,
+ });
+ setFixtures(`
+ <div data-discussion-id="${firstDiscussionId}"></div>
+ `);
+
+ vm.jumpToFirstUnresolvedDiscussion();
+
+ expect(vm.expandDiscussion).toHaveBeenCalledWith({ discussionId: firstDiscussionId });
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/notes/components/note_actions_spec.js b/spec/javascripts/notes/components/note_actions_spec.js
index 1dfe890e05e..52cc42cb53d 100644
--- a/spec/javascripts/notes/components/note_actions_spec.js
+++ b/spec/javascripts/notes/components/note_actions_spec.js
@@ -1,14 +1,16 @@
import Vue from 'vue';
-import store from '~/notes/stores';
+import createStore from '~/notes/stores';
import noteActions from '~/notes/components/note_actions.vue';
import { userDataMock } from '../mock_data';
describe('issue_note_actions component', () => {
let vm;
+ let store;
let Component;
beforeEach(() => {
Component = Vue.extend(noteActions);
+ store = createStore();
});
afterEach(() => {
@@ -20,14 +22,16 @@ describe('issue_note_actions component', () => {
beforeEach(() => {
props = {
- accessLevel: 'Master',
+ accessLevel: 'Maintainer',
authorId: 26,
canDelete: true,
canEdit: true,
canAwardEmoji: true,
canReportAsAbuse: true,
noteId: 539,
- reportAbusePath: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26',
+ noteUrl: 'https://localhost:3000/group/project/merge_requests/1#note_1',
+ reportAbusePath:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26',
};
store.dispatch('setUserData', userDataMock);
@@ -67,14 +71,16 @@ describe('issue_note_actions component', () => {
beforeEach(() => {
store.dispatch('setUserData', {});
props = {
- accessLevel: 'Master',
+ accessLevel: 'Maintainer',
authorId: 26,
canDelete: false,
canEdit: false,
canAwardEmoji: false,
canReportAsAbuse: false,
noteId: 539,
- reportAbusePath: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26',
+ noteUrl: 'https://localhost:3000/group/project/merge_requests/1#note_1',
+ reportAbusePath:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26',
};
vm = new Component({
store,
diff --git a/spec/javascripts/notes/components/note_app_spec.js b/spec/javascripts/notes/components/note_app_spec.js
index 0e792eee5e9..7eb4d3aed29 100644
--- a/spec/javascripts/notes/components/note_app_spec.js
+++ b/spec/javascripts/notes/components/note_app_spec.js
@@ -3,7 +3,9 @@ import _ from 'underscore';
import Vue from 'vue';
import notesApp from '~/notes/components/notes_app.vue';
import service from '~/notes/services/notes_service';
+import createStore from '~/notes/stores';
import '~/behaviors/markdown/render_gfm';
+import { mountComponentWithStore } from 'spec/helpers';
import * as mockData from '../mock_data';
const vueMatchers = {
@@ -22,6 +24,7 @@ const vueMatchers = {
describe('note_app', () => {
let mountComponent;
let vm;
+ let store;
beforeEach(() => {
jasmine.addMatchers(vueMatchers);
@@ -29,16 +32,18 @@ describe('note_app', () => {
const IssueNotesApp = Vue.extend(notesApp);
- mountComponent = (data) => {
+ store = createStore();
+ mountComponent = data => {
const props = data || {
noteableData: mockData.noteableDataMock,
notesData: mockData.notesDataMock,
userData: mockData.userDataMock,
};
- return new IssueNotesApp({
- propsData: props,
- }).$mount();
+ return mountComponentWithStore(IssueNotesApp, {
+ props,
+ store,
+ });
};
});
@@ -48,9 +53,11 @@ describe('note_app', () => {
describe('set data', () => {
const responseInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify([]), {
- status: 200,
- }));
+ next(
+ request.respondWith(JSON.stringify([]), {
+ status: 200,
+ }),
+ );
};
beforeEach(() => {
@@ -74,8 +81,8 @@ describe('note_app', () => {
expect(vm.$store.state.userData).toEqual(mockData.userDataMock);
});
- it('should fetch notes', () => {
- expect(vm.$store.state.notes).toEqual([]);
+ it('should fetch discussions', () => {
+ expect(vm.$store.state.discussions).toEqual([]);
});
});
@@ -89,15 +96,20 @@ describe('note_app', () => {
Vue.http.interceptors = _.without(Vue.http.interceptors, mockData.individualNoteInterceptor);
});
- it('should render list of notes', (done) => {
- const note = mockData.INDIVIDUAL_NOTE_RESPONSE_MAP.GET['/gitlab-org/gitlab-ce/issues/26/discussions.json'][0].notes[0];
+ it('should render list of notes', done => {
+ const note =
+ mockData.INDIVIDUAL_NOTE_RESPONSE_MAP.GET[
+ '/gitlab-org/gitlab-ce/issues/26/discussions.json'
+ ][0].notes[0];
setTimeout(() => {
expect(
vm.$el.querySelector('.main-notes-list .note-header-author-name').textContent.trim(),
).toEqual(note.author.name);
- expect(vm.$el.querySelector('.main-notes-list .note-text').innerHTML).toEqual(note.note_html);
+ expect(vm.$el.querySelector('.main-notes-list .note-text').innerHTML).toEqual(
+ note.note_html,
+ );
done();
}, 0);
});
@@ -106,13 +118,13 @@ describe('note_app', () => {
expect(vm.$el.querySelector('.js-main-target-form').tagName).toEqual('FORM');
expect(
vm.$el.querySelector('.js-main-target-form textarea').getAttribute('placeholder'),
- ).toEqual('Write a comment or drag your files here...');
+ ).toEqual('Write a comment or drag your files here…');
});
it('should render form comment button as disabled', () => {
- expect(
- vm.$el.querySelector('.js-note-new-discussion').getAttribute('disabled'),
- ).toEqual('disabled');
+ expect(vm.$el.querySelector('.js-note-new-discussion').getAttribute('disabled')).toEqual(
+ 'disabled',
+ );
});
});
@@ -129,13 +141,13 @@ describe('note_app', () => {
expect(vm.$el.querySelector('.js-main-target-form').tagName).toEqual('FORM');
expect(
vm.$el.querySelector('.js-main-target-form textarea').getAttribute('placeholder'),
- ).toEqual('Write a comment or drag your files here...');
+ ).toEqual('Write a comment or drag your files here…');
});
});
describe('update note', () => {
describe('individual note', () => {
- beforeEach((done) => {
+ beforeEach(done => {
Vue.http.interceptors.push(mockData.individualNoteInterceptor);
spyOn(service, 'updateNote').and.callThrough();
vm = mountComponent();
@@ -156,7 +168,7 @@ describe('note_app', () => {
expect(vm).toIncludeElement('.js-vue-issue-note-form');
});
- it('calls the service to update the note', (done) => {
+ it('calls the service to update the note', done => {
vm.$el.querySelector('.js-vue-issue-note-form').value = 'this is a note';
vm.$el.querySelector('.js-vue-issue-save').click();
@@ -169,7 +181,7 @@ describe('note_app', () => {
});
describe('discussion note', () => {
- beforeEach((done) => {
+ beforeEach(done => {
Vue.http.interceptors.push(mockData.discussionNoteInterceptor);
spyOn(service, 'updateNote').and.callThrough();
vm = mountComponent();
@@ -191,7 +203,7 @@ describe('note_app', () => {
expect(vm).toIncludeElement('.js-vue-issue-note-form');
});
- it('updates the note and resets the edit form', (done) => {
+ it('updates the note and resets the edit form', done => {
vm.$el.querySelector('.js-vue-issue-note-form').value = 'this is a note';
vm.$el.querySelector('.js-vue-issue-save').click();
@@ -211,12 +223,16 @@ describe('note_app', () => {
it('should render markdown docs url', () => {
const { markdownDocsPath } = mockData.notesDataMock;
- expect(vm.$el.querySelector(`a[href="${markdownDocsPath}"]`).textContent.trim()).toEqual('Markdown');
+ expect(vm.$el.querySelector(`a[href="${markdownDocsPath}"]`).textContent.trim()).toEqual(
+ 'Markdown',
+ );
});
it('should render quick action docs url', () => {
const { quickActionsDocsPath } = mockData.notesDataMock;
- expect(vm.$el.querySelector(`a[href="${quickActionsDocsPath}"]`).textContent.trim()).toEqual('quick actions');
+ expect(vm.$el.querySelector(`a[href="${quickActionsDocsPath}"]`).textContent.trim()).toEqual(
+ 'quick actions',
+ );
});
});
@@ -230,7 +246,7 @@ describe('note_app', () => {
Vue.http.interceptors = _.without(Vue.http.interceptors, mockData.individualNoteInterceptor);
});
- it('should render markdown docs url', (done) => {
+ it('should render markdown docs url', done => {
setTimeout(() => {
vm.$el.querySelector('.js-note-edit').click();
const { markdownDocsPath } = mockData.notesDataMock;
@@ -244,15 +260,15 @@ describe('note_app', () => {
}, 0);
});
- it('should not render quick actions docs url', (done) => {
+ it('should not render quick actions docs url', done => {
setTimeout(() => {
vm.$el.querySelector('.js-note-edit').click();
const { quickActionsDocsPath } = mockData.notesDataMock;
Vue.nextTick(() => {
- expect(
- vm.$el.querySelector(`.edit-note a[href="${quickActionsDocsPath}"]`),
- ).toEqual(null);
+ expect(vm.$el.querySelector(`.edit-note a[href="${quickActionsDocsPath}"]`)).toEqual(
+ null,
+ );
done();
});
}, 0);
diff --git a/spec/javascripts/notes/components/note_awards_list_spec.js b/spec/javascripts/notes/components/note_awards_list_spec.js
index 1c30d8691b1..9d98ba219da 100644
--- a/spec/javascripts/notes/components/note_awards_list_spec.js
+++ b/spec/javascripts/notes/components/note_awards_list_spec.js
@@ -1,15 +1,17 @@
import Vue from 'vue';
-import store from '~/notes/stores';
+import createStore from '~/notes/stores';
import awardsNote from '~/notes/components/note_awards_list.vue';
import { noteableDataMock, notesDataMock } from '../mock_data';
describe('note_awards_list component', () => {
+ let store;
let vm;
let awardsMock;
beforeEach(() => {
const Component = Vue.extend(awardsNote);
+ store = createStore();
store.dispatch('setNoteableData', noteableDataMock);
store.dispatch('setNotesData', notesDataMock);
awardsMock = [
@@ -41,7 +43,9 @@ describe('note_awards_list component', () => {
it('should render awarded emojis', () => {
expect(vm.$el.querySelector('.js-awards-block button [data-name="flag_tz"]')).toBeDefined();
- expect(vm.$el.querySelector('.js-awards-block button [data-name="cartwheel_tone3"]')).toBeDefined();
+ expect(
+ vm.$el.querySelector('.js-awards-block button [data-name="cartwheel_tone3"]'),
+ ).toBeDefined();
});
it('should be possible to remove awarded emoji', () => {
diff --git a/spec/javascripts/notes/components/note_body_spec.js b/spec/javascripts/notes/components/note_body_spec.js
index 4e551496ff0..efad0785afe 100644
--- a/spec/javascripts/notes/components/note_body_spec.js
+++ b/spec/javascripts/notes/components/note_body_spec.js
@@ -1,15 +1,16 @@
-
import Vue from 'vue';
-import store from '~/notes/stores';
+import createStore from '~/notes/stores';
import noteBody from '~/notes/components/note_body.vue';
import { noteableDataMock, notesDataMock, note } from '../mock_data';
describe('issue_note_body component', () => {
+ let store;
let vm;
beforeEach(() => {
const Component = Vue.extend(noteBody);
+ store = createStore();
store.dispatch('setNoteableData', noteableDataMock);
store.dispatch('setNotesData', notesDataMock);
@@ -37,7 +38,7 @@ describe('issue_note_body component', () => {
});
describe('isEditing', () => {
- beforeEach((done) => {
+ beforeEach(done => {
vm.isEditing = true;
Vue.nextTick(done);
});
diff --git a/spec/javascripts/notes/components/note_form_spec.js b/spec/javascripts/notes/components/note_form_spec.js
index f841a408d09..95d400ab3df 100644
--- a/spec/javascripts/notes/components/note_form_spec.js
+++ b/spec/javascripts/notes/components/note_form_spec.js
@@ -1,16 +1,18 @@
import Vue from 'vue';
-import store from '~/notes/stores';
+import createStore from '~/notes/stores';
import issueNoteForm from '~/notes/components/note_form.vue';
import { noteableDataMock, notesDataMock } from '../mock_data';
import { keyboardDownEvent } from '../../issue_show/helpers';
describe('issue_note_form component', () => {
+ let store;
let vm;
let props;
beforeEach(() => {
const Component = Vue.extend(issueNoteForm);
+ store = createStore();
store.dispatch('setNoteableData', noteableDataMock);
store.dispatch('setNotesData', notesDataMock);
@@ -31,14 +33,18 @@ describe('issue_note_form component', () => {
});
describe('conflicts editing', () => {
- it('should show conflict message if note changes outside the component', (done) => {
+ it('should show conflict message if note changes outside the component', done => {
vm.isEditing = true;
vm.noteBody = 'Foo';
- const message = 'This comment has changed since you started editing, please review the updated comment to ensure information is not lost.';
+ const message =
+ 'This comment has changed since you started editing, please review the updated comment to ensure information is not lost.';
Vue.nextTick(() => {
expect(
- vm.$el.querySelector('.js-conflict-edit-warning').textContent.replace(/\s+/g, ' ').trim(),
+ vm.$el
+ .querySelector('.js-conflict-edit-warning')
+ .textContent.replace(/\s+/g, ' ')
+ .trim(),
).toEqual(message);
done();
});
@@ -47,14 +53,16 @@ describe('issue_note_form component', () => {
describe('form', () => {
it('should render text area with placeholder', () => {
- expect(
- vm.$el.querySelector('textarea').getAttribute('placeholder'),
- ).toEqual('Write a comment or drag your files here...');
+ expect(vm.$el.querySelector('textarea').getAttribute('placeholder')).toEqual(
+ 'Write a comment or drag your files here…',
+ );
});
it('should link to markdown docs', () => {
const { markdownDocsPath } = notesDataMock;
- expect(vm.$el.querySelector(`a[href="${markdownDocsPath}"]`).textContent.trim()).toEqual('Markdown');
+ expect(vm.$el.querySelector(`a[href="${markdownDocsPath}"]`).textContent.trim()).toEqual(
+ 'Markdown',
+ );
});
describe('keyboard events', () => {
@@ -87,7 +95,7 @@ describe('issue_note_form component', () => {
});
describe('actions', () => {
- it('should be possible to cancel', (done) => {
+ it('should be possible to cancel', done => {
spyOn(vm, 'cancelHandler').and.callThrough();
vm.isEditing = true;
@@ -101,7 +109,7 @@ describe('issue_note_form component', () => {
});
});
- it('should be possible to update the note', (done) => {
+ it('should be possible to update the note', done => {
vm.isEditing = true;
Vue.nextTick(() => {
diff --git a/spec/javascripts/notes/components/note_header_spec.js b/spec/javascripts/notes/components/note_header_spec.js
index 5636f8d1a9f..a3c6bf78988 100644
--- a/spec/javascripts/notes/components/note_header_spec.js
+++ b/spec/javascripts/notes/components/note_header_spec.js
@@ -1,13 +1,15 @@
import Vue from 'vue';
import noteHeader from '~/notes/components/note_header.vue';
-import store from '~/notes/stores';
+import createStore from '~/notes/stores';
describe('note_header component', () => {
+ let store;
let vm;
let Component;
beforeEach(() => {
Component = Vue.extend(noteHeader);
+ store = createStore();
});
afterEach(() => {
@@ -38,12 +40,8 @@ describe('note_header component', () => {
});
it('should render user information', () => {
- expect(
- vm.$el.querySelector('.note-header-author-name').textContent.trim(),
- ).toEqual('Root');
- expect(
- vm.$el.querySelector('.note-header-info a').getAttribute('href'),
- ).toEqual('/root');
+ expect(vm.$el.querySelector('.note-header-author-name').textContent.trim()).toEqual('Root');
+ expect(vm.$el.querySelector('.note-header-info a').getAttribute('href')).toEqual('/root');
});
it('should render timestamp link', () => {
@@ -78,7 +76,7 @@ describe('note_header component', () => {
expect(vm.$el.querySelector('.js-vue-toggle-button')).toBeDefined();
});
- it('emits toggle event on click', (done) => {
+ it('emits toggle event on click', done => {
spyOn(vm, '$emit');
vm.$el.querySelector('.js-vue-toggle-button').click();
@@ -89,24 +87,24 @@ describe('note_header component', () => {
});
});
- it('renders up arrow when open', (done) => {
+ it('renders up arrow when open', done => {
vm.expanded = true;
Vue.nextTick(() => {
- expect(
- vm.$el.querySelector('.js-vue-toggle-button i').classList,
- ).toContain('fa-chevron-up');
+ expect(vm.$el.querySelector('.js-vue-toggle-button i').classList).toContain(
+ 'fa-chevron-up',
+ );
done();
});
});
- it('renders down arrow when closed', (done) => {
+ it('renders down arrow when closed', done => {
vm.expanded = false;
Vue.nextTick(() => {
- expect(
- vm.$el.querySelector('.js-vue-toggle-button i').classList,
- ).toContain('fa-chevron-down');
+ expect(vm.$el.querySelector('.js-vue-toggle-button i').classList).toContain(
+ 'fa-chevron-down',
+ );
done();
});
});
diff --git a/spec/javascripts/notes/components/note_signed_out_widget_spec.js b/spec/javascripts/notes/components/note_signed_out_widget_spec.js
index 6cba8053888..e217a2caa73 100644
--- a/spec/javascripts/notes/components/note_signed_out_widget_spec.js
+++ b/spec/javascripts/notes/components/note_signed_out_widget_spec.js
@@ -1,13 +1,15 @@
import Vue from 'vue';
import noteSignedOut from '~/notes/components/note_signed_out_widget.vue';
-import store from '~/notes/stores';
+import createStore from '~/notes/stores';
import { notesDataMock } from '../mock_data';
describe('note_signed_out_widget component', () => {
+ let store;
let vm;
beforeEach(() => {
const Component = Vue.extend(noteSignedOut);
+ store = createStore();
store.dispatch('setNotesData', notesDataMock);
vm = new Component({
@@ -20,18 +22,20 @@ describe('note_signed_out_widget component', () => {
});
it('should render sign in link provided in the store', () => {
- expect(
- vm.$el.querySelector(`a[href="${notesDataMock.newSessionPath}"]`).textContent,
- ).toEqual('sign in');
+ expect(vm.$el.querySelector(`a[href="${notesDataMock.newSessionPath}"]`).textContent).toEqual(
+ 'sign in',
+ );
});
it('should render register link provided in the store', () => {
- expect(
- vm.$el.querySelector(`a[href="${notesDataMock.registerPath}"]`).textContent,
- ).toEqual('register');
+ expect(vm.$el.querySelector(`a[href="${notesDataMock.registerPath}"]`).textContent).toEqual(
+ 'register',
+ );
});
it('should render information text', () => {
- expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toEqual('Please register or sign in to reply');
+ expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toEqual(
+ 'Please register or sign in to reply',
+ );
});
});
diff --git a/spec/javascripts/notes/components/noteable_discussion_spec.js b/spec/javascripts/notes/components/noteable_discussion_spec.js
index cda550760fe..7da931fd9cb 100644
--- a/spec/javascripts/notes/components/noteable_discussion_spec.js
+++ b/spec/javascripts/notes/components/noteable_discussion_spec.js
@@ -1,22 +1,26 @@
import Vue from 'vue';
-import store from '~/notes/stores';
-import issueDiscussion from '~/notes/components/noteable_discussion.vue';
+import createStore from '~/notes/stores';
+import noteableDiscussion from '~/notes/components/noteable_discussion.vue';
+import '~/behaviors/markdown/render_gfm';
import { noteableDataMock, discussionMock, notesDataMock } from '../mock_data';
-describe('issue_discussion component', () => {
+const discussionWithTwoUnresolvedNotes = 'merge_requests/resolved_diff_discussion.json';
+
+describe('noteable_discussion component', () => {
+ const Component = Vue.extend(noteableDiscussion);
+ let store;
let vm;
- beforeEach(() => {
- const Component = Vue.extend(issueDiscussion);
+ preloadFixtures(discussionWithTwoUnresolvedNotes);
+ beforeEach(() => {
+ store = createStore();
store.dispatch('setNoteableData', noteableDataMock);
store.dispatch('setNotesData', notesDataMock);
vm = new Component({
store,
- propsData: {
- note: discussionMock,
- },
+ propsData: { discussion: discussionMock },
}).$mount();
});
@@ -55,4 +59,76 @@ describe('issue_discussion component', () => {
).toBeNull();
});
});
+
+ describe('computed', () => {
+ describe('hasMultipleUnresolvedDiscussions', () => {
+ it('is false if there are no unresolved discussions', done => {
+ spyOnProperty(vm, 'unresolvedDiscussions').and.returnValue([]);
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vm.hasMultipleUnresolvedDiscussions).toBe(false);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('is false if there is one unresolved discussion', done => {
+ spyOnProperty(vm, 'unresolvedDiscussions').and.returnValue([discussionMock]);
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vm.hasMultipleUnresolvedDiscussions).toBe(false);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('is true if there are two unresolved discussions', done => {
+ const discussion = getJSONFixture(discussionWithTwoUnresolvedNotes)[0];
+ discussion.notes[0].resolved = false;
+ vm.$store.dispatch('setInitialNotes', [discussion, discussion]);
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vm.hasMultipleUnresolvedDiscussions).toBe(true);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('jumpToNextDiscussion', () => {
+ it('expands next unresolved discussion', () => {
+ spyOn(vm, 'expandDiscussion').and.stub();
+ const discussions = [
+ discussionMock,
+ {
+ ...discussionMock,
+ id: discussionMock.id + 1,
+ notes: [{ ...discussionMock.notes[0], resolvable: true, resolved: true }],
+ },
+ {
+ ...discussionMock,
+ id: discussionMock.id + 2,
+ notes: [{ ...discussionMock.notes[0], resolvable: true, resolved: false }],
+ },
+ ];
+ const nextDiscussionId = discussionMock.id + 2;
+ store.replaceState({
+ ...store.state,
+ discussions,
+ });
+ setFixtures(`
+ <div data-discussion-id="${nextDiscussionId}"></div>
+ `);
+
+ vm.jumpToNextDiscussion();
+
+ expect(vm.expandDiscussion).toHaveBeenCalledWith({ discussionId: nextDiscussionId });
+ });
+ });
+ });
});
diff --git a/spec/javascripts/notes/components/noteable_note_spec.js b/spec/javascripts/notes/components/noteable_note_spec.js
index cfd037633e9..a31d17cacbb 100644
--- a/spec/javascripts/notes/components/noteable_note_spec.js
+++ b/spec/javascripts/notes/components/noteable_note_spec.js
@@ -1,16 +1,18 @@
import $ from 'jquery';
import _ from 'underscore';
import Vue from 'vue';
-import store from '~/notes/stores';
+import createStore from '~/notes/stores';
import issueNote from '~/notes/components/noteable_note.vue';
import { noteableDataMock, notesDataMock, note } from '../mock_data';
describe('issue_note', () => {
+ let store;
let vm;
beforeEach(() => {
const Component = Vue.extend(issueNote);
+ store = createStore();
store.dispatch('setNoteableData', noteableDataMock);
store.dispatch('setNotesData', notesDataMock);
@@ -27,12 +29,14 @@ describe('issue_note', () => {
});
it('should render user information', () => {
- expect(vm.$el.querySelector('.user-avatar-link img').getAttribute('src')).toEqual(note.author.avatar_url);
+ expect(vm.$el.querySelector('.user-avatar-link img').getAttribute('src')).toEqual(
+ note.author.avatar_url,
+ );
});
it('should render note header content', () => {
- expect(vm.$el.querySelector('.note-header .note-header-author-name').textContent.trim()).toEqual(note.author.name);
- expect(vm.$el.querySelector('.note-header .note-headline-meta').textContent.trim()).toContain('commented');
+ const el = vm.$el.querySelector('.note-header .note-header-author-name');
+ expect(el.textContent.trim()).toEqual(note.author.name);
});
it('should render note actions', () => {
@@ -43,7 +47,7 @@ describe('issue_note', () => {
expect(vm.$el.querySelector('.note-text').innerHTML).toEqual(note.note_html);
});
- it('prevents note preview xss', (done) => {
+ it('prevents note preview xss', done => {
const imgSrc = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
const noteBody = `<img src="${imgSrc}" onload="alert(1)" />`;
const alertSpy = spyOn(window, 'alert');
@@ -59,7 +63,7 @@ describe('issue_note', () => {
});
describe('cancel edit', () => {
- it('restores content of updated note', (done) => {
+ it('restores content of updated note', done => {
const noteBody = 'updated note text';
vm.updateNote = () => Promise.resolve();
diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js
index bfe3a65feee..be2a8ba67fe 100644
--- a/spec/javascripts/notes/mock_data.js
+++ b/spec/javascripts/notes/mock_data.js
@@ -51,6 +51,7 @@ export const noteableDataMock = {
time_estimate: 0,
title: '14',
total_time_spent: 0,
+ noteable_note_url: '/group/project/merge_requests/1#note_1',
updated_at: '2017-08-04T09:53:01.226Z',
updated_by_id: 1,
web_url: '/gitlab-org/gitlab-ce/issues/26',
@@ -99,6 +100,8 @@ export const individualNote = {
{ name: 'art', user: { id: 1, name: 'Root', username: 'root' } },
],
toggle_award_path: '/gitlab-org/gitlab-ce/notes/1390/toggle_award_emoji',
+ noteable_note_url: '/group/project/merge_requests/1#note_1',
+ note_url: '/group/project/merge_requests/1#note_1',
report_abuse_path:
'/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1390&user_id=1',
path: '/gitlab-org/gitlab-ce/notes/1390',
@@ -157,9 +160,12 @@ export const note = {
},
],
toggle_award_path: '/gitlab-org/gitlab-ce/notes/546/toggle_award_emoji',
+ note_url: '/group/project/merge_requests/1#note_1',
+ noteable_note_url: '/group/project/merge_requests/1#note_1',
report_abuse_path:
'/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_546&user_id=1',
path: '/gitlab-org/gitlab-ce/notes/546',
+ cached_markdown_version: 11,
};
export const discussionMock = {
@@ -198,6 +204,7 @@ export const discussionMock = {
discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1',
emoji_awardable: true,
award_emoji: [],
+ noteable_note_url: '/group/project/merge_requests/1#note_1',
toggle_award_path: '/gitlab-org/gitlab-ce/notes/1395/toggle_award_emoji',
report_abuse_path:
'/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1395&user_id=1',
@@ -244,6 +251,7 @@ export const discussionMock = {
emoji_awardable: true,
award_emoji: [],
toggle_award_path: '/gitlab-org/gitlab-ce/notes/1396/toggle_award_emoji',
+ noteable_note_url: '/group/project/merge_requests/1#note_1',
report_abuse_path:
'/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1396&user_id=1',
path: '/gitlab-org/gitlab-ce/notes/1396',
@@ -288,6 +296,7 @@ export const discussionMock = {
discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1',
emoji_awardable: true,
award_emoji: [],
+ noteable_note_url: '/group/project/merge_requests/1#note_1',
toggle_award_path: '/gitlab-org/gitlab-ce/notes/1437/toggle_award_emoji',
report_abuse_path:
'/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1437&user_id=1',
@@ -295,6 +304,7 @@ export const discussionMock = {
},
],
individual_note: false,
+ resolvable: true,
};
export const loggedOutnoteableData = {
@@ -335,11 +345,85 @@ export const loggedOutnoteableData = {
can_create_note: false,
can_update: false,
},
+ noteable_note_url: '/group/project/merge_requests/1#note_1',
create_note_path: '/gitlab-org/gitlab-ce/notes?target_id=98&target_type=issue',
preview_note_path:
'/gitlab-org/gitlab-ce/preview_markdown?quick_actions_target_id=98&quick_actions_target_type=Issue',
};
+export const collapseNotesMock = [
+ {
+ expanded: true,
+ id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd',
+ individual_note: true,
+ notes: [
+ {
+ id: 1390,
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Root',
+ username: 'root',
+ state: 'active',
+ avatar_url: 'test',
+ path: '/root',
+ },
+ created_at: '2018-02-26T18:07:41.071Z',
+ updated_at: '2018-02-26T18:07:41.071Z',
+ system: true,
+ system_note_icon_name: 'pencil',
+ noteable_id: 98,
+ noteable_type: 'Issue',
+ type: null,
+ human_access: 'Owner',
+ note: 'changed the description',
+ note_html: '<p dir="auto">changed the description</p>',
+ current_user: { can_edit: false },
+ discussion_id: 'b97fb7bda470a65b3e009377a9032edec0a4dd05',
+ emoji_awardable: false,
+ path: '/h5bp/html5-boilerplate/notes/1057',
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fh5bp%2Fhtml5-boilerplate%2Fissues%2F10%23note_1057&user_id=1',
+ },
+ ],
+ },
+ {
+ expanded: true,
+ id: 'ffde43f25984ad7f2b4275135e0e2846875336c0',
+ individual_note: true,
+ notes: [
+ {
+ id: 1391,
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Root',
+ username: 'root',
+ state: 'active',
+ avatar_url: 'test',
+ path: '/root',
+ },
+ created_at: '2018-02-26T18:13:24.071Z',
+ updated_at: '2018-02-26T18:13:24.071Z',
+ system: true,
+ system_note_icon_name: 'pencil',
+ noteable_id: 99,
+ noteable_type: 'Issue',
+ type: null,
+ human_access: 'Owner',
+ note: 'changed the description',
+ note_html: '<p dir="auto">changed the description</p>',
+ current_user: { can_edit: false },
+ discussion_id: '3eb958b4d81dec207ec3537a2f3bd8b9f271bb34',
+ emoji_awardable: false,
+ path: '/h5bp/html5-boilerplate/notes/1057',
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fh5bp%2Fhtml5-boilerplate%2Fissues%2F10%23note_1057&user_id=1',
+ },
+ ],
+ },
+];
+
export const INDIVIDUAL_NOTE_RESPONSE_MAP = {
GET: {
'/gitlab-org/gitlab-ce/issues/26/discussions.json': [
@@ -396,6 +480,7 @@ export const INDIVIDUAL_NOTE_RESPONSE_MAP = {
},
},
],
+ noteable_note_url: '/group/project/merge_requests/1#note_1',
toggle_award_path: '/gitlab-org/gitlab-ce/notes/1390/toggle_award_emoji',
report_abuse_path:
'/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1390\u0026user_id=1',
@@ -440,6 +525,7 @@ export const INDIVIDUAL_NOTE_RESPONSE_MAP = {
discussion_id: '70d5c92a4039a36c70100c6691c18c27e4b0a790',
emoji_awardable: true,
award_emoji: [],
+ noteable_note_url: '/group/project/merge_requests/1#note_1',
toggle_award_path: '/gitlab-org/gitlab-ce/notes/1391/toggle_award_emoji',
report_abuse_path:
'/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1391\u0026user_id=1',
@@ -494,6 +580,7 @@ export const INDIVIDUAL_NOTE_RESPONSE_MAP = {
discussion_id: 'a3ed36e29b1957efb3b68c53e2d7a2b24b1df052',
emoji_awardable: true,
award_emoji: [],
+ noteable_note_url: '/group/project/merge_requests/1#note_1',
toggle_award_path: '/gitlab-org/gitlab-ce/notes/1471/toggle_award_emoji',
report_abuse_path:
'/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F29%23note_1471\u0026user_id=1',
@@ -545,6 +632,7 @@ export const DISCUSSION_NOTE_RESPONSE_MAP = {
emoji_awardable: true,
award_emoji: [],
toggle_award_path: '/gitlab-org/gitlab-ce/notes/1471/toggle_award_emoji',
+ noteable_note_url: '/group/project/merge_requests/1#note_1',
report_abuse_path:
'/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F29%23note_1471\u0026user_id=1',
path: '/gitlab-org/gitlab-ce/notes/1471',
@@ -575,3 +663,508 @@ export function discussionNoteInterceptor(request, next) {
}),
);
}
+
+export const notesWithDescriptionChanges = [
+ {
+ id: '39b271c2033e9ed43d8edb393702f65f7a830459',
+ reply_id: '39b271c2033e9ed43d8edb393702f65f7a830459',
+ expanded: true,
+ notes: [
+ {
+ id: 901,
+ type: null,
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ created_at: '2018-05-29T12:05:36.117Z',
+ updated_at: '2018-05-29T12:05:36.117Z',
+ system: false,
+ noteable_id: 182,
+ noteable_type: 'Issue',
+ resolvable: false,
+ noteable_iid: 12,
+ note:
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
+ note_html:
+ '<p dir="auto">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>',
+ current_user: { can_edit: true, can_award_emoji: true },
+ resolved: false,
+ resolved_by: null,
+ discussion_id: '39b271c2033e9ed43d8edb393702f65f7a830459',
+ emoji_awardable: true,
+ award_emoji: [],
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_901&user_id=1',
+ human_access: 'Owner',
+ toggle_award_path: '/gitlab-org/gitlab-shell/notes/901/toggle_award_emoji',
+ path: '/gitlab-org/gitlab-shell/notes/901',
+ },
+ ],
+ individual_note: true,
+ resolvable: false,
+ resolved: false,
+ diff_discussion: false,
+ },
+ {
+ id: '4852335d7dc40b9ceb8fde1a2bb9c1b67e4c7795',
+ reply_id: '4852335d7dc40b9ceb8fde1a2bb9c1b67e4c7795',
+ expanded: true,
+ notes: [
+ {
+ id: 902,
+ type: null,
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ created_at: '2018-05-29T12:05:58.694Z',
+ updated_at: '2018-05-29T12:05:58.694Z',
+ system: false,
+ noteable_id: 182,
+ noteable_type: 'Issue',
+ resolvable: false,
+ noteable_iid: 12,
+ note:
+ 'Varius vel pharetra vel turpis nunc eget lorem. Ipsum dolor sit amet consectetur adipiscing.',
+ note_html:
+ '<p dir="auto">Varius vel pharetra vel turpis nunc eget lorem. Ipsum dolor sit amet consectetur adipiscing.</p>',
+ current_user: { can_edit: true, can_award_emoji: true },
+ resolved: false,
+ resolved_by: null,
+ discussion_id: '4852335d7dc40b9ceb8fde1a2bb9c1b67e4c7795',
+ emoji_awardable: true,
+ award_emoji: [],
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_902&user_id=1',
+ human_access: 'Owner',
+ toggle_award_path: '/gitlab-org/gitlab-shell/notes/902/toggle_award_emoji',
+ path: '/gitlab-org/gitlab-shell/notes/902',
+ },
+ ],
+ individual_note: true,
+ resolvable: false,
+ resolved: false,
+ diff_discussion: false,
+ },
+ {
+ id: '7f1feda384083eb31763366e6392399fde6f3f31',
+ reply_id: '7f1feda384083eb31763366e6392399fde6f3f31',
+ expanded: true,
+ notes: [
+ {
+ id: 903,
+ type: null,
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ created_at: '2018-05-29T12:06:05.772Z',
+ updated_at: '2018-05-29T12:06:05.772Z',
+ system: true,
+ noteable_id: 182,
+ noteable_type: 'Issue',
+ resolvable: false,
+ noteable_iid: 12,
+ note: 'changed the description',
+ note_html: '<p dir="auto">changed the description</p>',
+ current_user: { can_edit: false, can_award_emoji: true },
+ resolved: false,
+ resolved_by: null,
+ system_note_icon_name: 'pencil-square',
+ discussion_id: '7f1feda384083eb31763366e6392399fde6f3f31',
+ emoji_awardable: false,
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_903&user_id=1',
+ human_access: 'Owner',
+ path: '/gitlab-org/gitlab-shell/notes/903',
+ },
+ ],
+ individual_note: true,
+ resolvable: false,
+ resolved: false,
+ diff_discussion: false,
+ },
+ {
+ id: '091865fe3ae20f0045234a3d103e3b15e73405b5',
+ reply_id: '091865fe3ae20f0045234a3d103e3b15e73405b5',
+ expanded: true,
+ notes: [
+ {
+ id: 904,
+ type: null,
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ created_at: '2018-05-29T12:06:16.112Z',
+ updated_at: '2018-05-29T12:06:16.112Z',
+ system: false,
+ noteable_id: 182,
+ noteable_type: 'Issue',
+ resolvable: false,
+ noteable_iid: 12,
+ note: 'Ullamcorper eget nulla facilisi etiam',
+ note_html: '<p dir="auto">Ullamcorper eget nulla facilisi etiam</p>',
+ current_user: { can_edit: true, can_award_emoji: true },
+ resolved: false,
+ resolved_by: null,
+ discussion_id: '091865fe3ae20f0045234a3d103e3b15e73405b5',
+ emoji_awardable: true,
+ award_emoji: [],
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_904&user_id=1',
+ human_access: 'Owner',
+ toggle_award_path: '/gitlab-org/gitlab-shell/notes/904/toggle_award_emoji',
+ path: '/gitlab-org/gitlab-shell/notes/904',
+ },
+ ],
+ individual_note: true,
+ resolvable: false,
+ resolved: false,
+ diff_discussion: false,
+ },
+ {
+ id: 'a21cf2e804acc3c60d07e37d75e395f5a9a4d044',
+ reply_id: 'a21cf2e804acc3c60d07e37d75e395f5a9a4d044',
+ expanded: true,
+ notes: [
+ {
+ id: 905,
+ type: null,
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ created_at: '2018-05-29T12:06:28.851Z',
+ updated_at: '2018-05-29T12:06:28.851Z',
+ system: true,
+ noteable_id: 182,
+ noteable_type: 'Issue',
+ resolvable: false,
+ noteable_iid: 12,
+ note: 'changed the description',
+ note_html: '<p dir="auto">changed the description</p>',
+ current_user: { can_edit: false, can_award_emoji: true },
+ resolved: false,
+ resolved_by: null,
+ system_note_icon_name: 'pencil-square',
+ discussion_id: 'a21cf2e804acc3c60d07e37d75e395f5a9a4d044',
+ emoji_awardable: false,
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_905&user_id=1',
+ human_access: 'Owner',
+ path: '/gitlab-org/gitlab-shell/notes/905',
+ },
+ ],
+ individual_note: true,
+ resolvable: false,
+ resolved: false,
+ diff_discussion: false,
+ },
+ {
+ id: '70411b08cdfc01f24187a06d77daa33464cb2620',
+ reply_id: '70411b08cdfc01f24187a06d77daa33464cb2620',
+ expanded: true,
+ notes: [
+ {
+ id: 906,
+ type: null,
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ created_at: '2018-05-29T12:20:02.925Z',
+ updated_at: '2018-05-29T12:20:02.925Z',
+ system: true,
+ noteable_id: 182,
+ noteable_type: 'Issue',
+ resolvable: false,
+ noteable_iid: 12,
+ note: 'changed the description',
+ note_html: '<p dir="auto">changed the description</p>',
+ current_user: { can_edit: false, can_award_emoji: true },
+ resolved: false,
+ resolved_by: null,
+ system_note_icon_name: 'pencil-square',
+ discussion_id: '70411b08cdfc01f24187a06d77daa33464cb2620',
+ emoji_awardable: false,
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_906&user_id=1',
+ human_access: 'Owner',
+ path: '/gitlab-org/gitlab-shell/notes/906',
+ },
+ ],
+ individual_note: true,
+ resolvable: false,
+ resolved: false,
+ diff_discussion: false,
+ },
+];
+
+export const collapsedSystemNotes = [
+ {
+ id: '39b271c2033e9ed43d8edb393702f65f7a830459',
+ reply_id: '39b271c2033e9ed43d8edb393702f65f7a830459',
+ expanded: true,
+ notes: [
+ {
+ id: 901,
+ type: null,
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ created_at: '2018-05-29T12:05:36.117Z',
+ updated_at: '2018-05-29T12:05:36.117Z',
+ system: false,
+ noteable_id: 182,
+ noteable_type: 'Issue',
+ resolvable: false,
+ noteable_iid: 12,
+ note:
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
+ note_html:
+ '<p dir="auto">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>',
+ current_user: { can_edit: true, can_award_emoji: true },
+ resolved: false,
+ resolved_by: null,
+ discussion_id: '39b271c2033e9ed43d8edb393702f65f7a830459',
+ emoji_awardable: true,
+ award_emoji: [],
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_901&user_id=1',
+ human_access: 'Owner',
+ toggle_award_path: '/gitlab-org/gitlab-shell/notes/901/toggle_award_emoji',
+ path: '/gitlab-org/gitlab-shell/notes/901',
+ },
+ ],
+ individual_note: true,
+ resolvable: false,
+ resolved: false,
+ diff_discussion: false,
+ },
+ {
+ id: '4852335d7dc40b9ceb8fde1a2bb9c1b67e4c7795',
+ reply_id: '4852335d7dc40b9ceb8fde1a2bb9c1b67e4c7795',
+ expanded: true,
+ notes: [
+ {
+ id: 902,
+ type: null,
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ created_at: '2018-05-29T12:05:58.694Z',
+ updated_at: '2018-05-29T12:05:58.694Z',
+ system: false,
+ noteable_id: 182,
+ noteable_type: 'Issue',
+ resolvable: false,
+ noteable_iid: 12,
+ note:
+ 'Varius vel pharetra vel turpis nunc eget lorem. Ipsum dolor sit amet consectetur adipiscing.',
+ note_html:
+ '<p dir="auto">Varius vel pharetra vel turpis nunc eget lorem. Ipsum dolor sit amet consectetur adipiscing.</p>',
+ current_user: { can_edit: true, can_award_emoji: true },
+ resolved: false,
+ resolved_by: null,
+ discussion_id: '4852335d7dc40b9ceb8fde1a2bb9c1b67e4c7795',
+ emoji_awardable: true,
+ award_emoji: [],
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_902&user_id=1',
+ human_access: 'Owner',
+ toggle_award_path: '/gitlab-org/gitlab-shell/notes/902/toggle_award_emoji',
+ path: '/gitlab-org/gitlab-shell/notes/902',
+ },
+ ],
+ individual_note: true,
+ resolvable: false,
+ resolved: false,
+ diff_discussion: false,
+ },
+ {
+ id: '091865fe3ae20f0045234a3d103e3b15e73405b5',
+ reply_id: '091865fe3ae20f0045234a3d103e3b15e73405b5',
+ expanded: true,
+ notes: [
+ {
+ id: 904,
+ type: null,
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ created_at: '2018-05-29T12:06:16.112Z',
+ updated_at: '2018-05-29T12:06:16.112Z',
+ system: false,
+ noteable_id: 182,
+ noteable_type: 'Issue',
+ resolvable: false,
+ noteable_iid: 12,
+ note: 'Ullamcorper eget nulla facilisi etiam',
+ note_html: '<p dir="auto">Ullamcorper eget nulla facilisi etiam</p>',
+ current_user: { can_edit: true, can_award_emoji: true },
+ resolved: false,
+ resolved_by: null,
+ discussion_id: '091865fe3ae20f0045234a3d103e3b15e73405b5',
+ emoji_awardable: true,
+ award_emoji: [],
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_904&user_id=1',
+ human_access: 'Owner',
+ toggle_award_path: '/gitlab-org/gitlab-shell/notes/904/toggle_award_emoji',
+ path: '/gitlab-org/gitlab-shell/notes/904',
+ },
+ ],
+ individual_note: true,
+ resolvable: false,
+ resolved: false,
+ diff_discussion: false,
+ },
+ {
+ id: 'a21cf2e804acc3c60d07e37d75e395f5a9a4d044',
+ reply_id: 'a21cf2e804acc3c60d07e37d75e395f5a9a4d044',
+ expanded: true,
+ notes: [
+ {
+ id: 905,
+ type: null,
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ created_at: '2018-05-29T12:06:28.851Z',
+ updated_at: '2018-05-29T12:06:28.851Z',
+ system: true,
+ noteable_id: 182,
+ noteable_type: 'Issue',
+ resolvable: false,
+ noteable_iid: 12,
+ note: 'changed the description',
+ note_html: '\n <p dir="auto">changed the description 2 times within 1 minute </p>',
+ current_user: { can_edit: false, can_award_emoji: true },
+ resolved: false,
+ resolved_by: null,
+ system_note_icon_name: 'pencil-square',
+ discussion_id: 'a21cf2e804acc3c60d07e37d75e395f5a9a4d044',
+ emoji_awardable: false,
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_905&user_id=1',
+ human_access: 'Owner',
+ path: '/gitlab-org/gitlab-shell/notes/905',
+ times_updated: 2,
+ },
+ ],
+ individual_note: true,
+ resolvable: false,
+ resolved: false,
+ diff_discussion: false,
+ },
+ {
+ id: '70411b08cdfc01f24187a06d77daa33464cb2620',
+ reply_id: '70411b08cdfc01f24187a06d77daa33464cb2620',
+ expanded: true,
+ notes: [
+ {
+ id: 906,
+ type: null,
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ created_at: '2018-05-29T12:20:02.925Z',
+ updated_at: '2018-05-29T12:20:02.925Z',
+ system: true,
+ noteable_id: 182,
+ noteable_type: 'Issue',
+ resolvable: false,
+ noteable_iid: 12,
+ note: 'changed the description',
+ note_html: '<p dir="auto">changed the description</p>',
+ current_user: { can_edit: false, can_award_emoji: true },
+ resolved: false,
+ resolved_by: null,
+ system_note_icon_name: 'pencil-square',
+ discussion_id: '70411b08cdfc01f24187a06d77daa33464cb2620',
+ emoji_awardable: false,
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_906&user_id=1',
+ human_access: 'Owner',
+ path: '/gitlab-org/gitlab-shell/notes/906',
+ },
+ ],
+ individual_note: true,
+ resolvable: false,
+ resolved: false,
+ diff_discussion: false,
+ },
+];
diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js
index 520a25cc5c6..b66e8e1ceb3 100644
--- a/spec/javascripts/notes/stores/actions_spec.js
+++ b/spec/javascripts/notes/stores/actions_spec.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import _ from 'underscore';
import { headersInterceptor } from 'spec/helpers/vue_resource_helper';
import * as actions from '~/notes/stores/actions';
-import store from '~/notes/stores';
+import createStore from '~/notes/stores';
import testAction from '../../helpers/vuex_action_helper';
import { resetStore } from '../helpers';
import {
@@ -14,6 +14,12 @@ import {
} from '../mock_data';
describe('Actions Notes Store', () => {
+ let store;
+
+ beforeEach(() => {
+ store = createStore();
+ });
+
afterEach(() => {
resetStore(store);
});
@@ -76,7 +82,7 @@ describe('Actions Notes Store', () => {
actions.setInitialNotes,
[individualNote],
{ notes: [] },
- [{ type: 'SET_INITIAL_NOTES', payload: [individualNote] }],
+ [{ type: 'SET_INITIAL_DISCUSSIONS', payload: [individualNote] }],
[],
done,
);
@@ -109,6 +115,32 @@ describe('Actions Notes Store', () => {
});
});
+ describe('expandDiscussion', () => {
+ it('should expand discussion', done => {
+ testAction(
+ actions.expandDiscussion,
+ { discussionId: discussionMock.id },
+ { notes: [discussionMock] },
+ [{ type: 'EXPAND_DISCUSSION', payload: { discussionId: discussionMock.id } }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('collapseDiscussion', () => {
+ it('should commit collapse discussion', done => {
+ testAction(
+ actions.collapseDiscussion,
+ { discussionId: discussionMock.id },
+ { notes: [discussionMock] },
+ [{ type: 'COLLAPSE_DISCUSSION', payload: { discussionId: discussionMock.id } }],
+ [],
+ done,
+ );
+ });
+ });
+
describe('async methods', () => {
const interceptor = (request, next) => {
next(
@@ -194,7 +226,14 @@ describe('Actions Notes Store', () => {
});
it('sets issue state as reopened', done => {
- testAction(actions.toggleIssueLocalState, 'reopened', {}, [{ type: 'REOPEN_ISSUE' }], [], done);
+ testAction(
+ actions.toggleIssueLocalState,
+ 'reopened',
+ {},
+ [{ type: 'REOPEN_ISSUE' }],
+ [],
+ done,
+ );
});
});
@@ -239,13 +278,7 @@ describe('Actions Notes Store', () => {
.dispatch('poll')
.then(() => new Promise(resolve => requestAnimationFrame(resolve)))
.then(() => {
- expect(Vue.http.get).toHaveBeenCalledWith(jasmine.anything(), {
- url: jasmine.anything(),
- method: 'get',
- headers: {
- 'X-Last-Fetched-At': undefined,
- },
- });
+ expect(Vue.http.get).toHaveBeenCalled();
expect(store.state.lastFetchedAt).toBe('123456');
jasmine.clock().tick(1500);
@@ -271,4 +304,17 @@ describe('Actions Notes Store', () => {
.catch(done.fail);
});
});
+
+ describe('setNotesFetchedState', () => {
+ it('should set notes fetched state', done => {
+ testAction(
+ actions.setNotesFetchedState,
+ true,
+ {},
+ [{ type: 'SET_NOTES_FETCHED_STATE', payload: true }],
+ [],
+ done,
+ );
+ });
+ });
});
diff --git a/spec/javascripts/notes/stores/collapse_utils_spec.js b/spec/javascripts/notes/stores/collapse_utils_spec.js
new file mode 100644
index 00000000000..06a6aab932a
--- /dev/null
+++ b/spec/javascripts/notes/stores/collapse_utils_spec.js
@@ -0,0 +1,46 @@
+import {
+ isDescriptionSystemNote,
+ changeDescriptionNote,
+ getTimeDifferenceMinutes,
+ collapseSystemNotes,
+} from '~/notes/stores/collapse_utils';
+import {
+ notesWithDescriptionChanges,
+ collapsedSystemNotes,
+} from '../mock_data';
+
+describe('Collapse utils', () => {
+ const mockSystemNote = {
+ note: 'changed the description',
+ note_html: '<p dir="auto">changed the description</p>',
+ system: true,
+ created_at: '2018-05-14T21:28:00.000Z',
+ };
+
+ it('checks if a system note is of a description type', () => {
+ expect(isDescriptionSystemNote(mockSystemNote)).toEqual(true);
+ });
+
+ it('returns false when a system note is not a description type', () => {
+ expect(isDescriptionSystemNote(Object.assign({}, mockSystemNote, { note: 'foo' }))).toEqual(false);
+ });
+
+ it('changes the description to contain the number of changed times', () => {
+ const changedNote = changeDescriptionNote(mockSystemNote, 3, 5);
+
+ expect(changedNote.times_updated).toEqual(3);
+ expect(changedNote.note_html.trim()).toContain('<p dir="auto">changed the description 3 times within 5 minutes </p>');
+ });
+
+ it('gets the time difference between two notes', () => {
+ const anotherSystemNote = {
+ created_at: '2018-05-14T21:33:00.000Z',
+ };
+
+ expect(getTimeDifferenceMinutes(mockSystemNote, anotherSystemNote)).toEqual(5);
+ });
+
+ it('collapses all description system notes made within 10 minutes or less from each other', () => {
+ expect(collapseSystemNotes(notesWithDescriptionChanges)).toEqual(collapsedSystemNotes);
+ });
+});
diff --git a/spec/javascripts/notes/stores/getters_spec.js b/spec/javascripts/notes/stores/getters_spec.js
index 8b2a8d2cd7a..41599e00122 100644
--- a/spec/javascripts/notes/stores/getters_spec.js
+++ b/spec/javascripts/notes/stores/getters_spec.js
@@ -1,22 +1,64 @@
import * as getters from '~/notes/stores/getters';
-import { notesDataMock, userDataMock, noteableDataMock, individualNote } from '../mock_data';
+import {
+ notesDataMock,
+ userDataMock,
+ noteableDataMock,
+ individualNote,
+ collapseNotesMock,
+} from '../mock_data';
+
+const discussionWithTwoUnresolvedNotes = 'merge_requests/resolved_diff_discussion.json';
describe('Getters Notes Store', () => {
let state;
+
+ preloadFixtures(discussionWithTwoUnresolvedNotes);
+
beforeEach(() => {
state = {
- notes: [individualNote],
+ discussions: [individualNote],
targetNoteHash: 'hash',
lastFetchedAt: 'timestamp',
+ isNotesFetched: false,
notesData: notesDataMock,
userData: userDataMock,
noteableData: noteableDataMock,
};
});
- describe('notes', () => {
- it('should return all notes in the store', () => {
- expect(getters.notes(state)).toEqual([individualNote]);
+
+ describe('discussions', () => {
+ it('should return all discussions in the store', () => {
+ expect(getters.discussions(state)).toEqual([individualNote]);
+ });
+ });
+
+ describe('resolvedDiscussionsById', () => {
+ it('ignores unresolved system notes', () => {
+ const [discussion] = getJSONFixture(discussionWithTwoUnresolvedNotes);
+ discussion.notes[0].resolved = true;
+ discussion.notes[1].resolved = false;
+ state.discussions.push(discussion);
+
+ expect(getters.resolvedDiscussionsById(state)).toEqual({
+ [discussion.id]: discussion,
+ });
+ });
+ });
+
+ describe('Collapsed notes', () => {
+ const stateCollapsedNotes = {
+ discussions: collapseNotesMock,
+ targetNoteHash: 'hash',
+ lastFetchedAt: 'timestamp',
+
+ notesData: notesDataMock,
+ userData: userDataMock,
+ noteableData: noteableDataMock,
+ };
+
+ it('should return a single system note when a description was updated multiple times', () => {
+ expect(getters.discussions(stateCollapsedNotes).length).toEqual(1);
});
});
@@ -61,4 +103,10 @@ describe('Getters Notes Store', () => {
expect(getters.openState(state)).toEqual(noteableDataMock.state);
});
});
+
+ describe('isNotesFetched', () => {
+ it('should return the state for the fetching notes', () => {
+ expect(getters.isNotesFetched(state)).toBeFalsy();
+ });
+ });
});
diff --git a/spec/javascripts/notes/stores/mutation_spec.js b/spec/javascripts/notes/stores/mutation_spec.js
index 98f101d6bc5..a15ff1a5888 100644
--- a/spec/javascripts/notes/stores/mutation_spec.js
+++ b/spec/javascripts/notes/stores/mutation_spec.js
@@ -1,5 +1,12 @@
import mutations from '~/notes/stores/mutations';
-import { note, discussionMock, notesDataMock, userDataMock, noteableDataMock, individualNote } from '../mock_data';
+import {
+ note,
+ discussionMock,
+ notesDataMock,
+ userDataMock,
+ noteableDataMock,
+ individualNote,
+} from '../mock_data';
describe('Notes Store mutations', () => {
describe('ADD_NEW_NOTE', () => {
@@ -7,7 +14,7 @@ describe('Notes Store mutations', () => {
let noteData;
beforeEach(() => {
- state = { notes: [] };
+ state = { discussions: [] };
noteData = {
expanded: true,
id: note.discussion_id,
@@ -20,46 +27,74 @@ describe('Notes Store mutations', () => {
it('should add a new note to an array of notes', () => {
expect(state).toEqual({
- notes: [noteData],
+ discussions: [noteData],
});
- expect(state.notes.length).toBe(1);
+ expect(state.discussions.length).toBe(1);
});
it('should not add the same note to the notes array', () => {
mutations.ADD_NEW_NOTE(state, note);
- expect(state.notes.length).toBe(1);
+ expect(state.discussions.length).toBe(1);
});
});
describe('ADD_NEW_REPLY_TO_DISCUSSION', () => {
it('should add a reply to a specific discussion', () => {
- const state = { notes: [discussionMock] };
+ const state = { discussions: [discussionMock] };
const newReply = Object.assign({}, note, { discussion_id: discussionMock.id });
mutations.ADD_NEW_REPLY_TO_DISCUSSION(state, newReply);
- expect(state.notes[0].notes.length).toEqual(4);
+ expect(state.discussions[0].notes.length).toEqual(4);
});
});
describe('DELETE_NOTE', () => {
it('should delete a note ', () => {
- const state = { notes: [discussionMock] };
+ const state = { discussions: [discussionMock] };
const toDelete = discussionMock.notes[0];
const lengthBefore = discussionMock.notes.length;
mutations.DELETE_NOTE(state, toDelete);
- expect(state.notes[0].notes.length).toEqual(lengthBefore - 1);
+ expect(state.discussions[0].notes.length).toEqual(lengthBefore - 1);
+ });
+ });
+
+ describe('EXPAND_DISCUSSION', () => {
+ it('should expand a collapsed discussion', () => {
+ const discussion = Object.assign({}, discussionMock, { expanded: false });
+
+ const state = {
+ discussions: [discussion],
+ };
+
+ mutations.EXPAND_DISCUSSION(state, { discussionId: discussion.id });
+
+ expect(state.discussions[0].expanded).toEqual(true);
+ });
+ });
+
+ describe('COLLAPSE_DISCUSSION', () => {
+ it('should collpase an expanded discussion', () => {
+ const discussion = Object.assign({}, discussionMock, { expanded: true });
+
+ const state = {
+ discussions: [discussion],
+ };
+
+ mutations.COLLAPSE_DISCUSSION(state, { discussionId: discussion.id });
+
+ expect(state.discussions[0].expanded).toEqual(false);
});
});
describe('REMOVE_PLACEHOLDER_NOTES', () => {
it('should remove all placeholder notes in indivudal notes and discussion', () => {
const placeholderNote = Object.assign({}, individualNote, { isPlaceholderNote: true });
- const state = { notes: [placeholderNote] };
+ const state = { discussions: [placeholderNote] };
mutations.REMOVE_PLACEHOLDER_NOTES(state);
- expect(state.notes).toEqual([]);
+ expect(state.discussions).toEqual([]);
});
});
@@ -96,26 +131,29 @@ describe('Notes Store mutations', () => {
});
});
- describe('SET_INITIAL_NOTES', () => {
+ describe('SET_INITIAL_DISCUSSIONS', () => {
it('should set the initial notes received', () => {
const state = {
- notes: [],
+ discussions: [],
};
const legacyNote = {
id: 2,
individual_note: true,
- notes: [{
- note: '1',
- }, {
- note: '2',
- }],
+ notes: [
+ {
+ note: '1',
+ },
+ {
+ note: '2',
+ },
+ ],
};
- mutations.SET_INITIAL_NOTES(state, [note, legacyNote]);
- expect(state.notes[0].id).toEqual(note.id);
- expect(state.notes[1].notes[0].note).toBe(legacyNote.notes[0].note);
- expect(state.notes[2].notes[0].note).toBe(legacyNote.notes[1].note);
- expect(state.notes.length).toEqual(3);
+ mutations.SET_INITIAL_DISCUSSIONS(state, [note, legacyNote]);
+ expect(state.discussions[0].id).toEqual(note.id);
+ expect(state.discussions[1].notes[0].note).toBe(legacyNote.notes[0].note);
+ expect(state.discussions[2].notes[0].note).toBe(legacyNote.notes[1].note);
+ expect(state.discussions.length).toEqual(3);
});
});
@@ -144,17 +182,17 @@ describe('Notes Store mutations', () => {
describe('SHOW_PLACEHOLDER_NOTE', () => {
it('should set a placeholder note', () => {
const state = {
- notes: [],
+ discussions: [],
};
mutations.SHOW_PLACEHOLDER_NOTE(state, note);
- expect(state.notes[0].isPlaceholderNote).toEqual(true);
+ expect(state.discussions[0].isPlaceholderNote).toEqual(true);
});
});
describe('TOGGLE_AWARD', () => {
it('should add award if user has not reacted yet', () => {
const state = {
- notes: [note],
+ discussions: [note],
userData: userDataMock,
};
@@ -164,9 +202,9 @@ describe('Notes Store mutations', () => {
};
mutations.TOGGLE_AWARD(state, data);
- const lastIndex = state.notes[0].award_emoji.length - 1;
+ const lastIndex = state.discussions[0].award_emoji.length - 1;
- expect(state.notes[0].award_emoji[lastIndex]).toEqual({
+ expect(state.discussions[0].award_emoji[lastIndex]).toEqual({
name: 'cartwheel',
user: { id: userDataMock.id, name: userDataMock.name, username: userDataMock.username },
});
@@ -174,7 +212,7 @@ describe('Notes Store mutations', () => {
it('should remove award if user already reacted', () => {
const state = {
- notes: [note],
+ discussions: [note],
userData: {
id: 1,
name: 'Administrator',
@@ -187,7 +225,7 @@ describe('Notes Store mutations', () => {
awardName: 'bath_tone3',
};
mutations.TOGGLE_AWARD(state, data);
- expect(state.notes[0].award_emoji.length).toEqual(2);
+ expect(state.discussions[0].award_emoji.length).toEqual(2);
});
});
@@ -196,43 +234,43 @@ describe('Notes Store mutations', () => {
const discussion = Object.assign({}, discussionMock, { expanded: false });
const state = {
- notes: [discussion],
+ discussions: [discussion],
};
mutations.TOGGLE_DISCUSSION(state, { discussionId: discussion.id });
- expect(state.notes[0].expanded).toEqual(true);
+ expect(state.discussions[0].expanded).toEqual(true);
});
it('should close a opened discussion', () => {
const state = {
- notes: [discussionMock],
+ discussions: [discussionMock],
};
mutations.TOGGLE_DISCUSSION(state, { discussionId: discussionMock.id });
- expect(state.notes[0].expanded).toEqual(false);
+ expect(state.discussions[0].expanded).toEqual(false);
});
});
describe('UPDATE_NOTE', () => {
it('should update a note', () => {
const state = {
- notes: [individualNote],
+ discussions: [individualNote],
};
const updated = Object.assign({}, individualNote.notes[0], { note: 'Foo' });
mutations.UPDATE_NOTE(state, updated);
- expect(state.notes[0].notes[0].note).toEqual('Foo');
+ expect(state.discussions[0].notes[0].note).toEqual('Foo');
});
});
describe('CLOSE_ISSUE', () => {
it('should set issue as closed', () => {
const state = {
- notes: [],
+ discussions: [],
targetNoteHash: null,
lastFetchedAt: null,
isToggleStateButtonLoading: false,
@@ -249,7 +287,7 @@ describe('Notes Store mutations', () => {
describe('REOPEN_ISSUE', () => {
it('should set issue as closed', () => {
const state = {
- notes: [],
+ discussions: [],
targetNoteHash: null,
lastFetchedAt: null,
isToggleStateButtonLoading: false,
@@ -266,7 +304,7 @@ describe('Notes Store mutations', () => {
describe('TOGGLE_STATE_BUTTON_LOADING', () => {
it('should set isToggleStateButtonLoading as true', () => {
const state = {
- notes: [],
+ discussions: [],
targetNoteHash: null,
lastFetchedAt: null,
isToggleStateButtonLoading: false,
@@ -281,7 +319,7 @@ describe('Notes Store mutations', () => {
it('should set isToggleStateButtonLoading as false', () => {
const state = {
- notes: [],
+ discussions: [],
targetNoteHash: null,
lastFetchedAt: null,
isToggleStateButtonLoading: true,
@@ -294,4 +332,15 @@ describe('Notes Store mutations', () => {
expect(state.isToggleStateButtonLoading).toEqual(false);
});
});
+
+ describe('SET_NOTES_FETCHING_STATE', () => {
+ it('should set the given state', () => {
+ const state = {
+ isNotesFetched: false,
+ };
+
+ mutations.SET_NOTES_FETCHED_STATE(state, true);
+ expect(state.isNotesFetched).toEqual(true);
+ });
+ });
});
diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
index 0952356c2f4..faeedae40e9 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/javascripts/notes_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable space-before-function-paren, no-unused-expressions, no-var, object-shorthand, comma-dangle, max-len */
+/* eslint-disable no-unused-expressions, no-var, object-shorthand */
import $ from 'jquery';
import _ from 'underscore';
import MockAdapter from 'axios-mock-adapter';
@@ -35,11 +35,11 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
describe('Notes', function() {
const FLASH_TYPE_ALERT = 'alert';
const NOTES_POST_PATH = /(.*)\/notes\?html=true$/;
- var commentsTemplate = 'merge_requests/merge_request_with_comment.html.raw';
- preloadFixtures(commentsTemplate);
+ var fixture = 'snippets/show.html.raw';
+ preloadFixtures(fixture);
beforeEach(function() {
- loadFixtures(commentsTemplate);
+ loadFixtures(fixture);
gl.utils.disableButtonIfEmptyField = _.noop;
window.project_uploads_path = 'http://test.host/uploads';
$('body').attr('data-page', 'projects:merge_requets:show');
@@ -65,16 +65,9 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
let mock;
beforeEach(function() {
- spyOn(axios, 'patch').and.callThrough();
+ spyOn(axios, 'patch').and.callFake(() => new Promise(() => {}));
mock = new MockAdapter(axios);
-
- mock
- .onPatch(
- `${
- gl.TEST_HOST
- }/frontend-fixtures/merge-requests-project/merge_requests/1.json`,
- )
- .reply(200, {});
+ mock.onAny().reply(200, {});
$('.js-comment-button').on('click', function(e) {
e.preventDefault();
@@ -90,26 +83,17 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
const changeEvent = document.createEvent('HTMLEvents');
changeEvent.initEvent('change', true, true);
$('input[type=checkbox]')
- .attr('checked', true)[1]
+ .attr('checked', true)[0]
.dispatchEvent(changeEvent);
- expect($('.js-task-list-field.original-task-list').val()).toBe(
- '- [x] Task List Item',
- );
+ expect($('.js-task-list-field.original-task-list').val()).toBe('- [x] Task List Item');
});
it('submits an ajax request on tasklist:changed', function(done) {
$('.js-task-list-container').trigger('tasklist:changed');
setTimeout(() => {
- expect(axios.patch).toHaveBeenCalledWith(
- `${
- gl.TEST_HOST
- }/frontend-fixtures/merge-requests-project/merge_requests/1.json`,
- {
- note: { note: '' },
- },
- );
+ expect(axios.patch).toHaveBeenCalled();
done();
});
});
@@ -200,9 +184,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
updatedNote.note = 'bar';
this.notes.updateNote(updatedNote, $targetNote);
- expect(this.notes.revertNoteEditForm).toHaveBeenCalledWith(
- $targetNote,
- );
+ expect(this.notes.revertNoteEditForm).toHaveBeenCalledWith($targetNote);
expect(this.notes.setupNewNote).toHaveBeenCalled();
done();
@@ -282,10 +264,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
Notes.isNewNote.and.returnValue(true);
Notes.prototype.renderNote.call(notes, note, null, $notesList);
- expect(Notes.animateAppendNote).toHaveBeenCalledWith(
- note.html,
- $notesList,
- );
+ expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.html, $notesList);
});
});
@@ -300,10 +279,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
Notes.prototype.renderNote.call(notes, note, null, $notesList);
- expect(Notes.animateUpdateNote).toHaveBeenCalledWith(
- note.html,
- $note,
- );
+ expect(Notes.animateUpdateNote).toHaveBeenCalledWith(note.html, $note);
expect(notes.setupNewNote).toHaveBeenCalledWith($newNote);
});
@@ -331,10 +307,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
$notesList.find.and.returnValue($note);
Notes.prototype.renderNote.call(notes, note, null, $notesList);
- expect(notes.putConflictEditWarningInPlace).toHaveBeenCalledWith(
- note,
- $note,
- );
+ expect(notes.putConflictEditWarningInPlace).toHaveBeenCalledWith(note, $note);
});
});
});
@@ -400,10 +373,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
$form.length = 1;
row = jasmine.createSpyObj('row', ['prevAll', 'first', 'find']);
- notes = jasmine.createSpyObj('notes', [
- 'isParallelView',
- 'updateNotesCount',
- ]);
+ notes = jasmine.createSpyObj('notes', ['isParallelView', 'updateNotesCount']);
notes.note_ids = [];
spyOn(Notes, 'isNewNote');
@@ -464,10 +434,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
});
it('should call Notes.animateAppendNote', () => {
- expect(Notes.animateAppendNote).toHaveBeenCalledWith(
- note.html,
- discussionContainer,
- );
+ expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.html, discussionContainer);
});
});
});
@@ -571,9 +538,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
mockNotesPost();
$('.js-comment-button').click();
- expect($notesContainer.find('.note.being-posted').length > 0).toEqual(
- true,
- );
+ expect($notesContainer.find('.note.being-posted').length > 0).toEqual(true);
});
it('should remove placeholder note when new comment is done posting', done => {
@@ -617,9 +582,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
$('.js-comment-button').click();
setTimeout(() => {
- expect($notesContainer.find(`#note_${note.id}`).length > 0).toEqual(
- true,
- );
+ expect($notesContainer.find(`#note_${note.id}`).length > 0).toEqual(true);
done();
});
@@ -734,14 +697,10 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
spyOn(gl.awardsHandler, 'addAwardToEmojiBar').and.callThrough();
$('.js-comment-button').click();
- expect(
- $notesContainer.find('.system-note.being-posted').length,
- ).toEqual(1); // Placeholder shown
+ expect($notesContainer.find('.system-note.being-posted').length).toEqual(1); // Placeholder shown
setTimeout(() => {
- expect(
- $notesContainer.find('.system-note.being-posted').length,
- ).toEqual(0); // Placeholder removed
+ expect($notesContainer.find('.system-note.being-posted').length).toEqual(0); // Placeholder removed
done();
});
});
@@ -815,9 +774,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
it('should return form metadata object from form reference', () => {
$form.find('textarea.js-note-text').val(sampleComment);
- const { formData, formContent, formAction } = this.notes.getFormData(
- $form,
- );
+ const { formData, formContent, formAction } = this.notes.getFormData($form);
expect(formData.indexOf(sampleComment) > -1).toBe(true);
expect(formContent).toEqual(sampleComment);
@@ -833,9 +790,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
const { formContent } = this.notes.getFormData($form);
expect(_.escape).toHaveBeenCalledWith(sampleComment);
- expect(formContent).toEqual(
- '&lt;script&gt;alert(&quot;Boom!&quot;);&lt;/script&gt;',
- );
+ expect(formContent).toEqual('&lt;script&gt;alert(&quot;Boom!&quot;);&lt;/script&gt;');
});
});
@@ -845,8 +800,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
});
it('should return true when comment begins with a quick action', () => {
- const sampleComment =
- '/wip\n/milestone %1.0\n/merge\n/unassign Merging this';
+ const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign Merging this';
const hasQuickActions = this.notes.hasQuickActions(sampleComment);
expect(hasQuickActions).toBeTruthy();
@@ -870,8 +824,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
describe('stripQuickActions', () => {
it('should strip quick actions from the comment which begins with a quick action', () => {
this.notes = new Notes();
- const sampleComment =
- '/wip\n/milestone %1.0\n/merge\n/unassign Merging this';
+ const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign Merging this';
const stripedComment = this.notes.stripQuickActions(sampleComment);
expect(stripedComment).toBe('');
@@ -879,8 +832,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
it('should strip quick actions from the comment but leaves plain comment if it is present', () => {
this.notes = new Notes();
- const sampleComment =
- '/wip\n/milestone %1.0\n/merge\n/unassign\nMerging this';
+ const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign\nMerging this';
const stripedComment = this.notes.stripQuickActions(sampleComment);
expect(stripedComment).toBe('Merging this');
@@ -888,8 +840,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
it('should NOT strip string that has slashes within', () => {
this.notes = new Notes();
- const sampleComment =
- 'http://127.0.0.1:3000/root/gitlab-shell/issues/1';
+ const sampleComment = 'http://127.0.0.1:3000/root/gitlab-shell/issues/1';
const stripedComment = this.notes.stripQuickActions(sampleComment);
expect(stripedComment).toBe(sampleComment);
@@ -909,29 +860,21 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
it('should return executing quick action description when note has single quick action', () => {
const sampleComment = '/close';
- expect(
- this.notes.getQuickActionDescription(
- sampleComment,
- availableQuickActions,
- ),
- ).toBe('Applying command to close this issue');
+ expect(this.notes.getQuickActionDescription(sampleComment, availableQuickActions)).toBe(
+ 'Applying command to close this issue',
+ );
});
it('should return generic multiple quick action description when note has multiple quick actions', () => {
const sampleComment = '/close\n/title [Duplicate] Issue foobar';
- expect(
- this.notes.getQuickActionDescription(
- sampleComment,
- availableQuickActions,
- ),
- ).toBe('Applying multiple commands');
+ expect(this.notes.getQuickActionDescription(sampleComment, availableQuickActions)).toBe(
+ 'Applying multiple commands',
+ );
});
it('should return generic quick action description when available quick actions list is not populated', () => {
const sampleComment = '/close\n/title [Duplicate] Issue foobar';
- expect(this.notes.getQuickActionDescription(sampleComment)).toBe(
- 'Applying command',
- );
+ expect(this.notes.getQuickActionDescription(sampleComment)).toBe('Applying command');
});
});
@@ -961,20 +904,14 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
expect($tempNote.attr('id')).toEqual(uniqueId);
expect($tempNote.hasClass('being-posted')).toBeTruthy();
expect($tempNote.hasClass('fade-in-half')).toBeTruthy();
- $tempNote
- .find('.timeline-icon > a, .note-header-info > a')
- .each(function() {
- expect($(this).attr('href')).toEqual(`/${currentUsername}`);
- });
- expect($tempNote.find('.timeline-icon .avatar').attr('src')).toEqual(
- currentUserAvatar,
- );
- expect(
- $tempNote.find('.timeline-content').hasClass('discussion'),
- ).toBeFalsy();
+ $tempNote.find('.timeline-icon > a, .note-header-info > a').each(function() {
+ expect($(this).attr('href')).toEqual(`/${currentUsername}`);
+ });
+ expect($tempNote.find('.timeline-icon .avatar').attr('src')).toEqual(currentUserAvatar);
+ expect($tempNote.find('.timeline-content').hasClass('discussion')).toBeFalsy();
expect(
$tempNoteHeader
- .find('.hidden-xs')
+ .find('.d-none.d-sm-inline-block')
.text()
.trim(),
).toEqual(currentUserFullname);
@@ -1002,9 +939,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
});
expect($tempNote.prop('nodeName')).toEqual('LI');
- expect(
- $tempNote.find('.timeline-content').hasClass('discussion'),
- ).toBeTruthy();
+ expect($tempNote.find('.timeline-content').hasClass('discussion')).toBeTruthy();
});
it('should return a escaped user name', () => {
@@ -1020,7 +955,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
const $tempNoteHeader = $tempNote.find('.note-header');
expect(
$tempNoteHeader
- .find('.hidden-xs')
+ .find('.d-none.d-sm-inline-block')
.text()
.trim(),
).toEqual('Foo &lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;');
@@ -1061,11 +996,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
});
it('shows a flash message', () => {
- this.notes.addFlash(
- 'Error message',
- FLASH_TYPE_ALERT,
- this.notes.parentTimeline.get(0),
- );
+ this.notes.addFlash('Error message', FLASH_TYPE_ALERT, this.notes.parentTimeline.get(0));
expect($('.flash-alert').is(':visible')).toBeTruthy();
});
@@ -1078,11 +1009,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
});
it('hides visible flash message', () => {
- this.notes.addFlash(
- 'Error message 1',
- FLASH_TYPE_ALERT,
- this.notes.parentTimeline.get(0),
- );
+ this.notes.addFlash('Error message 1', FLASH_TYPE_ALERT, this.notes.parentTimeline.get(0));
this.notes.clearFlash();
diff --git a/spec/javascripts/pdf/index_spec.js b/spec/javascripts/pdf/index_spec.js
index bebed432f91..69230bb0937 100644
--- a/spec/javascripts/pdf/index_spec.js
+++ b/spec/javascripts/pdf/index_spec.js
@@ -1,5 +1,3 @@
-/* eslint-disable import/no-unresolved */
-
import Vue from 'vue';
import { PDFJS } from 'vendor/pdf';
import workerSrc from 'vendor/pdf.worker.min';
diff --git a/spec/javascripts/pdf/page_spec.js b/spec/javascripts/pdf/page_spec.js
index ac5b21e8f6c..9c686748c10 100644
--- a/spec/javascripts/pdf/page_spec.js
+++ b/spec/javascripts/pdf/page_spec.js
@@ -1,5 +1,3 @@
-/* eslint-disable import/no-unresolved */
-
import Vue from 'vue';
import pdfjsLib from 'vendor/pdf';
import workerSrc from 'vendor/pdf.worker.min';
diff --git a/spec/javascripts/pipelines/empty_state_spec.js b/spec/javascripts/pipelines/empty_state_spec.js
index 71f77e5f42e..1e41a7bfe7c 100644
--- a/spec/javascripts/pipelines/empty_state_spec.js
+++ b/spec/javascripts/pipelines/empty_state_spec.js
@@ -29,7 +29,7 @@ describe('Pipelines Empty State', () => {
expect(
component.$el.querySelector('p').innerHTML.trim().replace(/\n+\s+/m, ' ').replace(/\s\s+/g, ' '),
- ).toContain('Continous Integration can help catch bugs by running your tests automatically,');
+ ).toContain('Continuous Integration can help catch bugs by running your tests automatically,');
expect(
component.$el.querySelector('p').innerHTML.trim().replace(/\n+\s+/m, ' ').replace(/\s\s+/g, ' '),
diff --git a/spec/javascripts/pipelines/graph/job_component_spec.js b/spec/javascripts/pipelines/graph/job_component_spec.js
index 073dae56c25..9c55a19ebc7 100644
--- a/spec/javascripts/pipelines/graph/job_component_spec.js
+++ b/spec/javascripts/pipelines/graph/job_component_spec.js
@@ -135,4 +135,34 @@ describe('pipeline graph job component', () => {
expect(component.$el.querySelector('.js-job-component-tooltip').getAttribute('data-original-title')).toEqual('test - success');
});
});
+
+ describe('tooltip placement', () => {
+ const tooltipBoundary = 'a[data-boundary="viewport"]';
+
+ it('does not set tooltip boundary by default', () => {
+ component = mountComponent(JobComponent, {
+ job: mockJob,
+ });
+
+ expect(component.$el.querySelector(tooltipBoundary)).toBeNull();
+ });
+
+ it('sets tooltip boundary to viewport for small dropdowns', () => {
+ component = mountComponent(JobComponent, {
+ job: mockJob,
+ dropdownLength: 1,
+ });
+
+ expect(component.$el.querySelector(tooltipBoundary)).not.toBeNull();
+ });
+
+ it('does not set tooltip boundary for large lists', () => {
+ component = mountComponent(JobComponent, {
+ job: mockJob,
+ dropdownLength: 7,
+ });
+
+ expect(component.$el.querySelector(tooltipBoundary)).toBeNull();
+ });
+ });
});
diff --git a/spec/javascripts/pipelines/graph/mock_data.js b/spec/javascripts/pipelines/graph/mock_data.js
index 70eba98e939..9e25a4b3fed 100644
--- a/spec/javascripts/pipelines/graph/mock_data.js
+++ b/spec/javascripts/pipelines/graph/mock_data.js
@@ -20,7 +20,7 @@ export default {
has_details: true,
details_path: '/root/ci-mock/pipelines/123',
favicon:
- '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
+ '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
},
duration: 9,
finished_at: '2017-04-19T14:30:27.542Z',
@@ -40,7 +40,7 @@ export default {
has_details: true,
details_path: '/root/ci-mock/builds/4153',
favicon:
- '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
+ '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
action: {
icon: 'retry',
title: 'Retry',
@@ -65,7 +65,7 @@ export default {
has_details: true,
details_path: '/root/ci-mock/builds/4153',
favicon:
- '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
+ '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
action: {
icon: 'retry',
title: 'Retry',
@@ -85,7 +85,7 @@ export default {
has_details: true,
details_path: '/root/ci-mock/pipelines/123#test',
favicon:
- '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
+ '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
},
path: '/root/ci-mock/pipelines/123#test',
dropdown_path: '/root/ci-mock/pipelines/123/stage.json?stage=test',
@@ -105,7 +105,7 @@ export default {
has_details: true,
details_path: '/root/ci-mock/builds/4166',
favicon:
- '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
+ '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
action: {
icon: 'retry',
title: 'Retry',
@@ -130,7 +130,7 @@ export default {
has_details: true,
details_path: '/root/ci-mock/builds/4166',
favicon:
- '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
+ '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
action: {
icon: 'retry',
title: 'Retry',
@@ -152,7 +152,7 @@ export default {
has_details: true,
details_path: '/root/ci-mock/builds/4159',
favicon:
- '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
+ '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
action: {
icon: 'retry',
title: 'Retry',
@@ -177,7 +177,7 @@ export default {
has_details: true,
details_path: '/root/ci-mock/builds/4159',
favicon:
- '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
+ '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
action: {
icon: 'retry',
title: 'Retry',
@@ -197,7 +197,7 @@ export default {
has_details: true,
details_path: '/root/ci-mock/pipelines/123#deploy',
favicon:
- '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
+ '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
},
path: '/root/ci-mock/pipelines/123#deploy',
dropdown_path: '/root/ci-mock/pipelines/123/stage.json?stage=deploy',
diff --git a/spec/javascripts/pipelines/mock_data.js b/spec/javascripts/pipelines/mock_data.js
index a5a200973d7..03ead6cd8ba 100644
--- a/spec/javascripts/pipelines/mock_data.js
+++ b/spec/javascripts/pipelines/mock_data.js
@@ -217,7 +217,7 @@ export const pipelineWithStages = {
browse_path: '/gitlab-org/gitlab-ee/-/jobs/62411442/artifacts/browse',
},
{
- name: 'codequality',
+ name: 'code_quality',
expired: false,
expire_at: '2018-04-18T14:16:24.484Z',
path: '/gitlab-org/gitlab-ee/-/jobs/62411441/artifacts/download',
diff --git a/spec/javascripts/pipelines/pipelines_spec.js b/spec/javascripts/pipelines/pipelines_spec.js
index ff17602da2b..50141bd99b4 100644
--- a/spec/javascripts/pipelines/pipelines_spec.js
+++ b/spec/javascripts/pipelines/pipelines_spec.js
@@ -427,7 +427,7 @@ describe('Pipelines', () => {
describe('methods', () => {
beforeEach(() => {
- spyOn(history, 'pushState').and.stub();
+ spyOn(window.history, 'pushState').and.stub();
});
describe('updateContent', () => {
diff --git a/spec/javascripts/pipelines/pipelines_table_row_spec.js b/spec/javascripts/pipelines/pipelines_table_row_spec.js
index 05ca4cb9044..03ffc122795 100644
--- a/spec/javascripts/pipelines/pipelines_table_row_spec.js
+++ b/spec/javascripts/pipelines/pipelines_table_row_spec.js
@@ -4,7 +4,7 @@ import eventHub from '~/pipelines/event_hub';
describe('Pipelines Table Row', () => {
const jsonFixtureName = 'pipelines/pipelines.json';
- const buildComponent = (pipeline) => {
+ const buildComponent = pipeline => {
const PipelinesTableRowComponent = Vue.extend(tableRowComp);
return new PipelinesTableRowComponent({
el: document.querySelector('.test-dom-element'),
@@ -24,7 +24,7 @@ describe('Pipelines Table Row', () => {
preloadFixtures(jsonFixtureName);
beforeEach(() => {
- const pipelines = getJSONFixture(jsonFixtureName).pipelines;
+ const { pipelines } = getJSONFixture(jsonFixtureName);
pipeline = pipelines.find(p => p.user !== null && p.commit !== null);
pipelineWithoutAuthor = pipelines.find(p => p.user === null && p.commit !== null);
@@ -52,9 +52,9 @@ describe('Pipelines Table Row', () => {
});
it('should render status text', () => {
- expect(
- component.$el.querySelector('.table-section.commit-link a').textContent,
- ).toContain(pipeline.details.status.text);
+ expect(component.$el.querySelector('.table-section.commit-link a').textContent).toContain(
+ pipeline.details.status.text,
+ );
});
});
@@ -78,11 +78,15 @@ describe('Pipelines Table Row', () => {
describe('when a user is provided', () => {
it('should render user information', () => {
expect(
- component.$el.querySelector('.table-section:nth-child(2) a:nth-child(3)').getAttribute('href'),
+ component.$el
+ .querySelector('.table-section:nth-child(2) a:nth-child(3)')
+ .getAttribute('href'),
).toEqual(pipeline.user.path);
expect(
- component.$el.querySelector('.table-section:nth-child(2) img').getAttribute('data-original-title'),
+ component.$el
+ .querySelector('.table-section:nth-child(2) img')
+ .getAttribute('data-original-title'),
).toEqual(pipeline.user.name);
});
});
@@ -105,7 +109,9 @@ describe('Pipelines Table Row', () => {
}
const commitAuthorLink = commitAuthorElement.getAttribute('href');
- const commitAuthorName = commitAuthorElement.querySelector('img.avatar').getAttribute('data-original-title');
+ const commitAuthorName = commitAuthorElement
+ .querySelector('img.avatar')
+ .getAttribute('data-original-title');
return { commitAuthorElement, commitAuthorLink, commitAuthorName };
};
@@ -145,7 +151,8 @@ describe('Pipelines Table Row', () => {
it('should render an icon for each stage', () => {
expect(
- component.$el.querySelectorAll('.table-section:nth-child(4) .js-builds-dropdown-button').length,
+ component.$el.querySelectorAll('.table-section:nth-child(4) .js-builds-dropdown-button')
+ .length,
).toEqual(pipeline.details.stages.length);
});
});
@@ -167,7 +174,7 @@ describe('Pipelines Table Row', () => {
});
it('emits `retryPipeline` event when retry button is clicked and toggles loading', () => {
- eventHub.$on('retryPipeline', (endpoint) => {
+ eventHub.$on('retryPipeline', endpoint => {
expect(endpoint).toEqual('/retry');
});
@@ -176,13 +183,22 @@ describe('Pipelines Table Row', () => {
});
it('emits `openConfirmationModal` event when cancel button is clicked and toggles loading', () => {
- eventHub.$on('openConfirmationModal', (data) => {
+ eventHub.$once('openConfirmationModal', data => {
expect(data.endpoint).toEqual('/cancel');
expect(data.pipelineId).toEqual(pipeline.id);
});
component.$el.querySelector('.js-pipelines-cancel-button').click();
- expect(component.isCancelling).toEqual(true);
+ });
+
+ it('renders a loading icon when `cancelingPipeline` matches pipeline id', done => {
+ component.cancelingPipeline = pipeline.id;
+ component.$nextTick()
+ .then(() => {
+ expect(component.isCancelling).toEqual(true);
+ })
+ .then(done)
+ .catch(done.fail);
});
});
});
diff --git a/spec/javascripts/pipelines/pipelines_table_spec.js b/spec/javascripts/pipelines/pipelines_table_spec.js
index 4fc3c08145e..d21ba35e96d 100644
--- a/spec/javascripts/pipelines/pipelines_table_spec.js
+++ b/spec/javascripts/pipelines/pipelines_table_spec.js
@@ -11,7 +11,7 @@ describe('Pipelines Table', () => {
preloadFixtures(jsonFixtureName);
beforeEach(() => {
- const pipelines = getJSONFixture(jsonFixtureName).pipelines;
+ const { pipelines } = getJSONFixture(jsonFixtureName);
PipelinesTableComponent = Vue.extend(pipelinesTableComp);
pipeline = pipelines.find(p => p.user !== null && p.commit !== null);
diff --git a/spec/javascripts/profile/account/components/update_username_spec.js b/spec/javascripts/profile/account/components/update_username_spec.js
index bac306edf5a..5311499fb73 100644
--- a/spec/javascripts/profile/account/components/update_username_spec.js
+++ b/spec/javascripts/profile/account/components/update_username_spec.js
@@ -90,7 +90,8 @@ describe('UpdateUsername component', () => {
it('confirmation modal should escape usernames properly', done => {
const { modalBody } = findElements();
- vm.username = vm.newUsername = '<i>Italic</i>';
+ vm.username = '<i>Italic</i>';
+ vm.newUsername = vm.username;
Vue.nextTick()
.then(() => {
diff --git a/spec/javascripts/profile/add_ssh_key_validation_spec.js b/spec/javascripts/profile/add_ssh_key_validation_spec.js
new file mode 100644
index 00000000000..c71a2885acc
--- /dev/null
+++ b/spec/javascripts/profile/add_ssh_key_validation_spec.js
@@ -0,0 +1,69 @@
+import AddSshKeyValidation from '../../../app/assets/javascripts/profile/add_ssh_key_validation';
+
+describe('AddSshKeyValidation', () => {
+ describe('submit', () => {
+ it('returns true if isValid is true', () => {
+ const addSshKeyValidation = new AddSshKeyValidation({});
+ spyOn(AddSshKeyValidation, 'isPublicKey').and.returnValue(true);
+
+ expect(addSshKeyValidation.submit()).toBeTruthy();
+ });
+
+ it('calls preventDefault and toggleWarning if isValid is false', () => {
+ const addSshKeyValidation = new AddSshKeyValidation({});
+ const event = jasmine.createSpyObj('event', ['preventDefault']);
+ spyOn(AddSshKeyValidation, 'isPublicKey').and.returnValue(false);
+ spyOn(addSshKeyValidation, 'toggleWarning');
+
+ addSshKeyValidation.submit(event);
+
+ expect(event.preventDefault).toHaveBeenCalled();
+ expect(addSshKeyValidation.toggleWarning).toHaveBeenCalledWith(true);
+ });
+ });
+
+ describe('toggleWarning', () => {
+ it('shows warningElement and hides originalSubmitElement if isVisible is true', () => {
+ const warningElement = document.createElement('div');
+ const originalSubmitElement = document.createElement('div');
+ warningElement.classList.add('hide');
+
+ const addSshKeyValidation = new AddSshKeyValidation(
+ {},
+ warningElement,
+ originalSubmitElement,
+ );
+ addSshKeyValidation.toggleWarning(true);
+
+ expect(warningElement.classList.contains('hide')).toBeFalsy();
+ expect(originalSubmitElement.classList.contains('hide')).toBeTruthy();
+ });
+
+ it('hides warningElement and shows originalSubmitElement if isVisible is false', () => {
+ const warningElement = document.createElement('div');
+ const originalSubmitElement = document.createElement('div');
+ originalSubmitElement.classList.add('hide');
+
+ const addSshKeyValidation = new AddSshKeyValidation(
+ {},
+ warningElement,
+ originalSubmitElement,
+ );
+ addSshKeyValidation.toggleWarning(false);
+
+ expect(warningElement.classList.contains('hide')).toBeTruthy();
+ expect(originalSubmitElement.classList.contains('hide')).toBeFalsy();
+ });
+ });
+
+ describe('isPublicKey', () => {
+ it('returns false if probably invalid public ssh key', () => {
+ expect(AddSshKeyValidation.isPublicKey('nope')).toBeFalsy();
+ });
+
+ it('returns true if probably valid public ssh key', () => {
+ expect(AddSshKeyValidation.isPublicKey('ssh-')).toBeTruthy();
+ expect(AddSshKeyValidation.isPublicKey('ecdsa-sha2-')).toBeTruthy();
+ });
+ });
+});
diff --git a/spec/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown_spec.js b/spec/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown_spec.js
new file mode 100644
index 00000000000..21805ef0b28
--- /dev/null
+++ b/spec/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown_spec.js
@@ -0,0 +1,103 @@
+import Vue from 'vue';
+import GkeMachineTypeDropdown from '~/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue';
+import { createStore } from '~/projects/gke_cluster_dropdowns/store';
+import {
+ SET_PROJECT,
+ SET_PROJECT_BILLING_STATUS,
+ SET_ZONE,
+ SET_MACHINE_TYPES,
+} from '~/projects/gke_cluster_dropdowns/store/mutation_types';
+import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import {
+ selectedZoneMock,
+ selectedProjectMock,
+ selectedMachineTypeMock,
+ gapiMachineTypesResponseMock,
+} from '../mock_data';
+
+const componentConfig = {
+ fieldId: 'cluster_provider_gcp_attributes_gcp_machine_type',
+ fieldName: 'cluster[provider_gcp_attributes][gcp_machine_type]',
+};
+
+const LABELS = {
+ LOADING: 'Fetching machine types',
+ DISABLED_NO_PROJECT: 'Select project and zone to choose machine type',
+ DISABLED_NO_ZONE: 'Select zone to choose machine type',
+ DEFAULT: 'Select machine type',
+};
+
+const createComponent = (store, props = componentConfig) => {
+ const Component = Vue.extend(GkeMachineTypeDropdown);
+
+ return mountComponentWithStore(Component, {
+ el: null,
+ props,
+ store,
+ });
+};
+
+describe('GkeMachineTypeDropdown', () => {
+ let vm;
+ let store;
+
+ beforeEach(() => {
+ store = createStore();
+ vm = createComponent(store);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('shows various toggle text depending on state', () => {
+ it('returns disabled state toggle text when no project and zone are selected', () => {
+ expect(vm.toggleText).toBe(LABELS.DISABLED_NO_PROJECT);
+ });
+
+ it('returns disabled state toggle text when no zone is selected', () => {
+ vm.$store.commit(SET_PROJECT, selectedProjectMock);
+ vm.$store.commit(SET_PROJECT_BILLING_STATUS, true);
+
+ expect(vm.toggleText).toBe(LABELS.DISABLED_NO_ZONE);
+ });
+
+ it('returns loading toggle text', () => {
+ vm.isLoading = true;
+
+ expect(vm.toggleText).toBe(LABELS.LOADING);
+ });
+
+ it('returns default toggle text', () => {
+ expect(vm.toggleText).toBe(LABELS.DISABLED_NO_PROJECT);
+
+ vm.$store.commit(SET_PROJECT, selectedProjectMock);
+ vm.$store.commit(SET_PROJECT_BILLING_STATUS, true);
+ vm.$store.commit(SET_ZONE, selectedZoneMock);
+
+ expect(vm.toggleText).toBe(LABELS.DEFAULT);
+ });
+
+ it('returns machine type name if machine type selected', () => {
+ vm.setItem(selectedMachineTypeMock);
+
+ expect(vm.toggleText).toBe(selectedMachineTypeMock);
+ });
+ });
+
+ describe('form input', () => {
+ it('reflects new value when dropdown item is clicked', done => {
+ expect(vm.$el.querySelector('input').value).toBe('');
+ vm.$store.commit(SET_MACHINE_TYPES, gapiMachineTypesResponseMock.items);
+
+ return vm.$nextTick().then(() => {
+ vm.$el.querySelector('.dropdown-content button').click();
+
+ return vm.$nextTick().then(() => {
+ expect(vm.$el.querySelector('input').value).toBe(selectedMachineTypeMock);
+ done();
+ });
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown_spec.js b/spec/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown_spec.js
new file mode 100644
index 00000000000..d4fcb2dc8ff
--- /dev/null
+++ b/spec/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown_spec.js
@@ -0,0 +1,92 @@
+import Vue from 'vue';
+import GkeProjectIdDropdown from '~/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue';
+import { createStore } from '~/projects/gke_cluster_dropdowns/store';
+import { SET_PROJECTS } from '~/projects/gke_cluster_dropdowns/store/mutation_types';
+import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { emptyProjectMock, selectedProjectMock } from '../mock_data';
+
+const componentConfig = {
+ docsUrl: 'https://console.cloud.google.com/home/dashboard',
+ fieldId: 'cluster_provider_gcp_attributes_gcp_project_id',
+ fieldName: 'cluster[provider_gcp_attributes][gcp_project_id]',
+};
+
+const LABELS = {
+ LOADING: 'Fetching projects',
+ VALIDATING_PROJECT_BILLING: 'Validating project billing status',
+ DEFAULT: 'Select project',
+ EMPTY: 'No projects found',
+};
+
+const createComponent = (store, props = componentConfig) => {
+ const Component = Vue.extend(GkeProjectIdDropdown);
+
+ return mountComponentWithStore(Component, {
+ el: null,
+ props,
+ store,
+ });
+};
+
+describe('GkeProjectIdDropdown', () => {
+ let vm;
+ let store;
+
+ beforeEach(() => {
+ store = createStore();
+ vm = createComponent(store);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('toggleText', () => {
+ it('returns loading toggle text', () => {
+ expect(vm.toggleText).toBe(LABELS.LOADING);
+ });
+
+ it('returns project billing validation text', () => {
+ vm.setIsValidatingProjectBilling(true);
+ expect(vm.toggleText).toBe(LABELS.VALIDATING_PROJECT_BILLING);
+ });
+
+ it('returns default toggle text', done =>
+ vm.$nextTick().then(() => {
+ vm.setItem(emptyProjectMock);
+
+ expect(vm.toggleText).toBe(LABELS.DEFAULT);
+ done();
+ }));
+
+ it('returns project name if project selected', done =>
+ vm.$nextTick().then(() => {
+ expect(vm.toggleText).toBe(selectedProjectMock.name);
+ done();
+ }));
+
+ it('returns empty toggle text', done =>
+ vm.$nextTick().then(() => {
+ vm.$store.commit(SET_PROJECTS, null);
+ vm.setItem(emptyProjectMock);
+
+ expect(vm.toggleText).toBe(LABELS.EMPTY);
+ done();
+ }));
+ });
+
+ describe('selectItem', () => {
+ it('reflects new value when dropdown item is clicked', done => {
+ expect(vm.$el.querySelector('input').value).toBe('');
+
+ return vm.$nextTick().then(() => {
+ vm.$el.querySelector('.dropdown-content button').click();
+
+ return vm.$nextTick().then(() => {
+ expect(vm.$el.querySelector('input').value).toBe(selectedProjectMock.projectId);
+ done();
+ });
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown_spec.js b/spec/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown_spec.js
new file mode 100644
index 00000000000..89a4a7ea2ce
--- /dev/null
+++ b/spec/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown_spec.js
@@ -0,0 +1,88 @@
+import Vue from 'vue';
+import GkeZoneDropdown from '~/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue';
+import { createStore } from '~/projects/gke_cluster_dropdowns/store';
+import {
+ SET_PROJECT,
+ SET_ZONES,
+ SET_PROJECT_BILLING_STATUS,
+} from '~/projects/gke_cluster_dropdowns/store/mutation_types';
+import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { selectedZoneMock, selectedProjectMock, gapiZonesResponseMock } from '../mock_data';
+
+const componentConfig = {
+ fieldId: 'cluster_provider_gcp_attributes_gcp_zone',
+ fieldName: 'cluster[provider_gcp_attributes][gcp_zone]',
+};
+
+const LABELS = {
+ LOADING: 'Fetching zones',
+ DISABLED: 'Select project to choose zone',
+ DEFAULT: 'Select zone',
+};
+
+const createComponent = (store, props = componentConfig) => {
+ const Component = Vue.extend(GkeZoneDropdown);
+
+ return mountComponentWithStore(Component, {
+ el: null,
+ props,
+ store,
+ });
+};
+
+describe('GkeZoneDropdown', () => {
+ let vm;
+ let store;
+
+ beforeEach(() => {
+ store = createStore();
+ vm = createComponent(store);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('toggleText', () => {
+ it('returns disabled state toggle text', () => {
+ expect(vm.toggleText).toBe(LABELS.DISABLED);
+ });
+
+ it('returns loading toggle text', () => {
+ vm.isLoading = true;
+
+ expect(vm.toggleText).toBe(LABELS.LOADING);
+ });
+
+ it('returns default toggle text', () => {
+ expect(vm.toggleText).toBe(LABELS.DISABLED);
+
+ vm.$store.commit(SET_PROJECT, selectedProjectMock);
+ vm.$store.commit(SET_PROJECT_BILLING_STATUS, true);
+
+ expect(vm.toggleText).toBe(LABELS.DEFAULT);
+ });
+
+ it('returns project name if project selected', () => {
+ vm.setItem(selectedZoneMock);
+
+ expect(vm.toggleText).toBe(selectedZoneMock);
+ });
+ });
+
+ describe('selectItem', () => {
+ it('reflects new value when dropdown item is clicked', done => {
+ expect(vm.$el.querySelector('input').value).toBe('');
+ vm.$store.commit(SET_ZONES, gapiZonesResponseMock.items);
+
+ return vm.$nextTick().then(() => {
+ vm.$el.querySelector('.dropdown-content button').click();
+
+ return vm.$nextTick().then(() => {
+ expect(vm.$el.querySelector('input').value).toBe(selectedZoneMock);
+ done();
+ });
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/projects/gke_cluster_dropdowns/helpers.js b/spec/javascripts/projects/gke_cluster_dropdowns/helpers.js
new file mode 100644
index 00000000000..6df511e9157
--- /dev/null
+++ b/spec/javascripts/projects/gke_cluster_dropdowns/helpers.js
@@ -0,0 +1,49 @@
+import {
+ gapiProjectsResponseMock,
+ gapiZonesResponseMock,
+ gapiMachineTypesResponseMock,
+} from './mock_data';
+
+// eslint-disable-next-line import/prefer-default-export
+export const gapi = () => ({
+ client: {
+ cloudbilling: {
+ projects: {
+ getBillingInfo: () =>
+ new Promise(resolve => {
+ resolve({
+ result: { billingEnabled: true },
+ });
+ }),
+ },
+ },
+ cloudresourcemanager: {
+ projects: {
+ list: () =>
+ new Promise(resolve => {
+ resolve({
+ result: { ...gapiProjectsResponseMock },
+ });
+ }),
+ },
+ },
+ compute: {
+ zones: {
+ list: () =>
+ new Promise(resolve => {
+ resolve({
+ result: { ...gapiZonesResponseMock },
+ });
+ }),
+ },
+ machineTypes: {
+ list: () =>
+ new Promise(resolve => {
+ resolve({
+ result: { ...gapiMachineTypesResponseMock },
+ });
+ }),
+ },
+ },
+ },
+});
diff --git a/spec/javascripts/projects/gke_cluster_dropdowns/mock_data.js b/spec/javascripts/projects/gke_cluster_dropdowns/mock_data.js
new file mode 100644
index 00000000000..d9f5dbc636f
--- /dev/null
+++ b/spec/javascripts/projects/gke_cluster_dropdowns/mock_data.js
@@ -0,0 +1,75 @@
+export const emptyProjectMock = {
+ projectId: '',
+ name: '',
+};
+
+export const selectedProjectMock = {
+ projectId: 'gcp-project-123',
+ name: 'gcp-project',
+};
+
+export const selectedZoneMock = 'us-central1-a';
+
+export const selectedMachineTypeMock = 'n1-standard-2';
+
+export const gapiProjectsResponseMock = {
+ projects: [
+ {
+ projectNumber: '1234',
+ projectId: 'gcp-project-123',
+ lifecycleState: 'ACTIVE',
+ name: 'gcp-project',
+ createTime: '2017-12-16T01:48:29.129Z',
+ parent: {
+ type: 'organization',
+ id: '12345',
+ },
+ },
+ ],
+};
+
+export const gapiZonesResponseMock = {
+ kind: 'compute#zoneList',
+ id: 'projects/gitlab-internal-153318/zones',
+ items: [
+ {
+ kind: 'compute#zone',
+ id: '2000',
+ creationTimestamp: '1969-12-31T16:00:00.000-08:00',
+ name: 'us-central1-a',
+ description: 'us-central1-a',
+ status: 'UP',
+ region:
+ 'https://www.googleapis.com/compute/v1/projects/gitlab-internal-153318/regions/us-central1',
+ selfLink:
+ 'https://www.googleapis.com/compute/v1/projects/gitlab-internal-153318/zones/us-central1-a',
+ availableCpuPlatforms: ['Intel Skylake', 'Intel Broadwell', 'Intel Sandy Bridge'],
+ },
+ ],
+ selfLink: 'https://www.googleapis.com/compute/v1/projects/gitlab-internal-153318/zones',
+};
+
+export const gapiMachineTypesResponseMock = {
+ kind: 'compute#machineTypeList',
+ id: 'projects/gitlab-internal-153318/zones/us-central1-a/machineTypes',
+ items: [
+ {
+ kind: 'compute#machineType',
+ id: '3002',
+ creationTimestamp: '1969-12-31T16:00:00.000-08:00',
+ name: 'n1-standard-2',
+ description: '2 vCPUs, 7.5 GB RAM',
+ guestCpus: 2,
+ memoryMb: 7680,
+ imageSpaceGb: 10,
+ maximumPersistentDisks: 64,
+ maximumPersistentDisksSizeGb: '65536',
+ zone: 'us-central1-a',
+ selfLink:
+ 'https://www.googleapis.com/compute/v1/projects/gitlab-internal-153318/zones/us-central1-a/machineTypes/n1-standard-2',
+ isSharedCpu: false,
+ },
+ ],
+ selfLink:
+ 'https://www.googleapis.com/compute/v1/projects/gitlab-internal-153318/zones/us-central1-a/machineTypes',
+};
diff --git a/spec/javascripts/projects/gke_cluster_dropdowns/stores/actions_spec.js b/spec/javascripts/projects/gke_cluster_dropdowns/stores/actions_spec.js
new file mode 100644
index 00000000000..9d892b8185b
--- /dev/null
+++ b/spec/javascripts/projects/gke_cluster_dropdowns/stores/actions_spec.js
@@ -0,0 +1,131 @@
+import testAction from 'spec/helpers/vuex_action_helper';
+import * as actions from '~/projects/gke_cluster_dropdowns/store/actions';
+import { createStore } from '~/projects/gke_cluster_dropdowns/store';
+import { gapi } from '../helpers';
+import { selectedProjectMock, selectedZoneMock, selectedMachineTypeMock } from '../mock_data';
+
+describe('GCP Cluster Dropdown Store Actions', () => {
+ let store;
+
+ beforeEach(() => {
+ store = createStore();
+ });
+
+ describe('setProject', () => {
+ it('should set project', done => {
+ testAction(
+ actions.setProject,
+ selectedProjectMock,
+ { selectedProject: {} },
+ [{ type: 'SET_PROJECT', payload: selectedProjectMock }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('setZone', () => {
+ it('should set zone', done => {
+ testAction(
+ actions.setZone,
+ selectedZoneMock,
+ { selectedZone: '' },
+ [{ type: 'SET_ZONE', payload: selectedZoneMock }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('setMachineType', () => {
+ it('should set machine type', done => {
+ testAction(
+ actions.setMachineType,
+ selectedMachineTypeMock,
+ { selectedMachineType: '' },
+ [{ type: 'SET_MACHINE_TYPE', payload: selectedMachineTypeMock }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('setIsValidatingProjectBilling', () => {
+ it('should set machine type', done => {
+ testAction(
+ actions.setIsValidatingProjectBilling,
+ true,
+ { isValidatingProjectBilling: null },
+ [{ type: 'SET_IS_VALIDATING_PROJECT_BILLING', payload: true }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('async fetch methods', () => {
+ window.gapi = gapi();
+
+ describe('fetchProjects', () => {
+ it('fetches projects from Google API', done => {
+ store
+ .dispatch('fetchProjects')
+ .then(() => {
+ expect(store.state.projects[0].projectId).toEqual(selectedProjectMock.projectId);
+ expect(store.state.projects[0].name).toEqual(selectedProjectMock.name);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('validateProjectBilling', () => {
+ it('checks project billing status from Google API', done => {
+ testAction(
+ actions.validateProjectBilling,
+ true,
+ {
+ selectedProject: selectedProjectMock,
+ selectedZone: '',
+ selectedMachineType: '',
+ projectHasBillingEnabled: null,
+ },
+ [
+ { type: 'SET_ZONE', payload: '' },
+ { type: 'SET_MACHINE_TYPE', payload: '' },
+ { type: 'SET_PROJECT_BILLING_STATUS', payload: true },
+ ],
+ [{ type: 'setIsValidatingProjectBilling', payload: false }],
+ done,
+ );
+ });
+ });
+
+ describe('fetchZones', () => {
+ it('fetches zones from Google API', done => {
+ store
+ .dispatch('fetchZones')
+ .then(() => {
+ expect(store.state.zones[0].name).toEqual(selectedZoneMock);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('fetchMachineTypes', () => {
+ it('fetches machine types from Google API', done => {
+ store
+ .dispatch('fetchMachineTypes')
+ .then(() => {
+ expect(store.state.machineTypes[0].name).toEqual(selectedMachineTypeMock);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/projects/gke_cluster_dropdowns/stores/getters_spec.js b/spec/javascripts/projects/gke_cluster_dropdowns/stores/getters_spec.js
new file mode 100644
index 00000000000..6f89158f807
--- /dev/null
+++ b/spec/javascripts/projects/gke_cluster_dropdowns/stores/getters_spec.js
@@ -0,0 +1,65 @@
+import * as getters from '~/projects/gke_cluster_dropdowns/store/getters';
+import { selectedProjectMock, selectedZoneMock, selectedMachineTypeMock } from '../mock_data';
+
+describe('GCP Cluster Dropdown Store Getters', () => {
+ let state;
+
+ describe('valid states', () => {
+ beforeEach(() => {
+ state = {
+ selectedProject: selectedProjectMock,
+ selectedZone: selectedZoneMock,
+ selectedMachineType: selectedMachineTypeMock,
+ };
+ });
+
+ describe('hasProject', () => {
+ it('should return true when project is selected', () => {
+ expect(getters.hasProject(state)).toEqual(true);
+ });
+ });
+
+ describe('hasZone', () => {
+ it('should return true when zone is selected', () => {
+ expect(getters.hasZone(state)).toEqual(true);
+ });
+ });
+
+ describe('hasMachineType', () => {
+ it('should return true when machine type is selected', () => {
+ expect(getters.hasMachineType(state)).toEqual(true);
+ });
+ });
+ });
+
+ describe('invalid states', () => {
+ beforeEach(() => {
+ state = {
+ selectedProject: {
+ projectId: '',
+ name: '',
+ },
+ selectedZone: '',
+ selectedMachineType: '',
+ };
+ });
+
+ describe('hasProject', () => {
+ it('should return false when project is not selected', () => {
+ expect(getters.hasProject(state)).toEqual(false);
+ });
+ });
+
+ describe('hasZone', () => {
+ it('should return false when zone is not selected', () => {
+ expect(getters.hasZone(state)).toEqual(false);
+ });
+ });
+
+ describe('hasMachineType', () => {
+ it('should return false when machine type is not selected', () => {
+ expect(getters.hasMachineType(state)).toEqual(false);
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/projects/gke_cluster_dropdowns/stores/mutations_spec.js b/spec/javascripts/projects/gke_cluster_dropdowns/stores/mutations_spec.js
new file mode 100644
index 00000000000..7f8c4f314e4
--- /dev/null
+++ b/spec/javascripts/projects/gke_cluster_dropdowns/stores/mutations_spec.js
@@ -0,0 +1,87 @@
+import { createStore } from '~/projects/gke_cluster_dropdowns/store';
+import * as types from '~/projects/gke_cluster_dropdowns/store/mutation_types';
+import {
+ selectedProjectMock,
+ selectedZoneMock,
+ selectedMachineTypeMock,
+ gapiProjectsResponseMock,
+ gapiZonesResponseMock,
+ gapiMachineTypesResponseMock,
+} from '../mock_data';
+
+describe('GCP Cluster Dropdown Store Mutations', () => {
+ let store;
+
+ beforeEach(() => {
+ store = createStore();
+ });
+
+ describe('SET_PROJECT', () => {
+ it('should set GCP project as selectedProject', () => {
+ const projectToSelect = gapiProjectsResponseMock.projects[0];
+
+ store.commit(types.SET_PROJECT, projectToSelect);
+
+ expect(store.state.selectedProject.projectId).toEqual(selectedProjectMock.projectId);
+ expect(store.state.selectedProject.name).toEqual(selectedProjectMock.name);
+ });
+ });
+
+ describe('SET_PROJECT_BILLING_STATUS', () => {
+ it('should set project billing status', () => {
+ store.commit(types.SET_PROJECT_BILLING_STATUS, true);
+
+ expect(store.state.projectHasBillingEnabled).toBeTruthy();
+ });
+ });
+
+ describe('SET_ZONE', () => {
+ it('should set GCP zone as selectedZone', () => {
+ const zoneToSelect = gapiZonesResponseMock.items[0].name;
+
+ store.commit(types.SET_ZONE, zoneToSelect);
+
+ expect(store.state.selectedZone).toEqual(selectedZoneMock);
+ });
+ });
+
+ describe('SET_MACHINE_TYPE', () => {
+ it('should set GCP machine type as selectedMachineType', () => {
+ const machineTypeToSelect = gapiMachineTypesResponseMock.items[0].name;
+
+ store.commit(types.SET_MACHINE_TYPE, machineTypeToSelect);
+
+ expect(store.state.selectedMachineType).toEqual(selectedMachineTypeMock);
+ });
+ });
+
+ describe('SET_PROJECTS', () => {
+ it('should set Google API Projects response as projects', () => {
+ expect(store.state.projects.length).toEqual(0);
+
+ store.commit(types.SET_PROJECTS, gapiProjectsResponseMock.projects);
+
+ expect(store.state.projects.length).toEqual(gapiProjectsResponseMock.projects.length);
+ });
+ });
+
+ describe('SET_ZONES', () => {
+ it('should set Google API Zones response as zones', () => {
+ expect(store.state.zones.length).toEqual(0);
+
+ store.commit(types.SET_ZONES, gapiZonesResponseMock.items);
+
+ expect(store.state.zones.length).toEqual(gapiZonesResponseMock.items.length);
+ });
+ });
+
+ describe('SET_MACHINE_TYPES', () => {
+ it('should set Google API Machine Types response as machineTypes', () => {
+ expect(store.state.machineTypes.length).toEqual(0);
+
+ store.commit(types.SET_MACHINE_TYPES, gapiMachineTypesResponseMock.items);
+
+ expect(store.state.machineTypes.length).toEqual(gapiMachineTypesResponseMock.items.length);
+ });
+ });
+});
diff --git a/spec/javascripts/projects_dropdown/components/app_spec.js b/spec/javascripts/projects_dropdown/components/app_spec.js
deleted file mode 100644
index 38b31c3d727..00000000000
--- a/spec/javascripts/projects_dropdown/components/app_spec.js
+++ /dev/null
@@ -1,349 +0,0 @@
-import Vue from 'vue';
-
-import bp from '~/breakpoints';
-import appComponent from '~/projects_dropdown/components/app.vue';
-import eventHub from '~/projects_dropdown/event_hub';
-import ProjectsStore from '~/projects_dropdown/store/projects_store';
-import ProjectsService from '~/projects_dropdown/service/projects_service';
-
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { currentSession, mockProject, mockRawProject } from '../mock_data';
-
-const createComponent = () => {
- gon.api_version = currentSession.apiVersion;
- const Component = Vue.extend(appComponent);
- const store = new ProjectsStore();
- const service = new ProjectsService(currentSession.username);
-
- return mountComponent(Component, {
- store,
- service,
- currentUserName: currentSession.username,
- currentProject: currentSession.project,
- });
-};
-
-const returnServicePromise = (data, failed) =>
- new Promise((resolve, reject) => {
- if (failed) {
- reject(data);
- } else {
- resolve({
- json() {
- return data;
- },
- });
- }
- });
-
-describe('AppComponent', () => {
- describe('computed', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('frequentProjects', () => {
- it('should return list of frequently accessed projects from store', () => {
- expect(vm.frequentProjects).toBeDefined();
- expect(vm.frequentProjects.length).toBe(0);
-
- vm.store.setFrequentProjects([mockProject]);
- expect(vm.frequentProjects).toBeDefined();
- expect(vm.frequentProjects.length).toBe(1);
- });
- });
-
- describe('searchProjects', () => {
- it('should return list of frequently accessed projects from store', () => {
- expect(vm.searchProjects).toBeDefined();
- expect(vm.searchProjects.length).toBe(0);
-
- vm.store.setSearchedProjects([mockRawProject]);
- expect(vm.searchProjects).toBeDefined();
- expect(vm.searchProjects.length).toBe(1);
- });
- });
- });
-
- describe('methods', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('toggleFrequentProjectsList', () => {
- it('should toggle props which control visibility of Frequent Projects list from state passed', () => {
- vm.toggleFrequentProjectsList(true);
- expect(vm.isLoadingProjects).toBeFalsy();
- expect(vm.isSearchListVisible).toBeFalsy();
- expect(vm.isFrequentsListVisible).toBeTruthy();
-
- vm.toggleFrequentProjectsList(false);
- expect(vm.isLoadingProjects).toBeTruthy();
- expect(vm.isSearchListVisible).toBeTruthy();
- expect(vm.isFrequentsListVisible).toBeFalsy();
- });
- });
-
- describe('toggleSearchProjectsList', () => {
- it('should toggle props which control visibility of Searched Projects list from state passed', () => {
- vm.toggleSearchProjectsList(true);
- expect(vm.isLoadingProjects).toBeFalsy();
- expect(vm.isFrequentsListVisible).toBeFalsy();
- expect(vm.isSearchListVisible).toBeTruthy();
-
- vm.toggleSearchProjectsList(false);
- expect(vm.isLoadingProjects).toBeTruthy();
- expect(vm.isFrequentsListVisible).toBeTruthy();
- expect(vm.isSearchListVisible).toBeFalsy();
- });
- });
-
- describe('toggleLoader', () => {
- it('should toggle props which control visibility of list loading animation from state passed', () => {
- vm.toggleLoader(true);
- expect(vm.isFrequentsListVisible).toBeFalsy();
- expect(vm.isSearchListVisible).toBeFalsy();
- expect(vm.isLoadingProjects).toBeTruthy();
-
- vm.toggleLoader(false);
- expect(vm.isFrequentsListVisible).toBeTruthy();
- expect(vm.isSearchListVisible).toBeTruthy();
- expect(vm.isLoadingProjects).toBeFalsy();
- });
- });
-
- describe('fetchFrequentProjects', () => {
- it('should set props for loading animation to `true` while frequent projects list is being loaded', () => {
- spyOn(vm, 'toggleLoader');
-
- vm.fetchFrequentProjects();
- expect(vm.isLocalStorageFailed).toBeFalsy();
- expect(vm.toggleLoader).toHaveBeenCalledWith(true);
- });
-
- it('should set props for loading animation to `false` and props for frequent projects list to `true` once data is loaded', () => {
- const mockData = [mockProject];
-
- spyOn(vm.service, 'getFrequentProjects').and.returnValue(mockData);
- spyOn(vm.store, 'setFrequentProjects');
- spyOn(vm, 'toggleFrequentProjectsList');
-
- vm.fetchFrequentProjects();
- expect(vm.service.getFrequentProjects).toHaveBeenCalled();
- expect(vm.store.setFrequentProjects).toHaveBeenCalledWith(mockData);
- expect(vm.toggleFrequentProjectsList).toHaveBeenCalledWith(true);
- });
-
- it('should set props for failure message to `true` when method fails to fetch frequent projects list', () => {
- spyOn(vm.service, 'getFrequentProjects').and.returnValue(null);
- spyOn(vm.store, 'setFrequentProjects');
- spyOn(vm, 'toggleFrequentProjectsList');
-
- expect(vm.isLocalStorageFailed).toBeFalsy();
-
- vm.fetchFrequentProjects();
- expect(vm.service.getFrequentProjects).toHaveBeenCalled();
- expect(vm.store.setFrequentProjects).toHaveBeenCalledWith([]);
- expect(vm.toggleFrequentProjectsList).toHaveBeenCalledWith(true);
- expect(vm.isLocalStorageFailed).toBeTruthy();
- });
-
- it('should set props for search results list to `true` if search query was already made previously', () => {
- spyOn(bp, 'getBreakpointSize').and.returnValue('md');
- spyOn(vm.service, 'getFrequentProjects');
- spyOn(vm, 'toggleSearchProjectsList');
-
- vm.searchQuery = 'test';
- vm.fetchFrequentProjects();
- expect(vm.service.getFrequentProjects).not.toHaveBeenCalled();
- expect(vm.toggleSearchProjectsList).toHaveBeenCalledWith(true);
- });
-
- it('should set props for frequent projects list to `true` if search query was already made but screen size is less than 768px', () => {
- spyOn(bp, 'getBreakpointSize').and.returnValue('sm');
- spyOn(vm, 'toggleSearchProjectsList');
- spyOn(vm.service, 'getFrequentProjects');
-
- vm.searchQuery = 'test';
- vm.fetchFrequentProjects();
- expect(vm.service.getFrequentProjects).toHaveBeenCalled();
- expect(vm.toggleSearchProjectsList).not.toHaveBeenCalled();
- });
- });
-
- describe('fetchSearchedProjects', () => {
- const searchQuery = 'test';
-
- it('should perform search with provided search query', done => {
- const mockData = [mockRawProject];
- spyOn(vm, 'toggleLoader');
- spyOn(vm, 'toggleSearchProjectsList');
- spyOn(vm.service, 'getSearchedProjects').and.returnValue(returnServicePromise(mockData));
- spyOn(vm.store, 'setSearchedProjects');
-
- vm.fetchSearchedProjects(searchQuery);
- setTimeout(() => {
- expect(vm.searchQuery).toBe(searchQuery);
- expect(vm.toggleLoader).toHaveBeenCalledWith(true);
- expect(vm.service.getSearchedProjects).toHaveBeenCalledWith(searchQuery);
- expect(vm.toggleSearchProjectsList).toHaveBeenCalledWith(true);
- expect(vm.store.setSearchedProjects).toHaveBeenCalledWith(mockData);
- done();
- }, 0);
- });
-
- it('should update props for showing search failure', done => {
- spyOn(vm, 'toggleSearchProjectsList');
- spyOn(vm.service, 'getSearchedProjects').and.returnValue(returnServicePromise({}, true));
-
- vm.fetchSearchedProjects(searchQuery);
- setTimeout(() => {
- expect(vm.searchQuery).toBe(searchQuery);
- expect(vm.service.getSearchedProjects).toHaveBeenCalledWith(searchQuery);
- expect(vm.isSearchFailed).toBeTruthy();
- expect(vm.toggleSearchProjectsList).toHaveBeenCalledWith(true);
- done();
- }, 0);
- });
- });
-
- describe('logCurrentProjectAccess', () => {
- it('should log current project access via service', done => {
- spyOn(vm.service, 'logProjectAccess');
-
- vm.currentProject = mockProject;
- vm.logCurrentProjectAccess();
-
- setTimeout(() => {
- expect(vm.service.logProjectAccess).toHaveBeenCalledWith(mockProject);
- done();
- }, 1);
- });
- });
-
- describe('handleSearchClear', () => {
- it('should show frequent projects list when search input is cleared', () => {
- spyOn(vm.store, 'clearSearchedProjects');
- spyOn(vm, 'toggleFrequentProjectsList');
-
- vm.handleSearchClear();
-
- expect(vm.toggleFrequentProjectsList).toHaveBeenCalledWith(true);
- expect(vm.store.clearSearchedProjects).toHaveBeenCalled();
- expect(vm.searchQuery).toBe('');
- });
- });
-
- describe('handleSearchFailure', () => {
- it('should show failure message within dropdown', () => {
- spyOn(vm, 'toggleSearchProjectsList');
-
- vm.handleSearchFailure();
- expect(vm.toggleSearchProjectsList).toHaveBeenCalledWith(true);
- expect(vm.isSearchFailed).toBeTruthy();
- });
- });
- });
-
- describe('created', () => {
- it('should bind event listeners on eventHub', done => {
- spyOn(eventHub, '$on');
-
- createComponent().$mount();
-
- Vue.nextTick(() => {
- expect(eventHub.$on).toHaveBeenCalledWith('dropdownOpen', jasmine.any(Function));
- expect(eventHub.$on).toHaveBeenCalledWith('searchProjects', jasmine.any(Function));
- expect(eventHub.$on).toHaveBeenCalledWith('searchCleared', jasmine.any(Function));
- expect(eventHub.$on).toHaveBeenCalledWith('searchFailed', jasmine.any(Function));
- done();
- });
- });
- });
-
- describe('beforeDestroy', () => {
- it('should unbind event listeners on eventHub', done => {
- const vm = createComponent();
- spyOn(eventHub, '$off');
-
- vm.$mount();
- vm.$destroy();
-
- Vue.nextTick(() => {
- expect(eventHub.$off).toHaveBeenCalledWith('dropdownOpen', jasmine.any(Function));
- expect(eventHub.$off).toHaveBeenCalledWith('searchProjects', jasmine.any(Function));
- expect(eventHub.$off).toHaveBeenCalledWith('searchCleared', jasmine.any(Function));
- expect(eventHub.$off).toHaveBeenCalledWith('searchFailed', jasmine.any(Function));
- done();
- });
- });
- });
-
- describe('template', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('should render search input', () => {
- expect(vm.$el.querySelector('.search-input-container')).toBeDefined();
- });
-
- it('should render loading animation', done => {
- vm.toggleLoader(true);
- Vue.nextTick(() => {
- const loadingEl = vm.$el.querySelector('.loading-animation');
-
- expect(loadingEl).toBeDefined();
- expect(loadingEl.classList.contains('prepend-top-20')).toBeTruthy();
- expect(loadingEl.querySelector('i').getAttribute('aria-label')).toBe('Loading projects');
- done();
- });
- });
-
- it('should render frequent projects list header', done => {
- vm.toggleFrequentProjectsList(true);
- Vue.nextTick(() => {
- const sectionHeaderEl = vm.$el.querySelector('.section-header');
-
- expect(sectionHeaderEl).toBeDefined();
- expect(sectionHeaderEl.innerText.trim()).toBe('Frequently visited');
- done();
- });
- });
-
- it('should render frequent projects list', done => {
- vm.toggleFrequentProjectsList(true);
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.projects-list-frequent-container')).toBeDefined();
- done();
- });
- });
-
- it('should render searched projects list', done => {
- vm.toggleSearchProjectsList(true);
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.section-header')).toBe(null);
- expect(vm.$el.querySelector('.projects-list-search-container')).toBeDefined();
- done();
- });
- });
- });
-});
diff --git a/spec/javascripts/projects_dropdown/components/projects_list_frequent_spec.js b/spec/javascripts/projects_dropdown/components/projects_list_frequent_spec.js
deleted file mode 100644
index 2bafb4e81ca..00000000000
--- a/spec/javascripts/projects_dropdown/components/projects_list_frequent_spec.js
+++ /dev/null
@@ -1,72 +0,0 @@
-import Vue from 'vue';
-
-import projectsListFrequentComponent from '~/projects_dropdown/components/projects_list_frequent.vue';
-
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { mockFrequents } from '../mock_data';
-
-const createComponent = () => {
- const Component = Vue.extend(projectsListFrequentComponent);
-
- return mountComponent(Component, {
- projects: mockFrequents,
- localStorageFailed: false,
- });
-};
-
-describe('ProjectsListFrequentComponent', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('computed', () => {
- describe('isListEmpty', () => {
- it('should return `true` or `false` representing whether if `projects` is empty of not', () => {
- vm.projects = [];
- expect(vm.isListEmpty).toBeTruthy();
-
- vm.projects = mockFrequents;
- expect(vm.isListEmpty).toBeFalsy();
- });
- });
-
- describe('listEmptyMessage', () => {
- it('should return appropriate empty list message based on value of `localStorageFailed` prop', () => {
- vm.localStorageFailed = true;
- expect(vm.listEmptyMessage).toBe('This feature requires browser localStorage support');
-
- vm.localStorageFailed = false;
- expect(vm.listEmptyMessage).toBe('Projects you visit often will appear here');
- });
- });
- });
-
- describe('template', () => {
- it('should render component element with list of projects', (done) => {
- vm.projects = mockFrequents;
-
- Vue.nextTick(() => {
- expect(vm.$el.classList.contains('projects-list-frequent-container')).toBeTruthy();
- expect(vm.$el.querySelectorAll('ul.list-unstyled').length).toBe(1);
- expect(vm.$el.querySelectorAll('li.projects-list-item-container').length).toBe(5);
- done();
- });
- });
-
- it('should render component element with empty message', (done) => {
- vm.projects = [];
-
- Vue.nextTick(() => {
- expect(vm.$el.querySelectorAll('li.section-empty').length).toBe(1);
- expect(vm.$el.querySelectorAll('li.projects-list-item-container').length).toBe(0);
- done();
- });
- });
- });
-});
diff --git a/spec/javascripts/projects_dropdown/components/projects_list_item_spec.js b/spec/javascripts/projects_dropdown/components/projects_list_item_spec.js
deleted file mode 100644
index c193258474e..00000000000
--- a/spec/javascripts/projects_dropdown/components/projects_list_item_spec.js
+++ /dev/null
@@ -1,77 +0,0 @@
-import Vue from 'vue';
-
-import projectsListItemComponent from '~/projects_dropdown/components/projects_list_item.vue';
-
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { mockProject } from '../mock_data';
-
-const createComponent = () => {
- const Component = Vue.extend(projectsListItemComponent);
-
- return mountComponent(Component, {
- projectId: mockProject.id,
- projectName: mockProject.name,
- namespace: mockProject.namespace,
- webUrl: mockProject.webUrl,
- avatarUrl: mockProject.avatarUrl,
- });
-};
-
-describe('ProjectsListItemComponent', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('computed', () => {
- describe('hasAvatar', () => {
- it('should return `true` or `false` if whether avatar is present or not', () => {
- vm.avatarUrl = 'path/to/avatar.png';
- expect(vm.hasAvatar).toBeTruthy();
-
- vm.avatarUrl = null;
- expect(vm.hasAvatar).toBeFalsy();
- });
- });
-
- describe('highlightedProjectName', () => {
- it('should enclose part of project name in <b> & </b> which matches with `matcher` prop', () => {
- vm.matcher = 'lab';
- expect(vm.highlightedProjectName).toContain('<b>Lab</b>');
- });
-
- it('should return project name as it is if `matcher` is not available', () => {
- vm.matcher = null;
- expect(vm.highlightedProjectName).toBe(mockProject.name);
- });
- });
-
- describe('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', () => {
- it('should render component element', () => {
- expect(vm.$el.classList.contains('projects-list-item-container')).toBeTruthy();
- expect(vm.$el.querySelectorAll('a').length).toBe(1);
- expect(vm.$el.querySelectorAll('.project-item-avatar-container').length).toBe(1);
- expect(vm.$el.querySelectorAll('.project-item-metadata-container').length).toBe(1);
- expect(vm.$el.querySelectorAll('.project-title').length).toBe(1);
- expect(vm.$el.querySelectorAll('.project-namespace').length).toBe(1);
- });
- });
-});
diff --git a/spec/javascripts/projects_dropdown/components/projects_list_search_spec.js b/spec/javascripts/projects_dropdown/components/projects_list_search_spec.js
deleted file mode 100644
index c4b86d77034..00000000000
--- a/spec/javascripts/projects_dropdown/components/projects_list_search_spec.js
+++ /dev/null
@@ -1,84 +0,0 @@
-import Vue from 'vue';
-
-import projectsListSearchComponent from '~/projects_dropdown/components/projects_list_search.vue';
-
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { mockProject } from '../mock_data';
-
-const createComponent = () => {
- const Component = Vue.extend(projectsListSearchComponent);
-
- return mountComponent(Component, {
- projects: [mockProject],
- matcher: 'lab',
- searchFailed: false,
- });
-};
-
-describe('ProjectsListSearchComponent', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('computed', () => {
- describe('isListEmpty', () => {
- it('should return `true` or `false` representing whether if `projects` is empty of not', () => {
- vm.projects = [];
- expect(vm.isListEmpty).toBeTruthy();
-
- vm.projects = [mockProject];
- expect(vm.isListEmpty).toBeFalsy();
- });
- });
-
- describe('listEmptyMessage', () => {
- it('should return appropriate empty list message based on value of `searchFailed` prop', () => {
- vm.searchFailed = true;
- expect(vm.listEmptyMessage).toBe('Something went wrong on our end.');
-
- vm.searchFailed = false;
- expect(vm.listEmptyMessage).toBe('Sorry, no projects matched your search');
- });
- });
- });
-
- describe('template', () => {
- it('should render component element with list of projects', (done) => {
- vm.projects = [mockProject];
-
- Vue.nextTick(() => {
- expect(vm.$el.classList.contains('projects-list-search-container')).toBeTruthy();
- expect(vm.$el.querySelectorAll('ul.list-unstyled').length).toBe(1);
- expect(vm.$el.querySelectorAll('li.projects-list-item-container').length).toBe(1);
- done();
- });
- });
-
- it('should render component element with empty message', (done) => {
- vm.projects = [];
-
- Vue.nextTick(() => {
- expect(vm.$el.querySelectorAll('li.section-empty').length).toBe(1);
- expect(vm.$el.querySelectorAll('li.projects-list-item-container').length).toBe(0);
- done();
- });
- });
-
- it('should render component element with failure message', (done) => {
- vm.searchFailed = true;
- vm.projects = [];
-
- Vue.nextTick(() => {
- expect(vm.$el.querySelectorAll('li.section-empty.section-failure').length).toBe(1);
- expect(vm.$el.querySelectorAll('li.projects-list-item-container').length).toBe(0);
- done();
- });
- });
- });
-});
diff --git a/spec/javascripts/projects_dropdown/components/search_spec.js b/spec/javascripts/projects_dropdown/components/search_spec.js
deleted file mode 100644
index 601264258c2..00000000000
--- a/spec/javascripts/projects_dropdown/components/search_spec.js
+++ /dev/null
@@ -1,101 +0,0 @@
-import Vue from 'vue';
-
-import searchComponent from '~/projects_dropdown/components/search.vue';
-import eventHub from '~/projects_dropdown/event_hub';
-
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-const createComponent = () => {
- const Component = Vue.extend(searchComponent);
-
- return mountComponent(Component);
-};
-
-describe('SearchComponent', () => {
- describe('methods', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('setFocus', () => {
- it('should set focus to search input', () => {
- spyOn(vm.$refs.search, 'focus');
-
- vm.setFocus();
- expect(vm.$refs.search.focus).toHaveBeenCalled();
- });
- });
-
- describe('emitSearchEvents', () => {
- it('should emit `searchProjects` event via eventHub when `searchQuery` present', () => {
- const searchQuery = 'test';
- spyOn(eventHub, '$emit');
- vm.searchQuery = searchQuery;
- vm.emitSearchEvents();
- expect(eventHub.$emit).toHaveBeenCalledWith('searchProjects', searchQuery);
- });
-
- it('should emit `searchCleared` event via eventHub when `searchQuery` is cleared', () => {
- spyOn(eventHub, '$emit');
- vm.searchQuery = '';
- vm.emitSearchEvents();
- expect(eventHub.$emit).toHaveBeenCalledWith('searchCleared');
- });
- });
- });
-
- describe('mounted', () => {
- it('should listen `dropdownOpen` event', (done) => {
- spyOn(eventHub, '$on');
- createComponent();
-
- Vue.nextTick(() => {
- expect(eventHub.$on).toHaveBeenCalledWith('dropdownOpen', jasmine.any(Function));
- done();
- });
- });
- });
-
- describe('beforeDestroy', () => {
- it('should unbind event listeners on eventHub', (done) => {
- const vm = createComponent();
- spyOn(eventHub, '$off');
-
- vm.$mount();
- vm.$destroy();
-
- Vue.nextTick(() => {
- expect(eventHub.$off).toHaveBeenCalledWith('dropdownOpen', jasmine.any(Function));
- done();
- });
- });
- });
-
- describe('template', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('should render component element', () => {
- const inputEl = vm.$el.querySelector('input.form-control');
-
- expect(vm.$el.classList.contains('search-input-container')).toBeTruthy();
- expect(vm.$el.classList.contains('hidden-xs')).toBeTruthy();
- expect(inputEl).not.toBe(null);
- expect(inputEl.getAttribute('placeholder')).toBe('Search your projects');
- expect(vm.$el.querySelector('.search-icon')).toBeDefined();
- });
- });
-});
diff --git a/spec/javascripts/projects_dropdown/mock_data.js b/spec/javascripts/projects_dropdown/mock_data.js
deleted file mode 100644
index d6a79fb8ac1..00000000000
--- a/spec/javascripts/projects_dropdown/mock_data.js
+++ /dev/null
@@ -1,96 +0,0 @@
-export const currentSession = {
- username: 'root',
- storageKey: 'root/frequent-projects',
- apiVersion: 'v4',
- project: {
- id: 1,
- name: 'dummy-project',
- namespace: 'SamepleGroup / Dummy-Project',
- webUrl: 'http://127.0.0.1/samplegroup/dummy-project',
- avatarUrl: null,
- lastAccessedOn: Date.now(),
- },
-};
-
-export const mockProject = {
- id: 1,
- name: 'GitLab Community Edition',
- namespace: 'gitlab-org / gitlab-ce',
- webUrl: 'http://127.0.0.1:3000/gitlab-org/gitlab-ce',
- avatarUrl: null,
-};
-
-export const mockRawProject = {
- id: 1,
- name: 'GitLab Community Edition',
- name_with_namespace: 'gitlab-org / gitlab-ce',
- web_url: 'http://127.0.0.1:3000/gitlab-org/gitlab-ce',
- avatar_url: null,
-};
-
-export const mockFrequents = [
- {
- id: 1,
- name: 'GitLab Community Edition',
- namespace: 'gitlab-org / gitlab-ce',
- webUrl: 'http://127.0.0.1:3000/gitlab-org/gitlab-ce',
- avatarUrl: null,
- },
- {
- id: 2,
- name: 'GitLab CI',
- namespace: 'gitlab-org / gitlab-ci',
- webUrl: 'http://127.0.0.1:3000/gitlab-org/gitlab-ci',
- avatarUrl: null,
- },
- {
- id: 3,
- name: 'Typeahead.Js',
- namespace: 'twitter / typeahead-js',
- webUrl: 'http://127.0.0.1:3000/twitter/typeahead-js',
- avatarUrl: '/uploads/-/system/project/avatar/7/TWBS.png',
- },
- {
- id: 4,
- name: 'Intel',
- namespace: 'platform / hardware / bsp / intel',
- webUrl: 'http://127.0.0.1:3000/platform/hardware/bsp/intel',
- avatarUrl: null,
- },
- {
- id: 5,
- name: 'v4.4',
- namespace: 'platform / hardware / bsp / kernel / common / v4.4',
- webUrl: 'http://localhost:3000/platform/hardware/bsp/kernel/common/v4.4',
- avatarUrl: null,
- },
-];
-
-export const unsortedFrequents = [
- { id: 1, frequency: 12, lastAccessedOn: 1491400843391 },
- { id: 2, frequency: 14, lastAccessedOn: 1488240890738 },
- { id: 3, frequency: 44, lastAccessedOn: 1497675908472 },
- { id: 4, frequency: 8, lastAccessedOn: 1497979281815 },
- { id: 5, frequency: 34, lastAccessedOn: 1488089211943 },
- { id: 6, frequency: 14, lastAccessedOn: 1493517292488 },
- { id: 7, frequency: 42, lastAccessedOn: 1486815299875 },
- { id: 8, frequency: 33, lastAccessedOn: 1500762279114 },
- { id: 10, frequency: 46, lastAccessedOn: 1483251641543 },
-];
-
-/**
- * This const has a specific order which tests authenticity
- * of `ProjectsService.getTopFrequentProjects` method so
- * DO NOT change order of items in this const.
- */
-export const sortedFrequents = [
- { id: 10, frequency: 46, lastAccessedOn: 1483251641543 },
- { id: 3, frequency: 44, lastAccessedOn: 1497675908472 },
- { id: 7, frequency: 42, lastAccessedOn: 1486815299875 },
- { id: 5, frequency: 34, lastAccessedOn: 1488089211943 },
- { id: 8, frequency: 33, lastAccessedOn: 1500762279114 },
- { id: 6, frequency: 14, lastAccessedOn: 1493517292488 },
- { id: 2, frequency: 14, lastAccessedOn: 1488240890738 },
- { id: 1, frequency: 12, lastAccessedOn: 1491400843391 },
- { id: 4, frequency: 8, lastAccessedOn: 1497979281815 },
-];
diff --git a/spec/javascripts/projects_dropdown/service/projects_service_spec.js b/spec/javascripts/projects_dropdown/service/projects_service_spec.js
deleted file mode 100644
index cfd1bb7d24f..00000000000
--- a/spec/javascripts/projects_dropdown/service/projects_service_spec.js
+++ /dev/null
@@ -1,179 +0,0 @@
-import Vue from 'vue';
-import VueResource from 'vue-resource';
-
-import bp from '~/breakpoints';
-import ProjectsService from '~/projects_dropdown/service/projects_service';
-import { FREQUENT_PROJECTS } from '~/projects_dropdown/constants';
-import { currentSession, unsortedFrequents, sortedFrequents } from '../mock_data';
-
-Vue.use(VueResource);
-
-FREQUENT_PROJECTS.MAX_COUNT = 3;
-
-describe('ProjectsService', () => {
- let service;
-
- beforeEach(() => {
- gon.api_version = currentSession.apiVersion;
- gon.current_user_id = 1;
- service = new ProjectsService(currentSession.username);
- });
-
- describe('contructor', () => {
- it('should initialize default properties of class', () => {
- expect(service.isLocalStorageAvailable).toBeTruthy();
- expect(service.currentUserName).toBe(currentSession.username);
- expect(service.storageKey).toBe(currentSession.storageKey);
- expect(service.projectsPath).toBeDefined();
- });
- });
-
- describe('getSearchedProjects', () => {
- it('should return promise from VueResource HTTP GET', () => {
- spyOn(service.projectsPath, 'get').and.stub();
-
- const searchQuery = 'lab';
- const queryParams = {
- simple: true,
- per_page: 20,
- membership: true,
- order_by: 'last_activity_at',
- search: searchQuery,
- };
-
- service.getSearchedProjects(searchQuery);
- expect(service.projectsPath.get).toHaveBeenCalledWith(queryParams);
- });
- });
-
- describe('logProjectAccess', () => {
- let storage;
-
- beforeEach(() => {
- storage = {};
-
- spyOn(window.localStorage, 'setItem').and.callFake((storageKey, value) => {
- storage[storageKey] = value;
- });
-
- spyOn(window.localStorage, 'getItem').and.callFake((storageKey) => {
- if (storage[storageKey]) {
- return storage[storageKey];
- }
-
- return null;
- });
- });
-
- it('should create a project store if it does not exist and adds a project', () => {
- service.logProjectAccess(currentSession.project);
-
- const projects = JSON.parse(storage[currentSession.storageKey]);
- expect(projects.length).toBe(1);
- expect(projects[0].frequency).toBe(1);
- expect(projects[0].lastAccessedOn).toBeDefined();
- });
-
- it('should prevent inserting same report multiple times into store', () => {
- service.logProjectAccess(currentSession.project);
- service.logProjectAccess(currentSession.project);
-
- const projects = JSON.parse(storage[currentSession.storageKey]);
- expect(projects.length).toBe(1);
- });
-
- it('should increase frequency of report if it was logged multiple times over the course of an hour', () => {
- let projects;
- spyOn(Math, 'abs').and.returnValue(3600001); // this will lead to `diff` > 1;
- service.logProjectAccess(currentSession.project);
-
- projects = JSON.parse(storage[currentSession.storageKey]);
- expect(projects[0].frequency).toBe(1);
-
- service.logProjectAccess(currentSession.project);
- projects = JSON.parse(storage[currentSession.storageKey]);
- expect(projects[0].frequency).toBe(2);
- expect(projects[0].lastAccessedOn).not.toBe(currentSession.project.lastAccessedOn);
- });
-
- it('should always update project metadata', () => {
- let projects;
- const oldProject = {
- ...currentSession.project,
- };
-
- const newProject = {
- ...currentSession.project,
- name: 'New Name',
- avatarUrl: 'new/avatar.png',
- namespace: 'New / Namespace',
- webUrl: 'http://localhost/new/web/url',
- };
-
- service.logProjectAccess(oldProject);
- projects = JSON.parse(storage[currentSession.storageKey]);
- expect(projects[0].name).toBe(oldProject.name);
- expect(projects[0].avatarUrl).toBe(oldProject.avatarUrl);
- expect(projects[0].namespace).toBe(oldProject.namespace);
- expect(projects[0].webUrl).toBe(oldProject.webUrl);
-
- service.logProjectAccess(newProject);
- projects = JSON.parse(storage[currentSession.storageKey]);
- expect(projects[0].name).toBe(newProject.name);
- expect(projects[0].avatarUrl).toBe(newProject.avatarUrl);
- expect(projects[0].namespace).toBe(newProject.namespace);
- expect(projects[0].webUrl).toBe(newProject.webUrl);
- });
-
- it('should not add more than 20 projects in store', () => {
- for (let i = 1; i <= 5; i += 1) {
- const project = Object.assign(currentSession.project, { id: i });
- service.logProjectAccess(project);
- }
-
- const projects = JSON.parse(storage[currentSession.storageKey]);
- expect(projects.length).toBe(3);
- });
- });
-
- describe('getTopFrequentProjects', () => {
- let storage = {};
-
- beforeEach(() => {
- storage[currentSession.storageKey] = JSON.stringify(unsortedFrequents);
-
- spyOn(window.localStorage, 'getItem').and.callFake((storageKey) => {
- if (storage[storageKey]) {
- return storage[storageKey];
- }
-
- return null;
- });
- });
-
- it('should return top 5 frequently accessed projects for desktop screens', () => {
- spyOn(bp, 'getBreakpointSize').and.returnValue('md');
- const frequentProjects = service.getTopFrequentProjects();
-
- expect(frequentProjects.length).toBe(5);
- frequentProjects.forEach((project, index) => {
- expect(project.id).toBe(sortedFrequents[index].id);
- });
- });
-
- it('should return top 3 frequently accessed projects for mobile screens', () => {
- spyOn(bp, 'getBreakpointSize').and.returnValue('sm');
- const frequentProjects = service.getTopFrequentProjects();
-
- expect(frequentProjects.length).toBe(3);
- frequentProjects.forEach((project, index) => {
- expect(project.id).toBe(sortedFrequents[index].id);
- });
- });
-
- it('should return empty array if there are no projects available in store', () => {
- storage = {};
- expect(service.getTopFrequentProjects().length).toBe(0);
- });
- });
-});
diff --git a/spec/javascripts/projects_dropdown/store/projects_store_spec.js b/spec/javascripts/projects_dropdown/store/projects_store_spec.js
deleted file mode 100644
index e57399d37cd..00000000000
--- a/spec/javascripts/projects_dropdown/store/projects_store_spec.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import ProjectsStore from '~/projects_dropdown/store/projects_store';
-import { mockProject, mockRawProject } from '../mock_data';
-
-describe('ProjectsStore', () => {
- let store;
-
- beforeEach(() => {
- store = new ProjectsStore();
- });
-
- describe('setFrequentProjects', () => {
- it('should set frequent projects list to state', () => {
- store.setFrequentProjects([mockProject]);
-
- expect(store.getFrequentProjects().length).toBe(1);
- expect(store.getFrequentProjects()[0].id).toBe(mockProject.id);
- });
- });
-
- describe('setSearchedProjects', () => {
- it('should set searched projects list to state', () => {
- store.setSearchedProjects([mockRawProject]);
-
- const processedProjects = store.getSearchedProjects();
- expect(processedProjects.length).toBe(1);
- expect(processedProjects[0].id).toBe(mockRawProject.id);
- expect(processedProjects[0].namespace).toBe(mockRawProject.name_with_namespace);
- expect(processedProjects[0].webUrl).toBe(mockRawProject.web_url);
- expect(processedProjects[0].avatarUrl).toBe(mockRawProject.avatar_url);
- });
- });
-
- describe('clearSearchedProjects', () => {
- it('should clear searched projects list from state', () => {
- store.setSearchedProjects([mockRawProject]);
- expect(store.getSearchedProjects().length).toBe(1);
- store.clearSearchedProjects();
- expect(store.getSearchedProjects().length).toBe(0);
- });
- });
-});
diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js
index e264b16335f..6d49536a712 100644
--- a/spec/javascripts/right_sidebar_spec.js
+++ b/spec/javascripts/right_sidebar_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, new-parens, no-return-assign, new-cap, vars-on-top, max-len */
+/* eslint-disable no-var, one-var, one-var-declaration-per-line, no-return-assign, vars-on-top, max-len */
import $ from 'jquery';
import MockAdapter from 'axios-mock-adapter';
diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js
index 4f515f98a7e..86c001678c5 100644
--- a/spec/javascripts/search_autocomplete_spec.js
+++ b/spec/javascripts/search_autocomplete_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable space-before-function-paren, max-len, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, consistent-return, no-param-reassign, default-case, no-return-assign, comma-dangle, object-shorthand, prefer-template, quotes, new-parens, vars-on-top, new-cap, max-len */
+/* eslint-disable max-len, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, consistent-return, no-param-reassign, default-case, no-return-assign, object-shorthand, prefer-template, vars-on-top, max-len */
import $ from 'jquery';
import '~/gl_dropdown';
diff --git a/spec/javascripts/settings_panels_spec.js b/spec/javascripts/settings_panels_spec.js
index 4fba36bd4de..c1a69bd7018 100644
--- a/spec/javascripts/settings_panels_spec.js
+++ b/spec/javascripts/settings_panels_spec.js
@@ -9,11 +9,11 @@ describe('Settings Panels', () => {
describe('initSettingsPane', () => {
afterEach(() => {
- location.hash = '';
+ window.location.hash = '';
});
it('should expand linked hash fragment panel', () => {
- location.hash = '#autodevops-settings';
+ window.location.hash = '#autodevops-settings';
const pipelineSettingsPanel = document.querySelector('#autodevops-settings');
// Our test environment automatically expands everything so we need to clear that out first
diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js
index d73608ed0ed..a4753ab7cde 100644
--- a/spec/javascripts/shortcuts_issuable_spec.js
+++ b/spec/javascripts/shortcuts_issuable_spec.js
@@ -4,71 +4,102 @@ import ShortcutsIssuable from '~/shortcuts_issuable';
initCopyAsGFM();
-describe('ShortcutsIssuable', function () {
- const fixtureName = 'merge_requests/diff_comment.html.raw';
+const FORM_SELECTOR = '.js-main-target-form .js-vue-comment-form';
+
+describe('ShortcutsIssuable', function() {
+ const fixtureName = 'snippets/show.html.raw';
preloadFixtures(fixtureName);
+
beforeEach(() => {
loadFixtures(fixtureName);
+ $('body').append(
+ `<div class="js-main-target-form">
+ <textare class="js-vue-comment-form"></textare>
+ </div>`,
+ );
document.querySelector('.js-new-note-form').classList.add('js-main-target-form');
this.shortcut = new ShortcutsIssuable(true);
});
+
+ afterEach(() => {
+ $(FORM_SELECTOR).remove();
+ });
+
describe('replyWithSelectedText', () => {
// Stub window.gl.utils.getSelectedFragment to return a node with the provided HTML.
- const stubSelection = (html) => {
+ const stubSelection = html => {
window.gl.utils.getSelectedFragment = () => {
const node = document.createElement('div');
node.innerHTML = html;
+
return node;
};
};
- beforeEach(() => {
- this.selector = '.js-main-target-form #note_note';
- });
describe('with empty selection', () => {
it('does not return an error', () => {
- this.shortcut.replyWithSelectedText(true);
- expect($(this.selector).val()).toBe('');
+ ShortcutsIssuable.replyWithSelectedText(true);
+
+ expect($(FORM_SELECTOR).val()).toBe('');
});
+
it('triggers `focus`', () => {
- this.shortcut.replyWithSelectedText(true);
- expect(document.activeElement).toBe(document.querySelector(this.selector));
+ const spy = spyOn(document.querySelector(FORM_SELECTOR), 'focus');
+ ShortcutsIssuable.replyWithSelectedText(true);
+
+ expect(spy).toHaveBeenCalled();
});
});
+
describe('with any selection', () => {
beforeEach(() => {
stubSelection('<p>Selected text.</p>');
});
+
it('leaves existing input intact', () => {
- $(this.selector).val('This text was already here.');
- expect($(this.selector).val()).toBe('This text was already here.');
- this.shortcut.replyWithSelectedText(true);
- expect($(this.selector).val()).toBe('This text was already here.\n\n> Selected text.\n\n');
+ $(FORM_SELECTOR).val('This text was already here.');
+ expect($(FORM_SELECTOR).val()).toBe('This text was already here.');
+
+ ShortcutsIssuable.replyWithSelectedText(true);
+ expect($(FORM_SELECTOR).val()).toBe('This text was already here.\n\n> Selected text.\n\n');
});
+
it('triggers `input`', () => {
let triggered = false;
- $(this.selector).on('input', () => {
+ $(FORM_SELECTOR).on('input', () => {
triggered = true;
});
- this.shortcut.replyWithSelectedText(true);
+
+ ShortcutsIssuable.replyWithSelectedText(true);
expect(triggered).toBe(true);
});
+
it('triggers `focus`', () => {
- this.shortcut.replyWithSelectedText(true);
- expect(document.activeElement).toBe(document.querySelector(this.selector));
+ const spy = spyOn(document.querySelector(FORM_SELECTOR), 'focus');
+ ShortcutsIssuable.replyWithSelectedText(true);
+
+ expect(spy).toHaveBeenCalled();
});
});
+
describe('with a one-line selection', () => {
it('quotes the selection', () => {
stubSelection('<p>This text has been selected.</p>');
- this.shortcut.replyWithSelectedText(true);
- expect($(this.selector).val()).toBe('> This text has been selected.\n\n');
+ ShortcutsIssuable.replyWithSelectedText(true);
+
+ expect($(FORM_SELECTOR).val()).toBe('> This text has been selected.\n\n');
});
});
+
describe('with a multi-line selection', () => {
it('quotes the selected lines as a group', () => {
- stubSelection('<p>Selected line one.</p>\n\n<p>Selected line two.</p>\n\n<p>Selected line three.</p>');
- this.shortcut.replyWithSelectedText(true);
- expect($(this.selector).val()).toBe('> Selected line one.\n>\n> Selected line two.\n>\n> Selected line three.\n\n');
+ stubSelection(
+ '<p>Selected line one.</p>\n<p>Selected line two.</p>\n<p>Selected line three.</p>',
+ );
+ ShortcutsIssuable.replyWithSelectedText(true);
+
+ expect($(FORM_SELECTOR).val()).toBe(
+ '> Selected line one.\n>\n> Selected line two.\n>\n> Selected line three.\n\n',
+ );
});
});
});
diff --git a/spec/javascripts/shortcuts_spec.js b/spec/javascripts/shortcuts_spec.js
index ee92295ef5e..94cded7ee37 100644
--- a/spec/javascripts/shortcuts_spec.js
+++ b/spec/javascripts/shortcuts_spec.js
@@ -2,10 +2,11 @@ import $ from 'jquery';
import Shortcuts from '~/shortcuts';
describe('Shortcuts', () => {
- const fixtureName = 'merge_requests/diff_comment.html.raw';
- const createEvent = (type, target) => $.Event(type, {
- target,
- });
+ const fixtureName = 'snippets/show.html.raw';
+ const createEvent = (type, target) =>
+ $.Event(type, {
+ target,
+ });
preloadFixtures(fixtureName);
@@ -21,19 +22,19 @@ describe('Shortcuts', () => {
it('focuses preview button in form', () => {
Shortcuts.toggleMarkdownPreview(
- createEvent('KeyboardEvent', document.querySelector('.js-new-note-form .js-note-text'),
- ));
+ createEvent('KeyboardEvent', document.querySelector('.js-new-note-form .js-note-text')),
+ );
expect('focus').toHaveBeenTriggeredOn('.js-new-note-form .js-md-preview-button');
});
- it('focues preview button inside edit comment form', (done) => {
+ it('focues preview button inside edit comment form', done => {
document.querySelector('.js-note-edit').click();
setTimeout(() => {
Shortcuts.toggleMarkdownPreview(
- createEvent('KeyboardEvent', document.querySelector('.edit-note .js-note-text'),
- ));
+ createEvent('KeyboardEvent', document.querySelector('.edit-note .js-note-text')),
+ );
expect('focus').not.toHaveBeenTriggeredOn('.js-new-note-form .js-md-preview-button');
expect('focus').toHaveBeenTriggeredOn('.edit-note .js-md-preview-button');
diff --git a/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js b/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js
index 0c173062835..6110d5d89ac 100644
--- a/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js
+++ b/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js
@@ -8,10 +8,7 @@ describe('Confidential Issue Sidebar Block', () => {
beforeEach(() => {
const Component = Vue.extend(confidentialIssueSidebar);
const service = {
- update: () => new Promise((resolve, reject) => {
- resolve(true);
- reject('failed!');
- }),
+ update: () => Promise.resolve(true),
};
vm1 = new Component({
diff --git a/spec/javascripts/sidebar/sidebar_move_issue_spec.js b/spec/javascripts/sidebar/sidebar_move_issue_spec.js
index 00847df4b60..8f35b9ca437 100644
--- a/spec/javascripts/sidebar/sidebar_move_issue_spec.js
+++ b/spec/javascripts/sidebar/sidebar_move_issue_spec.js
@@ -14,7 +14,9 @@ describe('SidebarMoveIssue', function () {
this.$content = $(`
<div class="dropdown">
<div class="js-toggle"></div>
- <div class="dropdown-content"></div>
+ <div class="dropdown-menu">
+ <div class="dropdown-content"></div>
+ </div>
<div class="js-confirm-button"></div>
</div>
`);
diff --git a/spec/javascripts/signin_tabs_memoizer_spec.js b/spec/javascripts/signin_tabs_memoizer_spec.js
index 423432c9e5d..9d3905fa1d8 100644
--- a/spec/javascripts/signin_tabs_memoizer_spec.js
+++ b/spec/javascripts/signin_tabs_memoizer_spec.js
@@ -45,6 +45,21 @@ import SigninTabsMemoizer from '~/pages/sessions/new/signin_tabs_memoizer';
expect(fakeTab.click).toHaveBeenCalled();
});
+ it('clicks the first tab if value in local storage is bad', () => {
+ createMemoizer().saveData('#bogus');
+ const fakeTab = {
+ click: () => {},
+ };
+ spyOn(document, 'querySelector').and.callFake(selector => (selector === `${tabSelector} a[href="#bogus"]` ? null : fakeTab));
+ spyOn(fakeTab, 'click');
+
+ memo.bootstrap();
+
+ // verify that triggers click on stored selector and fallback
+ expect(document.querySelector.calls.allArgs()).toEqual([['ul.new-session-tabs a[href="#bogus"]'], ['ul.new-session-tabs a']]);
+ expect(fakeTab.click).toHaveBeenCalled();
+ });
+
it('saves last selected tab on change', () => {
createMemoizer();
diff --git a/spec/javascripts/smart_interval_spec.js b/spec/javascripts/smart_interval_spec.js
index a54219d58c2..d9b6dd1d487 100644
--- a/spec/javascripts/smart_interval_spec.js
+++ b/spec/javascripts/smart_interval_spec.js
@@ -1,12 +1,12 @@
import $ from 'jquery';
import _ from 'underscore';
import SmartInterval from '~/smart_interval';
+import waitForPromises from 'spec/helpers/wait_for_promises';
describe('SmartInterval', function () {
const DEFAULT_MAX_INTERVAL = 100;
const DEFAULT_STARTING_INTERVAL = 5;
const DEFAULT_SHORT_TIMEOUT = 75;
- const DEFAULT_LONG_TIMEOUT = 1000;
const DEFAULT_INCREMENT_FACTOR = 2;
function createDefaultSmartInterval(config) {
@@ -27,52 +27,65 @@ describe('SmartInterval', function () {
return new SmartInterval(defaultParams);
}
+ beforeEach(() => {
+ jasmine.clock().install();
+ });
+
+ afterEach(() => {
+ jasmine.clock().uninstall();
+ });
+
describe('Increment Interval', function () {
- beforeEach(function () {
- this.smartInterval = createDefaultSmartInterval();
- });
+ it('should increment the interval delay', (done) => {
+ const 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)
+ jasmine.clock().tick(DEFAULT_SHORT_TIMEOUT);
+
+ waitForPromises()
+ .then(() => {
+ const intervalConfig = smartInterval.cfg;
+ const iterationCount = 4;
+ const maxIntervalAfterIterations = intervalConfig.startingInterval *
+ (intervalConfig.incrementByFactorOf ** iterationCount);
+ const currentInterval = smartInterval.getCurrentInterval();
+
+ // Provide some flexibility for performance of testing environment
+ expect(currentInterval).toBeGreaterThan(intervalConfig.startingInterval);
+ expect(currentInterval).toBeLessThanOrEqual(maxIntervalAfterIterations);
+ })
+ .then(done)
+ .catch(done.fail);
});
- it('should not increment past maxInterval', function (done) {
- const interval = this.smartInterval;
+ it('should not increment past maxInterval', (done) => {
+ const smartInterval = createDefaultSmartInterval({ maxInterval: DEFAULT_STARTING_INTERVAL });
- setTimeout(() => {
- const currentInterval = interval.getCurrentInterval();
- expect(currentInterval).toBe(interval.cfg.maxInterval);
+ jasmine.clock().tick(DEFAULT_STARTING_INTERVAL);
+ jasmine.clock().tick(DEFAULT_STARTING_INTERVAL * DEFAULT_INCREMENT_FACTOR);
- done();
- }, DEFAULT_LONG_TIMEOUT);
+ waitForPromises()
+ .then(() => {
+ const currentInterval = smartInterval.getCurrentInterval();
+ expect(currentInterval).toBe(smartInterval.cfg.maxInterval);
+ })
+ .then(done)
+ .catch(done.fail);
});
- it('does not increment while waiting for callback', function () {
- jasmine.clock().install();
-
+ it('does not increment while waiting for callback', done => {
const smartInterval = createDefaultSmartInterval({
callback: () => new Promise($.noop),
});
jasmine.clock().tick(DEFAULT_SHORT_TIMEOUT);
- const oneInterval = smartInterval.cfg.startingInterval * DEFAULT_INCREMENT_FACTOR;
- expect(smartInterval.getCurrentInterval()).toEqual(oneInterval);
-
- jasmine.clock().uninstall();
+ waitForPromises()
+ .then(() => {
+ const oneInterval = smartInterval.cfg.startingInterval * DEFAULT_INCREMENT_FACTOR;
+ expect(smartInterval.getCurrentInterval()).toEqual(oneInterval);
+ })
+ .then(done)
+ .catch(done.fail);
});
});
@@ -84,34 +97,39 @@ describe('SmartInterval', function () {
it('should cancel an interval', function (done) {
const interval = this.smartInterval;
- setTimeout(() => {
- interval.cancel();
+ jasmine.clock().tick(DEFAULT_SHORT_TIMEOUT);
- const intervalId = interval.state.intervalId;
- const currentInterval = interval.getCurrentInterval();
- const intervalLowerLimit = interval.cfg.startingInterval;
+ interval.cancel();
- expect(intervalId).toBeUndefined();
- expect(currentInterval).toBe(intervalLowerLimit);
+ waitForPromises()
+ .then(() => {
+ const { intervalId } = interval.state;
+ const currentInterval = interval.getCurrentInterval();
+ const intervalLowerLimit = interval.cfg.startingInterval;
- done();
- }, DEFAULT_SHORT_TIMEOUT);
+ expect(intervalId).toBeUndefined();
+ expect(currentInterval).toBe(intervalLowerLimit);
+ })
+ .then(done)
+ .catch(done.fail);
});
it('should resume an interval', function (done) {
const interval = this.smartInterval;
- setTimeout(() => {
- interval.cancel();
-
- interval.resume();
+ jasmine.clock().tick(DEFAULT_SHORT_TIMEOUT);
- const intervalId = interval.state.intervalId;
+ interval.cancel();
- expect(intervalId).toBeTruthy();
+ interval.resume();
- done();
- }, DEFAULT_SHORT_TIMEOUT);
+ waitForPromises()
+ .then(() => {
+ const { intervalId } = interval.state;
+ expect(intervalId).toBeTruthy();
+ })
+ .then(done)
+ .catch(done.fail);
});
});
@@ -126,64 +144,79 @@ describe('SmartInterval', function () {
it('should pause when page is not visible', function (done) {
const interval = this.smartInterval;
- setTimeout(() => {
- expect(interval.state.intervalId).toBeTruthy();
+ jasmine.clock().tick(DEFAULT_SHORT_TIMEOUT);
+
+ waitForPromises()
+ .then(() => {
+ 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();
+ })
+ .then(done)
+ .catch(done.fail);
});
- it('should change to the hidden interval when page is not visible', function (done) {
+ it('should change to the hidden interval when page is not visible', 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();
+ jasmine.clock().tick(DEFAULT_SHORT_TIMEOUT);
+
+ waitForPromises()
+ .then(() => {
+ 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);
+ })
+ .then(done)
+ .catch(done.fail);
});
it('should resume when page is becomes visible at the previous interval', function (done) {
const interval = this.smartInterval;
- setTimeout(() => {
- expect(interval.state.intervalId).toBeTruthy();
+ jasmine.clock().tick(DEFAULT_SHORT_TIMEOUT);
- // simulates triggering of visibilitychange event
- interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } });
+ waitForPromises()
+ .then(() => {
+ expect(interval.state.intervalId).toBeTruthy();
- expect(interval.state.intervalId).toBeUndefined();
+ // simulates triggering of visibilitychange event
+ interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } });
- // simulates triggering of visibilitychange event
- interval.handleVisibilityChange({ target: { visibilityState: 'visible' } });
+ expect(interval.state.intervalId).toBeUndefined();
- expect(interval.state.intervalId).toBeTruthy();
+ // simulates triggering of visibilitychange event
+ interval.handleVisibilityChange({ target: { visibilityState: 'visible' } });
- done();
- }, DEFAULT_SHORT_TIMEOUT);
+ expect(interval.state.intervalId).toBeTruthy();
+ })
+ .then(done)
+ .catch(done.fail);
});
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);
+ jasmine.clock().tick(DEFAULT_SHORT_TIMEOUT);
+
+ waitForPromises()
+ .then(() => {
+ $(document).triggerHandler('beforeunload');
+ expect(interval.state.intervalId).toBeUndefined();
+ expect(interval.getCurrentInterval()).toBe(interval.cfg.startingInterval);
+ })
+ .then(done)
+ .catch(done.fail);
});
it('should execute callback before first interval', function () {
diff --git a/spec/javascripts/syntax_highlight_spec.js b/spec/javascripts/syntax_highlight_spec.js
index 0d1fa680e00..1c3dac3584e 100644
--- a/spec/javascripts/syntax_highlight_spec.js
+++ b/spec/javascripts/syntax_highlight_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable space-before-function-paren, no-var, no-return-assign, quotes */
+/* eslint-disable no-var, no-return-assign, quotes */
import $ from 'jquery';
import syntaxHighlight from '~/syntax_highlight';
diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js
index 2411d33a496..0eff98bcc9d 100644
--- a/spec/javascripts/test_bundle.js
+++ b/spec/javascripts/test_bundle.js
@@ -3,7 +3,6 @@
import $ from 'jquery';
import 'vendor/jasmine-jquery';
import '~/commons';
-
import Vue from 'vue';
import VueResource from 'vue-resource';
import Translate from '~/vue_shared/translate';
@@ -39,7 +38,8 @@ jasmine.getJSONFixtures().fixturesPath = FIXTURES_PATH;
beforeAll(() => jasmine.addMatchers(customMatchers));
// globalize common libraries
-window.$ = window.jQuery = $;
+window.$ = $;
+window.jQuery = window.$;
// stub expected globals
window.gl = window.gl || {};
@@ -90,7 +90,8 @@ testsContext.keys().forEach(function(path) {
try {
testsContext(path);
} catch (err) {
- console.error('[ERROR] Unable to load spec: ', path);
+ console.log(err);
+ console.error('[GL SPEC RUNNER ERROR] Unable to load spec: ', path);
describe('Test bundle', function() {
it(`includes '${path}'`, function() {
expect(err).toBeNull();
@@ -134,7 +135,7 @@ if (process.env.BABEL_ENV === 'coverage') {
// exempt these files from the coverage report
const troubleMakers = [
'./blob_edit/blob_bundle.js',
- './boards/components/modal/empty_state.js',
+ './boards/components/modal/empty_state.vue',
'./boards/components/modal/footer.js',
'./boards/components/modal/header.js',
'./cycle_analytics/cycle_analytics_bundle.js',
diff --git a/spec/javascripts/test_constants.js b/spec/javascripts/test_constants.js
index df59195e9f6..a820dd2d09c 100644
--- a/spec/javascripts/test_constants.js
+++ b/spec/javascripts/test_constants.js
@@ -2,3 +2,6 @@ export const FIXTURES_PATH = '/base/spec/javascripts/fixtures';
export const TEST_HOST = 'http://test.host';
export const DUMMY_IMAGE_URL = `${FIXTURES_PATH}/one_white_pixel.png`;
+
+export const GREEN_BOX_IMAGE_URL = `${FIXTURES_PATH}/images/green_box.png`;
+export const RED_BOX_IMAGE_URL = `${FIXTURES_PATH}/images/red_box.png`;
diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js
index d84b13b07c4..57e0caa692c 100644
--- a/spec/javascripts/u2f/authenticate_spec.js
+++ b/spec/javascripts/u2f/authenticate_spec.js
@@ -6,7 +6,7 @@ import MockU2FDevice from './mock_u2f_device';
describe('U2FAuthenticate', function () {
preloadFixtures('u2f/authenticate.html.raw');
- beforeEach((done) => {
+ beforeEach(() => {
loadFixtures('u2f/authenticate.html.raw');
this.u2fDevice = new MockU2FDevice();
this.container = $('#js-authenticate-u2f');
@@ -19,46 +19,70 @@ describe('U2FAuthenticate', function () {
document.querySelector('#js-login-2fa-device'),
document.querySelector('.js-2fa-form'),
);
+ });
- // bypass automatic form submission within renderAuthenticated
- spyOn(this.component, 'renderAuthenticated').and.returnValue(true);
+ describe('with u2f unavailable', () => {
+ beforeEach(() => {
+ spyOn(this.component, 'switchToFallbackUI');
+ this.oldu2f = window.u2f;
+ window.u2f = null;
+ });
- this.component.start().then(done).catch(done.fail);
- });
+ afterEach(() => {
+ window.u2f = this.oldu2f;
+ });
- 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('falls back to normal 2fa', (done) => {
+ this.component.start().then(() => {
+ expect(this.component.switchToFallbackUI).toHaveBeenCalled();
+ done();
+ }).catch(done.fail);
});
- expect(this.component.renderAuthenticated).toHaveBeenCalledWith('{"deviceData":"this is data from the device"}');
});
- describe('errors', () => {
- it('displays an error message', () => {
- const setupButton = this.container.find('#js-login-u2f-device');
- setupButton.trigger('click');
- this.u2fDevice.respondToAuthenticateRequest({
- errorCode: 'error!',
- });
- const errorMessage = this.container.find('p');
- return expect(errorMessage.text()).toContain('There was a problem communicating with your device');
+ describe('with u2f available', () => {
+ beforeEach((done) => {
+ // bypass automatic form submission within renderAuthenticated
+ spyOn(this.component, 'renderAuthenticated').and.returnValue(true);
+ this.u2fDevice = new MockU2FDevice();
+
+ this.component.start().then(done).catch(done.fail);
});
- 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!',
- });
- const retryButton = this.container.find('#js-u2f-try-again');
- retryButton.trigger('click');
- setupButton = this.container.find('#js-login-u2f-device');
- setupButton.trigger('click');
+
+ 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',
});
expect(this.component.renderAuthenticated).toHaveBeenCalledWith('{"deviceData":"this is data from the device"}');
});
+
+ describe('errors', () => {
+ it('displays an error message', () => {
+ const setupButton = this.container.find('#js-login-u2f-device');
+ setupButton.trigger('click');
+ this.u2fDevice.respondToAuthenticateRequest({
+ errorCode: 'error!',
+ });
+ const 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!',
+ });
+ 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"}');
+ });
+ });
});
});
diff --git a/spec/javascripts/u2f/mock_u2f_device.js b/spec/javascripts/u2f/mock_u2f_device.js
index 5a1ace2b4d6..012a1cefbbf 100644
--- a/spec/javascripts/u2f/mock_u2f_device.js
+++ b/spec/javascripts/u2f/mock_u2f_device.js
@@ -1,5 +1,4 @@
-/* eslint-disable prefer-rest-params, wrap-iife,
-no-unused-expressions, no-return-assign, no-param-reassign*/
+/* eslint-disable wrap-iife, no-unused-expressions, no-return-assign, no-param-reassign */
export default class MockU2FDevice {
constructor() {
diff --git a/spec/javascripts/vue_mr_widget/components/deployment_spec.js b/spec/javascripts/vue_mr_widget/components/deployment_spec.js
index c82ba61a5b1..50c2b0e2bd0 100644
--- a/spec/javascripts/vue_mr_widget/components/deployment_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/deployment_spec.js
@@ -153,7 +153,7 @@ describe('Deployment component', () => {
it('renders external URL', () => {
expect(el.querySelector('.js-deploy-url').getAttribute('href')).toEqual(deploymentMockData.external_url);
- expect(el.querySelector('.js-deploy-url').innerText).toContain(deploymentMockData.external_url_formatted);
+ expect(el.querySelector('.js-deploy-url').innerText).toContain('View app');
});
it('renders stop button', () => {
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js
index 6784b498c29..10143402acf 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js
@@ -1,12 +1,12 @@
import Vue from 'vue';
-import authorTimeComponent from '~/vue_merge_request_widget/components/mr_widget_author_time.vue';
+import MrWidgetAuthorTime from '~/vue_merge_request_widget/components/mr_widget_author_time.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
-describe('MRWidgetAuthorTime', () => {
+describe('MrWidgetAuthorTime', () => {
let vm;
beforeEach(() => {
- const Component = Vue.extend(authorTimeComponent);
+ const Component = Vue.extend(MrWidgetAuthorTime);
vm = mountComponent(Component, {
actionText: 'Merged by',
@@ -34,7 +34,7 @@ describe('MRWidgetAuthorTime', () => {
});
it('renders provided time', () => {
- expect(vm.$el.querySelector('time').getAttribute('title')).toEqual('2017-03-23T23:02:00.807Z');
+ expect(vm.$el.querySelector('time').getAttribute('data-original-title')).toEqual('2017-03-23T23:02:00.807Z');
expect(vm.$el.querySelector('time').textContent.trim()).toEqual('12 hours ago');
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
index 9b9c9656979..61b7bd2c226 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
@@ -12,6 +12,7 @@ describe('MRWidgetHeader', () => {
afterEach(() => {
vm.$destroy();
+ gon.relative_url_root = '';
});
describe('computed', () => {
@@ -144,8 +145,17 @@ describe('MRWidgetHeader', () => {
it('renders web ide button', () => {
const button = vm.$el.querySelector('.js-web-ide');
- expect(button.textContent.trim()).toEqual('Web IDE');
- expect(button.getAttribute('href')).toEqual('undefined/-/ide/projectabc');
+ expect(button.textContent.trim()).toEqual('Open in Web IDE');
+ expect(button.getAttribute('href')).toEqual('/-/ide/projectabc');
+ });
+
+ it('renders web ide button with relative URL', () => {
+ gon.relative_url_root = '/gitlab';
+
+ const button = vm.$el.querySelector('.js-web-ide');
+
+ expect(button.textContent.trim()).toEqual('Open in Web IDE');
+ expect(button.getAttribute('href')).toEqual('/-/ide/projectabc');
});
it('renders download dropdown with links', () => {
@@ -243,8 +253,8 @@ describe('MRWidgetHeader', () => {
});
it('renders diverged commits info', () => {
- expect(vm.$el.querySelector('.diverged-commits-count').textContent.trim()).toEqual(
- '(12 commits behind)',
+ expect(vm.$el.querySelector('.diverged-commits-count').textContent).toMatch(
+ /(mr-widget-refactor[\s\S]+?is 12 commits behind[\s\S]+?master)/,
);
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js
index a0a74648328..8de99fd3c96 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js
@@ -6,6 +6,7 @@ import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('MRWidgetFailedToMerge', () => {
const dummyIntervalId = 1337;
let Component;
+ let mr;
let vm;
beforeEach(() => {
@@ -13,10 +14,11 @@ describe('MRWidgetFailedToMerge', () => {
spyOn(eventHub, '$emit');
spyOn(window, 'setInterval').and.returnValue(dummyIntervalId);
spyOn(window, 'clearInterval').and.stub();
+ mr = {
+ mergeError: 'Merge error happened',
+ };
vm = mountComponent(Component, {
- mr: {
- mergeError: 'Merge error happened.',
- },
+ mr,
});
});
@@ -44,6 +46,19 @@ describe('MRWidgetFailedToMerge', () => {
expect(vm.timerText).toEqual('Refreshing in a second to show the updated status...');
});
});
+
+ describe('mergeError', () => {
+ it('removes forced line breaks', done => {
+ mr.mergeError = 'contains<br />line breaks<br />';
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vm.mergeError).toBe('contains line breaks');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
});
describe('created', () => {
@@ -103,7 +118,7 @@ describe('MRWidgetFailedToMerge', () => {
it('renders given error', () => {
expect(vm.$el.querySelector('.has-error-message').textContent.trim()).toEqual(
- 'Merge error happened..',
+ 'Merge error happened.',
);
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js
index adeea03481f..efa5c878678 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js
@@ -39,7 +39,8 @@ describe('MRWidgetMerged', () => {
readableClosedAt: '',
},
updatedAt: 'mergedUpdatedAt',
- shortMergeCommitSha: 'asdf1234',
+ shortMergeCommitSha: '958c0475',
+ mergeCommitSha: '958c047516e182dfc52317f721f696e8a1ee85ed',
mergeCommitPath: 'http://localhost:3000/root/nautilus/commit/f7ce827c314c9340b075657fd61c789fb01cf74d',
sourceBranch: 'bar',
targetBranch,
@@ -153,7 +154,7 @@ describe('MRWidgetMerged', () => {
it('shows button to copy commit SHA to clipboard', () => {
expect(selectors.copyMergeShaButton).toExist();
- expect(selectors.copyMergeShaButton.getAttribute('data-clipboard-text')).toBe(vm.mr.shortMergeCommitSha);
+ expect(selectors.copyMergeShaButton.getAttribute('data-clipboard-text')).toBe(vm.mr.mergeCommitSha);
});
it('shows merge commit SHA link', () => {
@@ -186,7 +187,7 @@ describe('MRWidgetMerged', () => {
it('should use mergedEvent mergedAt as tooltip title', () => {
expect(
- vm.$el.querySelector('time').getAttribute('title'),
+ vm.$el.querySelector('time').getAttribute('data-original-title'),
).toBe('Jan 24, 2018 1:02pm GMT+0000');
});
});
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 30918428da2..6342ea00436 100644
--- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
@@ -5,6 +5,7 @@ import notify from '~/lib/utils/notify';
import { stateKey } from '~/vue_merge_request_widget/stores/state_maps';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import mockData from './mock_data';
+import { faviconDataUrl, overlayDataUrl, faviconWithOverlayDataUrl } from '../lib/utils/mock_data';
const returnPromise = data => new Promise((resolve) => {
resolve({
@@ -273,6 +274,7 @@ describe('mrWidgetOptions', () => {
beforeEach(() => {
const favicon = document.createElement('link');
favicon.setAttribute('id', 'favicon');
+ favicon.setAttribute('data-original-href', faviconDataUrl);
document.body.appendChild(favicon);
faviconElement = document.getElementById('favicon');
@@ -282,10 +284,13 @@ describe('mrWidgetOptions', () => {
document.body.removeChild(document.getElementById('favicon'));
});
- it('should call setFavicon method', () => {
- vm.setFaviconHelper();
-
- expect(faviconElement.getAttribute('href')).toEqual(vm.mr.ciStatusFaviconPath);
+ it('should call setFavicon method', (done) => {
+ vm.mr.ciStatusFaviconPath = overlayDataUrl;
+ vm.setFaviconHelper().then(() => {
+ expect(faviconElement.getAttribute('href')).toEqual(faviconWithOverlayDataUrl);
+ done();
+ })
+ .catch(done.fail);
});
it('should not call setFavicon when there is no ciStatusFaviconPath', () => {
diff --git a/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js b/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js
index 383f0cd29ea..e2c34508b0d 100644
--- a/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js
+++ b/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js
@@ -3,6 +3,7 @@ import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import contentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import { GREEN_BOX_IMAGE_URL } from 'spec/test_constants';
describe('ContentViewer', () => {
let vm;
@@ -41,12 +42,12 @@ describe('ContentViewer', () => {
it('renders image preview', done => {
createComponent({
- path: 'test.jpg',
+ path: GREEN_BOX_IMAGE_URL,
fileSize: 1024,
});
setTimeout(() => {
- expect(vm.$el.querySelector('.image_file img').getAttribute('src')).toBe('test.jpg');
+ expect(vm.$el.querySelector('.image_file img').getAttribute('src')).toBe(GREEN_BOX_IMAGE_URL);
done();
});
@@ -59,9 +60,8 @@ describe('ContentViewer', () => {
});
setTimeout(() => {
- expect(vm.$el.querySelector('.file-info').textContent.trim()).toContain(
- 'test.abc (1.00 KiB)',
- );
+ expect(vm.$el.querySelector('.file-info').textContent.trim()).toContain('test.abc');
+ expect(vm.$el.querySelector('.file-info').textContent.trim()).toContain('(1.00 KiB)');
expect(vm.$el.querySelector('.btn.btn-default').textContent.trim()).toContain('Download');
done();
diff --git a/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js b/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js
new file mode 100644
index 00000000000..71d9145bf22
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js
@@ -0,0 +1,70 @@
+import Vue from 'vue';
+import diffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import { GREEN_BOX_IMAGE_URL, RED_BOX_IMAGE_URL } from 'spec/test_constants';
+
+describe('DiffViewer', () => {
+ let vm;
+
+ function createComponent(props) {
+ const DiffViewer = Vue.extend(diffViewer);
+ vm = mountComponent(DiffViewer, props);
+ }
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders image diff', done => {
+ window.gon = {
+ relative_url_root: '',
+ };
+
+ createComponent({
+ diffMode: 'replaced',
+ newPath: GREEN_BOX_IMAGE_URL,
+ newSha: 'ABC',
+ oldPath: RED_BOX_IMAGE_URL,
+ oldSha: 'DEF',
+ projectPath: '',
+ });
+
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.deleted .image_file img').getAttribute('src')).toBe(
+ `//raw/DEF/${RED_BOX_IMAGE_URL}`,
+ );
+
+ expect(vm.$el.querySelector('.added .image_file img').getAttribute('src')).toBe(
+ `//raw/ABC/${GREEN_BOX_IMAGE_URL}`,
+ );
+
+ done();
+ });
+ });
+
+ it('renders fallback download diff display', done => {
+ createComponent({
+ diffMode: 'replaced',
+ newPath: 'test.abc',
+ newSha: 'ABC',
+ oldPath: 'testold.abc',
+ oldSha: 'DEF',
+ });
+
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.deleted .file-info').textContent.trim()).toContain(
+ 'testold.abc',
+ );
+ expect(vm.$el.querySelector('.deleted .btn.btn-default').textContent.trim()).toContain(
+ 'Download',
+ );
+
+ expect(vm.$el.querySelector('.added .file-info').textContent.trim()).toContain('test.abc');
+ expect(vm.$el.querySelector('.added .btn.btn-default').textContent.trim()).toContain(
+ 'Download',
+ );
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js b/spec/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js
new file mode 100644
index 00000000000..b878286ae3f
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js
@@ -0,0 +1,185 @@
+import Vue from 'vue';
+import imageDiffViewer from '~/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import { GREEN_BOX_IMAGE_URL, RED_BOX_IMAGE_URL } from 'spec/test_constants';
+
+describe('ImageDiffViewer', () => {
+ let vm;
+
+ function createComponent(props) {
+ const ImageDiffViewer = Vue.extend(imageDiffViewer);
+ vm = mountComponent(ImageDiffViewer, props);
+ }
+
+ const triggerEvent = (eventName, el = vm.$el, clientX = 0) => {
+ const event = document.createEvent('MouseEvents');
+ event.initMouseEvent(
+ eventName,
+ true,
+ true,
+ window,
+ 1,
+ clientX,
+ 0,
+ clientX,
+ 0,
+ false,
+ false,
+ false,
+ false,
+ 0,
+ null,
+ );
+
+ el.dispatchEvent(event);
+ };
+
+ const dragSlider = (sliderElement, dragPixel = 20) => {
+ triggerEvent('mousedown', sliderElement);
+ triggerEvent('mousemove', document.body, dragPixel);
+ triggerEvent('mouseup', document.body);
+ };
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders image diff for replaced', done => {
+ createComponent({
+ diffMode: 'replaced',
+ newPath: GREEN_BOX_IMAGE_URL,
+ oldPath: RED_BOX_IMAGE_URL,
+ });
+
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.added .image_file img').getAttribute('src')).toBe(
+ GREEN_BOX_IMAGE_URL,
+ );
+ expect(vm.$el.querySelector('.deleted .image_file img').getAttribute('src')).toBe(
+ RED_BOX_IMAGE_URL,
+ );
+
+ expect(vm.$el.querySelector('.view-modes-menu li.active').textContent.trim()).toBe('2-up');
+ expect(vm.$el.querySelector('.view-modes-menu li:nth-child(2)').textContent.trim()).toBe(
+ 'Swipe',
+ );
+ expect(vm.$el.querySelector('.view-modes-menu li:nth-child(3)').textContent.trim()).toBe(
+ 'Onion skin',
+ );
+
+ done();
+ });
+ });
+
+ it('renders image diff for new', done => {
+ createComponent({
+ diffMode: 'new',
+ newPath: GREEN_BOX_IMAGE_URL,
+ oldPath: '',
+ });
+
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.added .image_file img').getAttribute('src')).toBe(
+ GREEN_BOX_IMAGE_URL,
+ );
+
+ done();
+ });
+ });
+
+ it('renders image diff for deleted', done => {
+ createComponent({
+ diffMode: 'deleted',
+ newPath: '',
+ oldPath: RED_BOX_IMAGE_URL,
+ });
+
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.deleted .image_file img').getAttribute('src')).toBe(
+ RED_BOX_IMAGE_URL,
+ );
+
+ done();
+ });
+ });
+
+ describe('swipeMode', () => {
+ beforeEach(done => {
+ createComponent({
+ diffMode: 'replaced',
+ newPath: GREEN_BOX_IMAGE_URL,
+ oldPath: RED_BOX_IMAGE_URL,
+ });
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ it('switches to Swipe Mode', done => {
+ vm.$el.querySelector('.view-modes-menu li:nth-child(2)').click();
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.view-modes-menu li.active').textContent.trim()).toBe('Swipe');
+ done();
+ });
+ });
+
+ it('drag handler is working', done => {
+ vm.$el.querySelector('.view-modes-menu li:nth-child(2)').click();
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.swipe-bar').style.left).toBe('1px');
+ expect(vm.$el.querySelector('.top-handle')).not.toBeNull();
+
+ dragSlider(vm.$el.querySelector('.swipe-bar'), 40);
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.swipe-bar').style.left).toBe('-20px');
+ done();
+ });
+ });
+ });
+ });
+
+ describe('onionSkin', () => {
+ beforeEach(done => {
+ createComponent({
+ diffMode: 'replaced',
+ newPath: GREEN_BOX_IMAGE_URL,
+ oldPath: RED_BOX_IMAGE_URL,
+ });
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ it('switches to Onion Skin Mode', done => {
+ vm.$el.querySelector('.view-modes-menu li:nth-child(3)').click();
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.view-modes-menu li.active').textContent.trim()).toBe(
+ 'Onion skin',
+ );
+ done();
+ });
+ });
+
+ it('has working drag handler', done => {
+ vm.$el.querySelector('.view-modes-menu li:nth-child(3)').click();
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.dragger').style.left).toBe('100px');
+
+ dragSlider(vm.$el.querySelector('.dragger'));
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.dragger').style.left).toBe('20px');
+ expect(vm.$el.querySelector('.added.frame').style.opacity).toBe('0.2');
+ done();
+ });
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/dropdown/dropdown_button_spec.js b/spec/javascripts/vue_shared/components/dropdown/dropdown_button_spec.js
new file mode 100644
index 00000000000..ba897f4660d
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/dropdown/dropdown_button_spec.js
@@ -0,0 +1,69 @@
+import Vue from 'vue';
+
+import dropdownButtonComponent from '~/vue_shared/components/dropdown/dropdown_button.vue';
+
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+
+const defaultLabel = 'Select';
+const customLabel = 'Select project';
+
+const createComponent = config => {
+ const Component = Vue.extend(dropdownButtonComponent);
+
+ return mountComponent(Component, config);
+};
+
+describe('DropdownButtonComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('computed', () => {
+ describe('dropdownToggleText', () => {
+ it('returns default toggle text', () => {
+ expect(vm.toggleText).toBe(defaultLabel);
+ });
+
+ it('returns custom toggle text when provided via props', () => {
+ const vmEmptyLabels = createComponent({ toggleText: customLabel });
+
+ expect(vmEmptyLabels.toggleText).toBe(customLabel);
+ vmEmptyLabels.$destroy();
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('renders component container element of type `button`', () => {
+ expect(vm.$el.nodeName).toBe('BUTTON');
+ });
+
+ it('renders component container element with required data attributes', () => {
+ expect(vm.$el.dataset.abilityName).toBe(vm.abilityName);
+ expect(vm.$el.dataset.fieldName).toBe(vm.fieldName);
+ expect(vm.$el.dataset.issueUpdate).toBe(vm.updatePath);
+ expect(vm.$el.dataset.labels).toBe(vm.labelsPath);
+ expect(vm.$el.dataset.namespacePath).toBe(vm.namespace);
+ expect(vm.$el.dataset.showAny).not.toBeDefined();
+ });
+
+ it('renders dropdown toggle text element', () => {
+ const dropdownToggleTextEl = vm.$el.querySelector('.dropdown-toggle-text');
+ expect(dropdownToggleTextEl).not.toBeNull();
+ expect(dropdownToggleTextEl.innerText.trim()).toBe(defaultLabel);
+ });
+
+ it('renders dropdown button icon', () => {
+ const dropdownIconEl = vm.$el.querySelector('.dropdown-toggle-icon i.fa');
+
+ expect(dropdownIconEl).not.toBeNull();
+ expect(dropdownIconEl.classList.contains('fa-chevron-down')).toBe(true);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/dropdown/dropdown_hidden_input_spec.js b/spec/javascripts/vue_shared/components/dropdown/dropdown_hidden_input_spec.js
new file mode 100644
index 00000000000..445ab0cb40e
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/dropdown/dropdown_hidden_input_spec.js
@@ -0,0 +1,37 @@
+import Vue from 'vue';
+
+import dropdownHiddenInputComponent from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
+
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+
+import { mockLabels } from './mock_data';
+
+const createComponent = (name = 'label_id[]', value = mockLabels[0].id) => {
+ const Component = Vue.extend(dropdownHiddenInputComponent);
+
+ return mountComponent(Component, {
+ name,
+ value,
+ });
+};
+
+describe('DropdownHiddenInputComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('template', () => {
+ it('renders input element of type `hidden`', () => {
+ expect(vm.$el.nodeName).toBe('INPUT');
+ expect(vm.$el.getAttribute('type')).toBe('hidden');
+ expect(vm.$el.getAttribute('name')).toBe(vm.name);
+ expect(vm.$el.getAttribute('value')).toBe(`${vm.value}`);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/dropdown/dropdown_search_input_spec.js b/spec/javascripts/vue_shared/components/dropdown/dropdown_search_input_spec.js
new file mode 100644
index 00000000000..551520721e5
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/dropdown/dropdown_search_input_spec.js
@@ -0,0 +1,52 @@
+import Vue from 'vue';
+
+import dropdownSearchInputComponent from '~/vue_shared/components/dropdown/dropdown_search_input.vue';
+
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+
+const componentConfig = {
+ placeholderText: 'Search something',
+};
+
+const createComponent = (config = componentConfig) => {
+ const Component = Vue.extend(dropdownSearchInputComponent);
+
+ return mountComponent(Component, config);
+};
+
+describe('DropdownSearchInputComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('template', () => {
+ it('renders input element with type `search`', () => {
+ const inputEl = vm.$el.querySelector('input.dropdown-input-field');
+
+ expect(inputEl).not.toBeNull();
+ expect(inputEl.getAttribute('type')).toBe('search');
+ });
+
+ it('renders search icon element', () => {
+ expect(vm.$el.querySelector('.fa-search.dropdown-input-search')).not.toBeNull();
+ });
+
+ it('renders clear search icon element', () => {
+ expect(
+ vm.$el.querySelector('.fa-times.dropdown-input-clear.js-dropdown-input-clear'),
+ ).not.toBeNull();
+ });
+
+ it('displays custom placeholder text', () => {
+ const inputEl = vm.$el.querySelector('input.dropdown-input-field');
+
+ expect(inputEl.getAttribute('placeholder')).toBe(componentConfig.placeholderText);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/dropdown/mock_data.js b/spec/javascripts/vue_shared/components/dropdown/mock_data.js
new file mode 100644
index 00000000000..b09d42da401
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/dropdown/mock_data.js
@@ -0,0 +1,11 @@
+export const mockLabels = [
+ {
+ id: 26,
+ title: 'Foo Label',
+ description: 'Foobar',
+ color: '#BADA55',
+ text_color: '#FFFFFF',
+ },
+];
+
+export default mockLabels;
diff --git a/spec/javascripts/vue_shared/components/expand_button_spec.js b/spec/javascripts/vue_shared/components/expand_button_spec.js
index af9693c48fd..98fee9a74a5 100644
--- a/spec/javascripts/vue_shared/components/expand_button_spec.js
+++ b/spec/javascripts/vue_shared/components/expand_button_spec.js
@@ -19,7 +19,7 @@ describe('expand button', () => {
});
it('renders a collpased button', () => {
- expect(vm.$el.textContent.trim()).toEqual('...');
+ expect(vm.$children[0].iconTestClass).toEqual('ic-ellipsis_h');
});
it('hides expander on click', done => {
diff --git a/spec/javascripts/vue_shared/components/file_icon_spec.js b/spec/javascripts/vue_shared/components/file_icon_spec.js
index f7581251bf0..1c666fc6c55 100644
--- a/spec/javascripts/vue_shared/components/file_icon_spec.js
+++ b/spec/javascripts/vue_shared/components/file_icon_spec.js
@@ -74,7 +74,7 @@ describe('File Icon component', () => {
size: 120,
});
- const classList = vm.$el.firstChild.classList;
+ const { classList } = vm.$el.firstChild;
const containsSizeClass = classList.contains('s120');
const containsCustomClass = classList.contains('extraclasses');
expect(containsSizeClass).toBe(true);
diff --git a/spec/javascripts/vue_shared/components/gl_modal_spec.js b/spec/javascripts/vue_shared/components/gl_modal_spec.js
index 85cb1b90fc6..e4737714312 100644
--- a/spec/javascripts/vue_shared/components/gl_modal_spec.js
+++ b/spec/javascripts/vue_shared/components/gl_modal_spec.js
@@ -190,4 +190,45 @@ describe('GlModal', () => {
});
});
});
+
+ describe('handling sizes', () => {
+ it('should render modal-sm', () => {
+ vm = mountComponent(modalComponent, {
+ modalSize: 'sm',
+ });
+
+ expect(vm.$el.querySelector('.modal-dialog').classList.contains('modal-sm')).toEqual(true);
+ });
+
+ it('should render modal-lg', () => {
+ vm = mountComponent(modalComponent, {
+ modalSize: 'lg',
+ });
+
+ expect(vm.$el.querySelector('.modal-dialog').classList.contains('modal-lg')).toEqual(true);
+ });
+
+ it('should render modal-xl', () => {
+ vm = mountComponent(modalComponent, {
+ modalSize: 'xl',
+ });
+
+ expect(vm.$el.querySelector('.modal-dialog').classList.contains('modal-xl')).toEqual(true);
+ });
+
+ it('should not add modal size classes when md size is passed', () => {
+ vm = mountComponent(modalComponent, {
+ modalSize: 'md',
+ });
+
+ expect(vm.$el.querySelector('.modal-dialog').classList.contains('modal-md')).toEqual(false);
+ });
+
+ it('should not add modal size classes by default', () => {
+ vm = mountComponent(modalComponent, {});
+
+ expect(vm.$el.querySelector('.modal-dialog').classList.contains('modal-sm')).toEqual(false);
+ expect(vm.$el.querySelector('.modal-dialog').classList.contains('modal-lg')).toEqual(false);
+ });
+ });
});
diff --git a/spec/javascripts/vue_shared/components/icon_spec.js b/spec/javascripts/vue_shared/components/icon_spec.js
index 68d57ebc8f0..cc030e29d61 100644
--- a/spec/javascripts/vue_shared/components/icon_spec.js
+++ b/spec/javascripts/vue_shared/components/icon_spec.js
@@ -44,7 +44,7 @@ describe('Sprite Icon Component', function () {
});
it('should properly render img css', function () {
- const classList = icon.$el.classList;
+ const { classList } = icon.$el;
const containsSizeClass = classList.contains('s32');
const containsCustomClass = classList.contains('extraclasses');
expect(containsSizeClass).toBe(true);
diff --git a/spec/javascripts/vue_shared/components/lib/utils/dom_utils_spec.js b/spec/javascripts/vue_shared/components/lib/utils/dom_utils_spec.js
new file mode 100644
index 00000000000..2388660b0c2
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/lib/utils/dom_utils_spec.js
@@ -0,0 +1,13 @@
+import * as domUtils from '~/vue_shared/components/lib/utils/dom_utils';
+
+describe('domUtils', () => {
+ describe('pixeliseValue', () => {
+ it('should add px to a given Number', () => {
+ expect(domUtils.pixeliseValue(12)).toEqual('12px');
+ });
+
+ it('should not add px to 0', () => {
+ expect(domUtils.pixeliseValue(0)).toEqual('');
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/markdown/header_spec.js b/spec/javascripts/vue_shared/components/markdown/header_spec.js
index 02117638b63..488575df401 100644
--- a/spec/javascripts/vue_shared/components/markdown/header_spec.js
+++ b/spec/javascripts/vue_shared/components/markdown/header_spec.js
@@ -51,7 +51,7 @@ describe('Markdown field header component', () => {
spyOn(vm, '$emit');
$(document).triggerHandler('markdown-preview:show', [
- $('<form><textarea class="markdown-area"></textarea></textarea></form>'),
+ $('<form><div class="js-vue-markdown-field"><textarea class="markdown-area"></textarea></div></form>'),
]);
expect(vm.$emit).not.toHaveBeenCalled();
diff --git a/spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js b/spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js
index ba8ab0b2cd7..7e57c51bf29 100644
--- a/spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js
+++ b/spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js
@@ -1,13 +1,15 @@
import Vue from 'vue';
import issuePlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue';
-import store from '~/notes/stores';
+import createStore from '~/notes/stores';
import { userDataMock } from '../../../notes/mock_data';
describe('issue placeholder system note component', () => {
+ let store;
let vm;
beforeEach(() => {
const Component = Vue.extend(issuePlaceholderNote);
+ store = createStore();
store.dispatch('setUserData', userDataMock);
vm = new Component({
store,
@@ -21,15 +23,23 @@ describe('issue placeholder system note component', () => {
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);
+ 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}`);
+ 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', () => {
diff --git a/spec/javascripts/vue_shared/components/notes/system_note_spec.js b/spec/javascripts/vue_shared/components/notes/system_note_spec.js
index 36aaf0a6c2e..aa4c9c4c88c 100644
--- a/spec/javascripts/vue_shared/components/notes/system_note_spec.js
+++ b/spec/javascripts/vue_shared/components/notes/system_note_spec.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
import issueSystemNote from '~/vue_shared/components/notes/system_note.vue';
-import store from '~/notes/stores';
+import createStore from '~/notes/stores';
-describe('issue system note', () => {
+describe('system note component', () => {
let vm;
let props;
@@ -24,6 +24,7 @@ describe('issue system note', () => {
},
};
+ const store = createStore();
store.dispatch('setTargetNoteHash', `note_${props.note.id}`);
const Component = Vue.extend(issueSystemNote);
@@ -49,9 +50,10 @@ describe('issue system note', () => {
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);
+ // Redcarpet Markdown renderer wraps text in `<p>` tags
+ // we need to strip them because they break layout of commit lists in system notes:
+ // https://gitlab.com/gitlab-org/gitlab-ce/uploads/b07a10670919254f0220d3ff5c1aa110/jqzI.png
+ it('removes wrapping paragraph from note HTML', () => {
+ expect(vm.$el.querySelector('.system-note-message').innerHTML).toEqual('<span>closed</span>');
});
});
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input_spec.js
deleted file mode 100644
index 88733922a59..00000000000
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input_spec.js
+++ /dev/null
@@ -1,37 +0,0 @@
-import Vue from 'vue';
-
-import dropdownHiddenInputComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_hidden_input.vue';
-
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-import { mockLabels } from './mock_data';
-
-const createComponent = (name = 'label_id[]', label = mockLabels[0]) => {
- const Component = Vue.extend(dropdownHiddenInputComponent);
-
- return mountComponent(Component, {
- name,
- label,
- });
-};
-
-describe('DropdownHiddenInputComponent', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('template', () => {
- it('renders input element of type `hidden`', () => {
- expect(vm.$el.nodeName).toBe('INPUT');
- expect(vm.$el.getAttribute('type')).toBe('hidden');
- expect(vm.$el.getAttribute('name')).toBe(vm.name);
- expect(vm.$el.getAttribute('value')).toBe(`${vm.label.id}`);
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
index 4397b00acfa..370a296bd8f 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
@@ -82,7 +82,7 @@ describe('DropdownValueComponent', () => {
});
it('renders label element with tooltip and styles based on label details', () => {
- const labelEl = vm.$el.querySelector('a span.label.color-label');
+ const labelEl = vm.$el.querySelector('a span.badge.color-label');
expect(labelEl).not.toBeNull();
expect(labelEl.dataset.placement).toBe('bottom');
expect(labelEl.dataset.container).toBe('body');
diff --git a/spec/javascripts/vue_shared/components/table_pagination_spec.js b/spec/javascripts/vue_shared/components/table_pagination_spec.js
index c63f15e5880..c36b607a34e 100644
--- a/spec/javascripts/vue_shared/components/table_pagination_spec.js
+++ b/spec/javascripts/vue_shared/components/table_pagination_spec.js
@@ -51,7 +51,7 @@ describe('Pagination component', () => {
expect(
component.$el.querySelector('.js-previous-button').classList.contains('disabled'),
- ).toEqual(true);
+ ).toEqual(true);
component.$el.querySelector('.js-previous-button a').click();
diff --git a/spec/javascripts/vue_shared/components/tabs/tab_spec.js b/spec/javascripts/vue_shared/components/tabs/tab_spec.js
new file mode 100644
index 00000000000..8437fe37738
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/tabs/tab_spec.js
@@ -0,0 +1,32 @@
+import Vue from 'vue';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import Tab from '~/vue_shared/components/tabs/tab.vue';
+
+describe('Tab component', () => {
+ const Component = Vue.extend(Tab);
+ let vm;
+
+ beforeEach(() => {
+ vm = mountComponent(Component);
+ });
+
+ it('sets localActive to equal active', done => {
+ vm.active = true;
+
+ vm.$nextTick(() => {
+ expect(vm.localActive).toBe(true);
+
+ done();
+ });
+ });
+
+ it('sets active class', done => {
+ vm.active = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.classList).toContain('active');
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/tabs/tabs_spec.js b/spec/javascripts/vue_shared/components/tabs/tabs_spec.js
new file mode 100644
index 00000000000..50ba18cd338
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/tabs/tabs_spec.js
@@ -0,0 +1,68 @@
+import Vue from 'vue';
+import Tabs from '~/vue_shared/components/tabs/tabs';
+import Tab from '~/vue_shared/components/tabs/tab.vue';
+
+describe('Tabs component', () => {
+ let vm;
+
+ beforeEach(done => {
+ vm = new Vue({
+ components: {
+ Tabs,
+ Tab,
+ },
+ template: `
+ <div>
+ <tabs>
+ <tab title="Testing" active>
+ First tab
+ </tab>
+ <tab>
+ <template slot="title">Test slot</template>
+ Second tab
+ </tab>
+ </tabs>
+ </div>
+ `,
+ }).$mount();
+
+ setTimeout(done);
+ });
+
+ describe('tab links', () => {
+ it('renders links for tabs', () => {
+ expect(vm.$el.querySelectorAll('a').length).toBe(2);
+ });
+
+ it('renders link titles from props', () => {
+ expect(vm.$el.querySelector('a').textContent).toContain('Testing');
+ });
+
+ it('renders link titles from slot', () => {
+ expect(vm.$el.querySelectorAll('a')[1].textContent).toContain('Test slot');
+ });
+
+ it('renders active class', () => {
+ expect(vm.$el.querySelector('a').classList).toContain('active');
+ });
+
+ it('updates active class on click', done => {
+ vm.$el.querySelectorAll('a')[1].click();
+
+ setTimeout(() => {
+ expect(vm.$el.querySelector('a').classList).not.toContain('active');
+ expect(vm.$el.querySelectorAll('a')[1].classList).toContain('active');
+
+ done();
+ });
+ });
+ });
+
+ describe('content', () => {
+ it('renders content panes', () => {
+ expect(vm.$el.querySelectorAll('.tab-pane').length).toBe(2);
+ expect(vm.$el.querySelectorAll('.tab-pane')[0].textContent).toContain('First tab');
+ expect(vm.$el.querySelectorAll('.tab-pane')[1].textContent).toContain('Second tab');
+ });
+ });
+});
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
index 446f025c127..656b57d764e 100644
--- 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
@@ -51,7 +51,7 @@ describe('User Avatar Image Component', function () {
});
it('should properly render img css', function () {
- const classList = vm.$el.classList;
+ const { classList } = vm.$el;
const containsAvatar = classList.contains('avatar');
const containsSizeClass = classList.contains('s99');
const containsCustomClass = classList.contains(DEFAULT_PROPS.cssClasses);
@@ -73,7 +73,7 @@ describe('User Avatar Image Component', function () {
});
it('should add lazy attributes', function () {
- const classList = vm.$el.classList;
+ const { classList } = vm.$el;
const lazyClass = classList.contains('lazy');
expect(lazyClass).toBe(true);
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
index adf80d0c2bb..4c5c242cbb3 100644
--- 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
@@ -21,7 +21,7 @@ describe('User Avatar Link Component', function () {
propsData: this.propsData,
}).$mount();
- this.userAvatarImage = this.userAvatarLink.$children[0];
+ [this.userAvatarImage] = this.userAvatarLink.$children;
});
it('should return a defined Vue component', function () {
diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js
index 7fe3bd92049..bdeebe0de75 100644
--- a/spec/javascripts/zen_mode_spec.js
+++ b/spec/javascripts/zen_mode_spec.js
@@ -1,11 +1,12 @@
import $ from 'jquery';
-import Mousetrap from 'mousetrap';
import Dropzone from 'dropzone';
+import Mousetrap from 'mousetrap';
import ZenMode from '~/zen_mode';
describe('ZenMode', () => {
let zen;
- const fixtureName = 'merge_requests/merge_request_with_comment.html.raw';
+ let dropzoneForElementSpy;
+ const fixtureName = 'snippets/show.html.raw';
preloadFixtures(fixtureName);
@@ -18,15 +19,17 @@ describe('ZenMode', () => {
}
function escapeKeydown() {
- $('.notes-form textarea').trigger($.Event('keydown', {
- keyCode: 27,
- }));
+ $('.notes-form textarea').trigger(
+ $.Event('keydown', {
+ keyCode: 27,
+ }),
+ );
}
beforeEach(() => {
loadFixtures(fixtureName);
- spyOn(Dropzone, 'forElement').and.callFake(() => ({
+ dropzoneForElementSpy = spyOn(Dropzone, 'forElement').and.callFake(() => ({
enable: () => true,
}));
zen = new ZenMode();
@@ -35,11 +38,29 @@ describe('ZenMode', () => {
zen.scroll_position = 456;
});
+ describe('enabling dropzone', () => {
+ beforeEach(() => {
+ enterZen();
+ });
+
+ it('should not call dropzone if element is not dropzone valid', () => {
+ $('.div-dropzone').addClass('js-invalid-dropzone');
+ exitZen();
+ expect(dropzoneForElementSpy.calls.count()).toEqual(0);
+ });
+
+ it('should call dropzone if element is dropzone valid', () => {
+ $('.div-dropzone').removeClass('js-invalid-dropzone');
+ exitZen();
+ expect(dropzoneForElementSpy.calls.count()).toEqual(2);
+ });
+ });
+
describe('on enter', () => {
it('pauses Mousetrap', () => {
- spyOn(Mousetrap, 'pause');
+ const mouseTrapPauseSpy = spyOn(Mousetrap, 'pause');
enterZen();
- expect(Mousetrap.pause).toHaveBeenCalled();
+ expect(mouseTrapPauseSpy).toHaveBeenCalled();
});
it('removes textarea styling', () => {
@@ -62,9 +83,9 @@ describe('ZenMode', () => {
beforeEach(enterZen);
it('unpauses Mousetrap', () => {
- spyOn(Mousetrap, 'unpause');
+ const mouseTrapUnpauseSpy = spyOn(Mousetrap, 'unpause');
exitZen();
- expect(Mousetrap.unpause).toHaveBeenCalled();
+ expect(mouseTrapUnpauseSpy).toHaveBeenCalled();
});
it('restores the scroll position', () => {
@@ -73,22 +94,4 @@ describe('ZenMode', () => {
expect(zen.scrollTo).toHaveBeenCalled();
});
});
-
- describe('enabling dropzone', () => {
- beforeEach(() => {
- enterZen();
- });
-
- it('should not call dropzone if element is not dropzone valid', () => {
- $('.div-dropzone').addClass('js-invalid-dropzone');
- exitZen();
- expect(Dropzone.forElement).not.toHaveBeenCalled();
- });
-
- 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/backup/files_spec.rb b/spec/lib/backup/files_spec.rb
index 99872211a4e..63f2298357f 100644
--- a/spec/lib/backup/files_spec.rb
+++ b/spec/lib/backup/files_spec.rb
@@ -46,7 +46,9 @@ describe Backup::Files do
end
it 'calls tar command with unlink' do
- expect(subject).to receive(:run_pipeline!).with([%w(gzip -cd), %w(tar --unlink-first --recursive-unlink -C /var/gitlab-registry -xf -)], any_args)
+ expect(subject).to receive(:tar).and_return('blabla-tar')
+
+ expect(subject).to receive(:run_pipeline!).with([%w(gzip -cd), %w(blabla-tar --unlink-first --recursive-unlink -C /var/gitlab-registry -xf -)], any_args)
subject.restore
end
end
diff --git a/spec/lib/backup/manager_spec.rb b/spec/lib/backup/manager_spec.rb
index 84688845fa5..ca319679e80 100644
--- a/spec/lib/backup/manager_spec.rb
+++ b/spec/lib/backup/manager_spec.rb
@@ -5,6 +5,8 @@ describe Backup::Manager do
let(:progress) { StringIO.new }
+ subject { described_class.new(progress) }
+
before do
allow(progress).to receive(:puts)
allow(progress).to receive(:print)
@@ -272,16 +274,13 @@ describe Backup::Manager do
}
)
- # the Fog mock only knows about directories we create explicitly
Fog.mock!
+
+ # the Fog mock only knows about directories we create explicitly
connection = ::Fog::Storage.new(Gitlab.config.backup.upload.connection.symbolize_keys)
connection.directories.create(key: Gitlab.config.backup.upload.remote_directory)
end
- after do
- Fog.unmock!
- end
-
context 'target path' do
it 'uses the tar filename by default' do
expect_any_instance_of(Fog::Collection).to receive(:create)
diff --git a/spec/lib/backup/repository_spec.rb b/spec/lib/backup/repository_spec.rb
index b1ea9c0b622..92a27e308d2 100644
--- a/spec/lib/backup/repository_spec.rb
+++ b/spec/lib/backup/repository_spec.rb
@@ -3,6 +3,7 @@ require 'spec_helper'
describe Backup::Repository do
let(:progress) { StringIO.new }
let!(:project) { create(:project, :wiki_repo) }
+ subject { described_class.new(progress) }
before do
allow(progress).to receive(:puts)
@@ -24,18 +25,18 @@ describe Backup::Repository do
end
it 'does not raise error' do
- expect { described_class.new.dump }.not_to raise_error
+ expect { subject.dump }.not_to raise_error
end
end
end
describe '#restore' do
- subject { described_class.new }
-
let(:timestamp) { Time.utc(2017, 3, 22) }
let(:temp_dirs) do
Gitlab.config.repositories.storages.map do |name, storage|
- File.join(storage.legacy_disk_path, '..', 'repositories.old.' + timestamp.to_i.to_s)
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ File.join(storage.legacy_disk_path, '..', 'repositories.old.' + timestamp.to_i.to_s)
+ end
end
end
@@ -49,14 +50,14 @@ describe Backup::Repository do
describe 'command failure' do
before do
- allow(Gitlab::Popen).to receive(:popen).and_return(['error', 1])
+ allow_any_instance_of(Gitlab::Shell).to receive(:create_repository).and_return(false)
end
context 'hashed storage' do
it 'shows the appropriate error' do
subject.restore
- expect(progress).to have_received(:puts).with("Ignoring error on #{project.full_path} (#{project.disk_path}) - error")
+ expect(progress).to have_received(:puts).with("[Failed] restoring #{project.full_path} repository")
end
end
@@ -66,32 +67,43 @@ describe Backup::Repository do
it 'shows the appropriate error' do
subject.restore
- expect(progress).to have_received(:puts).with("Ignoring error on #{project.full_path} - error")
+ expect(progress).to have_received(:puts).with("[Failed] restoring #{project.full_path} repository")
end
end
end
+ end
- describe 'folders without permissions' do
+ describe '#delete_all_repositories', :seed_helper do
+ shared_examples('delete_all_repositories') do
before do
- allow(FileUtils).to receive(:mv).and_raise(Errno::EACCES)
+ allow(FileUtils).to receive(:mkdir_p).and_call_original
+ allow(FileUtils).to receive(:mv).and_call_original
end
- it 'shows error message' do
- expect(subject).to receive(:access_denied_error)
- subject.restore
+ after(:all) do
+ ensure_seeds
end
- end
- describe 'folder that is a mountpoint' do
- before do
- allow(FileUtils).to receive(:mv).and_raise(Errno::EBUSY)
- end
+ it 'removes all repositories' do
+ # Sanity check: there should be something for us to delete
+ expect(list_repositories).to include(File.join(SEED_STORAGE_PATH, TEST_REPO_PATH))
- it 'shows error message' do
- expect(subject).to receive(:resource_busy_error).and_call_original
+ subject.delete_all_repositories('default', Gitlab.config.repositories.storages['default'])
- expect { subject.restore }.to raise_error(/is a mountpoint/)
+ expect(list_repositories).to be_empty
end
+
+ def list_repositories
+ Dir[File.join(SEED_STORAGE_PATH, '*.git')]
+ end
+ end
+
+ context 'with gitaly' do
+ it_behaves_like 'delete_all_repositories'
+ end
+
+ context 'without gitaly', :skip_gitaly_mock do
+ it_behaves_like 'delete_all_repositories'
end
end
@@ -102,20 +114,20 @@ describe Backup::Repository do
it 'invalidates the emptiness cache' do
expect(wiki.repository).to receive(:expire_emptiness_caches).once
- described_class.new.send(:empty_repo?, wiki)
+ subject.send(:empty_repo?, wiki)
end
context 'wiki repo has content' do
let!(:wiki_page) { create(:wiki_page, wiki: wiki) }
it 'returns true, regardless of bad cache value' do
- expect(described_class.new.send(:empty_repo?, wiki)).to be(false)
+ expect(subject.send(:empty_repo?, wiki)).to be(false)
end
end
context 'wiki repo does not have content' do
it 'returns true, regardless of bad cache value' do
- expect(described_class.new.send(:empty_repo?, wiki)).to be_truthy
+ expect(subject.send(:empty_repo?, wiki)).to be_truthy
end
end
end
diff --git a/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb b/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb
index 8224dc5a6b9..b645e49bd43 100644
--- a/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb
+++ b/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb
@@ -11,4 +11,8 @@ describe Banzai::Filter::BlockquoteFenceFilter do
expect(output).to eq(expected)
end
+
+ it 'allows trailing whitespace on blockquote fence lines' do
+ expect(filter(">>> \ntest\n>>> ")).to eq("> test")
+ end
end
diff --git a/spec/lib/banzai/filter/emoji_filter_spec.rb b/spec/lib/banzai/filter/emoji_filter_spec.rb
index 10910f22d4a..85a4619e33d 100644
--- a/spec/lib/banzai/filter/emoji_filter_spec.rb
+++ b/spec/lib/banzai/filter/emoji_filter_spec.rb
@@ -3,15 +3,6 @@ require 'spec_helper'
describe Banzai::Filter::EmojiFilter do
include FilterSpecHelper
- before do
- @original_asset_host = ActionController::Base.asset_host
- ActionController::Base.asset_host = 'https://foo.com'
- end
-
- after do
- ActionController::Base.asset_host = @original_asset_host
- end
-
it 'replaces supported name emoji' do
doc = filter('<p>:heart:</p>')
expect(doc.css('gl-emoji').first.text).to eq 'â¤'
diff --git a/spec/lib/banzai/filter/image_lazy_load_filter_spec.rb b/spec/lib/banzai/filter/image_lazy_load_filter_spec.rb
index c19de7b784a..41f957c4e00 100644
--- a/spec/lib/banzai/filter/image_lazy_load_filter_spec.rb
+++ b/spec/lib/banzai/filter/image_lazy_load_filter_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Banzai::Filter::ImageLazyLoadFilter, lib: true do
+describe Banzai::Filter::ImageLazyLoadFilter do
include FilterSpecHelper
def image(path)
diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb
index 392905076dc..00257ed7904 100644
--- a/spec/lib/banzai/filter/label_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb
@@ -59,7 +59,7 @@ describe Banzai::Filter::LabelReferenceFilter do
describe 'label span element' do
it 'includes default classes' do
doc = reference_filter("Label #{reference}")
- expect(doc.css('a span').first.attr('class')).to eq 'label color-label has-tooltip'
+ expect(doc.css('a span').first.attr('class')).to eq 'badge color-label has-tooltip'
end
it 'includes a style attribute' do
@@ -148,9 +148,11 @@ describe Banzai::Filter::LabelReferenceFilter do
expect(doc.text).to eq 'See ?g.fm&'
end
- it 'links with adjacent text' do
- doc = reference_filter("Label (#{reference}).")
- expect(doc.to_html).to match(%r(\(<a.+><span.+>\?g\.fm&amp;</span></a>\)\.))
+ it 'does not include trailing punctuation', :aggregate_failures do
+ ['.', ', ok?', '...', '?', '!', ': is that ok?'].each do |trailing_punctuation|
+ doc = filter("Label #{reference}#{trailing_punctuation}")
+ expect(doc.to_html).to match(%r(<a.+><span.+>\?g\.fm&amp;</span></a>#{Regexp.escape(trailing_punctuation)}))
+ end
end
it 'ignores invalid label names' do
diff --git a/spec/lib/banzai/filter/markdown_filter_spec.rb b/spec/lib/banzai/filter/markdown_filter_spec.rb
index 00c407d1b69..a515d07b072 100644
--- a/spec/lib/banzai/filter/markdown_filter_spec.rb
+++ b/spec/lib/banzai/filter/markdown_filter_spec.rb
@@ -3,17 +3,61 @@ require 'spec_helper'
describe Banzai::Filter::MarkdownFilter do
include FilterSpecHelper
- context 'code block' do
- it 'adds language to lang attribute when specified' do
- result = filter("```html\nsome code\n```")
+ describe 'markdown engine from context' do
+ it 'defaults to CommonMark' do
+ expect_any_instance_of(Banzai::Filter::MarkdownEngines::CommonMark).to receive(:render).and_return('test')
- expect(result).to start_with("\n<pre><code lang=\"html\">")
+ filter('test')
end
- it 'does not add language to lang attribute when not specified' do
- result = filter("```\nsome code\n```")
+ it 'uses Redcarpet' do
+ expect_any_instance_of(Banzai::Filter::MarkdownEngines::Redcarpet).to receive(:render).and_return('test')
- expect(result).to start_with("\n<pre><code>")
+ filter('test', { markdown_engine: :redcarpet })
+ end
+
+ it 'uses CommonMark' do
+ expect_any_instance_of(Banzai::Filter::MarkdownEngines::CommonMark).to receive(:render).and_return('test')
+
+ filter('test', { markdown_engine: :common_mark })
+ end
+ end
+
+ describe 'code block' do
+ context 'using CommonMark' do
+ before do
+ stub_const('Banzai::Filter::MarkdownFilter::DEFAULT_ENGINE', :common_mark)
+ end
+
+ it 'adds language to lang attribute when specified' do
+ result = filter("```html\nsome code\n```")
+
+ expect(result).to start_with("<pre><code lang=\"html\">")
+ end
+
+ it 'does not add language to lang attribute when not specified' do
+ result = filter("```\nsome code\n```")
+
+ expect(result).to start_with("<pre><code>")
+ end
+ end
+
+ context 'using Redcarpet' do
+ before do
+ stub_const('Banzai::Filter::MarkdownFilter::DEFAULT_ENGINE', :redcarpet)
+ end
+
+ it 'adds language to lang attribute when specified' do
+ result = filter("```html\nsome code\n```")
+
+ expect(result).to start_with("\n<pre><code lang=\"html\">")
+ end
+
+ it 'does not add language to lang attribute when not specified' do
+ result = filter("```\nsome code\n```")
+
+ expect(result).to start_with("\n<pre><code>")
+ end
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 a1dd72c498f..55c41e55437 100644
--- a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
@@ -210,6 +210,13 @@ describe Banzai::Filter::MergeRequestReferenceFilter do
.to eq reference
end
+ it 'commit ref tag is valid' do
+ doc = reference_filter("See #{reference}")
+ commit_ref_tag = doc.css('a').first.css('span.gfm.gfm-commit')
+
+ expect(commit_ref_tag.text).to eq(commit.short_id)
+ end
+
it 'has valid text' do
doc = reference_filter("See #{reference}")
diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
index f8fa9b2d13d..91d4a60ba95 100644
--- a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
@@ -3,7 +3,8 @@ require 'spec_helper'
describe Banzai::Filter::MilestoneReferenceFilter do
include FilterSpecHelper
- let(:group) { create(:group, :public) }
+ let(:parent_group) { create(:group, :public) }
+ let(:group) { create(:group, :public, parent: parent_group) }
let(:project) { create(:project, :public, group: group) }
it 'requires project context' do
@@ -340,6 +341,13 @@ describe Banzai::Filter::MilestoneReferenceFilter do
expect(doc.css('a')).to be_empty
end
+
+ it 'supports parent group references', :nested_groups do
+ milestone.update!(group: parent_group)
+
+ doc = reference_filter("See #{reference}")
+ expect(doc.css('a').first.text).to eq(milestone.name)
+ end
end
context 'group context' do
diff --git a/spec/lib/banzai/filter/redactor_filter_spec.rb b/spec/lib/banzai/filter/redactor_filter_spec.rb
index 9a2e521fdcf..919825a6102 100644
--- a/spec/lib/banzai/filter/redactor_filter_spec.rb
+++ b/spec/lib/banzai/filter/redactor_filter_spec.rb
@@ -46,7 +46,7 @@ describe Banzai::Filter::RedactorFilter do
it 'allows permitted Project references' do
user = create(:user)
project = create(:project)
- project.add_master(user)
+ project.add_maintainer(user)
link = reference_link(project: project.id, reference_type: 'test')
doc = filter(link, current_user: user)
diff --git a/spec/lib/banzai/filter/sanitization_filter_spec.rb b/spec/lib/banzai/filter/sanitization_filter_spec.rb
index 17a620ef603..d930c608b18 100644
--- a/spec/lib/banzai/filter/sanitization_filter_spec.rb
+++ b/spec/lib/banzai/filter/sanitization_filter_spec.rb
@@ -93,6 +93,16 @@ describe Banzai::Filter::SanitizationFilter do
expect(doc.at_css('td')['style']).to eq 'text-align: center'
end
+ it 'disallows `text-align` property in `style` attribute on other elements' do
+ html = <<~HTML
+ <div style="text-align: center">Text</div>
+ HTML
+
+ doc = filter(html)
+
+ expect(doc.at_css('div')['style']).to be_nil
+ end
+
it 'allows `span` elements' do
exp = act = %q{<span>Hello</span>}
expect(filter(act).to_html).to eq exp
@@ -224,7 +234,7 @@ describe Banzai::Filter::SanitizationFilter do
'protocol-based JS injection: spaces and entities' => {
input: '<a href=" &#14; javascript:alert(\'XSS\');">foo</a>',
- output: '<a href="">foo</a>'
+ output: '<a href>foo</a>'
},
'protocol whitespace' => {
diff --git a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
index 0cfef4ff5bf..7213cd58ea7 100644
--- a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
+++ b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
@@ -139,5 +139,14 @@ describe Banzai::Filter::TableOfContentsFilter do
expect(items[5].ancestors).to include(items[4])
end
end
+
+ context 'header text contains escaped content' do
+ let(:content) { '&lt;img src="x" onerror="alert(42)"&gt;' }
+ let(:results) { result(header(1, content)) }
+
+ it 'outputs escaped content' do
+ expect(doc.inner_html).to include(content)
+ end
+ end
end
end
diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb
index e13406d1972..8947e2ac4fb 100644
--- a/spec/lib/extracts_path_spec.rb
+++ b/spec/lib/extracts_path_spec.rb
@@ -203,4 +203,30 @@ describe ExtractsPath do
expect(extract_ref_without_atom('foo.atom')).to eq(nil)
end
end
+
+ describe '#lfs_blob_ids' do
+ shared_examples '#lfs_blob_ids' do
+ let(:tag) { @project.repository.add_tag(@project.owner, 'my-annotated-tag', 'master', 'test tag') }
+ let(:ref) { tag.target }
+ let(:params) { { ref: ref, path: 'README.md' } }
+
+ before do
+ @project = create(:project, :repository)
+ end
+
+ it 'handles annotated tags' do
+ assign_ref_vars
+
+ expect(lfs_blob_ids).to eq([])
+ end
+ end
+
+ context 'when gitaly is enabled' do
+ it_behaves_like '#lfs_blob_ids'
+ end
+
+ context 'when gitaly is disabled', :skip_gitaly_mock do
+ it_behaves_like '#lfs_blob_ids'
+ end
+ end
end
diff --git a/spec/lib/feature_spec.rb b/spec/lib/feature_spec.rb
index 10020511bf8..6eb10497428 100644
--- a/spec/lib/feature_spec.rb
+++ b/spec/lib/feature_spec.rb
@@ -64,4 +64,28 @@ describe Feature do
expect(described_class.all).to eq(features.to_a)
end
end
+
+ describe '.flipper' do
+ shared_examples 'a memoized Flipper instance' do
+ it 'memoizes the Flipper instance' do
+ expect(Flipper).to receive(:new).once.and_call_original
+
+ 2.times do
+ described_class.flipper
+ end
+ end
+ end
+
+ context 'when request store is inactive' do
+ before do
+ described_class.instance_variable_set(:@flipper, nil)
+ end
+
+ it_behaves_like 'a memoized Flipper instance'
+ end
+
+ context 'when request store is inactive', :request_store do
+ it_behaves_like 'a memoized Flipper instance'
+ end
+ end
end
diff --git a/spec/lib/gitaly/server_spec.rb b/spec/lib/gitaly/server_spec.rb
index ed5d56e91d4..09bf21b5946 100644
--- a/spec/lib/gitaly/server_spec.rb
+++ b/spec/lib/gitaly/server_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Gitaly::Server do
+ let(:server) { described_class.new('default') }
+
describe '.all' do
let(:storages) { Gitlab.config.repositories.storages }
@@ -17,6 +19,38 @@ describe Gitaly::Server do
it { is_expected.to respond_to(:up_to_date?) }
it { is_expected.to respond_to(:address) }
+ describe 'readable?' do
+ context 'when the storage is readable' do
+ it 'returns true' do
+ expect(server).to be_readable
+ end
+ end
+
+ context 'when the storage is not readable' do
+ let(:server) { described_class.new('broken') }
+
+ it 'returns false' do
+ expect(server).not_to be_readable
+ end
+ end
+ end
+
+ describe 'writeable?' do
+ context 'when the storage is writeable' do
+ it 'returns true' do
+ expect(server).to be_writeable
+ end
+ end
+
+ context 'when the storage is not writeable' do
+ let(:server) { described_class.new('broken') }
+
+ it 'returns false' do
+ expect(server).not_to be_writeable
+ end
+ end
+ end
+
describe 'request memoization' do
context 'when requesting multiple properties', :request_store do
it 'uses memoization for the info request' do
diff --git a/spec/lib/gitlab/auth/o_auth/user_spec.rb b/spec/lib/gitlab/auth/o_auth/user_spec.rb
index 64f3d09a25b..3a8667e434d 100644
--- a/spec/lib/gitlab/auth/o_auth/user_spec.rb
+++ b/spec/lib/gitlab/auth/o_auth/user_spec.rb
@@ -779,4 +779,12 @@ describe Gitlab::Auth::OAuth::User do
end
end
end
+
+ describe '#bypass_two_factor?' do
+ subject { oauth_user.bypass_two_factor? }
+
+ it 'returns always false' do
+ is_expected.to be_falsey
+ end
+ end
end
diff --git a/spec/lib/gitlab/auth/request_authenticator_spec.rb b/spec/lib/gitlab/auth/request_authenticator_spec.rb
index ffcd90b9fcb..242ab4a91dd 100644
--- a/spec/lib/gitlab/auth/request_authenticator_spec.rb
+++ b/spec/lib/gitlab/auth/request_authenticator_spec.rb
@@ -39,19 +39,19 @@ describe Gitlab::Auth::RequestAuthenticator do
describe '#find_sessionless_user' do
let!(:access_token_user) { build(:user) }
- let!(:rss_token_user) { build(:user) }
+ let!(:feed_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)
+ allow_any_instance_of(described_class).to receive(:find_user_from_feed_token).and_return(feed_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)
+ it 'returns feed_token user if no access_token user found' do
+ allow_any_instance_of(described_class).to receive(:find_user_from_feed_token).and_return(feed_token_user)
- expect(subject.find_sessionless_user).to eq rss_token_user
+ expect(subject.find_sessionless_user).to eq feed_token_user
end
it 'returns nil if no user found' do
diff --git a/spec/lib/gitlab/auth/saml/auth_hash_spec.rb b/spec/lib/gitlab/auth/saml/auth_hash_spec.rb
index bb950e6bbf8..76f49e778fb 100644
--- a/spec/lib/gitlab/auth/saml/auth_hash_spec.rb
+++ b/spec/lib/gitlab/auth/saml/auth_hash_spec.rb
@@ -37,4 +37,55 @@ describe Gitlab::Auth::Saml::AuthHash do
end
end
end
+
+ describe '#authn_context' do
+ let(:auth_hash_data) do
+ {
+ provider: 'saml',
+ uid: 'some_uid',
+ info:
+ {
+ name: 'mockuser',
+ email: 'mock@email.ch',
+ image: 'mock_user_thumbnail_url'
+ },
+ credentials:
+ {
+ token: 'mock_token',
+ secret: 'mock_secret'
+ },
+ extra:
+ {
+ raw_info:
+ {
+ info:
+ {
+ name: 'mockuser',
+ email: 'mock@email.ch',
+ image: 'mock_user_thumbnail_url'
+ }
+ }
+ }
+ }
+ end
+
+ subject(:saml_auth_hash) { described_class.new(OmniAuth::AuthHash.new(auth_hash_data)) }
+
+ context 'with response_object' do
+ before do
+ auth_hash_data[:extra][:response_object] = { document:
+ saml_xml(File.read('spec/fixtures/authentication/saml_response.xml')) }
+ end
+
+ it 'can extract authn_context' do
+ expect(saml_auth_hash.authn_context).to eq 'urn:oasis:names:tc:SAML:2.0:ac:classes:Password'
+ end
+ end
+
+ context 'without response_object' do
+ it 'returns an empty string' do
+ expect(saml_auth_hash.authn_context).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/auth/saml/user_spec.rb b/spec/lib/gitlab/auth/saml/user_spec.rb
index 62514ca0688..c523f5e177f 100644
--- a/spec/lib/gitlab/auth/saml/user_spec.rb
+++ b/spec/lib/gitlab/auth/saml/user_spec.rb
@@ -400,4 +400,45 @@ describe Gitlab::Auth::Saml::User do
end
end
end
+
+ describe '#bypass_two_factor?' do
+ let(:saml_config) { mock_saml_config_with_upstream_two_factor_authn_contexts }
+
+ subject { saml_user.bypass_two_factor? }
+
+ context 'with authn_contexts_worth_two_factors configured' do
+ before do
+ stub_omniauth_saml_config(enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'], providers: [saml_config])
+ end
+
+ it 'returns true when authn_context is worth two factors' do
+ allow(saml_user.auth_hash).to receive(:authn_context).and_return('urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorOTPSMS')
+ is_expected.to be_truthy
+ end
+
+ it 'returns false when authn_context is not worth two factors' do
+ allow(saml_user.auth_hash).to receive(:authn_context).and_return('urn:oasis:names:tc:SAML:2.0:ac:classes:Password')
+ is_expected.to be_falsey
+ end
+
+ it 'returns false when authn_context is blank' do
+ is_expected.to be_falsey
+ end
+ end
+
+ context 'without auth_contexts_worth_two_factors_configured' do
+ before do
+ stub_omniauth_saml_config(enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'], providers: [mock_saml_config])
+ end
+
+ it 'returns false when authn_context is present' do
+ allow(saml_user.auth_hash).to receive(:authn_context).and_return('urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorOTPSMS')
+ is_expected.to be_falsey
+ end
+
+ it 'returns false when authn_context is blank' do
+ is_expected.to be_falsey
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/auth/user_access_denied_reason_spec.rb b/spec/lib/gitlab/auth/user_access_denied_reason_spec.rb
index fa209bed74e..002ce776be9 100644
--- a/spec/lib/gitlab/auth/user_access_denied_reason_spec.rb
+++ b/spec/lib/gitlab/auth/user_access_denied_reason_spec.rb
@@ -22,7 +22,8 @@ describe Gitlab::Auth::UserAccessDeniedReason do
enforce_terms
end
- it { is_expected.to match /You must accept the Terms of Service/ }
+ it { is_expected.to match /must accept the Terms of Service/ }
+ it { is_expected.to include(user.username) }
end
context 'when the user is internal' do
diff --git a/spec/lib/gitlab/auth/user_auth_finders_spec.rb b/spec/lib/gitlab/auth/user_auth_finders_spec.rb
index 2733eef6611..454ad1589b9 100644
--- a/spec/lib/gitlab/auth/user_auth_finders_spec.rb
+++ b/spec/lib/gitlab/auth/user_auth_finders_spec.rb
@@ -46,42 +46,62 @@ describe Gitlab::Auth::UserAuthFinders do
end
end
- describe '#find_user_from_rss_token' do
+ describe '#find_user_from_feed_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)
+ context 'when feed_token param is provided' do
+ it 'returns user if valid feed_token' do
+ set_param(:feed_token, user.feed_token)
- expect(find_user_from_rss_token).to eq user
- end
+ expect(find_user_from_feed_token).to eq user
+ end
+
+ it 'returns nil if feed_token is blank' do
+ expect(find_user_from_feed_token).to be_nil
+ end
+
+ it 'returns exception if invalid feed_token' do
+ set_param(:feed_token, 'invalid_token')
- it 'returns nil if rss_token is blank' do
- expect(find_user_from_rss_token).to be_nil
+ expect { find_user_from_feed_token }.to raise_error(Gitlab::Auth::UnauthorizedError)
+ end
end
- it 'returns exception if invalid rss_token' do
- set_param(:rss_token, 'invalid_token')
+ context 'when rss_token param is provided' do
+ it 'returns user if valid rssd_token' do
+ set_param(:rss_token, user.feed_token)
- expect { find_user_from_rss_token }.to raise_error(Gitlab::Auth::UnauthorizedError)
+ expect(find_user_from_feed_token).to eq user
+ end
+
+ it 'returns nil if rss_token is blank' do
+ expect(find_user_from_feed_token).to be_nil
+ end
+
+ it 'returns exception if invalid rss_token' do
+ set_param(:rss_token, 'invalid_token')
+
+ expect { find_user_from_feed_token }.to raise_error(Gitlab::Auth::UnauthorizedError)
+ end
end
end
context 'when the request format is not atom' do
it 'returns nil' do
- set_param(:rss_token, user.rss_token)
+ set_param(:feed_token, user.feed_token)
- expect(find_user_from_rss_token).to be_nil
+ expect(find_user_from_feed_token).to be_nil
end
end
context 'when the request format is empty' do
it 'the method call does not modify the original value' do
- env['action_dispatch.request.formats'] = nil
+ env.delete('action_dispatch.request.formats')
- find_user_from_rss_token
+ find_user_from_feed_token
expect(env['action_dispatch.request.formats']).to be_nil
end
diff --git a/spec/lib/gitlab/background_migration/archive_legacy_traces_spec.rb b/spec/lib/gitlab/background_migration/archive_legacy_traces_spec.rb
new file mode 100644
index 00000000000..877c061d11b
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/archive_legacy_traces_spec.rb
@@ -0,0 +1,59 @@
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::ArchiveLegacyTraces, :migration, schema: 20180529152628 do
+ include TraceHelpers
+
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:builds) { table(:ci_builds) }
+ let(:job_artifacts) { table(:ci_job_artifacts) }
+
+ before do
+ namespaces.create!(id: 123, name: 'gitlab1', path: 'gitlab1')
+ projects.create!(id: 123, name: 'gitlab1', path: 'gitlab1', namespace_id: 123)
+ @build = builds.create!(id: 1, project_id: 123, status: 'success', type: 'Ci::Build')
+ end
+
+ context 'when trace file exsits at the right place' do
+ before do
+ create_legacy_trace(@build, 'trace in file')
+ end
+
+ it 'correctly archive legacy traces' do
+ expect(job_artifacts.count).to eq(0)
+ expect(File.exist?(legacy_trace_path(@build))).to be_truthy
+
+ described_class.new.perform(1, 1)
+
+ expect(job_artifacts.count).to eq(1)
+ expect(File.exist?(legacy_trace_path(@build))).to be_falsy
+ expect(File.read(archived_trace_path(job_artifacts.first))).to eq('trace in file')
+ end
+ end
+
+ context 'when trace file does not exsits at the right place' do
+ it 'does not raise errors nor create job artifact' do
+ expect { described_class.new.perform(1, 1) }.not_to raise_error
+
+ expect(job_artifacts.count).to eq(0)
+ end
+ end
+
+ context 'when trace data exsits in database' do
+ before do
+ create_legacy_trace_in_db(@build, 'trace in db')
+ end
+
+ it 'correctly archive legacy traces' do
+ expect(job_artifacts.count).to eq(0)
+ expect(@build.read_attribute(:trace)).not_to be_empty
+
+ described_class.new.perform(1, 1)
+
+ @build.reload
+ expect(job_artifacts.count).to eq(1)
+ expect(@build.read_attribute(:trace)).to be_nil
+ expect(File.read(archived_trace_path(job_artifacts.first))).to eq('trace in db')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/delete_diff_files_spec.rb b/spec/lib/gitlab/background_migration/delete_diff_files_spec.rb
new file mode 100644
index 00000000000..64c994a268f
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/delete_diff_files_spec.rb
@@ -0,0 +1,73 @@
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::DeleteDiffFiles, :migration, schema: 20180619121030 do
+ describe '#perform' do
+ context 'when diff files can be deleted' do
+ let(:merge_request) { create(:merge_request, :merged) }
+ let!(:merge_request_diff) do
+ merge_request.create_merge_request_diff
+ merge_request.merge_request_diffs.first
+ end
+
+ let(:perform) do
+ described_class.new.perform(MergeRequestDiff.pluck(:id))
+ end
+
+ it 'deletes all merge request diff files' do
+ expect { perform }
+ .to change { merge_request_diff.merge_request_diff_files.count }
+ .from(20).to(0)
+ end
+
+ it 'updates state to without_files' do
+ expect { perform }
+ .to change { merge_request_diff.reload.state }
+ .from('collected').to('without_files')
+ end
+
+ it 'rollsback if something goes wrong' do
+ expect(described_class::MergeRequestDiffFile).to receive_message_chain(:where, :delete_all)
+ .and_raise
+
+ expect { perform }
+ .to raise_error
+
+ merge_request_diff.reload
+
+ expect(merge_request_diff.state).to eq('collected')
+ expect(merge_request_diff.merge_request_diff_files.count).to eq(20)
+ end
+ end
+
+ it 'reschedules itself when should_wait_deadtuple_vacuum' do
+ merge_request = create(:merge_request, :merged)
+ first_diff = merge_request.merge_request_diff
+ second_diff = merge_request.create_merge_request_diff
+
+ Sidekiq::Testing.fake! do
+ worker = described_class.new
+ allow(worker).to receive(:should_wait_deadtuple_vacuum?) { true }
+
+ worker.perform([first_diff.id, second_diff.id])
+
+ expect(described_class.name.demodulize).to be_scheduled_delayed_migration(5.minutes, [first_diff.id, second_diff.id])
+ expect(BackgroundMigrationWorker.jobs.size).to eq(1)
+ end
+ end
+ end
+
+ describe '#should_wait_deadtuple_vacuum?' do
+ it 'returns true when hitting merge_request_diff_files hits DEAD_TUPLES_THRESHOLD', :postgresql do
+ worker = described_class.new
+ threshold_query_result = [{ "n_dead_tup" => described_class::DEAD_TUPLES_THRESHOLD.to_s }]
+ normal_query_result = [{ "n_dead_tup" => '3' }]
+
+ allow(worker)
+ .to receive(:execute_statement)
+ .with(/SELECT n_dead_tup */)
+ .and_return(threshold_query_result, normal_query_result)
+
+ expect(worker.should_wait_deadtuple_vacuum?).to be(true)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb b/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb
index 007e93c1db6..211e3aaa94b 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
@@ -299,7 +299,11 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :m
let(:commits) { merge_request_diff.commits.map(&:to_hash) }
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(:diffs) do
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ first_commit.rugged_diff_from_parent.patches
+ end
+ end
let(:expected_diffs) { [] }
include_examples 'updated MR diff'
@@ -309,7 +313,11 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :m
let(:commits) { merge_request_diff.commits.map(&:to_hash) }
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(:diffs) do
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ first_commit.rugged_diff_from_parent.deltas
+ end
+ end
let(:expected_diffs) { [] }
include_examples 'updated MR diff'
diff --git a/spec/lib/gitlab/background_migration/fix_cross_project_label_links_spec.rb b/spec/lib/gitlab/background_migration/fix_cross_project_label_links_spec.rb
new file mode 100644
index 00000000000..20af63bc6c8
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/fix_cross_project_label_links_spec.rb
@@ -0,0 +1,109 @@
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::FixCrossProjectLabelLinks, :migration, schema: 20180702120647 do
+ let(:namespaces_table) { table(:namespaces) }
+ let(:projects_table) { table(:projects) }
+ let(:issues_table) { table(:issues) }
+ let(:merge_requests_table) { table(:merge_requests) }
+ let(:labels_table) { table(:labels) }
+ let(:label_links_table) { table(:label_links) }
+
+ let!(:group1) { namespaces_table.create(id: 10, type: 'Group', name: 'group1', path: 'group1') }
+ let!(:group2) { namespaces_table.create(id: 20, type: 'Group', name: 'group2', path: 'group2') }
+
+ let!(:project1) { projects_table.create(id: 1, name: 'project1', path: 'group1/project1', namespace_id: 10) }
+ let!(:project2) { projects_table.create(id: 3, name: 'project2', path: 'group1/project2', namespace_id: 20) }
+
+ let!(:label1) { labels_table.create(id: 1, title: 'bug', color: 'red', group_id: 10, type: 'GroupLabel') }
+ let!(:label2) { labels_table.create(id: 2, title: 'bug', color: 'red', group_id: 20, type: 'GroupLabel') }
+
+ def create_merge_request(id, project_id)
+ merge_requests_table.create(id: id,
+ target_project_id: project_id,
+ target_branch: 'master',
+ source_project_id: project_id,
+ source_branch: 'mr name',
+ title: "mr name#{id}")
+ end
+
+ def create_issue(id, project_id)
+ issues_table.create(id: id, title: "issue#{id}", project_id: project_id)
+ end
+
+ def create_resource(target_type, id, project_id)
+ target_type == 'Issue' ? create_issue(id, project_id) : create_merge_request(id, project_id)
+ end
+
+ shared_examples_for 'resource with cross-project labels' do
+ it 'updates only cross-project label links which exist in the local project or group' do
+ create_resource(target_type, 1, 1)
+ create_resource(target_type, 2, 3)
+ labels_table.create(id: 3, title: 'bug', color: 'red', project_id: 3, type: 'ProjectLabel')
+ link = label_links_table.create(label_id: 2, target_type: target_type, target_id: 1)
+ link2 = label_links_table.create(label_id: 3, target_type: target_type, target_id: 2)
+
+ subject.perform(1, 100)
+
+ expect(link.reload.label_id).to eq(1)
+ expect(link2.reload.label_id).to eq(3)
+ end
+
+ it 'ignores cross-project label links if label color is different' do
+ labels_table.create(id: 3, title: 'bug', color: 'green', group_id: 20, type: 'GroupLabel')
+ create_resource(target_type, 1, 1)
+ link = label_links_table.create(label_id: 3, target_type: target_type, target_id: 1)
+
+ subject.perform(1, 100)
+
+ expect(link.reload.label_id).to eq(3)
+ end
+
+ it 'ignores cross-project label links if label name is different' do
+ labels_table.create(id: 3, title: 'bug1', color: 'red', group_id: 20, type: 'GroupLabel')
+ create_resource(target_type, 1, 1)
+ link = label_links_table.create(label_id: 3, target_type: target_type, target_id: 1)
+
+ subject.perform(1, 100)
+
+ expect(link.reload.label_id).to eq(3)
+ end
+
+ context 'with nested group' do
+ before do
+ namespaces_table.create(id: 11, type: 'Group', name: 'subgroup1', path: 'group1/subgroup1', parent_id: 10)
+ projects_table.create(id: 2, name: 'subproject1', path: 'group1/subgroup1/subproject1', namespace_id: 11)
+ create_resource(target_type, 1, 2)
+ end
+
+ it 'ignores label links referencing ancestor group labels', :nested_groups do
+ labels_table.create(id: 4, title: 'bug', color: 'red', project_id: 2, type: 'ProjectLabel')
+ label_links_table.create(label_id: 4, target_type: target_type, target_id: 1)
+ link = label_links_table.create(label_id: 1, target_type: target_type, target_id: 1)
+
+ subject.perform(1, 100)
+
+ expect(link.reload.label_id).to eq(1)
+ end
+
+ it 'checks also issues and MRs in subgroups', :nested_groups do
+ link = label_links_table.create(label_id: 2, target_type: target_type, target_id: 1)
+
+ subject.perform(1, 100)
+
+ expect(link.reload.label_id).to eq(1)
+ end
+ end
+ end
+
+ context 'resource is Issue' do
+ it_behaves_like 'resource with cross-project labels' do
+ let(:target_type) { 'Issue' }
+ end
+ end
+
+ context 'resource is Merge Request' do
+ it_behaves_like 'resource with cross-project labels' do
+ let(:target_type) { 'MergeRequest' }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/move_personal_snippet_files_spec.rb b/spec/lib/gitlab/background_migration/move_personal_snippet_files_spec.rb
index ee60e498b59..2e77e80ee46 100644
--- a/spec/lib/gitlab/background_migration/move_personal_snippet_files_spec.rb
+++ b/spec/lib/gitlab/background_migration/move_personal_snippet_files_spec.rb
@@ -7,7 +7,7 @@ describe Gitlab::BackgroundMigration::MovePersonalSnippetFiles do
let(:snippet) do
snippet = create(:personal_snippet)
create_upload_for_snippet(snippet)
- snippet.update_attributes!(description: markdown_linking_file(snippet))
+ snippet.update!(description: markdown_linking_file(snippet))
snippet
end
diff --git a/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb b/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb
index 0d2074eed22..0dee683350f 100644
--- a/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb
+++ b/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb
@@ -114,7 +114,7 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migra
it 'does not drop the temporary tracking table after processing the batch, if there are still untracked rows' do
subject.perform(1, untracked_files_for_uploads.last.id - 1)
- expect(ActiveRecord::Base.connection.table_exists?(:untracked_files_for_uploads)).to be_truthy
+ expect(ActiveRecord::Base.connection.data_source_exists?(:untracked_files_for_uploads)).to be_truthy
end
it 'drops the temporary tracking table after processing the batch, if there are no untracked rows left' do
diff --git a/spec/lib/gitlab/background_migration/schedule_diff_files_deletion_spec.rb b/spec/lib/gitlab/background_migration/schedule_diff_files_deletion_spec.rb
new file mode 100644
index 00000000000..fb5093b0bd1
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/schedule_diff_files_deletion_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::ScheduleDiffFilesDeletion, :migration, schema: 20180619121030 do
+ describe '#perform' do
+ let(:merge_request_diffs) { table(:merge_request_diffs) }
+ let(:merge_requests) { table(:merge_requests) }
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+
+ before do
+ stub_const("#{described_class.name}::DIFF_BATCH_SIZE", 3)
+
+ namespaces.create!(id: 1, name: 'gitlab', path: 'gitlab')
+ projects.create!(id: 1, namespace_id: 1, name: 'gitlab', path: 'gitlab')
+
+ merge_requests.create!(id: 1, target_project_id: 1, source_project_id: 1, target_branch: 'feature', source_branch: 'master', state: 'merged')
+
+ merge_request_diffs.create!(id: 1, merge_request_id: 1, state: 'collected')
+ merge_request_diffs.create!(id: 2, merge_request_id: 1, state: 'empty')
+ merge_request_diffs.create!(id: 3, merge_request_id: 1, state: 'without_files')
+ merge_request_diffs.create!(id: 4, merge_request_id: 1, state: 'collected')
+ merge_request_diffs.create!(id: 5, merge_request_id: 1, state: 'collected')
+ merge_request_diffs.create!(id: 6, merge_request_id: 1, state: 'collected')
+ merge_request_diffs.create!(id: 7, merge_request_id: 1, state: 'collected')
+
+ merge_requests.update(1, latest_merge_request_diff_id: 7)
+ end
+
+ it 'correctly schedules diff file deletion workers' do
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ described_class.new.perform
+
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, [1, 4, 5])
+
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(10.minutes, [6])
+
+ expect(BackgroundMigrationWorker.jobs.size).to eq(2)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/bare_repository_import/importer_spec.rb b/spec/lib/gitlab/bare_repository_import/importer_spec.rb
index 5c8a19a53bc..468f6ff6d24 100644
--- a/spec/lib/gitlab/bare_repository_import/importer_spec.rb
+++ b/spec/lib/gitlab/bare_repository_import/importer_spec.rb
@@ -20,6 +20,13 @@ describe Gitlab::BareRepositoryImport::Importer, repository: true do
Rainbow.enabled = @rainbow
end
+ around do |example|
+ # TODO migrate BareRepositoryImport https://gitlab.com/gitlab-org/gitaly/issues/953
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ example.run
+ end
+ end
+
shared_examples 'importing a repository' do
describe '.execute' do
it 'creates a project for a repository in storage' do
diff --git a/spec/lib/gitlab/bare_repository_import/repository_spec.rb b/spec/lib/gitlab/bare_repository_import/repository_spec.rb
index 1504826c7a5..afd8f5da39f 100644
--- a/spec/lib/gitlab/bare_repository_import/repository_spec.rb
+++ b/spec/lib/gitlab/bare_repository_import/repository_spec.rb
@@ -62,8 +62,10 @@ describe ::Gitlab::BareRepositoryImport::Repository do
before do
gitlab_shell.create_repository(repository_storage, hashed_path)
- repository = Rugged::Repository.new(repo_path)
- repository.config['gitlab.fullpath'] = 'to/repo'
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ repository = Rugged::Repository.new(repo_path)
+ repository.config['gitlab.fullpath'] = 'to/repo'
+ end
end
after do
diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
index c63120b0b29..05c232d22cf 100644
--- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
@@ -19,6 +19,18 @@ describe Gitlab::BitbucketImport::Importer do
]
end
+ let(:reporters) do
+ [
+ nil,
+ { "username" => "reporter1" },
+ nil,
+ { "username" => "reporter2" },
+ { "username" => "reporter1" },
+ nil,
+ { "username" => "reporter3" }
+ ]
+ end
+
let(:sample_issues_statuses) do
issues = []
@@ -36,6 +48,10 @@ describe Gitlab::BitbucketImport::Importer do
}
end
+ reporters.map.with_index do |reporter, index|
+ issues[index]['reporter'] = reporter
+ end
+
issues
end
@@ -147,5 +163,19 @@ describe Gitlab::BitbucketImport::Importer do
expect(importer.errors).to be_empty
end
end
+
+ describe 'issue import' do
+ it 'maps reporters to anonymous if bitbucket reporter is nil' do
+ allow(importer).to receive(:import_wiki)
+ importer.execute
+
+ expect(project.issues.size).to eq(7)
+ expect(project.issues.where("description LIKE ?", '%Anonymous%').size).to eq(3)
+ expect(project.issues.where("description LIKE ?", '%reporter1%').size).to eq(2)
+ expect(project.issues.where("description LIKE ?", '%reporter2%').size).to eq(1)
+ expect(project.issues.where("description LIKE ?", '%reporter3%').size).to eq(1)
+ expect(importer.errors).to be_empty
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb
index c3f528dd6fc..ed6fa3d229f 100644
--- a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb
@@ -25,7 +25,9 @@ describe Gitlab::BitbucketImport::ProjectCreator do
end
it 'creates project' do
- allow_any_instance_of(Project).to receive(:add_import_job)
+ expect_next_instance_of(Project) do |project|
+ expect(project).to receive(:add_import_job)
+ end
project_creator = described_class.new(repo, 'vim', namespace, user, access_params)
project = project_creator.execute
diff --git a/spec/lib/gitlab/checks/change_access_spec.rb b/spec/lib/gitlab/checks/change_access_spec.rb
index 48e9902027c..7c04aa27971 100644
--- a/spec/lib/gitlab/checks/change_access_spec.rb
+++ b/spec/lib/gitlab/checks/change_access_spec.rb
@@ -52,9 +52,9 @@ describe Gitlab::Checks::ChangeAccess do
context 'with protected tag' do
let!(:protected_tag) { create(:protected_tag, project: project, name: 'v*') }
- context 'as master' do
+ context 'as maintainer' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
context 'deletion' do
@@ -138,13 +138,13 @@ describe Gitlab::Checks::ChangeAccess do
context 'if the user is not allowed to delete protected branches' do
it 'raises an error' do
- 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.')
+ expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to delete protected branches from this project. Only a project maintainer or owner can delete a protected branch.')
end
end
context 'if the user is allowed to delete protected branches' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
context 'through the web interface' do
diff --git a/spec/lib/gitlab/checks/force_push_spec.rb b/spec/lib/gitlab/checks/force_push_spec.rb
index a65012d2314..0e0788ce974 100644
--- a/spec/lib/gitlab/checks/force_push_spec.rb
+++ b/spec/lib/gitlab/checks/force_push_spec.rb
@@ -1,21 +1,17 @@
require 'spec_helper'
describe Gitlab::Checks::ForcePush do
- let(:project) { create(:project, :repository) }
- let(:repository) { project.repository.raw }
+ set(:project) { create(:project, :repository) }
- 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(repository).to receive(:popen).and_return(['normal output', 0])
+ describe '.force_push?' do
+ it 'returns false if the repo is empty' do
+ allow(project).to receive(:empty_repo?).and_return(true)
- expect { described_class.force_push?(project, 'oldrev', 'newrev') }.not_to raise_error
+ expect(described_class.force_push?(project, 'HEAD', 'HEAD~')).to be(false)
end
- it "raises a GitError error if the `popen` call to git returns a non-zero exit code" do
- allow(repository).to receive(:popen).and_return(['error', 1])
-
- expect { described_class.force_push?(project, 'oldrev', 'newrev') }
- .to raise_error(Gitlab::Git::Repository::GitError)
+ it 'checks if old rev is an anchestor' do
+ expect(described_class.force_push?(project, 'HEAD', 'HEAD~')).to be(true)
end
end
end
diff --git a/spec/lib/gitlab/checks/lfs_integrity_spec.rb b/spec/lib/gitlab/checks/lfs_integrity_spec.rb
index 7201e4f7bf6..ec22e3a198e 100644
--- a/spec/lib/gitlab/checks/lfs_integrity_spec.rb
+++ b/spec/lib/gitlab/checks/lfs_integrity_spec.rb
@@ -6,7 +6,9 @@ describe Gitlab::Checks::LfsIntegrity do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
let(:newrev) do
- operations = BareRepoOperations.new(repository.path)
+ operations = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ BareRepoOperations.new(repository.path)
+ end
# Create a commit not pointed at by any ref to emulate being in the
# pre-receive hook so that `--not --all` returns some objects
diff --git a/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb b/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb
index 6a52ae01b2f..e327399d82d 100644
--- a/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb
+++ b/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb
@@ -2,13 +2,21 @@ require 'spec_helper'
describe Gitlab::Ci::Build::Artifacts::Metadata do
def metadata(path = '', **opts)
- described_class.new(metadata_file_path, path, **opts)
+ described_class.new(metadata_file_stream, path, **opts)
end
let(:metadata_file_path) do
Rails.root + 'spec/fixtures/ci_build_artifacts_metadata.gz'
end
+ let(:metadata_file_stream) do
+ File.open(metadata_file_path) if metadata_file_path
+ end
+
+ after do
+ metadata_file_stream&.close
+ end
+
context 'metadata file exists' do
describe '#find_entries! empty string' do
subject { metadata('').find_entries! }
@@ -86,11 +94,21 @@ describe Gitlab::Ci::Build::Artifacts::Metadata do
end
context 'metadata file does not exist' do
- let(:metadata_file_path) { '' }
+ let(:metadata_file_path) { nil }
+
+ describe '#find_entries!' do
+ it 'raises error' do
+ expect { metadata.find_entries! }.to raise_error(described_class::InvalidStreamError, /Invalid stream/)
+ end
+ end
+ end
+
+ context 'metadata file is invalid' do
+ let(:metadata_file_path) { Rails.root + 'spec/fixtures/ci_build_artifacts.zip' }
describe '#find_entries!' do
it 'raises error' do
- expect { metadata.find_entries! }.to raise_error(Errno::ENOENT)
+ expect { metadata.find_entries! }.to raise_error(described_class::InvalidStreamError, /not in gzip format/)
end
end
end
diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb
index bc5a5e43103..2e204da307d 100644
--- a/spec/lib/gitlab/ci/config_spec.rb
+++ b/spec/lib/gitlab/ci/config_spec.rb
@@ -49,7 +49,7 @@ describe Gitlab::Ci::Config do
describe '.new' do
it 'raises error' do
expect { config }.to raise_error(
- Gitlab::Ci::Config::Loader::FormatError,
+ ::Gitlab::Ci::Config::Loader::FormatError,
/Invalid configuration format/
)
end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb
index 4d7d6951a51..284aed91e29 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::Ci::Pipeline::Chain::Populate do
- set(:project) { create(:project) }
+ set(:project) { create(:project, :repository) }
set(:user) { create(:user) }
let(:pipeline) do
@@ -42,6 +42,10 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do
it 'correctly assigns user' do
expect(pipeline.builds).to all(have_attributes(user: user))
end
+
+ it 'has pipeline iid' do
+ expect(pipeline.iid).to be > 0
+ end
end
context 'when pipeline is empty' do
@@ -68,6 +72,10 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do
expect(pipeline.errors.to_a)
.to include 'No stages / jobs for this pipeline.'
end
+
+ it 'wastes pipeline iid' do
+ expect(InternalId.ci_pipelines.where(project_id: project.id).last.last_value).to be > 0
+ end
end
context 'when pipeline has validation errors' do
@@ -87,6 +95,10 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do
expect(pipeline.errors.to_a)
.to include 'Failed to build the pipeline!'
end
+
+ it 'wastes pipeline iid' do
+ expect(InternalId.ci_pipelines.where(project_id: project.id).last.last_value).to be > 0
+ end
end
context 'when there is a seed blocks present' do
@@ -111,6 +123,12 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do
expect(pipeline.variables.first.key).to eq 'VAR'
expect(pipeline.variables.first.value).to eq '123'
end
+
+ it 'has pipeline iid' do
+ step.perform!
+
+ expect(pipeline.iid).to be > 0
+ end
end
context 'when seeds block tries to persist some resources' do
@@ -121,6 +139,12 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do
it 'raises exception' do
expect { step.perform! }.to raise_error(ActiveRecord::RecordNotSaved)
end
+
+ it 'wastes pipeline iid' do
+ expect { step.perform! }.to raise_error
+
+ expect(InternalId.ci_pipelines.where(project_id: project.id).last.last_value).to be > 0
+ end
end
end
@@ -132,22 +156,39 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do
end
end
- context 'when using only/except build policies' do
- let(:config) do
- { rspec: { script: 'rspec', stage: 'test', only: ['master'] },
- prod: { script: 'cap prod', stage: 'deploy', only: ['tags'] } }
- end
+ context 'when variables policy is specified' do
+ shared_examples_for 'a correct pipeline' do
+ it 'populates pipeline according to used policies' do
+ step.perform!
- let(:pipeline) do
- build(:ci_pipeline, ref: 'master', config: config)
+ expect(pipeline.stages.size).to eq 1
+ expect(pipeline.stages.first.builds.size).to eq 1
+ expect(pipeline.stages.first.builds.first.name).to eq 'rspec'
+ end
end
- it 'populates pipeline according to used policies' do
- step.perform!
+ context 'when using only/except build policies' do
+ let(:config) do
+ { rspec: { script: 'rspec', stage: 'test', only: ['master'] },
+ prod: { script: 'cap prod', stage: 'deploy', only: ['tags'] } }
+ end
+
+ let(:pipeline) do
+ build(:ci_pipeline, ref: 'master', project: project, config: config)
+ end
- expect(pipeline.stages.size).to eq 1
- expect(pipeline.stages.first.builds.size).to eq 1
- expect(pipeline.stages.first.builds.first.name).to eq 'rspec'
+ it_behaves_like 'a correct pipeline'
+
+ context 'when variables expression is specified' do
+ context 'when pipeline iid is the subject' do
+ let(:config) do
+ { rspec: { script: 'rspec', only: { variables: ["$CI_PIPELINE_IID == '1'"] } },
+ prod: { script: 'cap prod', only: { variables: ["$CI_PIPELINE_IID == '1000'"] } } }
+ end
+
+ it_behaves_like 'a correct pipeline'
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb
index a973ccda8de..8ba56d73838 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb
@@ -99,9 +99,9 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Abilities do
end
end
- context 'when user is a master' do
+ context 'when user is a maintainer' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
it { is_expected.to be_truthy }
diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb
index c53294d091c..a8dc5356413 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::Ci::Pipeline::Chain::Validate::Config do
- set(:project) { create(:project) }
+ set(:project) { create(:project, :repository) }
set(:user) { create(:user) }
let(:command) do
diff --git a/spec/lib/gitlab/ci/pipeline/preloader_spec.rb b/spec/lib/gitlab/ci/pipeline/preloader_spec.rb
index 477c7477df0..40dfd893465 100644
--- a/spec/lib/gitlab/ci/pipeline/preloader_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/preloader_spec.rb
@@ -3,18 +3,47 @@
require 'spec_helper'
describe Gitlab::Ci::Pipeline::Preloader do
- describe '.preload' do
- it 'preloads the author of every pipeline commit' do
- commit = double(:commit)
- pipeline = double(:pipeline, commit: commit)
+ let(:stage) { double(:stage) }
+ let(:commit) { double(:commit) }
- expect(commit)
- .to receive(:lazy_author)
+ let(:pipeline) do
+ double(:pipeline, commit: commit, stages: [stage])
+ end
+
+ describe '.preload!' do
+ context 'when preloading multiple commits' do
+ let(:project) { create(:project, :repository) }
+
+ it 'preloads all commits once' do
+ expect(Commit).to receive(:decorate).once.and_call_original
+
+ pipelines = [build_pipeline(ref: 'HEAD'),
+ build_pipeline(ref: 'HEAD~1')]
+
+ described_class.preload!(pipelines)
+ end
+
+ def build_pipeline(ref:)
+ build_stubbed(:ci_pipeline, project: project, sha: project.commit(ref).id)
+ end
+ end
+
+ it 'preloads commit authors and number of warnings' do
+ expect(commit).to receive(:lazy_author)
+ expect(pipeline).to receive(:number_of_warnings)
+ expect(stage).to receive(:number_of_warnings)
+
+ described_class.preload!([pipeline])
+ end
+
+ it 'returns original collection' do
+ allow(commit).to receive(:lazy_author)
+ allow(pipeline).to receive(:number_of_warnings)
+ allow(stage).to receive(:number_of_warnings)
- expect(pipeline)
- .to receive(:number_of_warnings)
+ pipelines = [pipeline, pipeline]
- described_class.preload([pipeline])
+ expect(described_class.preload!(pipelines)).to eq pipelines
end
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 e2bb378f663..02f8c4c114b 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
context 'when user can not push to the branch' do
before do
build.project.add_developer(user)
- create(:protected_branch, :masters_can_push,
+ create(:protected_branch, :maintainers_can_push,
name: build.ref, project: project)
end
diff --git a/spec/lib/gitlab/ci/status/stage/common_spec.rb b/spec/lib/gitlab/ci/status/stage/common_spec.rb
index 6ec35f8da7e..bb2d0a2c75c 100644
--- a/spec/lib/gitlab/ci/status/stage/common_spec.rb
+++ b/spec/lib/gitlab/ci/status/stage/common_spec.rb
@@ -27,7 +27,7 @@ describe Gitlab::Ci::Status::Stage::Common do
context 'when user has permission to read pipeline' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
it 'has details' do
diff --git a/spec/lib/gitlab/ci/trace/http_io_spec.rb b/spec/lib/gitlab/ci/trace/http_io_spec.rb
deleted file mode 100644
index 5474e2f518c..00000000000
--- a/spec/lib/gitlab/ci/trace/http_io_spec.rb
+++ /dev/null
@@ -1,315 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Ci::Trace::HttpIO do
- include HttpIOHelpers
-
- let(:http_io) { described_class.new(url, size) }
- let(:url) { remote_trace_url }
- let(:size) { remote_trace_size }
-
- describe '#close' do
- subject { http_io.close }
-
- it { is_expected.to be_nil }
- end
-
- describe '#binmode' do
- subject { http_io.binmode }
-
- it { is_expected.to be_nil }
- end
-
- describe '#binmode?' do
- subject { http_io.binmode? }
-
- it { is_expected.to be_truthy }
- end
-
- describe '#path' do
- subject { http_io.path }
-
- it { is_expected.to be_nil }
- end
-
- describe '#url' do
- subject { http_io.url }
-
- it { is_expected.to eq(url) }
- end
-
- describe '#seek' do
- subject { http_io.seek(pos, where) }
-
- context 'when moves pos to end of the file' do
- let(:pos) { 0 }
- let(:where) { IO::SEEK_END }
-
- it { is_expected.to eq(size) }
- end
-
- context 'when moves pos to middle of the file' do
- let(:pos) { size / 2 }
- let(:where) { IO::SEEK_SET }
-
- it { is_expected.to eq(size / 2) }
- end
-
- context 'when moves pos around' do
- it 'matches the result' do
- expect(http_io.seek(0)).to eq(0)
- expect(http_io.seek(100, IO::SEEK_CUR)).to eq(100)
- expect { http_io.seek(size + 1, IO::SEEK_CUR) }.to raise_error('new position is outside of file')
- end
- end
- end
-
- describe '#eof?' do
- subject { http_io.eof? }
-
- context 'when current pos is at end of the file' do
- before do
- http_io.seek(size, IO::SEEK_SET)
- end
-
- it { is_expected.to be_truthy }
- end
-
- context 'when current pos is not at end of the file' do
- before do
- http_io.seek(0, IO::SEEK_SET)
- end
-
- it { is_expected.to be_falsey }
- end
- end
-
- describe '#each_line' do
- subject { http_io.each_line }
-
- let(:string_io) { StringIO.new(remote_trace_body) }
-
- before do
- stub_remote_trace_206
- end
-
- it 'yields lines' do
- expect { |b| http_io.each_line(&b) }.to yield_successive_args(*string_io.each_line.to_a)
- end
-
- context 'when buckets on GCS' do
- context 'when BUFFER_SIZE is larger than file size' do
- before do
- stub_remote_trace_200
- set_larger_buffer_size_than(size)
- end
-
- it 'calls get_chunk only once' do
- expect_any_instance_of(Net::HTTP).to receive(:request).once.and_call_original
-
- http_io.each_line { |line| }
- end
- end
- end
- end
-
- describe '#read' do
- subject { http_io.read(length) }
-
- context 'when there are no network issue' do
- before do
- stub_remote_trace_206
- end
-
- context 'when read whole size' do
- let(:length) { nil }
-
- context 'when BUFFER_SIZE is smaller than file size' do
- before do
- set_smaller_buffer_size_than(size)
- end
-
- it 'reads a trace' do
- is_expected.to eq(remote_trace_body)
- end
- end
-
- context 'when BUFFER_SIZE is larger than file size' do
- before do
- set_larger_buffer_size_than(size)
- end
-
- it 'reads a trace' do
- is_expected.to eq(remote_trace_body)
- end
- end
- end
-
- context 'when read only first 100 bytes' do
- let(:length) { 100 }
-
- context 'when BUFFER_SIZE is smaller than file size' do
- before do
- set_smaller_buffer_size_than(size)
- end
-
- it 'reads a trace' do
- is_expected.to eq(remote_trace_body[0, length])
- end
- end
-
- context 'when BUFFER_SIZE is larger than file size' do
- before do
- set_larger_buffer_size_than(size)
- end
-
- it 'reads a trace' do
- is_expected.to eq(remote_trace_body[0, length])
- end
- end
- end
-
- context 'when tries to read oversize' do
- let(:length) { size + 1000 }
-
- context 'when BUFFER_SIZE is smaller than file size' do
- before do
- set_smaller_buffer_size_than(size)
- end
-
- it 'reads a trace' do
- is_expected.to eq(remote_trace_body)
- end
- end
-
- context 'when BUFFER_SIZE is larger than file size' do
- before do
- set_larger_buffer_size_than(size)
- end
-
- it 'reads a trace' do
- is_expected.to eq(remote_trace_body)
- end
- end
- end
-
- context 'when tries to read 0 bytes' do
- let(:length) { 0 }
-
- context 'when BUFFER_SIZE is smaller than file size' do
- before do
- set_smaller_buffer_size_than(size)
- end
-
- it 'reads a trace' do
- is_expected.to be_empty
- end
- end
-
- context 'when BUFFER_SIZE is larger than file size' do
- before do
- set_larger_buffer_size_than(size)
- end
-
- it 'reads a trace' do
- is_expected.to be_empty
- end
- end
- end
- end
-
- context 'when there is anetwork issue' do
- let(:length) { nil }
-
- before do
- stub_remote_trace_500
- end
-
- it 'reads a trace' do
- expect { subject }.to raise_error(Gitlab::Ci::Trace::HttpIO::FailedToGetChunkError)
- end
- end
- end
-
- describe '#readline' do
- subject { http_io.readline }
-
- let(:string_io) { StringIO.new(remote_trace_body) }
-
- before do
- stub_remote_trace_206
- end
-
- shared_examples 'all line matching' do
- it 'reads a line' do
- (0...remote_trace_body.lines.count).each do
- expect(http_io.readline).to eq(string_io.readline)
- end
- end
- end
-
- context 'when there is anetwork issue' do
- let(:length) { nil }
-
- before do
- stub_remote_trace_500
- end
-
- it 'reads a trace' do
- expect { subject }.to raise_error(Gitlab::Ci::Trace::HttpIO::FailedToGetChunkError)
- end
- end
-
- context 'when BUFFER_SIZE is smaller than file size' do
- before do
- set_smaller_buffer_size_than(size)
- end
-
- it_behaves_like 'all line matching'
- end
-
- context 'when BUFFER_SIZE is larger than file size' do
- before do
- set_larger_buffer_size_than(size)
- end
-
- it_behaves_like 'all line matching'
- end
-
- context 'when pos is at middle of the file' do
- before do
- set_smaller_buffer_size_than(size)
-
- http_io.seek(size / 2)
- string_io.seek(size / 2)
- end
-
- it 'reads from pos' do
- expect(http_io.readline).to eq(string_io.readline)
- end
- end
- end
-
- describe '#write' do
- subject { http_io.write(nil) }
-
- it { expect { subject }.to raise_error(NotImplementedError) }
- end
-
- describe '#truncate' do
- subject { http_io.truncate(nil) }
-
- it { expect { subject }.to raise_error(NotImplementedError) }
- end
-
- describe '#flush' do
- subject { http_io.flush }
-
- it { expect { subject }.to raise_error(NotImplementedError) }
- end
-
- describe '#present?' do
- subject { http_io.present? }
-
- it { is_expected.to be_truthy }
- end
-end
diff --git a/spec/lib/gitlab/ci/trace_spec.rb b/spec/lib/gitlab/ci/trace_spec.rb
index e9d755c2021..d6510649dba 100644
--- a/spec/lib/gitlab/ci/trace_spec.rb
+++ b/spec/lib/gitlab/ci/trace_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Ci::Trace, :clean_gitlab_redis_cache do
+describe Gitlab::Ci::Trace, :clean_gitlab_redis_shared_state do
let(:build) { create(:ci_build) }
let(:trace) { described_class.new(build) }
diff --git a/spec/lib/gitlab/ci/variables/collection/item_spec.rb b/spec/lib/gitlab/ci/variables/collection/item_spec.rb
index e79f0a7f257..adb3ff4321f 100644
--- a/spec/lib/gitlab/ci/variables/collection/item_spec.rb
+++ b/spec/lib/gitlab/ci/variables/collection/item_spec.rb
@@ -1,19 +1,69 @@
require 'spec_helper'
describe Gitlab::Ci::Variables::Collection::Item do
+ let(:variable_key) { 'VAR' }
+ let(:variable_value) { 'something' }
+ let(:expected_value) { variable_value }
+
let(:variable) do
- { key: 'VAR', value: 'something', public: true }
+ { key: variable_key, value: variable_value, public: true }
end
describe '.new' do
- it 'raises error if unknown key i specified' do
- expect { described_class.new(key: 'VAR', value: 'abc', files: true) }
- .to raise_error ArgumentError, 'unknown keyword: files'
+ context 'when unknown keyword is specified' do
+ it 'raises error' do
+ expect { described_class.new(key: variable_key, value: 'abc', files: true) }
+ .to raise_error ArgumentError, 'unknown keyword: files'
+ end
+ end
+
+ context 'when required keywords are not specified' do
+ it 'raises error' do
+ expect { described_class.new(key: variable_key) }
+ .to raise_error ArgumentError, 'missing keyword: value'
+ end
end
- it 'raises error when required keywords are not specified' do
- expect { described_class.new(key: 'VAR') }
- .to raise_error ArgumentError, 'missing keyword: value'
+ shared_examples 'creates variable' do
+ subject { described_class.new(key: variable_key, value: variable_value) }
+
+ it 'saves given value' do
+ expect(subject[:key]).to eq variable_key
+ expect(subject[:value]).to eq expected_value
+ end
+ end
+
+ shared_examples 'raises error for invalid type' do
+ it do
+ expect { described_class.new(key: variable_key, value: variable_value) }
+ .to raise_error ArgumentError, /`value` must be of type String, while it was:/
+ end
+ end
+
+ it_behaves_like 'creates variable'
+
+ context "when it's nil" do
+ let(:variable_value) { nil }
+ let(:expected_value) { nil }
+
+ it_behaves_like 'creates variable'
+ end
+
+ context "when it's an empty string" do
+ let(:variable_value) { '' }
+ let(:expected_value) { '' }
+
+ it_behaves_like 'creates variable'
+ end
+
+ context 'when provided value is not a string' do
+ [1, false, [], {}, Object.new].each do |val|
+ context "when it's #{val}" do
+ let(:variable_value) { val }
+
+ it_behaves_like 'raises error for invalid type'
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/ci/variables/collection_spec.rb b/spec/lib/gitlab/ci/variables/collection_spec.rb
index cb2f7718c9c..5c91816a586 100644
--- a/spec/lib/gitlab/ci/variables/collection_spec.rb
+++ b/spec/lib/gitlab/ci/variables/collection_spec.rb
@@ -29,7 +29,7 @@ describe Gitlab::Ci::Variables::Collection do
end
it 'appends an internal resource' do
- collection = described_class.new([{ key: 'TEST', value: 1 }])
+ collection = described_class.new([{ key: 'TEST', value: '1' }])
subject.append(collection.first)
@@ -74,15 +74,15 @@ describe Gitlab::Ci::Variables::Collection do
describe '#+' do
it 'makes it possible to combine with an array' do
- collection = described_class.new([{ key: 'TEST', value: 1 }])
+ collection = described_class.new([{ key: 'TEST', value: '1' }])
variables = [{ key: 'TEST', value: 'something' }]
expect((collection + variables).count).to eq 2
end
it 'makes it possible to combine with another collection' do
- collection = described_class.new([{ key: 'TEST', value: 1 }])
- other = described_class.new([{ key: 'TEST', value: 2 }])
+ collection = described_class.new([{ key: 'TEST', value: '1' }])
+ other = described_class.new([{ key: 'TEST', value: '2' }])
expect((collection + other).count).to eq 2
end
@@ -90,10 +90,10 @@ describe Gitlab::Ci::Variables::Collection do
describe '#to_runner_variables' do
it 'creates an array of hashes in a runner-compatible format' do
- collection = described_class.new([{ key: 'TEST', value: 1 }])
+ collection = described_class.new([{ key: 'TEST', value: '1' }])
expect(collection.to_runner_variables)
- .to eq [{ key: 'TEST', value: 1, public: true }]
+ .to eq [{ key: 'TEST', value: '1', public: true }]
end
end
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index ecb16daec96..e73cdc54a15 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -2,19 +2,9 @@ require 'spec_helper'
module Gitlab
module Ci
- describe YamlProcessor, :lib do
+ describe YamlProcessor do
subject { described_class.new(config) }
- describe 'our current .gitlab-ci.yml' do
- let(:config) { File.read("#{Rails.root}/.gitlab-ci.yml") }
-
- it 'is valid' do
- error_message = described_class.validation_message(config)
-
- expect(error_message).to be_nil
- end
- end
-
describe '#build_attributes' do
subject { described_class.new(config).build_attributes(:rspec) }
diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb
index 8d4862932b2..3a0688b7cbb 100644
--- a/spec/lib/gitlab/closing_issue_extractor_spec.rb
+++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb
@@ -15,7 +15,7 @@ describe Gitlab::ClosingIssueExtractor do
before do
project.add_developer(project.creator)
project.add_developer(project2.creator)
- project2.add_master(project.creator)
+ project2.add_maintainer(project.creator)
end
describe "#closed_by_message" do
@@ -298,7 +298,7 @@ describe Gitlab::ClosingIssueExtractor do
context 'with an external issue tracker reference' do
it 'extracts the referenced issue' do
jira_project = create(:jira_project, name: 'JIRA_EXT1')
- jira_project.add_master(jira_project.creator)
+ jira_project.add_maintainer(jira_project.creator)
jira_issue = ExternalIssue.new("#{jira_project.name}-1", project: jira_project)
closing_issue_extractor = described_class.new(jira_project, jira_project.creator)
message = "Resolve #{jira_issue.to_reference}"
diff --git a/spec/lib/gitlab/conflict/file_spec.rb b/spec/lib/gitlab/conflict/file_spec.rb
index 92792144429..5b343920429 100644
--- a/spec/lib/gitlab/conflict/file_spec.rb
+++ b/spec/lib/gitlab/conflict/file_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Gitlab::Conflict::File do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
- let(:rugged) { repository.rugged }
+ let(:rugged) { Gitlab::GitalyClient::StorageSettings.allow_disk_access { repository.rugged } }
let(:their_commit) { rugged.branches['conflict-start'].target }
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) }
diff --git a/spec/lib/gitlab/current_settings_spec.rb b/spec/lib/gitlab/current_settings_spec.rb
index 19028495f52..55490f37ac7 100644
--- a/spec/lib/gitlab/current_settings_spec.rb
+++ b/spec/lib/gitlab/current_settings_spec.rb
@@ -5,6 +5,13 @@ describe Gitlab::CurrentSettings do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
end
+ shared_context 'with settings in cache' do
+ before do
+ create(:application_setting)
+ described_class.current_application_settings # warm the cache
+ end
+ end
+
describe '#current_application_settings', :use_clean_rails_memory_store_caching do
it 'allows keys to be called directly' do
db_settings = create(:application_setting,
@@ -31,16 +38,29 @@ describe Gitlab::CurrentSettings do
end
context 'with DB unavailable' do
- before do
- # For some reason, `allow(described_class).to receive(:connect_to_db?).and_return(false)` causes issues
- # during the initialization phase of the test suite, so instead let's mock the internals of it
- allow(ActiveRecord::Base.connection).to receive(:active?).and_return(false)
+ context 'and settings in cache' do
+ include_context 'with settings in cache'
+
+ it 'fetches the settings from cache without issuing any query' do
+ expect(ActiveRecord::QueryRecorder.new { described_class.current_application_settings }.count).to eq(0)
+ end
end
- it 'returns an in-memory ApplicationSetting object' do
- expect(ApplicationSetting).not_to receive(:current)
+ context 'and no settings in cache' do
+ before do
+ # For some reason, `allow(described_class).to receive(:connect_to_db?).and_return(false)` causes issues
+ # during the initialization phase of the test suite, so instead let's mock the internals of it
+ allow(ActiveRecord::Base.connection).to receive(:active?).and_return(false)
+ expect(ApplicationSetting).not_to receive(:current)
+ end
- expect(described_class.current_application_settings).to be_a(Gitlab::FakeApplicationSettings)
+ it 'returns an in-memory ApplicationSetting object' do
+ expect(described_class.current_application_settings).to be_a(Gitlab::FakeApplicationSettings)
+ end
+
+ it 'does not issue any query' do
+ expect(ActiveRecord::QueryRecorder.new { described_class.current_application_settings }.count).to eq(0)
+ end
end
end
@@ -52,73 +72,86 @@ describe Gitlab::CurrentSettings do
ar_wrapped_defaults.slice(*::ApplicationSetting.defaults.keys)
end
- before do
- # For some reason, `allow(described_class).to receive(:connect_to_db?).and_return(true)` causes issues
- # during the initialization phase of the test suite, so instead let's mock the internals of it
- allow(ActiveRecord::Base.connection).to receive(:active?).and_return(true)
- allow(ActiveRecord::Base.connection).to receive(:cached_table_exists?).with('application_settings').and_return(true)
- end
+ context 'and settings in cache' do
+ include_context 'with settings in cache'
- it 'creates default ApplicationSettings if none are present' do
- settings = described_class.current_application_settings
-
- expect(settings).to be_a(ApplicationSetting)
- expect(settings).to be_persisted
- expect(settings).to have_attributes(settings_from_defaults)
+ it 'fetches the settings from cache' do
+ # For some reason, `allow(described_class).to receive(:connect_to_db?).and_return(true)` causes issues
+ # during the initialization phase of the test suite, so instead let's mock the internals of it
+ expect(ActiveRecord::Base.connection).not_to receive(:active?)
+ expect(ActiveRecord::Base.connection).not_to receive(:cached_table_exists?)
+ expect(ActiveRecord::Migrator).not_to receive(:needs_migration?)
+ expect(ActiveRecord::QueryRecorder.new { described_class.current_application_settings }.count).to eq(0)
+ end
end
- context 'with migrations pending' do
+ context 'and no settings in cache' do
before do
- expect(ActiveRecord::Migrator).to receive(:needs_migration?).and_return(true)
+ allow(ActiveRecord::Base.connection).to receive(:active?).and_return(true)
+ allow(ActiveRecord::Base.connection).to receive(:cached_table_exists?).with('application_settings').and_return(true)
end
- it 'returns an in-memory ApplicationSetting object' do
+ it 'creates default ApplicationSettings if none are present' do
settings = described_class.current_application_settings
- expect(settings).to be_a(Gitlab::FakeApplicationSettings)
- expect(settings.sign_in_enabled?).to eq(settings.sign_in_enabled)
- expect(settings.sign_up_enabled?).to eq(settings.sign_up_enabled)
+ expect(settings).to be_a(ApplicationSetting)
+ expect(settings).to be_persisted
+ expect(settings).to have_attributes(settings_from_defaults)
end
- it 'uses the existing database settings and falls back to defaults' do
- db_settings = create(:application_setting,
- home_page_url: 'http://mydomain.com',
- signup_enabled: false)
- settings = described_class.current_application_settings
- app_defaults = ApplicationSetting.last
-
- expect(settings).to be_a(Gitlab::FakeApplicationSettings)
- expect(settings.home_page_url).to eq(db_settings.home_page_url)
- expect(settings.signup_enabled?).to be_falsey
- expect(settings.signup_enabled).to be_falsey
-
- # Check that unspecified values use the defaults
- settings.reject! { |key, _| [:home_page_url, :signup_enabled].include? key }
- settings.each { |key, _| expect(settings[key]).to eq(app_defaults[key]) }
+ context 'with migrations pending' do
+ before do
+ expect(ActiveRecord::Migrator).to receive(:needs_migration?).and_return(true)
+ end
+
+ it 'returns an in-memory ApplicationSetting object' do
+ settings = described_class.current_application_settings
+
+ expect(settings).to be_a(Gitlab::FakeApplicationSettings)
+ expect(settings.sign_in_enabled?).to eq(settings.sign_in_enabled)
+ expect(settings.sign_up_enabled?).to eq(settings.sign_up_enabled)
+ end
+
+ it 'uses the existing database settings and falls back to defaults' do
+ db_settings = create(:application_setting,
+ home_page_url: 'http://mydomain.com',
+ signup_enabled: false)
+ settings = described_class.current_application_settings
+ app_defaults = ApplicationSetting.last
+
+ expect(settings).to be_a(Gitlab::FakeApplicationSettings)
+ expect(settings.home_page_url).to eq(db_settings.home_page_url)
+ expect(settings.signup_enabled?).to be_falsey
+ expect(settings.signup_enabled).to be_falsey
+
+ # Check that unspecified values use the defaults
+ settings.reject! { |key, _| [:home_page_url, :signup_enabled].include? key }
+ settings.each { |key, _| expect(settings[key]).to eq(app_defaults[key]) }
+ end
end
- end
- context 'when ApplicationSettings.current is present' do
- it 'returns the existing application settings' do
- expect(ApplicationSetting).to receive(:current).and_return(:current_settings)
+ context 'when ApplicationSettings.current is present' do
+ it 'returns the existing application settings' do
+ expect(ApplicationSetting).to receive(:current).and_return(:current_settings)
- expect(described_class.current_application_settings).to eq(:current_settings)
+ expect(described_class.current_application_settings).to eq(:current_settings)
+ end
end
- end
- context 'when the application_settings table does not exists' do
- it 'returns an in-memory ApplicationSetting object' do
- expect(ApplicationSetting).to receive(:create_from_defaults).and_raise(ActiveRecord::StatementInvalid)
+ context 'when the application_settings table does not exists' do
+ it 'returns an in-memory ApplicationSetting object' do
+ expect(ApplicationSetting).to receive(:create_from_defaults).and_raise(ActiveRecord::StatementInvalid)
- expect(described_class.current_application_settings).to be_a(Gitlab::FakeApplicationSettings)
+ expect(described_class.current_application_settings).to be_a(Gitlab::FakeApplicationSettings)
+ end
end
- end
- context 'when the application_settings table is not fully migrated' do
- it 'returns an in-memory ApplicationSetting object' do
- expect(ApplicationSetting).to receive(:create_from_defaults).and_raise(ActiveRecord::UnknownAttributeError)
+ context 'when the application_settings table is not fully migrated' do
+ it 'returns an in-memory ApplicationSetting object' do
+ expect(ApplicationSetting).to receive(:create_from_defaults).and_raise(ActiveRecord::UnknownAttributeError)
- expect(described_class.current_application_settings).to be_a(Gitlab::FakeApplicationSettings)
+ expect(described_class.current_application_settings).to be_a(Gitlab::FakeApplicationSettings)
+ end
end
end
end
diff --git a/spec/lib/gitlab/cycle_analytics/permissions_spec.rb b/spec/lib/gitlab/cycle_analytics/permissions_spec.rb
index 6de4bd3dc7c..f670c7f6c75 100644
--- a/spec/lib/gitlab/cycle_analytics/permissions_spec.rb
+++ b/spec/lib/gitlab/cycle_analytics/permissions_spec.rb
@@ -36,9 +36,9 @@ describe Gitlab::CycleAnalytics::Permissions do
end
end
- context 'user is master' do
+ context 'user is maintainer' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
it 'has permissions to issue stage' do
diff --git a/spec/lib/gitlab/cycle_analytics/usage_data_spec.rb b/spec/lib/gitlab/cycle_analytics/usage_data_spec.rb
index 56a316318cb..a785b17f682 100644
--- a/spec/lib/gitlab/cycle_analytics/usage_data_spec.rb
+++ b/spec/lib/gitlab/cycle_analytics/usage_data_spec.rb
@@ -3,7 +3,12 @@ require 'spec_helper'
describe Gitlab::CycleAnalytics::UsageData do
describe '#to_json' do
before do
- Timecop.freeze do
+ # Since git commits only have second precision, round up to the
+ # nearest second to ensure we have accurate median and standard
+ # deviation calculations.
+ current_time = Time.at(Time.now.to_i)
+
+ Timecop.freeze(current_time) do
user = create(:user, :admin)
projects = create_list(:project, 2, :repository)
@@ -37,13 +42,7 @@ describe Gitlab::CycleAnalytics::UsageData do
expected_values.each_pair do |op, value|
expect(stage_values).to have_key(op)
-
- if op == :missing
- expect(stage_values[op]).to eq(value)
- else
- # delta is used because of git timings that Timecop does not stub
- expect(stage_values[op].to_i).to be_within(5).of(value.to_i)
- end
+ expect(stage_values[op]).to eq(value)
end
end
end
@@ -58,8 +57,8 @@ describe Gitlab::CycleAnalytics::UsageData do
missing: 0
},
plan: {
- average: 2,
- sd: 2,
+ average: 1,
+ sd: 0,
missing: 0
},
code: {
diff --git a/spec/lib/gitlab/data_builder/note_spec.rb b/spec/lib/gitlab/data_builder/note_spec.rb
index 4f8412108ba..b236c1a9c49 100644
--- a/spec/lib/gitlab/data_builder/note_spec.rb
+++ b/spec/lib/gitlab/data_builder/note_spec.rb
@@ -52,7 +52,7 @@ describe Gitlab::DataBuilder::Note do
expect(data[:issue].except('updated_at'))
.to eq(issue.reload.hook_attrs.except('updated_at'))
expect(data[:issue]['updated_at'])
- .to be > issue.hook_attrs['updated_at']
+ .to be >= issue.hook_attrs['updated_at']
end
context 'with confidential issue' do
@@ -84,7 +84,7 @@ describe Gitlab::DataBuilder::Note do
expect(data[:merge_request].except('updated_at'))
.to eq(merge_request.reload.hook_attrs.except('updated_at'))
expect(data[:merge_request]['updated_at'])
- .to be > merge_request.hook_attrs['updated_at']
+ .to be >= merge_request.hook_attrs['updated_at']
end
include_examples 'project hook data'
@@ -107,7 +107,7 @@ describe Gitlab::DataBuilder::Note do
expect(data[:merge_request].except('updated_at'))
.to eq(merge_request.reload.hook_attrs.except('updated_at'))
expect(data[:merge_request]['updated_at'])
- .to be > merge_request.hook_attrs['updated_at']
+ .to be >= merge_request.hook_attrs['updated_at']
end
include_examples 'project hook data'
@@ -130,7 +130,7 @@ describe Gitlab::DataBuilder::Note do
expect(data[:snippet].except('updated_at'))
.to eq(snippet.reload.hook_attrs.except('updated_at'))
expect(data[:snippet]['updated_at'])
- .to be > snippet.hook_attrs['updated_at']
+ .to be >= snippet.hook_attrs['updated_at']
end
include_examples 'project hook data'
diff --git a/spec/lib/gitlab/database/count_spec.rb b/spec/lib/gitlab/database/count_spec.rb
index 9d9caaabe16..407d9470785 100644
--- a/spec/lib/gitlab/database/count_spec.rb
+++ b/spec/lib/gitlab/database/count_spec.rb
@@ -3,59 +3,68 @@ require 'spec_helper'
describe Gitlab::Database::Count do
before do
create_list(:project, 3)
+ create(:identity)
end
- describe '.execute_estimate_if_updated_recently', :postgresql do
- context 'when reltuples have not been updated' do
- before do
- expect(described_class).to receive(:reltuples_updated_recently?).and_return(false)
- end
+ let(:models) { [Project, Identity] }
- it 'returns nil' do
- expect(described_class.execute_estimate_if_updated_recently(Project)).to be nil
- end
- end
+ describe '.approximate_counts' do
+ context 'with MySQL' do
+ context 'when reltuples have not been updated' do
+ it 'counts all models the normal way' do
+ expect(Gitlab::Database).to receive(:postgresql?).and_return(false)
- context 'when reltuples have been updated' do
- before do
- ActiveRecord::Base.connection.execute('ANALYZE projects')
- end
+ expect(Project).to receive(:count).and_call_original
+ expect(Identity).to receive(:count).and_call_original
- it 'calls postgresql_estimate_query' do
- expect(described_class).to receive(:postgresql_estimate_query).with(Project).and_call_original
- expect(described_class.execute_estimate_if_updated_recently(Project)).to eq(3)
+ expect(described_class.approximate_counts(models)).to eq({ Project => 3, Identity => 1 })
+ end
end
end
- end
- describe '.approximate_count' do
- context 'when reltuples have not been updated' do
- it 'counts all projects the normal way' do
- allow(described_class).to receive(:reltuples_updated_recently?).and_return(false)
+ context 'with PostgreSQL', :postgresql do
+ describe 'when reltuples have not been updated' do
+ it 'counts all models the normal way' do
+ expect(described_class).to receive(:reltuples_from_recently_updated).with(%w(projects identities)).and_return({})
- expect(Project).to receive(:count).and_call_original
- expect(described_class.approximate_count(Project)).to eq(3)
+ expect(Project).to receive(:count).and_call_original
+ expect(Identity).to receive(:count).and_call_original
+ expect(described_class.approximate_counts(models)).to eq({ Project => 3, Identity => 1 })
+ end
end
- end
- context 'no permission' do
- it 'falls back to standard query' do
- allow(described_class).to receive(:reltuples_updated_recently?).and_raise(PG::InsufficientPrivilege)
+ describe 'no permission' do
+ it 'falls back to standard query' do
+ allow(described_class).to receive(:postgresql_estimate_query).and_raise(PG::InsufficientPrivilege)
- expect(Project).to receive(:count).and_call_original
- expect(described_class.approximate_count(Project)).to eq(3)
+ expect(Project).to receive(:count).and_call_original
+ expect(Identity).to receive(:count).and_call_original
+ expect(described_class.approximate_counts(models)).to eq({ Project => 3, Identity => 1 })
+ end
end
- end
- describe 'when reltuples have been updated', :postgresql do
- before do
- ActiveRecord::Base.connection.execute('ANALYZE projects')
+ describe 'when some reltuples have been updated' do
+ it 'counts projects in the fast way' do
+ expect(described_class).to receive(:reltuples_from_recently_updated).with(%w(projects identities)).and_return({ 'projects' => 3 })
+
+ expect(Project).not_to receive(:count).and_call_original
+ expect(Identity).to receive(:count).and_call_original
+ expect(described_class.approximate_counts(models)).to eq({ Project => 3, Identity => 1 })
+ end
end
- it 'counts all projects in the fast way' do
- expect(described_class).to receive(:postgresql_estimate_query).with(Project).and_call_original
+ describe 'when all reltuples have been updated' do
+ before do
+ ActiveRecord::Base.connection.execute('ANALYZE projects')
+ ActiveRecord::Base.connection.execute('ANALYZE identities')
+ end
+
+ it 'counts models with the standard way' do
+ expect(Project).not_to receive(:count)
+ expect(Identity).not_to receive(:count)
- expect(described_class.approximate_count(Project)).to eq(3)
+ expect(described_class.approximate_counts(models)).to eq({ Project => 3, Identity => 1 })
+ end
end
end
end
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index 280f799f2ab..eb7148ff108 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -1178,6 +1178,61 @@ describe Gitlab::Database::MigrationHelpers do
end
end
+ describe '#rename_column_using_background_migration' do
+ let!(:issue) { create(:issue, :closed, closed_at: Time.zone.now) }
+
+ it 'renames a column using a background migration' do
+ expect(model)
+ .to receive(:add_column)
+ .with(
+ 'issues',
+ :closed_at_timestamp,
+ :datetime_with_timezone,
+ limit: anything,
+ precision: anything,
+ scale: anything
+ )
+
+ expect(model)
+ .to receive(:install_rename_triggers)
+ .with('issues', :closed_at, :closed_at_timestamp)
+
+ expect(BackgroundMigrationWorker)
+ .to receive(:perform_in)
+ .ordered
+ .with(
+ 10.minutes,
+ 'CopyColumn',
+ ['issues', :closed_at, :closed_at_timestamp, issue.id, issue.id]
+ )
+
+ expect(BackgroundMigrationWorker)
+ .to receive(:perform_in)
+ .ordered
+ .with(
+ 1.hour + 10.minutes,
+ 'CleanupConcurrentRename',
+ ['issues', :closed_at, :closed_at_timestamp]
+ )
+
+ expect(Gitlab::BackgroundMigration)
+ .to receive(:steal)
+ .ordered
+ .with('CopyColumn')
+
+ expect(Gitlab::BackgroundMigration)
+ .to receive(:steal)
+ .ordered
+ .with('CleanupConcurrentRename')
+
+ model.rename_column_using_background_migration(
+ 'issues',
+ :closed_at,
+ :closed_at_timestamp
+ )
+ end
+ end
+
describe '#perform_background_migration_inline?' do
it 'returns true in a test environment' do
allow(Rails.env)
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 b411aaa19da..0a8c77b0ad9 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
@@ -281,7 +281,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, :
it "doesn't break when the namespace was renamed" do
subject.rename_namespace(namespace)
- namespace.update_attributes!(path: 'renamed-afterwards')
+ namespace.update!(path: 'renamed-afterwards')
expect { subject.revert_renames }.not_to raise_error
end
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 b4896d69077..d4d7a83921c 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
@@ -169,7 +169,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :de
it "doesn't break when the project was renamed" do
subject.rename_project(project)
- project.update_attributes!(path: 'renamed-afterwards')
+ project.update!(path: 'renamed-afterwards')
expect { subject.revert_renames }.not_to raise_error
end
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
index 8ac36ae8bab..782e4e45a91 100644
--- a/spec/lib/gitlab/database_spec.rb
+++ b/spec/lib/gitlab/database_spec.rb
@@ -314,8 +314,13 @@ describe Gitlab::Database do
describe '.cached_table_exists?' do
it 'only retrieves data once per table' do
- expect(ActiveRecord::Base.connection).to receive(:table_exists?).with(:projects).once.and_call_original
- expect(ActiveRecord::Base.connection).to receive(:table_exists?).with(:bogus_table_name).once.and_call_original
+ if Gitlab.rails5?
+ expect(ActiveRecord::Base.connection).to receive(:data_source_exists?).with(:projects).once.and_call_original
+ expect(ActiveRecord::Base.connection).to receive(:data_source_exists?).with(:bogus_table_name).once.and_call_original
+ else
+ expect(ActiveRecord::Base.connection).to receive(:table_exists?).with(:projects).once.and_call_original
+ expect(ActiveRecord::Base.connection).to receive(:table_exists?).with(:bogus_table_name).once.and_call_original
+ end
2.times do
expect(described_class.cached_table_exists?(:projects)).to be_truthy
@@ -352,6 +357,35 @@ describe Gitlab::Database do
end
end
+ describe '.db_read_only?' do
+ context 'when using PostgreSQL' do
+ before do
+ allow(ActiveRecord::Base.connection).to receive(:execute).and_call_original
+ expect(described_class).to receive(:postgresql?).and_return(true)
+ end
+
+ it 'detects a read only database' do
+ allow(ActiveRecord::Base.connection).to receive(:execute).with('SELECT pg_is_in_recovery()').and_return([{ "pg_is_in_recovery" => "t" }])
+
+ expect(described_class.db_read_only?).to be_truthy
+ end
+
+ it 'detects a read write database' do
+ allow(ActiveRecord::Base.connection).to receive(:execute).with('SELECT pg_is_in_recovery()').and_return([{ "pg_is_in_recovery" => "f" }])
+
+ expect(described_class.db_read_only?).to be_falsey
+ end
+ end
+
+ context 'when using MySQL' do
+ before do
+ expect(described_class).to receive(:postgresql?).and_return(false)
+ end
+
+ it { expect(described_class.db_read_only?).to be_falsey }
+ end
+ end
+
describe '#sanitize_timestamp' do
let(:max_timestamp) { Time.at((1 << 31) - 1) }
diff --git a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb
index f48ee8924e8..79287021981 100644
--- a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb
+++ b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb
@@ -20,6 +20,15 @@ describe Gitlab::Diff::FileCollection::MergeRequestDiff do
diff_files
end
+ it 'it uses a different cache key if diff line keys change' do
+ mr_diff = described_class.new(merge_request.merge_request_diff, diff_options: nil)
+ key = mr_diff.cache_key
+
+ stub_const('Gitlab::Diff::Line::SERIALIZE_KEYS', [:foo])
+
+ expect(mr_diff.cache_key).not_to eq(key)
+ end
+
shared_examples 'initializes a DiffCollection' do
it 'returns a valid instance of a DiffCollection' do
expect(diff_files).to be_a(Gitlab::Git::DiffCollection)
diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb
index 0c2e18c268a..ebeb05d6e02 100644
--- a/spec/lib/gitlab/diff/file_spec.rb
+++ b/spec/lib/gitlab/diff/file_spec.rb
@@ -26,6 +26,21 @@ describe Gitlab::Diff::File do
end
end
+ describe '#diff_lines_for_serializer' do
+ it 'includes bottom match line if not in the end' do
+ expect(diff_file.diff_lines_for_serializer.last.type).to eq('match')
+ end
+
+ context 'when deleted' do
+ let(:commit) { project.commit('d59c60028b053793cecfb4022de34602e1a9218e') }
+ let(:diff_file) { commit.diffs.diff_file_with_old_path('files/js/commit.js.coffee') }
+
+ it 'does not include bottom match line' do
+ expect(diff_file.diff_lines_for_serializer.last.type).not_to eq('match')
+ end
+ end
+ end
+
describe '#mode_changed?' do
it { expect(diff_file.mode_changed?).to be_falsey }
end
@@ -79,7 +94,9 @@ describe Gitlab::Diff::File do
let(:diffs) { commit.diffs }
before do
- info_dir_path = File.join(project.repository.path_to_repo, 'info')
+ info_dir_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ File.join(project.repository.path_to_repo, 'info')
+ end
FileUtils.mkdir(info_dir_path) unless File.exist?(info_dir_path)
File.write(File.join(info_dir_path, 'attributes'), "*.md -diff\n")
@@ -468,4 +485,71 @@ describe Gitlab::Diff::File do
end
end
end
+
+ describe '#diff_hunk' do
+ context 'when first line is a match' do
+ let(:raw_diff) do
+ <<~EOS
+ --- a/files/ruby/popen.rb
+ +++ b/files/ruby/popen.rb
+ @@ -6,12 +6,18 @@ module Popen
+
+ def popen(cmd, path=nil)
+ unless cmd.is_a?(Array)
+ - raise "System commands must be given as an array of strings"
+ + raise RuntimeError, "System commands must be given as an array of strings"
+ end
+ EOS
+ end
+
+ it 'returns raw diff up to given line index' do
+ allow(diff_file).to receive(:raw_diff) { raw_diff }
+ diff_line = instance_double(Gitlab::Diff::Line, index: 4)
+
+ diff_hunk = <<~EOS
+ @@ -6,12 +6,18 @@ module Popen
+
+ def popen(cmd, path=nil)
+ unless cmd.is_a?(Array)
+ - raise "System commands must be given as an array of strings"
+ + raise RuntimeError, "System commands must be given as an array of strings"
+ EOS
+
+ expect(diff_file.diff_hunk(diff_line)).to eq(diff_hunk.strip)
+ end
+ end
+
+ context 'when first line is not a match' do
+ let(:raw_diff) do
+ <<~EOS
+ @@ -1,4 +1,4 @@
+ -Copyright (c) 2011-2017 GitLab B.V.
+ +Copyright (c) 2011-2019 GitLab B.V.
+
+ With regard to the GitLab Software:
+
+ @@ -9,17 +9,21 @@ 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:
+ EOS
+ end
+
+ it 'returns raw diff up to given line index' do
+ allow(diff_file).to receive(:raw_diff) { raw_diff }
+ diff_line = instance_double(Gitlab::Diff::Line, index: 5)
+
+ diff_hunk = <<~EOS
+ -Copyright (c) 2011-2017 GitLab B.V.
+ +Copyright (c) 2011-2019 GitLab B.V.
+
+ With regard to the GitLab Software:
+
+ @@ -9,17 +9,21 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ EOS
+
+ expect(diff_file.diff_hunk(diff_line)).to eq(diff_hunk.strip)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/exclusive_lease_helpers_spec.rb b/spec/lib/gitlab/exclusive_lease_helpers_spec.rb
new file mode 100644
index 00000000000..2e3656b52fb
--- /dev/null
+++ b/spec/lib/gitlab/exclusive_lease_helpers_spec.rb
@@ -0,0 +1,76 @@
+require 'spec_helper'
+
+describe Gitlab::ExclusiveLeaseHelpers, :clean_gitlab_redis_shared_state do
+ include ::ExclusiveLeaseHelpers
+
+ let(:class_instance) { (Class.new { include ::Gitlab::ExclusiveLeaseHelpers }).new }
+ let(:unique_key) { SecureRandom.hex(10) }
+
+ describe '#in_lock' do
+ subject { class_instance.in_lock(unique_key, **options) { } }
+
+ let(:options) { {} }
+
+ context 'when the lease is not obtained yet' do
+ before do
+ stub_exclusive_lease(unique_key, 'uuid')
+ end
+
+ it 'calls the given block' do
+ expect { |b| class_instance.in_lock(unique_key, &b) }.to yield_control.once
+ end
+
+ it 'calls the given block continuously' do
+ expect { |b| class_instance.in_lock(unique_key, &b) }.to yield_control.once
+ expect { |b| class_instance.in_lock(unique_key, &b) }.to yield_control.once
+ expect { |b| class_instance.in_lock(unique_key, &b) }.to yield_control.once
+ end
+
+ it 'cancels the exclusive lease after the block' do
+ expect_to_cancel_exclusive_lease(unique_key, 'uuid')
+
+ subject
+ end
+ end
+
+ context 'when the lease is obtained already' do
+ let!(:lease) { stub_exclusive_lease_taken(unique_key) }
+
+ it 'retries to obtain a lease and raises an error' do
+ expect(lease).to receive(:try_obtain).exactly(11).times
+
+ expect { subject }.to raise_error('Failed to obtain a lock')
+ end
+
+ context 'when ttl is specified' do
+ let(:options) { { ttl: 10.minutes } }
+
+ it 'receives the specified argument' do
+ expect(Gitlab::ExclusiveLease).to receive(:new).with(unique_key, { timeout: 10.minutes } )
+
+ expect { subject }.to raise_error('Failed to obtain a lock')
+ end
+ end
+
+ context 'when retry count is specified' do
+ let(:options) { { retries: 3 } }
+
+ it 'retries for the specified times' do
+ expect(lease).to receive(:try_obtain).exactly(4).times
+
+ expect { subject }.to raise_error('Failed to obtain a lock')
+ end
+ end
+
+ context 'when sleep second is specified' do
+ let(:options) { { retries: 0, sleep_sec: 0.05.seconds } }
+
+ it 'receives the specified argument' do
+ expect(class_instance).to receive(:sleep).with(0.05.seconds).once
+
+ expect { subject }.to raise_error('Failed to obtain a lock')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/favicon_spec.rb b/spec/lib/gitlab/favicon_spec.rb
new file mode 100644
index 00000000000..68abcb3520a
--- /dev/null
+++ b/spec/lib/gitlab/favicon_spec.rb
@@ -0,0 +1,67 @@
+require 'rails_helper'
+
+RSpec.describe Gitlab::Favicon, :request_store do
+ describe '.main' do
+ it 'defaults to favicon.png' do
+ allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('production'))
+ expect(described_class.main).to match_asset_path '/assets/favicon.png'
+ end
+
+ it 'has blue favicon for development' do
+ allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('development'))
+ expect(described_class.main).to match_asset_path '/assets/favicon-blue.png'
+ end
+
+ it 'has yellow favicon for canary' do
+ stub_env('CANARY', 'true')
+ expect(described_class.main).to match_asset_path 'favicon-yellow.png'
+ end
+
+ it 'uses the custom favicon if a favicon appearance is present' do
+ create :appearance, favicon: fixture_file_upload('spec/fixtures/dk.png')
+ expect(described_class.main).to match %r{/uploads/-/system/appearance/favicon/\d+/dk.png}
+ end
+
+ context 'asset host' do
+ before do
+ allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('production'))
+ end
+
+ it 'returns a relative url when the asset host is not configured' do
+ expect(described_class.main).to match %r{^/assets/favicon-(?:\h+).png$}
+ end
+
+ it 'returns a full url when the asset host is configured' do
+ allow(ActionController::Base).to receive(:asset_host).and_return('http://assets.local')
+ expect(described_class.main).to match %r{^http://localhost/assets/favicon-(?:\h+).png$}
+ end
+ end
+ end
+
+ describe '.status_overlay' do
+ subject { described_class.status_overlay('favicon_status_created') }
+
+ it 'returns the overlay for the status' do
+ expect(subject).to match_asset_path '/assets/ci_favicons/favicon_status_created.png'
+ end
+ end
+
+ describe '.available_status_names' do
+ subject { described_class.available_status_names }
+
+ it 'returns the available status names' do
+ expect(subject).to eq %w(
+ favicon_status_canceled
+ favicon_status_created
+ favicon_status_failed
+ favicon_status_manual
+ favicon_status_not_found
+ favicon_status_pending
+ favicon_status_running
+ favicon_status_skipped
+ favicon_status_success
+ favicon_status_warning
+ )
+ end
+ end
+end
diff --git a/spec/lib/gitlab/file_finder_spec.rb b/spec/lib/gitlab/file_finder_spec.rb
index 07cb10e563e..b49c5817131 100644
--- a/spec/lib/gitlab/file_finder_spec.rb
+++ b/spec/lib/gitlab/file_finder_spec.rb
@@ -3,27 +3,29 @@ require 'spec_helper'
describe Gitlab::FileFinder do
describe '#find' do
let(:project) { create(:project, :public, :repository) }
- let(:finder) { described_class.new(project, project.default_branch) }
+ subject { described_class.new(project, project.default_branch) }
- it 'finds by name' do
- results = finder.find('files')
+ it_behaves_like 'file finder' do
+ let(:expected_file_by_name) { 'files/images/wm.svg' }
+ let(:expected_file_by_content) { 'CHANGELOG' }
+ end
+
+ it 'filters by name' do
+ results = subject.find('files filename:wm.svg')
- filename, blob = results.find { |_, blob| blob.filename == 'files/images/wm.svg' }
- expect(filename).to eq('files/images/wm.svg')
- expect(blob).to be_a(Gitlab::SearchResults::FoundBlob)
- expect(blob.ref).to eq(finder.ref)
- expect(blob.data).not_to be_empty
+ expect(results.count).to eq(1)
end
- it 'finds by content' do
- results = finder.find('files')
+ it 'filters by path' do
+ results = subject.find('white path:images')
+
+ expect(results.count).to eq(1)
+ end
- filename, blob = results.find { |_, blob| blob.filename == 'CHANGELOG' }
+ it 'filters by extension' do
+ results = subject.find('files extension:svg')
- expect(filename).to eq('CHANGELOG')
- expect(blob).to be_a(Gitlab::SearchResults::FoundBlob)
- expect(blob.ref).to eq(finder.ref)
- expect(blob.data).not_to be_empty
+ expect(results.count).to eq(1)
end
end
end
diff --git a/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb
index 13df8531b63..ef52a25f47e 100644
--- a/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb
+++ b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb
@@ -20,37 +20,55 @@ describe Gitlab::Gfm::UploadsRewriter do
"Text and #{image_uploader.markdown_link} and #{zip_uploader.markdown_link}"
end
- describe '#rewrite' do
- let!(:new_text) { rewriter.rewrite(new_project) }
+ shared_examples "files are accessible" do
+ describe '#rewrite' do
+ let!(:new_text) { rewriter.rewrite(new_project) }
- let(:old_files) { [image_uploader, zip_uploader].map(&:file) }
- let(:new_files) do
- described_class.new(new_text, new_project, user).files
- end
+ let(:old_files) { [image_uploader, zip_uploader] }
+ let(:new_files) do
+ described_class.new(new_text, new_project, user).files
+ end
- let(:old_paths) { old_files.map(&:path) }
- let(:new_paths) { new_files.map(&:path) }
+ let(:old_paths) { old_files.map(&:path) }
+ let(:new_paths) { new_files.map(&:path) }
- it 'rewrites content' do
- expect(new_text).not_to eq text
- expect(new_text.length).to eq text.length
- end
+ it 'rewrites content' do
+ expect(new_text).not_to eq text
+ expect(new_text.length).to eq text.length
+ end
- it 'copies files' do
- expect(new_files).to all(exist)
- expect(old_paths).not_to match_array new_paths
- expect(old_paths).to all(include(old_project.disk_path))
- expect(new_paths).to all(include(new_project.disk_path))
- end
+ it 'copies files' do
+ expect(new_files).to all(exist)
+ expect(old_paths).not_to match_array new_paths
+ expect(old_paths).to all(include(old_project.disk_path))
+ expect(new_paths).to all(include(new_project.disk_path))
+ end
- it 'does not remove old files' do
- expect(old_files).to all(exist)
+ it 'does not remove old files' do
+ expect(old_files).to all(exist)
+ end
+
+ it 'generates a new secret for each file' do
+ expect(new_paths).not_to include image_uploader.secret
+ expect(new_paths).not_to include zip_uploader.secret
+ end
end
+ end
- it 'generates a new secret for each file' do
- expect(new_paths).not_to include image_uploader.secret
- expect(new_paths).not_to include zip_uploader.secret
+ context "file are stored locally" do
+ include_examples "files are accessible"
+ end
+
+ context "files are stored remotely" do
+ before do
+ stub_uploads_object_storage(FileUploader)
+
+ old_files.each do |file|
+ file.migrate!(ObjectStorage::Store::REMOTE)
+ end
end
+
+ include_examples "files are accessible"
end
describe '#needs_rewrite?' do
diff --git a/spec/lib/gitlab/git/blame_spec.rb b/spec/lib/gitlab/git/blame_spec.rb
index 793228701cf..ba790b717ae 100644
--- a/spec/lib/gitlab/git/blame_spec.rb
+++ b/spec/lib/gitlab/git/blame_spec.rb
@@ -7,7 +7,7 @@ describe Gitlab::Git::Blame, seed_helper: true do
Gitlab::Git::Blame.new(repository, SeedRepo::Commit::ID, "CONTRIBUTING.md")
end
- shared_examples 'blaming a file' do
+ describe 'blaming a file' do
context "each count" do
it do
data = []
@@ -68,12 +68,4 @@ describe Gitlab::Git::Blame, seed_helper: true do
end
end
end
-
- context 'when Gitaly blame feature is enabled' do
- it_behaves_like 'blaming a file'
- end
-
- 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 e2547ed0311..034b89a46fa 100644
--- a/spec/lib/gitlab/git/blob_spec.rb
+++ b/spec/lib/gitlab/git/blob_spec.rb
@@ -15,13 +15,19 @@ describe Gitlab::Git::Blob, seed_helper: true do
end
end
- shared_examples 'finding blobs' do
+ describe '.find' do
context 'nil path' do
let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, nil) }
it { expect(blob).to eq(nil) }
end
+ context 'utf-8 branch' do
+ let(:blob) { Gitlab::Git::Blob.find(repository, 'Ääh-test-utf-8', "files/ruby/popen.rb")}
+
+ it { expect(blob.id).to eq(SeedRepo::RubyBlob::ID) }
+ end
+
context 'blank path' do
let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, '') }
@@ -119,16 +125,6 @@ describe Gitlab::Git::Blob, seed_helper: true do
end
end
- describe '.find' do
- context 'when project_raw_show Gitaly feature is enabled' do
- it_behaves_like 'finding blobs'
- end
-
- context 'when project_raw_show Gitaly feature is disabled', :skip_gitaly_mock do
- it_behaves_like 'finding blobs'
- end
- end
-
shared_examples 'finding blobs by ID' do
let(:raw_blob) { Gitlab::Git::Blob.raw(repository, SeedRepo::RubyBlob::ID) }
let(:bad_blob) { Gitlab::Git::Blob.raw(repository, SeedRepo::BigCommit::ID) }
@@ -143,7 +139,9 @@ describe Gitlab::Git::Blob, seed_helper: true do
it 'limits the size of a large file' do
blob_size = Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE + 1
buffer = Array.new(blob_size, 0)
- rugged_blob = Rugged::Blob.from_buffer(repository.rugged, buffer.join(''))
+ rugged_blob = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ Rugged::Blob.from_buffer(repository.rugged, buffer.join(''))
+ end
blob = Gitlab::Git::Blob.raw(repository, rugged_blob)
expect(blob.size).to eq(blob_size)
@@ -158,7 +156,9 @@ describe Gitlab::Git::Blob, seed_helper: true do
context 'when sha references a tree' do
it 'returns nil' do
- tree = repository.rugged.rev_parse('master^{tree}')
+ tree = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ repository.rugged.rev_parse('master^{tree}')
+ end
blob = Gitlab::Git::Blob.raw(repository, tree.oid)
@@ -178,77 +178,67 @@ describe Gitlab::Git::Blob, seed_helper: true do
end
describe '.batch' do
- shared_examples 'loading blobs in batch' do
- let(:blob_references) do
- [
- [SeedRepo::Commit::ID, "files/ruby/popen.rb"],
- [SeedRepo::Commit::ID, 'six']
- ]
- end
+ let(:blob_references) do
+ [
+ [SeedRepo::Commit::ID, "files/ruby/popen.rb"],
+ [SeedRepo::Commit::ID, 'six']
+ ]
+ end
- subject { described_class.batch(repository, blob_references) }
+ subject { described_class.batch(repository, blob_references) }
- it { expect(subject.size).to eq(blob_references.size) }
+ it { expect(subject.size).to eq(blob_references.size) }
- context 'first blob' do
- let(:blob) { subject[0] }
+ context 'first blob' do
+ let(:blob) { subject[0] }
- it { expect(blob.id).to eq(SeedRepo::RubyBlob::ID) }
- it { expect(blob.name).to eq(SeedRepo::RubyBlob::NAME) }
- it { expect(blob.path).to eq("files/ruby/popen.rb") }
- it { expect(blob.commit_id).to eq(SeedRepo::Commit::ID) }
- it { expect(blob.data[0..10]).to eq(SeedRepo::RubyBlob::CONTENT[0..10]) }
- it { expect(blob.size).to eq(669) }
- it { expect(blob.mode).to eq("100644") }
- end
+ it { expect(blob.id).to eq(SeedRepo::RubyBlob::ID) }
+ it { expect(blob.name).to eq(SeedRepo::RubyBlob::NAME) }
+ it { expect(blob.path).to eq("files/ruby/popen.rb") }
+ it { expect(blob.commit_id).to eq(SeedRepo::Commit::ID) }
+ it { expect(blob.data[0..10]).to eq(SeedRepo::RubyBlob::CONTENT[0..10]) }
+ it { expect(blob.size).to eq(669) }
+ it { expect(blob.mode).to eq("100644") }
+ end
- context 'second blob' do
- let(:blob) { subject[1] }
+ context 'second blob' do
+ let(:blob) { subject[1] }
- it { expect(blob.id).to eq('409f37c4f05865e4fb208c771485f211a22c4c2d') }
- it { expect(blob.data).to eq('') }
- it 'does not mark the blob as binary' do
- expect(blob).not_to be_binary
- end
+ it { expect(blob.id).to eq('409f37c4f05865e4fb208c771485f211a22c4c2d') }
+ it { expect(blob.data).to eq('') }
+ it 'does not mark the blob as binary' do
+ expect(blob).not_to be_binary
end
+ end
- context 'limiting' do
- subject { described_class.batch(repository, blob_references, blob_size_limit: blob_size_limit) }
+ context 'limiting' do
+ subject { described_class.batch(repository, blob_references, blob_size_limit: blob_size_limit) }
- context 'positive' do
- let(:blob_size_limit) { 10 }
+ context 'positive' do
+ let(:blob_size_limit) { 10 }
- it { expect(subject.first.data.size).to eq(10) }
- end
+ it { expect(subject.first.data.size).to eq(10) }
+ end
- context 'zero' do
- let(:blob_size_limit) { 0 }
+ context 'zero' do
+ let(:blob_size_limit) { 0 }
- it 'only loads the metadata' do
- expect(subject.first.size).not_to be(0)
- expect(subject.first.data).to eq('')
- end
+ it 'only loads the metadata' do
+ expect(subject.first.size).not_to be(0)
+ expect(subject.first.data).to eq('')
end
+ end
- context 'negative' do
- let(:blob_size_limit) { -1 }
+ context 'negative' do
+ let(:blob_size_limit) { -1 }
- it 'ignores MAX_DATA_DISPLAY_SIZE' do
- stub_const('Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE', 100)
+ it 'ignores MAX_DATA_DISPLAY_SIZE' do
+ stub_const('Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE', 100)
- expect(subject.first.data.size).to eq(669)
- end
+ expect(subject.first.data.size).to eq(669)
end
end
end
-
- context 'when Gitaly list_blobs_by_sha_path feature is enabled' do
- it_behaves_like 'loading blobs in batch'
- end
-
- context 'when Gitaly list_blobs_by_sha_path feature is disabled', :disable_gitaly do
- it_behaves_like 'loading blobs in batch'
- end
end
describe '.batch_metadata' do
@@ -272,7 +262,11 @@ describe Gitlab::Git::Blob, seed_helper: true do
end
describe '.batch_lfs_pointers' do
- let(:tree_object) { repository.rugged.rev_parse('master^{tree}') }
+ let(:tree_object) do
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ repository.rugged.rev_parse('master^{tree}')
+ end
+ end
let(:non_lfs_blob) do
Gitlab::Git::Blob.find(
@@ -290,58 +284,48 @@ describe Gitlab::Git::Blob, seed_helper: true do
)
end
- shared_examples 'fetching batch of LFS pointers' do
- it 'returns a list of Gitlab::Git::Blob' do
- blobs = described_class.batch_lfs_pointers(repository, [lfs_blob.id])
+ 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) )
- expect(blobs).to be_an(Array)
- end
-
- it 'accepts blob IDs as a lazy enumerator' do
- blobs = described_class.batch_lfs_pointers(repository, [lfs_blob.id].lazy)
-
- expect(blobs.count).to eq(1)
- expect(blobs).to all( be_a(Gitlab::Git::Blob) )
- end
+ expect(blobs.count).to eq(1)
+ expect(blobs).to all( be_a(Gitlab::Git::Blob) )
+ expect(blobs).to be_an(Array)
+ end
- it 'handles empty list of IDs gracefully' do
- blobs_1 = described_class.batch_lfs_pointers(repository, [].lazy)
- blobs_2 = described_class.batch_lfs_pointers(repository, [])
+ it 'accepts blob IDs as a lazy enumerator' do
+ blobs = described_class.batch_lfs_pointers(repository, [lfs_blob.id].lazy)
- expect(blobs_1).to eq([])
- expect(blobs_2).to eq([])
- end
+ 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])
+ it 'handles empty list of IDs gracefully' do
+ blobs_1 = described_class.batch_lfs_pointers(repository, [].lazy)
+ blobs_2 = described_class.batch_lfs_pointers(repository, [])
- 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_1).to eq([])
+ expect(blobs_2).to eq([])
+ end
- expect(blobs).to eq([])
- end
+ it 'silently ignores tree objects' do
+ blobs = described_class.batch_lfs_pointers(repository, [tree_object.oid])
- it 'avoids loading large blobs into memory' do
- # This line could call `lookup` on `repository`, so do here before mocking.
- non_lfs_blob_id = non_lfs_blob.id
+ expect(blobs).to eq([])
+ end
- expect(repository).not_to receive(:lookup)
+ it 'silently ignores non lfs objects' do
+ blobs = described_class.batch_lfs_pointers(repository, [non_lfs_blob.id])
- described_class.batch_lfs_pointers(repository, [non_lfs_blob_id])
- end
+ expect(blobs).to eq([])
end
- context 'when Gitaly batch_lfs_pointers is enabled' do
- it_behaves_like 'fetching batch of LFS pointers'
- end
+ it 'avoids loading large blobs into memory' do
+ # This line could call `lookup` on `repository`, so do here before mocking.
+ non_lfs_blob_id = non_lfs_blob.id
+
+ expect(repository).not_to receive(:lookup)
- context 'when Gitaly batch_lfs_pointers is disabled', :disable_gitaly do
- it_behaves_like 'fetching batch of LFS pointers'
+ described_class.batch_lfs_pointers(repository, [non_lfs_blob_id])
end
end
@@ -528,8 +512,8 @@ describe Gitlab::Git::Blob, seed_helper: true do
subject { blob.load_all_data!(repository) }
it 'loads missing data' do
- expect(Gitlab::GitalyClient).to receive(:migrate)
- .with(:git_blob_load_all_data).and_return(full_data)
+ expect(repository.gitaly_blob_client).to receive(:get_blob)
+ .and_return(double(:response, data: full_data))
subject
@@ -540,8 +524,7 @@ describe Gitlab::Git::Blob, seed_helper: true do
let(:blob) { Gitlab::Git::Blob.new(name: 'test', size: 4, data: full_data) }
it "doesn't perform any loading" do
- expect(Gitlab::GitalyClient).not_to receive(:migrate)
- .with(:git_blob_load_all_data)
+ expect(repository.gitaly_blob_client).not_to receive(:get_blob)
subject
diff --git a/spec/lib/gitlab/git/branch_spec.rb b/spec/lib/gitlab/git/branch_spec.rb
index a19155ed5b0..ec1a684cfbc 100644
--- a/spec/lib/gitlab/git/branch_spec.rb
+++ b/spec/lib/gitlab/git/branch_spec.rb
@@ -69,7 +69,9 @@ describe Gitlab::Git::Branch, seed_helper: true do
Gitlab::Git.committer_hash(email: user.email, name: user.name)
end
let(:params) do
- parents = [repository.rugged.head.target]
+ parents = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ [repository.rugged.head.target]
+ end
tree = parents.first.tree
{
diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb
index 08c6d1e55e9..ee74c2769eb 100644
--- a/spec/lib/gitlab/git/commit_spec.rb
+++ b/spec/lib/gitlab/git/commit_spec.rb
@@ -4,12 +4,15 @@ describe Gitlab::Git::Commit, seed_helper: true do
let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
let(:commit) { described_class.find(repository, SeedRepo::Commit::ID) }
let(:rugged_commit) do
- repository.rugged.lookup(SeedRepo::Commit::ID)
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ repository.rugged.lookup(SeedRepo::Commit::ID)
+ end
end
-
describe "Commit info" do
before do
- repo = Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '').rugged
+ repo = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '').rugged
+ end
@committer = {
email: 'mike@smith.com',
@@ -58,7 +61,9 @@ describe Gitlab::Git::Commit, seed_helper: true do
after do
# Erase the new commit so other tests get the original repo
- repo = Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '').rugged
+ repo = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '').rugged
+ end
repo.references.update("refs/heads/master", SeedRepo::LastCommit::ID)
end
end
@@ -115,7 +120,9 @@ describe Gitlab::Git::Commit, seed_helper: true do
describe '.find' do
it "should return first head commit if without params" do
expect(described_class.last(repository).id).to eq(
- repository.rugged.head.target.oid
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ repository.rugged.head.target.oid
+ end
)
end
@@ -302,7 +309,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
it { is_expected.not_to include(SeedRepo::FirstCommit::ID) }
end
- shared_examples '.shas_with_signatures' do
+ describe '.shas_with_signatures' do
let(:signed_shas) { %w[5937ac0a7beb003549fc5fd26fc247adbce4a52e 570e7b2abdd848b95f2f578043fc23bd6f6fd24d] }
let(:unsigned_shas) { %w[19e2e9b4ef76b422ce1154af39a91323ccc57434 c642fe9b8b9f28f9225d7ea953fe14e74748d53b] }
let(:first_signed_shas) { %w[5937ac0a7beb003549fc5fd26fc247adbce4a52e c642fe9b8b9f28f9225d7ea953fe14e74748d53b] }
@@ -323,93 +330,65 @@ describe Gitlab::Git::Commit, seed_helper: true do
end
end
- describe '.shas_with_signatures with gitaly on' do
- it_should_behave_like '.shas_with_signatures'
- end
-
- describe '.shas_with_signatures with gitaly disabled', :disable_gitaly do
- it_should_behave_like '.shas_with_signatures'
- end
-
describe '.find_all' do
- shared_examples 'finding all commits' do
- it 'should return a return a collection of commits' do
- commits = described_class.find_all(repository)
-
- expect(commits).to all( be_a_kind_of(described_class) )
- end
-
- context 'max_count' do
- subject do
- commits = described_class.find_all(
- repository,
- max_count: 50
- )
+ it 'should return a return a collection of commits' do
+ commits = described_class.find_all(repository)
- commits.map(&:id)
- end
+ expect(commits).to all( be_a_kind_of(described_class) )
+ end
- it 'has 34 elements' do
- expect(subject.size).to eq(34)
- end
+ context 'max_count' do
+ subject do
+ commits = described_class.find_all(
+ repository,
+ max_count: 50
+ )
- it 'includes the expected commits' do
- expect(subject).to include(
- SeedRepo::Commit::ID,
- SeedRepo::Commit::PARENT_ID,
- SeedRepo::FirstCommit::ID
- )
- end
+ commits.map(&:id)
end
- context 'ref + max_count + skip' do
- subject do
- commits = described_class.find_all(
- repository,
- ref: 'master',
- max_count: 50,
- skip: 1
- )
-
- commits.map(&:id)
- end
-
- it 'has 24 elements' do
- expect(subject.size).to eq(24)
- end
-
- it 'includes the expected commits' do
- expect(subject).to include(SeedRepo::Commit::ID, SeedRepo::FirstCommit::ID)
- expect(subject).not_to include(SeedRepo::LastCommit::ID)
- end
+ it 'has 34 elements' do
+ expect(subject.size).to eq(34)
end
- end
- context 'when Gitaly find_all_commits feature is enabled' do
- it_behaves_like 'finding all commits'
+ it 'includes the expected commits' do
+ expect(subject).to include(
+ SeedRepo::Commit::ID,
+ SeedRepo::Commit::PARENT_ID,
+ SeedRepo::FirstCommit::ID
+ )
+ end
end
- 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
- it "allows ordering topologically (no parents shown before their children)" do
- expect_any_instance_of(Rugged::Walker).to receive(:sorting).with(Rugged::SORT_TOPO)
+ context 'ref + max_count + skip' do
+ subject do
+ commits = described_class.find_all(
+ repository,
+ ref: 'master',
+ max_count: 50,
+ skip: 1
+ )
- described_class.find_all(repository, order: :topo)
- end
+ commits.map(&:id)
+ end
- it "allows ordering by date" do
- expect_any_instance_of(Rugged::Walker).to receive(:sorting).with(Rugged::SORT_DATE | Rugged::SORT_TOPO)
+ it 'has 24 elements' do
+ expect(subject.size).to eq(24)
+ end
- described_class.find_all(repository, order: :date)
- end
+ it 'includes the expected commits' do
+ expect(subject).to include(SeedRepo::Commit::ID, SeedRepo::FirstCommit::ID)
+ expect(subject).not_to include(SeedRepo::LastCommit::ID)
+ end
+ end
+ end
- it "applies no sorting by default" do
- expect_any_instance_of(Rugged::Walker).to receive(:sorting).with(Rugged::SORT_NONE)
+ describe '#batch_by_oid' do
+ context 'when oids is empty' do
+ it 'makes no Gitaly request' do
+ expect(Gitlab::GitalyClient).not_to receive(:call)
- described_class.find_all(repository)
- end
+ described_class.batch_by_oid(repository, [])
end
end
end
@@ -481,7 +460,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
end
describe '.extract_signature_lazily' do
- shared_examples 'loading signatures in batch once' do
+ describe 'loading signatures in batch once' do
it 'fetches signatures in batch once' do
commit_ids = %w[0b4bc9a49b562e85de7cc9e834518ea6828729b9 4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6]
signatures = commit_ids.map do |commit_id|
@@ -499,27 +478,13 @@ describe Gitlab::Git::Commit, seed_helper: true do
subject { described_class.extract_signature_lazily(repository, commit_id).itself }
- context 'with Gitaly extract_commit_signature_in_batch feature enabled' do
- it_behaves_like 'extracting commit signature'
- it_behaves_like 'loading signatures in batch once'
- end
-
- context 'with Gitaly extract_commit_signature_in_batch feature disabled', :disable_gitaly do
- it_behaves_like 'extracting commit signature'
- it_behaves_like 'loading signatures in batch once'
- end
+ it_behaves_like 'extracting commit signature'
end
describe '.extract_signature' do
subject { described_class.extract_signature(repository, commit_id) }
- context 'with gitaly' do
- it_behaves_like 'extracting commit signature'
- end
-
- context 'without gitaly', :disable_gitaly do
- it_behaves_like 'extracting commit signature'
- end
+ it_behaves_like 'extracting commit signature'
end
end
@@ -603,8 +568,8 @@ describe Gitlab::Git::Commit, seed_helper: true do
let(:commit) { described_class.find(repository, 'master') }
subject { commit.ref_names(repository) }
- it 'has 1 element' do
- expect(subject.size).to eq(1)
+ it 'has 2 element' do
+ expect(subject.size).to eq(2)
end
it { is_expected.to include("master") }
it { is_expected.not_to include("feature") }
diff --git a/spec/lib/gitlab/git/committer_with_hooks_spec.rb b/spec/lib/gitlab/git/committer_with_hooks_spec.rb
index 267056b96e6..2100690f873 100644
--- a/spec/lib/gitlab/git/committer_with_hooks_spec.rb
+++ b/spec/lib/gitlab/git/committer_with_hooks_spec.rb
@@ -1,154 +1,156 @@
require 'spec_helper'
describe Gitlab::Git::CommitterWithHooks, seed_helper: true do
- shared_examples 'calling wiki hooks' do
- let(:project) { create(:project) }
- let(:user) { project.owner }
- let(:project_wiki) { ProjectWiki.new(project, user) }
- let(:wiki) { project_wiki.wiki }
- let(:options) do
- {
- id: user.id,
- username: user.username,
- name: user.name,
- email: user.email,
- message: 'commit message'
- }
- end
-
- subject { described_class.new(wiki, options) }
+ # TODO https://gitlab.com/gitlab-org/gitaly/issues/1234
+ skip 'needs to be moved to gitaly-ruby test suite' do
+ shared_examples 'calling wiki hooks' do
+ let(:project) { create(:project) }
+ let(:user) { project.owner }
+ let(:project_wiki) { ProjectWiki.new(project, user) }
+ let(:wiki) { project_wiki.wiki }
+ let(:options) do
+ {
+ id: user.id,
+ username: user.username,
+ name: user.name,
+ email: user.email,
+ message: 'commit message'
+ }
+ end
- before do
- project_wiki.create_page('home', 'test content')
- end
+ subject { described_class.new(wiki, options) }
- shared_examples 'failing pre-receive hook' do
before do
- expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('pre-receive').and_return([false, ''])
- expect_any_instance_of(Gitlab::Git::HooksService).not_to receive(:run_hook).with('update')
- expect_any_instance_of(Gitlab::Git::HooksService).not_to receive(:run_hook).with('post-receive')
+ project_wiki.create_page('home', 'test content')
end
- it 'raises exception' do
- expect { subject.commit }.to raise_error(Gitlab::Git::Wiki::OperationError)
- end
+ shared_examples 'failing pre-receive hook' do
+ before do
+ expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('pre-receive').and_return([false, ''])
+ expect_any_instance_of(Gitlab::Git::HooksService).not_to receive(:run_hook).with('update')
+ expect_any_instance_of(Gitlab::Git::HooksService).not_to receive(:run_hook).with('post-receive')
+ end
- it 'does not create a new commit inside the repository' do
- current_rev = find_current_rev
+ it 'raises exception' do
+ expect { subject.commit }.to raise_error(Gitlab::Git::Wiki::OperationError)
+ end
- expect { subject.commit }.to raise_error(Gitlab::Git::Wiki::OperationError)
+ it 'does not create a new commit inside the repository' do
+ current_rev = find_current_rev
- expect(current_rev).to eq find_current_rev
- end
- end
+ expect { subject.commit }.to raise_error(Gitlab::Git::Wiki::OperationError)
- shared_examples 'failing update hook' do
- before do
- expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('pre-receive').and_return([true, ''])
- expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('update').and_return([false, ''])
- expect_any_instance_of(Gitlab::Git::HooksService).not_to receive(:run_hook).with('post-receive')
+ expect(current_rev).to eq find_current_rev
+ end
end
- it 'raises exception' do
- expect { subject.commit }.to raise_error(Gitlab::Git::Wiki::OperationError)
- end
+ shared_examples 'failing update hook' do
+ before do
+ expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('pre-receive').and_return([true, ''])
+ expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('update').and_return([false, ''])
+ expect_any_instance_of(Gitlab::Git::HooksService).not_to receive(:run_hook).with('post-receive')
+ end
- it 'does not create a new commit inside the repository' do
- current_rev = find_current_rev
+ it 'raises exception' do
+ expect { subject.commit }.to raise_error(Gitlab::Git::Wiki::OperationError)
+ end
- expect { subject.commit }.to raise_error(Gitlab::Git::Wiki::OperationError)
+ it 'does not create a new commit inside the repository' do
+ current_rev = find_current_rev
- expect(current_rev).to eq find_current_rev
- end
- end
+ expect { subject.commit }.to raise_error(Gitlab::Git::Wiki::OperationError)
- shared_examples 'failing post-receive hook' do
- before do
- expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('pre-receive').and_return([true, ''])
- expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('update').and_return([true, ''])
- expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('post-receive').and_return([false, ''])
+ expect(current_rev).to eq find_current_rev
+ end
end
- it 'does not raise exception' do
- expect { subject.commit }.not_to raise_error
- end
+ shared_examples 'failing post-receive hook' do
+ before do
+ expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('pre-receive').and_return([true, ''])
+ expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('update').and_return([true, ''])
+ expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('post-receive').and_return([false, ''])
+ end
+
+ it 'does not raise exception' do
+ expect { subject.commit }.not_to raise_error
+ end
- it 'creates the commit' do
- current_rev = find_current_rev
+ it 'creates the commit' do
+ current_rev = find_current_rev
- subject.commit
+ subject.commit
- expect(current_rev).not_to eq find_current_rev
+ expect(current_rev).not_to eq find_current_rev
+ end
end
- end
- shared_examples 'when hooks call succceeds' do
- let(:hook) { double(:hook) }
+ shared_examples 'when hooks call succceeds' do
+ let(:hook) { double(:hook) }
- it 'calls the three hooks' do
- expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook)
- expect(hook).to receive(:trigger).exactly(3).times.and_return([true, nil])
+ it 'calls the three hooks' do
+ expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook)
+ expect(hook).to receive(:trigger).exactly(3).times.and_return([true, nil])
- subject.commit
- end
+ subject.commit
+ end
- it 'creates the commit' do
- current_rev = find_current_rev
+ it 'creates the commit' do
+ current_rev = find_current_rev
- subject.commit
+ subject.commit
- expect(current_rev).not_to eq find_current_rev
+ expect(current_rev).not_to eq find_current_rev
+ end
end
- end
- context 'when creating a page' do
- before do
- project_wiki.create_page('index', 'test content')
+ context 'when creating a page' do
+ before do
+ project_wiki.create_page('index', 'test content')
+ end
+
+ it_behaves_like 'failing pre-receive hook'
+ it_behaves_like 'failing update hook'
+ it_behaves_like 'failing post-receive hook'
+ it_behaves_like 'when hooks call succceeds'
end
- it_behaves_like 'failing pre-receive hook'
- it_behaves_like 'failing update hook'
- it_behaves_like 'failing post-receive hook'
- it_behaves_like 'when hooks call succceeds'
- end
+ context 'when updating a page' do
+ before do
+ project_wiki.update_page(find_page('home'), content: 'some other content', format: :markdown)
+ end
- context 'when updating a page' do
- before do
- project_wiki.update_page(find_page('home'), content: 'some other content', format: :markdown)
+ it_behaves_like 'failing pre-receive hook'
+ it_behaves_like 'failing update hook'
+ it_behaves_like 'failing post-receive hook'
+ it_behaves_like 'when hooks call succceeds'
end
- it_behaves_like 'failing pre-receive hook'
- it_behaves_like 'failing update hook'
- it_behaves_like 'failing post-receive hook'
- it_behaves_like 'when hooks call succceeds'
- end
+ context 'when deleting a page' do
+ before do
+ project_wiki.delete_page(find_page('home'))
+ end
- context 'when deleting a page' do
- before do
- project_wiki.delete_page(find_page('home'))
+ it_behaves_like 'failing pre-receive hook'
+ it_behaves_like 'failing update hook'
+ it_behaves_like 'failing post-receive hook'
+ it_behaves_like 'when hooks call succceeds'
end
- it_behaves_like 'failing pre-receive hook'
- it_behaves_like 'failing update hook'
- it_behaves_like 'failing post-receive hook'
- it_behaves_like 'when hooks call succceeds'
- end
+ def find_current_rev
+ wiki.gollum_wiki.repo.commits.first&.sha
+ end
- def find_current_rev
- wiki.gollum_wiki.repo.commits.first&.sha
+ def find_page(name)
+ wiki.page(title: name)
+ end
end
- def find_page(name)
- wiki.page(title: name)
+ context 'when Gitaly is enabled' do
+ it_behaves_like 'calling wiki hooks'
end
- end
-
- # TODO: Uncomment once Gitaly updates the ruby vendor code
- # context 'when Gitaly is enabled' do
- # it_behaves_like 'calling wiki hooks'
- # end
- context 'when Gitaly is disabled', :skip_gitaly_mock do
- it_behaves_like 'calling wiki hooks'
+ context 'when Gitaly is disabled', :disable_gitaly do
+ it_behaves_like 'calling wiki hooks'
+ end
end
end
diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb
index 4a7b06003fc..3bb0b5be15b 100644
--- a/spec/lib/gitlab/git/diff_spec.rb
+++ b/spec/lib/gitlab/git/diff_spec.rb
@@ -27,8 +27,10 @@ EOT
too_large: false
}
- @rugged_diff = repository.rugged.diff("5937ac0a7beb003549fc5fd26fc247adbce4a52e^", "5937ac0a7beb003549fc5fd26fc247adbce4a52e", paths:
- [".gitmodules"]).patches.first
+ @rugged_diff = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ repository.rugged.diff("5937ac0a7beb003549fc5fd26fc247adbce4a52e^", "5937ac0a7beb003549fc5fd26fc247adbce4a52e", paths:
+ [".gitmodules"]).patches.first
+ end
end
describe '.new' do
diff --git a/spec/lib/gitlab/git/gitlab_projects_spec.rb b/spec/lib/gitlab/git/gitlab_projects_spec.rb
index 8b715d717c1..f5d8503c30c 100644
--- a/spec/lib/gitlab/git/gitlab_projects_spec.rb
+++ b/spec/lib/gitlab/git/gitlab_projects_spec.rb
@@ -5,6 +5,13 @@ describe Gitlab::Git::GitlabProjects do
TestEnv.clean_test_path
end
+ around do |example|
+ # TODO move this spec to gitaly-ruby. GitlabProjects is not used in gitlab-ce
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ example.run
+ end
+ end
+
let(:project) { create(:project, :repository) }
if $VERBOSE
@@ -190,36 +197,30 @@ describe Gitlab::Git::GitlabProjects do
end
end
- context 'when Gitaly import_repository feature is enabled' do
- it_behaves_like 'importing repository'
- end
+ describe 'logging' do
+ it 'imports a repo' do
+ message = "Importing project from <#{import_url}> to <#{tmp_repo_path}>."
+ expect(logger).to receive(:info).with(message)
- context 'when Gitaly import_repository feature is disabled', :disable_gitaly do
- describe 'logging' do
- it 'imports a repo' do
- message = "Importing project from <#{import_url}> to <#{tmp_repo_path}>."
- expect(logger).to receive(:info).with(message)
-
- subject
- end
+ subject
end
+ end
- context 'timeout' do
- it 'does not import a repo' do
- stub_spawn_timeout(cmd, timeout, nil)
+ context 'timeout' do
+ it 'does not import a repo' do
+ stub_spawn_timeout(cmd, timeout, nil)
- message = "Importing project from <#{import_url}> to <#{tmp_repo_path}> failed."
- expect(logger).to receive(:error).with(message)
+ message = "Importing project from <#{import_url}> to <#{tmp_repo_path}> failed."
+ expect(logger).to receive(:error).with(message)
- is_expected.to be_falsy
+ is_expected.to be_falsy
- expect(gl_projects.output).to eq("Timed out\n")
- expect(File.exist?(File.join(tmp_repo_path, 'HEAD'))).to be_falsy
- end
+ expect(gl_projects.output).to eq("Timed out\n")
+ expect(File.exist?(File.join(tmp_repo_path, 'HEAD'))).to be_falsy
end
-
- it_behaves_like 'importing repository'
end
+
+ it_behaves_like 'importing repository'
end
describe '#fork_repository' do
@@ -232,9 +233,6 @@ describe Gitlab::Git::GitlabProjects do
before do
FileUtils.mkdir_p(dest_repos_path)
-
- # Undo spec_helper stub that deletes hooks
- allow_any_instance_of(described_class).to receive(:fork_repository).and_call_original
end
after do
@@ -258,51 +256,45 @@ describe Gitlab::Git::GitlabProjects do
end
end
- context 'when Gitaly fork_repository feature is enabled' do
- it_behaves_like 'forking a repository'
- end
-
- context 'when Gitaly fork_repository feature is disabled', :disable_gitaly do
- it_behaves_like 'forking a repository'
+ it_behaves_like 'forking a repository'
- # We seem to be stuck to having only one working Gitaly storage in tests, changing
- # that is not very straight-forward so I'm leaving this test here for now till
- # https://gitlab.com/gitlab-org/gitlab-ce/issues/41393 is fixed.
- context 'different storages' do
- let(:dest_repos) { 'alternative' }
- let(:dest_repos_path) { File.join(File.dirname(tmp_repos_path), dest_repos) }
+ # We seem to be stuck to having only one working Gitaly storage in tests, changing
+ # that is not very straight-forward so I'm leaving this test here for now till
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/41393 is fixed.
+ context 'different storages' do
+ let(:dest_repos) { 'alternative' }
+ let(:dest_repos_path) { File.join(File.dirname(tmp_repos_path), dest_repos) }
- before do
- stub_storage_settings(dest_repos => { 'path' => dest_repos_path })
- end
+ before do
+ stub_storage_settings(dest_repos => { 'path' => dest_repos_path })
+ end
- it 'forks the repo' do
- is_expected.to be_truthy
+ it 'forks the repo' do
+ is_expected.to be_truthy
- expect(File.exist?(dest_repo)).to be_truthy
- expect(File.exist?(File.join(dest_repo, 'hooks', 'pre-receive'))).to be_truthy
- expect(File.exist?(File.join(dest_repo, 'hooks', 'post-receive'))).to be_truthy
- end
+ expect(File.exist?(dest_repo)).to be_truthy
+ expect(File.exist?(File.join(dest_repo, 'hooks', 'pre-receive'))).to be_truthy
+ expect(File.exist?(File.join(dest_repo, 'hooks', 'post-receive'))).to be_truthy
end
+ end
- describe 'log messages' do
- describe 'successful fork' do
- it do
- message = "Forking repository from <#{tmp_repo_path}> to <#{dest_repo}>."
- expect(logger).to receive(:info).with(message)
+ describe 'log messages' do
+ describe 'successful fork' do
+ it do
+ message = "Forking repository from <#{tmp_repo_path}> to <#{dest_repo}>."
+ expect(logger).to receive(:info).with(message)
- subject
- end
+ subject
end
+ end
- describe 'failed fork due existing destination' do
- it do
- FileUtils.mkdir_p(dest_repo)
- message = "fork-repository failed: destination repository <#{dest_repo}> already exists."
- expect(logger).to receive(:error).with(message)
+ describe 'failed fork due existing destination' do
+ it do
+ FileUtils.mkdir_p(dest_repo)
+ message = "fork-repository failed: destination repository <#{dest_repo}> already exists."
+ expect(logger).to receive(:error).with(message)
- subject
- end
+ subject
end
end
end
diff --git a/spec/lib/gitlab/git/hook_spec.rb b/spec/lib/gitlab/git/hook_spec.rb
index 2fe1f5603ce..a45c8510b15 100644
--- a/spec/lib/gitlab/git/hook_spec.rb
+++ b/spec/lib/gitlab/git/hook_spec.rb
@@ -8,25 +8,39 @@ describe Gitlab::Git::Hook do
allow_any_instance_of(described_class).to receive(:trigger).and_call_original
end
+ around do |example|
+ # TODO move hook tests to gitaly-ruby. Hook will disappear from gitlab-ce
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ example.run
+ end
+ end
+
describe "#trigger" do
- let(:project) { create(:project, :repository) }
+ set(:project) { create(:project, :repository) }
let(:repository) { project.repository.raw_repository }
let(:repo_path) { repository.path }
+ let(:hooks_dir) { File.join(repo_path, 'hooks') }
let(:user) { create(:user) }
let(:gl_id) { Gitlab::GlId.gl_id(user) }
let(:gl_username) { user.username }
def create_hook(name)
- FileUtils.mkdir_p(File.join(repo_path, 'hooks'))
- File.open(File.join(repo_path, 'hooks', name), 'w', 0755) do |f|
- f.write('exit 0')
+ FileUtils.mkdir_p(hooks_dir)
+ hook_path = File.join(hooks_dir, name)
+ File.open(hook_path, 'w', 0755) do |f|
+ f.write(<<~HOOK)
+ #!/bin/sh
+ exit 0
+ HOOK
end
end
def create_failing_hook(name)
- FileUtils.mkdir_p(File.join(repo_path, 'hooks'))
- File.open(File.join(repo_path, 'hooks', name), 'w', 0755) do |f|
- f.write(<<-HOOK)
+ FileUtils.mkdir_p(hooks_dir)
+ hook_path = File.join(hooks_dir, name)
+ File.open(hook_path, 'w', 0755) do |f|
+ f.write(<<~HOOK)
+ #!/bin/sh
echo 'regular message from the hook'
echo 'error message from the hook' 1>&2
echo 'error message from the hook line 2' 1>&2
@@ -38,7 +52,7 @@ describe Gitlab::Git::Hook do
['pre-receive', 'post-receive', 'update'].each do |hook_name|
context "when triggering a #{hook_name} hook" do
context "when the hook is successful" do
- let(:hook_path) { File.join(repo_path, 'hooks', hook_name) }
+ let(:hook_path) { File.join(hooks_dir, hook_name) }
let(:gl_repository) { Gitlab::GlRepository.gl_repository(project, false) }
let(:env) do
{
@@ -76,7 +90,7 @@ describe Gitlab::Git::Hook do
status, errors = hook.trigger(gl_id, gl_username, blank, blank, ref)
expect(status).to be false
- expect(errors).to eq("error message from the hook<br>error message from the hook line 2<br>")
+ expect(errors).to eq("error message from the hook\nerror message from the hook line 2\n")
end
end
end
diff --git a/spec/lib/gitlab/git/hooks_service_spec.rb b/spec/lib/gitlab/git/hooks_service_spec.rb
index 3ed3feb4c74..9337aa39e13 100644
--- a/spec/lib/gitlab/git/hooks_service_spec.rb
+++ b/spec/lib/gitlab/git/hooks_service_spec.rb
@@ -26,24 +26,24 @@ describe Gitlab::Git::HooksService, seed_helper: true do
context 'when pre-receive hook failed' do
it 'does not call post-receive hook' do
- expect(service).to receive(:run_hook).with('pre-receive').and_return([false, ''])
+ expect(service).to receive(:run_hook).with('pre-receive').and_return([false, 'hello world'])
expect(service).not_to receive(:run_hook).with('post-receive')
expect do
service.execute(user, repository, blankrev, newrev, ref)
- end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
+ end.to raise_error(Gitlab::Git::PreReceiveError, 'hello world')
end
end
context 'when update hook failed' do
it 'does not call post-receive hook' do
expect(service).to receive(:run_hook).with('pre-receive').and_return([true, nil])
- expect(service).to receive(:run_hook).with('update').and_return([false, ''])
+ expect(service).to receive(:run_hook).with('update').and_return([false, 'hello world'])
expect(service).not_to receive(:run_hook).with('post-receive')
expect do
service.execute(user, repository, blankrev, newrev, ref)
- end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
+ end.to raise_error(Gitlab::Git::PreReceiveError, 'hello world')
end
end
end
diff --git a/spec/lib/gitlab/git/index_spec.rb b/spec/lib/gitlab/git/index_spec.rb
index 73fbc6a6afa..16e6bd35449 100644
--- a/spec/lib/gitlab/git/index_spec.rb
+++ b/spec/lib/gitlab/git/index_spec.rb
@@ -8,6 +8,13 @@ describe Gitlab::Git::Index, seed_helper: true do
index.read_tree(repository.lookup('master').tree)
end
+ around do |example|
+ # TODO move these specs to gitaly-ruby. The Index class will disappear from gitlab-ce
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ example.run
+ end
+ end
+
describe '#create' do
let(:options) do
{
diff --git a/spec/lib/gitlab/git/lfs_changes_spec.rb b/spec/lib/gitlab/git/lfs_changes_spec.rb
index d0dd8c6303f..c5e7ab959b2 100644
--- a/spec/lib/gitlab/git/lfs_changes_spec.rb
+++ b/spec/lib/gitlab/git/lfs_changes_spec.rb
@@ -1,50 +1,19 @@
require 'spec_helper'
describe Gitlab::Git::LfsChanges do
- let(:project) { create(:project, :repository) }
+ set(:project) { create(:project, :repository) }
let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' }
let(:blob_object_id) { '0c304a93cb8430108629bbbcaa27db3343299bc0' }
subject { described_class.new(project.repository, newrev) }
describe '#new_pointers' do
- shared_examples 'new pointers' do
- it 'filters new objects to find lfs pointers' do
- expect(subject.new_pointers(not_in: []).first.id).to eq(blob_object_id)
- end
-
- it 'limits new_objects using object_limit' do
- expect(subject.new_pointers(object_limit: 1)).to eq([])
- end
- end
-
- context 'with gitaly enabled' do
- it_behaves_like 'new pointers'
+ it 'filters new objects to find lfs pointers' do
+ expect(subject.new_pointers(not_in: []).first.id).to eq(blob_object_id)
end
- context 'with gitaly disabled', :skip_gitaly_mock do
- it_behaves_like 'new pointers'
-
- 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
- end
- end
-
- describe '#all_pointers', :skip_gitaly_mock 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
+ it 'limits new_objects using object_limit' do
+ expect(subject.new_pointers(object_limit: 1)).to eq([])
end
end
end
diff --git a/spec/lib/gitlab/git/pre_receive_error_spec.rb b/spec/lib/gitlab/git/pre_receive_error_spec.rb
new file mode 100644
index 00000000000..1b8be62dec6
--- /dev/null
+++ b/spec/lib/gitlab/git/pre_receive_error_spec.rb
@@ -0,0 +1,9 @@
+require 'spec_helper'
+
+describe Gitlab::Git::PreReceiveError do
+ it 'makes its message HTML-friendly' do
+ ex = described_class.new("hello\nworld\n")
+
+ expect(ex.message).to eq('hello<br>world<br>')
+ end
+end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index af6a486ab20..4f8e6f29255 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -77,17 +77,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe '#root_ref' do
- context 'with gitaly disabled' do
- before do
- allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(false)
- end
-
- it 'calls #discover_default_branch' do
- expect(repository).to receive(:discover_default_branch)
- repository.root_ref
- end
- end
-
it 'returns UTF-8' do
expect(repository.root_ref).to be_utf8
end
@@ -114,7 +103,9 @@ describe Gitlab::Git::Repository, seed_helper: true do
it 'raises a no repository exception when there is no repo' do
broken_repo = described_class.new('default', 'a/path.git', '')
- expect { broken_repo.rugged }.to raise_error(Gitlab::Git::Repository::NoRepository)
+ expect do
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access { broken_repo.rugged }
+ end.to raise_error(Gitlab::Git::Repository::NoRepository)
end
describe 'alternates keyword argument' do
@@ -124,9 +115,9 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
it "is passed an empty array" do
- expect(Rugged::Repository).to receive(:new).with(repository.path, alternates: [])
+ expect(Rugged::Repository).to receive(:new).with(repository_path, alternates: [])
- repository.rugged
+ repository_rugged
end
end
@@ -142,48 +133,15 @@ describe Gitlab::Git::Repository, seed_helper: true do
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)
+ 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
+ repository_rugged
end
end
end
end
- describe "#discover_default_branch" do
- let(:master) { 'master' }
- let(:feature) { 'feature' }
- let(:feature2) { 'feature2' }
-
- it "returns 'master' when master exists" do
- expect(repository).to receive(:branch_names).at_least(:once).and_return([feature, master])
- expect(repository.discover_default_branch).to eq('master')
- end
-
- it "returns non-master when master exists but default branch is set to something else" do
- File.write(File.join(repository.path, 'HEAD'), 'ref: refs/heads/feature')
- expect(repository).to receive(:branch_names).at_least(:once).and_return([feature, master])
- expect(repository.discover_default_branch).to eq('feature')
- File.write(File.join(repository.path, 'HEAD'), 'ref: refs/heads/master')
- end
-
- it "returns a non-master branch when only one exists" do
- expect(repository).to receive(:branch_names).at_least(:once).and_return([feature])
- expect(repository.discover_default_branch).to eq('feature')
- end
-
- it "returns a non-master branch when more than one exists and master does not" do
- expect(repository).to receive(:branch_names).at_least(:once).and_return([feature, feature2])
- expect(repository.discover_default_branch).to eq('feature')
- end
-
- it "returns nil when no branch exists" do
- expect(repository).to receive(:branch_names).at_least(:once).and_return([])
- expect(repository.discover_default_branch).to be_nil
- end
- end
-
describe '#branch_names' do
subject { repository.branch_names }
@@ -247,7 +205,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
let(:expected_path) { File.join(storage_path, cache_key, expected_filename) }
let(:expected_prefix) { "gitlab-git-test-#{ref}-#{SeedRepo::LastCommit::ID}" }
- subject(:metadata) { repository.archive_metadata(ref, storage_path, format, append_sha: append_sha) }
+ subject(:metadata) { repository.archive_metadata(ref, storage_path, 'gitlab-git-test', format, append_sha: append_sha) }
it 'sets CommitId to the commit SHA' do
expect(metadata['CommitId']).to eq(SeedRepo::LastCommit::ID)
@@ -364,6 +322,13 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
context '#submodules' do
+ around do |example|
+ # TODO #submodules will be removed, has been migrated to gitaly
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ example.run
+ end
+ end
+
let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
context 'where repo has submodules' do
@@ -460,7 +425,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe '#has_local_branches?' do
- shared_examples 'check for local branches' do
+ context 'check for local branches' do
it { expect(repository.has_local_branches?).to eq(true) }
context 'mutable' do
@@ -474,8 +439,8 @@ describe Gitlab::Git::Repository, seed_helper: true do
# Sanity check
expect(repository.has_local_branches?).to eq(true)
- FileUtils.rm_rf(File.join(repository.path, 'packed-refs'))
- heads_dir = File.join(repository.path, 'refs/heads')
+ FileUtils.rm_rf(File.join(repository_path, 'packed-refs'))
+ heads_dir = File.join(repository_path, 'refs/heads')
FileUtils.rm_rf(heads_dir)
FileUtils.mkdir_p(heads_dir)
@@ -494,14 +459,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
end
-
- context 'with gitaly' do
- it_behaves_like 'check for local branches'
- end
-
- context 'without gitaly', :skip_gitaly_mock do
- it_behaves_like 'check for local branches'
- end
end
describe "#delete_branch" do
@@ -516,10 +473,10 @@ describe Gitlab::Git::Repository, seed_helper: true do
branch_name = "to-be-deleted-soon"
repository.create_branch(branch_name)
- expect(repository.rugged.branches[branch_name]).not_to be_nil
+ expect(repository_rugged.branches[branch_name]).not_to be_nil
repository.delete_branch(branch_name)
- expect(repository.rugged.branches[branch_name]).to be_nil
+ expect(repository_rugged.branches[branch_name]).to be_nil
end
context "when branch does not exist" do
@@ -577,6 +534,12 @@ describe Gitlab::Git::Repository, seed_helper: true do
shared_examples 'deleting refs' do
let(:repo) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') }
+ def repo_rugged
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ repo.rugged
+ end
+ end
+
after do
ensure_seeds
end
@@ -584,7 +547,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
it 'deletes the ref' do
repo.delete_refs('refs/heads/feature')
- expect(repo.rugged.references['refs/heads/feature']).to be_nil
+ expect(repo_rugged.references['refs/heads/feature']).to be_nil
end
it 'deletes all refs' do
@@ -592,7 +555,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
repo.delete_refs(*refs)
refs.each do |ref|
- expect(repo.rugged.references[ref]).to be_nil
+ expect(repo_rugged.references[ref]).to be_nil
end
end
@@ -615,7 +578,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe '#branch_names_contains_sha' do
- let(:head_id) { repository.rugged.head.target.oid }
+ let(:head_id) { repository_rugged.head.target.oid }
let(:new_branch) { head_id }
let(:utf8_branch) { 'branch-é' }
@@ -699,7 +662,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
it 'fetches a repository as a mirror remote' do
subject
- expect(refs(new_repository.path)).to eq(refs(repository.path))
+ expect(refs(new_repository_path)).to eq(refs(repository_path))
end
context 'with keep-around refs' do
@@ -708,15 +671,15 @@ describe Gitlab::Git::Repository, seed_helper: true do
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)
+ 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)
+ expect(refs(new_repository_path)).to include(keep_around_ref)
+ expect(refs(new_repository_path)).to include(tmp_ref)
end
end
end
@@ -728,6 +691,12 @@ describe Gitlab::Git::Repository, seed_helper: true do
context 'with gitaly enabled', :skip_gitaly_mock do
it_behaves_like 'repository mirror fecthing'
end
+
+ def new_repository_path
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ new_repository.path
+ end
+ end
end
describe '#remote_tags' do
@@ -739,10 +708,17 @@ describe Gitlab::Git::Repository, seed_helper: true do
Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
end
+ around do |example|
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ example.run
+ end
+ end
+
subject { repository.remote_tags(remote_name) }
before do
- repository.add_remote(remote_name, remote_repository.path)
+ remote_repository_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access { remote_repository.path }
+ repository.add_remote(remote_name, remote_repository_path)
remote_repository.add_tag(tag_name, user: user, target: target_commit_id)
end
@@ -975,8 +951,10 @@ describe Gitlab::Git::Repository, seed_helper: true do
let(:options) { { ref: 'master', path: ['PROCESS.md', 'README.md'] } }
def commit_files(commit)
- commit.rugged_diff_from_parent.deltas.flat_map do |delta|
- [delta.old_file[:path], delta.new_file[:path]].uniq.compact
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ commit.rugged_diff_from_parent.deltas.flat_map do |delta|
+ [delta.old_file[:path], delta.new_file[:path]].uniq.compact
+ end
end
end
@@ -1018,39 +996,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
- describe "#rugged_commits_between" do
- context 'two SHAs' do
- let(:first_sha) { 'b0e52af38d7ea43cf41d8a6f2471351ac036d6c9' }
- let(:second_sha) { '0e50ec4d3c7ce42ab74dda1d422cb2cbffe1e326' }
-
- it 'returns the number of commits between' do
- expect(repository.rugged_commits_between(first_sha, second_sha).count).to eq(3)
- end
- end
-
- context 'SHA and master branch' do
- let(:sha) { 'b0e52af38d7ea43cf41d8a6f2471351ac036d6c9' }
- let(:branch) { 'master' }
-
- it 'returns the number of commits between a sha and a branch' do
- expect(repository.rugged_commits_between(sha, branch).count).to eq(5)
- end
-
- it 'returns the number of commits between a branch and a sha' do
- expect(repository.rugged_commits_between(branch, sha).count).to eq(0) # sha is before branch
- end
- end
-
- context 'two branches' do
- let(:first_branch) { 'feature' }
- let(:second_branch) { 'master' }
-
- it 'returns the number of commits between' do
- expect(repository.rugged_commits_between(first_branch, second_branch).count).to eq(17)
- end
- end
- end
-
describe '#count_commits_between' do
subject { repository.count_commits_between('feature', 'master') }
@@ -1058,50 +1003,40 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe '#raw_changes_between' do
- shared_examples 'raw changes' do
- let(:old_rev) { }
- let(:new_rev) { }
- let(:changes) { repository.raw_changes_between(old_rev, new_rev) }
+ let(:old_rev) { }
+ let(:new_rev) { }
+ let(:changes) { repository.raw_changes_between(old_rev, new_rev) }
- context 'initial commit' do
- let(:old_rev) { Gitlab::Git::BLANK_SHA }
- let(:new_rev) { '1a0b36b3cdad1d2ee32457c102a8c0b7056fa863' }
+ context 'initial commit' do
+ let(:old_rev) { Gitlab::Git::BLANK_SHA }
+ let(:new_rev) { '1a0b36b3cdad1d2ee32457c102a8c0b7056fa863' }
- it 'returns the changes' do
- expect(changes).to be_present
- expect(changes.size).to eq(3)
- end
+ it 'returns the changes' do
+ expect(changes).to be_present
+ expect(changes.size).to eq(3)
end
+ end
- context 'with an invalid rev' do
- let(:old_rev) { 'foo' }
- let(:new_rev) { 'bar' }
+ context 'with an invalid rev' do
+ let(:old_rev) { 'foo' }
+ let(:new_rev) { 'bar' }
- it 'returns an error' do
- expect { changes }.to raise_error(Gitlab::Git::Repository::GitError)
- end
- end
-
- context 'with valid revs' do
- let(:old_rev) { 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6' }
- let(:new_rev) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' }
-
- it 'returns the changes' do
- expect(changes.size).to eq(9)
- expect(changes.first.operation).to eq(:modified)
- expect(changes.first.new_path).to eq('.gitmodules')
- expect(changes.last.operation).to eq(:added)
- expect(changes.last.new_path).to eq('files/lfs/picture-invalid.png')
- end
+ it 'returns an error' do
+ expect { changes }.to raise_error(Gitlab::Git::Repository::GitError)
end
end
- context 'when gitaly is enabled' do
- it_behaves_like 'raw changes'
- end
+ context 'with valid revs' do
+ let(:old_rev) { 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6' }
+ let(:new_rev) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' }
- context 'when gitaly is disabled', :disable_gitaly do
- it_behaves_like 'raw changes'
+ it 'returns the changes' do
+ expect(changes.size).to eq(9)
+ expect(changes.first.operation).to eq(:modified)
+ expect(changes.first.new_path).to eq('.gitmodules')
+ expect(changes.last.operation).to eq(:added)
+ expect(changes.last.new_path).to eq('files/lfs/picture-invalid.png')
+ end
end
end
@@ -1129,7 +1064,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe '#count_commits' do
- shared_examples 'extended commit counting' do
+ describe 'extended commit counting' do
context 'with after timestamp' do
it 'returns the number of commits after timestamp' do
options = { ref: 'master', after: Time.iso8601('2013-03-03T20:15:01+00:00') }
@@ -1214,14 +1149,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
end
-
- context 'when Gitaly count_commits feature is enabled' do
- it_behaves_like 'extended commit counting'
- end
-
- context 'when Gitaly count_commits feature is disabled', :disable_gitaly do
- it_behaves_like 'extended commit counting'
- end
end
describe '#autocrlf' do
@@ -1351,30 +1278,12 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
- # With Gitaly enabled, Gitaly just doesn't return deleted branches.
- context 'with deleted branch with Gitaly disabled' do
- before do
- allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(false)
- end
-
- it 'returns no results' do
- ref = double()
- allow(ref).to receive(:name) { 'bad-branch' }
- allow(ref).to receive(:target) { raise Rugged::ReferenceError }
- branches = double()
- allow(branches).to receive(:each) { [ref].each }
- allow(repository.rugged).to receive(:branches) { branches }
-
- expect(subject).to be_empty
- end
- end
-
it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RefService, :branches
end
describe '#branch_count' do
it 'returns the number of branches' do
- expect(repository.branch_count).to eq(10)
+ expect(repository.branch_count).to eq(11)
end
context 'with local and remote branches' do
@@ -1493,94 +1402,84 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe "#copy_gitattributes" do
- shared_examples 'applying git attributes' do
- let(:attributes_path) { File.join(SEED_STORAGE_PATH, TEST_REPO_PATH, 'info/attributes') }
+ let(:attributes_path) { File.join(SEED_STORAGE_PATH, TEST_REPO_PATH, 'info/attributes') }
- after do
- FileUtils.rm_rf(attributes_path) if Dir.exist?(attributes_path)
- end
-
- it "raises an error with invalid ref" do
- expect { repository.copy_gitattributes("invalid") }.to raise_error(Gitlab::Git::Repository::InvalidRef)
- end
-
- context 'when forcing encoding issues' do
- let(:branch_name) { "ʕ•ᴥ•ʔ" }
+ after do
+ FileUtils.rm_rf(attributes_path) if Dir.exist?(attributes_path)
+ end
- before do
- repository.create_branch(branch_name, "master")
- end
+ it "raises an error with invalid ref" do
+ expect { repository.copy_gitattributes("invalid") }.to raise_error(Gitlab::Git::Repository::InvalidRef)
+ end
- after do
- repository.rm_branch(branch_name, user: build(:admin))
- end
+ context 'when forcing encoding issues' do
+ let(:branch_name) { "ʕ•ᴥ•ʔ" }
- it "doesn't raise with a valid unicode ref" do
- expect { repository.copy_gitattributes(branch_name) }.not_to raise_error
+ before do
+ repository.create_branch(branch_name, "master")
+ end
- repository
- end
+ after do
+ repository.rm_branch(branch_name, user: build(:admin))
end
- context "with no .gitattrbutes" do
- before do
- repository.copy_gitattributes("master")
- end
+ it "doesn't raise with a valid unicode ref" do
+ expect { repository.copy_gitattributes(branch_name) }.not_to raise_error
- it "does not have an info/attributes" do
- expect(File.exist?(attributes_path)).to be_falsey
- end
+ repository
end
+ end
- context "with .gitattrbutes" do
- before do
- repository.copy_gitattributes("gitattributes")
- end
+ context "with no .gitattrbutes" do
+ before do
+ repository.copy_gitattributes("master")
+ end
- it "has an info/attributes" do
- expect(File.exist?(attributes_path)).to be_truthy
- end
+ it "does not have an info/attributes" do
+ expect(File.exist?(attributes_path)).to be_falsey
+ end
+ end
- it "has the same content in info/attributes as .gitattributes" do
- contents = File.open(attributes_path, "rb") { |f| f.read }
- expect(contents).to eq("*.md binary\n")
- end
+ context "with .gitattrbutes" do
+ before do
+ repository.copy_gitattributes("gitattributes")
end
- context "with updated .gitattrbutes" do
- before do
- repository.copy_gitattributes("gitattributes")
- repository.copy_gitattributes("gitattributes-updated")
- end
+ it "has an info/attributes" do
+ expect(File.exist?(attributes_path)).to be_truthy
+ end
- it "has an info/attributes" do
- expect(File.exist?(attributes_path)).to be_truthy
- end
+ it "has the same content in info/attributes as .gitattributes" do
+ contents = File.open(attributes_path, "rb") { |f| f.read }
+ expect(contents).to eq("*.md binary\n")
+ end
+ end
- it "has the updated content in info/attributes" do
- contents = File.read(attributes_path)
- expect(contents).to eq("*.txt binary\n")
- end
+ context "with updated .gitattrbutes" do
+ before do
+ repository.copy_gitattributes("gitattributes")
+ repository.copy_gitattributes("gitattributes-updated")
end
- context "with no .gitattrbutes in HEAD but with previous info/attributes" do
- before do
- repository.copy_gitattributes("gitattributes")
- repository.copy_gitattributes("master")
- end
+ it "has an info/attributes" do
+ expect(File.exist?(attributes_path)).to be_truthy
+ end
- it "does not have an info/attributes" do
- expect(File.exist?(attributes_path)).to be_falsey
- end
+ it "has the updated content in info/attributes" do
+ contents = File.read(attributes_path)
+ expect(contents).to eq("*.txt binary\n")
end
end
- context 'when gitaly is enabled' do
- it_behaves_like 'applying git attributes'
- end
+ context "with no .gitattrbutes in HEAD but with previous info/attributes" do
+ before do
+ repository.copy_gitattributes("gitattributes")
+ repository.copy_gitattributes("master")
+ end
- context 'when gitaly is disabled', :disable_gitaly do
- it_behaves_like 'applying git attributes'
+ it "does not have an info/attributes" do
+ expect(File.exist?(attributes_path)).to be_falsey
+ end
end
end
@@ -1661,6 +1560,13 @@ describe Gitlab::Git::Repository, seed_helper: true do
describe '#batch_existence' do
let(:refs) { ['deadbeef', SeedRepo::RubyBlob::ID, '909e6157199'] }
+ around do |example|
+ # TODO #batch_existence isn't used anywhere, can we remove it?
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ example.run
+ end
+ end
+
it 'returns existing refs back' do
result = repository.batch_existence(refs)
@@ -1714,70 +1620,52 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe '#languages' do
- shared_examples 'languages' do
- it 'returns exactly the expected results' do
- languages = repository.languages('4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6')
- expected_languages = [
- { value: 66.63, label: "Ruby", color: "#701516", highlight: "#701516" },
- { value: 22.96, label: "JavaScript", color: "#f1e05a", highlight: "#f1e05a" },
- { value: 7.9, label: "HTML", color: "#e34c26", highlight: "#e34c26" },
- { value: 2.51, label: "CoffeeScript", color: "#244776", highlight: "#244776" }
- ]
+ it 'returns exactly the expected results' do
+ languages = repository.languages('4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6')
+ expected_languages = [
+ { value: 66.63, label: "Ruby", color: "#701516", highlight: "#701516" },
+ { value: 22.96, label: "JavaScript", color: "#f1e05a", highlight: "#f1e05a" },
+ { value: 7.9, label: "HTML", color: "#e34c26", highlight: "#e34c26" },
+ { value: 2.51, label: "CoffeeScript", color: "#244776", highlight: "#244776" }
+ ]
- expect(languages.size).to eq(expected_languages.size)
+ expect(languages.size).to eq(expected_languages.size)
- expected_languages.size.times do |i|
- a = expected_languages[i]
- b = languages[i]
+ expected_languages.size.times do |i|
+ a = expected_languages[i]
+ b = languages[i]
- expect(a.keys.sort).to eq(b.keys.sort)
- expect(a[:value]).to be_within(0.1).of(b[:value])
+ expect(a.keys.sort).to eq(b.keys.sort)
+ expect(a[:value]).to be_within(0.1).of(b[:value])
- non_float_keys = a.keys - [:value]
- expect(a.values_at(*non_float_keys)).to eq(b.values_at(*non_float_keys))
- end
- end
-
- it "uses the repository's HEAD when no ref is passed" do
- lang = repository.languages.first
-
- expect(lang[:label]).to eq('Ruby')
+ non_float_keys = a.keys - [:value]
+ expect(a.values_at(*non_float_keys)).to eq(b.values_at(*non_float_keys))
end
end
- it_behaves_like 'languages'
+ it "uses the repository's HEAD when no ref is passed" do
+ lang = repository.languages.first
- context 'with rugged', :skip_gitaly_mock do
- it_behaves_like 'languages'
+ expect(lang[:label]).to eq('Ruby')
end
end
describe '#license_short_name' do
- shared_examples 'acquiring the Licensee license key' do
- subject { repository.license_short_name }
-
- context 'when no license file can be found' do
- let(:project) { create(:project, :repository) }
- let(:repository) { project.repository.raw_repository }
+ subject { repository.license_short_name }
- before do
- project.repository.delete_file(project.owner, 'LICENSE', message: 'remove license', branch_name: 'master')
- end
-
- it { is_expected.to be_nil }
- end
+ context 'when no license file can be found' do
+ let(:project) { create(:project, :repository) }
+ let(:repository) { project.repository.raw_repository }
- context 'when an mit license is found' do
- it { is_expected.to eq('mit') }
+ before do
+ project.repository.delete_file(project.owner, 'LICENSE', message: 'remove license', branch_name: 'master')
end
- end
- context 'when gitaly is enabled' do
- it_behaves_like 'acquiring the Licensee license key'
+ it { is_expected.to be_nil }
end
- context 'when gitaly is disabled', :disable_gitaly do
- it_behaves_like 'acquiring the Licensee license key'
+ context 'when an mit license is found' do
+ it { is_expected.to eq('mit') }
end
end
@@ -1828,59 +1716,51 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
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, '') }
-
- after do
- ensure_seeds
- end
+ 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
- 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 }
+ after do
+ ensure_seeds
+ end
- before do
- source_rugged.branches.create(source_branch, new_oid)
- end
+ 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) { Gitlab::GitalyClient::StorageSettings.allow_disk_access { source_repository.rugged } }
+ let(:new_oid) { new_commit_edit_old_file(source_rugged).oid }
- 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
+ before do
+ source_rugged.branches.create(source_branch, new_oid)
end
- context 'when the commit exists locally' do
- let(:source_branch) { 'master' }
- let(:expected_oid) { SeedRepo::LastCommit::ID }
-
- it 'writes the ref' do
- # Sanity check: the commit should already exist
- expect(repository.commit(expected_oid)).not_to be_nil
-
- 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
+ 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.fetch_source_branch!(source_repository, source_branch, local_ref)).to eq(false)
- expect(repository.commit(local_ref)).to be_nil
+ it 'writes the ref' do
+ # Sanity check: the commit should already exist
+ expect(repository.commit(expected_oid)).not_to be_nil
+
+ 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_behaves_like '#fetch_source_branch!'
+ context 'when the branch does not exist' do
+ let(:source_branch) { 'definitely-not-master' }
- context 'without gitaly', :skip_gitaly_mock do
- it_behaves_like '#fetch_source_branch!'
+ 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
@@ -1898,7 +1778,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
it "removes the branch from the repo" do
repository.rm_branch(branch_name, user: user)
- expect(repository.rugged.branches[branch_name]).to be_nil
+ expect(repository_rugged.branches[branch_name]).to be_nil
end
end
@@ -1930,39 +1810,89 @@ describe Gitlab::Git::Repository, seed_helper: true do
describe '#write_config' do
before do
- repository.rugged.config["gitlab.fullpath"] = repository.path
+ repository_rugged.config["gitlab.fullpath"] = repository_path
end
- shared_examples 'writing repo config' do
- context 'is given a path' do
- it 'writes it to disk' do
- repository.write_config(full_path: "not-the/real-path.git")
+ context 'is given a path' do
+ it 'writes it to disk' do
+ repository.write_config(full_path: "not-the/real-path.git")
- config = File.read(File.join(repository.path, "config"))
+ config = File.read(File.join(repository_path, "config"))
- expect(config).to include("[gitlab]")
- expect(config).to include("fullpath = not-the/real-path.git")
- end
+ expect(config).to include("[gitlab]")
+ expect(config).to include("fullpath = not-the/real-path.git")
end
+ end
- context 'it is given an empty path' do
- it 'does not write it to disk' do
- repository.write_config(full_path: "")
+ context 'it is given an empty path' do
+ it 'does not write it to disk' do
+ repository.write_config(full_path: "")
- config = File.read(File.join(repository.path, "config"))
+ config = File.read(File.join(repository_path, "config"))
- expect(config).to include("[gitlab]")
- expect(config).to include("fullpath = #{repository.path}")
- end
+ expect(config).to include("[gitlab]")
+ expect(config).to include("fullpath = #{repository_path}")
+ end
+ end
+
+ context 'repository does not exist' do
+ it 'raises NoRepository and does not call Gitaly WriteConfig' do
+ repository = Gitlab::Git::Repository.new('default', 'does/not/exist.git', '')
+
+ expect(repository.gitaly_repository_client).not_to receive(:write_config)
+
+ expect do
+ repository.write_config(full_path: 'foo/bar.git')
+ end.to raise_error(Gitlab::Git::Repository::NoRepository)
end
end
+ end
+
+ describe '#set_config' do
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') }
+ let(:rugged) { repository_rugged }
+ let(:entries) do
+ {
+ 'test.foo1' => 'bla bla',
+ 'test.foo2' => 1234,
+ 'test.foo3' => true
+ }
+ end
+
+ it 'can set config settings' do
+ expect(repository.set_config(entries)).to be_nil
+
+ expect(rugged.config['test.foo1']).to eq('bla bla')
+ expect(rugged.config['test.foo2']).to eq('1234')
+ expect(rugged.config['test.foo3']).to eq('true')
+ end
- context "when gitaly_write_config is enabled" do
- it_behaves_like "writing repo config"
+ after do
+ entries.keys.each { |k| rugged.config.delete(k) }
+ end
+ end
+
+ describe '#delete_config' do
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') }
+ let(:rugged) { repository_rugged }
+ let(:entries) do
+ {
+ 'test.foo1' => 'bla bla',
+ 'test.foo2' => 1234,
+ 'test.foo3' => true
+ }
end
- context "when gitaly_write_config is disabled", :disable_gitaly do
- it_behaves_like "writing repo config"
+ it 'can delete config settings' do
+ entries.each do |key, value|
+ rugged.config[key] = value
+ end
+
+ expect(repository.delete_config(*%w[does.not.exist test.foo1 test.foo2])).to be_nil
+
+ config_keys = rugged.config.each_key.to_a
+ expect(config_keys).not_to include('test.foo1')
+ expect(config_keys).not_to include('test.foo2')
end
end
@@ -2071,21 +2001,15 @@ describe Gitlab::Git::Repository, seed_helper: true do
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 "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)
- it_behaves_like '#ff_merge'
+ subject
end
- context 'without gitaly', :skip_gitaly_mock do
- it_behaves_like '#ff_merge'
- end
+ it_behaves_like '#ff_merge'
end
describe '#delete_all_refs_except' do
@@ -2173,7 +2097,11 @@ describe Gitlab::Git::Repository, seed_helper: true do
describe '#gitlab_projects' do
subject { repository.gitlab_projects }
- it { expect(subject.shard_path).to eq(storage_path) }
+ it do
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ expect(subject.shard_path).to eq(storage_path)
+ end
+ end
it { expect(subject.repository_relative_path).to eq(repository.relative_path) }
end
@@ -2189,7 +2117,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
repository.bundle_to_disk(save_path)
success = system(
- *%W(#{Gitlab.config.git.bin_path} -C #{repository.path} bundle verify #{save_path}),
+ *%W(#{Gitlab.config.git.bin_path} -C #{repository_path} bundle verify #{save_path}),
[:out, :err] => '/dev/null'
)
expect(success).to be true
@@ -2206,49 +2134,39 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe '#create_from_bundle' do
- shared_examples 'creating repo from bundle' do
- let(:bundle_path) { File.join(Dir.tmpdir, "repo-#{SecureRandom.hex}.bundle") }
- let(:project) { create(:project) }
- let(:imported_repo) { project.repository.raw }
-
- before do
- expect(repository.bundle_to_disk(bundle_path)).to be true
- end
-
- after do
- FileUtils.rm_rf(bundle_path)
- end
+ let(:bundle_path) { File.join(Dir.tmpdir, "repo-#{SecureRandom.hex}.bundle") }
+ let(:project) { create(:project) }
+ let(:imported_repo) { project.repository.raw }
- it 'creates a repo from a bundle file' do
- expect(imported_repo).not_to exist
+ before do
+ expect(repository.bundle_to_disk(bundle_path)).to be_truthy
+ end
- result = imported_repo.create_from_bundle(bundle_path)
+ after do
+ FileUtils.rm_rf(bundle_path)
+ end
- expect(result).to be true
- expect(imported_repo).to exist
- expect { imported_repo.fsck }.not_to raise_exception
- end
+ it 'creates a repo from a bundle file' do
+ expect(imported_repo).not_to exist
- it 'creates a symlink to the global hooks dir' do
- imported_repo.create_from_bundle(bundle_path)
- hooks_path = File.join(imported_repo.path, 'hooks')
+ result = imported_repo.create_from_bundle(bundle_path)
- expect(File.readlink(hooks_path)).to eq(Gitlab.config.gitlab_shell.hooks_path)
- end
+ expect(result).to be_truthy
+ expect(imported_repo).to exist
+ expect { imported_repo.fsck }.not_to raise_exception
end
- context 'when Gitaly create_repo_from_bundle feature is enabled' do
- it_behaves_like 'creating repo from bundle'
- end
+ it 'creates a symlink to the global hooks dir' do
+ imported_repo.create_from_bundle(bundle_path)
+ hooks_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access { File.join(imported_repo.path, 'hooks') }
- context 'when Gitaly create_repo_from_bundle feature is disabled', :disable_gitaly do
- it_behaves_like 'creating repo from bundle'
+ expect(File.readlink(hooks_path)).to eq(Gitlab.config.gitlab_shell.hooks_path)
end
end
describe '#checksum' do
it 'calculates the checksum for non-empty repo' do
- expect(repository.checksum).to eq '54f21be4c32c02f6788d72207fa03ad3bce725e4'
+ expect(repository.checksum).to eq '4be7d24ce7e8d845502d599b72d567d23e6a40c0'
end
it 'returns 0000000000000000000000000000000000000000 for an empty repo' do
@@ -2360,7 +2278,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe '#clean_stale_repository_files' do
- let(:worktree_path) { File.join(repository.path, 'worktrees', 'delete-me') }
+ let(:worktree_path) { File.join(repository_path, 'worktrees', 'delete-me') }
it 'cleans up the files' do
repository.with_worktree(worktree_path, 'master', env: ENV) do
@@ -2414,92 +2332,95 @@ describe Gitlab::Git::Repository, seed_helper: true do
expect { subject }.to raise_error(Gitlab::Git::CommandError, 'error')
end
end
+ end
- describe '#squash' do
- let(:squash_id) { '1' }
- let(:branch_name) { 'fix' }
- let(:start_sha) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' }
- let(:end_sha) { '12d65c8dd2b2676fa3ac47d955accc085a37a9c1' }
+ describe '#squash' do
+ let(:squash_id) { '1' }
+ let(:branch_name) { 'fix' }
+ let(:start_sha) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' }
+ let(:end_sha) { '12d65c8dd2b2676fa3ac47d955accc085a37a9c1' }
- subject do
- opts = {
- branch: branch_name,
- start_sha: start_sha,
- end_sha: end_sha,
- author: user,
- message: 'Squash commit message'
- }
+ subject do
+ opts = {
+ branch: branch_name,
+ start_sha: start_sha,
+ end_sha: end_sha,
+ author: user,
+ message: 'Squash commit message'
+ }
+
+ repository.squash(user, squash_id, opts)
+ end
+
+ # Should be ported to gitaly-ruby rspec suite https://gitlab.com/gitlab-org/gitaly/issues/1234
+ skip 'sparse checkout' do
+ let(:expected_files) { %w(files files/js files/js/application.js) }
+
+ it 'checks out only the files in the diff' do
+ allow(repository).to receive(:with_worktree).and_wrap_original do |m, *args|
+ m.call(*args) do
+ worktree_path = args[0]
+ files_pattern = File.join(worktree_path, '**', '*')
+ expected = expected_files.map do |path|
+ File.expand_path(path, worktree_path)
+ end
- repository.squash(user, squash_id, opts)
+ expect(Dir[files_pattern]).to eq(expected)
+ end
+ end
+
+ subject
end
- context 'sparse checkout', :skip_gitaly_mock do
- let(:expected_files) { %w(files files/js files/js/application.js) }
+ context 'when the diff contains a rename' do
+ let(:repo) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '').rugged }
+ let(:end_sha) { new_commit_move_file(repo).oid }
+
+ after do
+ # Erase our commits so other tests get the original repo
+ repo = Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '').rugged
+ repo.references.update('refs/heads/master', SeedRepo::LastCommit::ID)
+ end
- it 'checks out only the files in the diff' do
+ it 'does not include the renamed file in the sparse checkout' do
allow(repository).to receive(:with_worktree).and_wrap_original do |m, *args|
m.call(*args) do
worktree_path = args[0]
files_pattern = File.join(worktree_path, '**', '*')
- expected = expected_files.map do |path|
- File.expand_path(path, worktree_path)
- end
- expect(Dir[files_pattern]).to eq(expected)
+ expect(Dir[files_pattern]).not_to include('CHANGELOG')
+ expect(Dir[files_pattern]).not_to include('encoding/CHANGELOG')
end
end
subject
end
-
- context 'when the diff contains a rename' do
- let(:repo) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '').rugged }
- let(:end_sha) { new_commit_move_file(repo).oid }
-
- after do
- # Erase our commits so other tests get the original repo
- repo = Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '').rugged
- repo.references.update('refs/heads/master', SeedRepo::LastCommit::ID)
- end
-
- it 'does not include the renamed file in the sparse checkout' do
- allow(repository).to receive(:with_worktree).and_wrap_original do |m, *args|
- m.call(*args) do
- worktree_path = args[0]
- files_pattern = File.join(worktree_path, '**', '*')
-
- expect(Dir[files_pattern]).not_to include('CHANGELOG')
- expect(Dir[files_pattern]).not_to include('encoding/CHANGELOG')
- end
- end
-
- subject
- end
- end
end
+ end
- context 'with an ASCII-8BIT diff', :skip_gitaly_mock do
- let(:diff) { "diff --git a/README.md b/README.md\nindex faaf198..43c5edf 100644\n--- a/README.md\n+++ b/README.md\n@@ -1,4 +1,4 @@\n-testme\n+✓ testme\n ======\n \n Sample repo for testing gitlab features\n" }
+ # Should be ported to gitaly-ruby rspec suite https://gitlab.com/gitlab-org/gitaly/issues/1234
+ skip 'with an ASCII-8BIT diff' do
+ let(:diff) { "diff --git a/README.md b/README.md\nindex faaf198..43c5edf 100644\n--- a/README.md\n+++ b/README.md\n@@ -1,4 +1,4 @@\n-testme\n+✓ testme\n ======\n \n Sample repo for testing gitlab features\n" }
- it 'applies a ASCII-8BIT diff' do
- allow(repository).to receive(:run_git!).and_call_original
- allow(repository).to receive(:run_git!).with(%W(diff --binary #{start_sha}...#{end_sha})).and_return(diff.force_encoding('ASCII-8BIT'))
+ it 'applies a ASCII-8BIT diff' do
+ allow(repository).to receive(:run_git!).and_call_original
+ allow(repository).to receive(:run_git!).with(%W(diff --binary #{start_sha}...#{end_sha})).and_return(diff.force_encoding('ASCII-8BIT'))
- expect(subject).to match(/\h{40}/)
- end
+ expect(subject).to match(/\h{40}/)
end
+ end
- context 'with trailing whitespace in an invalid patch', :skip_gitaly_mock do
- let(:diff) { "diff --git a/README.md b/README.md\nindex faaf198..43c5edf 100644\n--- a/README.md\n+++ b/README.md\n@@ -1,4 +1,4 @@\n-testme\n+ \n ====== \n \n Sample repo for testing gitlab features\n" }
+ # Should be ported to gitaly-ruby rspec suite https://gitlab.com/gitlab-org/gitaly/issues/1234
+ skip 'with trailing whitespace in an invalid patch' do
+ let(:diff) { "diff --git a/README.md b/README.md\nindex faaf198..43c5edf 100644\n--- a/README.md\n+++ b/README.md\n@@ -1,4 +1,4 @@\n-testme\n+ \n ====== \n \n Sample repo for testing gitlab features\n" }
- it 'does not include whitespace warnings in the error' do
- allow(repository).to receive(:run_git!).and_call_original
- allow(repository).to receive(:run_git!).with(%W(diff --binary #{start_sha}...#{end_sha})).and_return(diff.force_encoding('ASCII-8BIT'))
+ it 'does not include whitespace warnings in the error' do
+ allow(repository).to receive(:run_git!).and_call_original
+ allow(repository).to receive(:run_git!).with(%W(diff --binary #{start_sha}...#{end_sha})).and_return(diff.force_encoding('ASCII-8BIT'))
- expect { subject }.to raise_error do |error|
- expect(error).to be_a(described_class::GitError)
- expect(error.message).not_to include('trailing whitespace')
- end
+ expect { subject }.to raise_error do |error|
+ expect(error).to be_a(described_class::GitError)
+ expect(error.message).not_to include('trailing whitespace')
end
end
end
@@ -2507,7 +2428,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
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
+ rugged = repository_rugged
rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", source_branch.dereferenced_target.sha)
end
@@ -2586,4 +2507,16 @@ describe Gitlab::Git::Repository, seed_helper: true do
line.split("\t").last
end
end
+
+ def repository_rugged
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ repository.rugged
+ end
+ end
+
+ def repository_path
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ repository.path
+ end
+ end
end
diff --git a/spec/lib/gitlab/git/rev_list_spec.rb b/spec/lib/gitlab/git/rev_list_spec.rb
index 32ec1e029c8..b752c3e8341 100644
--- a/spec/lib/gitlab/git/rev_list_spec.rb
+++ b/spec/lib/gitlab/git/rev_list_spec.rb
@@ -9,9 +9,11 @@ describe Gitlab::Git::RevList do
end
def stub_popen_rev_list(*additional_args, with_lazy_block: true, output:)
+ repo_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access { repository.path }
+
params = [
args_for_popen(additional_args),
- repository.path,
+ repo_path,
{},
hash_including(lazy_block: with_lazy_block ? anything : nil)
]
@@ -91,14 +93,4 @@ describe Gitlab::Git::RevList do
expect { |b| rev_list.all_objects(&b) }.to yield_with_args(%w[sha1 sha2])
end
end
-
- context "#missed_ref" do
- let(:rev_list) { described_class.new(repository, oldrev: 'oldrev', newrev: 'newrev') }
-
- it 'calls out to `popen`' do
- stub_popen_rev_list('--max-count=1', 'oldrev', '^newrev', with_lazy_block: false, output: "sha1\nsha2")
-
- expect(rev_list.missed_ref).to eq(%w[sha1 sha2])
- end
- end
end
diff --git a/spec/lib/gitlab/git/wiki_spec.rb b/spec/lib/gitlab/git/wiki_spec.rb
index 722d697c28e..b63658e1b3b 100644
--- a/spec/lib/gitlab/git/wiki_spec.rb
+++ b/spec/lib/gitlab/git/wiki_spec.rb
@@ -6,9 +6,7 @@ describe Gitlab::Git::Wiki do
let(:project_wiki) { ProjectWiki.new(project, user) }
subject { project_wiki.wiki }
- # Remove skip_gitaly_mock flag when gitaly_find_page when
- # https://gitlab.com/gitlab-org/gitlab-ce/issues/42039 is solved
- describe '#page', :skip_gitaly_mock do
+ describe '#page' do
before do
create_page('page1', 'content')
create_page('foo/page1', 'content foo/page1')
@@ -25,6 +23,22 @@ describe Gitlab::Git::Wiki do
end
end
+ describe '#delete_page' do
+ after do
+ destroy_page('page1')
+ end
+
+ it 'only removes the page with the same path' do
+ create_page('page1', 'content')
+ create_page('*', 'content')
+
+ subject.delete_page('*', commit_details('whatever'))
+
+ expect(subject.pages.count).to eq 1
+ expect(subject.pages.first.title).to eq 'page1'
+ end
+ end
+
def create_page(name, content)
subject.write_page(name, :markdown, content, commit_details(name))
end
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index 317a932d5a6..832db2bd906 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -13,14 +13,6 @@ describe Gitlab::GitAccess do
let(:authentication_abilities) { %i[read_project download_code push_code] }
let(:redirected_path) { nil }
let(:auth_result_type) { nil }
-
- let(:access) do
- described_class.new(actor, project,
- protocol, authentication_abilities: authentication_abilities,
- namespace_path: namespace_path, project_path: project_path,
- redirected_path: redirected_path, auth_result_type: auth_result_type)
- end
-
let(:changes) { '_any' }
let(:push_access_check) { access.check('git-receive-pack', changes) }
let(:pull_access_check) { access.check('git-upload-pack', changes) }
@@ -48,7 +40,7 @@ describe Gitlab::GitAccess do
before do
disable_protocol('http')
- project.add_master(user)
+ project.add_maintainer(user)
end
it 'blocks http push and pull' do
@@ -113,7 +105,7 @@ describe Gitlab::GitAccess do
context 'when actor is a User' do
context 'when the User can read the project' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
it 'allows push and pull access' do
@@ -254,7 +246,7 @@ describe Gitlab::GitAccess do
shared_examples '#check with a key that is not valid' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
context 'key is too small' do
@@ -307,7 +299,7 @@ describe Gitlab::GitAccess do
describe '#add_project_moved_message!', :clean_gitlab_redis_shared_state do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
context 'when a redirect was not followed to find the project' do
@@ -335,7 +327,7 @@ describe Gitlab::GitAccess do
describe '#check_authentication_abilities!' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
context 'when download' do
@@ -381,7 +373,7 @@ describe Gitlab::GitAccess do
describe '#check_command_disabled!' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
context 'over http' do
@@ -529,8 +521,8 @@ describe Gitlab::GitAccess do
end
describe '#check_download_access!' do
- it 'allows masters to pull' do
- project.add_master(user)
+ it 'allows maintainers to pull' do
+ project.add_maintainer(user)
expect { pull_access_check }.not_to raise_error
end
@@ -542,7 +534,7 @@ describe Gitlab::GitAccess do
end
it 'disallows blocked users to pull' do
- project.add_master(user)
+ project.add_maintainer(user)
user.block
expect { pull_access_check }.to raise_unauthorized('Your account has been blocked.')
@@ -552,7 +544,7 @@ describe Gitlab::GitAccess do
it 'returns not found' do
project.add_guest(user)
repo = project.repository
- FileUtils.rm_rf(repo.path)
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access { FileUtils.rm_rf(repo.path) }
# Sanity check for rm_rf
expect(repo.exists?).to eq(false)
@@ -724,10 +716,11 @@ describe Gitlab::GitAccess do
end
describe '#check_push_access!' do
+ let(:unprotected_branch) { 'unprotected_branch' }
+
before do
merge_into_protected_branch
end
- let(:unprotected_branch) { 'unprotected_branch' }
let(:changes) do
{ push_new_branch: "#{Gitlab::Git::BLANK_SHA} 570e7b2ab refs/heads/wow",
@@ -750,20 +743,22 @@ describe Gitlab::GitAccess do
def merge_into_protected_branch
@protected_branch_merge_commit ||= begin
- stub_git_hooks
- project.repository.add_branch(user, unprotected_branch, 'feature')
- target_branch = project.repository.lookup('feature')
- source_branch = project.repository.create_file(
- user,
- 'filename',
- 'This is the file content',
- message: 'This is a good commit message',
- branch_name: unprotected_branch)
- rugged = project.repository.rugged
- author = { email: "email@example.com", time: Time.now, name: "Example Git User" }
-
- merge_index = rugged.merge_commits(target_branch, source_branch)
- Rugged::Commit.create(rugged, author: author, committer: author, message: "commit message", parents: [target_branch, source_branch], tree: merge_index.write_tree(rugged))
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ stub_git_hooks
+ project.repository.add_branch(user, unprotected_branch, 'feature')
+ target_branch = project.repository.lookup('feature')
+ source_branch = project.repository.create_file(
+ user,
+ 'filename',
+ 'This is the file content',
+ message: 'This is a good commit message',
+ branch_name: unprotected_branch)
+ rugged = project.repository.rugged
+ author = { email: "email@example.com", time: Time.now, name: "Example Git User" }
+
+ merge_index = rugged.merge_commits(target_branch, source_branch)
+ Rugged::Commit.create(rugged, author: author, committer: author, message: "commit message", parents: [target_branch, source_branch], tree: merge_index.write_tree(rugged))
+ end
end
end
@@ -783,7 +778,7 @@ describe Gitlab::GitAccess do
aggregate_failures do
matrix.each do |action, allowed|
- check = -> { access.send(:check_push_access!, changes[action]) }
+ check = -> { push_changes(changes[action]) }
if allowed
expect(&check).not_to raise_error,
@@ -810,7 +805,7 @@ describe Gitlab::GitAccess do
merge_into_protected_branch: true
},
- master: {
+ maintainer: {
push_new_branch: true,
push_master: true,
push_protected_branch: true,
@@ -915,7 +910,7 @@ describe Gitlab::GitAccess do
end
run_permission_checks(permissions_matrix.deep_merge(developer: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false },
- master: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false },
+ maintainer: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false },
admin: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false }))
end
end
@@ -932,6 +927,22 @@ describe Gitlab::GitAccess do
expect(project.repository).to receive(:clean_stale_repository_files).and_call_original
expect { push_access_check }.not_to raise_error
end
+
+ it 'avoids N+1 queries', :request_store do
+ # Run this once to establish a baseline. Cached queries should get
+ # cached, so that when we introduce another change we shouldn't see
+ # additional queries.
+ access.check('git-receive-pack', changes)
+
+ control_count = ActiveRecord::QueryRecorder.new do
+ access.check('git-receive-pack', changes)
+ end
+
+ changes = ['6f6d7e7ed 570e7b2ab refs/heads/master', '6f6d7e7ed 570e7b2ab refs/heads/feature']
+
+ # There is still an N+1 query with protected branches
+ expect { access.check('git-receive-pack', changes) }.not_to exceed_query_limit(control_count).with_threshold(1)
+ end
end
end
@@ -971,7 +982,7 @@ describe Gitlab::GitAccess do
let(:project) { create(:project, :repository, :read_only) }
it 'denies push access' do
- project.add_master(user)
+ project.add_maintainer(user)
expect { push_access_check }.to raise_unauthorized('The repository is temporarily read-only. Please try again later.')
end
@@ -1055,7 +1066,7 @@ describe Gitlab::GitAccess do
it 'blocks access when the user did not accept terms', :aggregate_failures do
actions.each do |action|
- expect { action.call }.to raise_unauthorized(/You must accept the Terms of Service in order to perform this action/)
+ expect { action.call }.to raise_unauthorized(/must accept the Terms of Service in order to perform this action/)
end
end
@@ -1101,9 +1112,9 @@ describe Gitlab::GitAccess do
it_behaves_like 'access after accepting terms'
end
- describe 'as a master of the project' do
+ describe 'as a maintainer of the project' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
it_behaves_like 'access after accepting terms'
@@ -1134,6 +1145,17 @@ describe Gitlab::GitAccess do
private
+ def access
+ described_class.new(actor, project, protocol,
+ authentication_abilities: authentication_abilities,
+ namespace_path: namespace_path, project_path: project_path,
+ redirected_path: redirected_path, auth_result_type: auth_result_type)
+ end
+
+ def push_changes(changes)
+ access.check('git-receive-pack', changes)
+ end
+
def raise_unauthorized(message)
raise_error(Gitlab::GitAccess::UnauthorizedError, message)
end
diff --git a/spec/lib/gitlab/git_access_wiki_spec.rb b/spec/lib/gitlab/git_access_wiki_spec.rb
index 730ede99fc9..9c6c9fe13bf 100644
--- a/spec/lib/gitlab/git_access_wiki_spec.rb
+++ b/spec/lib/gitlab/git_access_wiki_spec.rb
@@ -52,7 +52,9 @@ describe Gitlab::GitAccessWiki do
context 'when the wiki repository does not exist' do
it 'returns not found' do
wiki_repo = project.wiki.repository
- FileUtils.rm_rf(wiki_repo.path)
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ FileUtils.rm_rf(wiki_repo.path)
+ end
# Sanity check for rm_rf
expect(wiki_repo.exists?).to eq(false)
diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
index 7951cbe7b1d..54f2ea33f90 100644
--- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
@@ -17,7 +17,7 @@ describe Gitlab::GitalyClient::CommitService do
repository: repository_message,
left_commit_id: 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660',
right_commit_id: commit.id,
- collapse_diffs: true,
+ collapse_diffs: false,
enforce_limits: true,
**Gitlab::Git::DiffCollection.collection_limits.to_h
)
@@ -35,7 +35,7 @@ describe Gitlab::GitalyClient::CommitService do
repository: repository_message,
left_commit_id: Gitlab::Git::EMPTY_TREE_ID,
right_commit_id: initial_commit.id,
- collapse_diffs: true,
+ collapse_diffs: false,
enforce_limits: true,
**Gitlab::Git::DiffCollection.collection_limits.to_h
)
diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
index 9fbdd73ee0e..031d1e87dc1 100644
--- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
@@ -48,7 +48,48 @@ describe Gitlab::GitalyClient::OperationService do
.and_return(response)
expect { subject }.to raise_error(
- Gitlab::Git::HooksService::PreReceiveError, "something failed")
+ Gitlab::Git::PreReceiveError, "something failed")
+ end
+ end
+ end
+
+ describe '#user_update_branch' do
+ let(:branch_name) { 'my-branch' }
+ let(:newrev) { '01e' }
+ let(:oldrev) { '01d' }
+ let(:request) do
+ Gitaly::UserUpdateBranchRequest.new(
+ repository: repository.gitaly_repository,
+ branch_name: branch_name,
+ newrev: newrev,
+ oldrev: oldrev,
+ user: gitaly_user
+ )
+ end
+ let(:response) { Gitaly::UserUpdateBranchResponse.new }
+
+ subject { client.user_update_branch(branch_name, user, newrev, oldrev) }
+
+ it 'sends a user_update_branch message' do
+ expect_any_instance_of(Gitaly::OperationService::Stub)
+ .to receive(:user_update_branch).with(request, kind_of(Hash))
+ .and_return(response)
+
+ subject
+ end
+
+ context "when pre_receive_error is present" do
+ let(:response) do
+ Gitaly::UserUpdateBranchResponse.new(pre_receive_error: "something failed")
+ end
+
+ it "throws a PreReceive exception" do
+ expect_any_instance_of(Gitaly::OperationService::Stub)
+ .to receive(:user_update_branch).with(request, kind_of(Hash))
+ .and_return(response)
+
+ expect { subject }.to raise_error(
+ Gitlab::Git::PreReceiveError, "something failed")
end
end
end
@@ -85,7 +126,7 @@ describe Gitlab::GitalyClient::OperationService do
.and_return(response)
expect { subject }.to raise_error(
- Gitlab::Git::HooksService::PreReceiveError, "something failed")
+ Gitlab::Git::PreReceiveError, "something failed")
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
index d34ca0b76b8..81fe97c1e49 100644
--- a/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb
@@ -180,12 +180,12 @@ describe Gitlab::GithubImport::Importer::IssueImporter, :clean_gitlab_redis_cach
allow(importer.user_finder)
.to receive(:user_id_for)
- .ordered.with(issue.assignees[0])
+ .with(issue.assignees[0])
.and_return(4)
allow(importer.user_finder)
.to receive(:user_id_for)
- .ordered.with(issue.assignees[1])
+ .with(issue.assignees[1])
.and_return(5)
expect(Gitlab::Database)
diff --git a/spec/lib/gitlab/github_import/importer/lfs_object_importer_spec.rb b/spec/lib/gitlab/github_import/importer/lfs_object_importer_spec.rb
new file mode 100644
index 00000000000..4857f2afbe2
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer/lfs_object_importer_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Importer::LfsObjectImporter do
+ let(:project) { create(:project) }
+ let(:download_link) { "http://www.gitlab.com/lfs_objects/oid" }
+
+ let(:github_lfs_object) do
+ Gitlab::GithubImport::Representation::LfsObject.new(
+ oid: 'oid', download_link: download_link
+ )
+ end
+
+ let(:importer) { described_class.new(github_lfs_object, project, nil) }
+
+ describe '#execute' do
+ it 'calls the LfsDownloadService with the lfs object attributes' do
+ expect_any_instance_of(Projects::LfsPointers::LfsDownloadService)
+ .to receive(:execute).with('oid', download_link)
+
+ importer.execute
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb b/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb
new file mode 100644
index 00000000000..5f5c6b803c0
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb
@@ -0,0 +1,94 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Importer::LfsObjectsImporter do
+ let(:project) { double(:project, id: 4, import_source: 'foo/bar') }
+ let(:client) { double(:client) }
+ let(:download_link) { "http://www.gitlab.com/lfs_objects/oid" }
+
+ let(:github_lfs_object) { ['oid', download_link] }
+
+ 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 lfs objects 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 lfs objects 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 lfs object in sequence' do
+ importer = described_class.new(project, client, parallel: false)
+ lfs_object_importer = double(:lfs_object_importer)
+
+ allow(importer)
+ .to receive(:each_object_to_import)
+ .and_yield(['oid', download_link])
+
+ expect(Gitlab::GithubImport::Importer::LfsObjectImporter)
+ .to receive(:new)
+ .with(
+ an_instance_of(Gitlab::GithubImport::Representation::LfsObject),
+ project,
+ client
+ )
+ .and_return(lfs_object_importer)
+
+ expect(lfs_object_importer).to receive(:execute)
+
+ importer.sequential_import
+ end
+ end
+
+ describe '#parallel_import' do
+ it 'imports each lfs object in parallel' do
+ importer = described_class.new(project, client)
+
+ allow(importer)
+ .to receive(:each_object_to_import)
+ .and_yield(github_lfs_object)
+
+ expect(Gitlab::GithubImport::ImportLfsObjectWorker)
+ .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 '#collection_options' do
+ it 'returns an empty Hash' do
+ 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
index 35f3fdf8304..3422a1e82fc 100644
--- a/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb
@@ -40,13 +40,19 @@ describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redi
describe '#execute' do
it 'imports the pull request' do
+ mr = double(:merge_request, id: 10)
+
expect(importer)
.to receive(:create_merge_request)
- .and_return(10)
+ .and_return([mr, false])
+
+ expect(importer)
+ .to receive(:insert_git_data)
+ .with(mr, false)
expect_any_instance_of(Gitlab::GithubImport::IssuableFinder)
.to receive(:cache_database_id)
- .with(10)
+ .with(mr.id)
importer.execute
end
@@ -99,18 +105,11 @@ describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redi
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
+ it 'returns the created merge request' do
+ mr, exists = importer.create_merge_request
- expect(mr.merge_request_diffs.exists?).to eq(true)
+ expect(mr).to be_instance_of(MergeRequest)
+ expect(exists).to eq(false)
end
end
@@ -217,5 +216,77 @@ describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redi
expect { importer.create_merge_request }.not_to raise_error
end
end
+
+ context 'when the merge request already exists' 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 'returns the existing merge request' do
+ mr1, exists1 = importer.create_merge_request
+ mr2, exists2 = importer.create_merge_request
+
+ expect(mr2).to eq(mr1)
+ expect(exists1).to eq(false)
+ expect(exists2).to eq(true)
+ end
+ end
+ end
+
+ describe '#insert_git_data' do
+ before do
+ allow(importer.milestone_finder)
+ .to receive(:id_for)
+ .with(pull_request)
+ .and_return(milestone.id)
+
+ 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 'creates the merge request diffs' do
+ mr, exists = importer.create_merge_request
+
+ importer.insert_git_data(mr, exists)
+
+ expect(mr.merge_request_diffs.exists?).to eq(true)
+ end
+
+ it 'creates the merge request diff commits' do
+ mr, exists = importer.create_merge_request
+
+ importer.insert_git_data(mr, exists)
+
+ diff = mr.merge_request_diffs.take
+
+ expect(diff.merge_request_diff_commits.exists?).to eq(true)
+ end
+
+ context 'when the merge request exists' do
+ it 'creates the merge request diffs if they do not yet exist' do
+ mr, _ = importer.create_merge_request
+
+ mr.merge_request_diffs.delete_all
+
+ importer.insert_git_data(mr, true)
+
+ expect(mr.merge_request_diffs.exists?).to eq(true)
+ 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
index 44695acbe7d..b2e544e6fed 100644
--- a/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb
@@ -27,9 +27,9 @@ describe Gitlab::GithubImport::Importer::PullRequestsImporter do
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
+ created_at: 1.second.ago,
+ updated_at: 1.second.ago,
+ merged_at: 1.second.ago
)
end
@@ -164,7 +164,7 @@ describe Gitlab::GithubImport::Importer::PullRequestsImporter do
Timecop.freeze do
importer.update_repository
- expect(project.last_repository_updated_at).to eq(Time.zone.now)
+ expect(project.last_repository_updated_at).to be_like_time(Time.zone.now)
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
index cc9e4b67e72..d8f01dcb76b 100644
--- a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
@@ -14,7 +14,8 @@ describe Gitlab::GithubImport::Importer::RepositoryImporter do
disk_path: 'foo',
repository: repository,
create_wiki: true,
- import_state: import_state
+ import_state: import_state,
+ lfs_enabled?: true
)
end
diff --git a/spec/lib/gitlab/github_import/sequential_importer_spec.rb b/spec/lib/gitlab/github_import/sequential_importer_spec.rb
index 6089b0b751f..05d3243f806 100644
--- a/spec/lib/gitlab/github_import/sequential_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/sequential_importer_spec.rb
@@ -30,7 +30,6 @@ describe Gitlab::GithubImport::SequentialImporter do
expect(instance).to receive(:execute)
end
- expect(repository).to receive(:after_import)
expect(importer.execute).to eq(true)
end
end
diff --git a/spec/lib/gitlab/gitlab_import/importer_spec.rb b/spec/lib/gitlab/gitlab_import/importer_spec.rb
index e1d935602b5..200edceca8c 100644
--- a/spec/lib/gitlab/gitlab_import/importer_spec.rb
+++ b/spec/lib/gitlab/gitlab_import/importer_spec.rb
@@ -20,7 +20,7 @@ describe Gitlab::GitlabImport::Importer do
}
}
])
- stub_request('issues/2579857/notes', [])
+ stub_request('issues/3/notes', [])
end
it 'persists issues' do
@@ -43,7 +43,7 @@ describe Gitlab::GitlabImport::Importer do
end
def stub_request(path, body)
- url = "https://gitlab.com/api/v3/projects/asd%2Fvim/#{path}?page=1&per_page=100"
+ url = "https://gitlab.com/api/v4/projects/asd%2Fvim/#{path}?page=1&per_page=100"
WebMock.stub_request(:get, url)
.to_return(
diff --git a/spec/lib/gitlab/gitlab_import/project_creator_spec.rb b/spec/lib/gitlab/gitlab_import/project_creator_spec.rb
index 5ea086e4abd..b814f5fc76c 100644
--- a/spec/lib/gitlab/gitlab_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/gitlab_import/project_creator_spec.rb
@@ -21,7 +21,9 @@ describe Gitlab::GitlabImport::ProjectCreator do
end
it 'creates project' do
- allow_any_instance_of(Project).to receive(:add_import_job)
+ expect_next_instance_of(Project) do |project|
+ expect(project).to receive(:add_import_job)
+ end
project_creator = described_class.new(repo, namespace, user, access_params)
project = project_creator.execute
diff --git a/spec/lib/gitlab/google_code_import/importer_spec.rb b/spec/lib/gitlab/google_code_import/importer_spec.rb
index 017facd0f5e..031f57dbc65 100644
--- a/spec/lib/gitlab/google_code_import/importer_spec.rb
+++ b/spec/lib/gitlab/google_code_import/importer_spec.rb
@@ -15,7 +15,7 @@ describe Gitlab::GoogleCodeImport::Importer do
subject { described_class.new(project) }
before do
- project.add_master(project.creator)
+ project.add_maintainer(project.creator)
project.create_import_data(data: import_data)
end
diff --git a/spec/lib/gitlab/google_code_import/project_creator_spec.rb b/spec/lib/gitlab/google_code_import/project_creator_spec.rb
index 24cd518c77b..b959e006292 100644
--- a/spec/lib/gitlab/google_code_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/google_code_import/project_creator_spec.rb
@@ -16,7 +16,9 @@ describe Gitlab::GoogleCodeImport::ProjectCreator do
end
it 'creates project' do
- allow_any_instance_of(Project).to receive(:add_import_job)
+ expect_next_instance_of(Project) do |project|
+ expect(project).to receive(:add_import_job)
+ end
project_creator = described_class.new(repo, namespace, user)
project = project_creator.execute
diff --git a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb
index 6fbffc38444..1a2c6ef25c4 100644
--- a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb
+++ b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb
@@ -141,7 +141,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
expect(invalid_gpg_signature.reload.verification_status).to eq 'unverified_key'
# InvalidGpgSignatureUpdater is called by the after_update hook
- user.update_attributes!(email: GpgHelpers::User1.emails.first)
+ user.update!(email: GpgHelpers::User1.emails.first)
expect(invalid_gpg_signature.reload).to have_attributes(
project: project,
@@ -166,7 +166,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
)
# InvalidGpgSignatureUpdater is called by the after_update hook
- user.update_attributes!(email: 'still.unrelated@example.com')
+ user.update!(email: 'still.unrelated@example.com')
expect(invalid_gpg_signature.reload).to have_attributes(
project: project,
diff --git a/spec/lib/gitlab/gpg_spec.rb b/spec/lib/gitlab/gpg_spec.rb
index ab9a166db00..47f37cae98f 100644
--- a/spec/lib/gitlab/gpg_spec.rb
+++ b/spec/lib/gitlab/gpg_spec.rb
@@ -74,6 +74,19 @@ describe Gitlab::Gpg do
email: 'nannie.bernhard@example.com'
}])
end
+
+ it 'rejects non UTF-8 names and addresses' do
+ public_key = double(:key)
+ fingerprints = double(:fingerprints)
+ email = "\xEEch@test.com".force_encoding('ASCII-8BIT')
+ uid = double(:uid, name: 'Test User', email: email)
+ raw_key = double(:raw_key, uids: [uid])
+ allow(Gitlab::Gpg::CurrentKeyChain).to receive(:fingerprints_from_key).with(public_key).and_return(fingerprints)
+ allow(GPGME::Key).to receive(:find).with(:public, anything).and_return([raw_key])
+
+ user_infos = described_class.user_infos_from_key(public_key)
+ expect(user_infos).to eq([])
+ end
end
describe '.current_home_dir' do
diff --git a/spec/lib/gitlab/grape_logging/loggers/queue_duration_logger_spec.rb b/spec/lib/gitlab/grape_logging/loggers/queue_duration_logger_spec.rb
new file mode 100644
index 00000000000..f47b9dd3498
--- /dev/null
+++ b/spec/lib/gitlab/grape_logging/loggers/queue_duration_logger_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe Gitlab::GrapeLogging::Loggers::QueueDurationLogger do
+ subject { described_class.new }
+
+ describe ".parameters" do
+ let(:start_time) { Time.new(2018, 01, 01) }
+
+ describe 'when no proxy time is available' do
+ let(:mock_request) { OpenStruct.new(env: {}) }
+
+ it 'returns an empty hash' do
+ expect(subject.parameters(mock_request, nil)).to eq({})
+ end
+ end
+
+ describe 'when a proxy time is available' do
+ let(:mock_request) do
+ OpenStruct.new(
+ env: {
+ 'HTTP_GITLAB_WORKHORSE_PROXY_START' => (start_time - 1.hour).to_i * (10**9)
+ }
+ )
+ end
+
+ it 'returns the correct duration in ms' do
+ Timecop.freeze(start_time) do
+ subject.before
+
+ expect(subject.parameters(mock_request, nil)).to eq( { 'queue_duration': 1.hour.to_f * 1000 })
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/graphql/connections/keyset_connection_spec.rb b/spec/lib/gitlab/graphql/connections/keyset_connection_spec.rb
new file mode 100644
index 00000000000..96615ae80de
--- /dev/null
+++ b/spec/lib/gitlab/graphql/connections/keyset_connection_spec.rb
@@ -0,0 +1,112 @@
+require 'spec_helper'
+
+describe Gitlab::Graphql::Connections::KeysetConnection do
+ let(:nodes) { Project.all.order(id: :asc) }
+ let(:arguments) { {} }
+ subject(:connection) do
+ described_class.new(nodes, arguments, max_page_size: 3)
+ end
+
+ def encoded_property(value)
+ Base64.strict_encode64(value.to_s)
+ end
+
+ describe '#cursor_from_nodes' do
+ let(:project) { create(:project) }
+
+ it 'returns an encoded ID' do
+ expect(connection.cursor_from_node(project))
+ .to eq(encoded_property(project.id))
+ end
+
+ context 'when an order was specified' do
+ let(:nodes) { Project.order(:updated_at) }
+
+ it 'returns the encoded value of the order' do
+ expect(connection.cursor_from_node(project))
+ .to eq(encoded_property(project.updated_at))
+ end
+ end
+ end
+
+ describe '#sliced_nodes' do
+ let(:projects) { create_list(:project, 4) }
+
+ context 'when before is passed' do
+ let(:arguments) { { before: encoded_property(projects[1].id) } }
+
+ it 'only returns the project before the selected one' do
+ expect(subject.sliced_nodes).to contain_exactly(projects.first)
+ end
+
+ context 'when the sort order is descending' do
+ let(:nodes) { Project.all.order(id: :desc) }
+
+ it 'returns the correct nodes' do
+ expect(subject.sliced_nodes).to contain_exactly(*projects[2..-1])
+ end
+ end
+ end
+
+ context 'when after is passed' do
+ let(:arguments) { { after: encoded_property(projects[1].id) } }
+
+ it 'only returns the project before the selected one' do
+ expect(subject.sliced_nodes).to contain_exactly(*projects[2..-1])
+ end
+
+ context 'when the sort order is descending' do
+ let(:nodes) { Project.all.order(id: :desc) }
+
+ it 'returns the correct nodes' do
+ expect(subject.sliced_nodes).to contain_exactly(projects.first)
+ end
+ end
+ end
+
+ context 'when both before and after are passed' do
+ let(:arguments) do
+ {
+ after: encoded_property(projects[1].id),
+ before: encoded_property(projects[3].id)
+ }
+ end
+
+ it 'returns the expected set' do
+ expect(subject.sliced_nodes).to contain_exactly(projects[2])
+ end
+ end
+ end
+
+ describe '#paged_nodes' do
+ let!(:projects) { create_list(:project, 5) }
+
+ it 'returns the collection limited to max page size' do
+ expect(subject.paged_nodes.size).to eq(3)
+ end
+
+ context 'when `first` is passed' do
+ let(:arguments) { { first: 2 } }
+
+ it 'returns only the first elements' do
+ expect(subject.paged_nodes).to contain_exactly(projects.first, projects.second)
+ end
+ end
+
+ context 'when `last` is passed' do
+ let(:arguments) { { last: 2 } }
+
+ it 'returns only the last elements' do
+ expect(subject.paged_nodes).to contain_exactly(projects[3], projects[4])
+ end
+ end
+
+ context 'when both are passed' do
+ let(:arguments) { { first: 2, last: 2 } }
+
+ it 'raises an error' do
+ expect { subject.paged_nodes }.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/hashed_storage/migrator_spec.rb b/spec/lib/gitlab/hashed_storage/migrator_spec.rb
new file mode 100644
index 00000000000..813ae43b4d3
--- /dev/null
+++ b/spec/lib/gitlab/hashed_storage/migrator_spec.rb
@@ -0,0 +1,75 @@
+require 'spec_helper'
+
+describe Gitlab::HashedStorage::Migrator do
+ describe '#bulk_schedule' do
+ it 'schedules job to StorageMigratorWorker' do
+ Sidekiq::Testing.fake! do
+ expect { subject.bulk_schedule(1, 5) }.to change(StorageMigratorWorker.jobs, :size).by(1)
+ end
+ end
+ end
+
+ describe '#bulk_migrate' do
+ let(:projects) { create_list(:project, 2, :legacy_storage) }
+ let(:ids) { projects.map(&:id) }
+
+ it 'enqueue jobs to ProjectMigrateHashedStorageWorker' do
+ Sidekiq::Testing.fake! do
+ expect { subject.bulk_migrate(ids.min, ids.max) }.to change(ProjectMigrateHashedStorageWorker.jobs, :size).by(2)
+ end
+ end
+
+ it 'sets projects as read only' do
+ allow(ProjectMigrateHashedStorageWorker).to receive(:perform_async).twice
+ subject.bulk_migrate(ids.min, ids.max)
+
+ projects.each do |project|
+ expect(project.reload.repository_read_only?).to be_truthy
+ end
+ end
+
+ it 'rescues and log exceptions' do
+ allow_any_instance_of(Project).to receive(:migrate_to_hashed_storage!).and_raise(StandardError)
+ expect { subject.bulk_migrate(ids.min, ids.max) }.not_to raise_error
+ end
+
+ it 'delegates each project in specified range to #migrate' do
+ projects.each do |project|
+ expect(subject).to receive(:migrate).with(project)
+ end
+
+ subject.bulk_migrate(ids.min, ids.max)
+ end
+ end
+
+ describe '#migrate' do
+ let(:project) { create(:project, :legacy_storage, :empty_repo) }
+
+ it 'enqueues job to ProjectMigrateHashedStorageWorker' do
+ Sidekiq::Testing.fake! do
+ expect { subject.migrate(project) }.to change(ProjectMigrateHashedStorageWorker.jobs, :size).by(1)
+ end
+ end
+
+ it 'rescues and log exceptions' do
+ allow(project).to receive(:migrate_to_hashed_storage!).and_raise(StandardError)
+
+ expect { subject.migrate(project) }.not_to raise_error
+ end
+
+ it 'sets project as read only' do
+ allow(ProjectMigrateHashedStorageWorker).to receive(:perform_async)
+ subject.migrate(project)
+
+ expect(project.reload.repository_read_only?).to be_truthy
+ end
+
+ it 'migrate project' do
+ Sidekiq::Testing.inline! do
+ subject.migrate(project)
+ end
+
+ expect(project.reload.hashed_storage?(:attachments)).to be_truthy
+ end
+ end
+end
diff --git a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb
deleted file mode 100644
index 9dcf272d25e..00000000000
--- a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb
+++ /dev/null
@@ -1,200 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::HealthChecks::FsShardsCheck do
- def command_exists?(command)
- _, status = Gitlab::Popen.popen(%W{ #{command} 1 echo })
- status.zero?
- rescue Errno::ENOENT
- false
- end
-
- def timeout_command
- @timeout_command ||=
- if command_exists?('timeout')
- 'timeout'
- elsif command_exists?('gtimeout')
- 'gtimeout'
- else
- ''
- end
- end
-
- let(:metric_class) { Gitlab::HealthChecks::Metric }
- let(:result_class) { Gitlab::HealthChecks::Result }
- let(:repository_storages) { ['default'] }
- let(:tmp_dir) { Dir.mktmpdir }
-
- let(:storages_paths) do
- {
- default: Gitlab::GitalyClient::StorageSettings.new('path' => tmp_dir)
- }.with_indifferent_access
- end
-
- before do
- allow(described_class).to receive(:repository_storages) { repository_storages }
- allow(described_class).to receive(:storages_paths) { storages_paths }
- stub_const('Gitlab::HealthChecks::FsShardsCheck::TIMEOUT_EXECUTABLE', timeout_command)
- end
-
- after do
- FileUtils.remove_entry_secure(tmp_dir) if Dir.exist?(tmp_dir)
- end
-
- shared_examples 'filesystem checks' do
- describe '#readiness' do
- subject { described_class.readiness }
-
- context 'storage has a tripped circuitbreaker', :broken_storage do
- let(:repository_storages) { ['broken'] }
- let(:storages_paths) do
- Gitlab.config.repositories.storages
- end
-
- it { is_expected.to include(result_class.new(false, 'circuitbreaker tripped', shard: 'broken')) }
- end
-
- context 'storage points to not existing folder' do
- let(:storages_paths) do
- {
- default: Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/this/path/doesnt/exist')
- }.with_indifferent_access
- end
-
- before do
- allow(described_class).to receive(:storage_circuitbreaker_test) { true }
- end
-
- it { is_expected.to include(result_class.new(false, 'cannot stat storage', shard: 'default')) }
- end
-
- context 'storage points to directory that has both read and write rights' do
- before do
- FileUtils.chmod_R(0755, tmp_dir)
- end
-
- it { is_expected.to include(result_class.new(true, nil, shard: 'default')) }
-
- it 'cleans up files used for testing' do
- expect(described_class).to receive(:storage_write_test).with(any_args).and_call_original
-
- expect { subject }.not_to change(Dir.entries(tmp_dir), :count)
- end
-
- context 'read test fails' do
- before do
- allow(described_class).to receive(:storage_read_test).with(any_args).and_return(false)
- end
-
- it { is_expected.to include(result_class.new(false, 'cannot read from storage', shard: 'default')) }
- end
-
- context 'write test fails' do
- before do
- allow(described_class).to receive(:storage_write_test).with(any_args).and_return(false)
- end
-
- it { is_expected.to include(result_class.new(false, 'cannot write to storage', shard: 'default')) }
- end
- end
- end
-
- describe '#metrics' do
- context 'storage points to not existing folder' do
- let(:storages_paths) do
- {
- default: Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/this/path/doesnt/exist')
- }.with_indifferent_access
- end
-
- it 'provides metrics' do
- metrics = described_class.metrics
-
- expect(metrics).to all(have_attributes(labels: { shard: 'default' }))
- expect(metrics).to include(an_object_having_attributes(name: :filesystem_accessible, value: 0))
- expect(metrics).to include(an_object_having_attributes(name: :filesystem_readable, value: 0))
- expect(metrics).to include(an_object_having_attributes(name: :filesystem_writable, value: 0))
- expect(metrics).to include(an_object_having_attributes(name: :filesystem_access_latency_seconds, value: be >= 0))
- expect(metrics).to include(an_object_having_attributes(name: :filesystem_read_latency_seconds, value: be >= 0))
- expect(metrics).to include(an_object_having_attributes(name: :filesystem_write_latency_seconds, value: be >= 0))
- expect(metrics).to include(an_object_having_attributes(name: :filesystem_circuitbreaker_latency_seconds, value: be >= 0))
- end
- end
-
- context 'storage points to directory that has both read and write rights' do
- before do
- FileUtils.chmod_R(0755, tmp_dir)
- end
-
- it 'provides metrics' do
- metrics = described_class.metrics
-
- expect(metrics).to all(have_attributes(labels: { shard: 'default' }))
- expect(metrics).to include(an_object_having_attributes(name: :filesystem_accessible, value: 1))
- expect(metrics).to include(an_object_having_attributes(name: :filesystem_readable, value: 1))
- expect(metrics).to include(an_object_having_attributes(name: :filesystem_writable, value: 1))
- expect(metrics).to include(an_object_having_attributes(name: :filesystem_access_latency_seconds, value: be >= 0))
- expect(metrics).to include(an_object_having_attributes(name: :filesystem_read_latency_seconds, value: be >= 0))
- expect(metrics).to include(an_object_having_attributes(name: :filesystem_write_latency_seconds, value: be >= 0))
- expect(metrics).to include(an_object_having_attributes(name: :filesystem_circuitbreaker_latency_seconds, value: be >= 0))
- end
-
- it 'cleans up files used for metrics' do
- expect { described_class.metrics }.not_to change(Dir.entries(tmp_dir), :count)
- end
- end
- end
- end
-
- context 'when timeout kills fs checks' do
- before do
- stub_const('Gitlab::HealthChecks::FsShardsCheck::COMMAND_TIMEOUT', '1')
-
- allow(described_class).to receive(:exec_with_timeout).and_wrap_original { |m| m.call(%w(sleep 60)) }
- FileUtils.chmod_R(0755, tmp_dir)
- end
-
- describe '#readiness' do
- subject { described_class.readiness }
-
- it { is_expected.to include(result_class.new(false, 'cannot stat storage', shard: 'default')) }
- end
-
- describe '#metrics' do
- it 'provides metrics' do
- metrics = described_class.metrics
-
- expect(metrics).to all(have_attributes(labels: { shard: 'default' }))
- expect(metrics).to include(an_object_having_attributes(name: :filesystem_accessible, value: 0))
- expect(metrics).to include(an_object_having_attributes(name: :filesystem_readable, value: 0))
- expect(metrics).to include(an_object_having_attributes(name: :filesystem_writable, value: 0))
- expect(metrics).to include(an_object_having_attributes(name: :filesystem_access_latency_seconds, value: be >= 0))
- expect(metrics).to include(an_object_having_attributes(name: :filesystem_read_latency_seconds, value: be >= 0))
- expect(metrics).to include(an_object_having_attributes(name: :filesystem_write_latency_seconds, value: be >= 0))
- end
- end
- end
-
- context 'when popen always finds required binaries' do
- before do
- allow(described_class).to receive(:exec_with_timeout).and_wrap_original do |method, *args, &block|
- begin
- method.call(*args, &block)
- rescue RuntimeError, Errno::ENOENT
- raise 'expected not to happen'
- end
- end
-
- stub_const('Gitlab::HealthChecks::FsShardsCheck::COMMAND_TIMEOUT', '10')
- end
-
- it_behaves_like 'filesystem checks'
- end
-
- context 'when popen never finds required binaries' do
- before do
- allow(Gitlab::Popen).to receive(:popen).and_raise(Errno::ENOENT)
- end
-
- it_behaves_like 'filesystem checks'
- end
-end
diff --git a/spec/lib/gitlab/health_checks/gitaly_check_spec.rb b/spec/lib/gitlab/health_checks/gitaly_check_spec.rb
index 724beefff69..4912cd48761 100644
--- a/spec/lib/gitlab/health_checks/gitaly_check_spec.rb
+++ b/spec/lib/gitlab/health_checks/gitaly_check_spec.rb
@@ -30,13 +30,14 @@ describe Gitlab::HealthChecks::GitalyCheck do
describe '#metrics' do
subject { described_class.metrics }
+ let(:server) { double(storage: 'default', read_writeable?: up) }
before do
- expect(Gitlab::GitalyClient::HealthCheckService).to receive(:new).and_return(gitaly_check)
+ allow(Gitaly::Server).to receive(:new).and_return(server)
end
context 'Gitaly server is up' do
- let(:gitaly_check) { double(check: { success: true }) }
+ let(:up) { true }
it 'provides metrics' do
expect(subject).to all(have_attributes(labels: { shard: 'default' }))
@@ -46,7 +47,7 @@ describe Gitlab::HealthChecks::GitalyCheck do
end
context 'Gitaly server is down' do
- let(:gitaly_check) { double(check: { success: false, message: 'Connection refused' }) }
+ let(:up) { false }
it 'provides metrics' do
expect(subject).to include(an_object_having_attributes(name: 'gitaly_health_check_success', value: 0))
diff --git a/spec/lib/gitlab/http_io_spec.rb b/spec/lib/gitlab/http_io_spec.rb
new file mode 100644
index 00000000000..788bddb8f59
--- /dev/null
+++ b/spec/lib/gitlab/http_io_spec.rb
@@ -0,0 +1,318 @@
+require 'spec_helper'
+
+describe Gitlab::HttpIO do
+ include HttpIOHelpers
+
+ let(:http_io) { described_class.new(url, size) }
+
+ let(:url) { 'http://object-storage/trace' }
+ let(:file_path) { expand_fixture_path('trace/sample_trace') }
+ let(:file_body) { File.read(file_path).force_encoding(Encoding::BINARY) }
+ let(:size) { File.size(file_path) }
+
+ describe '#close' do
+ subject { http_io.close }
+
+ it { is_expected.to be_nil }
+ end
+
+ describe '#binmode' do
+ subject { http_io.binmode }
+
+ it { is_expected.to be_nil }
+ end
+
+ describe '#binmode?' do
+ subject { http_io.binmode? }
+
+ it { is_expected.to be_truthy }
+ end
+
+ describe '#path' do
+ subject { http_io.path }
+
+ it { is_expected.to be_nil }
+ end
+
+ describe '#url' do
+ subject { http_io.url }
+
+ it { is_expected.to eq(url) }
+ end
+
+ describe '#seek' do
+ subject { http_io.seek(pos, where) }
+
+ context 'when moves pos to end of the file' do
+ let(:pos) { 0 }
+ let(:where) { IO::SEEK_END }
+
+ it { is_expected.to eq(size) }
+ end
+
+ context 'when moves pos to middle of the file' do
+ let(:pos) { size / 2 }
+ let(:where) { IO::SEEK_SET }
+
+ it { is_expected.to eq(size / 2) }
+ end
+
+ context 'when moves pos around' do
+ it 'matches the result' do
+ expect(http_io.seek(0)).to eq(0)
+ expect(http_io.seek(100, IO::SEEK_CUR)).to eq(100)
+ expect { http_io.seek(size + 1, IO::SEEK_CUR) }.to raise_error('new position is outside of file')
+ end
+ end
+ end
+
+ describe '#eof?' do
+ subject { http_io.eof? }
+
+ context 'when current pos is at end of the file' do
+ before do
+ http_io.seek(size, IO::SEEK_SET)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when current pos is not at end of the file' do
+ before do
+ http_io.seek(0, IO::SEEK_SET)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#each_line' do
+ subject { http_io.each_line }
+
+ let(:string_io) { StringIO.new(file_body) }
+
+ before do
+ stub_remote_url_206(url, file_path)
+ end
+
+ it 'yields lines' do
+ expect { |b| http_io.each_line(&b) }.to yield_successive_args(*string_io.each_line.to_a)
+ end
+
+ context 'when buckets on GCS' do
+ context 'when BUFFER_SIZE is larger than file size' do
+ before do
+ stub_remote_url_200(url, file_path)
+ set_larger_buffer_size_than(size)
+ end
+
+ it 'calls get_chunk only once' do
+ expect_any_instance_of(Net::HTTP).to receive(:request).once.and_call_original
+
+ http_io.each_line { |line| }
+ end
+ end
+ end
+ end
+
+ describe '#read' do
+ subject { http_io.read(length) }
+
+ context 'when there are no network issue' do
+ before do
+ stub_remote_url_206(url, file_path)
+ end
+
+ context 'when read whole size' do
+ let(:length) { nil }
+
+ context 'when BUFFER_SIZE is smaller than file size' do
+ before do
+ set_smaller_buffer_size_than(size)
+ end
+
+ it 'reads a trace' do
+ is_expected.to eq(file_body)
+ end
+ end
+
+ context 'when BUFFER_SIZE is larger than file size' do
+ before do
+ set_larger_buffer_size_than(size)
+ end
+
+ it 'reads a trace' do
+ is_expected.to eq(file_body)
+ end
+ end
+ end
+
+ context 'when read only first 100 bytes' do
+ let(:length) { 100 }
+
+ context 'when BUFFER_SIZE is smaller than file size' do
+ before do
+ set_smaller_buffer_size_than(size)
+ end
+
+ it 'reads a trace' do
+ is_expected.to eq(file_body[0, length])
+ end
+ end
+
+ context 'when BUFFER_SIZE is larger than file size' do
+ before do
+ set_larger_buffer_size_than(size)
+ end
+
+ it 'reads a trace' do
+ is_expected.to eq(file_body[0, length])
+ end
+ end
+ end
+
+ context 'when tries to read oversize' do
+ let(:length) { size + 1000 }
+
+ context 'when BUFFER_SIZE is smaller than file size' do
+ before do
+ set_smaller_buffer_size_than(size)
+ end
+
+ it 'reads a trace' do
+ is_expected.to eq(file_body)
+ end
+ end
+
+ context 'when BUFFER_SIZE is larger than file size' do
+ before do
+ set_larger_buffer_size_than(size)
+ end
+
+ it 'reads a trace' do
+ is_expected.to eq(file_body)
+ end
+ end
+ end
+
+ context 'when tries to read 0 bytes' do
+ let(:length) { 0 }
+
+ context 'when BUFFER_SIZE is smaller than file size' do
+ before do
+ set_smaller_buffer_size_than(size)
+ end
+
+ it 'reads a trace' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when BUFFER_SIZE is larger than file size' do
+ before do
+ set_larger_buffer_size_than(size)
+ end
+
+ it 'reads a trace' do
+ is_expected.to be_empty
+ end
+ end
+ end
+ end
+
+ context 'when there is anetwork issue' do
+ let(:length) { nil }
+
+ before do
+ stub_remote_url_500(url)
+ end
+
+ it 'reads a trace' do
+ expect { subject }.to raise_error(Gitlab::HttpIO::FailedToGetChunkError)
+ end
+ end
+ end
+
+ describe '#readline' do
+ subject { http_io.readline }
+
+ let(:string_io) { StringIO.new(file_body) }
+
+ before do
+ stub_remote_url_206(url, file_path)
+ end
+
+ shared_examples 'all line matching' do
+ it 'reads a line' do
+ (0...file_body.lines.count).each do
+ expect(http_io.readline).to eq(string_io.readline)
+ end
+ end
+ end
+
+ context 'when there is anetwork issue' do
+ let(:length) { nil }
+
+ before do
+ stub_remote_url_500(url)
+ end
+
+ it 'reads a trace' do
+ expect { subject }.to raise_error(Gitlab::HttpIO::FailedToGetChunkError)
+ end
+ end
+
+ context 'when BUFFER_SIZE is smaller than file size' do
+ before do
+ set_smaller_buffer_size_than(size)
+ end
+
+ it_behaves_like 'all line matching'
+ end
+
+ context 'when BUFFER_SIZE is larger than file size' do
+ before do
+ set_larger_buffer_size_than(size)
+ end
+
+ it_behaves_like 'all line matching'
+ end
+
+ context 'when pos is at middle of the file' do
+ before do
+ set_smaller_buffer_size_than(size)
+
+ http_io.seek(size / 2)
+ string_io.seek(size / 2)
+ end
+
+ it 'reads from pos' do
+ expect(http_io.readline).to eq(string_io.readline)
+ end
+ end
+ end
+
+ describe '#write' do
+ subject { http_io.write(nil) }
+
+ it { expect { subject }.to raise_error(NotImplementedError) }
+ end
+
+ describe '#truncate' do
+ subject { http_io.truncate(nil) }
+
+ it { expect { subject }.to raise_error(NotImplementedError) }
+ end
+
+ describe '#flush' do
+ subject { http_io.flush }
+
+ it { expect { subject }.to raise_error(NotImplementedError) }
+ end
+
+ describe '#present?' do
+ subject { http_io.present? }
+
+ it { is_expected.to be_truthy }
+ end
+end
diff --git a/spec/lib/gitlab/i18n/metadata_entry_spec.rb b/spec/lib/gitlab/i18n/metadata_entry_spec.rb
index ab71d6454a9..a399517cc04 100644
--- a/spec/lib/gitlab/i18n/metadata_entry_spec.rb
+++ b/spec/lib/gitlab/i18n/metadata_entry_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::I18n::MetadataEntry do
- describe '#expected_plurals' do
+ describe '#expected_forms' do
it 'returns the number of plurals' do
data = {
msgid: "",
@@ -22,7 +22,7 @@ describe Gitlab::I18n::MetadataEntry do
}
entry = described_class.new(data)
- expect(entry.expected_plurals).to eq(2)
+ expect(entry.expected_forms).to eq(2)
end
it 'returns 0 for the POT-metadata' do
@@ -45,7 +45,7 @@ describe Gitlab::I18n::MetadataEntry do
}
entry = described_class.new(data)
- expect(entry.expected_plurals).to eq(0)
+ expect(entry.expected_forms).to eq(0)
end
end
end
diff --git a/spec/lib/gitlab/i18n/po_linter_spec.rb b/spec/lib/gitlab/i18n/po_linter_spec.rb
index 3a962ba7f22..3dbc23d2aaf 100644
--- a/spec/lib/gitlab/i18n/po_linter_spec.rb
+++ b/spec/lib/gitlab/i18n/po_linter_spec.rb
@@ -1,10 +1,31 @@
require 'spec_helper'
require 'simple_po_parser'
+# Disabling this cop to allow for multi-language examples in comments
+# rubocop:disable Style/AsciiComments
describe Gitlab::I18n::PoLinter do
let(:linter) { described_class.new(po_path) }
let(:po_path) { 'spec/fixtures/valid.po' }
+ def fake_translation(msgid:, translation:, plural_id: nil, plurals: [])
+ data = { msgid: msgid, msgid_plural: plural_id }
+
+ if plural_id
+ [translation, *plurals].each_with_index do |plural, index|
+ allow(FastGettext::Translation).to receive(:n_).with(msgid, plural_id, index).and_return(plural)
+ data.merge!("msgstr[#{index}]" => plural)
+ end
+ else
+ allow(FastGettext::Translation).to receive(:_).with(msgid).and_return(translation)
+ data[:msgstr] = translation
+ end
+
+ Gitlab::I18n::TranslationEntry.new(
+ data,
+ plurals.size + 1
+ )
+ end
+
describe '#errors' do
it 'only calls validation once' do
expect(linter).to receive(:validate_po).once.and_call_original
@@ -155,9 +176,8 @@ describe Gitlab::I18n::PoLinter do
describe '#validate_entries' do
it 'keeps track of errors for entries' do
- fake_invalid_entry = Gitlab::I18n::TranslationEntry.new(
- { msgid: "Hello %{world}", msgstr: "Bonjour %{monde}" }, 2
- )
+ fake_invalid_entry = fake_translation(msgid: "Hello %{world}",
+ translation: "Bonjour %{monde}")
allow(linter).to receive(:translation_entries) { [fake_invalid_entry] }
expect(linter).to receive(:validate_entry)
@@ -177,6 +197,7 @@ describe Gitlab::I18n::PoLinter do
expect(linter).to receive(:validate_newlines).with([], fake_entry)
expect(linter).to receive(:validate_number_of_plurals).with([], fake_entry)
expect(linter).to receive(:validate_unescaped_chars).with([], fake_entry)
+ expect(linter).to receive(:validate_translation).with([], fake_entry)
linter.validate_entry(fake_entry)
end
@@ -185,7 +206,7 @@ describe Gitlab::I18n::PoLinter do
describe '#validate_number_of_plurals' do
it 'validates when there are an incorrect number of translations' do
fake_metadata = double
- allow(fake_metadata).to receive(:expected_plurals).and_return(2)
+ allow(fake_metadata).to receive(:expected_forms).and_return(2)
allow(linter).to receive(:metadata_entry).and_return(fake_metadata)
fake_entry = Gitlab::I18n::TranslationEntry.new(
@@ -201,13 +222,16 @@ describe Gitlab::I18n::PoLinter do
end
describe '#validate_variables' do
- it 'validates both signular and plural in a pluralized string when the entry has a singular' do
- pluralized_entry = Gitlab::I18n::TranslationEntry.new(
- { msgid: 'Hello %{world}',
- msgid_plural: 'Hello all %{world}',
- 'msgstr[0]' => 'Bonjour %{world}',
- 'msgstr[1]' => 'Bonjour tous %{world}' },
- 2
+ before do
+ allow(linter).to receive(:validate_variables_in_message).and_call_original
+ end
+
+ it 'validates both singular and plural in a pluralized string when the entry has a singular' do
+ pluralized_entry = fake_translation(
+ msgid: 'Hello %{world}',
+ translation: 'Bonjour %{world}',
+ plural_id: 'Hello all %{world}',
+ plurals: ['Bonjour tous %{world}']
)
expect(linter).to receive(:validate_variables_in_message)
@@ -221,11 +245,10 @@ describe Gitlab::I18n::PoLinter do
end
it 'only validates plural when there is no separate singular' do
- pluralized_entry = Gitlab::I18n::TranslationEntry.new(
- { msgid: 'Hello %{world}',
- msgid_plural: 'Hello all %{world}',
- 'msgstr[0]' => 'Bonjour %{world}' },
- 1
+ pluralized_entry = fake_translation(
+ msgid: 'Hello %{world}',
+ translation: 'Bonjour %{world}',
+ plural_id: 'Hello all %{world}'
)
expect(linter).to receive(:validate_variables_in_message)
@@ -235,37 +258,65 @@ describe Gitlab::I18n::PoLinter do
end
it 'validates the message variables' do
- entry = Gitlab::I18n::TranslationEntry.new(
- { msgid: 'Hello', msgstr: 'Bonjour' },
- 2
- )
+ entry = fake_translation(msgid: 'Hello', translation: 'Bonjour')
expect(linter).to receive(:validate_variables_in_message)
.with([], 'Hello', 'Bonjour')
linter.validate_variables([], entry)
end
+
+ it 'validates variable usage in message ids' do
+ entry = fake_translation(
+ msgid: 'Hello %{world}',
+ translation: 'Bonjour %{world}',
+ plural_id: 'Hello all %{world}',
+ plurals: ['Bonjour tous %{world}']
+ )
+
+ expect(linter).to receive(:validate_variables_in_message)
+ .with([], 'Hello %{world}', 'Hello %{world}')
+ .and_call_original
+ expect(linter).to receive(:validate_variables_in_message)
+ .with([], 'Hello all %{world}', 'Hello all %{world}')
+ .and_call_original
+
+ linter.validate_variables([], entry)
+ end
end
describe '#validate_variables_in_message' do
it 'detects when a variables are used incorrectly' do
errors = []
- expected_errors = ['<hello %{world} %d> is missing: [%{hello}]',
- '<hello %{world} %d> is using unknown variables: [%{world}]',
- 'is combining multiple unnamed variables']
+ expected_errors = ['<%d hello %{world} %s> is missing: [%{hello}]',
+ '<%d hello %{world} %s> is using unknown variables: [%{world}]',
+ 'is combining multiple unnamed variables',
+ 'is combining named variables with unnamed variables']
- linter.validate_variables_in_message(errors, '%{hello} world %d', 'hello %{world} %d')
+ linter.validate_variables_in_message(errors, '%d %{hello} world %s', '%d hello %{world} %s')
expect(errors).to include(*expected_errors)
end
+
+ it 'does not allow combining 1 `%d` unnamed variable with named variables' do
+ errors = []
+
+ linter.validate_variables_in_message(errors,
+ '%{type} detected %d vulnerability',
+ '%{type} detecteerde %d kwetsbaarheid')
+
+ expect(errors).not_to be_empty
+ end
end
describe '#validate_translation' do
+ let(:entry) { fake_translation(msgid: 'Hello %{world}', translation: 'Bonjour %{world}') }
+
it 'succeeds with valid variables' do
errors = []
- linter.validate_translation(errors, 'Hello %{world}', ['%{world}'])
+ linter.validate_translation(errors, entry)
expect(errors).to be_empty
end
@@ -275,43 +326,80 @@ describe Gitlab::I18n::PoLinter do
expect(FastGettext::Translation).to receive(:_) { raise 'broken' }
- linter.validate_translation(errors, 'Hello', [])
+ linter.validate_translation(errors, entry)
- expect(errors).to include('Failure translating to en with []: broken')
+ expect(errors).to include('Failure translating to en: broken')
end
it 'adds an error message when translating fails when translating with context' do
+ entry = fake_translation(msgid: 'Tests|Hello', translation: 'broken')
errors = []
expect(FastGettext::Translation).to receive(:s_) { raise 'broken' }
- linter.validate_translation(errors, 'Tests|Hello', [])
+ linter.validate_translation(errors, entry)
- expect(errors).to include('Failure translating to en with []: broken')
+ expect(errors).to include('Failure translating to en: broken')
end
it "adds an error when trying to translate with incorrect variables when using unnamed variables" do
+ entry = fake_translation(msgid: 'Hello %s', translation: 'Hello %d')
errors = []
- linter.validate_translation(errors, 'Hello %d', ['%s'])
+ linter.validate_translation(errors, entry)
- expect(errors.first).to start_with("Failure translating to en with")
+ expect(errors.first).to start_with("Failure translating to en")
end
it "adds an error when trying to translate with named variables when unnamed variables are expected" do
+ entry = fake_translation(msgid: 'Hello %s', translation: 'Hello %{thing}')
errors = []
- linter.validate_translation(errors, 'Hello %d', ['%{world}'])
+ linter.validate_translation(errors, entry)
- expect(errors.first).to start_with("Failure translating to en with")
+ expect(errors.first).to start_with("Failure translating to en")
end
- it 'adds an error when translated with incorrect variables using named variables' do
- errors = []
+ it 'tests translation for all given forms' do
+ # Fake a language that has 3 forms to translate
+ fake_metadata = double
+ allow(fake_metadata).to receive(:forms_to_test).and_return(3)
+ allow(linter).to receive(:metadata_entry).and_return(fake_metadata)
+ entry = fake_translation(
+ msgid: '%d exception',
+ translation: '%d uitzondering',
+ plural_id: '%d exceptions',
+ plurals: ['%d uitzonderingen', '%d uitzonderingetjes']
+ )
+
+ # Make each count use a different index
+ allow(linter).to receive(:index_for_pluralization).with(0).and_return(0)
+ allow(linter).to receive(:index_for_pluralization).with(1).and_return(1)
+ allow(linter).to receive(:index_for_pluralization).with(2).and_return(2)
+
+ expect(FastGettext::Translation).to receive(:n_).with('%d exception', '%d exceptions', 0).and_call_original
+ expect(FastGettext::Translation).to receive(:n_).with('%d exception', '%d exceptions', 1).and_call_original
+ expect(FastGettext::Translation).to receive(:n_).with('%d exception', '%d exceptions', 2).and_call_original
+
+ linter.validate_translation([], entry)
+ end
+ end
+
+ describe '#numbers_covering_all_plurals' do
+ it 'can correctly find all required numbers to translate to Polish' do
+ # Polish used as an example with 3 different forms:
+ # 0, all plurals except the ones ending in 2,3,4: Kotów
+ # 1: Kot
+ # 2-3-4: Koty
+ # So translating with [0, 1, 2] will give us all different posibilities
+ fake_metadata = double
+ allow(fake_metadata).to receive(:forms_to_test).and_return(4)
+ allow(linter).to receive(:metadata_entry).and_return(fake_metadata)
+ allow(linter).to receive(:locale).and_return('pl_PL')
- linter.validate_translation(errors, 'Hello %{thing}', ['%d'])
+ numbers = linter.numbers_covering_all_plurals
- expect(errors.first).to start_with("Failure translating to en with")
+ expect(numbers).to contain_exactly(0, 1, 2)
end
end
@@ -336,3 +424,4 @@ describe Gitlab::I18n::PoLinter do
end
end
end
+# rubocop:enable Style/AsciiComments
diff --git a/spec/lib/gitlab/i18n/translation_entry_spec.rb b/spec/lib/gitlab/i18n/translation_entry_spec.rb
index f68bc8feff9..b301e6ea443 100644
--- a/spec/lib/gitlab/i18n/translation_entry_spec.rb
+++ b/spec/lib/gitlab/i18n/translation_entry_spec.rb
@@ -109,7 +109,7 @@ describe Gitlab::I18n::TranslationEntry do
data = { msgid: %w(hello world) }
entry = described_class.new(data, 2)
- expect(entry.msgid_contains_newlines?).to be_truthy
+ expect(entry.msgid_has_multiple_lines?).to be_truthy
end
end
@@ -118,7 +118,7 @@ describe Gitlab::I18n::TranslationEntry do
data = { msgid_plural: %w(hello world) }
entry = described_class.new(data, 2)
- expect(entry.plural_id_contains_newlines?).to be_truthy
+ expect(entry.plural_id_has_multiple_lines?).to be_truthy
end
end
@@ -127,7 +127,7 @@ describe Gitlab::I18n::TranslationEntry do
data = { msgstr: %w(hello world) }
entry = described_class.new(data, 2)
- expect(entry.translations_contain_newlines?).to be_truthy
+ expect(entry.translations_have_multiple_lines?).to be_truthy
end
end
diff --git a/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_object_storage_spec.rb b/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_object_storage_spec.rb
new file mode 100644
index 00000000000..5059d68e54b
--- /dev/null
+++ b/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_object_storage_spec.rb
@@ -0,0 +1,105 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::AfterExportStrategies::BaseAfterExportStrategy do
+ let!(:service) { described_class.new }
+ let!(:project) { create(:project, :with_object_export) }
+ let(:shared) { project.import_export_shared }
+ let!(:user) { create(:user) }
+
+ describe '#execute' do
+ before do
+ allow(service).to receive(:strategy_execute)
+ stub_feature_flags(import_export_object_storage: true)
+ end
+
+ it 'returns if project exported file is not found' do
+ allow(project).to receive(:export_project_object_exists?).and_return(false)
+
+ expect(service).not_to receive(:strategy_execute)
+
+ service.execute(user, project)
+ end
+
+ it 'creates a lock file in the export dir' do
+ allow(service).to receive(:delete_after_export_lock)
+
+ service.execute(user, project)
+
+ expect(lock_path_exist?).to be_truthy
+ end
+
+ context 'when the method succeeds' do
+ it 'removes the lock file' do
+ service.execute(user, project)
+
+ expect(lock_path_exist?).to be_falsey
+ end
+ end
+
+ context 'when the method fails' do
+ before do
+ allow(service).to receive(:strategy_execute).and_call_original
+ end
+
+ context 'when validation fails' do
+ before do
+ allow(service).to receive(:invalid?).and_return(true)
+ end
+
+ it 'does not create the lock file' do
+ expect(service).not_to receive(:create_or_update_after_export_lock)
+
+ service.execute(user, project)
+ end
+
+ it 'does not execute main logic' do
+ expect(service).not_to receive(:strategy_execute)
+
+ service.execute(user, project)
+ end
+
+ it 'logs validation errors in shared context' do
+ expect(service).to receive(:log_validation_errors)
+
+ service.execute(user, project)
+ end
+ end
+
+ context 'when an exception is raised' do
+ it 'removes the lock' do
+ expect { service.execute(user, project) }.to raise_error(NotImplementedError)
+
+ expect(lock_path_exist?).to be_falsey
+ end
+ end
+ end
+ end
+
+ describe '#log_validation_errors' do
+ it 'add the message to the shared context' do
+ errors = %w(test_message test_message2)
+
+ allow(service).to receive(:invalid?).and_return(true)
+ allow(service.errors).to receive(:full_messages).and_return(errors)
+
+ expect(shared).to receive(:add_error_message).twice.and_call_original
+
+ service.execute(user, project)
+
+ expect(shared.errors).to eq errors
+ end
+ end
+
+ describe '#to_json' do
+ it 'adds the current strategy class to the serialized attributes' do
+ params = { param1: 1 }
+ result = params.merge(klass: described_class.to_s).to_json
+
+ expect(described_class.new(params).to_json).to eq result
+ end
+ end
+
+ def lock_path_exist?
+ File.exist?(described_class.lock_file_path(project))
+ end
+end
diff --git a/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb b/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb
index ed54d87de4a..566b7f46c87 100644
--- a/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb
+++ b/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb
@@ -9,6 +9,7 @@ describe Gitlab::ImportExport::AfterExportStrategies::BaseAfterExportStrategy do
describe '#execute' do
before do
allow(service).to receive(:strategy_execute)
+ stub_feature_flags(import_export_object_storage: false)
end
it 'returns if project exported file is not found' do
diff --git a/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb b/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb
index 5fe57d9987b..7f2e0a4ee2c 100644
--- a/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb
+++ b/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb
@@ -24,13 +24,34 @@ describe Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy do
end
describe '#execute' do
- it 'removes the exported project file after the upload' do
- allow(strategy).to receive(:send_file)
- allow(strategy).to receive(:handle_response_error)
+ context 'without object storage' do
+ before do
+ stub_feature_flags(import_export_object_storage: false)
+ end
+
+ it 'removes the exported project file after the upload' do
+ allow(strategy).to receive(:send_file)
+ allow(strategy).to receive(:handle_response_error)
+
+ expect(project).to receive(:remove_exported_project_file)
+
+ strategy.execute(user, project)
+ end
+ end
+
+ context 'with object storage' do
+ before do
+ stub_feature_flags(import_export_object_storage: true)
+ end
- expect(project).to receive(:remove_exported_project_file)
+ it 'removes the exported project file after the upload' do
+ allow(strategy).to receive(:send_file)
+ allow(strategy).to receive(:handle_response_error)
- strategy.execute(user, project)
+ expect(project).to receive(:remove_exported_project_file)
+
+ strategy.execute(user, project)
+ end
end
end
end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 8b46b04b8b5..084ce3066d6 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -35,6 +35,7 @@ notes:
- todos
- events
- system_note_metadata
+- note_diff_file
label_links:
- target
- label
@@ -256,6 +257,7 @@ project:
- import_data
- commit_statuses
- pipelines
+- stages
- builds
- runner_projects
- runners
@@ -291,6 +293,7 @@ project:
- deploy_tokens
- settings
- ci_cd_settings
+- import_export_upload
award_emoji:
- awardable
- user
@@ -309,3 +312,8 @@ lfs_file_locks:
- user
project_badges:
- project
+metrics:
+- merge_request
+- latest_closed_by
+- merged_by
+- pipeline
diff --git a/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb b/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb
index cd5a1b2982b..536cc359d39 100644
--- a/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb
+++ b/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb
@@ -15,7 +15,10 @@ describe Gitlab::ImportExport::AttributeCleaner do
'project_id' => 99,
'user_id' => 99,
'random_id_in_the_middle' => 99,
- 'notid' => 99
+ 'notid' => 99,
+ 'import_source' => 'whatever',
+ 'import_type' => 'whatever',
+ 'non_existent_attr' => 'whatever'
}
end
@@ -28,10 +31,30 @@ describe Gitlab::ImportExport::AttributeCleaner do
}
end
+ let(:excluded_keys) { %w[import_source import_type] }
+
+ subject { described_class.clean(relation_hash: unsafe_hash, relation_class: relation_class, excluded_keys: excluded_keys) }
+
+ before do
+ allow(relation_class).to receive(:attribute_method?).and_return(true)
+ allow(relation_class).to receive(:attribute_method?).with('non_existent_attr').and_return(false)
+ end
+
it 'removes unwanted attributes from the hash' do
- # allow(relation_class).to receive(:attribute_method?).and_return(true)
+ expect(subject).to eq(post_safe_hash)
+ end
+
+ it 'removes attributes not present in relation_class' do
+ expect(subject.keys).not_to include 'non_existent_attr'
+ end
+
+ it 'removes excluded keys from the hash' do
+ expect(subject.keys).not_to include excluded_keys
+ end
+
+ it 'does not remove excluded key if not listed' do
parsed_hash = described_class.clean(relation_hash: unsafe_hash, relation_class: relation_class)
- expect(parsed_hash).to eq(post_safe_hash)
+ expect(parsed_hash.keys).to eq post_safe_hash.keys + excluded_keys
end
end
diff --git a/spec/lib/gitlab/import_export/avatar_saver_spec.rb b/spec/lib/gitlab/import_export/avatar_saver_spec.rb
index f40d4bc2d08..2223f163177 100644
--- a/spec/lib/gitlab/import_export/avatar_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/avatar_saver_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Gitlab::ImportExport::AvatarSaver do
let(:shared) { project.import_export_shared }
let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
- let(:project_with_avatar) { create(:project, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
+ let(:project_with_avatar) { create(:project, avatar: fixture_file_upload("spec/fixtures/dk.png", "image/png")) }
let(:project) { create(:project) }
before do
diff --git a/spec/lib/gitlab/import_export/fork_spec.rb b/spec/lib/gitlab/import_export/fork_spec.rb
index 17e06a6a83f..71fd5a51c3b 100644
--- a/spec/lib/gitlab/import_export/fork_spec.rb
+++ b/spec/lib/gitlab/import_export/fork_spec.rb
@@ -41,8 +41,10 @@ describe 'forked project import' do
after do
FileUtils.rm_rf(export_path)
- FileUtils.rm_rf(project_with_repo.repository.path_to_repo)
- FileUtils.rm_rf(project.repository.path_to_repo)
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ FileUtils.rm_rf(project_with_repo.repository.path_to_repo)
+ FileUtils.rm_rf(project.repository.path_to_repo)
+ end
end
it 'can access the MR' do
diff --git a/spec/lib/gitlab/import_export/group_project_object_builder_spec.rb b/spec/lib/gitlab/import_export/group_project_object_builder_spec.rb
new file mode 100644
index 00000000000..6a803c48b34
--- /dev/null
+++ b/spec/lib/gitlab/import_export/group_project_object_builder_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::GroupProjectObjectBuilder do
+ let(:project) do
+ create(:project,
+ :builds_disabled,
+ :issues_disabled,
+ name: 'project',
+ path: 'project',
+ group: create(:group))
+ end
+
+ context 'labels' do
+ it 'finds the right group label' do
+ group_label = create(:group_label, 'name': 'group label', 'group': project.group)
+
+ expect(described_class.build(Label,
+ 'title' => 'group label',
+ 'project' => project,
+ 'group' => project.group)).to eq(group_label)
+ end
+
+ it 'creates a new label' do
+ label = described_class.build(Label,
+ 'title' => 'group label',
+ 'project' => project,
+ 'group' => project.group)
+
+ expect(label.persisted?).to be true
+ end
+ end
+
+ context 'milestones' do
+ it 'finds the right group milestone' do
+ milestone = create(:milestone, 'name' => 'group milestone', 'group' => project.group)
+
+ expect(described_class.build(Milestone,
+ 'title' => 'group milestone',
+ 'project' => project,
+ 'group' => project.group)).to eq(milestone)
+ end
+
+ it 'creates a new milestone' do
+ milestone = described_class.build(Milestone,
+ 'title' => 'group milestone',
+ 'project' => project,
+ 'group' => project.group)
+
+ expect(milestone.persisted?).to be true
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/importer_spec.rb b/spec/lib/gitlab/import_export/importer_spec.rb
index 991e354f499..c074e61da26 100644
--- a/spec/lib/gitlab/import_export/importer_spec.rb
+++ b/spec/lib/gitlab/import_export/importer_spec.rb
@@ -4,14 +4,14 @@ describe Gitlab::ImportExport::Importer do
let(:user) { create(:user) }
let(:test_path) { "#{Dir.tmpdir}/importer_spec" }
let(:shared) { project.import_export_shared }
- let(:project) { create(:project, import_source: File.join(test_path, 'exported-project.gz')) }
+ let(:project) { create(:project, import_source: File.join(test_path, 'test_project_export.tar.gz')) }
subject(:importer) { described_class.new(project) }
before do
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(test_path)
FileUtils.mkdir_p(shared.export_path)
- FileUtils.cp(Rails.root.join('spec', 'fixtures', 'exported-project.gz'), test_path)
+ FileUtils.cp(Rails.root.join('spec/features/projects/import_export/test_project_export.tar.gz'), test_path)
allow(subject).to receive(:remove_import_file)
end
diff --git a/spec/lib/gitlab/import_export/members_mapper_spec.rb b/spec/lib/gitlab/import_export/members_mapper_spec.rb
index 246f009ad27..67e4c289906 100644
--- a/spec/lib/gitlab/import_export/members_mapper_spec.rb
+++ b/spec/lib/gitlab/import_export/members_mapper_spec.rb
@@ -111,7 +111,7 @@ describe Gitlab::ImportExport::MembersMapper do
end
it 'maps the project member if it already exists' do
- project.add_master(user2)
+ project.add_maintainer(user2)
expect(members_mapper.map[exported_user_id]).to eq(user2.id)
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 b793636c4d6..68eaa70e6b6 100644
--- a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb
+++ b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb
@@ -19,7 +19,9 @@ describe Gitlab::ImportExport::MergeRequestParser do
end
after do
- FileUtils.rm_rf(project.repository.path_to_repo)
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ FileUtils.rm_rf(project.repository.path_to_repo)
+ end
end
it 'has a source branch' do
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index 4f64f2bd6b4..1b7fa11cb3c 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -1,5 +1,7 @@
{
"description": "Nisi et repellendus ut enim quo accusamus vel magnam.",
+ "import_type": "gitlab_project",
+ "creator_id": 123,
"visibility_level": 10,
"archived": false,
"labels": [
diff --git a/spec/lib/gitlab/import_export/project.light.json b/spec/lib/gitlab/import_export/project.light.json
index 5dbf0ed289b..ba2248073f5 100644
--- a/spec/lib/gitlab/import_export/project.light.json
+++ b/spec/lib/gitlab/import_export/project.light.json
@@ -1,11 +1,13 @@
{
"description": "Nisi et repellendus ut enim quo accusamus vel magnam.",
+ "import_type": "gitlab_project",
+ "creator_id": 123,
"visibility_level": 10,
"archived": false,
"milestones": [
{
"id": 1,
- "title": "Project milestone",
+ "title": "A milestone",
"project_id": 8,
"description": "Project-level milestone",
"due_date": null,
@@ -64,8 +66,8 @@
"group_milestone_id": null,
"milestone": {
"id": 1,
- "title": "Project milestone",
- "project_id": 8,
+ "title": "A milestone",
+ "group_id": 8,
"description": "Project-level milestone",
"due_date": null,
"created_at": "2016-06-14T15:02:04.415Z",
@@ -84,7 +86,7 @@
"updated_at": "2017-08-15T18:37:40.795Z",
"label": {
"id": 6,
- "title": "Another project label",
+ "title": "Another label",
"color": "#A8D695",
"project_id": null,
"created_at": "2017-08-15T18:37:19.698Z",
diff --git a/spec/lib/gitlab/import_export/project.milestone-iid.json b/spec/lib/gitlab/import_export/project.milestone-iid.json
new file mode 100644
index 00000000000..b028147b5eb
--- /dev/null
+++ b/spec/lib/gitlab/import_export/project.milestone-iid.json
@@ -0,0 +1,80 @@
+{
+ "description": "Nisi et repellendus ut enim quo accusamus vel magnam.",
+ "import_type": "gitlab_project",
+ "creator_id": 123,
+ "visibility_level": 10,
+ "archived": false,
+ "issues": [
+ {
+ "id": 1,
+ "title": "Fugiat est minima quae maxime non similique.",
+ "assignee_id": null,
+ "project_id": 8,
+ "author_id": 1,
+ "created_at": "2017-07-07T18:13:01.138Z",
+ "updated_at": "2017-08-15T18:37:40.807Z",
+ "branch_name": null,
+ "description": "Quam totam fuga numquam in eveniet.",
+ "state": "opened",
+ "iid": 20,
+ "updated_by_id": 1,
+ "confidential": false,
+ "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": "Group-level 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": 8
+ }
+ },
+ {
+ "id": 2,
+ "title": "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": 21,
+ "updated_by_id": 1,
+ "confidential": false,
+ "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": "Another milestone",
+ "project_id": 8,
+ "description": "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
+ }
+ }
+ ],
+ "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 13a8c9adcee..bac5693c830 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -23,6 +23,10 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
allow_any_instance_of(Gitlab::Git::Repository).to receive(:create_branch)
project_tree_restorer = described_class.new(user: @user, shared: @shared, project: @project)
+
+ expect(Gitlab::ImportExport::RelationFactory).to receive(:create).with(hash_including(excluded_keys: ['whatever'])).and_call_original.at_least(:once)
+ allow(project_tree_restorer).to receive(:excluded_keys_for_relation).and_return(['whatever'])
+
@restored_project_json = project_tree_restorer.restore
end
end
@@ -185,8 +189,8 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
@project.pipelines.zip([2, 2, 2, 2, 2])
.each do |(pipeline, expected_status_size)|
- expect(pipeline.statuses.size).to eq(expected_status_size)
- end
+ expect(pipeline.statuses.size).to eq(expected_status_size)
+ end
end
end
@@ -242,11 +246,9 @@ describe Gitlab::ImportExport::ProjectTreeRestorer 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))
- expect(labels.where(type: "ProjectLabel").where.not(group_id: nil).count).to eq(0)
+ it 'does not set params that are excluded from import_export settings' do
+ expect(project.import_type).to be_nil
+ expect(project.creator_id).not_to eq 123
end
end
@@ -259,12 +261,6 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
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
@@ -351,13 +347,72 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
it_behaves_like 'restores project correctly',
issues: 2,
labels: 1,
- milestones: 1,
+ milestones: 2,
first_issue_labels: 1
it_behaves_like 'restores group correctly',
- labels: 1,
- milestones: 1,
+ labels: 0,
+ milestones: 0,
first_issue_labels: 1
end
+
+ context 'with existing group models' do
+ let!(:project) do
+ create(:project,
+ :builds_disabled,
+ :issues_disabled,
+ name: 'project',
+ path: 'project',
+ group: create(:group))
+ end
+
+ before do
+ project_tree_restorer.instance_variable_set(:@path, "spec/lib/gitlab/import_export/project.light.json")
+ end
+
+ it 'imports labels' do
+ create(:group_label, name: 'Another label', group: project.group)
+
+ expect_any_instance_of(Gitlab::ImportExport::Shared).not_to receive(:error)
+
+ restored_project_json
+
+ expect(project.labels.count).to eq(1)
+ end
+
+ it 'imports milestones' do
+ create(:milestone, name: 'A milestone', group: project.group)
+
+ expect_any_instance_of(Gitlab::ImportExport::Shared).not_to receive(:error)
+
+ restored_project_json
+
+ expect(project.group.milestones.count).to eq(1)
+ expect(project.milestones.count).to eq(0)
+ end
+ end
+
+ context 'with clashing milestones on IID' do
+ let!(:project) do
+ create(:project,
+ :builds_disabled,
+ :issues_disabled,
+ name: 'project',
+ path: 'project',
+ group: create(:group))
+ end
+
+ it 'preserves the project milestone IID' do
+ project_tree_restorer.instance_variable_set(:@path, "spec/lib/gitlab/import_export/project.milestone-iid.json")
+
+ expect_any_instance_of(Gitlab::ImportExport::Shared).not_to receive(:error)
+
+ restored_project_json
+
+ expect(project.milestones.count).to eq(2)
+ expect(Milestone.find_by_title('Another milestone').iid).to eq(1)
+ expect(Milestone.find_by_title('Group-level milestone').iid).to eq(2)
+ end
+ 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 2b8a11ce8f9..fec8a2af9ab 100644
--- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
@@ -9,7 +9,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver do
let!(:project) { setup_project }
before do
- project.add_master(user)
+ project.add_maintainer(user)
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
allow_any_instance_of(MergeRequest).to receive(:source_branch_sha).and_return('ABCD')
allow_any_instance_of(MergeRequest).to receive(:target_branch_sha).and_return('DCBA')
@@ -217,8 +217,8 @@ describe Gitlab::ImportExport::ProjectTreeSaver do
expect(member_emails).not_to include('group@member.com')
end
- it 'does not export group members as master' do
- Group.first.add_master(user)
+ it 'does not export group members as maintainer' do
+ Group.first.add_maintainer(user)
expect(member_emails).not_to include('group@member.com')
end
diff --git a/spec/lib/gitlab/import_export/relation_factory_spec.rb b/spec/lib/gitlab/import_export/relation_factory_spec.rb
index 5c61a5a2044..cf9e0f71910 100644
--- a/spec/lib/gitlab/import_export/relation_factory_spec.rb
+++ b/spec/lib/gitlab/import_export/relation_factory_spec.rb
@@ -4,12 +4,14 @@ describe Gitlab::ImportExport::RelationFactory do
let(:project) { create(:project) }
let(:members_mapper) { double('members_mapper').as_null_object }
let(:user) { create(:admin) }
+ let(:excluded_keys) { [] }
let(:created_object) do
described_class.create(relation_sym: relation_sym,
relation_hash: relation_hash,
members_mapper: members_mapper,
user: user,
- project: project)
+ project: project,
+ excluded_keys: excluded_keys)
end
context 'hook object' do
@@ -67,6 +69,14 @@ describe Gitlab::ImportExport::RelationFactory do
expect(created_object.service_id).not_to eq(service_id)
end
end
+
+ context 'excluded attributes' do
+ let(:excluded_keys) { %w[url] }
+
+ it 'are removed from the imported object' do
+ expect(created_object.url).to be_nil
+ end
+ end
end
# Mocks an ActiveRecordish object with the dodgy columns
@@ -109,6 +119,25 @@ describe Gitlab::ImportExport::RelationFactory do
end
end
+ context 'overrided model with pluralized name' do
+ let(:relation_sym) { :metrics }
+
+ let(:relation_hash) do
+ {
+ 'id' => 99,
+ 'merge_request_id' => 99,
+ 'merged_at' => Time.now,
+ 'merged_by_id' => 99,
+ 'latest_closed_at' => nil,
+ 'latest_closed_by_id' => nil
+ }
+ end
+
+ it 'does not raise errors' do
+ expect { created_object }.not_to raise_error
+ end
+ end
+
context 'Project references' do
let(:relation_sym) { :project_foo_model }
let(:relation_hash) do
diff --git a/spec/lib/gitlab/import_export/repo_restorer_spec.rb b/spec/lib/gitlab/import_export/repo_restorer_spec.rb
index dc806d036ff..7ffa84f906d 100644
--- a/spec/lib/gitlab/import_export/repo_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/repo_restorer_spec.rb
@@ -23,18 +23,22 @@ describe Gitlab::ImportExport::RepoRestorer do
after do
FileUtils.rm_rf(export_path)
- FileUtils.rm_rf(project_with_repo.repository.path_to_repo)
- FileUtils.rm_rf(project.repository.path_to_repo)
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ FileUtils.rm_rf(project_with_repo.repository.path_to_repo)
+ FileUtils.rm_rf(project.repository.path_to_repo)
+ end
end
it 'restores the repo successfully' do
- expect(restorer.restore).to be true
+ expect(restorer.restore).to be_truthy
end
it 'has the webhooks' do
restorer.restore
- expect(Gitlab::Git::Hook.new('post-receive', project.repository.raw_repository)).to exist
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ expect(Gitlab::Git::Hook.new('post-receive', project.repository.raw_repository)).to exist
+ end
end
end
end
diff --git a/spec/lib/gitlab/import_export/repo_saver_spec.rb b/spec/lib/gitlab/import_export/repo_saver_spec.rb
index 187ec8fcfa2..5a646b4aac8 100644
--- a/spec/lib/gitlab/import_export/repo_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/repo_saver_spec.rb
@@ -9,7 +9,7 @@ describe Gitlab::ImportExport::RepoSaver do
let(:bundler) { described_class.new(project: project, shared: shared) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
end
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 62da967cf96..0a1e3eb83d3 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -165,6 +165,7 @@ MergeRequest:
- approvals_before_merge
- rebase_commit_sha
- time_estimate
+- squash
- last_edited_at
- last_edited_by_id
- head_pipeline_id
@@ -204,6 +205,19 @@ MergeRequestDiffFile:
- b_mode
- too_large
- binary
+MergeRequest::Metrics:
+- id
+- created_at
+- updated_at
+- merge_request_id
+- pipeline_id
+- latest_closed_by_id
+- latest_closed_at
+- merged_by_id
+- merged_at
+- latest_build_started_at
+- latest_build_finished_at
+- first_deployed_to_production_at
Ci::Pipeline:
- id
- project_id
@@ -228,6 +242,7 @@ Ci::Pipeline:
- config_source
- failure_reason
- protected
+- iid
Ci::Stage:
- id
- name
@@ -525,6 +540,7 @@ ProjectAutoDevops:
- id
- enabled
- domain
+- deploy_strategy
- project_id
- created_at
- updated_at
diff --git a/spec/lib/gitlab/import_export/saver_spec.rb b/spec/lib/gitlab/import_export/saver_spec.rb
new file mode 100644
index 00000000000..02f1a4b81aa
--- /dev/null
+++ b/spec/lib/gitlab/import_export/saver_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+require 'fileutils'
+
+describe Gitlab::ImportExport::Saver do
+ let!(:project) { create(:project, :public, name: 'project') }
+ let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
+ let(:shared) { project.import_export_shared }
+ subject { described_class.new(project: project, shared: shared) }
+
+ before do
+ allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
+
+ FileUtils.mkdir_p(shared.export_path)
+ FileUtils.touch("#{shared.export_path}/tmp.bundle")
+ end
+
+ after do
+ FileUtils.rm_rf(export_path)
+ end
+
+ context 'local archive' do
+ it 'saves the repo to disk' do
+ stub_feature_flags(import_export_object_storage: false)
+
+ subject.save
+
+ expect(shared.errors).to be_empty
+ expect(Dir.empty?(shared.archive_path)).to be false
+ end
+ end
+
+ context 'object storage' do
+ it 'saves the repo using object storage' do
+ stub_feature_flags(import_export_object_storage: true)
+ stub_uploads_object_storage(ImportExportUploader)
+
+ subject.save
+
+ expect(ImportExportUpload.find_by(project: project).export_file.url)
+ .to match(%r[\/uploads\/-\/system\/import_export_upload\/export_file.*])
+ 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
index 1304d8fabfc..095687fa89d 100644
--- a/spec/lib/gitlab/import_export/uploads_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/uploads_saver_spec.rb
@@ -3,7 +3,7 @@ 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(:file) { fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif') }
let(:shared) { project.import_export_shared }
before do
diff --git a/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb b/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb
index 24bc231d5a0..441aa1defe6 100644
--- a/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb
@@ -10,7 +10,7 @@ describe Gitlab::ImportExport::WikiRepoSaver do
let!(:project_wiki) { ProjectWiki.new(project, user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
project_wiki.wiki
project_wiki.create_page("index", "test content")
diff --git a/spec/lib/gitlab/import_sources_spec.rb b/spec/lib/gitlab/import_sources_spec.rb
index f2fa315e3ec..10341486512 100644
--- a/spec/lib/gitlab/import_sources_spec.rb
+++ b/spec/lib/gitlab/import_sources_spec.rb
@@ -91,4 +91,23 @@ describe Gitlab::ImportSources do
end
end
end
+
+ describe 'imports_repository? checker' do
+ let(:allowed_importers) { %w[github gitlab_project] }
+
+ it 'fails if any importer other than the allowed ones implements this method' do
+ current_importers = described_class.values.select { |kind| described_class.importer(kind).try(:imports_repository?) }
+ not_allowed_importers = current_importers - allowed_importers
+
+ expect(not_allowed_importers).to be_empty, failure_message(not_allowed_importers)
+ end
+
+ def failure_message(importers_class_names)
+ <<-MSG
+ It looks like the #{importers_class_names.join(', ')} importers implements its own way to import the repository.
+ That means that the lfs object download must be handled for each of them. You can use 'LfsImportService' and
+ 'LfsDownloadService' to implement it. After that, add the importer name to the list of allowed importers in this spec.
+ MSG
+ end
+ end
end
diff --git a/spec/lib/gitlab/kubernetes/helm/api_spec.rb b/spec/lib/gitlab/kubernetes/helm/api_spec.rb
index 740466ea5cb..aa7e43dfb16 100644
--- a/spec/lib/gitlab/kubernetes/helm/api_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/api_spec.rb
@@ -7,13 +7,7 @@ describe Gitlab::Kubernetes::Helm::Api do
let(:namespace) { Gitlab::Kubernetes::Namespace.new(gitlab_namespace, client) }
let(:application) { create(:clusters_applications_prometheus) }
- let(:command) do
- Gitlab::Kubernetes::Helm::InstallCommand.new(
- application.name,
- chart: application.chart,
- values: application.values
- )
- end
+ let(:command) { application.install_command }
subject { helm }
diff --git a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb
index 547f3f1752c..25c6fa3b9a3 100644
--- a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb
@@ -3,44 +3,60 @@ require 'rails_helper'
describe Gitlab::Kubernetes::Helm::InstallCommand do
let(:application) { create(:clusters_applications_prometheus) }
let(:namespace) { Gitlab::Kubernetes::Helm::NAMESPACE }
-
- let(:install_command) do
- described_class.new(
- application.name,
- chart: application.chart,
- values: application.values
- )
- end
+ let(:install_command) { application.install_command }
subject { install_command }
- it_behaves_like 'helm commands' do
- let(:commands) do
- <<~EOS
+ context 'for ingress' do
+ let(:application) { create(:clusters_applications_ingress) }
+
+ it_behaves_like 'helm commands' do
+ let(:commands) do
+ <<~EOS
helm init --client-only >/dev/null
helm install #{application.chart} --name #{application.name} --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml >/dev/null
- EOS
+ EOS
+ end
+ end
+ end
+
+ context 'for prometheus' do
+ let(:application) { create(:clusters_applications_prometheus) }
+
+ it_behaves_like 'helm commands' do
+ let(:commands) do
+ <<~EOS
+ helm init --client-only >/dev/null
+ helm install #{application.chart} --name #{application.name} --version #{application.version} --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml >/dev/null
+ EOS
+ end
end
end
- context 'with an application with a repository' do
+ context 'for runner' do
let(:ci_runner) { create(:ci_runner) }
let(:application) { create(:clusters_applications_runner, runner: ci_runner) }
- let(:install_command) do
- described_class.new(
- application.name,
- chart: application.chart,
- values: application.values,
- repository: application.repository
- )
+
+ it_behaves_like 'helm commands' do
+ let(:commands) do
+ <<~EOS
+ helm init --client-only >/dev/null
+ helm repo add #{application.name} #{application.repository}
+ helm install #{application.chart} --name #{application.name} --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml >/dev/null
+ EOS
+ end
end
+ end
+
+ context 'for jupyter' do
+ let(:application) { create(:clusters_applications_jupyter) }
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
- helm init --client-only >/dev/null
- helm repo add #{application.name} #{application.repository}
- helm install #{application.chart} --name #{application.name} --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml >/dev/null
+ helm init --client-only >/dev/null
+ helm repo add #{application.name} #{application.repository}
+ helm install #{application.chart} --name #{application.name} --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml >/dev/null
EOS
end
end
diff --git a/spec/lib/gitlab/kubernetes_spec.rb b/spec/lib/gitlab/kubernetes_spec.rb
index 34b33772578..5c03a2ce7d3 100644
--- a/spec/lib/gitlab/kubernetes_spec.rb
+++ b/spec/lib/gitlab/kubernetes_spec.rb
@@ -70,4 +70,19 @@ describe Gitlab::Kubernetes do
it { is_expected.to eq(YAML.load_file(path)) }
end
end
+
+ describe '#add_terminal_auth' do
+ it 'adds authentication parameters to a hash' do
+ terminal = { original: 'value' }
+
+ add_terminal_auth(terminal, token: 'foo', max_session_time: 0, ca_pem: 'bar')
+
+ expect(terminal).to eq(
+ original: 'value',
+ headers: { 'Authorization' => ['Bearer foo'] },
+ max_session_time: 0,
+ ca_pem: 'bar'
+ )
+ end
+ 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
index eb1b13704ea..3d4240fa4ba 100644
--- a/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb
@@ -17,7 +17,10 @@ describe Gitlab::LegacyGithubImport::ProjectCreator do
before do
namespace.add_owner(user)
- allow_any_instance_of(Project).to receive(:add_import_job)
+
+ expect_next_instance_of(Project) do |project|
+ expect(project).to receive(:add_import_job)
+ end
end
describe '#execute' do
@@ -44,16 +47,44 @@ describe Gitlab::LegacyGithubImport::ProjectCreator do
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
+ it 'sets project visibility to public' do
repo.private = false
project = service.execute
- expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL)
+ expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC)
+ end
+ end
+
+ context 'when visibility level is restricted' do
+ context 'when GitHub project is private' do
+ before do
+ stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PRIVATE])
+ 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 = true
+
+ project = service.execute
+
+ expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL)
+ end
+ end
+
+ context 'when GitHub project is public' do
+ before do
+ stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
+ 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
end
diff --git a/spec/lib/gitlab/metrics/samplers/influx_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/influx_sampler_spec.rb
index f66451c5188..81954fcf8c5 100644
--- a/spec/lib/gitlab/metrics/samplers/influx_sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/samplers/influx_sampler_spec.rb
@@ -3,10 +3,6 @@ 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
diff --git a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
index 54781dd52fc..7972ff253fe 100644
--- a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
@@ -8,10 +8,6 @@ describe Gitlab::Metrics::Samplers::RubySampler do
allow(Gitlab::Metrics::NullMetric).to receive(:instance).and_return(null_metric)
end
- 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)
@@ -49,7 +45,7 @@ describe Gitlab::Metrics::Samplers::RubySampler 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)
+ expect(sampler.metrics[:total_time]).to receive(:increment).with({}, 0.24)
sampler.sample
end
diff --git a/spec/lib/gitlab/metrics/web_transaction_spec.rb b/spec/lib/gitlab/metrics/web_transaction_spec.rb
index 6eb0600f49e..0b3b23e930f 100644
--- a/spec/lib/gitlab/metrics/web_transaction_spec.rb
+++ b/spec/lib/gitlab/metrics/web_transaction_spec.rb
@@ -194,7 +194,7 @@ describe Gitlab::Metrics::WebTransaction do
expect(transaction.action).to eq('TestController#show')
end
- context 'when the response content type is not :html' do
+ context 'when the request content type is not :html' do
let(:request) { double(:request, format: double(:format, ref: :json)) }
it 'appends the mime type to the transaction action' do
@@ -202,6 +202,15 @@ describe Gitlab::Metrics::WebTransaction do
expect(transaction.action).to eq('TestController#show.json')
end
end
+
+ context 'when the request content type is not' do
+ let(:request) { double(:request, format: double(:format, ref: 'http://example.com')) }
+
+ it 'does not append the MIME type to the transaction action' do
+ expect(transaction.labels).to eq({ controller: 'TestController', action: 'show' })
+ expect(transaction.action).to eq('TestController#show')
+ end
+ end
end
it 'returns no labels when no route information is present in env' do
diff --git a/spec/lib/gitlab/middleware/go_spec.rb b/spec/lib/gitlab/middleware/go_spec.rb
index b24c9882c0c..7a3a9ab875b 100644
--- a/spec/lib/gitlab/middleware/go_spec.rb
+++ b/spec/lib/gitlab/middleware/go_spec.rb
@@ -79,7 +79,7 @@ describe Gitlab::Middleware::Go do
let(:current_user) { project.creator }
before do
- project.team.add_master(current_user)
+ project.team.add_maintainer(current_user)
end
shared_examples 'authenticated' do
diff --git a/spec/lib/gitlab/middleware/multipart_spec.rb b/spec/lib/gitlab/middleware/multipart_spec.rb
index a2ba91dae80..f788f8ee276 100644
--- a/spec/lib/gitlab/middleware/multipart_spec.rb
+++ b/spec/lib/gitlab/middleware/multipart_spec.rb
@@ -7,18 +7,47 @@ describe Gitlab::Middleware::Multipart do
let(:middleware) { described_class.new(app) }
let(:original_filename) { 'filename' }
- it 'opens top-level files' do
- Tempfile.open('top-level') do |tempfile|
- env = post_env({ 'file' => tempfile.path }, { 'file.name' => original_filename }, Gitlab::Workhorse.secret, 'gitlab-workhorse')
+ shared_examples_for 'multipart upload files' do
+ it 'opens top-level files' do
+ Tempfile.open('top-level') do |tempfile|
+ env = post_env({ 'file' => tempfile.path }, { 'file.name' => original_filename, 'file.path' => tempfile.path, 'file.remote_id' => remote_id }, Gitlab::Workhorse.secret, 'gitlab-workhorse')
+ expect_uploaded_file(tempfile, %w(file))
+
+ middleware.call(env)
+ end
+ end
+
+ it 'opens files one level deep' do
+ Tempfile.open('one-level') do |tempfile|
+ in_params = { 'user' => { 'avatar' => { '.name' => original_filename, '.path' => tempfile.path, '.remote_id' => remote_id } } }
+ env = post_env({ 'user[avatar]' => tempfile.path }, in_params, Gitlab::Workhorse.secret, 'gitlab-workhorse')
+
+ expect_uploaded_file(tempfile, %w(user avatar))
+
+ middleware.call(env)
+ end
+ end
+
+ it 'opens files two levels deep' do
+ Tempfile.open('two-levels') do |tempfile|
+ in_params = { 'project' => { 'milestone' => { 'themesong' => { '.name' => original_filename, '.path' => tempfile.path, '.remote_id' => remote_id } } } }
+ env = post_env({ 'project[milestone][themesong]' => tempfile.path }, in_params, Gitlab::Workhorse.secret, 'gitlab-workhorse')
+
+ expect_uploaded_file(tempfile, %w(project milestone themesong))
+
+ middleware.call(env)
+ end
+ end
+
+ def expect_uploaded_file(tempfile, path, remote: false)
expect(app).to receive(:call) do |env|
- file = Rack::Request.new(env).params['file']
+ file = Rack::Request.new(env).params.dig(*path)
expect(file).to be_a(::UploadedFile)
expect(file.path).to eq(tempfile.path)
expect(file.original_filename).to eq(original_filename)
+ expect(file.remote_id).to eq(remote_id)
end
-
- middleware.call(env)
end
end
@@ -34,32 +63,39 @@ describe Gitlab::Middleware::Multipart do
expect { middleware.call(env) }.to raise_error(JWT::InvalidIssuerError)
end
- it 'opens files one level deep' do
- Tempfile.open('one-level') do |tempfile|
- in_params = { 'user' => { 'avatar' => { '.name' => original_filename } } }
- env = post_env({ 'user[avatar]' => tempfile.path }, in_params, Gitlab::Workhorse.secret, 'gitlab-workhorse')
+ context 'with remote file' do
+ let(:remote_id) { 'someid' }
- expect(app).to receive(:call) do |env|
- file = Rack::Request.new(env).params['user']['avatar']
- expect(file).to be_a(::UploadedFile)
- expect(file.path).to eq(tempfile.path)
- expect(file.original_filename).to eq(original_filename)
- end
+ it_behaves_like 'multipart upload files'
+ end
- middleware.call(env)
- end
+ context 'with local file' do
+ let(:remote_id) { nil }
+
+ it_behaves_like 'multipart upload files'
end
- it 'opens files two levels deep' do
+ it 'allows symlinks for uploads dir' do
Tempfile.open('two-levels') do |tempfile|
- in_params = { 'project' => { 'milestone' => { 'themesong' => { '.name' => original_filename } } } }
- env = post_env({ 'project[milestone][themesong]' => tempfile.path }, in_params, Gitlab::Workhorse.secret, 'gitlab-workhorse')
+ symlinked_dir = '/some/dir/uploads'
+ symlinked_path = File.join(symlinked_dir, File.basename(tempfile.path))
+ env = post_env({ 'file' => symlinked_path }, { 'file.name' => original_filename, 'file.path' => symlinked_path }, Gitlab::Workhorse.secret, 'gitlab-workhorse')
+
+ allow(FileUploader).to receive(:root).and_return(symlinked_dir)
+ allow(UploadedFile).to receive(:allowed_paths).and_return([symlinked_dir, Gitlab.config.uploads.storage_path])
+ allow(File).to receive(:realpath).and_call_original
+ allow(File).to receive(:realpath).with(symlinked_dir).and_return(Dir.tmpdir)
+ allow(File).to receive(:realpath).with(symlinked_path).and_return(tempfile.path)
+ allow(File).to receive(:exist?).and_call_original
+ allow(File).to receive(:exist?).with(symlinked_dir).and_return(true)
+
+ # override Dir.tmpdir because this dir is in the list of allowed paths
+ # and it would match FileUploader.root path (which in this test is linked
+ # to /tmp too)
+ allow(Dir).to receive(:tmpdir).and_return(File.join(Dir.tmpdir, 'tmpsubdir'))
expect(app).to receive(:call) do |env|
- file = Rack::Request.new(env).params['project']['milestone']['themesong']
- expect(file).to be_a(::UploadedFile)
- expect(file.path).to eq(tempfile.path)
- expect(file.original_filename).to eq(original_filename)
+ expect(Rack::Request.new(env).params['file']).to be_a(::UploadedFile)
end
middleware.call(env)
diff --git a/spec/lib/gitlab/middleware/read_only_spec.rb b/spec/lib/gitlab/middleware/read_only_spec.rb
index 39ec2f37a83..8fbeaa065fa 100644
--- a/spec/lib/gitlab/middleware/read_only_spec.rb
+++ b/spec/lib/gitlab/middleware/read_only_spec.rb
@@ -2,28 +2,7 @@ require 'spec_helper'
describe Gitlab::Middleware::ReadOnly do
include Rack::Test::Methods
-
- RSpec::Matchers.define :be_a_redirect do
- match do |response|
- response.status == 301
- end
- end
-
- RSpec::Matchers.define :disallow_request do
- match do |middleware|
- alert = middleware.env['rack.session'].to_hash
- .dig('flash', 'flashes', 'alert')
-
- alert&.include?('You cannot perform write operations')
- end
- end
-
- RSpec::Matchers.define :disallow_request_in_json do
- match do |response|
- json_response = JSON.parse(response.body)
- response.body.include?('You cannot perform write operations') && json_response.key?('message')
- end
- end
+ using RSpec::Parameterized::TableSyntax
let(:rack_stack) do
rack = Rack::Builder.new do
@@ -65,38 +44,38 @@ describe Gitlab::Middleware::ReadOnly do
it 'expects PATCH requests to be disallowed' do
response = request.patch('/test_request')
- expect(response).to be_a_redirect
+ expect(response).to be_redirect
expect(subject).to disallow_request
end
it 'expects PUT requests to be disallowed' do
response = request.put('/test_request')
- expect(response).to be_a_redirect
+ expect(response).to be_redirect
expect(subject).to disallow_request
end
it 'expects POST requests to be disallowed' do
response = request.post('/test_request')
- expect(response).to be_a_redirect
+ expect(response).to be_redirect
expect(subject).to disallow_request
end
it 'expects a internal POST request to be allowed after a disallowed request' do
response = request.post('/test_request')
- expect(response).to be_a_redirect
+ expect(response).to be_redirect
response = request.post("/api/#{API::API.version}/internal")
- expect(response).not_to be_a_redirect
+ expect(response).not_to be_redirect
end
it 'expects DELETE requests to be disallowed' do
response = request.delete('/test_request')
- expect(response).to be_a_redirect
+ expect(response).to be_redirect
expect(subject).to disallow_request
end
@@ -104,7 +83,7 @@ describe Gitlab::Middleware::ReadOnly 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).to be_a_redirect
+ expect(response).to be_redirect
expect(subject).to disallow_request
end
@@ -117,39 +96,41 @@ describe Gitlab::Middleware::ReadOnly do
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
+ expect(response).not_to be_redirect
expect(subject).not_to disallow_request
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')
+ it 'expects requests to sidekiq admin to be allowed' do
+ response = request.post('/admin/sidekiq')
- expect(response).not_to be_a_redirect
+ expect(response).not_to be_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')
+ response = request.get('/admin/sidekiq')
- expect(response).not_to be_a_redirect
+ expect(response).not_to be_redirect
expect(subject).not_to disallow_request
end
- it 'expects requests to sidekiq admin to be allowed' do
- response = request.post('/admin/sidekiq')
+ where(:description, :path) do
+ 'LFS request to batch' | '/root/rouge.git/info/lfs/objects/batch'
+ 'LFS request to locks verify' | '/root/rouge.git/info/lfs/locks/verify'
+ 'LFS request to locks create' | '/root/rouge.git/info/lfs/locks'
+ 'LFS request to locks unlock' | '/root/rouge.git/info/lfs/locks/1/unlock'
+ 'request to git-upload-pack' | '/root/rouge.git/git-upload-pack'
+ 'request to git-receive-pack' | '/root/rouge.git/git-receive-pack'
+ end
- expect(response).not_to be_a_redirect
- expect(subject).not_to disallow_request
+ with_them do
+ it "expects a POST #{description} URL to be allowed" do
+ expect(Rails.application.routes).to receive(:recognize_path).and_call_original
+ response = request.post(path)
- response = request.get('/admin/sidekiq')
-
- expect(response).not_to be_a_redirect
- expect(subject).not_to disallow_request
+ expect(response).not_to be_redirect
+ expect(subject).not_to disallow_request
+ end
end
end
end
diff --git a/spec/lib/gitlab/path_regex_spec.rb b/spec/lib/gitlab/path_regex_spec.rb
index a40330d853f..e90e0aba0a4 100644
--- a/spec/lib/gitlab/path_regex_spec.rb
+++ b/spec/lib/gitlab/path_regex_spec.rb
@@ -90,11 +90,13 @@ describe Gitlab::PathRegex do
let(:routes_not_starting_in_wildcard) { routes_without_format.select { |p| p !~ %r{^/[:*]} } }
let(:top_level_words) do
- words = routes_not_starting_in_wildcard.map do |route|
- route.split('/')[1]
- end.compact
-
- (words + ee_top_level_words + files_in_public + Array(API::API.prefix.to_s)).uniq
+ routes_not_starting_in_wildcard
+ .map { |route| route.split('/')[1] }
+ .concat(ee_top_level_words)
+ .concat(files_in_public)
+ .concat(Array(API::API.prefix.to_s))
+ .compact
+ .uniq
end
let(:ee_top_level_words) do
diff --git a/spec/lib/gitlab/profiler_spec.rb b/spec/lib/gitlab/profiler_spec.rb
index 548eb28fe4d..4059188fba1 100644
--- a/spec/lib/gitlab/profiler_spec.rb
+++ b/spec/lib/gitlab/profiler_spec.rb
@@ -135,6 +135,51 @@ describe Gitlab::Profiler do
end
end
+ describe '.clean_backtrace' do
+ it 'uses the Rails backtrace cleaner' do
+ backtrace = []
+
+ expect(Rails.backtrace_cleaner).to receive(:clean).with(backtrace)
+
+ described_class.clean_backtrace(backtrace)
+ end
+
+ it 'removes lines from IGNORE_BACKTRACES' do
+ backtrace = [
+ "lib/gitlab/gitaly_client.rb:294:in `block (2 levels) in migrate'",
+ "lib/gitlab/gitaly_client.rb:331:in `allow_n_plus_1_calls'",
+ "lib/gitlab/gitaly_client.rb:280:in `block in migrate'",
+ "lib/gitlab/metrics/influx_db.rb:103:in `measure'",
+ "lib/gitlab/gitaly_client.rb:278:in `migrate'",
+ "lib/gitlab/git/repository.rb:1451:in `gitaly_migrate'",
+ "lib/gitlab/git/commit.rb:66:in `find'",
+ "app/models/repository.rb:1047:in `find_commit'",
+ "lib/gitlab/metrics/instrumentation.rb:159:in `block in find_commit'",
+ "lib/gitlab/metrics/method_call.rb:36:in `measure'",
+ "lib/gitlab/metrics/instrumentation.rb:159:in `find_commit'",
+ "app/models/repository.rb:113:in `commit'",
+ "lib/gitlab/i18n.rb:50:in `with_locale'",
+ "lib/gitlab/middleware/multipart.rb:95:in `call'",
+ "lib/gitlab/request_profiler/middleware.rb:14:in `call'",
+ "ee/lib/gitlab/database/load_balancing/rack_middleware.rb:37:in `call'",
+ "ee/lib/gitlab/jira/middleware.rb:15:in `call'"
+ ]
+
+ expect(described_class.clean_backtrace(backtrace))
+ .to eq([
+ "lib/gitlab/gitaly_client.rb:294:in `block (2 levels) in migrate'",
+ "lib/gitlab/gitaly_client.rb:331:in `allow_n_plus_1_calls'",
+ "lib/gitlab/gitaly_client.rb:280:in `block in migrate'",
+ "lib/gitlab/gitaly_client.rb:278:in `migrate'",
+ "lib/gitlab/git/repository.rb:1451:in `gitaly_migrate'",
+ "lib/gitlab/git/commit.rb:66:in `find'",
+ "app/models/repository.rb:1047:in `find_commit'",
+ "app/models/repository.rb:113:in `commit'",
+ "ee/lib/gitlab/jira/middleware.rb:15:in `call'"
+ ])
+ end
+ end
+
describe '.with_custom_logger' do
context 'when the logger is set' do
it 'uses the replacement logger for the duration of the block' do
diff --git a/spec/lib/gitlab/project_authorizations_spec.rb b/spec/lib/gitlab/project_authorizations_spec.rb
index f3cd6961e94..00c62c7bf96 100644
--- a/spec/lib/gitlab/project_authorizations_spec.rb
+++ b/spec/lib/gitlab/project_authorizations_spec.rb
@@ -41,7 +41,7 @@ describe Gitlab::ProjectAuthorizations do
it 'includes the correct access levels' do
mapping = map_access_levels(authorizations)
- expect(mapping[owned_project.id]).to eq(Gitlab::Access::MASTER)
+ expect(mapping[owned_project.id]).to eq(Gitlab::Access::MAINTAINER)
expect(mapping[other_project.id]).to eq(Gitlab::Access::REPORTER)
expect(mapping[group_project.id]).to eq(Gitlab::Access::DEVELOPER)
end
@@ -62,11 +62,11 @@ describe Gitlab::ProjectAuthorizations do
end
it 'uses the greatest access level when a user is a member of a nested group' do
- nested_group.add_master(user)
+ nested_group.add_maintainer(user)
mapping = map_access_levels(authorizations)
- expect(mapping[nested_project.id]).to eq(Gitlab::Access::MASTER)
+ expect(mapping[nested_project.id]).to eq(Gitlab::Access::MAINTAINER)
end
end
end
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index e3f705d2299..767a3092c73 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -22,47 +22,57 @@ describe Gitlab::ProjectSearchResults do
it { expect(results.query).to eq('hello world') }
end
- describe 'blob search' do
- let(:project) { create(:project, :public, :repository) }
-
- subject(:results) { described_class.new(user, project, 'files').objects('blobs') }
-
- context 'when repository is disabled' do
- let(:project) { create(:project, :public, :repository, :repository_disabled) }
+ shared_examples 'general blob search' do |entity_type, blob_kind|
+ let(:query) { 'files' }
+ subject(:results) { described_class.new(user, project, query).objects(blob_type) }
- it 'hides blobs from members' do
+ context "when #{entity_type} is disabled" do
+ let(:project) { disabled_project }
+ it "hides #{blob_kind} from members" do
project.add_reporter(user)
is_expected.to be_empty
end
- it 'hides blobs from non-members' do
+ it "hides #{blob_kind} from non-members" do
is_expected.to be_empty
end
end
- context 'when repository is internal' do
- let(:project) { create(:project, :public, :repository, :repository_private) }
+ context "when #{entity_type} is internal" do
+ let(:project) { private_project }
- it 'finds blobs for members' do
+ it "finds #{blob_kind} for members" do
project.add_reporter(user)
is_expected.not_to be_empty
end
- it 'hides blobs from non-members' do
+ it "hides #{blob_kind} from non-members" do
is_expected.to be_empty
end
end
it 'finds by name' do
- expect(results.map(&:first)).to include('files/images/wm.svg')
+ expect(results.map(&:first)).to include(expected_file_by_name)
end
it 'finds by content' do
- blob = results.select { |result| result.first == "CHANGELOG" }.flatten.last
+ blob = results.select { |result| result.first == expected_file_by_content }.flatten.last
- expect(blob.filename).to eq("CHANGELOG")
+ expect(blob.filename).to eq(expected_file_by_content)
+ end
+ end
+
+ describe 'blob search' do
+ let(:project) { create(:project, :public, :repository) }
+
+ it_behaves_like 'general blob search', 'repository', 'blobs' do
+ let(:blob_type) { 'blobs' }
+ let(:disabled_project) { create(:project, :public, :repository, :repository_disabled) }
+ let(:private_project) { create(:project, :public, :repository, :repository_private) }
+ let(:expected_file_by_name) { 'files/images/wm.svg' }
+ let(:expected_file_by_content) { 'CHANGELOG' }
end
describe 'parsing results' do
@@ -189,40 +199,18 @@ describe Gitlab::ProjectSearchResults do
describe 'wiki search' do
let(:project) { create(:project, :public, :wiki_repo) }
let(:wiki) { build(:project_wiki, project: project) }
- let!(:wiki_page) { wiki.create_page('Title', 'Content') }
-
- subject(:results) { described_class.new(user, project, 'Content').objects('wiki_blobs') }
-
- context 'when wiki is disabled' do
- let(:project) { create(:project, :public, :wiki_repo, :wiki_disabled) }
- it 'hides wiki blobs from members' do
- project.add_reporter(user)
-
- is_expected.to be_empty
- end
-
- it 'hides wiki blobs from non-members' do
- is_expected.to be_empty
- end
- end
-
- context 'when wiki is internal' do
- let(:project) { create(:project, :public, :wiki_repo, :wiki_private) }
-
- it 'finds wiki blobs for guest' do
- project.add_guest(user)
-
- is_expected.not_to be_empty
- end
-
- it 'hides wiki blobs from non-members' do
- is_expected.to be_empty
- end
+ before do
+ wiki.create_page('Files/Title', 'Content')
+ wiki.create_page('CHANGELOG', 'Files example')
end
- it 'finds by content' do
- expect(results).to include("master:Title.md\x001\x00Content\n")
+ it_behaves_like 'general blob search', 'wiki', 'wiki blobs' do
+ let(:blob_type) { 'wiki_blobs' }
+ let(:disabled_project) { create(:project, :public, :wiki_repo, :wiki_disabled) }
+ let(:private_project) { create(:project, :public, :wiki_repo, :wiki_private) }
+ let(:expected_file_by_name) { 'Files/Title.md' }
+ let(:expected_file_by_content) { 'CHANGELOG.md' }
end
end
@@ -397,7 +385,7 @@ describe Gitlab::ProjectSearchResults do
let!(:private_project) { create(:project, :private, :repository, creator: creator, namespace: creator.namespace) }
let(:team_master) do
user = create(:user, username: 'private-project-master')
- private_project.add_master(user)
+ private_project.add_maintainer(user)
user
end
let(:team_reporter) do
diff --git a/spec/lib/gitlab/quick_actions/extractor_spec.rb b/spec/lib/gitlab/quick_actions/extractor_spec.rb
index f7c288f2393..0166f6c2ee0 100644
--- a/spec/lib/gitlab/quick_actions/extractor_spec.rb
+++ b/spec/lib/gitlab/quick_actions/extractor_spec.rb
@@ -182,6 +182,14 @@ describe Gitlab::QuickActions::Extractor do
expect(msg).to eq "hello\nworld"
end
+ it 'extracts command case insensitive' do
+ msg = %(hello\n/PoWer @user.name %9.10 ~"bar baz.2"\nworld)
+ msg, commands = extractor.extract_commands(msg)
+
+ expect(commands).to eq [['power', '@user.name %9.10 ~"bar baz.2"']]
+ expect(msg).to eq "hello\nworld"
+ end
+
it 'does not extract noop commands' do
msg = %(hello\nworld\n/reopen\n/noop_command)
msg, commands = extractor.extract_commands(msg)
@@ -206,6 +214,14 @@ describe Gitlab::QuickActions::Extractor do
expect(msg).to eq "hello\nworld\nthis is great? SHRUG"
end
+ it 'extracts and performs substitution commands case insensitive' do
+ msg = %(hello\nworld\n/reOpen\n/sHRuG this is great?)
+ msg, commands = extractor.extract_commands(msg)
+
+ expect(commands).to eq [['reopen'], ['shrug', 'this is great?']]
+ expect(msg).to eq "hello\nworld\nthis is great? SHRUG"
+ end
+
it 'extracts and performs substitution commands with comments' do
msg = %(hello\nworld\n/reopen\n/substitution wow this is a thing.)
msg, commands = extractor.extract_commands(msg)
diff --git a/spec/lib/gitlab/repository_cache_adapter_spec.rb b/spec/lib/gitlab/repository_cache_adapter_spec.rb
index 85971f2a7ef..5bd4d6c6a48 100644
--- a/spec/lib/gitlab/repository_cache_adapter_spec.rb
+++ b/spec/lib/gitlab/repository_cache_adapter_spec.rb
@@ -67,10 +67,18 @@ describe Gitlab::RepositoryCacheAdapter do
describe '#expire_method_caches' do
it 'expires the caches of the given methods' do
- expect(cache).to receive(:expire).with(:readme)
+ expect(cache).to receive(:expire).with(:rendered_readme)
expect(cache).to receive(:expire).with(:gitignore)
- repository.expire_method_caches(%i(readme gitignore))
+ repository.expire_method_caches(%i(rendered_readme gitignore))
+ end
+
+ it 'does not expire caches for non-existent methods' do
+ expect(cache).not_to receive(:expire).with(:nonexistent)
+ expect(Rails.logger).to(
+ receive(:error).with("Requested to expire non-existent method 'nonexistent' for Repository"))
+
+ repository.expire_method_caches(%i(nonexistent))
end
end
end
diff --git a/spec/lib/gitlab/sanitizers/svg_spec.rb b/spec/lib/gitlab/sanitizers/svg_spec.rb
index 030c2063ab2..df46a874528 100644
--- a/spec/lib/gitlab/sanitizers/svg_spec.rb
+++ b/spec/lib/gitlab/sanitizers/svg_spec.rb
@@ -7,9 +7,9 @@ describe Gitlab::Sanitizers::SVG do
describe '.clean' do
let(:input_svg_path) { File.join(Rails.root, 'spec', 'fixtures', 'unsanitized.svg') }
- let(:data) { open(input_svg_path).read }
+ let(:data) { File.read(input_svg_path) }
let(:sanitized_svg_path) { File.join(Rails.root, 'spec', 'fixtures', 'sanitized.svg') }
- let(:sanitized) { open(sanitized_svg_path).read }
+ let(:sanitized) { File.read(sanitized_svg_path) }
it 'delegates sanitization to scrubber' do
expect_any_instance_of(Gitlab::Sanitizers::SVG::Scrubber).to receive(:scrub).at_least(:once)
diff --git a/spec/lib/gitlab/search/query_spec.rb b/spec/lib/gitlab/search/query_spec.rb
new file mode 100644
index 00000000000..2d00428fffa
--- /dev/null
+++ b/spec/lib/gitlab/search/query_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+describe Gitlab::Search::Query do
+ let(:query) { 'base filter:wow anotherfilter:noway name:maybe other:mmm leftover' }
+ let(:subject) do
+ described_class.new(query) do
+ filter :filter
+ filter :name, parser: :upcase.to_proc
+ filter :other
+ end
+ end
+
+ it { expect(described_class).to be < SimpleDelegator }
+
+ it 'leaves undefined filters in the main query' do
+ expect(subject.term).to eq('base anotherfilter:noway leftover')
+ end
+
+ it 'parses filters' do
+ expect(subject.filters.count).to eq(3)
+ expect(subject.filters.map { |f| f[:value] }).to match_array(%w[wow MAYBE mmm])
+ end
+
+ context 'with an empty filter' do
+ let(:query) { 'some bar name: baz' }
+
+ it 'ignores empty filters' do
+ expect(subject.term).to eq('some bar name: baz')
+ end
+ end
+
+ context 'with a pipe' do
+ let(:query) { 'base | nofilter' }
+
+ it 'does not escape the pipe' do
+ expect(subject.term).to eq(query)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/shard_health_cache_spec.rb b/spec/lib/gitlab/shard_health_cache_spec.rb
new file mode 100644
index 00000000000..e1a69261939
--- /dev/null
+++ b/spec/lib/gitlab/shard_health_cache_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+describe Gitlab::ShardHealthCache, :clean_gitlab_redis_cache do
+ let(:shards) { %w(foo bar) }
+
+ before do
+ described_class.update(shards)
+ end
+
+ describe '.clear' do
+ it 'leaves no shards around' do
+ described_class.clear
+
+ expect(described_class.healthy_shard_count).to eq(0)
+ end
+ end
+
+ describe '.update' do
+ it 'returns the healthy shards' do
+ expect(described_class.cached_healthy_shards).to match_array(shards)
+ end
+
+ it 'replaces the existing set' do
+ new_set = %w(test me more)
+ described_class.update(new_set)
+
+ expect(described_class.cached_healthy_shards).to match_array(new_set)
+ end
+ end
+
+ describe '.healthy_shard_count' do
+ it 'returns the healthy shard count' do
+ expect(described_class.healthy_shard_count).to eq(2)
+ end
+
+ it 'returns 0 if no shards are available' do
+ described_class.update([])
+
+ expect(described_class.healthy_shard_count).to eq(0)
+ end
+ end
+
+ describe '.healthy_shard?' do
+ it 'returns true for a healthy shard' do
+ expect(described_class.healthy_shard?('foo')).to be_truthy
+ end
+
+ it 'returns false for an unknown shard' do
+ expect(described_class.healthy_shard?('unknown')).to be_falsey
+ end
+ end
+end
diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb
index bf6ee4b0b59..f8bf896950e 100644
--- a/spec/lib/gitlab/shell_spec.rb
+++ b/spec/lib/gitlab/shell_spec.rb
@@ -403,42 +403,36 @@ describe Gitlab::Shell do
end
describe '#create_repository' do
- shared_examples '#create_repository' do
- let(:repository_storage) { 'default' }
- let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage].legacy_disk_path }
- let(:repo_name) { 'project/path' }
- let(:created_path) { File.join(repository_storage_path, repo_name + '.git') }
-
- after do
- FileUtils.rm_rf(created_path)
+ let(:repository_storage) { 'default' }
+ let(:repository_storage_path) do
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ Gitlab.config.repositories.storages[repository_storage].legacy_disk_path
end
+ end
+ let(:repo_name) { 'project/path' }
+ let(:created_path) { File.join(repository_storage_path, repo_name + '.git') }
- it 'creates a repository' do
- expect(gitlab_shell.create_repository(repository_storage, repo_name)).to be_truthy
-
- expect(File.stat(created_path).mode & 0o777).to eq(0o770)
+ after do
+ FileUtils.rm_rf(created_path)
+ end
- hooks_path = File.join(created_path, 'hooks')
- expect(File.lstat(hooks_path)).to be_symlink
- expect(File.realpath(hooks_path)).to eq(gitlab_shell_hooks_path)
- end
+ it 'creates a repository' do
+ expect(gitlab_shell.create_repository(repository_storage, repo_name)).to be_truthy
- it 'returns false when the command fails' do
- FileUtils.mkdir_p(File.dirname(created_path))
- # This file will block the creation of the repo's .git directory. That
- # should cause #create_repository to fail.
- FileUtils.touch(created_path)
+ expect(File.stat(created_path).mode & 0o777).to eq(0o770)
- expect(gitlab_shell.create_repository(repository_storage, repo_name)).to be_falsy
- end
+ hooks_path = File.join(created_path, 'hooks')
+ expect(File.lstat(hooks_path)).to be_symlink
+ expect(File.realpath(hooks_path)).to eq(gitlab_shell_hooks_path)
end
- context 'with gitaly' do
- it_behaves_like '#create_repository'
- end
+ it 'returns false when the command fails' do
+ FileUtils.mkdir_p(File.dirname(created_path))
+ # This file will block the creation of the repo's .git directory. That
+ # should cause #create_repository to fail.
+ FileUtils.touch(created_path)
- context 'without gitaly', :skip_gitaly_mock do
- it_behaves_like '#create_repository'
+ expect(gitlab_shell.create_repository(repository_storage, repo_name)).to be_falsy
end
end
@@ -495,34 +489,26 @@ describe Gitlab::Shell do
end
it 'returns true when the command succeeds' do
- expect(gitlab_projects).to receive(:fork_repository).with('nfs-file05', 'fork/path.git') { true }
+ expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:fork_repository)
+ .with(repository.raw_repository) { :gitaly_response_object }
is_expected.to be_truthy
end
it 'return false when the command fails' do
- expect(gitlab_projects).to receive(:fork_repository).with('nfs-file05', 'fork/path.git') { false }
+ expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:fork_repository)
+ .with(repository.raw_repository) { raise GRPC::BadStatus, 'bla' }
is_expected.to be_falsy
end
end
- shared_examples 'fetch_remote' do |gitaly_on|
+ describe '#fetch_remote' do
def fetch_remote(ssh_auth = nil, prune = true)
gitlab_shell.fetch_remote(repository.raw_repository, 'remote-name', ssh_auth: ssh_auth, prune: prune)
end
- def expect_gitlab_projects(fail = false, options = {})
- expect(gitlab_projects).to receive(:fetch_remote).with(
- 'remote-name',
- timeout,
- options
- ).and_return(!fail)
-
- allow(gitlab_projects).to receive(:output).and_return('error') if fail
- end
-
- def expect_gitaly_call(fail, options = {})
+ def expect_call(fail, options = {})
receive_fetch_remote =
if fail
receive(:fetch_remote).and_raise(GRPC::NotFound)
@@ -533,16 +519,6 @@ describe Gitlab::Shell do
expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive_fetch_remote
end
- if gitaly_on
- def expect_call(fail, options = {})
- expect_gitaly_call(fail, options)
- end
- else
- def expect_call(fail, options = {})
- expect_gitlab_projects(fail, options)
- end
- end
-
def build_ssh_auth(opts = {})
defaults = {
ssh_import?: true,
@@ -628,14 +604,6 @@ describe Gitlab::Shell do
expect(fetch_remote(ssh_auth)).to be_truthy
end
end
- end
-
- describe '#fetch_remote local', :skip_gitaly_mock do
- it_should_behave_like 'fetch_remote', false
- end
-
- describe '#fetch_remote gitaly' do
- it_should_behave_like 'fetch_remote', true
context 'gitaly call' do
let(:remote_name) { 'remote-name' }
@@ -643,7 +611,7 @@ describe Gitlab::Shell do
subject do
gitlab_shell.fetch_remote(repository.raw_repository, remote_name,
- forced: true, no_tags: true, ssh_auth: ssh_auth)
+ forced: true, no_tags: true, ssh_auth: ssh_auth)
end
it 'passes the correct params to the gitaly service' do
@@ -658,21 +626,24 @@ describe Gitlab::Shell do
describe '#import_repository' do
let(:import_url) { 'https://gitlab.com/gitlab-org/gitlab-ce.git' }
- it 'returns true when the command succeeds' do
- expect(gitlab_projects).to receive(:import_project).with(import_url, timeout) { true }
+ context 'with gitaly' do
+ it 'returns true when the command succeeds' do
+ expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:import_repository).with(import_url)
- result = gitlab_shell.import_repository(project.repository_storage, project.disk_path, import_url)
+ result = gitlab_shell.import_repository(project.repository_storage, project.disk_path, import_url)
- expect(result).to be_truthy
- end
+ expect(result).to be_truthy
+ end
- it 'raises an exception when the command fails' do
- allow(gitlab_projects).to receive(:output) { 'error' }
- expect(gitlab_projects).to receive(:import_project) { false }
+ it 'raises an exception when the command fails' do
+ expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:import_repository)
+ .with(import_url) { raise GRPC::BadStatus, 'bla' }
+ expect_any_instance_of(Gitlab::Shell::GitalyGitlabProjects).to receive(:output) { 'error'}
- expect do
- gitlab_shell.import_repository(project.repository_storage, project.disk_path, import_url)
- end.to raise_error(Gitlab::Shell::Error, "error")
+ expect do
+ gitlab_shell.import_repository(project.repository_storage, project.disk_path, import_url)
+ end.to raise_error(Gitlab::Shell::Error, "error")
+ end
end
end
end
diff --git a/spec/lib/gitlab/slash_commands/issue_move_spec.rb b/spec/lib/gitlab/slash_commands/issue_move_spec.rb
index d41441c9472..9a990e1fad7 100644
--- a/spec/lib/gitlab/slash_commands/issue_move_spec.rb
+++ b/spec/lib/gitlab/slash_commands/issue_move_spec.rb
@@ -27,7 +27,7 @@ describe Gitlab::SlashCommands::IssueMove, service: true do
set(:other_project) { create(:project, namespace: project.namespace) }
before do
- [project, other_project].each { |prj| prj.add_master(user) }
+ [project, other_project].each { |prj| prj.add_maintainer(user) }
end
subject { described_class.new(project, chat_name) }
diff --git a/spec/lib/gitlab/slash_commands/issue_new_spec.rb b/spec/lib/gitlab/slash_commands/issue_new_spec.rb
index 8e7df946529..724c76ade6e 100644
--- a/spec/lib/gitlab/slash_commands/issue_new_spec.rb
+++ b/spec/lib/gitlab/slash_commands/issue_new_spec.rb
@@ -8,7 +8,7 @@ describe Gitlab::SlashCommands::IssueNew do
let(:regex_match) { described_class.match("issue create bird is the word") }
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
subject do
diff --git a/spec/lib/gitlab/slash_commands/issue_search_spec.rb b/spec/lib/gitlab/slash_commands/issue_search_spec.rb
index 189e9592f1b..47787307990 100644
--- a/spec/lib/gitlab/slash_commands/issue_search_spec.rb
+++ b/spec/lib/gitlab/slash_commands/issue_search_spec.rb
@@ -22,7 +22,7 @@ describe Gitlab::SlashCommands::IssueSearch do
context 'the user has access' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
it 'returns all results' do
diff --git a/spec/lib/gitlab/slash_commands/issue_show_spec.rb b/spec/lib/gitlab/slash_commands/issue_show_spec.rb
index b1db1638237..5c4ba2736ba 100644
--- a/spec/lib/gitlab/slash_commands/issue_show_spec.rb
+++ b/spec/lib/gitlab/slash_commands/issue_show_spec.rb
@@ -9,7 +9,7 @@ describe Gitlab::SlashCommands::IssueShow do
let(:regex_match) { described_class.match("issue show #{issue.iid}") }
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
subject do
diff --git a/spec/lib/gitlab/sql/cte_spec.rb b/spec/lib/gitlab/sql/cte_spec.rb
new file mode 100644
index 00000000000..d6763c7b2e1
--- /dev/null
+++ b/spec/lib/gitlab/sql/cte_spec.rb
@@ -0,0 +1,42 @@
+require 'spec_helper'
+
+describe Gitlab::SQL::CTE, :postgresql do
+ describe '#to_arel' do
+ it 'generates an Arel relation for the CTE body' do
+ relation = User.where(id: 1)
+ cte = described_class.new(:cte_name, relation)
+ sql = cte.to_arel.to_sql
+ name = ActiveRecord::Base.connection.quote_table_name(:cte_name)
+
+ sql1 = ActiveRecord::Base.connection.unprepared_statement do
+ relation.except(:order).to_sql
+ end
+
+ expect(sql).to eq("#{name} AS (#{sql1})")
+ end
+ end
+
+ describe '#alias_to' do
+ it 'returns an alias for the CTE' do
+ cte = described_class.new(:cte_name, nil)
+ table = Arel::Table.new(:kittens)
+
+ source_name = ActiveRecord::Base.connection.quote_table_name(:cte_name)
+ alias_name = ActiveRecord::Base.connection.quote_table_name(:kittens)
+
+ expect(cte.alias_to(table).to_sql).to eq("#{source_name} AS #{alias_name}")
+ end
+ end
+
+ describe '#apply_to' do
+ it 'applies a CTE to an ActiveRecord::Relation' do
+ user = create(:user)
+ cte = described_class.new(:cte_name, User.where(id: user.id))
+
+ relation = cte.apply_to(User.all)
+
+ expect(relation.to_sql).to match(/WITH .+cte_name/)
+ expect(relation.to_a).to eq(User.where(id: user.id).to_a)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sql/glob_spec.rb b/spec/lib/gitlab/sql/glob_spec.rb
index f0bb4294d62..1cf8935bfe3 100644
--- a/spec/lib/gitlab/sql/glob_spec.rb
+++ b/spec/lib/gitlab/sql/glob_spec.rb
@@ -35,8 +35,9 @@ describe Gitlab::SQL::Glob do
value = query("SELECT #{quote(string)} LIKE #{pattern}")
.rows.flatten.first
+ check = Gitlab.rails5? ? true : 't'
case value
- when 't', 1
+ when check, 1
true
else
false
diff --git a/spec/lib/gitlab/themes_spec.rb b/spec/lib/gitlab/themes_spec.rb
index ecacea6bb35..a8213988f70 100644
--- a/spec/lib/gitlab/themes_spec.rb
+++ b/spec/lib/gitlab/themes_spec.rb
@@ -5,9 +5,9 @@ describe Gitlab::Themes, lib: true do
it 'returns a space-separated list of class names' do
css = described_class.body_classes
- expect(css).to include('ui_indigo')
- expect(css).to include(' ui_dark ')
- expect(css).to include(' ui_blue')
+ expect(css).to include('ui-indigo')
+ expect(css).to include('ui-dark')
+ expect(css).to include('ui-blue')
end
end
diff --git a/spec/lib/gitlab/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb
index a3b3dc3be6d..6f5f9938eca 100644
--- a/spec/lib/gitlab/url_blocker_spec.rb
+++ b/spec/lib/gitlab/url_blocker_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Gitlab::UrlBlocker do
describe '#blocked_url?' do
- let(:valid_ports) { Project::VALID_IMPORT_PORTS }
+ let(:ports) { Project::VALID_IMPORT_PORTS }
it 'allows imports from configured web host and port' do
import_url = "http://#{Gitlab.config.gitlab.host}:#{Gitlab.config.gitlab.port}/t.git"
@@ -19,7 +19,13 @@ describe Gitlab::UrlBlocker do
end
it 'returns true for bad port' do
- expect(described_class.blocked_url?('https://gitlab.com:25/foo/foo.git', valid_ports: valid_ports)).to be true
+ expect(described_class.blocked_url?('https://gitlab.com:25/foo/foo.git', ports: ports)).to be true
+ end
+
+ it 'returns true for bad protocol' do
+ expect(described_class.blocked_url?('https://gitlab.com/foo/foo.git', protocols: ['https'])).to be false
+ expect(described_class.blocked_url?('https://gitlab.com/foo/foo.git')).to be false
+ expect(described_class.blocked_url?('https://gitlab.com/foo/foo.git', protocols: ['http'])).to be true
end
it 'returns true for alternative version of 127.0.0.1 (0177.1)' do
@@ -52,20 +58,6 @@ describe Gitlab::UrlBlocker do
end
end
- it 'returns true for a non-alphanumeric username' do
- stub_resolv
-
- aggregate_failures do
- expect(described_class).to be_blocked_url('ssh://-oProxyCommand=whoami@example.com/a')
-
- # The leading character here is a Unicode "soft hyphen"
- expect(described_class).to be_blocked_url('ssh://­oProxyCommand=whoami@example.com/a')
-
- # Unicode alphanumerics are allowed
- expect(described_class).not_to be_blocked_url('ssh://ÄŸitlab@example.com/a')
- end
- end
-
it 'returns true for invalid URL' do
expect(described_class.blocked_url?('http://:8080')).to be true
end
@@ -114,6 +106,38 @@ describe Gitlab::UrlBlocker do
allow(Addrinfo).to receive(:getaddrinfo).and_call_original
end
end
+
+ context 'when enforce_user is' do
+ before do
+ stub_resolv
+ end
+
+ context 'false (default)' do
+ it 'does not block urls with a non-alphanumeric username' do
+ expect(described_class).not_to be_blocked_url('ssh://-oProxyCommand=whoami@example.com/a')
+
+ # The leading character here is a Unicode "soft hyphen"
+ expect(described_class).not_to be_blocked_url('ssh://­oProxyCommand=whoami@example.com/a')
+
+ # Unicode alphanumerics are allowed
+ expect(described_class).not_to be_blocked_url('ssh://ÄŸitlab@example.com/a')
+ end
+ end
+
+ context 'true' do
+ it 'blocks urls with a non-alphanumeric username' do
+ aggregate_failures do
+ expect(described_class).to be_blocked_url('ssh://-oProxyCommand=whoami@example.com/a', enforce_user: true)
+
+ # The leading character here is a Unicode "soft hyphen"
+ expect(described_class).to be_blocked_url('ssh://­oProxyCommand=whoami@example.com/a', enforce_user: true)
+
+ # Unicode alphanumerics are allowed
+ expect(described_class).not_to be_blocked_url('ssh://ÄŸitlab@example.com/a', enforce_user: true)
+ end
+ end
+ end
+ end
end
# Resolv does not support resolving UTF-8 domain names
diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb
index b81749cf428..9f495a5d50b 100644
--- a/spec/lib/gitlab/url_builder_spec.rb
+++ b/spec/lib/gitlab/url_builder_spec.rb
@@ -22,6 +22,31 @@ describe Gitlab::UrlBuilder do
end
end
+ context 'when passing a Milestone' do
+ let(:group) { create(:group) }
+ let(:project) { create(:project, :public, namespace: group) }
+
+ context 'belonging to a project' do
+ it 'returns a proper URL' do
+ milestone = create(:milestone, project: project)
+
+ url = described_class.build(milestone)
+
+ expect(url).to eq "#{Settings.gitlab['url']}/#{milestone.project.full_path}/milestones/#{milestone.iid}"
+ end
+ end
+
+ context 'belonging to a group' do
+ it 'returns a proper URL' do
+ milestone = create(:milestone, group: group)
+
+ url = described_class.build(milestone)
+
+ expect(url).to eq "#{Settings.gitlab['url']}/groups/#{milestone.group.full_path}/-/milestones/#{milestone.iid}"
+ end
+ end
+ end
+
context 'when passing a MergeRequest' do
it 'returns a proper URL' do
merge_request = build_stubbed(:merge_request, iid: 42)
diff --git a/spec/lib/gitlab/url_sanitizer_spec.rb b/spec/lib/gitlab/url_sanitizer_spec.rb
index fc8991fd31f..758a9bc5a2b 100644
--- a/spec/lib/gitlab/url_sanitizer_spec.rb
+++ b/spec/lib/gitlab/url_sanitizer_spec.rb
@@ -92,6 +92,7 @@ describe Gitlab::UrlSanitizer do
context 'credentials in URL' do
where(:url, :credentials) do
'http://foo:bar@example.com' | { user: 'foo', password: 'bar' }
+ 'http://foo:bar:baz@example.com' | { user: 'foo', password: 'bar:baz' }
'http://:bar@example.com' | { user: nil, password: 'bar' }
'http://foo:@example.com' | { user: 'foo', password: nil }
'http://foo@example.com' | { user: 'foo', password: nil }
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index a716e6f5434..20def4fefe2 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -29,19 +29,20 @@ describe Gitlab::UsageData do
active_user_count
counts
recorded_at
- mattermost_enabled
edition
version
+ installation_type
uuid
hostname
- signup
- ldap
- gravatar
- omniauth
- reply_by_email
- container_registry
+ mattermost_enabled
+ signup_enabled
+ ldap_enabled
+ gravatar_enabled
+ omniauth_enabled
+ reply_by_email_enabled
+ container_registry_enabled
+ gitlab_shared_runners_enabled
gitlab_pages
- gitlab_shared_runners
git
database
avg_cycle_analytics
@@ -128,13 +129,14 @@ describe Gitlab::UsageData do
subject { described_class.features_usage_data_ce }
it 'gathers feature usage data' do
- expect(subject[:signup]).to eq(Gitlab::CurrentSettings.allow_signup?)
- expect(subject[:ldap]).to eq(Gitlab.config.ldap.enabled)
- expect(subject[:gravatar]).to eq(Gitlab::CurrentSettings.gravatar_enabled?)
- expect(subject[:omniauth]).to eq(Gitlab.config.omniauth.enabled)
- expect(subject[:reply_by_email]).to eq(Gitlab::IncomingEmail.enabled?)
- expect(subject[:container_registry]).to eq(Gitlab.config.registry.enabled)
- expect(subject[:gitlab_shared_runners]).to eq(Gitlab.config.gitlab_ci.shared_runners_enabled)
+ expect(subject[:mattermost_enabled]).to eq(Gitlab.config.mattermost.enabled)
+ expect(subject[:signup_enabled]).to eq(Gitlab::CurrentSettings.allow_signup?)
+ expect(subject[:ldap_enabled]).to eq(Gitlab.config.ldap.enabled)
+ expect(subject[:gravatar_enabled]).to eq(Gitlab::CurrentSettings.gravatar_enabled?)
+ expect(subject[:omniauth_enabled]).to eq(Gitlab.config.omniauth.enabled)
+ expect(subject[:reply_by_email_enabled]).to eq(Gitlab::IncomingEmail.enabled?)
+ expect(subject[:container_registry_enabled]).to eq(Gitlab.config.registry.enabled)
+ expect(subject[:gitlab_shared_runners_enabled]).to eq(Gitlab.config.gitlab_ci.shared_runners_enabled)
end
end
@@ -156,6 +158,7 @@ describe Gitlab::UsageData do
it "gathers license data" do
expect(subject[:uuid]).to eq(Gitlab::CurrentSettings.uuid)
expect(subject[:version]).to eq(Gitlab::VERSION)
+ expect(subject[:installation_type]).to eq(Gitlab::INSTALLATION_TYPE)
expect(subject[:active_user_count]).to eq(User.active.count)
expect(subject[:recorded_at]).to be_a(Time)
end
diff --git a/spec/lib/gitlab/user_access_spec.rb b/spec/lib/gitlab/user_access_spec.rb
index 97b6069f64d..9da06bb40f4 100644
--- a/spec/lib/gitlab/user_access_spec.rb
+++ b/spec/lib/gitlab/user_access_spec.rb
@@ -9,8 +9,8 @@ describe Gitlab::UserAccess do
describe '#can_push_to_branch?' do
describe 'push to none protected branch' do
- it 'returns true if user is a master' do
- project.add_master(user)
+ it 'returns true if user is a maintainer' do
+ project.add_maintainer(user)
expect(access.can_push_to_branch?('random_branch')).to be_truthy
end
@@ -38,8 +38,8 @@ describe Gitlab::UserAccess do
expect(access.can_push_to_branch?('master')).to be_truthy
end
- it 'returns true if user is master' do
- empty_project.add_master(user)
+ it 'returns true if user is maintainer' do
+ empty_project.add_maintainer(user)
expect(project_access.can_push_to_branch?('master')).to be_truthy
end
@@ -83,8 +83,8 @@ describe Gitlab::UserAccess do
expect(access.can_push_to_branch?(branch.name)).to be_truthy
end
- it 'returns true if user is a master' do
- project.add_master(user)
+ it 'returns true if user is a maintainer' do
+ project.add_maintainer(user)
expect(access.can_push_to_branch?(branch.name)).to be_truthy
end
@@ -113,8 +113,8 @@ describe Gitlab::UserAccess do
@branch = create :protected_branch, :developers_can_push, project: project
end
- it 'returns true if user is a master' do
- project.add_master(user)
+ it 'returns true if user is a maintainer' do
+ project.add_maintainer(user)
expect(access.can_push_to_branch?(@branch.name)).to be_truthy
end
@@ -142,7 +142,7 @@ describe Gitlab::UserAccess do
target_project: canonical_project,
source_project: project,
source_branch: 'awesome-feature',
- allow_maintainer_to_push: true
+ allow_collaboration: true
)
end
@@ -170,8 +170,8 @@ describe Gitlab::UserAccess do
@branch = create :protected_branch, :developers_can_merge, project: project
end
- it 'returns true if user is a master' do
- project.add_master(user)
+ it 'returns true if user is a maintainer' do
+ project.add_maintainer(user)
expect(access.can_merge_to_branch?(@branch.name)).to be_truthy
end
@@ -192,8 +192,8 @@ describe Gitlab::UserAccess do
describe '#can_create_tag?' do
describe 'push to none protected tag' do
- it 'returns true if user is a master' do
- project.add_user(user, :master)
+ it 'returns true if user is a maintainer' do
+ project.add_user(user, :maintainer)
expect(access.can_create_tag?('random_tag')).to be_truthy
end
@@ -215,8 +215,8 @@ describe Gitlab::UserAccess do
let(:tag) { create(:protected_tag, project: project, name: "test") }
let(:not_existing_tag) { create :protected_tag, project: project }
- it 'returns true if user is a master' do
- project.add_user(user, :master)
+ it 'returns true if user is a maintainer' do
+ project.add_user(user, :maintainer)
expect(access.can_create_tag?(tag.name)).to be_truthy
end
@@ -239,8 +239,8 @@ describe Gitlab::UserAccess do
@tag = create(:protected_tag, :developers_can_create, project: project)
end
- it 'returns true if user is a master' do
- project.add_user(user, :master)
+ it 'returns true if user is a maintainer' do
+ project.add_user(user, :maintainer)
expect(access.can_create_tag?(@tag.name)).to be_truthy
end
@@ -261,8 +261,8 @@ describe Gitlab::UserAccess do
describe '#can_delete_branch?' do
describe 'delete unprotected branch' do
- it 'returns true if user is a master' do
- project.add_user(user, :master)
+ it 'returns true if user is a maintainer' do
+ project.add_user(user, :maintainer)
expect(access.can_delete_branch?('random_branch')).to be_truthy
end
@@ -283,8 +283,8 @@ describe Gitlab::UserAccess do
describe 'delete protected branch' do
let(:branch) { create(:protected_branch, project: project, name: "test") }
- it 'returns true if user is a master' do
- project.add_user(user, :master)
+ it 'returns true if user is a maintainer' do
+ project.add_user(user, :maintainer)
expect(access.can_delete_branch?(branch.name)).to be_truthy
end
diff --git a/spec/lib/gitlab/utils/override_spec.rb b/spec/lib/gitlab/utils/override_spec.rb
index 7c97cee982a..fc08ebcfc6d 100644
--- a/spec/lib/gitlab/utils/override_spec.rb
+++ b/spec/lib/gitlab/utils/override_spec.rb
@@ -1,7 +1,13 @@
-require 'spec_helper'
+require 'fast_spec_helper'
describe Gitlab::Utils::Override do
- let(:base) { Struct.new(:good) }
+ let(:base) do
+ Struct.new(:good) do
+ def self.good
+ 0
+ end
+ end
+ end
let(:derived) { Class.new(base).tap { |m| m.extend described_class } }
let(:extension) { Module.new.tap { |m| m.extend described_class } }
@@ -9,6 +15,14 @@ describe Gitlab::Utils::Override do
let(:prepending_class) { base.tap { |m| m.prepend extension } }
let(:including_class) { base.tap { |m| m.include extension } }
+ let(:prepending_class_methods) do
+ base.tap { |m| m.singleton_class.prepend extension }
+ end
+
+ let(:extending_class_methods) do
+ base.tap { |m| m.extend extension }
+ end
+
let(:klass) { subject }
def good(mod)
@@ -36,7 +50,7 @@ describe Gitlab::Utils::Override do
shared_examples 'checking as intended' do
it 'checks ok for overriding method' do
good(subject)
- result = klass.new(0).good
+ result = instance.good
expect(result).to eq(1)
described_class.verify!
@@ -45,7 +59,25 @@ describe Gitlab::Utils::Override do
it 'raises NotImplementedError when it is not overriding anything' do
expect do
bad(subject)
- klass.new(0).bad
+ instance.bad
+ described_class.verify!
+ end.to raise_error(NotImplementedError)
+ end
+ end
+
+ shared_examples 'checking as intended, nothing was overridden' do
+ it 'raises NotImplementedError because it is not overriding it' do
+ expect do
+ good(subject)
+ instance.good
+ described_class.verify!
+ end.to raise_error(NotImplementedError)
+ end
+
+ it 'raises NotImplementedError when it is not overriding anything' do
+ expect do
+ bad(subject)
+ instance.bad
described_class.verify!
end.to raise_error(NotImplementedError)
end
@@ -54,7 +86,7 @@ describe Gitlab::Utils::Override do
shared_examples 'nothing happened' do
it 'does not complain when it is overriding something' do
good(subject)
- result = klass.new(0).good
+ result = instance.good
expect(result).to eq(1)
described_class.verify!
@@ -62,7 +94,7 @@ describe Gitlab::Utils::Override do
it 'does not complain when it is not overriding anything' do
bad(subject)
- result = klass.new(0).bad
+ result = instance.bad
expect(result).to eq(true)
described_class.verify!
@@ -75,83 +107,97 @@ describe Gitlab::Utils::Override do
end
describe '#override' do
- context 'when STATIC_VERIFICATION is set' do
- before do
- stub_env('STATIC_VERIFICATION', 'true')
- end
+ context 'when instance is klass.new(0)' do
+ let(:instance) { klass.new(0) }
- context 'when subject is a class' do
- subject { derived }
+ context 'when STATIC_VERIFICATION is set' do
+ before do
+ stub_env('STATIC_VERIFICATION', 'true')
+ end
- it_behaves_like 'checking as intended'
- end
+ context 'when subject is a class' do
+ subject { derived }
+
+ it_behaves_like 'checking as intended'
+ end
+
+ context 'when subject is a module, and class is prepending it' do
+ subject { extension }
+ let(:klass) { prepending_class }
+
+ it_behaves_like 'checking as intended'
+ end
- context 'when subject is a module, and class is prepending it' do
- subject { extension }
- let(:klass) { prepending_class }
+ context 'when subject is a module, and class is including it' do
+ subject { extension }
+ let(:klass) { including_class }
- it_behaves_like 'checking as intended'
+ it_behaves_like 'checking as intended, nothing was overridden'
+ end
end
- context 'when subject is a module, and class is including it' do
- subject { extension }
- let(:klass) { including_class }
+ context 'when STATIC_VERIFICATION is not set' do
+ before do
+ stub_env('STATIC_VERIFICATION', nil)
+ end
- it 'raises NotImplementedError because it is not overriding it' do
- expect do
- good(subject)
- klass.new(0).good
- described_class.verify!
- end.to raise_error(NotImplementedError)
+ context 'when subject is a class' do
+ subject { derived }
+
+ it_behaves_like 'nothing happened'
end
- it 'raises NotImplementedError when it is not overriding anything' do
- expect do
- bad(subject)
- klass.new(0).bad
- described_class.verify!
- end.to raise_error(NotImplementedError)
+ context 'when subject is a module, and class is prepending it' do
+ subject { extension }
+ let(:klass) { prepending_class }
+
+ it_behaves_like 'nothing happened'
end
- end
- end
- end
- context 'when STATIC_VERIFICATION is not set' do
- before do
- stub_env('STATIC_VERIFICATION', nil)
- end
+ context 'when subject is a module, and class is including it' do
+ subject { extension }
+ let(:klass) { including_class }
- context 'when subject is a class' do
- subject { derived }
+ it 'does not complain when it is overriding something' do
+ good(subject)
+ result = instance.good
- it_behaves_like 'nothing happened'
- end
+ expect(result).to eq(0)
+ described_class.verify!
+ end
- context 'when subject is a module, and class is prepending it' do
- subject { extension }
- let(:klass) { prepending_class }
+ it 'does not complain when it is not overriding anything' do
+ bad(subject)
+ result = instance.bad
- it_behaves_like 'nothing happened'
+ expect(result).to eq(true)
+ described_class.verify!
+ end
+ end
+ end
end
- context 'when subject is a module, and class is including it' do
- subject { extension }
- let(:klass) { including_class }
+ context 'when instance is klass' do
+ let(:instance) { klass }
- it 'does not complain when it is overriding something' do
- good(subject)
- result = klass.new(0).good
+ context 'when STATIC_VERIFICATION is set' do
+ before do
+ stub_env('STATIC_VERIFICATION', 'true')
+ end
- expect(result).to eq(0)
- described_class.verify!
- end
+ context 'when subject is a module, and class is prepending it' do
+ subject { extension }
+ let(:klass) { prepending_class_methods }
- it 'does not complain when it is not overriding anything' do
- bad(subject)
- result = klass.new(0).bad
+ it_behaves_like 'checking as intended'
+ end
- expect(result).to eq(true)
- described_class.verify!
+ context 'when subject is a module, and class is extending it' do
+ subject { extension }
+ let(:klass) { extending_class_methods }
+
+ it_behaves_like 'checking as intended, nothing was overridden'
+ end
end
end
end
diff --git a/spec/lib/gitlab/verify/job_artifacts_spec.rb b/spec/lib/gitlab/verify/job_artifacts_spec.rb
index ec490bdfde2..6e916a56564 100644
--- a/spec/lib/gitlab/verify/job_artifacts_spec.rb
+++ b/spec/lib/gitlab/verify/job_artifacts_spec.rb
@@ -21,15 +21,38 @@ describe Gitlab::Verify::JobArtifacts do
FileUtils.rm_f(artifact.file.path)
expect(failures.keys).to contain_exactly(artifact)
- expect(failure).to be_a(Errno::ENOENT)
- expect(failure.to_s).to include(artifact.file.path)
+ expect(failure).to include('No such file or directory')
+ expect(failure).to include(artifact.file.path)
end
it 'fails artifacts with a mismatched checksum' do
File.truncate(artifact.file.path, 0)
expect(failures.keys).to contain_exactly(artifact)
- expect(failure.to_s).to include('Checksum mismatch')
+ expect(failure).to include('Checksum mismatch')
+ end
+
+ context 'with remote files' do
+ let(:file) { double(:file) }
+
+ before do
+ stub_artifacts_object_storage
+ artifact.update!(file_store: ObjectStorage::Store::REMOTE)
+ expect(CarrierWave::Storage::Fog::File).to receive(:new).and_return(file)
+ end
+
+ it 'passes artifacts in object storage that exist' do
+ expect(file).to receive(:exists?).and_return(true)
+
+ expect(failures).to eq({})
+ end
+
+ it 'fails artifacts in object storage that do not exist' do
+ expect(file).to receive(:exists?).and_return(false)
+
+ expect(failures.keys).to contain_exactly(artifact)
+ expect(failure).to include('Remote object does not exist')
+ end
end
end
end
diff --git a/spec/lib/gitlab/verify/lfs_objects_spec.rb b/spec/lib/gitlab/verify/lfs_objects_spec.rb
index 0f890e2c7ce..2feaedd6f14 100644
--- a/spec/lib/gitlab/verify/lfs_objects_spec.rb
+++ b/spec/lib/gitlab/verify/lfs_objects_spec.rb
@@ -21,30 +21,37 @@ describe Gitlab::Verify::LfsObjects do
FileUtils.rm_f(lfs_object.file.path)
expect(failures.keys).to contain_exactly(lfs_object)
- expect(failure).to be_a(Errno::ENOENT)
- expect(failure.to_s).to include(lfs_object.file.path)
+ expect(failure).to include('No such file or directory')
+ expect(failure).to include(lfs_object.file.path)
end
it 'fails LFS objects with a mismatched oid' do
File.truncate(lfs_object.file.path, 0)
expect(failures.keys).to contain_exactly(lfs_object)
- expect(failure.to_s).to include('Checksum mismatch')
+ expect(failure).to include('Checksum mismatch')
end
context 'with remote files' do
+ let(:file) { double(:file) }
+
before do
stub_lfs_object_storage
+ lfs_object.update!(file_store: ObjectStorage::Store::REMOTE)
+ expect(CarrierWave::Storage::Fog::File).to receive(:new).and_return(file)
end
- it 'skips LFS objects in object storage' do
- local_failure = create(:lfs_object)
- create(:lfs_object, :object_storage)
+ it 'passes LFS objects in object storage that exist' do
+ expect(file).to receive(:exists?).and_return(true)
+
+ expect(failures).to eq({})
+ end
- failures = {}
- described_class.new(batch_size: 10).run_batches { |_, failed| failures.merge!(failed) }
+ it 'fails LFS objects in object storage that do not exist' do
+ expect(file).to receive(:exists?).and_return(false)
- expect(failures.keys).to contain_exactly(local_failure)
+ expect(failures.keys).to contain_exactly(lfs_object)
+ expect(failure).to include('Remote object does not exist')
end
end
end
diff --git a/spec/lib/gitlab/verify/uploads_spec.rb b/spec/lib/gitlab/verify/uploads_spec.rb
index 85768308edc..38c30fab1ba 100644
--- a/spec/lib/gitlab/verify/uploads_spec.rb
+++ b/spec/lib/gitlab/verify/uploads_spec.rb
@@ -23,37 +23,73 @@ describe Gitlab::Verify::Uploads do
FileUtils.rm_f(upload.absolute_path)
expect(failures.keys).to contain_exactly(upload)
- expect(failure).to be_a(Errno::ENOENT)
- expect(failure.to_s).to include(upload.absolute_path)
+ expect(failure).to include('No such file or directory')
+ expect(failure).to include(upload.absolute_path)
end
it 'fails uploads with a mismatched checksum' do
upload.update!(checksum: 'something incorrect')
expect(failures.keys).to contain_exactly(upload)
- expect(failure.to_s).to include('Checksum mismatch')
+ expect(failure).to include('Checksum mismatch')
end
it 'fails uploads with a missing precalculated checksum' do
upload.update!(checksum: '')
expect(failures.keys).to contain_exactly(upload)
- expect(failure.to_s).to include('Checksum missing')
+ expect(failure).to include('Checksum missing')
end
context 'with remote files' do
+ let(:file) { double(:file) }
+
before do
stub_uploads_object_storage(AvatarUploader)
+ upload.update!(store: ObjectStorage::Store::REMOTE)
end
- it 'skips uploads in object storage' do
- local_failure = create(:upload)
- create(:upload, :object_storage)
+ describe 'returned hash object' do
+ before do
+ expect(CarrierWave::Storage::Fog::File).to receive(:new).and_return(file)
+ end
+
+ it 'passes uploads in object storage that exist' do
+ expect(file).to receive(:exists?).and_return(true)
+
+ expect(failures).to eq({})
+ end
+
+ it 'fails uploads in object storage that do not exist' do
+ expect(file).to receive(:exists?).and_return(false)
+
+ expect(failures.keys).to contain_exactly(upload)
+ expect(failure).to include('Remote object does not exist')
+ end
+ end
+
+ describe 'performance' do
+ before do
+ allow(file).to receive(:exists?)
+ allow(CarrierWave::Storage::Fog::File).to receive(:new).and_return(file)
+ end
+
+ it "avoids N+1 queries" do
+ control_count = ActiveRecord::QueryRecorder.new { perform_task }
+
+ # Create additional uploads in object storage
+ projects = create_list(:project, 3, :with_avatar)
+ uploads = projects.flat_map(&:uploads)
+ uploads.each do |upload|
+ upload.update!(store: ObjectStorage::Store::REMOTE)
+ end
- failures = {}
- described_class.new(batch_size: 10).run_batches { |_, failed| failures.merge!(failed) }
+ expect { perform_task }.not_to exceed_query_limit(control_count)
+ end
- expect(failures.keys).to contain_exactly(local_failure)
+ def perform_task
+ described_class.new(batch_size: 100).run_batches { }
+ end
end
end
end
diff --git a/spec/lib/gitlab/wiki_file_finder_spec.rb b/spec/lib/gitlab/wiki_file_finder_spec.rb
new file mode 100644
index 00000000000..025d1203dc5
--- /dev/null
+++ b/spec/lib/gitlab/wiki_file_finder_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe Gitlab::WikiFileFinder do
+ describe '#find' do
+ let(:project) { create(:project, :public, :wiki_repo) }
+ let(:wiki) { build(:project_wiki, project: project) }
+
+ before do
+ wiki.create_page('Files/Title', 'Content')
+ wiki.create_page('CHANGELOG', 'Files example')
+ end
+
+ it_behaves_like 'file finder' do
+ subject { described_class.new(project, project.wiki.default_branch) }
+
+ let(:expected_file_by_name) { 'Files/Title.md' }
+ let(:expected_file_by_content) { 'CHANGELOG.md' }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index 660671cefaf..23869f3d2da 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -36,22 +36,20 @@ describe Gitlab::Workhorse do
allow(described_class).to receive(:git_archive_cache_disabled?).and_return(cache_disabled)
end
- context 'when Gitaly workhorse_archive feature is enabled' do
- it 'sets the header correctly' do
- key, command, params = decode_workhorse_header(subject)
+ it 'sets the header correctly' do
+ key, command, params = decode_workhorse_header(subject)
- expect(key).to eq('Gitlab-Workhorse-Send-Data')
- expect(command).to eq('git-archive')
- expect(params).to include(gitaly_params)
- end
+ expect(key).to eq('Gitlab-Workhorse-Send-Data')
+ expect(command).to eq('git-archive')
+ expect(params).to include(gitaly_params)
+ end
- context 'when archive caching is disabled' do
- let(:cache_disabled) { true }
+ context 'when archive caching is disabled' do
+ let(:cache_disabled) { true }
- it 'tells workhorse not to use the cache' do
- _, _, params = decode_workhorse_header(subject)
- expect(params).to include({ 'DisableCache' => true })
- end
+ it 'tells workhorse not to use the cache' do
+ _, _, params = decode_workhorse_header(subject)
+ expect(params).to include({ 'DisableCache' => true })
end
end
@@ -70,34 +68,22 @@ describe Gitlab::Workhorse do
let(:diff_refs) { double(base_sha: "base", head_sha: "head") }
subject { described_class.send_git_patch(repository, diff_refs) }
- 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({
- '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', :disable_gitaly do
- it 'sets the header correctly' do
- key, command, params = decode_workhorse_header(subject)
+ 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
+ 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
@@ -143,34 +129,22 @@ describe Gitlab::Workhorse do
let(:diff_refs) { double(base_sha: "base", head_sha: "head") }
subject { described_class.send_git_diff(repository, diff_refs) }
- 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-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', :disable_gitaly do
- it 'sets the header correctly' do
- key, command, params = decode_workhorse_header(subject)
+ 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
+ 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
@@ -189,7 +163,7 @@ describe Gitlab::Workhorse do
end
it 'accepts a trailing newline' do
- open(described_class.secret_path, 'a') { |f| f.write "\n" }
+ File.open(described_class.secret_path, 'a') { |f| f.write "\n" }
expect(subject.length).to eq(32)
end
@@ -425,34 +399,22 @@ describe Gitlab::Workhorse do
subject { described_class.send_git_blob(repository, blob) }
- context 'when Gitaly workhorse_raw_show 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-blob')
- expect(params).to eq({
- 'GitalyServer' => {
- address: Gitlab::GitalyClient.address(project.repository_storage),
- token: Gitlab::GitalyClient.token(project.repository_storage)
- },
- 'GetBlobRequest' => {
- repository: repository.gitaly_repository.to_h,
- oid: blob.id,
- limit: -1
- }
- }.deep_stringify_keys)
- end
- end
-
- context 'when Gitaly workhorse_raw_show feature is disabled', :disable_gitaly do
- it 'sets the header correctly' do
- key, command, params = decode_workhorse_header(subject)
+ 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-blob')
- expect(params).to eq('RepoPath' => repository.path_to_repo, 'BlobId' => blob.id)
- end
+ expect(key).to eq('Gitlab-Workhorse-Send-Data')
+ expect(command).to eq('git-blob')
+ expect(params).to eq({
+ 'GitalyServer' => {
+ address: Gitlab::GitalyClient.address(project.repository_storage),
+ token: Gitlab::GitalyClient.token(project.repository_storage)
+ },
+ 'GetBlobRequest' => {
+ repository: repository.gitaly_repository.to_h,
+ oid: blob.id,
+ limit: -1
+ }
+ }.deep_stringify_keys)
end
end
diff --git a/spec/lib/gitlab_spec.rb b/spec/lib/gitlab_spec.rb
index da146e24893..d63f448883b 100644
--- a/spec/lib/gitlab_spec.rb
+++ b/spec/lib/gitlab_spec.rb
@@ -8,6 +8,66 @@ describe Gitlab do
expect(described_class.root).to eq(Pathname.new(File.expand_path('../..', __dir__)))
end
end
+ describe '.revision' do
+ let(:cmd) { %W[#{described_class.config.git.bin_path} log --pretty=format:%h -n 1] }
+
+ around do |example|
+ described_class.instance_variable_set(:@_revision, nil)
+ example.run
+ described_class.instance_variable_set(:@_revision, nil)
+ end
+
+ context 'when a REVISION file exists' do
+ before do
+ expect(File).to receive(:exist?)
+ .with(described_class.root.join('REVISION'))
+ .and_return(true)
+ end
+
+ it 'returns the actual Git revision' do
+ expect(File).to receive(:read)
+ .with(described_class.root.join('REVISION'))
+ .and_return("abc123\n")
+
+ expect(described_class.revision).to eq('abc123')
+ end
+
+ it 'memoizes the revision' do
+ expect(File).to receive(:read)
+ .once
+ .with(described_class.root.join('REVISION'))
+ .and_return("abc123\n")
+
+ 2.times { described_class.revision }
+ end
+ end
+
+ context 'when no REVISION file exist' do
+ context 'when the Git command succeeds' do
+ before do
+ expect(Gitlab::Popen).to receive(:popen_with_detail)
+ .with(cmd)
+ .and_return(Gitlab::Popen::Result.new(cmd, 'abc123', '', double(success?: true)))
+ end
+
+ it 'returns the actual Git revision' do
+ expect(described_class.revision).to eq('abc123')
+ end
+ end
+
+ context 'when the Git command fails' do
+ before do
+ expect(Gitlab::Popen).to receive(:popen_with_detail)
+ .with(cmd)
+ .and_return(Gitlab::Popen::Result.new(cmd, '', 'fatal: Not a git repository', double('Process::Status', success?: false)))
+ end
+
+ it 'returns "Unknown"' do
+ expect(described_class.revision).to eq('Unknown')
+ end
+ end
+ end
+ end
describe '.com?' do
it 'is true when on GitLab.com' do
diff --git a/spec/lib/google_api/cloud_platform/client_spec.rb b/spec/lib/google_api/cloud_platform/client_spec.rb
index db9d9158b29..27cb3198e5b 100644
--- a/spec/lib/google_api/cloud_platform/client_spec.rb
+++ b/spec/lib/google_api/cloud_platform/client_spec.rb
@@ -50,30 +50,6 @@ describe GoogleApi::CloudPlatform::Client do
end
end
- describe '#projects_list' do
- subject { client.projects_list }
- let(:projects) { double }
-
- before do
- allow_any_instance_of(Google::Apis::CloudresourcemanagerV1::CloudResourceManagerService)
- .to receive(:fetch_all).and_return(projects)
- end
-
- it { is_expected.to eq(projects) }
- end
-
- describe '#projects_get_billing_info' do
- subject { client.projects_get_billing_info('project') }
- let(:billing_info) { double }
-
- before do
- allow_any_instance_of(Google::Apis::CloudbillingV1::CloudbillingService)
- .to receive(:get_project_billing_info).and_return(billing_info)
- end
-
- it { is_expected.to eq(billing_info) }
- end
-
describe '#projects_zones_clusters_get' do
subject { client.projects_zones_clusters_get(spy, spy, spy) }
let(:gke_cluster) { double }
diff --git a/spec/lib/mattermost/command_spec.rb b/spec/lib/mattermost/command_spec.rb
index 8ba15ae0f38..7c194749dfb 100644
--- a/spec/lib/mattermost/command_spec.rb
+++ b/spec/lib/mattermost/command_spec.rb
@@ -21,13 +21,13 @@ describe Mattermost::Command do
context 'for valid trigger word' do
before do
- stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create')
+ stub_request(:post, 'http://mattermost.example.com/api/v4/commands')
.with(body: {
team_id: 'abc',
trigger: 'gitlab'
}.to_json)
.to_return(
- status: 200,
+ status: 201,
headers: { 'Content-Type' => 'application/json' },
body: { token: 'token' }.to_json
)
@@ -40,16 +40,16 @@ describe Mattermost::Command do
context 'for error message' do
before do
- stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create')
+ stub_request(:post, 'http://mattermost.example.com/api/v4/commands')
.to_return(
- status: 500,
+ status: 400,
headers: { 'Content-Type' => 'application/json' },
body: {
id: 'api.command.duplicate_trigger.app_error',
message: 'This trigger word is already in use. Please choose another word.',
detailed_error: '',
request_id: 'obc374man7bx5r3dbc1q5qhf3r',
- status_code: 500
+ status_code: 400
}.to_json
)
end
diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb
index c855643c4d8..b7687d48c68 100644
--- a/spec/lib/mattermost/session_spec.rb
+++ b/spec/lib/mattermost/session_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Mattermost::Session, type: :request do
+ include ExclusiveLeaseHelpers
+
let(:user) { create(:user) }
let(:gitlab_url) { "http://gitlab.com" }
@@ -22,8 +24,8 @@ describe Mattermost::Session, type: :request do
let(:location) { 'http://location.tld' }
let(:cookie_header) {'MMOAUTH=taskik8az7rq8k6rkpuas7htia; Path=/;'}
let!(:stub) do
- WebMock.stub_request(:get, "#{mattermost_url}/api/v3/oauth/gitlab/login")
- .to_return(headers: { 'location' => location, 'Set-Cookie' => cookie_header }, status: 307)
+ WebMock.stub_request(:get, "#{mattermost_url}/oauth/gitlab/login")
+ .to_return(headers: { 'location' => location, 'Set-Cookie' => cookie_header }, status: 302)
end
context 'without oauth uri' do
@@ -76,7 +78,7 @@ describe Mattermost::Session, type: :request do
end
end
- WebMock.stub_request(:post, "#{mattermost_url}/api/v3/users/logout")
+ WebMock.stub_request(:post, "#{mattermost_url}/api/v4/users/logout")
.to_return(headers: { Authorization: 'token thisworksnow' }, status: 200)
end
@@ -97,26 +99,20 @@ describe Mattermost::Session, type: :request do
end
end
- context 'with lease' do
- before do
- allow(subject).to receive(:lease_try_obtain).and_return('aldkfjsldfk')
- end
+ context 'exclusive lease' do
+ let(:lease_key) { 'mattermost:session' }
it 'tries to obtain a lease' do
- expect(subject).to receive(:lease_try_obtain)
- expect(Gitlab::ExclusiveLease).to receive(:cancel)
+ expect_to_obtain_exclusive_lease(lease_key, 'uuid')
+ expect_to_cancel_exclusive_lease(lease_key, 'uuid')
# Cannot setup a session, but we should still cancel the lease
expect { subject.with_session }.to raise_error(Mattermost::NoSessionError)
end
- end
- context 'without lease' do
- before do
- allow(subject).to receive(:lease_try_obtain).and_return(nil)
- end
+ it 'returns a NoSessionError error without lease' do
+ stub_exclusive_lease_taken(lease_key)
- it 'returns a NoSessionError error' do
expect { subject.with_session }.to raise_error(Mattermost::NoSessionError)
end
end
diff --git a/spec/lib/mattermost/team_spec.rb b/spec/lib/mattermost/team_spec.rb
index 2cfa6802612..030aa5d06a8 100644
--- a/spec/lib/mattermost/team_spec.rb
+++ b/spec/lib/mattermost/team_spec.rb
@@ -12,26 +12,28 @@ describe Mattermost::Team do
describe '#all' do
subject { described_class.new(nil).all }
+ let(:test_team) do
+ {
+ "id" => "xiyro8huptfhdndadpz8r3wnbo",
+ "create_at" => 1482174222155,
+ "update_at" => 1482174222155,
+ "delete_at" => 0,
+ "display_name" => "chatops",
+ "name" => "chatops",
+ "email" => "admin@example.com",
+ "type" => "O",
+ "company_name" => "",
+ "allowed_domains" => "",
+ "invite_id" => "o4utakb9jtb7imctdfzbf9r5ro",
+ "allow_open_invite" => false
+ }
+ end
+
context 'for valid request' do
- let(:response) do
- { "xiyro8huptfhdndadpz8r3wnbo" => {
- "id" => "xiyro8huptfhdndadpz8r3wnbo",
- "create_at" => 1482174222155,
- "update_at" => 1482174222155,
- "delete_at" => 0,
- "display_name" => "chatops",
- "name" => "chatops",
- "email" => "admin@example.com",
- "type" => "O",
- "company_name" => "",
- "allowed_domains" => "",
- "invite_id" => "o4utakb9jtb7imctdfzbf9r5ro",
- "allow_open_invite" => false
- } }
- end
+ let(:response) { [test_team] }
before do
- stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all')
+ stub_request(:get, 'http://mattermost.example.com/api/v4/users/me/teams')
.to_return(
status: 200,
headers: { 'Content-Type' => 'application/json' },
@@ -39,14 +41,14 @@ describe Mattermost::Team do
)
end
- it 'returns a token' do
- is_expected.to eq(response.values)
+ it 'returns teams' do
+ is_expected.to eq(response)
end
end
context 'for error message' do
before do
- stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all')
+ stub_request(:get, 'http://mattermost.example.com/api/v4/users/me/teams')
.to_return(
status: 500,
headers: { 'Content-Type' => 'application/json' },
@@ -89,9 +91,9 @@ describe Mattermost::Team do
end
before do
- stub_request(:post, "http://mattermost.example.com/api/v3/teams/create")
+ stub_request(:post, "http://mattermost.example.com/api/v4/teams")
.to_return(
- status: 200,
+ status: 201,
body: response.to_json,
headers: { 'Content-Type' => 'application/json' }
)
@@ -104,7 +106,7 @@ describe Mattermost::Team do
context 'for existing team' do
before do
- stub_request(:post, 'http://mattermost.example.com/api/v3/teams/create')
+ stub_request(:post, 'http://mattermost.example.com/api/v4/teams')
.to_return(
status: 400,
headers: { 'Content-Type' => 'application/json' },
diff --git a/spec/lib/microsoft_teams/notifier_spec.rb b/spec/lib/microsoft_teams/notifier_spec.rb
index 3035693812f..c9756544bd6 100644
--- a/spec/lib/microsoft_teams/notifier_spec.rb
+++ b/spec/lib/microsoft_teams/notifier_spec.rb
@@ -8,7 +8,7 @@ describe MicrosoftTeams::Notifier do
let(:options) do
{
title: 'JohnDoe4/project2',
- pretext: '[[JohnDoe4/project2](http://localhost/namespace2/gitlabhq)] Issue [#1 Awesome issue](http://localhost/namespace2/gitlabhq/issues/1) opened by user6',
+ summary: '[[JohnDoe4/project2](http://localhost/namespace2/gitlabhq)] Issue [#1 Awesome issue](http://localhost/namespace2/gitlabhq/issues/1) opened by user6',
activity: {
title: 'Issue opened by user6',
subtitle: 'in [JohnDoe4/project2](http://localhost/namespace2/gitlabhq)',
diff --git a/spec/lib/object_storage/direct_upload_spec.rb b/spec/lib/object_storage/direct_upload_spec.rb
new file mode 100644
index 00000000000..e0569218d78
--- /dev/null
+++ b/spec/lib/object_storage/direct_upload_spec.rb
@@ -0,0 +1,168 @@
+require 'spec_helper'
+
+describe ObjectStorage::DirectUpload do
+ let(:credentials) do
+ {
+ provider: 'AWS',
+ aws_access_key_id: 'AWS_ACCESS_KEY_ID',
+ aws_secret_access_key: 'AWS_SECRET_ACCESS_KEY'
+ }
+ end
+
+ let(:storage_url) { 'https://uploads.s3.amazonaws.com/' }
+
+ let(:bucket_name) { 'uploads' }
+ let(:object_name) { 'tmp/uploads/my-file' }
+ let(:maximum_size) { 1.gigabyte }
+
+ let(:direct_upload) { described_class.new(credentials, bucket_name, object_name, has_length: has_length, maximum_size: maximum_size) }
+
+ before do
+ Fog.unmock!
+ end
+
+ describe '#has_length' do
+ context 'is known' do
+ let(:has_length) { true }
+ let(:maximum_size) { nil }
+
+ it "maximum size is not required" do
+ expect { direct_upload }.not_to raise_error
+ end
+ end
+
+ context 'is unknown' do
+ let(:has_length) { false }
+
+ context 'and maximum size is specified' do
+ let(:maximum_size) { 1.gigabyte }
+
+ it "does not raise an error" do
+ expect { direct_upload }.not_to raise_error
+ end
+ end
+
+ context 'and maximum size is not specified' do
+ let(:maximum_size) { nil }
+
+ it "raises an error" do
+ expect { direct_upload }.to raise_error /maximum_size has to be specified if length is unknown/
+ end
+ end
+ end
+ end
+
+ describe '#to_hash' do
+ subject { direct_upload.to_hash }
+
+ shared_examples 'a valid upload' do
+ it "returns valid structure" do
+ expect(subject).to have_key(:Timeout)
+ expect(subject[:GetURL]).to start_with(storage_url)
+ expect(subject[:StoreURL]).to start_with(storage_url)
+ expect(subject[:DeleteURL]).to start_with(storage_url)
+ end
+ end
+
+ shared_examples 'a valid upload with multipart data' do
+ before do
+ stub_object_storage_multipart_init(storage_url, "myUpload")
+ end
+
+ it_behaves_like 'a valid upload'
+
+ it "returns valid structure" do
+ expect(subject).to have_key(:MultipartUpload)
+ expect(subject[:MultipartUpload]).to have_key(:PartSize)
+ expect(subject[:MultipartUpload][:PartURLs]).to all(start_with(storage_url))
+ expect(subject[:MultipartUpload][:PartURLs]).to all(include('uploadId=myUpload'))
+ expect(subject[:MultipartUpload][:CompleteURL]).to start_with(storage_url)
+ expect(subject[:MultipartUpload][:CompleteURL]).to include('uploadId=myUpload')
+ expect(subject[:MultipartUpload][:AbortURL]).to start_with(storage_url)
+ expect(subject[:MultipartUpload][:AbortURL]).to include('uploadId=myUpload')
+ end
+ end
+
+ shared_examples 'a valid upload without multipart data' do
+ it_behaves_like 'a valid upload'
+
+ it "returns valid structure" do
+ expect(subject).not_to have_key(:MultipartUpload)
+ end
+ end
+
+ context 'when AWS is used' do
+ context 'when length is known' do
+ let(:has_length) { true }
+
+ it_behaves_like 'a valid upload without multipart data'
+ end
+
+ context 'when length is unknown' do
+ let(:has_length) { false }
+
+ it_behaves_like 'a valid upload with multipart data' do
+ context 'when maximum upload size is 10MB' do
+ let(:maximum_size) { 10.megabyte }
+
+ it 'returns only 2 parts' do
+ expect(subject[:MultipartUpload][:PartURLs].length).to eq(2)
+ end
+
+ it 'part size is mimimum, 5MB' do
+ expect(subject[:MultipartUpload][:PartSize]).to eq(5.megabyte)
+ end
+ end
+
+ context 'when maximum upload size is 12MB' do
+ let(:maximum_size) { 12.megabyte }
+
+ it 'returns only 3 parts' do
+ expect(subject[:MultipartUpload][:PartURLs].length).to eq(3)
+ end
+
+ it 'part size is rounded-up to 5MB' do
+ expect(subject[:MultipartUpload][:PartSize]).to eq(5.megabyte)
+ end
+ end
+
+ context 'when maximum upload size is 49GB' do
+ let(:maximum_size) { 49.gigabyte }
+
+ it 'returns maximum, 100 parts' do
+ expect(subject[:MultipartUpload][:PartURLs].length).to eq(100)
+ end
+
+ it 'part size is rounded-up to 5MB' do
+ expect(subject[:MultipartUpload][:PartSize]).to eq(505.megabyte)
+ end
+ end
+ end
+ end
+ end
+
+ context 'when Google is used' do
+ let(:credentials) do
+ {
+ provider: 'Google',
+ google_storage_access_key_id: 'GOOGLE_ACCESS_KEY_ID',
+ google_storage_secret_access_key: 'GOOGLE_SECRET_ACCESS_KEY'
+ }
+ end
+
+ let(:storage_url) { 'https://storage.googleapis.com/uploads/' }
+
+ context 'when length is known' do
+ let(:has_length) { true }
+
+ it_behaves_like 'a valid upload without multipart data'
+ end
+
+ context 'when length is unknown' do
+ let(:has_length) { false }
+
+ it_behaves_like 'a valid upload without multipart data'
+ end
+ end
+ end
+end
diff --git a/spec/lib/omni_auth/strategies/jwt_spec.rb b/spec/lib/omni_auth/strategies/jwt_spec.rb
index 23485fbcb18..88d6d0b559a 100644
--- a/spec/lib/omni_auth/strategies/jwt_spec.rb
+++ b/spec/lib/omni_auth/strategies/jwt_spec.rb
@@ -43,7 +43,7 @@ describe OmniAuth::Strategies::Jwt do
end
it 'raises error' do
- expect { strategy.decoded }.to raise_error(OmniAuth::Strategies::JWT::ClaimInvalid)
+ expect { strategy.decoded }.to raise_error(OmniAuth::Strategies::Jwt::ClaimInvalid)
end
end
@@ -61,7 +61,7 @@ describe OmniAuth::Strategies::Jwt do
end
it 'raises error' do
- expect { strategy.decoded }.to raise_error(OmniAuth::Strategies::JWT::ClaimInvalid)
+ expect { strategy.decoded }.to raise_error(OmniAuth::Strategies::Jwt::ClaimInvalid)
end
end
@@ -80,7 +80,7 @@ describe OmniAuth::Strategies::Jwt do
end
it 'raises error' do
- expect { strategy.decoded }.to raise_error(OmniAuth::Strategies::JWT::ClaimInvalid)
+ expect { strategy.decoded }.to raise_error(OmniAuth::Strategies::Jwt::ClaimInvalid)
end
end
end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 8a52c151cc4..581132b6672 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -68,7 +68,7 @@ describe Notify do
end
it 'contains the description' do
- is_expected.to have_html_escaped_body_text issue.description
+ is_expected.to have_body_text issue.description
end
it 'does not add a reason header' do
@@ -89,7 +89,7 @@ describe Notify do
end
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(issue.author_name)
is_expected.to have_body_text 'created an issue:'
end
end
@@ -115,8 +115,8 @@ describe Notify do
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(previous_assignee.name)
+ is_expected.to have_body_text(assignee.name)
is_expected.to have_body_text(project_issue_path(project, issue))
end
end
@@ -190,7 +190,7 @@ describe Notify 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(current_user.name)
is_expected.to have_body_text(project_issue_path project, issue)
end
end
@@ -243,7 +243,7 @@ describe Notify do
end
it 'contains the description' do
- is_expected.to have_html_escaped_body_text merge_request.description
+ is_expected.to have_body_text merge_request.description
end
context 'when sent with a reason' do
@@ -260,7 +260,7 @@ describe Notify do
end
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 merge_request.author_name
is_expected.to have_body_text 'created a merge request:'
end
end
@@ -286,9 +286,9 @@ describe Notify do
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(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)
+ is_expected.to have_body_text(assignee.name)
end
end
@@ -314,6 +314,17 @@ describe Notify do
end
end
+ describe 'that are new with a description' do
+ subject { described_class.new_merge_request_email(merge_request.assignee_id, merge_request.id) }
+
+ it_behaves_like 'it should show Gmail Actions View Merge request link'
+ it_behaves_like "an unsubscribeable thread"
+
+ it 'contains the description' do
+ is_expected.to have_body_text(merge_request.description)
+ end
+ 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) }
@@ -358,7 +369,7 @@ describe Notify 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(current_user.name)
is_expected.to have_body_text(project_merge_request_path(project, merge_request))
end
end
@@ -390,6 +401,40 @@ describe Notify do
end
end
+ describe 'that are unmergeable' do
+ set(:merge_request) do
+ create(:merge_request, :conflict,
+ source_project: project,
+ target_project: project,
+ author: current_user,
+ assignee: assignee,
+ description: 'Awesome description')
+ end
+
+ subject { described_class.merge_request_unmergeable_email(recipient.id, merge_request.id) }
+
+ 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 merge request author' do
+ sender = subject.header[:from].addrs[0]
+ expect(sender.display_name).to eq(merge_request.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(project_merge_request_path(project, merge_request))
+ is_expected.to have_body_text('due to conflict.')
+ end
+ end
+ end
+
shared_examples 'a push to an existing merge request' do
let(:push_user) { create(:user) }
@@ -486,7 +531,7 @@ describe Notify do
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
+ is_expected.to have_body_text project_snippet_note.note
end
end
@@ -499,7 +544,7 @@ describe Notify do
it 'has the correct subject and body' do
is_expected.to have_subject("#{project.name} | Project was moved")
- is_expected.to have_html_escaped_body_text project.full_name
+ is_expected.to have_body_text project.full_name
is_expected.to have_body_text(project.ssh_url_to_repo)
end
end
@@ -507,7 +552,7 @@ describe Notify do
describe 'project access requested' do
let(:project) do
create(:project, :public, :access_requestable) do |project|
- project.add_master(project.owner)
+ project.add_maintainer(project.owner)
end
end
@@ -526,7 +571,7 @@ describe Notify do
expect(to_emails).to eq([recipient.notification_email])
is_expected.to have_subject "Request to join the #{project.full_name} project"
- is_expected.to have_html_escaped_body_text project.full_name
+ is_expected.to have_body_text project.full_name
is_expected.to have_body_text project_project_members_url(project)
is_expected.to have_body_text project_member.human_access
end
@@ -546,7 +591,7 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject "Access to the #{project.full_name} project was denied"
- is_expected.to have_html_escaped_body_text project.full_name
+ is_expected.to have_body_text project.full_name
is_expected.to have_body_text project.web_url
end
end
@@ -563,7 +608,7 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject "Access to the #{project.full_name} project was granted"
- is_expected.to have_html_escaped_body_text project.full_name
+ is_expected.to have_body_text project.full_name
is_expected.to have_body_text project.web_url
is_expected.to have_body_text project_member.human_access
end
@@ -582,8 +627,8 @@ describe Notify do
end
describe 'project invitation' do
- let(:master) { create(:user).tap { |u| project.add_master(u) } }
- let(:project_member) { invite_to_project(project, inviter: master) }
+ let(:maintainer) { create(:user).tap { |u| project.add_maintainer(u) } }
+ let(:project_member) { invite_to_project(project, inviter: maintainer) }
subject { described_class.member_invited_email('project', project_member.id, project_member.invite_token) }
@@ -593,7 +638,7 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject "Invitation to join the #{project.full_name} project"
- is_expected.to have_html_escaped_body_text project.full_name
+ is_expected.to have_body_text project.full_name
is_expected.to have_body_text project.full_name
is_expected.to have_body_text project_member.human_access
is_expected.to have_body_text project_member.invite_token
@@ -602,9 +647,9 @@ describe Notify do
describe 'project invitation accepted' do
let(:invited_user) { create(:user, name: 'invited user') }
- let(:master) { create(:user).tap { |u| project.add_master(u) } }
+ let(:maintainer) { create(:user).tap { |u| project.add_maintainer(u) } }
let(:project_member) do
- invitee = invite_to_project(project, inviter: master)
+ invitee = invite_to_project(project, inviter: maintainer)
invitee.accept_invite!(invited_user)
invitee
end
@@ -617,22 +662,22 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject 'Invitation accepted'
- is_expected.to have_html_escaped_body_text project.full_name
+ is_expected.to have_body_text project.full_name
is_expected.to have_body_text project.web_url
is_expected.to have_body_text project_member.invite_email
- is_expected.to have_html_escaped_body_text invited_user.name
+ is_expected.to have_body_text invited_user.name
end
end
describe 'project invitation declined' do
- let(:master) { create(:user).tap { |u| project.add_master(u) } }
+ let(:maintainer) { create(:user).tap { |u| project.add_maintainer(u) } }
let(:project_member) do
- invitee = invite_to_project(project, inviter: master)
+ invitee = invite_to_project(project, inviter: maintainer)
invitee.decline_invite!
invitee
end
- subject { described_class.member_invite_declined_email('project', project.id, project_member.invite_email, master.id) }
+ subject { described_class.member_invite_declined_email('project', project.id, project_member.invite_email, maintainer.id) }
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
@@ -640,7 +685,7 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject 'Invitation declined'
- is_expected.to have_html_escaped_body_text project.full_name
+ is_expected.to have_body_text project.full_name
is_expected.to have_body_text project.web_url
is_expected.to have_body_text project_member.invite_email
end
@@ -892,7 +937,7 @@ describe Notify do
end
it 'contains the message from the note' do
- is_expected.to have_html_escaped_body_text note.note
+ is_expected.to have_body_text note.note
end
it 'contains an introduction' do
@@ -951,7 +996,7 @@ describe Notify do
expect(to_emails).to eq([recipient.notification_email])
is_expected.to have_subject "Request to join the #{group.name} group"
- is_expected.to have_html_escaped_body_text group.name
+ is_expected.to have_body_text group.name
is_expected.to have_body_text group_group_members_url(group)
is_expected.to have_body_text group_member.human_access
end
@@ -970,7 +1015,7 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject "Access to the #{group.name} group was denied"
- is_expected.to have_html_escaped_body_text group.name
+ is_expected.to have_body_text group.name
is_expected.to have_body_text group.web_url
end
end
@@ -986,7 +1031,7 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject "Access to the #{group.name} group was granted"
- is_expected.to have_html_escaped_body_text group.name
+ is_expected.to have_body_text group.name
is_expected.to have_body_text group.web_url
is_expected.to have_body_text group_member.human_access
end
@@ -1016,7 +1061,7 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject "Invitation to join the #{group.name} group"
- is_expected.to have_html_escaped_body_text group.name
+ is_expected.to have_body_text group.name
is_expected.to have_body_text group.web_url
is_expected.to have_body_text group_member.human_access
is_expected.to have_body_text group_member.invite_token
@@ -1040,10 +1085,10 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject 'Invitation accepted'
- is_expected.to have_html_escaped_body_text group.name
+ is_expected.to have_body_text group.name
is_expected.to have_body_text group.web_url
is_expected.to have_body_text group_member.invite_email
- is_expected.to have_html_escaped_body_text invited_user.name
+ is_expected.to have_body_text invited_user.name
end
end
@@ -1063,7 +1108,7 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject 'Invitation declined'
- is_expected.to have_html_escaped_body_text group.name
+ is_expected.to have_body_text group.name
is_expected.to have_body_text group.web_url
is_expected.to have_body_text group_member.invite_email
end
@@ -1356,7 +1401,7 @@ describe Notify do
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
+ is_expected.to have_body_text personal_snippet_note.note
end
end
end
diff --git a/spec/mailers/previews/notify_preview.rb b/spec/mailers/previews/notify_preview.rb
deleted file mode 100644
index e32fd0bd120..00000000000
--- a/spec/mailers/previews/notify_preview.rb
+++ /dev/null
@@ -1,170 +0,0 @@
-class NotifyPreview < ActionMailer::Preview
- def note_merge_request_email_for_individual_note
- note_email(:note_merge_request_email) do
- note = <<-MD.strip_heredoc
- This is an individual note on a merge request :smiley:
-
- In this notification email, we expect to see:
-
- - The note contents (that's what you're looking at)
- - A link to view this note on Gitlab
- - An explanation for why the user is receiving this notification
- MD
-
- create_note(noteable_type: 'merge_request', noteable_id: merge_request.id, note: note)
- end
- end
-
- def note_merge_request_email_for_discussion
- note_email(:note_merge_request_email) do
- note = <<-MD.strip_heredoc
- This is a new discussion on a merge request :smiley:
-
- In this notification email, we expect to see:
-
- - A line saying who started this discussion
- - The note contents (that's what you're looking at)
- - A link to view this discussion on Gitlab
- - An explanation for why the user is receiving this notification
- MD
-
- create_note(noteable_type: 'merge_request', noteable_id: merge_request.id, type: 'DiscussionNote', note: note)
- end
- end
-
- def note_merge_request_email_for_diff_discussion
- note_email(:note_merge_request_email) do
- note = <<-MD.strip_heredoc
- This is a new discussion on a merge request :smiley:
-
- In this notification email, we expect to see:
-
- - A line saying who started this discussion and on what file
- - The diff
- - The note contents (that's what you're looking at)
- - A link to view this discussion on Gitlab
- - An explanation for why the user is receiving this notification
- MD
-
- position = Gitlab::Diff::Position.new(
- old_path: "files/ruby/popen.rb",
- new_path: "files/ruby/popen.rb",
- old_line: nil,
- new_line: 14,
- diff_refs: merge_request.diff_refs
- )
-
- create_note(noteable_type: 'merge_request', noteable_id: merge_request.id, type: 'DiffNote', position: position, note: note)
- end
- end
-
- def closed_issue_email
- Notify.closed_issue_email(user.id, issue.id, user.id).message
- end
-
- def issue_status_changed_email
- Notify.issue_status_changed_email(user.id, issue.id, 'closed', user.id).message
- end
-
- def closed_merge_request_email
- Notify.closed_merge_request_email(user.id, issue.id, user.id).message
- end
-
- def merge_request_status_email
- Notify.merge_request_status_email(user.id, merge_request.id, 'closed', user.id).message
- end
-
- def merged_merge_request_email
- Notify.merged_merge_request_email(user.id, merge_request.id, user.id).message
- end
-
- def member_access_denied_email
- Notify.member_access_denied_email('project', project.id, user.id).message
- end
-
- def member_access_granted_email
- Notify.member_access_granted_email('project', user.id).message
- end
-
- def member_access_requested_email
- Notify.member_access_requested_email('group', user.id, 'some@example.com').message
- end
-
- def member_invite_accepted_email
- Notify.member_invite_accepted_email('project', user.id).message
- end
-
- def member_invite_declined_email
- Notify.member_invite_declined_email(
- 'project',
- project.id,
- 'invite@example.com',
- user.id
- ).message
- end
-
- def member_invited_email
- Notify.member_invited_email('project', user.id, '1234').message
- end
-
- def pages_domain_enabled_email
- cleanup do
- pages_domain = PagesDomain.new(domain: 'my.example.com', project: project, verified_at: Time.now, enabled_until: 1.week.from_now)
-
- Notify.pages_domain_enabled_email(pages_domain, user).message
- end
- end
-
- def pipeline_success_email
- Notify.pipeline_success_email(pipeline, pipeline.user.try(:email))
- end
-
- def pipeline_failed_email
- Notify.pipeline_failed_email(pipeline, pipeline.user.try(:email))
- end
-
- private
-
- def project
- @project ||= Project.find_by_full_path('gitlab-org/gitlab-test')
- end
-
- def issue
- @merge_request ||= project.issues.first
- end
-
- def merge_request
- @merge_request ||= project.merge_requests.first
- end
-
- def pipeline
- @pipeline = Ci::Pipeline.last
- end
-
- def user
- @user ||= User.last
- end
-
- def create_note(params)
- Notes::CreateService.new(project, user, params).execute
- end
-
- def note_email(method)
- cleanup do
- note = yield
-
- Notify.public_send(method, user.id, note)
- end
- end
-
- def cleanup
- email = nil
-
- ActiveRecord::Base.transaction do
- email = yield
- raise ActiveRecord::Rollback
- end
-
- email
- end
-end
diff --git a/spec/migrations/active_record/schema_spec.rb b/spec/migrations/active_record/schema_spec.rb
index e132529d8d8..9d35b3cd642 100644
--- a/spec/migrations/active_record/schema_spec.rb
+++ b/spec/migrations/active_record/schema_spec.rb
@@ -5,7 +5,11 @@ require 'spec_helper'
describe ActiveRecord::Schema do
let(:latest_migration_timestamp) do
- migrations = Dir[Rails.root.join('db', 'migrate', '*'), Rails.root.join('db', 'post_migrate', '*')]
+ migrations_paths = %w[db ee/db]
+ .product(%w[migrate post_migrate])
+ .map { |path| Rails.root.join(*path, '*') }
+
+ migrations = Dir[*migrations_paths]
migrations.map { |migration| File.basename(migration).split('_').first.to_i }.max
end
diff --git a/spec/migrations/change_default_value_for_dsa_key_restriction_spec.rb b/spec/migrations/change_default_value_for_dsa_key_restriction_spec.rb
new file mode 100644
index 00000000000..7e61ab9b52e
--- /dev/null
+++ b/spec/migrations/change_default_value_for_dsa_key_restriction_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20180531220618_change_default_value_for_dsa_key_restriction.rb')
+
+describe ChangeDefaultValueForDsaKeyRestriction, :migration do
+ let(:application_settings) { table(:application_settings) }
+
+ before do
+ application_settings.create!
+ end
+
+ it 'changes the default value for dsa_key_restriction' do
+ expect(application_settings.first.dsa_key_restriction).to eq(0)
+
+ migrate!
+
+ application_settings.reset_column_information
+ new_setting = application_settings.create!
+
+ expect(application_settings.count).to eq(2)
+ expect(new_setting.dsa_key_restriction).to eq(-1)
+ end
+
+ it 'changes the existing setting' do
+ setting = application_settings.last
+
+ expect(setting.dsa_key_restriction).to eq(0)
+
+ migrate!
+
+ expect(application_settings.count).to eq(1)
+ expect(setting.reload.dsa_key_restriction).to eq(-1)
+ end
+end
diff --git a/spec/migrations/cleanup_stages_position_migration_spec.rb b/spec/migrations/cleanup_stages_position_migration_spec.rb
new file mode 100644
index 00000000000..dde5a777487
--- /dev/null
+++ b/spec/migrations/cleanup_stages_position_migration_spec.rb
@@ -0,0 +1,67 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20180604123514_cleanup_stages_position_migration.rb')
+
+describe CleanupStagesPositionMigration, :migration, :sidekiq, :redis do
+ let(:migration) { spy('migration') }
+
+ before do
+ allow(Gitlab::BackgroundMigration::MigrateStageIndex)
+ .to receive(:new).and_return(migration)
+ end
+
+ context 'when there are pending background migrations' do
+ it 'processes pending jobs synchronously' do
+ Sidekiq::Testing.disable! do
+ BackgroundMigrationWorker
+ .perform_in(2.minutes, 'MigrateStageIndex', [1, 1])
+ BackgroundMigrationWorker
+ .perform_async('MigrateStageIndex', [1, 1])
+
+ migrate!
+
+ expect(migration).to have_received(:perform).with(1, 1).twice
+ end
+ end
+ end
+
+ context 'when there are no background migrations pending' do
+ it 'does nothing' do
+ Sidekiq::Testing.disable! do
+ migrate!
+
+ expect(migration).not_to have_received(:perform)
+ end
+ end
+ end
+
+ context 'when there are still unmigrated stages present' do
+ let(:stages) { table('ci_stages') }
+ let(:builds) { table('ci_builds') }
+
+ let!(:entities) do
+ %w[build test broken].map do |name|
+ stages.create(name: name)
+ end
+ end
+
+ before do
+ stages.update_all(position: nil)
+
+ builds.create(name: 'unit', stage_id: entities.first.id, stage_idx: 1, ref: 'master')
+ builds.create(name: 'unit', stage_id: entities.second.id, stage_idx: 1, ref: 'master')
+ end
+
+ it 'migrates stages sequentially for every stage' do
+ expect(stages.all).to all(have_attributes(position: nil))
+
+ migrate!
+
+ expect(migration).to have_received(:perform)
+ .with(entities.first.id, entities.first.id)
+ expect(migration).to have_received(:perform)
+ .with(entities.second.id, entities.second.id)
+ expect(migration).not_to have_received(:perform)
+ .with(entities.third.id, entities.third.id)
+ end
+ end
+end
diff --git a/spec/migrations/enqueue_delete_diff_files_workers_spec.rb b/spec/migrations/enqueue_delete_diff_files_workers_spec.rb
new file mode 100644
index 00000000000..6bae870920c
--- /dev/null
+++ b/spec/migrations/enqueue_delete_diff_files_workers_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20180619121030_enqueue_delete_diff_files_workers.rb')
+
+describe EnqueueDeleteDiffFilesWorkers, :migration, :sidekiq do
+ it 'correctly schedules diff files deletion schedulers' do
+ Sidekiq::Testing.fake! do
+ expect(BackgroundMigrationWorker)
+ .to receive(:perform_async)
+ .with(described_class::SCHEDULER)
+ .and_call_original
+
+ migrate!
+
+ expect(BackgroundMigrationWorker.jobs.size).to eq(1)
+ end
+ end
+end
diff --git a/spec/migrations/fill_file_store_spec.rb b/spec/migrations/fill_file_store_spec.rb
new file mode 100644
index 00000000000..5ff7aa56ce2
--- /dev/null
+++ b/spec/migrations/fill_file_store_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20180424151928_fill_file_store')
+
+describe FillFileStore, :migration do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:builds) { table(:ci_builds) }
+ let(:job_artifacts) { table(:ci_job_artifacts) }
+ let(:lfs_objects) { table(:lfs_objects) }
+ let(:uploads) { table(:uploads) }
+
+ before do
+ namespaces.create!(id: 123, name: 'gitlab1', path: 'gitlab1')
+ projects.create!(id: 123, name: 'gitlab1', path: 'gitlab1', namespace_id: 123)
+ builds.create!(id: 1)
+
+ ##
+ # Create rows that have nullfied `file_store` column
+ job_artifacts.create!(project_id: 123, job_id: 1, file_type: 1, file_store: nil)
+ lfs_objects.create!(oid: 123, size: 10, file: 'file_name', file_store: nil)
+ uploads.create!(size: 10, path: 'path', uploader: 'uploader', mount_point: 'file_name', store: nil)
+ end
+
+ it 'correctly migrates nullified file_store/store column' do
+ expect(job_artifacts.where(file_store: nil).count).to eq(1)
+ expect(lfs_objects.where(file_store: nil).count).to eq(1)
+ expect(uploads.where(store: nil).count).to eq(1)
+
+ expect(job_artifacts.where(file_store: 1).count).to eq(0)
+ expect(lfs_objects.where(file_store: 1).count).to eq(0)
+ expect(uploads.where(store: 1).count).to eq(0)
+
+ migrate!
+
+ expect(job_artifacts.where(file_store: nil).count).to eq(0)
+ expect(lfs_objects.where(file_store: nil).count).to eq(0)
+ expect(uploads.where(store: nil).count).to eq(0)
+
+ expect(job_artifacts.where(file_store: 1).count).to eq(1)
+ expect(lfs_objects.where(file_store: 1).count).to eq(1)
+ expect(uploads.where(store: 1).count).to eq(1)
+ end
+end
diff --git a/spec/migrations/issues_moved_to_id_foreign_key_spec.rb b/spec/migrations/issues_moved_to_id_foreign_key_spec.rb
index dd2b08099f2..495e86ee888 100644
--- a/spec/migrations/issues_moved_to_id_foreign_key_spec.rb
+++ b/spec/migrations/issues_moved_to_id_foreign_key_spec.rb
@@ -14,7 +14,7 @@ describe IssuesMovedToIdForeignKey, :migration, schema: 20171114150259 do
it 'removes the orphaned moved_to_id' do
subject.down
- issue_third.update_attributes(moved_to_id: 100000)
+ issue_third.update(moved_to_id: 100000)
subject.up
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
index df009cec25c..ba4c66057d4 100644
--- a/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb
+++ b/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb
@@ -12,7 +12,7 @@ describe MigrateGcpClustersToNewClustersArchitectures, :migration do
class KubernetesService < ActiveRecord::Base
self.table_name = 'services'
- serialize :properties, JSON # rubocop:disable Cop/ActiveRecordSerialize
+ serialize :properties, JSON
default_value_for :active, true
default_value_for :type, 'KubernetesService'
@@ -175,7 +175,7 @@ describe MigrateGcpClustersToNewClustersArchitectures, :migration do
end
end
- def tr(s)
- s.delete("'")
+ def tr(str)
+ str.delete("'")
end
end
diff --git a/spec/migrations/migrate_object_storage_upload_sidekiq_queue_spec.rb b/spec/migrations/migrate_object_storage_upload_sidekiq_queue_spec.rb
new file mode 100644
index 00000000000..1ee6c440cf4
--- /dev/null
+++ b/spec/migrations/migrate_object_storage_upload_sidekiq_queue_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20180603190921_migrate_object_storage_upload_sidekiq_queue.rb')
+
+describe MigrateObjectStorageUploadSidekiqQueue, :sidekiq, :redis do
+ include Gitlab::Database::MigrationHelpers
+
+ context 'when there are jobs in the queue' do
+ it 'correctly migrates queue when migrating up' do
+ Sidekiq::Testing.disable! do
+ stubbed_worker(queue: 'object_storage_upload').perform_async('Something', [1])
+ stubbed_worker(queue: 'object_storage:object_storage_background_move').perform_async('Something', [1])
+
+ described_class.new.up
+
+ expect(sidekiq_queue_length('object_storage_upload')).to eq 0
+ expect(sidekiq_queue_length('object_storage:object_storage_background_move')).to eq 2
+ end
+ end
+ end
+
+ context 'when there are no jobs in the queues' do
+ it 'does not raise error when migrating up' do
+ expect { described_class.new.up }.not_to raise_error
+ end
+ end
+
+ def stubbed_worker(queue:)
+ Class.new do
+ include Sidekiq::Worker
+ sidekiq_options queue: queue
+ end
+ end
+end
diff --git a/spec/migrations/migrate_process_commit_worker_jobs_spec.rb b/spec/migrations/migrate_process_commit_worker_jobs_spec.rb
index 4ee1d255fbd..ac34efa4f9d 100644
--- a/spec/migrations/migrate_process_commit_worker_jobs_spec.rb
+++ b/spec/migrations/migrate_process_commit_worker_jobs_spec.rb
@@ -6,7 +6,11 @@ require Rails.root.join('db', 'migrate', '20161124141322_migrate_process_commit_
describe MigrateProcessCommitWorkerJobs do
let(:project) { create(:project, :legacy_storage, :repository) } # rubocop:disable RSpec/FactoriesInMigrationSpecs
let(:user) { create(:user) } # rubocop:disable RSpec/FactoriesInMigrationSpecs
- let(:commit) { project.commit.raw.rugged_commit }
+ let(:commit) do
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ project.commit.raw.rugged_commit
+ end
+ end
describe 'Project' do
describe 'find_including_path' do
diff --git a/spec/migrations/migrate_remaining_mr_metrics_populating_background_migration_spec.rb b/spec/migrations/migrate_remaining_mr_metrics_populating_background_migration_spec.rb
new file mode 100644
index 00000000000..47dab18183c
--- /dev/null
+++ b/spec/migrations/migrate_remaining_mr_metrics_populating_background_migration_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20180521162137_migrate_remaining_mr_metrics_populating_background_migration.rb')
+
+describe MigrateRemainingMrMetricsPopulatingBackgroundMigration, :migration, :sidekiq do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:mrs) { table(:merge_requests) }
+
+ before do
+ namespaces.create!(id: 1, name: 'foo', path: 'foo')
+ projects.create!(id: 123, name: 'gitlab1', path: 'gitlab1', namespace_id: 1)
+ projects.create!(id: 456, name: 'gitlab2', path: 'gitlab2', namespace_id: 1)
+ projects.create!(id: 789, name: 'gitlab3', path: 'gitlab3', namespace_id: 1)
+ mrs.create!(title: 'foo', target_branch: 'target', source_branch: 'source', target_project_id: 123)
+ mrs.create!(title: 'bar', target_branch: 'target', source_branch: 'source', target_project_id: 456)
+ mrs.create!(title: 'kux', target_branch: 'target', source_branch: 'source', target_project_id: 789)
+ end
+
+ it 'correctly schedules background migrations' do
+ stub_const("#{described_class.name}::BATCH_SIZE", 2)
+
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ migrate!
+
+ expect(described_class::MIGRATION)
+ .to be_scheduled_delayed_migration(10.minutes, mrs.first.id, mrs.second.id)
+
+ expect(described_class::MIGRATION)
+ .to be_scheduled_delayed_migration(20.minutes, mrs.third.id, mrs.third.id)
+
+ expect(BackgroundMigrationWorker.jobs.size).to eq(2)
+ end
+ end
+ end
+end
diff --git a/spec/migrations/remove_soft_removed_objects_spec.rb b/spec/migrations/remove_soft_removed_objects_spec.rb
index fb70c284f5e..d0bde98b80e 100644
--- a/spec/migrations/remove_soft_removed_objects_spec.rb
+++ b/spec/migrations/remove_soft_removed_objects_spec.rb
@@ -3,6 +3,18 @@ require Rails.root.join('db', 'post_migrate', '20171207150343_remove_soft_remove
describe RemoveSoftRemovedObjects, :migration do
describe '#up' do
+ let!(:groups) do
+ table(:namespaces).tap do |t|
+ t.inheritance_column = nil
+ end
+ end
+
+ let!(:routes) do
+ table(:routes).tap do |t|
+ t.inheritance_column = nil
+ end
+ end
+
it 'removes various soft removed objects' do
5.times do
create_with_deleted_at(:issue)
@@ -28,19 +40,20 @@ describe RemoveSoftRemovedObjects, :migration do
it 'removes routes of soft removed personal namespaces' do
namespace = create_with_deleted_at(:namespace)
- group = create(:group) # rubocop:disable RSpec/FactoriesInMigrationSpecs
+ group = groups.create!(name: 'group', path: 'group_path', type: 'Group')
+ routes.create!(source_id: group.id, source_type: 'Group', name: 'group', path: 'group_path')
- expect(Route.where(source: namespace).exists?).to eq(true)
- expect(Route.where(source: group).exists?).to eq(true)
+ expect(routes.where(source_id: namespace.id).exists?).to eq(true)
+ expect(routes.where(source_id: group.id).exists?).to eq(true)
run_migration
- expect(Route.where(source: namespace).exists?).to eq(false)
- expect(Route.where(source: group).exists?).to eq(true)
+ expect(routes.where(source_id: namespace.id).exists?).to eq(false)
+ expect(routes.where(source_id: group.id).exists?).to eq(true)
end
it 'schedules the removal of soft removed groups' do
- group = create_with_deleted_at(:group)
+ group = create_deleted_group
admin = create(:user, admin: true) # rubocop:disable RSpec/FactoriesInMigrationSpecs
expect_any_instance_of(GroupDestroyWorker)
@@ -51,7 +64,7 @@ describe RemoveSoftRemovedObjects, :migration do
end
it 'does not remove soft removed groups when no admin user could be found' do
- create_with_deleted_at(:group)
+ create_deleted_group
expect_any_instance_of(GroupDestroyWorker)
.not_to receive(:perform)
@@ -74,4 +87,13 @@ describe RemoveSoftRemovedObjects, :migration do
row
end
+
+ def create_deleted_group
+ group = groups.create!(name: 'group', path: 'group_path', type: 'Group')
+ routes.create!(source_id: group.id, source_type: 'Group', name: 'group', path: 'group_path')
+
+ groups.where(id: group.id).update_all(deleted_at: 1.year.ago)
+
+ group
+ end
end
diff --git a/spec/migrations/schedule_to_archive_legacy_traces_spec.rb b/spec/migrations/schedule_to_archive_legacy_traces_spec.rb
new file mode 100644
index 00000000000..d3eac3c45ea
--- /dev/null
+++ b/spec/migrations/schedule_to_archive_legacy_traces_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20180529152628_schedule_to_archive_legacy_traces')
+
+describe ScheduleToArchiveLegacyTraces, :migration do
+ include TraceHelpers
+
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:builds) { table(:ci_builds) }
+ let(:job_artifacts) { table(:ci_job_artifacts) }
+
+ before do
+ namespaces.create!(id: 123, name: 'gitlab1', path: 'gitlab1')
+ projects.create!(id: 123, name: 'gitlab1', path: 'gitlab1', namespace_id: 123)
+ @build_success = builds.create!(id: 1, project_id: 123, status: 'success', type: 'Ci::Build')
+ @build_failed = builds.create!(id: 2, project_id: 123, status: 'failed', type: 'Ci::Build')
+ @builds_canceled = builds.create!(id: 3, project_id: 123, status: 'canceled', type: 'Ci::Build')
+ @build_running = builds.create!(id: 4, project_id: 123, status: 'running', type: 'Ci::Build')
+
+ create_legacy_trace(@build_success, 'This job is done')
+ create_legacy_trace(@build_failed, 'This job is done')
+ create_legacy_trace(@builds_canceled, 'This job is done')
+ create_legacy_trace(@build_running, 'This job is not done yet')
+ end
+
+ it 'correctly archive legacy traces' do
+ expect(job_artifacts.count).to eq(0)
+ expect(File.exist?(legacy_trace_path(@build_success))).to be_truthy
+ expect(File.exist?(legacy_trace_path(@build_failed))).to be_truthy
+ expect(File.exist?(legacy_trace_path(@builds_canceled))).to be_truthy
+ expect(File.exist?(legacy_trace_path(@build_running))).to be_truthy
+
+ migrate!
+
+ expect(job_artifacts.count).to eq(3)
+ expect(File.exist?(legacy_trace_path(@build_success))).to be_falsy
+ expect(File.exist?(legacy_trace_path(@build_failed))).to be_falsy
+ expect(File.exist?(legacy_trace_path(@builds_canceled))).to be_falsy
+ expect(File.exist?(legacy_trace_path(@build_running))).to be_truthy
+ expect(File.exist?(archived_trace_path(job_artifacts.where(job_id: @build_success.id).first))).to be_truthy
+ expect(File.exist?(archived_trace_path(job_artifacts.where(job_id: @build_failed.id).first))).to be_truthy
+ expect(File.exist?(archived_trace_path(job_artifacts.where(job_id: @builds_canceled.id).first))).to be_truthy
+ expect(job_artifacts.where(job_id: @build_running.id)).not_to be_exist
+ end
+end
diff --git a/spec/migrations/turn_nested_groups_into_regular_groups_for_mysql_spec.rb b/spec/migrations/turn_nested_groups_into_regular_groups_for_mysql_spec.rb
index 560409f08de..5f5ba426d69 100644
--- a/spec/migrations/turn_nested_groups_into_regular_groups_for_mysql_spec.rb
+++ b/spec/migrations/turn_nested_groups_into_regular_groups_for_mysql_spec.rb
@@ -49,10 +49,14 @@ describe TurnNestedGroupsIntoRegularGroupsForMysql do
end
it 'renames the repository of any projects' do
- expect(updated_project.repository.path)
+ repo_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ updated_project.repository.path
+ end
+
+ expect(repo_path)
.to end_with("#{parent_group.name}-#{child_group.name}/#{updated_project.path}.git")
- expect(File.directory?(updated_project.repository.path)).to eq(true)
+ expect(File.directory?(repo_path)).to eq(true)
end
it 'creates a redirect route for renamed projects' do
diff --git a/spec/models/application_setting/term_spec.rb b/spec/models/application_setting/term_spec.rb
index 1eddf3c56ff..aa49594f4d1 100644
--- a/spec/models/application_setting/term_spec.rb
+++ b/spec/models/application_setting/term_spec.rb
@@ -12,4 +12,41 @@ describe ApplicationSetting::Term do
expect(described_class.latest).to eq(terms)
end
end
+
+ describe '#accepted_by_user?' do
+ let(:user) { create(:user) }
+ let(:term) { create(:term) }
+
+ it 'is true when the user accepted the terms' do
+ accept_terms(term, user)
+
+ expect(term.accepted_by_user?(user)).to be(true)
+ end
+
+ it 'is false when the user declined the terms' do
+ decline_terms(term, user)
+
+ expect(term.accepted_by_user?(user)).to be(false)
+ end
+
+ it 'does not cause a query when the user accepted the current terms' do
+ accept_terms(term, user)
+
+ expect { term.accepted_by_user?(user) }.not_to exceed_query_limit(0)
+ end
+
+ it 'returns false if the currently accepted terms are different' do
+ accept_terms(create(:term), user)
+
+ expect(term.accepted_by_user?(user)).to be(false)
+ end
+
+ def accept_terms(term, user)
+ Users::RespondToTermsService.new(user, term).execute(accepted: true)
+ end
+
+ def decline_terms(term, user)
+ Users::RespondToTermsService.new(user, term).execute(accepted: false)
+ end
+ end
end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 7e47043a1cb..02f74e2ea54 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -25,15 +25,6 @@ describe ApplicationSetting do
it { is_expected.to allow_value(https).for(:after_sign_out_path) }
it { is_expected.not_to allow_value(ftp).for(:after_sign_out_path) }
- describe 'disabled_oauth_sign_in_sources validations' do
- before do
- allow(Devise).to receive(:omniauth_providers).and_return([:github])
- end
-
- it { is_expected.to allow_value(['github']).for(:disabled_oauth_sign_in_sources) }
- it { is_expected.not_to allow_value(['test']).for(:disabled_oauth_sign_in_sources) }
- end
-
describe 'default_artifacts_expire_in' do
it 'sets an error if it cannot parse' do
setting.update(default_artifacts_expire_in: 'a')
@@ -110,10 +101,9 @@ describe ApplicationSetting do
# Upgraded databases will have this sort of content
context 'repository_storages is a String, not an Array' do
before do
- setting.__send__(:raw_write_attribute, :repository_storages, 'default')
+ described_class.where(id: setting.id).update_all(repository_storages: 'default')
end
- it { expect(setting.repository_storages_before_type_cast).to eq('default') }
it { expect(setting.repository_storages).to eq(['default']) }
end
@@ -195,22 +185,6 @@ describe ApplicationSetting do
expect(setting.pick_repository_storage).to eq('random')
end
-
- describe '#repository_storage' do
- it 'returns the first storage' do
- setting.repository_storages = %w(good bad)
-
- expect(setting.repository_storage).to eq('good')
- end
- end
-
- describe '#repository_storage=' do
- it 'overwrites repository_storages' do
- setting.repository_storage = 'overwritten'
-
- expect(setting.repository_storages).to eq(['overwritten'])
- end
- end
end
end
@@ -331,6 +305,33 @@ describe ApplicationSetting do
end
end
+ describe '#disabled_oauth_sign_in_sources=' do
+ before do
+ allow(Devise).to receive(:omniauth_providers).and_return([:github])
+ end
+
+ it 'removes unknown sources (as strings) from the array' do
+ subject.disabled_oauth_sign_in_sources = %w[github test]
+
+ expect(subject).to be_valid
+ expect(subject.disabled_oauth_sign_in_sources).to eq ['github']
+ end
+
+ it 'removes unknown sources (as symbols) from the array' do
+ subject.disabled_oauth_sign_in_sources = %i[github test]
+
+ expect(subject).to be_valid
+ expect(subject.disabled_oauth_sign_in_sources).to eq ['github']
+ end
+
+ it 'ignores nil' do
+ subject.disabled_oauth_sign_in_sources = nil
+
+ expect(subject).to be_valid
+ expect(subject.disabled_oauth_sign_in_sources).to be_empty
+ end
+ end
+
context 'restricted signup domains' do
it 'sets single domain' do
setting.domain_whitelist_raw = 'example.com'
@@ -391,68 +392,6 @@ describe ApplicationSetting do
end
describe 'performance bar settings' do
- describe 'performance_bar_allowed_group_id=' do
- context 'with a blank path' do
- before do
- setting.performance_bar_allowed_group_id = create(:group).full_path
- end
-
- it 'persists nil for a "" path and clears allowed user IDs cache' do
- expect(Gitlab::PerformanceBar).to receive(:expire_allowed_user_ids_cache)
-
- setting.performance_bar_allowed_group_id = ''
-
- expect(setting.performance_bar_allowed_group_id).to be_nil
- end
- end
-
- context 'with an invalid path' do
- it 'does not persist an invalid group path' do
- setting.performance_bar_allowed_group_id = 'foo'
-
- expect(setting.performance_bar_allowed_group_id).to be_nil
- end
- end
-
- context 'with a path to an existing group' do
- let(:group) { create(:group) }
-
- it 'persists a valid group path and clears allowed user IDs cache' do
- expect(Gitlab::PerformanceBar).to receive(:expire_allowed_user_ids_cache)
-
- setting.performance_bar_allowed_group_id = group.full_path
-
- expect(setting.performance_bar_allowed_group_id).to eq(group.id)
- end
-
- context 'when the given path is the same' do
- context 'with a blank path' do
- before do
- setting.performance_bar_allowed_group_id = nil
- end
-
- it 'clears the cached allowed user IDs' do
- expect(Gitlab::PerformanceBar).not_to receive(:expire_allowed_user_ids_cache)
-
- setting.performance_bar_allowed_group_id = ''
- end
- end
-
- context 'with a valid path' do
- before do
- setting.performance_bar_allowed_group_id = group.full_path
- end
-
- it 'clears the cached allowed user IDs' do
- expect(Gitlab::PerformanceBar).not_to receive(:expire_allowed_user_ids_cache)
-
- setting.performance_bar_allowed_group_id = group.full_path
- end
- end
- end
- end
- end
-
describe 'performance_bar_allowed_group' do
context 'with no performance_bar_allowed_group_id saved' do
it 'returns nil' do
@@ -464,11 +403,11 @@ describe ApplicationSetting do
let(:group) { create(:group) }
before do
- setting.performance_bar_allowed_group_id = group.full_path
+ setting.update!(performance_bar_allowed_group_id: group.id)
end
it 'returns the group' do
- expect(setting.performance_bar_allowed_group).to eq(group)
+ expect(setting.reload.performance_bar_allowed_group).to eq(group)
end
end
end
@@ -478,67 +417,11 @@ describe ApplicationSetting do
let(:group) { create(:group) }
before do
- setting.performance_bar_allowed_group_id = group.full_path
+ setting.update!(performance_bar_allowed_group_id: group.id)
end
it 'returns true' do
- expect(setting.performance_bar_enabled).to be_truthy
- end
- end
- end
-
- describe 'performance_bar_enabled=' do
- context 'when the performance bar is enabled' do
- let(:group) { create(:group) }
-
- before do
- setting.performance_bar_allowed_group_id = group.full_path
- end
-
- context 'when passing true' do
- it 'does not clear allowed user IDs cache' do
- expect(Gitlab::PerformanceBar).not_to receive(:expire_allowed_user_ids_cache)
-
- setting.performance_bar_enabled = true
-
- expect(setting.performance_bar_allowed_group_id).to eq(group.id)
- expect(setting.performance_bar_enabled).to be_truthy
- end
- end
-
- context 'when passing false' do
- it 'disables the performance bar and clears allowed user IDs cache' do
- expect(Gitlab::PerformanceBar).to receive(:expire_allowed_user_ids_cache)
-
- setting.performance_bar_enabled = false
-
- expect(setting.performance_bar_allowed_group_id).to be_nil
- expect(setting.performance_bar_enabled).to be_falsey
- end
- end
- end
-
- context 'when the performance bar is disabled' do
- context 'when passing true' do
- it 'does nothing and does not clear allowed user IDs cache' do
- expect(Gitlab::PerformanceBar).not_to receive(:expire_allowed_user_ids_cache)
-
- setting.performance_bar_enabled = true
-
- expect(setting.performance_bar_allowed_group_id).to be_nil
- expect(setting.performance_bar_enabled).to be_falsey
- end
- end
-
- context 'when passing false' do
- it 'does nothing and does not clear allowed user IDs cache' do
- expect(Gitlab::PerformanceBar).not_to receive(:expire_allowed_user_ids_cache)
-
- setting.performance_bar_enabled = false
-
- expect(setting.performance_bar_allowed_group_id).to be_nil
- expect(setting.performance_bar_enabled).to be_falsey
- end
+ expect(setting.reload.performance_bar_enabled).to be_truthy
end
end
end
diff --git a/spec/models/ci/build_metadata_spec.rb b/spec/models/ci/build_metadata_spec.rb
index 7e75d5a5411..6dba132184c 100644
--- a/spec/models/ci/build_metadata_spec.rb
+++ b/spec/models/ci/build_metadata_spec.rb
@@ -30,7 +30,7 @@ describe Ci::BuildMetadata do
context 'when runner is assigned to the job' do
before do
- build.update_attributes(runner: runner)
+ build.update(runner: runner)
end
context 'when runner timeout is lower than project timeout' do
diff --git a/spec/models/ci/build_runner_session_spec.rb b/spec/models/ci/build_runner_session_spec.rb
new file mode 100644
index 00000000000..7183957aa50
--- /dev/null
+++ b/spec/models/ci/build_runner_session_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe Ci::BuildRunnerSession, model: true do
+ let!(:build) { create(:ci_build, :with_runner_session) }
+
+ subject { build.runner_session }
+
+ it { is_expected.to belong_to(:build) }
+
+ it { is_expected.to validate_presence_of(:build) }
+ it { is_expected.to validate_presence_of(:url).with_message('must be a valid URL') }
+
+ describe '#terminal_specification' do
+ let(:terminal_specification) { subject.terminal_specification }
+
+ it 'returns empty hash if no url' do
+ subject.url = ''
+
+ expect(terminal_specification).to be_empty
+ end
+
+ context 'when url is present' do
+ it 'returns ca_pem nil if empty certificate' do
+ subject.certificate = ''
+
+ expect(terminal_specification[:ca_pem]).to be_nil
+ end
+
+ it 'adds Authorization header if authorization is present' do
+ subject.authorization = 'whatever'
+
+ expect(terminal_specification[:headers]).to include(Authorization: 'whatever')
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 96173889ccd..3c96fe76829 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -19,6 +19,7 @@ describe Ci::Build do
it { is_expected.to belong_to(:erased_by) }
it { is_expected.to have_many(:deployments) }
it { is_expected.to have_many(:trace_sections)}
+ it { is_expected.to have_one(:runner_session)}
it { is_expected.to validate_presence_of(:ref) }
it { is_expected.to respond_to(:has_trace?) }
it { is_expected.to respond_to(:trace) }
@@ -42,6 +43,20 @@ describe Ci::Build do
end
end
+ describe 'status' do
+ context 'when transitioning to any state from running' do
+ it 'removes runner_session' do
+ %w(success drop cancel).each do |event|
+ build = FactoryBot.create(:ci_build, :running, :with_runner_session, pipeline: pipeline)
+
+ build.fire_events!(event)
+
+ expect(build.reload.runner_session).to be_nil
+ end
+ end
+ end
+ end
+
describe '.manual_actions' do
let!(:manual_but_created) { create(:ci_build, :manual, status: :created, pipeline: pipeline) }
let!(:manual_but_succeeded) { create(:ci_build, :manual, status: :success, pipeline: pipeline) }
@@ -116,6 +131,26 @@ describe Ci::Build do
end
end
+ describe '.with_live_trace' do
+ subject { described_class.with_live_trace }
+
+ context 'when build has live trace' do
+ let!(:build) { create(:ci_build, :success, :trace_live) }
+
+ it 'selects the build' do
+ is_expected.to eq([build])
+ end
+ end
+
+ context 'when build does not have live trace' do
+ let!(:build) { create(:ci_build, :success, :trace_artifact) }
+
+ it 'does not select the build' do
+ is_expected.to be_empty
+ end
+ end
+ end
+
describe '#actionize' do
context 'when build is a created' do
before do
@@ -148,22 +183,21 @@ describe Ci::Build do
end
context 'when there are runners' do
- let(:runner) { create(:ci_runner) }
+ let(:runner) { create(:ci_runner, :project, projects: [build.project]) }
before do
- build.project.runners << runner
- runner.update_attributes(contacted_at: 1.second.ago)
+ runner.update(contacted_at: 1.second.ago)
end
it { is_expected.to be_truthy }
it 'that is inactive' do
- runner.update_attributes(active: false)
+ runner.update(active: false)
is_expected.to be_falsey
end
it 'that is not online' do
- runner.update_attributes(contacted_at: nil)
+ runner.update(contacted_at: nil)
is_expected.to be_falsey
end
@@ -227,7 +261,7 @@ describe Ci::Build do
context 'artifacts metadata does not exist' do
before do
- build.update_attributes(legacy_artifacts_metadata: nil)
+ build.update(legacy_artifacts_metadata: nil)
end
it { is_expected.to be_falsy }
@@ -480,6 +514,22 @@ describe Ci::Build do
end
end
+ describe '#has_old_trace?' do
+ subject { build.has_old_trace? }
+
+ context 'when old trace exists' do
+ before do
+ build.update_column(:trace, 'old trace')
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when old trace does not exist' do
+ it { is_expected.to be_falsy }
+ end
+ end
+
describe '#trace=' do
it "expect to fail trace=" do
expect { build.trace = "new" }.to raise_error(NotImplementedError)
@@ -499,16 +549,32 @@ describe Ci::Build do
end
describe '#erase_old_trace!' do
- subject { build.send(:read_attribute, :trace) }
+ subject { build.erase_old_trace! }
- before do
- build.send(:write_attribute, :trace, 'old trace')
+ context 'when old trace exists' do
+ before do
+ build.update_column(:trace, 'old trace')
+ end
+
+ it "erases old trace" do
+ subject
+
+ expect(build.old_trace).to be_nil
+ end
+
+ it "executes UPDATE query" do
+ recorded = ActiveRecord::QueryRecorder.new { subject }
+
+ expect(recorded.log.select { |l| l.match?(/UPDATE.*ci_builds/) }.count).to eq(1)
+ end
end
- it "expect to receive data from database" do
- build.erase_old_trace!
+ context 'when old trace does not exist' do
+ it 'does not execute UPDATE query' do
+ recorded = ActiveRecord::QueryRecorder.new { subject }
- is_expected.to be_nil
+ expect(recorded.log.select { |l| l.match?(/UPDATE.*ci_builds/) }.count).to eq(0)
+ end
end
end
@@ -1388,12 +1454,7 @@ describe Ci::Build do
it { is_expected.to be_truthy }
context "and there are specific runner" do
- let(:runner) { create(:ci_runner, contacted_at: 1.second.ago) }
-
- before do
- build.project.runners << runner
- runner.save
- end
+ let!(:runner) { create(:ci_runner, :project, projects: [build.project], contacted_at: 1.second.ago) }
it { is_expected.to be_falsey }
end
@@ -1474,7 +1535,7 @@ describe Ci::Build do
expect(ProjectStatistics)
.not_to receive(:increment_statistic)
- build.project.update_attributes(pending_delete: true)
+ build.project.update(pending_delete: true)
build.project.destroy!
end
end
@@ -1534,7 +1595,9 @@ describe Ci::Build do
let(:predefined_variables) do
[
{ key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true },
+ { key: 'CI_PIPELINE_URL', value: project.web_url + "/pipelines/#{pipeline.id}", public: true },
{ key: 'CI_JOB_ID', value: build.id.to_s, public: true },
+ { key: 'CI_JOB_URL', value: project.web_url + "/-/jobs/#{build.id}", public: true },
{ key: 'CI_JOB_TOKEN', value: build.token, public: false },
{ key: 'CI_BUILD_ID', value: build.id.to_s, public: true },
{ key: 'CI_BUILD_TOKEN', value: build.token, public: false },
@@ -1546,7 +1609,7 @@ describe Ci::Build do
{ key: 'GITLAB_FEATURES', value: project.licensed_features.join(','), public: true },
{ key: 'CI_SERVER_NAME', value: 'GitLab', public: true },
{ key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true },
- { key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true },
+ { key: 'CI_SERVER_REVISION', value: Gitlab.revision, public: true },
{ key: 'CI_JOB_NAME', value: 'test', public: true },
{ key: 'CI_JOB_STAGE', value: 'test', public: true },
{ key: 'CI_COMMIT_SHA', value: build.sha, public: true },
@@ -1565,6 +1628,7 @@ describe Ci::Build do
{ 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_IID', value: pipeline.iid.to_s, public: true },
{ key: 'CI_CONFIG_PATH', value: pipeline.ci_yaml_file_path, public: true },
{ key: 'CI_PIPELINE_SOURCE', value: pipeline.source, public: true },
{ key: 'CI_COMMIT_MESSAGE', value: pipeline.git_commit_message, public: true },
@@ -1598,7 +1662,7 @@ describe Ci::Build do
end
before do
- build.update_attributes(user: user)
+ build.update(user: user)
end
it { user_variables.each { |v| is_expected.to include(v) } }
@@ -1676,7 +1740,7 @@ describe Ci::Build do
context 'when build started manually' do
before do
- build.update_attributes(when: :manual)
+ build.update(when: :manual)
end
let(:manual_variable) do
@@ -1692,7 +1756,7 @@ describe Ci::Build do
end
before do
- build.update_attributes(tag: true)
+ build.update(tag: true)
end
it { is_expected.to include(tag_variable) }
@@ -1854,7 +1918,11 @@ describe Ci::Build do
end
context 'when yaml_variables are undefined' do
- let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:pipeline) do
+ create(:ci_pipeline, project: project,
+ sha: project.commit.id,
+ ref: project.default_branch)
+ end
before do
build.yaml_variables = nil
@@ -2156,6 +2224,7 @@ describe Ci::Build do
it 'does not return prohibited variables' do
keys = %w[CI_JOB_ID
+ CI_JOB_URL
CI_JOB_TOKEN
CI_BUILD_ID
CI_BUILD_TOKEN
@@ -2511,4 +2580,165 @@ describe Ci::Build do
end
end
end
+
+ describe 'pages deployments' do
+ set(:build) { create(:ci_build, project: project, user: user) }
+
+ context 'when job is "pages"' do
+ before do
+ build.name = 'pages'
+ end
+
+ context 'when pages are enabled' do
+ before do
+ allow(Gitlab.config.pages).to receive_messages(enabled: true)
+ end
+
+ it 'is marked as pages generator' do
+ expect(build).to be_pages_generator
+ end
+
+ context 'job succeeds' do
+ it "calls pages worker" do
+ expect(PagesWorker).to receive(:perform_async).with(:deploy, build.id)
+
+ build.success!
+ end
+ end
+
+ context 'job fails' do
+ it "does not call pages worker" do
+ expect(PagesWorker).not_to receive(:perform_async)
+
+ build.drop!
+ end
+ end
+ end
+
+ context 'when pages are disabled' do
+ before do
+ allow(Gitlab.config.pages).to receive_messages(enabled: false)
+ end
+
+ it 'is not marked as pages generator' do
+ expect(build).not_to be_pages_generator
+ end
+
+ context 'job succeeds' do
+ it "does not call pages worker" do
+ expect(PagesWorker).not_to receive(:perform_async)
+
+ build.success!
+ end
+ end
+ end
+ end
+
+ context 'when job is not "pages"' do
+ before do
+ build.name = 'other-job'
+ end
+
+ it 'is not marked as pages generator' do
+ expect(build).not_to be_pages_generator
+ end
+
+ context 'job succeeds' do
+ it "does not call pages worker" do
+ expect(PagesWorker).not_to receive(:perform_async)
+
+ build.success
+ end
+ end
+ end
+ end
+
+ describe '#has_terminal?' do
+ let(:states) { described_class.state_machines[:status].states.keys - [:running] }
+
+ subject { build.has_terminal? }
+
+ it 'returns true if the build is running and it has a runner_session_url' do
+ build.build_runner_session(url: 'whatever')
+ build.status = :running
+
+ expect(subject).to be_truthy
+ end
+
+ context 'returns false' do
+ it 'when runner_session_url is empty' do
+ build.status = :running
+
+ expect(subject).to be_falsey
+ end
+
+ context 'unless the build is running' do
+ before do
+ build.build_runner_session(url: 'whatever')
+ end
+
+ it do
+ states.each do |state|
+ build.status = state
+
+ is_expected.to be_falsey
+ end
+ end
+ end
+ end
+ end
+
+ describe '#artifacts_metadata_entry' do
+ set(:build) { create(:ci_build, project: project) }
+ let(:path) { 'other_artifacts_0.1.2/another-subdirectory/banana_sample.gif' }
+
+ before do
+ stub_artifacts_object_storage
+ end
+
+ subject { build.artifacts_metadata_entry(path) }
+
+ context 'when using local storage' do
+ let!(:metadata) { create(:ci_job_artifact, :metadata, job: build) }
+
+ context 'for existing file' do
+ it 'does exist' do
+ is_expected.to be_exists
+ end
+ end
+
+ context 'for non-existing file' do
+ let(:path) { 'invalid-file' }
+
+ it 'does not exist' do
+ is_expected.not_to be_exists
+ end
+ end
+ end
+
+ context 'when using remote storage' do
+ include HttpIOHelpers
+
+ let!(:metadata) { create(:ci_job_artifact, :remote_store, :metadata, job: build) }
+ let(:file_path) { expand_fixture_path('ci_build_artifacts_metadata.gz') }
+
+ before do
+ stub_remote_url_206(metadata.file.url, file_path)
+ end
+
+ context 'for existing file' do
+ it 'does exist' do
+ is_expected.to be_exists
+ end
+ end
+
+ context 'for non-existing file' do
+ let(:path) { 'invalid-file' }
+
+ it 'does not exist' do
+ is_expected.not_to be_exists
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/ci/build_trace_chunk_spec.rb b/spec/models/ci/build_trace_chunk_spec.rb
index cbcf1e55979..774a638b430 100644
--- a/spec/models/ci/build_trace_chunk_spec.rb
+++ b/spec/models/ci/build_trace_chunk_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
+ include ExclusiveLeaseHelpers
+
set(:build) { create(:ci_build, :running) }
let(:chunk_index) { 0 }
let(:data_store) { :redis }
@@ -12,6 +14,7 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
before do
stub_feature_flags(ci_enable_live_trace: true)
+ stub_artifacts_object_storage
end
context 'FastDestroyAll' do
@@ -35,6 +38,22 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
end
end
+ describe '.all_stores' do
+ subject { described_class.all_stores }
+
+ it 'returns a correctly ordered array' do
+ is_expected.to eq(%w[redis database fog])
+ end
+
+ it 'returns redis store as the the lowest precedence' do
+ expect(subject.first).to eq('redis')
+ end
+
+ it 'returns fog store as the the highest precedence' do
+ expect(subject.last).to eq('fog')
+ end
+ end
+
describe '#data' do
subject { build_trace_chunk.data }
@@ -42,197 +61,269 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
let(:data_store) { :redis }
before do
- build_trace_chunk.send(:redis_set_data, 'Sample data in redis')
+ build_trace_chunk.send(:unsafe_set_data!, 'Sample data in redis')
end
it { is_expected.to eq('Sample data in redis') }
end
context 'when data_store is database' do
- let(:data_store) { :db }
- let(:raw_data) { 'Sample data in db' }
+ let(:data_store) { :database }
+ let(:raw_data) { 'Sample data in database' }
- it { is_expected.to eq('Sample data in db') }
+ it { is_expected.to eq('Sample data in database') }
end
- context 'when data_store is others' do
+ context 'when data_store is fog' do
+ let(:data_store) { :fog }
+
before do
- build_trace_chunk.send(:write_attribute, :data_store, -1)
+ build_trace_chunk.send(:unsafe_set_data!, 'Sample data in fog')
end
- it { expect { subject }.to raise_error('Unsupported data store') }
+ it { is_expected.to eq('Sample data in fog') }
end
end
- describe '#set_data' do
- subject { build_trace_chunk.send(:set_data, value) }
+ describe '#append' do
+ subject { build_trace_chunk.append(new_data, offset) }
- let(:value) { 'Sample data' }
+ let(:new_data) { 'Sample new data' }
+ let(:offset) { 0 }
+ let(:merged_data) { data + new_data.to_s }
- context 'when value bytesize is bigger than CHUNK_SIZE' do
- let(:value) { 'a' * (described_class::CHUNK_SIZE + 1) }
+ shared_examples_for 'Appending correctly' do
+ context 'when offset is negative' do
+ let(:offset) { -1 }
- it { expect { subject }.to raise_error('too much data') }
- end
+ it { expect { subject }.to raise_error('Offset is out of range') }
+ end
- context 'when data_store is redis' do
- let(:data_store) { :redis }
+ context 'when offset is bigger than data size' do
+ let(:offset) { data.bytesize + 1 }
- it do
- expect(build_trace_chunk.send(:redis_data)).to be_nil
+ it { expect { subject }.to raise_error('Offset is out of range') }
+ end
- subject
+ context 'when new data overflows chunk size' do
+ let(:new_data) { 'a' * (described_class::CHUNK_SIZE + 1) }
- expect(build_trace_chunk.send(:redis_data)).to eq(value)
+ it { expect { subject }.to raise_error('Chunk size overflow') }
end
- context 'when fullfilled chunk size' do
- let(:value) { 'a' * described_class::CHUNK_SIZE }
-
- it 'schedules stashing data' do
- expect(Ci::BuildTraceChunkFlushWorker).to receive(:perform_async).once
+ context 'when offset is EOF' do
+ let(:offset) { data.bytesize }
+ it 'appends' do
subject
+
+ expect(build_trace_chunk.data).to eq(merged_data)
end
- end
- end
- context 'when data_store is database' do
- let(:data_store) { :db }
+ context 'when the other process is appending' do
+ let(:lease_key) { "trace_write:#{build_trace_chunk.build.id}:chunks:#{build_trace_chunk.chunk_index}" }
- it 'sets data' do
- expect(build_trace_chunk.raw_data).to be_nil
+ before do
+ stub_exclusive_lease_taken(lease_key)
+ end
- subject
+ it 'raise an error' do
+ expect { subject }.to raise_error('Failed to obtain a lock')
+ end
+ end
- expect(build_trace_chunk.raw_data).to eq(value)
- expect(build_trace_chunk.persisted?).to be_truthy
- end
+ context 'when new_data is nil' do
+ let(:new_data) { nil }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error('New data is missing')
+ end
+ end
+
+ context 'when new_data is empty' do
+ let(:new_data) { '' }
- context 'when raw_data is not changed' do
- it 'does not execute UPDATE' do
- expect(build_trace_chunk.raw_data).to be_nil
- build_trace_chunk.save!
+ it 'does not append' do
+ subject
- # First set
- expect(ActiveRecord::QueryRecorder.new { subject }.count).to be > 0
- expect(build_trace_chunk.raw_data).to eq(value)
- expect(build_trace_chunk.persisted?).to be_truthy
+ expect(build_trace_chunk.data).to eq(data)
+ end
- # Second set
- build_trace_chunk.reload
- expect(ActiveRecord::QueryRecorder.new { subject }.count).to be(0)
+ it 'does not execute UPDATE' do
+ ActiveRecord::QueryRecorder.new { subject }.log.map do |query|
+ expect(query).not_to include('UPDATE')
+ end
+ end
end
end
- context 'when fullfilled chunk size' do
- it 'does not schedule stashing data' do
- expect(Ci::BuildTraceChunkFlushWorker).not_to receive(:perform_async)
+ context 'when offset is middle of datasize' do
+ let(:offset) { data.bytesize / 2 }
+ it 'appends' do
subject
+
+ expect(build_trace_chunk.data).to eq(data.byteslice(0, offset) + new_data)
end
end
end
- context 'when data_store is others' do
- before do
- build_trace_chunk.send(:write_attribute, :data_store, -1)
- end
+ shared_examples_for 'Scheduling sidekiq worker to flush data to persist store' do
+ context 'when new data fullfilled chunk size' do
+ let(:new_data) { 'a' * described_class::CHUNK_SIZE }
- it { expect { subject }.to raise_error('Unsupported data store') }
- end
- end
+ it 'schedules trace chunk flush worker' do
+ expect(Ci::BuildTraceChunkFlushWorker).to receive(:perform_async).once
- describe '#truncate' do
- subject { build_trace_chunk.truncate(offset) }
+ subject
+ end
- shared_examples_for 'truncates' do
- context 'when offset is negative' do
- let(:offset) { -1 }
+ it 'migrates data to object storage' do
+ Sidekiq::Testing.inline! do
+ subject
- it { expect { subject }.to raise_error('Offset is out of range') }
+ build_trace_chunk.reload
+ expect(build_trace_chunk.fog?).to be_truthy
+ expect(build_trace_chunk.data).to eq(new_data)
+ end
+ end
end
+ end
- context 'when offset is bigger than data size' do
- let(:offset) { data.bytesize + 1 }
-
- it { expect { subject }.to raise_error('Offset is out of range') }
- end
+ shared_examples_for 'Scheduling no sidekiq worker' do
+ context 'when new data fullfilled chunk size' do
+ let(:new_data) { 'a' * described_class::CHUNK_SIZE }
- context 'when offset is 10' do
- let(:offset) { 10 }
+ it 'does not schedule trace chunk flush worker' do
+ expect(Ci::BuildTraceChunkFlushWorker).not_to receive(:perform_async)
- it 'truncates' do
subject
+ end
- expect(build_trace_chunk.data).to eq(data.byteslice(0, offset))
+ it 'does not migrate data to object storage' do
+ Sidekiq::Testing.inline! do
+ data_store = build_trace_chunk.data_store
+
+ subject
+
+ build_trace_chunk.reload
+ expect(build_trace_chunk.data_store).to eq(data_store)
+ end
end
end
end
context 'when data_store is redis' do
let(:data_store) { :redis }
- let(:data) { 'Sample data in redis' }
- before do
- build_trace_chunk.send(:redis_set_data, data)
+ context 'when there are no data' do
+ let(:data) { '' }
+
+ it 'has no data' do
+ expect(build_trace_chunk.data).to be_empty
+ end
+
+ it_behaves_like 'Appending correctly'
+ it_behaves_like 'Scheduling sidekiq worker to flush data to persist store'
end
- it_behaves_like 'truncates'
- end
+ context 'when there are some data' do
+ let(:data) { 'Sample data in redis' }
- context 'when data_store is database' do
- let(:data_store) { :db }
- let(:raw_data) { 'Sample data in db' }
- let(:data) { raw_data }
+ before do
+ build_trace_chunk.send(:unsafe_set_data!, data)
+ end
- it_behaves_like 'truncates'
+ it 'has data' do
+ expect(build_trace_chunk.data).to eq(data)
+ end
+
+ it_behaves_like 'Appending correctly'
+ it_behaves_like 'Scheduling sidekiq worker to flush data to persist store'
+ end
end
- end
- describe '#append' do
- subject { build_trace_chunk.append(new_data, offset) }
+ context 'when data_store is database' do
+ let(:data_store) { :database }
- let(:new_data) { 'Sample new data' }
- let(:offset) { 0 }
- let(:total_data) { data + new_data }
+ context 'when there are no data' do
+ let(:data) { '' }
- shared_examples_for 'appends' do
- context 'when offset is negative' do
- let(:offset) { -1 }
+ it 'has no data' do
+ expect(build_trace_chunk.data).to be_empty
+ end
- it { expect { subject }.to raise_error('Offset is out of range') }
+ it_behaves_like 'Appending correctly'
+ it_behaves_like 'Scheduling no sidekiq worker'
end
- context 'when offset is bigger than data size' do
- let(:offset) { data.bytesize + 1 }
+ context 'when there are some data' do
+ let(:raw_data) { 'Sample data in database' }
+ let(:data) { raw_data }
- it { expect { subject }.to raise_error('Offset is out of range') }
+ it 'has data' do
+ expect(build_trace_chunk.data).to eq(data)
+ end
+
+ it_behaves_like 'Appending correctly'
+ it_behaves_like 'Scheduling no sidekiq worker'
end
+ end
- context 'when offset is bigger than data size' do
- let(:new_data) { 'a' * (described_class::CHUNK_SIZE + 1) }
+ context 'when data_store is fog' do
+ let(:data_store) { :fog }
- it { expect { subject }.to raise_error('Chunk size overflow') }
+ context 'when there are no data' do
+ let(:data) { '' }
+
+ it 'has no data' do
+ expect(build_trace_chunk.data).to be_empty
+ end
+
+ it_behaves_like 'Appending correctly'
+ it_behaves_like 'Scheduling no sidekiq worker'
end
- context 'when offset is EOF' do
- let(:offset) { data.bytesize }
+ context 'when there are some data' do
+ let(:data) { 'Sample data in fog' }
- it 'appends' do
- subject
+ before do
+ build_trace_chunk.send(:unsafe_set_data!, data)
+ end
- expect(build_trace_chunk.data).to eq(total_data)
+ it 'has data' do
+ expect(build_trace_chunk.data).to eq(data)
end
+
+ it_behaves_like 'Appending correctly'
+ it_behaves_like 'Scheduling no sidekiq worker'
+ end
+ end
+ end
+
+ describe '#truncate' do
+ subject { build_trace_chunk.truncate(offset) }
+
+ shared_examples_for 'truncates' do
+ context 'when offset is negative' do
+ let(:offset) { -1 }
+
+ it { expect { subject }.to raise_error('Offset is out of range') }
+ end
+
+ context 'when offset is bigger than data size' do
+ let(:offset) { data.bytesize + 1 }
+
+ it { expect { subject }.to raise_error('Offset is out of range') }
end
context 'when offset is 10' do
let(:offset) { 10 }
- it 'appends' do
+ it 'truncates' do
subject
- expect(build_trace_chunk.data).to eq(data.byteslice(0, offset) + new_data)
+ expect(build_trace_chunk.data).to eq(data.byteslice(0, offset))
end
end
end
@@ -242,18 +333,29 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
let(:data) { 'Sample data in redis' }
before do
- build_trace_chunk.send(:redis_set_data, data)
+ build_trace_chunk.send(:unsafe_set_data!, data)
end
- it_behaves_like 'appends'
+ it_behaves_like 'truncates'
end
context 'when data_store is database' do
- let(:data_store) { :db }
- let(:raw_data) { 'Sample data in db' }
+ let(:data_store) { :database }
+ let(:raw_data) { 'Sample data in database' }
let(:data) { raw_data }
- it_behaves_like 'appends'
+ it_behaves_like 'truncates'
+ end
+
+ context 'when data_store is fog' do
+ let(:data_store) { :fog }
+ let(:data) { 'Sample data in fog' }
+
+ before do
+ build_trace_chunk.send(:unsafe_set_data!, data)
+ end
+
+ it_behaves_like 'truncates'
end
end
@@ -267,7 +369,7 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
let(:data) { 'Sample data in redis' }
before do
- build_trace_chunk.send(:redis_set_data, data)
+ build_trace_chunk.send(:unsafe_set_data!, data)
end
it { is_expected.to eq(data.bytesize) }
@@ -279,10 +381,10 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
end
context 'when data_store is database' do
- let(:data_store) { :db }
+ let(:data_store) { :database }
context 'when data exists' do
- let(:raw_data) { 'Sample data in db' }
+ let(:raw_data) { 'Sample data in database' }
let(:data) { raw_data }
it { is_expected.to eq(data.bytesize) }
@@ -292,10 +394,43 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
it { is_expected.to eq(0) }
end
end
+
+ context 'when data_store is fog' do
+ let(:data_store) { :fog }
+
+ context 'when data exists' do
+ let(:data) { 'Sample data in fog' }
+ let(:key) { "tmp/builds/#{build.id}/chunks/#{chunk_index}.log" }
+
+ before do
+ build_trace_chunk.send(:unsafe_set_data!, data)
+ end
+
+ it { is_expected.to eq(data.bytesize) }
+ end
+
+ context 'when data does not exist' do
+ it { is_expected.to eq(0) }
+ end
+ end
end
- describe '#use_database!' do
- subject { build_trace_chunk.use_database! }
+ describe '#persist_data!' do
+ subject { build_trace_chunk.persist_data! }
+
+ shared_examples_for 'Atomic operation' do
+ context 'when the other process is persisting' do
+ let(:lease_key) { "trace_write:#{build_trace_chunk.build.id}:chunks:#{build_trace_chunk.chunk_index}" }
+
+ before do
+ stub_exclusive_lease_taken(lease_key)
+ end
+
+ it 'raise an error' do
+ expect { subject }.to raise_error('Failed to obtain a lock')
+ end
+ end
+ end
context 'when data_store is redis' do
let(:data_store) { :redis }
@@ -304,46 +439,93 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
let(:data) { 'Sample data in redis' }
before do
- build_trace_chunk.send(:redis_set_data, data)
+ build_trace_chunk.send(:unsafe_set_data!, data)
end
- it 'stashes the data' do
- expect(build_trace_chunk.data_store).to eq('redis')
- expect(build_trace_chunk.send(:redis_data)).to eq(data)
- expect(build_trace_chunk.raw_data).to be_nil
+ it 'persists the data' do
+ expect(build_trace_chunk.redis?).to be_truthy
+ expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to eq(data)
+ expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil
+ expect { Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk) }.to raise_error(Excon::Error::NotFound)
subject
- expect(build_trace_chunk.data_store).to eq('db')
- expect(build_trace_chunk.send(:redis_data)).to be_nil
- expect(build_trace_chunk.raw_data).to eq(data)
+ expect(build_trace_chunk.fog?).to be_truthy
+ expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil
+ expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil
+ expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to eq(data)
end
+
+ it_behaves_like 'Atomic operation'
end
context 'when data does not exist' do
- it 'does not call UPDATE' do
- expect(ActiveRecord::QueryRecorder.new { subject }.count).to eq(0)
+ it 'does not persist' do
+ expect { subject }.to raise_error('Can not persist empty data')
end
end
end
context 'when data_store is database' do
- let(:data_store) { :db }
+ let(:data_store) { :database }
- it 'does not call UPDATE' do
- expect(ActiveRecord::QueryRecorder.new { subject }.count).to eq(0)
+ context 'when data exists' do
+ let(:data) { 'Sample data in database' }
+
+ before do
+ build_trace_chunk.send(:unsafe_set_data!, data)
+ end
+
+ it 'persists the data' do
+ expect(build_trace_chunk.database?).to be_truthy
+ expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil
+ expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to eq(data)
+ expect { Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk) }.to raise_error(Excon::Error::NotFound)
+
+ subject
+
+ expect(build_trace_chunk.fog?).to be_truthy
+ expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil
+ expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil
+ expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to eq(data)
+ end
+
+ it_behaves_like 'Atomic operation'
end
- end
- end
- describe 'ExclusiveLock' do
- before do
- allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain) { nil }
- stub_const('Ci::BuildTraceChunk::WRITE_LOCK_RETRY', 1)
+ context 'when data does not exist' do
+ it 'does not persist' do
+ expect { subject }.to raise_error('Can not persist empty data')
+ end
+ end
end
- it 'raise an error' do
- expect { build_trace_chunk.append('ABC', 0) }.to raise_error('Failed to obtain write lock')
+ context 'when data_store is fog' do
+ let(:data_store) { :fog }
+
+ context 'when data exists' do
+ let(:data) { 'Sample data in fog' }
+
+ before do
+ build_trace_chunk.send(:unsafe_set_data!, data)
+ end
+
+ it 'does not change data store' do
+ expect(build_trace_chunk.fog?).to be_truthy
+ expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil
+ expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil
+ expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to eq(data)
+
+ subject
+
+ expect(build_trace_chunk.fog?).to be_truthy
+ expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil
+ expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil
+ expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to eq(data)
+ end
+
+ it_behaves_like 'Atomic operation'
+ end
end
end
diff --git a/spec/models/ci/build_trace_chunks/database_spec.rb b/spec/models/ci/build_trace_chunks/database_spec.rb
new file mode 100644
index 00000000000..d8fc9d57e95
--- /dev/null
+++ b/spec/models/ci/build_trace_chunks/database_spec.rb
@@ -0,0 +1,105 @@
+require 'spec_helper'
+
+describe Ci::BuildTraceChunks::Database do
+ let(:data_store) { described_class.new }
+
+ describe '#available?' do
+ subject { data_store.available? }
+
+ it { is_expected.to be_truthy }
+ end
+
+ describe '#data' do
+ subject { data_store.data(model) }
+
+ context 'when data exists' do
+ let(:model) { create(:ci_build_trace_chunk, :database_with_data, initial_data: 'sample data in database') }
+
+ it 'returns the data' do
+ is_expected.to eq('sample data in database')
+ end
+ end
+
+ context 'when data does not exist' do
+ let(:model) { create(:ci_build_trace_chunk, :database_without_data) }
+
+ it 'returns nil' do
+ is_expected.to be_nil
+ end
+ end
+ end
+
+ describe '#set_data' do
+ subject { data_store.set_data(model, data) }
+
+ let(:data) { 'abc123' }
+
+ context 'when data exists' do
+ let(:model) { create(:ci_build_trace_chunk, :database_with_data, initial_data: 'sample data in database') }
+
+ it 'overwrites data' do
+ expect(data_store.data(model)).to eq('sample data in database')
+
+ subject
+
+ expect(data_store.data(model)).to eq('abc123')
+ end
+ end
+
+ context 'when data does not exist' do
+ let(:model) { create(:ci_build_trace_chunk, :database_without_data) }
+
+ it 'sets new data' do
+ expect(data_store.data(model)).to be_nil
+
+ subject
+
+ expect(data_store.data(model)).to eq('abc123')
+ end
+ end
+ end
+
+ describe '#delete_data' do
+ subject { data_store.delete_data(model) }
+
+ context 'when data exists' do
+ let(:model) { create(:ci_build_trace_chunk, :database_with_data, initial_data: 'sample data in database') }
+
+ it 'deletes data' do
+ expect(data_store.data(model)).to eq('sample data in database')
+
+ subject
+
+ expect(data_store.data(model)).to be_nil
+ end
+ end
+
+ context 'when data does not exist' do
+ let(:model) { create(:ci_build_trace_chunk, :database_without_data) }
+
+ it 'does nothing' do
+ expect(data_store.data(model)).to be_nil
+
+ subject
+
+ expect(data_store.data(model)).to be_nil
+ end
+ end
+ end
+
+ describe '#keys' do
+ subject { data_store.keys(relation) }
+
+ let(:build) { create(:ci_build) }
+ let(:relation) { build.trace_chunks }
+
+ before do
+ create(:ci_build_trace_chunk, :database_with_data, chunk_index: 0, build: build)
+ create(:ci_build_trace_chunk, :database_with_data, chunk_index: 1, build: build)
+ end
+
+ it 'returns empty array' do
+ is_expected.to eq([])
+ end
+ end
+end
diff --git a/spec/models/ci/build_trace_chunks/fog_spec.rb b/spec/models/ci/build_trace_chunks/fog_spec.rb
new file mode 100644
index 00000000000..8f49190af13
--- /dev/null
+++ b/spec/models/ci/build_trace_chunks/fog_spec.rb
@@ -0,0 +1,146 @@
+require 'spec_helper'
+
+describe Ci::BuildTraceChunks::Fog do
+ let(:data_store) { described_class.new }
+
+ before do
+ stub_artifacts_object_storage
+ end
+
+ describe '#available?' do
+ subject { data_store.available? }
+
+ context 'when object storage is enabled' do
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when object storage is disabled' do
+ before do
+ stub_artifacts_object_storage(enabled: false)
+ end
+
+ it { is_expected.to be_falsy }
+ end
+ end
+
+ describe '#data' do
+ subject { data_store.data(model) }
+
+ context 'when data exists' do
+ let(:model) { create(:ci_build_trace_chunk, :fog_with_data, initial_data: 'sample data in fog') }
+
+ it 'returns the data' do
+ is_expected.to eq('sample data in fog')
+ end
+ end
+
+ context 'when data does not exist' do
+ let(:model) { create(:ci_build_trace_chunk, :fog_without_data) }
+
+ it 'returns nil' do
+ expect { data_store.data(model) }.to raise_error(Excon::Error::NotFound)
+ end
+ end
+ end
+
+ describe '#set_data' do
+ subject { data_store.set_data(model, data) }
+
+ let(:data) { 'abc123' }
+
+ context 'when data exists' do
+ let(:model) { create(:ci_build_trace_chunk, :fog_with_data, initial_data: 'sample data in fog') }
+
+ it 'overwrites data' do
+ expect(data_store.data(model)).to eq('sample data in fog')
+
+ subject
+
+ expect(data_store.data(model)).to eq('abc123')
+ end
+ end
+
+ context 'when data does not exist' do
+ let(:model) { create(:ci_build_trace_chunk, :fog_without_data) }
+
+ it 'sets new data' do
+ expect { data_store.data(model) }.to raise_error(Excon::Error::NotFound)
+
+ subject
+
+ expect(data_store.data(model)).to eq('abc123')
+ end
+ end
+ end
+
+ describe '#delete_data' do
+ subject { data_store.delete_data(model) }
+
+ context 'when data exists' do
+ let(:model) { create(:ci_build_trace_chunk, :fog_with_data, initial_data: 'sample data in fog') }
+
+ it 'deletes data' do
+ expect(data_store.data(model)).to eq('sample data in fog')
+
+ subject
+
+ expect { data_store.data(model) }.to raise_error(Excon::Error::NotFound)
+ end
+ end
+
+ context 'when data does not exist' do
+ let(:model) { create(:ci_build_trace_chunk, :fog_without_data) }
+
+ it 'does nothing' do
+ expect { data_store.data(model) }.to raise_error(Excon::Error::NotFound)
+
+ subject
+
+ expect { data_store.data(model) }.to raise_error(Excon::Error::NotFound)
+ end
+ end
+ end
+
+ describe '#keys' do
+ subject { data_store.keys(relation) }
+
+ let(:build) { create(:ci_build) }
+ let(:relation) { build.trace_chunks }
+
+ before do
+ create(:ci_build_trace_chunk, :fog_with_data, chunk_index: 0, build: build)
+ create(:ci_build_trace_chunk, :fog_with_data, chunk_index: 1, build: build)
+ end
+
+ it 'returns keys' do
+ is_expected.to eq([[build.id, 0], [build.id, 1]])
+ end
+ end
+
+ describe '#delete_keys' do
+ subject { data_store.delete_keys(keys) }
+
+ let(:build) { create(:ci_build) }
+ let(:relation) { build.trace_chunks }
+ let(:keys) { data_store.keys(relation) }
+
+ before do
+ create(:ci_build_trace_chunk, :fog_with_data, chunk_index: 0, build: build)
+ create(:ci_build_trace_chunk, :fog_with_data, chunk_index: 1, build: build)
+ end
+
+ it 'deletes multiple data' do
+ ::Fog::Storage.new(JobArtifactUploader.object_store_credentials).tap do |connection|
+ expect(connection.get_object('artifacts', "tmp/builds/#{build.id}/chunks/0.log")[:body]).to be_present
+ expect(connection.get_object('artifacts', "tmp/builds/#{build.id}/chunks/1.log")[:body]).to be_present
+ end
+
+ subject
+
+ ::Fog::Storage.new(JobArtifactUploader.object_store_credentials).tap do |connection|
+ expect { connection.get_object('artifacts', "tmp/builds/#{build.id}/chunks/0.log")[:body] }.to raise_error(Excon::Error::NotFound)
+ expect { connection.get_object('artifacts', "tmp/builds/#{build.id}/chunks/1.log")[:body] }.to raise_error(Excon::Error::NotFound)
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/build_trace_chunks/redis_spec.rb b/spec/models/ci/build_trace_chunks/redis_spec.rb
new file mode 100644
index 00000000000..9da1e6a95ee
--- /dev/null
+++ b/spec/models/ci/build_trace_chunks/redis_spec.rb
@@ -0,0 +1,132 @@
+require 'spec_helper'
+
+describe Ci::BuildTraceChunks::Redis, :clean_gitlab_redis_shared_state do
+ let(:data_store) { described_class.new }
+
+ describe '#available?' do
+ subject { data_store.available? }
+
+ it { is_expected.to be_truthy }
+ end
+
+ describe '#data' do
+ subject { data_store.data(model) }
+
+ context 'when data exists' do
+ let(:model) { create(:ci_build_trace_chunk, :redis_with_data, initial_data: 'sample data in redis') }
+
+ it 'returns the data' do
+ is_expected.to eq('sample data in redis')
+ end
+ end
+
+ context 'when data does not exist' do
+ let(:model) { create(:ci_build_trace_chunk, :redis_without_data) }
+
+ it 'returns nil' do
+ is_expected.to be_nil
+ end
+ end
+ end
+
+ describe '#set_data' do
+ subject { data_store.set_data(model, data) }
+
+ let(:data) { 'abc123' }
+
+ context 'when data exists' do
+ let(:model) { create(:ci_build_trace_chunk, :redis_with_data, initial_data: 'sample data in redis') }
+
+ it 'overwrites data' do
+ expect(data_store.data(model)).to eq('sample data in redis')
+
+ subject
+
+ expect(data_store.data(model)).to eq('abc123')
+ end
+ end
+
+ context 'when data does not exist' do
+ let(:model) { create(:ci_build_trace_chunk, :redis_without_data) }
+
+ it 'sets new data' do
+ expect(data_store.data(model)).to be_nil
+
+ subject
+
+ expect(data_store.data(model)).to eq('abc123')
+ end
+ end
+ end
+
+ describe '#delete_data' do
+ subject { data_store.delete_data(model) }
+
+ context 'when data exists' do
+ let(:model) { create(:ci_build_trace_chunk, :redis_with_data, initial_data: 'sample data in redis') }
+
+ it 'deletes data' do
+ expect(data_store.data(model)).to eq('sample data in redis')
+
+ subject
+
+ expect(data_store.data(model)).to be_nil
+ end
+ end
+
+ context 'when data does not exist' do
+ let(:model) { create(:ci_build_trace_chunk, :redis_without_data) }
+
+ it 'does nothing' do
+ expect(data_store.data(model)).to be_nil
+
+ subject
+
+ expect(data_store.data(model)).to be_nil
+ end
+ end
+ end
+
+ describe '#keys' do
+ subject { data_store.keys(relation) }
+
+ let(:build) { create(:ci_build) }
+ let(:relation) { build.trace_chunks }
+
+ before do
+ create(:ci_build_trace_chunk, :redis_with_data, chunk_index: 0, build: build)
+ create(:ci_build_trace_chunk, :redis_with_data, chunk_index: 1, build: build)
+ end
+
+ it 'returns keys' do
+ is_expected.to eq([[build.id, 0], [build.id, 1]])
+ end
+ end
+
+ describe '#delete_keys' do
+ subject { data_store.delete_keys(keys) }
+
+ let(:build) { create(:ci_build) }
+ let(:relation) { build.trace_chunks }
+ let(:keys) { data_store.keys(relation) }
+
+ before do
+ create(:ci_build_trace_chunk, :redis_with_data, chunk_index: 0, build: build)
+ create(:ci_build_trace_chunk, :redis_with_data, chunk_index: 1, build: build)
+ end
+
+ it 'deletes multiple data' do
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis.exists("gitlab:ci:trace:#{build.id}:chunks:0")).to be_truthy
+ expect(redis.exists("gitlab:ci:trace:#{build.id}:chunks:1")).to be_truthy
+ end
+
+ subject
+
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis.exists("gitlab:ci:trace:#{build.id}:chunks:0")).to be_falsy
+ expect(redis.exists("gitlab:ci:trace:#{build.id}:chunks:1")).to be_falsy
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/group_spec.rb b/spec/models/ci/group_spec.rb
index 51123e73fe6..838fa63cb1f 100644
--- a/spec/models/ci/group_spec.rb
+++ b/spec/models/ci/group_spec.rb
@@ -41,4 +41,55 @@ describe Ci::Group do
end
end
end
+
+ describe '.fabricate' do
+ let(:pipeline) { create(:ci_empty_pipeline) }
+ let(:stage) { create(:ci_stage_entity, pipeline: pipeline) }
+
+ before do
+ create_build(:ci_build, name: 'rspec 0 2')
+ create_build(:ci_build, name: 'rspec 0 1')
+ create_build(:ci_build, name: 'spinach 0 1')
+ create_build(:commit_status, name: 'aaaaa')
+ end
+
+ it 'returns an array of three groups' do
+ expect(stage.groups).to be_a Array
+ expect(stage.groups).to all(be_a described_class)
+ expect(stage.groups.size).to eq 3
+ end
+
+ it 'returns groups with correctly ordered statuses' do
+ expect(stage.groups.first.jobs.map(&:name))
+ .to eq ['aaaaa']
+ expect(stage.groups.second.jobs.map(&:name))
+ .to eq ['rspec 0 1', 'rspec 0 2']
+ expect(stage.groups.third.jobs.map(&:name))
+ .to eq ['spinach 0 1']
+ end
+
+ it 'returns groups with correct names' do
+ expect(stage.groups.map(&:name))
+ .to eq %w[aaaaa rspec spinach]
+ end
+
+ context 'when a name is nil on legacy pipelines' do
+ before do
+ pipeline.builds.first.update_attribute(:name, nil)
+ end
+
+ it 'returns an array of three groups' do
+ expect(stage.groups.map(&:name))
+ .to eq ['', 'aaaaa', 'rspec', 'spinach']
+ end
+ end
+
+ def create_build(type, status: 'success', **opts)
+ create(type, pipeline: pipeline,
+ stage: stage.name,
+ status: status,
+ stage_id: stage.id,
+ **opts)
+ end
+ end
end
diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb
index a3e119cbc27..0fd7612c011 100644
--- a/spec/models/ci/job_artifact_spec.rb
+++ b/spec/models/ci/job_artifact_spec.rb
@@ -25,7 +25,7 @@ describe Ci::JobArtifact do
end
it 'does not schedule the migration' do
- expect(ObjectStorageUploadWorker).not_to receive(:perform_async)
+ expect(ObjectStorage::BackgroundMoveWorker).not_to receive(:perform_async)
subject
end
@@ -76,12 +76,12 @@ describe Ci::JobArtifact do
context 'updating the artifact file' do
it 'updates the artifact size' do
- artifact.update!(file: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png')))
+ artifact.update!(file: fixture_file_upload('spec/fixtures/dk.png'))
expect(artifact.size).to eq(1062)
end
it 'updates the project statistics' do
- expect { artifact.update!(file: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png'))) }
+ expect { artifact.update!(file: fixture_file_upload('spec/fixtures/dk.png')) }
.to change { artifact.project.statistics.reload.build_artifacts_size }
.by(1062 - 106365)
end
@@ -163,7 +163,7 @@ describe Ci::JobArtifact do
expect(ProjectStatistics)
.not_to receive(:increment_statistic)
- project.update_attributes(pending_delete: true)
+ project.update(pending_delete: true)
project.destroy!
end
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index e4f4c62bd22..a41657b53b7 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -35,6 +35,16 @@ describe Ci::Pipeline, :mailer do
end
end
+ describe 'modules' do
+ it_behaves_like 'AtomicInternalId', validate_presence: false do
+ let(:internal_id_attribute) { :iid }
+ let(:instance) { build(:ci_pipeline) }
+ let(:scope) { :project }
+ let(:scope_attrs) { { project: instance.project } }
+ let(:usage) { :ci_pipelines }
+ end
+ end
+
describe '#source' do
context 'when creating new pipeline' do
let(:pipeline) do
@@ -184,7 +194,7 @@ describe Ci::Pipeline, :mailer do
it 'does contains persisted variables' do
keys = subject.map { |variable| variable[:key] }
- expect(keys).to eq %w[CI_PIPELINE_ID]
+ expect(keys).to eq %w[CI_PIPELINE_ID CI_PIPELINE_URL]
end
end
end
@@ -195,7 +205,8 @@ describe Ci::Pipeline, :mailer do
it 'includes all predefined variables in a valid order' do
keys = subject.map { |variable| variable[:key] }
- expect(keys).to eq %w[CI_CONFIG_PATH
+ expect(keys).to eq %w[CI_PIPELINE_IID
+ CI_CONFIG_PATH
CI_PIPELINE_SOURCE
CI_COMMIT_MESSAGE
CI_COMMIT_TITLE
@@ -526,6 +537,87 @@ describe Ci::Pipeline, :mailer do
end
end
end
+
+ describe '#stages' do
+ before do
+ create(:ci_stage_entity, project: project,
+ pipeline: pipeline,
+ name: 'build')
+ end
+
+ it 'returns persisted stages' do
+ expect(pipeline.stages).not_to be_empty
+ expect(pipeline.stages).to all(be_persisted)
+ end
+ end
+
+ describe '#ordered_stages' do
+ before do
+ create(:ci_stage_entity, project: project,
+ pipeline: pipeline,
+ position: 4,
+ name: 'deploy')
+
+ create(:ci_build, project: project,
+ pipeline: pipeline,
+ stage: 'test',
+ stage_idx: 3,
+ name: 'test')
+
+ create(:ci_build, project: project,
+ pipeline: pipeline,
+ stage: 'build',
+ stage_idx: 2,
+ name: 'build')
+
+ create(:ci_stage_entity, project: project,
+ pipeline: pipeline,
+ position: 1,
+ name: 'sanity')
+
+ create(:ci_stage_entity, project: project,
+ pipeline: pipeline,
+ position: 5,
+ name: 'cleanup')
+ end
+
+ subject { pipeline.ordered_stages }
+
+ context 'when using legacy stages' do
+ before do
+ stub_feature_flags(ci_pipeline_persisted_stages: false)
+ end
+
+ it 'returns legacy stages in valid order' do
+ expect(subject.map(&:name)).to eq %w[build test]
+ end
+ end
+
+ context 'when using persisted stages' do
+ before do
+ stub_feature_flags(ci_pipeline_persisted_stages: true)
+ end
+
+ context 'when pipelines is not complete' do
+ it 'still returns legacy stages' do
+ expect(subject).to all(be_a Ci::LegacyStage)
+ expect(subject.map(&:name)).to eq %w[build test]
+ end
+ end
+
+ context 'when pipeline is complete' do
+ before do
+ pipeline.succeed!
+ end
+
+ it 'returns stages in valid order' do
+ expect(subject).to all(be_a Ci::Stage)
+ expect(subject.map(&:name))
+ .to eq %w[sanity build test deploy cleanup]
+ end
+ end
+ end
+ end
end
describe 'state machine' do
@@ -1170,6 +1262,43 @@ describe Ci::Pipeline, :mailer do
end
end
+ describe '#update_status' do
+ context 'when pipeline is empty' do
+ it 'updates does not change pipeline status' do
+ expect(pipeline.statuses.latest.status).to be_nil
+
+ expect { pipeline.update_status }
+ .to change { pipeline.reload.status }.to 'skipped'
+ end
+ end
+
+ context 'when updating status to pending' do
+ before do
+ allow(pipeline)
+ .to receive_message_chain(:statuses, :latest, :status)
+ .and_return(:running)
+ end
+
+ it 'updates pipeline status to running' do
+ expect { pipeline.update_status }
+ .to change { pipeline.reload.status }.to 'running'
+ end
+ end
+
+ context 'when statuses status was not recognized' do
+ before do
+ allow(pipeline)
+ .to receive(:latest_builds_status)
+ .and_return(:unknown)
+ end
+
+ it 'raises an exception' do
+ expect { pipeline.update_status }
+ .to raise_error(HasStatus::UnknownStatusError)
+ end
+ end
+ end
+
describe '#detailed_status' do
subject { pipeline.detailed_status(user) }
@@ -1589,7 +1718,7 @@ describe Ci::Pipeline, :mailer do
context 'when pipeline is not stuck' do
before do
- create(:ci_runner, :shared, :online)
+ create(:ci_runner, :instance, :online)
end
it 'is not stuck' do
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index 0fbc934f669..953af2c4710 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -21,60 +21,58 @@ describe Ci::Runner do
end
end
- context 'either_projects_or_group' do
+ context '#exactly_one_group' do
let(:group) { create(:group) }
+ let(:runner) { create(:ci_runner, :group, groups: [group]) }
- it 'disallows assigning to a group if already assigned to a group' do
- runner = create(:ci_runner, groups: [group])
-
+ it 'disallows assigning group if already assigned to a group' do
runner.groups << build(:group)
expect(runner).not_to be_valid
- expect(runner.errors.full_messages).to eq ['Runner can only be assigned to one group']
+ expect(runner.errors.full_messages).to include('Runner needs to be assigned to exactly one group')
end
+ end
- it 'disallows assigning to a group if already assigned to a project' do
- project = create(:project)
- runner = create(:ci_runner, projects: [project])
+ context 'runner_type validations' do
+ set(:group) { create(:group) }
+ set(:project) { create(:project) }
+ let(:group_runner) { create(:ci_runner, :group, groups: [group]) }
+ let(:project_runner) { create(:ci_runner, :project, projects: [project]) }
+ let(:instance_runner) { create(:ci_runner, :instance) }
- runner.groups << build(:group)
+ it 'disallows assigning group to project_type runner' do
+ project_runner.groups << build(:group)
- expect(runner).not_to be_valid
- expect(runner.errors.full_messages).to eq ['Runner can only be assigned either to projects or to a group']
+ expect(project_runner).not_to be_valid
+ expect(project_runner.errors.full_messages).to include('Runner cannot have groups assigned')
end
- it 'disallows assigning to a project if already assigned to a group' do
- runner = create(:ci_runner, groups: [group])
+ it 'disallows assigning group to instance_type runner' do
+ instance_runner.groups << build(:group)
- runner.projects << build(:project)
-
- expect(runner).not_to be_valid
- expect(runner.errors.full_messages).to eq ['Runner can only be assigned either to projects or to a group']
+ expect(instance_runner).not_to be_valid
+ expect(instance_runner.errors.full_messages).to include('Runner cannot have groups assigned')
end
- it 'allows assigning to a group if not assigned to a group nor a project' do
- runner = create(:ci_runner)
+ it 'disallows assigning project to group_type runner' do
+ group_runner.projects << build(:project)
- runner.groups << build(:group)
-
- expect(runner).to be_valid
+ expect(group_runner).not_to be_valid
+ expect(group_runner.errors.full_messages).to include('Runner cannot have projects assigned')
end
- it 'allows assigning to a project if not assigned to a group nor a project' do
- runner = create(:ci_runner)
-
- runner.projects << build(:project)
+ it 'disallows assigning project to instance_type runner' do
+ instance_runner.projects << build(:project)
- expect(runner).to be_valid
+ expect(instance_runner).not_to be_valid
+ expect(instance_runner.errors.full_messages).to include('Runner cannot have projects assigned')
end
- it 'allows assigning to a project if already assigned to a project' do
- project = create(:project)
- runner = create(:ci_runner, projects: [project])
-
- runner.projects << build(:project)
+ it 'should fail to save a group assigned to a project runner even if the runner is already saved' do
+ group_runner
- expect(runner).to be_valid
+ expect { create(:group, runners: [project_runner]) }
+ .to raise_error(ActiveRecord::RecordInvalid)
end
end
end
@@ -107,20 +105,15 @@ describe Ci::Runner do
end
end
- describe '.shared' do
+ describe '.instance_type' do
let(:group) { create(:group) }
let(:project) { create(:project) }
+ let!(:group_runner) { create(:ci_runner, :group, groups: [group]) }
+ let!(:project_runner) { create(:ci_runner, :project, projects: [project]) }
+ let!(:shared_runner) { create(:ci_runner, :instance) }
- it 'returns the shared group runner' do
- runner = create(:ci_runner, :shared, groups: [group])
-
- expect(described_class.shared).to eq [runner]
- end
-
- it 'returns the shared project runner' do
- runner = create(:ci_runner, :shared, projects: [project])
-
- expect(described_class.shared).to eq [runner]
+ it 'returns only shared runners' do
+ expect(described_class.instance_type).to contain_exactly(shared_runner)
end
end
@@ -128,11 +121,11 @@ describe Ci::Runner do
it 'returns the specific project runner' do
# own
specific_project = create(:project)
- specific_runner = create(:ci_runner, :specific, projects: [specific_project])
+ specific_runner = create(:ci_runner, :project, projects: [specific_project])
# other
other_project = create(:project)
- create(:ci_runner, :specific, projects: [other_project])
+ create(:ci_runner, :project, projects: [other_project])
expect(described_class.belonging_to_project(specific_project.id)).to eq [specific_runner]
end
@@ -141,17 +134,17 @@ describe Ci::Runner do
describe '.belonging_to_parent_group_of_project' do
let(:project) { create(:project, group: group) }
let(:group) { create(:group) }
- let(:runner) { create(:ci_runner, :specific, groups: [group]) }
+ let(:runner) { create(:ci_runner, :group, groups: [group]) }
let!(:unrelated_group) { create(:group) }
let!(:unrelated_project) { create(:project, group: unrelated_group) }
- let!(:unrelated_runner) { create(:ci_runner, :specific, groups: [unrelated_group]) }
+ let!(:unrelated_runner) { create(:ci_runner, :group, groups: [unrelated_group]) }
it 'returns the specific group runner' do
expect(described_class.belonging_to_parent_group_of_project(project.id)).to contain_exactly(runner)
end
context 'with a parent group with a runner', :nested_groups do
- let(:runner) { create(:ci_runner, :specific, groups: [parent_group]) }
+ let(:runner) { create(:ci_runner, :group, groups: [parent_group]) }
let(:project) { create(:project, group: group) }
let(:group) { create(:group, parent: parent_group) }
let(:parent_group) { create(:group) }
@@ -162,20 +155,20 @@ describe Ci::Runner do
end
end
- describe '.owned_or_shared' do
+ describe '.owned_or_instance_wide' do
it 'returns a globally shared, a project specific and a group specific runner' do
# group specific
group = create(:group)
project = create(:project, group: group)
- group_runner = create(:ci_runner, :specific, groups: [group])
+ group_runner = create(:ci_runner, :group, groups: [group])
# project specific
- project_runner = create(:ci_runner, :specific, projects: [project])
+ project_runner = create(:ci_runner, :project, projects: [project])
# globally shared
- shared_runner = create(:ci_runner, :shared)
+ shared_runner = create(:ci_runner, :instance)
- expect(described_class.owned_or_shared(project.id)).to contain_exactly(
+ expect(described_class.owned_or_instance_wide(project.id)).to contain_exactly(
group_runner, project_runner, shared_runner
)
end
@@ -183,32 +176,32 @@ describe Ci::Runner do
describe '#display_name' do
it 'returns the description if it has a value' do
- runner = FactoryBot.build(:ci_runner, description: 'Linux/Ruby-1.9.3-p448')
+ runner = build(:ci_runner, description: 'Linux/Ruby-1.9.3-p448')
expect(runner.display_name).to eq 'Linux/Ruby-1.9.3-p448'
end
it 'returns the token if it does not have a description' do
- runner = FactoryBot.create(:ci_runner)
+ runner = create(:ci_runner)
expect(runner.display_name).to eq runner.description
end
it 'returns the token if the description is an empty string' do
- runner = FactoryBot.build(:ci_runner, description: '', token: 'token')
+ runner = build(:ci_runner, description: '', token: 'token')
expect(runner.display_name).to eq runner.token
end
end
describe '#assign_to' do
- let!(:project) { FactoryBot.create(:project) }
+ let(:project) { create(:project) }
subject { runner.assign_to(project) }
context 'with shared_runner' do
- let!(:runner) { FactoryBot.create(:ci_runner, :shared) }
+ let(:runner) { create(:ci_runner, :instance) }
it 'transitions shared runner to project runner and assigns project' do
- subject
- expect(runner).to be_specific
+ expect(subject).to be_truthy
+
expect(runner).to be_project_type
expect(runner.projects).to eq([project])
expect(runner.only_for?(project)).to be_truthy
@@ -216,7 +209,8 @@ describe Ci::Runner do
end
context 'with group runner' do
- let!(:runner) { FactoryBot.create(:ci_runner, runner_type: :group_type) }
+ let(:group) { create(:group) }
+ let(:runner) { create(:ci_runner, :group, groups: [group]) }
it 'raises an error' do
expect { subject }
@@ -229,15 +223,15 @@ describe Ci::Runner do
subject { described_class.online }
before do
- @runner1 = FactoryBot.create(:ci_runner, :shared, contacted_at: 1.year.ago)
- @runner2 = FactoryBot.create(:ci_runner, :shared, contacted_at: 1.second.ago)
+ @runner1 = create(:ci_runner, :instance, contacted_at: 1.year.ago)
+ @runner2 = create(:ci_runner, :instance, contacted_at: 1.second.ago)
end
it { is_expected.to eq([@runner2])}
end
describe '#online?' do
- let(:runner) { FactoryBot.create(:ci_runner, :shared) }
+ let(:runner) { create(:ci_runner, :instance) }
subject { runner.online? }
@@ -307,21 +301,20 @@ describe Ci::Runner do
end
describe '#can_pick?' do
- let(:pipeline) { create(:ci_pipeline) }
+ set(:pipeline) { create(:ci_pipeline) }
let(:build) { create(:ci_build, pipeline: pipeline) }
- let(:runner) { create(:ci_runner, tag_list: tag_list, run_untagged: run_untagged) }
+ let(:runner_project) { build.project }
+ let(:runner) { create(:ci_runner, :project, projects: [runner_project], tag_list: tag_list, run_untagged: run_untagged) }
let(:tag_list) { [] }
let(:run_untagged) { true }
subject { runner.can_pick?(build) }
- before do
- build.project.runners << runner
- end
-
context 'a different runner' do
+ let(:other_project) { create(:project) }
+ let(:other_runner) { create(:ci_runner, :project, projects: [other_project], tag_list: tag_list, run_untagged: run_untagged) }
+
it 'cannot handle builds' do
- other_runner = create(:ci_runner)
expect(other_runner.can_pick?(build)).to be_falsey
end
end
@@ -375,18 +368,14 @@ describe Ci::Runner do
end
context 'when runner is shared' do
- let(:runner) { create(:ci_runner, :shared) }
-
- before do
- build.project.runners = []
- end
+ let(:runner) { create(:ci_runner, :instance) }
it 'can handle builds' do
expect(runner.can_pick?(build)).to be_truthy
end
context 'when runner is locked' do
- let(:runner) { create(:ci_runner, :shared, locked: true) }
+ let(:runner) { create(:ci_runner, :instance, locked: true) }
it 'can handle builds' do
expect(runner.can_pick?(build)).to be_truthy
@@ -401,10 +390,8 @@ describe Ci::Runner do
end
end
- context 'when runner is not assigned to a project' do
- before do
- build.project.runners = []
- end
+ context 'when runner is assigned to another project' do
+ let(:runner_project) { create(:project) }
it 'cannot handle builds' do
expect(runner.can_pick?(build)).to be_falsey
@@ -412,10 +399,8 @@ describe Ci::Runner do
end
context 'when runner is assigned to a group' do
- before do
- build.project.runners = []
- runner.groups << create(:group, projects: [build.project])
- end
+ let(:group) { create(:group, projects: [build.project]) }
+ let(:runner) { create(:ci_runner, :group, tag_list: tag_list, run_untagged: run_untagged, groups: [group]) }
it 'can handle builds' do
expect(runner.can_pick?(build)).to be_truthy
@@ -469,7 +454,7 @@ describe Ci::Runner do
end
describe '#status' do
- let(:runner) { FactoryBot.create(:ci_runner, :shared, contacted_at: 1.second.ago) }
+ let(:runner) { create(:ci_runner, :instance, contacted_at: 1.second.ago) }
subject { runner.status }
@@ -563,7 +548,7 @@ describe Ci::Runner do
end
describe '#update_cached_info' do
- let(:runner) { create(:ci_runner) }
+ let(:runner) { create(:ci_runner, :project) }
subject { runner.update_cached_info(architecture: '18-bit') }
@@ -584,17 +569,22 @@ describe Ci::Runner do
runner.contacted_at = 2.hours.ago
end
- it 'updates database' do
- expect_redis_update
+ context 'with invalid runner' do
+ before do
+ runner.projects = []
+ end
- expect { subject }.to change { runner.reload.read_attribute(:contacted_at) }
- .and change { runner.reload.read_attribute(:architecture) }
+ it 'still updates redis cache and database' do
+ expect(runner).to be_invalid
+
+ expect_redis_update
+ does_db_update
+ end
end
- it 'updates cache' do
+ it 'updates redis cache and database' do
expect_redis_update
-
- subject
+ does_db_update
end
end
@@ -604,6 +594,11 @@ describe Ci::Runner do
expect(redis).to receive(:set).with(redis_key, anything, any_args)
end
end
+
+ def does_db_update
+ expect { subject }.to change { runner.reload.read_attribute(:contacted_at) }
+ .and change { runner.reload.read_attribute(:architecture) }
+ end
end
describe '#destroy' do
@@ -626,12 +621,13 @@ describe Ci::Runner do
end
describe '.assignable_for' do
- let!(:unlocked_project_runner) { create(:ci_runner, runner_type: :project_type, projects: [project]) }
- let!(:locked_project_runner) { create(:ci_runner, runner_type: :project_type, locked: true, projects: [project]) }
- let!(:group_runner) { create(:ci_runner, runner_type: :group_type) }
- let!(:instance_runner) { create(:ci_runner, :shared) }
let(:project) { create(:project) }
+ let(:group) { create(:group) }
let(:another_project) { create(:project) }
+ let!(:unlocked_project_runner) { create(:ci_runner, :project, projects: [project]) }
+ let!(:locked_project_runner) { create(:ci_runner, :project, locked: true, projects: [project]) }
+ let!(:group_runner) { create(:ci_runner, :group, groups: [group]) }
+ let!(:instance_runner) { create(:ci_runner, :instance) }
context 'with already assigned project' do
subject { described_class.assignable_for(project) }
@@ -651,19 +647,16 @@ describe Ci::Runner do
describe "belongs_to_one_project?" do
it "returns false if there are two projects runner assigned to" do
- runner = FactoryBot.create(:ci_runner)
- project = FactoryBot.create(:project)
- project1 = FactoryBot.create(:project)
- project.runners << runner
- project1.runners << runner
+ project1 = create(:project)
+ project2 = create(:project)
+ runner = create(:ci_runner, :project, projects: [project1, project2])
expect(runner.belongs_to_one_project?).to be_falsey
end
it "returns true" do
- runner = FactoryBot.create(:ci_runner)
- project = FactoryBot.create(:project)
- project.runners << runner
+ project = create(:project)
+ runner = create(:ci_runner, :project, projects: [project])
expect(runner.belongs_to_one_project?).to be_truthy
end
@@ -713,21 +706,21 @@ describe Ci::Runner do
subject { runner.assigned_to_group? }
context 'when project runner' do
- let(:runner) { create(:ci_runner, description: 'Project runner', projects: [project]) }
+ let(:runner) { create(:ci_runner, :project, description: 'Project runner', projects: [project]) }
let(:project) { create(:project) }
it { is_expected.to be_falsey }
end
context 'when shared runner' do
- let(:runner) { create(:ci_runner, :shared, description: 'Shared runner') }
+ let(:runner) { create(:ci_runner, :instance, description: 'Shared runner') }
it { is_expected.to be_falsey }
end
context 'when group runner' do
let(:group) { create(:group) }
- let(:runner) { create(:ci_runner, description: 'Group runner', groups: [group]) }
+ let(:runner) { create(:ci_runner, :group, description: 'Group runner', groups: [group]) }
it { is_expected.to be_truthy }
end
@@ -737,18 +730,18 @@ describe Ci::Runner do
subject { runner.assigned_to_project? }
context 'when group runner' do
- let(:runner) { create(:ci_runner, description: 'Group runner', groups: [group]) }
+ let(:runner) { create(:ci_runner, :group, description: 'Group runner', groups: [group]) }
let(:group) { create(:group) }
it { is_expected.to be_falsey }
end
context 'when shared runner' do
- let(:runner) { create(:ci_runner, :shared, description: 'Shared runner') }
+ let(:runner) { create(:ci_runner, :instance, description: 'Shared runner') }
it { is_expected.to be_falsey }
end
context 'when project runner' do
- let(:runner) { create(:ci_runner, description: 'Group runner', projects: [project]) }
+ let(:runner) { create(:ci_runner, :project, description: 'Project runner', projects: [project]) }
let(:project) { create(:project) }
it { is_expected.to be_truthy }
@@ -780,4 +773,17 @@ describe Ci::Runner do
end
end
end
+
+ describe 'project runner without projects is destroyable' do
+ subject { create(:ci_runner, :project, :without_projects) }
+
+ it 'does not have projects' do
+ expect(subject.runner_projects).to be_empty
+ end
+
+ it 'can be destroyed' do
+ subject
+ expect { subject.destroy }.to change { described_class.count }.by(-1)
+ end
+ end
end
diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb
index a00db1d2bfc..22a4556c10c 100644
--- a/spec/models/ci/stage_spec.rb
+++ b/spec/models/ci/stage_spec.rb
@@ -65,7 +65,31 @@ describe Ci::Stage, :models do
end
end
- context 'when stage is skipped' do
+ context 'when stage has only created builds' do
+ let(:stage) { create(:ci_stage_entity, status: :created) }
+
+ before do
+ create(:ci_build, :created, stage_id: stage.id)
+ end
+
+ it 'updates status to skipped' do
+ expect(stage.reload.status).to eq 'created'
+ end
+ end
+
+ context 'when stage is skipped because of skipped builds' do
+ before do
+ create(:ci_build, :skipped, stage_id: stage.id)
+ end
+
+ it 'updates status to skipped' do
+ expect { stage.update_status }
+ .to change { stage.reload.status }
+ .to 'skipped'
+ end
+ end
+
+ context 'when stage is skipped because is empty' do
it 'updates status to skipped' do
expect { stage.update_status }
.to change { stage.reload.status }
@@ -86,9 +110,85 @@ describe Ci::Stage, :models do
expect(stage.reload).to be_failed
end
end
+
+ context 'when statuses status was not recognized' do
+ before do
+ allow(stage)
+ .to receive_message_chain(:statuses, :latest, :status)
+ .and_return(:unknown)
+ end
+
+ it 'raises an exception' do
+ expect { stage.update_status }
+ .to raise_error(HasStatus::UnknownStatusError)
+ end
+ end
+ end
+
+ describe '#detailed_status' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:user) { create(:user) }
+ let(:stage) { create(:ci_stage_entity, status: :created) }
+ subject { stage.detailed_status(user) }
+
+ where(:statuses, :label) do
+ %w[created] | :created
+ %w[success] | :passed
+ %w[pending] | :pending
+ %w[skipped] | :skipped
+ %w[canceled] | :canceled
+ %w[success failed] | :failed
+ %w[running pending] | :running
+ end
+
+ with_them do
+ before do
+ statuses.each do |status|
+ create(:commit_status, project: stage.project,
+ pipeline: stage.pipeline,
+ stage_id: stage.id,
+ status: status)
+
+ stage.update_status
+ end
+ end
+
+ it 'has a correct label' do
+ expect(subject.label).to eq label.to_s
+ end
+ end
+
+ context 'when stage has warnings' do
+ before do
+ create(:ci_build, project: stage.project,
+ pipeline: stage.pipeline,
+ stage_id: stage.id,
+ status: :failed,
+ allow_failure: true)
+
+ stage.update_status
+ end
+
+ it 'is passed with warnings' do
+ expect(subject.label).to eq 'passed with warnings'
+ end
+ end
end
- describe '#index' do
+ describe '#groups' do
+ before do
+ create(:ci_build, stage_id: stage.id, name: 'rspec 0 1')
+ create(:ci_build, stage_id: stage.id, name: 'rspec 0 2')
+ end
+
+ it 'groups stage builds by name' do
+ expect(stage.groups).to be_one
+ expect(stage.groups.first.name).to eq 'rspec'
+ end
+ end
+
+ describe '#position' do
context 'when stage has been imported and does not have position index set' do
before do
stage.update_column(:position, nil)
@@ -119,4 +219,42 @@ describe Ci::Stage, :models do
end
end
end
+
+ context 'when stage has warnings' do
+ before do
+ create(:ci_build, :failed, :allowed_to_fail, stage_id: stage.id)
+ end
+
+ describe '#has_warnings?' do
+ it 'returns true' do
+ expect(stage).to have_warnings
+ end
+ end
+
+ describe '#number_of_warnings' do
+ it 'returns a lazy stage warnings counter' do
+ lazy_queries = ActiveRecord::QueryRecorder.new do
+ stage.number_of_warnings
+ end
+
+ synced_queries = ActiveRecord::QueryRecorder.new do
+ stage.number_of_warnings.to_i
+ end
+
+ expect(lazy_queries.count).to eq 0
+ expect(synced_queries.count).to eq 1
+
+ expect(stage.number_of_warnings.inspect).to include 'BatchLoader'
+ expect(stage.number_of_warnings).to eq 1
+ end
+ end
+ end
+
+ context 'when stage does not have warnings' do
+ describe '#has_warnings?' do
+ it 'returns false' do
+ expect(stage).not_to have_warnings
+ end
+ end
+ end
end
diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb
index a47a07d908d..bb5b2ef3a47 100644
--- a/spec/models/clusters/applications/ingress_spec.rb
+++ b/spec/models/clusters/applications/ingress_spec.rb
@@ -73,6 +73,7 @@ describe Clusters::Applications::Ingress do
it 'should be initialized with ingress arguments' do
expect(subject.name).to eq('ingress')
expect(subject.chart).to eq('stable/nginx-ingress')
+ expect(subject.version).to be_nil
expect(subject.values).to eq(ingress.values)
end
end
diff --git a/spec/models/clusters/applications/jupyter_spec.rb b/spec/models/clusters/applications/jupyter_spec.rb
new file mode 100644
index 00000000000..65750141e65
--- /dev/null
+++ b/spec/models/clusters/applications/jupyter_spec.rb
@@ -0,0 +1,60 @@
+require 'rails_helper'
+
+describe Clusters::Applications::Jupyter do
+ include_examples 'cluster application core specs', :clusters_applications_jupyter
+
+ it { is_expected.to belong_to(:oauth_application) }
+
+ describe '#set_initial_status' do
+ before do
+ jupyter.set_initial_status
+ end
+
+ context 'when ingress is not installed' do
+ let(:cluster) { create(:cluster, :provided_by_gcp) }
+ let(:jupyter) { create(:clusters_applications_jupyter, cluster: cluster) }
+
+ it { expect(jupyter).to be_not_installable }
+ end
+
+ context 'when ingress is installed and external_ip is assigned' do
+ let(:ingress) { create(:clusters_applications_ingress, :installed, external_ip: '127.0.0.1') }
+ let(:jupyter) { create(:clusters_applications_jupyter, cluster: ingress.cluster) }
+
+ it { expect(jupyter).to be_installable }
+ end
+ end
+
+ describe '#install_command' do
+ let!(:ingress) { create(:clusters_applications_ingress, :installed, external_ip: '127.0.0.1') }
+ let!(:jupyter) { create(:clusters_applications_jupyter, cluster: ingress.cluster) }
+
+ subject { jupyter.install_command }
+
+ it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) }
+
+ it 'should be initialized with 4 arguments' do
+ expect(subject.name).to eq('jupyter')
+ expect(subject.chart).to eq('jupyter/jupyterhub')
+ expect(subject.version).to be_nil
+ expect(subject.repository).to eq('https://jupyterhub.github.io/helm-chart/')
+ expect(subject.values).to eq(jupyter.values)
+ end
+ end
+
+ describe '#values' do
+ let(:jupyter) { create(:clusters_applications_jupyter) }
+
+ subject { jupyter.values }
+
+ it 'should include valid values' do
+ is_expected.to include('ingress')
+ is_expected.to include('hub')
+ is_expected.to include('rbac')
+ is_expected.to include('proxy')
+ is_expected.to include('auth')
+ is_expected.to include("clientId: #{jupyter.oauth_application.uid}")
+ is_expected.to include("callbackUrl: #{jupyter.callback_url}")
+ end
+ end
+end
diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb
index d2302583ac8..efd57040005 100644
--- a/spec/models/clusters/applications/prometheus_spec.rb
+++ b/spec/models/clusters/applications/prometheus_spec.rb
@@ -109,6 +109,7 @@ describe Clusters::Applications::Prometheus do
it 'should be initialized with 3 arguments' do
expect(subject.name).to eq('prometheus')
expect(subject.chart).to eq('stable/prometheus')
+ expect(subject.version).to eq('6.7.3')
expect(subject.values).to eq(prometheus.values)
end
end
diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb
index 3ef59457c5f..b12500d0acd 100644
--- a/spec/models/clusters/applications/runner_spec.rb
+++ b/spec/models/clusters/applications/runner_spec.rb
@@ -31,6 +31,7 @@ describe Clusters::Applications::Runner do
it 'should be initialized with 4 arguments' do
expect(subject.name).to eq('runner')
expect(subject.chart).to eq('runner/gitlab-runner')
+ expect(subject.version).to be_nil
expect(subject.repository).to eq('https://charts.gitlab.io')
expect(subject.values).to eq(gitlab_runner.values)
end
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index b942554d67b..6f66515b45f 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -234,9 +234,10 @@ describe Clusters::Cluster do
let!(:ingress) { create(:clusters_applications_ingress, cluster: cluster) }
let!(:prometheus) { create(:clusters_applications_prometheus, cluster: cluster) }
let!(:runner) { create(:clusters_applications_runner, cluster: cluster) }
+ let!(:jupyter) { create(:clusters_applications_jupyter, cluster: cluster) }
it 'returns a list of created applications' do
- is_expected.to contain_exactly(helm, ingress, prometheus, runner)
+ is_expected.to contain_exactly(helm, ingress, prometheus, runner, jupyter)
end
end
end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 090f91168ad..5157d8fc645 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -514,30 +514,21 @@ eos
end
describe '#uri_type' do
- shared_examples 'URI type' do
- it 'returns the URI type at the given path' do
- expect(commit.uri_type('files/html')).to be(:tree)
- expect(commit.uri_type('files/images/logo-black.png')).to be(:raw)
- expect(project.commit('video').uri_type('files/videos/intro.mp4')).to be(:raw)
- expect(commit.uri_type('files/js/application.js')).to be(:blob)
- end
-
- it "returns nil if the path doesn't exists" do
- expect(commit.uri_type('this/path/doesnt/exist')).to be_nil
- end
-
- it 'is nil if the path is nil or empty' do
- expect(commit.uri_type(nil)).to be_nil
- expect(commit.uri_type("")).to be_nil
- end
+ it 'returns the URI type at the given path' do
+ expect(commit.uri_type('files/html')).to be(:tree)
+ expect(commit.uri_type('files/images/logo-black.png')).to be(:raw)
+ expect(project.commit('video').uri_type('files/videos/intro.mp4')).to be(:raw)
+ expect(commit.uri_type('files/js/application.js')).to be(:blob)
end
- context 'when Gitaly commit_tree_entry feature is enabled' do
- it_behaves_like 'URI type'
+ it "returns nil if the path doesn't exists" do
+ expect(commit.uri_type('this/path/doesnt/exist')).to be_nil
+ expect(commit.uri_type('../path/doesnt/exist')).to be_nil
end
- context 'when Gitaly commit_tree_entry feature is disabled', :disable_gitaly do
- it_behaves_like 'URI type'
+ it 'is nil if the path is nil or empty' do
+ expect(commit.uri_type(nil)).to be_nil
+ expect(commit.uri_type("")).to be_nil
end
end
diff --git a/spec/models/concerns/batch_destroy_dependent_associations_spec.rb b/spec/models/concerns/batch_destroy_dependent_associations_spec.rb
new file mode 100644
index 00000000000..e5392fe0462
--- /dev/null
+++ b/spec/models/concerns/batch_destroy_dependent_associations_spec.rb
@@ -0,0 +1,60 @@
+require 'spec_helper'
+
+describe BatchDestroyDependentAssociations do
+ class TestProject < ActiveRecord::Base
+ self.table_name = 'projects'
+
+ has_many :builds, dependent: :destroy
+ has_many :notification_settings, as: :source, dependent: :delete_all
+ has_many :pages_domains
+ has_many :todos
+
+ include BatchDestroyDependentAssociations
+ end
+
+ describe '#dependent_associations_to_destroy' do
+ set(:project) { TestProject.new }
+
+ it 'returns the right associations' do
+ expect(project.dependent_associations_to_destroy.map(&:name)).to match_array([:builds])
+ end
+ end
+
+ describe '#destroy_dependent_associations_in_batches' do
+ set(:project) { create(:project) }
+ set(:build) { create(:ci_build, project: project) }
+ set(:notification_setting) { create(:notification_setting, project: project) }
+ let!(:todos) { create(:todo, project: project) }
+
+ it 'destroys multiple builds' do
+ create(:ci_build, project: project)
+
+ expect(Ci::Build.count).to eq(2)
+
+ project.destroy_dependent_associations_in_batches
+
+ expect(Ci::Build.count).to eq(0)
+ end
+
+ it 'destroys builds in batches' do
+ expect(project).to receive_message_chain(:builds, :find_each).and_yield(build)
+ expect(build).to receive(:destroy).and_call_original
+
+ project.destroy_dependent_associations_in_batches
+
+ expect(Ci::Build.count).to eq(0)
+ expect(Todo.count).to eq(1)
+ expect(User.count).to be > 0
+ expect(NotificationSetting.count).to eq(User.count)
+ end
+
+ it 'excludes associations' do
+ project.destroy_dependent_associations_in_batches(exclude: [:builds])
+
+ expect(Ci::Build.count).to eq(1)
+ expect(Todo.count).to eq(1)
+ expect(User.count).to be > 0
+ expect(NotificationSetting.count).to eq(User.count)
+ end
+ end
+end
diff --git a/spec/models/concerns/cache_markdown_field_spec.rb b/spec/models/concerns/cache_markdown_field_spec.rb
index b3797c1fb46..da26d802688 100644
--- a/spec/models/concerns/cache_markdown_field_spec.rb
+++ b/spec/models/concerns/cache_markdown_field_spec.rb
@@ -156,7 +156,7 @@ describe CacheMarkdownField do
end
it { expect(thing.foo_html).to eq(updated_html) }
- it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_REDCARPET_VERSION) }
+ it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_COMMONMARK_VERSION) }
end
describe '#cached_html_up_to_date?' do
@@ -234,7 +234,7 @@ describe CacheMarkdownField do
it 'returns default version when version is nil' do
thing.cached_markdown_version = nil
- is_expected.to eq(CacheMarkdownField::CACHE_REDCARPET_VERSION)
+ is_expected.to eq(CacheMarkdownField::CACHE_COMMONMARK_VERSION)
end
end
@@ -261,7 +261,7 @@ describe CacheMarkdownField do
thing.cached_markdown_version = nil
thing.refresh_markdown_cache
- expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_REDCARPET_VERSION)
+ expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_COMMONMARK_VERSION)
end
end
@@ -346,7 +346,7 @@ describe CacheMarkdownField do
expect(thing.foo_html).to eq(updated_html)
expect(thing.baz_html).to eq(updated_html)
- expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_REDCARPET_VERSION)
+ expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_COMMONMARK_VERSION)
end
end
@@ -366,8 +366,24 @@ describe CacheMarkdownField do
expect(thing.foo_html).to eq(updated_html)
expect(thing.baz_html).to eq(updated_html)
- expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_REDCARPET_VERSION)
+ expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_COMMONMARK_VERSION)
end
end
end
+
+ describe CacheMarkdownField::MarkdownEngine do
+ subject { lambda { |version| CacheMarkdownField::MarkdownEngine.from_version(version) } }
+
+ it 'returns :common_mark as a default' do
+ expect(subject.call(nil)).to eq :common_mark
+ end
+
+ it 'returns :common_mark' do
+ expect(subject.call(CacheMarkdownField::CACHE_COMMONMARK_VERSION)).to eq :common_mark
+ end
+
+ it 'returns :redcarpet' do
+ expect(subject.call(CacheMarkdownField::CACHE_REDCARPET_VERSION)).to eq :redcarpet
+ end
+ end
end
diff --git a/spec/models/concerns/cacheable_attributes_spec.rb b/spec/models/concerns/cacheable_attributes_spec.rb
index 49e4b23ebc7..f8c2e29fadd 100644
--- a/spec/models/concerns/cacheable_attributes_spec.rb
+++ b/spec/models/concerns/cacheable_attributes_spec.rb
@@ -22,7 +22,7 @@ describe CacheableAttributes do
attr_accessor :attributes
- def initialize(attrs = {})
+ def initialize(attrs = {}, *)
@attributes = attrs
end
end
@@ -52,7 +52,7 @@ describe CacheableAttributes do
describe '.cache_key' do
it 'excludes cache attributes' do
- expect(minimal_test_class.cache_key).to eq("TestClass:#{Gitlab::VERSION}:#{Gitlab.migrations_hash}:json")
+ expect(minimal_test_class.cache_key).to eq("TestClass:#{Gitlab::VERSION}:#{Rails.version}")
end
end
@@ -75,49 +75,117 @@ describe CacheableAttributes do
context 'without any attributes given' do
it 'intializes a new object with the defaults' do
- expect(minimal_test_class.build_from_defaults).not_to be_persisted
+ expect(minimal_test_class.build_from_defaults.attributes).to eq(minimal_test_class.defaults)
end
end
- context 'without attributes given' do
+ context 'with attributes given' do
it 'intializes a new object with the given attributes merged into the defaults' do
expect(minimal_test_class.build_from_defaults(foo: 'd').attributes[:foo]).to eq('d')
end
end
+
+ describe 'edge cases on concrete implementations' do
+ describe '.build_from_defaults' do
+ context 'without any attributes given' do
+ it 'intializes all attributes even if they are nil' do
+ record = ApplicationSetting.build_from_defaults
+
+ expect(record).not_to be_persisted
+ expect(record.sign_in_text).to be_nil
+ end
+ end
+ end
+ end
end
describe '.current', :use_clean_rails_memory_store_caching do
context 'redis unavailable' do
- it 'returns an uncached record' do
+ before do
allow(minimal_test_class).to receive(:last).and_return(:last)
- expect(Rails.cache).to receive(:read).and_raise(Redis::BaseError)
+ expect(Rails.cache).to receive(:read).with(minimal_test_class.cache_key).and_raise(Redis::BaseError)
+ end
+
+ context 'in production environment' do
+ before do
+ expect(Rails.env).to receive(:production?).and_return(true)
+ end
+
+ it 'returns an uncached record and logs a warning' do
+ expect(Rails.logger).to receive(:warn).with("Cached record for TestClass couldn't be loaded, falling back to uncached record: Redis::BaseError")
- expect(minimal_test_class.current).to eq(:last)
+ expect(minimal_test_class.current).to eq(:last)
+ end
+ end
+
+ context 'in other environments' do
+ before do
+ expect(Rails.env).to receive(:production?).and_return(false)
+ end
+
+ it 'returns an uncached record and logs a warning' do
+ expect(Rails.logger).not_to receive(:warn)
+
+ expect { minimal_test_class.current }.to raise_error(Redis::BaseError)
+ end
end
end
context 'when a record is not yet present' do
it 'does not cache nil object' do
# when missing settings a nil object is returned, but not cached
- allow(minimal_test_class).to receive(:last).twice.and_return(nil)
+ allow(ApplicationSetting).to receive(:current_without_cache).twice.and_return(nil)
- expect(minimal_test_class.current).to be_nil
- expect(Rails.cache.exist?(minimal_test_class.cache_key)).to be(false)
+ expect(ApplicationSetting.current).to be_nil
+ expect(Rails.cache.exist?(ApplicationSetting.cache_key)).to be(false)
end
- it 'cache non-nil object' do
- # when the settings are set the method returns a valid object
- allow(minimal_test_class).to receive(:last).and_call_original
+ it 'caches non-nil object' do
+ create(:application_setting)
- expect(minimal_test_class.current).to eq(minimal_test_class.last)
- expect(Rails.cache.exist?(minimal_test_class.cache_key)).to be(true)
+ expect(ApplicationSetting.current).to eq(ApplicationSetting.last)
+ expect(Rails.cache.exist?(ApplicationSetting.cache_key)).to be(true)
# subsequent calls retrieve the record from the cache
- last_record = minimal_test_class.last
- expect(minimal_test_class).not_to receive(:last)
- expect(minimal_test_class.current.attributes).to eq(last_record.attributes)
+ last_record = ApplicationSetting.last
+ expect(ApplicationSetting).not_to receive(:current_without_cache)
+ expect(ApplicationSetting.current.attributes).to eq(last_record.attributes)
end
end
+
+ describe 'edge cases' do
+ describe 'caching behavior', :use_clean_rails_memory_store_caching do
+ it 'retrieves upload fields properly' do
+ ar_record = create(:appearance, :with_logo)
+ ar_record.cache!
+
+ cache_record = Appearance.current
+
+ expect(cache_record).to be_persisted
+ expect(cache_record.logo).to be_an(AttachmentUploader)
+ expect(cache_record.logo.url).to end_with('/dk.png')
+ end
+
+ it 'retrieves markdown fields properly' do
+ ar_record = create(:appearance, description: '**Hello**')
+ ar_record.cache!
+
+ cache_record = Appearance.current
+
+ expect(cache_record.description).to eq('**Hello**')
+ expect(cache_record.description_html).to eq('<p dir="auto"><strong>Hello</strong></p>')
+ end
+ end
+ end
+
+ it 'uses RequestStore in addition to Rails.cache', :request_store do
+ # Warm up the cache
+ create(:application_setting).cache!
+
+ expect(Rails.cache).to receive(:read).with(ApplicationSetting.cache_key).once.and_call_original
+
+ 2.times { ApplicationSetting.current }
+ end
end
describe '.cached', :use_clean_rails_memory_store_caching do
@@ -127,27 +195,36 @@ describe CacheableAttributes do
end
end
- context 'when cached settings do not include the latest defaults' do
+ context 'when cached is warm' do
before do
- Rails.cache.write(minimal_test_class.cache_key, { bar: 'b', baz: 'c' }.to_json)
- minimal_test_class.define_singleton_method(:defaults) do
- { foo: 'a', bar: 'b', baz: 'c' }
- end
+ # Warm up the cache
+ create(:appearance).cache!
end
- it 'includes attributes from defaults' do
- expect(minimal_test_class.cached.attributes[:foo]).to eq(minimal_test_class.defaults[:foo])
+ it 'retrieves the record from cache' do
+ expect(ActiveRecord::QueryRecorder.new { Appearance.cached }.count).to eq(0)
+ expect(Appearance.cached).to eq(Appearance.current_without_cache)
end
end
end
describe '#cache!', :use_clean_rails_memory_store_caching do
- let(:appearance_record) { create(:appearance) }
+ let(:record) { create(:appearance) }
it 'caches the attributes' do
- appearance_record.cache!
+ record.cache!
- expect(Rails.cache.read(Appearance.cache_key)).to eq(appearance_record.attributes.to_json)
+ expect(Rails.cache.read(Appearance.cache_key)).to eq(record)
+ end
+
+ describe 'edge cases' do
+ let(:record) { create(:appearance) }
+
+ it 'caches the attributes' do
+ record.cache!
+
+ expect(Rails.cache.read(Appearance.cache_key)).to eq(record)
+ end
end
end
end
diff --git a/spec/models/concerns/discussion_on_diff_spec.rb b/spec/models/concerns/discussion_on_diff_spec.rb
index 30572ce9332..8cd129dc851 100644
--- a/spec/models/concerns/discussion_on_diff_spec.rb
+++ b/spec/models/concerns/discussion_on_diff_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe DiscussionOnDiff do
- subject { create(:diff_note_on_merge_request).to_discussion }
+ subject { create(:diff_note_on_merge_request, line_number: 18).to_discussion }
describe "#truncated_diff_lines" do
let(:truncated_lines) { subject.truncated_diff_lines }
diff --git a/spec/models/concerns/has_variable_spec.rb b/spec/models/concerns/has_variable_spec.rb
index f87869a2fdc..3fbe86c5b56 100644
--- a/spec/models/concerns/has_variable_spec.rb
+++ b/spec/models/concerns/has_variable_spec.rb
@@ -45,8 +45,10 @@ describe HasVariable do
end
it 'fails to decrypt if iv is incorrect' do
- subject.encrypted_value_iv = SecureRandom.hex
+ # attr_encrypted expects the IV to be 16 bytes and base64-encoded
+ subject.encrypted_value_iv = [SecureRandom.hex(8)].pack('m')
subject.instance_variable_set(:@value, nil)
+
expect { subject.value }
.to raise_error(OpenSSL::Cipher::CipherError, 'bad decrypt')
end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index bd6bf5b0712..ec6374f3963 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -12,6 +12,7 @@ describe Issuable do
it { is_expected.to belong_to(:author) }
it { is_expected.to have_many(:notes).dependent(:destroy) }
it { is_expected.to have_many(:todos).dependent(:destroy) }
+ it { is_expected.to have_many(:labels) }
context 'Notes' do
let!(:note) { create(:note, noteable: issue, project: issue.project) }
@@ -274,8 +275,8 @@ describe Issuable do
it 'skips coercion for not Integer values' do
expect { issue.time_estimate = nil }.to change { issue.time_estimate }.to(nil)
- expect { issue.time_estimate = 'invalid time' }.not_to raise_error(StandardError)
- expect { issue.time_estimate = 22.33 }.not_to raise_error(StandardError)
+ expect { issue.time_estimate = 'invalid time' }.not_to raise_error
+ expect { issue.time_estimate = 22.33 }.not_to raise_error
end
end
@@ -548,7 +549,7 @@ describe Issuable do
let(:project) { create(:project, namespace: group) }
let(:other_project) { create(:project) }
let(:owner) { create(:owner) }
- let(:master) { create(:user) }
+ let(:maintainer) { create(:user) }
let(:reporter) { create(:user) }
let(:guest) { create(:user) }
@@ -557,7 +558,7 @@ describe Issuable do
before do
group.add_owner(owner)
- project.add_master(master)
+ project.add_maintainer(maintainer)
project.add_reporter(reporter)
project.add_guest(guest)
project.add_guest(contributor)
@@ -569,8 +570,8 @@ describe Issuable do
let(:merged_mr_other_project) { create(:merge_request, :merged, author: first_time_contributor, target_project: other_project, source_project: other_project) }
context "for merge requests" do
- it "is false for MASTER" do
- mr = create(:merge_request, author: master, target_project: project, source_project: project)
+ it "is false for MAINTAINER" do
+ mr = create(:merge_request, author: maintainer, target_project: project, source_project: project)
expect(mr).not_to be_first_contribution
end
diff --git a/spec/models/concerns/mentionable_spec.rb b/spec/models/concerns/mentionable_spec.rb
index c73ea6aa94c..a9b237fa9ea 100644
--- a/spec/models/concerns/mentionable_spec.rb
+++ b/spec/models/concerns/mentionable_spec.rb
@@ -136,7 +136,7 @@ describe Issue, "Mentionable" do
expect(SystemNoteService).not_to receive(:cross_reference)
- issue.update_attributes(description: 'New description')
+ issue.update(description: 'New description')
issue.create_new_cross_references!
end
@@ -145,7 +145,7 @@ describe Issue, "Mentionable" do
expect(SystemNoteService).to receive(:cross_reference).with(issues[1], any_args)
- issue.update_attributes(description: issues[1].to_reference)
+ issue.update(description: issues[1].to_reference)
issue.create_new_cross_references!
end
@@ -155,7 +155,7 @@ describe Issue, "Mentionable" do
expect(SystemNoteService).to receive(:cross_reference).with(issues[1], any_args)
- note.update_attributes(note: issues[1].to_reference)
+ note.update(note: issues[1].to_reference)
note.create_new_cross_references!
end
end
diff --git a/spec/models/concerns/protected_ref_access_spec.rb b/spec/models/concerns/protected_ref_access_spec.rb
index a62ca391e25..ce602337647 100644
--- a/spec/models/concerns/protected_ref_access_spec.rb
+++ b/spec/models/concerns/protected_ref_access_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe ProtectedRefAccess do
subject(:protected_ref_access) do
- create(:protected_branch, :masters_can_push).push_access_levels.first
+ create(:protected_branch, :maintainers_can_push).push_access_levels.first
end
let(:project) { protected_ref_access.project }
@@ -14,11 +14,11 @@ describe ProtectedRefAccess do
expect(protected_ref_access.check_access(admin)).to be_truthy
end
- it 'is true for masters' do
- master = create(:user)
- project.add_master(master)
+ it 'is true for maintainers' do
+ maintainer = create(:user)
+ project.add_maintainer(maintainer)
- expect(protected_ref_access.check_access(master)).to be_truthy
+ expect(protected_ref_access.check_access(maintainer)).to be_truthy
end
it 'is for developers of the project' do
diff --git a/spec/models/concerns/reactive_caching_spec.rb b/spec/models/concerns/reactive_caching_spec.rb
index 4570dbb1d8e..0f156619e9e 100644
--- a/spec/models/concerns/reactive_caching_spec.rb
+++ b/spec/models/concerns/reactive_caching_spec.rb
@@ -1,6 +1,7 @@
require 'spec_helper'
describe ReactiveCaching, :use_clean_rails_memory_store_caching do
+ include ExclusiveLeaseHelpers
include ReactiveCachingHelpers
class CacheTest
@@ -94,6 +95,7 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
end
it { expect(instance.result).to be_nil }
+ it { expect(reactive_cache_alive?(instance)).to be_falsy }
end
describe '#exclusively_update_reactive_cache!' do
@@ -105,8 +107,8 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
end
it 'takes and releases the lease' do
- expect_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return("000000")
- expect(Gitlab::ExclusiveLease).to receive(:cancel).with(cache_key, "000000")
+ expect_to_obtain_exclusive_lease(cache_key, 'uuid')
+ expect_to_cancel_exclusive_lease(cache_key, 'uuid')
go!
end
@@ -152,11 +154,9 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
end
context 'when the lease is already taken' do
- before do
- expect_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(nil)
- end
-
it 'skips the calculation' do
+ stub_exclusive_lease_taken(cache_key)
+
expect(instance).to receive(:calculate_reactive_cache).never
go!
diff --git a/spec/models/concerns/resolvable_discussion_spec.rb b/spec/models/concerns/resolvable_discussion_spec.rb
index 2a2ef5a304d..97b046b0f21 100644
--- a/spec/models/concerns/resolvable_discussion_spec.rb
+++ b/spec/models/concerns/resolvable_discussion_spec.rb
@@ -190,7 +190,7 @@ describe Discussion, ResolvableDiscussion do
context "when the signed in user can push to the project" do
before do
- subject.project.add_master(current_user)
+ subject.project.add_maintainer(current_user)
end
it "returns true" do
@@ -534,11 +534,18 @@ describe Discussion, ResolvableDiscussion do
describe "#last_resolved_note" do
let(:current_user) { create(:user) }
+ let(:time) { Time.now.utc }
before do
- first_note.resolve!(current_user)
- third_note.resolve!(current_user)
- second_note.resolve!(current_user)
+ Timecop.freeze(time - 1.second) do
+ first_note.resolve!(current_user)
+ end
+ Timecop.freeze(time) do
+ third_note.resolve!(current_user)
+ end
+ Timecop.freeze(time + 1.second) do
+ second_note.resolve!(current_user)
+ end
end
it "returns the last note that was resolved" do
diff --git a/spec/models/concerns/resolvable_note_spec.rb b/spec/models/concerns/resolvable_note_spec.rb
index 91591017587..fcb5250278e 100644
--- a/spec/models/concerns/resolvable_note_spec.rb
+++ b/spec/models/concerns/resolvable_note_spec.rb
@@ -326,4 +326,12 @@ describe Note, ResolvableNote do
end
end
end
+
+ describe "#potentially_resolvable?" do
+ it 'returns false if noteable could not be found' do
+ allow(subject).to receive(:noteable).and_return(nil)
+
+ expect(subject.potentially_resolvable?).to be_falsey
+ end
+ end
end
diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb
index 8cb50d7465c..ed3e28fbeca 100644
--- a/spec/models/concerns/routable_spec.rb
+++ b/spec/models/concerns/routable_spec.rb
@@ -29,7 +29,7 @@ describe Group, 'Routable' do
end
it 'updates route record on path change' do
- group.update_attributes(path: 'wow', name: 'much')
+ group.update(path: 'wow', name: 'much')
expect(group.route.path).to eq('wow')
expect(group.route.name).to eq('much')
diff --git a/spec/models/concerns/sortable_spec.rb b/spec/models/concerns/sortable_spec.rb
index b821a84d5e0..39c16ae60af 100644
--- a/spec/models/concerns/sortable_spec.rb
+++ b/spec/models/concerns/sortable_spec.rb
@@ -40,15 +40,25 @@ describe Sortable do
describe 'ordering by name' do
it 'ascending' do
- expect(relation).to receive(:reorder).with("lower(name) asc")
+ expect(relation).to receive(:reorder).once.and_call_original
- relation.order_by('name_asc')
+ table = Regexp.escape(ActiveRecord::Base.connection.quote_table_name(:namespaces))
+ column = Regexp.escape(ActiveRecord::Base.connection.quote_column_name(:name))
+
+ sql = relation.order_by('name_asc').to_sql
+
+ expect(sql).to match /.+ORDER BY LOWER\(#{table}.#{column}\) ASC\z/
end
it 'descending' do
- expect(relation).to receive(:reorder).with("lower(name) desc")
+ expect(relation).to receive(:reorder).once.and_call_original
+
+ table = Regexp.escape(ActiveRecord::Base.connection.quote_table_name(:namespaces))
+ column = Regexp.escape(ActiveRecord::Base.connection.quote_column_name(:name))
+
+ sql = relation.order_by('name_desc').to_sql
- relation.order_by('name_desc')
+ expect(sql).to match /.+ORDER BY LOWER\(#{table}.#{column}\) DESC\z/
end
end
diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb
index dfb83578fce..9b804429138 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) { :rss_token }
+ let(:token_field) { :feed_token }
it_behaves_like 'TokenAuthenticatable'
describe 'ensures authentication token' do
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index aee70bcfb29..e01906f4b6c 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -20,6 +20,7 @@ describe Deployment do
it_behaves_like 'AtomicInternalId' do
let(:internal_id_attribute) { :iid }
let(:instance) { build(:deployment) }
+ let(:scope) { :project }
let(:scope_attrs) { { project: instance.project } }
let(:usage) { :deployments }
end
diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb
index fb51c0172ab..8624f0daa4d 100644
--- a/spec/models/diff_note_spec.rb
+++ b/spec/models/diff_note_spec.rb
@@ -84,7 +84,47 @@ describe DiffNote do
end
end
- describe "#diff_file" do
+ describe '#create_diff_file callback' do
+ let(:noteable) { create(:merge_request) }
+ let(:project) { noteable.project }
+
+ context 'merge request' do
+ let!(:diff_note) { create(:diff_note_on_merge_request, project: project, noteable: noteable) }
+
+ it 'creates a diff note file' do
+ expect(diff_note.reload.note_diff_file).to be_present
+ end
+
+ it 'does not create diff note file if it is a reply' do
+ expect { create(:diff_note_on_merge_request, noteable: noteable, in_reply_to: diff_note) }
+ .not_to change(NoteDiffFile, :count)
+ end
+ end
+
+ context 'commit' do
+ let!(:diff_note) { create(:diff_note_on_commit, project: project) }
+
+ it 'creates a diff note file' do
+ expect(diff_note.reload.note_diff_file).to be_present
+ end
+
+ it 'does not create diff note file if it is a reply' do
+ expect { create(:diff_note_on_commit, in_reply_to: diff_note) }
+ .not_to change(NoteDiffFile, :count)
+ end
+ end
+ end
+
+ describe '#diff_file', :clean_gitlab_redis_shared_state do
+ context 'when note_diff_file association exists' do
+ it 'returns persisted diff file data' do
+ diff_file = subject.diff_file
+
+ expect(diff_file.diff.to_hash.with_indifferent_access)
+ .to include(subject.note_diff_file.to_hash)
+ end
+ end
+
context 'when the discussion was created in the diff' do
it 'returns correct diff file' do
diff_file = subject.diff_file
@@ -115,6 +155,26 @@ describe DiffNote do
expect(diff_file.diff_refs).to eq(position.diff_refs)
end
end
+
+ context 'note diff file creation enqueuing' do
+ it 'enqueues CreateNoteDiffFileWorker if it is the first note of a discussion' do
+ subject.note_diff_file.destroy!
+
+ expect(CreateNoteDiffFileWorker).to receive(:perform_async).with(subject.id)
+
+ subject.reload.diff_file
+ end
+
+ it 'does not enqueues CreateNoteDiffFileWorker if not first note of a discussion' do
+ mr = create(:merge_request)
+ diff_note = create(:diff_note_on_merge_request, project: mr.project, noteable: mr)
+ reply_diff_note = create(:diff_note_on_merge_request, in_reply_to: diff_note)
+
+ expect(CreateNoteDiffFileWorker).not_to receive(:perform_async).with(reply_diff_note.id)
+
+ reply_diff_note.reload.diff_file
+ end
+ end
end
describe "#diff_line" do
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index 25d6597084c..4bded9efe91 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -562,7 +562,7 @@ describe Environment do
it "is not regenerated if name changes" do
original_slug = environment.slug
- environment.update_attributes!(name: environment.name.reverse)
+ environment.update!(name: environment.name.reverse)
expect(environment.slug).to eq(original_slug)
end
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index 8ea92410022..c1eac4fa489 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -50,6 +50,19 @@ describe Event do
end
end
+ describe '#set_last_repository_updated_at' do
+ it 'only updates once every Event::REPOSITORY_UPDATED_AT_INTERVAL minutes' do
+ last_known_timestamp = (Event::REPOSITORY_UPDATED_AT_INTERVAL - 1.minute).ago
+ project.update(last_repository_updated_at: last_known_timestamp)
+ project.reload # a reload removes fractions of seconds
+
+ expect do
+ create_push_event(project, project.owner)
+ project.reload
+ end.not_to change { project.last_repository_updated_at }
+ end
+ end
+
describe 'after_create :track_user_interacted_projects' do
let(:event) { build(:push_event, project: project, author: project.owner) }
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index f83b52e8975..0729eb99e78 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -67,6 +67,30 @@ describe Group do
end
end
+ describe '#notification_settings', :nested_groups do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+ let(:sub_group) { create(:group, parent_id: group.id) }
+
+ before do
+ group.add_developer(user)
+ sub_group.add_developer(user)
+ end
+
+ it 'also gets notification settings from parent groups' do
+ expect(sub_group.notification_settings.size).to eq(2)
+ expect(sub_group.notification_settings).to include(group.notification_settings.first)
+ end
+
+ context 'when sub group is deleted' do
+ it 'does not delete parent notification settings' do
+ expect do
+ sub_group.destroy
+ end.to change { NotificationSetting.count }.by(-1)
+ end
+ end
+ end
+
describe '#visibility_level_allowed_by_parent' do
let(:parent) { create(:group, :internal) }
let(:sub_group) { build(:group, parent_id: parent.id) }
@@ -153,7 +177,7 @@ describe Group do
describe 'when the user has access to a group' do
before do
- group.add_user(user, Gitlab::Access::MASTER)
+ group.add_user(user, Gitlab::Access::MAINTAINER)
end
it { is_expected.to eq([group]) }
@@ -205,10 +229,10 @@ describe Group do
let(:user) { create(:user) }
before do
- group.add_user(user, GroupMember::MASTER)
+ group.add_user(user, GroupMember::MAINTAINER)
end
- it { expect(group.group_members.masters.map(&:user)).to include(user) }
+ it { expect(group.group_members.maintainers.map(&:user)).to include(user) }
end
describe '#add_users' do
@@ -230,7 +254,7 @@ describe Group do
let(:user) { create(:user) }
before do
- group.add_user(user, GroupMember::MASTER)
+ group.add_user(user, GroupMember::MAINTAINER)
end
it "is true if avatar is image" do
@@ -240,7 +264,7 @@ describe Group do
it "is false if avatar is html page" do
group.update_attribute(:avatar, 'uploads/avatar.html')
- expect(group.avatar_type).to eq(["file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff"])
+ expect(group.avatar_type).to eq(["file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff, ico"])
end
end
@@ -250,7 +274,7 @@ describe Group do
context 'when avatar file is uploaded' do
before do
- group.add_master(user)
+ group.add_maintainer(user)
end
it 'shows correct avatar url' do
@@ -293,7 +317,7 @@ describe Group do
end
it { expect(group.has_owner?(@members[:owner])).to be_truthy }
- it { expect(group.has_owner?(@members[:master])).to be_falsey }
+ it { expect(group.has_owner?(@members[:maintainer])).to be_falsey }
it { expect(group.has_owner?(@members[:developer])).to be_falsey }
it { expect(group.has_owner?(@members[:reporter])).to be_falsey }
it { expect(group.has_owner?(@members[:guest])).to be_falsey }
@@ -301,19 +325,19 @@ describe Group do
it { expect(group.has_owner?(nil)).to be_falsey }
end
- describe '#has_master?' do
+ describe '#has_maintainer?' do
before do
@members = setup_group_members(group)
- create(:group_member, :invited, :master, group: group)
+ create(:group_member, :invited, :maintainer, group: group)
end
- it { expect(group.has_master?(@members[:owner])).to be_falsey }
- it { expect(group.has_master?(@members[:master])).to be_truthy }
- it { expect(group.has_master?(@members[:developer])).to be_falsey }
- it { expect(group.has_master?(@members[:reporter])).to be_falsey }
- it { expect(group.has_master?(@members[:guest])).to be_falsey }
- it { expect(group.has_master?(@members[:requester])).to be_falsey }
- it { expect(group.has_master?(nil)).to be_falsey }
+ it { expect(group.has_maintainer?(@members[:owner])).to be_falsey }
+ it { expect(group.has_maintainer?(@members[:maintainer])).to be_truthy }
+ it { expect(group.has_maintainer?(@members[:developer])).to be_falsey }
+ it { expect(group.has_maintainer?(@members[:reporter])).to be_falsey }
+ it { expect(group.has_maintainer?(@members[:guest])).to be_falsey }
+ it { expect(group.has_maintainer?(@members[:requester])).to be_falsey }
+ it { expect(group.has_maintainer?(nil)).to be_falsey }
end
describe '#lfs_enabled?' do
@@ -377,7 +401,7 @@ describe Group do
def setup_group_members(group)
members = {
owner: create(:user),
- master: create(:user),
+ maintainer: create(:user),
developer: create(:user),
reporter: create(:user),
guest: create(:user),
@@ -385,7 +409,7 @@ describe Group do
}
group.add_user(members[:owner], GroupMember::OWNER)
- group.add_user(members[:master], GroupMember::MASTER)
+ group.add_user(members[:maintainer], GroupMember::MAINTAINER)
group.add_user(members[:developer], GroupMember::DEVELOPER)
group.add_user(members[:reporter], GroupMember::REPORTER)
group.add_user(members[:guest], GroupMember::GUEST)
@@ -415,25 +439,25 @@ describe Group do
describe '#members_with_parents', :nested_groups do
let!(:group) { create(:group, :nested) }
- let!(:master) { group.parent.add_user(create(:user), GroupMember::MASTER) }
+ let!(:maintainer) { group.parent.add_user(create(:user), GroupMember::MAINTAINER) }
let!(:developer) { group.add_user(create(:user), GroupMember::DEVELOPER) }
it 'returns parents members' do
expect(group.members_with_parents).to include(developer)
- expect(group.members_with_parents).to include(master)
+ expect(group.members_with_parents).to include(maintainer)
end
end
describe '#direct_and_indirect_members', :nested_groups do
let!(:group) { create(:group, :nested) }
let!(:sub_group) { create(:group, parent: group) }
- let!(:master) { group.parent.add_user(create(:user), GroupMember::MASTER) }
+ let!(:maintainer) { group.parent.add_user(create(:user), GroupMember::MAINTAINER) }
let!(:developer) { group.add_user(create(:user), GroupMember::DEVELOPER) }
let!(:other_developer) { group.add_user(create(:user), GroupMember::DEVELOPER) }
it 'returns parents members' do
expect(group.direct_and_indirect_members).to include(developer)
- expect(group.direct_and_indirect_members).to include(master)
+ expect(group.direct_and_indirect_members).to include(maintainer)
end
it 'returns descendant members' do
@@ -515,14 +539,14 @@ describe Group do
describe '#user_ids_for_project_authorizations' do
it 'returns the user IDs for which to refresh authorizations' do
- master = create(:user)
+ maintainer = create(:user)
developer = create(:user)
- group.add_user(master, GroupMember::MASTER)
+ group.add_user(maintainer, GroupMember::MAINTAINER)
group.add_user(developer, GroupMember::DEVELOPER)
expect(group.user_ids_for_project_authorizations)
- .to include(master.id, developer.id)
+ .to include(maintainer.id, developer.id)
end
end
@@ -593,7 +617,7 @@ describe Group 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)
+ group.update!(path: new_path)
end
end
@@ -601,7 +625,7 @@ describe Group do
it 'does not trigger system hook' do
expect(group).not_to receive(:system_hook_service)
- group.update_attributes!(name: 'new name')
+ group.update!(name: 'new name')
end
end
end
diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb
index 8bc45715dcd..01129df1107 100644
--- a/spec/models/hooks/system_hook_spec.rb
+++ b/spec/models/hooks/system_hook_spec.rb
@@ -63,7 +63,7 @@ describe SystemHook do
end
it "project_create hook" do
- project.add_master(user)
+ project.add_maintainer(user)
expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /user_add_to_team/,
@@ -72,7 +72,7 @@ describe SystemHook do
end
it "project_destroy hook" do
- project.add_master(user)
+ project.add_maintainer(user)
project.project_members.destroy_all
expect(WebMock).to have_requested(:post, system_hook.url).with(
@@ -100,7 +100,7 @@ describe SystemHook do
end
it 'group member create hook' do
- group.add_master(user)
+ group.add_maintainer(user)
expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /user_add_to_group/,
@@ -109,7 +109,7 @@ describe SystemHook do
end
it 'group member destroy hook' do
- group.add_master(user)
+ group.add_maintainer(user)
group.group_members.destroy_all
expect(WebMock).to have_requested(:post, system_hook.url).with(
diff --git a/spec/models/hooks/web_hook_log_spec.rb b/spec/models/hooks/web_hook_log_spec.rb
index 19bc88b1333..744a6ccae8b 100644
--- a/spec/models/hooks/web_hook_log_spec.rb
+++ b/spec/models/hooks/web_hook_log_spec.rb
@@ -9,6 +9,24 @@ describe WebHookLog do
it { is_expected.to validate_presence_of(:web_hook) }
+ describe '.recent' do
+ let(:hook) { create(:project_hook) }
+
+ it 'does not return web hook logs that are too old' do
+ create(:web_hook_log, web_hook: hook, created_at: 91.days.ago)
+
+ expect(described_class.recent.size).to be_zero
+ end
+
+ it 'returns the web hook logs in descending order' do
+ hook1 = create(:web_hook_log, web_hook: hook, created_at: 2.hours.ago)
+ hook2 = create(:web_hook_log, web_hook: hook, created_at: 1.hour.ago)
+ hooks = described_class.recent.to_a
+
+ expect(hooks).to eq([hook2, hook1])
+ end
+ end
+
describe '#success?' do
let(:web_hook_log) { build(:web_hook_log, response_status: status) }
diff --git a/spec/models/import_export_upload_spec.rb b/spec/models/import_export_upload_spec.rb
new file mode 100644
index 00000000000..58af84b8a08
--- /dev/null
+++ b/spec/models/import_export_upload_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe ImportExportUpload do
+ subject { described_class.new(project: create(:project)) }
+
+ shared_examples 'stores the Import/Export file' do |method|
+ it 'stores the import file' do
+ subject.public_send("#{method}=", fixture_file_upload('spec/fixtures/project_export.tar.gz'))
+
+ subject.save!
+
+ url = "/uploads/-/system/import_export_upload/#{method}/#{subject.id}/project_export.tar.gz"
+
+ expect(subject.public_send(method).url).to eq(url)
+ end
+ end
+
+ context 'import' do
+ it_behaves_like 'stores the Import/Export file', :import_file
+ end
+
+ context 'export' do
+ it_behaves_like 'stores the Import/Export file', :export_file
+ end
+end
diff --git a/spec/models/internal_id_spec.rb b/spec/models/internal_id_spec.rb
index 8ef91e8fab5..581fd0293cc 100644
--- a/spec/models/internal_id_spec.rb
+++ b/spec/models/internal_id_spec.rb
@@ -5,7 +5,7 @@ describe InternalId do
let(:usage) { :issues }
let(:issue) { build(:issue, project: project) }
let(:scope) { { project: project } }
- let(:init) { ->(s) { s.project.issues.maximum(:iid) } }
+ let(:init) { ->(s) { s.project.issues.size } }
context 'validations' do
it { is_expected.to validate_presence_of(:usage) }
@@ -39,29 +39,6 @@ describe InternalId do
end
end
- context 'with an InternalId record present and existing issues with a higher internal id' do
- # This can happen if the old NonatomicInternalId is still in use
- before do
- issues = Array.new(rand(1..10)).map { create(:issue, project: project) }
-
- issue = issues.last
- issue.iid = issues.map { |i| i.iid }.max + 1
- issue.save
- end
-
- let(:maximum_iid) { project.issues.map { |i| i.iid }.max }
-
- it 'updates last_value to the maximum internal id present' do
- subject
-
- expect(described_class.find_by(project: project, usage: described_class.usages[usage.to_s]).last_value).to eq(maximum_iid + 1)
- end
-
- it 'returns next internal id correctly' do
- expect(subject).to eq(maximum_iid + 1)
- end
- end
-
context 'with concurrent inserts on table' do
it 'looks up the record if it was created concurrently' do
args = { **scope, usage: described_class.usages[usage.to_s] }
@@ -104,8 +81,7 @@ describe InternalId do
describe '#increment_and_save!' do
let(:id) { create(:internal_id) }
- let(:maximum_iid) { nil }
- subject { id.increment_and_save!(maximum_iid) }
+ subject { id.increment_and_save! }
it 'returns incremented iid' do
value = id.last_value
@@ -126,14 +102,5 @@ describe InternalId do
expect(subject).to eq(1)
end
end
-
- context 'with maximum_iid given' do
- let(:id) { create(:internal_id, last_value: 1) }
- let(:maximum_iid) { id.last_value + 10 }
-
- it 'returns maximum_iid instead' do
- expect(subject).to eq(12)
- end
- end
end
end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 128acf83686..84edfc3ff00 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -17,6 +17,7 @@ describe Issue do
it_behaves_like 'AtomicInternalId' do
let(:internal_id_attribute) { :iid }
let(:instance) { build(:issue) }
+ let(:scope) { :project }
let(:scope_attrs) { { project: instance.project } }
let(:usage) { :issues }
end
@@ -668,7 +669,7 @@ describe Issue do
context 'when the user is the project owner' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
it 'returns true for a regular issue' do
diff --git a/spec/models/lfs_file_lock_spec.rb b/spec/models/lfs_file_lock_spec.rb
index ce87b01b49c..e74f342d3eb 100644
--- a/spec/models/lfs_file_lock_spec.rb
+++ b/spec/models/lfs_file_lock_spec.rb
@@ -13,13 +13,13 @@ describe LfsFileLock do
describe '#can_be_unlocked_by?' do
let(:developer) { create(:user) }
- let(:master) { create(:user) }
+ let(:maintainer) { create(:user) }
before do
project = lfs_file_lock.project
project.add_developer(developer)
- project.add_master(master)
+ project.add_maintainer(maintainer)
end
context "when it's forced" do
@@ -29,8 +29,8 @@ describe LfsFileLock do
expect(lfs_file_lock.can_be_unlocked_by?(user, true)).to eq(true)
end
- it 'can be unlocked by a master' do
- expect(lfs_file_lock.can_be_unlocked_by?(master, true)).to eq(true)
+ it 'can be unlocked by a maintainer' do
+ expect(lfs_file_lock.can_be_unlocked_by?(maintainer, true)).to eq(true)
end
it "can't be unlocked by other user" do
@@ -45,8 +45,8 @@ describe LfsFileLock do
expect(lfs_file_lock.can_be_unlocked_by?(user)).to eq(true)
end
- it "can't be unlocked by a master" do
- expect(lfs_file_lock.can_be_unlocked_by?(master)).to eq(false)
+ it "can't be unlocked by a maintainer" do
+ expect(lfs_file_lock.can_be_unlocked_by?(maintainer)).to eq(false)
end
it "can't be unlocked by other user" do
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index c64cdf8f812..fca1b1f90d9 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -62,16 +62,16 @@ describe Member do
@owner_user = create(:user).tap { |u| group.add_owner(u) }
@owner = group.members.find_by(user_id: @owner_user.id)
- @master_user = create(:user).tap { |u| project.add_master(u) }
- @master = project.members.find_by(user_id: @master_user.id)
+ @maintainer_user = create(:user).tap { |u| project.add_maintainer(u) }
+ @maintainer = project.members.find_by(user_id: @maintainer_user.id)
@blocked_user = create(:user).tap do |u|
- project.add_master(u)
+ project.add_maintainer(u)
project.add_developer(u)
u.block!
end
- @blocked_master = project.members.find_by(user_id: @blocked_user.id, access_level: Gitlab::Access::MASTER)
+ @blocked_maintainer = project.members.find_by(user_id: @blocked_user.id, access_level: Gitlab::Access::MAINTAINER)
@blocked_developer = project.members.find_by(user_id: @blocked_user.id, access_level: Gitlab::Access::DEVELOPER)
@invited_member = create(:project_member, :developer,
@@ -95,10 +95,10 @@ describe Member do
describe '.access_for_user_ids' do
it 'returns the right access levels' do
- users = [@owner_user.id, @master_user.id, @blocked_user.id]
+ users = [@owner_user.id, @maintainer_user.id, @blocked_user.id]
expected = {
@owner_user.id => Gitlab::Access::OWNER,
- @master_user.id => Gitlab::Access::MASTER
+ @maintainer_user.id => Gitlab::Access::MAINTAINER
}
expect(described_class.access_for_user_ids(users)).to eq(expected)
@@ -106,7 +106,7 @@ describe Member do
end
describe '.invite' do
- it { expect(described_class.invite).not_to include @master }
+ it { expect(described_class.invite).not_to include @maintainer }
it { expect(described_class.invite).to include @invited_member }
it { expect(described_class.invite).not_to include @accepted_invite_member }
it { expect(described_class.invite).not_to include @requested_member }
@@ -114,7 +114,7 @@ describe Member do
end
describe '.non_invite' do
- it { expect(described_class.non_invite).to include @master }
+ it { expect(described_class.non_invite).to include @maintainer }
it { expect(described_class.non_invite).not_to include @invited_member }
it { expect(described_class.non_invite).to include @accepted_invite_member }
it { expect(described_class.non_invite).to include @requested_member }
@@ -122,7 +122,7 @@ describe Member do
end
describe '.request' do
- it { expect(described_class.request).not_to include @master }
+ it { expect(described_class.request).not_to include @maintainer }
it { expect(described_class.request).not_to include @invited_member }
it { expect(described_class.request).not_to include @accepted_invite_member }
it { expect(described_class.request).to include @requested_member }
@@ -130,7 +130,7 @@ describe Member do
end
describe '.non_request' do
- it { expect(described_class.non_request).to include @master }
+ it { expect(described_class.non_request).to include @maintainer }
it { expect(described_class.non_request).to include @invited_member }
it { expect(described_class.non_request).to include @accepted_invite_member }
it { expect(described_class.non_request).not_to include @requested_member }
@@ -141,35 +141,35 @@ describe Member do
subject { described_class.developers.to_a }
it { is_expected.not_to include @owner }
- it { is_expected.not_to include @master }
+ it { is_expected.not_to include @maintainer }
it { is_expected.to include @invited_member }
it { is_expected.to include @accepted_invite_member }
it { is_expected.not_to include @requested_member }
it { is_expected.to include @accepted_request_member }
- it { is_expected.not_to include @blocked_master }
+ it { is_expected.not_to include @blocked_maintainer }
it { is_expected.not_to include @blocked_developer }
end
- describe '.owners_and_masters' do
- it { expect(described_class.owners_and_masters).to include @owner }
- it { expect(described_class.owners_and_masters).to include @master }
- it { expect(described_class.owners_and_masters).not_to include @invited_member }
- it { expect(described_class.owners_and_masters).not_to include @accepted_invite_member }
- it { expect(described_class.owners_and_masters).not_to include @requested_member }
- it { expect(described_class.owners_and_masters).not_to include @accepted_request_member }
- it { expect(described_class.owners_and_masters).not_to include @blocked_master }
+ describe '.owners_and_maintainers' do
+ it { expect(described_class.owners_and_maintainers).to include @owner }
+ it { expect(described_class.owners_and_maintainers).to include @maintainer }
+ it { expect(described_class.owners_and_maintainers).not_to include @invited_member }
+ it { expect(described_class.owners_and_maintainers).not_to include @accepted_invite_member }
+ it { expect(described_class.owners_and_maintainers).not_to include @requested_member }
+ it { expect(described_class.owners_and_maintainers).not_to include @accepted_request_member }
+ it { expect(described_class.owners_and_maintainers).not_to include @blocked_maintainer }
end
describe '.has_access' do
subject { described_class.has_access.to_a }
it { is_expected.to include @owner }
- it { is_expected.to include @master }
+ it { is_expected.to include @maintainer }
it { is_expected.to include @invited_member }
it { is_expected.to include @accepted_invite_member }
it { is_expected.not_to include @requested_member }
it { is_expected.to include @accepted_request_member }
- it { is_expected.not_to include @blocked_master }
+ it { is_expected.not_to include @blocked_maintainer }
it { is_expected.not_to include @blocked_developer }
end
end
@@ -187,20 +187,20 @@ describe Member do
let!(:admin) { create(:admin) }
it 'returns a <Source>Member object' do
- member = described_class.add_user(source, user, :master)
+ member = described_class.add_user(source, user, :maintainer)
expect(member).to be_a "#{source_type.classify}Member".constantize
expect(member).to be_persisted
end
it 'sets members.created_by to the given current_user' do
- member = described_class.add_user(source, user, :master, current_user: admin)
+ member = described_class.add_user(source, user, :maintainer, current_user: admin)
expect(member.created_by).to eq(admin)
end
it 'sets members.expires_at to the given expires_at' do
- member = described_class.add_user(source, user, :master, expires_at: Date.new(2016, 9, 22))
+ member = described_class.add_user(source, user, :maintainer, expires_at: Date.new(2016, 9, 22))
expect(member.expires_at).to eq(Date.new(2016, 9, 22))
end
@@ -230,7 +230,7 @@ describe Member do
it 'adds the user as a member' do
expect(source.users).not_to include(user)
- described_class.add_user(source, user.id, :master)
+ described_class.add_user(source, user.id, :maintainer)
expect(source.users.reload).to include(user)
end
@@ -240,7 +240,7 @@ describe Member do
it 'adds the user as a member' do
expect(source.users).not_to include(user)
- described_class.add_user(source, 42, :master)
+ described_class.add_user(source, 42, :maintainer)
expect(source.users.reload).not_to include(user)
end
@@ -250,7 +250,7 @@ describe Member do
it 'adds the user as a member' do
expect(source.users).not_to include(user)
- described_class.add_user(source, user, :master)
+ described_class.add_user(source, user, :maintainer)
expect(source.users.reload).to include(user)
end
@@ -265,7 +265,7 @@ describe Member do
expect(source.users).not_to include(user)
expect(source.requesters.exists?(user_id: user)).to be_truthy
- expect { described_class.add_user(source, user, :master) }
+ expect { described_class.add_user(source, user, :maintainer) }
.to raise_error(Gitlab::Access::AccessDeniedError)
expect(source.users.reload).not_to include(user)
@@ -277,7 +277,7 @@ describe Member do
it 'adds the user as a member' do
expect(source.users).not_to include(user)
- described_class.add_user(source, user.email, :master)
+ described_class.add_user(source, user.email, :maintainer)
expect(source.users.reload).to include(user)
end
@@ -287,7 +287,7 @@ describe Member do
it 'creates an invited member' do
expect(source.users).not_to include(user)
- described_class.add_user(source, 'user@example.com', :master)
+ described_class.add_user(source, 'user@example.com', :maintainer)
expect(source.members.invite.pluck(:invite_email)).to include('user@example.com')
end
@@ -298,7 +298,7 @@ describe Member do
it 'creates the member' do
expect(source.users).not_to include(user)
- described_class.add_user(source, user, :master, current_user: admin)
+ described_class.add_user(source, user, :maintainer, current_user: admin)
expect(source.users.reload).to include(user)
end
@@ -312,7 +312,7 @@ describe Member do
expect(source.users).not_to include(user)
expect(source.requesters.exists?(user_id: user)).to be_truthy
- described_class.add_user(source, user, :master, current_user: admin)
+ described_class.add_user(source, user, :maintainer, current_user: admin)
expect(source.users.reload).to include(user)
expect(source.requesters.reload.exists?(user_id: user)).to be_falsy
@@ -324,7 +324,7 @@ describe Member do
it 'does not create the member' do
expect(source.users).not_to include(user)
- member = described_class.add_user(source, user, :master, current_user: user)
+ member = described_class.add_user(source, user, :maintainer, current_user: user)
expect(source.users.reload).not_to include(user)
expect(member).not_to be_persisted
@@ -339,7 +339,7 @@ describe Member do
expect(source.users).not_to include(user)
expect(source.requesters.exists?(user_id: user)).to be_truthy
- described_class.add_user(source, user, :master, current_user: user)
+ described_class.add_user(source, user, :maintainer, current_user: user)
expect(source.users.reload).not_to include(user)
expect(source.requesters.exists?(user_id: user)).to be_truthy
@@ -356,9 +356,9 @@ describe Member do
it 'updates the member' do
expect(source.users).to include(user)
- described_class.add_user(source, user, :master)
+ described_class.add_user(source, user, :maintainer)
- expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MASTER)
+ expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MAINTAINER)
end
end
@@ -366,9 +366,9 @@ describe Member do
it 'updates the member' do
expect(source.users).to include(user)
- described_class.add_user(source, user, :master, current_user: admin)
+ described_class.add_user(source, user, :maintainer, current_user: admin)
- expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MASTER)
+ expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MAINTAINER)
end
end
@@ -376,7 +376,7 @@ describe Member do
it 'does not update the member' do
expect(source.users).to include(user)
- described_class.add_user(source, user, :master, current_user: user)
+ described_class.add_user(source, user, :maintainer, current_user: user)
expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::DEVELOPER)
end
@@ -395,7 +395,7 @@ describe Member do
let(:user2) { create(:user) }
it 'returns a <Source>Member objects' do
- members = described_class.add_users(source, [user1, user2], :master)
+ members = described_class.add_users(source, [user1, user2], :maintainer)
expect(members).to be_a Array
expect(members.size).to eq(2)
@@ -404,7 +404,7 @@ describe Member do
end
it 'returns an empty array' do
- members = described_class.add_users(source, [], :master)
+ members = described_class.add_users(source, [], :maintainer)
expect(members).to be_a Array
expect(members).to be_empty
@@ -413,7 +413,7 @@ describe Member do
it 'supports differents formats' do
list = ['joe@local.test', admin, user1.id, user2.id.to_s]
- members = described_class.add_users(source, list, :master)
+ members = described_class.add_users(source, list, :maintainer)
expect(members.size).to eq(4)
expect(members.first).to be_invite
diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb
index ffc78015f94..97959ed4304 100644
--- a/spec/models/members/group_member_spec.rb
+++ b/spec/models/members/group_member_spec.rb
@@ -21,7 +21,7 @@ describe GroupMember do
described_class.add_users(
group,
[users.first.id, users.second],
- described_class::MASTER
+ described_class::MAINTAINER
)
expect(group.users).to include(users.first, users.second)
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index 574eb468e4c..334d4f95f53 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -28,7 +28,7 @@ describe ProjectMember do
expect(project.users).not_to include(user)
- described_class.add_user(project, user, :master, current_user: project.owner)
+ described_class.add_user(project, user, :maintainer, current_user: project.owner)
expect(project.users.reload).to include(user)
end
@@ -41,9 +41,9 @@ describe ProjectMember do
end
describe "#destroy" do
- let(:owner) { create(:project_member, access_level: ProjectMember::MASTER) }
+ let(:owner) { create(:project_member, access_level: ProjectMember::MAINTAINER) }
let(:project) { owner.project }
- let(:master) { create(:project_member, project: project) }
+ let(:maintainer) { create(:project_member, project: project) }
it "creates an expired event when left due to expiry" do
expired = create(:project_member, project: project, expires_at: Time.now - 6.days)
@@ -52,7 +52,7 @@ describe ProjectMember do
end
it "creates a left event when left due to leave" do
- master.destroy
+ maintainer.destroy
expect(Event.recent.first.action).to eq(Event::LEFT)
end
end
@@ -95,7 +95,7 @@ describe ProjectMember do
described_class.add_users_to_projects(
[projects.first.id, projects.second.id],
[users.first.id, users.second],
- described_class::MASTER)
+ described_class::MAINTAINER)
expect(projects.first.users).to include(users.first)
expect(projects.first.users).to include(users.second)
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index b4249d72fc8..ccc3ff861c5 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -47,6 +47,45 @@ describe MergeRequestDiff do
end
describe '#diffs' do
+ let(:merge_request) { create(:merge_request, :with_diffs) }
+ let!(:diff) { merge_request.merge_request_diff.reload }
+
+ context 'when it was not cleaned by the system' do
+ it 'returns persisted diffs' do
+ expect(diff).to receive(:load_diffs)
+
+ diff.diffs
+ end
+ end
+
+ context 'when diff was cleaned by the system' do
+ before do
+ diff.clean!
+ end
+
+ it 'returns diffs from repository if can compare with current diff refs' do
+ expect(diff).not_to receive(:load_diffs)
+
+ expect(Compare)
+ .to receive(:new)
+ .with(instance_of(Gitlab::Git::Compare), merge_request.target_project,
+ base_sha: diff.base_commit_sha, straight: false)
+ .and_call_original
+
+ diff.diffs
+ end
+
+ it 'returns persisted diffs if cannot compare with diff refs' do
+ expect(diff).to receive(:load_diffs)
+
+ diff.update!(head_commit_sha: 'invalid-sha')
+
+ diff.diffs
+ end
+ end
+ end
+
+ describe '#raw_diffs' do
context 'when the :ignore_whitespace_change option is set' do
it 'creates a new compare object instead of loading from the DB' do
expect(diff_with_commits).not_to receive(:load_diffs)
@@ -114,6 +153,13 @@ describe MergeRequestDiff do
expect(mr_diff.empty?).to be_truthy
end
+ it 'expands collapsed diffs before saving' do
+ mr_diff = create(:merge_request, source_branch: 'expand-collapse-lines', target_branch: 'master').merge_request_diff
+ diff_file = mr_diff.merge_request_diff_files.find_by(new_path: 'expand-collapse/file-5.txt')
+
+ expect(diff_file.diff).not_to be_empty
+ end
+
it 'saves binary diffs correctly' do
path = 'files/images/icn-time-tracking.pdf'
mr_diff = create(:merge_request, source_branch: 'add-pdf-text-binary', target_branch: 'master').merge_request_diff
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 04379e7d2c3..b0d9d03bf6c 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -14,6 +14,69 @@ describe MergeRequest do
it { is_expected.to have_many(:merge_request_diffs) }
end
+ describe '#squash_in_progress?' do
+ shared_examples 'checking whether a squash is in progress' do
+ let(:repo_path) do
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ subject.source_project.repository.path
+ end
+ end
+ let(:squash_path) { File.join(repo_path, "gitlab-worktree", "squash-#{subject.id}") }
+
+ before do
+ system(*%W(#{Gitlab.config.git.bin_path} -C #{repo_path} worktree add --detach #{squash_path} master))
+ end
+
+ it 'returns true when there is a current squash directory' do
+ expect(subject.squash_in_progress?).to be_truthy
+ end
+
+ it 'returns false when there is no squash directory' do
+ FileUtils.rm_rf(squash_path)
+
+ expect(subject.squash_in_progress?).to be_falsey
+ end
+
+ it 'returns false when the squash directory has expired' do
+ time = 20.minutes.ago.to_time
+ File.utime(time, time, squash_path)
+
+ expect(subject.squash_in_progress?).to be_falsey
+ end
+
+ it 'returns false when the source project has been removed' do
+ allow(subject).to receive(:source_project).and_return(nil)
+
+ expect(subject.squash_in_progress?).to be_falsey
+ end
+ end
+
+ context 'when Gitaly squash_in_progress is enabled' do
+ it_behaves_like 'checking whether a squash is in progress'
+ end
+
+ context 'when Gitaly squash_in_progress is disabled', :disable_gitaly do
+ it_behaves_like 'checking whether a squash is in progress'
+ end
+ end
+
+ describe '#squash?' do
+ let(:merge_request) { build(:merge_request, squash: squash) }
+ subject { merge_request.squash? }
+
+ context 'disabled in database' do
+ let(:squash) { false }
+
+ it { is_expected.to be_falsy }
+ end
+
+ context 'enabled in database' do
+ let(:squash) { true }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
describe 'modules' do
subject { described_class }
@@ -25,6 +88,7 @@ describe MergeRequest do
it_behaves_like 'AtomicInternalId' do
let(:internal_id_attribute) { :iid }
let(:instance) { build(:merge_request) }
+ let(:scope) { :target_project }
let(:scope_attrs) { { project: instance.target_project } }
let(:usage) { :merge_requests }
end
@@ -660,7 +724,7 @@ describe MergeRequest do
subject { merge_request }
before do
- subject.source_project.add_master(user)
+ subject.source_project.add_maintainer(user)
end
it "can't be removed when its a protected branch" do
@@ -1135,7 +1199,7 @@ describe MergeRequest do
end
before do
- project.add_master(current_user)
+ project.add_maintainer(current_user)
ProcessCommitWorker.new.perform(project.id,
current_user.id,
@@ -1245,38 +1309,51 @@ describe MergeRequest do
describe '#check_if_can_be_merged' do
let(:project) { create(:project, only_allow_merge_if_pipeline_succeeds: true) }
- subject { create(:merge_request, source_project: project, merge_status: :unchecked) }
+ shared_examples 'checking if can be merged' do
+ context 'when it is not broken and has no conflicts' do
+ before do
+ allow(subject).to receive(:broken?) { false }
+ allow(project.repository).to receive(:can_be_merged?).and_return(true)
+ end
- context 'when it is not broken and has no conflicts' do
- before do
- allow(subject).to receive(:broken?) { false }
- allow(project.repository).to receive(:can_be_merged?).and_return(true)
+ it 'is marked as mergeable' do
+ expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('can_be_merged')
+ end
end
- it 'is marked as mergeable' do
- expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('can_be_merged')
- end
- end
+ context 'when broken' do
+ before do
+ allow(subject).to receive(:broken?) { true }
+ allow(project.repository).to receive(:can_be_merged?).and_return(false)
+ end
- context 'when broken' do
- before do
- allow(subject).to receive(:broken?) { true }
+ it 'becomes unmergeable' do
+ expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('cannot_be_merged')
+ end
end
- it 'becomes unmergeable' do
- expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('cannot_be_merged')
+ context 'when it has conflicts' do
+ before do
+ allow(subject).to receive(:broken?) { false }
+ allow(project.repository).to receive(:can_be_merged?).and_return(false)
+ end
+
+ it 'becomes unmergeable' do
+ expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('cannot_be_merged')
+ end
end
end
- context 'when it has conflicts' do
- before do
- allow(subject).to receive(:broken?) { false }
- allow(project.repository).to receive(:can_be_merged?).and_return(false)
- end
+ context 'when merge_status is unchecked' do
+ subject { create(:merge_request, source_project: project, merge_status: :unchecked) }
- it 'becomes unmergeable' do
- expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('cannot_be_merged')
- end
+ it_behaves_like 'checking if can be merged'
+ end
+
+ context 'when merge_status is unchecked' do
+ subject { create(:merge_request, source_project: project, merge_status: :cannot_be_merged_recheck) }
+
+ it_behaves_like 'checking if can be merged'
end
end
@@ -1492,8 +1569,8 @@ describe MergeRequest do
let(:merge_request) { create(:merge_request, source_project: project) }
before do
- merge_request.source_project.add_master(user)
- merge_request.target_project.add_master(user)
+ merge_request.source_project.add_maintainer(user)
+ merge_request.target_project.add_maintainer(user)
end
context 'with multiple environments' do
@@ -1553,28 +1630,17 @@ describe MergeRequest do
end
describe "#reload_diff" do
- let(:discussion) { create(:diff_note_on_merge_request, project: subject.project, noteable: subject).to_discussion }
- let(:commit) { subject.project.commit(sample_commit.id) }
-
- it "does not change existing merge request diff" do
- expect(subject.merge_request_diff).not_to receive(:save_git_content)
- subject.reload_diff
- end
-
- it "creates new merge request diff" do
- expect { subject.reload_diff }.to change { subject.merge_request_diffs.count }.by(1)
- end
-
- it "executes diff cache service" do
- expect_any_instance_of(MergeRequests::MergeRequestDiffCacheService).to receive(:execute).with(subject, an_instance_of(MergeRequestDiff))
+ it 'calls MergeRequests::ReloadDiffsService#execute with correct params' do
+ user = create(:user)
+ service = instance_double(MergeRequests::ReloadDiffsService, execute: nil)
- subject.reload_diff
- end
+ expect(MergeRequests::ReloadDiffsService)
+ .to receive(:new).with(subject, user)
+ .and_return(service)
- it "calls update_diff_discussion_positions" do
- expect(subject).to receive(:update_diff_discussion_positions)
+ subject.reload_diff(user)
- subject.reload_diff
+ expect(service).to have_received(:execute)
end
context 'when using the after_update hook to update' do
@@ -1825,7 +1891,7 @@ describe MergeRequest do
end
it 'returns false if the merge request is merged' do
- merge_request.update_attributes(state: 'merged')
+ merge_request.update(state: 'merged')
expect(merge_request.reload.reopenable?).to be_falsey
end
@@ -2064,6 +2130,98 @@ describe MergeRequest do
expect(subject.merge_jid).to be_nil
end
end
+
+ describe 'transition to cannot_be_merged' do
+ let(:notification_service) { double(:notification_service) }
+ let(:todo_service) { double(:todo_service) }
+ subject { create(:merge_request, state, merge_status: :unchecked) }
+
+ before do
+ allow(NotificationService).to receive(:new).and_return(notification_service)
+ allow(TodoService).to receive(:new).and_return(todo_service)
+
+ allow(subject.project.repository).to receive(:can_be_merged?).and_return(false)
+ end
+
+ [:opened, :locked].each do |state|
+ context state do
+ let(:state) { state }
+
+ it 'notifies conflict, but does not notify again if rechecking still results in cannot_be_merged' do
+ expect(notification_service).to receive(:merge_request_unmergeable).with(subject).once
+ expect(todo_service).to receive(:merge_request_became_unmergeable).with(subject).once
+
+ subject.mark_as_unmergeable
+ subject.mark_as_unchecked
+ subject.mark_as_unmergeable
+ end
+
+ it 'notifies conflict, whenever newly unmergeable' do
+ expect(notification_service).to receive(:merge_request_unmergeable).with(subject).twice
+ expect(todo_service).to receive(:merge_request_became_unmergeable).with(subject).twice
+
+ subject.mark_as_unmergeable
+ subject.mark_as_unchecked
+ subject.mark_as_mergeable
+ subject.mark_as_unchecked
+ subject.mark_as_unmergeable
+ end
+
+ it 'does not notify whenever merge request is newly unmergeable due to other reasons' do
+ allow(subject.project.repository).to receive(:can_be_merged?).and_return(true)
+
+ expect(notification_service).not_to receive(:merge_request_unmergeable)
+ expect(todo_service).not_to receive(:merge_request_became_unmergeable)
+
+ subject.mark_as_unmergeable
+ end
+ end
+ end
+
+ [:closed, :merged].each do |state|
+ let(:state) { state }
+
+ context state do
+ it 'does not notify' do
+ expect(notification_service).not_to receive(:merge_request_unmergeable)
+ expect(todo_service).not_to receive(:merge_request_became_unmergeable)
+
+ subject.mark_as_unmergeable
+ end
+ end
+ end
+
+ context 'source branch is missing' do
+ subject { create(:merge_request, :invalid, :opened, merge_status: :unchecked, target_branch: 'master') }
+
+ before do
+ allow(subject.project.repository).to receive(:can_be_merged?).and_call_original
+ end
+
+ it 'does not raise error' do
+ expect(notification_service).not_to receive(:merge_request_unmergeable)
+ expect(todo_service).not_to receive(:merge_request_became_unmergeable)
+
+ expect { subject.mark_as_unmergeable }.not_to raise_error
+ expect(subject.cannot_be_merged?).to eq(true)
+ end
+ end
+ end
+
+ describe 'check_state?' do
+ it 'indicates whether MR is still checking for mergeability' do
+ state_machine = described_class.state_machines[:merge_status]
+ check_states = [:unchecked, :cannot_be_merged_recheck]
+
+ check_states.each do |merge_status|
+ expect(state_machine.check_state?(merge_status)).to be true
+ end
+
+ (state_machine.states.map(&:name) - check_states).each do |merge_status|
+ expect(state_machine.check_state?(merge_status)).to be false
+ end
+ end
+ end
end
describe '#should_be_rebased?' do
@@ -2078,7 +2236,11 @@ describe MergeRequest do
describe '#rebase_in_progress?' do
shared_examples 'checking whether a rebase is in progress' do
- let(:repo_path) { subject.source_project.repository.path }
+ let(:repo_path) do
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ subject.source_project.repository.path
+ end
+ end
let(:rebase_path) { File.join(repo_path, "gitlab-worktree", "rebase-#{subject.id}") }
before do
@@ -2118,25 +2280,25 @@ describe MergeRequest do
end
end
- describe '#allow_maintainer_to_push' do
+ describe '#allow_collaboration' do
let(:merge_request) do
- build(:merge_request, source_branch: 'fixes', allow_maintainer_to_push: true)
+ build(:merge_request, source_branch: 'fixes', allow_collaboration: true)
end
it 'is false when pushing by a maintainer is not possible' do
- expect(merge_request).to receive(:maintainer_push_possible?) { false }
+ expect(merge_request).to receive(:collaborative_push_possible?) { false }
- expect(merge_request.allow_maintainer_to_push).to be_falsy
+ expect(merge_request.allow_collaboration).to be_falsy
end
it 'is true when pushing by a maintainer is possible' do
- expect(merge_request).to receive(:maintainer_push_possible?) { true }
+ expect(merge_request).to receive(:collaborative_push_possible?) { true }
- expect(merge_request.allow_maintainer_to_push).to be_truthy
+ expect(merge_request.allow_collaboration).to be_truthy
end
end
- describe '#maintainer_push_possible?' do
+ describe '#collaborative_push_possible?' do
let(:merge_request) do
build(:merge_request, source_branch: 'fixes')
end
@@ -2148,14 +2310,14 @@ describe MergeRequest do
it 'does not allow maintainer to push if the source project is the same as the target' do
merge_request.target_project = merge_request.source_project = create(:project, :public)
- expect(merge_request.maintainer_push_possible?).to be_falsy
+ expect(merge_request.collaborative_push_possible?).to be_falsy
end
it 'allows maintainer to push when both source and target are public' do
merge_request.target_project = build(:project, :public)
merge_request.source_project = build(:project, :public)
- expect(merge_request.maintainer_push_possible?).to be_truthy
+ expect(merge_request.collaborative_push_possible?).to be_truthy
end
it 'is not available for protected branches' do
@@ -2166,11 +2328,11 @@ describe MergeRequest do
.with(merge_request.source_project, 'fixes')
.and_return(true)
- expect(merge_request.maintainer_push_possible?).to be_falsy
+ expect(merge_request.collaborative_push_possible?).to be_falsy
end
end
- describe '#can_allow_maintainer_to_push?' do
+ describe '#can_allow_collaboration?' do
let(:target_project) { create(:project, :public) }
let(:source_project) { fork_project(target_project) }
let(:merge_request) do
@@ -2182,17 +2344,52 @@ describe MergeRequest do
let(:user) { create(:user) }
before do
- allow(merge_request).to receive(:maintainer_push_possible?) { true }
+ allow(merge_request).to receive(:collaborative_push_possible?) { true }
end
it 'is false if the user does not have push access to the source project' do
- expect(merge_request.can_allow_maintainer_to_push?(user)).to be_falsy
+ expect(merge_request.can_allow_collaboration?(user)).to be_falsy
end
it 'is true when the user has push access to the source project' do
source_project.add_developer(user)
- expect(merge_request.can_allow_maintainer_to_push?(user)).to be_truthy
+ expect(merge_request.can_allow_collaboration?(user)).to be_truthy
+ end
+ end
+
+ describe '#merge_participants' do
+ it 'contains author' do
+ expect(subject.merge_participants).to eq([subject.author])
+ end
+
+ describe 'when merge_when_pipeline_succeeds? is true' do
+ describe 'when merge user is author' do
+ let(:user) { create(:user) }
+ subject do
+ create(:merge_request,
+ merge_when_pipeline_succeeds: true,
+ merge_user: user,
+ author: user)
+ end
+
+ it 'contains author only' do
+ expect(subject.merge_participants).to eq([subject.author])
+ end
+ end
+
+ describe 'when merge user and author are different users' do
+ let(:merge_user) { create(:user) }
+ subject do
+ create(:merge_request,
+ merge_when_pipeline_succeeds: true,
+ merge_user: merge_user)
+ end
+
+ it 'contains author and merge user' do
+ expect(subject.merge_participants).to eq([subject.author, merge_user])
+ end
+ end
end
end
end
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index 4bb9717d33e..204d6b47832 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -6,6 +6,7 @@ describe Milestone do
it_behaves_like 'AtomicInternalId' do
let(:internal_id_attribute) { :iid }
let(:instance) { build(:milestone, project: build(:project), group: nil) }
+ let(:scope) { :project }
let(:scope_attrs) { { project: instance.project } }
let(:usage) { :milestones }
end
@@ -15,6 +16,7 @@ describe Milestone do
it_behaves_like 'AtomicInternalId' do
let(:internal_id_attribute) { :iid }
let(:instance) { build(:milestone, project: nil, group: build(:group)) }
+ let(:scope) { :group }
let(:scope_attrs) { { namespace: instance.group } }
let(:usage) { :milestones }
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 6f702d8d95e..c1b385aaf76 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -200,7 +200,7 @@ describe Namespace do
end
it "moves dir if path changed" do
- namespace.update_attributes(path: namespace.full_path + '_new')
+ namespace.update(path: namespace.full_path + '_new')
expect(gitlab_shell.exists?(project.repository_storage, "#{namespace.path}/#{project.path}.git")).to be_truthy
end
@@ -279,7 +279,7 @@ describe Namespace do
it "repository directory remains unchanged if path changed" do
before_disk_path = project.disk_path
- namespace.update_attributes(path: namespace.full_path + '_new')
+ namespace.update(path: namespace.full_path + '_new')
expect(before_disk_path).to eq(project.disk_path)
expect(gitlab_shell.exists?(project.repository_storage, "#{project.disk_path}.git")).to be_truthy
@@ -301,12 +301,18 @@ describe Namespace do
end
def project_rugged(project)
- project.repository.rugged
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ project.repository.rugged
+ end
end
end
describe '#rm_dir', 'callback' do
- let(:repository_storage_path) { Gitlab.config.repositories.storages.default.legacy_disk_path }
+ let(:repository_storage_path) do
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ Gitlab.config.repositories.storages.default.legacy_disk_path
+ end
+ end
let(:path_in_dir) { File.join(repository_storage_path, namespace.full_path) }
let(:deleted_path) { namespace.full_path.gsub(namespace.path, "#{namespace.full_path}+#{namespace.id}+deleted") }
let(:deleted_path_in_dir) { File.join(repository_storage_path, deleted_path) }
@@ -649,6 +655,19 @@ describe Namespace do
end
end
+ describe '#root_ancestor' do
+ it 'returns the top most ancestor', :nested_groups do
+ root_group = create(:group)
+ nested_group = create(:group, parent: root_group)
+ deep_nested_group = create(:group, parent: nested_group)
+ very_deep_nested_group = create(:group, parent: deep_nested_group)
+
+ expect(nested_group.root_ancestor).to eq(root_group)
+ expect(deep_nested_group.root_ancestor).to eq(root_group)
+ expect(very_deep_nested_group.root_ancestor).to eq(root_group)
+ end
+ end
+
describe '#remove_exports' do
let(:legacy_project) { create(:project, :with_export, :legacy_storage, namespace: namespace) }
let(:hashed_project) { create(:project, :with_export, namespace: namespace) }
diff --git a/spec/models/note_diff_file_spec.rb b/spec/models/note_diff_file_spec.rb
new file mode 100644
index 00000000000..591c1a89748
--- /dev/null
+++ b/spec/models/note_diff_file_spec.rb
@@ -0,0 +1,11 @@
+require 'rails_helper'
+
+describe NoteDiffFile do
+ describe 'associations' do
+ it { is_expected.to belong_to(:diff_note) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:diff_note) }
+ end
+end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 6a6c71e6c82..947be44c903 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -144,8 +144,8 @@ describe Note do
describe 'admin' do
before do
@p1.project_members.create(user: @u1, access_level: ProjectMember::REPORTER)
- @p1.project_members.create(user: @u2, access_level: ProjectMember::MASTER)
- @p2.project_members.create(user: @u3, access_level: ProjectMember::MASTER)
+ @p1.project_members.create(user: @u2, access_level: ProjectMember::MAINTAINER)
+ @p2.project_members.create(user: @u3, access_level: ProjectMember::MAINTAINER)
end
it { expect(Ability.allowed?(@u1, :admin_note, @p1)).to be_falsey }
@@ -225,7 +225,7 @@ describe Note do
describe "cross_reference_not_visible_for?" do
let(:private_user) { create(:user) }
- let(:private_project) { create(:project, namespace: private_user.namespace) { |p| p.add_master(private_user) } }
+ let(:private_project) { create(:project, namespace: private_user.namespace) { |p| p.add_maintainer(private_user) } }
let(:private_issue) { create(:issue, project: private_project) }
let(:ext_proj) { create(:project, :public) }
@@ -828,5 +828,15 @@ describe Note do
note.destroy!
end
+
+ context 'when issuable etag caching is disabled' do
+ it 'does not store cache key' do
+ allow(note.noteable).to receive(:etag_caching_enabled?).and_return(false)
+
+ expect_any_instance_of(Gitlab::EtagCaching::Store).not_to receive(:touch)
+
+ note.save!
+ end
+ end
end
end
diff --git a/spec/models/notification_recipient_spec.rb b/spec/models/notification_recipient_spec.rb
index eda0e1da835..13fe47799ed 100644
--- a/spec/models/notification_recipient_spec.rb
+++ b/spec/models/notification_recipient_spec.rb
@@ -13,4 +13,48 @@ describe NotificationRecipient do
expect(recipient.has_access?).to be_falsy
end
+
+ context '#notification_setting' do
+ context 'for child groups', :nested_groups do
+ let!(:moved_group) { create(:group) }
+ let(:group) { create(:group) }
+ let(:sub_group_1) { create(:group, parent: group) }
+ let(:sub_group_2) { create(:group, parent: sub_group_1) }
+ let(:project) { create(:project, namespace: moved_group) }
+
+ before do
+ sub_group_2.add_owner(user)
+ moved_group.add_owner(user)
+ Groups::TransferService.new(moved_group, user).execute(sub_group_2)
+
+ moved_group.reload
+ end
+
+ context 'when notification setting is global' do
+ before do
+ user.notification_settings_for(group).global!
+ user.notification_settings_for(sub_group_1).mention!
+ user.notification_settings_for(sub_group_2).global!
+ user.notification_settings_for(moved_group).global!
+ end
+
+ it 'considers notification setting from the first parent without global setting' do
+ expect(subject.notification_setting.source).to eq(sub_group_1)
+ end
+ end
+
+ context 'when notification setting is not global' do
+ before do
+ user.notification_settings_for(group).global!
+ user.notification_settings_for(sub_group_1).mention!
+ user.notification_settings_for(sub_group_2).watch!
+ user.notification_settings_for(moved_group).disabled!
+ end
+
+ it 'considers notification setting from lowest group member in hierarchy' do
+ expect(subject.notification_setting.source).to eq(moved_group)
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/notification_setting_spec.rb b/spec/models/notification_setting_spec.rb
index 12681a147b4..d7c5f26ab67 100644
--- a/spec/models/notification_setting_spec.rb
+++ b/spec/models/notification_setting_spec.rb
@@ -58,7 +58,7 @@ RSpec.describe NotificationSetting do
1.upto(4) do |i|
setting = create(:notification_setting, user: user)
- setting.project.update_attributes(pending_delete: true) if i.even?
+ setting.project.update(pending_delete: true) if i.even?
end
end
diff --git a/spec/models/project_authorization_spec.rb b/spec/models/project_authorization_spec.rb
index 9e7e525b2c0..c289ee0859a 100644
--- a/spec/models/project_authorization_spec.rb
+++ b/spec/models/project_authorization_spec.rb
@@ -8,15 +8,15 @@ describe ProjectAuthorization do
describe '.insert_authorizations' do
it 'inserts the authorizations' do
described_class
- .insert_authorizations([[user.id, project1.id, Gitlab::Access::MASTER]])
+ .insert_authorizations([[user.id, project1.id, Gitlab::Access::MAINTAINER]])
expect(user.project_authorizations.count).to eq(1)
end
it 'inserts rows in batches' do
described_class.insert_authorizations([
- [user.id, project1.id, Gitlab::Access::MASTER],
- [user.id, project2.id, Gitlab::Access::MASTER]
+ [user.id, project1.id, Gitlab::Access::MAINTAINER],
+ [user.id, project2.id, Gitlab::Access::MAINTAINER]
], 1)
expect(user.project_authorizations.count).to eq(2)
diff --git a/spec/models/project_auto_devops_spec.rb b/spec/models/project_auto_devops_spec.rb
index 7545c0797e9..749b2094787 100644
--- a/spec/models/project_auto_devops_spec.rb
+++ b/spec/models/project_auto_devops_spec.rb
@@ -5,6 +5,8 @@ describe ProjectAutoDevops do
it { is_expected.to belong_to(:project) }
+ it { is_expected.to define_enum_for(:deploy_strategy) }
+
it { is_expected.to respond_to(:created_at) }
it { is_expected.to respond_to(:updated_at) }
@@ -67,8 +69,127 @@ describe ProjectAutoDevops do
end
end
+ context 'when deploy_strategy is manual' do
+ let(:domain) { 'example.com' }
+
+ before do
+ auto_devops.deploy_strategy = 'manual'
+ end
+
+ it do
+ expect(auto_devops.predefined_variables.map { |var| var[:key] })
+ .to include("STAGING_ENABLED", "INCREMENTAL_ROLLOUT_ENABLED")
+ end
+ end
+
+ context 'when deploy_strategy is continuous' do
+ let(:domain) { 'example.com' }
+
+ before do
+ auto_devops.deploy_strategy = 'continuous'
+ end
+
+ it do
+ expect(auto_devops.predefined_variables.map { |var| var[:key] })
+ .not_to include("STAGING_ENABLED", "INCREMENTAL_ROLLOUT_ENABLED")
+ end
+ end
+
def domain_variable
{ key: 'AUTO_DEVOPS_DOMAIN', value: 'example.com', public: true }
end
end
+
+ describe '#set_gitlab_deploy_token' do
+ let(:auto_devops) { build(:project_auto_devops, project: project) }
+
+ context 'when the project is public' do
+ let(:project) { create(:project, :repository, :public) }
+
+ it 'should not create a gitlab deploy token' do
+ expect do
+ auto_devops.save
+ end.not_to change { DeployToken.count }
+ end
+ end
+
+ context 'when the project is internal' do
+ let(:project) { create(:project, :repository, :internal) }
+
+ it 'should create a gitlab deploy token' do
+ expect do
+ auto_devops.save
+ end.to change { DeployToken.count }.by(1)
+ end
+ end
+
+ context 'when the project is private' do
+ let(:project) { create(:project, :repository, :private) }
+
+ it 'should create a gitlab deploy token' do
+ expect do
+ auto_devops.save
+ end.to change { DeployToken.count }.by(1)
+ end
+ end
+
+ context 'when autodevops is enabled at project level' do
+ let(:project) { create(:project, :repository, :internal) }
+ let(:auto_devops) { build(:project_auto_devops, project: project) }
+
+ it 'should create a deploy token' do
+ expect do
+ auto_devops.save
+ end.to change { DeployToken.count }.by(1)
+ end
+ end
+
+ context 'when autodevops is enabled at instancel level' do
+ let(:project) { create(:project, :repository, :internal) }
+ let(:auto_devops) { build(:project_auto_devops, :disabled, project: project) }
+
+ it 'should create a deploy token' do
+ allow(Gitlab::CurrentSettings).to receive(:auto_devops_enabled?).and_return(true)
+
+ expect do
+ auto_devops.save
+ end.to change { DeployToken.count }.by(1)
+ end
+ end
+
+ context 'when autodevops is disabled' do
+ let(:project) { create(:project, :repository, :internal) }
+ let(:auto_devops) { build(:project_auto_devops, :disabled, project: project) }
+
+ it 'should not create a deploy token' do
+ expect do
+ auto_devops.save
+ end.not_to change { DeployToken.count }
+ end
+ end
+
+ context 'when the project already has an active gitlab-deploy-token' do
+ let(:project) { create(:project, :repository, :internal) }
+ let!(:deploy_token) { create(:deploy_token, :gitlab_deploy_token, projects: [project]) }
+ let(:auto_devops) { build(:project_auto_devops, project: project) }
+
+ it 'should not create a deploy token' do
+ expect do
+ auto_devops.save
+ end.not_to change { DeployToken.count }
+ end
+ end
+
+ context 'when the project already has a revoked gitlab-deploy-token' do
+ let(:project) { create(:project, :repository, :internal) }
+ let!(:deploy_token) { create(:deploy_token, :gitlab_deploy_token, :expired, projects: [project]) }
+ let(:auto_devops) { build(:project_auto_devops, project: project) }
+
+ it 'should not create a deploy token' do
+ expect do
+ auto_devops.save
+ end.not_to change { DeployToken.count }
+ end
+ end
+ end
end
diff --git a/spec/models/project_feature_spec.rb b/spec/models/project_feature_spec.rb
index 63c6fbda3f2..cd7f77024da 100644
--- a/spec/models/project_feature_spec.rb
+++ b/spec/models/project_feature_spec.rb
@@ -77,7 +77,7 @@ describe ProjectFeature do
context 'repository related features' do
before do
- project.project_feature.update_attributes(
+ project.project_feature.update(
merge_requests_access_level: ProjectFeature::DISABLED,
builds_access_level: ProjectFeature::DISABLED,
repository_access_level: ProjectFeature::PRIVATE
diff --git a/spec/models/project_services/bamboo_service_spec.rb b/spec/models/project_services/bamboo_service_spec.rb
index 85baaccf035..f4f7afb1b92 100644
--- a/spec/models/project_services/bamboo_service_spec.rb
+++ b/spec/models/project_services/bamboo_service_spec.rb
@@ -120,6 +120,14 @@ describe BambooService, :use_clean_rails_memory_store_caching do
end
end
+ describe '#execute' do
+ it 'runs update and build action' do
+ stub_update_and_build_request
+
+ subject.execute(Gitlab::DataBuilder::Push::SAMPLE_DATA)
+ end
+ end
+
describe '#build_page' do
it 'returns the contents of the reactive cache' do
stub_reactive_cache(service, { build_page: 'foo' }, 'sha', 'ref')
@@ -216,10 +224,20 @@ describe BambooService, :use_clean_rails_memory_store_caching do
end
end
+ def stub_update_and_build_request(status: 200, body: nil)
+ bamboo_full_url = 'http://gitlab.com/bamboo/updateAndBuild.action?buildKey=foo&os_authType=basic'
+
+ stub_bamboo_request(bamboo_full_url, status, body)
+ end
+
def stub_request(status: 200, body: nil)
- bamboo_full_url = 'http://gitlab.com/bamboo/rest/api/latest/result?label=123&os_authType=basic'
+ bamboo_full_url = 'http://gitlab.com/bamboo/rest/api/latest/result/byChangeset/123?os_authType=basic'
+
+ stub_bamboo_request(bamboo_full_url, status, body)
+ end
- WebMock.stub_request(:get, bamboo_full_url).to_return(
+ def stub_bamboo_request(url, status, body)
+ WebMock.stub_request(:get, url).to_return(
status: status,
headers: { 'Content-Type' => 'application/json' },
body: body
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index 54ef0be67ff..6c637533c6b 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe JiraService do
include Gitlab::Routing
+ include AssetsHelpers
describe '#options' do
let(:service) do
@@ -164,6 +165,8 @@ describe JiraService do
it "creates Remote Link reference in JIRA for comment" do
@jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project))
+ favicon_path = "http://localhost/assets/#{find_asset('favicon.png').digest_path}"
+
# Creates comment
expect(WebMock).to have_requested(:post, @comment_url)
# Creates Remote Link in JIRA issue fields
@@ -173,7 +176,7 @@ describe JiraService do
object: {
url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/commit/#{merge_request.diff_head_sha}",
title: "GitLab: Solved by commit #{merge_request.diff_head_sha}.",
- icon: { title: "GitLab", url16x16: "http://localhost/favicon.ico" },
+ icon: { title: "GitLab", url16x16: favicon_path },
status: { resolved: true }
}
)
@@ -464,4 +467,18 @@ describe JiraService do
end
end
end
+
+ describe 'favicon urls', :request_store do
+ it 'includes the standard favicon' do
+ props = described_class.new.send(:build_remote_link_props, url: 'http://example.com', title: 'title')
+ expect(props[:object][:icon][:url16x16]).to match %r{^http://localhost/assets/favicon(?:-\h+).png$}
+ end
+
+ it 'includes returns the custom favicon' do
+ create :appearance, favicon: fixture_file_upload('spec/fixtures/dk.png')
+
+ props = described_class.new.send(:build_remote_link_props, url: 'http://example.com', title: 'title')
+ expect(props[:object][:icon][:url16x16]).to match %r{^http://localhost/uploads/-/system/appearance/favicon/\d+/dk.png$}
+ end
+ end
end
diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb
index 3be023a48c1..68ab9fd08ec 100644
--- a/spec/models/project_services/kubernetes_service_spec.rb
+++ b/spec/models/project_services/kubernetes_service_spec.rb
@@ -65,7 +65,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
before do
kubernetes_service.update_attribute(:active, false)
- kubernetes_service.properties[:namespace] = "foo"
+ kubernetes_service.properties['namespace'] = "foo"
end
it 'should not update attributes' do
@@ -82,7 +82,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
let(:kubernetes_service) { create(:kubernetes_service) }
it 'should update attributes' do
- kubernetes_service.properties[:namespace] = 'foo'
+ kubernetes_service.properties['namespace'] = 'foo'
expect(kubernetes_service.save).to be_truthy
end
end
@@ -92,7 +92,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
before do
kubernetes_service.active = false
- kubernetes_service.properties[:namespace] = 'foo'
+ kubernetes_service.properties['namespace'] = 'foo'
kubernetes_service.save
end
@@ -105,7 +105,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
end
it 'should update attributes' do
- expect(kubernetes_service.properties[:namespace]).to eq("foo")
+ expect(kubernetes_service.properties['namespace']).to eq("foo")
end
end
@@ -113,12 +113,12 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
let(:kubernetes_service) { create(:kubernetes_service, template: true, active: false) }
before do
- kubernetes_service.properties[:namespace] = 'foo'
+ kubernetes_service.properties['namespace'] = 'foo'
end
it 'should update attributes' do
expect(kubernetes_service.save).to be_truthy
- expect(kubernetes_service.properties[:namespace]).to eq('foo')
+ expect(kubernetes_service.properties['namespace']).to eq('foo')
end
end
end
diff --git a/spec/models/project_services/mattermost_slash_commands_service_spec.rb b/spec/models/project_services/mattermost_slash_commands_service_spec.rb
index 05d33cd3874..1983e0cc967 100644
--- a/spec/models/project_services/mattermost_slash_commands_service_spec.rb
+++ b/spec/models/project_services/mattermost_slash_commands_service_spec.rb
@@ -25,7 +25,7 @@ describe MattermostSlashCommandsService do
context 'the requests succeeds' do
before do
- stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create')
+ stub_request(:post, 'http://mattermost.example.com/api/v4/commands')
.with(body: {
team_id: 'abc',
trigger: 'gitlab',
@@ -59,7 +59,7 @@ describe MattermostSlashCommandsService do
context 'an error is received' do
before do
- stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create')
+ stub_request(:post, 'http://mattermost.example.com/api/v4/commands')
.to_return(
status: 500,
headers: { 'Content-Type' => 'application/json' },
@@ -89,11 +89,11 @@ describe MattermostSlashCommandsService do
context 'the requests succeeds' do
before do
- stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all')
+ stub_request(:get, 'http://mattermost.example.com/api/v4/users/me/teams')
.to_return(
status: 200,
headers: { 'Content-Type' => 'application/json' },
- body: { 'list' => true }.to_json
+ body: [{ id: 'test_team_id' }].to_json
)
end
@@ -104,7 +104,7 @@ describe MattermostSlashCommandsService do
context 'an error is received' do
before do
- stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all')
+ stub_request(:get, 'http://mattermost.example.com/api/v4/users/me/teams')
.to_return(
status: 500,
headers: { 'Content-Type' => 'application/json' },
diff --git a/spec/models/project_services/microsoft_teams_service_spec.rb b/spec/models/project_services/microsoft_teams_service_spec.rb
index 8d9ee96227f..3351c6280b4 100644
--- a/spec/models/project_services/microsoft_teams_service_spec.rb
+++ b/spec/models/project_services/microsoft_teams_service_spec.rb
@@ -225,10 +225,15 @@ describe MicrosoftTeamsService do
it 'calls Microsoft Teams API for pipeline events' do
data = Gitlab::DataBuilder::Pipeline.build(pipeline)
+ data[:markdown] = true
chat_service.execute(data)
- expect(WebMock).to have_requested(:post, webhook_url).once
+ message = ChatMessage::PipelineMessage.new(data)
+
+ expect(WebMock).to have_requested(:post, webhook_url)
+ .with(body: hash_including({ summary: message.summary }))
+ .once
end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index af2240f4f89..c01c7bc47b5 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -238,20 +238,27 @@ describe Project do
expect(project2.import_data).to be_nil
end
- it "does not allow blocked import_url localhost" do
+ it "does not allow import_url pointing to localhost" do
project2 = build(:project, import_url: 'http://localhost:9000/t.git')
expect(project2).to be_invalid
expect(project2.errors[:import_url].first).to include('Requests to localhost are not allowed')
end
- it "does not allow blocked import_url port" do
+ it "does not allow import_url with invalid ports" do
project2 = build(:project, import_url: 'http://github.com:25/t.git')
expect(project2).to be_invalid
expect(project2.errors[:import_url].first).to include('Only allowed ports are 22, 80, 443')
end
+ it "does not allow import_url with invalid user" do
+ project2 = build(:project, import_url: 'http://$user:password@github.com/t.git')
+
+ expect(project2).to be_invalid
+ expect(project2.errors[:import_url].first).to include('Username needs to start with an alphanumeric character')
+ end
+
describe 'project pending deletion' do
let!(:project_pending_deletion) do
create(:project,
@@ -329,7 +336,7 @@ describe Project do
end
describe 'delegation' do
- [:add_guest, :add_reporter, :add_developer, :add_master, :add_user, :add_users].each do |method|
+ [:add_guest, :add_reporter, :add_developer, :add_maintainer, :add_user, :add_users].each do |method|
it { is_expected.to delegate_method(method).to(:team) }
end
@@ -560,17 +567,17 @@ describe Project do
end
it 'returns the most recent timestamp' do
- project.update_attributes(updated_at: nil,
- last_activity_at: timestamp,
- last_repository_updated_at: timestamp - 1.hour)
+ project.update(updated_at: nil,
+ last_activity_at: timestamp,
+ last_repository_updated_at: timestamp - 1.hour)
- expect(project.last_activity_date).to eq(timestamp)
+ expect(project.last_activity_date).to be_like_time(timestamp)
- project.update_attributes(updated_at: timestamp,
- last_activity_at: timestamp - 1.hour,
- last_repository_updated_at: nil)
+ project.update(updated_at: timestamp,
+ last_activity_at: timestamp - 1.hour,
+ last_repository_updated_at: nil)
- expect(project.last_activity_date).to eq(timestamp)
+ expect(project.last_activity_date).to be_like_time(timestamp)
end
end
end
@@ -960,7 +967,7 @@ describe Project do
it 'is false if avatar is html page' do
project.update_attribute(:avatar, 'uploads/avatar.html')
- expect(project.avatar_type).to eq(['file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff'])
+ expect(project.avatar_type).to eq(['file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff, ico'])
end
end
@@ -1123,7 +1130,7 @@ describe Project do
describe 'when a user has access to a project' do
before do
- project.add_user(user, Gitlab::Access::MASTER)
+ project.add_user(user, Gitlab::Access::MAINTAINER)
end
it { is_expected.to eq([project]) }
@@ -1177,8 +1184,8 @@ describe Project do
describe '#any_runners?' do
context 'shared runners' do
let(:project) { create(:project, shared_runners_enabled: shared_runners_enabled) }
- let(:specific_runner) { create(:ci_runner) }
- let(:shared_runner) { create(:ci_runner, :shared) }
+ let(:specific_runner) { create(:ci_runner, :project, projects: [project]) }
+ let(:shared_runner) { create(:ci_runner, :instance) }
context 'for shared runners disabled' do
let(:shared_runners_enabled) { false }
@@ -1188,7 +1195,7 @@ describe Project do
end
it 'has a specific runner' do
- project.runners << specific_runner
+ specific_runner
expect(project.any_runners?).to be_truthy
end
@@ -1200,13 +1207,13 @@ describe Project do
end
it 'checks the presence of specific runner' do
- project.runners << specific_runner
+ specific_runner
expect(project.any_runners? { |runner| runner == specific_runner }).to be_truthy
end
it 'returns false if match cannot be found' do
- project.runners << specific_runner
+ specific_runner
expect(project.any_runners? { false }).to be_falsey
end
@@ -1238,7 +1245,7 @@ describe Project do
context 'group runners' do
let(:project) { create(:project, group_runners_enabled: group_runners_enabled) }
let(:group) { create(:group, projects: [project]) }
- let(:group_runner) { create(:ci_runner, groups: [group]) }
+ let(:group_runner) { create(:ci_runner, :group, groups: [group]) }
context 'for group runners disabled' do
let(:group_runners_enabled) { false }
@@ -1279,7 +1286,7 @@ describe Project do
end
describe '#shared_runners' do
- let!(:runner) { create(:ci_runner, :shared) }
+ let!(:runner) { create(:ci_runner, :instance) }
subject { project.shared_runners }
@@ -1693,6 +1700,31 @@ describe Project do
end
end
+ describe '#human_import_status_name' do
+ context 'when import_state exists' do
+ it 'returns the humanized status name' do
+ project = create(:project)
+ create(:import_state, :started, project: project)
+
+ expect(project.human_import_status_name).to eq("started")
+ end
+ end
+
+ context 'when import_state was not created yet' do
+ let(:project) { create(:project, :import_started) }
+
+ it 'ensures import_state is created and returns humanized status name' do
+ expect do
+ project.human_import_status_name
+ end.to change { ProjectImportState.count }.from(0).to(1)
+ end
+
+ it 'returns humanized status name' do
+ expect(project.human_import_status_name).to eq("started")
+ end
+ end
+ end
+
describe 'Project import job' do
let(:project) { create(:project, import_url: generate(:url)) }
@@ -1701,7 +1733,11 @@ describe Project do
.with(project.repository_storage, project.disk_path, project.import_url)
.and_return(true)
- expect_any_instance_of(Repository).to receive(:after_import)
+ # Works around https://github.com/rspec/rspec-mocks/issues/910
+ allow(described_class).to receive(:find).with(project.id).and_return(project)
+ expect(project.repository).to receive(:after_import)
+ .and_call_original
+ expect(project.wiki.repository).to receive(:after_import)
.and_call_original
end
@@ -1732,7 +1768,7 @@ describe Project do
it 'resets project import_error' do
error_message = 'Some error'
mirror = create(:project_empty_repo, :import_started)
- mirror.import_state.update_attributes(last_error: error_message)
+ mirror.import_state.update(last_error: error_message)
expect { mirror.import_finish }.to change { mirror.import_error }.from(error_message).to(nil)
end
@@ -1893,7 +1929,7 @@ describe Project do
end
it 'returns false when remote mirror is disabled' do
- project.remote_mirrors.first.update_attributes(enabled: false)
+ project.remote_mirrors.first.update(enabled: false)
is_expected.to be_falsy
end
@@ -1923,7 +1959,7 @@ describe Project do
end
it 'does not sync disabled remote mirrors' do
- project.remote_mirrors.first.update_attributes(enabled: false)
+ project.remote_mirrors.first.update(enabled: false)
expect_any_instance_of(RemoteMirror).not_to receive(:sync)
@@ -2256,6 +2292,28 @@ describe Project do
end
end
+ describe '#default_environment' do
+ let(:project) { create(:project) }
+
+ it 'returns production environment when it exists' do
+ production = create(:environment, name: "production", project: project)
+ create(:environment, name: 'staging', project: project)
+
+ expect(project.default_environment).to eq(production)
+ end
+
+ it 'returns first environment when no production environment exists' do
+ create(:environment, name: 'staging', project: project)
+ create(:environment, name: 'foo', project: project)
+
+ expect(project.default_environment).to eq(project.environments.first)
+ end
+
+ it 'returns nil when no available environment exists' do
+ expect(project.default_environment).to be_nil
+ end
+ end
+
describe '#secret_variables_for' do
let(:project) { create(:project) }
@@ -2303,6 +2361,22 @@ describe Project do
end
end
+ describe '#any_lfs_file_locks?', :request_store do
+ set(:project) { create(:project) }
+
+ it 'returns false when there are no LFS file locks' do
+ expect(project.any_lfs_file_locks?).to be_falsey
+ end
+
+ it 'returns a cached true when there are LFS file locks' do
+ create(:lfs_file_lock, project: project)
+
+ expect(project.lfs_file_locks).to receive(:any?).once.and_call_original
+
+ 2.times { expect(project.any_lfs_file_locks?).to be_truthy }
+ end
+ end
+
describe '#protected_for?' do
let(:project) { create(:project) }
@@ -2708,6 +2782,10 @@ describe Project do
let(:legacy_project) { create(:project, :legacy_storage, :with_export) }
let(:project) { create(:project, :with_export) }
+ before do
+ stub_feature_flags(import_export_object_storage: false)
+ end
+
it 'removes the exports directory for the project' do
expect(File.exist?(project.export_path)).to be_truthy
@@ -2756,12 +2834,14 @@ describe Project do
let(:project) { create(:project, :with_export) }
it 'removes the exported project file' do
+ stub_feature_flags(import_export_object_storage: false)
+
exported_file = project.export_project_path
expect(File.exist?(exported_file)).to be_truthy
- allow(FileUtils).to receive(:rm_f).and_call_original
- expect(FileUtils).to receive(:rm_f).with(exported_file).and_call_original
+ allow(FileUtils).to receive(:rm_rf).and_call_original
+ expect(FileUtils).to receive(:rm_rf).with(exported_file).and_call_original
project.remove_exported_project_file
@@ -2907,7 +2987,7 @@ describe Project do
project.rename_repo
- expect(project.repository.rugged.config['gitlab.fullpath']).to eq(project.full_path)
+ expect(rugged_config['gitlab.fullpath']).to eq(project.full_path)
end
end
@@ -3068,7 +3148,7 @@ describe Project do
it 'updates project full path in .git/config' do
project.rename_repo
- expect(project.repository.rugged.config['gitlab.fullpath']).to eq(project.full_path)
+ expect(rugged_config['gitlab.fullpath']).to eq(project.full_path)
end
end
@@ -3366,10 +3446,11 @@ describe Project do
end
describe '#after_import' do
- let(:project) { build(:project) }
+ let(:project) { create(:project) }
it 'runs the correct hooks' do
expect(project.repository).to receive(:after_import)
+ expect(project.wiki.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)
@@ -3415,8 +3496,8 @@ describe Project do
expect(project.protected_branches).not_to be_empty
expect(project.default_branch).to eq(project.protected_branches.first.name)
- expect(project.protected_branches.first.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
- expect(project.protected_branches.first.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
+ expect(project.protected_branches.first.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::MAINTAINER])
+ expect(project.protected_branches.first.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MAINTAINER])
end
end
end
@@ -3488,13 +3569,13 @@ describe Project do
it 'writes full path in .git/config when key is missing' do
project.write_repository_config
- expect(project.repository.rugged.config['gitlab.fullpath']).to eq project.full_path
+ expect(rugged_config['gitlab.fullpath']).to eq project.full_path
end
it 'updates full path in .git/config when key is present' do
project.write_repository_config(gl_full_path: 'old/path')
- expect { project.write_repository_config }.to change { project.repository.rugged.config['gitlab.fullpath'] }.from('old/path').to(project.full_path)
+ expect { project.write_repository_config }.to change { rugged_config['gitlab.fullpath'] }.from('old/path').to(project.full_path)
end
it 'does not raise an error with an empty repository' do
@@ -3583,7 +3664,7 @@ describe Project do
target_branch: 'target-branch',
source_project: project,
source_branch: 'awesome-feature-1',
- allow_maintainer_to_push: true
+ allow_collaboration: true
)
end
@@ -3620,9 +3701,14 @@ describe Project do
end
end
- describe '#branch_allows_maintainer_push?' do
+ describe '#branch_allows_collaboration_push?' do
it 'allows access if the user can merge the merge request' do
- expect(project.branch_allows_maintainer_push?(user, 'awesome-feature-1'))
+ expect(project.branch_allows_collaboration?(user, 'awesome-feature-1'))
+ .to be_truthy
+ end
+
+ it 'allows access when there are merge requests open but no branch name is given' do
+ expect(project.branch_allows_collaboration?(user, nil))
.to be_truthy
end
@@ -3630,7 +3716,7 @@ describe Project do
guest = create(:user)
target_project.add_guest(guest)
- expect(project.branch_allows_maintainer_push?(guest, 'awesome-feature-1'))
+ expect(project.branch_allows_collaboration?(guest, 'awesome-feature-1'))
.to be_falsy
end
@@ -3640,31 +3726,31 @@ describe Project do
target_branch: 'target-branch',
source_project: project,
source_branch: 'rejected-feature-1',
- allow_maintainer_to_push: true)
+ allow_collaboration: true)
- expect(project.branch_allows_maintainer_push?(user, 'rejected-feature-1'))
+ expect(project.branch_allows_collaboration?(user, 'rejected-feature-1'))
.to be_falsy
end
it 'does not allow access if the user cannot merge the merge request' do
- create(:protected_branch, :masters_can_push, project: target_project, name: 'target-branch')
+ create(:protected_branch, :maintainers_can_push, project: target_project, name: 'target-branch')
- expect(project.branch_allows_maintainer_push?(user, 'awesome-feature-1'))
+ expect(project.branch_allows_collaboration?(user, 'awesome-feature-1'))
.to be_falsy
end
it 'caches the result' do
- control = ActiveRecord::QueryRecorder.new { project.branch_allows_maintainer_push?(user, 'awesome-feature-1') }
+ control = ActiveRecord::QueryRecorder.new { project.branch_allows_collaboration?(user, 'awesome-feature-1') }
- expect { 3.times { project.branch_allows_maintainer_push?(user, 'awesome-feature-1') } }
+ expect { 3.times { project.branch_allows_collaboration?(user, 'awesome-feature-1') } }
.not_to exceed_query_limit(control)
end
context 'when the requeststore is active', :request_store do
it 'only queries per project across instances' do
- control = ActiveRecord::QueryRecorder.new { project.branch_allows_maintainer_push?(user, 'awesome-feature-1') }
+ control = ActiveRecord::QueryRecorder.new { project.branch_allows_collaboration?(user, 'awesome-feature-1') }
- expect { 2.times { described_class.find(project.id).branch_allows_maintainer_push?(user, 'awesome-feature-1') } }
+ expect { 2.times { described_class.find(project.id).branch_allows_collaboration?(user, 'awesome-feature-1') } }
.not_to exceed_query_limit(control).with_threshold(2)
end
end
@@ -3775,4 +3861,10 @@ describe Project do
let(:uploader_class) { AttachmentUploader }
end
end
+
+ def rugged_config
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ project.repository.rugged.config
+ end
+ end
end
diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb
index e07c522800a..c4af17f4726 100644
--- a/spec/models/project_team_spec.rb
+++ b/spec/models/project_team_spec.rb
@@ -1,7 +1,7 @@
require "spec_helper"
describe ProjectTeam do
- let(:master) { create(:user) }
+ let(:maintainer) { create(:user) }
let(:reporter) { create(:user) }
let(:guest) { create(:user) }
let(:nonmember) { create(:user) }
@@ -10,23 +10,23 @@ describe ProjectTeam do
let(:project) { create(:project) }
before do
- project.add_master(master)
+ project.add_maintainer(maintainer)
project.add_reporter(reporter)
project.add_guest(guest)
end
describe 'members collection' do
- it { expect(project.team.masters).to include(master) }
- it { expect(project.team.masters).not_to include(guest) }
- it { expect(project.team.masters).not_to include(reporter) }
- it { expect(project.team.masters).not_to include(nonmember) }
+ it { expect(project.team.maintainers).to include(maintainer) }
+ it { expect(project.team.maintainers).not_to include(guest) }
+ it { expect(project.team.maintainers).not_to include(reporter) }
+ it { expect(project.team.maintainers).not_to include(nonmember) }
end
describe 'access methods' do
- it { expect(project.team.master?(master)).to be_truthy }
- it { expect(project.team.master?(guest)).to be_falsey }
- it { expect(project.team.master?(reporter)).to be_falsey }
- it { expect(project.team.master?(nonmember)).to be_falsey }
+ it { expect(project.team.maintainer?(maintainer)).to be_truthy }
+ it { expect(project.team.maintainer?(guest)).to be_falsey }
+ it { expect(project.team.maintainer?(reporter)).to be_falsey }
+ it { expect(project.team.maintainer?(nonmember)).to be_falsey }
it { expect(project.team.member?(nonmember)).to be_falsey }
it { expect(project.team.member?(guest)).to be_truthy }
it { expect(project.team.member?(reporter, Gitlab::Access::REPORTER)).to be_truthy }
@@ -40,35 +40,35 @@ describe ProjectTeam do
let!(:project) { create(:project, group: group) }
before do
- group.add_master(master)
+ group.add_maintainer(maintainer)
group.add_reporter(reporter)
group.add_guest(guest)
# If user is a group and a project member - GitLab uses highest permission
- # So we add group guest as master and add group master as guest
+ # So we add group guest as maintainer and add group maintainer as guest
# to this project to test highest access
- project.add_master(guest)
- project.add_guest(master)
+ project.add_maintainer(guest)
+ project.add_guest(maintainer)
end
describe 'members collection' do
it { expect(project.team.reporters).to include(reporter) }
- it { expect(project.team.masters).to include(master) }
- it { expect(project.team.masters).to include(guest) }
- it { expect(project.team.masters).not_to include(reporter) }
- it { expect(project.team.masters).not_to include(nonmember) }
+ it { expect(project.team.maintainers).to include(maintainer) }
+ it { expect(project.team.maintainers).to include(guest) }
+ it { expect(project.team.maintainers).not_to include(reporter) }
+ it { expect(project.team.maintainers).not_to include(nonmember) }
end
describe 'access methods' do
it { expect(project.team.reporter?(reporter)).to be_truthy }
- it { expect(project.team.master?(master)).to be_truthy }
- it { expect(project.team.master?(guest)).to be_truthy }
- it { expect(project.team.master?(reporter)).to be_falsey }
- it { expect(project.team.master?(nonmember)).to be_falsey }
+ it { expect(project.team.maintainer?(maintainer)).to be_truthy }
+ it { expect(project.team.maintainer?(guest)).to be_truthy }
+ it { expect(project.team.maintainer?(reporter)).to be_falsey }
+ it { expect(project.team.maintainer?(nonmember)).to be_falsey }
it { expect(project.team.member?(nonmember)).to be_falsey }
it { expect(project.team.member?(guest)).to be_truthy }
- it { expect(project.team.member?(guest, Gitlab::Access::MASTER)).to be_truthy }
- it { expect(project.team.member?(reporter, Gitlab::Access::MASTER)).to be_falsey }
+ it { expect(project.team.member?(guest, Gitlab::Access::MAINTAINER)).to be_truthy }
+ it { expect(project.team.member?(reporter, Gitlab::Access::MAINTAINER)).to be_falsey }
it { expect(project.team.member?(nonmember, Gitlab::Access::GUEST)).to be_falsey }
end
end
@@ -145,13 +145,13 @@ describe ProjectTeam do
let(:requester) { create(:user) }
before do
- project.add_master(master)
+ project.add_maintainer(maintainer)
project.add_reporter(reporter)
project.add_guest(guest)
project.request_access(requester)
end
- it { expect(project.team.find_member(master.id)).to be_a(ProjectMember) }
+ it { expect(project.team.find_member(maintainer.id)).to be_a(ProjectMember) }
it { expect(project.team.find_member(reporter.id)).to be_a(ProjectMember) }
it { expect(project.team.find_member(guest.id)).to be_a(ProjectMember) }
it { expect(project.team.find_member(nonmember.id)).to be_nil }
@@ -164,13 +164,13 @@ describe ProjectTeam do
let(:requester) { create(:user) }
before do
- group.add_master(master)
+ group.add_maintainer(maintainer)
group.add_reporter(reporter)
group.add_guest(guest)
group.request_access(requester)
end
- it { expect(project.team.find_member(master.id)).to be_a(GroupMember) }
+ it { expect(project.team.find_member(maintainer.id)).to be_a(GroupMember) }
it { expect(project.team.find_member(reporter.id)).to be_a(GroupMember) }
it { expect(project.team.find_member(guest.id)).to be_a(GroupMember) }
it { expect(project.team.find_member(nonmember.id)).to be_nil }
@@ -179,14 +179,14 @@ describe ProjectTeam do
end
describe "#human_max_access" do
- it 'returns Master role' do
+ it 'returns Maintainer role' do
user = create(:user)
group = create(:group)
project = create(:project, namespace: group)
- group.add_master(user)
+ group.add_maintainer(user)
- expect(project.team.human_max_access(user.id)).to eq 'Master'
+ expect(project.team.human_max_access(user.id)).to eq 'Maintainer'
end
it 'returns Owner role' do
@@ -210,13 +210,13 @@ describe ProjectTeam do
context 'when project is not shared with group' do
before do
- project.add_master(master)
+ project.add_maintainer(maintainer)
project.add_reporter(reporter)
project.add_guest(guest)
project.request_access(requester)
end
- it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::MASTER) }
+ it { expect(project.team.max_member_access(maintainer.id)).to eq(Gitlab::Access::MAINTAINER) }
it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::REPORTER) }
it { expect(project.team.max_member_access(guest.id)).to eq(Gitlab::Access::GUEST) }
it { expect(project.team.max_member_access(nonmember.id)).to eq(Gitlab::Access::NO_ACCESS) }
@@ -230,11 +230,11 @@ describe ProjectTeam do
group: group,
group_access: Gitlab::Access::DEVELOPER)
- group.add_master(master)
+ group.add_maintainer(maintainer)
group.add_reporter(reporter)
end
- it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::DEVELOPER) }
+ it { expect(project.team.max_member_access(maintainer.id)).to eq(Gitlab::Access::DEVELOPER) }
it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::REPORTER) }
it { expect(project.team.max_member_access(nonmember.id)).to eq(Gitlab::Access::NO_ACCESS) }
it { expect(project.team.max_member_access(requester.id)).to eq(Gitlab::Access::NO_ACCESS) }
@@ -244,7 +244,7 @@ describe ProjectTeam do
project.namespace.update(share_with_group_lock: true)
end
- it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::NO_ACCESS) }
+ it { expect(project.team.max_member_access(maintainer.id)).to eq(Gitlab::Access::NO_ACCESS) }
it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::NO_ACCESS) }
end
end
@@ -257,13 +257,13 @@ describe ProjectTeam do
end
before do
- group.add_master(master)
+ group.add_maintainer(maintainer)
group.add_reporter(reporter)
group.add_guest(guest)
group.request_access(requester)
end
- it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::MASTER) }
+ it { expect(project.team.max_member_access(maintainer.id)).to eq(Gitlab::Access::MAINTAINER) }
it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::REPORTER) }
it { expect(project.team.max_member_access(guest.id)).to eq(Gitlab::Access::GUEST) }
it { expect(project.team.max_member_access(nonmember.id)).to eq(Gitlab::Access::NO_ACCESS) }
@@ -274,7 +274,7 @@ describe ProjectTeam do
describe '#member?' do
let(:group) { create(:group) }
let(:developer) { create(:user) }
- let(:master) { create(:user) }
+ let(:maintainer) { create(:user) }
let(:personal_project) do
create(:project, namespace: developer.namespace)
@@ -288,11 +288,11 @@ describe ProjectTeam do
let(:shared_project) { create(:project) }
before do
- group.add_master(master)
+ group.add_maintainer(maintainer)
group.add_developer(developer)
members_project.add_developer(developer)
- members_project.add_master(master)
+ members_project.add_maintainer(maintainer)
create(:project_group_link, project: shared_project, group: group)
end
@@ -318,14 +318,14 @@ describe ProjectTeam do
end
it 'checks for the correct minimum level access' do
- expect(group_project.team.member?(developer, Gitlab::Access::MASTER)).to be(false)
- expect(group_project.team.member?(master, Gitlab::Access::MASTER)).to be(true)
- expect(members_project.team.member?(developer, Gitlab::Access::MASTER)).to be(false)
- expect(members_project.team.member?(master, Gitlab::Access::MASTER)).to be(true)
- expect(shared_project.team.member?(developer, Gitlab::Access::MASTER)).to be(false)
- expect(shared_project.team.member?(master, Gitlab::Access::MASTER)).to be(false)
+ expect(group_project.team.member?(developer, Gitlab::Access::MAINTAINER)).to be(false)
+ expect(group_project.team.member?(maintainer, Gitlab::Access::MAINTAINER)).to be(true)
+ expect(members_project.team.member?(developer, Gitlab::Access::MAINTAINER)).to be(false)
+ expect(members_project.team.member?(maintainer, Gitlab::Access::MAINTAINER)).to be(true)
+ expect(shared_project.team.member?(developer, Gitlab::Access::MAINTAINER)).to be(false)
+ expect(shared_project.team.member?(maintainer, Gitlab::Access::MAINTAINER)).to be(false)
expect(shared_project.team.member?(developer, Gitlab::Access::DEVELOPER)).to be(true)
- expect(shared_project.team.member?(master, Gitlab::Access::DEVELOPER)).to be(true)
+ expect(shared_project.team.member?(maintainer, Gitlab::Access::DEVELOPER)).to be(true)
end
end
@@ -334,7 +334,7 @@ describe ProjectTeam do
let(:group) { create(:group) }
let(:second_group) { create(:group) }
- let(:master) { create(:user) }
+ let(:maintainer) { create(:user) }
let(:reporter) { create(:user) }
let(:guest) { create(:user) }
@@ -347,23 +347,23 @@ describe ProjectTeam do
let(:second_user_without_access) { create(:user) }
let(:users) do
- [master, reporter, promoted_guest, guest, group_developer, second_developer, user_without_access].map(&:id)
+ [maintainer, reporter, promoted_guest, guest, group_developer, second_developer, user_without_access].map(&:id)
end
let(:expected) do
{
- master.id => Gitlab::Access::MASTER,
+ maintainer.id => Gitlab::Access::MAINTAINER,
reporter.id => Gitlab::Access::REPORTER,
promoted_guest.id => Gitlab::Access::DEVELOPER,
guest.id => Gitlab::Access::GUEST,
group_developer.id => Gitlab::Access::DEVELOPER,
- second_developer.id => Gitlab::Access::MASTER,
+ second_developer.id => Gitlab::Access::MAINTAINER,
user_without_access.id => Gitlab::Access::NO_ACCESS
}
end
before do
- project.add_master(master)
+ project.add_maintainer(maintainer)
project.add_reporter(reporter)
project.add_guest(promoted_guest)
project.add_guest(guest)
@@ -373,16 +373,16 @@ describe ProjectTeam do
group_access: Gitlab::Access::DEVELOPER
)
- group.add_master(promoted_guest)
+ group.add_maintainer(promoted_guest)
group.add_developer(group_developer)
group.add_developer(second_developer)
project.project_group_links.create(
group: second_group,
- group_access: Gitlab::Access::MASTER
+ group_access: Gitlab::Access::MAINTAINER
)
- second_group.add_master(second_developer)
+ second_group.add_maintainer(second_developer)
end
it 'returns correct roles for different users' do
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index f1142832f1a..a3c20b3b3c1 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -188,7 +188,11 @@ describe ProjectWiki do
before do
subject.wiki # Make sure the wiki repo exists
- BareRepoOperations.new(subject.repository.path_to_repo).commit_file(image, 'image.png')
+ repo_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ subject.repository.path_to_repo
+ end
+
+ BareRepoOperations.new(repo_path).commit_file(image, 'image.png')
end
it 'returns the latest version of the file if it exists' do
diff --git a/spec/models/protected_branch/merge_access_level_spec.rb b/spec/models/protected_branch/merge_access_level_spec.rb
index f70503eadbc..612e4a0e332 100644
--- a/spec/models/protected_branch/merge_access_level_spec.rb
+++ b/spec/models/protected_branch/merge_access_level_spec.rb
@@ -1,5 +1,5 @@
require 'spec_helper'
describe ProtectedBranch::MergeAccessLevel do
- it { is_expected.to validate_inclusion_of(:access_level).in_array([Gitlab::Access::MASTER, Gitlab::Access::DEVELOPER, Gitlab::Access::NO_ACCESS]) }
+ it { is_expected.to validate_inclusion_of(:access_level).in_array([Gitlab::Access::MAINTAINER, Gitlab::Access::DEVELOPER, Gitlab::Access::NO_ACCESS]) }
end
diff --git a/spec/models/protected_branch/push_access_level_spec.rb b/spec/models/protected_branch/push_access_level_spec.rb
index f161f345761..9ccdc22fd41 100644
--- a/spec/models/protected_branch/push_access_level_spec.rb
+++ b/spec/models/protected_branch/push_access_level_spec.rb
@@ -1,5 +1,5 @@
require 'spec_helper'
describe ProtectedBranch::PushAccessLevel do
- it { is_expected.to validate_inclusion_of(:access_level).in_array([Gitlab::Access::MASTER, Gitlab::Access::DEVELOPER, Gitlab::Access::NO_ACCESS]) }
+ it { is_expected.to validate_inclusion_of(:access_level).in_array([Gitlab::Access::MAINTAINER, Gitlab::Access::DEVELOPER, Gitlab::Access::NO_ACCESS]) }
end
diff --git a/spec/models/remote_mirror_spec.rb b/spec/models/remote_mirror_spec.rb
index a80800c6c92..c2ef0435c8e 100644
--- a/spec/models/remote_mirror_spec.rb
+++ b/spec/models/remote_mirror_spec.rb
@@ -12,8 +12,15 @@ describe RemoteMirror do
context 'with an invalid URL' do
it 'should not be valid' do
remote_mirror = build(:remote_mirror, url: 'ftp://invalid.invalid')
+
expect(remote_mirror).not_to be_valid
- expect(remote_mirror.errors[:url].size).to eq(2)
+ end
+
+ it 'does not allow url with an invalid user' do
+ remote_mirror = build(:remote_mirror, url: 'http://$user:password@invalid.invalid')
+
+ expect(remote_mirror).to be_invalid
+ expect(remote_mirror.errors[:url].first).to include('Username needs to start with an alphanumeric character')
end
end
end
@@ -67,7 +74,9 @@ describe RemoteMirror do
mirror.update_attribute(:url, 'http://foo:baz@test.com')
- config = repo.raw_repository.rugged.config
+ config = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ repo.raw_repository.rugged.config
+ end
expect(config["remote.#{mirror.remote_name}.url"]).to eq('http://foo:baz@test.com')
end
@@ -76,7 +85,7 @@ describe RemoteMirror do
expect(RepositoryRemoveRemoteWorker).to receive(:perform_async).with(mirror.project.id, mirror.remote_name).and_call_original
- mirror.update_attributes(url: 'http://test.com')
+ mirror.update(url: 'http://test.com')
end
end
end
@@ -158,7 +167,7 @@ describe RemoteMirror do
context 'with remote mirroring disabled' do
it 'returns nil' do
- remote_mirror.update_attributes(enabled: false)
+ remote_mirror.update(enabled: false)
expect(remote_mirror.sync).to be_nil
end
@@ -220,7 +229,7 @@ describe RemoteMirror do
end
before do
- remote_mirror.update_attributes(last_update_started_at: Time.now)
+ remote_mirror.update(last_update_started_at: Time.now)
end
context 'when remote mirror does not have status failed' do
@@ -235,7 +244,7 @@ describe RemoteMirror do
context 'when remote mirror has status failed' do
it 'returns false when last update started after the timestamp' do
- remote_mirror.update_attributes(update_status: 'failed')
+ remote_mirror.update(update_status: 'failed')
expect(remote_mirror.updated_since?(timestamp)).to be false
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 0ccf55bd895..caf5d829d21 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -46,7 +46,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 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)
@@ -136,7 +136,10 @@ describe Repository do
before do
options = { message: 'test tag message\n',
tagger: { name: 'John Smith', email: 'john@gmail.com' } }
- repository.rugged.tags.create(annotated_tag_name, 'a48e4fc218069f68ef2e769dd8dfea3991362175', options)
+
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ repository.rugged.tags.create(annotated_tag_name, 'a48e4fc218069f68ef2e769dd8dfea3991362175', options)
+ end
double_first = double(committed_date: Time.now - 1.second)
double_last = double(committed_date: Time.now)
@@ -189,7 +192,7 @@ describe Repository do
it { is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') }
- describe 'when storage is broken', :broken_storage 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')
@@ -223,7 +226,7 @@ describe Repository do
is_expected.to eq('c1acaa5')
end
- describe 'when storage is broken', :broken_storage 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
@@ -388,7 +391,7 @@ describe Repository do
it_behaves_like 'finding commits by message'
end
- describe 'when storage is broken', :broken_storage 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
@@ -428,47 +431,43 @@ describe Repository do
it { is_expected.to be_falsey }
end
- end
- describe '#can_be_merged?' do
- shared_examples 'can be merged' do
- context 'mergeable branches' do
- subject { repository.can_be_merged?('0b4bc9a49b562e85de7cc9e834518ea6828729b9', 'master') }
+ context 'non merged branch' do
+ subject { repository.merged_to_root_ref?('fix') }
- it { is_expected.to be_truthy }
- end
+ it { is_expected.to be_falsey }
+ end
- context 'non-mergeable branches without conflict sides missing' do
- subject { repository.can_be_merged?('bb5206fee213d983da88c47f9cf4cc6caf9c66dc', 'feature') }
+ context 'non existent branch' do
+ subject { repository.merged_to_root_ref?('non_existent_branch') }
- it { is_expected.to be_falsey }
- end
+ it { is_expected.to be_nil }
+ end
+ end
- context 'non-mergeable branches with conflict sides missing' do
- subject { repository.can_be_merged?('conflict-missing-side', 'conflict-start') }
+ describe '#can_be_merged?' do
+ context 'mergeable branches' do
+ subject { repository.can_be_merged?('0b4bc9a49b562e85de7cc9e834518ea6828729b9', 'master') }
- it { is_expected.to be_falsey }
- end
+ it { is_expected.to be_truthy }
+ end
- context 'non merged branch' do
- subject { repository.merged_to_root_ref?('fix') }
+ context 'non-mergeable branches without conflict sides missing' do
+ subject { repository.can_be_merged?('bb5206fee213d983da88c47f9cf4cc6caf9c66dc', 'feature') }
- it { is_expected.to be_falsey }
- end
+ it { is_expected.to be_falsey }
+ end
- context 'non existent branch' do
- subject { repository.merged_to_root_ref?('non_existent_branch') }
+ context 'non-mergeable branches with conflict sides missing' do
+ subject { repository.can_be_merged?('conflict-missing-side', 'conflict-start') }
- it { is_expected.to be_nil }
- end
+ it { is_expected.to be_falsey }
end
- context 'when Gitaly can_be_merged feature is enabled' do
- it_behaves_like 'can be merged'
- end
+ context 'submodule changes that confuse rugged' do
+ subject { repository.can_be_merged?('update-gitlab-shell-v-6-0-1', 'update-gitlab-shell-v-6-0-3') }
- context 'when Gitaly can_be_merged feature is disabled', :disable_gitaly do
- it_behaves_like 'can be merged'
+ it { is_expected.to be_falsey }
end
end
@@ -486,6 +485,14 @@ describe Repository do
end
end
+ context 'when ref is not specified' do
+ it 'is using a root ref' do
+ expect(repository).to receive(:find_commit).with('master')
+
+ repository.commit
+ end
+ end
+
context 'when ref is not valid' do
context 'when preceding tree element exists' do
it 'returns nil' do
@@ -671,7 +678,7 @@ describe Repository do
end
end
- shared_examples "search_files_by_content" do
+ describe "search_files_by_content" do
let(:results) { repository.search_files_by_content('feature', 'master') }
subject { results }
@@ -702,7 +709,7 @@ describe Repository do
expect(results).to match_array([])
end
- describe 'when storage is broken', :broken_storage 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')
@@ -718,7 +725,7 @@ describe Repository do
end
end
- shared_examples "search_files_by_name" do
+ describe "search_files_by_name" do
let(:results) { repository.search_files_by_name('files', 'master') }
it 'returns result' do
@@ -751,23 +758,13 @@ describe Repository do
expect(results).to match_array([])
end
- describe 'when storage is broken', :broken_storage 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
end
end
- describe 'with gitaly enabled' do
- it_behaves_like 'search_files_by_content'
- it_behaves_like 'search_files_by_name'
- end
-
- describe 'with gitaly disabled', :disable_gitaly do
- it_behaves_like 'search_files_by_content'
- it_behaves_like 'search_files_by_name'
- end
-
describe '#async_remove_remote' do
before do
masterrev = repository.find_branch('master').dereferenced_target
@@ -803,7 +800,7 @@ describe Repository do
describe '#fetch_ref' do
let(:broken_repository) { create(:project, :broken_storage).repository }
- describe 'when storage is broken', :broken_storage 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')
@@ -1048,6 +1045,13 @@ describe Repository do
let(:target_project) { project }
let(:target_repository) { target_project.repository }
+ around do |example|
+ # TODO Gitlab::Git::OperationService will be moved to gitaly-ruby and disappear from this repo
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ example.run
+ end
+ end
+
context 'when pre hooks were successful' do
before do
service = Gitlab::Git::HooksService.new
@@ -1185,7 +1189,7 @@ describe Repository do
Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch('feature') do
new_rev
end
- end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
+ end.to raise_error(Gitlab::Git::PreReceiveError)
end
end
@@ -1309,6 +1313,13 @@ describe Repository do
end
describe '#update_autocrlf_option' do
+ around do |example|
+ # TODO Gitlab::Git::OperationService will be moved to gitaly-ruby and disappear from this repo
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ example.run
+ end
+ end
+
describe 'when autocrlf is not already set to :input' do
before do
repository.raw_repository.autocrlf = true
@@ -1692,19 +1703,29 @@ describe Repository do
end
describe '#after_change_head' do
- it 'flushes the readme cache' do
+ it 'flushes the method caches' do
expect(repository).to receive(:expire_method_caches).with([
- :readme,
+ :size,
+ :commit_count,
+ :rendered_readme,
+ :contribution_guide,
:changelog,
- :license,
- :contributing,
+ :license_blob,
+ :license_key,
:gitignore,
- :koding,
- :gitlab_ci,
+ :koding_yml,
+ :gitlab_ci_yml,
+ :branch_names,
+ :tag_names,
+ :branch_count,
+ :tag_count,
:avatar,
- :issue_template,
- :merge_request_template,
- :xcode_config
+ :exists?,
+ :root_ref,
+ :has_visible_content?,
+ :issue_template_names,
+ :merge_request_template_names,
+ :xcode_project?
])
repository.after_change_head
@@ -1802,7 +1823,9 @@ describe Repository do
expect(repository.branch_count).to be_an(Integer)
# NOTE: Until rugged goes away, make sure rugged and gitaly are in sync
- rugged_count = repository.raw_repository.rugged.branches.count
+ rugged_count = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ repository.raw_repository.rugged.branches.count
+ end
expect(repository.branch_count).to eq(rugged_count)
end
@@ -1813,7 +1836,9 @@ describe Repository do
expect(repository.tag_count).to be_an(Integer)
# NOTE: Until rugged goes away, make sure rugged and gitaly are in sync
- rugged_count = repository.raw_repository.rugged.tags.count
+ rugged_count = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ repository.raw_repository.rugged.tags.count
+ end
expect(repository.tag_count).to eq(rugged_count)
end
@@ -1842,155 +1867,61 @@ describe Repository do
describe '#add_tag' do
let(:user) { build_stubbed(:user) }
- shared_examples 'adding tag' do
- context 'with a valid target' do
- it 'creates the tag' do
- repository.add_tag(user, '8.5', 'master', 'foo')
-
- tag = repository.find_tag('8.5')
- expect(tag).to be_present
- expect(tag.message).to eq('foo')
- expect(tag.dereferenced_target.id).to eq(repository.commit('master').id)
- end
-
- it 'returns a Gitlab::Git::Tag object' do
- tag = repository.add_tag(user, '8.5', 'master', 'foo')
-
- expect(tag).to be_a(Gitlab::Git::Tag)
- end
- end
+ context 'with a valid target' do
+ it 'creates the tag' do
+ repository.add_tag(user, '8.5', 'master', 'foo')
- context 'with an invalid target' do
- it 'returns false' do
- expect(repository.add_tag(user, '8.5', 'bar', 'foo')).to be false
- end
+ tag = repository.find_tag('8.5')
+ expect(tag).to be_present
+ expect(tag.message).to eq('foo')
+ expect(tag.dereferenced_target.id).to eq(repository.commit('master').id)
end
- end
-
- context 'when Gitaly operation_user_add_tag feature is enabled' do
- it_behaves_like 'adding tag'
- end
-
- context 'when Gitaly operation_user_add_tag feature is disabled', :disable_gitaly do
- it_behaves_like 'adding tag'
-
- it 'passes commit SHA to pre-receive and update hooks and tag SHA to post-receive hook' do
- pre_receive_hook = Gitlab::Git::Hook.new('pre-receive', project)
- update_hook = Gitlab::Git::Hook.new('update', project)
- post_receive_hook = Gitlab::Git::Hook.new('post-receive', project)
-
- allow(Gitlab::Git::Hook).to receive(:new)
- .and_return(pre_receive_hook, update_hook, post_receive_hook)
-
- allow(pre_receive_hook).to receive(:trigger).and_call_original
- allow(update_hook).to receive(:trigger).and_call_original
- allow(post_receive_hook).to receive(:trigger).and_call_original
+ it 'returns a Gitlab::Git::Tag object' do
tag = repository.add_tag(user, '8.5', 'master', 'foo')
- commit_sha = repository.commit('master').id
- tag_sha = tag.target
-
- expect(pre_receive_hook).to have_received(:trigger)
- .with(anything, anything, anything, commit_sha, anything)
- expect(update_hook).to have_received(:trigger)
- .with(anything, anything, anything, commit_sha, anything)
- expect(post_receive_hook).to have_received(:trigger)
- .with(anything, anything, anything, tag_sha, anything)
+ expect(tag).to be_a(Gitlab::Git::Tag)
end
end
- end
- describe '#rm_branch' do
- shared_examples "user deleting a branch" do
- it 'removes a branch' do
- expect(repository).to receive(:before_remove_branch)
- expect(repository).to receive(:after_remove_branch)
-
- repository.rm_branch(user, 'feature')
+ context 'with an invalid target' do
+ it 'returns false' do
+ expect(repository.add_tag(user, '8.5', 'bar', 'foo')).to be false
end
end
+ end
- context 'with gitaly enabled' do
- it_behaves_like "user deleting a branch"
-
- context 'when pre hooks failed' do
- before do
- allow_any_instance_of(Gitlab::GitalyClient::OperationService)
- .to receive(:user_delete_branch).and_raise(Gitlab::Git::HooksService::PreReceiveError)
- end
-
- it 'gets an error and does not delete the branch' do
- expect do
- repository.rm_branch(user, 'feature')
- end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
+ describe '#rm_branch' do
+ it 'removes a branch' do
+ expect(repository).to receive(:before_remove_branch)
+ expect(repository).to receive(:after_remove_branch)
- expect(repository.find_branch('feature')).not_to be_nil
- end
- end
+ repository.rm_branch(user, 'feature')
end
- context 'with gitaly disabled', :disable_gitaly do
- it_behaves_like "user deleting a branch"
-
- let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature
- let(:blank_sha) { '0000000000000000000000000000000000000000' }
-
- context 'when pre hooks were successful' do
- it 'runs without errors' do
- expect_any_instance_of(Gitlab::Git::HooksService).to receive(:execute)
- .with(git_user, repository.raw_repository, old_rev, blank_sha, 'refs/heads/feature')
-
- expect { repository.rm_branch(user, 'feature') }.not_to raise_error
- end
-
- it 'deletes the branch' do
- allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil])
-
- expect { repository.rm_branch(user, 'feature') }.not_to raise_error
-
- expect(repository.find_branch('feature')).to be_nil
- end
+ context 'when pre hooks failed' do
+ before do
+ allow_any_instance_of(Gitlab::GitalyClient::OperationService)
+ .to receive(:user_delete_branch).and_raise(Gitlab::Git::PreReceiveError)
end
- context 'when pre hooks failed' do
- it 'gets an error' do
- allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
-
- expect do
- repository.rm_branch(user, 'feature')
- end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
- end
-
- it 'does not delete the branch' do
- allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
+ it 'gets an error and does not delete the branch' do
+ expect do
+ repository.rm_branch(user, 'feature')
+ end.to raise_error(Gitlab::Git::PreReceiveError)
- expect do
- repository.rm_branch(user, 'feature')
- end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
- expect(repository.find_branch('feature')).not_to be_nil
- end
+ expect(repository.find_branch('feature')).not_to be_nil
end
end
end
describe '#rm_tag' do
- shared_examples 'removing tag' do
- it 'removes a tag' do
- expect(repository).to receive(:before_remove_tag)
-
- repository.rm_tag(build_stubbed(:user), 'v1.1.0')
+ it 'removes a tag' do
+ expect(repository).to receive(:before_remove_tag)
- expect(repository.find_tag('v1.1.0')).to be_nil
- end
- end
+ repository.rm_tag(build_stubbed(:user), 'v1.1.0')
- context 'when Gitaly operation_user_delete_tag feature is enabled' do
- it_behaves_like 'removing tag'
- end
-
- context 'when Gitaly operation_user_delete_tag feature is disabled', :skip_gitaly_mock do
- it_behaves_like 'removing tag'
+ expect(repository.find_tag('v1.1.0')).to be_nil
end
end
@@ -2073,7 +2004,10 @@ describe Repository do
it "attempting to call keep_around on truncated ref does not fail" do
repository.keep_around(sample_commit.id)
ref = repository.send(:keep_around_ref_name, sample_commit.id)
- path = File.join(repository.path, ref)
+
+ path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ File.join(repository.path, ref)
+ end
# Corrupt the reference
File.truncate(path, 0)
@@ -2088,6 +2022,13 @@ describe Repository do
end
describe '#update_ref' do
+ around do |example|
+ # TODO Gitlab::Git::OperationService will be moved to gitaly-ruby and disappear from this repo
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ example.run
+ end
+ end
+
it 'can create a ref' do
Gitlab::Git::OperationService.new(nil, repository.raw_repository).send(:update_ref, 'refs/heads/foobar', 'refs/heads/master', Gitlab::Git::BLANK_SHA)
@@ -2273,6 +2214,28 @@ describe Repository do
end
end
+ describe '#local_branches' do
+ it 'returns the local branches' do
+ masterrev = repository.find_branch('master').dereferenced_target
+ create_remote_branch('joe', 'remote_branch', masterrev)
+ repository.add_branch(user, 'local_branch', masterrev.id)
+
+ expect(repository.local_branches.any? { |branch| branch.name == 'remote_branch' }).to eq(false)
+ expect(repository.local_branches.any? { |branch| branch.name == 'local_branch' }).to eq(true)
+ end
+ end
+
+ describe '#remote_branches' do
+ it 'returns the remote branches' do
+ masterrev = repository.find_branch('master').dereferenced_target
+ create_remote_branch('joe', 'remote_branch', masterrev)
+ repository.add_branch(user, 'local_branch', masterrev.id)
+
+ expect(repository.remote_branches('joe').any? { |branch| branch.name == 'local_branch' }).to eq(false)
+ expect(repository.remote_branches('joe').any? { |branch| branch.name == 'remote_branch' }).to eq(true)
+ end
+ end
+
describe '#commit_count' do
context 'with a non-existing repository' do
it 'returns 0' do
@@ -2372,7 +2335,9 @@ describe Repository do
end
def create_remote_branch(remote_name, branch_name, target)
- rugged = repository.rugged
+ rugged = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ repository.rugged
+ end
rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", target.id)
end
@@ -2410,6 +2375,32 @@ describe Repository do
end
end
+ describe '#archive_metadata' do
+ let(:ref) { 'master' }
+ let(:storage_path) { '/tmp' }
+
+ let(:prefix) { [project.path, ref].join('-') }
+ let(:filename) { prefix + '.tar.gz' }
+
+ subject(:result) { repository.archive_metadata(ref, storage_path, append_sha: false) }
+
+ context 'with hashed storage disabled' do
+ let(:project) { create(:project, :repository, :legacy_storage) }
+
+ it 'uses the project path to generate the filename' do
+ expect(result['ArchivePrefix']).to eq(prefix)
+ expect(File.basename(result['ArchivePath'])).to eq(filename)
+ end
+ end
+
+ context 'with hashed storage enabled' do
+ it 'uses the project path to generate the filename' do
+ expect(result['ArchivePrefix']).to eq(prefix)
+ expect(File.basename(result['ArchivePath'])).to eq(filename)
+ end
+ end
+ end
+
describe 'commit cache' do
set(:project) { create(:project, :repository) }
diff --git a/spec/models/route_spec.rb b/spec/models/route_spec.rb
index 01238a89a81..48799781b87 100644
--- a/spec/models/route_spec.rb
+++ b/spec/models/route_spec.rb
@@ -29,12 +29,12 @@ describe Route do
context 'after update' do
it 'calls #create_redirect_for_old_path' do
expect(route).to receive(:create_redirect_for_old_path)
- route.update_attributes(path: 'foo')
+ route.update(path: 'foo')
end
it 'calls #delete_conflicting_redirects' do
expect(route).to receive(:delete_conflicting_redirects)
- route.update_attributes(path: 'foo')
+ route.update(path: 'foo')
end
end
@@ -70,7 +70,7 @@ describe Route do
context 'path update' do
context 'when route name is set' do
before do
- route.update_attributes(path: 'bar')
+ route.update(path: 'bar')
end
it 'updates children routes with new path' do
@@ -89,7 +89,7 @@ describe Route do
end
it "does not fail" do
- expect(route.update_attributes(path: 'bar')).to be_truthy
+ expect(route.update(path: 'bar')).to be_truthy
end
end
@@ -100,7 +100,7 @@ describe Route do
let!(:conflicting_redirect3) { route.create_redirect('gitlab-org') }
it 'deletes the conflicting redirects' do
- route.update_attributes(path: 'bar')
+ route.update(path: 'bar')
expect(RedirectRoute.exists?(path: 'bar/test')).to be_falsey
expect(RedirectRoute.exists?(path: 'bar/test/foo')).to be_falsey
@@ -111,7 +111,7 @@ describe Route do
context 'name update' do
it 'updates children routes with new path' do
- route.update_attributes(name: 'bar')
+ route.update(name: 'bar')
expect(described_class.exists?(name: 'bar')).to be_truthy
expect(described_class.exists?(name: 'bar / test')).to be_truthy
@@ -123,7 +123,7 @@ describe Route do
# Note: using `update_columns` to skip all validation and callbacks
route.update_columns(name: nil)
- expect { route.update_attributes(name: 'bar') }
+ expect { route.update(name: 'bar') }
.to change { route.name }.from(nil).to('bar')
end
end
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index 28c908ea425..029ad7f3e9f 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -78,7 +78,7 @@ describe Service do
context 'when template is invalid' do
it 'sets service template to inactive when template is invalid' do
project = create(:project)
- template = JiraService.new(template: true, active: true)
+ template = KubernetesService.new(template: true, active: true)
template.save(validate: false)
service = described_class.build_from_template(project.id, template)
@@ -280,7 +280,7 @@ describe Service do
service.save!
expect do
- service.update_attributes(active: false)
+ service.update(active: false)
end.to change { service.project.has_external_issue_tracker }.from(true).to(false)
end
end
diff --git a/spec/models/term_agreement_spec.rb b/spec/models/term_agreement_spec.rb
index a59bf119692..950dfa09a6a 100644
--- a/spec/models/term_agreement_spec.rb
+++ b/spec/models/term_agreement_spec.rb
@@ -5,4 +5,13 @@ describe TermAgreement do
it { is_expected.to validate_presence_of(:term) }
it { is_expected.to validate_presence_of(:user) }
end
+
+ describe '.accepted' do
+ it 'only includes accepted terms' do
+ accepted = create(:term_agreement, :accepted)
+ create(:term_agreement, :declined)
+
+ expect(described_class.accepted).to contain_exactly(accepted)
+ end
+ end
end
diff --git a/spec/models/timelog_spec.rb b/spec/models/timelog_spec.rb
index 6e30798356c..a0c93c531ea 100644
--- a/spec/models/timelog_spec.rb
+++ b/spec/models/timelog_spec.rb
@@ -5,6 +5,9 @@ RSpec.describe Timelog do
let(:issue) { create(:issue) }
let(:merge_request) { create(:merge_request) }
+ it { is_expected.to belong_to(:issue).touch(true) }
+ it { is_expected.to belong_to(:merge_request).touch(true) }
+
it { is_expected.to be_valid }
it { is_expected.to validate_presence_of(:time_spent) }
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 6a2f4a39f09..fc46551c3be 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -383,7 +383,7 @@ describe User do
let(:secondary) { create(:email, :confirmed, email: 'secondary@example.com', user: user) }
it 'allows a verfied secondary email to be used as the primary without needing reconfirmation' do
- user.update_attributes!(email: secondary.email)
+ user.update!(email: secondary.email)
user.reload
expect(user.email).to eq secondary.email
expect(user.unconfirmed_email).to eq nil
@@ -405,11 +405,11 @@ describe User do
it 'gets called when email updated' do
expect(@user).to receive(:update_emails_with_primary_email)
- @user.update_attributes!(email: 'new_primary@example.com')
+ @user.update!(email: 'new_primary@example.com')
end
it 'adds old primary to secondary emails when secondary is a new email ' do
- @user.update_attributes!(email: 'new_primary@example.com')
+ @user.update!(email: 'new_primary@example.com')
@user.reload
expect(@user.emails.count).to eq 2
@@ -417,7 +417,7 @@ describe User do
end
it 'adds old primary to secondary emails if secondary is becoming a primary' do
- @user.update_attributes!(email: @secondary.email)
+ @user.update!(email: @secondary.email)
@user.reload
expect(@user.emails.count).to eq 1
@@ -425,7 +425,7 @@ describe User do
end
it 'transfers old confirmation values into new secondary' do
- @user.update_attributes!(email: @secondary.email)
+ @user.update!(email: @secondary.email)
@user.reload
expect(@user.emails.count).to eq 1
@@ -494,12 +494,12 @@ describe User do
it 'does nothing when the name is updated' do
expect(user).not_to receive(:update_invalid_gpg_signatures)
- user.update_attributes!(name: 'Bette')
+ user.update!(name: 'Bette')
end
it 'synchronizes the gpg keys when the email is updated' do
expect(user).to receive(:update_invalid_gpg_signatures).at_most(:twice)
- user.update_attributes!(email: 'shawnee.ritchie@denesik.com')
+ user.update!(email: 'shawnee.ritchie@denesik.com')
end
end
end
@@ -617,13 +617,13 @@ describe User do
it 'receives callback when external changes' do
expect(user).to receive(:ensure_user_rights_and_limits)
- user.update_attributes(external: false)
+ user.update(external: false)
end
it 'ensures correct rights and limits for user' do
stub_config_setting(default_can_create_group: true)
- expect { user.update_attributes(external: false) }.to change { user.can_create_group }.to(true)
+ expect { user.update(external: false) }.to change { user.can_create_group }.to(true)
.and change { user.projects_limit }.to(Gitlab::CurrentSettings.default_projects_limit)
end
end
@@ -634,23 +634,23 @@ describe User do
it 'receives callback when external changes' do
expect(user).to receive(:ensure_user_rights_and_limits)
- user.update_attributes(external: true)
+ user.update(external: true)
end
it 'ensures correct rights and limits for user' do
- expect { user.update_attributes(external: true) }.to change { user.can_create_group }.to(false)
+ expect { user.update(external: true) }.to change { user.can_create_group }.to(false)
.and change { user.projects_limit }.to(0)
end
end
end
- describe 'rss token' do
- it 'ensures an rss token on read' do
- user = create(:user, rss_token: nil)
- rss_token = user.rss_token
+ describe 'feed token' do
+ it 'ensures a feed token on read' do
+ user = create(:user, feed_token: nil)
+ feed_token = user.feed_token
- expect(rss_token).not_to be_blank
- expect(user.reload.rss_token).to eq rss_token
+ expect(feed_token).not_to be_blank
+ expect(user.reload.feed_token).to eq feed_token
end
end
@@ -700,7 +700,7 @@ describe User do
@project = create(:project, namespace: @user.namespace)
@project_2 = create(:project, group: create(:group)) do |project|
- project.add_master(@user)
+ project.add_maintainer(@user)
end
@project_3 = create(:project, group: create(:group)) do |project|
project.add_developer(@user)
@@ -836,7 +836,7 @@ describe User do
before do
# add user to project
- project.add_master(user)
+ project.add_maintainer(user)
# create invite to projet
create(:project_member, :developer, project: project, invite_token: '1234', invite_email: 'inviteduser1@example.com')
@@ -1260,7 +1260,7 @@ describe User do
it 'is false if avatar is html page' do
user.update_attribute(:avatar, 'uploads/avatar.html')
- expect(user.avatar_type).to eq(['file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff'])
+ expect(user.avatar_type).to eq(['file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff, ico'])
end
end
@@ -1581,8 +1581,8 @@ describe User do
let!(:merge_event) { create(:event, :created, project: project3, target: merge_request, author: subject) }
before do
- project1.add_master(subject)
- project2.add_master(subject)
+ project1.add_maintainer(subject)
+ project2.add_maintainer(subject)
end
it "includes IDs for projects the user has pushed to" do
@@ -1663,8 +1663,8 @@ describe User do
let!(:project) { create(:project, group: project_group) }
before do
- private_group.add_user(user, Gitlab::Access::MASTER)
- project.add_master(user)
+ private_group.add_user(user, Gitlab::Access::MAINTAINER)
+ project.add_maintainer(user)
end
subject { user.authorized_groups }
@@ -1678,7 +1678,7 @@ describe User do
let!(:child_group) { create(:group, parent: parent_group) }
before do
- parent_group.add_user(user, Gitlab::Access::MASTER)
+ parent_group.add_user(user, Gitlab::Access::MAINTAINER)
end
subject { user.membership_groups }
@@ -1696,7 +1696,7 @@ describe User do
it 'includes projects that belong to a user, but no other projects' do
owned = create(:project, :private, namespace: user.namespace)
- member = create(:project, :private).tap { |p| p.add_master(user) }
+ member = create(:project, :private).tap { |p| p.add_maintainer(user) }
other = create(:project)
expect(subject).to include(owned)
@@ -1726,11 +1726,11 @@ describe User do
.to contain_exactly(project)
end
- it 'includes projects for which the user is a master' do
+ it 'includes projects for which the user is a maintainer' do
user = create(:user)
project = create(:project, :private)
- project.add_master(user)
+ project.add_maintainer(user)
expect(user.authorized_projects(Gitlab::Access::REPORTER))
.to contain_exactly(project)
@@ -1824,10 +1824,10 @@ describe User do
it 'includes projects for which the user access level is above or equal to reporter' do
reporter_project = create(:project) { |p| p.add_reporter(user) }
developer_project = create(:project) { |p| p.add_developer(user) }
- master_project = create(:project) { |p| p.add_master(user) }
+ maintainer_project = create(:project) { |p| p.add_maintainer(user) }
- expect(user.projects_where_can_admin_issues.to_a).to match_array([master_project, developer_project, reporter_project])
- expect(user.can?(:admin_issue, master_project)).to eq(true)
+ expect(user.projects_where_can_admin_issues.to_a).to match_array([maintainer_project, developer_project, reporter_project])
+ expect(user.can?(:admin_issue, maintainer_project)).to eq(true)
expect(user.can?(:admin_issue, developer_project)).to eq(true)
expect(user.can?(:admin_issue, reporter_project)).to eq(true)
end
@@ -1858,13 +1858,10 @@ describe User do
describe '#ci_owned_runners' do
let(:user) { create(:user) }
- let(:runner_1) { create(:ci_runner) }
- let(:runner_2) { create(:ci_runner) }
+ let!(:project) { create(:project) }
+ let(:runner) { create(:ci_runner, :project, projects: [project]) }
context 'without any projects nor groups' do
- let!(:project) { create(:project, runners: [runner_1]) }
- let!(:group) { create(:group) }
-
it 'does not load' do
expect(user.ci_owned_runners).to be_empty
end
@@ -1872,49 +1869,51 @@ describe User do
context 'with personal projects runners' do
let(:namespace) { create(:namespace, owner: user) }
- let!(:project) { create(:project, namespace: namespace, runners: [runner_1]) }
+ let!(:project) { create(:project, namespace: namespace) }
it 'loads' do
- expect(user.ci_owned_runners).to contain_exactly(runner_1)
+ expect(user.ci_owned_runners).to contain_exactly(runner)
end
end
context 'with personal group runner' do
- let!(:project) { create(:project, runners: [runner_1]) }
+ let!(:project) { create(:project) }
+ let(:group_runner) { create(:ci_runner, :group, groups: [group]) }
let!(:group) do
- create(:group, runners: [runner_2]).tap do |group|
+ create(:group).tap do |group|
group.add_owner(user)
end
end
it 'loads' do
- expect(user.ci_owned_runners).to contain_exactly(runner_2)
+ expect(user.ci_owned_runners).to contain_exactly(group_runner)
end
end
context 'with personal project and group runner' do
let(:namespace) { create(:namespace, owner: user) }
- let!(:project) { create(:project, namespace: namespace, runners: [runner_1]) }
+ let!(:project) { create(:project, namespace: namespace) }
+ let!(:group_runner) { create(:ci_runner, :group, groups: [group]) }
let!(:group) do
- create(:group, runners: [runner_2]).tap do |group|
+ create(:group).tap do |group|
group.add_owner(user)
end
end
it 'loads' do
- expect(user.ci_owned_runners).to contain_exactly(runner_1, runner_2)
+ expect(user.ci_owned_runners).to contain_exactly(runner, group_runner)
end
end
shared_examples :member do
- context 'when the user is a master' do
+ context 'when the user is a maintainer' do
before do
- add_user(:master)
+ add_user(:maintainer)
end
it 'loads' do
- expect(user.ci_owned_runners).to contain_exactly(runner_1)
+ expect(user.ci_owned_runners).to contain_exactly(runner)
end
end
@@ -1931,7 +1930,7 @@ describe User do
context 'with groups projects runners' do
let(:group) { create(:group) }
- let!(:project) { create(:project, group: group, runners: [runner_1]) }
+ let!(:project) { create(:project, group: group) }
def add_user(access)
group.add_user(user, access)
@@ -1941,11 +1940,8 @@ describe User do
end
context 'with groups runners' do
- let!(:group) do
- create(:group, runners: [runner_1]).tap do |group|
- group.add_owner(user)
- end
- end
+ let!(:runner) { create(:ci_runner, :group, groups: [group]) }
+ let!(:group) { create(:group) }
def add_user(access)
group.add_user(user, access)
@@ -1955,7 +1951,7 @@ describe User do
end
context 'with other projects runners' do
- let!(:project) { create(:project, runners: [runner_1]) }
+ let!(:project) { create(:project) }
def add_user(access)
project.add_role(user, access)
@@ -1968,7 +1964,7 @@ describe User do
let(:group) { create(:group) }
let(:another_user) { create(:user) }
let(:subgroup) { create(:group, parent: group) }
- let!(:project) { create(:project, group: subgroup, runners: [runner_1]) }
+ let!(:project) { create(:project, group: subgroup) }
def add_user(access)
group.add_user(user, access)
@@ -2465,18 +2461,20 @@ describe User do
it 'changes the namespace (just to compare to when username is not changed)' do
expect do
- user.update_attributes!(username: new_username)
+ Timecop.freeze(1.second.from_now) do
+ user.update!(username: new_username)
+ end
end.to change { user.namespace.updated_at }
end
it 'updates the namespace name' do
- user.update_attributes!(username: new_username)
+ user.update!(username: new_username)
expect(user.namespace.name).to eq(new_username)
end
it 'updates the namespace path' do
- user.update_attributes!(username: new_username)
+ user.update!(username: new_username)
expect(user.namespace.path).to eq(new_username)
end
@@ -2485,12 +2483,12 @@ describe User do
let!(:conflicting_namespace) { create(:group, path: new_username) }
it 'causes the user save to fail' do
- expect(user.update_attributes(username: new_username)).to be_falsey
+ expect(user.update(username: new_username)).to be_falsey
expect(user.namespace.errors.messages[:path].first).to eq('has already been taken')
end
it 'adds the namespace errors to the user' do
- user.update_attributes(username: new_username)
+ user.update(username: new_username)
expect(user.errors.full_messages.first).to eq('Username has already been taken')
end
@@ -2500,7 +2498,7 @@ describe User do
context 'when the username is not changed' do
it 'does not change the namespace' do
expect do
- user.update_attributes!(email: 'asdf@asdf.com')
+ user.update!(email: 'asdf@asdf.com')
end.not_to change { user.namespace.updated_at }
end
end
@@ -2530,7 +2528,7 @@ describe User do
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)
+ user.update!(username: new_username)
end
end
@@ -2538,7 +2536,7 @@ describe User do
it 'does not trigger system hook' do
expect(user).not_to receive(:system_hook_service)
- user.update_attributes!(email: 'asdf@asdf.com')
+ user.update!(email: 'asdf@asdf.com')
end
end
end
@@ -2670,20 +2668,20 @@ describe User do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:owner_project) { create(:project, group: group) }
- let(:master_project) { create(:project) }
+ let(:maintainer_project) { create(:project) }
let(:reporter_project) { create(:project) }
let(:developer_project) { create(:project) }
let(:guest_project) { create(:project) }
let(:no_access_project) { create(:project) }
let(:projects) do
- [owner_project, master_project, reporter_project, developer_project, guest_project, no_access_project].map(&:id)
+ [owner_project, maintainer_project, reporter_project, developer_project, guest_project, no_access_project].map(&:id)
end
let(:expected) do
{
owner_project.id => Gitlab::Access::OWNER,
- master_project.id => Gitlab::Access::MASTER,
+ maintainer_project.id => Gitlab::Access::MAINTAINER,
reporter_project.id => Gitlab::Access::REPORTER,
developer_project.id => Gitlab::Access::DEVELOPER,
guest_project.id => Gitlab::Access::GUEST,
@@ -2693,7 +2691,7 @@ describe User do
before do
create(:group_member, user: user, group: group)
- master_project.add_master(user)
+ maintainer_project.add_maintainer(user)
reporter_project.add_reporter(user)
developer_project.add_developer(user)
guest_project.add_guest(user)
@@ -2720,14 +2718,14 @@ describe User do
end
it 'only requests the extra projects when uncached projects are passed' do
- second_master_project = create(:project)
+ second_maintainer_project = create(:project)
second_developer_project = create(:project)
- second_master_project.add_master(user)
+ second_maintainer_project.add_maintainer(user)
second_developer_project.add_developer(user)
- all_projects = projects + [second_master_project.id, second_developer_project.id]
+ all_projects = projects + [second_maintainer_project.id, second_developer_project.id]
- expected_all = expected.merge(second_master_project.id => Gitlab::Access::MASTER,
+ expected_all = expected.merge(second_maintainer_project.id => Gitlab::Access::MAINTAINER,
second_developer_project.id => Gitlab::Access::DEVELOPER)
access_levels(projects)
@@ -2735,7 +2733,7 @@ describe User do
queries = ActiveRecord::QueryRecorder.new { access_levels(all_projects) }
expect(queries.count).to eq(1)
- expect(queries.log_message).to match(/\W(#{second_master_project.id}, #{second_developer_project.id})\W/)
+ expect(queries.log_message).to match(/\W(#{second_maintainer_project.id}, #{second_developer_project.id})\W/)
expect(access_levels(all_projects)).to eq(expected_all)
end
end
@@ -2749,20 +2747,20 @@ describe User do
shared_examples 'max member access for groups' do
let(:user) { create(:user) }
let(:owner_group) { create(:group) }
- let(:master_group) { create(:group) }
+ let(:maintainer_group) { create(:group) }
let(:reporter_group) { create(:group) }
let(:developer_group) { create(:group) }
let(:guest_group) { create(:group) }
let(:no_access_group) { create(:group) }
let(:groups) do
- [owner_group, master_group, reporter_group, developer_group, guest_group, no_access_group].map(&:id)
+ [owner_group, maintainer_group, reporter_group, developer_group, guest_group, no_access_group].map(&:id)
end
let(:expected) do
{
owner_group.id => Gitlab::Access::OWNER,
- master_group.id => Gitlab::Access::MASTER,
+ maintainer_group.id => Gitlab::Access::MAINTAINER,
reporter_group.id => Gitlab::Access::REPORTER,
developer_group.id => Gitlab::Access::DEVELOPER,
guest_group.id => Gitlab::Access::GUEST,
@@ -2772,7 +2770,7 @@ describe User do
before do
owner_group.add_owner(user)
- master_group.add_master(user)
+ maintainer_group.add_maintainer(user)
reporter_group.add_reporter(user)
developer_group.add_developer(user)
guest_group.add_guest(user)
@@ -2799,14 +2797,14 @@ describe User do
end
it 'only requests the extra groups when uncached groups are passed' do
- second_master_group = create(:group)
+ second_maintainer_group = create(:group)
second_developer_group = create(:group)
- second_master_group.add_master(user)
+ second_maintainer_group.add_maintainer(user)
second_developer_group.add_developer(user)
- all_groups = groups + [second_master_group.id, second_developer_group.id]
+ all_groups = groups + [second_maintainer_group.id, second_developer_group.id]
- expected_all = expected.merge(second_master_group.id => Gitlab::Access::MASTER,
+ expected_all = expected.merge(second_maintainer_group.id => Gitlab::Access::MAINTAINER,
second_developer_group.id => Gitlab::Access::DEVELOPER)
access_levels(groups)
@@ -2814,7 +2812,7 @@ describe User do
queries = ActiveRecord::QueryRecorder.new { access_levels(all_groups) }
expect(queries.count).to eq(1)
- expect(queries.log_message).to match(/\W(#{second_master_group.id}, #{second_developer_group.id})\W/)
+ expect(queries.log_message).to match(/\W(#{second_maintainer_group.id}, #{second_developer_group.id})\W/)
expect(access_levels(all_groups)).to eq(expected_all)
end
end
diff --git a/spec/policies/ci/build_policy_spec.rb b/spec/policies/ci/build_policy_spec.rb
index 9ca156deaa0..79a616899fa 100644
--- a/spec/policies/ci/build_policy_spec.rb
+++ b/spec/policies/ci/build_policy_spec.rb
@@ -101,7 +101,7 @@ describe Ci::BuildPolicy do
it 'enables update_build if user is maintainer' do
allow_any_instance_of(Project).to receive(:empty_repo?).and_return(false)
- allow_any_instance_of(Project).to receive(:branch_allows_maintainer_push?).and_return(true)
+ allow_any_instance_of(Project).to receive(:branch_allows_collaboration?).and_return(true)
expect(policy).to be_allowed :update_build
expect(policy).to be_allowed :update_commit_status
@@ -204,18 +204,18 @@ describe Ci::BuildPolicy do
end
end
- context 'when a master erases a build' do
+ context 'when a maintainer erases a build' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
- context 'when masters can push to the branch' do
+ context 'when maintainers can push to the branch' do
before do
- create(:protected_branch, :masters_can_push,
+ create(:protected_branch, :maintainers_can_push,
name: build.ref, project: project)
end
- context 'when the build was created by the master' do
+ context 'when the build was created by the maintainer' do
let(:owner) { user }
it { expect(policy).to be_allowed :erase_build }
diff --git a/spec/policies/ci/pipeline_policy_spec.rb b/spec/policies/ci/pipeline_policy_spec.rb
index a5e509cfa0f..bd32faf06ef 100644
--- a/spec/policies/ci/pipeline_policy_spec.rb
+++ b/spec/policies/ci/pipeline_policy_spec.rb
@@ -69,7 +69,7 @@ describe Ci::PipelinePolicy, :models do
it 'enables update_pipeline if user is maintainer' do
allow_any_instance_of(Project).to receive(:empty_repo?).and_return(false)
- allow_any_instance_of(Project).to receive(:branch_allows_maintainer_push?).and_return(true)
+ allow_any_instance_of(Project).to receive(:branch_allows_collaboration?).and_return(true)
expect(policy).to be_allowed :update_pipeline
end
diff --git a/spec/policies/ci/pipeline_schedule_policy_spec.rb b/spec/policies/ci/pipeline_schedule_policy_spec.rb
index c0c3eda4911..f1d3cd04e32 100644
--- a/spec/policies/ci/pipeline_schedule_policy_spec.rb
+++ b/spec/policies/ci/pipeline_schedule_policy_spec.rb
@@ -77,9 +77,9 @@ describe Ci::PipelineSchedulePolicy, :models do
end
end
- describe 'rules for a master' do
+ describe 'rules for a maintainer' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
it 'includes abilities to do do all operations on pipeline schedule' do
@@ -93,8 +93,8 @@ describe Ci::PipelineSchedulePolicy, :models do
let(:owner) { create(:user) }
before do
- project.add_master(owner)
- project.add_master(user)
+ project.add_maintainer(owner)
+ project.add_maintainer(user)
pipeline_schedule.update(owner: owner)
end
diff --git a/spec/policies/ci/trigger_policy_spec.rb b/spec/policies/ci/trigger_policy_spec.rb
index 14630748c90..d8a63066265 100644
--- a/spec/policies/ci/trigger_policy_spec.rb
+++ b/spec/policies/ci/trigger_policy_spec.rb
@@ -43,9 +43,9 @@ describe Ci::TriggerPolicy do
context 'when owner is undefined' do
let(:owner) { nil }
- context 'when user is master of the project' do
+ context 'when user is maintainer of the project' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
it_behaves_like 'allows to admin and manage trigger'
@@ -67,9 +67,9 @@ describe Ci::TriggerPolicy do
context 'when owner is an user' do
let(:owner) { user }
- context 'when user is master of the project' do
+ context 'when user is maintainer of the project' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
it_behaves_like 'allows to admin and manage trigger'
@@ -79,9 +79,9 @@ describe Ci::TriggerPolicy do
context 'when owner is another user' do
let(:owner) { create(:user) }
- context 'when user is master of the project' do
+ context 'when user is maintainer of the project' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
it_behaves_like 'allows to manage trigger'
diff --git a/spec/policies/clusters/cluster_policy_spec.rb b/spec/policies/clusters/cluster_policy_spec.rb
index 4207f42b07f..ced969830d8 100644
--- a/spec/policies/clusters/cluster_policy_spec.rb
+++ b/spec/policies/clusters/cluster_policy_spec.rb
@@ -16,9 +16,9 @@ describe Clusters::ClusterPolicy, :models do
it { expect(policy).to be_disallowed :admin_cluster }
end
- context 'when master' do
+ context 'when maintainer' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
it { expect(policy).to be_allowed :update_cluster }
diff --git a/spec/policies/deploy_key_policy_spec.rb b/spec/policies/deploy_key_policy_spec.rb
index ca7b7fe7ef7..e7263d49613 100644
--- a/spec/policies/deploy_key_policy_spec.rb
+++ b/spec/policies/deploy_key_policy_spec.rb
@@ -12,7 +12,7 @@ describe DeployKeyPolicy do
let(:project) { create(:project_empty_repo) }
before do
- project.add_master(current_user)
+ project.add_maintainer(current_user)
project.deploy_keys << deploy_key
end
diff --git a/spec/policies/deploy_token_policy_spec.rb b/spec/policies/deploy_token_policy_spec.rb
index eea287d895e..cef5a4a22bc 100644
--- a/spec/policies/deploy_token_policy_spec.rb
+++ b/spec/policies/deploy_token_policy_spec.rb
@@ -8,15 +8,15 @@ describe DeployTokenPolicy do
subject { described_class.new(current_user, deploy_token) }
describe 'creating a deploy key' do
- context 'when user is master' do
+ context 'when user is maintainer' do
before do
- project.add_master(current_user)
+ project.add_maintainer(current_user)
end
it { is_expected.to be_allowed(:create_deploy_token) }
end
- context 'when user is not master' do
+ context 'when user is not maintainer' do
before do
project.add_developer(current_user)
end
@@ -26,15 +26,15 @@ describe DeployTokenPolicy do
end
describe 'updating a deploy key' do
- context 'when user is master' do
+ context 'when user is maintainer' do
before do
- project.add_master(current_user)
+ project.add_maintainer(current_user)
end
it { is_expected.to be_allowed(:update_deploy_token) }
end
- context 'when user is not master' do
+ context 'when user is not maintainer' do
before do
project.add_developer(current_user)
end
diff --git a/spec/policies/environment_policy_spec.rb b/spec/policies/environment_policy_spec.rb
index de4cb5b30c5..0442b032e89 100644
--- a/spec/policies/environment_policy_spec.rb
+++ b/spec/policies/environment_policy_spec.rb
@@ -1,57 +1,101 @@
require 'spec_helper'
describe EnvironmentPolicy do
- let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
+ using RSpec::Parameterized::TableSyntax
- let(:environment) do
- create(:environment, :with_review_app, project: project)
- end
+ let(:user) { create(:user) }
let(:policy) do
described_class.new(user, environment)
end
describe '#rules' do
- context 'when user does not have access to the project' do
- let(:project) { create(:project, :private, :repository) }
+ shared_examples 'project permissions' do
+ context 'with stop action' do
+ let(:environment) do
+ create(:environment, :with_review_app, project: project)
+ end
- it 'does not include ability to stop environment' do
- expect(policy).to be_disallowed :stop_environment
- end
- end
+ where(:access_level, :allowed?) do
+ nil | false
+ :guest | false
+ :reporter | false
+ :developer | true
+ :maintainer | true
+ end
- context 'when anonymous user has access to the project' do
- let(:project) { create(:project, :public, :repository) }
+ with_them do
+ before do
+ project.add_user(user, access_level) unless access_level.nil?
+ end
- it 'does not include ability to stop environment' do
- expect(policy).to be_disallowed :stop_environment
- end
- end
+ it { expect(policy.allowed?(:stop_environment)).to be allowed? }
+ end
- context 'when team member has access to the project' do
- let(:project) { create(:project, :public, :repository) }
+ context 'when an admin user' do
+ let(:user) { create(:user, :admin) }
- before do
- project.add_developer(user)
- end
+ it { expect(policy).to be_allowed :stop_environment }
+ end
+
+ context 'with protected branch' do
+ with_them do
+ before do
+ project.add_user(user, access_level) unless access_level.nil?
+ create(:protected_branch, :no_one_can_push,
+ name: 'master', project: project)
+ end
- context 'when team member has ability to stop environment' do
- it 'does includes ability to stop environment' do
- expect(policy).to be_allowed :stop_environment
+ it { expect(policy).to be_disallowed :stop_environment }
+ end
+
+ context 'when an admin user' do
+ let(:user) { create(:user, :admin) }
+
+ it { expect(policy).to be_allowed :stop_environment }
+ end
end
end
- context 'when team member has no ability to stop environment' do
- before do
- create(:protected_branch, :no_one_can_push,
- name: 'master', project: project)
+ context 'without stop action' do
+ let(:environment) do
+ create(:environment, project: project)
+ end
+
+ where(:access_level, :allowed?) do
+ nil | false
+ :guest | false
+ :reporter | false
+ :developer | false
+ :maintainer | true
end
- it 'does not include ability to stop environment' do
- expect(policy).to be_disallowed :stop_environment
+ with_them do
+ before do
+ project.add_user(user, access_level) unless access_level.nil?
+ end
+
+ it { expect(policy.allowed?(:stop_environment)).to be allowed? }
+ end
+
+ context 'when an admin user' do
+ let(:user) { create(:user, :admin) }
+
+ it { expect(policy).to be_allowed :stop_environment }
end
end
end
+
+ context 'when project is public' do
+ let(:project) { create(:project, :public, :repository) }
+
+ include_examples 'project permissions'
+ end
+
+ context 'when project is private' do
+ let(:project) { create(:project, :private, :repository) }
+
+ include_examples 'project permissions'
+ end
end
end
diff --git a/spec/policies/global_policy_spec.rb b/spec/policies/global_policy_spec.rb
index 873673b50ef..a2047b54deb 100644
--- a/spec/policies/global_policy_spec.rb
+++ b/spec/policies/global_policy_spec.rb
@@ -65,12 +65,12 @@ describe GlobalPolicy do
it { is_expected.not_to be_allowed(:create_fork) }
end
- context "when user is a master in a group" do
+ context "when user is a maintainer in a group" do
let(:group) { create(:group) }
let(:current_user) { create(:user, projects_limit: 0) }
before do
- group.add_master(current_user)
+ group.add_maintainer(current_user)
end
it { is_expected.to be_allowed(:create_fork) }
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index 9b5c290b9f9..35951251bc5 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -4,18 +4,22 @@ describe GroupPolicy do
let(:guest) { create(:user) }
let(:reporter) { create(:user) }
let(:developer) { create(:user) }
- let(:master) { create(:user) }
+ let(:maintainer) { create(:user) }
let(:owner) { create(:user) }
let(:admin) { create(:admin) }
let(:group) { create(:group, :private) }
- let(:guest_permissions) { [:read_label, :read_group, :upload_file, :read_namespace] }
+ let(:guest_permissions) do
+ [:read_label, :read_group, :upload_file, :read_namespace, :read_group_activity,
+ :read_group_issues, :read_group_boards, :read_group_labels, :read_group_milestones,
+ :read_group_merge_requests]
+ end
let(:reporter_permissions) { [:admin_label] }
let(:developer_permissions) { [:admin_milestones] }
- let(:master_permissions) do
+ let(:maintainer_permissions) do
[
:create_projects
]
@@ -35,7 +39,7 @@ describe GroupPolicy do
group.add_guest(guest)
group.add_reporter(reporter)
group.add_developer(developer)
- group.add_master(master)
+ group.add_maintainer(maintainer)
group.add_owner(owner)
end
@@ -58,7 +62,7 @@ describe GroupPolicy do
expect_disallowed(:upload_file)
expect_disallowed(*reporter_permissions)
expect_disallowed(*developer_permissions)
- expect_disallowed(*master_permissions)
+ expect_disallowed(*maintainer_permissions)
expect_disallowed(*owner_permissions)
expect_disallowed(:read_namespace)
end
@@ -93,7 +97,7 @@ describe GroupPolicy do
expect_allowed(*guest_permissions)
expect_disallowed(*reporter_permissions)
expect_disallowed(*developer_permissions)
- expect_disallowed(*master_permissions)
+ expect_disallowed(*maintainer_permissions)
expect_disallowed(*owner_permissions)
end
end
@@ -105,7 +109,7 @@ describe GroupPolicy do
expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_disallowed(*developer_permissions)
- expect_disallowed(*master_permissions)
+ expect_disallowed(*maintainer_permissions)
expect_disallowed(*owner_permissions)
end
end
@@ -117,19 +121,19 @@ describe GroupPolicy do
expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_allowed(*developer_permissions)
- expect_disallowed(*master_permissions)
+ expect_disallowed(*maintainer_permissions)
expect_disallowed(*owner_permissions)
end
end
- context 'master' do
- let(:current_user) { master }
+ context 'maintainer' do
+ let(:current_user) { maintainer }
it do
expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_allowed(*developer_permissions)
- expect_allowed(*master_permissions)
+ expect_allowed(*maintainer_permissions)
expect_disallowed(*owner_permissions)
end
end
@@ -143,7 +147,7 @@ describe GroupPolicy do
expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_allowed(*developer_permissions)
- expect_allowed(*master_permissions)
+ expect_allowed(*maintainer_permissions)
expect_allowed(*owner_permissions)
end
end
@@ -157,7 +161,7 @@ describe GroupPolicy do
expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_allowed(*developer_permissions)
- expect_allowed(*master_permissions)
+ expect_allowed(*maintainer_permissions)
expect_allowed(*owner_permissions)
end
end
@@ -199,7 +203,7 @@ describe GroupPolicy do
nested_group.add_guest(guest)
nested_group.add_guest(reporter)
nested_group.add_guest(developer)
- nested_group.add_guest(master)
+ nested_group.add_guest(maintainer)
group.owners.destroy_all
@@ -216,7 +220,7 @@ describe GroupPolicy do
expect_disallowed(*guest_permissions)
expect_disallowed(*reporter_permissions)
expect_disallowed(*developer_permissions)
- expect_disallowed(*master_permissions)
+ expect_disallowed(*maintainer_permissions)
expect_disallowed(*owner_permissions)
end
end
@@ -228,7 +232,7 @@ describe GroupPolicy do
expect_allowed(*guest_permissions)
expect_disallowed(*reporter_permissions)
expect_disallowed(*developer_permissions)
- expect_disallowed(*master_permissions)
+ expect_disallowed(*maintainer_permissions)
expect_disallowed(*owner_permissions)
end
end
@@ -240,7 +244,7 @@ describe GroupPolicy do
expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_disallowed(*developer_permissions)
- expect_disallowed(*master_permissions)
+ expect_disallowed(*maintainer_permissions)
expect_disallowed(*owner_permissions)
end
end
@@ -252,19 +256,19 @@ describe GroupPolicy do
expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_allowed(*developer_permissions)
- expect_disallowed(*master_permissions)
+ expect_disallowed(*maintainer_permissions)
expect_disallowed(*owner_permissions)
end
end
- context 'master' do
- let(:current_user) { master }
+ context 'maintainer' do
+ let(:current_user) { maintainer }
it do
expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_allowed(*developer_permissions)
- expect_allowed(*master_permissions)
+ expect_allowed(*maintainer_permissions)
expect_disallowed(*owner_permissions)
end
end
@@ -278,7 +282,7 @@ describe GroupPolicy do
expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_allowed(*developer_permissions)
- expect_allowed(*master_permissions)
+ expect_allowed(*maintainer_permissions)
expect_allowed(*owner_permissions)
end
end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index 6609f5f7afd..dd3fa4e6a51 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -4,7 +4,7 @@ describe ProjectPolicy do
set(:guest) { create(:user) }
set(:reporter) { create(:user) }
set(:developer) { create(:user) }
- set(:master) { create(:user) }
+ set(:maintainer) { create(:user) }
set(:owner) { create(:user) }
set(:admin) { create(:admin) }
let(:project) { create(:project, :public, namespace: owner.namespace) }
@@ -42,7 +42,7 @@ describe ProjectPolicy do
]
end
- let(:base_master_permissions) do
+ let(:base_maintainer_permissions) do
%i[
push_to_delete_protected_branch update_project_snippet update_environment
update_deployment admin_project_snippet
@@ -70,15 +70,15 @@ describe ProjectPolicy do
# Used in EE specs
let(:additional_guest_permissions) { [] }
let(:additional_reporter_permissions) { [] }
- let(:additional_master_permissions) { [] }
+ let(:additional_maintainer_permissions) { [] }
let(:guest_permissions) { base_guest_permissions + additional_guest_permissions }
let(:reporter_permissions) { base_reporter_permissions + additional_reporter_permissions }
- let(:master_permissions) { base_master_permissions + additional_master_permissions }
+ let(:maintainer_permissions) { base_maintainer_permissions + additional_maintainer_permissions }
before do
project.add_guest(guest)
- project.add_master(master)
+ project.add_maintainer(maintainer)
project.add_developer(developer)
project.add_reporter(reporter)
end
@@ -151,6 +151,44 @@ describe ProjectPolicy do
end
end
+ context 'builds feature' do
+ subject { described_class.new(owner, project) }
+
+ it 'disallows all permissions when the feature is disabled' do
+ project.project_feature.update(builds_access_level: ProjectFeature::DISABLED)
+
+ builds_permissions = [
+ :create_pipeline, :update_pipeline, :admin_pipeline, :destroy_pipeline,
+ :create_build, :read_build, :update_build, :admin_build, :destroy_build,
+ :create_pipeline_schedule, :read_pipeline_schedule, :update_pipeline_schedule, :admin_pipeline_schedule, :destroy_pipeline_schedule,
+ :create_environment, :read_environment, :update_environment, :admin_environment, :destroy_environment,
+ :create_cluster, :read_cluster, :update_cluster, :admin_cluster, :destroy_cluster,
+ :create_deployment, :read_deployment, :update_deployment, :admin_deployment, :destroy_deployment
+ ]
+
+ expect_disallowed(*builds_permissions)
+ end
+ end
+
+ context 'repository feature' do
+ subject { described_class.new(owner, project) }
+
+ it 'disallows all permissions when the feature is disabled' do
+ project.project_feature.update(repository_access_level: ProjectFeature::DISABLED)
+
+ repository_permissions = [
+ :create_pipeline, :update_pipeline, :admin_pipeline, :destroy_pipeline,
+ :create_build, :read_build, :update_build, :admin_build, :destroy_build,
+ :create_pipeline_schedule, :read_pipeline_schedule, :update_pipeline_schedule, :admin_pipeline_schedule, :destroy_pipeline_schedule,
+ :create_environment, :read_environment, :update_environment, :admin_environment, :destroy_environment,
+ :create_cluster, :read_cluster, :update_cluster, :admin_cluster, :destroy_cluster,
+ :create_deployment, :read_deployment, :update_deployment, :admin_deployment, :destroy_deployment
+ ]
+
+ expect_disallowed(*repository_permissions)
+ end
+ end
+
shared_examples 'archived project policies' do
let(:feature_write_abilities) do
described_class::READONLY_FEATURES_WHEN_ARCHIVED.flat_map do |feature|
@@ -238,7 +276,7 @@ describe ProjectPolicy do
expect_disallowed(*reporter_public_build_permissions)
expect_disallowed(*team_member_reporter_permissions)
expect_disallowed(*developer_permissions)
- expect_disallowed(*master_permissions)
+ expect_disallowed(*maintainer_permissions)
expect_disallowed(*owner_permissions)
end
@@ -288,7 +326,7 @@ describe ProjectPolicy do
expect_allowed(*reporter_permissions)
expect_allowed(*team_member_reporter_permissions)
expect_disallowed(*developer_permissions)
- expect_disallowed(*master_permissions)
+ expect_disallowed(*maintainer_permissions)
expect_disallowed(*owner_permissions)
end
@@ -309,7 +347,7 @@ describe ProjectPolicy do
expect_allowed(*reporter_permissions)
expect_allowed(*team_member_reporter_permissions)
expect_allowed(*developer_permissions)
- expect_disallowed(*master_permissions)
+ expect_disallowed(*maintainer_permissions)
expect_disallowed(*owner_permissions)
end
@@ -319,23 +357,23 @@ describe ProjectPolicy do
end
end
- shared_examples 'project policies as master' do
+ shared_examples 'project policies as maintainer' do
context 'abilities for non-public projects' do
let(:project) { create(:project, namespace: owner.namespace) }
- subject { described_class.new(master, project) }
+ subject { described_class.new(maintainer, project) }
it do
expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_allowed(*team_member_reporter_permissions)
expect_allowed(*developer_permissions)
- expect_allowed(*master_permissions)
+ expect_allowed(*maintainer_permissions)
expect_disallowed(*owner_permissions)
end
it_behaves_like 'archived project policies' do
- let(:regular_abilities) { master_permissions }
+ let(:regular_abilities) { maintainer_permissions }
end
end
end
@@ -351,7 +389,7 @@ describe ProjectPolicy do
expect_allowed(*reporter_permissions)
expect_allowed(*team_member_reporter_permissions)
expect_allowed(*developer_permissions)
- expect_allowed(*master_permissions)
+ expect_allowed(*maintainer_permissions)
expect_allowed(*owner_permissions)
end
@@ -372,7 +410,7 @@ describe ProjectPolicy do
expect_allowed(*reporter_permissions)
expect_disallowed(*team_member_reporter_permissions)
expect_allowed(*developer_permissions)
- expect_allowed(*master_permissions)
+ expect_allowed(*maintainer_permissions)
expect_allowed(*owner_permissions)
end
@@ -386,7 +424,7 @@ describe ProjectPolicy do
it_behaves_like 'project policies as guest'
it_behaves_like 'project policies as reporter'
it_behaves_like 'project policies as developer'
- it_behaves_like 'project policies as master'
+ it_behaves_like 'project policies as maintainer'
it_behaves_like 'project policies as owner'
it_behaves_like 'project policies as admin'
@@ -400,7 +438,7 @@ describe ProjectPolicy do
:merge_request,
target_project: target_project,
source_project: project,
- allow_maintainer_to_push: true
+ allow_collaboration: true
)
end
let(:maintainer_abilities) do
diff --git a/spec/policies/protected_branch_policy_spec.rb b/spec/policies/protected_branch_policy_spec.rb
index b39de42d721..1587196754d 100644
--- a/spec/policies/protected_branch_policy_spec.rb
+++ b/spec/policies/protected_branch_policy_spec.rb
@@ -8,8 +8,8 @@ describe ProtectedBranchPolicy do
subject { described_class.new(user, protected_branch) }
- it 'branches can be updated via project masters' do
- project.add_master(user)
+ it 'branches can be updated via project maintainers' do
+ project.add_maintainer(user)
is_expected.to be_allowed(:update_protected_branch)
end
diff --git a/spec/presenters/ci/build_presenter_spec.rb b/spec/presenters/ci/build_presenter_spec.rb
index efd175247b5..6dfaa3b72f7 100644
--- a/spec/presenters/ci/build_presenter_spec.rb
+++ b/spec/presenters/ci/build_presenter_spec.rb
@@ -219,11 +219,11 @@ describe Ci::BuildPresenter do
end
describe '#callout_failure_message' do
- let(:build) { create(:ci_build, :failed, :script_failure) }
+ let(:build) { create(:ci_build, :failed, :api_failure) }
it 'returns a verbose failure reason' do
description = subject.callout_failure_message
- expect(description).to eq('There has been a script failure. Check the job log for more information')
+ expect(description).to eq('There has been an API failure, please try again')
end
end
diff --git a/spec/presenters/merge_request_presenter_spec.rb b/spec/presenters/merge_request_presenter_spec.rb
index e3b37739e8e..46ba6f442f5 100644
--- a/spec/presenters/merge_request_presenter_spec.rb
+++ b/spec/presenters/merge_request_presenter_spec.rb
@@ -270,7 +270,7 @@ describe MergeRequestPresenter do
context 'when can create issue and issues enabled' do
it 'returns path' do
allow(project).to receive(:issues_enabled?) { true }
- project.add_master(user)
+ project.add_maintainer(user)
is_expected
.to eq("/#{resource.project.full_path}/issues/new?merge_request_to_resolve_discussions_of=#{resource.iid}")
@@ -288,7 +288,7 @@ describe MergeRequestPresenter do
context 'when issues disabled' do
it 'returns nil' do
allow(project).to receive(:issues_enabled?) { false }
- project.add_master(user)
+ project.add_maintainer(user)
is_expected.to be_nil
end
@@ -307,7 +307,7 @@ describe MergeRequestPresenter do
context 'when merge request enabled and has permission' do
it 'has remove_wip_path' do
allow(project).to receive(:merge_requests_enabled?) { true }
- project.add_master(user)
+ project.add_maintainer(user)
is_expected
.to eq("/#{resource.project.full_path}/merge_requests/#{resource.iid}/remove_wip")
diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb
index 830d2ee3b20..01085dbcb49 100644
--- a/spec/presenters/project_presenter_spec.rb
+++ b/spec/presenters/project_presenter_spec.rb
@@ -326,7 +326,7 @@ describe ProjectPresenter do
context 'when user can admin pipeline and CI yml does not exists' do
it 'returns anchor data' do
- project.add_master(user)
+ project.add_maintainer(user)
allow(project).to receive(:auto_devops_enabled?).and_return(false)
allow(project.repository).to receive(:gitlab_ci_yml).and_return(nil)
@@ -340,7 +340,7 @@ describe ProjectPresenter do
describe '#kubernetes_cluster_anchor_data' do
context 'when user can create Kubernetes cluster' do
it 'returns link to cluster if only one exists' do
- project.add_master(user)
+ project.add_maintainer(user)
cluster = create(:cluster, projects: [project])
expect(presenter.kubernetes_cluster_anchor_data).to eq(OpenStruct.new(enabled: true,
@@ -349,7 +349,7 @@ describe ProjectPresenter do
end
it 'returns link to clusters page if more than one exists' do
- project.add_master(user)
+ project.add_maintainer(user)
create(:cluster, :production_environment, projects: [project])
create(:cluster, projects: [project])
@@ -359,7 +359,7 @@ describe ProjectPresenter do
end
it 'returns link to create a cluster if no cluster exists' do
- project.add_master(user)
+ project.add_maintainer(user)
expect(presenter.kubernetes_cluster_anchor_data).to eq(OpenStruct.new(enabled: false,
label: 'Add Kubernetes cluster',
diff --git a/spec/requests/api/access_requests_spec.rb b/spec/requests/api/access_requests_spec.rb
index 24389f28b21..e13129967b2 100644
--- a/spec/requests/api/access_requests_spec.rb
+++ b/spec/requests/api/access_requests_spec.rb
@@ -1,15 +1,15 @@
require 'spec_helper'
describe API::AccessRequests do
- set(:master) { create(:user) }
+ set(:maintainer) { create(:user) }
set(:developer) { create(:user) }
set(:access_requester) { create(:user) }
set(:stranger) { create(:user) }
set(:project) do
- create(:project, :public, :access_requestable, creator_id: master.id, namespace: master.namespace) do |project|
+ create(:project, :public, :access_requestable, creator_id: maintainer.id, namespace: maintainer.namespace) do |project|
project.add_developer(developer)
- project.add_master(master)
+ project.add_maintainer(maintainer)
project.request_access(access_requester)
end
end
@@ -17,7 +17,7 @@ describe API::AccessRequests do
set(:group) do
create(:group, :public, :access_requestable) do |group|
group.add_developer(developer)
- group.add_owner(master)
+ group.add_owner(maintainer)
group.request_access(access_requester)
end
end
@@ -28,7 +28,7 @@ describe API::AccessRequests do
let(:route) { get api("/#{source_type.pluralize}/#{source.id}/access_requests", stranger) }
end
- context 'when authenticated as a non-master/owner' do
+ context 'when authenticated as a non-maintainer/owner' do
%i[developer access_requester stranger].each do |type|
context "as a #{type}" do
it 'returns 403' do
@@ -41,9 +41,9 @@ describe API::AccessRequests do
end
end
- context 'when authenticated as a master/owner' do
+ context 'when authenticated as a maintainer/owner' do
it 'returns access requesters' do
- get api("/#{source_type.pluralize}/#{source.id}/access_requests", master)
+ get api("/#{source_type.pluralize}/#{source.id}/access_requests", maintainer)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
@@ -61,7 +61,7 @@ describe API::AccessRequests do
end
context 'when authenticated as a member' do
- %i[developer master].each do |type|
+ %i[developer maintainer].each do |type|
context "as a #{type}" do
it 'returns 403' do
expect do
@@ -88,7 +88,7 @@ describe API::AccessRequests do
context 'when authenticated as a stranger' do
context "when access request is disabled for the #{source_type}" do
before do
- source.update_attributes(request_access_enabled: false)
+ source.update(request_access_enabled: false)
end
it 'returns 403' do
@@ -128,7 +128,7 @@ describe API::AccessRequests do
let(:route) { put api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}/approve", stranger) }
end
- context 'when authenticated as a non-master/owner' do
+ context 'when authenticated as a non-maintainer/owner' do
%i[developer access_requester stranger].each do |type|
context "as a #{type}" do
it 'returns 403' do
@@ -141,11 +141,11 @@ describe API::AccessRequests do
end
end
- context 'when authenticated as a master/owner' do
+ context 'when authenticated as a maintainer/owner' do
it 'returns 201' do
expect do
- put api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}/approve", master),
- access_level: Member::MASTER
+ put api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}/approve", maintainer),
+ access_level: Member::MAINTAINER
expect(response).to have_gitlab_http_status(201)
end.to change { source.members.count }.by(1)
@@ -158,13 +158,13 @@ describe API::AccessRequests do
expect(json_response['web_url']).to eq(Gitlab::Routing.url_helpers.user_url(access_requester))
# Member attributes
- expect(json_response['access_level']).to eq(Member::MASTER)
+ expect(json_response['access_level']).to eq(Member::MAINTAINER)
end
context 'user_id does not match an existing access requester' do
it 'returns 404' do
expect do
- put api("/#{source_type.pluralize}/#{source.id}/access_requests/#{stranger.id}/approve", master)
+ put api("/#{source_type.pluralize}/#{source.id}/access_requests/#{stranger.id}/approve", maintainer)
expect(response).to have_gitlab_http_status(404)
end.not_to change { source.members.count }
@@ -180,7 +180,7 @@ describe API::AccessRequests do
let(:route) { delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", stranger) }
end
- context 'when authenticated as a non-master/owner' do
+ context 'when authenticated as a non-maintainer/owner' do
%i[developer stranger].each do |type|
context "as a #{type}" do
it 'returns 403' do
@@ -203,10 +203,10 @@ describe API::AccessRequests do
end
end
- context 'when authenticated as a master/owner' do
+ context 'when authenticated as a maintainer/owner' do
it 'deletes the access requester' do
expect do
- delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", master)
+ delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", maintainer)
expect(response).to have_gitlab_http_status(204)
end.to change { source.requesters.count }.by(-1)
@@ -215,7 +215,7 @@ describe API::AccessRequests do
context 'user_id matches a member, not an access requester' do
it 'returns 404' do
expect do
- delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{developer.id}", master)
+ delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{developer.id}", maintainer)
expect(response).to have_gitlab_http_status(404)
end.not_to change { source.requesters.count }
@@ -225,7 +225,7 @@ describe API::AccessRequests do
context 'user_id does not match an existing access requester' do
it 'returns 404' do
expect do
- delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{stranger.id}", master)
+ delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{stranger.id}", maintainer)
expect(response).to have_gitlab_http_status(404)
end.not_to change { source.requesters.count }
diff --git a/spec/requests/api/avatar_spec.rb b/spec/requests/api/avatar_spec.rb
new file mode 100644
index 00000000000..26e0435a6d5
--- /dev/null
+++ b/spec/requests/api/avatar_spec.rb
@@ -0,0 +1,106 @@
+require 'spec_helper'
+
+describe API::Avatar do
+ let(:gravatar_service) { double('GravatarService') }
+
+ describe 'GET /avatar' do
+ context 'avatar uploaded to GitLab' do
+ context 'user with matching public email address' do
+ let(:user) { create(:user, :with_avatar, email: 'public@example.com', public_email: 'public@example.com') }
+
+ before do
+ user
+ end
+
+ it 'returns the avatar url' do
+ get api('/avatar'), { email: 'public@example.com' }
+
+ expect(response.status).to eq 200
+ expect(json_response['avatar_url']).to eql("#{::Settings.gitlab.base_url}#{user.avatar.local_url}")
+ end
+ end
+
+ context 'no user with matching public email address' do
+ before do
+ expect(GravatarService).to receive(:new).and_return(gravatar_service)
+ expect(gravatar_service).to(
+ receive(:execute)
+ .with('private@example.com', nil, 2, { username: nil })
+ .and_return('https://gravatar'))
+ end
+
+ it 'returns the avatar url from Gravatar' do
+ get api('/avatar'), { email: 'private@example.com' }
+
+ expect(response.status).to eq 200
+ expect(json_response['avatar_url']).to eq('https://gravatar')
+ end
+ end
+ end
+
+ context 'avatar uploaded to Gravatar' do
+ context 'user with matching public email address' do
+ let(:user) { create(:user, email: 'public@example.com', public_email: 'public@example.com') }
+
+ before do
+ user
+
+ expect(GravatarService).to receive(:new).and_return(gravatar_service)
+ expect(gravatar_service).to(
+ receive(:execute)
+ .with('public@example.com', nil, 2, { username: user.username })
+ .and_return('https://gravatar'))
+ end
+
+ it 'returns the avatar url from Gravatar' do
+ get api('/avatar'), { email: 'public@example.com' }
+
+ expect(response.status).to eq 200
+ expect(json_response['avatar_url']).to eq('https://gravatar')
+ end
+ end
+
+ context 'no user with matching public email address' do
+ before do
+ expect(GravatarService).to receive(:new).and_return(gravatar_service)
+ expect(gravatar_service).to(
+ receive(:execute)
+ .with('private@example.com', nil, 2, { username: nil })
+ .and_return('https://gravatar'))
+ end
+
+ it 'returns the avatar url from Gravatar' do
+ get api('/avatar'), { email: 'private@example.com' }
+
+ expect(response.status).to eq 200
+ expect(json_response['avatar_url']).to eq('https://gravatar')
+ end
+ end
+
+ context 'public visibility level restricted' do
+ let(:user) { create(:user, :with_avatar, email: 'public@example.com', public_email: 'public@example.com') }
+
+ before do
+ user
+
+ stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
+ end
+
+ context 'when authenticated' do
+ it 'returns the avatar url' do
+ get api('/avatar', user), { email: 'public@example.com' }
+
+ expect(response.status).to eq 200
+ expect(json_response['avatar_url']).to eql("#{::Settings.gitlab.base_url}#{user.avatar.local_url}")
+ end
+ end
+
+ context 'when unauthenticated' do
+ it_behaves_like '403 response' do
+ let(:request) { get api('/avatar'), { email: 'public@example.com' } }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb
index 5adfb33677f..0921fecb933 100644
--- a/spec/requests/api/award_emoji_spec.rb
+++ b/spec/requests/api/award_emoji_spec.rb
@@ -10,7 +10,7 @@ describe API::AwardEmoji do
set(:note) { create(:note, project: project, noteable: issue) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
describe "GET /projects/:id/awardable/:awardable_id/award_emoji" do
diff --git a/spec/requests/api/badges_spec.rb b/spec/requests/api/badges_spec.rb
index ae64a9ca162..e232e2e04ee 100644
--- a/spec/requests/api/badges_spec.rb
+++ b/spec/requests/api/badges_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe API::Badges do
- let(:master) { create(:user, username: 'master_user') }
+ let(:maintainer) { create(:user, username: 'maintainer_user') }
let(:developer) { create(:user) }
let(:access_requester) { create(:user) }
let(:stranger) { create(:user) }
@@ -25,7 +25,7 @@ describe API::Badges do
let(:route) { get api("/#{source_type.pluralize}/#{source.id}/badges", stranger) }
end
- %i[master developer access_requester stranger].each do |type|
+ %i[maintainer developer access_requester stranger].each do |type|
context "when authenticated as a #{type}" do
it 'returns 200' do
user = public_send(type)
@@ -43,16 +43,16 @@ describe API::Badges do
it 'avoids N+1 queries' do
# Establish baseline
- get api("/#{source_type.pluralize}/#{source.id}/badges", master)
+ get api("/#{source_type.pluralize}/#{source.id}/badges", maintainer)
control = ActiveRecord::QueryRecorder.new do
- get api("/#{source_type.pluralize}/#{source.id}/badges", master)
+ get api("/#{source_type.pluralize}/#{source.id}/badges", maintainer)
end
project.add_developer(create(:user))
expect do
- get api("/#{source_type.pluralize}/#{source.id}/badges", master)
+ get api("/#{source_type.pluralize}/#{source.id}/badges", maintainer)
end.not_to exceed_query_limit(control)
end
end
@@ -69,7 +69,7 @@ describe API::Badges do
end
context 'when authenticated as a non-member' do
- %i[master developer access_requester stranger].each do |type|
+ %i[maintainer developer access_requester stranger].each do |type|
let(:badge) { source.badges.first }
context "as a #{type}" do
@@ -122,10 +122,10 @@ describe API::Badges do
end
end
- context 'when authenticated as a master/owner' do
+ context 'when authenticated as a maintainer/owner' do
it 'creates a new badge' do
expect do
- post api("/#{source_type.pluralize}/#{source.id}/badges", master),
+ post api("/#{source_type.pluralize}/#{source.id}/badges", maintainer),
link_url: example_url, image_url: example_url2
expect(response).to have_gitlab_http_status(201)
@@ -138,21 +138,21 @@ describe API::Badges do
end
it 'returns 400 when link_url is not given' do
- post api("/#{source_type.pluralize}/#{source.id}/badges", master),
+ post api("/#{source_type.pluralize}/#{source.id}/badges", maintainer),
link_url: example_url
expect(response).to have_gitlab_http_status(400)
end
it 'returns 400 when image_url is not given' do
- post api("/#{source_type.pluralize}/#{source.id}/badges", master),
+ post api("/#{source_type.pluralize}/#{source.id}/badges", maintainer),
image_url: example_url2
expect(response).to have_gitlab_http_status(400)
end
it 'returns 400 when link_url or image_url is not valid' do
- post api("/#{source_type.pluralize}/#{source.id}/badges", master),
+ post api("/#{source_type.pluralize}/#{source.id}/badges", maintainer),
link_url: 'whatever', image_url: 'whatever'
expect(response).to have_gitlab_http_status(400)
@@ -192,9 +192,9 @@ describe API::Badges do
end
end
- context 'when authenticated as a master/owner' do
+ context 'when authenticated as a maintainer/owner' do
it 'updates the member' do
- put api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", master),
+ put api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", maintainer),
link_url: example_url, image_url: example_url2
expect(response).to have_gitlab_http_status(200)
@@ -205,7 +205,7 @@ describe API::Badges do
end
it 'returns 400 when link_url or image_url is not valid' do
- put api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", master),
+ put api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", maintainer),
link_url: 'whatever', image_url: 'whatever'
expect(response).to have_gitlab_http_status(400)
@@ -239,22 +239,22 @@ describe API::Badges do
end
end
- context 'when authenticated as a master/owner' do
+ context 'when authenticated as a maintainer/owner' do
it 'deletes the badge' do
expect do
- delete api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", master)
+ delete api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", maintainer)
expect(response).to have_gitlab_http_status(204)
end.to change { source.badges.count }.by(-1)
end
it_behaves_like '412 response' do
- let(:request) { api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", master) }
+ let(:request) { api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", maintainer) }
end
end
it 'returns 404 if badge does not exist' do
- delete api("/#{source_type.pluralize}/#{source.id}/badges/123", master)
+ delete api("/#{source_type.pluralize}/#{source.id}/badges/123", maintainer)
expect(response).to have_gitlab_http_status(404)
end
@@ -289,9 +289,9 @@ describe API::Badges do
end
end
- context 'when authenticated as a master/owner' do
+ context 'when authenticated as a maintainer/owner' do
it 'gets the rendered badge values' do
- get api("/#{source_type.pluralize}/#{source.id}/badges/render?link_url=#{example_url}&image_url=#{example_url2}", master)
+ get api("/#{source_type.pluralize}/#{source.id}/badges/render?link_url=#{example_url}&image_url=#{example_url2}", maintainer)
expect(response).to have_gitlab_http_status(200)
@@ -304,19 +304,19 @@ describe API::Badges do
end
it 'returns 400 when link_url is not given' do
- get api("/#{source_type.pluralize}/#{source.id}/badges/render?link_url=#{example_url}", master)
+ get api("/#{source_type.pluralize}/#{source.id}/badges/render?link_url=#{example_url}", maintainer)
expect(response).to have_gitlab_http_status(400)
end
it 'returns 400 when image_url is not given' do
- get api("/#{source_type.pluralize}/#{source.id}/badges/render?image_url=#{example_url}", master)
+ get api("/#{source_type.pluralize}/#{source.id}/badges/render?image_url=#{example_url}", maintainer)
expect(response).to have_gitlab_http_status(400)
end
it 'returns 400 when link_url or image_url is not valid' do
- get api("/#{source_type.pluralize}/#{source.id}/badges/render?link_url=whatever&image_url=whatever", master)
+ get api("/#{source_type.pluralize}/#{source.id}/badges/render?link_url=whatever&image_url=whatever", maintainer)
expect(response).to have_gitlab_http_status(400)
end
@@ -326,7 +326,7 @@ describe API::Badges do
context 'when deleting a badge' do
context 'and the source is a project' do
it 'cannot delete badges owned by the project group' do
- delete api("/projects/#{project.id}/badges/#{project_group.badges.first.id}", master)
+ delete api("/projects/#{project.id}/badges/#{project_group.badges.first.id}", maintainer)
expect(response).to have_gitlab_http_status(403)
end
@@ -345,9 +345,9 @@ describe API::Badges do
end
def setup_project
- create(:project, :public, :access_requestable, creator_id: master.id, namespace: project_group) do |project|
+ create(:project, :public, :access_requestable, creator_id: maintainer.id, namespace: project_group) do |project|
project.add_developer(developer)
- project.add_master(master)
+ project.add_maintainer(maintainer)
project.request_access(access_requester)
project.project_badges << build(:project_badge, project: project)
project.project_badges << build(:project_badge, project: project)
@@ -358,7 +358,7 @@ describe API::Badges do
def setup_group
create(:group, :public, :access_requestable) do |group|
group.add_developer(developer)
- group.add_owner(master)
+ group.add_owner(maintainer)
group.request_access(access_requester)
group.badges << build(:group_badge, group: group)
group.badges << build(:group_badge, group: group)
diff --git a/spec/requests/api/boards_spec.rb b/spec/requests/api/boards_spec.rb
index 92b614b087e..7710f19ce4e 100644
--- a/spec/requests/api/boards_spec.rb
+++ b/spec/requests/api/boards_spec.rb
@@ -2,7 +2,6 @@ require 'spec_helper'
describe API::Boards do
set(:user) { create(:user) }
- set(:user2) { create(:user) }
set(:non_member) { create(:user) }
set(:guest) { create(:user) }
set(:admin) { create(:user, :admin) }
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index 64f51d9843d..7fff0a6cce6 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -13,7 +13,7 @@ describe API::Branches do
let(:current_user) { nil }
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
describe "GET /projects/:id/repository/branches" do
@@ -75,7 +75,7 @@ describe API::Branches do
end
end
- context 'when authenticated', 'as a master' do
+ context 'when authenticated', 'as a maintainer' do
let(:current_user) { user }
it_behaves_like 'repository branches'
@@ -155,6 +155,12 @@ describe API::Branches do
end
it_behaves_like 'repository branch'
+
+ it 'returns that the current user cannot push' do
+ get api(route, current_user)
+
+ expect(json_response['can_push']).to eq(false)
+ end
end
context 'when unauthenticated', 'and project is private' do
@@ -164,11 +170,17 @@ describe API::Branches do
end
end
- context 'when authenticated', 'as a master' do
+ context 'when authenticated', 'as a maintainer' do
let(:current_user) { user }
it_behaves_like 'repository branch'
+ it 'returns that the current user can push' do
+ get api(route, current_user)
+
+ expect(json_response['can_push']).to eq(true)
+ end
+
context 'when branch contains a dot' do
let(:branch_name) { branch_with_dot.name }
@@ -202,6 +214,23 @@ describe API::Branches do
end
end
+ context 'when authenticated', 'as a developer and branch is protected' do
+ let(:current_user) { create(:user) }
+ let!(:protected_branch) { create(:protected_branch, project: project, name: branch_name) }
+
+ before do
+ project.add_developer(current_user)
+ end
+
+ it_behaves_like 'repository branch'
+
+ it 'returns that the current user cannot push' do
+ get api(route, current_user)
+
+ expect(json_response['can_push']).to eq(false)
+ end
+ end
+
context 'when authenticated', 'as a guest' do
it_behaves_like '403 response' do
let(:request) { get api(route, guest) }
@@ -295,7 +324,7 @@ describe API::Branches do
end
end
- context 'when authenticated', 'as a master' do
+ context 'when authenticated', 'as a maintainer' do
let(:current_user) { user }
context "when a protected branch doesn't already exist" do
@@ -352,8 +381,8 @@ describe API::Branches do
expect(json_response['protected']).to eq(true)
expect(json_response['developers_can_push']).to eq(false)
expect(json_response['developers_can_merge']).to eq(false)
- expect(protected_branch.reload.push_access_levels.first.access_level).to eq(Gitlab::Access::MASTER)
- expect(protected_branch.reload.merge_access_levels.first.access_level).to eq(Gitlab::Access::MASTER)
+ expect(protected_branch.reload.push_access_levels.first.access_level).to eq(Gitlab::Access::MAINTAINER)
+ expect(protected_branch.reload.merge_access_levels.first.access_level).to eq(Gitlab::Access::MAINTAINER)
end
end
@@ -429,7 +458,7 @@ describe API::Branches do
end
end
- context 'when authenticated', 'as a master' do
+ context 'when authenticated', 'as a maintainer' do
let(:current_user) { user }
context "when a protected branch doesn't already exist" do
@@ -505,7 +534,7 @@ describe API::Branches do
end
end
- context 'when authenticated', 'as a master' do
+ context 'when authenticated', 'as a maintainer' do
let(:current_user) { user }
context "when a protected branch doesn't already exist" do
diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb
index f246bb79ab7..cd43bec35df 100644
--- a/spec/requests/api/commit_statuses_spec.rb
+++ b/spec/requests/api/commit_statuses_spec.rb
@@ -304,7 +304,7 @@ describe API::CommitStatuses do
it 'responds with bad request status and validation errors' do
expect(response).to have_gitlab_http_status(400)
expect(json_response['message']['target_url'])
- .to include 'must be a valid URL'
+ .to include 'is blocked: Only allowed protocols are http, https'
end
end
end
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 8ad19e3f0f5..113703fac38 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -12,20 +12,20 @@ describe API::Commits do
let(:current_user) { nil }
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
describe 'GET /projects/:id/repository/commits' do
let(:route) { "/projects/#{project_id}/repository/commits" }
- shared_examples_for 'project commits' do
+ shared_examples_for 'project commits' do |schema: 'public_api/v4/commits'|
it "returns project commits" do
commit = project.repository.commit
get api(route, current_user)
expect(response).to have_gitlab_http_status(200)
- expect(response).to match_response_schema('public_api/v4/commits')
+ expect(response).to match_response_schema(schema)
expect(json_response.first['id']).to eq(commit.id)
expect(json_response.first['committer_name']).to eq(commit.committer_name)
expect(json_response.first['committer_email']).to eq(commit.committer_email)
@@ -55,7 +55,7 @@ describe API::Commits do
end
end
- context 'when authenticated', 'as a master' do
+ context 'when authenticated', 'as a maintainer' do
let(:current_user) { user }
it_behaves_like 'project commits'
@@ -161,6 +161,23 @@ describe API::Commits do
end
end
+ context 'with_stats optional parameter' do
+ let(:project) { create(:project, :public, :repository) }
+
+ it_behaves_like 'project commits', schema: 'public_api/v4/commits_with_stats' do
+ let(:route) { "/projects/#{project_id}/repository/commits?with_stats=true" }
+
+ it 'include commits details' do
+ commit = project.repository.commit
+ get api(route, current_user)
+
+ expect(json_response.first['stats']['additions']).to eq(commit.stats.additions)
+ expect(json_response.first['stats']['deletions']).to eq(commit.stats.deletions)
+ expect(json_response.first['stats']['total']).to eq(commit.stats.total)
+ end
+ end
+ end
+
context 'with pagination params' do
let(:page) { 1 }
let(:per_page) { 5 }
@@ -247,6 +264,19 @@ describe API::Commits do
]
}
end
+ let!(:valid_utf8_c_params) do
+ {
+ branch: 'master',
+ commit_message: message,
+ actions: [
+ {
+ action: 'create',
+ file_path: 'foo/bar/baz.txt',
+ content: 'puts 🦊'
+ }
+ ]
+ }
+ end
it 'a new file in project repo' do
post api(url, user), valid_c_params
@@ -257,6 +287,15 @@ describe API::Commits do
expect(json_response['committer_email']).to eq(user.email)
end
+ it 'a new file with utf8 chars in project repo' do
+ post api(url, user), valid_utf8_c_params
+
+ 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)
+ end
+
it 'returns a 400 bad request if file exists' do
post api(url, user), invalid_c_params
@@ -628,7 +667,7 @@ describe API::Commits do
end
end
- context 'when authenticated', 'as a master' do
+ context 'when authenticated', 'as a maintainer' do
let(:current_user) { user }
it_behaves_like 'ref commit'
@@ -746,7 +785,7 @@ describe API::Commits do
end
end
- context 'when authenticated', 'as a master' do
+ context 'when authenticated', 'as a maintainer' do
let(:current_user) { user }
it_behaves_like 'ref diff'
@@ -845,7 +884,7 @@ describe API::Commits do
end
end
- context 'when authenticated', 'as a master' do
+ context 'when authenticated', 'as a maintainer' do
let(:current_user) { user }
it_behaves_like 'ref comments'
diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb
index ae9c0e9c304..32fc704a79b 100644
--- a/spec/requests/api/deploy_keys_spec.rb
+++ b/spec/requests/api/deploy_keys_spec.rb
@@ -171,7 +171,7 @@ describe API::DeployKeys do
deploy_key
end
- it 'deletes existing key' do
+ it 'removes existing key from project' do
expect do
delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin)
@@ -179,6 +179,44 @@ describe API::DeployKeys do
end.to change { project.deploy_keys.count }.by(-1)
end
+ context 'when the deploy key is public' do
+ it 'does not delete the deploy key' do
+ expect do
+ delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin)
+
+ expect(response).to have_gitlab_http_status(204)
+ end.not_to change { DeployKey.count }
+ end
+ end
+
+ context 'when the deploy key is not public' do
+ let!(:deploy_key) { create(:deploy_key, public: false) }
+
+ context 'when the deploy key is only used by this project' do
+ it 'deletes the deploy key' do
+ expect do
+ delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin)
+
+ expect(response).to have_gitlab_http_status(204)
+ end.to change { DeployKey.count }.by(-1)
+ end
+ end
+
+ context 'when the deploy key is used by other projects' do
+ before do
+ create(:deploy_keys_project, project: project2, deploy_key: deploy_key)
+ end
+
+ it 'does not delete the deploy key' do
+ expect do
+ delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin)
+
+ expect(response).to have_gitlab_http_status(204)
+ end.not_to change { DeployKey.count }
+ end
+ end
+ end
+
it 'returns 404 Not Found with invalid ID' do
delete api("/projects/#{project.id}/deploy_keys/404", admin)
diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb
index 51b70fda148..61ae053cea7 100644
--- a/spec/requests/api/deployments_spec.rb
+++ b/spec/requests/api/deployments_spec.rb
@@ -5,7 +5,7 @@ describe API::Deployments do
let(:non_member) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
describe 'GET /projects/:id/deployments' do
diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb
index fdddca5d0ef..bf93555b0f0 100644
--- a/spec/requests/api/environments_spec.rb
+++ b/spec/requests/api/environments_spec.rb
@@ -7,7 +7,7 @@ describe API::Environments do
let!(:environment) { create(:environment, project: project) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
describe 'GET /projects/:id/environments' do
@@ -126,7 +126,7 @@ describe API::Environments do
end
describe 'DELETE /projects/:id/environments/:environment_id' do
- context 'as a master' do
+ context 'as a maintainer' do
it 'returns a 200 for an existing environment' do
delete api("/projects/#{project.id}/environments/#{environment.id}", user)
@@ -155,7 +155,7 @@ describe API::Environments do
end
describe 'POST /projects/:id/environments/:environment_id/stop' do
- context 'as a master' do
+ context 'as a maintainer' do
context 'with a stoppable environment' do
before do
environment.update(state: :available)
diff --git a/spec/requests/api/events_spec.rb b/spec/requests/api/events_spec.rb
index 962c845f36d..e6a61fdcf39 100644
--- a/spec/requests/api/events_spec.rb
+++ b/spec/requests/api/events_spec.rb
@@ -176,7 +176,7 @@ describe API::Events do
end
it 'avoids N+1 queries' do
- control_count = ActiveRecord::QueryRecorder.new do
+ control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
get api("/projects/#{private_project.id}/events", user), target_type: :merge_request
end.count
@@ -184,7 +184,7 @@ describe API::Events do
expect do
get api("/projects/#{private_project.id}/events", user), target_type: :merge_request
- end.not_to exceed_query_limit(control_count)
+ end.not_to exceed_all_query_limit(control_count)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb
index d8fdfd6dee1..4bc5d3ee899 100644
--- a/spec/requests/api/files_spec.rb
+++ b/spec/requests/api/files_spec.rb
@@ -21,6 +21,89 @@ describe API::Files do
"/projects/#{project.id}/repository/files/#{file_path}"
end
+ describe "HEAD /projects/:id/repository/files/:file_path" do
+ shared_examples_for 'repository files' do
+ it 'returns file attributes in headers' do
+ head api(route(file_path), current_user), params
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response.headers['X-Gitlab-File-Path']).to eq(CGI.unescape(file_path))
+ expect(response.headers['X-Gitlab-File-Name']).to eq('popen.rb')
+ expect(response.headers['X-Gitlab-Last-Commit-Id']).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d')
+ expect(response.headers['X-Gitlab-Content-Sha256']).to eq('c440cd09bae50c4632cc58638ad33c6aa375b6109d811e76a9cc3a613c1e8887')
+ end
+
+ it 'returns file by commit sha' do
+ # This file is deleted on HEAD
+ file_path = "files%2Fjs%2Fcommit%2Ejs%2Ecoffee"
+ params[:ref] = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9"
+
+ head api(route(file_path), current_user), params
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response.headers['X-Gitlab-File-Name']).to eq('commit.js.coffee')
+ expect(response.headers['X-Gitlab-Content-Sha256']).to eq('08785f04375b47f81f46e68cc125d5ef368aa20576ddb53f91f4d83f1d04b929')
+ end
+
+ context 'when mandatory params are not given' do
+ it "responds with a 400 status" do
+ head api(route("any%2Ffile"), current_user)
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+ end
+
+ context 'when file_path does not exist' do
+ it "responds with a 404 status" do
+ params[:ref] = 'master'
+
+ head api(route('app%2Fmodels%2Fapplication%2Erb'), current_user), params
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'when file_path does not exist' do
+ include_context 'disabled repository'
+
+ it "responds with a 403 status" do
+ head api(route(file_path), current_user), params
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+ end
+
+ context 'when unauthenticated', 'and project is public' do
+ it_behaves_like 'repository files' do
+ let(:project) { create(:project, :public, :repository) }
+ let(:current_user) { nil }
+ end
+ end
+
+ context 'when unauthenticated', 'and project is private' do
+ it "responds with a 404 status" do
+ current_user = nil
+
+ head api(route(file_path), current_user), params
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'when authenticated', 'as a developer' do
+ it_behaves_like 'repository files' do
+ let(:current_user) { user }
+ end
+ end
+
+ context 'when authenticated', 'as a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { head api(route(file_path), guest), params }
+ end
+ end
+ end
+
describe "GET /projects/:id/repository/files/:file_path" do
shared_examples_for 'repository files' do
it 'returns file attributes as json' do
@@ -30,6 +113,7 @@ describe API::Files do
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')
+ expect(json_response['content_sha256']).to eq('c440cd09bae50c4632cc58638ad33c6aa375b6109d811e76a9cc3a613c1e8887')
expect(Base64.decode64(json_response['content']).lines.first).to eq("require 'fileutils'\n")
end
@@ -51,6 +135,7 @@ describe API::Files do
expect(response).to have_gitlab_http_status(200)
expect(json_response['file_name']).to eq('commit.js.coffee')
+ expect(json_response['content_sha256']).to eq('08785f04375b47f81f46e68cc125d5ef368aa20576ddb53f91f4d83f1d04b929')
expect(Base64.decode64(json_response['content']).lines.first).to eq("class Commit\n")
end
diff --git a/spec/requests/api/graphql/project/merge_request_spec.rb b/spec/requests/api/graphql/project/merge_request_spec.rb
new file mode 100644
index 00000000000..deb6abbc026
--- /dev/null
+++ b/spec/requests/api/graphql/project/merge_request_spec.rb
@@ -0,0 +1,94 @@
+require 'spec_helper'
+
+describe 'getting merge request information nested in a project' do
+ include GraphqlHelpers
+
+ let(:project) { create(:project, :repository, :public) }
+ let(:current_user) { create(:user) }
+ let(:merge_request_graphql_data) { graphql_data['project']['mergeRequest'] }
+ let!(:merge_request) { create(:merge_request, source_project: project) }
+
+ let(:query) do
+ graphql_query_for(
+ 'project',
+ { 'fullPath' => project.full_path },
+ query_graphql_field('mergeRequest', iid: merge_request.iid)
+ )
+ end
+
+ it_behaves_like 'a working graphql query' do
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+ end
+
+ it 'contains merge request information' do
+ post_graphql(query, current_user: current_user)
+
+ expect(merge_request_graphql_data).not_to be_nil
+ end
+
+ # This is a field coming from the `MergeRequestPresenter`
+ it 'includes a web_url' do
+ post_graphql(query, current_user: current_user)
+
+ expect(merge_request_graphql_data['webUrl']).to be_present
+ end
+
+ context 'permissions on the merge request' do
+ it 'includes the permissions for the current user on a public project' do
+ expected_permissions = {
+ 'readMergeRequest' => true,
+ 'adminMergeRequest' => false,
+ 'createNote' => true,
+ 'pushToSourceBranch' => false,
+ 'removeSourceBranch' => false,
+ 'cherryPickOnCurrentMergeRequest' => false,
+ 'revertOnCurrentMergeRequest' => false,
+ 'updateMergeRequest' => false
+ }
+ post_graphql(query, current_user: current_user)
+
+ permission_data = merge_request_graphql_data['userPermissions']
+
+ expect(permission_data).to be_present
+ expect(permission_data).to eq(expected_permissions)
+ end
+ end
+
+ context 'when the user does not have access to the merge request' do
+ let(:project) { create(:project, :public, :repository) }
+
+ it 'returns nil' do
+ project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE)
+
+ post_graphql(query)
+
+ expect(merge_request_graphql_data).to be_nil
+ end
+ end
+
+ context 'when there are pipelines' do
+ before do
+ pipeline = create(
+ :ci_pipeline,
+ project: merge_request.source_project,
+ ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha
+ )
+ merge_request.update!(head_pipeline: pipeline)
+ end
+
+ it 'has a head pipeline' do
+ post_graphql(query, current_user: current_user)
+
+ expect(merge_request_graphql_data['headPipeline']).to be_present
+ end
+
+ it 'has pipeline connections' do
+ post_graphql(query, current_user: current_user)
+
+ expect(merge_request_graphql_data['pipelines']['edges'].size).to eq(1)
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project_query_spec.rb b/spec/requests/api/graphql/project_query_spec.rb
new file mode 100644
index 00000000000..0727ada4691
--- /dev/null
+++ b/spec/requests/api/graphql/project_query_spec.rb
@@ -0,0 +1,56 @@
+require 'spec_helper'
+
+describe 'getting project information' do
+ include GraphqlHelpers
+
+ let(:project) { create(:project, :repository) }
+ let(:current_user) { create(:user) }
+
+ let(:query) do
+ graphql_query_for('project', 'fullPath' => project.full_path)
+ end
+
+ context 'when the user has access to the project' do
+ before do
+ project.add_developer(current_user)
+ end
+
+ it 'includes the project' do
+ post_graphql(query, current_user: current_user)
+
+ expect(graphql_data['project']).not_to be_nil
+ end
+
+ it_behaves_like 'a working graphql query' do
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+ end
+
+ context 'when there are pipelines present' do
+ before do
+ create(:ci_pipeline, project: project)
+ end
+
+ it 'is included in the pipelines connection' do
+ post_graphql(query, current_user: current_user)
+
+ expect(graphql_data['project']['pipelines']['edges'].size).to eq(1)
+ end
+ end
+ end
+
+ context 'when the user does not have access to the project' do
+ it 'returns an empty field' do
+ post_graphql(query, current_user: current_user)
+
+ expect(graphql_data['project']).to be_nil
+ end
+
+ it_behaves_like 'a working graphql query' do
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/group_variables_spec.rb b/spec/requests/api/group_variables_spec.rb
index 64fa7dc824c..f87e035c89d 100644
--- a/spec/requests/api/group_variables_spec.rb
+++ b/spec/requests/api/group_variables_spec.rb
@@ -9,7 +9,7 @@ describe API::GroupVariables do
context 'authorized user with proper permissions' do
before do
- group.add_master(user)
+ group.add_maintainer(user)
end
it 'returns group variables' do
@@ -42,7 +42,7 @@ describe API::GroupVariables do
context 'authorized user with proper permissions' do
before do
- group.add_master(user)
+ group.add_maintainer(user)
end
it 'returns group variable details' do
@@ -82,7 +82,7 @@ describe API::GroupVariables do
let!(:variable) { create(:ci_group_variable, group: group) }
before do
- group.add_master(user)
+ group.add_maintainer(user)
end
it 'creates variable' do
@@ -138,7 +138,7 @@ describe API::GroupVariables do
context 'authorized user with proper permissions' do
before do
- group.add_master(user)
+ group.add_maintainer(user)
end
it 'updates variable data' do
@@ -184,7 +184,7 @@ describe API::GroupVariables do
context 'authorized user with proper permissions' do
before do
- group.add_master(user)
+ group.add_maintainer(user)
end
it 'deletes variable' do
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 7d923932309..65b387a2170 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -138,10 +138,15 @@ describe API::Groups do
context "when using sorting" do
let(:group3) { create(:group, name: "a#{group1.name}", path: "z#{group1.path}") }
+ let(:group4) { create(:group, name: "same-name", path: "y#{group1.path}") }
+ let(:group5) { create(:group, name: "same-name") }
let(:response_groups) { json_response.map { |group| group['name'] } }
+ let(:response_groups_ids) { json_response.map { |group| group['id'] } }
before do
group3.add_owner(user1)
+ group4.add_owner(user1)
+ group5.add_owner(user1)
end
it "sorts by name ascending by default" do
@@ -150,7 +155,7 @@ describe API::Groups do
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])
+ expect(response_groups).to eq(Group.visible_to_user(user1).order(:name).pluck(:name))
end
it "sorts in descending order when passed" do
@@ -159,22 +164,58 @@ describe API::Groups do
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])
+ expect(response_groups).to eq(Group.visible_to_user(user1).order(name: :desc).pluck(:name))
end
- it "sorts by the order_by param" do
+ it "sorts by path in order_by param" do
get api("/groups", user1), order_by: "path"
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])
+ expect(response_groups).to eq(Group.visible_to_user(user1).order(:path).pluck(:name))
+ end
+
+ it "sorts by id in the order_by param" do
+ get api("/groups", user1), order_by: "id"
+
+ 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(Group.visible_to_user(user1).order(:id).pluck(:name))
+ end
+
+ it "sorts also by descending id with pagination fix" do
+ get api("/groups", user1), order_by: "id", sort: "desc"
+
+ 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(Group.visible_to_user(user1).order(id: :desc).pluck(:name))
+ end
+
+ it "sorts identical keys by id for good pagination" do
+ get api("/groups", user1), search: "same-name", order_by: "name"
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(response_groups_ids).to eq(Group.select { |group| group['name'] == 'same-name' }.map { |group| group['id'] }.sort)
+ end
+
+ it "sorts descending identical keys by id for good pagination" do
+ get api("/groups", user1), search: "same-name", order_by: "name", sort: "desc"
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(response_groups_ids).to eq(Group.select { |group| group['name'] == 'same-name' }.map { |group| group['id'] }.sort)
end
end
context 'when using owned in the request' do
it 'returns an array of groups the user owns' do
- group1.add_master(user2)
+ group1.add_maintainer(user2)
get api('/groups', user2), owned: true
@@ -210,14 +251,22 @@ describe API::Groups do
projects
end
+ def response_project_ids(json_response, key)
+ json_response[key].map do |project|
+ project['id'].to_i
+ end
+ end
+
context 'when unauthenticated' do
it 'returns 404 for a private group' do
get api("/groups/#{group2.id}")
+
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_gitlab_http_status(200)
end
@@ -227,7 +276,7 @@ describe API::Groups do
get api("/groups/#{public_group.id}")
- expect(json_response['projects'].map { |p| p['id'].to_i })
+ expect(response_project_ids(json_response, 'projects'))
.to contain_exactly(projects[:public].id)
end
@@ -237,7 +286,7 @@ describe API::Groups do
get api("/groups/#{group1.id}")
- expect(json_response['shared_projects'].map { |p| p['id'].to_i })
+ expect(response_project_ids(json_response, 'shared_projects'))
.to contain_exactly(projects[:public].id)
end
end
@@ -268,6 +317,17 @@ describe API::Groups do
expect(json_response['shared_projects'][0]['id']).to eq(project.id)
end
+ it "returns one of user1's groups without projects when with_projects option is set to false" do
+ project = create(:project, namespace: group2, path: 'Foo')
+ create(:project_group_link, project: project, group: group1)
+
+ get api("/groups/#{group1.id}", user1), with_projects: false
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['projects']).to be_nil
+ expect(json_response['shared_projects']).to be_nil
+ end
+
it "does not return a non existing group" do
get api("/groups/1328", user1)
@@ -286,7 +346,7 @@ describe API::Groups do
get api("/groups/#{public_group.id}", user2)
- expect(json_response['projects'].map { |p| p['id'].to_i })
+ expect(response_project_ids(json_response, 'projects'))
.to contain_exactly(projects[:public].id, projects[:internal].id)
end
@@ -296,7 +356,7 @@ describe API::Groups do
get api("/groups/#{group1.id}", user2)
- expect(json_response['shared_projects'].map { |p| p['id'].to_i })
+ expect(response_project_ids(json_response, 'shared_projects'))
.to contain_exactly(projects[:public].id, projects[:internal].id)
end
end
@@ -674,9 +734,9 @@ describe API::Groups do
end
end
- context 'as master', :nested_groups do
+ context 'as maintainer', :nested_groups do
before do
- group2.add_master(user1)
+ group2.add_maintainer(user1)
end
it 'cannot create subgroups' do
@@ -752,7 +812,7 @@ describe API::Groups do
it "does not remove a group if not an owner" do
user4 = create(:user)
- group1.add_master(user4)
+ group1.add_maintainer(user4)
delete api("/groups/#{group1.id}", user3)
diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb
index d3ab44c0d7e..0a789d58fd8 100644
--- a/spec/requests/api/helpers_spec.rb
+++ b/spec/requests/api/helpers_spec.rb
@@ -171,7 +171,7 @@ describe API::Helpers do
end
it 'returns a 403 when a user has not accepted the terms' do
- expect { current_user }.to raise_error /You must accept the Terms of Service/
+ expect { current_user }.to raise_error /must accept the Terms of Service/
end
it 'sets the current user when the user accepted the terms' do
@@ -201,7 +201,7 @@ describe API::Helpers do
end
it 'does not allow expired tokens' do
- personal_access_token.update_attributes!(expires_at: 1.day.ago)
+ personal_access_token.update!(expires_at: 1.day.ago)
env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect { current_user }.to raise_error Gitlab::Auth::ExpiredError
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index db8c5f963d6..a56b913198c 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -1,9 +1,9 @@
require 'spec_helper'
describe API::Internal do
- let(:user) { create(:user) }
+ set(:user) { create(:user) }
let(:key) { create(:key, user: user) }
- let(:project) { create(:project, :repository, :wiki_repo) }
+ set(:project) { create(:project, :repository, :wiki_repo) }
let(:secret_token) { Gitlab::Shell.secret_token }
let(:gl_repository) { "project-#{project.id}" }
let(:reference_counter) { double('ReferenceCounter') }
@@ -277,7 +277,7 @@ describe API::Internal do
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["repository_path"]).to eq('/')
expect(json_response["gl_repository"]).to eq("wiki-#{project.id}")
expect(user).not_to have_an_activity_record
end
@@ -289,7 +289,7 @@ describe API::Internal do
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["repository_path"]).to eq('/')
expect(json_response["gl_repository"]).to eq("wiki-#{project.id}")
expect(user).to have_an_activity_record
end
@@ -301,7 +301,7 @@ describe API::Internal do
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["repository_path"]).to eq('/')
expect(json_response["gl_repository"]).to eq("project-#{project.id}")
expect(json_response["gitaly"]).not_to be_nil
expect(json_response["gitaly"]["repository"]).not_to be_nil
@@ -320,7 +320,7 @@ describe API::Internal do
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["repository_path"]).to eq('/')
expect(json_response["gl_repository"]).to eq("project-#{project.id}")
expect(json_response["gitaly"]).not_to be_nil
expect(json_response["gitaly"]["repository"]).not_to be_nil
@@ -522,7 +522,6 @@ describe API::Internal do
context 'the project path was changed' do
let(:project) { create(:project, :repository, :legacy_storage) }
- let!(:old_path_to_repo) { project.repository.path_to_repo }
let!(:repository) { project.repository }
before do
@@ -835,8 +834,7 @@ describe API::Internal do
end
def push(key, project, protocol = 'ssh', env: nil)
- post(
- api("/internal/allowed"),
+ params = {
changes: 'd14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/master',
key_id: key.id,
project: project.full_path,
@@ -845,7 +843,19 @@ describe API::Internal do
secret_token: secret_token,
protocol: protocol,
env: env
- )
+ }
+
+ if Gitlab.rails5?
+ post(
+ api("/internal/allowed"),
+ params: params
+ )
+ else
+ post(
+ api("/internal/allowed"),
+ params
+ )
+ end
end
def archive(key, project)
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 3106083293f..66eb18229fa 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -630,15 +630,17 @@ describe API::Issues do
end
it 'avoids N+1 queries' do
- control_count = ActiveRecord::QueryRecorder.new do
+ get api("/projects/#{project.id}/issues", user)
+
+ control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
get api("/projects/#{project.id}/issues", user)
end.count
- create(:issue, author: user, project: project)
+ create_list(:issue, 3, project: project)
expect do
get api("/projects/#{project.id}/issues", user)
- end.not_to exceed_query_limit(control_count)
+ end.not_to exceed_all_query_limit(control_count)
end
it 'returns 404 when project does not exist' do
@@ -1081,7 +1083,7 @@ describe API::Issues do
let(:project) { merge_request.source_project }
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
context 'resolving all discussions in a merge request' do
@@ -1351,19 +1353,25 @@ describe API::Issues do
expect(json_response['labels']).to eq([label.title])
end
- it 'removes all labels' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user), labels: ''
+ it 'removes all labels and touches the record' do
+ Timecop.travel(1.minute.from_now) do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user), labels: ''
+ end
expect(response).to have_gitlab_http_status(200)
expect(json_response['labels']).to eq([])
+ expect(json_response['updated_at']).to be > Time.now
end
- it 'updates labels' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+ it 'updates labels and touches the record' do
+ Timecop.travel(1.minute.from_now) do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
labels: 'foo,bar'
+ end
expect(response).to have_gitlab_http_status(200)
expect(json_response['labels']).to include 'foo'
expect(json_response['labels']).to include 'bar'
+ expect(json_response['updated_at']).to be > Time.now
end
it 'allows special label names' do
@@ -1671,7 +1679,7 @@ describe API::Issues do
let!(:user_agent_detail) { create(:user_agent_detail, subject: issue) }
context 'when unauthenticated' do
- it "returns unautorized" do
+ it "returns unauthorized" do
get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail")
expect(response).to have_gitlab_http_status(401)
@@ -1687,7 +1695,7 @@ describe API::Issues do
expect(json_response['akismet_submitted']).to eq(user_agent_detail.submitted)
end
- it "returns unautorized for non-admin users" do
+ it "returns unauthorized for non-admin users" do
get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail", user)
expect(response).to have_gitlab_http_status(403)
diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb
index 0a2963452e4..7d1a5c12805 100644
--- a/spec/requests/api/jobs_spec.rb
+++ b/spec/requests/api/jobs_spec.rb
@@ -13,7 +13,10 @@ describe API::Jobs do
ref: project.default_branch)
end
- let!(:job) { create(:ci_build, :success, pipeline: pipeline) }
+ let!(:job) do
+ create(:ci_build, :success, pipeline: pipeline,
+ artifacts_expire_at: 1.day.since)
+ end
let(:user) { create(:user) }
let(:api_user) { user }
@@ -43,6 +46,7 @@ describe API::Jobs do
it 'returns correct values' do
expect(json_response).not_to be_empty
expect(json_response.first['commit']['id']).to eq project.commit.id
+ expect(Time.parse(json_response.first['artifacts_expire_at'])).to be_like_time(job.artifacts_expire_at)
end
it 'returns pipeline data' do
@@ -128,6 +132,7 @@ describe API::Jobs do
it 'returns correct values' do
expect(json_response).not_to be_empty
expect(json_response.first['commit']['id']).to eq project.commit.id
+ expect(Time.parse(json_response.first['artifacts_expire_at'])).to be_like_time(job.artifacts_expire_at)
end
it 'returns pipeline data' do
@@ -172,6 +177,18 @@ describe API::Jobs do
json_response.each { |job| expect(job['pipeline']['id']).to eq(pipeline.id) }
end
end
+
+ it 'avoids N+1 queries' do
+ control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ get api("/projects/#{project.id}/pipelines/#{pipeline.id}/jobs", api_user), query
+ end.count
+
+ 3.times { create(:ci_build, :artifacts, pipeline: pipeline) }
+
+ expect do
+ get api("/projects/#{project.id}/pipelines/#{pipeline.id}/jobs", api_user), query
+ end.not_to exceed_all_query_limit(control_count)
+ end
end
context 'unauthorized user' do
@@ -201,6 +218,7 @@ describe API::Jobs do
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(Time.parse(json_response['artifacts_expire_at'])).to be_like_time(job.artifacts_expire_at)
expect(json_response['duration']).to eq(job.duration)
end
@@ -517,12 +535,14 @@ describe API::Jobs do
context 'authorized user' do
context 'when trace is in ObjectStorage' do
let!(:job) { create(:ci_build, :trace_artifact, pipeline: pipeline) }
+ let(:url) { 'http://object-storage/trace' }
+ let(:file_path) { expand_fixture_path('trace/sample_trace') }
before do
- stub_remote_trace_206
+ stub_remote_url_206(url, file_path)
allow_any_instance_of(JobArtifactUploader).to receive(:file_storage?) { false }
- allow_any_instance_of(JobArtifactUploader).to receive(:url) { remote_trace_url }
- allow_any_instance_of(JobArtifactUploader).to receive(:size) { remote_trace_size }
+ allow_any_instance_of(JobArtifactUploader).to receive(:url) { url }
+ allow_any_instance_of(JobArtifactUploader).to receive(:size) { File.size(file_path) }
end
it 'returns specific job trace' do
@@ -625,7 +645,7 @@ describe API::Jobs do
end
describe 'POST /projects/:id/jobs/:job_id/erase' do
- let(:role) { :master }
+ let(:role) { :maintainer }
before do
project.add_role(user, role)
diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb
index 34cbf75f4c1..a4220f5b2be 100644
--- a/spec/requests/api/labels_spec.rb
+++ b/spec/requests/api/labels_spec.rb
@@ -7,7 +7,7 @@ describe API::Labels do
let!(:priority_label) { create(:label, title: 'bug', project: project, priority: 3) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
describe 'GET /projects/:id/labels' do
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index ec500838eb2..01bbe7f5ec6 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -1,15 +1,15 @@
require 'spec_helper'
describe API::Members do
- let(:master) { create(:user, username: 'master_user') }
+ let(:maintainer) { create(:user, username: 'maintainer_user') }
let(:developer) { create(:user) }
let(:access_requester) { create(:user) }
let(:stranger) { create(:user) }
let(:project) do
- create(:project, :public, :access_requestable, creator_id: master.id, namespace: master.namespace) do |project|
+ create(:project, :public, :access_requestable, creator_id: maintainer.id, namespace: maintainer.namespace) do |project|
project.add_developer(developer)
- project.add_master(master)
+ project.add_maintainer(maintainer)
project.request_access(access_requester)
end
end
@@ -17,7 +17,7 @@ describe API::Members do
let!(:group) do
create(:group, :public, :access_requestable) do |group|
group.add_developer(developer)
- group.add_owner(master)
+ group.add_owner(maintainer)
group.request_access(access_requester)
end
end
@@ -28,7 +28,7 @@ describe API::Members do
let(:route) { get api("/#{source_type.pluralize}/#{source.id}/members", stranger) }
end
- %i[master developer access_requester stranger].each do |type|
+ %i[maintainer developer access_requester stranger].each do |type|
context "when authenticated as a #{type}" do
it 'returns 200' do
user = public_send(type)
@@ -39,23 +39,23 @@ describe API::Members do
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(2)
- expect(json_response.map { |u| u['id'] }).to match_array [master.id, developer.id]
+ expect(json_response.map { |u| u['id'] }).to match_array [maintainer.id, developer.id]
end
end
end
it 'avoids N+1 queries' do
# Establish baseline
- get api("/#{source_type.pluralize}/#{source.id}/members", master)
+ get api("/#{source_type.pluralize}/#{source.id}/members", maintainer)
control = ActiveRecord::QueryRecorder.new do
- get api("/#{source_type.pluralize}/#{source.id}/members", master)
+ get api("/#{source_type.pluralize}/#{source.id}/members", maintainer)
end
project.add_developer(create(:user))
expect do
- get api("/#{source_type.pluralize}/#{source.id}/members", master)
+ get api("/#{source_type.pluralize}/#{source.id}/members", maintainer)
end.not_to exceed_query_limit(control)
end
@@ -68,17 +68,17 @@ describe API::Members do
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(2)
- expect(json_response.map { |u| u['id'] }).to match_array [master.id, developer.id]
+ expect(json_response.map { |u| u['id'] }).to match_array [maintainer.id, developer.id]
end
it 'finds members with query string' do
- get api("/#{source_type.pluralize}/#{source.id}/members", developer), query: master.username
+ get api("/#{source_type.pluralize}/#{source.id}/members", developer), query: maintainer.username
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)
- expect(json_response.first['username']).to eq(master.username)
+ expect(json_response.first['username']).to eq(maintainer.username)
end
it 'finds all members with no query specified' do
@@ -88,7 +88,7 @@ describe API::Members do
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.count).to eq(2)
- expect(json_response.map { |u| u['id'] }).to match_array [master.id, developer.id]
+ expect(json_response.map { |u| u['id'] }).to match_array [maintainer.id, developer.id]
end
end
end
@@ -129,7 +129,7 @@ describe API::Members do
it_behaves_like 'a 404 response when source is private' do
let(:route) do
post api("/#{source_type.pluralize}/#{source.id}/members", stranger),
- user_id: access_requester.id, access_level: Member::MASTER
+ user_id: access_requester.id, access_level: Member::MAINTAINER
end
end
@@ -139,7 +139,7 @@ describe API::Members do
it 'returns 403' do
user = public_send(type)
post api("/#{source_type.pluralize}/#{source.id}/members", user),
- user_id: access_requester.id, access_level: Member::MASTER
+ user_id: access_requester.id, access_level: Member::MAINTAINER
expect(response).to have_gitlab_http_status(403)
end
@@ -147,24 +147,24 @@ describe API::Members do
end
end
- context 'when authenticated as a master/owner' do
+ context 'when authenticated as a maintainer/owner' do
context 'and new member is already a requester' do
it 'transforms the requester into a proper member' do
expect do
- post api("/#{source_type.pluralize}/#{source.id}/members", master),
- user_id: access_requester.id, access_level: Member::MASTER
+ post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
+ user_id: access_requester.id, access_level: Member::MAINTAINER
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)
- expect(json_response['access_level']).to eq(Member::MASTER)
+ expect(json_response['access_level']).to eq(Member::MAINTAINER)
end
end
it 'creates a new member' do
expect do
- post api("/#{source_type.pluralize}/#{source.id}/members", master),
+ post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
user_id: stranger.id, access_level: Member::DEVELOPER, expires_at: '2016-08-05'
expect(response).to have_gitlab_http_status(201)
@@ -176,28 +176,28 @@ describe API::Members do
end
it "returns 409 if member already exists" do
- post api("/#{source_type.pluralize}/#{source.id}/members", master),
- user_id: master.id, access_level: Member::MASTER
+ post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
+ user_id: maintainer.id, access_level: Member::MAINTAINER
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
+ post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
+ access_level: Member::MAINTAINER
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),
+ post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
user_id: stranger.id
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),
+ post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
user_id: stranger.id, access_level: 1234
expect(response).to have_gitlab_http_status(400)
@@ -210,7 +210,7 @@ describe API::Members do
it_behaves_like 'a 404 response when source is private' do
let(:route) do
put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger),
- access_level: Member::MASTER
+ access_level: Member::MAINTAINER
end
end
@@ -220,7 +220,7 @@ describe API::Members do
it 'returns 403' do
user = public_send(type)
put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user),
- access_level: Member::MASTER
+ access_level: Member::MAINTAINER
expect(response).to have_gitlab_http_status(403)
end
@@ -228,33 +228,33 @@ describe API::Members do
end
end
- context 'when authenticated as a master/owner' do
+ context 'when authenticated as a maintainer/owner' do
it 'updates the member' do
- put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master),
- access_level: Member::MASTER, expires_at: '2016-08-05'
+ put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", maintainer),
+ access_level: Member::MAINTAINER, expires_at: '2016-08-05'
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['access_level']).to eq(Member::MAINTAINER)
expect(json_response['expires_at']).to eq('2016-08-05')
end
end
it 'returns 409 if member does not exist' do
- put api("/#{source_type.pluralize}/#{source.id}/members/123", master),
- access_level: Member::MASTER
+ put api("/#{source_type.pluralize}/#{source.id}/members/123", maintainer),
+ access_level: Member::MAINTAINER
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)
+ put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", maintainer)
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),
+ put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", maintainer),
access_level: 1234
expect(response).to have_gitlab_http_status(400)
@@ -291,11 +291,11 @@ describe API::Members do
end
end
- context 'when authenticated as a master/owner' do
+ context 'when authenticated as a maintainer/owner' do
context 'and member is a requester' do
it 'returns 404' do
expect do
- delete api("/#{source_type.pluralize}/#{source.id}/members/#{access_requester.id}", master)
+ delete api("/#{source_type.pluralize}/#{source.id}/members/#{access_requester.id}", maintainer)
expect(response).to have_gitlab_http_status(404)
end.not_to change { source.requesters.count }
@@ -304,19 +304,19 @@ describe API::Members do
it 'deletes the member' do
expect do
- delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master)
+ delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", maintainer)
expect(response).to have_gitlab_http_status(204)
end.to change { source.members.count }.by(-1)
end
it_behaves_like '412 response' do
- let(:request) { api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master) }
+ let(:request) { api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", maintainer) }
end
end
it 'returns 404 if member does not exist' do
- delete api("/#{source_type.pluralize}/#{source.id}/members/123", master)
+ delete api("/#{source_type.pluralize}/#{source.id}/members/123", maintainer)
expect(response).to have_gitlab_http_status(404)
end
@@ -366,7 +366,7 @@ describe API::Members do
context 'Adding owner to project' do
it 'returns 403' do
expect do
- post api("/projects/#{project.id}/members", master),
+ post api("/projects/#{project.id}/members", maintainer),
user_id: stranger.id, access_level: Member::OWNER
expect(response).to have_gitlab_http_status(400)
diff --git a/spec/requests/api/merge_request_diffs_spec.rb b/spec/requests/api/merge_request_diffs_spec.rb
index cb647aee70f..6530dc956cb 100644
--- a/spec/requests/api/merge_request_diffs_spec.rb
+++ b/spec/requests/api/merge_request_diffs_spec.rb
@@ -8,7 +8,7 @@ describe API::MergeRequestDiffs, 'MergeRequestDiffs' do
before do
merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9')
merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e')
- project.add_master(user)
+ project.add_maintainer(user)
end
describe 'GET /projects/:id/merge_requests/:merge_request_iid/versions' do
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 1eeeb4f1045..1716d182782 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -14,6 +14,7 @@ describe API::MergeRequests do
let!(:merge_request) { create(:merge_request, :simple, milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) }
let!(:merge_request_closed) { create(:merge_request, state: "closed", milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Closed test", created_at: base_time + 1.second) }
let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds, merge_commit_sha: '9999999999999999999999999999999999999999') }
+ let!(:merge_request_locked) { create(:merge_request, state: "locked", milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Locked test", created_at: base_time + 1.second) }
let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") }
let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") }
let!(:label) do
@@ -29,6 +30,18 @@ describe API::MergeRequests do
project.add_reporter(user)
end
+ describe 'route shadowing' do
+ include GrapePathHelpers::NamedRouteMatcher
+
+ it 'does not occur' do
+ path = api_v4_projects_merge_requests_path(id: 1)
+ expect(path).to eq('/api/v4/projects/1/merge_requests')
+
+ path = api_v4_projects_merge_requests_path(id: 1, merge_request_iid: 3)
+ expect(path).to eq('/api/v4/projects/1/merge_requests/3')
+ end
+ end
+
describe 'GET /merge_requests' do
context 'when unauthenticated' do
it 'returns an array of all merge requests' do
@@ -60,12 +73,6 @@ describe API::MergeRequests do
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_gitlab_http_status(401)
- end
end
context 'when authenticated' do
@@ -79,7 +86,7 @@ describe API::MergeRequests do
get api('/merge_requests', user), scope: :all
- expect_response_contain_exactly(merge_request2, merge_request_merged, merge_request_closed, merge_request)
+ expect_response_contain_exactly(merge_request2, merge_request_merged, merge_request_closed, merge_request, merge_request_locked)
expect(json_response.map { |mr| mr['id'] }).not_to include(merge_request3.id)
end
@@ -152,7 +159,7 @@ describe API::MergeRequests do
it 'returns merge requests with the given source branch' do
get api('/merge_requests', user), source_branch: merge_request_closed.source_branch, state: 'all'
- expect_response_contain_exactly(merge_request_closed, merge_request_merged)
+ expect_response_contain_exactly(merge_request_closed, merge_request_merged, merge_request_locked)
end
end
@@ -160,7 +167,7 @@ describe API::MergeRequests do
it 'returns merge requests with the given target branch' do
get api('/merge_requests', user), target_branch: merge_request_closed.target_branch, state: 'all'
- expect_response_contain_exactly(merge_request_closed, merge_request_merged)
+ expect_response_contain_exactly(merge_request_closed, merge_request_merged, merge_request_locked)
end
end
@@ -213,227 +220,58 @@ describe API::MergeRequests do
expect_response_ordered_exactly(merge_request)
end
end
- end
- end
-
- describe "GET /projects/:id/merge_requests" do
- context "when unauthenticated" do
- it 'returns merge requests for public projects' do
- get api("/projects/#{project.id}/merge_requests")
-
- expect_paginated_array_response
- end
- it "returns 404 for non public projects" do
- project = create(:project, :private)
- get api("/projects/#{project.id}/merge_requests")
+ context 'state param' do
+ it 'returns merge requests with the given state' do
+ get api('/merge_requests', user), state: 'locked'
- expect(response).to have_gitlab_http_status(404)
- end
- end
-
- context "when authenticated" do
- it 'avoids N+1 queries' do
- control = ActiveRecord::QueryRecorder.new do
- get api("/projects/#{project.id}/merge_requests", user)
+ expect_response_contain_exactly(merge_request_locked)
end
-
- create(:merge_request, state: 'closed', milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time)
-
- create(:merge_request, 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)
- end
-
- it "returns an array of all merge_requests" do
- get api("/projects/#{project.id}/merge_requests", user)
-
- expect_response_ordered_exactly(merge_request_merged, merge_request_closed, merge_request)
- expect(json_response.last['title']).to eq(merge_request.title)
- expect(json_response.last).to have_key('web_url')
- expect(json_response.last['sha']).to eq(merge_request.diff_head_sha)
- expect(json_response.last['merge_commit_sha']).to be_nil
- expect(json_response.last['merge_commit_sha']).to eq(merge_request.merge_commit_sha)
- expect(json_response.last['downvotes']).to eq(1)
- expect(json_response.last['upvotes']).to eq(1)
- expect(json_response.last['labels']).to eq([label2.title, label.title])
- expect(json_response.first['title']).to eq(merge_request_merged.title)
- expect(json_response.first['sha']).to eq(merge_request_merged.diff_head_sha)
- expect(json_response.first['merge_commit_sha']).not_to be_nil
- expect(json_response.first['merge_commit_sha']).to eq(merge_request_merged.merge_commit_sha)
- end
-
- it "returns an array of all merge_requests using simple mode" do
- get api("/projects/#{project.id}/merge_requests?view=simple", user)
-
- expect_response_ordered_exactly(merge_request_merged, merge_request_closed, merge_request)
- expect(json_response.last.keys).to match_array(%w(id iid title web_url created_at description project_id state updated_at))
- expect(json_response.last['iid']).to eq(merge_request.iid)
- expect(json_response.last['title']).to eq(merge_request.title)
- expect(json_response.last).to have_key('web_url')
- expect(json_response.first['iid']).to eq(merge_request_merged.iid)
- expect(json_response.first['title']).to eq(merge_request_merged.title)
- expect(json_response.first).to have_key('web_url')
- end
-
- it "returns an array of all merge_requests" do
- get api("/projects/#{project.id}/merge_requests?state", user)
-
- expect_response_ordered_exactly(merge_request_merged, merge_request_closed, merge_request)
- expect(json_response.last['title']).to eq(merge_request.title)
- end
-
- it "returns an array of open merge_requests" do
- get api("/projects/#{project.id}/merge_requests?state=opened", user)
-
- expect_response_ordered_exactly(merge_request)
- expect(json_response.last['title']).to eq(merge_request.title)
- end
-
- it "returns an array of closed merge_requests" do
- get api("/projects/#{project.id}/merge_requests?state=closed", user)
-
- expect_response_ordered_exactly(merge_request_closed)
- expect(json_response.first['title']).to eq(merge_request_closed.title)
- end
-
- it "returns an array of merged merge_requests" do
- get api("/projects/#{project.id}/merge_requests?state=merged", user)
-
- expect_response_ordered_exactly(merge_request_merged)
- expect(json_response.first['title']).to eq(merge_request_merged.title)
- end
-
- it 'returns merge_request by "iids" array' do
- get api("/projects/#{project.id}/merge_requests", user), iids: [merge_request.iid, merge_request_closed.iid]
-
- expect_response_ordered_exactly(merge_request_closed, merge_request)
- expect(json_response.first['title']).to eq merge_request_closed.title
- end
-
- it 'matches V4 response schema' do
- get api("/projects/#{project.id}/merge_requests", user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to match_response_schema('public_api/v4/merge_requests')
- end
-
- it 'returns an empty array if no issue matches milestone' do
- get api("/projects/#{project.id}/merge_requests", user), milestone: '1.0.0'
-
- expect_paginated_array_response
- expect(json_response.length).to eq(0)
- end
-
- it 'returns an empty array if milestone does not exist' do
- get api("/projects/#{project.id}/merge_requests", user), milestone: 'foo'
-
- expect_paginated_array_response
- expect(json_response.length).to eq(0)
- end
-
- it 'returns an array of merge requests in given milestone' do
- get api("/projects/#{project.id}/merge_requests", user), milestone: '0.9'
-
- expect(json_response.first['title']).to eq merge_request_closed.title
- expect(json_response.first['id']).to eq merge_request_closed.id
- end
-
- it 'returns an array of merge requests matching state in milestone' do
- get api("/projects/#{project.id}/merge_requests", user), milestone: '0.9', state: 'closed'
-
- expect_response_ordered_exactly(merge_request_closed)
- end
-
- it 'returns an array of labeled merge requests' do
- get api("/projects/#{project.id}/merge_requests?labels=#{label.title}", user)
-
- expect_paginated_array_response
- expect(json_response.length).to eq(1)
- expect(json_response.first['labels']).to eq([label2.title, label.title])
- end
-
- it 'returns an array of labeled merge requests where all labels match' do
- get api("/projects/#{project.id}/merge_requests?labels=#{label.title},foo,bar", user)
-
- expect_paginated_array_response
- expect(json_response.length).to eq(0)
- end
-
- it 'returns an empty array if no merge request matches labels' do
- get api("/projects/#{project.id}/merge_requests?labels=foo,bar", user)
-
- expect_paginated_array_response
- expect(json_response.length).to eq(0)
- end
-
- it 'returns an array of labeled merge requests that are merged for a milestone' do
- bug_label = create(:label, title: 'bug', color: '#FFAABB', project: project)
-
- mr1 = create(:merge_request, state: "merged", source_project: project, target_project: project, milestone: milestone)
- mr2 = create(:merge_request, state: "merged", source_project: project, target_project: project, milestone: milestone1)
- mr3 = create(:merge_request, state: "closed", source_project: project, target_project: project, milestone: milestone1)
- _mr = create(:merge_request, state: "merged", source_project: project, target_project: project, milestone: milestone1)
-
- create(:label_link, label: bug_label, target: mr1)
- create(:label_link, label: bug_label, target: mr2)
- create(:label_link, label: bug_label, target: mr3)
-
- get api("/projects/#{project.id}/merge_requests?labels=#{bug_label.title}&milestone=#{milestone1.title}&state=merged", user)
-
- expect_response_ordered_exactly(mr2)
end
+ end
+ end
- context "with ordering" do
- let(:merge_requests) { [merge_request_merged, merge_request_closed, merge_request] }
-
- before do
- @mr_later = mr_with_later_created_and_updated_at_time
- @mr_earlier = mr_with_earlier_created_and_updated_at_time
- end
-
- it "returns an array of merge_requests in ascending order" do
- get api("/projects/#{project.id}/merge_requests?sort=asc", user)
+ describe "GET /projects/:id/merge_requests" do
+ let(:endpoint_path) { "/projects/#{project.id}/merge_requests" }
- expect_response_ordered_exactly(*merge_requests.sort_by { |mr| mr['created_at'] })
- end
+ it_behaves_like 'merge requests list'
- it "returns an array of merge_requests in descending order" do
- get api("/projects/#{project.id}/merge_requests?sort=desc", user)
+ it "returns 404 for non public projects" do
+ project = create(:project, :private)
- expect_response_ordered_exactly(*merge_requests.sort_by { |mr| mr['created_at'] }.reverse)
- end
+ get api("/projects/#{project.id}/merge_requests")
- it "returns an array of merge_requests ordered by updated_at" do
- get api("/projects/#{project.id}/merge_requests?order_by=updated_at", user)
+ expect(response).to have_gitlab_http_status(404)
+ end
- expect_response_ordered_exactly(*merge_requests.sort_by { |mr| mr['updated_at'] }.reverse)
- end
+ it 'returns merge_request by "iids" array' do
+ get api(endpoint_path, user), iids: [merge_request.iid, merge_request_closed.iid]
- it "returns an array of merge_requests ordered by created_at" do
- get api("/projects/#{project.id}/merge_requests?order_by=created_at&sort=asc", user)
+ expect(response).to have_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
+ expect(json_response.first['id']).to eq merge_request_closed.id
+ end
+ end
- expect_response_ordered_exactly(*merge_requests.sort_by { |mr| mr['created_at'] })
- end
- end
+ describe "GET /groups/:id/merge_requests" do
+ let!(:group) { create(:group, :public) }
+ let!(:project) { create(:project, :public, :repository, creator: user, namespace: group, only_allow_merge_if_pipeline_succeeds: false) }
+ let(:endpoint_path) { "/groups/#{group.id}/merge_requests" }
- context 'source_branch param' do
- it 'returns merge requests with the given source branch' do
- get api('/merge_requests', user), source_branch: merge_request_closed.source_branch, state: 'all'
+ before do
+ group.add_reporter(user)
+ end
- expect_response_contain_exactly(merge_request_closed, merge_request_merged)
- end
- end
+ it_behaves_like 'merge requests list'
- context 'target_branch param' do
- it 'returns merge requests with the given target branch' do
- get api('/merge_requests', user), target_branch: merge_request_closed.target_branch, state: 'all'
+ context 'when have subgroups', :nested_groups do
+ let!(:group) { create(:group, :public) }
+ let!(:subgroup) { create(:group, parent: group) }
+ let!(:project) { create(:project, :public, :repository, creator: user, namespace: subgroup, only_allow_merge_if_pipeline_succeeds: false) }
- expect_response_contain_exactly(merge_request_closed, merge_request_merged)
- end
- end
+ it_behaves_like 'merge requests list'
end
end
@@ -468,6 +306,14 @@ describe API::MergeRequests do
expect(json_response['changes_count']).to eq(merge_request.merge_request_diff.real_size)
end
+ it 'exposes description and title html when render_html is true' do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), render_html: true
+
+ expect(response).to have_gitlab_http_status(200)
+
+ expect(json_response).to include('title_html', 'description_html')
+ end
+
context 'merge_request_metrics' do
before do
merge_request.metrics.update!(merged_by: user,
@@ -557,12 +403,13 @@ describe API::MergeRequests do
source_project: forked_project,
target_project: project,
source_branch: 'fixes',
- allow_maintainer_to_push: true)
+ allow_collaboration: true)
end
- it 'includes the `allow_maintainer_to_push` field' do
+ it 'includes the `allow_collaboration` field' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user)
+ expect(json_response['allow_collaboration']).to be_truthy
expect(json_response['allow_maintainer_to_push']).to be_truthy
end
end
@@ -671,12 +518,14 @@ describe API::MergeRequests do
target_branch: 'master',
author: user,
labels: 'label, label2',
- milestone_id: milestone.id
+ milestone_id: milestone.id,
+ squash: true
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)
+ expect(json_response['squash']).to be_truthy
expect(json_response['force_remove_source_branch']).to be_falsy
end
@@ -823,11 +672,12 @@ describe API::MergeRequests do
expect(response).to have_gitlab_http_status(400)
end
- it 'allows setting `allow_maintainer_to_push`' do
+ it 'allows setting `allow_collaboration`' do
post api("/projects/#{forked_project.id}/merge_requests", user2),
- title: 'Test merge_request', source_branch: "feature_conflict", target_branch: "master",
- author: user2, target_project_id: project.id, allow_maintainer_to_push: true
+ title: 'Test merge_request', source_branch: "feature_conflict", target_branch: "master",
+ author: user2, target_project_id: project.id, allow_collaboration: true
expect(response).to have_gitlab_http_status(201)
+ expect(json_response['allow_collaboration']).to be_truthy
expect(json_response['allow_maintainer_to_push']).to be_truthy
end
@@ -965,6 +815,14 @@ describe API::MergeRequests do
expect(response).to have_gitlab_http_status(200)
end
+ it "updates the MR's squash attribute" do
+ expect do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user), squash: true
+ end.to change { merge_request.reload.squash }
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+
it "enables merge when pipeline succeeds if the pipeline is active" do
allow_any_instance_of(MergeRequest).to receive(:head_pipeline).and_return(pipeline)
allow(pipeline).to receive(:active?).and_return(true)
@@ -1029,6 +887,13 @@ describe API::MergeRequests do
expect(json_response['milestone']['id']).to eq(milestone.id)
end
+ it "updates squash and returns merge_request" do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), squash: true
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['squash']).to be_truthy
+ end
+
it "returns merge_request with renamed target_branch" do
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), target_branch: "wiki"
expect(response).to have_gitlab_http_status(200)
diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb
index 98102fcd6a7..e2000ab42e8 100644
--- a/spec/requests/api/namespaces_spec.rb
+++ b/spec/requests/api/namespaces_spec.rb
@@ -23,10 +23,10 @@ describe API::Namespaces do
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')
+ expect(group_kind_json_response.keys).to include('id', 'kind', 'name', 'path', 'full_path',
+ 'parent_id', 'members_count_with_descendants')
- expect(user_kind_json_response.keys).to contain_exactly('id', 'kind', 'name', 'path', 'full_path', 'parent_id')
+ expect(user_kind_json_response.keys).to include('id', 'kind', 'name', 'path', 'full_path', 'parent_id')
end
it "admin: returns an array of all namespaces" do
@@ -58,8 +58,8 @@ describe API::Namespaces do
owned_group_response = json_response.find { |resource| resource['id'] == group1.id }
- expect(owned_group_response.keys).to contain_exactly('id', 'kind', 'name', 'path', 'full_path',
- 'parent_id', 'members_count_with_descendants')
+ expect(owned_group_response.keys).to include('id', 'kind', 'name', 'path', 'full_path',
+ 'parent_id', 'members_count_with_descendants')
end
it "returns correct attributes when user cannot admin group" do
@@ -69,7 +69,7 @@ describe API::Namespaces do
guest_group_response = json_response.find { |resource| resource['id'] == group1.id }
- expect(guest_group_response.keys).to contain_exactly('id', 'kind', 'name', 'path', 'full_path', 'parent_id')
+ expect(guest_group_response.keys).to include('id', 'kind', 'name', 'path', 'full_path', 'parent_id')
end
it "user: returns an array of namespaces" do
diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb
index dd568c24c72..3fb45449c74 100644
--- a/spec/requests/api/notes_spec.rb
+++ b/spec/requests/api/notes_spec.rb
@@ -44,7 +44,7 @@ describe API::Notes do
# For testing the cross-reference of a private issue in a public project
let(:private_project) do
create(:project, namespace: private_user.namespace)
- .tap { |p| p.add_master(private_user) }
+ .tap { |p| p.add_maintainer(private_user) }
end
let(:private_issue) { create(:issue, project: private_project) }
@@ -71,7 +71,7 @@ describe API::Notes do
context "issue is confidential" do
before do
- ext_issue.update_attributes(confidential: true)
+ ext_issue.update(confidential: true)
end
it "returns 404" do
@@ -104,7 +104,7 @@ describe API::Notes do
context "when issue is confidential" do
before do
- issue.update_attributes(confidential: true)
+ issue.update(confidential: true)
end
it "returns 404" do
diff --git a/spec/requests/api/pages_domains_spec.rb b/spec/requests/api/pages_domains_spec.rb
index a9ccbb32666..35b6ed8d5c0 100644
--- a/spec/requests/api/pages_domains_spec.rb
+++ b/spec/requests/api/pages_domains_spec.rb
@@ -80,7 +80,7 @@ describe API::PagesDomains do
context 'when pages is disabled' do
before do
allow(Gitlab.config.pages).to receive(:enabled).and_return(false)
- project.add_master(user)
+ project.add_maintainer(user)
end
it_behaves_like '404 response' do
@@ -88,9 +88,9 @@ describe API::PagesDomains do
end
end
- context 'when user is a master' do
+ context 'when user is a maintainer' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
it_behaves_like 'get pages domains'
@@ -177,7 +177,7 @@ describe API::PagesDomains do
context 'when domain is vacant' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
it_behaves_like '404 response' do
@@ -185,9 +185,9 @@ describe API::PagesDomains do
end
end
- context 'when user is a master' do
+ context 'when user is a maintainer' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
it_behaves_like 'get pages domain'
@@ -270,9 +270,9 @@ describe API::PagesDomains do
end
end
- context 'when user is a master' do
+ context 'when user is a maintainer' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
it_behaves_like 'post pages domains'
@@ -380,7 +380,7 @@ describe API::PagesDomains do
context 'when domain is vacant' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
it_behaves_like '404 response' do
@@ -388,9 +388,9 @@ describe API::PagesDomains do
end
end
- context 'when user is a master' do
+ context 'when user is a maintainer' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
it_behaves_like 'put pages domain'
@@ -444,7 +444,7 @@ describe API::PagesDomains do
context 'when domain is vacant' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
it_behaves_like '404 response' do
@@ -452,9 +452,9 @@ describe API::PagesDomains do
end
end
- context 'when user is a master' do
+ context 'when user is a maintainer' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
it_behaves_like 'delete pages domain'
diff --git a/spec/requests/api/pipeline_schedules_spec.rb b/spec/requests/api/pipeline_schedules_spec.rb
index 91d4d5d3de9..997d413eb4f 100644
--- a/spec/requests/api/pipeline_schedules_spec.rb
+++ b/spec/requests/api/pipeline_schedules_spec.rb
@@ -22,7 +22,7 @@ describe API::PipelineSchedules do
.each do |pipeline_schedule|
create(:user).tap do |user|
project.add_developer(user)
- pipeline_schedule.update_attributes(owner: user)
+ pipeline_schedule.update(owner: user)
end
pipeline_schedule.pipelines << build(:ci_pipeline, project: project)
end
@@ -270,38 +270,38 @@ describe API::PipelineSchedules do
end
describe 'DELETE /projects/:id/pipeline_schedules/:pipeline_schedule_id' do
- let(:master) { create(:user) }
+ let(:maintainer) { create(:user) }
let!(:pipeline_schedule) do
create(:ci_pipeline_schedule, project: project, owner: developer)
end
before do
- project.add_master(master)
+ project.add_maintainer(maintainer)
end
context 'authenticated user with valid permissions' do
it 'deletes pipeline_schedule' do
expect do
- delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", master)
+ delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", maintainer)
end.to change { project.pipeline_schedules.count }.by(-1)
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)
+ delete api("/projects/#{project.id}/pipeline_schedules/-5", maintainer)
expect(response).to have_gitlab_http_status(:not_found)
end
it_behaves_like '412 response' do
- let(:request) { api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", master) }
+ let(:request) { api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", maintainer) }
end
end
context 'authenticated user with invalid permissions' do
- let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: master) }
+ let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: maintainer) }
it 'does not delete pipeline_schedule' do
delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", developer)
@@ -415,7 +415,7 @@ describe API::PipelineSchedules do
end
describe 'DELETE /projects/:id/pipeline_schedules/:pipeline_schedule_id/variables/:key' do
- let(:master) { create(:user) }
+ let(:maintainer) { create(:user) }
set(:pipeline_schedule) do
create(:ci_pipeline_schedule, project: project, owner: developer)
@@ -426,13 +426,13 @@ describe API::PipelineSchedules do
end
before do
- project.add_master(master)
+ project.add_maintainer(maintainer)
end
context 'authenticated user with valid permissions' do
it 'deletes pipeline_schedule_variable' do
expect do
- delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables/#{pipeline_schedule_variable.key}", master)
+ delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables/#{pipeline_schedule_variable.key}", maintainer)
end.to change { Ci::PipelineScheduleVariable.count }.by(-1)
expect(response).to have_gitlab_http_status(:accepted)
@@ -440,14 +440,14 @@ describe API::PipelineSchedules do
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)
+ delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables/____", maintainer)
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'authenticated user with invalid permissions' do
- let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: master) }
+ let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: maintainer) }
it 'does not delete pipeline_schedule_variable' do
delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables/#{pipeline_schedule_variable.key}", developer)
diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb
index 0736329f9fd..e2ca27f5d41 100644
--- a/spec/requests/api/pipelines_spec.rb
+++ b/spec/requests/api/pipelines_spec.rb
@@ -11,7 +11,7 @@ describe API::Pipelines do
end
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
describe 'GET /projects/:id/pipelines ' do
@@ -285,6 +285,15 @@ describe API::Pipelines do
end
describe 'POST /projects/:id/pipeline ' do
+ def expect_variables(variables, expected_variables)
+ variables.each_with_index do |variable, index|
+ expected_variable = expected_variables[index]
+
+ expect(variable.key).to eq(expected_variable['key'])
+ expect(variable.value).to eq(expected_variable['value'])
+ end
+ end
+
context 'authorized user' do
context 'with gitlab-ci.yml' do
before do
@@ -294,13 +303,62 @@ describe API::Pipelines do
it 'creates and returns a new pipeline' do
expect do
post api("/projects/#{project.id}/pipeline", user), ref: project.default_branch
- end.to change { Ci::Pipeline.count }.by(1)
+ end.to change { project.pipelines.count }.by(1)
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
+ context 'variables given' do
+ let(:variables) { [{ 'key' => 'UPLOAD_TO_S3', 'value' => 'true' }] }
+
+ it 'creates and returns a new pipeline using the given variables' do
+ expect do
+ post api("/projects/#{project.id}/pipeline", user), ref: project.default_branch, variables: variables
+ end.to change { project.pipelines.count }.by(1)
+ expect_variables(project.pipelines.last.variables, variables)
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response).to be_a Hash
+ expect(json_response['sha']).to eq project.commit.id
+ expect(json_response).not_to have_key('variables')
+ end
+ end
+
+ describe 'using variables conditions' do
+ let(:variables) { [{ 'key' => 'STAGING', 'value' => 'true' }] }
+
+ before do
+ config = YAML.dump(test: { script: 'test', only: { variables: ['$STAGING'] } })
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ it 'creates and returns a new pipeline using the given variables' do
+ expect do
+ post api("/projects/#{project.id}/pipeline", user), ref: project.default_branch, variables: variables
+ end.to change { project.pipelines.count }.by(1)
+ expect_variables(project.pipelines.last.variables, variables)
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response).to be_a Hash
+ expect(json_response['sha']).to eq project.commit.id
+ expect(json_response).not_to have_key('variables')
+ end
+
+ context 'condition unmatch' do
+ let(:variables) { [{ 'key' => 'STAGING', 'value' => 'false' }] }
+
+ it "doesn't create a job" do
+ expect do
+ post api("/projects/#{project.id}/pipeline", user), ref: project.default_branch
+ end.not_to change { project.pipelines.count }
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+ end
+ end
+
it 'fails when using an invalid ref' do
post api("/projects/#{project.id}/pipeline", user), ref: 'invalid_ref'
diff --git a/spec/requests/api/project_export_spec.rb b/spec/requests/api/project_export_spec.rb
index 3834d27d0a9..45e4e35d773 100644
--- a/spec/requests/api/project_export_spec.rb
+++ b/spec/requests/api/project_export_spec.rb
@@ -109,13 +109,13 @@ describe API::ProjectExport do
it_behaves_like 'get project export status ok'
end
- context 'when user is a master' do
+ context 'when user is a maintainer' do
before do
- project.add_master(user)
- project_none.add_master(user)
- project_started.add_master(user)
- project_finished.add_master(user)
- project_after_export.add_master(user)
+ project.add_maintainer(user)
+ project_none.add_maintainer(user)
+ project_started.add_maintainer(user)
+ project_finished.add_maintainer(user)
+ project_after_export.add_maintainer(user)
end
it_behaves_like 'get project export status ok'
@@ -192,6 +192,13 @@ describe API::ProjectExport do
context 'when upload complete' do
before do
FileUtils.rm_rf(project_after_export.export_path)
+
+ if project_after_export.export_project_object_exists?
+ upload = project_after_export.import_export_upload
+
+ upload.remove_export_file!
+ upload.save
+ end
end
it_behaves_like '404 response' do
@@ -221,13 +228,13 @@ describe API::ProjectExport do
it_behaves_like 'get project download by strategy'
end
- context 'when user is a master' do
+ context 'when user is a maintainer' do
before do
- project.add_master(user)
- project_none.add_master(user)
- project_started.add_master(user)
- project_finished.add_master(user)
- project_after_export.add_master(user)
+ project.add_maintainer(user)
+ project_none.add_maintainer(user)
+ project_started.add_maintainer(user)
+ project_finished.add_maintainer(user)
+ project_after_export.add_maintainer(user)
end
it_behaves_like 'get project download by strategy'
@@ -261,6 +268,22 @@ describe API::ProjectExport do
it_behaves_like 'get project export download not found'
end
end
+
+ context 'when an uploader is used' do
+ before do
+ stub_uploads_object_storage(ImportExportUploader)
+
+ [project, project_finished, project_after_export].each do |p|
+ p.add_maintainer(user)
+
+ upload = ImportExportUpload.new(project: p)
+ upload.export_file = fixture_file_upload('spec/fixtures/project_export.tar.gz', "`/tar.gz")
+ upload.save!
+ end
+ end
+
+ it_behaves_like 'get project download by strategy'
+ end
end
describe 'POST /projects/:project_id/export' do
@@ -315,13 +338,13 @@ describe API::ProjectExport do
it_behaves_like 'post project export start'
end
- context 'when user is a master' do
+ context 'when user is a maintainer' do
before do
- project.add_master(user)
- project_none.add_master(user)
- project_started.add_master(user)
- project_finished.add_master(user)
- project_after_export.add_master(user)
+ project.add_maintainer(user)
+ project_none.add_maintainer(user)
+ project_started.add_maintainer(user)
+ project_finished.add_maintainer(user)
+ project_after_export.add_maintainer(user)
end
it_behaves_like 'post project export start'
diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb
index 12a183fed1e..bc45a63d9f1 100644
--- a/spec/requests/api/project_hooks_spec.rb
+++ b/spec/requests/api/project_hooks_spec.rb
@@ -13,7 +13,7 @@ describe API::ProjectHooks, 'ProjectHooks' do
end
before do
- project.add_master(user)
+ project.add_maintainer(user)
project.add_developer(user3)
end
@@ -214,7 +214,7 @@ describe API::ProjectHooks, 'ProjectHooks' do
it "returns a 404 if a user attempts to delete project hooks he/she does not own" do
test_user = create(:user)
other_project = create(:project)
- other_project.add_master(test_user)
+ other_project.add_maintainer(test_user)
delete api("/projects/#{other_project.id}/hooks/#{hook.id}", test_user)
expect(response).to have_gitlab_http_status(404)
diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb
index f8c64f063af..41243854ebc 100644
--- a/spec/requests/api/project_import_spec.rb
+++ b/spec/requests/api/project_import_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe API::ProjectImport do
let(:export_path) { "#{Dir.tmpdir}/project_export_spec" }
let(:user) { create(:user) }
- let(:file) { File.join(Rails.root, 'spec', 'features', 'projects', 'import_export', 'test_project_export.tar.gz') }
+ let(:file) { File.join('spec', 'features', 'projects', 'import_export', 'test_project_export.tar.gz') }
let(:namespace) { create(:group) }
before do
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
@@ -146,7 +146,7 @@ describe API::ProjectImport do
describe 'GET /projects/:id/import' do
it 'returns the import status' do
project = create(:project, :import_started)
- project.add_master(user)
+ project.add_maintainer(user)
get api("/projects/#{project.id}/import", user)
@@ -156,8 +156,8 @@ describe API::ProjectImport do
it 'returns the import status and the error if failed' do
project = create(:project, :import_failed)
- project.add_master(user)
- project.import_state.update_attributes(last_error: 'error')
+ project.add_maintainer(user)
+ project.import_state.update(last_error: 'error')
get api("/projects/#{project.id}/import", user)
diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb
index 4a2289ca137..a3b5e8c6223 100644
--- a/spec/requests/api/project_snippets_spec.rb
+++ b/spec/requests/api/project_snippets_spec.rb
@@ -25,7 +25,7 @@ describe API::ProjectSnippets do
expect(response).to have_gitlab_http_status(404)
end
- it "returns unautorized for non-admin users" do
+ it "returns unauthorized for non-admin users" do
get api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/user_agent_detail", user)
expect(response).to have_gitlab_http_status(403)
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 9b7c3205c1f..8389cb7cf9c 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -40,7 +40,7 @@ describe API::Projects do
create(:project_member,
user: user4,
project: project3,
- access_level: ProjectMember::MASTER)
+ access_level: ProjectMember::MAINTAINER)
end
let(:project4) do
create(:project,
@@ -312,7 +312,7 @@ describe API::Projects do
before do
project_member
- user3.update_attributes(starred_projects: [project, project2, project3, public_project])
+ user3.update(starred_projects: [project, project2, project3, public_project])
end
it 'returns the starred projects viewable by the user' do
@@ -333,7 +333,7 @@ describe API::Projects do
let!(:project9) { create(:project, :public, path: 'gitlab9') }
before do
- user.update_attributes(starred_projects: [project5, project7, project8, project9])
+ user.update(starred_projects: [project5, project7, project8, project9])
end
context 'including owned filter' do
@@ -353,7 +353,7 @@ describe API::Projects do
create(:project_member,
user: user,
project: project5,
- access_level: ProjectMember::MASTER)
+ access_level: ProjectMember::MAINTAINER)
end
it 'returns only projects that satisfy all query parameters' do
@@ -518,7 +518,7 @@ describe API::Projects do
end
it 'uploads avatar for project a project' do
- project = attributes_for(:project, avatar: fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif'))
+ project = attributes_for(:project, avatar: fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif'))
post api('/projects', user), project
@@ -777,7 +777,7 @@ describe API::Projects do
end
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")
+ post api("/projects/#{project.id}/uploads", user), file: fixture_file_upload("spec/fixtures/dk.png", "image/png")
expect(response).to have_gitlab_http_status(201)
expect(json_response['alt']).to eq("dk")
@@ -961,7 +961,7 @@ describe API::Projects do
describe 'permissions' do
context 'all projects' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
it 'contains permission information' do
@@ -969,19 +969,19 @@ describe API::Projects do
expect(response).to have_gitlab_http_status(200)
expect(json_response.first['permissions']['project_access']['access_level'])
- .to eq(Gitlab::Access::MASTER)
+ .to eq(Gitlab::Access::MAINTAINER)
expect(json_response.first['permissions']['group_access']).to be_nil
end
end
context 'personal project' do
it 'sets project access and returns 200' do
- project.add_master(user)
+ project.add_maintainer(user)
get api("/projects/#{project.id}", user)
expect(response).to have_gitlab_http_status(200)
expect(json_response['permissions']['project_access']['access_level'])
- .to eq(Gitlab::Access::MASTER)
+ .to eq(Gitlab::Access::MAINTAINER)
expect(json_response['permissions']['group_access']).to be_nil
end
end
@@ -1451,7 +1451,7 @@ describe API::Projects do
end
it 'updates visibility_level from public to private' do
- project3.update_attributes({ visibility_level: Gitlab::VisibilityLevel::PUBLIC })
+ project3.update({ visibility_level: Gitlab::VisibilityLevel::PUBLIC })
project_param = { visibility: 'private' }
put api("/projects/#{project3.id}", user), project_param
@@ -1526,9 +1526,23 @@ describe API::Projects do
expect(response).to have_gitlab_http_status(400)
end
+
+ it 'updates avatar' do
+ project_param = {
+ avatar: fixture_file_upload('spec/fixtures/banana_sample.gif',
+ 'image/gif')
+ }
+
+ put api("/projects/#{project3.id}", user), project_param
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['avatar_url']).to eq('http://localhost/uploads/'\
+ '-/system/project/avatar/'\
+ "#{project3.id}/banana_sample.gif")
+ end
end
- context 'when authenticated as project master' do
+ context 'when authenticated as project maintainer' do
it 'updates path' do
project_param = { path: 'bar' }
put api("/projects/#{project3.id}", user4), project_param
@@ -1990,6 +2004,38 @@ describe API::Projects do
end
end
+ describe 'PUT /projects/:id/transfer' do
+ context 'when authenticated as owner' do
+ let(:group) { create :group }
+
+ it 'transfers the project to the new namespace' do
+ group.add_owner(user)
+
+ put api("/projects/#{project.id}/transfer", user), namespace: group.id
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+
+ it 'fails when transferring to a non owned namespace' do
+ put api("/projects/#{project.id}/transfer", user), namespace: group.id
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'fails when transferring to an unknown namespace' do
+ put api("/projects/#{project.id}/transfer", user), namespace: 'unknown'
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'fails on missing namespace' do
+ put api("/projects/#{project.id}/transfer", user)
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+ end
+ end
+
it_behaves_like 'custom attributes endpoints', 'projects' do
let(:attributable) { project }
let(:other_attributable) { project2 }
diff --git a/spec/requests/api/protected_branches_spec.rb b/spec/requests/api/protected_branches_spec.rb
index 576fde46615..69a601d7b40 100644
--- a/spec/requests/api/protected_branches_spec.rb
+++ b/spec/requests/api/protected_branches_spec.rb
@@ -26,9 +26,9 @@ describe API::ProtectedBranches do
end
end
- context 'when authenticated as a master' do
+ context 'when authenticated as a maintainer' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
it_behaves_like 'protected branches'
@@ -54,8 +54,8 @@ describe API::ProtectedBranches do
expect(response).to have_gitlab_http_status(200)
expect(json_response['name']).to eq(branch_name)
- expect(json_response['push_access_levels'][0]['access_level']).to eq(::Gitlab::Access::MASTER)
- expect(json_response['merge_access_levels'][0]['access_level']).to eq(::Gitlab::Access::MASTER)
+ expect(json_response['push_access_levels'][0]['access_level']).to eq(::Gitlab::Access::MAINTAINER)
+ expect(json_response['merge_access_levels'][0]['access_level']).to eq(::Gitlab::Access::MAINTAINER)
end
context 'when protected branch does not exist' do
@@ -68,9 +68,9 @@ describe API::ProtectedBranches do
end
end
- context 'when authenticated as a master' do
+ context 'when authenticated as a maintainer' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
it_behaves_like 'protected branch'
@@ -108,9 +108,9 @@ describe API::ProtectedBranches do
expect(json_response['name']).to eq(branch_name)
end
- context 'when authenticated as a master' do
+ context 'when authenticated as a maintainer' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
it 'protects a single branch' do
@@ -118,8 +118,8 @@ describe API::ProtectedBranches do
expect(response).to have_gitlab_http_status(201)
expect(json_response['name']).to eq(branch_name)
- expect(json_response['push_access_levels'][0]['access_level']).to eq(Gitlab::Access::MASTER)
- expect(json_response['merge_access_levels'][0]['access_level']).to eq(Gitlab::Access::MASTER)
+ expect(json_response['push_access_levels'][0]['access_level']).to eq(Gitlab::Access::MAINTAINER)
+ expect(json_response['merge_access_levels'][0]['access_level']).to eq(Gitlab::Access::MAINTAINER)
end
it 'protects a single branch and developers can push' do
@@ -128,7 +128,7 @@ describe API::ProtectedBranches do
expect(response).to have_gitlab_http_status(201)
expect(json_response['name']).to eq(branch_name)
expect(json_response['push_access_levels'][0]['access_level']).to eq(Gitlab::Access::DEVELOPER)
- expect(json_response['merge_access_levels'][0]['access_level']).to eq(Gitlab::Access::MASTER)
+ expect(json_response['merge_access_levels'][0]['access_level']).to eq(Gitlab::Access::MAINTAINER)
end
it 'protects a single branch and developers can merge' do
@@ -136,7 +136,7 @@ describe API::ProtectedBranches do
expect(response).to have_gitlab_http_status(201)
expect(json_response['name']).to eq(branch_name)
- expect(json_response['push_access_levels'][0]['access_level']).to eq(Gitlab::Access::MASTER)
+ expect(json_response['push_access_levels'][0]['access_level']).to eq(Gitlab::Access::MAINTAINER)
expect(json_response['merge_access_levels'][0]['access_level']).to eq(Gitlab::Access::DEVELOPER)
end
@@ -155,7 +155,7 @@ describe API::ProtectedBranches do
expect(response).to have_gitlab_http_status(201)
expect(json_response['name']).to eq(branch_name)
expect(json_response['push_access_levels'][0]['access_level']).to eq(Gitlab::Access::NO_ACCESS)
- expect(json_response['merge_access_levels'][0]['access_level']).to eq(Gitlab::Access::MASTER)
+ expect(json_response['merge_access_levels'][0]['access_level']).to eq(Gitlab::Access::MAINTAINER)
end
it 'protects a single branch and no one can merge' do
@@ -163,7 +163,7 @@ describe API::ProtectedBranches do
expect(response).to have_gitlab_http_status(201)
expect(json_response['name']).to eq(branch_name)
- expect(json_response['push_access_levels'][0]['access_level']).to eq(Gitlab::Access::MASTER)
+ expect(json_response['push_access_levels'][0]['access_level']).to eq(Gitlab::Access::MAINTAINER)
expect(json_response['merge_access_levels'][0]['access_level']).to eq(Gitlab::Access::NO_ACCESS)
end
@@ -189,8 +189,8 @@ describe API::ProtectedBranches do
post post_endpoint, name: branch_name
expect_protection_to_be_successful
- expect(json_response['push_access_levels'][0]['access_level']).to eq(Gitlab::Access::MASTER)
- expect(json_response['merge_access_levels'][0]['access_level']).to eq(Gitlab::Access::MASTER)
+ expect(json_response['push_access_levels'][0]['access_level']).to eq(Gitlab::Access::MAINTAINER)
+ expect(json_response['merge_access_levels'][0]['access_level']).to eq(Gitlab::Access::MAINTAINER)
end
end
@@ -225,7 +225,7 @@ describe API::ProtectedBranches do
let(:delete_endpoint) { api("/projects/#{project.id}/protected_branches/#{branch_name}", user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
it "unprotects a single branch" do
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index 9e6d69e3874..6063afc213d 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -8,7 +8,7 @@ describe API::Repositories do
let(:user) { create(:user) }
let(:guest) { create(:user).tap { |u| create(:project_member, :guest, user: u, project: project) } }
let!(:project) { create(:project, :repository, creator: user) }
- let!(:master) { create(:project_member, :master, user: user, project: project) }
+ let!(:maintainer) { create(:project_member, :maintainer, user: user, project: project) }
describe "GET /projects/:id/repository/tree" do
let(:route) { "/projects/#{project.id}/repository/tree" }
@@ -220,11 +220,10 @@ describe API::Repositories do
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')
- expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.gz/)
+ expect(params['ArchivePath']).to match(/#{project.path}\-[^\.]+\.tar.gz/)
end
it 'returns the repository archive archive.zip' do
@@ -232,11 +231,10 @@ describe API::Repositories do
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')
- expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.zip/)
+ expect(params['ArchivePath']).to match(/#{project.path}\-[^\.]+\.zip/)
end
it 'returns the repository archive archive.tar.bz2' do
@@ -244,11 +242,10 @@ describe API::Repositories do
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')
- expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.bz2/)
+ expect(params['ArchivePath']).to match(/#{project.path}\-[^\.]+\.tar.bz2/)
end
context 'when sha does not exist' do
@@ -291,6 +288,9 @@ describe API::Repositories do
shared_examples_for 'repository compare' do
it "compares branches" do
+ expect(::Gitlab::Git::Compare).to receive(:new).with(anything, anything, anything, {
+ straight: false
+ }).and_call_original
get api(route, current_user), from: 'master', to: 'feature'
expect(response).to have_gitlab_http_status(200)
@@ -298,6 +298,28 @@ describe API::Repositories do
expect(json_response['diffs']).to be_present
end
+ it "compares branches with explicit merge-base mode" do
+ expect(::Gitlab::Git::Compare).to receive(:new).with(anything, anything, anything, {
+ straight: false
+ }).and_call_original
+ get api(route, current_user), from: 'master', to: 'feature', straight: false
+
+ 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 branches with explicit straight mode" do
+ expect(::Gitlab::Git::Compare).to receive(:new).with(anything, anything, anything, {
+ straight: true
+ }).and_call_original
+ get api(route, current_user), from: 'master', to: 'feature', straight: true
+
+ 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 api(route, current_user), from: 'v1.0.0', to: 'v1.1.0'
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index efb9bddde44..d57993ab454 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -111,11 +111,13 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
end
context 'when tags are not provided' do
- it 'returns 404 error' do
+ it 'returns 400 error' do
post api('/runners'), token: registration_token,
run_untagged: false
- expect(response).to have_gitlab_http_status 404
+ expect(response).to have_gitlab_http_status 400
+ expect(json_response['message']).to include(
+ 'tags_list' => ['can not be empty when runner is not allowed to pick untagged jobs'])
end
end
end
@@ -262,16 +264,12 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
describe '/api/v4/jobs' do
let(:project) { create(:project, shared_runners_enabled: false) }
let(:pipeline) { create(:ci_pipeline_without_jobs, project: project, ref: 'master') }
- let(:runner) { create(:ci_runner) }
+ let(:runner) { create(:ci_runner, :project, projects: [project]) }
let(:job) do
create(:ci_build, :artifacts, :extended_options,
pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0, commands: "ls\ndate")
end
- before do
- project.runners << runner
- end
-
describe 'POST /api/v4/jobs/request' do
let!(:last_update) {}
let!(:new_update) { }
@@ -353,11 +351,13 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
context 'when valid token is provided' do
context 'when Runner is not active' do
let(:runner) { create(:ci_runner, :inactive) }
+ let(:update_value) { runner.ensure_runner_queue_value }
it 'returns 204 error' do
request_job
- expect(response).to have_gitlab_http_status 204
+ expect(response).to have_gitlab_http_status(204)
+ expect(response.header['X-GitLab-Last-Update']).to eq(update_value)
end
end
@@ -379,7 +379,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
end
context 'when shared runner requests job for project without shared_runners_enabled' do
- let(:runner) { create(:ci_runner, :shared) }
+ let(:runner) { create(:ci_runner, :instance) }
it_behaves_like 'no jobs available'
end
@@ -724,7 +724,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
end
context 'when runner specifies lower timeout' do
- let(:runner) { create(:ci_runner, maximum_timeout: 1000) }
+ let(:runner) { create(:ci_runner, :project, maximum_timeout: 1000, projects: [project]) }
it 'contains info about timeout overridden by runner' do
request_job
@@ -735,7 +735,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
end
context 'when runner specifies bigger timeout' do
- let(:runner) { create(:ci_runner, maximum_timeout: 2000) }
+ let(:runner) { create(:ci_runner, :project, maximum_timeout: 2000, projects: [project]) }
it 'contains info about timeout not overridden by runner' do
request_job
@@ -818,6 +818,18 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
expect(job.reload.trace.raw).to eq 'BUILD TRACE'
end
+
+ context 'when running state is sent' do
+ it 'updates update_at value' do
+ expect { update_job_after_time }.to change { job.reload.updated_at }
+ end
+ end
+
+ context 'when other state is sent' do
+ it "doesn't update update_at value" do
+ expect { update_job_after_time(20.minutes, state: 'success') }.not_to change { job.reload.updated_at }
+ end
+ end
end
context 'when job has been erased' do
@@ -830,10 +842,33 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
end
end
+ context 'when job has already been finished' do
+ before do
+ job.trace.set('Job failed')
+ job.drop!(:script_failure)
+ end
+
+ it 'does not update job status and job trace' do
+ update_job(state: 'success', trace: 'BUILD TRACE UPDATED')
+
+ job.reload
+ expect(response).to have_gitlab_http_status(403)
+ expect(response.header['Job-Status']).to eq 'failed'
+ expect(job.trace.raw).to eq 'Job failed'
+ expect(job).to be_failed
+ end
+ end
+
def update_job(token = job.token, **params)
new_params = params.merge(token: token)
put api("/jobs/#{job.id}"), new_params
end
+
+ def update_job_after_time(update_interval = 20.minutes, state = 'running')
+ Timecop.travel(job.updated_at + update_interval) do
+ update_job(job.token, state: state)
+ end
+ end
end
describe 'PATCH /api/v4/jobs/:id/trace' do
@@ -966,6 +1001,17 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
end
end
end
+
+ context 'when the job is canceled' do
+ before do
+ job.cancel
+ patch_the_trace
+ end
+
+ it 'receives status in header' do
+ expect(response.header['Job-Status']).to eq 'canceled'
+ end
+ end
end
context 'when Runner makes a force-patch' do
@@ -1042,8 +1088,8 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
let(:jwt_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
let(:headers) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => jwt_token } }
let(:headers_with_token) { headers.merge(API::Helpers::Runner::JOB_TOKEN_HEADER => job.token) }
- let(:file_upload) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') }
- let(:file_upload2) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/gif') }
+ let(:file_upload) { fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif') }
+ let(:file_upload2) { fixture_file_upload('spec/fixtures/dk.png', 'image/gif') }
before do
stub_artifacts_object_storage
@@ -1088,6 +1134,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
expect(json_response['RemoteObject']).to have_key('GetURL')
expect(json_response['RemoteObject']).to have_key('StoreURL')
expect(json_response['RemoteObject']).to have_key('DeleteURL')
+ expect(json_response['RemoteObject']).to have_key('MultipartUpload')
end
end
@@ -1210,7 +1257,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
before do
fog_connection.directories.get('artifacts').files.create(
- key: 'tmp/upload/12312300',
+ key: 'tmp/uploads/12312300',
body: 'content'
)
diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb
index c7587c877fc..3ebdb54f71f 100644
--- a/spec/requests/api/runners_spec.rb
+++ b/spec/requests/api/runners_spec.rb
@@ -11,28 +11,15 @@ describe API::Runners do
let(:group) { create(:group).tap { |group| group.add_owner(user) } }
let(:group2) { create(:group).tap { |group| group.add_owner(user) } }
- let!(:shared_runner) { create(:ci_runner, :shared, description: 'Shared runner') }
- let!(:unused_project_runner) { create(:ci_runner) }
-
- let!(:project_runner) do
- create(:ci_runner, description: 'Project runner').tap do |runner|
- create(:ci_runner_project, runner: runner, project: project)
- end
- end
-
- let!(:two_projects_runner) do
- create(:ci_runner, description: 'Two projects runner').tap do |runner|
- create(:ci_runner_project, runner: runner, project: project)
- create(:ci_runner_project, runner: runner, project: project2)
- end
- end
-
- let!(:group_runner) { create(:ci_runner, description: 'Group runner', groups: [group], runner_type: :group_type) }
+ let!(:shared_runner) { create(:ci_runner, :instance, description: 'Shared runner') }
+ let!(:project_runner) { create(:ci_runner, :project, description: 'Project runner', projects: [project]) }
+ let!(:two_projects_runner) { create(:ci_runner, :project, description: 'Two projects runner', projects: [project, project2]) }
+ let!(:group_runner) { create(:ci_runner, :group, description: 'Group runner', groups: [group]) }
before do
# Set project access for users
- create(:project_member, :master, user: user, project: project)
- create(:project_member, :master, user: user, project: project2)
+ create(:project_member, :maintainer, user: user, project: project)
+ create(:project_member, :maintainer, user: user, project: project2)
create(:project_member, :reporter, user: user2, project: project)
end
@@ -103,6 +90,17 @@ describe API::Runners do
end
it 'filters runners by scope' do
+ get api('/runners/all?scope=shared', admin)
+
+ shared = json_response.all? { |r| r['is_shared'] }
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response[0]).to have_key('ip_address')
+ expect(shared).to be_truthy
+ end
+
+ it 'filters runners by scope' do
get api('/runners/all?scope=specific', admin)
shared = json_response.any? { |r| r['is_shared'] }
@@ -141,6 +139,18 @@ describe API::Runners do
end
context 'when runner is not shared' do
+ context 'when unused runner is present' do
+ let!(:unused_project_runner) { create(:ci_runner, :project, :without_projects) }
+
+ it 'deletes unused runner' do
+ expect do
+ delete api("/runners/#{unused_project_runner.id}", admin)
+
+ expect(response).to have_gitlab_http_status(204)
+ end.to change { Ci::Runner.project_type.count }.by(-1)
+ end
+ end
+
it "returns runner's details" do
get api("/runners/#{project_runner.id}", admin)
@@ -201,6 +211,69 @@ describe API::Runners do
describe 'PUT /runners/:id' do
context 'admin user' do
+ # see https://gitlab.com/gitlab-org/gitlab-ce/issues/48625
+ context 'single parameter update' do
+ it 'runner description' do
+ description = shared_runner.description
+ update_runner(shared_runner.id, admin, description: "#{description}_updated")
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(shared_runner.reload.description).to eq("#{description}_updated")
+ end
+
+ it 'runner active state' do
+ active = shared_runner.active
+ update_runner(shared_runner.id, admin, active: !active)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(shared_runner.reload.active).to eq(!active)
+ end
+
+ it 'runner tag list' do
+ update_runner(shared_runner.id, admin, tag_list: ['ruby2.1', 'pgsql', 'mysql'])
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(shared_runner.reload.tag_list).to include('ruby2.1', 'pgsql', 'mysql')
+ end
+
+ it 'runner untagged flag' do
+ # Ensure tag list is non-empty before setting untagged to false.
+ update_runner(shared_runner.id, admin, tag_list: ['ruby2.1', 'pgsql', 'mysql'])
+ update_runner(shared_runner.id, admin, run_untagged: 'false')
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(shared_runner.reload.run_untagged?).to be(false)
+ end
+
+ it 'runner unlocked flag' do
+ update_runner(shared_runner.id, admin, locked: 'true')
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(shared_runner.reload.locked?).to be(true)
+ end
+
+ it 'runner access level' do
+ update_runner(shared_runner.id, admin, access_level: 'ref_protected')
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(shared_runner.reload.ref_protected?).to be_truthy
+ end
+
+ it 'runner maximum timeout' do
+ update_runner(shared_runner.id, admin, maximum_timeout: 1234)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(shared_runner.reload.maximum_timeout).to eq(1234)
+ end
+
+ it 'fails with no parameters' do
+ put api("/runners/#{shared_runner.id}", admin)
+
+ shared_runner.reload
+ expect(response).to have_gitlab_http_status(400)
+ end
+ end
+
context 'when runner is shared' do
it 'updates runner' do
description = shared_runner.description
@@ -301,7 +374,7 @@ describe API::Runners do
delete api("/runners/#{shared_runner.id}", admin)
expect(response).to have_gitlab_http_status(204)
- end.to change { Ci::Runner.shared.count }.by(-1)
+ end.to change { Ci::Runner.instance_type.count }.by(-1)
end
it_behaves_like '412 response' do
@@ -310,20 +383,12 @@ describe API::Runners do
end
context 'when runner is not shared' do
- it 'deletes unused runner' do
- expect do
- delete api("/runners/#{unused_project_runner.id}", admin)
-
- expect(response).to have_gitlab_http_status(204)
- end.to change { Ci::Runner.specific.count }.by(-1)
- end
-
it 'deletes used project runner' do
expect do
delete api("/runners/#{project_runner.id}", admin)
expect(response).to have_http_status(204)
- end.to change { Ci::Runner.specific.count }.by(-1)
+ end.to change { Ci::Runner.project_type.count }.by(-1)
end
end
@@ -358,7 +423,7 @@ describe API::Runners do
delete api("/runners/#{project_runner.id}", user)
expect(response).to have_http_status(204)
- end.to change { Ci::Runner.specific.count }.by(-1)
+ end.to change { Ci::Runner.project_type.count }.by(-1)
end
it_behaves_like '412 response' do
@@ -511,7 +576,7 @@ describe API::Runners do
end
describe 'GET /projects/:id/runners' do
- context 'authorized user with master privileges' do
+ context 'authorized user with maintainer privileges' do
it "returns project's runners" do
get api("/projects/#{project.id}/runners", user)
@@ -524,7 +589,7 @@ describe API::Runners do
end
end
- context 'authorized user without master privileges' do
+ context 'authorized user without maintainer privileges' do
it "does not return project's runners" do
get api("/projects/#{project.id}/runners", user2)
@@ -543,11 +608,7 @@ describe API::Runners do
describe 'POST /projects/:id/runners' do
context 'authorized user' do
- let(:project_runner2) do
- create(:ci_runner).tap do |runner|
- create(:ci_runner_project, runner: runner, project: project2)
- end
- end
+ let(:project_runner2) { create(:ci_runner, :project, projects: [project2]) }
it 'enables specific runner' do
expect do
@@ -560,7 +621,7 @@ describe API::Runners do
expect do
post api("/projects/#{project.id}/runners", user), runner_id: project_runner.id
end.to change { project.runners.count }.by(0)
- expect(response).to have_gitlab_http_status(409)
+ expect(response).to have_gitlab_http_status(400)
end
it 'does not enable locked runner' do
@@ -586,31 +647,27 @@ describe API::Runners do
end
context 'user is admin' do
- it 'enables any specific runner' do
- expect do
- post api("/projects/#{project.id}/runners", admin), runner_id: unused_project_runner.id
- end.to change { project.runners.count }.by(+1)
- expect(response).to have_gitlab_http_status(201)
+ context 'when project runner is used' do
+ let!(:new_project_runner) { create(:ci_runner, :project) }
+
+ it 'enables any specific runner' do
+ expect do
+ post api("/projects/#{project.id}/runners", admin), runner_id: new_project_runner.id
+ end.to change { project.runners.count }.by(+1)
+ expect(response).to have_gitlab_http_status(201)
+ end
end
- it 'enables a shared runner' do
+ it 'enables a instance type runner' do
expect do
post api("/projects/#{project.id}/runners", admin), runner_id: shared_runner.id
end.to change { project.runners.count }.by(1)
- expect(shared_runner.reload).not_to be_shared
+ expect(shared_runner.reload).not_to be_instance_type
expect(response).to have_gitlab_http_status(201)
end
end
- context 'user is not admin' do
- it 'does not enable runner without access to' do
- post api("/projects/#{project.id}/runners", user), runner_id: unused_project_runner.id
-
- 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)
@@ -618,6 +675,16 @@ describe API::Runners do
end
end
+ context 'user is not admin' do
+ let!(:new_project_runner) { create(:ci_runner, :project) }
+
+ it 'does not enable runner without access to' do
+ post api("/projects/#{project.id}/runners", user), runner_id: new_project_runner.id
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+
context 'authorized user without permissions' do
it 'does not enable runner' do
post api("/projects/#{project.id}/runners", user2)
diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb
index aca4aa40027..f8e468be170 100644
--- a/spec/requests/api/search_spec.rb
+++ b/spec/requests/api/search_spec.rb
@@ -312,6 +312,30 @@ describe API::Search do
end
it_behaves_like 'response is correct', schema: 'public_api/v4/blobs', size: 2
+
+ context 'filters' do
+ it 'by filename' do
+ get api("/projects/#{repo_project.id}/search", user), scope: 'blobs', search: 'mon filename:PROCESS.md'
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response.size).to eq(2)
+ expect(json_response.first['filename']).to eq('PROCESS.md')
+ end
+
+ it 'by path' do
+ get api("/projects/#{repo_project.id}/search", user), scope: 'blobs', search: 'mon path:markdown'
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response.size).to eq(8)
+ end
+
+ it 'by extension' do
+ get api("/projects/#{repo_project.id}/search", user), scope: 'blobs', search: 'mon extension:md'
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response.size).to eq(11)
+ end
+ end
end
end
end
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 8b22d1e72f3..57adc3ca7a6 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -24,13 +24,20 @@ describe API::Settings, 'Settings' do
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
+ expect(json_response['performance_bar_allowed_group_id']).to be_nil
+ expect(json_response).not_to have_key('performance_bar_allowed_group_path')
+ expect(json_response).not_to have_key('performance_bar_enabled')
end
end
describe "PUT /application/settings" do
+ let(:group) { create(:group) }
+
context "custom repository storage type set in the config" do
before do
- storages = { 'custom' => 'tmp/tests/custom_repositories' }
+ # Add a possible storage to the config
+ storages = Gitlab.config.repositories.storages
+ .merge({ 'custom' => 'tmp/tests/custom_repositories' })
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
end
@@ -56,7 +63,8 @@ describe API::Settings, 'Settings' do
ed25519_key_restriction: 256,
circuitbreaker_check_interval: 2,
enforce_terms: true,
- terms: 'Hello world!'
+ terms: 'Hello world!',
+ performance_bar_allowed_group_path: group.full_path
expect(response).to have_gitlab_http_status(200)
expect(json_response['default_projects_limit']).to eq(3)
@@ -80,9 +88,27 @@ describe API::Settings, 'Settings' do
expect(json_response['circuitbreaker_check_interval']).to eq(2)
expect(json_response['enforce_terms']).to be(true)
expect(json_response['terms']).to eq('Hello world!')
+ expect(json_response['performance_bar_allowed_group_id']).to eq(group.id)
end
end
+ it "supports legacy performance_bar_allowed_group_id" do
+ put api("/application/settings", admin),
+ performance_bar_allowed_group_id: group.full_path
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['performance_bar_allowed_group_id']).to eq(group.id)
+ end
+
+ it "supports legacy performance_bar_enabled" do
+ put api("/application/settings", admin),
+ performance_bar_enabled: false,
+ performance_bar_allowed_group_id: group.full_path
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['performance_bar_allowed_group_id']).to be_nil
+ end
+
context "missing koding_url value when koding_enabled is true" do
it "returns a blank parameter error message" do
put api("/application/settings", admin), koding_enabled: true
diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb
index b3e253befc6..6da769cb3ed 100644
--- a/spec/requests/api/snippets_spec.rb
+++ b/spec/requests/api/snippets_spec.rb
@@ -20,6 +20,7 @@ describe API::Snippets do
private_snippet.id)
expect(json_response.last).to have_key('web_url')
expect(json_response.last).to have_key('raw_url')
+ expect(json_response.last).to have_key('visibility')
end
it 'hides private snippets from regular user' do
@@ -112,6 +113,7 @@ describe API::Snippets do
expect(json_response['title']).to eq(snippet.title)
expect(json_response['description']).to eq(snippet.description)
expect(json_response['file_name']).to eq(snippet.file_name)
+ expect(json_response['visibility']).to eq(snippet.visibility)
end
it 'returns 404 for invalid snippet id' do
@@ -142,6 +144,7 @@ describe API::Snippets do
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])
+ expect(json_response['visibility']).to eq(params[:visibility])
end
it 'returns 400 for missing parameters' do
@@ -311,7 +314,7 @@ describe API::Snippets do
expect(json_response['akismet_submitted']).to eq(user_agent_detail.submitted)
end
- it "returns unautorized for non-admin users" do
+ it "returns unauthorized for non-admin users" do
get api("/snippets/#{snippet.id}/user_agent_detail", user)
expect(response).to have_gitlab_http_status(403)
diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb
index e2b19ad59f9..98f995df06f 100644
--- a/spec/requests/api/tags_spec.rb
+++ b/spec/requests/api/tags_spec.rb
@@ -10,7 +10,7 @@ describe API::Tags do
let(:current_user) { nil }
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
describe 'GET /projects/:id/repository/tags' do
@@ -86,7 +86,7 @@ describe API::Tags do
end
end
- context 'when authenticated', 'as a master' do
+ context 'when authenticated', 'as a maintainer' do
let(:current_user) { user }
it_behaves_like 'repository tags'
@@ -109,7 +109,7 @@ describe API::Tags do
before do
release = project.releases.find_or_initialize_by(tag: tag_name)
- release.update_attributes(description: description)
+ release.update(description: description)
end
it 'returns an array of project tags with release info' do
@@ -168,7 +168,7 @@ describe API::Tags do
end
end
- context 'when authenticated', 'as a master' do
+ context 'when authenticated', 'as a maintainer' do
let(:current_user) { user }
it_behaves_like 'repository tag'
@@ -222,7 +222,7 @@ describe API::Tags do
end
end
- context 'when authenticated', 'as a master' do
+ context 'when authenticated', 'as a maintainer' do
let(:current_user) { user }
context "when a protected branch doesn't already exist" do
@@ -287,7 +287,10 @@ describe API::Tags do
context 'annotated tag' do
it 'creates a new annotated tag' do
# Identity must be set in .gitconfig to create annotated tag.
- repo_path = project.repository.path_to_repo
+ repo_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ project.repository.path_to_repo
+ end
+
system(*%W(#{Gitlab.config.git.bin_path} --git-dir=#{repo_path} config user.name #{user.name}))
system(*%W(#{Gitlab.config.git.bin_path} --git-dir=#{repo_path} config user.email #{user.email}))
@@ -338,7 +341,7 @@ describe API::Tags do
end
end
- context 'when authenticated', 'as a master' do
+ context 'when authenticated', 'as a maintainer' do
let(:current_user) { user }
it_behaves_like 'repository delete tag'
@@ -383,7 +386,7 @@ describe API::Tags do
end
end
- context 'when authenticated', 'as a master' do
+ context 'when authenticated', 'as a maintainer' do
let(:current_user) { user }
it_behaves_like 'repository new release'
@@ -397,7 +400,7 @@ describe API::Tags do
context 'on tag with existing release' do
before do
release = project.releases.find_or_initialize_by(tag: tag_name)
- release.update_attributes(description: description)
+ release.update(description: description)
end
it 'returns 409 if there is already a release' do
@@ -419,7 +422,7 @@ describe API::Tags do
context 'on tag with existing release' do
before do
release = project.releases.find_or_initialize_by(tag: tag_name)
- release.update_attributes(description: description)
+ release.update(description: description)
end
it 'updates the release description' do
@@ -449,7 +452,7 @@ describe API::Tags do
end
end
- context 'when authenticated', 'as a master' do
+ context 'when authenticated', 'as a maintainer' do
let(:current_user) { user }
it_behaves_like 'repository update release'
diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb
index b2c56f7af2c..0ae6796d1e4 100644
--- a/spec/requests/api/triggers_spec.rb
+++ b/spec/requests/api/triggers_spec.rb
@@ -6,7 +6,7 @@ describe API::Triggers do
let!(:trigger_token) { 'secure_token' }
let!(:trigger_token_2) { 'secure_token_2' }
let!(:project) { create(:project, :repository, creator: user) }
- let!(:master) { create(:project_member, :master, user: user, project: project) }
+ let!(:maintainer) { create(:project_member, :maintainer, user: user, project: project) }
let!(:developer) { create(:project_member, :developer, user: user2, project: project) }
let!(:trigger) { create(:ci_trigger, project: project, token: trigger_token, owner: user) }
let!(:trigger2) { create(:ci_trigger, project: project, token: trigger_token_2, owner: user2) }
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 05637eb0729..a97c3f3461a 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -512,7 +512,7 @@ describe API::Users do
end
it 'updates user with avatar' do
- put api("/users/#{user.id}", admin), { avatar: fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') }
+ put api("/users/#{user.id}", admin), { avatar: fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif') }
user.reload
@@ -1123,58 +1123,63 @@ describe API::Users do
describe "GET /user" do
let(:personal_access_token) { create(:personal_access_token, user: user).token }
- context 'with regular user' do
- context 'with personal access token' do
- it 'returns 403 without private token when sudo is defined' do
- get api("/user?private_token=#{personal_access_token}&sudo=123")
+ shared_examples 'get user info' do |version|
+ context 'with regular user' do
+ context 'with personal access token' do
+ it 'returns 403 without private token when sudo is defined' do
+ get api("/user?private_token=#{personal_access_token}&sudo=123", version: version)
- expect(response).to have_gitlab_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
+ end
end
- end
- it 'returns current user without private token when sudo not defined' do
- get api("/user", user)
+ it 'returns current user without private token when sudo not defined' do
+ get api("/user", user, version: version)
- 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
+ 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
- context "scopes" do
- let(:path) { "/user" }
- let(:api_call) { method(:api) }
+ context "scopes" do
+ let(:path) { "/user" }
+ let(:api_call) { method(:api) }
- include_examples 'allows the "read_user" scope'
+ include_examples 'allows the "read_user" scope', version
+ end
end
- end
- context 'with admin' do
- let(:admin_personal_access_token) { create(:personal_access_token, user: admin).token }
+ context 'with admin' do
+ let(:admin_personal_access_token) { create(:personal_access_token, user: admin).token }
- context 'with personal access token' do
- it 'returns 403 without private token when sudo defined' do
- get api("/user?private_token=#{admin_personal_access_token}&sudo=#{user.id}")
+ context 'with personal access token' do
+ it 'returns 403 without private token when sudo defined' do
+ get api("/user?private_token=#{admin_personal_access_token}&sudo=#{user.id}", version: version)
- expect(response).to have_gitlab_http_status(403)
- end
+ 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}")
+ 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}", version: version)
- 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)
+ 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
end
end
- end
- context 'with unauthenticated user' do
- it "returns 401 error if user is unauthenticated" do
- get api("/user")
+ context 'with unauthenticated user' do
+ it "returns 401 error if user is unauthenticated" do
+ get api("/user", version: version)
- expect(response).to have_gitlab_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
+ end
end
end
+
+ it_behaves_like 'get user info', 'v3'
+ it_behaves_like 'get user info', 'v4'
end
describe "GET /user/keys" do
diff --git a/spec/requests/api/v3/award_emoji_spec.rb b/spec/requests/api/v3/award_emoji_spec.rb
deleted file mode 100644
index 6dc430676b0..00000000000
--- a/spec/requests/api/v3/award_emoji_spec.rb
+++ /dev/null
@@ -1,297 +0,0 @@
-require 'spec_helper'
-
-describe API::V3::AwardEmoji do
- set(:user) { create(:user) }
- set(:project) { create(:project) }
- set(:issue) { create(:issue, project: project) }
- set(:award_emoji) { create(:award_emoji, awardable: issue, user: user) }
- let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
- let!(:downvote) { create(:award_emoji, :downvote, awardable: merge_request, user: user) }
- set(:note) { create(:note, project: project, noteable: issue) }
-
- before { project.add_master(user) }
-
- describe "GET /projects/:id/awardable/:awardable_id/award_emoji" do
- context 'on an issue' 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_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.first['name']).to eq(award_emoji.name)
- end
-
- 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_gitlab_http_status(404)
- end
- end
-
- context 'on a merge request' 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_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)
- end
- end
-
- context 'on a snippet' do
- let(:snippet) { create(:project_snippet, :public, project: project) }
- let!(:award) { create(:award_emoji, awardable: snippet) }
-
- it 'returns the awarded emoji' do
- get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji", user)
-
- 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
- end
-
- context 'when the user has no access' do
- it 'returns a status code 404' do
- user1 = create(:user)
-
- get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji", user1)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
- end
-
- describe 'GET /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji' do
- let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket') }
-
- 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_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.first['name']).to eq(rocket.name)
- end
- end
-
- describe "GET /projects/:id/awardable/:awardable_id/award_emoji/:award_id" do
- context 'on an issue' 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_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")
- end
-
- 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_gitlab_http_status(404)
- end
- end
-
- context 'on a merge request' 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_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")
- end
- end
-
- context 'on a snippet' do
- let(:snippet) { create(:project_snippet, :public, project: project) }
- let!(:award) { create(:award_emoji, awardable: snippet) }
-
- it 'returns the awarded emoji' do
- get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji/#{award.id}", user)
-
- 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")
- end
- end
-
- context 'when the user has no access' do
- it 'returns a status code 404' do
- user1 = create(:user)
-
- get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user1)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
- end
-
- describe 'GET /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji/:award_id' do
- let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket') }
-
- 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_gitlab_http_status(200)
- expect(json_response).not_to be_an Array
- expect(json_response['name']).to eq(rocket.name)
- end
- end
-
- describe "POST /projects/:id/awardable/:awardable_id/award_emoji" do
- let(:issue2) { create(:issue, project: project, author: user) }
-
- context "on an issue" 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_gitlab_http_status(201)
- expect(json_response['name']).to eq('blowfish')
- expect(json_response['user']['username']).to eq(user.username)
- end
-
- 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_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_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_gitlab_http_status(404)
- end
-
- it "normalizes +1 as thumbsup award" do
- post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: '+1'
-
- expect(issue.award_emoji.last.name).to eq("thumbsup")
- end
-
- context 'when the emoji already has been awarded' do
- it 'returns a 404 status code' 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_gitlab_http_status(404)
- expect(json_response["message"]).to match("has already been taken")
- end
- end
- end
-
- context 'on a snippet' do
- it 'creates a new award emoji' do
- snippet = create(:project_snippet, :public, project: project)
-
- post v3_api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji", user), name: 'blowfish'
-
- 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
- end
- end
-
- describe "POST /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji" do
- let(:note2) { create(:note, project: project, noteable: issue, author: user) }
-
- it 'creates a new award emoji' do
- expect 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_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_gitlab_http_status(404)
- end
-
- it "normalizes +1 as thumbsup award" do
- post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: '+1'
-
- expect(note.award_emoji.last.name).to eq("thumbsup")
- end
-
- context 'when the emoji already has been awarded' do
- it 'returns a 404 status code' 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_gitlab_http_status(404)
- expect(json_response["message"]).to match("has already been taken")
- end
- end
- end
-
- describe 'DELETE /projects/:id/awardable/:awardable_id/award_emoji/:award_id' do
- context 'when the awardable is an Issue' do
- it 'deletes the award' do
- expect do
- delete v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/#{award_emoji.id}", user)
-
- 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_gitlab_http_status(404)
- end
- end
-
- context 'when the awardable is a Merge Request' do
- it 'deletes the award' do
- expect do
- delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user)
-
- 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_gitlab_http_status(404)
- end
- end
-
- context 'when the awardable is a Snippet' do
- let(:snippet) { create(:project_snippet, :public, project: project) }
- let!(:award) { create(:award_emoji, awardable: snippet, user: user) }
-
- it 'deletes the award' do
- expect do
- delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji/#{award.id}", user)
-
- expect(response).to have_gitlab_http_status(200)
- end.to change { snippet.award_emoji.count }.from(1).to(0)
- end
- end
- end
-
- describe 'DELETE /projects/:id/awardable/:awardable_id/award_emoji/:award_emoji_id' do
- let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket', user: user) }
-
- it 'deletes the award' do
- expect do
- delete v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji/#{rocket.id}", user)
-
- expect(response).to have_gitlab_http_status(200)
- end.to change { note.award_emoji.count }.from(1).to(0)
- end
- end
-end
diff --git a/spec/requests/api/v3/boards_spec.rb b/spec/requests/api/v3/boards_spec.rb
deleted file mode 100644
index dde4f096193..00000000000
--- a/spec/requests/api/v3/boards_spec.rb
+++ /dev/null
@@ -1,114 +0,0 @@
-require 'spec_helper'
-
-describe API::V3::Boards do
- set(:user) { create(:user) }
- set(:guest) { create(:user) }
- set(:non_member) { create(:user) }
- set(:project) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) }
-
- set(:dev_label) do
- create(:label, title: 'Development', color: '#FFAABB', project: project)
- end
-
- set(:test_label) do
- create(:label, title: 'Testing', color: '#FFAACC', project: project)
- end
-
- set(:dev_list) do
- create(:list, label: dev_label, position: 1)
- end
-
- set(:test_list) do
- create(:list, label: test_label, position: 2)
- end
-
- set(:board) do
- create(:board, project: project, lists: [dev_list, test_list])
- end
-
- before do
- project.add_reporter(user)
- project.add_guest(guest)
- end
-
- describe "GET /projects/:id/boards" do
- let(:base_url) { "/projects/#{project.id}/boards" }
-
- context "when unauthenticated" do
- it "returns authentication error" do
- get v3_api(base_url)
-
- expect(response).to have_gitlab_http_status(401)
- end
- end
-
- context "when authenticated" do
- it "returns the project issue board" do
- get v3_api(base_url, user)
-
- 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)
- expect(json_response.first['lists']).to be_an Array
- expect(json_response.first['lists'].length).to eq(2)
- expect(json_response.first['lists'].last).to have_key('position')
- end
- end
- end
-
- describe "GET /projects/:id/boards/:board_id/lists" do
- let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
-
- it 'returns issue board lists' do
- get v3_api(base_url, user)
-
- 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)
- end
-
- it 'returns 404 if board not found' do
- get v3_api("/projects/#{project.id}/boards/22343/lists", user)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
-
- describe "DELETE /projects/:id/board/lists/:list_id" do
- let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
-
- it "rejects a non member from deleting a list" do
- delete v3_api("#{base_url}/#{dev_list.id}", non_member)
-
- 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_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_gitlab_http_status(404)
- end
-
- context "when the user is project owner" do
- set(:owner) { create(:user) }
-
- before do
- project.update(namespace: owner.namespace)
- end
-
- it "deletes the list if an admin requests it" do
- delete v3_api("#{base_url}/#{dev_list.id}", owner)
-
- expect(response).to have_gitlab_http_status(200)
- end
- end
- end
-end
diff --git a/spec/requests/api/v3/branches_spec.rb b/spec/requests/api/v3/branches_spec.rb
deleted file mode 100644
index 1e038595a1f..00000000000
--- a/spec/requests/api/v3/branches_spec.rb
+++ /dev/null
@@ -1,120 +0,0 @@
-require 'spec_helper'
-require 'mime/types'
-
-describe API::V3::Branches do
- set(:user) { create(:user) }
- set(:user2) { create(:user) }
- set(:project) { create(:project, :repository, creator: user) }
- set(:master) { create(:project_member, :master, user: user, project: project) }
- set(:guest) { create(:project_member, :guest, user: user2, project: project) }
- let!(:branch_name) { 'feature' }
- let!(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' }
- let!(:branch_with_dot) { CreateBranchService.new(project, user).execute("with.1.2.3", "master") }
-
- describe "GET /projects/:id/repository/branches" do
- it "returns an array of project branches" do
- project.repository.expire_all_method_caches
-
- get v3_api("/projects/#{project.id}/repository/branches", user), per_page: 100
-
- 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)
- end
- end
-
- describe "DELETE /projects/:id/repository/branches/:branch" do
- before do
- allow_any_instance_of(Repository).to receive(:rm_branch).and_return(true)
- end
-
- it "removes branch" do
- delete v3_api("/projects/#{project.id}/repository/branches/#{branch_name}", user)
-
- 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_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_gitlab_http_status(404)
- end
- end
-
- describe "DELETE /projects/:id/repository/merged_branches" do
- before do
- allow_any_instance_of(Repository).to receive(:rm_branch).and_return(true)
- end
-
- it 'returns 200' do
- delete v3_api("/projects/#{project.id}/repository/merged_branches", user)
-
- 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_gitlab_http_status(403)
- end
- end
-
- describe "POST /projects/:id/repository/branches" do
- it "creates a new branch" do
- post v3_api("/projects/#{project.id}/repository/branches", user),
- branch_name: 'feature1',
- ref: branch_sha
-
- expect(response).to have_gitlab_http_status(201)
-
- expect(json_response['name']).to eq('feature1')
- expect(json_response['commit']['id']).to eq(branch_sha)
- end
-
- it "denies for user without push access" do
- post v3_api("/projects/#{project.id}/repository/branches", user2),
- branch_name: branch_name,
- ref: branch_sha
- 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_gitlab_http_status(400)
- expect(json_response['message']).to eq('Branch name is invalid')
- end
-
- it 'returns 400 if branch already exists' do
- post v3_api("/projects/#{project.id}/repository/branches", user),
- branch_name: 'new_design1',
- ref: branch_sha
- 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_gitlab_http_status(400)
- expect(json_response['message']).to eq('Branch already exists')
- end
-
- it 'returns 400 if ref name is invalid' do
- post v3_api("/projects/#{project.id}/repository/branches", user),
- branch_name: 'new_design3',
- ref: 'foo'
-
- expect(response).to have_gitlab_http_status(400)
- expect(json_response['message']).to eq('Invalid reference name')
- end
- end
-end
diff --git a/spec/requests/api/v3/broadcast_messages_spec.rb b/spec/requests/api/v3/broadcast_messages_spec.rb
deleted file mode 100644
index d9641011491..00000000000
--- a/spec/requests/api/v3/broadcast_messages_spec.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-require 'spec_helper'
-
-describe API::V3::BroadcastMessages do
- set(:user) { create(:user) }
- set(:admin) { create(:admin) }
-
- describe 'DELETE /broadcast_messages/:id' do
- set(:message) { create(:broadcast_message) }
-
- it 'returns a 401 for anonymous users' do
- delete v3_api("/broadcast_messages/#{message.id}"),
- attributes_for(:broadcast_message)
-
- 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_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_gitlab_http_status(200)
- end.to change { BroadcastMessage.count }.by(-1)
- end
- end
-end
diff --git a/spec/requests/api/v3/builds_spec.rb b/spec/requests/api/v3/builds_spec.rb
deleted file mode 100644
index 485d7c2cc43..00000000000
--- a/spec/requests/api/v3/builds_spec.rb
+++ /dev/null
@@ -1,550 +0,0 @@
-require 'spec_helper'
-
-describe API::V3::Builds do
- set(:user) { create(:user) }
- let(:api_user) { user }
- set(:project) { create(:project, :repository, creator: user, public_builds: false) }
- let!(:developer) { create(:project_member, :developer, user: user, project: project) }
- let(:reporter) { create(:project_member, :reporter, project: project) }
- let(:guest) { create(:project_member, :guest, project: project) }
- let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) }
- let(:build) { create(:ci_build, pipeline: pipeline) }
-
- describe 'GET /projects/:id/builds ' do
- let(:query) { '' }
-
- before do |example|
- build
-
- create(:ci_build, :skipped, pipeline: pipeline)
-
- unless example.metadata[:skip_before_request]
- get v3_api("/projects/#{project.id}/builds?#{query}", api_user)
- end
- end
-
- context 'authorized user' do
- it 'returns project builds' do
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- end
-
- it 'returns correct values' do
- expect(json_response).not_to be_empty
- expect(json_response.first['commit']['id']).to eq project.commit.id
- end
-
- it 'returns pipeline data' do
- json_build = json_response.first
- expect(json_build['pipeline']).not_to be_empty
- expect(json_build['pipeline']['id']).to eq build.pipeline.id
- expect(json_build['pipeline']['ref']).to eq build.pipeline.ref
- expect(json_build['pipeline']['sha']).to eq build.pipeline.sha
- expect(json_build['pipeline']['status']).to eq build.pipeline.status
- end
-
- it 'avoids N+1 queries', :skip_before_request do
- first_build = create(:ci_build, :artifacts, pipeline: pipeline)
- first_build.runner = create(:ci_runner)
- first_build.user = create(:user)
- first_build.save
-
- control_count = ActiveRecord::QueryRecorder.new { go }.count
-
- second_pipeline = create(:ci_empty_pipeline, project: project, sha: project.commit.id, ref: project.default_branch)
- second_build = create(:ci_build, :artifacts, pipeline: second_pipeline)
- second_build.runner = create(:ci_runner)
- second_build.user = create(:user)
- second_build.save
-
- expect { go }.not_to exceed_query_limit(control_count)
- end
-
- context 'filter project with one scope element' do
- let(:query) { 'scope=pending' }
-
- it do
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- end
- end
-
- context 'filter project with scope skipped' do
- let(:query) { 'scope=skipped' }
- let(:json_build) { json_response.first }
-
- it 'return builds with status skipped' do
- 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'
- end
- end
-
- context 'filter project with array of scope elements' do
- let(:query) { 'scope[0]=pending&scope[1]=running' }
-
- it do
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- end
- end
-
- context 'respond 400 when scope contains invalid state' do
- let(:query) { 'scope[0]=pending&scope[1]=unknown_status' }
-
- it { expect(response).to have_gitlab_http_status(400) }
- end
- end
-
- context 'unauthorized user' do
- let(:api_user) { nil }
-
- it 'does not return project builds' do
- expect(response).to have_gitlab_http_status(401)
- end
- end
-
- def go
- get v3_api("/projects/#{project.id}/builds?#{query}", api_user)
- end
- end
-
- describe 'GET /projects/:id/repository/commits/:sha/builds' do
- before do
- build
- end
-
- context 'when commit does not exist in repository' do
- before do
- get v3_api("/projects/#{project.id}/repository/commits/1a271fd1/builds", api_user)
- end
-
- it 'responds with 404' do
- expect(response).to have_gitlab_http_status(404)
- end
- end
-
- context 'when commit exists in repository' do
- context 'when user is authorized' do
- context 'when pipeline has jobs' do
- before do
- create(:ci_pipeline, project: project, sha: project.commit.id)
- create(:ci_build, pipeline: pipeline)
- create(:ci_build)
-
- get v3_api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", api_user)
- end
-
- it 'returns project jobs for specific commit' do
- 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
- end
-
- it 'returns pipeline data' do
- json_build = json_response.first
- expect(json_build['pipeline']).not_to be_empty
- expect(json_build['pipeline']['id']).to eq build.pipeline.id
- expect(json_build['pipeline']['ref']).to eq build.pipeline.ref
- expect(json_build['pipeline']['sha']).to eq build.pipeline.sha
- expect(json_build['pipeline']['status']).to eq build.pipeline.status
- end
- end
-
- context 'when pipeline has no jobs' do
- before do
- branch_head = project.commit('feature').id
- get v3_api("/projects/#{project.id}/repository/commits/#{branch_head}/builds", api_user)
- end
-
- it 'returns an empty array' do
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response).to be_empty
- end
- end
- end
-
- context 'when user is not authorized' do
- before do
- create(:ci_pipeline, project: project, sha: project.commit.id)
- create(:ci_build, pipeline: pipeline)
-
- get v3_api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", nil)
- end
-
- it 'does not return project jobs' do
- expect(response).to have_gitlab_http_status(401)
- expect(json_response.except('message')).to be_empty
- end
- end
- end
- end
-
- describe 'GET /projects/:id/builds/:build_id' do
- before do
- get v3_api("/projects/#{project.id}/builds/#{build.id}", api_user)
- end
-
- context 'authorized user' do
- it 'returns specific job data' do
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['name']).to eq('test')
- end
-
- it 'returns pipeline data' do
- json_build = json_response
- expect(json_build['pipeline']).not_to be_empty
- expect(json_build['pipeline']['id']).to eq build.pipeline.id
- expect(json_build['pipeline']['ref']).to eq build.pipeline.ref
- expect(json_build['pipeline']['sha']).to eq build.pipeline.sha
- expect(json_build['pipeline']['status']).to eq build.pipeline.status
- end
- end
-
- context 'unauthorized user' do
- let(:api_user) { nil }
-
- it 'does not return specific job data' do
- expect(response).to have_gitlab_http_status(401)
- end
- end
- end
-
- describe 'GET /projects/:id/builds/:build_id/artifacts' do
- before do
- stub_artifacts_object_storage
- get v3_api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user)
- end
-
- context 'job with artifacts' do
- context 'when artifacts are stored locally' do
- let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
-
- context 'authorized user' do
- let(:download_headers) do
- { 'Content-Transfer-Encoding' => 'binary',
- 'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' }
- end
-
- it 'returns specific job artifacts' do
- expect(response).to have_http_status(200)
- expect(response.headers.to_h).to include(download_headers)
- expect(response.body).to match_file(build.artifacts_file.file.file)
- end
- end
- end
-
- context 'when artifacts are stored remotely' do
- let(:build) { create(:ci_build, pipeline: pipeline) }
- let!(:artifact) { create(:ci_job_artifact, :archive, :remote_store, job: build) }
-
- it 'returns location redirect' do
- get v3_api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user)
-
- expect(response).to have_gitlab_http_status(302)
- end
- end
-
- context 'unauthorized user' do
- let(:api_user) { nil }
-
- it 'does not return specific job artifacts' do
- 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_gitlab_http_status(404)
- end
- end
-
- describe 'GET /projects/:id/artifacts/:ref_name/download?job=name' do
- let(:api_user) { reporter.user }
- let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
-
- before do
- stub_artifacts_object_storage
- build.success
- end
-
- def path_for_ref(ref = pipeline.ref, job = build.name)
- v3_api("/projects/#{project.id}/builds/artifacts/#{ref}/download?job=#{job}", api_user)
- end
-
- context 'when not logged in' do
- let(:api_user) { nil }
-
- before do
- get path_for_ref
- end
-
- it 'gives 401' do
- expect(response).to have_gitlab_http_status(401)
- end
- end
-
- context 'when logging as guest' do
- let(:api_user) { guest.user }
-
- before do
- get path_for_ref
- end
-
- it 'gives 403' do
- 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_gitlab_http_status(:not_found) }
- end
-
- context 'has no such ref' do
- before do
- get path_for_ref('TAIL', build.name)
- end
-
- it_behaves_like 'not found'
- end
-
- context 'has no such job' do
- before do
- get path_for_ref(pipeline.ref, 'NOBUILD')
- end
-
- it_behaves_like 'not found'
- end
- end
-
- context 'find proper job' do
- shared_examples 'a valid file' do
- context 'when artifacts are stored locally' do
- let(:download_headers) do
- { 'Content-Transfer-Encoding' => 'binary',
- 'Content-Disposition' =>
- "attachment; filename=#{build.artifacts_file.filename}" }
- end
-
- it { expect(response).to have_http_status(200) }
- it { expect(response.headers.to_h).to include(download_headers) }
- end
-
- context 'when artifacts are stored remotely' do
- let(:build) { create(:ci_build, pipeline: pipeline) }
- let!(:artifact) { create(:ci_job_artifact, :archive, :remote_store, job: build) }
-
- before do
- build.reload
-
- get v3_api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user)
- end
-
- it 'returns location redirect' do
- expect(response).to have_http_status(302)
- end
- end
- end
-
- context 'with regular branch' do
- before do
- pipeline.reload
- pipeline.update(ref: 'master',
- sha: project.commit('master').sha)
-
- get path_for_ref('master')
- end
-
- it_behaves_like 'a valid file'
- end
-
- context 'with branch name containing slash' do
- before do
- pipeline.reload
- pipeline.update(ref: 'improve/awesome',
- sha: project.commit('improve/awesome').sha)
- end
-
- before do
- get path_for_ref('improve/awesome')
- end
-
- it_behaves_like 'a valid file'
- end
- end
- end
-
- describe 'GET /projects/:id/builds/:build_id/trace' do
- let(:build) { create(:ci_build, :trace_live, pipeline: pipeline) }
-
- before do
- get v3_api("/projects/#{project.id}/builds/#{build.id}/trace", api_user)
- end
-
- context 'authorized user' do
- it 'returns specific job trace' do
- expect(response).to have_gitlab_http_status(200)
- expect(response.body).to eq(build.trace.raw)
- end
- end
-
- context 'unauthorized user' do
- let(:api_user) { nil }
-
- it 'does not return specific job trace' do
- expect(response).to have_gitlab_http_status(401)
- end
- end
- end
-
- describe 'POST /projects/:id/builds/:build_id/cancel' do
- before do
- post v3_api("/projects/#{project.id}/builds/#{build.id}/cancel", api_user)
- end
-
- context 'authorized user' do
- context 'user with :update_build persmission' do
- it 'cancels running or pending job' do
- expect(response).to have_gitlab_http_status(201)
- expect(project.builds.first.status).to eq('canceled')
- end
- end
-
- context 'user without :update_build permission' do
- let(:api_user) { reporter.user }
-
- it 'does not cancel job' do
- expect(response).to have_gitlab_http_status(403)
- end
- end
- end
-
- context 'unauthorized user' do
- let(:api_user) { nil }
-
- it 'does not cancel job' do
- expect(response).to have_gitlab_http_status(401)
- end
- end
- end
-
- describe 'POST /projects/:id/builds/:build_id/retry' do
- let(:build) { create(:ci_build, :canceled, pipeline: pipeline) }
-
- before do
- post v3_api("/projects/#{project.id}/builds/#{build.id}/retry", api_user)
- end
-
- context 'authorized user' do
- context 'user with :update_build permission' do
- it 'retries non-running job' do
- expect(response).to have_gitlab_http_status(201)
- expect(project.builds.first.status).to eq('canceled')
- expect(json_response['status']).to eq('pending')
- end
- end
-
- context 'user without :update_build permission' do
- let(:api_user) { reporter.user }
-
- it 'does not retry job' do
- expect(response).to have_gitlab_http_status(403)
- end
- end
- end
-
- context 'unauthorized user' do
- let(:api_user) { nil }
-
- it 'does not retry job' do
- 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
-
- context 'job is erasable' do
- let(:build) { create(:ci_build, :trace_artifact, :artifacts, :success, project: project, pipeline: pipeline) }
-
- it 'erases job content' do
- expect(response.status).to eq 201
- expect(build).not_to have_trace
- expect(build.artifacts_file.exists?).to be_falsy
- expect(build.artifacts_metadata.exists?).to be_falsy
- end
-
- it 'updates job' do
- expect(build.reload.erased_at).to be_truthy
- expect(build.reload.erased_by).to eq user
- end
- end
-
- context 'job is not erasable' do
- let(:build) { create(:ci_build, :trace_live, project: project, pipeline: pipeline) }
-
- it 'responds with forbidden' do
- expect(response.status).to eq 403
- end
- end
- end
-
- describe 'POST /projects/:id/builds/:build_id/artifacts/keep' do
- before do
- post v3_api("/projects/#{project.id}/builds/#{build.id}/artifacts/keep", user)
- end
-
- context 'artifacts did not expire' do
- let(:build) do
- create(:ci_build, :trace_artifact, :artifacts, :success,
- project: project, pipeline: pipeline, artifacts_expire_at: Time.now + 7.days)
- end
-
- it 'keeps artifacts' do
- expect(response.status).to eq 200
- expect(build.reload.artifacts_expire_at).to be_nil
- end
- end
-
- context 'no artifacts' do
- let(:build) { create(:ci_build, project: project, pipeline: pipeline) }
-
- it 'responds with not found' do
- expect(response.status).to eq 404
- end
- end
- end
-
- describe 'POST /projects/:id/builds/:build_id/play' do
- before do
- post v3_api("/projects/#{project.id}/builds/#{build.id}/play", user)
- end
-
- context 'on an playable job' do
- let(:build) { create(:ci_build, :manual, project: project, pipeline: pipeline) }
-
- it 'plays the job' do
- 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
- end
-
- context 'on a non-playable job' do
- it 'returns a status code 400, Bad Request' do
- expect(response).to have_gitlab_http_status 400
- expect(response.body).to match("Unplayable Job")
- end
- end
- end
-end
diff --git a/spec/requests/api/v3/commits_spec.rb b/spec/requests/api/v3/commits_spec.rb
deleted file mode 100644
index 9ef3b859001..00000000000
--- a/spec/requests/api/v3/commits_spec.rb
+++ /dev/null
@@ -1,603 +0,0 @@
-require 'spec_helper'
-require 'mime/types'
-
-describe API::V3::Commits do
- let(:user) { create(:user) }
- let(:user2) { create(:user) }
- let!(:project) { create(:project, :repository, creator: user, namespace: user.namespace) }
- let!(:guest) { create(:project_member, :guest, user: user2, project: project) }
- let!(:note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'a comment on a commit') }
- let!(:another_note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'another comment on a commit') }
-
- before { project.add_reporter(user) }
-
- describe "List repository commits" do
- context "authorized user" do
- before { project.add_reporter(user2) }
-
- it "returns project commits" do
- commit = project.repository.commit
- get v3_api("/projects/#{project.id}/repository/commits", user)
-
- 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)
- expect(json_response.first['committer_email']).to eq(commit.committer_email)
- end
- end
-
- context "unauthorized user" do
- it "does not return project commits" do
- get v3_api("/projects/#{project.id}/repository/commits")
- expect(response).to have_gitlab_http_status(401)
- end
- end
-
- context "since optional parameter" do
- it "returns project commits since provided parameter" do
- commits = project.repository.commits("master", limit: 2)
- since = commits.second.created_at
-
- get v3_api("/projects/#{project.id}/repository/commits?since=#{since.utc.iso8601}", user)
-
- expect(json_response.size).to eq 2
- expect(json_response.first["id"]).to eq(commits.first.id)
- expect(json_response.second["id"]).to eq(commits.second.id)
- end
- end
-
- context "until optional parameter" do
- it "returns project commits until provided parameter" do
- commits = project.repository.commits("master", limit: 20)
- before = commits.second.created_at
-
- get v3_api("/projects/#{project.id}/repository/commits?until=#{before.utc.iso8601}", user)
-
- if commits.size == 20
- expect(json_response.size).to eq(20)
- else
- expect(json_response.size).to eq(commits.size - 1)
- end
-
- expect(json_response.first["id"]).to eq(commits.second.id)
- expect(json_response.second["id"]).to eq(commits.third.id)
- end
- end
-
- context "invalid xmlschema date parameters" 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_gitlab_http_status(400)
- expect(json_response['error']).to eq('since is invalid')
- end
- end
-
- context "path optional parameter" do
- it "returns project commits matching provided path parameter" do
- path = 'files/ruby/popen.rb'
-
- get v3_api("/projects/#{project.id}/repository/commits?path=#{path}", user)
-
- expect(json_response.size).to eq(3)
- expect(json_response.first["id"]).to eq("570e7b2abdd848b95f2f578043fc23bd6f6fd24d")
- end
- end
- end
-
- describe "POST /projects/:id/repository/commits" do
- let!(:url) { "/projects/#{project.id}/repository/commits" }
-
- it 'returns a 403 unauthorized for user without permissions' do
- post v3_api(url, user2)
-
- 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_gitlab_http_status(400)
- end
-
- describe 'create' do
- let(:message) { 'Created file' }
- let!(:invalid_c_params) do
- {
- branch_name: 'master',
- commit_message: message,
- actions: [
- {
- action: 'create',
- file_path: 'files/ruby/popen.rb',
- content: 'puts 8'
- }
- ]
- }
- end
- let!(:valid_c_params) do
- {
- branch_name: 'master',
- commit_message: message,
- actions: [
- {
- action: 'create',
- file_path: 'foo/bar/baz.txt',
- content: 'puts 8'
- }
- ]
- }
- end
-
- it 'a new file in project repo' do
- post v3_api(url, user), valid_c_params
-
- 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)
- end
-
- it 'returns a 400 bad request if file exists' do
- post v3_api(url, user), invalid_c_params
-
- expect(response).to have_gitlab_http_status(400)
- end
-
- context 'with project path containing a dot in URL' do
- let!(:user) { create(:user, username: 'foo.bar') }
- let(:url) { "/projects/#{CGI.escape(project.full_path)}/repository/commits" }
-
- it 'a new file in project repo' do
- post v3_api(url, user), valid_c_params
-
- expect(response).to have_gitlab_http_status(201)
- end
- end
- end
-
- describe 'delete' do
- let(:message) { 'Deleted file' }
- let!(:invalid_d_params) do
- {
- branch_name: 'markdown',
- commit_message: message,
- actions: [
- {
- action: 'delete',
- file_path: 'doc/api/projects.md'
- }
- ]
- }
- end
- let!(:valid_d_params) do
- {
- branch_name: 'markdown',
- commit_message: message,
- actions: [
- {
- action: 'delete',
- file_path: 'doc/api/users.md'
- }
- ]
- }
- end
-
- it 'an existing file in project repo' do
- post v3_api(url, user), valid_d_params
-
- 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_gitlab_http_status(400)
- end
- end
-
- describe 'move' do
- let(:message) { 'Moved file' }
- let!(:invalid_m_params) do
- {
- branch_name: 'feature',
- commit_message: message,
- actions: [
- {
- action: 'move',
- file_path: 'CHANGELOG',
- previous_path: 'VERSION',
- content: '6.7.0.pre'
- }
- ]
- }
- end
- let!(:valid_m_params) do
- {
- branch_name: 'feature',
- commit_message: message,
- actions: [
- {
- action: 'move',
- file_path: 'VERSION.txt',
- previous_path: 'VERSION',
- content: '6.7.0.pre'
- }
- ]
- }
- end
-
- it 'an existing file in project repo' do
- post v3_api(url, user), valid_m_params
-
- 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_gitlab_http_status(400)
- end
- end
-
- describe 'update' do
- let(:message) { 'Updated file' }
- let!(:invalid_u_params) do
- {
- branch_name: 'master',
- commit_message: message,
- actions: [
- {
- action: 'update',
- file_path: 'foo/bar.baz',
- content: 'puts 8'
- }
- ]
- }
- end
- let!(:valid_u_params) do
- {
- branch_name: 'master',
- commit_message: message,
- actions: [
- {
- action: 'update',
- file_path: 'files/ruby/popen.rb',
- content: 'puts 8'
- }
- ]
- }
- end
-
- it 'an existing file in project repo' do
- post v3_api(url, user), valid_u_params
-
- 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_gitlab_http_status(400)
- end
- end
-
- context "multiple operations" do
- let(:message) { 'Multiple actions' }
- let!(:invalid_mo_params) do
- {
- branch_name: 'master',
- commit_message: message,
- actions: [
- {
- action: 'create',
- file_path: 'files/ruby/popen.rb',
- content: 'puts 8'
- },
- {
- action: 'delete',
- file_path: 'doc/v3_api/projects.md'
- },
- {
- action: 'move',
- file_path: 'CHANGELOG',
- previous_path: 'VERSION',
- content: '6.7.0.pre'
- },
- {
- action: 'update',
- file_path: 'foo/bar.baz',
- content: 'puts 8'
- }
- ]
- }
- end
- let!(:valid_mo_params) do
- {
- branch_name: 'master',
- commit_message: message,
- actions: [
- {
- action: 'create',
- file_path: 'foo/bar/baz.txt',
- content: 'puts 8'
- },
- {
- action: 'delete',
- file_path: 'Gemfile.zip'
- },
- {
- action: 'move',
- file_path: 'VERSION.txt',
- previous_path: 'VERSION',
- content: '6.7.0.pre'
- },
- {
- action: 'update',
- file_path: 'files/ruby/popen.rb',
- content: 'puts 8'
- }
- ]
- }
- end
-
- it 'are commited as one in project repo' do
- post v3_api(url, user), valid_mo_params
-
- 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_gitlab_http_status(400)
- end
- end
- end
-
- describe "Get a single commit" do
- context "authorized user" 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_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)
- expect(json_response['stats']['deletions']).to eq(project.repository.commit.stats.deletions)
- expect(json_response['stats']['total']).to eq(project.repository.commit.stats.total)
- end
-
- it "returns a 404 error if not found" do
- get v3_api("/projects/#{project.id}/repository/commits/invalid_sha", user)
- 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_gitlab_http_status(200)
- expect(json_response['status']).to be_nil
- end
-
- it "returns status for CI" do
- pipeline = project.pipelines.create(source: :push, ref: 'master', sha: project.repository.commit.sha, protected: false)
- pipeline.update(status: 'success')
-
- get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['status']).to eq(pipeline.status)
- end
-
- it "returns status for CI when pipeline is created" do
- project.pipelines.create(source: :push, ref: 'master', sha: project.repository.commit.sha, protected: false)
-
- get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['status']).to eq("created")
- end
-
- context 'when stat param' do
- let(:project_id) { project.id }
- let(:commit_id) { project.repository.commit.id }
- let(:route) { "/projects/#{project_id}/repository/commits/#{commit_id}" }
-
- it 'is not present return stats by default' do
- get v3_api(route, user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to include 'stats'
- end
-
- it "is false it does not include stats" do
- get v3_api(route, user), stats: false
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).not_to include 'stats'
- end
-
- it "is true it includes stats" do
- get v3_api(route, user), stats: true
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to include 'stats'
- end
- end
- end
-
- 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_gitlab_http_status(401)
- end
- end
- end
-
- describe "Get the diff of a commit" do
- context "authorized user" do
- before { project.add_reporter(user2) }
-
- 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_gitlab_http_status(200)
-
- expect(json_response).to be_an Array
- expect(json_response.length).to be >= 1
- expect(json_response.first.keys).to include "diff"
- end
-
- 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_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_gitlab_http_status(401)
- end
- end
- end
-
- describe 'Get the comments of a commit' 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_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')
- expect(json_response.first['author']['id']).to eq(user.id)
- end
-
- 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_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_gitlab_http_status(401)
- end
- end
- end
-
- describe 'POST :id/repository/commits/:sha/cherry_pick' do
- let(:master_pickable_commit) { project.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') }
-
- context 'authorized user' 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_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)
- expect(json_response['committer_name']).to eq(user.name)
- end
-
- 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_gitlab_http_status(400)
- expect(json_response['message']).to include('Sorry, we cannot cherry-pick this commit automatically.')
- end
-
- it 'returns 400 if you are not allowed to push to the target branch' do
- project.add_developer(user2)
- protected_branch = create(:protected_branch, project: project, name: 'feature')
-
- post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user2), branch: protected_branch.name
-
- 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_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_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_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_gitlab_http_status(400)
- expect(json_response['error']).to eq('branch is missing')
- end
- end
-
- context 'unauthorized user' 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_gitlab_http_status(401)
- end
- end
- end
-
- describe 'Post comment to commit' 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_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
- expect(json_response['line_type']).to be_nil
- end
-
- 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_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)
- expect(json_response['line_type']).to eq('new')
- end
-
- 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_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_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_gitlab_http_status(401)
- end
- end
- end
-end
diff --git a/spec/requests/api/v3/deploy_keys_spec.rb b/spec/requests/api/v3/deploy_keys_spec.rb
deleted file mode 100644
index 501af587ad4..00000000000
--- a/spec/requests/api/v3/deploy_keys_spec.rb
+++ /dev/null
@@ -1,179 +0,0 @@
-require 'spec_helper'
-
-describe API::V3::DeployKeys do
- let(:user) { create(:user) }
- let(:admin) { create(:admin) }
- let(:project) { create(:project, creator_id: user.id) }
- let(:project2) { create(:project, creator_id: user.id) }
- let(:deploy_key) { create(:deploy_key, public: true) }
-
- let!(:deploy_keys_project) do
- create(:deploy_keys_project, project: project, deploy_key: deploy_key)
- end
-
- describe 'GET /deploy_keys' do
- context 'when unauthenticated' do
- it 'should return authentication error' do
- get v3_api('/deploy_keys')
-
- expect(response.status).to eq(401)
- end
- end
-
- context 'when authenticated as non-admin user' do
- it 'should return a 403 error' do
- get v3_api('/deploy_keys', user)
-
- expect(response.status).to eq(403)
- end
- end
-
- context 'when authenticated as admin' do
- it 'should return all deploy keys' do
- get v3_api('/deploy_keys', admin)
-
- expect(response.status).to eq(200)
- expect(json_response).to be_an Array
- expect(json_response.first['id']).to eq(deploy_keys_project.deploy_key.id)
- end
- end
- end
-
- %w(deploy_keys keys).each do |path|
- describe "GET /projects/:id/#{path}" do
- before { deploy_key }
-
- it 'should return array of ssh keys' do
- get v3_api("/projects/#{project.id}/#{path}", admin)
-
- 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
- end
-
- describe "GET /projects/:id/#{path}/:key_id" do
- it 'should return a single key' do
- get v3_api("/projects/#{project.id}/#{path}/#{deploy_key.id}", admin)
-
- 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_gitlab_http_status(404)
- end
- end
-
- describe "POST /projects/:id/deploy_keys" 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_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_gitlab_http_status(400)
- expect(json_response['error']).to eq('title is missing')
- end
-
- it 'should create new ssh key' do
- key_attrs = attributes_for :another_key
-
- expect do
- post v3_api("/projects/#{project.id}/#{path}", admin), key_attrs
- end.to change { project.deploy_keys.count }.by(1)
- end
-
- it 'returns an existing ssh key when attempting to add a duplicate' do
- expect 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_gitlab_http_status(201)
- end
-
- it 'joins an existing ssh key to a new project' do
- expect 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_gitlab_http_status(201)
- end
-
- it 'accepts can_push parameter' do
- key_attrs = attributes_for(:another_key).merge(can_push: true)
-
- post v3_api("/projects/#{project.id}/#{path}", admin), key_attrs
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['can_push']).to eq(true)
- end
- end
-
- describe "DELETE /projects/:id/#{path}/:key_id" do
- before { deploy_key }
-
- it 'should delete existing key' do
- expect do
- delete v3_api("/projects/#{project.id}/#{path}/#{deploy_key.id}", admin)
- end.to change { project.deploy_keys.count }.by(-1)
- end
-
- it 'should return 404 Not Found with invalid ID' do
- delete v3_api("/projects/#{project.id}/#{path}/404", admin)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
-
- describe "POST /projects/:id/#{path}/:key_id/enable" do
- let(:project2) { create(:project) }
-
- context 'when the user can admin the project' do
- it 'enables the key' do
- expect 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_gitlab_http_status(201)
- expect(json_response['id']).to eq(deploy_key.id)
- end
- end
-
- context 'when authenticated as non-admin user' do
- it 'should return a 404 error' do
- post v3_api("/projects/#{project2.id}/#{path}/#{deploy_key.id}/enable", user)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
- end
-
- describe "DELETE /projects/:id/deploy_keys/:key_id/disable" do
- context 'when the user can admin the project' do
- it 'disables the key' do
- expect 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_gitlab_http_status(200)
- expect(json_response['id']).to eq(deploy_key.id)
- end
- end
-
- context 'when authenticated as non-admin user' do
- it 'should return a 404 error' do
- delete v3_api("/projects/#{project.id}/#{path}/#{deploy_key.id}/disable", user)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
- end
- end
-end
diff --git a/spec/requests/api/v3/deployments_spec.rb b/spec/requests/api/v3/deployments_spec.rb
deleted file mode 100644
index ac86fbea498..00000000000
--- a/spec/requests/api/v3/deployments_spec.rb
+++ /dev/null
@@ -1,69 +0,0 @@
-require 'spec_helper'
-
-describe API::V3::Deployments do
- let(:user) { create(:user) }
- let(:non_member) { create(:user) }
- let(:project) { deployment.environment.project }
- let!(:deployment) { create(:deployment) }
-
- before do
- project.add_master(user)
- end
-
- shared_examples 'a paginated resources' do
- before do
- # Fires the request
- request
- end
-
- it 'has pagination headers' do
- expect(response).to include_pagination_headers
- end
- end
-
- describe 'GET /projects/:id/deployments' do
- context 'as member of the project' do
- it_behaves_like 'a paginated resources' do
- let(:request) { get v3_api("/projects/#{project.id}/deployments", user) }
- end
-
- it 'returns projects deployments' do
- get v3_api("/projects/#{project.id}/deployments", user)
-
- 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)
- expect(json_response.first['sha']).to match /\A\h{40}\z/
- end
- end
-
- context 'as non member' do
- it 'returns a 404 status code' do
- get v3_api("/projects/#{project.id}/deployments", non_member)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
- end
-
- describe 'GET /projects/:id/deployments/:deployment_id' do
- context 'as a member of the project' do
- it 'returns the projects deployment' do
- get v3_api("/projects/#{project.id}/deployments/#{deployment.id}", user)
-
- 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
- end
-
- context 'as non member' do
- it 'returns a 404 status code' do
- get v3_api("/projects/#{project.id}/deployments/#{deployment.id}", non_member)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
- end
-end
diff --git a/spec/requests/api/v3/environments_spec.rb b/spec/requests/api/v3/environments_spec.rb
deleted file mode 100644
index 68be5256b64..00000000000
--- a/spec/requests/api/v3/environments_spec.rb
+++ /dev/null
@@ -1,163 +0,0 @@
-require 'spec_helper'
-
-describe API::V3::Environments do
- let(:user) { create(:user) }
- let(:non_member) { create(:user) }
- let(:project) { create(:project, :private, namespace: user.namespace) }
- let!(:environment) { create(:environment, project: project) }
-
- before do
- project.add_master(user)
- end
-
- shared_examples 'a paginated resources' do
- before do
- # Fires the request
- request
- end
-
- it 'has pagination headers' do
- expect(response.headers).to include('X-Total')
- expect(response.headers).to include('X-Total-Pages')
- expect(response.headers).to include('X-Per-Page')
- expect(response.headers).to include('X-Page')
- expect(response.headers).to include('X-Next-Page')
- expect(response.headers).to include('X-Prev-Page')
- expect(response.headers).to include('Link')
- end
- end
-
- describe 'GET /projects/:id/environments' do
- context 'as member of the project' do
- it_behaves_like 'a paginated resources' do
- let(:request) { get v3_api("/projects/#{project.id}/environments", user) }
- end
-
- it 'returns project environments' do
- get v3_api("/projects/#{project.id}/environments", user)
-
- 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)
- expect(json_response.first['external_url']).to eq(environment.external_url)
- expect(json_response.first['project']['id']).to eq(project.id)
- expect(json_response.first['project']['visibility_level']).to be_present
- end
- end
-
- context 'as non member' do
- it 'returns a 404 status code' do
- get v3_api("/projects/#{project.id}/environments", non_member)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
- end
-
- describe 'POST /projects/:id/environments' do
- context 'as a member' do
- it 'creates a environment with valid params' do
- post v3_api("/projects/#{project.id}/environments", user), name: "mepmep"
-
- 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
- end
-
- it 'requires name to be passed' do
- post v3_api("/projects/#{project.id}/environments", user), external_url: 'test.gitlab.com'
-
- 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_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_gitlab_http_status(400)
- expect(json_response["error"]).to eq("slug is automatically generated and cannot be changed")
- end
- end
-
- context 'a non member' do
- it 'rejects the request' do
- post v3_api("/projects/#{project.id}/environments", non_member), name: 'gitlab.com'
-
- expect(response).to have_gitlab_http_status(404)
- end
-
- it 'returns a 400 when the required params are missing' do
- post v3_api("/projects/12345/environments", non_member), external_url: 'http://env.git.com'
- end
- end
- end
-
- describe 'PUT /projects/:id/environments/:environment_id' do
- it 'returns a 200 if name and external_url are changed' do
- url = 'https://mepmep.whatever.ninja'
- put v3_api("/projects/#{project.id}/environments/#{environment.id}", user),
- name: 'Mepmep', external_url: url
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['name']).to eq('Mepmep')
- expect(json_response['external_url']).to eq(url)
- end
-
- it "won't allow slug to be changed" do
- slug = environment.slug
- api_url = v3_api("/projects/#{project.id}/environments/#{environment.id}", user)
- put api_url, slug: slug + "-foo"
-
- expect(response).to have_gitlab_http_status(400)
- expect(json_response["error"]).to eq("slug is automatically generated and cannot be changed")
- end
-
- it "won't update the external_url if only the name is passed" do
- url = environment.external_url
- put v3_api("/projects/#{project.id}/environments/#{environment.id}", user),
- name: 'Mepmep'
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['name']).to eq('Mepmep')
- expect(json_response['external_url']).to eq(url)
- end
-
- it 'returns a 404 if the environment does not exist' do
- put v3_api("/projects/#{project.id}/environments/12345", user)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
-
- describe 'DELETE /projects/:id/environments/:environment_id' do
- context 'as a master' do
- it 'returns a 200 for an existing environment' do
- delete v3_api("/projects/#{project.id}/environments/#{environment.id}", user)
-
- 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_gitlab_http_status(404)
- expect(json_response['message']).to eq('404 Not found')
- end
- end
-
- context 'a non member' do
- it 'rejects the request' do
- delete v3_api("/projects/#{project.id}/environments/#{environment.id}", non_member)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
- end
-end
diff --git a/spec/requests/api/v3/files_spec.rb b/spec/requests/api/v3/files_spec.rb
deleted file mode 100644
index 26a3d8870a0..00000000000
--- a/spec/requests/api/v3/files_spec.rb
+++ /dev/null
@@ -1,283 +0,0 @@
-require 'spec_helper'
-
-describe API::V3::Files do
- # I have to remove periods from the end of the name
- # This happened when the user's name had a suffix (i.e. "Sr.")
- # This seems to be what git does under the hood. For example, this commit:
- #
- # $ git commit --author='Foo Sr. <foo@example.com>' -m 'Where's my trailing period?'
- #
- # results in this:
- #
- # $ git show --pretty
- # ...
- # Author: Foo Sr <foo@example.com>
- # ...
-
- let(:user) { create(:user) }
- let!(:project) { create(:project, :repository, namespace: user.namespace ) }
- let(:guest) { create(:user) { |u| project.add_guest(u) } }
- let(:file_path) { 'files/ruby/popen.rb' }
- let(:params) do
- {
- file_path: file_path,
- ref: 'master'
- }
- end
- let(:author_email) { 'user@example.org' }
- let(:author_name) { 'John Doe' }
-
- before { project.add_developer(user) }
-
- describe "GET /projects/:id/repository/files" do
- let(:route) { "/projects/#{project.id}/repository/files" }
-
- shared_examples_for 'repository files' do
- it "returns file info" do
- get v3_api(route, current_user), params
-
- 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')
- expect(Base64.decode64(json_response['content']).lines.first).to eq("require 'fileutils'\n")
- end
-
- context 'when no params are given' do
- it_behaves_like '400 response' do
- let(:request) { get v3_api(route, current_user) }
- end
- end
-
- context 'when file_path does not exist' do
- let(:params) do
- {
- file_path: 'app/models/application.rb',
- ref: 'master'
- }
- end
-
- it_behaves_like '404 response' do
- let(:request) { get v3_api(route, current_user), params }
- let(:message) { '404 File Not Found' }
- end
- end
-
- context 'when repository is disabled' do
- include_context 'disabled repository'
-
- it_behaves_like '403 response' do
- let(:request) { get v3_api(route, current_user), params }
- end
- end
- end
-
- context 'when unauthenticated', 'and project is public' do
- it_behaves_like 'repository files' do
- let(:project) { create(:project, :public, :repository) }
- let(:current_user) { nil }
- end
- end
-
- context 'when unauthenticated', 'and project is private' do
- it_behaves_like '404 response' do
- let(:request) { get v3_api(route), params }
- let(:message) { '404 Project Not Found' }
- end
- end
-
- context 'when authenticated', 'as a developer' do
- it_behaves_like 'repository files' do
- let(:current_user) { user }
- end
- end
-
- context 'when authenticated', 'as a guest' do
- it_behaves_like '403 response' do
- let(:request) { get v3_api(route, guest), params }
- end
- end
- end
-
- describe "POST /projects/:id/repository/files" do
- let(:valid_params) do
- {
- file_path: 'newfile.rb',
- branch_name: 'master',
- content: 'puts 8',
- commit_message: 'Added newfile'
- }
- end
-
- it "creates a new file in project repo" do
- post v3_api("/projects/#{project.id}/repository/files", user), valid_params
-
- 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)
- expect(last_commit.author_name).to eq(user.name)
- end
-
- it "returns a 400 bad request if no params given" do
- post v3_api("/projects/#{project.id}/repository/files", user)
-
- expect(response).to have_gitlab_http_status(400)
- end
-
- it "returns a 400 if editor fails to create file" do
- allow_any_instance_of(Repository).to receive(:create_file)
- .and_raise(Gitlab::Git::CommitError, 'Cannot create file')
-
- post v3_api("/projects/#{project.id}/repository/files", user), valid_params
-
- expect(response).to have_gitlab_http_status(400)
- end
-
- context "when specifying an author" do
- it "creates a new file with the specified author" do
- valid_params.merge!(author_email: author_email, author_name: author_name)
-
- post v3_api("/projects/#{project.id}/repository/files", user), valid_params
-
- 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)
- end
- end
-
- context 'when the repo is empty' do
- let!(:project) { create(:project_empty_repo, namespace: user.namespace ) }
-
- it "creates a new file in project repo" do
- post v3_api("/projects/#{project.id}/repository/files", user), valid_params
-
- 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)
- expect(last_commit.author_name).to eq(user.name)
- end
- end
- end
-
- describe "PUT /projects/:id/repository/files" do
- let(:valid_params) do
- {
- file_path: file_path,
- branch_name: 'master',
- content: 'puts 8',
- commit_message: 'Changed file'
- }
- end
-
- it "updates existing file in project repo" do
- put v3_api("/projects/#{project.id}/repository/files", user), valid_params
-
- 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)
- expect(last_commit.author_name).to eq(user.name)
- end
-
- it "returns a 400 bad request if no params given" do
- put v3_api("/projects/#{project.id}/repository/files", user)
-
- expect(response).to have_gitlab_http_status(400)
- end
-
- context "when specifying an author" do
- it "updates a file with the specified author" do
- valid_params.merge!(author_email: author_email, author_name: author_name, content: "New content")
-
- put v3_api("/projects/#{project.id}/repository/files", user), valid_params
-
- 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)
- end
- end
- end
-
- describe "DELETE /projects/:id/repository/files" do
- let(:valid_params) do
- {
- file_path: file_path,
- branch_name: 'master',
- commit_message: 'Changed file'
- }
- end
-
- it "deletes existing file in project repo" do
- delete v3_api("/projects/#{project.id}/repository/files", user), valid_params
-
- 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)
- expect(last_commit.author_name).to eq(user.name)
- end
-
- it "returns a 400 bad request if no params given" do
- delete v3_api("/projects/#{project.id}/repository/files", user)
-
- expect(response).to have_gitlab_http_status(400)
- end
-
- it "returns a 400 if fails to delete file" do
- allow_any_instance_of(Repository).to receive(:delete_file).and_raise(Gitlab::Git::CommitError, 'Cannot delete file')
-
- delete v3_api("/projects/#{project.id}/repository/files", user), valid_params
-
- expect(response).to have_gitlab_http_status(400)
- end
-
- context "when specifying an author" do
- it "removes a file with the specified author" do
- valid_params.merge!(author_email: author_email, author_name: author_name)
-
- delete v3_api("/projects/#{project.id}/repository/files", user), valid_params
-
- 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)
- end
- end
- end
-
- describe "POST /projects/:id/repository/files with binary file" do
- let(:file_path) { 'test.bin' }
- let(:put_params) do
- {
- file_path: file_path,
- branch_name: 'master',
- content: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=',
- commit_message: 'Binary file with a \n should not be touched',
- encoding: 'base64'
- }
- end
- let(:get_params) do
- {
- file_path: file_path,
- ref: 'master'
- }
- end
-
- before do
- post v3_api("/projects/#{project.id}/repository/files", user), put_params
- end
-
- it "remains unchanged" do
- get v3_api("/projects/#{project.id}/repository/files", user), get_params
-
- 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])
- end
- end
-end
diff --git a/spec/requests/api/v3/groups_spec.rb b/spec/requests/api/v3/groups_spec.rb
deleted file mode 100644
index 34d4b8e9565..00000000000
--- a/spec/requests/api/v3/groups_spec.rb
+++ /dev/null
@@ -1,566 +0,0 @@
-require 'spec_helper'
-
-describe API::V3::Groups do
- include UploadHelpers
-
- let(:user1) { create(:user, can_create_group: false) }
- let(:user2) { create(:user) }
- let(:user3) { create(:user) }
- let(:admin) { create(:admin) }
- let!(:group1) { create(:group, avatar: File.open(uploaded_image_temp_path)) }
- let!(:group2) { create(:group, :private) }
- let!(:project1) { create(:project, namespace: group1) }
- let!(:project2) { create(:project, namespace: group2) }
- let!(:project3) { create(:project, namespace: group1, path: 'test', visibility_level: Gitlab::VisibilityLevel::PRIVATE) }
-
- before do
- group1.add_owner(user1)
- group2.add_owner(user2)
- end
-
- describe "GET /groups" do
- context "when unauthenticated" do
- it "returns authentication error" do
- get v3_api("/groups")
-
- expect(response).to have_gitlab_http_status(401)
- end
- end
-
- context "when authenticated as user" do
- it "normal user: returns an array of groups of user1" do
- get v3_api("/groups", 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)
- .to satisfy_one { |group| group['name'] == group1.name }
- end
-
- it "does not include statistics" do
- get v3_api("/groups", user1), 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
-
- context "when authenticated as admin" do
- it "admin: returns an array of all groups" do
- get v3_api("/groups", 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 "does not include statistics by default" do
- get v3_api("/groups", 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
- attributes = {
- storage_size: 702,
- repository_size: 123,
- lfs_objects_size: 234,
- build_artifacts_size: 345
- }.stringify_keys
-
- project1.statistics.update!(attributes)
-
- get v3_api("/groups", admin), statistics: true
-
- 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 }
- end
- end
-
- context "when using skip_groups in request" do
- it "returns all groups excluding skipped groups" do
- get v3_api("/groups", admin), skip_groups: [group2.id]
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
- end
- end
-
- context "when using all_available in request" do
- let(:response_groups) { json_response.map { |group| group['name'] } }
-
- it "returns all groups you have access to" do
- public_group = create :group, :public
-
- get v3_api("/groups", user1), all_available: true
-
- 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
- end
-
- context "when using sorting" do
- let(:group3) { create(:group, name: "a#{group1.name}", path: "z#{group1.path}") }
- let(:response_groups) { json_response.map { |group| group['name'] } }
-
- before do
- group3.add_owner(user1)
- end
-
- it "sorts by name ascending by default" do
- get v3_api("/groups", user1)
-
- 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
-
- it "sorts in descending order when passed" do
- get v3_api("/groups", user1), sort: "desc"
-
- 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
-
- it "sorts by the order_by param" do
- get v3_api("/groups", user1), order_by: "path"
-
- 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
- end
- end
-
- describe 'GET /groups/owned' do
- context 'when unauthenticated' do
- it 'returns authentication error' do
- get v3_api('/groups/owned')
-
- expect(response).to have_gitlab_http_status(401)
- end
- end
-
- context 'when authenticated as group owner' do
- it 'returns an array of groups the user owns' do
- get v3_api('/groups/owned', 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.first['name']).to eq(group2.name)
- end
- end
- end
-
- describe "GET /groups/:id" do
- context "when authenticated as user" do
- it "returns one of user1's groups" do
- project = create(:project, namespace: group2, path: 'Foo')
- create(:project_group_link, project: project, group: group1)
-
- get v3_api("/groups/#{group1.id}", user1)
-
- 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)
- expect(json_response['description']).to eq(group1.description)
- expect(json_response['visibility_level']).to eq(group1.visibility_level)
- expect(json_response['avatar_url']).to eq(group1.avatar_url(only_path: false))
- expect(json_response['web_url']).to eq(group1.web_url)
- expect(json_response['request_access_enabled']).to eq(group1.request_access_enabled)
- expect(json_response['full_name']).to eq(group1.full_name)
- expect(json_response['full_path']).to eq(group1.full_path)
- expect(json_response['parent_id']).to eq(group1.parent_id)
- expect(json_response['projects']).to be_an Array
- expect(json_response['projects'].length).to eq(2)
- expect(json_response['shared_projects']).to be_an Array
- expect(json_response['shared_projects'].length).to eq(1)
- expect(json_response['shared_projects'][0]['id']).to eq(project.id)
- end
-
- it "does not return a non existing group" do
- get v3_api("/groups/1328", user1)
-
- 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_gitlab_http_status(404)
- end
- end
-
- context "when authenticated as admin" do
- it "returns any existing group" do
- get v3_api("/groups/#{group2.id}", admin)
-
- 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_gitlab_http_status(404)
- end
- end
-
- context 'when using group path in URL' do
- it 'returns any existing group' do
- get v3_api("/groups/#{group1.path}", admin)
-
- 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_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_gitlab_http_status(404)
- end
- end
- end
-
- describe 'PUT /groups/:id' do
- let(:new_group_name) { 'New Group'}
-
- context 'when authenticated as the group owner' 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_gitlab_http_status(200)
- expect(json_response['name']).to eq(new_group_name)
- expect(json_response['request_access_enabled']).to eq(true)
- end
-
- it 'returns 404 for a non existing group' do
- put v3_api('/groups/1328', user1), name: new_group_name
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
-
- context 'when authenticated as the admin' do
- it 'updates the group' do
- put v3_api("/groups/#{group1.id}", admin), name: new_group_name
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['name']).to eq(new_group_name)
- end
- end
-
- context 'when authenticated as an user that can see the group' do
- it 'does not updates the group' do
- put v3_api("/groups/#{group1.id}", user2), name: new_group_name
-
- expect(response).to have_gitlab_http_status(403)
- end
- end
-
- context 'when authenticated as an user that cannot see the group' 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_gitlab_http_status(404)
- end
- end
- end
-
- describe "GET /groups/:id/projects" do
- context "when authenticated as user" do
- it "returns the group's projects" do
- get v3_api("/groups/#{group1.id}/projects", user1)
-
- 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])
- expect(json_response.first['visibility_level']).to be_present
- end
-
- it "returns the group's projects with simple representation" do
- get v3_api("/groups/#{group1.id}/projects", user1), simple: true
-
- 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])
- expect(json_response.first['visibility_level']).not_to be_present
- end
-
- it 'filters the groups projects' do
- public_project = create(:project, :public, path: 'test1', group: group1)
-
- get v3_api("/groups/#{group1.id}/projects", user1), visibility: 'public'
-
- 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)
- end
-
- it "does not return a non existing group" do
- get v3_api("/groups/1328/projects", user1)
-
- 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_gitlab_http_status(404)
- end
-
- it "only returns projects to which user has access" do
- project3.add_developer(user3)
-
- get v3_api("/groups/#{group1.id}/projects", user3)
-
- 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
-
- it 'only returns the projects owned by user' do
- project2.group.add_owner(user3)
-
- get v3_api("/groups/#{project2.group.id}/projects", user3), owned: true
-
- 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
-
- it 'only returns the projects starred by user' do
- user1.starred_projects = [project1]
-
- get v3_api("/groups/#{group1.id}/projects", user1), starred: true
-
- 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
- end
-
- context "when authenticated as admin" do
- it "returns any existing group" do
- get v3_api("/groups/#{group2.id}/projects", admin)
-
- 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
-
- it "does not return a non existing group" do
- get v3_api("/groups/1328/projects", admin)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
-
- context 'when using group path in URL' do
- it 'returns any existing group' do
- get v3_api("/groups/#{group1.path}/projects", admin)
-
- 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
-
- it 'does not return a non existing group' do
- get v3_api('/groups/unknown/projects', admin)
-
- 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_gitlab_http_status(404)
- end
- end
- end
-
- describe "POST /groups" do
- context "when authenticated as user without group permissions" do
- it "does not create group" do
- post v3_api("/groups", user1), attributes_for(:group)
-
- expect(response).to have_gitlab_http_status(403)
- end
- end
-
- context "when authenticated as user with group permissions" do
- it "creates group" do
- group = attributes_for(:group, { request_access_enabled: false })
-
- post v3_api("/groups", user3), group
-
- expect(response).to have_gitlab_http_status(201)
-
- expect(json_response["name"]).to eq(group[:name])
- expect(json_response["path"]).to eq(group[:path])
- expect(json_response["request_access_enabled"]).to eq(group[:request_access_enabled])
- end
-
- it "creates a nested group", :nested_groups do
- parent = create(:group)
- parent.add_owner(user3)
- group = attributes_for(:group, { parent_id: parent.id })
-
- post v3_api("/groups", user3), group
-
- 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)
- end
-
- it "does not create group, duplicate" do
- post v3_api("/groups", user3), { name: 'Duplicate Test', path: group2.path }
-
- 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_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_gitlab_http_status(400)
- end
- end
- end
-
- describe "DELETE /groups/:id" do
- context "when authenticated as user" do
- it "removes group" do
- Sidekiq::Testing.fake! do
- expect { delete v3_api("/groups/#{group1.id}", user1) }.to change(GroupDestroyWorker.jobs, :size).by(1)
- end
-
- expect(response).to have_gitlab_http_status(202)
- end
-
- it "does not remove a group if not an owner" do
- user4 = create(:user)
- group1.add_master(user4)
-
- delete v3_api("/groups/#{group1.id}", user3)
-
- 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_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_gitlab_http_status(404)
- end
- end
-
- context "when authenticated as admin" do
- it "removes any existing group" do
- delete v3_api("/groups/#{group2.id}", admin)
-
- expect(response).to have_gitlab_http_status(202)
- end
-
- it "does not remove a non existing group" do
- delete v3_api("/groups/1328", admin)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
- end
-
- describe "POST /groups/:id/projects/:project_id" do
- let(:project) { create(:project) }
- let(:project_path) { CGI.escape(project.full_path) }
-
- before do
- allow_any_instance_of(Projects::TransferService)
- .to receive(:execute).and_return(true)
- end
-
- context "when authenticated as user" do
- it "does not transfer project to group" do
- post v3_api("/groups/#{group1.id}/projects/#{project.id}", user2)
-
- expect(response).to have_gitlab_http_status(403)
- end
- end
-
- context "when authenticated as admin" do
- it "transfers project to group" do
- post v3_api("/groups/#{group1.id}/projects/#{project.id}", admin)
-
- expect(response).to have_gitlab_http_status(201)
- end
-
- context 'when using project path in URL' do
- context 'with a valid project path' do
- it "transfers project to group" do
- post v3_api("/groups/#{group1.id}/projects/#{project_path}", admin)
-
- expect(response).to have_gitlab_http_status(201)
- end
- end
-
- context 'with a non-existent project path' do
- it "does not transfer project to group" do
- post v3_api("/groups/#{group1.id}/projects/nogroup%2Fnoproject", admin)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
- end
-
- context 'when using a group path in URL' do
- context 'with a valid group path' do
- it "transfers project to group" do
- post v3_api("/groups/#{group1.path}/projects/#{project_path}", admin)
-
- expect(response).to have_gitlab_http_status(201)
- end
- end
-
- context 'with a non-existent group path' do
- it "does not transfer project to group" do
- post v3_api("/groups/noexist/projects/#{project_path}", admin)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
- end
- end
- end
-end
diff --git a/spec/requests/api/v3/issues_spec.rb b/spec/requests/api/v3/issues_spec.rb
deleted file mode 100644
index 11b5469be7b..00000000000
--- a/spec/requests/api/v3/issues_spec.rb
+++ /dev/null
@@ -1,1298 +0,0 @@
-require 'spec_helper'
-
-describe API::V3::Issues do
- set(:user) { create(:user) }
- set(:user2) { create(:user) }
- set(:non_member) { create(:user) }
- set(:guest) { create(:user) }
- set(:author) { create(:author) }
- set(:assignee) { create(:assignee) }
- set(:admin) { create(:user, :admin) }
- let!(:project) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) }
- let!(:closed_issue) do
- create :closed_issue,
- author: user,
- assignees: [user],
- project: project,
- state: :closed,
- milestone: milestone,
- created_at: generate(:past_time),
- updated_at: 3.hours.ago
- end
- let!(:confidential_issue) do
- create :issue,
- :confidential,
- project: project,
- author: author,
- assignees: [assignee],
- created_at: generate(:past_time),
- updated_at: 2.hours.ago
- end
- let!(:issue) do
- create :issue,
- author: user,
- assignees: [user],
- project: project,
- milestone: milestone,
- created_at: generate(:past_time),
- updated_at: 1.hour.ago
- end
- let!(:label) do
- create(:label, title: 'label', color: '#FFAABB', project: project)
- end
- let!(:label_link) { create(:label_link, label: label, target: issue) }
- let!(:milestone) { create(:milestone, title: '1.0.0', project: project) }
- let!(:empty_milestone) do
- create(:milestone, title: '2.0.0', project: project)
- end
- let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) }
-
- let(:no_milestone_title) { URI.escape(Milestone::None.title) }
-
- before do
- project.add_reporter(user)
- project.add_guest(guest)
- end
-
- describe "GET /issues" do
- context "when unauthenticated" do
- it "returns authentication error" do
- get v3_api("/issues")
-
- expect(response).to have_gitlab_http_status(401)
- end
- end
-
- context "when authenticated" do
- it "returns an array of issues" do
- get v3_api("/issues", user)
-
- 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')
- end
-
- it 'returns an array of closed issues' do
- get v3_api('/issues?state=closed', user)
-
- 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)
- end
-
- it 'returns an array of opened issues' do
- get v3_api('/issues?state=opened', user)
-
- 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)
- end
-
- it 'returns an array of all issues' do
- get v3_api('/issues?state=all', user)
-
- 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)
- expect(json_response.second['id']).to eq(closed_issue.id)
- end
-
- it 'returns an array of labeled issues' do
- get v3_api("/issues?labels=#{label.title}", user)
-
- 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])
- end
-
- 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_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])
- end
-
- it 'returns an empty array if no issue matches labels' do
- get v3_api('/issues?labels=foo,bar', user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(0)
- end
-
- 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_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])
- expect(json_response.first['state']).to eq('opened')
- end
-
- 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_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(0)
- end
-
- it 'returns an empty array if no issue matches milestone' do
- get v3_api("/issues?milestone=#{empty_milestone.title}", user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(0)
- end
-
- it 'returns an empty array if milestone does not exist' do
- get v3_api("/issues?milestone=foo", user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(0)
- end
-
- it 'returns an array of issues in given milestone' do
- get v3_api("/issues?milestone=#{milestone.title}", user)
-
- 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)
- expect(json_response.second['id']).to eq(closed_issue.id)
- end
-
- it 'returns an array of issues matching state in milestone' do
- get v3_api("/issues?milestone=#{milestone.title}", user),
- '&state=closed'
-
- 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)
- end
-
- it 'returns an array of issues with no milestone' do
- get v3_api("/issues?milestone=#{no_milestone_title}", author)
-
- 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)
- end
-
- it 'sorts by created_at descending by default' do
- get v3_api('/issues', user)
-
- response_dates = json_response.map { |issue| issue['created_at'] }
-
- 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
-
- it 'sorts ascending when requested' do
- get v3_api('/issues?sort=asc', user)
-
- response_dates = json_response.map { |issue| issue['created_at'] }
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(response_dates).to eq(response_dates.sort)
- end
-
- it 'sorts by updated_at descending when requested' do
- get v3_api('/issues?order_by=updated_at', user)
-
- response_dates = json_response.map { |issue| issue['updated_at'] }
-
- 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
-
- it 'sorts by updated_at ascending when requested' do
- get v3_api('/issues?order_by=updated_at&sort=asc', user)
-
- response_dates = json_response.map { |issue| issue['updated_at'] }
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(response_dates).to eq(response_dates.sort)
- end
-
- it 'matches V3 response schema' do
- get v3_api('/issues', user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to match_response_schema('public_api/v3/issues')
- end
- end
- end
-
- describe "GET /groups/:id/issues" do
- let!(:group) { create(:group) }
- let!(:group_project) { create(:project, :public, creator_id: user.id, namespace: group) }
- let!(:group_closed_issue) do
- create :closed_issue,
- author: user,
- assignees: [user],
- project: group_project,
- state: :closed,
- milestone: group_milestone,
- updated_at: 3.hours.ago
- end
- let!(:group_confidential_issue) do
- create :issue,
- :confidential,
- project: group_project,
- author: author,
- assignees: [assignee],
- updated_at: 2.hours.ago
- end
- let!(:group_issue) do
- create :issue,
- author: user,
- assignees: [user],
- project: group_project,
- milestone: group_milestone,
- updated_at: 1.hour.ago
- end
- let!(:group_label) do
- create(:label, title: 'group_lbl', color: '#FFAABB', project: group_project)
- end
- let!(:group_label_link) { create(:label_link, label: group_label, target: group_issue) }
- let!(:group_milestone) { create(:milestone, title: '3.0.0', project: group_project) }
- let!(:group_empty_milestone) do
- create(:milestone, title: '4.0.0', project: group_project)
- end
- let!(:group_note) { create(:note_on_issue, author: user, project: group_project, noteable: group_issue) }
-
- before do
- group_project.add_reporter(user)
- end
- let(:base_url) { "/groups/#{group.id}/issues" }
-
- it 'returns all group issues (including opened and closed)' do
- get v3_api(base_url, admin)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(3)
- end
-
- 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_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)
- end
-
- it 'returns group confidential issues for author' do
- get v3_api("#{base_url}?state=opened", author)
-
- 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 group confidential issues for assignee' do
- get v3_api("#{base_url}?state=opened", assignee)
-
- 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 group issues with confidential issues for project members' do
- get v3_api("#{base_url}?state=opened", user)
-
- 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 group confidential issues for admin' do
- get v3_api("#{base_url}?state=opened", 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 an array of labeled group issues' do
- get v3_api("#{base_url}?labels=#{group_label.title}", user)
-
- 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])
- end
-
- 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_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(0)
- end
-
- it 'returns an empty array if no group issue matches labels' do
- get v3_api("#{base_url}?labels=foo,bar", user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(0)
- end
-
- it 'returns an empty array if no issue matches milestone' do
- get v3_api("#{base_url}?milestone=#{group_empty_milestone.title}", user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(0)
- end
-
- it 'returns an empty array if milestone does not exist' do
- get v3_api("#{base_url}?milestone=foo", user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(0)
- end
-
- 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_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)
- end
-
- it 'returns an array of issues matching state in milestone' do
- get v3_api("#{base_url}?milestone=#{group_milestone.title}", user),
- '&state=closed'
-
- 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)
- end
-
- it 'returns an array of issues with no milestone' do
- get v3_api("#{base_url}?milestone=#{no_milestone_title}", user)
-
- 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)
- end
-
- it 'sorts by created_at descending by default' do
- get v3_api(base_url, user)
-
- response_dates = json_response.map { |issue| issue['created_at'] }
-
- 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
-
- it 'sorts ascending when requested' do
- get v3_api("#{base_url}?sort=asc", user)
-
- response_dates = json_response.map { |issue| issue['created_at'] }
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(response_dates).to eq(response_dates.sort)
- end
-
- it 'sorts by updated_at descending when requested' do
- get v3_api("#{base_url}?order_by=updated_at", user)
-
- response_dates = json_response.map { |issue| issue['updated_at'] }
-
- 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
-
- it 'sorts by updated_at ascending when requested' do
- get v3_api("#{base_url}?order_by=updated_at&sort=asc", user)
-
- response_dates = json_response.map { |issue| issue['updated_at'] }
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(response_dates).to eq(response_dates.sort)
- end
- end
-
- describe "GET /projects/:id/issues" do
- let(:base_url) { "/projects/#{project.id}" }
-
- it 'returns 404 when project does not exist' do
- get v3_api('/projects/1000/issues', non_member)
-
- expect(response).to have_gitlab_http_status(404)
- end
-
- it "returns 404 on private projects for other users" do
- private_project = create(:project, :private)
- create(:issue, project: private_project)
-
- get v3_api("/projects/#{private_project.id}/issues", non_member)
-
- expect(response).to have_gitlab_http_status(404)
- end
-
- it 'returns no issues when user has access to project but not issues' do
- restricted_project = create(:project, :public, issues_access_level: ProjectFeature::PRIVATE)
- create(:issue, project: restricted_project)
-
- get v3_api("/projects/#{restricted_project.id}/issues", non_member)
-
- expect(json_response).to eq([])
- end
-
- it 'returns project issues without confidential issues for non project members' do
- get v3_api("#{base_url}/issues", non_member)
-
- 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)
- end
-
- 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_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)
- end
-
- it 'returns project confidential issues for author' do
- get v3_api("#{base_url}/issues", author)
-
- 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)
- end
-
- it 'returns project confidential issues for assignee' do
- get v3_api("#{base_url}/issues", assignee)
-
- 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)
- end
-
- it 'returns project issues with confidential issues for project members' do
- get v3_api("#{base_url}/issues", user)
-
- 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)
- end
-
- it 'returns project confidential issues for admin' do
- get v3_api("#{base_url}/issues", admin)
-
- 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)
- end
-
- it 'returns an array of labeled project issues' do
- get v3_api("#{base_url}/issues?labels=#{label.title}", user)
-
- 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])
- end
-
- 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_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])
- end
-
- 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_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(0)
- end
-
- it 'returns an empty array if no issue matches milestone' do
- get v3_api("#{base_url}/issues?milestone=#{empty_milestone.title}", user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(0)
- end
-
- it 'returns an empty array if milestone does not exist' do
- get v3_api("#{base_url}/issues?milestone=foo", user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(0)
- end
-
- it 'returns an array of issues in given milestone' do
- get v3_api("#{base_url}/issues?milestone=#{milestone.title}", user)
-
- 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)
- expect(json_response.second['id']).to eq(closed_issue.id)
- end
-
- it 'returns an array of issues matching state in milestone' do
- get v3_api("#{base_url}/issues?milestone=#{milestone.title}", user),
- '&state=closed'
-
- 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)
- end
-
- 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_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)
- end
-
- it 'sorts by created_at descending by default' do
- get v3_api("#{base_url}/issues", user)
-
- response_dates = json_response.map { |issue| issue['created_at'] }
-
- 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
-
- it 'sorts ascending when requested' do
- get v3_api("#{base_url}/issues?sort=asc", user)
-
- response_dates = json_response.map { |issue| issue['created_at'] }
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(response_dates).to eq(response_dates.sort)
- end
-
- it 'sorts by updated_at descending when requested' do
- get v3_api("#{base_url}/issues?order_by=updated_at", user)
-
- response_dates = json_response.map { |issue| issue['updated_at'] }
-
- 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
-
- it 'sorts by updated_at ascending when requested' do
- get v3_api("#{base_url}/issues?order_by=updated_at&sort=asc", user)
-
- response_dates = json_response.map { |issue| issue['updated_at'] }
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(response_dates).to eq(response_dates.sort)
- end
- end
-
- describe "GET /projects/:id/issues/:issue_id" do
- it 'exposes known attributes' do
- get v3_api("/projects/#{project.id}/issues/#{issue.id}", user)
-
- 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)
- expect(json_response['title']).to eq(issue.title)
- expect(json_response['description']).to eq(issue.description)
- expect(json_response['state']).to eq(issue.state)
- expect(json_response['created_at']).to be_present
- expect(json_response['updated_at']).to be_present
- expect(json_response['labels']).to eq(issue.label_names)
- expect(json_response['milestone']).to be_a Hash
- expect(json_response['assignee']).to be_a Hash
- expect(json_response['author']).to be_a Hash
- expect(json_response['confidential']).to be_falsy
- end
-
- it "returns a project issue by id" do
- get v3_api("/projects/#{project.id}/issues/#{issue.id}", user)
-
- 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 a project issue by iid' do
- get v3_api("/projects/#{project.id}/issues?iid=#{issue.iid}", user)
-
- expect(response.status).to eq 200
- expect(json_response.length).to eq 1
- expect(json_response.first['title']).to eq issue.title
- expect(json_response.first['id']).to eq issue.id
- expect(json_response.first['iid']).to eq issue.iid
- end
-
- it 'returns an empty array for an unknown project issue iid' do
- get v3_api("/projects/#{project.id}/issues?iid=#{issue.iid + 10}", user)
-
- expect(response.status).to eq 200
- expect(json_response.length).to eq 0
- end
-
- it "returns 404 if issue id not found" do
- get v3_api("/projects/#{project.id}/issues/54321", user)
-
- 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_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_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_gitlab_http_status(200)
- expect(json_response['title']).to eq(confidential_issue.title)
- expect(json_response['iid']).to eq(confidential_issue.iid)
- end
-
- it "returns confidential issue for author" do
- get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", author)
-
- 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
-
- it "returns confidential issue for assignee" do
- get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", assignee)
-
- 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
-
- it "returns confidential issue for admin" do
- get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin)
-
- 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
- end
- end
-
- describe "POST /projects/:id/issues" do
- it 'creates a new project issue' do
- post v3_api("/projects/#{project.id}/issues", user),
- title: 'new issue', labels: 'label, label2', assignee_id: assignee.id
-
- 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))
- expect(json_response['confidential']).to be_falsy
- expect(json_response['assignee']['name']).to eq(assignee.name)
- end
-
- it 'creates a new confidential project issue' do
- post v3_api("/projects/#{project.id}/issues", user),
- title: 'new issue', confidential: true
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['title']).to eq('new issue')
- expect(json_response['confidential']).to be_truthy
- end
-
- it 'creates a new confidential project issue with a different param' do
- post v3_api("/projects/#{project.id}/issues", user),
- title: 'new issue', confidential: 'y'
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['title']).to eq('new issue')
- expect(json_response['confidential']).to be_truthy
- end
-
- it 'creates a public issue when confidential param is false' do
- post v3_api("/projects/#{project.id}/issues", user),
- title: 'new issue', confidential: false
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['title']).to eq('new issue')
- expect(json_response['confidential']).to be_falsy
- end
-
- it 'creates a public issue when confidential param is invalid' do
- post v3_api("/projects/#{project.id}/issues", user),
- title: 'new issue', confidential: 'foo'
-
- expect(response).to have_gitlab_http_status(400)
- expect(json_response['error']).to eq('confidential is invalid')
- end
-
- 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_gitlab_http_status(400)
- end
-
- it 'allows special label names' do
- post v3_api("/projects/#{project.id}/issues", user),
- title: 'new issue',
- labels: 'label, label?, label&foo, ?, &'
-
- expect(response.status).to eq(201)
- expect(json_response['labels']).to include 'label'
- expect(json_response['labels']).to include 'label?'
- expect(json_response['labels']).to include 'label&foo'
- expect(json_response['labels']).to include '?'
- expect(json_response['labels']).to include '&'
- end
-
- it 'returns 400 if title is too long' do
- post v3_api("/projects/#{project.id}/issues", user),
- title: 'g' * 256
-
- expect(response).to have_gitlab_http_status(400)
- expect(json_response['message']['title']).to eq([
- 'is too long (maximum is 255 characters)'
- ])
- end
-
- context 'resolving issues in a merge request' do
- set(:diff_note_on_merge_request) { create(:diff_note_on_merge_request) }
- let(:discussion) { diff_note_on_merge_request.to_discussion }
- let(:merge_request) { discussion.noteable }
- let(:project) { merge_request.source_project }
- before do
- project.add_master(user)
- post v3_api("/projects/#{project.id}/issues", user),
- title: 'New Issue',
- merge_request_for_resolving_discussions: merge_request.iid
- end
-
- it 'creates a new project issue' do
- expect(response).to have_gitlab_http_status(:created)
- end
-
- it 'resolves the discussions in a merge request' do
- discussion.first_note.reload
-
- expect(discussion.resolved?).to be(true)
- end
-
- it 'assigns a description to the issue mentioning the merge request' do
- expect(json_response['description']).to include(merge_request.to_reference)
- end
- end
-
- context 'with due date' do
- it 'creates a new project issue' do
- due_date = 2.weeks.from_now.strftime('%Y-%m-%d')
-
- post v3_api("/projects/#{project.id}/issues", user),
- title: 'new issue', due_date: due_date
-
- 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)
- end
- end
-
- context 'when an admin or owner makes the request' do
- it 'accepts the creation date to be set' do
- creation_time = 2.weeks.ago
- post v3_api("/projects/#{project.id}/issues", user),
- title: 'new issue', labels: 'label, label2', created_at: creation_time
-
- expect(response).to have_gitlab_http_status(201)
- expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time)
- end
- end
-
- context 'the user can only read the issue' do
- it 'cannot create new labels' do
- expect do
- post v3_api("/projects/#{project.id}/issues", non_member), title: 'new issue', labels: 'label, label2'
- end.not_to change { project.labels.count }
- end
- end
- end
-
- describe 'POST /projects/:id/issues with spam filtering' do
- before do
- allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
- allow_any_instance_of(AkismetService).to receive_messages(spam?: true)
- end
-
- let(:params) do
- {
- title: 'new issue',
- description: 'content here',
- labels: 'label, label2'
- }
- end
-
- 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_gitlab_http_status(400)
- expect(json_response['message']).to eq({ "error" => "Spam detected" })
-
- spam_logs = SpamLog.all
-
- expect(spam_logs.count).to eq(1)
- expect(spam_logs[0].title).to eq('new issue')
- expect(spam_logs[0].description).to eq('content here')
- expect(spam_logs[0].user).to eq(user)
- expect(spam_logs[0].noteable_type).to eq('Issue')
- end
- end
-
- describe "PUT /projects/:id/issues/:issue_id to update only title" do
- it "updates a project issue" do
- put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
- title: 'updated title'
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['title']).to eq('updated title')
- end
-
- it "returns 404 error if issue id not found" do
- put v3_api("/projects/#{project.id}/issues/44444", user),
- title: 'updated title'
-
- expect(response).to have_gitlab_http_status(404)
- end
-
- it 'allows special label names' do
- put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
- title: 'updated title',
- labels: 'label, label?, label&foo, ?, &'
-
- expect(response.status).to eq(200)
- expect(json_response['labels']).to include 'label'
- expect(json_response['labels']).to include 'label?'
- expect(json_response['labels']).to include 'label&foo'
- expect(json_response['labels']).to include '?'
- expect(json_response['labels']).to include '&'
- end
-
- context 'confidential issues' do
- it "returns 403 for non project members" do
- put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member),
- title: 'updated title'
-
- 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_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_gitlab_http_status(200)
- expect(json_response['title']).to eq('updated title')
- end
-
- it "updates a confidential issue for author" do
- put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", author),
- title: 'updated title'
-
- 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 v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin),
- title: 'updated title'
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['title']).to eq('updated title')
- end
-
- it 'sets an issue to confidential' do
- put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
- confidential: true
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['confidential']).to be_truthy
- end
-
- it 'makes a confidential issue public' do
- put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
- confidential: false
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['confidential']).to be_falsy
- end
-
- it 'does not update a confidential issue with wrong confidential flag' do
- put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
- confidential: 'foo'
-
- expect(response).to have_gitlab_http_status(400)
- expect(json_response['error']).to eq('confidential is invalid')
- end
- end
- end
-
- describe 'PUT /projects/:id/issues/:issue_id with spam filtering' do
- let(:params) do
- {
- title: 'updated title',
- description: 'content here',
- labels: 'label, label2'
- }
- end
-
- it "does not create a new project issue" do
- allow_any_instance_of(SpamService).to receive_messages(check_for_spam?: true)
- allow_any_instance_of(AkismetService).to receive_messages(spam?: true)
-
- put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), params
-
- expect(response).to have_gitlab_http_status(400)
- expect(json_response['message']).to eq({ "error" => "Spam detected" })
-
- spam_logs = SpamLog.all
- expect(spam_logs.count).to eq(1)
- expect(spam_logs[0].title).to eq('updated title')
- expect(spam_logs[0].description).to eq('content here')
- expect(spam_logs[0].user).to eq(user)
- expect(spam_logs[0].noteable_type).to eq('Issue')
- end
- end
-
- describe 'PUT /projects/:id/issues/:issue_id to update labels' do
- let!(:label) { create(:label, title: 'dummy', project: project) }
- let!(:label_link) { create(:label_link, label: label, target: issue) }
-
- it 'does not update labels if not present' do
- put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
- title: 'updated title'
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['labels']).to eq([label.title])
- end
-
- it 'removes all labels' do
- put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), labels: ''
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['labels']).to eq([])
- end
-
- it 'updates labels' do
- put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
- labels: 'foo,bar'
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['labels']).to include 'foo'
- expect(json_response['labels']).to include 'bar'
- end
-
- it 'allows special label names' do
- put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
- labels: 'label:foo, label-bar,label_bar,label/bar,label?bar,label&bar,?,&'
-
- expect(response.status).to eq(200)
- expect(json_response['labels']).to include 'label:foo'
- expect(json_response['labels']).to include 'label-bar'
- expect(json_response['labels']).to include 'label_bar'
- expect(json_response['labels']).to include 'label/bar'
- expect(json_response['labels']).to include 'label?bar'
- expect(json_response['labels']).to include 'label&bar'
- expect(json_response['labels']).to include '?'
- expect(json_response['labels']).to include '&'
- end
-
- it 'returns 400 if title is too long' do
- put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
- title: 'g' * 256
-
- expect(response).to have_gitlab_http_status(400)
- expect(json_response['message']['title']).to eq([
- 'is too long (maximum is 255 characters)'
- ])
- end
- end
-
- describe "PUT /projects/:id/issues/:issue_id to update state and label" do
- it "updates a project issue" do
- put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
- labels: 'label2', state_event: "close"
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['labels']).to include 'label2'
- expect(json_response['state']).to eq "closed"
- end
-
- it 'reopens a project isssue' do
- put v3_api("/projects/#{project.id}/issues/#{closed_issue.id}", user), state_event: 'reopen'
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['state']).to eq 'opened'
- end
-
- context 'when an admin or owner makes the request' do
- it 'accepts the update date to be set' do
- update_time = 2.weeks.ago
- put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
- labels: 'label3', state_event: 'close', updated_at: update_time
-
- 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
- end
- end
-
- describe 'PUT /projects/:id/issues/:issue_id to update due date' do
- it 'creates a new project issue' do
- due_date = 2.weeks.from_now.strftime('%Y-%m-%d')
-
- put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), due_date: due_date
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['due_date']).to eq(due_date)
- end
- end
-
- describe 'PUT /projects/:id/issues/:issue_id to update assignee' 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_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_gitlab_http_status(200)
- expect(json_response['assignee']['name']).to eq(user2.name)
- end
- end
-
- describe "DELETE /projects/:id/issues/:issue_id" 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_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_gitlab_http_status(403)
- end
-
- context "when the user is project owner" do
- set(:owner) { create(:user) }
- let(:project) { create(:project, namespace: owner.namespace) }
-
- it "deletes the issue if an admin requests it" do
- delete v3_api("/projects/#{project.id}/issues/#{issue.id}", owner)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['state']).to eq 'opened'
- end
- end
-
- context 'when issue does not exist' do
- it 'returns 404 when trying to move an issue' do
- delete v3_api("/projects/#{project.id}/issues/123", user)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
- end
-
- describe '/projects/:id/issues/:issue_id/move' do
- let!(:target_project) { create(:project, creator_id: user.id, namespace: user.namespace ) }
- let!(:target_project2) { create(:project, creator_id: non_member.id, namespace: non_member.namespace ) }
-
- it 'moves an issue' do
- post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user),
- to_project_id: target_project.id
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['project_id']).to eq(target_project.id)
- end
-
- context 'when source and target projects are the same' do
- it 'returns 400 when trying to move an issue' do
- post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user),
- to_project_id: project.id
-
- expect(response).to have_gitlab_http_status(400)
- expect(json_response['message']).to eq('Cannot move issue to project it originates from!')
- end
- end
-
- context 'when the user does not have the permission to move issues' do
- it 'returns 400 when trying to move an issue' do
- post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user),
- to_project_id: target_project2.id
-
- expect(response).to have_gitlab_http_status(400)
- expect(json_response['message']).to eq('Cannot move issue due to insufficient permissions!')
- end
- end
-
- it 'moves the issue to another namespace if I am admin' do
- post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", admin),
- to_project_id: target_project2.id
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['project_id']).to eq(target_project2.id)
- end
-
- context 'when issue does not exist' do
- it 'returns 404 when trying to move an issue' do
- post v3_api("/projects/#{project.id}/issues/123/move", user),
- to_project_id: target_project.id
-
- expect(response).to have_gitlab_http_status(404)
- expect(json_response['message']).to eq('404 Issue Not Found')
- end
- end
-
- context 'when source project does not exist' do
- it 'returns 404 when trying to move an issue' do
- post v3_api("/projects/0/issues/#{issue.id}/move", user),
- to_project_id: target_project.id
-
- expect(response).to have_gitlab_http_status(404)
- expect(json_response['message']).to eq('404 Project Not Found')
- end
- end
-
- context 'when target project does not exist' do
- it 'returns 404 when trying to move an issue' do
- post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user),
- to_project_id: 0
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
- end
-
- describe 'POST :id/issues/:issue_id/subscription' do
- it 'subscribes to an issue' do
- post v3_api("/projects/#{project.id}/issues/#{issue.id}/subscription", user2)
-
- 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_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_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_gitlab_http_status(404)
- end
- end
-
- describe 'DELETE :id/issues/:issue_id/subscription' do
- it 'unsubscribes from an issue' do
- delete v3_api("/projects/#{project.id}/issues/#{issue.id}/subscription", user)
-
- 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_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_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_gitlab_http_status(404)
- end
- end
-
- describe 'time tracking endpoints' do
- let(:issuable) { issue }
-
- include_examples 'V3 time tracking endpoints', 'issue'
- end
-end
diff --git a/spec/requests/api/v3/labels_spec.rb b/spec/requests/api/v3/labels_spec.rb
deleted file mode 100644
index cdab4d2bd73..00000000000
--- a/spec/requests/api/v3/labels_spec.rb
+++ /dev/null
@@ -1,169 +0,0 @@
-require 'spec_helper'
-
-describe API::V3::Labels do
- let(:user) { create(:user) }
- let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
- let!(:label1) { create(:label, title: 'label1', project: project) }
- let!(:priority_label) { create(:label, title: 'bug', project: project, priority: 3) }
-
- before do
- project.add_master(user)
- end
-
- describe 'GET /projects/:id/labels' do
- it 'returns all available labels to the project' do
- group = create(:group)
- group_label = create(:group_label, title: 'feature', group: group)
- project.update(group: group)
- create(:labeled_issue, project: project, labels: [group_label], author: user)
- create(:labeled_issue, project: project, labels: [label1], author: user, state: :closed)
- create(:labeled_merge_request, labels: [priority_label], author: user, source_project: project )
-
- expected_keys = %w(
- id name color description
- open_issues_count closed_issues_count open_merge_requests_count
- subscribed priority
- )
-
- get v3_api("/projects/#{project.id}/labels", user)
-
- 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
- expect(json_response.map { |l| l['name'] }).to match_array([group_label.name, priority_label.name, label1.name])
-
- label1_response = json_response.find { |l| l['name'] == label1.title }
- group_label_response = json_response.find { |l| l['name'] == group_label.title }
- priority_label_response = json_response.find { |l| l['name'] == priority_label.title }
-
- expect(label1_response['open_issues_count']).to eq(0)
- expect(label1_response['closed_issues_count']).to eq(1)
- expect(label1_response['open_merge_requests_count']).to eq(0)
- expect(label1_response['name']).to eq(label1.name)
- expect(label1_response['color']).to be_present
- expect(label1_response['description']).to be_nil
- expect(label1_response['priority']).to be_nil
- expect(label1_response['subscribed']).to be_falsey
-
- expect(group_label_response['open_issues_count']).to eq(1)
- expect(group_label_response['closed_issues_count']).to eq(0)
- expect(group_label_response['open_merge_requests_count']).to eq(0)
- expect(group_label_response['name']).to eq(group_label.name)
- expect(group_label_response['color']).to be_present
- expect(group_label_response['description']).to be_nil
- expect(group_label_response['priority']).to be_nil
- expect(group_label_response['subscribed']).to be_falsey
-
- expect(priority_label_response['open_issues_count']).to eq(0)
- expect(priority_label_response['closed_issues_count']).to eq(0)
- expect(priority_label_response['open_merge_requests_count']).to eq(1)
- expect(priority_label_response['name']).to eq(priority_label.name)
- expect(priority_label_response['color']).to be_present
- expect(priority_label_response['description']).to be_nil
- expect(priority_label_response['priority']).to eq(3)
- expect(priority_label_response['subscribed']).to be_falsey
- end
- end
-
- describe "POST /projects/:id/labels/:label_id/subscription" do
- context "when label_id is a label title" do
- it "subscribes to the label" do
- post v3_api("/projects/#{project.id}/labels/#{label1.title}/subscription", user)
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response["name"]).to eq(label1.title)
- expect(json_response["subscribed"]).to be_truthy
- end
- end
-
- context "when label_id is a label ID" do
- it "subscribes to the label" do
- post v3_api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response["name"]).to eq(label1.title)
- expect(json_response["subscribed"]).to be_truthy
- end
- end
-
- context "when user is already subscribed to label" do
- before { label1.subscribe(user, project) }
-
- it "returns 304" do
- post v3_api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
-
- expect(response).to have_gitlab_http_status(304)
- end
- end
-
- context "when label ID is not found" do
- it "returns 404 error" do
- post v3_api("/projects/#{project.id}/labels/1234/subscription", user)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
- end
-
- describe "DELETE /projects/:id/labels/:label_id/subscription" do
- before { label1.subscribe(user, project) }
-
- context "when label_id is a label title" do
- it "unsubscribes from the label" do
- delete v3_api("/projects/#{project.id}/labels/#{label1.title}/subscription", user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response["name"]).to eq(label1.title)
- expect(json_response["subscribed"]).to be_falsey
- end
- end
-
- context "when label_id is a label ID" do
- it "unsubscribes from the label" do
- delete v3_api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response["name"]).to eq(label1.title)
- expect(json_response["subscribed"]).to be_falsey
- end
- end
-
- context "when user is already unsubscribed from label" do
- before { label1.unsubscribe(user, project) }
-
- it "returns 304" do
- delete v3_api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
-
- expect(response).to have_gitlab_http_status(304)
- end
- end
-
- context "when label ID is not found" do
- it "returns 404 error" do
- delete v3_api("/projects/#{project.id}/labels/1234/subscription", user)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
- end
-
- describe 'DELETE /projects/:id/labels' do
- it 'returns 200 for existing label' do
- delete v3_api("/projects/#{project.id}/labels", user), name: 'label1'
-
- 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_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_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
deleted file mode 100644
index de4339ecb8b..00000000000
--- a/spec/requests/api/v3/members_spec.rb
+++ /dev/null
@@ -1,350 +0,0 @@
-require 'spec_helper'
-
-describe API::V3::Members do
- let(:master) { create(:user, username: 'master_user') }
- let(:developer) { create(:user) }
- let(:access_requester) { create(:user) }
- let(:stranger) { create(:user) }
-
- let(:project) do
- create(:project, :public, :access_requestable, creator_id: master.id, namespace: master.namespace) do |project|
- project.add_developer(developer)
- project.add_master(master)
- project.request_access(access_requester)
- end
- end
-
- let!(:group) do
- create(:group, :public, :access_requestable) do |group|
- group.add_developer(developer)
- group.add_owner(master)
- group.request_access(access_requester)
- end
- end
-
- shared_examples 'GET /:sources/:id/members' do |source_type|
- context "with :sources == #{source_type.pluralize}" do
- it_behaves_like 'a 404 response when source is private' do
- let(:route) { get v3_api("/#{source_type.pluralize}/#{source.id}/members", stranger) }
- end
-
- %i[master developer access_requester stranger].each do |type|
- context "when authenticated as a #{type}" do
- it 'returns 200' do
- user = public_send(type)
- get v3_api("/#{source_type.pluralize}/#{source.id}/members", user)
-
- 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
- end
- end
-
- it 'does not return invitees' do
- create(:"#{source_type}_member", invite_token: '123', invite_email: 'test@abc.com', source: source, user: nil)
-
- get v3_api("/#{source_type.pluralize}/#{source.id}/members", developer)
-
- 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
-
- it 'finds members with query string' do
- get v3_api("/#{source_type.pluralize}/#{source.id}/members", developer), query: master.username
-
- 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
-
- it 'finds all members with no query specified' do
- get v3_api("/#{source_type.pluralize}/#{source.id}/members", developer), query: ''
-
- 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(2)
- expect(json_response.map { |u| u['id'] }).to match_array [master.id, developer.id]
- end
- end
- end
-
- shared_examples 'GET /:sources/:id/members/:user_id' do |source_type|
- context "with :sources == #{source_type.pluralize}" do
- it_behaves_like 'a 404 response when source is private' do
- let(:route) { get v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger) }
- end
-
- context 'when authenticated as a non-member' do
- %i[access_requester stranger].each do |type|
- context "as a #{type}" do
- it 'returns 200' do
- user = public_send(type)
- get v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user)
-
- 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)
- expect(json_response['username']).to eq(developer.username)
- expect(json_response['state']).to eq(developer.state)
- expect(json_response['avatar_url']).to eq(developer.avatar_url)
- expect(json_response['web_url']).to eq(Gitlab::Routing.url_helpers.user_url(developer))
-
- # Member attributes
- expect(json_response['access_level']).to eq(Member::DEVELOPER)
- end
- end
- end
- end
- end
- end
-
- shared_examples 'POST /:sources/:id/members' do |source_type|
- context "with :sources == #{source_type.pluralize}" do
- it_behaves_like 'a 404 response when source is private' do
- let(:route) do
- post v3_api("/#{source_type.pluralize}/#{source.id}/members", stranger),
- user_id: access_requester.id, access_level: Member::MASTER
- end
- end
-
- context 'when authenticated as a non-member or member with insufficient rights' do
- %i[access_requester stranger developer].each do |type|
- context "as a #{type}" do
- it 'returns 403' do
- user = public_send(type)
- post v3_api("/#{source_type.pluralize}/#{source.id}/members", user),
- user_id: access_requester.id, access_level: Member::MASTER
-
- expect(response).to have_gitlab_http_status(403)
- end
- end
- end
- end
-
- context 'when authenticated as a master/owner' do
- context 'and new member is already a requester' do
- it 'transforms the requester into a proper member' do
- expect do
- post v3_api("/#{source_type.pluralize}/#{source.id}/members", master),
- user_id: access_requester.id, access_level: Member::MASTER
-
- 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)
- expect(json_response['access_level']).to eq(Member::MASTER)
- end
- end
-
- it 'creates a new member' do
- expect 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_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)
- expect(json_response['expires_at']).to eq('2016-08-05')
- end
- end
-
- it "returns #{source_type == 'project' ? 201 : 409} if member already exists" do
- post v3_api("/#{source_type.pluralize}/#{source.id}/members", master),
- user_id: master.id, access_level: Member::MASTER
-
- 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_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_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_gitlab_http_status(422)
- end
- end
- end
-
- shared_examples 'PUT /:sources/:id/members/:user_id' do |source_type|
- context "with :sources == #{source_type.pluralize}" do
- it_behaves_like 'a 404 response when source is private' do
- let(:route) do
- put v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger),
- access_level: Member::MASTER
- end
- end
-
- context 'when authenticated as a non-member or member with insufficient rights' do
- %i[access_requester stranger developer].each do |type|
- context "as a #{type}" do
- it 'returns 403' do
- user = public_send(type)
- put v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user),
- access_level: Member::MASTER
-
- expect(response).to have_gitlab_http_status(403)
- end
- end
- end
- end
-
- context 'when authenticated as a master/owner' do
- it 'updates the member' 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_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')
- end
- end
-
- it 'returns 409 if member does not exist' do
- put v3_api("/#{source_type.pluralize}/#{source.id}/members/123", master),
- access_level: Member::MASTER
-
- 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_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_gitlab_http_status(422)
- end
- end
- end
-
- shared_examples 'DELETE /:sources/:id/members/:user_id' do |source_type|
- context "with :sources == #{source_type.pluralize}" do
- it_behaves_like 'a 404 response when source is private' do
- let(:route) { delete v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger) }
- end
-
- context 'when authenticated as a non-member or member with insufficient rights' do
- %i[access_requester stranger].each do |type|
- context "as a #{type}" do
- it 'returns 403' do
- user = public_send(type)
- delete v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user)
-
- expect(response).to have_gitlab_http_status(403)
- end
- end
- end
- end
-
- context 'when authenticated as a member and deleting themself' do
- it 'deletes the member' do
- expect do
- delete v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", developer)
-
- expect(response).to have_gitlab_http_status(200)
- end.to change { source.members.count }.by(-1)
- end
- end
-
- context 'when authenticated as a master/owner' do
- context 'and member is a requester' do
- it "returns #{source_type == 'project' ? 200 : 404}" do
- expect do
- delete v3_api("/#{source_type.pluralize}/#{source.id}/members/#{access_requester.id}", master)
-
- expect(response).to have_gitlab_http_status(source_type == 'project' ? 200 : 404)
- end.not_to change { source.requesters.count }
- end
- end
-
- it 'deletes the member' do
- expect do
- delete v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master)
-
- expect(response).to have_gitlab_http_status(200)
- end.to change { source.members.count }.by(-1)
- end
- end
-
- 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_gitlab_http_status(source_type == 'project' ? 200 : 404)
- end
- end
- end
-
- it_behaves_like 'GET /:sources/:id/members', 'project' do
- let(:source) { project }
- end
-
- it_behaves_like 'GET /:sources/:id/members', 'group' do
- let(:source) { group }
- end
-
- it_behaves_like 'GET /:sources/:id/members/:user_id', 'project' do
- let(:source) { project }
- end
-
- it_behaves_like 'GET /:sources/:id/members/:user_id', 'group' do
- let(:source) { group }
- end
-
- it_behaves_like 'POST /:sources/:id/members', 'project' do
- let(:source) { project }
- end
-
- it_behaves_like 'POST /:sources/:id/members', 'group' do
- let(:source) { group }
- end
-
- it_behaves_like 'PUT /:sources/:id/members/:user_id', 'project' do
- let(:source) { project }
- end
-
- it_behaves_like 'PUT /:sources/:id/members/:user_id', 'group' do
- let(:source) { group }
- end
-
- it_behaves_like 'DELETE /:sources/:id/members/:user_id', 'project' do
- let(:source) { project }
- end
-
- it_behaves_like 'DELETE /:sources/:id/members/:user_id', 'group' do
- let(:source) { group }
- end
-
- context 'Adding owner to project' do
- it 'returns 403' do
- expect do
- post v3_api("/projects/#{project.id}/members", master),
- user_id: stranger.id, access_level: Member::OWNER
-
- expect(response).to have_gitlab_http_status(422)
- end.to change { project.members.count }.by(0)
- end
- end
-end
diff --git a/spec/requests/api/v3/merge_request_diffs_spec.rb b/spec/requests/api/v3/merge_request_diffs_spec.rb
deleted file mode 100644
index 547c066fadc..00000000000
--- a/spec/requests/api/v3/merge_request_diffs_spec.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-require "spec_helper"
-
-describe API::V3::MergeRequestDiffs, 'MergeRequestDiffs' do
- let!(:user) { create(:user) }
- let!(:merge_request) { create(:merge_request, importing: true) }
- let!(:project) { merge_request.target_project }
-
- before do
- merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9')
- merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e')
- project.add_master(user)
- end
-
- describe 'GET /projects/:id/merge_requests/:merge_request_id/versions' do
- it 'returns 200 for a valid merge request' do
- get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions", user)
- merge_request_diff = merge_request.merge_request_diffs.last
-
- expect(response.status).to eq 200
- expect(json_response.size).to eq(merge_request.merge_request_diffs.size)
- expect(json_response.first['id']).to eq(merge_request_diff.id)
- expect(json_response.first['head_commit_sha']).to eq(merge_request_diff.head_commit_sha)
- end
-
- 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_gitlab_http_status(404)
- end
- end
-
- describe 'GET /projects/:id/merge_requests/:merge_request_id/versions/:version_id' do
- it 'returns a 200 for a valid merge request' do
- merge_request_diff = merge_request.merge_request_diffs.first
- get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/#{merge_request_diff.id}", user)
-
- expect(response.status).to eq 200
- expect(json_response['id']).to eq(merge_request_diff.id)
- expect(json_response['head_commit_sha']).to eq(merge_request_diff.head_commit_sha)
- expect(json_response['diffs'].size).to eq(merge_request_diff.diffs.size)
- end
-
- 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_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
deleted file mode 100644
index be70cb24dce..00000000000
--- a/spec/requests/api/v3/merge_requests_spec.rb
+++ /dev/null
@@ -1,749 +0,0 @@
-require "spec_helper"
-
-describe API::MergeRequests do
- include ProjectForksHelper
-
- let(:base_time) { Time.now }
- let(:user) { create(:user) }
- let(:admin) { create(:user, :admin) }
- let(:non_member) { create(:user) }
- let!(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace) }
- let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, title: "Test", created_at: base_time) }
- let!(:merge_request_closed) { create(:merge_request, state: "closed", author: user, assignee: user, source_project: project, title: "Closed test", created_at: base_time + 1.second) }
- let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, title: "Merged test", created_at: base_time + 2.seconds, merge_commit_sha: '9999999999999999999999999999999999999999') }
- let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
-
- before do
- project.add_reporter(user)
- end
-
- describe "GET /projects/:id/merge_requests" do
- context "when unauthenticated" do
- it "returns authentication error" do
- get v3_api("/projects/#{project.id}/merge_requests")
- 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_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)
- expect(json_response.last).to have_key('web_url')
- expect(json_response.last['sha']).to eq(merge_request.diff_head_sha)
- expect(json_response.last['merge_commit_sha']).to be_nil
- expect(json_response.last['merge_commit_sha']).to eq(merge_request.merge_commit_sha)
- expect(json_response.first['title']).to eq(merge_request_merged.title)
- expect(json_response.first['sha']).to eq(merge_request_merged.diff_head_sha)
- expect(json_response.first['merge_commit_sha']).not_to be_nil
- expect(json_response.first['merge_commit_sha']).to eq(merge_request_merged.merge_commit_sha)
- end
-
- it "returns an array of all merge_requests" do
- get v3_api("/projects/#{project.id}/merge_requests?state", user)
- 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)
- end
-
- it "returns an array of open merge_requests" do
- get v3_api("/projects/#{project.id}/merge_requests?state=opened", user)
- 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)
- end
-
- it "returns an array of closed merge_requests" do
- get v3_api("/projects/#{project.id}/merge_requests?state=closed", user)
- 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)
- end
-
- it "returns an array of merged merge_requests" do
- get v3_api("/projects/#{project.id}/merge_requests?state=merged", user)
- 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)
- end
-
- it 'matches V3 response schema' do
- get v3_api("/projects/#{project.id}/merge_requests", user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to match_response_schema('public_api/v3/merge_requests')
- end
-
- context "with ordering" do
- before do
- @mr_later = mr_with_later_created_and_updated_at_time
- @mr_earlier = mr_with_earlier_created_and_updated_at_time
- end
-
- 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_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'] }
- expect(response_dates).to eq(response_dates.sort)
- end
-
- 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_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'] }
- expect(response_dates).to eq(response_dates.sort.reverse)
- end
-
- 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_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'] }
- expect(response_dates).to eq(response_dates.sort.reverse)
- end
-
- 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_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'] }
- expect(response_dates).to eq(response_dates.sort)
- end
- end
- end
- end
-
- describe "GET /projects/:id/merge_requests/:merge_request_id" do
- it 'exposes known attributes' do
- get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user)
-
- 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)
- expect(json_response['title']).to eq(merge_request.title)
- expect(json_response['description']).to eq(merge_request.description)
- expect(json_response['state']).to eq(merge_request.state)
- expect(json_response['created_at']).to be_present
- expect(json_response['updated_at']).to be_present
- expect(json_response['labels']).to eq(merge_request.label_names)
- expect(json_response['milestone']).to be_nil
- expect(json_response['assignee']).to be_a Hash
- expect(json_response['author']).to be_a Hash
- expect(json_response['target_branch']).to eq(merge_request.target_branch)
- expect(json_response['source_branch']).to eq(merge_request.source_branch)
- expect(json_response['upvotes']).to eq(0)
- expect(json_response['downvotes']).to eq(0)
- expect(json_response['source_project_id']).to eq(merge_request.source_project.id)
- expect(json_response['target_project_id']).to eq(merge_request.target_project.id)
- expect(json_response['work_in_progress']).to be_falsy
- expect(json_response['merge_when_build_succeeds']).to be_falsy
- 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 v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", 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
- end
-
- it 'returns merge_request by iid' do
- url = "/projects/#{project.id}/merge_requests?iid=#{merge_request.iid}"
- get v3_api(url, user)
- expect(response.status).to eq 200
- expect(json_response.first['title']).to eq merge_request.title
- expect(json_response.first['id']).to eq merge_request.id
- end
-
- 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_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
- expect(json_response.first['id']).to eq merge_request_closed.id
- end
-
- 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_gitlab_http_status(404)
- end
-
- 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
- get v3_api("/projects/#{project.id}/merge_requests/#{merge_request_wip.id}", user)
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['work_in_progress']).to eq(true)
- end
- end
- end
-
- describe 'GET /projects/:id/merge_requests/:merge_request_id/commits' do
- it 'returns a 200 when merge request is valid' do
- get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/commits", user)
- commit = merge_request.commits.first
-
- expect(response.status).to eq 200
- expect(json_response.size).to eq(merge_request.commits.size)
- expect(json_response.first['id']).to eq(commit.id)
- expect(json_response.first['title']).to eq(commit.title)
- end
-
- 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_gitlab_http_status(404)
- end
- end
-
- describe 'GET /projects/:id/merge_requests/:merge_request_id/changes' do
- it 'returns the change information of the merge_request' do
- get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/changes", user)
- expect(response.status).to eq 200
- expect(json_response['changes'].size).to eq(merge_request.diffs.size)
- end
-
- 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_gitlab_http_status(404)
- end
- end
-
- describe "POST /projects/:id/merge_requests" do
- context 'between branches projects' do
- it "returns merge_request" do
- post v3_api("/projects/#{project.id}/merge_requests", user),
- title: 'Test merge_request',
- source_branch: 'feature_conflict',
- target_branch: 'master',
- author: user,
- labels: 'label, label2',
- milestone_id: milestone.id,
- remove_source_branch: true
-
- 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)
- expect(json_response['force_remove_source_branch']).to be_truthy
- end
-
- 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_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_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_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_gitlab_http_status(400)
- end
-
- it 'allows special label names' do
- post v3_api("/projects/#{project.id}/merge_requests", user),
- title: 'Test merge_request',
- source_branch: 'markdown',
- target_branch: 'master',
- author: user,
- labels: 'label, label?, label&foo, ?, &'
- expect(response.status).to eq(201)
- expect(json_response['labels']).to include 'label'
- expect(json_response['labels']).to include 'label?'
- expect(json_response['labels']).to include 'label&foo'
- expect(json_response['labels']).to include '?'
- expect(json_response['labels']).to include '&'
- end
-
- context 'with existing MR' do
- before do
- post v3_api("/projects/#{project.id}/merge_requests", user),
- title: 'Test merge_request',
- source_branch: 'feature_conflict',
- target_branch: 'master',
- author: user
- @mr = MergeRequest.all.last
- end
-
- it 'returns 409 when MR already exists for source/target' do
- expect do
- post v3_api("/projects/#{project.id}/merge_requests", user),
- title: 'New test merge_request',
- source_branch: 'feature_conflict',
- target_branch: 'master',
- author: user
- end.to change { MergeRequest.count }.by(0)
- 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, repository: true) }
- let!(:unrelated_project) { create(:project, namespace: create(:user).namespace, creator_id: user2.id) }
-
- before do
- forked_project.add_reporter(user2)
- end
-
- it "returns merge_request" do
- post v3_api("/projects/#{forked_project.id}/merge_requests", user2),
- title: 'Test merge_request', source_branch: "feature_conflict", target_branch: "master",
- author: user2, target_project_id: project.id, description: 'Test description for Test merge_request'
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['title']).to eq('Test merge_request')
- expect(json_response['description']).to eq('Test description for Test merge_request')
- end
-
- it "does not return 422 when source_branch equals target_branch" do
- expect(project.id).not_to eq(forked_project.id)
- expect(forked_project.forked?).to be_truthy
- expect(forked_project.forked_from_project).to eq(project)
- post v3_api("/projects/#{forked_project.id}/merge_requests", user2),
- title: 'Test merge_request', source_branch: "master", target_branch: "master", author: user2, target_project_id: project.id
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['title']).to eq('Test merge_request')
- end
-
- it "returns 403 when target project has disabled merge requests" do
- project.project_feature.update(merge_requests_access_level: 0)
-
- post v3_api("/projects/#{forked_project.id}/merge_requests", user2),
- title: 'Test',
- target_branch: "master",
- source_branch: 'markdown',
- author: user2,
- target_project_id: project.id
-
- expect(response).to have_gitlab_http_status(403)
- end
-
- it "returns 400 when source_branch is missing" do
- post v3_api("/projects/#{forked_project.id}/merge_requests", user2),
- title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id
- expect(response).to have_gitlab_http_status(400)
- end
-
- it "returns 400 when target_branch is missing" do
- post v3_api("/projects/#{forked_project.id}/merge_requests", user2),
- title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id
- expect(response).to have_gitlab_http_status(400)
- end
-
- it "returns 400 when title is missing" do
- post v3_api("/projects/#{forked_project.id}/merge_requests", user2),
- target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: project.id
- expect(response).to have_gitlab_http_status(400)
- end
-
- context 'when target_branch and target_project_id is specified' do
- let(:params) do
- { title: 'Test merge_request',
- target_branch: 'master',
- source_branch: 'markdown',
- author: user2,
- target_project_id: unrelated_project.id }
- end
-
- it 'returns 422 if targeting a different fork' do
- unrelated_project.add_developer(user2)
-
- post v3_api("/projects/#{forked_project.id}/merge_requests", user2), params
-
- expect(response).to have_gitlab_http_status(422)
- end
-
- it 'returns 403 if targeting a different fork which user can not access' do
- post v3_api("/projects/#{forked_project.id}/merge_requests", user2), params
-
- expect(response).to have_gitlab_http_status(403)
- end
- end
-
- it "returns 201 when target_branch is specified and for the same project" do
- post v3_api("/projects/#{forked_project.id}/merge_requests", user2),
- title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: forked_project.id
- expect(response).to have_gitlab_http_status(201)
- end
- end
- end
-
- describe "DELETE /projects/:id/merge_requests/:merge_request_id" do
- context "when the user is developer" do
- let(:developer) { create(:user) }
-
- before do
- project.add_developer(developer)
- end
-
- 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_gitlab_http_status(403)
- end
- end
-
- context "when the user is project owner" 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_gitlab_http_status(200)
- end
- end
- end
-
- describe "PUT /projects/:id/merge_requests/:merge_request_id/merge" do
- let(:pipeline) { create(:ci_pipeline_without_jobs) }
-
- 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_gitlab_http_status(200)
- end
-
- it "returns 406 if branch can't be merged" do
- allow_any_instance_of(MergeRequest)
- .to receive(:can_be_merged?).and_return(false)
-
- put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
-
- 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_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_gitlab_http_status(405)
- expect(json_response['message']).to eq('405 Method Not Allowed')
- end
-
- it 'returns 405 if the build failed for a merge request that requires success' do
- allow_any_instance_of(MergeRequest).to receive(:mergeable_ci_state?).and_return(false)
-
- put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
-
- expect(response).to have_gitlab_http_status(405)
- expect(json_response['message']).to eq('405 Method Not Allowed')
- end
-
- it "returns 401 if user has no permissions to merge" do
- user2 = create(:user)
- project.add_reporter(user2)
- put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user2)
- 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_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_gitlab_http_status(200)
- end
-
- it "enables merge when pipeline succeeds if the pipeline is active" do
- allow_any_instance_of(MergeRequest).to receive(:head_pipeline).and_return(pipeline)
- allow(pipeline).to receive(:active?).and_return(true)
-
- put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), merge_when_build_succeeds: true
-
- 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
- end
-
- describe "PUT /projects/:id/merge_requests/:merge_request_id" do
- context "to close a MR" 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_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_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_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_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_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_gitlab_http_status(200)
- expect(json_response['force_remove_source_branch']).to be_truthy
- end
-
- it 'allows special label names' do
- put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user),
- title: 'new issue',
- labels: 'label, label?, label&foo, ?, &'
-
- expect(response.status).to eq(200)
- expect(json_response['labels']).to include 'label'
- expect(json_response['labels']).to include 'label?'
- expect(json_response['labels']).to include 'label&foo'
- expect(json_response['labels']).to include '?'
- expect(json_response['labels']).to include '&'
- end
-
- it 'does not update state when title is empty' 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_gitlab_http_status(400)
- expect(merge_request.state).to eq('opened')
- end
-
- it 'does not update state when target_branch is empty' 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_gitlab_http_status(400)
- expect(merge_request.state).to eq('opened')
- end
- end
-
- describe "POST /projects/:id/merge_requests/:merge_request_id/comments" do
- it "returns comment" do
- original_count = merge_request.notes.size
-
- post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user), note: "My comment"
-
- 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)
- expect(merge_request.reload.notes.size).to eq(original_count + 1)
- end
-
- 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_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_gitlab_http_status(404)
- end
- end
-
- describe "GET :id/merge_requests/:merge_request_id/comments" do
- let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") }
- let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") }
-
- 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_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")
- expect(json_response.first['author']['id']).to eq(user.id)
- expect(json_response.last['note']).to eq("another comment on a MR")
- end
-
- 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_gitlab_http_status(404)
- end
- end
-
- describe 'GET :id/merge_requests/:merge_request_id/closes_issues' do
- it 'returns the issue that will be closed on merge' do
- issue = create(:issue, project: project)
- mr = merge_request.tap do |mr|
- mr.update_attribute(:description, "Closes #{issue.to_reference(mr.project)}")
- end
-
- get v3_api("/projects/#{project.id}/merge_requests/#{mr.id}/closes_issues", user)
- 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)
- end
-
- 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_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(0)
- end
-
- it 'handles external issues' do
- jira_project = create(:jira_project, :public, :repository, name: 'JIR_EXT1')
- issue = ExternalIssue.new("#{jira_project.name}-123", jira_project)
- merge_request = create(:merge_request, :simple, author: user, assignee: user, source_project: jira_project)
- merge_request.update_attribute(:description, "Closes #{issue.to_reference(jira_project)}")
-
- get v3_api("/projects/#{jira_project.id}/merge_requests/#{merge_request.id}/closes_issues", user)
-
- 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)
- expect(json_response.first['id']).to eq(issue.id)
- end
-
- it 'returns 403 if the user has no access to the merge request' do
- project = create(:project, :private, :repository)
- merge_request = create(:merge_request, :simple, source_project: project)
- guest = create(:user)
- project.add_guest(guest)
-
- get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", guest)
-
- expect(response).to have_gitlab_http_status(403)
- end
- end
-
- describe 'POST :id/merge_requests/:merge_request_id/subscription' 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_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_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_gitlab_http_status(404)
- end
-
- it 'returns 403 if user has no access to read code' do
- guest = create(:user)
- project.add_guest(guest)
-
- post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest)
-
- expect(response).to have_gitlab_http_status(403)
- end
- end
-
- describe 'DELETE :id/merge_requests/:merge_request_id/subscription' 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_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_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_gitlab_http_status(404)
- end
-
- it 'returns 403 if user has no access to read code' do
- guest = create(:user)
- project.add_guest(guest)
-
- delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest)
-
- expect(response).to have_gitlab_http_status(403)
- end
- end
-
- describe 'Time tracking' do
- let(:issuable) { merge_request }
-
- include_examples 'V3 time tracking endpoints', 'merge_request'
- end
-
- def mr_with_later_created_and_updated_at_time
- merge_request
- merge_request.created_at += 1.hour
- merge_request.updated_at += 30.minutes
- merge_request.save
- merge_request
- end
-
- def mr_with_earlier_created_and_updated_at_time
- merge_request_closed
- merge_request_closed.created_at -= 1.hour
- merge_request_closed.updated_at -= 30.minutes
- merge_request_closed.save
- merge_request_closed
- end
-end
diff --git a/spec/requests/api/v3/milestones_spec.rb b/spec/requests/api/v3/milestones_spec.rb
deleted file mode 100644
index 6021600e09c..00000000000
--- a/spec/requests/api/v3/milestones_spec.rb
+++ /dev/null
@@ -1,238 +0,0 @@
-require 'spec_helper'
-
-describe API::V3::Milestones do
- let(:user) { create(:user) }
- let!(:project) { create(:project, namespace: user.namespace ) }
- let!(:closed_milestone) { create(:closed_milestone, project: project) }
- let!(:milestone) { create(:milestone, project: project) }
-
- before { project.add_developer(user) }
-
- describe 'GET /projects/:id/milestones' do
- it 'returns project milestones' do
- get v3_api("/projects/#{project.id}/milestones", user)
-
- 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
-
- it 'returns a 401 error if user not authenticated' do
- get v3_api("/projects/#{project.id}/milestones")
-
- 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_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)
- end
-
- it 'returns an array of closed milestones' do
- get v3_api("/projects/#{project.id}/milestones?state=closed", user)
-
- 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)
- end
- end
-
- describe 'GET /projects/:id/milestones/:milestone_id' do
- it 'returns a project milestone by id' do
- get v3_api("/projects/#{project.id}/milestones/#{milestone.id}", user)
-
- 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
-
- it 'returns a project milestone by iid' do
- get v3_api("/projects/#{project.id}/milestones?iid=#{closed_milestone.iid}", user)
-
- expect(response.status).to eq 200
- expect(json_response.size).to eq(1)
- expect(json_response.first['title']).to eq closed_milestone.title
- expect(json_response.first['id']).to eq closed_milestone.id
- end
-
- 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_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
- end
-
- it 'returns 401 error if user not authenticated' do
- get v3_api("/projects/#{project.id}/milestones/#{milestone.id}")
-
- 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_gitlab_http_status(404)
- end
- end
-
- describe 'POST /projects/:id/milestones' do
- it 'creates a new project milestone' do
- post v3_api("/projects/#{project.id}/milestones", user), title: 'new milestone'
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['title']).to eq('new milestone')
- expect(json_response['description']).to be_nil
- end
-
- it 'creates a new project milestone with description and dates' 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_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')
- end
-
- it 'returns a 400 error if title is missing' do
- post v3_api("/projects/#{project.id}/milestones", user)
-
- 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_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_gitlab_http_status(201)
- expect(json_response['title']).to eq('foo & bar 1.1 -> 2.2')
- expect(json_response['description']).to be_nil
- end
- end
-
- describe 'PUT /projects/:id/milestones/:milestone_id' do
- it 'updates a project milestone' do
- put v3_api("/projects/#{project.id}/milestones/#{milestone.id}", user),
- title: 'updated title'
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['title']).to eq('updated title')
- end
-
- it 'removes a due date if nil is passed' do
- milestone.update!(due_date: "2016-08-05")
-
- put v3_api("/projects/#{project.id}/milestones/#{milestone.id}", user), due_date: nil
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['due_date']).to be_nil
- end
-
- it 'returns a 404 error if milestone id not found' do
- put v3_api("/projects/#{project.id}/milestones/1234", user),
- title: 'updated title'
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
-
- describe 'PUT /projects/:id/milestones/:milestone_id to close milestone' do
- it 'updates a project milestone' do
- put v3_api("/projects/#{project.id}/milestones/#{milestone.id}", user),
- state_event: 'close'
- expect(response).to have_gitlab_http_status(200)
-
- expect(json_response['state']).to eq('closed')
- end
- end
-
- describe 'PUT /projects/:id/milestones/:milestone_id to test observer on close' do
- it 'creates an activity event when an milestone is closed' do
- expect(Event).to receive(:create!)
-
- put v3_api("/projects/#{project.id}/milestones/#{milestone.id}", user),
- state_event: 'close'
- end
- end
-
- describe 'GET /projects/:id/milestones/:milestone_id/issues' do
- before do
- milestone.issues << create(:issue, project: project)
- end
- it 'returns project issues for a particular milestone' do
- get v3_api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user)
-
- 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
-
- 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_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_gitlab_http_status(401)
- end
-
- describe 'confidential issues' do
- let(:public_project) { create(:project, :public) }
- let(:milestone) { create(:milestone, project: public_project) }
- let(:issue) { create(:issue, project: public_project) }
- let(:confidential_issue) { create(:issue, confidential: true, project: public_project) }
-
- before do
- public_project.add_developer(user)
- milestone.issues << issue << confidential_issue
- end
-
- it 'returns confidential issues to team members' do
- get v3_api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", user)
-
- 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)
- end
-
- it 'does not return confidential issues to team members with guest role' do
- member = create(:user)
- project.add_guest(member)
-
- get v3_api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", member)
-
- 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)
- end
-
- 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_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)
- end
- end
- end
-end
diff --git a/spec/requests/api/v3/notes_spec.rb b/spec/requests/api/v3/notes_spec.rb
deleted file mode 100644
index 5532795ab02..00000000000
--- a/spec/requests/api/v3/notes_spec.rb
+++ /dev/null
@@ -1,431 +0,0 @@
-require 'spec_helper'
-
-describe API::V3::Notes do
- let(:user) { create(:user) }
- let!(:project) { create(:project, :public, namespace: user.namespace) }
- let!(:issue) { create(:issue, project: project, author: user) }
- let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: user) }
- let!(:snippet) { create(:project_snippet, project: project, author: user) }
- let!(:issue_note) { create(:note, noteable: issue, project: project, author: user) }
- let!(:merge_request_note) { create(:note, noteable: merge_request, project: project, author: user) }
- let!(:snippet_note) { create(:note, noteable: snippet, project: project, author: user) }
-
- # For testing the cross-reference of a private issue in a public issue
- let(:private_user) { create(:user) }
- let(:private_project) do
- create(:project, namespace: private_user.namespace)
- .tap { |p| p.add_master(private_user) }
- end
- let(:private_issue) { create(:issue, project: private_project) }
-
- let(:ext_proj) { create(:project, :public) }
- let(:ext_issue) { create(:issue, project: ext_proj) }
-
- let!(:cross_reference_note) do
- create :note,
- noteable: ext_issue, project: ext_proj,
- note: "mentioned in issue #{private_issue.to_reference(ext_proj)}",
- system: true
- end
-
- before { project.add_reporter(user) }
-
- describe "GET /projects/:id/noteable/:noteable_id/notes" do
- context "when noteable is an Issue" do
- it "returns an array of issue notes" do
- get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", 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.first['body']).to eq(issue_note.note)
- expect(json_response.first['upvote']).to be_falsey
- expect(json_response.first['downvote']).to be_falsey
- end
-
- 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_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_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response).to be_empty
- end
-
- context "and issue is confidential" do
- before { ext_issue.update_attributes(confidential: true) }
-
- it "returns 404" do
- get v3_api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
-
- context "and current user can view the note" 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_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)
- end
- end
- end
- end
-
- context "when noteable is a Snippet" do
- it "returns an array of snippet notes" do
- get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes", 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.first['body']).to eq(snippet_note.note)
- end
-
- 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_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_gitlab_http_status(404)
- end
- end
-
- context "when noteable is a Merge Request" 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_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)
- end
-
- 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_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_gitlab_http_status(404)
- end
- end
- end
-
- describe "GET /projects/:id/noteable/:noteable_id/notes/:note_id" do
- context "when noteable is an Issue" 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_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_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_gitlab_http_status(404)
- end
-
- context "when issue is confidential" do
- before { issue.update_attributes(confidential: true) }
-
- it "returns 404" do
- get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{issue_note.id}", private_user)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
-
- context "and current user can view the note" 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_gitlab_http_status(200)
- expect(json_response['body']).to eq(cross_reference_note.note)
- end
- end
- end
- end
-
- context "when noteable is a Snippet" 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_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_gitlab_http_status(404)
- end
- end
- end
-
- describe "POST /projects/:id/noteable/:noteable_id/notes" do
- context "when noteable is an Issue" 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_gitlab_http_status(201)
- expect(json_response['body']).to eq('hi!')
- expect(json_response['author']['username']).to eq(user.username)
- end
-
- 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_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_gitlab_http_status(401)
- end
-
- context 'when an admin or owner makes the request' do
- it 'accepts the creation date to be set' do
- creation_time = 2.weeks.ago
- post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user),
- body: 'hi!', created_at: creation_time
-
- 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)
- end
- end
-
- context 'when the user is posting an award emoji on an issue created by someone else' do
- let(:issue2) { create(:issue, project: project) }
-
- it 'creates a new issue note' do
- post v3_api("/projects/#{project.id}/issues/#{issue2.id}/notes", user), body: ':+1:'
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['body']).to eq(':+1:')
- end
- end
-
- context 'when the user is posting an award emoji on his/her own issue' 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_gitlab_http_status(201)
- expect(json_response['body']).to eq(':+1:')
- end
- end
- end
-
- context "when noteable is a Snippet" 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_gitlab_http_status(201)
- expect(json_response['body']).to eq('hi!')
- expect(json_response['author']['username']).to eq(user.username)
- end
-
- 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_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_gitlab_http_status(401)
- end
- end
-
- context 'when user does not have access to read the noteable' do
- it 'responds with 404' do
- project = create(:project, :private) { |p| p.add_guest(user) }
- issue = create(:issue, :confidential, project: project)
-
- post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user),
- body: 'Foo'
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
-
- context 'when user does not have access to create noteable' do
- let(:private_issue) { create(:issue, project: create(:project, :private)) }
-
- ##
- # We are posting to project user has access to, but we use issue id
- # from a different project, see #15577
- #
- before do
- post v3_api("/projects/#{project.id}/issues/#{private_issue.id}/notes", user),
- body: 'Hi!'
- end
-
- it 'responds with resource not found error' do
- expect(response.status).to eq 404
- end
-
- it 'does not create new note' do
- expect(private_issue.notes.reload).to be_empty
- end
- end
- end
-
- describe "POST /projects/:id/noteable/:noteable_id/notes to test observer on create" do
- it "creates an activity event when an issue note is created" do
- expect(Event).to receive(:create!)
-
- post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!'
- end
- end
-
- describe 'PUT /projects/:id/noteable/:noteable_id/notes/:note_id' do
- context 'when noteable is an Issue' do
- it 'returns modified note' do
- put v3_api("/projects/#{project.id}/issues/#{issue.id}/"\
- "notes/#{issue_note.id}", user), body: 'Hello!'
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['body']).to eq('Hello!')
- end
-
- it 'returns a 404 error when note id not found' do
- put v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user),
- body: 'Hello!'
-
- 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_gitlab_http_status(400)
- end
- end
-
- context 'when noteable is a Snippet' do
- it 'returns modified note' do
- put v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\
- "notes/#{snippet_note.id}", user), body: 'Hello!'
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['body']).to eq('Hello!')
- end
-
- it 'returns a 404 error when note id not found' do
- put v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\
- "notes/12345", user), body: "Hello!"
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
-
- context 'when noteable is a Merge Request' do
- it 'returns modified note' do
- put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\
- "notes/#{merge_request_note.id}", user), body: 'Hello!'
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['body']).to eq('Hello!')
- end
-
- it 'returns a 404 error when note id not found' do
- put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\
- "notes/12345", user), body: "Hello!"
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
- end
-
- describe 'DELETE /projects/:id/noteable/:noteable_id/notes/:note_id' do
- context 'when noteable is an Issue' do
- it 'deletes a note' do
- delete v3_api("/projects/#{project.id}/issues/#{issue.id}/"\
- "notes/#{issue_note.id}", user)
-
- 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_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_gitlab_http_status(404)
- end
- end
-
- context 'when noteable is a Snippet' do
- it 'deletes a note' do
- delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\
- "notes/#{snippet_note.id}", user)
-
- 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_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_gitlab_http_status(404)
- end
- end
-
- context 'when noteable is a Merge Request' do
- it 'deletes a note' do
- delete v3_api("/projects/#{project.id}/merge_requests/"\
- "#{merge_request.id}/notes/#{merge_request_note.id}", user)
-
- 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_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_gitlab_http_status(404)
- end
- end
- end
-end
diff --git a/spec/requests/api/v3/pipelines_spec.rb b/spec/requests/api/v3/pipelines_spec.rb
deleted file mode 100644
index ea943f22c41..00000000000
--- a/spec/requests/api/v3/pipelines_spec.rb
+++ /dev/null
@@ -1,201 +0,0 @@
-require 'spec_helper'
-
-describe API::V3::Pipelines do
- let(:user) { create(:user) }
- let(:non_member) { create(:user) }
- let(:project) { create(:project, :repository, creator: user) }
-
- let!(:pipeline) do
- create(:ci_empty_pipeline, project: project, sha: project.commit.id,
- ref: project.default_branch)
- end
-
- before { project.add_master(user) }
-
- shared_examples 'a paginated resources' do
- before do
- # Fires the request
- request
- end
-
- it 'has pagination headers' do
- expect(response).to include_pagination_headers
- end
- end
-
- describe 'GET /projects/:id/pipelines ' do
- it_behaves_like 'a paginated resources' do
- let(:request) { get v3_api("/projects/#{project.id}/pipelines", user) }
- end
-
- context 'authorized user' do
- it 'returns project pipelines' do
- get v3_api("/projects/#{project.id}/pipelines", user)
-
- 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
- expect(json_response.first.keys).to contain_exactly(*%w[id sha ref status before_sha tag yaml_errors user created_at updated_at started_at finished_at committed_at duration coverage])
- end
- end
-
- context 'unauthorized user' do
- it 'does not return project pipelines' do
- get v3_api("/projects/#{project.id}/pipelines", non_member)
-
- 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
- end
- end
-
- describe 'POST /projects/:id/pipeline ' do
- context 'authorized user' do
- context 'with gitlab-ci.yml' do
- before { stub_ci_pipeline_to_return_yaml_file }
-
- it 'creates and returns a new pipeline' do
- expect 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_gitlab_http_status(201)
- expect(json_response).to be_a Hash
- expect(json_response['sha']).to eq project.commit.id
- end
-
- it 'fails when using an invalid ref' do
- post v3_api("/projects/#{project.id}/pipeline", user), ref: 'invalid_ref'
-
- 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
- end
-
- context 'without gitlab-ci.yml' do
- it 'fails to create pipeline' do
- post v3_api("/projects/#{project.id}/pipeline", user), ref: project.default_branch
-
- 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
- end
- end
-
- context 'unauthorized user' do
- it 'does not create pipeline' do
- post v3_api("/projects/#{project.id}/pipeline", non_member), ref: project.default_branch
-
- 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
- end
- end
-
- describe 'GET /projects/:id/pipelines/:pipeline_id' do
- context 'authorized user' do
- it 'returns project pipelines' do
- get v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}", user)
-
- 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_gitlab_http_status(404)
- expect(json_response['message']).to eq '404 Not found'
- expect(json_response['id']).to be nil
- end
-
- context 'with coverage' do
- before do
- create(:ci_build, coverage: 30, pipeline: pipeline)
- end
-
- it 'exposes the coverage' do
- get v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}", user)
-
- expect(json_response["coverage"].to_i).to eq(30)
- end
- end
- end
-
- context 'unauthorized user' do
- it 'should not return a project pipeline' do
- get v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}", non_member)
-
- 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
- end
- end
-
- describe 'POST /projects/:id/pipelines/:pipeline_id/retry' do
- context 'authorized user' do
- let!(:pipeline) do
- create(:ci_pipeline, project: project, sha: project.commit.id,
- ref: project.default_branch)
- end
-
- let!(:build) { create(:ci_build, :failed, pipeline: pipeline) }
-
- it 'retries failed builds' do
- expect 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_gitlab_http_status(201)
- expect(build.reload.retried?).to be true
- end
- end
-
- context 'unauthorized user' 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_gitlab_http_status(404)
- expect(json_response['message']).to eq '404 Project Not Found'
- expect(json_response['id']).to be nil
- end
- end
- end
-
- describe 'POST /projects/:id/pipelines/:pipeline_id/cancel' do
- let!(:pipeline) do
- create(:ci_empty_pipeline, project: project, sha: project.commit.id,
- ref: project.default_branch)
- end
-
- let!(:build) { create(:ci_build, :running, pipeline: pipeline) }
-
- context 'authorized user' do
- it 'retries failed builds' do
- post v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['status']).to eq('canceled')
- end
- end
-
- context 'user without proper access rights' do
- let!(:reporter) { create(:user) }
-
- before { project.add_reporter(reporter) }
-
- it 'rejects the action' do
- post v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", reporter)
-
- expect(response).to have_gitlab_http_status(403)
- expect(pipeline.reload.status).to eq('pending')
- end
- end
- end
-end
diff --git a/spec/requests/api/v3/project_hooks_spec.rb b/spec/requests/api/v3/project_hooks_spec.rb
deleted file mode 100644
index 8f6a2330d25..00000000000
--- a/spec/requests/api/v3/project_hooks_spec.rb
+++ /dev/null
@@ -1,219 +0,0 @@
-require 'spec_helper'
-
-describe API::ProjectHooks, 'ProjectHooks' do
- let(:user) { create(:user) }
- let(:user3) { create(:user) }
- let!(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
- let!(:hook) do
- create(:project_hook,
- :all_events_enabled,
- project: project,
- url: 'http://example.com',
- enable_ssl_verification: true)
- end
-
- before do
- project.add_master(user)
- project.add_developer(user3)
- end
-
- describe "GET /projects/:id/hooks" do
- context "authorized user" do
- it "returns project hooks" do
- get v3_api("/projects/#{project.id}/hooks", user)
-
- 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")
- expect(json_response.first['issues_events']).to eq(true)
- expect(json_response.first['confidential_issues_events']).to eq(true)
- expect(json_response.first['push_events']).to eq(true)
- expect(json_response.first['merge_requests_events']).to eq(true)
- expect(json_response.first['tag_push_events']).to eq(true)
- expect(json_response.first['note_events']).to eq(true)
- expect(json_response.first['build_events']).to eq(true)
- expect(json_response.first['pipeline_events']).to eq(true)
- expect(json_response.first['wiki_page_events']).to eq(true)
- expect(json_response.first['enable_ssl_verification']).to eq(true)
- end
- end
-
- context "unauthorized user" do
- it "does not access project hooks" do
- get v3_api("/projects/#{project.id}/hooks", user3)
-
- expect(response).to have_gitlab_http_status(403)
- end
- end
- end
-
- describe "GET /projects/:id/hooks/:hook_id" 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_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['confidential_issues_events']).to eq(hook.confidential_issues_events)
- expect(json_response['push_events']).to eq(hook.push_events)
- expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events)
- expect(json_response['tag_push_events']).to eq(hook.tag_push_events)
- expect(json_response['note_events']).to eq(hook.note_events)
- expect(json_response['build_events']).to eq(hook.job_events)
- expect(json_response['pipeline_events']).to eq(hook.pipeline_events)
- expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events)
- expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification)
- 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_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_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_gitlab_http_status(404)
- end
- end
-
- describe "POST /projects/:id/hooks" do
- it "adds hook to project" do
- expect do
- post v3_api("/projects/#{project.id}/hooks", user),
- url: "http://example.com", issues_events: true, confidential_issues_events: true, wiki_page_events: true, build_events: true
- end.to change {project.hooks.count}.by(1)
-
- 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['confidential_issues_events']).to eq(true)
- expect(json_response['push_events']).to eq(true)
- expect(json_response['merge_requests_events']).to eq(false)
- expect(json_response['tag_push_events']).to eq(false)
- expect(json_response['note_events']).to eq(false)
- expect(json_response['build_events']).to eq(true)
- expect(json_response['pipeline_events']).to eq(false)
- expect(json_response['wiki_page_events']).to eq(true)
- expect(json_response['enable_ssl_verification']).to eq(true)
- expect(json_response).not_to include('token')
- end
-
- it "adds the token without including it in the response" do
- token = "secret token"
-
- expect 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_gitlab_http_status(201)
- expect(json_response["url"]).to eq("http://example.com")
- expect(json_response).not_to include("token")
-
- hook = project.hooks.find(json_response["id"])
-
- expect(hook.url).to eq("http://example.com")
- expect(hook.token).to eq(token)
- end
-
- it "returns a 400 error if url not given" do
- post v3_api("/projects/#{project.id}/hooks", user)
- 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_gitlab_http_status(422)
- end
- end
-
- describe "PUT /projects/:id/hooks/:hook_id" 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_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['confidential_issues_events']).to eq(hook.confidential_issues_events)
- expect(json_response['push_events']).to eq(false)
- expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events)
- expect(json_response['tag_push_events']).to eq(hook.tag_push_events)
- expect(json_response['note_events']).to eq(hook.note_events)
- expect(json_response['build_events']).to eq(hook.job_events)
- expect(json_response['pipeline_events']).to eq(hook.pipeline_events)
- expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events)
- expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification)
- end
-
- it "adds the token without including it in the response" do
- token = "secret token"
-
- put v3_api("/projects/#{project.id}/hooks/#{hook.id}", user), url: "http://example.org", token: token
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response["url"]).to eq("http://example.org")
- expect(json_response).not_to include("token")
-
- expect(hook.reload.url).to eq("http://example.org")
- expect(hook.reload.token).to eq(token)
- end
-
- 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_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_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_gitlab_http_status(422)
- end
- end
-
- describe "DELETE /projects/:id/hooks/:hook_id" do
- it "deletes hook from project" 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_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_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_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_gitlab_http_status(404)
- end
-
- it "returns a 404 if a user attempts to delete project hooks he/she does not own" do
- test_user = create(:user)
- other_project = create(:project)
- other_project.add_master(test_user)
-
- delete v3_api("/projects/#{other_project.id}/hooks/#{hook.id}", test_user)
- expect(response).to have_gitlab_http_status(404)
- expect(WebHook.exists?(hook.id)).to be_truthy
- end
- end
-end
diff --git a/spec/requests/api/v3/project_snippets_spec.rb b/spec/requests/api/v3/project_snippets_spec.rb
deleted file mode 100644
index 2ed31b99516..00000000000
--- a/spec/requests/api/v3/project_snippets_spec.rb
+++ /dev/null
@@ -1,226 +0,0 @@
-require 'rails_helper'
-
-describe API::ProjectSnippets do
- let(:project) { create(:project, :public) }
- let(:user) { create(:user) }
- let(:admin) { create(:admin) }
-
- describe 'GET /projects/:project_id/snippets/:id' do
- # TODO (rspeicher): Deprecated; remove in 9.0
- it 'always exposes expires_at as nil' do
- snippet = create(:project_snippet, author: admin)
-
- get v3_api("/projects/#{snippet.project.id}/snippets/#{snippet.id}", admin)
-
- expect(json_response).to have_key('expires_at')
- expect(json_response['expires_at']).to be_nil
- end
- end
-
- describe 'GET /projects/:project_id/snippets/' do
- let(:user) { create(:user) }
-
- it 'returns all snippets available to team member' do
- project.add_developer(user)
- public_snippet = create(:project_snippet, :public, project: project)
- internal_snippet = create(:project_snippet, :internal, project: project)
- private_snippet = create(:project_snippet, :private, project: project)
-
- get v3_api("/projects/#{project.id}/snippets/", user)
-
- 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')
- end
-
- it 'hides private snippets from regular user' do
- create(:project_snippet, :private, project: project)
-
- get v3_api("/projects/#{project.id}/snippets/", user)
- expect(response).to have_gitlab_http_status(200)
- expect(json_response.size).to eq(0)
- end
- end
-
- describe 'POST /projects/:project_id/snippets/' do
- let(:params) do
- {
- title: 'Test Title',
- file_name: 'test.rb',
- code: 'puts "hello world"',
- visibility_level: Snippet::PUBLIC
- }
- end
-
- it 'creates a new snippet' do
- post v3_api("/projects/#{project.id}/snippets/", admin), params
-
- 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])
- expect(snippet.file_name).to eq(params[:file_name])
- expect(snippet.visibility_level).to eq(params[:visibility_level])
- end
-
- it 'returns 400 for missing parameters' do
- params.delete(:title)
-
- post v3_api("/projects/#{project.id}/snippets/", admin), params
-
- expect(response).to have_gitlab_http_status(400)
- end
-
- context 'when the snippet is spam' do
- def create_snippet(project, snippet_params = {})
- project.add_developer(user)
-
- post v3_api("/projects/#{project.id}/snippets", user), params.merge(snippet_params)
- end
-
- before do
- allow_any_instance_of(AkismetService).to receive(:spam?).and_return(true)
- end
-
- context 'when the snippet is private' do
- it 'creates the snippet' do
- expect { create_snippet(project, visibility_level: Snippet::PRIVATE) }
- .to change { Snippet.count }.by(1)
- end
- end
-
- context 'when the snippet is public' do
- it 'rejects the shippet' do
- expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }
- .not_to change { Snippet.count }
-
- expect(response).to have_gitlab_http_status(400)
- expect(json_response['message']).to eq({ "error" => "Spam detected" })
- end
-
- it 'creates a spam log' do
- expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }
- .to change { SpamLog.count }.by(1)
- end
- end
- end
- end
-
- describe 'PUT /projects/:project_id/snippets/:id/' do
- let(:visibility_level) { Snippet::PUBLIC }
- let(:snippet) { create(:project_snippet, author: admin, visibility_level: visibility_level) }
-
- it 'updates snippet' do
- new_content = 'New content'
-
- put v3_api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin), code: new_content
-
- expect(response).to have_gitlab_http_status(200)
- snippet.reload
- expect(snippet.content).to eq(new_content)
- end
-
- it 'returns 404 for invalid snippet id' do
- put v3_api("/projects/#{snippet.project.id}/snippets/1234", admin), title: 'foo'
-
- 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_gitlab_http_status(400)
- end
-
- context 'when the snippet is spam' do
- def update_snippet(snippet_params = {})
- put v3_api("/projects/#{snippet.project.id}/snippets/#{snippet.id}", admin), snippet_params
- end
-
- before do
- allow_any_instance_of(AkismetService).to receive(:spam?).and_return(true)
- end
-
- context 'when the snippet is private' do
- let(:visibility_level) { Snippet::PRIVATE }
-
- it 'creates the snippet' do
- expect { update_snippet(title: 'Foo') }
- .to change { snippet.reload.title }.to('Foo')
- end
- end
-
- context 'when the snippet is public' do
- let(:visibility_level) { Snippet::PUBLIC }
-
- it 'rejects the snippet' do
- expect { update_snippet(title: 'Foo') }
- .not_to change { snippet.reload.title }
- end
-
- it 'creates a spam log' do
- expect { update_snippet(title: 'Foo') }
- .to change { SpamLog.count }.by(1)
- end
- end
-
- context 'when the private snippet is made public' do
- let(:visibility_level) { Snippet::PRIVATE }
-
- it 'rejects the snippet' do
- expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }
- .not_to change { snippet.reload.title }
-
- expect(response).to have_gitlab_http_status(400)
- expect(json_response['message']).to eq({ "error" => "Spam detected" })
- end
-
- it 'creates a spam log' do
- expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }
- .to change { SpamLog.count }.by(1)
- end
- end
- end
- end
-
- describe 'DELETE /projects/:project_id/snippets/:id/' do
- let(:snippet) { create(:project_snippet, author: admin) }
-
- it 'deletes snippet' do
- admin = create(:admin)
- snippet = create(:project_snippet, author: admin)
-
- delete v3_api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin)
-
- 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_gitlab_http_status(404)
- expect(json_response['message']).to eq('404 Snippet Not Found')
- end
- end
-
- describe 'GET /projects/:project_id/snippets/:id/raw' do
- let(:snippet) { create(:project_snippet, author: admin) }
-
- it 'returns raw text' do
- get v3_api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/raw", admin)
-
- expect(response).to have_gitlab_http_status(200)
- expect(response.content_type).to eq 'text/plain'
- expect(response.body).to eq(snippet.content)
- end
-
- it 'returns 404 for invalid snippet id' do
- delete v3_api("/projects/#{snippet.project.id}/snippets/1234", admin)
-
- expect(response).to have_gitlab_http_status(404)
- expect(json_response['message']).to eq('404 Snippet Not Found')
- end
- end
-end
diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb
deleted file mode 100644
index 158ddf171bc..00000000000
--- a/spec/requests/api/v3/projects_spec.rb
+++ /dev/null
@@ -1,1495 +0,0 @@
-require 'spec_helper'
-
-describe API::V3::Projects do
- let(:user) { create(:user) }
- let(:user2) { create(:user) }
- let(:user3) { create(:user) }
- let(:admin) { create(:admin) }
- let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
- let(:project2) { create(:project, creator_id: user.id, namespace: user.namespace) }
- let(:snippet) { create(:project_snippet, :public, author: user, project: project, title: 'example') }
- let(:project_member) { create(:project_member, :developer, user: user3, project: project) }
- let(:user4) { create(:user) }
- let(:project3) do
- create(:project,
- :private,
- :repository,
- name: 'second_project',
- path: 'second_project',
- creator_id: user.id,
- namespace: user.namespace,
- merge_requests_enabled: false,
- issues_enabled: false, wiki_enabled: false,
- snippets_enabled: false)
- end
- let(:project_member2) do
- create(:project_member,
- user: user4,
- project: project3,
- access_level: ProjectMember::MASTER)
- end
- let(:project4) do
- create(:project,
- name: 'third_project',
- path: 'third_project',
- creator_id: user4.id,
- namespace: user4.namespace)
- end
-
- describe 'GET /projects' do
- before { project }
-
- context 'when unauthenticated' do
- it 'returns authentication error' do
- get v3_api('/projects')
- 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_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)
- end
-
- it 'includes the project labels as the tag_list' do
- get v3_api('/projects', user)
- expect(response.status).to eq 200
- expect(json_response).to be_an Array
- expect(json_response.first.keys).to include('tag_list')
- end
-
- it 'includes open_issues_count' do
- get v3_api('/projects', user)
- expect(response.status).to eq 200
- expect(json_response).to be_an Array
- expect(json_response.first.keys).to include('open_issues_count')
- end
-
- it 'does not include open_issues_count' do
- project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
-
- get v3_api('/projects', user)
- expect(response.status).to eq 200
- expect(json_response).to be_an Array
- expect(json_response.first.keys).not_to include('open_issues_count')
- end
-
- context 'GET /projects?simple=true' do
- it 'returns a simplified version of all the projects' do
- expected_keys = %w(
- id description default_branch tag_list
- ssh_url_to_repo http_url_to_repo web_url readme_url
- name name_with_namespace
- path path_with_namespace
- star_count forks_count
- created_at last_activity_at
- avatar_url
- )
-
- get v3_api('/projects?simple=true', user)
-
- 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
- end
-
- context 'and using search' do
- it 'returns searched project' do
- get v3_api('/projects', user), { search: project.name }
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
- end
- end
-
- 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_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_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_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
- end
-
- context 'and using archived' do
- let!(:archived_project) { create(:project, creator_id: user.id, namespace: user.namespace, archived: true) }
-
- it 'returns archived project' do
- get v3_api('/projects?archived=true', user)
-
- 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)
- end
-
- it 'returns non-archived project' do
- get v3_api('/projects?archived=false', user)
-
- 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)
- end
-
- it 'returns all project' do
- get v3_api('/projects', user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(2)
- end
- end
-
- context 'and using sorting' do
- before do
- project2
- project3
- end
-
- it 'returns the correct order when sorted by id' do
- get v3_api('/projects', user), { order_by: 'id', sort: 'desc' }
- 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
- end
- end
- end
-
- describe 'GET /projects/all' do
- before { project }
-
- context 'when unauthenticated' do
- it 'returns authentication error' do
- get v3_api('/projects/all')
- 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_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_gitlab_http_status(200)
- expect(json_response).to be_an Array
-
- expect(json_response).to satisfy do |response|
- response.one? do |entry|
- entry.key?('permissions') &&
- entry['name'] == project.name &&
- entry['owner']['username'] == user.username
- end
- end
- end
-
- it "does not include statistics by default" do
- get v3_api('/projects/all', 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 v3_api('/projects/all', 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
-
- describe 'GET /projects/owned' do
- before do
- project3
- project4
- end
-
- context 'when unauthenticated' do
- it 'returns authentication error' do
- get v3_api('/projects/owned')
- 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_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)
- end
-
- it "does not include statistics by default" do
- get v3_api('/projects/owned', user4)
-
- 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
- attributes = {
- commit_count: 23,
- storage_size: 702,
- repository_size: 123,
- lfs_objects_size: 234,
- build_artifacts_size: 345
- }
-
- project4.statistics.update!(attributes)
-
- get v3_api('/projects/owned', user4), statistics: true
-
- 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
- end
- end
-
- describe 'GET /projects/visible' do
- shared_examples_for 'visible projects response' do
- it 'returns the visible projects' do
- get v3_api('/projects/visible', current_user)
-
- 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
- end
-
- let!(:public_project) { create(:project, :public) }
- before do
- project
- project2
- project3
- project4
- end
-
- context 'when unauthenticated' do
- it_behaves_like 'visible projects response' do
- let(:current_user) { nil }
- let(:projects) { [public_project] }
- end
- end
-
- context 'when authenticated' do
- it_behaves_like 'visible projects response' do
- let(:current_user) { user }
- let(:projects) { [public_project, project, project2, project3] }
- end
- end
-
- context 'when authenticated as a different user' do
- it_behaves_like 'visible projects response' do
- let(:current_user) { user2 }
- let(:projects) { [public_project] }
- end
- end
- end
-
- describe 'GET /projects/starred' do
- let(:public_project) { create(:project, :public) }
-
- before do
- project_member
- user3.update_attributes(starred_projects: [project, project2, project3, public_project])
- end
-
- it 'returns the starred projects viewable by the user' do
- get v3_api('/projects/starred', user3)
- 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
- end
-
- describe 'POST /projects' do
- context 'maximum number of projects reached' do
- it 'does not create new project and respond with 403' 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_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_gitlab_http_status(201)
-
- project = Project.first
-
- expect(project.name).to eq('Foo Project')
- expect(project.path).to eq('foo-project')
- end
-
- 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_gitlab_http_status(201)
-
- project = Project.first
-
- expect(project.name).to eq('foo_project')
- expect(project.path).to eq('foo_project')
- end
-
- 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_gitlab_http_status(201)
-
- project = Project.first
-
- expect(project.name).to eq('Foo Project')
- expect(project.path).to eq('foo-Project')
- end
-
- 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_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_gitlab_http_status(400)
- end
-
- it "assigns attributes to project" do
- project = attributes_for(:project, {
- path: 'camelCasePath',
- issues_enabled: false,
- merge_requests_enabled: false,
- wiki_enabled: false,
- only_allow_merge_if_build_succeeds: false,
- request_access_enabled: true,
- only_allow_merge_if_all_discussions_are_resolved: false
- })
-
- post v3_api('/projects', user), project
-
- project.each_pair do |k, v|
- next if %i[storage_version has_external_issue_tracker issues_enabled merge_requests_enabled wiki_enabled].include?(k)
-
- expect(json_response[k.to_s]).to eq(v)
- end
-
- # Check feature permissions attributes
- project = Project.find_by_path(project[:path])
- expect(project.project_feature.issues_access_level).to eq(ProjectFeature::DISABLED)
- expect(project.project_feature.merge_requests_access_level).to eq(ProjectFeature::DISABLED)
- expect(project.project_feature.wiki_access_level).to eq(ProjectFeature::DISABLED)
- end
-
- it 'sets a project as public' do
- project = attributes_for(:project, :public)
- post v3_api('/projects', user), project
- expect(json_response['public']).to be_truthy
- expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC)
- end
-
- it 'sets a project as public using :public' do
- project = attributes_for(:project, { public: true })
- post v3_api('/projects', user), project
- expect(json_response['public']).to be_truthy
- expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC)
- end
-
- it 'sets a project as internal' do
- project = attributes_for(:project, :internal)
- post v3_api('/projects', user), project
- expect(json_response['public']).to be_falsey
- expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL)
- end
-
- it 'sets a project as internal overriding :public' do
- project = attributes_for(:project, :internal, { public: true })
- post v3_api('/projects', user), project
- expect(json_response['public']).to be_falsey
- expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL)
- end
-
- it 'sets a project as private' do
- project = attributes_for(:project, :private)
- post v3_api('/projects', user), project
- expect(json_response['public']).to be_falsey
- expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
- end
-
- it 'sets a project as private using :public' do
- project = attributes_for(:project, { public: false })
- post v3_api('/projects', user), project
- expect(json_response['public']).to be_falsey
- expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
- end
-
- it 'sets a project as allowing merge even if build fails' do
- project = attributes_for(:project, { only_allow_merge_if_build_succeeds: false })
- post v3_api('/projects', user), project
- expect(json_response['only_allow_merge_if_build_succeeds']).to be_falsey
- end
-
- it 'sets a project as allowing merge only if merge_when_pipeline_succeeds' do
- project = attributes_for(:project, { only_allow_merge_if_build_succeeds: true })
- post v3_api('/projects', user), project
- expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy
- end
-
- it 'sets a project as allowing merge even if discussions are unresolved' do
- project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: false })
-
- post v3_api('/projects', user), project
-
- expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey
- end
-
- it 'sets a project as allowing merge if only_allow_merge_if_all_discussions_are_resolved is nil' do
- project = attributes_for(:project, only_allow_merge_if_all_discussions_are_resolved: nil)
-
- post v3_api('/projects', user), project
-
- expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey
- end
-
- it 'sets a project as allowing merge only if all discussions are resolved' do
- project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: true })
-
- post v3_api('/projects', user), project
-
- expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_truthy
- end
-
- context 'when a visibility level is restricted' do
- before do
- @project = attributes_for(:project, { public: true })
- stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
- end
-
- it 'does not allow a non-admin to use a restricted visibility level' do
- post v3_api('/projects', user), @project
-
- expect(response).to have_gitlab_http_status(400)
- expect(json_response['message']['visibility_level'].first).to(
- match('restricted by your GitLab administrator')
- )
- end
-
- it 'allows an admin to override restricted visibility settings' do
- post v3_api('/projects', admin), @project
- expect(json_response['public']).to be_truthy
- expect(json_response['visibility_level']).to(
- eq(Gitlab::VisibilityLevel::PUBLIC)
- )
- end
- end
- end
-
- describe 'POST /projects/user/:id' do
- before { project }
- before { admin }
-
- 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_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_gitlab_http_status(400)
- expect(json_response['error']).to eq('name is missing')
- end
-
- it 'assigns attributes to project' do
- project = attributes_for(:project, {
- issues_enabled: false,
- merge_requests_enabled: false,
- wiki_enabled: false,
- request_access_enabled: true
- })
-
- post v3_api("/projects/user/#{user.id}", admin), project
-
- expect(response).to have_gitlab_http_status(201)
- project.each_pair do |k, v|
- next if %i[storage_version has_external_issue_tracker path].include?(k)
-
- expect(json_response[k.to_s]).to eq(v)
- end
- end
-
- it 'sets a project as public' do
- project = attributes_for(:project, :public)
- post v3_api("/projects/user/#{user.id}", admin), project
-
- 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
-
- it 'sets a project as public using :public' do
- project = attributes_for(:project, { public: true })
- post v3_api("/projects/user/#{user.id}", admin), project
-
- 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
-
- it 'sets a project as internal' do
- project = attributes_for(:project, :internal)
- post v3_api("/projects/user/#{user.id}", admin), project
-
- 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
-
- 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_gitlab_http_status(201)
- expect(json_response['public']).to be_falsey
- expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL)
- end
-
- it 'sets a project as private' do
- project = attributes_for(:project, :private)
- post v3_api("/projects/user/#{user.id}", admin), project
- expect(json_response['public']).to be_falsey
- expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
- end
-
- it 'sets a project as private using :public' do
- project = attributes_for(:project, { public: false })
- post v3_api("/projects/user/#{user.id}", admin), project
- expect(json_response['public']).to be_falsey
- expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
- end
-
- it 'sets a project as allowing merge even if build fails' do
- project = attributes_for(:project, { only_allow_merge_if_build_succeeds: false })
- post v3_api("/projects/user/#{user.id}", admin), project
- expect(json_response['only_allow_merge_if_build_succeeds']).to be_falsey
- end
-
- it 'sets a project as allowing merge only if merge_when_pipeline_succeeds' do
- project = attributes_for(:project, { only_allow_merge_if_build_succeeds: true })
- post v3_api("/projects/user/#{user.id}", admin), project
- expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy
- end
-
- it 'sets a project as allowing merge even if discussions are unresolved' do
- project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: false })
-
- post v3_api("/projects/user/#{user.id}", admin), project
-
- expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey
- end
-
- it 'sets a project as allowing merge only if all discussions are resolved' do
- project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: true })
-
- post v3_api("/projects/user/#{user.id}", admin), project
-
- expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_truthy
- end
- end
-
- describe "POST /projects/:id/uploads" do
- before { project }
-
- 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_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")
- end
- end
-
- describe 'GET /projects/:id' do
- context 'when unauthenticated' do
- it 'returns the public projects' do
- public_project = create(:project, :public)
-
- get v3_api("/projects/#{public_project.id}")
-
- 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)
- expect(json_response.keys).not_to include('permissions')
- end
- end
-
- context 'when authenticated' do
- before do
- project
- end
-
- it 'returns a project by id' do
- group = create(:group)
- link = create(:project_group_link, project: project, group: group)
-
- get v3_api("/projects/#{project.id}", user)
-
- 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)
- expect(json_response['tag_list']).to be_an Array
- expect(json_response['public']).to be_falsey
- expect(json_response['archived']).to be_falsey
- expect(json_response['visibility_level']).to be_present
- expect(json_response['ssh_url_to_repo']).to be_present
- expect(json_response['http_url_to_repo']).to be_present
- expect(json_response['web_url']).to be_present
- expect(json_response['owner']).to be_a Hash
- expect(json_response['owner']).to be_a Hash
- expect(json_response['name']).to eq(project.name)
- expect(json_response['path']).to be_present
- expect(json_response['issues_enabled']).to be_present
- expect(json_response['merge_requests_enabled']).to be_present
- expect(json_response['wiki_enabled']).to be_present
- expect(json_response['builds_enabled']).to be_present
- expect(json_response['snippets_enabled']).to be_present
- expect(json_response['resolve_outdated_diff_discussions']).to eq(project.resolve_outdated_diff_discussions)
- expect(json_response['container_registry_enabled']).to be_present
- expect(json_response['created_at']).to be_present
- expect(json_response['last_activity_at']).to be_present
- expect(json_response['shared_runners_enabled']).to be_present
- expect(json_response['creator_id']).to be_present
- expect(json_response['namespace']).to be_present
- expect(json_response['avatar_url']).to be_nil
- expect(json_response['star_count']).to be_present
- expect(json_response['forks_count']).to be_present
- expect(json_response['public_builds']).to be_present
- expect(json_response['shared_with_groups']).to be_an Array
- expect(json_response['shared_with_groups'].length).to eq(1)
- expect(json_response['shared_with_groups'][0]['group_id']).to eq(group.id)
- expect(json_response['shared_with_groups'][0]['group_name']).to eq(group.name)
- expect(json_response['shared_with_groups'][0]['group_access_level']).to eq(link.group_access)
- expect(json_response['only_allow_merge_if_build_succeeds']).to eq(project.only_allow_merge_if_pipeline_succeeds)
- expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to eq(project.only_allow_merge_if_all_discussions_are_resolved)
- end
-
- it 'returns a project by path name' do
- get v3_api("/projects/#{project.id}", user)
- 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_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_gitlab_http_status(404)
- end
-
- it 'handles users with dots' do
- dot_user = create(:user, username: 'dot.user')
- 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_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_gitlab_http_status(200)
- expect(json_response['namespace']).to eq({
- 'id' => user.namespace.id,
- 'name' => user.namespace.name,
- 'path' => user.namespace.path,
- 'kind' => user.namespace.kind,
- 'full_path' => user.namespace.full_path,
- 'parent_id' => nil
- })
- end
-
- describe 'permissions' do
- context 'all projects' do
- before { project.add_master(user) }
-
- it 'contains permission information' do
- get v3_api("/projects", user)
-
- 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
- end
- end
-
- context 'personal project' do
- it 'sets project access and returns 200' do
- project.add_master(user)
- get v3_api("/projects/#{project.id}", user)
-
- 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
- end
- end
-
- context 'group project' do
- let(:project2) { create(:project, group: create(:group)) }
-
- before { project2.group.add_owner(user) }
-
- it 'sets the owner and return 200' do
- get v3_api("/projects/#{project2.id}", user)
-
- 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)
- end
- end
- end
- end
- end
-
- describe 'GET /projects/:id/events' do
- shared_examples_for 'project events response' do
- it 'returns the project events' do
- member = create(:user)
- create(:project_member, :developer, user: member, project: project)
- note = create(:note_on_issue, note: 'What an awesome day!', project: project)
- EventCreateService.new.leave_note(note, note.author)
-
- get v3_api("/projects/#{project.id}/events", current_user)
-
- expect(response).to have_gitlab_http_status(200)
-
- first_event = json_response.first
-
- expect(first_event['action_name']).to eq('commented on')
- expect(first_event['note']['body']).to eq('What an awesome day!')
-
- last_event = json_response.last
-
- expect(last_event['action_name']).to eq('joined')
- expect(last_event['project_id'].to_i).to eq(project.id)
- expect(last_event['author_username']).to eq(member.username)
- expect(last_event['author']['name']).to eq(member.name)
- end
- end
-
- context 'when unauthenticated' do
- it_behaves_like 'project events response' do
- let(:project) { create(:project, :public) }
- let(:current_user) { nil }
- end
- end
-
- context 'when authenticated' do
- context 'valid request' do
- it_behaves_like 'project events response' do
- let(:current_user) { user }
- end
- end
-
- it 'returns a 404 error if not found' do
- get v3_api('/projects/42/events', user)
-
- 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}/events", other_user)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
- end
-
- describe 'GET /projects/:id/users' do
- shared_examples_for 'project users response' do
- it 'returns the project users' do
- member = project.owner
-
- get v3_api("/projects/#{project.id}/users", current_user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.size).to eq(1)
-
- first_user = json_response.first
-
- expect(first_user['username']).to eq(member.username)
- expect(first_user['name']).to eq(member.name)
- expect(first_user.keys).to contain_exactly(*%w[name username id state avatar_url web_url])
- end
- end
-
- context 'when unauthenticated' do
- it_behaves_like 'project users response' do
- let(:project) { create(:project, :public) }
- let(:current_user) { nil }
- end
- end
-
- context 'when authenticated' do
- context 'valid request' do
- it_behaves_like 'project users response' do
- let(:current_user) { user }
- end
- end
-
- it 'returns a 404 error if not found' do
- get v3_api('/projects/42/users', user)
-
- 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}/users", other_user)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
- end
-
- describe 'GET /projects/:id/snippets' do
- before { snippet }
-
- it 'returns an array of project snippets' do
- get v3_api("/projects/#{project.id}/snippets", user)
- 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
- end
-
- 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_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_gitlab_http_status(404)
- end
- end
-
- describe 'POST /projects/:id/snippets' do
- it 'creates a new project snippet' 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_gitlab_http_status(201)
- expect(json_response['title']).to eq('v3_api test')
- end
-
- it 'returns a 400 error if invalid snippet is given' do
- post v3_api("/projects/#{project.id}/snippets", user)
- expect(status).to eq(400)
- end
- end
-
- describe 'PUT /projects/:id/snippets/:snippet_id' 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_gitlab_http_status(200)
- expect(json_response['title']).to eq('example')
- expect(snippet.reload.content).to eq('updated code')
- end
-
- 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_gitlab_http_status(200)
- expect(json_response['title']).to eq('other v3_api test')
- end
- end
-
- describe 'DELETE /projects/:id/snippets/:snippet_id' do
- before { snippet }
-
- it 'deletes existing project snippet' do
- expect do
- delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}", user)
- end.to change { Snippet.count }.by(-1)
- 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_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_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_gitlab_http_status(404)
- end
- end
-
- describe 'fork management' do
- let(:project_fork_target) { create(:project) }
- let(:project_fork_source) { create(:project, :public) }
-
- describe 'POST /projects/:id/fork/:forked_from_id' do
- let(:new_project_fork_source) { create(:project, :public) }
-
- 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_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_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
- expect(project_fork_target.forked?).to be_truthy
- end
-
- it 'refreshes the forks count cachce' do
- expect(project_fork_source.forks_count).to be_zero
-
- post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin)
-
- expect(project_fork_source.forks_count).to eq(1)
- end
-
- it 'fails if forked_from project which does not exist' do
- post v3_api("/projects/#{project_fork_target.id}/fork/9999", admin)
- expect(response).to have_gitlab_http_status(404)
- end
-
- it 'fails with 409 if already forked' do
- post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin)
- 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_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
- end
- end
-
- 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_gitlab_http_status(404)
- end
-
- context 'when users belong to project group' do
- let(:project_fork_target) { create(:project, group: create(:group)) }
-
- before do
- project_fork_target.group.add_owner user
- project_fork_target.group.add_developer user2
- end
-
- it 'is forbidden to non-owner users' do
- delete v3_api("/projects/#{project_fork_target.id}/fork", user2)
- expect(response).to have_gitlab_http_status(403)
- end
-
- it 'makes forked project unforked' do
- post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin)
- project_fork_target.reload
- expect(project_fork_target.forked_from_project).not_to be_nil
- expect(project_fork_target.forked?).to be_truthy
- delete v3_api("/projects/#{project_fork_target.id}/fork", admin)
- 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
- end
-
- 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_gitlab_http_status(304)
- expect(project_fork_target.reload.forked_from_project).to be_nil
- end
- end
- end
- end
-
- describe "POST /projects/:id/share" do
- let(:group) { create(:group) }
-
- it "shares project with group" do
- expires_at = 10.days.from_now.to_date
-
- expect 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_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)
- end
-
- 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_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_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_gitlab_http_status(400)
- end
-
- it 'returns a 404 error when user cannot read group' do
- private_group = create(:group, :private)
-
- post v3_api("/projects/#{project.id}/share", user), group_id: private_group.id, group_access: Gitlab::Access::DEVELOPER
-
- 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_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_gitlab_http_status(400)
- expect(json_response['error']).to eq 'group_access does not have a valid value'
- end
- end
-
- describe 'DELETE /projects/:id/share/:group_id' do
- it 'returns 204 when deleting a group share' do
- group = create(:group, :public)
- create(:project_group_link, group: group, project: project)
-
- delete v3_api("/projects/#{project.id}/share/#{group.id}", user)
-
- 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_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_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_gitlab_http_status(404)
- end
- end
-
- describe 'GET /projects/search/:query' do
- let!(:query) { 'query'}
- let!(:search) { create(:project, name: query, creator_id: user.id, namespace: user.namespace) }
- let!(:pre) { create(:project, name: "pre_#{query}", creator_id: user.id, namespace: user.namespace) }
- let!(:post) { create(:project, name: "#{query}_post", creator_id: user.id, namespace: user.namespace) }
- let!(:pre_post) { create(:project, name: "pre_#{query}_post", creator_id: user.id, namespace: user.namespace) }
- let!(:unfound) { create(:project, name: 'unfound', creator_id: user.id, namespace: user.namespace) }
- let!(:internal) { create(:project, :internal, name: "internal #{query}") }
- let!(:unfound_internal) { create(:project, :internal, name: 'unfound internal') }
- let!(:public) { create(:project, :public, name: "public #{query}") }
- let!(:unfound_public) { create(:project, :public, name: 'unfound public') }
- let!(:one_dot_two) { create(:project, :public, name: "one.dot.two") }
-
- shared_examples_for 'project search response' do |args = {}|
- it 'returns project search responses' do
- get v3_api("/projects/search/#{args[:query]}", current_user)
-
- 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]}.*/) }
- end
- end
-
- context 'when unauthenticated' do
- it_behaves_like 'project search response', query: 'query', results: 1 do
- let(:current_user) { nil }
- end
- end
-
- context 'when authenticated' do
- it_behaves_like 'project search response', query: 'query', results: 6 do
- let(:current_user) { user }
- end
- it_behaves_like 'project search response', query: 'one.dot.two', results: 1 do
- let(:current_user) { user }
- end
- end
-
- context 'when authenticated as a different user' do
- it_behaves_like 'project search response', query: 'query', results: 2, match_regex: /(internal|public) query/ do
- let(:current_user) { user2 }
- end
- end
- end
-
- describe 'PUT /projects/:id' do
- before { project }
- before { user }
- before { user3 }
- before { user4 }
- before { project3 }
- before { project4 }
- before { project_member2 }
- before { project_member }
-
- context 'when unauthenticated' do
- it 'returns authentication error' do
- project_param = { name: 'bar' }
- put v3_api("/projects/#{project.id}"), project_param
- expect(response).to have_gitlab_http_status(401)
- end
- end
-
- context 'when authenticated as project owner' do
- it 'updates name' do
- project_param = { name: 'bar' }
- put v3_api("/projects/#{project.id}", user), project_param
- 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
- end
-
- it 'updates visibility_level' do
- project_param = { visibility_level: 20 }
- put v3_api("/projects/#{project3.id}", user), project_param
- 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
- end
-
- it 'updates visibility_level from public to private' 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_gitlab_http_status(200)
- project_param.each_pair do |k, v|
- expect(json_response[k.to_s]).to eq(v)
- end
- expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
- end
-
- 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_gitlab_http_status(400)
- expect(json_response['message']['name']).to eq(['has already been taken'])
- end
-
- it 'updates request_access_enabled' do
- project_param = { request_access_enabled: false }
-
- put v3_api("/projects/#{project.id}", user), project_param
-
- 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_gitlab_http_status(200)
- project_param.each_pair do |k, v|
- expect(json_response[k.to_s]).to eq(v)
- end
- end
- end
-
- context 'when authenticated as project master' do
- it 'updates path' do
- project_param = { path: 'bar' }
- put v3_api("/projects/#{project3.id}", user4), project_param
- 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
- end
-
- it 'updates other attributes' do
- project_param = { issues_enabled: true,
- wiki_enabled: true,
- snippets_enabled: true,
- merge_requests_enabled: true,
- description: 'new description' }
-
- put v3_api("/projects/#{project3.id}", user4), project_param
- 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
- end
-
- 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_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_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_gitlab_http_status(403)
- end
- end
-
- context 'when authenticated as project developer' do
- it 'does not update other attributes' do
- project_param = { path: 'bar',
- issues_enabled: true,
- wiki_enabled: true,
- snippets_enabled: true,
- merge_requests_enabled: true,
- description: 'new description',
- request_access_enabled: true }
- put v3_api("/projects/#{project.id}", user3), project_param
- expect(response).to have_gitlab_http_status(403)
- end
- end
- end
-
- describe 'POST /projects/:id/archive' do
- context 'on an unarchived project' do
- it 'archives the project' do
- post v3_api("/projects/#{project.id}/archive", user)
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['archived']).to be_truthy
- end
- end
-
- context 'on an archived project' do
- before do
- project.archive!
- end
-
- it 'remains archived' do
- post v3_api("/projects/#{project.id}/archive", user)
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['archived']).to be_truthy
- end
- end
-
- context 'user without archiving rights to the project' do
- before do
- project.add_developer(user3)
- end
-
- it 'rejects the action' do
- post v3_api("/projects/#{project.id}/archive", user3)
-
- expect(response).to have_gitlab_http_status(403)
- end
- end
- end
-
- describe 'POST /projects/:id/unarchive' do
- context 'on an unarchived project' do
- it 'remains unarchived' do
- post v3_api("/projects/#{project.id}/unarchive", user)
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['archived']).to be_falsey
- end
- end
-
- context 'on an archived project' do
- before do
- project.archive!
- end
-
- it 'unarchives the project' do
- post v3_api("/projects/#{project.id}/unarchive", user)
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['archived']).to be_falsey
- end
- end
-
- context 'user without archiving rights to the project' do
- before do
- project.add_developer(user3)
- end
-
- it 'rejects the action' do
- post v3_api("/projects/#{project.id}/unarchive", user3)
-
- expect(response).to have_gitlab_http_status(403)
- end
- end
- end
-
- describe 'POST /projects/:id/star' do
- context 'on an unstarred project' 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_gitlab_http_status(201)
- expect(json_response['star_count']).to eq(1)
- end
- end
-
- context 'on a starred project' do
- before do
- user.toggle_star(project)
- project.reload
- end
-
- 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_gitlab_http_status(304)
- end
- end
- end
-
- describe 'DELETE /projects/:id/star' do
- context 'on a starred project' do
- before do
- user.toggle_star(project)
- project.reload
- end
-
- 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_gitlab_http_status(200)
- expect(json_response['star_count']).to eq(0)
- end
- end
-
- context 'on an unstarred project' 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_gitlab_http_status(304)
- end
- end
- end
-
- describe 'DELETE /projects/:id' do
- context 'when authenticated as user' do
- it 'removes project' do
- delete v3_api("/projects/#{project.id}", user)
- expect(response).to have_gitlab_http_status(200)
- end
-
- it 'does not remove a project if not an owner' do
- user3 = create(:user)
- project.add_developer(user3)
- delete v3_api("/projects/#{project.id}", user3)
- 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_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_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_gitlab_http_status(200)
- end
-
- it 'does not remove a non existing project' do
- delete v3_api('/projects/1328', admin)
- expect(response).to have_gitlab_http_status(404)
- end
- end
- end
-end
diff --git a/spec/requests/api/v3/repositories_spec.rb b/spec/requests/api/v3/repositories_spec.rb
deleted file mode 100644
index 0167eb2c4f6..00000000000
--- a/spec/requests/api/v3/repositories_spec.rb
+++ /dev/null
@@ -1,366 +0,0 @@
-require 'spec_helper'
-require 'mime/types'
-
-describe API::V3::Repositories do
- include RepoHelpers
- include WorkhorseHelpers
-
- let(:user) { create(:user) }
- let(:guest) { create(:user).tap { |u| create(:project_member, :guest, user: u, project: project) } }
- let!(:project) { create(:project, :repository, creator: user) }
- let!(:master) { create(:project_member, :master, user: user, project: project) }
-
- describe "GET /projects/:id/repository/tree" do
- let(:route) { "/projects/#{project.id}/repository/tree" }
-
- shared_examples_for 'repository tree' do
- it 'returns the repository tree' do
- get v3_api(route, current_user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
-
- first_commit = json_response.first
- expect(first_commit['name']).to eq('bar')
- expect(first_commit['type']).to eq('tree')
- expect(first_commit['mode']).to eq('040000')
- end
-
- context 'when ref does not exist' do
- it_behaves_like '404 response' do
- let(:request) { get v3_api("#{route}?ref_name=foo", current_user) }
- let(:message) { '404 Tree Not Found' }
- end
- end
-
- context 'when repository is disabled' do
- include_context 'disabled repository'
-
- it_behaves_like '403 response' do
- let(:request) { get v3_api(route, current_user) }
- end
- end
-
- context 'with recursive=1' do
- it 'returns recursive project paths tree' do
- get v3_api("#{route}?recursive=1", current_user)
-
- expect(response.status).to eq(200)
- expect(json_response).to be_an Array
- expect(json_response[4]['name']).to eq('html')
- expect(json_response[4]['path']).to eq('files/html')
- expect(json_response[4]['type']).to eq('tree')
- expect(json_response[4]['mode']).to eq('040000')
- end
-
- context 'when repository is disabled' do
- include_context 'disabled repository'
-
- it_behaves_like '403 response' do
- let(:request) { get v3_api(route, current_user) }
- end
- end
-
- context 'when ref does not exist' do
- it_behaves_like '404 response' do
- let(:request) { get v3_api("#{route}?recursive=1&ref_name=foo", current_user) }
- let(:message) { '404 Tree Not Found' }
- end
- end
- end
- end
-
- context 'when unauthenticated', 'and project is public' do
- it_behaves_like 'repository tree' do
- let(:project) { create(:project, :public, :repository) }
- let(:current_user) { nil }
- end
- end
-
- context 'when unauthenticated', 'and project is private' do
- it_behaves_like '404 response' do
- let(:request) { get v3_api(route) }
- let(:message) { '404 Project Not Found' }
- end
- end
-
- context 'when authenticated', 'as a developer' do
- it_behaves_like 'repository tree' do
- let(:current_user) { user }
- end
- end
-
- context 'when authenticated', 'as a guest' do
- it_behaves_like '403 response' do
- let(:request) { get v3_api(route, guest) }
- end
- end
- end
-
- [
- ['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_gitlab_http_status(200)
- end
- context 'when sha does not exist' do
- it_behaves_like '404 response' do
- 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
- context 'when filepath does not exist' do
- it_behaves_like '404 response' do
- let(:request) { get v3_api(route.sub('README.md', 'README.invalid'), current_user) }
- let(:message) { '404 File Not Found' }
- end
- end
- context 'when no filepath is given' do
- it_behaves_like '400 response' do
- let(:request) { get v3_api(route.sub('?filepath=README.md', ''), current_user) }
- end
- end
- context 'when repository is disabled' do
- include_context 'disabled repository'
- it_behaves_like '403 response' do
- let(:request) { get v3_api(route, current_user) }
- end
- end
- end
- context 'when unauthenticated', 'and project is public' do
- it_behaves_like 'repository blob' do
- let(:project) { create(:project, :public, :repository) }
- let(:current_user) { nil }
- end
- end
- context 'when unauthenticated', 'and project is private' do
- it_behaves_like '404 response' do
- let(:request) { get v3_api(route) }
- let(:message) { '404 Project Not Found' }
- end
- end
- context 'when authenticated', 'as a developer' do
- it_behaves_like 'repository blob' do
- let(:current_user) { user }
- end
- end
- context 'when authenticated', 'as a guest' do
- it_behaves_like '403 response' do
- let(:request) { get v3_api(route, guest) }
- end
- end
- end
- end
- describe "GET /projects/:id/repository/raw_blobs/:sha" do
- let(:route) { "/projects/#{project.id}/repository/raw_blobs/#{sample_blob.oid}" }
- shared_examples_for 'repository raw blob' do
- it 'returns the repository raw blob' do
- get v3_api(route, current_user)
- 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(sample_blob.oid, '123456'), current_user) }
- let(:message) { '404 Blob Not Found' }
- end
- end
- context 'when repository is disabled' do
- include_context 'disabled repository'
- it_behaves_like '403 response' do
- let(:request) { get v3_api(route, current_user) }
- end
- end
- end
- context 'when unauthenticated', 'and project is public' do
- it_behaves_like 'repository raw blob' do
- let(:project) { create(:project, :public, :repository) }
- let(:current_user) { nil }
- end
- end
- context 'when unauthenticated', 'and project is private' do
- it_behaves_like '404 response' do
- let(:request) { get v3_api(route) }
- let(:message) { '404 Project Not Found' }
- end
- end
- context 'when authenticated', 'as a developer' do
- it_behaves_like 'repository raw blob' do
- let(:current_user) { user }
- end
- end
- context 'when authenticated', 'as a guest' do
- it_behaves_like '403 response' do
- let(:request) { get v3_api(route, guest) }
- end
- end
- end
- describe "GET /projects/:id/repository/archive(.:format)?:sha" do
- let(:route) { "/projects/#{project.id}/repository/archive" }
- shared_examples_for 'repository archive' do
- it 'returns the repository archive' do
- get v3_api(route, current_user)
- 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')
- expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.gz/)
- end
- it 'returns the repository archive archive.zip' do
- get v3_api("/projects/#{project.id}/repository/archive.zip", user)
- 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')
- expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.zip/)
- 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_gitlab_http_status(200)
- repo_name = project.repository.name.gsub("\.git", "")
- type, params = workhorse_send_data
- expect(type).to eq('git-archive')
- expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.bz2/)
- end
- context 'when sha does not exist' do
- it_behaves_like '404 response' do
- let(:request) { get v3_api("#{route}?sha=xxx", current_user) }
- let(:message) { '404 File Not Found' }
- end
- end
- end
- context 'when unauthenticated', 'and project is public' do
- it_behaves_like 'repository archive' do
- let(:project) { create(:project, :public, :repository) }
- let(:current_user) { nil }
- end
- end
- context 'when unauthenticated', 'and project is private' do
- it_behaves_like '404 response' do
- let(:request) { get v3_api(route) }
- let(:message) { '404 Project Not Found' }
- end
- end
- context 'when authenticated', 'as a developer' do
- it_behaves_like 'repository archive' do
- let(:current_user) { user }
- end
- end
- context 'when authenticated', 'as a guest' do
- it_behaves_like '403 response' do
- let(:request) { get v3_api(route, guest) }
- end
- end
- end
-
- describe 'GET /projects/:id/repository/compare' do
- let(:route) { "/projects/#{project.id}/repository/compare" }
- shared_examples_for 'repository compare' do
- it "compares branches" do
- get v3_api(route, current_user), from: 'master', to: 'feature'
- 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_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_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_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_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
- end
- end
- context 'when unauthenticated', 'and project is public' do
- it_behaves_like 'repository compare' do
- let(:project) { create(:project, :public, :repository) }
- let(:current_user) { nil }
- end
- end
- context 'when unauthenticated', 'and project is private' do
- it_behaves_like '404 response' do
- let(:request) { get v3_api(route) }
- let(:message) { '404 Project Not Found' }
- end
- end
- context 'when authenticated', 'as a developer' do
- it_behaves_like 'repository compare' do
- let(:current_user) { user }
- end
- end
- context 'when authenticated', 'as a guest' do
- it_behaves_like '403 response' do
- let(:request) { get v3_api(route, guest) }
- end
- end
- end
-
- describe 'GET /projects/:id/repository/contributors' do
- let(:route) { "/projects/#{project.id}/repository/contributors" }
-
- shared_examples_for 'repository contributors' do
- it 'returns valid data' do
- get v3_api(route, current_user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
-
- first_contributor = json_response.first
- expect(first_contributor['email']).to eq('tiagonbotelho@hotmail.com')
- expect(first_contributor['name']).to eq('tiagonbotelho')
- expect(first_contributor['commits']).to eq(1)
- expect(first_contributor['additions']).to eq(0)
- expect(first_contributor['deletions']).to eq(0)
- end
- end
-
- context 'when unauthenticated', 'and project is public' do
- it_behaves_like 'repository contributors' do
- let(:project) { create(:project, :public, :repository) }
- let(:current_user) { nil }
- end
- end
-
- context 'when unauthenticated', 'and project is private' do
- it_behaves_like '404 response' do
- let(:request) { get v3_api(route) }
- let(:message) { '404 Project Not Found' }
- end
- end
-
- context 'when authenticated', 'as a developer' do
- it_behaves_like 'repository contributors' do
- let(:current_user) { user }
- end
- end
-
- context 'when authenticated', 'as a guest' do
- it_behaves_like '403 response' do
- let(:request) { get v3_api(route, guest) }
- end
- end
- end
-end
diff --git a/spec/requests/api/v3/runners_spec.rb b/spec/requests/api/v3/runners_spec.rb
deleted file mode 100644
index c91b097a3c7..00000000000
--- a/spec/requests/api/v3/runners_spec.rb
+++ /dev/null
@@ -1,152 +0,0 @@
-require 'spec_helper'
-
-describe API::V3::Runners do
- let(:admin) { create(:user, :admin) }
- let(:user) { create(:user) }
- let(:user2) { create(:user) }
-
- let(:project) { create(:project, creator_id: user.id) }
- let(:project2) { create(:project, creator_id: user.id) }
-
- let!(:shared_runner) { create(:ci_runner, :shared) }
- let!(:unused_specific_runner) { create(:ci_runner) }
-
- let!(:specific_runner) do
- create(:ci_runner).tap do |runner|
- create(:ci_runner_project, runner: runner, project: project)
- end
- end
-
- let!(:two_projects_runner) do
- create(:ci_runner).tap do |runner|
- create(:ci_runner_project, runner: runner, project: project)
- create(:ci_runner_project, runner: runner, project: project2)
- end
- end
-
- before do
- # Set project access for users
- create(:project_member, :master, user: user, project: project)
- create(:project_member, :reporter, user: user2, project: project)
- end
-
- describe 'DELETE /runners/:id' do
- context 'admin user' do
- context 'when runner is shared' do
- it 'deletes runner' do
- expect do
- delete v3_api("/runners/#{shared_runner.id}", admin)
-
- expect(response).to have_gitlab_http_status(200)
- end.to change { Ci::Runner.shared.count }.by(-1)
- end
- end
-
- context 'when runner is not shared' do
- it 'deletes unused runner' do
- expect do
- delete v3_api("/runners/#{unused_specific_runner.id}", admin)
-
- expect(response).to have_gitlab_http_status(200)
- end.to change { Ci::Runner.specific.count }.by(-1)
- end
-
- it 'deletes used runner' do
- expect do
- delete v3_api("/runners/#{specific_runner.id}", admin)
-
- expect(response).to have_gitlab_http_status(200)
- end.to change { Ci::Runner.specific.count }.by(-1)
- end
- end
-
- it 'returns 404 if runner does not exists' do
- delete v3_api('/runners/9999', admin)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
-
- context 'authorized user' 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_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_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_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_gitlab_http_status(200)
- end.to change { Ci::Runner.specific.count }.by(-1)
- end
- end
- end
-
- context 'unauthorized user' do
- it 'does not delete runner' do
- delete v3_api("/runners/#{specific_runner.id}")
-
- expect(response).to have_gitlab_http_status(401)
- end
- end
- end
-
- describe 'DELETE /projects/:id/runners/:runner_id' do
- context 'authorized user' do
- context 'when runner have more than one associated projects' do
- it "disables project's runner" do
- expect do
- delete v3_api("/projects/#{project.id}/runners/#{two_projects_runner.id}", user)
-
- expect(response).to have_gitlab_http_status(200)
- end.to change { project.runners.count }.by(-1)
- end
- end
-
- context 'when runner have one associated projects' do
- it "does not disable project's runner" do
- expect do
- delete v3_api("/projects/#{project.id}/runners/#{specific_runner.id}", user)
- end.to change { project.runners.count }.by(0)
- 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_gitlab_http_status(404)
- end
- end
-
- context 'authorized user without permissions' do
- it "does not disable project's runner" do
- delete v3_api("/projects/#{project.id}/runners/#{specific_runner.id}", user2)
-
- expect(response).to have_gitlab_http_status(403)
- end
- end
-
- context 'unauthorized user' do
- it "does not disable project's runner" do
- delete v3_api("/projects/#{project.id}/runners/#{specific_runner.id}")
-
- expect(response).to have_gitlab_http_status(401)
- end
- end
- end
-end
diff --git a/spec/requests/api/v3/services_spec.rb b/spec/requests/api/v3/services_spec.rb
deleted file mode 100644
index c69a7d58ca6..00000000000
--- a/spec/requests/api/v3/services_spec.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-require "spec_helper"
-
-describe API::V3::Services do
- let(:user) { create(:user) }
- let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
-
- available_services = Service.available_services_names
- available_services.delete('prometheus')
- available_services.each do |service|
- describe "DELETE /projects/:id/services/#{service.dasherize}" do
- include_context service
-
- before do
- initialize_service(service)
- end
-
- it "deletes #{service}" do
- delete v3_api("/projects/#{project.id}/services/#{dashed_service}", user)
-
- expect(response).to have_gitlab_http_status(200)
- project.send(service_method).reload
- expect(project.send(service_method).activated?).to be_falsey
- end
- end
- end
-end
diff --git a/spec/requests/api/v3/settings_spec.rb b/spec/requests/api/v3/settings_spec.rb
deleted file mode 100644
index 985bfbfa09c..00000000000
--- a/spec/requests/api/v3/settings_spec.rb
+++ /dev/null
@@ -1,63 +0,0 @@
-require 'spec_helper'
-
-describe API::V3::Settings, 'Settings' do
- let(:user) { create(:user) }
- let(:admin) { create(:admin) }
-
- describe "GET /application/settings" do
- it "returns application settings" do
- get v3_api("/application/settings", admin)
- 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['repository_storage']).to eq('default')
- expect(json_response['koding_enabled']).to be_falsey
- expect(json_response['koding_url']).to be_nil
- expect(json_response['plantuml_enabled']).to be_falsey
- expect(json_response['plantuml_url']).to be_nil
- end
- end
-
- describe "PUT /application/settings" do
- context "custom repository storage type set in the config" do
- before do
- storages = { 'custom' => 'tmp/tests/custom_repositories' }
- allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
- end
-
- it "updates application settings" do
- put v3_api("/application/settings", admin),
- 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_gitlab_http_status(200)
- expect(json_response['default_projects_limit']).to eq(3)
- 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
- expect(json_response['koding_url']).to eq('http://koding.example.com')
- expect(json_response['plantuml_enabled']).to be_truthy
- expect(json_response['plantuml_url']).to eq('http://plantuml.example.com')
- end
- end
-
- context "missing koding_url value when koding_enabled is true" do
- it "returns a blank parameter error message" do
- put v3_api("/application/settings", admin), koding_enabled: true
-
- expect(response).to have_gitlab_http_status(400)
- expect(json_response['error']).to eq('koding_url is missing')
- end
- end
-
- context "missing plantuml_url value when plantuml_enabled is true" do
- it "returns a blank parameter error message" do
- put v3_api("/application/settings", admin), plantuml_enabled: true
-
- expect(response).to have_gitlab_http_status(400)
- expect(json_response['error']).to eq('plantuml_url is missing')
- end
- end
- end
-end
diff --git a/spec/requests/api/v3/snippets_spec.rb b/spec/requests/api/v3/snippets_spec.rb
deleted file mode 100644
index e8913039194..00000000000
--- a/spec/requests/api/v3/snippets_spec.rb
+++ /dev/null
@@ -1,186 +0,0 @@
-require 'rails_helper'
-
-describe API::V3::Snippets do
- let!(:user) { create(:user) }
-
- describe 'GET /snippets/' do
- it 'returns snippets available' do
- public_snippet = create(:personal_snippet, :public, author: user)
- private_snippet = create(:personal_snippet, :private, author: user)
- internal_snippet = create(:personal_snippet, :internal, author: user)
-
- get v3_api("/snippets/", user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly(
- public_snippet.id,
- internal_snippet.id,
- private_snippet.id)
- expect(json_response.last).to have_key('web_url')
- expect(json_response.last).to have_key('raw_url')
- end
-
- it 'hides private snippets from regular user' do
- create(:personal_snippet, :private)
-
- get v3_api("/snippets/", user)
- expect(response).to have_gitlab_http_status(200)
- expect(json_response.size).to eq(0)
- end
- end
-
- describe 'GET /snippets/public' do
- let!(:other_user) { create(:user) }
- let!(:public_snippet) { create(:personal_snippet, :public, author: user) }
- let!(:private_snippet) { create(:personal_snippet, :private, author: user) }
- let!(:internal_snippet) { create(:personal_snippet, :internal, author: user) }
- let!(:public_snippet_other) { create(:personal_snippet, :public, author: other_user) }
- let!(:private_snippet_other) { create(:personal_snippet, :private, author: other_user) }
- let!(:internal_snippet_other) { create(:personal_snippet, :internal, author: other_user) }
-
- it 'returns all snippets with public visibility from all users' do
- get v3_api("/snippets/public", user)
-
- 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)
- expect(json_response.map { |snippet| snippet['web_url']} ).to include(
- "http://localhost/snippets/#{public_snippet.id}",
- "http://localhost/snippets/#{public_snippet_other.id}")
- expect(json_response.map { |snippet| snippet['raw_url']} ).to include(
- "http://localhost/snippets/#{public_snippet.id}/raw",
- "http://localhost/snippets/#{public_snippet_other.id}/raw")
- end
- end
-
- describe 'GET /snippets/:id/raw' do
- let(:snippet) { create(:personal_snippet, author: user) }
-
- it 'returns raw text' do
- get v3_api("/snippets/#{snippet.id}/raw", user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(response.content_type).to eq 'text/plain'
- expect(response.body).to eq(snippet.content)
- end
-
- it 'returns 404 for invalid snippet id' do
- delete v3_api("/snippets/1234", user)
-
- expect(response).to have_gitlab_http_status(404)
- expect(json_response['message']).to eq('404 Snippet Not Found')
- end
- end
-
- describe 'POST /snippets/' do
- let(:params) do
- {
- title: 'Test Title',
- file_name: 'test.rb',
- content: 'puts "hello world"',
- visibility_level: Snippet::PUBLIC
- }
- end
-
- it 'creates a new snippet' do
- expect do
- post v3_api("/snippets/", user), params
- end.to change { PersonalSnippet.count }.by(1)
-
- 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
-
- it 'returns 400 for missing parameters' do
- params.delete(:title)
-
- post v3_api("/snippets/", user), params
-
- expect(response).to have_gitlab_http_status(400)
- end
-
- context 'when the snippet is spam' do
- def create_snippet(snippet_params = {})
- post v3_api('/snippets', user), params.merge(snippet_params)
- end
-
- before do
- allow_any_instance_of(AkismetService).to receive(:spam?).and_return(true)
- end
-
- context 'when the snippet is private' do
- it 'creates the snippet' do
- expect { create_snippet(visibility_level: Snippet::PRIVATE) }
- .to change { Snippet.count }.by(1)
- end
- end
-
- context 'when the snippet is public' do
- it 'rejects the shippet' do
- expect { create_snippet(visibility_level: Snippet::PUBLIC) }
- .not_to change { Snippet.count }
- expect(response).to have_gitlab_http_status(400)
- end
-
- it 'creates a spam log' do
- expect { create_snippet(visibility_level: Snippet::PUBLIC) }
- .to change { SpamLog.count }.by(1)
- end
- end
- end
- end
-
- describe 'PUT /snippets/:id' do
- let(:other_user) { create(:user) }
- let(:public_snippet) { create(:personal_snippet, :public, author: user) }
- it 'updates snippet' do
- new_content = 'New content'
-
- put v3_api("/snippets/#{public_snippet.id}", user), content: new_content
-
- expect(response).to have_gitlab_http_status(200)
- public_snippet.reload
- expect(public_snippet.content).to eq(new_content)
- end
-
- it 'returns 404 for invalid snippet id' do
- put v3_api("/snippets/1234", user), title: 'foo'
-
- 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_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_gitlab_http_status(400)
- end
- end
-
- describe 'DELETE /snippets/:id' do
- let!(:public_snippet) { create(:personal_snippet, :public, author: user) }
- it 'deletes snippet' do
- expect do
- delete v3_api("/snippets/#{public_snippet.id}", user)
-
- 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_gitlab_http_status(404)
- expect(json_response['message']).to eq('404 Snippet Not Found')
- end
- end
-end
diff --git a/spec/requests/api/v3/system_hooks_spec.rb b/spec/requests/api/v3/system_hooks_spec.rb
deleted file mode 100644
index 30711c60faa..00000000000
--- a/spec/requests/api/v3/system_hooks_spec.rb
+++ /dev/null
@@ -1,56 +0,0 @@
-require 'spec_helper'
-
-describe API::V3::SystemHooks do
- let(:user) { create(:user) }
- let(:admin) { create(:admin) }
- let!(:hook) { create(:system_hook, url: "http://example.com") }
-
- before { stub_request(:post, hook.url) }
-
- describe "GET /hooks" do
- context "when no user" do
- it "returns authentication error" do
- get v3_api("/hooks")
-
- expect(response).to have_gitlab_http_status(401)
- end
- end
-
- context "when not an admin" do
- it "returns forbidden error" do
- get v3_api("/hooks", user)
-
- expect(response).to have_gitlab_http_status(403)
- end
- end
-
- context "when authenticated as admin" do
- it "returns an array of hooks" do
- get v3_api("/hooks", admin)
-
- 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
- expect(json_response.first['tag_push_events']).to be false
- expect(json_response.first['repository_update_events']).to be true
- end
- end
- end
-
- describe "DELETE /hooks/:id" do
- it "deletes a hook" do
- expect do
- delete v3_api("/hooks/#{hook.id}", admin)
-
- 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_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
deleted file mode 100644
index e6ad005fa87..00000000000
--- a/spec/requests/api/v3/tags_spec.rb
+++ /dev/null
@@ -1,88 +0,0 @@
-require 'spec_helper'
-require 'mime/types'
-
-describe API::V3::Tags do
- include RepoHelpers
-
- let(:user) { create(:user) }
- let(:user2) { create(:user) }
- let!(:project) { create(:project, :repository, creator: user) }
- let!(:master) { create(:project_member, :master, user: user, project: project) }
-
- describe "GET /projects/:id/repository/tags" do
- let(:tag_name) { project.repository.tag_names.sort.reverse.first }
- let(:description) { 'Awesome release!' }
-
- shared_examples_for 'repository tags' do
- it 'returns the repository tags' do
- get v3_api("/projects/#{project.id}/repository/tags", current_user)
-
- 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
- end
-
- context 'when unauthenticated' do
- it_behaves_like 'repository tags' do
- let(:project) { create(:project, :public, :repository) }
- let(:current_user) { nil }
- end
- end
-
- context 'when authenticated' do
- it_behaves_like 'repository tags' do
- let(:current_user) { user }
- end
- end
-
- context 'without releases' do
- it "returns an array of project tags" do
- get v3_api("/projects/#{project.id}/repository/tags", user)
-
- 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
- end
-
- context 'with releases' do
- before do
- release = project.releases.find_or_initialize_by(tag: tag_name)
- release.update_attributes(description: description)
- end
-
- it "returns an array of project tags with release info" do
- get v3_api("/projects/#{project.id}/repository/tags", user)
-
- 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')
- expect(json_response.first['release']['description']).to eq(description)
- end
- end
- end
-
- describe 'DELETE /projects/:id/repository/tags/:tag_name' do
- let(:tag_name) { project.repository.tag_names.sort.reverse.first }
-
- before do
- allow_any_instance_of(Repository).to receive(:rm_tag).and_return(true)
- end
-
- context 'delete tag' do
- it 'deletes an existing tag' do
- delete v3_api("/projects/#{project.id}/repository/tags/#{tag_name}", user)
-
- 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_gitlab_http_status(404)
- end
- end
- end
-end
diff --git a/spec/requests/api/v3/templates_spec.rb b/spec/requests/api/v3/templates_spec.rb
deleted file mode 100644
index 1a637f3cf96..00000000000
--- a/spec/requests/api/v3/templates_spec.rb
+++ /dev/null
@@ -1,201 +0,0 @@
-require 'spec_helper'
-
-describe API::V3::Templates do
- shared_examples_for 'the Template Entity' do |path|
- before { get v3_api(path) }
-
- it { expect(json_response['name']).to eq('Ruby') }
- it { expect(json_response['content']).to include('*.gem') }
- end
-
- shared_examples_for 'the TemplateList Entity' do |path|
- before { get v3_api(path) }
-
- it { expect(json_response.first['name']).not_to be_nil }
- it { expect(json_response.first['content']).to be_nil }
- end
-
- shared_examples_for 'requesting gitignores' do |path|
- it 'returns a list of available gitignore templates' do
- get v3_api(path)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.size).to be > 15
- end
- end
-
- shared_examples_for 'requesting gitlab-ci-ymls' do |path|
- it 'returns a list of available gitlab_ci_ymls' do
- get v3_api(path)
-
- 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
- end
-
- shared_examples_for 'requesting gitlab-ci-yml for Ruby' do |path|
- it 'adds a disclaimer on the top' do
- get v3_api(path)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['content']).to start_with("# This file is a template,")
- end
- end
-
- shared_examples_for 'the License Template Entity' do |path|
- before { get v3_api(path) }
-
- it 'returns a license template' do
- expect(json_response['key']).to eq('mit')
- expect(json_response['name']).to eq('MIT License')
- expect(json_response['nickname']).to be_nil
- expect(json_response['popular']).to be true
- expect(json_response['html_url']).to eq('http://choosealicense.com/licenses/mit/')
- expect(json_response['source_url']).to eq('https://opensource.org/licenses/MIT')
- expect(json_response['description']).to include('A short and simple permissive license with conditions')
- expect(json_response['conditions']).to eq(%w[include-copyright])
- expect(json_response['permissions']).to eq(%w[commercial-use modifications distribution private-use])
- expect(json_response['limitations']).to eq(%w[liability warranty])
- expect(json_response['content']).to include('MIT License')
- end
- end
-
- shared_examples_for 'GET licenses' do |path|
- it 'returns a list of available license templates' do
- get v3_api(path)
-
- 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')
- end
-
- describe 'the popular parameter' do
- context 'with popular=1' do
- it 'returns a list of available popular license templates' do
- get v3_api("#{path}?popular=1")
-
- 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')
- end
- end
- end
- end
-
- shared_examples_for 'GET licenses/:name' do |path|
- context 'with :project and :fullname given' do
- before do
- get v3_api("#{path}/#{license_type}?project=My+Awesome+Project&fullname=Anton+#{license_type.upcase}")
- end
-
- context 'for the mit license' do
- let(:license_type) { 'mit' }
-
- it 'returns the license text' do
- expect(json_response['content']).to include('MIT License')
- end
-
- it 'replaces placeholder values' do
- expect(json_response['content']).to include("Copyright (c) #{Time.now.year} Anton")
- end
- end
-
- context 'for the agpl-3.0 license' do
- let(:license_type) { 'agpl-3.0' }
-
- it 'returns the license text' do
- expect(json_response['content']).to include('GNU AFFERO GENERAL PUBLIC LICENSE')
- end
-
- it 'replaces placeholder values' do
- expect(json_response['content']).to include('My Awesome Project')
- expect(json_response['content']).to include("Copyright (C) #{Time.now.year} Anton")
- end
- end
-
- context 'for the gpl-3.0 license' do
- let(:license_type) { 'gpl-3.0' }
-
- it 'returns the license text' do
- expect(json_response['content']).to include('GNU GENERAL PUBLIC LICENSE')
- end
-
- it 'replaces placeholder values' do
- expect(json_response['content']).to include('My Awesome Project')
- expect(json_response['content']).to include("Copyright (C) #{Time.now.year} Anton")
- end
- end
-
- context 'for the gpl-2.0 license' do
- let(:license_type) { 'gpl-2.0' }
-
- it 'returns the license text' do
- expect(json_response['content']).to include('GNU GENERAL PUBLIC LICENSE')
- end
-
- it 'replaces placeholder values' do
- expect(json_response['content']).to include('My Awesome Project')
- expect(json_response['content']).to include("Copyright (C) #{Time.now.year} Anton")
- end
- end
-
- context 'for the apache-2.0 license' do
- let(:license_type) { 'apache-2.0' }
-
- it 'returns the license text' do
- expect(json_response['content']).to include('Apache License')
- end
-
- it 'replaces placeholder values' do
- expect(json_response['content']).to include("Copyright #{Time.now.year} Anton")
- end
- end
-
- context 'for an uknown license' do
- let(:license_type) { 'muth-over9000' }
-
- it 'returns a 404' do
- expect(response).to have_gitlab_http_status(404)
- end
- end
- end
-
- context 'with no :fullname given' do
- context 'with an authenticated user' do
- let(:user) { create(:user) }
-
- it 'replaces the copyright owner placeholder with the name of the current user' do
- get v3_api('/templates/licenses/mit', user)
-
- expect(json_response['content']).to include("Copyright (c) #{Time.now.year} #{user.name}")
- end
- end
- end
- end
-
- describe 'with /templates namespace' do
- it_behaves_like 'the Template Entity', '/templates/gitignores/Ruby'
- it_behaves_like 'the TemplateList Entity', '/templates/gitignores'
- it_behaves_like 'requesting gitignores', '/templates/gitignores'
- it_behaves_like 'requesting gitlab-ci-ymls', '/templates/gitlab_ci_ymls'
- it_behaves_like 'requesting gitlab-ci-yml for Ruby', '/templates/gitlab_ci_ymls/Ruby'
- it_behaves_like 'the License Template Entity', '/templates/licenses/mit'
- it_behaves_like 'GET licenses', '/templates/licenses'
- it_behaves_like 'GET licenses/:name', '/templates/licenses'
- end
-
- describe 'without /templates namespace' do
- it_behaves_like 'the Template Entity', '/gitignores/Ruby'
- it_behaves_like 'the TemplateList Entity', '/gitignores'
- it_behaves_like 'requesting gitignores', '/gitignores'
- it_behaves_like 'requesting gitlab-ci-ymls', '/gitlab_ci_ymls'
- it_behaves_like 'requesting gitlab-ci-yml for Ruby', '/gitlab_ci_ymls/Ruby'
- it_behaves_like 'the License Template Entity', '/licenses/mit'
- it_behaves_like 'GET licenses', '/licenses'
- it_behaves_like 'GET licenses/:name', '/licenses'
- end
-end
diff --git a/spec/requests/api/v3/todos_spec.rb b/spec/requests/api/v3/todos_spec.rb
deleted file mode 100644
index ea648e3917f..00000000000
--- a/spec/requests/api/v3/todos_spec.rb
+++ /dev/null
@@ -1,77 +0,0 @@
-require 'spec_helper'
-
-describe API::V3::Todos do
- let(:project_1) { create(:project) }
- let(:project_2) { create(:project) }
- let(:author_1) { create(:user) }
- let(:author_2) { create(:user) }
- let(:john_doe) { create(:user, username: 'john_doe') }
- let!(:pending_1) { create(:todo, :mentioned, project: project_1, author: author_1, user: john_doe) }
- let!(:pending_2) { create(:todo, project: project_2, author: author_2, user: john_doe) }
- let!(:pending_3) { create(:todo, project: project_1, author: author_2, user: john_doe) }
- let!(:done) { create(:todo, :done, project: project_1, author: author_1, user: john_doe) }
-
- before do
- project_1.add_developer(john_doe)
- project_2.add_developer(john_doe)
- end
-
- describe 'DELETE /todos/:id' do
- context 'when unauthenticated' do
- it 'returns authentication error' do
- delete v3_api("/todos/#{pending_1.id}")
-
- expect(response.status).to eq(401)
- end
- end
-
- context 'when authenticated' do
- it 'marks a todo as done' do
- delete v3_api("/todos/#{pending_1.id}", john_doe)
-
- expect(response.status).to eq(200)
- expect(pending_1.reload).to be_done
- end
-
- it 'updates todos cache' do
- expect_any_instance_of(User).to receive(:update_todos_count_cache).and_call_original
-
- delete v3_api("/todos/#{pending_1.id}", john_doe)
- end
-
- it 'returns 404 if the todo does not belong to the current user' do
- delete v3_api("/todos/#{pending_1.id}", author_1)
-
- expect(response.status).to eq(404)
- end
- end
- end
-
- describe 'DELETE /todos' do
- context 'when unauthenticated' do
- it 'returns authentication error' do
- delete v3_api('/todos')
-
- expect(response.status).to eq(401)
- end
- end
-
- context 'when authenticated' do
- it 'marks all todos as done' do
- delete v3_api('/todos', john_doe)
-
- expect(response.status).to eq(200)
- expect(response.body).to eq('3')
- expect(pending_1.reload).to be_done
- expect(pending_2.reload).to be_done
- expect(pending_3.reload).to be_done
- end
-
- it 'updates todos cache' do
- expect_any_instance_of(User).to receive(:update_todos_count_cache).and_call_original
-
- delete v3_api("/todos", john_doe)
- end
- end
- end
-end
diff --git a/spec/requests/api/v3/triggers_spec.rb b/spec/requests/api/v3/triggers_spec.rb
deleted file mode 100644
index e8e2f49d7a0..00000000000
--- a/spec/requests/api/v3/triggers_spec.rb
+++ /dev/null
@@ -1,235 +0,0 @@
-require 'spec_helper'
-
-describe API::V3::Triggers do
- let(:user) { create(:user) }
- let(:user2) { create(:user) }
- let!(:trigger_token) { 'secure_token' }
- let!(:project) { create(:project, :repository, creator: user) }
- let!(:master) { create(:project_member, :master, user: user, project: project) }
- let!(:developer) { create(:project_member, :developer, user: user2, project: project) }
-
- let!(:trigger) do
- create(:ci_trigger, project: project, token: trigger_token, owner: user)
- end
-
- describe 'POST /projects/:project_id/trigger' do
- let!(:project2) { create(:project) }
- let(:options) do
- {
- token: trigger_token
- }
- end
-
- before do
- stub_ci_pipeline_to_return_yaml_file
- end
-
- 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_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_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_gitlab_http_status(404)
- end
- end
-
- context 'Have a commit' do
- let(:pipeline) { project.pipelines.last }
-
- it 'creates builds' do
- post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'master')
- 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)
- end
-
- 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_gitlab_http_status(400)
- expect(json_response['message']['base'])
- .to contain_exactly('Reference not found')
- end
-
- context 'Validates variables' do
- let(:variables) do
- { 'TRIGGER_KEY' => 'TRIGGER_VALUE' }
- end
-
- it 'validates variables to be a hash' do
- post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(variables: 'value', ref: 'master')
- 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_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_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)
- end
- end
- end
-
- context 'when triggering a pipeline from a trigger token' do
- it 'creates builds from the ref given in the URL, not in the body' 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_gitlab_http_status(201)
- end
-
- context 'when ref contains a dot' do
- it 'creates builds from the ref given in the URL, not in the body' do
- project.repository.create_file(user, '.gitlab/gitlabhq/new_feature.md', 'something valid', message: 'new_feature', branch_name: 'v.1-branch')
-
- expect 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_gitlab_http_status(201)
- end
- end
- end
- end
-
- describe 'GET /projects/:id/triggers' do
- context 'authenticated user with valid permissions' do
- it 'returns list of triggers' do
- get v3_api("/projects/#{project.id}/triggers", user)
-
- 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')
- end
- end
-
- context 'authenticated user with invalid permissions' do
- it 'does not return triggers list' do
- get v3_api("/projects/#{project.id}/triggers", user2)
-
- expect(response).to have_gitlab_http_status(403)
- end
- end
-
- context 'unauthenticated user' do
- it 'does not return triggers list' do
- get v3_api("/projects/#{project.id}/triggers")
-
- expect(response).to have_gitlab_http_status(401)
- end
- end
- end
-
- describe 'GET /projects/:id/triggers/:token' do
- context 'authenticated user with valid permissions' do
- it 'returns trigger details' do
- get v3_api("/projects/#{project.id}/triggers/#{trigger.token}", user)
-
- 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_gitlab_http_status(404)
- end
- end
-
- context 'authenticated user with invalid permissions' do
- it 'does not return triggers list' do
- get v3_api("/projects/#{project.id}/triggers/#{trigger.token}", user2)
-
- expect(response).to have_gitlab_http_status(403)
- end
- end
-
- context 'unauthenticated user' do
- it 'does not return triggers list' do
- get v3_api("/projects/#{project.id}/triggers/#{trigger.token}")
-
- expect(response).to have_gitlab_http_status(401)
- end
- end
- end
-
- describe 'POST /projects/:id/triggers' do
- context 'authenticated user with valid permissions' do
- it 'creates trigger' do
- expect do
- post v3_api("/projects/#{project.id}/triggers", user)
- end.to change {project.triggers.count}.by(1)
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response).to be_a(Hash)
- end
- end
-
- context 'authenticated user with invalid permissions' do
- it 'does not create trigger' do
- post v3_api("/projects/#{project.id}/triggers", user2)
-
- expect(response).to have_gitlab_http_status(403)
- end
- end
-
- context 'unauthenticated user' do
- it 'does not create trigger' do
- post v3_api("/projects/#{project.id}/triggers")
-
- expect(response).to have_gitlab_http_status(401)
- end
- end
- end
-
- describe 'DELETE /projects/:id/triggers/:token' do
- context 'authenticated user with valid permissions' do
- it 'deletes trigger' do
- expect do
- delete v3_api("/projects/#{project.id}/triggers/#{trigger.token}", user)
-
- 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_gitlab_http_status(404)
- end
- end
-
- context 'authenticated user with invalid permissions' do
- it 'does not delete trigger' do
- delete v3_api("/projects/#{project.id}/triggers/#{trigger.token}", user2)
-
- expect(response).to have_gitlab_http_status(403)
- end
- end
-
- context 'unauthenticated user' do
- it 'does not delete trigger' do
- delete v3_api("/projects/#{project.id}/triggers/#{trigger.token}")
-
- expect(response).to have_gitlab_http_status(401)
- end
- end
- end
-end
diff --git a/spec/requests/api/v3/users_spec.rb b/spec/requests/api/v3/users_spec.rb
deleted file mode 100644
index bbd05f240d2..00000000000
--- a/spec/requests/api/v3/users_spec.rb
+++ /dev/null
@@ -1,362 +0,0 @@
-require 'spec_helper'
-
-describe API::V3::Users do
- let(:user) { create(:user) }
- let(:admin) { create(:admin) }
- let(:key) { create(:key, user: user) }
- let(:email) { create(:email, user: user) }
- let(:ldap_blocked_user) { create(:omniauth_user, provider: 'ldapmain', state: 'ldap_blocked') }
-
- describe 'GET /users' do
- context 'when authenticated' do
- it 'returns an array of users' do
- get v3_api('/users', user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- username = user.username
- expect(json_response.detect do |user|
- user['username'] == username
- end['username']).to eq(username)
- end
- end
-
- context 'when authenticated as user' do
- it 'does not reveal the `is_admin` flag of the user' do
- get v3_api('/users', user)
-
- expect(json_response.first.keys).not_to include 'is_admin'
- end
- end
-
- context 'when authenticated as admin' do
- it 'reveals the `is_admin` flag of the user' do
- get v3_api('/users', admin)
-
- expect(json_response.first.keys).to include 'is_admin'
- end
- end
- end
-
- describe 'GET /user/:id/keys' do
- before { admin }
-
- context 'when unauthenticated' do
- it 'returns authentication error' do
- get v3_api("/users/#{user.id}/keys")
- 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_gitlab_http_status(404)
- expect(json_response['message']).to eq('404 User Not Found')
- end
-
- it 'returns array of ssh keys' do
- user.keys << key
- user.save
-
- get v3_api("/users/#{user.id}/keys", admin)
-
- 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
- end
-
- context "scopes" do
- let(:user) { admin }
- let(:path) { "/users/#{user.id}/keys" }
- let(:api_call) { method(:v3_api) }
-
- before do
- user.keys << key
- user.save
- end
-
- include_examples 'allows the "read_user" scope'
- end
- end
-
- describe 'GET /user/:id/emails' do
- before { admin }
-
- context 'when unauthenticated' do
- it 'returns authentication error' do
- get v3_api("/users/#{user.id}/emails")
- 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_gitlab_http_status(404)
- expect(json_response['message']).to eq('404 User Not Found')
- end
-
- it 'returns array of emails' do
- user.emails << email
- user.save
-
- get v3_api("/users/#{user.id}/emails", admin)
-
- 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
-
- it "returns a 404 for invalid ID" do
- put v3_api("/users/ASDF/emails", admin)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
- end
-
- describe "GET /user/keys" do
- context "when unauthenticated" do
- it "returns authentication error" do
- get v3_api("/user/keys")
- expect(response).to have_gitlab_http_status(401)
- end
- end
-
- context "when authenticated" do
- it "returns array of ssh keys" do
- user.keys << key
- user.save
-
- get v3_api("/user/keys", user)
-
- 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
- end
- end
-
- describe "GET /user/emails" do
- context "when unauthenticated" do
- it "returns authentication error" do
- get v3_api("/user/emails")
- expect(response).to have_gitlab_http_status(401)
- end
- end
-
- context "when authenticated" do
- it "returns array of emails" do
- user.emails << email
- user.save
-
- get v3_api("/user/emails", user)
-
- 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
- end
- end
-
- describe 'PUT /users/:id/block' do
- before { admin }
- it 'blocks existing user' do
- put v3_api("/users/#{user.id}/block", admin)
- 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_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_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_gitlab_http_status(404)
- expect(json_response['message']).to eq('404 User Not Found')
- end
- end
-
- describe 'PUT /users/:id/unblock' do
- let(:blocked_user) { create(:user, state: 'blocked') }
- before { admin }
-
- it 'unblocks existing user' do
- put v3_api("/users/#{user.id}/unblock", admin)
- 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_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_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_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_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_gitlab_http_status(404)
- end
- end
-
- describe 'GET /users/:id/events' do
- let(:user) { create(:user) }
- let(:project) { create(:project) }
- let(:note) { create(:note_on_issue, note: 'What an awesome day!', project: project) }
-
- before do
- project.add_user(user, :developer)
- EventCreateService.new.leave_note(note, user)
- end
-
- context "as a user than cannot see the event's project" do
- it 'returns no events' do
- other_user = create(:user)
-
- get api("/users/#{user.id}/events", other_user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_empty
- end
- end
-
- context "as a user than can see the event's project" do
- context 'when the list of events includes push events' do
- let(:event) { create(:push_event, author: user, project: project) }
- let!(:payload) { create(:push_event_payload, event: event) }
- let(:payload_hash) { json_response[0]['push_data'] }
-
- before do
- get api("/users/#{user.id}/events?action=pushed", user)
- end
-
- it 'responds with HTTP 200 OK' do
- expect(response).to have_gitlab_http_status(200)
- end
-
- it 'includes the push payload as a Hash' do
- expect(payload_hash).to be_an_instance_of(Hash)
- end
-
- it 'includes the push payload details' do
- expect(payload_hash['commit_count']).to eq(payload.commit_count)
- expect(payload_hash['action']).to eq(payload.action)
- expect(payload_hash['ref_type']).to eq(payload.ref_type)
- expect(payload_hash['commit_to']).to eq(payload.commit_to)
- end
- end
-
- context 'joined event' do
- it 'returns the "joined" event' do
- get v3_api("/users/#{user.id}/events", user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
-
- comment_event = json_response.find { |e| e['action_name'] == 'commented on' }
-
- expect(comment_event['project_id'].to_i).to eq(project.id)
- expect(comment_event['author_username']).to eq(user.username)
- expect(comment_event['note']['id']).to eq(note.id)
- expect(comment_event['note']['body']).to eq('What an awesome day!')
-
- joined_event = json_response.find { |e| e['action_name'] == 'joined' }
-
- expect(joined_event['project_id'].to_i).to eq(project.id)
- expect(joined_event['author_username']).to eq(user.username)
- expect(joined_event['author']['name']).to eq(user.name)
- end
- end
-
- context 'when there are multiple events from different projects' do
- let(:second_note) { create(:note_on_issue, project: create(:project)) }
- let(:third_note) { create(:note_on_issue, project: project) }
-
- before do
- second_note.project.add_user(user, :developer)
-
- [second_note, third_note].each do |note|
- EventCreateService.new.leave_note(note, user)
- end
- end
-
- it 'returns events in the correct order (from newest to oldest)' do
- get v3_api("/users/#{user.id}/events", user)
-
- comment_events = json_response.select { |e| e['action_name'] == 'commented on' }
-
- expect(comment_events[0]['target_id']).to eq(third_note.id)
- expect(comment_events[1]['target_id']).to eq(second_note.id)
- expect(comment_events[2]['target_id']).to eq(note.id)
- end
- end
- end
-
- it 'returns a 404 error if not found' do
- get v3_api('/users/420/events', user)
-
- expect(response).to have_gitlab_http_status(404)
- expect(json_response['message']).to eq('404 User Not Found')
- end
- end
-
- describe 'POST /users' do
- it 'creates confirmed user when confirm parameter is false' do
- optional_attributes = { confirm: false }
- attributes = attributes_for(:user).merge(optional_attributes)
-
- post v3_api('/users', admin), attributes
-
- user_id = json_response['id']
- new_user = User.find(user_id)
-
- expect(new_user).to be_confirmed
- end
-
- it 'does not reveal the `is_admin` flag of the user' do
- post v3_api('/users', admin), attributes_for(:user)
-
- expect(json_response['is_admin']).to be_nil
- end
-
- context "scopes" do
- let(:user) { admin }
- let(:path) { '/users' }
- let(:api_call) { method(:v3_api) }
-
- include_examples 'does not allow the "read_user" scope'
- end
- end
-end
diff --git a/spec/requests/api/variables_spec.rb b/spec/requests/api/variables_spec.rb
index 62215ea3d7d..be333df1d78 100644
--- a/spec/requests/api/variables_spec.rb
+++ b/spec/requests/api/variables_spec.rb
@@ -4,7 +4,7 @@ describe API::Variables do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let!(:project) { create(:project, creator_id: user.id) }
- let!(:master) { create(:project_member, :master, user: user, project: project) }
+ let!(:maintainer) { create(:project_member, :maintainer, user: user, project: project) }
let!(:developer) { create(:project_member, :developer, user: user2, project: project) }
let!(:variable) { create(:ci_variable, project: project) }
diff --git a/spec/requests/api/version_spec.rb b/spec/requests/api/version_spec.rb
index 7bbf34422b8..38b618191fb 100644
--- a/spec/requests/api/version_spec.rb
+++ b/spec/requests/api/version_spec.rb
@@ -18,7 +18,7 @@ describe API::Version do
expect(response).to have_gitlab_http_status(200)
expect(json_response['version']).to eq(Gitlab::VERSION)
- expect(json_response['revision']).to eq(Gitlab::REVISION)
+ expect(json_response['revision']).to eq(Gitlab.revision)
end
end
end
diff --git a/spec/requests/api/wikis_spec.rb b/spec/requests/api/wikis_spec.rb
index 850ba696098..489cb001b82 100644
--- a/spec/requests/api/wikis_spec.rb
+++ b/spec/requests/api/wikis_spec.rb
@@ -7,7 +7,7 @@ require 'spec_helper'
# Every state is tested for 3 user roles:
# - guest
# - developer
-# - master
+# - maintainer
# because they are 3 edge cases of using wiki pages.
describe API::Wikis do
@@ -163,9 +163,9 @@ describe API::Wikis do
include_examples '403 Forbidden'
end
- context 'when user is master' do
+ context 'when user is maintainer' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
get api(url, user)
end
@@ -193,9 +193,9 @@ describe API::Wikis do
include_examples 'returns list of wiki pages'
end
- context 'when user is master' do
+ context 'when user is maintainer' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
include_examples 'returns list of wiki pages'
@@ -221,9 +221,9 @@ describe API::Wikis do
include_examples 'returns list of wiki pages'
end
- context 'when user is master' do
+ context 'when user is maintainer' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
include_examples 'returns list of wiki pages'
@@ -256,9 +256,9 @@ describe API::Wikis do
include_examples '403 Forbidden'
end
- context 'when user is master' do
+ context 'when user is maintainer' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
get api(url, user)
end
@@ -293,9 +293,9 @@ describe API::Wikis do
end
end
- context 'when user is master' do
+ context 'when user is maintainer' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
get api(url, user)
end
@@ -337,9 +337,9 @@ describe API::Wikis do
end
end
- context 'when user is master' do
+ context 'when user is maintainer' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
get api(url, user)
end
@@ -379,9 +379,9 @@ describe API::Wikis do
include_examples '403 Forbidden'
end
- context 'when user is master' do
+ context 'when user is maintainer' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
post(api(url, user), payload)
end
@@ -408,9 +408,9 @@ describe API::Wikis do
include_examples 'creates wiki page'
end
- context 'when user is master' do
+ context 'when user is maintainer' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
include_examples 'creates wiki page'
@@ -436,9 +436,9 @@ describe API::Wikis do
include_examples 'creates wiki page'
end
- context 'when user is master' do
+ context 'when user is maintainer' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
include_examples 'creates wiki page'
@@ -472,9 +472,9 @@ describe API::Wikis do
include_examples '403 Forbidden'
end
- context 'when user is master' do
+ context 'when user is maintainer' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
put(api(url, user), payload)
end
@@ -510,9 +510,9 @@ describe API::Wikis do
end
end
- context 'when user is master' do
+ context 'when user is maintainer' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
put(api(url, user), payload)
end
@@ -554,9 +554,9 @@ describe API::Wikis do
end
end
- context 'when user is master' do
+ context 'when user is maintainer' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
put(api(url, user), payload)
end
@@ -607,9 +607,9 @@ describe API::Wikis do
include_examples '403 Forbidden'
end
- context 'when user is master' do
+ context 'when user is maintainer' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
delete(api(url, user))
end
@@ -639,9 +639,9 @@ describe API::Wikis do
include_examples '403 Forbidden'
end
- context 'when user is master' do
+ context 'when user is maintainer' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
delete(api(url, user))
end
@@ -671,9 +671,9 @@ describe API::Wikis do
include_examples '403 Forbidden'
end
- context 'when user is master' do
+ context 'when user is maintainer' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
delete(api(url, user))
end
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index 2514dab1714..b030d9862c6 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -1,6 +1,7 @@
require "spec_helper"
describe 'Git HTTP requests' do
+ include ProjectForksHelper
include TermsHelper
include GitHttpHelpers
include WorkhorseHelpers
@@ -305,6 +306,22 @@ describe 'Git HTTP requests' do
expect(response.body).to eq(change_access_error(:push_code))
end
end
+
+ context 'when merge requests are open that allow maintainer access' do
+ let(:canonical_project) { create(:project, :public, :repository) }
+ let(:project) { fork_project(canonical_project, nil, repository: true) }
+
+ before do
+ canonical_project.add_maintainer(user)
+ create(:merge_request,
+ source_project: project,
+ target_project: canonical_project,
+ source_branch: 'fixes',
+ allow_collaboration: true)
+ end
+
+ it_behaves_like 'pushes are allowed'
+ end
end
end
@@ -381,13 +398,13 @@ describe 'Git HTTP requests' do
context "when the user has access to the project" do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
context "when the user is blocked" do
it "rejects pulls with 401 Unauthorized" do
user.block
- project.add_master(user)
+ project.add_maintainer(user)
download(path, env) do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
@@ -450,7 +467,7 @@ describe 'Git HTTP requests' do
let(:path) { "#{project.full_path}.git" }
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
context 'when username and password are provided' do
@@ -810,7 +827,7 @@ describe 'Git HTTP requests' do
context 'and the user is on the team' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
it "responds with status 200" do
@@ -833,7 +850,7 @@ describe 'Git HTTP requests' do
let(:env) { { user: user.username, password: user.password } }
before do
- project.add_master(user)
+ project.add_maintainer(user)
enforce_terms
end
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index f80abb06fca..de39abdb746 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -63,7 +63,7 @@ describe 'Git LFS API and storage' do
context 'with LFS disabled globally' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
allow(Gitlab.config.lfs).to receive(:enabled).and_return(false)
end
@@ -106,7 +106,7 @@ describe 'Git LFS API and storage' do
context 'with LFS enabled globally' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
enable_lfs
end
@@ -236,7 +236,7 @@ describe 'Git LFS API and storage' do
context 'and does have project access' do
let(:update_permissions) do
- project.add_master(user)
+ project.add_maintainer(user)
project.lfs_objects << lfs_object
end
@@ -293,7 +293,7 @@ describe 'Git LFS API and storage' do
context 'when user allowed' do
let(:update_permissions) do
- project.add_master(user)
+ project.add_maintainer(user)
project.lfs_objects << lfs_object
end
@@ -829,7 +829,7 @@ describe 'Git LFS API and storage' do
context 'when user is not authenticated' do
context 'when user has push access' do
let(:update_user_permissions) do
- project.add_master(user)
+ project.add_maintainer(user)
end
it 'responds with status 401' do
@@ -874,7 +874,7 @@ describe 'Git LFS API and storage' do
before do
allow(Gitlab::Database).to receive(:read_only?) { true }
- project.add_master(user)
+ project.add_maintainer(user)
enable_lfs
end
@@ -1021,6 +1021,7 @@ describe 'Git LFS API and storage' do
expect(json_response['RemoteObject']).to have_key('GetURL')
expect(json_response['RemoteObject']).to have_key('StoreURL')
expect(json_response['RemoteObject']).to have_key('DeleteURL')
+ expect(json_response['RemoteObject']).not_to have_key('MultipartUpload')
expect(json_response['LfsOid']).to eq(sample_oid)
expect(json_response['LfsSize']).to eq(sample_size)
end
@@ -1089,7 +1090,7 @@ describe 'Git LFS API and storage' do
context 'with valid remote_id' do
before do
fog_connection.directories.get('lfs-objects').files.create(
- key: 'tmp/upload/12312300',
+ key: 'tmp/uploads/12312300',
body: 'content'
)
end
@@ -1306,7 +1307,7 @@ describe 'Git LFS API and storage' do
let(:authorization) { authorize_user }
before do
- second_project.add_master(user)
+ second_project.add_maintainer(user)
upstream_project.lfs_objects << lfs_object
end
diff --git a/spec/requests/lfs_locks_api_spec.rb b/spec/requests/lfs_locks_api_spec.rb
index e44a11a7232..a44b43a591f 100644
--- a/spec/requests/lfs_locks_api_spec.rb
+++ b/spec/requests/lfs_locks_api_spec.rb
@@ -4,7 +4,7 @@ describe 'Git LFS File Locking API' do
include WorkhorseHelpers
let(:project) { create(:project) }
- let(:master) { create(:user) }
+ let(:maintainer) { create(:user) }
let(:developer) { create(:user) }
let(:guest) { create(:user) }
let(:path) { 'README.md' }
@@ -29,7 +29,7 @@ describe 'Git LFS File Locking API' do
before do
allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
- project.add_developer(master)
+ project.add_developer(maintainer)
project.add_developer(developer)
project.add_guest(guest)
end
@@ -99,7 +99,7 @@ describe 'Git LFS File Locking API' do
include_examples 'unauthorized request'
it 'returns the list of locked files grouped by owner' do
- lock_file('README.md', master)
+ lock_file('README.md', maintainer)
lock_file('README', developer)
post_lfs_json url, nil, headers
diff --git a/spec/requests/oauth_tokens_spec.rb b/spec/requests/oauth_tokens_spec.rb
new file mode 100644
index 00000000000..000c3a2b868
--- /dev/null
+++ b/spec/requests/oauth_tokens_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+describe 'OAuth Tokens requests' do
+ let(:user) { create :user }
+ let(:application) { create :oauth_application, scopes: 'api' }
+
+ def request_access_token(user)
+ post '/oauth/token',
+ grant_type: 'authorization_code',
+ code: generate_access_grant(user).token,
+ redirect_uri: application.redirect_uri,
+ client_id: application.uid,
+ client_secret: application.secret
+ end
+
+ def generate_access_grant(user)
+ create :oauth_access_grant, application: application, resource_owner_id: user.id
+ end
+
+ context 'when there is already a token for the application' do
+ let!(:existing_token) { create :oauth_access_token, application: application, resource_owner_id: user.id }
+
+ context 'and the request is done by the resource owner' do
+ it 'reuses and returns the stored token' do
+ expect do
+ request_access_token(user)
+ end.not_to change { Doorkeeper::AccessToken.count }
+
+ expect(json_response['access_token']).to eq existing_token.token
+ end
+ end
+
+ context 'and the request is done by a different user' do
+ let(:other_user) { create :user }
+
+ it 'generates and returns a different token for a different owner' do
+ expect do
+ request_access_token(other_user)
+ end.to change { Doorkeeper::AccessToken.count }.by(1)
+
+ expect(json_response['access_token']).not_to be_nil
+ end
+ end
+ end
+
+ context 'when there is no token stored for the application' do
+ it 'generates and returns a new token' do
+ expect do
+ request_access_token(user)
+ end.to change { Doorkeeper::AccessToken.count }.by(1)
+
+ expect(json_response['access_token']).not_to be_nil
+ end
+ end
+end
diff --git a/spec/requests/openid_connect_spec.rb b/spec/requests/openid_connect_spec.rb
index be286c490fe..b14d4b8fb6e 100644
--- a/spec/requests/openid_connect_spec.rb
+++ b/spec/requests/openid_connect_spec.rb
@@ -1,11 +1,49 @@
require 'spec_helper'
describe 'OpenID Connect requests' do
- let(:user) { create :user }
+ let(:user) do
+ create(
+ :user,
+ name: 'Alice',
+ username: 'alice',
+ email: 'private@example.com',
+ emails: [public_email],
+ public_email: public_email.email,
+ website_url: 'https://example.com',
+ avatar: fixture_file_upload('spec/fixtures/dk.png')
+ )
+ end
+
+ let(:public_email) { build :email, email: 'public@example.com' }
+
let(:access_grant) { create :oauth_access_grant, application: application, resource_owner_id: user.id }
let(:access_token) { create :oauth_access_token, application: application, resource_owner_id: user.id }
- def request_access_token
+ let(:hashed_subject) do
+ Digest::SHA256.hexdigest("#{user.id}-#{Rails.application.secrets.secret_key_base}")
+ end
+
+ let(:id_token_claims) do
+ {
+ 'sub' => user.id.to_s,
+ 'sub_legacy' => hashed_subject
+ }
+ end
+
+ let(:user_info_claims) do
+ {
+ 'name' => 'Alice',
+ 'nickname' => 'alice',
+ 'email' => 'public@example.com',
+ 'email_verified' => true,
+ 'website' => 'https://example.com',
+ 'profile' => 'http://localhost/alice',
+ 'picture' => "http://localhost/uploads/-/system/user/avatar/#{user.id}/dk.png",
+ 'groups' => kind_of(Array)
+ }
+ end
+
+ def request_access_token!
login_as user
post '/oauth/token',
@@ -16,26 +54,22 @@ describe 'OpenID Connect requests' do
client_secret: application.secret
end
- def request_user_info
+ def request_user_info!
get '/oauth/userinfo', nil, 'Authorization' => "Bearer #{access_token.token}"
end
- def hashed_subject
- Digest::SHA256.hexdigest("#{user.id}-#{Rails.application.secrets.secret_key_base}")
- end
-
context 'Application without OpenID scope' do
let(:application) { create :oauth_application, scopes: 'api' }
it 'token response does not include an ID token' do
- request_access_token
+ request_access_token!
expect(json_response).to include 'access_token'
expect(json_response).not_to include 'id_token'
end
it 'userinfo response is unauthorized' do
- request_user_info
+ request_user_info!
expect(response).to have_gitlab_http_status 403
expect(response.body).to be_blank
@@ -46,28 +80,12 @@ describe 'OpenID Connect requests' do
let(:application) { create :oauth_application, scopes: 'openid' }
it 'token response includes an ID token' do
- request_access_token
+ request_access_token!
expect(json_response).to include 'id_token'
end
context 'UserInfo payload' do
- let(:user) do
- create(
- :user,
- name: 'Alice',
- username: 'alice',
- emails: [private_email, public_email],
- email: private_email.email,
- public_email: public_email.email,
- website_url: 'https://example.com',
- avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png")
- )
- end
-
- let!(:public_email) { build :email, email: 'public@example.com' }
- let!(:private_email) { build :email, email: 'private@example.com' }
-
let!(:group1) { create :group }
let!(:group2) { create :group }
let!(:group3) { create :group, parent: group2 }
@@ -76,41 +94,35 @@ describe 'OpenID Connect requests' do
before do
group1.add_user(user, GroupMember::OWNER)
group3.add_user(user, Gitlab::Access::DEVELOPER)
+
+ request_user_info!
end
it 'includes all user information and group memberships' do
- request_user_info
-
- expect(json_response).to match(a_hash_including({
- 'sub' => hashed_subject,
- 'name' => 'Alice',
- 'nickname' => 'alice',
- 'email' => 'public@example.com',
- 'email_verified' => true,
- 'website' => 'https://example.com',
- 'profile' => 'http://localhost/alice',
- 'picture' => "http://localhost/uploads/-/system/user/avatar/#{user.id}/dk.png",
- 'groups' => anything
- }))
+ expect(json_response).to match(id_token_claims.merge(user_info_claims))
expected_groups = [group1.full_path, group3.full_path]
expected_groups << group4.full_path if Group.supports_nested_groups?
expect(json_response['groups']).to match_array(expected_groups)
end
+
+ it 'does not include any unknown claims' do
+ expect(json_response.keys).to eq %w[sub sub_legacy] + user_info_claims.keys
+ end
end
context 'ID token payload' do
before do
- request_access_token
+ request_access_token!
@payload = JSON::JWT.decode(json_response['id_token'], :skip_verification)
end
- it 'includes the Gitlab root URL' do
- expect(@payload['iss']).to eq Gitlab.config.gitlab.url
+ it 'includes the subject claims' do
+ expect(@payload).to match(a_hash_including(id_token_claims))
end
- it 'includes the hashed user ID' do
- expect(@payload['sub']).to eq hashed_subject
+ it 'includes the Gitlab root URL' do
+ expect(@payload['iss']).to eq Gitlab.config.gitlab.url
end
it 'includes the time of the last authentication', :clean_gitlab_redis_shared_state do
@@ -118,7 +130,7 @@ describe 'OpenID Connect requests' do
end
it 'does not include any unknown properties' do
- expect(@payload.keys).to eq %w[iss sub aud exp iat auth_time]
+ expect(@payload.keys).to eq %w[iss sub aud exp iat auth_time sub_legacy]
end
end
@@ -134,10 +146,10 @@ describe 'OpenID Connect requests' do
context 'when user is blocked' do
it 'returns authentication error' do
access_grant
- user.block
+ user.block!
expect do
- request_access_token
+ request_access_token!
end.to raise_error UncaughtThrowError
end
end
@@ -145,10 +157,10 @@ describe 'OpenID Connect requests' do
context 'when user is ldap_blocked' do
it 'returns authentication error' do
access_grant
- user.ldap_block
+ user.ldap_block!
expect do
- request_access_token
+ request_access_token!
end.to raise_error UncaughtThrowError
end
end
diff --git a/spec/requests/rack_attack_global_spec.rb b/spec/requests/rack_attack_global_spec.rb
index b18e922b063..c0a3ea397df 100644
--- a/spec/requests/rack_attack_global_spec.rb
+++ b/spec/requests/rack_attack_global_spec.rb
@@ -349,7 +349,7 @@ describe 'Rack Attack global throttles' do
end
def rss_url(user)
- "/dashboard/projects.atom?rss_token=#{user.rss_token}"
+ "/dashboard/projects.atom?feed_token=#{user.feed_token}"
end
def private_token_headers(user)
diff --git a/spec/routing/api_routing_spec.rb b/spec/routing/api_routing_spec.rb
new file mode 100644
index 00000000000..5fde4bd885b
--- /dev/null
+++ b/spec/routing/api_routing_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe 'api', 'routing' do
+ context 'when graphql is disabled' do
+ before do
+ stub_feature_flags(graphql: false)
+ end
+
+ it 'does not route to the GraphqlController' do
+ expect(get('/api/graphql')).not_to route_to('graphql#execute')
+ end
+
+ it 'does not expose graphiql' do
+ expect(get('/-/graphql-explorer')).not_to route_to('graphiql/rails/editors#show')
+ end
+ end
+
+ context 'when graphql is disabled' do
+ before do
+ stub_feature_flags(graphql: true)
+ end
+
+ it 'routes to the GraphqlController' do
+ expect(get('/api/graphql')).not_to route_to('graphql#execute')
+ end
+
+ it 'exposes graphiql' do
+ expect(get('/-/graphql-explorer')).not_to route_to('graphiql/rails/editors#show')
+ end
+ end
+end
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index e1b4e618092..56d93095a85 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -36,33 +36,36 @@ describe 'project routing' do
shared_examples 'RESTful project resources' do
let(:actions) { [:index, :create, :new, :edit, :show, :update, :destroy] }
let(:controller_path) { controller }
+ let(:id) { { id: '1' } }
+ let(:format) { {} } # response format, e.g. { format: :html }
+ let(:params) { { namespace_id: 'gitlab', project_id: 'gitlabhq' } }
it 'to #index' do
- expect(get("/gitlab/gitlabhq/#{controller_path}")).to route_to("projects/#{controller}#index", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:index)
+ expect(get("/gitlab/gitlabhq/#{controller_path}")).to route_to("projects/#{controller}#index", params) if actions.include?(:index)
end
it 'to #create' do
- expect(post("/gitlab/gitlabhq/#{controller_path}")).to route_to("projects/#{controller}#create", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:create)
+ expect(post("/gitlab/gitlabhq/#{controller_path}")).to route_to("projects/#{controller}#create", params) if actions.include?(:create)
end
it 'to #new' do
- expect(get("/gitlab/gitlabhq/#{controller_path}/new")).to route_to("projects/#{controller}#new", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:new)
+ expect(get("/gitlab/gitlabhq/#{controller_path}/new")).to route_to("projects/#{controller}#new", params) if actions.include?(:new)
end
it 'to #edit' do
- expect(get("/gitlab/gitlabhq/#{controller_path}/1/edit")).to route_to("projects/#{controller}#edit", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:edit)
+ expect(get("/gitlab/gitlabhq/#{controller_path}/1/edit")).to route_to("projects/#{controller}#edit", params.merge(**id, **format)) if actions.include?(:edit)
end
it 'to #show' do
- expect(get("/gitlab/gitlabhq/#{controller_path}/1")).to route_to("projects/#{controller}#show", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:show)
+ expect(get("/gitlab/gitlabhq/#{controller_path}/1")).to route_to("projects/#{controller}#show", params.merge(**id, **format)) if actions.include?(:show)
end
it 'to #update' do
- expect(put("/gitlab/gitlabhq/#{controller_path}/1")).to route_to("projects/#{controller}#update", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:update)
+ expect(put("/gitlab/gitlabhq/#{controller_path}/1")).to route_to("projects/#{controller}#update", params.merge(id)) if actions.include?(:update)
end
it 'to #destroy' do
- expect(delete("/gitlab/gitlabhq/#{controller_path}/1")).to route_to("projects/#{controller}#destroy", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:destroy)
+ expect(delete("/gitlab/gitlabhq/#{controller_path}/1")).to route_to("projects/#{controller}#destroy", params.merge(**id, **format)) if actions.include?(:destroy)
end
end
@@ -150,12 +153,13 @@ describe 'project routing' do
end
it 'to #history' do
- expect(get('/gitlab/gitlabhq/wikis/1/history')).to route_to('projects/wikis#history', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1')
+ expect(get('/gitlab/gitlabhq/wikis/1/history')).to route_to('projects/wikis#history', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', format: :html)
end
it_behaves_like 'RESTful project resources' do
let(:actions) { [:create, :edit, :show, :destroy] }
let(:controller) { 'wikis' }
+ let(:format) { { format: :html } }
end
end
diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb
index 9345671a1a7..dd8f6239587 100644
--- a/spec/routing/routing_spec.rb
+++ b/spec/routing/routing_spec.rb
@@ -162,8 +162,8 @@ describe ProfilesController, "routing" do
expect(get("/profile/audit_log")).to route_to('profiles#audit_log')
end
- it "to #reset_rss_token" do
- expect(put("/profile/reset_rss_token")).to route_to('profiles#reset_rss_token')
+ it "to #reset_feed_token" do
+ expect(put("/profile/reset_feed_token")).to route_to('profiles#reset_feed_token')
end
it "to #show" do
@@ -249,7 +249,11 @@ describe DashboardController, "routing" do
end
it "to #issues" do
- expect(get("/dashboard/issues")).to route_to('dashboard#issues')
+ expect(get("/dashboard/issues.html")).to route_to('dashboard#issues', format: 'html')
+ end
+
+ it "to #calendar_issues" do
+ expect(get("/dashboard/issues.ics")).to route_to('dashboard#issues_calendar', format: 'ics')
end
it "to #merge_requests" do
diff --git a/spec/rubocop/cop/gitlab/finder_with_find_by_spec.rb b/spec/rubocop/cop/gitlab/finder_with_find_by_spec.rb
new file mode 100644
index 00000000000..7f689b196c5
--- /dev/null
+++ b/spec/rubocop/cop/gitlab/finder_with_find_by_spec.rb
@@ -0,0 +1,56 @@
+require 'spec_helper'
+
+require 'rubocop'
+require 'rubocop/rspec/support'
+
+require_relative '../../../../rubocop/cop/gitlab/finder_with_find_by'
+
+describe RuboCop::Cop::Gitlab::FinderWithFindBy do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ context 'when calling execute.find' do
+ let(:source) do
+ <<~SRC
+ DummyFinder.new(some_args)
+ .execute
+ .find_by!(1)
+ SRC
+ end
+ let(:corrected_source) do
+ <<~SRC
+ DummyFinder.new(some_args)
+ .find_by!(1)
+ SRC
+ end
+
+ it 'registers an offence' do
+ inspect_source(source)
+
+ expect(cop.offenses.size).to eq(1)
+ end
+
+ it 'can autocorrect the source' do
+ expect(autocorrect_source(source)).to eq(corrected_source)
+ end
+
+ context 'when called within the `FinderMethods` module' do
+ let(:source) do
+ <<~SRC
+ module FinderMethods
+ def find_by!(*args)
+ execute.find_by!(args)
+ end
+ end
+ SRC
+ end
+
+ it 'does not register an offence' do
+ inspect_source(source)
+
+ expect(cop.offenses).to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/rubocop/cop/line_break_around_conditional_block_spec.rb b/spec/rubocop/cop/line_break_around_conditional_block_spec.rb
index 7ddf9141fcd..03eeffe6483 100644
--- a/spec/rubocop/cop/line_break_around_conditional_block_spec.rb
+++ b/spec/rubocop/cop/line_break_around_conditional_block_spec.rb
@@ -256,6 +256,18 @@ describe RuboCop::Cop::LineBreakAroundConditionalBlock do
expect(cop.offenses).to be_empty
end
+ it "doesn't flag violation for #{conditional} followed by a comment" do
+ source = <<~RUBY
+ #{conditional} condition
+ do_something
+ end
+ # a short comment
+ RUBY
+ inspect_source(source)
+
+ expect(cop.offenses).to be_empty
+ end
+
it "doesn't flag violation for #{conditional} followed by an end" do
source = <<~RUBY
class Foo
diff --git a/spec/rubocop/cop/migration/update_large_table_spec.rb b/spec/rubocop/cop/migration/update_large_table_spec.rb
index ef724fc8bad..5e08eb4f772 100644
--- a/spec/rubocop/cop/migration/update_large_table_spec.rb
+++ b/spec/rubocop/cop/migration/update_large_table_spec.rb
@@ -32,6 +32,14 @@ describe RuboCop::Cop::Migration::UpdateLargeTable do
include_examples 'large tables', 'add_column_with_default'
end
+ context 'for the change_column_type_concurrently method' do
+ include_examples 'large tables', 'change_column_type_concurrently'
+ end
+
+ context 'for the rename_column_concurrently method' do
+ include_examples 'large tables', 'rename_column_concurrently'
+ end
+
context 'for the update_column_in_batches method' do
include_examples 'large tables', 'update_column_in_batches'
end
@@ -60,6 +68,18 @@ describe RuboCop::Cop::Migration::UpdateLargeTable do
expect(cop.offenses).to be_empty
end
+ it 'registers no offense for change_column_type_concurrently' do
+ inspect_source("change_column_type_concurrently :#{table}, :column, default: true")
+
+ expect(cop.offenses).to be_empty
+ end
+
+ it 'registers no offense for update_column_in_batches' do
+ inspect_source("rename_column_concurrently :#{table}, :column, default: true")
+
+ expect(cop.offenses).to be_empty
+ end
+
it 'registers no offense for update_column_in_batches' do
inspect_source("add_column_with_default :#{table}, :column, default: true")
diff --git a/spec/serializers/blob_entity_spec.rb b/spec/serializers/blob_entity_spec.rb
new file mode 100644
index 00000000000..dde59ff72df
--- /dev/null
+++ b/spec/serializers/blob_entity_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe BlobEntity do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+ let(:blob) { project.commit('master').diffs.diff_files.first.blob }
+ let(:request) { EntityRequest.new(project: project, ref: 'master') }
+
+ let(:entity) do
+ described_class.new(blob, request: request)
+ end
+
+ context 'as json' do
+ subject { entity.as_json }
+
+ it 'exposes needed attributes' do
+ expect(subject).to include(:readable_text, :url)
+ end
+ end
+end
diff --git a/spec/serializers/build_serializer_spec.rb b/spec/serializers/build_serializer_spec.rb
index 98cd15e248b..52459cd369d 100644
--- a/spec/serializers/build_serializer_spec.rb
+++ b/spec/serializers/build_serializer_spec.rb
@@ -39,7 +39,7 @@ describe BuildSerializer do
expect(subject[:label]).to eq('failed')
expect(subject[:tooltip]).to eq('failed <br> (unknown failure)')
expect(subject[:icon]).to eq(status.icon)
- expect(subject[:favicon]).to match_asset_path("/assets/ci_favicons/#{status.favicon}.ico")
+ expect(subject[:favicon]).to match_asset_path("/assets/ci_favicons/#{status.favicon}.png")
end
end
@@ -54,7 +54,7 @@ describe BuildSerializer do
expect(subject[:label]).to eq('passed')
expect(subject[:tooltip]).to eq('passed')
expect(subject[:icon]).to eq(status.icon)
- expect(subject[:favicon]).to match_asset_path("/assets/ci_favicons/#{status.favicon}.ico")
+ expect(subject[:favicon]).to match_asset_path("/assets/ci_favicons/#{status.favicon}.png")
end
end
end
diff --git a/spec/serializers/deploy_key_entity_spec.rb b/spec/serializers/deploy_key_entity_spec.rb
index 2bd8162d1b7..01264cf7fb5 100644
--- a/spec/serializers/deploy_key_entity_spec.rb
+++ b/spec/serializers/deploy_key_entity_spec.rb
@@ -44,9 +44,9 @@ describe DeployKeyEntity do
it { expect(entity.as_json).to eq(expected_result) }
end
- describe 'returns can_edit true if user is a master of project' do
+ describe 'returns can_edit true if user is a maintainer of project' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
it { expect(entity.as_json).to include(can_edit: true) }
diff --git a/spec/serializers/diff_file_entity_spec.rb b/spec/serializers/diff_file_entity_spec.rb
index 45d7c703df3..00b2146dc86 100644
--- a/spec/serializers/diff_file_entity_spec.rb
+++ b/spec/serializers/diff_file_entity_spec.rb
@@ -9,16 +9,62 @@ describe DiffFileEntity do
let(:diff_refs) { commit.diff_refs }
let(:diff) { commit.raw_diffs.first }
let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: diff_refs, repository: repository) }
- let(:entity) { described_class.new(diff_file) }
+ let(:entity) { described_class.new(diff_file, request: {}) }
subject { entity.as_json }
- it 'exposes correct attributes' do
- expect(subject).to include(
- :submodule, :submodule_link, :file_path,
- :deleted_file, :old_path, :new_path, :mode_changed,
- :a_mode, :b_mode, :text, :old_path_html,
- :new_path_html
- )
+ shared_examples 'diff file entity' do
+ it 'exposes correct attributes' do
+ expect(subject).to include(
+ :submodule, :submodule_link, :submodule_tree_url, :file_path,
+ :deleted_file, :old_path, :new_path, :mode_changed,
+ :a_mode, :b_mode, :text, :old_path_html,
+ :new_path_html, :highlighted_diff_lines, :parallel_diff_lines,
+ :blob, :file_hash, :added_lines, :removed_lines, :diff_refs, :content_sha,
+ :stored_externally, :external_storage, :too_large, :collapsed, :new_file,
+ :context_lines_path
+ )
+ end
+
+ # Converted diff files from GitHub import does not contain blob file
+ # and content sha.
+ context 'when diff file does not have a blob and content sha' do
+ it 'exposes some attributes as nil' do
+ allow(diff_file).to receive(:content_sha).and_return(nil)
+ allow(diff_file).to receive(:blob).and_return(nil)
+
+ expect(subject[:context_lines_path]).to be_nil
+ expect(subject[:view_path]).to be_nil
+ expect(subject[:highlighted_diff_lines]).to be_nil
+ expect(subject[:can_modify_blob]).to be_nil
+ end
+ end
+ end
+
+ context 'when there is no merge request' do
+ it_behaves_like 'diff file entity'
+ end
+
+ context 'when there is a merge request' do
+ let(:user) { create(:user) }
+ let(:request) { EntityRequest.new(project: project, current_user: user) }
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let(:entity) { described_class.new(diff_file, request: request, merge_request: merge_request) }
+ let(:exposed_urls) { %i(load_collapsed_diff_url edit_path view_path context_lines_path) }
+
+ it_behaves_like 'diff file entity'
+
+ it 'exposes additional attributes' do
+ expect(subject).to include(*exposed_urls)
+ expect(subject).to include(:replaced_view_path)
+ end
+
+ it 'points all urls to merge request target project' do
+ response = subject
+
+ exposed_urls.each do |attribute|
+ expect(response[attribute]).to include(merge_request.target_project.to_param)
+ end
+ end
end
end
diff --git a/spec/serializers/diffs_entity_spec.rb b/spec/serializers/diffs_entity_spec.rb
new file mode 100644
index 00000000000..19a843b0cb7
--- /dev/null
+++ b/spec/serializers/diffs_entity_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe DiffsEntity do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+ let(:request) { EntityRequest.new(project: project, current_user: user) }
+ let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
+ let(:merge_request_diffs) { merge_request.merge_request_diffs }
+
+ let(:entity) do
+ described_class.new(merge_request_diffs.first.diffs, request: request, merge_request: merge_request, merge_request_diffs: merge_request_diffs)
+ end
+
+ context 'as json' do
+ subject { entity.as_json }
+
+ it 'contains needed attributes' do
+ expect(subject).to include(
+ :real_size, :size, :branch_name,
+ :target_branch_name, :commit, :merge_request_diff,
+ :start_version, :latest_diff, :latest_version_path,
+ :added_lines, :removed_lines, :render_overflow_warning,
+ :email_patch_path, :plain_diff_path, :diff_files,
+ :merge_request_diffs
+ )
+ end
+ end
+end
diff --git a/spec/serializers/discussion_entity_spec.rb b/spec/serializers/discussion_entity_spec.rb
index 7e19e74ca00..378540a35b6 100644
--- a/spec/serializers/discussion_entity_spec.rb
+++ b/spec/serializers/discussion_entity_spec.rb
@@ -19,18 +19,61 @@ describe DiscussionEntity do
end
it 'exposes correct attributes' do
- expect(subject).to include(
- :id, :expanded, :notes, :individual_note,
- :resolvable, :resolved, :resolve_path,
- :resolve_with_issue_path, :diff_discussion
+ expect(subject.keys.sort).to include(
+ :diff_discussion,
+ :expanded,
+ :id,
+ :individual_note,
+ :notes,
+ :resolvable,
+ :resolve_path,
+ :resolve_with_issue_path,
+ :resolved,
+ :discussion_path,
+ :resolved_at,
+ :for_commit,
+ :commit_id
)
end
+ context 'when is LegacyDiffDiscussion' do
+ let(:project) { create(:project) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+ let(:discussion) { create(:legacy_diff_note_on_merge_request, noteable: merge_request, project: project).to_discussion }
+
+ it 'exposes correct attributes' do
+ expect(subject.keys.sort).to include(
+ :diff_discussion,
+ :expanded,
+ :id,
+ :individual_note,
+ :notes,
+ :discussion_path,
+ :for_commit,
+ :commit_id
+ )
+ end
+ end
+
context 'when diff file is present' do
let(:note) { create(:diff_note_on_merge_request) }
it 'exposes diff file attributes' do
- expect(subject).to include(:diff_file, :truncated_diff_lines, :image_diff_html)
+ expect(subject.keys.sort).to include(
+ :diff_file,
+ :truncated_diff_lines,
+ :position,
+ :line_code,
+ :active
+ )
+ end
+
+ context 'when diff file is a image' do
+ it 'exposes image attributes' do
+ allow(discussion).to receive(:on_image?).and_return(true)
+
+ expect(subject.keys).to include(:image_diff_html)
+ end
end
end
end
diff --git a/spec/serializers/environment_entity_spec.rb b/spec/serializers/environment_entity_spec.rb
index 8f32c5639a1..b7324a26ed2 100644
--- a/spec/serializers/environment_entity_spec.rb
+++ b/spec/serializers/environment_entity_spec.rb
@@ -1,8 +1,9 @@
require 'spec_helper'
describe EnvironmentEntity do
+ let(:request) { double('request') }
let(:entity) do
- described_class.new(environment, request: double)
+ described_class.new(environment, request: spy('request'))
end
let(:environment) { create(:environment) }
diff --git a/spec/serializers/environment_serializer_spec.rb b/spec/serializers/environment_serializer_spec.rb
index ca9b520fb38..0f0ab5ac796 100644
--- a/spec/serializers/environment_serializer_spec.rb
+++ b/spec/serializers/environment_serializer_spec.rb
@@ -54,7 +54,9 @@ describe EnvironmentSerializer do
context 'when representing environments within folders' do
let(:serializer) do
- described_class.new(project: project).within_folders
+ described_class
+ .new(current_user: user, project: project)
+ .within_folders
end
let(:resource) { Environment.all }
@@ -123,7 +125,8 @@ describe EnvironmentSerializer do
let(:pagination) { { page: 1, per_page: 2 } }
let(:serializer) do
- described_class.new(project: project)
+ described_class
+ .new(current_user: user, project: project)
.with_pagination(request, response)
end
@@ -169,7 +172,8 @@ describe EnvironmentSerializer do
context 'when grouping environments within folders' do
let(:serializer) do
- described_class.new(project: project)
+ described_class
+ .new(current_user: user, project: project)
.with_pagination(request, response)
.within_folders
end
diff --git a/spec/serializers/group_child_entity_spec.rb b/spec/serializers/group_child_entity_spec.rb
index 505a9eaac5a..dbc40bddc30 100644
--- a/spec/serializers/group_child_entity_spec.rb
+++ b/spec/serializers/group_child_entity_spec.rb
@@ -42,7 +42,7 @@ describe GroupChildEntity do
end
before do
- object.add_master(user)
+ object.add_maintainer(user)
end
it 'has the correct type' do
diff --git a/spec/serializers/job_entity_spec.rb b/spec/serializers/job_entity_spec.rb
index c90396ebb28..a5581a34517 100644
--- a/spec/serializers/job_entity_spec.rb
+++ b/spec/serializers/job_entity_spec.rb
@@ -131,7 +131,7 @@ describe JobEntity do
end
context 'when job failed' do
- let(:job) { create(:ci_build, :script_failure) }
+ let(:job) { create(:ci_build, :api_failure) }
it 'contains details' do
expect(subject[:status]).to include :icon, :favicon, :text, :label, :tooltip
@@ -142,20 +142,20 @@ describe JobEntity do
end
it 'should indicate the failure reason on tooltip' do
- expect(subject[:status][:tooltip]).to eq('failed <br> (script failure)')
+ expect(subject[:status][:tooltip]).to eq('failed <br> (API failure)')
end
it 'should include a callout message with a verbose output' do
- expect(subject[:callout_message]).to eq('There has been a script failure. Check the job log for more information')
+ expect(subject[:callout_message]).to eq('There has been an API failure, please try again')
end
it 'should state that it is not recoverable' do
- expect(subject[:recoverable]).to be_falsy
+ expect(subject[:recoverable]).to be_truthy
end
end
context 'when job is allowed to fail' do
- let(:job) { create(:ci_build, :allowed_to_fail, :script_failure) }
+ let(:job) { create(:ci_build, :allowed_to_fail, :api_failure) }
it 'contains details' do
expect(subject[:status]).to include :icon, :favicon, :text, :label, :tooltip
@@ -166,15 +166,24 @@ describe JobEntity do
end
it 'should indicate the failure reason on tooltip' do
- expect(subject[:status][:tooltip]).to eq('failed <br> (script failure) (allowed to fail)')
+ expect(subject[:status][:tooltip]).to eq('failed <br> (API failure) (allowed to fail)')
end
it 'should include a callout message with a verbose output' do
- expect(subject[:callout_message]).to eq('There has been a script failure. Check the job log for more information')
+ expect(subject[:callout_message]).to eq('There has been an API failure, please try again')
end
it 'should state that it is not recoverable' do
- expect(subject[:recoverable]).to be_falsy
+ expect(subject[:recoverable]).to be_truthy
+ end
+ end
+
+ context 'when the job failed with a script failure' do
+ let(:job) { create(:ci_build, :failed, :script_failure) }
+
+ it 'should not include callout message or recoverable keys' do
+ expect(subject).not_to include('callout_message')
+ expect(subject).not_to include('recoverable')
end
end
diff --git a/spec/serializers/merge_request_diff_entity_spec.rb b/spec/serializers/merge_request_diff_entity_spec.rb
new file mode 100644
index 00000000000..84f6833d88a
--- /dev/null
+++ b/spec/serializers/merge_request_diff_entity_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe MergeRequestDiffEntity do
+ let(:project) { create(:project, :repository) }
+ let(:request) { EntityRequest.new(project: project) }
+ let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
+ let(:merge_request_diffs) { merge_request.merge_request_diffs }
+
+ let(:entity) do
+ described_class.new(merge_request_diffs.first, request: request, merge_request: merge_request, merge_request_diffs: merge_request_diffs)
+ end
+
+ context 'as json' do
+ subject { entity.as_json }
+
+ it 'exposes needed attributes' do
+ expect(subject).to include(
+ :version_index, :created_at, :commits_count,
+ :latest, :short_commit_sha, :version_path,
+ :compare_path
+ )
+ end
+ end
+end
diff --git a/spec/serializers/merge_request_user_entity_spec.rb b/spec/serializers/merge_request_user_entity_spec.rb
new file mode 100644
index 00000000000..c91ea4aa681
--- /dev/null
+++ b/spec/serializers/merge_request_user_entity_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe MergeRequestUserEntity do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+ let(:request) { EntityRequest.new(project: project, current_user: user) }
+
+ let(:entity) do
+ described_class.new(user, request: request)
+ end
+
+ context 'as json' do
+ subject { entity.as_json }
+
+ it 'exposes needed attributes' do
+ expect(subject).to include(:can_fork, :can_create_merge_request, :fork_path)
+ end
+ end
+end
diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb
index b741308e2c5..eb4235e3ee6 100644
--- a/spec/serializers/pipeline_serializer_spec.rb
+++ b/spec/serializers/pipeline_serializer_spec.rb
@@ -8,6 +8,10 @@ describe PipelineSerializer do
described_class.new(current_user: user)
end
+ before do
+ stub_feature_flags(ci_pipeline_persisted_stages: true)
+ end
+
subject { serializer.represent(resource) }
describe '#represent' do
@@ -99,7 +103,8 @@ describe PipelineSerializer do
end
end
- context 'number of queries' do
+ describe 'number of queries when preloaded' do
+ subject { serializer.represent(resource, preload: true) }
let(:resource) { Ci::Pipeline.all }
before do
@@ -107,7 +112,7 @@ describe PipelineSerializer do
# gitaly calls in this block
# Issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/37772
Gitlab::GitalyClient.allow_n_plus_1_calls do
- Ci::Pipeline::AVAILABLE_STATUSES.each do |status|
+ Ci::Pipeline::COMPLETED_STATUSES.each do |status|
create_pipeline(status)
end
end
@@ -120,7 +125,7 @@ describe PipelineSerializer do
it 'verifies number of queries', :request_store do
recorded = ActiveRecord::QueryRecorder.new { subject }
- expect(recorded.count).to be_within(1).of(44)
+ expect(recorded.count).to be_within(2).of(27)
expect(recorded.cached_count).to eq(0)
end
end
@@ -139,7 +144,7 @@ describe PipelineSerializer do
# pipeline. With the same ref this check is cached but if refs are
# different then there is an extra query per ref
# https://gitlab.com/gitlab-org/gitlab-ce/issues/46368
- expect(recorded.count).to be_within(1).of(51)
+ expect(recorded.count).to be_within(2).of(30)
expect(recorded.cached_count).to eq(0)
end
end
@@ -174,7 +179,7 @@ describe PipelineSerializer do
expect(subject[:text]).to eq(status.text)
expect(subject[:label]).to eq(status.label)
expect(subject[:icon]).to eq(status.icon)
- expect(subject[:favicon]).to match_asset_path("/assets/ci_favicons/#{status.favicon}.ico")
+ expect(subject[:favicon]).to match_asset_path("/assets/ci_favicons/#{status.favicon}.png")
end
end
end
diff --git a/spec/serializers/runner_entity_spec.rb b/spec/serializers/runner_entity_spec.rb
index 439ba2cbca2..ba99d568eba 100644
--- a/spec/serializers/runner_entity_spec.rb
+++ b/spec/serializers/runner_entity_spec.rb
@@ -1,10 +1,10 @@
require 'spec_helper'
describe RunnerEntity do
- let(:runner) { create(:ci_runner, :specific) }
+ let(:project) { create(:project) }
+ let(:runner) { create(:ci_runner, :project, projects: [project]) }
let(:entity) { described_class.new(runner, request: request, current_user: user) }
let(:request) { double('request') }
- let(:project) { create(:project) }
let(:user) { create(:admin) }
before do
diff --git a/spec/serializers/status_entity_spec.rb b/spec/serializers/status_entity_spec.rb
index 559475e571c..0b010ebd507 100644
--- a/spec/serializers/status_entity_spec.rb
+++ b/spec/serializers/status_entity_spec.rb
@@ -18,17 +18,7 @@ describe StatusEntity do
it 'contains status details' do
expect(subject).to include :text, :icon, :favicon, :label, :group, :tooltip
expect(subject).to include :has_details, :details_path
- expect(subject[:favicon]).to match_asset_path('/assets/ci_favicons/favicon_status_success.ico')
- end
-
- it 'contains a dev namespaced favicon if dev env' do
- allow(Rails.env).to receive(:development?) { true }
- expect(entity.as_json[:favicon]).to match_asset_path('/assets/ci_favicons/dev/favicon_status_success.ico')
- end
-
- it 'contains a canary namespaced favicon if canary env' do
- stub_env('CANARY', 'true')
- expect(entity.as_json[:favicon]).to match_asset_path('/assets/ci_favicons/canary/favicon_status_success.ico')
+ expect(subject[:favicon]).to match_asset_path('/assets/ci_favicons/favicon_status_success.png')
end
end
end
diff --git a/spec/services/application_settings/update_service_spec.rb b/spec/services/application_settings/update_service_spec.rb
index fb07ecc6ae8..6337ee7d724 100644
--- a/spec/services/application_settings/update_service_spec.rb
+++ b/spec/services/application_settings/update_service_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe ApplicationSettings::UpdateService do
- let(:application_settings) { Gitlab::CurrentSettings.current_application_settings }
+ let(:application_settings) { create(:application_setting) }
let(:admin) { create(:user, :admin) }
let(:params) { {} }
@@ -54,4 +54,90 @@ describe ApplicationSettings::UpdateService do
end
end
end
+
+ describe 'performance bar settings' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:params_performance_bar_enabled,
+ :params_performance_bar_allowed_group_path,
+ :previous_performance_bar_allowed_group_id,
+ :expected_performance_bar_allowed_group_id) do
+ true | '' | nil | nil
+ true | '' | 42_000_000 | nil
+ true | nil | nil | nil
+ true | nil | 42_000_000 | nil
+ true | 'foo' | nil | nil
+ true | 'foo' | 42_000_000 | nil
+ true | 'group_a' | nil | 42_000_000
+ true | 'group_b' | 42_000_000 | 43_000_000
+ true | 'group_a' | 42_000_000 | 42_000_000
+ false | '' | nil | nil
+ false | '' | 42_000_000 | nil
+ false | nil | nil | nil
+ false | nil | 42_000_000 | nil
+ false | 'foo' | nil | nil
+ false | 'foo' | 42_000_000 | nil
+ false | 'group_a' | nil | nil
+ false | 'group_b' | 42_000_000 | nil
+ false | 'group_a' | 42_000_000 | nil
+ end
+
+ with_them do
+ let(:params) do
+ {
+ performance_bar_enabled: params_performance_bar_enabled,
+ performance_bar_allowed_group_path: params_performance_bar_allowed_group_path
+ }
+ end
+
+ before do
+ if previous_performance_bar_allowed_group_id == 42_000_000 || params_performance_bar_allowed_group_path == 'group_a'
+ create(:group, id: 42_000_000, path: 'group_a')
+ end
+
+ if expected_performance_bar_allowed_group_id == 43_000_000 || params_performance_bar_allowed_group_path == 'group_b'
+ create(:group, id: 43_000_000, path: 'group_b')
+ end
+
+ application_settings.update!(performance_bar_allowed_group_id: previous_performance_bar_allowed_group_id)
+ end
+
+ it 'sets performance_bar_allowed_group_id when present and performance_bar_enabled == true' do
+ expect(application_settings.performance_bar_allowed_group_id).to eq(previous_performance_bar_allowed_group_id)
+
+ if previous_performance_bar_allowed_group_id != expected_performance_bar_allowed_group_id
+ expect { subject.execute }
+ .to change(application_settings, :performance_bar_allowed_group_id)
+ .from(previous_performance_bar_allowed_group_id).to(expected_performance_bar_allowed_group_id)
+ else
+ expect { subject.execute }
+ .not_to change(application_settings, :performance_bar_allowed_group_id)
+ end
+ end
+ end
+
+ context 'when :performance_bar_allowed_group_path is not present' do
+ let(:group) { create(:group) }
+
+ before do
+ application_settings.update!(performance_bar_allowed_group_id: group.id)
+ end
+
+ it 'does not change the performance bar settings' do
+ expect { subject.execute }
+ .not_to change(application_settings, :performance_bar_allowed_group_id)
+ end
+ end
+
+ context 'when :performance_bar_enabled is not present' do
+ let(:group) { create(:group) }
+ let(:params) { { performance_bar_allowed_group_path: group.full_path } }
+
+ it 'implicitely defaults to true' do
+ expect { subject.execute }
+ .to change(application_settings, :performance_bar_allowed_group_id)
+ .from(nil).to(group.id)
+ end
+ end
+ 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 da8e660c16b..037484931b8 100644
--- a/spec/services/auth/container_registry_authentication_service_spec.rb
+++ b/spec/services/auth/container_registry_authentication_service_spec.rb
@@ -21,6 +21,11 @@ describe Auth::ContainerRegistryAuthenticationService do
allow_any_instance_of(JSONWebToken::RSAToken).to receive(:key).and_return(rsa_key)
end
+ shared_examples 'an authenticated' do
+ it { is_expected.to include(:token) }
+ it { expect(payload).to include('access') }
+ end
+
shared_examples 'a valid token' do
it { is_expected.to include(:token) }
it { expect(payload).to include('access') }
@@ -343,7 +348,7 @@ describe Auth::ContainerRegistryAuthenticationService do
end
end
- context 'delete authorized as master' do
+ context 'delete authorized as maintainer' do
let(:current_project) { create(:project) }
let(:current_user) { create(:user) }
@@ -352,7 +357,7 @@ describe Auth::ContainerRegistryAuthenticationService do
end
before do
- current_project.add_master(current_user)
+ current_project.add_maintainer(current_user)
end
it_behaves_like 'a valid token'
@@ -380,6 +385,14 @@ describe Auth::ContainerRegistryAuthenticationService do
current_project.add_developer(current_user)
end
+ context 'allow to use offline_token' do
+ let(:current_params) do
+ { offline_token: true }
+ end
+
+ it_behaves_like 'an authenticated'
+ end
+
it_behaves_like 'a valid token'
context 'allow to pull and push images' do
diff --git a/spec/services/check_gcp_project_billing_service_spec.rb b/spec/services/check_gcp_project_billing_service_spec.rb
deleted file mode 100644
index 3e68d906e71..00000000000
--- a/spec/services/check_gcp_project_billing_service_spec.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-require 'spec_helper'
-
-describe CheckGcpProjectBillingService do
- include GoogleApi::CloudPlatformHelpers
-
- let(:service) { described_class.new }
- let(:project_id) { 'test-project-1234' }
-
- describe '#execute' do
- before do
- stub_cloud_platform_projects_list(project_id: project_id)
- end
-
- subject { service.execute('bogustoken') }
-
- context 'google account has a billing enabled gcp project' do
- before do
- stub_cloud_platform_projects_get_billing_info(project_id, true)
- end
-
- it { is_expected.to all(satisfy { |project| project.project_id == project_id }) }
- end
-
- context 'google account does not have a billing enabled gcp project' do
- before do
- stub_cloud_platform_projects_get_billing_info(project_id, false)
- end
-
- it { is_expected.to eq([]) }
- end
- end
-end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 2b88fcc9a96..054b7b1561c 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -462,11 +462,11 @@ describe Ci::CreatePipelineService do
end
end
- context 'when user is master' do
+ context 'when user is maintainer' do
let(:pipeline) { execute_service }
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
it 'creates a protected pipeline' do
@@ -503,13 +503,13 @@ describe Ci::CreatePipelineService do
end
end
- context 'when trigger belongs to a master' do
+ context 'when trigger belongs to a maintainer' do
let(:user) { create(:user) }
let(:trigger) { create(:ci_trigger, owner: user) }
let(:trigger_request) { create(:ci_trigger_request, trigger: trigger) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
it 'creates a pipeline' do
diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb
index 8063bc7e1ac..dbb5e33bbdc 100644
--- a/spec/services/ci/register_job_service_spec.rb
+++ b/spec/services/ci/register_job_service_spec.rb
@@ -5,15 +5,11 @@ module Ci
set(:group) { create(:group) }
set(:project) { create(:project, group: group, shared_runners_enabled: false, group_runners_enabled: false) }
set(:pipeline) { create(:ci_pipeline, project: project) }
- let!(:shared_runner) { create(:ci_runner, is_shared: true) }
- let!(:specific_runner) { create(:ci_runner, is_shared: false) }
- let!(:group_runner) { create(:ci_runner, groups: [group], runner_type: :group_type) }
+ let!(:shared_runner) { create(:ci_runner, :instance) }
+ let!(:specific_runner) { create(:ci_runner, :project, projects: [project]) }
+ let!(:group_runner) { create(:ci_runner, :group, groups: [group]) }
let!(:pending_job) { create(:ci_build, pipeline: pipeline) }
- before do
- specific_runner.assign_to(project)
- end
-
describe '#execute' do
context 'runner follow tag list' do
it "picks build with the same tag" do
@@ -181,24 +177,24 @@ module Ci
end
context 'for multiple builds' do
- let!(:project2) { create :project, group_runners_enabled: true, group: group }
- let!(:pipeline2) { create :ci_pipeline, project: project2 }
- let!(:project3) { create :project, group_runners_enabled: true, group: group }
- let!(:pipeline3) { create :ci_pipeline, project: project3 }
+ let!(:project2) { create(:project, group_runners_enabled: true, group: group) }
+ let!(:pipeline2) { create(:ci_pipeline, project: project2) }
+ let!(:project3) { create(:project, group_runners_enabled: true, group: group) }
+ let!(:pipeline3) { create(:ci_pipeline, project: project3) }
let!(:build1_project1) { pending_job }
- let!(:build2_project1) { create :ci_build, pipeline: pipeline }
- let!(:build3_project1) { create :ci_build, pipeline: pipeline }
- let!(:build1_project2) { create :ci_build, pipeline: pipeline2 }
- let!(:build2_project2) { create :ci_build, pipeline: pipeline2 }
- let!(:build1_project3) { create :ci_build, pipeline: pipeline3 }
+ let!(:build2_project1) { create(:ci_build, pipeline: pipeline) }
+ let!(:build3_project1) { create(:ci_build, pipeline: pipeline) }
+ let!(:build1_project2) { create(:ci_build, pipeline: pipeline2) }
+ let!(:build2_project2) { create(:ci_build, pipeline: pipeline2) }
+ let!(:build1_project3) { create(:ci_build, pipeline: pipeline3) }
# these shouldn't influence the scheduling
- let!(:unrelated_group) { create :group }
- let!(:unrelated_project) { create :project, group_runners_enabled: true, group: unrelated_group }
- let!(:unrelated_pipeline) { create :ci_pipeline, project: unrelated_project }
- let!(:build1_unrelated_project) { create :ci_build, pipeline: unrelated_pipeline }
- let!(:unrelated_group_runner) { create :ci_runner, groups: [unrelated_group] }
+ let!(:unrelated_group) { create(:group) }
+ let!(:unrelated_project) { create(:project, group_runners_enabled: true, group: unrelated_group) }
+ let!(:unrelated_pipeline) { create(:ci_pipeline, project: unrelated_project) }
+ let!(:build1_unrelated_project) { create(:ci_build, pipeline: unrelated_pipeline) }
+ let!(:unrelated_group_runner) { create(:ci_runner, :group, groups: [unrelated_group]) }
it 'does not consider builds from other group runners' do
expect(described_class.new(group_runner).send(:builds_for_group_runner).count).to eq 6
@@ -292,7 +288,7 @@ module Ci
end
context 'when access_level of runner is not_protected' do
- let!(:specific_runner) { create(:ci_runner, :specific) }
+ let!(:specific_runner) { create(:ci_runner, :project, projects: [project]) }
context 'when a job is protected' do
let!(:pending_job) { create(:ci_build, :protected, pipeline: pipeline) }
@@ -324,7 +320,7 @@ module Ci
end
context 'when access_level of runner is ref_protected' do
- let!(:specific_runner) { create(:ci_runner, :ref_protected, :specific) }
+ let!(:specific_runner) { create(:ci_runner, :project, :ref_protected, projects: [project]) }
context 'when a job is protected' do
let!(:pending_job) { create(:ci_build, :protected, pipeline: pipeline) }
@@ -552,8 +548,21 @@ module Ci
end
end
- def execute(runner)
- described_class.new(runner).execute.build
+ context 'when runner_session params are' do
+ it 'present sets runner session configuration in the build' do
+ runner_session_params = { session: { 'url' => 'https://example.com' } }
+
+ expect(execute(specific_runner, runner_session_params).runner_session.attributes)
+ .to include(runner_session_params[:session])
+ end
+
+ it 'not present it does not configure the runner session' do
+ expect(execute(specific_runner).runner_session).to be_nil
+ end
+ end
+
+ def execute(runner, params = {})
+ described_class.new(runner).execute(params).build
end
end
end
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index e1cb7ed8110..ecf5d849d3f 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -32,7 +32,7 @@ describe Ci::RetryBuildService do
runner_id tag_taggings taggings tags trigger_request_id
user_id auto_canceled_by_id retried failure_reason
artifacts_file_store artifacts_metadata_store
- metadata trace_chunks].freeze
+ metadata runner_session trace_chunks].freeze
shared_examples 'build duplication' do
let(:another_pipeline) { create(:ci_empty_pipeline, project: project) }
@@ -49,7 +49,7 @@ describe Ci::RetryBuildService do
# Make sure that build has both `stage_id` and `stage` because FactoryBot
# can reset one of the fields when assigning another. We plan to deprecate
# and remove legacy `stage` column in the future.
- build.update_attributes(stage: 'test', stage_id: stage.id)
+ build.update(stage: 'test', stage_id: stage.id)
end
describe 'clone accessors' do
@@ -100,7 +100,11 @@ describe Ci::RetryBuildService do
end
describe '#execute' do
- let(:new_build) { service.execute(build) }
+ let(:new_build) do
+ Timecop.freeze(1.second.from_now) do
+ service.execute(build)
+ end
+ end
context 'when user has ability to execute build' do
before do
@@ -150,7 +154,11 @@ describe Ci::RetryBuildService do
end
describe '#reprocess' do
- let(:new_build) { service.reprocess!(build) }
+ let(:new_build) do
+ Timecop.freeze(1.second.from_now) do
+ service.reprocess!(build)
+ end
+ end
context 'when user has ability to execute build' do
before do
diff --git a/spec/services/ci/retry_pipeline_service_spec.rb b/spec/services/ci/retry_pipeline_service_spec.rb
index a73bd7a0268..55445e71539 100644
--- a/spec/services/ci/retry_pipeline_service_spec.rb
+++ b/spec/services/ci/retry_pipeline_service_spec.rb
@@ -237,7 +237,7 @@ describe Ci::RetryPipelineService, '#execute' do
context 'when user is not allowed to trigger manual action' do
before do
project.add_developer(user)
- create(:protected_branch, :masters_can_push,
+ create(:protected_branch, :maintainers_can_push,
name: pipeline.ref, project: project)
end
@@ -275,17 +275,17 @@ describe Ci::RetryPipelineService, '#execute' do
let(:pipeline) { create(:ci_pipeline, project: forked_project, ref: 'fixes') }
before do
- project.add_master(user)
+ project.add_maintainer(user)
create(:merge_request,
source_project: forked_project,
target_project: project,
source_branch: 'fixes',
- allow_maintainer_to_push: true)
+ allow_collaboration: true)
create_build('rspec 1', :failed, 1)
end
it 'allows to retry failed pipeline' do
- allow_any_instance_of(Project).to receive(:fetch_branch_allows_maintainer_push?).and_return(true)
+ allow_any_instance_of(Project).to receive(:fetch_branch_allows_collaboration?).and_return(true)
allow_any_instance_of(Project).to receive(:empty_repo?).and_return(false)
service.execute(pipeline)
diff --git a/spec/services/ci/stop_environments_service_spec.rb b/spec/services/ci/stop_environments_service_spec.rb
index 3fc4e499b0c..cdd3d851f61 100644
--- a/spec/services/ci/stop_environments_service_spec.rb
+++ b/spec/services/ci/stop_environments_service_spec.rb
@@ -86,7 +86,7 @@ describe Ci::StopEnvironmentsService do
context 'when user has permission to stop environments' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
it 'does not stop environment' do
diff --git a/spec/services/ci/update_build_queue_service_spec.rb b/spec/services/ci/update_build_queue_service_spec.rb
index 74a23ed2a3f..ca0c6be5da6 100644
--- a/spec/services/ci/update_build_queue_service_spec.rb
+++ b/spec/services/ci/update_build_queue_service_spec.rb
@@ -6,19 +6,18 @@ describe Ci::UpdateBuildQueueService do
let(:pipeline) { create(:ci_pipeline, project: project) }
context 'when updating specific runners' do
- let(:runner) { create(:ci_runner) }
+ let(:runner) { create(:ci_runner, :project, projects: [project]) }
context 'when there is a runner that can pick build' do
- before do
- build.project.runners << runner
- end
-
it 'ticks runner queue value' do
expect { subject.execute(build) }.to change { runner.ensure_runner_queue_value }
end
end
context 'when there is no runner that can pick build' do
+ let(:another_project) { create(:project) }
+ let(:runner) { create(:ci_runner, :project, projects: [another_project]) }
+
it 'does not tick runner queue value' do
expect { subject.execute(build) }.not_to change { runner.ensure_runner_queue_value }
end
@@ -26,7 +25,7 @@ describe Ci::UpdateBuildQueueService do
end
context 'when updating shared runners' do
- let(:runner) { create(:ci_runner, :shared) }
+ let(:runner) { create(:ci_runner, :instance) }
context 'when there is no runner that can pick build' do
it 'ticks runner queue value' do
@@ -56,9 +55,9 @@ describe Ci::UpdateBuildQueueService do
end
context 'when updating group runners' do
- let(:group) { create :group }
- let(:project) { create :project, group: group }
- let(:runner) { create :ci_runner, groups: [group] }
+ let(:group) { create(:group) }
+ let(:project) { create(:project, group: group) }
+ let(:runner) { create(:ci_runner, :group, groups: [group]) }
context 'when there is a runner that can pick build' do
it 'ticks runner queue value' do
diff --git a/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb b/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb
index bf038595a4d..eb0bdb61ee3 100644
--- a/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb
+++ b/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb
@@ -1,11 +1,13 @@
require 'spec_helper'
describe Clusters::Applications::CheckIngressIpAddressService do
+ include ExclusiveLeaseHelpers
+
let(:application) { create(:clusters_applications_ingress, :installed) }
let(:service) { described_class.new(application) }
let(:kubeclient) { double(::Kubeclient::Client, get_service: kube_service) }
let(:ingress) { [{ ip: '111.222.111.222' }] }
- let(:exclusive_lease) { instance_double(Gitlab::ExclusiveLease, try_obtain: true) }
+ let(:lease_key) { "check_ingress_ip_address_service:#{application.id}" }
let(:kube_service) do
::Kubeclient::Resource.new(
@@ -22,11 +24,8 @@ describe Clusters::Applications::CheckIngressIpAddressService do
subject { service.execute }
before do
+ stub_exclusive_lease(lease_key, timeout: 15.seconds.to_i)
allow(application.cluster).to receive(:kubeclient).and_return(kubeclient)
- allow(Gitlab::ExclusiveLease)
- .to receive(:new)
- .with("check_ingress_ip_address_service:#{application.id}", timeout: 15.seconds.to_i)
- .and_return(exclusive_lease)
end
describe '#execute' do
@@ -47,13 +46,9 @@ describe Clusters::Applications::CheckIngressIpAddressService do
end
context 'when the exclusive lease cannot be obtained' do
- before do
- allow(exclusive_lease)
- .to receive(:try_obtain)
- .and_return(false)
- end
-
it 'does not call kubeclient' do
+ stub_exclusive_lease_taken(lease_key, timeout: 15.seconds.to_i)
+
subject
expect(kubeclient).not_to have_received(:get_service)
diff --git a/spec/services/clusters/applications/schedule_installation_service_spec.rb b/spec/services/clusters/applications/schedule_installation_service_spec.rb
index 473dbcbb692..bca1e71bef2 100644
--- a/spec/services/clusters/applications/schedule_installation_service_spec.rb
+++ b/spec/services/clusters/applications/schedule_installation_service_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Clusters::Applications::ScheduleInstallationService do
def count_scheduled
- application_class&.with_status(:scheduled)&.count || 0
+ application&.class&.with_status(:scheduled)&.count || 0
end
shared_examples 'a failing service' do
@@ -10,45 +10,42 @@ describe Clusters::Applications::ScheduleInstallationService do
expect(ClusterInstallAppWorker).not_to receive(:perform_async)
count_before = count_scheduled
- expect { service.execute }.to raise_error(StandardError)
+ expect { service.execute(application) }.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) }
+ let(:project) { double(:project) }
+ let(:service) { described_class.new(project, nil) }
- it 'creates a new application' do
- allow(ClusterInstallAppWorker).to receive(:perform_async)
+ context 'when application is installable' do
+ let(:application) { create(:clusters_applications_helm, :installable) }
- 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
+ it 'make the application scheduled' do
+ expect(ClusterInstallAppWorker).to receive(:perform_async).with(application.name, kind_of(Numeric)).once
- expect { service.execute }.to change { application_class.with_status(:scheduled).count }.by(1)
+ expect { service.execute(application) }.to change { application.class.with_status(:scheduled).count }.by(1)
+ end
end
context 'when installation is already in progress' do
let(:application) { create(:clusters_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 }
+ context 'when application is nil' do
+ let(:application) { nil }
it_behaves_like 'a failing service'
end
context 'when application cannot be persisted' do
+ let(:application) { create(:clusters_applications_helm) }
+
before do
- expect_any_instance_of(application_class).to receive(:make_scheduled!).once.and_raise(ActiveRecord::RecordInvalid)
+ expect(application).to receive(:make_scheduled!).once.and_raise(ActiveRecord::RecordInvalid)
end
it_behaves_like 'a failing service'
diff --git a/spec/services/discussions/resolve_service_spec.rb b/spec/services/discussions/resolve_service_spec.rb
index 3895a0b3aea..4e0d4749239 100644
--- a/spec/services/discussions/resolve_service_spec.rb
+++ b/spec/services/discussions/resolve_service_spec.rb
@@ -9,7 +9,7 @@ describe Discussions::ResolveService do
let(:service) { described_class.new(discussion.noteable.project, user, merge_request: merge_request) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
it "doesn't resolve discussions the user can't resolve" do
diff --git a/spec/services/files/create_service_spec.rb b/spec/services/files/create_service_spec.rb
index abe99b9e794..30d94e4318d 100644
--- a/spec/services/files/create_service_spec.rb
+++ b/spec/services/files/create_service_spec.rb
@@ -23,7 +23,7 @@ describe Files::CreateService do
subject { described_class.new(project, user, commit_params) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
describe "#execute" do
diff --git a/spec/services/files/delete_service_spec.rb b/spec/services/files/delete_service_spec.rb
index ace5f293097..73566afe8c8 100644
--- a/spec/services/files/delete_service_spec.rb
+++ b/spec/services/files/delete_service_spec.rb
@@ -37,7 +37,7 @@ describe Files::DeleteService do
end
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
describe "#execute" do
diff --git a/spec/services/files/multi_service_spec.rb b/spec/services/files/multi_service_spec.rb
index 59984c10990..3bdedaf3770 100644
--- a/spec/services/files/multi_service_spec.rb
+++ b/spec/services/files/multi_service_spec.rb
@@ -38,7 +38,7 @@ describe Files::MultiService do
end
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
describe '#execute' do
diff --git a/spec/services/files/update_service_spec.rb b/spec/services/files/update_service_spec.rb
index 16bfbdf3089..e01fe487ffa 100644
--- a/spec/services/files/update_service_spec.rb
+++ b/spec/services/files/update_service_spec.rb
@@ -24,7 +24,7 @@ describe Files::UpdateService do
end
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
describe "#execute" do
@@ -71,17 +71,5 @@ describe Files::UpdateService do
expect(results.data).to eq(new_contents)
end
end
-
- context 'with gitaly disabled', :skip_gitaly_mock do
- context 'when target branch is different than source branch' do
- let(:branch_name) { "#{project.default_branch}-new" }
-
- it 'fires hooks only once' do
- expect(Gitlab::Git::HooksService).to receive(:new).once.and_call_original
-
- subject.execute
- end
- end
- end
end
end
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index 35826de5814..3f62e7e61e1 100644
--- a/spec/services/git_push_service_spec.rb
+++ b/spec/services/git_push_service_spec.rb
@@ -11,7 +11,7 @@ describe GitPushService, services: true do
let(:ref) { 'refs/heads/master' }
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
describe 'with remote mirrors' do
@@ -267,8 +267,8 @@ describe GitPushService, services: true do
expect(project.default_branch).to eq("master")
execute_service(project, user, blankrev, 'newrev', ref)
expect(project.protected_branches).not_to be_empty
- expect(project.protected_branches.first.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
- expect(project.protected_branches.first.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
+ expect(project.protected_branches.first.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::MAINTAINER])
+ expect(project.protected_branches.first.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MAINTAINER])
end
it "when pushing a branch for the first time with default branch protection disabled" do
@@ -290,7 +290,7 @@ describe GitPushService, services: true do
expect(project.protected_branches).not_to be_empty
expect(project.protected_branches.last.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::DEVELOPER])
- expect(project.protected_branches.last.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
+ expect(project.protected_branches.last.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MAINTAINER])
end
it "when pushing a branch for the first time with an existing branch permission configured" do
@@ -315,7 +315,7 @@ describe GitPushService, services: true do
expect(project.default_branch).to eq("master")
execute_service(project, user, blankrev, 'newrev', ref)
expect(project.protected_branches).not_to be_empty
- expect(project.protected_branches.first.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
+ expect(project.protected_branches.first.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::MAINTAINER])
expect(project.protected_branches.first.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::DEVELOPER])
end
@@ -442,7 +442,7 @@ describe GitPushService, services: true do
allow_any_instance_of(ProcessCommitWorker).to receive(:build_commit)
.and_return(closing_commit)
- project.add_master(commit_author)
+ project.add_maintainer(commit_author)
end
context "to default branches" do
diff --git a/spec/services/git_tag_push_service_spec.rb b/spec/services/git_tag_push_service_spec.rb
index 33405d7a7ec..92159e1e372 100644
--- a/spec/services/git_tag_push_service_spec.rb
+++ b/spec/services/git_tag_push_service_spec.rb
@@ -118,7 +118,9 @@ describe GitTagPushService do
before do
# Create the lightweight tag
- project.repository.raw_repository.rugged.tags.create(tag_name, newrev)
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ project.repository.raw_repository.rugged.tags.create(tag_name, newrev)
+ end
# Clear tag list cache
project.repository.expire_tags_cache
diff --git a/spec/services/groups/update_service_spec.rb b/spec/services/groups/update_service_spec.rb
index 1737fd0a9fc..48d689e11d4 100644
--- a/spec/services/groups/update_service_spec.rb
+++ b/spec/services/groups/update_service_spec.rb
@@ -12,7 +12,7 @@ describe Groups::UpdateService do
let!(:service) { described_class.new(public_group, user, visibility_level: Gitlab::VisibilityLevel::INTERNAL) }
before do
- public_group.add_user(user, Gitlab::Access::MASTER)
+ public_group.add_user(user, Gitlab::Access::MAINTAINER)
create(:project, :public, group: public_group)
end
@@ -26,7 +26,7 @@ describe Groups::UpdateService do
let!(:service) { described_class.new(internal_group, user, visibility_level: Gitlab::VisibilityLevel::PRIVATE) }
before do
- internal_group.add_user(user, Gitlab::Access::MASTER)
+ internal_group.add_user(user, Gitlab::Access::MAINTAINER)
create(:project, :internal, group: internal_group)
end
@@ -55,7 +55,7 @@ describe Groups::UpdateService do
context "unauthorized visibility_level validation" do
let!(:service) { described_class.new(internal_group, user, visibility_level: 99) }
before do
- internal_group.add_user(user, Gitlab::Access::MASTER)
+ internal_group.add_user(user, Gitlab::Access::MAINTAINER)
end
it "does not change permission level" do
@@ -68,7 +68,7 @@ describe Groups::UpdateService do
let!(:service) { described_class.new(internal_group, user, path: SecureRandom.hex) }
before do
- internal_group.add_user(user, Gitlab::Access::MASTER)
+ internal_group.add_user(user, Gitlab::Access::MAINTAINER)
create(:project, :internal, group: internal_group)
end
diff --git a/spec/services/import_export_clean_up_service_spec.rb b/spec/services/import_export_clean_up_service_spec.rb
index 1875d0448cd..d5fcef1246f 100644
--- a/spec/services/import_export_clean_up_service_spec.rb
+++ b/spec/services/import_export_clean_up_service_spec.rb
@@ -11,7 +11,6 @@ describe ImportExportCleanUpService do
path = '/invalid/path/'
stub_repository_downloads_path(path)
- expect(File).to receive(:directory?).with(path + tmp_import_export_folder).and_return(false).at_least(:once)
expect(service).not_to receive(:clean_up_export_files)
service.execute
@@ -38,6 +37,24 @@ describe ImportExportCleanUpService do
end
end
+ context 'with uploader exports' do
+ it 'removes old files' do
+ upload = create(:import_export_upload,
+ updated_at: 2.days.ago,
+ export_file: fixture_file_upload('spec/fixtures/project_export.tar.gz'))
+
+ expect { service.execute }.to change { upload.reload.export_file.file.nil? }.to(true)
+ end
+
+ it 'does not remove new files' do
+ upload = create(:import_export_upload,
+ updated_at: 1.hour.ago,
+ export_file: fixture_file_upload('spec/fixtures/project_export.tar.gz'))
+
+ expect { service.execute }.not_to change { upload.reload.export_file.file.nil? }
+ end
+ end
+
def in_directory_with_files(mtime:)
Dir.mktmpdir do |tmpdir|
stub_repository_downloads_path(tmpdir)
diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb
index 7ae49c06896..5e38d0aeb6a 100644
--- a/spec/services/issues/close_service_spec.rb
+++ b/spec/services/issues/close_service_spec.rb
@@ -9,7 +9,7 @@ describe Issues::CloseService do
let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
project.add_developer(user2)
project.add_guest(guest)
end
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index 79bcdc41fb0..c61c1ddcb3d 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -13,8 +13,8 @@ describe Issues::CreateService do
let(:labels) { create_pair(:label, project: project) }
before do
- project.add_master(user)
- project.add_master(assignee)
+ project.add_maintainer(user)
+ project.add_maintainer(assignee)
end
let(:opts) do
@@ -130,7 +130,7 @@ describe Issues::CreateService do
end
it 'invalidates open issues counter for assignees when issue is assigned' do
- project.add_master(assignee)
+ project.add_maintainer(assignee)
described_class.new(project, user, opts).execute
@@ -160,7 +160,7 @@ describe Issues::CreateService do
context 'issue create service' do
context 'assignees' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
it 'removes assignee when user id is invalid' do
@@ -180,7 +180,7 @@ describe Issues::CreateService do
end
it 'saves assignee when user id is valid' do
- project.add_master(assignee)
+ project.add_maintainer(assignee)
opts = { title: 'Title', description: 'Description', assignee_ids: [assignee.id] }
issue = described_class.new(project, user, opts).execute
@@ -224,8 +224,8 @@ describe Issues::CreateService do
end
before do
- project.add_master(user)
- project.add_master(assignee)
+ project.add_maintainer(user)
+ project.add_maintainer(assignee)
end
it 'assigns and sets milestone to issuable from command' do
@@ -242,7 +242,7 @@ describe Issues::CreateService do
let(:project) { merge_request.source_project }
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
describe 'for a single discussion' do
diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb
index a9aee9e100f..609eef76d2c 100644
--- a/spec/services/issues/move_service_spec.rb
+++ b/spec/services/issues/move_service_spec.rb
@@ -5,8 +5,11 @@ describe Issues::MoveService do
let(:author) { create(:user) }
let(:title) { 'Some issue' }
let(:description) { 'Some issue description' }
- let(:old_project) { create(:project) }
- let(:new_project) { create(:project) }
+ let(:group) { create(:group, :private) }
+ let(:sub_group_1) { create(:group, :private, parent: group) }
+ let(:sub_group_2) { create(:group, :private, parent: group) }
+ let(:old_project) { create(:project, namespace: sub_group_1) }
+ let(:new_project) { create(:project, namespace: sub_group_2) }
let(:milestone1) { create(:milestone, project_id: old_project.id, title: 'v9.0') }
let(:old_issue) do
@@ -14,7 +17,7 @@ describe Issues::MoveService do
project: old_project, author: author, milestone: milestone1)
end
- let(:move_service) do
+ subject(:move_service) do
described_class.new(old_project, user)
end
@@ -102,6 +105,23 @@ describe Issues::MoveService do
end
end
+ context 'issue with group labels', :nested_groups do
+ it 'assigns group labels to new issue' do
+ label = create(:group_label, group: group)
+ label_issue = create(:labeled_issue, description: description, project: old_project,
+ milestone: milestone1, labels: [label])
+ old_project.add_reporter(user)
+ new_project.add_reporter(user)
+
+ new_issue = move_service.execute(label_issue, new_project)
+
+ expect(new_issue).to have_attributes(
+ project: new_project,
+ labels: include(label)
+ )
+ end
+ end
+
context 'generic issue' do
include_context 'issue move executed'
diff --git a/spec/services/issues/reopen_service_spec.rb b/spec/services/issues/reopen_service_spec.rb
index 42e5d544f4c..3b617d4ca54 100644
--- a/spec/services/issues/reopen_service_spec.rb
+++ b/spec/services/issues/reopen_service_spec.rb
@@ -24,7 +24,7 @@ describe Issues::ReopenService do
let(:user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
it 'invalidates counter cache for assignees' do
diff --git a/spec/services/issues/resolve_discussions_spec.rb b/spec/services/issues/resolve_discussions_spec.rb
index 13accc6ae1b..b6cfc09da65 100644
--- a/spec/services/issues/resolve_discussions_spec.rb
+++ b/spec/services/issues/resolve_discussions_spec.rb
@@ -31,10 +31,8 @@ describe Issues::ResolveDiscussions do
it "only queries for the merge request once" do
fake_finder = double
- fake_results = double
- expect(fake_finder).to receive(:execute).and_return(fake_results).exactly(1)
- expect(fake_results).to receive(:find_by).exactly(1)
+ expect(fake_finder).to receive(:find_by).exactly(1)
expect(MergeRequestsFinder).to receive(:new).and_return(fake_finder).exactly(1)
2.times { service.merge_request_to_resolve_discussions_of }
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 23b1134b5a3..fa98d05c61b 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -18,7 +18,7 @@ describe Issues::UpdateService, :mailer do
end
before do
- project.add_master(user)
+ project.add_maintainer(user)
project.add_developer(user2)
project.add_developer(user3)
end
@@ -337,12 +337,18 @@ describe Issues::UpdateService, :mailer do
context 'when the labels change' do
before do
- update_issue(label_ids: [label.id])
+ Timecop.freeze(1.minute.from_now) do
+ update_issue(label_ids: [label.id])
+ end
end
it 'marks todos as done' do
expect(todo.reload.done?).to eq true
end
+
+ it 'updates updated_at' do
+ expect(issue.reload.updated_at).to be > Time.now
+ end
end
end
@@ -495,7 +501,7 @@ describe Issues::UpdateService, :mailer do
let(:params) { { label_ids: [], remove_label_ids: [label.id] } }
before do
- issue.update_attributes(labels: [label, label3])
+ issue.update(labels: [label, label3])
end
it 'ignores the label_ids parameter' do
@@ -511,7 +517,7 @@ describe Issues::UpdateService, :mailer do
let(:params) { { add_label_ids: [label3.id], remove_label_ids: [label.id] } }
before do
- issue.update_attributes(labels: [label])
+ issue.update(labels: [label])
end
it 'adds the passed labels' do
@@ -590,7 +596,7 @@ describe Issues::UpdateService, :mailer do
context 'valid project' do
before do
- target_project.add_master(user)
+ target_project.add_maintainer(user)
end
it 'calls the move service with the proper issue and project' do
diff --git a/spec/services/keys/last_used_service_spec.rb b/spec/services/keys/last_used_service_spec.rb
index bb0fb6acf39..8e553c2f1fa 100644
--- a/spec/services/keys/last_used_service_spec.rb
+++ b/spec/services/keys/last_used_service_spec.rb
@@ -8,7 +8,7 @@ describe Keys::LastUsedService do
Timecop.freeze(time) { described_class.new(key).execute }
- expect(key.last_used_at).to eq(time)
+ expect(key.reload.last_used_at).to be_like_time(time)
end
it 'does not update the key when it has been used recently' do
@@ -17,7 +17,7 @@ describe Keys::LastUsedService do
described_class.new(key).execute
- expect(key.last_used_at).to eq(time)
+ expect(key.last_used_at).to be_like_time(time)
end
it 'does not update the updated_at field' do
diff --git a/spec/services/labels/find_or_create_service_spec.rb b/spec/services/labels/find_or_create_service_spec.rb
index 68d5660445a..97ba2742392 100644
--- a/spec/services/labels/find_or_create_service_spec.rb
+++ b/spec/services/labels/find_or_create_service_spec.rb
@@ -44,6 +44,26 @@ describe Labels::FindOrCreateService do
expect(service.execute).to eq project_label
end
end
+
+ context 'when include_ancestor_groups is true' do
+ let(:group) { create(:group, :nested) }
+ let(:params) do
+ {
+ title: 'Audit',
+ include_ancestor_groups: true
+ }
+ end
+
+ it 'returns the ancestor group labels' do
+ group_label = create(:group_label, group: group.parent, title: 'Audit')
+
+ expect(service.execute).to eq group_label
+ end
+
+ it 'creates new labels if labels are not found' do
+ expect { service.execute }.to change(project.labels, :count).by(1)
+ end
+ end
end
context 'when finding labels on group level' do
diff --git a/spec/services/lfs/unlock_file_service_spec.rb b/spec/services/lfs/unlock_file_service_spec.rb
index 4bea112b9c6..539417644db 100644
--- a/spec/services/lfs/unlock_file_service_spec.rb
+++ b/spec/services/lfs/unlock_file_service_spec.rb
@@ -62,11 +62,11 @@ describe Lfs::UnlockFileService do
context 'when forced' do
let(:developer) { create(:user) }
- let(:master) { create(:user) }
+ let(:maintainer) { create(:user) }
before do
project.add_developer(developer)
- project.add_master(master)
+ project.add_maintainer(maintainer)
end
context 'by a regular user' do
@@ -80,13 +80,13 @@ describe Lfs::UnlockFileService do
result = subject.execute
expect(result[:status]).to eq(:error)
- expect(result[:message]).to match(/You must have master access/)
+ expect(result[:message]).to match(/You must have maintainer access/)
expect(result[:http_status]).to eq(403)
end
end
- context 'by a master user' do
- let(:current_user) { master }
+ context 'by a maintainer user' do
+ let(:current_user) { maintainer }
let(:params) do
{ id: lock.id,
force: true }
diff --git a/spec/services/members/approve_access_request_service_spec.rb b/spec/services/members/approve_access_request_service_spec.rb
index 7076571b753..5c30f5b6a61 100644
--- a/spec/services/members/approve_access_request_service_spec.rb
+++ b/spec/services/members/approve_access_request_service_spec.rb
@@ -34,9 +34,9 @@ describe Members::ApproveAccessRequestService do
context 'with a custom access level' do
it 'returns a ProjectMember with the custom access level' do
- member = described_class.new(current_user, access_level: Gitlab::Access::MASTER).execute(access_requester, opts)
+ member = described_class.new(current_user, access_level: Gitlab::Access::MAINTAINER).execute(access_requester, opts)
- expect(member.access_level).to eq(Gitlab::Access::MASTER)
+ expect(member.access_level).to eq(Gitlab::Access::MAINTAINER)
end
end
end
@@ -97,7 +97,7 @@ describe Members::ApproveAccessRequestService do
context 'when current user can approve access request to the project' do
before do
- project.add_master(current_user)
+ project.add_maintainer(current_user)
group.add_owner(current_user)
end
diff --git a/spec/services/members/create_service_spec.rb b/spec/services/members/create_service_spec.rb
index 1831c62d788..5c01463d757 100644
--- a/spec/services/members/create_service_spec.rb
+++ b/spec/services/members/create_service_spec.rb
@@ -6,7 +6,7 @@ describe Members::CreateService do
let(:project_user) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
it 'adds user to members' do
diff --git a/spec/services/members/destroy_service_spec.rb b/spec/services/members/destroy_service_spec.rb
index 36b6e5a701e..ef47b0a450b 100644
--- a/spec/services/members/destroy_service_spec.rb
+++ b/spec/services/members/destroy_service_spec.rb
@@ -114,7 +114,7 @@ describe Members::DestroyService do
context 'when current user can destroy the given member' do
before do
- group_project.add_master(current_user)
+ group_project.add_maintainer(current_user)
group.add_owner(current_user)
end
@@ -142,8 +142,8 @@ describe Members::DestroyService do
context 'with an access requester' do
before do
- group_project.update_attributes(request_access_enabled: true)
- group.update_attributes(request_access_enabled: true)
+ group_project.update(request_access_enabled: true)
+ group.update(request_access_enabled: true)
group_project.request_access(member_user)
group.request_access(member_user)
end
@@ -170,7 +170,7 @@ describe Members::DestroyService do
context 'when current user can destroy the given access requester' do
before do
- group_project.add_master(current_user)
+ group_project.add_maintainer(current_user)
group.add_owner(current_user)
end
@@ -210,7 +210,7 @@ describe Members::DestroyService do
context 'when current user can destroy the given invited user' do
before do
- group_project.add_master(current_user)
+ group_project.add_maintainer(current_user)
group.add_owner(current_user)
end
diff --git a/spec/services/members/update_service_spec.rb b/spec/services/members/update_service_spec.rb
index a451272dd1f..6d19a95ffeb 100644
--- a/spec/services/members/update_service_spec.rb
+++ b/spec/services/members/update_service_spec.rb
@@ -8,7 +8,7 @@ describe Members::UpdateService do
let(:permission) { :update }
let(:member) { source.members_and_requesters.find_by!(user_id: member_user.id) }
let(:params) do
- { access_level: Gitlab::Access::MASTER }
+ { access_level: Gitlab::Access::MAINTAINER }
end
shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do
@@ -23,7 +23,7 @@ describe Members::UpdateService do
updated_member = described_class.new(current_user, params).execute(member, permission: permission)
expect(updated_member).to be_valid
- expect(updated_member.access_level).to eq(Gitlab::Access::MASTER)
+ expect(updated_member.access_level).to eq(Gitlab::Access::MAINTAINER)
end
end
@@ -44,7 +44,7 @@ describe Members::UpdateService do
context 'when current user can update the given member' do
before do
- project.add_master(current_user)
+ project.add_maintainer(current_user)
group.add_owner(current_user)
end
diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb
index 216e0cd4266..433ffbd97f0 100644
--- a/spec/services/merge_requests/close_service_spec.rb
+++ b/spec/services/merge_requests/close_service_spec.rb
@@ -9,7 +9,7 @@ describe MergeRequests::CloseService do
let!(:todo) { create(:todo, :assigned, user: user, project: project, target: merge_request, author: user2) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
project.add_developer(user2)
project.add_guest(guest)
end
diff --git a/spec/services/merge_requests/conflicts/list_service_spec.rb b/spec/services/merge_requests/conflicts/list_service_spec.rb
index 837b8a56d12..c81fa95e4b7 100644
--- a/spec/services/merge_requests/conflicts/list_service_spec.rb
+++ b/spec/services/merge_requests/conflicts/list_service_spec.rb
@@ -2,8 +2,8 @@ require 'spec_helper'
describe MergeRequests::Conflicts::ListService do
describe '#can_be_resolved_in_ui?' do
- def create_merge_request(source_branch)
- create(:merge_request, source_branch: source_branch, target_branch: 'conflict-start') do |mr|
+ def create_merge_request(source_branch, target_branch = 'conflict-start')
+ create(:merge_request, source_branch: source_branch, target_branch: target_branch, merge_status: :unchecked) do |mr|
mr.mark_as_unmergeable
end
end
@@ -34,7 +34,7 @@ describe MergeRequests::Conflicts::ListService do
it 'returns a falsey value when the MR does not support new diff notes' do
merge_request = create_merge_request('conflict-resolvable')
- merge_request.merge_request_diff.update_attributes(start_commit_sha: nil)
+ merge_request.merge_request_diff.update(start_commit_sha: nil)
expect(conflicts_service(merge_request).can_be_resolved_in_ui?).to be_falsey
end
@@ -85,22 +85,10 @@ describe MergeRequests::Conflicts::ListService do
expect(service.can_be_resolved_in_ui?).to be_falsey
end
- context 'with gitaly disabled', :skip_gitaly_mock 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_any_instance_of(Rugged::Repository).to receive(:merge_commits).and_raise(Rugged::OdbError)
+ it 'returns a falsey value when the conflict is in a submodule revision' do
+ merge_request = create_merge_request('update-gitlab-shell-v-6-0-3', 'update-gitlab-shell-v-6-0-1')
- expect(service.can_be_resolved_in_ui?).to be_falsey
- end
-
- it 'returns a falsey value when the MR has a missing revision after a force push' do
- merge_request = create_merge_request('conflict-resolvable')
- service = conflicts_service(merge_request)
- allow(merge_request).to receive_message_chain(:target_branch_head, :raw, :id).and_return(Gitlab::Git::BLANK_SHA)
-
- expect(service.can_be_resolved_in_ui?).to be_falsey
- end
+ expect(conflicts_service(merge_request).can_be_resolved_in_ui?).to be_falsey
end
end
end
diff --git a/spec/services/merge_requests/conflicts/resolve_service_spec.rb b/spec/services/merge_requests/conflicts/resolve_service_spec.rb
index cff09237005..7edf8a96c94 100644
--- a/spec/services/merge_requests/conflicts/resolve_service_spec.rb
+++ b/spec/services/merge_requests/conflicts/resolve_service_spec.rb
@@ -123,17 +123,6 @@ describe MergeRequests::Conflicts::ResolveService do
expect(merge_request_from_fork.source_branch_head.parents.map(&:id))
.to eq(['404fa3fc7c2c9b5dacff102f353bdf55b1be2813', target_head])
end
-
- context 'when gitaly is disabled', :skip_gitaly_mock do
- 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
-
- subject
- end
- 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 38d84cf0ceb..b1882df732d 100644
--- a/spec/services/merge_requests/create_from_issue_service_spec.rb
+++ b/spec/services/merge_requests/create_from_issue_service_spec.rb
@@ -125,9 +125,14 @@ describe MergeRequests::CreateFromIssueService do
end
context 'when ref branch does not exist' do
- it 'does not create a merge request' do
- expect { described_class.new(project, user, issue_iid: issue.iid, ref: 'nobr').execute }
- .not_to change { project.merge_requests.count }
+ subject { described_class.new(project, user, issue_iid: issue.iid, ref: 'no-such-branch').execute }
+
+ it 'creates a merge request' do
+ expect { subject }.to change(project.merge_requests, :count).by(1)
+ end
+
+ it 'sets the merge request target branch to the project default branch' do
+ expect(subject[:merge_request].target_branch).to eq(project.default_branch)
end
end
end
diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb
index 736a50b2c15..06fb61baf33 100644
--- a/spec/services/merge_requests/create_service_spec.rb
+++ b/spec/services/merge_requests/create_service_spec.rb
@@ -23,7 +23,7 @@ describe MergeRequests::CreateService do
let(:merge_request) { service.execute }
before do
- project.add_master(user)
+ project.add_maintainer(user)
project.add_developer(assignee)
allow(service).to receive(:execute_hooks)
end
@@ -185,8 +185,8 @@ describe MergeRequests::CreateService do
end
before do
- project.add_master(user)
- project.add_master(assignee)
+ project.add_maintainer(user)
+ project.add_maintainer(assignee)
end
it 'assigns and sets milestone to issuable from command' do
@@ -202,7 +202,7 @@ describe MergeRequests::CreateService do
let(:assignee) { create(:user) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
it 'removes assignee_id when user id is invalid' do
@@ -222,7 +222,7 @@ describe MergeRequests::CreateService do
end
it 'saves assignee when user id is valid' do
- project.add_master(assignee)
+ project.add_maintainer(assignee)
opts = { title: 'Title', description: 'Description', assignee_id: assignee.id }
merge_request = described_class.new(project, user, opts).execute
@@ -242,7 +242,7 @@ describe MergeRequests::CreateService do
end
it 'invalidates open merge request counter for assignees when merge request is assigned' do
- project.add_master(assignee)
+ project.add_maintainer(assignee)
described_class.new(project, user, opts).execute
@@ -286,7 +286,7 @@ describe MergeRequests::CreateService do
end
before do
- project.add_master(user)
+ project.add_maintainer(user)
project.add_developer(assignee)
end
@@ -316,7 +316,7 @@ describe MergeRequests::CreateService do
context 'when user can not access source project' do
before do
target_project.add_developer(assignee)
- target_project.add_master(user)
+ target_project.add_maintainer(user)
end
it 'raises an error' do
@@ -328,7 +328,7 @@ describe MergeRequests::CreateService do
context 'when user can not access target project' do
before do
target_project.add_developer(assignee)
- target_project.add_master(user)
+ target_project.add_maintainer(user)
end
it 'raises an error' do
@@ -372,7 +372,7 @@ describe MergeRequests::CreateService do
before do
project.add_developer(assignee)
- project.add_master(user)
+ project.add_maintainer(user)
end
it 'ignores source_project_id' do
diff --git a/spec/services/merge_requests/delete_non_latest_diffs_service_spec.rb b/spec/services/merge_requests/delete_non_latest_diffs_service_spec.rb
new file mode 100644
index 00000000000..1c632847940
--- /dev/null
+++ b/spec/services/merge_requests/delete_non_latest_diffs_service_spec.rb
@@ -0,0 +1,59 @@
+require 'spec_helper'
+
+describe MergeRequests::DeleteNonLatestDiffsService, :clean_gitlab_redis_shared_state do
+ let(:merge_request) { create(:merge_request) }
+
+ let!(:subject) { described_class.new(merge_request) }
+
+ describe '#execute' do
+ before do
+ stub_const("#{described_class.name}::BATCH_SIZE", 2)
+
+ 3.times { merge_request.create_merge_request_diff }
+ end
+
+ it 'schedules non-latest merge request diffs removal' do
+ diffs = merge_request.merge_request_diffs
+
+ expect(diffs.count).to eq(4)
+
+ Timecop.freeze do
+ expect(DeleteDiffFilesWorker)
+ .to receive(:bulk_perform_in)
+ .with(5.minutes, [[diffs.first.id], [diffs.second.id]])
+ expect(DeleteDiffFilesWorker)
+ .to receive(:bulk_perform_in)
+ .with(10.minutes, [[diffs.third.id]])
+
+ subject.execute
+ end
+ end
+
+ it 'schedules no removal if it is already cleaned' do
+ merge_request.merge_request_diffs.each(&:clean!)
+
+ expect(DeleteDiffFilesWorker).not_to receive(:bulk_perform_in)
+
+ subject.execute
+ end
+
+ it 'schedules no removal if it is empty' do
+ merge_request.merge_request_diffs.each { |diff| diff.update!(state: :empty) }
+
+ expect(DeleteDiffFilesWorker).not_to receive(:bulk_perform_in)
+
+ subject.execute
+ end
+
+ it 'schedules no removal if there is no non-latest diffs' do
+ merge_request
+ .merge_request_diffs
+ .where.not(id: merge_request.latest_merge_request_diff_id)
+ .destroy_all
+
+ expect(DeleteDiffFilesWorker).not_to receive(:bulk_perform_in)
+
+ subject.execute
+ end
+ end
+end
diff --git a/spec/services/merge_requests/ff_merge_service_spec.rb b/spec/services/merge_requests/ff_merge_service_spec.rb
index 5ef6365fcc9..fe673de46aa 100644
--- a/spec/services/merge_requests/ff_merge_service_spec.rb
+++ b/spec/services/merge_requests/ff_merge_service_spec.rb
@@ -13,7 +13,7 @@ describe MergeRequests::FfMergeService do
let(:project) { merge_request.project }
before do
- project.add_master(user)
+ project.add_maintainer(user)
project.add_developer(user2)
end
@@ -72,7 +72,7 @@ describe MergeRequests::FfMergeService do
it 'logs and saves error if there is an PreReceiveError exception' do
error_message = 'error message'
- allow(service).to receive(:repository).and_raise(Gitlab::Git::HooksService::PreReceiveError, error_message)
+ allow(service).to receive(:repository).and_raise(Gitlab::Git::PreReceiveError, error_message)
allow(service).to receive(:execute_hooks)
service.execute(merge_request)
diff --git a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb
deleted file mode 100644
index 57b6165cfb0..00000000000
--- a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-require 'spec_helper'
-
-describe MergeRequests::MergeRequestDiffCacheService, :use_clean_rails_memory_store_caching do
- let(:subject) { described_class.new }
- let(:merge_request) { create(:merge_request) }
-
- describe '#execute' do
- before do
- allow_any_instance_of(Gitlab::Diff::File).to receive(:text?).and_return(true)
- allow_any_instance_of(Gitlab::Diff::File).to receive(:diffable?).and_return(true)
- end
-
- it 'retrieves the diff files to cache the highlighted result' do
- new_diff = merge_request.merge_request_diff
- cache_key = new_diff.diffs.cache_key
-
- expect(Rails.cache).to receive(:read).with(cache_key).and_call_original
- expect(Rails.cache).to receive(:write).with(cache_key, anything, anything).and_call_original
-
- subject.execute(merge_request, new_diff)
- end
-
- it 'clears the cache for older diffs on the merge request' do
- old_diff = merge_request.merge_request_diff
- old_cache_key = old_diff.diffs.cache_key
-
- subject.execute(merge_request, old_diff)
-
- new_diff = merge_request.create_merge_request_diff
- new_cache_key = new_diff.diffs.cache_key
-
- expect(Rails.cache).to receive(:delete).with(old_cache_key).and_call_original
- expect(Rails.cache).to receive(:read).with(new_cache_key).and_call_original
- expect(Rails.cache).to receive(:write).with(new_cache_key, anything, anything).and_call_original
-
- subject.execute(merge_request, new_diff)
- end
- end
-end
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index e8568bf8bb3..9dd235f6660 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -7,7 +7,7 @@ describe MergeRequests::MergeService do
let(:project) { merge_request.project }
before do
- project.add_master(user)
+ project.add_maintainer(user)
project.add_developer(user2)
end
@@ -63,7 +63,7 @@ describe MergeRequests::MergeService do
let(:commit) { double('commit', safe_message: "Fixes #{jira_issue.to_reference}") }
before do
- project.update_attributes!(has_external_issue_tracker: true)
+ project.update!(has_external_issue_tracker: true)
jira_service_settings
stub_jira_urls(jira_issue.id)
allow(merge_request).to receive(:commits).and_return([commit])
@@ -226,7 +226,7 @@ describe MergeRequests::MergeService do
it 'logs and saves error if there is an PreReceiveError exception' do
error_message = 'error message'
- allow(service).to receive(:repository).and_raise(Gitlab::Git::HooksService::PreReceiveError, error_message)
+ allow(service).to receive(:repository).and_raise(Gitlab::Git::PreReceiveError, error_message)
allow(service).to receive(:execute_hooks)
service.execute(merge_request)
@@ -249,24 +249,58 @@ describe MergeRequests::MergeService do
expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message))
end
- context "when fast-forward merge is not allowed" do
+ context 'when squashing' do
before do
- allow_any_instance_of(Repository).to receive(:ancestor?).and_return(nil)
+ merge_request.update!(source_branch: 'master', target_branch: 'feature')
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)
+ it 'logs and saves error if there is an error when squashing' do
+ error_message = 'Failed to squash. Should be done manually'
- service.execute(merge_request)
+ allow_any_instance_of(MergeRequests::SquashService).to receive(:squash).and_return(nil)
+ merge_request.update(squash: true)
+
+ 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
+
+ it 'logs and saves error if there is a squash in progress' do
+ error_message = 'another squash is already in progress'
+
+ allow_any_instance_of(MergeRequest).to receive(:squash_in_progress?).and_return(true)
+ merge_request.update(squash: true)
+
+ 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))
+ 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
+
+ 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
diff --git a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb
index 240aa638f79..8838742a637 100644
--- a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb
+++ b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb
@@ -112,32 +112,6 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
service.trigger(unrelated_pipeline)
end
end
-
- context 'when the merge request is not mergeable' do
- let(:mr_conflict) do
- create(:merge_request, merge_when_pipeline_succeeds: true, merge_user: user,
- source_branch: 'master', target_branch: 'feature-conflict',
- source_project: project, target_project: project)
- end
-
- let(:conflict_pipeline) do
- create(:ci_pipeline, project: project, ref: mr_conflict.source_branch,
- sha: mr_conflict.diff_head_sha, status: 'success',
- head_pipeline_of: mr_conflict)
- end
-
- it 'does not merge the merge request' do
- expect(MergeWorker).not_to receive(:perform_async)
-
- service.trigger(conflict_pipeline)
- end
-
- it 'creates todos for unmergeability' do
- expect_any_instance_of(TodoService).to receive(:merge_request_became_unmergeable).with(mr_conflict)
-
- service.trigger(conflict_pipeline)
- end
- end
end
describe "#cancel" do
diff --git a/spec/services/merge_requests/post_merge_service_spec.rb b/spec/services/merge_requests/post_merge_service_spec.rb
index 70957431942..ba2b062875b 100644
--- a/spec/services/merge_requests/post_merge_service_spec.rb
+++ b/spec/services/merge_requests/post_merge_service_spec.rb
@@ -6,7 +6,7 @@ describe MergeRequests::PostMergeService do
let(:project) { merge_request.project }
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
describe '#execute' do
@@ -35,5 +35,30 @@ describe MergeRequests::PostMergeService do
described_class.new(project, user, {}).execute(merge_request)
end
+
+ it 'deletes non-latest diffs' do
+ diff_removal_service = instance_double(MergeRequests::DeleteNonLatestDiffsService, execute: nil)
+
+ expect(MergeRequests::DeleteNonLatestDiffsService)
+ .to receive(:new).with(merge_request)
+ .and_return(diff_removal_service)
+
+ described_class.new(project, user, {}).execute(merge_request)
+
+ expect(diff_removal_service).to have_received(:execute)
+ end
+
+ it 'marks MR as merged regardless of errors when closing issues' do
+ merge_request.update(target_branch: 'foo')
+ allow(project).to receive(:default_branch).and_return('foo')
+
+ issue = create(:issue, project: project)
+ allow(merge_request).to receive(:closes_issues).and_return([issue])
+ allow_any_instance_of(Issues::CloseService).to receive(:execute).with(issue, commit: merge_request).and_raise
+
+ expect { described_class.new(project, user, {}).execute(merge_request) }.to raise_error
+
+ expect(merge_request.reload).to be_merged
+ end
end
end
diff --git a/spec/services/merge_requests/rebase_service_spec.rb b/spec/services/merge_requests/rebase_service_spec.rb
index 757c31ab692..2703da7ae44 100644
--- a/spec/services/merge_requests/rebase_service_spec.rb
+++ b/spec/services/merge_requests/rebase_service_spec.rb
@@ -15,7 +15,7 @@ describe MergeRequests::RebaseService do
subject(:service) { described_class.new(project, user, {}) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
describe '#execute' do
@@ -36,9 +36,9 @@ describe MergeRequests::RebaseService do
end
end
- context 'when unexpected error occurs', :disable_gitaly do
+ context 'when unexpected error occurs' do
before do
- allow(repository).to receive(:run_git!).and_raise('Something went wrong')
+ allow(repository).to receive(:gitaly_operation_client).and_raise('Something went wrong')
end
it 'saves a generic error message' do
@@ -53,9 +53,9 @@ describe MergeRequests::RebaseService do
end
end
- context 'with git command failure', :disable_gitaly do
+ context 'with git command failure' do
before do
- allow(repository).to receive(:run_git!).and_raise(Gitlab::Git::Repository::GitError, 'Something went wrong')
+ allow(repository).to receive(:gitaly_operation_client).and_raise(Gitlab::Git::Repository::GitError, 'Something went wrong')
end
it 'saves a generic error message' do
@@ -71,7 +71,7 @@ describe MergeRequests::RebaseService do
end
context 'valid params' do
- shared_examples 'successful rebase' do
+ describe 'successful rebase' do
before do
service.execute(merge_request)
end
@@ -97,26 +97,8 @@ describe MergeRequests::RebaseService do
end
end
- context 'when Gitaly rebase feature is enabled' do
- it_behaves_like 'successful rebase'
- end
-
- context 'when Gitaly rebase feature is disabled', :disable_gitaly do
- it_behaves_like 'successful rebase'
- end
-
- context 'git commands', :disable_gitaly do
- it 'sets GL_REPOSITORY env variable when calling git commands' do
- expect(repository).to receive(:popen).exactly(3)
- .with(anything, anything, hash_including('GL_REPOSITORY'), anything)
- .and_return(['', 0])
-
- service.execute(merge_request)
- end
- end
-
context 'fork' do
- shared_examples 'successful fork rebase' do
+ describe 'successful fork rebase' do
let(:forked_project) do
fork_project(project, user, repository: true)
end
@@ -140,14 +122,6 @@ describe MergeRequests::RebaseService do
expect(parent_sha).to eq(target_branch_sha)
end
end
-
- context 'when Gitaly rebase feature is enabled' do
- it_behaves_like 'successful fork rebase'
- end
-
- context 'when Gitaly rebase feature is disabled', :disable_gitaly do
- it_behaves_like 'successful fork rebase'
- end
end
end
end
diff --git a/spec/services/merge_requests/reload_diffs_service_spec.rb b/spec/services/merge_requests/reload_diffs_service_spec.rb
new file mode 100644
index 00000000000..a0a27d247fc
--- /dev/null
+++ b/spec/services/merge_requests/reload_diffs_service_spec.rb
@@ -0,0 +1,64 @@
+require 'spec_helper'
+
+describe MergeRequests::ReloadDiffsService, :use_clean_rails_memory_store_caching do
+ let(:current_user) { create(:user) }
+ let(:merge_request) { create(:merge_request) }
+ let(:subject) { described_class.new(merge_request, current_user) }
+
+ describe '#execute' do
+ it 'creates new merge request diff' do
+ expect { subject.execute }.to change { merge_request.merge_request_diffs.count }.by(1)
+ end
+
+ it 'calls update_diff_discussion_positions with correct params' do
+ old_diff_refs = merge_request.diff_refs
+ new_diff = merge_request.create_merge_request_diff
+ new_diff_refs = merge_request.diff_refs
+
+ expect(merge_request).to receive(:create_merge_request_diff).and_return(new_diff)
+ expect(merge_request).to receive(:update_diff_discussion_positions)
+ .with(old_diff_refs: old_diff_refs,
+ new_diff_refs: new_diff_refs,
+ current_user: current_user)
+
+ subject.execute
+ end
+
+ it 'does not change existing merge request diff' do
+ expect(merge_request.merge_request_diff).not_to receive(:save_git_content)
+
+ subject.execute
+ end
+
+ context 'cache clearing' do
+ before do
+ allow_any_instance_of(Gitlab::Diff::File).to receive(:text?).and_return(true)
+ allow_any_instance_of(Gitlab::Diff::File).to receive(:diffable?).and_return(true)
+ end
+
+ it 'retrieves the diff files to cache the highlighted result' do
+ new_diff = merge_request.create_merge_request_diff
+ cache_key = new_diff.diffs_collection.cache_key
+
+ expect(merge_request).to receive(:create_merge_request_diff).and_return(new_diff)
+ expect(Rails.cache).to receive(:read).with(cache_key).and_call_original
+ expect(Rails.cache).to receive(:write).with(cache_key, anything, anything).and_call_original
+
+ subject.execute
+ end
+
+ it 'clears the cache for older diffs on the merge request' do
+ old_diff = merge_request.merge_request_diff
+ old_cache_key = old_diff.diffs_collection.cache_key
+ new_diff = merge_request.create_merge_request_diff
+ new_cache_key = new_diff.diffs_collection.cache_key
+
+ expect(merge_request).to receive(:create_merge_request_diff).and_return(new_diff)
+ expect(Rails.cache).to receive(:delete).with(old_cache_key).and_call_original
+ expect(Rails.cache).to receive(:read).with(new_cache_key).and_call_original
+ expect(Rails.cache).to receive(:write).with(new_cache_key, anything, anything).and_call_original
+ subject.execute
+ end
+ end
+ end
+end
diff --git a/spec/services/merge_requests/reopen_service_spec.rb b/spec/services/merge_requests/reopen_service_spec.rb
index 9ee37c51d95..e10eaa95da4 100644
--- a/spec/services/merge_requests/reopen_service_spec.rb
+++ b/spec/services/merge_requests/reopen_service_spec.rb
@@ -8,7 +8,7 @@ describe MergeRequests::ReopenService do
let(:project) { merge_request.project }
before do
- project.add_master(user)
+ project.add_maintainer(user)
project.add_developer(user2)
project.add_guest(guest)
end
diff --git a/spec/services/merge_requests/squash_service_spec.rb b/spec/services/merge_requests/squash_service_spec.rb
new file mode 100644
index 00000000000..8ab09412f55
--- /dev/null
+++ b/spec/services/merge_requests/squash_service_spec.rb
@@ -0,0 +1,156 @@
+require 'spec_helper'
+
+describe MergeRequests::SquashService do
+ let(:service) { described_class.new(project, user, {}) }
+ let(:user) { project.owner }
+ let(:project) { create(:project, :repository) }
+ let(:repository) { project.repository.raw }
+ let(:log_error) { "Failed to squash merge request #{merge_request.to_reference(full: true)}:" }
+ let(:squash_dir_path) do
+ File.join(Gitlab.config.shared.path, 'tmp/squash', repository.gl_repository, merge_request.id.to_s)
+ end
+ let(:merge_request_with_one_commit) do
+ create(:merge_request,
+ source_branch: 'feature', source_project: project,
+ target_branch: 'master', target_project: project)
+ end
+
+ let(:merge_request_with_only_new_files) do
+ create(:merge_request,
+ source_branch: 'video', source_project: project,
+ target_branch: 'master', target_project: project)
+ end
+
+ let(:merge_request_with_large_files) do
+ create(:merge_request,
+ source_branch: 'squash-large-files', source_project: project,
+ target_branch: 'master', target_project: project)
+ end
+
+ shared_examples 'the squash succeeds' do
+ it 'returns the squashed commit SHA' do
+ result = service.execute(merge_request)
+
+ expect(result).to match(status: :success, squash_sha: a_string_matching(/\h{40}/))
+ expect(result[:squash_sha]).not_to eq(merge_request.diff_head_sha)
+ end
+
+ it 'cleans up the temporary directory' do
+ service.execute(merge_request)
+
+ expect(File.exist?(squash_dir_path)).to be(false)
+ end
+
+ it 'does not keep the branch push event' do
+ expect { service.execute(merge_request) }.not_to change { Event.count }
+ end
+
+ context 'the squashed commit' do
+ let(:squash_sha) { service.execute(merge_request)[:squash_sha] }
+ let(:squash_commit) { project.repository.commit(squash_sha) }
+
+ it 'copies the author info and message from the merge request' do
+ expect(squash_commit.author_name).to eq(merge_request.author.name)
+ expect(squash_commit.author_email).to eq(merge_request.author.email)
+
+ # Commit messages have a trailing newline, but titles don't.
+ expect(squash_commit.message.chomp).to eq(merge_request.title)
+ end
+
+ it 'sets the current user as the committer' do
+ expect(squash_commit.committer_name).to eq(user.name.chomp('.'))
+ expect(squash_commit.committer_email).to eq(user.email)
+ end
+
+ it 'has the same diff as the merge request, but a different SHA' do
+ rugged = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ project.repository.rugged
+ end
+ mr_diff = rugged.diff(merge_request.diff_base_sha, merge_request.diff_head_sha)
+ squash_diff = rugged.diff(merge_request.diff_start_sha, squash_sha)
+
+ expect(squash_diff.patch.length).to eq(mr_diff.patch.length)
+ expect(squash_commit.sha).not_to eq(merge_request.diff_head_sha)
+ end
+ end
+ end
+
+ describe '#execute' do
+ context 'when there is only one commit in the merge request' do
+ it 'returns that commit SHA' do
+ result = service.execute(merge_request_with_one_commit)
+
+ expect(result).to match(status: :success, squash_sha: merge_request_with_one_commit.diff_head_sha)
+ end
+
+ it 'does not perform any git actions' do
+ expect(repository).not_to receive(:popen)
+
+ service.execute(merge_request_with_one_commit)
+ end
+ end
+
+ context 'when squashing only new files' do
+ let(:merge_request) { merge_request_with_only_new_files }
+
+ include_examples 'the squash succeeds'
+ end
+
+ context 'when squashing with files too large to display' do
+ let(:merge_request) { merge_request_with_large_files }
+
+ include_examples 'the squash succeeds'
+ end
+
+ context 'git errors' do
+ let(:merge_request) { merge_request_with_only_new_files }
+ let(:error) { 'A test error' }
+
+ context 'with gitaly enabled' do
+ before do
+ allow(repository.gitaly_operation_client).to receive(:user_squash)
+ .and_raise(Gitlab::Git::Repository::GitError, error)
+ end
+
+ it 'logs the stage and output' do
+ expect(service).to receive(:log_error).with(log_error)
+ expect(service).to receive(:log_error).with(error)
+
+ service.execute(merge_request)
+ end
+
+ it 'returns an error' do
+ expect(service.execute(merge_request)).to match(status: :error,
+ message: a_string_including('squash'))
+ end
+ end
+ end
+
+ context 'when any other exception is thrown' do
+ let(:merge_request) { merge_request_with_only_new_files }
+ let(:error) { 'A test error' }
+
+ before do
+ allow(merge_request).to receive(:commits_count).and_raise(error)
+ end
+
+ it 'logs the MR reference and exception' do
+ expect(service).to receive(:log_error).with(a_string_including("#{project.full_path}#{merge_request.to_reference}"))
+ expect(service).to receive(:log_error).with(error)
+
+ service.execute(merge_request)
+ end
+
+ it 'returns an error' do
+ expect(service.execute(merge_request)).to match(status: :error,
+ message: a_string_including('squash'))
+ end
+
+ it 'cleans up the temporary directory' do
+ service.execute(merge_request)
+
+ expect(File.exist?(squash_dir_path)).to be(false)
+ end
+ end
+ end
+end
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 5279ea6164e..f0029af83cc 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -19,7 +19,7 @@ describe MergeRequests::UpdateService, :mailer do
end
before do
- project.add_master(user)
+ project.add_maintainer(user)
project.add_developer(user2)
project.add_developer(user3)
end
@@ -326,12 +326,18 @@ describe MergeRequests::UpdateService, :mailer do
context 'when the labels change' do
before do
- update_merge_request({ label_ids: [label.id] })
+ Timecop.freeze(1.minute.from_now) do
+ update_merge_request({ label_ids: [label.id] })
+ end
end
it 'marks pending todos as done' do
expect(pending_todo.reload).to be_done
end
+
+ it 'updates updated_at' do
+ expect(merge_request.reload.updated_at).to be > Time.now
+ end
end
context 'when the assignee changes' do
@@ -541,7 +547,7 @@ describe MergeRequests::UpdateService, :mailer do
let(:closed_issuable) { create(:closed_merge_request, source_project: project) }
end
- context 'setting `allow_maintainer_to_push`' do
+ context 'setting `allow_collaboration`' do
let(:target_project) { create(:project, :public) }
let(:source_project) { fork_project(target_project) }
let(:user) { create(:user) }
@@ -556,23 +562,23 @@ describe MergeRequests::UpdateService, :mailer do
allow(ProtectedBranch).to receive(:protected?).with(source_project, 'fixes') { false }
end
- it 'does not allow a maintainer of the target project to set `allow_maintainer_to_push`' do
+ it 'does not allow a maintainer of the target project to set `allow_collaboration`' do
target_project.add_developer(user)
- update_merge_request(allow_maintainer_to_push: true, title: 'Updated title')
+ update_merge_request(allow_collaboration: true, title: 'Updated title')
expect(merge_request.title).to eq('Updated title')
- expect(merge_request.allow_maintainer_to_push).to be_falsy
+ expect(merge_request.allow_collaboration).to be_falsy
end
it 'is allowed by a user that can push to the source and can update the merge request' do
merge_request.update!(assignee: user)
source_project.add_developer(user)
- update_merge_request(allow_maintainer_to_push: true, title: 'Updated title')
+ update_merge_request(allow_collaboration: true, title: 'Updated title')
expect(merge_request.title).to eq('Updated title')
- expect(merge_request.allow_maintainer_to_push).to be_truthy
+ expect(merge_request.allow_collaboration).to be_truthy
end
end
end
diff --git a/spec/services/milestones/close_service_spec.rb b/spec/services/milestones/close_service_spec.rb
index adad73f7e11..3f7a544ea0a 100644
--- a/spec/services/milestones/close_service_spec.rb
+++ b/spec/services/milestones/close_service_spec.rb
@@ -6,7 +6,7 @@ describe Milestones::CloseService do
let(:milestone) { create(:milestone, title: "Milestone v1.2", project: project) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
describe '#execute' do
diff --git a/spec/services/milestones/create_service_spec.rb b/spec/services/milestones/create_service_spec.rb
index f2a18c7295a..0c91112026f 100644
--- a/spec/services/milestones/create_service_spec.rb
+++ b/spec/services/milestones/create_service_spec.rb
@@ -7,7 +7,7 @@ describe Milestones::CreateService do
describe '#execute' do
context "valid params" do
before do
- project.add_master(user)
+ project.add_maintainer(user)
opts = {
title: 'v2.1.9',
diff --git a/spec/services/milestones/destroy_service_spec.rb b/spec/services/milestones/destroy_service_spec.rb
index 9703780b0e9..6f3612501f4 100644
--- a/spec/services/milestones/destroy_service_spec.rb
+++ b/spec/services/milestones/destroy_service_spec.rb
@@ -8,7 +8,7 @@ describe Milestones::DestroyService do
let!(:merge_request) { create(:merge_request, source_project: project, milestone: milestone) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
def service
diff --git a/spec/services/milestones/promote_service_spec.rb b/spec/services/milestones/promote_service_spec.rb
index a0a2843b676..df212d912e9 100644
--- a/spec/services/milestones/promote_service_spec.rb
+++ b/spec/services/milestones/promote_service_spec.rb
@@ -10,7 +10,7 @@ describe Milestones::PromoteService do
describe '#execute' do
before do
- group.add_master(user)
+ group.add_maintainer(user)
end
context 'validations' do
diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb
index f5cff66de6d..0fd37c95e42 100644
--- a/spec/services/notes/create_service_spec.rb
+++ b/spec/services/notes/create_service_spec.rb
@@ -10,7 +10,7 @@ describe Notes::CreateService do
describe '#execute' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
context "valid params" do
@@ -57,6 +57,88 @@ describe Notes::CreateService do
end
end
+ context 'note diff file' do
+ let(:project_with_repo) { create(:project, :repository) }
+ let(:merge_request) do
+ create(:merge_request,
+ source_project: project_with_repo,
+ target_project: project_with_repo)
+ end
+ let(:line_number) { 14 }
+ let(:position) do
+ Gitlab::Diff::Position.new(old_path: "files/ruby/popen.rb",
+ new_path: "files/ruby/popen.rb",
+ old_line: nil,
+ new_line: line_number,
+ diff_refs: merge_request.diff_refs)
+ end
+ let(:previous_note) do
+ create(:diff_note_on_merge_request, noteable: merge_request, project: project_with_repo)
+ end
+
+ context 'when eligible to have a note diff file' do
+ let(:new_opts) do
+ opts.merge(in_reply_to_discussion_id: nil,
+ type: 'DiffNote',
+ noteable_type: 'MergeRequest',
+ noteable_id: merge_request.id,
+ position: position.to_h)
+ end
+
+ it 'note is associated with a note diff file' do
+ note = described_class.new(project_with_repo, user, new_opts).execute
+
+ expect(note).to be_persisted
+ expect(note.note_diff_file).to be_present
+ end
+ end
+
+ context 'when DiffNote is a reply' do
+ let(:new_opts) do
+ opts.merge(in_reply_to_discussion_id: previous_note.discussion_id,
+ type: 'DiffNote',
+ noteable_type: 'MergeRequest',
+ noteable_id: merge_request.id,
+ position: position.to_h)
+ end
+
+ it 'note is not associated with a note diff file' do
+ note = described_class.new(project_with_repo, user, new_opts).execute
+
+ expect(note).to be_persisted
+ expect(note.note_diff_file).to be_nil
+ end
+
+ context 'when DiffNote from an image' do
+ let(:image_position) do
+ Gitlab::Diff::Position.new(old_path: "files/images/6049019_460s.jpg",
+ new_path: "files/images/6049019_460s.jpg",
+ width: 100,
+ height: 100,
+ x: 1,
+ y: 100,
+ diff_refs: merge_request.diff_refs,
+ position_type: 'image')
+ end
+
+ let(:new_opts) do
+ opts.merge(in_reply_to_discussion_id: nil,
+ type: 'DiffNote',
+ noteable_type: 'MergeRequest',
+ noteable_id: merge_request.id,
+ position: image_position.to_h)
+ end
+
+ it 'note is not associated with a note diff file' do
+ note = described_class.new(project_with_repo, user, new_opts).execute
+
+ expect(note).to be_persisted
+ expect(note.note_diff_file).to be_nil
+ end
+ end
+ end
+ end
+
context 'note with commands' do
context 'as a user who can update the target' do
context '/close, /label, /assign & /milestone' do
diff --git a/spec/services/notes/post_process_service_spec.rb b/spec/services/notes/post_process_service_spec.rb
index 4e2ab919f0f..5aae0d711c3 100644
--- a/spec/services/notes/post_process_service_spec.rb
+++ b/spec/services/notes/post_process_service_spec.rb
@@ -7,7 +7,7 @@ describe Notes::PostProcessService do
describe '#execute' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
note_opts = {
note: 'Awesome comment',
noteable_type: 'Issue',
diff --git a/spec/services/notes/quick_actions_service_spec.rb b/spec/services/notes/quick_actions_service_spec.rb
index b1e218821d2..784dac55454 100644
--- a/spec/services/notes/quick_actions_service_spec.rb
+++ b/spec/services/notes/quick_actions_service_spec.rb
@@ -3,11 +3,11 @@ require 'spec_helper'
describe Notes::QuickActionsService do
shared_context 'note on noteable' do
let(:project) { create(:project) }
- let(:master) { create(:user).tap { |u| project.add_master(u) } }
+ let(:maintainer) { create(:user).tap { |u| project.add_maintainer(u) } }
let(:assignee) { create(:user) }
before do
- project.add_master(assignee)
+ project.add_maintainer(assignee)
end
end
@@ -184,7 +184,7 @@ describe Notes::QuickActionsService do
include_context 'note on noteable'
it 'delegates to the class method' do
- service = described_class.new(project, master)
+ service = described_class.new(project, maintainer)
note = create(:note_on_issue, project: project)
expect(described_class).to receive(:supported?).with(note)
@@ -194,7 +194,7 @@ describe Notes::QuickActionsService do
end
describe '#execute' do
- let(:service) { described_class.new(project, master) }
+ let(:service) { described_class.new(project, maintainer) }
it_behaves_like 'note on noteable that supports quick actions' do
let(:note) { build(:note_on_issue, project: project) }
@@ -212,19 +212,19 @@ describe Notes::QuickActionsService do
context 'CE restriction for issue assignees' do
describe '/assign' do
let(:project) { create(:project) }
- let(:master) { create(:user).tap { |u| project.add_master(u) } }
+ let(:maintainer) { create(:user).tap { |u| project.add_maintainer(u) } }
let(:assignee) { create(:user) }
- let(:master) { create(:user) }
- let(:service) { described_class.new(project, master) }
+ let(:maintainer) { create(:user) }
+ let(:service) { described_class.new(project, maintainer) }
let(:note) { create(:note_on_issue, note: note_text, project: project) }
let(:note_text) do
- %(/assign @#{assignee.username} @#{master.username}\n")
+ %(/assign @#{assignee.username} @#{maintainer.username}\n")
end
before do
- project.add_master(master)
- project.add_master(assignee)
+ project.add_maintainer(maintainer)
+ project.add_maintainer(assignee)
end
it 'adds only one assignee from the list' do
diff --git a/spec/services/notes/update_service_spec.rb b/spec/services/notes/update_service_spec.rb
index 65b1d613998..533dcdcd6cd 100644
--- a/spec/services/notes/update_service_spec.rb
+++ b/spec/services/notes/update_service_spec.rb
@@ -9,7 +9,7 @@ describe Notes::UpdateService do
let(:note) { create(:note, project: project, noteable: issue, author: user, note: "Old note #{user2.to_reference}") }
before do
- project.add_master(user)
+ project.add_maintainer(user)
project.add_developer(user2)
project.add_developer(user3)
end
diff --git a/spec/services/notification_recipient_service_spec.rb b/spec/services/notification_recipient_service_spec.rb
new file mode 100644
index 00000000000..14ba6b7bed2
--- /dev/null
+++ b/spec/services/notification_recipient_service_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe NotificationRecipientService do
+ let(:service) { described_class }
+ let(:assignee) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:other_projects) { create_list(:project, 5, :public) }
+
+ describe '#build_new_note_recipients' do
+ let(:issue) { create(:issue, project: project, assignees: [assignee]) }
+ let(:note) { create(:note_on_issue, noteable: issue, project_id: issue.project_id) }
+
+ def create_watcher
+ watcher = create(:user)
+ create(:notification_setting, source: project, user: watcher, level: :watch)
+
+ other_projects.each do |other_project|
+ create(:notification_setting, source: other_project, user: watcher, level: :watch)
+ end
+ end
+
+ it 'avoids N+1 queries', :request_store do
+ create_watcher
+
+ service.build_new_note_recipients(note)
+
+ control_count = ActiveRecord::QueryRecorder.new do
+ service.build_new_note_recipients(note)
+ end
+
+ create_watcher
+
+ expect { service.build_new_note_recipients(note) }.not_to exceed_query_limit(control_count)
+ end
+ end
+end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 5f28bc123f3..c442f6fe32f 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -59,6 +59,20 @@ describe NotificationService, :mailer do
should_email(participant)
end
+
+ context 'for subgroups', :nested_groups do
+ before do
+ build_group(project)
+ end
+
+ it 'emails the participant' do
+ create(:note_on_issue, noteable: issuable, project_id: project.id, note: 'anything', author: @pg_participant)
+
+ notification_trigger
+
+ should_email_nested_group_user(@pg_participant)
+ end
+ end
end
shared_examples 'participating by assignee notification' do
@@ -158,9 +172,9 @@ describe NotificationService, :mailer do
before do
build_team(note.project)
- project.add_master(issue.author)
- project.add_master(assignee)
- project.add_master(note.author)
+ project.add_maintainer(issue.author)
+ project.add_maintainer(assignee)
+ project.add_maintainer(note.author)
@u_custom_off = create_user_with_notification(:custom, 'custom_off')
project.add_guest(@u_custom_off)
@@ -239,34 +253,56 @@ describe NotificationService, :mailer do
end
describe 'new note on issue in project that belongs to a group' do
- let(:group) { create(:group) }
-
before do
note.project.namespace_id = group.id
- note.project.group.add_user(@u_watcher, GroupMember::MASTER)
- note.project.group.add_user(@u_custom_global, GroupMember::MASTER)
+ group.add_user(@u_watcher, GroupMember::MAINTAINER)
+ group.add_user(@u_custom_global, GroupMember::MAINTAINER)
note.project.save
@u_watcher.notification_settings_for(note.project).participating!
- @u_watcher.notification_settings_for(note.project.group).global!
+ @u_watcher.notification_settings_for(group).global!
update_custom_notification(:new_note, @u_custom_global)
reset_delivered_emails!
end
- it do
- notification.new_note(note)
+ shared_examples 'new note notifications' do
+ it do
+ notification.new_note(note)
+
+ should_email(note.noteable.author)
+ should_email(note.noteable.assignees.first)
+ should_email(@u_mentioned)
+ should_email(@u_custom_global)
+ should_not_email(@u_guest_custom)
+ should_not_email(@u_guest_watcher)
+ should_not_email(@u_watcher)
+ should_not_email(note.author)
+ should_not_email(@u_participating)
+ should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
+ end
+ end
- should_email(note.noteable.author)
- should_email(note.noteable.assignees.first)
- should_email(@u_mentioned)
- should_email(@u_custom_global)
- should_not_email(@u_guest_custom)
- should_not_email(@u_guest_watcher)
- should_not_email(@u_watcher)
- should_not_email(note.author)
- should_not_email(@u_participating)
- should_not_email(@u_disabled)
- should_not_email(@u_lazy_participant)
+ let(:group) { create(:group) }
+
+ it_behaves_like 'new note notifications'
+
+ context 'which is a subgroup', :nested_groups do
+ let!(:parent) { create(:group) }
+ let!(:group) { create(:group, parent: parent) }
+
+ it_behaves_like 'new note notifications'
+
+ it 'overrides child objects with global level' do
+ user = create(:user)
+ parent.add_developer(user)
+ user.notification_settings_for(parent).watch!
+ reset_delivered_emails!
+
+ notification.new_note(note)
+
+ should_email(user)
+ end
end
end
end
@@ -301,6 +337,31 @@ describe NotificationService, :mailer do
should_email(member)
should_email(admin)
end
+
+ context 'on project that belongs to subgroup', :nested_groups do
+ let(:group_reporter) { create(:user) }
+ let(:group_guest) { create(:user) }
+ let(:parent_group) { create(:group) }
+ let(:child_group) { create(:group, parent: parent_group) }
+ let(:project) { create(:project, namespace: child_group) }
+
+ context 'when user is group guest member' do
+ before do
+ parent_group.add_reporter(group_reporter)
+ parent_group.add_guest(group_guest)
+ group_guest.notification_settings_for(parent_group).watch!
+ group_reporter.notification_settings_for(parent_group).watch!
+ reset_delivered_emails!
+ end
+
+ it 'does not email guest user' do
+ notification.new_note(note)
+
+ should_email(group_reporter)
+ should_not_email(group_guest)
+ end
+ end
+ end
end
context 'issue note mention' do
@@ -311,7 +372,8 @@ describe NotificationService, :mailer do
before do
build_team(note.project)
- note.project.add_master(note.author)
+ build_group(note.project)
+ note.project.add_maintainer(note.author)
add_users_with_subscription(note.project, issue)
reset_delivered_emails!
end
@@ -336,10 +398,20 @@ describe NotificationService, :mailer do
should_email(@u_guest_watcher)
should_email(note.noteable.author)
should_email(note.noteable.assignees.first)
- should_not_email(note.author)
+ should_email_nested_group_user(@pg_watcher)
should_email(@u_mentioned)
- should_not_email(@u_disabled)
should_email(@u_not_mentioned)
+ should_not_email(note.author)
+ should_not_email(@u_disabled)
+ should_not_email_nested_group_user(@pg_disabled)
+ end
+
+ it 'notifies parent group members with mention level', :nested_groups do
+ note = create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: "@#{@pg_mention.username}")
+
+ notification.new_note(note)
+
+ should_email_nested_group_user(@pg_mention)
end
it 'filters out "mentioned in" notes' do
@@ -352,25 +424,25 @@ describe NotificationService, :mailer do
end
context 'project snippet note' do
- let(:project) { create(:project, :public) }
+ let!(:project) { create(:project, :public) }
let(:snippet) { create(:project_snippet, project: project, author: create(:user)) }
- let(:note) { create(:note_on_project_snippet, noteable: snippet, project_id: snippet.project.id, note: '@all mentioned') }
+ let(:note) { create(:note_on_project_snippet, noteable: snippet, project_id: project.id, note: '@all mentioned') }
before do
- build_team(note.project)
+ build_team(project)
+ build_group(project)
# make sure these users can read the project snippet!
project.add_guest(@u_guest_watcher)
project.add_guest(@u_guest_custom)
-
- note.project.add_master(note.author)
+ add_member_for_parent_group(@pg_watcher, project)
+ note.project.add_maintainer(note.author)
reset_delivered_emails!
end
describe '#new_note' do
it 'notifies the team members' do
notification.new_note(note)
-
# Notify all team members
note.project.team.members.each do |member|
# User with disabled notification should not be notified
@@ -449,6 +521,7 @@ describe NotificationService, :mailer do
before do
build_team(note.project)
+ build_group(project)
reset_delivered_emails!
allow(note.noteable).to receive(:author).and_return(@u_committer)
update_custom_notification(:new_note, @u_guest_custom, resource: project)
@@ -463,11 +536,13 @@ describe NotificationService, :mailer do
should_email(@u_guest_custom)
should_email(@u_committer)
should_email(@u_watcher)
+ should_email_nested_group_user(@pg_watcher)
should_not_email(@u_mentioned)
should_not_email(note.author)
should_not_email(@u_participating)
should_not_email(@u_disabled)
should_not_email(@u_lazy_participant)
+ should_not_email_nested_group_user(@pg_disabled)
end
it do
@@ -478,10 +553,12 @@ describe NotificationService, :mailer do
should_email(@u_committer)
should_email(@u_watcher)
should_email(@u_mentioned)
+ should_email_nested_group_user(@pg_watcher)
should_not_email(note.author)
should_not_email(@u_participating)
should_not_email(@u_disabled)
should_not_email(@u_lazy_participant)
+ should_not_email_nested_group_user(@pg_disabled)
end
it do
@@ -500,8 +577,8 @@ describe NotificationService, :mailer do
before do
build_team(note.project)
- project.add_master(merge_request.author)
- project.add_master(merge_request.assignee)
+ project.add_maintainer(merge_request.author)
+ project.add_maintainer(merge_request.assignee)
end
describe '#new_note' do
@@ -548,10 +625,13 @@ describe NotificationService, :mailer do
should_email(@g_global_watcher)
should_email(@g_watcher)
should_email(@unsubscribed_mentioned)
+ should_email_nested_group_user(@pg_watcher)
should_not_email(@u_mentioned)
should_not_email(@u_participating)
should_not_email(@u_disabled)
should_not_email(@u_lazy_participant)
+ should_not_email_nested_group_user(@pg_disabled)
+ should_not_email_nested_group_user(@pg_mention)
end
it do
@@ -1008,8 +1088,8 @@ describe NotificationService, :mailer do
let(:merge_request) { create :merge_request, source_project: project, assignee: create(:user), description: 'cc @participant' }
before do
- project.add_master(merge_request.author)
- project.add_master(merge_request.assignee)
+ project.add_maintainer(merge_request.author)
+ project.add_maintainer(merge_request.assignee)
build_team(merge_request.target_project)
add_users_with_subscription(merge_request.target_project, merge_request)
update_custom_notification(:new_merge_request, @u_guest_custom, resource: project)
@@ -1224,6 +1304,32 @@ describe NotificationService, :mailer do
end
end
+ describe '#merge_request_unmergeable' do
+ it "sends email to merge request author" do
+ notification.merge_request_unmergeable(merge_request)
+
+ should_email(merge_request.author)
+ expect(email_recipients.size).to eq(1)
+ end
+
+ describe 'when merge_when_pipeline_succeeds is true' do
+ before do
+ merge_request.update(
+ merge_when_pipeline_succeeds: true,
+ merge_user: create(:user)
+ )
+ end
+
+ it "sends email to merge request author and merge_user" do
+ notification.merge_request_unmergeable(merge_request)
+
+ should_email(merge_request.author)
+ should_email(merge_request.merge_user)
+ expect(email_recipients.size).to eq(2)
+ end
+ end
+ end
+
describe '#closed_merge_request' do
before do
update_custom_notification(:close_merge_request, @u_guest_custom, resource: project)
@@ -1423,13 +1529,13 @@ describe NotificationService, :mailer do
let(:added_user) { create(:user) }
describe '#new_access_request' do
- let(:master) { create(:user) }
+ let(:maintainer) { create(:user) }
let(:owner) { create(:user) }
let(:developer) { create(:user) }
let!(:group) do
create(:group, :public, :access_requestable) do |group|
group.add_owner(owner)
- group.add_master(master)
+ group.add_maintainer(maintainer)
group.add_developer(developer)
end
end
@@ -1438,11 +1544,11 @@ describe NotificationService, :mailer do
reset_delivered_emails!
end
- it 'sends notification to group owners_and_masters' do
+ it 'sends notification to group owners_and_maintainers' do
group.request_access(added_user)
should_email(owner)
- should_email(master)
+ should_email(maintainer)
should_not_email(developer)
end
end
@@ -1495,11 +1601,11 @@ describe NotificationService, :mailer do
context 'for a project in a user namespace' do
let(:project) do
create(:project, :public, :access_requestable) do |project|
- project.add_master(project.owner)
+ project.add_maintainer(project.owner)
end
end
- it 'sends notification to project owners_and_masters' do
+ it 'sends notification to project owners_and_maintainers' do
project.request_access(added_user)
should_only_email(project.owner)
@@ -1515,7 +1621,7 @@ describe NotificationService, :mailer do
reset_delivered_emails!
end
- it 'sends notification to group owners_and_masters' do
+ it 'sends notification to group owners_and_maintainers' do
project.request_access(added_user)
should_only_email(group_owner)
@@ -1653,11 +1759,11 @@ describe NotificationService, :mailer do
end
before do
- project.add_master(u_member)
- project.add_master(u_watcher)
- project.add_master(u_custom_notification_unset)
- project.add_master(u_custom_notification_enabled)
- project.add_master(u_custom_notification_disabled)
+ project.add_maintainer(u_member)
+ project.add_maintainer(u_watcher)
+ project.add_maintainer(u_custom_notification_unset)
+ project.add_maintainer(u_custom_notification_enabled)
+ project.add_maintainer(u_custom_notification_disabled)
reset_delivered_emails!
end
@@ -1797,15 +1903,15 @@ describe NotificationService, :mailer do
set(:u_blocked) { create(:user, :blocked) }
set(:u_silence) { create_user_with_notification(:disabled, 'silent', project) }
set(:u_owner) { project.owner }
- set(:u_master1) { create(:user) }
- set(:u_master2) { create(:user) }
+ set(:u_maintainer1) { create(:user) }
+ set(:u_maintainer2) { create(:user) }
set(:u_developer) { create(:user) }
before do
- project.add_master(u_blocked)
- project.add_master(u_silence)
- project.add_master(u_master1)
- project.add_master(u_master2)
+ project.add_maintainer(u_blocked)
+ project.add_maintainer(u_silence)
+ project.add_maintainer(u_maintainer1)
+ project.add_maintainer(u_maintainer2)
project.add_developer(u_developer)
reset_delivered_emails!
@@ -1820,12 +1926,12 @@ describe NotificationService, :mailer do
describe "##{sym}" do
subject(:notify!) { notification.send(sym, domain) }
- it 'emails current watching masters' do
+ it 'emails current watching maintainers' do
expect(Notify).to receive(:"#{sym}_email").at_least(:once).and_call_original
notify!
- should_only_email(u_master1, u_master2, u_owner)
+ should_only_email(u_maintainer1, u_maintainer2, u_owner)
end
it 'emails nobody if the project is missing' do
@@ -1839,26 +1945,26 @@ describe NotificationService, :mailer do
end
describe '#pages_domain_verification_failed' do
- it 'emails current watching masters' do
+ it 'emails current watching maintainers' do
notification.pages_domain_verification_failed(domain)
- should_only_email(u_master1, u_master2, u_owner)
+ should_only_email(u_maintainer1, u_maintainer2, u_owner)
end
end
describe '#pages_domain_enabled' do
- it 'emails current watching masters' do
+ it 'emails current watching maintainers' do
notification.pages_domain_enabled(domain)
- should_only_email(u_master1, u_master2, u_owner)
+ should_only_email(u_maintainer1, u_maintainer2, u_owner)
end
end
describe '#pages_domain_disabled' do
- it 'emails current watching masters' do
+ it 'emails current watching maintainers' do
notification.pages_domain_disabled(domain)
- should_only_email(u_master1, u_master2, u_owner)
+ should_only_email(u_maintainer1, u_maintainer2, u_owner)
end
end
end
@@ -1882,33 +1988,83 @@ describe NotificationService, :mailer do
@u_guest_watcher = create_user_with_notification(:watch, 'guest_watching')
@u_guest_custom = create_user_with_notification(:custom, 'guest_custom')
- project.add_master(@u_watcher)
- project.add_master(@u_participating)
- project.add_master(@u_participant_mentioned)
- project.add_master(@u_disabled)
- project.add_master(@u_mentioned)
- project.add_master(@u_committer)
- project.add_master(@u_not_mentioned)
- project.add_master(@u_lazy_participant)
- project.add_master(@u_custom_global)
+ project.add_maintainer(@u_watcher)
+ project.add_maintainer(@u_participating)
+ project.add_maintainer(@u_participant_mentioned)
+ project.add_maintainer(@u_disabled)
+ project.add_maintainer(@u_mentioned)
+ project.add_maintainer(@u_committer)
+ project.add_maintainer(@u_not_mentioned)
+ project.add_maintainer(@u_lazy_participant)
+ project.add_maintainer(@u_custom_global)
end
# Users in the project's group but not part of project's team
# with different notification settings
def build_group(project)
- group = create(:group, :public)
- project.group = group
+ group = create_nested_group
+ project.update(namespace_id: group.id)
# Group member: global=disabled, group=watch
- @g_watcher = create_user_with_notification(:watch, 'group_watcher', project.group)
+ @g_watcher ||= create_user_with_notification(:watch, 'group_watcher', project.group)
@g_watcher.notification_settings_for(nil).disabled!
# Group member: global=watch, group=global
- @g_global_watcher = create_global_setting_for(create(:user), :watch)
- group.add_users([@g_watcher, @g_global_watcher], :master)
+ @g_global_watcher ||= create_global_setting_for(create(:user), :watch)
+ group.add_users([@g_watcher, @g_global_watcher], :maintainer)
+
group
end
+ # Creates a nested group only if supported
+ # to avoid errors on MySQL
+ def create_nested_group
+ if Group.supports_nested_groups?
+ parent_group = create(:group, :public)
+ child_group = create(:group, :public, parent: parent_group)
+
+ # Parent group member: global=disabled, parent_group=watch, child_group=global
+ @pg_watcher ||= create_user_with_notification(:watch, 'parent_group_watcher', parent_group)
+ @pg_watcher.notification_settings_for(nil).disabled!
+
+ # Parent group member: global=global, parent_group=disabled, child_group=global
+ @pg_disabled ||= create_user_with_notification(:disabled, 'parent_group_disabled', parent_group)
+ @pg_disabled.notification_settings_for(nil).global!
+
+ # Parent group member: global=global, parent_group=mention, child_group=global
+ @pg_mention ||= create_user_with_notification(:mention, 'parent_group_mention', parent_group)
+ @pg_mention.notification_settings_for(nil).global!
+
+ # Parent group member: global=global, parent_group=participating, child_group=global
+ @pg_participant ||= create_user_with_notification(:participating, 'parent_group_participant', parent_group)
+ @pg_mention.notification_settings_for(nil).global!
+
+ child_group
+ else
+ create(:group, :public)
+ end
+ end
+
+ def add_member_for_parent_group(user, project)
+ return unless Group.supports_nested_groups?
+
+ project.reload
+
+ project.group.parent.add_maintainer(user)
+ end
+
+ def should_email_nested_group_user(user, times: 1, recipients: email_recipients)
+ return unless Group.supports_nested_groups?
+
+ should_email(user, times: 1, recipients: email_recipients)
+ end
+
+ def should_not_email_nested_group_user(user, recipients: email_recipients)
+ return unless Group.supports_nested_groups?
+
+ should_not_email(user, recipients: email_recipients)
+ end
+
def add_users_with_subscription(project, issuable)
@subscriber = create :user
@unsubscriber = create :user
@@ -1916,11 +2072,11 @@ describe NotificationService, :mailer do
@subscribed_participant = create_global_setting_for(create(:user, username: 'subscribed_participant'), :participating)
@watcher_and_subscriber = create_global_setting_for(create(:user), :watch)
- project.add_master(@subscribed_participant)
- project.add_master(@subscriber)
- project.add_master(@unsubscriber)
- project.add_master(@watcher_and_subscriber)
- project.add_master(@unsubscribed_mentioned)
+ project.add_maintainer(@subscribed_participant)
+ project.add_maintainer(@subscriber)
+ project.add_maintainer(@unsubscriber)
+ project.add_maintainer(@watcher_and_subscriber)
+ project.add_maintainer(@unsubscribed_mentioned)
issuable.subscriptions.create(user: @unsubscribed_mentioned, project: project, subscribed: false)
issuable.subscriptions.create(user: @subscriber, project: project, subscribed: true)
diff --git a/spec/services/pages_service_spec.rb b/spec/services/pages_service_spec.rb
deleted file mode 100644
index f8db6900a0a..00000000000
--- a/spec/services/pages_service_spec.rb
+++ /dev/null
@@ -1,53 +0,0 @@
-require 'spec_helper'
-
-describe PagesService do
- let(:build) { create(:ci_build) }
- let(:data) { Gitlab::DataBuilder::Build.build(build) }
- let(:service) { described_class.new(data) }
-
- before do
- allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
- end
-
- context 'execute asynchronously for pages job' do
- before do
- build.name = 'pages'
- end
-
- context 'on success' do
- before do
- build.success
- end
-
- it 'executes worker' do
- expect(PagesWorker).to receive(:perform_async)
- service.execute
- end
- end
-
- %w(pending running failed canceled).each do |status|
- context "on #{status}" do
- before do
- build.status = status
- end
-
- it 'does not execute worker' do
- expect(PagesWorker).not_to receive(:perform_async)
- service.execute
- end
- end
- end
- end
-
- context 'for other jobs' do
- before do
- build.name = 'other job'
- build.success
- end
-
- it 'does not execute worker' do
- expect(PagesWorker).not_to receive(:perform_async)
- service.execute
- end
- end
-end
diff --git a/spec/services/preview_markdown_service_spec.rb b/spec/services/preview_markdown_service_spec.rb
index 64a9559791f..81dc7c57f4a 100644
--- a/spec/services/preview_markdown_service_spec.rb
+++ b/spec/services/preview_markdown_service_spec.rb
@@ -64,4 +64,16 @@ describe PreviewMarkdownService do
expect(result[:commands]).to eq 'Sets time estimate to 2y.'
end
end
+
+ it 'sets correct markdown engine' do
+ service = described_class.new(project, user, { markdown_version: CacheMarkdownField::CACHE_REDCARPET_VERSION })
+ result = service.execute
+
+ expect(result[:markdown_engine]).to eq :redcarpet
+
+ service = described_class.new(project, user, { markdown_version: CacheMarkdownField::CACHE_COMMONMARK_VERSION })
+ result = service.execute
+
+ expect(result[:markdown_engine]).to eq :common_mark
+ end
end
diff --git a/spec/services/projects/after_import_service_spec.rb b/spec/services/projects/after_import_service_spec.rb
index c6678fc1f5c..cd52bc88f4c 100644
--- a/spec/services/projects/after_import_service_spec.rb
+++ b/spec/services/projects/after_import_service_spec.rb
@@ -32,7 +32,7 @@ describe Projects::AfterImportService do
end
it 'removes refs/pull/**/*' do
- expect(repository.rugged.references.map(&:name))
+ expect(rugged.references.map(&:name))
.not_to include(%r{\Arefs/pull/})
end
end
@@ -46,10 +46,14 @@ describe Projects::AfterImportService do
end
it "does not remove refs/#{name}/tmp" do
- expect(repository.rugged.references.map(&:name))
+ expect(rugged.references.map(&:name))
.to include("refs/#{name}/tmp")
end
end
end
+
+ def rugged
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access { repository.rugged }
+ end
end
end
diff --git a/spec/services/projects/autocomplete_service_spec.rb b/spec/services/projects/autocomplete_service_spec.rb
index f7ff8b80bd7..e98df375d48 100644
--- a/spec/services/projects/autocomplete_service_spec.rb
+++ b/spec/services/projects/autocomplete_service_spec.rb
@@ -115,5 +115,74 @@ describe Projects::AutocompleteService do
expect(milestone_titles).to eq([group_milestone2.title, group_milestone1.title])
end
+
+ context 'with nested groups', :nested_groups do
+ let(:subgroup) { create(:group, :public, parent: group) }
+ let!(:subgroup_milestone) { create(:milestone, group: subgroup) }
+
+ before do
+ project.update(namespace: subgroup)
+ end
+
+ it 'includes project milestones and all acestors milestones' do
+ expect(milestone_titles).to match_array(
+ [project_milestone.title, group_milestone2.title, group_milestone1.title, subgroup_milestone.title]
+ )
+ end
+ end
+ end
+
+ describe '#labels_as_hash' do
+ def expect_labels_to_equal(labels, expected_labels)
+ expect(labels.size).to eq(expected_labels.size)
+ extract_title = lambda { |label| label['title'] }
+ expect(labels.map(&extract_title)).to eq(expected_labels.map(&extract_title))
+ end
+
+ let(:user) { create(:user) }
+ let(:group) { create(:group, :nested) }
+ let!(:sub_group) { create(:group, parent: group) }
+ let(:project) { create(:project, :public, group: group) }
+ let(:issue) { create(:issue, project: project) }
+
+ let!(:label1) { create(:label, project: project) }
+ let!(:label2) { create(:label, project: project) }
+ let!(:sub_group_label) { create(:group_label, group: sub_group) }
+ let!(:parent_group_label) { create(:group_label, group: group.parent) }
+
+ before do
+ create(:group_member, group: group, user: user)
+ end
+
+ it 'returns labels from project and ancestor groups' do
+ service = described_class.new(project, user)
+ results = service.labels_as_hash
+ expected_labels = [label1, label2, parent_group_label]
+
+ expect_labels_to_equal(results, expected_labels)
+ end
+
+ context 'some labels are already assigned' do
+ before do
+ issue.labels << label1
+ end
+
+ it 'marks already assigned as set' do
+ service = described_class.new(project, user)
+ results = service.labels_as_hash(issue)
+ expected_labels = [label1, label2, parent_group_label]
+
+ expect_labels_to_equal(results, expected_labels)
+
+ assigned_label_titles = issue.labels.map(&:title)
+ results.each do |hash|
+ if assigned_label_titles.include?(hash['title'])
+ expect(hash[:set]).to eq(true)
+ else
+ expect(hash.key?(:set)).to eq(false)
+ end
+ end
+ end
+ end
end
end
diff --git a/spec/services/projects/batch_open_issues_count_service_spec.rb b/spec/services/projects/batch_open_issues_count_service_spec.rb
new file mode 100644
index 00000000000..599aaf62080
--- /dev/null
+++ b/spec/services/projects/batch_open_issues_count_service_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+describe Projects::BatchOpenIssuesCountService do
+ let!(:project_1) { create(:project) }
+ let!(:project_2) { create(:project) }
+
+ let(:subject) { described_class.new([project_1, project_2]) }
+
+ context '#refresh_cache', :use_clean_rails_memory_store_caching do
+ before do
+ create(:issue, project: project_1)
+ create(:issue, project: project_1, confidential: true)
+
+ create(:issue, project: project_2)
+ create(:issue, project: project_2, confidential: true)
+ end
+
+ context 'when cache is clean' do
+ it 'refreshes cache keys correctly' do
+ subject.refresh_cache
+
+ # It does not update total issues cache
+ expect(Rails.cache.read(get_cache_key(subject, project_1))).to eq(nil)
+ expect(Rails.cache.read(get_cache_key(subject, project_2))).to eq(nil)
+
+ expect(Rails.cache.read(get_cache_key(subject, project_1, true))).to eq(1)
+ expect(Rails.cache.read(get_cache_key(subject, project_1, true))).to eq(1)
+ end
+ end
+
+ context 'when issues count is already cached' do
+ before do
+ create(:issue, project: project_2)
+ subject.refresh_cache
+ end
+
+ it 'does update cache again' do
+ expect(Rails.cache).not_to receive(:write)
+
+ subject.refresh_cache
+ end
+ end
+ end
+
+ def get_cache_key(subject, project, public_key = false)
+ service = subject.count_service.new(project)
+
+ if public_key
+ service.cache_key(service.class::PUBLIC_COUNT_KEY)
+ else
+ service.cache_key(service.class::TOTAL_COUNT_KEY)
+ end
+ end
+end
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index a8f003b1073..fd69fe04053 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -23,7 +23,7 @@ describe Projects::CreateService, '#execute' do
expect(project).to be_valid
expect(project.owner).to eq(user)
- expect(project.team.masters).to include(user)
+ expect(project.team.maintainers).to include(user)
expect(project.namespace).to eq(user.namespace)
end
end
@@ -47,7 +47,7 @@ describe Projects::CreateService, '#execute' do
expect(project).to be_persisted
expect(project.owner).to eq(user)
- expect(project.team.masters).to contain_exactly(user)
+ expect(project.team.maintainers).to contain_exactly(user)
expect(project.namespace).to eq(user.namespace)
end
end
@@ -236,6 +236,18 @@ describe Projects::CreateService, '#execute' do
end
end
+ context 'when readme initialization is requested' do
+ it 'creates README.md' do
+ opts[:initialize_with_readme] = '1'
+
+ project = create_project(user, opts)
+
+ expect(project.repository.commit_count).to be(1)
+ expect(project.repository.readme.name).to eql('README.md')
+ expect(project.repository.readme.data).to include('# GitLab')
+ end
+ end
+
context 'when there is an active service template' do
before do
create(:service, project: nil, template: true, active: true)
@@ -272,8 +284,11 @@ describe Projects::CreateService, '#execute' do
it 'writes project full path to .git/config' do
project = create_project(user, opts)
+ rugged = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ project.repository.rugged
+ end
- expect(project.repository.rugged.config['gitlab.fullpath']).to eq project.full_path
+ expect(rugged.config['gitlab.fullpath']).to eq project.full_path
end
def create_project(user, opts)
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index b63f409579e..38660ad7a01 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -5,7 +5,11 @@ describe Projects::DestroyService do
let!(:user) { create(:user) }
let!(:project) { create(:project, :repository, namespace: user.namespace) }
- let!(:path) { project.repository.path_to_repo }
+ let!(:path) do
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ project.repository.path_to_repo
+ end
+ end
let!(:remove_path) { path.sub(/\.git\Z/, "+#{project.id}+deleted.git") }
let!(:async) { false } # execute or async_execute
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index a93f6f1ddc2..f89f9b54f53 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -8,7 +8,7 @@ describe Projects::ForkService do
before do
@from_user = create(:user)
@from_namespace = @from_user.namespace
- avatar = fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")
+ avatar = fixture_file_upload("spec/fixtures/dk.png", "image/png")
@from_project = create(:project,
:repository,
creator_id: @from_user.id,
@@ -135,7 +135,7 @@ describe Projects::ForkService do
context "when project has restricted visibility level" do
context "and only one visibility level is restricted" do
before do
- @from_project.update_attributes(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
+ @from_project.update(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL])
end
diff --git a/spec/services/projects/gitlab_projects_import_service_spec.rb b/spec/services/projects/gitlab_projects_import_service_spec.rb
index ee1a886f5d6..0a898e9b89b 100644
--- a/spec/services/projects/gitlab_projects_import_service_spec.rb
+++ b/spec/services/projects/gitlab_projects_import_service_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Projects::GitlabProjectsImportService do
set(:namespace) { create(:namespace) }
let(:path) { 'test-path' }
- let(:file) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') }
+ let(:file) { fixture_file_upload('spec/fixtures/doc_sample.txt', 'text/plain') }
let(:overwrite) { false }
let(:import_params) { { namespace_id: namespace.id, path: path, file: file, overwrite: overwrite } }
subject { described_class.new(namespace.owner, import_params) }
diff --git a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb
index 7dca81eb59e..ed4930313c5 100644
--- a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb
+++ b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb
@@ -37,7 +37,11 @@ describe Projects::HashedStorage::MigrateRepositoryService do
it 'writes project full path to .git/config' do
service.execute
- expect(project.repository.rugged.config['gitlab.fullpath']).to eq project.full_path
+ rugged_config = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ project.repository.rugged.config['gitlab.fullpath']
+ end
+
+ expect(rugged_config).to eq project.full_path
end
end
diff --git a/spec/services/projects/housekeeping_service_spec.rb b/spec/services/projects/housekeeping_service_spec.rb
index b7b5de07380..1cf373d1d72 100644
--- a/spec/services/projects/housekeeping_service_spec.rb
+++ b/spec/services/projects/housekeeping_service_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Projects::HousekeepingService do
subject { described_class.new(project) }
- let(:project) { create(:project, :repository) }
+ set(:project) { create(:project, :repository) }
before do
project.reset_pushes_since_gc
@@ -16,12 +16,12 @@ describe Projects::HousekeepingService do
it 'enqueues a sidekiq job' do
expect(subject).to receive(:try_obtain_lease).and_return(:the_uuid)
expect(subject).to receive(:lease_key).and_return(:the_lease_key)
- expect(subject).to receive(:task).and_return(:the_task)
- expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :the_task, :the_lease_key, :the_uuid)
+ expect(subject).to receive(:task).and_return(:incremental_repack)
+ expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :incremental_repack, :the_lease_key, :the_uuid).and_call_original
- subject.execute
-
- expect(project.reload.pushes_since_gc).to eq(0)
+ Sidekiq::Testing.fake! do
+ expect { subject.execute }.to change(GitGarbageCollectWorker.jobs, :size).by(1)
+ end
end
it 'yields the block if given' do
@@ -30,6 +30,16 @@ describe Projects::HousekeepingService do
end.to yield_with_no_args
end
+ it 'resets counter after execution' do
+ expect(subject).to receive(:try_obtain_lease).and_return(:the_uuid)
+ allow(subject).to receive(:gc_period).and_return(1)
+ project.increment_pushes_since_gc
+
+ Sidekiq::Testing.inline! do
+ expect { subject.execute }.to change { project.pushes_since_gc }.to(0)
+ end
+ end
+
context 'when no lease can be obtained' do
before do
expect(subject).to receive(:try_obtain_lease).and_return(false)
@@ -54,6 +64,30 @@ describe Projects::HousekeepingService do
end.not_to yield_with_no_args
end
end
+
+ context 'task type' do
+ it 'goes through all three housekeeping tasks, executing only the highest task when there is overlap' do
+ allow(subject).to receive(:try_obtain_lease).and_return(:the_uuid)
+ allow(subject).to receive(:lease_key).and_return(:the_lease_key)
+
+ # At push 200
+ expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :gc, :the_lease_key, :the_uuid)
+ .exactly(1).times
+ # At push 50, 100, 150
+ expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :full_repack, :the_lease_key, :the_uuid)
+ .exactly(3).times
+ # At push 10, 20, ... (except those above)
+ expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :incremental_repack, :the_lease_key, :the_uuid)
+ .exactly(16).times
+
+ 201.times do
+ subject.increment!
+ subject.execute if subject.needed?
+ end
+
+ expect(project.pushes_since_gc).to eq(1)
+ end
+ end
end
describe '#needed?' do
@@ -69,31 +103,7 @@ describe Projects::HousekeepingService do
describe '#increment!' do
it 'increments the pushes_since_gc counter' do
- expect do
- subject.increment!
- end.to change { project.pushes_since_gc }.from(0).to(1)
+ expect { subject.increment! }.to change { project.pushes_since_gc }.by(1)
end
end
-
- it 'goes through all three housekeeping tasks, executing only the highest task when there is overlap' do
- allow(subject).to receive(:try_obtain_lease).and_return(:the_uuid)
- allow(subject).to receive(:lease_key).and_return(:the_lease_key)
-
- # At push 200
- expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :gc, :the_lease_key, :the_uuid)
- .exactly(1).times
- # At push 50, 100, 150
- expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :full_repack, :the_lease_key, :the_uuid)
- .exactly(3).times
- # At push 10, 20, ... (except those above)
- expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :incremental_repack, :the_lease_key, :the_uuid)
- .exactly(16).times
-
- 201.times do
- subject.increment!
- subject.execute if subject.needed?
- end
-
- expect(project.pushes_since_gc).to eq(1)
- end
end
diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb
index 30c89ebd821..b3815045792 100644
--- a/spec/services/projects/import_service_spec.rb
+++ b/spec/services/projects/import_service_spec.rb
@@ -3,9 +3,17 @@ require 'spec_helper'
describe Projects::ImportService do
let!(:project) { create(:project) }
let(:user) { project.creator }
+ let(:import_url) { 'http://www.gitlab.com/demo/repo.git' }
+ let(:oid_download_links) { { 'oid1' => "#{import_url}/gitlab-lfs/objects/oid1", 'oid2' => "#{import_url}/gitlab-lfs/objects/oid2" } }
subject { described_class.new(project, user) }
+ before do
+ allow(project).to receive(:lfs_enabled?).and_return(true)
+ allow_any_instance_of(Projects::LfsPointers::LfsDownloadService).to receive(:execute)
+ allow_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return(oid_download_links)
+ end
+
describe '#async?' do
it 'returns true for an asynchronous importer' do
importer_class = double(:importer, async?: true)
@@ -63,6 +71,15 @@ describe Projects::ImportService do
expect(result[:status]).to eq :error
expect(result[:message]).to eq "Error importing repository #{project.import_url} into #{project.full_path} - The repository could not be created."
end
+
+ context 'when repository creation succeeds' do
+ it 'does not download lfs files' do
+ expect_any_instance_of(Projects::LfsPointers::LfsImportService).not_to receive(:execute)
+ expect_any_instance_of(Projects::LfsPointers::LfsDownloadService).not_to receive(:execute)
+
+ subject.execute
+ end
+ end
end
context 'with known url' do
@@ -91,6 +108,15 @@ describe Projects::ImportService do
expect(result[:status]).to eq :error
end
+
+ context 'when repository import scheduled' do
+ it 'does not download lfs objects' do
+ expect_any_instance_of(Projects::LfsPointers::LfsImportService).not_to receive(:execute)
+ expect_any_instance_of(Projects::LfsPointers::LfsDownloadService).not_to receive(:execute)
+
+ subject.execute
+ end
+ end
end
context 'with a non Github repository' do
@@ -99,9 +125,10 @@ describe Projects::ImportService do
project.import_type = 'bitbucket'
end
- it 'succeeds if repository import is successfully' do
+ it 'succeeds if repository import is successfull' do
expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_return(true)
expect_any_instance_of(Gitlab::BitbucketImport::Importer).to receive(:execute).and_return(true)
+ expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return({})
result = subject.execute
@@ -116,6 +143,29 @@ describe Projects::ImportService do
expect(result[:status]).to eq :error
expect(result[:message]).to eq "Error importing repository #{project.import_url} into #{project.full_path} - Failed to import the repository"
end
+
+ context 'when repository import scheduled' do
+ before do
+ allow_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_return(true)
+ allow(subject).to receive(:import_data)
+ end
+
+ it 'downloads lfs objects if lfs_enabled is enabled for project' do
+ allow(project).to receive(:lfs_enabled?).and_return(true)
+ expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return(oid_download_links)
+ expect_any_instance_of(Projects::LfsPointers::LfsDownloadService).to receive(:execute).twice
+
+ subject.execute
+ end
+
+ it 'does not download lfs objects if lfs_enabled is not enabled for project' do
+ allow(project).to receive(:lfs_enabled?).and_return(false)
+ expect_any_instance_of(Projects::LfsPointers::LfsImportService).not_to receive(:execute)
+ expect_any_instance_of(Projects::LfsPointers::LfsDownloadService).not_to receive(:execute)
+
+ subject.execute
+ end
+ end
end
end
@@ -147,6 +197,26 @@ describe Projects::ImportService do
expect(result[:status]).to eq :error
end
+
+ context 'when importer' do
+ it 'has a custom repository importer it does not download lfs objects' do
+ allow(Gitlab::GithubImport::ParallelImporter).to receive(:imports_repository?).and_return(true)
+
+ expect_any_instance_of(Projects::LfsPointers::LfsImportService).not_to receive(:execute)
+ expect_any_instance_of(Projects::LfsPointers::LfsDownloadService).not_to receive(:execute)
+
+ subject.execute
+ end
+
+ it 'does not have a custom repository importer downloads lfs objects' do
+ allow(Gitlab::GithubImport::ParallelImporter).to receive(:imports_repository?).and_return(false)
+
+ expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return(oid_download_links)
+ expect_any_instance_of(Projects::LfsPointers::LfsDownloadService).to receive(:execute)
+
+ subject.execute
+ end
+ end
end
context 'with blocked import_URL' do
diff --git a/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb
new file mode 100644
index 00000000000..d7a2829d5f8
--- /dev/null
+++ b/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb
@@ -0,0 +1,102 @@
+require 'spec_helper'
+
+describe Projects::LfsPointers::LfsDownloadLinkListService do
+ let(:import_url) { 'http://www.gitlab.com/demo/repo.git' }
+ let(:lfs_endpoint) { "#{import_url}/info/lfs/objects/batch" }
+ let!(:project) { create(:project, import_url: import_url) }
+ let(:new_oids) { { 'oid1' => 123, 'oid2' => 125 } }
+ let(:remote_uri) { URI.parse(lfs_endpoint) }
+
+ let(:objects_response) do
+ body = new_oids.map do |oid, size|
+ {
+ 'oid' => oid,
+ 'size' => size,
+ 'actions' => {
+ 'download' => { 'href' => "#{import_url}/gitlab-lfs/objects/#{oid}" }
+ }
+ }
+ end
+
+ Struct.new(:success?, :objects).new(true, body)
+ end
+
+ let(:invalid_object_response) do
+ [
+ 'oid' => 'whatever',
+ 'size' => 123
+ ]
+ end
+
+ subject { described_class.new(project, remote_uri: remote_uri) }
+
+ before do
+ allow(project).to receive(:lfs_enabled?).and_return(true)
+ allow(Gitlab::HTTP).to receive(:post).and_return(objects_response)
+ end
+
+ describe '#execute' do
+ it 'retrieves each download link of every non existent lfs object' do
+ subject.execute(new_oids).each do |oid, link|
+ expect(link).to eq "#{import_url}/gitlab-lfs/objects/#{oid}"
+ end
+ end
+
+ context 'credentials' do
+ context 'when the download link and the lfs_endpoint have the same host' do
+ context 'when lfs_endpoint has credentials' do
+ let(:import_url) { 'http://user:password@www.gitlab.com/demo/repo.git' }
+
+ it 'adds credentials to the download_link' do
+ result = subject.execute(new_oids)
+
+ result.each do |oid, link|
+ expect(link.starts_with?('http://user:password@')).to be_truthy
+ end
+ end
+ end
+
+ context 'when lfs_endpoint does not have any credentials' do
+ it 'does not add any credentials' do
+ result = subject.execute(new_oids)
+
+ result.each do |oid, link|
+ expect(link.starts_with?('http://user:password@')).to be_falsey
+ end
+ end
+ end
+ end
+
+ context 'when the download link and the lfs_endpoint have different hosts' do
+ let(:import_url_with_credentials) { 'http://user:password@www.otherdomain.com/demo/repo.git' }
+ let(:lfs_endpoint) { "#{import_url_with_credentials}/info/lfs/objects/batch" }
+
+ it 'downloads without any credentials' do
+ result = subject.execute(new_oids)
+
+ result.each do |oid, link|
+ expect(link.starts_with?('http://user:password@')).to be_falsey
+ end
+ end
+ end
+ end
+ end
+
+ describe '#get_download_links' do
+ it 'raise errorif request fails' do
+ allow(Gitlab::HTTP).to receive(:post).and_return(Struct.new(:success?, :message).new(false, 'Failed request'))
+
+ expect { subject.send(:get_download_links, new_oids) }.to raise_error(described_class::DownloadLinksError)
+ end
+ end
+
+ describe '#parse_response_links' do
+ it 'does not add oid entry if href not found' do
+ expect(Rails.logger).to receive(:error).with("Link for Lfs Object with oid whatever not found or invalid.")
+
+ result = subject.send(:parse_response_links, invalid_object_response)
+
+ expect(result).to be_empty
+ end
+ end
+end
diff --git a/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb
new file mode 100644
index 00000000000..6af5bfc7689
--- /dev/null
+++ b/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb
@@ -0,0 +1,69 @@
+require 'spec_helper'
+
+describe Projects::LfsPointers::LfsDownloadService do
+ let(:project) { create(:project) }
+ let(:oid) { '9e548e25631dd9ce6b43afd6359ab76da2819d6a5b474e66118c7819e1d8b3e8' }
+ let(:download_link) { "http://gitlab.com/#{oid}" }
+ let(:lfs_content) do
+ <<~HEREDOC
+ whatever
+ HEREDOC
+ end
+
+ subject { described_class.new(project) }
+
+ before do
+ allow(project).to receive(:lfs_enabled?).and_return(true)
+ WebMock.stub_request(:get, download_link).to_return(body: lfs_content)
+ end
+
+ describe '#execute' do
+ context 'when file download succeeds' do
+ it 'a new lfs object is created' do
+ expect { subject.execute(oid, download_link) }.to change { LfsObject.count }.from(0).to(1)
+ end
+
+ it 'has the same oid' do
+ subject.execute(oid, download_link)
+
+ expect(LfsObject.first.oid).to eq oid
+ end
+
+ it 'stores the content' do
+ subject.execute(oid, download_link)
+
+ expect(File.read(LfsObject.first.file.file.file)).to eq lfs_content
+ end
+ end
+
+ context 'when file download fails' do
+ it 'no lfs object is created' do
+ expect { subject.execute(oid, download_link) }.to change { LfsObject.count }
+ end
+ end
+
+ context 'when credentials present' do
+ let(:download_link_with_credentials) { "http://user:password@gitlab.com/#{oid}" }
+
+ before do
+ WebMock.stub_request(:get, download_link).with(headers: { 'Authorization' => 'Basic dXNlcjpwYXNzd29yZA==' }).to_return(body: lfs_content)
+ end
+
+ it 'the request adds authorization headers' do
+ subject.execute(oid, download_link_with_credentials)
+ end
+ end
+
+ context 'when an lfs object with the same oid already exists' do
+ before do
+ create(:lfs_object, oid: 'oid')
+ end
+
+ it 'does not download the file' do
+ expect(subject).not_to receive(:download_and_save_file)
+
+ subject.execute('oid', download_link)
+ end
+ end
+ end
+end
diff --git a/spec/services/projects/lfs_pointers/lfs_import_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_import_service_spec.rb
new file mode 100644
index 00000000000..5a75fb38dec
--- /dev/null
+++ b/spec/services/projects/lfs_pointers/lfs_import_service_spec.rb
@@ -0,0 +1,146 @@
+require 'spec_helper'
+
+describe Projects::LfsPointers::LfsImportService do
+ let(:import_url) { 'http://www.gitlab.com/demo/repo.git' }
+ let(:default_endpoint) { "#{import_url}/info/lfs/objects/batch"}
+ let(:group) { create(:group, lfs_enabled: true)}
+ let!(:project) { create(:project, namespace: group, import_url: import_url, lfs_enabled: true) }
+ let!(:lfs_objects_project) { create_list(:lfs_objects_project, 2, project: project) }
+ let!(:existing_lfs_objects) { LfsObject.pluck(:oid, :size).to_h }
+ let(:oids) { { 'oid1' => 123, 'oid2' => 125 } }
+ let(:oid_download_links) { { 'oid1' => "#{import_url}/gitlab-lfs/objects/oid1", 'oid2' => "#{import_url}/gitlab-lfs/objects/oid2" } }
+ let(:all_oids) { existing_lfs_objects.merge(oids) }
+ let(:remote_uri) { URI.parse(lfs_endpoint) }
+
+ subject { described_class.new(project) }
+
+ before do
+ allow(project.repository).to receive(:lfsconfig_for).and_return(nil)
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ allow_any_instance_of(Projects::LfsPointers::LfsListService).to receive(:execute).and_return(all_oids)
+ end
+
+ describe '#execute' do
+ context 'when no lfs pointer is linked' do
+ before do
+ allow_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute).and_return([])
+ allow_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).and_return(oid_download_links)
+ expect(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:new).with(project, remote_uri: URI.parse(default_endpoint)).and_call_original
+ end
+
+ it 'retrieves all lfs pointers in the project repository' do
+ expect_any_instance_of(Projects::LfsPointers::LfsListService).to receive(:execute)
+
+ subject.execute
+ end
+
+ it 'links existent lfs objects to the project' do
+ expect_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute)
+
+ subject.execute
+ end
+
+ it 'retrieves the download links of non existent objects' do
+ expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).with(all_oids)
+
+ subject.execute
+ end
+ end
+
+ context 'when some lfs objects are linked' do
+ before do
+ allow_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute).and_return(existing_lfs_objects.keys)
+ allow_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).and_return(oid_download_links)
+ end
+
+ it 'retrieves the download links of non existent objects' do
+ expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).with(oids)
+
+ subject.execute
+ end
+ end
+
+ context 'when all lfs objects are linked' do
+ before do
+ allow_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute).and_return(all_oids.keys)
+ allow_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute)
+ end
+
+ it 'retrieves no download links' do
+ expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).with({}).and_call_original
+
+ expect(subject.execute).to be_empty
+ end
+ end
+
+ context 'when lfsconfig file exists' do
+ before do
+ allow(project.repository).to receive(:lfsconfig_for).and_return("[lfs]\n\turl = #{lfs_endpoint}\n")
+ end
+
+ context 'when url points to the same import url host' do
+ let(:lfs_endpoint) { "#{import_url}/different_endpoint" }
+ let(:service) { double }
+
+ before do
+ allow(service).to receive(:execute)
+ end
+ it 'downloads lfs object using the new endpoint' do
+ expect(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:new).with(project, remote_uri: remote_uri).and_return(service)
+
+ subject.execute
+ end
+
+ context 'when import url has credentials' do
+ let(:import_url) { 'http://user:password@www.gitlab.com/demo/repo.git'}
+
+ it 'adds the credentials to the new endpoint' do
+ expect(Projects::LfsPointers::LfsDownloadLinkListService)
+ .to receive(:new).with(project, remote_uri: URI.parse("http://user:password@www.gitlab.com/demo/repo.git/different_endpoint"))
+ .and_return(service)
+
+ subject.execute
+ end
+
+ context 'when url has its own credentials' do
+ let(:lfs_endpoint) { "http://user1:password1@www.gitlab.com/demo/repo.git/different_endpoint" }
+
+ it 'does not add the import url credentials' do
+ expect(Projects::LfsPointers::LfsDownloadLinkListService)
+ .to receive(:new).with(project, remote_uri: remote_uri)
+ .and_return(service)
+
+ subject.execute
+ end
+ end
+ end
+ end
+
+ context 'when url points to a third party service' do
+ let(:lfs_endpoint) { 'http://third_party_service.com/info/lfs/objects/' }
+
+ it 'disables lfs from the project' do
+ expect(project.lfs_enabled?).to be_truthy
+
+ subject.execute
+
+ expect(project.lfs_enabled?).to be_falsey
+ end
+
+ it 'does not download anything' do
+ expect_any_instance_of(Projects::LfsPointers::LfsListService).not_to receive(:execute)
+
+ subject.execute
+ end
+ end
+ end
+ end
+
+ describe '#default_endpoint_uri' do
+ let(:import_url) { 'http://www.gitlab.com/demo/repo' }
+
+ it 'adds suffix .git if the url does not have it' do
+ expect(subject.send(:default_endpoint_uri).path).to match(/repo.git/)
+ end
+ end
+end
diff --git a/spec/services/projects/lfs_pointers/lfs_link_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_link_service_spec.rb
new file mode 100644
index 00000000000..b7b153655db
--- /dev/null
+++ b/spec/services/projects/lfs_pointers/lfs_link_service_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe Projects::LfsPointers::LfsLinkService do
+ let!(:project) { create(:project, lfs_enabled: true) }
+ let!(:lfs_objects_project) { create_list(:lfs_objects_project, 2, project: project) }
+ let(:new_oids) { { 'oid1' => 123, 'oid2' => 125 } }
+ let(:all_oids) { LfsObject.pluck(:oid, :size).to_h.merge(new_oids) }
+ let(:new_lfs_object) { create(:lfs_object) }
+ let(:new_oid_list) { all_oids.merge(new_lfs_object.oid => new_lfs_object.size) }
+
+ subject { described_class.new(project) }
+
+ before do
+ allow(project).to receive(:lfs_enabled?).and_return(true)
+ end
+
+ describe '#execute' do
+ it 'links existing lfs objects to the project' do
+ expect(project.all_lfs_objects.count).to eq 2
+
+ linked = subject.execute(new_oid_list.keys)
+
+ expect(project.all_lfs_objects.count).to eq 3
+ expect(linked.size).to eq 3
+ end
+
+ it 'returns linked oids' do
+ linked = lfs_objects_project.map(&:lfs_object).map(&:oid) << new_lfs_object.oid
+
+ expect(subject.execute(new_oid_list.keys)).to eq linked
+ end
+ end
+end
diff --git a/spec/services/projects/move_access_service_spec.rb b/spec/services/projects/move_access_service_spec.rb
index a820ebd91f4..88d9d93c33b 100644
--- a/spec/services/projects/move_access_service_spec.rb
+++ b/spec/services/projects/move_access_service_spec.rb
@@ -4,18 +4,18 @@ describe Projects::MoveAccessService do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project_with_access) { create(:project, namespace: user.namespace) }
- let(:master_user) { create(:user) }
+ let(:maintainer_user) { create(:user) }
let(:reporter_user) { create(:user) }
let(:developer_user) { create(:user) }
- let(:master_group) { create(:group) }
+ let(:maintainer_group) { create(:group) }
let(:reporter_group) { create(:group) }
let(:developer_group) { create(:group) }
before do
- project_with_access.add_master(master_user)
+ project_with_access.add_maintainer(maintainer_user)
project_with_access.add_developer(developer_user)
project_with_access.add_reporter(reporter_user)
- project_with_access.project_group_links.create(group: master_group, group_access: Gitlab::Access::MASTER)
+ project_with_access.project_group_links.create(group: maintainer_group, group_access: Gitlab::Access::MAINTAINER)
project_with_access.project_group_links.create(group: developer_group, group_access: Gitlab::Access::DEVELOPER)
project_with_access.project_group_links.create(group: reporter_group, group_access: Gitlab::Access::REPORTER)
end
@@ -87,7 +87,7 @@ describe Projects::MoveAccessService do
let(:options) { { remove_remaining_elements: false } }
it 'does not remove remaining memberships' do
- target_project.add_master(master_user)
+ target_project.add_maintainer(maintainer_user)
subject.execute(project_with_access, options)
@@ -95,7 +95,7 @@ describe Projects::MoveAccessService do
end
it 'does not remove remaining group links' do
- target_project.project_group_links.create(group: master_group, group_access: Gitlab::Access::MASTER)
+ target_project.project_group_links.create(group: maintainer_group, group_access: Gitlab::Access::MAINTAINER)
subject.execute(project_with_access, options)
diff --git a/spec/services/projects/move_project_authorizations_service_spec.rb b/spec/services/projects/move_project_authorizations_service_spec.rb
index f7262b9b887..b4408393624 100644
--- a/spec/services/projects/move_project_authorizations_service_spec.rb
+++ b/spec/services/projects/move_project_authorizations_service_spec.rb
@@ -4,7 +4,7 @@ describe Projects::MoveProjectAuthorizationsService do
let!(:user) { create(:user) }
let(:project_with_users) { create(:project, namespace: user.namespace) }
let(:target_project) { create(:project, namespace: user.namespace) }
- let(:master_user) { create(:user) }
+ let(:maintainer_user) { create(:user) }
let(:reporter_user) { create(:user) }
let(:developer_user) { create(:user) }
@@ -12,7 +12,7 @@ describe Projects::MoveProjectAuthorizationsService do
describe '#execute' do
before do
- project_with_users.add_master(master_user)
+ project_with_users.add_maintainer(maintainer_user)
project_with_users.add_developer(developer_user)
project_with_users.add_reporter(reporter_user)
end
@@ -28,7 +28,7 @@ describe Projects::MoveProjectAuthorizationsService do
end
it 'does not move existent authorizations to the current project' do
- target_project.add_master(developer_user)
+ target_project.add_maintainer(developer_user)
target_project.add_developer(reporter_user)
expect(project_with_users.authorized_users.count).to eq 4
@@ -44,7 +44,7 @@ describe Projects::MoveProjectAuthorizationsService do
let(:options) { { remove_remaining_elements: false } }
it 'does not remove remaining project authorizations' do
- target_project.add_master(developer_user)
+ target_project.add_maintainer(developer_user)
target_project.add_developer(reporter_user)
subject.execute(project_with_users, options)
diff --git a/spec/services/projects/move_project_group_links_service_spec.rb b/spec/services/projects/move_project_group_links_service_spec.rb
index e3d06e6d3d7..7ca8cf304fe 100644
--- a/spec/services/projects/move_project_group_links_service_spec.rb
+++ b/spec/services/projects/move_project_group_links_service_spec.rb
@@ -4,7 +4,7 @@ describe Projects::MoveProjectGroupLinksService do
let!(:user) { create(:user) }
let(:project_with_groups) { create(:project, namespace: user.namespace) }
let(:target_project) { create(:project, namespace: user.namespace) }
- let(:master_group) { create(:group) }
+ let(:maintainer_group) { create(:group) }
let(:reporter_group) { create(:group) }
let(:developer_group) { create(:group) }
@@ -12,7 +12,7 @@ describe Projects::MoveProjectGroupLinksService do
describe '#execute' do
before do
- project_with_groups.project_group_links.create(group: master_group, group_access: Gitlab::Access::MASTER)
+ project_with_groups.project_group_links.create(group: maintainer_group, group_access: Gitlab::Access::MAINTAINER)
project_with_groups.project_group_links.create(group: developer_group, group_access: Gitlab::Access::DEVELOPER)
project_with_groups.project_group_links.create(group: reporter_group, group_access: Gitlab::Access::REPORTER)
end
@@ -28,7 +28,7 @@ describe Projects::MoveProjectGroupLinksService do
end
it 'does not move existent group links in the current project' do
- target_project.project_group_links.create(group: master_group, group_access: Gitlab::Access::MASTER)
+ target_project.project_group_links.create(group: maintainer_group, group_access: Gitlab::Access::MAINTAINER)
target_project.project_group_links.create(group: developer_group, group_access: Gitlab::Access::DEVELOPER)
expect(project_with_groups.project_group_links.count).to eq 3
@@ -53,7 +53,7 @@ describe Projects::MoveProjectGroupLinksService do
let(:options) { { remove_remaining_elements: false } }
it 'does not remove remaining project group links' do
- target_project.project_group_links.create(group: master_group, group_access: Gitlab::Access::MASTER)
+ target_project.project_group_links.create(group: maintainer_group, group_access: Gitlab::Access::MAINTAINER)
target_project.project_group_links.create(group: developer_group, group_access: Gitlab::Access::DEVELOPER)
subject.execute(project_with_groups, options)
diff --git a/spec/services/projects/move_project_members_service_spec.rb b/spec/services/projects/move_project_members_service_spec.rb
index 9c9a2d2fde1..c8c0eac1f13 100644
--- a/spec/services/projects/move_project_members_service_spec.rb
+++ b/spec/services/projects/move_project_members_service_spec.rb
@@ -4,7 +4,7 @@ describe Projects::MoveProjectMembersService do
let!(:user) { create(:user) }
let(:project_with_users) { create(:project, namespace: user.namespace) }
let(:target_project) { create(:project, namespace: user.namespace) }
- let(:master_user) { create(:user) }
+ let(:maintainer_user) { create(:user) }
let(:reporter_user) { create(:user) }
let(:developer_user) { create(:user) }
@@ -12,7 +12,7 @@ describe Projects::MoveProjectMembersService do
describe '#execute' do
before do
- project_with_users.add_master(master_user)
+ project_with_users.add_maintainer(maintainer_user)
project_with_users.add_developer(developer_user)
project_with_users.add_reporter(reporter_user)
end
@@ -28,7 +28,7 @@ describe Projects::MoveProjectMembersService do
end
it 'does not move existent members to the current project' do
- target_project.add_master(developer_user)
+ target_project.add_maintainer(developer_user)
target_project.add_developer(reporter_user)
expect(project_with_users.project_members.count).to eq 4
@@ -53,7 +53,7 @@ describe Projects::MoveProjectMembersService do
let(:options) { { remove_remaining_elements: false } }
it 'does not remove remaining project members' do
- target_project.add_master(developer_user)
+ target_project.add_maintainer(developer_user)
target_project.add_developer(reporter_user)
subject.execute(project_with_users, options)
diff --git a/spec/services/projects/open_issues_count_service_spec.rb b/spec/services/projects/open_issues_count_service_spec.rb
index f964f9972cd..562c14a8df8 100644
--- a/spec/services/projects/open_issues_count_service_spec.rb
+++ b/spec/services/projects/open_issues_count_service_spec.rb
@@ -2,20 +2,88 @@ require 'spec_helper'
describe Projects::OpenIssuesCountService do
describe '#count' do
- it 'returns the number of open issues' do
- project = create(:project)
- create(:issue, :opened, project: project)
+ let(:project) { create(:project) }
- expect(described_class.new(project).count).to eq(1)
+ context 'when user is nil' do
+ it 'does not include confidential issues in the issue count' do
+ create(:issue, :opened, project: project)
+ create(:issue, :opened, confidential: true, project: project)
+
+ expect(described_class.new(project).count).to eq(1)
+ end
+ end
+
+ context 'when user is provided' do
+ let(:user) { create(:user) }
+
+ context 'when user can read confidential issues' do
+ before do
+ project.add_reporter(user)
+ end
+
+ it 'returns the right count with confidential issues' do
+ create(:issue, :opened, project: project)
+ create(:issue, :opened, confidential: true, project: project)
+
+ expect(described_class.new(project, user).count).to eq(2)
+ end
+
+ it 'uses total_open_issues_count cache key' do
+ expect(described_class.new(project, user).cache_key_name).to eq('total_open_issues_count')
+ end
+ end
+
+ context 'when user cannot read confidential issues' do
+ before do
+ project.add_guest(user)
+ end
+
+ it 'does not include confidential issues' do
+ create(:issue, :opened, project: project)
+ create(:issue, :opened, confidential: true, project: project)
+
+ expect(described_class.new(project, user).count).to eq(1)
+ end
+
+ it 'uses public_open_issues_count cache key' do
+ expect(described_class.new(project, user).cache_key_name).to eq('public_open_issues_count')
+ end
+ end
end
- it 'does not include confidential issues in the issue count' do
- project = create(:project)
+ context '#refresh_cache', :use_clean_rails_memory_store_caching do
+ let(:subject) { described_class.new(project) }
+
+ before do
+ create(:issue, :opened, project: project)
+ create(:issue, :opened, project: project)
+ create(:issue, :opened, confidential: true, project: project)
+ end
+
+ context 'when cache is empty' do
+ it 'refreshes cache keys correctly' do
+ subject.refresh_cache
+
+ expect(Rails.cache.read(subject.cache_key(described_class::PUBLIC_COUNT_KEY))).to eq(2)
+ expect(Rails.cache.read(subject.cache_key(described_class::TOTAL_COUNT_KEY))).to eq(3)
+ end
+ end
+
+ context 'when cache is outdated' do
+ before do
+ subject.refresh_cache
+ end
+
+ it 'refreshes cache keys correctly' do
+ create(:issue, :opened, project: project)
+ create(:issue, :opened, confidential: true, project: project)
- create(:issue, :opened, project: project)
- create(:issue, :opened, confidential: true, project: project)
+ subject.refresh_cache
- expect(described_class.new(project).count).to eq(1)
+ expect(Rails.cache.read(subject.cache_key(described_class::PUBLIC_COUNT_KEY))).to eq(3)
+ expect(Rails.cache.read(subject.cache_key(described_class::TOTAL_COUNT_KEY))).to eq(5)
+ end
+ end
end
end
end
diff --git a/spec/services/projects/overwrite_project_service_spec.rb b/spec/services/projects/overwrite_project_service_spec.rb
index 252c61f4224..c7900629f5f 100644
--- a/spec/services/projects/overwrite_project_service_spec.rb
+++ b/spec/services/projects/overwrite_project_service_spec.rb
@@ -96,10 +96,10 @@ describe Projects::OverwriteProjectService do
context 'when project with elements' do
it_behaves_like 'overwrite actions' do
- let(:master_user) { create(:user) }
+ let(:maintainer_user) { create(:user) }
let(:reporter_user) { create(:user) }
let(:developer_user) { create(:user) }
- let(:master_group) { create(:group) }
+ let(:maintainer_group) { create(:group) }
let(:reporter_group) { create(:group) }
let(:developer_group) { create(:group) }
@@ -107,10 +107,10 @@ describe Projects::OverwriteProjectService do
create_list(:deploy_keys_project, 2, project: project_from)
create_list(:notification_setting, 2, source: project_from)
create_list(:users_star_project, 2, project: project_from)
- project_from.project_group_links.create(group: master_group, group_access: Gitlab::Access::MASTER)
+ project_from.project_group_links.create(group: maintainer_group, group_access: Gitlab::Access::MAINTAINER)
project_from.project_group_links.create(group: developer_group, group_access: Gitlab::Access::DEVELOPER)
project_from.project_group_links.create(group: reporter_group, group_access: Gitlab::Access::REPORTER)
- project_from.add_master(master_user)
+ project_from.add_maintainer(maintainer_user)
project_from.add_developer(developer_user)
project_from.add_reporter(reporter_user)
end
diff --git a/spec/services/projects/participants_service_spec.rb b/spec/services/projects/participants_service_spec.rb
index 0d18ceb8ff8..6040f9100f8 100644
--- a/spec/services/projects/participants_service_spec.rb
+++ b/spec/services/projects/participants_service_spec.rb
@@ -4,7 +4,7 @@ describe Projects::ParticipantsService do
describe '#groups' do
describe 'avatar_url' do
let(:project) { create(:project, :public) }
- let(:group) { create(:group, avatar: fixture_file_upload(Rails.root + 'spec/fixtures/dk.png')) }
+ let(:group) { create(:group, avatar: fixture_file_upload('spec/fixtures/dk.png')) }
let(:user) { create(:user) }
let!(:group_member) { create(:group_member, group: group, user: user) }
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index 3e6483d7e28..7e85f599afb 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -64,7 +64,7 @@ describe Projects::TransferService do
it 'updates project full path in .git/config' do
transfer_project(project, user, group)
- expect(project.repository.rugged.config['gitlab.fullpath']).to eq "#{group.full_path}/#{project.path}"
+ expect(rugged_config['gitlab.fullpath']).to eq "#{group.full_path}/#{project.path}"
end
end
@@ -84,7 +84,9 @@ describe Projects::TransferService do
end
def project_path(project)
- project.repository.path_to_repo
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ project.repository.path_to_repo
+ end
end
def current_path
@@ -101,7 +103,7 @@ describe Projects::TransferService do
it 'rolls back project full path in .git/config' do
attempt_project_transfer
- expect(project.repository.rugged.config['gitlab.fullpath']).to eq project.full_path
+ expect(rugged_config['gitlab.fullpath']).to eq project.full_path
end
it "doesn't send move notifications" do
@@ -245,7 +247,7 @@ describe Projects::TransferService do
let(:group_member) { create(:user) }
before do
- group.add_user(owner, GroupMember::MASTER)
+ group.add_user(owner, GroupMember::MAINTAINER)
group.add_user(group_member, GroupMember::DEVELOPER)
end
@@ -264,4 +266,10 @@ describe Projects::TransferService do
transfer_project(project, owner, group)
end
end
+
+ def rugged_config
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ project.repository.rugged.config
+ end
+ end
end
diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb
index 347ac13828c..a4c103e6f30 100644
--- a/spec/services/projects/update_pages_service_spec.rb
+++ b/spec/services/projects/update_pages_service_spec.rb
@@ -4,13 +4,13 @@ describe Projects::UpdatePagesService do
set(:project) { create(:project, :repository) }
set(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit('HEAD').sha) }
set(:build) { create(:ci_build, pipeline: pipeline, ref: 'HEAD') }
- let(:invalid_file) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png') }
+ let(:invalid_file) { fixture_file_upload('spec/fixtures/dk.png') }
let(:extension) { 'zip' }
- let(:file) { fixture_file_upload(Rails.root + "spec/fixtures/pages.#{extension}") }
- let(:empty_file) { fixture_file_upload(Rails.root + "spec/fixtures/pages_empty.#{extension}") }
+ let(:file) { fixture_file_upload("spec/fixtures/pages.#{extension}") }
+ let(:empty_file) { fixture_file_upload("spec/fixtures/pages_empty.#{extension}") }
let(:metadata) do
- filename = Rails.root + "spec/fixtures/pages.#{extension}.meta"
+ filename = "spec/fixtures/pages.#{extension}.meta"
fixture_file_upload(filename) if File.exist?(filename)
end
@@ -24,8 +24,8 @@ describe Projects::UpdatePagesService do
let(:extension) { 'zip' }
before do
- build.update_attributes(legacy_artifacts_file: file)
- build.update_attributes(legacy_artifacts_metadata: metadata)
+ build.update(legacy_artifacts_file: file)
+ build.update(legacy_artifacts_metadata: metadata)
end
describe 'pages artifacts' do
@@ -62,13 +62,13 @@ describe Projects::UpdatePagesService do
end
it 'fails if sha on branch is not latest' do
- build.update_attributes(ref: 'feature')
+ build.update(ref: 'feature')
expect(execute).not_to eq(:success)
end
it 'fails for empty file fails' do
- build.update_attributes(legacy_artifacts_file: empty_file)
+ build.update(legacy_artifacts_file: empty_file)
expect { execute }
.to raise_error(Projects::UpdatePagesService::FailedToExtractError)
@@ -118,7 +118,7 @@ describe Projects::UpdatePagesService do
end
it 'fails if sha on branch is not latest' do
- build.update_attributes(ref: 'feature')
+ build.update(ref: 'feature')
expect(execute).not_to eq(:success)
end
@@ -188,7 +188,7 @@ describe Projects::UpdatePagesService do
end
it 'fails for invalid archive' do
- build.update_attributes(legacy_artifacts_file: invalid_file)
+ build.update(legacy_artifacts_file: invalid_file)
expect(execute).not_to eq(:success)
end
@@ -196,11 +196,11 @@ describe Projects::UpdatePagesService do
let(:metadata) { spy('metadata') }
before do
- file = fixture_file_upload(Rails.root + 'spec/fixtures/pages.zip')
- metafile = fixture_file_upload(Rails.root + 'spec/fixtures/pages.zip.meta')
+ file = fixture_file_upload('spec/fixtures/pages.zip')
+ metafile = fixture_file_upload('spec/fixtures/pages.zip.meta')
- build.update_attributes(legacy_artifacts_file: file)
- build.update_attributes(legacy_artifacts_metadata: metafile)
+ build.update(legacy_artifacts_file: file)
+ build.update(legacy_artifacts_metadata: metafile)
allow(build).to receive(:artifacts_metadata_entry)
.and_return(metadata)
diff --git a/spec/services/projects/update_remote_mirror_service_spec.rb b/spec/services/projects/update_remote_mirror_service_spec.rb
index 723cb374c37..5c2e79ff9af 100644
--- a/spec/services/projects/update_remote_mirror_service_spec.rb
+++ b/spec/services/projects/update_remote_mirror_service_spec.rb
@@ -1,7 +1,8 @@
require 'spec_helper'
describe Projects::UpdateRemoteMirrorService do
- let(:project) { create(:project, :repository) }
+ set(:project) { create(:project, :repository) }
+ let(:owner) { project.owner }
let(:remote_project) { create(:forked_project_with_submodules) }
let(:repository) { project.repository }
let(:raw_repository) { repository.raw }
@@ -9,13 +10,11 @@ describe Projects::UpdateRemoteMirrorService do
subject { described_class.new(project, project.creator) }
- describe "#execute", :skip_gitaly_mock do
+ describe "#execute" do
before do
- create_branch(repository, 'existing-branch')
- allow(raw_repository).to receive(:remote_tags) do
- generate_tags(repository, 'v1.0.0', 'v1.1.0')
- end
- allow(raw_repository).to receive(:push_remote_branches).and_return(true)
+ repository.add_branch(owner, 'existing-branch', 'master')
+
+ allow(remote_mirror).to receive(:update_repository).and_return(true)
end
it "fetches the remote repository" do
@@ -34,307 +33,57 @@ describe Projects::UpdateRemoteMirrorService do
expect(result[:status]).to eq(:success)
end
- describe 'Syncing branches' do
+ context 'when syncing all branches' do
it "push all the branches the first time" do
allow(repository).to receive(:fetch_remote)
- expect(raw_repository).to receive(:push_remote_branches).with(remote_mirror.remote_name, local_branch_names)
-
- subject.execute(remote_mirror)
- end
-
- it "does not push anything is remote is up to date" do
- allow(repository).to receive(:fetch_remote) { sync_remote(repository, remote_mirror.remote_name, local_branch_names) }
-
- expect(raw_repository).not_to receive(:push_remote_branches)
-
- subject.execute(remote_mirror)
- end
-
- it "sync new branches" do
- # call local_branch_names early so it is not called after the new branch has been created
- current_branches = local_branch_names
- allow(repository).to receive(:fetch_remote) { sync_remote(repository, remote_mirror.remote_name, current_branches) }
- create_branch(repository, 'my-new-branch')
-
- expect(raw_repository).to receive(:push_remote_branches).with(remote_mirror.remote_name, ['my-new-branch'])
-
- subject.execute(remote_mirror)
- end
-
- it "sync updated branches" do
- allow(repository).to receive(:fetch_remote) do
- sync_remote(repository, remote_mirror.remote_name, local_branch_names)
- update_branch(repository, 'existing-branch')
- end
-
- expect(raw_repository).to receive(:push_remote_branches).with(remote_mirror.remote_name, ['existing-branch'])
+ expect(remote_mirror).to receive(:update_repository).with({})
subject.execute(remote_mirror)
end
-
- context 'when push only protected branches option is set' do
- let(:unprotected_branch_name) { 'existing-branch' }
- let(:protected_branch_name) do
- project.repository.branch_names.find { |n| n != unprotected_branch_name }
- end
- let!(:protected_branch) do
- create(:protected_branch, project: project, name: protected_branch_name)
- end
-
- before do
- project.reload
- remote_mirror.only_protected_branches = true
- end
-
- it "sync updated protected branches" do
- allow(repository).to receive(:fetch_remote) do
- sync_remote(repository, remote_mirror.remote_name, local_branch_names)
- update_branch(repository, protected_branch_name)
- end
-
- expect(raw_repository).to receive(:push_remote_branches).with(remote_mirror.remote_name, [protected_branch_name])
-
- subject.execute(remote_mirror)
- end
-
- it 'does not sync unprotected branches' do
- allow(repository).to receive(:fetch_remote) do
- sync_remote(repository, remote_mirror.remote_name, local_branch_names)
- update_branch(repository, unprotected_branch_name)
- end
-
- expect(raw_repository).not_to receive(:push_remote_branches).with(remote_mirror.remote_name, [unprotected_branch_name])
-
- subject.execute(remote_mirror)
- end
- end
-
- context 'when branch exists in local and remote repo' do
- context 'when it has diverged' do
- it 'syncs branches' do
- allow(repository).to receive(:fetch_remote) do
- sync_remote(repository, remote_mirror.remote_name, local_branch_names)
- update_remote_branch(repository, remote_mirror.remote_name, 'markdown')
- end
-
- expect(raw_repository).to receive(:push_remote_branches).with(remote_mirror.remote_name, ['markdown'])
-
- subject.execute(remote_mirror)
- end
- end
- end
-
- describe 'for delete' do
- context 'when branch exists in local and remote repo' do
- it 'deletes the branch from remote repo' do
- allow(repository).to receive(:fetch_remote) do
- sync_remote(repository, remote_mirror.remote_name, local_branch_names)
- delete_branch(repository, 'existing-branch')
- end
-
- expect(raw_repository).to receive(:delete_remote_branches).with(remote_mirror.remote_name, ['existing-branch'])
-
- subject.execute(remote_mirror)
- end
- end
-
- context 'when push only protected branches option is set' do
- before do
- remote_mirror.only_protected_branches = true
- end
-
- context 'when branch exists in local and remote repo' do
- let!(:protected_branch_name) { local_branch_names.first }
-
- before do
- create(:protected_branch, project: project, name: protected_branch_name)
- project.reload
- end
-
- it 'deletes the protected branch from remote repo' do
- allow(repository).to receive(:fetch_remote) do
- sync_remote(repository, remote_mirror.remote_name, local_branch_names)
- delete_branch(repository, protected_branch_name)
- end
-
- expect(raw_repository).not_to receive(:delete_remote_branches).with(remote_mirror.remote_name, [protected_branch_name])
-
- subject.execute(remote_mirror)
- end
-
- it 'does not delete the unprotected branch from remote repo' do
- allow(repository).to receive(:fetch_remote) do
- sync_remote(repository, remote_mirror.remote_name, local_branch_names)
- delete_branch(repository, 'existing-branch')
- end
-
- expect(raw_repository).not_to receive(:delete_remote_branches).with(remote_mirror.remote_name, ['existing-branch'])
-
- subject.execute(remote_mirror)
- end
- end
-
- context 'when branch only exists on remote repo' do
- let!(:protected_branch_name) { 'remote-branch' }
-
- before do
- create(:protected_branch, project: project, name: protected_branch_name)
- end
-
- context 'when it has diverged' do
- it 'does not delete the remote branch' do
- allow(repository).to receive(:fetch_remote) do
- sync_remote(repository, remote_mirror.remote_name, local_branch_names)
-
- rev = repository.find_branch('markdown').dereferenced_target
- create_remote_branch(repository, remote_mirror.remote_name, 'remote-branch', rev.id)
- end
-
- expect(raw_repository).not_to receive(:delete_remote_branches)
-
- subject.execute(remote_mirror)
- end
- end
-
- context 'when it has not diverged' do
- it 'deletes the remote branch' do
- allow(repository).to receive(:fetch_remote) do
- sync_remote(repository, remote_mirror.remote_name, local_branch_names)
-
- masterrev = repository.find_branch('master').dereferenced_target
- create_remote_branch(repository, remote_mirror.remote_name, protected_branch_name, masterrev.id)
- end
-
- expect(raw_repository).to receive(:delete_remote_branches).with(remote_mirror.remote_name, [protected_branch_name])
-
- subject.execute(remote_mirror)
- end
- end
- end
- end
-
- context 'when branch only exists on remote repo' do
- context 'when it has diverged' do
- it 'does not delete the remote branch' do
- allow(repository).to receive(:fetch_remote) do
- sync_remote(repository, remote_mirror.remote_name, local_branch_names)
-
- rev = repository.find_branch('markdown').dereferenced_target
- create_remote_branch(repository, remote_mirror.remote_name, 'remote-branch', rev.id)
- end
-
- expect(raw_repository).not_to receive(:delete_remote_branches)
-
- subject.execute(remote_mirror)
- end
- end
-
- context 'when it has not diverged' do
- it 'deletes the remote branch' do
- allow(repository).to receive(:fetch_remote) do
- sync_remote(repository, remote_mirror.remote_name, local_branch_names)
-
- masterrev = repository.find_branch('master').dereferenced_target
- create_remote_branch(repository, remote_mirror.remote_name, 'remote-branch', masterrev.id)
- end
-
- expect(raw_repository).to receive(:delete_remote_branches).with(remote_mirror.remote_name, ['remote-branch'])
-
- subject.execute(remote_mirror)
- end
- end
- end
- end
end
- describe 'Syncing tags' do
- before do
- allow(repository).to receive(:fetch_remote) { sync_remote(repository, remote_mirror.remote_name, local_branch_names) }
+ context 'when only syncing protected branches' do
+ let(:unprotected_branch_name) { 'existing-branch' }
+ let(:protected_branch_name) do
+ project.repository.branch_names.find { |n| n != unprotected_branch_name }
end
-
- context 'when there are not tags to push' do
- it 'does not try to push tags' do
- allow(repository).to receive(:remote_tags) { {} }
- allow(repository).to receive(:tags) { [] }
-
- expect(repository).not_to receive(:push_tags)
-
- subject.execute(remote_mirror)
- end
+ let!(:protected_branch) do
+ create(:protected_branch, project: project, name: protected_branch_name)
end
- context 'when there are some tags to push' do
- it 'pushes tags to remote' do
- allow(raw_repository).to receive(:remote_tags) { {} }
-
- expect(raw_repository).to receive(:push_remote_branches).with(remote_mirror.remote_name, ['v1.0.0', 'v1.1.0'])
-
- subject.execute(remote_mirror)
- end
+ before do
+ project.reload
+ remote_mirror.only_protected_branches = true
end
- context 'when there are some tags to delete' do
- it 'deletes tags from remote' do
- remote_tags = generate_tags(repository, 'v1.0.0', 'v1.1.0')
- allow(raw_repository).to receive(:remote_tags) { remote_tags }
-
- repository.rm_tag(create(:user), 'v1.0.0')
-
- expect(raw_repository).to receive(:delete_remote_branches).with(remote_mirror.remote_name, ['v1.0.0'])
+ it "sync updated protected branches" do
+ allow(repository).to receive(:fetch_remote)
+ expect(remote_mirror).to receive(:update_repository).with(only_branches_matching: [protected_branch_name])
- subject.execute(remote_mirror)
- end
+ subject.execute(remote_mirror)
end
end
end
- def create_branch(repository, branch_name)
- rugged = repository.rugged
- masterrev = repository.find_branch('master').dereferenced_target
- parentrev = repository.commit(masterrev).parent_id
-
- rugged.references.create("refs/heads/#{branch_name}", parentrev)
-
- repository.expire_branches_cache
- end
-
- def create_remote_branch(repository, remote_name, branch_name, source_id)
- rugged = repository.rugged
-
- rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", source_id)
- end
-
def sync_remote(repository, remote_name, local_branch_names)
- rugged = repository.rugged
-
local_branch_names.each do |branch|
- target = repository.find_branch(branch).try(:dereferenced_target)
- rugged.references.create("refs/remotes/#{remote_name}/#{branch}", target.id) if target
+ commit = repository.commit(branch)
+ repository.write_ref("refs/remotes/#{remote_name}/#{branch}", commit.id) if commit
end
end
def update_remote_branch(repository, remote_name, branch)
- rugged = repository.rugged
- masterrev = repository.find_branch('master').dereferenced_target.id
+ masterrev = repository.commit('master').id
- rugged.references.create("refs/remotes/#{remote_name}/#{branch}", masterrev, force: true)
+ repository.write_ref("refs/remotes/#{remote_name}/#{branch}", masterrev, force: true)
repository.expire_branches_cache
end
def update_branch(repository, branch)
- rugged = repository.rugged
- masterrev = repository.find_branch('master').dereferenced_target.id
-
- # Updated existing branch
- rugged.references.create("refs/heads/#{branch}", masterrev, force: true)
- repository.expire_branches_cache
- end
-
- def delete_branch(repository, branch)
- rugged = repository.rugged
+ masterrev = repository.commit('master').id
- rugged.references.delete("refs/heads/#{branch}")
+ repository.write_ref("refs/heads/#{branch}", masterrev, force: true)
repository.expire_branches_cache
end
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index 3e6073b9861..ecf1ba05618 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -125,7 +125,7 @@ describe Projects::UpdateService do
context 'when we update project but not enabling a wiki' do
it 'does not try to create an empty wiki' do
- FileUtils.rm_rf(project.wiki.repository.path)
+ Gitlab::Shell.new.rm_directory(project.repository_storage, project.wiki.path)
result = update_project(project, user, { name: 'test1' })
@@ -146,7 +146,7 @@ describe Projects::UpdateService do
context 'when enabling a wiki' do
it 'creates a wiki' do
project.project_feature.update(wiki_access_level: ProjectFeature::DISABLED)
- FileUtils.rm_rf(project.wiki.repository.path)
+ Gitlab::Shell.new.rm_directory(project.repository_storage, project.wiki.path)
result = update_project(project, user, project_feature_attributes: { wiki_access_level: ProjectFeature::ENABLED })
@@ -275,6 +275,10 @@ describe Projects::UpdateService do
it { is_expected.to eq(false) }
end
+ context 'when auto devops is nil' do
+ it { is_expected.to eq(false) }
+ end
+
context 'when auto devops is explicitly enabled' do
before do
project.create_auto_devops!(enabled: true)
diff --git a/spec/services/protected_branches/create_service_spec.rb b/spec/services/protected_branches/create_service_spec.rb
index 786493c3577..79b744142c6 100644
--- a/spec/services/protected_branches/create_service_spec.rb
+++ b/spec/services/protected_branches/create_service_spec.rb
@@ -6,8 +6,8 @@ describe ProtectedBranches::CreateService do
let(:params) do
{
name: 'master',
- merge_access_levels_attributes: [{ access_level: Gitlab::Access::MASTER }],
- push_access_levels_attributes: [{ access_level: Gitlab::Access::MASTER }]
+ merge_access_levels_attributes: [{ access_level: Gitlab::Access::MAINTAINER }],
+ push_access_levels_attributes: [{ access_level: Gitlab::Access::MAINTAINER }]
}
end
@@ -16,8 +16,8 @@ describe ProtectedBranches::CreateService do
it 'creates a new protected branch' do
expect { service.execute }.to change(ProtectedBranch, :count).by(1)
- expect(project.protected_branches.last.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
- expect(project.protected_branches.last.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
+ expect(project.protected_branches.last.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::MAINTAINER])
+ expect(project.protected_branches.last.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MAINTAINER])
end
context 'when user does not have permission' do
diff --git a/spec/services/protected_tags/create_service_spec.rb b/spec/services/protected_tags/create_service_spec.rb
index c3ed95aaebf..b16acf1d36c 100644
--- a/spec/services/protected_tags/create_service_spec.rb
+++ b/spec/services/protected_tags/create_service_spec.rb
@@ -6,7 +6,7 @@ describe ProtectedTags::CreateService do
let(:params) do
{
name: 'master',
- create_access_levels_attributes: [{ access_level: Gitlab::Access::MASTER }]
+ create_access_levels_attributes: [{ access_level: Gitlab::Access::MAINTAINER }]
}
end
@@ -15,7 +15,7 @@ describe ProtectedTags::CreateService do
it 'creates a new protected tag' do
expect { service.execute }.to change(ProtectedTag, :count).by(1)
- expect(project.protected_tags.last.create_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
+ expect(project.protected_tags.last.create_access_levels.map(&:access_level)).to eq([Gitlab::Access::MAINTAINER])
end
end
end
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index bd835a1fca6..743e281183e 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -323,6 +323,14 @@ describe QuickActions::InterpretService do
end
end
+ shared_examples 'confidential command' do
+ it 'marks issue as confidential if content contains /confidential' do
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(confidential: true)
+ end
+ end
+
shared_examples 'shrug command' do
it 'appends ¯\_(ツ)_/¯ to the comment' do
new_content, _ = service.execute(content, issuable)
@@ -774,6 +782,11 @@ describe QuickActions::InterpretService do
let(:issuable) { issue }
end
+ it_behaves_like 'confidential command' do
+ let(:content) { '/confidential' }
+ let(:issuable) { issue }
+ end
+
context '/copy_metadata command' do
let(:todo_label) { create(:label, project: project, title: 'To Do') }
let(:inreview_label) { create(:label, project: project, title: 'In Review') }
@@ -919,6 +932,11 @@ describe QuickActions::InterpretService do
end
it_behaves_like 'empty command' do
+ let(:content) { '/confidential' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'empty command' do
let(:content) { '/duplicate #{issue.to_reference}' }
let(:issuable) { issue }
end
diff --git a/spec/services/reset_project_cache_service_spec.rb b/spec/services/reset_project_cache_service_spec.rb
index de475d16586..1490ad5fe3b 100644
--- a/spec/services/reset_project_cache_service_spec.rb
+++ b/spec/services/reset_project_cache_service_spec.rb
@@ -18,7 +18,7 @@ describe ResetProjectCacheService do
context 'when project cache_index is a numeric value' do
before do
- project.update_attributes(jobs_cache_index: 1)
+ project.update(jobs_cache_index: 1)
end
it 'increments project cache index' do
diff --git a/spec/services/search/global_service_spec.rb b/spec/services/search/global_service_spec.rb
index d8dba26e194..980545b8083 100644
--- a/spec/services/search/global_service_spec.rb
+++ b/spec/services/search/global_service_spec.rb
@@ -10,7 +10,7 @@ describe Search::GlobalService do
let!(:public_project) { create(:project, :public, name: 'searchable_public_project') }
before do
- found_project.add_master(user)
+ found_project.add_maintainer(user)
end
describe '#execute' do
diff --git a/spec/services/search_service_spec.rb b/spec/services/search_service_spec.rb
index 02de83a2df8..e5e036c7d44 100644
--- a/spec/services/search_service_spec.rb
+++ b/spec/services/search_service_spec.rb
@@ -16,7 +16,7 @@ describe SearchService do
let(:public_project) { create(:project, :public, name: 'public_project') }
before do
- accessible_project.add_master(user)
+ accessible_project.add_maintainer(user)
end
describe '#project' do
diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb
index 51396d34f8f..e0335880e8e 100644
--- a/spec/services/system_hooks_service_spec.rb
+++ b/spec/services/system_hooks_service_spec.rb
@@ -75,7 +75,7 @@ describe SystemHooksService do
end
it 'handles nil datetime columns' do
- user.update_attributes(created_at: nil, updated_at: nil)
+ user.update(created_at: nil, updated_at: nil)
data = event_data(user, :destroy)
expect(data[:created_at]).to be(nil)
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index e28b0ea5cf2..57d081cffb3 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -3,6 +3,7 @@ require 'spec_helper'
describe SystemNoteService do
include Gitlab::Routing
include RepoHelpers
+ include AssetsHelpers
set(:group) { create(:group) }
set(:project) { create(:project, :repository, group: group) }
@@ -769,6 +770,8 @@ describe SystemNoteService do
end
describe "new reference" do
+ let(:favicon_path) { "http://localhost/assets/#{find_asset('favicon.png').digest_path}" }
+
before do
allow(JIRA::Resource::Remotelink).to receive(:all).and_return([])
end
@@ -789,7 +792,7 @@ describe SystemNoteService do
object: {
url: project_commit_url(project, commit),
title: "GitLab: Mentioned on commit - #{commit.title}",
- icon: { title: "GitLab", url16x16: "http://localhost/favicon.ico" },
+ icon: { title: "GitLab", url16x16: favicon_path },
status: { resolved: false }
}
)
@@ -815,7 +818,7 @@ describe SystemNoteService do
object: {
url: project_issue_url(project, issue),
title: "GitLab: Mentioned on issue - #{issue.title}",
- icon: { title: "GitLab", url16x16: "http://localhost/favicon.ico" },
+ icon: { title: "GitLab", url16x16: favicon_path },
status: { resolved: false }
}
)
@@ -841,7 +844,7 @@ describe SystemNoteService do
object: {
url: project_snippet_url(project, snippet),
title: "GitLab: Mentioned on snippet - #{snippet.title}",
- icon: { title: "GitLab", url16x16: "http://localhost/favicon.ico" },
+ icon: { title: "GitLab", url16x16: favicon_path },
status: { resolved: false }
}
)
diff --git a/spec/services/tags/create_service_spec.rb b/spec/services/tags/create_service_spec.rb
index e7e9080b6b0..0cbe57352be 100644
--- a/spec/services/tags/create_service_spec.rb
+++ b/spec/services/tags/create_service_spec.rb
@@ -41,7 +41,7 @@ describe Tags::CreateService do
it 'returns an error' do
expect(repository).to receive(:add_tag)
.with(user, 'v1.1.0', 'master', 'Foo')
- .and_raise(Gitlab::Git::HooksService::PreReceiveError, 'something went wrong')
+ .and_raise(Gitlab::Git::PreReceiveError, 'something went wrong')
response = service.execute('v1.1.0', 'master', 'Foo')
diff --git a/spec/services/test_hooks/project_service_spec.rb b/spec/services/test_hooks/project_service_spec.rb
index 962b9f40c4f..19e1c5ff3b2 100644
--- a/spec/services/test_hooks/project_service_spec.rb
+++ b/spec/services/test_hooks/project_service_spec.rb
@@ -6,13 +6,19 @@ describe TestHooks::ProjectService do
describe '#execute' do
let(:project) { create(:project, :repository) }
let(:hook) { create(:project_hook, project: project) }
+ let(:trigger) { 'not_implemented_events' }
let(:service) { described_class.new(hook, current_user, trigger) }
let(:sample_data) { { data: 'sample' } }
let(:success_result) { { status: :success, http_status: 200, message: 'ok' } }
- context 'hook with not implemented test' do
- let(:trigger) { 'not_implemented_events' }
+ it 'allows to set a custom project' do
+ project = double
+ service.project = project
+
+ expect(service.project).to eq(project)
+ end
+ context 'hook with not implemented test' do
it 'returns error message' do
expect(hook).not_to receive(:execute)
expect(service.execute).to include({ status: :error, message: 'Testing not available for this hook' })
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index 562b89e6767..9a51c873b30 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -721,17 +721,18 @@ describe TodoService do
end
describe '#merge_request_build_failed' do
- it 'creates a pending todo for the merge request author' do
- service.merge_request_build_failed(mr_unassigned)
+ let(:merge_participants) { [mr_unassigned.author, admin] }
- should_create_todo(user: author, target: mr_unassigned, action: Todo::BUILD_FAILED)
+ before do
+ allow(mr_unassigned).to receive(:merge_participants).and_return(merge_participants)
end
- it 'creates a pending todo for merge_user' do
- mr_unassigned.update(merge_when_pipeline_succeeds: true, merge_user: admin)
+ it 'creates a pending todo for each merge_participant' do
service.merge_request_build_failed(mr_unassigned)
- should_create_todo(user: admin, author: admin, target: mr_unassigned, action: Todo::BUILD_FAILED)
+ merge_participants.each do |participant|
+ should_create_todo(user: participant, author: participant, target: mr_unassigned, action: Todo::BUILD_FAILED)
+ end
end
end
@@ -747,11 +748,19 @@ describe TodoService do
end
describe '#merge_request_became_unmergeable' do
- it 'creates a pending todo for a merge_user' do
+ let(:merge_participants) { [admin, create(:user)] }
+
+ before do
+ allow(mr_unassigned).to receive(:merge_participants).and_return(merge_participants)
+ end
+
+ it 'creates a pending todo for each merge_participant' do
mr_unassigned.update(merge_when_pipeline_succeeds: true, merge_user: admin)
service.merge_request_became_unmergeable(mr_unassigned)
- should_create_todo(user: admin, author: admin, target: mr_unassigned, action: Todo::UNMERGEABLE)
+ merge_participants.each do |participant|
+ should_create_todo(user: participant, author: participant, target: mr_unassigned, action: Todo::UNMERGEABLE)
+ end
end
end
diff --git a/spec/services/update_merge_request_metrics_service_spec.rb b/spec/services/update_merge_request_metrics_service_spec.rb
index b5fb999381d..812dd42934d 100644
--- a/spec/services/update_merge_request_metrics_service_spec.rb
+++ b/spec/services/update_merge_request_metrics_service_spec.rb
@@ -12,7 +12,7 @@ describe MergeRequestMetricsService do
service.merge(event)
expect(metrics.merged_by).to eq(user)
- expect(metrics.merged_at).to eq(event.created_at)
+ expect(metrics.merged_at).to be_like_time(event.created_at)
end
end
@@ -25,7 +25,7 @@ describe MergeRequestMetricsService do
service.close(event)
expect(metrics.latest_closed_by).to eq(user)
- expect(metrics.latest_closed_at).to eq(event.created_at)
+ expect(metrics.latest_closed_at).to be_like_time(event.created_at)
end
end
diff --git a/spec/services/upload_service_spec.rb b/spec/services/upload_service_spec.rb
index 24f3a5c5ff0..9b232a52efa 100644
--- a/spec/services/upload_service_spec.rb
+++ b/spec/services/upload_service_spec.rb
@@ -9,7 +9,7 @@ describe UploadService do
context 'for valid gif file' do
before do
- gif = fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif')
+ gif = fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif')
@link_to_file = upload_file(@project, gif)
end
@@ -21,7 +21,7 @@ describe UploadService do
context 'for valid png file' do
before do
- png = fixture_file_upload(Rails.root + 'spec/fixtures/dk.png',
+ png = fixture_file_upload('spec/fixtures/dk.png',
'image/png')
@link_to_file = upload_file(@project, png)
end
@@ -34,7 +34,7 @@ describe UploadService do
context 'for valid jpg file' do
before do
- jpg = fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg')
+ jpg = fixture_file_upload('spec/fixtures/rails_sample.jpg', 'image/jpg')
@link_to_file = upload_file(@project, jpg)
end
@@ -46,7 +46,7 @@ describe UploadService do
context 'for txt file' do
before do
- txt = fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain')
+ txt = fixture_file_upload('spec/fixtures/doc_sample.txt', 'text/plain')
@link_to_file = upload_file(@project, txt)
end
@@ -58,7 +58,7 @@ describe UploadService do
context 'for too large a file' do
before do
- txt = fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain')
+ txt = fixture_file_upload('spec/fixtures/doc_sample.txt', 'text/plain')
allow(txt).to receive(:size) { 1000.megabytes.to_i }
@link_to_file = upload_file(@project, txt)
end
diff --git a/spec/services/users/destroy_service_spec.rb b/spec/services/users/destroy_service_spec.rb
index 76f1e625fda..f82d4b483e7 100644
--- a/spec/services/users/destroy_service_spec.rb
+++ b/spec/services/users/destroy_service_spec.rb
@@ -19,7 +19,9 @@ describe Users::DestroyService do
end
it 'will delete the project' do
- expect_any_instance_of(Projects::DestroyService).to receive(:execute).once
+ expect_next_instance_of(Projects::DestroyService) do |destroy_service|
+ expect(destroy_service).to receive(:execute).once
+ end
service.execute(user)
end
@@ -32,7 +34,9 @@ describe Users::DestroyService do
end
it 'destroys a project in pending_delete' do
- expect_any_instance_of(Projects::DestroyService).to receive(:execute).once
+ expect_next_instance_of(Projects::DestroyService) do |destroy_service|
+ expect(destroy_service).to receive(:execute).once
+ end
service.execute(user)
diff --git a/spec/services/users/refresh_authorized_projects_service_spec.rb b/spec/services/users/refresh_authorized_projects_service_spec.rb
index 08fd26d67fd..122b96ef216 100644
--- a/spec/services/users/refresh_authorized_projects_service_spec.rb
+++ b/spec/services/users/refresh_authorized_projects_service_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Users::RefreshAuthorizedProjectsService do
+ include ExclusiveLeaseHelpers
+
# We're using let! here so that any expectations for the service class are not
# triggered twice.
let!(:project) { create(:project) }
@@ -10,12 +12,10 @@ describe Users::RefreshAuthorizedProjectsService do
describe '#execute', :clean_gitlab_redis_shared_state do
it 'refreshes the authorizations using a lease' do
- expect_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain)
- .and_return('foo')
-
- expect(Gitlab::ExclusiveLease).to receive(:cancel)
- .with(an_instance_of(String), 'foo')
+ lease_key = "refresh_authorized_projects:#{user.id}"
+ expect_to_obtain_exclusive_lease(lease_key, 'uuid')
+ expect_to_cancel_exclusive_lease(lease_key, 'uuid')
expect(service).to receive(:execute_without_lease)
service.execute
@@ -30,10 +30,10 @@ describe Users::RefreshAuthorizedProjectsService do
it 'updates the authorized projects of the user' do
project2 = create(:project)
to_remove = user.project_authorizations
- .create!(project: project2, access_level: Gitlab::Access::MASTER)
+ .create!(project: project2, access_level: Gitlab::Access::MAINTAINER)
expect(service).to receive(:update_authorizations)
- .with([to_remove.project_id], [[user.id, project.id, Gitlab::Access::MASTER]])
+ .with([to_remove.project_id], [[user.id, project.id, Gitlab::Access::MAINTAINER]])
service.execute_without_lease
end
@@ -45,7 +45,7 @@ describe Users::RefreshAuthorizedProjectsService do
.create!(project: project, access_level: Gitlab::Access::DEVELOPER)
expect(service).to receive(:update_authorizations)
- .with([to_remove.project_id], [[user.id, project.id, Gitlab::Access::MASTER]])
+ .with([to_remove.project_id], [[user.id, project.id, Gitlab::Access::MAINTAINER]])
service.execute_without_lease
end
@@ -76,14 +76,14 @@ describe Users::RefreshAuthorizedProjectsService do
it 'inserts authorizations that should be added' do
user.project_authorizations.delete_all
- service.update_authorizations([], [[user.id, project.id, Gitlab::Access::MASTER]])
+ service.update_authorizations([], [[user.id, project.id, Gitlab::Access::MAINTAINER]])
authorizations = user.project_authorizations
expect(authorizations.length).to eq(1)
expect(authorizations[0].user_id).to eq(user.id)
expect(authorizations[0].project_id).to eq(project.id)
- expect(authorizations[0].access_level).to eq(Gitlab::Access::MASTER)
+ expect(authorizations[0].access_level).to eq(Gitlab::Access::MAINTAINER)
end
end
@@ -99,12 +99,12 @@ describe Users::RefreshAuthorizedProjectsService do
end
it 'sets the values to the access levels' do
- expect(hash.values).to eq([Gitlab::Access::MASTER])
+ expect(hash.values).to eq([Gitlab::Access::MAINTAINER])
end
context 'personal projects' do
it 'includes the project with the right access level' do
- expect(hash[project.id]).to eq(Gitlab::Access::MASTER)
+ expect(hash[project.id]).to eq(Gitlab::Access::MAINTAINER)
end
end
@@ -139,11 +139,11 @@ describe Users::RefreshAuthorizedProjectsService do
let!(:other_project) { create(:project, group: nested_group) }
before do
- group.add_master(user)
+ group.add_maintainer(user)
end
it 'includes the project with the right access level' do
- expect(hash[other_project.id]).to eq(Gitlab::Access::MASTER)
+ expect(hash[other_project.id]).to eq(Gitlab::Access::MAINTAINER)
end
end
@@ -153,7 +153,7 @@ describe Users::RefreshAuthorizedProjectsService do
let!(:project_group_link) { create(:project_group_link, project: other_project, group: group, group_access: Gitlab::Access::GUEST) }
before do
- group.add_master(user)
+ group.add_maintainer(user)
end
it 'includes the project with the right access level' do
@@ -168,7 +168,7 @@ describe Users::RefreshAuthorizedProjectsService do
let!(:project_group_link) { create(:project_group_link, project: other_project, group: nested_group, group_access: Gitlab::Access::DEVELOPER) }
before do
- group.add_master(user)
+ group.add_maintainer(user)
end
it 'includes the project with the right access level' do
@@ -194,7 +194,7 @@ describe Users::RefreshAuthorizedProjectsService do
value = hash.values[0]
expect(value.project_id).to eq(project.id)
- expect(value.access_level).to eq(Gitlab::Access::MASTER)
+ expect(value.access_level).to eq(Gitlab::Access::MAINTAINER)
end
end
@@ -219,7 +219,7 @@ describe Users::RefreshAuthorizedProjectsService do
end
it 'includes the access level for every row' do
- expect(row.access_level).to eq(Gitlab::Access::MASTER)
+ expect(row.access_level).to eq(Gitlab::Access::MAINTAINER)
end
end
end
@@ -235,7 +235,7 @@ describe Users::RefreshAuthorizedProjectsService do
rows = service.fresh_authorizations.to_a
expect(rows.length).to eq(1)
- expect(rows.first.access_level).to eq(Gitlab::Access::MASTER)
+ expect(rows.first.access_level).to eq(Gitlab::Access::MAINTAINER)
end
context 'every returned row' do
@@ -246,7 +246,7 @@ describe Users::RefreshAuthorizedProjectsService do
end
it 'includes the access level' do
- expect(row.access_level).to eq(Gitlab::Access::MASTER)
+ expect(row.access_level).to eq(Gitlab::Access::MAINTAINER)
end
end
end
diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb
index 7995f2c9ae7..622e56e1da5 100644
--- a/spec/services/web_hook_service_spec.rb
+++ b/spec/services/web_hook_service_spec.rb
@@ -60,6 +60,36 @@ describe WebHookService do
).once
end
+ context 'when auth credentials are present' do
+ let(:url) {'https://example.org'}
+ let(:project_hook) { create(:project_hook, url: 'https://demo:demo@example.org/') }
+
+ it 'uses the credentials' do
+ WebMock.stub_request(:post, url)
+
+ service_instance.execute
+
+ expect(WebMock).to have_requested(:post, url).with(
+ headers: headers.merge('Authorization' => 'Basic ZGVtbzpkZW1v')
+ ).once
+ end
+ end
+
+ context 'when auth credentials are partial present' do
+ let(:url) {'https://example.org'}
+ let(:project_hook) { create(:project_hook, url: 'https://demo@example.org/') }
+
+ it 'uses the credentials anyways' do
+ WebMock.stub_request(:post, url)
+
+ service_instance.execute
+
+ expect(WebMock).to have_requested(:post, url).with(
+ headers: headers.merge('Authorization' => 'Basic ZGVtbzo=')
+ ).once
+ end
+ end
+
it 'catches exceptions' do
WebMock.stub_request(:post, project_hook.url).to_raise(StandardError.new('Some error'))
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index d3de2331244..bd564cc60a6 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -4,7 +4,7 @@ SimpleCovEnv.start!
ENV["RAILS_ENV"] = 'test'
ENV["IN_MEMORY_APPLICATION_SETTINGS"] = 'true'
-require File.expand_path("../../config/environment", __FILE__)
+require File.expand_path('../config/environment', __dir__)
require 'rspec/rails'
require 'shoulda/matchers'
require 'rspec/retry'
@@ -69,6 +69,7 @@ RSpec.configure do |config|
config.include StubFeatureFlags
config.include StubGitlabCalls
config.include StubGitlabData
+ config.include ExpectNextInstanceOf
config.include TestEnv
config.include Devise::Test::ControllerHelpers, type: :controller
config.include Devise::Test::IntegrationHelpers, type: :feature
@@ -87,6 +88,7 @@ RSpec.configure do |config|
config.include LiveDebugger, :js
config.include MigrationsHelpers, :migration
config.include RedisHelpers
+ config.include Rails.application.routes.url_helpers, type: :routing
if ENV['CI']
# This includes the first try, i.e. tests will be run 4 times before failing.
@@ -107,19 +109,6 @@ RSpec.configure do |config|
end
config.before(:example) do
- # Skip pre-receive hook check so we can use the web editor and merge.
- allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil])
-
- allow_any_instance_of(Gitlab::Git::GitlabProjects).to receive(:fork_repository).and_wrap_original do |m, *args|
- m.call(*args)
-
- shard_name, repository_relative_path = args
- # We can't leave the hooks in place after a fork, as those would fail in tests
- # The "internal" API is not available
- Gitlab::Shell.new.rm_directory(shard_name,
- File.join(repository_relative_path, 'hooks'))
- end
-
# Enable all features by default for testing
allow(Feature).to receive(:enabled?) { true }
end
@@ -133,6 +122,10 @@ RSpec.configure do |config|
RequestStore.clear!
end
+ config.after(:example) do
+ Fog.unmock! if Fog.mock?
+ end
+
config.before(:example, :mailer) do
reset_delivered_emails!
end
@@ -177,6 +170,17 @@ RSpec.configure do |config|
redis_queues_cleanup!
end
+ config.around(:each, :use_clean_rails_memory_store_fragment_caching) do |example|
+ caching_store = ActionController::Base.cache_store
+ ActionController::Base.cache_store = ActiveSupport::Cache::MemoryStore.new
+ ActionController::Base.perform_caching = true
+
+ example.run
+
+ ActionController::Base.perform_caching = false
+ ActionController::Base.cache_store = caching_store
+ end
+
# The :each scope runs "inside" the example, so this hook ensures the DB is in the
# correct state before any examples' before hooks are called. This prevents a
# problem where `ScheduleIssuesClosedAtTypeChange` (or any migration that depends
diff --git a/spec/support/api/milestones_shared_examples.rb b/spec/support/api/milestones_shared_examples.rb
index 0388f110d71..a15189db35f 100644
--- a/spec/support/api/milestones_shared_examples.rb
+++ b/spec/support/api/milestones_shared_examples.rb
@@ -196,6 +196,12 @@ shared_examples_for 'group and project milestones' do |route_definition|
expect(json_response['state']).to eq('closed')
end
+
+ it 'updates milestone with only start date' do
+ put api(resource_route, user), start_date: Date.tomorrow
+
+ expect(response).to have_gitlab_http_status(200)
+ end
end
describe "GET #{route_definition}/:milestone_id/issues" do
diff --git a/spec/support/api/repositories_shared_context.rb b/spec/support/api/repositories_shared_context.rb
index ea38fe4f5b8..f1341804e56 100644
--- a/spec/support/api/repositories_shared_context.rb
+++ b/spec/support/api/repositories_shared_context.rb
@@ -1,6 +1,6 @@
shared_context 'disabled repository' do
before do
- project.project_feature.update_attributes!(
+ project.project_feature.update!(
repository_access_level: ProjectFeature::DISABLED,
merge_requests_access_level: ProjectFeature::DISABLED,
builds_access_level: ProjectFeature::DISABLED
diff --git a/spec/support/api/scopes/read_user_shared_examples.rb b/spec/support/api/scopes/read_user_shared_examples.rb
index 06ae8792c61..d7cef137989 100644
--- a/spec/support/api/scopes/read_user_shared_examples.rb
+++ b/spec/support/api/scopes/read_user_shared_examples.rb
@@ -1,10 +1,12 @@
-shared_examples_for 'allows the "read_user" scope' do
+shared_examples_for 'allows the "read_user" scope' do |api_version|
+ let(:version) { api_version || 'v4' }
+
context 'for personal access tokens' do
context 'when the requesting token has the "api" scope' do
let(:token) { create(:personal_access_token, scopes: ['api'], user: user) }
it 'returns a "200" response' do
- get api_call.call(path, user, personal_access_token: token)
+ get api_call.call(path, user, personal_access_token: token, version: version)
expect(response).to have_gitlab_http_status(200)
end
@@ -14,7 +16,7 @@ shared_examples_for 'allows the "read_user" scope' do
let(:token) { create(:personal_access_token, scopes: ['read_user'], user: user) }
it 'returns a "200" response' do
- get api_call.call(path, user, personal_access_token: token)
+ get api_call.call(path, user, personal_access_token: token, version: version)
expect(response).to have_gitlab_http_status(200)
end
@@ -28,7 +30,7 @@ shared_examples_for 'allows the "read_user" scope' do
end
it 'returns a "403" response' do
- get api_call.call(path, user, personal_access_token: token)
+ get api_call.call(path, user, personal_access_token: token, version: version)
expect(response).to have_gitlab_http_status(403)
end
diff --git a/spec/support/api/time_tracking_shared_examples.rb b/spec/support/api/time_tracking_shared_examples.rb
index 52e1bc55191..fee464c15a3 100644
--- a/spec/support/api/time_tracking_shared_examples.rb
+++ b/spec/support/api/time_tracking_shared_examples.rb
@@ -85,7 +85,7 @@ shared_examples 'time tracking endpoints' do |issuable_name|
it 'subtracts time of the total spent time' do
Timecop.travel(1.minute.from_now) do
expect do
- issuable.update_attributes!(spend_time: { duration: 7200, user_id: user.id })
+ issuable.update!(spend_time: { duration: 7200, user_id: user.id })
end.to change { issuable.reload.updated_at }
end
@@ -99,7 +99,7 @@ shared_examples 'time tracking endpoints' do |issuable_name|
context 'when time to subtract is greater than the total spent time' do
it 'does not modify the total time spent' do
- issuable.update_attributes!(spend_time: { duration: 7200, user_id: user.id })
+ issuable.update!(spend_time: { duration: 7200, user_id: user.id })
Timecop.travel(1.minute.from_now) do
expect do
@@ -135,8 +135,8 @@ shared_examples 'time tracking endpoints' do |issuable_name|
describe "GET /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/time_stats" do
it "returns the time stats for #{issuable_name}" do
- issuable.update_attributes!(spend_time: { duration: 1800, user_id: user.id },
- time_estimate: 3600)
+ issuable.update!(spend_time: { duration: 1800, user_id: user.id },
+ time_estimate: 3600)
get api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_stats", user)
diff --git a/spec/support/api/v3/time_tracking_shared_examples.rb b/spec/support/api/v3/time_tracking_shared_examples.rb
deleted file mode 100644
index f27a2d06c83..00000000000
--- a/spec/support/api/v3/time_tracking_shared_examples.rb
+++ /dev/null
@@ -1,128 +0,0 @@
-shared_examples 'V3 time tracking endpoints' do |issuable_name|
- issuable_collection_name = issuable_name.pluralize
-
- describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/time_estimate" do
- context 'with an unauthorized user' do
- subject { post(v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", non_member), duration: '1w') }
-
- it_behaves_like 'an unauthorized API user'
- end
-
- 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_gitlab_http_status(200)
- expect(json_response['human_time_estimate']).to eq('1w')
- end
-
- describe 'updating the current estimate' do
- before do
- post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '1w'
- end
-
- context 'when duration has a bad format' do
- 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_gitlab_http_status(400)
- expect(issuable.reload.human_time_estimate).to eq('1w')
- end
- end
-
- context 'with a valid duration' do
- 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_gitlab_http_status(200)
- expect(issuable.reload.human_time_estimate).to eq('3w 1h')
- end
- end
- end
- end
-
- describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/reset_time_estimate" do
- context 'with an unauthorized user' do
- subject { post(v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_time_estimate", non_member)) }
-
- it_behaves_like 'an unauthorized API user'
- end
-
- 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_gitlab_http_status(200)
- expect(json_response['time_estimate']).to eq(0)
- end
- end
-
- describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/add_spent_time" do
- context 'with an unauthorized user' do
- subject do
- post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", non_member),
- duration: '2h'
- end
-
- it_behaves_like 'an unauthorized API user'
- end
-
- it "add spent time for #{issuable_name}" do
- post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user),
- duration: '2h'
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['human_total_time_spent']).to eq('2h')
- end
-
- context 'when subtracting time' do
- it 'subtracts time of the total spent time' do
- issuable.update_attributes!(spend_time: { duration: 7200, user_id: user.id })
-
- post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user),
- duration: '-1h'
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['total_time_spent']).to eq(3600)
- end
- end
-
- context 'when time to subtract is greater than the total spent time' do
- it 'does not modify the total time spent' do
- issuable.update_attributes!(spend_time: { duration: 7200, user_id: user.id })
-
- post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user),
- duration: '-1w'
-
- expect(response).to have_gitlab_http_status(400)
- expect(json_response['message']['time_spent'].first).to match(/exceeds the total time spent/)
- end
- end
- end
-
- describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/reset_spent_time" do
- context 'with an unauthorized user' do
- subject { post(v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_spent_time", non_member)) }
-
- it_behaves_like 'an unauthorized API user'
- end
-
- 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_gitlab_http_status(200)
- expect(json_response['total_time_spent']).to eq(0)
- end
- end
-
- describe "GET /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/time_stats" do
- it "returns the time stats for #{issuable_name}" do
- issuable.update_attributes!(spend_time: { duration: 1800, user_id: user.id },
- time_estimate: 3600)
-
- get v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_stats", user)
-
- 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
- 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 368439aa5b0..1c1b68c12a2 100644
--- a/spec/support/controllers/githubish_import_controller_shared_examples.rb
+++ b/spec/support/controllers/githubish_import_controller_shared_examples.rb
@@ -118,14 +118,19 @@ shared_examples 'a GitHub-ish import controller: POST create' do
expect(response).to have_gitlab_http_status(200)
end
- it 'returns 422 response when the project could not be imported' do
+ it 'returns 422 response with the base error when the project could not be imported' do
+ project = build(:project)
+ project.errors.add(:name, 'is invalid')
+ project.errors.add(:path, 'is old')
+
allow(Gitlab::LegacyGithubImport::ProjectCreator)
.to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider)
- .and_return(double(execute: build(:project)))
+ .and_return(double(execute: project))
post :create, format: :json
expect(response).to have_gitlab_http_status(422)
+ expect(json_response['errors']).to eq('Name is invalid, Path is old')
end
context "when the repository owner is the provider user" do
diff --git a/spec/support/features/issuable_slash_commands_shared_examples.rb b/spec/support/features/issuable_slash_commands_shared_examples.rb
index 1bd6c25100e..9b44c532ff6 100644
--- a/spec/support/features/issuable_slash_commands_shared_examples.rb
+++ b/spec/support/features/issuable_slash_commands_shared_examples.rb
@@ -4,7 +4,7 @@
shared_examples 'issuable record that supports quick actions in its description and notes' do |issuable_type|
include Spec::Support::Helpers::Features::NotesHelpers
- let(:master) { create(:user) }
+ let(:maintainer) { create(:user) }
let(:project) do
case issuable_type
when :merge_request
@@ -19,9 +19,9 @@ shared_examples 'issuable record that supports quick actions in its description
let(:new_url_opts) { {} }
before do
- project.add_master(master)
+ project.add_maintainer(maintainer)
- gitlab_sign_in(master)
+ gitlab_sign_in(maintainer)
end
after do
@@ -210,31 +210,31 @@ shared_examples 'issuable record that supports quick actions in its description
expect(page).not_to have_content '/todo'
expect(page).to have_content 'Commands applied'
- todos = TodosFinder.new(master).execute
+ todos = TodosFinder.new(maintainer).execute
todo = todos.first
expect(todos.size).to eq 1
expect(todo).to be_pending
expect(todo.target).to eq issuable
- expect(todo.author).to eq master
- expect(todo.user).to eq master
+ expect(todo.author).to eq maintainer
+ expect(todo.user).to eq maintainer
end
end
context "with a note marking the #{issuable_type} as done" do
before do
- TodoService.new.mark_todo(issuable, master)
+ TodoService.new.mark_todo(issuable, maintainer)
end
it "creates a new todo for the #{issuable_type}" do
- todos = TodosFinder.new(master).execute
+ todos = TodosFinder.new(maintainer).execute
todo = todos.first
expect(todos.size).to eq 1
expect(todos.first).to be_pending
expect(todo.target).to eq issuable
- expect(todo.author).to eq master
- expect(todo.user).to eq master
+ expect(todo.author).to eq maintainer
+ expect(todo.user).to eq maintainer
add_note("/done")
@@ -247,31 +247,31 @@ shared_examples 'issuable record that supports quick actions in its description
context "with a note subscribing to the #{issuable_type}" do
it "creates a new todo for the #{issuable_type}" do
- expect(issuable.subscribed?(master, project)).to be_falsy
+ expect(issuable.subscribed?(maintainer, project)).to be_falsy
add_note("/subscribe")
expect(page).not_to have_content '/subscribe'
expect(page).to have_content 'Commands applied'
- expect(issuable.subscribed?(master, project)).to be_truthy
+ expect(issuable.subscribed?(maintainer, project)).to be_truthy
end
end
context "with a note unsubscribing to the #{issuable_type} as done" do
before do
- issuable.subscribe(master, project)
+ issuable.subscribe(maintainer, project)
end
it "creates a new todo for the #{issuable_type}" do
- expect(issuable.subscribed?(master, project)).to be_truthy
+ expect(issuable.subscribed?(maintainer, project)).to be_truthy
add_note("/unsubscribe")
expect(page).not_to have_content '/unsubscribe'
expect(page).to have_content 'Commands applied'
- expect(issuable.subscribed?(master, project)).to be_falsy
+ expect(issuable.subscribed?(maintainer, project)).to be_falsy
end
end
@@ -282,7 +282,7 @@ shared_examples 'issuable record that supports quick actions in its description
expect(page).not_to have_content '/assign me'
expect(page).to have_content 'Commands applied'
- expect(issuable.reload.assignees).to eq [master]
+ expect(issuable.reload.assignees).to eq [maintainer]
end
end
end
diff --git a/spec/support/features/reportable_note_shared_examples.rb b/spec/support/features/reportable_note_shared_examples.rb
index 836e5e7be23..89a5518239d 100644
--- a/spec/support/features/reportable_note_shared_examples.rb
+++ b/spec/support/features/reportable_note_shared_examples.rb
@@ -1,6 +1,7 @@
require 'spec_helper'
shared_examples 'reportable note' do |type|
+ include MobileHelpers
include NotesHelper
let(:comment) { find("##{ActionView::RecordIdentifier.dom_id(note)}") }
@@ -21,7 +22,7 @@ shared_examples 'reportable note' do |type|
expect(dropdown).to have_link('Report as abuse', href: abuse_report_path)
- if type == 'issue'
+ if type == 'issue' || type == 'merge_request'
expect(dropdown).to have_button('Delete comment')
else
expect(dropdown).to have_link('Delete comment', href: note_url(note, project))
@@ -39,6 +40,9 @@ shared_examples 'reportable note' do |type|
end
def open_dropdown(dropdown)
+ # make window wide enough that tooltip doesn't trigger horizontal scrollbar
+ resize_window(1200, 800)
+
dropdown.find('.more-actions-toggle').click
dropdown.find('.dropdown-menu li', match: :first)
end
diff --git a/spec/support/features/rss_shared_examples.rb b/spec/support/features/rss_shared_examples.rb
index 50fbbc7f55b..0de92aedba5 100644
--- a/spec/support/features/rss_shared_examples.rb
+++ b/spec/support/features/rss_shared_examples.rb
@@ -1,23 +1,23 @@
-shared_examples "an autodiscoverable RSS feed with current_user's RSS token" do
- it "has an RSS autodiscovery link tag with current_user's RSS token" do
- expect(page).to have_css("link[type*='atom+xml'][href*='rss_token=#{user.rss_token}']", visible: false)
+shared_examples "an autodiscoverable RSS feed with current_user's feed token" do
+ it "has an RSS autodiscovery link tag with current_user's feed token" do
+ expect(page).to have_css("link[type*='atom+xml'][href*='feed_token=#{user.feed_token}']", visible: false)
end
end
-shared_examples "it has an RSS button with current_user's RSS token" do
- it "shows the RSS button with current_user's RSS token" do
- expect(page).to have_css("a:has(.fa-rss)[href*='rss_token=#{user.rss_token}']")
+shared_examples "it has an RSS button with current_user's feed token" do
+ it "shows the RSS button with current_user's feed token" do
+ expect(page).to have_css("a:has(.fa-rss)[href*='feed_token=#{user.feed_token}']")
end
end
-shared_examples "an autodiscoverable RSS feed without an RSS token" do
- it "has an RSS autodiscovery link tag without an RSS token" do
- expect(page).to have_css("link[type*='atom+xml']:not([href*='rss_token'])", visible: false)
+shared_examples "an autodiscoverable RSS feed without a feed token" do
+ it "has an RSS autodiscovery link tag without a feed token" do
+ expect(page).to have_css("link[type*='atom+xml']:not([href*='feed_token'])", visible: false)
end
end
-shared_examples "it has an RSS button without an RSS token" do
- it "shows the RSS button without an RSS token" do
- expect(page).to have_css("a:has(.fa-rss):not([href*='rss_token'])")
+shared_examples "it has an RSS button without a feed token" do
+ it "shows the RSS button without a feed token" do
+ expect(page).to have_css("a:has(.fa-rss):not([href*='feed_token'])")
end
end
diff --git a/spec/support/generate-seed-repo-rb b/spec/support/generate-seed-repo-rb
index 44b3de23b99..bee9d419376 100755
--- a/spec/support/generate-seed-repo-rb
+++ b/spec/support/generate-seed-repo-rb
@@ -15,7 +15,7 @@
require 'erb'
require 'tempfile'
-SOURCE = File.expand_path('../gitlab-git-test.git', __FILE__).freeze
+SOURCE = File.expand_path('gitlab-git-test.git', __dir__).freeze
SCRIPT_NAME = 'generate-seed-repo-rb'.freeze
REPO_NAME = 'gitlab-git-test.git'.freeze
diff --git a/spec/support/gitaly.rb b/spec/support/gitaly.rb
index 9cf541372b5..614aaa73693 100644
--- a/spec/support/gitaly.rb
+++ b/spec/support/gitaly.rb
@@ -7,7 +7,10 @@ RSpec.configure do |config|
next if example.metadata[:skip_gitaly_mock]
# Use 'and_wrap_original' to make sure the arguments are valid
- allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_wrap_original { |m, *args| m.call(*args) || true }
+ allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_wrap_original do |m, *args|
+ m.call(*args)
+ !Gitlab::GitalyClient.explicit_opt_in_required.include?(args.first)
+ end
end
end
end
diff --git a/spec/support/gitlab-git-test.git/packed-refs b/spec/support/gitlab-git-test.git/packed-refs
index ea50e4ad3f6..6a61e5df267 100644
--- a/spec/support/gitlab-git-test.git/packed-refs
+++ b/spec/support/gitlab-git-test.git/packed-refs
@@ -9,6 +9,7 @@
4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6 refs/heads/master
5937ac0a7beb003549fc5fd26fc247adbce4a52e refs/heads/merge-test
9596bc54a6f0c0c98248fe97077eb5ccf48a98d0 refs/heads/missing-gitmodules
+4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6 refs/heads/Ääh-test-utf-8
f4e6814c3e4e7a0de82a9e7cd20c626cc963a2f8 refs/tags/v1.0.0
^6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b refs/tags/v1.1.0
diff --git a/spec/support/gitlab_stubs/project_8.json b/spec/support/gitlab_stubs/project_8.json
index f0a9fce859c..81b08ab8288 100644
--- a/spec/support/gitlab_stubs/project_8.json
+++ b/spec/support/gitlab_stubs/project_8.json
@@ -1,38 +1,38 @@
{
- "id":8,
- "description":"ssh access and repository management app for GitLab",
- "default_branch":"master",
- "public":false,
- "visibility_level":0,
- "ssh_url_to_repo":"git@demo.gitlab.com:gitlab/gitlab-shell.git",
- "http_url_to_repo":"http://demo.gitlab.com/gitlab/gitlab-shell.git",
- "web_url":"http://demo.gitlab.com/gitlab/gitlab-shell",
- "owner": {
- "id":4,
- "name":"GitLab",
- "created_at":"2012-12-21T13:03:05Z"
- },
- "name":"gitlab-shell",
- "name_with_namespace":"GitLab / gitlab-shell",
- "path":"gitlab-shell",
- "path_with_namespace":"gitlab/gitlab-shell",
- "issues_enabled":true,
- "merge_requests_enabled":true,
- "wall_enabled":false,
- "wiki_enabled":true,
- "snippets_enabled":false,
- "created_at":"2013-03-20T13:28:53Z",
- "last_activity_at":"2013-11-30T00:11:17Z",
- "namespace":{
- "created_at":"2012-12-21T13:03:05Z",
- "description":"Self hosted Git management software",
- "id":4,
- "name":"GitLab",
- "owner_id":1,
- "path":"gitlab",
- "updated_at":"2013-03-20T13:29:13Z"
+ "id": 8,
+ "description": "ssh access and repository management app for GitLab",
+ "default_branch": "master",
+ "visibility": "private",
+ "ssh_url_to_repo": "git@demo.gitlab.com:gitlab/gitlab-shell.git",
+ "http_url_to_repo": "http://demo.gitlab.com/gitlab/gitlab-shell.git",
+ "web_url": "http://demo.gitlab.com/gitlab/gitlab-shell",
+ "owner": {
+ "id": 4,
+ "name": "GitLab",
+ "username": "gitlab",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61",
+ "web_url": "http://demo.gitlab.com/gitlab"
},
- "permissions":{
+ "name": "gitlab-shell",
+ "name_with_namespace": "GitLab / gitlab-shell",
+ "path": "gitlab-shell",
+ "path_with_namespace": "gitlab/gitlab-shell",
+ "issues_enabled": true,
+ "merge_requests_enabled": true,
+ "wiki_enabled": true,
+ "snippets_enabled": false,
+ "created_at": "2013-03-20T13:28:53Z",
+ "last_activity_at": "2013-11-30T00:11:17Z",
+ "namespace": {
+ "id": 4,
+ "name": "GitLab",
+ "path": "gitlab",
+ "kind": "group",
+ "full_path": "gitlab",
+ "parent_id": null
+ },
+ "permissions": {
"project_access": {
"access_level": 10,
"notification_level": 3
@@ -42,4 +42,4 @@
"notification_level": 3
}
}
-} \ No newline at end of file
+}
diff --git a/spec/support/gitlab_stubs/projects.json b/spec/support/gitlab_stubs/projects.json
index ca42c14c5d8..67ce1acca2c 100644
--- a/spec/support/gitlab_stubs/projects.json
+++ b/spec/support/gitlab_stubs/projects.json
@@ -1 +1,282 @@
-[{"id":3,"description":"GitLab is open source software to collaborate on code. Create projects and repositories, manage access and do code reviews.","default_branch":"master","public":true,"visibility_level":20,"ssh_url_to_repo":"git@demo.gitlab.com:gitlab/gitlabhq.git","http_url_to_repo":"http://demo.gitlab.com/gitlab/gitlabhq.git","web_url":"http://demo.gitlab.com/gitlab/gitlabhq","owner":{"id":4,"name":"GitLab","created_at":"2012-12-21T13:03:05Z"},"name":"gitlabhq","name_with_namespace":"GitLab / gitlabhq","path":"gitlabhq","path_with_namespace":"gitlab/gitlabhq","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":true,"wiki_enabled":true,"snippets_enabled":true,"created_at":"2012-12-21T13:06:34Z","last_activity_at":"2013-12-02T19:10:10Z","namespace":{"created_at":"2012-12-21T13:03:05Z","description":"Self hosted Git management software","id":4,"name":"GitLab","owner_id":1,"path":"gitlab","updated_at":"2013-03-20T13:29:13Z"}},{"id":4,"description":"Component of GitLab CI. Web application","default_branch":"master","public":false,"visibility_level":0,"ssh_url_to_repo":"git@demo.gitlab.com:gitlab/gitlab-ci.git","http_url_to_repo":"http://demo.gitlab.com/gitlab/gitlab-ci.git","web_url":"http://demo.gitlab.com/gitlab/gitlab-ci","owner":{"id":4,"name":"GitLab","created_at":"2012-12-21T13:03:05Z"},"name":"gitlab-ci","name_with_namespace":"GitLab / gitlab-ci","path":"gitlab-ci","path_with_namespace":"gitlab/gitlab-ci","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":true,"wiki_enabled":true,"snippets_enabled":true,"created_at":"2012-12-21T13:06:50Z","last_activity_at":"2013-11-28T19:26:54Z","namespace":{"created_at":"2012-12-21T13:03:05Z","description":"Self hosted Git management software","id":4,"name":"GitLab","owner_id":1,"path":"gitlab","updated_at":"2013-03-20T13:29:13Z"}},{"id":5,"description":"","default_branch":"master","public":true,"visibility_level":20,"ssh_url_to_repo":"git@demo.gitlab.com:gitlab/gitlab-recipes.git","http_url_to_repo":"http://demo.gitlab.com/gitlab/gitlab-recipes.git","web_url":"http://demo.gitlab.com/gitlab/gitlab-recipes","owner":{"id":4,"name":"GitLab","created_at":"2012-12-21T13:03:05Z"},"name":"gitlab-recipes","name_with_namespace":"GitLab / gitlab-recipes","path":"gitlab-recipes","path_with_namespace":"gitlab/gitlab-recipes","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":true,"wiki_enabled":true,"snippets_enabled":true,"created_at":"2012-12-21T13:07:02Z","last_activity_at":"2013-12-02T13:54:10Z","namespace":{"created_at":"2012-12-21T13:03:05Z","description":"Self hosted Git management software","id":4,"name":"GitLab","owner_id":1,"path":"gitlab","updated_at":"2013-03-20T13:29:13Z"}},{"id":8,"description":"ssh access and repository management app for GitLab","default_branch":"master","public":false,"visibility_level":0,"ssh_url_to_repo":"git@demo.gitlab.com:gitlab/gitlab-shell.git","http_url_to_repo":"http://demo.gitlab.com/gitlab/gitlab-shell.git","web_url":"http://demo.gitlab.com/gitlab/gitlab-shell","owner":{"id":4,"name":"GitLab","created_at":"2012-12-21T13:03:05Z"},"name":"gitlab-shell","name_with_namespace":"GitLab / gitlab-shell","path":"gitlab-shell","path_with_namespace":"gitlab/gitlab-shell","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":false,"wiki_enabled":true,"snippets_enabled":false,"created_at":"2013-03-20T13:28:53Z","last_activity_at":"2013-11-30T00:11:17Z","namespace":{"created_at":"2012-12-21T13:03:05Z","description":"Self hosted Git management software","id":4,"name":"GitLab","owner_id":1,"path":"gitlab","updated_at":"2013-03-20T13:29:13Z"}},{"id":9,"description":null,"default_branch":"master","public":false,"visibility_level":0,"ssh_url_to_repo":"git@demo.gitlab.com:gitlab/gitlab_git.git","http_url_to_repo":"http://demo.gitlab.com/gitlab/gitlab_git.git","web_url":"http://demo.gitlab.com/gitlab/gitlab_git","owner":{"id":4,"name":"GitLab","created_at":"2012-12-21T13:03:05Z"},"name":"gitlab_git","name_with_namespace":"GitLab / gitlab_git","path":"gitlab_git","path_with_namespace":"gitlab/gitlab_git","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":false,"wiki_enabled":true,"snippets_enabled":false,"created_at":"2013-04-28T19:15:08Z","last_activity_at":"2013-12-02T13:07:13Z","namespace":{"created_at":"2012-12-21T13:03:05Z","description":"Self hosted Git management software","id":4,"name":"GitLab","owner_id":1,"path":"gitlab","updated_at":"2013-03-20T13:29:13Z"}},{"id":10,"description":"ultra lite authorization library http://randx.github.com/six/\\r\\n ","default_branch":"master","public":true,"visibility_level":20,"ssh_url_to_repo":"git@demo.gitlab.com:sandbox/six.git","http_url_to_repo":"http://demo.gitlab.com/sandbox/six.git","web_url":"http://demo.gitlab.com/sandbox/six","owner":{"id":8,"name":"Sandbox","created_at":"2013-08-01T16:44:17Z"},"name":"Six","name_with_namespace":"Sandbox / Six","path":"six","path_with_namespace":"sandbox/six","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":false,"wiki_enabled":true,"snippets_enabled":false,"created_at":"2013-08-01T16:45:02Z","last_activity_at":"2013-11-29T11:30:56Z","namespace":{"created_at":"2013-08-01T16:44:17Z","description":"","id":8,"name":"Sandbox","owner_id":1,"path":"sandbox","updated_at":"2013-08-01T16:44:17Z"}},{"id":11,"description":"Simple HTML5 Charts using the <canvas> tag ","default_branch":"master","public":false,"visibility_level":0,"ssh_url_to_repo":"git@demo.gitlab.com:sandbox/charts-js.git","http_url_to_repo":"http://demo.gitlab.com/sandbox/charts-js.git","web_url":"http://demo.gitlab.com/sandbox/charts-js","owner":{"id":8,"name":"Sandbox","created_at":"2013-08-01T16:44:17Z"},"name":"Charts.js","name_with_namespace":"Sandbox / Charts.js","path":"charts-js","path_with_namespace":"sandbox/charts-js","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":false,"wiki_enabled":true,"snippets_enabled":false,"created_at":"2013-08-01T16:47:29Z","last_activity_at":"2013-12-02T15:18:11Z","namespace":{"created_at":"2013-08-01T16:44:17Z","description":"","id":8,"name":"Sandbox","owner_id":1,"path":"sandbox","updated_at":"2013-08-01T16:44:17Z"}},{"id":13,"description":"","default_branch":"master","public":false,"visibility_level":0,"ssh_url_to_repo":"git@demo.gitlab.com:sandbox/afro.git","http_url_to_repo":"http://demo.gitlab.com/sandbox/afro.git","web_url":"http://demo.gitlab.com/sandbox/afro","owner":{"id":8,"name":"Sandbox","created_at":"2013-08-01T16:44:17Z"},"name":"Afro","name_with_namespace":"Sandbox / Afro","path":"afro","path_with_namespace":"sandbox/afro","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":false,"wiki_enabled":true,"snippets_enabled":false,"created_at":"2013-11-14T17:45:19Z","last_activity_at":"2013-12-02T17:41:45Z","namespace":{"created_at":"2013-08-01T16:44:17Z","description":"","id":8,"name":"Sandbox","owner_id":1,"path":"sandbox","updated_at":"2013-08-01T16:44:17Z"}}] \ No newline at end of file
+[
+ {
+ "id": 3,
+ "description": "GitLab is open source software to collaborate on code. Create projects and repositories, manage access and do code reviews.",
+ "default_branch": "master",
+ "visibility": "public",
+ "ssh_url_to_repo": "git@demo.gitlab.com:gitlab/gitlabhq.git",
+ "http_url_to_repo": "http://demo.gitlab.com/gitlab/gitlabhq.git",
+ "web_url": "http://demo.gitlab.com/gitlab/gitlabhq",
+ "owner": {
+ "id": 4,
+ "name": "GitLab",
+ "username": "gitlab",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61",
+ "web_url": "http://demo.gitlab.com/gitlab"
+ },
+ "name": "gitlabhq",
+ "name_with_namespace": "GitLab / gitlabhq",
+ "path": "gitlabhq",
+ "path_with_namespace": "gitlab/gitlabhq",
+ "issues_enabled": true,
+ "merge_requests_enabled": true,
+ "wiki_enabled": true,
+ "snippets_enabled": true,
+ "created_at": "2012-12-21T13:06:34Z",
+ "last_activity_at": "2013-12-02T19:10:10Z",
+ "namespace": {
+ "id": 4,
+ "name": "GitLab",
+ "path": "gitlab",
+ "kind": "group",
+ "full_path": "gitlab",
+ "parent_id": null
+ }
+ },
+ {
+ "id": 4,
+ "description": "Component of GitLab CI. Web application",
+ "default_branch": "master",
+ "visibility": "private",
+ "ssh_url_to_repo": "git@demo.gitlab.com:gitlab/gitlab-ci.git",
+ "http_url_to_repo": "http://demo.gitlab.com/gitlab/gitlab-ci.git",
+ "web_url": "http://demo.gitlab.com/gitlab/gitlab-ci",
+ "owner": {
+ "id": 4,
+ "name": "GitLab",
+ "username": "gitlab",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61",
+ "web_url": "http://demo.gitlab.com/gitlab"
+ },
+ "name": "gitlab-ci",
+ "name_with_namespace": "GitLab / gitlab-ci",
+ "path": "gitlab-ci",
+ "path_with_namespace": "gitlab/gitlab-ci",
+ "issues_enabled": true,
+ "merge_requests_enabled": true,
+ "wiki_enabled": true,
+ "snippets_enabled": true,
+ "created_at": "2012-12-21T13:06:50Z",
+ "last_activity_at": "2013-11-28T19:26:54Z",
+ "namespace": {
+ "id": 4,
+ "name": "GitLab",
+ "path": "gitlab",
+ "kind": "group",
+ "full_path": "gitlab",
+ "parent_id": null
+ }
+ },
+ {
+ "id": 5,
+ "description": "",
+ "default_branch": "master",
+ "visibility": "public",
+ "ssh_url_to_repo": "git@demo.gitlab.com:gitlab/gitlab-recipes.git",
+ "http_url_to_repo": "http://demo.gitlab.com/gitlab/gitlab-recipes.git",
+ "web_url": "http://demo.gitlab.com/gitlab/gitlab-recipes",
+ "owner": {
+ "id": 4,
+ "name": "GitLab",
+ "username": "gitlab",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61",
+ "web_url": "http://demo.gitlab.com/gitlab"
+ },
+ "name": "gitlab-recipes",
+ "name_with_namespace": "GitLab / gitlab-recipes",
+ "path": "gitlab-recipes",
+ "path_with_namespace": "gitlab/gitlab-recipes",
+ "issues_enabled": true,
+ "merge_requests_enabled": true,
+ "wiki_enabled": true,
+ "snippets_enabled": true,
+ "created_at": "2012-12-21T13:07:02Z",
+ "last_activity_at": "2013-12-02T13:54:10Z",
+ "namespace": {
+ "id": 4,
+ "name": "GitLab",
+ "path": "gitlab",
+ "kind": "group",
+ "full_path": "gitlab",
+ "parent_id": null
+ }
+ },
+ {
+ "id": 8,
+ "description": "ssh access and repository management app for GitLab",
+ "default_branch": "master",
+ "visibility": "private",
+ "ssh_url_to_repo": "git@demo.gitlab.com:gitlab/gitlab-shell.git",
+ "http_url_to_repo": "http://demo.gitlab.com/gitlab/gitlab-shell.git",
+ "web_url": "http://demo.gitlab.com/gitlab/gitlab-shell",
+ "owner": {
+ "id": 4,
+ "name": "GitLab",
+ "username": "gitlab",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61",
+ "web_url": "http://demo.gitlab.com/gitlab"
+ },
+ "name": "gitlab-shell",
+ "name_with_namespace": "GitLab / gitlab-shell",
+ "path": "gitlab-shell",
+ "path_with_namespace": "gitlab/gitlab-shell",
+ "issues_enabled": true,
+ "merge_requests_enabled": true,
+ "wiki_enabled": true,
+ "snippets_enabled": false,
+ "created_at": "2013-03-20T13:28:53Z",
+ "last_activity_at": "2013-11-30T00:11:17Z",
+ "namespace": {
+ "id": 4,
+ "name": "GitLab",
+ "path": "gitlab",
+ "kind": "group",
+ "full_path": "gitlab",
+ "parent_id": null
+ }
+ },
+ {
+ "id": 9,
+ "description": null,
+ "default_branch": "master",
+ "visibility": "private",
+ "ssh_url_to_repo": "git@demo.gitlab.com:gitlab/gitlab_git.git",
+ "http_url_to_repo": "http://demo.gitlab.com/gitlab/gitlab_git.git",
+ "web_url": "http://demo.gitlab.com/gitlab/gitlab_git",
+ "owner": {
+ "id": 4,
+ "name": "GitLab",
+ "username": "gitlab",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61",
+ "web_url": "http://demo.gitlab.com/gitlab"
+ },
+ "name": "gitlab_git",
+ "name_with_namespace": "GitLab / gitlab_git",
+ "path": "gitlab_git",
+ "path_with_namespace": "gitlab/gitlab_git",
+ "issues_enabled": true,
+ "merge_requests_enabled": true,
+ "wiki_enabled": true,
+ "snippets_enabled": false,
+ "created_at": "2013-04-28T19:15:08Z",
+ "last_activity_at": "2013-12-02T13:07:13Z",
+ "namespace": {
+ "id": 4,
+ "name": "GitLab",
+ "path": "gitlab",
+ "kind": "group",
+ "full_path": "gitlab",
+ "parent_id": null
+ }
+ },
+ {
+ "id": 10,
+ "description": "ultra lite authorization library http://randx.github.com/six/\\r\\n ",
+ "default_branch": "master",
+ "visibility": "public",
+ "ssh_url_to_repo": "git@demo.gitlab.com:sandbox/six.git",
+ "http_url_to_repo": "http://demo.gitlab.com/sandbox/six.git",
+ "web_url": "http://demo.gitlab.com/sandbox/six",
+ "owner": {
+ "id": 4,
+ "name": "GitLab",
+ "username": "gitlab",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61",
+ "web_url": "http://demo.gitlab.com/gitlab"
+ },
+ "name": "Six",
+ "name_with_namespace": "Sandbox / Six",
+ "path": "six",
+ "path_with_namespace": "sandbox/six",
+ "issues_enabled": true,
+ "merge_requests_enabled": true,
+ "wiki_enabled": true,
+ "snippets_enabled": false,
+ "created_at": "2013-08-01T16:45:02Z",
+ "last_activity_at": "2013-11-29T11:30:56Z",
+ "namespace": {
+ "id": 4,
+ "name": "GitLab",
+ "path": "gitlab",
+ "kind": "group",
+ "full_path": "gitlab",
+ "parent_id": null
+ }
+ },
+ {
+ "id": 11,
+ "description": "Simple HTML5 Charts using the <canvas> tag ",
+ "default_branch": "master",
+ "visibility": "private",
+ "ssh_url_to_repo": "git@demo.gitlab.com:sandbox/charts-js.git",
+ "http_url_to_repo": "http://demo.gitlab.com/sandbox/charts-js.git",
+ "web_url": "http://demo.gitlab.com/sandbox/charts-js",
+ "owner": {
+ "id": 4,
+ "name": "GitLab",
+ "username": "gitlab",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61",
+ "web_url": "http://demo.gitlab.com/gitlab"
+ },
+ "name": "Charts.js",
+ "name_with_namespace": "Sandbox / Charts.js",
+ "path": "charts-js",
+ "path_with_namespace": "sandbox/charts-js",
+ "issues_enabled": true,
+ "merge_requests_enabled": true,
+ "wiki_enabled": true,
+ "snippets_enabled": false,
+ "created_at": "2013-08-01T16:47:29Z",
+ "last_activity_at": "2013-12-02T15:18:11Z",
+ "namespace": {
+ "id": 4,
+ "name": "GitLab",
+ "path": "gitlab",
+ "kind": "group",
+ "full_path": "gitlab",
+ "parent_id": null
+ }
+ },
+ {
+ "id": 13,
+ "description": "",
+ "default_branch": "master",
+ "visibility": "private",
+ "ssh_url_to_repo": "git@demo.gitlab.com:sandbox/afro.git",
+ "http_url_to_repo": "http://demo.gitlab.com/sandbox/afro.git",
+ "web_url": "http://demo.gitlab.com/sandbox/afro",
+ "owner": {
+ "id": 4,
+ "name": "GitLab",
+ "username": "gitlab",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61",
+ "web_url": "http://demo.gitlab.com/gitlab"
+ },
+ "name": "Afro",
+ "name_with_namespace": "Sandbox / Afro",
+ "path": "afro",
+ "path_with_namespace": "sandbox/afro",
+ "issues_enabled": true,
+ "merge_requests_enabled": true,
+ "wiki_enabled": true,
+ "snippets_enabled": false,
+ "created_at": "2013-11-14T17:45:19Z",
+ "last_activity_at": "2013-12-02T17:41:45Z",
+ "namespace": {
+ "id": 4,
+ "name": "GitLab",
+ "path": "gitlab",
+ "kind": "group",
+ "full_path": "gitlab",
+ "parent_id": null
+ }
+ }
+]
diff --git a/spec/support/gitlab_stubs/session.json b/spec/support/gitlab_stubs/session.json
deleted file mode 100644
index 658ff5871b0..00000000000
--- a/spec/support/gitlab_stubs/session.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "id":2,
- "username":"jsmith",
- "email":"test@test.com",
- "name":"John Smith",
- "bio":"",
- "skype":"aertert",
- "linkedin":"",
- "twitter":"",
- "theme_id":2,"color_scheme_id":2,
- "state":"active",
- "created_at":"2012-12-21T13:02:20Z",
- "extern_uid":null,
- "provider":null,
- "is_admin":false,
- "can_create_group":false,
- "can_create_project":false
-}
diff --git a/spec/support/helpers/api_helpers.rb b/spec/support/helpers/api_helpers.rb
index ac0c7a9b493..a57a3b2cf34 100644
--- a/spec/support/helpers/api_helpers.rb
+++ b/spec/support/helpers/api_helpers.rb
@@ -36,15 +36,4 @@ module ApiHelpers
full_path
end
-
- # Temporary helper method for simplifying V3 exclusive API specs
- def v3_api(path, user = nil, personal_access_token: nil, oauth_access_token: nil)
- api(
- path,
- user,
- version: 'v3',
- personal_access_token: personal_access_token,
- oauth_access_token: oauth_access_token
- )
- end
end
diff --git a/spec/support/helpers/assets_helpers.rb b/spec/support/helpers/assets_helpers.rb
new file mode 100644
index 00000000000..09bbf451671
--- /dev/null
+++ b/spec/support/helpers/assets_helpers.rb
@@ -0,0 +1,15 @@
+module AssetsHelpers
+ # In a CI environment the assets are not compiled, as there is a CI job
+ # `compile-assets` that compiles them in the prepare stage for all following
+ # specs.
+ # Locally the assets are precompiled dynamically.
+ #
+ # Sprockets doesn't provide one method to access an asset for both cases.
+ def find_asset(asset_name)
+ if ENV['CI']
+ Sprockets::Railtie.build_environment(Rails.application, true)[asset_name]
+ else
+ Rails.application.assets.find_asset(asset_name)
+ end
+ end
+end
diff --git a/spec/support/helpers/board_helpers.rb b/spec/support/helpers/board_helpers.rb
index 507d0432d7f..b85fde222ea 100644
--- a/spec/support/helpers/board_helpers.rb
+++ b/spec/support/helpers/board_helpers.rb
@@ -1,7 +1,7 @@
module BoardHelpers
def click_card(card)
within card do
- first('.card-number').click
+ first('.board-card-number').click
end
wait_for_sidebar
diff --git a/spec/support/helpers/cycle_analytics_helpers.rb b/spec/support/helpers/cycle_analytics_helpers.rb
index 55359d36597..32d9807f06a 100644
--- a/spec/support/helpers/cycle_analytics_helpers.rb
+++ b/spec/support/helpers/cycle_analytics_helpers.rb
@@ -4,12 +4,12 @@ module CycleAnalyticsHelpers
create_commit("Commit for ##{issue.iid}", issue.project, user, branch_name)
end
- def create_commit(message, project, user, branch_name, count: 1)
+ def create_commit(message, project, user, branch_name, count: 1, commit_time: nil, skip_push_handler: false)
repository = project.repository
- oldrev = repository.commit(branch_name).sha
+ oldrev = repository.commit(branch_name)&.sha || Gitlab::Git::BLANK_SHA
if Timecop.frozen? && Gitlab::GitalyClient.feature_enabled?(:operation_user_commit_files)
- mock_gitaly_multi_action_dates(repository.raw)
+ mock_gitaly_multi_action_dates(repository.raw, commit_time)
end
commit_shas = Array.new(count) do |index|
@@ -19,6 +19,8 @@ module CycleAnalyticsHelpers
commit_sha
end
+ return if skip_push_handler
+
GitPushService.new(project,
user,
oldrev: oldrev,
@@ -44,13 +46,11 @@ module CycleAnalyticsHelpers
project.repository.add_branch(user, source_branch, 'master')
end
- sha = project.repository.create_file(
- user,
- generate(:branch),
- 'content',
- message: commit_message,
- branch_name: source_branch)
- project.repository.commit(sha)
+ # Cycle analytic specs often test with frozen times, which causes metrics to be
+ # pinned to the current time. For example, in the plan stage, we assume that an issue
+ # milestone has been created before any code has been written. We add a second
+ # to ensure that the plan time is positive.
+ create_commit(commit_message, project, user, source_branch, commit_time: Time.now + 1.second, skip_push_handler: true)
opts = {
title: 'Awesome merge_request',
@@ -116,14 +116,18 @@ module CycleAnalyticsHelpers
protected: false)
end
- def mock_gitaly_multi_action_dates(raw_repository)
+ def mock_gitaly_multi_action_dates(raw_repository, commit_time)
allow(raw_repository).to receive(:multi_action).and_wrap_original do |m, *args|
- new_date = Time.now
+ new_date = commit_time || Time.now
branch_update = m.call(*args)
if branch_update.newrev
_, opts = args
- commit = raw_repository.commit(branch_update.newrev).rugged_commit
+
+ commit = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ raw_repository.commit(branch_update.newrev).rugged_commit
+ end
+
branch_update.newrev = commit.amend(
update_ref: "#{Gitlab::Git::BRANCH_REF_PREFIX}#{opts[:branch_name]}",
author: commit.author.merge(time: new_date),
diff --git a/spec/support/helpers/drag_to_helper.rb b/spec/support/helpers/drag_to_helper.rb
index ae149631ed9..6d53ad0b602 100644
--- a/spec/support/helpers/drag_to_helper.rb
+++ b/spec/support/helpers/drag_to_helper.rb
@@ -1,6 +1,6 @@
module DragTo
- def drag_to(list_from_index: 0, from_index: 0, to_index: 0, list_to_index: 0, selector: '', scrollable: 'body')
- evaluate_script("simulateDrag({scrollable: $('#{scrollable}').get(0), from: {el: $('#{selector}').eq(#{list_from_index}).get(0), index: #{from_index}}, to: {el: $('#{selector}').eq(#{list_to_index}).get(0), index: #{to_index}}});")
+ def drag_to(list_from_index: 0, from_index: 0, to_index: 0, list_to_index: 0, selector: '', scrollable: 'body', duration: 1000)
+ evaluate_script("simulateDrag({scrollable: $('#{scrollable}').get(0), duration: #{duration}, from: {el: $('#{selector}').eq(#{list_from_index}).get(0), index: #{from_index}}, to: {el: $('#{selector}').eq(#{list_to_index}).get(0), index: #{to_index}}});")
Timeout.timeout(Capybara.default_max_wait_time) do
loop while drag_active?
diff --git a/spec/support/helpers/exclusive_lease_helpers.rb b/spec/support/helpers/exclusive_lease_helpers.rb
new file mode 100644
index 00000000000..383cc7dee81
--- /dev/null
+++ b/spec/support/helpers/exclusive_lease_helpers.rb
@@ -0,0 +1,36 @@
+module ExclusiveLeaseHelpers
+ def stub_exclusive_lease(key = nil, uuid = 'uuid', renew: false, timeout: nil)
+ key ||= instance_of(String)
+ timeout ||= instance_of(Integer)
+
+ lease = instance_double(
+ Gitlab::ExclusiveLease,
+ try_obtain: uuid,
+ exists?: true,
+ renew: renew
+ )
+
+ allow(Gitlab::ExclusiveLease)
+ .to receive(:new)
+ .with(key, timeout: timeout)
+ .and_return(lease)
+
+ lease
+ end
+
+ def stub_exclusive_lease_taken(key = nil, timeout: nil)
+ stub_exclusive_lease(key, nil, timeout: timeout)
+ end
+
+ def expect_to_obtain_exclusive_lease(key, uuid = 'uuid', timeout: nil)
+ lease = stub_exclusive_lease(key, uuid, timeout: timeout)
+
+ expect(lease).to receive(:try_obtain)
+ end
+
+ def expect_to_cancel_exclusive_lease(key, uuid)
+ expect(Gitlab::ExclusiveLease)
+ .to receive(:cancel)
+ .with(key, uuid)
+ end
+end
diff --git a/spec/support/helpers/expect_next_instance_of.rb b/spec/support/helpers/expect_next_instance_of.rb
new file mode 100644
index 00000000000..b95046b2b42
--- /dev/null
+++ b/spec/support/helpers/expect_next_instance_of.rb
@@ -0,0 +1,13 @@
+module ExpectNextInstanceOf
+ def expect_next_instance_of(klass, *new_args)
+ receive_new = receive(:new)
+ receive_new.with(*new_args) if new_args.any?
+
+ expect(klass).to receive_new
+ .and_wrap_original do |method, *original_args|
+ method.call(*original_args).tap do |instance|
+ yield(instance)
+ end
+ end
+ end
+end
diff --git a/spec/support/helpers/features/notes_helpers.rb b/spec/support/helpers/features/notes_helpers.rb
index 1a1d5853a7a..2b9f8b30c60 100644
--- a/spec/support/helpers/features/notes_helpers.rb
+++ b/spec/support/helpers/features/notes_helpers.rb
@@ -13,7 +13,7 @@ module Spec
module Features
module NotesHelpers
def add_note(text)
- Sidekiq::Testing.fake! do
+ perform_enqueued_jobs do
page.within(".js-main-target-form") do
fill_in("note[note]", with: text)
find(".js-comment-submit-button").click
diff --git a/spec/support/helpers/features/sorting_helpers.rb b/spec/support/helpers/features/sorting_helpers.rb
index 50457b64745..ad0053ec5cf 100644
--- a/spec/support/helpers/features/sorting_helpers.rb
+++ b/spec/support/helpers/features/sorting_helpers.rb
@@ -15,7 +15,7 @@ module Spec
def sort_by(value)
find('button.dropdown-toggle').click
- page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do
+ page.within('.content ul.dropdown-menu.dropdown-menu-right li') do
click_link(value)
end
end
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
new file mode 100644
index 00000000000..b9322975b5a
--- /dev/null
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -0,0 +1,111 @@
+module GraphqlHelpers
+ # makes an underscored string look like a fieldname
+ # "merge_request" => "mergeRequest"
+ def self.fieldnamerize(underscored_field_name)
+ graphql_field_name = underscored_field_name.to_s.camelize
+ graphql_field_name[0] = graphql_field_name[0].downcase
+
+ graphql_field_name
+ end
+
+ # Run a loader's named resolver
+ def resolve(resolver_class, obj: nil, args: {}, ctx: {})
+ resolver_class.new(object: obj, context: ctx).resolve(args)
+ end
+
+ # Runs a block inside a BatchLoader::Executor wrapper
+ def batch(max_queries: nil, &blk)
+ wrapper = proc do
+ begin
+ BatchLoader::Executor.ensure_current
+ yield
+ ensure
+ BatchLoader::Executor.clear_current
+ end
+ end
+
+ if max_queries
+ result = nil
+ expect { result = wrapper.call }.not_to exceed_query_limit(max_queries)
+ result
+ else
+ wrapper.call
+ end
+ end
+
+ def graphql_query_for(name, attributes = {}, fields = nil)
+ <<~QUERY
+ {
+ #{query_graphql_field(name, attributes, fields)}
+ }
+ QUERY
+ end
+
+ def query_graphql_field(name, attributes = {}, fields = nil)
+ fields ||= all_graphql_fields_for(name.classify)
+ attributes = attributes_to_graphql(attributes)
+ <<~QUERY
+ #{name}(#{attributes}) {
+ #{fields}
+ }
+ QUERY
+ end
+
+ def all_graphql_fields_for(class_name)
+ type = GitlabSchema.types[class_name.to_s]
+ return "" unless type
+
+ type.fields.map do |name, field|
+ # We can't guess arguments, so skip fields that require them
+ next if required_arguments?(field)
+
+ if nested_fields?(field)
+ "#{name} { #{all_graphql_fields_for(field_type(field))} }"
+ else
+ name
+ end
+ end.compact.join("\n")
+ end
+
+ def attributes_to_graphql(attributes)
+ attributes.map do |name, value|
+ "#{GraphqlHelpers.fieldnamerize(name.to_s)}: \"#{value}\""
+ end.join(", ")
+ end
+
+ def post_graphql(query, current_user: nil)
+ post api('/', current_user, version: 'graphql'), query: query
+ end
+
+ def graphql_data
+ json_response['data']
+ end
+
+ def graphql_errors
+ json_response['data']
+ end
+
+ def nested_fields?(field)
+ !scalar?(field) && !enum?(field)
+ end
+
+ def scalar?(field)
+ field_type(field).kind.scalar?
+ end
+
+ def enum?(field)
+ field_type(field).kind.enum?
+ end
+
+ def required_arguments?(field)
+ field.arguments.values.any? { |argument| argument.type.non_null? }
+ end
+
+ def field_type(field)
+ if field.type.respond_to?(:of_type)
+ field.type.of_type
+ else
+ field.type
+ end
+ end
+end
diff --git a/spec/support/helpers/jira_service_helper.rb b/spec/support/helpers/jira_service_helper.rb
index 88a7aeba461..f4d5343c4ed 100644
--- a/spec/support/helpers/jira_service_helper.rb
+++ b/spec/support/helpers/jira_service_helper.rb
@@ -12,7 +12,7 @@ module JiraServiceHelper
jira_issue_transition_id: '1'
}
- jira_tracker.update_attributes(properties: properties, active: true)
+ jira_tracker.update(properties: properties, active: true)
end
def jira_issue_comments
diff --git a/spec/support/helpers/key_generator_helper.rb b/spec/support/helpers/key_generator_helper.rb
index b1c289ffef7..d55d8312c65 100644
--- a/spec/support/helpers/key_generator_helper.rb
+++ b/spec/support/helpers/key_generator_helper.rb
@@ -24,7 +24,7 @@ module Spec
private
# Encodes an openssh-mpi-encoded integer.
- def encode_mpi(n)
+ def encode_mpi(n) # rubocop:disable Naming/UncommunicativeMethodParamName
chars, n = [], n.to_i
chars << (n & 0xff) && n >>= 8 while n != 0
chars << 0 if chars.empty? || chars.last >= 0x80
diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb
index 72e5c2d66dd..87cfb6c04dc 100644
--- a/spec/support/helpers/login_helpers.rb
+++ b/spec/support/helpers/login_helpers.rb
@@ -46,8 +46,8 @@ module LoginHelpers
@current_user = user
end
- def gitlab_sign_in_via(provider, user, uid)
- mock_auth_hash(provider, uid, user.email)
+ def gitlab_sign_in_via(provider, user, uid, saml_response = nil)
+ mock_auth_hash(provider, uid, user.email, saml_response)
visit new_user_session_path
click_link provider
end
@@ -87,7 +87,7 @@ module LoginHelpers
click_link "oauth-login-#{provider}"
end
- def mock_auth_hash(provider, uid, email)
+ def mock_auth_hash(provider, uid, email, saml_response = nil)
# The mock_auth configuration allows you to set per-provider (or default)
# authentication hashes to return during integration testing.
OmniAuth.config.mock_auth[provider.to_sym] = OmniAuth::AuthHash.new({
@@ -109,12 +109,21 @@ module LoginHelpers
email: email,
image: 'mock_user_thumbnail_url'
}
+ },
+ response_object: {
+ document: saml_xml(saml_response)
}
}
})
Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[provider.to_sym]
end
+ def saml_xml(raw_saml_response)
+ return '' if raw_saml_response.blank?
+
+ XMLSecurity::SignedDocument.new(raw_saml_response, [])
+ end
+
def mock_saml_config
OpenStruct.new(name: 'saml', label: 'saml', args: {
assertion_consumer_service_url: 'https://localhost:3443/users/auth/saml/callback',
@@ -125,6 +134,14 @@ module LoginHelpers
})
end
+ def mock_saml_config_with_upstream_two_factor_authn_contexts
+ config = mock_saml_config
+ config.args[:upstream_two_factor_authn_contexts] = %w(urn:oasis:names:tc:SAML:2.0:ac:classes:CertificateProtectedTransport
+ urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorOTPSMS
+ urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorIGTOKEN)
+ config
+ end
+
def stub_omniauth_provider(provider, context: Rails.application)
env = env_from_context(context)
@@ -132,20 +149,36 @@ module LoginHelpers
env['omniauth.auth'] = OmniAuth.config.mock_auth[provider.to_sym]
end
- def stub_omniauth_saml_config(messages)
- set_devise_mapping(context: Rails.application)
- Rails.application.routes.disable_clear_and_finalize = true
- Rails.application.routes.draw do
+ def stub_omniauth_failure(strategy, message_key, exception = nil)
+ env = @request.env
+
+ env['omniauth.error'] = exception
+ env['omniauth.error.type'] = message_key.to_sym
+ env['omniauth.error.strategy'] = strategy
+ end
+
+ def stub_omniauth_saml_config(messages, context: Rails.application)
+ set_devise_mapping(context: context)
+ routes = Rails.application.routes
+ routes.disable_clear_and_finalize = true
+ routes.formatter.clear
+ routes.draw do
post '/users/auth/saml' => 'omniauth_callbacks#saml'
end
- allow(Gitlab::Auth::OAuth::Provider).to receive_messages(providers: [:saml], config_for: mock_saml_config)
+ saml_config = messages.key?(:providers) ? messages[:providers].first : mock_saml_config
+ allow(Gitlab::Auth::OAuth::Provider).to receive_messages(providers: [:saml], config_for: saml_config)
stub_omniauth_setting(messages)
stub_saml_authorize_path_helpers
end
def stub_saml_authorize_path_helpers
- 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')
+ allow_any_instance_of(ActionDispatch::Routing::RoutesProxy)
+ .to receive(:user_saml_omniauth_authorize_path)
+ .and_return('/users/auth/saml')
+ allow(Devise::OmniAuth::UrlHelpers)
+ .to receive(:omniauth_authorize_path)
+ .with(:user, "saml")
+ .and_return('/users/auth/saml')
end
def stub_omniauth_config(messages)
diff --git a/spec/support/helpers/markdown_feature.rb b/spec/support/helpers/markdown_feature.rb
index 39e94ad53de..346f5b1cc4d 100644
--- a/spec/support/helpers/markdown_feature.rb
+++ b/spec/support/helpers/markdown_feature.rb
@@ -24,7 +24,7 @@ class MarkdownFeature
def project
@project ||= create(:project, :repository, group: group).tap do |project|
- project.add_master(user)
+ project.add_maintainer(user)
end
end
diff --git a/spec/support/helpers/merge_request_diff_helpers.rb b/spec/support/helpers/merge_request_diff_helpers.rb
index c98aa503ed1..3b49d0b3319 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', visible: false).send_keys(:return)
+ line[:num].find('.js-add-diff-note-button', visible: false).send_keys(:return)
end
def get_line_components(line_holder, diff_side = nil)
diff --git a/spec/support/helpers/migrations_helpers.rb b/spec/support/helpers/migrations_helpers.rb
index 84abec75c26..0bc235701eb 100644
--- a/spec/support/helpers/migrations_helpers.rb
+++ b/spec/support/helpers/migrations_helpers.rb
@@ -10,10 +10,6 @@ module MigrationsHelpers
ActiveRecord::Migrator.migrations_paths
end
- def table_exists?(name)
- ActiveRecord::Base.connection.table_exists?(name)
- end
-
def migrations
ActiveRecord::Migrator.migrations(migrations_paths)
end
diff --git a/spec/support/helpers/mobile_helpers.rb b/spec/support/helpers/mobile_helpers.rb
index 3b9eb84e824..9dc1f1de436 100644
--- a/spec/support/helpers/mobile_helpers.rb
+++ b/spec/support/helpers/mobile_helpers.rb
@@ -1,10 +1,10 @@
module MobileHelpers
def resize_screen_xs
- resize_window(767, 768)
+ resize_window(575, 768)
end
def resize_screen_sm
- resize_window(900, 768)
+ resize_window(767, 768)
end
def restore_window_size
diff --git a/spec/support/helpers/query_recorder.rb b/spec/support/helpers/query_recorder.rb
index 28536bbef5e..7ce63375d34 100644
--- a/spec/support/helpers/query_recorder.rb
+++ b/spec/support/helpers/query_recorder.rb
@@ -1,10 +1,11 @@
module ActiveRecord
class QueryRecorder
- attr_reader :log, :cached
+ attr_reader :log, :skip_cached, :cached
- def initialize(&block)
+ def initialize(skip_cached: true, &block)
@log = []
@cached = []
+ @skip_cached = skip_cached
ActiveSupport::Notifications.subscribed(method(:callback), 'sql.active_record', &block)
end
@@ -16,7 +17,7 @@ module ActiveRecord
def callback(name, start, finish, message_id, values)
show_backtrace(values) if ENV['QUERY_RECORDER_DEBUG']
- if values[:name]&.include?("CACHE")
+ if values[:name]&.include?("CACHE") && skip_cached
@cached << values[:sql]
elsif !values[:name]&.include?("SCHEMA")
@log << values[:sql]
diff --git a/spec/support/helpers/routes_helpers.rb b/spec/support/helpers/routes_helpers.rb
new file mode 100644
index 00000000000..c4129606418
--- /dev/null
+++ b/spec/support/helpers/routes_helpers.rb
@@ -0,0 +1,7 @@
+module RoutesHelpers
+ def fake_routes(&block)
+ @routes = @routes.dup
+ @routes.formatter.clear
+ ActionDispatch::Routing::Mapper.new(@routes).instance_exec(&block)
+ end
+end
diff --git a/spec/support/helpers/seed_repo.rb b/spec/support/helpers/seed_repo.rb
index b4868e82cd7..71f1a86b0c1 100644
--- a/spec/support/helpers/seed_repo.rb
+++ b/spec/support/helpers/seed_repo.rb
@@ -98,6 +98,7 @@ module SeedRepo
master
merge-test
missing-gitmodules
+ Ääh-test-utf-8
].freeze
TAGS = %w[
v1.0.0
diff --git a/spec/support/helpers/sorting_helper.rb b/spec/support/helpers/sorting_helper.rb
index 577518d726c..9496a94d8f4 100644
--- a/spec/support/helpers/sorting_helper.rb
+++ b/spec/support/helpers/sorting_helper.rb
@@ -11,7 +11,7 @@
module SortingHelper
def sorting_by(value)
find('button.dropdown-toggle').click
- page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do
+ page.within('.content ul.dropdown-menu.dropdown-menu-right li') do
click_link value
end
end
diff --git a/spec/support/helpers/stub_gitlab_calls.rb b/spec/support/helpers/stub_gitlab_calls.rb
index c1618f5086c..2933f2c78dc 100644
--- a/spec/support/helpers/stub_gitlab_calls.rb
+++ b/spec/support/helpers/stub_gitlab_calls.rb
@@ -1,6 +1,5 @@
module StubGitlabCalls
def stub_gitlab_calls
- stub_session
stub_user
stub_project_8
stub_project_8_hooks
@@ -71,23 +70,14 @@ module StubGitlabCalls
Gitlab.config.gitlab.url
end
- def stub_session
- f = File.read(Rails.root.join('spec/support/gitlab_stubs/session.json'))
-
- stub_request(:post, "#{gitlab_url}api/v3/session.json")
- .with(body: "{\"email\":\"test@test.com\",\"password\":\"123456\"}",
- headers: { 'Content-Type' => 'application/json' })
- .to_return(status: 201, body: f, headers: { 'Content-Type' => 'application/json' })
- end
-
def stub_user
f = File.read(Rails.root.join('spec/support/gitlab_stubs/user.json'))
- stub_request(:get, "#{gitlab_url}api/v3/user?private_token=Wvjy2Krpb7y8xi93owUz")
+ stub_request(:get, "#{gitlab_url}api/v4/user?private_token=Wvjy2Krpb7y8xi93owUz")
.with(headers: { 'Content-Type' => 'application/json' })
.to_return(status: 200, body: f, headers: { 'Content-Type' => 'application/json' })
- stub_request(:get, "#{gitlab_url}api/v3/user?access_token=some_token")
+ stub_request(:get, "#{gitlab_url}api/v4/user?access_token=some_token")
.with(headers: { 'Content-Type' => 'application/json' })
.to_return(status: 200, body: f, headers: { 'Content-Type' => 'application/json' })
end
@@ -105,19 +95,19 @@ module StubGitlabCalls
def stub_projects
f = File.read(Rails.root.join('spec/support/gitlab_stubs/projects.json'))
- stub_request(:get, "#{gitlab_url}api/v3/projects.json?archived=false&ci_enabled_first=true&private_token=Wvjy2Krpb7y8xi93owUz")
+ stub_request(:get, "#{gitlab_url}api/v4/projects.json?archived=false&ci_enabled_first=true&private_token=Wvjy2Krpb7y8xi93owUz")
.with(headers: { 'Content-Type' => 'application/json' })
.to_return(status: 200, body: f, headers: { 'Content-Type' => 'application/json' })
end
def stub_projects_owned
- stub_request(:get, "#{gitlab_url}api/v3/projects/owned.json?archived=false&ci_enabled_first=true&private_token=Wvjy2Krpb7y8xi93owUz")
+ stub_request(:get, "#{gitlab_url}api/v4/projects?owned=true&archived=false&ci_enabled_first=true&private_token=Wvjy2Krpb7y8xi93owUz")
.with(headers: { 'Content-Type' => 'application/json' })
.to_return(status: 200, body: "", headers: {})
end
def stub_ci_enable
- stub_request(:put, "#{gitlab_url}api/v3/projects/2/services/gitlab-ci.json?private_token=Wvjy2Krpb7y8xi93owUz")
+ stub_request(:put, "#{gitlab_url}api/v4/projects/2/services/gitlab-ci.json?private_token=Wvjy2Krpb7y8xi93owUz")
.with(headers: { 'Content-Type' => 'application/json' })
.to_return(status: 200, body: "", headers: {})
end
diff --git a/spec/support/helpers/stub_object_storage.rb b/spec/support/helpers/stub_object_storage.rb
index 19d744b959a..58b5c6a6435 100644
--- a/spec/support/helpers/stub_object_storage.rb
+++ b/spec/support/helpers/stub_object_storage.rb
@@ -15,11 +15,21 @@ module StubObjectStorage
return unless enabled
+ stub_object_storage(connection_params: uploader.object_store_credentials,
+ remote_directory: remote_directory)
+ end
+
+ def stub_object_storage(connection_params:, remote_directory:)
Fog.mock!
- ::Fog::Storage.new(uploader.object_store_credentials).tap do |connection|
+ ::Fog::Storage.new(connection_params).tap do |connection|
begin
connection.directories.create(key: remote_directory)
+
+ # Cleanup remaining files
+ connection.directories.each do |directory|
+ directory.files.map(&:destroy)
+ end
rescue Excon::Error::Conflict
end
end
@@ -45,4 +55,16 @@ module StubObjectStorage
remote_directory: 'uploads',
**params)
end
+
+ def stub_object_storage_multipart_init(endpoint, upload_id = "upload_id")
+ stub_request(:post, %r{\A#{endpoint}tmp/uploads/[a-z0-9-]*\?uploads\z})
+ .to_return status: 200, body: <<-EOS.strip_heredoc
+ <?xml version="1.0" encoding="UTF-8"?>
+ <InitiateMultipartUploadResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
+ <Bucket>example-bucket</Bucket>
+ <Key>example-object</Key>
+ <UploadId>#{upload_id}</UploadId>
+ </InitiateMultipartUploadResult>
+ EOS
+ end
end
diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb
index 57aa07cf4fa..e531495d917 100644
--- a/spec/support/helpers/test_env.rb
+++ b/spec/support/helpers/test_env.rb
@@ -47,8 +47,11 @@ module TestEnv
'v1.1.0' => 'b83d6e3',
'add-ipython-files' => '93ee732',
'add-pdf-file' => 'e774ebd',
+ 'squash-large-files' => '54cec52',
'add-pdf-text-binary' => '79faa7b',
- 'add_images_and_changes' => '010d106'
+ 'add_images_and_changes' => '010d106',
+ 'update-gitlab-shell-v-6-0-1' => '2f61d70',
+ 'update-gitlab-shell-v-6-0-3' => 'de78448'
}.freeze
# gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
@@ -134,6 +137,16 @@ module TestEnv
install_dir: Gitlab.config.gitlab_shell.path,
version: Gitlab::Shell.version_required,
task: 'gitlab:shell:install')
+
+ create_fake_git_hooks
+ end
+
+ def create_fake_git_hooks
+ # gitlab-shell hooks don't work in our test environment because they try to make internal API calls
+ hooks_dir = File.join(Gitlab.config.gitlab_shell.path, 'hooks')
+ %w[pre-receive post-receive update].each do |hook|
+ File.open(File.join(hooks_dir, hook), 'w', 0755) { |f| f.puts '#!/bin/sh' }
+ end
end
def setup_gitaly
diff --git a/spec/support/helpers/wait_for_requests.rb b/spec/support/helpers/wait_for_requests.rb
index fda0e29f983..c7f878b7371 100644
--- a/spec/support/helpers/wait_for_requests.rb
+++ b/spec/support/helpers/wait_for_requests.rb
@@ -24,7 +24,9 @@ module WaitForRequests
# Wait for client-side AJAX requests
def wait_for_requests
- wait_for('JS requests complete') { finished_all_js_requests? }
+ wait_for('JS requests complete', max_wait_time: 2 * Capybara.default_max_wait_time) do
+ finished_all_js_requests?
+ end
end
# Wait for active Rack requests and client-side AJAX requests
diff --git a/spec/support/http_io/http_io_helpers.rb b/spec/support/http_io/http_io_helpers.rb
index 2c68c2cd9a6..42144870eb5 100644
--- a/spec/support/http_io/http_io_helpers.rb
+++ b/spec/support/http_io/http_io_helpers.rb
@@ -1,65 +1,49 @@
module HttpIOHelpers
- def stub_remote_trace_206
- WebMock.stub_request(:get, remote_trace_url)
- .to_return { |request| remote_trace_response(request, 206) }
+ def stub_remote_url_206(url, file_path)
+ WebMock.stub_request(:get, url)
+ .to_return { |request| remote_url_response(file_path, request, 206) }
end
- def stub_remote_trace_200
- WebMock.stub_request(:get, remote_trace_url)
- .to_return { |request| remote_trace_response(request, 200) }
+ def stub_remote_url_200(url, file_path)
+ WebMock.stub_request(:get, url)
+ .to_return { |request| remote_url_response(file_path, request, 200) }
end
- def stub_remote_trace_500
- WebMock.stub_request(:get, remote_trace_url)
+ def stub_remote_url_500(url)
+ WebMock.stub_request(:get, url)
.to_return(status: [500, "Internal Server Error"])
end
- def remote_trace_url
- "http://trace.com/trace"
- end
-
- def remote_trace_response(request, responce_status)
+ def remote_url_response(file_path, request, response_status)
range = request.headers['Range'].match(/bytes=(\d+)-(\d+)/)
+ body = File.read(file_path).force_encoding(Encoding::BINARY)
+ size = body.bytesize
+
{
- status: responce_status,
- headers: remote_trace_response_headers(responce_status, range[1].to_i, range[2].to_i),
- body: range_trace_body(range[1].to_i, range[2].to_i)
+ status: response_status,
+ headers: remote_url_response_headers(response_status, range[1].to_i, range[2].to_i, size),
+ body: body[range[1].to_i..range[2].to_i]
}
end
- def remote_trace_response_headers(responce_status, from, to)
- headers = { 'Content-Type' => 'text/plain' }
-
- if responce_status == 206
- headers.merge('Content-Range' => "bytes #{from}-#{to}/#{remote_trace_size}")
+ def remote_url_response_headers(response_status, from, to, size)
+ { 'Content-Type' => 'text/plain' }.tap do |headers|
+ if response_status == 206
+ headers.merge('Content-Range' => "bytes #{from}-#{to}/#{size}")
+ end
end
-
- headers
- end
-
- def range_trace_body(from, to)
- remote_trace_body[from..to]
- end
-
- def remote_trace_body
- @remote_trace_body ||= File.read(expand_fixture_path('trace/sample_trace'))
- .force_encoding(Encoding::BINARY)
- end
-
- def remote_trace_size
- remote_trace_body.bytesize
end
def set_smaller_buffer_size_than(file_size)
blocks = (file_size / 128)
new_size = (blocks / 2) * 128
- stub_const("Gitlab::Ci::Trace::HttpIO::BUFFER_SIZE", new_size)
+ stub_const("Gitlab::HttpIO::BUFFER_SIZE", new_size)
end
def set_larger_buffer_size_than(file_size)
blocks = (file_size / 128)
new_size = (blocks * 2) * 128
- stub_const("Gitlab::Ci::Trace::HttpIO::BUFFER_SIZE", new_size)
+ stub_const("Gitlab::HttpIO::BUFFER_SIZE", new_size)
end
end
diff --git a/spec/support/import_export/configuration_helper.rb b/spec/support/import_export/configuration_helper.rb
index f752508d48c..bbac6ca6a9c 100644
--- a/spec/support/import_export/configuration_helper.rb
+++ b/spec/support/import_export/configuration_helper.rb
@@ -10,7 +10,7 @@ module ConfigurationHelper
def relation_class_for_name(relation_name)
relation_name = Gitlab::ImportExport::RelationFactory::OVERRIDES[relation_name.to_sym] || relation_name
- relation_name.to_s.classify.constantize
+ Gitlab::ImportExport::RelationFactory.relation_class(relation_name)
end
def parsed_attributes(relation_name, attributes)
diff --git a/spec/support/import_export/export_file_helper.rb b/spec/support/import_export/export_file_helper.rb
index 562423afc2a..4d925ac77f4 100644
--- a/spec/support/import_export/export_file_helper.rb
+++ b/spec/support/import_export/export_file_helper.rb
@@ -37,7 +37,7 @@ module ExportFileHelper
event = create(:event, :created, target: milestone, project: project, author: user, action: 5)
create(:push_event_payload, event: event)
- create(:project_member, :master, user: user, project: project)
+ create(:project_member, :maintainer, user: user, project: project)
create(:ci_variable, project: project)
create(:ci_trigger, project: project)
key = create(:deploy_key)
diff --git a/spec/support/matchers/access_matchers_for_controller.rb b/spec/support/matchers/access_matchers_for_controller.rb
index 42a9ed9ff34..429401a5da8 100644
--- a/spec/support/matchers/access_matchers_for_controller.rb
+++ b/spec/support/matchers/access_matchers_for_controller.rb
@@ -24,7 +24,7 @@ module AccessMatchersForController
when User
user = role
sign_in(user)
- when *Gitlab::Access.sym_options_with_owner.keys # owner, master, developer, reporter, guest
+ when *Gitlab::Access.sym_options_with_owner.keys # owner, maintainer, developer, reporter, guest
raise ArgumentError, "cannot emulate #{role} without membership parent" unless membership
user = create_user_by_membership(role, membership)
diff --git a/spec/support/matchers/disallow_request_matchers.rb b/spec/support/matchers/disallow_request_matchers.rb
new file mode 100644
index 00000000000..db4d90e4fd0
--- /dev/null
+++ b/spec/support/matchers/disallow_request_matchers.rb
@@ -0,0 +1,15 @@
+RSpec::Matchers.define :disallow_request do
+ match do |middleware|
+ alert = middleware.env['rack.session'].to_hash
+ .dig('flash', 'flashes', 'alert')
+
+ alert&.include?('You cannot perform write operations')
+ end
+end
+
+RSpec::Matchers.define :disallow_request_in_json do
+ match do |response|
+ json_response = JSON.parse(response.body)
+ response.body.include?('You cannot perform write operations') && json_response.key?('message')
+ end
+end
diff --git a/spec/support/matchers/email_matchers.rb b/spec/support/matchers/email_matchers.rb
deleted file mode 100644
index d9d59ec12ec..00000000000
--- a/spec/support/matchers/email_matchers.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-RSpec::Matchers.define :have_html_escaped_body_text do |expected|
- match do |actual|
- expect(actual).to have_body_text(ERB::Util.html_escape(expected))
- end
-end
diff --git a/spec/support/matchers/exceed_query_limit.rb b/spec/support/matchers/exceed_query_limit.rb
index 88d22a3ddd9..cd042401f3a 100644
--- a/spec/support/matchers/exceed_query_limit.rb
+++ b/spec/support/matchers/exceed_query_limit.rb
@@ -1,17 +1,4 @@
-RSpec::Matchers.define :exceed_query_limit do |expected|
- supports_block_expectations
-
- match do |block|
- @subject_block = block
- actual_count > expected_count + threshold
- end
-
- failure_message_when_negated do |actual|
- threshold_message = threshold > 0 ? " (+#{@threshold})" : ''
- counts = "#{expected_count}#{threshold_message}"
- "Expected a maximum of #{counts} queries, got #{actual_count}:\n\n#{log_message}"
- end
-
+module ExceedQueryLimitHelpers
def with_threshold(threshold)
@threshold = threshold
self
@@ -43,7 +30,7 @@ RSpec::Matchers.define :exceed_query_limit do |expected|
end
def recorder
- @recorder ||= ActiveRecord::QueryRecorder.new(&@subject_block)
+ @recorder ||= ActiveRecord::QueryRecorder.new(skip_cached: skip_cached, &@subject_block)
end
def count_queries(queries)
@@ -61,4 +48,52 @@ RSpec::Matchers.define :exceed_query_limit do |expected|
@recorder.log_message
end
end
+
+ def skip_cached
+ true
+ end
+
+ def verify_count(&block)
+ @subject_block = block
+ actual_count > expected_count + threshold
+ end
+
+ def failure_message
+ threshold_message = threshold > 0 ? " (+#{@threshold})" : ''
+ counts = "#{expected_count}#{threshold_message}"
+ "Expected a maximum of #{counts} queries, got #{actual_count}:\n\n#{log_message}"
+ end
+end
+
+RSpec::Matchers.define :exceed_all_query_limit do |expected|
+ supports_block_expectations
+
+ include ExceedQueryLimitHelpers
+
+ match do |block|
+ verify_count(&block)
+ end
+
+ failure_message_when_negated do |actual|
+ failure_message
+ end
+
+ def skip_cached
+ false
+ end
+end
+
+# Excludes cached methods from the query count
+RSpec::Matchers.define :exceed_query_limit do |expected|
+ supports_block_expectations
+
+ include ExceedQueryLimitHelpers
+
+ match do |block|
+ verify_count(&block)
+ end
+
+ failure_message_when_negated do |actual|
+ failure_message
+ end
end
diff --git a/spec/support/matchers/graphql_matchers.rb b/spec/support/matchers/graphql_matchers.rb
new file mode 100644
index 00000000000..be6fa4c71a0
--- /dev/null
+++ b/spec/support/matchers/graphql_matchers.rb
@@ -0,0 +1,71 @@
+RSpec::Matchers.define :require_graphql_authorizations do |*expected|
+ match do |field|
+ field_definition = field.metadata[:type_class]
+ expect(field_definition).to respond_to(:required_permissions)
+ expect(field_definition.required_permissions).to contain_exactly(*expected)
+ end
+end
+
+RSpec::Matchers.define :have_graphql_fields do |*expected|
+ def expected_field_names
+ expected.map { |name| GraphqlHelpers.fieldnamerize(name) }
+ end
+
+ match do |kls|
+ expect(kls.fields.keys).to contain_exactly(*expected_field_names)
+ end
+
+ failure_message do |kls|
+ missing = expected_field_names - kls.fields.keys
+ extra = kls.fields.keys - expected_field_names
+
+ message = []
+
+ message << "is missing fields: <#{missing.inspect}>" if missing.any?
+ message << "contained unexpected fields: <#{extra.inspect}>" if extra.any?
+
+ message.join("\n")
+ end
+end
+
+RSpec::Matchers.define :have_graphql_field do |field_name|
+ match do |kls|
+ expect(kls.fields.keys).to include(GraphqlHelpers.fieldnamerize(field_name))
+ end
+end
+
+RSpec::Matchers.define :have_graphql_arguments do |*expected|
+ include GraphqlHelpers
+
+ match do |field|
+ argument_names = expected.map { |name| GraphqlHelpers.fieldnamerize(name) }
+ expect(field.arguments.keys).to contain_exactly(*argument_names)
+ end
+end
+
+RSpec::Matchers.define :have_graphql_type do |expected|
+ match do |field|
+ expect(field.type).to eq(expected.to_graphql)
+ end
+end
+
+RSpec::Matchers.define :have_graphql_resolver do |expected|
+ match do |field|
+ case expected
+ when Method
+ expect(field.metadata[:type_class].resolve_proc).to eq(expected)
+ else
+ expect(field.metadata[:type_class].resolver).to eq(expected)
+ end
+ end
+end
+
+RSpec::Matchers.define :expose_permissions_using do |expected|
+ match do |type|
+ permission_field = type.fields['userPermissions']
+
+ expect(permission_field).not_to be_nil
+ expect(permission_field.type).to be_non_null
+ expect(permission_field.type.of_type.graphql_name).to eq(expected.graphql_name)
+ end
+end
diff --git a/spec/support/matchers/match_ids.rb b/spec/support/matchers/match_ids.rb
index d8424405b96..1cb6b74acac 100644
--- a/spec/support/matchers/match_ids.rb
+++ b/spec/support/matchers/match_ids.rb
@@ -10,6 +10,13 @@ RSpec::Matchers.define :match_ids do |*expected|
'matches elements by ids'
end
+ failure_message do
+ actual_ids = map_ids(actual)
+ expected_ids = map_ids(expected)
+
+ "expected IDs #{actual_ids} in:\n\n #{actual.inspect}\n\nto match IDs #{expected_ids} in:\n\n #{expected.inspect}"
+ end
+
def map_ids(elements)
elements = elements.flatten if elements.respond_to?(:flatten)
diff --git a/spec/support/redis/redis_shared_examples.rb b/spec/support/redis/redis_shared_examples.rb
index 8676f895a83..e650a176041 100644
--- a/spec/support/redis/redis_shared_examples.rb
+++ b/spec/support/redis/redis_shared_examples.rb
@@ -65,6 +65,14 @@ RSpec.shared_examples "redis_shared_examples" do
end
describe '.url' do
+ it 'withstands mutation' do
+ url1 = described_class.url
+ url2 = described_class.url
+ url1 << 'foobar' unless url1.frozen?
+
+ expect(url2).not_to end_with('foobar')
+ end
+
context 'when yml file with env variable' do
let(:config_file_name) { config_with_environment_variable_inside }
@@ -101,7 +109,6 @@ RSpec.shared_examples "redis_shared_examples" do
before do
clear_pool
end
-
after do
clear_pool
end
diff --git a/spec/support/rspec.rb b/spec/support/rspec.rb
index dffab22d8b5..54b8df7aa19 100644
--- a/spec/support/rspec.rb
+++ b/spec/support/rspec.rb
@@ -9,4 +9,6 @@ RSpec.configure do |config|
config.include StubConfiguration
config.include StubObjectStorage
config.include StubENV
+
+ config.fixture_path = Rails.root if defined?(Rails)
end
diff --git a/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb b/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb
index 7b064162726..8b4cffaac19 100644
--- a/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb
+++ b/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb
@@ -3,7 +3,7 @@
shared_examples 'new issuable record that supports quick actions' do
let!(:project) { create(:project, :repository) }
- let(:user) { create(:user).tap { |u| project.add_master(u) } }
+ let(:user) { create(:user).tap { |u| project.add_maintainer(u) } }
let(:assignee) { create(:user) }
let!(:milestone) { create(:milestone, project: project) }
let!(:labels) { create_list(:label, 3, project: project) }
@@ -12,7 +12,7 @@ shared_examples 'new issuable record that supports quick actions' do
let(:issuable) { described_class.new(project, user, params).execute }
before do
- project.add_master(assignee)
+ project.add_maintainer(assignee)
end
context 'with labels in command only' do
diff --git a/spec/support/shared_examples/ci_trace_shared_examples.rb b/spec/support/shared_examples/ci_trace_shared_examples.rb
index 21c6f3c829f..94e82b8ce90 100644
--- a/spec/support/shared_examples/ci_trace_shared_examples.rb
+++ b/spec/support/shared_examples/ci_trace_shared_examples.rb
@@ -138,6 +138,28 @@ shared_examples_for 'common trace features' do
end
end
+ describe '#write' do
+ subject { trace.send(:write, mode) { } }
+
+ let(:mode) { 'wb' }
+
+ context 'when arhicved trace does not exist yet' do
+ it 'does not raise an error' do
+ expect { subject }.not_to raise_error
+ end
+ end
+
+ context 'when arhicved trace already exists' do
+ before do
+ create(:ci_job_artifact, :trace, job: build)
+ end
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(Gitlab::Ci::Trace::AlreadyArchivedError)
+ end
+ end
+ end
+
describe '#set' do
before do
trace.set("12")
@@ -227,6 +249,44 @@ shared_examples_for 'common trace features' do
end
end
end
+
+ describe '#archive!' do
+ subject { trace.archive! }
+
+ context 'when build status is success' do
+ let!(:build) { create(:ci_build, :success, :trace_live) }
+
+ it 'does not have an archived trace yet' do
+ expect(build.job_artifacts_trace).to be_nil
+ end
+
+ context 'when archives' do
+ it 'has an archived trace' do
+ subject
+
+ build.reload
+ expect(build.job_artifacts_trace).to be_exist
+ end
+
+ context 'when another process has already been archiving', :clean_gitlab_redis_shared_state do
+ include ExclusiveLeaseHelpers
+
+ before do
+ stub_exclusive_lease_taken("trace:archive:#{trace.job.id}", timeout: 1.hour)
+ end
+
+ it 'blocks concurrent archiving' do
+ expect(Rails.logger).to receive(:error).with('Cannot obtain an exclusive lease. There must be another instance already in execution.')
+
+ subject
+
+ build.reload
+ expect(build.job_artifacts_trace).to be_nil
+ end
+ end
+ end
+ end
+ end
end
shared_examples_for 'trace with disabled live trace feature' do
@@ -536,7 +596,7 @@ shared_examples_for 'trace with disabled live trace feature' do
it 'does not archive' do
expect_any_instance_of(described_class).not_to receive(:archive_stream!)
- expect { subject }.to raise_error('Already archived')
+ expect { subject }.to raise_error(Gitlab::Ci::Trace::AlreadyArchivedError)
expect(build.job_artifacts_trace.file.exists?).to be_truthy
end
end
@@ -551,6 +611,55 @@ shared_examples_for 'trace with disabled live trace feature' do
end
end
end
+
+ describe '#erase!' do
+ subject { trace.erase! }
+
+ context 'when it is a live trace' do
+ context 'when trace is stored in database' do
+ let(:build) { create(:ci_build) }
+
+ before do
+ build.update_column(:trace, 'sample trace')
+ end
+
+ it { expect(trace.raw).not_to be_nil }
+
+ it "removes trace" do
+ subject
+
+ expect(trace.raw).to be_nil
+ end
+ end
+
+ context 'when trace is stored in file storage' do
+ let(:build) { create(:ci_build, :trace_live) }
+
+ it { expect(trace.raw).not_to be_nil }
+
+ it "removes trace" do
+ subject
+
+ expect(trace.raw).to be_nil
+ end
+ end
+ end
+
+ context 'when it is an archived trace' do
+ let(:build) { create(:ci_build, :trace_artifact) }
+
+ it "has trace at first" do
+ expect(trace.raw).not_to be_nil
+ end
+
+ it "removes trace" do
+ subject
+
+ build.reload
+ expect(trace.raw).to be_nil
+ end
+ end
+ end
end
shared_examples_for 'trace with enabled live trace feature' do
@@ -723,7 +832,7 @@ shared_examples_for 'trace with enabled live trace feature' do
it 'does not archive' do
expect_any_instance_of(described_class).not_to receive(:archive_stream!)
- expect { subject }.to raise_error('Already archived')
+ expect { subject }.to raise_error(Gitlab::Ci::Trace::AlreadyArchivedError)
expect(build.job_artifacts_trace.file.exists?).to be_truthy
end
end
@@ -738,4 +847,35 @@ shared_examples_for 'trace with enabled live trace feature' do
end
end
end
+
+ describe '#erase!' do
+ subject { trace.erase! }
+
+ context 'when it is a live trace' do
+ let(:build) { create(:ci_build, :trace_live) }
+
+ it { expect(trace.raw).not_to be_nil }
+
+ it "removes trace" do
+ subject
+
+ expect(trace.raw).to be_nil
+ end
+ end
+
+ context 'when it is an archived trace' do
+ let(:build) { create(:ci_build, :trace_artifact) }
+
+ it "has trace at first" do
+ expect(trace.raw).not_to be_nil
+ end
+
+ it "removes trace" do
+ subject
+
+ build.reload
+ expect(trace.raw).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb
index ea7dbade171..7088fb1e5fb 100644
--- a/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb
@@ -1,7 +1,7 @@
shared_examples 'handle uploads' do
let(:user) { create(:user) }
- let(:jpg) { fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg') }
- let(:txt) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') }
+ let(:jpg) { fixture_file_upload('spec/fixtures/rails_sample.jpg', 'image/jpg') }
+ let(:txt) { fixture_file_upload('spec/fixtures/doc_sample.txt', 'text/plain') }
let(:secret) { FileUploader.generate_secret }
let(:uploader_class) { FileUploader }
@@ -260,4 +260,83 @@ shared_examples 'handle uploads' do
end
end
end
+
+ describe "POST #authorize" do
+ context 'when a user is not authorized to upload a file' do
+ it 'returns 404 status' do
+ post_authorize
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context 'when a user can upload a file' do
+ before do
+ sign_in(user)
+ model.add_developer(user)
+ end
+
+ context 'and the request bypassed workhorse' do
+ it 'raises an exception' do
+ expect { post_authorize(verified: false) }.to raise_error JWT::DecodeError
+ end
+ end
+
+ context 'and request is sent by gitlab-workhorse to authorize the request' do
+ shared_examples 'a valid response' do
+ before do
+ post_authorize
+ end
+
+ it 'responds with status 200' do
+ expect(response).to have_gitlab_http_status(200)
+ end
+
+ it 'uses the gitlab-workhorse content type' do
+ expect(response.headers["Content-Type"]).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ end
+ end
+
+ shared_examples 'a local file' do
+ it_behaves_like 'a valid response' do
+ it 'responds with status 200, location of uploads store and object details' do
+ expect(json_response['TempPath']).to eq(uploader_class.workhorse_local_upload_path)
+ expect(json_response['RemoteObject']).to be_nil
+ end
+ end
+ end
+
+ context 'when using local storage' do
+ it_behaves_like 'a local file'
+ end
+
+ context 'when using remote storage' do
+ context 'when direct upload is enabled' do
+ before do
+ stub_uploads_object_storage(uploader_class, direct_upload: true)
+ end
+
+ it_behaves_like 'a valid response' do
+ it 'responds with status 200, location of uploads remote store and object details' do
+ expect(json_response['TempPath']).to eq(uploader_class.workhorse_local_upload_path)
+ expect(json_response['RemoteObject']).to have_key('ID')
+ expect(json_response['RemoteObject']).to have_key('GetURL')
+ expect(json_response['RemoteObject']).to have_key('StoreURL')
+ expect(json_response['RemoteObject']).to have_key('DeleteURL')
+ expect(json_response['RemoteObject']).to have_key('MultipartUpload')
+ end
+ end
+ end
+
+ context 'when direct upload is disabled' do
+ before do
+ stub_uploads_object_storage(uploader_class, direct_upload: false)
+ end
+
+ it_behaves_like 'a local file'
+ end
+ end
+ end
+ end
+ end
end
diff --git a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb
index 5a569d233bc..7038a366144 100644
--- a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb
+++ b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb
@@ -10,9 +10,9 @@ RSpec.shared_examples 'a creatable merge request' do
let!(:label2) { create(:label, project: target_project) }
before do
- source_project.add_master(user)
- target_project.add_master(user)
- target_project.add_master(user2)
+ source_project.add_maintainer(user)
+ target_project.add_maintainer(user)
+ target_project.add_maintainer(user2)
sign_in(user)
visit project_new_merge_request_path(
diff --git a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb
index 645db41cddc..3057845061b 100644
--- a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb
+++ b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb
@@ -15,9 +15,9 @@ RSpec.shared_examples 'an editable merge request' do
end
before do
- source_project.add_master(user)
- target_project.add_master(user)
- target_project.add_master(user2)
+ source_project.add_maintainer(user)
+ target_project.add_maintainer(user)
+ target_project.add_maintainer(user2)
sign_in(user)
visit edit_project_merge_request_path(target_project, merge_request)
diff --git a/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb b/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb
index b29bb3c2fc0..75ad948e42c 100644
--- a/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb
+++ b/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb
@@ -1,20 +1,20 @@
-RSpec.shared_examples 'Master manages access requests' do
+RSpec.shared_examples 'Maintainer manages access requests' do
let(:user) { create(:user) }
- let(:master) { create(:user) }
+ let(:maintainer) { create(:user) }
before do
entity.request_access(user)
- entity.respond_to?(:add_owner) ? entity.add_owner(master) : entity.add_master(master)
- sign_in(master)
+ entity.respond_to?(:add_owner) ? entity.add_owner(maintainer) : entity.add_maintainer(maintainer)
+ sign_in(maintainer)
end
- it 'master can see access requests' do
+ it 'maintainer can see access requests' do
visit members_page_path
expect_visible_access_request(entity, user)
end
- it 'master can grant access', :js do
+ it 'maintainer can grant access', :js do
visit members_page_path
expect_visible_access_request(entity, user)
@@ -28,7 +28,7 @@ RSpec.shared_examples 'Master manages access requests' do
end
end
- it 'master can deny access', :js do
+ it 'maintainer can deny access', :js do
visit members_page_path
expect_visible_access_request(entity, user)
diff --git a/spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb b/spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb
index 639b0924197..64c3b80136d 100644
--- a/spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb
+++ b/spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb
@@ -18,7 +18,7 @@ shared_examples 'project features apply to issuables' do |klass|
before do
_ = issuable
- gitlab_sign_in(user) if user
+ sign_in(user) if user
visit path
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 17f319f49e9..a8f2c2e7a5a 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
@@ -5,12 +5,18 @@ shared_examples "protected branches > access control > CE" do
set_protected_branch_name('master')
+ find(".js-allowed-to-merge").click
+ within('.qa-allowed-to-merge-dropdown') do
+ expect(first("li")).to have_content("Roles")
+ find(:link, 'No one').click
+ end
+
within('.js-new-protected-branch') do
allowed_to_push_button = find(".js-allowed-to-push")
unless allowed_to_push_button.text == access_type_name
allowed_to_push_button.click
- within(".dropdown.open .dropdown-menu") { click_on access_type_name }
+ within(".dropdown.show .dropdown-menu") { click_on access_type_name }
end
end
@@ -25,6 +31,18 @@ shared_examples "protected branches > access control > CE" do
set_protected_branch_name('master')
+ find(".js-allowed-to-merge").click
+ within('.qa-allowed-to-merge-dropdown') do
+ expect(first("li")).to have_content("Roles")
+ find(:link, 'No one').click
+ end
+
+ find(".js-allowed-to-push").click
+ within('.qa-allowed-to-push-dropdown') do
+ expect(first("li")).to have_content("Roles")
+ find(:link, 'No one').click
+ end
+
click_on "Protect"
expect(ProtectedBranch.count).to eq(1)
@@ -55,10 +73,16 @@ shared_examples "protected branches > access control > CE" do
unless allowed_to_merge_button.text == access_type_name
allowed_to_merge_button.click
- within(".dropdown.open .dropdown-menu") { click_on access_type_name }
+ within(".dropdown.show .dropdown-menu") { click_on access_type_name }
end
end
+ find(".js-allowed-to-push").click
+ within('.qa-allowed-to-push-dropdown') do
+ expect(first("li")).to have_content("Roles")
+ find(:link, 'No one').click
+ end
+
click_on "Protect"
expect(ProtectedBranch.count).to eq(1)
@@ -70,6 +94,18 @@ shared_examples "protected branches > access control > CE" do
set_protected_branch_name('master')
+ find(".js-allowed-to-merge").click
+ within('.qa-allowed-to-merge-dropdown') do
+ expect(first("li")).to have_content("Roles")
+ find(:link, 'No one').click
+ end
+
+ find(".js-allowed-to-push").click
+ within('.qa-allowed-to-push-dropdown') do
+ expect(first("li")).to have_content("Roles")
+ find(:link, 'No one').click
+ end
+
click_on "Protect"
expect(ProtectedBranch.count).to eq(1)
diff --git a/spec/support/shared_examples/file_finder.rb b/spec/support/shared_examples/file_finder.rb
new file mode 100644
index 00000000000..ef144bdf61c
--- /dev/null
+++ b/spec/support/shared_examples/file_finder.rb
@@ -0,0 +1,21 @@
+shared_examples 'file finder' do
+ let(:query) { 'files' }
+ let(:search_results) { subject.find(query) }
+
+ it 'finds by name' do
+ filename, blob = search_results.find { |_, blob| blob.filename == expected_file_by_name }
+ expect(filename).to eq(expected_file_by_name)
+ expect(blob).to be_a(Gitlab::SearchResults::FoundBlob)
+ expect(blob.ref).to eq(subject.ref)
+ expect(blob.data).not_to be_empty
+ end
+
+ it 'finds by content' do
+ filename, blob = search_results.find { |_, blob| blob.filename == expected_file_by_content }
+
+ expect(filename).to eq(expected_file_by_content)
+ expect(blob).to be_a(Gitlab::SearchResults::FoundBlob)
+ expect(blob.ref).to eq(subject.ref)
+ expect(blob.data).not_to be_empty
+ end
+end
diff --git a/spec/support/shared_examples/helm_generated_script.rb b/spec/support/shared_examples/helm_generated_script.rb
index 56e86a87ab9..05d53a13fd3 100644
--- a/spec/support/shared_examples/helm_generated_script.rb
+++ b/spec/support/shared_examples/helm_generated_script.rb
@@ -6,7 +6,7 @@ shared_examples 'helm commands' do
ALPINE_VERSION=$(cat /etc/alpine-release | cut -d '.' -f 1,2)
echo http://mirror.clarkson.edu/alpine/v$ALPINE_VERSION/main >> /etc/apk/repositories
echo http://mirror1.hs-esslingen.de/pub/Mirrors/alpine/v$ALPINE_VERSION/main >> /etc/apk/repositories
- apk add -U ca-certificates openssl >/dev/null
+ apk add -U wget ca-certificates openssl >/dev/null
wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v2.7.0-linux-amd64.tar.gz | tar zxC /tmp >/dev/null
mv /tmp/linux-amd64/helm /usr/bin/
EOS
diff --git a/spec/support/shared_examples/models/atomic_internal_id_spec.rb b/spec/support/shared_examples/models/atomic_internal_id_spec.rb
index 6a6e13418a9..7ab1041d17c 100644
--- a/spec/support/shared_examples/models/atomic_internal_id_spec.rb
+++ b/spec/support/shared_examples/models/atomic_internal_id_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-shared_examples_for 'AtomicInternalId' do
+shared_examples_for 'AtomicInternalId' do |validate_presence: true|
describe '.has_internal_id' do
describe 'Module inclusion' do
subject { described_class }
@@ -9,14 +9,31 @@ shared_examples_for 'AtomicInternalId' do
end
describe 'Validation' do
- subject { instance }
-
before do
- allow(InternalId).to receive(:generate_next).and_return(nil)
+ allow_any_instance_of(described_class).to receive(:"ensure_#{scope}_#{internal_id_attribute}!")
+
+ instance.valid?
end
- it { is_expected.to validate_presence_of(internal_id_attribute) }
- it { is_expected.to validate_numericality_of(internal_id_attribute) }
+ context 'when presence validation is required' do
+ before do
+ skip unless validate_presence
+ end
+
+ it 'validates presence' do
+ expect(instance.errors[internal_id_attribute]).to include("can't be blank")
+ end
+ end
+
+ context 'when presence validation is not required' do
+ before do
+ skip if validate_presence
+ end
+
+ it 'does not validate presence' do
+ expect(instance.errors[internal_id_attribute]).to be_empty
+ end
+ end
end
describe 'Creating an instance' do
diff --git a/spec/support/shared_examples/models/members_notifications_shared_example.rb b/spec/support/shared_examples/models/members_notifications_shared_example.rb
index 76611e54306..ef5cea3f2a5 100644
--- a/spec/support/shared_examples/models/members_notifications_shared_example.rb
+++ b/spec/support/shared_examples/models/members_notifications_shared_example.rb
@@ -21,7 +21,7 @@ RSpec.shared_examples 'members notifications' do |entity_type|
it "calls NotificationService.update_#{entity_type}_member" do
expect(notification_service).to receive(:"update_#{entity_type}_member").with(member)
- member.update_attribute(:access_level, Member::MASTER)
+ member.update_attribute(:access_level, Member::MAINTAINER)
end
it "does not send an email when the access level has not changed" do
diff --git a/spec/support/shared_examples/notify_shared_examples.rb b/spec/support/shared_examples/notify_shared_examples.rb
index 43fdaddf545..d176d3fa425 100644
--- a/spec/support/shared_examples/notify_shared_examples.rb
+++ b/spec/support/shared_examples/notify_shared_examples.rb
@@ -212,7 +212,7 @@ shared_examples 'a note email' do
end
it 'contains the message from the note' do
- is_expected.to have_html_escaped_body_text note.note
+ is_expected.to have_body_text note.note
end
it 'does not contain note author' do
@@ -225,7 +225,7 @@ shared_examples 'a note email' do
end
it 'contains a link to note author' do
- is_expected.to have_html_escaped_body_text note.author_name
+ is_expected.to have_body_text note.author_name
end
end
end
diff --git a/spec/support/shared_examples/requests/api/merge_requests_list.rb b/spec/support/shared_examples/requests/api/merge_requests_list.rb
new file mode 100644
index 00000000000..1aed8ab0113
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/merge_requests_list.rb
@@ -0,0 +1,281 @@
+shared_examples 'merge requests list' do
+ context 'when unauthenticated' do
+ it 'returns merge requests for public projects' do
+ get api(endpoint_path)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ end
+ end
+
+ context 'when authenticated' do
+ it 'avoids N+1 queries' do
+ control = ActiveRecord::QueryRecorder.new do
+ get api(endpoint_path, user)
+ end
+
+ create(:merge_request, state: 'closed', milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: 'Test', created_at: base_time)
+
+ create(:merge_request, milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: 'Test', created_at: base_time)
+
+ expect do
+ get api(endpoint_path, user)
+ end.not_to exceed_query_limit(control)
+ end
+
+ it 'returns an array of all merge_requests' do
+ get api(endpoint_path, 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(4)
+ expect(json_response.last['title']).to eq(merge_request.title)
+ expect(json_response.last).to have_key('web_url')
+ expect(json_response.last['sha']).to eq(merge_request.diff_head_sha)
+ expect(json_response.last['merge_commit_sha']).to be_nil
+ expect(json_response.last['merge_commit_sha']).to eq(merge_request.merge_commit_sha)
+ expect(json_response.last['downvotes']).to eq(1)
+ expect(json_response.last['upvotes']).to eq(1)
+ expect(json_response.last['labels']).to eq([label2.title, label.title])
+ expect(json_response.first['title']).to eq(merge_request_merged.title)
+ expect(json_response.first['sha']).to eq(merge_request_merged.diff_head_sha)
+ expect(json_response.first['merge_commit_sha']).not_to be_nil
+ expect(json_response.first['merge_commit_sha']).to eq(merge_request_merged.merge_commit_sha)
+ end
+
+ it 'returns an array of all merge_requests using simple mode' do
+ path = endpoint_path + '?view=simple'
+
+ get api(path, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response.last.keys).to match_array(%w(id iid title web_url created_at description project_id state updated_at))
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(4)
+ expect(json_response.last['iid']).to eq(merge_request.iid)
+ expect(json_response.last['title']).to eq(merge_request.title)
+ expect(json_response.last).to have_key('web_url')
+ expect(json_response.first['iid']).to eq(merge_request_merged.iid)
+ expect(json_response.first['title']).to eq(merge_request_merged.title)
+ expect(json_response.first).to have_key('web_url')
+ end
+
+ it 'returns an array of all merge_requests' do
+ path = endpoint_path + '?state'
+
+ get api(path, 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(4)
+ expect(json_response.last['title']).to eq(merge_request.title)
+ end
+
+ it 'returns an array of open merge_requests' do
+ path = endpoint_path + '?state=opened'
+
+ get api(path, 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.last['title']).to eq(merge_request.title)
+ end
+
+ it 'returns an array of closed merge_requests' do
+ path = endpoint_path + '?state=closed'
+
+ get api(path, 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['title']).to eq(merge_request_closed.title)
+ end
+
+ it 'returns an array of merged merge_requests' do
+ path = endpoint_path + '?state=merged'
+
+ get api(path, 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['title']).to eq(merge_request_merged.title)
+ end
+
+ it 'matches V4 response schema' do
+ get api(endpoint_path, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/merge_requests')
+ end
+
+ it 'returns an empty array if no issue matches milestone' do
+ get api(endpoint_path, user), milestone: '1.0.0'
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an empty array if milestone does not exist' do
+ get api(endpoint_path, user), milestone: 'foo'
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an array of merge requests in given milestone' do
+ get api(endpoint_path, user), milestone: '0.9'
+
+ closed_issues = json_response.select { |mr| mr['id'] == merge_request_closed.id }
+ expect(closed_issues.length).to eq(1)
+ expect(closed_issues.first['title']).to eq merge_request_closed.title
+ end
+
+ it 'returns an array of merge requests matching state in milestone' do
+ get api(endpoint_path, user), milestone: '0.9', state: 'closed'
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(merge_request_closed.id)
+ end
+
+ it 'returns an array of labeled merge requests' do
+ path = endpoint_path + "?labels=#{label.title}"
+
+ get api(path, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['labels']).to eq([label2.title, label.title])
+ end
+
+ it 'returns an array of labeled merge requests where all labels match' do
+ path = endpoint_path + "?labels=#{label.title},foo,bar"
+
+ get api(path, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an empty array if no merge request matches labels' do
+ path = endpoint_path + '?labels=foo,bar'
+
+ get api(path, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an array of labeled merge requests that are merged for a milestone' do
+ bug_label = create(:label, title: 'bug', color: '#FFAABB', project: project)
+
+ mr1 = create(:merge_request, state: 'merged', source_project: project, target_project: project, milestone: milestone)
+ mr2 = create(:merge_request, state: 'merged', source_project: project, target_project: project, milestone: milestone1)
+ mr3 = create(:merge_request, state: 'closed', source_project: project, target_project: project, milestone: milestone1)
+ _mr = create(:merge_request, state: 'merged', source_project: project, target_project: project, milestone: milestone1)
+
+ create(:label_link, label: bug_label, target: mr1)
+ create(:label_link, label: bug_label, target: mr2)
+ create(:label_link, label: bug_label, target: mr3)
+
+ path = endpoint_path + "?labels=#{bug_label.title}&milestone=#{milestone1.title}&state=merged"
+
+ get api(path, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(mr2.id)
+ end
+
+ context 'with ordering' do
+ before do
+ @mr_later = mr_with_later_created_and_updated_at_time
+ @mr_earlier = mr_with_earlier_created_and_updated_at_time
+ end
+
+ it 'returns an array of merge_requests in ascending order' do
+ path = endpoint_path + '?sort=asc'
+
+ get api(path, 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(4)
+ response_dates = json_response.map { |merge_request| merge_request['created_at'] }
+ expect(response_dates).to eq(response_dates.sort)
+ end
+
+ it 'returns an array of merge_requests in descending order' do
+ path = endpoint_path + '?sort=desc'
+
+ get api(path, 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(4)
+ response_dates = json_response.map { |merge_request| merge_request['created_at'] }
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'returns an array of merge_requests ordered by updated_at' do
+ path = endpoint_path + '?order_by=updated_at'
+
+ get api(path, 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(4)
+ response_dates = json_response.map { |merge_request| merge_request['updated_at'] }
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'returns an array of merge_requests ordered by created_at' do
+ path = endpoint_path + '?order_by=created_at&sort=asc'
+
+ get api(path, 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(4)
+ response_dates = json_response.map { |merge_request| merge_request['created_at'] }
+ expect(response_dates).to eq(response_dates.sort)
+ end
+ end
+
+ context 'source_branch param' do
+ it 'returns merge requests with the given source branch' do
+ get api(endpoint_path, user), source_branch: merge_request_closed.source_branch, state: 'all'
+
+ expect_response_contain_exactly(merge_request_closed, merge_request_merged, merge_request_locked)
+ end
+ end
+
+ context 'target_branch param' do
+ it 'returns merge requests with the given target branch' do
+ get api(endpoint_path, user), target_branch: merge_request_closed.target_branch, state: 'all'
+
+ expect_response_contain_exactly(merge_request_closed, merge_request_merged, merge_request_locked)
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/graphql_shared_examples.rb b/spec/support/shared_examples/requests/graphql_shared_examples.rb
new file mode 100644
index 00000000000..fe7b7bc306f
--- /dev/null
+++ b/spec/support/shared_examples/requests/graphql_shared_examples.rb
@@ -0,0 +1,11 @@
+require 'spec_helper'
+
+shared_examples 'a working graphql query' do
+ include GraphqlHelpers
+
+ it 'returns a successful response', :aggregate_failures do
+ expect(response).to have_gitlab_http_status(:success)
+ expect(graphql_errors['errors']).to be_nil
+ expect(json_response.keys).to include('data')
+ end
+end
diff --git a/spec/support/shared_examples/serializers/note_entity_examples.rb b/spec/support/shared_examples/serializers/note_entity_examples.rb
index 9097c8e5513..ec208aba2a9 100644
--- a/spec/support/shared_examples/serializers/note_entity_examples.rb
+++ b/spec/support/shared_examples/serializers/note_entity_examples.rb
@@ -3,8 +3,8 @@ shared_examples 'note entity' do
context 'basic note' do
it 'exposes correct elements' do
- expect(subject).to include(:type, :author, :note, :note_html, :current_user,
- :discussion_id, :emoji_awardable, :award_emoji, :report_abuse_path, :attachment)
+ expect(subject).to include(:type, :author, :note, :note_html, :current_user, :discussion_id,
+ :emoji_awardable, :award_emoji, :report_abuse_path, :attachment, :noteable_note_url, :resolvable)
end
it 'does not expose elements for specific notes cases' do
diff --git a/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb b/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb
index 2228e872926..940c24c8d67 100644
--- a/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb
+++ b/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb
@@ -130,7 +130,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
context "event channels" do
it "uses the right channel for push event" do
- chat_service.update_attributes(push_channel: "random")
+ chat_service.update(push_channel: "random")
expect(Slack::Notifier).to receive(:new)
.with(webhook_url, channel: "random")
@@ -142,7 +142,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
end
it "uses the right channel for merge request event" do
- chat_service.update_attributes(merge_request_channel: "random")
+ chat_service.update(merge_request_channel: "random")
expect(Slack::Notifier).to receive(:new)
.with(webhook_url, channel: "random")
@@ -154,7 +154,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
end
it "uses the right channel for issue event" do
- chat_service.update_attributes(issue_channel: "random")
+ chat_service.update(issue_channel: "random")
expect(Slack::Notifier).to receive(:new)
.with(webhook_url, channel: "random")
@@ -169,7 +169,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
let(:issue_service_options) { { title: 'Secret', confidential: true } }
it "uses confidential issue channel" do
- chat_service.update_attributes(confidential_issue_channel: 'confidential')
+ chat_service.update(confidential_issue_channel: 'confidential')
expect(Slack::Notifier).to execute_with_options(channel: 'confidential')
@@ -177,7 +177,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
end
it 'falls back to issue channel' do
- chat_service.update_attributes(issue_channel: 'fallback_channel')
+ chat_service.update(issue_channel: 'fallback_channel')
expect(Slack::Notifier).to execute_with_options(channel: 'fallback_channel')
@@ -186,7 +186,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
end
it "uses the right channel for wiki event" do
- chat_service.update_attributes(wiki_page_channel: "random")
+ chat_service.update(wiki_page_channel: "random")
expect(Slack::Notifier).to receive(:new)
.with(webhook_url, channel: "random")
@@ -203,7 +203,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
end
it "uses the right channel" do
- chat_service.update_attributes(note_channel: "random")
+ chat_service.update(note_channel: "random")
note_data = Gitlab::DataBuilder::Note.build(issue_note, user)
@@ -222,7 +222,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
end
it "uses confidential channel" do
- chat_service.update_attributes(confidential_note_channel: "confidential")
+ chat_service.update(confidential_note_channel: "confidential")
note_data = Gitlab::DataBuilder::Note.build(issue_note, user)
@@ -232,7 +232,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
end
it 'falls back to note channel' do
- chat_service.update_attributes(note_channel: "fallback_channel")
+ chat_service.update(note_channel: "fallback_channel")
note_data = Gitlab::DataBuilder::Note.build(issue_note, user)
@@ -245,6 +245,70 @@ RSpec.shared_examples 'slack or mattermost notifications' do
end
end
+ describe 'Push events' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository, creator: user) }
+
+ before do
+ allow(chat_service).to receive_messages(
+ project: project,
+ service_hook: true,
+ webhook: webhook_url
+ )
+
+ WebMock.stub_request(:post, webhook_url)
+ end
+
+ context 'only notify for the default branch' do
+ context 'when enabled' do
+ before do
+ chat_service.notify_only_default_branch = true
+ end
+
+ it 'does not notify push events if they are not for the default branch' do
+ ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}test"
+ push_sample_data = Gitlab::DataBuilder::Push.build(project, user, nil, nil, ref, [])
+
+ chat_service.execute(push_sample_data)
+
+ expect(WebMock).not_to have_requested(:post, webhook_url)
+ end
+
+ it 'notifies about push events for the default branch' do
+ push_sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
+
+ chat_service.execute(push_sample_data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+
+ it 'still notifies about pushed tags' do
+ ref = "#{Gitlab::Git::TAG_REF_PREFIX}test"
+ push_sample_data = Gitlab::DataBuilder::Push.build(project, user, nil, nil, ref, [])
+
+ chat_service.execute(push_sample_data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+ end
+
+ context 'when disabled' do
+ before do
+ chat_service.notify_only_default_branch = false
+ end
+
+ it 'notifies about all push events' do
+ ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}test"
+ push_sample_data = Gitlab::DataBuilder::Push.build(project, user, nil, nil, ref, [])
+
+ chat_service.execute(push_sample_data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+ end
+ end
+ end
+
describe "Note events" do
let(:user) { create(:user) }
let(:project) { create(:project, :repository, creator: user) }
@@ -394,23 +458,6 @@ RSpec.shared_examples 'slack or mattermost notifications' do
expect(result).to be_falsy
end
-
- it 'does not notify push events if they are not for the default branch' do
- ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}test"
- push_sample_data = Gitlab::DataBuilder::Push.build(project, user, nil, nil, ref, [])
-
- chat_service.execute(push_sample_data)
-
- expect(WebMock).not_to have_requested(:post, webhook_url)
- end
-
- it 'notifies about push events for the default branch' do
- push_sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
-
- chat_service.execute(push_sample_data)
-
- expect(WebMock).to have_requested(:post, webhook_url).once
- end
end
context 'when disabled' do
diff --git a/spec/support/shared_examples/throttled_touch.rb b/spec/support/shared_examples/throttled_touch.rb
index 4a25bb9b750..eba990d4037 100644
--- a/spec/support/shared_examples/throttled_touch.rb
+++ b/spec/support/shared_examples/throttled_touch.rb
@@ -3,7 +3,7 @@ shared_examples_for 'throttled touch' do
it 'updates the updated_at timestamp' do
Timecop.freeze do
subject.touch
- expect(subject.updated_at).to eq(Time.zone.now)
+ expect(subject.updated_at).to be_like_time(Time.zone.now)
end
end
@@ -14,7 +14,7 @@ shared_examples_for 'throttled touch' do
Timecop.freeze(first_updated_at) { subject.touch }
Timecop.freeze(second_updated_at) { subject.touch }
- expect(subject.updated_at).to eq(first_updated_at)
+ expect(subject.updated_at).to be_like_time(first_updated_at)
end
end
end
diff --git a/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb b/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb
index 6352f1527cd..1bd176280c5 100644
--- a/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb
+++ b/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb
@@ -76,26 +76,22 @@ shared_examples "migrates" do |to_store:, from_store: nil|
end
context 'when migrate! is occupied by another process' do
- let(:exclusive_lease_key) { "object_storage_migrate:#{subject.model.class}:#{subject.model.id}" }
+ include ExclusiveLeaseHelpers
before do
- @uuid = Gitlab::ExclusiveLease.new(exclusive_lease_key, timeout: 1.hour.to_i).try_obtain
+ stub_exclusive_lease_taken(subject.exclusive_lease_key, timeout: 1.hour.to_i)
end
it 'does not execute migrate!' do
expect(subject).not_to receive(:unsafe_migrate!)
- expect { migrate(to) }.to raise_error('exclusive lease already taken')
+ expect { migrate(to) }.to raise_error(ObjectStorage::ExclusiveLeaseTaken)
end
it 'does not execute use_file' do
expect(subject).not_to receive(:unsafe_use_file)
- expect { subject.use_file }.to raise_error('exclusive lease already taken')
- end
-
- after do
- Gitlab::ExclusiveLease.cancel(exclusive_lease_key, @uuid)
+ expect { subject.use_file }.to raise_error(ObjectStorage::ExclusiveLeaseTaken)
end
end
diff --git a/spec/support/shared_examples/url_validator_examples.rb b/spec/support/shared_examples/url_validator_examples.rb
new file mode 100644
index 00000000000..b4757a70984
--- /dev/null
+++ b/spec/support/shared_examples/url_validator_examples.rb
@@ -0,0 +1,42 @@
+RSpec.shared_examples 'url validator examples' do |protocols|
+ let(:validator) { described_class.new(attributes: [:link_url], **options) }
+ let!(:badge) { build(:badge, link_url: 'http://www.example.com') }
+
+ subject { validator.validate_each(badge, :link_url, badge.link_url) }
+
+ describe '#validates_each' do
+ context 'with no options' do
+ let(:options) { {} }
+
+ it "allows #{protocols.join(',')} protocols by default" do
+ expect(validator.send(:default_options)[:protocols]).to eq protocols
+ end
+
+ it 'checks that the url structure is valid' do
+ badge.link_url = "#{badge.link_url}:invalid_port"
+
+ subject
+
+ expect(badge.errors.empty?).to be false
+ end
+ end
+
+ context 'with protocols' do
+ let(:options) { { protocols: %w[http] } }
+
+ it 'allows urls with the defined protocols' do
+ subject
+
+ expect(badge.errors.empty?).to be true
+ end
+
+ it 'add error if the url protocol does not match the selected ones' do
+ badge.link_url = 'https://www.example.com'
+
+ subject
+
+ expect(badge.errors.empty?).to be false
+ end
+ end
+ end
+end
diff --git a/spec/support/shoulda/matchers/rails_shim.rb b/spec/support/shoulda/matchers/rails_shim.rb
new file mode 100644
index 00000000000..8d70598beb5
--- /dev/null
+++ b/spec/support/shoulda/matchers/rails_shim.rb
@@ -0,0 +1,27 @@
+# monkey patch which fixes serialization matcher in Rails 5
+# https://github.com/thoughtbot/shoulda-matchers/issues/913
+# This can be removed when a new version of shoulda-matchers
+# is released
+module Shoulda
+ module Matchers
+ class RailsShim
+ def self.serialized_attributes_for(model)
+ if defined?(::ActiveRecord::Type::Serialized)
+ # Rails 5+
+ serialized_columns = model.columns.select do |column|
+ model.type_for_attribute(column.name).is_a?(
+ ::ActiveRecord::Type::Serialized
+ )
+ end
+
+ serialized_columns.inject({}) do |hash, column| # rubocop:disable Style/EachWithObject
+ hash[column.name.to_s] = model.type_for_attribute(column.name).coder
+ hash
+ end
+ else
+ model.serialized_attributes
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/trace/trace_helpers.rb b/spec/support/trace/trace_helpers.rb
new file mode 100644
index 00000000000..c7802bbcb94
--- /dev/null
+++ b/spec/support/trace/trace_helpers.rb
@@ -0,0 +1,27 @@
+module TraceHelpers
+ def create_legacy_trace(build, content)
+ File.open(legacy_trace_path(build), 'wb') { |stream| stream.write(content) }
+ end
+
+ def create_legacy_trace_in_db(build, content)
+ build.update_column(:trace, content)
+ end
+
+ def legacy_trace_path(build)
+ legacy_trace_dir = File.join(Settings.gitlab_ci.builds_path,
+ build.created_at.utc.strftime("%Y_%m"),
+ build.project_id.to_s)
+
+ FileUtils.mkdir_p(legacy_trace_dir)
+
+ File.join(legacy_trace_dir, "#{build.id}.log")
+ end
+
+ def archived_trace_path(job_artifact)
+ disk_hash = Digest::SHA2.hexdigest(job_artifact.project_id.to_s)
+ creation_date = job_artifact.created_at.utc.strftime('%Y_%m_%d')
+
+ File.join(Gitlab.config.artifacts.path, disk_hash[0..1], disk_hash[2..3], disk_hash,
+ creation_date, job_artifact.job_id.to_s, job_artifact.id.to_s, 'job.log')
+ end
+end
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index a2e5642a72c..93a436cb2b5 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -101,7 +101,9 @@ describe 'gitlab:app namespace rake task' do
before do
stub_env('SKIP', 'db')
- path = File.join(project.repository.path_to_repo, 'custom_hooks')
+ path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ File.join(project.repository.path_to_repo, 'custom_hooks')
+ end
FileUtils.mkdir_p(path)
FileUtils.touch(File.join(path, "dummy.txt"))
end
@@ -122,7 +124,20 @@ describe 'gitlab:app namespace rake task' do
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")
+ repo_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ project.repository.path
+ end
+ expect(Dir.entries(File.join(repo_path, 'custom_hooks'))).to include("dummy.txt")
+ end
+ end
+
+ context 'specific backup tasks' do
+ let(:task_list) { %w(db repo uploads builds artifacts pages lfs registry) }
+
+ it 'prints a progress message to stdout' do
+ task_list.each do |task|
+ expect { run_rake_task("gitlab:backup:#{task}:create") }.to output(/Dumping /).to_stdout
+ end
end
end
end
@@ -233,10 +248,12 @@ describe 'gitlab:app namespace rake task' do
FileUtils.mkdir_p(b_storage_dir)
# Even when overriding the storage, we have to move it there, so it exists
- FileUtils.mv(
- File.join(Settings.absolute(storages['default'].legacy_disk_path), project_b.repository.disk_path + '.git'),
- Rails.root.join(storages['test_second_storage'].legacy_disk_path, project_b.repository.disk_path + '.git')
- )
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ FileUtils.mv(
+ File.join(Settings.absolute(storages['default'].legacy_disk_path), project_b.repository.disk_path + '.git'),
+ Rails.root.join(storages['test_second_storage'].legacy_disk_path, project_b.repository.disk_path + '.git')
+ )
+ end
expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout
diff --git a/spec/tasks/gitlab/db_rake_spec.rb b/spec/tasks/gitlab/db_rake_spec.rb
index fc52c04e78d..b81aea23306 100644
--- a/spec/tasks/gitlab/db_rake_spec.rb
+++ b/spec/tasks/gitlab/db_rake_spec.rb
@@ -20,7 +20,7 @@ describe 'gitlab:db namespace rake task' do
describe 'configure' do
it 'invokes db:migrate when schema has already been loaded' do
- allow(ActiveRecord::Base.connection).to receive(:tables).and_return(['default'])
+ allow(ActiveRecord::Base.connection).to receive(:tables).and_return(%w[table1 table2])
expect(Rake::Task['db:migrate']).to receive(:invoke)
expect(Rake::Task['db:schema:load']).not_to receive(:invoke)
expect(Rake::Task['db:seed_fu']).not_to receive(:invoke)
@@ -35,6 +35,14 @@ describe 'gitlab:db namespace rake task' do
expect { run_rake_task('gitlab:db:configure') }.not_to raise_error
end
+ it 'invokes db:shema:load and db:seed_fu when there is only a single table present' do
+ allow(ActiveRecord::Base.connection).to receive(:tables).and_return(['default'])
+ expect(Rake::Task['db:schema:load']).to receive(:invoke)
+ expect(Rake::Task['db:seed_fu']).to receive(:invoke)
+ expect(Rake::Task['db:migrate']).not_to receive(:invoke)
+ expect { run_rake_task('gitlab:db:configure') }.not_to raise_error
+ end
+
it 'does not invoke any other rake tasks during an error' do
allow(ActiveRecord::Base).to receive(:connection).and_raise(RuntimeError, 'error')
expect(Rake::Task['db:migrate']).not_to receive(:invoke)
diff --git a/spec/tasks/gitlab/git_rake_spec.rb b/spec/tasks/gitlab/git_rake_spec.rb
index 1efaecc63a5..d0263ad9a37 100644
--- a/spec/tasks/gitlab/git_rake_spec.rb
+++ b/spec/tasks/gitlab/git_rake_spec.rb
@@ -1,15 +1,20 @@
require 'rake_helper'
describe 'gitlab:git rake tasks' do
+ let(:base_path) { 'tmp/tests/default_storage' }
+
before(:all) do
@default_storage_hash = Gitlab.config.repositories.storages.default.to_h
end
before do
Rake.application.rake_require 'tasks/gitlab/git'
- storages = { 'default' => Gitlab::GitalyClient::StorageSettings.new(@default_storage_hash.merge('path' => 'tmp/tests/default_storage')) }
+ storages = { 'default' => Gitlab::GitalyClient::StorageSettings.new(@default_storage_hash.merge('path' => base_path)) }
+
+ path = Settings.absolute("#{base_path}/@hashed/1/2/test.git")
+ FileUtils.mkdir_p(path)
+ Gitlab::Popen.popen(%W[git -C #{path} init --bare])
- FileUtils.mkdir_p(Settings.absolute('tmp/tests/default_storage/@hashed/1/2/test.git'))
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
allow_any_instance_of(String).to receive(:color) { |string, _color| string }
@@ -17,7 +22,7 @@ describe 'gitlab:git rake tasks' do
end
after do
- FileUtils.rm_rf(Settings.absolute('tmp/tests/default_storage'))
+ FileUtils.rm_rf(Settings.absolute(base_path))
end
describe 'fsck' do
@@ -26,14 +31,14 @@ describe 'gitlab:git rake tasks' do
end
it 'errors out about config.lock issues' do
- FileUtils.touch(Settings.absolute('tmp/tests/default_storage/@hashed/1/2/test.git/config.lock'))
+ FileUtils.touch(Settings.absolute("#{base_path}/@hashed/1/2/test.git/config.lock"))
expect { run_rake_task('gitlab:git:fsck') }.to output(/file exists\? ... yes/).to_stdout
end
it 'errors out about ref lock issues' do
- FileUtils.mkdir_p(Settings.absolute('tmp/tests/default_storage/@hashed/1/2/test.git/refs/heads'))
- FileUtils.touch(Settings.absolute('tmp/tests/default_storage/@hashed/1/2/test.git/refs/heads/blah.lock'))
+ FileUtils.mkdir_p(Settings.absolute("#{base_path}/@hashed/1/2/test.git/refs/heads"))
+ FileUtils.touch(Settings.absolute("#{base_path}/@hashed/1/2/test.git/refs/heads/blah.lock"))
expect { run_rake_task('gitlab:git:fsck') }.to output(/Ref lock files exist:/).to_stdout
end
diff --git a/spec/tasks/gitlab/gitaly_rake_spec.rb b/spec/tasks/gitlab/gitaly_rake_spec.rb
index 1e507c0236e..4545226d78c 100644
--- a/spec/tasks/gitlab/gitaly_rake_spec.rb
+++ b/spec/tasks/gitlab/gitaly_rake_spec.rb
@@ -134,7 +134,9 @@ describe 'gitlab:gitaly namespace rake task' do
parsed_output = TomlRB.parse(expected_output)
config.each do |name, params|
- expect(parsed_output['storage']).to include({ 'name' => name, 'path' => params.legacy_disk_path })
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ expect(parsed_output['storage']).to include({ 'name' => name, 'path' => params.legacy_disk_path })
+ end
end
end
end
diff --git a/spec/tasks/gitlab/shell_rake_spec.rb b/spec/tasks/gitlab/shell_rake_spec.rb
index 4a756c5742d..0ed5d3e27b9 100644
--- a/spec/tasks/gitlab/shell_rake_spec.rb
+++ b/spec/tasks/gitlab/shell_rake_spec.rb
@@ -7,11 +7,17 @@ describe 'gitlab:shell rake tasks' do
stub_warn_user_is_not_gitlab
end
+ after do
+ TestEnv.create_fake_git_hooks
+ end
+
describe 'install task' do
it 'invokes create_hooks task' do
expect(Rake::Task['gitlab:shell:create_hooks']).to receive(:invoke)
- storages = Gitlab.config.repositories.storages.values.map(&:legacy_disk_path)
+ storages = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ Gitlab.config.repositories.storages.values.map(&:legacy_disk_path)
+ end
expect(Kernel).to receive(:system).with('bin/install', *storages).and_call_original
expect(Kernel).to receive(:system).with('bin/compile').and_call_original
diff --git a/spec/tasks/gitlab/storage_rake_spec.rb b/spec/tasks/gitlab/storage_rake_spec.rb
index f59792c3d36..233076ad6fa 100644
--- a/spec/tasks/gitlab/storage_rake_spec.rb
+++ b/spec/tasks/gitlab/storage_rake_spec.rb
@@ -1,23 +1,61 @@
require 'rake_helper'
-describe 'gitlab:storage rake tasks' do
+describe 'rake gitlab:storage:*' do
before do
Rake.application.rake_require 'tasks/gitlab/storage'
stub_warn_user_is_not_gitlab
end
- describe 'migrate_to_hashed rake task' do
+ shared_examples "rake listing entities" do |entity_name, storage_type|
+ context 'limiting to 2' do
+ before do
+ stub_env('LIMIT' => 2)
+ end
+
+ it "lists 2 out of 3 #{storage_type.downcase} #{entity_name}" do
+ create_collection
+
+ expect { run_rake_task(task) }.to output(/Found 3 #{entity_name} using #{storage_type} Storage.*Displaying first 2 #{entity_name}/m).to_stdout
+ end
+ end
+
+ context "without any #{storage_type.downcase} #{entity_name.singularize}" do
+ it 'displays message for empty results' do
+ expect { run_rake_task(task) }.to output(/Found 0 #{entity_name} using #{storage_type} Storage/).to_stdout
+ end
+ end
+ end
+
+ shared_examples "rake entities summary" do |entity_name, storage_type|
+ context "with existing 3 #{storage_type.downcase} #{entity_name}" do
+ it "reports 3 #{storage_type.downcase} #{entity_name}" do
+ create_collection
+
+ expect { run_rake_task(task) }.to output(/Found 3 #{entity_name} using #{storage_type} Storage/).to_stdout
+ end
+ end
+
+ context "without any #{storage_type.downcase} #{entity_name.singularize}" do
+ it 'displays message for empty results' do
+ expect { run_rake_task(task) }.to output(/Found 0 #{entity_name} using #{storage_type} Storage/).to_stdout
+ end
+ end
+ end
+
+ describe 'gitlab:storage:migrate_to_hashed' do
+ let(:task) { 'gitlab:storage:migrate_to_hashed' }
+
context '0 legacy projects' do
it 'does nothing' do
expect(StorageMigratorWorker).not_to receive(:perform_async)
- run_rake_task('gitlab:storage:migrate_to_hashed')
+ run_rake_task(task)
end
end
- context '5 legacy projects' do
- let(:projects) { create_list(:project, 5, storage_version: 0) }
+ context '3 legacy projects' do
+ let(:projects) { create_list(:project, 3, :legacy_storage) }
context 'in batches of 1' do
before do
@@ -29,7 +67,7 @@ describe 'gitlab:storage rake tasks' do
expect(StorageMigratorWorker).to receive(:perform_async).with(project.id, project.id)
end
- run_rake_task('gitlab:storage:migrate_to_hashed')
+ run_rake_task(task)
end
end
@@ -44,9 +82,94 @@ describe 'gitlab:storage rake tasks' do
expect(StorageMigratorWorker).to receive(:perform_async).with(first, last)
end
- run_rake_task('gitlab:storage:migrate_to_hashed')
+ run_rake_task(task)
end
end
end
+
+ context 'with same id in range' do
+ it 'displays message when project cant be found' do
+ stub_env('ID_FROM', 99999)
+ stub_env('ID_TO', 99999)
+
+ expect { run_rake_task(task) }.to output(/There are no projects requiring storage migration with ID=99999/).to_stdout
+ end
+
+ it 'displays a message when project exists but its already migrated' do
+ project = create(:project)
+ stub_env('ID_FROM', project.id)
+ stub_env('ID_TO', project.id)
+
+ expect { run_rake_task(task) }.to output(/There are no projects requiring storage migration with ID=#{project.id}/).to_stdout
+ end
+
+ it 'enqueues migration when project can be found' do
+ project = create(:project, :legacy_storage)
+ stub_env('ID_FROM', project.id)
+ stub_env('ID_TO', project.id)
+
+ expect { run_rake_task(task) }.to output(/Enqueueing storage migration .* \(ID=#{project.id}\)/).to_stdout
+ end
+ end
+ end
+
+ describe 'gitlab:storage:legacy_projects' do
+ it_behaves_like 'rake entities summary', 'projects', 'Legacy' do
+ let(:task) { 'gitlab:storage:legacy_projects' }
+ let(:create_collection) { create_list(:project, 3, :legacy_storage) }
+ end
+ end
+
+ describe 'gitlab:storage:list_legacy_projects' do
+ it_behaves_like 'rake listing entities', 'projects', 'Legacy' do
+ let(:task) { 'gitlab:storage:list_legacy_projects' }
+ let(:create_collection) { create_list(:project, 3, :legacy_storage) }
+ end
+ end
+
+ describe 'gitlab:storage:hashed_projects' do
+ it_behaves_like 'rake entities summary', 'projects', 'Hashed' do
+ let(:task) { 'gitlab:storage:hashed_projects' }
+ let(:create_collection) { create_list(:project, 3, storage_version: 1) }
+ end
+ end
+
+ describe 'gitlab:storage:list_hashed_projects' do
+ it_behaves_like 'rake listing entities', 'projects', 'Hashed' do
+ let(:task) { 'gitlab:storage:list_hashed_projects' }
+ let(:create_collection) { create_list(:project, 3, storage_version: 1) }
+ end
+ end
+
+ describe 'gitlab:storage:legacy_attachments' do
+ it_behaves_like 'rake entities summary', 'attachments', 'Legacy' do
+ let(:task) { 'gitlab:storage:legacy_attachments' }
+ let(:project) { create(:project, storage_version: 1) }
+ let(:create_collection) { create_list(:upload, 3, model: project) }
+ end
+ end
+
+ describe 'gitlab:storage:list_legacy_attachments' do
+ it_behaves_like 'rake listing entities', 'attachments', 'Legacy' do
+ let(:task) { 'gitlab:storage:list_legacy_attachments' }
+ let(:project) { create(:project, storage_version: 1) }
+ let(:create_collection) { create_list(:upload, 3, model: project) }
+ end
+ end
+
+ describe 'gitlab:storage:hashed_attachments' do
+ it_behaves_like 'rake entities summary', 'attachments', 'Hashed' do
+ let(:task) { 'gitlab:storage:hashed_attachments' }
+ let(:project) { create(:project) }
+ let(:create_collection) { create_list(:upload, 3, model: project) }
+ end
+ end
+
+ describe 'gitlab:storage:list_hashed_attachments' do
+ it_behaves_like 'rake listing entities', 'attachments', 'Hashed' do
+ let(:task) { 'gitlab:storage:list_hashed_attachments' }
+ let(:project) { create(:project) }
+ let(:create_collection) { create_list(:upload, 3, model: project) }
+ end
end
end
diff --git a/spec/tasks/tokens_spec.rb b/spec/tasks/tokens_spec.rb
index 51f7a536cbb..555a58e9aa1 100644
--- a/spec/tasks/tokens_spec.rb
+++ b/spec/tasks/tokens_spec.rb
@@ -13,9 +13,9 @@ describe 'tokens rake tasks' do
end
end
- describe 'reset_all_rss task' do
+ describe 'reset_all_feed task' do
it 'invokes create_hooks task' do
- expect { run_rake_task('tokens:reset_all_rss') }.to change { user.reload.rss_token }
+ expect { run_rake_task('tokens:reset_all_feed') }.to change { user.reload.feed_token }
end
end
end
diff --git a/spec/uploaders/attachment_uploader_spec.rb b/spec/uploaders/attachment_uploader_spec.rb
index d302c14efb9..a9415854d25 100644
--- a/spec/uploaders/attachment_uploader_spec.rb
+++ b/spec/uploaders/attachment_uploader_spec.rb
@@ -26,7 +26,7 @@ describe AttachmentUploader do
describe "#migrate!" do
before do
- uploader.store!(fixture_file_upload(Rails.root.join('spec/fixtures/doc_sample.txt')))
+ uploader.store!(fixture_file_upload(File.join('spec/fixtures/doc_sample.txt')))
stub_uploads_object_storage
end
diff --git a/spec/uploaders/file_mover_spec.rb b/spec/uploaders/file_mover_spec.rb
index 68b7e24776d..de29d0c943f 100644
--- a/spec/uploaders/file_mover_spec.rb
+++ b/spec/uploaders/file_mover_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe FileMover do
let(:filename) { 'banana_sample.gif' }
- let(:file) { fixture_file_upload(Rails.root.join('spec', 'fixtures', filename)) }
+ let(:file) { fixture_file_upload(File.join('spec', 'fixtures', filename)) }
let(:temp_file_path) { File.join('uploads/-/system/temp', 'secret55', filename) }
let(:temp_description) do
diff --git a/spec/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb
index db2810bbe1d..7ba28b4fc1f 100644
--- a/spec/uploaders/file_uploader_spec.rb
+++ b/spec/uploaders/file_uploader_spec.rb
@@ -80,6 +80,50 @@ describe FileUploader do
end
end
+ describe 'copy_to' do
+ shared_examples 'returns a valid uploader' do
+ describe 'returned uploader' do
+ let(:new_project) { create(:project) }
+ let(:moved) { described_class.copy_to(subject, new_project) }
+
+ it 'generates a new secret' do
+ expect(subject).to be
+ expect(described_class).to receive(:generate_secret).once.and_call_original
+ expect(moved).to be
+ end
+
+ it 'create new upload' do
+ expect(moved.upload).not_to eq(subject.upload)
+ end
+
+ it 'copies the file' do
+ expect(subject.file).to exist
+ expect(moved.file).to exist
+ expect(subject.file).not_to eq(moved.file)
+ expect(subject.object_store).to eq(moved.object_store)
+ end
+ end
+ end
+
+ context 'files are stored locally' do
+ before do
+ subject.store!(fixture_file_upload('spec/fixtures/dk.png'))
+ end
+
+ include_examples 'returns a valid uploader'
+ end
+
+ context 'files are stored remotely' do
+ before do
+ stub_uploads_object_storage
+ subject.store!(fixture_file_upload('spec/fixtures/dk.png'))
+ subject.migrate!(ObjectStorage::Store::REMOTE)
+ end
+
+ include_examples 'returns a valid uploader'
+ end
+ end
+
describe '#secret' do
it 'generates a secret if none is provided' do
expect(described_class).to receive(:generate_secret).and_return('secret')
@@ -89,7 +133,7 @@ describe FileUploader do
describe "#migrate!" do
before do
- uploader.store!(fixture_file_upload(Rails.root.join('spec/fixtures/dk.png')))
+ uploader.store!(fixture_file_upload('spec/fixtures/dk.png'))
stub_uploads_object_storage
end
diff --git a/spec/uploaders/gitlab_uploader_spec.rb b/spec/uploaders/gitlab_uploader_spec.rb
index 4fba122cce1..44718ed1212 100644
--- a/spec/uploaders/gitlab_uploader_spec.rb
+++ b/spec/uploaders/gitlab_uploader_spec.rb
@@ -62,10 +62,72 @@ describe GitlabUploader do
expect(FileUtils).to receive(:mv).with(anything, /^#{subject.work_dir}/).and_call_original
expect(FileUtils).to receive(:mv).with(/^#{subject.work_dir}/, /#{subject.cache_dir}/).and_call_original
- fixture = Rails.root.join('spec', 'fixtures', 'rails_sample.jpg')
+ fixture = File.join('spec', 'fixtures', 'rails_sample.jpg')
subject.cache!(fixture_file_upload(fixture))
expect(subject.file.path).to match(/#{subject.cache_dir}/)
end
end
+
+ describe '#open' do
+ context 'when trace is stored in File storage' do
+ context 'when file exists' do
+ let(:file) do
+ fixture_file_upload('spec/fixtures/trace/sample_trace', 'text/plain')
+ end
+
+ before do
+ subject.store!(file)
+ end
+
+ it 'returns io stream' do
+ expect(subject.open).to be_a(IO)
+ end
+
+ it 'when passing block it yields' do
+ expect { |b| subject.open(&b) }.to yield_control
+ end
+ end
+
+ context 'when file does not exist' do
+ it 'returns nil' do
+ expect(subject.open).to be_nil
+ end
+
+ it 'when passing block it does not yield' do
+ expect { |b| subject.open(&b) }.not_to yield_control
+ end
+ end
+ end
+
+ context 'when trace is stored in Object storage' do
+ before do
+ allow(subject).to receive(:file_storage?) { false }
+ end
+
+ context 'when file exists' do
+ before do
+ allow(subject).to receive(:url) { 'http://object_storage.com/trace' }
+ end
+
+ it 'returns http io stream' do
+ expect(subject.open).to be_a(Gitlab::HttpIO)
+ end
+
+ it 'when passing block it yields' do
+ expect { |b| subject.open(&b) }.to yield_control.once
+ end
+ end
+
+ context 'when file does not exist' do
+ it 'returns nil' do
+ expect(subject.open).to be_nil
+ end
+
+ it 'when passing block it does not yield' do
+ expect { |b| subject.open(&b) }.not_to yield_control
+ end
+ end
+ end
+ end
end
diff --git a/spec/uploaders/import_export_uploader_spec.rb b/spec/uploaders/import_export_uploader_spec.rb
new file mode 100644
index 00000000000..51b173b682d
--- /dev/null
+++ b/spec/uploaders/import_export_uploader_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe ImportExportUploader do
+ let(:model) { build_stubbed(:import_export_upload) }
+ let(:upload) { create(:upload, model: model) }
+
+ subject { described_class.new(model, :import_file) }
+
+ context "object_store is REMOTE" do
+ before do
+ stub_uploads_object_storage
+ end
+
+ include_context 'with storage', described_class::Store::REMOTE
+
+ it_behaves_like 'builds correct paths',
+ store_dir: %r[import_export_upload/import_file/],
+ upload_path: %r[import_export_upload/import_file/]
+ end
+end
diff --git a/spec/uploaders/job_artifact_uploader_spec.rb b/spec/uploaders/job_artifact_uploader_spec.rb
index 42036d67f3d..3ad5fe7e3b3 100644
--- a/spec/uploaders/job_artifact_uploader_spec.rb
+++ b/spec/uploaders/job_artifact_uploader_spec.rb
@@ -23,48 +23,9 @@ describe JobArtifactUploader do
store_dir: %r[\h{2}/\h{2}/\h{64}/\d{4}_\d{1,2}_\d{1,2}/\d+/\d+\z]
end
- describe '#open' do
- subject { uploader.open }
-
- context 'when trace is stored in File storage' do
- context 'when file exists' do
- let(:file) do
- fixture_file_upload(
- Rails.root.join('spec/fixtures/trace/sample_trace'), 'text/plain')
- end
-
- before do
- uploader.store!(file)
- end
-
- it 'returns io stream' do
- is_expected.to be_a(IO)
- end
- end
-
- context 'when file does not exist' do
- it 'returns nil' do
- is_expected.to be_nil
- end
- end
- end
-
- context 'when trace is stored in Object storage' do
- before do
- allow(uploader).to receive(:file_storage?) { false }
- allow(uploader).to receive(:url) { 'http://object_storage.com/trace' }
- end
-
- it 'returns http io stream' do
- is_expected.to be_a(Gitlab::Ci::Trace::HttpIO)
- end
- end
- end
-
context 'file is stored in valid local_path' do
let(:file) do
- fixture_file_upload(
- Rails.root.join('spec/fixtures/ci_build_artifacts.zip'), 'application/zip')
+ fixture_file_upload('spec/fixtures/ci_build_artifacts.zip', 'application/zip')
end
before do
@@ -81,7 +42,7 @@ describe JobArtifactUploader do
describe "#migrate!" do
before do
- uploader.store!(fixture_file_upload(Rails.root.join('spec/fixtures/trace/sample_trace')))
+ uploader.store!(fixture_file_upload('spec/fixtures/trace/sample_trace'))
stub_artifacts_object_storage
end
diff --git a/spec/uploaders/legacy_artifact_uploader_spec.rb b/spec/uploaders/legacy_artifact_uploader_spec.rb
index eeb6fd90c9d..0589563b502 100644
--- a/spec/uploaders/legacy_artifact_uploader_spec.rb
+++ b/spec/uploaders/legacy_artifact_uploader_spec.rb
@@ -44,8 +44,7 @@ describe LegacyArtifactUploader do
context 'file is stored in valid path' do
let(:file) do
- fixture_file_upload(
- Rails.root.join('spec/fixtures/ci_build_artifacts.zip'), 'application/zip')
+ fixture_file_upload('spec/fixtures/ci_build_artifacts.zip', 'application/zip')
end
before do
diff --git a/spec/uploaders/namespace_file_uploader_spec.rb b/spec/uploaders/namespace_file_uploader_spec.rb
index a8ba01d70b8..71fe2c353c0 100644
--- a/spec/uploaders/namespace_file_uploader_spec.rb
+++ b/spec/uploaders/namespace_file_uploader_spec.rb
@@ -28,7 +28,7 @@ describe NamespaceFileUploader do
describe "#migrate!" do
before do
- uploader.store!(fixture_file_upload(Rails.root.join('spec/fixtures/doc_sample.txt')))
+ uploader.store!(fixture_file_upload(File.join('spec/fixtures/doc_sample.txt')))
stub_uploads_object_storage
end
diff --git a/spec/uploaders/object_storage_spec.rb b/spec/uploaders/object_storage_spec.rb
index e7277b337f6..7e673681c31 100644
--- a/spec/uploaders/object_storage_spec.rb
+++ b/spec/uploaders/object_storage_spec.rb
@@ -191,6 +191,18 @@ describe ObjectStorage do
it "calls a cache path" do
expect { |b| uploader.use_file(&b) }.to yield_with_args(%r[tmp/cache])
end
+
+ it "cleans up the cached file" do
+ cached_path = ''
+
+ uploader.use_file do |path|
+ cached_path = path
+
+ expect(File.exist?(cached_path)).to be_truthy
+ end
+
+ expect(File.exist?(cached_path)).to be_falsey
+ end
end
end
@@ -321,7 +333,7 @@ describe ObjectStorage do
when_file_is_in_use do
expect(uploader).not_to receive(:unsafe_migrate!)
- expect { uploader.migrate!(described_class::Store::REMOTE) }.to raise_error('exclusive lease already taken')
+ expect { uploader.migrate!(described_class::Store::REMOTE) }.to raise_error(ObjectStorage::ExclusiveLeaseTaken)
end
end
@@ -329,7 +341,19 @@ describe ObjectStorage do
when_file_is_in_use do
expect(uploader).not_to receive(:unsafe_use_file)
- expect { uploader.use_file }.to raise_error('exclusive lease already taken')
+ expect { uploader.use_file }.to raise_error(ObjectStorage::ExclusiveLeaseTaken)
+ end
+ end
+
+ it 'can still migrate other files of the same model' do
+ uploader2 = uploader_class.new(object, :file)
+ uploader2.upload = create(:upload)
+ uploader.upload = create(:upload)
+
+ when_file_is_in_use do
+ expect(uploader2).to receive(:unsafe_migrate!)
+
+ uploader2.migrate!(described_class::Store::REMOTE)
end
end
end
@@ -355,14 +379,10 @@ describe ObjectStorage do
end
describe '.workhorse_authorize' do
- subject { uploader_class.workhorse_authorize }
+ let(:has_length) { true }
+ let(:maximum_size) { nil }
- before do
- # ensure that we use regular Fog libraries
- # other tests might call `Fog.mock!` and
- # it will make tests to fail
- Fog.unmock!
- end
+ subject { uploader_class.workhorse_authorize(has_length: has_length, maximum_size: maximum_size) }
shared_examples 'uses local storage' do
it "returns temporary path" do
@@ -371,10 +391,6 @@ describe ObjectStorage do
expect(subject[:TempPath]).to start_with(uploader_class.root)
expect(subject[:TempPath]).to include(described_class::TMP_UPLOAD_PATH)
end
-
- it "does not return remote store" do
- is_expected.not_to have_key('RemoteObject')
- end
end
shared_examples 'uses remote storage' do
@@ -382,6 +398,8 @@ describe ObjectStorage do
is_expected.to have_key(:RemoteObject)
expect(subject[:RemoteObject]).to have_key(:ID)
+ expect(subject[:RemoteObject]).to include(Timeout: a_kind_of(Integer))
+ expect(subject[:RemoteObject][:Timeout]).to be(ObjectStorage::DirectUpload::TIMEOUT)
expect(subject[:RemoteObject]).to have_key(:GetURL)
expect(subject[:RemoteObject]).to have_key(:DeleteURL)
expect(subject[:RemoteObject]).to have_key(:StoreURL)
@@ -389,9 +407,31 @@ describe ObjectStorage do
expect(subject[:RemoteObject][:DeleteURL]).to include(described_class::TMP_UPLOAD_PATH)
expect(subject[:RemoteObject][:StoreURL]).to include(described_class::TMP_UPLOAD_PATH)
end
+ end
+
+ shared_examples 'uses remote storage with multipart uploads' do
+ it_behaves_like 'uses remote storage' do
+ it "returns multipart upload" do
+ is_expected.to have_key(:RemoteObject)
+
+ expect(subject[:RemoteObject]).to have_key(:MultipartUpload)
+ expect(subject[:RemoteObject][:MultipartUpload]).to have_key(:PartSize)
+ expect(subject[:RemoteObject][:MultipartUpload]).to have_key(:PartURLs)
+ expect(subject[:RemoteObject][:MultipartUpload]).to have_key(:CompleteURL)
+ expect(subject[:RemoteObject][:MultipartUpload]).to have_key(:AbortURL)
+ expect(subject[:RemoteObject][:MultipartUpload][:PartURLs]).to all(include(described_class::TMP_UPLOAD_PATH))
+ expect(subject[:RemoteObject][:MultipartUpload][:CompleteURL]).to include(described_class::TMP_UPLOAD_PATH)
+ expect(subject[:RemoteObject][:MultipartUpload][:AbortURL]).to include(described_class::TMP_UPLOAD_PATH)
+ end
+ end
+ end
- it "does not return local store" do
- is_expected.not_to have_key('TempPath')
+ shared_examples 'uses remote storage without multipart uploads' do
+ it_behaves_like 'uses remote storage' do
+ it "does not return multipart upload" do
+ is_expected.to have_key(:RemoteObject)
+ expect(subject[:RemoteObject]).not_to have_key(:MultipartUpload)
+ end
end
end
@@ -414,6 +454,8 @@ describe ObjectStorage do
end
context 'uses AWS' do
+ let(:storage_url) { "https://uploads.s3-eu-central-1.amazonaws.com/" }
+
before do
expect(uploader_class).to receive(:object_store_credentials) do
{ provider: "AWS",
@@ -423,18 +465,40 @@ describe ObjectStorage do
end
end
- it_behaves_like 'uses remote storage' do
- let(:storage_url) { "https://uploads.s3-eu-central-1.amazonaws.com/" }
+ context 'for known length' do
+ it_behaves_like 'uses remote storage without multipart uploads' do
+ it 'returns links for S3' do
+ expect(subject[:RemoteObject][:GetURL]).to start_with(storage_url)
+ expect(subject[:RemoteObject][:DeleteURL]).to start_with(storage_url)
+ expect(subject[:RemoteObject][:StoreURL]).to start_with(storage_url)
+ end
+ end
+ end
+
+ context 'for unknown length' do
+ let(:has_length) { false }
+ let(:maximum_size) { 1.gigabyte }
- it 'returns links for S3' do
- expect(subject[:RemoteObject][:GetURL]).to start_with(storage_url)
- expect(subject[:RemoteObject][:DeleteURL]).to start_with(storage_url)
- expect(subject[:RemoteObject][:StoreURL]).to start_with(storage_url)
+ before do
+ stub_object_storage_multipart_init(storage_url)
+ end
+
+ it_behaves_like 'uses remote storage with multipart uploads' do
+ it 'returns links for S3' do
+ expect(subject[:RemoteObject][:GetURL]).to start_with(storage_url)
+ expect(subject[:RemoteObject][:DeleteURL]).to start_with(storage_url)
+ expect(subject[:RemoteObject][:StoreURL]).to start_with(storage_url)
+ expect(subject[:RemoteObject][:MultipartUpload][:PartURLs]).to all(start_with(storage_url))
+ expect(subject[:RemoteObject][:MultipartUpload][:CompleteURL]).to start_with(storage_url)
+ expect(subject[:RemoteObject][:MultipartUpload][:AbortURL]).to start_with(storage_url)
+ end
end
end
end
context 'uses Google' do
+ let(:storage_url) { "https://storage.googleapis.com/uploads/" }
+
before do
expect(uploader_class).to receive(:object_store_credentials) do
{ provider: "Google",
@@ -443,36 +507,71 @@ describe ObjectStorage do
end
end
- it_behaves_like 'uses remote storage' do
- let(:storage_url) { "https://storage.googleapis.com/uploads/" }
+ context 'for known length' do
+ it_behaves_like 'uses remote storage without multipart uploads' do
+ it 'returns links for Google Cloud' do
+ expect(subject[:RemoteObject][:GetURL]).to start_with(storage_url)
+ expect(subject[:RemoteObject][:DeleteURL]).to start_with(storage_url)
+ expect(subject[:RemoteObject][:StoreURL]).to start_with(storage_url)
+ end
+ end
+ end
+
+ context 'for unknown length' do
+ let(:has_length) { false }
+ let(:maximum_size) { 1.gigabyte }
- it 'returns links for Google Cloud' do
- expect(subject[:RemoteObject][:GetURL]).to start_with(storage_url)
- expect(subject[:RemoteObject][:DeleteURL]).to start_with(storage_url)
- expect(subject[:RemoteObject][:StoreURL]).to start_with(storage_url)
+ it_behaves_like 'uses remote storage without multipart uploads' do
+ it 'returns links for Google Cloud' do
+ expect(subject[:RemoteObject][:GetURL]).to start_with(storage_url)
+ expect(subject[:RemoteObject][:DeleteURL]).to start_with(storage_url)
+ expect(subject[:RemoteObject][:StoreURL]).to start_with(storage_url)
+ end
end
end
end
context 'uses GDK/minio' do
+ let(:storage_url) { "http://minio:9000/uploads/" }
+
before do
expect(uploader_class).to receive(:object_store_credentials) do
{ provider: "AWS",
aws_access_key_id: "AWS_ACCESS_KEY_ID",
aws_secret_access_key: "AWS_SECRET_ACCESS_KEY",
- endpoint: 'http://127.0.0.1:9000',
+ endpoint: 'http://minio:9000',
path_style: true,
region: "gdk" }
end
end
- it_behaves_like 'uses remote storage' do
- let(:storage_url) { "http://127.0.0.1:9000/uploads/" }
+ context 'for known length' do
+ it_behaves_like 'uses remote storage without multipart uploads' do
+ it 'returns links for S3' do
+ expect(subject[:RemoteObject][:GetURL]).to start_with(storage_url)
+ expect(subject[:RemoteObject][:DeleteURL]).to start_with(storage_url)
+ expect(subject[:RemoteObject][:StoreURL]).to start_with(storage_url)
+ end
+ end
+ end
+
+ context 'for unknown length' do
+ let(:has_length) { false }
+ let(:maximum_size) { 1.gigabyte }
- it 'returns links for S3' do
- expect(subject[:RemoteObject][:GetURL]).to start_with(storage_url)
- expect(subject[:RemoteObject][:DeleteURL]).to start_with(storage_url)
- expect(subject[:RemoteObject][:StoreURL]).to start_with(storage_url)
+ before do
+ stub_object_storage_multipart_init(storage_url)
+ end
+
+ it_behaves_like 'uses remote storage with multipart uploads' do
+ it 'returns links for S3' do
+ expect(subject[:RemoteObject][:GetURL]).to start_with(storage_url)
+ expect(subject[:RemoteObject][:DeleteURL]).to start_with(storage_url)
+ expect(subject[:RemoteObject][:StoreURL]).to start_with(storage_url)
+ expect(subject[:RemoteObject][:MultipartUpload][:PartURLs]).to all(start_with(storage_url))
+ expect(subject[:RemoteObject][:MultipartUpload][:CompleteURL]).to start_with(storage_url)
+ expect(subject[:RemoteObject][:MultipartUpload][:AbortURL]).to start_with(storage_url)
+ end
end
end
end
@@ -496,7 +595,7 @@ describe ObjectStorage do
context 'when local file is used' do
context 'when valid file is used' do
let(:uploaded_file) do
- fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg')
+ fixture_file_upload('spec/fixtures/rails_sample.jpg', 'image/jpg')
end
it "properly caches the file" do
@@ -618,7 +717,7 @@ describe ObjectStorage do
let!(:fog_file) do
fog_connection.directories.get('uploads').files.create(
- key: 'tmp/upload/test/123123',
+ key: 'tmp/uploads/test/123123',
body: 'content'
)
end
@@ -657,4 +756,26 @@ describe ObjectStorage do
end
end
end
+
+ describe '#retrieve_from_store!' do
+ [:group, :project, :user].each do |model|
+ context "for #{model}s" do
+ let(:models) { create_list(model, 3, :with_avatar).map(&:reload) }
+ let(:avatars) { models.map(&:avatar) }
+
+ it 'batches fetching uploads from the database' do
+ # Ensure that these are all created and fully loaded before we start
+ # running queries for avatars
+ models
+
+ expect { avatars }.not_to exceed_query_limit(1)
+ end
+
+ it 'fetches a unique upload for each model' do
+ expect(avatars.map(&:url).uniq).to eq(avatars.map(&:url))
+ expect(avatars.map(&:upload).uniq).to eq(avatars.map(&:upload))
+ end
+ end
+ end
+ end
end
diff --git a/spec/uploaders/personal_file_uploader_spec.rb b/spec/uploaders/personal_file_uploader_spec.rb
index c70521d90dc..7700b14ce6b 100644
--- a/spec/uploaders/personal_file_uploader_spec.rb
+++ b/spec/uploaders/personal_file_uploader_spec.rb
@@ -45,7 +45,7 @@ describe PersonalFileUploader do
describe "#migrate!" do
before do
- uploader.store!(fixture_file_upload(Rails.root.join('spec/fixtures/doc_sample.txt')))
+ uploader.store!(fixture_file_upload('spec/fixtures/doc_sample.txt'))
stub_uploads_object_storage
end
diff --git a/spec/uploaders/records_uploads_spec.rb b/spec/uploaders/records_uploads_spec.rb
index 9a3e5d83e01..3592a11360d 100644
--- a/spec/uploaders/records_uploads_spec.rb
+++ b/spec/uploaders/records_uploads_spec.rb
@@ -16,7 +16,7 @@ describe RecordsUploads do
end
def upload_fixture(filename)
- fixture_file_upload(Rails.root.join('spec', 'fixtures', filename))
+ fixture_file_upload(File.join('spec', 'fixtures', filename))
end
describe 'callbacks' do
diff --git a/spec/uploaders/uploader_helper_spec.rb b/spec/uploaders/uploader_helper_spec.rb
index c47f09adb6d..33da93cc9d0 100644
--- a/spec/uploaders/uploader_helper_spec.rb
+++ b/spec/uploaders/uploader_helper_spec.rb
@@ -12,7 +12,7 @@ describe UploaderHelper do
end
def upload_fixture(filename)
- fixture_file_upload(Rails.root.join('spec', 'fixtures', filename))
+ fixture_file_upload(File.join('spec', 'fixtures', filename))
end
describe '#image_or_video?' do
diff --git a/spec/uploaders/workers/object_storage/background_move_worker_spec.rb b/spec/uploaders/workers/object_storage/background_move_worker_spec.rb
index b34f427fd8a..95813d15e52 100644
--- a/spec/uploaders/workers/object_storage/background_move_worker_spec.rb
+++ b/spec/uploaders/workers/object_storage/background_move_worker_spec.rb
@@ -125,8 +125,10 @@ describe ObjectStorage::BackgroundMoveWorker do
it "migrates file to remote storage" do
perform
+ project.reload
+ BatchLoader::Executor.clear_current
- expect(project.reload.avatar.file_storage?).to be_falsey
+ expect(project.avatar).not_to be_file_storage
end
end
@@ -137,7 +139,7 @@ describe ObjectStorage::BackgroundMoveWorker do
it "migrates file to remote storage" do
perform
- expect(project.reload.avatar.file_storage?).to be_falsey
+ expect(project.reload.avatar).not_to be_file_storage
end
end
end
diff --git a/spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb b/spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb
index aed62f97448..da490cb02af 100644
--- a/spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb
+++ b/spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb
@@ -11,6 +11,12 @@ describe ObjectStorage::MigrateUploadsWorker, :sidekiq do
let(:uploads) { Upload.all }
let(:to_store) { ObjectStorage::Store::REMOTE }
+ def perform(uploads)
+ described_class.new.perform(uploads.ids, model_class.to_s, mounted_as, to_store)
+ rescue ObjectStorage::MigrateUploadsWorker::Report::MigrationFailures
+ # swallow
+ end
+
shared_examples "uploads migration worker" do
describe '.enqueue!' do
def enqueue!
@@ -69,12 +75,6 @@ describe ObjectStorage::MigrateUploadsWorker, :sidekiq do
end
describe '#perform' do
- def perform
- described_class.new.perform(uploads.ids, model_class.to_s, mounted_as, to_store)
- rescue ObjectStorage::MigrateUploadsWorker::Report::MigrationFailures
- # swallow
- end
-
shared_examples 'outputs correctly' do |success: 0, failures: 0|
total = success + failures
@@ -82,7 +82,7 @@ describe ObjectStorage::MigrateUploadsWorker, :sidekiq do
it 'outputs the reports' do
expect(Rails.logger).to receive(:info).with(%r{Migrated #{success}/#{total} files})
- perform
+ perform(uploads)
end
end
@@ -90,7 +90,7 @@ describe ObjectStorage::MigrateUploadsWorker, :sidekiq do
it 'outputs upload failures' do
expect(Rails.logger).to receive(:warn).with(/Error .* I am a teapot/)
- perform
+ perform(uploads)
end
end
end
@@ -98,7 +98,7 @@ describe ObjectStorage::MigrateUploadsWorker, :sidekiq do
it_behaves_like 'outputs correctly', success: 10
it 'migrates files' do
- perform
+ perform(uploads)
expect(Upload.where(store: ObjectStorage::Store::LOCAL).count).to eq(0)
end
@@ -123,6 +123,17 @@ describe ObjectStorage::MigrateUploadsWorker, :sidekiq do
end
it_behaves_like "uploads migration worker"
+
+ describe "limits N+1 queries" do
+ it "to N*5" do
+ query_count = ActiveRecord::QueryRecorder.new { perform(uploads) }
+
+ more_projects = create_list(:project, 3, :with_avatar)
+
+ expected_queries_per_migration = 5 * more_projects.count
+ expect { perform(Upload.all) }.not_to exceed_query_limit(query_count).with_threshold(expected_queries_per_migration)
+ end
+ end
end
context "for FileUploader" do
@@ -130,15 +141,29 @@ describe ObjectStorage::MigrateUploadsWorker, :sidekiq do
let(:secret) { SecureRandom.hex }
let(:mounted_as) { nil }
+ def upload_file(project)
+ uploader = FileUploader.new(project)
+ uploader.store!(fixture_file_upload('spec/fixtures/doc_sample.txt'))
+ end
+
before do
stub_uploads_object_storage(FileUploader)
- projects.map do |project|
- uploader = FileUploader.new(project)
- uploader.store!(fixture_file_upload('spec/fixtures/doc_sample.txt'))
- end
+ projects.map(&method(:upload_file))
end
it_behaves_like "uploads migration worker"
+
+ describe "limits N+1 queries" do
+ it "to N*5" do
+ query_count = ActiveRecord::QueryRecorder.new { perform(uploads) }
+
+ more_projects = create_list(:project, 3)
+ more_projects.map(&method(:upload_file))
+
+ expected_queries_per_migration = 5 * more_projects.count
+ expect { perform(Upload.all) }.not_to exceed_query_limit(query_count).with_threshold(expected_queries_per_migration)
+ end
+ end
end
end
diff --git a/spec/validators/public_url_validator_spec.rb b/spec/validators/public_url_validator_spec.rb
new file mode 100644
index 00000000000..710dd3dc38e
--- /dev/null
+++ b/spec/validators/public_url_validator_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe PublicUrlValidator do
+ include_examples 'url validator examples', described_class::DEFAULT_PROTOCOLS
+
+ context 'by default' do
+ let(:validator) { described_class.new(attributes: [:link_url]) }
+ let!(:badge) { build(:badge, link_url: 'http://www.example.com') }
+
+ subject { validator.validate_each(badge, :link_url, badge.link_url) }
+
+ it 'blocks urls pointing to localhost' do
+ badge.link_url = 'https://127.0.0.1'
+
+ subject
+
+ expect(badge.errors.empty?).to be false
+ end
+
+ it 'blocks urls pointing to the local network' do
+ badge.link_url = 'https://192.168.1.1'
+
+ subject
+
+ expect(badge.errors.empty?).to be false
+ end
+ end
+end
diff --git a/spec/validators/url_placeholder_validator_spec.rb b/spec/validators/url_placeholder_validator_spec.rb
deleted file mode 100644
index b76d8acdf88..00000000000
--- a/spec/validators/url_placeholder_validator_spec.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-require 'spec_helper'
-
-describe UrlPlaceholderValidator do
- let(:validator) { described_class.new(attributes: [:link_url], **options) }
- let!(:badge) { build(:badge) }
- let(:placeholder_url) { 'http://www.example.com/%{project_path}/%{project_id}/%{default_branch}/%{commit_sha}' }
-
- subject { validator.validate_each(badge, :link_url, badge.link_url) }
-
- describe '#validates_each' do
- context 'with no options' do
- let(:options) { {} }
-
- it 'allows http and https protocols by default' do
- expect(validator.send(:default_options)[:protocols]).to eq %w(http https)
- end
-
- it 'checks that the url structure is valid' do
- badge.link_url = placeholder_url
-
- subject
-
- expect(badge.errors.empty?).to be false
- end
- end
-
- context 'with placeholder regex' do
- let(:options) { { placeholder_regex: /(project_path|project_id|commit_sha|default_branch)/ } }
-
- it 'checks that the url is valid and obviate placeholders that match regex' do
- badge.link_url = placeholder_url
-
- subject
-
- expect(badge.errors.empty?).to be true
- end
- end
- end
-end
diff --git a/spec/validators/url_validator_spec.rb b/spec/validators/url_validator_spec.rb
index 763dff181d2..93fe013d11c 100644
--- a/spec/validators/url_validator_spec.rb
+++ b/spec/validators/url_validator_spec.rb
@@ -1,45 +1,104 @@
require 'spec_helper'
describe UrlValidator do
- let(:validator) { described_class.new(attributes: [:link_url], **options) }
- let!(:badge) { build(:badge) }
-
+ let!(:badge) { build(:badge, link_url: 'http://www.example.com') }
subject { validator.validate_each(badge, :link_url, badge.link_url) }
- describe '#validates_each' do
- context 'with no options' do
- let(:options) { {} }
+ include_examples 'url validator examples', described_class::DEFAULT_PROTOCOLS
+
+ context 'by default' do
+ let(:validator) { described_class.new(attributes: [:link_url]) }
+
+ it 'does not block urls pointing to localhost' do
+ badge.link_url = 'https://127.0.0.1'
+
+ subject
+
+ expect(badge.errors.empty?).to be true
+ end
+
+ it 'does not block urls pointing to the local network' do
+ badge.link_url = 'https://192.168.1.1'
+
+ subject
+
+ expect(badge.errors.empty?).to be true
+ end
+ end
+
+ context 'when allow_localhost is set to false' do
+ let(:validator) { described_class.new(attributes: [:link_url], allow_localhost: false) }
+
+ it 'blocks urls pointing to localhost' do
+ badge.link_url = 'https://127.0.0.1'
+
+ subject
+
+ expect(badge.errors.empty?).to be false
+ end
+ end
+
+ context 'when allow_local_network is set to false' do
+ let(:validator) { described_class.new(attributes: [:link_url], allow_local_network: false) }
+
+ it 'blocks urls pointing to the local network' do
+ badge.link_url = 'https://192.168.1.1'
- it 'allows http and https protocols by default' do
- expect(validator.send(:default_options)[:protocols]).to eq %w(http https)
+ subject
+
+ expect(badge.errors.empty?).to be false
+ end
+ end
+
+ context 'when ports is' do
+ let(:validator) { described_class.new(attributes: [:link_url], ports: ports) }
+
+ context 'empty' do
+ let(:ports) { [] }
+
+ it 'does not block any port' do
+ subject
+
+ expect(badge.errors.empty?).to be true
end
+ end
- it 'checks that the url structure is valid' do
- badge.link_url = 'http://www.google.es/%{whatever}'
+ context 'set' do
+ let(:ports) { [443] }
+ it 'blocks urls with a different port' do
subject
expect(badge.errors.empty?).to be false
end
end
+ end
+
+ context 'when enforce_user is' do
+ let(:url) { 'http://$user@example.com'}
+ let(:validator) { described_class.new(attributes: [:link_url], enforce_user: enforce_user) }
- context 'with protocols' do
- let(:options) { { protocols: %w(http) } }
+ context 'true' do
+ let(:enforce_user) { true }
- it 'allows urls with the defined protocols' do
- badge.link_url = 'http://www.example.com'
+ it 'checks user format' do
+ badge.link_url = url
subject
- expect(badge.errors.empty?).to be true
+ expect(badge.errors.empty?).to be false
end
+ end
+
+ context 'false (default)' do
+ let(:enforce_user) { false }
- it 'add error if the url protocol does not match the selected ones' do
- badge.link_url = 'https://www.example.com'
+ it 'does not check user format' do
+ badge.link_url = url
subject
- expect(badge.errors.empty?).to be false
+ expect(badge.errors.empty?).to be true
end
end
end
diff --git a/spec/views/admin/dashboard/index.html.haml_spec.rb b/spec/views/admin/dashboard/index.html.haml_spec.rb
index 099baacf019..0e8b7c82d3a 100644
--- a/spec/views/admin/dashboard/index.html.haml_spec.rb
+++ b/spec/views/admin/dashboard/index.html.haml_spec.rb
@@ -4,6 +4,11 @@ describe 'admin/dashboard/index.html.haml' do
include Devise::Test::ControllerHelpers
before do
+ counts = Admin::DashboardController::COUNTED_ITEMS.each_with_object({}) do |item, hash|
+ hash[item] = 100
+ end
+
+ assign(:counts, counts)
assign(:projects, create_list(:project, 1))
assign(:users, create_list(:user, 1))
assign(:groups, create_list(:group, 1))
@@ -22,6 +27,6 @@ describe 'admin/dashboard/index.html.haml' do
it "includes revision of GitLab" do
render
- expect(rendered).to have_content "#{Gitlab::VERSION} (#{Gitlab::REVISION})"
+ expect(rendered).to have_content "#{Gitlab::VERSION} (#{Gitlab.revision})"
end
end
diff --git a/spec/views/devise/shared/_signin_box.html.haml_spec.rb b/spec/views/devise/shared/_signin_box.html.haml_spec.rb
index 0870b8f09f9..66c064e3fba 100644
--- a/spec/views/devise/shared/_signin_box.html.haml_spec.rb
+++ b/spec/views/devise/shared/_signin_box.html.haml_spec.rb
@@ -6,6 +6,7 @@ describe 'devise/shared/_signin_box' do
stub_devise
assign(:ldap_servers, [])
allow(view).to receive(:current_application_settings).and_return(Gitlab::CurrentSettings.current_application_settings)
+ allow(view).to receive(:captcha_enabled?).and_return(false)
end
it 'is shown when Crowd is enabled' do
diff --git a/spec/views/errors/access_denied.html.haml_spec.rb b/spec/views/errors/access_denied.html.haml_spec.rb
new file mode 100644
index 00000000000..bde2f6f0169
--- /dev/null
+++ b/spec/views/errors/access_denied.html.haml_spec.rb
@@ -0,0 +1,7 @@
+require 'spec_helper'
+
+describe 'errors/access_denied' do
+ it 'does not fail to render when there is no message provided' do
+ expect { render }.not_to raise_error
+ end
+end
diff --git a/spec/views/help/index.html.haml_spec.rb b/spec/views/help/index.html.haml_spec.rb
index 0a78606171d..836d452304c 100644
--- a/spec/views/help/index.html.haml_spec.rb
+++ b/spec/views/help/index.html.haml_spec.rb
@@ -39,7 +39,7 @@ describe 'help/index' do
def stub_version(version, revision)
stub_const('Gitlab::VERSION', version)
- stub_const('Gitlab::REVISION', revision)
+ allow(Gitlab).to receive(:revision).and_return(revision)
end
def stub_helpers
diff --git a/spec/views/projects/imports/new.html.haml_spec.rb b/spec/views/projects/imports/new.html.haml_spec.rb
index 32d73d0c5ab..11fe144d1d2 100644
--- a/spec/views/projects/imports/new.html.haml_spec.rb
+++ b/spec/views/projects/imports/new.html.haml_spec.rb
@@ -7,9 +7,9 @@ describe "projects/imports/new.html.haml" do
let(:project) { create(:project_empty_repo, :import_failed, import_type: :gitlab_project, import_source: '/var/opt/gitlab/gitlab-rails/shared/tmp/project_exports/uploads/t.tar.gz', import_url: nil) }
before do
- project.import_state.update_attributes(last_error: '<a href="http://googl.com">Foo</a>')
+ project.import_state.update(last_error: '<a href="http://googl.com">Foo</a>')
sign_in(user)
- project.add_master(user)
+ project.add_maintainer(user)
end
it "escapes HTML in import errors" do
diff --git a/spec/views/projects/merge_requests/show.html.haml_spec.rb b/spec/views/projects/merge_requests/show.html.haml_spec.rb
index 264e0ce0b40..fe6ad26a6f6 100644
--- a/spec/views/projects/merge_requests/show.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/show.html.haml_spec.rb
@@ -52,7 +52,7 @@ describe 'projects/merge_requests/show.html.haml' do
context 'when the merge request is open' do
it 'closes the merge request if the source project does not exist' do
- closed_merge_request.update_attributes(state: 'open')
+ closed_merge_request.update(state: 'open')
forked_project.destroy
# Reload merge request so MergeRequest#source_project turns to `nil`
closed_merge_request.reload
diff --git a/spec/views/projects/pipeline_schedules/_pipeline_schedule.html.haml_spec.rb b/spec/views/projects/pipeline_schedules/_pipeline_schedule.html.haml_spec.rb
index 6e7d8db99c4..5d60d6bc5e7 100644
--- a/spec/views/projects/pipeline_schedules/_pipeline_schedule.html.haml_spec.rb
+++ b/spec/views/projects/pipeline_schedules/_pipeline_schedule.html.haml_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe 'projects/pipeline_schedules/_pipeline_schedule' do
let(:owner) { create(:user) }
- let(:master) { create(:user) }
+ let(:maintainer) { create(:user) }
let(:project) { create(:project) }
let(:pipeline_schedule) { create(:ci_pipeline_schedule, :nightly, project: project) }
@@ -17,10 +17,10 @@ describe 'projects/pipeline_schedules/_pipeline_schedule' do
context 'taking ownership of schedule' do
context 'when non-owner is signed in' do
- let(:user) { master }
+ let(:user) { maintainer }
before do
- allow(view).to receive(:can?).with(master, :take_ownership_pipeline_schedule, pipeline_schedule).and_return(true)
+ allow(view).to receive(:can?).with(maintainer, :take_ownership_pipeline_schedule, pipeline_schedule).and_return(true)
end
it 'non-owner can take ownership of pipeline' do
diff --git a/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb b/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb
index d15391911c1..cb1b9e6f5fb 100644
--- a/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb
+++ b/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb
@@ -12,7 +12,7 @@ describe 'projects/settings/ci_cd/_autodevops_form' do
it 'shows warning message' do
render
- expect(rendered).to have_css('.settings-message')
+ expect(rendered).to have_css('.auto-devops-warning-message')
expect(rendered).to have_text('Auto Review Apps and Auto Deploy need a domain name and a')
expect(rendered).to have_link('Kubernetes cluster')
end
@@ -26,7 +26,7 @@ describe 'projects/settings/ci_cd/_autodevops_form' do
it 'shows warning message' do
render
- expect(rendered).to have_css('.settings-message')
+ expect(rendered).to have_css('.auto-devops-warning-message')
expect(rendered).to have_text('Auto Review Apps and Auto Deploy need a')
expect(rendered).to have_link('Kubernetes cluster')
end
@@ -42,7 +42,7 @@ describe 'projects/settings/ci_cd/_autodevops_form' do
it 'shows warning message' do
render
- expect(rendered).to have_css('.settings-message')
+ expect(rendered).to have_css('.auto-devops-warning-message')
expect(rendered).to have_text('Auto Review Apps and Auto Deploy need a domain name to work correctly.')
end
end
@@ -55,7 +55,7 @@ describe 'projects/settings/ci_cd/_autodevops_form' do
it 'does not show warning message' do
render
- expect(rendered).not_to have_css('.settings-message')
+ expect(rendered).not_to have_css('.auto-devops-warning-message')
end
end
end
diff --git a/spec/views/shared/notes/_form.html.haml_spec.rb b/spec/views/shared/notes/_form.html.haml_spec.rb
index 50980718e66..c57319869f3 100644
--- a/spec/views/shared/notes/_form.html.haml_spec.rb
+++ b/spec/views/shared/notes/_form.html.haml_spec.rb
@@ -7,7 +7,7 @@ describe 'shared/notes/_form' do
let(:project) { create(:project, :repository) }
before do
- project.add_master(user)
+ project.add_maintainer(user)
assign(:project, project)
assign(:note, note)
diff --git a/spec/workers/check_gcp_project_billing_worker_spec.rb b/spec/workers/check_gcp_project_billing_worker_spec.rb
deleted file mode 100644
index 526ecf75921..00000000000
--- a/spec/workers/check_gcp_project_billing_worker_spec.rb
+++ /dev/null
@@ -1,116 +0,0 @@
-require 'spec_helper'
-
-describe CheckGcpProjectBillingWorker do
- describe '.perform' do
- let(:token) { 'bogustoken' }
-
- subject { described_class.new.perform('token_key') }
-
- before do
- allow(described_class).to receive(:get_billing_state)
- allow_any_instance_of(described_class).to receive(:update_billing_change_counter)
- end
-
- context 'when there is a token in redis' do
- before do
- allow(described_class).to receive(:get_session_token).and_return(token)
- end
-
- context 'when there is no lease' do
- before do
- allow_any_instance_of(described_class).to receive(:try_obtain_lease_for).and_return('randomuuid')
- end
-
- it 'calls the service' do
- expect(CheckGcpProjectBillingService).to receive_message_chain(:new, :execute).and_return([double])
-
- subject
- end
-
- it 'stores billing status in redis' do
- expect(CheckGcpProjectBillingService).to receive_message_chain(:new, :execute).and_return([double])
- expect(described_class).to receive(:set_billing_state).with(token, true)
-
- subject
- end
- end
-
- context 'when there is a lease' do
- before do
- allow_any_instance_of(described_class).to receive(:try_obtain_lease_for).and_return(false)
- end
-
- it 'does not call the service' do
- expect(CheckGcpProjectBillingService).not_to receive(:new)
-
- subject
- end
- end
- end
-
- context 'when there is no token in redis' do
- before do
- allow(described_class).to receive(:get_session_token).and_return(nil)
- end
-
- it 'does not call the service' do
- expect(CheckGcpProjectBillingService).not_to receive(:new)
-
- subject
- end
- end
- end
-
- describe 'billing change counter' do
- subject { described_class.new.perform('token_key') }
-
- before do
- allow(described_class).to receive(:get_session_token).and_return('bogustoken')
- allow_any_instance_of(described_class).to receive(:try_obtain_lease_for).and_return('randomuuid')
- allow(described_class).to receive(:set_billing_state)
- end
-
- context 'when previous state was false' do
- before do
- expect(described_class).to receive(:get_billing_state).and_return(false)
- end
-
- context 'when the current state is false' do
- before do
- expect(CheckGcpProjectBillingService).to receive_message_chain(:new, :execute).and_return([])
- end
-
- it 'increments the billing change counter' do
- expect_any_instance_of(described_class).to receive_message_chain(:billing_changed_counter, :increment)
-
- subject
- end
- end
-
- context 'when the current state is true' do
- before do
- expect(CheckGcpProjectBillingService).to receive_message_chain(:new, :execute).and_return([double])
- end
-
- it 'increments the billing change counter' do
- expect_any_instance_of(described_class).to receive_message_chain(:billing_changed_counter, :increment)
-
- subject
- end
- end
- end
-
- context 'when previous state was true' do
- before do
- expect(described_class).to receive(:get_billing_state).and_return(true)
- expect(CheckGcpProjectBillingService).to receive_message_chain(:new, :execute).and_return([double])
- end
-
- it 'increment the billing change counter' do
- expect_any_instance_of(described_class).to receive_message_chain(:billing_changed_counter, :increment)
-
- subject
- end
- end
- end
-end
diff --git a/spec/workers/ci/archive_traces_cron_worker_spec.rb b/spec/workers/ci/archive_traces_cron_worker_spec.rb
new file mode 100644
index 00000000000..23f5dda298a
--- /dev/null
+++ b/spec/workers/ci/archive_traces_cron_worker_spec.rb
@@ -0,0 +1,71 @@
+require 'spec_helper'
+
+describe Ci::ArchiveTracesCronWorker do
+ subject { described_class.new.perform }
+
+ before do
+ stub_feature_flags(ci_enable_live_trace: true)
+ end
+
+ shared_examples_for 'archives trace' do
+ it do
+ subject
+
+ build.reload
+ expect(build.job_artifacts_trace).to be_exist
+ end
+ end
+
+ shared_examples_for 'does not archive trace' do
+ it do
+ subject
+
+ build.reload
+ expect(build.job_artifacts_trace).to be_nil
+ end
+ end
+
+ context 'when a job succeeded' do
+ let!(:build) { create(:ci_build, :success, :trace_live) }
+
+ it_behaves_like 'archives trace'
+
+ context 'when a trace had already been archived' do
+ let!(:build) { create(:ci_build, :success, :trace_live, :trace_artifact) }
+ let!(:build2) { create(:ci_build, :success, :trace_live) }
+
+ it 'continues to archive live traces' do
+ subject
+
+ build2.reload
+ expect(build2.job_artifacts_trace).to be_exist
+ end
+ end
+
+ context 'when an unexpected exception happened during archiving' do
+ let!(:build) { create(:ci_build, :success, :trace_live) }
+
+ before do
+ allow_any_instance_of(Gitlab::Ci::Trace).to receive(:archive!).and_raise('Unexpected error')
+ end
+
+ it 'puts a log' do
+ expect(Rails.logger).to receive(:error).with("Failed to archive stale live trace. id: #{build.id} message: Unexpected error")
+
+ subject
+ end
+ end
+ end
+
+ context 'when a job was cancelled' do
+ let!(:build) { create(:ci_build, :canceled, :trace_live) }
+
+ it_behaves_like 'archives trace'
+ end
+
+ context 'when a job is running' do
+ let!(:build) { create(:ci_build, :running, :trace_live) }
+
+ it_behaves_like 'does not archive trace'
+ end
+end
diff --git a/spec/workers/concerns/waitable_worker_spec.rb b/spec/workers/concerns/waitable_worker_spec.rb
index 54ab07981a4..ce38cde9208 100644
--- a/spec/workers/concerns/waitable_worker_spec.rb
+++ b/spec/workers/concerns/waitable_worker_spec.rb
@@ -7,9 +7,7 @@ describe WaitableWorker do
'Gitlab::Foo::Bar::DummyWorker'
end
- class << self
- cattr_accessor(:counter) { 0 }
- end
+ cattr_accessor(:counter) { 0 }
include ApplicationWorker
prepend WaitableWorker
@@ -20,8 +18,8 @@ describe WaitableWorker do
def self.bulk_perform_inline(args_list)
end
- def perform(i = 0)
- self.class.counter += i
+ def perform(count = 0)
+ self.class.counter += count
end
end
end
diff --git a/spec/workers/create_note_diff_file_worker_spec.rb b/spec/workers/create_note_diff_file_worker_spec.rb
new file mode 100644
index 00000000000..0ac946a1232
--- /dev/null
+++ b/spec/workers/create_note_diff_file_worker_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe CreateNoteDiffFileWorker do
+ describe '#perform' do
+ let(:diff_note) { create(:diff_note_on_merge_request) }
+
+ it 'creates diff file' do
+ diff_note.note_diff_file.destroy!
+
+ expect_any_instance_of(DiffNote).to receive(:create_diff_file)
+ .and_call_original
+
+ described_class.new.perform(diff_note.id)
+ end
+ end
+end
diff --git a/spec/workers/delete_diff_files_worker_spec.rb b/spec/workers/delete_diff_files_worker_spec.rb
new file mode 100644
index 00000000000..e0edd313922
--- /dev/null
+++ b/spec/workers/delete_diff_files_worker_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe DeleteDiffFilesWorker do
+ describe '#perform' do
+ let(:merge_request) { create(:merge_request) }
+ let(:merge_request_diff) { merge_request.merge_request_diff }
+
+ it 'deletes all merge request diff files' do
+ expect { described_class.new.perform(merge_request_diff.id) }
+ .to change { merge_request_diff.merge_request_diff_files.count }
+ .from(20).to(0)
+ end
+
+ it 'updates state to without_files' do
+ expect { described_class.new.perform(merge_request_diff.id) }
+ .to change { merge_request_diff.reload.state }
+ .from('collected').to('without_files')
+ end
+
+ it 'does nothing if diff was already marked as "without_files"' do
+ merge_request_diff.clean!
+
+ expect_any_instance_of(MergeRequestDiff).not_to receive(:clean!)
+
+ described_class.new.perform(merge_request_diff.id)
+ end
+
+ it 'rollsback if something goes wrong' do
+ expect(MergeRequestDiffFile).to receive_message_chain(:where, :delete_all)
+ .and_raise
+
+ expect { described_class.new.perform(merge_request_diff.id) }
+ .to raise_error
+
+ merge_request_diff.reload
+
+ expect(merge_request_diff.state).to eq('collected')
+ expect(merge_request_diff.merge_request_diff_files.count).to eq(20)
+ end
+ end
+end
diff --git a/spec/workers/delete_user_worker_spec.rb b/spec/workers/delete_user_worker_spec.rb
index 36594515005..06d9e125105 100644
--- a/spec/workers/delete_user_worker_spec.rb
+++ b/spec/workers/delete_user_worker_spec.rb
@@ -5,15 +5,17 @@ describe DeleteUserWorker do
let!(:current_user) { create(:user) }
it "calls the DeleteUserWorker with the params it was given" do
- expect_any_instance_of(Users::DestroyService).to receive(:execute)
- .with(user, {})
+ expect_next_instance_of(Users::DestroyService) do |service|
+ expect(service).to receive(:execute).with(user, {})
+ end
described_class.new.perform(current_user.id, user.id)
end
it "uses symbolized keys" do
- expect_any_instance_of(Users::DestroyService).to receive(:execute)
- .with(user, test: "test")
+ expect_next_instance_of(Users::DestroyService) do |service|
+ expect(service).to receive(:execute).with(user, test: "test")
+ end
described_class.new.perform(current_user.id, user.id, "test" => "test")
end
diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb
index 9e3b99b3502..2106959e23c 100644
--- a/spec/workers/every_sidekiq_worker_spec.rb
+++ b/spec/workers/every_sidekiq_worker_spec.rb
@@ -13,7 +13,7 @@ describe 'Every Sidekiq worker' do
file_worker_queues = Gitlab::SidekiqConfig.worker_queues.to_set
worker_queues = Gitlab::SidekiqConfig.workers.map(&:queue).to_set
- worker_queues << ActionMailer::DeliveryJob.queue_name
+ worker_queues << ActionMailer::DeliveryJob.new.queue_name
worker_queues << 'default'
missing_from_file = worker_queues - file_worker_queues
diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb
index 74539a7e493..d5808e21271 100644
--- a/spec/workers/git_garbage_collect_worker_spec.rb
+++ b/spec/workers/git_garbage_collect_worker_spec.rb
@@ -11,36 +11,69 @@ describe GitGarbageCollectWorker do
subject { described_class.new }
describe "#perform" do
- shared_examples 'flushing ref caches' do |gitaly|
- context 'with active lease_uuid' do
+ context 'with active lease_uuid' do
+ before do
+ allow(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
+ end
+
+ it "flushes ref caches when the task if 'gc'" do
+ expect(subject).to receive(:renew_lease).with(lease_key, lease_uuid).and_call_original
+ expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:garbage_collect)
+ .and_return(nil)
+ expect_any_instance_of(Repository).to receive(:after_create_branch).and_call_original
+ expect_any_instance_of(Repository).to receive(:branch_names).and_call_original
+ expect_any_instance_of(Repository).to receive(:has_visible_content?).and_call_original
+ expect_any_instance_of(Gitlab::Git::Repository).to receive(:has_visible_content?).and_call_original
+
+ subject.perform(project.id, :gc, lease_key, lease_uuid)
+ end
+
+ it 'handles gRPC errors' do
+ expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:garbage_collect).and_raise(GRPC::NotFound)
+
+ expect { subject.perform(project.id, :gc, lease_key, lease_uuid) }.to raise_exception(Gitlab::Git::Repository::NoRepository)
+ end
+ end
+
+ context 'with different lease than the active one' do
+ before do
+ allow(subject).to receive(:get_lease_uuid).and_return(SecureRandom.uuid)
+ end
+
+ it 'returns silently' do
+ expect_any_instance_of(Repository).not_to receive(:after_create_branch).and_call_original
+ expect_any_instance_of(Repository).not_to receive(:branch_names).and_call_original
+ expect_any_instance_of(Repository).not_to receive(:has_visible_content?).and_call_original
+
+ subject.perform(project.id, :gc, lease_key, lease_uuid)
+ end
+ end
+
+ context 'with no active lease' do
+ before do
+ allow(subject).to receive(:get_lease_uuid).and_return(false)
+ end
+
+ context 'when is able to get the lease' do
before do
- allow(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
+ allow(subject).to receive(:try_obtain_lease).and_return(SecureRandom.uuid)
end
it "flushes ref caches when the task if 'gc'" do
- expect(subject).to receive(:renew_lease).with(lease_key, lease_uuid).and_call_original
- expect(subject).to receive(:command).with(:gc).and_return([:the, :command])
-
- if gitaly
- expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:garbage_collect)
- .and_return(nil)
- else
- expect(Gitlab::Popen).to receive(:popen)
- .with([:the, :command], project.repository.path_to_repo).and_return(["", 0])
- end
-
+ expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:garbage_collect)
+ .and_return(nil)
expect_any_instance_of(Repository).to receive(:after_create_branch).and_call_original
expect_any_instance_of(Repository).to receive(:branch_names).and_call_original
expect_any_instance_of(Repository).to receive(:has_visible_content?).and_call_original
expect_any_instance_of(Gitlab::Git::Repository).to receive(:has_visible_content?).and_call_original
- subject.perform(project.id, :gc, lease_key, lease_uuid)
+ subject.perform(project.id)
end
end
- context 'with different lease than the active one' do
+ context 'when no lease can be obtained' do
before do
- allow(subject).to receive(:get_lease_uuid).and_return(SecureRandom.uuid)
+ expect(subject).to receive(:try_obtain_lease).and_return(false)
end
it 'returns silently' do
@@ -49,65 +82,11 @@ describe GitGarbageCollectWorker do
expect_any_instance_of(Repository).not_to receive(:branch_names).and_call_original
expect_any_instance_of(Repository).not_to receive(:has_visible_content?).and_call_original
- subject.perform(project.id, :gc, lease_key, lease_uuid)
- end
- end
-
- context 'with no active lease' do
- before do
- allow(subject).to receive(:get_lease_uuid).and_return(false)
- end
-
- context 'when is able to get the lease' do
- before do
- allow(subject).to receive(:try_obtain_lease).and_return(SecureRandom.uuid)
- end
-
- it "flushes ref caches when the task if 'gc'" do
- expect(subject).to receive(:command).with(:gc).and_return([:the, :command])
-
- if gitaly
- expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:garbage_collect)
- .and_return(nil)
- else
- expect(Gitlab::Popen).to receive(:popen)
- .with([:the, :command], project.repository.path_to_repo).and_return(["", 0])
- end
-
- expect_any_instance_of(Repository).to receive(:after_create_branch).and_call_original
- expect_any_instance_of(Repository).to receive(:branch_names).and_call_original
- expect_any_instance_of(Repository).to receive(:has_visible_content?).and_call_original
- expect_any_instance_of(Gitlab::Git::Repository).to receive(:has_visible_content?).and_call_original
-
- subject.perform(project.id)
- end
- end
-
- context 'when no lease can be obtained' do
- before do
- expect(subject).to receive(:try_obtain_lease).and_return(false)
- end
-
- it 'returns silently' do
- expect(subject).not_to receive(:command)
- expect_any_instance_of(Repository).not_to receive(:after_create_branch).and_call_original
- expect_any_instance_of(Repository).not_to receive(:branch_names).and_call_original
- expect_any_instance_of(Repository).not_to receive(:has_visible_content?).and_call_original
-
- subject.perform(project.id)
- end
+ subject.perform(project.id)
end
end
end
- context "with Gitaly turned on" do
- it_should_behave_like 'flushing ref caches', true
- end
-
- context "with Gitaly turned off", :skip_gitaly_mock do
- it_should_behave_like 'flushing ref caches', false
- end
-
context "repack_full" do
before do
expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
@@ -218,7 +197,9 @@ describe GitGarbageCollectWorker do
# Create a new commit on a random new branch
def create_objects(project)
- rugged = project.repository.rugged
+ rugged = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ project.repository.rugged
+ end
old_commit = rugged.branches.first.target
new_commit_sha = Rugged::Commit.create(
rugged,
@@ -237,7 +218,9 @@ describe GitGarbageCollectWorker do
end
def packs(project)
- Dir["#{project.repository.path_to_repo}/objects/pack/*.pack"]
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ Dir["#{project.repository.path_to_repo}/objects/pack/*.pack"]
+ end
end
def packed_refs(project)
diff --git a/spec/workers/gitlab/github_import/stage/import_lfs_objects_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_lfs_objects_worker_spec.rb
new file mode 100644
index 00000000000..b19884d7991
--- /dev/null
+++ b/spec/workers/gitlab/github_import/stage/import_lfs_objects_worker_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Stage::ImportLfsObjectsWorker do
+ let(:project) { create(:project) }
+ let(:worker) { described_class.new }
+
+ describe '#import' do
+ it 'imports all the lfs objects' do
+ importer = double(:importer)
+ waiter = Gitlab::JobWaiter.new(2, '123')
+
+ expect(Gitlab::GithubImport::Importer::LfsObjectsImporter)
+ .to receive(:new)
+ .with(project, nil)
+ .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(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
index 098d2d55386..94cff9e4e80 100644
--- a/spec/workers/gitlab/github_import/stage/import_notes_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_notes_worker_spec.rb
@@ -21,7 +21,7 @@ describe Gitlab::GithubImport::Stage::ImportNotesWorker do
expect(Gitlab::GithubImport::AdvanceStageWorker)
.to receive(:perform_async)
- .with(project.id, { '123' => 2 }, :finish)
+ .with(project.id, { '123' => 2 }, :lfs_objects)
worker.import(client, project)
end
diff --git a/spec/workers/merge_worker_spec.rb b/spec/workers/merge_worker_spec.rb
index c861a56497e..b57c275c770 100644
--- a/spec/workers/merge_worker_spec.rb
+++ b/spec/workers/merge_worker_spec.rb
@@ -8,7 +8,7 @@ describe MergeWorker do
let!(:author) { merge_request.author }
before do
- source_project.add_master(author)
+ source_project.add_maintainer(author)
source_project.repository.expire_branches_cache
end
diff --git a/spec/workers/object_storage_upload_worker_spec.rb b/spec/workers/object_storage_upload_worker_spec.rb
deleted file mode 100644
index 32ddcbe9757..00000000000
--- a/spec/workers/object_storage_upload_worker_spec.rb
+++ /dev/null
@@ -1,108 +0,0 @@
-require 'spec_helper'
-
-describe ObjectStorageUploadWorker do
- let(:local) { ObjectStorage::Store::LOCAL }
- let(:remote) { ObjectStorage::Store::REMOTE }
-
- def perform
- described_class.perform_async(uploader_class.name, subject_class, file_field, subject_id)
- end
-
- context 'for LFS' do
- let!(:lfs_object) { create(:lfs_object, :with_file, file_store: local) }
- let(:uploader_class) { LfsObjectUploader }
- let(:subject_class) { LfsObject }
- let(:file_field) { :file }
- let(:subject_id) { lfs_object.id }
-
- context 'when object storage is enabled' do
- before do
- stub_lfs_object_storage(background_upload: true)
- end
-
- it 'uploads object to storage' do
- expect { perform }.to change { lfs_object.reload.file_store }.from(local).to(remote)
- end
-
- context 'when background upload is disabled' do
- before do
- allow(Gitlab.config.lfs.object_store).to receive(:background_upload) { false }
- end
-
- it 'is skipped' do
- expect { perform }.not_to change { lfs_object.reload.file_store }
- end
- end
- end
-
- context 'when object storage is disabled' do
- before do
- stub_lfs_object_storage(enabled: false)
- end
-
- it "doesn't migrate files" do
- perform
-
- expect(lfs_object.reload.file_store).to eq(local)
- end
- end
- end
-
- context 'for legacy artifacts' do
- let(:build) { create(:ci_build, :legacy_artifacts) }
- let(:uploader_class) { LegacyArtifactUploader }
- let(:subject_class) { Ci::Build }
- let(:file_field) { :artifacts_file }
- let(:subject_id) { build.id }
-
- context 'when local storage is used' do
- let(:store) { local }
-
- context 'and remote storage is defined' do
- before do
- stub_artifacts_object_storage(background_upload: true)
- end
-
- it "migrates file to remote storage" do
- perform
-
- expect(build.reload.artifacts_file_store).to eq(remote)
- end
-
- context 'for artifacts_metadata' do
- let(:file_field) { :artifacts_metadata }
-
- it 'migrates metadata to remote storage' do
- perform
-
- expect(build.reload.artifacts_metadata_store).to eq(remote)
- end
- end
- end
- end
- end
-
- context 'for job artifacts' do
- let(:artifact) { create(:ci_job_artifact, :archive) }
- let(:uploader_class) { JobArtifactUploader }
- let(:subject_class) { Ci::JobArtifact }
- let(:file_field) { :file }
- let(:subject_id) { artifact.id }
-
- context 'when local storage is used' do
- let(:store) { local }
-
- context 'and remote storage is defined' do
- before do
- stub_artifacts_object_storage(background_upload: true)
- end
-
- it "migrates file to remote storage" do
- perform
-
- expect(artifact.reload.file_store).to eq(remote)
- end
- end
- end
- end
-end
diff --git a/spec/workers/pipeline_schedule_worker_spec.rb b/spec/workers/pipeline_schedule_worker_spec.rb
index e7a4ac0f3d6..a2fe4734d47 100644
--- a/spec/workers/pipeline_schedule_worker_spec.rb
+++ b/spec/workers/pipeline_schedule_worker_spec.rb
@@ -18,7 +18,7 @@ describe PipelineScheduleWorker do
context 'when the schedule is runnable by the user' do
before do
- project.add_master(user)
+ project.add_maintainer(user)
end
context 'when there is a scheduled pipeline within next_run_at' do
diff --git a/spec/workers/process_commit_worker_spec.rb b/spec/workers/process_commit_worker_spec.rb
index ac79d9c0ac1..2d071c181c2 100644
--- a/spec/workers/process_commit_worker_spec.rb
+++ b/spec/workers/process_commit_worker_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe ProcessCommitWorker do
+ include ProjectForksHelper
+
let(:worker) { described_class.new }
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
@@ -32,15 +34,41 @@ describe ProcessCommitWorker do
worker.perform(project.id, user.id, commit.to_hash)
end
- context 'when commit already exists in upstream project' do
- let(:forked) { create(:project, :public, :repository) }
+ context 'when the project is forked' do
+ context 'when commit already exists in the upstream project' do
+ it 'does not process the commit message' do
+ forked = fork_project(project, user, repository: true)
+
+ expect(worker).not_to receive(:process_commit_message)
+
+ worker.perform(forked.id, user.id, forked.commit.to_hash)
+ end
+ end
+
+ context 'when the commit does not exist in the upstream project' do
+ it 'processes the commit message' do
+ empty_project = create(:project, :public)
+ forked = fork_project(empty_project, user, repository: true)
+
+ TestEnv.copy_repo(forked,
+ bare_repo: TestEnv.factory_repo_path_bare,
+ refs: TestEnv::BRANCH_SHA)
+
+ expect(worker).to receive(:process_commit_message)
+
+ worker.perform(forked.id, user.id, forked.commit.to_hash)
+ end
+ end
- it 'does not process commit message' do
- create(:forked_project_link, forked_to_project: forked, forked_from_project: project)
+ context 'when the upstream project no longer exists' do
+ it 'processes the commit message' do
+ forked = fork_project(project, user, repository: true)
+ project.destroy!
- expect(worker).not_to receive(:process_commit_message)
+ expect(worker).to receive(:process_commit_message)
- worker.perform(forked.id, user.id, forked.commit.to_hash)
+ worker.perform(forked.id, user.id, forked.commit.to_hash)
+ end
end
end
end
diff --git a/spec/workers/project_cache_worker_spec.rb b/spec/workers/project_cache_worker_spec.rb
index 6b1f2ff3227..b9b5445562f 100644
--- a/spec/workers/project_cache_worker_spec.rb
+++ b/spec/workers/project_cache_worker_spec.rb
@@ -1,14 +1,17 @@
require 'spec_helper'
describe ProjectCacheWorker do
+ include ExclusiveLeaseHelpers
+
let(:worker) { described_class.new }
let(:project) { create(:project, :repository) }
let(:statistics) { project.statistics }
+ let(:lease_key) { "project_cache_worker:#{project.id}:update_statistics" }
+ let(:lease_timeout) { ProjectCacheWorker::LEASE_TIMEOUT }
describe '#perform' do
before do
- allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain)
- .and_return(true)
+ stub_exclusive_lease(lease_key, timeout: lease_timeout)
end
context 'with a non-existing project' do
@@ -63,9 +66,7 @@ describe ProjectCacheWorker do
describe '#update_statistics' do
context 'when a lease could not be obtained' do
it 'does not update the repository size' do
- allow(worker).to receive(:try_obtain_lease_for)
- .with(project.id, :update_statistics)
- .and_return(false)
+ stub_exclusive_lease_taken(lease_key, timeout: lease_timeout)
expect(statistics).not_to receive(:refresh!)
@@ -75,9 +76,7 @@ describe ProjectCacheWorker do
context 'when a lease could be obtained' do
it 'updates the project statistics' do
- allow(worker).to receive(:try_obtain_lease_for)
- .with(project.id, :update_statistics)
- .and_return(true)
+ stub_exclusive_lease(lease_key, timeout: lease_timeout)
expect(statistics).to receive(:refresh!)
.with(only: %i(repository_size))
diff --git a/spec/workers/project_destroy_worker_spec.rb b/spec/workers/project_destroy_worker_spec.rb
index f19c9dff941..42e1d86e3bb 100644
--- a/spec/workers/project_destroy_worker_spec.rb
+++ b/spec/workers/project_destroy_worker_spec.rb
@@ -2,7 +2,11 @@ require 'spec_helper'
describe ProjectDestroyWorker do
let(:project) { create(:project, :repository, pending_delete: true) }
- let(:path) { project.repository.path_to_repo }
+ let(:path) do
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ project.repository.path_to_repo
+ end
+ end
subject { described_class.new }
diff --git a/spec/workers/project_migrate_hashed_storage_worker_spec.rb b/spec/workers/project_migrate_hashed_storage_worker_spec.rb
index 2e3951e7afc..9551e358af1 100644
--- a/spec/workers/project_migrate_hashed_storage_worker_spec.rb
+++ b/spec/workers/project_migrate_hashed_storage_worker_spec.rb
@@ -1,53 +1,47 @@
require 'spec_helper'
describe ProjectMigrateHashedStorageWorker, :clean_gitlab_redis_shared_state do
+ include ExclusiveLeaseHelpers
+
describe '#perform' do
let(:project) { create(:project, :empty_repo) }
- let(:pending_delete_project) { create(:project, :empty_repo, pending_delete: true) }
+ let(:lease_key) { "project_migrate_hashed_storage_worker:#{project.id}" }
+ let(:lease_timeout) { ProjectMigrateHashedStorageWorker::LEASE_TIMEOUT }
+
+ it 'skips when project no longer exists' do
+ expect(::Projects::HashedStorageMigrationService).not_to receive(:new)
+
+ subject.perform(-1)
+ end
- context 'when have exclusive lease' do
- before do
- lease = subject.lease_for(project.id)
+ it 'skips when project is pending delete' do
+ pending_delete_project = create(:project, :empty_repo, pending_delete: true)
- allow(Gitlab::ExclusiveLease).to receive(:new).and_return(lease)
- allow(lease).to receive(:try_obtain).and_return(true)
- end
+ expect(::Projects::HashedStorageMigrationService).not_to receive(:new)
- it 'skips when project no longer exists' do
- nonexistent_id = 999999999999
+ subject.perform(pending_delete_project.id)
+ end
- expect(::Projects::HashedStorageMigrationService).not_to receive(:new)
- subject.perform(nonexistent_id)
- end
+ it 'delegates removal to service class when have exclusive lease' do
+ stub_exclusive_lease(lease_key, 'uuid', timeout: lease_timeout)
- it 'skips when project is pending delete' do
- expect(::Projects::HashedStorageMigrationService).not_to receive(:new)
+ migration_service = spy
- subject.perform(pending_delete_project.id)
- end
+ allow(::Projects::HashedStorageMigrationService)
+ .to receive(:new).with(project, subject.logger)
+ .and_return(migration_service)
- 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)
- subject.perform(project.id)
- end
+ expect(migration_service).to have_received(:execute)
end
- 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 when dont have exclusive lease' do
+ stub_exclusive_lease_taken(lease_key, timeout: lease_timeout)
- it 'skips when dont have lease' do
- expect(::Projects::HashedStorageMigrationService).not_to receive(:new)
+ expect(::Projects::HashedStorageMigrationService).not_to receive(:new)
- subject.perform(project.id)
- end
+ subject.perform(project.id)
end
end
end
diff --git a/spec/workers/propagate_service_template_worker_spec.rb b/spec/workers/propagate_service_template_worker_spec.rb
index b8b65ead9b3..af1fb80a51d 100644
--- a/spec/workers/propagate_service_template_worker_spec.rb
+++ b/spec/workers/propagate_service_template_worker_spec.rb
@@ -1,29 +1,29 @@
require 'spec_helper'
describe PropagateServiceTemplateWorker do
- let!(:service_template) do
- PushoverService.create(
- template: true,
- active: true,
- properties: {
- device: 'MyDevice',
- sound: 'mic',
- priority: 4,
- user_key: 'asdf',
- api_key: '123456789'
- })
- end
-
- before do
- allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain)
- .and_return(true)
- end
+ include ExclusiveLeaseHelpers
describe '#perform' do
it 'calls the propagate service with the template' do
- expect(Projects::PropagateServiceTemplate).to receive(:propagate).with(service_template)
+ template = PushoverService.create(
+ template: true,
+ active: true,
+ properties: {
+ device: 'MyDevice',
+ sound: 'mic',
+ priority: 4,
+ user_key: 'asdf',
+ api_key: '123456789'
+ })
+
+ stub_exclusive_lease("propagate_service_template_worker:#{template.id}",
+ timeout: PropagateServiceTemplateWorker::LEASE_TIMEOUT)
+
+ expect(Projects::PropagateServiceTemplate)
+ .to receive(:propagate)
+ .with(template)
- subject.perform(service_template.id)
+ subject.perform(template.id)
end
end
end
diff --git a/spec/workers/prune_web_hook_logs_worker_spec.rb b/spec/workers/prune_web_hook_logs_worker_spec.rb
new file mode 100644
index 00000000000..d7d64a1f641
--- /dev/null
+++ b/spec/workers/prune_web_hook_logs_worker_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe PruneWebHookLogsWorker do
+ describe '#perform' do
+ before do
+ hook = create(:project_hook)
+
+ 5.times do
+ create(:web_hook_log, web_hook: hook, created_at: 5.months.ago)
+ end
+
+ create(:web_hook_log, web_hook: hook, response_status: '404')
+ end
+
+ it 'removes all web hook logs older than one month' do
+ described_class.new.perform
+
+ expect(WebHookLog.count).to eq(1)
+ expect(WebHookLog.first.response_status).to eq('404')
+ end
+ end
+end
diff --git a/spec/workers/repository_check/batch_worker_spec.rb b/spec/workers/repository_check/batch_worker_spec.rb
index 6cd27d2fafb..ede271b2cdd 100644
--- a/spec/workers/repository_check/batch_worker_spec.rb
+++ b/spec/workers/repository_check/batch_worker_spec.rb
@@ -1,14 +1,19 @@
require 'spec_helper'
describe RepositoryCheck::BatchWorker do
+ let(:shard_name) { 'default' }
subject { described_class.new }
+ before do
+ Gitlab::ShardHealthCache.update([shard_name])
+ end
+
it 'prefers projects that have never been checked' do
projects = create_list(:project, 3, created_at: 1.week.ago)
projects[0].update_column(:last_repository_check_at, 4.months.ago)
projects[2].update_column(:last_repository_check_at, 3.months.ago)
- expect(subject.perform).to eq(projects.values_at(1, 0, 2).map(&:id))
+ expect(subject.perform(shard_name)).to eq(projects.values_at(1, 0, 2).map(&:id))
end
it 'sorts projects by last_repository_check_at' do
@@ -17,7 +22,7 @@ describe RepositoryCheck::BatchWorker do
projects[1].update_column(:last_repository_check_at, 4.months.ago)
projects[2].update_column(:last_repository_check_at, 3.months.ago)
- expect(subject.perform).to eq(projects.values_at(1, 2, 0).map(&:id))
+ expect(subject.perform(shard_name)).to eq(projects.values_at(1, 2, 0).map(&:id))
end
it 'excludes projects that were checked recently' do
@@ -26,7 +31,14 @@ describe RepositoryCheck::BatchWorker do
projects[1].update_column(:last_repository_check_at, 2.months.ago)
projects[2].update_column(:last_repository_check_at, 3.days.ago)
- expect(subject.perform).to eq([projects[1].id])
+ expect(subject.perform(shard_name)).to eq([projects[1].id])
+ end
+
+ it 'excludes projects on another shard' do
+ projects = create_list(:project, 2, created_at: 1.week.ago)
+ projects[0].update_column(:repository_storage, 'other')
+
+ expect(subject.perform(shard_name)).to eq([projects[1].id])
end
it 'does nothing when repository checks are disabled' do
@@ -34,13 +46,28 @@ describe RepositoryCheck::BatchWorker do
stub_application_setting(repository_checks_enabled: false)
- expect(subject.perform).to eq(nil)
+ expect(subject.perform(shard_name)).to eq(nil)
+ end
+
+ it 'does nothing when shard is unhealthy' do
+ shard_name = 'broken'
+ create(:project, created_at: 1.week.ago, repository_storage: shard_name)
+
+ expect(subject.perform(shard_name)).to eq(nil)
end
it 'skips projects created less than 24 hours ago' do
project = create(:project)
project.update_column(:created_at, 23.hours.ago)
- expect(subject.perform).to eq([])
+ expect(subject.perform(shard_name)).to eq([])
+ end
+
+ it 'does not run if the exclusive lease is taken' do
+ allow(subject).to receive(:try_obtain_lease).and_return(false)
+
+ expect(subject).not_to receive(:perform_repository_checks)
+
+ subject.perform(shard_name)
end
end
diff --git a/spec/workers/repository_check/dispatch_worker_spec.rb b/spec/workers/repository_check/dispatch_worker_spec.rb
new file mode 100644
index 00000000000..7877429aa8f
--- /dev/null
+++ b/spec/workers/repository_check/dispatch_worker_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe RepositoryCheck::DispatchWorker do
+ subject { described_class.new }
+
+ it 'does nothing when repository checks are disabled' do
+ stub_application_setting(repository_checks_enabled: false)
+
+ expect(RepositoryCheck::BatchWorker).not_to receive(:perform_async)
+
+ subject.perform
+ end
+
+ it 'does nothing if the exclusive lease is taken' do
+ allow(subject).to receive(:try_obtain_lease).and_return(false)
+
+ expect(RepositoryCheck::BatchWorker).not_to receive(:perform_async)
+
+ subject.perform
+ end
+
+ it 'dispatches work to RepositoryCheck::BatchWorker' do
+ expect(RepositoryCheck::BatchWorker).to receive(:perform_async).at_least(:once)
+
+ subject.perform
+ end
+
+ context 'with unhealthy shard' do
+ let(:default_shard_name) { 'default' }
+ let(:unhealthy_shard_name) { 'unhealthy' }
+ let(:default_shard) { Gitlab::HealthChecks::Result.new(true, nil, shard: default_shard_name) }
+ let(:unhealthy_shard) { Gitlab::HealthChecks::Result.new(false, '14:Connect Failed', shard: unhealthy_shard_name) }
+
+ before do
+ allow(Gitlab::HealthChecks::GitalyCheck).to receive(:readiness).and_return([default_shard, unhealthy_shard])
+ end
+
+ it 'only triggers RepositoryCheck::BatchWorker for healthy shards' do
+ expect(RepositoryCheck::BatchWorker).to receive(:perform_async).with('default')
+
+ subject.perform
+ end
+ end
+end
diff --git a/spec/workers/repository_check/single_repository_worker_spec.rb b/spec/workers/repository_check/single_repository_worker_spec.rb
index a021235aed6..22fc64c1536 100644
--- a/spec/workers/repository_check/single_repository_worker_spec.rb
+++ b/spec/workers/repository_check/single_repository_worker_spec.rb
@@ -88,7 +88,9 @@ describe RepositoryCheck::SingleRepositoryWorker do
end
def break_wiki(project)
- break_repo(wiki_path(project))
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ break_repo(wiki_path(project))
+ end
end
def wiki_path(project)
@@ -96,7 +98,9 @@ describe RepositoryCheck::SingleRepositoryWorker do
end
def break_project(project)
- break_repo(project.repository.path_to_repo)
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ break_repo(project.repository.path_to_repo)
+ end
end
def break_repo(repo)
diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb
index 4b3c1736ea0..ac8716ecfb1 100644
--- a/spec/workers/repository_fork_worker_spec.rb
+++ b/spec/workers/repository_fork_worker_spec.rb
@@ -55,10 +55,15 @@ describe RepositoryForkWorker do
it 'flushes various caches' do
expect_fork_repository.and_return(true)
- expect_any_instance_of(Repository).to receive(:expire_emptiness_caches)
+ # Works around https://github.com/rspec/rspec-mocks/issues/910
+ expect(Project).to receive(:find).with(fork_project.id).and_return(fork_project)
+ expect(fork_project.repository).to receive(:expire_emptiness_caches)
.and_call_original
-
- expect_any_instance_of(Repository).to receive(:expire_exists_cache)
+ expect(fork_project.repository).to receive(:expire_exists_cache)
+ .and_call_original
+ expect(fork_project.wiki.repository).to receive(:expire_emptiness_caches)
+ .and_call_original
+ expect(fork_project.wiki.repository).to receive(:expire_exists_cache)
.and_call_original
perform!
@@ -87,13 +92,6 @@ describe RepositoryForkWorker do
end
it_behaves_like 'RepositoryForkWorker performing'
-
- it 'logs a message about forking with old-style arguments' do
- allow(Rails.logger).to receive(:info).with(anything) # To compensate for other logs
- expect(Rails.logger).to receive(:info).with("Project #{fork_project.id} is being forked using old-style arguments.")
-
- perform!
- end
end
end
end
diff --git a/spec/workers/repository_import_worker_spec.rb b/spec/workers/repository_import_worker_spec.rb
index 84d1b38ef19..d07e40377d4 100644
--- a/spec/workers/repository_import_worker_spec.rb
+++ b/spec/workers/repository_import_worker_spec.rb
@@ -22,8 +22,11 @@ describe RepositoryImportWorker do
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)
+ # Works around https://github.com/rspec/rspec-mocks/issues/910
+ expect(Project).to receive(:find).with(project.id).and_return(project)
+ expect(project.repository).to receive(:expire_emptiness_caches)
+ expect(project.wiki.repository).to receive(:expire_emptiness_caches)
+ expect(project).to receive(:import_finish)
subject.perform(project.id)
end
@@ -34,9 +37,11 @@ describe RepositoryImportWorker do
expect_any_instance_of(Projects::ImportService).to receive(:execute)
.and_return({ status: :ok })
- expect_any_instance_of(Project).to receive(:after_import).and_call_original
- expect_any_instance_of(Repository).to receive(:expire_emptiness_caches)
- expect_any_instance_of(Project).to receive(:import_finish)
+ # Works around https://github.com/rspec/rspec-mocks/issues/910
+ expect(Project).to receive(:find).with(project.id).and_return(project)
+ expect(project.repository).to receive(:expire_emptiness_caches)
+ expect(project.wiki.repository).to receive(:expire_emptiness_caches)
+ expect(project).to receive(:import_finish)
subject.perform(project.id)
end
@@ -46,7 +51,7 @@ describe RepositoryImportWorker do
it 'hide the credentials that were used in the import URL' do
error = %q{remote: Not Found fatal: repository 'https://user:pass@test.com/root/repoC.git/' not found }
- project.update_attributes(import_jid: '123')
+ project.update(import_jid: '123')
expect_any_instance_of(Projects::ImportService).to receive(:execute).and_return({ status: :error, message: error })
expect do
@@ -58,7 +63,7 @@ describe RepositoryImportWorker do
it 'updates the error on Import/Export' do
error = %q{remote: Not Found fatal: repository 'https://user:pass@test.com/root/repoC.git/' not found }
- project.update_attributes(import_jid: '123', import_type: 'gitlab_project')
+ project.update(import_jid: '123', import_type: 'gitlab_project')
expect_any_instance_of(Projects::ImportService).to receive(:execute).and_return({ status: :error, message: error })
expect do
diff --git a/spec/workers/repository_remove_remote_worker_spec.rb b/spec/workers/repository_remove_remote_worker_spec.rb
index f22d7c1d073..a653f6f926c 100644
--- a/spec/workers/repository_remove_remote_worker_spec.rb
+++ b/spec/workers/repository_remove_remote_worker_spec.rb
@@ -1,50 +1,59 @@
require 'rails_helper'
describe RepositoryRemoveRemoteWorker do
- subject(:worker) { described_class.new }
+ include ExclusiveLeaseHelpers
describe '#perform' do
- let(:remote_name) { 'joe'}
let!(:project) { create(:project, :repository) }
+ let(:remote_name) { 'joe'}
+ let(:lease_key) { "remove_remote_#{project.id}_#{remote_name}" }
+ let(:lease_timeout) { RepositoryRemoveRemoteWorker::LEASE_TIMEOUT }
- context 'when it cannot obtain lease' do
- it 'logs error' do
- allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain) { nil }
-
- expect_any_instance_of(Repository).not_to receive(:remove_remote)
- expect(worker).to receive(:log_error).with('Cannot obtain an exclusive lease. There must be another instance already in execution.')
-
- worker.perform(project.id, remote_name)
- end
+ it 'returns nil when project does not exist' do
+ expect(subject.perform(-1, 'remote_name')).to be_nil
end
- context 'when it gets the lease' do
+ context 'when project exists' do
before do
- allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(true)
+ allow(Project)
+ .to receive(:find_by)
+ .with(id: project.id)
+ .and_return(project)
end
- context 'when project does not exist' do
- it 'returns nil' do
- expect(worker.perform(-1, 'remote_name')).to be_nil
- end
- end
+ it 'does not remove remote when cannot obtain lease' do
+ stub_exclusive_lease_taken(lease_key, timeout: lease_timeout)
- context 'when project exists' do
- it 'removes remote from repository' do
- masterrev = project.repository.find_branch('master').dereferenced_target
+ expect(project.repository)
+ .not_to receive(:remove_remote)
- create_remote_branch(remote_name, 'remote_branch', masterrev)
+ expect(subject)
+ .to receive(:log_error)
+ .with('Cannot obtain an exclusive lease. There must be another instance already in execution.')
- expect_any_instance_of(Repository).to receive(:remove_remote).with(remote_name).and_call_original
+ subject.perform(project.id, remote_name)
+ end
+
+ it 'removes remote from repository when obtain a lease' do
+ stub_exclusive_lease(lease_key, timeout: lease_timeout)
+ masterrev = project.repository.find_branch('master').dereferenced_target
+ create_remote_branch(remote_name, 'remote_branch', masterrev)
+
+ expect(project.repository)
+ .to receive(:remove_remote)
+ .with(remote_name)
+ .and_call_original
- worker.perform(project.id, remote_name)
- end
+ subject.perform(project.id, remote_name)
end
end
end
def create_remote_branch(remote_name, branch_name, target)
- rugged = project.repository.rugged
+ rugged = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ project.repository.rugged
+ end
+
rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", target.id)
end
end
diff --git a/spec/workers/repository_update_remote_mirror_worker_spec.rb b/spec/workers/repository_update_remote_mirror_worker_spec.rb
index 152ba2509b9..4f1ad2474f5 100644
--- a/spec/workers/repository_update_remote_mirror_worker_spec.rb
+++ b/spec/workers/repository_update_remote_mirror_worker_spec.rb
@@ -13,7 +13,7 @@ describe RepositoryUpdateRemoteMirrorWorker do
describe '#perform' do
context 'with status none' do
before do
- remote_mirror.update_attributes(update_status: 'none')
+ remote_mirror.update(update_status: 'none')
end
it 'sets status as finished when update remote mirror service executes successfully' do
@@ -34,7 +34,7 @@ describe RepositoryUpdateRemoteMirrorWorker do
end
it 'does nothing if last_update_started_at is higher than the time the job was scheduled in' do
- remote_mirror.update_attributes(last_update_started_at: Time.now)
+ remote_mirror.update(last_update_started_at: Time.now)
expect_any_instance_of(RemoteMirror).to receive(:updated_since?).with(scheduled_time).and_return(true)
expect_any_instance_of(Projects::UpdateRemoteMirrorService).not_to receive(:execute).with(remote_mirror)
@@ -56,7 +56,7 @@ describe RepositoryUpdateRemoteMirrorWorker do
context 'with another worker already running' do
before do
- remote_mirror.update_attributes(update_status: 'started')
+ remote_mirror.update(update_status: 'started')
end
it 'raises RemoteMirrorUpdateAlreadyInProgressError' do
@@ -68,11 +68,11 @@ describe RepositoryUpdateRemoteMirrorWorker do
context 'with status failed' do
before do
- remote_mirror.update_attributes(update_status: 'failed')
+ remote_mirror.update(update_status: 'failed')
end
it 'sets status as finished if last_update_started_at is higher than the time the job was scheduled in' do
- remote_mirror.update_attributes(last_update_started_at: Time.now)
+ remote_mirror.update(last_update_started_at: Time.now)
expect_any_instance_of(RemoteMirror).to receive(:updated_since?).with(scheduled_time).and_return(false)
expect_any_instance_of(Projects::UpdateRemoteMirrorService).to receive(:execute).with(remote_mirror).and_return(status: :success)
diff --git a/spec/workers/storage_migrator_worker_spec.rb b/spec/workers/storage_migrator_worker_spec.rb
index ff625164142..815432aacce 100644
--- a/spec/workers/storage_migrator_worker_spec.rb
+++ b/spec/workers/storage_migrator_worker_spec.rb
@@ -2,29 +2,24 @@ require 'spec_helper'
describe StorageMigratorWorker do
subject(:worker) { described_class.new }
- let(:projects) { create_list(:project, 2, :legacy_storage) }
+ let(:projects) { create_list(:project, 2, :legacy_storage, :empty_repo) }
+ let(:ids) { projects.map(&:id) }
describe '#perform' do
- let(:ids) { projects.map(&:id) }
+ it 'delegates to MigratorService' do
+ expect_any_instance_of(Gitlab::HashedStorage::Migrator).to receive(:bulk_migrate).with(5, 10)
- it 'enqueue jobs to ProjectMigrateHashedStorageWorker' do
- expect(ProjectMigrateHashedStorageWorker).to receive(:perform_async).twice
-
- worker.perform(ids.min, ids.max)
+ worker.perform(5, 10)
end
- it 'sets projects as read only' do
- allow(ProjectMigrateHashedStorageWorker).to receive(:perform_async).twice
- worker.perform(ids.min, ids.max)
+ it 'migrates projects in the specified range' do
+ Sidekiq::Testing.inline! do
+ worker.perform(ids.min, ids.max)
+ end
projects.each do |project|
- expect(project.reload.repository_read_only?).to be_truthy
+ expect(project.reload.hashed_storage?(:attachments)).to be_truthy
end
end
-
- it 'rescues and log exceptions' do
- allow_any_instance_of(Project).to receive(:migrate_to_hashed_storage!).and_raise(StandardError)
- expect { worker.perform(ids.min, ids.max) }.not_to raise_error
- end
end
end
diff --git a/spec/workers/stuck_ci_jobs_worker_spec.rb b/spec/workers/stuck_ci_jobs_worker_spec.rb
index bdc64c6785b..856886e3df5 100644
--- a/spec/workers/stuck_ci_jobs_worker_spec.rb
+++ b/spec/workers/stuck_ci_jobs_worker_spec.rb
@@ -1,14 +1,21 @@
require 'spec_helper'
describe StuckCiJobsWorker do
+ include ExclusiveLeaseHelpers
+
let!(:runner) { create :ci_runner }
let!(:job) { create :ci_build, runner: runner }
- let(:worker) { described_class.new }
- let(:exclusive_lease_uuid) { SecureRandom.uuid }
+ let(:trace_lease_key) { "trace:archive:#{job.id}" }
+ let(:trace_lease_uuid) { SecureRandom.uuid }
+ let(:worker_lease_key) { StuckCiJobsWorker::EXCLUSIVE_LEASE_KEY }
+ let(:worker_lease_uuid) { SecureRandom.uuid }
+
+ subject(:worker) { described_class.new }
before do
+ stub_exclusive_lease(worker_lease_key, worker_lease_uuid)
+ stub_exclusive_lease(trace_lease_key, trace_lease_uuid)
job.update!(status: status, updated_at: updated_at)
- allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(exclusive_lease_uuid)
end
shared_examples 'job is dropped' do
@@ -44,16 +51,19 @@ describe StuckCiJobsWorker do
context 'when job was not updated for more than 1 day ago' do
let(:updated_at) { 2.days.ago }
+
it_behaves_like 'job is dropped'
end
context 'when job was updated in less than 1 day ago' do
let(:updated_at) { 6.hours.ago }
+
it_behaves_like 'job is unchanged'
end
context 'when job was not updated for more than 1 hour ago' do
let(:updated_at) { 2.hours.ago }
+
it_behaves_like 'job is unchanged'
end
end
@@ -65,11 +75,14 @@ describe StuckCiJobsWorker do
context 'when job was not updated for more than 1 hour ago' do
let(:updated_at) { 2.hours.ago }
+
it_behaves_like 'job is dropped'
end
- context 'when job was updated in less than 1 hour ago' do
+ context 'when job was updated in less than 1
+ hour ago' do
let(:updated_at) { 30.minutes.ago }
+
it_behaves_like 'job is unchanged'
end
end
@@ -80,11 +93,13 @@ describe StuckCiJobsWorker do
context 'when job was not updated for more than 1 hour ago' do
let(:updated_at) { 2.hours.ago }
+
it_behaves_like 'job is dropped'
end
context 'when job was updated in less than 1 hour ago' do
let(:updated_at) { 30.minutes.ago }
+
it_behaves_like 'job is unchanged'
end
end
@@ -93,6 +108,7 @@ describe StuckCiJobsWorker do
context "when job is #{status}" do
let(:status) { status }
let(:updated_at) { 2.days.ago }
+
it_behaves_like 'job is unchanged'
end
end
@@ -119,20 +135,26 @@ describe StuckCiJobsWorker do
it 'is guard by exclusive lease when executed concurrently' do
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)
+
+ stub_exclusive_lease_taken(worker_lease_key)
+
worker2.perform
end
it 'can be executed in sequence' do
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
- it 'cancels exclusive lease after worker perform' do
- expect(Gitlab::ExclusiveLease).to receive(:cancel).with(described_class::EXCLUSIVE_LEASE_KEY, exclusive_lease_uuid)
+ it 'cancels exclusive leases after worker perform' do
+ expect_to_cancel_exclusive_lease(trace_lease_key, trace_lease_uuid)
+ expect_to_cancel_exclusive_lease(worker_lease_key, worker_lease_uuid)
+
worker.perform
end
end
diff --git a/spec/workers/stuck_import_jobs_worker_spec.rb b/spec/workers/stuck_import_jobs_worker_spec.rb
index af7675c8cab..2169c14218b 100644
--- a/spec/workers/stuck_import_jobs_worker_spec.rb
+++ b/spec/workers/stuck_import_jobs_worker_spec.rb
@@ -51,7 +51,7 @@ describe StuckImportJobsWorker do
let(:project) { create(:project, :import_scheduled) }
before do
- project.import_state.update_attributes(jid: '123')
+ project.import_state.update(jid: '123')
end
end
end
@@ -61,7 +61,7 @@ describe StuckImportJobsWorker do
let(:project) { create(:project, :import_started) }
before do
- project.import_state.update_attributes(jid: '123')
+ project.import_state.update(jid: '123')
end
end
end
diff --git a/spec/workers/update_merge_requests_worker_spec.rb b/spec/workers/update_merge_requests_worker_spec.rb
index 0fa19ac84bb..0b553db0ca4 100644
--- a/spec/workers/update_merge_requests_worker_spec.rb
+++ b/spec/workers/update_merge_requests_worker_spec.rb
@@ -18,8 +18,9 @@ describe UpdateMergeRequestsWorker do
end
it 'executes MergeRequests::RefreshService with expected values' do
- expect(MergeRequests::RefreshService).to receive(:new).with(project, user).and_call_original
- expect_any_instance_of(MergeRequests::RefreshService).to receive(:execute).with(oldrev, newrev, ref)
+ expect_next_instance_of(MergeRequests::RefreshService, project, user) do |refresh_service|
+ expect(refresh_service).to receive(:execute).with(oldrev, newrev, ref)
+ end
perform
end
diff --git a/vendor/assets/javascripts/Sortable.js b/vendor/assets/javascripts/Sortable.js
deleted file mode 100644
index f9e57bcb855..00000000000
--- a/vendor/assets/javascripts/Sortable.js
+++ /dev/null
@@ -1,1374 +0,0 @@
-/**!
- * Sortable
- * @author RubaXa <trash@rubaxa.org>
- * @license MIT
- */
-
-(function sortableModule(factory) {
- "use strict";
-
- if (typeof define === "function" && define.amd) {
- define(factory);
- }
- else if (typeof module != "undefined" && typeof module.exports != "undefined") {
- module.exports = factory();
- }
- else if (typeof Package !== "undefined") {
- //noinspection JSUnresolvedVariable
- Sortable = factory(); // export for Meteor.js
- }
- else {
- /* jshint sub:true */
- window["Sortable"] = factory();
- }
-})(function sortableFactory() {
- "use strict";
-
- if (typeof window == "undefined" || !window.document) {
- return function sortableError() {
- throw new Error("Sortable.js requires a window with a document");
- };
- }
-
- var dragEl,
- parentEl,
- ghostEl,
- cloneEl,
- rootEl,
- nextEl,
-
- scrollEl,
- scrollParentEl,
- scrollCustomFn,
-
- lastEl,
- lastCSS,
- lastParentCSS,
-
- oldIndex,
- newIndex,
-
- activeGroup,
- putSortable,
-
- autoScroll = {},
-
- tapEvt,
- touchEvt,
-
- moved,
-
- /** @const */
- RSPACE = /\s+/g,
-
- expando = 'Sortable' + (new Date).getTime(),
-
- win = window,
- document = win.document,
- parseInt = win.parseInt,
-
- $ = win.jQuery || win.Zepto,
- Polymer = win.Polymer,
-
- supportDraggable = !!('draggable' in document.createElement('div')),
- supportCssPointerEvents = (function (el) {
- // false when IE11
- if (!!navigator.userAgent.match(/Trident.*rv[ :]?11\./)) {
- return false;
- }
- el = document.createElement('x');
- el.style.cssText = 'pointer-events:auto';
- return el.style.pointerEvents === 'auto';
- })(),
-
- _silent = false,
-
- abs = Math.abs,
- min = Math.min,
- slice = [].slice,
-
- touchDragOverListeners = [],
-
- _autoScroll = _throttle(function (/**Event*/evt, /**Object*/options, /**HTMLElement*/rootEl) {
- // Bug: https://bugzilla.mozilla.org/show_bug.cgi?id=505521
- if (rootEl && options.scroll) {
- var el,
- rect,
- sens = options.scrollSensitivity,
- speed = options.scrollSpeed,
-
- x = evt.clientX,
- y = evt.clientY,
-
- winWidth = window.innerWidth,
- winHeight = window.innerHeight,
-
- vx,
- vy,
-
- scrollOffsetX,
- scrollOffsetY
- ;
-
- // Delect scrollEl
- if (scrollParentEl !== rootEl) {
- scrollEl = options.scroll;
- scrollParentEl = rootEl;
- scrollCustomFn = options.scrollFn;
-
- if (scrollEl === true) {
- scrollEl = rootEl;
-
- do {
- if ((scrollEl.offsetWidth < scrollEl.scrollWidth) ||
- (scrollEl.offsetHeight < scrollEl.scrollHeight)
- ) {
- break;
- }
- /* jshint boss:true */
- } while (scrollEl = scrollEl.parentNode);
- }
- }
-
- if (scrollEl) {
- el = scrollEl;
- rect = scrollEl.getBoundingClientRect();
- vx = (abs(rect.right - x) <= sens) - (abs(rect.left - x) <= sens);
- vy = (abs(rect.bottom - y) <= sens) - (abs(rect.top - y) <= sens);
- }
-
-
- if (!(vx || vy)) {
- vx = (winWidth - x <= sens) - (x <= sens);
- vy = (winHeight - y <= sens) - (y <= sens);
-
- /* jshint expr:true */
- (vx || vy) && (el = win);
- }
-
-
- if (autoScroll.vx !== vx || autoScroll.vy !== vy || autoScroll.el !== el) {
- autoScroll.el = el;
- autoScroll.vx = vx;
- autoScroll.vy = vy;
-
- clearInterval(autoScroll.pid);
-
- if (el) {
- autoScroll.pid = setInterval(function () {
- scrollOffsetY = vy ? vy * speed : 0;
- scrollOffsetX = vx ? vx * speed : 0;
-
- if ('function' === typeof(scrollCustomFn)) {
- return scrollCustomFn.call(_this, scrollOffsetX, scrollOffsetY, evt);
- }
-
- if (el === win) {
- win.scrollTo(win.pageXOffset + scrollOffsetX, win.pageYOffset + scrollOffsetY);
- } else {
- el.scrollTop += scrollOffsetY;
- el.scrollLeft += scrollOffsetX;
- }
- }, 24);
- }
- }
- }
- }, 30),
-
- _prepareGroup = function (options) {
- function toFn(value, pull) {
- if (value === void 0 || value === true) {
- value = group.name;
- }
-
- if (typeof value === 'function') {
- return value;
- } else {
- return function (to, from) {
- var fromGroup = from.options.group.name;
-
- return pull
- ? value
- : value && (value.join
- ? value.indexOf(fromGroup) > -1
- : (fromGroup == value)
- );
- };
- }
- }
-
- var group = {};
- var originalGroup = options.group;
-
- if (!originalGroup || typeof originalGroup != 'object') {
- originalGroup = {name: originalGroup};
- }
-
- group.name = originalGroup.name;
- group.checkPull = toFn(originalGroup.pull, true);
- group.checkPut = toFn(originalGroup.put);
-
- options.group = group;
- }
- ;
-
-
-
- /**
- * @class Sortable
- * @param {HTMLElement} el
- * @param {Object} [options]
- */
- function Sortable(el, options) {
- if (!(el && el.nodeType && el.nodeType === 1)) {
- throw 'Sortable: `el` must be HTMLElement, and not ' + {}.toString.call(el);
- }
-
- this.el = el; // root element
- this.options = options = _extend({}, options);
-
-
- // Export instance
- el[expando] = this;
-
-
- // Default options
- var defaults = {
- group: Math.random(),
- sort: true,
- disabled: false,
- store: null,
- handle: null,
- scroll: true,
- scrollSensitivity: 30,
- scrollSpeed: 10,
- draggable: /[uo]l/i.test(el.nodeName) ? 'li' : '>*',
- ghostClass: 'sortable-ghost',
- chosenClass: 'sortable-chosen',
- dragClass: 'sortable-drag',
- ignore: 'a, img',
- filter: null,
- animation: 0,
- setData: function (dataTransfer, dragEl) {
- dataTransfer.setData('Text', dragEl.textContent);
- },
- dropBubble: false,
- dragoverBubble: false,
- dataIdAttr: 'data-id',
- delay: 0,
- forceFallback: false,
- fallbackClass: 'sortable-fallback',
- fallbackOnBody: false,
- fallbackTolerance: 0,
- fallbackOffset: {x: 0, y: 0}
- };
-
-
- // Set default options
- for (var name in defaults) {
- !(name in options) && (options[name] = defaults[name]);
- }
-
- _prepareGroup(options);
-
- // Bind all private methods
- for (var fn in this) {
- if (fn.charAt(0) === '_' && typeof this[fn] === 'function') {
- this[fn] = this[fn].bind(this);
- }
- }
-
- // Setup drag mode
- this.nativeDraggable = options.forceFallback ? false : supportDraggable;
-
- // Bind events
- _on(el, 'mousedown', this._onTapStart);
- _on(el, 'touchstart', this._onTapStart);
-
- if (this.nativeDraggable) {
- _on(el, 'dragover', this);
- _on(el, 'dragenter', this);
- }
-
- touchDragOverListeners.push(this._onDragOver);
-
- // Restore sorting
- options.store && this.sort(options.store.get(this));
- }
-
-
- Sortable.prototype = /** @lends Sortable.prototype */ {
- constructor: Sortable,
-
- _onTapStart: function (/** Event|TouchEvent */evt) {
- var _this = this,
- el = this.el,
- options = this.options,
- type = evt.type,
- touch = evt.touches && evt.touches[0],
- target = (touch || evt).target,
- originalTarget = evt.target.shadowRoot && evt.path[0] || target,
- filter = options.filter,
- startIndex;
-
- // Don't trigger start event when an element is been dragged, otherwise the evt.oldindex always wrong when set option.group.
- if (dragEl) {
- return;
- }
-
- if (type === 'mousedown' && evt.button !== 0 || options.disabled) {
- return; // only left button or enabled
- }
-
- if (options.handle && !_closest(originalTarget, options.handle, el)) {
- return;
- }
-
- target = _closest(target, options.draggable, el);
-
- if (!target) {
- return;
- }
-
- // Get the index of the dragged element within its parent
- startIndex = _index(target, options.draggable);
-
- // Check filter
- if (typeof filter === 'function') {
- if (filter.call(this, evt, target, this)) {
- _dispatchEvent(_this, originalTarget, 'filter', target, el, startIndex);
- evt.preventDefault();
- return; // cancel dnd
- }
- }
- else if (filter) {
- filter = filter.split(',').some(function (criteria) {
- criteria = _closest(originalTarget, criteria.trim(), el);
-
- if (criteria) {
- _dispatchEvent(_this, criteria, 'filter', target, el, startIndex);
- return true;
- }
- });
-
- if (filter) {
- evt.preventDefault();
- return; // cancel dnd
- }
- }
-
- // Prepare `dragstart`
- this._prepareDragStart(evt, touch, target, startIndex);
- },
-
- _prepareDragStart: function (/** Event */evt, /** Touch */touch, /** HTMLElement */target, /** Number */startIndex) {
- var _this = this,
- el = _this.el,
- options = _this.options,
- ownerDocument = el.ownerDocument,
- dragStartFn;
-
- if (target && !dragEl && (target.parentNode === el)) {
- tapEvt = evt;
-
- rootEl = el;
- dragEl = target;
- parentEl = dragEl.parentNode;
- nextEl = dragEl.nextSibling;
- activeGroup = options.group;
- oldIndex = startIndex;
-
- this._lastX = (touch || evt).clientX;
- this._lastY = (touch || evt).clientY;
-
- dragEl.style['will-change'] = 'transform';
-
- dragStartFn = function () {
- // Delayed drag has been triggered
- // we can re-enable the events: touchmove/mousemove
- _this._disableDelayedDrag();
-
- // Make the element draggable
- dragEl.draggable = _this.nativeDraggable;
-
- // Chosen item
- _toggleClass(dragEl, options.chosenClass, true);
-
- // Bind the events: dragstart/dragend
- _this._triggerDragStart(touch);
-
- // Drag start event
- _dispatchEvent(_this, rootEl, 'choose', dragEl, rootEl, oldIndex);
- };
-
- // Disable "draggable"
- options.ignore.split(',').forEach(function (criteria) {
- _find(dragEl, criteria.trim(), _disableDraggable);
- });
-
- _on(ownerDocument, 'mouseup', _this._onDrop);
- _on(ownerDocument, 'touchend', _this._onDrop);
- _on(ownerDocument, 'touchcancel', _this._onDrop);
-
- if (options.delay) {
- // If the user moves the pointer or let go the click or touch
- // before the delay has been reached:
- // disable the delayed drag
- _on(ownerDocument, 'mouseup', _this._disableDelayedDrag);
- _on(ownerDocument, 'touchend', _this._disableDelayedDrag);
- _on(ownerDocument, 'touchcancel', _this._disableDelayedDrag);
- _on(ownerDocument, 'mousemove', _this._disableDelayedDrag);
- _on(ownerDocument, 'touchmove', _this._disableDelayedDrag);
-
- _this._dragStartTimer = setTimeout(dragStartFn, options.delay);
- } else {
- dragStartFn();
- }
- }
- },
-
- _disableDelayedDrag: function () {
- var ownerDocument = this.el.ownerDocument;
-
- clearTimeout(this._dragStartTimer);
- _off(ownerDocument, 'mouseup', this._disableDelayedDrag);
- _off(ownerDocument, 'touchend', this._disableDelayedDrag);
- _off(ownerDocument, 'touchcancel', this._disableDelayedDrag);
- _off(ownerDocument, 'mousemove', this._disableDelayedDrag);
- _off(ownerDocument, 'touchmove', this._disableDelayedDrag);
- },
-
- _triggerDragStart: function (/** Touch */touch) {
- if (touch) {
- // Touch device support
- tapEvt = {
- target: dragEl,
- clientX: touch.clientX,
- clientY: touch.clientY
- };
-
- this._onDragStart(tapEvt, 'touch');
- }
- else if (!this.nativeDraggable) {
- this._onDragStart(tapEvt, true);
- }
- else {
- _on(dragEl, 'dragend', this);
- _on(rootEl, 'dragstart', this._onDragStart);
- }
-
- try {
- if (document.selection) {
- // Timeout neccessary for IE9
- setTimeout(function () {
- document.selection.empty();
- });
- } else {
- window.getSelection().removeAllRanges();
- }
- } catch (err) {
- }
- },
-
- _dragStarted: function () {
- if (rootEl && dragEl) {
- var options = this.options;
-
- // Apply effect
- _toggleClass(dragEl, options.ghostClass, true);
- _toggleClass(dragEl, options.dragClass, false);
-
- Sortable.active = this;
-
- // Drag start event
- _dispatchEvent(this, rootEl, 'start', dragEl, rootEl, oldIndex);
- }
- },
-
- _emulateDragOver: function () {
- if (touchEvt) {
- if (this._lastX === touchEvt.clientX && this._lastY === touchEvt.clientY) {
- return;
- }
-
- this._lastX = touchEvt.clientX;
- this._lastY = touchEvt.clientY;
-
- if (!supportCssPointerEvents) {
- _css(ghostEl, 'display', 'none');
- }
-
- var target = document.elementFromPoint(touchEvt.clientX, touchEvt.clientY),
- parent = target,
- i = touchDragOverListeners.length;
-
- if (parent) {
- do {
- if (parent[expando]) {
- while (i--) {
- touchDragOverListeners[i]({
- clientX: touchEvt.clientX,
- clientY: touchEvt.clientY,
- target: target,
- rootEl: parent
- });
- }
-
- break;
- }
-
- target = parent; // store last element
- }
- /* jshint boss:true */
- while (parent = parent.parentNode);
- }
-
- if (!supportCssPointerEvents) {
- _css(ghostEl, 'display', '');
- }
- }
- },
-
-
- _onTouchMove: function (/**TouchEvent*/evt) {
- if (tapEvt) {
- var options = this.options,
- fallbackTolerance = options.fallbackTolerance,
- fallbackOffset = options.fallbackOffset,
- touch = evt.touches ? evt.touches[0] : evt,
- dx = (touch.clientX - tapEvt.clientX) + fallbackOffset.x,
- dy = (touch.clientY - tapEvt.clientY) + fallbackOffset.y,
- translate3d = evt.touches ? 'translate3d(' + dx + 'px,' + dy + 'px,0)' : 'translate(' + dx + 'px,' + dy + 'px)';
-
- // only set the status to dragging, when we are actually dragging
- if (!Sortable.active) {
- if (fallbackTolerance &&
- min(abs(touch.clientX - this._lastX), abs(touch.clientY - this._lastY)) < fallbackTolerance
- ) {
- return;
- }
-
- this._dragStarted();
- }
-
- // as well as creating the ghost element on the document body
- this._appendGhost();
-
- moved = true;
- touchEvt = touch;
-
- _css(ghostEl, 'webkitTransform', translate3d);
- _css(ghostEl, 'mozTransform', translate3d);
- _css(ghostEl, 'msTransform', translate3d);
- _css(ghostEl, 'transform', translate3d);
-
- evt.preventDefault();
- }
- },
-
- _appendGhost: function () {
- if (!ghostEl) {
- var rect = dragEl.getBoundingClientRect(),
- css = _css(dragEl),
- options = this.options,
- ghostRect;
-
- ghostEl = dragEl.cloneNode(true);
-
- _toggleClass(ghostEl, options.ghostClass, false);
- _toggleClass(ghostEl, options.fallbackClass, true);
- _toggleClass(ghostEl, options.dragClass, true);
-
- _css(ghostEl, 'top', rect.top - parseInt(css.marginTop, 10));
- _css(ghostEl, 'left', rect.left - parseInt(css.marginLeft, 10));
- _css(ghostEl, 'width', rect.width);
- _css(ghostEl, 'height', rect.height);
- _css(ghostEl, 'opacity', '0.8');
- _css(ghostEl, 'position', 'fixed');
- _css(ghostEl, 'zIndex', '100000');
- _css(ghostEl, 'pointerEvents', 'none');
-
- options.fallbackOnBody && document.body.appendChild(ghostEl) || rootEl.appendChild(ghostEl);
-
- // Fixing dimensions.
- ghostRect = ghostEl.getBoundingClientRect();
- _css(ghostEl, 'width', rect.width * 2 - ghostRect.width);
- _css(ghostEl, 'height', rect.height * 2 - ghostRect.height);
- }
- },
-
- _onDragStart: function (/**Event*/evt, /**boolean*/useFallback) {
- var dataTransfer = evt.dataTransfer,
- options = this.options;
-
- this._offUpEvents();
-
- if (activeGroup.checkPull(this, this, dragEl, evt) == 'clone') {
- cloneEl = _clone(dragEl);
- _css(cloneEl, 'display', 'none');
- rootEl.insertBefore(cloneEl, dragEl);
- _dispatchEvent(this, rootEl, 'clone', dragEl);
- }
-
- _toggleClass(dragEl, options.dragClass, true);
-
- if (useFallback) {
- if (useFallback === 'touch') {
- // Bind touch events
- _on(document, 'touchmove', this._onTouchMove);
- _on(document, 'touchend', this._onDrop);
- _on(document, 'touchcancel', this._onDrop);
- } else {
- // Old brwoser
- _on(document, 'mousemove', this._onTouchMove);
- _on(document, 'mouseup', this._onDrop);
- }
-
- this._loopId = setInterval(this._emulateDragOver, 50);
- }
- else {
- if (dataTransfer) {
- dataTransfer.effectAllowed = 'move';
- options.setData && options.setData.call(this, dataTransfer, dragEl);
- }
-
- _on(document, 'drop', this);
- setTimeout(this._dragStarted, 0);
- }
- },
-
- _onDragOver: function (/**Event*/evt) {
- var el = this.el,
- target,
- dragRect,
- targetRect,
- revert,
- options = this.options,
- group = options.group,
- activeSortable = Sortable.active,
- isOwner = (activeGroup === group),
- canSort = options.sort;
-
- if (evt.preventDefault !== void 0) {
- evt.preventDefault();
- !options.dragoverBubble && evt.stopPropagation();
- }
-
- moved = true;
-
- if (activeGroup && !options.disabled &&
- (isOwner
- ? canSort || (revert = !rootEl.contains(dragEl)) // Reverting item into the original list
- : (
- putSortable === this ||
- activeGroup.checkPull(this, activeSortable, dragEl, evt) && group.checkPut(this, activeSortable, dragEl, evt)
- )
- ) &&
- (evt.rootEl === void 0 || evt.rootEl === this.el) // touch fallback
- ) {
- // Smart auto-scrolling
- _autoScroll(evt, options, this.el);
-
- if (_silent) {
- return;
- }
-
- target = _closest(evt.target, options.draggable, el);
- dragRect = dragEl.getBoundingClientRect();
- putSortable = this;
-
- if (revert) {
- _cloneHide(true);
- parentEl = rootEl; // actualization
-
- if (cloneEl || nextEl) {
- rootEl.insertBefore(dragEl, cloneEl || nextEl);
- }
- else if (!canSort) {
- rootEl.appendChild(dragEl);
- }
-
- return;
- }
-
-
- if ((el.children.length === 0) || (el.children[0] === ghostEl) ||
- (el === evt.target) && (target = _ghostIsLast(el, evt))
- ) {
- if (target) {
- if (target.animated) {
- return;
- }
-
- targetRect = target.getBoundingClientRect();
- }
-
- _cloneHide(isOwner);
-
- if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt) !== false) {
- if (!dragEl.contains(el)) {
- el.appendChild(dragEl);
- parentEl = el; // actualization
- }
-
- this._animate(dragRect, dragEl);
- target && this._animate(targetRect, target);
- }
- }
- else if (target && !target.animated && target !== dragEl && (target.parentNode[expando] !== void 0)) {
- if (lastEl !== target) {
- lastEl = target;
- lastCSS = _css(target);
- lastParentCSS = _css(target.parentNode);
- }
-
- targetRect = target.getBoundingClientRect();
-
- var width = targetRect.right - targetRect.left,
- height = targetRect.bottom - targetRect.top,
- floating = /left|right|inline/.test(lastCSS.cssFloat + lastCSS.display)
- || (lastParentCSS.display == 'flex' && lastParentCSS['flex-direction'].indexOf('row') === 0),
- isWide = (target.offsetWidth > dragEl.offsetWidth),
- isLong = (target.offsetHeight > dragEl.offsetHeight),
- halfway = (floating ? (evt.clientX - targetRect.left) / width : (evt.clientY - targetRect.top) / height) > 0.5,
- nextSibling = target.nextElementSibling,
- moveVector = _onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt),
- after
- ;
-
- if (moveVector !== false) {
- _silent = true;
- setTimeout(_unsilent, 30);
-
- _cloneHide(isOwner);
-
- if (moveVector === 1 || moveVector === -1) {
- after = (moveVector === 1);
- }
- else if (floating) {
- var elTop = dragEl.offsetTop,
- tgTop = target.offsetTop;
-
- if (elTop === tgTop) {
- after = (target.previousElementSibling === dragEl) && !isWide || halfway && isWide;
- }
- else if (target.previousElementSibling === dragEl || dragEl.previousElementSibling === target) {
- after = (evt.clientY - targetRect.top) / height > 0.5;
- } else {
- after = tgTop > elTop;
- }
- } else {
- after = (nextSibling !== dragEl) && !isLong || halfway && isLong;
- }
-
- if (!dragEl.contains(el)) {
- if (after && !nextSibling) {
- el.appendChild(dragEl);
- } else {
- target.parentNode.insertBefore(dragEl, after ? nextSibling : target);
- }
- }
-
- parentEl = dragEl.parentNode; // actualization
-
- this._animate(dragRect, dragEl);
- this._animate(targetRect, target);
- }
- }
- }
- },
-
- _animate: function (prevRect, target) {
- var ms = this.options.animation;
-
- if (ms) {
- var currentRect = target.getBoundingClientRect();
-
- _css(target, 'transition', 'none');
- _css(target, 'transform', 'translate3d('
- + (prevRect.left - currentRect.left) + 'px,'
- + (prevRect.top - currentRect.top) + 'px,0)'
- );
-
- target.offsetWidth; // repaint
-
- _css(target, 'transition', 'all ' + ms + 'ms');
- _css(target, 'transform', 'translate3d(0,0,0)');
-
- clearTimeout(target.animated);
- target.animated = setTimeout(function () {
- _css(target, 'transition', '');
- _css(target, 'transform', '');
- target.animated = false;
- }, ms);
- }
- },
-
- _offUpEvents: function () {
- var ownerDocument = this.el.ownerDocument;
-
- _off(document, 'touchmove', this._onTouchMove);
- _off(ownerDocument, 'mouseup', this._onDrop);
- _off(ownerDocument, 'touchend', this._onDrop);
- _off(ownerDocument, 'touchcancel', this._onDrop);
- },
-
- _onDrop: function (/**Event*/evt) {
- var el = this.el,
- options = this.options;
-
- clearInterval(this._loopId);
- clearInterval(autoScroll.pid);
- clearTimeout(this._dragStartTimer);
-
- // Unbind events
- _off(document, 'mousemove', this._onTouchMove);
-
- if (this.nativeDraggable) {
- _off(document, 'drop', this);
- _off(el, 'dragstart', this._onDragStart);
- }
-
- this._offUpEvents();
-
- if (evt) {
- if (moved) {
- evt.preventDefault();
- !options.dropBubble && evt.stopPropagation();
- }
-
- ghostEl && ghostEl.parentNode.removeChild(ghostEl);
-
- if (dragEl) {
- if (this.nativeDraggable) {
- _off(dragEl, 'dragend', this);
- }
-
- _disableDraggable(dragEl);
- dragEl.style['will-change'] = '';
-
- // Remove class's
- _toggleClass(dragEl, this.options.ghostClass, false);
- _toggleClass(dragEl, this.options.chosenClass, false);
-
- if (rootEl !== parentEl) {
- newIndex = _index(dragEl, options.draggable);
-
- if (newIndex >= 0) {
-
- // Add event
- _dispatchEvent(null, parentEl, 'add', dragEl, rootEl, oldIndex, newIndex);
-
- // Remove event
- _dispatchEvent(this, rootEl, 'remove', dragEl, rootEl, oldIndex, newIndex);
-
- // drag from one list and drop into another
- _dispatchEvent(null, parentEl, 'sort', dragEl, rootEl, oldIndex, newIndex);
- _dispatchEvent(this, rootEl, 'sort', dragEl, rootEl, oldIndex, newIndex);
- }
- }
- else {
- // Remove clone
- cloneEl && cloneEl.parentNode.removeChild(cloneEl);
-
- if (dragEl.nextSibling !== nextEl) {
- // Get the index of the dragged element within its parent
- newIndex = _index(dragEl, options.draggable);
-
- if (newIndex >= 0) {
- // drag & drop within the same list
- _dispatchEvent(this, rootEl, 'update', dragEl, rootEl, oldIndex, newIndex);
- _dispatchEvent(this, rootEl, 'sort', dragEl, rootEl, oldIndex, newIndex);
- }
- }
- }
-
- if (Sortable.active) {
- /* jshint eqnull:true */
- if (newIndex == null || newIndex === -1) {
- newIndex = oldIndex;
- }
-
- _dispatchEvent(this, rootEl, 'end', dragEl, rootEl, oldIndex, newIndex);
-
- // Save sorting
- this.save();
- }
- }
-
- }
-
- this._nulling();
- },
-
- _nulling: function() {
- rootEl =
- dragEl =
- parentEl =
- ghostEl =
- nextEl =
- cloneEl =
-
- scrollEl =
- scrollParentEl =
-
- tapEvt =
- touchEvt =
-
- moved =
- newIndex =
-
- lastEl =
- lastCSS =
-
- putSortable =
- activeGroup =
- Sortable.active = null;
- },
-
- handleEvent: function (/**Event*/evt) {
- var type = evt.type;
-
- if (type === 'dragover' || type === 'dragenter') {
- if (dragEl) {
- this._onDragOver(evt);
- _globalDragOver(evt);
- }
- }
- else if (type === 'drop' || type === 'dragend') {
- this._onDrop(evt);
- }
- },
-
-
- /**
- * Serializes the item into an array of string.
- * @returns {String[]}
- */
- toArray: function () {
- var order = [],
- el,
- children = this.el.children,
- i = 0,
- n = children.length,
- options = this.options;
-
- for (; i < n; i++) {
- el = children[i];
- if (_closest(el, options.draggable, this.el)) {
- order.push(el.getAttribute(options.dataIdAttr) || _generateId(el));
- }
- }
-
- return order;
- },
-
-
- /**
- * Sorts the elements according to the array.
- * @param {String[]} order order of the items
- */
- sort: function (order) {
- var items = {}, rootEl = this.el;
-
- this.toArray().forEach(function (id, i) {
- var el = rootEl.children[i];
-
- if (_closest(el, this.options.draggable, rootEl)) {
- items[id] = el;
- }
- }, this);
-
- order.forEach(function (id) {
- if (items[id]) {
- rootEl.removeChild(items[id]);
- rootEl.appendChild(items[id]);
- }
- });
- },
-
-
- /**
- * Save the current sorting
- */
- save: function () {
- var store = this.options.store;
- store && store.set(this);
- },
-
-
- /**
- * For each element in the set, get the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree.
- * @param {HTMLElement} el
- * @param {String} [selector] default: `options.draggable`
- * @returns {HTMLElement|null}
- */
- closest: function (el, selector) {
- return _closest(el, selector || this.options.draggable, this.el);
- },
-
-
- /**
- * Set/get option
- * @param {string} name
- * @param {*} [value]
- * @returns {*}
- */
- option: function (name, value) {
- var options = this.options;
-
- if (value === void 0) {
- return options[name];
- } else {
- options[name] = value;
-
- if (name === 'group') {
- _prepareGroup(options);
- }
- }
- },
-
-
- /**
- * Destroy
- */
- destroy: function () {
- var el = this.el;
-
- el[expando] = null;
-
- _off(el, 'mousedown', this._onTapStart);
- _off(el, 'touchstart', this._onTapStart);
-
- if (this.nativeDraggable) {
- _off(el, 'dragover', this);
- _off(el, 'dragenter', this);
- }
-
- // Remove draggable attributes
- Array.prototype.forEach.call(el.querySelectorAll('[draggable]'), function (el) {
- el.removeAttribute('draggable');
- });
-
- touchDragOverListeners.splice(touchDragOverListeners.indexOf(this._onDragOver), 1);
-
- this._onDrop();
-
- this.el = el = null;
- }
- };
-
-
- function _cloneHide(state) {
- if (cloneEl && (cloneEl.state !== state)) {
- _css(cloneEl, 'display', state ? 'none' : '');
- !state && cloneEl.state && rootEl.insertBefore(cloneEl, dragEl);
- cloneEl.state = state;
- }
- }
-
-
- function _closest(/**HTMLElement*/el, /**String*/selector, /**HTMLElement*/ctx) {
- if (el) {
- ctx = ctx || document;
-
- do {
- if ((selector === '>*' && el.parentNode === ctx) || _matches(el, selector)) {
- return el;
- }
- /* jshint boss:true */
- } while (el = _getParentOrHost(el));
- }
-
- return null;
- }
-
-
- function _getParentOrHost(el) {
- var parent = el.host;
-
- return (parent && parent.nodeType) ? parent : el.parentNode;
- }
-
-
- function _globalDragOver(/**Event*/evt) {
- if (evt.dataTransfer) {
- evt.dataTransfer.dropEffect = 'move';
- }
- evt.preventDefault();
- }
-
-
- function _on(el, event, fn) {
- el.addEventListener(event, fn, false);
- }
-
-
- function _off(el, event, fn) {
- el.removeEventListener(event, fn, false);
- }
-
-
- function _toggleClass(el, name, state) {
- if (el) {
- if (el.classList) {
- el.classList[state ? 'add' : 'remove'](name);
- }
- else {
- var className = (' ' + el.className + ' ').replace(RSPACE, ' ').replace(' ' + name + ' ', ' ');
- el.className = (className + (state ? ' ' + name : '')).replace(RSPACE, ' ');
- }
- }
- }
-
-
- function _css(el, prop, val) {
- var style = el && el.style;
-
- if (style) {
- if (val === void 0) {
- if (document.defaultView && document.defaultView.getComputedStyle) {
- val = document.defaultView.getComputedStyle(el, '');
- }
- else if (el.currentStyle) {
- val = el.currentStyle;
- }
-
- return prop === void 0 ? val : val[prop];
- }
- else {
- if (!(prop in style)) {
- prop = '-webkit-' + prop;
- }
-
- style[prop] = val + (typeof val === 'string' ? '' : 'px');
- }
- }
- }
-
-
- function _find(ctx, tagName, iterator) {
- if (ctx) {
- var list = ctx.getElementsByTagName(tagName), i = 0, n = list.length;
-
- if (iterator) {
- for (; i < n; i++) {
- iterator(list[i], i);
- }
- }
-
- return list;
- }
-
- return [];
- }
-
-
-
- function _dispatchEvent(sortable, rootEl, name, targetEl, fromEl, startIndex, newIndex) {
- sortable = (sortable || rootEl[expando]);
-
- var evt = document.createEvent('Event'),
- options = sortable.options,
- onName = 'on' + name.charAt(0).toUpperCase() + name.substr(1);
-
- evt.initEvent(name, true, true);
-
- evt.to = rootEl;
- evt.from = fromEl || rootEl;
- evt.item = targetEl || rootEl;
- evt.clone = cloneEl;
-
- evt.oldIndex = startIndex;
- evt.newIndex = newIndex;
-
- rootEl.dispatchEvent(evt);
-
- if (options[onName]) {
- options[onName].call(sortable, evt);
- }
- }
-
-
- function _onMove(fromEl, toEl, dragEl, dragRect, targetEl, targetRect, originalEvt) {
- var evt,
- sortable = fromEl[expando],
- onMoveFn = sortable.options.onMove,
- retVal;
-
- evt = document.createEvent('Event');
- evt.initEvent('move', true, true);
-
- evt.to = toEl;
- evt.from = fromEl;
- evt.dragged = dragEl;
- evt.draggedRect = dragRect;
- evt.related = targetEl || toEl;
- evt.relatedRect = targetRect || toEl.getBoundingClientRect();
-
- fromEl.dispatchEvent(evt);
-
- if (onMoveFn) {
- retVal = onMoveFn.call(sortable, evt, originalEvt);
- }
-
- return retVal;
- }
-
-
- function _disableDraggable(el) {
- el.draggable = false;
- }
-
-
- function _unsilent() {
- _silent = false;
- }
-
-
- /** @returns {HTMLElement|false} */
- function _ghostIsLast(el, evt) {
- var lastEl = el.lastElementChild,
- rect = lastEl.getBoundingClientRect();
-
- // 5 — min delta
- // abs — Ð½ÐµÐ»ÑŒÐ·Ñ Ð´Ð¾Ð±Ð°Ð²Ð»Ñть, а то глюки при наведении Ñверху
- return (
- (evt.clientY - (rect.top + rect.height) > 5) ||
- (evt.clientX - (rect.right + rect.width) > 5)
- ) && lastEl;
- }
-
-
- /**
- * Generate id
- * @param {HTMLElement} el
- * @returns {String}
- * @private
- */
- function _generateId(el) {
- var str = el.tagName + el.className + el.src + el.href + el.textContent,
- i = str.length,
- sum = 0;
-
- while (i--) {
- sum += str.charCodeAt(i);
- }
-
- return sum.toString(36);
- }
-
- /**
- * Returns the index of an element within its parent for a selected set of
- * elements
- * @param {HTMLElement} el
- * @param {selector} selector
- * @return {number}
- */
- function _index(el, selector) {
- var index = 0;
-
- if (!el || !el.parentNode) {
- return -1;
- }
-
- while (el && (el = el.previousElementSibling)) {
- if ((el.nodeName.toUpperCase() !== 'TEMPLATE') && (selector === '>*' || _matches(el, selector))) {
- index++;
- }
- }
-
- return index;
- }
-
- function _matches(/**HTMLElement*/el, /**String*/selector) {
- if (el) {
- selector = selector.split('.');
-
- var tag = selector.shift().toUpperCase(),
- re = new RegExp('\\s(' + selector.join('|') + ')(?=\\s)', 'g');
-
- return (
- (tag === '' || el.nodeName.toUpperCase() == tag) &&
- (!selector.length || ((' ' + el.className + ' ').match(re) || []).length == selector.length)
- );
- }
-
- return false;
- }
-
- function _throttle(callback, ms) {
- var args, _this;
-
- return function () {
- if (args === void 0) {
- args = arguments;
- _this = this;
-
- setTimeout(function () {
- if (args.length === 1) {
- callback.call(_this, args[0]);
- } else {
- callback.apply(_this, args);
- }
-
- args = void 0;
- }, ms);
- }
- };
- }
-
- function _extend(dst, src) {
- if (dst && src) {
- for (var key in src) {
- if (src.hasOwnProperty(key)) {
- dst[key] = src[key];
- }
- }
- }
-
- return dst;
- }
-
- function _clone(el) {
- return $
- ? $(el).clone(true)[0]
- : (Polymer && Polymer.dom
- ? Polymer.dom(el).cloneNode(true)
- : el.cloneNode(true)
- );
- }
-
-
- // Export utils
- Sortable.utils = {
- on: _on,
- off: _off,
- css: _css,
- find: _find,
- is: function (el, selector) {
- return !!_closest(el, selector, el);
- },
- extend: _extend,
- throttle: _throttle,
- closest: _closest,
- toggleClass: _toggleClass,
- clone: _clone,
- index: _index
- };
-
-
- /**
- * Create sortable instance
- * @param {HTMLElement} el
- * @param {Object} [options]
- */
- Sortable.create = function (el, options) {
- return new Sortable(el, options);
- };
-
-
- // Export
- Sortable.version = '1.4.2';
- return Sortable;
-});
diff --git a/vendor/assets/javascripts/date.format.js b/vendor/assets/javascripts/date.format.js
deleted file mode 100644
index 2c9b4825443..00000000000
--- a/vendor/assets/javascripts/date.format.js
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * Date Format 1.2.3
- * (c) 2007-2009 Steven Levithan <stevenlevithan.com>
- * MIT license
- *
- * Includes enhancements by Scott Trenda <scott.trenda.net>
- * and Kris Kowal <cixar.com/~kris.kowal/>
- *
- * Accepts a date, a mask, or a date and a mask.
- * Returns a formatted version of the given date.
- * The date defaults to the current date/time.
- * The mask defaults to dateFormat.masks.default.
- */
- (function (global, factory) {
- typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
- typeof define === 'function' && define.amd ? define(factory) :
- (global.dateFormat = factory());
- }(this, (function () { 'use strict';
- var dateFormat = function () {
- var token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g,
- timezone = /\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b/g,
- timezoneClip = /[^-+\dA-Z]/g,
- pad = function (val, len) {
- val = String(val);
- len = len || 2;
- while (val.length < len) val = "0" + val;
- return val;
- };
-
- // Regexes and supporting functions are cached through closure
- return function (date, mask, utc) {
- var dF = dateFormat;
-
- // You can't provide utc if you skip other args (use the "UTC:" mask prefix)
- if (arguments.length == 1 && Object.prototype.toString.call(date) == "[object String]" && !/\d/.test(date)) {
- mask = date;
- date = undefined;
- }
-
- // Passing date through Date applies Date.parse, if necessary
- date = date ? new Date(date) : new Date;
- if (isNaN(date)) throw SyntaxError("invalid date");
-
- mask = String(dF.masks[mask] || mask || dF.masks["default"]);
-
- // Allow setting the utc argument via the mask
- if (mask.slice(0, 4) == "UTC:") {
- mask = mask.slice(4);
- utc = true;
- }
-
- var _ = utc ? "getUTC" : "get",
- d = date[_ + "Date"](),
- D = date[_ + "Day"](),
- m = date[_ + "Month"](),
- y = date[_ + "FullYear"](),
- H = date[_ + "Hours"](),
- M = date[_ + "Minutes"](),
- s = date[_ + "Seconds"](),
- L = date[_ + "Milliseconds"](),
- o = utc ? 0 : date.getTimezoneOffset(),
- flags = {
- d: d,
- dd: pad(d),
- ddd: dF.i18n.dayNames[D],
- dddd: dF.i18n.dayNames[D + 7],
- m: m + 1,
- mm: pad(m + 1),
- mmm: dF.i18n.monthNames[m],
- mmmm: dF.i18n.monthNames[m + 12],
- yy: String(y).slice(2),
- yyyy: y,
- h: H % 12 || 12,
- hh: pad(H % 12 || 12),
- H: H,
- HH: pad(H),
- M: M,
- MM: pad(M),
- s: s,
- ss: pad(s),
- l: pad(L, 3),
- L: pad(L > 99 ? Math.round(L / 10) : L),
- t: H < 12 ? "a" : "p",
- tt: H < 12 ? "am" : "pm",
- T: H < 12 ? "A" : "P",
- TT: H < 12 ? "AM" : "PM",
- Z: utc ? "UTC" : (String(date).match(timezone) || [""]).pop().replace(timezoneClip, ""),
- o: (o > 0 ? "-" : "+") + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4),
- S: ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10]
- };
-
- return mask.replace(token, function ($0) {
- return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1);
- });
- };
- }();
-
- // Some common format strings
- dateFormat.masks = {
- "default": "ddd mmm dd yyyy HH:MM:ss",
- shortDate: "m/d/yy",
- mediumDate: "mmm d, yyyy",
- longDate: "mmmm d, yyyy",
- fullDate: "dddd, mmmm d, yyyy",
- shortTime: "h:MM TT",
- mediumTime: "h:MM:ss TT",
- longTime: "h:MM:ss TT Z",
- isoDate: "yyyy-mm-dd",
- isoTime: "HH:MM:ss",
- isoDateTime: "yyyy-mm-dd'T'HH:MM:ss",
- isoUtcDateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'"
- };
-
- // Internationalization strings
- dateFormat.i18n = {
- dayNames: [
- "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat",
- "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
- ],
- monthNames: [
- "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
- "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"
- ]
- };
-
- // For convenience...
- Date.prototype.format = function (mask, utc) {
- return dateFormat(this, mask, utc);
- };
-
- return dateFormat;
-})));
diff --git a/vendor/gitignore/Autotools.gitignore b/vendor/gitignore/Autotools.gitignore
index ffa6ecc3f9b..96d6ed2cfea 100644
--- a/vendor/gitignore/Autotools.gitignore
+++ b/vendor/gitignore/Autotools.gitignore
@@ -9,7 +9,7 @@ Makefile.in
# http://www.gnu.org/software/autoconf
-/autom4te.cache
+autom4te.cache
/autoscan.log
/autoscan-*.log
/aclocal.m4
@@ -39,4 +39,3 @@ m4/ltoptions.m4
m4/ltsugar.m4
m4/ltversion.m4
m4/lt~obsolete.m4
-autom4te.cache
diff --git a/vendor/gitignore/CraftCMS.gitignore b/vendor/gitignore/CraftCMS.gitignore
index a70d4772c46..0d81b397e35 100644
--- a/vendor/gitignore/CraftCMS.gitignore
+++ b/vendor/gitignore/CraftCMS.gitignore
@@ -1,3 +1,4 @@
-# Craft Storage (cache) [http://buildwithcraft.com/help/craft-storage-gitignore]
+# Craft 2 Storage (https://craftcms.com/support/craft-storage-gitignore)
+# not necessary for Craft 3 (https://github.com/craftcms/craft/issues/26)
/craft/storage/*
-!/craft/storage/logo/* \ No newline at end of file
+!/craft/storage/rebrand
diff --git a/vendor/gitignore/Dart.gitignore b/vendor/gitignore/Dart.gitignore
index 7bf00e82cc9..dbef116d224 100644
--- a/vendor/gitignore/Dart.gitignore
+++ b/vendor/gitignore/Dart.gitignore
@@ -3,7 +3,6 @@
# Files and directories created by pub
.dart_tool/
.packages
-.pub/
build/
# If you're building an application, you may want to check-in your pubspec.lock
pubspec.lock
@@ -11,3 +10,12 @@ pubspec.lock
# Directory created by dartdoc
# If you don't generate documentation locally you can remove this line.
doc/api/
+
+# Avoid committing generated Javascript files:
+*.dart.js
+*.info.json # Produced by the --dump-info flag.
+*.js # When generated by dart2js. Don't specify *.js if your
+ # project includes source files written in JavaScript.
+*.js_
+*.js.deps
+*.js.map
diff --git a/vendor/gitignore/Delphi.gitignore b/vendor/gitignore/Delphi.gitignore
index 19864c6bbef..000ee5f104b 100644
--- a/vendor/gitignore/Delphi.gitignore
+++ b/vendor/gitignore/Delphi.gitignore
@@ -20,7 +20,7 @@
# Deployment Manager configuration file for your project. Added in Delphi XE2.
# Uncomment this if it is not mobile development and you do not use remote debug feature.
#*.deployproj
-#
+#
# C++ object files produced when C/C++ Output file generation is configured.
# Uncomment this if you are not using external objects (zlib library for example).
#*.obj
diff --git a/vendor/gitignore/Eagle.gitignore b/vendor/gitignore/Eagle.gitignore
index 9afc324d6ae..28f0b9715e6 100644
--- a/vendor/gitignore/Eagle.gitignore
+++ b/vendor/gitignore/Eagle.gitignore
@@ -35,7 +35,6 @@ eagle.epf
*.gpi
*.pls
*.ger
-*.gpi
*.xln
*.drd
diff --git a/vendor/gitignore/GWT.gitignore b/vendor/gitignore/GWT.gitignore
index 07704e54bbc..a01e7fcd921 100644
--- a/vendor/gitignore/GWT.gitignore
+++ b/vendor/gitignore/GWT.gitignore
@@ -18,9 +18,6 @@ war/WEB-INF/classes/
#compilation logs
.gwt/
-#caching for already compiled files
-gwt-unitCache/
-
#gwt junit compilation files
www-test/
diff --git a/vendor/gitignore/Global/Backup.gitignore b/vendor/gitignore/Global/Backup.gitignore
new file mode 100644
index 00000000000..825ce52db53
--- /dev/null
+++ b/vendor/gitignore/Global/Backup.gitignore
@@ -0,0 +1,5 @@
+*.bak
+*.gho
+*.ori
+*.orig
+*.tmp
diff --git a/vendor/gitignore/Global/CodeKit.gitignore b/vendor/gitignore/Global/CodeKit.gitignore
index bd9e67fcca2..09b84126cea 100644
--- a/vendor/gitignore/Global/CodeKit.gitignore
+++ b/vendor/gitignore/Global/CodeKit.gitignore
@@ -1,3 +1,4 @@
# General CodeKit files to ignore
config.codekit
+config.codekit3
/min
diff --git a/vendor/gitignore/Global/Eclipse.gitignore b/vendor/gitignore/Global/Eclipse.gitignore
index 0eb8a5e8571..a65649a9ed2 100644
--- a/vendor/gitignore/Global/Eclipse.gitignore
+++ b/vendor/gitignore/Global/Eclipse.gitignore
@@ -23,7 +23,7 @@ local.properties
# CDT-specific (C/C++ Development Tooling)
.cproject
-# CDT- autotools
+# CDT- autotools
.autotools
# Java annotation processor (APT)
@@ -47,6 +47,9 @@ local.properties
# Code Recommenders
.recommenders/
+# Annotation Processing
+.apt_generated/
+
# Scala IDE specific (Scala & Java development for Eclipse)
.cache-main
.scala_dependencies
diff --git a/vendor/gitignore/Global/JetBrains.gitignore b/vendor/gitignore/Global/JetBrains.gitignore
index b09cb3dbc04..0d95b087f19 100644
--- a/vendor/gitignore/Global/JetBrains.gitignore
+++ b/vendor/gitignore/Global/JetBrains.gitignore
@@ -4,6 +4,7 @@
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
@@ -14,14 +15,22 @@
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn. Uncomment if using
+# auto-import.
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+
# CMake
-cmake-build-debug/
-cmake-build-release/
+cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
diff --git a/vendor/gitignore/Global/Matlab.gitignore b/vendor/gitignore/Global/Matlab.gitignore
index d87a6bdbeeb..46a83d635ba 100644
--- a/vendor/gitignore/Global/Matlab.gitignore
+++ b/vendor/gitignore/Global/Matlab.gitignore
@@ -7,17 +7,20 @@
# Compiled MEX binaries (all platforms)
*.mex*
-# Packaged app and toolbox files
-*.mlappinstall
-*.mltbx
-
-# Generated helpsearch folders
-helpsearch*/
+# Packaged app and toolbox files
+*.mlappinstall
+*.mltbx
+
+# Generated helpsearch folders
+helpsearch*/
# Simulink code generation folders
slprj/
sccprj/
+# Matlab code generation folders
+codegen/
+
# Simulink autosave extension
*.autosave
diff --git a/vendor/gitignore/Global/Patch.gitignore b/vendor/gitignore/Global/Patch.gitignore
new file mode 100644
index 00000000000..6ffab9ad295
--- /dev/null
+++ b/vendor/gitignore/Global/Patch.gitignore
@@ -0,0 +1,2 @@
+*.orig
+*.rej
diff --git a/vendor/gitignore/Global/SynopsysVCS.gitignore b/vendor/gitignore/Global/SynopsysVCS.gitignore
index eed2432fb78..ad751f6bd75 100644
--- a/vendor/gitignore/Global/SynopsysVCS.gitignore
+++ b/vendor/gitignore/Global/SynopsysVCS.gitignore
@@ -4,8 +4,8 @@
*.evcd
*.fsdb
-# Default name of the simulation executable. A different name can be
-# specified with this switch (the associated daidir database name is
+# Default name of the simulation executable. A different name can be
+# specified with this switch (the associated daidir database name is
# also taken from here): -o <path>/<filename>
simv
@@ -13,7 +13,7 @@ simv
simv.daidir/
simv.db.dir/
-# Infrastructure necessary to co-simulate SystemC models with
+# Infrastructure necessary to co-simulate SystemC models with
# Verilog/VHDL models. An alternate directory may be specified with this
# switch: -Mdir=<directory_path>
csrc/
@@ -22,7 +22,7 @@ csrc/
# used to write all messages from simulation: -l <filename>
*.log
-# Coverage results (generated with urg) and database location. The
+# Coverage results (generated with urg) and database location. The
# following switch can also be used: urg -dir <coverage_directory>.vdb
simv.vdb/
urgReport/
diff --git a/vendor/gitignore/Global/Vim.gitignore b/vendor/gitignore/Global/Vim.gitignore
index 19cfe22f583..741518ffd24 100644
--- a/vendor/gitignore/Global/Vim.gitignore
+++ b/vendor/gitignore/Global/Vim.gitignore
@@ -1,7 +1,8 @@
# Swap
[._]*.s[a-v][a-z]
[._]*.sw[a-p]
-[._]s[a-v][a-z]
+[._]s[a-rt-v][a-z]
+[._]ss[a-gi-z]
[._]sw[a-p]
# Session
diff --git a/vendor/gitignore/LabVIEW.gitignore b/vendor/gitignore/LabVIEW.gitignore
index 122450865cf..31619f59814 100644
--- a/vendor/gitignore/LabVIEW.gitignore
+++ b/vendor/gitignore/LabVIEW.gitignore
@@ -14,3 +14,4 @@
# Metadata
*.aliases
*.lvlps
+.cache/
diff --git a/vendor/gitignore/Maven.gitignore b/vendor/gitignore/Maven.gitignore
index 5f2dbe11df9..e8d57d08088 100644
--- a/vendor/gitignore/Maven.gitignore
+++ b/vendor/gitignore/Maven.gitignore
@@ -7,6 +7,4 @@ release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
-
-# Avoid ignoring Maven wrapper jar file (.jar files are usually ignored)
-!/.mvn/wrapper/maven-wrapper.jar
+.mvn/wrapper/maven-wrapper.jar
diff --git a/vendor/gitignore/Node.gitignore b/vendor/gitignore/Node.gitignore
index ad46b30886f..3a4c8581b3a 100644
--- a/vendor/gitignore/Node.gitignore
+++ b/vendor/gitignore/Node.gitignore
@@ -57,5 +57,17 @@ typings/
# dotenv environment variables file
.env
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+
# next.js build output
.next
+
+# nuxt.js build output
+.nuxt
+
+# vuepress build output
+.vuepress/dist
+
+# Serverless directories
+.serverless
diff --git a/vendor/gitignore/Objective-C.gitignore b/vendor/gitignore/Objective-C.gitignore
index 86de6aa3f5f..a0bd6b453a8 100644
--- a/vendor/gitignore/Objective-C.gitignore
+++ b/vendor/gitignore/Objective-C.gitignore
@@ -35,6 +35,9 @@ xcuserdata/
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
#
# Pods/
+#
+# Add this line if you want to avoid checking in source code from the Xcode workspace
+# *.xcworkspace
# Carthage
#
diff --git a/vendor/gitignore/Perl6.gitignore b/vendor/gitignore/Perl6.gitignore
new file mode 100644
index 00000000000..7b2c018a562
--- /dev/null
+++ b/vendor/gitignore/Perl6.gitignore
@@ -0,0 +1,7 @@
+# Gitignore for Perl 6 (http://www.perl6.org)
+# As part of https://github.com/github/gitignore
+
+# precompiled files
+.precomp
+lib/.precomp
+
diff --git a/vendor/gitignore/Sass.gitignore b/vendor/gitignore/Sass.gitignore
index 486b32ce90c..159f515170b 100644
--- a/vendor/gitignore/Sass.gitignore
+++ b/vendor/gitignore/Sass.gitignore
@@ -1,2 +1,4 @@
.sass-cache/
*.css.map
+*.sass.map
+*.scss.map
diff --git a/vendor/gitignore/Swift.gitignore b/vendor/gitignore/Swift.gitignore
index 312d1f652c6..b8e04d98e33 100644
--- a/vendor/gitignore/Swift.gitignore
+++ b/vendor/gitignore/Swift.gitignore
@@ -47,6 +47,9 @@ playground.xcworkspace
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
#
# Pods/
+#
+# Add this line if you want to avoid checking in source code from the Xcode workspace
+# *.xcworkspace
# Carthage
#
diff --git a/vendor/gitignore/TeX.gitignore b/vendor/gitignore/TeX.gitignore
index e6598ba1727..79a66f9ebfa 100644
--- a/vendor/gitignore/TeX.gitignore
+++ b/vendor/gitignore/TeX.gitignore
@@ -40,6 +40,10 @@
*.synctex.gz(busy)
*.pdfsync
+## Build tool directories for auxiliary files
+# latexrun
+latex.out/
+
## Auxiliary and intermediate files from other packages:
# algorithms
*.alg
@@ -222,6 +226,9 @@ TSWLatexianTemp*
# Texpad
.texpadtmp
+# LyX
+*.lyx~
+
# Kile
*.backup
@@ -237,6 +244,3 @@ TSWLatexianTemp*
# standalone packages
*.sta
-
-# generated if using elsarticle.cls
-*.spl
diff --git a/vendor/gitignore/Terraform.gitignore b/vendor/gitignore/Terraform.gitignore
index 1fef4ab91e1..d9397e2d7d9 100644
--- a/vendor/gitignore/Terraform.gitignore
+++ b/vendor/gitignore/Terraform.gitignore
@@ -1,9 +1,15 @@
-# Local .terraform directories
+# Local .terraform directories
**/.terraform/*
# .tfstate files
*.tfstate
*.tfstate.*
-# .tfvars files
-*.tfvars
+# Crash log files
+crash.log
+
+# Ignore any .tfvars files that are generated automatically for each Terraform run. Most
+# .tfvars files are managed as part of configuration and so should be included in
+# version control.
+#
+# example.tfvars
diff --git a/vendor/gitignore/Typo3.gitignore b/vendor/gitignore/Typo3.gitignore
index cb024fefe99..200c2a2bf79 100644
--- a/vendor/gitignore/Typo3.gitignore
+++ b/vendor/gitignore/Typo3.gitignore
@@ -8,12 +8,15 @@
/typo3conf/temp_CACHED*
/typo3conf/temp_fieldInfo.php
/typo3conf/deprecation_*.log
-/typo3conf/AdditionalConfiguration.php
+/typo3conf/ENABLE_INSTALL_TOOL
+/typo3conf/realurl_autoconf.php
+/FIRST_INSTALL
# Ignore system folders, you should have them symlinked.
# If not comment out the following entries.
/typo3
/typo3_src
/typo3_src-*
+/Packages
/.htaccess
/index.php
# Ignore temp directory.
diff --git a/vendor/gitignore/Umbraco.gitignore b/vendor/gitignore/Umbraco.gitignore
index 10fc2b4d825..cd90af3071a 100644
--- a/vendor/gitignore/Umbraco.gitignore
+++ b/vendor/gitignore/Umbraco.gitignore
@@ -19,7 +19,7 @@
!**/App_Data/[Pp]ackages/*
!**/[Uu]mbraco/[Dd]eveloper/[Pp]ackages/*
-# ImageProcessor DiskCache
+# ImageProcessor DiskCache
**/App_Data/cache/
# Ignore the Models Builder models out of date flag
diff --git a/vendor/gitignore/UnrealEngine.gitignore b/vendor/gitignore/UnrealEngine.gitignore
index 1daca8b50d9..6582eaf9a11 100644
--- a/vendor/gitignore/UnrealEngine.gitignore
+++ b/vendor/gitignore/UnrealEngine.gitignore
@@ -1,9 +1,6 @@
# Visual Studio 2015 user specific files
.vs/
-# Visual Studio 2015 database file
-*.VC.db
-
# Compiled Object files
*.slo
*.lo
diff --git a/vendor/gitignore/VisualStudio.gitignore b/vendor/gitignore/VisualStudio.gitignore
index 3e759b75bf4..f431ddc7cf5 100644
--- a/vendor/gitignore/VisualStudio.gitignore
+++ b/vendor/gitignore/VisualStudio.gitignore
@@ -52,7 +52,6 @@ BenchmarkDotNet.Artifacts/
project.lock.json
project.fragment.lock.json
artifacts/
-**/Properties/launchSettings.json
# StyleCop
StyleCopReport.xml
@@ -221,7 +220,7 @@ ClientBin/
*.publishsettings
orleans.codegen.cs
-# Including strong name files can present a security risk
+# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
@@ -317,7 +316,7 @@ __pycache__/
# OpenCover UI analysis results
OpenCover/
-# Azure Stream Analytics local run output
+# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
@@ -326,5 +325,5 @@ ASALocalRun/
# NVidia Nsight GPU debugger configuration file
*.nvuser
-# MFractors (Xamarin productivity tool) working folder
+# MFractors (Xamarin productivity tool) working folder
.mfractor/
diff --git a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
index a00c6e89a1d..ee0df7711e7 100644
--- a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
@@ -7,6 +7,18 @@
# * creating a review app for each topic branch,
# * and continuous deployment to production
#
+# Test jobs may be disabled by setting environment variables:
+# * test: TEST_DISABLED
+# * code_quality: CODE_QUALITY_DISABLED
+# * license_management: LICENSE_MANAGEMENT_DISABLED
+# * performance: PERFORMANCE_DISABLED
+# * sast: SAST_DISABLED
+# * dependency_scanning: DEPENDENCY_SCANNING_DISABLED
+# * container_scanning: CONTAINER_SCANNING_DISABLED
+# * dast: DAST_DISABLED
+# * review: REVIEW_DISABLED
+# * stop_review: REVIEW_DISABLED
+#
# In order to deploy, you must have a Kubernetes cluster configured either
# via a project integration, or via group/project variables.
# AUTO_DEVOPS_DOMAIN must also be set as a variable at the group or project
@@ -15,7 +27,7 @@
# Continuous deployment to production is enabled by default.
# If you want to deploy to staging first, or enable incremental rollouts,
# set STAGING_ENABLED or INCREMENTAL_ROLLOUT_ENABLED environment variables.
-# If you want to use canary deployments, uncomment the canary job.
+# If you want to use canary deployments, set CANARY_ENABLED environment variable.
#
# If Auto DevOps fails to detect the proper buildpack, or if you want to
# specify a custom buildpack, set a project variable `BUILDPACK_URL` to the
@@ -76,8 +88,31 @@ test:
- /bin/herokuish buildpack test
only:
- branches
+ except:
+ variables:
+ - $TEST_DISABLED
+
+code_quality:
+ stage: test
+ image: docker:stable
+ variables:
+ DOCKER_DRIVER: overlay2
+ allow_failure: true
+ services:
+ - docker:stable-dind
+ script:
+ - setup_docker
+ - code_quality
+ artifacts:
+ paths: [gl-code-quality-report.json]
+ only:
+ - branches
+ except:
+ variables:
+ - $CODE_QUALITY_DISABLED
-codequality:
+license_management:
+ stage: test
image: docker:stable
variables:
DOCKER_DRIVER: overlay2
@@ -86,9 +121,14 @@ codequality:
- docker:stable-dind
script:
- setup_docker
- - codeclimate
+ - license_management
artifacts:
- paths: [codeclimate.json]
+ paths: [gl-license-management-report.json]
+ only:
+ - branches
+ except:
+ variables:
+ - $LICENSE_MANAGEMENT_DISABLED
performance:
stage: performance
@@ -109,8 +149,12 @@ performance:
refs:
- branches
kubernetes: active
+ except:
+ variables:
+ - $PERFORMANCE_DISABLED
sast:
+ stage: test
image: docker:stable
variables:
DOCKER_DRIVER: overlay2
@@ -122,8 +166,14 @@ sast:
- sast
artifacts:
paths: [gl-sast-report.json]
+ only:
+ - branches
+ except:
+ variables:
+ - $SAST_DISABLED
dependency_scanning:
+ stage: test
image: docker:stable
variables:
DOCKER_DRIVER: overlay2
@@ -135,8 +185,14 @@ dependency_scanning:
- dependency_scanning
artifacts:
paths: [gl-dependency-scanning-report.json]
+ only:
+ - branches
+ except:
+ variables:
+ - $DEPENDENCY_SCANNING_DISABLED
-sast:container:
+container_scanning:
+ stage: test
image: docker:stable
variables:
DOCKER_DRIVER: overlay2
@@ -145,9 +201,14 @@ sast:container:
- docker:stable-dind
script:
- setup_docker
- - sast_container
+ - container_scanning
artifacts:
- paths: [gl-sast-container-report.json]
+ paths: [gl-container-scanning-report.json]
+ only:
+ - branches
+ except:
+ variables:
+ - $CONTAINER_SCANNING_DISABLED
dast:
stage: dast
@@ -164,7 +225,10 @@ dast:
- branches
kubernetes: active
except:
- - master
+ refs:
+ - master
+ variables:
+ - $DAST_DISABLED
review:
stage: review
@@ -188,7 +252,10 @@ review:
- branches
kubernetes: active
except:
- - master
+ refs:
+ - master
+ variables:
+ - $REVIEW_DISABLED
stop_review:
stage: cleanup
@@ -207,7 +274,10 @@ stop_review:
- branches
kubernetes: active
except:
- - master
+ refs:
+ - master
+ variables:
+ - $REVIEW_DISABLED
# Keys that start with a dot (.) will not be processed by GitLab CI.
# Staging and canary jobs are disabled by default, to enable them
@@ -240,10 +310,11 @@ staging:
variables:
- $STAGING_ENABLED
-# Canaries are disabled by default, but if you want them,
-# and know what the downsides are, enable this job by removing the dot (.).
+# Canaries are also disabled by default, but if you want them,
+# and know what the downsides are, you can enable this by setting
+# CANARY_ENABLED.
-.canary:
+canary:
stage: canary
script:
- check_kube_domain
@@ -261,6 +332,8 @@ staging:
refs:
- master
kubernetes: active
+ variables:
+ - $CANARY_ENABLED
.production: &production_template
stage: production
@@ -290,6 +363,7 @@ production:
except:
variables:
- $STAGING_ENABLED
+ - $CANARY_ENABLED
- $INCREMENTAL_ROLLOUT_ENABLED
production_manual:
@@ -302,6 +376,7 @@ production_manual:
kubernetes: active
variables:
- $STAGING_ENABLED
+ - $CANARY_ENABLED
except:
variables:
- $INCREMENTAL_ROLLOUT_ENABLED
@@ -388,7 +463,7 @@ rollout 100%:
# Extract "MAJOR.MINOR" from CI_SERVER_VERSION and generate "MAJOR-MINOR-stable" for Security Products
export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
- function sast_container() {
+ function container_scanning() {
if [[ -n "$CI_REGISTRY_USER" ]]; then
echo "Logging to GitLab Container Registry with CI credentials..."
docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
@@ -406,16 +481,28 @@ rollout 100%:
retries=0
echo "Waiting for clair daemon to start"
while( ! wget -T 10 -q -O /dev/null http://docker:6060/v1/namespaces ) ; do sleep 1 ; echo -n "." ; if [ $retries -eq 10 ] ; then echo " Timeout, aborting." ; exit 1 ; fi ; retries=$(($retries+1)) ; done
- ./clair-scanner -c http://docker:6060 --ip $(hostname -i) -r gl-sast-container-report.json -l clair.log -w clair-whitelist.yml ${CI_APPLICATION_REPOSITORY}:${CI_APPLICATION_TAG} || true
+ ./clair-scanner -c http://docker:6060 --ip $(hostname -i) -r gl-container-scanning-report.json -l clair.log -w clair-whitelist.yml ${CI_APPLICATION_REPOSITORY}:${CI_APPLICATION_TAG} || true
}
- function codeclimate() {
+ function code_quality() {
docker run --env SOURCE_CODE="$PWD" \
--volume "$PWD":/code \
--volume /var/run/docker.sock:/var/run/docker.sock \
"registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code
}
+ function license_management() {
+ if echo $GITLAB_FEATURES |grep license_management > /dev/null ; then
+ # Extract "MAJOR.MINOR" from CI_SERVER_VERSION and generate "MAJOR-MINOR-stable"
+ LICENSE_MANAGEMENT_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
+
+ docker run --volume "$PWD:/code" \
+ "registry.gitlab.com/gitlab-org/security-products/license-management:$LICENSE_MANAGEMENT_VERSION" analyze /code
+ else
+ echo "License management is not available in your subscription"
+ fi
+ }
+
function sast() {
case "$CI_SERVER_VERSION" in
*-ee)
@@ -615,7 +702,7 @@ rollout 100%:
function check_kube_domain() {
if [ -z ${AUTO_DEVOPS_DOMAIN+x} ]; then
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 do it in Auto DevOps project settings or defining a variable at group or project level"
echo "You can also manually add it in .gitlab-ci.yml"
false
else
@@ -624,7 +711,6 @@ rollout 100%:
}
function build() {
-
if [[ -n "$CI_REGISTRY_USER" ]]; then
echo "Logging to GitLab Container Registry with CI credentials..."
docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
@@ -636,7 +722,7 @@ rollout 100%:
docker build -t "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" .
else
echo "Building Heroku-based application using gliderlabs/herokuish docker image..."
- docker run -i --name="$CI_CONTAINER_NAME" -v "$(pwd):/tmp/app:ro" gliderlabs/herokuish /bin/herokuish buildpack build
+ docker run -i -e BUILDPACK_URL --name="$CI_CONTAINER_NAME" -v "$(pwd):/tmp/app:ro" gliderlabs/herokuish /bin/herokuish buildpack build
docker commit "$CI_CONTAINER_NAME" "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG"
docker rm "$CI_CONTAINER_NAME" >/dev/null
echo ""
diff --git a/vendor/gitlab-ci-yml/autodeploy/Kubernetes-with-canary.gitlab-ci.yml b/vendor/gitlab-ci-yml/autodeploy/Kubernetes-with-canary.gitlab-ci.yml
deleted file mode 100644
index 6e5fe97cf6d..00000000000
--- a/vendor/gitlab-ci-yml/autodeploy/Kubernetes-with-canary.gitlab-ci.yml
+++ /dev/null
@@ -1,87 +0,0 @@
-# This template has been DEPRECATED. Consider using Auto DevOps instead:
-# https://docs.gitlab.com/ee/topics/autodevops
-
-# Explanation on the scripts:
-# https://gitlab.com/gitlab-examples/kubernetes-deploy/blob/master/README.md
-image: registry.gitlab.com/gitlab-examples/kubernetes-deploy
-
-variables:
- # Application deployment domain
- KUBE_DOMAIN: domain.example.com
-
-stages:
- - build
- - test
- - review
- - staging
- - canary
- - production
- - cleanup
-
-build:
- stage: build
- script:
- - command build
- only:
- - branches
-
-canary:
- stage: canary
- script:
- - command canary
- environment:
- name: production
- url: http://$CI_PROJECT_PATH_SLUG.$KUBE_DOMAIN
- when: manual
- only:
- - master
-
-production:
- stage: production
- script:
- - command deploy
- environment:
- name: production
- url: http://$CI_PROJECT_PATH_SLUG.$KUBE_DOMAIN
- when: manual
- only:
- - master
-
-staging:
- stage: staging
- script:
- - command deploy
- environment:
- name: staging
- url: http://$CI_PROJECT_PATH_SLUG-staging.$KUBE_DOMAIN
- only:
- - master
-
-review:
- stage: review
- script:
- - command deploy
- environment:
- name: review/$CI_COMMIT_REF_NAME
- url: http://$CI_PROJECT_PATH_SLUG-$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN
- on_stop: stop_review
- only:
- - branches
- except:
- - master
-
-stop_review:
- stage: cleanup
- variables:
- GIT_STRATEGY: none
- script:
- - command destroy
- environment:
- name: review/$CI_COMMIT_REF_NAME
- action: stop
- when: manual
- allow_failure: true
- only:
- - branches
- except:
- - master
diff --git a/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml b/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml
deleted file mode 100644
index 019a4d4cd7d..00000000000
--- a/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml
+++ /dev/null
@@ -1,74 +0,0 @@
-# This template has been DEPRECATED. Consider using Auto DevOps instead:
-# https://docs.gitlab.com/ee/topics/autodevops
-
-# Explanation on the scripts:
-# https://gitlab.com/gitlab-examples/kubernetes-deploy/blob/master/README.md
-image: registry.gitlab.com/gitlab-examples/kubernetes-deploy
-
-variables:
- # Application deployment domain
- KUBE_DOMAIN: domain.example.com
-
-stages:
- - build
- - test
- - review
- - staging
- - production
- - cleanup
-
-build:
- stage: build
- script:
- - command build
- only:
- - branches
-
-production:
- stage: production
- script:
- - command deploy
- environment:
- name: production
- url: http://$CI_PROJECT_PATH_SLUG.$KUBE_DOMAIN
- when: manual
- only:
- - master
-
-staging:
- stage: staging
- script:
- - command deploy
- environment:
- name: staging
- url: http://$CI_PROJECT_PATH_SLUG-staging.$KUBE_DOMAIN
- only:
- - master
-
-review:
- stage: review
- script:
- - command deploy
- environment:
- name: review/$CI_COMMIT_REF_NAME
- url: http://$CI_PROJECT_PATH_SLUG-$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN
- on_stop: stop_review
- only:
- - branches
- except:
- - master
-
-stop_review:
- stage: cleanup
- variables:
- GIT_STRATEGY: none
- script:
- - command destroy
- environment:
- name: review/$CI_COMMIT_REF_NAME
- action: stop
- when: manual
- only:
- - branches
- except:
- - master
diff --git a/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml b/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml
deleted file mode 100644
index 60a9430a839..00000000000
--- a/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml
+++ /dev/null
@@ -1,74 +0,0 @@
-# This template has been DEPRECATED. Consider using Auto DevOps instead:
-# https://docs.gitlab.com/ee/topics/autodevops
-
-# Explanation on the scripts:
-# https://gitlab.com/gitlab-examples/openshift-deploy/blob/master/README.md
-image: registry.gitlab.com/gitlab-examples/openshift-deploy
-
-variables:
- # Application deployment domain
- KUBE_DOMAIN: domain.example.com
-
-stages:
- - build
- - test
- - review
- - staging
- - production
- - cleanup
-
-build:
- stage: build
- script:
- - command build
- only:
- - branches
-
-production:
- stage: production
- script:
- - command deploy
- environment:
- name: production
- url: http://$CI_PROJECT_PATH_SLUG.$KUBE_DOMAIN
- when: manual
- only:
- - master
-
-staging:
- stage: staging
- script:
- - command deploy
- environment:
- name: staging
- url: http://$CI_PROJECT_PATH_SLUG-staging.$KUBE_DOMAIN
- only:
- - master
-
-review:
- stage: review
- script:
- - command deploy
- environment:
- name: review/$CI_COMMIT_REF_NAME
- url: http://$CI_PROJECT_PATH_SLUG-$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN
- on_stop: stop_review
- only:
- - branches
- except:
- - master
-
-stop_review:
- stage: cleanup
- variables:
- GIT_STRATEGY: none
- script:
- - command destroy
- environment:
- name: review/$CI_COMMIT_REF_NAME
- action: stop
- when: manual
- only:
- - branches
- except:
- - master
diff --git a/vendor/jupyter/values.yaml b/vendor/jupyter/values.yaml
new file mode 100644
index 00000000000..90817de0f1b
--- /dev/null
+++ b/vendor/jupyter/values.yaml
@@ -0,0 +1,19 @@
+rbac:
+ enabled: false
+
+hub:
+ extraEnv:
+ JUPYTER_ENABLE_LAB: 1
+ extraConfig: |
+ c.KubeSpawner.cmd = ['jupyter-labhub']
+
+auth:
+ type: gitlab
+
+singleuser:
+ defaultUrl: "/lab"
+
+ingress:
+ enabled: true
+ annotations:
+ kubernetes.io/ingress.class: "nginx"
diff --git a/vendor/licenses.csv b/vendor/licenses.csv
index 07861631a07..7503160baa0 100644
--- a/vendor/licenses.csv
+++ b/vendor/licenses.csv
@@ -1,20 +1,41 @@
-@babel/code-frame,7.0.0-beta.32,MIT
-@babel/helper-function-name,7.0.0-beta.32,MIT
-@babel/helper-get-function-arity,7.0.0-beta.32,MIT
-@babel/template,7.0.0-beta.32,MIT
-@babel/traverse,7.0.0-beta.32,MIT
-@babel/types,7.0.0-beta.32,MIT
-@gitlab-org/gitlab-svgs,1.20.0,SEE LICENSE IN LICENSE
-@mrmlnc/readdir-enhanced,2.2.1,MIT
+@babel/code-frame,7.0.0-beta.44,MIT
+@babel/generator,7.0.0-beta.44,MIT
+@babel/helper-function-name,7.0.0-beta.44,MIT
+@babel/helper-get-function-arity,7.0.0-beta.44,MIT
+@babel/helper-split-export-declaration,7.0.0-beta.44,MIT
+@babel/highlight,7.0.0-beta.44,MIT
+@babel/template,7.0.0-beta.44,MIT
+@babel/traverse,7.0.0-beta.44,MIT
+@babel/types,7.0.0-beta.44,MIT
+@gitlab-org/gitlab-svgs,1.25.0,SEE LICENSE IN LICENSE
@sindresorhus/is,0.7.0,MIT
@types/jquery,2.0.48,MIT
+@vue/component-compiler-utils,1.2.1,MIT
+@webassemblyjs/ast,1.5.10,MIT
+@webassemblyjs/floating-point-hex-parser,1.5.10,MIT
+@webassemblyjs/helper-api-error,1.5.10,MIT
+@webassemblyjs/helper-buffer,1.5.10,MIT
+@webassemblyjs/helper-code-frame,1.5.10,MIT
+@webassemblyjs/helper-fsm,1.5.10,ISC
+@webassemblyjs/helper-module-context,1.5.10,MIT
+@webassemblyjs/helper-wasm-bytecode,1.5.10,MIT
+@webassemblyjs/helper-wasm-section,1.5.10,MIT
+@webassemblyjs/ieee754,1.5.10,Unknown
+@webassemblyjs/leb128,1.5.10,Apache 2.0
+@webassemblyjs/utf8,1.5.10,MIT
+@webassemblyjs/wasm-edit,1.5.10,MIT
+@webassemblyjs/wasm-gen,1.5.10,MIT
+@webassemblyjs/wasm-opt,1.5.10,MIT
+@webassemblyjs/wasm-parser,1.5.10,MIT
+@webassemblyjs/wast-parser,1.5.10,MIT
+@webassemblyjs/wast-printer,1.5.10,MIT
RedCloth,4.3.2,MIT
abbrev,1.0.9,ISC
abbrev,1.1.1,ISC
accepts,1.3.4,MIT
ace-rails-ap,4.1.2,MIT
acorn,3.3.0,MIT
-acorn,5.4.1,MIT
+acorn,5.6.2,MIT
acorn-dynamic-import,3.0.0,MIT
acorn-jsx,3.0.1,MIT
actionmailer,4.2.10,MIT
@@ -30,27 +51,22 @@ addressparser,1.0.1,MIT
aes_key_wrap,1.0.1,MIT
after,0.8.2,MIT
agent-base,2.1.1,MIT
-ajv,4.11.8,MIT
ajv,5.5.2,MIT
ajv,6.1.1,MIT
-ajv-keywords,1.5.1,MIT
+ajv-keywords,2.1.1,MIT
ajv-keywords,3.1.0,MIT
akismet,2.0.0,MIT
align-text,0.1.4,MIT
-allocations,1.0.5,MIT
alphanum-sort,1.0.2,MIT
amdefine,1.0.1,BSD-3-Clause OR MIT
amqplib,0.5.2,MIT
ansi-align,2.0.0,ISC
-ansi-escapes,1.4.0,MIT
ansi-escapes,3.0.0,MIT
ansi-html,0.0.7,Apache 2.0
ansi-regex,2.1.1,MIT
ansi-regex,3.0.0,MIT
-ansi-styles,1.0.0,MIT
ansi-styles,2.2.1,MIT
ansi-styles,3.2.1,MIT
-any-observable,0.2.0,MIT
anymatch,1.3.2,ISC
anymatch,2.0.0,ISC
append-transform,0.4.0,MIT
@@ -62,7 +78,6 @@ arr-diff,2.0.0,MIT
arr-diff,4.0.0,MIT
arr-flatten,1.1.0,MIT
arr-union,3.1.0,MIT
-array-differ,1.0.0,MIT
array-find,1.0.0,MIT
array-find-index,1.0.2,MIT
array-flatten,1.1.1,MIT
@@ -85,12 +100,9 @@ assert-plus,0.2.0,MIT
assert-plus,1.0.0,MIT
asset_sync,2.4.0,MIT
assign-symbols,1.0.0,MIT
-ast-types,0.10.1,MIT
-ast-types,0.11.1,MIT
ast-types,0.11.3,MIT
async,1.5.2,MIT
async,2.1.5,MIT
-async,2.4.1,MIT
async,2.6.0,MIT
async-each,1.0.1,MIT
async-limiter,1.0.0,MIT
@@ -100,7 +112,6 @@ atomic,1.1.99,Apache 2.0
attr_encrypted,3.1.0,MIT
attr_required,1.0.0,MIT
autoprefixer,6.7.7,MIT
-autoprefixer-rails,6.2.3,MIT
autosize,4.0.0,MIT
aws-sign2,0.6.0,Apache 2.0
aws-sign2,0.7.0,Apache 2.0
@@ -108,10 +119,10 @@ aws4,1.6.0,MIT
axiom-types,0.1.1,MIT
axios,0.15.3,MIT
axios,0.17.1,MIT
-axios-mock-adapter,1.10.0,MIT
+axios-mock-adapter,1.15.0,MIT
babel-code-frame,6.26.0,MIT
babel-core,6.26.3,MIT
-babel-eslint,8.0.2,MIT
+babel-eslint,8.2.3,MIT
babel-generator,6.26.0,MIT
babel-helper-bindify-decorators,6.24.1,MIT
babel-helper-builder-binary-assignment-operator-visitor,6.24.1,MIT
@@ -134,18 +145,14 @@ babel-plugin-istanbul,4.1.6,New BSD
babel-plugin-rewire,1.1.0,ISC
babel-plugin-syntax-async-functions,6.13.0,MIT
babel-plugin-syntax-async-generators,6.13.0,MIT
-babel-plugin-syntax-class-constructor-call,6.18.0,MIT
babel-plugin-syntax-class-properties,6.13.0,MIT
babel-plugin-syntax-decorators,6.13.0,MIT
babel-plugin-syntax-dynamic-import,6.18.0,MIT
babel-plugin-syntax-exponentiation-operator,6.13.0,MIT
-babel-plugin-syntax-export-extensions,6.13.0,MIT
-babel-plugin-syntax-flow,6.18.0,MIT
babel-plugin-syntax-object-rest-spread,6.13.0,MIT
babel-plugin-syntax-trailing-function-commas,6.22.0,MIT
babel-plugin-transform-async-generator-functions,6.24.1,MIT
babel-plugin-transform-async-to-generator,6.24.1,MIT
-babel-plugin-transform-class-constructor-call,6.24.1,MIT
babel-plugin-transform-class-properties,6.24.1,MIT
babel-plugin-transform-decorators,6.24.1,MIT
babel-plugin-transform-define,1.3.0,MIT
@@ -172,8 +179,6 @@ babel-plugin-transform-es2015-template-literals,6.22.0,MIT
babel-plugin-transform-es2015-typeof-symbol,6.23.0,MIT
babel-plugin-transform-es2015-unicode-regex,6.24.1,MIT
babel-plugin-transform-exponentiation-operator,6.24.1,MIT
-babel-plugin-transform-export-extensions,6.22.0,MIT
-babel-plugin-transform-flow-strip-types,6.22.0,MIT
babel-plugin-transform-object-rest-spread,6.23.0,MIT
babel-plugin-transform-regenerator,6.26.0,MIT
babel-plugin-transform-strict-mode,6.24.1,MIT
@@ -181,7 +186,6 @@ babel-preset-es2015,6.24.1,MIT
babel-preset-es2016,6.24.1,MIT
babel-preset-es2017,6.24.1,MIT
babel-preset-latest,6.24.1,MIT
-babel-preset-stage-1,6.24.1,MIT
babel-preset-stage-2,6.24.1,MIT
babel-preset-stage-3,6.24.1,MIT
babel-register,6.26.0,MIT
@@ -191,7 +195,6 @@ babel-traverse,6.26.0,MIT
babel-types,6.26.0,MIT
babosa,1.0.2,MIT
babylon,6.18.0,MIT
-babylon,7.0.0-beta.32,MIT
babylon,7.0.0-beta.44,MIT
backo2,1.0.2,MIT
balanced-match,0.4.2,MIT
@@ -203,20 +206,18 @@ base64-js,1.2.3,MIT
base64id,1.0.0,MIT
batch,0.6.1,MIT
batch-loader,1.2.1,MIT
-bcrypt,3.1.11,MIT
+bcrypt,3.1.12,MIT
bcrypt-pbkdf,1.0.1,New BSD
bcrypt_pbkdf,1.0.0,MIT
better-assert,1.0.2,MIT
bfj-node4,5.2.1,MIT
big.js,3.1.3,MIT
binary-extensions,1.11.0,MIT
-binaryextensions,2.1.1,MIT
bindata,2.4.3,ruby
-bitsyntax,0.0.4,UNKNOWN
+bitsyntax,0.0.4,Unknown
bl,1.1.2,MIT
blackst0ne-mermaid,7.1.0-fixed,MIT
blob,0.0.4,MIT*
-block-stream,0.0.9,ISC
bluebird,3.5.1,MIT
bn.js,4.11.8,MIT
body-parser,1.18.2,MIT
@@ -224,7 +225,7 @@ bonjour,3.5.0,MIT
boom,2.10.1,New BSD
boom,4.3.1,New BSD
boom,5.2.0,New BSD
-bootstrap-sass,3.3.6,MIT
+bootstrap,4.1.1,MIT
bootstrap_form,2.7.0,MIT
boxen,1.3.0,MIT
brace-expansion,1.1.11,MIT
@@ -238,9 +239,10 @@ browserify-cipher,1.0.0,MIT
browserify-des,1.0.0,MIT
browserify-rsa,4.0.1,MIT
browserify-sign,4.0.4,ISC
-browserify-zlib,0.1.4,MIT
+browserify-zlib,0.2.0,MIT
browserslist,1.7.7,MIT
buffer,4.9.1,MIT
+buffer-from,1.0.0,MIT
buffer-indexof,1.1.0,MIT
buffer-more-ints,0.0.2,MIT
buffer-xor,1.0.3,MIT
@@ -252,8 +254,8 @@ bytes,2.5.0,MIT
bytes,3.0.0,MIT
cacache,10.0.4,ISC
cache-base,1.0.1,MIT
+cache-loader,1.2.2,MIT
cacheable-request,2.1.4,MIT
-call-me-maybe,1.0.1,MIT
caller-path,0.1.0,MIT
callsite,1.0.0,MIT*
callsites,0.2.0,MIT
@@ -264,14 +266,12 @@ camelcase-keys,2.1.0,MIT
caniuse-api,1.6.1,MIT
caniuse-db,1.0.30000649,CC-BY-4.0
capture-stack-trace,1.0.0,MIT
-carrierwave,1.2.1,MIT
+carrierwave,1.2.3,MIT
caseless,0.11.0,Apache 2.0
caseless,0.12.0,Apache 2.0
cause,0.1,MIT
center-align,0.1.3,MIT
-chalk,0.4.0,MIT
chalk,1.1.3,MIT
-chalk,2.4.0,MIT
chalk,2.4.1,MIT
chardet,0.4.2,MIT
charenc,0.0.2,New BSD
@@ -293,23 +293,13 @@ clap,1.1.3,MIT
class-utils,0.3.6,MIT
classlist-polyfill,1.2.0,Unlicense
cli-boxes,1.0.0,MIT
-cli-cursor,1.0.2,MIT
cli-cursor,2.1.0,MIT
-cli-spinners,0.1.2,MIT
-cli-table,0.3.1,MIT
-cli-truncate,0.2.1,MIT
cli-width,2.1.0,ISC
clipboard,1.7.1,MIT
cliui,2.1.0,ISC
cliui,4.0.0,ISC
-clone,1.0.2,MIT
clone,1.0.3,MIT
-clone,2.1.1,MIT
-clone-buffer,1.0.0,MIT
clone-response,1.0.2,MIT
-clone-stats,0.0.1,MIT
-clone-stats,1.0.0,MIT
-cloneable-readable,1.0.0,MIT
co,3.0.6,MIT
co,4.6.0,MIT
coa,1.0.1,MIT
@@ -321,7 +311,6 @@ color-convert,1.9.1,MIT
color-name,1.1.2,MIT
color-string,0.3.0,MIT
colormin,1.1.2,MIT
-colors,1.0.3,MIT
colors,1.1.2,MIT
combine-lists,1.0.1,MIT
combined-stream,1.0.6,MIT
@@ -336,7 +325,7 @@ compressible,2.0.11,MIT
compression,1.7.0,MIT
compression-webpack-plugin,1.1.11,MIT
concat-map,0.0.1,MIT
-concat-stream,1.6.0,MIT
+concat-stream,1.6.2,MIT
concurrent-ruby-ext,1.0.5,MIT
configstore,3.1.1,Simplified BSD
connect,3.6.6,MIT
@@ -344,7 +333,7 @@ connect-history-api-fallback,1.3.0,MIT
connection_pool,2.2.1,MIT
console-browserify,1.1.0,MIT
console-control-strings,1.1.0,ISC
-consolidate,0.14.5,MIT
+consolidate,0.15.1,MIT
constants-browserify,1.0.0,MIT
contains-path,0.1.0,MIT
content-disposition,0.5.2,MIT
@@ -354,11 +343,9 @@ cookie,0.3.1,MIT
cookie-signature,1.0.6,MIT
copy-concurrently,1.0.5,ISC
copy-descriptor,0.1.1,MIT
-copy-webpack-plugin,4.5.1,MIT
core-js,2.3.0,MIT
core-js,2.5.3,MIT
core-util-is,1.0.2,MIT
-cosmiconfig,2.2.2,MIT
crack,0.4.3,MIT
crass,1.0.4,MIT
create-ecdh,4.0.0,MIT
@@ -384,8 +371,6 @@ csso,2.3.2,MIT
currently-unhandled,0.4.1,MIT
custom-event,1.0.1,MIT
cyclist,0.2.2,MIT*
-d,0.1.1,MIT
-d,1.0.0,MIT
d3,3.5.17,New BSD
d3-array,1.2.1,New BSD
d3-axis,1.0.8,New BSD
@@ -405,13 +390,10 @@ d3-time,1.0.8,New BSD
d3-time-format,2.1.1,New BSD
d3-timer,1.0.7,New BSD
d3-transition,1.1.1,New BSD
-d3_rails,3.5.11,MIT
dagre-d3-renderer,0.4.24,MIT
dagre-layout,0.8.0,MIT
-dargs,5.1.0,MIT
dashdash,1.14.1,MIT
data-uri-to-buffer,1.2.0,MIT
-date-fns,1.29.0,MIT
date-format,1.2.0,MIT
date-now,0.1.4,MIT
dateformat,3.0.3,MIT
@@ -429,7 +411,6 @@ decode-uri-component,0.2.0,MIT
decompress-response,3.3.0,MIT
deep-equal,1.0.1,MIT
deep-extend,0.4.2,MIT
-deep-extend,0.5.1,MIT
deep-is,0.1.3,MIT
default-require-extensions,1.0.0,MIT
default_value_for,3.0.2,MIT
@@ -448,7 +429,6 @@ depd,1.1.1,MIT
des.js,1.0.0,MIT
descendants_tracker,0.0.4,MIT
destroy,1.0.4,MIT
-detect-conflict,1.0.1,MIT
detect-indent,4.0.0,MIT
detect-libc,1.0.3,Apache 2.0
detect-node,2.0.3,ISC
@@ -456,28 +436,26 @@ device_detector,1.0.0,LGPL
devise,4.4.3,MIT
devise-two-factor,3.0.0,MIT
di,0.0.1,MIT
-diff,3.4.0,New BSD
diff,3.5.0,New BSD
diff-lcs,1.3,"MIT,Artistic-2.0,GPL-2.0+"
diffie-hellman,5.0.2,MIT
diffy,3.1.0,MIT
-dir-glob,2.0.0,MIT
dns-equal,1.0.0,MIT
dns-packet,1.2.2,MIT
dns-txt,2.0.2,MIT
doctrine,1.5.0,Simplified BSD
-doctrine,2.0.0,Apache 2.0
+doctrine,2.1.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.20170404,"Simplified BSD,New BSD,Mozilla Public License 2.0"
+domain_name,0.5.20180417,"Simplified BSD,New BSD,Mozilla Public License 2.0"
domelementtype,1.1.3,Simplified BSD
domelementtype,1.3.0,Simplified BSD
domhandler,2.4.1,Simplified BSD
domutils,1.6.2,Simplified BSD
doorkeeper,4.3.2,MIT
-doorkeeper-openid_connect,1.3.0,MIT
+doorkeeper-openid_connect,1.5.0,MIT
dot-prop,4.2.0,MIT
double-ended-queue,2.1.0-0,MIT
dropzone,4.2.0,MIT
@@ -486,12 +464,10 @@ duplexer,0.1.1,MIT
duplexer3,0.1.4,New BSD
duplexify,3.5.3,MIT
ecc-jsbn,0.1.1,MIT
-editions,1.3.4,MIT
+ed25519,1.2.4,MIT
ee-first,1.1.1,MIT
-ejs,2.5.7,Apache 2.0
ejs,2.5.9,Apache 2.0
electron-to-chromium,1.3.3,ISC
-elegant-spinner,1.0.1,MIT
elliptic,6.4.0,MIT
email_reply_trimmer,0.1.6,MIT
emoji-unicode-version,0.2.1,MIT
@@ -506,56 +482,45 @@ enhanced-resolve,0.9.1,MIT
enhanced-resolve,4.0.0,MIT
ent,2.2.0,MIT
entities,1.1.1,Simplified BSD
-envinfo,4.4.2,MIT
equalizer,0.0.11,MIT
errno,0.1.4,MIT
errno,0.1.7,MIT
-error,7.0.2,MIT
error-ex,1.3.0,MIT
-error-ex,1.3.1,MIT
erubis,2.7.0,MIT
es-abstract,1.10.0,MIT
es-to-primitive,1.1.1,MIT
-es5-ext,0.10.24,MIT
-es6-iterator,2.0.1,MIT
-es6-map,0.1.5,MIT
es6-promise,3.0.2,MIT
-es6-set,0.1.5,MIT
-es6-symbol,3.1.1,MIT
-es6-weak-map,2.0.1,MIT
escape-html,1.0.3,MIT
escape-string-regexp,1.0.5,MIT
escape_utils,1.1.1,MIT
escodegen,1.8.1,Simplified BSD
escodegen,1.9.0,Simplified BSD
-escope,3.6.0,Simplified BSD
-eslint,3.19.0,MIT
-eslint-config-airbnb-base,10.0.1,MIT
-eslint-import-resolver-node,0.2.3,MIT
-eslint-import-resolver-webpack,0.8.3,MIT
-eslint-module-utils,2.0.0,MIT
-eslint-plugin-filenames,1.1.0,MIT
-eslint-plugin-html,2.0.1,ISC
-eslint-plugin-import,2.2.0,MIT
+eslint,4.12.1,MIT
+eslint-config-airbnb-base,12.1.0,MIT
+eslint-import-resolver-node,0.3.2,MIT
+eslint-import-resolver-webpack,0.10.0,MIT
+eslint-module-utils,2.2.0,MIT
+eslint-plugin-filenames,1.2.0,MIT
+eslint-plugin-html,4.0.3,ISC
+eslint-plugin-import,2.12.0,MIT
eslint-plugin-jasmine,2.2.0,MIT
-eslint-plugin-promise,3.5.0,ISC
-eslint-plugin-vue,4.0.1,MIT
+eslint-plugin-promise,3.8.0,ISC
+eslint-plugin-vue,4.5.0,MIT
+eslint-restricted-globals,0.1.1,MIT
eslint-scope,3.7.1,Simplified BSD
eslint-visitor-keys,1.0.0,Apache 2.0
-espree,3.5.2,Simplified BSD
+espree,3.5.4,Simplified BSD
esprima,2.7.3,Simplified BSD
esprima,3.1.3,Simplified BSD
esprima,4.0.0,Simplified BSD
-esquery,1.0.0,New BSD
-esrecurse,4.1.0,Simplified BSD
+esquery,1.0.1,New BSD
+esrecurse,4.2.1,Simplified BSD
estraverse,1.9.3,Simplified BSD
-estraverse,4.1.1,Simplified BSD
estraverse,4.2.0,Simplified BSD
esutils,2.0.2,Simplified BSD
et-orbi,1.0.3,MIT
etag,1.8.1,MIT
eve-raphael,0.5.0,Apache 2.0
-event-emitter,0.3.5,MIT
event-stream,3.3.4,MIT
eventemitter3,1.2.0,MIT
events,1.1.1,MIT
@@ -564,20 +529,17 @@ evp_bytestokey,1.0.3,MIT
excon,0.62.0,MIT
execa,0.7.0,MIT
execjs,2.6.0,MIT
-exit-hook,1.1.1,MIT
expand-braces,0.1.2,MIT
expand-brackets,0.1.5,MIT
expand-brackets,2.1.4,MIT
expand-range,0.1.1,MIT
expand-range,1.8.2,MIT
-expand-tilde,2.0.2,MIT
exports-loader,0.7.0,MIT
express,4.16.2,MIT
expression_parser,0.9.0,MIT
extend,3.0.1,MIT
extend-shallow,2.0.1,MIT
extend-shallow,3.0.2,MIT
-external-editor,2.1.0,MIT
external-editor,2.2.0,MIT
extglob,0.3.2,MIT
extglob,2.0.4,MIT
@@ -587,7 +549,6 @@ faraday,0.12.2,MIT
faraday_middleware,0.12.2,MIT
faraday_middleware-multi_json,0.0.6,MIT
fast-deep-equal,1.0.0,MIT
-fast-glob,2.2.1,MIT
fast-json-stable-stringify,2.0.0,MIT
fast-levenshtein,2.0.6,MIT
fast_blank,1.0.0,MIT
@@ -596,7 +557,6 @@ fastparse,1.1.1,MIT
faye-websocket,0.10.0,MIT
faye-websocket,0.11.1,MIT
ffi,1.9.18,New BSD
-figures,1.7.0,MIT
figures,2.0.0,MIT
file-entry-cache,2.0.0,MIT
file-loader,1.1.11,MIT
@@ -608,16 +568,14 @@ fill-range,2.2.3,MIT
fill-range,4.0.0,MIT
finalhandler,1.1.0,MIT
find-cache-dir,1.0.0,MIT
-find-root,0.1.2,MIT
+find-root,1.1.0,MIT
find-up,1.1.2,MIT
find-up,2.1.0,MIT
-first-chunk-stream,2.0.0,MIT
flat-cache,1.2.2,MIT
flatten,1.0.2,MIT
flipper,0.13.0,MIT
flipper-active_record,0.13.0,MIT
flipper-active_support_cache_store,0.13.0,MIT
-flow-parser,0.66.0,MIT
flowdock,0.7.1,MIT
flush-write-stream,1.0.2,MIT
fog-aliyun,0.2.0,MIT
@@ -646,13 +604,13 @@ fresh,0.5.2,MIT
from,0.1.7,MIT
from2,2.3.0,MIT
fs-access,1.0.1,MIT
+fs-minipass,1.2.5,ISC
fs-write-stream-atomic,1.0.10,ISC
fs.realpath,1.0.0,ISC
-fsevents,1.1.3,MIT
-fstream,1.0.11,ISC
-fstream-ignore,1.0.5,ISC
+fsevents,1.2.4,MIT
ftp,0.3.10,MIT
function-bind,1.1.1,MIT
+functional-red-black-tree,1.0.1,MIT
fuzzaldrin-plus,0.5.0,MIT
gauge,2.7.4,ISC
gemnasium-gitlab-service,0.2.6,MIT
@@ -668,70 +626,58 @@ get_process_mem,0.2.0,MIT
getpass,0.1.7,MIT
gettext_i18n_rails,1.8.0,MIT
gettext_i18n_rails_js,1.3.0,MIT
-gh-got,6.0.0,MIT
-gitaly-proto,0.99.0,MIT
+gitaly-proto,0.105.0,MIT
github-linguist,5.3.3,MIT
github-markup,1.7.0,MIT
-github-username,4.1.0,MIT
gitlab-flowdock-git-hook,1.0.1,MIT
-gitlab-gollum-lib,4.2.7.2,MIT
-gitlab-gollum-rugged_adapter,0.4.4,MIT
+gitlab-gollum-lib,4.2.7.5,MIT
+gitlab-gollum-rugged_adapter,0.4.4.1,MIT
gitlab-grit,2.8.2,MIT
-gitlab-markup,1.6.3,MIT
+gitlab-markup,1.6.4,MIT
gitlab_omniauth-ldap,2.0.4,MIT
glob,5.0.15,ISC
-glob,7.1.1,ISC
glob,7.1.2,ISC
-glob-all,3.1.0,MIT
glob-base,0.3.0,MIT
glob-parent,2.0.0,ISC
glob-parent,3.1.0,ISC
-glob-to-regexp,0.3.0,BSD
global-dirs,0.1.1,MIT
-global-modules,1.0.0,MIT
-global-prefix,1.0.2,MIT
+global-modules-path,2.1.0,Apache 2.0
globalid,0.4.1,MIT
-globals,10.4.0,MIT
+globals,11.5.0,MIT
globals,9.18.0,MIT
globby,5.0.0,MIT
globby,6.1.0,MIT
-globby,7.1.1,MIT
-globby,8.0.1,MIT
gollum-grit_adapter,1.0.1,MIT
-gon,6.1.0,MIT
+gon,6.2.0,MIT
good-listener,1.2.2,MIT
google-api-client,0.19.8,Apache 2.0
google-protobuf,3.5.1,New BSD
googleapis-common-protos-types,1.0.1,Apache 2.0
googleauth,0.6.2,Apache 2.0
got,6.7.1,MIT
-got,7.1.0,MIT
got,8.3.0,MIT
gpgme,2.0.13,LGPL-2.1+
graceful-fs,4.1.11,ISC
-grape,1.0.2,MIT
+grape,1.0.3,MIT
grape-entity,0.7.1,MIT
-grape-route-helpers,2.1.0,MIT
+grape-path-helpers,1.0.5,MIT
grape_logging,1.7.0,MIT
+graphiql-rails,1.4.10,MIT
graphlib,2.1.1,MIT
-grouped-queue,0.3.3,MIT
+graphql,1.8.1,MIT
grpc,1.11.0,Apache 2.0
gzip-size,4.1.0,MIT
hamlit,2.6.1,MIT
handle-thing,1.2.5,MIT
handlebars,4.0.6,MIT
-har-schema,1.0.5,ISC
har-schema,2.0.0,ISC
har-validator,2.0.6,ISC
-har-validator,4.2.1,ISC
har-validator,5.0.3,ISC
has,1.0.1,MIT
has-ansi,2.0.0,MIT
has-binary2,1.0.2,MIT
-has-color,0.1.7,MIT
has-cors,1.1.0,MIT
has-flag,1.0.0,MIT
-has-flag,2.0.0,MIT
has-flag,3.0.0,MIT
has-symbol-support-x,1.3.0,MIT
has-to-string-tag-x,1.3.0,MIT
@@ -756,12 +702,11 @@ hmac-drbg,1.0.1,MIT
hoek,2.16.3,New BSD
hoek,4.2.1,New BSD
home-or-tmp,2.0.0,MIT
-homedir-polyfill,1.0.1,MIT
hosted-git-info,2.2.0,ISC
hpack.js,2.1.6,MIT
html-comment-regex,1.1.1,MIT
html-entities,1.2.0,MIT
-html-pipeline,2.7.1,MIT
+html-pipeline,2.8.3,MIT
html2text,0.2.0,MIT
htmlentities,4.3.4,MIT
htmlparser2,3.9.2,MIT
@@ -781,25 +726,27 @@ httparty,0.13.7,MIT
httpclient,2.8.3,ruby
httpntlm,1.6.1,MIT
httpreq,0.4.24,MIT
-https-browserify,0.0.1,MIT
+https-browserify,1.0.0,MIT
https-proxy-agent,1.0.0,MIT
i18n,0.9.5,MIT
+icalendar,2.4.1,ruby
ice_nine,0.11.2,MIT
iconv-lite,0.4.15,MIT
iconv-lite,0.4.19,MIT
+iconv-lite,0.4.23,MIT
icss-replace-symbols,1.1.0,ISC
icss-utils,2.1.0,ISC
-ieee754,1.1.8,New BSD
+ieee754,1.1.11,New BSD
iferr,0.1.5,MIT
-ignore,3.3.7,MIT
+ignore,3.3.8,MIT
ignore-by-default,1.0.1,ISC
+ignore-walk,3.0.1,ISC
immediate,3.0.6,MIT
import-lazy,2.1.0,MIT
import-local,1.0.0,MIT
imports-loader,0.8.0,MIT
imurmurhash,0.1.4,MIT
indent-string,2.1.0,MIT
-indent-string,3.2.0,MIT
indexes-of,1.0.1,MIT
indexof,0.0.1,MIT*
inflection,1.10.0,MIT
@@ -809,11 +756,9 @@ influxdb,0.2.3,MIT
inherits,2.0.1,ISC
inherits,2.0.3,ISC
ini,1.3.5,ISC
-inquirer,0.12.0,MIT
inquirer,3.3.0,MIT
inquirer,5.2.0,MIT
internal-ip,1.2.0,MIT
-interpret,1.0.1,MIT
interpret,1.1.0,MIT
into-stream,3.1.0,MIT
invariant,2.2.2,New BSD
@@ -822,7 +767,6 @@ ip,1.0.1,MIT
ip,1.1.5,MIT
ipaddr.js,1.6.0,MIT
ipaddress,0.8.3,MIT
-is-absolute,0.2.6,MIT
is-absolute-url,2.1.0,MIT
is-accessor-descriptor,0.1.6,MIT
is-accessor-descriptor,1.0.0,MIT
@@ -836,7 +780,6 @@ is-data-descriptor,1.0.0,MIT
is-date-object,1.0.1,MIT
is-descriptor,0.1.6,MIT
is-descriptor,1.0.2,MIT
-is-directory,0.3.1,MIT
is-dotfile,1.0.3,MIT
is-equal-shallow,0.1.3,MIT
is-extendable,0.1.1,MIT
@@ -859,7 +802,6 @@ is-number,3.0.0,MIT
is-number,4.0.0,MIT
is-obj,1.0.1,MIT
is-object,1.0.1,MIT
-is-observable,0.2.0,MIT
is-odd,2.0.0,MIT
is-path-cwd,1.0.0,MIT
is-path-in-cwd,1.0.0,MIT
@@ -872,17 +814,13 @@ is-promise,2.1.0,MIT
is-property,1.0.2,MIT
is-redirect,1.0.0,MIT
is-regex,1.0.4,MIT
-is-relative,0.2.1,MIT
is-resolvable,1.0.0,MIT
is-retry-allowed,1.1.0,MIT
-is-scoped,1.0.0,MIT
is-stream,1.1.0,MIT
is-svg,2.1.0,MIT
is-symbol,1.0.1,MIT
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
is-windows,1.0.2,MIT
is-wsl,1.1.0,MIT
isarray,0.0.1,MIT
@@ -899,11 +837,9 @@ istanbul-lib-coverage,1.1.1,New BSD
istanbul-lib-coverage,1.2.0,New BSD
istanbul-lib-hook,1.1.0,New BSD
istanbul-lib-instrument,1.10.1,New BSD
-istanbul-lib-instrument,1.9.1,New BSD
istanbul-lib-report,1.1.2,New BSD
istanbul-lib-source-maps,1.2.2,New BSD
istanbul-reports,1.1.3,New BSD
-istextorbinary,2.2.1,MIT
isurl,1.0.0,MIT
jasmine-core,2.9.0,MIT
jasmine-jquery,2.1.1,MIT
@@ -918,23 +854,20 @@ js-cookie,2.1.3,MIT
js-tokens,3.0.2,MIT
js-yaml,3.11.0,MIT
js-yaml,3.7.0,MIT
-js-yaml,3.9.1,MIT
jsbn,0.1.1,MIT
-jscodeshift,0.4.1,New BSD
-jscodeshift,0.5.0,New BSD
jsesc,0.5.0,MIT
jsesc,1.3.0,MIT
+jsesc,2.5.1,MIT
json,1.8.6,ruby
json-buffer,3.0.0,MIT
-json-jwt,1.9.2,MIT
+json-jwt,1.9.4,MIT
json-parse-better-errors,1.0.2,MIT
json-schema,0.2.3,BSD
json-schema-traverse,0.3.1,MIT
-json-stable-stringify,1.0.1,MIT
+json-stable-stringify-without-jsonify,1.0.1,MIT
json-stringify-safe,5.0.1,ISC
json3,3.3.2,MIT
json5,0.5.1,MIT
-jsonify,0.0.0,Public Domain
jsonpointer,4.0.1,MIT
jsprim,1.4.1,MIT
jszip,3.1.3,(MIT OR GPL-3.0)
@@ -959,62 +892,44 @@ kind-of,3.2.2,MIT
kind-of,4.0.0,MIT
kind-of,5.1.0,MIT
kind-of,6.0.2,MIT
-kubeclient,3.0.0,MIT
+kubeclient,3.1.0,MIT
latest-version,3.1.0,MIT
lazy-cache,1.0.4,MIT
lazy-cache,2.0.2,MIT
lcid,1.0.0,MIT
+leb,0.3.0,Apache 2.0
levn,0.3.0,MIT
libbase64,0.1.0,MIT
libmime,3.0.0,MIT
libqp,1.1.0,MIT
licensee,8.9.2,MIT
lie,3.1.1,MIT
-listr,0.13.0,MIT
-listr-silent-renderer,1.1.1,MIT
-listr-update-renderer,0.4.0,MIT
-listr-verbose-renderer,0.4.1,MIT
little-plugger,1.1.4,MIT
load-json-file,1.1.0,MIT
-load-json-file,4.0.0,MIT
+load-json-file,2.0.0,MIT
loader-runner,2.3.0,MIT
loader-utils,1.1.0,MIT
locale,2.1.2,"ruby,LGPLv3+"
locate-path,2.0.0,MIT
lodash,4.17.10,MIT
lodash,4.17.4,MIT
-lodash,4.17.5,MIT
-lodash._baseget,3.7.2,MIT
-lodash._topath,3.8.1,MIT
-lodash.camelcase,4.1.1,MIT
lodash.camelcase,4.3.0,MIT
-lodash.capitalize,4.2.1,MIT
lodash.clonedeep,4.5.0,MIT
-lodash.cond,4.5.2,MIT
-lodash.deburr,4.1.0,MIT
-lodash.endswith,4.2.1,MIT
lodash.escaperegexp,4.1.2,MIT
-lodash.get,3.7.0,MIT
-lodash.isarray,3.0.4,MIT
-lodash.isfunction,3.0.9,MIT
-lodash.isstring,4.0.1,MIT
-lodash.kebabcase,4.0.1,MIT
+lodash.kebabcase,4.1.1,MIT
lodash.memoize,4.1.2,MIT
lodash.mergewith,4.6.0,MIT
-lodash.snakecase,4.0.1,MIT
-lodash.startswith,4.2.1,MIT
+lodash.snakecase,4.1.1,MIT
lodash.uniq,4.5.0,MIT
-lodash.words,4.2.0,MIT
-log-symbols,1.0.2,MIT
-log-symbols,2.1.0,MIT
+lodash.upperfirst,4.3.1,MIT
log-symbols,2.2.0,MIT
-log-update,1.0.2,MIT
log4js,2.5.3,Apache 2.0
logging,2.2.2,MIT
loggly,1.1.1,MIT
loglevel,1.4.1,MIT
loglevelnext,1.0.3,MIT
lograge,0.10.0,MIT
+long,3.2.0,Apache 2.0
longest,1.0.1,MIT
loofah,2.2.2,MIT
loose-envify,1.3.1,MIT
@@ -1022,17 +937,17 @@ loud-rejection,1.6.0,MIT
lowercase-keys,1.0.0,MIT
lru-cache,2.2.4,MIT
lru-cache,2.6.5,ISC
-lru-cache,4.1.1,ISC
+lru-cache,4.1.3,ISC
macaddress,0.2.8,MIT
mail,2.7.0,MIT
mail_room,0.9.1,MIT
mailcomposer,4.0.1,MIT
mailgun-js,0.7.15,MIT
-make-dir,1.0.0,MIT
make-dir,1.2.0,MIT
+mamacro,0.0.3,MIT
map-cache,0.2.2,MIT
map-obj,1.0.1,MIT
-map-stream,0.1.0,UNKNOWN
+map-stream,0.1.0,Unknown
map-visit,1.0.0,MIT
marked,0.3.12,MIT
match-at,0.1.1,MIT
@@ -1040,24 +955,19 @@ math-expression-evaluator,1.2.16,MIT
md5.js,1.3.4,MIT
media-typer,0.3.0,MIT
mem,1.1.0,MIT
-mem-fs,1.1.3,MIT
-mem-fs-editor,4.0.1,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
-merge2,1.2.2,MIT
+merge-source-map,1.1.0,MIT
method_source,0.8.2,MIT
methods,1.1.2,MIT
micromatch,2.3.11,MIT
micromatch,3.1.10,MIT
-micromatch,3.1.6,MIT
-micromatch,3.1.9,MIT
miller-rabin,4.0.1,MIT
mime,1.4.1,MIT
mime,1.6.0,MIT
-mime,2.2.0,MIT
mime,2.3.1,MIT
mime-db,1.33.0,MIT
mime-types,2.1.18,MIT
@@ -1066,6 +976,7 @@ 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_magick,4.8.0,MIT
mini_mime,1.0.0,MIT
mini_portile2,2.3.0,MIT
minimalistic-assert,1.0.0,ISC
@@ -1073,13 +984,15 @@ minimalistic-crypto-utils,1.0.1,MIT
minimatch,3.0.4,ISC
minimist,0.0.10,MIT
minimist,0.0.8,MIT
-minimist,0.1.0,MIT
minimist,1.2.0,MIT
+minipass,2.3.3,ISC
+minizlib,1.1.0,MIT
mississippi,2.0.0,Simplified BSD
mixin-deep,1.3.1,MIT
mkdirp,0.5.1,MIT
moment,2.19.2,MIT
-monaco-editor,0.10.0,MIT
+monaco-editor,0.13.1,MIT
+monaco-editor-webpack-plugin,1.4.0,MIT
mousetrap,1.4.6,Apache 2.0
mousetrap-rails,1.4.6,"MIT,Apache"
move-concurrently,1.0.1,ISC
@@ -1089,28 +1002,25 @@ multi_json,1.13.1,MIT
multi_xml,0.6.0,MIT
multicast-dns,6.1.1,MIT
multicast-dns-service-types,1.1.0,MIT
-multimatch,2.1.0,MIT
multipart-post,2.0.0,MIT
mustermann,1.0.2,MIT
mustermann-grape,1.0.0,MIT
-mute-stream,0.0.5,ISC
mute-stream,0.0.7,ISC
mysql2,0.4.10,MIT
-nan,2.8.0,MIT
+nan,2.10.0,MIT
nanomatch,1.2.9,MIT
natural-compare,1.4.0,MIT
+needle,2.2.1,MIT
negotiator,0.6.1,MIT
neo-async,2.5.0,MIT
net-ldap,0.16.0,MIT
-net-ssh,4.2.0,MIT
+net-ssh,5.0.1,MIT
netmask,1.0.6,MIT
netrc,0.11.0,MIT
nice-try,1.0.4,MIT
-node-dir,0.1.8,MIT
node-forge,0.6.33,New BSD
-node-libs-browser,1.1.1,MIT
-node-libs-browser,2.0.0,MIT
-node-pre-gyp,0.6.39,New BSD
+node-libs-browser,2.1.0,MIT
+node-pre-gyp,0.10.0,New BSD
node-uuid,1.4.8,MIT
nodemailer,2.7.2,MIT
nodemailer-direct-transport,3.3.2,MIT
@@ -1120,8 +1030,8 @@ nodemailer-smtp-pool,2.8.2,MIT
nodemailer-smtp-transport,2.7.2,MIT
nodemailer-wellknown,0.1.10,MIT
nodemon,1.17.3,MIT
-nokogiri,1.8.2,MIT
-nomnom,1.8.1,MIT
+nokogiri,1.8.3,MIT
+nokogumbo,1.5.0,Apache 2.0
nopt,1.0.10,MIT
nopt,3.0.6,ISC
nopt,4.0.1,ISC
@@ -1130,6 +1040,8 @@ normalize-path,2.1.1,MIT
normalize-range,0.1.2,MIT
normalize-url,1.9.1,MIT
normalize-url,2.0.1,MIT
+npm-bundled,1.0.3,ISC
+npm-packlist,1.1.10,ISC
npm-run-path,2.0.2,MIT
npmlog,4.1.2,ISC
null-check,1.0.0,MIT
@@ -1147,15 +1059,15 @@ object-visit,1.0.1,MIT
object.omit,2.0.1,MIT
object.pick,1.3.0,MIT
obuf,1.1.1,MIT
-octokit,4.8.0,MIT
+octokit,4.9.0,MIT
omniauth,1.8.1,MIT
omniauth-auth0,2.0.0,MIT
-omniauth-authentiq,0.3.1,MIT
+omniauth-authentiq,0.3.3,MIT
omniauth-azure-oauth2,0.0.9,MIT
omniauth-cas3,1.1.4,MIT
omniauth-facebook,4.0.0,MIT
omniauth-github,1.3.0,MIT
-omniauth-gitlab,1.0.2,MIT
+omniauth-gitlab,1.0.3,MIT
omniauth-google-oauth2,0.5.3,MIT
omniauth-kerberos,0.3.0,MIT
omniauth-multipassword,0.4.2,MIT
@@ -1163,52 +1075,42 @@ omniauth-oauth,1.1.0,MIT
omniauth-oauth2,1.5.0,MIT
omniauth-oauth2-generic,0.2.2,MIT
omniauth-saml,1.10.0,MIT
-omniauth-shibboleth,1.2.1,MIT
+omniauth-shibboleth,1.3.0,MIT
omniauth-twitter,1.4.0,MIT
omniauth_crowd,2.2.3,MIT
on-finished,2.3.0,MIT
on-headers,1.0.1,MIT
once,1.4.0,ISC
-onetime,1.1.0,MIT
onetime,2.0.1,MIT
opener,1.4.3,(WTFPL OR MIT)
opn,5.2.0,MIT
optimist,0.6.1,MIT
optionator,0.8.2,MIT
-ora,0.2.3,MIT
org-ruby,0.9.12,MIT
original,1.0.0,MIT
orm_adapter,0.5.0,MIT
os,0.9.6,MIT
-os-browserify,0.2.1,MIT
+os-browserify,0.3.0,MIT
os-homedir,1.0.2,MIT
os-locale,2.1.0,MIT
os-tmpdir,1.0.2,MIT
osenv,0.1.5,ISC
-p-cancelable,0.3.0,MIT
p-cancelable,0.4.1,MIT
-p-each-series,1.0.0,MIT
p-finally,1.0.0,MIT
p-is-promise,1.1.0,MIT
-p-lazy,1.0.0,MIT
p-limit,1.2.0,MIT
p-locate,2.0.0,MIT
p-map,1.1.1,MIT
-p-reduce,1.0.0,MIT
-p-timeout,1.2.1,MIT
p-timeout,2.0.1,MIT
p-try,1.0.0,MIT
pac-proxy-agent,1.1.0,MIT
pac-resolver,2.0.0,MIT
package-json,4.0.1,MIT
-pako,0.2.9,MIT
pako,1.0.6,(MIT AND Zlib)
parallel-transform,1.1.0,MIT
parse-asn1,5.1.0,ISC
parse-glob,3.0.4,MIT
parse-json,2.2.0,MIT
-parse-json,4.0.0,MIT
-parse-passwd,1.0.0,MIT
parseqs,0.0.5,MIT
parseuri,0.0.5,MIT
parseurl,1.3.2,MIT
@@ -1224,7 +1126,7 @@ path-parse,1.0.5,MIT
path-proxy,1.0.0,MIT
path-to-regexp,0.1.7,MIT
path-type,1.1.0,MIT
-path-type,3.0.0,MIT
+path-type,2.0.0,MIT
pause-stream,0.0.11,Apache 2.0
pbkdf2,3.0.14,MIT
peek,1.0.1,MIT
@@ -1234,7 +1136,6 @@ peek-pg,1.3.0,MIT
peek-rblineprof,0.2.0,MIT
peek-redis,1.2.0,MIT
peek-sidekiq,1.0.3,MIT
-performance-now,0.2.0,MIT
performance-now,2.1.0,MIT
pg,0.18.4,"BSD,ruby,GPL"
pify,2.3.0,MIT
@@ -1244,15 +1145,14 @@ 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
+pluralize,7.0.0,MIT
po_to_json,1.0.1,MIT
+popper.js,1.14.3,MIT
portfinder,1.0.13,MIT
posix-character-classes,0.1.1,MIT
posix-spawn,0.3.13,MIT
postcss,5.2.16,MIT
-postcss,6.0.19,MIT
-postcss,6.0.21,MIT
+postcss,6.0.22,MIT
postcss-calc,5.3.1,MIT
postcss-colormin,2.2.2,MIT
postcss-convert-values,2.6.1,MIT
@@ -1262,9 +1162,6 @@ postcss-discard-empty,2.1.0,MIT
postcss-discard-overridden,0.1.1,MIT
postcss-discard-unused,2.2.3,MIT
postcss-filter-plugins,2.0.2,MIT
-postcss-load-config,1.2.0,MIT
-postcss-load-options,1.2.0,MIT
-postcss-load-plugins,2.3.0,MIT
postcss-merge-idents,2.1.7,MIT
postcss-merge-longhand,2.0.2,MIT
postcss-merge-rules,2.1.2,MIT
@@ -1284,6 +1181,7 @@ postcss-reduce-idents,2.4.0,MIT
postcss-reduce-initial,1.0.1,MIT
postcss-reduce-transforms,1.0.4,MIT
postcss-selector-parser,2.2.3,MIT
+postcss-selector-parser,3.1.1,MIT
postcss-svgo,2.1.6,MIT
postcss-unique-selectors,2.0.2,MIT
postcss-value-parser,3.3.0,MIT
@@ -1294,17 +1192,14 @@ premailer-rails,1.9.7,MIT
prepend-http,1.0.4,MIT
prepend-http,2.0.0,MIT
preserve,0.2.0,MIT
-prettier,1.10.2,MIT
-prettier,1.11.1,MIT
-prettier,1.8.2,MIT
-pretty-bytes,4.0.2,MIT
+prettier,1.12.1,MIT
prismjs,1.6.0,MIT
private,0.1.8,MIT
process,0.11.10,MIT
process-nextick-args,1.0.7,MIT
process-nextick-args,2.0.0,MIT
-progress,1.1.8,MIT
-prometheus-client-mmap,0.9.1,Apache 2.0
+progress,2.0.0,MIT
+prometheus-client-mmap,0.9.3,Apache 2.0
promise-inflight,1.0.1,ISC
proxy-addr,2.0.3,MIT
proxy-agent,2.0.0,MIT
@@ -1324,7 +1219,6 @@ q,1.4.1,MIT
q,1.5.0,MIT
qjobs,1.2.0,MIT
qs,6.2.3,New BSD
-qs,6.4.0,New BSD
qs,6.5.1,New BSD
query-string,4.3.2,MIT
query-string,5.1.1,MIT
@@ -1359,26 +1253,20 @@ raw-body,2.3.2,MIT
raw-loader,0.5.1,MIT
rb-fsevent,0.10.2,MIT
rb-inotify,0.9.10,MIT
-rbnacl,4.0.2,MIT
-rbnacl-libsodium,1.0.11,MIT
rc,1.2.5,(BSD-2-Clause OR MIT OR Apache-2.0)
-rdoc,4.2.2,ruby
+rdoc,6.0.4,ruby
re2,1.1.1,New BSD
-read-chunk,2.1.0,MIT
read-pkg,1.1.0,MIT
-read-pkg,3.0.0,MIT
+read-pkg,2.0.0,MIT
read-pkg-up,1.0.1,MIT
-read-pkg-up,3.0.0,MIT
+read-pkg-up,2.0.0,MIT
readable-stream,1.1.14,MIT
readable-stream,2.0.6,MIT
readable-stream,2.3.4,MIT
+readable-stream,2.3.6,MIT
readdirp,2.1.0,MIT
-readline2,1.0.1,MIT
recaptcha,3.0.0,MIT
-recast,0.12.9,MIT
-recast,0.14.7,MIT
-rechoir,0.6.2,MIT
-recursive-open-struct,1.0.5,MIT
+recursive-open-struct,1.1.0,MIT
redcarpet,3.4.0,MIT
redent,1.0.0,MIT
redis,2.8.0,MIT
@@ -1386,7 +1274,7 @@ redis,3.3.5,MIT
redis-actionpack,5.0.2,MIT
redis-activesupport,5.0.4,MIT
redis-commands,1.3.1,MIT
-redis-namespace,1.5.2,MIT
+redis-namespace,1.6.0,MIT
redis-parser,2.6.0,MIT
redis-rack,2.0.4,MIT
redis-rails,5.0.2,MIT
@@ -1409,38 +1297,28 @@ repeat-element,1.1.2,MIT
repeat-string,0.2.2,MIT
repeat-string,1.6.1,MIT
repeating,2.0.1,MIT
-replace-ext,0.0.1,MIT
-replace-ext,1.0.0,MIT
representable,3.0.4,MIT
request,2.75.0,Apache 2.0
-request,2.81.0,Apache 2.0
request,2.83.0,Apache 2.0
request_store,1.3.1,MIT
requestretry,1.13.0,MIT
-require-all,2.2.0,MIT
require-directory,2.1.1,MIT
-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.5.0,MIT
resolve,1.7.1,MIT
resolve-cwd,2.0.0,MIT
-resolve-dir,1.0.1,MIT
resolve-from,1.0.1,MIT
resolve-from,3.0.0,MIT
resolve-url,0.2.1,MIT
responders,2.4.0,MIT
responselike,1.0.2,MIT
rest-client,2.0.2,MIT
-restore-cursor,1.0.1,MIT
restore-cursor,2.0.0,MIT
ret,0.1.15,MIT
retriable,3.1.1,MIT
right-align,0.1.3,MIT
-rimraf,2.2.8,MIT
-rimraf,2.6.1,ISC
rimraf,2.6.2,ISC
rinku,2.0.0,ISC
ripemd160,2.0.1,MIT
@@ -1451,32 +1329,32 @@ rqrcode-rails3,0.1.7,MIT
ruby-enum,0.7.2,MIT
ruby-fogbugz,0.2.1,MIT
ruby-prof,0.17.0,Simplified BSD
+ruby-progressbar,1.9.0,MIT
ruby-saml,1.7.2,MIT
ruby_parser,3.9.0,MIT
rubyntlm,0.6.2,MIT
rubypants,0.2.0,BSD
rufus-scheduler,3.4.0,MIT
-rugged,0.27.0,MIT
-run-async,0.1.0,MIT
+rugged,0.27.2,MIT
run-async,2.3.0,MIT
run-queue,1.0.3,ISC
-rx-lite,3.1.2,Apache 2.0
rx-lite,4.0.8,Apache 2.0
rx-lite-aggregates,4.0.8,Apache 2.0
rxjs,5.5.10,Apache 2.0
safe-buffer,5.1.1,MIT
+safe-buffer,5.1.2,MIT
safe-regex,1.1.0,MIT
safe_yaml,1.0.4,MIT
-sanitize,2.1.0,MIT
+safer-buffer,2.1.2,MIT
+sanitize,4.6.5,MIT
sanitize-html,1.16.3,MIT
sass,3.5.5,MIT
sass-listen,4.0.0,MIT
sass-rails,5.0.6,MIT
sawyer,0.8.1,MIT
sax,1.2.2,ISC
+sax,1.2.4,ISC
schema-utils,0.4.5,MIT
-scoped-regex,1.0.0,MIT
-securecompare,1.0.0,MIT
seed-fu,2.3.7,MIT
select,1.1.2,MIT
select-hose,2.0.0,MIT
@@ -1484,7 +1362,6 @@ select2,3.5.2-browserify,Apache*
select2-rails,3.5.9.3,MIT
selfsigned,1.10.1,MIT
semver,5.0.3,ISC
-semver,5.3.0,ISC
semver,5.5.0,ISC
semver-diff,2.1.0,MIT
send,0.16.1,MIT
@@ -1506,9 +1383,7 @@ sha.js,2.4.10,MIT
sha1,1.1.1,New BSD
shebang-command,1.2.0,MIT
shebang-regex,1.0.0,MIT
-shelljs,0.7.8,New BSD
-shelljs,0.8.1,New BSD
-sidekiq,5.0.5,LGPL
+sidekiq,5.1.3,LGPL
sidekiq-cron,0.6.0,MIT
sidekiq-limit_fetch,3.4.0,MIT
signal-exit,3.0.2,ISC
@@ -1516,8 +1391,7 @@ signet,0.8.1,Apache 2.0
slack-node,0.2.0,MIT
slack-notifier,1.5.1,MIT
slash,1.0.0,MIT
-slice-ansi,0.0.4,MIT
-slide,1.1.6,ISC
+slice-ansi,1.0.0,MIT
smart-buffer,1.1.15,MIT
smtp-connection,2.12.0,MIT
snapdragon,0.8.1,MIT
@@ -1536,6 +1410,7 @@ socks,1.1.9,MIT
socks-proxy-agent,2.1.1,MIT
sort-keys,1.1.2,MIT
sort-keys,2.0.0,MIT
+sortablejs,1.7.0,MIT
source-list-map,2.0.0,MIT
source-map,0.2.0,New BSD
source-map,0.4.4,New BSD
@@ -1554,7 +1429,7 @@ spdy-transport,2.0.20,MIT
split,0.3.3,MIT
split-string,3.1.0,MIT
sprintf-js,1.0.3,New BSD
-sprockets,3.7.1,MIT
+sprockets,3.7.2,MIT
sprockets-rails,3.2.1,MIT
sql.js,0.4.0,MIT
srcset,1.0.0,MIT
@@ -1567,53 +1442,45 @@ state_machines-activerecord,0.5.1,MIT
static-extend,0.1.2,MIT
statuses,1.3.1,MIT
statuses,1.4.0,MIT
+stickyfilljs,2.0.5,MIT
stream-browserify,2.0.1,MIT
stream-combiner,0.0.4,MIT
stream-each,1.2.2,MIT
-stream-http,2.8.0,MIT
+stream-http,2.8.2,MIT
stream-shift,1.0.0,MIT
-stream-to-observable,0.2.0,MIT
streamroller,0.7.0,MIT
strict-uri-encode,1.1.0,MIT
-string-template,0.2.1,MIT
string-width,1.0.2,MIT
string-width,2.1.1,MIT
string_decoder,0.10.31,MIT
string_decoder,1.0.3,MIT
+string_decoder,1.1.1,MIT
stringex,2.8.4,MIT
stringstream,0.0.5,MIT
-strip-ansi,0.1.1,MIT
strip-ansi,3.0.1,MIT
strip-ansi,4.0.0,MIT
strip-bom,2.0.0,MIT
strip-bom,3.0.0,MIT
-strip-bom-stream,2.0.0,MIT
strip-eof,1.0.0,MIT
strip-indent,1.0.1,MIT
strip-json-comments,2.0.1,MIT
style-loader,0.21.0,MIT
supports-color,2.0.0,MIT
supports-color,3.2.3,MIT
-supports-color,5.1.0,MIT
-supports-color,5.2.0,MIT
supports-color,5.4.0,MIT
svg4everybody,2.1.9,CC0-1.0
svgo,0.7.2,MIT
-symbol-observable,0.2.4,MIT
symbol-observable,1.0.1,MIT
sys-filesystem,1.1.6,Artistic 2.0
-table,3.8.3,New BSD
+table,4.0.2,New BSD
tapable,0.1.10,MIT
tapable,1.0.0,MIT
-tar,2.2.1,ISC
-tar-pack,3.4.1,Simplified BSD
-temp,0.8.3,MIT
+tar,4.4.4,ISC
temple,0.7.7,MIT
term-size,1.2.0,MIT
test-exclude,4.2.1,ISC
text,1.3.1,MIT
text-table,0.2.0,MIT
-textextensions,2.2.0,MIT
thor,0.19.4,MIT
thread_safe,0.3.6,Apache 2.0
three,0.84.0,MIT
@@ -1626,8 +1493,7 @@ thunky,0.1.0,MIT*
tilt,2.0.6,MIT
timeago.js,3.0.2,MIT
timed-out,4.0.1,MIT
-timers-browserify,1.4.2,MIT
-timers-browserify,2.0.4,MIT
+timers-browserify,2.0.10,MIT
timespan,2.3.0,MIT
timfel-krb5-auth,0.8.3,LGPL
tiny-emitter,2.0.2,MIT
@@ -1637,7 +1503,6 @@ to-arraybuffer,1.0.1,MIT
to-fast-properties,1.0.3,MIT
to-fast-properties,2.0.0,MIT
to-object-path,0.3.0,MIT
-to-regex,3.0.1,MIT
to-regex,3.0.2,MIT
to-regex-range,2.1.1,MIT
toml-rb,1.0.0,MIT
@@ -1665,11 +1530,8 @@ uglify-es,3.3.9,Simplified BSD
uglify-js,2.8.29,Simplified BSD
uglify-to-browserify,1.0.2,MIT
uglifyjs-webpack-plugin,1.2.5,MIT
-uid-number,0.0.6,ISC
ultron,1.1.1,MIT
-unc-path-regex,0.1.2,MIT
undefsafe,2.0.2,MIT
-underscore,1.6.0,MIT
underscore,1.7.0,MIT
underscore,1.9.0,MIT
unf,0.1.4,BSD
@@ -1685,9 +1547,8 @@ unique-slug,2.0.0,ISC
unique-string,1.0.0,MIT
unpipe,1.0.0,MIT
unset-value,1.0.0,MIT
-untildify,3.0.2,MIT
unzip-response,2.0.1,MIT
-upath,1.0.2,MIT
+upath,1.0.5,MIT
update-notifier,2.3.0,Simplified BSD
urix,0.1.0,MIT
url,0.11.0,MIT
@@ -1699,16 +1560,14 @@ url-parse,1.1.9,MIT
url-parse-lax,1.0.0,MIT
url-parse-lax,3.0.0,MIT
url-to-options,1.0.1,MIT
-url_safe_base64,0.2.2,MIT
use,2.0.2,MIT
-user-home,2.0.0,MIT
useragent,2.2.1,MIT
util,0.10.3,MIT
util-deprecate,1.0.2,MIT
utils-merge,1.0.1,MIT
uuid,3.2.1,MIT
uws,9.14.0,Zlib
-v8-compile-cache,1.1.2,MIT
+v8-compile-cache,2.0.0,MIT
validate-npm-package-license,3.0.1,Apache 2.0
validates_hostname,1.0.6,MIT
vary,1.1.1,MIT
@@ -1716,18 +1575,15 @@ vary,1.1.2,MIT
vendors,1.0.1,MIT
verror,1.10.0,MIT
version_sorter,2.1.0,MIT
-vinyl,1.2.0,MIT
-vinyl,2.1.0,MIT
-vinyl-file,2.0.0,MIT
virtus,1.0.5,MIT
visibilityjs,1.2.4,MIT
vm-browserify,0.0.4,MIT
vmstat,2.3.0,MIT
void-elements,2.0.1,MIT
vue,2.5.16,MIT
-vue-eslint-parser,2.0.1,MIT
+vue-eslint-parser,2.0.3,MIT
vue-hot-reload-api,2.3.0,MIT
-vue-loader,14.2.2,MIT
+vue-loader,15.2.0,MIT
vue-resource,1.5.0,MIT
vue-router,3.0.1,MIT
vue-style-loader,4.1.0,MIT
@@ -1738,17 +1594,14 @@ vuex,3.0.1,MIT
warden,1.2.7,MIT
watchpack,1.5.0,MIT
wbuf,1.7.2,MIT
-webpack,4.7.0,MIT
-webpack-addons,1.1.5,MIT
+webpack,4.11.1,MIT
webpack-bundle-analyzer,2.11.1,MIT
-webpack-cli,2.1.2,MIT
+webpack-cli,3.0.2,MIT
webpack-dev-middleware,2.0.6,MIT
webpack-dev-middleware,3.1.3,MIT
webpack-dev-server,3.1.4,MIT
-webpack-log,1.1.2,MIT
webpack-log,1.2.0,MIT
webpack-rails,0.9.10,MIT
-webpack-sources,1.0.1,MIT
webpack-sources,1.1.0,MIT
webpack-stats-plugin,0.2.1,MIT
websocket-driver,0.6.5,MIT
@@ -1765,11 +1618,10 @@ wordwrap,0.0.2,MIT
wordwrap,0.0.3,MIT
wordwrap,1.0.0,MIT
worker-farm,1.5.2,MIT
-worker-loader,1.1.1,MIT
+worker-loader,2.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
write-file-atomic,2.3.0,ISC
ws,3.3.3,MIT
ws,4.0.0,MIT
@@ -1781,12 +1633,9 @@ xtend,4.0.1,MIT
y18n,3.2.1,ISC
y18n,4.0.0,ISC
yallist,2.1.2,ISC
-yargs,1.2.6,MIT
+yallist,3.0.2,ISC
yargs,11.0.0,MIT
yargs,11.1.0,MIT
yargs,3.10.0,MIT
yargs-parser,9.0.2,ISC
yeast,0.1.2,MIT
-yeoman-environment,2.0.5,Simplified BSD
-yeoman-environment,2.0.6,Simplified BSD
-yeoman-generator,2.0.5,Simplified BSD
diff --git a/vendor/project_templates/express.tar.gz b/vendor/project_templates/express.tar.gz
index 8dd5fa36987..fb357639a69 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 89337dc5c31..8454d2fc03b 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 31c90d0820f..55e25fdbe7c 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 e0d46609f7d..d844ae4f8e9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2,68 +2,85 @@
# 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"
+"@babel/code-frame@7.0.0-beta.44":
+ version "7.0.0-beta.44"
+ resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0-beta.44.tgz#2a02643368de80916162be70865c97774f3adbd9"
dependencies:
- chalk "^2.0.0"
- esutils "^2.0.2"
- js-tokens "^3.0.0"
+ "@babel/highlight" "7.0.0-beta.44"
-"@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"
+"@babel/generator@7.0.0-beta.44":
+ version "7.0.0-beta.44"
+ resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.0.0-beta.44.tgz#c7e67b9b5284afcf69b309b50d7d37f3e5033d42"
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/types" "7.0.0-beta.44"
+ jsesc "^2.5.1"
+ lodash "^4.2.0"
+ source-map "^0.5.0"
+ trim-right "^1.0.1"
-"@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"
+"@babel/helper-function-name@7.0.0-beta.44":
+ version "7.0.0-beta.44"
+ resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.44.tgz#e18552aaae2231100a6e485e03854bc3532d44dd"
dependencies:
- "@babel/types" "7.0.0-beta.32"
+ "@babel/helper-get-function-arity" "7.0.0-beta.44"
+ "@babel/template" "7.0.0-beta.44"
+ "@babel/types" "7.0.0-beta.44"
-"@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"
+"@babel/helper-get-function-arity@7.0.0-beta.44":
+ version "7.0.0-beta.44"
+ resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.44.tgz#d03ca6dd2b9f7b0b1e6b32c56c72836140db3a15"
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/types" "7.0.0-beta.44"
-"@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"
+"@babel/helper-split-export-declaration@7.0.0-beta.44":
+ version "7.0.0-beta.44"
+ resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0-beta.44.tgz#c0b351735e0fbcb3822c8ad8db4e583b05ebd9dc"
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"
+ "@babel/types" "7.0.0-beta.44"
+
+"@babel/highlight@7.0.0-beta.44":
+ version "7.0.0-beta.44"
+ resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0-beta.44.tgz#18c94ce543916a80553edcdcf681890b200747d5"
+ dependencies:
+ chalk "^2.0.0"
+ esutils "^2.0.2"
+ js-tokens "^3.0.0"
+
+"@babel/template@7.0.0-beta.44":
+ version "7.0.0-beta.44"
+ resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.0.0-beta.44.tgz#f8832f4fdcee5d59bf515e595fc5106c529b394f"
+ dependencies:
+ "@babel/code-frame" "7.0.0-beta.44"
+ "@babel/types" "7.0.0-beta.44"
+ babylon "7.0.0-beta.44"
+ lodash "^4.2.0"
+
+"@babel/traverse@7.0.0-beta.44":
+ version "7.0.0-beta.44"
+ resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.0.0-beta.44.tgz#a970a2c45477ad18017e2e465a0606feee0d2966"
+ dependencies:
+ "@babel/code-frame" "7.0.0-beta.44"
+ "@babel/generator" "7.0.0-beta.44"
+ "@babel/helper-function-name" "7.0.0-beta.44"
+ "@babel/helper-split-export-declaration" "7.0.0-beta.44"
+ "@babel/types" "7.0.0-beta.44"
+ babylon "7.0.0-beta.44"
+ debug "^3.1.0"
+ globals "^11.1.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"
+"@babel/types@7.0.0-beta.44":
+ version "7.0.0-beta.44"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.0.0-beta.44.tgz#6b1b164591f77dec0a0342aca995f2d046b3a757"
dependencies:
esutils "^2.0.2"
lodash "^4.2.0"
to-fast-properties "^2.0.0"
-"@gitlab-org/gitlab-svgs@^1.22.0":
- version "1.22.0"
- resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.22.0.tgz#9f2daefebcda911cba8341313c8c464c8087fe44"
-
-"@mrmlnc/readdir-enhanced@^2.2.1":
- version "2.2.1"
- resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde"
- dependencies:
- call-me-maybe "^1.0.1"
- glob-to-regexp "^0.3.0"
+"@gitlab-org/gitlab-svgs@^1.25.0":
+ version "1.25.0"
+ resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.25.0.tgz#1a82b1be43e1a46e6b0767ef46f26f5fd6bbd101"
"@sindresorhus/is@^0.7.0":
version "0.7.0"
@@ -87,6 +104,141 @@
source-map "^0.5.6"
vue-template-es2015-compiler "^1.6.0"
+"@webassemblyjs/ast@1.5.10":
+ version "1.5.10"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.5.10.tgz#7f1e81149ca4e103c9e7cc321ea0dcb83a392512"
+ dependencies:
+ "@webassemblyjs/helper-module-context" "1.5.10"
+ "@webassemblyjs/helper-wasm-bytecode" "1.5.10"
+ "@webassemblyjs/wast-parser" "1.5.10"
+ debug "^3.1.0"
+ mamacro "^0.0.3"
+
+"@webassemblyjs/floating-point-hex-parser@1.5.10":
+ version "1.5.10"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.5.10.tgz#ae48705fd58927df62023f114520b8215330ff86"
+
+"@webassemblyjs/helper-api-error@1.5.10":
+ version "1.5.10"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.5.10.tgz#0baf9453ce2fd8db58f0fdb4fb2852557c71d5a7"
+
+"@webassemblyjs/helper-buffer@1.5.10":
+ version "1.5.10"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.5.10.tgz#abee4284161e9cd6ba7619785ca277bfcb8052ce"
+ dependencies:
+ debug "^3.1.0"
+
+"@webassemblyjs/helper-code-frame@1.5.10":
+ version "1.5.10"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.5.10.tgz#4e23c05431665f16322104580af7c06253d4b4e0"
+ dependencies:
+ "@webassemblyjs/wast-printer" "1.5.10"
+
+"@webassemblyjs/helper-fsm@1.5.10":
+ version "1.5.10"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.5.10.tgz#490bab613ea255a9272b764826d3cc9d15170676"
+
+"@webassemblyjs/helper-module-context@1.5.10":
+ version "1.5.10"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.5.10.tgz#6fca93585228bf33e6da076d0a1373db1fdd6580"
+ dependencies:
+ mamacro "^0.0.3"
+
+"@webassemblyjs/helper-wasm-bytecode@1.5.10":
+ version "1.5.10"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.5.10.tgz#90f6da93c7a186bfb2f587de442982ff533c4b44"
+
+"@webassemblyjs/helper-wasm-section@1.5.10":
+ version "1.5.10"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.5.10.tgz#d64292a19f7f357c49719461065efdf7ec975d66"
+ dependencies:
+ "@webassemblyjs/ast" "1.5.10"
+ "@webassemblyjs/helper-buffer" "1.5.10"
+ "@webassemblyjs/helper-wasm-bytecode" "1.5.10"
+ "@webassemblyjs/wasm-gen" "1.5.10"
+ debug "^3.1.0"
+
+"@webassemblyjs/ieee754@1.5.10":
+ version "1.5.10"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.5.10.tgz#257cad440dd6c8a339402d31e035ba2e38e9c245"
+ dependencies:
+ ieee754 "^1.1.11"
+
+"@webassemblyjs/leb128@1.5.10":
+ version "1.5.10"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.5.10.tgz#a8e4fe5f4b16daadb241fcc44d9735e9f27b05a3"
+ dependencies:
+ leb "^0.3.0"
+
+"@webassemblyjs/utf8@1.5.10":
+ version "1.5.10"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.5.10.tgz#0b3b6bc86b7619c5dc7b2789db6665aa35689983"
+
+"@webassemblyjs/wasm-edit@1.5.10":
+ version "1.5.10"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.5.10.tgz#0fe80f19e57f669eab1caa8c1faf9690b259d5b9"
+ dependencies:
+ "@webassemblyjs/ast" "1.5.10"
+ "@webassemblyjs/helper-buffer" "1.5.10"
+ "@webassemblyjs/helper-wasm-bytecode" "1.5.10"
+ "@webassemblyjs/helper-wasm-section" "1.5.10"
+ "@webassemblyjs/wasm-gen" "1.5.10"
+ "@webassemblyjs/wasm-opt" "1.5.10"
+ "@webassemblyjs/wasm-parser" "1.5.10"
+ "@webassemblyjs/wast-printer" "1.5.10"
+ debug "^3.1.0"
+
+"@webassemblyjs/wasm-gen@1.5.10":
+ version "1.5.10"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.5.10.tgz#8b29ddd3651259408ae5d5c816a011fb3f3f3584"
+ dependencies:
+ "@webassemblyjs/ast" "1.5.10"
+ "@webassemblyjs/helper-wasm-bytecode" "1.5.10"
+ "@webassemblyjs/ieee754" "1.5.10"
+ "@webassemblyjs/leb128" "1.5.10"
+ "@webassemblyjs/utf8" "1.5.10"
+
+"@webassemblyjs/wasm-opt@1.5.10":
+ version "1.5.10"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.5.10.tgz#569e45ab1b2bf0a7706cdf6d1b51d1188e9e4c7b"
+ dependencies:
+ "@webassemblyjs/ast" "1.5.10"
+ "@webassemblyjs/helper-buffer" "1.5.10"
+ "@webassemblyjs/wasm-gen" "1.5.10"
+ "@webassemblyjs/wasm-parser" "1.5.10"
+ debug "^3.1.0"
+
+"@webassemblyjs/wasm-parser@1.5.10":
+ version "1.5.10"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.5.10.tgz#3e1017e49f833f46b840db7cf9d194d4f00037ff"
+ dependencies:
+ "@webassemblyjs/ast" "1.5.10"
+ "@webassemblyjs/helper-api-error" "1.5.10"
+ "@webassemblyjs/helper-wasm-bytecode" "1.5.10"
+ "@webassemblyjs/ieee754" "1.5.10"
+ "@webassemblyjs/leb128" "1.5.10"
+ "@webassemblyjs/wasm-parser" "1.5.10"
+
+"@webassemblyjs/wast-parser@1.5.10":
+ version "1.5.10"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.5.10.tgz#1a3235926483c985a00ee8ebca856ffda9544934"
+ dependencies:
+ "@webassemblyjs/ast" "1.5.10"
+ "@webassemblyjs/floating-point-hex-parser" "1.5.10"
+ "@webassemblyjs/helper-api-error" "1.5.10"
+ "@webassemblyjs/helper-code-frame" "1.5.10"
+ "@webassemblyjs/helper-fsm" "1.5.10"
+ long "^3.2.0"
+ mamacro "^0.0.3"
+
+"@webassemblyjs/wast-printer@1.5.10":
+ version "1.5.10"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.5.10.tgz#adb38831ba45efd0a5c7971b666e179b64f68bba"
+ dependencies:
+ "@webassemblyjs/ast" "1.5.10"
+ "@webassemblyjs/wast-parser" "1.5.10"
+ long "^3.2.0"
+
abbrev@1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
@@ -118,9 +270,9 @@ acorn@^3.0.4:
version "3.3.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
-acorn@^5.0.0, acorn@^5.2.1, acorn@^5.3.0:
- version "5.4.1"
- resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.4.1.tgz#fdc58d9d17f4a4e98d102ded826a9b9759125102"
+acorn@^5.0.0, acorn@^5.3.0, acorn@^5.5.0:
+ version "5.6.2"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.6.2.tgz#b1da1d7be2ac1b4a327fb9eab851702c5045b4e7"
addressparser@1.0.1:
version "1.0.1"
@@ -137,22 +289,15 @@ agent-base@2:
extend "~3.0.0"
semver "~5.0.1"
-ajv-keywords@^1.0.0:
- version "1.5.1"
- resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c"
+ajv-keywords@^2.1.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762"
ajv-keywords@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.1.0.tgz#ac2b27939c543e95d2c06e7f7f5c27be4aa543be"
-ajv@^4.7.0, ajv@^4.9.1:
- version "4.11.8"
- resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536"
- dependencies:
- co "^4.6.0"
- json-stable-stringify "^1.0.1"
-
-ajv@^5.1.0:
+ajv@^5.1.0, ajv@^5.2.3, ajv@^5.3.0:
version "5.5.2"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965"
dependencies:
@@ -201,10 +346,6 @@ ansi-align@^2.0.0:
dependencies:
string-width "^2.0.0"
-ansi-escapes@^1.0.0, ansi-escapes@^1.1.0:
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e"
-
ansi-escapes@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.0.0.tgz#ec3e8b4e9f8064fc02c3ac9b65f1c275bda8ef92"
@@ -231,14 +372,6 @@ ansi-styles@^3.2.1:
dependencies:
color-convert "^1.9.0"
-ansi-styles@~1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.0.0.tgz#cb102df1c56f5123eab8b67cd7b98027a0279178"
-
-any-observable@^0.2.0:
- version "0.2.0"
- resolved "https://registry.yarnpkg.com/any-observable/-/any-observable-0.2.0.tgz#c67870058003579009083f54ac0abafb5c33d242"
-
anymatch@^1.3.0:
version "1.3.2"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a"
@@ -294,10 +427,6 @@ arr-union@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4"
-array-differ@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-1.0.0.tgz#eff52e3758249d33be402b8bb8e564bb2b5d4031"
-
array-find-index@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1"
@@ -381,18 +510,10 @@ assign-symbols@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
-ast-types@0.10.1:
- version "0.10.1"
- resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.10.1.tgz#f52fca9715579a14f841d67d7f8d25432ab6a3dd"
-
-ast-types@0.11.3:
+ast-types@0.x.x:
version "0.11.3"
resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.11.3.tgz#c20757fe72ee71278ea0ff3d87e5c2ca30d9edf8"
-ast-types@0.x.x:
- version "0.11.1"
- resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.11.1.tgz#5bb3a8d5ba292c3f4ae94d46df37afc30300b990"
-
async-each@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
@@ -401,22 +522,16 @@ async-limiter@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8"
-async@1.x, async@^1.4.0, async@^1.5.0, async@^1.5.2:
+async@1.x, async@^1.4.0, async@^1.5.2:
version "1.5.2"
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
-async@^2.0.0, async@^2.6.0:
+async@^2.0.0, async@^2.1.4:
version "2.6.0"
resolved "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4"
dependencies:
lodash "^4.14.0"
-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:
version "2.1.5"
resolved "https://registry.yarnpkg.com/async/-/async-2.1.5.tgz#e587c68580994ac67fc56ff86d3ac56bdbe810bc"
@@ -477,7 +592,7 @@ axios@^0.17.1:
follow-redirects "^1.2.5"
is-buffer "^1.1.5"
-babel-code-frame@^6.16.0, babel-code-frame@^6.26.0:
+babel-code-frame@^6.22.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:
@@ -509,14 +624,16 @@ babel-core@^6.26.0, babel-core@^6.26.3:
slash "^1.0.0"
source-map "^0.5.7"
-babel-eslint@^8.0.2:
- version "8.0.2"
- resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-8.0.2.tgz#e44fb9a037d749486071d52d65312f5c20aa7530"
+babel-eslint@^8.2.3:
+ version "8.2.3"
+ resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-8.2.3.tgz#1a2e6681cc9bc4473c32899e59915e19cd6733cf"
dependencies:
- "@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/code-frame" "7.0.0-beta.44"
+ "@babel/traverse" "7.0.0-beta.44"
+ "@babel/types" "7.0.0-beta.44"
+ babylon "7.0.0-beta.44"
+ eslint-scope "~3.7.1"
+ eslint-visitor-keys "^1.0.0"
babel-generator@^6.18.0, babel-generator@^6.26.0:
version "6.26.0"
@@ -690,10 +807,6 @@ babel-plugin-syntax-async-generators@^6.5.0:
version "6.13.0"
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-generators/-/babel-plugin-syntax-async-generators-6.13.0.tgz#6bc963ebb16eccbae6b92b596eb7f35c342a8b9a"
-babel-plugin-syntax-class-constructor-call@^6.18.0:
- version "6.18.0"
- resolved "https://registry.yarnpkg.com/babel-plugin-syntax-class-constructor-call/-/babel-plugin-syntax-class-constructor-call-6.18.0.tgz#9cb9d39fe43c8600bec8146456ddcbd4e1a76416"
-
babel-plugin-syntax-class-properties@^6.8.0:
version "6.13.0"
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz#d7eb23b79a317f8543962c505b827c7d6cac27de"
@@ -710,14 +823,6 @@ babel-plugin-syntax-exponentiation-operator@^6.8.0:
version "6.13.0"
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de"
-babel-plugin-syntax-export-extensions@^6.8.0:
- version "6.13.0"
- resolved "https://registry.yarnpkg.com/babel-plugin-syntax-export-extensions/-/babel-plugin-syntax-export-extensions-6.13.0.tgz#70a1484f0f9089a4e84ad44bac353c95b9b12721"
-
-babel-plugin-syntax-flow@^6.18.0:
- version "6.18.0"
- resolved "https://registry.yarnpkg.com/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz#4c3ab20a2af26aa20cd25995c398c4eb70310c8d"
-
babel-plugin-syntax-object-rest-spread@^6.13.0, babel-plugin-syntax-object-rest-spread@^6.8.0:
version "6.13.0"
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5"
@@ -742,14 +847,6 @@ babel-plugin-transform-async-to-generator@^6.24.1:
babel-plugin-syntax-async-functions "^6.8.0"
babel-runtime "^6.22.0"
-babel-plugin-transform-class-constructor-call@^6.24.1:
- version "6.24.1"
- resolved "https://registry.yarnpkg.com/babel-plugin-transform-class-constructor-call/-/babel-plugin-transform-class-constructor-call-6.24.1.tgz#80dc285505ac067dcb8d6c65e2f6f11ab7765ef9"
- dependencies:
- babel-plugin-syntax-class-constructor-call "^6.18.0"
- babel-runtime "^6.22.0"
- babel-template "^6.24.1"
-
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"
@@ -952,20 +1049,6 @@ babel-plugin-transform-exponentiation-operator@^6.24.1:
babel-plugin-syntax-exponentiation-operator "^6.8.0"
babel-runtime "^6.22.0"
-babel-plugin-transform-export-extensions@^6.22.0:
- version "6.22.0"
- resolved "https://registry.yarnpkg.com/babel-plugin-transform-export-extensions/-/babel-plugin-transform-export-extensions-6.22.0.tgz#53738b47e75e8218589eea946cbbd39109bbe653"
- dependencies:
- babel-plugin-syntax-export-extensions "^6.8.0"
- babel-runtime "^6.22.0"
-
-babel-plugin-transform-flow-strip-types@^6.8.0:
- version "6.22.0"
- resolved "https://registry.yarnpkg.com/babel-plugin-transform-flow-strip-types/-/babel-plugin-transform-flow-strip-types-6.22.0.tgz#84cb672935d43714fdc32bce84568d87441cf7cf"
- dependencies:
- babel-plugin-syntax-flow "^6.18.0"
- babel-runtime "^6.22.0"
-
babel-plugin-transform-object-rest-spread@^6.22.0:
version "6.23.0"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.23.0.tgz#875d6bc9be761c58a2ae3feee5dc4895d8c7f921"
@@ -986,7 +1069,7 @@ babel-plugin-transform-strict-mode@^6.24.1:
babel-runtime "^6.22.0"
babel-types "^6.24.1"
-babel-preset-es2015@^6.24.1, babel-preset-es2015@^6.9.0:
+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:
@@ -1036,14 +1119,6 @@ babel-preset-latest@^6.24.1:
babel-preset-es2016 "^6.24.1"
babel-preset-es2017 "^6.24.1"
-babel-preset-stage-1@^6.5.0:
- version "6.24.1"
- resolved "https://registry.yarnpkg.com/babel-preset-stage-1/-/babel-preset-stage-1-6.24.1.tgz#7692cd7dcd6849907e6ae4a0a85589cfb9e2bfb0"
- dependencies:
- babel-plugin-transform-class-constructor-call "^6.24.1"
- babel-plugin-transform-export-extensions "^6.22.0"
- babel-preset-stage-2 "^6.24.1"
-
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"
@@ -1063,7 +1138,7 @@ babel-preset-stage-3@^6.24.1:
babel-plugin-transform-exponentiation-operator "^6.24.1"
babel-plugin-transform-object-rest-spread "^6.22.0"
-babel-register@^6.26.0, babel-register@^6.9.0:
+babel-register@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071"
dependencies:
@@ -1115,18 +1190,14 @@ babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.26
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@7.0.0-beta.44:
+ version "7.0.0-beta.44"
+ resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.44.tgz#89159e15e6e30c5096e22d738d8c0af8a0e8ca1d"
-babylon@^6.17.3, babylon@^6.18.0:
+babylon@^6.18.0:
version "6.18.0"
resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3"
-babylon@^7.0.0-beta.30:
- version "7.0.0-beta.44"
- resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.44.tgz#89159e15e6e30c5096e22d738d8c0af8a0e8ca1d"
-
backo2@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947"
@@ -1195,10 +1266,6 @@ binary-extensions@^1.0.0:
version "1.11.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.11.0.tgz#46aa1751fb6a2f93ee5e689bb1087d4b14c6c205"
-binaryextensions@2:
- version "2.1.1"
- resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-2.1.1.tgz#3209a51ca4a4ad541a3b8d3d6a6d5b83a2485935"
-
bitsyntax@~0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/bitsyntax/-/bitsyntax-0.0.4.tgz#eb10cc6f82b8c490e3e85698f07e83d46e0cba82"
@@ -1226,12 +1293,6 @@ blob@0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.4.tgz#bcf13052ca54463f30f9fc7e95b9a47630a94921"
-block-stream@*:
- version "0.0.9"
- resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a"
- dependencies:
- inherits "~2.0.0"
-
bluebird@^3.1.1, bluebird@^3.3.0, bluebird@^3.4.6, bluebird@^3.5.1:
version "3.5.1"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9"
@@ -1284,9 +1345,9 @@ boom@5.x.x:
dependencies:
hoek "4.x.x"
-bootstrap-sass@^3.3.6:
- version "3.3.6"
- resolved "https://registry.yarnpkg.com/bootstrap-sass/-/bootstrap-sass-3.3.6.tgz#363b0d300e868d3e70134c1a742bb17288444fd1"
+bootstrap@~4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.1.1.tgz#3aec85000fa619085da8d2e4983dfd67cf2114cb"
boxen@^1.2.1:
version "1.3.0"
@@ -1388,11 +1449,11 @@ browserify-sign@^4.0.0:
inherits "^2.0.1"
parse-asn1 "^5.0.0"
-browserify-zlib@^0.1.4:
- version "0.1.4"
- resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.1.4.tgz#bb35f8a519f600e0fa6b8485241c979d0141fb2d"
+browserify-zlib@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f"
dependencies:
- pako "~0.2.0"
+ pako "~1.0.5"
browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6:
version "1.7.7"
@@ -1401,6 +1462,10 @@ browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6:
caniuse-db "^1.0.30000639"
electron-to-chromium "^1.2.7"
+buffer-from@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.0.0.tgz#4cb8832d23612589b0406e9e2956c17f06fdf531"
+
buffer-indexof@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.0.tgz#f54f647c4f4e25228baa656a2e57e43d5f270982"
@@ -1433,7 +1498,7 @@ buildmail@4.0.1:
nodemailer-shared "1.1.0"
punycode "1.4.1"
-builtin-modules@^1.0.0, builtin-modules@^1.1.1:
+builtin-modules@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
@@ -1502,10 +1567,6 @@ cacheable-request@^2.1.1:
normalize-url "2.0.1"
responselike "1.0.2"
-call-me-maybe@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b"
-
caller-path@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f"
@@ -1571,7 +1632,7 @@ center-align@^0.1.1:
align-text "^0.1.3"
lazy-cache "^1.0.3"
-chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
+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:
@@ -1581,7 +1642,7 @@ chalk@^1.0.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.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.1, chalk@^2.4.1:
+chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e"
dependencies:
@@ -1589,22 +1650,6 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.1, chalk@^2.4
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
-chalk@^2.3.2:
- version "2.4.0"
- resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.0.tgz#a060a297a6b57e15b61ca63ce84995daa0fe6e52"
- dependencies:
- ansi-styles "^3.2.1"
- escape-string-regexp "^1.0.5"
- supports-color "^5.3.0"
-
-chalk@~0.4.0:
- version "0.4.0"
- resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.4.0.tgz#5199a3ddcd0c1efe23bc08c1b027b06176e0c64f"
- dependencies:
- ansi-styles "~1.0.0"
- has-color "~0.1.0"
- strip-ansi "~0.1.0"
-
chardet@^0.4.0:
version "0.4.2"
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2"
@@ -1700,35 +1745,12 @@ cli-boxes@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143"
-cli-cursor@^1.0.1, cli-cursor@^1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987"
- dependencies:
- restore-cursor "^1.0.1"
-
cli-cursor@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5"
dependencies:
restore-cursor "^2.0.0"
-cli-spinners@^0.1.2:
- version "0.1.2"
- resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-0.1.2.tgz#bb764d88e185fb9e1e6a2a1f19772318f605e31c"
-
-cli-table@^0.3.1:
- version "0.3.1"
- resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23"
- dependencies:
- colors "1.0.3"
-
-cli-truncate@^0.2.1:
- version "0.2.1"
- resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-0.2.1.tgz#9f15cfbb0705005369216c626ac7d05ab90dd574"
- dependencies:
- slice-ansi "0.0.4"
- string-width "^1.0.1"
-
cli-width@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.1.0.tgz#b234ca209b29ef66fc518d9b98d5847b00edf00a"
@@ -1757,44 +1779,16 @@ cliui@^4.0.0:
strip-ansi "^4.0.0"
wrap-ansi "^2.0.0"
-clone-buffer@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58"
-
clone-response@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b"
dependencies:
mimic-response "^1.0.0"
-clone-stats@^0.0.1:
- version "0.0.1"
- resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-0.0.1.tgz#b88f94a82cf38b8791d58046ea4029ad88ca99d1"
-
-clone-stats@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-1.0.0.tgz#b3782dff8bb5474e18b9b6bf0fdfe782f8777680"
-
-clone@^1.0.0:
+clone@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.3.tgz#298d7e2231660f40c003c2ed3140decf3f53085f"
-clone@^1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.2.tgz#260b7a99ebb1edfe247538175f783243cb19d149"
-
-clone@^2.1.1:
- version "2.1.1"
- resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.1.tgz#d217d1e961118e3ac9a4b8bba3285553bf647cdb"
-
-cloneable-readable@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/cloneable-readable/-/cloneable-readable-1.0.0.tgz#a6290d413f217a61232f95e458ff38418cfb0117"
- dependencies:
- inherits "^2.0.1"
- process-nextick-args "^1.0.6"
- through2 "^2.0.1"
-
co@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
@@ -1852,11 +1846,7 @@ colormin@^1.0.5:
css-color-names "0.0.4"
has "^1.0.1"
-colors@1.0.3:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b"
-
-colors@^1.1.0, colors@^1.1.2, colors@~1.1.2:
+colors@^1.1.0, colors@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63"
@@ -1928,10 +1918,11 @@ concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
-concat-stream@^1.5.0, concat-stream@^1.5.2:
- version "1.6.0"
- resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7"
+concat-stream@^1.5.0, concat-stream@^1.6.0:
+ version "1.6.2"
+ resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
dependencies:
+ buffer-from "^1.0.0"
inherits "^2.0.3"
readable-stream "^2.2.2"
typedarray "^0.0.6"
@@ -2019,19 +2010,6 @@ copy-descriptor@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
-copy-webpack-plugin@^4.5.1:
- version "4.5.1"
- resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-4.5.1.tgz#fc4f68f4add837cc5e13d111b20715793225d29c"
- dependencies:
- cacache "^10.0.4"
- find-cache-dir "^1.0.0"
- globby "^7.1.1"
- is-glob "^4.0.0"
- loader-utils "^1.1.0"
- minimatch "^3.0.4"
- p-limit "^1.0.0"
- serialize-javascript "^1.4.0"
-
core-js@^2.2.0, core-js@^2.4.0, core-js@^2.4.1, core-js@^2.5.0:
version "2.5.3"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.3.tgz#8acc38345824f16d8365b7c9b4259168e8ed603e"
@@ -2083,7 +2061,7 @@ cropper@^2.3.0:
dependencies:
jquery ">= 1.9.1"
-cross-spawn@^5.0.1:
+cross-spawn@^5.0.1, cross-spawn@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
dependencies:
@@ -2336,18 +2314,6 @@ d3@3.5.17:
version "3.5.17"
resolved "https://registry.yarnpkg.com/d3/-/d3-3.5.17.tgz#bc46748004378b21a360c9fc7cf5231790762fb8"
-d@1:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f"
- dependencies:
- es5-ext "^0.10.9"
-
-d@^0.1.1:
- version "0.1.1"
- resolved "https://registry.yarnpkg.com/d/-/d-0.1.1.tgz#da184c535d18d8ee7ba2aa229b914009fae11309"
- 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"
@@ -2364,10 +2330,6 @@ dagre-layout@^0.8.0:
graphlib "^2.1.1"
lodash "^4.17.4"
-dargs@^5.1.0:
- version "5.1.0"
- resolved "https://registry.yarnpkg.com/dargs/-/dargs-5.1.0.tgz#ec7ea50c78564cd36c9d5ec18f66329fade27829"
-
dashdash@^1.12.0:
version "1.14.1"
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
@@ -2378,10 +2340,6 @@ data-uri-to-buffer@1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-1.2.0.tgz#77163ea9c20d8641b4707e8f18abdf9a78f34835"
-date-fns@^1.27.2:
- version "1.29.0"
- resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.29.0.tgz#12e609cdcb935127311d04d33334e2960a2a54e6"
-
date-format@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/date-format/-/date-format-1.2.0.tgz#615e828e233dd1ab9bb9ae0950e0ceccfa6ecad8"
@@ -2398,18 +2356,12 @@ de-indent@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
-debug@2, debug@2.6.9, debug@^2.1.1, debug@^2.2.0, debug@^2.3.3, debug@^2.6.6, debug@^2.6.8, debug@^2.6.9, debug@~2.6.4, debug@~2.6.6:
+debug@2, debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.6, debug@^2.6.8, debug@^2.6.9, debug@~2.6.4, debug@~2.6.6:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
dependencies:
ms "2.0.0"
-debug@2.2.0, debug@~2.2.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da"
- dependencies:
- ms "0.7.1"
-
debug@2.6.8:
version "2.6.8"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc"
@@ -2422,6 +2374,12 @@ debug@^3.0.1, debug@^3.1.0, debug@~3.1.0:
dependencies:
ms "2.0.0"
+debug@~2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da"
+ dependencies:
+ ms "0.7.1"
+
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"
@@ -2434,7 +2392,7 @@ decode-uri-component@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
-decompress-response@^3.2.0, decompress-response@^3.3.0:
+decompress-response@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3"
dependencies:
@@ -2444,10 +2402,6 @@ deep-equal@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
-deep-extend@^0.5.1:
- version "0.5.1"
- resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.5.1.tgz#b894a9dd90d3023fbf1c55a394fb858eb2066f1f"
-
deep-extend@~0.4.0:
version "0.4.2"
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f"
@@ -2550,10 +2504,6 @@ destroy@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
-detect-conflict@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/detect-conflict/-/detect-conflict-1.0.1.tgz#088657a66a961c05019db7c4230883b1c6b4176e"
-
detect-indent@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208"
@@ -2572,11 +2522,7 @@ di@^0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c"
-diff@^3.3.1, diff@^3.4.0:
- version "3.4.0"
- resolved "https://registry.yarnpkg.com/diff/-/diff-3.4.0.tgz#b1d85507daf3964828de54b37d0d73ba67dda56c"
-
-diff@^3.5.0:
+diff@^3.4.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
@@ -2588,13 +2534,6 @@ diffie-hellman@^5.0.0:
miller-rabin "^4.0.0"
randombytes "^2.0.0"
-dir-glob@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.0.0.tgz#0b205d2b6aef98238ca286598a8204d29d0a0034"
- dependencies:
- arrify "^1.0.1"
- path-type "^3.0.0"
-
dns-equal@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d"
@@ -2619,12 +2558,11 @@ doctrine@1.5.0:
esutils "^2.0.2"
isarray "^1.0.0"
-doctrine@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.0.0.tgz#c73d8d2909d22291e1a007a395804da8b665fe63"
+doctrine@^2.0.2:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
dependencies:
esutils "^2.0.2"
- isarray "^1.0.0"
document-register-element@1.3.0:
version "1.3.0"
@@ -2708,19 +2646,11 @@ ecc-jsbn@~0.1.1:
dependencies:
jsbn "~0.1.0"
-editions@^1.3.3:
- version "1.3.4"
- resolved "https://registry.yarnpkg.com/editions/-/editions-1.3.4.tgz#3662cb592347c3168eb8e498a0ff73271d67f50b"
-
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
ejs@^2.5.7:
- version "2.5.7"
- resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.7.tgz#cc872c168880ae3c7189762fd5ffc00896c9518a"
-
-ejs@^2.5.9:
version "2.5.9"
resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.9.tgz#7ba254582a560d267437109a68354112475b0ce5"
@@ -2728,10 +2658,6 @@ electron-to-chromium@^1.2.7:
version "1.3.3"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.3.tgz#651eb63fe89f39db70ffc8dbd5d9b66958bc6a0e"
-elegant-spinner@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e"
-
elliptic@^6.0.0:
version "6.4.0"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df"
@@ -2825,10 +2751,6 @@ entities@^1.1.1, entities@~1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0"
-envinfo@^4.4.2:
- version "4.4.2"
- resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-4.4.2.tgz#472c49f3a8b9bca73962641ce7cb692bf623cd1c"
-
errno@^0.1.3:
version "0.1.4"
resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.4.tgz#b896e23a9e5e8ba33871fc996abd3635fc9a1c7d"
@@ -2847,19 +2769,6 @@ error-ex@^1.2.0:
dependencies:
is-arrayish "^0.2.1"
-error-ex@^1.3.1:
- version "1.3.1"
- resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc"
- dependencies:
- is-arrayish "^0.2.1"
-
-error@^7.0.2:
- version "7.0.2"
- resolved "https://registry.yarnpkg.com/error/-/error-7.0.2.tgz#a5f75fff4d9926126ddac0ea5dc38e689153cb02"
- dependencies:
- string-template "~0.2.1"
- xtend "~4.0.0"
-
es-abstract@^1.7.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.10.0.tgz#1ecb36c197842a00d8ee4c2dfd8646bb97d60864"
@@ -2878,62 +2787,10 @@ es-to-primitive@^1.1.1:
is-date-object "^1.0.1"
is-symbol "^1.0.1"
-es5-ext@^0.10.14, es5-ext@^0.10.8, es5-ext@^0.10.9, es5-ext@~0.10.14, es5-ext@~0.10.2:
- version "0.10.24"
- resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.24.tgz#a55877c9924bc0c8d9bd3c2cbe17495ac1709b14"
- dependencies:
- es6-iterator "2"
- es6-symbol "~3.1"
-
-es6-iterator@2, es6-iterator@~2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.1.tgz#8e319c9f0453bf575d374940a655920e59ca5512"
- dependencies:
- d "1"
- es5-ext "^0.10.14"
- es6-symbol "^3.1"
-
-es6-map@^0.1.3:
- version "0.1.5"
- resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.5.tgz#9136e0503dcc06a301690f0bb14ff4e364e949f0"
- dependencies:
- d "1"
- es5-ext "~0.10.14"
- es6-iterator "~2.0.1"
- es6-set "~0.1.5"
- es6-symbol "~3.1.1"
- event-emitter "~0.3.5"
-
es6-promise@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.0.2.tgz#010d5858423a5f118979665f46486a95c6ee2bb6"
-es6-set@~0.1.5:
- version "0.1.5"
- resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.5.tgz#d2b3ec5d4d800ced818db538d28974db0a73ccb1"
- dependencies:
- d "1"
- es5-ext "~0.10.14"
- es6-iterator "~2.0.1"
- es6-symbol "3.1.1"
- event-emitter "~0.3.5"
-
-es6-symbol@3, es6-symbol@3.1.1, es6-symbol@^3.1, es6-symbol@~3.1, es6-symbol@~3.1.1:
- version "3.1.1"
- resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77"
- dependencies:
- d "1"
- es5-ext "~0.10.14"
-
-es6-weak-map@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.1.tgz#0d2bbd8827eb5fb4ba8f97fbfea50d43db21ea81"
- dependencies:
- d "^0.1.1"
- es5-ext "^0.10.8"
- es6-iterator "2"
- es6-symbol "3"
-
escape-html@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
@@ -2964,95 +2821,90 @@ escodegen@1.x.x:
optionalDependencies:
source-map "~0.5.6"
-escope@^3.6.0:
- version "3.6.0"
- resolved "https://registry.yarnpkg.com/escope/-/escope-3.6.0.tgz#e01975e812781a163a6dadfdd80398dc64c889c3"
+eslint-config-airbnb-base@^12.1.0:
+ version "12.1.0"
+ resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-12.1.0.tgz#386441e54a12ccd957b0a92564a4bafebd747944"
dependencies:
- es6-map "^0.1.3"
- es6-weak-map "^2.0.1"
- esrecurse "^4.1.0"
- estraverse "^4.1.1"
-
-eslint-config-airbnb-base@^10.0.1:
- version "10.0.1"
- resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-10.0.1.tgz#f17d4e52992c1d45d1b7713efbcd5ecd0e7e0506"
+ eslint-restricted-globals "^0.1.1"
-eslint-import-resolver-node@^0.2.0:
- version "0.2.3"
- resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.2.3.tgz#5add8106e8c928db2cba232bcd9efa846e3da16c"
+eslint-import-resolver-node@^0.3.1:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz#58f15fb839b8d0576ca980413476aab2472db66a"
dependencies:
- debug "^2.2.0"
- object-assign "^4.0.1"
- resolve "^1.1.6"
+ debug "^2.6.9"
+ resolve "^1.5.0"
-eslint-import-resolver-webpack@^0.8.3:
- version "0.8.3"
- resolved "https://registry.yarnpkg.com/eslint-import-resolver-webpack/-/eslint-import-resolver-webpack-0.8.3.tgz#ad61e28df378a474459d953f246fd43f92675385"
+eslint-import-resolver-webpack@^0.10.0:
+ version "0.10.0"
+ resolved "https://registry.yarnpkg.com/eslint-import-resolver-webpack/-/eslint-import-resolver-webpack-0.10.0.tgz#b6f2468dc3e8b4ea076e5d75bece8da932789b07"
dependencies:
array-find "^1.0.0"
debug "^2.6.8"
enhanced-resolve "~0.9.0"
- find-root "^0.1.1"
+ find-root "^1.1.0"
has "^1.0.1"
interpret "^1.0.0"
- is-absolute "^0.2.3"
- lodash.get "^3.7.0"
- node-libs-browser "^1.0.0"
- resolve "^1.2.0"
+ lodash "^4.17.4"
+ node-libs-browser "^1.0.0 || ^2.0.0"
+ resolve "^1.4.0"
semver "^5.3.0"
-eslint-module-utils@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.0.0.tgz#a6f8c21d901358759cdc35dbac1982ae1ee58bce"
+eslint-module-utils@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.2.0.tgz#b270362cd88b1a48ad308976ce7fa54e98411746"
dependencies:
- debug "2.2.0"
+ debug "^2.6.8"
pkg-dir "^1.0.0"
-eslint-plugin-filenames@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/eslint-plugin-filenames/-/eslint-plugin-filenames-1.1.0.tgz#bb925218ab25b1aad1c622cfa9cb8f43cc03a4ff"
+eslint-plugin-filenames@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-filenames/-/eslint-plugin-filenames-1.2.0.tgz#aee9c1c90189c95d2e49902c160eceefecd99f53"
dependencies:
- lodash.camelcase "4.1.1"
- lodash.kebabcase "4.0.1"
- lodash.snakecase "4.0.1"
+ lodash.camelcase "4.3.0"
+ lodash.kebabcase "4.1.1"
+ lodash.snakecase "4.1.1"
+ lodash.upperfirst "4.3.1"
-eslint-plugin-html@2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/eslint-plugin-html/-/eslint-plugin-html-2.0.1.tgz#3a829510e82522f1e2e44d55d7661a176121fce1"
+eslint-plugin-html@4.0.3:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-html/-/eslint-plugin-html-4.0.3.tgz#97d52dcf9e22724505d02719fbd02754013c8a17"
dependencies:
htmlparser2 "^3.8.2"
-eslint-plugin-import@^2.2.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.2.0.tgz#72ba306fad305d67c4816348a4699a4229ac8b4e"
+eslint-plugin-import@^2.12.0:
+ version "2.12.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.12.0.tgz#dad31781292d6664b25317fd049d2e2b2f02205d"
dependencies:
- builtin-modules "^1.1.1"
contains-path "^0.1.0"
- debug "^2.2.0"
+ debug "^2.6.8"
doctrine "1.5.0"
- eslint-import-resolver-node "^0.2.0"
- eslint-module-utils "^2.0.0"
+ eslint-import-resolver-node "^0.3.1"
+ eslint-module-utils "^2.2.0"
has "^1.0.1"
- lodash.cond "^4.3.0"
+ lodash "^4.17.4"
minimatch "^3.0.3"
- pkg-up "^1.0.0"
+ read-pkg-up "^2.0.0"
+ resolve "^1.6.0"
eslint-plugin-jasmine@^2.1.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-jasmine/-/eslint-plugin-jasmine-2.2.0.tgz#7135879383c39a667c721d302b9f20f0389543de"
-eslint-plugin-promise@^3.5.0:
- version "3.5.0"
- resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-3.5.0.tgz#78fbb6ffe047201627569e85a6c5373af2a68fca"
+eslint-plugin-promise@^3.8.0:
+ version "3.8.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-3.8.0.tgz#65ebf27a845e3c1e9d6f6a5622ddd3801694b621"
-eslint-plugin-vue@^4.0.1:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-4.0.1.tgz#afda92cfd7e7363b1fbdb1a772dd63359a9ce96a"
+eslint-plugin-vue@^4.5.0:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-4.5.0.tgz#09d6597f4849e31a3846c2c395fccf17685b69c3"
dependencies:
- require-all "^2.2.0"
- vue-eslint-parser "^2.0.1"
+ vue-eslint-parser "^2.0.3"
-eslint-scope@^3.7.1:
+eslint-restricted-globals@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/eslint-restricted-globals/-/eslint-restricted-globals-0.1.1.tgz#35f0d5cbc64c2e3ed62e93b4b1a7af05ba7ed4d7"
+
+eslint-scope@^3.7.1, eslint-scope@~3.7.1:
version "3.7.1"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.1.tgz#3d63c3edfda02e06e01a452ad88caacc7cdcb6e8"
dependencies:
@@ -3063,51 +2915,53 @@ eslint-visitor-keys@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d"
-eslint@^3.18.0:
- version "3.19.0"
- resolved "https://registry.yarnpkg.com/eslint/-/eslint-3.19.0.tgz#c8fc6201c7f40dd08941b87c085767386a679acc"
+eslint@~4.12.1:
+ version "4.12.1"
+ resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.12.1.tgz#5ec1973822b4a066b353770c3c6d69a2a188e880"
dependencies:
- babel-code-frame "^6.16.0"
- chalk "^1.1.3"
- concat-stream "^1.5.2"
- debug "^2.1.1"
- doctrine "^2.0.0"
- escope "^3.6.0"
- espree "^3.4.0"
+ ajv "^5.3.0"
+ babel-code-frame "^6.22.0"
+ chalk "^2.1.0"
+ concat-stream "^1.6.0"
+ cross-spawn "^5.1.0"
+ debug "^3.0.1"
+ doctrine "^2.0.2"
+ eslint-scope "^3.7.1"
+ espree "^3.5.2"
esquery "^1.0.0"
estraverse "^4.2.0"
esutils "^2.0.2"
file-entry-cache "^2.0.0"
- glob "^7.0.3"
- globals "^9.14.0"
- ignore "^3.2.0"
+ functional-red-black-tree "^1.0.1"
+ glob "^7.1.2"
+ globals "^11.0.1"
+ ignore "^3.3.3"
imurmurhash "^0.1.4"
- inquirer "^0.12.0"
- is-my-json-valid "^2.10.0"
+ inquirer "^3.0.6"
is-resolvable "^1.0.0"
- js-yaml "^3.5.1"
- json-stable-stringify "^1.0.0"
+ js-yaml "^3.9.1"
+ json-stable-stringify-without-jsonify "^1.0.1"
levn "^0.3.0"
- lodash "^4.0.0"
- mkdirp "^0.5.0"
+ lodash "^4.17.4"
+ minimatch "^3.0.2"
+ mkdirp "^0.5.1"
natural-compare "^1.4.0"
optionator "^0.8.2"
- path-is-inside "^1.0.1"
- pluralize "^1.2.1"
- progress "^1.1.8"
- require-uncached "^1.0.2"
- shelljs "^0.7.5"
- strip-bom "^3.0.0"
+ path-is-inside "^1.0.2"
+ pluralize "^7.0.0"
+ progress "^2.0.0"
+ require-uncached "^1.0.3"
+ semver "^5.3.0"
+ strip-ansi "^4.0.0"
strip-json-comments "~2.0.1"
- table "^3.7.8"
+ table "^4.0.1"
text-table "~0.2.0"
- user-home "^2.0.0"
-espree@^3.4.0, espree@^3.5.2:
- version "3.5.2"
- resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.2.tgz#756ada8b979e9dcfcdb30aad8d1a9304a905e1ca"
+espree@^3.5.2:
+ version "3.5.4"
+ resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.4.tgz#b0f447187c8a8bed944b815a660bddf5deb5d1a7"
dependencies:
- acorn "^5.2.1"
+ acorn "^5.5.0"
acorn-jsx "^3.0.0"
esprima@2.7.x, esprima@^2.6.0, esprima@^2.7.1:
@@ -3118,35 +2972,30 @@ esprima@3.x.x, esprima@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
-esprima@^4.0.0, esprima@~4.0.0:
+esprima@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804"
esquery@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.0.tgz#cfba8b57d7fba93f17298a8a006a04cda13d80fa"
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708"
dependencies:
estraverse "^4.0.0"
esrecurse@^4.1.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.1.0.tgz#4713b6536adf7f2ac4f327d559e7756bff648220"
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf"
dependencies:
- estraverse "~4.1.0"
- object-assign "^4.0.1"
+ estraverse "^4.1.0"
estraverse@^1.9.1:
version "1.9.3"
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-1.9.3.tgz#af67f2dc922582415950926091a4005d29c9bb44"
-estraverse@^4.0.0, estraverse@^4.1.1, estraverse@^4.2.0:
+estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
-estraverse@~4.1.0:
- version "4.1.1"
- resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.1.1.tgz#f6caca728933a850ef90661d0e17982ba47111a2"
-
esutils@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
@@ -3159,13 +3008,6 @@ eve-raphael@0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/eve-raphael/-/eve-raphael-0.5.0.tgz#17c754b792beef3fa6684d79cf5a47c63c4cda30"
-event-emitter@~0.3.5:
- version "0.3.5"
- resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39"
- dependencies:
- d "1"
- es5-ext "~0.10.14"
-
event-stream@~3.3.0:
version "3.3.4"
resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-3.3.4.tgz#4ab4c9a0f5a54db9338b4c34d86bfce8f4b35571"
@@ -3211,10 +3053,6 @@ execa@^0.7.0:
signal-exit "^3.0.0"
strip-eof "^1.0.0"
-exit-hook@^1.0.0:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8"
-
expand-braces@^0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/expand-braces/-/expand-braces-0.1.2.tgz#488b1d1d2451cb3d3a6b192cfc030f44c5855fea"
@@ -3254,12 +3092,6 @@ expand-range@^1.8.1:
dependencies:
fill-range "^2.1.0"
-expand-tilde@^2.0.0, expand-tilde@^2.0.2:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502"
- dependencies:
- homedir-polyfill "^1.0.1"
-
exports-loader@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/exports-loader/-/exports-loader-0.7.0.tgz#84881c784dea6036b8e1cd1dac3da9b6409e21a5"
@@ -3319,15 +3151,7 @@ extend@3, extend@^3.0.0, extend@~3.0.0, extend@~3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
-external-editor@^2.0.4:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.1.0.tgz#3d026a21b7f95b5726387d4200ac160d372c3b48"
- dependencies:
- chardet "^0.4.0"
- iconv-lite "^0.4.17"
- tmp "^0.0.33"
-
-external-editor@^2.1.0:
+external-editor@^2.0.4, external-editor@^2.1.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.2.0.tgz#045511cfd8d133f3846673d1047c154e214ad3d5"
dependencies:
@@ -3366,16 +3190,6 @@ 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-glob@^2.0.2:
- version "2.2.1"
- resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.1.tgz#686c2345be88f3741e174add0be6f2e5b6078889"
- dependencies:
- "@mrmlnc/readdir-enhanced" "^2.2.1"
- glob-parent "^3.1.0"
- is-glob "^4.0.0"
- merge2 "^1.2.1"
- micromatch "^3.1.10"
-
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"
@@ -3400,13 +3214,6 @@ faye-websocket@~0.11.0:
dependencies:
websocket-driver ">=0.5.1"
-figures@^1.3.5, figures@^1.7.0:
- version "1.7.0"
- resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e"
- dependencies:
- escape-string-regexp "^1.0.5"
- object-assign "^4.1.0"
-
figures@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962"
@@ -3485,9 +3292,9 @@ find-cache-dir@^1.0.0:
make-dir "^1.0.0"
pkg-dir "^2.0.0"
-find-root@^0.1.1:
- version "0.1.2"
- resolved "https://registry.yarnpkg.com/find-root/-/find-root-0.1.2.tgz#98d2267cff1916ccaf2743b3a0eea81d79d7dcd1"
+find-root@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4"
find-up@^1.0.0:
version "1.1.2"
@@ -3502,12 +3309,6 @@ find-up@^2.0.0, find-up@^2.1.0:
dependencies:
locate-path "^2.0.0"
-first-chunk-stream@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/first-chunk-stream/-/first-chunk-stream-2.0.0.tgz#1bdecdb8e083c0664b91945581577a43a9f31d70"
- dependencies:
- readable-stream "^2.0.2"
-
flat-cache@^1.2.1:
version "1.2.2"
resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.2.2.tgz#fa86714e72c21db88601761ecf2f555d1abc6b96"
@@ -3521,10 +3322,6 @@ flatten@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782"
-flow-parser@^0.*:
- version "0.66.0"
- resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.66.0.tgz#be583fefb01192aa5164415d31a6241b35718983"
-
flush-write-stream@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.2.tgz#c81b90d8746766f1a609a46809946c45dd8ae417"
@@ -3617,6 +3414,12 @@ fs-access@^1.0.0:
dependencies:
null-check "^1.0.0"
+fs-minipass@^1.2.5:
+ version "1.2.5"
+ resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d"
+ dependencies:
+ minipass "^2.2.1"
+
fs-write-stream-atomic@^1.0.8:
version "1.0.10"
resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9"
@@ -3631,28 +3434,11 @@ fs.realpath@^1.0.0:
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
fsevents@^1.0.0:
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.3.tgz#11f82318f5fe7bb2cd22965a108e9306208216d8"
- dependencies:
- nan "^2.3.0"
- node-pre-gyp "^0.6.39"
-
-fstream-ignore@^1.0.5:
- version "1.0.5"
- resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105"
- dependencies:
- fstream "^1.0.0"
- inherits "2"
- minimatch "^3.0.0"
-
-fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2:
- version "1.0.11"
- resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171"
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.4.tgz#f41dcb1af2582af3692da36fc55cbd8e1041c426"
dependencies:
- graceful-fs "^4.1.2"
- inherits "~2.0.0"
- mkdirp ">=0.5 0"
- rimraf "2"
+ nan "^2.9.2"
+ node-pre-gyp "^0.10.0"
ftp@~0.3.10:
version "0.3.10"
@@ -3665,6 +3451,10 @@ function-bind@^1.0.2, function-bind@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+functional-red-black-tree@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
+
fuzzaldrin-plus@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/fuzzaldrin-plus/-/fuzzaldrin-plus-0.5.0.tgz#ef5f26f0c2fc7e9e9a16ea149a802d6cb4804b1e"
@@ -3725,26 +3515,6 @@ getpass@^0.1.1:
dependencies:
assert-plus "^1.0.0"
-gh-got@^6.0.0:
- version "6.0.0"
- resolved "https://registry.yarnpkg.com/gh-got/-/gh-got-6.0.0.tgz#d74353004c6ec466647520a10bd46f7299d268d0"
- dependencies:
- got "^7.0.0"
- is-plain-obj "^1.1.0"
-
-github-username@^4.0.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/github-username/-/github-username-4.1.0.tgz#cbe280041883206da4212ae9e4b5f169c30bf417"
- dependencies:
- gh-got "^6.0.0"
-
-glob-all@^3.1.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/glob-all/-/glob-all-3.1.0.tgz#8913ddfb5ee1ac7812656241b03d5217c64b02ab"
- dependencies:
- glob "^7.0.5"
- yargs "~1.2.6"
-
glob-base@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4"
@@ -3765,10 +3535,6 @@ glob-parent@^3.1.0:
is-glob "^3.1.0"
path-dirname "^1.0.0"
-glob-to-regexp@^0.3.0:
- version "0.3.0"
- resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab"
-
glob@^5.0.15:
version "5.0.15"
resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1"
@@ -3779,18 +3545,7 @@ glob@^5.0.15:
once "^1.3.0"
path-is-absolute "^1.0.0"
-glob@^7.0.0, glob@^7.0.3:
- 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.2"
- once "^1.3.0"
- path-is-absolute "^1.0.0"
-
-glob@^7.0.5, glob@^7.1.1, glob@^7.1.2:
+glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2:
version "7.1.2"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
dependencies:
@@ -3807,29 +3562,15 @@ global-dirs@^0.1.0:
dependencies:
ini "^1.3.4"
-global-modules@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea"
- dependencies:
- global-prefix "^1.0.1"
- is-windows "^1.0.1"
- resolve-dir "^1.0.0"
-
-global-prefix@^1.0.1:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe"
- dependencies:
- expand-tilde "^2.0.2"
- homedir-polyfill "^1.0.1"
- ini "^1.3.4"
- is-windows "^1.0.1"
- which "^1.2.14"
+global-modules-path@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/global-modules-path/-/global-modules-path-2.1.0.tgz#923ec524e8726bb0c1a4ed4b8e21e1ff80c88bbb"
-globals@^10.0.0:
- version "10.4.0"
- resolved "https://registry.yarnpkg.com/globals/-/globals-10.4.0.tgz#5c477388b128a9e4c5c5d01c7a2aca68c68b2da7"
+globals@^11.0.1, globals@^11.1.0:
+ version "11.5.0"
+ resolved "https://registry.yarnpkg.com/globals/-/globals-11.5.0.tgz#6bc840de6771173b191f13d3a9c94d441ee92642"
-globals@^9.14.0, globals@^9.18.0:
+globals@^9.18.0:
version "9.18.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
@@ -3854,29 +3595,6 @@ globby@^6.1.0:
pify "^2.0.0"
pinkie-promise "^2.0.0"
-globby@^7.1.1:
- version "7.1.1"
- resolved "https://registry.yarnpkg.com/globby/-/globby-7.1.1.tgz#fb2ccff9401f8600945dfada97440cca972b8680"
- dependencies:
- array-union "^1.0.1"
- dir-glob "^2.0.0"
- glob "^7.1.2"
- ignore "^3.3.5"
- pify "^3.0.0"
- slash "^1.0.0"
-
-globby@^8.0.0:
- version "8.0.1"
- resolved "https://registry.yarnpkg.com/globby/-/globby-8.0.1.tgz#b5ad48b8aa80b35b814fc1281ecc851f1d2b5b50"
- dependencies:
- array-union "^1.0.1"
- dir-glob "^2.0.0"
- fast-glob "^2.0.2"
- glob "^7.1.2"
- ignore "^3.3.5"
- pify "^3.0.0"
- slash "^1.0.0"
-
good-listener@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50"
@@ -3899,26 +3617,7 @@ got@^6.7.1:
unzip-response "^2.0.1"
url-parse-lax "^1.0.0"
-got@^7.0.0:
- version "7.1.0"
- resolved "https://registry.yarnpkg.com/got/-/got-7.1.0.tgz#05450fd84094e6bbea56f451a43a9c289166385a"
- dependencies:
- decompress-response "^3.2.0"
- duplexer3 "^0.1.4"
- get-stream "^3.0.0"
- is-plain-obj "^1.1.0"
- is-retry-allowed "^1.0.0"
- is-stream "^1.0.0"
- isurl "^1.0.0-alpha5"
- lowercase-keys "^1.0.0"
- p-cancelable "^0.3.0"
- p-timeout "^1.1.1"
- safe-buffer "^5.0.1"
- timed-out "^4.0.0"
- url-parse-lax "^1.0.0"
- url-to-options "^1.0.1"
-
-got@^8.0.3, got@^8.2.0:
+got@^8.0.3:
version "8.3.0"
resolved "https://registry.yarnpkg.com/got/-/got-8.3.0.tgz#6ba26e75f8a6cc4c6b3eb1fe7ce4fec7abac8533"
dependencies:
@@ -3950,12 +3649,6 @@ graphlib@^2.1.1:
dependencies:
lodash "^4.11.1"
-grouped-queue@^0.3.3:
- version "0.3.3"
- resolved "https://registry.yarnpkg.com/grouped-queue/-/grouped-queue-0.3.3.tgz#c167d2a5319c5a0e0964ef6a25b7c2df8996c85c"
- dependencies:
- lodash "^4.17.2"
-
gzip-size@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-4.1.0.tgz#8ae096257eabe7d69c45be2b67c448124ffb517c"
@@ -3977,10 +3670,6 @@ handlebars@^4.0.1, handlebars@^4.0.3:
optionalDependencies:
uglify-js "^2.6"
-har-schema@^1.0.5:
- version "1.0.5"
- resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e"
-
har-schema@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
@@ -3994,13 +3683,6 @@ har-validator@~2.0.6:
is-my-json-valid "^2.12.4"
pinkie-promise "^2.0.0"
-har-validator@~4.2.1:
- version "4.2.1"
- resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a"
- dependencies:
- ajv "^4.9.1"
- har-schema "^1.0.5"
-
har-validator@~5.0.3:
version "5.0.3"
resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd"
@@ -4020,10 +3702,6 @@ has-binary2@~1.0.2:
dependencies:
isarray "2.0.1"
-has-color@~0.1.0:
- version "0.1.7"
- resolved "https://registry.yarnpkg.com/has-color/-/has-color-0.1.7.tgz#67144a5260c34fc3cca677d041daf52fe7b78b2f"
-
has-cors@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39"
@@ -4032,10 +3710,6 @@ has-flag@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa"
-has-flag@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51"
-
has-flag@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
@@ -4111,7 +3785,7 @@ hash.js@^1.0.0, hash.js@^1.0.3:
inherits "^2.0.3"
minimalistic-assert "^1.0.0"
-hawk@3.1.3, hawk@~3.1.3:
+hawk@~3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4"
dependencies:
@@ -4163,12 +3837,6 @@ home-or-tmp@^2.0.0:
os-homedir "^1.0.0"
os-tmpdir "^1.0.1"
-homedir-polyfill@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz#4c2bbc8a758998feebf5ed68580f76d46768b4bc"
- dependencies:
- parse-passwd "^1.0.0"
-
hosted-git-info@^2.1.4:
version "2.2.0"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.2.0.tgz#7a0d097863d886c0fabbdcd37bf1758d8becf8a5"
@@ -4269,9 +3937,9 @@ httpreq@>=0.4.22:
version "0.4.24"
resolved "https://registry.yarnpkg.com/httpreq/-/httpreq-0.4.24.tgz#4335ffd82cd969668a39465c929ac61d6393627f"
-https-browserify@0.0.1:
- version "0.0.1"
- resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-0.0.1.tgz#3f91365cabe60b77ed0ebba24b454e3e09d95a82"
+https-browserify@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
https-proxy-agent@1:
version "1.0.0"
@@ -4289,6 +3957,12 @@ iconv-lite@0.4.19, iconv-lite@^0.4.17:
version "0.4.19"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"
+iconv-lite@^0.4.4:
+ version "0.4.23"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63"
+ dependencies:
+ safer-buffer ">= 2.1.2 < 3"
+
icss-replace-symbols@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded"
@@ -4299,9 +3973,9 @@ icss-utils@^2.1.0:
dependencies:
postcss "^6.0.1"
-ieee754@^1.1.4:
- version "1.1.8"
- resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4"
+ieee754@^1.1.11, ieee754@^1.1.4:
+ version "1.1.11"
+ resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.11.tgz#c16384ffe00f5b7835824e67b6f2bd44a5229455"
iferr@^0.1.5:
version "0.1.5"
@@ -4311,9 +3985,15 @@ ignore-by-default@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09"
-ignore@^3.2.0, ignore@^3.3.5, ignore@^3.3.7:
- version "3.3.7"
- resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.7.tgz#612289bfb3c220e186a58118618d5be8c1bab021"
+ignore-walk@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8"
+ dependencies:
+ minimatch "^3.0.4"
+
+ignore@^3.3.3, ignore@^3.3.7:
+ version "3.3.8"
+ resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.8.tgz#3f8e9c35d38708a3a7e0e9abb6c73e7ee7707b2b"
immediate@~3.0.5:
version "3.0.6"
@@ -4347,10 +4027,6 @@ indent-string@^2.1.0:
dependencies:
repeating "^2.0.0"
-indent-string@^3.0.0:
- version "3.2.0"
- resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289"
-
indexes-of@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
@@ -4374,7 +4050,7 @@ inflight@^1.0.4:
once "^1.3.0"
wrappy "1"
-inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3:
+inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
@@ -4386,25 +4062,7 @@ ini@^1.3.4, ini@~1.3.0:
version "1.3.5"
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
-inquirer@^0.12.0:
- version "0.12.0"
- resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e"
- dependencies:
- ansi-escapes "^1.1.0"
- ansi-regex "^2.0.0"
- chalk "^1.0.0"
- cli-cursor "^1.0.1"
- cli-width "^2.0.0"
- figures "^1.3.5"
- lodash "^4.3.0"
- readline2 "^1.0.1"
- run-async "^0.1.0"
- rx-lite "^3.1.2"
- string-width "^1.0.1"
- strip-ansi "^3.0.0"
- through "^2.3.6"
-
-inquirer@^3.3.0:
+inquirer@^3.0.6:
version "3.3.0"
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9"
dependencies:
@@ -4423,7 +4081,7 @@ inquirer@^3.3.0:
strip-ansi "^4.0.0"
through "^2.3.6"
-inquirer@^5.1.0:
+inquirer@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-5.2.0.tgz#db350c2b73daca77ff1243962e9f22f099685726"
dependencies:
@@ -4447,11 +4105,7 @@ internal-ip@1.2.0:
dependencies:
meow "^3.3.0"
-interpret@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.1.tgz#d579fb7f693b858004947af39fa0db49f795602c"
-
-interpret@^1.0.4:
+interpret@^1.0.0, interpret@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614"
@@ -4488,13 +4142,6 @@ is-absolute-url@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6"
-is-absolute@^0.2.3:
- version "0.2.6"
- resolved "https://registry.yarnpkg.com/is-absolute/-/is-absolute-0.2.6.tgz#20de69f3db942ef2d87b9c2da36f172235b1b5eb"
- dependencies:
- is-relative "^0.2.1"
- is-windows "^0.2.0"
-
is-accessor-descriptor@^0.1.6:
version "0.1.6"
resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6"
@@ -4636,7 +4283,7 @@ is-my-ip-valid@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz#7b351b8e8edd4d3995d4d066680e664d94696824"
-is-my-json-valid@^2.10.0, is-my-json-valid@^2.12.4:
+is-my-json-valid@^2.12.4:
version "2.17.2"
resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.17.2.tgz#6b2103a288e94ef3de5cf15d29dd85fc4b78d65c"
dependencies:
@@ -4678,12 +4325,6 @@ is-object@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.1.tgz#8952688c5ec2ffd6b03ecc85e769e02903083470"
-is-observable@^0.2.0:
- version "0.2.0"
- resolved "https://registry.yarnpkg.com/is-observable/-/is-observable-0.2.0.tgz#b361311d83c6e5d726cabf5e250b0237106f5ae2"
- dependencies:
- symbol-observable "^0.2.2"
-
is-odd@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/is-odd/-/is-odd-2.0.0.tgz#7646624671fd7ea558ccd9a2795182f2958f1b24"
@@ -4706,7 +4347,7 @@ is-path-inside@^1.0.0:
dependencies:
path-is-inside "^1.0.1"
-is-plain-obj@^1.0.0, is-plain-obj@^1.1.0:
+is-plain-obj@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
@@ -4742,12 +4383,6 @@ is-regex@^1.0.4:
dependencies:
has "^1.0.1"
-is-relative@^0.2.1:
- version "0.2.1"
- resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-0.2.1.tgz#d27f4c7d516d175fb610db84bbeef23c3bc97aa5"
- dependencies:
- is-unc-path "^0.1.1"
-
is-resolvable@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.0.0.tgz#8df57c61ea2e3c501408d100fb013cf8d6e0cc62"
@@ -4758,12 +4393,6 @@ is-retry-allowed@^1.0.0, is-retry-allowed@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz#11a060568b67339444033d0125a61a20d564fb34"
-is-scoped@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/is-scoped/-/is-scoped-1.0.0.tgz#449ca98299e713038256289ecb2b540dc437cb30"
- dependencies:
- scoped-regex "^1.0.0"
-
is-stream@^1.0.0, is-stream@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
@@ -4782,21 +4411,11 @@ is-typedarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
-is-unc-path@^0.1.1:
- version "0.1.2"
- resolved "https://registry.yarnpkg.com/is-unc-path/-/is-unc-path-0.1.2.tgz#6ab053a72573c10250ff416a3814c35178af39b9"
- dependencies:
- unc-path-regex "^0.1.0"
-
is-utf8@^0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
-is-windows@^0.2.0:
- version "0.2.0"
- resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-0.2.0.tgz#de1aa6d63ea29dd248737b69f1ff8b8002d2108c"
-
-is-windows@^1.0.1, is-windows@^1.0.2:
+is-windows@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
@@ -4816,7 +4435,7 @@ isarray@2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.1.tgz#a37d94ed9cda2d59865c9f76fe596ee1f338741e"
-isbinaryfile@^3.0.0, isbinaryfile@^3.0.2:
+isbinaryfile@^3.0.0:
version "3.0.2"
resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-3.0.2.tgz#4a3e974ec0cba9004d3fc6cde7209ea69368a621"
@@ -4868,7 +4487,7 @@ istanbul-lib-hook@^1.1.0:
dependencies:
append-transform "^0.4.0"
-istanbul-lib-instrument@^1.10.1:
+istanbul-lib-instrument@^1.10.1, istanbul-lib-instrument@^1.9.1:
version "1.10.1"
resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.1.tgz#724b4b6caceba8692d3f1f9d0727e279c401af7b"
dependencies:
@@ -4880,18 +4499,6 @@ istanbul-lib-instrument@^1.10.1:
istanbul-lib-coverage "^1.2.0"
semver "^5.3.0"
-istanbul-lib-instrument@^1.9.1:
- 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.18.0"
- istanbul-lib-coverage "^1.1.1"
- semver "^5.3.0"
-
istanbul-lib-report@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-1.1.2.tgz#922be27c13b9511b979bd1587359f69798c1d425"
@@ -4936,14 +4543,6 @@ istanbul@^0.4.5:
which "^1.1.1"
wordwrap "^1.0.0"
-istextorbinary@^2.2.1:
- version "2.2.1"
- resolved "https://registry.yarnpkg.com/istextorbinary/-/istextorbinary-2.2.1.tgz#a5231a08ef6dd22b268d0895084cf8d58b5bec53"
- dependencies:
- binaryextensions "2"
- editions "^1.3.3"
- textextensions "2"
-
isurl@^1.0.0-alpha5:
version "1.0.0"
resolved "https://registry.yarnpkg.com/isurl/-/isurl-1.0.0.tgz#b27f4f49f3cdaa3ea44a0a5b7f3462e6edc39d67"
@@ -4989,9 +4588,9 @@ 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.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"
+js-yaml@3.x, js-yaml@^3.7.0, js-yaml@^3.9.1:
+ version "3.11.0"
+ resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.11.0.tgz#597c1a8bd57152f26d622ce4117851a51f5ebaef"
dependencies:
argparse "^1.0.7"
esprima "^4.0.0"
@@ -5007,50 +4606,14 @@ jsbn@~0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
-jscodeshift@^0.4.0:
- version "0.4.1"
- resolved "https://registry.yarnpkg.com/jscodeshift/-/jscodeshift-0.4.1.tgz#da91a1c2eccfa03a3387a21d39948e251ced444a"
- dependencies:
- async "^1.5.0"
- babel-plugin-transform-flow-strip-types "^6.8.0"
- babel-preset-es2015 "^6.9.0"
- babel-preset-stage-1 "^6.5.0"
- babel-register "^6.9.0"
- babylon "^6.17.3"
- colors "^1.1.2"
- flow-parser "^0.*"
- lodash "^4.13.1"
- micromatch "^2.3.7"
- node-dir "0.1.8"
- nomnom "^1.8.1"
- recast "^0.12.5"
- temp "^0.8.1"
- write-file-atomic "^1.2.0"
-
-jscodeshift@^0.5.0:
- version "0.5.0"
- resolved "https://registry.yarnpkg.com/jscodeshift/-/jscodeshift-0.5.0.tgz#bdb7b6cc20dd62c16aa728c3fa2d2fe66ca7c748"
- dependencies:
- babel-plugin-transform-flow-strip-types "^6.8.0"
- babel-preset-es2015 "^6.9.0"
- babel-preset-stage-1 "^6.5.0"
- babel-register "^6.9.0"
- babylon "^7.0.0-beta.30"
- colors "^1.1.2"
- flow-parser "^0.*"
- lodash "^4.13.1"
- micromatch "^2.3.7"
- neo-async "^2.5.0"
- node-dir "0.1.8"
- nomnom "^1.8.1"
- recast "^0.14.1"
- temp "^0.8.1"
- write-file-atomic "^1.2.0"
-
jsesc@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b"
+jsesc@^2.5.1:
+ version "2.5.1"
+ resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.1.tgz#e421a2a8e20d6b0819df28908f782526b96dd1fe"
+
jsesc@~0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
@@ -5059,7 +4622,7 @@ json-buffer@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898"
-json-parse-better-errors@^1.0.1:
+json-parse-better-errors@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
@@ -5071,11 +4634,9 @@ json-schema@0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
-json-stable-stringify@^1.0.0, json-stable-stringify@^1.0.1:
+json-stable-stringify-without-jsonify@^1.0.1:
version "1.0.1"
- resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af"
- dependencies:
- jsonify "~0.0.0"
+ resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
json-stringify-safe@5.0.x, json-stringify-safe@~5.0.1:
version "5.0.1"
@@ -5089,10 +4650,6 @@ json5@^0.5.0, json5@^0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
-jsonify@~0.0.0:
- version "0.0.0"
- resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
-
jsonpointer@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9"
@@ -5253,6 +4810,10 @@ lcid@^1.0.0:
dependencies:
invert-kv "^1.0.0"
+leb@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/leb/-/leb-0.3.0.tgz#32bee9fad168328d6aea8522d833f4180eed1da3"
+
levn@^0.3.0, levn@~0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
@@ -5282,54 +4843,6 @@ lie@~3.1.0:
dependencies:
immediate "~3.0.5"
-listr-silent-renderer@^1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz#924b5a3757153770bf1a8e3fbf74b8bbf3f9242e"
-
-listr-update-renderer@^0.4.0:
- version "0.4.0"
- resolved "https://registry.yarnpkg.com/listr-update-renderer/-/listr-update-renderer-0.4.0.tgz#344d980da2ca2e8b145ba305908f32ae3f4cc8a7"
- dependencies:
- chalk "^1.1.3"
- cli-truncate "^0.2.1"
- elegant-spinner "^1.0.1"
- figures "^1.7.0"
- indent-string "^3.0.0"
- log-symbols "^1.0.2"
- log-update "^1.0.2"
- strip-ansi "^3.0.1"
-
-listr-verbose-renderer@^0.4.0:
- version "0.4.1"
- resolved "https://registry.yarnpkg.com/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz#8206f4cf6d52ddc5827e5fd14989e0e965933a35"
- dependencies:
- chalk "^1.1.3"
- cli-cursor "^1.0.2"
- date-fns "^1.27.2"
- figures "^1.7.0"
-
-listr@^0.13.0:
- version "0.13.0"
- resolved "https://registry.yarnpkg.com/listr/-/listr-0.13.0.tgz#20bb0ba30bae660ee84cc0503df4be3d5623887d"
- dependencies:
- chalk "^1.1.3"
- cli-truncate "^0.2.1"
- figures "^1.7.0"
- indent-string "^2.1.0"
- is-observable "^0.2.0"
- is-promise "^2.1.0"
- is-stream "^1.1.0"
- listr-silent-renderer "^1.1.1"
- listr-update-renderer "^0.4.0"
- listr-verbose-renderer "^0.4.0"
- log-symbols "^1.0.2"
- log-update "^1.0.2"
- ora "^0.2.3"
- p-map "^1.1.1"
- rxjs "^5.4.2"
- stream-to-observable "^0.2.0"
- strip-ansi "^3.0.1"
-
load-json-file@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
@@ -5340,13 +4853,13 @@ load-json-file@^1.0.0:
pinkie-promise "^2.0.0"
strip-bom "^2.0.0"
-load-json-file@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b"
+load-json-file@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8"
dependencies:
graceful-fs "^4.1.2"
- parse-json "^4.0.0"
- pify "^3.0.0"
+ parse-json "^2.2.0"
+ pify "^2.0.0"
strip-bom "^3.0.0"
loader-runner@^2.3.0:
@@ -5368,65 +4881,21 @@ locate-path@^2.0.0:
p-locate "^2.0.0"
path-exists "^3.0.0"
-lodash._baseget@^3.0.0:
- version "3.7.2"
- resolved "https://registry.yarnpkg.com/lodash._baseget/-/lodash._baseget-3.7.2.tgz#1b6ae1d5facf3c25532350a13c1197cb8bb674f4"
-
-lodash._topath@^3.0.0:
- version "3.8.1"
- resolved "https://registry.yarnpkg.com/lodash._topath/-/lodash._topath-3.8.1.tgz#3ec5e2606014f4cb97f755fe6914edd8bfc00eac"
- dependencies:
- lodash.isarray "^3.0.0"
-
-lodash.camelcase@4.1.1:
- version "4.1.1"
- resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.1.1.tgz#065b3ff08f0b7662f389934c46a5504c90e0b2d8"
- dependencies:
- lodash.capitalize "^4.0.0"
- lodash.deburr "^4.0.0"
- lodash.words "^4.0.0"
-
-lodash.camelcase@^4.3.0:
+lodash.camelcase@4.3.0, lodash.camelcase@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
-lodash.capitalize@^4.0.0:
- version "4.2.1"
- resolved "https://registry.yarnpkg.com/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz#f826c9b4e2a8511d84e3aca29db05e1a4f3b72a9"
-
lodash.clonedeep@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
-lodash.cond@^4.3.0:
- version "4.5.2"
- resolved "https://registry.yarnpkg.com/lodash.cond/-/lodash.cond-4.5.2.tgz#f471a1da486be60f6ab955d17115523dd1d255d5"
-
-lodash.deburr@^4.0.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/lodash.deburr/-/lodash.deburr-4.1.0.tgz#ddb1bbb3ef07458c0177ba07de14422cb033ff9b"
-
lodash.escaperegexp@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347"
-lodash.get@^3.7.0:
- version "3.7.0"
- resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-3.7.0.tgz#3ce68ae2c91683b281cc5394128303cbf75e691f"
- dependencies:
- lodash._baseget "^3.0.0"
- lodash._topath "^3.0.0"
-
-lodash.isarray@^3.0.0:
- version "3.0.4"
- resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55"
-
-lodash.kebabcase@4.0.1:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.0.1.tgz#5e63bc9aa2a5562ff3b97ca7af2f803de1bcb90e"
- dependencies:
- lodash.deburr "^4.0.0"
- lodash.words "^4.0.0"
+lodash.kebabcase@4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36"
lodash.memoize@^4.1.2:
version "4.1.2"
@@ -5436,58 +4905,32 @@ lodash.mergewith@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.0.tgz#150cf0a16791f5903b8891eab154609274bdea55"
-lodash.snakecase@4.0.1:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/lodash.snakecase/-/lodash.snakecase-4.0.1.tgz#bd012e5d2f93f7b58b9303e9a7fbfd5db13d6281"
- dependencies:
- lodash.deburr "^4.0.0"
- lodash.words "^4.0.0"
+lodash.snakecase@4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz#39d714a35357147837aefd64b5dcbb16becd8f8d"
lodash.uniq@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
-lodash.words@^4.0.0:
- version "4.2.0"
- resolved "https://registry.yarnpkg.com/lodash.words/-/lodash.words-4.2.0.tgz#5ecfeaf8ecf8acaa8e0c8386295f1993c9cf4036"
+lodash.upperfirst@4.3.1:
+ version "4.3.1"
+ resolved "https://registry.yarnpkg.com/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz#1365edf431480481ef0d1c68957a5ed99d49f7ce"
lodash@4.17.4:
version "4.17.4"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
-lodash@^4.0.0, lodash@^4.11.1, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.0:
- version "4.17.5"
- resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511"
-
-lodash@^4.17.10:
+lodash@^4.0.0, lodash@^4.11.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.0:
version "4.17.10"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
-log-symbols@^1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18"
- dependencies:
- chalk "^1.0.0"
-
log-symbols@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.1.0.tgz#f35fa60e278832b538dc4dddcbb478a45d3e3be6"
- dependencies:
- chalk "^2.0.1"
-
-log-symbols@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a"
dependencies:
chalk "^2.0.1"
-log-update@^1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/log-update/-/log-update-1.0.2.tgz#19929f64c4093d2d2e7075a1dad8af59c296b8d1"
- dependencies:
- ansi-escapes "^1.0.0"
- cli-cursor "^1.0.2"
-
log4js@^2.3.9:
version "2.5.3"
resolved "https://registry.yarnpkg.com/log4js/-/log4js-2.5.3.tgz#38bb7bde5e9c1c181bd75e8bc128c5cd0409caf1"
@@ -5523,6 +4966,10 @@ loglevelnext@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/loglevelnext/-/loglevelnext-1.0.3.tgz#0f69277e73bbbf2cd61b94d82313216bf87ac66e"
+long@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/long/-/long-3.2.0.tgz#d821b7138ca1cb581c172990ef14db200b5c474b"
+
longest@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
@@ -5548,14 +4995,7 @@ 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@^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.2"
- yallist "^2.1.2"
-
-lru-cache@^4.1.2:
+lru-cache@^4.0.1, lru-cache@^4.1.1, lru-cache@^4.1.2:
version "4.1.3"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.3.tgz#a1175cf3496dfc8436c156c334b4955992bce69c"
dependencies:
@@ -5592,17 +5032,15 @@ mailgun-js@^0.7.0:
tsscmp "~1.0.0"
make-dir@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.0.0.tgz#97a011751e91dd87cfadef58832ebb04936de978"
- dependencies:
- pify "^2.3.0"
-
-make-dir@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.2.0.tgz#6d6a49eead4aae296c53bbf3a1a008bd6c89469b"
dependencies:
pify "^3.0.0"
+mamacro@^0.0.3:
+ version "0.0.3"
+ resolved "https://registry.yarnpkg.com/mamacro/-/mamacro-0.0.3.tgz#ad2c9576197c9f1abf308d0787865bd975a3f3e4"
+
map-cache@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
@@ -5644,30 +5082,6 @@ media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
-mem-fs-editor@^4.0.0:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/mem-fs-editor/-/mem-fs-editor-4.0.1.tgz#27e6b59df91b37248e9be2145b1bea84695103ed"
- dependencies:
- commondir "^1.0.1"
- deep-extend "^0.5.1"
- ejs "^2.5.9"
- glob "^7.0.3"
- globby "^8.0.0"
- isbinaryfile "^3.0.2"
- mkdirp "^0.5.0"
- multimatch "^2.0.0"
- rimraf "^2.2.8"
- through2 "^2.0.0"
- vinyl "^2.0.1"
-
-mem-fs@^1.1.0:
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/mem-fs/-/mem-fs-1.1.3.tgz#b8ae8d2e3fcb6f5d3f9165c12d4551a065d989cc"
- dependencies:
- through2 "^2.0.0"
- vinyl "^1.1.0"
- vinyl-file "^2.0.0"
-
mem@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76"
@@ -5710,15 +5124,11 @@ merge-source-map@^1.1.0:
dependencies:
source-map "^0.6.1"
-merge2@^1.2.1:
- version "1.2.2"
- resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.2.2.tgz#03212e3da8d86c4d8523cebd6318193414f94e34"
-
methods@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
-micromatch@^2.1.5, micromatch@^2.3.7:
+micromatch@^2.1.5:
version "2.3.11"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565"
dependencies:
@@ -5736,7 +5146,7 @@ micromatch@^2.1.5, micromatch@^2.3.7:
parse-glob "^3.0.4"
regex-cache "^0.4.2"
-micromatch@^3.1.10, micromatch@^3.1.9:
+micromatch@^3.1.4, micromatch@^3.1.8, micromatch@^3.1.9:
version "3.1.10"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
dependencies:
@@ -5754,42 +5164,6 @@ micromatch@^3.1.10, micromatch@^3.1.9:
snapdragon "^0.8.1"
to-regex "^3.0.2"
-micromatch@^3.1.4:
- version "3.1.6"
- resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.6.tgz#8d7c043b48156f408ca07a4715182b79b99420bf"
- dependencies:
- arr-diff "^4.0.0"
- array-unique "^0.3.2"
- braces "^2.3.1"
- define-property "^2.0.2"
- extend-shallow "^3.0.2"
- extglob "^2.0.4"
- fragment-cache "^0.2.1"
- kind-of "^6.0.2"
- nanomatch "^1.2.9"
- object.pick "^1.3.0"
- regex-not "^1.0.0"
- snapdragon "^0.8.1"
- to-regex "^3.0.1"
-
-micromatch@^3.1.8:
- version "3.1.9"
- resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.9.tgz#15dc93175ae39e52e93087847096effc73efcf89"
- dependencies:
- arr-diff "^4.0.0"
- array-unique "^0.3.2"
- braces "^2.3.1"
- define-property "^2.0.2"
- extend-shallow "^3.0.2"
- extglob "^2.0.4"
- fragment-cache "^0.2.1"
- kind-of "^6.0.2"
- nanomatch "^1.2.9"
- object.pick "^1.3.0"
- regex-not "^1.0.0"
- snapdragon "^0.8.1"
- to-regex "^3.0.1"
-
miller-rabin@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d"
@@ -5815,14 +5189,10 @@ mime@^1.3.4:
version "1.6.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
-mime@^2.0.3:
+mime@^2.0.3, mime@^2.1.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/mime/-/mime-2.3.1.tgz#b1621c54d63b97c47d3cfe7f7215f7d64517c369"
-mime@^2.1.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/mime/-/mime-2.2.0.tgz#161e541965551d3b549fa1114391e3a3d55b923b"
-
mimic-fn@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18"
@@ -5839,7 +5209,7 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
-"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4:
+"minimatch@2 || 3", 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:
@@ -5849,10 +5219,6 @@ minimist@0.0.8:
version "0.0.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
-minimist@^0.1.0:
- version "0.1.0"
- resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.1.0.tgz#99df657a52574c21c9057497df742790b2b4c0de"
-
minimist@^1.1.3, minimist@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
@@ -5861,6 +5227,19 @@ minimist@~0.0.1:
version "0.0.10"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
+minipass@^2.2.1, minipass@^2.3.3:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.3.tgz#a7dcc8b7b833f5d368759cce544dccb55f50f233"
+ dependencies:
+ safe-buffer "^5.1.2"
+ yallist "^3.0.0"
+
+minizlib@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.1.0.tgz#11e13658ce46bc3a70a267aac58359d1e0c29ceb"
+ dependencies:
+ minipass "^2.2.1"
+
mississippi@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-2.0.0.tgz#3442a508fafc28500486feea99409676e4ee5a6f"
@@ -5883,7 +5262,7 @@ mixin-deep@^1.2.0:
for-in "^1.0.2"
is-extendable "^1.0.1"
-mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1:
+mkdirp@0.5.x, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
dependencies:
@@ -5893,9 +5272,13 @@ 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.10.0:
- version "0.10.0"
- resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.10.0.tgz#6604932585fe9c1f993f000a503d0d20fbe5896a"
+monaco-editor-webpack-plugin@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/monaco-editor-webpack-plugin/-/monaco-editor-webpack-plugin-1.4.0.tgz#7324258ab3695464cfe3bc12edb2e8c55b80d92f"
+
+monaco-editor@0.13.1:
+ version "0.13.1"
+ resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.13.1.tgz#6b9ce20e4d1c945042d256825eb133cb23315a52"
mousetrap@^1.4.6:
version "1.4.6"
@@ -5931,26 +5314,13 @@ multicast-dns@^6.0.1:
dns-packet "^1.0.1"
thunky "^0.1.0"
-multimatch@^2.0.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-2.1.0.tgz#9c7906a22fb4c02919e2f5f75161b4cdbd4b2a2b"
- dependencies:
- array-differ "^1.0.0"
- array-union "^1.0.1"
- arrify "^1.0.0"
- minimatch "^3.0.0"
-
-mute-stream@0.0.5:
- version "0.0.5"
- resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0"
-
mute-stream@0.0.7:
version "0.0.7"
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
-nan@^2.3.0:
- version "2.8.0"
- resolved "https://registry.yarnpkg.com/nan/-/nan-2.8.0.tgz#ed715f3fe9de02b57a5e6252d90a96675e1f085a"
+nan@^2.9.2:
+ version "2.10.0"
+ resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f"
nanomatch@^1.2.9:
version "1.2.9"
@@ -5973,6 +5343,14 @@ natural-compare@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
+needle@^2.2.0:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/needle/-/needle-2.2.1.tgz#b5e325bd3aae8c2678902fa296f729455d1d3a7d"
+ dependencies:
+ debug "^2.1.2"
+ iconv-lite "^0.4.4"
+ sax "^1.2.4"
+
negotiator@0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9"
@@ -5989,85 +5367,52 @@ nice-try@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.4.tgz#d93962f6c52f2c1558c0fbda6d512819f1efe1c4"
-node-dir@0.1.8:
- version "0.1.8"
- resolved "https://registry.yarnpkg.com/node-dir/-/node-dir-0.1.8.tgz#55fb8deb699070707fb67f91a460f0448294c77d"
-
node-forge@0.6.33:
version "0.6.33"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.6.33.tgz#463811879f573d45155ad6a9f43dc296e8e85ebc"
-node-libs-browser@^1.0.0:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-1.1.1.tgz#2a38243abedd7dffcd07a97c9aca5668975a6fea"
- dependencies:
- assert "^1.1.1"
- browserify-zlib "^0.1.4"
- buffer "^4.3.0"
- console-browserify "^1.1.0"
- constants-browserify "^1.0.0"
- crypto-browserify "^3.11.0"
- domain-browser "^1.1.1"
- events "^1.0.0"
- https-browserify "0.0.1"
- os-browserify "^0.2.0"
- path-browserify "0.0.0"
- process "^0.11.0"
- punycode "^1.2.4"
- querystring-es3 "^0.2.0"
- readable-stream "^2.0.5"
- stream-browserify "^2.0.1"
- stream-http "^2.3.1"
- string_decoder "^0.10.25"
- timers-browserify "^1.4.2"
- tty-browserify "0.0.0"
- url "^0.11.0"
- util "^0.10.3"
- vm-browserify "0.0.4"
-
-node-libs-browser@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.0.0.tgz#a3a59ec97024985b46e958379646f96c4b616646"
+"node-libs-browser@^1.0.0 || ^2.0.0", node-libs-browser@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.1.0.tgz#5f94263d404f6e44767d726901fff05478d600df"
dependencies:
assert "^1.1.1"
- browserify-zlib "^0.1.4"
+ browserify-zlib "^0.2.0"
buffer "^4.3.0"
console-browserify "^1.1.0"
constants-browserify "^1.0.0"
crypto-browserify "^3.11.0"
domain-browser "^1.1.1"
events "^1.0.0"
- https-browserify "0.0.1"
- os-browserify "^0.2.0"
+ https-browserify "^1.0.0"
+ os-browserify "^0.3.0"
path-browserify "0.0.0"
- process "^0.11.0"
+ process "^0.11.10"
punycode "^1.2.4"
querystring-es3 "^0.2.0"
- readable-stream "^2.0.5"
+ readable-stream "^2.3.3"
stream-browserify "^2.0.1"
- stream-http "^2.3.1"
- string_decoder "^0.10.25"
- timers-browserify "^2.0.2"
+ stream-http "^2.7.2"
+ string_decoder "^1.0.0"
+ timers-browserify "^2.0.4"
tty-browserify "0.0.0"
url "^0.11.0"
util "^0.10.3"
vm-browserify "0.0.4"
-node-pre-gyp@^0.6.39:
- version "0.6.39"
- resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz#c00e96860b23c0e1420ac7befc5044e1d78d8649"
+node-pre-gyp@^0.10.0:
+ version "0.10.0"
+ resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.0.tgz#6e4ef5bb5c5203c6552448828c852c40111aac46"
dependencies:
detect-libc "^1.0.2"
- hawk "3.1.3"
mkdirp "^0.5.1"
+ needle "^2.2.0"
nopt "^4.0.1"
+ npm-packlist "^1.1.6"
npmlog "^4.0.2"
rc "^1.1.7"
- request "2.81.0"
rimraf "^2.6.1"
semver "^5.3.0"
- tar "^2.2.1"
- tar-pack "^3.4.0"
+ tar "^4"
node-uuid@~1.4.7:
version "1.4.8"
@@ -6137,13 +5482,6 @@ nodemon@^1.17.3:
undefsafe "^2.0.2"
update-notifier "^2.3.0"
-nomnom@^1.8.1:
- version "1.8.1"
- resolved "https://registry.yarnpkg.com/nomnom/-/nomnom-1.8.1.tgz#2151f722472ba79e50a76fc125bb8c8f2e4dc2a7"
- dependencies:
- chalk "~0.4.0"
- underscore "~1.6.0"
-
nopt@3.x:
version "3.0.6"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9"
@@ -6199,6 +5537,17 @@ normalize-url@^1.4.0:
query-string "^4.1.0"
sort-keys "^1.0.0"
+npm-bundled@^1.0.1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.3.tgz#7e71703d973af3370a9591bafe3a63aca0be2308"
+
+npm-packlist@^1.1.6:
+ version "1.1.10"
+ resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.1.10.tgz#1039db9e985727e464df066f4cf0ab6ef85c398a"
+ dependencies:
+ ignore-walk "^3.0.1"
+ npm-bundled "^1.0.1"
+
npm-run-path@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
@@ -6283,16 +5632,12 @@ on-headers@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7"
-once@1.x, once@^1.3.0, once@^1.3.1, once@^1.3.3, once@^1.4.0:
+once@1.x, once@^1.3.0, once@^1.3.1, once@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
dependencies:
wrappy "1"
-onetime@^1.0.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789"
-
onetime@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4"
@@ -6327,24 +5672,15 @@ optionator@^0.8.1, optionator@^0.8.2:
type-check "~0.3.2"
wordwrap "~1.0.0"
-ora@^0.2.3:
- version "0.2.3"
- resolved "https://registry.yarnpkg.com/ora/-/ora-0.2.3.tgz#37527d220adcd53c39b73571d754156d5db657a4"
- dependencies:
- chalk "^1.1.1"
- cli-cursor "^1.0.2"
- cli-spinners "^0.1.2"
- object-assign "^4.0.1"
-
original@>=0.0.5:
version "1.0.0"
resolved "https://registry.yarnpkg.com/original/-/original-1.0.0.tgz#9147f93fa1696d04be61e01bd50baeaca656bd3b"
dependencies:
url-parse "1.0.x"
-os-browserify@^0.2.0:
- version "0.2.1"
- resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.2.1.tgz#63fc4ccee5d2d7763d26bbf8601078e6c2e0044f"
+os-browserify@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
os-homedir@^1.0.0:
version "1.0.2"
@@ -6369,20 +5705,10 @@ osenv@^0.1.4:
os-homedir "^1.0.0"
os-tmpdir "^1.0.0"
-p-cancelable@^0.3.0:
- version "0.3.0"
- resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.3.0.tgz#b9e123800bcebb7ac13a479be195b507b98d30fa"
-
p-cancelable@^0.4.0:
version "0.4.1"
resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.4.1.tgz#35f363d67d52081c8d9585e37bcceb7e0bbcb2a0"
-p-each-series@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-1.0.0.tgz#930f3d12dd1f50e7434457a22cd6f04ac6ad7f71"
- dependencies:
- p-reduce "^1.0.0"
-
p-finally@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
@@ -6391,11 +5717,7 @@ p-is-promise@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-1.1.0.tgz#9c9456989e9f6588017b0434d56097675c3da05e"
-p-lazy@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/p-lazy/-/p-lazy-1.0.0.tgz#ec53c802f2ee3ac28f166cc82d0b2b02de27a835"
-
-p-limit@^1.0.0, p-limit@^1.1.0:
+p-limit@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.2.0.tgz#0e92b6bedcb59f022c13d0f1949dc82d15909f1c"
dependencies:
@@ -6411,16 +5733,6 @@ p-map@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.1.1.tgz#05f5e4ae97a068371bc2a5cc86bfbdbc19c4ae7a"
-p-reduce@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/p-reduce/-/p-reduce-1.0.0.tgz#18c2b0dd936a4690a529f8231f58a0fdb6a47dfa"
-
-p-timeout@^1.1.1:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-1.2.1.tgz#5eb3b353b7fce99f101a1038880bb054ebbea386"
- dependencies:
- p-finally "^1.0.0"
-
p-timeout@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-2.0.1.tgz#d8dd1979595d2dc0139e1fe46b8b646cb3cdf038"
@@ -6464,11 +5776,7 @@ package-json@^4.0.0:
registry-url "^3.0.3"
semver "^5.1.0"
-pako@~0.2.0:
- version "0.2.9"
- resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75"
-
-pako@~1.0.2:
+pako@~1.0.2, pako@~1.0.5:
version "1.0.6"
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258"
@@ -6505,17 +5813,6 @@ parse-json@^2.2.0:
dependencies:
error-ex "^1.2.0"
-parse-json@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
- dependencies:
- error-ex "^1.3.1"
- json-parse-better-errors "^1.0.1"
-
-parse-passwd@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
-
parseqs@0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d"
@@ -6558,7 +5855,7 @@ 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"
-path-is-inside@^1.0.1:
+path-is-inside@^1.0.1, path-is-inside@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
@@ -6588,11 +5885,11 @@ path-type@^1.0.0:
pify "^2.0.0"
pinkie-promise "^2.0.0"
-path-type@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
+path-type@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73"
dependencies:
- pify "^3.0.0"
+ pify "^2.0.0"
pause-stream@0.0.11:
version "0.0.11"
@@ -6610,15 +5907,11 @@ pbkdf2@^3.0.3:
safe-buffer "^5.0.1"
sha.js "^2.4.8"
-performance-now@^0.2.0:
- version "0.2.0"
- resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5"
-
performance-now@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
-pify@^2.0.0, pify@^2.3.0:
+pify@^2.0.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
@@ -6654,15 +5947,13 @@ pkg-dir@^2.0.0:
dependencies:
find-up "^2.1.0"
-pkg-up@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-1.0.0.tgz#3e08fb461525c4421624a33b9f7e6d0af5b05a26"
- dependencies:
- find-up "^1.0.0"
+pluralize@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777"
-pluralize@^1.2.1:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45"
+popper.js@^1.14.3:
+ version "1.14.3"
+ resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.3.tgz#1438f98d046acf7b4d78cd502bf418ac64d4f095"
portfinder@^1.0.9:
version "1.0.13"
@@ -6922,15 +6213,7 @@ 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.1, postcss@^6.0.14:
- version "6.0.19"
- resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.19.tgz#76a78386f670b9d9494a655bf23ac012effd1555"
- dependencies:
- chalk "^2.3.1"
- source-map "^0.6.1"
- supports-color "^5.2.0"
-
-postcss@^6.0.20:
+postcss@^6.0.1, postcss@^6.0.14, postcss@^6.0.20:
version "6.0.22"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.22.tgz#e23b78314905c3b90cbd61702121e7a78848f2a3"
dependencies:
@@ -6954,33 +6237,21 @@ preserve@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
-prettier@1.11.1:
- version "1.11.1"
- resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.11.1.tgz#61e43fc4cd44e68f2b0dfc2c38cd4bb0fccdcc75"
-
-prettier@^1.11.1:
+prettier@1.12.1, prettier@^1.11.1:
version "1.12.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.12.1.tgz#c1ad20e803e7749faf905a409d2367e06bbe7325"
-prettier@^1.5.3:
- version "1.10.2"
- resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.10.2.tgz#1af8356d1842276a99a5b5529c82dd9e9ad3cc93"
-
-pretty-bytes@^4.0.2:
- version "4.0.2"
- resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-4.0.2.tgz#b2bf82e7350d65c6c33aa95aaa5a4f6327f61cd9"
-
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, private@^0.1.8, private@~0.1.5:
+private@^0.1.6, private@^0.1.8:
version "0.1.8"
resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
-process-nextick-args@^1.0.6, process-nextick-args@~1.0.6:
+process-nextick-args@~1.0.6:
version "1.0.7"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3"
@@ -6988,13 +6259,13 @@ process-nextick-args@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa"
-process@^0.11.0, process@~0.11.0:
+process@^0.11.10:
version "0.11.10"
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
-progress@^1.1.8:
- version "1.1.8"
- resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be"
+progress@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f"
promise-inflight@^1.0.1:
version "1.0.1"
@@ -7097,10 +6368,6 @@ qs@~6.2.0:
version "6.2.3"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.2.3.tgz#1cfcb25c10a9b2b483053ff39f5dfc9233908cfe"
-qs@~6.4.0:
- version "6.4.0"
- resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
-
query-string@^4.1.0:
version "4.3.2"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.2.tgz#ec0fd765f58a50031a3968c2431386f8947a5cdd"
@@ -7188,13 +6455,6 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.1.7:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
-read-chunk@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/read-chunk/-/read-chunk-2.1.0.tgz#6a04c0928005ed9d42e1a6ac5600e19cbc7ff655"
- dependencies:
- pify "^3.0.0"
- safe-buffer "^5.1.1"
-
read-pkg-up@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02"
@@ -7202,12 +6462,12 @@ read-pkg-up@^1.0.1:
find-up "^1.0.0"
read-pkg "^1.0.0"
-read-pkg-up@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07"
+read-pkg-up@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be"
dependencies:
find-up "^2.0.0"
- read-pkg "^3.0.0"
+ read-pkg "^2.0.0"
read-pkg@^1.0.0:
version "1.1.0"
@@ -7217,15 +6477,15 @@ read-pkg@^1.0.0:
normalize-package-data "^2.3.2"
path-type "^1.0.0"
-read-pkg@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389"
+read-pkg@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8"
dependencies:
- load-json-file "^4.0.0"
+ load-json-file "^2.0.0"
normalize-package-data "^2.3.2"
- path-type "^3.0.0"
+ path-type "^2.0.0"
-"readable-stream@1 || 2", readable-stream@2, readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.0, readable-stream@^2.3.3:
+"readable-stream@1 || 2", readable-stream@2, readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.0, readable-stream@^2.3.3:
version "2.3.4"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.4.tgz#c946c3f47fa7d8eabc0b6150f4a12f69a4574071"
dependencies:
@@ -7246,6 +6506,18 @@ readable-stream@1.1.x, "readable-stream@1.x >=1.1.9":
isarray "0.0.1"
string_decoder "~0.10.x"
+readable-stream@^2.3.6:
+ version "2.3.6"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
+ dependencies:
+ core-util-is "~1.0.0"
+ inherits "~2.0.3"
+ isarray "~1.0.0"
+ process-nextick-args "~2.0.0"
+ safe-buffer "~5.1.1"
+ string_decoder "~1.1.1"
+ util-deprecate "~1.0.1"
+
readable-stream@~2.0.5, readable-stream@~2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e"
@@ -7266,39 +6538,6 @@ readdirp@^2.0.0:
readable-stream "^2.0.2"
set-immediate-shim "^1.0.1"
-readline2@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/readline2/-/readline2-1.0.1.tgz#41059608ffc154757b715d9989d199ffbf372e35"
- dependencies:
- code-point-at "^1.0.0"
- is-fullwidth-code-point "^1.0.0"
- mute-stream "0.0.5"
-
-recast@^0.12.5:
- version "0.12.9"
- resolved "https://registry.yarnpkg.com/recast/-/recast-0.12.9.tgz#e8e52bdb9691af462ccbd7c15d5a5113647a15f1"
- dependencies:
- ast-types "0.10.1"
- core-js "^2.4.1"
- esprima "~4.0.0"
- private "~0.1.5"
- source-map "~0.6.1"
-
-recast@^0.14.1:
- version "0.14.7"
- resolved "https://registry.yarnpkg.com/recast/-/recast-0.14.7.tgz#4f1497c2b5826d42a66e8e3c9d80c512983ff61d"
- dependencies:
- ast-types "0.11.3"
- esprima "~4.0.0"
- private "~0.1.5"
- source-map "~0.6.1"
-
-rechoir@^0.6.2:
- version "0.6.2"
- resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384"
- dependencies:
- resolve "^1.1.6"
-
redent@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde"
@@ -7426,14 +6665,6 @@ repeating@^2.0.0:
dependencies:
is-finite "^1.0.0"
-replace-ext@0.0.1:
- version "0.0.1"
- resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-0.0.1.tgz#29bbd92078a739f0bcce2b4ee41e837953522924"
-
-replace-ext@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb"
-
request@2.75.x:
version "2.75.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.75.0.tgz#d2b8268a286da13eaa5d01adf5d18cc90f657d93"
@@ -7460,33 +6691,6 @@ request@2.75.x:
tough-cookie "~2.3.0"
tunnel-agent "~0.4.1"
-request@2.81.0:
- version "2.81.0"
- resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0"
- dependencies:
- aws-sign2 "~0.6.0"
- aws4 "^1.2.1"
- caseless "~0.12.0"
- combined-stream "~1.0.5"
- extend "~3.0.0"
- forever-agent "~0.6.1"
- form-data "~2.1.1"
- har-validator "~4.2.1"
- hawk "~3.1.3"
- http-signature "~1.1.0"
- is-typedarray "~1.0.0"
- isstream "~0.1.2"
- json-stringify-safe "~5.0.1"
- mime-types "~2.1.7"
- oauth-sign "~0.8.1"
- performance-now "^0.2.0"
- qs "~6.4.0"
- safe-buffer "^5.0.1"
- stringstream "~0.0.4"
- tough-cookie "~2.3.0"
- tunnel-agent "^0.6.0"
- uuid "^3.0.0"
-
request@^2.0.0, request@^2.74.0:
version "2.83.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356"
@@ -7523,10 +6727,6 @@ requestretry@^1.2.2:
request "^2.74.0"
when "^3.7.7"
-require-all@^2.2.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/require-all/-/require-all-2.2.0.tgz#b4420c233ac0282d0ff49b277fb880a8b5de0894"
-
require-directory@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
@@ -7535,7 +6735,7 @@ require-main-filename@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
-require-uncached@^1.0.2:
+require-uncached@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3"
dependencies:
@@ -7552,13 +6752,6 @@ resolve-cwd@^2.0.0:
dependencies:
resolve-from "^3.0.0"
-resolve-dir@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43"
- dependencies:
- expand-tilde "^2.0.0"
- global-modules "^1.0.0"
-
resolve-from@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226"
@@ -7575,9 +6768,9 @@ 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.5.0"
- resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.5.0.tgz#1f09acce796c9a762579f31b2c1cc4c3cddf9f36"
+resolve@^1.4.0, resolve@^1.5.0, resolve@^1.6.0:
+ version "1.7.1"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.7.1.tgz#aadd656374fd298aee895bc026b8297418677fd3"
dependencies:
path-parse "^1.0.5"
@@ -7587,13 +6780,6 @@ responselike@1.0.2:
dependencies:
lowercase-keys "^1.0.0"
-restore-cursor@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541"
- dependencies:
- exit-hook "^1.0.0"
- onetime "^1.0.0"
-
restore-cursor@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf"
@@ -7611,22 +6797,12 @@ right-align@^0.1.1:
dependencies:
align-text "^0.1.1"
-rimraf@2, rimraf@^2.5.1, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1, rimraf@^2.6.2:
+rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1, rimraf@^2.6.2:
version "2.6.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36"
dependencies:
glob "^7.0.5"
-rimraf@^2.2.8:
- version "2.6.1"
- resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d"
- dependencies:
- glob "^7.0.5"
-
-rimraf@~2.2.6:
- version "2.2.8"
- resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.2.8.tgz#e439be2aaee327321952730f99a8929e4fc50582"
-
ripemd160@^2.0.0, ripemd160@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.1.tgz#0f4584295c53a3628af7e6d79aca21ce57d1c6e7"
@@ -7634,13 +6810,7 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
hash-base "^2.0.0"
inherits "^2.0.1"
-run-async@^0.1.0:
- version "0.1.0"
- resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389"
- dependencies:
- once "^1.3.0"
-
-run-async@^2.0.0, run-async@^2.2.0:
+run-async@^2.2.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0"
dependencies:
@@ -7662,11 +6832,7 @@ rx-lite@*, rx-lite@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444"
-rx-lite@^3.1.2:
- version "3.1.2"
- resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102"
-
-rxjs@^5.4.2, rxjs@^5.5.2:
+rxjs@^5.5.2:
version "5.5.10"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.10.tgz#fde02d7a614f6c8683d0d1957827f492e09db045"
dependencies:
@@ -7676,12 +6842,20 @@ safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, s
version "5.1.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
+safe-buffer@^5.1.2:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
+
safe-regex@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e"
dependencies:
ret "~0.1.10"
+"safer-buffer@>= 2.1.2 < 3":
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
+
sanitize-html@^1.16.1:
version "1.16.3"
resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-1.16.3.tgz#96c1b44a36ff7312e1c22a14b05274370ac8bd56"
@@ -7694,6 +6868,10 @@ sanitize-html@^1.16.1:
srcset "^1.0.0"
xtend "^4.0.0"
+sax@^1.2.4:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
+
sax@~1.2.1:
version "1.2.2"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.2.tgz#fd8631a23bc7826bef5d871bdb87378c95647828"
@@ -7705,10 +6883,6 @@ schema-utils@^0.4.0, schema-utils@^0.4.2, schema-utils@^0.4.3, schema-utils@^0.4
ajv "^6.1.0"
ajv-keywords "^3.1.0"
-scoped-regex@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/scoped-regex/-/scoped-regex-1.0.0.tgz#a346bb1acd4207ae70bd7c0c7ca9e566b6baddb8"
-
select-hose@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
@@ -7733,11 +6907,7 @@ semver-diff@^2.0.0:
dependencies:
semver "^5.0.3"
-"semver@2 || 3 || 4 || 5", semver@^5.0.3:
- version "5.3.0"
- resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
-
-semver@^5.1.0, semver@^5.3.0, semver@^5.5.0:
+"semver@2 || 3 || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.5.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab"
@@ -7856,22 +7026,6 @@ shebang-regex@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
-shelljs@^0.7.5:
- version "0.7.8"
- resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.8.tgz#decbcf874b0d1e5fb72e14b164a9683048e9acb3"
- dependencies:
- glob "^7.0.0"
- interpret "^1.0.0"
- rechoir "^0.6.2"
-
-shelljs@^0.8.0:
- version "0.8.1"
- resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.1.tgz#729e038c413a2254c4078b95ed46e0397154a9f1"
- dependencies:
- glob "^7.0.0"
- interpret "^1.0.0"
- rechoir "^0.6.2"
-
signal-exit@^3.0.0, signal-exit@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
@@ -7886,13 +7040,11 @@ slash@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
-slice-ansi@0.0.4:
- version "0.0.4"
- resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35"
-
-slide@^1.1.5:
- version "1.1.6"
- resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707"
+slice-ansi@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-1.0.0.tgz#044f1a49d8842ff307aad6b505ed178bd950134d"
+ dependencies:
+ is-fullwidth-code-point "^2.0.0"
smart-buffer@^1.0.13, smart-buffer@^1.0.4:
version "1.1.15"
@@ -8037,6 +7189,10 @@ sort-keys@^2.0.0:
dependencies:
is-plain-obj "^1.0.0"
+sortablejs@^1.7.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.7.0.tgz#80a2b2370abd568e1cec8c271131ef30a904fa28"
+
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"
@@ -8071,14 +7227,14 @@ source-map@^0.4.4:
dependencies:
amdefine ">=0.0.4"
+source-map@^0.5.0, source-map@^0.5.7, source-map@~0.5.6:
+ version "0.5.7"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
+
source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1:
version "0.5.6"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
-source-map@^0.5.7, source-map@~0.5.3, source-map@~0.5.6:
- version "0.5.7"
- resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
-
source-map@^0.6.1, source-map@~0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
@@ -8188,6 +7344,10 @@ statuses@~1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e"
+stickyfilljs@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/stickyfilljs/-/stickyfilljs-2.0.5.tgz#d229e372d2199ddf5d283bbe34ac1f7d2529c2fc"
+
stream-browserify@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db"
@@ -8208,13 +7368,13 @@ stream-each@^1.1.0:
end-of-stream "^1.1.0"
stream-shift "^1.0.0"
-stream-http@^2.3.1:
- version "2.8.0"
- resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.0.tgz#fd86546dac9b1c91aff8fc5d287b98fafb41bc10"
+stream-http@^2.7.2:
+ version "2.8.2"
+ resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.2.tgz#4126e8c6b107004465918aa2fc35549e77402c87"
dependencies:
builtin-status-codes "^3.0.0"
inherits "^2.0.1"
- readable-stream "^2.3.3"
+ readable-stream "^2.3.6"
to-arraybuffer "^1.0.0"
xtend "^4.0.0"
@@ -8222,12 +7382,6 @@ stream-shift@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952"
-stream-to-observable@^0.2.0:
- version "0.2.0"
- resolved "https://registry.yarnpkg.com/stream-to-observable/-/stream-to-observable-0.2.0.tgz#59d6ea393d87c2c0ddac10aa0d561bc6ba6f0e10"
- dependencies:
- any-observable "^0.2.0"
-
streamroller@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-0.7.0.tgz#a1d1b7cf83d39afb0d63049a5acbf93493bdf64b"
@@ -8241,10 +7395,6 @@ strict-uri-encode@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
-string-template@~0.2.1:
- version "0.2.1"
- resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add"
-
string-width@^1.0.1, string-width@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
@@ -8260,7 +7410,13 @@ string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^4.0.0"
-string_decoder@^0.10.25, string_decoder@~0.10.x:
+string_decoder@^1.0.0, string_decoder@~1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
+ dependencies:
+ safe-buffer "~5.1.0"
+
+string_decoder@~0.10.x:
version "0.10.31"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
@@ -8286,17 +7442,6 @@ strip-ansi@^4.0.0:
dependencies:
ansi-regex "^3.0.0"
-strip-ansi@~0.1.0:
- version "0.1.1"
- resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-0.1.1.tgz#39e8a98d044d150660abe4a6808acf70bb7bc991"
-
-strip-bom-stream@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/strip-bom-stream/-/strip-bom-stream-2.0.0.tgz#f87db5ef2613f6968aa545abfe1ec728b6a829ca"
- dependencies:
- first-chunk-stream "^2.0.0"
- strip-bom "^2.0.0"
-
strip-bom@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e"
@@ -8338,19 +7483,7 @@ supports-color@^3.1.0, supports-color@^3.1.2, supports-color@^3.2.3:
dependencies:
has-flag "^1.0.0"
-supports-color@^5.1.0:
- version "5.1.0"
- resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.1.0.tgz#058a021d1b619f7ddf3980d712ea3590ce7de3d5"
- dependencies:
- has-flag "^2.0.0"
-
-supports-color@^5.2.0:
- version "5.2.0"
- resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.2.0.tgz#b0d5333b1184dd3666cbe5aa0b45c5ac7ac17a4a"
- dependencies:
- has-flag "^3.0.0"
-
-supports-color@^5.3.0, supports-color@^5.4.0:
+supports-color@^5.1.0, supports-color@^5.2.0, supports-color@^5.3.0, supports-color@^5.4.0:
version "5.4.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54"
dependencies:
@@ -8376,20 +7509,16 @@ symbol-observable@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.1.tgz#8340fc4702c3122df5d22288f88283f513d3fdd4"
-symbol-observable@^0.2.2:
- version "0.2.4"
- resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-0.2.4.tgz#95a83db26186d6af7e7a18dbd9760a2f86d08f40"
-
-table@^3.7.8:
- version "3.8.3"
- resolved "https://registry.yarnpkg.com/table/-/table-3.8.3.tgz#2bbc542f0fda9861a755d3947fefd8b3f513855f"
+table@^4.0.1:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/table/-/table-4.0.2.tgz#a33447375391e766ad34d3486e6e2aedc84d2e36"
dependencies:
- ajv "^4.7.0"
- ajv-keywords "^1.0.0"
- chalk "^1.1.1"
- lodash "^4.0.0"
- slice-ansi "0.0.4"
- string-width "^2.0.0"
+ ajv "^5.2.3"
+ ajv-keywords "^2.1.0"
+ chalk "^2.1.0"
+ lodash "^4.17.4"
+ slice-ansi "1.0.0"
+ string-width "^2.1.1"
tapable@^0.1.8:
version "0.1.10"
@@ -8399,33 +7528,17 @@ tapable@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.0.0.tgz#cbb639d9002eed9c6b5975eb20598d7936f1f9f2"
-tar-pack@^3.4.0:
- version "3.4.1"
- resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.1.tgz#e1dbc03a9b9d3ba07e896ad027317eb679a10a1f"
- dependencies:
- debug "^2.2.0"
- fstream "^1.0.10"
- fstream-ignore "^1.0.5"
- once "^1.3.3"
- readable-stream "^2.1.4"
- rimraf "^2.5.1"
- tar "^2.2.1"
- uid-number "^0.0.6"
-
-tar@^2.2.1:
- version "2.2.1"
- resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1"
+tar@^4:
+ version "4.4.4"
+ resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.4.tgz#ec8409fae9f665a4355cc3b4087d0820232bb8cd"
dependencies:
- block-stream "*"
- fstream "^1.0.2"
- inherits "2"
-
-temp@^0.8.1:
- version "0.8.3"
- resolved "https://registry.yarnpkg.com/temp/-/temp-0.8.3.tgz#e0c6bc4d26b903124410e4fed81103014dfc1f59"
- dependencies:
- os-tmpdir "^1.0.0"
- rimraf "~2.2.6"
+ chownr "^1.0.1"
+ fs-minipass "^1.2.5"
+ minipass "^2.3.3"
+ minizlib "^1.1.0"
+ mkdirp "^0.5.0"
+ safe-buffer "^5.1.2"
+ yallist "^3.0.2"
term-size@^1.2.0:
version "1.2.0"
@@ -8443,14 +7556,10 @@ test-exclude@^4.2.1:
read-pkg-up "^1.0.1"
require-main-filename "^1.0.1"
-text-table@^0.2.0, text-table@~0.2.0:
+text-table@~0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
-textextensions@2:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/textextensions/-/textextensions-2.2.0.tgz#38ac676151285b658654581987a0ce1a4490d286"
-
three-orbit-controls@^82.1.0:
version "82.1.0"
resolved "https://registry.yarnpkg.com/three-orbit-controls/-/three-orbit-controls-82.1.0.tgz#11a7f33d0a20ecec98f098b37780f6537374fab4"
@@ -8463,7 +7572,7 @@ three@^0.84.0:
version "0.84.0"
resolved "https://registry.yarnpkg.com/three/-/three-0.84.0.tgz#95be85a55a0fa002aa625ed559130957dcffd918"
-through2@^2.0.0, through2@^2.0.1:
+through2@^2.0.0:
version "2.0.3"
resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be"
dependencies:
@@ -8492,15 +7601,9 @@ timed-out@^4.0.0, timed-out@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f"
-timers-browserify@^1.4.2:
- version "1.4.2"
- resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-1.4.2.tgz#c9c58b575be8407375cb5e2462dacee74359f41d"
- dependencies:
- process "~0.11.0"
-
-timers-browserify@^2.0.2:
- version "2.0.4"
- resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.4.tgz#96ca53f4b794a5e7c0e1bd7cc88a372298fa01e6"
+timers-browserify@^2.0.4:
+ version "2.0.10"
+ resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.10.tgz#1d28e3d2aadf1d5a5996c4e9f95601cd053480ae"
dependencies:
setimmediate "^1.0.4"
@@ -8547,15 +7650,7 @@ to-regex-range@^2.1.0:
is-number "^3.0.0"
repeat-string "^1.6.1"
-to-regex@^3.0.1:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.1.tgz#15358bee4a2c83bd76377ba1dc049d0f18837aae"
- dependencies:
- define-property "^0.2.5"
- extend-shallow "^2.0.1"
- regex-not "^1.0.0"
-
-to-regex@^3.0.2:
+to-regex@^3.0.1, to-regex@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce"
dependencies:
@@ -8668,18 +7763,10 @@ uglifyjs-webpack-plugin@^1.2.4:
webpack-sources "^1.1.0"
worker-farm "^1.5.2"
-uid-number@^0.0.6:
- version "0.0.6"
- resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81"
-
ultron@~1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c"
-unc-path-regex@^0.1.0:
- version "0.1.2"
- resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa"
-
undefsafe@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.2.tgz#225f6b9e0337663e0d8e7cfd686fc2836ccace76"
@@ -8690,10 +7777,6 @@ underscore@^1.9.0:
version "1.9.0"
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.9.0.tgz#31dbb314cfcc88f169cd3692d9149d81a00a73e4"
-underscore@~1.6.0:
- version "1.6.0"
- resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.6.0.tgz#8b38b10cacdef63337b8b24e4ff86d45aea529a8"
-
underscore@~1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.7.0.tgz#6bbaf0877500d36be34ecaa584e0db9fef035209"
@@ -8750,10 +7833,6 @@ unset-value@^1.0.0:
has-value "^0.3.1"
isobject "^3.0.0"
-untildify@^3.0.2:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/untildify/-/untildify-3.0.2.tgz#7f1f302055b3fea0f3e81dc78eb36766cb65e3f1"
-
unzip-response@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97"
@@ -8841,12 +7920,6 @@ use@^2.0.0:
isobject "^3.0.0"
lazy-cache "^2.0.2"
-user-home@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/user-home/-/user-home-2.0.0.tgz#9c70bfd8169bc1dcbf48604e0f04b8b49cde9e9f"
- dependencies:
- os-homedir "^1.0.0"
-
useragent@2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/useragent/-/useragent-2.2.1.tgz#cf593ef4f2d175875e8bb658ea92e18a4fd06d8e"
@@ -8868,7 +7941,7 @@ utils-merge@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
-uuid@^3.0.0, uuid@^3.0.1, uuid@^3.1.0:
+uuid@^3.0.1, uuid@^3.1.0:
version "3.2.1"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14"
@@ -8876,9 +7949,9 @@ uws@~9.14.0:
version "9.14.0"
resolved "https://registry.yarnpkg.com/uws/-/uws-9.14.0.tgz#fac8386befc33a7a3705cbd58dc47b430ca4dd95"
-v8-compile-cache@^1.1.2:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-1.1.2.tgz#8d32e4f16974654657e676e0e467a348e89b0dc4"
+v8-compile-cache@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.0.tgz#526492e35fc616864284700b7043e01baee09f0a"
validate-npm-package-license@^3.0.1:
version "3.0.1"
@@ -8907,36 +7980,6 @@ verror@1.10.0:
core-util-is "1.0.2"
extsprintf "^1.2.0"
-vinyl-file@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/vinyl-file/-/vinyl-file-2.0.0.tgz#a7ebf5ffbefda1b7d18d140fcb07b223efb6751a"
- dependencies:
- graceful-fs "^4.1.2"
- pify "^2.3.0"
- pinkie-promise "^2.0.0"
- strip-bom "^2.0.0"
- strip-bom-stream "^2.0.0"
- vinyl "^1.1.0"
-
-vinyl@^1.1.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-1.2.0.tgz#5c88036cf565e5df05558bfc911f8656df218884"
- dependencies:
- clone "^1.0.0"
- clone-stats "^0.0.1"
- replace-ext "0.0.1"
-
-vinyl@^2.0.1:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.1.0.tgz#021f9c2cf951d6b939943c89eb5ee5add4fd924c"
- dependencies:
- clone "^2.1.1"
- clone-buffer "^1.0.0"
- clone-stats "^1.0.0"
- cloneable-readable "^1.0.0"
- remove-trailing-separator "^1.0.1"
- replace-ext "^1.0.0"
-
visibilityjs@^1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/visibilityjs/-/visibilityjs-1.2.4.tgz#bff8663da62c8c10ad4ee5ae6a1ae6fac4259d63"
@@ -8951,9 +7994,9 @@ void-elements@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
-vue-eslint-parser@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-2.0.1.tgz#30135771c4fad00fdbac4542a2d59f3b1d776834"
+vue-eslint-parser@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-2.0.3.tgz#c268c96c6d94cfe3d938a5f7593959b0ca3360d1"
dependencies:
debug "^3.1.0"
eslint-scope "^3.7.1"
@@ -9030,12 +8073,6 @@ wbuf@^1.1.0, wbuf@^1.7.2:
dependencies:
minimalistic-assert "^1.0.0"
-webpack-addons@^1.1.5:
- version "1.1.5"
- resolved "https://registry.yarnpkg.com/webpack-addons/-/webpack-addons-1.1.5.tgz#2b178dfe873fb6e75e40a819fa5c26e4a9bc837a"
- dependencies:
- jscodeshift "^0.4.0"
-
webpack-bundle-analyzer@^2.11.1:
version "2.11.1"
resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-2.11.1.tgz#b9fbfb6a32c0a8c1c3237223e90890796b950ab9"
@@ -9053,36 +8090,21 @@ webpack-bundle-analyzer@^2.11.1:
opener "^1.4.3"
ws "^4.0.0"
-webpack-cli@^2.1.2:
- version "2.1.2"
- resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-2.1.2.tgz#9c9a4b90584f7b8acaf591238ef0667e04c817f6"
+webpack-cli@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.0.2.tgz#e48c5662aff8ed5aac3db5f82f51d7f32e50459e"
dependencies:
- chalk "^2.3.2"
+ chalk "^2.4.1"
cross-spawn "^6.0.5"
- diff "^3.5.0"
enhanced-resolve "^4.0.0"
- envinfo "^4.4.2"
- glob-all "^3.1.0"
- global-modules "^1.0.0"
- got "^8.2.0"
+ global-modules-path "^2.1.0"
import-local "^1.0.0"
- inquirer "^5.1.0"
- interpret "^1.0.4"
- jscodeshift "^0.5.0"
- listr "^0.13.0"
+ inquirer "^5.2.0"
+ interpret "^1.1.0"
loader-utils "^1.1.0"
- lodash "^4.17.5"
- log-symbols "^2.2.0"
- mkdirp "^0.5.1"
- p-each-series "^1.0.0"
- p-lazy "^1.0.0"
- prettier "^1.5.3"
- supports-color "^5.3.0"
- v8-compile-cache "^1.1.2"
- webpack-addons "^1.1.5"
+ supports-color "^5.4.0"
+ v8-compile-cache "^2.0.0"
yargs "^11.1.0"
- yeoman-environment "^2.0.0"
- yeoman-generator "^2.0.4"
webpack-dev-middleware@3.1.3:
version "3.1.3"
@@ -9141,7 +8163,7 @@ webpack-dev-server@^3.1.4:
webpack-log "^1.1.2"
yargs "11.0.0"
-webpack-log@^1.0.1:
+webpack-log@^1.0.1, webpack-log@^1.1.2:
version "1.2.0"
resolved "https://registry.yarnpkg.com/webpack-log/-/webpack-log-1.2.0.tgz#a4b34cda6b22b518dbb0ab32e567962d5c72a43d"
dependencies:
@@ -9150,23 +8172,7 @@ webpack-log@^1.0.1:
loglevelnext "^1.0.1"
uuid "^3.1.0"
-webpack-log@^1.1.2:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/webpack-log/-/webpack-log-1.1.2.tgz#cdc76016537eed24708dc6aa3d1e52189efee107"
- dependencies:
- chalk "^2.1.0"
- log-symbols "^2.1.0"
- loglevelnext "^1.0.1"
- uuid "^3.1.0"
-
-webpack-sources@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.0.1.tgz#c7356436a4d13123be2e2426a05d1dad9cbe65cf"
- dependencies:
- source-list-map "^2.0.0"
- source-map "~0.5.3"
-
-webpack-sources@^1.1.0:
+webpack-sources@^1.0.1, webpack-sources@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.1.0.tgz#a101ebae59d6507354d71d8013950a3a8b7a5a54"
dependencies:
@@ -9177,10 +8183,15 @@ webpack-stats-plugin@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/webpack-stats-plugin/-/webpack-stats-plugin-0.2.1.tgz#1f5bac13fc25d62cbb5fd0ff646757dc802b8595"
-webpack@^4.7.0:
- version "4.7.0"
- resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.7.0.tgz#a04f68dab86d5545fd0277d07ffc44e4078154c9"
+webpack@^4.11.1:
+ version "4.11.1"
+ resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.11.1.tgz#1aa0b936f7ae93a52cf38d2ad0d0f46dcf3c2723"
dependencies:
+ "@webassemblyjs/ast" "1.5.10"
+ "@webassemblyjs/helper-module-context" "1.5.10"
+ "@webassemblyjs/wasm-edit" "1.5.10"
+ "@webassemblyjs/wasm-opt" "1.5.10"
+ "@webassemblyjs/wasm-parser" "1.5.10"
acorn "^5.0.0"
acorn-dynamic-import "^3.0.0"
ajv "^6.1.0"
@@ -9188,6 +8199,7 @@ webpack@^4.7.0:
chrome-trace-event "^0.1.1"
enhanced-resolve "^4.0.0"
eslint-scope "^3.7.1"
+ json-parse-better-errors "^1.0.2"
loader-runner "^2.3.0"
loader-utils "^1.1.0"
memory-fs "~0.4.1"
@@ -9223,7 +8235,7 @@ which-module@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
-which@^1.1.1, which@^1.2.1, which@^1.2.14, which@^1.2.9:
+which@^1.1.1, which@^1.2.1, which@^1.2.9:
version "1.3.0"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a"
dependencies:
@@ -9264,9 +8276,9 @@ worker-farm@^1.5.2:
errno "^0.1.4"
xtend "^4.0.1"
-worker-loader@^1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/worker-loader/-/worker-loader-1.1.1.tgz#920d74ddac6816fc635392653ed8b4af1929fd92"
+worker-loader@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/worker-loader/-/worker-loader-2.0.0.tgz#45fda3ef76aca815771a89107399ee4119b430ac"
dependencies:
loader-utils "^1.0.0"
schema-utils "^0.4.0"
@@ -9282,14 +8294,6 @@ wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
-write-file-atomic@^1.2.0:
- version "1.3.4"
- resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-1.3.4.tgz#f807a4f0b1d9e913ae7a48112e6cc3af1991b45f"
- dependencies:
- graceful-fs "^4.1.11"
- imurmurhash "^0.1.4"
- slide "^1.1.5"
-
write-file-atomic@^2.0.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.3.0.tgz#1ff61575c2e2a4e8e510d6fa4e243cce183999ab"
@@ -9332,7 +8336,7 @@ xregexp@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-2.0.0.tgz#52a63e56ca0b84a7f3a5f3d61872f126ad7a5943"
-xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.0, xtend@~4.0.1:
+xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
@@ -9348,6 +8352,10 @@ yallist@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
+yallist@^3.0.0, yallist@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.2.tgz#8452b4bb7e83c7c188d8041c1a837c773d6d8bb9"
+
yargs-parser@^9.0.2:
version "9.0.2"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-9.0.2.tgz#9ccf6a43460fe4ed40a9bb68f48d43b8a68cc077"
@@ -9388,12 +8396,6 @@ yargs@^11.1.0:
y18n "^3.2.1"
yargs-parser "^9.0.2"
-yargs@~1.2.6:
- version "1.2.6"
- resolved "https://registry.yarnpkg.com/yargs/-/yargs-1.2.6.tgz#9c7b4a82fd5d595b2bf17ab6dcc43135432fe34b"
- dependencies:
- minimist "^0.1.0"
-
yargs@~3.10.0:
version "3.10.0"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1"
@@ -9406,69 +8408,3 @@ yargs@~3.10.0:
yeast@0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419"
-
-yeoman-environment@^2.0.0:
- version "2.0.5"
- resolved "https://registry.yarnpkg.com/yeoman-environment/-/yeoman-environment-2.0.5.tgz#84f22bafa84088971fe99ea85f654a3a3dd2b693"
- dependencies:
- chalk "^2.1.0"
- debug "^3.1.0"
- diff "^3.3.1"
- escape-string-regexp "^1.0.2"
- globby "^6.1.0"
- grouped-queue "^0.3.3"
- inquirer "^3.3.0"
- is-scoped "^1.0.0"
- lodash "^4.17.4"
- log-symbols "^2.1.0"
- mem-fs "^1.1.0"
- text-table "^0.2.0"
- untildify "^3.0.2"
-
-yeoman-environment@^2.0.5:
- version "2.0.6"
- resolved "https://registry.yarnpkg.com/yeoman-environment/-/yeoman-environment-2.0.6.tgz#ae1b21d826b363f3d637f88a7fc9ea7414cb5377"
- dependencies:
- chalk "^2.1.0"
- debug "^3.1.0"
- diff "^3.3.1"
- escape-string-regexp "^1.0.2"
- globby "^6.1.0"
- grouped-queue "^0.3.3"
- inquirer "^3.3.0"
- is-scoped "^1.0.0"
- lodash "^4.17.4"
- log-symbols "^2.1.0"
- mem-fs "^1.1.0"
- text-table "^0.2.0"
- untildify "^3.0.2"
-
-yeoman-generator@^2.0.4:
- version "2.0.5"
- resolved "https://registry.yarnpkg.com/yeoman-generator/-/yeoman-generator-2.0.5.tgz#57b0b3474701293cc9ec965288f3400b00887c81"
- dependencies:
- async "^2.6.0"
- chalk "^2.3.0"
- cli-table "^0.3.1"
- cross-spawn "^6.0.5"
- dargs "^5.1.0"
- dateformat "^3.0.3"
- debug "^3.1.0"
- detect-conflict "^1.0.0"
- error "^7.0.2"
- find-up "^2.1.0"
- github-username "^4.0.0"
- istextorbinary "^2.2.1"
- lodash "^4.17.10"
- make-dir "^1.1.0"
- mem-fs-editor "^4.0.0"
- minimist "^1.2.0"
- pretty-bytes "^4.0.2"
- read-chunk "^2.1.0"
- read-pkg-up "^3.0.0"
- rimraf "^2.6.2"
- run-async "^2.0.0"
- shelljs "^0.8.0"
- text-table "^0.2.0"
- through2 "^2.0.0"
- yeoman-environment "^2.0.5"